standardising invoice and oa, all documents do same redirect, more document fixes

This commit is contained in:
Finley Ghosh 2026-01-26 00:30:43 +11:00
parent e6b05a726d
commit fe2db6d597
25 changed files with 995 additions and 205 deletions

View file

@ -63,6 +63,7 @@ func main() {
// Load handlers // Load handlers
quoteHandler := quotes.NewQuotesHandler(queries, tmpl, emailService) quoteHandler := quotes.NewQuotesHandler(queries, tmpl, emailService)
attachmentHandler := attachments.NewAttachmentHandler(queries) attachmentHandler := attachments.NewAttachmentHandler(queries)
documentHandler := documents.NewDocumentHandler(queries)
// Setup routes // Setup routes
r := mux.NewRouter() r := mux.NewRouter()
@ -83,12 +84,12 @@ func main() {
goRouter.HandleFunc("/attachments/{id}", attachmentHandler.Delete).Methods("DELETE") goRouter.HandleFunc("/attachments/{id}", attachmentHandler.Delete).Methods("DELETE")
// Document generation routes // Document generation routes
goRouter.HandleFunc("/document/generate/invoice", documents.GenerateInvoicePDF).Methods("POST") goRouter.HandleFunc("/document/generate/invoice", documentHandler.GenerateInvoicePDF).Methods("POST")
goRouter.HandleFunc("/document/generate/quote", documents.GenerateQuotePDF).Methods("POST") goRouter.HandleFunc("/document/generate/quote", documentHandler.GenerateQuotePDF).Methods("POST")
goRouter.HandleFunc("/document/generate/purchase-order", documents.GeneratePurchaseOrderPDF).Methods("POST") goRouter.HandleFunc("/document/generate/purchase-order", documentHandler.GeneratePurchaseOrderPDF).Methods("POST")
goRouter.HandleFunc("/document/generate/packing-list", documents.GeneratePackingListPDF).Methods("POST") goRouter.HandleFunc("/document/generate/packing-list", documentHandler.GeneratePackingListPDF).Methods("POST")
goRouter.HandleFunc("/document/generate/order-acknowledgement", documents.GenerateOrderAckPDF).Methods("POST") goRouter.HandleFunc("/document/generate/order-acknowledgement", documentHandler.GenerateOrderAckPDF).Methods("POST")
goRouter.HandleFunc("/document/page-count", documents.CountPages).Methods("POST") goRouter.HandleFunc("/document/page-count", documentHandler.CountPages).Methods("POST")
// Serve generated PDFs // Serve generated PDFs
pdfDir := os.Getenv("PDF_OUTPUT_DIR") pdfDir := os.Getenv("PDF_OUTPUT_DIR")

View file

@ -1,4 +1,4 @@
package pdf package documents
import ( import (
"fmt" "fmt"

View file

@ -1,4 +1,4 @@
package pdf package documents
import ( import (
"context" "context"
@ -376,12 +376,17 @@ func (g *HTMLDocumentGenerator) loadLogo(logoFileName string) string {
return logoFileName 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) // Returns (filename, error)
func (g *HTMLDocumentGenerator) GeneratePurchaseOrderPDF(data *PurchaseOrderPDFData) (string, 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) 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") tempHTML := filepath.Join(g.outputDir, "temp_po.html")
if err := ioutil.WriteFile(tempHTML, []byte(html), 0644); err != nil { if err := ioutil.WriteFile(tempHTML, []byte(html), 0644); err != nil {
return "", fmt.Errorf("failed to write temp HTML: %w", err) 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(tempHTML)
defer os.Remove(filepath.Join(g.outputDir, "quote_logo.png")) 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 == "" { if poNumber == "" {
poNumber = fmt.Sprintf("PO-%d", data.PurchaseOrder.ID) poNumber = fmt.Sprintf("PO-%d", data.Document.ID)
} }
filename := fmt.Sprintf("%s.pdf", poNumber) filename := fmt.Sprintf("%s.pdf", poNumber)
pdfPath := filepath.Join(g.outputDir, filename) pdfPath := filepath.Join(g.outputDir, filename)
if err := g.htmlToPDF(tempHTML, pdfPath); err != nil { 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 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) // Returns (filename, error)
func (g *HTMLDocumentGenerator) GeneratePackingListPDF(data *PackingListPDFData) (string, 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) 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") tempHTML := filepath.Join(g.outputDir, "temp_packinglist.html")
if err := ioutil.WriteFile(tempHTML, []byte(html), 0644); err != nil { if err := ioutil.WriteFile(tempHTML, []byte(html), 0644); err != nil {
return "", fmt.Errorf("failed to write temp HTML: %w", err) 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(tempHTML)
defer os.Remove(filepath.Join(g.outputDir, "quote_logo.png")) 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 packingListNumber := data.PackingList.CmcReference
if packingListNumber == "" { if packingListNumber == "" {
packingListNumber = fmt.Sprintf("PackingList-%d", data.PackingList.ID) packingListNumber = fmt.Sprintf("PackingList-%d", data.PackingList.ID)
@ -426,18 +557,50 @@ func (g *HTMLDocumentGenerator) GeneratePackingListPDF(data *PackingListPDFData)
pdfPath := filepath.Join(g.outputDir, filename) pdfPath := filepath.Join(g.outputDir, filename)
if err := g.htmlToPDF(tempHTML, pdfPath); err != nil { 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 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) // Returns (filename, error)
func (g *HTMLDocumentGenerator) GenerateOrderAckPDF(data *OrderAckPDFData) (string, 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) 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") tempHTML := filepath.Join(g.outputDir, "temp_orderack.html")
if err := ioutil.WriteFile(tempHTML, []byte(html), 0644); err != nil { if err := ioutil.WriteFile(tempHTML, []byte(html), 0644); err != nil {
return "", fmt.Errorf("failed to write temp HTML: %w", err) 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(tempHTML)
defer os.Remove(filepath.Join(g.outputDir, "quote_logo.png")) 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 orderAckNumber := data.OrderAcknowledgement.CmcReference
if orderAckNumber == "" { if orderAckNumber == "" {
orderAckNumber = fmt.Sprintf("OrderAck-%d", data.OrderAcknowledgement.ID) 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) pdfPath := filepath.Join(g.outputDir, filename)
if err := g.htmlToPDF(tempHTML, pdfPath); err != nil { 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 return filename, nil
} }

View file

@ -1,4 +1,4 @@
package pdf package documents
import ( import (
"fmt" "fmt"
@ -111,6 +111,7 @@ type OrderAckPDFData struct {
ShipVia string ShipVia string
FOB string FOB string
PaymentTerms string PaymentTerms string
FreightDetails string
CurrencyCode string CurrencyCode string
CurrencySymbol string CurrencySymbol string
ShowGST bool ShowGST bool

View file

@ -1,4 +1,4 @@
package pdf package documents
import ( import (
"bytes" "bytes"
@ -42,6 +42,7 @@ func (g *HTMLDocumentGenerator) BuildInvoiceHTML(data *InvoicePDFData, totalPage
ShowGST bool ShowGST bool
PageCount int PageCount int
CurrentPage int CurrentPage int
FreightDetails template.HTML
LogoDataURI string LogoDataURI string
}{ }{
InvoiceNumber: invoiceNum, InvoiceNumber: invoiceNum,
@ -61,6 +62,7 @@ func (g *HTMLDocumentGenerator) BuildInvoiceHTML(data *InvoicePDFData, totalPage
ShowGST: data.ShowGST, ShowGST: data.ShowGST,
PageCount: totalPages, PageCount: totalPages,
CurrentPage: currentPage, CurrentPage: currentPage,
FreightDetails: template.HTML(data.ShippingDetails),
LogoDataURI: g.loadLogo("invoice_logo.png"), 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 Description: template.HTML(item.Description), // Allow HTML in description
Quantity: item.Quantity, Quantity: item.Quantity,
GrossUnitPrice: item.GrossUnitPrice, GrossUnitPrice: item.GrossUnitPrice,
GrossUnitPriceText: item.UnitPriceString,
DiscountAmountTotal: item.DiscountAmountTotal, DiscountAmountTotal: item.DiscountAmountTotal,
GrossPrice: item.GrossPrice, GrossPrice: item.GrossPrice,
GrossPriceText: item.GrossPriceString,
HasTextPrices: item.HasTextPrices,
}) })
} }
// Define template functions // Define template functions
funcMap := template.FuncMap{ 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 == "" { if !price.Valid || price.String == "" {
return "" return ""
} }
formatted := FormatPriceWithCommas(price.String) formatted := FormatPriceWithCommas(price.String)
return template.HTML(fmt.Sprintf("%s%s", data.CurrencySymbol, formatted)) 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 == "" { if !discount.Valid || discount.String == "" {
return template.HTML(fmt.Sprintf("%s0.00", data.CurrencySymbol)) return template.HTML(fmt.Sprintf("%s0.00", data.CurrencySymbol))
} }
@ -170,8 +183,11 @@ type LineItemTemplateData struct {
Description template.HTML Description template.HTML
Quantity string Quantity string
GrossUnitPrice sql.NullString GrossUnitPrice sql.NullString
GrossUnitPriceText sql.NullString
DiscountAmountTotal sql.NullString DiscountAmountTotal sql.NullString
GrossPrice sql.NullString GrossPrice sql.NullString
GrossPriceText sql.NullString
HasTextPrices bool
} }
// FormatPriceWithCommas formats a price string with comma separators // FormatPriceWithCommas formats a price string with comma separators

View file

@ -1,7 +1,7 @@
//go:build never //go:build never
// +build never // +build never
package pdf package documents
import ( import (
"fmt" "fmt"

View file

@ -1,7 +1,7 @@
//go:build never //go:build never
// +build never // +build never
package pdf package documents
package pdf package documents
import ( import (
"os" "os"

View file

@ -1,4 +1,4 @@
package pdf package documents
import ( import (
"bytes" "bytes"
@ -10,12 +10,15 @@ import (
// OrderAckLineItemTemplateData represents an order ack line item for template rendering // OrderAckLineItemTemplateData represents an order ack line item for template rendering
type OrderAckLineItemTemplateData struct { type OrderAckLineItemTemplateData struct {
Title string Title string
Description template.HTML Description template.HTML
Quantity string Quantity string
UnitPrice float64 UnitPrice float64
Discount float64 UnitPriceText string
TotalPrice float64 Discount float64
TotalPrice float64
TotalPriceText string
HasTextPrices bool
} }
// BuildOrderAckHTML generates the complete HTML for an order acknowledgement using templates // 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 unitPrice := 0.0
totalPrice := 0.0 totalPrice := 0.0
discount := 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) 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) totalPrice, _ = strconv.ParseFloat(item.GrossPrice.String, 64)
subtotal += totalPrice subtotal += totalPrice
} }
if item.DiscountAmountTotal.Valid { if item.DiscountAmountTotal.Valid {
discount, _ = strconv.ParseFloat(item.DiscountAmountTotal.String, 64) discount, _ = strconv.ParseFloat(item.DiscountAmountTotal.String, 64)
} }
lineItemsData = append(lineItemsData, OrderAckLineItemTemplateData{ lineItemsData = append(lineItemsData, OrderAckLineItemTemplateData{
Title: item.Title, Title: item.Title,
Description: template.HTML(item.Description), Description: template.HTML(item.Description),
Quantity: item.Quantity, Quantity: item.Quantity,
UnitPrice: unitPrice, UnitPrice: unitPrice,
Discount: discount, UnitPriceText: unitPriceText,
TotalPrice: totalPrice, Discount: discount,
TotalPrice: totalPrice,
TotalPriceText: totalPriceText,
HasTextPrices: hasTextPrices || item.HasTextPrices,
}) })
} }
@ -68,11 +87,12 @@ func (g *HTMLDocumentGenerator) BuildOrderAckHTML(data *OrderAckPDFData, totalPa
IssueDateString string IssueDateString string
YourReference string YourReference string
JobTitle string JobTitle string
BillTo string BillTo template.HTML
ShipTo string ShipTo template.HTML
ShipVia string ShipVia string
FOB string FOB string
PaymentTerms string PaymentTerms string
CustomerABN string
CurrencyCode string CurrencyCode string
CurrencySymbol string CurrencySymbol string
LineItems []OrderAckLineItemTemplateData LineItems []OrderAckLineItemTemplateData
@ -80,6 +100,9 @@ func (g *HTMLDocumentGenerator) BuildOrderAckHTML(data *OrderAckPDFData, totalPa
GSTAmount float64 GSTAmount float64
Total float64 Total float64
ShowGST bool ShowGST bool
PageCount int
CurrentPage int
FreightDetails template.HTML
LogoDataURI string LogoDataURI string
}{ }{
OrderAckNumber: orderAckNumber, OrderAckNumber: orderAckNumber,
@ -89,11 +112,12 @@ func (g *HTMLDocumentGenerator) BuildOrderAckHTML(data *OrderAckPDFData, totalPa
IssueDateString: data.IssueDateString, IssueDateString: data.IssueDateString,
YourReference: data.YourReference, YourReference: data.YourReference,
JobTitle: data.JobTitle, JobTitle: data.JobTitle,
BillTo: data.BillTo, BillTo: template.HTML(data.BillTo),
ShipTo: data.ShipTo, ShipTo: template.HTML(data.ShipTo),
ShipVia: data.ShipVia, ShipVia: data.ShipVia,
FOB: data.FOB, FOB: data.FOB,
PaymentTerms: data.PaymentTerms, PaymentTerms: data.PaymentTerms,
CustomerABN: "",
CurrencyCode: data.CurrencyCode, CurrencyCode: data.CurrencyCode,
CurrencySymbol: data.CurrencySymbol, CurrencySymbol: data.CurrencySymbol,
LineItems: lineItemsData, LineItems: lineItemsData,
@ -101,15 +125,34 @@ func (g *HTMLDocumentGenerator) BuildOrderAckHTML(data *OrderAckPDFData, totalPa
GSTAmount: gstAmount, GSTAmount: gstAmount,
Total: total, Total: total,
ShowGST: data.ShowGST, ShowGST: data.ShowGST,
PageCount: totalPages,
CurrentPage: currentPage,
FreightDetails: template.HTML(data.FreightDetails),
LogoDataURI: g.loadLogo("quote_logo.png"), LogoDataURI: g.loadLogo("quote_logo.png"),
} }
// Define template functions // Define template functions
funcMap := template.FuncMap{ 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)) formatted := FormatPriceWithCommas(fmt.Sprintf("%.2f", price))
return template.HTML(fmt.Sprintf("%s%s", data.CurrencySymbol, formatted)) 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 { "formatTotal": func(amount float64) template.HTML {
formatted := FormatPriceWithCommas(fmt.Sprintf("%.2f", amount)) formatted := FormatPriceWithCommas(fmt.Sprintf("%.2f", amount))
return template.HTML(fmt.Sprintf("%s%s", data.CurrencySymbol, formatted)) return template.HTML(fmt.Sprintf("%s%s", data.CurrencySymbol, formatted))

View file

@ -1,4 +1,4 @@
package pdf package documents
import ( import (
"bytes" "bytes"

View file

@ -1,4 +1,4 @@
package pdf package documents
import ( import (
"fmt" "fmt"

View file

@ -1,4 +1,4 @@
package pdf package documents
import ( import (
"bytes" "bytes"
@ -14,6 +14,7 @@ type PurchaseOrderLineItemTemplateData struct {
Description template.HTML Description template.HTML
Quantity string Quantity string
UnitPrice float64 UnitPrice float64
Discount float64
TotalPrice float64 TotalPrice float64
} }
@ -28,6 +29,7 @@ func (g *HTMLDocumentGenerator) BuildPurchaseOrderHTML(data *PurchaseOrderPDFDat
for _, item := range data.LineItems { for _, item := range data.LineItems {
unitPrice := 0.0 unitPrice := 0.0
totalPrice := 0.0 totalPrice := 0.0
discount := 0.0
if item.NetUnitPrice.Valid { if item.NetUnitPrice.Valid {
unitPrice, _ = strconv.ParseFloat(item.NetUnitPrice.String, 64) unitPrice, _ = strconv.ParseFloat(item.NetUnitPrice.String, 64)
@ -36,12 +38,16 @@ func (g *HTMLDocumentGenerator) BuildPurchaseOrderHTML(data *PurchaseOrderPDFDat
totalPrice, _ = strconv.ParseFloat(item.NetPrice.String, 64) totalPrice, _ = strconv.ParseFloat(item.NetPrice.String, 64)
subtotal += totalPrice subtotal += totalPrice
} }
if item.DiscountAmountTotal.Valid {
discount, _ = strconv.ParseFloat(item.DiscountAmountTotal.String, 64)
}
lineItemsData = append(lineItemsData, PurchaseOrderLineItemTemplateData{ lineItemsData = append(lineItemsData, PurchaseOrderLineItemTemplateData{
Title: item.Title, Title: item.Title,
Description: template.HTML(item.Description), Description: template.HTML(item.Description),
Quantity: item.Quantity, Quantity: item.Quantity,
UnitPrice: unitPrice, UnitPrice: unitPrice,
Discount: discount,
TotalPrice: totalPrice, TotalPrice: totalPrice,
}) })
} }
@ -57,12 +63,12 @@ func (g *HTMLDocumentGenerator) BuildPurchaseOrderHTML(data *PurchaseOrderPDFDat
templateData := struct { templateData := struct {
PONumber string PONumber string
PrincipleName string PrincipleName string
YourReference string YourReference template.HTML
IssueDateString string IssueDateString string
OrderedFrom string OrderedFrom template.HTML
DeliverTo string DeliverTo template.HTML
DispatchBy string DispatchBy string
ShippingInstructions string ShippingInstructions template.HTML
CurrencyCode string CurrencyCode string
CurrencySymbol string CurrencySymbol string
LineItems []PurchaseOrderLineItemTemplateData LineItems []PurchaseOrderLineItemTemplateData
@ -74,12 +80,12 @@ func (g *HTMLDocumentGenerator) BuildPurchaseOrderHTML(data *PurchaseOrderPDFDat
}{ }{
PONumber: poNumber, PONumber: poNumber,
PrincipleName: data.Principle.Name, PrincipleName: data.Principle.Name,
YourReference: data.PurchaseOrder.PrincipleReference, YourReference: template.HTML(data.PurchaseOrder.PrincipleReference),
IssueDateString: data.IssueDateString, IssueDateString: data.IssueDateString,
OrderedFrom: data.PurchaseOrder.OrderedFrom, OrderedFrom: template.HTML(data.PurchaseOrder.OrderedFrom),
DeliverTo: data.PurchaseOrder.DeliverTo, DeliverTo: template.HTML(data.PurchaseOrder.DeliverTo),
DispatchBy: data.PurchaseOrder.DispatchBy, DispatchBy: data.PurchaseOrder.DispatchBy,
ShippingInstructions: data.PurchaseOrder.ShippingInstructions, ShippingInstructions: template.HTML(data.PurchaseOrder.ShippingInstructions),
CurrencyCode: data.CurrencyCode, CurrencyCode: data.CurrencyCode,
CurrencySymbol: data.CurrencySymbol, CurrencySymbol: data.CurrencySymbol,
LineItems: lineItemsData, LineItems: lineItemsData,

View file

@ -1,4 +1,4 @@
package pdf package documents
import ( import (
"bytes" "bytes"

View file

@ -384,9 +384,9 @@
<tr> <tr>
<td class="description"><strong>{{.Title}}</strong><br>{{.Description}}</td> <td class="description"><strong>{{.Title}}</strong><br>{{.Description}}</td>
<td class="qty">{{.Quantity}}</td> <td class="qty">{{.Quantity}}</td>
<td class="unit-price">{{formatPrice .GrossUnitPrice}}</td> <td class="unit-price">{{formatPrice .GrossUnitPrice .GrossUnitPriceText}}</td>
<td class="discount">{{formatDiscount .DiscountAmountTotal}}</td> <td class="discount">{{formatDiscount .DiscountAmountTotal .HasTextPrices}}</td>
<td class="total">{{formatPrice .GrossPrice}}</td> <td class="total">{{formatPrice .GrossPrice .GrossPriceText}}</td>
</tr> </tr>
{{end}} {{end}}
</tbody> </tbody>
@ -395,13 +395,28 @@
<!-- Payment and Totals Side by Side --> <!-- Payment and Totals Side by Side -->
<div style="display: flex; gap: 10mm; align-items: flex-start; margin-top: 5mm;"> <div style="display: flex; gap: 10mm; align-items: flex-start; margin-top: 5mm;">
<div style="width: 50%; padding: 3mm; font-size: 9pt; border: 1px solid #000;"> <div style="width: 50%; padding: 3mm; font-size: 9pt; border: 1px solid #000;">
<h4 style="margin: 0 0 1mm 0; font-weight: bold; font-size: 10pt;">MAKE PAYMENT TO:</h4> <h4 style="margin: 0 0 2mm 0; font-weight: bold; font-size: 10pt;">MAKE PAYMENT TO:</h4>
<table style="width: 100%; border-collapse: collapse; font-size: 9pt; table-layout: fixed;"> <table style="width: 100%; border-collapse: collapse;">
<tr><td style="font-weight: bold; width: 40%; padding: 0.3mm 0; padding-right: 6mm; white-space: nowrap;">Account Name:</td><td style="padding: 0.3mm 0; white-space: nowrap;">CMC Technologies Pty Ltd</td></tr> <tr>
<tr><td style="font-weight: bold; width: 40%; padding: 0.3mm 0; padding-right: 6mm; white-space: nowrap;">BSB:</td><td style="padding: 0.3mm 0; white-space: nowrap;">062-458</td></tr> <td style="font-weight: bold; width: 50%; padding: 1mm 0;">Account Name:</td>
<tr><td style="font-weight: bold; width: 40%; padding: 0.3mm 0; padding-right: 6mm; white-space: nowrap;">Account Number:</td><td style="padding: 0.3mm 0; white-space: nowrap;">10067982</td></tr> <td style="width: 50%; padding: 1mm 0;">CMC Technologies Pty Ltd</td>
<tr><td style="font-weight: bold; width: 40%; padding: 0.3mm 0; padding-right: 6mm; white-space: nowrap;">SWIFT Code:</td><td style="padding: 0.3mm 0; white-space: nowrap;">CTBAAU2S</td></tr> </tr>
<tr><td style="font-weight: bold; width: 40%; padding: 0.3mm 0; padding-right: 6mm; white-space: nowrap;">IBAN:</td><td style="padding: 0.3mm 0; white-space: nowrap;">0624581006782</td></tr> <tr>
<td style="font-weight: bold; padding: 1mm 0;">BSB:</td>
<td style="padding: 1mm 0;">062-458</td>
</tr>
<tr>
<td style="font-weight: bold; padding: 1mm 0;">Account Number:</td>
<td style="padding: 1mm 0;">10067982</td>
</tr>
<tr>
<td style="font-weight: bold; padding: 1mm 0;">SWIFT:</td>
<td style="padding: 1mm 0;">CTBAAU2S</td>
</tr>
<tr>
<td style="font-weight: bold; padding: 1mm 0;">IBAN:</td>
<td style="padding: 1mm 0;">0624581006782</td>
</tr>
</table> </table>
</div> </div>
<div style="width: 50%; margin-left: auto; margin-right: 1mm;"> <div style="width: 50%; margin-left: auto; margin-right: 1mm;">
@ -414,5 +429,15 @@
</table> </table>
</div> </div>
</div> </div>
<!-- Freight Details -->
<div style="margin-top: 5mm; margin-right: 1mm; padding: 3mm; border: 1px solid #000; font-size: 9pt;">
<h4 style="margin: 0 0 1mm 0; font-weight: bold; font-size: 10pt;">FREIGHT DETAILS:</h4>
<div style="font-size: 9pt; line-height: 1.3;">
{{if .FreightDetails}}
{{.FreightDetails}}
{{end}}
</div>
</div>
</body> </body>
</html> </html>

View file

@ -14,10 +14,26 @@
font-size: 9pt; font-size: 9pt;
line-height: 1.4; line-height: 1.4;
margin: 0; margin: 0;
padding: 0 0 25mm 0; padding: 0;
color: #000; 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 { .details-table {
width: 100%; width: 100%;
border-collapse: collapse; border-collapse: collapse;
@ -196,26 +212,39 @@
</head> </head>
<body> <body>
{{template "CompanyHeader" .}} {{template "CompanyHeader" .}}
<!-- Order Ack Details Table --> <!-- ORDER ACKNOWLEDGEMENT heading and info, mirroring invoice layout -->
<div style="display: flex; justify-content: space-between; align-items: center; margin: 2mm 0 3mm 0;">
<div style="flex: 1;"></div>
<div style="flex: 1; text-align: center;">
<h2 style="font-size: 16pt; margin: 0; font-weight: bold;">ORDER ACKNOWLEDGEMENT</h2>
</div>
<div style="flex: 1; text-align: right; font-size: 10pt;">
{{if .OrderAckNumber}}
<div style="font-weight: bold; margin-bottom: 1mm;">ORDER ACK# {{.OrderAckNumber}}</div>
{{end}}
{{if .IssueDateString}}
<div><strong>Date:</strong> {{.IssueDateString}}</div>
{{end}}
{{if .PageCount}}
<div><strong>Page:</strong> {{.CurrentPage}} of {{.PageCount}}</div>
{{end}}
</div>
</div>
<!-- Optional Order Ack details (kept for field completeness) -->
<table class="details-table"> <table class="details-table">
<tr> <tr>
<td class="label">COMPANY NAME:</td> <td class="label">COMPANY NAME:</td>
<td>{{.CompanyName}}</td> <td>{{.CompanyName}}</td>
<td class="label">ORDER ACK #:</td> <td class="label">YOUR REFERENCE:</td>
<td>{{.OrderAckNumber}}</td> <td>{{.YourReference}}</td>
</tr> </tr>
<tr> <tr>
<td class="label">CONTACT:</td> <td class="label">CONTACT:</td>
<td>{{.Attention}}</td> <td>{{.Attention}}</td>
<td class="label">DATE:</td>
<td>{{.IssueDateString}}</td>
</tr>
<tr>
<td class="label">EMAIL:</td> <td class="label">EMAIL:</td>
<td>{{.EmailTo}}</td> <td>{{.EmailTo}}</td>
<td class="label">YOUR REFERENCE:</td>
<td>{{.YourReference}}</td>
</tr> </tr>
<tr> <tr>
<td class="label">JOB TITLE:</td> <td class="label">JOB TITLE:</td>
@ -224,29 +253,37 @@
</table> </table>
<!-- Address Boxes --> <!-- Address Boxes -->
<div class="address-boxes"> <div class="address-boxes" style="display: flex; gap: 5mm; margin-bottom: 3mm;">
<div class="address-box"> <div class="address-box" style="flex: 1;">
<h3>BILL TO:</h3> <h3>Sold To / Invoice Address:</h3>
<p>{{.BillTo}}</p> <div>{{.BillTo}}</div>
</div> </div>
<div class="address-box"> <div class="address-box" style="flex: 1; margin-right: 1mm; margin-left: auto;">
<h3>SHIP TO:</h3> <h3>Delivery Address:</h3>
<p>{{.ShipTo}}</p> <div>{{.ShipTo}}</div>
</div> </div>
</div> </div>
<!-- Shipping Details --> <!-- Details Table - match invoice 5-column layout -->
<table class="details-table"> <table class="line-items" style="margin-bottom: 3mm;">
<tr> <thead>
<td class="label">SHIP VIA:</td> <tr>
<td>{{.ShipVia}}</td> <th style="width: 20%;">CUSTOMER ORDER NO</th>
<td class="label">FOB:</td> <th style="width: 20%;">CMC JOB #</th>
<td>{{.FOB}}</td> <th style="width: 20%;">INCOTERMS 2010</th>
</tr> <th style="width: 20%;">PAYMENT TERMS</th>
<tr> <th style="width: 20%;">CUSTOMER ABN</th>
<td class="label">PAYMENT TERMS:</td> </tr>
<td colspan="3">{{.PaymentTerms}}</td> </thead>
</tr> <tbody>
<tr>
<td>{{.YourReference}}</td>
<td>{{.JobTitle}}</td>
<td>{{.FOB}}</td>
<td>{{.PaymentTerms}}</td>
<td>{{.CustomerABN}}</td>
</tr>
</tbody>
</table> </table>
<!-- Currency Code Header --> <!-- Currency Code Header -->
@ -268,34 +305,62 @@
<tbody> <tbody>
{{range .LineItems}} {{range .LineItems}}
<tr> <tr>
<td class="description"><strong>{{.Title}}</strong>{{if .Description}}<br>{{.Description}}{{end}}</td> <td class="description"><strong>{{.Title}}</strong>{{if .Description}}<br/>{{.Description}}{{end}}</td>
<td class="qty">{{.Quantity}}</td> <td class="qty">{{.Quantity}}</td>
<td class="unit-price">{{formatPrice .UnitPrice}}</td> <td class="unit-price">{{formatPrice .UnitPrice .UnitPriceText}}</td>
<td class="discount">{{if .Discount}}{{formatPrice .Discount}}{{else}}$0.00{{end}}</td> <td class="discount">{{formatDiscount .Discount .HasTextPrices}}</td>
<td class="total">{{formatPrice .TotalPrice}}</td> <td class="total">{{formatPrice .TotalPrice .TotalPriceText}}</td>
</tr> </tr>
{{end}} {{end}}
</tbody> </tbody>
</table> </table>
<!-- Totals --> <!-- Payment and Totals Side by Side -->
<div class="totals-container"> <div style="display: flex; gap: 10mm; align-items: flex-start; margin-top: 5mm;">
<table class="totals-table"> <div style="width: 50%; padding: 3mm; font-size: 9pt; border: 1px solid #000;">
<tr> <h4 style="margin: 0 0 2mm 0; font-weight: bold; font-size: 10pt;">MAKE PAYMENT TO:</h4>
<td>SUBTOTAL</td> <table style="width: 100%; border-collapse: collapse;">
<td>{{formatTotal .Subtotal}}</td> <tr>
</tr> <td style="font-weight: bold; width: 50%; padding: 1mm 0;">Account Name:</td>
{{if .ShowGST}} <td style="width: 50%; padding: 1mm 0;">CMC Technologies Pty Ltd</td>
<tr> </tr>
<td>GST (10%)</td> <tr>
<td>{{formatTotal .GSTAmount}}</td> <td style="font-weight: bold; padding: 1mm 0;">BSB:</td>
</tr> <td style="padding: 1mm 0;">062-458</td>
</tr>
<tr>
<td style="font-weight: bold; padding: 1mm 0;">Account Number:</td>
<td style="padding: 1mm 0;">10067982</td>
</tr>
<tr>
<td style="font-weight: bold; padding: 1mm 0;">SWIFT:</td>
<td style="padding: 1mm 0;">CTBAAU2S</td>
</tr>
<tr>
<td style="font-weight: bold; padding: 1mm 0;">IBAN:</td>
<td style="padding: 1mm 0;">0624581006782</td>
</tr>
</table>
</div>
<div style="width: 50%; margin-left: auto; margin-right: 1mm;">
<table class="totals-table" style="width: 100%;">
<tr><td>Subtotal:</td><td>{{formatTotal .Subtotal}}</td></tr>
{{if .ShowGST}}
<tr><td>GST (10%):</td><td>{{formatTotal .GSTAmount}}</td></tr>
{{end}}
<tr class="total-row"><td>TOTAL:</td><td>{{formatTotal .Total}}</td></tr>
</table>
</div>
</div>
<!-- Freight Details -->
<div style="margin-top: 5mm; margin-right: 1mm; padding: 3mm; border: 1px solid #000; font-size: 9pt;">
<h4 style="margin: 0 0 1mm 0; font-weight: bold; font-size: 10pt;">FREIGHT DETAILS:</h4>
<div style="font-size: 9pt; line-height: 1.3;">
{{if .FreightDetails}}
{{.FreightDetails}}
{{end}} {{end}}
<tr class="total-row"> </div>
<td>{{if .ShowGST}}TOTAL PAYABLE{{else}}TOTAL{{end}}</td>
<td>{{formatTotal .Total}}</td>
</tr>
</table>
</div> </div>
<!-- Footer --> <!-- Footer -->

View file

@ -190,7 +190,7 @@
<tbody> <tbody>
{{range .LineItems}} {{range .LineItems}}
<tr> <tr>
<td class="description"><strong>{{.Title}}</strong>{{if .Description}}<br>{{.Description}}{{end}}</td> <td class="description"><strong>{{.Title}}</strong>{{if .Description}}<br/>{{.Description}}{{end}}</td>
<td class="qty">{{.Quantity}}</td> <td class="qty">{{.Quantity}}</td>
<td class="weight">{{if .Weight}}{{.Weight}}{{else}}-{{end}}</td> <td class="weight">{{if .Weight}}{{.Weight}}{{else}}-{{end}}</td>
</tr> </tr>

View file

@ -65,7 +65,7 @@
} }
.line-items .description { .line-items .description {
width: 50%; width: 40%;
word-wrap: break-word; word-wrap: break-word;
overflow-wrap: break-word; overflow-wrap: break-word;
} }
@ -76,7 +76,12 @@
} }
.line-items .unit-price { .line-items .unit-price {
width: 20%; width: 15%;
text-align: right;
}
.line-items .discount {
width: 15%;
text-align: right; text-align: right;
} }
@ -221,27 +226,29 @@
</div> </div>
{{end}} {{end}}
<!-- Currency Code Header --> <!-- Currency Code Header - keep with table -->
<div style="margin-bottom: 1mm; text-align: right; font-weight: bold; font-size: 9pt;"> <div style="margin-bottom: 1mm; text-align: right; font-weight: bold; font-size: 9pt; page-break-after: avoid; page-break-inside: avoid;">
Shown in {{.CurrencyCode}} Shown in {{.CurrencyCode}}
</div> </div>
<!-- Line Items Table --> <!-- Line Items Table -->
<table class="line-items"> <table class="line-items" style="page-break-before: avoid;">
<thead> <thead>
<tr> <tr>
<th class="description">DESCRIPTION</th> <th class="description">DESCRIPTION</th>
<th class="qty">QTY</th> <th class="qty">QTY</th>
<th class="unit-price">UNIT PRICE</th> <th class="unit-price">UNIT PRICE</th>
<th class="discount">DISCOUNT</th>
<th class="total">TOTAL</th> <th class="total">TOTAL</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{{range .LineItems}} {{range .LineItems}}
<tr> <tr>
<td class="description"><strong>{{.Title}}</strong>{{if .Description}}<br>{{.Description}}{{end}}</td> <td class="description"><strong>{{.Title}}</strong>{{if .Description}}<br/>{{.Description}}{{end}}</td>
<td class="qty">{{.Quantity}}</td> <td class="qty">{{.Quantity}}</td>
<td class="unit-price">{{formatPrice .UnitPrice}}</td> <td class="unit-price">{{formatPrice .UnitPrice}}</td>
<td class="discount">{{formatPrice .Discount}}</td>
<td class="total">{{formatPrice .TotalPrice}}</td> <td class="total">{{formatPrice .TotalPrice}}</td>
</tr> </tr>
{{end}} {{end}}

View file

@ -1,6 +1,7 @@
package documents package documents
import ( import (
"context"
"database/sql" "database/sql"
"encoding/json" "encoding/json"
"fmt" "fmt"
@ -15,6 +16,18 @@ import (
pdf "code.springupsoftware.com/cmc/cmc-sales/internal/cmc/documents" 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 <br> // escapeToHTML converts plain text to HTML with newlines as <br>
func escapeToHTML(s string) string { func escapeToHTML(s string) string {
s = html.EscapeString(s) 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}, GrossUnitPrice: sql.NullString{String: fmt.Sprintf("%.2f", li.UnitPrice), Valid: true},
GrossPrice: sql.NullString{String: fmt.Sprintf("%.2f", finalPrice), 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}, 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 // PurchaseOrderPDFRequest payload from PHP for POs
type PurchaseOrderPDFRequest struct { type PurchaseOrderPDFRequest struct {
DocumentID int32 `json:"document_id"` DocumentID int32 `json:"document_id"`
UserID int32 `json:"user_id"`
Title string `json:"title"` Title string `json:"title"`
IssueDate string `json:"issue_date"` // YYYY-MM-DD IssueDate string `json:"issue_date"` // YYYY-MM-DD
IssueDateString string `json:"issue_date_string"` // formatted date IssueDateString string `json:"issue_date_string"` // formatted date
@ -338,8 +355,8 @@ type PurchaseOrderPDFRequest struct {
OutputDir string `json:"output_dir"` OutputDir string `json:"output_dir"`
} }
// GeneratePurchaseOrderPDF handles POST /go/pdf/generate-po // GeneratePurchaseOrderPDF handles POST /go/document/generate/purchase-order
func GeneratePurchaseOrderPDF(w http.ResponseWriter, r *http.Request) { func (h *DocumentHandler) GeneratePurchaseOrderPDF(w http.ResponseWriter, r *http.Request) {
var req PurchaseOrderPDFRequest var req PurchaseOrderPDFRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil { if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
http.Error(w, "invalid JSON payload", http.StatusBadRequest) 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)) lineItems := make([]db.GetLineItemsTableRow, len(req.LineItems))
for i, li := range req.LineItems { for i, li := range req.LineItems {
// Keep description as-is to support HTML rendering
lineItems[i] = db.GetLineItemsTableRow{ lineItems[i] = db.GetLineItemsTableRow{
ItemNumber: li.ItemNumber, ItemNumber: li.ItemNumber,
Quantity: li.Quantity, Quantity: li.Quantity,
Title: li.Title, Title: li.Title,
GrossUnitPrice: sql.NullString{String: fmt.Sprintf("%.2f", li.UnitPrice), Valid: true}, Description: li.Description,
GrossPrice: sql.NullString{String: fmt.Sprintf("%.2f", li.TotalPrice), Valid: true}, 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 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") 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})
} } // GeneratePackingListPDF handles POST /go/pdf/generate-packinglist
// GeneratePackingListPDF handles POST /go/pdf/generate-packinglist
func GeneratePackingListPDF(w http.ResponseWriter, r *http.Request) { func GeneratePackingListPDF(w http.ResponseWriter, r *http.Request) {
var req PackingListPDFRequest var req PackingListPDFRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil { 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)) lineItems := make([]db.GetLineItemsTableRow, len(req.LineItems))
for i, li := range req.LineItems { for i, li := range req.LineItems {
lineItems[i] = db.GetLineItemsTableRow{ lineItems[i] = db.GetLineItemsTableRow{
ItemNumber: li.ItemNumber, ItemNumber: li.ItemNumber,
Quantity: li.Quantity, Quantity: li.Quantity,
Title: li.Title, Title: li.Title,
GrossUnitPrice: sql.NullString{String: fmt.Sprintf("%.2f", li.UnitPrice), Valid: true}, Description: li.Description,
GrossPrice: sql.NullString{String: fmt.Sprintf("%.2f", li.TotalPrice), Valid: true}, 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, ShipVia: req.ShipVia,
FOB: req.FOB, FOB: req.FOB,
PaymentTerms: req.PaymentTerms, PaymentTerms: req.PaymentTerms,
FreightDetails: req.FreightDetails,
CurrencyCode: req.CurrencyCode, CurrencyCode: req.CurrencyCode,
CurrencySymbol: req.CurrencySymbol, CurrencySymbol: req.CurrencySymbol,
ShowGST: req.ShowGST, ShowGST: req.ShowGST,
@ -594,6 +644,7 @@ type OrderAckPDFRequest struct {
ShipVia string `json:"ship_via"` ShipVia string `json:"ship_via"`
FOB string `json:"fob"` FOB string `json:"fob"`
PaymentTerms string `json:"payment_terms"` PaymentTerms string `json:"payment_terms"`
FreightDetails string `json:"freight_details"`
EstimatedDelivery string `json:"estimated_delivery"` EstimatedDelivery string `json:"estimated_delivery"`
CurrencySymbol string `json:"currency_symbol"` CurrencySymbol string `json:"currency_symbol"`
CurrencyCode string `json:"currency_code"` CurrencyCode string `json:"currency_code"`
@ -645,3 +696,24 @@ func CountPages(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json") w.Header().Set("Content-Type", "application/json")
_ = json.NewEncoder(w).Encode(map[string]int{"page_count": pageCount}) _ = 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)
}

View file

@ -12,7 +12,7 @@ import (
"time" "time"
"code.springupsoftware.com/cmc/cmc-sales/internal/cmc/db" "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 <br> // escapeToHTML converts plain text to HTML with newlines as <br>

View file

@ -894,7 +894,11 @@ ENDINSTRUCTIONS;
break; break;
case "purchaseOrder": 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); $this->set('principle', $principle);
$purchase_order = $this->Document->PurchaseOrder->find('first', $purchase_order = $this->Document->PurchaseOrder->find('first',

View file

@ -24,7 +24,11 @@ class PurchaseOrdersController extends AppController {
$this->Session->setFlash(__('Invalid PurchaseOrder.', true)); $this->Session->setFlash(__('Invalid PurchaseOrder.', true));
$this->redirect(array('action'=>'index')); $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() { function add() {

View file

@ -136,19 +136,48 @@ if ($httpCode < 200 || $httpCode >= 300) {
exit; 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); $result = json_decode($response, true);
if (isset($result['filename'])) { 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 = ClassRegistry::init('Document');
$Document->id = $document['Document']['id']; $Document->id = $document['Document']['id'];
$Document->saveField('pdf_filename', $result['filename']); $Document->saveField('pdf_filename', $result['filename']);
$Document->saveField('pdf_created_at', date('Y-m-d H:i:s')); $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; $userId = null;
$sessionUser = null;
if (isset($this->Session)) { 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) { if ($userId) {
$Document->saveField('pdf_created_by_user_id', $userId); $Document->saveField('pdf_created_by_user_id', $userId);
} }
@ -156,29 +185,24 @@ if (isset($result['filename'])) {
// Count pages using the Go service // Count pages using the Go service
App::import('Vendor','pagecounter'); App::import('Vendor','pagecounter');
$pageCounter = new PageCounter(); $pageCounter = new PageCounter();
$pdfPath = $outputDir . '/' . $result['filename'];
error_log("=== pdf_invoice.ctp: Counting pages for PDF: " . $pdfPath . " ===");
$pageCount = $pageCounter->count($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); $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'] . " ===");
?> ?>
<!DOCTYPE html> <!DOCTYPE html>
<html> <html>
<head> <head>
<meta http-equiv="refresh" content="0;url=/documents/view/<?=$document['Document']['id']?>"> <meta http-equiv="refresh" content="0;url=/documents/view/<?=$document['Document']['id']?>" />
<title>Redirecting...</title>
</head> </head>
<body> <body>
<p>PDF generated successfully. <a href="/documents/view/<?=$document['Document']['id']?>">Click here if you are not redirected</a>.</p> <p>PDF generated successfully. Redirecting back to invoice...</p>
<script type="text/javascript"> <p><a href="/documents/view/<?=$document['Document']['id']?>">Click here if not redirected</a></p>
window.location.replace("/documents/view/<?=$document['Document']['id']?>"); <script type="text/javascript">
</script> window.location.replace("/documents/view/<?=$document['Document']['id']?>");
</script>
</body> </body>
</html> </html>

View file

@ -1,5 +1,5 @@
<?php <?php
// Generate the Order Acknowledgement PDF by calling the Go service instead of TCPDF/FPDI. // Generate the Order Acknowledgement PDF by calling the Go service (similar to invoice)
$goBaseUrl = AppController::getGoBaseUrlOrFail(); $goBaseUrl = AppController::getGoBaseUrlOrFail();
$goEndpoint = $goBaseUrl . '/go/document/generate/order-acknowledgement'; $goEndpoint = $goBaseUrl . '/go/document/generate/order-acknowledgement';
@ -7,41 +7,100 @@ $goEndpoint = $goBaseUrl . '/go/document/generate/order-acknowledgement';
$outputDir = Configure::read('pdf_directory'); $outputDir = Configure::read('pdf_directory');
$lineItems = array(); $lineItems = array();
if (isset($document['LineItem']) && is_array($document['LineItem'])) { foreach ($document['LineItem'] as $li) {
foreach ($document['LineItem'] as $li) { $lineItems[] = array(
$lineItems[] = array( 'item_number' => $li['item_number'],
'item_number' => isset($li['item_number']) ? $li['item_number'] : '', 'quantity' => $li['quantity'],
'quantity' => isset($li['quantity']) ? $li['quantity'] : '', 'title' => $li['title'],
'title' => isset($li['title']) ? $li['title'] : '', 'description' => isset($li['description']) ? $li['description'] : '',
'description' => isset($li['description']) ? $li['description'] : '', 'is_html' => true, // Description is always HTML
'unit_price' => isset($li['gross_unit_price']) ? floatval($li['gross_unit_price']) : 0.0, 'unit_price' => floatval($li['gross_unit_price']),
'total_price' => isset($li['gross_price']) ? floatval($li['gross_price']) : 0.0, 'total_price' => floatval($li['gross_price']),
'net_unit_price' => isset($li['net_unit_price']) ? floatval($li['net_unit_price']) : 0.0, 'net_unit_price' => floatval($li['net_unit_price']),
'net_price' => isset($li['net_price']) ? floatval($li['net_price']) : 0.0, 'net_price' => floatval($li['net_price']),
'discount_percent' => isset($li['discount_percent']) ? floatval($li['discount_percent']) : 0.0, 'discount_percent' => floatval($li['discount_percent']),
'discount_amount_unit' => isset($li['discount_amount_unit']) ? floatval($li['discount_amount_unit']) : 0.0, 'discount_amount_unit' => floatval($li['discount_amount_unit']),
'discount_amount_total' => isset($li['discount_amount_total']) ? floatval($li['discount_amount_total']) : 0.0, 'discount_amount_total' => floatval($li['discount_amount_total']),
'option' => isset($li['option']) ? intval($li['option']) : 0, 'option' => intval($li['option']),
'has_text_prices' => isset($li['has_text_prices']) ? (bool)$li['has_text_prices'] : false, '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'] : '', 'unit_price_string' => isset($li['unit_price_string']) ? $li['unit_price_string'] : '',
'gross_price_string' => isset($li['gross_price_string']) ? $li['gross_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( $payload = array(
'document_id' => intval($document['Document']['id']), 'document_id' => intval($document['Document']['id']),
'title' => $document['OrderAcknowledgement']['title'], 'order_ack_number' => $orderAckNumber,
'customer_name' => $enquiry['Customer']['name'], 'title' => $orderAckTitle,
'job_title' => isset($job['Job']['title']) ? $job['Job']['title'] : '', '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' => $document['OrderAcknowledgement']['issue_date'],
'issue_date_string' => isset($issue_date_string) ? $issue_date_string : '', '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_symbol' => $currencySymbol,
'currency_code' => isset($currencyCode) ? $currencyCode : 'AUD', 'currency_code' => $currencyCode,
'show_gst' => (bool)$gst, '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, 'line_items' => $lineItems,
'output_dir' => $outputDir 'output_dir' => $outputDir
); );
@ -65,8 +124,74 @@ if ($httpCode < 200 || $httpCode >= 300) {
echo "</p>"; echo "</p>";
exit; exit;
} }
?>
<script type="text/javascript"> // PDF generated successfully - now save metadata and count pages
window.location.replace("/documents/view/<?=$document['Document']['id']?>"); $result = json_decode($response, true);
</script> 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);
}
}
?>
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="refresh" content="0;url=/documents/view/<?=$document['Document']['id']?>" />
<title>Redirecting...</title>
</head>
<body>
<p>PDF generated successfully. Redirecting back to order acknowledgement...</p>
<p><a href="/documents/view/<?=$document['Document']['id']?>">Click here if not redirected</a></p>
<script type="text/javascript">
window.location.replace("/documents/view/<?=$document['Document']['id']?>");
</script>
</body>
</html>

View file

@ -64,9 +64,75 @@ if ($httpCode < 200 || $httpCode >= 300) {
echo "</p>"; echo "</p>";
exit; 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);
}
}
?> ?>
<!DOCTYPE html>
<script type="text/javascript"> <html>
window.location.replace("/documents/view/<?=$document['Document']['id']?>"); <head>
</script> <meta http-equiv="refresh" content="0;url=/documents/view/<?=$document['Document']['id']?>" />
<title>Redirecting...</title>
</head>
<body>
<p>PDF generated successfully. Redirecting back to packing list...</p>
<p><a href="/documents/view/<?=$document['Document']['id']?>">Click here if not redirected</a></p>
<script type="text/javascript">
window.location.replace("/documents/view/<?=$document['Document']['id']?>");
</script>
</body>
</html>

View file

@ -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 "<p style='color: red; font-weight: bold;'>ERROR: No currency information found for this purchase order. Please ensure the document or supplier has a currency set.</p>";
error_log("PO PDF Generation Error: No currency found for document ID " . $document['Document']['id']);
exit;
}
$payload = array( $payload = array(
'document_id' => intval($document['Document']['id']), 'document_id' => intval($document['Document']['id']),
'user_id' => isset($user['User']['id']) ? intval($user['User']['id']) : 0,
'title' => $document['PurchaseOrder']['title'], 'title' => $document['PurchaseOrder']['title'],
'issue_date' => $document['PurchaseOrder']['issue_date'], 'issue_date' => $document['PurchaseOrder']['issue_date'],
'issue_date_string' => isset($issue_date) ? $issue_date : '', 'issue_date_string' => isset($issue_date) ? $issue_date : '',
@ -40,8 +68,8 @@ $payload = array(
'dispatch_by' => $document['PurchaseOrder']['dispatch_by'], 'dispatch_by' => $document['PurchaseOrder']['dispatch_by'],
'deliver_to' => $document['PurchaseOrder']['deliver_to'], 'deliver_to' => $document['PurchaseOrder']['deliver_to'],
'shipping_instructions' => $document['PurchaseOrder']['shipping_instructions'], 'shipping_instructions' => $document['PurchaseOrder']['shipping_instructions'],
'currency_symbol' => $currencySymbol, 'currency_symbol' => $currencySymbolToUse,
'currency_code' => isset($currencyCode) ? $currencyCode : 'AUD', 'currency_code' => $currencyCodeToUse,
'show_gst' => (bool)$gst, 'show_gst' => (bool)$gst,
'subtotal' => isset($totals['subtotal']) ? floatval($totals['subtotal']) : 0.0, 'subtotal' => isset($totals['subtotal']) ? floatval($totals['subtotal']) : 0.0,
'gst_amount' => isset($totals['gst']) ? floatval($totals['gst']) : 0.0, 'gst_amount' => isset($totals['gst']) ? floatval($totals['gst']) : 0.0,
@ -69,9 +97,74 @@ if ($httpCode < 200 || $httpCode >= 300) {
echo "</p>"; echo "</p>";
exit; 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);
}
}
?> ?>
<!DOCTYPE html>
<script type="text/javascript"> <html>
window.location.replace("/documents/view/<?=$document['Document']['id']?>"); <head>
</script> <meta http-equiv="refresh" content="0;url=/documents/view/<?=$document['Document']['id']?>" />
<title>Redirecting...</title>
</head>
<body>
<p>PDF generated successfully. Redirecting back to purchase order...</p>
<p><a href="/documents/view/<?=$document['Document']['id']?>">Click here if not redirected</a></p>
<script type="text/javascript">
window.location.replace("/documents/view/<?=$document['Document']['id']?>");
</script>
</body>
</html>

View file

@ -1,4 +1,5 @@
<div class="purchaseOrders view"> <div class="purchaseOrders view">
<?=$this->element('pdf_created_message'); ?>
<h2><?php __('PurchaseOrder');?></h2> <h2><?php __('PurchaseOrder');?></h2>
<dl><?php $i = 0; $class = ' class="altrow"';?> <dl><?php $i = 0; $class = ' class="altrow"';?>
<dt<?php if ($i % 2 == 0) echo $class;?>><?php __('Id'); ?></dt> <dt<?php if ($i % 2 == 0) echo $class;?>><?php __('Id'); ?></dt>