mirror of
https://github.com/OCA/stock-logistics-warehouse.git
synced 2025-01-21 14:27:28 +02:00
@@ -70,7 +70,25 @@ class StockLocation(models.Model):
|
|||||||
@api.depends("product_restriction")
|
@api.depends("product_restriction")
|
||||||
def _compute_restriction_violation(self):
|
def _compute_restriction_violation(self):
|
||||||
records = 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"]
|
ProductProduct = self.env["product.product"]
|
||||||
|
precision_digits = max(
|
||||||
|
6, self.sudo().env.ref("product.decimal_product_uom").digits * 2
|
||||||
|
)
|
||||||
SQL = """
|
SQL = """
|
||||||
SELECT
|
SELECT
|
||||||
stock_quant.location_id,
|
stock_quant.location_id,
|
||||||
@@ -82,6 +100,11 @@ class StockLocation(models.Model):
|
|||||||
stock_quant.location_id in %s
|
stock_quant.location_id in %s
|
||||||
and stock_location.id = stock_quant.location_id
|
and stock_location.id = stock_quant.location_id
|
||||||
and stock_location.product_restriction = 'same'
|
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
|
GROUP BY
|
||||||
stock_quant.location_id
|
stock_quant.location_id
|
||||||
HAVING count(distinct(product_id)) > 1
|
HAVING count(distinct(product_id)) > 1
|
||||||
@@ -93,7 +116,9 @@ class StockLocation(models.Model):
|
|||||||
if not ids:
|
if not ids:
|
||||||
product_ids_by_location_id = dict()
|
product_ids_by_location_id = dict()
|
||||||
else:
|
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())
|
product_ids_by_location_id = dict(self.env.cr.fetchall())
|
||||||
for record in self:
|
for record in self:
|
||||||
record_id = record.id
|
record_id = record.id
|
||||||
@@ -105,18 +130,37 @@ class StockLocation(models.Model):
|
|||||||
has_restriction_violation = True
|
has_restriction_violation = True
|
||||||
restriction_violation_message = _(
|
restriction_violation_message = _(
|
||||||
"This location should only contain items of the same "
|
"This location should only contain items of the same "
|
||||||
"product but it contains items of products {products}"
|
"product but it contains items of products %(products)s",
|
||||||
).format(products=" | ".join(products.mapped("name")))
|
products=" | ".join(products.mapped("name")),
|
||||||
|
)
|
||||||
record.has_restriction_violation = has_restriction_violation
|
record.has_restriction_violation = has_restriction_violation
|
||||||
record.restriction_violation_message = restriction_violation_message
|
record.restriction_violation_message = restriction_violation_message
|
||||||
|
|
||||||
def _search_has_restriction_violation(self, operator, value):
|
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 = (
|
search_has_violation = (
|
||||||
# has_restriction_violation != False
|
# has_restriction_violation != False
|
||||||
(operator in NEGATIVE_TERM_OPERATORS and not value)
|
(operator in NEGATIVE_TERM_OPERATORS and not value)
|
||||||
# has_restriction_violation = True
|
# has_restriction_violation = True
|
||||||
or (operator not in NEGATIVE_TERM_OPERATORS and value)
|
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 = """
|
SQL = """
|
||||||
SELECT
|
SELECT
|
||||||
stock_quant.location_id
|
stock_quant.location_id
|
||||||
@@ -126,11 +170,23 @@ class StockLocation(models.Model):
|
|||||||
WHERE
|
WHERE
|
||||||
stock_location.id = stock_quant.location_id
|
stock_location.id = stock_quant.location_id
|
||||||
and stock_location.product_restriction = 'same'
|
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
|
GROUP BY
|
||||||
stock_quant.location_id
|
stock_quant.location_id
|
||||||
HAVING count(distinct(product_id)) > 1
|
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()]
|
violation_ids = [r[0] for r in self.env.cr.fetchall()]
|
||||||
if search_has_violation:
|
if search_has_violation:
|
||||||
op = "in"
|
op = "in"
|
||||||
|
|||||||
@@ -35,15 +35,27 @@ class StockQuant(models.Model):
|
|||||||
products = ProductProduct.browse(list(product_ids))
|
products = ProductProduct.browse(list(product_ids))
|
||||||
error_msgs.append(
|
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 "
|
"product. You plan to put different products into "
|
||||||
"this location. ({products})"
|
"this location. (%(products)s)",
|
||||||
).format(
|
|
||||||
location=location.name,
|
location=location.name,
|
||||||
products=", ".join(products.mapped("name")),
|
products=", ".join(products.mapped("name")),
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
# Get existing product already in the locations
|
# 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 = """
|
SQL = """
|
||||||
SELECT
|
SELECT
|
||||||
location_id,
|
location_id,
|
||||||
@@ -52,10 +64,23 @@ class StockQuant(models.Model):
|
|||||||
stock_quant
|
stock_quant
|
||||||
WHERE
|
WHERE
|
||||||
location_id in %s
|
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
|
GROUP BY
|
||||||
location_id
|
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())
|
existing_product_ids_by_location_id = dict(self.env.cr.fetchall())
|
||||||
|
|
||||||
for (
|
for (
|
||||||
@@ -69,12 +94,12 @@ class StockQuant(models.Model):
|
|||||||
to_move_products = ProductProduct.browse(list(product_ids_to_add))
|
to_move_products = ProductProduct.browse(list(product_ids_to_add))
|
||||||
error_msgs.append(
|
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 "
|
"but the location must only contain items of same "
|
||||||
"product and already contains items of other "
|
"product and already contains items of other "
|
||||||
"product(s) "
|
"product(s) "
|
||||||
"({existing_products})."
|
"(%(existing_products)s).",
|
||||||
).format(
|
|
||||||
product=" | ".join(to_move_products.mapped("name")),
|
product=" | ".join(to_move_products.mapped("name")),
|
||||||
location=location.name,
|
location=location.name,
|
||||||
existing_products=" | ".join(existing_products.mapped("name")),
|
existing_products=" | ".join(existing_products.mapped("name")),
|
||||||
|
|||||||
@@ -38,7 +38,7 @@ class TestStockLocation(TransactionCase):
|
|||||||
|
|
||||||
# quants
|
# quants
|
||||||
StockQuant = cls.env["stock.quant"]
|
StockQuant = cls.env["stock.quant"]
|
||||||
StockQuant.create(
|
cls.quant_1_lvl_1_1_1 = StockQuant.create(
|
||||||
{
|
{
|
||||||
"product_id": cls.product_1.id,
|
"product_id": cls.product_1.id,
|
||||||
"location_id": cls.loc_lvl_1_1_1.id,
|
"location_id": cls.loc_lvl_1_1_1.id,
|
||||||
@@ -46,7 +46,7 @@ class TestStockLocation(TransactionCase):
|
|||||||
"owner_id": cls.env.user.id,
|
"owner_id": cls.env.user.id,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
StockQuant.create(
|
cls.quant_2_lvl_1_1_1 = StockQuant.create(
|
||||||
{
|
{
|
||||||
"product_id": cls.product_2.id,
|
"product_id": cls.product_2.id,
|
||||||
"location_id": cls.loc_lvl_1_1_1.id,
|
"location_id": cls.loc_lvl_1_1_1.id,
|
||||||
@@ -54,7 +54,7 @@ class TestStockLocation(TransactionCase):
|
|||||||
"owner_id": cls.env.user.id,
|
"owner_id": cls.env.user.id,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
StockQuant.create(
|
cls.quant_1_lvl_1_1_2 = StockQuant.create(
|
||||||
{
|
{
|
||||||
"product_id": cls.product_1.id,
|
"product_id": cls.product_1.id,
|
||||||
"location_id": cls.loc_lvl_1_1_2.id,
|
"location_id": cls.loc_lvl_1_1_2.id,
|
||||||
@@ -62,7 +62,7 @@ class TestStockLocation(TransactionCase):
|
|||||||
"owner_id": cls.env.user.id,
|
"owner_id": cls.env.user.id,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
StockQuant.create(
|
cls.quant_2_lvl_1_1_2 = StockQuant.create(
|
||||||
{
|
{
|
||||||
"product_id": cls.product_2.id,
|
"product_id": cls.product_2.id,
|
||||||
"location_id": cls.loc_lvl_1_1_2.id,
|
"location_id": cls.loc_lvl_1_1_2.id,
|
||||||
@@ -262,3 +262,24 @@ class TestStockLocation(TransactionCase):
|
|||||||
# Check location creation
|
# Check location creation
|
||||||
with Form(self.StockLocation) as location_form:
|
with Form(self.StockLocation) as location_form:
|
||||||
location_form.name = "Test"
|
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)
|
||||||
|
|||||||
@@ -93,6 +93,9 @@ class TestStockMove(TransactionCase):
|
|||||||
.mapped("product_id")
|
.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):
|
def _create_and_assign_picking(self, short_move_infos, location_dest=None):
|
||||||
location_dest = location_dest or self.location_1
|
location_dest = location_dest or self.location_1
|
||||||
picking_in = self.StockPicking.create(
|
picking_in = self.StockPicking.create(
|
||||||
@@ -323,3 +326,36 @@ class TestStockMove(TransactionCase):
|
|||||||
picking.move_line_ids.location_dest_id = self.location_1
|
picking.move_line_ids.location_dest_id = self.location_1
|
||||||
with self.assertRaises(ValidationError):
|
with self.assertRaises(ValidationError):
|
||||||
self._process_picking(picking)
|
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)
|
||||||
|
|||||||
Reference in New Issue
Block a user