From 65d5e7900f41aa7f65159f8f4cdb0c6638eaf8ca Mon Sep 17 00:00:00 2001 From: Adrien Widart Date: Mon, 25 Jul 2022 11:35:02 +0000 Subject: [PATCH] [FIX] {sale_,}stock: decrease the SOL qty with MTSO rule To reproduce the issue: 1. In Settings, enable "Multi-Step Routes" 2. In the Routes, unarchive the route R called "Replenish on Order (MTO)" 3. Create a storable product P 4. Update its quantity: 10 5. Create and confirm a sale order with one line: - Product: P - Quantity: 10 - Route: R 6. Update the SO line: - Quantity: 8 Error: Once the SO is saved, 2 new deliveries are created: - One from Customers to Stock with 2 x P (this one should be merged with the existing one) - One from Stock to Vendors with 2 x P (his one should not exist) The route R only contains one rule, which is a MTSO one. On step 5, when confirming the SO, `_run_pull` will be called and will create a stock move SM01 based on that rule. In case of a MTSO rule and because there are already some available P, the procure method of SM01 will be MTS (see [1]). Then, once SM01 is created, we will confirm it and we won't do anything more. The process is not the same on step 6. When saving the SO, `_run_pull` is called again but this time, because the needed quantity is negative (-2), the procurement method is set to MTO (see [1]). Therefore, when confirming such a stock move, two undesirable consequences occur: - Because its procure method is not the same than SM01, we won't merge the stock moves (this explains why a picking is created from Customers to Stock) - Because its procure method is MTO, we will create and run a procurement to get -2 x P at Stock. It will find a rule thanks to the route "Receive in 1 step". The rule creates a receipt from Vendors to fulfill the need. This explains why an unexpected picking is created. When processing a procurement with a MTSO rule and a negative quantity, if we want to know the procurement method of the new SM, we should try to find and copy the decision we took for the positive move (if it exists). That way, we ensure that the 'returning phase' (the quantity decreasing) will follow the same behavior as the initial one (when we confirmed the SO). [1] https://github.com/odoo/odoo/blob/20e0ad13afa2a0ff85d555ab57a6800d3db30341/addons/stock/models/stock_rule.py#L244-L254 OPW-2885751 closes odoo/odoo#97332 X-original-commit: 8c6d282de13b205436a7c5c49facd3efc71a740f Signed-off-by: Adrien Widart --- addons/sale_stock/tests/test_sale_stock.py | 35 ++++++++++++++++++++++ addons/stock/models/stock_rule.py | 6 +++- 2 files changed, 40 insertions(+), 1 deletion(-) diff --git a/addons/sale_stock/tests/test_sale_stock.py b/addons/sale_stock/tests/test_sale_stock.py index e056c6d7d03..4b7da8a0e3c 100644 --- a/addons/sale_stock/tests/test_sale_stock.py +++ b/addons/sale_stock/tests/test_sale_stock.py @@ -1257,3 +1257,38 @@ class TestSaleStock(TestSaleCommon, ValuationReconciliationTestCommon): ret_pack_sm.picking_id.action_assign() self.assertEqual(ret_pack_sm.state, 'assigned') self.assertEqual(ret_pack_sm.move_line_ids.reserved_uom_qty, 2) + + def test_mtso_and_qty_decreasing(self): + """ + First, confirm a SO that has a line with the MTO route (the product + should already be available in stock). Then, decrease the qty on the SO + line: + - The delivery should be updated + - There should not be any other picking + """ + warehouse = self.company_data['default_warehouse'] + customer_location = self.env.ref('stock.stock_location_customers') + mto_route = self.env.ref('stock.route_warehouse0_mto') + mto_route.active = True + + self.product_a.type = 'product' + self.env['stock.quant']._update_available_quantity(self.product_a, warehouse.lot_stock_id, 10) + + so = self.env['sale.order'].create({ + 'partner_id': self.partner_a.id, + 'warehouse_id': warehouse.id, + 'order_line': [(0, 0, { + 'name': self.product_a.name, + 'product_id': self.product_a.id, + 'product_uom_qty': 10, + 'product_uom': self.product_a.uom_id.id, + 'price_unit': 1, + 'route_id': mto_route.id, + })], + }) + so.action_confirm() + self.assertRecordValues(so.picking_ids, [{'location_id': warehouse.lot_stock_id.id, 'location_dest_id': customer_location.id}]) + + so.order_line.product_uom_qty = 8 + self.assertRecordValues(so.picking_ids, [{'location_id': warehouse.lot_stock_id.id, 'location_dest_id': customer_location.id}]) + self.assertEqual(so.picking_ids.move_ids.product_uom_qty, 8) diff --git a/addons/stock/models/stock_rule.py b/addons/stock/models/stock_rule.py index 0c4ae8071d6..8a13dc0b0ec 100644 --- a/addons/stock/models/stock_rule.py +++ b/addons/stock/models/stock_rule.py @@ -244,8 +244,12 @@ class StockRule(models.Model): if rule.procure_method == 'mts_else_mto': qty_needed = procurement.product_uom._compute_quantity(procurement.product_qty, procurement.product_id.uom_id) if float_compare(qty_needed, 0, precision_rounding=procurement.product_id.uom_id.rounding) <= 0: - forecasted_qties_by_loc[rule.location_src_id][procurement.product_id.id] -= qty_needed procure_method = 'make_to_order' + for move in procurement.values.get('group_id', self.env['procurement.group']).stock_move_ids: + if move.rule_id == rule and float_compare(move.product_uom_qty, 0, precision_rounding=move.product_uom.rounding) > 0: + procure_method = move.procure_method + break + forecasted_qties_by_loc[rule.location_src_id][procurement.product_id.id] -= qty_needed elif float_compare(qty_needed, forecasted_qties_by_loc[rule.location_src_id][procurement.product_id.id], precision_rounding=procurement.product_id.uom_id.rounding) > 0: procure_method = 'make_to_order'