Skip to content

Commit 8ca10a8

Browse files
committed
[FIX] stock: ensure safe UoM change for done smls
We fix 2 related UoM issues: 1. Fix quant inconsistency from changing the UoM of Done stock.move.lines Steps to reproduce: - Enable "Storage Locations" setting - Create a new "Storable Product" and create a receipt for 1 unit of it - Validate the 1 unit receieved - Open "Detailed Operations" of the move and change the stock.move.line UoM to dozen. Expected result: 13 on hand Actual result: 1 on hand To fix this: - prevent users from editing the UoM after the picking is done (i.e. unless adding a new stock.move.line and not saving). - update the write on done logic so stock.move.line UoM changes are considering and will update the quant correctly (in case of RPC or direct write). 2. Prevent changing UoM of Done stock.move to prevent inconsistent field values within stock.move and confusion for users Steps to reproduce: - Complete a picking (incoming is easiest to see) with a new product (i.e. 0 qty) having 1 unit done. - Unlock picking and add a new stock.move with 1 unit done and save. - Edit the just added stock.move's UoM from Units to Dozen. - Check the quantity on hand / Done qty of stock.move after leaving and returning to form. Expected result: 13 On Hand Actual Result: 2 On Hand and the "Done" qty in the picking is 0.0083 (i.e. 1/12 of a dozen) To fix this: - prevent users from editing the UoM after the picking is done (unless adding a new stock.move and not saving) - if a Done stock.move UoM is uodated, a UserError occurs because there is no straightforward way to ensure the quant is updated correctly since is handled within the move.line (i.e. has no visibility to its move's uom change => changing only UoM and not qty done will result in no quant update) closes odoo#75823 Signed-off-by: Arnold Moyaux <[email protected]>
1 parent ecac935 commit 8ca10a8

File tree

5 files changed

+48
-8
lines changed

5 files changed

+48
-8
lines changed

addons/stock/models/stock_move.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -447,6 +447,8 @@ def write(self, vals):
447447
# messages according to the state of the stock.move records.
448448
receipt_moves_to_reassign = self.env['stock.move']
449449
move_to_recompute_state = self.env['stock.move']
450+
if 'product_uom' in vals and any(move.state == 'done' for move in self):
451+
raise UserError(_('You cannot change the UoM for a stock move that has been set to \'Done\'.'))
450452
if 'product_uom_qty' in vals:
451453
move_to_unreserve = self.env['stock.move']
452454
for move in self.filtered(lambda m: m.state not in ('done', 'draft') and m.picking_id):

addons/stock/models/stock_move_line.py

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -255,7 +255,8 @@ def write(self, vals):
255255
('lot_id', 'stock.production.lot'),
256256
('package_id', 'stock.quant.package'),
257257
('result_package_id', 'stock.quant.package'),
258-
('owner_id', 'res.partner')
258+
('owner_id', 'res.partner'),
259+
('product_uom_id', 'uom.uom')
259260
]
260261
updates = {}
261262
for key, model in triggers:
@@ -316,7 +317,7 @@ def write(self, vals):
316317
mls = mls.filtered(lambda ml: not float_is_zero(ml.qty_done - vals['qty_done'], precision_rounding=ml.product_uom_id.rounding))
317318
for ml in mls:
318319
# undo the original move line
319-
qty_done_orig = ml.move_id.product_uom._compute_quantity(ml.qty_done, ml.move_id.product_id.uom_id, rounding_method='HALF-UP')
320+
qty_done_orig = ml.product_uom_id._compute_quantity(ml.qty_done, ml.move_id.product_id.uom_id, rounding_method='HALF-UP')
320321
in_date = Quant._update_available_quantity(ml.product_id, ml.location_dest_id, -qty_done_orig, lot_id=ml.lot_id,
321322
package_id=ml.result_package_id, owner_id=ml.owner_id)[1]
322323
Quant._update_available_quantity(ml.product_id, ml.location_id, qty_done_orig, lot_id=ml.lot_id,
@@ -331,7 +332,8 @@ def write(self, vals):
331332
package_id = updates.get('package_id', ml.package_id)
332333
result_package_id = updates.get('result_package_id', ml.result_package_id)
333334
owner_id = updates.get('owner_id', ml.owner_id)
334-
quantity = ml.move_id.product_uom._compute_quantity(qty_done, ml.move_id.product_id.uom_id, rounding_method='HALF-UP')
335+
product_uom_id = updates.get('product_uom_id', ml.product_uom_id)
336+
quantity = product_uom_id._compute_quantity(qty_done, ml.move_id.product_id.uom_id, rounding_method='HALF-UP')
335337
if not ml._should_bypass_reservation(location_id):
336338
ml._free_reservation(product_id, location_id, quantity, lot_id=lot_id, package_id=package_id, owner_id=owner_id)
337339
if not float_is_zero(quantity, precision_digits=precision):

addons/stock/tests/test_move.py

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3368,6 +3368,39 @@ def test_edit_done_move_line_13(self):
33683368
self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product_lot, self.stock_location, lot_id=lot1, package_id=package1), 0.0)
33693369
self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product_lot, self.stock_location, lot_id=lot2, package_id=package1), 1.0)
33703370

