2026-01-16 20:41:24 -08:00
|
|
|
package pdf
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
"bytes"
|
|
|
|
|
"database/sql"
|
|
|
|
|
"fmt"
|
|
|
|
|
"html/template"
|
|
|
|
|
"path/filepath"
|
|
|
|
|
"strconv"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
// QuoteLineItemTemplateData represents a quote line item for template rendering
|
|
|
|
|
type QuoteLineItemTemplateData struct {
|
|
|
|
|
ItemNumber string
|
|
|
|
|
Title string
|
|
|
|
|
Description template.HTML
|
|
|
|
|
Quantity string
|
|
|
|
|
GrossUnitPrice sql.NullString
|
|
|
|
|
GrossPrice sql.NullString
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// BuildQuoteHTML generates the complete HTML for a quote using templates
|
|
|
|
|
func (g *HTMLDocumentGenerator) BuildQuoteHTML(data *QuotePDFData, totalPages int, currentPage int) string {
|
|
|
|
|
// Get quote number from Document.CmcReference
|
|
|
|
|
quoteNum := ""
|
|
|
|
|
if data.Document != nil {
|
|
|
|
|
quoteNum = data.Document.CmcReference
|
|
|
|
|
if data.Document.Revision > 0 {
|
|
|
|
|
quoteNum = fmt.Sprintf("%s.%d", quoteNum, data.Document.Revision)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
fmt.Printf("=== buildQuoteHTML: Quote number: %s ===\n", quoteNum)
|
|
|
|
|
|
|
|
|
|
// Prepare FROM name
|
|
|
|
|
fromName := ""
|
|
|
|
|
if data.User != nil {
|
|
|
|
|
fromName = fmt.Sprintf("%s %s", data.User.FirstName, data.User.LastName)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fromEmail := ""
|
|
|
|
|
if data.User != nil {
|
|
|
|
|
fromEmail = data.User.Email
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Prepare company name
|
|
|
|
|
companyName := ""
|
|
|
|
|
if data.Customer != nil {
|
|
|
|
|
companyName = data.Customer.Name
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Prepare page content (first page only for now, multi-page support later)
|
|
|
|
|
pageContent := template.HTML("")
|
|
|
|
|
if len(data.Pages) > 0 {
|
|
|
|
|
pageContent = template.HTML(data.Pages[0])
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Calculate totals
|
|
|
|
|
subtotal := 0.0
|
|
|
|
|
for _, item := range data.LineItems {
|
|
|
|
|
if item.GrossPrice.Valid {
|
|
|
|
|
price, _ := strconv.ParseFloat(item.GrossPrice.String, 64)
|
|
|
|
|
subtotal += price
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
gstAmount := 0.0
|
|
|
|
|
total := subtotal
|
|
|
|
|
if data.ShowGST {
|
|
|
|
|
gstAmount = subtotal * 0.1
|
|
|
|
|
total = subtotal + gstAmount
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Prepare template data
|
|
|
|
|
templateData := struct {
|
|
|
|
|
QuoteNumber string
|
|
|
|
|
CompanyName string
|
|
|
|
|
EmailTo string
|
|
|
|
|
Attention string
|
|
|
|
|
FromName string
|
|
|
|
|
FromEmail string
|
|
|
|
|
YourReference string
|
|
|
|
|
IssueDateString string
|
|
|
|
|
PageContent template.HTML
|
|
|
|
|
CurrencyCode string
|
|
|
|
|
CurrencySymbol string
|
|
|
|
|
LineItems []QuoteLineItemTemplateData
|
|
|
|
|
Subtotal float64
|
|
|
|
|
GSTAmount float64
|
|
|
|
|
Total float64
|
|
|
|
|
ShowGST bool
|
|
|
|
|
CommercialComments string
|
|
|
|
|
DeliveryTime string
|
|
|
|
|
PaymentTerms string
|
|
|
|
|
DaysValid int
|
|
|
|
|
DeliveryPoint string
|
|
|
|
|
ExchangeRate string
|
|
|
|
|
CustomsDuty string
|
|
|
|
|
GSTPhrase string
|
|
|
|
|
SalesEngineer string
|
|
|
|
|
PageCount int
|
|
|
|
|
CurrentPage int
|
|
|
|
|
LogoDataURI string
|
|
|
|
|
}{
|
|
|
|
|
QuoteNumber: quoteNum,
|
|
|
|
|
CompanyName: companyName,
|
|
|
|
|
EmailTo: data.EmailTo,
|
|
|
|
|
Attention: data.Attention,
|
|
|
|
|
FromName: fromName,
|
|
|
|
|
FromEmail: fromEmail,
|
|
|
|
|
YourReference: fmt.Sprintf("Enquiry on %s", data.IssueDateString),
|
|
|
|
|
IssueDateString: data.IssueDateString,
|
|
|
|
|
PageContent: pageContent,
|
|
|
|
|
CurrencyCode: data.CurrencyCode,
|
|
|
|
|
CurrencySymbol: data.CurrencySymbol,
|
|
|
|
|
Subtotal: subtotal,
|
|
|
|
|
GSTAmount: gstAmount,
|
|
|
|
|
Total: total,
|
|
|
|
|
ShowGST: data.ShowGST,
|
|
|
|
|
CommercialComments: data.CommercialComments,
|
|
|
|
|
DeliveryTime: data.DeliveryTime,
|
|
|
|
|
PaymentTerms: data.PaymentTerms,
|
|
|
|
|
DaysValid: data.DaysValid,
|
|
|
|
|
DeliveryPoint: data.DeliveryPoint,
|
|
|
|
|
ExchangeRate: data.ExchangeRate,
|
|
|
|
|
CustomsDuty: data.CustomsDuty,
|
|
|
|
|
GSTPhrase: data.GSTPhrase,
|
|
|
|
|
SalesEngineer: data.SalesEngineer,
|
|
|
|
|
PageCount: totalPages,
|
|
|
|
|
CurrentPage: currentPage,
|
|
|
|
|
LogoDataURI: g.loadLogo("quote_logo.png"),
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Convert line items to template format
|
|
|
|
|
for _, item := range data.LineItems {
|
|
|
|
|
templateData.LineItems = append(templateData.LineItems, QuoteLineItemTemplateData{
|
|
|
|
|
ItemNumber: item.ItemNumber,
|
|
|
|
|
Title: item.Title,
|
|
|
|
|
Quantity: item.Quantity,
|
|
|
|
|
Description: template.HTML(""), // Quotes don't have descriptions in line items by default
|
|
|
|
|
GrossUnitPrice: item.GrossUnitPrice,
|
|
|
|
|
GrossPrice: item.GrossPrice,
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Define template functions
|
|
|
|
|
funcMap := template.FuncMap{
|
|
|
|
|
"formatPrice": func(price sql.NullString) template.HTML {
|
|
|
|
|
if !price.Valid || price.String == "" {
|
|
|
|
|
return ""
|
|
|
|
|
}
|
|
|
|
|
formatted := FormatPriceWithCommas(price.String)
|
|
|
|
|
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))
|
|
|
|
|
},
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Parse and execute template
|
2026-01-20 23:43:55 -08:00
|
|
|
// Try multiple possible path sets to find the quote and shared header templates together
|
|
|
|
|
possiblePathSets := [][]string{
|
|
|
|
|
{
|
|
|
|
|
filepath.Join("internal", "cmc", "pdf", "templates", "quote.html"),
|
|
|
|
|
filepath.Join("internal", "cmc", "pdf", "templates", "header.html"),
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
filepath.Join("go", "internal", "cmc", "pdf", "templates", "quote.html"),
|
|
|
|
|
filepath.Join("go", "internal", "cmc", "pdf", "templates", "header.html"),
|
|
|
|
|
},
|
2026-01-21 00:53:57 -08:00
|
|
|
{
|
|
|
|
|
filepath.Join("templates", "quote.html"),
|
|
|
|
|
filepath.Join("templates", "header.html"),
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
"/app/templates/quote.html",
|
|
|
|
|
"/app/templates/header.html",
|
|
|
|
|
},
|
2026-01-20 23:43:55 -08:00
|
|
|
{
|
|
|
|
|
"/app/go/internal/cmc/pdf/templates/quote.html",
|
|
|
|
|
"/app/go/internal/cmc/pdf/templates/header.html",
|
|
|
|
|
},
|
2026-01-16 20:41:24 -08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var tmpl *template.Template
|
|
|
|
|
var err error
|
|
|
|
|
|
2026-01-20 23:43:55 -08:00
|
|
|
for _, pathSet := range possiblePathSets {
|
|
|
|
|
tmpl, err = template.New("quote.html").Funcs(funcMap).ParseFiles(pathSet...)
|
2026-01-16 20:41:24 -08:00
|
|
|
if err == nil {
|
|
|
|
|
break
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if tmpl == nil || err != nil {
|
|
|
|
|
fmt.Printf("Error parsing template from any path: %v\n", err)
|
|
|
|
|
return ""
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var buf bytes.Buffer
|
|
|
|
|
if err := tmpl.Execute(&buf, templateData); err != nil {
|
|
|
|
|
fmt.Printf("Error executing template: %v\n", err)
|
|
|
|
|
return ""
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return buf.String()
|
|
|
|
|
}
|