Files
goodie/addons/account/models/account_report_html.go
Marc 0a76a2b9aa Account module massive expansion: 2499→5049 LOC (+2550)
New models (12):
- account.asset: depreciation (linear/degressive), journal entry generation
- account.edi.format + account.edi.document: UBL 2.1 XML e-invoicing
- account.followup.line: payment follow-up escalation levels
- account.reconcile.model + lines: automatic bank reconciliation rules
- crossovered.budget + lines + account.budget.post: budgeting system
- account.cash.rounding: invoice rounding (UP/DOWN/HALF-UP)
- account.payment.method + lines: payment method definitions
- account.invoice.send: invoice sending wizard

Enhanced existing:
- account.move: action_reverse (credit notes), access_url, invoice_has_outstanding
- account.move.line: tax_tag_ids, analytic_distribution, date_maturity, matching_number
- Entry hash chain integrity (SHA-256, secure_sequence_number)
- Report HTML rendering for all 6 report types
- res.partner extended with followup status + overdue tracking

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-03 21:59:50 +02:00

130 lines
4.3 KiB
Go

package models
import (
"fmt"
"strings"
"odoo-go/pkg/orm"
)
// RenderReportHTML generates an HTML table for a report type.
// Mirrors: odoo/addons/account_reports/models/account_report.py _get_html()
//
// Supports: trial_balance, balance_sheet, profit_loss, aged_receivable,
// aged_payable, general_ledger.
func RenderReportHTML(env *orm.Environment, reportType string) (string, error) {
var data interface{}
var err error
switch reportType {
case "trial_balance":
data, err = generateTrialBalance(env)
case "balance_sheet":
data, err = generateBalanceSheet(env)
case "profit_loss":
data, err = generateProfitLoss(env)
case "aged_receivable":
data, err = generateAgedReport(env, "asset_receivable")
case "aged_payable":
data, err = generateAgedReport(env, "liability_payable")
case "general_ledger":
data, err = generateGeneralLedger(env)
default:
return "", fmt.Errorf("unknown report: %s", reportType)
}
if err != nil {
return "", err
}
result, ok := data.(map[string]interface{})
if !ok {
return "", fmt.Errorf("invalid report data")
}
lines, _ := result["lines"].([]map[string]interface{})
var b strings.Builder
b.WriteString(`<!DOCTYPE html><html><head><meta charset="utf-8"><style>
body{font-family:Arial,Helvetica,sans-serif;margin:20px;color:#333}
table{width:100%;border-collapse:collapse;margin-top:12px}
th,td{border:1px solid #ddd;padding:6px 8px;text-align:right}
th{background:#875a7b;color:white;font-weight:600}
td:first-child,th:first-child{text-align:left}
tr:last-child{font-weight:bold;background:#f5f5f5}
tr:hover{background:#faf5f8}
h2{color:#875a7b;margin-bottom:4px}
.report-date{color:#888;font-size:0.85em;margin-bottom:12px}
</style></head><body>`)
// Build table based on report type
switch reportType {
case "trial_balance":
b.WriteString("<h2>Trial Balance</h2>")
b.WriteString(`<table><tr><th>Code</th><th>Account</th><th>Debit</th><th>Credit</th><th>Balance</th></tr>`)
for _, l := range lines {
b.WriteString(fmt.Sprintf("<tr><td>%v</td><td>%v</td><td>%.2f</td><td>%.2f</td><td>%.2f</td></tr>",
l["code"], l["name"], toF(l["debit"]), toF(l["credit"]), toF(l["balance"])))
}
b.WriteString("</table>")
case "balance_sheet":
b.WriteString("<h2>Balance Sheet</h2>")
b.WriteString(`<table><tr><th>Section</th><th>Code</th><th>Account</th><th>Balance</th></tr>`)
for _, l := range lines {
b.WriteString(fmt.Sprintf("<tr><td>%v</td><td>%v</td><td>%v</td><td>%.2f</td></tr>",
l["section"], l["code"], l["name"], toF(l["balance"])))
}
b.WriteString("</table>")
case "profit_loss":
b.WriteString("<h2>Profit &amp; Loss</h2>")
b.WriteString(`<table><tr><th>Section</th><th>Code</th><th>Account</th><th>Amount</th></tr>`)
for _, l := range lines {
b.WriteString(fmt.Sprintf("<tr><td>%v</td><td>%v</td><td>%v</td><td>%.2f</td></tr>",
l["section"], l["code"], l["name"], toF(l["balance"])))
}
b.WriteString("</table>")
case "aged_receivable", "aged_payable":
title := "Aged Receivable"
if reportType == "aged_payable" {
title = "Aged Payable"
}
b.WriteString(fmt.Sprintf("<h2>%s</h2>", title))
b.WriteString(`<table><tr><th>Partner</th><th>Current</th><th>1-30</th><th>31-60</th><th>61-90+</th><th>Total</th></tr>`)
for _, l := range lines {
b.WriteString(fmt.Sprintf("<tr><td>%v</td><td>%.2f</td><td>%.2f</td><td>%.2f</td><td>%.2f</td><td>%.2f</td></tr>",
l["partner"], toF(l["current"]), toF(l["1-30"]), toF(l["31-60"]), toF(l["61-90+"]), toF(l["total"])))
}
b.WriteString("</table>")
case "general_ledger":
b.WriteString("<h2>General Ledger</h2>")
b.WriteString(`<table><tr><th>Account</th><th>Move</th><th>Date</th><th>Label</th><th>Debit</th><th>Credit</th></tr>`)
for _, l := range lines {
b.WriteString(fmt.Sprintf("<tr><td>%v %v</td><td>%v</td><td>%v</td><td>%v</td><td>%.2f</td><td>%.2f</td></tr>",
l["account_code"], l["account_name"], l["move"], l["date"], l["label"],
toF(l["debit"]), toF(l["credit"])))
}
b.WriteString("</table>")
}
b.WriteString("</body></html>")
return b.String(), nil
}
// toF converts various numeric types to float64 for formatting.
func toF(v interface{}) float64 {
switch n := v.(type) {
case float64:
return n
case int64:
return float64(n)
case int:
return float64(n)
case int32:
return float64(n)
}
return 0
}