[FIX] stock: unable to return SN in chained moves
Use case to reproduce the bug: - Set multi location in settings - Set warehouse's deliery method as pick/pack/ship - Deliver a product to customer - Process a return from customer to output. - Create a return from output to pack -> Unable to reserver product. It happens because return moves where not linked together but have the picking that created the return as origin. Thus action_assign does not detect the return moves and can't guess the real quantity available. This commit add a behavior that will create links between return move. It can be guess from parent move(the picking's move from which the return is created) and its move dest/orig that have already created other return. It also correct action assign because the group by could retourn same line with return moves. It add a test that check returns and links between them.
This commit is contained in:
@@ -84,6 +84,7 @@ class MrpStockReport(models.TransientModel):
|
|||||||
('lot_id', '=', context.get('active_id')),
|
('lot_id', '=', context.get('active_id')),
|
||||||
('location_id.usage', '!=', 'internal'),
|
('location_id.usage', '!=', 'internal'),
|
||||||
('state', '=', 'done'),
|
('state', '=', 'done'),
|
||||||
|
('move_id.returned_move_ids', '=', False),
|
||||||
])
|
])
|
||||||
res += self._lines(line_id, model_id=model_id, model='stock.move.line', level=level, parent_quant=parent_quant,
|
res += self._lines(line_id, model_id=model_id, model='stock.move.line', level=level, parent_quant=parent_quant,
|
||||||
stream=stream, obj_ids=move_ids)
|
stream=stream, obj_ids=move_ids)
|
||||||
@@ -99,6 +100,7 @@ class MrpStockReport(models.TransientModel):
|
|||||||
('lot_id', '=', context.get('active_id')),
|
('lot_id', '=', context.get('active_id')),
|
||||||
('location_dest_id.usage', '!=', 'internal'),
|
('location_dest_id.usage', '!=', 'internal'),
|
||||||
('state', '=', 'done'),
|
('state', '=', 'done'),
|
||||||
|
('move_id.returned_move_ids', '=', False),
|
||||||
])
|
])
|
||||||
res += self._lines(line_id, model_id=model_id, model='stock.move.line', level=level, parent_quant=parent_quant,
|
res += self._lines(line_id, model_id=model_id, model='stock.move.line', level=level, parent_quant=parent_quant,
|
||||||
stream=stream, obj_ids=move_ids)
|
stream=stream, obj_ids=move_ids)
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ class TestStockCommon(common.TransactionCase):
|
|||||||
self.supplier_location = self.ModelDataObj.xmlid_to_res_id('stock.stock_location_suppliers')
|
self.supplier_location = self.ModelDataObj.xmlid_to_res_id('stock.stock_location_suppliers')
|
||||||
self.stock_location = self.ModelDataObj.xmlid_to_res_id('stock.stock_location_stock')
|
self.stock_location = self.ModelDataObj.xmlid_to_res_id('stock.stock_location_stock')
|
||||||
self.pack_location = self.ModelDataObj.xmlid_to_res_id('stock.location_pack_zone')
|
self.pack_location = self.ModelDataObj.xmlid_to_res_id('stock.location_pack_zone')
|
||||||
|
self.output_location = self.ModelDataObj.xmlid_to_res_id('stock.stock_location_output')
|
||||||
self.customer_location = self.ModelDataObj.xmlid_to_res_id('stock.stock_location_customers')
|
self.customer_location = self.ModelDataObj.xmlid_to_res_id('stock.stock_location_customers')
|
||||||
self.categ_unit = self.ModelDataObj.xmlid_to_res_id('product.product_uom_categ_unit')
|
self.categ_unit = self.ModelDataObj.xmlid_to_res_id('product.product_uom_categ_unit')
|
||||||
self.categ_kgm = self.ModelDataObj.xmlid_to_res_id('product.product_uom_categ_kgm')
|
self.categ_kgm = self.ModelDataObj.xmlid_to_res_id('product.product_uom_categ_kgm')
|
||||||
|
|||||||
@@ -45,6 +45,62 @@ class TestPickShip(TestStockCommon):
|
|||||||
})
|
})
|
||||||
return picking_pick, picking_client
|
return picking_pick, picking_client
|
||||||
|
|
||||||
|
def create_pick_pack_ship(self):
|
||||||
|
picking_ship = self.env['stock.picking'].create({
|
||||||
|
'location_id': self.pack_location,
|
||||||
|
'location_dest_id': self.customer_location,
|
||||||
|
'partner_id': self.partner_delta_id,
|
||||||
|
'picking_type_id': self.picking_type_out,
|
||||||
|
})
|
||||||
|
|
||||||
|
ship = self.MoveObj.create({
|
||||||
|
'name': self.productA.name,
|
||||||
|
'product_id': self.productA.id,
|
||||||
|
'product_uom_qty': 1,
|
||||||
|
'product_uom': self.productA.uom_id.id,
|
||||||
|
'picking_id': picking_ship.id,
|
||||||
|
'location_id': self.output_location,
|
||||||
|
'location_dest_id': self.customer_location,
|
||||||
|
})
|
||||||
|
|
||||||
|
picking_pack = self.env['stock.picking'].create({
|
||||||
|
'location_id': self.stock_location,
|
||||||
|
'location_dest_id': self.pack_location,
|
||||||
|
'partner_id': self.partner_delta_id,
|
||||||
|
'picking_type_id': self.picking_type_out,
|
||||||
|
})
|
||||||
|
|
||||||
|
pack = self.MoveObj.create({
|
||||||
|
'name': self.productA.name,
|
||||||
|
'product_id': self.productA.id,
|
||||||
|
'product_uom_qty': 1,
|
||||||
|
'product_uom': self.productA.uom_id.id,
|
||||||
|
'picking_id': picking_pack.id,
|
||||||
|
'location_id': self.pack_location,
|
||||||
|
'location_dest_id': self.output_location,
|
||||||
|
'move_dest_ids': [(4, ship.id)],
|
||||||
|
})
|
||||||
|
|
||||||
|
picking_pick = self.env['stock.picking'].create({
|
||||||
|
'location_id': self.stock_location,
|
||||||
|
'location_dest_id': self.pack_location,
|
||||||
|
'partner_id': self.partner_delta_id,
|
||||||
|
'picking_type_id': self.picking_type_out,
|
||||||
|
})
|
||||||
|
|
||||||
|
self.MoveObj.create({
|
||||||
|
'name': self.productA.name,
|
||||||
|
'product_id': self.productA.id,
|
||||||
|
'product_uom_qty': 1,
|
||||||
|
'product_uom': self.productA.uom_id.id,
|
||||||
|
'picking_id': picking_pick.id,
|
||||||
|
'location_id': self.stock_location,
|
||||||
|
'location_dest_id': self.pack_location,
|
||||||
|
'move_dest_ids': [(4, pack.id)],
|
||||||
|
'state': 'confirmed',
|
||||||
|
})
|
||||||
|
return picking_pick, picking_pack, picking_ship
|
||||||
|
|
||||||
def test_mto_moves(self):
|
def test_mto_moves(self):
|
||||||
"""
|
"""
|
||||||
10 in stock, do pick->ship and check ship is assigned when pick is done, then backorder of ship
|
10 in stock, do pick->ship and check ship is assigned when pick is done, then backorder of ship
|
||||||
@@ -279,6 +335,178 @@ class TestPickShip(TestStockCommon):
|
|||||||
self.assertEqual(picking_client.state, 'assigned')
|
self.assertEqual(picking_client.state, 'assigned')
|
||||||
self.assertEqual(picking_client.move_lines.reserved_availability, 5.0)
|
self.assertEqual(picking_client.move_lines.reserved_availability, 5.0)
|
||||||
|
|
||||||
|
def test_pick_ship_return(self):
|
||||||
|
""" Create pick and ship. Bring it ot the customer and then return
|
||||||
|
it to stock. This test check the state and the quantity after each move in
|
||||||
|
order to ensure that it is correct.
|
||||||
|
"""
|
||||||
|
picking_pick, picking_ship = self.create_pick_ship()
|
||||||
|
stock_location = self.env['stock.location'].browse(self.stock_location)
|
||||||
|
pack_location = self.env['stock.location'].browse(self.pack_location)
|
||||||
|
customer_location = self.env['stock.location'].browse(self.customer_location)
|
||||||
|
self.productA.tracking = 'lot'
|
||||||
|
lot = self.env['stock.production.lot'].create({
|
||||||
|
'product_id': self.productA.id,
|
||||||
|
'name': '123456789'
|
||||||
|
})
|
||||||
|
self.env['stock.quant']._update_available_quantity(self.productA, stock_location, 10.0, lot_id=lot)
|
||||||
|
|
||||||
|
picking_pick.action_assign()
|
||||||
|
picking_pick.move_lines[0].move_line_ids[0].qty_done = 10.0
|
||||||
|
picking_pick.action_done()
|
||||||
|
self.assertEqual(picking_pick.state, 'done')
|
||||||
|
self.assertEqual(picking_ship.state, 'assigned')
|
||||||
|
|
||||||
|
picking_ship.action_assign()
|
||||||
|
picking_ship.move_lines[0].move_line_ids[0].qty_done = 10.0
|
||||||
|
picking_ship.action_done()
|
||||||
|
|
||||||
|
customer_quantity = self.env['stock.quant']._get_available_quantity(self.productA, customer_location, lot_id=lot)
|
||||||
|
self.assertEqual(customer_quantity, 10, 'It should be one product in customer')
|
||||||
|
|
||||||
|
""" First we create the return picking for pick pinking.
|
||||||
|
Since we do not have created the return between customer and
|
||||||
|
output. This return should not be available and should only have
|
||||||
|
picking pick as origin move.
|
||||||
|
"""
|
||||||
|
stock_return_picking = self.env['stock.return.picking']\
|
||||||
|
.with_context(active_ids=picking_pick.ids, active_id=picking_pick.ids[0])\
|
||||||
|
.create({})
|
||||||
|
stock_return_picking.product_return_moves.quantity = 10.0
|
||||||
|
stock_return_picking_action = stock_return_picking.create_returns()
|
||||||
|
return_pick_picking = self.env['stock.picking'].browse(stock_return_picking_action['res_id'])
|
||||||
|
|
||||||
|
self.assertEqual(return_pick_picking.state, 'waiting')
|
||||||
|
|
||||||
|
stock_return_picking = self.env['stock.return.picking']\
|
||||||
|
.with_context(active_ids=picking_ship.ids, active_id=picking_ship.ids[0])\
|
||||||
|
.create({})
|
||||||
|
stock_return_picking.product_return_moves.quantity = 10.0
|
||||||
|
stock_return_picking_action = stock_return_picking.create_returns()
|
||||||
|
return_ship_picking = self.env['stock.picking'].browse(stock_return_picking_action['res_id'])
|
||||||
|
|
||||||
|
self.assertEqual(return_ship_picking.state, 'assigned', 'Return ship picking should automatically be assigned')
|
||||||
|
""" We created the return for ship picking. The origin/destination
|
||||||
|
link between return moves should have been created during return creation.
|
||||||
|
"""
|
||||||
|
self.assertTrue(return_ship_picking.move_lines in return_pick_picking.move_lines.mapped('move_orig_ids'),
|
||||||
|
'The pick return picking\'s moves should have the ship return picking\'s moves as origin')
|
||||||
|
|
||||||
|
self.assertTrue(return_pick_picking.move_lines in return_ship_picking.move_lines.mapped('move_dest_ids'),
|
||||||
|
'The ship return picking\'s moves should have the pick return picking\'s moves as destination')
|
||||||
|
|
||||||
|
return_ship_picking.move_lines[0].move_line_ids[0].write({
|
||||||
|
'qty_done': 10.0,
|
||||||
|
'lot_id': lot.id,
|
||||||
|
})
|
||||||
|
return_ship_picking.action_done()
|
||||||
|
self.assertEqual(return_ship_picking.state, 'done')
|
||||||
|
self.assertEqual(return_pick_picking.state, 'assigned')
|
||||||
|
|
||||||
|
customer_quantity = self.env['stock.quant']._get_available_quantity(self.productA, customer_location, lot_id=lot)
|
||||||
|
self.assertEqual(customer_quantity, 0, 'It should be one product in customer')
|
||||||
|
|
||||||
|
pack_quantity = self.env['stock.quant']._get_available_quantity(self.productA, pack_location, lot_id=lot)
|
||||||
|
self.assertEqual(pack_quantity, 0, 'It should be one product in pack location but is reserved')
|
||||||
|
|
||||||
|
# Should use previous move lot.
|
||||||
|
return_pick_picking.move_lines[0].move_line_ids[0].qty_done = 10.0
|
||||||
|
return_pick_picking.action_done()
|
||||||
|
self.assertEqual(return_pick_picking.state, 'done')
|
||||||
|
|
||||||
|
stock_quantity = self.env['stock.quant']._get_available_quantity(self.productA, stock_location, lot_id=lot)
|
||||||
|
self.assertEqual(stock_quantity, 10, 'The product is not back in stock')
|
||||||
|
|
||||||
|
def test_pick_pack_ship_return(self):
|
||||||
|
""" This test do a pick pack ship delivery to customer and then
|
||||||
|
return it to stock. Once everything is done, this test will check
|
||||||
|
if all the link orgini/destination between moves are correct.
|
||||||
|
"""
|
||||||
|
picking_pick, picking_pack, picking_ship = self.create_pick_pack_ship()
|
||||||
|
stock_location = self.env['stock.location'].browse(self.stock_location)
|
||||||
|
self.productA.tracking = 'serial'
|
||||||
|
lot = self.env['stock.production.lot'].create({
|
||||||
|
'product_id': self.productA.id,
|
||||||
|
'name': '123456789'
|
||||||
|
})
|
||||||
|
self.env['stock.quant']._update_available_quantity(self.productA, stock_location, 1.0, lot_id=lot)
|
||||||
|
|
||||||
|
picking_pick.action_assign()
|
||||||
|
picking_pick.move_lines[0].move_line_ids[0].qty_done = 1.0
|
||||||
|
picking_pick.action_done()
|
||||||
|
|
||||||
|
picking_pack.action_assign()
|
||||||
|
picking_pack.move_lines[0].move_line_ids[0].qty_done = 1.0
|
||||||
|
picking_pack.action_done()
|
||||||
|
|
||||||
|
picking_ship.action_assign()
|
||||||
|
picking_ship.move_lines[0].move_line_ids[0].qty_done = 1.0
|
||||||
|
picking_ship.action_done()
|
||||||
|
|
||||||
|
stock_return_picking = self.env['stock.return.picking']\
|
||||||
|
.with_context(active_ids=picking_ship.ids, active_id=picking_ship.ids[0])\
|
||||||
|
.create({})
|
||||||
|
stock_return_picking.product_return_moves.quantity = 1.0
|
||||||
|
stock_return_picking_action = stock_return_picking.create_returns()
|
||||||
|
return_ship_picking = self.env['stock.picking'].browse(stock_return_picking_action['res_id'])
|
||||||
|
|
||||||
|
return_ship_picking.move_lines[0].move_line_ids[0].write({
|
||||||
|
'qty_done': 1.0,
|
||||||
|
'lot_id': lot.id,
|
||||||
|
})
|
||||||
|
return_ship_picking.action_done()
|
||||||
|
|
||||||
|
stock_return_picking = self.env['stock.return.picking']\
|
||||||
|
.with_context(active_ids=picking_pack.ids, active_id=picking_pack.ids[0])\
|
||||||
|
.create({})
|
||||||
|
stock_return_picking.product_return_moves.quantity = 1.0
|
||||||
|
stock_return_picking_action = stock_return_picking.create_returns()
|
||||||
|
return_pack_picking = self.env['stock.picking'].browse(stock_return_picking_action['res_id'])
|
||||||
|
|
||||||
|
return_pack_picking.move_lines[0].move_line_ids[0].qty_done = 1.0
|
||||||
|
return_pack_picking.action_done()
|
||||||
|
|
||||||
|
stock_return_picking = self.env['stock.return.picking']\
|
||||||
|
.with_context(active_ids=picking_pick.ids, active_id=picking_pick.ids[0])\
|
||||||
|
.create({})
|
||||||
|
stock_return_picking.product_return_moves.quantity = 1.0
|
||||||
|
stock_return_picking_action = stock_return_picking.create_returns()
|
||||||
|
return_pick_picking = self.env['stock.picking'].browse(stock_return_picking_action['res_id'])
|
||||||
|
|
||||||
|
return_pick_picking.move_lines[0].move_line_ids[0].qty_done = 1.0
|
||||||
|
return_pick_picking.action_done()
|
||||||
|
|
||||||
|
# Now that everything is returned we will check if the return moves are correctly linked between them.
|
||||||
|
# +--------------------------------------------------------------------------------------------------------+
|
||||||
|
# | -- picking_pick(1) --> -- picking_pack(2) --> -- picking_ship(3) -->
|
||||||
|
# | Stock Pack Output Customer
|
||||||
|
# | <--- return pick(6) -- <--- return pack(5) -- <--- return ship(4) --
|
||||||
|
# +--------------------------------------------------------------------------------------------------------+
|
||||||
|
# Recaps of final link (MO = move_orig_ids, MD = move_dest_ids)
|
||||||
|
# picking_pick(1) : MO = (), MD = (2,6)
|
||||||
|
# picking_pack(2) : MO = (1), MD = (3,5)
|
||||||
|
# picking ship(3) : MO = (2), MD = (4)
|
||||||
|
# return ship(4) : MO = (3), MD = (5)
|
||||||
|
# return pack(5) : MO = (2, 4), MD = (6)
|
||||||
|
# return pick(6) : MO = (1, 5), MD = ()
|
||||||
|
|
||||||
|
self.assertEqual(len(picking_pick.move_lines.move_orig_ids), 0, 'Picking pick should not have origin moves')
|
||||||
|
self.assertEqual(set(picking_pick.move_lines.move_dest_ids.ids), set((picking_pack.move_lines | return_pick_picking.move_lines).ids))
|
||||||
|
|
||||||
|
self.assertEqual(set(picking_pack.move_lines.move_orig_ids.ids), set(picking_pick.move_lines.ids))
|
||||||
|
self.assertEqual(set(picking_pack.move_lines.move_dest_ids.ids), set((picking_ship.move_lines | return_pack_picking.move_lines).ids))
|
||||||
|
|
||||||
|
self.assertEqual(set(picking_ship.move_lines.move_orig_ids.ids), set(picking_pack.move_lines.ids))
|
||||||
|
self.assertEqual(set(picking_ship.move_lines.move_dest_ids.ids), set(return_ship_picking.move_lines.ids))
|
||||||
|
|
||||||
|
self.assertEqual(set(return_ship_picking.move_lines.move_orig_ids.ids), set(picking_ship.move_lines.ids))
|
||||||
|
self.assertEqual(set(return_ship_picking.move_lines.move_dest_ids.ids), set(return_pack_picking.move_lines.ids))
|
||||||
|
|
||||||
|
self.assertEqual(set(return_pack_picking.move_lines.move_orig_ids.ids), set((picking_pack.move_lines | return_ship_picking.move_lines).ids))
|
||||||
|
self.assertEqual(set(return_pack_picking.move_lines.move_dest_ids.ids), set(return_pick_picking.move_lines.ids))
|
||||||
|
|
||||||
|
self.assertEqual(set(return_pick_picking.move_lines.move_orig_ids.ids), set((picking_pick.move_lines | return_pack_picking.move_lines).ids))
|
||||||
|
self.assertEqual(len(return_pick_picking.move_lines.move_dest_ids), 0)
|
||||||
|
|
||||||
class TestSinglePicking(TestStockCommon):
|
class TestSinglePicking(TestStockCommon):
|
||||||
def test_backorder_1(self):
|
def test_backorder_1(self):
|
||||||
|
|||||||
@@ -110,8 +110,19 @@ class ReturnPicking(models.TransientModel):
|
|||||||
returned_lines += 1
|
returned_lines += 1
|
||||||
vals = self._prepare_move_default_values(return_line, new_picking)
|
vals = self._prepare_move_default_values(return_line, new_picking)
|
||||||
r = return_line.move_id.copy(vals)
|
r = return_line.move_id.copy(vals)
|
||||||
r.write({'move_orig_ids': [(4, return_line.move_id.id, False)]})
|
vals = {}
|
||||||
|
|
||||||
|
# +--------------------------------------------------------------------------------------------------------+
|
||||||
|
# | picking_pick <--Move Orig-- picking_pack --Move Dest--> picking_ship
|
||||||
|
# | | returned_move_ids ↑ | returned_move_ids
|
||||||
|
# | ↓ | return_line.move_id ↓
|
||||||
|
# | return pick(Add as dest) return toLink return ship(Add as orig)
|
||||||
|
# +--------------------------------------------------------------------------------------------------------+
|
||||||
|
move_orig_to_link = return_line.move_id.move_dest_ids.mapped('returned_move_ids')
|
||||||
|
move_dest_to_link = return_line.move_id.move_orig_ids.mapped('returned_move_ids')
|
||||||
|
vals['move_orig_ids'] = [(4, m.id) for m in move_orig_to_link | return_line.move_id]
|
||||||
|
vals['move_dest_ids'] = [(4, m.id) for m in move_dest_to_link]
|
||||||
|
r.write(vals)
|
||||||
if not returned_lines:
|
if not returned_lines:
|
||||||
raise UserError(_("Please specify at least one non-zero quantity."))
|
raise UserError(_("Please specify at least one non-zero quantity."))
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user