Adds Aged Receivable, Aged Payable, and Partner Ledger as fusion.report records using the new compute_partner_grouped engine method. REPORT_TYPES is extended with aged_receivable / aged_payable / partner_ledger so each report has a unique report_type. The HTTP controller dispatches these to engine.compute_partner_grouped with the appropriate account_type via PARTNER_GROUPED_ACCOUNT_TYPE. Output includes per-partner aging buckets: current, 1-30, 31-60, 61-90, 90+ days. Westin total: 4 + 4 + 3 = 11 of Enterprise's 22 standard reports. Made-with: Cursor
266 lines
10 KiB
Python
266 lines
10 KiB
Python
"""HTTP controller: 8 JSON-RPC endpoints for the OWL reports widget.
|
|
|
|
All endpoints route through fusion.report.engine - no direct ORM
|
|
aggregation from the controller. Uses V19's type='jsonrpc'.
|
|
"""
|
|
|
|
import logging
|
|
from datetime import date, datetime
|
|
|
|
from odoo import _, http
|
|
from odoo.exceptions import ValidationError
|
|
from odoo.http import request
|
|
|
|
from ..services.anomaly_detection import detect as detect_anomalies
|
|
from ..services.commentary_generator import generate_commentary
|
|
from ..services.date_periods import Period
|
|
|
|
_logger = logging.getLogger(__name__)
|
|
|
|
|
|
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):
|
|
if isinstance(value, date):
|
|
return value
|
|
return datetime.strptime(value, '%Y-%m-%d').date()
|
|
|
|
|
|
def _build_period(date_from, date_to, label=None):
|
|
df = _parse_date(date_from)
|
|
dt = _parse_date(date_to)
|
|
return Period(date_from=df, date_to=dt, label=label or f"{df} - {dt}")
|
|
|
|
|
|
class FusionReportsController(http.Controller):
|
|
|
|
@http.route('/fusion/reports/list_available', type='jsonrpc', auth='user')
|
|
def list_available(self, company_id=None):
|
|
company_id = int(company_id) if company_id else request.env.company.id
|
|
Report = request.env['fusion.report'].sudo()
|
|
reports = Report.search([
|
|
('active', '=', True),
|
|
'|', ('company_id', '=', company_id), ('company_id', '=', False),
|
|
], order='sequence, name')
|
|
return {
|
|
'reports': [{
|
|
'id': r.id,
|
|
'name': r.name,
|
|
'code': r.code,
|
|
'report_type': r.report_type,
|
|
'description': r.description or '',
|
|
'default_comparison_mode': r.default_comparison_mode,
|
|
} for r in reports],
|
|
}
|
|
|
|
@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):
|
|
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
|
|
engine = request.env['fusion.report.engine']
|
|
|
|
if report_type == 'pnl':
|
|
period = _build_period(date_from, date_to)
|
|
return engine.compute_pnl(
|
|
period, comparison=comparison, company_id=company_id,
|
|
)
|
|
if report_type == 'balance_sheet':
|
|
return engine.compute_balance_sheet(
|
|
_parse_date(date_to),
|
|
comparison=comparison,
|
|
company_id=company_id,
|
|
)
|
|
if report_type == 'trial_balance':
|
|
period = _build_period(date_from, date_to)
|
|
return engine.compute_trial_balance(period, company_id=company_id)
|
|
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)
|
|
|
|
@http.route('/fusion/reports/drill_down', type='jsonrpc', auth='user')
|
|
def drill_down(self, account_id, date_from, date_to, company_id=None):
|
|
company_id = int(company_id) if company_id else request.env.company.id
|
|
engine = request.env['fusion.report.engine']
|
|
period = _build_period(date_from, date_to)
|
|
rows = engine.drill_down(
|
|
account_id=int(account_id),
|
|
period=period,
|
|
company_id=company_id,
|
|
)
|
|
return {'rows': rows, 'count': len(rows)}
|
|
|
|
@http.route('/fusion/reports/get_anomalies', type='jsonrpc', auth='user')
|
|
def get_anomalies(self, report_type, date_from, date_to,
|
|
comparison='previous_year', persist=False, company_id=None):
|
|
company_id = int(company_id) if company_id else request.env.company.id
|
|
report_result = self.run(
|
|
report_type=report_type,
|
|
date_from=date_from, date_to=date_to,
|
|
comparison=comparison, company_id=company_id,
|
|
)
|
|
anomalies = detect_anomalies(report_result)
|
|
if persist and anomalies:
|
|
Report = request.env['fusion.report']
|
|
report_def = Report.search([('report_type', '=', report_type)], limit=1)
|
|
if report_def:
|
|
self._persist_anomalies(
|
|
report_def,
|
|
_parse_date(date_from), _parse_date(date_to),
|
|
anomalies,
|
|
)
|
|
return {'anomalies': anomalies, 'count': len(anomalies)}
|
|
|
|
def _persist_anomalies(self, report, period_from, period_to, anomalies):
|
|
Anomaly = request.env['fusion.report.anomaly']
|
|
for a in anomalies:
|
|
existing = Anomaly.search([
|
|
('report_id', '=', report.id),
|
|
('period_from', '=', period_from),
|
|
('period_to', '=', period_to),
|
|
('row_id', '=', a['row_id']),
|
|
], limit=1)
|
|
vals = {
|
|
'report_id': report.id,
|
|
'period_from': period_from,
|
|
'period_to': period_to,
|
|
'row_id': a['row_id'],
|
|
'label': a['label'],
|
|
'current_amount': a['current_amount'],
|
|
'comparison_amount': a['comparison_amount'],
|
|
'variance_amount': a['variance_amount'],
|
|
'variance_pct': a['variance_pct'],
|
|
'severity': a['severity'],
|
|
'direction': a['direction'],
|
|
}
|
|
if existing:
|
|
existing.write(vals)
|
|
else:
|
|
Anomaly.create(vals)
|
|
|
|
@http.route('/fusion/reports/get_commentary', type='jsonrpc', auth='user')
|
|
def get_commentary(self, report_type, date_from, date_to,
|
|
comparison='none', force_regenerate=False, company_id=None):
|
|
company_id = int(company_id) if company_id else request.env.company.id
|
|
Report = request.env['fusion.report']
|
|
Commentary = request.env['fusion.report.commentary']
|
|
report_def = Report.search([('report_type', '=', report_type)], limit=1)
|
|
if not report_def:
|
|
raise ValidationError(_("No report definition for %s") % report_type)
|
|
|
|
period_from = _parse_date(date_from)
|
|
period_to = _parse_date(date_to)
|
|
|
|
cached = Commentary.search([
|
|
('report_id', '=', report_def.id),
|
|
('company_id', '=', company_id),
|
|
('period_from', '=', period_from),
|
|
('period_to', '=', period_to),
|
|
('comparison_mode', '=', comparison),
|
|
], limit=1)
|
|
if cached and not force_regenerate:
|
|
return {
|
|
'cached': True,
|
|
'summary': cached.summary or '',
|
|
'highlights': cached.highlights or [],
|
|
'concerns': cached.concerns or [],
|
|
'next_actions': cached.next_actions or [],
|
|
'generated_at': str(cached.generated_at),
|
|
}
|
|
|
|
report_result = self.run(
|
|
report_type=report_type, date_from=date_from,
|
|
date_to=date_to, comparison=comparison,
|
|
company_id=company_id,
|
|
)
|
|
anomalies = detect_anomalies(report_result)
|
|
commentary = generate_commentary(
|
|
request.env,
|
|
report_result=report_result,
|
|
anomalies=anomalies,
|
|
)
|
|
vals = {
|
|
'report_id': report_def.id,
|
|
'company_id': company_id,
|
|
'period_from': period_from,
|
|
'period_to': period_to,
|
|
'comparison_mode': comparison,
|
|
'summary': commentary.get('summary', ''),
|
|
'highlights': commentary.get('highlights', []),
|
|
'concerns': commentary.get('concerns', []),
|
|
'next_actions': commentary.get('next_actions', []),
|
|
}
|
|
if cached:
|
|
cached.write(vals)
|
|
else:
|
|
Commentary.create(vals)
|
|
return {'cached': False, **commentary}
|
|
|
|
@http.route('/fusion/reports/compare_periods', type='jsonrpc', auth='user')
|
|
def compare_periods(self, report_type, date_from, date_to,
|
|
comparison='previous_year', company_id=None):
|
|
return self.run(
|
|
report_type=report_type, date_from=date_from,
|
|
date_to=date_to, comparison=comparison,
|
|
company_id=company_id,
|
|
)
|
|
|
|
@http.route('/fusion/reports/export_pdf', type='jsonrpc', auth='user')
|
|
def export_pdf(self, report_type, date_from, date_to,
|
|
comparison='none', company_id=None):
|
|
Report = request.env['fusion.report']
|
|
report_def = Report.search([('report_type', '=', report_type)], limit=1)
|
|
if not report_def:
|
|
return {'status': 'error', 'message': f'No report definition for {report_type}'}
|
|
company_id = int(company_id) if company_id else request.env.company.id
|
|
pdf, _ct = request.env['ir.actions.report'].sudo()._render_qweb_pdf(
|
|
'fusion_accounting_reports.report_pdf_template',
|
|
res_ids=[report_def.id],
|
|
data={
|
|
'report_type': report_type,
|
|
'date_from': date_from, 'date_to': date_to,
|
|
'comparison': comparison, 'company_id': company_id,
|
|
},
|
|
)
|
|
import base64
|
|
return {
|
|
'status': 'ok',
|
|
'pdf_base64': base64.b64encode(pdf).decode('ascii'),
|
|
'filename': f'{report_type}_{date_from}_{date_to}.pdf',
|
|
}
|
|
|
|
@http.route('/fusion/reports/export_xlsx', type='jsonrpc', auth='user')
|
|
def export_xlsx(self, report_type, date_from, date_to,
|
|
comparison='none', company_id=None):
|
|
wizard = request.env['fusion.xlsx.export.wizard'].create({
|
|
'report_type': report_type,
|
|
'date_from': _parse_date(date_from),
|
|
'date_to': _parse_date(date_to),
|
|
'comparison': comparison,
|
|
})
|
|
wizard.action_export()
|
|
return {
|
|
'status': 'ok',
|
|
'xlsx_base64': wizard.xlsx_file.decode('ascii') if wizard.xlsx_file else '',
|
|
'filename': wizard.xlsx_filename,
|
|
}
|