fix error in gitignore
This commit is contained in:
@@ -0,0 +1,194 @@
|
||||
"""Final report generation.
|
||||
|
||||
Reports are built from the workflow's active report definition and the order's
|
||||
data (customer, weights, selected steps, price breakdown, execution timeline,
|
||||
measurements, payments and invoices). The schema is configurable — there is no
|
||||
hard-coded pistachio quality schema.
|
||||
"""
|
||||
from io import BytesIO
|
||||
|
||||
from reportlab.lib import colors
|
||||
from reportlab.lib.pagesizes import A4
|
||||
from reportlab.lib.styles import ParagraphStyle, getSampleStyleSheet
|
||||
from reportlab.lib.units import mm
|
||||
from reportlab.platypus import Paragraph, SimpleDocTemplate, Spacer, Table, TableStyle
|
||||
|
||||
from apps.reports.models import FinalReport, ReportDefinition
|
||||
|
||||
GREEN = colors.HexColor("#166534")
|
||||
DARK = colors.HexColor("#123B24")
|
||||
LINE = colors.HexColor("#E7E1D4")
|
||||
CREAM = colors.HexColor("#FAF6EE")
|
||||
|
||||
|
||||
class ReportService:
|
||||
def generate(self, order) -> FinalReport:
|
||||
definition = ReportDefinition.objects.filter(
|
||||
workflow=order.workflow_version.workflow, active=True
|
||||
).first()
|
||||
data = self._build_data(order, definition)
|
||||
report, _ = FinalReport.objects.update_or_create(order=order, defaults={"data": data})
|
||||
return report
|
||||
|
||||
def _build_data(self, order, definition) -> dict:
|
||||
measurements = {}
|
||||
timeline = []
|
||||
for execution in order.step_executions.select_related("step", "assigned_to").order_by("created_at"):
|
||||
for key, value in (execution.measurements or {}).items():
|
||||
measurements.setdefault(key, value)
|
||||
timeline.append({
|
||||
"step": execution.step.name,
|
||||
"state": execution.state,
|
||||
"attempt": execution.attempt,
|
||||
"operator": execution.assigned_to.username if execution.assigned_to else "",
|
||||
"started_at": execution.started_at.isoformat() if execution.started_at else "",
|
||||
"completed_at": execution.completed_at.isoformat() if execution.completed_at else "",
|
||||
})
|
||||
|
||||
fields = []
|
||||
if definition:
|
||||
for field in definition.fields.all():
|
||||
fields.append({
|
||||
"key": field.key,
|
||||
"label": field.label,
|
||||
"type": field.field_type,
|
||||
"value": measurements.get(field.key, ""),
|
||||
})
|
||||
|
||||
customer = order.customer
|
||||
customer_name = customer.company_name or str(customer.user)
|
||||
return {
|
||||
"order_number": order.order_number,
|
||||
"customer": {
|
||||
"name": customer_name,
|
||||
"code": customer.customer_code or "",
|
||||
"phone": customer.user.phone,
|
||||
"email": customer.user.email,
|
||||
},
|
||||
"customer_name": customer_name,
|
||||
"gross_weight_kg": str(order.gross_weight_kg or ""),
|
||||
"tare_weight_kg": str(order.tare_weight_kg or ""),
|
||||
"net_weight_kg": str(order.net_weight_kg or ""),
|
||||
"selected_steps": order.selected_step_keys or [],
|
||||
"price": order.price_snapshot or {},
|
||||
"status": order.status,
|
||||
"created_at": order.created_at.isoformat(),
|
||||
"confirmed_at": order.confirmed_at.isoformat() if order.confirmed_at else "",
|
||||
"timeline": timeline,
|
||||
"fields": fields,
|
||||
"measurements": measurements,
|
||||
"payments": [
|
||||
{"status": p.status, "amount": str(p.amount), "currency": p.currency}
|
||||
for p in order.payments.all()
|
||||
],
|
||||
"invoices": [
|
||||
{"number": i.number, "sync_state": i.sync_state, "amount": str(i.amount)}
|
||||
for i in order.invoices.all()
|
||||
],
|
||||
}
|
||||
|
||||
# ------------------------------------------------------------------ PDF
|
||||
def render_pdf(self, report) -> bytes:
|
||||
buf = BytesIO()
|
||||
doc = SimpleDocTemplate(
|
||||
buf, pagesize=A4,
|
||||
rightMargin=18 * mm, leftMargin=18 * mm, topMargin=18 * mm, bottomMargin=18 * mm,
|
||||
title=f"Cetana report {report.order.order_number}",
|
||||
)
|
||||
styles = getSampleStyleSheet()
|
||||
h2 = ParagraphStyle("H2", parent=styles["Heading2"], textColor=GREEN, spaceBefore=10, spaceAfter=6)
|
||||
title = ParagraphStyle("T", parent=styles["Title"], fontSize=20, textColor=DARK)
|
||||
|
||||
data = report.data or {}
|
||||
story = [Paragraph("Cetana — Processing Report", title), Spacer(1, 4 * mm)]
|
||||
|
||||
# Summary
|
||||
summary = [
|
||||
["Order", data.get("order_number", "")],
|
||||
["Customer", data.get("customer_name", "")],
|
||||
["Status", data.get("status", "")],
|
||||
["Net weight (kg)", data.get("net_weight_kg", "")],
|
||||
["Gross / Tare (kg)", f"{data.get('gross_weight_kg', '')} / {data.get('tare_weight_kg', '')}"],
|
||||
]
|
||||
story.append(self._table(summary, [50 * mm, 120 * mm]))
|
||||
story.append(Spacer(1, 4 * mm))
|
||||
|
||||
# Selected steps
|
||||
steps = data.get("selected_steps") or []
|
||||
if steps:
|
||||
story.append(Paragraph("Selected processing steps", h2))
|
||||
story.append(Paragraph(", ".join(steps), styles["Normal"]))
|
||||
story.append(Spacer(1, 3 * mm))
|
||||
|
||||
# Execution timeline
|
||||
timeline = data.get("timeline") or []
|
||||
if timeline:
|
||||
story.append(Paragraph("Execution timeline", h2))
|
||||
rows = [["Step", "State", "Attempt", "Operator", "Completed"]]
|
||||
for item in timeline:
|
||||
rows.append([
|
||||
item.get("step", ""), item.get("state", ""), str(item.get("attempt", "")),
|
||||
item.get("operator", ""), item.get("completed_at", ""),
|
||||
])
|
||||
story.append(self._table(rows, [45 * mm, 30 * mm, 18 * mm, 35 * mm, 42 * mm], header=True))
|
||||
story.append(Spacer(1, 3 * mm))
|
||||
|
||||
# Quality / dynamic fields
|
||||
fields = data.get("fields") or []
|
||||
if fields:
|
||||
story.append(Paragraph("Quality & results", h2))
|
||||
rows = [["Field", "Value"]]
|
||||
for field in fields:
|
||||
rows.append([field.get("label", ""), str(field.get("value", "") or "—")])
|
||||
story.append(self._table(rows, [70 * mm, 100 * mm], header=True))
|
||||
story.append(Spacer(1, 3 * mm))
|
||||
|
||||
# Price breakdown
|
||||
price = data.get("price") or {}
|
||||
if price:
|
||||
story.append(Paragraph("Price breakdown", h2))
|
||||
rows = [["Item", "Quantity", "Unit price", "Amount"]]
|
||||
for line in price.get("lines", []):
|
||||
rows.append([
|
||||
line.get("rule", ""), line.get("quantity", ""),
|
||||
line.get("unit_price", ""), line.get("amount", ""),
|
||||
])
|
||||
rows.append(["Subtotal", "", "", price.get("subtotal", "")])
|
||||
rows.append(["Discounts", "", "", price.get("discounts", "")])
|
||||
rows.append(["Taxes", "", "", price.get("taxes", "")])
|
||||
rows.append(["TOTAL", "", "", f"{price.get('total', '')} {price.get('currency', '')}"])
|
||||
story.append(self._table(rows, [70 * mm, 25 * mm, 30 * mm, 45 * mm], header=True))
|
||||
story.append(Spacer(1, 3 * mm))
|
||||
|
||||
# Payments / invoices
|
||||
payments = data.get("payments") or []
|
||||
invoices = data.get("invoices") or []
|
||||
if payments or invoices:
|
||||
story.append(Paragraph("Payments & invoices", h2))
|
||||
rows = [["Type", "Reference", "Status", "Amount"]]
|
||||
for p in payments:
|
||||
rows.append(["Payment", "—", p.get("status", ""), f"{p.get('amount', '')} {p.get('currency', '')}"])
|
||||
for i in invoices:
|
||||
rows.append(["Invoice", i.get("number", ""), i.get("sync_state", ""), i.get("amount", "")])
|
||||
story.append(self._table(rows, [30 * mm, 60 * mm, 40 * mm, 40 * mm], header=True))
|
||||
|
||||
doc.build(story)
|
||||
return buf.getvalue()
|
||||
|
||||
def _table(self, rows, col_widths, header=False):
|
||||
table = Table(rows, colWidths=col_widths)
|
||||
style = [
|
||||
("GRID", (0, 0), (-1, -1), 0.5, LINE),
|
||||
("FONTSIZE", (0, 0), (-1, -1), 9),
|
||||
("PADDING", (0, 0), (-1, -1), 5),
|
||||
("VALIGN", (0, 0), (-1, -1), "TOP"),
|
||||
("ROWBACKGROUNDS", (0, 0), (-1, -1), [colors.white, CREAM]),
|
||||
]
|
||||
if header:
|
||||
style += [
|
||||
("BACKGROUND", (0, 0), (-1, 0), GREEN),
|
||||
("TEXTCOLOR", (0, 0), (-1, 0), colors.white),
|
||||
("FONTNAME", (0, 0), (-1, 0), "Helvetica-Bold"),
|
||||
]
|
||||
table.setStyle(TableStyle(style))
|
||||
return table
|
||||
Reference in New Issue
Block a user