[FIX] mrp_subcontracting: avoid setting done qty on subcontracted SM

On the backorders, the consumed quantities are incorrect.

To reproduce the issue:
1. Create two products:
   - C01: consumable
   - P01: storable
2. Create a BoM
   - Product: P01
   - Type: Subcontracting
   - Subcontractor: S
   - Components: 1 x C01
3. Create and confirm a planned receipt:
   - From: S
   - 10 x P01
4. Set the done quantity (directly on the tree view) to 1
5. Validate + Backorder
6. On this first backorder, set the qty to 2
7. Validate + Backorder
8. Validate the second backorder
9. Inventory > Reporting > Moves History, filter on C01

Error: There are three SMs, and here are the done quantities: 1, 9
and 7. The second one is incorrect, it should be 2 (see step 6).

Since [1] the done quantity is always editable on the picking's
operations, but the feature is not implemented in subcontracting,
therefore the communication between the MO and the picking does not
work well. For instance, step 4: save the done quantity and open the
MO: nothing changed on it, its producing qty is still zero. Another
example: step 4, instead of setting the done quantity directly on
the picking, open the wizard (the MO), record 3 produced products and
close the wizard. The done quantity of the stock move is 3 but the
user can still edit it. However, if he tries to update the line,
nothing will change on MO side.

On top of these "little" bugs, it may lead to a more important one
as shown in the above use case. Step 5: at some point, we split the
MO to generate a backorder for 9 x P01:
https://github.com/odoo/odoo/blob/c47e4ac7cb5fdd690f40ab2e0f8d022ba565145e/addons/mrp_subcontracting/models/stock_picking.py#L72-L76
And, as shown, the flag `set_consumed_qty` is set to `True`. As
explained in the method description:
https://github.com/odoo/odoo/blob/a2574b45aef1ff6ff3a221b996d0bb0dda2a8c74/addons/mrp/models/mrp_production.py#L1602-L1603
So, because of this flag, the method will create a SML for C01 on
the backorder, with its done quantity set to the reserved one: 9.
However, back to the first block code: `move.move_line_ids` only
contains the SML created at step 4 (i.e., the SML of the inital
SM, with a done quantity equal to 1). As a result, in the for loop,
we only define the producing quantity of the initial MO.

This is a first issue: step 6, suppose the user rather opens the
detailed operations of the line: it displays the new MO, the
producing qty is 0 but the consumed quantity of the component is 9.
This is not perfect, but let's ignore this and continue with the
above steps. The user sets the done quantity to 2 and validates with
backorder. It runs the productions split again, we use the done
quantity as producing one on the MO, but we don't edit the consumed
quantities, hence the error.

For all these reasons, the safer solution is probably to prevent the
feature 'qty done always editable' in case of subcontracting.

[1] cacc677a08066bc23e1c89df36027599d05f2a08

OPW-3441197

closes odoo/odoo#135862

