Quotes almost working correctly

This commit is contained in:
Finley Ghosh 2026-01-21 21:20:14 +11:00
parent e197dc540a
commit b9d44fc82b
5 changed files with 123 additions and 58 deletions

View file

@ -267,6 +267,7 @@ func GenerateQuotePDF(w http.ResponseWriter, r *http.Request) {
ItemNumber: li.ItemNumber, ItemNumber: li.ItemNumber,
Quantity: li.Quantity, Quantity: li.Quantity,
Title: li.Title, Title: li.Title,
Description: li.Description,
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", li.TotalPrice), Valid: true}, GrossPrice: sql.NullString{String: fmt.Sprintf("%.2f", li.TotalPrice), Valid: true},
} }

View file

@ -83,9 +83,9 @@ func (g *HTMLDocumentGenerator) GenerateInvoicePDF(data *InvoicePDFData) (string
os.Remove(tempPDFPath) os.Remove(tempPDFPath)
os.Remove(tempMergedPath) os.Remove(tempMergedPath)
// SECOND PASS: Generate final PDF with correct page count // SECOND PASS: Generate final PDF without page count in HTML (will be added via pdfcpu after merge)
fmt.Printf("=== HTML Generator: Second pass - regenerating with page count %d ===\n", totalPageCount) fmt.Println("=== HTML Generator: Second pass - regenerating final PDF ===")
html = g.BuildInvoiceHTML(data, totalPageCount, 1) html = g.BuildInvoiceHTML(data, 0, 0)
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 (second pass): %w", err) return "", fmt.Errorf("failed to write temp HTML (second pass): %w", err)
@ -121,9 +121,17 @@ func (g *HTMLDocumentGenerator) GenerateInvoicePDF(data *InvoicePDFData) (string
// Replace original with merged version // Replace original with merged version
if err := os.Rename(tempMergedPath, pdfPath); err != nil { if err := os.Rename(tempMergedPath, pdfPath); err != nil {
fmt.Printf("=== HTML Generator: Warning - could not replace PDF: %v\n", err) fmt.Printf("=== HTML Generator: Warning - could not replace PDF: %v\n", err)
return filename, err
} }
} }
// Add page numbers to final PDF
// TODO: Re-enable after fixing pdfcpu watermark API usage
// fmt.Println("=== HTML Generator: Adding page numbers to final PDF ===")
// if err := AddPageNumbers(pdfPath); err != nil {
// fmt.Printf("=== HTML Generator: Warning - could not add page numbers: %v\n", err)
// }
return filename, nil return filename, nil
} }
@ -183,9 +191,9 @@ func (g *HTMLDocumentGenerator) GenerateQuotePDF(data *QuotePDFData) (string, er
os.Remove(tempPDFPath) os.Remove(tempPDFPath)
os.Remove(tempMergedPath) os.Remove(tempMergedPath)
// SECOND PASS: Generate final PDF with correct page count // SECOND PASS: Generate final PDF without page count in HTML (will be added via pdfcpu after merge)
fmt.Printf("=== HTML Generator: Second pass - regenerating with page count %d ===\n", totalPageCount) fmt.Println("=== HTML Generator: Second pass - regenerating final PDF ===")
html = g.BuildQuoteHTML(data, totalPageCount, 1) html = g.BuildQuoteHTML(data, 0, 0)
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 (second pass): %w", err) return "", fmt.Errorf("failed to write temp HTML (second pass): %w", err)
@ -241,18 +249,26 @@ func (g *HTMLDocumentGenerator) GenerateQuotePDF(data *QuotePDFData) (string, er
fmt.Printf("=== HTML Generator: Warning - could not replace PDF: %v\n", err) fmt.Printf("=== HTML Generator: Warning - could not replace PDF: %v\n", err)
fmt.Printf("=== HTML Generator: tempMergedPath: %s\n", tempMergedPath) fmt.Printf("=== HTML Generator: tempMergedPath: %s\n", tempMergedPath)
fmt.Printf("=== HTML Generator: pdfPath: %s\n", pdfPath) fmt.Printf("=== HTML Generator: pdfPath: %s\n", pdfPath)
} else { return filename, err
fmt.Printf("=== HTML Generator: Replaced PDF successfully\n")
} }
fmt.Printf("=== HTML Generator: Replaced PDF successfully\n")
// Verify the final file exists // Verify the final file exists
if stat, err := os.Stat(pdfPath); err == nil { if stat, err := os.Stat(pdfPath); err == nil {
fmt.Printf("=== HTML Generator: Final PDF verified, size=%d bytes\n", stat.Size()) fmt.Printf("=== HTML Generator: Final PDF verified, size=%d bytes\n", stat.Size())
} else { } else {
fmt.Printf("=== HTML Generator: ERROR - Final PDF does not exist after merge: %v\n", err) fmt.Printf("=== HTML Generator: ERROR - Final PDF does not exist after merge: %v\n", err)
return filename, fmt.Errorf("final PDF does not exist: %w", err)
} }
} }
// Add page numbers to final PDF
// TODO: Re-enable after fixing pdfcpu watermark API usage
// fmt.Println("=== HTML Generator: Adding page numbers to final PDF ===")
// if err := AddPageNumbers(pdfPath); err != nil {
// fmt.Printf("=== HTML Generator: Warning - could not add page numbers: %v\n", err)
// }
return filename, nil return filename, nil
} }

