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>
130 lines
4.3 KiB
Go
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 & 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
|
|
}
|