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

1294 lines
42 KiB
Go

package pdf
import (
"context"
"fmt"
"os"
"path/filepath"
"regexp"
"strconv"
"strings"
"time"
"github.com/chromedp/chromedp"
"github.com/jung-kurt/gofpdf"
"golang.org/x/net/html"
"golang.org/x/net/html/atom"
)
// TextSegment represents a text segment with formatting information
type TextSegment struct {
Text string
Bold bool
Italic bool
}
// formatCurrency formats a float as currency with thousand separators
func formatCurrency(symbol string, amount float64) string {
// Format with 2 decimal places
amountStr := strconv.FormatFloat(amount, 'f', 2, 64)
// Split into integer and decimal parts
parts := strings.Split(amountStr, ".")
intPart := parts[0]
decPart := "." + parts[1]
// Add comma separators to integer part
var result strings.Builder
for i, ch := range intPart {
if i > 0 && (len(intPart)-i)%3 == 0 {
result.WriteString(",")
}
result.WriteRune(ch)
}
return symbol + result.String() + decPart
}
// renderHTMLToImage renders an HTML fragment to a PNG image using chromedp
func renderHTMLToImage(htmlContent string, width int) (string, float64, error) {
// Wrap HTML fragment in a document
fullHTML := fmt.Sprintf(`<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<style>
body {
margin: 0;
padding: 8px;
font-family: Helvetica, Arial, sans-serif;
font-size: 11px;
width: %dpx;
line-height: 1.4;
}
strong, b { font-weight: bold; }
em, i { font-style: italic; }
ul, ol { margin: 4px 0; padding-left: 20px; }
li { margin: 2px 0; }
p { margin: 4px 0; }
</style>
</head>
<body>%s</body>
</html>`, width, htmlContent)
// Create temp file for screenshot
tmpFile := filepath.Join(os.TempDir(), fmt.Sprintf("pdf_desc_%d.png", time.Now().UnixNano()))
// Create chromedp allocator with headless options
opts := append(chromedp.DefaultExecAllocatorOptions[:],
chromedp.Flag("headless", true),
chromedp.Flag("disable-gpu", true),
chromedp.Flag("no-sandbox", true),
chromedp.Flag("disable-dev-shm-usage", true),
chromedp.Flag("disable-software-rasterizer", true),
)
allocCtx, cancel := chromedp.NewExecAllocator(context.Background(), opts...)
defer cancel()
// Create chromedp context
ctx, cancel := chromedp.NewContext(allocCtx)
defer cancel()
// Set timeout
ctx, cancel = context.WithTimeout(ctx, 10*time.Second)
defer cancel()
var height int64
var buf []byte
// Run chromedp tasks
err := chromedp.Run(ctx,
chromedp.Navigate("about:blank"),
chromedp.ActionFunc(func(ctx context.Context) error {
return chromedp.Run(ctx,
chromedp.Evaluate(fmt.Sprintf(`document.write(%q)`, fullHTML), nil),
)
}),
chromedp.Sleep(100*time.Millisecond), // Wait for render
chromedp.Evaluate(`document.body.scrollHeight`, &height),
chromedp.CaptureScreenshot(&buf),
)
if err != nil {
return "", 0, fmt.Errorf("chromedp error: %w", err)
}
// Write screenshot to file
if err := os.WriteFile(tmpFile, buf, 0644); err != nil {
return "", 0, err
}
// Convert height from pixels to mm (assuming 96 DPI)
heightMM := float64(height) * 25.4 / 96.0
return tmpFile, heightMM, nil
}
// parseHTMLToSegments parses HTML using the DOM and extracts text segments with formatting
func parseHTMLToSegments(htmlStr string) []TextSegment {
segments := []TextSegment{}
// Parse HTML into a DOM tree
doc, err := html.Parse(strings.NewReader(htmlStr))
if err != nil {
// Fallback: treat as plain text if parsing fails
return []TextSegment{{Text: htmlStr, Bold: false, Italic: false}}
}
// Traverse the DOM tree and extract segments
var traverse func(*html.Node, bool, bool)
traverse = func(n *html.Node, bold, italic bool) {
if n == nil {
return
}
switch n.Type {
case html.TextNode:
text := strings.TrimSpace(n.Data)
if text != "" {
// Decode HTML entities
text = decodeEntities(text)
segments = append(segments, TextSegment{
Text: text,
Bold: bold,
Italic: italic,
})
}
case html.ElementNode:
// Determine if this element adds bold or italic
newBold := bold
newItalic := italic
addLineBreak := false
switch n.DataAtom {
case atom.Strong, atom.B:
newBold = true
case atom.Em, atom.I:
newItalic = true
case atom.Br:
segments = append(segments, TextSegment{Text: "\n", Bold: bold, Italic: italic})
return
case atom.P, atom.Div:
// Add line break after paragraph/div
addLineBreak = true
case atom.Li:
// Add bullet point for list items
segments = append(segments, TextSegment{Text: "• ", Bold: false, Italic: false})
}
// Recursively traverse children
for c := n.FirstChild; c != nil; c = c.NextSibling {
traverse(c, newBold, newItalic)
}
// Add line break after block elements
if addLineBreak {
segments = append(segments, TextSegment{Text: "\n", Bold: false, Italic: false})
}
}
}
traverse(doc, false, false)
// Clean up segments
cleanedSegments := []TextSegment{}
for _, seg := range segments {
// Collapse multiple newlines
text := seg.Text
for strings.Contains(text, "\n\n\n") {
text = strings.ReplaceAll(text, "\n\n\n", "\n\n")
}
// Trim leading/trailing whitespace (but preserve newlines)
text = strings.TrimRight(text, " \t")
text = strings.TrimLeft(text, " \t")
if text != "" {
cleanedSegments = append(cleanedSegments, TextSegment{
Text: text,
Bold: seg.Bold,
Italic: seg.Italic,
})
}
}
return cleanedSegments
}
// cleanHTMLLineBreaks converts HTML line breaks to newlines
func cleanHTMLLineBreaks(text string) string {
text = strings.ReplaceAll(text, "<br>", "\n")
text = strings.ReplaceAll(text, "<br/>", "\n")
text = strings.ReplaceAll(text, "<br />", "\n")
return text
}
// decodeEntities decodes HTML entities to their character equivalents
func decodeEntities(text string) string {
// Handle common HTML entities
text = strings.ReplaceAll(text, "&nbsp;", " ")
text = strings.ReplaceAll(text, "&amp;", "&")
text = strings.ReplaceAll(text, "&lt;", "<")
text = strings.ReplaceAll(text, "&gt;", ">")
text = strings.ReplaceAll(text, "&quot;", "\"")
text = strings.ReplaceAll(text, "&#39;", "'")
text = strings.ReplaceAll(text, "&apos;", "'")
text = strings.ReplaceAll(text, "&reg;", "®")
text = strings.ReplaceAll(text, "&#174;", "®")
text = strings.ReplaceAll(text, "&trade;", "™")
text = strings.ReplaceAll(text, "&#8482;", "™")
text = strings.ReplaceAll(text, "&copy;", "©")
text = strings.ReplaceAll(text, "&#169;", "©")
text = strings.ReplaceAll(text, "&#xB0;", "°")
text = strings.ReplaceAll(text, "&deg;", "°")
text = strings.ReplaceAll(text, "Ëš", "°")
text = strings.ReplaceAll(text, "°", "°")
text = strings.ReplaceAll(text, "Â", "")
text = strings.ReplaceAll(text, "•", "•")
text = strings.ReplaceAll(text, "’", "'")
text = strings.ReplaceAll(text, "“", "\"")
text = strings.ReplaceAll(text, "â€", "\"")
return text
}
// estimateHTMLHeight estimates the height needed for HTML content with formatting
func estimateHTMLHeight(pdf *gofpdf.Fpdf, html string, width float64, lineHeight float64) float64 {
// Parse to plain text to estimate line count
segments := parseHTMLToSegments(html)
currentWidth := 0.0
lineCount := 1.0
lineSpacing := 3.1
currentStyle := ""
for _, segment := range segments {
style := ""
if segment.Bold {
style += "B"
}
if segment.Italic {
style += "I"
}
if style != currentStyle {
pdf.SetFont("Helvetica", style, 9)
currentStyle = style
}
// Handle newlines
if strings.Contains(segment.Text, "\n") {
parts := strings.Split(segment.Text, "\n")
for i, part := range parts {
if i > 0 {
lineCount++
currentWidth = 0
}
if part != "" {
words := strings.Fields(part)
for _, word := range words {
wordWidth := pdf.GetStringWidth(word + " ")
if currentWidth+wordWidth > width && currentWidth > 0 {
lineCount++
currentWidth = wordWidth
} else {
currentWidth += wordWidth
}
}
}
}
} else {
words := strings.Fields(segment.Text)
for _, word := range words {
wordWidth := pdf.GetStringWidth(word + " ")
if currentWidth+wordWidth > width && currentWidth > 0 {
lineCount++
currentWidth = wordWidth
} else {
currentWidth += wordWidth
}
}
}
}
return lineCount * lineSpacing
}
// renderHTMLWithFormatting renders HTML content with rich formatting in a MultiCell
// Returns the height actually used by the content
func renderHTMLWithFormatting(pdf *gofpdf.Fpdf, html string, width float64, lineHeight float64, align string) float64 {
segments := parseHTMLToSegments(html)
startX := pdf.GetX()
currentX := startX
startY := pdf.GetY()
currentY := startY
maxWidth := width
lineSpacing := 3.1 // Proper line spacing to prevent overlapping
for _, segment := range segments {
// Handle newlines
if strings.Contains(segment.Text, "\n") {
parts := strings.Split(segment.Text, "\n")
for i, part := range parts {
if i > 0 {
currentY += lineSpacing
currentX = startX
pdf.SetXY(currentX, currentY)
}
if part != "" {
// Set font style
style := ""
if segment.Bold {
style += "B"
}
if segment.Italic {
style += "I"
}
pdf.SetFont("Helvetica", style, 9)
// Render text character by character for proper wrapping
for _, ch := range part {
charStr := string(ch)
charWidth := pdf.GetStringWidth(charStr)
if currentX+charWidth > startX+maxWidth {
currentY += lineSpacing
currentX = startX
pdf.SetXY(currentX, currentY)
}
pdf.CellFormat(charWidth, lineSpacing, charStr, "", 0, "L", false, 0, "")
currentX += charWidth
}
}
}
} else {
// Set font style
style := ""
if segment.Bold {
style += "B"
}
if segment.Italic {
style += "I"
}
pdf.SetFont("Helvetica", style, 9)
// Handle word wrapping
words := strings.Fields(segment.Text)
for wi, word := range words {
// Add space after word except for last word
if wi > 0 {
word = " " + word
}
if wi < len(words)-1 {
word = word + " "
} else if wi == len(words)-1 && wi > 0 {
word = word + " "
}
wordWidth := pdf.GetStringWidth(word)
if currentX+wordWidth > startX+maxWidth && currentX > startX {
currentY += lineSpacing
currentX = startX
pdf.SetXY(currentX, currentY)
// Remove leading space if we wrapped
word = strings.TrimLeft(word, " ")
wordWidth = pdf.GetStringWidth(word)
}
pdf.CellFormat(wordWidth, lineSpacing, word, "", 0, "L", false, 0, "")
currentX += wordWidth
}
}
}
// Set position to end of rendered content and return height used
pdf.SetXY(startX, currentY)
return currentY - startY + lineSpacing // Add one line height for proper spacing
}
// renderHTMLContent renders HTML content with proper formatting support
// Returns a string suitable for MultiCell, with formatting preserved through segments
func renderHTMLContent(pdf *gofpdf.Fpdf, htmlStr string, width float64) string {
segments := parseHTMLToSegments(htmlStr)
// Build formatted text by applying font changes inline
// This is a simplified approach - we'll just return plain text with newlines preserved
// The actual formatting (bold/italic) will be handled at render time
var result strings.Builder
for _, seg := range segments {
result.WriteString(seg.Text)
// Add space after non-newline segments for proper word spacing
if !strings.HasSuffix(seg.Text, "\n") && !strings.HasSuffix(seg.Text, " ") {
result.WriteString(" ")
}
}
text := strings.TrimSpace(result.String())
// Clean up multiple spaces and newlines
text = regexp.MustCompile(` +`).ReplaceAllString(text, " ")
text = regexp.MustCompile(`\n\n+`).ReplaceAllString(text, "\n")
text = regexp.MustCompile(`\n `).ReplaceAllString(text, "\n")
return text
}
// renderFormattedDescription renders HTML segments with proper formatting (bold/italic) within a cell
func (g *Generator) renderFormattedDescription(title string, htmlContent string, x, y, cellWidth, lineHeight float64) float64 {
fmt.Printf("=== renderFormattedDescription called: title=%s, html=%s ===\n", title, htmlContent)
// Build full HTML with title
fullHTML := fmt.Sprintf("<strong>%s</strong><br>%s", title, htmlContent)
// Convert cell width from mm to pixels (96 DPI)
widthPx := int(cellWidth * 96.0 / 25.4)
fmt.Printf("=== Calling renderHTMLToImage with width=%d ===\n", widthPx)
// Render HTML to image
imgPath, heightMM, err := renderHTMLToImage(fullHTML, widthPx)
if err != nil {
// No fallback - panic so we see the error
panic(fmt.Sprintf("chromedp render failed: %v", err))
}
defer os.Remove(imgPath) // Clean up temp file
fmt.Printf("=== Image rendered: path=%s, height=%.2fmm ===\n", imgPath, heightMM)
// Embed image in PDF
g.pdf.Image(imgPath, x, y, cellWidth, heightMM, false, "", 0, "")
return y + heightMM
}
// 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() {
// Add logo at top left (above the line)
logoPath := filepath.Join("static", "images", "CMC-Mobile-Logo.png")
g.pdf.ImageOptions(logoPath, 10, 10, 0, 28, false, gofpdf.ImageOptions{ImageType: "PNG"}, 0, "http://www.cmctechnologies.com.au")
// Company name centered
g.pdf.SetTextColor(0, 0, 152)
g.pdf.SetFont("Helvetica", "B", 30)
g.pdf.SetXY(10, 15)
g.pdf.CellFormat(190, 10, g.headerText, "", 1, "C", false, 0, "")
// Company details centered with more spacing
g.pdf.SetFont("Helvetica", "", 10)
g.pdf.SetXY(10, 26)
g.pdf.CellFormat(190, 5, "PTY LIMITED ACN: 085 991 224 ABN: 47 085 991 224", "", 1, "C", false, 0, "")
// Add padding before the line
lineY := 34.0
// Draw horizontal line
g.pdf.SetDrawColor(0, 0, 0)
g.pdf.Line(10, lineY, 200, lineY)
// Engineering text on LEFT below the line, beneath the logo
g.pdf.SetTextColor(0, 0, 152)
g.pdf.SetFont("Helvetica", "B", 9)
startY := lineY + 2
lineHeight := 4.0
g.pdf.SetXY(10, startY)
g.pdf.Cell(50, lineHeight, "Engineering &")
g.pdf.SetXY(10, startY+lineHeight)
g.pdf.Cell(50, lineHeight, "Industrial")
g.pdf.SetXY(10, startY+lineHeight*2)
g.pdf.Cell(50, lineHeight, "Instrumentation")
// Contact details on RIGHT side
g.pdf.SetTextColor(0, 0, 0)
g.pdf.SetFont("Helvetica", "", 9)
contactX := 120.0
// Phone
g.pdf.SetXY(contactX, startY)
g.pdf.Cell(15, lineHeight, "Phone:")
g.pdf.SetXY(contactX+15, startY)
g.pdf.Cell(50, lineHeight, "+61 2 9669 4000")
// Fax
g.pdf.SetXY(contactX, startY+lineHeight)
g.pdf.Cell(15, lineHeight, "Fax:")
g.pdf.SetXY(contactX+15, startY+lineHeight)
g.pdf.Cell(50, lineHeight, "+61 2 9669 4111")
// Email
g.pdf.SetXY(contactX, startY+lineHeight*2)
g.pdf.Cell(15, lineHeight, "Email:")
g.pdf.SetXY(contactX+15, startY+lineHeight*2)
g.pdf.Cell(50, lineHeight, "sales@cmctechnologies.com.au")
// Web Site
g.pdf.SetXY(contactX, startY+lineHeight*3)
g.pdf.Cell(15, lineHeight, "Web Site:")
g.pdf.SetXY(contactX+15, startY+lineHeight*3)
g.pdf.Cell(50, lineHeight, "www.cmctechnologies.net.au")
// Address on far RIGHT
g.pdf.SetXY(contactX, startY+lineHeight*5)
g.pdf.Cell(50, lineHeight, "Unit 19, 77 Bourke Rd")
g.pdf.SetXY(contactX, startY+lineHeight*6)
g.pdf.Cell(50, lineHeight, "Alexandria NSW 2015")
g.pdf.SetXY(contactX, startY+lineHeight*7)
g.pdf.Cell(50, lineHeight, "AUSTRALIA")
// Reset text color and set position for content below
g.pdf.SetTextColor(0, 0, 0)
g.pdf.SetY(68)
}
// 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)
}
currentY := g.pdf.GetY()
// Render the description with formatting using chromedp
descEndY := g.renderFormattedDescription(item.Title, item.Description, 40, currentY, descWidth, 5)
rowHeight := descEndY - currentY
if rowHeight < 6.0 {
rowHeight = 6.0
}
// Item number at X=10, width=15 - stretch to match description height
g.pdf.SetXY(10, currentY)
g.pdf.CellFormat(itemWidth, rowHeight, item.ItemNumber, "1", 0, "C", false, 0, "")
// Quantity at X=25 (10+15), width=15 - stretch to match description height
g.pdf.SetXY(25, currentY)
g.pdf.CellFormat(qtyWidth, rowHeight, item.Quantity, "1", 0, "C", false, 0, "")
// Description cell with border (drawing border only since we render content manually)
g.pdf.SetXY(40, currentY)
g.pdf.Rect(40, currentY, descWidth, rowHeight, "")
// Unit Price at X=140 (10+15+15+100), width=30 - stretch to match description height
g.pdf.SetXY(140, currentY)
g.pdf.CellFormat(unitWidth, rowHeight, fmt.Sprintf("%s%.2f", currencySymbol, item.UnitPrice), "1", 0, "R", false, 0, "")
// Total Price at X=170 (10+15+15+100+30), width=30 - stretch to match description height
g.pdf.SetXY(170, currentY)
g.pdf.CellFormat(totalWidth, rowHeight, fmt.Sprintf("%s%.2f", currencySymbol, item.TotalPrice), "1", 0, "R", false, 0, "")
// Move Y position for next row
g.pdf.SetY(currentY + rowHeight)
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.SetTextColor(171, 49, 248) // Purple/magenta color matching old format
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.SetTextColor(171, 49, 248) // Keep purple for body text
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)
g.pdf.SetTextColor(0, 0, 0) // Reset to black
}
// AddQuoteDetailsTable adds quote commercial details table (delivery time, payment terms, etc.)
func (g *Generator) AddQuoteDetailsTable(data *QuotePDFData) {
g.pdf.Ln(8)
g.pdf.SetFont("Helvetica", "B", 10)
g.pdf.CellFormat(0, 5, "QUOTE DETAILS", "", 1, "L", false, 0, "")
g.pdf.Ln(2)
// Create a simple table for quote details
g.pdf.SetFont("Helvetica", "", 9)
g.pdf.SetLeftMargin(15)
// Set column widths for a 3-column table
colWidth := 50.0
// Row 1: Delivery Time | Payment Terms | Days Valid
g.pdf.SetX(15)
g.pdf.SetLeftMargin(15)
if data.DeliveryTime != "" {
g.pdf.SetFont("Helvetica", "B", 8)
g.pdf.CellFormat(colWidth, 4, "Delivery Time:", "L", 0, "L", false, 0, "")
g.pdf.SetFont("Helvetica", "", 8)
g.pdf.CellFormat(colWidth, 4, data.DeliveryTime, "", 0, "L", false, 0, "")
g.pdf.SetFont("Helvetica", "B", 8)
g.pdf.CellFormat(colWidth, 4, "Payment Terms:", "", 0, "L", false, 0, "")
g.pdf.SetFont("Helvetica", "", 8)
g.pdf.MultiCell(0, 4, cleanHTMLLineBreaks(data.PaymentTerms), "R", "L", false)
}
// Row 2: Delivery Point | Days Valid
if data.DeliveryPoint != "" || data.DaysValid > 0 {
g.pdf.SetFont("Helvetica", "B", 8)
g.pdf.CellFormat(colWidth, 4, "Delivery Point:", "L", 0, "L", false, 0, "")
g.pdf.SetFont("Helvetica", "", 8)
g.pdf.CellFormat(colWidth, 4, data.DeliveryPoint, "", 0, "L", false, 0, "")
g.pdf.SetFont("Helvetica", "B", 8)
daysStr := ""
if data.DaysValid > 0 {
daysStr = fmt.Sprintf("%d days", data.DaysValid)
}
g.pdf.CellFormat(colWidth, 4, "Valid For:", "", 0, "L", false, 0, "")
g.pdf.SetFont("Helvetica", "", 8)
g.pdf.CellFormat(0, 4, daysStr, "R", 1, "L", false, 0, "")
}
g.pdf.SetLeftMargin(10)
}
// AddInvoiceAddressBoxes adds the Sold To / Delivery Address boxes for invoices
func (g *Generator) AddInvoiceAddressBoxes(data *InvoicePDFData) {
g.pdf.SetFont("Helvetica", "", 9)
// Start position
x := 10.0
y := g.pdf.GetY()
boxHeight := 30.0
// Left box: Sold To / Invoice Address header
g.pdf.SetXY(x, y)
g.pdf.SetFillColor(242, 242, 242)
g.pdf.SetFont("Helvetica", "B", 9)
g.pdf.CellFormat(58, 5, "Sold To / Invoice Address:", "1", 0, "L", true, 0, "")
// Middle box: Delivery Address header
g.pdf.CellFormat(58, 5, "Delivery Address:", "1", 0, "L", true, 0, "")
// Right side: Invoice number (only on first line)
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 - address content
y += 5
g.pdf.SetXY(x, y)
g.pdf.SetFont("Helvetica", "", 8)
// Sold To content with borders
g.pdf.MultiCell(58, 4, cleanHTMLLineBreaks(data.BillTo), "1", "L", false)
soldToEndY := g.pdf.GetY()
// Ship To content - positioned next to Sold To
g.pdf.SetXY(x+58, y)
g.pdf.MultiCell(58, 4, cleanHTMLLineBreaks(data.ShipTo), "1", "L", false)
shipToEndY := g.pdf.GetY()
// Calculate max end Y for right column positioning
maxEndY := soldToEndY
if shipToEndY > maxEndY {
maxEndY = shipToEndY
}
if maxEndY < y+boxHeight {
maxEndY = y + boxHeight
}
// Right column: Date and Page on first two lines
g.pdf.SetXY(x+122, y)
g.pdf.SetFont("Helvetica", "", 8)
g.pdf.Cell(0, 4, fmt.Sprintf("Date: %s", data.IssueDateString))
g.pdf.SetXY(x+122, y+4)
g.pdf.Cell(0, 4, "Page: 1 of {nb}")
// Right column: MAKE PAYMENT TO section
g.pdf.SetXY(x+122, y+10)
g.pdf.SetFont("Helvetica", "B", 8)
g.pdf.Cell(0, 4, "MAKE PAYMENT TO:")
// Bank details
g.pdf.SetFont("Helvetica", "", 7)
bankY := y + 14
bankLineHeight := 3.0
switch data.CurrencyCode {
case "EUR":
g.pdf.SetXY(x+122, bankY)
g.pdf.MultiCell(80, bankLineHeight, "Account Name: CMC Technologies Pty Ltd\nAccount Number/IBAN: 06200015682004\nBranch code: 06200\nSWIFT Code/BIC: CTBAAU2S", "", "L", false)
case "GBP":
g.pdf.SetXY(x+122, bankY)
g.pdf.MultiCell(80, bankLineHeight, "Account Name: CMC Technologies Pty Ltd\nAccount Number/IBAN: 06200015642694\nBranch code: 06200\nSWIFT Code/BIC: CTBAAU2S", "", "L", false)
case "USD":
g.pdf.SetXY(x+122, bankY)
g.pdf.MultiCell(80, bankLineHeight, "Account Name: CMC Technologies Pty Ltd\nAccount Number/IBAN: 06200015681984\nBranch code: 06200\nSWIFT Code/BIC: CTBAAU2S", "", "L", false)
default: // AUD and others
g.pdf.SetXY(x+122, bankY)
g.pdf.MultiCell(80, bankLineHeight, "Account Name: CMC Technologies Pty Ltd\nBSB: 062-458\nAccount Number: 10067982\nSWIFT Code: CTBAAU2S\nIBAN: 06245810067982", "", "L", false)
}
// Position for next section
g.pdf.SetY(maxEndY + 2)
}
// 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)
// Record starting Y for the row
rowStartY := g.pdf.GetY()
// Calculate payment terms height first
paymentTermsLines := g.pdf.SplitLines([]byte(data.PaymentTerms), colWidth-2)
paymentTermsHeight := float64(len(paymentTermsLines))*3.0 + 2.0 // Add padding
if paymentTermsHeight < 5 {
paymentTermsHeight = 5
}
// Draw all cells with the calculated height
g.pdf.CellFormat(colWidth, paymentTermsHeight, data.CustomerOrderNumber, "1", 0, "C", false, 0, "")
g.pdf.CellFormat(colWidth, paymentTermsHeight, data.JobTitle, "1", 0, "C", false, 0, "")
g.pdf.CellFormat(colWidth, paymentTermsHeight, data.FOB, "1", 0, "C", false, 0, "")
// Payment terms with wrapping
paymentTermsX := g.pdf.GetX()
g.pdf.CellFormat(colWidth, paymentTermsHeight, "", "1", 0, "C", false, 0, "")
g.pdf.SetXY(paymentTermsX+1, rowStartY+0.5)
g.pdf.MultiCell(colWidth-2, 3, data.PaymentTerms, "", "C", false)
g.pdf.SetXY(paymentTermsX+colWidth, rowStartY)
g.pdf.CellFormat(colWidth, paymentTermsHeight, 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)
g.pdf.SetTextColor(0, 0, 0)
firstPageBreak := false
// Line items
for i, item := range data.LineItems {
// Build description HTML with title and description
descriptionHTML := "<strong>" + item.Title + "</strong>"
if item.Description != "" {
// Use raw description - PHP should format it before sending
descriptionHTML += "<br>" + item.Description
}
// Check if row will fit on current page
estimatedHeight := estimateHTMLHeight(g.pdf, descriptionHTML, 98, 4)
if g.pdf.GetY()+estimatedHeight > 240 && i > 0 {
g.AddPage()
if !firstPageBreak {
g.pdf.SetFont("Helvetica", "B", 12)
g.pdf.CellFormat(0, 6, "CONTINUED: "+data.Invoice.Title, "", 1, "L", false, 0, "")
g.pdf.Ln(4)
firstPageBreak = true
}
// Re-draw header on new page
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, "")
g.pdf.SetFont("Helvetica", "", 9)
}
unitPrice := 0.0
totalPrice := 0.0
netUnitPrice := 0.0
netTotalPrice := 0.0
discountPercent := 0.0
discountAmountUnit := 0.0
discountAmountTotal := 0.0
unitPriceStr := ""
totalPriceStr := ""
if item.GrossUnitPrice.Valid && item.GrossUnitPrice.String != "" {
fmt.Sscanf(item.GrossUnitPrice.String, "%f", &unitPrice)
}
if item.GrossPrice.Valid && item.GrossPrice.String != "" {
fmt.Sscanf(item.GrossPrice.String, "%f", &totalPrice)
}
if item.NetUnitPrice.Valid && item.NetUnitPrice.String != "" {
fmt.Sscanf(item.NetUnitPrice.String, "%f", &netUnitPrice)
}
if item.NetPrice.Valid && item.NetPrice.String != "" {
fmt.Sscanf(item.NetPrice.String, "%f", &netTotalPrice)
}
if item.DiscountPercent.Valid && item.DiscountPercent.String != "" {
fmt.Sscanf(item.DiscountPercent.String, "%f", &discountPercent)
}
if item.DiscountAmountUnit.Valid && item.DiscountAmountUnit.String != "" {
fmt.Sscanf(item.DiscountAmountUnit.String, "%f", &discountAmountUnit)
}
if item.DiscountAmountTotal.Valid && item.DiscountAmountTotal.String != "" {
fmt.Sscanf(item.DiscountAmountTotal.String, "%f", &discountAmountTotal)
}
// Calculate net prices if not provided
if netUnitPrice == 0 && unitPrice > 0 {
netUnitPrice = unitPrice - discountAmountUnit
}
if netTotalPrice == 0 && totalPrice > 0 {
netTotalPrice = totalPrice - discountAmountTotal
}
// Build unit price string with discount formatting
if discountPercent > 0 || discountAmountUnit > 0 {
unitPriceStr = fmt.Sprintf("%s\nless\n%.2f%%\ndiscount*\n(-%s)\n=\n%s",
formatCurrency(data.CurrencySymbol, unitPrice),
discountPercent,
formatCurrency("", discountAmountUnit),
formatCurrency(data.CurrencySymbol, netUnitPrice))
} else {
unitPriceStr = formatCurrency(data.CurrencySymbol, unitPrice)
}
// Build total price string with discount formatting
if discountPercent > 0 || discountAmountTotal > 0 {
totalPriceStr = fmt.Sprintf("%s\nless\n%.2f%%\ndiscount*\n(-%s)\n=\n%s",
formatCurrency(data.CurrencySymbol, totalPrice),
discountPercent,
formatCurrency("", discountAmountTotal),
formatCurrency(data.CurrencySymbol, netTotalPrice))
} else {
totalPriceStr = formatCurrency(data.CurrencySymbol, totalPrice)
}
// Record starting Y position
startY := g.pdf.GetY()
// Calculate heights for all columns WITHOUT rendering
// Description height - count lines in HTML
plainDesc := descriptionHTML
re := regexp.MustCompile(`<[^>]*>`)
plainDesc = re.ReplaceAllString(plainDesc, "")
descLines := strings.Count(plainDesc, "\n") + 1
if descLines < 1 {
descLines = 1
}
descriptionHeight := float64(descLines) * 3.1
// Price columns heights
unitPriceLines := g.pdf.SplitLines([]byte(unitPriceStr), 28)
unitPriceTextHeight := float64(len(unitPriceLines))*3.1 + 0.5
totalPriceLines := g.pdf.SplitLines([]byte(totalPriceStr), 28)
totalPriceTextHeight := float64(len(totalPriceLines))*3.1 + 0.5
// Use the maximum height from all columns
rowHeight := descriptionHeight + 1.5 // Add minimal padding to description
if unitPriceTextHeight > rowHeight {
rowHeight = unitPriceTextHeight
}
if totalPriceTextHeight > rowHeight {
rowHeight = totalPriceTextHeight
}
if rowHeight < 5 {
rowHeight = 5
}
// Now draw all cells with borders at the correct row height
// Draw item number at X=10, width=15
g.pdf.SetXY(10, startY)
g.pdf.CellFormat(15, rowHeight, item.ItemNumber, "1", 0, "C", false, 0, "")
// Draw quantity at X=25 (10+15), width=15
g.pdf.SetXY(25, startY)
g.pdf.CellFormat(15, rowHeight, item.Quantity, "1", 0, "C", false, 0, "")
// Draw description border at X=40, width=100
g.pdf.SetXY(40, startY)
g.pdf.CellFormat(100, rowHeight, "", "1", 0, "L", false, 0, "")
// Render description using chromedp
g.pdf.SetXY(41, startY+1)
_ = g.renderFormattedDescription(item.Title, item.Description, 41, startY+1, 98, 3.1)
// Reset to normal font
g.pdf.SetFont("Helvetica", "", 9)
// Recalculate vertical centering offset for price text with new row height
unitPriceTextHeight = float64(len(unitPriceLines)) * 3.1
unitPriceOffset := (rowHeight - unitPriceTextHeight) / 2
if unitPriceOffset < 0.5 {
unitPriceOffset = 0.5
}
totalPriceTextHeight = float64(len(totalPriceLines)) * 3.1
totalPriceOffset := (rowHeight - totalPriceTextHeight) / 2
if totalPriceOffset < 0.5 {
totalPriceOffset = 0.5
}
// Draw unit price at X=140 (10+15+15+100), width=30
g.pdf.SetXY(140, startY)
g.pdf.CellFormat(30, rowHeight, "", "1", 0, "C", false, 0, "")
g.pdf.SetXY(141, startY+unitPriceOffset)
g.pdf.SetFont("Helvetica", "", 9)
g.pdf.MultiCell(28, 3.1, unitPriceStr, "", "C", false)
// Draw total price at X=170 (10+15+15+100+30), width=30
g.pdf.SetXY(170, startY)
g.pdf.CellFormat(30, rowHeight, "", "1", 0, "C", false, 0, "")
g.pdf.SetXY(171, startY+totalPriceOffset)
g.pdf.SetFont("Helvetica", "", 9)
g.pdf.MultiCell(28, 3.1, totalPriceStr, "", "C", false)
// Position cursor for next row - CRITICAL to avoid overlapping cells
g.pdf.SetXY(10, startY+rowHeight)
}
// Freight details and totals
g.pdf.SetFillColor(242, 242, 242)
g.pdf.SetFont("Helvetica", "B", 9)
// Record starting Y for freight section
y := g.pdf.GetY()
// Freight details header (left side)
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, "")
// GST row (right side)
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 (right side)
g.pdf.SetXY(140, y+10)
g.pdf.SetFont("Helvetica", "B", 9)
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, "")
// Calculate height of freight section (from y to bottom of total due)
totalDueEndY := y + 15 // Header (5) + GST (5) + Total (5)
freightDetailsHeight := 10.0 // Should span GST + Total rows
// Draw shipping details box on left side with proper height
g.pdf.SetFont("Helvetica", "", 8)
g.pdf.SetXY(10, y+5)
// Draw the border box first
g.pdf.CellFormat(130, freightDetailsHeight, "", "1", 0, "L", false, 0, "")
// Then draw the text inside without border
g.pdf.SetXY(11, y+6) // Slight offset for padding
g.pdf.MultiCell(128, 4, cleanHTMLLineBreaks(data.ShippingDetails), "", "L", false)
// Make sure we're past the freight section
g.pdf.SetY(totalDueEndY)
}
// 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
Description string
UnitPrice float64
TotalPrice float64
DiscountPercent float64
DiscountAmountUnit float64
DiscountAmountTotal float64
}