From d37f10f1c30ce7891f49c563e828651ffba66b8b Mon Sep 17 00:00:00 2001 From: gsinghpal Date: Fri, 29 May 2026 09:11:39 -0400 Subject: [PATCH] feat(receiving): technicians can count+close receivings from the tablet ACL: grant group_fp_technician write+create on fp.receiving / line / damage. sudo the internal sale.order x_fc_receiving_status write so a non-privileged technician isn't blocked inside action_mark_counted / action_close. Tests deferred to entech (local Docker unavailable this session). Co-Authored-By: Claude Opus 4.7 --- .../fusion_plating_receiving/__manifest__.py | 2 +- .../models/fp_receiving.py | 14 +++-- .../security/ir.model.access.csv | 6 +- .../tests/__init__.py | 1 + .../tests/test_technician_receiving_acl.py | 59 +++++++++++++++++++ 5 files changed, 73 insertions(+), 9 deletions(-) create mode 100644 fusion_plating/fusion_plating_receiving/tests/test_technician_receiving_acl.py diff --git a/fusion_plating/fusion_plating_receiving/__manifest__.py b/fusion_plating/fusion_plating_receiving/__manifest__.py index c6807bde..70c3ca4c 100644 --- a/fusion_plating/fusion_plating_receiving/__manifest__.py +++ b/fusion_plating/fusion_plating_receiving/__manifest__.py @@ -5,7 +5,7 @@ { 'name': 'Fusion Plating — Receiving & Inspection', - 'version': '19.0.3.28.2', + 'version': '19.0.3.28.3', 'category': 'Manufacturing/Plating', 'summary': 'Parts receiving, inspection, damage logging, and manufacturing gate.', 'description': """ diff --git a/fusion_plating/fusion_plating_receiving/models/fp_receiving.py b/fusion_plating/fusion_plating_receiving/models/fp_receiving.py index c0303e5b..2168e531 100644 --- a/fusion_plating/fusion_plating_receiving/models/fp_receiving.py +++ b/fusion_plating/fusion_plating_receiving/models/fp_receiving.py @@ -1358,17 +1358,21 @@ class FpReceiving(models.Model): for rec in self: if not rec.sale_order_id: continue + # Internal denormalized status field — elevate the write so a + # non-privileged technician (tablet receiving) isn't blocked by + # sale.order ACL inside action_mark_counted / action_close. + so = rec.sale_order_id.sudo() if rec.state == 'closed': - rec.sale_order_id.x_fc_receiving_status = 'received' + so.x_fc_receiving_status = 'received' elif rec.state in ('counted', 'staged'): - rec.sale_order_id.x_fc_receiving_status = 'partial' + so.x_fc_receiving_status = 'partial' # Legacy states preserved. elif rec.state in ('accepted', 'resolved'): - rec.sale_order_id.x_fc_receiving_status = 'received' + so.x_fc_receiving_status = 'received' elif rec.state in ('discrepancy', 'inspecting'): - rec.sale_order_id.x_fc_receiving_status = 'partial' + so.x_fc_receiving_status = 'partial' elif rec.state == 'draft': - rec.sale_order_id.x_fc_receiving_status = 'not_received' + so.x_fc_receiving_status = 'not_received' # Propagate the per-part qty onto the matching fp.job records # so the 2026-05-18 mark_done gate can see what was received. rec._update_job_qty_received() diff --git a/fusion_plating/fusion_plating_receiving/security/ir.model.access.csv b/fusion_plating/fusion_plating_receiving/security/ir.model.access.csv index fee5ef49..c12d8d9f 100644 --- a/fusion_plating/fusion_plating_receiving/security/ir.model.access.csv +++ b/fusion_plating/fusion_plating_receiving/security/ir.model.access.csv @@ -1,11 +1,11 @@ id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink -access_fp_receiving_operator,fp.receiving.operator,model_fp_receiving,fusion_plating.group_fp_technician,1,0,0,0 +access_fp_receiving_operator,fp.receiving.operator,model_fp_receiving,fusion_plating.group_fp_technician,1,1,1,0 access_fp_receiving_receiver,fp.receiving.receiver,model_fp_receiving,fusion_plating.group_fp_shop_manager_v2,1,1,1,0 access_fp_receiving_manager,fp.receiving.manager,model_fp_receiving,fusion_plating.group_fp_manager,1,1,1,1 -access_fp_receiving_line_operator,fp.receiving.line.operator,model_fp_receiving_line,fusion_plating.group_fp_technician,1,0,0,0 +access_fp_receiving_line_operator,fp.receiving.line.operator,model_fp_receiving_line,fusion_plating.group_fp_technician,1,1,1,0 access_fp_receiving_line_receiver,fp.receiving.line.receiver,model_fp_receiving_line,fusion_plating.group_fp_shop_manager_v2,1,1,1,0 access_fp_receiving_line_manager,fp.receiving.line.manager,model_fp_receiving_line,fusion_plating.group_fp_manager,1,1,1,1 -access_fp_receiving_damage_operator,fp.receiving.damage.operator,model_fp_receiving_damage,fusion_plating.group_fp_technician,1,0,0,0 +access_fp_receiving_damage_operator,fp.receiving.damage.operator,model_fp_receiving_damage,fusion_plating.group_fp_technician,1,1,1,0 access_fp_receiving_damage_receiver,fp.receiving.damage.receiver,model_fp_receiving_damage,fusion_plating.group_fp_shop_manager_v2,1,1,1,0 access_fp_receiving_damage_manager,fp.receiving.damage.manager,model_fp_receiving_damage,fusion_plating.group_fp_manager,1,1,1,1 access_fp_racking_inspection_operator,fp.racking.inspection.operator,model_fp_racking_inspection,fusion_plating.group_fp_technician,1,1,1,0 diff --git a/fusion_plating/fusion_plating_receiving/tests/__init__.py b/fusion_plating/fusion_plating_receiving/tests/__init__.py index c49da65d..f173c91d 100644 --- a/fusion_plating/fusion_plating_receiving/tests/__init__.py +++ b/fusion_plating/fusion_plating_receiving/tests/__init__.py @@ -1,3 +1,4 @@ # -*- coding: utf-8 -*- from . import test_carrier_fields +from . import test_technician_receiving_acl diff --git a/fusion_plating/fusion_plating_receiving/tests/test_technician_receiving_acl.py b/fusion_plating/fusion_plating_receiving/tests/test_technician_receiving_acl.py new file mode 100644 index 00000000..f1a9f83f --- /dev/null +++ b/fusion_plating/fusion_plating_receiving/tests/test_technician_receiving_acl.py @@ -0,0 +1,59 @@ +# -*- coding: utf-8 -*- +# Copyright 2026 Nexa Systems Inc. +# License OPL-1 (Odoo Proprietary License v1.0) +"""Technician can receive (count + close) from the tablet. + +Spec: docs/superpowers/specs/2026-05-29-technician-receiving-shipping-tablet-design.md +""" +from odoo.tests.common import TransactionCase +from odoo.exceptions import AccessError + + +class TestTechnicianReceivingAcl(TransactionCase): + + @classmethod + def setUpClass(cls): + super().setUpClass() + cls.partner = cls.env['res.partner'].create({'name': 'AclCust'}) + cls.product = cls.env['product.product'].create({'name': 'AclWidget'}) + cls.so = cls.env['sale.order'].create({ + 'partner_id': cls.partner.id, + 'order_line': [(0, 0, { + 'product_id': cls.product.id, + 'product_uom_qty': 1, + })], + }) + cls.tech = cls.env['res.users'].create({ + 'name': 'Tech ACL', + 'login': 'tech_acl_recv', + # Odoo 19: group_ids (NOT groups_id) — CLAUDE.md rule 13c. + 'group_ids': [(6, 0, [ + cls.env.ref('fusion_plating.group_fp_technician').id, + ])], + }) + + def test_technician_can_count_and_close_receiving(self): + # Created as admin; the technician must be able to count + close. + rec = self.env['fp.receiving'].create({ + 'sale_order_id': self.so.id, + 'box_count_in': 3, + }) + rec_as_tech = rec.with_user(self.tech) + try: + rec_as_tech.action_mark_counted() + except AccessError as e: + self.fail("Technician blocked marking counted: %s" % e) + self.assertEqual(rec.state, 'counted') + rec_as_tech.action_close() + self.assertEqual(rec.state, 'closed') + # The SO status write inside _update_so_receiving_status must have + # gone through (it is sudo'd) — proves no AccessError on sale.order. + self.assertEqual(self.so.x_fc_receiving_status, 'received') + + def test_technician_can_create_damage(self): + rec = self.env['fp.receiving'].create({'sale_order_id': self.so.id}) + dmg = self.env['fp.receiving.damage'].with_user(self.tech).create({ + 'receiving_id': rec.id, + 'description': 'scratch on flange', + }) + self.assertTrue(dmg.id)