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:
@@ -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': """
|
||||
|
||||
@@ -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',
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user