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

199 lines
5.8 KiB
Go

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
}