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 <noreply@anthropic.com>
This commit is contained in:
@@ -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': """
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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
|
||||
|
||||
|
@@ -1,3 +1,4 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from . import test_carrier_fields
|
||||
from . import test_technician_receiving_acl
|
||||
|
||||
|
||||
@@ -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)
|
||||
Reference in New Issue
Block a user