Skip to content

Commit

Permalink
[FIX] stock: ensure safe UoM change for done smls
Browse files Browse the repository at this point in the history
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]>
  • Loading branch information
ticodoo committed Sep 2, 2021
1 parent ecac935 commit 8ca10a8
Show file tree
Hide file tree
Showing 5 changed files with 48 additions and 8 deletions.
2 changes: 2 additions & 0 deletions addons/stock/models/stock_move.py
Original file line number Diff line number Diff line change
Expand Up @@ -447,6 +447,8 @@ def write(self, vals):
# messages according to the state of the stock.move records.
receipt_moves_to_reassign = self.env['stock.move']
move_to_recompute_state = self.env['stock.move']
if 'product_uom' in vals and any(move.state == 'done' for move in self):
raise UserError(_('You cannot change the UoM for a stock move that has been set to \'Done\'.'))
if 'product_uom_qty' in vals:
move_to_unreserve = self.env['stock.move']
for move in self.filtered(lambda m: m.state not in ('done', 'draft') and m.picking_id):
Expand Down
8 changes: 5 additions & 3 deletions addons/stock/models/stock_move_line.py
Original file line number Diff line number Diff line change
Expand Up @@ -255,7 +255,8 @@ def write(self, vals):
('lot_id', 'stock.production.lot'),
('package_id', 'stock.quant.package'),
('result_package_id', 'stock.quant.package'),
('owner_id', 'res.partner')
('owner_id', 'res.partner'),
('product_uom_id', 'uom.uom')
]
updates = {}
for key, model in triggers:
Expand Down Expand Up @@ -316,7 +317,7 @@ def write(self, vals):
mls = mls.filtered(lambda ml: not float_is_zero(ml.qty_done - vals['qty_done'], precision_rounding=ml.product_uom_id.rounding))
for ml in mls:
# undo the original move line
qty_done_orig = ml.move_id.product_uom._compute_quantity(ml.qty_done, ml.move_id.product_id.uom_id, rounding_method='HALF-UP')
qty_done_orig = ml.product_uom_id._compute_quantity(ml.qty_done, ml.move_id.product_id.uom_id, rounding_method='HALF-UP')
in_date = Quant._update_available_quantity(ml.product_id, ml.location_dest_id, -qty_done_orig, lot_id=ml.lot_id,
package_id=ml.result_package_id, owner_id=ml.owner_id)[1]
Quant._update_available_quantity(ml.product_id, ml.location_id, qty_done_orig, lot_id=ml.lot_id,
Expand All @@ -331,7 +332,8 @@ def write(self, vals):
package_id = updates.get('package_id', ml.package_id)
result_package_id = updates.get('result_package_id', ml.result_package_id)
owner_id = updates.get('owner_id', ml.owner_id)
quantity = ml.move_id.product_uom._compute_quantity(qty_done, ml.move_id.product_id.uom_id, rounding_method='HALF-UP')
product_uom_id = updates.get('product_uom_id', ml.product_uom_id)
quantity = product_uom_id._compute_quantity(qty_done, ml.move_id.product_id.uom_id, rounding_method='HALF-UP')
if not ml._should_bypass_reservation(location_id):
ml._free_reservation(product_id, location_id, quantity, lot_id=lot_id, package_id=package_id, owner_id=owner_id)
if not float_is_zero(quantity, precision_digits=precision):
Expand Down
33 changes: 33 additions & 0 deletions addons/stock/tests/test_move.py
Original file line number Diff line number Diff line change
Expand Up @@ -3368,6 +3368,39 @@ def test_edit_done_move_line_13(self):
self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product_lot, self.stock_location, lot_id=lot1, package_id=package1), 0.0)
self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product_lot, self.stock_location, lot_id=lot2, package_id=package1), 1.0)

def test_edit_done_move_line_14(self):
""" Test that editing a done stock move line with a different UoM from its stock move correctly
updates the quant when its qty and/or its UoM is edited. Also check that we don't allow editing
a done stock move's UoM.
"""
move1 = self.env['stock.move'].create({
'name': 'test_edit_moveline',
'location_id': self.supplier_location.id,
'location_dest_id': self.stock_location.id,
'product_id': self.product.id,
'product_uom': self.uom_unit.id,
'product_uom_qty': 12.0,
})
move1._action_confirm()
move1._action_assign()
move1.move_line_ids.product_uom_id = self.uom_dozen
move1.move_line_ids.qty_done = 1
move1._action_done()
self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product, self.stock_location), 12.0)

