diff --git a/go/internal/cmc/handlers/pdf_api.go b/go/internal/cmc/handlers/pdf_api.go index 829ef55a..c3d206a3 100644 --- a/go/internal/cmc/handlers/pdf_api.go +++ b/go/internal/cmc/handlers/pdf_api.go @@ -15,31 +15,53 @@ import ( // InvoiceLineItemRequest is the JSON shape for a single line item. type InvoiceLineItemRequest struct { - ItemNumber string `json:"item_number"` - Quantity string `json:"quantity"` - Title string `json:"title"` - UnitPrice float64 `json:"unit_price"` - TotalPrice float64 `json:"total_price"` + ItemNumber string `json:"item_number"` + Quantity string `json:"quantity"` + Title string `json:"title"` + Description string `json:"description"` + UnitPrice float64 `json:"unit_price"` + TotalPrice float64 `json:"total_price"` + NetUnitPrice float64 `json:"net_unit_price"` + NetPrice float64 `json:"net_price"` + DiscountPercent float64 `json:"discount_percent"` + DiscountAmountUnit float64 `json:"discount_amount_unit"` + DiscountAmountTotal float64 `json:"discount_amount_total"` + Option int `json:"option"` + HasTextPrices bool `json:"has_text_prices"` + UnitPriceString string `json:"unit_price_string"` + GrossPriceString string `json:"gross_price_string"` } // InvoicePDFRequest is the expected payload from the PHP app. type InvoicePDFRequest struct { - DocumentID int32 `json:"document_id"` - InvoiceTitle string `json:"invoice_title"` - CustomerName string `json:"customer_name"` - ContactEmail string `json:"contact_email"` - ContactName string `json:"contact_name"` - UserFirstName string `json:"user_first_name"` - UserLastName string `json:"user_last_name"` - UserEmail string `json:"user_email"` - YourReference string `json:"your_reference"` - ShipVia string `json:"ship_via"` - FOB string `json:"fob"` - IssueDate string `json:"issue_date"` // ISO date: 2006-01-02 - CurrencySymbol string `json:"currency_symbol"` // e.g. "$" - ShowGST bool `json:"show_gst"` - LineItems []InvoiceLineItemRequest `json:"line_items"` - OutputDir string `json:"output_dir"` // optional override + DocumentID int32 `json:"document_id"` + InvoiceTitle string `json:"invoice_title"` + CustomerName string `json:"customer_name"` + ContactEmail string `json:"contact_email"` + ContactName string `json:"contact_name"` + UserFirstName string `json:"user_first_name"` + UserLastName string `json:"user_last_name"` + UserEmail string `json:"user_email"` + YourReference string `json:"your_reference"` + ShipVia string `json:"ship_via"` + FOB string `json:"fob"` + IssueDate string `json:"issue_date"` // ISO date: 2006-01-02 + IssueDateString string `json:"issue_date_string"` // Formatted: "12 January 2026" + CurrencySymbol string `json:"currency_symbol"` // e.g. "$" + CurrencyCode string `json:"currency_code"` // e.g. "AUD", "USD" + ShowGST bool `json:"show_gst"` + BillTo string `json:"bill_to"` + ShipTo string `json:"ship_to"` + ShippingDetails string `json:"shipping_details"` + CustomerOrderNumber string `json:"customer_order_number"` + JobTitle string `json:"job_title"` + PaymentTerms string `json:"payment_terms"` + CustomerABN string `json:"customer_abn"` + Subtotal interface{} `json:"subtotal"` // Can be float or "TBA" + GSTAmount interface{} `json:"gst_amount"` + Total interface{} `json:"total"` + LineItems []InvoiceLineItemRequest `json:"line_items"` + OutputDir string `json:"output_dir"` // optional override } // GenerateInvoicePDF handles POST /api/pdf/invoice and writes a PDF to disk. @@ -93,20 +115,32 @@ func GenerateInvoicePDF(w http.ResponseWriter, r *http.Request) { } data := &pdf.InvoicePDFData{ - Document: doc, - Invoice: inv, - Customer: cust, - LineItems: lineItems, - CurrencySymbol: req.CurrencySymbol, - ShowGST: req.ShowGST, - ShipVia: req.ShipVia, - FOB: req.FOB, - IssueDate: issueDate, - EmailTo: req.ContactEmail, - Attention: req.ContactName, - FromName: fmt.Sprintf("%s %s", req.UserFirstName, req.UserLastName), - FromEmail: req.UserEmail, - YourReference: req.YourReference, + Document: doc, + Invoice: inv, + Customer: cust, + LineItems: lineItems, + CurrencySymbol: req.CurrencySymbol, + CurrencyCode: req.CurrencyCode, + ShowGST: req.ShowGST, + ShipVia: req.ShipVia, + FOB: req.FOB, + IssueDate: issueDate, + IssueDateString: req.IssueDateString, + EmailTo: req.ContactEmail, + Attention: req.ContactName, + FromName: fmt.Sprintf("%s %s", req.UserFirstName, req.UserLastName), + FromEmail: req.UserEmail, + YourReference: req.YourReference, + BillTo: req.BillTo, + ShipTo: req.ShipTo, + ShippingDetails: req.ShippingDetails, + CustomerOrderNumber: req.CustomerOrderNumber, + JobTitle: req.JobTitle, + PaymentTerms: req.PaymentTerms, + CustomerABN: req.CustomerABN, + Subtotal: req.Subtotal, + GSTAmount: req.GSTAmount, + Total: req.Total, } filename, err := pdf.GenerateInvoicePDF(data, outputDir) diff --git a/go/internal/cmc/pdf/generator.go b/go/internal/cmc/pdf/generator.go index c958376a..911f237e 100644 --- a/go/internal/cmc/pdf/generator.go +++ b/go/internal/cmc/pdf/generator.go @@ -318,6 +318,201 @@ For full terms and conditions, please refer to our website or contact CMC TECHNO g.pdf.MultiCell(0, 4, disclaimerText, "", "L", false) } +// AddInvoiceAddressBoxes adds the Sold To / Delivery Address boxes for invoices +func (g *Generator) AddInvoiceAddressBoxes(data *InvoicePDFData) { + g.pdf.SetFont("Helvetica", "", 9) + + // Top row with headers and invoice details + x := g.pdf.GetX() + y := g.pdf.GetY() + + // Left: Sold To header + g.pdf.SetXY(x, y) + g.pdf.SetFillColor(242, 242, 242) + g.pdf.CellFormat(58, 5, "Sold To / Invoice Address:", "1", 0, "L", true, 0, "") + + // Middle: Delivery Address header + g.pdf.CellFormat(58, 5, "Delivery Address:", "1", 0, "L", true, 0, "") + + // Right: Invoice details (starts here, spans 2 rows) + g.pdf.SetXY(x+122, y) + g.pdf.SetFont("Helvetica", "U", 11) + g.pdf.CellFormat(0, 5, fmt.Sprintf("CMC INVOICE#: %s", data.Invoice.Title), "", 1, "L", false, 0, "") + + // Second row with address content + y += 5 + g.pdf.SetXY(x, y) + g.pdf.SetFont("Helvetica", "", 9) + + // Sold To content + billToHeight := 30.0 + g.pdf.MultiCell(58, 4, data.BillTo, "1", "L", false) + soldToEndY := g.pdf.GetY() + + // Ship To content + g.pdf.SetXY(x+58, y) + g.pdf.MultiCell(58, 4, data.ShipTo, "1", "L", false) + shipToEndY := g.pdf.GetY() + + // Ensure both cells have same height + maxEndY := soldToEndY + if shipToEndY > maxEndY { + maxEndY = shipToEndY + } + if maxEndY < y+billToHeight { + maxEndY = y + billToHeight + } + + // Add remaining invoice details on the right + g.pdf.SetXY(x+122, y) + g.pdf.SetFont("Helvetica", "", 9) + detailsText := fmt.Sprintf("Date: %s\nPage: 1 of {nb}", data.IssueDateString) + g.pdf.MultiCell(0, 4, detailsText, "", "L", false) + + g.pdf.Ln(2) + g.pdf.SetFont("Helvetica", "U", 9) + g.pdf.CellFormat(0, 4, "MAKE PAYMENT TO:", "", 1, "L", false, 0, "") + g.pdf.SetFont("Helvetica", "", 8) + + // Bank details based on currency + bankDetails := g.getBankDetails(data.CurrencyCode) + g.pdf.MultiCell(0, 3, bankDetails, "", "L", false) + + g.pdf.SetY(maxEndY + 2) +} + +// getBankDetails returns bank payment details based on currency code +func (g *Generator) getBankDetails(currencyCode string) string { + switch currencyCode { + case "EUR": + return "Account Name: CMC Technologies Pty Ltd\nAccount Number/IBAN: 06200015682004\nBranch code: 06200\nSWIFT Code/BIC: CTBAAU2S" + case "GBP": + return "Account Name: CMC Technologies Pty Ltd\nAccount Number/IBAN: 06200015642694\nBranch code: 06200\nSWIFT Code/BIC: CTBAAU2S" + case "USD": + return "Account Name: CMC Technologies Pty Ltd\nAccount Number/IBAN: 06200015681984\nBranch code: 06200\nSWIFT Code/BIC: CTBAAU2S" + default: // AUD and others + return "Account Name: CMC Technologies Pty Ltd\nBank Number BSB#: 062-458\nAccount Number: 10067982\nSWIFT Code: CTBAAU2S\nIBAN: 06245810067982" + } +} + +// AddInvoiceDetailsTable adds the order number, job, FOB, payment terms, ABN table +func (g *Generator) AddInvoiceDetailsTable(data *InvoicePDFData) { + g.pdf.SetFont("Helvetica", "B", 9) + g.pdf.SetFillColor(242, 242, 242) + + // Header row + colWidth := 38.0 + g.pdf.CellFormat(colWidth, 5, "CUSTOMER ORDER NO", "1", 0, "C", true, 0, "") + g.pdf.CellFormat(colWidth, 5, "CMC JOB #", "1", 0, "C", true, 0, "") + g.pdf.CellFormat(colWidth, 5, "INCOTERMS 2010", "1", 0, "C", true, 0, "") + g.pdf.CellFormat(colWidth, 5, "PAYMENT TERMS", "1", 0, "C", true, 0, "") + g.pdf.CellFormat(colWidth, 5, "CUSTOMER ABN", "1", 1, "C", true, 0, "") + + // Data row + g.pdf.SetFont("Helvetica", "", 9) + g.pdf.CellFormat(colWidth, 5, data.CustomerOrderNumber, "1", 0, "C", false, 0, "") + g.pdf.CellFormat(colWidth, 5, data.JobTitle, "1", 0, "C", false, 0, "") + g.pdf.CellFormat(colWidth, 5, data.FOB, "1", 0, "C", false, 0, "") + g.pdf.CellFormat(colWidth, 5, data.PaymentTerms, "1", 0, "C", false, 0, "") + g.pdf.CellFormat(colWidth, 5, data.CustomerABN, "1", 1, "C", false, 0, "") +} + +// AddInvoiceLineItemsHeader adds just the header row for line items table +func (g *Generator) AddInvoiceLineItemsHeader(currencyCode string) { + g.pdf.SetFont("Helvetica", "B", 9) + g.pdf.SetFillColor(242, 242, 242) + + g.pdf.CellFormat(15, 5, "ITEM NO.", "1", 0, "C", true, 0, "") + g.pdf.CellFormat(15, 5, "QTY", "1", 0, "C", true, 0, "") + g.pdf.CellFormat(100, 5, "DESCRIPTION", "1", 0, "C", true, 0, "") + g.pdf.CellFormat(30, 5, "UNIT PRICE", "1", 0, "C", true, 0, "") + g.pdf.CellFormat(30, 5, "TOTAL PRICE", "1", 1, "C", true, 0, "") + + // Currency row + g.pdf.SetFont("Helvetica", "", 8) + g.pdf.CellFormat(15, 4, "", "1", 0, "C", false, 0, "") + g.pdf.CellFormat(15, 4, "", "1", 0, "C", false, 0, "") + g.pdf.CellFormat(100, 4, "", "1", 0, "C", false, 0, "") + g.pdf.CellFormat(30, 4, currencyCode, "1", 0, "C", false, 0, "") + g.pdf.CellFormat(30, 4, currencyCode, "1", 1, "C", false, 0, "") +} + +// AddInvoiceLineItemsContent adds line items and totals for invoices +func (g *Generator) AddInvoiceLineItemsContent(data *InvoicePDFData) { + g.pdf.SetFont("Helvetica", "", 9) + + // Line items + for _, item := range data.LineItems { + unitPrice := 0.0 + totalPrice := 0.0 + + if item.GrossUnitPrice.Valid { + fmt.Sscanf(item.GrossUnitPrice.String, "%f", &unitPrice) + } + if item.GrossPrice.Valid { + fmt.Sscanf(item.GrossPrice.String, "%f", &totalPrice) + } + + g.pdf.CellFormat(15, 5, item.ItemNumber, "1", 0, "C", false, 0, "") + g.pdf.CellFormat(15, 5, item.Quantity, "1", 0, "C", false, 0, "") + g.pdf.CellFormat(100, 5, item.Title, "1", 0, "L", false, 0, "") + g.pdf.CellFormat(30, 5, fmt.Sprintf("%s%.2f", data.CurrencySymbol, unitPrice), "1", 0, "C", false, 0, "") + g.pdf.CellFormat(30, 5, fmt.Sprintf("%s%.2f", data.CurrencySymbol, totalPrice), "1", 1, "C", false, 0, "") + } + + // Freight details and totals + g.pdf.SetFillColor(242, 242, 242) + g.pdf.SetFont("Helvetica", "B", 9) + + // Freight details (left side, spans 2 rows) + y := g.pdf.GetY() + g.pdf.CellFormat(130, 5, "FREIGHT DETAILS:", "1", 0, "L", true, 0, "") + + // Subtotal + g.pdf.CellFormat(30, 5, "SUBTOTAL", "1", 0, "L", true, 0, "") + g.pdf.SetFont("Helvetica", "", 9) + subtotalStr := fmt.Sprintf("%v", data.Subtotal) + if val, ok := data.Subtotal.(float64); ok { + subtotalStr = fmt.Sprintf("%s%.2f", data.CurrencySymbol, val) + } + g.pdf.CellFormat(30, 5, subtotalStr, "1", 1, "C", false, 0, "") + + // Shipping details text (left side) + g.pdf.SetFont("Helvetica", "", 8) + g.pdf.SetXY(10, y+5) + g.pdf.MultiCell(130, 4, data.ShippingDetails, "1", "L", false) + shippingEndY := g.pdf.GetY() + + // GST row + g.pdf.SetXY(140, y+5) + g.pdf.SetFont("Helvetica", "B", 9) + g.pdf.SetFillColor(242, 242, 242) + g.pdf.CellFormat(30, 5, "GST (10%)", "1", 0, "L", true, 0, "") + g.pdf.SetFont("Helvetica", "", 9) + gstStr := fmt.Sprintf("%v", data.GSTAmount) + if val, ok := data.GSTAmount.(float64); ok { + gstStr = fmt.Sprintf("%s%.2f", data.CurrencySymbol, val) + } + g.pdf.CellFormat(30, 5, gstStr, "1", 1, "C", false, 0, "") + + // Total row + g.pdf.SetFont("Helvetica", "B", 9) + g.pdf.CellFormat(130, 5, "", "", 0, "L", false, 0, "") + g.pdf.SetFillColor(242, 242, 242) + g.pdf.CellFormat(30, 5, "TOTAL DUE", "1", 0, "L", true, 0, "") + g.pdf.SetFont("Helvetica", "", 9) + totalStr := fmt.Sprintf("%v", data.Total) + if val, ok := data.Total.(float64); ok { + totalStr = fmt.Sprintf("%s%.2f", data.CurrencySymbol, val) + } + g.pdf.CellFormat(30, 5, totalStr, "1", 1, "C", false, 0, "") + + // Make sure we're past the shipping details + if g.pdf.GetY() < shippingEndY { + g.pdf.SetY(shippingEndY) + } +} + // Save saves the PDF to a file func (g *Generator) Save(filename string) error { g.pdf.AliasNbPages("") diff --git a/go/internal/cmc/pdf/templates.go b/go/internal/cmc/pdf/templates.go index 64993081..b89d04be 100644 --- a/go/internal/cmc/pdf/templates.go +++ b/go/internal/cmc/pdf/templates.go @@ -139,23 +139,35 @@ func GenerateQuotePDF(data *QuotePDFData, outputDir string) (string, error) { // InvoicePDFData contains all data needed to generate an invoice PDF type InvoicePDFData struct { - Document *db.Document - Invoice *db.Invoice - Enquiry *db.Enquiry - Customer *db.Customer - Job interface{} // Job data - LineItems []db.GetLineItemsTableRow - Currency interface{} // Currency data - CurrencySymbol string - ShowGST bool - ShipVia string - FOB string - IssueDate time.Time - EmailTo string - Attention string - FromName string - FromEmail string - YourReference string + Document *db.Document + Invoice *db.Invoice + Enquiry *db.Enquiry + Customer *db.Customer + Job interface{} // Job data + LineItems []db.GetLineItemsTableRow + Currency interface{} // Currency data + CurrencySymbol string + CurrencyCode string + ShowGST bool + ShipVia string + FOB string + IssueDate time.Time + IssueDateString string + EmailTo string + Attention string + FromName string + FromEmail string + YourReference string + BillTo string + ShipTo string + ShippingDetails string + CustomerOrderNumber string + JobTitle string + PaymentTerms string + CustomerABN string + Subtotal interface{} // Can be float or "TBA" + GSTAmount interface{} + Total interface{} } // GenerateInvoicePDF generates a PDF for an invoice @@ -166,70 +178,39 @@ func GenerateInvoicePDF(data *InvoicePDFData, outputDir string) (string, error) gen.AddPage() gen.Page1Header() - // Extract data for details box - companyName := data.Customer.Name - emailTo := data.EmailTo - attention := data.Attention - fromName := data.FromName - fromEmail := data.FromEmail - invoiceNumber := data.Invoice.Title - - yourReference := data.YourReference - issueDate := data.IssueDate.Format("2 January 2006") - - // Add details box - gen.DetailsBox("INVOICE", companyName, emailTo, attention, fromName, fromEmail, invoiceNumber, yourReference, issueDate) - // Add shipping details + // Title + gen.pdf.SetFont("Helvetica", "B", 18) + gen.pdf.SetTextColor(0, 0, 0) + gen.pdf.CellFormat(0, 10, "TAX INVOICE", "", 1, "C", false, 0, "") gen.pdf.Ln(5) - gen.pdf.SetFont("Helvetica", "B", 10) - gen.pdf.CellFormat(30, 5, "Ship Via:", "", 0, "L", false, 0, "") - gen.pdf.SetFont("Helvetica", "", 10) - gen.pdf.CellFormat(60, 5, data.ShipVia, "", 1, "L", false, 0, "") - gen.pdf.SetFont("Helvetica", "B", 10) - gen.pdf.CellFormat(30, 5, "FOB:", "", 0, "L", false, 0, "") - gen.pdf.SetFont("Helvetica", "", 10) - gen.pdf.CellFormat(60, 5, data.FOB, "", 1, "L", false, 0, "") + // Sold To / Delivery Address boxes + gen.AddInvoiceAddressBoxes(data) + + // More details table (Order Number, Job, FOB, Payment Terms, ABN) + gen.AddInvoiceDetailsTable(data) + + gen.pdf.Ln(5) + + // Line items table header + gen.AddInvoiceLineItemsHeader(data.CurrencyCode) gen.Page1Footer() - // Add line items page + // Add line items on new page(s) gen.AddPage() - gen.pdf.SetFont("Helvetica", "B", 14) - gen.pdf.CellFormat(0, 10, "INVOICE DETAILS", "", 1, "C", false, 0, "") - gen.pdf.Ln(5) + gen.pdf.SetFont("Helvetica", "B", 12) + gen.pdf.CellFormat(0, 6, "CONTINUED: "+data.Invoice.Title, "", 1, "L", false, 0, "") + gen.pdf.Ln(4) - // Convert line items - pdfItems := make([]LineItem, len(data.LineItems)) - for i, item := range data.LineItems { - unitPrice := 0.0 - totalPrice := 0.0 - - // Parse prices - if item.GrossUnitPrice.Valid { - fmt.Sscanf(item.GrossUnitPrice.String, "%f", &unitPrice) - } - if item.GrossPrice.Valid { - fmt.Sscanf(item.GrossPrice.String, "%f", &totalPrice) - } - - pdfItems[i] = LineItem{ - ItemNumber: item.ItemNumber, - Quantity: item.Quantity, - Title: item.Title, - UnitPrice: unitPrice, - TotalPrice: totalPrice, - } - } - - // Add line items table - gen.AddLineItemsTable(pdfItems, data.CurrencySymbol, data.ShowGST) + // Add line items content + gen.AddInvoiceLineItemsContent(data) // Add terms and conditions page gen.AddTermsAndConditions() - // Generate filename and save directly (no merge) - filename := fmt.Sprintf("%s.pdf", invoiceNumber) + // Generate filename and save + filename := fmt.Sprintf("%s.pdf", data.Invoice.Title) if err := gen.Save(filename); err != nil { return "", err } diff --git a/php/app/views/documents/pdf_invoice.ctp b/php/app/views/documents/pdf_invoice.ctp index 1e8c7e5d..67ddb474 100755 --- a/php/app/views/documents/pdf_invoice.ctp +++ b/php/app/views/documents/pdf_invoice.ctp @@ -12,8 +12,18 @@ foreach ($document['LineItem'] as $li) { 'item_number' => $li['item_number'], 'quantity' => $li['quantity'], 'title' => $li['title'], + 'description' => isset($li['description']) ? $li['description'] : '', 'unit_price' => floatval($li['gross_unit_price']), - 'total_price' => floatval($li['gross_price']) + 'total_price' => floatval($li['gross_price']), + 'net_unit_price' => floatval($li['net_unit_price']), + 'net_price' => floatval($li['net_price']), + 'discount_percent' => floatval($li['discount_percent']), + 'discount_amount_unit' => floatval($li['discount_amount_unit']), + 'discount_amount_total' => floatval($li['discount_amount_total']), + 'option' => intval($li['option']), + 'has_text_prices' => isset($li['has_text_prices']) ? (bool)$li['has_text_prices'] : false, + 'unit_price_string' => isset($li['unit_price_string']) ? $li['unit_price_string'] : '', + 'gross_price_string' => isset($li['gross_price_string']) ? $li['gross_price_string'] : '' ); } @@ -28,10 +38,22 @@ $payload = array( 'user_email' => $enquiry['User']['email'], 'your_reference' => isset($enquiry['Enquiry']['customer_reference']) ? $enquiry['Enquiry']['customer_reference'] : ('Enquiry on '.date('j M Y', strtotime($enquiry['Enquiry']['created']))), 'ship_via' => $document['Invoice']['ship_via'], - 'fob' => $document['Invoice']['fob'], + 'fob' => $fob, 'issue_date' => $document['Invoice']['issue_date'], // expects YYYY-MM-DD + 'issue_date_string' => $issue_date_string, 'currency_symbol' => $currencySymbol, + 'currency_code' => $currencyCode, 'show_gst' => (bool)$gst, + 'bill_to' => isset($document['Document']['bill_to']) ? $document['Document']['bill_to'] : '', + 'ship_to' => isset($document['Document']['ship_to']) ? $document['Document']['ship_to'] : '', + 'shipping_details' => isset($document['Document']['shipping_details']) ? $document['Document']['shipping_details'] : '', + 'customer_order_number' => isset($job['Job']['customer_order_number']) ? $job['Job']['customer_order_number'] : '', + 'job_title' => isset($job['Job']['title']) ? $job['Job']['title'] : '', + 'payment_terms' => isset($job['Customer']['payment_terms']) ? $job['Customer']['payment_terms'] : '', + 'customer_abn' => isset($job['Customer']['abn']) ? $job['Customer']['abn'] : '', + 'subtotal' => $totals['subtotal'], + 'gst_amount' => $totals['gst'], + 'total' => $totals['total'], 'line_items' => $lineItems, 'output_dir' => $outputDir );