diff --git a/fusion_plating/CLAUDE.md b/fusion_plating/CLAUDE.md index c48e0f72..e73f7730 100644 --- a/fusion_plating/CLAUDE.md +++ b/fusion_plating/CLAUDE.md @@ -460,10 +460,12 @@ invisible="x_fc_ncr_count == 0" diff --git a/fusion_plating/fusion_plating_jobs/views/sale_order_views.xml b/fusion_plating/fusion_plating_jobs/views/sale_order_views.xml index 29936b99..73aeef90 100644 --- a/fusion_plating/fusion_plating_jobs/views/sale_order_views.xml +++ b/fusion_plating/fusion_plating_jobs/views/sale_order_views.xml @@ -15,11 +15,15 @@ + Hidden at a count of 0 (2026-06-04) so the SO button row only + shows what has data. A confirmed plating SO always has >=1 WO, + so this only hides the button on drafts/quotations with no job + yet; the header "Work Order" button below still reaches jobs + when one is open. --> diff --git a/fusion_plating/fusion_plating_quality/__manifest__.py b/fusion_plating/fusion_plating_quality/__manifest__.py index cfc72ab6..c160d3ae 100644 --- a/fusion_plating/fusion_plating_quality/__manifest__.py +++ b/fusion_plating/fusion_plating_quality/__manifest__.py @@ -5,7 +5,7 @@ { 'name': 'Fusion Plating — Quality (QMS)', - 'version': '19.0.8.1.0', + 'version': '19.0.8.2.0', 'category': 'Manufacturing/Plating', 'summary': 'Native QMS for plating shops: NCR, CAPA, calibration, AVL, FAIR, ' 'internal audits, customer specs, document control. CE + EE compatible.', diff --git a/fusion_plating/fusion_plating_quality/models/fp_quality_smart_buttons.py b/fusion_plating/fusion_plating_quality/models/fp_quality_smart_buttons.py index dbc4bf8c..173b0b76 100644 --- a/fusion_plating/fusion_plating_quality/models/fp_quality_smart_buttons.py +++ b/fusion_plating/fusion_plating_quality/models/fp_quality_smart_buttons.py @@ -6,8 +6,10 @@ # Sub 12 Phase D — smart-button counts on fp.job, sale.order, res.partner. # # Each parent record gets badge counts for: Holds, Checks, NCRs, CAPAs, -# RMAs. Counts always render (zero is acceptable). Action methods open -# the relevant kanban filtered to that record. +# RMAs. The buttons hide at a count of 0 (set in the view XML), so the row +# only shows quality work that actually exists. NCR/CAPA counts are scoped +# to the order/job via RMAs + holds (see _fp_quality_ncr_ids) — NOT the +# customer's whole NCR history. Action methods open the matching list. from odoo import _, api, fields, models @@ -35,7 +37,6 @@ class FpJobQualitySmart(models.Model): def _compute_fp_quality_counts(self): Hold = self.env['fusion.plating.quality.hold'] Check = self.env['fusion.plating.quality.check'] - Ncr = self.env['fusion.plating.ncr'] Capa = self.env['fusion.plating.capa'] Rma = self.env['fusion.plating.rma'] for job in self: @@ -43,23 +44,39 @@ class FpJobQualitySmart(models.Model): [('job_id', '=', job.id)]) job.fp_qc_check_count = Check.search_count( [('job_id', '=', job.id)]) - ncr_ids = [] - capa_ids = [] - rma_ids = [] - if job.sale_order_id: - rma_ids = Rma.search( - [('sale_order_id', '=', job.sale_order_id.id)]).ids - if rma_ids: - ncr_ids = Ncr.search([('rma_id', 'in', rma_ids)]).ids - if job.partner_id: - ncr_ids = list(set(ncr_ids + Ncr.search([ - ('customer_partner_id', '=', job.partner_id.id), - ]).ids)) - if ncr_ids: - capa_ids = Capa.search([('ncr_id', 'in', ncr_ids)]).ids + job.fp_qc_rma_count = Rma.search_count( + [('sale_order_id', '=', job.sale_order_id.id)] + ) if job.sale_order_id else 0 + ncr_ids = job._fp_quality_ncr_ids() job.fp_qc_ncr_count = len(ncr_ids) - job.fp_qc_capa_count = len(capa_ids) - job.fp_qc_rma_count = len(rma_ids) + job.fp_qc_capa_count = Capa.search_count( + [('ncr_id', 'in', ncr_ids)]) if ncr_ids else 0 + + def _fp_quality_ncr_ids(self): + """Job-scoped NCR ids — single source of truth for both the badge + count and the smart-button list. + + There is no ncr.job_id / ncr.sale_order_id field, so the only honest + links are: + - NCRs reached via this job's order's RMAs (ncr.rma_id), and + - NCRs spawned from this job's holds (hold.ncr_id). + An NCR with neither link is customer-level history with no tie to a + single job, so it is intentionally excluded. + """ + self.ensure_one() + Ncr = self.env['fusion.plating.ncr'] + Hold = self.env['fusion.plating.quality.hold'] + Rma = self.env['fusion.plating.rma'] + ncr_ids = set() + if self.sale_order_id: + rma_ids = Rma.search( + [('sale_order_id', '=', self.sale_order_id.id)]).ids + if rma_ids: + ncr_ids.update(Ncr.search([('rma_id', 'in', rma_ids)]).ids) + ncr_ids.update(Hold.search([ + ('job_id', '=', self.id), ('ncr_id', '!=', False), + ]).mapped('ncr_id').ids) + return list(ncr_ids) def action_view_fp_holds(self): self.ensure_one() @@ -85,43 +102,39 @@ class FpJobQualitySmart(models.Model): def action_view_fp_ncrs(self): self.ensure_one() - domain = [('customer_partner_id', '=', self.partner_id.id)] return { 'name': _('NCRs'), 'type': 'ir.actions.act_window', 'res_model': 'fusion.plating.ncr', 'view_mode': 'kanban,list,form', - 'domain': domain, + 'domain': [('id', 'in', self._fp_quality_ncr_ids())], 'context': {'default_customer_partner_id': self.partner_id.id}, } def action_view_fp_capas(self): self.ensure_one() - Ncr = self.env['fusion.plating.ncr'] - ncr_ids = Ncr.search([ - ('customer_partner_id', '=', self.partner_id.id), - ]).ids return { 'name': _('CAPAs'), 'type': 'ir.actions.act_window', 'res_model': 'fusion.plating.capa', 'view_mode': 'list,form', - 'domain': [('ncr_id', 'in', ncr_ids)], + 'domain': [('ncr_id', 'in', self._fp_quality_ncr_ids())], } def action_view_fp_rmas(self): self.ensure_one() - domain = [('partner_id', '=', self.partner_id.id)] - if self.sale_order_id: - domain = ['|', ('sale_order_id', '=', self.sale_order_id.id), - ('partner_id', '=', self.partner_id.id)] + # SO-scoped to match the badge count. RMA.sale_order_id is required, + # so a job with no order matches nothing — same as the count's 0. return { 'name': _('RMAs'), 'type': 'ir.actions.act_window', 'res_model': 'fusion.plating.rma', 'view_mode': 'kanban,list,form', - 'domain': domain, - 'context': {'default_partner_id': self.partner_id.id}, + 'domain': [('sale_order_id', '=', self.sale_order_id.id)], + 'context': { + 'default_partner_id': self.partner_id.id, + 'default_sale_order_id': self.sale_order_id.id, + }, } @@ -148,7 +161,6 @@ class SaleOrderQualitySmart(models.Model): def _compute_fp_qc_counts(self): Hold = self.env['fusion.plating.quality.hold'] Check = self.env['fusion.plating.quality.check'] - Ncr = self.env['fusion.plating.ncr'] Capa = self.env['fusion.plating.capa'] Rma = self.env['fusion.plating.rma'] Job = self.env['fp.job'] @@ -158,19 +170,40 @@ class SaleOrderQualitySmart(models.Model): [('job_id', 'in', job_ids)]) if job_ids else 0 so.fp_qc_check_count = Check.search_count( [('job_id', 'in', job_ids)]) if job_ids else 0 - rma_ids = Rma.search([('sale_order_id', '=', so.id)]).ids - so.fp_qc_rma_count = len(rma_ids) - ncr_ids = [] - if rma_ids: - ncr_ids = Ncr.search([('rma_id', 'in', rma_ids)]).ids - if so.partner_id: - ncr_ids = list(set(ncr_ids + Ncr.search([ - ('customer_partner_id', '=', so.partner_id.id), - ]).ids)) + so.fp_qc_rma_count = Rma.search_count( + [('sale_order_id', '=', so.id)]) + ncr_ids = so._fp_quality_ncr_ids() so.fp_qc_ncr_count_so = len(ncr_ids) so.fp_qc_capa_count = Capa.search_count( [('ncr_id', 'in', ncr_ids)]) if ncr_ids else 0 + def _fp_quality_ncr_ids(self): + """Order-scoped NCR ids — single source of truth for both the badge + count and the smart-button list. + + There is no ncr.sale_order_id / ncr.job_id field, so the only honest + links are: + - NCRs reached via this order's RMAs (ncr.rma_id), and + - NCRs spawned from holds on this order's jobs (hold.ncr_id). + An NCR with neither link is customer-level history with no tie to a + single order, so it is intentionally excluded. + """ + self.ensure_one() + Ncr = self.env['fusion.plating.ncr'] + Hold = self.env['fusion.plating.quality.hold'] + Rma = self.env['fusion.plating.rma'] + Job = self.env['fp.job'] + ncr_ids = set() + rma_ids = Rma.search([('sale_order_id', '=', self.id)]).ids + if rma_ids: + ncr_ids.update(Ncr.search([('rma_id', 'in', rma_ids)]).ids) + job_ids = Job.search([('sale_order_id', '=', self.id)]).ids + if job_ids: + ncr_ids.update(Hold.search([ + ('job_id', 'in', job_ids), ('ncr_id', '!=', False), + ]).mapped('ncr_id').ids) + return list(ncr_ids) + def action_view_fp_holds(self): self.ensure_one() Job = self.env['fp.job'] @@ -202,21 +235,17 @@ class SaleOrderQualitySmart(models.Model): 'type': 'ir.actions.act_window', 'res_model': 'fusion.plating.ncr', 'view_mode': 'kanban,list,form', - 'domain': [('customer_partner_id', '=', self.partner_id.id)], + 'domain': [('id', 'in', self._fp_quality_ncr_ids())], } def action_view_fp_capas(self): self.ensure_one() - Ncr = self.env['fusion.plating.ncr'] - ncr_ids = Ncr.search([ - ('customer_partner_id', '=', self.partner_id.id), - ]).ids return { 'name': _('CAPAs'), 'type': 'ir.actions.act_window', 'res_model': 'fusion.plating.capa', 'view_mode': 'list,form', - 'domain': [('ncr_id', 'in', ncr_ids)], + 'domain': [('ncr_id', 'in', self._fp_quality_ncr_ids())], } def action_view_fp_rmas(self): diff --git a/fusion_plating/fusion_plating_quality/views/fp_quality_smart_button_views.xml b/fusion_plating/fusion_plating_quality/views/fp_quality_smart_button_views.xml index 1576c00c..18ed2819 100644 --- a/fusion_plating/fusion_plating_quality/views/fp_quality_smart_button_views.xml +++ b/fusion_plating/fusion_plating_quality/views/fp_quality_smart_button_views.xml @@ -25,24 +25,31 @@ +