From 72f75fe754e49f92bb2380a78e19161841bf6096 Mon Sep 17 00:00:00 2001 From: gsinghpal Date: Mon, 25 May 2026 12:19:48 -0400 Subject: [PATCH] feat(quality_dashboard): snapshot endpoint scaffold (Task 2) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replaces /counts with /snapshot. Helper class FpQualityDashboardSnapshot returns response with correct shape β€” banner placeholder + per-type sections with open/overdue counts (reuses old counts endpoint thresholds). Items + critical-customer banner come in Tasks 3-5. Per CLAUDE.md Rule 13m, Model.sudo() on cross-module reads. Per Rule 24 the in-self.env check guards missing-model paths. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../controllers/fp_quality_dashboard.py | 223 +++++++++++------- 1 file changed, 135 insertions(+), 88 deletions(-) diff --git a/fusion_plating/fusion_plating_quality/controllers/fp_quality_dashboard.py b/fusion_plating/fusion_plating_quality/controllers/fp_quality_dashboard.py index 1ec09b83..386abd8b 100644 --- a/fusion_plating/fusion_plating_quality/controllers/fp_quality_dashboard.py +++ b/fusion_plating/fusion_plating_quality/controllers/fp_quality_dashboard.py @@ -2,99 +2,146 @@ # Copyright 2026 Nexa Systems Inc. # License OPL-1 (Odoo Proprietary License v1.0) # Part of the Fusion Plating product family. -# -# Sub 12 Phase D β€” counts endpoint for the Unified Quality Dashboard. +"""Quality Dashboard snapshot endpoint. + +Spec: docs/superpowers/specs/2026-05-25-quality-dashboard-redesign-design.md + +Replaces the old /fp/quality/dashboard/counts router-style endpoint +with a single /fp/quality/dashboard/snapshot that returns: + - banner: up to 6 most urgent items across all types (red "Needs + Attention Today" surface) + - sections: 6 per-type sections in canonical order, each with top-5 + items + open/overdue counts +""" +import logging +from datetime import timedelta from odoo import fields, http from odoo.http import request +_logger = logging.getLogger(__name__) + + +# Canonical section order β€” drives sections[] in the response. +# Sections whose model isn't installed are omitted (no error). +SECTION_ORDER = ['cert', 'hold', 'ncr', 'rma', 'capa', 'check'] + + +# Per-type config: model name, kanban xmlid, display label, icon. +TYPE_CONFIG = { + 'cert': {'model': 'fp.certificate', + 'label': 'Certificates', 'icon': '🏷️', + 'kanban_xmlid': 'fusion_plating_certificates.action_fp_certificate'}, + 'hold': {'model': 'fusion.plating.quality.hold', + 'label': 'Holds', 'icon': 'πŸ›‘', + 'kanban_xmlid': 'fusion_plating_quality.action_fusion_plating_quality_hold'}, + 'ncr': {'model': 'fusion.plating.ncr', + 'label': 'NCRs', 'icon': 'πŸ”¬', + 'kanban_xmlid': 'fusion_plating_quality.action_fusion_plating_ncr'}, + 'rma': {'model': 'fusion.plating.rma', + 'label': 'RMAs', 'icon': '↩️', + 'kanban_xmlid': 'fusion_plating_quality.action_fusion_plating_rma'}, + 'capa': {'model': 'fusion.plating.capa', + 'label': 'CAPAs', 'icon': 'πŸ“‹', + 'kanban_xmlid': 'fusion_plating_quality.action_fusion_plating_capa'}, + 'check': {'model': 'fusion.plating.quality.check', + 'label': 'QC Checks', 'icon': 'βœ“', + 'kanban_xmlid': 'fusion_plating_quality.action_fusion_plating_quality_check'}, +} + + +# Per-type "overdue" thresholds (reused from the old counts endpoint β€” +# battle-tested). CAPA branches on due_date < today via use_due_date. +OVERDUE_THRESHOLDS = { + 'cert': {'days': 1, 'use_due_date': False, + 'state_domain': [('state', '=', 'draft')]}, + 'hold': {'days': 3, 'use_due_date': False, + 'state_domain': [('state', 'in', ('on_hold', 'under_review'))]}, + 'ncr': {'days': 7, 'use_due_date': False, + 'state_domain': [('state', 'in', + ('open', 'containment', 'disposition'))]}, + 'rma': {'days': 5, 'use_due_date': False, + 'state_domain': [('state', '=', 'received')]}, + 'capa': {'days': None, 'use_due_date': True, + 'state_domain': [('state', 'not in', + ('closed', 'effective'))]}, + 'check': {'days': 1, 'use_due_date': False, + 'state_domain': [('state', '=', 'pending')]}, +} + + +class FpQualityDashboardSnapshot: + """Builds the dashboard snapshot in one pass. + + Per Rule 13m, all cross-module field reads are sudo'd so non-admin + users (Sales Reps viewing the dashboard) don't AccessError on + partner_id.x_fc_rush / part_catalog_id.name / customer_spec_id.code. + The Open-action navigates via standard act_window which re-enforces + ACL on click (Rule 24 / D15). + """ + + BANNER_MAX = 6 + SECTION_TOP_N = 5 + + def __init__(self, env): + self.env = env + self.now = fields.Datetime.now() + self.today = fields.Date.context_today(env.user) + + def build(self): + sections = [] + for type_code in SECTION_ORDER: + section = self._build_section(type_code) + if section is None: + continue # model not installed β€” omit + sections.append(section) + # Banner placeholder for Task 2 β€” fully implemented in Task 4. + banner = {'items': [], 'all_clear': True, 'total_matching': 0} + return { + 'banner': banner, + 'sections': sections, + 'computed_at': self.now.isoformat(), + } + + def _build_section(self, type_code): + """Return section dict with top-N items + counts. + Returns None when the model isn't installed.""" + cfg = TYPE_CONFIG[type_code] + if cfg['model'] not in self.env: + return None + # Open/overdue counts using the existing state filters + state_dom = list(OVERDUE_THRESHOLDS[type_code]['state_domain']) + Model = self.env[cfg['model']].sudo() + open_count = Model.search_count(state_dom) + overdue_count = self._overdue_count(type_code, Model) + # Section items β€” fully populated in Task 3; empty for now + return { + 'type': type_code, + 'label': cfg['label'], + 'icon': cfg['icon'], + 'open': open_count, + 'overdue': overdue_count, + 'items': [], + 'open_kanban_xmlid': cfg['kanban_xmlid'], + } + + def _overdue_count(self, type_code, Model): + """Count overdue per the OVERDUE_THRESHOLDS dispatch table.""" + cfg = OVERDUE_THRESHOLDS[type_code] + dom = list(cfg['state_domain']) + if cfg['use_due_date']: + dom += [('due_date', '<', self.today), + ('due_date', '!=', False)] + else: + cutoff = self.now - timedelta(days=cfg['days']) + dom += [('create_date', '<', cutoff)] + return Model.search_count(dom) + class FpQualityDashboardController(http.Controller): - @http.route('/fp/quality/dashboard/counts', + @http.route('/fp/quality/dashboard/snapshot', type='jsonrpc', auth='user', methods=['POST']) - def counts(self): - """Return per-tab open + overdue counts for the dashboard. - - "Overdue" definition: - - Hold: state='on_hold' for > 3 days - - Check: state='pending' for > 1 day - - NCR: state in (open, containment, disposition) AND reported >7d - - CAPA: due_date < today AND state not in (effective, closed) - - RMA: state='received' for > 5 days (triage past due) OR - state in (authorised, shipped_to_us) for > 14 days - - Certificate: state='draft' for > 1 day (spec 2026-05-25) - """ - env = request.env - today = fields.Date.context_today(env.user) - now = fields.Datetime.now() - - Hold = env['fusion.plating.quality.hold'] - Check = env['fusion.plating.quality.check'] - Ncr = env['fusion.plating.ncr'] - Capa = env['fusion.plating.capa'] - Rma = env['fusion.plating.rma'] - Cert = env['fp.certificate'] if 'fp.certificate' in env else None - - d3 = fields.Datetime.subtract(now, days=3) - d1 = fields.Datetime.subtract(now, days=1) - d7 = fields.Datetime.subtract(now, days=7) - d5 = fields.Datetime.subtract(now, days=5) - d14 = fields.Datetime.subtract(now, days=14) - - return { - 'holds': { - 'open': Hold.search_count( - [('state', 'in', ('on_hold', 'under_review'))]), - 'overdue': Hold.search_count([ - ('state', 'in', ('on_hold', 'under_review')), - ('create_date', '<', d3), - ]), - }, - 'checks': { - 'open': Check.search_count([('state', '=', 'pending')]), - 'overdue': Check.search_count([ - ('state', '=', 'pending'), - ('create_date', '<', d1), - ]), - }, - 'ncrs': { - 'open': Ncr.search_count([ - ('state', 'in', ('open', 'containment', 'disposition')), - ]), - 'overdue': Ncr.search_count([ - ('state', 'in', ('open', 'containment', 'disposition')), - ('reported_date', '<', d7), - ]), - }, - 'capas': { - 'open': Capa.search_count([ - ('state', 'not in', ('effective', 'closed')), - ]), - 'overdue': Capa.search_count([ - ('state', 'not in', ('effective', 'closed')), - ('due_date', '<', today), - ('due_date', '!=', False), - ]), - }, - 'rmas': { - 'open': Rma.search_count([ - ('state', 'not in', ('closed', 'cancelled')), - ]), - 'overdue': Rma.search_count([ - '|', - '&', ('state', '=', 'received'), - ('create_date', '<', d5), - '&', ('state', 'in', ('authorised', 'shipped_to_us')), - ('create_date', '<', d14), - ]), - }, - # Spec 2026-05-25 β€” Certificates tab - 'certificates': ({ - 'open': Cert.search_count([('state', '=', 'draft')]), - 'overdue': Cert.search_count([ - ('state', '=', 'draft'), - ('create_date', '<', d1), - ]), - } if Cert is not None else {'open': 0, 'overdue': 0}), - } + def snapshot(self): + """Return the full dashboard snapshot. See spec Β§Data model.""" + return FpQualityDashboardSnapshot(request.env).build()