Compare commits
12 Commits
53fd6114e7
...
b70fff01e1
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b70fff01e1 | ||
|
|
07f9bcf79b | ||
|
|
1420a5c445 | ||
|
|
2bfb1015ea | ||
|
|
ace82de88c | ||
|
|
1b1e9fdb9e | ||
|
|
95e0e2d9bd | ||
|
|
cdc9f864b2 | ||
|
|
a00c891277 | ||
|
|
f45883233c | ||
|
|
d5e79cdc10 | ||
|
|
1a8a96d94e |
@@ -5,7 +5,7 @@
|
||||
|
||||
{
|
||||
'name': 'Fusion Claims',
|
||||
'version': '19.0.8.0.7',
|
||||
'version': '19.0.9.0.0',
|
||||
'category': 'Sales',
|
||||
'summary': 'Complete ADP Claims Management with Dashboard, Sales Integration, Billing Automation, and Two-Stage Verification.',
|
||||
'description': """
|
||||
@@ -175,6 +175,18 @@
|
||||
'fusion_claims/static/src/js/attachment_image_compress.js',
|
||||
'fusion_claims/static/src/js/debug_required_fields.js',
|
||||
'fusion_claims/static/src/xml/document_preview.xml',
|
||||
# Dashboard: tokens MUST load before dashboard layout
|
||||
'fusion_claims/static/src/scss/_fc_dashboard_tokens.scss',
|
||||
'fusion_claims/static/src/scss/fc_dashboard.scss',
|
||||
# Dashboard OWL countdown widget
|
||||
'fusion_claims/static/src/js/fc_posting_countdown.js',
|
||||
'fusion_claims/static/src/xml/fc_posting_countdown.xml',
|
||||
],
|
||||
'web.assets_web_dark': [
|
||||
# Dark bundle recompiles the same SCSS with the dark
|
||||
# $o-webclient-color-scheme default so tokens branch correctly.
|
||||
'fusion_claims/static/src/scss/_fc_dashboard_tokens.scss',
|
||||
'fusion_claims/static/src/scss/fc_dashboard.scss',
|
||||
],
|
||||
},
|
||||
'images': ['static/description/icon.png'],
|
||||
|
||||
@@ -4,159 +4,544 @@
|
||||
|
||||
from odoo import api, fields, models
|
||||
|
||||
CASE_TYPE_SELECTION = [
|
||||
('adp', 'ADP'),
|
||||
('odsp', 'ODSP'),
|
||||
('march_of_dimes', 'March of Dimes'),
|
||||
('hardship', 'Hardship Funding'),
|
||||
('acsd', 'ACSD'),
|
||||
('muscular_dystrophy', 'Muscular Dystrophy'),
|
||||
('insurance', 'Insurance'),
|
||||
('wsib', 'WSIB'),
|
||||
]
|
||||
|
||||
TYPE_DOMAINS = {
|
||||
'adp': [('x_fc_sale_type', 'in', ['adp', 'adp_odsp'])],
|
||||
'odsp': [('x_fc_sale_type', 'in', ['odsp', 'adp_odsp'])],
|
||||
'march_of_dimes': [('x_fc_sale_type', '=', 'march_of_dimes')],
|
||||
'hardship': [('x_fc_sale_type', '=', 'hardship')],
|
||||
'acsd': [('x_fc_client_type', '=', 'ACS')],
|
||||
'muscular_dystrophy': [('x_fc_sale_type', '=', 'muscular_dystrophy')],
|
||||
'insurance': [('x_fc_sale_type', '=', 'insurance')],
|
||||
'wsib': [('x_fc_sale_type', '=', 'wsib')],
|
||||
}
|
||||
|
||||
TYPE_LABELS = dict(CASE_TYPE_SELECTION)
|
||||
|
||||
|
||||
class FusionClaimsDashboard(models.TransientModel):
|
||||
_name = 'fusion.claims.dashboard'
|
||||
_inherit = 'fusion_claims.adp.posting.schedule.mixin'
|
||||
_description = 'Fusion Claims Dashboard'
|
||||
_rec_name = 'name'
|
||||
|
||||
name = fields.Char(default='Dashboard', readonly=True)
|
||||
|
||||
# Case counts by funding type
|
||||
adp_count = fields.Integer(compute='_compute_stats')
|
||||
odsp_count = fields.Integer(compute='_compute_stats')
|
||||
march_of_dimes_count = fields.Integer(compute='_compute_stats')
|
||||
hardship_count = fields.Integer(compute='_compute_stats')
|
||||
acsd_count = fields.Integer(compute='_compute_stats')
|
||||
muscular_dystrophy_count = fields.Integer(compute='_compute_stats')
|
||||
insurance_count = fields.Integer(compute='_compute_stats')
|
||||
wsib_count = fields.Integer(compute='_compute_stats')
|
||||
total_profiles = fields.Integer(compute='_compute_stats')
|
||||
# =========================================================================
|
||||
# Role-aware filter
|
||||
# =========================================================================
|
||||
is_manager = fields.Boolean(compute='_compute_is_manager')
|
||||
|
||||
# Panel selectors (4 panels)
|
||||
panel1_type = fields.Selection(CASE_TYPE_SELECTION, string='Window 1', default='adp')
|
||||
panel2_type = fields.Selection(CASE_TYPE_SELECTION, string='Window 2', default='odsp')
|
||||
panel3_type = fields.Selection(CASE_TYPE_SELECTION, string='Window 3', default='march_of_dimes')
|
||||
panel4_type = fields.Selection(CASE_TYPE_SELECTION, string='Window 4', default='hardship')
|
||||
|
||||
# Panel HTML
|
||||
panel1_html = fields.Html(compute='_compute_panels', sanitize=False)
|
||||
panel2_html = fields.Html(compute='_compute_panels', sanitize=False)
|
||||
panel3_html = fields.Html(compute='_compute_panels', sanitize=False)
|
||||
panel4_html = fields.Html(compute='_compute_panels', sanitize=False)
|
||||
panel1_title = fields.Char(compute='_compute_panels')
|
||||
panel2_title = fields.Char(compute='_compute_panels')
|
||||
panel3_title = fields.Char(compute='_compute_panels')
|
||||
panel4_title = fields.Char(compute='_compute_panels')
|
||||
|
||||
def _compute_stats(self):
|
||||
SO = self.env['sale.order'].sudo()
|
||||
Profile = self.env['fusion.client.profile'].sudo()
|
||||
def _compute_is_manager(self):
|
||||
manager_group = self.env.ref('fusion_claims.group_fusion_claims_manager',
|
||||
raise_if_not_found=False)
|
||||
sale_mgr_group = self.env.ref('sales_team.group_sale_manager',
|
||||
raise_if_not_found=False)
|
||||
for rec in self:
|
||||
rec.adp_count = SO.search_count(TYPE_DOMAINS['adp'])
|
||||
rec.odsp_count = SO.search_count(TYPE_DOMAINS['odsp'])
|
||||
rec.march_of_dimes_count = SO.search_count(TYPE_DOMAINS['march_of_dimes'])
|
||||
rec.hardship_count = SO.search_count(TYPE_DOMAINS['hardship'])
|
||||
rec.acsd_count = SO.search_count(TYPE_DOMAINS['acsd'])
|
||||
rec.muscular_dystrophy_count = SO.search_count(TYPE_DOMAINS['muscular_dystrophy'])
|
||||
rec.insurance_count = SO.search_count(TYPE_DOMAINS['insurance'])
|
||||
rec.wsib_count = SO.search_count(TYPE_DOMAINS['wsib'])
|
||||
rec.total_profiles = Profile.search_count([])
|
||||
|
||||
@api.depends('panel1_type', 'panel2_type', 'panel3_type', 'panel4_type')
|
||||
def _compute_panels(self):
|
||||
SO = self.env['sale.order'].sudo()
|
||||
for rec in self:
|
||||
for i in range(1, 5):
|
||||
ptype = getattr(rec, f'panel{i}_type') or 'adp'
|
||||
domain = TYPE_DOMAINS.get(ptype, [])
|
||||
orders = SO.search(domain, order='create_date desc', limit=50)
|
||||
count = SO.search_count(domain)
|
||||
title = f'Window {i} - {TYPE_LABELS.get(ptype, ptype)} ({count} cases)'
|
||||
html = rec._build_top_list(orders)
|
||||
setattr(rec, f'panel{i}_title', title)
|
||||
setattr(rec, f'panel{i}_html', html)
|
||||
|
||||
def _build_top_list(self, orders):
|
||||
if not orders:
|
||||
return '<p class="text-muted text-center py-4">No cases found</p>'
|
||||
rows = []
|
||||
for o in orders:
|
||||
status = o.x_fc_adp_application_status or ''
|
||||
status_label = dict(o._fields['x_fc_adp_application_status'].selection).get(status, status)
|
||||
rows.append(
|
||||
f'<tr>'
|
||||
f'<td><a href="/odoo/sales/{o.id}">{o.name}</a></td>'
|
||||
f'<td>{o.partner_id.name or ""}</td>'
|
||||
f'<td>{status_label}</td>'
|
||||
f'<td class="text-end">${o.amount_total:,.2f}</td>'
|
||||
f'</tr>'
|
||||
user = rec.env.user
|
||||
rec.is_manager = bool(
|
||||
(manager_group and user.has_group('fusion_claims.group_fusion_claims_manager'))
|
||||
or (sale_mgr_group and user.has_group('sales_team.group_sale_manager'))
|
||||
)
|
||||
return (
|
||||
'<table class="table table-sm table-hover mb-0">'
|
||||
'<thead><tr><th>Order</th><th>Client</th><th>Status</th><th class="text-end">Total</th></tr></thead>'
|
||||
'<tbody>' + ''.join(rows) + '</tbody></table>'
|
||||
)
|
||||
|
||||
def action_open_order(self, order_id):
|
||||
"""Open a specific sale order with breadcrumbs."""
|
||||
def _role_filter_domain(self):
|
||||
"""Common domain prefix for SO-based counts.
|
||||
|
||||
Managers (fusion_claims.group_fusion_claims_manager or
|
||||
sales_team.group_sale_manager) see everything.
|
||||
Other users see only SOs where they are the salesperson.
|
||||
"""
|
||||
self.ensure_one()
|
||||
if self.is_manager:
|
||||
return []
|
||||
return [('user_id', '=', self.env.user.id)]
|
||||
|
||||
# =========================================================================
|
||||
# Header banner
|
||||
# =========================================================================
|
||||
posting_period_label = fields.Char(compute='_compute_banner')
|
||||
posting_period_start = fields.Date(compute='_compute_banner')
|
||||
posting_period_end = fields.Date(compute='_compute_banner')
|
||||
submission_deadline_dt = fields.Datetime(compute='_compute_banner')
|
||||
is_pre_first_posting = fields.Boolean(compute='_compute_banner')
|
||||
|
||||
def _compute_banner(self):
|
||||
from datetime import date, datetime, time, timedelta
|
||||
import pytz
|
||||
|
||||
today = date.today()
|
||||
for rec in self:
|
||||
base_date = rec._get_adp_posting_base_date()
|
||||
rec.is_pre_first_posting = today < base_date
|
||||
|
||||
current = rec._get_current_posting_date(today)
|
||||
nxt = rec._get_next_posting_date(today)
|
||||
# If we're sitting on a posting date, current == next; treat
|
||||
# the period as the one starting today.
|
||||
if current == nxt:
|
||||
period_start = current
|
||||
period_end = current + timedelta(days=rec._get_adp_posting_frequency())
|
||||
else:
|
||||
period_start = current
|
||||
period_end = nxt
|
||||
|
||||
rec.posting_period_start = period_start
|
||||
rec.posting_period_end = period_end
|
||||
|
||||
if rec.is_pre_first_posting:
|
||||
rec.posting_period_label = f"Posting starts {base_date.strftime('%b %d')}"
|
||||
else:
|
||||
rec.posting_period_label = (
|
||||
f"{period_start.strftime('%b %d')} – "
|
||||
f"{period_end.strftime('%b %d')}"
|
||||
)
|
||||
|
||||
wednesday = rec._get_posting_week_wednesday(nxt)
|
||||
naive_deadline = datetime.combine(wednesday, time(18, 0, 0))
|
||||
# Store as UTC; users see it in their TZ; OWL widget computes in local TZ.
|
||||
tz = pytz.timezone(rec.env.user.tz or 'America/Toronto')
|
||||
local_deadline = tz.localize(naive_deadline)
|
||||
rec.submission_deadline_dt = local_deadline.astimezone(pytz.UTC).replace(tzinfo=None)
|
||||
|
||||
# =========================================================================
|
||||
# KPI tiles (3-up)
|
||||
# =========================================================================
|
||||
currency_id = fields.Many2one('res.currency', compute='_compute_kpis')
|
||||
kpi_ready_amount = fields.Monetary(compute='_compute_kpis',
|
||||
currency_field='currency_id')
|
||||
kpi_ready_count = fields.Integer(compute='_compute_kpis')
|
||||
kpi_claimed_amount = fields.Monetary(compute='_compute_kpis',
|
||||
currency_field='currency_id')
|
||||
kpi_claimed_count = fields.Integer(compute='_compute_kpis')
|
||||
kpi_ar_amount = fields.Monetary(compute='_compute_kpis',
|
||||
currency_field='currency_id')
|
||||
kpi_ar_count = fields.Integer(compute='_compute_kpis')
|
||||
|
||||
def _invoice_role_filter(self):
|
||||
"""Role filter for invoices — applied through linked SO's user_id."""
|
||||
self.ensure_one()
|
||||
if self.is_manager:
|
||||
return []
|
||||
return [('x_fc_source_sale_order_id.user_id', '=', self.env.user.id)]
|
||||
|
||||
def _compute_kpis(self):
|
||||
Move = self.env['account.move'].sudo()
|
||||
for rec in self:
|
||||
rec.currency_id = rec.env.company.currency_id
|
||||
|
||||
inv_filter = rec._invoice_role_filter()
|
||||
|
||||
# KPI 1: Ready to Claim
|
||||
ready_domain = inv_filter + [
|
||||
('move_type', '=', 'out_invoice'),
|
||||
('state', '=', 'posted'),
|
||||
('x_fc_adp_billing_status', '=', 'waiting'),
|
||||
('adp_exported', '=', False),
|
||||
]
|
||||
ready_invoices = Move.search(ready_domain)
|
||||
rec.kpi_ready_count = len(ready_invoices)
|
||||
rec.kpi_ready_amount = sum(ready_invoices.mapped('amount_total'))
|
||||
|
||||
# KPI 2: Claimed This Period
|
||||
claimed_domain = inv_filter + [
|
||||
('move_type', '=', 'out_invoice'),
|
||||
('state', '=', 'posted'),
|
||||
('x_fc_adp_billing_status', 'in', ['submitted', 'resubmitted']),
|
||||
('adp_export_date', '>=', rec.posting_period_start),
|
||||
]
|
||||
claimed_invoices = Move.search(claimed_domain)
|
||||
rec.kpi_claimed_count = len(claimed_invoices)
|
||||
rec.kpi_claimed_amount = sum(claimed_invoices.mapped('amount_total'))
|
||||
|
||||
# KPI 3: Total AR (ADP-portion invoices, unpaid)
|
||||
ar_domain = inv_filter + [
|
||||
('move_type', '=', 'out_invoice'),
|
||||
('state', '=', 'posted'),
|
||||
('x_fc_invoice_type', '=', 'adp'),
|
||||
('payment_state', 'in', ['not_paid', 'partial']),
|
||||
]
|
||||
ar_invoices = Move.search(ar_domain)
|
||||
rec.kpi_ar_count = len(ar_invoices)
|
||||
rec.kpi_ar_amount = sum(ar_invoices.mapped('amount_total'))
|
||||
|
||||
# =========================================================================
|
||||
# Activities (left column)
|
||||
# =========================================================================
|
||||
my_activities_count = fields.Integer(compute='_compute_activities')
|
||||
my_activities_html = fields.Html(compute='_compute_activities', sanitize=False)
|
||||
|
||||
def _compute_activities(self):
|
||||
Activity = self.env['mail.activity'].sudo()
|
||||
domain = [
|
||||
('user_id', '=', self.env.user.id),
|
||||
('res_model', 'in', ['sale.order', 'account.move', 'fusion.technician.task']),
|
||||
]
|
||||
for rec in self:
|
||||
activities = Activity.search(domain, order='date_deadline asc', limit=10)
|
||||
rec.my_activities_count = Activity.search_count(domain)
|
||||
if not activities:
|
||||
rec.my_activities_html = (
|
||||
'<p class="o_fc_empty">No activities assigned.</p>'
|
||||
)
|
||||
continue
|
||||
from datetime import date
|
||||
today = date.today()
|
||||
rows = []
|
||||
for act in activities:
|
||||
overdue = act.date_deadline and act.date_deadline < today
|
||||
row_class = 'o_fc_activity_row o_fc_activity_overdue' if overdue else 'o_fc_activity_row'
|
||||
deadline_text = act.date_deadline.strftime('%b %d') if act.date_deadline else '—'
|
||||
url = f'/odoo/{act.res_model.replace(".", "_")}/{act.res_id}'
|
||||
rows.append(
|
||||
f'<div class="{row_class}">'
|
||||
f'<a href="{url}"><b>{act.summary or act.activity_type_id.name or "Activity"}</b></a>'
|
||||
f'<span class="o_fc_activity_deadline">{deadline_text}</span>'
|
||||
f'</div>'
|
||||
)
|
||||
rec.my_activities_html = '\n'.join(rows)
|
||||
|
||||
# =========================================================================
|
||||
# Bottlenecks (left column) + Other funder counts
|
||||
# =========================================================================
|
||||
bottleneck_no_pod_count = fields.Integer(compute='_compute_secondary_counts')
|
||||
bottleneck_no_response_count = fields.Integer(compute='_compute_secondary_counts')
|
||||
|
||||
count_odsp = fields.Integer(compute='_compute_secondary_counts')
|
||||
count_wsib = fields.Integer(compute='_compute_secondary_counts')
|
||||
count_insurance = fields.Integer(compute='_compute_secondary_counts')
|
||||
count_mdc = fields.Integer(compute='_compute_secondary_counts')
|
||||
count_hardship = fields.Integer(compute='_compute_secondary_counts')
|
||||
count_acsd = fields.Integer(compute='_compute_secondary_counts')
|
||||
|
||||
def _compute_secondary_counts(self):
|
||||
from datetime import date, timedelta
|
||||
SO = self.env['sale.order'].sudo()
|
||||
cutoff_14d_ago = date.today() - timedelta(days=14)
|
||||
for rec in self:
|
||||
base = rec._role_filter_domain()
|
||||
active = base + [('state', '!=', 'cancel')]
|
||||
|
||||
rec.bottleneck_no_pod_count = SO.search_count(base + [
|
||||
('x_fc_adp_application_status', 'in', ['approved', 'approved_deduction']),
|
||||
('x_fc_proof_of_delivery', '=', False),
|
||||
])
|
||||
rec.bottleneck_no_response_count = SO.search_count(base + [
|
||||
('x_fc_adp_application_status', 'in', ['submitted', 'resubmitted']),
|
||||
('x_fc_claim_submission_date', '<', cutoff_14d_ago),
|
||||
])
|
||||
|
||||
rec.count_odsp = SO.search_count(active + [
|
||||
('x_fc_sale_type', 'in', ['odsp', 'adp_odsp']),
|
||||
])
|
||||
rec.count_wsib = SO.search_count(active + [('x_fc_sale_type', '=', 'wsib')])
|
||||
rec.count_insurance = SO.search_count(active + [('x_fc_sale_type', '=', 'insurance')])
|
||||
rec.count_mdc = SO.search_count(active + [('x_fc_sale_type', '=', 'muscular_dystrophy')])
|
||||
rec.count_hardship = SO.search_count(active + [('x_fc_sale_type', '=', 'hardship')])
|
||||
rec.count_acsd = SO.search_count(active + [('x_fc_client_type', '=', 'ACS')])
|
||||
|
||||
# =========================================================================
|
||||
# ADP Pre-Approval (right column, 4 tiles)
|
||||
# =========================================================================
|
||||
adp_waiting_app_count = fields.Integer(compute='_compute_workflow_counts')
|
||||
adp_app_received_count = fields.Integer(compute='_compute_workflow_counts')
|
||||
adp_ready_submit_count = fields.Integer(compute='_compute_workflow_counts')
|
||||
adp_needs_correction_count = fields.Integer(compute='_compute_workflow_counts')
|
||||
|
||||
# =========================================================================
|
||||
# ADP Post-Approval (right column, 4 tiles)
|
||||
# =========================================================================
|
||||
adp_approved_count = fields.Integer(compute='_compute_workflow_counts')
|
||||
adp_ready_delivery_count = fields.Integer(compute='_compute_workflow_counts')
|
||||
adp_ready_bill_count = fields.Integer(compute='_compute_workflow_counts')
|
||||
adp_on_hold_count = fields.Integer(compute='_compute_workflow_counts')
|
||||
|
||||
# =========================================================================
|
||||
# MOD (right column, 5 tiles)
|
||||
# =========================================================================
|
||||
mod_awaiting_funding_count = fields.Integer(compute='_compute_workflow_counts')
|
||||
mod_funding_approved_count = fields.Integer(compute='_compute_workflow_counts')
|
||||
mod_pca_received_count = fields.Integer(compute='_compute_workflow_counts')
|
||||
mod_project_complete_count = fields.Integer(compute='_compute_workflow_counts')
|
||||
mod_pod_submitted_count = fields.Integer(compute='_compute_workflow_counts')
|
||||
|
||||
def _compute_workflow_counts(self):
|
||||
SO = self.env['sale.order'].sudo()
|
||||
for rec in self:
|
||||
base = rec._role_filter_domain()
|
||||
|
||||
# ADP Pre-Approval
|
||||
rec.adp_waiting_app_count = SO.search_count(base + [
|
||||
('x_fc_adp_application_status', 'in',
|
||||
['waiting_for_application', 'assessment_completed']),
|
||||
])
|
||||
rec.adp_app_received_count = SO.search_count(base + [
|
||||
('x_fc_adp_application_status', '=', 'application_received'),
|
||||
])
|
||||
rec.adp_ready_submit_count = SO.search_count(base + [
|
||||
('x_fc_adp_application_status', '=', 'ready_submission'),
|
||||
])
|
||||
rec.adp_needs_correction_count = SO.search_count(base + [
|
||||
('x_fc_adp_application_status', '=', 'needs_correction'),
|
||||
])
|
||||
|
||||
# ADP Post-Approval
|
||||
rec.adp_approved_count = SO.search_count(base + [
|
||||
('x_fc_adp_application_status', 'in', ['approved', 'approved_deduction']),
|
||||
])
|
||||
rec.adp_ready_delivery_count = SO.search_count(base + [
|
||||
('x_fc_adp_application_status', '=', 'ready_delivery'),
|
||||
])
|
||||
rec.adp_ready_bill_count = SO.search_count(base + [
|
||||
('x_fc_adp_application_status', '=', 'ready_bill'),
|
||||
])
|
||||
rec.adp_on_hold_count = SO.search_count(base + [
|
||||
('x_fc_adp_application_status', '=', 'on_hold'),
|
||||
])
|
||||
|
||||
# MOD
|
||||
rec.mod_awaiting_funding_count = SO.search_count(base + [
|
||||
('x_fc_mod_status', '=', 'awaiting_funding'),
|
||||
])
|
||||
rec.mod_funding_approved_count = SO.search_count(base + [
|
||||
('x_fc_mod_status', '=', 'funding_approved'),
|
||||
])
|
||||
rec.mod_pca_received_count = SO.search_count(base + [
|
||||
('x_fc_mod_status', '=', 'contract_received'),
|
||||
])
|
||||
rec.mod_project_complete_count = SO.search_count(base + [
|
||||
('x_fc_mod_status', '=', 'project_complete'),
|
||||
])
|
||||
rec.mod_pod_submitted_count = SO.search_count(base + [
|
||||
('x_fc_mod_status', '=', 'pod_submitted'),
|
||||
])
|
||||
|
||||
# =========================================================================
|
||||
# Open-list action methods
|
||||
# =========================================================================
|
||||
def _so_list_action(self, name, domain):
|
||||
return {
|
||||
'type': 'ir.actions.act_window',
|
||||
'name': 'Sale Order',
|
||||
'name': name,
|
||||
'res_model': 'sale.order',
|
||||
'view_mode': 'form',
|
||||
'res_id': order_id,
|
||||
'view_mode': 'list,form',
|
||||
'domain': self._role_filter_domain() + domain,
|
||||
'target': 'current',
|
||||
}
|
||||
|
||||
def action_open_adp(self):
|
||||
return self._open_type_action('adp')
|
||||
# ----- ADP Pre-Approval -----
|
||||
def action_open_adp_waiting_app(self):
|
||||
return self._so_list_action('ADP — Waiting for Application', [
|
||||
('x_fc_adp_application_status', 'in',
|
||||
['waiting_for_application', 'assessment_completed']),
|
||||
])
|
||||
|
||||
def action_open_odsp(self):
|
||||
return self._open_type_action('odsp')
|
||||
def action_open_adp_app_received(self):
|
||||
return self._so_list_action('ADP — Application Received', [
|
||||
('x_fc_adp_application_status', '=', 'application_received'),
|
||||
])
|
||||
|
||||
def action_open_march(self):
|
||||
return self._open_type_action('march_of_dimes')
|
||||
def action_open_adp_ready_submit(self):
|
||||
return self._so_list_action('ADP — Ready for Submission', [
|
||||
('x_fc_adp_application_status', '=', 'ready_submission'),
|
||||
])
|
||||
|
||||
def action_open_hardship(self):
|
||||
return self._open_type_action('hardship')
|
||||
def action_open_adp_needs_correction(self):
|
||||
return self._so_list_action('ADP — Needs Correction', [
|
||||
('x_fc_adp_application_status', '=', 'needs_correction'),
|
||||
])
|
||||
|
||||
def action_open_acsd(self):
|
||||
return self._open_type_action('acsd')
|
||||
# ----- ADP Post-Approval -----
|
||||
def action_open_adp_approved(self):
|
||||
return self._so_list_action('ADP — Approved', [
|
||||
('x_fc_adp_application_status', 'in', ['approved', 'approved_deduction']),
|
||||
])
|
||||
|
||||
def action_open_muscular(self):
|
||||
return self._open_type_action('muscular_dystrophy')
|
||||
def action_open_adp_ready_delivery(self):
|
||||
return self._so_list_action('ADP — Ready for Delivery', [
|
||||
('x_fc_adp_application_status', '=', 'ready_delivery'),
|
||||
])
|
||||
|
||||
def action_open_insurance(self):
|
||||
return self._open_type_action('insurance')
|
||||
def action_open_adp_ready_bill(self):
|
||||
return self._so_list_action('ADP — Ready to Bill', [
|
||||
('x_fc_adp_application_status', '=', 'ready_bill'),
|
||||
])
|
||||
|
||||
def action_open_wsib(self):
|
||||
return self._open_type_action('wsib')
|
||||
def action_open_adp_on_hold(self):
|
||||
return self._so_list_action('ADP — On Hold', [
|
||||
('x_fc_adp_application_status', '=', 'on_hold'),
|
||||
])
|
||||
|
||||
def action_open_profiles(self):
|
||||
return {
|
||||
'type': 'ir.actions.act_window', 'name': 'Client Profiles',
|
||||
'res_model': 'fusion.client.profile', 'view_mode': 'list,form',
|
||||
}
|
||||
# ----- MOD -----
|
||||
def action_open_mod_awaiting_funding(self):
|
||||
return self._so_list_action('MOD — Awaiting Funding', [
|
||||
('x_fc_mod_status', '=', 'awaiting_funding'),
|
||||
])
|
||||
|
||||
def _open_type_action(self, type_key):
|
||||
def action_open_mod_funding_approved(self):
|
||||
return self._so_list_action('MOD — Funding Approved', [
|
||||
('x_fc_mod_status', '=', 'funding_approved'),
|
||||
])
|
||||
|
||||
def action_open_mod_pca_received(self):
|
||||
return self._so_list_action('MOD — PCA Received', [
|
||||
('x_fc_mod_status', '=', 'contract_received'),
|
||||
])
|
||||
|
||||
def action_open_mod_project_complete(self):
|
||||
return self._so_list_action('MOD — Project Complete', [
|
||||
('x_fc_mod_status', '=', 'project_complete'),
|
||||
])
|
||||
|
||||
def action_open_mod_pod_submitted(self):
|
||||
return self._so_list_action('MOD — POD Submitted', [
|
||||
('x_fc_mod_status', '=', 'pod_submitted'),
|
||||
])
|
||||
|
||||
# ----- Other funders -----
|
||||
def action_open_odsp_cases(self):
|
||||
return self._so_list_action('ODSP Cases', [
|
||||
('state', '!=', 'cancel'),
|
||||
('x_fc_sale_type', 'in', ['odsp', 'adp_odsp']),
|
||||
])
|
||||
|
||||
def action_open_wsib_cases(self):
|
||||
return self._so_list_action('WSIB Cases', [
|
||||
('state', '!=', 'cancel'),
|
||||
('x_fc_sale_type', '=', 'wsib'),
|
||||
])
|
||||
|
||||
def action_open_insurance_cases(self):
|
||||
return self._so_list_action('Insurance Cases', [
|
||||
('state', '!=', 'cancel'),
|
||||
('x_fc_sale_type', '=', 'insurance'),
|
||||
])
|
||||
|
||||
def action_open_mdc_cases(self):
|
||||
return self._so_list_action('Muscular Dystrophy Cases', [
|
||||
('state', '!=', 'cancel'),
|
||||
('x_fc_sale_type', '=', 'muscular_dystrophy'),
|
||||
])
|
||||
|
||||
def action_open_hardship_cases(self):
|
||||
return self._so_list_action('Hardship Cases', [
|
||||
('state', '!=', 'cancel'),
|
||||
('x_fc_sale_type', '=', 'hardship'),
|
||||
])
|
||||
|
||||
def action_open_acsd_cases(self):
|
||||
return self._so_list_action('ACSD Cases', [
|
||||
('state', '!=', 'cancel'),
|
||||
('x_fc_client_type', '=', 'ACS'),
|
||||
])
|
||||
|
||||
# ----- Bottlenecks -----
|
||||
def action_open_bottleneck_no_pod(self):
|
||||
return self._so_list_action('Bottleneck — Approved without POD', [
|
||||
('x_fc_adp_application_status', 'in', ['approved', 'approved_deduction']),
|
||||
('x_fc_proof_of_delivery', '=', False),
|
||||
])
|
||||
|
||||
def action_open_bottleneck_no_response(self):
|
||||
from datetime import date, timedelta
|
||||
cutoff = date.today() - timedelta(days=14)
|
||||
return self._so_list_action('Bottleneck — Submitted, no response', [
|
||||
('x_fc_adp_application_status', 'in', ['submitted', 'resubmitted']),
|
||||
('x_fc_claim_submission_date', '<', cutoff),
|
||||
])
|
||||
|
||||
# ----- Activities -----
|
||||
def action_open_my_activities(self):
|
||||
return {
|
||||
'type': 'ir.actions.act_window',
|
||||
'name': f'{TYPE_LABELS.get(type_key, type_key)} Cases',
|
||||
'res_model': 'sale.order', 'view_mode': 'list,form',
|
||||
'domain': TYPE_DOMAINS.get(type_key, []),
|
||||
'name': 'My Activities',
|
||||
'res_model': 'mail.activity',
|
||||
'view_mode': 'list,form',
|
||||
'domain': [
|
||||
('user_id', '=', self.env.user.id),
|
||||
('res_model', 'in', ['sale.order', 'account.move',
|
||||
'fusion.technician.task']),
|
||||
],
|
||||
'target': 'current',
|
||||
}
|
||||
|
||||
# ----- KPI drill-downs -----
|
||||
def action_open_kpi_ready(self):
|
||||
return {
|
||||
'type': 'ir.actions.act_window',
|
||||
'name': 'Ready to Claim (ADP)',
|
||||
'res_model': 'account.move',
|
||||
'view_mode': 'list,form',
|
||||
'domain': self._invoice_role_filter() + [
|
||||
('move_type', '=', 'out_invoice'),
|
||||
('state', '=', 'posted'),
|
||||
('x_fc_adp_billing_status', '=', 'waiting'),
|
||||
('adp_exported', '=', False),
|
||||
],
|
||||
'target': 'current',
|
||||
}
|
||||
|
||||
def action_open_kpi_claimed(self):
|
||||
return {
|
||||
'type': 'ir.actions.act_window',
|
||||
'name': 'Claimed This Period',
|
||||
'res_model': 'account.move',
|
||||
'view_mode': 'list,form',
|
||||
'domain': self._invoice_role_filter() + [
|
||||
('move_type', '=', 'out_invoice'),
|
||||
('state', '=', 'posted'),
|
||||
('x_fc_adp_billing_status', 'in', ['submitted', 'resubmitted']),
|
||||
('adp_export_date', '>=', self.posting_period_start),
|
||||
],
|
||||
'target': 'current',
|
||||
}
|
||||
|
||||
def action_open_kpi_ar(self):
|
||||
return {
|
||||
'type': 'ir.actions.act_window',
|
||||
'name': 'Total AR (ADP)',
|
||||
'res_model': 'account.move',
|
||||
'view_mode': 'list,form',
|
||||
'domain': self._invoice_role_filter() + [
|
||||
('move_type', '=', 'out_invoice'),
|
||||
('state', '=', 'posted'),
|
||||
('x_fc_invoice_type', '=', 'adp'),
|
||||
('payment_state', 'in', ['not_paid', 'partial']),
|
||||
],
|
||||
'target': 'current',
|
||||
}
|
||||
|
||||
# =========================================================================
|
||||
# Create-SO hotlinks
|
||||
# =========================================================================
|
||||
def _create_so_action(self, name, ctx_extra):
|
||||
context = dict(self.env.context)
|
||||
context.update(ctx_extra)
|
||||
return {
|
||||
'type': 'ir.actions.act_window',
|
||||
'name': name,
|
||||
'res_model': 'sale.order',
|
||||
'view_mode': 'form',
|
||||
'view_id': False,
|
||||
'context': context,
|
||||
'target': 'current',
|
||||
}
|
||||
|
||||
def action_create_adp_so(self):
|
||||
return self._create_so_action('New ADP Order',
|
||||
{'default_x_fc_sale_type': 'adp'})
|
||||
|
||||
def action_create_mod_so(self):
|
||||
return self._create_so_action('New MOD Order',
|
||||
{'default_x_fc_sale_type': 'march_of_dimes'})
|
||||
|
||||
def action_create_odsp_so(self):
|
||||
return self._create_so_action('New ODSP Order', {
|
||||
'default_x_fc_sale_type': 'odsp',
|
||||
'default_x_fc_odsp_division': 'standard',
|
||||
})
|
||||
|
||||
def action_create_wsib_so(self):
|
||||
return self._create_so_action('New WSIB Order',
|
||||
{'default_x_fc_sale_type': 'wsib'})
|
||||
|
||||
def action_create_insurance_so(self):
|
||||
return self._create_so_action('New Insurance Order',
|
||||
{'default_x_fc_sale_type': 'insurance'})
|
||||
|
||||
def action_create_mdc_so(self):
|
||||
return self._create_so_action('New MDC Order',
|
||||
{'default_x_fc_sale_type': 'muscular_dystrophy'})
|
||||
|
||||
def action_create_hardship_so(self):
|
||||
return self._create_so_action('New Hardship Order',
|
||||
{'default_x_fc_sale_type': 'hardship'})
|
||||
|
||||
def action_create_private_so(self):
|
||||
return self._create_so_action('New Private Order',
|
||||
{'default_x_fc_sale_type': 'direct_private'})
|
||||
|
||||
63
fusion_claims/static/src/js/fc_posting_countdown.js
Normal file
63
fusion_claims/static/src/js/fc_posting_countdown.js
Normal file
@@ -0,0 +1,63 @@
|
||||
/** @odoo-module **/
|
||||
// Fusion Claims — Posting Period Countdown
|
||||
// Reads the submission_deadline_dt field, computes "Nd Xh to cutoff" client-side,
|
||||
// re-renders every 60 seconds, swaps colour class as the deadline approaches.
|
||||
// Copyright 2026 Nexa Systems Inc.
|
||||
// License OPL-1
|
||||
|
||||
import { Component, useState, onWillDestroy } from "@odoo/owl";
|
||||
import { registry } from "@web/core/registry";
|
||||
import { standardFieldProps } from "@web/views/fields/standard_field_props";
|
||||
|
||||
class FcPostingCountdown extends Component {
|
||||
static template = "fusion_claims.PostingCountdown";
|
||||
static props = { ...standardFieldProps };
|
||||
|
||||
setup() {
|
||||
this.state = useState({ text: "", level: "info" });
|
||||
this._render();
|
||||
this._timer = setInterval(() => this._render(), 60_000);
|
||||
onWillDestroy(() => {
|
||||
if (this._timer) {
|
||||
clearInterval(this._timer);
|
||||
this._timer = null;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
_render() {
|
||||
const deadline = this.props.record.data[this.props.name];
|
||||
if (!deadline) {
|
||||
this.state.text = "";
|
||||
this.state.level = "muted";
|
||||
return;
|
||||
}
|
||||
// Odoo provides a luxon DateTime for Datetime fields
|
||||
const now = luxon.DateTime.now();
|
||||
const diff = deadline.diff(now, ["days", "hours", "minutes"]).toObject();
|
||||
|
||||
if (diff.days < 0 || (diff.days === 0 && diff.hours < 0)) {
|
||||
this.state.text = "Cutoff passed";
|
||||
this.state.level = "muted";
|
||||
return;
|
||||
}
|
||||
|
||||
const days = Math.floor(diff.days);
|
||||
const hours = Math.floor(diff.hours);
|
||||
|
||||
if (days < 1) {
|
||||
this.state.text = `${hours}h to cutoff`;
|
||||
this.state.level = "danger";
|
||||
} else if (days < 3) {
|
||||
this.state.text = `${days}d ${hours}h to cutoff`;
|
||||
this.state.level = "warning";
|
||||
} else {
|
||||
this.state.text = `${days} days to cutoff`;
|
||||
this.state.level = "info";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
registry.category("fields").add("fc_posting_countdown", {
|
||||
component: FcPostingCountdown,
|
||||
});
|
||||
81
fusion_claims/static/src/scss/_fc_dashboard_tokens.scss
Normal file
81
fusion_claims/static/src/scss/_fc_dashboard_tokens.scss
Normal file
@@ -0,0 +1,81 @@
|
||||
// =============================================================================
|
||||
// Fusion Claims Dashboard — Palette Tokens
|
||||
// Compile-time branch on $o-webclient-color-scheme so the same SCSS file
|
||||
// produces different palettes in web.assets_backend (light) and
|
||||
// web.assets_web_dark (dark). Tokens load FIRST in each bundle.
|
||||
// =============================================================================
|
||||
|
||||
$o-webclient-color-scheme: bright !default;
|
||||
|
||||
// ---------- LIGHT (defaults) ----------
|
||||
$_fc-page-bg: #f7f7f8 !default;
|
||||
$_fc-card-bg: #ffffff !default;
|
||||
$_fc-card-border: #d8dadd !default;
|
||||
$_fc-text: #2b2b2b !default;
|
||||
$_fc-text-muted: #6c7480 !default;
|
||||
|
||||
$_fc-banner-from: #eef2ff !default;
|
||||
$_fc-banner-to: #fce7f3 !default;
|
||||
$_fc-banner-border: #c7d2fe !default;
|
||||
$_fc-banner-text: #3730a3 !default;
|
||||
$_fc-deadline-text: #b91c1c !default;
|
||||
|
||||
$_fc-kpi-bg: #f0f4ff !default;
|
||||
$_fc-kpi-border: #c7d2fe !default;
|
||||
$_fc-kpi-num: #1e3a8a !default;
|
||||
|
||||
$_fc-action-bg: #ecfdf5 !default;
|
||||
$_fc-action-border: #6ee7b7 !default;
|
||||
$_fc-action-text: #047857 !default;
|
||||
|
||||
$_fc-tile-bg: #f3f4f6 !default;
|
||||
$_fc-tile-border: #e5e7eb !default;
|
||||
$_fc-tile-num: #111827 !default;
|
||||
|
||||
$_fc-urgent-bg: #fee2e2 !default;
|
||||
$_fc-urgent-border: #fca5a5 !default;
|
||||
$_fc-urgent-num: #991b1b !default;
|
||||
$_fc-urgent-text: #7f1d1d !default;
|
||||
|
||||
$_fc-activity-bg: #fefce8 !default;
|
||||
$_fc-activity-border: #fde047 !default;
|
||||
$_fc-bottleneck-bg: #fef2f2 !default;
|
||||
$_fc-bottleneck-border: #fecaca !default;
|
||||
|
||||
// ---------- DARK overrides ----------
|
||||
@if $o-webclient-color-scheme == dark {
|
||||
$_fc-page-bg: #1a1d21 !global;
|
||||
$_fc-card-bg: #22262d !global;
|
||||
$_fc-card-border: #3a3f47 !global;
|
||||
$_fc-text: #e5e7eb !global;
|
||||
$_fc-text-muted: #9ca3af !global;
|
||||
|
||||
// Cool blue monochrome banner (selected option A from brainstorm)
|
||||
$_fc-banner-from: #1e293b !global;
|
||||
$_fc-banner-to: #1e3a5f !global;
|
||||
$_fc-banner-border: #3b82f6 !global;
|
||||
$_fc-banner-text: #93c5fd !global;
|
||||
$_fc-deadline-text: #fca5a5 !global;
|
||||
|
||||
$_fc-kpi-bg: #1e293b !global;
|
||||
$_fc-kpi-border: #334155 !global;
|
||||
$_fc-kpi-num: #93c5fd !global;
|
||||
|
||||
$_fc-action-bg: #064e3b !global;
|
||||
$_fc-action-border: #047857 !global;
|
||||
$_fc-action-text: #6ee7b7 !global;
|
||||
|
||||
$_fc-tile-bg: #2d3138 !global;
|
||||
$_fc-tile-border: #3a3f47 !global;
|
||||
$_fc-tile-num: #f3f4f6 !global;
|
||||
|
||||
$_fc-urgent-bg: #4a1414 !global;
|
||||
$_fc-urgent-border: #7f1d1d !global;
|
||||
$_fc-urgent-num: #fca5a5 !global;
|
||||
$_fc-urgent-text: #fecaca !global;
|
||||
|
||||
$_fc-activity-bg: #3a2e0a !global;
|
||||
$_fc-activity-border: #854d0e !global;
|
||||
$_fc-bottleneck-bg: #3a1414 !global;
|
||||
$_fc-bottleneck-border: #7f1d1d !global;
|
||||
}
|
||||
212
fusion_claims/static/src/scss/fc_dashboard.scss
Normal file
212
fusion_claims/static/src/scss/fc_dashboard.scss
Normal file
@@ -0,0 +1,212 @@
|
||||
// =============================================================================
|
||||
// Fusion Claims Dashboard — Layout & Section Styles
|
||||
// Consumes tokens from _fc_dashboard_tokens.scss (must load FIRST in bundle).
|
||||
// =============================================================================
|
||||
|
||||
.o_fc_dashboard {
|
||||
// Re-export tokens as CSS custom properties for devtools inspection
|
||||
--fc-page-bg: #{$_fc-page-bg};
|
||||
--fc-card-bg: #{$_fc-card-bg};
|
||||
--fc-card-border: #{$_fc-card-border};
|
||||
--fc-text: #{$_fc-text};
|
||||
--fc-text-muted: #{$_fc-text-muted};
|
||||
--fc-banner-from: #{$_fc-banner-from};
|
||||
--fc-banner-to: #{$_fc-banner-to};
|
||||
--fc-banner-border: #{$_fc-banner-border};
|
||||
--fc-banner-text: #{$_fc-banner-text};
|
||||
--fc-deadline-text: #{$_fc-deadline-text};
|
||||
--fc-kpi-bg: #{$_fc-kpi-bg};
|
||||
--fc-kpi-border: #{$_fc-kpi-border};
|
||||
--fc-kpi-num: #{$_fc-kpi-num};
|
||||
--fc-action-bg: #{$_fc-action-bg};
|
||||
--fc-action-border: #{$_fc-action-border};
|
||||
--fc-action-text: #{$_fc-action-text};
|
||||
--fc-tile-bg: #{$_fc-tile-bg};
|
||||
--fc-tile-border: #{$_fc-tile-border};
|
||||
--fc-tile-num: #{$_fc-tile-num};
|
||||
--fc-urgent-bg: #{$_fc-urgent-bg};
|
||||
--fc-urgent-border: #{$_fc-urgent-border};
|
||||
--fc-urgent-num: #{$_fc-urgent-num};
|
||||
--fc-urgent-text: #{$_fc-urgent-text};
|
||||
--fc-activity-bg: #{$_fc-activity-bg};
|
||||
--fc-activity-border: #{$_fc-activity-border};
|
||||
--fc-bottleneck-bg: #{$_fc-bottleneck-bg};
|
||||
--fc-bottleneck-border: #{$_fc-bottleneck-border};
|
||||
|
||||
background: var(--fc-page-bg);
|
||||
color: $_fc-text;
|
||||
|
||||
.o_fc_banner {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
background: linear-gradient(90deg, var(--fc-banner-from), var(--fc-banner-to));
|
||||
border: 1px solid var(--fc-banner-border);
|
||||
border-radius: 8px;
|
||||
padding: 10px 14px;
|
||||
font-weight: 600;
|
||||
color: var(--fc-banner-text);
|
||||
}
|
||||
.o_fc_banner__deadline { font-weight: 700; }
|
||||
|
||||
.o_fc_kpi {
|
||||
background: var(--fc-kpi-bg);
|
||||
border: 1px solid var(--fc-kpi-border);
|
||||
border-radius: 8px;
|
||||
padding: 14px 10px;
|
||||
text-align: center;
|
||||
transition: transform 0.15s ease;
|
||||
|
||||
&:hover { transform: translateY(-2px); }
|
||||
}
|
||||
.o_fc_kpi__num {
|
||||
display: block;
|
||||
font-size: 1.6rem;
|
||||
font-weight: 700;
|
||||
color: var(--fc-kpi-num);
|
||||
}
|
||||
.o_fc_kpi__lbl {
|
||||
display: block;
|
||||
font-size: 0.75rem;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
color: var(--fc-text-muted);
|
||||
margin-top: 2px;
|
||||
}
|
||||
|
||||
.o_fc_actions {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 6px;
|
||||
}
|
||||
.o_fc_pill {
|
||||
background: var(--fc-action-bg);
|
||||
border: 1px solid var(--fc-action-border);
|
||||
color: var(--fc-action-text);
|
||||
border-radius: 16px;
|
||||
padding: 5px 12px;
|
||||
font-size: 0.85rem;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: background 0.15s ease;
|
||||
|
||||
&:hover { background: var(--fc-action-border); }
|
||||
}
|
||||
|
||||
.o_fc_section {
|
||||
background: var(--fc-card-bg);
|
||||
border: 1px solid var(--fc-card-border);
|
||||
border-radius: 8px;
|
||||
padding: 10px 12px;
|
||||
}
|
||||
|
||||
.o_fc_h6 {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font-size: 0.9rem;
|
||||
font-weight: 700;
|
||||
margin-bottom: 8px;
|
||||
color: var(--fc-text);
|
||||
}
|
||||
.o_fc_tag {
|
||||
display: inline-block;
|
||||
font-size: 0.65rem;
|
||||
padding: 2px 7px;
|
||||
border-radius: 4px;
|
||||
background: var(--fc-banner-border);
|
||||
color: var(--fc-banner-text);
|
||||
margin-left: 8px;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.o_fc_tile {
|
||||
background: var(--fc-tile-bg);
|
||||
border: 1px solid var(--fc-tile-border);
|
||||
border-radius: 6px;
|
||||
padding: 8px 6px;
|
||||
text-align: center;
|
||||
font-size: 0.75rem;
|
||||
line-height: 1.3;
|
||||
cursor: pointer;
|
||||
transition: transform 0.15s ease, box-shadow 0.15s ease;
|
||||
|
||||
&:hover {
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
||||
}
|
||||
}
|
||||
.o_fc_tile__num {
|
||||
display: block;
|
||||
font-size: 1.3rem;
|
||||
font-weight: 700;
|
||||
color: var(--fc-tile-num);
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
.o_fc_tile--urgent {
|
||||
background: var(--fc-urgent-bg);
|
||||
border-color: var(--fc-urgent-border);
|
||||
color: var(--fc-urgent-text);
|
||||
|
||||
.o_fc_tile__num { color: var(--fc-urgent-num); }
|
||||
}
|
||||
|
||||
.o_fc_activities {
|
||||
background: var(--fc-activity-bg);
|
||||
border: 1px solid var(--fc-activity-border);
|
||||
border-radius: 8px;
|
||||
padding: 10px 12px;
|
||||
}
|
||||
.o_fc_activity_row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
padding: 4px 0;
|
||||
border-bottom: 1px dashed var(--fc-card-border);
|
||||
font-size: 0.85rem;
|
||||
|
||||
&:last-child { border-bottom: none; }
|
||||
}
|
||||
.o_fc_activity_overdue {
|
||||
color: var(--fc-urgent-text);
|
||||
font-weight: 600;
|
||||
}
|
||||
.o_fc_activity_deadline { color: var(--fc-text-muted); }
|
||||
.o_fc_empty {
|
||||
color: var(--fc-text-muted);
|
||||
font-style: italic;
|
||||
text-align: center;
|
||||
padding: 12px;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.o_fc_bottleneck {
|
||||
background: var(--fc-bottleneck-bg);
|
||||
border: 1px solid var(--fc-bottleneck-border);
|
||||
border-radius: 8px;
|
||||
padding: 10px 12px;
|
||||
}
|
||||
.o_fc_bottleneck_row {
|
||||
display: block;
|
||||
width: 100%;
|
||||
text-align: left;
|
||||
padding: 4px 0;
|
||||
color: var(--fc-text);
|
||||
text-decoration: none;
|
||||
|
||||
&:hover { color: var(--fc-urgent-num); text-decoration: underline; }
|
||||
}
|
||||
|
||||
// Countdown widget colour levels (driven by OWL state)
|
||||
.o_fc_countdown {
|
||||
display: inline-block;
|
||||
padding: 2px 8px;
|
||||
border-radius: 12px;
|
||||
font-weight: 700;
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
.o_fc_countdown--info { color: var(--fc-banner-text); }
|
||||
.o_fc_countdown--warning { color: #d97706; } // amber (intentional fixed hex)
|
||||
.o_fc_countdown--danger { color: var(--fc-urgent-num); }
|
||||
.o_fc_countdown--muted { color: var(--fc-text-muted); font-style: italic; }
|
||||
}
|
||||
7
fusion_claims/static/src/xml/fc_posting_countdown.xml
Normal file
7
fusion_claims/static/src/xml/fc_posting_countdown.xml
Normal file
@@ -0,0 +1,7 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<templates xml:space="preserve">
|
||||
<t t-name="fusion_claims.PostingCountdown">
|
||||
<span t-attf-class="o_fc_countdown o_fc_countdown--{{state.level}}"
|
||||
t-esc="state.text"/>
|
||||
</t>
|
||||
</templates>
|
||||
@@ -34,6 +34,36 @@ class TestFusionClaimsDashboard(TransactionCase):
|
||||
|
||||
cls.partner = cls.Partner.create({'name': 'Test Client'})
|
||||
|
||||
@classmethod
|
||||
def _make_invoice(cls, user, billing_status, amount=1000.0,
|
||||
exported=False, export_date=None,
|
||||
invoice_type='adp', payment_state='not_paid'):
|
||||
"""Helper: create a posted ADP invoice linked to an SO owned by `user`."""
|
||||
so = cls.env['sale.order'].with_context(skip_status_validation=True).create({
|
||||
'partner_id': cls.partner.id,
|
||||
'user_id': user.id,
|
||||
'x_fc_sale_type': 'adp',
|
||||
'x_fc_adp_application_status': 'approved',
|
||||
})
|
||||
invoice = cls.env['account.move'].with_context(skip_sync=True).create({
|
||||
'move_type': 'out_invoice',
|
||||
'partner_id': cls.partner.id,
|
||||
'x_fc_source_sale_order_id': so.id,
|
||||
'x_fc_invoice_type': invoice_type,
|
||||
'x_fc_adp_billing_status': billing_status,
|
||||
'adp_exported': exported,
|
||||
'adp_export_date': export_date,
|
||||
'invoice_line_ids': [(0, 0, {
|
||||
'name': 'Test line',
|
||||
'quantity': 1.0,
|
||||
'price_unit': amount,
|
||||
'tax_ids': [(5, 0)], # clear taxes so amount_total == price_unit
|
||||
})],
|
||||
})
|
||||
invoice.action_post()
|
||||
invoice.with_context(skip_sync=True).write({'payment_state': payment_state})
|
||||
return invoice
|
||||
|
||||
def test_dashboard_record_creates(self):
|
||||
dashboard = self.Dashboard.create({})
|
||||
self.assertTrue(dashboard.id, "Dashboard record should be creatable")
|
||||
@@ -57,3 +87,281 @@ class TestFusionClaimsDashboard(TransactionCase):
|
||||
def test_is_manager_false_for_salesrep(self):
|
||||
dashboard = self.Dashboard.with_user(self.salesrep).create({})
|
||||
self.assertFalse(dashboard.is_manager)
|
||||
|
||||
# -------------------------------------------------------------------------
|
||||
# Task 2 — Banner
|
||||
# -------------------------------------------------------------------------
|
||||
def test_banner_posting_period_label_format(self):
|
||||
dashboard = self.Dashboard.with_user(self.manager).create({})
|
||||
label = dashboard.posting_period_label
|
||||
self.assertTrue(any(month in label
|
||||
for month in ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun',
|
||||
'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec']),
|
||||
"Label should contain a month abbreviation")
|
||||
|
||||
def test_banner_posting_period_start_and_end_are_dates(self):
|
||||
dashboard = self.Dashboard.with_user(self.manager).create({})
|
||||
self.assertTrue(dashboard.posting_period_start)
|
||||
self.assertTrue(dashboard.posting_period_end)
|
||||
delta = (dashboard.posting_period_end - dashboard.posting_period_start).days
|
||||
self.assertEqual(delta, 14)
|
||||
|
||||
def test_banner_submission_deadline_is_wednesday_6pm(self):
|
||||
dashboard = self.Dashboard.with_user(self.manager).create({})
|
||||
deadline = dashboard.submission_deadline_dt
|
||||
self.assertTrue(deadline, "Deadline should be set")
|
||||
# Stored in UTC; convert to user's TZ to assert the wall-clock weekday/hour
|
||||
import pytz
|
||||
tz = pytz.timezone(self.manager.tz or 'America/Toronto')
|
||||
local = pytz.UTC.localize(deadline).astimezone(tz)
|
||||
self.assertEqual(local.weekday(), 2, "Deadline should be Wednesday")
|
||||
self.assertEqual(local.hour, 18, "Deadline should be 18:00 (6 PM)")
|
||||
|
||||
def test_is_pre_first_posting_false_when_today_is_past_base_date(self):
|
||||
# Test runs after 2026-01-23 by default.
|
||||
dashboard = self.Dashboard.with_user(self.manager).create({})
|
||||
self.assertFalse(dashboard.is_pre_first_posting)
|
||||
|
||||
# -------------------------------------------------------------------------
|
||||
# Task 3 — KPI tiles
|
||||
# -------------------------------------------------------------------------
|
||||
def test_kpi_ready_counts_waiting_invoices_not_exported(self):
|
||||
self._make_invoice(self.manager, 'waiting', amount=500.0, exported=False)
|
||||
dashboard = self.Dashboard.with_user(self.manager).create({})
|
||||
self.assertEqual(dashboard.kpi_ready_count, 1)
|
||||
self.assertAlmostEqual(dashboard.kpi_ready_amount, 500.0, places=2)
|
||||
|
||||
def test_kpi_ready_excludes_already_exported(self):
|
||||
from datetime import date
|
||||
self._make_invoice(self.manager, 'waiting', amount=500.0,
|
||||
exported=True, export_date=date.today())
|
||||
dashboard = self.Dashboard.with_user(self.manager).create({})
|
||||
self.assertEqual(dashboard.kpi_ready_count, 0)
|
||||
self.assertAlmostEqual(dashboard.kpi_ready_amount, 0.0, places=2)
|
||||
|
||||
def test_kpi_claimed_counts_exported_in_current_period(self):
|
||||
dashboard = self.Dashboard.with_user(self.manager).create({})
|
||||
in_period_date = dashboard.posting_period_start
|
||||
self._make_invoice(self.manager, 'submitted', amount=700.0,
|
||||
exported=True, export_date=in_period_date)
|
||||
dashboard2 = self.Dashboard.with_user(self.manager).create({})
|
||||
self.assertEqual(dashboard2.kpi_claimed_count, 1)
|
||||
self.assertAlmostEqual(dashboard2.kpi_claimed_amount, 700.0, places=2)
|
||||
|
||||
def test_kpi_ar_counts_posted_unpaid_adp_invoices(self):
|
||||
self._make_invoice(self.manager, 'submitted', amount=2000.0,
|
||||
exported=True, payment_state='not_paid')
|
||||
dashboard = self.Dashboard.with_user(self.manager).create({})
|
||||
self.assertEqual(dashboard.kpi_ar_count, 1)
|
||||
self.assertAlmostEqual(dashboard.kpi_ar_amount, 2000.0, places=2)
|
||||
|
||||
def test_kpi_ready_respects_role_filter(self):
|
||||
self._make_invoice(self.manager, 'waiting', amount=500.0)
|
||||
dashboard_rep = self.Dashboard.with_user(self.salesrep).create({})
|
||||
self.assertEqual(dashboard_rep.kpi_ready_count, 0,
|
||||
"Salesrep must not see manager's invoice")
|
||||
|
||||
# -------------------------------------------------------------------------
|
||||
# Task 4 — Activities + bottlenecks
|
||||
# -------------------------------------------------------------------------
|
||||
def test_my_activities_count_zero_when_none(self):
|
||||
dashboard = self.Dashboard.with_user(self.manager).create({})
|
||||
self.assertEqual(dashboard.my_activities_count, 0)
|
||||
|
||||
def test_my_activities_count_picks_up_user_activity(self):
|
||||
so = self.env['sale.order'].with_context(skip_status_validation=True).create({
|
||||
'partner_id': self.partner.id,
|
||||
'user_id': self.manager.id,
|
||||
'x_fc_sale_type': 'adp',
|
||||
})
|
||||
self.env['mail.activity'].create({
|
||||
'res_model_id': self.env['ir.model']._get('sale.order').id,
|
||||
'res_id': so.id,
|
||||
'res_model': 'sale.order',
|
||||
'user_id': self.manager.id,
|
||||
'activity_type_id': self.env.ref('mail.mail_activity_data_todo').id,
|
||||
'summary': 'Test activity',
|
||||
})
|
||||
dashboard = self.Dashboard.with_user(self.manager).create({})
|
||||
self.assertEqual(dashboard.my_activities_count, 1)
|
||||
self.assertIn('Test activity', dashboard.my_activities_html or '')
|
||||
|
||||
def test_bottleneck_no_pod_count(self):
|
||||
self.env['sale.order'].with_context(skip_status_validation=True).create({
|
||||
'partner_id': self.partner.id,
|
||||
'user_id': self.manager.id,
|
||||
'x_fc_sale_type': 'adp',
|
||||
'x_fc_adp_application_status': 'approved',
|
||||
})
|
||||
dashboard = self.Dashboard.with_user(self.manager).create({})
|
||||
self.assertEqual(dashboard.bottleneck_no_pod_count, 1)
|
||||
|
||||
def test_bottleneck_no_response_count(self):
|
||||
from datetime import date, timedelta
|
||||
old_date = date.today() - timedelta(days=20)
|
||||
self.env['sale.order'].with_context(skip_status_validation=True).create({
|
||||
'partner_id': self.partner.id,
|
||||
'user_id': self.manager.id,
|
||||
'x_fc_sale_type': 'adp',
|
||||
'x_fc_adp_application_status': 'submitted',
|
||||
'x_fc_claim_submission_date': old_date,
|
||||
})
|
||||
dashboard = self.Dashboard.with_user(self.manager).create({})
|
||||
self.assertEqual(dashboard.bottleneck_no_response_count, 1)
|
||||
|
||||
# -------------------------------------------------------------------------
|
||||
# Task 5 — Other funder counts
|
||||
# -------------------------------------------------------------------------
|
||||
def test_other_funder_counts_segregate_by_sale_type(self):
|
||||
SO = self.env['sale.order'].with_context(skip_status_validation=True)
|
||||
SO.create({'partner_id': self.partner.id, 'user_id': self.manager.id,
|
||||
'x_fc_sale_type': 'odsp'})
|
||||
SO.create({'partner_id': self.partner.id, 'user_id': self.manager.id,
|
||||
'x_fc_sale_type': 'wsib'})
|
||||
SO.create({'partner_id': self.partner.id, 'user_id': self.manager.id,
|
||||
'x_fc_sale_type': 'insurance'})
|
||||
SO.create({'partner_id': self.partner.id, 'user_id': self.manager.id,
|
||||
'x_fc_sale_type': 'muscular_dystrophy'})
|
||||
SO.create({'partner_id': self.partner.id, 'user_id': self.manager.id,
|
||||
'x_fc_sale_type': 'hardship'})
|
||||
SO.create({'partner_id': self.partner.id, 'user_id': self.manager.id,
|
||||
'x_fc_sale_type': 'adp', 'x_fc_client_type': 'ACS'})
|
||||
dashboard = self.Dashboard.with_user(self.manager).create({})
|
||||
self.assertEqual(dashboard.count_odsp, 1)
|
||||
self.assertEqual(dashboard.count_wsib, 1)
|
||||
self.assertEqual(dashboard.count_insurance, 1)
|
||||
self.assertEqual(dashboard.count_mdc, 1)
|
||||
self.assertEqual(dashboard.count_hardship, 1)
|
||||
self.assertEqual(dashboard.count_acsd, 1)
|
||||
|
||||
def test_other_funder_counts_exclude_cancelled(self):
|
||||
so = self.env['sale.order'].with_context(skip_status_validation=True).create({
|
||||
'partner_id': self.partner.id, 'user_id': self.manager.id,
|
||||
'x_fc_sale_type': 'wsib',
|
||||
})
|
||||
so.with_context(skip_status_validation=True).write({'state': 'cancel'})
|
||||
dashboard = self.Dashboard.with_user(self.manager).create({})
|
||||
self.assertEqual(dashboard.count_wsib, 0)
|
||||
|
||||
# -------------------------------------------------------------------------
|
||||
# Task 6 — ADP + MOD workflow counts
|
||||
# -------------------------------------------------------------------------
|
||||
def test_adp_pre_approval_tile_counts(self):
|
||||
SO = self.env['sale.order'].with_context(skip_status_validation=True)
|
||||
SO.create({'partner_id': self.partner.id, 'user_id': self.manager.id,
|
||||
'x_fc_sale_type': 'adp',
|
||||
'x_fc_adp_application_status': 'waiting_for_application'})
|
||||
SO.create({'partner_id': self.partner.id, 'user_id': self.manager.id,
|
||||
'x_fc_sale_type': 'adp',
|
||||
'x_fc_adp_application_status': 'application_received'})
|
||||
SO.create({'partner_id': self.partner.id, 'user_id': self.manager.id,
|
||||
'x_fc_sale_type': 'adp',
|
||||
'x_fc_adp_application_status': 'ready_submission'})
|
||||
SO.create({'partner_id': self.partner.id, 'user_id': self.manager.id,
|
||||
'x_fc_sale_type': 'adp',
|
||||
'x_fc_adp_application_status': 'needs_correction'})
|
||||
dashboard = self.Dashboard.with_user(self.manager).create({})
|
||||
self.assertEqual(dashboard.adp_waiting_app_count, 1)
|
||||
self.assertEqual(dashboard.adp_app_received_count, 1)
|
||||
self.assertEqual(dashboard.adp_ready_submit_count, 1)
|
||||
self.assertEqual(dashboard.adp_needs_correction_count, 1)
|
||||
|
||||
def test_adp_post_approval_tile_counts(self):
|
||||
SO = self.env['sale.order'].with_context(skip_status_validation=True)
|
||||
SO.create({'partner_id': self.partner.id, 'user_id': self.manager.id,
|
||||
'x_fc_sale_type': 'adp',
|
||||
'x_fc_adp_application_status': 'approved'})
|
||||
SO.create({'partner_id': self.partner.id, 'user_id': self.manager.id,
|
||||
'x_fc_sale_type': 'adp',
|
||||
'x_fc_adp_application_status': 'ready_delivery'})
|
||||
SO.create({'partner_id': self.partner.id, 'user_id': self.manager.id,
|
||||
'x_fc_sale_type': 'adp',
|
||||
'x_fc_adp_application_status': 'ready_bill'})
|
||||
SO.create({'partner_id': self.partner.id, 'user_id': self.manager.id,
|
||||
'x_fc_sale_type': 'adp',
|
||||
'x_fc_adp_application_status': 'on_hold'})
|
||||
dashboard = self.Dashboard.with_user(self.manager).create({})
|
||||
self.assertEqual(dashboard.adp_approved_count, 1)
|
||||
self.assertEqual(dashboard.adp_ready_delivery_count, 1)
|
||||
self.assertEqual(dashboard.adp_ready_bill_count, 1)
|
||||
self.assertEqual(dashboard.adp_on_hold_count, 1)
|
||||
|
||||
def test_mod_tile_counts(self):
|
||||
SO = self.env['sale.order'].with_context(skip_status_validation=True)
|
||||
for status in ('awaiting_funding', 'funding_approved', 'contract_received',
|
||||
'project_complete', 'pod_submitted'):
|
||||
SO.create({'partner_id': self.partner.id, 'user_id': self.manager.id,
|
||||
'x_fc_sale_type': 'march_of_dimes',
|
||||
'x_fc_mod_status': status})
|
||||
dashboard = self.Dashboard.with_user(self.manager).create({})
|
||||
self.assertEqual(dashboard.mod_awaiting_funding_count, 1)
|
||||
self.assertEqual(dashboard.mod_funding_approved_count, 1)
|
||||
self.assertEqual(dashboard.mod_pca_received_count, 1)
|
||||
self.assertEqual(dashboard.mod_project_complete_count, 1)
|
||||
self.assertEqual(dashboard.mod_pod_submitted_count, 1)
|
||||
|
||||
# -------------------------------------------------------------------------
|
||||
# Task 7 — Open-list action methods
|
||||
# -------------------------------------------------------------------------
|
||||
def test_action_open_adp_waiting_app_returns_correct_domain(self):
|
||||
dashboard = self.Dashboard.with_user(self.manager).create({})
|
||||
action = dashboard.action_open_adp_waiting_app()
|
||||
self.assertEqual(action['res_model'], 'sale.order')
|
||||
self.assertIn(('x_fc_adp_application_status', 'in',
|
||||
['waiting_for_application', 'assessment_completed']),
|
||||
action['domain'])
|
||||
|
||||
def test_action_open_bottleneck_no_pod_returns_correct_domain(self):
|
||||
dashboard = self.Dashboard.with_user(self.manager).create({})
|
||||
action = dashboard.action_open_bottleneck_no_pod()
|
||||
self.assertEqual(action['res_model'], 'sale.order')
|
||||
self.assertIn(('x_fc_proof_of_delivery', '=', False), action['domain'])
|
||||
|
||||
def test_action_open_mod_awaiting_funding_returns_correct_domain(self):
|
||||
dashboard = self.Dashboard.with_user(self.manager).create({})
|
||||
action = dashboard.action_open_mod_awaiting_funding()
|
||||
self.assertEqual(action['res_model'], 'sale.order')
|
||||
self.assertIn(('x_fc_mod_status', '=', 'awaiting_funding'), action['domain'])
|
||||
|
||||
def test_action_open_my_activities_returns_activity_model(self):
|
||||
dashboard = self.Dashboard.with_user(self.manager).create({})
|
||||
action = dashboard.action_open_my_activities()
|
||||
self.assertEqual(action['res_model'], 'mail.activity')
|
||||
|
||||
# -------------------------------------------------------------------------
|
||||
# Task 8 — Create-SO hotlinks
|
||||
# -------------------------------------------------------------------------
|
||||
def test_action_create_adp_so_has_default_sale_type(self):
|
||||
dashboard = self.Dashboard.with_user(self.manager).create({})
|
||||
action = dashboard.action_create_adp_so()
|
||||
self.assertEqual(action['res_model'], 'sale.order')
|
||||
self.assertEqual(action['view_mode'], 'form')
|
||||
self.assertEqual(action['context']['default_x_fc_sale_type'], 'adp')
|
||||
|
||||
def test_action_create_mod_so_has_default_sale_type(self):
|
||||
dashboard = self.Dashboard.with_user(self.manager).create({})
|
||||
action = dashboard.action_create_mod_so()
|
||||
self.assertEqual(action['context']['default_x_fc_sale_type'], 'march_of_dimes')
|
||||
|
||||
def test_action_create_odsp_so_has_division_default(self):
|
||||
dashboard = self.Dashboard.with_user(self.manager).create({})
|
||||
action = dashboard.action_create_odsp_so()
|
||||
self.assertEqual(action['context']['default_x_fc_sale_type'], 'odsp')
|
||||
self.assertEqual(action['context']['default_x_fc_odsp_division'], 'standard')
|
||||
|
||||
def test_all_create_so_actions_exist(self):
|
||||
dashboard = self.Dashboard.with_user(self.manager).create({})
|
||||
for method_name, expected_type in [
|
||||
('action_create_adp_so', 'adp'),
|
||||
('action_create_mod_so', 'march_of_dimes'),
|
||||
('action_create_odsp_so', 'odsp'),
|
||||
('action_create_wsib_so', 'wsib'),
|
||||
('action_create_insurance_so', 'insurance'),
|
||||
('action_create_mdc_so', 'muscular_dystrophy'),
|
||||
('action_create_hardship_so', 'hardship'),
|
||||
('action_create_private_so', 'direct_private'),
|
||||
]:
|
||||
action = getattr(dashboard, method_name)()
|
||||
self.assertEqual(action['res_model'], 'sale.order')
|
||||
self.assertEqual(action['context']['default_x_fc_sale_type'], expected_type,
|
||||
f"{method_name} returned wrong default sale type")
|
||||
|
||||
@@ -4,157 +4,384 @@
|
||||
<field name="name">fusion.claims.dashboard.form</field>
|
||||
<field name="model">fusion.claims.dashboard</field>
|
||||
<field name="arch" type="xml">
|
||||
<form string="Dashboard" create="0" delete="0">
|
||||
<form string="Dashboard" create="0" delete="0" edit="0"
|
||||
class="o_fc_dashboard">
|
||||
<sheet>
|
||||
<!-- ===== FUNDING CARDS (one line, bigger) ===== -->
|
||||
<div class="d-flex flex-nowrap gap-2 mb-4 overflow-auto">
|
||||
<div invisible="adp_count == 0" style="flex: 1 1 0; min-width: 120px;">
|
||||
<button name="action_open_adp" type="object" class="btn p-0 w-100 border-0">
|
||||
<div class="text-white text-center py-3 px-2" style="background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); border-radius: 14px;">
|
||||
<div class="fw-bold" style="font-size: 1.8rem;"><field name="adp_count"/></div>
|
||||
<div style="font-size: 0.85rem;">ADP</div>
|
||||
</div>
|
||||
</button>
|
||||
|
||||
<!-- Hidden invariants used by buttons + widgets -->
|
||||
<field name="currency_id" invisible="1"/>
|
||||
<field name="posting_period_start" invisible="1"/>
|
||||
<field name="is_manager" invisible="1"/>
|
||||
<field name="is_pre_first_posting" invisible="1"/>
|
||||
|
||||
<!-- BANNER -->
|
||||
<div class="o_fc_banner mb-3">
|
||||
<div class="o_fc_banner__label">
|
||||
<i class="fa fa-calendar me-2"/>
|
||||
<span>Posting Period: </span>
|
||||
<field name="posting_period_label" nolabel="1"
|
||||
class="fw-bold"/>
|
||||
</div>
|
||||
<div invisible="odsp_count == 0" style="flex: 1 1 0; min-width: 120px;">
|
||||
<button name="action_open_odsp" type="object" class="btn p-0 w-100 border-0">
|
||||
<div class="text-white text-center py-3 px-2" style="background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%); border-radius: 14px;">
|
||||
<div class="fw-bold" style="font-size: 1.8rem;"><field name="odsp_count"/></div>
|
||||
<div style="font-size: 0.85rem;">ODSP</div>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
<div invisible="march_of_dimes_count == 0" style="flex: 1 1 0; min-width: 120px;">
|
||||
<button name="action_open_march" type="object" class="btn p-0 w-100 border-0">
|
||||
<div class="text-white text-center py-3 px-2" style="background: linear-gradient(135deg, #4facfe 0%, #00f2fe 100%); border-radius: 14px;">
|
||||
<div class="fw-bold" style="font-size: 1.8rem;"><field name="march_of_dimes_count"/></div>
|
||||
<div style="font-size: 0.85rem;">March of Dimes</div>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
<div invisible="hardship_count == 0" style="flex: 1 1 0; min-width: 120px;">
|
||||
<button name="action_open_hardship" type="object" class="btn p-0 w-100 border-0">
|
||||
<div class="text-white text-center py-3 px-2" style="background: linear-gradient(135deg, #43e97b 0%, #38f9d7 100%); border-radius: 14px;">
|
||||
<div class="fw-bold" style="font-size: 1.8rem;"><field name="hardship_count"/></div>
|
||||
<div style="font-size: 0.85rem;">Hardship</div>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
<div invisible="acsd_count == 0" style="flex: 1 1 0; min-width: 120px;">
|
||||
<button name="action_open_acsd" type="object" class="btn p-0 w-100 border-0">
|
||||
<div class="text-white text-center py-3 px-2" style="background: linear-gradient(135deg, #fa709a 0%, #fee140 100%); border-radius: 14px;">
|
||||
<div class="fw-bold" style="font-size: 1.8rem;"><field name="acsd_count"/></div>
|
||||
<div style="font-size: 0.85rem;">ACSD</div>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
<div invisible="muscular_dystrophy_count == 0" style="flex: 1 1 0; min-width: 120px;">
|
||||
<button name="action_open_muscular" type="object" class="btn p-0 w-100 border-0">
|
||||
<div class="text-white text-center py-3 px-2" style="background: linear-gradient(135deg, #a18cd1 0%, #fbc2eb 100%); border-radius: 14px;">
|
||||
<div class="fw-bold" style="font-size: 1.8rem;"><field name="muscular_dystrophy_count"/></div>
|
||||
<div style="font-size: 0.85rem;">Muscular Dystrophy</div>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
<div invisible="insurance_count == 0" style="flex: 1 1 0; min-width: 120px;">
|
||||
<button name="action_open_insurance" type="object" class="btn p-0 w-100 border-0">
|
||||
<div class="text-dark text-center py-3 px-2" style="background: linear-gradient(135deg, #ffecd2 0%, #fcb69f 100%); border-radius: 14px;">
|
||||
<div class="fw-bold" style="font-size: 1.8rem;"><field name="insurance_count"/></div>
|
||||
<div style="font-size: 0.85rem;">Insurance</div>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
<div invisible="wsib_count == 0" style="flex: 1 1 0; min-width: 120px;">
|
||||
<button name="action_open_wsib" type="object" class="btn p-0 w-100 border-0">
|
||||
<div class="text-dark text-center py-3 px-2" style="background: linear-gradient(135deg, #ff9a9e 0%, #fad0c4 100%); border-radius: 14px;">
|
||||
<div class="fw-bold" style="font-size: 1.8rem;"><field name="wsib_count"/></div>
|
||||
<div style="font-size: 0.85rem;">WSIB</div>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
<div invisible="total_profiles == 0" style="flex: 1 1 0; min-width: 120px;">
|
||||
<button name="action_open_profiles" type="object" class="btn p-0 w-100 border-0">
|
||||
<div class="text-white text-center py-3 px-2" style="background: linear-gradient(135deg, #30cfd0 0%, #330867 100%); border-radius: 14px;">
|
||||
<div class="fw-bold" style="font-size: 1.8rem;"><field name="total_profiles"/></div>
|
||||
<div style="font-size: 0.85rem;">Profiles</div>
|
||||
</div>
|
||||
</button>
|
||||
<div class="o_fc_banner__deadline">
|
||||
<field name="submission_deadline_dt"
|
||||
widget="fc_posting_countdown"
|
||||
nolabel="1" readonly="1"/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ===== PANEL SELECTORS (4 dropdowns) ===== -->
|
||||
<!-- "Showing your cases" hint when role-filtered -->
|
||||
<div class="alert alert-info py-2 mb-2"
|
||||
invisible="is_manager">
|
||||
Showing your assigned cases only.
|
||||
</div>
|
||||
|
||||
<!-- KPI TILES (3-up) -->
|
||||
<div class="row g-2 mb-3">
|
||||
<div class="col-3">
|
||||
<div class="fw-bold mb-1" style="font-size: 0.8rem;">Window 1</div>
|
||||
<field name="panel1_type" nolabel="1"/>
|
||||
<div class="col-12 col-md-4">
|
||||
<button name="action_open_kpi_ready" type="object"
|
||||
class="btn p-0 w-100 border-0">
|
||||
<div class="o_fc_kpi">
|
||||
<span class="o_fc_kpi__num">
|
||||
<field name="kpi_ready_amount"
|
||||
widget="monetary" nolabel="1"
|
||||
options="{'currency_field': 'currency_id'}"/>
|
||||
</span>
|
||||
<span class="o_fc_kpi__lbl">Ready to Claim
|
||||
(<field name="kpi_ready_count" nolabel="1"/>)
|
||||
</span>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
<div class="col-3">
|
||||
<div class="fw-bold mb-1" style="font-size: 0.8rem;">Window 2</div>
|
||||
<field name="panel2_type" nolabel="1"/>
|
||||
<div class="col-12 col-md-4">
|
||||
<button name="action_open_kpi_claimed" type="object"
|
||||
class="btn p-0 w-100 border-0">
|
||||
<div class="o_fc_kpi">
|
||||
<span class="o_fc_kpi__num">
|
||||
<field name="kpi_claimed_amount"
|
||||
widget="monetary" nolabel="1"
|
||||
options="{'currency_field': 'currency_id'}"/>
|
||||
</span>
|
||||
<span class="o_fc_kpi__lbl">Claimed This Period
|
||||
(<field name="kpi_claimed_count" nolabel="1"/>)
|
||||
</span>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
<div class="col-3">
|
||||
<div class="fw-bold mb-1" style="font-size: 0.8rem;">Window 3</div>
|
||||
<field name="panel3_type" nolabel="1"/>
|
||||
</div>
|
||||
<div class="col-3">
|
||||
<div class="fw-bold mb-1" style="font-size: 0.8rem;">Window 4</div>
|
||||
<field name="panel4_type" nolabel="1"/>
|
||||
<div class="col-12 col-md-4">
|
||||
<button name="action_open_kpi_ar" type="object"
|
||||
class="btn p-0 w-100 border-0">
|
||||
<div class="o_fc_kpi">
|
||||
<span class="o_fc_kpi__num">
|
||||
<field name="kpi_ar_amount"
|
||||
widget="monetary" nolabel="1"
|
||||
options="{'currency_field': 'currency_id'}"/>
|
||||
</span>
|
||||
<span class="o_fc_kpi__lbl">Total AR
|
||||
(<field name="kpi_ar_count" nolabel="1"/>)
|
||||
</span>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ===== TOP PANELS ROW 1 ===== -->
|
||||
<div class="row g-3 mb-3">
|
||||
<div class="col-12 col-lg-6">
|
||||
<div class="card" style="border-radius: 14px; overflow: hidden;">
|
||||
<div class="card-header fw-bold text-white py-2" style="background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);">
|
||||
<field name="panel1_title" nolabel="1"/>
|
||||
</div>
|
||||
<div class="card-body p-0" style="max-height: 400px; overflow-y: auto;">
|
||||
<field name="panel1_html" class="w-100" nolabel="1"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-12 col-lg-6">
|
||||
<div class="card" style="border-radius: 14px; overflow: hidden;">
|
||||
<div class="card-header fw-bold text-white py-2" style="background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%);">
|
||||
<field name="panel2_title" nolabel="1"/>
|
||||
</div>
|
||||
<div class="card-body p-0" style="max-height: 400px; overflow-y: auto;">
|
||||
<field name="panel2_html" class="w-100" nolabel="1"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- QUICK ACTION PILLS -->
|
||||
<div class="o_fc_actions mb-3">
|
||||
<button name="action_create_adp_so" type="object"
|
||||
class="o_fc_pill">+ ADP</button>
|
||||
<button name="action_create_mod_so" type="object"
|
||||
class="o_fc_pill">+ MOD</button>
|
||||
<button name="action_create_odsp_so" type="object"
|
||||
class="o_fc_pill">+ ODSP</button>
|
||||
<button name="action_create_wsib_so" type="object"
|
||||
class="o_fc_pill">+ WSIB</button>
|
||||
<button name="action_create_insurance_so" type="object"
|
||||
class="o_fc_pill">+ Insurance</button>
|
||||
<button name="action_create_mdc_so" type="object"
|
||||
class="o_fc_pill">+ MDC</button>
|
||||
<button name="action_create_hardship_so" type="object"
|
||||
class="o_fc_pill">+ Hardship</button>
|
||||
<button name="action_create_private_so" type="object"
|
||||
class="o_fc_pill">+ Private</button>
|
||||
</div>
|
||||
|
||||
<!-- ===== TOP PANELS ROW 2 ===== -->
|
||||
<!-- 2-COLUMN GRID -->
|
||||
<div class="row g-3">
|
||||
<div class="col-12 col-lg-6">
|
||||
<div class="card" style="border-radius: 14px; overflow: hidden;">
|
||||
<div class="card-header fw-bold text-white py-2" style="background: linear-gradient(135deg, #4facfe 0%, #00f2fe 100%);">
|
||||
<field name="panel3_title" nolabel="1"/>
|
||||
</div>
|
||||
<div class="card-body p-0" style="max-height: 400px; overflow-y: auto;">
|
||||
<field name="panel3_html" class="w-100" nolabel="1"/>
|
||||
|
||||
<!-- LEFT COLUMN -->
|
||||
<div class="col-12 col-lg-5">
|
||||
|
||||
<!-- Your Activities -->
|
||||
<div class="o_fc_activities mb-3">
|
||||
<h6 class="o_fc_h6">
|
||||
<i class="fa fa-thumb-tack me-2"/>
|
||||
Your Activities
|
||||
<span class="o_fc_tag">
|
||||
<field name="my_activities_count" nolabel="1"/>
|
||||
</span>
|
||||
<button name="action_open_my_activities" type="object"
|
||||
class="btn btn-link btn-sm ms-auto p-0">
|
||||
View all
|
||||
</button>
|
||||
</h6>
|
||||
<field name="my_activities_html" nolabel="1"/>
|
||||
</div>
|
||||
|
||||
<!-- Bottlenecks -->
|
||||
<div class="o_fc_bottleneck mb-3">
|
||||
<h6 class="o_fc_h6">
|
||||
<i class="fa fa-exclamation-triangle me-2"/>
|
||||
Bottlenecks
|
||||
</h6>
|
||||
<button name="action_open_bottleneck_no_pod" type="object"
|
||||
class="o_fc_bottleneck_row btn btn-link p-0">
|
||||
Approved without POD:
|
||||
<span class="fw-bold ms-1">
|
||||
<field name="bottleneck_no_pod_count" nolabel="1"/>
|
||||
</span>
|
||||
</button>
|
||||
<button name="action_open_bottleneck_no_response" type="object"
|
||||
class="o_fc_bottleneck_row btn btn-link p-0">
|
||||
Submitted > 14d, no response:
|
||||
<span class="fw-bold ms-1">
|
||||
<field name="bottleneck_no_response_count" nolabel="1"/>
|
||||
</span>
|
||||
</button>
|
||||
</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">
|
||||
<button name="action_open_odsp_cases" type="object"
|
||||
class="btn p-0 w-100 border-0">
|
||||
<div class="o_fc_tile">
|
||||
<span class="o_fc_tile__num">
|
||||
<field name="count_odsp" nolabel="1"/>
|
||||
</span>ODSP
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
<div class="col-4">
|
||||
<button name="action_open_wsib_cases" type="object"
|
||||
class="btn p-0 w-100 border-0">
|
||||
<div class="o_fc_tile">
|
||||
<span class="o_fc_tile__num">
|
||||
<field name="count_wsib" nolabel="1"/>
|
||||
</span>WSIB
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
<div class="col-4">
|
||||
<button name="action_open_insurance_cases" type="object"
|
||||
class="btn p-0 w-100 border-0">
|
||||
<div class="o_fc_tile">
|
||||
<span class="o_fc_tile__num">
|
||||
<field name="count_insurance" nolabel="1"/>
|
||||
</span>Insurance
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
<div class="col-4">
|
||||
<button name="action_open_mdc_cases" type="object"
|
||||
class="btn p-0 w-100 border-0">
|
||||
<div class="o_fc_tile">
|
||||
<span class="o_fc_tile__num">
|
||||
<field name="count_mdc" nolabel="1"/>
|
||||
</span>MDC
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
<div class="col-4">
|
||||
<button name="action_open_hardship_cases" type="object"
|
||||
class="btn p-0 w-100 border-0">
|
||||
<div class="o_fc_tile">
|
||||
<span class="o_fc_tile__num">
|
||||
<field name="count_hardship" nolabel="1"/>
|
||||
</span>Hardship
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
<div class="col-4">
|
||||
<button name="action_open_acsd_cases" type="object"
|
||||
class="btn p-0 w-100 border-0">
|
||||
<div class="o_fc_tile">
|
||||
<span class="o_fc_tile__num">
|
||||
<field name="count_acsd" nolabel="1"/>
|
||||
</span>ACSD
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-12 col-lg-6">
|
||||
<div class="card" style="border-radius: 14px; overflow: hidden;">
|
||||
<div class="card-header fw-bold text-white py-2" style="background: linear-gradient(135deg, #43e97b 0%, #38f9d7 100%);">
|
||||
<field name="panel4_title" nolabel="1"/>
|
||||
|
||||
<!-- RIGHT COLUMN -->
|
||||
<div class="col-12 col-lg-7">
|
||||
|
||||
<!-- ADP Pre-Approval -->
|
||||
<div class="o_fc_section mb-3">
|
||||
<h6 class="o_fc_h6">ADP
|
||||
<span class="o_fc_tag">Pre-Approval</span>
|
||||
</h6>
|
||||
<div class="row g-2">
|
||||
<div class="col-6 col-md-3">
|
||||
<button name="action_open_adp_waiting_app" 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="adp_waiting_app_count" nolabel="1"/>
|
||||
</span>Waiting App
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
<div class="col-6 col-md-3">
|
||||
<button name="action_open_adp_app_received" type="object"
|
||||
class="btn p-0 w-100 border-0">
|
||||
<div class="o_fc_tile">
|
||||
<span class="o_fc_tile__num">
|
||||
<field name="adp_app_received_count" nolabel="1"/>
|
||||
</span>App Received
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
<div class="col-6 col-md-3">
|
||||
<button name="action_open_adp_ready_submit" type="object"
|
||||
class="btn p-0 w-100 border-0">
|
||||
<div class="o_fc_tile">
|
||||
<span class="o_fc_tile__num">
|
||||
<field name="adp_ready_submit_count" nolabel="1"/>
|
||||
</span>Ready Submit
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
<div class="col-6 col-md-3">
|
||||
<button name="action_open_adp_needs_correction" 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="adp_needs_correction_count" nolabel="1"/>
|
||||
</span>Needs Correction
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-body p-0" style="max-height: 400px; overflow-y: auto;">
|
||||
<field name="panel4_html" class="w-100" nolabel="1"/>
|
||||
</div>
|
||||
|
||||
<!-- ADP Post-Approval -->
|
||||
<div class="o_fc_section mb-3">
|
||||
<h6 class="o_fc_h6">ADP
|
||||
<span class="o_fc_tag">Post-Approval</span>
|
||||
</h6>
|
||||
<div class="row g-2">
|
||||
<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_tile">
|
||||
<span class="o_fc_tile__num">
|
||||
<field name="adp_approved_count" nolabel="1"/>
|
||||
</span>Approved
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
<div class="col-6 col-md-3">
|
||||
<button name="action_open_adp_ready_delivery" type="object"
|
||||
class="btn p-0 w-100 border-0">
|
||||
<div class="o_fc_tile">
|
||||
<span class="o_fc_tile__num">
|
||||
<field name="adp_ready_delivery_count" nolabel="1"/>
|
||||
</span>Ready Delivery
|
||||
</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_tile">
|
||||
<span class="o_fc_tile__num">
|
||||
<field name="adp_ready_bill_count" nolabel="1"/>
|
||||
</span>Ready Bill
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
<div class="col-6 col-md-3">
|
||||
<button name="action_open_adp_on_hold" 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="adp_on_hold_count" nolabel="1"/>
|
||||
</span>On Hold
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- MOD -->
|
||||
<div class="o_fc_section mb-3">
|
||||
<h6 class="o_fc_h6">MOD</h6>
|
||||
<div class="row g-2">
|
||||
<div class="col-6 col-md-2">
|
||||
<button name="action_open_mod_awaiting_funding" type="object"
|
||||
class="btn p-0 w-100 border-0">
|
||||
<div class="o_fc_tile">
|
||||
<span class="o_fc_tile__num">
|
||||
<field name="mod_awaiting_funding_count" nolabel="1"/>
|
||||
</span>Awaiting
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
<div class="col-6 col-md-2">
|
||||
<button name="action_open_mod_funding_approved" type="object"
|
||||
class="btn p-0 w-100 border-0">
|
||||
<div class="o_fc_tile">
|
||||
<span class="o_fc_tile__num">
|
||||
<field name="mod_funding_approved_count" nolabel="1"/>
|
||||
</span>Approved
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
<div class="col-6 col-md-2">
|
||||
<button name="action_open_mod_pca_received" type="object"
|
||||
class="btn p-0 w-100 border-0">
|
||||
<div class="o_fc_tile">
|
||||
<span class="o_fc_tile__num">
|
||||
<field name="mod_pca_received_count" nolabel="1"/>
|
||||
</span>PCA
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
<div class="col-6 col-md-3">
|
||||
<button name="action_open_mod_project_complete" type="object"
|
||||
class="btn p-0 w-100 border-0">
|
||||
<div class="o_fc_tile">
|
||||
<span class="o_fc_tile__num">
|
||||
<field name="mod_project_complete_count" nolabel="1"/>
|
||||
</span>Proj. Done
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
<div class="col-6 col-md-3">
|
||||
<button name="action_open_mod_pod_submitted" type="object"
|
||||
class="btn p-0 w-100 border-0">
|
||||
<div class="o_fc_tile">
|
||||
<span class="o_fc_tile__num">
|
||||
<field name="mod_pod_submitted_count" nolabel="1"/>
|
||||
</span>POD Submitted
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</sheet>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!-- Dashboard Action -->
|
||||
<!-- Dashboard Action — preserved id so existing menu items resolve -->
|
||||
<record id="action_fusion_claims_dashboard" model="ir.actions.act_window">
|
||||
<field name="name">Dashboard</field>
|
||||
<field name="res_model">fusion.claims.dashboard</field>
|
||||
|
||||
Reference in New Issue
Block a user