# Fusion Claims Dashboard Implementation Plan > **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. **Goal:** Rewrite the existing `fusion.claims.dashboard` TransientModel + view as an action-oriented dashboard with posting-week banner, live deadline countdown, 3 KPI tiles, 8 funder hotlinks, ADP + MOD workflow flag tiles, role-aware filtering, and dual-bundle light/dark themed SCSS. **Architecture:** Hybrid — server-rendered Bootstrap-grid form view on a TransientModel with ~36 computed fields, plus one OWL field-widget for the live countdown that ticks every 60s. SCSS palette tokens branch on `$o-webclient-color-scheme` at compile time and the file is registered in both `web.assets_backend` and `web.assets_web_dark`. **Tech Stack:** Odoo 19, Python 3.11, OWL 2 (Odoo Web Library), SCSS, Bootstrap 5. **Reference spec:** `docs/superpowers/specs/2026-05-21-fusion-claims-dashboard-design.md`. **Test environment (per CLAUDE.md §26):** - Docker container: `odoo-modsdev-app` - Database: `modsdev` - Run all module tests: `docker exec odoo-modsdev-app odoo -d modsdev -u fusion_claims --test-enable --test-tags fusion_claims --stop-after-init` - Run a specific class: append `:TestFusionClaimsDashboard` to the tag. - Run a specific test: append `:TestFusionClaimsDashboard.test_xxx`. **File structure:** ``` fusion_claims/ ├── __manifest__.py (modify) ├── models/dashboard.py (rewrite) ├── views/dashboard_views.xml (rewrite) ├── static/src/scss/_fc_dashboard_tokens.scss (new) ├── static/src/scss/fc_dashboard.scss (new) ├── static/src/js/fc_posting_countdown.js (new) ├── static/src/xml/fc_posting_countdown.xml (new) └── tests/test_dashboard.py (new) ``` **Cross-cutting conventions:** - All new fields use `x_fc_*` prefix only where they live on existing models (`sale.order`, `account.move`). Fields on the dashboard model itself don't need the prefix because it's a private TransientModel. - All tests inherit `TransactionCase` and use `@tagged('-at_install', 'post_install', 'fusion_claims')`. - Test data setup uses `with_context(skip_status_validation=True)` whenever writing controlled statuses, per CLAUDE.md gotcha #25. - Commits use the existing module convention: `feat(fusion_claims): ...` or `test(fusion_claims): ...` (style matches the project's recent commits like `fix(fusion_schedule): ...`). --- ## Task 1: Scaffold model + role-filter helper + first test **Files:** - Modify: `fusion_claims/models/dashboard.py` (rewrite from scratch) - Create: `fusion_claims/tests/test_dashboard.py` - Create: `fusion_claims/tests/__init__.py` if missing (check first) - [ ] **Step 1: Confirm tests/__init__.py exists** Run: ```bash cat K:/Github/Odoo-Modules/fusion_claims/tests/__init__.py ``` Expected: file lists existing test modules. If `from . import test_dashboard` is not yet present, we'll add it in step 5 below. - [ ] **Step 2: Write the failing test (test class skeleton)** Create `fusion_claims/tests/test_dashboard.py`: ```python # -*- coding: utf-8 -*- from odoo.tests.common import TransactionCase, tagged @tagged('-at_install', 'post_install', 'fusion_claims') class TestFusionClaimsDashboard(TransactionCase): @classmethod def setUpClass(cls): super().setUpClass() cls.Dashboard = cls.env['fusion.claims.dashboard'] cls.User = cls.env['res.users'] cls.Partner = cls.env['res.partner'] # Manager user (sees everything) cls.manager = cls.User.create({ 'name': 'Test Dashboard Manager', 'login': 'test_dash_mgr', 'group_ids': [ (4, cls.env.ref('fusion_claims.group_fusion_claims_manager').id), (4, cls.env.ref('sales_team.group_sale_salesman').id), ], }) # Sales rep (sees only own cases) cls.salesrep = cls.User.create({ 'name': 'Test Dashboard Salesrep', 'login': 'test_dash_rep', 'group_ids': [ (4, cls.env.ref('fusion_claims.group_fusion_claims_user').id), (4, cls.env.ref('sales_team.group_sale_salesman').id), ], }) cls.partner = cls.Partner.create({'name': 'Test Client'}) def test_dashboard_record_creates(self): dashboard = self.Dashboard.create({}) self.assertTrue(dashboard.id, "Dashboard record should be creatable") self.assertEqual(dashboard.name, 'Dashboard') def test_role_filter_empty_for_manager(self): dashboard = self.Dashboard.with_user(self.manager).create({}) self.assertEqual(dashboard._role_filter_domain(), [], "Manager should see all cases (empty domain)") def test_role_filter_restricts_for_salesrep(self): dashboard = self.Dashboard.with_user(self.salesrep).create({}) domain = dashboard._role_filter_domain() self.assertEqual(domain, [('user_id', '=', self.salesrep.id)], "Sales rep should see only their own SOs") def test_is_manager_true_for_manager(self): dashboard = self.Dashboard.with_user(self.manager).create({}) self.assertTrue(dashboard.is_manager) def test_is_manager_false_for_salesrep(self): dashboard = self.Dashboard.with_user(self.salesrep).create({}) self.assertFalse(dashboard.is_manager) ``` - [ ] **Step 3: Register the test in tests/__init__.py** Edit `fusion_claims/tests/__init__.py` to add the line (preserve existing imports): ```python from . import test_dashboard ``` - [ ] **Step 4: Run test to verify it fails** Run: ```bash docker exec odoo-modsdev-app odoo -d modsdev -u fusion_claims --test-enable --test-tags fusion_claims:TestFusionClaimsDashboard --stop-after-init 2>&1 | tail -40 ``` Expected: tests fail because the new model doesn't have `_role_filter_domain` or `is_manager`. Errors like `AttributeError: 'fusion.claims.dashboard' object has no attribute '_role_filter_domain'`. - [ ] **Step 5: Replace models/dashboard.py with the new skeleton** Replace the entire contents of `fusion_claims/models/dashboard.py` with: ```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)] ``` - [ ] **Step 6: Run test to verify it passes** Run: ```bash docker exec odoo-modsdev-app odoo -d modsdev -u fusion_claims --test-enable --test-tags fusion_claims:TestFusionClaimsDashboard --stop-after-init 2>&1 | tail -20 ``` Expected: `5 passed, 0 failed`. - [ ] **Step 7: Commit** ```bash git -C K:/Github/Odoo-Modules add fusion_claims/models/dashboard.py fusion_claims/tests/test_dashboard.py fusion_claims/tests/__init__.py git -C K:/Github/Odoo-Modules commit -m "feat(fusion_claims): scaffold dashboard model with role filter" ``` --- ## Task 2: Banner fields & `_compute_banner` **Files:** - Modify: `fusion_claims/models/dashboard.py` - Modify: `fusion_claims/tests/test_dashboard.py` - [ ] **Step 1: Write failing tests for banner fields** Append to `test_dashboard.py` (inside the existing class): ```python def test_banner_posting_period_label_format(self): dashboard = self.Dashboard.with_user(self.manager).create({}) label = dashboard.posting_period_label self.assertIn(' – ', label, "Label should use en dash separator between start and end") self.assertTrue(any(month in label for month in ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec']), "Label should contain a month abbreviation") def test_banner_posting_period_start_and_end_are_dates(self): dashboard = self.Dashboard.with_user(self.manager).create({}) self.assertTrue(dashboard.posting_period_start) self.assertTrue(dashboard.posting_period_end) # end = start + 14 days (one cycle) delta = (dashboard.posting_period_end - dashboard.posting_period_start).days self.assertEqual(delta, 14) def test_banner_submission_deadline_is_wednesday_6pm(self): dashboard = self.Dashboard.with_user(self.manager).create({}) deadline = dashboard.submission_deadline_dt self.assertTrue(deadline, "Deadline should be set") self.assertEqual(deadline.weekday(), 2, "Deadline should be Wednesday") self.assertEqual(deadline.hour, 18, "Deadline should be 18:00 (6 PM)") def test_is_pre_first_posting_false_when_today_is_past_base_date(self): # In the test environment, today is past 2026-01-23 (the default base date). # If this ever runs before the base date, the assertion will need adjusting. dashboard = self.Dashboard.with_user(self.manager).create({}) self.assertFalse(dashboard.is_pre_first_posting) ``` - [ ] **Step 2: Run tests to verify they fail** Run: ```bash docker exec odoo-modsdev-app odoo -d modsdev -u fusion_claims --test-enable --test-tags fusion_claims:TestFusionClaimsDashboard --stop-after-init 2>&1 | tail -30 ``` Expected: 4 new tests fail with `AttributeError` for `posting_period_label`, `posting_period_start`, etc. - [ ] **Step 3: Add banner fields and `_compute_banner` to the model** Insert immediately AFTER the role-filter block in `models/dashboard.py`: ```python # ========================================================================= # 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) ``` - [ ] **Step 4: Run tests to verify they pass** Run: ```bash docker exec odoo-modsdev-app odoo -d modsdev -u fusion_claims --test-enable --test-tags fusion_claims:TestFusionClaimsDashboard --stop-after-init 2>&1 | tail -20 ``` Expected: all 9 tests pass. - [ ] **Step 5: Commit** ```bash git -C K:/Github/Odoo-Modules add fusion_claims/models/dashboard.py fusion_claims/tests/test_dashboard.py git -C K:/Github/Odoo-Modules commit -m "feat(fusion_claims): add dashboard banner fields" ``` --- ## Task 3: KPI fields & `_compute_kpis` **Files:** - Modify: `fusion_claims/models/dashboard.py` - Modify: `fusion_claims/tests/test_dashboard.py` - [ ] **Step 1: Add test data setup for invoices** Append a new helper to `test_dashboard.py` (add inside the class, after `setUpClass`): ```python @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', }) # Use vals dict so unset fields are False; avoid auto-create of lines 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, })], }) invoice.action_post() # Force-set payment_state since it's normally computed invoice.with_context(skip_sync=True).write({'payment_state': payment_state}) return invoice ``` - [ ] **Step 2: Write the failing tests for KPIs** Add to `test_dashboard.py`: ```python def test_kpi_ready_counts_waiting_invoices_not_exported(self): # Create one "ready" invoice owned by manager 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): # Build dashboard first to read the current period 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): # Manager's invoice; salesrep should NOT see it 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") ``` - [ ] **Step 3: Run tests to verify they fail** Run: ```bash docker exec odoo-modsdev-app odoo -d modsdev -u fusion_claims --test-enable --test-tags fusion_claims:TestFusionClaimsDashboard --stop-after-init 2>&1 | tail -30 ``` Expected: 5 new tests fail (`kpi_ready_count` etc. don't exist). - [ ] **Step 4: Add KPI fields and `_compute_kpis`** Insert after the banner block in `models/dashboard.py`: ```python # ========================================================================= # 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')) ``` - [ ] **Step 5: Run tests to verify they pass** Run: ```bash docker exec odoo-modsdev-app odoo -d modsdev -u fusion_claims --test-enable --test-tags fusion_claims:TestFusionClaimsDashboard --stop-after-init 2>&1 | tail -20 ``` Expected: 14 tests pass. - [ ] **Step 6: Commit** ```bash git -C K:/Github/Odoo-Modules add fusion_claims/models/dashboard.py fusion_claims/tests/test_dashboard.py git -C K:/Github/Odoo-Modules commit -m "feat(fusion_claims): add dashboard KPI tiles (ready/claimed/AR)" ``` --- ## Task 4: Activities + bottlenecks (left column) **Files:** - Modify: `fusion_claims/models/dashboard.py` - Modify: `fusion_claims/tests/test_dashboard.py` - [ ] **Step 1: Write failing tests** Add to `test_dashboard.py`: ```python def test_my_activities_count_zero_when_none(self): dashboard = self.Dashboard.with_user(self.manager).create({}) self.assertEqual(dashboard.my_activities_count, 0) def test_my_activities_count_picks_up_user_activity(self): so = self.env['sale.order'].with_context(skip_status_validation=True).create({ 'partner_id': self.partner.id, 'user_id': self.manager.id, 'x_fc_sale_type': 'adp', }) self.env['mail.activity'].create({ 'res_model_id': self.env['ir.model']._get('sale.order').id, 'res_id': so.id, 'res_model': 'sale.order', 'user_id': self.manager.id, 'activity_type_id': self.env.ref('mail.mail_activity_data_todo').id, 'summary': 'Test activity', }) dashboard = self.Dashboard.with_user(self.manager).create({}) self.assertEqual(dashboard.my_activities_count, 1) self.assertIn('Test activity', dashboard.my_activities_html or '') def test_bottleneck_no_pod_count(self): # Approved SO with no POD self.env['sale.order'].with_context(skip_status_validation=True).create({ 'partner_id': self.partner.id, 'user_id': self.manager.id, 'x_fc_sale_type': 'adp', 'x_fc_adp_application_status': 'approved', }) dashboard = self.Dashboard.with_user(self.manager).create({}) self.assertEqual(dashboard.bottleneck_no_pod_count, 1) def test_bottleneck_no_response_count(self): from datetime import date, timedelta old_date = date.today() - timedelta(days=20) self.env['sale.order'].with_context(skip_status_validation=True).create({ 'partner_id': self.partner.id, 'user_id': self.manager.id, 'x_fc_sale_type': 'adp', 'x_fc_adp_application_status': 'submitted', 'x_fc_claim_submission_date': old_date, }) dashboard = self.Dashboard.with_user(self.manager).create({}) self.assertEqual(dashboard.bottleneck_no_response_count, 1) ``` - [ ] **Step 2: Run tests to verify they fail** Run: ```bash docker exec odoo-modsdev-app odoo -d modsdev -u fusion_claims --test-enable --test-tags fusion_claims:TestFusionClaimsDashboard --stop-after-init 2>&1 | tail -30 ``` Expected: 4 new tests fail. - [ ] **Step 3: Add activities + bottleneck fields and compute methods** Insert after the KPI block in `models/dashboard.py`: ```python # ========================================================================= # 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 rows = [] from datetime import date today = date.today() 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) # ========================================================================= bottleneck_no_pod_count = fields.Integer(compute='_compute_secondary_counts') bottleneck_no_response_count = 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() 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), ]) ``` - [ ] **Step 4: Run tests to verify they pass** Run: ```bash docker exec odoo-modsdev-app odoo -d modsdev -u fusion_claims --test-enable --test-tags fusion_claims:TestFusionClaimsDashboard --stop-after-init 2>&1 | tail -20 ``` Expected: 18 tests pass. - [ ] **Step 5: Commit** ```bash git -C K:/Github/Odoo-Modules add fusion_claims/models/dashboard.py fusion_claims/tests/test_dashboard.py git -C K:/Github/Odoo-Modules commit -m "feat(fusion_claims): add dashboard activities and bottlenecks" ``` --- ## Task 5: Other-funder counts **Files:** - Modify: `fusion_claims/models/dashboard.py` - Modify: `fusion_claims/tests/test_dashboard.py` - [ ] **Step 1: Write failing tests** Add to `test_dashboard.py`: ```python def test_other_funder_counts_segregate_by_sale_type(self): SO = self.env['sale.order'].with_context(skip_status_validation=True) SO.create({'partner_id': self.partner.id, 'user_id': self.manager.id, 'x_fc_sale_type': 'odsp'}) SO.create({'partner_id': self.partner.id, 'user_id': self.manager.id, 'x_fc_sale_type': 'wsib'}) SO.create({'partner_id': self.partner.id, 'user_id': self.manager.id, 'x_fc_sale_type': 'insurance'}) SO.create({'partner_id': self.partner.id, 'user_id': self.manager.id, 'x_fc_sale_type': 'muscular_dystrophy'}) SO.create({'partner_id': self.partner.id, 'user_id': self.manager.id, 'x_fc_sale_type': 'hardship'}) # ACSD = client_type ACS, regardless of sale_type SO.create({'partner_id': self.partner.id, 'user_id': self.manager.id, 'x_fc_sale_type': 'adp', 'x_fc_client_type': 'ACS'}) dashboard = self.Dashboard.with_user(self.manager).create({}) self.assertEqual(dashboard.count_odsp, 1) self.assertEqual(dashboard.count_wsib, 1) self.assertEqual(dashboard.count_insurance, 1) self.assertEqual(dashboard.count_mdc, 1) self.assertEqual(dashboard.count_hardship, 1) self.assertEqual(dashboard.count_acsd, 1) def test_other_funder_counts_exclude_cancelled(self): so = self.env['sale.order'].with_context(skip_status_validation=True).create({ 'partner_id': self.partner.id, 'user_id': self.manager.id, 'x_fc_sale_type': 'wsib', }) so.with_context(skip_status_validation=True).write({'state': 'cancel'}) dashboard = self.Dashboard.with_user(self.manager).create({}) self.assertEqual(dashboard.count_wsib, 0) ``` - [ ] **Step 2: Run tests to verify they fail** Run: ```bash docker exec odoo-modsdev-app odoo -d modsdev -u fusion_claims --test-enable --test-tags fusion_claims:TestFusionClaimsDashboard --stop-after-init 2>&1 | tail -20 ``` Expected: 2 tests fail (missing fields). - [ ] **Step 3: Add other-funder count fields** Inside `models/dashboard.py`, add these fields under the "Bottlenecks" comment block and **extend** the existing `_compute_secondary_counts` method: ```python # ========================================================================= # Other funders (left column count cards) # ========================================================================= 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') ``` Then extend `_compute_secondary_counts` — replace the existing method body with the version below (preserves the two bottleneck assignments and adds six more): ```python 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')]) ``` - [ ] **Step 4: Run tests to verify they pass** Run: ```bash docker exec odoo-modsdev-app odoo -d modsdev -u fusion_claims --test-enable --test-tags fusion_claims:TestFusionClaimsDashboard --stop-after-init 2>&1 | tail -20 ``` Expected: 20 tests pass. - [ ] **Step 5: Commit** ```bash git -C K:/Github/Odoo-Modules add fusion_claims/models/dashboard.py fusion_claims/tests/test_dashboard.py git -C K:/Github/Odoo-Modules commit -m "feat(fusion_claims): add dashboard other-funder counts" ``` --- ## Task 6: ADP + MOD workflow counts **Files:** - Modify: `fusion_claims/models/dashboard.py` - Modify: `fusion_claims/tests/test_dashboard.py` - [ ] **Step 1: Write failing tests** Add to `test_dashboard.py`: ```python def test_adp_pre_approval_tile_counts(self): SO = self.env['sale.order'].with_context(skip_status_validation=True) SO.create({'partner_id': self.partner.id, 'user_id': self.manager.id, 'x_fc_sale_type': 'adp', 'x_fc_adp_application_status': 'waiting_for_application'}) SO.create({'partner_id': self.partner.id, 'user_id': self.manager.id, 'x_fc_sale_type': 'adp', 'x_fc_adp_application_status': 'application_received'}) SO.create({'partner_id': self.partner.id, 'user_id': self.manager.id, 'x_fc_sale_type': 'adp', 'x_fc_adp_application_status': 'ready_submission'}) SO.create({'partner_id': self.partner.id, 'user_id': self.manager.id, 'x_fc_sale_type': 'adp', 'x_fc_adp_application_status': 'needs_correction'}) dashboard = self.Dashboard.with_user(self.manager).create({}) self.assertEqual(dashboard.adp_waiting_app_count, 1) self.assertEqual(dashboard.adp_app_received_count, 1) self.assertEqual(dashboard.adp_ready_submit_count, 1) self.assertEqual(dashboard.adp_needs_correction_count, 1) def test_adp_post_approval_tile_counts(self): SO = self.env['sale.order'].with_context(skip_status_validation=True) SO.create({'partner_id': self.partner.id, 'user_id': self.manager.id, 'x_fc_sale_type': 'adp', 'x_fc_adp_application_status': 'approved'}) SO.create({'partner_id': self.partner.id, 'user_id': self.manager.id, 'x_fc_sale_type': 'adp', 'x_fc_adp_application_status': 'ready_delivery'}) SO.create({'partner_id': self.partner.id, 'user_id': self.manager.id, 'x_fc_sale_type': 'adp', 'x_fc_adp_application_status': 'ready_bill'}) SO.create({'partner_id': self.partner.id, 'user_id': self.manager.id, 'x_fc_sale_type': 'adp', 'x_fc_adp_application_status': 'on_hold'}) dashboard = self.Dashboard.with_user(self.manager).create({}) self.assertEqual(dashboard.adp_approved_count, 1) self.assertEqual(dashboard.adp_ready_delivery_count, 1) self.assertEqual(dashboard.adp_ready_bill_count, 1) self.assertEqual(dashboard.adp_on_hold_count, 1) def test_mod_tile_counts(self): SO = self.env['sale.order'].with_context(skip_status_validation=True) for status in ('awaiting_funding', 'funding_approved', 'contract_received', 'project_complete', 'pod_submitted'): SO.create({'partner_id': self.partner.id, 'user_id': self.manager.id, 'x_fc_sale_type': 'march_of_dimes', 'x_fc_mod_status': status}) dashboard = self.Dashboard.with_user(self.manager).create({}) self.assertEqual(dashboard.mod_awaiting_funding_count, 1) self.assertEqual(dashboard.mod_funding_approved_count, 1) self.assertEqual(dashboard.mod_pca_received_count, 1) self.assertEqual(dashboard.mod_project_complete_count, 1) self.assertEqual(dashboard.mod_pod_submitted_count, 1) ``` - [ ] **Step 2: Run tests to verify they fail** Run: ```bash docker exec odoo-modsdev-app odoo -d modsdev -u fusion_claims --test-enable --test-tags fusion_claims:TestFusionClaimsDashboard --stop-after-init 2>&1 | tail -30 ``` Expected: 3 tests fail with `AttributeError` for the new count fields. - [ ] **Step 3: Add workflow count fields and `_compute_workflow_counts`** Insert after the other-funder counts in `models/dashboard.py`: ```python # ========================================================================= # ADP Pre-Approval (right column, 4 tiles) # ========================================================================= adp_waiting_app_count = fields.Integer(compute='_compute_workflow_counts') adp_app_received_count = fields.Integer(compute='_compute_workflow_counts') adp_ready_submit_count = fields.Integer(compute='_compute_workflow_counts') adp_needs_correction_count = fields.Integer(compute='_compute_workflow_counts') # ========================================================================= # ADP Post-Approval (right column, 4 tiles) # ========================================================================= adp_approved_count = fields.Integer(compute='_compute_workflow_counts') adp_ready_delivery_count = fields.Integer(compute='_compute_workflow_counts') adp_ready_bill_count = fields.Integer(compute='_compute_workflow_counts') adp_on_hold_count = fields.Integer(compute='_compute_workflow_counts') # ========================================================================= # MOD (right column, 5 tiles) # ========================================================================= mod_awaiting_funding_count = fields.Integer(compute='_compute_workflow_counts') mod_funding_approved_count = fields.Integer(compute='_compute_workflow_counts') mod_pca_received_count = fields.Integer(compute='_compute_workflow_counts') mod_project_complete_count = fields.Integer(compute='_compute_workflow_counts') mod_pod_submitted_count = fields.Integer(compute='_compute_workflow_counts') def _compute_workflow_counts(self): SO = self.env['sale.order'].sudo() for rec in self: base = rec._role_filter_domain() # ADP Pre-Approval rec.adp_waiting_app_count = SO.search_count(base + [ ('x_fc_adp_application_status', 'in', ['waiting_for_application', 'assessment_completed']), ]) rec.adp_app_received_count = SO.search_count(base + [ ('x_fc_adp_application_status', '=', 'application_received'), ]) rec.adp_ready_submit_count = SO.search_count(base + [ ('x_fc_adp_application_status', '=', 'ready_submission'), ]) rec.adp_needs_correction_count = SO.search_count(base + [ ('x_fc_adp_application_status', '=', 'needs_correction'), ]) # ADP Post-Approval rec.adp_approved_count = SO.search_count(base + [ ('x_fc_adp_application_status', 'in', ['approved', 'approved_deduction']), ]) rec.adp_ready_delivery_count = SO.search_count(base + [ ('x_fc_adp_application_status', '=', 'ready_delivery'), ]) rec.adp_ready_bill_count = SO.search_count(base + [ ('x_fc_adp_application_status', '=', 'ready_bill'), ]) rec.adp_on_hold_count = SO.search_count(base + [ ('x_fc_adp_application_status', '=', 'on_hold'), ]) # MOD rec.mod_awaiting_funding_count = SO.search_count(base + [ ('x_fc_mod_status', '=', 'awaiting_funding'), ]) rec.mod_funding_approved_count = SO.search_count(base + [ ('x_fc_mod_status', '=', 'funding_approved'), ]) rec.mod_pca_received_count = SO.search_count(base + [ ('x_fc_mod_status', '=', 'contract_received'), ]) rec.mod_project_complete_count = SO.search_count(base + [ ('x_fc_mod_status', '=', 'project_complete'), ]) rec.mod_pod_submitted_count = SO.search_count(base + [ ('x_fc_mod_status', '=', 'pod_submitted'), ]) ``` - [ ] **Step 4: Run tests to verify they pass** Run: ```bash docker exec odoo-modsdev-app odoo -d modsdev -u fusion_claims --test-enable --test-tags fusion_claims:TestFusionClaimsDashboard --stop-after-init 2>&1 | tail -20 ``` Expected: 23 tests pass. - [ ] **Step 5: Commit** ```bash git -C K:/Github/Odoo-Modules add fusion_claims/models/dashboard.py fusion_claims/tests/test_dashboard.py git -C K:/Github/Odoo-Modules commit -m "feat(fusion_claims): add dashboard ADP + MOD workflow tile counts" ``` --- ## Task 7: Open-list action methods **Files:** - Modify: `fusion_claims/models/dashboard.py` - Modify: `fusion_claims/tests/test_dashboard.py` - [ ] **Step 1: Write failing tests** Add to `test_dashboard.py`: ```python def test_action_open_adp_waiting_app_returns_correct_domain(self): dashboard = self.Dashboard.with_user(self.manager).create({}) action = dashboard.action_open_adp_waiting_app() self.assertEqual(action['res_model'], 'sale.order') self.assertIn(('x_fc_adp_application_status', 'in', ['waiting_for_application', 'assessment_completed']), action['domain']) def test_action_open_bottleneck_no_pod_returns_correct_domain(self): dashboard = self.Dashboard.with_user(self.manager).create({}) action = dashboard.action_open_bottleneck_no_pod() self.assertEqual(action['res_model'], 'sale.order') self.assertIn(('x_fc_proof_of_delivery', '=', False), action['domain']) def test_action_open_mod_awaiting_funding_returns_correct_domain(self): dashboard = self.Dashboard.with_user(self.manager).create({}) action = dashboard.action_open_mod_awaiting_funding() self.assertEqual(action['res_model'], 'sale.order') self.assertIn(('x_fc_mod_status', '=', 'awaiting_funding'), action['domain']) def test_action_open_my_activities_returns_activity_model(self): dashboard = self.Dashboard.with_user(self.manager).create({}) action = dashboard.action_open_my_activities() self.assertEqual(action['res_model'], 'mail.activity') ``` - [ ] **Step 2: Run tests to verify they fail** Run: ```bash docker exec odoo-modsdev-app odoo -d modsdev -u fusion_claims --test-enable --test-tags fusion_claims:TestFusionClaimsDashboard --stop-after-init 2>&1 | tail -20 ``` Expected: 4 tests fail. - [ ] **Step 3: Add open-list action methods** Append the following section to `models/dashboard.py`: ```python # ========================================================================= # Open-list action methods # ========================================================================= def _so_list_action(self, name, domain): return { 'type': 'ir.actions.act_window', 'name': name, 'res_model': 'sale.order', 'view_mode': 'list,form', 'domain': self._role_filter_domain() + domain, 'target': 'current', } # ----- ADP Pre-Approval ----- def action_open_adp_waiting_app(self): return self._so_list_action('ADP — Waiting for Application', [ ('x_fc_adp_application_status', 'in', ['waiting_for_application', 'assessment_completed']), ]) def action_open_adp_app_received(self): return self._so_list_action('ADP — Application Received', [ ('x_fc_adp_application_status', '=', 'application_received'), ]) def action_open_adp_ready_submit(self): return self._so_list_action('ADP — Ready for Submission', [ ('x_fc_adp_application_status', '=', 'ready_submission'), ]) def action_open_adp_needs_correction(self): return self._so_list_action('ADP — Needs Correction', [ ('x_fc_adp_application_status', '=', 'needs_correction'), ]) # ----- ADP Post-Approval ----- def action_open_adp_approved(self): return self._so_list_action('ADP — Approved', [ ('x_fc_adp_application_status', 'in', ['approved', 'approved_deduction']), ]) def action_open_adp_ready_delivery(self): return self._so_list_action('ADP — Ready for Delivery', [ ('x_fc_adp_application_status', '=', 'ready_delivery'), ]) def action_open_adp_ready_bill(self): return self._so_list_action('ADP — Ready to Bill', [ ('x_fc_adp_application_status', '=', 'ready_bill'), ]) def action_open_adp_on_hold(self): return self._so_list_action('ADP — On Hold', [ ('x_fc_adp_application_status', '=', 'on_hold'), ]) # ----- MOD ----- def action_open_mod_awaiting_funding(self): return self._so_list_action('MOD — Awaiting Funding', [ ('x_fc_mod_status', '=', 'awaiting_funding'), ]) def action_open_mod_funding_approved(self): return self._so_list_action('MOD — Funding Approved', [ ('x_fc_mod_status', '=', 'funding_approved'), ]) def action_open_mod_pca_received(self): return self._so_list_action('MOD — PCA Received', [ ('x_fc_mod_status', '=', 'contract_received'), ]) def action_open_mod_project_complete(self): return self._so_list_action('MOD — Project Complete', [ ('x_fc_mod_status', '=', 'project_complete'), ]) def action_open_mod_pod_submitted(self): return self._so_list_action('MOD — POD Submitted', [ ('x_fc_mod_status', '=', 'pod_submitted'), ]) # ----- Other funders ----- def action_open_odsp_cases(self): return self._so_list_action('ODSP Cases', [ ('state', '!=', 'cancel'), ('x_fc_sale_type', 'in', ['odsp', 'adp_odsp']), ]) def action_open_wsib_cases(self): return self._so_list_action('WSIB Cases', [ ('state', '!=', 'cancel'), ('x_fc_sale_type', '=', 'wsib'), ]) def action_open_insurance_cases(self): return self._so_list_action('Insurance Cases', [ ('state', '!=', 'cancel'), ('x_fc_sale_type', '=', 'insurance'), ]) def action_open_mdc_cases(self): return self._so_list_action('Muscular Dystrophy Cases', [ ('state', '!=', 'cancel'), ('x_fc_sale_type', '=', 'muscular_dystrophy'), ]) def action_open_hardship_cases(self): return self._so_list_action('Hardship Cases', [ ('state', '!=', 'cancel'), ('x_fc_sale_type', '=', 'hardship'), ]) def action_open_acsd_cases(self): return self._so_list_action('ACSD Cases', [ ('state', '!=', 'cancel'), ('x_fc_client_type', '=', 'ACS'), ]) # ----- Bottlenecks ----- def action_open_bottleneck_no_pod(self): return self._so_list_action('Bottleneck — Approved without POD', [ ('x_fc_adp_application_status', 'in', ['approved', 'approved_deduction']), ('x_fc_proof_of_delivery', '=', False), ]) def action_open_bottleneck_no_response(self): from datetime import date, timedelta cutoff = date.today() - timedelta(days=14) return self._so_list_action('Bottleneck — Submitted, no response', [ ('x_fc_adp_application_status', 'in', ['submitted', 'resubmitted']), ('x_fc_claim_submission_date', '<', cutoff), ]) # ----- Activities ----- def action_open_my_activities(self): return { 'type': 'ir.actions.act_window', 'name': 'My Activities', 'res_model': 'mail.activity', 'view_mode': 'list,form', 'domain': [ ('user_id', '=', self.env.user.id), ('res_model', 'in', ['sale.order', 'account.move', 'fusion.technician.task']), ], 'target': 'current', } # ----- KPI drill-downs ----- def action_open_kpi_ready(self): return { 'type': 'ir.actions.act_window', 'name': 'Ready to Claim (ADP)', 'res_model': 'account.move', 'view_mode': 'list,form', 'domain': self._invoice_role_filter() + [ ('move_type', '=', 'out_invoice'), ('state', '=', 'posted'), ('x_fc_adp_billing_status', '=', 'waiting'), ('adp_exported', '=', False), ], 'target': 'current', } def action_open_kpi_claimed(self): return { 'type': 'ir.actions.act_window', 'name': 'Claimed This Period', 'res_model': 'account.move', 'view_mode': 'list,form', 'domain': self._invoice_role_filter() + [ ('move_type', '=', 'out_invoice'), ('state', '=', 'posted'), ('x_fc_adp_billing_status', 'in', ['submitted', 'resubmitted']), ('adp_export_date', '>=', self.posting_period_start), ], 'target': 'current', } def action_open_kpi_ar(self): return { 'type': 'ir.actions.act_window', 'name': 'Total AR (ADP)', 'res_model': 'account.move', 'view_mode': 'list,form', 'domain': self._invoice_role_filter() + [ ('move_type', '=', 'out_invoice'), ('state', '=', 'posted'), ('x_fc_invoice_type', '=', 'adp'), ('payment_state', 'in', ['not_paid', 'partial']), ], 'target': 'current', } ``` - [ ] **Step 4: Run tests to verify they pass** Run: ```bash docker exec odoo-modsdev-app odoo -d modsdev -u fusion_claims --test-enable --test-tags fusion_claims:TestFusionClaimsDashboard --stop-after-init 2>&1 | tail -20 ``` Expected: 27 tests pass. - [ ] **Step 5: Commit** ```bash git -C K:/Github/Odoo-Modules add fusion_claims/models/dashboard.py fusion_claims/tests/test_dashboard.py git -C K:/Github/Odoo-Modules commit -m "feat(fusion_claims): add dashboard open-list action methods" ``` --- ## Task 8: Create-SO action methods (8 hotlinks) **Files:** - Modify: `fusion_claims/models/dashboard.py` - Modify: `fusion_claims/tests/test_dashboard.py` - [ ] **Step 1: Write failing tests** Add to `test_dashboard.py`: ```python def test_action_create_adp_so_has_default_sale_type(self): dashboard = self.Dashboard.with_user(self.manager).create({}) action = dashboard.action_create_adp_so() self.assertEqual(action['res_model'], 'sale.order') self.assertEqual(action['view_mode'], 'form') self.assertEqual(action['context']['default_x_fc_sale_type'], 'adp') def test_action_create_mod_so_has_default_sale_type(self): dashboard = self.Dashboard.with_user(self.manager).create({}) action = dashboard.action_create_mod_so() self.assertEqual(action['context']['default_x_fc_sale_type'], 'march_of_dimes') def test_action_create_odsp_so_has_division_default(self): dashboard = self.Dashboard.with_user(self.manager).create({}) action = dashboard.action_create_odsp_so() self.assertEqual(action['context']['default_x_fc_sale_type'], 'odsp') self.assertEqual(action['context']['default_x_fc_odsp_division'], 'standard') def test_all_create_so_actions_exist(self): dashboard = self.Dashboard.with_user(self.manager).create({}) # Smoke check: every hotlink method returns a valid action dict for method_name, expected_type in [ ('action_create_adp_so', 'adp'), ('action_create_mod_so', 'march_of_dimes'), ('action_create_odsp_so', 'odsp'), ('action_create_wsib_so', 'wsib'), ('action_create_insurance_so', 'insurance'), ('action_create_mdc_so', 'muscular_dystrophy'), ('action_create_hardship_so', 'hardship'), ('action_create_private_so', 'direct_private'), ]: action = getattr(dashboard, method_name)() self.assertEqual(action['res_model'], 'sale.order') self.assertEqual(action['context']['default_x_fc_sale_type'], expected_type, f"{method_name} returned wrong default sale type") ``` - [ ] **Step 2: Run tests to verify they fail** Run: ```bash docker exec odoo-modsdev-app odoo -d modsdev -u fusion_claims --test-enable --test-tags fusion_claims:TestFusionClaimsDashboard --stop-after-init 2>&1 | tail -30 ``` Expected: 4 new tests fail (missing methods). - [ ] **Step 3: Add create-SO action methods** Append to `models/dashboard.py`: ```python # ========================================================================= # Create-SO hotlinks # ========================================================================= def _create_so_action(self, name, ctx_extra): context = dict(self.env.context) context.update(ctx_extra) return { 'type': 'ir.actions.act_window', 'name': name, 'res_model': 'sale.order', 'view_mode': 'form', 'view_id': False, 'context': context, 'target': 'current', } def action_create_adp_so(self): return self._create_so_action('New ADP Order', {'default_x_fc_sale_type': 'adp'}) def action_create_mod_so(self): return self._create_so_action('New MOD Order', {'default_x_fc_sale_type': 'march_of_dimes'}) def action_create_odsp_so(self): return self._create_so_action('New ODSP Order', { 'default_x_fc_sale_type': 'odsp', 'default_x_fc_odsp_division': 'standard', }) def action_create_wsib_so(self): return self._create_so_action('New WSIB Order', {'default_x_fc_sale_type': 'wsib'}) def action_create_insurance_so(self): return self._create_so_action('New Insurance Order', {'default_x_fc_sale_type': 'insurance'}) def action_create_mdc_so(self): return self._create_so_action('New MDC Order', {'default_x_fc_sale_type': 'muscular_dystrophy'}) def action_create_hardship_so(self): return self._create_so_action('New Hardship Order', {'default_x_fc_sale_type': 'hardship'}) def action_create_private_so(self): return self._create_so_action('New Private Order', {'default_x_fc_sale_type': 'direct_private'}) ``` - [ ] **Step 4: Run tests to verify they pass** Run: ```bash docker exec odoo-modsdev-app odoo -d modsdev -u fusion_claims --test-enable --test-tags fusion_claims:TestFusionClaimsDashboard --stop-after-init 2>&1 | tail -20 ``` Expected: 31 tests pass. - [ ] **Step 5: Commit** ```bash git -C K:/Github/Odoo-Modules add fusion_claims/models/dashboard.py fusion_claims/tests/test_dashboard.py git -C K:/Github/Odoo-Modules commit -m "feat(fusion_claims): add dashboard create-SO hotlinks" ``` --- ## Task 9: Form view rewrite **Files:** - Modify: `fusion_claims/views/dashboard_views.xml` (full rewrite) This task has no Python TDD — the view is verified by loading the module and rendering the page. Test step is a module-upgrade smoke check. - [ ] **Step 1: Rewrite `views/dashboard_views.xml`** Replace the entire contents with: ```xml fusion.claims.dashboard.form fusion.claims.dashboard
Posting Period:
Showing your assigned cases only.
Your Activities
Bottlenecks
Other Funders
ADP Pre-Approval
ADP Post-Approval
MOD
Dashboard fusion.claims.dashboard form current
``` - [ ] **Step 2: Upgrade the module to load the new view** Run: ```bash docker exec odoo-modsdev-app odoo -d modsdev -u fusion_claims --stop-after-init 2>&1 | tail -20 ``` Expected: no `ParseError` or `AttributeError` lines. Last line should be similar to `INFO ... Modules loaded.` > Note: the `widget="fc_posting_countdown"` reference does not exist yet — Odoo will render the field with the default datetime widget until Task 11 ships the widget. This is intentional; the rest of the page must load successfully without it. - [ ] **Step 3: Run all dashboard tests to confirm no Python regression** Run: ```bash docker exec odoo-modsdev-app odoo -d modsdev -u fusion_claims --test-enable --test-tags fusion_claims:TestFusionClaimsDashboard --stop-after-init 2>&1 | tail -15 ``` Expected: 31 tests still pass. - [ ] **Step 4: Commit** ```bash git -C K:/Github/Odoo-Modules add fusion_claims/views/dashboard_views.xml git -C K:/Github/Odoo-Modules commit -m "feat(fusion_claims): rewrite dashboard form view with action-oriented layout" ``` --- ## Task 10: SCSS palette tokens + layout + manifest asset registration **Files:** - Create: `fusion_claims/static/src/scss/_fc_dashboard_tokens.scss` - Create: `fusion_claims/static/src/scss/fc_dashboard.scss` - Modify: `fusion_claims/__manifest__.py` This task is verified by upgrade-and-render rather than unit tests. The acceptance check is comparing the asset bundle URLs. - [ ] **Step 1: Create the palette tokens file** Create `fusion_claims/static/src/scss/_fc_dashboard_tokens.scss`: ```scss // ============================================================================= // Fusion Claims Dashboard — Palette Tokens // Compile-time branch on $o-webclient-color-scheme so the same SCSS file // produces different palettes in web.assets_backend (light) and // web.assets_web_dark (dark). Tokens load FIRST in each bundle. // ============================================================================= $o-webclient-color-scheme: bright !default; // ---------- LIGHT (defaults) ---------- $_fc-page-bg: #f7f7f8 !default; $_fc-card-bg: #ffffff !default; $_fc-card-border: #d8dadd !default; $_fc-text: #2b2b2b !default; $_fc-text-muted: #6c7480 !default; $_fc-banner-from: #eef2ff !default; $_fc-banner-to: #fce7f3 !default; $_fc-banner-border: #c7d2fe !default; $_fc-banner-text: #3730a3 !default; $_fc-deadline-text: #b91c1c !default; $_fc-kpi-bg: #f0f4ff !default; $_fc-kpi-border: #c7d2fe !default; $_fc-kpi-num: #1e3a8a !default; $_fc-action-bg: #ecfdf5 !default; $_fc-action-border: #6ee7b7 !default; $_fc-action-text: #047857 !default; $_fc-tile-bg: #f3f4f6 !default; $_fc-tile-border: #e5e7eb !default; $_fc-tile-num: #111827 !default; $_fc-urgent-bg: #fee2e2 !default; $_fc-urgent-border: #fca5a5 !default; $_fc-urgent-num: #991b1b !default; $_fc-urgent-text: #7f1d1d !default; $_fc-activity-bg: #fefce8 !default; $_fc-activity-border: #fde047 !default; $_fc-bottleneck-bg: #fef2f2 !default; $_fc-bottleneck-border: #fecaca !default; // ---------- DARK overrides ---------- @if $o-webclient-color-scheme == dark { $_fc-page-bg: #1a1d21 !global; $_fc-card-bg: #22262d !global; $_fc-card-border: #3a3f47 !global; $_fc-text: #e5e7eb !global; $_fc-text-muted: #9ca3af !global; // Cool blue monochrome banner (selected option A from brainstorm) $_fc-banner-from: #1e293b !global; $_fc-banner-to: #1e3a5f !global; $_fc-banner-border: #3b82f6 !global; $_fc-banner-text: #93c5fd !global; $_fc-deadline-text: #fca5a5 !global; $_fc-kpi-bg: #1e293b !global; $_fc-kpi-border: #334155 !global; $_fc-kpi-num: #93c5fd !global; $_fc-action-bg: #064e3b !global; $_fc-action-border: #047857 !global; $_fc-action-text: #6ee7b7 !global; $_fc-tile-bg: #2d3138 !global; $_fc-tile-border: #3a3f47 !global; $_fc-tile-num: #f3f4f6 !global; $_fc-urgent-bg: #4a1414 !global; $_fc-urgent-border: #7f1d1d !global; $_fc-urgent-num: #fca5a5 !global; $_fc-urgent-text: #fecaca !global; $_fc-activity-bg: #3a2e0a !global; $_fc-activity-border: #854d0e !global; $_fc-bottleneck-bg: #3a1414 !global; $_fc-bottleneck-border: #7f1d1d !global; } ``` - [ ] **Step 2: Create the layout file** Create `fusion_claims/static/src/scss/fc_dashboard.scss`: ```scss // ============================================================================= // Fusion Claims Dashboard — Layout & Section Styles // Consumes tokens from _fc_dashboard_tokens.scss (must load FIRST in bundle). // ============================================================================= .o_fc_dashboard { // Re-export tokens as CSS custom properties so devtools can inspect them --fc-page-bg: #{$_fc-page-bg}; --fc-card-bg: #{$_fc-card-bg}; --fc-card-border: #{$_fc-card-border}; --fc-text: #{$_fc-text}; --fc-text-muted: #{$_fc-text-muted}; --fc-banner-from: #{$_fc-banner-from}; --fc-banner-to: #{$_fc-banner-to}; --fc-banner-border: #{$_fc-banner-border}; --fc-banner-text: #{$_fc-banner-text}; --fc-deadline-text: #{$_fc-deadline-text}; --fc-kpi-bg: #{$_fc-kpi-bg}; --fc-kpi-border: #{$_fc-kpi-border}; --fc-kpi-num: #{$_fc-kpi-num}; --fc-action-bg: #{$_fc-action-bg}; --fc-action-border: #{$_fc-action-border}; --fc-action-text: #{$_fc-action-text}; --fc-tile-bg: #{$_fc-tile-bg}; --fc-tile-border: #{$_fc-tile-border}; --fc-tile-num: #{$_fc-tile-num}; --fc-urgent-bg: #{$_fc-urgent-bg}; --fc-urgent-border: #{$_fc-urgent-border}; --fc-urgent-num: #{$_fc-urgent-num}; --fc-urgent-text: #{$_fc-urgent-text}; --fc-activity-bg: #{$_fc-activity-bg}; --fc-activity-border: #{$_fc-activity-border}; --fc-bottleneck-bg: #{$_fc-bottleneck-bg}; --fc-bottleneck-border: #{$_fc-bottleneck-border}; background: var(--fc-page-bg); color: $_fc-text; .o_fc_banner { display: flex; justify-content: space-between; align-items: center; background: linear-gradient(90deg, var(--fc-banner-from), var(--fc-banner-to)); border: 1px solid var(--fc-banner-border); border-radius: 8px; padding: 10px 14px; font-weight: 600; color: var(--fc-banner-text); } .o_fc_banner__deadline { font-weight: 700; } .o_fc_kpi { background: var(--fc-kpi-bg); border: 1px solid var(--fc-kpi-border); border-radius: 8px; padding: 14px 10px; text-align: center; transition: transform 0.15s ease; &:hover { transform: translateY(-2px); } } .o_fc_kpi__num { display: block; font-size: 1.6rem; font-weight: 700; color: var(--fc-kpi-num); } .o_fc_kpi__lbl { display: block; font-size: 0.75rem; text-transform: uppercase; letter-spacing: 0.5px; color: var(--fc-text-muted); margin-top: 2px; } .o_fc_actions { display: flex; flex-wrap: wrap; gap: 6px; } .o_fc_pill { background: var(--fc-action-bg); border: 1px solid var(--fc-action-border); color: var(--fc-action-text); border-radius: 16px; padding: 5px 12px; font-size: 0.85rem; font-weight: 600; cursor: pointer; transition: background 0.15s ease; &:hover { background: var(--fc-action-border); } } .o_fc_section { background: var(--fc-card-bg); border: 1px solid var(--fc-card-border); border-radius: 8px; padding: 10px 12px; } .o_fc_h6 { display: flex; align-items: center; font-size: 0.9rem; font-weight: 700; margin-bottom: 8px; color: var(--fc-text); } .o_fc_tag { display: inline-block; font-size: 0.65rem; padding: 2px 7px; border-radius: 4px; background: var(--fc-banner-border); color: var(--fc-banner-text); margin-left: 8px; text-transform: uppercase; letter-spacing: 0.5px; font-weight: 600; } .o_fc_tile { background: var(--fc-tile-bg); border: 1px solid var(--fc-tile-border); border-radius: 6px; padding: 8px 6px; text-align: center; font-size: 0.75rem; line-height: 1.3; cursor: pointer; transition: transform 0.15s ease, box-shadow 0.15s ease; &:hover { transform: translateY(-1px); box-shadow: 0 2px 4px rgba(0,0,0,0.1); } } .o_fc_tile__num { display: block; font-size: 1.3rem; font-weight: 700; color: var(--fc-tile-num); margin-bottom: 2px; } .o_fc_tile--urgent { background: var(--fc-urgent-bg); border-color: var(--fc-urgent-border); color: var(--fc-urgent-text); .o_fc_tile__num { color: var(--fc-urgent-num); } } .o_fc_activities { background: var(--fc-activity-bg); border: 1px solid var(--fc-activity-border); border-radius: 8px; padding: 10px 12px; } .o_fc_activity_row { display: flex; justify-content: space-between; padding: 4px 0; border-bottom: 1px dashed var(--fc-card-border); font-size: 0.85rem; &:last-child { border-bottom: none; } } .o_fc_activity_overdue { color: var(--fc-urgent-text); font-weight: 600; } .o_fc_activity_deadline { color: var(--fc-text-muted); } .o_fc_empty { color: var(--fc-text-muted); font-style: italic; text-align: center; padding: 12px; margin: 0; } .o_fc_bottleneck { background: var(--fc-bottleneck-bg); border: 1px solid var(--fc-bottleneck-border); border-radius: 8px; padding: 10px 12px; } .o_fc_bottleneck_row { display: block; width: 100%; text-align: left; padding: 4px 0; color: var(--fc-text); text-decoration: none; &:hover { color: var(--fc-urgent-num); text-decoration: underline; } } // Countdown widget colour levels (driven by OWL state) .o_fc_countdown { display: inline-block; padding: 2px 8px; border-radius: 12px; font-weight: 700; font-size: 0.85rem; } .o_fc_countdown--info { color: var(--fc-banner-text); } .o_fc_countdown--warning { color: #d97706; } // amber (intentional fixed hex) .o_fc_countdown--danger { color: var(--fc-urgent-num); } .o_fc_countdown--muted { color: var(--fc-text-muted); font-style: italic; } } ``` - [ ] **Step 3: Register both SCSS files in the manifest, in both bundles** Open `fusion_claims/__manifest__.py`. Replace the `'assets':` block with: ```python 'assets': { 'web.assets_backend': [ # Existing module styles + JS — preserve order 'fusion_claims/static/src/scss/fusion_claims.scss', 'fusion_claims/static/src/js/document_preview.js', 'fusion_claims/static/src/js/preview_button_widget.js', 'fusion_claims/static/src/js/status_selection_filter.js', 'fusion_claims/static/src/js/gallery_preview.js', 'fusion_claims/static/src/js/tax_totals_patch.js', 'fusion_claims/static/src/js/google_address_autocomplete.js', 'fusion_claims/static/src/js/calendar_store_hours.js', 'fusion_claims/static/src/js/attachment_image_compress.js', 'fusion_claims/static/src/js/debug_required_fields.js', 'fusion_claims/static/src/xml/document_preview.xml', # NEW: dashboard tokens MUST load before dashboard layout 'fusion_claims/static/src/scss/_fc_dashboard_tokens.scss', 'fusion_claims/static/src/scss/fc_dashboard.scss', ], 'web.assets_web_dark': [ # NEW: dark bundle re-compiles the same SCSS with the dark # $o-webclient-color-scheme default so tokens branch correctly. 'fusion_claims/static/src/scss/_fc_dashboard_tokens.scss', 'fusion_claims/static/src/scss/fc_dashboard.scss', ], }, ``` - [ ] **Step 4: Upgrade the module and inspect for SCSS errors** Run: ```bash docker exec odoo-modsdev-app odoo -d modsdev -u fusion_claims --stop-after-init 2>&1 | tail -40 ``` Expected: no `SCSS compile error` lines; final lines show `Modules loaded.` - [ ] **Step 5: Verify both bundles compile distinct URLs** Run via odoo-shell. Note: this is a one-off verification, not automated: ```bash docker exec -i odoo-modsdev-app odoo shell -d modsdev --no-http <<'PY' 2>&1 | tail -10 backend = env['ir.qweb']._get_asset_bundle('web.assets_backend') dark = env['ir.qweb']._get_asset_bundle('web.assets_web_dark') print('LIGHT_URL:', backend.get_files_info()[0] if backend.get_files_info() else None) print('DARK_URL:', dark.get_files_info()[0] if dark.get_files_info() else None) PY ``` Expected: two different URLs printed. If identical, see CLAUDE.md §Asset Cache Busting for fixes (delete `ir.attachment` rows under `/web/assets/%`, restart). - [ ] **Step 6: Commit** ```bash git -C K:/Github/Odoo-Modules add fusion_claims/static/src/scss/_fc_dashboard_tokens.scss fusion_claims/static/src/scss/fc_dashboard.scss fusion_claims/__manifest__.py git -C K:/Github/Odoo-Modules commit -m "feat(fusion_claims): add dashboard SCSS with dual-bundle theming" ``` --- ## Task 11: OWL countdown widget **Files:** - Create: `fusion_claims/static/src/js/fc_posting_countdown.js` - Create: `fusion_claims/static/src/xml/fc_posting_countdown.xml` - Modify: `fusion_claims/__manifest__.py` (register the new JS + XML) - [ ] **Step 1: Create the OWL widget JS** Create `fusion_claims/static/src/js/fc_posting_countdown.js`: ```javascript /** @odoo-module **/ // Fusion Claims — Posting Period Countdown // Reads the submission_deadline_dt field, computes "Nd Xh to cutoff" client-side, // re-renders every 60 seconds, swaps colour class as the deadline approaches. // Copyright 2026 Nexa Systems Inc. // License OPL-1 import { Component, useState, onWillStart, onWillDestroy } from "@odoo/owl"; import { registry } from "@web/core/registry"; import { standardFieldProps } from "@web/views/fields/standard_field_props"; class FcPostingCountdown extends Component { static template = "fusion_claims.PostingCountdown"; static props = { ...standardFieldProps }; setup() { this.state = useState({ text: "", level: "info" }); this._render(); this._timer = setInterval(() => this._render(), 60_000); onWillDestroy(() => { if (this._timer) { clearInterval(this._timer); this._timer = null; } }); } _render() { const deadline = this.props.record.data[this.props.name]; if (!deadline) { this.state.text = ""; this.state.level = "muted"; return; } // Odoo gives a luxon DateTime for Datetime fields const now = luxon.DateTime.now(); const diff = deadline.diff(now, ["days", "hours", "minutes"]).toObject(); if (diff.days < 0 || (diff.days === 0 && diff.hours < 0)) { this.state.text = "Cutoff passed"; this.state.level = "muted"; return; } const days = Math.floor(diff.days); const hours = Math.floor(diff.hours); if (days < 1) { this.state.text = `${hours}h to cutoff`; this.state.level = "danger"; } else if (days < 3) { this.state.text = `${days}d ${hours}h to cutoff`; this.state.level = "warning"; } else { this.state.text = `${days} days to cutoff`; this.state.level = "info"; } } } registry.category("fields").add("fc_posting_countdown", { component: FcPostingCountdown, }); ``` - [ ] **Step 2: Create the OWL template** Create `fusion_claims/static/src/xml/fc_posting_countdown.xml`: ```xml ``` - [ ] **Step 3: Register the JS + XML in the manifest** In `fusion_claims/__manifest__.py`, extend the `web.assets_backend` list (add these two lines AFTER the SCSS dashboard files added in Task 10): ```python 'web.assets_backend': [ # ...existing entries plus Task 10 SCSS lines... 'fusion_claims/static/src/scss/_fc_dashboard_tokens.scss', 'fusion_claims/static/src/scss/fc_dashboard.scss', # NEW: countdown widget 'fusion_claims/static/src/js/fc_posting_countdown.js', 'fusion_claims/static/src/xml/fc_posting_countdown.xml', ], ``` > Do NOT add the JS/XML to `web.assets_web_dark` — Odoo loads JS once from the backend bundle; only SCSS goes in both. - [ ] **Step 4: Upgrade the module to register the widget** Run: ```bash docker exec odoo-modsdev-app odoo -d modsdev -u fusion_claims --stop-after-init 2>&1 | tail -20 ``` Expected: no template or JS errors. Last line `Modules loaded.` - [ ] **Step 5: Manual browser smoke test** Open the dashboard menu in the browser (http://localhost:8069 → ADP Claims → Dashboard). Confirm: - The deadline area now shows text like `3 days to cutoff` (info colour) or `2d 5h to cutoff` (warning) instead of the raw datetime. - The text colour matches the level (info = banner-text colour, warning = amber, danger = red). - Leave the page open for ~60 seconds and verify the displayed minutes shift by one (the widget re-renders). - [ ] **Step 6: Run all tests to confirm no Python regression** Run: ```bash docker exec odoo-modsdev-app odoo -d modsdev -u fusion_claims --test-enable --test-tags fusion_claims:TestFusionClaimsDashboard --stop-after-init 2>&1 | tail -15 ``` Expected: 31 tests still pass. - [ ] **Step 7: Commit** ```bash git -C K:/Github/Odoo-Modules add fusion_claims/static/src/js/fc_posting_countdown.js fusion_claims/static/src/xml/fc_posting_countdown.xml fusion_claims/__manifest__.py git -C K:/Github/Odoo-Modules commit -m "feat(fusion_claims): add OWL countdown widget for posting deadline" ``` --- ## Task 12: Manifest version bump + end-to-end smoke test **Files:** - Modify: `fusion_claims/__manifest__.py` (version bump) - [ ] **Step 1: Bump module version for asset cache-bust** In `fusion_claims/__manifest__.py`, change: ```python 'version': '19.0.8.0.7', ``` to: ```python 'version': '19.0.9.0.0', ``` Per CLAUDE.md §Asset Cache Busting: bump the minor version so browsers don't serve stale CSS/JS. - [ ] **Step 2: Upgrade the module clean** Run: ```bash docker exec odoo-modsdev-app odoo -d modsdev -u fusion_claims --stop-after-init 2>&1 | tail -10 ``` Expected: `Modules loaded.` and no errors. - [ ] **Step 3: Run the full module test suite (not just our new tests)** Run: ```bash docker exec odoo-modsdev-app odoo -d modsdev -u fusion_claims --test-enable --test-tags fusion_claims --stop-after-init 2>&1 | tail -30 ``` Expected: all `fusion_claims`-tagged tests pass — existing `test_signed_pages_gate.py` (11 tests) + `test_application_received_wizard.py` (17 tests) + new `test_dashboard.py` (31 tests). Total ~59 tests. - [ ] **Step 4: Manual end-to-end light-mode smoke test** In a browser (http://localhost:8069): - Log in as admin. - Navigate ADP Claims → Dashboard. - Verify visually: - Banner gradient is purple → pink wash with dark-indigo text. - 3 KPI tiles render with $ amounts and the small uppercase labels below. - 8 quick-action pills are green-tinted on white. - Left column shows yellow-tinted Activities, red-tinted Bottlenecks, white Other Funders section. - Right column shows three white ADP/MOD sections. - Two ADP tiles (Waiting App, Needs Correction) and the On Hold tile are red-tinted (urgent). - Countdown widget text reflects "days to cutoff". - Click any workflow tile — confirm it opens a filtered SO list with the matching status. - Click `+ ADP` — confirm a fresh SO form opens with `Sale Type` already set to `adp`. - [ ] **Step 5: Manual end-to-end dark-mode smoke test** - Click user profile (top right) → Color Scheme → Dark. - Page reloads. - Navigate ADP Claims → Dashboard. - Verify visually: - Banner is the cool blue monochrome (#1e293b → #1e3a5f) with sky-blue text. - Card surfaces are #22262d-ish dark, borders subtle. - Urgent tiles are dark red (#4a1414) with light coral numbers. - KPI numbers are sky blue, readable on the dark slate KPI tile background. - No element appears invisible / mid-grey on mid-grey. - [ ] **Step 6: Manual role-filter check** - Create or use an existing user who is in `fusion_claims.group_fusion_claims_user` but NOT in `fusion_claims.group_fusion_claims_manager`. - Log in as that user. - Open the dashboard. - Verify: - The "Showing your assigned cases only" alert is visible. - All tile counts reflect only the SOs where `user_id = this user`. - Switching back to admin (manager) — alert disappears, counts go back to full. - [ ] **Step 7: Final commit** ```bash git -C K:/Github/Odoo-Modules add fusion_claims/__manifest__.py git -C K:/Github/Odoo-Modules commit -m "feat(fusion_claims): bump version to 19.0.9.0.0 for dashboard rewrite" ``` --- ## Plan Self-Review Notes **Spec coverage check:** | Spec section | Implementing task | |---|---| | §2 audience / role filter | Task 1, verified across all subsequent count tasks | | §3 scope — banner | Task 2 | | §3 scope — 3 KPI tiles | Task 3 | | §3 scope — 8 hotlinks | Task 8 | | §3 scope — activities | Task 4 | | §3 scope — bottlenecks | Task 4 | | §3 scope — ADP pre/post | Task 6 | | §3 scope — MOD | Task 6 | | §3 scope — other funders | Task 5 | | §3 scope — dark/light SCSS | Task 10 | | §4.2 file list | Each task creates/modifies its file | | §5 role filter | Task 1 | | §6 field inventory (36) | Tasks 1–6 | | §7 compute clustering (5 methods) | Distributed across Tasks 1–6 | | §8 action methods (~24) | Tasks 7 + 8 | | §9 SCSS structure | Task 10 | | §10 OWL countdown | Task 11 | | §11 manifest changes | Task 10 (assets) + Task 12 (version) | | §12 edge cases | §12.1 pre-first-posting handled in Task 2; §12.2 empty system handled by empty-state HTML in Task 4; §12.3 salesrep-no-cases handled by role filter; §12.4 portal user gating preserved by reusing the existing menu item; §12.5 multi-currency limitation documented in the spec, not enforced in code | | §14 acceptance criteria | Task 12 manual smoke test covers AC #1, #2, #3, #4, #5, #7, #8, #9, #10. AC #6 is exercised by clicking the bottleneck tile in Step 4. | **Placeholder scan:** none found. All steps contain runnable code or commands. **Type consistency:** field names match across tasks. Method names match between definition (Task 1, 4, 5, 6, 7, 8) and consumers (form view in Task 9). The widget name `fc_posting_countdown` is consistent between Task 9 (use site) and Task 11 (definition). --- Plan complete and saved to `docs/superpowers/plans/2026-05-21-fusion-claims-dashboard.md`.