package pdf import ( "fmt" "math" "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", "") 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) // Set text color to blue g.pdf.SetTextColor(0, 0, 152) // Add logo if available (assuming logo is in static directory) logoPath := filepath.Join("static", "images", "cmclogosmall.png") // Try to add logo, but don't fail if it doesn't exist or isn't a proper image g.pdf.ImageOptions(logoPath, 10, 10, 0, 28, false, gofpdf.ImageOptions{ImageType: "PNG"}, 0, "http://www.cmctechnologies.com.au") // Company name g.pdf.SetFont("Helvetica", "B", 30) g.pdf.SetX(40) g.pdf.CellFormat(0, 0, g.headerText, "", 1, "C", false, 0, "") // 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, "") // Draw horizontal line g.pdf.SetDrawColor(0, 0, 0) g.pdf.Line(43, 24, 200, 24) // Contact details g.pdf.SetTextColor(0, 0, 0) g.pdf.SetY(32) // Left column - labels g.pdf.SetX(45) g.pdf.MultiCell(30, 5, "Phone:\nFax:\nEmail:\nWeb Site:", "", "L", false) // 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) // 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) // 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) // Footer line g.pdf.SetDrawColor(0, 0, 0) g.pdf.Line(10, g.pdf.GetY(), 200, g.pdf.GetY()) // 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, "") // Color-coded services g.pdf.SetY(-13) g.pdf.SetX(10) // First line of services services := []struct { text string 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}, } 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 } // 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) // Create details table lineHeight := 5.0 col1Width := 40.0 col2Width := 60.0 col3Width := 40.0 col4Width := 50.0 // 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, "") // 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, "") // 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, "") // 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, "") // 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) // 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) // Column widths itemWidth := 15.0 qtyWidth := 15.0 descWidth := 100.0 unitWidth := 30.0 totalWidth := 30.0 // 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, "") // Table rows g.pdf.SetFont("Helvetica", "", 9) 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) } 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, "") subtotal += item.TotalPrice } // Totals g.pdf.SetFont("Helvetica", "B", 10) // 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, "") // 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, "") // 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, "") } } // 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) } // 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 }