From c574a7f4e9135c006a0dfe01e313a1cf12e4475d Mon Sep 17 00:00:00 2001 From: Adrien Widart Date: Fri, 12 Aug 2022 13:05:44 +0000 Subject: [PATCH] [FIX] stock: put in pack the half-up rounded quantity In some cases, when putting in pack, it will lead to a "unreserve more than reserved" error To reproduce the issue: 1. In Settings, enable "Packages" 2. Create a storable product P 3. Update the on-hand quantity: 0.4 x P 4. Create and mark as todo a planned delivery order DO - Operations: 0.4 x P 5. In the detailed operations, set the done quantity to 0.3 6. Put in Pack 7. Open the detailed operations - Error: There is a line with 0.11 x P reserved instead of 0.10. The total reserved quantity is therefore 0.41 which is not possible. Moreover, the available quantity of the related quant is 0.29 which does not make sense 8. Set the done quantity to 0.1 9. Put in Pack 10. Validate Error: a User Error is displayed "It is not possible to unreserve more products of ... than you have in stock." The user is now stuck, he can neither process the picking nor unreserve it. On step 6, when putting in pack, we split the SML into two ones. To do so, we also split the reserved quantity: https://github.com/odoo/odoo/blob/fcc74186330c8df37cfac08455bd7bba44d4656b/addons/stock/models/stock_picking.py#L1384-L1388 However, because of a floating point issue, we have: `0.4 - 0.3 = 0.10000000000000003` Therefore, since we use the rounding method `UP`, we have: ```py ml.product_uom_qty == 0.4 quantity_left_todo == 0.11 done_to_keep == 0.3 ``` And we then use these values to update the reserved quantity on each SML: https://github.com/odoo/odoo/blob/fcc74186330c8df37cfac08455bd7bba44d4656b/addons/stock/models/stock_picking.py#L1391-L1398 When writing on that field, we also try to update the quants: https://github.com/odoo/odoo/blob/55921e32baa1b4b226bd43edf20e93619e411905/addons/stock/models/stock_move_line.py#L378-L383 (As shown, if an error is raised, we ignore it) After the reservation of `quantity_left_todo`, there are 0.29 x P left. Therefore, when trying to reserve `done_to_keep`, it will raise an error: https://github.com/odoo/odoo/blob/b5d16141dc48d4379452ea40e167f3b00f956c20/addons/stock/models/stock_quant.py#L679-L680 But as shown above, the error will be ignored. This explains the inconsistency between the reserved quantity on the SMLs and on the quant. On step 10, when validating the picking, we try to unreserve all SMLs: https://github.com/odoo/odoo/blob/55921e32baa1b4b226bd43edf20e93619e411905/addons/stock/models/stock_move_line.py#L570-L572 So we will try to unreserve more than actually reserved on the quant. That's the reason why an error will be displayed. OPW-2942054 closes odoo/odoo#98521 X-original-commit: 4f593706ee6d1b3ddf0f1c9c90c86df2977459d9 Signed-off-by: Arnold Moyaux (arm) Signed-off-by: Adrien Widart --- addons/stock/models/stock_picking.py | 2 +- addons/stock/tests/test_packing.py | 38 ++++++++++++++++++++++++++++ 2 files changed, 39 insertions(+), 1 deletion(-) diff --git a/addons/stock/models/stock_picking.py b/addons/stock/models/stock_picking.py index fe0d182ffeb..82ac943c093 100644 --- a/addons/stock/models/stock_picking.py +++ b/addons/stock/models/stock_picking.py @@ -1404,7 +1404,7 @@ class Picking(models.Model): quantity_left_todo = float_round( ml.reserved_uom_qty - ml.qty_done, precision_rounding=ml.product_uom_id.rounding, - rounding_method='UP') + rounding_method='HALF-UP') done_to_keep = ml.qty_done new_move_line = ml.copy( default={'reserved_uom_qty': 0, 'qty_done': ml.qty_done}) diff --git a/addons/stock/tests/test_packing.py b/addons/stock/tests/test_packing.py index f5a2e43c5e0..f033fc82d0e 100644 --- a/addons/stock/tests/test_packing.py +++ b/addons/stock/tests/test_packing.py @@ -1225,3 +1225,41 @@ class TestPacking(TestPackingCommon): {'product_id': self.productA.id, 'reserved_uom_qty': 50, 'qty_done': 0, 'result_package_id': package_02.id, 'location_dest_id': sub_loc_02.id}, {'product_id': self.productB.id, 'reserved_uom_qty': 50, 'qty_done': 0, 'result_package_id': package_02.id, 'location_dest_id': sub_loc_02.id}, ]) + + def test_rounding_and_reserved_qty(self): + """ + Basic use case: deliver a storable product put in two packages. This + test actually ensures that the process 'put in pack' handles some + possible issues with the floating point representation + """ + self.env['stock.quant']._update_available_quantity(self.productA, self.stock_location, 0.4) + + picking = self.env['stock.picking'].create({ + 'picking_type_id': self.warehouse.out_type_id.id, + 'location_id': self.stock_location.id, + 'location_dest_id': self.customer_location.id, + 'move_ids': [(0, 0, { + 'name': self.productA.name, + 'product_id': self.productA.id, + 'product_uom_qty': 0.4, + 'product_uom': self.productA.uom_id.id, + 'location_id': self.stock_location.id, + 'location_dest_id': self.customer_location.id, + 'picking_type_id': self.warehouse.out_type_id.id, + })], + }) + picking.action_confirm() + + picking.move_line_ids.qty_done = 0.3 + picking.action_put_in_pack() + + picking.move_line_ids.filtered(lambda ml: not ml.result_package_id).qty_done = 0.1 + picking.action_put_in_pack() + + quant = self.env['stock.quant'].search([('product_id', '=', self.productA.id), ('location_id', '=', self.stock_location.id)]) + self.assertEqual(quant.available_quantity, 0) + + picking.button_validate() + self.assertEqual(picking.state, 'done') + self.assertEqual(picking.move_ids.quantity_done, 0.4) + self.assertEqual(len(picking.move_line_ids.result_package_id), 2)