152 lines
6.5 KiB
Python
152 lines
6.5 KiB
Python
# -*- 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'))
|