import hashlib from datetime import date from io import BytesIO from django.utils import timezone def generate_coa_pdf(qc, batch, spec_version="", issuer="IXG Ltd"): """Generate the IXG Quality Summary cover sheet PDF for an approved QC result. This is a companion to (never a replacement for) the independent laboratory certificate, which remains the authoritative source document. Returns (pdf_bytes, sha256_hex). """ 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 styles = getSampleStyleSheet() title_style = ParagraphStyle( "TitleX", parent=styles["Title"], fontSize=16, leading=20, textColor=colors.HexColor("#1A3D2B"), ) sub_style = ParagraphStyle( "SubX", parent=styles["Normal"], fontSize=9, leading=12, textColor=colors.HexColor("#4B5563"), ) cell_style = ParagraphStyle("CellX", parent=styles["Normal"], fontSize=9, leading=11) issued = date.today() reference = f"{batch.batch_reference}-COA-V{1}" rows = [ ["Batch reference", batch.batch_reference], ["Product", "Unrefined Shea Butter"], ["Origin", batch.origin_facility], ["Grade", batch.grade or "-"], ["Weight", f"{batch.weight_kg} kg" if batch.weight_kg is not None else "-"], ["Harvest date", batch.harvest_date.isoformat() if batch.harvest_date else "-"], ["Specification", spec_version or "-"], ["Lab reference", qc.lab_reference or "-"], ["Tested by", qc.tested_by or "-"], ["Quality approved by", (qc.verified_by.full_name or qc.verified_by.email) if qc.verified_by else "-"], ["Quality approved on", qc.approved_at.isoformat() if qc.approved_at else "-"], ["Issued by", issuer], ["Issue date", issued.isoformat()], ] body = [ ["Parameter", "Result", "Unit", "Result status"], ["FFA", f"{qc.ffa_percentage}", "%", "Within specification" if float(qc.ffa_percentage) <= 3 else "Outside specification"], ["Moisture", f"{qc.moisture_percentage}", "%", "Within specification" if float(qc.moisture_percentage) <= 0.1 else "Outside specification"], ["Peroxide value", f"{qc.peroxide_value}", "meq O2/kg", "Within specification" if float(qc.peroxide_value) <= 10 else "Outside specification"], ["Visual grade", qc.visual_grade, "-", "Within specification" if qc.visual_grade != "Fail" else "Outside specification"], ] pdf = BytesIO() doc = SimpleDocTemplate(pdf, pagesize=A4, leftMargin=20 * mm, rightMargin=20 * mm, topMargin=18 * mm, bottomMargin=18 * mm) elements = [] elements.append(Paragraph("IXG Ltd — Quality Summary", title_style)) elements.append(Spacer(1, 2 * mm)) elements.append(Paragraph( "Companion document to the independent laboratory Certificate of Analysis. " "The independent laboratory certificate remains the authoritative source document.", sub_style, )) elements.append(Spacer(1, 6 * mm)) header_table = Table([[Paragraph(f"Reference: {reference}", sub_style)]], colWidths=[120 * mm]) elements.append(header_table) elements.append(Spacer(1, 4 * mm)) meta = Table([[Paragraph(c, cell_style) for c in row] for row in rows], colWidths=[55 * mm, 115 * mm]) meta.setStyle(TableStyle([ ("BACKGROUND", (0, 0), (0, -1), colors.HexColor("#EAF3EF")), ("GRID", (0, 0), (-1, -1), 0.4, colors.HexColor("#CCD3CF")), ("VALIGN", (0, 0), (-1, -1), "TOP"), ("TOPPADDING", (0, 0), (-1, -1), 3), ("BOTTOMPADDING", (0, 0), (-1, -1), 3), ])) elements.append(meta) elements.append(Spacer(1, 6 * mm)) elements.append(Paragraph("Results vs Specification", styles["Heading3"])) elements.append(Spacer(1, 2 * mm)) result_table = Table(body, colWidths=[55 * mm, 35 * mm, 30 * mm, 50 * mm], repeatRows=1) result_table.setStyle(TableStyle([ ("BACKGROUND", (0, 0), (-1, 0), colors.HexColor("#1A3D2B")), ("TEXTCOLOR", (0, 0), (-1, 0), colors.white), ("GRID", (0, 0), (-1, -1), 0.4, colors.HexColor("#CCD3CF")), ("FONTSIZE", (0, 0), (-1, -1), 9), ("ROWBACKGROUNDS", (0, 1), (-1, -1), [colors.white, colors.HexColor("#F7F6F2")]), ])) elements.append(result_table) elements.append(Spacer(1, 6 * mm)) elements.append(Paragraph( "This summary is derived from the structured QC record on the IXG platform. " "The independent laboratory certificate remains the authoritative source document.", sub_style, )) doc.build(elements) data = pdf.getvalue() digest = hashlib.sha256(data).hexdigest() return data, digest def write_coa_file(qc, batch, pdf_bytes): """Persist the generated PDF under media/ixg-documents/{batch_id}/ and return (file_url, file_name).""" import os import uuid as uuid_mod from django.conf import settings batch_dir = os.path.join(settings.MEDIA_ROOT, "ixg-documents", str(batch.id)) os.makedirs(batch_dir, exist_ok=True) filename = f"coa-{uuid_mod.uuid4().hex[:8]}-{batch.batch_reference}.pdf" filepath = os.path.join(batch_dir, filename) with open(filepath, "wb") as f: f.write(pdf_bytes) return f"/media/ixg-documents/{batch.id}/{filename}", filename