Compare commits
5 Commits
111792599c
...
fusion_acc
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d36933d7f4 | ||
|
|
1817f63c67 | ||
|
|
1ebff01d35 | ||
|
|
ff6d21a561 | ||
|
|
6896c71b79 |
@@ -36,6 +36,13 @@ menu hides; the engine and AI tools remain available for the chat.
|
||||
'data/report_balance_sheet.xml',
|
||||
'data/report_trial_balance.xml',
|
||||
'data/report_general_ledger.xml',
|
||||
'data/report_cash_flow.xml',
|
||||
'data/report_executive_summary.xml',
|
||||
'data/report_tax_report.xml',
|
||||
'data/report_annual_statements.xml',
|
||||
'data/report_aged_receivable.xml',
|
||||
'data/report_aged_payable.xml',
|
||||
'data/report_partner_ledger.xml',
|
||||
'data/cron.xml',
|
||||
'reports/report_pdf_template.xml',
|
||||
'wizards/xlsx_export_wizard_views.xml',
|
||||
|
||||
@@ -18,7 +18,16 @@ from ..services.date_periods import Period
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
REPORT_TYPES = {'pnl', 'balance_sheet', 'trial_balance', 'general_ledger'}
|
||||
REPORT_TYPES = {
|
||||
'pnl', 'balance_sheet', 'trial_balance', 'general_ledger',
|
||||
'aged_receivable', 'aged_payable', 'partner_ledger',
|
||||
}
|
||||
|
||||
PARTNER_GROUPED_ACCOUNT_TYPE = {
|
||||
'aged_receivable': 'asset_receivable',
|
||||
'aged_payable': 'liability_payable',
|
||||
'partner_ledger': 'asset_receivable',
|
||||
}
|
||||
|
||||
|
||||
def _parse_date(value):
|
||||
@@ -56,7 +65,7 @@ class FusionReportsController(http.Controller):
|
||||
|
||||
@http.route('/fusion/reports/run', type='jsonrpc', auth='user')
|
||||
def run(self, report_type, date_from=None, date_to=None,
|
||||
comparison='none', company_id=None):
|
||||
comparison='none', company_id=None, report_code=None):
|
||||
if report_type not in REPORT_TYPES:
|
||||
raise ValidationError(_("Unknown report type: %s") % report_type)
|
||||
company_id = int(company_id) if company_id else request.env.company.id
|
||||
@@ -66,19 +75,33 @@ class FusionReportsController(http.Controller):
|
||||
period = _build_period(date_from, date_to)
|
||||
return engine.compute_pnl(
|
||||
period, comparison=comparison, company_id=company_id,
|
||||
report_code=report_code,
|
||||
)
|
||||
if report_type == 'balance_sheet':
|
||||
return engine.compute_balance_sheet(
|
||||
_parse_date(date_to),
|
||||
comparison=comparison,
|
||||
company_id=company_id,
|
||||
report_code=report_code,
|
||||
)
|
||||
if report_type == 'trial_balance':
|
||||
period = _build_period(date_from, date_to)
|
||||
return engine.compute_trial_balance(period, company_id=company_id)
|
||||
return engine.compute_trial_balance(
|
||||
period, company_id=company_id, report_code=report_code,
|
||||
)
|
||||
if report_type in PARTNER_GROUPED_ACCOUNT_TYPE:
|
||||
period = _build_period(date_from, date_to)
|
||||
return engine.compute_partner_grouped(
|
||||
period,
|
||||
account_type=PARTNER_GROUPED_ACCOUNT_TYPE[report_type],
|
||||
comparison=comparison,
|
||||
company_id=company_id,
|
||||
)
|
||||
# general_ledger
|
||||
period = _build_period(date_from, date_to)
|
||||
return engine.compute_gl(period, company_id=company_id)
|
||||
return engine.compute_gl(
|
||||
period, company_id=company_id, report_code=report_code,
|
||||
)
|
||||
|
||||
@http.route('/fusion/reports/drill_down', type='jsonrpc', auth='user')
|
||||
def drill_down(self, account_id, date_from, date_to, company_id=None):
|
||||
|
||||
14
fusion_accounting_reports/data/report_aged_payable.xml
Normal file
14
fusion_accounting_reports/data/report_aged_payable.xml
Normal file
@@ -0,0 +1,14 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo noupdate="1">
|
||||
<record id="report_aged_payable" model="fusion.report">
|
||||
<field name="name">Aged Payable</field>
|
||||
<field name="code">aged_payable</field>
|
||||
<field name="report_type">aged_payable</field>
|
||||
<field name="sequence">36</field>
|
||||
<field name="description">Per-vendor outstanding payables, bucketed by aging.</field>
|
||||
<field name="line_specs" eval="[
|
||||
{'label': 'Aged Payable', 'account_type_for_grouping': 'liability_payable'}
|
||||
]"/>
|
||||
<field name="company_id" eval="False"/>
|
||||
</record>
|
||||
</odoo>
|
||||
14
fusion_accounting_reports/data/report_aged_receivable.xml
Normal file
14
fusion_accounting_reports/data/report_aged_receivable.xml
Normal file
@@ -0,0 +1,14 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo noupdate="1">
|
||||
<record id="report_aged_receivable" model="fusion.report">
|
||||
<field name="name">Aged Receivable</field>
|
||||
<field name="code">aged_receivable</field>
|
||||
<field name="report_type">aged_receivable</field>
|
||||
<field name="sequence">35</field>
|
||||
<field name="description">Per-customer outstanding receivables, bucketed by aging.</field>
|
||||
<field name="line_specs" eval="[
|
||||
{'label': 'Aged Receivable', 'account_type_for_grouping': 'asset_receivable'}
|
||||
]"/>
|
||||
<field name="company_id" eval="False"/>
|
||||
</record>
|
||||
</odoo>
|
||||
19
fusion_accounting_reports/data/report_annual_statements.xml
Normal file
19
fusion_accounting_reports/data/report_annual_statements.xml
Normal file
@@ -0,0 +1,19 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo noupdate="1">
|
||||
<record id="report_annual_statements" model="fusion.report">
|
||||
<field name="name">Annual Statements</field>
|
||||
<field name="code">annual_statements</field>
|
||||
<field name="report_type">pnl</field>
|
||||
<field name="sequence">11</field>
|
||||
<field name="default_comparison_mode">previous_year</field>
|
||||
<field name="description">Year-over-year P&L comparison for annual reporting.</field>
|
||||
<field name="line_specs" eval="[
|
||||
{'label': 'Revenue', 'account_type_prefix': 'income', 'sign': -1, 'level': 0},
|
||||
{'label': 'Cost of Goods Sold', 'account_type_prefix': 'expense_direct_cost', 'sign': -1, 'level': 1},
|
||||
{'label': 'Gross Profit', 'compute': 'subtotal', 'above': 2, 'sign': 1, 'level': 0},
|
||||
{'label': 'Operating Expenses', 'account_type_prefix': 'expense', 'sign': -1, 'level': 1},
|
||||
{'label': 'OPERATING INCOME', 'compute': 'subtotal', 'above': 2, 'sign': 1, 'level': 0}
|
||||
]"/>
|
||||
<field name="company_id" eval="False"/>
|
||||
</record>
|
||||
</odoo>
|
||||
29
fusion_accounting_reports/data/report_cash_flow.xml
Normal file
29
fusion_accounting_reports/data/report_cash_flow.xml
Normal file
@@ -0,0 +1,29 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo noupdate="1">
|
||||
<record id="report_cash_flow" model="fusion.report">
|
||||
<field name="name">Cash Flow Statement</field>
|
||||
<field name="code">cash_flow</field>
|
||||
<field name="report_type">pnl</field>
|
||||
<field name="sequence">15</field>
|
||||
<field name="default_comparison_mode">previous_year</field>
|
||||
<field name="description">Cash flow by activity (operating, investing, financing).</field>
|
||||
<field name="line_specs" eval="[
|
||||
{'label': 'Operating Activities', 'level': 0},
|
||||
{'label': 'Net Income (from operations)', 'account_type_prefix': 'income', 'sign': -1, 'level': 1},
|
||||
{'label': 'Depreciation Add-back', 'account_type_prefix': 'expense_depreciation', 'sign': 1, 'level': 1},
|
||||
{'label': 'Operating Cash Flow', 'compute': 'subtotal', 'above': 2, 'sign': 1, 'level': 0},
|
||||
|
||||
{'label': 'Investing Activities', 'level': 0},
|
||||
{'label': 'Fixed Asset Purchases', 'account_type_prefix': 'asset_fixed', 'sign': -1, 'level': 1},
|
||||
{'label': 'Investing Cash Flow', 'compute': 'subtotal', 'above': 1, 'sign': 1, 'level': 0},
|
||||
|
||||
{'label': 'Financing Activities', 'level': 0},
|
||||
{'label': 'Liabilities (long-term)', 'account_type_prefix': 'liability_non_current', 'sign': 1, 'level': 1},
|
||||
{'label': 'Equity', 'account_type_prefix': 'equity', 'sign': 1, 'level': 1},
|
||||
{'label': 'Financing Cash Flow', 'compute': 'subtotal', 'above': 2, 'sign': 1, 'level': 0},
|
||||
|
||||
{'label': 'NET CHANGE IN CASH', 'compute': 'subtotal', 'above': 3, 'sign': 1, 'level': 0}
|
||||
]"/>
|
||||
<field name="company_id" eval="False"/>
|
||||
</record>
|
||||
</odoo>
|
||||
24
fusion_accounting_reports/data/report_executive_summary.xml
Normal file
24
fusion_accounting_reports/data/report_executive_summary.xml
Normal file
@@ -0,0 +1,24 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo noupdate="1">
|
||||
<record id="report_executive_summary" model="fusion.report">
|
||||
<field name="name">Executive Summary</field>
|
||||
<field name="code">executive_summary</field>
|
||||
<field name="report_type">pnl</field>
|
||||
<field name="sequence">5</field>
|
||||
<field name="default_comparison_mode">previous_year</field>
|
||||
<field name="description">Top-level KPI summary: revenue, expenses, net income, key balance positions.</field>
|
||||
<field name="line_specs" eval="[
|
||||
{'label': 'PROFIT & LOSS', 'level': 0},
|
||||
{'label': 'Revenue', 'account_type_prefix': 'income', 'sign': -1, 'level': 1},
|
||||
{'label': 'Expenses', 'account_type_prefix': 'expense', 'sign': -1, 'level': 1},
|
||||
{'label': 'Net Income', 'compute': 'subtotal', 'above': 2, 'sign': 1, 'level': 0},
|
||||
|
||||
{'label': 'BALANCE POSITIONS', 'level': 0},
|
||||
{'label': 'Cash & Bank', 'account_type_prefix': 'asset_cash', 'sign': 1, 'level': 1},
|
||||
{'label': 'Receivables', 'account_type_prefix': 'asset_receivable', 'sign': 1, 'level': 1},
|
||||
{'label': 'Payables', 'account_type_prefix': 'liability_payable', 'sign': -1, 'level': 1},
|
||||
{'label': 'Net Working Position', 'compute': 'subtotal', 'above': 3, 'sign': 1, 'level': 0}
|
||||
]"/>
|
||||
<field name="company_id" eval="False"/>
|
||||
</record>
|
||||
</odoo>
|
||||
14
fusion_accounting_reports/data/report_partner_ledger.xml
Normal file
14
fusion_accounting_reports/data/report_partner_ledger.xml
Normal file
@@ -0,0 +1,14 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo noupdate="1">
|
||||
<record id="report_partner_ledger" model="fusion.report">
|
||||
<field name="name">Partner Ledger</field>
|
||||
<field name="code">partner_ledger</field>
|
||||
<field name="report_type">partner_ledger</field>
|
||||
<field name="sequence">40</field>
|
||||
<field name="description">Per-partner ledger combining receivable and payable activity.</field>
|
||||
<field name="line_specs" eval="[
|
||||
{'label': 'Partner Ledger', 'account_type_for_grouping': 'asset_receivable'}
|
||||
]"/>
|
||||
<field name="company_id" eval="False"/>
|
||||
</record>
|
||||
</odoo>
|
||||
16
fusion_accounting_reports/data/report_tax_report.xml
Normal file
16
fusion_accounting_reports/data/report_tax_report.xml
Normal file
@@ -0,0 +1,16 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo noupdate="1">
|
||||
<record id="report_tax_summary" model="fusion.report">
|
||||
<field name="name">Tax Summary</field>
|
||||
<field name="code">tax_summary</field>
|
||||
<field name="report_type">trial_balance</field>
|
||||
<field name="sequence">25</field>
|
||||
<field name="description">Tax liability + asset positions. v1: aggregate-level only; per-tax-code breakdown is Phase 2.5.</field>
|
||||
<field name="line_specs" eval="[
|
||||
{'label': 'Tax Asset (recoverable)', 'account_type_prefix': 'asset_current', 'sign': 1, 'level': 0},
|
||||
{'label': 'Tax Liability (collected)', 'account_type_prefix': 'liability_current', 'sign': -1, 'level': 0},
|
||||
{'label': 'NET TAX POSITION', 'compute': 'subtotal', 'above': 2, 'sign': 1, 'level': 0}
|
||||
]"/>
|
||||
<field name="company_id" eval="False"/>
|
||||
</record>
|
||||
</odoo>
|
||||
@@ -13,6 +13,9 @@ REPORT_TYPES = [
|
||||
('balance_sheet', 'Balance Sheet'),
|
||||
('trial_balance', 'Trial Balance'),
|
||||
('general_ledger', 'General Ledger'),
|
||||
('aged_receivable', 'Aged Receivable'),
|
||||
('aged_payable', 'Aged Payable'),
|
||||
('partner_ledger', 'Partner Ledger'),
|
||||
]
|
||||
|
||||
|
||||
|
||||
@@ -14,7 +14,7 @@ Internal pipeline (per report run):
|
||||
"""
|
||||
|
||||
import logging
|
||||
from datetime import date
|
||||
from datetime import date, timedelta
|
||||
|
||||
from odoo import _, api, models
|
||||
from odoo.exceptions import ValidationError
|
||||
@@ -39,10 +39,17 @@ class FusionReportEngine(models.AbstractModel):
|
||||
@api.model
|
||||
def compute_pnl(
|
||||
self, period: Period, *, comparison: str = 'none',
|
||||
company_id: int | None = None,
|
||||
company_id: int | None = None, report_code: str | None = None,
|
||||
) -> dict:
|
||||
"""Income statement (P&L) for the given period."""
|
||||
report = self._get_report('pnl', company_id=company_id)
|
||||
"""Income statement (P&L) for the given period.
|
||||
|
||||
``report_code`` selects between multiple PnL-typed report definitions
|
||||
(``pnl``, ``cash_flow``, ``executive_summary``, ``annual_statements``).
|
||||
When omitted, falls back to the canonical ``pnl`` definition.
|
||||
"""
|
||||
report = self._get_report(
|
||||
'pnl', company_id=company_id, code=report_code,
|
||||
)
|
||||
return self._compute(
|
||||
report, period, comparison=comparison, company_id=company_id,
|
||||
)
|
||||
@@ -50,11 +57,13 @@ class FusionReportEngine(models.AbstractModel):
|
||||
@api.model
|
||||
def compute_balance_sheet(
|
||||
self, date_to: date, *, comparison: str = 'none',
|
||||
company_id: int | None = None,
|
||||
company_id: int | None = None, report_code: str | None = None,
|
||||
) -> dict:
|
||||
"""Balance sheet AS OF date_to. Period.date_from is set to a
|
||||
far-past date so balances are cumulative-since-inception."""
|
||||
report = self._get_report('balance_sheet', company_id=company_id)
|
||||
report = self._get_report(
|
||||
'balance_sheet', company_id=company_id, code=report_code,
|
||||
)
|
||||
period = Period(
|
||||
date_from=date(1970, 1, 1),
|
||||
date_to=date_to,
|
||||
@@ -67,10 +76,17 @@ class FusionReportEngine(models.AbstractModel):
|
||||
@api.model
|
||||
def compute_trial_balance(
|
||||
self, period: Period, *, company_id: int | None = None,
|
||||
report_code: str | None = None,
|
||||
) -> dict:
|
||||
"""Trial balance for the given period - every account with
|
||||
non-zero balance."""
|
||||
report = self._get_report('trial_balance', company_id=company_id)
|
||||
non-zero balance.
|
||||
|
||||
``report_code`` selects between multiple TB-typed reports (e.g.
|
||||
``trial_balance``, ``tax_summary``).
|
||||
"""
|
||||
report = self._get_report(
|
||||
'trial_balance', company_id=company_id, code=report_code,
|
||||
)
|
||||
return self._compute(
|
||||
report, period, comparison='none', company_id=company_id,
|
||||
)
|
||||
@@ -78,12 +94,14 @@ class FusionReportEngine(models.AbstractModel):
|
||||
@api.model
|
||||
def compute_gl(
|
||||
self, period: Period, *, account_ids: list | None = None,
|
||||
company_id: int | None = None,
|
||||
company_id: int | None = None, report_code: str | None = None,
|
||||
) -> dict:
|
||||
"""General ledger for the given period.
|
||||
|
||||
Returns per-account move-line listings rather than aggregated rows."""
|
||||
report = self._get_report('general_ledger', company_id=company_id)
|
||||
report = self._get_report(
|
||||
'general_ledger', company_id=company_id, code=report_code,
|
||||
)
|
||||
company_id = company_id or self.env.company.id
|
||||
result = self._compute(
|
||||
report, period, comparison='none', company_id=company_id,
|
||||
@@ -118,27 +136,188 @@ class FusionReportEngine(models.AbstractModel):
|
||||
limit=500,
|
||||
)
|
||||
|
||||
@api.model
|
||||
def compute_partner_grouped(
|
||||
self, period: Period, *, account_type: str = 'asset_receivable',
|
||||
comparison: str = 'none', company_id: int | None = None,
|
||||
) -> dict:
|
||||
"""Per-partner aggregation report (Aged Receivable, Aged Payable,
|
||||
Partner Ledger).
|
||||
|
||||
Returns a dict with ``rows`` = list of partner-level aggregates.
|
||||
Each row has the partner_id, partner_name, total residual, and
|
||||
aging buckets: current / 1-30 / 31-60 / 61-90 / 90+ days past
|
||||
``period.date_to``.
|
||||
|
||||
SQL-direct for performance: a single GROUP BY query with conditional
|
||||
sum per bucket. Only un-reconciled, posted lines with non-zero
|
||||
residual at the as-of date are included.
|
||||
"""
|
||||
company_id = company_id or self.env.company.id
|
||||
|
||||
accounts = self.env['account.account'].sudo().search([
|
||||
('account_type', '=', account_type),
|
||||
('company_ids', 'in', company_id),
|
||||
])
|
||||
if not accounts:
|
||||
return {
|
||||
'report_type': 'partner_grouped',
|
||||
'account_type': account_type,
|
||||
'period': {
|
||||
'date_from': str(period.date_from),
|
||||
'date_to': str(period.date_to),
|
||||
'label': period.label,
|
||||
},
|
||||
'rows': [],
|
||||
'total': 0.0,
|
||||
'partner_count': 0,
|
||||
}
|
||||
|
||||
as_of = period.date_to
|
||||
d30 = as_of - timedelta(days=30)
|
||||
d60 = as_of - timedelta(days=60)
|
||||
d90 = as_of - timedelta(days=90)
|
||||
|
||||
self.env.cr.execute(
|
||||
"""
|
||||
SELECT
|
||||
COALESCE(p.id, 0) AS partner_id,
|
||||
COALESCE(p.name, '(no partner)') AS partner_name,
|
||||
SUM(aml.amount_residual) AS total_residual,
|
||||
SUM(CASE
|
||||
WHEN aml.date_maturity >= %s
|
||||
OR aml.date_maturity IS NULL
|
||||
THEN aml.amount_residual ELSE 0
|
||||
END) AS bucket_current,
|
||||
SUM(CASE
|
||||
WHEN aml.date_maturity < %s
|
||||
AND aml.date_maturity >= %s
|
||||
THEN aml.amount_residual ELSE 0
|
||||
END) AS bucket_1_30,
|
||||
SUM(CASE
|
||||
WHEN aml.date_maturity < %s
|
||||
AND aml.date_maturity >= %s
|
||||
THEN aml.amount_residual ELSE 0
|
||||
END) AS bucket_31_60,
|
||||
SUM(CASE
|
||||
WHEN aml.date_maturity < %s
|
||||
AND aml.date_maturity >= %s
|
||||
THEN aml.amount_residual ELSE 0
|
||||
END) AS bucket_61_90,
|
||||
SUM(CASE
|
||||
WHEN aml.date_maturity < %s
|
||||
THEN aml.amount_residual ELSE 0
|
||||
END) AS bucket_90_plus,
|
||||
COUNT(*) AS line_count
|
||||
FROM account_move_line aml
|
||||
LEFT JOIN res_partner p ON p.id = aml.partner_id
|
||||
WHERE aml.account_id = ANY(%s)
|
||||
AND aml.parent_state = 'posted'
|
||||
AND aml.reconciled = false
|
||||
AND aml.amount_residual != 0
|
||||
AND aml.company_id = %s
|
||||
AND aml.date <= %s
|
||||
GROUP BY p.id, p.name
|
||||
HAVING SUM(aml.amount_residual) != 0
|
||||
ORDER BY total_residual DESC
|
||||
""",
|
||||
(
|
||||
as_of,
|
||||
as_of, d30,
|
||||
d30, d60,
|
||||
d60, d90,
|
||||
d90,
|
||||
list(accounts.ids), company_id, as_of,
|
||||
),
|
||||
)
|
||||
|
||||
rows = []
|
||||
for r in self.env.cr.dictfetchall():
|
||||
rows.append({
|
||||
'partner_id': r['partner_id'] or False,
|
||||
'partner_name': r['partner_name'] or '(no partner)',
|
||||
'total': float(r['total_residual'] or 0),
|
||||
'bucket_current': float(r['bucket_current'] or 0),
|
||||
'bucket_1_30': float(r['bucket_1_30'] or 0),
|
||||
'bucket_31_60': float(r['bucket_31_60'] or 0),
|
||||
'bucket_61_90': float(r['bucket_61_90'] or 0),
|
||||
'bucket_90_plus': float(r['bucket_90_plus'] or 0),
|
||||
'line_count': r['line_count'],
|
||||
})
|
||||
|
||||
total = sum(r['total'] for r in rows)
|
||||
return {
|
||||
'report_type': 'partner_grouped',
|
||||
'account_type': account_type,
|
||||
'period': {
|
||||
'date_from': str(period.date_from),
|
||||
'date_to': str(period.date_to),
|
||||
'label': period.label,
|
||||
},
|
||||
'company_id': company_id,
|
||||
'rows': rows,
|
||||
'total': total,
|
||||
'partner_count': len(rows),
|
||||
}
|
||||
|
||||
# ============================================================
|
||||
# PRIVATE HELPERS
|
||||
# ============================================================
|
||||
|
||||
def _get_report(self, report_type: str, *, company_id: int | None = None):
|
||||
"""Look up the active fusion.report definition for a given
|
||||
type+company. If no per-company override, falls back to global
|
||||
(company_id=False)."""
|
||||
def _get_report(
|
||||
self, report_type: str, *, company_id: int | None = None,
|
||||
code: str | None = None,
|
||||
):
|
||||
"""Look up the active fusion.report definition.
|
||||
|
||||
When ``code`` is provided, prefer the report with that exact code
|
||||
(validating its ``report_type`` matches). Otherwise fall back to
|
||||
the canonical-by-type lookup: prefer code == report_type, then any
|
||||
report of that type. Per-company overrides win over global.
|
||||
"""
|
||||
Report = self.env['fusion.report'].sudo()
|
||||
company_id = company_id or self.env.company.id
|
||||
company_domain = [
|
||||
('active', '=', True),
|
||||
'|',
|
||||
('company_id', '=', company_id),
|
||||
('company_id', '=', False),
|
||||
]
|
||||
if code:
|
||||
report = Report.search(
|
||||
[('code', '=', code)] + company_domain,
|
||||
order='company_id desc nulls last',
|
||||
limit=1,
|
||||
)
|
||||
if not report:
|
||||
raise ValidationError(
|
||||
_("No active fusion.report definition with code '%s'") % code
|
||||
)
|
||||
if report.report_type != report_type:
|
||||
raise ValidationError(
|
||||
_("Report '%(code)s' has type '%(actual)s' but '%(expected)s' was expected.")
|
||||
% {
|
||||
'code': code,
|
||||
'actual': report.report_type,
|
||||
'expected': report_type,
|
||||
}
|
||||
)
|
||||
return report
|
||||
|
||||
# No code: prefer the canonical (code == report_type), then any
|
||||
# other report of that type.
|
||||
report = Report.search(
|
||||
[
|
||||
('report_type', '=', report_type),
|
||||
('active', '=', True),
|
||||
'|',
|
||||
('company_id', '=', company_id),
|
||||
('company_id', '=', False),
|
||||
],
|
||||
[('code', '=', report_type), ('report_type', '=', report_type)] + company_domain,
|
||||
order='company_id desc nulls last',
|
||||
limit=1,
|
||||
)
|
||||
if report:
|
||||
return report
|
||||
report = Report.search(
|
||||
[('report_type', '=', report_type)] + company_domain,
|
||||
order='company_id desc nulls last, sequence',
|
||||
limit=1,
|
||||
)
|
||||
if not report:
|
||||
raise ValidationError(
|
||||
_("No active fusion.report definition for type '%s'") % report_type
|
||||
|
||||
@@ -90,6 +90,75 @@ class TestFusionReportEngine(TransactionCase):
|
||||
)
|
||||
self.assertIsInstance(rows, list)
|
||||
|
||||
def test_compute_partner_grouped_receivable(self):
|
||||
period = Period(date(2025, 1, 1), date(2025, 12, 31), 'Test')
|
||||
result = self.env['fusion.report.engine'].compute_partner_grouped(
|
||||
period, account_type='asset_receivable',
|
||||
)
|
||||
self.assertEqual(result['report_type'], 'partner_grouped')
|
||||
self.assertEqual(result['account_type'], 'asset_receivable')
|
||||
self.assertIn('rows', result)
|
||||
self.assertIn('total', result)
|
||||
self.assertIn('partner_count', result)
|
||||
if result['rows']:
|
||||
for key in (
|
||||
'partner_name', 'total', 'bucket_current', 'bucket_1_30',
|
||||
'bucket_31_60', 'bucket_61_90', 'bucket_90_plus',
|
||||
):
|
||||
self.assertIn(key, result['rows'][0])
|
||||
|
||||
def test_report_code_disambiguates_same_report_type(self):
|
||||
"""Multiple reports of report_type='pnl' must each be addressable
|
||||
by code so the engine returns the requested definition's line_specs
|
||||
(not whichever was first by company_id)."""
|
||||
spec_one = [
|
||||
{'label': 'A', 'account_type_prefix': 'income_', 'sign': 1},
|
||||
]
|
||||
spec_two = [
|
||||
{'label': 'X', 'account_type_prefix': 'income_', 'sign': 1},
|
||||
{'label': 'Y', 'account_type_prefix': 'expense_', 'sign': -1},
|
||||
{'label': 'Z', 'account_type_prefix': 'asset_', 'sign': 1},
|
||||
]
|
||||
self.env['fusion.report'].create({
|
||||
'name': 'Variant One', 'code': 'variant_one',
|
||||
'report_type': 'pnl', 'line_specs': spec_one,
|
||||
'company_id': self.env.company.id,
|
||||
})
|
||||
self.env['fusion.report'].create({
|
||||
'name': 'Variant Two', 'code': 'variant_two',
|
||||
'report_type': 'pnl', 'line_specs': spec_two,
|
||||
'company_id': self.env.company.id,
|
||||
})
|
||||
period = Period(date(2026, 1, 1), date(2026, 12, 31), 'Test')
|
||||
engine = self.env['fusion.report.engine']
|
||||
r1 = engine.compute_pnl(
|
||||
period, company_id=self.env.company.id,
|
||||
report_code='variant_one',
|
||||
)
|
||||
r2 = engine.compute_pnl(
|
||||
period, company_id=self.env.company.id,
|
||||
report_code='variant_two',
|
||||
)
|
||||
self.assertEqual(r1['report_name'], 'Variant One')
|
||||
self.assertEqual(r2['report_name'], 'Variant Two')
|
||||
self.assertEqual(len(r1['rows']), 1)
|
||||
self.assertEqual(len(r2['rows']), 3)
|
||||
|
||||
def test_report_code_validates_type_match(self):
|
||||
"""Asking for a 'pnl' computation but giving a balance_sheet code
|
||||
should raise ValidationError, not silently mis-render."""
|
||||
self.env['fusion.report'].create({
|
||||
'name': 'Wrong Type', 'code': 'wrong_type_test',
|
||||
'report_type': 'balance_sheet', 'line_specs': [],
|
||||
'company_id': self.env.company.id,
|
||||
})
|
||||
period = Period(date(2026, 1, 1), date(2026, 12, 31), 'Test')
|
||||
with self.assertRaises(ValidationError):
|
||||
self.env['fusion.report.engine'].compute_pnl(
|
||||
period, company_id=self.env.company.id,
|
||||
report_code='wrong_type_test',
|
||||
)
|
||||
|
||||
def test_no_report_raises_validation_error(self):
|
||||
period = Period(date(2026, 1, 1), date(2026, 12, 31), 'Test 2026')
|
||||
# Inactivate any pre-existing GL definitions so the lookup
|
||||
|
||||
@@ -96,15 +96,20 @@
|
||||
<br/>
|
||||
<small t-field="line.name"/>
|
||||
</td>
|
||||
<td t-field="line.x_fc_coating_config_id"/>
|
||||
<td class="text-end"
|
||||
t-field="line.product_uom_qty"/>
|
||||
<td class="text-end"
|
||||
t-field="line.price_unit"
|
||||
t-options='{"widget": "monetary", "display_currency": doc.currency_id}'/>
|
||||
<td class="text-end"
|
||||
t-field="line.price_subtotal"
|
||||
t-options='{"widget": "monetary", "display_currency": doc.currency_id}'/>
|
||||
<td>
|
||||
<span t-field="line.x_fc_coating_config_id"/>
|
||||
</td>
|
||||
<td class="text-end">
|
||||
<span t-field="line.product_uom_qty"/>
|
||||
</td>
|
||||
<td class="text-end">
|
||||
<span t-field="line.price_unit"
|
||||
t-options='{"widget": "monetary", "display_currency": doc.currency_id}'/>
|
||||
</td>
|
||||
<td class="text-end">
|
||||
<span t-field="line.price_subtotal"
|
||||
t-options='{"widget": "monetary", "display_currency": doc.currency_id}'/>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
<tfoot>
|
||||
@@ -113,8 +118,10 @@
|
||||
<strong>Total</strong>
|
||||
</td>
|
||||
<td class="text-end">
|
||||
<strong t-field="doc.amount_total"
|
||||
t-options='{"widget": "monetary", "display_currency": doc.currency_id}'/>
|
||||
<strong>
|
||||
<span t-field="doc.amount_total"
|
||||
t-options='{"widget": "monetary", "display_currency": doc.currency_id}'/>
|
||||
</strong>
|
||||
</td>
|
||||
</tr>
|
||||
</tfoot>
|
||||
|
||||
Reference in New Issue
Block a user