feat(fusion_accounting_reports): PDF export with QWeb template

Adds an AbstractModel report (report_pdf.py) and a single multi-purpose
QWeb template (report_pdf_template.xml) that renders P&L, Balance Sheet,
Trial Balance, and General Ledger results from the engine.

Wires the controller's /fusion/reports/export_pdf endpoint to actually
return base64-encoded PDF bytes via _render_qweb_pdf. The template walks
the result['rows'] list and applies indentation/bold based on level and
is_subtotal flags, with optional comparison columns when present.

Tests: 2 new (test_pdf_export.py) + 1 controller test updated to assert
the real PDF response. Net 109 -> 111.

Made-with: Cursor
This commit is contained in:
gsinghpal
2026-04-19 16:13:22 -04:00
parent d1661f3a33
commit 23b988c401
9 changed files with 191 additions and 5 deletions

View File

@@ -0,0 +1 @@
from . import report_pdf

View File

@@ -0,0 +1,58 @@
"""QWeb PDF report for fusion financial reports.
Wraps the engine's compute_* methods and feeds the result into a
single multi-purpose template that handles all 4 report types."""
from datetime import datetime
from odoo import api, models
from ..services.date_periods import Period
class FusionReportPdf(models.AbstractModel):
_name = "report.fusion_accounting_reports.report_pdf_template"
_description = "Fusion Financial Report PDF"
@api.model
def _get_report_values(self, docids, data=None):
"""data is expected to be {report_type, date_from, date_to, comparison, company_id}."""
data = data or {}
report_type = data.get('report_type', 'pnl')
company_id = data.get('company_id') or self.env.company.id
date_from = data.get('date_from')
date_to = data.get('date_to')
comparison = data.get('comparison', 'none')
if isinstance(date_from, str):
date_from = datetime.strptime(date_from, '%Y-%m-%d').date()
if isinstance(date_to, str):
date_to = datetime.strptime(date_to, '%Y-%m-%d').date()
engine = self.env['fusion.report.engine']
if report_type == 'pnl':
period = Period(date_from, date_to, f"{date_from} - {date_to}")
result = engine.compute_pnl(period, comparison=comparison, company_id=company_id)
elif report_type == 'balance_sheet':
result = engine.compute_balance_sheet(date_to, comparison=comparison, company_id=company_id)
elif report_type == 'trial_balance':
period = Period(date_from, date_to, f"{date_from} - {date_to}")
result = engine.compute_trial_balance(period, company_id=company_id)
elif report_type == 'general_ledger':
period = Period(date_from, date_to, f"{date_from} - {date_to}")
result = engine.compute_gl(period, company_id=company_id)
else:
result = {'rows': [], 'report_name': 'Unknown', 'period': {}}
company = self.env['res.company'].browse(company_id)
return {
'doc_ids': docids,
'doc_model': 'fusion.report',
'docs': self.env['fusion.report'].browse(docids) if docids else
self.env['fusion.report'].search([('report_type', '=', report_type)], limit=1),
'data': data,
'result': result,
'company_id': company,
'company': company,
'res_company': company,
}

View File

@@ -0,0 +1,72 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<template id="report_pdf_template">
<t t-call="web.html_container">
<t t-call="web.external_layout">
<div class="page">
<h2>
<t t-esc="result.get('report_name', 'Financial Report')"/>
</h2>
<p>
<strong>Period:</strong>
<span t-esc="result.get('period', {}).get('label', '')"/>
</p>
<p t-if="result.get('comparison_period')">
<strong>Compared to:</strong>
<span t-esc="result.get('comparison_period', {}).get('label', '')"/>
</p>
<table class="table table-sm">
<thead>
<tr>
<th>Line</th>
<th class="text-end">Amount</th>
<t t-if="result.get('comparison_period')">
<th class="text-end">Comparison</th>
<th class="text-end">Variance %</th>
</t>
</tr>
</thead>
<tbody>
<tr t-foreach="result.get('rows', [])" t-as="row"
t-attf-style="{{ 'font-weight: bold;' if row.get('is_subtotal') else '' }}">
<td t-attf-style="padding-left: {{ (row.get('level', 0) or 0) * 16 + 8 }}px;">
<span t-esc="row.get('label', '')"/>
</td>
<td class="text-end">
<span t-esc="'{:,.2f}'.format(row.get('amount', 0))"/>
</td>
<t t-if="result.get('comparison_period')">
<td class="text-end">
<t t-if="row.get('amount_comparison') is not None">
<span t-esc="'{:,.2f}'.format(row.get('amount_comparison'))"/>
</t>
</td>
<td class="text-end">
<t t-if="row.get('variance_pct') is not None">
<span t-esc="'{:+.1f}%'.format(row.get('variance_pct'))"/>
</t>
</td>
</t>
</tr>
</tbody>
</table>
<p class="text-muted" style="font-size: 0.75rem;">
Generated by Fusion Accounting Reports
</p>
</div>
</t>
</t>
</template>
<record id="action_report_fusion_financial" model="ir.actions.report">
<field name="name">Fusion Financial Report (PDF)</field>
<field name="model">fusion.report</field>
<field name="report_type">qweb-pdf</field>
<field name="report_name">fusion_accounting_reports.report_pdf_template</field>
<field name="report_file">fusion_accounting_reports.report_pdf_template</field>
<field name="binding_model_id" ref="model_fusion_report"/>
<field name="binding_view_types">form,list</field>
</record>
</odoo>