diff --git a/fusion_claims/models/dashboard.py b/fusion_claims/models/dashboard.py index 03ab31e3..dabf506c 100644 --- a/fusion_claims/models/dashboard.py +++ b/fusion_claims/models/dashboard.py @@ -88,3 +88,64 @@ class FusionClaimsDashboard(models.TransientModel): 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')) diff --git a/fusion_claims/tests/test_dashboard.py b/fusion_claims/tests/test_dashboard.py index 0f88d8c1..278d63bf 100644 --- a/fusion_claims/tests/test_dashboard.py +++ b/fusion_claims/tests/test_dashboard.py @@ -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") @@ -91,3 +121,42 @@ class TestFusionClaimsDashboard(TransactionCase): # 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")