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.
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)

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)
}
// 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("")

View file

@ -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
}

View file

@ -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
);