standardising invoice and oa, all documents do same redirect, more document fixes
This commit is contained in:
parent
e6b05a726d
commit
fe2db6d597
|
|
@ -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")
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
package pdf
|
||||
package documents
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
//go:build never
|
||||
// +build never
|
||||
|
||||
package pdf
|
||||
package documents
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
//go:build never
|
||||
// +build never
|
||||
package pdf
|
||||
package pdf
|
||||
package documents
|
||||
package documents
|
||||
|
||||
import (
|
||||
"os"
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
package pdf
|
||||
package documents
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
package pdf
|
||||
package documents
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
package pdf
|
||||
package documents
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
|
|
|
|||
|
|
@ -384,9 +384,9 @@
|
|||
<tr>
|
||||
<td class="description"><strong>{{.Title}}</strong><br>{{.Description}}</td>
|
||||
<td class="qty">{{.Quantity}}</td>
|
||||
<td class="unit-price">{{formatPrice .GrossUnitPrice}}</td>
|
||||
<td class="discount">{{formatDiscount .DiscountAmountTotal}}</td>
|
||||
<td class="total">{{formatPrice .GrossPrice}}</td>
|
||||
<td class="unit-price">{{formatPrice .GrossUnitPrice .GrossUnitPriceText}}</td>
|
||||
<td class="discount">{{formatDiscount .DiscountAmountTotal .HasTextPrices}}</td>
|
||||
<td class="total">{{formatPrice .GrossPrice .GrossPriceText}}</td>
|
||||
</tr>
|
||||
{{end}}
|
||||
</tbody>
|
||||
|
|
@ -395,13 +395,28 @@
|
|||
<!-- Payment and Totals Side by Side -->
|
||||
<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;">
|
||||
<h4 style="margin: 0 0 1mm 0; font-weight: bold; font-size: 10pt;">MAKE PAYMENT TO:</h4>
|
||||
<table style="width: 100%; border-collapse: collapse; font-size: 9pt; table-layout: fixed;">
|
||||
<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><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>
|
||||
<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>
|
||||
<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><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>
|
||||
<h4 style="margin: 0 0 2mm 0; font-weight: bold; font-size: 10pt;">MAKE PAYMENT TO:</h4>
|
||||
<table style="width: 100%; border-collapse: collapse;">
|
||||
<tr>
|
||||
<td style="font-weight: bold; width: 50%; padding: 1mm 0;">Account Name:</td>
|
||||
<td style="width: 50%; padding: 1mm 0;">CMC Technologies Pty Ltd</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>
|
||||
</div>
|
||||
<div style="width: 50%; margin-left: auto; margin-right: 1mm;">
|
||||
|
|
@ -414,5 +429,15 @@
|
|||
</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}}
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</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 @@
|
|||
</head>
|
||||
<body>
|
||||
{{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">
|
||||
<tr>
|
||||
<td class="label">COMPANY NAME:</td>
|
||||
<td>{{.CompanyName}}</td>
|
||||
<td class="label">ORDER ACK #:</td>
|
||||
<td>{{.OrderAckNumber}}</td>
|
||||
<td class="label">YOUR REFERENCE:</td>
|
||||
<td>{{.YourReference}}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="label">CONTACT:</td>
|
||||
<td>{{.Attention}}</td>
|
||||
<td class="label">DATE:</td>
|
||||
<td>{{.IssueDateString}}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="label">EMAIL:</td>
|
||||
<td>{{.EmailTo}}</td>
|
||||
<td class="label">YOUR REFERENCE:</td>
|
||||
<td>{{.YourReference}}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="label">JOB TITLE:</td>
|
||||
|
|
@ -224,29 +253,37 @@
|
|||
</table>
|
||||
|
||||
<!-- Address Boxes -->
|
||||
<div class="address-boxes">
|
||||
<div class="address-box">
|
||||
<h3>BILL TO:</h3>
|
||||
<p>{{.BillTo}}</p>
|
||||
<div class="address-boxes" style="display: flex; gap: 5mm; margin-bottom: 3mm;">
|
||||
<div class="address-box" style="flex: 1;">
|
||||
<h3>Sold To / Invoice Address:</h3>
|
||||
<div>{{.BillTo}}</div>
|
||||
</div>
|
||||
<div class="address-box">
|
||||
<h3>SHIP TO:</h3>
|
||||
<p>{{.ShipTo}}</p>
|
||||
<div class="address-box" style="flex: 1; margin-right: 1mm; margin-left: auto;">
|
||||
<h3>Delivery Address:</h3>
|
||||
<div>{{.ShipTo}}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Shipping Details -->
|
||||
<table class="details-table">
|
||||
<tr>
|
||||
<td class="label">SHIP VIA:</td>
|
||||
<td>{{.ShipVia}}</td>
|
||||
<td class="label">FOB:</td>
|
||||
<td>{{.FOB}}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="label">PAYMENT TERMS:</td>
|
||||
<td colspan="3">{{.PaymentTerms}}</td>
|
||||
</tr>
|
||||
<!-- Details Table - match invoice 5-column layout -->
|
||||
<table class="line-items" style="margin-bottom: 3mm;">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="width: 20%;">CUSTOMER ORDER NO</th>
|
||||
<th style="width: 20%;">CMC JOB #</th>
|
||||
<th style="width: 20%;">INCOTERMS 2010</th>
|
||||
<th style="width: 20%;">PAYMENT TERMS</th>
|
||||
<th style="width: 20%;">CUSTOMER ABN</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>{{.YourReference}}</td>
|
||||
<td>{{.JobTitle}}</td>
|
||||
<td>{{.FOB}}</td>
|
||||
<td>{{.PaymentTerms}}</td>
|
||||
<td>{{.CustomerABN}}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<!-- Currency Code Header -->
|
||||
|
|
@ -268,34 +305,62 @@
|
|||
<tbody>
|
||||
{{range .LineItems}}
|
||||
<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="unit-price">{{formatPrice .UnitPrice}}</td>
|
||||
<td class="discount">{{if .Discount}}{{formatPrice .Discount}}{{else}}$0.00{{end}}</td>
|
||||
<td class="total">{{formatPrice .TotalPrice}}</td>
|
||||
<td class="unit-price">{{formatPrice .UnitPrice .UnitPriceText}}</td>
|
||||
<td class="discount">{{formatDiscount .Discount .HasTextPrices}}</td>
|
||||
<td class="total">{{formatPrice .TotalPrice .TotalPriceText}}</td>
|
||||
</tr>
|
||||
{{end}}
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<!-- Totals -->
|
||||
<div class="totals-container">
|
||||
<table class="totals-table">
|
||||
<tr>
|
||||
<td>SUBTOTAL</td>
|
||||
<td>{{formatTotal .Subtotal}}</td>
|
||||
</tr>
|
||||
{{if .ShowGST}}
|
||||
<tr>
|
||||
<td>GST (10%)</td>
|
||||
<td>{{formatTotal .GSTAmount}}</td>
|
||||
</tr>
|
||||
<!-- Payment and Totals Side by Side -->
|
||||
<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;">
|
||||
<h4 style="margin: 0 0 2mm 0; font-weight: bold; font-size: 10pt;">MAKE PAYMENT TO:</h4>
|
||||
<table style="width: 100%; border-collapse: collapse;">
|
||||
<tr>
|
||||
<td style="font-weight: bold; width: 50%; padding: 1mm 0;">Account Name:</td>
|
||||
<td style="width: 50%; padding: 1mm 0;">CMC Technologies Pty Ltd</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>
|
||||
</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}}
|
||||
<tr class="total-row">
|
||||
<td>{{if .ShowGST}}TOTAL PAYABLE{{else}}TOTAL{{end}}</td>
|
||||
<td>{{formatTotal .Total}}</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Footer -->
|
||||
|
|
|
|||
|
|
@ -190,7 +190,7 @@
|
|||
<tbody>
|
||||
{{range .LineItems}}
|
||||
<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="weight">{{if .Weight}}{{.Weight}}{{else}}-{{end}}</td>
|
||||
</tr>
|
||||
|
|
|
|||
|
|
@ -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 @@
|
|||
</div>
|
||||
{{end}}
|
||||
|
||||
<!-- Currency Code Header -->
|
||||
<div style="margin-bottom: 1mm; text-align: right; font-weight: bold; font-size: 9pt;">
|
||||
<!-- Currency Code Header - keep with table -->
|
||||
<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}}
|
||||
</div>
|
||||
|
||||
<!-- Line Items Table -->
|
||||
<table class="line-items">
|
||||
<table class="line-items" style="page-break-before: avoid;">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="description">DESCRIPTION</th>
|
||||
<th class="qty">QTY</th>
|
||||
<th class="unit-price">UNIT PRICE</th>
|
||||
<th class="discount">DISCOUNT</th>
|
||||
<th class="total">TOTAL</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{{range .LineItems}}
|
||||
<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="unit-price">{{formatPrice .UnitPrice}}</td>
|
||||
<td class="discount">{{formatPrice .Discount}}</td>
|
||||
<td class="total">{{formatPrice .TotalPrice}}</td>
|
||||
</tr>
|
||||
{{end}}
|
||||
|
|
|
|||
|
|
@ -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 <br>
|
||||
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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 <br>
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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() {
|
||||
|
|
|
|||
|
|
@ -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'] . " ===");
|
||||
?>
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<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>
|
||||
<body>
|
||||
<p>PDF generated successfully. <a href="/documents/view/<?=$document['Document']['id']?>">Click here if you are not redirected</a>.</p>
|
||||
<script type="text/javascript">
|
||||
window.location.replace("/documents/view/<?=$document['Document']['id']?>");
|
||||
</script>
|
||||
<p>PDF generated successfully. Redirecting back to invoice...</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>
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
<?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();
|
||||
$goEndpoint = $goBaseUrl . '/go/document/generate/order-acknowledgement';
|
||||
|
|
@ -7,41 +7,100 @@ $goEndpoint = $goBaseUrl . '/go/document/generate/order-acknowledgement';
|
|||
$outputDir = Configure::read('pdf_directory');
|
||||
|
||||
$lineItems = array();
|
||||
if (isset($document['LineItem']) && is_array($document['LineItem'])) {
|
||||
foreach ($document['LineItem'] as $li) {
|
||||
$lineItems[] = array(
|
||||
'item_number' => 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 "</p>";
|
||||
exit;
|
||||
}
|
||||
?>
|
||||
|
||||
<script type="text/javascript">
|
||||
window.location.replace("/documents/view/<?=$document['Document']['id']?>");
|
||||
</script>
|
||||
// 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>
|
||||
<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>
|
||||
|
|
|
|||
|
|
@ -64,9 +64,75 @@ if ($httpCode < 200 || $httpCode >= 300) {
|
|||
echo "</p>";
|
||||
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);
|
||||
}
|
||||
}
|
||||
?>
|
||||
|
||||
<script type="text/javascript">
|
||||
window.location.replace("/documents/view/<?=$document['Document']['id']?>");
|
||||
</script>
|
||||
<!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 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>
|
||||
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
'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 "</p>";
|
||||
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);
|
||||
}
|
||||
}
|
||||
?>
|
||||
|
||||
<script type="text/javascript">
|
||||
window.location.replace("/documents/view/<?=$document['Document']['id']?>");
|
||||
</script>
|
||||
|
||||
<!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 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>
|
||||
|
|
@ -1,4 +1,5 @@
|
|||
<div class="purchaseOrders view">
|
||||
<?=$this->element('pdf_created_message'); ?>
|
||||
<h2><?php __('PurchaseOrder');?></h2>
|
||||
<dl><?php $i = 0; $class = ' class="altrow"';?>
|
||||
<dt<?php if ($i % 2 == 0) echo $class;?>><?php __('Id'); ?></dt>
|
||||
|
|
|
|||
Loading…
Reference in a new issue