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

@@ -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': """

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',
}

View File

@@ -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;

View File

@@ -84,6 +84,102 @@
</div>
</div>
<!-- THIS MONTH ROLLUP (4 count tiles) -->
<div class="row g-2 mb-3">
<div class="col-6 col-md-3">
<button name="action_open_month_submitted" type="object"
class="btn p-0 w-100 border-0">
<div class="o_fc_kpi o_fc_kpi--secondary">
<span class="o_fc_kpi__num"><field name="count_month_submitted" nolabel="1"/></span>
<span class="o_fc_kpi__lbl">Submitted MTD</span>
</div>
</button>
</div>
<div class="col-6 col-md-3">
<button name="action_open_month_approved" type="object"
class="btn p-0 w-100 border-0">
<div class="o_fc_kpi o_fc_kpi--secondary">
<span class="o_fc_kpi__num"><field name="count_month_approved" nolabel="1"/></span>
<span class="o_fc_kpi__lbl">Approved MTD</span>
</div>
</button>
</div>
<div class="col-6 col-md-3">
<button name="action_open_month_delivered" type="object"
class="btn p-0 w-100 border-0">
<div class="o_fc_kpi o_fc_kpi--secondary">
<span class="o_fc_kpi__num"><field name="count_month_delivered" nolabel="1"/></span>
<span class="o_fc_kpi__lbl">Delivered MTD</span>
</div>
</button>
</div>
<div class="col-6 col-md-3">
<button name="action_open_month_billed" type="object"
class="btn p-0 w-100 border-0">
<div class="o_fc_kpi o_fc_kpi--secondary">
<span class="o_fc_kpi__num"><field name="count_month_billed" nolabel="1"/></span>
<span class="o_fc_kpi__lbl">Billed MTD</span>
</div>
</button>
</div>
</div>
<!-- PIPELINE $ BY STAGE (4 amount tiles) -->
<div class="row g-2 mb-3">
<div class="col-6 col-md-3">
<button name="action_open_pipeline_pre" type="object"
class="btn p-0 w-100 border-0">
<div class="o_fc_kpi o_fc_kpi--secondary">
<span class="o_fc_kpi__num">
<field name="pipeline_pre_amount"
widget="monetary" nolabel="1"
options="{'currency_field': 'currency_id'}"/>
</span>
<span class="o_fc_kpi__lbl">Pipeline · Pre-Submit</span>
</div>
</button>
</div>
<div class="col-6 col-md-3">
<button name="action_open_pipeline_submitted" type="object"
class="btn p-0 w-100 border-0">
<div class="o_fc_kpi o_fc_kpi--secondary">
<span class="o_fc_kpi__num">
<field name="pipeline_submitted_amount"
widget="monetary" nolabel="1"
options="{'currency_field': 'currency_id'}"/>
</span>
<span class="o_fc_kpi__lbl">Pipeline · Submitted</span>
</div>
</button>
</div>
<div class="col-6 col-md-3">
<button name="action_open_adp_approved" type="object"
class="btn p-0 w-100 border-0">
<div class="o_fc_kpi o_fc_kpi--secondary">
<span class="o_fc_kpi__num">
<field name="pipeline_approved_amount"
widget="monetary" nolabel="1"
options="{'currency_field': 'currency_id'}"/>
</span>
<span class="o_fc_kpi__lbl">Pipeline · Approved</span>
</div>
</button>
</div>
<div class="col-6 col-md-3">
<button name="action_open_adp_ready_bill" type="object"
class="btn p-0 w-100 border-0">
<div class="o_fc_kpi o_fc_kpi--secondary">
<span class="o_fc_kpi__num">
<field name="pipeline_ready_bill_amount"
widget="monetary" nolabel="1"
options="{'currency_field': 'currency_id'}"/>
</span>
<span class="o_fc_kpi__lbl">Pipeline · Ready to Bill</span>
</div>
</button>
</div>
</div>
<!-- QUICK ACTION PILLS -->
<div class="o_fc_actions mb-3">
<button name="action_create_adp_so" type="object"
@@ -148,11 +244,51 @@
</button>
</div>
<!-- Aging buckets -->
<div class="o_fc_section mb-3">
<h6 class="o_fc_h6">
<i class="fa fa-clock-o me-2"/>
Aging
</h6>
<div class="row g-2">
<div class="col-4">
<button name="action_open_aging_30" type="object"
class="btn p-0 w-100 border-0">
<div class="o_fc_tile">
<span class="o_fc_tile__num">
<field name="aging_30_count" nolabel="1"/>
</span>30 59d
</div>
</button>
</div>
<div class="col-4">
<button name="action_open_aging_60" type="object"
class="btn p-0 w-100 border-0">
<div class="o_fc_tile o_fc_tile--urgent">
<span class="o_fc_tile__num">
<field name="aging_60_count" nolabel="1"/>
</span>60 89d
</div>
</button>
</div>
<div class="col-4">
<button name="action_open_aging_90" type="object"
class="btn p-0 w-100 border-0">
<div class="o_fc_tile o_fc_tile--urgent">
<span class="o_fc_tile__num">
<field name="aging_90_count" nolabel="1"/>
</span>90+ d
</div>
</button>
</div>
</div>
</div>
<!-- Other Funders -->
<div class="o_fc_section mb-3">
<h6 class="o_fc_h6">Other Funders</h6>
<div class="row g-2">
<div class="col-4">
<div class="col-4 col-xl-2">
<button name="action_open_odsp_cases" type="object"
class="btn p-0 w-100 border-0">
<div class="o_fc_tile">
@@ -162,7 +298,7 @@
</div>
</button>
</div>
<div class="col-4">
<div class="col-4 col-xl-2">
<button name="action_open_wsib_cases" type="object"
class="btn p-0 w-100 border-0">
<div class="o_fc_tile">
@@ -172,7 +308,7 @@
</div>
</button>
</div>
<div class="col-4">
<div class="col-4 col-xl-2">
<button name="action_open_insurance_cases" type="object"
class="btn p-0 w-100 border-0">
<div class="o_fc_tile">
@@ -182,7 +318,7 @@
</div>
</button>
</div>
<div class="col-4">
<div class="col-4 col-xl-2">
<button name="action_open_mdc_cases" type="object"
class="btn p-0 w-100 border-0">
<div class="o_fc_tile">
@@ -192,7 +328,7 @@
</div>
</button>
</div>
<div class="col-4">
<div class="col-4 col-xl-2">
<button name="action_open_hardship_cases" type="object"
class="btn p-0 w-100 border-0">
<div class="o_fc_tile">
@@ -202,7 +338,7 @@
</div>
</button>
</div>
<div class="col-4">
<div class="col-4 col-xl-2">
<button name="action_open_acsd_cases" type="object"
class="btn p-0 w-100 border-0">
<div class="o_fc_tile">
@@ -373,6 +509,22 @@
</div>
</div>
</div>
<!-- Recent ADP Exports (last 5) -->
<div class="o_fc_section mb-3">
<h6 class="o_fc_h6">
<i class="fa fa-file-text-o me-2"/>
Recent ADP Exports
<span class="o_fc_tag">
<field name="recent_exports_count" nolabel="1"/>
</span>
<button name="action_open_recent_exports" type="object"
class="btn btn-link btn-sm ms-auto p-0">
View all
</button>
</h6>
<field name="recent_exports_html" nolabel="1"/>
</div>
</div>
</div>