# -*- coding: utf-8 -*- # Copyright 2024-2026 Nexa Systems Inc. # License OPL-1 (Odoo Proprietary License v1.0) from odoo import api, fields, models 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) # ========================================================================= # Role-aware filter # ========================================================================= is_manager = fields.Boolean(compute='_compute_is_manager') 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: 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')) ) 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 = ( '

No activities assigned.

' ) 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'
' f'{act.summary or act.activity_type_id.name or "Activity"}' f'{deadline_text}' f'
' ) 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')])