move1.move_line_ids.qty_done = 2
self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product, self.stock_location), 24.0)
self.assertEqual(move1.product_uom_qty, 24.0)
self.assertEqual(move1.product_qty, 24.0)

move1.move_line_ids.product_uom_id = self.uom_unit
self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product, self.stock_location), 2.0)
self.assertEqual(move1.product_uom_qty, 2.0)
self.assertEqual(move1.product_qty, 2.0)

with self.assertRaises(UserError):
move1.product_uom = self.uom_dozen

def test_immediate_validate_1(self):
""" In a picking with a single available move, clicking on validate without filling any
quantities should open a wizard asking to process all the reservation (so, the whole move).
Expand Down
9 changes: 6 additions & 3 deletions addons/stock/views/stock_move_views.xml
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,7 @@
<field name="product_uom_qty" string="Initial Demand" attrs="{'readonly': [('is_initial_demand_editable', '=', False)]}"/>
<field name="reserved_availability" string="Reserved"/>
<field name="quantity_done" string="Done" attrs="{'readonly': [('is_quantity_done_editable', '=', False)]}"/>
<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"/>
<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"/>
</tree>
</field>
</record>
Expand Down Expand Up @@ -276,7 +276,10 @@
<field name="is_locked" invisible="1"/>
<field name="picking_code" invisible="1"/>
<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)]}"/>
<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"/>
<field name="product_uom_id" options="{'no_open': True, 'no_create': True}" string="Unit of Measure" groups="uom.group_uom"
attrs="{'readonly': ['|', '|', ('product_uom_qty', '!=', 0.0),
'&amp;', ('package_level_id', '!=', False), ('parent.picking_type_entire_packs', '=', True),
'&amp;', ('state', '=', 'done'), ('id', '!=', False)]}"/>
</tree>
</field>
</record>
Expand Down Expand Up @@ -304,7 +307,7 @@
<field name="product_uom_qty" readonly="1" attrs="{'column_invisible': ['|',('parent.immediate_transfer', '=', True),('parent.picking_type_code','=','incoming')]}"/>
<field name="is_locked" invisible="1"/>
<field name="qty_done" attrs="{'readonly': [('state', 'in', ('done', 'cancel')), ('is_locked', '=', True)]}" force_save="1"/>
<field name="product_uom_id" force_save="1" attrs="{'readonly': [('state', '!=', 'draft')]}" groups="uom.group_uom"/>
<field name="product_uom_id" force_save="1" attrs="{'readonly': [('state', '!=', 'draft'), ('id', '!=', False)]}" groups="uom.group_uom"/>
</tree>
</field>
</record>
Expand Down
4 changes: 2 additions & 2 deletions addons/stock/views/stock_picking_views.xml
Original file line number Diff line number Diff line change
Expand Up @@ -345,7 +345,7 @@
<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)]}"/>
<field name="reserved_availability" string="Reserved" attrs="{'column_invisible': (['|','|', ('parent.state','=', 'done'), ('parent.picking_type_code', '=', 'incoming'), ('parent.immediate_transfer', '=', True)])}"/>
<field name="quantity_done" string="Done" attrs="{'readonly': [('is_quantity_done_editable', '=', False)]}"/>
<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"/>
<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"/>
<button name="action_show_details" string="Register lots, packs, location" type="object" icon="fa-list" width="0.1"
attrs="{'invisible': [('show_details_visible', '=', False)]}" options='{"warn": true}'/>
<button name="action_assign_serial" type="object"
Expand All @@ -369,7 +369,7 @@
<field name="product_uom_qty" string="Initial Demand" attrs="{'invisible': [('parent.immediate_transfer', '=', True)], 'readonly': [('is_initial_demand_editable', '=', False)]}"/>
<field name="reserved_availability" string="Reserved" attrs="{'invisible': (['|','|', ('parent.state','=', 'done'), ('parent.picking_type_code', '=', 'incoming'), ('parent.immediate_transfer', '=', True)])}"/>
<field name="quantity_done" string="Done" attrs="{'readonly': [('is_quantity_done_editable', '=', False)]}"/>
<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"/>
<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"/>
<field name="description_picking" string="Description"/>
</group>
</form>
Expand Down

0 comments on commit 8ca10a8

Please sign in to comment.