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