feat(fusion_plating_jobs): group WOs by recipe step structure
Replace the old 5-tuple (recipe.id, part, spec, thickness, serial) grouping key with a structural signature so multiple parts that share the same recipe step tree (ENP clones) collapse onto one combined work order. Add three helpers: _fp_recipe_signature, _fp_line_express_signature, _fp_line_group_key. Add TransactionCase test covering merge, non-merge, and masking-split cases. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -11,3 +11,4 @@ from . import test_post_shop_states
|
||||
from . import test_recipe_cert_suppression
|
||||
from . import test_order_ship_state
|
||||
from . import test_combined_cert_creation
|
||||
from . import test_wo_recipe_grouping
|
||||
|
||||
@@ -0,0 +1,101 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from odoo.tests.common import TransactionCase
|
||||
|
||||
|
||||
class TestWoRecipeGrouping(TransactionCase):
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
self.SO = self.env['sale.order']
|
||||
self.Node = self.env['fusion.plating.process.node']
|
||||
# kind_id is required on process.node; reuse any seeded kind so
|
||||
# node creation doesn't depend on the default lookup resolving.
|
||||
self.kind = self.env['fp.step.kind'].search([], limit=1)
|
||||
|
||||
def _node_vals(self, name, node_type):
|
||||
v = {'name': name, 'node_type': node_type}
|
||||
if self.kind:
|
||||
v['kind_id'] = self.kind.id
|
||||
return v
|
||||
|
||||
def _recipe(self, name, step_names):
|
||||
root = self.Node.create(self._node_vals(name, 'recipe'))
|
||||
seq = 10
|
||||
for sn in step_names:
|
||||
v = self._node_vals(sn, 'step')
|
||||
v.update({'parent_id': root.id, 'sequence': seq})
|
||||
self.Node.create(v)
|
||||
seq += 10
|
||||
return root
|
||||
|
||||
def test_identical_structure_same_signature(self):
|
||||
r1 = self._recipe('ENP — PART-A', ['Soak Clean', 'Rinse', 'E-Nickel'])
|
||||
r2 = self._recipe('ENP — PART-B', ['Soak Clean', 'Rinse', 'E-Nickel'])
|
||||
self.assertEqual(
|
||||
self.SO._fp_recipe_signature(r1),
|
||||
self.SO._fp_recipe_signature(r2),
|
||||
'clones with identical steps share a signature')
|
||||
|
||||
def test_different_structure_different_signature(self):
|
||||
r1 = self._recipe('ENP — A', ['Soak Clean', 'Rinse', 'E-Nickel'])
|
||||
r2 = self._recipe('CHROME — B', ['Etch', 'Plate'])
|
||||
self.assertNotEqual(
|
||||
self.SO._fp_recipe_signature(r1),
|
||||
self.SO._fp_recipe_signature(r2))
|
||||
|
||||
def test_so_groups_same_structure_into_one_wo(self):
|
||||
partner = self.env['res.partner'].create({'name': 'G'})
|
||||
product = self.env['product.product'].create({'name': 'P'})
|
||||
pa = self.env['fp.part.catalog'].create({
|
||||
'name': 'A', 'partner_id': partner.id, 'part_number': 'A'})
|
||||
pb = self.env['fp.part.catalog'].create({
|
||||
'name': 'B', 'partner_id': partner.id, 'part_number': 'B'})
|
||||
pc = self.env['fp.part.catalog'].create({
|
||||
'name': 'C', 'partner_id': partner.id, 'part_number': 'C'})
|
||||
r1 = self._recipe('ENP — A', ['Soak Clean', 'Rinse'])
|
||||
r2 = self._recipe('ENP — B', ['Soak Clean', 'Rinse']) # same structure
|
||||
r3 = self._recipe('CHROME — C', ['Etch', 'Plate']) # different
|
||||
so = self.env['sale.order'].create({
|
||||
'partner_id': partner.id,
|
||||
'order_line': [
|
||||
(0, 0, {'product_id': product.id, 'product_uom_qty': 1,
|
||||
'x_fc_part_catalog_id': pa.id,
|
||||
'x_fc_process_variant_id': r1.id}),
|
||||
(0, 0, {'product_id': product.id, 'product_uom_qty': 1,
|
||||
'x_fc_part_catalog_id': pb.id,
|
||||
'x_fc_process_variant_id': r2.id}),
|
||||
(0, 0, {'product_id': product.id, 'product_uom_qty': 1,
|
||||
'x_fc_part_catalog_id': pc.id,
|
||||
'x_fc_process_variant_id': r3.id}),
|
||||
],
|
||||
})
|
||||
so._fp_auto_create_job()
|
||||
jobs = self.env['fp.job'].search([('sale_order_id', '=', so.id)])
|
||||
self.assertEqual(len(jobs), 2, 'A+B merge, C separate')
|
||||
sizes = sorted(len(j.sale_order_line_ids) for j in jobs)
|
||||
self.assertEqual(sizes, [1, 2])
|
||||
|
||||
def test_masking_toggle_splits_same_structure(self):
|
||||
partner = self.env['res.partner'].create({'name': 'M'})
|
||||
product = self.env['product.product'].create({'name': 'P'})
|
||||
pa = self.env['fp.part.catalog'].create({
|
||||
'name': 'A', 'partner_id': partner.id, 'part_number': 'A'})
|
||||
pb = self.env['fp.part.catalog'].create({
|
||||
'name': 'B', 'partner_id': partner.id, 'part_number': 'B'})
|
||||
r1 = self._recipe('ENP — A', ['Soak Clean', 'Rinse'])
|
||||
r2 = self._recipe('ENP — B', ['Soak Clean', 'Rinse'])
|
||||
so = self.env['sale.order'].create({
|
||||
'partner_id': partner.id,
|
||||
'order_line': [
|
||||
(0, 0, {'product_id': product.id, 'product_uom_qty': 1,
|
||||
'x_fc_part_catalog_id': pa.id,
|
||||
'x_fc_process_variant_id': r1.id,
|
||||
'x_fc_masking_enabled': True}),
|
||||
(0, 0, {'product_id': product.id, 'product_uom_qty': 1,
|
||||
'x_fc_part_catalog_id': pb.id,
|
||||
'x_fc_process_variant_id': r2.id,
|
||||
'x_fc_masking_enabled': False}),
|
||||
],
|
||||
})
|
||||
so._fp_auto_create_job()
|
||||
jobs = self.env['fp.job'].search([('sale_order_id', '=', so.id)])
|
||||
self.assertEqual(len(jobs), 2, 'masking on vs off must not merge')
|
||||
Reference in New Issue
Block a user