1294 lines
42 KiB
Plaintext
1294 lines
42 KiB
Plaintext
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, " ", " ")
|
|
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, "®", "®")
|
|
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, "°", "°")
|
|
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
|
|
}
|