cmc-sales/go/internal/cmc/pdf/generator.go

622 lines
20 KiB
Go
Raw Normal View History

2025-07-02 05:04:36 -07:00
package pdf
import (
"fmt"
2026-01-13 03:13:06 -08:00
"math"
2025-07-02 05:04:36 -07:00
"path/filepath"
"github.com/jung-kurt/gofpdf"
)
// Generator handles PDF generation for documents
type Generator struct {
pdf *gofpdf.Fpdf
outputDir string
headerText string
footerText string
docRef string
currentPage int
}
// NewGenerator creates a new PDF generator
func NewGenerator(outputDir string) *Generator {
pdf := gofpdf.New("P", "mm", "A4", "")
2025-07-06 14:34:12 -07:00
2025-07-02 05:04:36 -07:00
return &Generator{
pdf: pdf,
outputDir: outputDir,
headerText: "CMC TECHNOLOGIES",
footerText: "Copyright © %d CMC Technologies. All rights reserved.",
}
}
// AddPage adds a new page to the PDF
func (g *Generator) AddPage() {
g.pdf.AddPage()
g.currentPage++
}
// Page1Header adds the standard header for page 1
func (g *Generator) Page1Header() {
g.pdf.SetY(10)
2025-07-06 14:34:12 -07:00
2025-07-02 05:04:36 -07:00
// Set text color to blue
g.pdf.SetTextColor(0, 0, 152)
2025-07-06 14:34:12 -07:00
2025-07-02 05:04:36 -07:00
// Add logo if available (assuming logo is in static directory)
2025-07-06 14:34:12 -07:00
logoPath := filepath.Join("static", "images", "cmclogosmall.png")
2025-07-02 05:04:36 -07:00
// Try to add logo, but don't fail if it doesn't exist or isn't a proper image
2025-07-06 14:34:12 -07:00
g.pdf.ImageOptions(logoPath, 10, 10, 0, 28, false, gofpdf.ImageOptions{ImageType: "PNG"}, 0, "http://www.cmctechnologies.com.au")
2025-07-02 05:04:36 -07:00
// Company name
g.pdf.SetFont("Helvetica", "B", 30)
g.pdf.SetX(40)
g.pdf.CellFormat(0, 0, g.headerText, "", 1, "C", false, 0, "")
2025-07-06 14:34:12 -07:00
2025-07-02 05:04:36 -07:00
// Company details
g.pdf.SetFont("Helvetica", "", 10)
g.pdf.SetY(22)
g.pdf.SetX(40)
g.pdf.CellFormat(0, 0, "PTY LIMITED ACN: 085 991 224 ABN: 47 085 991 224", "", 1, "C", false, 0, "")
2025-07-06 14:34:12 -07:00
2025-07-02 05:04:36 -07:00
// Draw horizontal line
g.pdf.SetDrawColor(0, 0, 0)
g.pdf.Line(43, 24, 200, 24)
2025-07-06 14:34:12 -07:00
2025-07-02 05:04:36 -07:00
// Contact details
g.pdf.SetTextColor(0, 0, 0)
g.pdf.SetY(32)
2025-07-06 14:34:12 -07:00
2025-07-02 05:04:36 -07:00
// Left column - labels
g.pdf.SetX(45)
g.pdf.MultiCell(30, 5, "Phone:\nFax:\nEmail:\nWeb Site:", "", "L", false)
2025-07-06 14:34:12 -07:00
2025-07-02 05:04:36 -07:00
// Middle column - values
g.pdf.SetY(32)
g.pdf.SetX(65)
g.pdf.SetFont("Helvetica", "", 10)
g.pdf.MultiCell(55, 5, "+61 2 9669 4000\n+61 2 9669 4111\nsales@cmctechnologies.com.au\nwww.cmctechnologies.net.au", "", "L", false)
2025-07-06 14:34:12 -07:00
2025-07-02 05:04:36 -07:00
// Right column - address
g.pdf.SetY(32)
g.pdf.SetX(150)
g.pdf.MultiCell(52, 5, "Unit 19, 77 Bourke Rd\nAlexandria NSW 2015\nAUSTRALIA", "", "L", false)
2025-07-06 14:34:12 -07:00
2025-07-02 05:04:36 -07:00
// Engineering text
g.pdf.SetTextColor(0, 0, 152)
g.pdf.SetFont("Helvetica", "B", 10)
g.pdf.SetY(37)
g.pdf.SetX(10)
g.pdf.MultiCell(30, 5, "Engineering &\nIndustrial\nInstrumentation", "", "L", false)
}
// Page1Footer adds the standard footer
func (g *Generator) Page1Footer() {
g.pdf.SetY(-20)
2025-07-06 14:34:12 -07:00
2025-07-02 05:04:36 -07:00
// Footer line
g.pdf.SetDrawColor(0, 0, 0)
g.pdf.Line(10, g.pdf.GetY(), 200, g.pdf.GetY())
2025-07-06 14:34:12 -07:00
2025-07-02 05:04:36 -07:00
// Footer text
g.pdf.SetFont("Helvetica", "", 9)
g.pdf.SetY(-18)
g.pdf.CellFormat(0, 5, "CMC TECHNOLOGIES Provides Solutions in the Following Fields", "", 1, "C", false, 0, "")
2025-07-06 14:34:12 -07:00
2025-07-02 05:04:36 -07:00
// Color-coded services
g.pdf.SetY(-13)
g.pdf.SetX(10)
2025-07-06 14:34:12 -07:00
2025-07-02 05:04:36 -07:00
// First line of services
services := []struct {
2025-07-06 14:34:12 -07:00
text string
2025-07-02 05:04:36 -07:00
r, g, b int
}{
{"EXPLOSION PREVENTION AND PROTECTION", 153, 0, 10},
{"—", 0, 0, 0},
{"FIRE PROTECTION", 255, 153, 0},
{"—", 0, 0, 0},
{"PRESSURE RELIEF", 255, 0, 25},
{"—", 0, 0, 0},
{"VISION IN THE PROCESS", 0, 128, 30},
}
2025-07-06 14:34:12 -07:00
2025-07-02 05:04:36 -07:00
x := 15.0
for _, service := range services {
g.pdf.SetTextColor(service.r, service.g, service.b)
width := g.pdf.GetStringWidth(service.text)
g.pdf.SetX(x)
g.pdf.CellFormat(width, 5, service.text, "", 0, "L", false, 0, "")
x += width + 1
}
2025-07-06 14:34:12 -07:00
2025-07-02 05:04:36 -07:00
// Second line of services
g.pdf.SetY(-8)
g.pdf.SetX(60)
g.pdf.SetTextColor(47, 75, 224)
g.pdf.CellFormat(0, 5, "FLOW MEASUREMENT", "", 0, "L", false, 0, "")
g.pdf.SetX(110)
g.pdf.SetTextColor(171, 49, 248)
g.pdf.CellFormat(0, 5, "—PROCESS INSTRUMENTATION", "", 0, "L", false, 0, "")
}
// DetailsBox adds the document details box (quote/invoice details)
func (g *Generator) DetailsBox(docType, companyName, emailTo, attention, fromName, fromEmail, refNumber, yourRef, issueDate string) {
g.pdf.SetY(60)
g.pdf.SetFont("Helvetica", "", 10)
g.pdf.SetTextColor(0, 0, 0)
2025-07-06 14:34:12 -07:00
2025-07-02 05:04:36 -07:00
// Create details table
lineHeight := 5.0
col1Width := 40.0
col2Width := 60.0
col3Width := 40.0
col4Width := 50.0
2025-07-06 14:34:12 -07:00
2025-07-02 05:04:36 -07:00
// Row 1
g.pdf.SetFont("Helvetica", "B", 10)
g.pdf.CellFormat(col1Width, lineHeight, "COMPANY NAME:", "1", 0, "L", false, 0, "")
g.pdf.SetFont("Helvetica", "", 10)
g.pdf.CellFormat(col2Width, lineHeight, companyName, "1", 0, "L", false, 0, "")
g.pdf.SetFont("Helvetica", "B", 10)
g.pdf.CellFormat(col3Width, lineHeight, docType+" NO.:", "1", 0, "L", false, 0, "")
g.pdf.SetFont("Helvetica", "", 10)
g.pdf.CellFormat(col4Width, lineHeight, refNumber, "1", 1, "L", false, 0, "")
2025-07-06 14:34:12 -07:00
2025-07-02 05:04:36 -07:00
// Row 2
g.pdf.SetFont("Helvetica", "B", 10)
g.pdf.CellFormat(col1Width, lineHeight, "EMAIL TO:", "1", 0, "L", false, 0, "")
g.pdf.SetFont("Helvetica", "", 10)
g.pdf.CellFormat(col2Width, lineHeight, emailTo, "1", 0, "L", false, 0, "")
g.pdf.SetFont("Helvetica", "B", 10)
g.pdf.CellFormat(col3Width, lineHeight, "YOUR REFERENCE:", "1", 0, "L", false, 0, "")
g.pdf.SetFont("Helvetica", "", 10)
g.pdf.CellFormat(col4Width, lineHeight, yourRef, "1", 1, "L", false, 0, "")
2025-07-06 14:34:12 -07:00
2025-07-02 05:04:36 -07:00
// Row 3
g.pdf.SetFont("Helvetica", "B", 10)
g.pdf.CellFormat(col1Width, lineHeight, "ATTENTION:", "1", 0, "L", false, 0, "")
g.pdf.SetFont("Helvetica", "", 10)
g.pdf.CellFormat(col2Width, lineHeight, attention, "1", 0, "L", false, 0, "")
g.pdf.SetFont("Helvetica", "B", 10)
g.pdf.CellFormat(col3Width, lineHeight, "ISSUE DATE:", "1", 0, "L", false, 0, "")
g.pdf.SetFont("Helvetica", "", 10)
g.pdf.CellFormat(col4Width, lineHeight, issueDate, "1", 1, "L", false, 0, "")
2025-07-06 14:34:12 -07:00
2025-07-02 05:04:36 -07:00
// Row 4
g.pdf.SetFont("Helvetica", "B", 10)
g.pdf.CellFormat(col1Width, lineHeight, "FROM:", "1", 0, "L", false, 0, "")
g.pdf.SetFont("Helvetica", "", 10)
g.pdf.CellFormat(col2Width, lineHeight, fromName, "1", 0, "L", false, 0, "")
g.pdf.SetFont("Helvetica", "B", 10)
g.pdf.CellFormat(col3Width, lineHeight, "PAGES:", "1", 0, "L", false, 0, "")
g.pdf.SetFont("Helvetica", "", 10)
pageText := fmt.Sprintf("%d of {nb}", g.pdf.PageNo())
g.pdf.CellFormat(col4Width, lineHeight, pageText, "1", 1, "L", false, 0, "")
2025-07-06 14:34:12 -07:00
2025-07-02 05:04:36 -07:00
// Row 5
g.pdf.SetFont("Helvetica", "B", 10)
g.pdf.CellFormat(col1Width, lineHeight, "EMAIL:", "1", 0, "L", false, 0, "")
g.pdf.SetFont("Helvetica", "", 10)
g.pdf.CellFormat(col2Width, lineHeight, fromEmail, "1", 0, "L", false, 0, "")
g.pdf.CellFormat(col3Width+col4Width, lineHeight, "", "1", 1, "L", false, 0, "")
}
// AddContent adds HTML content to the PDF
func (g *Generator) AddContent(content string) {
g.pdf.SetFont("Helvetica", "", 10)
g.pdf.SetTextColor(0, 0, 0)
g.pdf.SetY(g.pdf.GetY() + 10)
2025-07-06 14:34:12 -07:00
2025-07-02 05:04:36 -07:00
// Basic HTML to PDF conversion
// Note: gofpdf has limited HTML support, so we'll need to parse and convert manually
// For now, we'll just add the content as plain text
g.pdf.MultiCell(0, 5, content, "", "L", false)
}
// AddLineItemsTable adds a table of line items
func (g *Generator) AddLineItemsTable(items []LineItem, currencySymbol string, showGST bool) {
g.pdf.SetFont("Helvetica", "B", 10)
g.pdf.SetTextColor(0, 0, 0)
2025-07-06 14:34:12 -07:00
2025-07-02 05:04:36 -07:00
// Column widths
itemWidth := 15.0
qtyWidth := 15.0
descWidth := 100.0
unitWidth := 30.0
totalWidth := 30.0
2025-07-06 14:34:12 -07:00
2025-07-02 05:04:36 -07:00
// Table header
g.pdf.CellFormat(itemWidth, 7, "ITEM", "1", 0, "C", false, 0, "")
g.pdf.CellFormat(qtyWidth, 7, "QTY", "1", 0, "C", false, 0, "")
g.pdf.CellFormat(descWidth, 7, "DESCRIPTION", "1", 0, "C", false, 0, "")
g.pdf.CellFormat(unitWidth, 7, "UNIT PRICE", "1", 0, "C", false, 0, "")
g.pdf.CellFormat(totalWidth, 7, "TOTAL", "1", 1, "C", false, 0, "")
2025-07-06 14:34:12 -07:00
2025-07-02 05:04:36 -07:00
// Table rows
g.pdf.SetFont("Helvetica", "", 9)
2025-07-06 14:34:12 -07:00
2025-07-02 05:04:36 -07:00
subtotal := 0.0
for _, item := range items {
// Check if we need a new page
if g.pdf.GetY() > 250 {
g.AddPage()
g.pdf.SetFont("Helvetica", "B", 10)
g.pdf.CellFormat(itemWidth, 7, "ITEM", "1", 0, "C", false, 0, "")
g.pdf.CellFormat(qtyWidth, 7, "QTY", "1", 0, "C", false, 0, "")
g.pdf.CellFormat(descWidth, 7, "DESCRIPTION", "1", 0, "C", false, 0, "")
g.pdf.CellFormat(unitWidth, 7, "UNIT PRICE", "1", 0, "C", false, 0, "")
g.pdf.CellFormat(totalWidth, 7, "TOTAL", "1", 1, "C", false, 0, "")
g.pdf.SetFont("Helvetica", "", 9)
}
2025-07-06 14:34:12 -07:00
2025-07-02 05:04:36 -07:00
g.pdf.CellFormat(itemWidth, 6, item.ItemNumber, "1", 0, "C", false, 0, "")
g.pdf.CellFormat(qtyWidth, 6, item.Quantity, "1", 0, "C", false, 0, "")
g.pdf.CellFormat(descWidth, 6, item.Title, "1", 0, "L", false, 0, "")
g.pdf.CellFormat(unitWidth, 6, fmt.Sprintf("%s%.2f", currencySymbol, item.UnitPrice), "1", 0, "R", false, 0, "")
g.pdf.CellFormat(totalWidth, 6, fmt.Sprintf("%s%.2f", currencySymbol, item.TotalPrice), "1", 1, "R", false, 0, "")
2025-07-06 14:34:12 -07:00
2025-07-02 05:04:36 -07:00
subtotal += item.TotalPrice
}
2025-07-06 14:34:12 -07:00
2025-07-02 05:04:36 -07:00
// Totals
g.pdf.SetFont("Helvetica", "B", 10)
2025-07-06 14:34:12 -07:00
2025-07-02 05:04:36 -07:00
// Subtotal
g.pdf.SetX(g.pdf.GetX() + itemWidth + qtyWidth + descWidth)
g.pdf.CellFormat(unitWidth, 6, "SUBTOTAL:", "1", 0, "R", false, 0, "")
g.pdf.CellFormat(totalWidth, 6, fmt.Sprintf("%s%.2f", currencySymbol, subtotal), "1", 1, "R", false, 0, "")
2025-07-06 14:34:12 -07:00
2025-07-02 05:04:36 -07:00
// GST if applicable
if showGST {
gst := subtotal * 0.10
g.pdf.SetX(g.pdf.GetX() + itemWidth + qtyWidth + descWidth)
g.pdf.CellFormat(unitWidth, 6, "GST (10%):", "1", 0, "R", false, 0, "")
g.pdf.CellFormat(totalWidth, 6, fmt.Sprintf("%s%.2f", currencySymbol, gst), "1", 1, "R", false, 0, "")
2025-07-06 14:34:12 -07:00
2025-07-02 05:04:36 -07:00
// Total
total := subtotal + gst
g.pdf.SetX(g.pdf.GetX() + itemWidth + qtyWidth + descWidth)
g.pdf.CellFormat(unitWidth, 6, "TOTAL DUE:", "1", 0, "R", false, 0, "")
g.pdf.CellFormat(totalWidth, 6, fmt.Sprintf("%s%.2f", currencySymbol, total), "1", 1, "R", false, 0, "")
} else {
// Total without GST
g.pdf.SetX(g.pdf.GetX() + itemWidth + qtyWidth + descWidth)
g.pdf.CellFormat(unitWidth, 6, "TOTAL:", "1", 0, "R", false, 0, "")
g.pdf.CellFormat(totalWidth, 6, fmt.Sprintf("%s%.2f", currencySymbol, subtotal), "1", 1, "R", false, 0, "")
}
}
2026-01-13 03:13:06 -08:00
// AddInvoiceShippingBillingBox adds the shipping/billing box with payment details matching old TCPDF layout
func (g *Generator) AddInvoiceShippingBillingBox(data interface{}) {
invoiceData := data.(*InvoicePDFData)
// Column widths: 30%, 30%, 3% spacer, 37%
pageWidth := 210.0 // A4 width in mm
margin := 10.0
contentWidth := pageWidth - 2*margin // ~190mm
soldToWidth := contentWidth * 0.30 // ~57mm
deliveryWidth := contentWidth * 0.30 // ~57mm
spacerWidth := contentWidth * 0.03 // ~5.7mm
invoiceDetailsWidth := contentWidth * 0.37 // ~70mm
// Row 1: Headers with gray background
g.pdf.SetFillColor(242, 242, 242) // Light gray
g.pdf.SetFont("Helvetica", "B", 10)
g.pdf.SetDrawColor(0, 0, 0)
g.pdf.SetLineWidth(0.5)
// "Sold To / Invoice Address:"
g.pdf.CellFormat(soldToWidth, 8, "Sold To / Invoice Address:", "1", 0, "C", true, 0, "")
// "Delivery Address:"
g.pdf.CellFormat(deliveryWidth, 8, "Delivery Address:", "1", 0, "C", true, 0, "")
// Spacer (no border)
g.pdf.SetDrawColor(255, 255, 255)
g.pdf.CellFormat(spacerWidth, 8, "", "0", 0, "C", false, 0, "")
g.pdf.SetDrawColor(0, 0, 0)
// Right column header spanning 2 rows - "MAKE PAYMENT TO:"
g.pdf.CellFormat(invoiceDetailsWidth, 8, "MAKE PAYMENT TO:", "1", 1, "C", true, 0, "")
// Row 2: Address content
g.pdf.SetFont("Helvetica", "", 9)
g.pdf.SetFillColor(255, 255, 255) // White background
// Bill To address (left column)
billToLines := len(invoiceData.BillTo)
maxHeight := float64(billToLines) * 4.5
startX := g.pdf.GetX()
g.pdf.MultiCell(soldToWidth, 4.5, invoiceData.BillTo, "1", "L", false)
leftY := g.pdf.GetY()
// Ship To address (middle column)
g.pdf.SetXY(startX+soldToWidth, g.pdf.GetY()-maxHeight)
g.pdf.MultiCell(deliveryWidth, 4.5, invoiceData.ShipTo, "1", "L", false)
// Spacer
g.pdf.SetXY(startX+soldToWidth+deliveryWidth, g.pdf.GetY()-maxHeight)
g.pdf.SetDrawColor(255, 255, 255)
g.pdf.CellFormat(spacerWidth, maxHeight, "", "0", 0, "C", false, 0, "")
g.pdf.SetDrawColor(0, 0, 0)
// Invoice details and payment info (right column)
g.pdf.SetXY(startX+soldToWidth+deliveryWidth+spacerWidth, g.pdf.GetY()-maxHeight)
g.pdf.SetFont("Helvetica", "B", 9)
paymentText := "CMC INVOICE#:\n" + invoiceData.Invoice.Title + "\n\n"
paymentText += "Date:\n" + invoiceData.IssueDateString + "\n\n"
paymentText += "Page:\n1\n\n"
// Add currency-specific bank details
bankDetails := g.getBankDetails(invoiceData.CurrencyCode)
for k, v := range bankDetails {
paymentText += k + ":\n" + v + "\n\n"
}
g.pdf.SetFont("Helvetica", "", 8)
g.pdf.MultiCell(invoiceDetailsWidth, 3.5, paymentText, "1", "L", false)
// Move to next line after the tallest cell
maxY := math.Max(leftY, g.pdf.GetY())
g.pdf.SetXY(startX, maxY)
g.pdf.Ln(2)
}
// getBankDetails returns currency-specific bank account information
func (g *Generator) getBankDetails(currencyCode string) map[string]string {
details := make(map[string]string)
switch currencyCode {
case "EUR":
details["Account"] = "06200015682004"
details["Branch"] = "06200"
details["SWIFT"] = "CTBAAU2S"
case "GBP":
details["Account"] = "06200015642694"
details["Branch"] = "06200"
details["SWIFT"] = "CTBAAU2S"
case "USD":
details["Account"] = "06200015681984"
details["Branch"] = "06200"
details["SWIFT"] = "CTBAAU2S"
default: // AUD
details["BSB"] = "062-458"
details["Account"] = "10067982"
details["SWIFT"] = "CTBAAU2S"
details["IBAN"] = "06245810067982"
}
return details
}
// AddInvoiceDetailsTable adds the order number, job, FOB, payment terms, ABN table
func (g *Generator) AddInvoiceDetailsTable(data interface{}) {
invoiceData := data.(*InvoicePDFData)
pageWidth := 210.0
margin := 10.0
contentWidth := pageWidth - 2*margin
colWidth := contentWidth / 5 // 5 equal columns
g.pdf.Ln(3)
// Header row with gray background
g.pdf.SetFillColor(242, 242, 242)
g.pdf.SetFont("Helvetica", "B", 9)
g.pdf.SetDrawColor(0, 0, 0)
g.pdf.SetLineWidth(0.5)
headers := []string{
"CUSTOMER ORDER NO",
"CMC JOB #",
"INCOTERMS 2010",
"PAYMENT TERMS",
"CUSTOMER ABN",
}
for _, header := range headers {
g.pdf.CellFormat(colWidth, 6, header, "1", 0, "C", true, 0, "")
}
g.pdf.Ln()
// Data row
g.pdf.SetFont("Helvetica", "", 9)
g.pdf.SetFillColor(255, 255, 255)
rowData := []string{
invoiceData.CustomerOrderNumber,
invoiceData.Invoice.Title,
invoiceData.FOB,
invoiceData.PaymentTerms,
invoiceData.CustomerABN,
}
for _, value := range rowData {
g.pdf.CellFormat(colWidth, 6, value, "1", 0, "C", false, 0, "")
}
g.pdf.Ln(2)
}
// AddInvoiceLineItemsTableWithShipping adds line items table with freight and totals
func (g *Generator) AddInvoiceLineItemsTableWithShipping(data interface{}) {
invoiceData := data.(*InvoicePDFData)
pageWidth := 210.0
margin := 10.0
contentWidth := pageWidth - 2*margin
// Column widths (variable for item no, then equal for others)
itemNoWidth := contentWidth * 0.10 // Item number column
qtyWidth := contentWidth * 0.08 // Qty column
descWidth := contentWidth * 0.42 // Description (larger)
unitPriceWidth := contentWidth * 0.20 // Unit Price
totalPriceWidth := contentWidth * 0.20 // Total Price
g.pdf.Ln(2)
g.pdf.SetFont("Helvetica", "B", 9)
g.pdf.SetFillColor(242, 242, 242)
g.pdf.SetDrawColor(0, 0, 0)
g.pdf.SetLineWidth(0.5)
// Table header
g.pdf.CellFormat(itemNoWidth, 6, "ITEM NO.", "1", 0, "C", true, 0, "")
g.pdf.CellFormat(qtyWidth, 6, "QTY", "1", 0, "C", true, 0, "")
g.pdf.CellFormat(descWidth, 6, "DESCRIPTION", "1", 0, "C", true, 0, "")
g.pdf.CellFormat(unitPriceWidth, 6, "UNIT PRICE", "1", 0, "C", true, 0, "")
g.pdf.CellFormat(totalPriceWidth, 6, "TOTAL PRICE", "1", 1, "C", true, 0, "")
// Currency row
g.pdf.SetFont("Helvetica", "", 8)
g.pdf.SetFillColor(255, 255, 255)
g.pdf.CellFormat(itemNoWidth, 4, "", "1", 0, "C", false, 0, "")
g.pdf.CellFormat(qtyWidth, 4, "", "1", 0, "C", false, 0, "")
g.pdf.CellFormat(descWidth, 4, "", "1", 0, "C", false, 0, "")
g.pdf.CellFormat(unitPriceWidth, 4, invoiceData.CurrencyCode, "1", 0, "C", false, 0, "")
g.pdf.CellFormat(totalPriceWidth, 4, invoiceData.CurrencyCode, "1", 1, "C", false, 0, "")
// Line items
g.pdf.SetFont("Helvetica", "", 9)
startY := g.pdf.GetY()
for _, item := range invoiceData.LineItems {
// Item number
g.pdf.CellFormat(itemNoWidth, 5, item.ItemNumber, "1", 0, "L", false, 0, "")
// Quantity
g.pdf.CellFormat(qtyWidth, 5, item.Quantity, "1", 0, "C", false, 0, "")
// Title and description (multi-line)
descText := item.Title
if item.Description != "" {
descText += "\n" + item.Description
}
if item.Option {
descText = "Option: " + descText
}
// Calculate description cell height based on content
descLines := len([]rune(descText))/50 + 1 // Rough estimation
descHeight := float64(descLines) * 4.5
currentX := g.pdf.GetX()
currentY := g.pdf.GetY()
g.pdf.MultiCell(descWidth, 4.5, descText, "1", "L", false)
// Back up to same Y as item no to place price cells
newY := g.pdf.GetY()
g.pdf.SetXY(currentX+descWidth, currentY)
// Unit Price (with discount breakdown if applicable)
unitPrice := item.GrossUnitPrice.String
netPrice := item.NetUnitPrice.String
discountPercent := item.DiscountPercent.String
discountAmount := item.DiscountAmountUnit.String
priceText := unitPrice
if discountPercent != "" && discountPercent != "0" {
priceText += "\nless " + discountPercent + "%*"
if discountAmount != "" {
priceText += "\n(-" + discountAmount + ")"
}
priceText += "\n=\n" + netPrice
}
g.pdf.MultiCell(unitPriceWidth, 4.5, priceText, "1", "R", false)
// Back up again for total price
g.pdf.SetXY(currentX+descWidth+unitPriceWidth, currentY)
// Total Price (with discount breakdown if applicable)
totalPrice := item.GrossPrice.String
netTotalPrice := item.NetPrice.String
discountTotal := item.DiscountAmountTotal.String
totalText := totalPrice
if discountPercent != "" && discountPercent != "0" {
totalText += "\nless " + discountPercent + "%*"
if discountTotal != "" {
totalText += "\n(-" + discountTotal + ")"
}
totalText += "\n=\n" + netTotalPrice
}
g.pdf.MultiCell(totalPriceWidth, 4.5, totalText, "1", "R", false)
// Move to the bottom of all cells in this row
g.pdf.SetXY(margin, newY)
}
currentY := g.pdf.GetY()
// FREIGHT DETAILS row
g.pdf.SetFillColor(242, 242, 242)
g.pdf.SetFont("Helvetica", "B", 9)
g.pdf.CellFormat(itemNoWidth+qtyWidth+descWidth, 6, "FREIGHT DETAILS:", "1", 0, "C", true, 0, "")
g.pdf.CellFormat(unitPriceWidth, 6, "SUBTOTAL", "1", 0, "C", true, 0, "")
g.pdf.CellFormat(totalPriceWidth, 6, formatCurrency(invoiceData.Subtotal, invoiceData.CurrencySymbol), "1", 1, "R", true, 0, "")
// Shipping details row (rowspan=2)
g.pdf.SetFont("Helvetica", "", 8)
g.pdf.SetFillColor(255, 255, 255)
currentX := g.pdf.GetX()
currentY = g.pdf.GetY()
// Shipping details spanning 3 columns and 2 rows
shippingText := invoiceData.ShippingDetails
if shippingText == "" {
shippingText = "TBA"
}
g.pdf.MultiCell(itemNoWidth+qtyWidth+descWidth, 12, shippingText, "1", "L", false)
// Move back to add GST row next to shipping details
g.pdf.SetXY(currentX+itemNoWidth+qtyWidth+descWidth, currentY)
g.pdf.SetFillColor(242, 242, 242)
g.pdf.SetFont("Helvetica", "B", 9)
g.pdf.CellFormat(unitPriceWidth, 6, "GST", "1", 0, "C", true, 0, "")
g.pdf.CellFormat(totalPriceWidth, 6, formatCurrency(invoiceData.GSTAmount, invoiceData.CurrencySymbol), "1", 1, "R", true, 0, "")
// TOTAL row
g.pdf.SetFillColor(242, 242, 242)
g.pdf.SetFont("Helvetica", "B", 9)
g.pdf.CellFormat(itemNoWidth+qtyWidth+descWidth, 6, "", "0", 0, "C", false, 0, "")
g.pdf.CellFormat(unitPriceWidth, 6, "TOTAL", "1", 0, "C", true, 0, "")
totalDisplay := formatCurrency(invoiceData.Total, invoiceData.CurrencySymbol)
g.pdf.CellFormat(totalPriceWidth, 6, totalDisplay, "1", 1, "R", true, 0, "")
g.pdf.Ln(2)
}
// formatCurrency formats a float as currency string
func formatCurrency(amount float64, symbol string) string {
return fmt.Sprintf("%s%.2f", symbol, amount)
}
2025-07-02 05:04:36 -07:00
// Save saves the PDF to a file
func (g *Generator) Save(filename string) error {
g.pdf.AliasNbPages("")
outputPath := filepath.Join(g.outputDir, filename)
fmt.Printf("Generator.Save: Saving PDF to path: %s\n", outputPath)
err := g.pdf.OutputFileAndClose(outputPath)
if err != nil {
fmt.Printf("Generator.Save: Error saving PDF: %v\n", err)
} else {
fmt.Printf("Generator.Save: PDF saved successfully to: %s\n", outputPath)
}
return err
}
// LineItem represents a line item for the PDF
type LineItem struct {
ItemNumber string
Quantity string
Title string
UnitPrice float64
TotalPrice float64
2025-07-06 14:34:12 -07:00
}