From 718a8ee554ca9139aaace24dee3eae781e8867d5 Mon Sep 17 00:00:00 2001 From: Arnold Moyaux 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 closes odoo/odoo#93141 X-original-commit: 83d55d8ed8b8b5a5232e3cdfc09abf5285f18ed7 Signed-off-by: Arnold Moyaux Signed-off-by: Arnold Moyaux (arm) --- addons/stock/models/stock_move_line.py | 31 ++++---------------------- addons/stock/models/stock_quant.py | 14 +++++++----- addons/stock/tests/test_inventory.py | 4 ++-- addons/stock/tests/test_move.py | 10 ++++----- addons/stock/tests/test_quant.py | 8 +++---- addons/stock/tests/test_stock_flow.py | 2 +- 6 files changed, 25 insertions(+), 44 deletions(-) diff --git a/addons/stock/models/stock_move_line.py b/addons/stock/models/stock_move_line.py index dd0635a11f8..2f692feaec3 100644 --- a/addons/stock/models/stock_move_line.py +++ b/addons/stock/models/stock_move_line.py @@ -369,14 +369,7 @@ class StockMoveLine(models.Model): # Unreserve the old charateristics of the move line. if not ml.move_id._should_bypass_reservation(ml.location_id): - try: - Quant._update_reserved_quantity(ml.product_id, ml.location_id, -ml.reserved_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.reserved_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.reserved_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.move_id._should_bypass_reservation(updates.get('location_id', ml.location_id)): @@ -386,14 +379,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_reserved_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_reserved_uom_qty: new_reserved_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 @@ -481,13 +467,7 @@ class StockMoveLine(models.Model): for ml in self: # Unlinking a move line should unreserve. if not float_is_zero(ml.reserved_qty, precision_digits=precision) and not ml.move_id._should_bypass_reservation(ml.location_id): - try: - self.env['stock.quant']._update_reserved_quantity(ml.product_id, ml.location_id, -ml.reserved_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.reserved_qty, lot_id=False, package_id=ml.package_id, owner_id=ml.owner_id, strict=True) - elif not self.env.context.get(MODULE_UNINSTALL_FLAG, False): - raise # pylint: disable=raise-unlink-override + self.env['stock.quant']._update_reserved_quantity(ml.product_id, ml.location_id, -ml.reserved_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: @@ -588,10 +568,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_ids_to_ignore=ml_ids_to_ignore) # unreserve what's been reserved if not ml.move_id._should_bypass_reservation(ml.location_id) and ml.product_id.type == 'product' and ml.reserved_qty: - try: - Quant._update_reserved_quantity(ml.product_id, ml.location_id, -ml.reserved_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.reserved_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.reserved_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 2a62632b91e..995867c5787 100644 --- a/addons/stock/models/stock_quant.py +++ b/addons/stock/models/stock_quant.py @@ -256,6 +256,8 @@ class StockQuant(models.Model): package_id = self.env['stock.quant.package'].browse(vals.get('package_id')) owner_id = self.env['res.partner'].browse(vals.get('owner_id')) quant = self._gather(product, location, lot_id=lot_id, package_id=package_id, owner_id=owner_id, strict=True) + if lot_id: + quant = quant.filtered(lambda q: q.lot_id) if quant: quant = quant[0].sudo() @@ -536,19 +538,19 @@ class StockQuant(models.Model): domain = [('product_id', '=', product_id.id)] 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]) - return self.search(domain, order=removal_strategy_order) + return self.search(domain, order=removal_strategy_order).sorted(lambda q: not q.lot_id) @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): @@ -696,6 +698,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) if location_id.should_bypass_reservation(): incoming_dates = [] @@ -714,7 +718,7 @@ class StockQuant(models.Model): quant = None if quants: # see _acquire_one_job for explanations - self._cr.execute("SELECT id FROM stock_quant WHERE id IN %s LIMIT 1 FOR NO KEY UPDATE SKIP LOCKED", [tuple(quants.ids)]) + self._cr.execute("SELECT id FROM stock_quant WHERE id IN %s ORDER BY lot_id LIMIT 1 FOR NO KEY UPDATE SKIP LOCKED", [tuple(quants.ids)]) stock_quant_result = self._cr.fetchone() if stock_quant_result: quant = self.browse(stock_quant_result[0]) @@ -756,7 +760,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 1c3defee8bd..f62dbf1fed2 100644 --- a/addons/stock/tests/test_inventory.py +++ b/addons/stock/tests/test_inventory.py @@ -155,10 +155,10 @@ class TestInventory(TransactionCase): stock_confirmation_wizard.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 6d344044aa4..1724c5d6480 100644 --- a/addons/stock/tests/test_move.py +++ b/addons/stock/tests/test_move.py @@ -354,10 +354,10 @@ class StockMove(TransactionCase): # 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}) @@ -667,7 +667,7 @@ class StockMove(TransactionCase): 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 d00b31ff01d..b8e50473bf3 100644 --- a/addons/stock/tests/test_quant.py +++ b/addons/stock/tests/test_quant.py @@ -471,26 +471,26 @@ class StockQuant(TransactionCase): 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 should not raise diff --git a/addons/stock/tests/test_stock_flow.py b/addons/stock/tests/test_stock_flow.py index 1bdb886b85b..b299c19bc94 100644 --- a/addons/stock/tests/test_stock_flow.py +++ b/addons/stock/tests/test_stock_flow.py @@ -1124,7 +1124,7 @@ class TestStockFlow(TestStockCommon): self.assertEqual(lotproduct.qty_available, 10, "Wrong qty available for lotproduct") quants = self.StockQuantObj.search([('product_id', '=', lotproduct.id), ('location_id', '=', self.stock_location), ('lot_id', '=', lot1.id)]) total_qty = sum([quant.quantity for quant in quants]) - self.assertEqual(total_qty, 10, 'Expecting 0 units lot of lotproduct, but we got %.4f on location stock!' % (total_qty)) + self.assertEqual(total_qty, 10, 'Expecting 10 units lot of lotproduct, but we got %.4f on location stock!' % (total_qty)) quants = self.StockQuantObj.search([('product_id', '=', lotproduct.id), ('location_id', '=', self.stock_location), ('lot_id', '=', False)]) total_qty = sum([quant.quantity for quant in quants]) self.assertEqual(total_qty, 0, 'Expecting 0 units lot of lotproduct, but we got %.4f on location stock!' % (total_qty))