[FIX] {sale_,}stock: reverse orig/dest moves if is return

In a multistep configuration, decreasing the quantity on a line of a
confirmed sale order may create some inconsistencies.

To reproduce the issue:
1. In Settings, enable "Multi-Step Routes"
2. Edit the existing warehouse:
    - Outgoing Shipments: 3 steps
3. Create a storable product P
4. Set the on-hand qty to 5
5. Create and confirm a sale order SO with 5 x P
6. Process the PICK/PACK pickings
7. Edit the SO:
    - Decrease the qty: 3 x P
8. In the SO's deliveries, open the return of the pack step (Output ->
Packing)
    - Error: Nothing changes, there isn't any reserved quantity.
However, there are currently 5 x P at the Output location, so the
reservation should work
9. Inventory > Report > Stock Moves, list all SM related to P
    - Error: The origin/destination moves of the pack's return (Output
-> Packing) are incorrect too: the origins contain the pick's return
(Packing -> Stock) while it should not contain anything and the
destination moves contain the ship move (Output -> Customer) while it
should contain the pick's return (Packing -> Stock)

`SM_ship`: SM used to deliver from Output to Customers
`SM_ret_ship`: SM used to return P from Customers to Output
`SM_ret_pack`: SM used to return P from Output to Packing
`SM_ret_pick`: SM used to return P from Packing to Stock

When confirming a move, we transform the move if its demand is negative:
https://github.com/odoo/odoo/blob/53b424d9e43b07fe7aa84ff00035fb7772ef6291/addons/stock/models/stock_move.py#L1239-L1247
We reverse the source/destination locations but we don't do the same
with the origin/destination moves. That is an issue: before reversing,
we have:
| SM          | Qty | From    | To        | Origin      | Destination |
|-------------|-----|---------|-----------|-------------|-------------|
| SM_ret_pick | -2  | Stock   | Packing   | /           | SM_ret_pack |
| SM_ret_pack | -2  | Packing | Output    | SM_ret_pick | SM_ret_ship |
| SM_ret_ship | -2  | Output  | Customers | SM_ret_pack | /           |

Therefore, reversing the source/destination locations without updating
the origin/destination moves does not make sense: we should reverse them
too depending on their direction (positive/negative) and their
source/destination location.

Also, before trying to transform the moves, we try to merge them:
https://github.com/odoo/odoo/blob/53b424d9e43b07fe7aa84ff00035fb7772ef6291/addons/stock/models/stock_move.py#L1236-L1237
This is relevant for `SM_ret_ship`: we find an existing SM (`SM_ship`,
qty=5) that can absorb the negative demand of `SM_ret_ship` (qty=-2). In
such case, we add the origin/destination moves of the negative SM to the
positive one. However, in the above case, it will create an
inconsistency: after the fix, at that point, `SM_ret_pick` and
`SM_ret_pack` have been correctly reversed, so we have:
| SM          | Qty | From    | To        | Origin      | Destination |
|-------------|-----|---------|-----------|-------------|-------------|
| SM_ret_pick | 2   | Packing | Stock     | SM_ret_pack | /           |
| SM_ret_pack | 2   | Output  | Packing   | SM_ret_pack | SM_ret_pick |
| SM_ret_ship | -2  | Output  | Customers | /           | SM_ret_pack |
| SM_ship     | 5   | Output  | Customers | (SM_pack)   | /           |

Therefore, adding `SM_ret_pack` to the destination moves of `SM_ship` is
incorrect. (Note that, without the fix, `SM_ret_pack` would be added to
the origins of `SM_ship`, which would be incorrect too)

Finally, the reservation (step 8) does't work because of two reasons:
- If there are some `move_orig_ids`, we use the done value of these SM
to assign the current SM. However, as explained above, there is an issue
with this field
- If there isn't any `move_orig_ids`, there is an issue with the procure
method. Its value is `make_to_order`: we skip the reservation for such a
value:
https://github.com/odoo/odoo/blob/53b424d9e43b07fe7aa84ff00035fb7772ef6291/addons/stock/models/stock_move.py#L1499-L1501

OPW-2889679

closes odoo/odoo#95263

closes odoo/odoo#95926

