# -*- 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'