Skip to content

Commit b986e9a

Browse files
lmignonrousseldenis
authored andcommitted
[ADD] stock_location_product_restriction: Prevent to mix different products into the same stock location
1 parent c90b8aa commit b986e9a

19 files changed

+1461
-0
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
==================================
2+
Stock Location Product Restriction
3+
==================================
4+
5+
.. !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
6+
!! This file is generated by oca-gen-addon-readme !!
7+
!! changes will be overwritten. !!
8+
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
9+
10+
.. |badge1| image:: https://img.shields.io/badge/maturity-Beta-yellow.png
11+
:target: https://odoo-community.org/page/development-status
12+
:alt: Beta
13+
.. |badge2| image:: https://img.shields.io/badge/licence-AGPL--3-blue.png
14+
:target: http://www.gnu.org/licenses/agpl-3.0-standalone.html
15+
:alt: License: AGPL-3
16+
.. |badge3| image:: https://img.shields.io/badge/github-OCA%2Fstock--logistics--warehouse-lightgray.png?logo=github
17+
:target: https://github.com/OCA/stock-logistics-warehouse/tree/10.0/stock_location_product_restriction
18+
:alt: OCA/stock-logistics-warehouse
19+
.. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png
20+
:target: https://translation.odoo-community.org/projects/stock-logistics-warehouse-10-0/stock-logistics-warehouse-10-0-stock_location_product_restriction
21+
:alt: Translate me on Weblate
22+
.. |badge5| image:: https://img.shields.io/badge/runbot-Try%20me-875A7B.png
23+
:target: https://runbot.odoo-community.org/runbot/153/10.0
24+
:alt: Try me on Runbot
25+
26+
|badge1| |badge2| |badge3| |badge4| |badge5|
27+
28+
This module extends the functionality of stock to allow you to prevent to put
29+
items of different products into the same stock location.
30+
31+
32+
**Table of contents**
33+
34+
.. contents::
35+
:local:
36+
37+
Usage
38+
=====
39+
40+
By default, Odoo allows you to put items of any product into the same location.
41+
This behaviour remains the one by default once the addon is installed.
42+
Once installed, you can specify at any level of the stock location hierarchy
43+
if you want to restrict the usage of the location to only items of the same
44+
product. This property is inherited by all the children locations while you
45+
don't specify an other specific value on a child location. The constrains only
46+
applies location by location.
47+
48+
Once a location is configured to only contains items of the same product, the
49+
system will prevent you to move items of any others products into a location
50+
that already contains product items. A new filter into the tree view of the
51+
stock locations will also allow you to find all the location where this new
52+
restriction is violated.
53+
54+
Bug Tracker
55+
===========
56+
57+
Bugs are tracked on `GitHub Issues <https://github.com/OCA/stock-logistics-warehouse/issues>`_.
58+
In case of trouble, please check there if your issue has already been reported.
59+
If you spotted it first, help us smashing it by providing a detailed and welcomed
60+
`feedback <https://github.com/OCA/stock-logistics-warehouse/issues/new?body=module:%20stock_location_product_restriction%0Aversion:%2010.0%0A%0A**Steps%20to%20reproduce**%0A-%20...%0A%0A**Current%20behavior**%0A%0A**Expected%20behavior**>`_.
61+
62+
Do not contact contributors directly about support or help with technical issues.
63+
64+
Credits
65+
=======
66+
67+
Authors
68+
~~~~~~~
69+
70+
* ACSONE SA/NV
71+
72+
Contributors
73+
~~~~~~~~~~~~
74+
75+
* Laurent Mignon <laurent.mignon@acsone.eu> (https://www.acsone.eu/)
76+
77+
Other credits
78+
~~~~~~~~~~~~~
79+
80+
The development of this module has been financially supported by:
81+
82+
* ACSONE SA/NV
83+
* Alcyon Benelux
84+
85+
Maintainers
86+
~~~~~~~~~~~
87+
88+
This module is maintained by the OCA.
89+
90+
.. image:: https://odoo-community.org/logo.png
91+
:alt: Odoo Community Association
92+
:target: https://odoo-community.org
93+
94+
OCA, or the Odoo Community Association, is a nonprofit organization whose
95+
mission is to support the collaborative development of Odoo features and
96+
promote its widespread use.
97+
98+
This module is part of the `OCA/stock-logistics-warehouse <https://github.com/OCA/stock-logistics-warehouse/tree/10.0/stock_location_product_restriction>`_ project on GitHub.
99+
100+
You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
from . import models
2+
from .hooks import pre_init_hook
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
# -*- coding: utf-8 -*-
2+
# Copyright 2020 ACSONE SA/NV
3+
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
4+
5+
{
6+
"name": "Stock Location Product Restriction",
7+
"summary": """
8+
Prevent to mix different products into the same stock location""",
9+
"version": "10.0.1.0.0",
10+
"license": "AGPL-3",
11+
"author": "ACSONE SA/NV,Odoo Community Association (OCA)",
12+
"website": "https://acsone.eu/",
13+
"depends": ["stock"],
14+
"data": ["views/stock_location.xml"],
15+
"demo": [],
16+
"pre_init_hook": "pre_init_hook",
17+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
# -*- coding: utf-8 -*-
2+
# Copyright 2020 ACSONE SA/NV
3+
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html)
4+
5+
import logging
6+
7+
_logger = logging.getLogger(__name__)
8+
9+
10+
def column_exists(cr, tablename, columnname):
11+
""" Return whether the given column exists. """
12+
query = """ SELECT 1 FROM information_schema.columns
13+
WHERE table_name=%s AND column_name=%s """
14+
cr.execute(query, (tablename, columnname))
15+
return cr.rowcount
16+
17+
18+
def pre_init_hook(cr):
19+
_logger.info("Initialize product_restriction on table stock_location")
20+
if not column_exists(cr, "stock_location", "product_restriction"):
21+
cr.execute(
22+
"""
23+
ALTER TABLE stock_location
24+
ADD COLUMN product_restriction character varying;
25+
ALTER TABLE stock_location
26+
ADD COLUMN parent_product_restriction character varying;
27+
"""
28+
)
29+
cr.execute(
30+
"""
31+
UPDATE stock_location set product_restriction = 'any';
32+
UPDATE stock_location set parent_product_restriction = 'any'
33+
where location_id is not null;
34+
"""
35+
)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
from . import stock_location
2+
from . import stock_move
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,141 @@
1+
# -*- coding: utf-8 -*-
2+
# Copyright 2020 ACSONE SA/NV
3+
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
4+
5+
from odoo import api, fields, models, _
6+
from odoo.osv.expression import NEGATIVE_TERM_OPERATORS
7+
8+
9+
class StockLocation(models.Model):
10+
11+
_inherit = "stock.location"
12+
13+
product_restriction = fields.Selection(
14+
string="Product restriction",
15+
selection="_selection_product_restriction",
16+
help="If 'Same product' is selected the system will prevent to put "
17+
"items of different products into the same location.",
18+
index=True,
19+
required=True,
20+
compute="_compute_product_restriction",
21+
store=True,
22+
default="any",
23+
)
24+
25+
specific_product_restriction = fields.Selection(
26+
string="Specific product restriction",
27+
selection="_selection_product_restriction",
28+
help="If specified the restriction specified will apply to "
29+
"the current location and all its children",
30+
default=False,
31+
)
32+
33+
parent_product_restriction = fields.Selection(
34+
selection="_selection_product_restriction",
35+
store=True,
36+
readonly=True,
37+
related="location_id.product_restriction",
38+
)
39+
40+
has_restriction_violation = fields.Boolean(
41+
compute="_compute_restriction_violation",
42+
search="_search_has_restriction_violation",
43+
)
44+
45+
restriction_violation_message = fields.Char(
46+
compute="_compute_restriction_violation"
47+
)
48+
49+
@api.model
50+
def _selection_product_restriction(self):
51+
return [
52+
("any", "Items of any products are allowed into the location"),
53+
(
54+
"same",
55+
"Only items of the same product allowed into the location",
56+
),
57+
]
58+
59+
@api.depends("specific_product_restriction", "parent_product_restriction")
60+
def _compute_product_restriction(self):
61+
default_value = "any"
62+
for rec in self:
63+
rec.product_restriction = (
64+
rec.specific_product_restriction
65+
or rec.parent_product_restriction
66+
or default_value
67+
)
68+
69+
@api.depends("product_restriction")
70+
def _compute_restriction_violation(self):
71+
records = self
72+
if self.env.in_onchange:
73+
records = self._origin
74+
if not records:
75+
# case where the compute is called from the create form
76+
return
77+
ProductProduct = self.env["product.product"]
78+
SQL = """
79+
SELECT
80+
stock_quant.location_id,
81+
array_agg(distinct(product_id))
82+
FROM
83+
stock_quant,
84+
stock_location
85+
WHERE
86+
stock_quant.location_id in %s
87+
and stock_location.id = stock_quant.location_id
88+
and stock_location.product_restriction = 'same'
89+
GROUP BY
90+
stock_quant.location_id
91+
HAVING count(distinct(product_id)) > 1
92+
"""
93+
self.env.cr.execute(SQL, (tuple(records.ids),))
94+
product_ids_by_location_id = dict(self.env.cr.fetchall())
95+
for record in self:
96+
record_id = record.id
97+
if self.env.in_onchange:
98+
record_id = self._origin.id
99+
has_restriction_violation = False
100+
restriction_violation_message = False
101+
product_ids = product_ids_by_location_id.get(record_id)
102+
if product_ids:
103+
products = ProductProduct.browse(product_ids)
104+
has_restriction_violation = True
105+
restriction_violation_message = _(
106+
"This location should only contain items of the same "
107+
"product but it contains items of products %s"
108+
) % " | ".join(products.mapped("name"))
109+
record.has_restriction_violation = has_restriction_violation
110+
record.restriction_violation_message = (
111+
restriction_violation_message
112+
)
113+
114+
def _search_has_restriction_violation(self, operator, value):
115+
search_has_violation = (
116+
# has_restriction_violation != False
117+
(operator in NEGATIVE_TERM_OPERATORS and not value)
118+
or
119+
# has_restriction_violation = True
120+
(operator not in NEGATIVE_TERM_OPERATORS and value)
121+
)
122+
SQL = """
123+
SELECT
124+
stock_quant.location_id
125+
FROM
126+
stock_quant,
127+
stock_location
128+
WHERE
129+
stock_location.id = stock_quant.location_id
130+
and stock_location.product_restriction = 'same'
131+
GROUP BY
132+
stock_quant.location_id
133+
HAVING count(distinct(product_id)) > 1
134+
"""
135+
self.env.cr.execute(SQL)
136+
violation_ids = [r[0] for r in self.env.cr.fetchall()]
137+
if search_has_violation:
138+
op = "in"
139+
else:
140+
op = "not in"
141+
return [("id", op, violation_ids)]

0 commit comments

Comments
 (0)