Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion stock_location_product_restriction/__manifest__.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
"author": "ACSONE SA/NV,Odoo Community Association (OCA)",
"maintainers": ["lmignon", "rousseldenis"],
"website": "https://github.com/OCA/stock-logistics-warehouse",
"depends": ["stock"],
"depends": ["stock", "stock_location_fill_state"],
"data": ["views/stock_location.xml"],
"pre_init_hook": "pre_init_hook",
}
110 changes: 79 additions & 31 deletions stock_location_product_restriction/models/stock_location.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,36 @@ class StockLocation(models.Model):
recursive=True,
)

@api.model
def _get_product_restriction_query(self):
SQL = """
SELECT
stock_quant.location_id,
array_agg(distinct(product_id))
FROM
stock_quant,
stock_location
WHERE
stock_quant.location_id in %s
and stock_location.id = stock_quant.location_id
and stock_location.product_restriction = 'same'
and stock_location.fill_state NOT IN ('being_filled', 'being_emptied')
/* 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
"""
return SQL

@api.model
def _get_product_restriction_query_having(self):
return """
HAVING count(distinct(product_id)) > 1
"""

@api.model
def _selection_product_restriction(self):
return [
Expand All @@ -67,9 +97,19 @@ def _compute_product_restriction(self):
or default_value
)

@api.depends("product_restriction")
def _compute_restriction_violation(self):
records = self
def _check_has_location_product_restriction(self, product):
"""
Call this if you want to check if product can
enter a location.
"""
product_ids_by_location_id = self._get_product_ids_by_locations()
for location in self:
product_ids = product_ids_by_location_id.get(location.id)
if product_ids and product.id not in product_ids:
return True
return False

def _get_product_ids_by_locations(self):
self.env["stock.quant"].flush_model(
[
"product_id",
Expand All @@ -80,35 +120,46 @@ def _compute_restriction_violation(self):
"inventory_quantity",
]
)
self.flush_model(
self.flush_model(["product_restriction", "fill_state"])

precision_digits = max(
6, self.sudo().env.ref("product.decimal_product_uom").digits * 2
)
SQL = SQL = self._get_product_restriction_query()
# Browse only real record ids
ids = tuple(
[record.id for record in self if not isinstance(record.id, fields.NewId)]
)
if not ids:
product_ids_by_location_id = dict()
else:
self.env.cr.execute(
SQL, (ids, precision_digits, precision_digits, precision_digits)
)
product_ids_by_location_id = dict(self.env.cr.fetchall())
return product_ids_by_location_id

@api.depends("product_restriction")
def _compute_restriction_violation(self):
records = self
self.env["stock.quant"].flush_model(
[
"product_restriction",
"product_id",
"location_id",
"quantity",
"reserved_quantity",
"available_quantity",
"inventory_quantity",
]
)
self.flush_model(["product_restriction", "fill_state"])
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,
array_agg(distinct(product_id))
FROM
stock_quant,
stock_location
WHERE
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
"""

SQL = self._get_product_restriction_query()
SQL += self._get_product_restriction_query_having()
# Browse only real record ids
ids = tuple(
[record.id for record in records if not isinstance(record.id, fields.NewId)]
Expand All @@ -123,7 +174,7 @@ def _compute_restriction_violation(self):
for record in self:
record_id = record.id
has_restriction_violation = False
restriction_violation_message = False
restriction_violation_message = ""
product_ids = product_ids_by_location_id.get(record_id)
if product_ids:
products = ProductProduct.browse(product_ids)
Expand Down Expand Up @@ -156,11 +207,7 @@ def _search_has_restriction_violation(self, operator, value):
"inventory_quantity",
]
)
self.flush_model(
[
"product_restriction",
]
)
self.flush_model(["product_restriction", "fill_state"])
SQL = """
SELECT
stock_quant.location_id
Expand All @@ -170,6 +217,7 @@ def _search_has_restriction_violation(self, operator, value):
WHERE
stock_location.id = stock_quant.location_id
and stock_location.product_restriction = 'same'
and stock_location.fill_state NOT IN ('being_filled', 'being_emptied')
/* 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
Expand Down
62 changes: 57 additions & 5 deletions stock_location_product_restriction/models/stock_quant.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,54 @@
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
from collections import defaultdict

from odoo import _, api, models
from odoo import _, api, fields, models
from odoo.exceptions import ValidationError


class StockQuant(models.Model):

_inherit = "stock.quant"

being_filled_before_done_location_ids = fields.One2many(
comodel_name="stock.location",
compute="_compute_being_filled_before_done_location_ids",
help="Technical field to compute locations with their fill state"
"at 'Being Filled' before they'll be filled with an incoming move.",
)

@api.depends_context("being_filled_locations_before_update")
@api.depends("location_id")
def _compute_being_filled_before_done_location_ids(self):
self.being_filled_before_done_location_ids = self.env["stock.location"].browse(
self.env.context.get("being_filled_locations_before_update", [])
)

@api.model
def _update_available_quantity(
self,
product_id,
location_id,
quantity,
lot_id=None,
package_id=None,
owner_id=None,
in_date=None,
):
new_self = self.with_context(
being_filled_locations_before_update=location_id.filtered(
lambda location: location.fill_state == "being_filled"
).ids
)
return super(StockQuant, new_self)._update_available_quantity(
product_id=product_id,
location_id=location_id,
quantity=quantity,
lot_id=lot_id,
package_id=package_id,
owner_id=owner_id,
in_date=in_date,
)

@api.constrains("location_id", "product_id")
def _check_location_product_restriction(self):
"""
Expand All @@ -30,6 +70,8 @@ def _check_location_product_restriction(self):
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 location_id in self.being_filled_before_done_location_ids.ids:
continue
if len(product_ids) > 1:
location = StockLocation.browse(location_id)
products = ProductProduct.browse(list(product_ids))
Expand All @@ -56,21 +98,29 @@ def _check_location_product_restriction(self):
"inventory_quantity",
]
)
self.env["stock.location"].flush_model(
[
"fill_state",
]
)
SQL = """
SELECT
location_id,
stock_quant.location_id,
array_agg(distinct(product_id))
FROM
stock_quant
stock_quant,
stock_location
WHERE
location_id in %s
stock_quant.location_id in %s
and stock_quant.location_id = stock_location.id
and stock_location.fill_state NOT IN ('being_filled', 'being_emptied')
/* 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
stock_quant.location_id
"""
self.env.cr.execute(
SQL,
Expand All @@ -87,6 +137,8 @@ def _check_location_product_restriction(self):
location_id,
existing_product_ids,
) in existing_product_ids_by_location_id.items():
if location_id in self.being_filled_before_done_location_ids.ids:
continue
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)
Expand Down
30 changes: 30 additions & 0 deletions stock_location_product_restriction/tests/test_stock_location.py
Original file line number Diff line number Diff line change
Expand Up @@ -283,3 +283,33 @@ def test_zero_quant(self):
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)

def test_check_product(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)

# Check for product_2
self.assertFalse(
self.loc_lvl_1_1_1._check_has_location_product_restriction(self.product_2)
)
# Check for product_1
self.assertTrue(
self.loc_lvl_1_1_1._check_has_location_product_restriction(self.product_1)
)
Loading