# -*- coding: utf-8 -*- # Copyright 2026 Nexa Systems Inc. # License OPL-1 (Odoo Proprietary License v1.0) # Part of the Fusion Plating product family. """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/snapshot', type='jsonrpc', auth='user', methods=['POST']) def snapshot(self): """Return the full dashboard snapshot. See spec Β§Data model.""" return FpQualityDashboardSnapshot(request.env).build()