3371+
def test_edit_done_move_line_14(self):
3372+
""" Test that editing a done stock move line with a different UoM from its stock move correctly
3373+
updates the quant when its qty and/or its UoM is edited. Also check that we don't allow editing
3374+
a done stock move's UoM.
3375+
"""
3376+
move1 = self.env['stock.move'].create({
3377+
'name': 'test_edit_moveline',
3378+
'location_id': self.supplier_location.id,
3379+
'location_dest_id': self.stock_location.id,
3380+
'product_id': self.product.id,
3381+
'product_uom': self.uom_unit.id,
3382+
'product_uom_qty': 12.0,
3383+
})
3384+
move1._action_confirm()
3385+
move1._action_assign()
3386+
move1.move_line_ids.product_uom_id = self.uom_dozen
3387+
move1.move_line_ids.qty_done = 1
3388+
move1._action_done()
3389+
self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product, self.stock_location), 12.0)
3390+
3391+
move1.move_line_ids.qty_done = 2
3392+
self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product, self.stock_location), 24.0)
3393+
self.assertEqual(move1.product_uom_qty, 24.0)
3394+
self.assertEqual(move1.product_qty, 24.0)
3395+
3396+
move1.move_line_ids.product_uom_id = self.uom_unit
3397+
self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product, self.stock_location), 2.0)
3398+
self.assertEqual(move1.product_uom_qty, 2.0)
3399+
self.assertEqual(move1.product_qty, 2.0)
3400+
3401+
with self.assertRaises(UserError):
3402+
move1.product_uom = self.uom_dozen
3403+
33713404
def test_immediate_validate_1(self):
33723405
""" In a picking with a single available move, clicking on validate without filling any
33733406
quantities should open a wizard asking to process all the reservation (so, the whole move).

addons/stock/views/stock_move_views.xml

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -95,7 +95,7 @@
9595
<field name="product_uom_qty" string="Initial Demand" attrs="{'readonly': [('is_initial_demand_editable', '=', False)]}"/>
9696
<field name="reserved_availability" string="Reserved"/>
9797
<field name="quantity_done" string="Done" attrs="{'readonly': [('is_quantity_done_editable', '=', False)]}"/>
98-
<field name="product_uom" attrs="{'readonly': [('state', '!=', 'draft'), ('additional', '=', False)]}" options="{'no_open': True, 'no_create': True}" string="Unit of Measure" groups="uom.group_uom"/>
98+
<field name="product_uom" attrs="{'readonly': [('state', '!=', 'draft'), ('id', '!=', False)]}" options="{'no_open': True, 'no_create': True}" string="Unit of Measure" groups="uom.group_uom"/>
9999
</tree>
100100
</field>
101101
</record>
@@ -276,7 +276,10 @@
276276
<field name="is_locked" invisible="1"/>
277277
<field name="picking_code" invisible="1"/>
278278
<field name="qty_done" attrs="{'readonly': ['|', '|', ('is_initial_demand_editable', '=', True), '&amp;', ('state', '=', 'done'), ('is_locked', '=', True), '&amp;', ('package_level_id', '!=', False), ('parent.picking_type_entire_packs', '=', True)]}"/>
279-
<field name="product_uom_id" options="{'no_open': True, 'no_create': True}" attrs="{'readonly': ['|', ('product_uom_qty', '!=', 0.0), '&amp;', ('package_level_id', '!=', False), ('parent.picking_type_entire_packs', '=', True)]}" string="Unit of Measure" groups="uom.group_uom"/>
279+
<field name="product_uom_id" options="{'no_open': True, 'no_create': True}" string="Unit of Measure" groups="uom.group_uom"
280+
attrs="{'readonly': ['|', '|', ('product_uom_qty', '!=', 0.0),
281+
'&amp;', ('package_level_id', '!=', False), ('parent.picking_type_entire_packs', '=', True),
282+
'&amp;', ('state', '=', 'done'), ('id', '!=', False)]}"/>
280283
</tree>
281284
</field>
282285
</record>
@@ -304,7 +307,7 @@
304307
<field name="product_uom_qty" readonly="1" attrs="{'column_invisible': ['|',('parent.immediate_transfer', '=', True),('parent.picking_type_code','=','incoming')]}"/>
305308
<field name="is_locked" invisible="1"/>
306309
<field name="qty_done" attrs="{'readonly': [('state', 'in', ('done', 'cancel')), ('is_locked', '=', True)]}" force_save="1"/>
307-
<field name="product_uom_id" force_save="1" attrs="{'readonly': [('state', '!=', 'draft')]}" groups="uom.group_uom"/>
310+
<field name="product_uom_id" force_save="1" attrs="{'readonly': [('state', '!=', 'draft'), ('id', '!=', False)]}" groups="uom.group_uom"/>
308311
</tree>
309312
</field>
310313
</record>

addons/stock/views/stock_picking_views.xml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -345,7 +345,7 @@
345345
<field name="product_uom_qty" string="Demand" attrs="{'column_invisible': [('parent.immediate_transfer', '=', True)], 'readonly': ['|', ('is_initial_demand_editable', '=', False), '&amp;', '&amp;', ('show_operations', '=', True), ('is_locked', '=', True), ('is_initial_demand_editable', '=', False)]}"/>
346346
<field name="reserved_availability" string="Reserved" attrs="{'column_invisible': (['|','|', ('parent.state','=', 'done'), ('parent.picking_type_code', '=', 'incoming'), ('parent.immediate_transfer', '=', True)])}"/>
347347
<field name="quantity_done" string="Done" attrs="{'readonly': [('is_quantity_done_editable', '=', False)]}"/>
348-
<field name="product_uom" attrs="{'readonly': [('state', '!=', 'draft'), ('additional', '=', False)]}" options="{'no_open': True, 'no_create': True}" string="Unit of Measure" groups="uom.group_uom"/>
348+
<field name="product_uom" attrs="{'readonly': [('state', '!=', 'draft'), ('id', '!=', False)]}" options="{'no_open': True, 'no_create': True}" string="Unit of Measure" groups="uom.group_uom"/>
349349
<button name="action_show_details" string="Register lots, packs, location" type="object" icon="fa-list" width="0.1"
350350
attrs="{'invisible': [('show_details_visible', '=', False)]}" options='{"warn": true}'/>
351351
<button name="action_assign_serial" type="object"
@@ -369,7 +369,7 @@
369369
<field name="product_uom_qty" string="Initial Demand" attrs="{'invisible': [('parent.immediate_transfer', '=', True)], 'readonly': [('is_initial_demand_editable', '=', False)]}"/>
370370
<field name="reserved_availability" string="Reserved" attrs="{'invisible': (['|','|', ('parent.state','=', 'done'), ('parent.picking_type_code', '=', 'incoming'), ('parent.immediate_transfer', '=', True)])}"/>
371371
<field name="quantity_done" string="Done" attrs="{'readonly': [('is_quantity_done_editable', '=', False)]}"/>
372-
<field name="product_uom" attrs="{'readonly': [('state', '!=', 'draft'), ('additional', '=', False)]}" options="{'no_open': True, 'no_create': True}" string="Unit of Measure" groups="uom.group_uom"/>
372+
<field name="product_uom" attrs="{'readonly': [('state', '!=', 'draft'), ('id', '!=', False)]}" options="{'no_open': True, 'no_create': True}" string="Unit of Measure" groups="uom.group_uom"/>
373373
<field name="description_picking" string="Description"/>
374374
</group>
375375
</form>

0 commit comments

Comments
 (0)