View file

@ -0,0 +1,41 @@
package pdf
import (
"fmt"
"github.com/pdfcpu/pdfcpu/pkg/api"
"github.com/pdfcpu/pdfcpu/pkg/pdfcpu/color"
"github.com/pdfcpu/pdfcpu/pkg/pdfcpu/model"
"github.com/pdfcpu/pdfcpu/pkg/pdfcpu/types"
)
// AddPageNumbers adds page numbers to a PDF file
// This should be called after all merging is complete to show accurate page counts
func AddPageNumbers(pdfPath string) error {
// Read the PDF to get page count
pageCount, err := api.PageCountFile(pdfPath)
if err != nil {
return fmt.Errorf("failed to get page count: %w", err)
}
// Create watermark configuration for page numbers
// Position: bottom right, slightly above footer
wm := &model.Watermark{
TextString: fmt.Sprintf("Page $p of %d", pageCount),
FontName: "Helvetica",
FontSize: 9,
ScaleAbs: true,
Color: color.Black,
Pos: types.BottomRight,
Dx: -15, // 15mm from right
Dy: 25, // 25mm from bottom
Rotation: 0,
}
// Apply watermark (page numbers) to all pages
if err := api.AddWatermarksFile(pdfPath, pdfPath, nil, wm, nil); err != nil {
return fmt.Errorf("failed to add page numbers: %w", err)
}
return nil
}

View file

