113 lines
4.1 KiB
Python
113 lines
4.1 KiB
Python
|
|
"""Order confirmation PDF generation (reportlab)."""
|
||
|
|
from __future__ import annotations
|
||
|
|
|
||
|
|
from datetime import datetime
|
||
|
|
from io import BytesIO
|
||
|
|
|
||
|
|
from app.models.client_access import ClientAccount
|
||
|
|
from app.models.ordering import Order
|
||
|
|
|
||
|
|
|
||
|
|
class OrderPdfUnavailableError(RuntimeError):
|
||
|
|
pass
|
||
|
|
|
||
|
|
|
||
|
|
def _fmt_money(value: float | None) -> str:
|
||
|
|
return "Quote" if value is None else f"${value:,.2f}"
|
||
|
|
|
||
|
|
|
||
|
|
def build_order_confirmation_pdf(order: Order, customer: ClientAccount | None) -> bytes:
|
||
|
|
try:
|
||
|
|
from reportlab.lib import colors
|
||
|
|
from reportlab.lib.pagesizes import A4
|
||
|
|
from reportlab.pdfgen import canvas
|
||
|
|
except ModuleNotFoundError as exc: # pragma: no cover
|
||
|
|
raise OrderPdfUnavailableError(
|
||
|
|
"PDF generation is unavailable because 'reportlab' is not installed."
|
||
|
|
) from exc
|
||
|
|
|
||
|
|
buffer = BytesIO()
|
||
|
|
page_width, page_height = A4
|
||
|
|
pdf = canvas.Canvas(buffer, pagesize=A4)
|
||
|
|
margin = 40
|
||
|
|
y = page_height - margin
|
||
|
|
|
||
|
|
pdf.setFont("Helvetica-Bold", 18)
|
||
|
|
pdf.drawString(margin, y, "Order Confirmation")
|
||
|
|
y -= 24
|
||
|
|
|
||
|
|
pdf.setFont("Helvetica", 10)
|
||
|
|
order_ref = order.order_number or f"#{order.id}"
|
||
|
|
pdf.drawString(margin, y, f"Order: {order_ref}")
|
||
|
|
pdf.drawRightString(page_width - margin, y, datetime.utcnow().strftime("%d %b %Y %H:%M UTC"))
|
||
|
|
y -= 14
|
||
|
|
pdf.drawString(margin, y, f"Customer: {customer.name if customer else order.client_account_id}")
|
||
|
|
y -= 14
|
||
|
|
pdf.drawString(margin, y, f"Status: {order.status.replace('_', ' ').title()}")
|
||
|
|
y -= 14
|
||
|
|
if order.purchase_order_number:
|
||
|
|
pdf.drawString(margin, y, f"PO Number: {order.purchase_order_number}")
|
||
|
|
y -= 14
|
||
|
|
pdf.drawString(margin, y, f"Fulfilment: {order.fulfilment_method.title()}")
|
||
|
|
y -= 14
|
||
|
|
if order.requested_delivery_date:
|
||
|
|
pdf.drawString(margin, y, f"Requested date: {order.requested_delivery_date.strftime('%d %b %Y')}")
|
||
|
|
y -= 14
|
||
|
|
y -= 6
|
||
|
|
|
||
|
|
# Table header
|
||
|
|
pdf.setFont("Helvetica-Bold", 9)
|
||
|
|
pdf.setFillColor(colors.HexColor("#22362d"))
|
||
|
|
pdf.drawString(margin, y, "Product")
|
||
|
|
pdf.drawString(margin + 220, y, "SKU")
|
||
|
|
pdf.drawRightString(margin + 330, y, "Qty")
|
||
|
|
pdf.drawRightString(margin + 420, y, "Unit (ex GST)")
|
||
|
|
pdf.drawRightString(page_width - margin, y, "Line total")
|
||
|
|
y -= 6
|
||
|
|
pdf.setStrokeColor(colors.HexColor("#cccccc"))
|
||
|
|
pdf.line(margin, y, page_width - margin, y)
|
||
|
|
y -= 14
|
||
|
|
|
||
|
|
pdf.setFont("Helvetica", 9)
|
||
|
|
pdf.setFillColor(colors.black)
|
||
|
|
for line in order.lines:
|
||
|
|
if y < margin + 60:
|
||
|
|
pdf.showPage()
|
||
|
|
y = page_height - margin
|
||
|
|
pdf.setFont("Helvetica", 9)
|
||
|
|
unit = line.admin_override_price if line.admin_override_price is not None else line.unit_price
|
||
|
|
line_total = None if unit is None else round(unit * line.quantity, 2)
|
||
|
|
pdf.drawString(margin, y, (line.product_name or "")[:38])
|
||
|
|
pdf.drawString(margin + 220, y, (line.product_sku or "")[:16])
|
||
|
|
pdf.drawRightString(margin + 330, y, f"{line.quantity:g}")
|
||
|
|
pdf.drawRightString(margin + 420, y, _fmt_money(unit))
|
||
|
|
pdf.drawRightString(page_width - margin, y, _fmt_money(line_total))
|
||
|
|
y -= 14
|
||
|
|
|
||
|
|
y -= 4
|
||
|
|
pdf.line(margin, y, page_width - margin, y)
|
||
|
|
y -= 16
|
||
|
|
pdf.setFont("Helvetica-Bold", 10)
|
||
|
|
pdf.drawRightString(margin + 420, y, "Subtotal (ex GST)")
|
||
|
|
pdf.drawRightString(page_width - margin, y, _fmt_money(order.subtotal_ex_gst))
|
||
|
|
y -= 20
|
||
|
|
|
||
|
|
if order.requires_quote:
|
||
|
|
pdf.setFont("Helvetica-Oblique", 9)
|
||
|
|
pdf.setFillColor(colors.HexColor("#8a5a00"))
|
||
|
|
pdf.drawString(margin, y, "This order contains quote-only items. Final pricing will be confirmed by our team.")
|
||
|
|
y -= 14
|
||
|
|
|
||
|
|
if order.delivery_notes:
|
||
|
|
pdf.setFont("Helvetica", 9)
|
||
|
|
pdf.setFillColor(colors.black)
|
||
|
|
pdf.drawString(margin, y, f"Notes: {order.delivery_notes[:90]}")
|
||
|
|
|
||
|
|
pdf.setFont("Helvetica-Oblique", 8)
|
||
|
|
pdf.setFillColor(colors.HexColor("#888888"))
|
||
|
|
pdf.drawString(margin, margin - 12, "Prices exclude GST. This is an order confirmation, not a tax invoice.")
|
||
|
|
|
||
|
|
pdf.showPage()
|
||
|
|
pdf.save()
|
||
|
|
return buffer.getvalue()
|