[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:
@@ -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)])
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user