package pdf import ( "bytes" "database/sql" "fmt" "html/template" "path/filepath" "strconv" "strings" ) // BuildInvoiceHTML generates the complete HTML for an invoice using templates func (g *HTMLDocumentGenerator) BuildInvoiceHTML(data *InvoicePDFData, totalPages int, currentPage int) string { // Get invoice number, fall back to invoice title so the template always shows something meaningful invoiceNum := "" if data.Document != nil { invoiceNum = data.Document.CmcReference } if invoiceNum == "" { invoiceNum = data.Invoice.Title } fmt.Printf("=== buildInvoiceHTML: Invoice number: %s ===\n", invoiceNum) // Prepare template data templateData := struct { InvoiceNumber string IssueDateString string BillTo template.HTML ShipTo template.HTML CustomerOrderNumber string JobTitle string FOB string PaymentTerms string CustomerABN string CurrencyCode string CurrencySymbol string LineItems []LineItemTemplateData Subtotal interface{} GSTAmount interface{} Total interface{} ShowGST bool PageCount int CurrentPage int LogoDataURI string }{ InvoiceNumber: invoiceNum, IssueDateString: data.IssueDateString, BillTo: template.HTML(data.BillTo), ShipTo: template.HTML(data.ShipTo), CustomerOrderNumber: data.CustomerOrderNumber, JobTitle: data.JobTitle, FOB: data.FOB, PaymentTerms: data.PaymentTerms, CustomerABN: data.CustomerABN, CurrencyCode: data.CurrencyCode, CurrencySymbol: data.CurrencySymbol, Subtotal: data.Subtotal, GSTAmount: data.GSTAmount, Total: data.Total, ShowGST: data.ShowGST, PageCount: totalPages, CurrentPage: currentPage, LogoDataURI: g.loadLogo("invoice_logo.png"), } // Convert line items to template format for _, item := range data.LineItems { templateData.LineItems = append(templateData.LineItems, LineItemTemplateData{ Title: item.Title, Description: template.HTML(item.Description), // Allow HTML in description Quantity: item.Quantity, GrossUnitPrice: item.GrossUnitPrice, DiscountAmountTotal: item.DiscountAmountTotal, GrossPrice: item.GrossPrice, }) } // Define template functions funcMap := template.FuncMap{ "formatPrice": func(price sql.NullString) template.HTML { if !price.Valid || price.String == "" { return "" } formatted := FormatPriceWithCommas(price.String) return template.HTML(fmt.Sprintf("%s%s", data.CurrencySymbol, formatted)) }, "formatDiscount": func(discount sql.NullString) template.HTML { if !discount.Valid || discount.String == "" { return template.HTML(fmt.Sprintf("%s0.00", data.CurrencySymbol)) } if discount.String == "0" || discount.String == "0.00" { return template.HTML(fmt.Sprintf("%s0.00", data.CurrencySymbol)) } formatted := FormatPriceWithCommas(discount.String) return template.HTML(fmt.Sprintf("-%s%s", data.CurrencySymbol, formatted)) }, "formatTotal": func(amount interface{}) template.HTML { if amount == nil { return "" } if val, ok := amount.(float64); ok { formatted := FormatPriceWithCommas(fmt.Sprintf("%.2f", val)) return template.HTML(fmt.Sprintf("%s%s", data.CurrencySymbol, formatted)) } return template.HTML(fmt.Sprintf("%v", amount)) }, } // Parse and execute template // Try multiple possible path sets to find the invoice and shared header templates together possiblePathSets := [][]string{ { filepath.Join("internal", "cmc", "pdf", "templates", "invoice.html"), filepath.Join("internal", "cmc", "pdf", "templates", "header.html"), }, { filepath.Join("go", "internal", "cmc", "pdf", "templates", "invoice.html"), filepath.Join("go", "internal", "cmc", "pdf", "templates", "header.html"), }, { filepath.Join("templates", "invoice.html"), filepath.Join("templates", "header.html"), }, { "/app/templates/invoice.html", "/app/templates/header.html", }, { "/app/go/internal/cmc/pdf/templates/invoice.html", "/app/go/internal/cmc/pdf/templates/header.html", }, } var tmpl *template.Template var err error for _, pathSet := range possiblePathSets { tmpl, err = template.New("invoice.html").Funcs(funcMap).ParseFiles(pathSet...) if err == nil { break } } if tmpl == nil || err != nil { fmt.Printf("Error parsing template from any path: %v\n", err) return "" } var buf bytes.Buffer if err := tmpl.Execute(&buf, templateData); err != nil { fmt.Printf("Error executing template: %v\n", err) return "" } return buf.String() } // LineItemTemplateData represents a line item for template rendering type LineItemTemplateData struct { Title string Description template.HTML Quantity string GrossUnitPrice sql.NullString DiscountAmountTotal sql.NullString GrossPrice sql.NullString } // FormatPriceWithCommas formats a price string with comma separators func FormatPriceWithCommas(priceStr string) string { if priceStr == "" { return "" } // Try to parse as float price, err := strconv.ParseFloat(priceStr, 64) if err != nil { // Return as-is if not parseable return priceStr } // Format with 2 decimal places and commas amountStr := strconv.FormatFloat(price, 'f', 2, 64) parts := strings.Split(amountStr, ".") intPart := parts[0] decPart := "." + parts[1] // Add comma separators var result strings.Builder for i, ch := range intPart { if i > 0 && (len(intPart)-i)%3 == 0 { result.WriteString(",") } result.WriteRune(ch) } return result.String() + decPart }