From 3b59d0b12bc9a80e325aa07d004ed9cf9d85f857 Mon Sep 17 00:00:00 2001 From: Arnold Moyaux <arm@odoo.com> Date: Tue, 26 Jan 2021 11:13:22 +0000 Subject: [PATCH] [FIX] stock: couldn't unreserve mixed tracking stock - Install stock - Go to Inventory > Configuration > Settings and enable "Lots" and "Storage Locations" - Create a Product tracked By Lots (i.e. Product X) - Go to Inventory > Operations > Inventory Adjustments - Create an Inventory Adjustment for Product X: Product | Location | Lot/SN | Real Quantity ------------------------------------------------------------- Product X | WH/Stock | LOT 01 | 20 Product X | WH/Stock | | 10 - Validate Inventory - Go to Inventory > Operations > Transfers and create one: * Source Location: WH/Stock * Destination Location: WH/Stock/Shelf1 * Operation Type: Internal Transfers * Operations: [Product: Product X, Initial Demand: 25] - Save Transfer, Mark As Todo and Check availability - Click on list icon of Operation line for Product X to display Detailed Operations - 20 units of LOT 01 and 5 units without lot have been reserved - Set LOT 01 for the 5 reserved units without lot and confirm - Open Detailed Operations again - There are now 20 units of LOT 01 and 5 units of LOT 01 - Remove the row with 5 units and confirm - Check availability and open Detailed Operation - There is now only a row with 25 reserved units of LOT 01 - Unreserve The following errror is raised: "It is not possible to unreserve more products of P than you have in stock." It happens because the system is not able to manage quants with lots and wihtout lots at the same time. When modifying the move line to 25 reserved units. It's composed of 20 quants with lot and 5 quants without lot. And when unreserving it will check if there is a quants with 25 units with the lot and if it's not found 25 units without lot. But never 25 units of quants with lots and without lots. opw-2419444 Close #64497 closes odoo/odoo#66029 X-original-commit: 217c76b11e6c2bb5bf861284343c87b4ca241da1 Signed-off-by: Arnold Moyaux <amoyaux@users.noreply.github.com> --- addons/stock/models/product.py | 2 ++ addons/stock/models/stock_move_line.py | 31 ++++---------------------- addons/stock/models/stock_quant.py | 12 ++++++---- addons/stock/tests/test_inventory.py | 4 ++-- addons/stock/tests/test_move.py | 10 ++++----- addons/stock/tests/test_quant.py | 8 +++---- 6 files changed, 25 insertions(+), 42 deletions(-) diff --git a/addons/stock/models/product.py b/addons/stock/models/product.py index a8f5e05ae766..84ac17a6b968 100644 --- a/addons/stock/models/product.py +++ b/addons/stock/models/product.py @@ -531,6 +531,8 @@ class Product(models.Model): owner_id = self.env['res.partner'].browse(owner_id) to_uom = self.env['uom.uom'].browse(to_uom) quants = self.env['stock.quant']._gather(product_id, location_id, lot_id=lot_id, package_id=package_id, owner_id=owner_id, strict=True) + if lot_id: + quants = quants.filtered(lambda q: q.lot_id == lot_id) theoretical_quantity = sum([quant.quantity for quant in quants]) if to_uom and product_id.uom_id != to_uom: theoretical_quantity = product_id.uom_id._compute_quantity(theoretical_quantity, to_uom) diff --git a/addons/stock/models/stock_move_line.py b/addons/stock/models/stock_move_line.py index 68b193305d29..633e1250fb5e 100644 --- a/addons/stock/models/stock_move_line.py +++ b/addons/stock/models/stock_move_line.py @@ -281,14 +281,7 @@ class StockMoveLine(models.Model): # Unreserve the old charateristics of the move line. if not ml._should_bypass_reservation(ml.location_id): - try: - Quant._update_reserved_quantity(ml.product_id, ml.location_id, -ml.product_qty, lot_id=ml.lot_id, package_id=ml.package_id, owner_id=ml.owner_id, strict=True) - except UserError: - # If we were not able to unreserve on tracked quants, we can use untracked ones. - if ml.lot_id: - Quant._update_reserved_quantity(ml.product_id, ml.location_id, -ml.product_qty, lot_id=False, package_id=ml.package_id, owner_id=ml.owner_id, strict=True) - else: - raise + Quant._update_reserved_quantity(ml.product_id, ml.location_id, -ml.product_qty, lot_id=ml.lot_id, package_id=ml.package_id, owner_id=ml.owner_id, strict=True) # Reserve the maximum available of the new charateristics of the move line. if not ml._should_bypass_reservation(updates.get('location_id', ml.location_id)): @@ -298,14 +291,7 @@ class StockMoveLine(models.Model): package_id=updates.get('package_id', ml.package_id), owner_id=updates.get('owner_id', ml.owner_id), strict=True) reserved_qty = sum([x[1] for x in q]) except UserError: - if updates.get('lot_id'): - # If we were not able to reserve on tracked quants, we can use untracked ones. - try: - q = Quant._update_reserved_quantity(ml.product_id, updates.get('location_id', ml.location_id), new_product_uom_qty, lot_id=False, - package_id=updates.get('package_id', ml.package_id), owner_id=updates.get('owner_id', ml.owner_id), strict=True) - reserved_qty = sum([x[1] for x in q]) - except UserError: - pass + pass if reserved_qty != new_product_uom_qty: new_product_uom_qty = ml.product_id.uom_id._compute_quantity(reserved_qty, ml.product_uom_id, rounding_method='HALF-UP') moves_to_recompute_state |= ml.move_id @@ -388,13 +374,7 @@ class StockMoveLine(models.Model): raise UserError(_('You can not delete product moves if the picking is done. You can only correct the done quantities.')) # Unlinking a move line should unreserve. if ml.product_id.type == 'product' and not ml._should_bypass_reservation(ml.location_id) and not float_is_zero(ml.product_qty, precision_digits=precision): - try: - self.env['stock.quant']._update_reserved_quantity(ml.product_id, ml.location_id, -ml.product_qty, lot_id=ml.lot_id, package_id=ml.package_id, owner_id=ml.owner_id, strict=True) - except UserError: - if ml.lot_id: - self.env['stock.quant']._update_reserved_quantity(ml.product_id, ml.location_id, -ml.product_qty, lot_id=False, package_id=ml.package_id, owner_id=ml.owner_id, strict=True) - else: - raise + self.env['stock.quant']._update_reserved_quantity(ml.product_id, ml.location_id, -ml.product_qty, lot_id=ml.lot_id, package_id=ml.package_id, owner_id=ml.owner_id, strict=True) moves = self.mapped('move_id') res = super(StockMoveLine, self).unlink() if moves: @@ -485,10 +465,7 @@ class StockMoveLine(models.Model): ml._free_reservation(ml.product_id, ml.location_id, extra_qty, lot_id=ml.lot_id, package_id=ml.package_id, owner_id=ml.owner_id, ml_to_ignore=ml_to_ignore) # unreserve what's been reserved if not ml._should_bypass_reservation(ml.location_id) and ml.product_id.type == 'product' and ml.product_qty: - try: - Quant._update_reserved_quantity(ml.product_id, ml.location_id, -ml.product_qty, lot_id=ml.lot_id, package_id=ml.package_id, owner_id=ml.owner_id, strict=True) - except UserError: - Quant._update_reserved_quantity(ml.product_id, ml.location_id, -ml.product_qty, lot_id=False, package_id=ml.package_id, owner_id=ml.owner_id, strict=True) + Quant._update_reserved_quantity(ml.product_id, ml.location_id, -ml.product_qty, lot_id=ml.lot_id, package_id=ml.package_id, owner_id=ml.owner_id, strict=True) # move what's been actually done quantity = ml.product_uom_id._compute_quantity(ml.qty_done, ml.move_id.product_id.uom_id, rounding_method='HALF-UP') diff --git a/addons/stock/models/stock_quant.py b/addons/stock/models/stock_quant.py index f8d5ed09a6da..21c0d69bc364 100644 --- a/addons/stock/models/stock_quant.py +++ b/addons/stock/models/stock_quant.py @@ -280,14 +280,14 @@ class StockQuant(models.Model): ] if not strict: if lot_id: - domain = expression.AND([[('lot_id', '=', lot_id.id)], domain]) + domain = expression.AND([['|', ('lot_id', '=', lot_id.id), ('lot_id', '=', False)], domain]) if package_id: domain = expression.AND([[('package_id', '=', package_id.id)], domain]) if owner_id: domain = expression.AND([[('owner_id', '=', owner_id.id)], domain]) domain = expression.AND([[('location_id', 'child_of', location_id.id)], domain]) else: - domain = expression.AND([[('lot_id', '=', lot_id and lot_id.id or False)], domain]) + domain = expression.AND([['|', ('lot_id', '=', lot_id.id), ('lot_id', '=', False)] if lot_id else [('lot_id', '=', False)], domain]) domain = expression.AND([[('package_id', '=', package_id and package_id.id or False)], domain]) domain = expression.AND([[('owner_id', '=', owner_id and owner_id.id or False)], domain]) domain = expression.AND([[('location_id', '=', location_id.id)], domain]) @@ -302,7 +302,9 @@ class StockQuant(models.Model): self._cr.execute(query_str, where_clause_params) res = self._cr.fetchall() # No uniquify list necessary as auto_join is not applied anyways... - return self.browse([x[0] for x in res]) + quants = self.browse([x[0] for x in res]) + quants = quants.sorted(lambda q: not q.lot_id) + return quants @api.model def _get_available_quantity(self, product_id, location_id, lot_id=None, package_id=None, owner_id=None, strict=False, allow_negative=False): @@ -401,6 +403,8 @@ class StockQuant(models.Model): """ self = self.sudo() quants = self._gather(product_id, location_id, lot_id=lot_id, package_id=package_id, owner_id=owner_id, strict=True) + if lot_id and quantity > 0: + quants = quants.filtered(lambda q: q.lot_id) incoming_dates = [d for d in quants.mapped('in_date') if d] incoming_dates = [fields.Datetime.from_string(incoming_date) for incoming_date in incoming_dates] @@ -462,7 +466,7 @@ class StockQuant(models.Model): if float_compare(quantity, 0, precision_rounding=rounding) > 0: # if we want to reserve - available_quantity = self._get_available_quantity(product_id, location_id, lot_id=lot_id, package_id=package_id, owner_id=owner_id, strict=strict) + available_quantity = sum(quants.filtered(lambda q: float_compare(q.quantity, 0, precision_rounding=rounding) > 0).mapped('quantity')) - sum(quants.mapped('reserved_quantity')) if float_compare(quantity, available_quantity, precision_rounding=rounding) > 0: raise UserError(_('It is not possible to reserve more products of %s than you have in stock.') % product_id.display_name) elif float_compare(quantity, 0, precision_rounding=rounding) < 0: diff --git a/addons/stock/tests/test_inventory.py b/addons/stock/tests/test_inventory.py index b787b21a423e..d09cfb487152 100644 --- a/addons/stock/tests/test_inventory.py +++ b/addons/stock/tests/test_inventory.py @@ -152,10 +152,10 @@ class TestInventory(SavepointCase): wizard_warning_lot.action_confirm() # check - self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product2, self.stock_location, lot_id=lot1, strict=True), 1.0) + self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product2, self.stock_location, lot_id=lot1, strict=True), 11.0) self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product2, self.stock_location, strict=True), 10.0) self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product2, self.stock_location), 11.0) - self.assertEqual(len(self.env['stock.quant']._gather(self.product2, self.stock_location, lot_id=lot1, strict=True)), 1.0) + self.assertEqual(len(self.env['stock.quant']._gather(self.product2, self.stock_location, lot_id=lot1, strict=True).filtered(lambda q: q.lot_id)), 1.0) self.assertEqual(len(self.env['stock.quant']._gather(self.product2, self.stock_location, strict=True)), 1.0) self.assertEqual(len(self.env['stock.quant']._gather(self.product2, self.stock_location)), 2.0) diff --git a/addons/stock/tests/test_move.py b/addons/stock/tests/test_move.py index 3a94fb83d07b..b6df2ff3aabd 100644 --- a/addons/stock/tests/test_move.py +++ b/addons/stock/tests/test_move.py @@ -365,10 +365,10 @@ class StockMove(SavepointCase): # no changes on quants, even if i made some move lines with a lot id whom reserved on untracked quants self.assertEqual(len(self.gather_relevant(self.product_serial, self.stock_location, strict=True)), 1.0) # with a qty of 2 - self.assertEqual(len(self.gather_relevant(self.product_serial, self.stock_location, lot_id=lot1, strict=True)), 1.0) - self.assertEqual(len(self.gather_relevant(self.product_serial, self.stock_location, lot_id=lot2, strict=True)), 1.0) - self.assertEqual(len(self.gather_relevant(self.product_serial, self.stock_location, lot_id=lot3, strict=True)), 0) - self.assertEqual(len(self.gather_relevant(self.product_serial, self.stock_location, lot_id=lot4, strict=True)), 0) + self.assertEqual(len(self.gather_relevant(self.product_serial, self.stock_location, lot_id=lot1, strict=True).filtered(lambda q: q.lot_id)), 1.0) + self.assertEqual(len(self.gather_relevant(self.product_serial, self.stock_location, lot_id=lot2, strict=True).filtered(lambda q: q.lot_id)), 1.0) + self.assertEqual(len(self.gather_relevant(self.product_serial, self.stock_location, lot_id=lot3, strict=True).filtered(lambda q: q.lot_id)), 0) + self.assertEqual(len(self.gather_relevant(self.product_serial, self.stock_location, lot_id=lot4, strict=True).filtered(lambda q: q.lot_id)), 0) move1.move_line_ids.write({'qty_done': 1.0}) @@ -678,7 +678,7 @@ class StockMove(SavepointCase): self.assertEqual(move1.reserved_availability, 0.0) self.assertEqual(len(move1.move_line_ids), 0) self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product_serial, self.stock_location, strict=True), 1.0) - self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product_serial, self.stock_location, lot_id=lot1, strict=True), 1.0) + self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product_serial, self.stock_location, lot_id=lot1, strict=True), 2.0) def test_putaway_1(self): """ Receive products from a supplier. Check that putaway rules are rightly applied on diff --git a/addons/stock/tests/test_quant.py b/addons/stock/tests/test_quant.py index 776a9a542b17..67ff098c9a0a 100644 --- a/addons/stock/tests/test_quant.py +++ b/addons/stock/tests/test_quant.py @@ -462,26 +462,26 @@ class StockQuant(SavepointCase): self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product_serial, self.stock_location), 2.0) self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product_serial, self.stock_location, strict=True), 1.0) - self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product_serial, self.stock_location, lot_id=lot1), 1.0) + self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product_serial, self.stock_location, lot_id=lot1), 2.0) self.env['stock.quant']._update_reserved_quantity(self.product_serial, self.stock_location, 1.0, lot_id=lot1, strict=True) self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product_serial, self.stock_location), 1.0) self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product_serial, self.stock_location, strict=True), 1.0) - self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product_serial, self.stock_location, lot_id=lot1), 0.0) + self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product_serial, self.stock_location, lot_id=lot1), 1.0) self.env['stock.quant']._update_reserved_quantity(self.product_serial, self.stock_location, -1.0, lot_id=lot1, strict=True) self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product_serial, self.stock_location), 2.0) self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product_serial, self.stock_location, strict=True), 1.0) - self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product_serial, self.stock_location, lot_id=lot1), 1.0) + self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product_serial, self.stock_location, lot_id=lot1), 2.0) with self.assertRaises(UserError): self.env['stock.quant']._update_reserved_quantity(self.product_serial, self.stock_location, -1.0, strict=True) self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product_serial, self.stock_location), 2.0) self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product_serial, self.stock_location, strict=True), 1.0) - self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product_serial, self.stock_location, lot_id=lot1), 1.0) + self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product_serial, self.stock_location, lot_id=lot1), 2.0) def test_access_rights_1(self): """ Directly update the quant with a user with or without stock access rights sould raise -- GitLab