diff --git a/.gitignore b/.gitignore
index 0eaa0c59..4c6dee81 100755
--- a/.gitignore
+++ b/.gitignore
@@ -14,6 +14,7 @@ app/cake_eclipse_helper.php
app/webroot/pdf/*
app/webroot/attachments_files/*
backups/*
+files/
# Go binaries
go/server
diff --git a/docker-compose.yml b/docker-compose.yml
index 371b4c45..c4c05e72 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -28,8 +28,8 @@ services:
depends_on:
- db
volumes:
- - ./php/app/webroot/pdf:/var/www/cmc-sales/app/webroot/pdf
- - ./php/app/webroot/attachments_files:/var/www/cmc-sales/app/webroot/attachments_files
+ - ./files/pdf:/var/www/cmc-sales/app/webroot/pdf
+ - ./files/attachments_files:/var/www/cmc-sales/app/webroot/attachments_files
networks:
- cmc-network
restart: unless-stopped
@@ -86,8 +86,8 @@ services:
- ./go:/app
- ./go/.air.toml:/root/.air.toml
- ./go/.env.example:/root/.env
- - ./php/app/webroot/pdf:/var/www/cmc-sales/app/webroot/pdf
- - ./php/app/webroot/attachments_files:/var/www/cmc-sales/app/webroot/attachments_files
+ - ./files/pdf:/var/www/cmc-sales/app/webroot/pdf
+ - ./files/attachments_files:/var/www/cmc-sales/app/webroot/attachments_files
networks:
- cmc-network
restart: unless-stopped
diff --git a/go/cmd/server/main.go b/go/cmd/server/main.go
index 64495b0f..7d98c1ee 100644
--- a/go/cmd/server/main.go
+++ b/go/cmd/server/main.go
@@ -84,7 +84,6 @@ func main() {
// PDF generation routes (under /api/pdf/* to avoid conflict with file server)
goRouter.HandleFunc("/api/pdf/generate-invoice", handlers.GenerateInvoicePDF).Methods("POST")
- goRouter.HandleFunc("/api/pdf/generate-invoice-html", handlers.GenerateInvoicePDFHTML).Methods("POST") // HTML version
goRouter.HandleFunc("/api/pdf/generate-quote", handlers.GenerateQuotePDF).Methods("POST")
goRouter.HandleFunc("/api/pdf/generate-po", handlers.GeneratePurchaseOrderPDF).Methods("POST")
goRouter.HandleFunc("/api/pdf/generate-packinglist", handlers.GeneratePackingListPDF).Methods("POST")
diff --git a/go/internal/cmc/handlers/attachments/attachments.go b/go/internal/cmc/handlers/attachments/attachments.go
index bc0a3167..9008db42 100644
--- a/go/internal/cmc/handlers/attachments/attachments.go
+++ b/go/internal/cmc/handlers/attachments/attachments.go
@@ -5,6 +5,7 @@ import (
"encoding/json"
"fmt"
"io"
+ "log"
"net/http"
"os"
"path/filepath"
@@ -129,16 +130,21 @@ func (h *AttachmentHandler) Create(w http.ResponseWriter, r *http.Request) {
// Get attachments directory from environment or use default
attachDir := os.Getenv("ATTACHMENTS_DIR")
+ log.Printf("=== Upload Debug: ATTACHMENTS_DIR env var = '%s'", attachDir)
if attachDir == "" {
attachDir = "webroot/attachments_files"
+ log.Printf("=== Upload Debug: Using fallback path: %s", attachDir)
}
+ log.Printf("=== Upload Debug: Final attachDir = '%s'", attachDir)
if err := os.MkdirAll(attachDir, 0755); err != nil {
+ log.Printf("=== Upload Debug: Failed to create directory: %v", err)
http.Error(w, "Failed to create attachments directory", http.StatusInternalServerError)
return
}
// Save file to disk
filePath := filepath.Join(attachDir, filename)
+ log.Printf("=== Upload Debug: Saving file to: %s", filePath)
dst, err := os.Create(filePath)
if err != nil {
http.Error(w, "Failed to save file", http.StatusInternalServerError)
@@ -190,6 +196,8 @@ func (h *AttachmentHandler) Create(w http.ResponseWriter, r *http.Request) {
params.Name = handler.Filename
}
+ log.Printf("=== Upload Debug: Storing in database - File path: %s, Name: %s", params.File, params.Name)
+
result, err := h.queries.CreateAttachment(r.Context(), params)
if err != nil {
// Clean up file on error
diff --git a/go/internal/cmc/handlers/pdf_api.go b/go/internal/cmc/handlers/pdf_api.go
index adf25b08..1af1795c 100644
--- a/go/internal/cmc/handlers/pdf_api.go
+++ b/go/internal/cmc/handlers/pdf_api.go
@@ -77,6 +77,7 @@ type InvoicePDFRequest struct {
// GenerateInvoicePDF handles POST /api/pdf/invoice and writes a PDF to disk.
// It returns JSON: {"filename":".pdf"}
+// GenerateInvoicePDF generates invoice using HTML template and chromedp
func GenerateInvoicePDF(w http.ResponseWriter, r *http.Request) {
var req InvoicePDFRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
@@ -109,105 +110,12 @@ func GenerateInvoicePDF(w http.ResponseWriter, r *http.Request) {
return
}
- // Map request into the existing PDF generation types.
- doc := &db.Document{ID: req.DocumentID}
- inv := &db.Invoice{Title: req.InvoiceTitle}
- cust := &db.Customer{Name: req.CustomerName}
-
- lineItems := make([]db.GetLineItemsTableRow, len(req.LineItems))
- for i, li := range req.LineItems {
- lineItems[i] = db.GetLineItemsTableRow{
- ItemNumber: li.ItemNumber,
- Quantity: li.Quantity,
- Title: li.Title,
- Description: li.Description,
- GrossUnitPrice: sql.NullString{String: fmt.Sprintf("%.2f", li.UnitPrice), Valid: true},
- GrossPrice: sql.NullString{String: fmt.Sprintf("%.2f", li.TotalPrice), Valid: true},
- DiscountPercent: sql.NullString{String: fmt.Sprintf("%.2f", li.DiscountPercent), Valid: li.DiscountPercent > 0},
- DiscountAmountUnit: sql.NullString{String: fmt.Sprintf("%.2f", li.DiscountAmountUnit), Valid: li.DiscountAmountUnit > 0},
- DiscountAmountTotal: sql.NullString{String: fmt.Sprintf("%.2f", li.DiscountAmountTotal), Valid: li.DiscountAmountTotal > 0},
- }
- }
-
- data := &pdf.InvoicePDFData{
- Document: doc,
- Invoice: inv,
- Customer: cust,
- LineItems: lineItems,
- CurrencySymbol: req.CurrencySymbol,
- CurrencyCode: req.CurrencyCode,
- ShowGST: req.ShowGST,
- ShipVia: req.ShipVia,
- FOB: req.FOB,
- IssueDate: issueDate,
- IssueDateString: req.IssueDateString,
- EmailTo: req.ContactEmail,
- Attention: req.ContactName,
- FromName: fmt.Sprintf("%s %s", req.UserFirstName, req.UserLastName),
- FromEmail: req.UserEmail,
- YourReference: req.YourReference,
- BillTo: req.BillTo,
- ShipTo: req.ShipTo,
- ShippingDetails: req.ShippingDetails,
- CustomerOrderNumber: req.CustomerOrderNumber,
- JobTitle: req.JobTitle,
- PaymentTerms: req.PaymentTerms,
- CustomerABN: req.CustomerABN,
- Subtotal: req.Subtotal,
- GSTAmount: req.GSTAmount,
- Total: req.Total,
- }
-
- filename, err := pdf.GenerateInvoicePDF(data, outputDir)
- if err != nil {
- log.Printf("GenerateInvoicePDF: failed to generate PDF: %v", err)
- http.Error(w, "failed to generate PDF", http.StatusInternalServerError)
- return
- }
-
- w.Header().Set("Content-Type", "application/json")
- _ = json.NewEncoder(w).Encode(map[string]string{"filename": filename})
-}
-
-// GenerateInvoicePDFHTML generates invoice using HTML template and chromedp
-func GenerateInvoicePDFHTML(w http.ResponseWriter, r *http.Request) {
- var req InvoicePDFRequest
- if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
- http.Error(w, "invalid JSON payload", http.StatusBadRequest)
- return
- }
-
- if req.InvoiceTitle == "" || req.CustomerName == "" {
- http.Error(w, "invoice_title and customer_name are required", http.StatusBadRequest)
- return
- }
-
- issueDate := time.Now()
- if req.IssueDate != "" {
- if parsed, err := time.Parse("2006-01-02", req.IssueDate); err == nil {
- issueDate = parsed
- }
- }
-
- outputDir := req.OutputDir
- if outputDir == "" {
- outputDir = os.Getenv("PDF_OUTPUT_DIR")
- }
- if outputDir == "" {
- outputDir = "../php/app/webroot/pdf"
- }
- if err := os.MkdirAll(outputDir, 0755); err != nil {
- log.Printf("GenerateInvoicePDFHTML: failed to create output dir: %v", err)
- http.Error(w, "failed to prepare output directory", http.StatusInternalServerError)
- return
- }
-
// Map request into the existing PDF generation types
doc := &db.Document{ID: req.DocumentID, CmcReference: req.InvoiceNumber}
inv := &db.Invoice{Title: req.InvoiceTitle}
cust := &db.Customer{Name: req.CustomerName}
- log.Printf("GenerateInvoicePDFHTML: Setting invoice number to: %s", req.InvoiceNumber)
+ log.Printf("GenerateInvoicePDF: Setting invoice number to: %s", req.InvoiceNumber)
lineItems := make([]db.GetLineItemsTableRow, len(req.LineItems))
for i, li := range req.LineItems {
@@ -262,16 +170,18 @@ func GenerateInvoicePDFHTML(w http.ResponseWriter, r *http.Request) {
}
// Use HTML generator instead of gofpdf
- htmlGen := pdf.NewHTMLInvoiceGenerator(outputDir)
+ htmlGen := pdf.NewHTMLDocumentGenerator(outputDir)
filename, err := htmlGen.GenerateInvoicePDF(data)
if err != nil {
- log.Printf("GenerateInvoicePDFHTML: failed to generate PDF: %v", err)
+ log.Printf("GenerateInvoicePDF: failed to generate PDF: %v", err)
http.Error(w, fmt.Sprintf("failed to generate PDF: %v", err), http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "application/json")
- _ = json.NewEncoder(w).Encode(map[string]string{"filename": filename})
+ _ = json.NewEncoder(w).Encode(map[string]string{
+ "filename": filename,
+ })
}
// QuoteLineItemRequest reuses the invoice item shape
@@ -314,10 +224,12 @@ type QuotePDFRequest struct {
func GenerateQuotePDF(w http.ResponseWriter, r *http.Request) {
var req QuotePDFRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
- http.Error(w, "invalid JSON payload", http.StatusBadRequest)
+ log.Printf("GenerateQuotePDF: JSON decode error: %v", err)
+ http.Error(w, fmt.Sprintf("invalid JSON payload: %v", err), http.StatusBadRequest)
return
}
if req.CmcReference == "" || req.CustomerName == "" {
+ log.Printf("GenerateQuotePDF: missing required fields - cmc_reference='%s', customer_name='%s'", req.CmcReference, req.CustomerName)
http.Error(w, "cmc_reference and customer_name are required", http.StatusBadRequest)
return
}
@@ -383,7 +295,9 @@ func GenerateQuotePDF(w http.ResponseWriter, r *http.Request) {
Pages: req.Pages,
}
- filename, err := pdf.GenerateQuotePDF(data, outputDir)
+ // Use HTML generator
+ htmlGen := pdf.NewHTMLDocumentGenerator(outputDir)
+ filename, err := htmlGen.GenerateQuotePDF(data)
if err != nil {
log.Printf("GenerateQuotePDF: failed to generate PDF: %v", err)
http.Error(w, "failed to generate PDF", http.StatusInternalServerError)
@@ -391,7 +305,9 @@ func GenerateQuotePDF(w http.ResponseWriter, r *http.Request) {
}
w.Header().Set("Content-Type", "application/json")
- _ = json.NewEncoder(w).Encode(map[string]string{"filename": filename})
+ _ = json.NewEncoder(w).Encode(map[string]string{
+ "filename": filename,
+ })
}
// PurchaseOrderLineItemRequest reuses the invoice item shape
@@ -681,15 +597,19 @@ type CountPagesRequest struct {
func CountPages(w http.ResponseWriter, r *http.Request) {
var req CountPagesRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
+ log.Printf("CountPages: JSON decode error: %v", err)
http.Error(w, "invalid JSON payload", http.StatusBadRequest)
return
}
if req.FilePath == "" {
+ log.Printf("CountPages: file_path is required")
http.Error(w, "file_path is required", http.StatusBadRequest)
return
}
+ log.Printf("CountPages: Attempting to count pages for file: %s", req.FilePath)
+
// Count pages in the PDF file
pageCount, err := pdf.CountPDFPages(req.FilePath)
if err != nil {
@@ -703,6 +623,7 @@ func CountPages(w http.ResponseWriter, r *http.Request) {
return
}
+ log.Printf("CountPages: Successfully counted %d pages in %s", pageCount, req.FilePath)
w.Header().Set("Content-Type", "application/json")
_ = json.NewEncoder(w).Encode(map[string]int{"page_count": pageCount})
}
diff --git a/go/internal/cmc/pdf/description_formatter.go b/go/internal/cmc/pdf/description_formatter.go
deleted file mode 100644
index 5b75613f..00000000
--- a/go/internal/cmc/pdf/description_formatter.go
+++ /dev/null
@@ -1,7 +0,0 @@
-package pdf
-
-// formatDescription passes through descriptions as-is
-// HTML formatting should be applied in PHP before sending to this API
-func formatDescription(text string) string {
- return text
-}
diff --git a/go/internal/cmc/pdf/description_formatter_test.go b/go/internal/cmc/pdf/description_formatter_test.go
deleted file mode 100644
index 891c8ff8..00000000
--- a/go/internal/cmc/pdf/description_formatter_test.go
+++ /dev/null
@@ -1,115 +0,0 @@
-package pdf
-
-import (
- "strings"
- "testing"
-)
-
-func TestFormatDescription(t *testing.T) {
- input := `Item Code: B25SEZ22B0
-Item Description: SE Sensor Zone 22
-Type: SE - To control the function of the rupture disc
-Cable Length: 2m
-Ui< 40V
-li<57mA
-Li, Ci negligible
-II 2G Ex ib IIC T6 (Gb)
-II 2D Ex ib IIC T 80 deg C IP65 ((Db) -25 deg C < Ta < +80 deg C
-IBEx U1ATEX1017
-Includes installation instruction
-
-With standard Angle Bracket to suit Brilex Non-Insulated Explosion Vents
-(If Insulated panels are used a modified (vertically extended) bracket needs to be used)
-See attached EC Conformity Declaration for the SE Sensor
-
-Testing at
-
-1. -4 deg C
-2. -30 deg C
-3. 20 deg C`
-
- output := formatDescription(input)
-
- // Check that key: value pairs are bolded
- if !strings.Contains(output, "Item Code:") {
- t.Error("Item Code should be bolded")
- }
-
- if !strings.Contains(output, "Item Description:") {
- t.Error("Item Description should be bolded")
- }
-
- // Check that list items are in - tags
- if !strings.Contains(output, "
") {
- t.Error("Ordered list should have tag")
- }
-
- if !strings.Contains(output, "- -4 deg C
") {
- t.Error("List item 1 not formatted correctly")
- }
-
- // Check that italic patterns are applied
- if !strings.Contains(output, "See attached EC Conformity Declaration for the SE Sensor") {
- t.Error("Italic pattern not applied to EC Conformity Declaration text")
- }
-
- // Verify HTML tags are properly balanced (count opening/closing tag pairs)
- strongOpens := strings.Count(output, "")
- strongCloses := strings.Count(output, "")
- if strongOpens != strongCloses {
- t.Errorf("Unbalanced tags: %d opens, %d closes", strongOpens, strongCloses)
- }
-
- emOpens := strings.Count(output, "")
- emCloses := strings.Count(output, "")
- if emOpens != emCloses {
- t.Errorf("Unbalanced tags: %d opens, %d closes", emOpens, emCloses)
- }
-
- olOpens := strings.Count(output, "")
- olCloses := strings.Count(output, "
")
- if olOpens != olCloses {
- t.Errorf("Unbalanced tags: %d opens, %d closes", olOpens, olCloses)
- }
-
- t.Logf("Formatted output:\n%s", output)
-}
-
-func TestIsOrderedListItem(t *testing.T) {
- tests := []struct {
- input string
- expected bool
- }{
- {"1. Item one", true},
- {"2. Item two", true},
- {"10. Item ten", true},
- {"Item without number", false},
- {"1 Item without dot", false},
- {"Item: with colon", false},
- }
-
- for _, test := range tests {
- result := isOrderedListItem(test.input)
- if result != test.expected {
- t.Errorf("isOrderedListItem(%q) = %v, want %v", test.input, result, test.expected)
- }
- }
-}
-
-func TestFormatLine(t *testing.T) {
- tests := []struct {
- input string
- contains string
- }{
- {"Item Code: B25SEZ22B0", "Item Code:"},
- {"Type: SE - To control", "Type:"},
- {"Random text here", "Random text here"},
- }
-
- for _, test := range tests {
- result := formatLine(test.input)
- if !strings.Contains(result, test.contains) {
- t.Errorf("formatLine(%q) should contain %q, got %q", test.input, test.contains, result)
- }
- }
-}
diff --git a/go/internal/cmc/pdf/html_generator.go b/go/internal/cmc/pdf/html_generator.go
index 737892c7..327a3a35 100644
--- a/go/internal/cmc/pdf/html_generator.go
+++ b/go/internal/cmc/pdf/html_generator.go
@@ -1,16 +1,11 @@
package pdf
import (
- "bytes"
"context"
- "database/sql"
"fmt"
- "html/template"
"io/ioutil"
"os"
"path/filepath"
- "strconv"
- "strings"
"time"
"github.com/chromedp/cdproto/page"
@@ -18,25 +13,26 @@ import (
"github.com/pdfcpu/pdfcpu/pkg/api"
)
-// HTMLInvoiceGenerator generates PDF invoices from HTML templates using chromedp
-type HTMLInvoiceGenerator struct {
+// HTMLDocumentGenerator generates PDF documents from HTML templates using chromedp
+type HTMLDocumentGenerator struct {
outputDir string
}
-// NewHTMLInvoiceGenerator creates a new HTML-based invoice generator
-func NewHTMLInvoiceGenerator(outputDir string) *HTMLInvoiceGenerator {
- return &HTMLInvoiceGenerator{
+// NewHTMLDocumentGenerator creates a new HTML-based document generator
+func NewHTMLDocumentGenerator(outputDir string) *HTMLDocumentGenerator {
+ return &HTMLDocumentGenerator{
outputDir: outputDir,
}
}
// GenerateInvoicePDF creates a PDF invoice from HTML template
-func (g *HTMLInvoiceGenerator) GenerateInvoicePDF(data *InvoicePDFData) (string, error) {
+// Returns (filename, error)
+func (g *HTMLDocumentGenerator) GenerateInvoicePDF(data *InvoicePDFData) (string, error) {
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 ===")
- html := g.buildInvoiceHTML(data, 0, 0)
+ html := g.BuildInvoiceHTML(data, 0, 0)
fmt.Printf("=== HTML Generator: Generated %d bytes of HTML ===\n", len(html))
@@ -88,7 +84,7 @@ func (g *HTMLInvoiceGenerator) GenerateInvoicePDF(data *InvoicePDFData) (string,
// SECOND PASS: Generate final PDF with correct page count
fmt.Printf("=== HTML Generator: Second pass - regenerating with page count %d ===\n", totalPageCount)
- html = g.buildInvoiceHTML(data, totalPageCount, 1)
+ html = g.BuildInvoiceHTML(data, totalPageCount, 1)
if err := ioutil.WriteFile(tempHTML, []byte(html), 0644); err != nil {
return "", fmt.Errorf("failed to write temp HTML (second pass): %w", err)
@@ -129,8 +125,110 @@ func (g *HTMLInvoiceGenerator) GenerateInvoicePDF(data *InvoicePDFData) (string,
return filename, nil
}
+// 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)
+
+ // SECOND PASS: Generate final PDF with correct page count
+ fmt.Printf("=== HTML Generator: Second pass - regenerating with page count %d ===\n", totalPageCount)
+ html = g.BuildQuoteHTML(data, totalPageCount, 1)
+
+ 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 {
+ quoteNumber = data.Document.CmcReference
+ if data.Document.Revision > 0 {
+ quoteNumber = fmt.Sprintf("%s_%d", quoteNumber, data.Document.Revision)
+ }
+ }
+ filenameBase := quoteNumber
+ if filenameBase == "" {
+ filenameBase = "CMC Quote"
+ }
+ 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)
+ 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)
+ }
+ }
+
+ return filename, nil
+}
+
// htmlToPDF converts an HTML file to PDF using chromedp
-func (g *HTMLInvoiceGenerator) htmlToPDF(htmlPath, pdfPath string) error {
+func (g *HTMLDocumentGenerator) htmlToPDF(htmlPath, pdfPath string) error {
// Read the HTML file to get its content
htmlContent, err := ioutil.ReadFile(htmlPath)
if err != nil {
@@ -198,7 +296,7 @@ func (g *HTMLInvoiceGenerator) htmlToPDF(htmlPath, pdfPath string) error {
}
// getPageCount extracts the page count from a PDF file
-func (g *HTMLInvoiceGenerator) getPageCount(pdfPath string) (int, error) {
+func (g *HTMLDocumentGenerator) getPageCount(pdfPath string) (int, error) {
pageCount, err := api.PageCountFile(pdfPath)
if err != nil {
return 0, fmt.Errorf("failed to get page count: %w", err)
@@ -206,8 +304,8 @@ func (g *HTMLInvoiceGenerator) getPageCount(pdfPath string) (int, error) {
return pageCount, nil
}
-// loadLogoAsBase64 loads the logo image and returns it as a relative path
-func (g *HTMLInvoiceGenerator) loadLogoAsBase64() string {
+// loadLogo loads the logo image and returns it as a relative path
+func (g *HTMLDocumentGenerator) loadLogo(logoFileName string) string {
// 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 {
@@ -222,7 +320,7 @@ func (g *HTMLInvoiceGenerator) loadLogoAsBase64() string {
}
// Copy logo to output directory for chromedp to access
- destPath := filepath.Join(g.outputDir, "invoice_logo.png")
+ destPath := filepath.Join(g.outputDir, logoFileName)
if err := ioutil.WriteFile(destPath, logoData, 0644); err != nil {
fmt.Printf("Warning: Could not write logo to output dir: %v\n", err)
return ""
@@ -230,160 +328,5 @@ func (g *HTMLInvoiceGenerator) loadLogoAsBase64() string {
fmt.Printf("=== Copied logo from %s to %s ===\n", logoPath, destPath)
// Return relative path (same directory as HTML file)
- return "invoice_logo.png"
-}
-
-// buildInvoiceHTML generates the complete HTML for an invoice using templates
-func (g *HTMLInvoiceGenerator) buildInvoiceHTML(data *InvoicePDFData, totalPages int, currentPage int) string {
- // Get invoice number, fall back to invoice title so the template always shows something meaningful
- invoiceNum := ""
- if data.Document != nil {
- invoiceNum = data.Document.CmcReference
- }
- if invoiceNum == "" {
- invoiceNum = data.Invoice.Title
- }
- fmt.Printf("=== buildInvoiceHTML: Invoice number: %s ===\n", invoiceNum)
-
- // Prepare template data
- templateData := struct {
- InvoiceNumber string
- IssueDateString string
- BillTo template.HTML
- ShipTo template.HTML
- CustomerOrderNumber string
- JobTitle string
- FOB string
- PaymentTerms string
- CustomerABN string
- CurrencyCode string
- CurrencySymbol string
- LineItems []LineItemTemplateData
- Subtotal interface{}
- GSTAmount interface{}
- Total interface{}
- ShowGST bool
- PageCount int
- CurrentPage int
- LogoDataURI string
- }{
- InvoiceNumber: invoiceNum,
- IssueDateString: data.IssueDateString,
- BillTo: template.HTML(data.BillTo),
- ShipTo: template.HTML(data.ShipTo),
- CustomerOrderNumber: data.CustomerOrderNumber,
- JobTitle: data.JobTitle,
- FOB: data.FOB,
- PaymentTerms: data.PaymentTerms,
- CustomerABN: data.CustomerABN,
- CurrencyCode: data.CurrencyCode,
- CurrencySymbol: data.CurrencySymbol,
- Subtotal: data.Subtotal,
- GSTAmount: data.GSTAmount,
- Total: data.Total,
- ShowGST: data.ShowGST,
- PageCount: totalPages,
- CurrentPage: currentPage,
- LogoDataURI: g.loadLogoAsBase64(),
- }
-
- // Convert line items to template format
- for _, item := range data.LineItems {
- templateData.LineItems = append(templateData.LineItems, LineItemTemplateData{
- Title: item.Title,
- Description: template.HTML(item.Description), // Allow HTML in description
- Quantity: item.Quantity,
- GrossUnitPrice: item.GrossUnitPrice,
- DiscountAmountTotal: item.DiscountAmountTotal,
- GrossPrice: item.GrossPrice,
- })
- }
-
- // Define template functions
- funcMap := template.FuncMap{
- "formatPrice": func(price sql.NullString) template.HTML {
- if !price.Valid || price.String == "" {
- return ""
- }
- formatted := formatPriceWithCommas(price.String)
- return template.HTML(fmt.Sprintf("%s%s", data.CurrencySymbol, formatted))
- },
- "formatDiscount": func(discount sql.NullString) template.HTML {
- if !discount.Valid || discount.String == "" {
- return template.HTML(fmt.Sprintf("%s0.00", data.CurrencySymbol))
- }
- if discount.String == "0" || discount.String == "0.00" {
- return template.HTML(fmt.Sprintf("%s0.00", data.CurrencySymbol))
- }
- formatted := formatPriceWithCommas(discount.String)
- return template.HTML(fmt.Sprintf("-%s%s", data.CurrencySymbol, formatted))
- },
- "formatTotal": func(amount interface{}) template.HTML {
- if amount == nil {
- return ""
- }
- if val, ok := amount.(float64); ok {
- formatted := formatPriceWithCommas(fmt.Sprintf("%.2f", val))
- return template.HTML(fmt.Sprintf("%s%s", data.CurrencySymbol, formatted))
- }
- return template.HTML(fmt.Sprintf("%v", amount))
- },
- }
-
- // Parse and execute template
- tmplPath := filepath.Join("internal", "cmc", "pdf", "templates", "invoice.html")
- tmpl, err := template.New("invoice.html").Funcs(funcMap).ParseFiles(tmplPath)
- if err != nil {
- fmt.Printf("Error parsing template: %v\n", err)
- return ""
- }
-
- var buf bytes.Buffer
- if err := tmpl.Execute(&buf, templateData); err != nil {
- fmt.Printf("Error executing template: %v\n", err)
- return ""
- }
-
- return buf.String()
-}
-
-// LineItemTemplateData represents a line item for template rendering
-type LineItemTemplateData struct {
- Title string
- Description template.HTML
- Quantity string
- GrossUnitPrice sql.NullString
- DiscountAmountTotal sql.NullString
- GrossPrice sql.NullString
-}
-
-// formatPriceWithCommas formats a price string with comma separators
-func formatPriceWithCommas(priceStr string) string {
- if priceStr == "" {
- return ""
- }
-
- // Try to parse as float
- price, err := strconv.ParseFloat(priceStr, 64)
- if err != nil {
- // Return as-is if not parseable
- return priceStr
- }
-
- // Format with 2 decimal places and commas
- amountStr := strconv.FormatFloat(price, 'f', 2, 64)
- parts := strings.Split(amountStr, ".")
- intPart := parts[0]
- decPart := "." + parts[1]
-
- // Add comma separators
- var result strings.Builder
- for i, ch := range intPart {
- if i > 0 && (len(intPart)-i)%3 == 0 {
- result.WriteString(",")
- }
- result.WriteRune(ch)
- }
-
- return result.String() + decPart
+ return logoFileName
}
diff --git a/go/internal/cmc/pdf/html_generator.go.applied b/go/internal/cmc/pdf/html_generator.go.applied
deleted file mode 100644
index e69de29b..00000000
diff --git a/go/internal/cmc/pdf/html_generator.go.backup b/go/internal/cmc/pdf/html_generator.go.backup
deleted file mode 100644
index e69de29b..00000000
diff --git a/go/internal/cmc/pdf/html_generator.go.patch b/go/internal/cmc/pdf/html_generator.go.patch
deleted file mode 100644
index e69de29b..00000000
diff --git a/go/internal/cmc/pdf/invoice_builder.go b/go/internal/cmc/pdf/invoice_builder.go
new file mode 100644
index 00000000..c0ca026b
--- /dev/null
+++ b/go/internal/cmc/pdf/invoice_builder.go
@@ -0,0 +1,181 @@
+package pdf
+
+import (
+ "bytes"
+ "database/sql"
+ "fmt"
+ "html/template"
+ "path/filepath"
+ "strconv"
+ "strings"
+)
+
+// BuildInvoiceHTML generates the complete HTML for an invoice using templates
+func (g *HTMLDocumentGenerator) BuildInvoiceHTML(data *InvoicePDFData, totalPages int, currentPage int) string {
+ // Get invoice number, fall back to invoice title so the template always shows something meaningful
+ invoiceNum := ""
+ if data.Document != nil {
+ invoiceNum = data.Document.CmcReference
+ }
+ if invoiceNum == "" {
+ invoiceNum = data.Invoice.Title
+ }
+ fmt.Printf("=== buildInvoiceHTML: Invoice number: %s ===\n", invoiceNum)
+
+ // Prepare template data
+ templateData := struct {
+ InvoiceNumber string
+ IssueDateString string
+ BillTo template.HTML
+ ShipTo template.HTML
+ CustomerOrderNumber string
+ JobTitle string
+ FOB string
+ PaymentTerms string
+ CustomerABN string
+ CurrencyCode string
+ CurrencySymbol string
+ LineItems []LineItemTemplateData
+ Subtotal interface{}
+ GSTAmount interface{}
+ Total interface{}
+ ShowGST bool
+ PageCount int
+ CurrentPage int
+ LogoDataURI string
+ }{
+ InvoiceNumber: invoiceNum,
+ IssueDateString: data.IssueDateString,
+ BillTo: template.HTML(data.BillTo),
+ ShipTo: template.HTML(data.ShipTo),
+ CustomerOrderNumber: data.CustomerOrderNumber,
+ JobTitle: data.JobTitle,
+ FOB: data.FOB,
+ PaymentTerms: data.PaymentTerms,
+ CustomerABN: data.CustomerABN,
+ CurrencyCode: data.CurrencyCode,
+ CurrencySymbol: data.CurrencySymbol,
+ Subtotal: data.Subtotal,
+ GSTAmount: data.GSTAmount,
+ Total: data.Total,
+ ShowGST: data.ShowGST,
+ PageCount: totalPages,
+ CurrentPage: currentPage,
+ LogoDataURI: g.loadLogo("invoice_logo.png"),
+ }
+
+ // Convert line items to template format
+ for _, item := range data.LineItems {
+ templateData.LineItems = append(templateData.LineItems, LineItemTemplateData{
+ Title: item.Title,
+ Description: template.HTML(item.Description), // Allow HTML in description
+ Quantity: item.Quantity,
+ GrossUnitPrice: item.GrossUnitPrice,
+ DiscountAmountTotal: item.DiscountAmountTotal,
+ GrossPrice: item.GrossPrice,
+ })
+ }
+
+ // Define template functions
+ funcMap := template.FuncMap{
+ "formatPrice": func(price sql.NullString) template.HTML {
+ if !price.Valid || price.String == "" {
+ return ""
+ }
+ formatted := FormatPriceWithCommas(price.String)
+ return template.HTML(fmt.Sprintf("%s%s", data.CurrencySymbol, formatted))
+ },
+ "formatDiscount": func(discount sql.NullString) template.HTML {
+ if !discount.Valid || discount.String == "" {
+ return template.HTML(fmt.Sprintf("%s0.00", data.CurrencySymbol))
+ }
+ if discount.String == "0" || discount.String == "0.00" {
+ return template.HTML(fmt.Sprintf("%s0.00", data.CurrencySymbol))
+ }
+ formatted := FormatPriceWithCommas(discount.String)
+ return template.HTML(fmt.Sprintf("-%s%s", data.CurrencySymbol, formatted))
+ },
+ "formatTotal": func(amount interface{}) template.HTML {
+ if amount == nil {
+ return ""
+ }
+ if val, ok := amount.(float64); ok {
+ formatted := FormatPriceWithCommas(fmt.Sprintf("%.2f", val))
+ return template.HTML(fmt.Sprintf("%s%s", data.CurrencySymbol, formatted))
+ }
+ return template.HTML(fmt.Sprintf("%v", amount))
+ },
+ }
+
+ // Parse and execute template
+ // Try multiple possible paths to find the template
+ possiblePaths := []string{
+ filepath.Join("internal", "cmc", "pdf", "templates", "invoice.html"),
+ filepath.Join("go", "internal", "cmc", "pdf", "templates", "invoice.html"),
+ "/app/go/internal/cmc/pdf/templates/invoice.html",
+ }
+
+ var tmpl *template.Template
+ var err error
+
+ for _, tmplPath := range possiblePaths {
+ tmpl, err = template.New("invoice.html").Funcs(funcMap).ParseFiles(tmplPath)
+ if err == nil {
+ break
+ }
+ }
+
+ if tmpl == nil || err != nil {
+ fmt.Printf("Error parsing template from any path: %v\n", err)
+ return ""
+ }
+
+ var buf bytes.Buffer
+ if err := tmpl.Execute(&buf, templateData); err != nil {
+ fmt.Printf("Error executing template: %v\n", err)
+ return ""
+ }
+
+ return buf.String()
+}
+
+// LineItemTemplateData represents a line item for template rendering
+type LineItemTemplateData struct {
+ Title string
+ Description template.HTML
+ Quantity string
+ GrossUnitPrice sql.NullString
+ DiscountAmountTotal sql.NullString
+ GrossPrice sql.NullString
+}
+
+// FormatPriceWithCommas formats a price string with comma separators
+func FormatPriceWithCommas(priceStr string) string {
+ if priceStr == "" {
+ return ""
+ }
+
+ // Try to parse as float
+ price, err := strconv.ParseFloat(priceStr, 64)
+ if err != nil {
+ // Return as-is if not parseable
+ return priceStr
+ }
+
+ // Format with 2 decimal places and commas
+ amountStr := strconv.FormatFloat(price, 'f', 2, 64)
+ parts := strings.Split(amountStr, ".")
+ intPart := parts[0]
+ decPart := "." + parts[1]
+
+ // Add comma separators
+ var result strings.Builder
+ for i, ch := range intPart {
+ if i > 0 && (len(intPart)-i)%3 == 0 {
+ result.WriteString(",")
+ }
+ result.WriteRune(ch)
+ }
+
+ return result.String() + decPart
+}
diff --git a/go/internal/cmc/pdf/quote_builder.go b/go/internal/cmc/pdf/quote_builder.go
new file mode 100644
index 00000000..0a17137d
--- /dev/null
+++ b/go/internal/cmc/pdf/quote_builder.go
@@ -0,0 +1,190 @@
+package pdf
+
+import (
+ "bytes"
+ "database/sql"
+ "fmt"
+ "html/template"
+ "path/filepath"
+ "strconv"
+)
+
+// QuoteLineItemTemplateData represents a quote line item for template rendering
+type QuoteLineItemTemplateData struct {
+ ItemNumber string
+ Title string
+ Description template.HTML
+ Quantity string
+ GrossUnitPrice sql.NullString
+ GrossPrice sql.NullString
+}
+
+// BuildQuoteHTML generates the complete HTML for a quote using templates
+func (g *HTMLDocumentGenerator) BuildQuoteHTML(data *QuotePDFData, totalPages int, currentPage int) string {
+ // Get quote number from Document.CmcReference
+ quoteNum := ""
+ if data.Document != nil {
+ quoteNum = data.Document.CmcReference
+ if data.Document.Revision > 0 {
+ quoteNum = fmt.Sprintf("%s.%d", quoteNum, data.Document.Revision)
+ }
+ }
+ fmt.Printf("=== buildQuoteHTML: Quote number: %s ===\n", quoteNum)
+
+ // Prepare FROM name
+ fromName := ""
+ if data.User != nil {
+ fromName = fmt.Sprintf("%s %s", data.User.FirstName, data.User.LastName)
+ }
+
+ fromEmail := ""
+ if data.User != nil {
+ fromEmail = data.User.Email
+ }
+
+ // Prepare company name
+ companyName := ""
+ if data.Customer != nil {
+ companyName = data.Customer.Name
+ }
+
+ // Prepare page content (first page only for now, multi-page support later)
+ pageContent := template.HTML("")
+ if len(data.Pages) > 0 {
+ pageContent = template.HTML(data.Pages[0])
+ }
+
+ // Calculate totals
+ subtotal := 0.0
+ for _, item := range data.LineItems {
+ if item.GrossPrice.Valid {
+ price, _ := strconv.ParseFloat(item.GrossPrice.String, 64)
+ subtotal += price
+ }
+ }
+
+ gstAmount := 0.0
+ total := subtotal
+ if data.ShowGST {
+ gstAmount = subtotal * 0.1
+ total = subtotal + gstAmount
+ }
+
+ // Prepare template data
+ templateData := struct {
+ QuoteNumber string
+ CompanyName string
+ EmailTo string
+ Attention string
+ FromName string
+ FromEmail string
+ YourReference string
+ IssueDateString string
+ PageContent template.HTML
+ CurrencyCode string
+ CurrencySymbol string
+ LineItems []QuoteLineItemTemplateData
+ Subtotal float64
+ GSTAmount float64
+ Total float64
+ ShowGST bool
+ CommercialComments string
+ DeliveryTime string
+ PaymentTerms string
+ DaysValid int
+ DeliveryPoint string
+ ExchangeRate string
+ CustomsDuty string
+ GSTPhrase string
+ SalesEngineer string
+ PageCount int
+ CurrentPage int
+ LogoDataURI string
+ }{
+ QuoteNumber: quoteNum,
+ CompanyName: companyName,
+ EmailTo: data.EmailTo,
+ Attention: data.Attention,
+ FromName: fromName,
+ FromEmail: fromEmail,
+ YourReference: fmt.Sprintf("Enquiry on %s", data.IssueDateString),
+ IssueDateString: data.IssueDateString,
+ PageContent: pageContent,
+ CurrencyCode: data.CurrencyCode,
+ CurrencySymbol: data.CurrencySymbol,
+ Subtotal: subtotal,
+ GSTAmount: gstAmount,
+ Total: total,
+ ShowGST: data.ShowGST,
+ CommercialComments: data.CommercialComments,
+ DeliveryTime: data.DeliveryTime,
+ PaymentTerms: data.PaymentTerms,
+ DaysValid: data.DaysValid,
+ DeliveryPoint: data.DeliveryPoint,
+ ExchangeRate: data.ExchangeRate,
+ CustomsDuty: data.CustomsDuty,
+ GSTPhrase: data.GSTPhrase,
+ SalesEngineer: data.SalesEngineer,
+ PageCount: totalPages,
+ CurrentPage: currentPage,
+ LogoDataURI: g.loadLogo("quote_logo.png"),
+ }
+
+ // Convert line items to template format
+ for _, item := range data.LineItems {
+ templateData.LineItems = append(templateData.LineItems, QuoteLineItemTemplateData{
+ ItemNumber: item.ItemNumber,
+ Title: item.Title,
+ Quantity: item.Quantity,
+ Description: template.HTML(""), // Quotes don't have descriptions in line items by default
+ GrossUnitPrice: item.GrossUnitPrice,
+ GrossPrice: item.GrossPrice,
+ })
+ }
+
+ // Define template functions
+ funcMap := template.FuncMap{
+ "formatPrice": func(price sql.NullString) template.HTML {
+ if !price.Valid || price.String == "" {
+ return ""
+ }
+ formatted := FormatPriceWithCommas(price.String)
+ return template.HTML(fmt.Sprintf("%s%s", data.CurrencySymbol, formatted))
+ },
+ "formatTotal": func(amount float64) template.HTML {
+ formatted := FormatPriceWithCommas(fmt.Sprintf("%.2f", amount))
+ return template.HTML(fmt.Sprintf("%s%s", data.CurrencySymbol, formatted))
+ },
+ }
+
+ // Parse and execute template
+ // Try multiple possible paths to find the template
+ possiblePaths := []string{
+ filepath.Join("internal", "cmc", "pdf", "templates", "quote.html"),
+ filepath.Join("go", "internal", "cmc", "pdf", "templates", "quote.html"),
+ "/app/go/internal/cmc/pdf/templates/quote.html",
+ }
+
+ var tmpl *template.Template
+ var err error
+
+ for _, tmplPath := range possiblePaths {
+ tmpl, err = template.New("quote.html").Funcs(funcMap).ParseFiles(tmplPath)
+ if err == nil {
+ break
+ }
+ }
+
+ if tmpl == nil || err != nil {
+ fmt.Printf("Error parsing template from any path: %v\n", err)
+ return ""
+ }
+
+ var buf bytes.Buffer
+ if err := tmpl.Execute(&buf, templateData); err != nil {
+ fmt.Printf("Error executing template: %v\n", err)
+ return ""
+ }
+
+ return buf.String()
+}
diff --git a/go/internal/cmc/pdf/templates/quote.html b/go/internal/cmc/pdf/templates/quote.html
new file mode 100644
index 00000000..8d43953a
--- /dev/null
+++ b/go/internal/cmc/pdf/templates/quote.html
@@ -0,0 +1,535 @@
+
+
+
+
+ {{if .QuoteNumber}}{{.QuoteNumber}}{{else}}CMC Quote{{end}}
+
+
+
+
+
+ {{if .LogoDataURI}}
+

+ {{end}}
+
+
+
+
+
+
+
+
+
+
+
+ | COMPANY NAME: |
+ {{.CompanyName}} |
+ QUOTE NO.: |
+ {{.QuoteNumber}} |
+
+
+ | EMAIL TO: |
+ {{.EmailTo}} |
+ YOUR REFERENCE: |
+ {{.YourReference}} |
+
+
+ | ATTENTION: |
+ {{.Attention}} |
+ ISSUE DATE: |
+ {{.IssueDateString}} |
+
+
+ | FROM: |
+ {{.FromName}} |
+ EMAIL: |
+ {{.FromEmail}} |
+
+
+
+
+ {{if .PageContent}}
+
+ {{.PageContent}}
+
+ {{end}}
+
+
+
+
+
+
+
+
+ | ITEM |
+ QTY |
+ DESCRIPTION |
+ UNIT PRICE |
+ TOTAL |
+
+
+
+ {{range .LineItems}}
+
+ | {{.ItemNumber}} |
+ {{.Quantity}} |
+
+ {{.Title}}
+ {{if .Description}}
+ {{.Description}}
+ {{end}}
+ |
+ {{formatPrice .GrossUnitPrice}} |
+ {{formatPrice .GrossPrice}} |
+
+ {{end}}
+
+
+
+
+
+
+
+ | SUBTOTAL |
+ {{formatTotal .Subtotal}} |
+
+ {{if .ShowGST}}
+
+ | GST (10%) |
+ {{formatTotal .GSTAmount}} |
+
+ {{end}}
+
+ | {{if .ShowGST}}TOTAL PAYABLE{{else}}TOTAL{{end}} |
+ {{formatTotal .Total}} |
+
+
+
+
+
+ {{if .CommercialComments}}
+
+ {{end}}
+
+
+ {{if or .DeliveryTime .PaymentTerms .DaysValid .DeliveryPoint .ExchangeRate .CustomsDuty .GSTPhrase .SalesEngineer}}
+
+ {{if .DeliveryTime}}
+
+ | DELIVERY TIME: |
+ {{.DeliveryTime}} |
+
+ {{end}}
+ {{if .PaymentTerms}}
+
+ | PAYMENT TERMS: |
+ {{.PaymentTerms}} |
+
+ {{end}}
+ {{if .DaysValid}}
+
+ | QUOTATION VALID FOR: |
+ {{.DaysValid}} days from date of issue |
+
+ {{end}}
+ {{if .DeliveryPoint}}
+
+ | DELIVERY POINT: |
+ {{.DeliveryPoint}} |
+
+ {{end}}
+ {{if .ExchangeRate}}
+
+ | EXCHANGE RATE: |
+ {{.ExchangeRate}} |
+
+ {{end}}
+ {{if .CustomsDuty}}
+
+ | CUSTOMS DUTY: |
+ {{.CustomsDuty}} |
+
+ {{end}}
+ {{if .GSTPhrase}}
+
+ | GST: |
+ {{.GSTPhrase}} |
+
+ {{end}}
+ {{if .SalesEngineer}}
+
+ | SALES ENGINEER: |
+ {{.SalesEngineer}} |
+
+ {{end}}
+
+ {{end}}
+
+
+ {{if .PageCount}}
+
+ Page {{.CurrentPage}} of {{.PageCount}}
+
+ {{end}}
+
+
+
+
+
diff --git a/php/app/controllers/documents_controller.php b/php/app/controllers/documents_controller.php
index e8e67c4f..bbaa8b36 100755
--- a/php/app/controllers/documents_controller.php
+++ b/php/app/controllers/documents_controller.php
@@ -807,6 +807,9 @@ ENDINSTRUCTIONS;
$this->redirect(array('controller'=>'documents', 'action'=>'index'));
}
+ $pdf_dir = Configure::read('pdf_directory');
+
+ $this->Document->recursive = 2; // Load associations deeply
$document = $this->Document->read(null,$id);
$this->set('document', $document);
@@ -864,11 +867,14 @@ ENDINSTRUCTIONS;
//
switch($docType) {
case "quote":
+ // Use enquiry title, or fall back to cmc_reference, or document ID
+ if (!empty($enquiry['Enquiry']['title'])) {
$filename = $enquiry['Enquiry']['title'];
- $template_name = 'pdf_quote';
- break;
-
- case "invoice":
+ } elseif (!empty($document['Document']['cmc_reference'])) {
+ $filename = $document['Document']['cmc_reference'];
+ } else {
+ $filename = 'Quote-' . $document['Document']['id'];
+ }
$filename = $document['Invoice']['title'];
$this->set('docTitle', $document['Invoice']['title']);
$this->set('job', $this->Document->Invoice->Job->find('first', array('conditions'=>array('Job.id'=>$document['Invoice']['job_id']))));
@@ -955,6 +961,16 @@ ENDINSTRUCTIONS;
$document['Document']['pdf_created_by_user_id'] = $this->getCurrentUserID();
if($this->Document->save($document)) {
//echo "Set pdf_filename attritbute to: ".$filename;
+
+ // Count pages and update doc_page_count
+ App::import('Vendor','pagecounter');
+ $pageCounter = new PageCounter();
+ $pdfPath = $pdf_dir . $filename;
+ $pageCount = $pageCounter->count($pdfPath);
+ if ($pageCount > 0) {
+ $this->Document->id = $document['Document']['id'];
+ $this->Document->saveField('doc_page_count', $pageCount);
+ }
}
else {
//echo 'Failed to set pdf_filename to: '.$filename;
diff --git a/php/app/vendors/pagecounter.php b/php/app/vendors/pagecounter.php
index 366d4ef9..b0d4c10d 100755
--- a/php/app/vendors/pagecounter.php
+++ b/php/app/vendors/pagecounter.php
@@ -9,7 +9,10 @@ class PageCounter {
* Returns the page count or null if unable to determine.
*/
function count($file) {
+ error_log("PageCounter: Attempting to count pages for file: $file");
+
if (!file_exists($file) || !is_readable($file)) {
+ error_log("PageCounter: File does not exist or is not readable: $file");
return null;
}
@@ -18,7 +21,9 @@ class PageCounter {
App::import('Controller', 'App');
$appController = new AppController();
$goBaseUrl = $appController::getGoBaseUrlOrFail();
- $goEndpoint = $goBaseUrl . '/go/pdf/count-pages';
+ $goEndpoint = $goBaseUrl . '/go/api/pdf/count-pages';
+
+ error_log("PageCounter: Calling Go endpoint: $goEndpoint with file: $file");
// Call the Go page counter endpoint
$payload = array('file_path' => $file);
@@ -34,17 +39,22 @@ class PageCounter {
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
+ error_log("PageCounter: Got response with HTTP code $httpCode: $response");
+
if ($httpCode >= 200 && $httpCode < 300) {
$result = json_decode($response, true);
if (isset($result['page_count']) && is_numeric($result['page_count'])) {
$count = (int)$result['page_count'];
+ error_log("PageCounter: Returning page count: $count");
return $count > 0 ? $count : null;
}
}
+ error_log("PageCounter: Failed to get valid page count from response");
return null;
} catch (Exception $e) {
// If Go service fails, return null gracefully
+ error_log("PageCounter: Exception: " . $e->getMessage());
return null;
}
}
diff --git a/php/app/views/documents/generate_first_page.ctp b/php/app/views/documents/generate_first_page.ctp
index cda551b5..599b2616 100755
--- a/php/app/views/documents/generate_first_page.ctp
+++ b/php/app/views/documents/generate_first_page.ctp
@@ -42,10 +42,39 @@ $pagecounter = new PageCounter();
count($attachment['Attachment']['file']).' pages)';
+ $count = $pagecounter->count($attachmentPath);
+ $pagecount = '('.$count.' pages)';
+ file_put_contents($debugPath, "Count result: {$count}\n---\n", FILE_APPEND);
+ } else {
+ file_put_contents($debugPath, "Skipped (non-pdf)\n---\n", FILE_APPEND);
}
?>
= $attachment['Attachment']['name']; ?> = $pagecount ?>
diff --git a/php/app/views/documents/pdf_invoice.ctp b/php/app/views/documents/pdf_invoice.ctp
index 27cbb5f4..8d6fb88c 100755
--- a/php/app/views/documents/pdf_invoice.ctp
+++ b/php/app/views/documents/pdf_invoice.ctp
@@ -2,7 +2,7 @@
// Generate the Invoice PDF by calling the Go service instead of TCPDF.
$goBaseUrl = AppController::getGoBaseUrlOrFail();
-$goEndpoint = $goBaseUrl . '/go/api/pdf/generate-invoice-html';
+$goEndpoint = $goBaseUrl . '/go/api/pdf/generate-invoice';
$outputDir = Configure::read('pdf_directory');
diff --git a/php/app/views/documents/pdf_quote.ctp b/php/app/views/documents/pdf_quote.ctp
index 12c28f9b..3d84b9b1 100755
--- a/php/app/views/documents/pdf_quote.ctp
+++ b/php/app/views/documents/pdf_quote.ctp
@@ -2,7 +2,7 @@
// Generate the Quote PDF by calling the Go service instead of TCPDF/FPDI.
$goBaseUrl = AppController::getGoBaseUrlOrFail();
-$goEndpoint = $goBaseUrl . '/go/pdf/generate-quote';
+$goEndpoint = $goBaseUrl . '/go/api/pdf/generate-quote';
$outputDir = Configure::read('pdf_directory');
@@ -17,18 +17,44 @@ foreach ($document['LineItem'] as $li) {
);
}
+// Prepare fields with fallbacks
+$cmcReference = '';
+if (!empty($enquiry['Enquiry']['title'])) {
+ $cmcReference = $enquiry['Enquiry']['title'];
+} elseif (!empty($document['Document']['cmc_reference'])) {
+ $cmcReference = $document['Document']['cmc_reference'];
+} else {
+ $cmcReference = 'Quote-' . $document['Document']['id'];
+}
+
+$customerName = '';
+if (!empty($enquiry['Customer']['name'])) {
+ $customerName = $enquiry['Customer']['name'];
+} else {
+ $customerName = 'Customer';
+}
+
+$contactEmail = !empty($enquiry['Contact']['email']) ? $enquiry['Contact']['email'] : '';
+$contactName = !empty($enquiry['Contact']['first_name']) ? $enquiry['Contact']['first_name'].' '.$enquiry['Contact']['last_name'] : '';
+$userFirstName = !empty($enquiry['User']['first_name']) ? $enquiry['User']['first_name'] : '';
+$userLastName = !empty($enquiry['User']['last_name']) ? $enquiry['User']['last_name'] : '';
+$userEmail = !empty($enquiry['User']['email']) ? $enquiry['User']['email'] : '';
+$createdDate = !empty($enquiry['Enquiry']['created']) ? $enquiry['Enquiry']['created'] : date('Y-m-d');
+
$payload = array(
'document_id' => intval($document['Document']['id']),
- 'cmc_reference' => $enquiry['Enquiry']['title'],
+ 'cmc_reference' => $cmcReference,
'revision' => intval($document['Document']['revision']),
- 'created_date' => date('Y-m-d', strtotime($enquiry['Enquiry']['created'])),
- 'customer_name' => $enquiry['Customer']['name'],
- 'contact_email' => $enquiry['Contact']['email'],
- 'contact_name' => $enquiry['Contact']['first_name'].' '.$enquiry['Contact']['last_name'],
- 'user_first_name' => $enquiry['User']['first_name'],
- 'user_last_name' => $enquiry['User']['last_name'],
- 'user_email' => $enquiry['User']['email'],
+ 'created_date' => date('Y-m-d', strtotime($createdDate)),
+ 'created_date_string' => date('j M Y', strtotime($createdDate)),
+ 'customer_name' => $customerName,
+ 'contact_email' => $contactEmail,
+ 'contact_name' => $contactName,
+ 'user_first_name' => $userFirstName,
+ 'user_last_name' => $userLastName,
+ 'user_email' => $userEmail,
'currency_symbol' => $currencySymbol,
+ 'currency_code' => isset($currencyCode) ? $currencyCode : 'AUD',
'show_gst' => (bool)$gst,
'commercial_comments' => isset($document['Quote']['commercial_comments']) ? $document['Quote']['commercial_comments'] : '',
'line_items' => $lineItems,
@@ -36,6 +62,12 @@ $payload = array(
'output_dir' => $outputDir
);
+// Debug: Write payload to file for debugging
+file_put_contents($outputDir . '/quote_payload_debug.txt',
+ "=== PAYLOAD ===\n" . print_r($payload, true) .
+ "\n\n=== ENQUIRY ===\n" . print_r($enquiry, true) .
+ "\n\n=== DOCUMENT ===\n" . print_r($document, true));
+
$ch = curl_init($goEndpoint);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
@@ -52,9 +84,31 @@ if ($httpCode < 200 || $httpCode >= 300) {
if ($curlErr) {
echo " Error: $curlErr";
}
+ if (!empty($response)) {
+ echo "
Response: " . htmlspecialchars($response);
+ }
+ echo "
Payload sent:
" . htmlspecialchars(json_encode($payload, JSON_PRETTY_PRINT)) . "
";
echo "
";
exit;
}
+
+// PDF generated successfully - now count pages and update database
+$result = json_decode($response, true);
+if (isset($result['filename'])) {
+ $pdfPath = $outputDir . '/' . $result['filename'];
+
+ // Count pages using the Go service
+ App::import('Vendor','pagecounter');
+ $pageCounter = new PageCounter();
+ $pageCount = $pageCounter->count($pdfPath);
+
+ if ($pageCount > 0) {
+ // Update the document with the page count
+ $Document = ClassRegistry::init('Document');
+ $Document->id = $document['Document']['id'];
+ $Document->saveField('doc_page_count', $pageCount);
+ }
+}
?>
COMMERCIAL COMMENTS
+{{.CommercialComments}}
+