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

595 lines
18 KiB
Go
Raw Normal View History

2025-07-02 05:04:36 -07:00
package pdf
import (
"fmt"
2026-01-13 04:26:37 -08:00
"log"
"os"
"path/filepath"
2025-07-02 05:04:36 -07:00
"time"
"code.springupsoftware.com/cmc/cmc-sales/internal/cmc/db"
2026-01-13 04:26:37 -08:00
"github.com/pdfcpu/pdfcpu/pkg/api"
2025-07-02 05:04:36 -07:00
)
// QuotePDFData contains all data needed to generate a quote PDF
type QuotePDFData struct {
2026-01-12 03:00:48 -08:00
Document *db.Document
Quote interface{} // Quote specific data
Enquiry *db.Enquiry
Customer *db.Customer
EmailTo string
Attention string
User *db.GetUserRow
LineItems []db.GetLineItemsTableRow
Currency interface{} // Currency data
CurrencySymbol string
2026-01-12 05:21:57 -08:00
CurrencyCode string
2026-01-12 03:00:48 -08:00
ShowGST bool
2025-07-02 05:04:36 -07:00
CommercialComments string
2026-01-12 05:21:57 -08:00
DeliveryTime string
PaymentTerms string
DaysValid int
DeliveryPoint string
ExchangeRate string
CustomsDuty string
GSTPhrase string
SalesEngineer string
BillTo string
ShipTo string
IssueDateString string
2026-01-12 03:00:48 -08:00
Pages []string
2025-07-02 05:04:36 -07:00
}
// GenerateQuotePDF generates a PDF for a quote
func GenerateQuotePDF(data *QuotePDFData, outputDir string) (string, error) {
fmt.Printf("GenerateQuotePDF called with outputDir: %s\n", outputDir)
gen := NewGenerator(outputDir)
2026-01-12 03:00:48 -08:00
2025-07-02 05:04:36 -07:00
// First page with header
gen.AddPage()
gen.Page1Header()
2026-01-12 03:00:48 -08:00
2025-07-02 05:04:36 -07:00
// Extract data for details box
2026-01-12 03:00:48 -08:00
companyName := ""
2025-07-06 14:34:12 -07:00
if data.Customer != nil {
companyName = data.Customer.Name
}
2026-01-12 03:00:48 -08:00
emailTo := data.EmailTo
attention := data.Attention
2025-07-02 05:04:36 -07:00
fromName := fmt.Sprintf("%s %s", data.User.FirstName, data.User.LastName)
fromEmail := data.User.Email
2026-01-12 03:00:48 -08:00
2025-07-06 14:34:12 -07:00
// Use CMC reference as the quote number
quoteNumber := data.Document.CmcReference
2025-07-02 05:04:36 -07:00
if data.Document.Revision > 0 {
2025-07-06 14:34:12 -07:00
quoteNumber = fmt.Sprintf("%s.%d", quoteNumber, data.Document.Revision)
2025-07-02 05:04:36 -07:00
}
2026-01-12 03:00:48 -08:00
2025-07-02 05:04:36 -07:00
yourReference := fmt.Sprintf("Enquiry on %s", data.Document.Created.Format("2 Jan 2006"))
2026-01-12 05:21:57 -08:00
issueDate := data.IssueDateString
if issueDate == "" {
issueDate = data.Document.Created.Format("2 January 2006")
}
2026-01-12 03:00:48 -08:00
2025-07-02 05:04:36 -07:00
// Add details box
2025-07-06 14:34:12 -07:00
gen.DetailsBox("QUOTE", companyName, emailTo, attention, fromName, fromEmail, quoteNumber, yourReference, issueDate)
2026-01-12 03:00:48 -08:00
2025-07-02 05:04:36 -07:00
// Add page content if any
2026-01-12 03:00:48 -08:00
for i, content := range data.Pages {
if i == 0 {
// Content under first page header
gen.AddContent(content)
gen.Page1Footer()
} else {
gen.AddPage()
// Continued header
gen.pdf.SetFont("Helvetica", "B", 12)
gen.pdf.CellFormat(0, 6, "CONTINUED: "+data.Document.CmcReference, "", 1, "L", false, 0, "")
gen.pdf.Ln(4)
gen.AddContent(content)
}
}
if len(data.Pages) == 0 {
gen.Page1Footer()
}
2025-07-02 05:04:36 -07:00
// Add pricing page
gen.AddPage()
gen.pdf.SetFont("Helvetica", "B", 14)
gen.pdf.CellFormat(0, 10, "PRICING & SPECIFICATIONS", "", 1, "C", false, 0, "")
gen.pdf.Ln(5)
2026-01-12 03:00:48 -08:00
2025-07-02 05:04:36 -07:00
// Convert line items
pdfItems := make([]LineItem, len(data.LineItems))
for i, item := range data.LineItems {
unitPrice := 0.0
totalPrice := 0.0
2026-01-12 03:00:48 -08:00
2025-07-02 05:04:36 -07:00
// Parse prices
if item.GrossUnitPrice.Valid {
fmt.Sscanf(item.GrossUnitPrice.String, "%f", &unitPrice)
}
if item.GrossPrice.Valid {
fmt.Sscanf(item.GrossPrice.String, "%f", &totalPrice)
}
2026-01-12 03:00:48 -08:00
2025-07-02 05:04:36 -07:00
pdfItems[i] = LineItem{
ItemNumber: item.ItemNumber,
Quantity: item.Quantity,
Title: item.Title,
UnitPrice: unitPrice,
TotalPrice: totalPrice,
}
}
2026-01-12 03:00:48 -08:00
2025-07-02 05:04:36 -07:00
// Add line items table
gen.AddLineItemsTable(pdfItems, data.CurrencySymbol, data.ShowGST)
2026-01-12 03:00:48 -08:00
2025-07-02 05:04:36 -07:00
// Add commercial comments if any
if data.CommercialComments != "" {
gen.pdf.Ln(10)
gen.pdf.SetFont("Helvetica", "B", 10)
gen.pdf.CellFormat(0, 5, "COMMERCIAL COMMENTS", "", 1, "L", false, 0, "")
gen.pdf.SetFont("Helvetica", "", 9)
gen.pdf.MultiCell(0, 5, data.CommercialComments, "", "L", false)
}
2026-01-12 03:00:48 -08:00
2026-01-12 05:21:57 -08:00
// Add quote details table (delivery time, payment terms, etc.)
if data.DeliveryTime != "" || data.PaymentTerms != "" || data.DaysValid > 0 {
gen.AddQuoteDetailsTable(data)
}
2026-01-12 03:00:48 -08:00
// Add terms and conditions page
gen.AddTermsAndConditions()
2025-07-02 05:04:36 -07:00
// Generate filename
2025-07-06 14:34:12 -07:00
filename := quoteNumber
2025-07-02 05:04:36 -07:00
if data.Document.Revision > 0 {
2025-07-06 14:34:12 -07:00
filename = fmt.Sprintf("%s_%d.pdf", quoteNumber, data.Document.Revision)
2025-07-02 05:04:36 -07:00
} else {
2025-07-06 14:34:12 -07:00
filename = fmt.Sprintf("%s.pdf", quoteNumber)
2025-07-02 05:04:36 -07:00
}
2026-01-12 03:00:48 -08:00
2025-07-02 05:04:36 -07:00
// Save PDF
fmt.Printf("Saving PDF with filename: %s to outputDir: %s\n", filename, outputDir)
err := gen.Save(filename)
if err != nil {
fmt.Printf("Error saving PDF: %v\n", err)
} else {
fmt.Printf("PDF saved successfully: %s\n", filename)
}
return filename, err
}
// InvoicePDFData contains all data needed to generate an invoice PDF
type InvoicePDFData struct {
Document *db.Document
Invoice *db.Invoice
Enquiry *db.Enquiry
Customer *db.Customer
Job interface{} // Job data
LineItems []db.GetLineItemsTableRow
Currency interface{} // Currency data
CurrencySymbol string
CurrencyCode string
ShowGST bool
ShipVia string
FOB string
IssueDate time.Time
IssueDateString string
EmailTo string
Attention string
FromName string
FromEmail string
YourReference string
BillTo string
ShipTo string
ShippingDetails string
CustomerOrderNumber string
JobTitle string
PaymentTerms string
CustomerABN string
Subtotal interface{} // Can be float or "TBA"
GSTAmount interface{}
Total interface{}
2025-07-02 05:04:36 -07:00
}
// GenerateInvoicePDF generates a PDF for an invoice
func GenerateInvoicePDF(data *InvoicePDFData, outputDir string) (string, error) {
gen := NewGenerator(outputDir)
2026-01-12 03:00:48 -08:00
2025-07-02 05:04:36 -07:00
// First page with header
gen.AddPage()
gen.Page1Header()
2026-01-12 03:00:48 -08:00
// Title
gen.pdf.SetFont("Helvetica", "B", 18)
gen.pdf.SetTextColor(0, 0, 0)
gen.pdf.CellFormat(0, 10, "TAX INVOICE", "", 1, "C", false, 0, "")
2025-07-02 05:04:36 -07:00
gen.pdf.Ln(5)
2026-01-12 03:00:48 -08:00
// Sold To / Delivery Address boxes
gen.AddInvoiceAddressBoxes(data)
2026-01-12 03:00:48 -08:00
// More details table (Order Number, Job, FOB, Payment Terms, ABN)
gen.AddInvoiceDetailsTable(data)
2026-01-12 03:00:48 -08:00
2025-07-02 05:04:36 -07:00
gen.pdf.Ln(5)
2026-01-12 03:00:48 -08:00
// Line items table header
gen.AddInvoiceLineItemsHeader(data.CurrencyCode)
2026-01-12 03:00:48 -08:00
2026-01-13 04:26:37 -08:00
// Add line items content (handles page breaks internally)
gen.AddInvoiceLineItemsContent(data)
2026-01-12 03:00:48 -08:00
// Generate filename and save
filename := fmt.Sprintf("%s.pdf", data.Invoice.Title)
2026-01-13 04:26:37 -08:00
invoicePath := filepath.Join(outputDir, filename)
2026-01-12 03:00:48 -08:00
if err := gen.Save(filename); err != nil {
return "", err
}
2026-01-13 04:26:37 -08:00
// Merge with actual terms and conditions PDF using pdfcpu
termsPath := filepath.Join(outputDir, "CMC_terms_and_conditions2006_A4.pdf")
// Check if terms file exists
if _, err := os.Stat(termsPath); err == nil {
// Terms file exists, merge it
finalPath := invoicePath
tempPath := filepath.Join(outputDir, fmt.Sprintf("%s_merged_temp.pdf", data.Invoice.Title))
if err := MergePDFs(invoicePath, termsPath, tempPath); err != nil {
log.Printf("GenerateInvoicePDF: Warning - could not merge T&C PDF: %v. Returning invoice without T&C.", err)
// Continue with just the invoice if merge fails
return filename, nil
}
// Replace original with merged version
if err := os.Rename(tempPath, finalPath); err != nil {
log.Printf("GenerateInvoicePDF: Warning - could not replace file with merged version: %v", err)
// Continue with original if rename fails
return filename, nil
}
} else {
log.Printf("GenerateInvoicePDF: Warning - terms and conditions PDF not found at %s", termsPath)
}
2026-01-12 03:00:48 -08:00
return filename, nil
2025-07-02 05:04:36 -07:00
}
// PurchaseOrderPDFData contains all data needed to generate a purchase order PDF
type PurchaseOrderPDFData struct {
2026-01-12 05:21:57 -08:00
Document *db.Document
PurchaseOrder *db.PurchaseOrder
Principle *db.Principle
LineItems []db.GetLineItemsTableRow
Currency interface{} // Currency data
CurrencySymbol string
CurrencyCode string
ShowGST bool
Subtotal float64
GSTAmount float64
Total float64
IssueDateString string
2025-07-02 05:04:36 -07:00
}
// GeneratePurchaseOrderPDF generates a PDF for a purchase order
func GeneratePurchaseOrderPDF(data *PurchaseOrderPDFData, outputDir string) (string, error) {
gen := NewGenerator(outputDir)
2026-01-12 03:00:48 -08:00
2025-07-02 05:04:36 -07:00
// First page with header
gen.AddPage()
gen.Page1Header()
2026-01-12 03:00:48 -08:00
2025-07-02 05:04:36 -07:00
// Extract data for details box
companyName := data.Principle.Name
2026-01-12 03:00:48 -08:00
emailTo := "" // TODO: Get from principle contact
2025-07-02 05:04:36 -07:00
attention := "" // TODO: Get from principle contact
2026-01-12 03:00:48 -08:00
fromName := "" // TODO: Get from user
2025-07-02 05:04:36 -07:00
fromEmail := "" // TODO: Get from user
poNumber := data.PurchaseOrder.Title
2026-01-12 03:00:48 -08:00
2025-07-02 05:04:36 -07:00
yourReference := data.PurchaseOrder.PrincipleReference
2026-01-12 05:21:57 -08:00
issueDate := data.IssueDateString
if issueDate == "" {
issueDate = data.PurchaseOrder.IssueDate.Format("Monday, 2 January 2006")
}
2026-01-12 03:00:48 -08:00
2025-07-02 05:04:36 -07:00
// Add details box
gen.DetailsBox("PURCHASE ORDER", companyName, emailTo, attention, fromName, fromEmail, poNumber, yourReference, issueDate)
2026-01-12 03:00:48 -08:00
2025-07-02 05:04:36 -07:00
// Add PO specific details
gen.pdf.Ln(5)
gen.pdf.SetFont("Helvetica", "B", 10)
gen.pdf.CellFormat(40, 5, "Ordered From:", "", 0, "L", false, 0, "")
gen.pdf.SetFont("Helvetica", "", 10)
gen.pdf.MultiCell(0, 5, data.PurchaseOrder.OrderedFrom, "", "L", false)
2026-01-12 03:00:48 -08:00
2025-07-02 05:04:36 -07:00
gen.pdf.SetFont("Helvetica", "B", 10)
gen.pdf.CellFormat(40, 5, "Dispatch By:", "", 0, "L", false, 0, "")
gen.pdf.SetFont("Helvetica", "", 10)
gen.pdf.CellFormat(0, 5, data.PurchaseOrder.DispatchBy, "", 1, "L", false, 0, "")
2026-01-12 03:00:48 -08:00
2025-07-02 05:04:36 -07:00
gen.pdf.SetFont("Helvetica", "B", 10)
gen.pdf.CellFormat(40, 5, "Deliver To:", "", 0, "L", false, 0, "")
gen.pdf.SetFont("Helvetica", "", 10)
gen.pdf.MultiCell(0, 5, data.PurchaseOrder.DeliverTo, "", "L", false)
2026-01-12 03:00:48 -08:00
2025-07-02 05:04:36 -07:00
if data.PurchaseOrder.ShippingInstructions != "" {
gen.pdf.SetFont("Helvetica", "B", 10)
gen.pdf.CellFormat(0, 5, "Shipping Instructions:", "", 1, "L", false, 0, "")
gen.pdf.SetFont("Helvetica", "", 10)
gen.pdf.MultiCell(0, 5, data.PurchaseOrder.ShippingInstructions, "", "L", false)
}
2026-01-12 03:00:48 -08:00
2025-07-02 05:04:36 -07:00
gen.Page1Footer()
2026-01-12 03:00:48 -08:00
2025-07-02 05:04:36 -07:00
// Add line items page
gen.AddPage()
gen.pdf.SetFont("Helvetica", "B", 14)
gen.pdf.CellFormat(0, 10, "ORDER DETAILS", "", 1, "C", false, 0, "")
gen.pdf.Ln(5)
2026-01-12 03:00:48 -08:00
2025-07-02 05:04:36 -07:00
// Convert line items
pdfItems := make([]LineItem, len(data.LineItems))
for i, item := range data.LineItems {
unitPrice := 0.0
totalPrice := 0.0
2026-01-12 03:00:48 -08:00
2025-07-02 05:04:36 -07:00
// Parse prices
if item.GrossUnitPrice.Valid {
fmt.Sscanf(item.GrossUnitPrice.String, "%f", &unitPrice)
}
if item.GrossPrice.Valid {
fmt.Sscanf(item.GrossPrice.String, "%f", &totalPrice)
}
2026-01-12 03:00:48 -08:00
2025-07-02 05:04:36 -07:00
pdfItems[i] = LineItem{
ItemNumber: item.ItemNumber,
Quantity: item.Quantity,
Title: item.Title,
UnitPrice: unitPrice,
TotalPrice: totalPrice,
}
}
2026-01-12 03:00:48 -08:00
2026-01-12 05:21:57 -08:00
// Add line items table with totals
2025-07-02 05:04:36 -07:00
gen.AddLineItemsTable(pdfItems, data.CurrencySymbol, data.ShowGST)
2026-01-12 03:00:48 -08:00
2026-01-12 05:21:57 -08:00
// Add totals section
gen.pdf.Ln(5)
gen.pdf.SetFont("Helvetica", "B", 10)
gen.pdf.CellFormat(120, 5, "SUBTOTAL:", "T", 0, "R", false, 0, "")
gen.pdf.CellFormat(0, 5, fmt.Sprintf("%s%.2f", data.CurrencySymbol, data.Subtotal), "T", 1, "R", false, 0, "")
if data.ShowGST && data.GSTAmount > 0 {
gen.pdf.SetFont("Helvetica", "", 10)
gen.pdf.CellFormat(120, 5, "GST:", "", 0, "R", false, 0, "")
gen.pdf.CellFormat(0, 5, fmt.Sprintf("%s%.2f", data.CurrencySymbol, data.GSTAmount), "", 1, "R", false, 0, "")
}
gen.pdf.SetFont("Helvetica", "B", 10)
gen.pdf.CellFormat(120, 5, "TOTAL:", "T", 0, "R", false, 0, "")
gen.pdf.CellFormat(0, 5, fmt.Sprintf("%s%.2f", data.CurrencySymbol, data.Total), "T", 1, "R", false, 0, "")
2026-01-12 03:00:48 -08:00
// Add terms and conditions page
gen.AddTermsAndConditions()
2025-07-02 05:04:36 -07:00
// Generate filename
filename := poNumber
if data.Document.Revision > 0 {
filename = fmt.Sprintf("%s-Rev%d.pdf", data.PurchaseOrder.Title, data.Document.Revision)
} else {
filename = fmt.Sprintf("%s.pdf", data.PurchaseOrder.Title)
}
2026-01-12 03:00:48 -08:00
2025-07-02 05:04:36 -07:00
// Save PDF
err := gen.Save(filename)
return filename, err
2026-01-12 03:00:48 -08:00
}
// PackingListPDFData contains data for a packing list
type PackingListPDFData struct {
2026-01-12 05:21:57 -08:00
Document *db.Document
Customer *db.Customer
Title string
JobTitle string
IssueDate string
IssueDateString string
ShipVia string
FOB string
LineItems []db.GetLineItemsTableRow
CurrencySymbol string
CurrencyCode string
ShowGST bool
2026-01-12 03:00:48 -08:00
}
// GeneratePackingListPDF generates a PDF for a packing list
func GeneratePackingListPDF(data *PackingListPDFData, outputDir string) (string, error) {
gen := NewGenerator(outputDir)
// Header
gen.AddPage()
gen.Page1Header()
gen.pdf.SetFont("Helvetica", "B", 16)
gen.pdf.CellFormat(0, 10, "PACKING LIST", "", 1, "C", false, 0, "")
gen.pdf.Ln(5)
2026-01-12 05:21:57 -08:00
// Details box with proper title and date
issueDate := data.IssueDateString
if issueDate == "" {
issueDate = time.Now().Format("2 January 2006")
}
refNumber := data.Title
if refNumber == "" {
refNumber = fmt.Sprintf("PL-%d", data.Document.ID)
}
gen.DetailsBox("PACKING LIST", data.Customer.Name, "", "", "", "", refNumber, "", issueDate)
// Add shipping details section
gen.pdf.Ln(5)
if data.JobTitle != "" {
gen.pdf.SetFont("Helvetica", "B", 10)
gen.pdf.CellFormat(40, 5, "Job Reference:", "", 0, "L", false, 0, "")
gen.pdf.SetFont("Helvetica", "", 10)
gen.pdf.CellFormat(0, 5, data.JobTitle, "", 1, "L", false, 0, "")
}
if data.ShipVia != "" {
gen.pdf.SetFont("Helvetica", "B", 10)
gen.pdf.CellFormat(40, 5, "Ship Via:", "", 0, "L", false, 0, "")
gen.pdf.SetFont("Helvetica", "", 10)
gen.pdf.CellFormat(0, 5, data.ShipVia, "", 1, "L", false, 0, "")
}
if data.FOB != "" {
gen.pdf.SetFont("Helvetica", "B", 10)
gen.pdf.CellFormat(40, 5, "FOB:", "", 0, "L", false, 0, "")
gen.pdf.SetFont("Helvetica", "", 10)
gen.pdf.CellFormat(0, 5, data.FOB, "", 1, "L", false, 0, "")
}
2026-01-12 03:00:48 -08:00
gen.Page1Footer()
// Line items
gen.AddPage()
pdfItems := make([]LineItem, len(data.LineItems))
for i, 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)
}
pdfItems[i] = LineItem{ItemNumber: item.ItemNumber, Quantity: item.Quantity, Title: item.Title, UnitPrice: unitPrice, TotalPrice: totalPrice}
}
gen.AddLineItemsTable(pdfItems, data.CurrencySymbol, data.ShowGST)
// Add terms and conditions page
gen.AddTermsAndConditions()
2026-01-12 05:21:57 -08:00
filename := data.Title
if filename == "" {
filename = fmt.Sprintf("PL-%d", data.Document.ID)
}
filename = fmt.Sprintf("%s.pdf", filename)
2026-01-12 03:00:48 -08:00
err := gen.Save(filename)
return filename, err
}
// OrderAckPDFData contains data for an order acknowledgement
type OrderAckPDFData struct {
2026-01-12 05:21:57 -08:00
Document *db.Document
Customer *db.Customer
Title string
JobTitle string
IssueDate string
IssueDateString string
ShipVia string
FOB string
EstimatedDelivery string
LineItems []db.GetLineItemsTableRow
CurrencySymbol string
CurrencyCode string
ShowGST bool
2026-01-12 03:00:48 -08:00
}
// GenerateOrderAckPDF generates a PDF for an order acknowledgement
func GenerateOrderAckPDF(data *OrderAckPDFData, outputDir string) (string, error) {
gen := NewGenerator(outputDir)
// Header
gen.AddPage()
gen.Page1Header()
gen.pdf.SetFont("Helvetica", "B", 16)
gen.pdf.CellFormat(0, 10, "ORDER ACKNOWLEDGEMENT", "", 1, "C", false, 0, "")
gen.pdf.Ln(5)
2026-01-12 05:21:57 -08:00
// Details box with proper title and date
issueDate := data.IssueDateString
if issueDate == "" {
issueDate = time.Now().Format("2 January 2006")
}
refNumber := data.Title
if refNumber == "" {
refNumber = fmt.Sprintf("OA-%d", data.Document.ID)
}
gen.DetailsBox("ORDER ACK", data.Customer.Name, "", "", "", "", refNumber, "", issueDate)
// Add shipping/delivery details section
gen.pdf.Ln(5)
if data.JobTitle != "" {
gen.pdf.SetFont("Helvetica", "B", 10)
gen.pdf.CellFormat(40, 5, "Job Reference:", "", 0, "L", false, 0, "")
gen.pdf.SetFont("Helvetica", "", 10)
gen.pdf.CellFormat(0, 5, data.JobTitle, "", 1, "L", false, 0, "")
}
if data.ShipVia != "" {
gen.pdf.SetFont("Helvetica", "B", 10)
gen.pdf.CellFormat(40, 5, "Ship Via:", "", 0, "L", false, 0, "")
gen.pdf.SetFont("Helvetica", "", 10)
gen.pdf.CellFormat(0, 5, data.ShipVia, "", 1, "L", false, 0, "")
}
if data.FOB != "" {
gen.pdf.SetFont("Helvetica", "B", 10)
gen.pdf.CellFormat(40, 5, "FOB:", "", 0, "L", false, 0, "")
gen.pdf.SetFont("Helvetica", "", 10)
gen.pdf.CellFormat(0, 5, data.FOB, "", 1, "L", false, 0, "")
}
if data.EstimatedDelivery != "" {
gen.pdf.SetFont("Helvetica", "B", 10)
gen.pdf.CellFormat(40, 5, "Est. Delivery:", "", 0, "L", false, 0, "")
gen.pdf.SetFont("Helvetica", "", 10)
gen.pdf.CellFormat(0, 5, data.EstimatedDelivery, "", 1, "L", false, 0, "")
}
2026-01-12 03:00:48 -08:00
gen.Page1Footer()
// Line items
gen.AddPage()
pdfItems := make([]LineItem, len(data.LineItems))
for i, 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)
}
pdfItems[i] = LineItem{ItemNumber: item.ItemNumber, Quantity: item.Quantity, Title: item.Title, UnitPrice: unitPrice, TotalPrice: totalPrice}
}
gen.AddLineItemsTable(pdfItems, data.CurrencySymbol, data.ShowGST)
// Add terms and conditions page
gen.AddTermsAndConditions()
2026-01-12 05:21:57 -08:00
filename := data.Title
if filename == "" {
filename = fmt.Sprintf("OA-%d", data.Document.ID)
}
filename = fmt.Sprintf("%s.pdf", filename)
2026-01-12 03:00:48 -08:00
err := gen.Save(filename)
return filename, err
}
2026-01-13 04:26:37 -08:00
// MergePDFs merges an invoice PDF with a terms and conditions PDF
// outputPath is the combined PDF file path
func MergePDFs(invoicePath, termsPath, outputPath string) error {
// Merge the PDFs: invoice first, then terms
inFiles := []string{invoicePath, termsPath}
err := api.MergeCreateFile(inFiles, outputPath, false, nil)
if err != nil {
return fmt.Errorf("failed to merge PDFs: %w", err)
}
return nil
}