@ -9,6 +9,15 @@
margin: 15mm; margin: 15mm;
} }
body {
font-family: Helvetica, Arial, sans-serif;
font-size: 9pt;
line-height: 1.4;
margin: 0;
padding: 0 0 25mm 0;
color: #000;
}
.logo { .logo {
width: 40mm; width: 40mm;
height: auto; height: auto;
@ -118,13 +127,6 @@
.pricing-header { .pricing-header {
text-align: center; text-align: center;
margin: 8mm 0 5mm 0; margin: 8mm 0 5mm 0;
page-break-before: always;
}
.pricing-header h2 {
font-size: 14pt;
font-weight: bold;
margin: 0;
} }
.line-items { .line-items {
@ -134,6 +136,7 @@
table-layout: fixed; table-layout: fixed;
margin-right: 1mm; margin-right: 1mm;
margin-left: auto; margin-left: auto;
page-break-after: avoid;
} }
.line-items th { .line-items th {
@ -144,33 +147,39 @@
text-align: left; text-align: left;
} }
.line-items tbody tr {
page-break-inside: avoid;
}
.line-items td { .line-items td {
border: 1px solid #000; border: 1px solid #000;
padding: 2mm; padding: 2mm;
vertical-align: top; vertical-align: top;
} }
.line-items .item-no {
width: 7%;
text-align: center;
}
.line-items .qty {
width: 7%;
text-align: center;
}
.line-items .description { .line-items .description {
width: 56%; width: 50%;
word-wrap: break-word;
overflow-wrap: break-word;
} }
.line-items .unit-price { .line-items .qty {
width: 15%; width: 10%;
text-align: right; text-align: center;
} }
.line-items .total { .line-items .unit-price {
width: 15%; width: 13%;
text-align: right;
}
.line-items .discount {
width: 13%;
text-align: right;
}
.line-items .total {
width: 14%;
text-align: right; text-align: right;
} }
@ -178,6 +187,8 @@
display: flex; display: flex;
justify-content: flex-end; justify-content: flex-end;
margin-top: 5mm; margin-top: 5mm;
page-break-inside: avoid;
page-break-before: avoid;
} }
.totals-table { .totals-table {
@ -298,12 +309,6 @@
.footer .service-flow { color: #2F4BE0; } .footer .service-flow { color: #2F4BE0; }
.footer .service-process { color: #AB31F8; } .footer .service-process { color: #AB31F8; }
.page-number {
position: fixed;
bottom: 25mm;
right: 15mm;
font-size: 9pt;
}
</style> </style>
</head> </head>
<body> <body>
@ -347,31 +352,29 @@
<!-- Pricing & Specifications Header --> <!-- Pricing & Specifications Header -->
<div class="pricing-header"> <div class="pricing-header">
<h2>PRICING & SPECIFICATIONS</h2> <h2>PRICING & SPECIFICATIONS</h2>
<div style="margin-top: 3mm; text-align: right; font-weight: bold; font-size: 9pt;">
Shown in {{.CurrencyCode}}
</div>
</div> </div>
<!-- Line Items Table --> <!-- Line Items Table -->
<table class="line-items"> <table class="line-items">
<thead> <thead>
<tr> <tr>
<th class="item-no">ITEM</th>
<th class="qty">QTY</th>
<th class="description">DESCRIPTION</th> <th class="description">DESCRIPTION</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="item-no">{{.ItemNumber}}</td> <td class="description"><strong>{{.Title}}</strong><br>{{.Description}}</td>
<td class="qty">{{.Quantity}}</td> <td class="qty">{{.Quantity}}</td>
<td class="description">
<strong>{{.Title}}</strong>
{{if .Description}}
<div>{{.Description}}</div>
{{end}}
</td>
<td class="unit-price">{{formatPrice .GrossUnitPrice}}</td> <td class="unit-price">{{formatPrice .GrossUnitPrice}}</td>
<td class="discount">$0.00</td>
<td class="total">{{formatPrice .GrossPrice}}</td> <td class="total">{{formatPrice .GrossPrice}}</td>
</tr> </tr>
{{end}} {{end}}
@ -460,13 +463,6 @@
</table> </table>
{{end}} {{end}}
<!-- Page Number -->
{{if .PageCount}}
<div class="page-number">
Page {{.CurrentPage}} of {{.PageCount}}
</div>
{{end}}
<!-- Footer --> <!-- Footer -->
<div class="footer"> <div class="footer">
<div class="services-title">CMC TECHNOLOGIES Provides Solutions in the Following Fields</div> <div class="services-title">CMC TECHNOLOGIES Provides Solutions in the Following Fields</div>

View file

@ -12,6 +12,7 @@ foreach ($document['LineItem'] as $li) {
'item_number' => $li['item_number'], 'item_number' => $li['item_number'],
'quantity' => $li['quantity'], 'quantity' => $li['quantity'],
'title' => $li['title'], 'title' => $li['title'],
'description' => isset($li['description']) ? $li['description'] : '',
'unit_price' => floatval($li['gross_unit_price']), 'unit_price' => floatval($li['gross_unit_price']),
'total_price' => floatval($li['gross_price']) 'total_price' => floatval($li['gross_price'])
); );
@ -41,6 +42,16 @@ $userLastName = !empty($enquiry['User']['last_name']) ? $enquiry['User']['last_n
$userEmail = !empty($enquiry['User']['email']) ? $enquiry['User']['email'] : ''; $userEmail = !empty($enquiry['User']['email']) ? $enquiry['User']['email'] : '';
$createdDate = !empty($enquiry['Enquiry']['created']) ? $enquiry['Enquiry']['created'] : date('Y-m-d'); $createdDate = !empty($enquiry['Enquiry']['created']) ? $enquiry['Enquiry']['created'] : date('Y-m-d');
// Extract GST setting from enquiry data (fallback to currency or document data)
$showGst = false;
if (isset($enquiry['Enquiry']['gst'])) {
$showGst = (bool)$enquiry['Enquiry']['gst'];
} elseif (isset($gst)) {
$showGst = (bool)$gst;
} elseif (isset($currencyCode) && $currencyCode === 'AUD') {
$showGst = true; // Default to GST for Australian quotes
}
$payload = array( $payload = array(
'document_id' => intval($document['Document']['id']), 'document_id' => intval($document['Document']['id']),
'cmc_reference' => $cmcReference, 'cmc_reference' => $cmcReference,
@ -55,7 +66,7 @@ $payload = array(
'user_email' => $userEmail, 'user_email' => $userEmail,
'currency_symbol' => $currencySymbol, 'currency_symbol' => $currencySymbol,
'currency_code' => isset($currencyCode) ? $currencyCode : 'AUD', 'currency_code' => isset($currencyCode) ? $currencyCode : 'AUD',
'show_gst' => (bool)$gst, 'show_gst' => $showGst,
'commercial_comments' => isset($document['Quote']['commercial_comments']) ? $document['Quote']['commercial_comments'] : '', 'commercial_comments' => isset($document['Quote']['commercial_comments']) ? $document['Quote']['commercial_comments'] : '',
'line_items' => $lineItems, 'line_items' => $lineItems,
'pages' => array_map(function($p) { return $p['content']; }, $document['DocPage']), 'pages' => array_map(function($p) { return $p['content']; }, $document['DocPage']),