package pdf import ( "fmt" "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, "") } } // AddTermsAndConditions adds a T&C page at the end of the PDF // This renders a simple text page indicating T&Cs apply func (g *Generator) AddTermsAndConditions() { g.AddPage() g.pdf.SetFont("Helvetica", "B", 14) g.pdf.SetY(20) g.pdf.CellFormat(0, 10, "TERMS AND CONDITIONS", "", 1, "C", false, 0, "") g.pdf.Ln(5) g.pdf.SetFont("Helvetica", "", 9) g.pdf.SetLeftMargin(15) g.pdf.SetRightMargin(15) // Add disclaimer text disclaimerText := `These Terms and Conditions apply to all quotations, invoices, purchase orders, and other documents issued by CMC TECHNOLOGIES. By accepting this document, you agree to be bound by these terms. Payment Terms: Invoices are due within 30 days of issue unless otherwise agreed in writing. Delivery: Delivery dates are estimates only and not guaranteed. CMC TECHNOLOGIES is not liable for delays in delivery. Intellectual Property: All designs, drawings, and specifications remain the property of CMC TECHNOLOGIES unless otherwise agreed. Limitation of Liability: CMC TECHNOLOGIES shall not be liable for any indirect, incidental, or consequential damages arising out of or related to this document or transaction. Entire Agreement: This document and any attached terms constitute the entire agreement between the parties. For full terms and conditions, please refer to our website or contact CMC TECHNOLOGIES directly.` 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("") 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 }