feat(fusion_claims): add dashboard KPI tiles (ready/claimed/AR)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -88,3 +88,64 @@ class FusionClaimsDashboard(models.TransientModel):
|
|||||||
tz = pytz.timezone(rec.env.user.tz or 'America/Toronto')
|
tz = pytz.timezone(rec.env.user.tz or 'America/Toronto')
|
||||||
local_deadline = tz.localize(naive_deadline)
|
local_deadline = tz.localize(naive_deadline)
|
||||||
rec.submission_deadline_dt = local_deadline.astimezone(pytz.UTC).replace(tzinfo=None)
|
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'))
|
||||||
|
|||||||
@@ -34,6 +34,36 @@ class TestFusionClaimsDashboard(TransactionCase):
|
|||||||
|
|
||||||
cls.partner = cls.Partner.create({'name': 'Test Client'})
|
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):
|
def test_dashboard_record_creates(self):
|
||||||
dashboard = self.Dashboard.create({})
|
dashboard = self.Dashboard.create({})
|
||||||
self.assertTrue(dashboard.id, "Dashboard record should be creatable")
|
self.assertTrue(dashboard.id, "Dashboard record should be creatable")
|
||||||
@@ -91,3 +121,42 @@ class TestFusionClaimsDashboard(TransactionCase):
|
|||||||
# Test runs after 2026-01-23 by default.
|
# Test runs after 2026-01-23 by default.
|
||||||
dashboard = self.Dashboard.with_user(self.manager).create({})
|
dashboard = self.Dashboard.with_user(self.manager).create({})
|
||||||
self.assertFalse(dashboard.is_pre_first_posting)
|
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")
|
||||||
|
|||||||
Reference in New Issue
Block a user