feat(fusion_accounting_reports): partner-grouped engine method
Adds engine.compute_partner_grouped(period, account_type=...) that returns per-partner aggregations with aging buckets (current/1-30/ 31-60/61-90/90+). SQL-direct for performance — single GROUP BY query with conditional sum per bucket. Foundation for the 3 partner-grouped reports landing in commit 3: Aged Receivable, Aged Payable, Partner Ledger. Made-with: Cursor
This commit is contained in:
@@ -14,7 +14,7 @@ Internal pipeline (per report run):
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
from datetime import date
|
from datetime import date, timedelta
|
||||||
|
|
||||||
from odoo import _, api, models
|
from odoo import _, api, models
|
||||||
from odoo.exceptions import ValidationError
|
from odoo.exceptions import ValidationError
|
||||||
@@ -118,6 +118,130 @@ class FusionReportEngine(models.AbstractModel):
|
|||||||
limit=500,
|
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
|
# PRIVATE HELPERS
|
||||||
# ============================================================
|
# ============================================================
|
||||||
|
|||||||
@@ -90,6 +90,23 @@ class TestFusionReportEngine(TransactionCase):
|
|||||||
)
|
)
|
||||||
self.assertIsInstance(rows, list)
|
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_no_report_raises_validation_error(self):
|
def test_no_report_raises_validation_error(self):
|
||||||
period = Period(date(2026, 1, 1), date(2026, 12, 31), 'Test 2026')
|
period = Period(date(2026, 1, 1), date(2026, 12, 31), 'Test 2026')
|
||||||
# Inactivate any pre-existing GL definitions so the lookup
|
# Inactivate any pre-existing GL definitions so the lookup
|
||||||
|
|||||||
Reference in New Issue
Block a user