[FIX] stock: scrap products thanks to internal move

A user should be able to scrap some products thanks to an internal
transfer.

To reproduce the issue:
1. In Settings, enable "Multi-Step Routes"
2. Create a storable product P and update its quantity (> 1)
3. Create a planned and internal transfer T:
    - From: WH/Stock
    - To: Virtual Locations/YourCompany: Scrap
    - With: 1 x P
4. Mark T as done

Error: T is still in draft

The `_compute_state` of a picking should not ignore the scrapped moves.
However, if we include them, we need to think about this use case: a
picking with a cancelled normal move and a done scrapped move -> its
state should be cancelled (see use case and test from [1])

[1] 429b589618e8dc2b0c0ccdec3f6eed88f1c73fc8

OPW-2841190

closes odoo/odoo#90870

X-original-commit: 1c956d49b45e9d3fec2bfa2c2020ea278ea80507
Signed-off-by: Arnold Moyaux <arm@odoo.com>
Signed-off-by: Adrien Widart <awt@odoo.com>
This commit is contained in:
Adrien Widart
2022-05-09 14:03:19 +02:00
parent d111b4b343
commit be6f3638cf
2 changed files with 69 additions and 2 deletions
+7 -2
View File
@@ -523,13 +523,15 @@ class Picking(models.Model):
'''
picking_moves_state_map = defaultdict(dict)
picking_move_lines = defaultdict(set)
for move in self.env['stock.move'].search([('picking_id', 'in', self.ids), ('scrapped', '=', False)]):
for move in self.env['stock.move'].search([('picking_id', 'in', self.ids)]):
picking_id = move.picking_id
move_state = move.state
picking_moves_state_map[picking_id.id].update({
'any_draft': picking_moves_state_map[picking_id.id].get('any_draft', False) or move_state == 'draft',
'all_cancel': picking_moves_state_map[picking_id.id].get('all_cancel', True) and move_state == 'cancel',
'all_cancel_done': picking_moves_state_map[picking_id.id].get('all_cancel_done', True) and move_state in ('cancel', 'done'),
'all_done_are_scrapped': picking_moves_state_map[picking_id.id].get('all_done_are_scrapped', True) and (move.scrapped if move_state == 'done' else True),
'any_cancel_and_not_scrapped': picking_moves_state_map[picking_id.id].get('any_cancel_and_not_scrapped', False) or (move_state == 'cancel' and not move.scrapped),
})
picking_move_lines[picking_id.id].add(move.id)
for picking in self:
@@ -541,7 +543,10 @@ class Picking(models.Model):
elif picking_moves_state_map[picking_id]['all_cancel']:
picking.state = 'cancel'
elif picking_moves_state_map[picking_id]['all_cancel_done']:
picking.state = 'done'
if picking_moves_state_map[picking_id]['all_done_are_scrapped'] and picking_moves_state_map[picking_id]['any_cancel_and_not_scrapped']:
picking.state = 'cancel'
else:
picking.state = 'done'
else:
relevant_move_state = self.env['stock.move'].browse(picking_move_lines[picking_id])._get_relevant_state_among_moves()
if picking.immediate_transfer and relevant_move_state not in ('draft', 'cancel', 'done'):
+62
View File
@@ -4655,6 +4655,68 @@ class StockMove(TransactionCase):
self.assertEqual(list(warning.keys())[0], 'warning', 'Warning message was not returned')
self.assertEqual(scrap.location_id, child_loc1, 'Location was not auto-corrected')
def test_scrap_8(self):
"""
Suppose a user wants to scrap some products thanks to internal moves.
This test checks the state of the picking based on few cases
"""
scrap_location = self.env['stock.location'].search([('company_id', '=', self.env.company.id), ('scrap_location', '=', True)], limit=1)
internal_operation = self.env['stock.picking.type'].with_context(active_test=False).search([('code', '=', 'internal'), ('company_id', '=', self.env.company.id)], limit=1)
internal_operation.active = True
product01 = self.product
product02 = self.env['product.product'].create({
'name': 'SuperProduct',
'type': 'product',
})
self.env['stock.quant']._update_available_quantity(product01, self.stock_location, 3)
self.env['stock.quant']._update_available_quantity(product02, self.stock_location, 1)
scrap_picking01, scrap_picking02, scrap_picking03 = self.env['stock.picking'].create([{
'location_id': self.stock_location.id,
'location_dest_id': scrap_location.id,
'picking_type_id': internal_operation.id,
'move_ids': [(0, 0, {
'name': 'Scrap %s' % product.display_name,
'location_id': self.stock_location.id,
'location_dest_id': scrap_location.id,
'product_id': product.id,
'product_uom': product.uom_id.id,
'product_uom_qty': 1.0,
'picking_type_id': internal_operation.id,
}) for product in products],
} for products in [(product01,), (product01,), (product01, product02)]])
(scrap_picking01 + scrap_picking02 + scrap_picking03).action_confirm()
# All SM are processed
scrap_picking01.move_ids.quantity_done = 1
scrap_picking01.button_validate()
# All SM are cancelled
scrap_picking02.action_cancel()
# Process one SM and cancel the other one
pick03_prod01_move = scrap_picking03.move_ids.filtered(lambda sm: sm.product_id == product01)
pick03_prod02_move = scrap_picking03.move_ids - pick03_prod01_move
pick03_prod01_move.quantity_done = 1
pick03_prod02_move._action_cancel()
scrap_picking03.button_validate()
self.assertEqual(scrap_picking01.move_ids.state, 'done')
self.assertEqual(scrap_picking01.state, 'done')
self.assertEqual(scrap_picking02.move_ids.state, 'cancel')
self.assertEqual(scrap_picking02.state, 'cancel')
self.assertEqual(pick03_prod01_move.state, 'done')
self.assertEqual(pick03_prod02_move.state, 'cancel')
self.assertEqual(scrap_picking03.state, 'done')
self.assertEqual(self.env['stock.quant']._get_available_quantity(product01, self.stock_location), 1)
self.assertEqual(self.env['stock.quant']._get_available_quantity(product02, self.stock_location), 1)
def test_in_date_1(self):
""" Check that moving a tracked quant keeps the incoming date.
"""