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 @@
+