From 4025789ba0fc1ee0f14cf49f6dd904d3b52af741 Mon Sep 17 00:00:00 2001 From: gsinghpal Date: Thu, 21 May 2026 04:26:25 -0400 Subject: [PATCH] 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) --- fusion_claims/__manifest__.py | 2 +- fusion_claims/models/dashboard.py | 219 ++++++++++++++++++ .../static/src/scss/fc_dashboard.scss | 37 +++ fusion_claims/views/dashboard_views.xml | 164 ++++++++++++- 4 files changed, 415 insertions(+), 7 deletions(-) diff --git a/fusion_claims/__manifest__.py b/fusion_claims/__manifest__.py index f873963f..12201f20 100644 --- a/fusion_claims/__manifest__.py +++ b/fusion_claims/__manifest__.py @@ -5,7 +5,7 @@ { 'name': 'Fusion Claims', - 'version': '19.0.9.0.1', + 'version': '19.0.9.1.0', 'category': 'Sales', 'summary': 'Complete ADP Claims Management with Dashboard, Sales Integration, Billing Automation, and Two-Stage Verification.', 'description': """ diff --git a/fusion_claims/models/dashboard.py b/fusion_claims/models/dashboard.py index 0db3f61a..1e36d9d6 100644 --- a/fusion_claims/models/dashboard.py +++ b/fusion_claims/models/dashboard.py @@ -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 = ( + '

No exports yet.

' + ) + 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'
' + f'
' + f'{label}' + f'
{date_str} · {inv_count} inv' + f'
' + f'
${total:,.0f}
' + f'
' + ) + 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', + } diff --git a/fusion_claims/static/src/scss/fc_dashboard.scss b/fusion_claims/static/src/scss/fc_dashboard.scss index bf3568cc..655a2cef 100644 --- a/fusion_claims/static/src/scss/fc_dashboard.scss +++ b/fusion_claims/static/src/scss/fc_dashboard.scss @@ -3,6 +3,15 @@ // Consumes tokens from _fc_dashboard_tokens.scss (must load FIRST in bundle). // ============================================================================= +// Override Odoo's form-sheet max-width so the dashboard uses the full +// browser width. The selector matches the form (which carries the class) +// and targets the inner sheet element. +.o_fc_dashboard .o_form_sheet, +.o_form_view.o_fc_dashboard .o_form_sheet { + max-width: none; + width: 100%; +} + .o_fc_dashboard { // Re-export tokens as CSS custom properties for devtools inspection --fc-page-bg: #{$_fc-page-bg}; @@ -73,6 +82,13 @@ color: var(--fc-text-muted); margin-top: 2px; } + // Secondary KPI variant — smaller, denser. Used for "This Month" and + // "Pipeline by stage" tile strips. + .o_fc_kpi--secondary { + padding: 10px 6px; + .o_fc_kpi__num { font-size: 1.15rem; } + .o_fc_kpi__lbl { font-size: 0.68rem; } + } .o_fc_actions { display: flex; @@ -197,6 +213,27 @@ &:hover { color: var(--fc-urgent-num); text-decoration: underline; } } + // Recent ADP Exports list rows + .o_fc_export_row { + display: flex; + justify-content: space-between; + align-items: center; + padding: 6px 0; + border-bottom: 1px dashed var(--fc-card-border); + font-size: 0.85rem; + + &:last-child { border-bottom: none; } + } + .o_fc_export_label small { + color: var(--fc-text-muted); + font-size: 0.72rem; + } + .o_fc_export_amount { + font-weight: 700; + color: var(--fc-kpi-num); + font-variant-numeric: tabular-nums; + } + // Countdown widget colour levels (driven by OWL state) .o_fc_countdown { display: inline-block; diff --git a/fusion_claims/views/dashboard_views.xml b/fusion_claims/views/dashboard_views.xml index de6e7f81..f846d519 100644 --- a/fusion_claims/views/dashboard_views.xml +++ b/fusion_claims/views/dashboard_views.xml @@ -84,6 +84,102 @@ + +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ + +
+
+ +
+
+ +
+
+ +
+
+ +
+
+
+ +
+
+ + Aging +
+
+
+ +
+
+ +
+
+ +
+
+
+
Other Funders
-
+
-
+
-
+
-
+
-
+
-
+
+ + +
+
+ + Recent ADP Exports + + + + +
+ +