Quotes almost working correctly
This commit is contained in:
parent
e197dc540a
commit
b9d44fc82b
|
|
@ -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},
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
41
go/internal/cmc/pdf/page_numbers.go
Normal file
41
go/internal/cmc/pdf/page_numbers.go
Normal 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
|
||||||
|
}
|
||||||
|
|
@ -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 {
|
.line-items .description {
|
||||||
width: 7%;
|
width: 50%;
|
||||||
text-align: center;
|
word-wrap: break-word;
|
||||||
|
overflow-wrap: break-word;
|
||||||
}
|
}
|
||||||
|
|
||||||
.line-items .qty {
|
.line-items .qty {
|
||||||
width: 7%;
|
width: 10%;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.line-items .description {
|
.line-items .unit-price {
|
||||||
width: 56%;
|
width: 13%;
|
||||||
|
text-align: right;
|
||||||
}
|
}
|
||||||
|
|
||||||
.line-items .unit-price {
|
.line-items .discount {
|
||||||
width: 15%;
|
width: 13%;
|
||||||
text-align: right;
|
text-align: right;
|
||||||
}
|
}
|
||||||
|
|
||||||
.line-items .total {
|
.line-items .total {
|
||||||
width: 15%;
|
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>
|
||||||
|
|
|
||||||
|
|
@ -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']),
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue