diff --git a/stock_location_product_restriction/models/stock_location.py b/stock_location_product_restriction/models/stock_location.py index 9224f0f96..6e36c2772 100644 --- a/stock_location_product_restriction/models/stock_location.py +++ b/stock_location_product_restriction/models/stock_location.py @@ -70,7 +70,25 @@ class StockLocation(models.Model): @api.depends("product_restriction") def _compute_restriction_violation(self): records = self + self.env["stock.quant"].flush_model( + [ + "product_id", + "location_id", + "quantity", + "reserved_quantity", + "available_quantity", + "inventory_quantity", + ] + ) + self.flush_model( + [ + "product_restriction", + ] + ) ProductProduct = self.env["product.product"] + precision_digits = max( + 6, self.sudo().env.ref("product.decimal_product_uom").digits * 2 + ) SQL = """ SELECT stock_quant.location_id, @@ -82,6 +100,11 @@ class StockLocation(models.Model): stock_quant.location_id in %s and stock_location.id = stock_quant.location_id and stock_location.product_restriction = 'same' + /* Mimic the _unlink_zero_quant() query in Odoo */ + AND (NOT (round(quantity::numeric, %s) = 0 OR quantity IS NULL) + OR NOT round(reserved_quantity::numeric, %s) = 0 + OR NOT (round(inventory_quantity::numeric, %s) = 0 + OR inventory_quantity IS NULL)) GROUP BY stock_quant.location_id HAVING count(distinct(product_id)) > 1 @@ -93,7 +116,9 @@ class StockLocation(models.Model): if not ids: product_ids_by_location_id = dict() else: - self.env.cr.execute(SQL, (ids,)) + self.env.cr.execute( + SQL, (ids, precision_digits, precision_digits, precision_digits) + ) product_ids_by_location_id = dict(self.env.cr.fetchall()) for record in self: record_id = record.id @@ -105,18 +130,37 @@ class StockLocation(models.Model): has_restriction_violation = True restriction_violation_message = _( "This location should only contain items of the same " - "product but it contains items of products {products}" - ).format(products=" | ".join(products.mapped("name"))) + "product but it contains items of products %(products)s", + products=" | ".join(products.mapped("name")), + ) record.has_restriction_violation = has_restriction_violation record.restriction_violation_message = restriction_violation_message def _search_has_restriction_violation(self, operator, value): + precision_digits = max( + 6, self.sudo().env.ref("product.decimal_product_uom").digits * 2 + ) search_has_violation = ( # has_restriction_violation != False (operator in NEGATIVE_TERM_OPERATORS and not value) # has_restriction_violation = True or (operator not in NEGATIVE_TERM_OPERATORS and value) ) + self.env["stock.quant"].flush_model( + [ + "product_id", + "location_id", + "quantity", + "reserved_quantity", + "available_quantity", + "inventory_quantity", + ] + ) + self.flush_model( + [ + "product_restriction", + ] + ) SQL = """ SELECT stock_quant.location_id @@ -126,11 +170,23 @@ class StockLocation(models.Model): WHERE stock_location.id = stock_quant.location_id and stock_location.product_restriction = 'same' + /* Mimic the _unlink_zero_quant() query in Odoo */ + AND (NOT (round(quantity::numeric, %s) = 0 OR quantity IS NULL) + OR NOT round(reserved_quantity::numeric, %s) = 0 + OR NOT (round(inventory_quantity::numeric, %s) = 0 + OR inventory_quantity IS NULL)) GROUP BY stock_quant.location_id HAVING count(distinct(product_id)) > 1 """ - self.env.cr.execute(SQL) + self.env.cr.execute( + SQL, + ( + precision_digits, + precision_digits, + precision_digits, + ), + ) violation_ids = [r[0] for r in self.env.cr.fetchall()] if search_has_violation: op = "in" diff --git a/stock_location_product_restriction/models/stock_quant.py b/stock_location_product_restriction/models/stock_quant.py index 6a991c183..4e02199a8 100644 --- a/stock_location_product_restriction/models/stock_quant.py +++ b/stock_location_product_restriction/models/stock_quant.py @@ -35,15 +35,27 @@ class StockQuant(models.Model): products = ProductProduct.browse(list(product_ids)) error_msgs.append( _( - "The location {location} can only contain items of the same " + "The location %(location)s can only contain items of the same " "product. You plan to put different products into " - "this location. ({products})" - ).format( + "this location. (%(products)s)", location=location.name, products=", ".join(products.mapped("name")), ) ) # Get existing product already in the locations + precision_digits = max( + 6, self.sudo().env.ref("product.decimal_product_uom").digits * 2 + ) + self.flush_model( + [ + "product_id", + "location_id", + "quantity", + "reserved_quantity", + "available_quantity", + "inventory_quantity", + ] + ) SQL = """ SELECT location_id, @@ -52,10 +64,23 @@ class StockQuant(models.Model): stock_quant WHERE location_id in %s + /* Mimic the _unlink_zero_quant() query in Odoo */ + AND (NOT (round(quantity::numeric, %s) = 0 OR quantity IS NULL) + OR NOT round(reserved_quantity::numeric, %s) = 0 + OR NOT (round(inventory_quantity::numeric, %s) = 0 + OR inventory_quantity IS NULL)) GROUP BY location_id """ - self.env.cr.execute(SQL, (tuple(quants_to_check.mapped("location_id").ids),)) + self.env.cr.execute( + SQL, + ( + tuple(quants_to_check.mapped("location_id").ids), + precision_digits, + precision_digits, + precision_digits, + ), + ) existing_product_ids_by_location_id = dict(self.env.cr.fetchall()) for ( @@ -69,12 +94,12 @@ class StockQuant(models.Model): to_move_products = ProductProduct.browse(list(product_ids_to_add)) error_msgs.append( _( - "You plan to add the product {product} into the location {location} " + "You plan to add the product %(product)s into the location" + " %(location)s " "but the location must only contain items of same " "product and already contains items of other " "product(s) " - "({existing_products})." - ).format( + "(%(existing_products)s).", product=" | ".join(to_move_products.mapped("name")), location=location.name, existing_products=" | ".join(existing_products.mapped("name")), diff --git a/stock_location_product_restriction/tests/test_stock_location.py b/stock_location_product_restriction/tests/test_stock_location.py index 39e154191..d822d7344 100644 --- a/stock_location_product_restriction/tests/test_stock_location.py +++ b/stock_location_product_restriction/tests/test_stock_location.py @@ -38,7 +38,7 @@ class TestStockLocation(TransactionCase): # quants StockQuant = cls.env["stock.quant"] - StockQuant.create( + cls.quant_1_lvl_1_1_1 = StockQuant.create( { "product_id": cls.product_1.id, "location_id": cls.loc_lvl_1_1_1.id, @@ -46,7 +46,7 @@ class TestStockLocation(TransactionCase): "owner_id": cls.env.user.id, } ) - StockQuant.create( + cls.quant_2_lvl_1_1_1 = StockQuant.create( { "product_id": cls.product_2.id, "location_id": cls.loc_lvl_1_1_1.id, @@ -54,7 +54,7 @@ class TestStockLocation(TransactionCase): "owner_id": cls.env.user.id, } ) - StockQuant.create( + cls.quant_1_lvl_1_1_2 = StockQuant.create( { "product_id": cls.product_1.id, "location_id": cls.loc_lvl_1_1_2.id, @@ -62,7 +62,7 @@ class TestStockLocation(TransactionCase): "owner_id": cls.env.user.id, } ) - StockQuant.create( + cls.quant_2_lvl_1_1_2 = StockQuant.create( { "product_id": cls.product_2.id, "location_id": cls.loc_lvl_1_1_2.id, @@ -262,3 +262,24 @@ class TestStockLocation(TransactionCase): # Check location creation with Form(self.StockLocation) as location_form: location_form.name = "Test" + + def test_zero_quant(self): + """ + Data: + * Location level_1_1_1 with 2 different products no restriction + Test Case: + 1. Check restriction message + 2. Change product 1 quant to 0.0 + 3. Set restriction 'same' on location level_1_1_1 + 4. Check restriction message + Expected result: + 1. No restriction message + """ + self.loc_lvl_1_1_1.product_restriction = "any" + self.assertFalse(self.loc_lvl_1_1_1.has_restriction_violation) + self.assertFalse(self.loc_lvl_1_1_1.restriction_violation_message) + self.quant_1_lvl_1_1_1.inventory_quantity = 0.0 + self.quant_1_lvl_1_1_1._apply_inventory() + self.loc_lvl_1_1_1.product_restriction = "same" + self.assertFalse(self.loc_lvl_1_1_1.has_restriction_violation) + self.assertFalse(self.loc_lvl_1_1_1.restriction_violation_message) diff --git a/stock_location_product_restriction/tests/test_stock_move.py b/stock_location_product_restriction/tests/test_stock_move.py index bc71cb035..27a7d8a1e 100644 --- a/stock_location_product_restriction/tests/test_stock_move.py +++ b/stock_location_product_restriction/tests/test_stock_move.py @@ -93,6 +93,9 @@ class TestStockMove(TransactionCase): .mapped("product_id") ) + def _get_quants_in_location(self, location): + return self.env["stock.quant"].search([("location_id", "=", location.id)]) + def _create_and_assign_picking(self, short_move_infos, location_dest=None): location_dest = location_dest or self.location_1 picking_in = self.StockPicking.create( @@ -323,3 +326,36 @@ class TestStockMove(TransactionCase): picking.move_line_ids.location_dest_id = self.location_1 with self.assertRaises(ValidationError): self._process_picking(picking) + + def test_location_with_zero_quant(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 + """ + + quants = self._get_quants_in_location(self.location_1) + quants.with_context(inventory_mode=True).write({"inventory_quantity": 0.0}) + quants._apply_inventory() + + 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 + self._process_picking(picking)