From 72a4b87193bb34312923214af4e4ad5b35d0be4a Mon Sep 17 00:00:00 2001 From: Finley Ghosh Date: Sat, 17 Jan 2026 15:41:24 +1100 Subject: [PATCH] Starting quote generation, better file mounting for local --- .gitignore | 1 + docker-compose.yml | 8 +- go/cmd/server/main.go | 1 - .../cmc/handlers/attachments/attachments.go | 8 + go/internal/cmc/handlers/pdf_api.go | 121 +--- go/internal/cmc/pdf/description_formatter.go | 7 - .../cmc/pdf/description_formatter_test.go | 115 ---- go/internal/cmc/pdf/html_generator.go | 291 ++++------ go/internal/cmc/pdf/html_generator.go.applied | 0 go/internal/cmc/pdf/html_generator.go.backup | 0 go/internal/cmc/pdf/html_generator.go.patch | 0 go/internal/cmc/pdf/invoice_builder.go | 181 ++++++ go/internal/cmc/pdf/quote_builder.go | 190 +++++++ go/internal/cmc/pdf/templates/quote.html | 535 ++++++++++++++++++ php/app/controllers/documents_controller.php | 24 +- php/app/vendors/pagecounter.php | 12 +- .../views/documents/generate_first_page.ctp | 31 +- php/app/views/documents/pdf_invoice.ctp | 2 +- php/app/views/documents/pdf_quote.ctp | 72 ++- php/app/webroot/pdf/CMC Invoice.pdf | Bin 163991 -> 0 bytes php/app/webroot/pdf/CMCIN11704.pdf | Bin 32134 -> 0 bytes php/app/webroot/pdf/CMCIN9387.pdf | Bin 374621 -> 0 bytes .../pdf/CMC_terms_and_conditions2006_A4.pdf | Bin 104165 -> 0 bytes 23 files changed, 1182 insertions(+), 417 deletions(-) delete mode 100644 go/internal/cmc/pdf/description_formatter.go delete mode 100644 go/internal/cmc/pdf/description_formatter_test.go delete mode 100644 go/internal/cmc/pdf/html_generator.go.applied delete mode 100644 go/internal/cmc/pdf/html_generator.go.backup delete mode 100644 go/internal/cmc/pdf/html_generator.go.patch create mode 100644 go/internal/cmc/pdf/invoice_builder.go create mode 100644 go/internal/cmc/pdf/quote_builder.go create mode 100644 go/internal/cmc/pdf/templates/quote.html delete mode 100644 php/app/webroot/pdf/CMC Invoice.pdf delete mode 100644 php/app/webroot/pdf/CMCIN11704.pdf delete mode 100644 php/app/webroot/pdf/CMCIN9387.pdf delete mode 100644 php/app/webroot/pdf/CMC_terms_and_conditions2006_A4.pdf 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
  1. tags - if !strings.Contains(output, "
      ") { - t.Error("Ordered list should have
        tag") - } - - if !strings.Contains(output, "
      1. -4 deg C
      2. ") { - 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}} +
          +

          CMC TECHNOLOGIES

          +

          PTY LIMITED ACN: 085 991 224    ABN: 47 085 991 224

          +
          +
          + +
          + + +
          +
          +
          Engineering &
          +
          Industrial
          +
          Instrumentation
          +
          +
          +
          Phone: +61 2 9669 4000
          +
          Fax: +61 2 9669 4111
          +
          Email: sales@cmctechnologies.com.au
          +
          Web Site: www.cmctechnologies.net.au
          +
          Unit 19, 77 Bourke Rd
          +
          Alexandria NSW 2015
          +
          AUSTRALIA
          +
          +
          + + + + + + + + + + + + + + + + + + + + + + + + + + + +
          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}} + + +
          +

          PRICING & SPECIFICATIONS

          +
          + + + + + + + + + + + + + + {{range .LineItems}} + + + + + + + + {{end}} + +
          ITEMQTYDESCRIPTIONUNIT PRICETOTAL
          {{.ItemNumber}}{{.Quantity}} + {{.Title}} + {{if .Description}} +
          {{.Description}}
          + {{end}} +
          {{formatPrice .GrossUnitPrice}}{{formatPrice .GrossPrice}}
          + + +
          + + + + + + {{if .ShowGST}} + + + + + {{end}} + + + + +
          SUBTOTAL{{formatTotal .Subtotal}}
          GST (10%){{formatTotal .GSTAmount}}
          {{if .ShowGST}}TOTAL PAYABLE{{else}}TOTAL{{end}}{{formatTotal .Total}}
          +
          + + + {{if .CommercialComments}} +
          +

          COMMERCIAL COMMENTS

          +

          {{.CommercialComments}}

          +
          + {{end}} + + + {{if or .DeliveryTime .PaymentTerms .DaysValid .DeliveryPoint .ExchangeRate .CustomsDuty .GSTPhrase .SalesEngineer}} + + {{if .DeliveryTime}} + + + + + {{end}} + {{if .PaymentTerms}} + + + + + {{end}} + {{if .DaysValid}} + + + + + {{end}} + {{if .DeliveryPoint}} + + + + + {{end}} + {{if .ExchangeRate}} + + + + + {{end}} + {{if .CustomsDuty}} + + + + + {{end}} + {{if .GSTPhrase}} + + + + + {{end}} + {{if .SalesEngineer}} + + + + + {{end}} +
          DELIVERY TIME:{{.DeliveryTime}}
          PAYMENT TERMS:{{.PaymentTerms}}
          QUOTATION VALID FOR:{{.DaysValid}} days from date of issue
          DELIVERY POINT:{{.DeliveryPoint}}
          EXCHANGE RATE:{{.ExchangeRate}}
          CUSTOMS DUTY:{{.CustomsDuty}}
          GST:{{.GSTPhrase}}
          SALES ENGINEER:{{.SalesEngineer}}
          + {{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); } ?>
          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); + } +} ?>