X-original-commit: fb94c140a91aca26a56c5162d61b00b4a4b0d85b
Signed-off-by: William Henrotin (whe) <whe@odoo.com>
Signed-off-by: Adrien Widart <awt@odoo.com>
This commit is contained in:
Adrien Widart
2022-07-13 19:55:57 +02:00
parent 5d4b5d71c0
commit fc5572c11f
2 changed files with 66 additions and 2 deletions
@@ -1205,3 +1205,55 @@ class TestSaleStock(TestSaleCommon, ValuationReconciliationTestCommon):
self.assertEqual(so.order_line[1].product_id, self.product_b)
self.assertEqual(so.order_line[1].qty_delivered, 1)
self.assertEqual(so.order_line[1].product_uom.id, uom_km_id)
def test_return_with_mto_and_multisteps(self):
"""
Suppose a product P and a 3-steps delivery.
Sell 5 x P, process pick & pack pickings and then decrease the qty on
the SO line:
- the ship picking should be updated
- there should be a return R1 for the pack picking
- there should be a return R2 for the pick picking
- it should be possible to reserve R1
"""
warehouse = self.env['stock.warehouse'].search([('company_id', '=', self.env.company.id)], limit=1)
warehouse.delivery_steps = 'pick_pack_ship'
stock_location = warehouse.lot_stock_id
pack_location, out_location, custo_location = warehouse.delivery_route_id.rule_ids.location_dest_id
product = self.env['product.product'].create({
'name': 'SuperProduct',
'type': 'product',
})
self.env['stock.quant']._update_available_quantity(product, stock_location, 5)
so_form = Form(self.env['sale.order'])
so_form.partner_id = self.partner_a
with so_form.order_line.new() as line:
line.product_id = product
line.product_uom_qty = 5
so = so_form.save()
so.action_confirm()
pick_picking, pack_picking, _ = so.picking_ids
(pick_picking + pack_picking).move_ids.quantity_done = 5
(pick_picking + pack_picking).button_validate()
with Form(so) as so_form:
with so_form.order_line.edit(0) as line:
line.product_uom_qty = 3
move_lines = so.picking_ids.move_ids.sorted('id')
ship_sm, pack_sm, pick_sm, ret_pack_sm, ret_pick_sm = move_lines
self.assertRecordValues(move_lines, [
{'location_id': out_location.id, 'location_dest_id': custo_location.id, 'move_orig_ids': pack_sm.ids, 'move_dest_ids': []},
{'location_id': pack_location.id, 'location_dest_id': out_location.id, 'move_orig_ids': pick_sm.ids, 'move_dest_ids': ship_sm.ids},
{'location_id': stock_location.id, 'location_dest_id': pack_location.id, 'move_orig_ids': [], 'move_dest_ids': pack_sm.ids},
{'location_id': out_location.id, 'location_dest_id': pack_location.id, 'move_orig_ids': [], 'move_dest_ids': ret_pick_sm.ids},
{'location_id': pack_location.id, 'location_dest_id': stock_location.id, 'move_orig_ids': ret_pack_sm.ids, 'move_dest_ids': []},
])
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)
+14 -2
View File
@@ -894,8 +894,8 @@ class StockMove(models.Model):
if float_compare(pos_move.product_uom_qty, abs(neg_move.product_uom_qty), precision_rounding=pos_move.product_uom.rounding) >= 0:
pos_move.product_uom_qty += neg_move.product_uom_qty
pos_move.write({
'move_dest_ids': [Command.link(m.id) for m in neg_move.mapped('move_dest_ids')],
'move_orig_ids': [Command.link(m.id) for m in neg_move.mapped('move_orig_ids')],
'move_dest_ids': [Command.link(m.id) for m in neg_move.mapped('move_dest_ids') if m.location_id == pos_move.location_dest_id],
'move_orig_ids': [Command.link(m.id) for m in neg_move.mapped('move_orig_ids') if m.location_dest_id == pos_move.location_id],
})
merged_moves |= pos_move
moves_to_unlink |= neg_move
@@ -1256,9 +1256,21 @@ class StockMove(models.Model):
move.product_uom_qty, 0, precision_rounding=move.product_uom.rounding) < 0)
for move in neg_r_moves:
move.location_id, move.location_dest_id = move.location_dest_id, move.location_id
orig_move_ids, dest_move_ids = [], []
for m in move.move_orig_ids | move.move_dest_ids:
from_loc, to_loc = m.location_id, m.location_dest_id
if float_compare(m.product_uom_qty, 0, precision_rounding=m.product_uom.rounding) < 0:
from_loc, to_loc = to_loc, from_loc
if to_loc == move.location_id:
orig_move_ids += m.ids
elif move.location_dest_id == from_loc:
dest_move_ids += m.ids
move.move_orig_ids, move.move_dest_ids = [(6, 0, orig_move_ids)], [(6, 0, dest_move_ids)]
move.product_uom_qty *= -1
if move.picking_type_id.return_picking_type_id:
move.picking_type_id = move.picking_type_id.return_picking_type_id
# We are returning some products, we must take them in the source location
move.procure_method = 'make_to_stock'
neg_r_moves._assign_picking()
# call `_action_assign` on every confirmed move which location_id bypasses the reservation + those expected to be auto-assigned