X-original-commit: 049f63fbc1ab09cf75824e5c22508ea091e5c4dd
Signed-off-by: William Henrotin (whe) <whe@odoo.com>
Signed-off-by: Adrien Widart (awt) <awt@odoo.com>
This commit is contained in:
Adrien Widart (awt)
2023-09-19 16:37:21 +00:00
parent ba8c4ca17d
commit fb20ec841a
4 changed files with 61 additions and 9 deletions
@@ -16,6 +16,11 @@ class StockMove(models.Model):
compute='_compute_show_subcontracting_details_visible'
)
@api.depends('is_subcontract', 'move_orig_ids.production_id')
def _compute_is_quantity_done_editable(self):
super()._compute_is_quantity_done_editable()
self.filtered(lambda m: m.is_subcontract).is_quantity_done_editable = False
def _compute_display_assign_serial(self):
super(StockMove, self)._compute_display_assign_serial()
for move in self:
@@ -651,6 +651,61 @@ class TestSubcontractingFlows(TestMrpSubcontractingCommon):
self.assertEqual(self.env['mrp.production'].search_count([('bom_id', '=', bom.id)]), 3)
def test_several_backorders02(self):
"""
Receipt for 5 subcontracted products
Process three of them with backorder, then the remaining qty. The user
should never be able to define the produced qty directly on the
picking's SM.
"""
finished, component = self.env['product.product'].create([{
'name': 'Finished Product',
'type': 'product',
}, {
'name': 'Component',
'type': 'product',
}])
self.env['mrp.bom'].create({
'product_tmpl_id': finished.product_tmpl_id.id,
'product_qty': 1.0,
'type': 'subcontract',
'subcontractor_ids': [(4, self.subcontractor_partner1.id)],
'bom_line_ids': [(0, 0, {'product_id': component.id, 'product_qty': 1.0})],
})
picking_form = Form(self.env['stock.picking'])
picking_form.picking_type_id = self.env.ref('stock.picking_type_in')
picking_form.partner_id = self.subcontractor_partner1
with picking_form.move_ids_without_package.new() as move:
move.product_id = finished
move.product_uom_qty = 5
picking = picking_form.save()
picking.action_confirm()
for done_qty in [2, 3]:
with self.assertRaises(AssertionError):
with Form(picking) as picking_form:
with picking_form.move_ids_without_package.edit(0) as line:
line.quantity_done = done_qty
action = picking.action_record_components()
mo = self.env['mrp.production'].browse(action['res_id'])
mo_form = Form(mo.with_context(**action['context']), view=action['view_id'])
mo_form.qty_producing = done_qty
mo = mo_form.save()
mo.subcontracting_record_component()
res = picking.button_validate()
if res is not True:
wizard = Form(self.env[res['res_model']].with_context(res['context'])).save()
wizard.process()
picking = picking.backorder_ids
productions = self.env['mrp.production'].search([('product_id', '=', finished.id)], order='id')
self.assertEqual(productions.mapped('state'), ['done', 'done'])
self.assertEqual(productions.move_raw_ids.mapped('quantity_done'), [2.0, 3.0])
def test_subcontracting_rules_replication(self):
""" Test activate/archive subcontracting location rules."""
reference_location_rules = self.env['stock.rule'].search(['|', ('location_src_id', '=', self.env.company.subcontracting_location_id.id), ('location_dest_id', '=', self.env.company.subcontracting_location_id.id)])
-8
View File
@@ -270,14 +270,6 @@ class StockMove(models.Model):
for move in self:
if not move.product_id:
move.is_quantity_done_editable = False
elif not move.picking_id.immediate_transfer and move.picking_id.state == 'draft':
move.is_quantity_done_editable = False
elif move.picking_id.is_locked and move.state in ('done', 'cancel'):
move.is_quantity_done_editable = False
elif move.show_details_visible:
move.is_quantity_done_editable = False
elif move.show_operations:
move.is_quantity_done_editable = False
else:
move.is_quantity_done_editable = True
+1 -1
View File
@@ -307,7 +307,7 @@
<field name="reserved_availability" string="Reserved"
column_invisible="parent.state in ['draft', 'done'] or parent.picking_type_code in ['incoming', 'outgoing'] or parent.immediate_transfer"/>
<field name="product_qty" readonly="1" column_invisible="True"/>
<field name="quantity_done" string="Done" column_invisible="parent.state == 'draft' and not parent.immediate_transfer" readonly="not product_id"
<field name="quantity_done" string="Done" column_invisible="parent.state == 'draft' and not parent.immediate_transfer" readonly="not is_quantity_done_editable"
decoration-danger="quantity_done > product_uom_qty"/>
<field name="product_uom" readonly="state != 'draft' and not additional" options="{'no_open': True, 'no_create': True}" string="Unit" groups="uom.group_uom"/>
<field name="lot_ids" widget="many2many_tags"