From bd25f471b39c6fa7f63ad2ea51315ab9abe2ea76 Mon Sep 17 00:00:00 2001 From: Denis Roussel Date: Wed, 26 Jul 2023 11:57:13 +0200 Subject: [PATCH 1/2] [FIX] stock_location_product_restriction: Don't fetch if ids are void --- .../models/stock_location.py | 11 +++++++++-- .../tests/test_stock_location.py | 8 ++++++-- 2 files changed, 15 insertions(+), 4 deletions(-) diff --git a/stock_location_product_restriction/models/stock_location.py b/stock_location_product_restriction/models/stock_location.py index 89371a1db..9224f0f96 100644 --- a/stock_location_product_restriction/models/stock_location.py +++ b/stock_location_product_restriction/models/stock_location.py @@ -86,8 +86,15 @@ class StockLocation(models.Model): stock_quant.location_id HAVING count(distinct(product_id)) > 1 """ - self.env.cr.execute(SQL, (tuple(records.ids),)) - product_ids_by_location_id = dict(self.env.cr.fetchall()) + # Browse only real record ids + ids = tuple( + [record.id for record in records if not isinstance(record.id, fields.NewId)] + ) + if not ids: + product_ids_by_location_id = dict() + else: + self.env.cr.execute(SQL, (ids,)) + product_ids_by_location_id = dict(self.env.cr.fetchall()) for record in self: record_id = record.id has_restriction_violation = False diff --git a/stock_location_product_restriction/tests/test_stock_location.py b/stock_location_product_restriction/tests/test_stock_location.py index a57eddcde..39e154191 100644 --- a/stock_location_product_restriction/tests/test_stock_location.py +++ b/stock_location_product_restriction/tests/test_stock_location.py @@ -1,7 +1,6 @@ # Copyright 2020 ACSONE SA/NV # License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). - -from odoo.tests.common import TransactionCase +from odoo.tests.common import Form, TransactionCase class TestStockLocation(TransactionCase): @@ -258,3 +257,8 @@ class TestStockLocation(TransactionCase): self.loc_lvl_1_1_1.flush_recordset() self.assertTrue(self.loc_lvl_1_1_1.has_restriction_violation) self.assertTrue(self.loc_lvl_1_1_1.restriction_violation_message) + + def test_05(self): + # Check location creation + with Form(self.StockLocation) as location_form: + location_form.name = "Test" From 602879b14a27951d8c43f4a476971b5c07400e59 Mon Sep 17 00:00:00 2001 From: "Laurent Mignon (ACSONE)" Date: Fri, 14 Jun 2024 12:05:42 +0200 Subject: [PATCH 2/2] [FIX] stock_location_product_restriction: Ensure restriction is always checked Transform check at move validation by a constraint on the stock.quant to ensure that the rule is checked in every cases --- stock_location_product_restriction/i18n/fr.po | 12 ++-- .../models/__init__.py | 2 +- .../models/{stock_move.py => stock_quant.py} | 60 ++++++++----------- .../tests/test_stock_move.py | 34 +++++++++++ 4 files changed, 68 insertions(+), 40 deletions(-) rename stock_location_product_restriction/models/{stock_move.py => stock_quant.py} (59%) diff --git a/stock_location_product_restriction/i18n/fr.po b/stock_location_product_restriction/i18n/fr.po index 586882d57..2d3a2a5d5 100644 --- a/stock_location_product_restriction/i18n/fr.po +++ b/stock_location_product_restriction/i18n/fr.po @@ -81,8 +81,9 @@ msgstr "Mouvement de stock" #, python-format msgid "" "The location {location} can only contain items of the same product. You plan " -"to move different products to this location. ({products})" -msgstr "" +"to put different products into this location. ({products})" +msgstr "L'emplacement {location} ne peut contenir que des articles identiques. Vous " +"essayer de mettre d'autres articles dans cet emplacement. ({products}) " #. module: stock_location_product_restriction #. odoo-python @@ -103,10 +104,13 @@ msgstr "Ne respecte pas les règles" #: code:addons/stock_location_product_restriction/models/stock_move.py:0 #, python-format msgid "" -"You plan to move the product {product} to the location {location} but the " +"You plan to add the product {product} into the location {location} but the " "location must only contain items of same product and already contains items " "of other product(s) ({existing_products})." -msgstr "" +msgstr "Vous essayer d'ajouter le produit {product} dans l'emplacemet {location} mais l'" +"emplacement ne peut contenir que des articles identiques et contient déja " +"d'autes articles ({existing_products})" + #~ msgid "Has restriction violation" #~ msgstr "Ne respecte pas les règles" diff --git a/stock_location_product_restriction/models/__init__.py b/stock_location_product_restriction/models/__init__.py index 4d109959e..014fdaabf 100644 --- a/stock_location_product_restriction/models/__init__.py +++ b/stock_location_product_restriction/models/__init__.py @@ -1 +1 @@ -from . import stock_location, stock_move +from . import stock_location, stock_quant diff --git a/stock_location_product_restriction/models/stock_move.py b/stock_location_product_restriction/models/stock_quant.py similarity index 59% rename from stock_location_product_restriction/models/stock_move.py rename to stock_location_product_restriction/models/stock_quant.py index 233571c2e..6a991c183 100644 --- a/stock_location_product_restriction/models/stock_move.py +++ b/stock_location_product_restriction/models/stock_quant.py @@ -1,55 +1,49 @@ -# Copyright 2020 ACSONE SA/NV +# Copyright 2024 ACSONE SA/NV # License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). - from collections import defaultdict -from odoo import _, models +from odoo import _, api, models from odoo.exceptions import ValidationError -class StockMove(models.Model): +class StockQuant(models.Model): - _inherit = "stock.move" + _inherit = "stock.quant" + @api.constrains("location_id", "product_id") def _check_location_product_restriction(self): """ - Check if the move can be executed according to potential restriction + Check if the quant can be put into the location according to restriction defined on the stock_location """ StockLocation = self.env["stock.location"] ProductProduct = self.env["product.product"] - # We only check moves with a location_dest that can + # We only check quants with a location_id that can # only contain the same product - moves_to_ckeck = self.filtered( - lambda m: m.location_dest_id.product_restriction == "same" + quants_to_check = self.filtered( + lambda q: q.location_id.product_restriction == "same" ) - if not moves_to_ckeck: + if not quants_to_check: return - product_ids_location_dest_id = defaultdict(set) + product_ids_location_id = defaultdict(set) error_msgs = [] - # check dest locations into the stock moves - for move in moves_to_ckeck: - product_ids_location_dest_id[move.location_dest_id.id].add( - move.product_id.id - ) - for location_id, product_ids in product_ids_location_dest_id.items(): + for quant in quants_to_check: + product_ids_location_id[quant.location_id.id].add(quant.product_id.id) + for location_id, product_ids in product_ids_location_id.items(): if len(product_ids) > 1: location = StockLocation.browse(location_id) products = ProductProduct.browse(list(product_ids)) error_msgs.append( _( "The location {location} can only contain items of the same " - "product. You plan to move different products to " + "product. You plan to put different products into " "this location. ({products})" ).format( location=location.name, products=", ".join(products.mapped("name")), ) ) - - # check dest locations by taking into account product already into the - # locations - # here we use a plain SQL to avoid performance issue + # Get existing product already in the locations SQL = """ SELECT location_id, @@ -61,22 +55,21 @@ class StockMove(models.Model): GROUP BY location_id """ - self.env.cr.execute( - SQL, (tuple(moves_to_ckeck.mapped("location_dest_id").ids),) - ) + self.env.cr.execute(SQL, (tuple(quants_to_check.mapped("location_id").ids),)) existing_product_ids_by_location_id = dict(self.env.cr.fetchall()) + for ( - location_dest_id, + location_id, existing_product_ids, ) in existing_product_ids_by_location_id.items(): - product_ids_to_move = product_ids_location_dest_id[location_dest_id] - if set(existing_product_ids).symmetric_difference(product_ids_to_move): - location = StockLocation.browse(location_dest_id) + product_ids_to_add = product_ids_location_id[location_id] + if set(existing_product_ids).symmetric_difference(product_ids_to_add): + location = StockLocation.browse(location_id) existing_products = ProductProduct.browse(existing_product_ids) - to_move_products = ProductProduct.browse(list(product_ids_to_move)) + to_move_products = ProductProduct.browse(list(product_ids_to_add)) error_msgs.append( _( - "You plan to move the product {product} to the location {location} " + "You plan to add the product {product} into the location {location} " "but the location must only contain items of same " "product and already contains items of other " "product(s) " @@ -87,9 +80,6 @@ class StockMove(models.Model): existing_products=" | ".join(existing_products.mapped("name")), ) ) + if error_msgs: raise ValidationError("\n".join(error_msgs)) - - def _action_done(self, cancel_backorder=False): - self._check_location_product_restriction() - return super()._action_done(cancel_backorder=cancel_backorder) diff --git a/stock_location_product_restriction/tests/test_stock_move.py b/stock_location_product_restriction/tests/test_stock_move.py index 2573c5da0..bc71cb035 100644 --- a/stock_location_product_restriction/tests/test_stock_move.py +++ b/stock_location_product_restriction/tests/test_stock_move.py @@ -289,3 +289,37 @@ class TestStockMove(TransactionCase): ) with self.assertRaises(ValidationError): self._process_picking(picking) + + def test_06(self): + """ + Data: + location_1 with product_1 but with product restriction = 'same' + a picking with one move: product_2 -> location_1 + Test case: + # set the location dest only on the move line and the parent on the + # move + Process the picking + Expected result: + ValidationError + """ + + self.assertEqual( + self.product_1, + self._get_products_in_location(self.location_1), + ) + self.location_1.specific_product_restriction = "same" + self.location_1.invalidate_recordset() + parent_location = self.location_1.location_id + picking = self._create_and_assign_picking( + [ + ShortMoveInfo( + product=self.product_2, + location_dest=parent_location, + qty=2, + ), + ], + location_dest=parent_location, + ) + picking.move_line_ids.location_dest_id = self.location_1 + with self.assertRaises(ValidationError): + self._process_picking(picking)