diff --git a/fusion_plating/fusion_plating_jobs/__manifest__.py b/fusion_plating/fusion_plating_jobs/__manifest__.py index 910c2352..597bc30e 100644 --- a/fusion_plating/fusion_plating_jobs/__manifest__.py +++ b/fusion_plating/fusion_plating_jobs/__manifest__.py @@ -3,7 +3,7 @@ # License OPL-1 (Odoo Proprietary License v1.0) { 'name': 'Fusion Plating — Native Jobs', - 'version': '19.0.8.21.5', + 'version': '19.0.8.22.0', 'category': 'Manufacturing/Plating', 'summary': 'Native plating job model — replaces mrp.production / mrp.workorder bridge.', 'author': 'Nexa Systems Inc.', diff --git a/fusion_plating/fusion_plating_jobs/models/sale_order.py b/fusion_plating/fusion_plating_jobs/models/sale_order.py index b249192c..cf588971 100644 --- a/fusion_plating/fusion_plating_jobs/models/sale_order.py +++ b/fusion_plating/fusion_plating_jobs/models/sale_order.py @@ -32,6 +32,40 @@ class SaleOrder(models.Model): 'to drill through the linked Plating Job first.', ) + # ------------------------------------------------------------------ + # Parent-number hierarchy (2026-05-12 design) + # See docs/superpowers/specs/2026-05-12-parent-number-hierarchy-design.md + # ------------------------------------------------------------------ + x_fc_parent_number = fields.Integer( + string='Parent Number', + readonly=True, + copy=False, + index=True, + help='Set on confirm. Drives every linked document\'s name ' + '(WO-NNN, IN-NNN, CoC-NNN, ...). Immutable post-assignment.', + ) + x_fc_quote_ref = fields.Char( + string='Originally Quoted As', + readonly=True, + copy=False, + help='The quote-stage name (e.g. Q202605-200). Preserved when ' + 'the SO is renamed on confirm.', + ) + # Per-model counters — monotonic, never decrement. Source of truth + # for the next sibling's x_fc_doc_index. Updated via row-locked SQL + # in fp.parent.numbered.mixin so concurrent creates can't drift. + x_fc_wo_count = fields.Integer(string='WO Count', readonly=True, copy=False, default=0) + x_fc_invoice_count = fields.Integer(string='Invoice Count', readonly=True, copy=False, default=0) + x_fc_cn_count = fields.Integer(string='Credit Note Count', readonly=True, copy=False, default=0) + x_fc_cert_count = fields.Integer(string='Certificate Count', readonly=True, copy=False, default=0) + x_fc_delivery_count = fields.Integer(string='Delivery Count', readonly=True, copy=False, default=0) + x_fc_receiving_count = fields.Integer(string='Receiving Count', readonly=True, copy=False, default=0) + x_fc_pickup_count = fields.Integer(string='Pickup Count', readonly=True, copy=False, default=0) + x_fc_ncr_count = fields.Integer(string='NCR Count', readonly=True, copy=False, default=0) + x_fc_capa_count = fields.Integer(string='CAPA Count', readonly=True, copy=False, default=0) + x_fc_hold_count = fields.Integer(string='Hold Count', readonly=True, copy=False, default=0) + x_fc_rma_count = fields.Integer(string='RMA Count', readonly=True, copy=False, default=0) + # ------------------------------------------------------------------ # Phase 4 (Sub 11) — workflow-stage field + assigned-manager field # relocated from fusion_plating_bridge_mrp. Field re-declared with @@ -278,13 +312,23 @@ class SaleOrder(models.Model): part = self.x_fc_part_catalog_id or False if not coating and 'x_fc_coating_config_id' in self._fields: coating = self.x_fc_coating_config_id or False - # Recipe lookup priority: + # Recipe lookup priority (specific → generic): # 1. line.x_fc_process_variant_id — Sarah explicitly picked # a part-scoped variant on this order line. Always wins. - # 2. coating.recipe_id — coating-config recipe. - # 3. part.default_process_id — part's flagged default. + # 2. part.default_process_id — part's flagged default + # variant. This is the customer-and-part-tuned recipe + # (months of process refinement) and must beat any + # generic template attached to the coating config. + # 3. coating.recipe_id — coating-config recipe + # (generic template fallback when no part variant exists). # 4. part.recipe_id — legacy fallback. # + # Order swap 2026-05-12: before this, step (3) ran before (2), + # so jobs with both a part variant AND a coating-attached + # template silently picked the template. WO #01373 (S00063) + # is the documented case — part 9876699373 Rev A had its own + # variant but the job linked to ENP-ALUM-BASIC instead. + # # If multiple lines in the same WO group have different # variants we use the FIRST line's variant (consistent with # everything else in this loop using `first_line`). @@ -296,12 +340,12 @@ class SaleOrder(models.Model): ) if picked_variant: recipe = picked_variant - if not recipe and coating and 'recipe_id' in coating._fields \ - and coating.recipe_id: - recipe = coating.recipe_id if not recipe and part and 'default_process_id' in part._fields \ and part.default_process_id: recipe = part.default_process_id + if not recipe and coating and 'recipe_id' in coating._fields \ + and coating.recipe_id: + recipe = coating.recipe_id if not recipe and part and 'recipe_id' in part._fields \ and part.recipe_id: recipe = part.recipe_id