`)
title := ""
if v, ok := rec["display_name"]; ok {
title = fmt.Sprintf("%v", v)
} else if v, ok := rec["name"]; ok {
title = fmt.Sprintf("%v", v)
}
if title != "" {
b.WriteString(fmt.Sprintf("
%s
", htmlEscape(title)))
}
b.WriteString("
")
b.WriteString("| Field | Value |
")
for key, val := range rec {
if key == "id" {
continue
}
valStr := fmt.Sprintf("%v", val)
if valStr == "" {
valStr = ""
}
b.WriteString(fmt.Sprintf("| %s | %s |
",
htmlEscape(key), htmlEscape(valStr)))
}
b.WriteString("
")
b.WriteString("
")
}
b.WriteString(`
`)
return b.String()
}
// resolveReportModel maps a report name to the ORM model it operates on.
// Mirrors: odoo ir.actions.report → model field.
func resolveReportModel(reportName string) string {
mapping := map[string]string{
"account.report_invoice": "account.move",
"sale.report_saleorder": "sale.order",
"stock.report_picking": "stock.picking",
"purchase.report_purchaseorder": "purchase.order",
"contacts.report_partner": "res.partner",
}
return mapping[reportName]
}
// renderHTMLReport generates a basic HTML report for the given records.
// This is a minimal implementation — Odoo uses QWeb templates for real reports.
func renderHTMLReport(reportName, modelName string, records []orm.Values) string {
var b strings.Builder
b.WriteString(`
`)
// Use "name" or "display_name" as section title if available
title := ""
if v, ok := rec["display_name"]; ok {
title = fmt.Sprintf("%v", v)
} else if v, ok := rec["name"]; ok {
title = fmt.Sprintf("%v", v)
}
if title != "" {
b.WriteString(fmt.Sprintf("
%s
", htmlEscape(title)))
}
b.WriteString("
")
b.WriteString("| Field | Value |
")
for key, val := range rec {
if key == "id" {
continue
}
valStr := fmt.Sprintf("%v", val)
if valStr == "" {
valStr = ""
}
b.WriteString(fmt.Sprintf("| %s | %s |
",
htmlEscape(key), htmlEscape(valStr)))
}
b.WriteString("
")
b.WriteString("
")
}
b.WriteString("")
return b.String()
}
// htmlEscape escapes HTML special characters.
func htmlEscape(s string) string {
return template.HTMLEscapeString(s)
}
// renderInvoiceHTML generates a professional invoice HTML document for PDF conversion.
// Reads directly from the database to build a complete, styled invoice.
// Mirrors: odoo/addons/account/report/account_invoice_report_templates.xml
func renderInvoiceHTML(env *orm.Environment, moveID int64) string {
// Read invoice header
var name, partnerName, moveType, state, date, dueDate string
var total, untaxed, tax float64
env.Tx().QueryRow(env.Ctx(), `
SELECT COALESCE(m.name,'/'), COALESCE(p.name,''), COALESCE(m.move_type,'entry'),
COALESCE(m.state,'draft'),
COALESCE(m.date::text,''), COALESCE(m.invoice_date_due::text,''),
COALESCE(m.amount_total::float8, 0), COALESCE(m.amount_untaxed::float8, 0),
COALESCE(m.amount_tax::float8, 0)
FROM account_move m
LEFT JOIN res_partner p ON p.id = m.partner_id
WHERE m.id = $1`, moveID,
).Scan(&name, &partnerName, &moveType, &state, &date, &dueDate, &total, &untaxed, &tax)
// Read partner address
var partnerStreet, partnerCity, partnerZip, partnerCountry string
env.Tx().QueryRow(env.Ctx(), `
SELECT COALESCE(p.street,''), COALESCE(p.city,''), COALESCE(p.zip,''),
COALESCE(co.name,'')
FROM account_move m
LEFT JOIN res_partner p ON p.id = m.partner_id
LEFT JOIN res_country co ON co.id = p.country_id
WHERE m.id = $1`, moveID,
).Scan(&partnerStreet, &partnerCity, &partnerZip, &partnerCountry)
// Read company info
var companyName, companyEmail, companyPhone, companyStreet, companyCity, companyZip, companyCountry string
env.Tx().QueryRow(env.Ctx(), `
SELECT COALESCE(c.name,''), COALESCE(p.email,''), COALESCE(p.phone,''),
COALESCE(p.street,''), COALESCE(p.city,''), COALESCE(p.zip,''),
COALESCE(co.name,'')
FROM res_company c
JOIN res_partner p ON p.id = c.partner_id
LEFT JOIN res_country co ON co.id = p.country_id
WHERE c.id = 1`).Scan(&companyName, &companyEmail, &companyPhone,
&companyStreet, &companyCity, &companyZip, &companyCountry)
// Read invoice lines
type invLine struct {
name string
qty float64
price float64
amount float64
dtype string
}
rows, err := env.Tx().Query(env.Ctx(), `
SELECT COALESCE(name,''), COALESCE(quantity,0), COALESCE(price_unit::float8,0),
COALESCE(ABS(balance::float8),0), COALESCE(display_type,'product')
FROM account_move_line WHERE move_id = $1 AND display_type IN ('product','tax')
ORDER BY display_type, id`, moveID)
var lines []invLine
if err == nil {
defer rows.Close()
for rows.Next() {
var l invLine
if err := rows.Scan(&l.name, &l.qty, &l.price, &l.amount, &l.dtype); err == nil {
lines = append(lines, l)
}
}
}
// Build professional HTML
var b strings.Builder
b.WriteString(`