Adds a TransientModel wizard fusion.xlsx.export.wizard that lets users pick a report type, date range, and comparison mode, then runs the engine and produces an XLSX via xlsxwriter (in-memory). The wizard exposes a download field that becomes available after export finishes. Works on P&L, Balance Sheet, Trial Balance, and General Ledger. Comparison columns are written when the engine returns a comparison_period in the result. Also wires the controller's /fusion/reports/export_xlsx endpoint to drive the wizard and return base64-encoded XLSX bytes (replaces the not_implemented placeholder). Tests: 2 new (test_xlsx_export.py) + 1 controller test updated. Manifest declares xlsxwriter as an external_dependency. Made-with: Cursor
106 lines
4.3 KiB
Python
106 lines
4.3 KiB
Python
"""XLSX export wizard for fusion financial reports."""
|
|
|
|
import base64
|
|
import io
|
|
|
|
from odoo import _, fields, models
|
|
from odoo.exceptions import UserError
|
|
|
|
from ..services.date_periods import Period
|
|
|
|
|
|
class FusionXlsxExportWizard(models.TransientModel):
|
|
_name = "fusion.xlsx.export.wizard"
|
|
_description = "Export Financial Report to XLSX"
|
|
|
|
report_type = fields.Selection([
|
|
('pnl', 'P&L'),
|
|
('balance_sheet', 'Balance Sheet'),
|
|
('trial_balance', 'Trial Balance'),
|
|
('general_ledger', 'General Ledger'),
|
|
], required=True, default='pnl')
|
|
date_from = fields.Date(required=True, default=fields.Date.today)
|
|
date_to = fields.Date(required=True, default=fields.Date.today)
|
|
comparison = fields.Selection([
|
|
('none', 'No Comparison'),
|
|
('previous_period', 'Previous Period'),
|
|
('previous_year', 'Previous Year'),
|
|
], default='none')
|
|
|
|
xlsx_file = fields.Binary(readonly=True)
|
|
xlsx_filename = fields.Char(readonly=True)
|
|
state = fields.Selection([('draft', 'Draft'), ('done', 'Done')], default='draft')
|
|
|
|
def action_export(self):
|
|
self.ensure_one()
|
|
company_id = self.env.company.id
|
|
engine = self.env['fusion.report.engine']
|
|
if self.report_type == 'pnl':
|
|
period = Period(self.date_from, self.date_to, f"{self.date_from} - {self.date_to}")
|
|
result = engine.compute_pnl(period, comparison=self.comparison, company_id=company_id)
|
|
elif self.report_type == 'balance_sheet':
|
|
result = engine.compute_balance_sheet(self.date_to, comparison=self.comparison, company_id=company_id)
|
|
elif self.report_type == 'trial_balance':
|
|
period = Period(self.date_from, self.date_to, f"{self.date_from} - {self.date_to}")
|
|
result = engine.compute_trial_balance(period, company_id=company_id)
|
|
else:
|
|
period = Period(self.date_from, self.date_to, f"{self.date_from} - {self.date_to}")
|
|
result = engine.compute_gl(period, company_id=company_id)
|
|
|
|
try:
|
|
import xlsxwriter
|
|
except ImportError:
|
|
raise UserError(_(
|
|
"xlsxwriter Python package is required for XLSX export. "
|
|
"Install with: pip install xlsxwriter"))
|
|
|
|
buf = io.BytesIO()
|
|
wb = xlsxwriter.Workbook(buf, {'in_memory': True})
|
|
ws = wb.add_worksheet(self.report_type[:30])
|
|
bold = wb.add_format({'bold': True})
|
|
money = wb.add_format({'num_format': '#,##0.00'})
|
|
money_bold = wb.add_format({'num_format': '#,##0.00', 'bold': True})
|
|
|
|
ws.write(0, 0, result.get('report_name', 'Report'), bold)
|
|
ws.write(1, 0, f"Period: {result.get('period', {}).get('label', '')}")
|
|
if result.get('comparison_period'):
|
|
ws.write(2, 0, f"Comparison: {result['comparison_period']['label']}")
|
|
|
|
row_idx = 4
|
|
ws.write(row_idx, 0, 'Line', bold)
|
|
ws.write(row_idx, 1, 'Amount', bold)
|
|
if result.get('comparison_period'):
|
|
ws.write(row_idx, 2, 'Comparison', bold)
|
|
ws.write(row_idx, 3, 'Variance %', bold)
|
|
|
|
for row in result.get('rows', []):
|
|
row_idx += 1
|
|
label = (' ' * (row.get('level', 0) or 0)) + (row.get('label', '') or '')
|
|
fmt = bold if row.get('is_subtotal') else None
|
|
money_fmt = money_bold if row.get('is_subtotal') else money
|
|
ws.write(row_idx, 0, label, fmt)
|
|
ws.write(row_idx, 1, row.get('amount', 0), money_fmt)
|
|
if result.get('comparison_period'):
|
|
if row.get('amount_comparison') is not None:
|
|
ws.write(row_idx, 2, row['amount_comparison'], money_fmt)
|
|
if row.get('variance_pct') is not None:
|
|
ws.write(row_idx, 3, row['variance_pct'] / 100,
|
|
wb.add_format({'num_format': '+0.0%;-0.0%;0.0%'}))
|
|
|
|
ws.set_column(0, 0, 40)
|
|
ws.set_column(1, 3, 16)
|
|
wb.close()
|
|
|
|
self.write({
|
|
'xlsx_file': base64.b64encode(buf.getvalue()),
|
|
'xlsx_filename': f'{self.report_type}_{self.date_from}_{self.date_to}.xlsx',
|
|
'state': 'done',
|
|
})
|
|
return {
|
|
'type': 'ir.actions.act_window',
|
|
'res_model': self._name,
|
|
'res_id': self.id,
|
|
'view_mode': 'form',
|
|
'target': 'new',
|
|
}
|