[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 <awt@odoo.com>
This commit is contained in:
Adrien Widart
2022-08-02 18:43:56 +02:00
parent 5cb18d286e
commit 65d5e7900f
2 changed files with 40 additions and 1 deletions
@@ -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)
+5 -1
View File
@@ -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'