diff --git a/go/cmd/server/main.go b/go/cmd/server/main.go index 5121c2f6..dc86ae7b 100644 --- a/go/cmd/server/main.go +++ b/go/cmd/server/main.go @@ -63,6 +63,7 @@ func main() { // Load handlers quoteHandler := quotes.NewQuotesHandler(queries, tmpl, emailService) attachmentHandler := attachments.NewAttachmentHandler(queries) + documentHandler := documents.NewDocumentHandler(queries) // Setup routes r := mux.NewRouter() @@ -83,12 +84,12 @@ func main() { goRouter.HandleFunc("/attachments/{id}", attachmentHandler.Delete).Methods("DELETE") // Document generation routes - goRouter.HandleFunc("/document/generate/invoice", documents.GenerateInvoicePDF).Methods("POST") - goRouter.HandleFunc("/document/generate/quote", documents.GenerateQuotePDF).Methods("POST") - goRouter.HandleFunc("/document/generate/purchase-order", documents.GeneratePurchaseOrderPDF).Methods("POST") - goRouter.HandleFunc("/document/generate/packing-list", documents.GeneratePackingListPDF).Methods("POST") - goRouter.HandleFunc("/document/generate/order-acknowledgement", documents.GenerateOrderAckPDF).Methods("POST") - goRouter.HandleFunc("/document/page-count", documents.CountPages).Methods("POST") + goRouter.HandleFunc("/document/generate/invoice", documentHandler.GenerateInvoicePDF).Methods("POST") + goRouter.HandleFunc("/document/generate/quote", documentHandler.GenerateQuotePDF).Methods("POST") + goRouter.HandleFunc("/document/generate/purchase-order", documentHandler.GeneratePurchaseOrderPDF).Methods("POST") + goRouter.HandleFunc("/document/generate/packing-list", documentHandler.GeneratePackingListPDF).Methods("POST") + goRouter.HandleFunc("/document/generate/order-acknowledgement", documentHandler.GenerateOrderAckPDF).Methods("POST") + goRouter.HandleFunc("/document/page-count", documentHandler.CountPages).Methods("POST") // Serve generated PDFs pdfDir := os.Getenv("PDF_OUTPUT_DIR") diff --git a/go/internal/cmc/documents/counter.go b/go/internal/cmc/documents/counter.go index 162bb837..bf4d769a 100644 --- a/go/internal/cmc/documents/counter.go +++ b/go/internal/cmc/documents/counter.go @@ -1,4 +1,4 @@ -package pdf +package documents import ( "fmt" diff --git a/go/internal/cmc/documents/html_generator.go b/go/internal/cmc/documents/html_generator.go index 25044037..910469d3 100644 --- a/go/internal/cmc/documents/html_generator.go +++ b/go/internal/cmc/documents/html_generator.go @@ -1,4 +1,4 @@ -package pdf +package documents import ( "context" @@ -376,12 +376,17 @@ func (g *HTMLDocumentGenerator) loadLogo(logoFileName string) string { return logoFileName } -// GeneratePurchaseOrderPDF creates a PDF purchase order from HTML template +// GeneratePurchaseOrderPDF creates a PDF purchase order from HTML template with T&C merging // Returns (filename, error) func (g *HTMLDocumentGenerator) GeneratePurchaseOrderPDF(data *PurchaseOrderPDFData) (string, error) { - fmt.Println("=== HTML Generator: Starting purchase order generation ===") + fmt.Println("=== HTML Generator: Starting purchase order 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.BuildPurchaseOrderHTML(data, 0, 0) + + fmt.Printf("=== HTML Generator: Generated %d bytes of HTML ===\n", len(html)) + tempHTML := filepath.Join(g.outputDir, "temp_po.html") if err := ioutil.WriteFile(tempHTML, []byte(html), 0644); err != nil { return "", fmt.Errorf("failed to write temp HTML: %w", err) @@ -389,27 +394,106 @@ func (g *HTMLDocumentGenerator) GeneratePurchaseOrderPDF(data *PurchaseOrderPDFD defer os.Remove(tempHTML) defer os.Remove(filepath.Join(g.outputDir, "quote_logo.png")) - poNumber := data.PurchaseOrder.Title + // Generate temp PDF + tempPDFPath := filepath.Join(g.outputDir, "temp_po_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 PO + poPageCount, err := g.getPageCount(tempPDFPath) + if err != nil { + fmt.Printf("Warning: Could not extract PO page count: %v\n", err) + poPageCount = 1 + } + + // Check if T&C exists and merge to get total page count + totalPageCount := poPageCount + termsPath := filepath.Join(g.outputDir, "CMC_terms_and_conditions2006_A4.pdf") + tempMergedPath := filepath.Join(g.outputDir, "temp_po_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 PO count: %v\n", err) + totalPageCount = poPageCount + } else { + fmt.Printf("=== HTML Generator: Total pages (PO + 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 + fmt.Println("=== HTML Generator: Second pass - regenerating final PDF ===") + html = g.BuildPurchaseOrderHTML(data, 0, 0) + + 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 + poNumber := data.Document.CmcReference if poNumber == "" { - poNumber = fmt.Sprintf("PO-%d", data.PurchaseOrder.ID) + poNumber = fmt.Sprintf("PO-%d", data.Document.ID) } filename := fmt.Sprintf("%s.pdf", poNumber) pdfPath := filepath.Join(g.outputDir, filename) if err := g.htmlToPDF(tempHTML, pdfPath); err != nil { - return "", fmt.Errorf("failed to convert HTML to PDF: %w", err) + return "", 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", poNumber)) + if err := MergePDFs(pdfPath, termsPath, tempMergedPath); err != nil { + fmt.Printf("=== HTML Generator: Warning - could not merge T&C PDF: %v. Returning PO without T&C.\n", err) + return filename, nil + } + + // Replace original PDF with merged version + if err := os.Rename(tempMergedPath, pdfPath); err != nil { + fmt.Printf("=== HTML Generator: Warning - could not replace original with merged: %v. Returning unmerged.\n", err) + return filename, nil + } + + fmt.Println("=== HTML Generator: Replaced PDF successfully") + } + + // Verify the file exists and get final size + fileInfo, err := os.Stat(pdfPath) + if err != nil { + return "", fmt.Errorf("failed to verify final PDF: %w", err) + } + + fmt.Printf("=== HTML Generator: Final PDF verified, size=%d bytes\n", fileInfo.Size()) return filename, nil } -// GeneratePackingListPDF creates a PDF packing list from HTML template +// GeneratePackingListPDF creates a PDF packing list from HTML template with T&C merging // Returns (filename, error) func (g *HTMLDocumentGenerator) GeneratePackingListPDF(data *PackingListPDFData) (string, error) { - fmt.Println("=== HTML Generator: Starting packing list generation ===") + fmt.Println("=== HTML Generator: Starting packing list 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.BuildPackingListHTML(data, 0, 0) + + fmt.Printf("=== HTML Generator: Generated %d bytes of HTML ===\n", len(html)) + tempHTML := filepath.Join(g.outputDir, "temp_packinglist.html") if err := ioutil.WriteFile(tempHTML, []byte(html), 0644); err != nil { return "", fmt.Errorf("failed to write temp HTML: %w", err) @@ -417,6 +501,53 @@ func (g *HTMLDocumentGenerator) GeneratePackingListPDF(data *PackingListPDFData) defer os.Remove(tempHTML) defer os.Remove(filepath.Join(g.outputDir, "quote_logo.png")) + // Generate temp PDF + tempPDFPath := filepath.Join(g.outputDir, "temp_packinglist_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 packing list + packingListPageCount, err := g.getPageCount(tempPDFPath) + if err != nil { + fmt.Printf("Warning: Could not extract packing list page count: %v\n", err) + packingListPageCount = 1 + } + + // Check if T&C exists and merge to get total page count + totalPageCount := packingListPageCount + termsPath := filepath.Join(g.outputDir, "CMC_terms_and_conditions2006_A4.pdf") + tempMergedPath := filepath.Join(g.outputDir, "temp_packinglist_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 packing list count: %v\n", err) + totalPageCount = packingListPageCount + } else { + fmt.Printf("=== HTML Generator: Total pages (packing list + 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 + fmt.Println("=== HTML Generator: Second pass - regenerating final PDF ===") + html = g.BuildPackingListHTML(data, 0, 0) + + 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 packingListNumber := data.PackingList.CmcReference if packingListNumber == "" { packingListNumber = fmt.Sprintf("PackingList-%d", data.PackingList.ID) @@ -426,18 +557,50 @@ func (g *HTMLDocumentGenerator) GeneratePackingListPDF(data *PackingListPDFData) pdfPath := filepath.Join(g.outputDir, filename) if err := g.htmlToPDF(tempHTML, pdfPath); err != nil { - return "", fmt.Errorf("failed to convert HTML to PDF: %w", err) + return "", 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", packingListNumber)) + if err := MergePDFs(pdfPath, termsPath, tempMergedPath); err != nil { + fmt.Printf("=== HTML Generator: Warning - could not merge T&C PDF: %v. Returning packing list without T&C.\n", err) + return filename, nil + } + + // Replace original PDF with merged version + if err := os.Rename(tempMergedPath, pdfPath); err != nil { + fmt.Printf("=== HTML Generator: Warning - could not replace original with merged: %v. Returning unmerged.\n", err) + return filename, nil + } + + fmt.Println("=== HTML Generator: Replaced PDF successfully") + } + + // Verify the file exists and get final size + fileInfo, err := os.Stat(pdfPath) + if err != nil { + return "", fmt.Errorf("failed to verify final PDF: %w", err) + } + + fmt.Printf("=== HTML Generator: Final PDF verified, size=%d bytes\n", fileInfo.Size()) return filename, nil } -// GenerateOrderAckPDF creates a PDF order acknowledgement from HTML template +// GenerateOrderAckPDF creates a PDF order acknowledgement from HTML template with T&C merging // Returns (filename, error) func (g *HTMLDocumentGenerator) GenerateOrderAckPDF(data *OrderAckPDFData) (string, error) { - fmt.Println("=== HTML Generator: Starting order acknowledgement generation ===") + fmt.Println("=== HTML Generator: Starting order acknowledgement 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.BuildOrderAckHTML(data, 0, 0) + + fmt.Printf("=== HTML Generator: Generated %d bytes of HTML ===\n", len(html)) + tempHTML := filepath.Join(g.outputDir, "temp_orderack.html") if err := ioutil.WriteFile(tempHTML, []byte(html), 0644); err != nil { return "", fmt.Errorf("failed to write temp HTML: %w", err) @@ -445,6 +608,53 @@ func (g *HTMLDocumentGenerator) GenerateOrderAckPDF(data *OrderAckPDFData) (stri defer os.Remove(tempHTML) defer os.Remove(filepath.Join(g.outputDir, "quote_logo.png")) + // Generate temp PDF + tempPDFPath := filepath.Join(g.outputDir, "temp_orderack_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 order ack + orderAckPageCount, err := g.getPageCount(tempPDFPath) + if err != nil { + fmt.Printf("Warning: Could not extract order ack page count: %v\n", err) + orderAckPageCount = 1 + } + + // Check if T&C exists and merge to get total page count + totalPageCount := orderAckPageCount + termsPath := filepath.Join(g.outputDir, "CMC_terms_and_conditions2006_A4.pdf") + tempMergedPath := filepath.Join(g.outputDir, "temp_orderack_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 order ack count: %v\n", err) + totalPageCount = orderAckPageCount + } else { + fmt.Printf("=== HTML Generator: Total pages (order ack + 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 + fmt.Println("=== HTML Generator: Second pass - regenerating final PDF ===") + html = g.BuildOrderAckHTML(data, 0, 0) + + 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 orderAckNumber := data.OrderAcknowledgement.CmcReference if orderAckNumber == "" { orderAckNumber = fmt.Sprintf("OrderAck-%d", data.OrderAcknowledgement.ID) @@ -454,8 +664,35 @@ func (g *HTMLDocumentGenerator) GenerateOrderAckPDF(data *OrderAckPDFData) (stri pdfPath := filepath.Join(g.outputDir, filename) if err := g.htmlToPDF(tempHTML, pdfPath); err != nil { - return "", fmt.Errorf("failed to convert HTML to PDF: %w", err) + return "", 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", orderAckNumber)) + if err := MergePDFs(pdfPath, termsPath, tempMergedPath); err != nil { + fmt.Printf("=== HTML Generator: Warning - could not merge T&C PDF: %v. Returning order ack without T&C.\n", err) + return filename, nil + } + + // Replace original PDF with merged version + if err := os.Rename(tempMergedPath, pdfPath); err != nil { + fmt.Printf("=== HTML Generator: Warning - could not replace original with merged: %v. Returning unmerged.\n", err) + return filename, nil + } + + fmt.Println("=== HTML Generator: Replaced PDF successfully") + } + + // Verify the file exists and get final size + fileInfo, err := os.Stat(pdfPath) + if err != nil { + return "", fmt.Errorf("failed to verify final PDF: %w", err) + } + + fmt.Printf("=== HTML Generator: Final PDF verified, size=%d bytes\n", fileInfo.Size()) return filename, nil } diff --git a/go/internal/cmc/documents/html_types.go b/go/internal/cmc/documents/html_types.go index 64f88e6f..5cf8bd77 100644 --- a/go/internal/cmc/documents/html_types.go +++ b/go/internal/cmc/documents/html_types.go @@ -1,4 +1,4 @@ -package pdf +package documents import ( "fmt" @@ -111,6 +111,7 @@ type OrderAckPDFData struct { ShipVia string FOB string PaymentTerms string + FreightDetails string CurrencyCode string CurrencySymbol string ShowGST bool diff --git a/go/internal/cmc/documents/invoice_builder.go b/go/internal/cmc/documents/invoice_builder.go index 890b0b83..66f67e70 100644 --- a/go/internal/cmc/documents/invoice_builder.go +++ b/go/internal/cmc/documents/invoice_builder.go @@ -1,4 +1,4 @@ -package pdf +package documents import ( "bytes" @@ -42,6 +42,7 @@ func (g *HTMLDocumentGenerator) BuildInvoiceHTML(data *InvoicePDFData, totalPage ShowGST bool PageCount int CurrentPage int + FreightDetails template.HTML LogoDataURI string }{ InvoiceNumber: invoiceNum, @@ -61,6 +62,7 @@ func (g *HTMLDocumentGenerator) BuildInvoiceHTML(data *InvoicePDFData, totalPage ShowGST: data.ShowGST, PageCount: totalPages, CurrentPage: currentPage, + FreightDetails: template.HTML(data.ShippingDetails), LogoDataURI: g.loadLogo("invoice_logo.png"), } @@ -71,21 +73,32 @@ func (g *HTMLDocumentGenerator) BuildInvoiceHTML(data *InvoicePDFData, totalPage Description: template.HTML(item.Description), // Allow HTML in description Quantity: item.Quantity, GrossUnitPrice: item.GrossUnitPrice, + GrossUnitPriceText: item.UnitPriceString, DiscountAmountTotal: item.DiscountAmountTotal, GrossPrice: item.GrossPrice, + GrossPriceText: item.GrossPriceString, + HasTextPrices: item.HasTextPrices, }) } // Define template functions funcMap := template.FuncMap{ - "formatPrice": func(price sql.NullString) template.HTML { + "formatPrice": func(price sql.NullString, textPrice sql.NullString) template.HTML { + // If a text price string is provided, use it as-is + if textPrice.Valid && textPrice.String != "" { + return template.HTML(textPrice.String) + } + // Otherwise format the numeric price 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 { + "formatDiscount": func(discount sql.NullString, hasTextPrices bool) template.HTML { + if hasTextPrices { + return template.HTML("-") + } if !discount.Valid || discount.String == "" { return template.HTML(fmt.Sprintf("%s0.00", data.CurrencySymbol)) } @@ -170,8 +183,11 @@ type LineItemTemplateData struct { Description template.HTML Quantity string GrossUnitPrice sql.NullString + GrossUnitPriceText sql.NullString DiscountAmountTotal sql.NullString GrossPrice sql.NullString + GrossPriceText sql.NullString + HasTextPrices bool } // FormatPriceWithCommas formats a price string with comma separators diff --git a/go/internal/cmc/documents/merge.go b/go/internal/cmc/documents/merge.go index da064c60..19d64724 100644 --- a/go/internal/cmc/documents/merge.go +++ b/go/internal/cmc/documents/merge.go @@ -1,7 +1,7 @@ //go:build never // +build never -package pdf +package documents import ( "fmt" diff --git a/go/internal/cmc/documents/merge_test.go b/go/internal/cmc/documents/merge_test.go index 440b3249..618ebae6 100644 --- a/go/internal/cmc/documents/merge_test.go +++ b/go/internal/cmc/documents/merge_test.go @@ -1,7 +1,7 @@ //go:build never // +build never -package pdf -package pdf +package documents +package documents import ( "os" diff --git a/go/internal/cmc/documents/order_ack_builder.go b/go/internal/cmc/documents/order_ack_builder.go index 7d97d4cd..b634f479 100644 --- a/go/internal/cmc/documents/order_ack_builder.go +++ b/go/internal/cmc/documents/order_ack_builder.go @@ -1,4 +1,4 @@ -package pdf +package documents import ( "bytes" @@ -10,12 +10,15 @@ import ( // OrderAckLineItemTemplateData represents an order ack line item for template rendering type OrderAckLineItemTemplateData struct { - Title string - Description template.HTML - Quantity string - UnitPrice float64 - Discount float64 - TotalPrice float64 + Title string + Description template.HTML + Quantity string + UnitPrice float64 + UnitPriceText string + Discount float64 + TotalPrice float64 + TotalPriceText string + HasTextPrices bool } // BuildOrderAckHTML generates the complete HTML for an order acknowledgement using templates @@ -30,25 +33,41 @@ func (g *HTMLDocumentGenerator) BuildOrderAckHTML(data *OrderAckPDFData, totalPa unitPrice := 0.0 totalPrice := 0.0 discount := 0.0 + unitPriceText := "" + totalPriceText := "" - if item.GrossUnitPrice.Valid { + // Check for text price strings first + hasTextPrices := false + if item.UnitPriceString.Valid && item.UnitPriceString.String != "" { + hasTextPrices = true + unitPriceText = item.UnitPriceString.String + } else if item.GrossUnitPrice.Valid { unitPrice, _ = strconv.ParseFloat(item.GrossUnitPrice.String, 64) } - if item.GrossPrice.Valid { + + if item.GrossPriceString.Valid && item.GrossPriceString.String != "" { + hasTextPrices = true + totalPriceText = item.GrossPriceString.String + // Don't add text prices to subtotal + } else if item.GrossPrice.Valid { totalPrice, _ = strconv.ParseFloat(item.GrossPrice.String, 64) subtotal += totalPrice } + if item.DiscountAmountTotal.Valid { discount, _ = strconv.ParseFloat(item.DiscountAmountTotal.String, 64) } lineItemsData = append(lineItemsData, OrderAckLineItemTemplateData{ - Title: item.Title, - Description: template.HTML(item.Description), - Quantity: item.Quantity, - UnitPrice: unitPrice, - Discount: discount, - TotalPrice: totalPrice, + Title: item.Title, + Description: template.HTML(item.Description), + Quantity: item.Quantity, + UnitPrice: unitPrice, + UnitPriceText: unitPriceText, + Discount: discount, + TotalPrice: totalPrice, + TotalPriceText: totalPriceText, + HasTextPrices: hasTextPrices || item.HasTextPrices, }) } @@ -68,11 +87,12 @@ func (g *HTMLDocumentGenerator) BuildOrderAckHTML(data *OrderAckPDFData, totalPa IssueDateString string YourReference string JobTitle string - BillTo string - ShipTo string + BillTo template.HTML + ShipTo template.HTML ShipVia string FOB string PaymentTerms string + CustomerABN string CurrencyCode string CurrencySymbol string LineItems []OrderAckLineItemTemplateData @@ -80,6 +100,9 @@ func (g *HTMLDocumentGenerator) BuildOrderAckHTML(data *OrderAckPDFData, totalPa GSTAmount float64 Total float64 ShowGST bool + PageCount int + CurrentPage int + FreightDetails template.HTML LogoDataURI string }{ OrderAckNumber: orderAckNumber, @@ -89,11 +112,12 @@ func (g *HTMLDocumentGenerator) BuildOrderAckHTML(data *OrderAckPDFData, totalPa IssueDateString: data.IssueDateString, YourReference: data.YourReference, JobTitle: data.JobTitle, - BillTo: data.BillTo, - ShipTo: data.ShipTo, + BillTo: template.HTML(data.BillTo), + ShipTo: template.HTML(data.ShipTo), ShipVia: data.ShipVia, FOB: data.FOB, PaymentTerms: data.PaymentTerms, + CustomerABN: "", CurrencyCode: data.CurrencyCode, CurrencySymbol: data.CurrencySymbol, LineItems: lineItemsData, @@ -101,15 +125,34 @@ func (g *HTMLDocumentGenerator) BuildOrderAckHTML(data *OrderAckPDFData, totalPa GSTAmount: gstAmount, Total: total, ShowGST: data.ShowGST, + PageCount: totalPages, + CurrentPage: currentPage, + FreightDetails: template.HTML(data.FreightDetails), LogoDataURI: g.loadLogo("quote_logo.png"), } // Define template functions funcMap := template.FuncMap{ - "formatPrice": func(price float64) template.HTML { + "formatPrice": func(price float64, textPrice string) template.HTML { + // If a text price string is provided, use it as-is + if textPrice != "" { + return template.HTML(textPrice) + } + // Otherwise format the numeric price formatted := FormatPriceWithCommas(fmt.Sprintf("%.2f", price)) return template.HTML(fmt.Sprintf("%s%s", data.CurrencySymbol, formatted)) }, + "formatDiscount": func(discount float64, hasTextPrices bool) template.HTML { + if hasTextPrices { + return template.HTML("-") + } + // Show 0.00 when no discount, otherwise prefix with minus + if discount <= 0 { + return template.HTML(fmt.Sprintf("%s0.00", data.CurrencySymbol)) + } + formatted := FormatPriceWithCommas(fmt.Sprintf("%.2f", discount)) + 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)) diff --git a/go/internal/cmc/documents/packing_list_builder.go b/go/internal/cmc/documents/packing_list_builder.go index 888e79e8..c3894ee6 100644 --- a/go/internal/cmc/documents/packing_list_builder.go +++ b/go/internal/cmc/documents/packing_list_builder.go @@ -1,4 +1,4 @@ -package pdf +package documents import ( "bytes" diff --git a/go/internal/cmc/documents/page_numbers.go b/go/internal/cmc/documents/page_numbers.go index da12dcf4..3f5a8692 100644 --- a/go/internal/cmc/documents/page_numbers.go +++ b/go/internal/cmc/documents/page_numbers.go @@ -1,4 +1,4 @@ -package pdf +package documents import ( "fmt" diff --git a/go/internal/cmc/documents/purchase_order_builder.go b/go/internal/cmc/documents/purchase_order_builder.go index d11bcbba..6779bea0 100644 --- a/go/internal/cmc/documents/purchase_order_builder.go +++ b/go/internal/cmc/documents/purchase_order_builder.go @@ -1,4 +1,4 @@ -package pdf +package documents import ( "bytes" @@ -14,6 +14,7 @@ type PurchaseOrderLineItemTemplateData struct { Description template.HTML Quantity string UnitPrice float64 + Discount float64 TotalPrice float64 } @@ -28,6 +29,7 @@ func (g *HTMLDocumentGenerator) BuildPurchaseOrderHTML(data *PurchaseOrderPDFDat for _, item := range data.LineItems { unitPrice := 0.0 totalPrice := 0.0 + discount := 0.0 if item.NetUnitPrice.Valid { unitPrice, _ = strconv.ParseFloat(item.NetUnitPrice.String, 64) @@ -36,12 +38,16 @@ func (g *HTMLDocumentGenerator) BuildPurchaseOrderHTML(data *PurchaseOrderPDFDat totalPrice, _ = strconv.ParseFloat(item.NetPrice.String, 64) subtotal += totalPrice } + if item.DiscountAmountTotal.Valid { + discount, _ = strconv.ParseFloat(item.DiscountAmountTotal.String, 64) + } lineItemsData = append(lineItemsData, PurchaseOrderLineItemTemplateData{ Title: item.Title, Description: template.HTML(item.Description), Quantity: item.Quantity, UnitPrice: unitPrice, + Discount: discount, TotalPrice: totalPrice, }) } @@ -57,12 +63,12 @@ func (g *HTMLDocumentGenerator) BuildPurchaseOrderHTML(data *PurchaseOrderPDFDat templateData := struct { PONumber string PrincipleName string - YourReference string + YourReference template.HTML IssueDateString string - OrderedFrom string - DeliverTo string + OrderedFrom template.HTML + DeliverTo template.HTML DispatchBy string - ShippingInstructions string + ShippingInstructions template.HTML CurrencyCode string CurrencySymbol string LineItems []PurchaseOrderLineItemTemplateData @@ -74,12 +80,12 @@ func (g *HTMLDocumentGenerator) BuildPurchaseOrderHTML(data *PurchaseOrderPDFDat }{ PONumber: poNumber, PrincipleName: data.Principle.Name, - YourReference: data.PurchaseOrder.PrincipleReference, + YourReference: template.HTML(data.PurchaseOrder.PrincipleReference), IssueDateString: data.IssueDateString, - OrderedFrom: data.PurchaseOrder.OrderedFrom, - DeliverTo: data.PurchaseOrder.DeliverTo, + OrderedFrom: template.HTML(data.PurchaseOrder.OrderedFrom), + DeliverTo: template.HTML(data.PurchaseOrder.DeliverTo), DispatchBy: data.PurchaseOrder.DispatchBy, - ShippingInstructions: data.PurchaseOrder.ShippingInstructions, + ShippingInstructions: template.HTML(data.PurchaseOrder.ShippingInstructions), CurrencyCode: data.CurrencyCode, CurrencySymbol: data.CurrencySymbol, LineItems: lineItemsData, diff --git a/go/internal/cmc/documents/quote_builder.go b/go/internal/cmc/documents/quote_builder.go index dd125297..b5d33c0a 100644 --- a/go/internal/cmc/documents/quote_builder.go +++ b/go/internal/cmc/documents/quote_builder.go @@ -1,4 +1,4 @@ -package pdf +package documents import ( "bytes" diff --git a/go/internal/cmc/documents/templates/invoice.html b/go/internal/cmc/documents/templates/invoice.html index e2bdd473..eebc7a6d 100644 --- a/go/internal/cmc/documents/templates/invoice.html +++ b/go/internal/cmc/documents/templates/invoice.html @@ -384,9 +384,9 @@ {{.Title}}
{{.Description}} {{.Quantity}} - {{formatPrice .GrossUnitPrice}} - {{formatDiscount .DiscountAmountTotal}} - {{formatPrice .GrossPrice}} + {{formatPrice .GrossUnitPrice .GrossUnitPriceText}} + {{formatDiscount .DiscountAmountTotal .HasTextPrices}} + {{formatPrice .GrossPrice .GrossPriceText}} {{end}} @@ -395,13 +395,28 @@
-

MAKE PAYMENT TO:

- - - - - - +

MAKE PAYMENT TO:

+
Account Name:CMC Technologies Pty Ltd
BSB:062-458
Account Number:10067982
SWIFT Code:CTBAAU2S
IBAN:0624581006782
+ + + + + + + + + + + + + + + + + + + +
Account Name:CMC Technologies Pty Ltd
BSB:062-458
Account Number:10067982
SWIFT:CTBAAU2S
IBAN:0624581006782
@@ -414,5 +429,15 @@
+ + +
+

FREIGHT DETAILS:

+
+ {{if .FreightDetails}} + {{.FreightDetails}} + {{end}} +
+
diff --git a/go/internal/cmc/documents/templates/order-acknowledgement.html b/go/internal/cmc/documents/templates/order-acknowledgement.html index 36590e49..d3b853ca 100644 --- a/go/internal/cmc/documents/templates/order-acknowledgement.html +++ b/go/internal/cmc/documents/templates/order-acknowledgement.html @@ -14,10 +14,26 @@ font-size: 9pt; line-height: 1.4; margin: 0; - padding: 0 0 25mm 0; + padding: 0; color: #000; } + a { + color: #0000FF; + text-decoration: underline; + } + + .header { + text-align: center; + margin-bottom: 10mm; + } + + .header h1 { + font-size: 20pt; + margin: 0 0 5mm 0; + font-weight: bold; + } + .details-table { width: 100%; border-collapse: collapse; @@ -196,26 +212,39 @@ {{template "CompanyHeader" .}} - - + + +
+
+
+

ORDER ACKNOWLEDGEMENT

+
+
+ {{if .OrderAckNumber}} +
ORDER ACK# {{.OrderAckNumber}}
+ {{end}} + {{if .IssueDateString}} +
Date: {{.IssueDateString}}
+ {{end}} + {{if .PageCount}} +
Page: {{.CurrentPage}} of {{.PageCount}}
+ {{end}} +
+
+ + - - + + - - - - - - @@ -224,29 +253,37 @@
COMPANY NAME: {{.CompanyName}}ORDER ACK #:{{.OrderAckNumber}}YOUR REFERENCE:{{.YourReference}}
CONTACT: {{.Attention}}DATE:{{.IssueDateString}}
EMAIL: {{.EmailTo}}YOUR REFERENCE:{{.YourReference}}
JOB TITLE:
-
-
-

BILL TO:

-

{{.BillTo}}

+
+
+

Sold To / Invoice Address:

+
{{.BillTo}}
-
-

SHIP TO:

-

{{.ShipTo}}

+
+

Delivery Address:

+
{{.ShipTo}}
- - - - - - - - - - - - + +
SHIP VIA:{{.ShipVia}}FOB:{{.FOB}}
PAYMENT TERMS:{{.PaymentTerms}}
+ + + + + + + + + + + + + + + + + +
CUSTOMER ORDER NOCMC JOB #INCOTERMS 2010PAYMENT TERMSCUSTOMER ABN
{{.YourReference}}{{.JobTitle}}{{.FOB}}{{.PaymentTerms}}{{.CustomerABN}}
@@ -268,34 +305,62 @@ {{range .LineItems}} - {{.Title}}{{if .Description}}
{{.Description}}{{end}} + {{.Title}}{{if .Description}}
{{.Description}}{{end}} {{.Quantity}} - {{formatPrice .UnitPrice}} - {{if .Discount}}{{formatPrice .Discount}}{{else}}$0.00{{end}} - {{formatPrice .TotalPrice}} + {{formatPrice .UnitPrice .UnitPriceText}} + {{formatDiscount .Discount .HasTextPrices}} + {{formatPrice .TotalPrice .TotalPriceText}} {{end}} - -
- - - - - - {{if .ShowGST}} - - - - + +
+
+

MAKE PAYMENT TO:

+
SUBTOTAL{{formatTotal .Subtotal}}
GST (10%){{formatTotal .GSTAmount}}
+ + + + + + + + + + + + + + + + + + + + +
Account Name:CMC Technologies Pty Ltd
BSB:062-458
Account Number:10067982
SWIFT:CTBAAU2S
IBAN:0624581006782
+
+
+ + + {{if .ShowGST}} + + {{end}} + +
Subtotal:{{formatTotal .Subtotal}}
GST (10%):{{formatTotal .GSTAmount}}
TOTAL:{{formatTotal .Total}}
+
+
+ + +
+

FREIGHT DETAILS:

+
+ {{if .FreightDetails}} + {{.FreightDetails}} {{end}} - - {{if .ShowGST}}TOTAL PAYABLE{{else}}TOTAL{{end}} - {{formatTotal .Total}} - - +
diff --git a/go/internal/cmc/documents/templates/packing-list.html b/go/internal/cmc/documents/templates/packing-list.html index e4629337..3dd97652 100644 --- a/go/internal/cmc/documents/templates/packing-list.html +++ b/go/internal/cmc/documents/templates/packing-list.html @@ -190,7 +190,7 @@ {{range .LineItems}} - {{.Title}}{{if .Description}}
{{.Description}}{{end}} + {{.Title}}{{if .Description}}
{{.Description}}{{end}} {{.Quantity}} {{if .Weight}}{{.Weight}}{{else}}-{{end}} diff --git a/go/internal/cmc/documents/templates/purchase-order.html b/go/internal/cmc/documents/templates/purchase-order.html index 93a63965..86643702 100644 --- a/go/internal/cmc/documents/templates/purchase-order.html +++ b/go/internal/cmc/documents/templates/purchase-order.html @@ -65,7 +65,7 @@ } .line-items .description { - width: 50%; + width: 40%; word-wrap: break-word; overflow-wrap: break-word; } @@ -76,7 +76,12 @@ } .line-items .unit-price { - width: 20%; + width: 15%; + text-align: right; + } + + .line-items .discount { + width: 15%; text-align: right; } @@ -221,27 +226,29 @@
{{end}} - -
+ +
Shown in {{.CurrencyCode}}
- +
+ {{range .LineItems}} - + + {{end}} diff --git a/go/internal/cmc/handlers/documents/documents_api.go b/go/internal/cmc/handlers/documents/documents_api.go index d1a318b0..0b007973 100644 --- a/go/internal/cmc/handlers/documents/documents_api.go +++ b/go/internal/cmc/handlers/documents/documents_api.go @@ -1,6 +1,7 @@ package documents import ( + "context" "database/sql" "encoding/json" "fmt" @@ -15,6 +16,18 @@ import ( pdf "code.springupsoftware.com/cmc/cmc-sales/internal/cmc/documents" ) +// DocumentHandler handles document PDF generation with database integration +type DocumentHandler struct { + queries *db.Queries +} + +// NewDocumentHandler creates a new DocumentHandler +func NewDocumentHandler(queries *db.Queries) *DocumentHandler { + return &DocumentHandler{ + queries: queries, + } +} + // escapeToHTML converts plain text to HTML with newlines as
func escapeToHTML(s string) string { s = html.EscapeString(s) @@ -137,6 +150,9 @@ func GenerateInvoicePDF(w http.ResponseWriter, r *http.Request) { GrossUnitPrice: sql.NullString{String: fmt.Sprintf("%.2f", li.UnitPrice), Valid: true}, GrossPrice: sql.NullString{String: fmt.Sprintf("%.2f", finalPrice), Valid: true}, DiscountAmountTotal: sql.NullString{String: fmt.Sprintf("%.2f", li.DiscountAmountTotal), Valid: li.DiscountAmountTotal > 0}, + HasTextPrices: li.HasTextPrices, + UnitPriceString: sql.NullString{String: li.UnitPriceString, Valid: li.UnitPriceString != ""}, + GrossPriceString: sql.NullString{String: li.GrossPriceString, Valid: li.GrossPriceString != ""}, } } @@ -319,6 +335,7 @@ type PurchaseOrderLineItemRequest = InvoiceLineItemRequest // PurchaseOrderPDFRequest payload from PHP for POs type PurchaseOrderPDFRequest struct { DocumentID int32 `json:"document_id"` + UserID int32 `json:"user_id"` Title string `json:"title"` IssueDate string `json:"issue_date"` // YYYY-MM-DD IssueDateString string `json:"issue_date_string"` // formatted date @@ -338,8 +355,8 @@ type PurchaseOrderPDFRequest struct { OutputDir string `json:"output_dir"` } -// GeneratePurchaseOrderPDF handles POST /go/pdf/generate-po -func GeneratePurchaseOrderPDF(w http.ResponseWriter, r *http.Request) { +// GeneratePurchaseOrderPDF handles POST /go/document/generate/purchase-order +func (h *DocumentHandler) GeneratePurchaseOrderPDF(w http.ResponseWriter, r *http.Request) { var req PurchaseOrderPDFRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { http.Error(w, "invalid JSON payload", http.StatusBadRequest) @@ -384,12 +401,17 @@ func GeneratePurchaseOrderPDF(w http.ResponseWriter, r *http.Request) { lineItems := make([]db.GetLineItemsTableRow, len(req.LineItems)) for i, li := range req.LineItems { + // Keep description as-is to support HTML rendering lineItems[i] = db.GetLineItemsTableRow{ - ItemNumber: li.ItemNumber, - Quantity: li.Quantity, - Title: li.Title, - GrossUnitPrice: sql.NullString{String: fmt.Sprintf("%.2f", li.UnitPrice), Valid: true}, - GrossPrice: sql.NullString{String: fmt.Sprintf("%.2f", li.TotalPrice), Valid: true}, + ItemNumber: li.ItemNumber, + Quantity: li.Quantity, + Title: li.Title, + Description: li.Description, + NetUnitPrice: sql.NullString{String: fmt.Sprintf("%.2f", li.NetUnitPrice), Valid: true}, + NetPrice: sql.NullString{String: fmt.Sprintf("%.2f", li.NetPrice), Valid: true}, + DiscountAmountTotal: sql.NullString{String: fmt.Sprintf("%.2f", li.DiscountAmountTotal), Valid: true}, + Option: li.Option != 0, + HasTextPrices: li.HasTextPrices, } } @@ -416,11 +438,33 @@ func GeneratePurchaseOrderPDF(w http.ResponseWriter, r *http.Request) { return } + // Update the document record with PDF generation info + ctx := context.Background() + updateParams := db.UpdateDocumentParams{ + ID: req.DocumentID, + PdfFilename: filename, + PdfCreatedAt: time.Now(), + PdfCreatedByUserID: req.UserID, + Type: db.DocumentsTypePurchaseOrder, + UserID: req.UserID, + DocPageCount: 0, // TODO: extract from PDF + CmcReference: "", + ShippingDetails: sql.NullString{}, + Revision: 0, + BillTo: sql.NullString{}, + ShipTo: sql.NullString{}, + EmailSentAt: time.Time{}, + EmailSentByUserID: 0, + } + + if err := h.queries.UpdateDocument(ctx, updateParams); err != nil { + log.Printf("GeneratePurchaseOrderPDF: failed to update document: %v", err) + // Don't fail the request if update fails, just log it + } + w.Header().Set("Content-Type", "application/json") _ = json.NewEncoder(w).Encode(map[string]string{"filename": filename}) -} - -// GeneratePackingListPDF handles POST /go/pdf/generate-packinglist +} // GeneratePackingListPDF handles POST /go/pdf/generate-packinglist func GeneratePackingListPDF(w http.ResponseWriter, r *http.Request) { var req PackingListPDFRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { @@ -514,11 +558,16 @@ func GenerateOrderAckPDF(w http.ResponseWriter, r *http.Request) { 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, - GrossUnitPrice: sql.NullString{String: fmt.Sprintf("%.2f", li.UnitPrice), Valid: true}, - GrossPrice: sql.NullString{String: fmt.Sprintf("%.2f", li.TotalPrice), Valid: true}, + 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}, + DiscountAmountTotal: sql.NullString{String: fmt.Sprintf("%.2f", li.DiscountAmountTotal), Valid: li.DiscountAmountTotal > 0}, + HasTextPrices: li.HasTextPrices, + UnitPriceString: sql.NullString{String: li.UnitPriceString, Valid: li.UnitPriceString != ""}, + GrossPriceString: sql.NullString{String: li.GrossPriceString, Valid: li.GrossPriceString != ""}, } } @@ -535,6 +584,7 @@ func GenerateOrderAckPDF(w http.ResponseWriter, r *http.Request) { ShipVia: req.ShipVia, FOB: req.FOB, PaymentTerms: req.PaymentTerms, + FreightDetails: req.FreightDetails, CurrencyCode: req.CurrencyCode, CurrencySymbol: req.CurrencySymbol, ShowGST: req.ShowGST, @@ -594,6 +644,7 @@ type OrderAckPDFRequest struct { ShipVia string `json:"ship_via"` FOB string `json:"fob"` PaymentTerms string `json:"payment_terms"` + FreightDetails string `json:"freight_details"` EstimatedDelivery string `json:"estimated_delivery"` CurrencySymbol string `json:"currency_symbol"` CurrencyCode string `json:"currency_code"` @@ -645,3 +696,24 @@ func CountPages(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") _ = json.NewEncoder(w).Encode(map[string]int{"page_count": pageCount}) } + +// Handler methods that delegate to standalone functions +func (h *DocumentHandler) GenerateInvoicePDF(w http.ResponseWriter, r *http.Request) { + GenerateInvoicePDF(w, r) +} + +func (h *DocumentHandler) GenerateQuotePDF(w http.ResponseWriter, r *http.Request) { + GenerateQuotePDF(w, r) +} + +func (h *DocumentHandler) GeneratePackingListPDF(w http.ResponseWriter, r *http.Request) { + GeneratePackingListPDF(w, r) +} + +func (h *DocumentHandler) GenerateOrderAckPDF(w http.ResponseWriter, r *http.Request) { + GenerateOrderAckPDF(w, r) +} + +func (h *DocumentHandler) CountPages(w http.ResponseWriter, r *http.Request) { + CountPages(w, r) +} diff --git a/go/internal/cmc/handlers/pdf_api.go b/go/internal/cmc/handlers/pdf_api.go index a45164c4..aac93f79 100644 --- a/go/internal/cmc/handlers/pdf_api.go +++ b/go/internal/cmc/handlers/pdf_api.go @@ -12,7 +12,7 @@ import ( "time" "code.springupsoftware.com/cmc/cmc-sales/internal/cmc/db" - "code.springupsoftware.com/cmc/cmc-sales/internal/cmc/pdf" + pdf "code.springupsoftware.com/cmc/cmc-sales/internal/cmc/documents" ) // escapeToHTML converts plain text to HTML with newlines as
diff --git a/php/app/controllers/documents_controller.php b/php/app/controllers/documents_controller.php index fe147f5d..097ea9d2 100755 --- a/php/app/controllers/documents_controller.php +++ b/php/app/controllers/documents_controller.php @@ -894,7 +894,11 @@ ENDINSTRUCTIONS; break; case "purchaseOrder": - $principle = $this->Document->LineItem->Product->Principle->find('first', array('conditions'=>array('Principle.id' => $document['PurchaseOrder']['principle_id']))); + // Load principle with its Currency relationship + $principle = $this->Document->LineItem->Product->Principle->find('first', array( + 'conditions'=>array('Principle.id' => $document['PurchaseOrder']['principle_id']), + 'recursive' => 1 // Load associated Currency and Country + )); $this->set('principle', $principle); $purchase_order = $this->Document->PurchaseOrder->find('first', diff --git a/php/app/controllers/purchase_orders_controller.php b/php/app/controllers/purchase_orders_controller.php index 3d80d822..f7d47d77 100755 --- a/php/app/controllers/purchase_orders_controller.php +++ b/php/app/controllers/purchase_orders_controller.php @@ -24,7 +24,11 @@ class PurchaseOrdersController extends AppController { $this->Session->setFlash(__('Invalid PurchaseOrder.', true)); $this->redirect(array('action'=>'index')); } - $this->set('purchaseOrder', $this->PurchaseOrder->read(null, $id)); + $purchaseOrder = $this->PurchaseOrder->read(null, $id); + $this->set('purchaseOrder', $purchaseOrder); + $this->set('users', $this->PurchaseOrder->User->getUsersList()); + $this->set('document', $purchaseOrder); + $this->set('docTypeFullName', 'Purchase Order'); } function add() { diff --git a/php/app/views/documents/pdf_invoice.ctp b/php/app/views/documents/pdf_invoice.ctp index c965033c..fca1a29e 100755 --- a/php/app/views/documents/pdf_invoice.ctp +++ b/php/app/views/documents/pdf_invoice.ctp @@ -136,19 +136,48 @@ if ($httpCode < 200 || $httpCode >= 300) { exit; } -// PDF generated successfully - capture the filename from Go and save to database +// PDF generated successfully - now save metadata and count pages $result = json_decode($response, true); if (isset($result['filename'])) { + // Build path, removing any double slashes + $pdfPath = $outputDir . '/' . $result['filename']; + $pdfPath = preg_replace('#/+#', '/', $pdfPath); + + // Update database with PDF metadata $Document = ClassRegistry::init('Document'); $Document->id = $document['Document']['id']; $Document->saveField('pdf_filename', $result['filename']); $Document->saveField('pdf_created_at', date('Y-m-d H:i:s')); - - // Get user ID safely + + // Get user ID safely (match quote logic) with Basic Auth fallback $userId = null; + $sessionUser = null; if (isset($this->Session)) { - $userId = $this->Session->read('Auth.User.id'); + $sessionUser = $this->Session->read('Auth.User'); + $userId = isset($sessionUser['id']) ? $sessionUser['id'] : null; } + if (!$userId && isset($_SESSION['Auth']['User']['id'])) { + $userId = $_SESSION['Auth']['User']['id']; + } + // Fallback: try Basic Auth username/email -> User lookup + if (!$userId && !empty($_SERVER['PHP_AUTH_USER'])) { + $User = ClassRegistry::init('User'); + $foundUser = $User->find('first', array( + 'conditions' => array( + 'OR' => array( + 'User.email' => $_SERVER['PHP_AUTH_USER'], + 'User.username' => $_SERVER['PHP_AUTH_USER'], + ), + ), + 'fields' => array('User.id'), + 'recursive' => -1, + 'order' => 'User.id ASC' + )); + if ($foundUser && isset($foundUser['User']['id'])) { + $userId = $foundUser['User']['id']; + } + } + if ($userId) { $Document->saveField('pdf_created_by_user_id', $userId); } @@ -156,29 +185,24 @@ if (isset($result['filename'])) { // Count pages using the Go service App::import('Vendor','pagecounter'); $pageCounter = new PageCounter(); - $pdfPath = $outputDir . '/' . $result['filename']; - error_log("=== pdf_invoice.ctp: Counting pages for PDF: " . $pdfPath . " ==="); $pageCount = $pageCounter->count($pdfPath); - error_log("=== pdf_invoice.ctp: Page count result: " . var_export($pageCount, true) . " ==="); - if ($pageCount !== null && $pageCount > 0) { + + if ($pageCount > 0) { $Document->saveField('doc_page_count', $pageCount); - error_log("=== pdf_invoice.ctp: Saved page count: " . $pageCount . " ==="); - } else { - error_log("=== pdf_invoice.ctp: Page count was null or 0, not saving ==="); } } - -error_log("=== pdf_invoice.ctp: About to redirect to /documents/view/" . $document['Document']['id'] . " ==="); ?> - + + Redirecting... -

PDF generated successfully. Click here if you are not redirected.

- +

PDF generated successfully. Redirecting back to invoice...

+

Click here if not redirected

+ \ No newline at end of file diff --git a/php/app/views/documents/pdf_orderack.ctp b/php/app/views/documents/pdf_orderack.ctp index 59f262ae..90d47fe7 100755 --- a/php/app/views/documents/pdf_orderack.ctp +++ b/php/app/views/documents/pdf_orderack.ctp @@ -1,5 +1,5 @@ isset($li['item_number']) ? $li['item_number'] : '', - 'quantity' => isset($li['quantity']) ? $li['quantity'] : '', - 'title' => isset($li['title']) ? $li['title'] : '', - 'description' => isset($li['description']) ? $li['description'] : '', - 'unit_price' => isset($li['gross_unit_price']) ? floatval($li['gross_unit_price']) : 0.0, - 'total_price' => isset($li['gross_price']) ? floatval($li['gross_price']) : 0.0, - 'net_unit_price' => isset($li['net_unit_price']) ? floatval($li['net_unit_price']) : 0.0, - 'net_price' => isset($li['net_price']) ? floatval($li['net_price']) : 0.0, - 'discount_percent' => isset($li['discount_percent']) ? floatval($li['discount_percent']) : 0.0, - 'discount_amount_unit' => isset($li['discount_amount_unit']) ? floatval($li['discount_amount_unit']) : 0.0, - 'discount_amount_total' => isset($li['discount_amount_total']) ? floatval($li['discount_amount_total']) : 0.0, - 'option' => isset($li['option']) ? intval($li['option']) : 0, - 'has_text_prices' => isset($li['has_text_prices']) ? (bool)$li['has_text_prices'] : false, - 'unit_price_string' => isset($li['unit_price_string']) ? $li['unit_price_string'] : '', - 'gross_price_string' => isset($li['gross_price_string']) ? $li['gross_price_string'] : '' - ); - } +foreach ($document['LineItem'] as $li) { + $lineItems[] = array( + 'item_number' => $li['item_number'], + 'quantity' => $li['quantity'], + 'title' => $li['title'], + 'description' => isset($li['description']) ? $li['description'] : '', + 'is_html' => true, // Description is always HTML + 'unit_price' => floatval($li['gross_unit_price']), + 'total_price' => floatval($li['gross_price']), + 'net_unit_price' => floatval($li['net_unit_price']), + 'net_price' => floatval($li['net_price']), + 'discount_percent' => floatval($li['discount_percent']), + 'discount_amount_unit' => floatval($li['discount_amount_unit']), + 'discount_amount_total' => floatval($li['discount_amount_total']), + 'option' => intval($li['option']), + 'has_text_prices' => isset($li['has_text_prices']) ? (bool)$li['has_text_prices'] : false, + 'unit_price_string' => isset($li['unit_price_string']) ? $li['unit_price_string'] : '', + 'gross_price_string' => isset($li['gross_price_string']) ? $li['gross_price_string'] : '' + ); } +// Similar field extraction as invoice +$orderAckNumber = ''; +if (!empty($document['Document']['cmc_reference'])) { + $orderAckNumber = $document['Document']['cmc_reference']; +} elseif (!empty($document['OrderAcknowledgement']['title'])) { + $orderAckNumber = $document['OrderAcknowledgement']['title']; +} + +$orderAckTitle = isset($document['OrderAcknowledgement']['title']) && !empty($document['OrderAcknowledgement']['title']) + ? $document['OrderAcknowledgement']['title'] + : 'OrderAck-' . $document['Document']['id']; + +$customerName = 'Customer'; +if (isset($enquiry['Customer']['name']) && !empty($enquiry['Customer']['name'])) { + $customerName = $enquiry['Customer']['name']; +} elseif (isset($document['Document']['cmc_reference']) && !empty($document['Document']['cmc_reference'])) { + $customerName = $document['Document']['cmc_reference']; +} + +// Add fallback logic for all enquiry-dependent fields (same as invoice) +$contactEmail = isset($enquiry['Contact']['email']) ? $enquiry['Contact']['email'] : ''; +$contactName = ''; +if (isset($enquiry['Contact']['first_name']) && isset($enquiry['Contact']['last_name'])) { + $contactName = $enquiry['Contact']['first_name'] . ' ' . $enquiry['Contact']['last_name']; +} + +$userFirstName = isset($enquiry['User']['first_name']) ? $enquiry['User']['first_name'] : ''; +$userLastName = isset($enquiry['User']['last_name']) ? $enquiry['User']['last_name'] : ''; +$userEmail = isset($enquiry['User']['email']) ? $enquiry['User']['email'] : ''; + +$yourReference = ''; +if (isset($enquiry['Enquiry']['customer_reference']) && !empty($enquiry['Enquiry']['customer_reference'])) { + $yourReference = $enquiry['Enquiry']['customer_reference']; +} else if (isset($enquiry['Enquiry']['created'])) { + $yourReference = 'Enquiry on ' . date('j M Y', strtotime($enquiry['Enquiry']['created'])); +} else { + $yourReference = 'Enquiry on ' . date('j M Y'); +} + +// Calculate totals (match invoice structure) +$subtotal = isset($totals['subtotal']) ? $totals['subtotal'] : 0.0; +$gstAmount = isset($totals['gst']) ? $totals['gst'] : 0.0; +$total = isset($totals['total']) ? $totals['total'] : 0.0; + $payload = array( 'document_id' => intval($document['Document']['id']), - 'title' => $document['OrderAcknowledgement']['title'], - 'customer_name' => $enquiry['Customer']['name'], - 'job_title' => isset($job['Job']['title']) ? $job['Job']['title'] : '', + 'order_ack_number' => $orderAckNumber, + 'title' => $orderAckTitle, + 'customer_name' => $customerName, + 'contact_email' => $contactEmail, + 'contact_name' => $contactName, + 'user_first_name' => $userFirstName, + 'user_last_name' => $userLastName, + 'user_email' => $userEmail, + 'your_reference' => $yourReference, + 'ship_via' => isset($document['OrderAcknowledgement']['ship_via']) ? $document['OrderAcknowledgement']['ship_via'] : '', + 'fob' => isset($document['OrderAcknowledgement']['fob']) ? $document['OrderAcknowledgement']['fob'] : '', + 'estimated_delivery' => isset($document['OrderAcknowledgement']['estimated_delivery']) ? $document['OrderAcknowledgement']['estimated_delivery'] : '', 'issue_date' => $document['OrderAcknowledgement']['issue_date'], 'issue_date_string' => isset($issue_date_string) ? $issue_date_string : '', - 'ship_via' => $document['OrderAcknowledgement']['ship_via'], - 'fob' => $document['OrderAcknowledgement']['fob'], - 'estimated_delivery' => $document['OrderAcknowledgement']['estimated_delivery'], 'currency_symbol' => $currencySymbol, - 'currency_code' => isset($currencyCode) ? $currencyCode : 'AUD', + 'currency_code' => $currencyCode, 'show_gst' => (bool)$gst, + 'bill_to' => isset($document['Document']['bill_to']) ? $document['Document']['bill_to'] : '', + 'ship_to' => isset($document['Document']['ship_to']) ? $document['Document']['ship_to'] : '', + 'shipping_details' => isset($document['Document']['shipping_details']) ? $document['Document']['shipping_details'] : '', + 'customer_order_number' => isset($job['Job']['customer_order_number']) ? $job['Job']['customer_order_number'] : '', + 'job_title' => isset($job['Job']['title']) ? $job['Job']['title'] : '', + 'payment_terms' => isset($job['Customer']['payment_terms']) ? $job['Customer']['payment_terms'] : '', + 'customer_abn' => isset($job['Customer']['abn']) ? $job['Customer']['abn'] : '', + 'subtotal' => $subtotal, + 'gst_amount' => $gstAmount, + 'total' => $total, 'line_items' => $lineItems, 'output_dir' => $outputDir ); @@ -65,8 +124,74 @@ if ($httpCode < 200 || $httpCode >= 300) { echo "

"; exit; } -?> - +// PDF generated successfully - now save metadata and count pages +$result = json_decode($response, true); +if (isset($result['filename'])) { + // Build path, removing any double slashes + $pdfPath = $outputDir . '/' . $result['filename']; + $pdfPath = preg_replace('#/+#', '/', $pdfPath); + + // Update database with PDF metadata + $Document = ClassRegistry::init('Document'); + $Document->id = $document['Document']['id']; + $Document->saveField('pdf_filename', $result['filename']); + $Document->saveField('pdf_created_at', date('Y-m-d H:i:s')); + + // Get user ID safely (match quote logic) with Basic Auth fallback + $userId = null; + $sessionUser = null; + if (isset($this->Session)) { + $sessionUser = $this->Session->read('Auth.User'); + $userId = isset($sessionUser['id']) ? $sessionUser['id'] : null; + } + if (!$userId && isset($_SESSION['Auth']['User']['id'])) { + $userId = $_SESSION['Auth']['User']['id']; + } + // Fallback: try Basic Auth username/email -> User lookup + if (!$userId && !empty($_SERVER['PHP_AUTH_USER'])) { + $User = ClassRegistry::init('User'); + $foundUser = $User->find('first', array( + 'conditions' => array( + 'OR' => array( + 'User.email' => $_SERVER['PHP_AUTH_USER'], + 'User.username' => $_SERVER['PHP_AUTH_USER'], + ), + ), + 'fields' => array('User.id'), + 'recursive' => -1, + 'order' => 'User.id ASC' + )); + if ($foundUser && isset($foundUser['User']['id'])) { + $userId = $foundUser['User']['id']; + } + } + + if ($userId) { + $Document->saveField('pdf_created_by_user_id', $userId); + } + + // Count pages using the Go service + App::import('Vendor','pagecounter'); + $pageCounter = new PageCounter(); + $pageCount = $pageCounter->count($pdfPath); + + if ($pageCount > 0) { + $Document->saveField('doc_page_count', $pageCount); + } +} +?> + + + + + Redirecting... + + +

PDF generated successfully. Redirecting back to order acknowledgement...

+

Click here if not redirected

+ + + diff --git a/php/app/views/documents/pdf_packinglist.ctp b/php/app/views/documents/pdf_packinglist.ctp index fb191f2e..a80c3e1a 100755 --- a/php/app/views/documents/pdf_packinglist.ctp +++ b/php/app/views/documents/pdf_packinglist.ctp @@ -64,9 +64,75 @@ if ($httpCode < 200 || $httpCode >= 300) { echo "

"; exit; } + +// PDF generated successfully - now save metadata and count pages +$result = json_decode($response, true); +if (isset($result['filename'])) { + // Build path, removing any double slashes + $pdfPath = $outputDir . '/' . $result['filename']; + $pdfPath = preg_replace('#/+#', '/', $pdfPath); + + // Update database with PDF metadata + $Document = ClassRegistry::init('Document'); + $Document->id = $document['Document']['id']; + $Document->saveField('pdf_filename', $result['filename']); + $Document->saveField('pdf_created_at', date('Y-m-d H:i:s')); + + // Get user ID safely (match quote logic) with Basic Auth fallback + $userId = null; + $sessionUser = null; + if (isset($this->Session)) { + $sessionUser = $this->Session->read('Auth.User'); + $userId = isset($sessionUser['id']) ? $sessionUser['id'] : null; + } + if (!$userId && isset($_SESSION['Auth']['User']['id'])) { + $userId = $_SESSION['Auth']['User']['id']; + } + // Fallback: try Basic Auth username/email -> User lookup + if (!$userId && !empty($_SERVER['PHP_AUTH_USER'])) { + $User = ClassRegistry::init('User'); + $foundUser = $User->find('first', array( + 'conditions' => array( + 'OR' => array( + 'User.email' => $_SERVER['PHP_AUTH_USER'], + 'User.username' => $_SERVER['PHP_AUTH_USER'], + ), + ), + 'fields' => array('User.id'), + 'recursive' => -1, + 'order' => 'User.id ASC' + )); + if ($foundUser && isset($foundUser['User']['id'])) { + $userId = $foundUser['User']['id']; + } + } + + if ($userId) { + $Document->saveField('pdf_created_by_user_id', $userId); + } + + // Count pages using the Go service + App::import('Vendor','pagecounter'); + $pageCounter = new PageCounter(); + $pageCount = $pageCounter->count($pdfPath); + + if ($pageCount > 0) { + $Document->saveField('doc_page_count', $pageCount); + } +} ?> - - + + + + + Redirecting... + + +

PDF generated successfully. Redirecting back to packing list...

+

Click here if not redirected

+ + + diff --git a/php/app/views/documents/pdf_po.ctp b/php/app/views/documents/pdf_po.ctp index 13751f34..fb144d6d 100755 --- a/php/app/views/documents/pdf_po.ctp +++ b/php/app/views/documents/pdf_po.ctp @@ -29,8 +29,36 @@ if (isset($document['LineItem']) && is_array($document['LineItem'])) { } } +// Get currency info - priority: document currency, then principle's currency, then error +$currencySymbolToUse = null; +$currencyCodeToUse = null; + +// First check if document/PO has currency loaded +if (isset($currency['Currency']['symbol']) && isset($currency['Currency']['iso4217'])) { + $currencySymbolToUse = $currency['Currency']['symbol']; + $currencyCodeToUse = $currency['Currency']['iso4217']; +} +// Fall back to principle's currency +elseif (isset($principle['Currency']['symbol']) && isset($principle['Currency']['iso4217'])) { + $currencySymbolToUse = $principle['Currency']['symbol']; + $currencyCodeToUse = $principle['Currency']['iso4217']; +} +// Last resort - check if variables were set directly +elseif (isset($currencySymbol) && isset($currencyCode)) { + $currencySymbolToUse = $currencySymbol; + $currencyCodeToUse = $currencyCode; +} + +// Error out if no currency found +if ($currencySymbolToUse === null || $currencyCodeToUse === null) { + echo "

ERROR: No currency information found for this purchase order. Please ensure the document or supplier has a currency set.

"; + error_log("PO PDF Generation Error: No currency found for document ID " . $document['Document']['id']); + exit; +} + $payload = array( 'document_id' => intval($document['Document']['id']), + 'user_id' => isset($user['User']['id']) ? intval($user['User']['id']) : 0, 'title' => $document['PurchaseOrder']['title'], 'issue_date' => $document['PurchaseOrder']['issue_date'], 'issue_date_string' => isset($issue_date) ? $issue_date : '', @@ -40,8 +68,8 @@ $payload = array( 'dispatch_by' => $document['PurchaseOrder']['dispatch_by'], 'deliver_to' => $document['PurchaseOrder']['deliver_to'], 'shipping_instructions' => $document['PurchaseOrder']['shipping_instructions'], - 'currency_symbol' => $currencySymbol, - 'currency_code' => isset($currencyCode) ? $currencyCode : 'AUD', + 'currency_symbol' => $currencySymbolToUse, + 'currency_code' => $currencyCodeToUse, 'show_gst' => (bool)$gst, 'subtotal' => isset($totals['subtotal']) ? floatval($totals['subtotal']) : 0.0, 'gst_amount' => isset($totals['gst']) ? floatval($totals['gst']) : 0.0, @@ -69,9 +97,74 @@ if ($httpCode < 200 || $httpCode >= 300) { echo "

"; exit; } + +// PDF generated successfully - now save metadata and count pages +$result = json_decode($response, true); +if (isset($result['filename'])) { + // Build path, removing any double slashes + $pdfPath = $outputDir . '/' . $result['filename']; + $pdfPath = preg_replace('#/+#', '/', $pdfPath); + + // Update database with PDF metadata + $Document = ClassRegistry::init('Document'); + $Document->id = $document['Document']['id']; + $Document->saveField('pdf_filename', $result['filename']); + $Document->saveField('pdf_created_at', date('Y-m-d H:i:s')); + + // Get user ID safely (match quote/invoice logic) with Basic Auth fallback + $userId = null; + $sessionUser = null; + if (isset($this->Session)) { + $sessionUser = $this->Session->read('Auth.User'); + $userId = isset($sessionUser['id']) ? $sessionUser['id'] : null; + } + if (!$userId && isset($_SESSION['Auth']['User']['id'])) { + $userId = $_SESSION['Auth']['User']['id']; + } + // Fallback: try Basic Auth username/email -> User lookup + if (!$userId && !empty($_SERVER['PHP_AUTH_USER'])) { + $User = ClassRegistry::init('User'); + $foundUser = $User->find('first', array( + 'conditions' => array( + 'OR' => array( + 'User.email' => $_SERVER['PHP_AUTH_USER'], + 'User.username' => $_SERVER['PHP_AUTH_USER'], + ), + ), + 'fields' => array('User.id'), + 'recursive' => -1, + 'order' => 'User.id ASC' + )); + if ($foundUser && isset($foundUser['User']['id'])) { + $userId = $foundUser['User']['id']; + } + } + + if ($userId) { + $Document->saveField('pdf_created_by_user_id', $userId); + } + + // Count pages using the Go service + App::import('Vendor','pagecounter'); + $pageCounter = new PageCounter(); + $pageCount = $pageCounter->count($pdfPath); + + if ($pageCount > 0) { + $Document->saveField('doc_page_count', $pageCount); + } +} ?> - - - + + + + + Redirecting... + + +

PDF generated successfully. Redirecting back to purchase order...

+

Click here if not redirected

+ + + \ No newline at end of file diff --git a/php/app/views/purchase_orders/view.ctp b/php/app/views/purchase_orders/view.ctp index 00d06498..ba630bfa 100755 --- a/php/app/views/purchase_orders/view.ctp +++ b/php/app/views/purchase_orders/view.ctp @@ -1,4 +1,5 @@
+element('pdf_created_message'); ?>

>
DESCRIPTION QTY UNIT PRICEDISCOUNT TOTAL
{{.Title}}{{if .Description}}
{{.Description}}{{end}}
{{.Title}}{{if .Description}}
{{.Description}}{{end}}
{{.Quantity}} {{formatPrice .UnitPrice}}{{formatPrice .Discount}} {{formatPrice .TotalPrice}}