feat(fusion_claims): expand dashboard with this-month, pipeline, aging, recent exports + full-width

Adds 4 new sections:
- This Month rollup: submitted/approved/delivered/billed counts MTD
- Pipeline $ by stage: pre-submit / submitted / approved / ready-to-bill amounts
- Aging buckets: 30-59d, 60-89d, 90+ days
- Recent ADP Exports: last 5 with totals

Also overrides Odoo's form-sheet max-width on .o_fc_dashboard so the
dashboard uses the full browser width.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
gsinghpal
2026-05-21 04:26:25 -04:00
parent 5b6e53c863
commit 4025789ba0
4 changed files with 415 additions and 7 deletions

View File

@@ -42,6 +42,10 @@ class FusionClaimsDashboard(models.TransientModel):
return []
return [('user_id', '=', self.env.user.id)]
def _month_start(self):
from datetime import date
return date.today().replace(day=1)
# =========================================================================
# Header banner
# =========================================================================
@@ -301,6 +305,139 @@ class FusionClaimsDashboard(models.TransientModel):
('x_fc_mod_status', '=', 'pod_submitted'),
])
# =========================================================================
# This Month rollup (4-up secondary KPI strip)
# =========================================================================
count_month_submitted = fields.Integer(compute='_compute_this_month')
count_month_approved = fields.Integer(compute='_compute_this_month')
count_month_delivered = fields.Integer(compute='_compute_this_month')
count_month_billed = fields.Integer(compute='_compute_this_month')
def _compute_this_month(self):
SO = self.env['sale.order'].sudo()
for rec in self:
base = rec._role_filter_domain()
ms = rec._month_start()
rec.count_month_submitted = SO.search_count(base + [
('x_fc_claim_submission_date', '>=', ms),
])
rec.count_month_approved = SO.search_count(base + [
('x_fc_claim_approval_date', '>=', ms),
])
rec.count_month_delivered = SO.search_count(base + [
('x_fc_adp_delivery_date', '>=', ms),
])
rec.count_month_billed = SO.search_count(base + [
('x_fc_billing_date', '>=', ms),
])
# =========================================================================
# Pipeline $ by stage (4-up money-in-motion strip)
# =========================================================================
pipeline_pre_amount = fields.Monetary(compute='_compute_pipeline',
currency_field='currency_id')
pipeline_submitted_amount = fields.Monetary(compute='_compute_pipeline',
currency_field='currency_id')
pipeline_approved_amount = fields.Monetary(compute='_compute_pipeline',
currency_field='currency_id')
pipeline_ready_bill_amount = fields.Monetary(compute='_compute_pipeline',
currency_field='currency_id')
def _compute_pipeline(self):
SO = self.env['sale.order'].sudo()
for rec in self:
base = rec._role_filter_domain()
pre = SO.search(base + [
('x_fc_adp_application_status', 'in',
['waiting_for_application', 'assessment_completed',
'application_received', 'ready_submission']),
])
sub = SO.search(base + [
('x_fc_adp_application_status', 'in', ['submitted', 'resubmitted']),
])
app = SO.search(base + [
('x_fc_adp_application_status', 'in', ['approved', 'approved_deduction']),
])
bill = SO.search(base + [
('x_fc_adp_application_status', '=', 'ready_bill'),
])
rec.pipeline_pre_amount = sum(pre.mapped('amount_total'))
rec.pipeline_submitted_amount = sum(sub.mapped('amount_total'))
rec.pipeline_approved_amount = sum(app.mapped('amount_total'))
rec.pipeline_ready_bill_amount = sum(bill.mapped('amount_total'))
# =========================================================================
# Aging buckets (disjoint: 30-59d, 60-89d, 90+d)
# =========================================================================
aging_30_count = fields.Integer(compute='_compute_aging')
aging_60_count = fields.Integer(compute='_compute_aging')
aging_90_count = fields.Integer(compute='_compute_aging')
def _compute_aging(self):
from datetime import date, timedelta
SO = self.env['sale.order'].sudo()
today = date.today()
cut_30 = today - timedelta(days=30)
cut_60 = today - timedelta(days=60)
cut_90 = today - timedelta(days=90)
# "Active" = SO not cancelled at order level, AND if it has an ADP
# status, it's not in a terminal ADP state.
terminal_adp = ['case_closed', 'cancelled', 'expired', 'withdrawn']
for rec in self:
base = rec._role_filter_domain() + [
('state', '!=', 'cancel'),
'|',
('x_fc_adp_application_status', '=', False),
('x_fc_adp_application_status', 'not in', terminal_adp),
]
rec.aging_30_count = SO.search_count(base + [
('create_date', '<', cut_30),
('create_date', '>=', cut_60),
])
rec.aging_60_count = SO.search_count(base + [
('create_date', '<', cut_60),
('create_date', '>=', cut_90),
])
rec.aging_90_count = SO.search_count(base + [
('create_date', '<', cut_90),
])
# =========================================================================
# Recent ADP Exports (last 5)
# =========================================================================
recent_exports_html = fields.Html(compute='_compute_recent_exports',
sanitize=False)
recent_exports_count = fields.Integer(compute='_compute_recent_exports')
def _compute_recent_exports(self):
Exp = self.env['fusion_claims.adp.export.record'].sudo()
for rec in self:
records = Exp.search([], order='export_date desc', limit=5)
rec.recent_exports_count = Exp.search_count([])
if not records:
rec.recent_exports_html = (
'<p class="o_fc_empty">No exports yet.</p>'
)
continue
rows = []
for r in records:
total = sum(r.invoice_ids.mapped('amount_total'))
date_str = (r.export_date.strftime('%b %d, %Y')
if r.export_date else '')
label = r.posting_period_label or r.name or 'Export'
inv_count = r.invoice_count or 0
rows.append(
f'<div class="o_fc_export_row" '
f'data-export-id="{r.id}">'
f'<div class="o_fc_export_label">'
f'<b>{label}</b>'
f'<br/><small>{date_str} · {inv_count} inv</small>'
f'</div>'
f'<div class="o_fc_export_amount">${total:,.0f}</div>'
f'</div>'
)
rec.recent_exports_html = '\n'.join(rows)
# =========================================================================
# Open-list action methods
# =========================================================================
@@ -545,3 +682,85 @@ class FusionClaimsDashboard(models.TransientModel):
def action_create_private_so(self):
return self._create_so_action('New Private Order',
{'default_x_fc_sale_type': 'direct_private'})
# =========================================================================
# Additional drill-downs (This Month, Pipeline, Aging, Exports)
# =========================================================================
def action_open_month_submitted(self):
return self._so_list_action('Submitted This Month', [
('x_fc_claim_submission_date', '>=', self._month_start()),
])
def action_open_month_approved(self):
return self._so_list_action('Approved This Month', [
('x_fc_claim_approval_date', '>=', self._month_start()),
])
def action_open_month_delivered(self):
return self._so_list_action('Delivered This Month', [
('x_fc_adp_delivery_date', '>=', self._month_start()),
])
def action_open_month_billed(self):
return self._so_list_action('Billed This Month', [
('x_fc_billing_date', '>=', self._month_start()),
])
def action_open_pipeline_pre(self):
return self._so_list_action('Pipeline — Pre-Submission', [
('x_fc_adp_application_status', 'in',
['waiting_for_application', 'assessment_completed',
'application_received', 'ready_submission']),
])
def action_open_pipeline_submitted(self):
return self._so_list_action('Pipeline — Submitted to ADP', [
('x_fc_adp_application_status', 'in', ['submitted', 'resubmitted']),
])
def action_open_aging_30(self):
from datetime import date, timedelta
today = date.today()
terminal_adp = ['case_closed', 'cancelled', 'expired', 'withdrawn']
return self._so_list_action('Aging — 30 to 59 Days', [
('state', '!=', 'cancel'),
'|',
('x_fc_adp_application_status', '=', False),
('x_fc_adp_application_status', 'not in', terminal_adp),
('create_date', '<', today - timedelta(days=30)),
('create_date', '>=', today - timedelta(days=60)),
])
def action_open_aging_60(self):
from datetime import date, timedelta
today = date.today()
terminal_adp = ['case_closed', 'cancelled', 'expired', 'withdrawn']
return self._so_list_action('Aging — 60 to 89 Days', [
('state', '!=', 'cancel'),
'|',
('x_fc_adp_application_status', '=', False),
('x_fc_adp_application_status', 'not in', terminal_adp),
('create_date', '<', today - timedelta(days=60)),
('create_date', '>=', today - timedelta(days=90)),
])
def action_open_aging_90(self):
from datetime import date, timedelta
today = date.today()
terminal_adp = ['case_closed', 'cancelled', 'expired', 'withdrawn']
return self._so_list_action('Aging — 90+ Days', [
('state', '!=', 'cancel'),
'|',
('x_fc_adp_application_status', '=', False),
('x_fc_adp_application_status', 'not in', terminal_adp),
('create_date', '<', today - timedelta(days=90)),
])
def action_open_recent_exports(self):
return {
'type': 'ir.actions.act_window',
'name': 'ADP Export History',
'res_model': 'fusion_claims.adp.export.record',
'view_mode': 'list,form',
'target': 'current',
}