summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorSimon Lejeune <sle@openerp.com>2017-10-01 14:40:50 +0200
committerSimon Lejeune <sle@openerp.com>2017-10-02 10:11:49 +0200
commit021a95d99827d1cb1d08d0b7c19ad56e8adc4c1a (patch)
tree4ed8f779991b131d2e4e93c33bba853fc5a13c4c
parentf6144fe1f36ad44affff785d0e9c2c32d37a8963 (diff)
[FIX] stock: move: _action_assign and _free_reservation
The code made the assumption that everything brought by in chained move is available on the quants. This is not always true, for example when x products are brought by a move in the chain, the user can make an inventory adjustment that reduce the quantity available. Thus, we implement a fallback in case the quantity brought by is not available: do not reserve more than what's really available. If the user make available some products with the same characteristics (lot/owner/pack) in the location, they will be used by a new call to _action_assign. Indeed, the reserved quants are not discernable. This is not an issue in reality, as the user cannot discern the products himself if they share the same characteristics.
-rw-r--r--addons/stock/models/stock_move.py18
-rw-r--r--addons/stock/tests/test_inventory.py102
2 files changed, 116 insertions, 4 deletions
diff --git a/addons/stock/models/stock_move.py b/addons/stock/models/stock_move.py
index 97c29b45aac..b340688bac4 100644
--- a/addons/stock/models/stock_move.py
+++ b/addons/stock/models/stock_move.py
@@ -775,11 +775,21 @@ class StockMove(models.Model):
taken_quantity = min(available_quantity, need)
+ quants = []
+ try:
+ quants = self.env['stock.quant']._update_reserved_quantity(
+ self.product_id, location_id, taken_quantity, lot_id=lot_id,
+ package_id=package_id, owner_id=owner_id, strict=strict
+ )
+ except UserError:
+ # If it raises here, it means that the `available_quantity` brought by a done move line
+ # is not available on the quants itself. This could be the result of an inventory
+ # adjustment that removed totally of partially `available_quantity`. When this happens, we
+ # chose to do nothing. This situation could not happen on MTS move, because in this case
+ # `available_quantity` is directly the quantity on the quants themselves.
+ taken_quantity = 0
+
# Find a candidate move line to update or create a new one.
- quants = self.env['stock.quant']._update_reserved_quantity(
- self.product_id, location_id, taken_quantity, lot_id=lot_id,
- package_id=package_id, owner_id=owner_id, strict=strict
- )
for reserved_quant, quantity in quants:
to_update = self.move_line_ids.filtered(lambda m: m.location_id.id == reserved_quant.location_id.id and m.lot_id.id == reserved_quant.lot_id.id and m.package_id.id == reserved_quant.package_id.id and m.owner_id.id == reserved_quant.owner_id.id)
if to_update:
diff --git a/addons/stock/tests/test_inventory.py b/addons/stock/tests/test_inventory.py
index f0a2901860a..796fed4bd2d 100644
--- a/addons/stock/tests/test_inventory.py
+++ b/addons/stock/tests/test_inventory.py
@@ -8,6 +8,9 @@ class TestInventory(TransactionCase):
def setUp(self):
super(TestInventory, self).setUp()
self.stock_location = self.env.ref('stock.stock_location_stock')
+ self.pack_location = self.env.ref('stock.location_pack_zone')
+ self.pack_location.active = True
+ self.customer_location = self.env.ref('stock.stock_location_customers')
self.uom_unit = self.env.ref('product.product_uom_unit')
self.product1 = self.env['product.product'].create({
'name': 'Product A',
@@ -165,3 +168,102 @@ class TestInventory(TransactionCase):
self.assertEqual(len(quant), 1)
self.assertEqual(quant.quantity, 5)
self.assertEqual(quant.owner_id.id, owner1.id)
+
+ def test_inventory_6(self):
+ """ Test that for chained moves, making an inventory adjustment to reduce a quantity that
+ has been reserved correctly free the reservation. After that, add products in stock and check
+ that they're used if the user encodes more than what's available through the chain
+ """
+ # add 10 products in stock
+ inventory = self.env['stock.inventory'].create({
+ 'name': 'add 10 products 1',
+ 'filter': 'product',
+ 'location_id': self.stock_location.id,
+ 'product_id': self.product1.id,
+ 'exhausted': True, # should be set by an onchange
+ })
+ inventory.action_start()
+ inventory.line_ids.product_qty = 10
+ inventory.action_done()
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product1, self.stock_location), 10.0)
+
+ # Make a chain of two moves, validate the first and check that 10 products are reserved
+ # in the second one.
+ move_stock_pack = self.env['stock.move'].create({
+ 'name': 'test_link_2_1',
+ 'location_id': self.stock_location.id,
+ 'location_dest_id': self.pack_location.id,
+ 'product_id': self.product1.id,
+ 'product_uom': self.uom_unit.id,
+ 'product_uom_qty': 10.0,
+ })
+ move_pack_cust = self.env['stock.move'].create({
+ 'name': 'test_link_2_2',
+ 'location_id': self.pack_location.id,
+ 'location_dest_id': self.customer_location.id,
+ 'product_id': self.product1.id,
+ 'product_uom': self.uom_unit.id,
+ 'product_uom_qty': 10.0,
+ })
+ move_stock_pack.write({'move_dest_ids': [(4, move_pack_cust.id, 0)]})
+ move_pack_cust.write({'move_orig_ids': [(4, move_stock_pack.id, 0)]})
+ (move_stock_pack + move_pack_cust)._action_confirm()
+ move_stock_pack._action_assign()
+ self.assertEqual(move_stock_pack.state, 'assigned')
+ move_stock_pack.move_line_ids.qty_done = 10
+ move_stock_pack._action_done()
+ self.assertEqual(move_stock_pack.state, 'done')
+ self.assertEqual(move_pack_cust.state, 'assigned')
+ self.assertEqual(self.env['stock.quant']._gather(self.product1, self.pack_location).quantity, 10.0)
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product1, self.pack_location), 0.0)
+
+ # Make and inventory adjustment and remove two products from the pack location. This should
+ # free the reservation of the second move.
+ inventory = self.env['stock.inventory'].create({
+ 'name': 'remove 2 products 1',
+ 'filter': 'product',
+ 'location_id': self.pack_location.id,
+ 'product_id': self.product1.id,
+ })
+ inventory.action_start()
+ inventory.line_ids.product_qty = 8
+ inventory.action_done()
+ self.assertEqual(self.env['stock.quant']._gather(self.product1, self.pack_location).quantity, 8.0)
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product1, self.pack_location), 0)
+ self.assertEqual(move_pack_cust.state, 'partially_available')
+ self.assertEqual(move_pack_cust.reserved_availability, 8)
+
+ # If the user tries to assign again, only 8 products are available and thus the reservation
+ # state should not change.
+ move_pack_cust._action_assign()
+ self.assertEqual(move_pack_cust.state, 'partially_available')
+ self.assertEqual(move_pack_cust.reserved_availability, 8)
+
+ # Make a new inventory adjustment and bring two now products.
+ inventory = self.env['stock.inventory'].create({
+ 'name': 'remove 2 products 1',
+ 'filter': 'product',
+ 'location_id': self.pack_location.id,
+ 'product_id': self.product1.id,
+ })
+ inventory.action_start()
+ inventory.line_ids.product_qty = 10
+ inventory.action_done()
+
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product1, self.pack_location), 2)
+
+ # Nothing should have changed for our pack move
+ self.assertEqual(move_pack_cust.state, 'partially_available')
+ self.assertEqual(move_pack_cust.reserved_availability, 8)
+
+ # Running _action_assign will now find the new available quantity. Indeed, as the products
+ # are not discernabl (not lot/pack/owner), even if the new available quantity is not directly
+ # brought by the chain, the system fill take them into account.
+ move_pack_cust._action_assign()
+ self.assertEqual(move_pack_cust.state, 'assigned')
+
+ # move all the things
+ move_pack_cust.move_line_ids.qty_done = 10
+ move_stock_pack._action_done()
+
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product1, self.pack_location), 0)