Altering go pdf template to match old style

This commit is contained in:
Finley Ghosh 2026-01-13 00:04:42 +11:00
parent 091ce77c49
commit 9c10d4c21f
4 changed files with 339 additions and 107 deletions

View file

@ -15,31 +15,53 @@ import (
// InvoiceLineItemRequest is the JSON shape for a single line item. // InvoiceLineItemRequest is the JSON shape for a single line item.
type InvoiceLineItemRequest struct { type InvoiceLineItemRequest struct {
ItemNumber string `json:"item_number"` ItemNumber string `json:"item_number"`
Quantity string `json:"quantity"` Quantity string `json:"quantity"`
Title string `json:"title"` Title string `json:"title"`
UnitPrice float64 `json:"unit_price"` Description string `json:"description"`
TotalPrice float64 `json:"total_price"` 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. // InvoicePDFRequest is the expected payload from the PHP app.
type InvoicePDFRequest struct { type InvoicePDFRequest struct {
DocumentID int32 `json:"document_id"` DocumentID int32 `json:"document_id"`
InvoiceTitle string `json:"invoice_title"` InvoiceTitle string `json:"invoice_title"`
CustomerName string `json:"customer_name"` CustomerName string `json:"customer_name"`
ContactEmail string `json:"contact_email"` ContactEmail string `json:"contact_email"`
ContactName string `json:"contact_name"` ContactName string `json:"contact_name"`
UserFirstName string `json:"user_first_name"` UserFirstName string `json:"user_first_name"`
UserLastName string `json:"user_last_name"` UserLastName string `json:"user_last_name"`
UserEmail string `json:"user_email"` UserEmail string `json:"user_email"`
YourReference string `json:"your_reference"` YourReference string `json:"your_reference"`
ShipVia string `json:"ship_via"` ShipVia string `json:"ship_via"`
FOB string `json:"fob"` FOB string `json:"fob"`
IssueDate string `json:"issue_date"` // ISO date: 2006-01-02 IssueDate string `json:"issue_date"` // ISO date: 2006-01-02
CurrencySymbol string `json:"currency_symbol"` // e.g. "$" IssueDateString string `json:"issue_date_string"` // Formatted: "12 January 2026"
ShowGST bool `json:"show_gst"` CurrencySymbol string `json:"currency_symbol"` // e.g. "$"
LineItems []InvoiceLineItemRequest `json:"line_items"` CurrencyCode string `json:"currency_code"` // e.g. "AUD", "USD"
OutputDir string `json:"output_dir"` // optional override 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. // 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{ data := &pdf.InvoicePDFData{
Document: doc, Document: doc,
Invoice: inv, Invoice: inv,
Customer: cust, Customer: cust,
LineItems: lineItems, LineItems: lineItems,
CurrencySymbol: req.CurrencySymbol, CurrencySymbol: req.CurrencySymbol,
ShowGST: req.ShowGST, CurrencyCode: req.CurrencyCode,
ShipVia: req.ShipVia, ShowGST: req.ShowGST,
FOB: req.FOB, ShipVia: req.ShipVia,
IssueDate: issueDate, FOB: req.FOB,
EmailTo: req.ContactEmail, IssueDate: issueDate,
Attention: req.ContactName, IssueDateString: req.IssueDateString,
FromName: fmt.Sprintf("%s %s", req.UserFirstName, req.UserLastName), EmailTo: req.ContactEmail,
FromEmail: req.UserEmail, Attention: req.ContactName,
YourReference: req.YourReference, 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) filename, err := pdf.GenerateInvoicePDF(data, outputDir)

View file

@ -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) 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 // Save saves the PDF to a file
func (g *Generator) Save(filename string) error { func (g *Generator) Save(filename string) error {
g.pdf.AliasNbPages("") g.pdf.AliasNbPages("")

View file

@ -139,23 +139,35 @@ func GenerateQuotePDF(data *QuotePDFData, outputDir string) (string, error) {
// InvoicePDFData contains all data needed to generate an invoice PDF // InvoicePDFData contains all data needed to generate an invoice PDF
type InvoicePDFData struct { type InvoicePDFData struct {
Document *db.Document Document *db.Document
Invoice *db.Invoice Invoice *db.Invoice
Enquiry *db.Enquiry Enquiry *db.Enquiry
Customer *db.Customer Customer *db.Customer
Job interface{} // Job data Job interface{} // Job data
LineItems []db.GetLineItemsTableRow LineItems []db.GetLineItemsTableRow
Currency interface{} // Currency data Currency interface{} // Currency data
CurrencySymbol string CurrencySymbol string
ShowGST bool CurrencyCode string
ShipVia string ShowGST bool
FOB string ShipVia string
IssueDate time.Time FOB string
EmailTo string IssueDate time.Time
Attention string IssueDateString string
FromName string EmailTo string
FromEmail string Attention string
YourReference 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 // GenerateInvoicePDF generates a PDF for an invoice
@ -166,70 +178,39 @@ func GenerateInvoicePDF(data *InvoicePDFData, outputDir string) (string, error)
gen.AddPage() gen.AddPage()
gen.Page1Header() gen.Page1Header()
// Extract data for details box // Title
companyName := data.Customer.Name gen.pdf.SetFont("Helvetica", "B", 18)
emailTo := data.EmailTo gen.pdf.SetTextColor(0, 0, 0)
attention := data.Attention gen.pdf.CellFormat(0, 10, "TAX INVOICE", "", 1, "C", false, 0, "")
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
gen.pdf.Ln(5) 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) // Sold To / Delivery Address boxes
gen.pdf.CellFormat(30, 5, "FOB:", "", 0, "L", false, 0, "") gen.AddInvoiceAddressBoxes(data)
gen.pdf.SetFont("Helvetica", "", 10)
gen.pdf.CellFormat(60, 5, data.FOB, "", 1, "L", false, 0, "") // 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() gen.Page1Footer()
// Add line items page // Add line items on new page(s)
gen.AddPage() gen.AddPage()
gen.pdf.SetFont("Helvetica", "B", 14) gen.pdf.SetFont("Helvetica", "B", 12)
gen.pdf.CellFormat(0, 10, "INVOICE DETAILS", "", 1, "C", false, 0, "") gen.pdf.CellFormat(0, 6, "CONTINUED: "+data.Invoice.Title, "", 1, "L", false, 0, "")
gen.pdf.Ln(5) gen.pdf.Ln(4)
// Convert line items // Add line items content
pdfItems := make([]LineItem, len(data.LineItems)) gen.AddInvoiceLineItemsContent(data)
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 terms and conditions page // Add terms and conditions page
gen.AddTermsAndConditions() gen.AddTermsAndConditions()
// Generate filename and save directly (no merge) // Generate filename and save
filename := fmt.Sprintf("%s.pdf", invoiceNumber) filename := fmt.Sprintf("%s.pdf", data.Invoice.Title)
if err := gen.Save(filename); err != nil { if err := gen.Save(filename); err != nil {
return "", err return "", err
} }

View file

@ -12,8 +12,18 @@ foreach ($document['LineItem'] as $li) {
'item_number' => $li['item_number'], 'item_number' => $li['item_number'],
'quantity' => $li['quantity'], 'quantity' => $li['quantity'],
'title' => $li['title'], 'title' => $li['title'],
'description' => isset($li['description']) ? $li['description'] : '',
'unit_price' => floatval($li['gross_unit_price']), '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'], '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']))), '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'], 'ship_via' => $document['Invoice']['ship_via'],
'fob' => $document['Invoice']['fob'], 'fob' => $fob,
'issue_date' => $document['Invoice']['issue_date'], // expects YYYY-MM-DD 'issue_date' => $document['Invoice']['issue_date'], // expects YYYY-MM-DD
'issue_date_string' => $issue_date_string,
'currency_symbol' => $currencySymbol, 'currency_symbol' => $currencySymbol,
'currency_code' => $currencyCode,
'show_gst' => (bool)$gst, '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, 'line_items' => $lineItems,
'output_dir' => $outputDir 'output_dir' => $outputDir
); );