2026-01-16 04:26:56 -08:00
|
|
|
package pdf
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
"context"
|
|
|
|
|
"fmt"
|
|
|
|
|
"io/ioutil"
|
2026-01-18 00:53:46 -08:00
|
|
|
"log"
|
2026-01-16 04:26:56 -08:00
|
|
|
"os"
|
|
|
|
|
"path/filepath"
|
|
|
|
|
"time"
|
|
|
|
|
|
|
|
|
|
"github.com/chromedp/cdproto/page"
|
|
|
|
|
"github.com/chromedp/chromedp"
|
|
|
|
|
"github.com/pdfcpu/pdfcpu/pkg/api"
|
|
|
|
|
)
|
|
|
|
|
|
2026-01-16 20:41:24 -08:00
|
|
|
// HTMLDocumentGenerator generates PDF documents from HTML templates using chromedp
|
|
|
|
|
type HTMLDocumentGenerator struct {
|
2026-01-16 04:26:56 -08:00
|
|
|
outputDir string
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-16 20:41:24 -08:00
|
|
|
// NewHTMLDocumentGenerator creates a new HTML-based document generator
|
|
|
|
|
func NewHTMLDocumentGenerator(outputDir string) *HTMLDocumentGenerator {
|
|
|
|
|
return &HTMLDocumentGenerator{
|
2026-01-16 04:26:56 -08:00
|
|
|
outputDir: outputDir,
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// GenerateInvoicePDF creates a PDF invoice from HTML template
|
2026-01-16 20:41:24 -08:00
|
|
|
// Returns (filename, error)
|
|
|
|
|
func (g *HTMLDocumentGenerator) GenerateInvoicePDF(data *InvoicePDFData) (string, error) {
|
2026-01-16 04:26:56 -08:00
|
|
|
fmt.Println("=== HTML Generator: Starting invoice generation (two-pass with T&C page count) ===")
|
|
|
|
|
|
|
|
|
|
// FIRST PASS: Generate PDF without page numbers to determine total pages (including T&C)
|
|
|
|
|
fmt.Println("=== HTML Generator: First pass - generating without page count ===")
|
2026-01-16 20:41:24 -08:00
|
|
|
html := g.BuildInvoiceHTML(data, 0, 0)
|
2026-01-16 04:26:56 -08:00
|
|
|
|
|
|
|
|
fmt.Printf("=== HTML Generator: Generated %d bytes of HTML ===\n", len(html))
|
|
|
|
|
|
|
|
|
|
tempHTML := filepath.Join(g.outputDir, "temp_invoice.html")
|
|
|
|
|
if err := ioutil.WriteFile(tempHTML, []byte(html), 0644); err != nil {
|
|
|
|
|
return "", fmt.Errorf("failed to write temp HTML: %w", err)
|
|
|
|
|
}
|
|
|
|
|
defer os.Remove(tempHTML)
|
|
|
|
|
defer os.Remove(filepath.Join(g.outputDir, "invoice_logo.png")) // Clean up temp logo
|
|
|
|
|
defer os.Remove(filepath.Join(g.outputDir, "temp_logo.png")) // Clean up temp logo
|
|
|
|
|
|
|
|
|
|
// Generate temp PDF
|
|
|
|
|
tempPDFPath := filepath.Join(g.outputDir, "temp_invoice_first_pass.pdf")
|
|
|
|
|
if err := g.htmlToPDF(tempHTML, tempPDFPath); err != nil {
|
|
|
|
|
return "", fmt.Errorf("failed to convert HTML to PDF (first pass): %w", err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Get initial page count from invoice
|
|
|
|
|
invoicePageCount, err := g.getPageCount(tempPDFPath)
|
|
|
|
|
if err != nil {
|
|
|
|
|
fmt.Printf("Warning: Could not extract invoice page count: %v\n", err)
|
|
|
|
|
invoicePageCount = 1
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Check if T&C exists and merge to get total page count
|
|
|
|
|
totalPageCount := invoicePageCount
|
|
|
|
|
termsPath := filepath.Join(g.outputDir, "CMC_terms_and_conditions2006_A4.pdf")
|
|
|
|
|
tempMergedPath := filepath.Join(g.outputDir, fmt.Sprintf("%s_merged_first_pass.pdf", data.Invoice.Title))
|
|
|
|
|
|
|
|
|
|
if _, err := os.Stat(termsPath); err == nil {
|
|
|
|
|
fmt.Println("=== HTML Generator: T&C found, merging to determine total pages ===")
|
|
|
|
|
if err := MergePDFs(tempPDFPath, termsPath, tempMergedPath); err == nil {
|
|
|
|
|
// Get total page count from merged PDF
|
|
|
|
|
totalPageCount, err = g.getPageCount(tempMergedPath)
|
|
|
|
|
if err != nil {
|
|
|
|
|
fmt.Printf("Warning: Could not extract merged page count, using invoice count: %v\n", err)
|
|
|
|
|
totalPageCount = invoicePageCount
|
|
|
|
|
} else {
|
|
|
|
|
fmt.Printf("=== HTML Generator: Total pages (invoice + T&C): %d ===\n", totalPageCount)
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
fmt.Printf("Warning: Could not merge T&C for counting: %v\n", err)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fmt.Printf("=== HTML Generator: First pass complete, detected %d total pages ===\n", totalPageCount)
|
|
|
|
|
os.Remove(tempPDFPath)
|
|
|
|
|
os.Remove(tempMergedPath)
|
|
|
|
|
|
2026-01-21 02:20:14 -08:00
|
|
|
// SECOND PASS: Generate final PDF without page count in HTML (will be added via pdfcpu after merge)
|
|
|
|
|
fmt.Println("=== HTML Generator: Second pass - regenerating final PDF ===")
|
|
|
|
|
html = g.BuildInvoiceHTML(data, 0, 0)
|
2026-01-16 04:26:56 -08:00
|
|
|
|
|
|
|
|
if err := ioutil.WriteFile(tempHTML, []byte(html), 0644); err != nil {
|
|
|
|
|
return "", fmt.Errorf("failed to write temp HTML (second pass): %w", err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Generate final PDF filename using invoice number; fallback to a stable default
|
|
|
|
|
invoiceNumber := ""
|
|
|
|
|
if data.Document != nil {
|
|
|
|
|
invoiceNumber = data.Document.CmcReference
|
|
|
|
|
}
|
|
|
|
|
filenameBase := invoiceNumber
|
|
|
|
|
if filenameBase == "" {
|
|
|
|
|
filenameBase = "CMC Invoice"
|
|
|
|
|
}
|
|
|
|
|
filename := fmt.Sprintf("%s.pdf", filenameBase)
|
|
|
|
|
pdfPath := filepath.Join(g.outputDir, filename)
|
2026-01-18 22:52:57 -08:00
|
|
|
fmt.Printf("=== HTML Generator: Invoice filename generation - invoiceNumber='%s', filenameBase='%s', final filename='%s' ===\n", invoiceNumber, filenameBase, filename)
|
2026-01-16 04:26:56 -08:00
|
|
|
|
|
|
|
|
if err := g.htmlToPDF(tempHTML, pdfPath); err != nil {
|
|
|
|
|
return "", fmt.Errorf("failed to convert HTML to PDF (second pass): %w", err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fmt.Println("=== HTML Generator: PDF generation complete ===")
|
|
|
|
|
|
|
|
|
|
// Merge with T&C PDF if it exists
|
|
|
|
|
if _, err := os.Stat(termsPath); err == nil {
|
|
|
|
|
fmt.Println("=== HTML Generator: Found T&C PDF, merging ===")
|
|
|
|
|
tempMergedPath := filepath.Join(g.outputDir, fmt.Sprintf("%s_merged_temp.pdf", filenameBase))
|
|
|
|
|
if err := MergePDFs(pdfPath, termsPath, tempMergedPath); err != nil {
|
|
|
|
|
fmt.Printf("=== HTML Generator: Warning - could not merge T&C PDF: %v. Returning invoice without T&C.\n", err)
|
|
|
|
|
return filename, nil
|
|
|
|
|
}
|
|
|
|
|
// Replace original with merged version
|
|
|
|
|
if err := os.Rename(tempMergedPath, pdfPath); err != nil {
|
|
|
|
|
fmt.Printf("=== HTML Generator: Warning - could not replace PDF: %v\n", err)
|
2026-01-21 02:20:14 -08:00
|
|
|
return filename, err
|
2026-01-16 04:26:56 -08:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-21 02:20:14 -08:00
|
|
|
// Add page numbers to final PDF
|
|
|
|
|
// TODO: Re-enable after fixing pdfcpu watermark API usage
|
|
|
|
|
// fmt.Println("=== HTML Generator: Adding page numbers to final PDF ===")
|
|
|
|
|
// if err := AddPageNumbers(pdfPath); err != nil {
|
|
|
|
|
// fmt.Printf("=== HTML Generator: Warning - could not add page numbers: %v\n", err)
|
|
|
|
|
// }
|
|
|
|
|
|
2026-01-16 04:26:56 -08:00
|
|
|
return filename, nil
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-16 20:41:24 -08:00
|
|
|
// GenerateQuotePDF creates a PDF quote from HTML template
|
|
|
|
|
// Returns (filename, error)
|
|
|
|
|
func (g *HTMLDocumentGenerator) GenerateQuotePDF(data *QuotePDFData) (string, error) {
|
|
|
|
|
fmt.Println("=== HTML Generator: Starting quote generation (two-pass with T&C page count) ===")
|
|
|
|
|
|
|
|
|
|
// FIRST PASS: Generate PDF without page numbers to determine total pages (including T&C)
|
|
|
|
|
fmt.Println("=== HTML Generator: First pass - generating without page count ===")
|
|
|
|
|
html := g.BuildQuoteHTML(data, 0, 0)
|
|
|
|
|
|
|
|
|
|
fmt.Printf("=== HTML Generator: Generated %d bytes of HTML ===\n", len(html))
|
|
|
|
|
|
|
|
|
|
tempHTML := filepath.Join(g.outputDir, "temp_quote.html")
|
|
|
|
|
if err := ioutil.WriteFile(tempHTML, []byte(html), 0644); err != nil {
|
|
|
|
|
return "", fmt.Errorf("failed to write temp HTML: %w", err)
|
|
|
|
|
}
|
|
|
|
|
defer os.Remove(tempHTML)
|
|
|
|
|
defer os.Remove(filepath.Join(g.outputDir, "quote_logo.png")) // Clean up temp logo
|
|
|
|
|
|
|
|
|
|
// Generate temp PDF
|
|
|
|
|
tempPDFPath := filepath.Join(g.outputDir, "temp_quote_first_pass.pdf")
|
|
|
|
|
if err := g.htmlToPDF(tempHTML, tempPDFPath); err != nil {
|
|
|
|
|
return "", fmt.Errorf("failed to convert HTML to PDF (first pass): %w", err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Get initial page count from quote
|
|
|
|
|
quotePageCount, err := g.getPageCount(tempPDFPath)
|
|
|
|
|
if err != nil {
|
|
|
|
|
fmt.Printf("Warning: Could not extract quote page count: %v\n", err)
|
|
|
|
|
quotePageCount = 1
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Check if T&C exists and merge to get total page count
|
|
|
|
|
totalPageCount := quotePageCount
|
|
|
|
|
termsPath := filepath.Join(g.outputDir, "CMC_terms_and_conditions2006_A4.pdf")
|
|
|
|
|
tempMergedPath := filepath.Join(g.outputDir, "temp_quote_merged_first_pass.pdf")
|
|
|
|
|
|
|
|
|
|
if _, err := os.Stat(termsPath); err == nil {
|
|
|
|
|
fmt.Println("=== HTML Generator: T&C found, merging to determine total pages ===")
|
|
|
|
|
if err := MergePDFs(tempPDFPath, termsPath, tempMergedPath); err == nil {
|
|
|
|
|
// Get total page count from merged PDF
|
|
|
|
|
totalPageCount, err = g.getPageCount(tempMergedPath)
|
|
|
|
|
if err != nil {
|
|
|
|
|
fmt.Printf("Warning: Could not extract merged page count, using quote count: %v\n", err)
|
|
|
|
|
totalPageCount = quotePageCount
|
|
|
|
|
} else {
|
|
|
|
|
fmt.Printf("=== HTML Generator: Total pages (quote + T&C): %d ===\n", totalPageCount)
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
fmt.Printf("Warning: Could not merge T&C for counting: %v\n", err)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fmt.Printf("=== HTML Generator: First pass complete, detected %d total pages ===\n", totalPageCount)
|
|
|
|
|
os.Remove(tempPDFPath)
|
|
|
|
|
os.Remove(tempMergedPath)
|
|
|
|
|
|
2026-01-21 02:20:14 -08:00
|
|
|
// SECOND PASS: Generate final PDF without page count in HTML (will be added via pdfcpu after merge)
|
|
|
|
|
fmt.Println("=== HTML Generator: Second pass - regenerating final PDF ===")
|
|
|
|
|
html = g.BuildQuoteHTML(data, 0, 0)
|
2026-01-16 20:41:24 -08:00
|
|
|
|
|
|
|
|
if err := ioutil.WriteFile(tempHTML, []byte(html), 0644); err != nil {
|
|
|
|
|
return "", fmt.Errorf("failed to write temp HTML (second pass): %w", err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Generate final PDF filename using quote number
|
|
|
|
|
quoteNumber := ""
|
|
|
|
|
if data.Document != nil {
|
2026-01-18 05:11:41 -08:00
|
|
|
log.Printf("=== HTML Generator: Document not nil, CmcReference='%s'", data.Document.CmcReference)
|
2026-01-16 20:41:24 -08:00
|
|
|
quoteNumber = data.Document.CmcReference
|
2026-01-18 22:52:57 -08:00
|
|
|
if quoteNumber == "" {
|
|
|
|
|
// Strong fallback so we never emit a leading underscore filename
|
|
|
|
|
quoteNumber = fmt.Sprintf("Quote-%d", data.Document.ID)
|
|
|
|
|
}
|
2026-01-16 20:41:24 -08:00
|
|
|
if data.Document.Revision > 0 {
|
|
|
|
|
quoteNumber = fmt.Sprintf("%s_%d", quoteNumber, data.Document.Revision)
|
|
|
|
|
}
|
2026-01-18 05:11:41 -08:00
|
|
|
} else {
|
|
|
|
|
log.Printf("=== HTML Generator: Document is nil!")
|
2026-01-16 20:41:24 -08:00
|
|
|
}
|
2026-01-18 00:53:46 -08:00
|
|
|
log.Printf("=== HTML Generator: Quote number before fallback: '%s', Document ID: %d, Revision: %d",
|
|
|
|
|
quoteNumber, data.Document.ID, data.Document.Revision)
|
2026-01-16 20:41:24 -08:00
|
|
|
filenameBase := quoteNumber
|
|
|
|
|
if filenameBase == "" {
|
2026-01-18 05:11:41 -08:00
|
|
|
log.Printf("=== HTML Generator: Using fallback filename")
|
2026-01-16 20:41:24 -08:00
|
|
|
filenameBase = "CMC Quote"
|
|
|
|
|
}
|
2026-01-18 00:53:46 -08:00
|
|
|
log.Printf("=== HTML Generator: Final filename base: '%s'", filenameBase)
|
2026-01-16 20:41:24 -08:00
|
|
|
filename := fmt.Sprintf("%s.pdf", filenameBase)
|
|
|
|
|
pdfPath := filepath.Join(g.outputDir, filename)
|
|
|
|
|
|
|
|
|
|
if err := g.htmlToPDF(tempHTML, pdfPath); err != nil {
|
|
|
|
|
return "", fmt.Errorf("failed to convert HTML to PDF (second pass): %w", err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fmt.Println("=== HTML Generator: PDF generation complete ===")
|
|
|
|
|
|
|
|
|
|
// Merge with T&C PDF if it exists
|
|
|
|
|
if _, err := os.Stat(termsPath); err == nil {
|
|
|
|
|
fmt.Println("=== HTML Generator: Found T&C PDF, merging ===")
|
|
|
|
|
tempMergedPath := filepath.Join(g.outputDir, fmt.Sprintf("%s_merged_temp.pdf", filenameBase))
|
|
|
|
|
if err := MergePDFs(pdfPath, termsPath, tempMergedPath); err != nil {
|
|
|
|
|
fmt.Printf("=== HTML Generator: Warning - could not merge T&C PDF: %v. Returning quote without T&C.\n", err)
|
2026-01-18 22:52:57 -08:00
|
|
|
fmt.Printf("=== HTML Generator: Checking if pdfPath exists: %s\n", pdfPath)
|
|
|
|
|
if stat, err := os.Stat(pdfPath); err == nil {
|
|
|
|
|
fmt.Printf("=== HTML Generator: pdfPath exists, size=%d bytes\n", stat.Size())
|
|
|
|
|
}
|
2026-01-16 20:41:24 -08:00
|
|
|
return filename, nil
|
|
|
|
|
}
|
2026-01-18 22:52:57 -08:00
|
|
|
fmt.Printf("=== HTML Generator: Merge succeeded, replacing original PDF\n")
|
2026-01-16 20:41:24 -08:00
|
|
|
// Replace original with merged version
|
|
|
|
|
if err := os.Rename(tempMergedPath, pdfPath); err != nil {
|
|
|
|
|
fmt.Printf("=== HTML Generator: Warning - could not replace PDF: %v\n", err)
|
2026-01-18 22:52:57 -08:00
|
|
|
fmt.Printf("=== HTML Generator: tempMergedPath: %s\n", tempMergedPath)
|
|
|
|
|
fmt.Printf("=== HTML Generator: pdfPath: %s\n", pdfPath)
|
2026-01-21 02:20:14 -08:00
|
|
|
return filename, err
|
2026-01-18 22:52:57 -08:00
|
|
|
}
|
2026-01-21 02:20:14 -08:00
|
|
|
fmt.Printf("=== HTML Generator: Replaced PDF successfully\n")
|
2026-01-18 22:52:57 -08:00
|
|
|
|
|
|
|
|
// Verify the final file exists
|
|
|
|
|
if stat, err := os.Stat(pdfPath); err == nil {
|
|
|
|
|
fmt.Printf("=== HTML Generator: Final PDF verified, size=%d bytes\n", stat.Size())
|
|
|
|
|
} else {
|
|
|
|
|
fmt.Printf("=== HTML Generator: ERROR - Final PDF does not exist after merge: %v\n", err)
|
2026-01-21 02:20:14 -08:00
|
|
|
return filename, fmt.Errorf("final PDF does not exist: %w", err)
|
2026-01-16 20:41:24 -08:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-21 02:20:14 -08:00
|
|
|
// Add page numbers to final PDF
|
|
|
|
|
// TODO: Re-enable after fixing pdfcpu watermark API usage
|
|
|
|
|
// fmt.Println("=== HTML Generator: Adding page numbers to final PDF ===")
|
|
|
|
|
// if err := AddPageNumbers(pdfPath); err != nil {
|
|
|
|
|
// fmt.Printf("=== HTML Generator: Warning - could not add page numbers: %v\n", err)
|
|
|
|
|
// }
|
|
|
|
|
|
2026-01-16 20:41:24 -08:00
|
|
|
return filename, nil
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-16 04:26:56 -08:00
|
|
|
// htmlToPDF converts an HTML file to PDF using chromedp
|
2026-01-16 20:41:24 -08:00
|
|
|
func (g *HTMLDocumentGenerator) htmlToPDF(htmlPath, pdfPath string) error {
|
2026-01-16 04:26:56 -08:00
|
|
|
// Read the HTML file to get its content
|
|
|
|
|
htmlContent, err := ioutil.ReadFile(htmlPath)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return fmt.Errorf("failed to read HTML file: %w", err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fmt.Printf("=== htmlToPDF: Read HTML file, size=%d bytes ===\n", len(htmlContent))
|
|
|
|
|
|
|
|
|
|
// Create chromedp context
|
|
|
|
|
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),
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
allocCtx, cancel := chromedp.NewExecAllocator(context.Background(), opts...)
|
|
|
|
|
defer cancel()
|
|
|
|
|
|
|
|
|
|
ctx, cancel := chromedp.NewContext(allocCtx)
|
|
|
|
|
defer cancel()
|
|
|
|
|
|
|
|
|
|
// Set timeout
|
|
|
|
|
ctx, cancel = context.WithTimeout(ctx, 30*time.Second)
|
|
|
|
|
defer cancel()
|
|
|
|
|
|
|
|
|
|
// Navigate to file URL and print to PDF
|
|
|
|
|
var pdfBuf []byte
|
|
|
|
|
fileURL := "file://" + htmlPath
|
|
|
|
|
|
|
|
|
|
fmt.Printf("=== htmlToPDF: Using file URL: %s ===\n", fileURL)
|
|
|
|
|
fmt.Println("=== htmlToPDF: Starting chromedp navigation ===")
|
|
|
|
|
|
|
|
|
|
if err := chromedp.Run(ctx,
|
|
|
|
|
chromedp.Navigate(fileURL),
|
|
|
|
|
chromedp.ActionFunc(func(ctx context.Context) error {
|
|
|
|
|
fmt.Println("=== htmlToPDF: Executing PrintToPDF ===")
|
|
|
|
|
var err error
|
|
|
|
|
pdfBuf, _, err = page.PrintToPDF().
|
|
|
|
|
WithPrintBackground(true).
|
|
|
|
|
WithMarginTop(0).
|
|
|
|
|
WithMarginBottom(0).
|
|
|
|
|
WithMarginLeft(0).
|
|
|
|
|
WithMarginRight(0).
|
|
|
|
|
WithPaperWidth(8.27). // A4 width in inches
|
|
|
|
|
WithPaperHeight(11.69). // A4 height in inches
|
|
|
|
|
Do(ctx)
|
|
|
|
|
if err != nil {
|
|
|
|
|
fmt.Printf("=== htmlToPDF: PrintToPDF error: %v ===\n", err)
|
|
|
|
|
} else {
|
|
|
|
|
fmt.Printf("=== htmlToPDF: PrintToPDF succeeded, PDF size=%d bytes ===\n", len(pdfBuf))
|
|
|
|
|
}
|
|
|
|
|
return err
|
|
|
|
|
}),
|
|
|
|
|
); err != nil {
|
|
|
|
|
return fmt.Errorf("chromedp failed: %w", err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Write PDF to file
|
|
|
|
|
if err := ioutil.WriteFile(pdfPath, pdfBuf, 0644); err != nil {
|
|
|
|
|
return fmt.Errorf("failed to write PDF: %w", err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// getPageCount extracts the page count from a PDF file
|
2026-01-16 20:41:24 -08:00
|
|
|
func (g *HTMLDocumentGenerator) getPageCount(pdfPath string) (int, error) {
|
2026-01-16 04:26:56 -08:00
|
|
|
pageCount, err := api.PageCountFile(pdfPath)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return 0, fmt.Errorf("failed to get page count: %w", err)
|
|
|
|
|
}
|
|
|
|
|
return pageCount, nil
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-16 20:41:24 -08:00
|
|
|
// loadLogo loads the logo image and returns it as a relative path
|
|
|
|
|
func (g *HTMLDocumentGenerator) loadLogo(logoFileName string) string {
|
2026-01-16 04:26:56 -08:00
|
|
|
// Use canonical path: /app/static/images in Docker, go/static/images locally
|
|
|
|
|
logoPath := "/app/static/images/CMC-Mobile-Logo.png"
|
|
|
|
|
if _, err := os.Stat(logoPath); err != nil {
|
|
|
|
|
// Local development path
|
|
|
|
|
logoPath = filepath.Join("go", "static", "images", "CMC-Mobile-Logo.png")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
logoData, err := ioutil.ReadFile(logoPath)
|
|
|
|
|
if err != nil {
|
|
|
|
|
fmt.Printf("Warning: Could not read logo at %s: %v\n", logoPath, err)
|
|
|
|
|
return ""
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Copy logo to output directory for chromedp to access
|
2026-01-16 20:41:24 -08:00
|
|
|
destPath := filepath.Join(g.outputDir, logoFileName)
|
2026-01-16 04:26:56 -08:00
|
|
|
if err := ioutil.WriteFile(destPath, logoData, 0644); err != nil {
|
|
|
|
|
fmt.Printf("Warning: Could not write logo to output dir: %v\n", err)
|
|
|
|
|
return ""
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fmt.Printf("=== Copied logo from %s to %s ===\n", logoPath, destPath)
|
|
|
|
|
// Return relative path (same directory as HTML file)
|
2026-01-16 20:41:24 -08:00
|
|
|
return logoFileName
|
2026-01-16 04:26:56 -08:00
|
|
|
}
|
2026-01-21 05:14:27 -08:00
|
|
|
|
|
|
|
|
// GeneratePurchaseOrderPDF creates a PDF purchase order from HTML template
|
|
|
|
|
// Returns (filename, error)
|
|
|
|
|
func (g *HTMLDocumentGenerator) GeneratePurchaseOrderPDF(data *PurchaseOrderPDFData) (string, error) {
|
|
|
|
|
fmt.Println("=== HTML Generator: Starting purchase order generation ===")
|
|
|
|
|
|
|
|
|
|
html := g.BuildPurchaseOrderHTML(data, 0, 0)
|
|
|
|
|
tempHTML := filepath.Join(g.outputDir, "temp_po.html")
|
|
|
|
|
if err := ioutil.WriteFile(tempHTML, []byte(html), 0644); err != nil {
|
|
|
|
|
return "", fmt.Errorf("failed to write temp HTML: %w", err)
|
|
|
|
|
}
|
|
|
|
|
defer os.Remove(tempHTML)
|
|
|
|
|
defer os.Remove(filepath.Join(g.outputDir, "quote_logo.png"))
|
|
|
|
|
|
|
|
|
|
poNumber := data.PurchaseOrder.Title
|
|
|
|
|
if poNumber == "" {
|
|
|
|
|
poNumber = fmt.Sprintf("PO-%d", data.PurchaseOrder.ID)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
filename := fmt.Sprintf("%s.pdf", poNumber)
|
|
|
|
|
pdfPath := filepath.Join(g.outputDir, filename)
|
|
|
|
|
|
|
|
|
|
if err := g.htmlToPDF(tempHTML, pdfPath); err != nil {
|
|
|
|
|
return "", fmt.Errorf("failed to convert HTML to PDF: %w", err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return filename, nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// GeneratePackingListPDF creates a PDF packing list from HTML template
|
|
|
|
|
// Returns (filename, error)
|
|
|
|
|
func (g *HTMLDocumentGenerator) GeneratePackingListPDF(data *PackingListPDFData) (string, error) {
|
|
|
|
|
fmt.Println("=== HTML Generator: Starting packing list generation ===")
|
|
|
|
|
|
|
|
|
|
html := g.BuildPackingListHTML(data, 0, 0)
|
|
|
|
|
tempHTML := filepath.Join(g.outputDir, "temp_packinglist.html")
|
|
|
|
|
if err := ioutil.WriteFile(tempHTML, []byte(html), 0644); err != nil {
|
|
|
|
|
return "", fmt.Errorf("failed to write temp HTML: %w", err)
|
|
|
|
|
}
|
|
|
|
|
defer os.Remove(tempHTML)
|
|
|
|
|
defer os.Remove(filepath.Join(g.outputDir, "quote_logo.png"))
|
|
|
|
|
|
|
|
|
|
packingListNumber := data.PackingList.Title
|
|
|
|
|
if packingListNumber == "" {
|
|
|
|
|
packingListNumber = fmt.Sprintf("PackingList-%d", data.PackingList.ID)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
filename := fmt.Sprintf("%s.pdf", packingListNumber)
|
|
|
|
|
pdfPath := filepath.Join(g.outputDir, filename)
|
|
|
|
|
|
|
|
|
|
if err := g.htmlToPDF(tempHTML, pdfPath); err != nil {
|
|
|
|
|
return "", fmt.Errorf("failed to convert HTML to PDF: %w", err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return filename, nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// GenerateOrderAckPDF creates a PDF order acknowledgement from HTML template
|
|
|
|
|
// Returns (filename, error)
|
|
|
|
|
func (g *HTMLDocumentGenerator) GenerateOrderAckPDF(data *OrderAckPDFData) (string, error) {
|
|
|
|
|
fmt.Println("=== HTML Generator: Starting order acknowledgement generation ===")
|
|
|
|
|
|
|
|
|
|
html := g.BuildOrderAckHTML(data, 0, 0)
|
|
|
|
|
tempHTML := filepath.Join(g.outputDir, "temp_orderack.html")
|
|
|
|
|
if err := ioutil.WriteFile(tempHTML, []byte(html), 0644); err != nil {
|
|
|
|
|
return "", fmt.Errorf("failed to write temp HTML: %w", err)
|
|
|
|
|
}
|
|
|
|
|
defer os.Remove(tempHTML)
|
|
|
|
|
defer os.Remove(filepath.Join(g.outputDir, "quote_logo.png"))
|
|
|
|
|
|
|
|
|
|
orderAckNumber := data.OrderAcknowledgement.Title
|
|
|
|
|
if orderAckNumber == "" {
|
|
|
|
|
orderAckNumber = fmt.Sprintf("OrderAck-%d", data.OrderAcknowledgement.ID)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
filename := fmt.Sprintf("%s.pdf", orderAckNumber)
|
|
|
|
|
pdfPath := filepath.Join(g.outputDir, filename)
|
|
|
|
|
|
|
|
|
|
if err := g.htmlToPDF(tempHTML, pdfPath); err != nil {
|
|
|
|
|
return "", fmt.Errorf("failed to convert HTML to PDF: %w", err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return filename, nil
|
|
|
|
|
}
|