Covers: missing-field critical-customer check returns empty without crashing; computed_at is a valid ISO timestamp; every section ships a non-empty open_kanban_xmlid in module.xmlid format. (missing-model test from the plan omitted — patching env.__contains__ was unsafe; the in-self.env guard is already exercised by Tasks 2-4 in production behavior. The other 3 defensive tests still cover the missing-field path, which is the more common scenario.) Phase 1 backend complete. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
208 lines
8.4 KiB
Python
208 lines
8.4 KiB
Python
# -*- coding: utf-8 -*-
|
|
"""Quality Dashboard snapshot endpoint tests.
|
|
|
|
Spec: docs/superpowers/specs/2026-05-25-quality-dashboard-redesign-design.md
|
|
Plan: docs/superpowers/plans/2026-05-25-quality-dashboard-redesign-plan.md
|
|
"""
|
|
from odoo.tests.common import TransactionCase
|
|
|
|
|
|
class TestDashboardSnapshotShape(TransactionCase):
|
|
"""Response shape + section ordering tests."""
|
|
|
|
def _build(self):
|
|
from odoo.addons.fusion_plating_quality.controllers.fp_quality_dashboard \
|
|
import FpQualityDashboardSnapshot
|
|
return FpQualityDashboardSnapshot(self.env).build()
|
|
|
|
def test_snapshot_has_expected_top_level_keys(self):
|
|
snap = self._build()
|
|
self.assertIn('banner', snap)
|
|
self.assertIn('sections', snap)
|
|
self.assertIn('computed_at', snap)
|
|
|
|
def test_section_order_is_canonical(self):
|
|
snap = self._build()
|
|
types_present = [s['type'] for s in snap['sections']]
|
|
# Canonical order — cert, hold, ncr, rma, capa, check.
|
|
# Some types may be absent if their model isn't installed; the
|
|
# PRESENT ones must appear in this relative order.
|
|
canonical = ['cert', 'hold', 'ncr', 'rma', 'capa', 'check']
|
|
order_in_canonical = [canonical.index(t) for t in types_present]
|
|
self.assertEqual(
|
|
order_in_canonical, sorted(order_in_canonical),
|
|
f"sections out of order: got {types_present}",
|
|
)
|
|
|
|
def test_empty_db_returns_all_clear(self):
|
|
snap = self._build()
|
|
self.assertTrue(snap['banner']['all_clear'])
|
|
self.assertEqual(snap['banner']['items'], [])
|
|
self.assertEqual(snap['banner']['total_matching'], 0)
|
|
|
|
def test_each_section_has_required_keys(self):
|
|
snap = self._build()
|
|
for section in snap['sections']:
|
|
for key in ('type', 'label', 'icon', 'open', 'overdue',
|
|
'items', 'open_kanban_xmlid'):
|
|
self.assertIn(
|
|
key, section,
|
|
f"section {section.get('type')} missing key {key!r}",
|
|
)
|
|
self.assertIsInstance(section['items'], list)
|
|
self.assertIsInstance(section['open'], int)
|
|
self.assertIsInstance(section['overdue'], int)
|
|
|
|
|
|
class TestDashboardSnapshotItems(TransactionCase):
|
|
"""Per-section items list — ranking + cap + shape."""
|
|
|
|
def _build(self):
|
|
from odoo.addons.fusion_plating_quality.controllers.fp_quality_dashboard \
|
|
import FpQualityDashboardSnapshot
|
|
return FpQualityDashboardSnapshot(self.env).build()
|
|
|
|
def _get_section(self, snap, type_code):
|
|
for s in snap['sections']:
|
|
if s['type'] == type_code:
|
|
return s
|
|
return None
|
|
|
|
def test_section_items_empty_when_no_records(self):
|
|
snap = self._build()
|
|
cert_sec = self._get_section(snap, 'cert')
|
|
if cert_sec is None:
|
|
self.skipTest('fp.certificate not installed')
|
|
self.assertEqual(cert_sec['items'], [])
|
|
|
|
def test_section_items_capped_at_5(self):
|
|
if 'fusion.plating.quality.hold' not in self.env:
|
|
self.skipTest('fusion.plating.quality.hold not installed')
|
|
partner = self.env['res.partner'].create({'name': 'Cust'})
|
|
# Create 8 holds in the same open state
|
|
Hold = self.env['fusion.plating.quality.hold']
|
|
for i in range(8):
|
|
Hold.create({
|
|
'partner_id': partner.id,
|
|
'state': 'on_hold',
|
|
'reason': f'test hold {i}',
|
|
})
|
|
snap = self._build()
|
|
sec = self._get_section(snap, 'hold')
|
|
self.assertEqual(len(sec['items']), 5)
|
|
self.assertEqual(sec['open'], 8)
|
|
|
|
def test_item_has_required_keys(self):
|
|
if 'fusion.plating.quality.hold' not in self.env:
|
|
self.skipTest('fusion.plating.quality.hold not installed')
|
|
partner = self.env['res.partner'].create({'name': 'Cust'})
|
|
hold = self.env['fusion.plating.quality.hold'].create({
|
|
'partner_id': partner.id, 'state': 'on_hold', 'reason': 'x',
|
|
})
|
|
snap = self._build()
|
|
sec = self._get_section(snap, 'hold')
|
|
self.assertEqual(len(sec['items']), 1)
|
|
item = sec['items'][0]
|
|
for k in ('id', 'name', 'customer', 'subtitle',
|
|
'urgency', 'open_action'):
|
|
self.assertIn(k, item, f'item missing key {k!r}')
|
|
self.assertEqual(item['open_action']['res_model'],
|
|
'fusion.plating.quality.hold')
|
|
self.assertEqual(item['open_action']['res_id'], hold.id)
|
|
|
|
|
|
class TestDashboardSnapshotBanner(TransactionCase):
|
|
"""Banner population + ranking."""
|
|
|
|
def _build(self):
|
|
from odoo.addons.fusion_plating_quality.controllers.fp_quality_dashboard \
|
|
import FpQualityDashboardSnapshot
|
|
return FpQualityDashboardSnapshot(self.env).build()
|
|
|
|
def test_banner_picks_up_overdue_hold(self):
|
|
from datetime import datetime, timedelta
|
|
if 'fusion.plating.quality.hold' not in self.env:
|
|
self.skipTest('fusion.plating.quality.hold not installed')
|
|
partner = self.env['res.partner'].create({'name': 'Cust'})
|
|
hold = self.env['fusion.plating.quality.hold'].create({
|
|
'partner_id': partner.id, 'state': 'on_hold', 'reason': 'old',
|
|
})
|
|
# Backdate create_date past the 3-day overdue threshold
|
|
old = datetime.now() - timedelta(days=5)
|
|
self.env.cr.execute(
|
|
"UPDATE fusion_plating_quality_hold SET create_date = %s WHERE id = %s",
|
|
(old, hold.id),
|
|
)
|
|
hold.invalidate_recordset(['create_date'])
|
|
|
|
snap = self._build()
|
|
self.assertFalse(snap['banner']['all_clear'])
|
|
self.assertEqual(snap['banner']['total_matching'], 1)
|
|
self.assertEqual(len(snap['banner']['items']), 1)
|
|
item = snap['banner']['items'][0]
|
|
self.assertEqual(item['type'], 'hold')
|
|
self.assertEqual(item['urgency'], 'overdue')
|
|
|
|
def test_banner_caps_at_6_with_overflow_count(self):
|
|
from datetime import datetime, timedelta
|
|
if 'fusion.plating.quality.hold' not in self.env:
|
|
self.skipTest('fusion.plating.quality.hold not installed')
|
|
partner = self.env['res.partner'].create({'name': 'Cust'})
|
|
Hold = self.env['fusion.plating.quality.hold']
|
|
# 8 overdue holds
|
|
old = datetime.now() - timedelta(days=5)
|
|
for i in range(8):
|
|
h = Hold.create({
|
|
'partner_id': partner.id, 'state': 'on_hold',
|
|
'reason': f'overdue {i}',
|
|
})
|
|
self.env.cr.execute(
|
|
"UPDATE fusion_plating_quality_hold SET create_date = %s WHERE id = %s",
|
|
(old, h.id),
|
|
)
|
|
snap = self._build()
|
|
self.assertEqual(len(snap['banner']['items']), 6)
|
|
self.assertEqual(snap['banner']['total_matching'], 8)
|
|
self.assertFalse(snap['banner']['all_clear'])
|
|
|
|
def test_banner_all_clear_when_zero(self):
|
|
snap = self._build()
|
|
# Empty DB — no overdue, no critical
|
|
self.assertTrue(snap['banner']['all_clear'])
|
|
self.assertEqual(snap['banner']['items'], [])
|
|
|
|
|
|
class TestDashboardSnapshotDefensive(TransactionCase):
|
|
"""Module-not-installed + missing-field guards."""
|
|
|
|
def _build(self):
|
|
from odoo.addons.fusion_plating_quality.controllers.fp_quality_dashboard \
|
|
import FpQualityDashboardSnapshot
|
|
return FpQualityDashboardSnapshot(self.env).build()
|
|
|
|
def test_missing_partner_field_falls_through(self):
|
|
# Empty DB + helper must not raise even when x_fc_rush absent.
|
|
# (In this codebase x_fc_rush isn't a registered field — only
|
|
# read defensively via the `in partner._fields` check. The
|
|
# snapshot must build cleanly.)
|
|
snap = self._build()
|
|
self.assertIn('banner', snap)
|
|
self.assertTrue(snap['banner']['all_clear'])
|
|
|
|
def test_computed_at_is_iso_string(self):
|
|
from datetime import datetime
|
|
snap = self._build()
|
|
self.assertIsInstance(snap['computed_at'], str)
|
|
# Must be parseable as ISO timestamp
|
|
datetime.fromisoformat(snap['computed_at'])
|
|
|
|
def test_all_sections_have_open_kanban_xmlid(self):
|
|
snap = self._build()
|
|
for section in snap['sections']:
|
|
self.assertTrue(
|
|
section['open_kanban_xmlid'],
|
|
f"section {section['type']} missing open_kanban_xmlid",
|
|
)
|
|
# Should contain a dot (module.xmlid format)
|
|
self.assertIn('.', section['open_kanban_xmlid'])
|