changes
This commit is contained in:
@@ -0,0 +1,2 @@
|
||||
from . import xlsx_export_wizard
|
||||
from . import period_picker_wizard
|
||||
@@ -0,0 +1,77 @@
|
||||
"""Period selection + comparison wizard.
|
||||
|
||||
Pre-fills date ranges for common report periods (current month, YTD, etc.)."""
|
||||
|
||||
from datetime import timedelta
|
||||
|
||||
from odoo import api, fields, models
|
||||
|
||||
from ..services.date_periods import (
|
||||
fiscal_year_bounds, month_bounds, quarter_bounds,
|
||||
)
|
||||
|
||||
|
||||
class FusionPeriodPickerWizard(models.TransientModel):
|
||||
_name = "fusion.period.picker.wizard"
|
||||
_description = "Period Selection Wizard"
|
||||
|
||||
report_type = fields.Selection([
|
||||
('pnl', 'P&L'),
|
||||
('balance_sheet', 'Balance Sheet'),
|
||||
('trial_balance', 'Trial Balance'),
|
||||
('general_ledger', 'General Ledger'),
|
||||
], required=True, default='pnl')
|
||||
period_preset = fields.Selection([
|
||||
('this_month', 'This Month'),
|
||||
('last_month', 'Last Month'),
|
||||
('this_quarter', 'This Quarter'),
|
||||
('last_quarter', 'Last Quarter'),
|
||||
('this_year', 'This Year (YTD)'),
|
||||
('last_year', 'Last Year'),
|
||||
('custom', 'Custom Range'),
|
||||
], default='this_month', required=True)
|
||||
date_from = fields.Date()
|
||||
date_to = fields.Date()
|
||||
comparison = fields.Selection([
|
||||
('none', 'No Comparison'),
|
||||
('previous_period', 'Previous Period'),
|
||||
('previous_year', 'Previous Year'),
|
||||
], default='none')
|
||||
|
||||
@api.onchange('period_preset')
|
||||
def _onchange_period_preset(self):
|
||||
today = fields.Date.today()
|
||||
if self.period_preset == 'this_month':
|
||||
p = month_bounds(today)
|
||||
self.date_from, self.date_to = p.date_from, p.date_to
|
||||
elif self.period_preset == 'last_month':
|
||||
p = month_bounds(today.replace(day=1) - timedelta(days=1))
|
||||
self.date_from, self.date_to = p.date_from, p.date_to
|
||||
elif self.period_preset == 'this_quarter':
|
||||
p = quarter_bounds(today)
|
||||
self.date_from, self.date_to = p.date_from, p.date_to
|
||||
elif self.period_preset == 'last_quarter':
|
||||
this_q = quarter_bounds(today)
|
||||
p = quarter_bounds(this_q.date_from - timedelta(days=1))
|
||||
self.date_from, self.date_to = p.date_from, p.date_to
|
||||
elif self.period_preset == 'this_year':
|
||||
p = fiscal_year_bounds(today)
|
||||
self.date_from, self.date_to = p.date_from, today
|
||||
elif self.period_preset == 'last_year':
|
||||
last_year = today.replace(year=today.year - 1)
|
||||
p = fiscal_year_bounds(last_year)
|
||||
self.date_from, self.date_to = p.date_from, p.date_to
|
||||
|
||||
def action_open_report(self):
|
||||
"""Open the fusion reports viewer pre-filled with selected period."""
|
||||
self.ensure_one()
|
||||
return {
|
||||
'type': 'ir.actions.client',
|
||||
'tag': 'fusion_reports',
|
||||
'context': {
|
||||
'default_report_type': self.report_type,
|
||||
'default_date_from': str(self.date_from),
|
||||
'default_date_to': str(self.date_to),
|
||||
'default_comparison': self.comparison,
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<record id="view_fusion_period_picker_wizard_form" model="ir.ui.view">
|
||||
<field name="name">fusion.period.picker.wizard.form</field>
|
||||
<field name="model">fusion.period.picker.wizard</field>
|
||||
<field name="arch" type="xml">
|
||||
<form string="Pick Reporting Period">
|
||||
<group>
|
||||
<field name="report_type"/>
|
||||
<field name="period_preset"/>
|
||||
<field name="date_from" invisible="period_preset != 'custom'"
|
||||
required="period_preset == 'custom'"/>
|
||||
<field name="date_to" invisible="period_preset != 'custom'"
|
||||
required="period_preset == 'custom'"/>
|
||||
<field name="comparison"/>
|
||||
</group>
|
||||
<footer>
|
||||
<button name="action_open_report" type="object" string="Open Report"
|
||||
class="btn-primary"/>
|
||||
<button special="cancel" string="Cancel"/>
|
||||
</footer>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="action_fusion_period_picker_wizard" model="ir.actions.act_window">
|
||||
<field name="name">Open Financial Report</field>
|
||||
<field name="res_model">fusion.period.picker.wizard</field>
|
||||
<field name="view_mode">form</field>
|
||||
<field name="target">new</field>
|
||||
</record>
|
||||
</odoo>
|
||||
@@ -0,0 +1,105 @@
|
||||
"""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',
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<record id="view_fusion_xlsx_export_wizard_form" model="ir.ui.view">
|
||||
<field name="name">fusion.xlsx.export.wizard.form</field>
|
||||
<field name="model">fusion.xlsx.export.wizard</field>
|
||||
<field name="arch" type="xml">
|
||||
<form string="Export to XLSX">
|
||||
<group invisible="state == 'done'">
|
||||
<field name="report_type"/>
|
||||
<field name="date_from"/>
|
||||
<field name="date_to"/>
|
||||
<field name="comparison"/>
|
||||
</group>
|
||||
<group invisible="state != 'done'">
|
||||
<field name="xlsx_file" filename="xlsx_filename" readonly="1"/>
|
||||
<field name="xlsx_filename" invisible="1"/>
|
||||
</group>
|
||||
<field name="state" invisible="1"/>
|
||||
<footer>
|
||||
<button name="action_export" type="object" string="Export"
|
||||
class="btn-primary" invisible="state == 'done'"/>
|
||||
<button special="cancel" string="Close"/>
|
||||
</footer>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="action_fusion_xlsx_export_wizard" model="ir.actions.act_window">
|
||||
<field name="name">Export Report (XLSX)</field>
|
||||
<field name="res_model">fusion.xlsx.export.wizard</field>
|
||||
<field name="view_mode">form</field>
|
||||
<field name="target">new</field>
|
||||
</record>
|
||||
</odoo>
|
||||
Reference in New Issue
Block a user