-
-
Notifications
You must be signed in to change notification settings - Fork 718
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Signed-off-by jbaudoux
- Loading branch information
Showing
20 changed files
with
358 additions
and
18 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
will be generated by the boat |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
from . import models |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,17 @@ | ||
# Copyright 2023 Jacques-Etienne Baudoux (BCIM) <je@bcim.be> | ||
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). | ||
{ | ||
"name": "Procurement Auto Create Group By Product", | ||
"version": "14.0.1.0.0", | ||
"license": "AGPL-3", | ||
"summary": "Generate one picking per product on the procurement run.", | ||
"author": "BCIM, Odoo Community Association (OCA)", | ||
"website": "https://github.com/OCA/stock-logistics-warehouse", | ||
"category": "Warehouse", | ||
"depends": ["procurement_auto_create_group"], | ||
"data": [ | ||
"views/stock_rule.xml", | ||
"views/procurement_group.xml", | ||
], | ||
"installable": True, | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,4 @@ | ||
from . import stock_rule | ||
from . import stock_move | ||
from . import procurement_group | ||
from . import product_product |
10 changes: 10 additions & 0 deletions
10
procurement_auto_create_group_by_product/models/procurement_group.py
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,10 @@ | ||
# Copyright 2023 Jacques-Etienne Baudoux (BCIM) <je@bcim.be> | ||
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). | ||
|
||
from odoo import fields, models | ||
|
||
|
||
class ProcurementGroup(models.Model): | ||
_inherit = "procurement.group" | ||
|
||
product_id = fields.Many2one("product.product", index=True) |
12 changes: 12 additions & 0 deletions
12
procurement_auto_create_group_by_product/models/product_product.py
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,12 @@ | ||
# Copyright 2023 Jacques-Etienne Baudoux (BCIM) <je@bcim.be> | ||
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). | ||
|
||
from odoo import fields, models | ||
|
||
|
||
class ProductProduct(models.Model): | ||
_inherit = "product.product" | ||
|
||
auto_create_procurement_group_ids = fields.One2many( | ||
"procurement.group", "product_id" | ||
) |
29 changes: 29 additions & 0 deletions
29
procurement_auto_create_group_by_product/models/stock_move.py
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,29 @@ | ||
# Copyright 2023 Jacques-Etienne Baudoux (BCIM) <je@bcim.be> | ||
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). | ||
|
||
from itertools import groupby | ||
|
||
from odoo import models | ||
|
||
|
||
class StockMove(models.Model): | ||
_inherit = "stock.move" | ||
|
||
def _merge_moves(self, merge_into=False): | ||
sorted_moves_by_rule = sorted(self, key=lambda m: m.rule_id.id) | ||
res_moves = self.browse() | ||
for _rule, move_list in groupby( | ||
sorted_moves_by_rule, key=lambda m: m.rule_id.id | ||
): | ||
moves = self.browse(m.id for m in move_list) | ||
res_moves |= super(StockMove, moves)._merge_moves(merge_into=merge_into) | ||
return res_moves | ||
|
||
def _prepare_merge_moves_distinct_fields(self): | ||
result = super()._prepare_merge_moves_distinct_fields() | ||
if self.rule_id.auto_create_group_by_product: | ||
# Allow to merge moves on a pick operation having different | ||
# deadlines | ||
if "date_deadline" in result: | ||
result.remove("date_deadline") | ||
return result |
52 changes: 52 additions & 0 deletions
52
procurement_auto_create_group_by_product/models/stock_rule.py
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,52 @@ | ||
# Copyright 2023 Jacques-Etienne Baudoux (BCIM) <je@bcim.be> | ||
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). | ||
|
||
import hashlib | ||
import struct | ||
|
||
from odoo import fields, models | ||
|
||
|
||
def pg_advisory_lock(env, int_lock: int): | ||
"""Attempts to acquire a Postgres transactional advisory lock. | ||
Raises an OperationalError LOCK_NOT_AVAILABLE if the lock could not be acquired. | ||
""" | ||
env.cr.execute( | ||
""" | ||
DO $$ | ||
BEGIN | ||
IF NOT pg_try_advisory_xact_lock(%s) THEN | ||
RAISE EXCEPTION USING | ||
MESSAGE = 'Lock not available', | ||
ERRCODE = '55P03'; | ||
END IF; | ||
END $$; | ||
""", | ||
(int_lock), | ||
) | ||
|
||
|
||
class StockRule(models.Model): | ||
_inherit = "stock.rule" | ||
|
||
auto_create_group_by_product = fields.Boolean(string="Procurement Group by Product") | ||
|
||
def _get_auto_procurement_group(self, product): | ||
if self.auto_create_group_by_product: | ||
if product.auto_create_procurement_group_ids: | ||
return fields.first(product.auto_create_procurement_group_ids) | ||
# Make sure that two transactions cannot create a procurement group | ||
# For the same product at the same time. | ||
lock_name = f"product.product,{product.id}-auto-proc-group" | ||
hasher = hashlib.sha1(str(lock_name).encode()) | ||
bigint_lock = struct.unpack("q", hasher.digest()[:8]) | ||
pg_advisory_lock(self.env, bigint_lock) | ||
return super()._get_auto_procurement_group(product) | ||
|
||
def _prepare_auto_procurement_group_data(self, product): | ||
result = super()._prepare_auto_procurement_group_data(product) | ||
if self.auto_create_group_by_product: | ||
result["product_id"] = product.id | ||
result["partner_id"] = False | ||
return result |
10 changes: 10 additions & 0 deletions
10
procurement_auto_create_group_by_product/readme/CONFIGURE.rst
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,10 @@ | ||
#. Go to *Inventory / Configuration / Settings* and check the option | ||
'Multi-Step Routes' and press the 'Save' button. | ||
#. Activate the developer mode. | ||
#. Go to *Inventory / Configuration / Warehouse Management / Routes* | ||
and select the route you want to change. Select the rule you wish | ||
to change, and in case of a Pull rule or Push & Pull rule Select | ||
'Propagation of Procurement Group': 'Propagate'. The checkbox | ||
'Auto-create Procurement Group' will then appear and you can set | ||
it if you want to procurement group to be automatically created. | ||
Activate also the checkbox 'Procurement Group by Product'. |
1 change: 1 addition & 0 deletions
1
procurement_auto_create_group_by_product/readme/CONTRIBUTORS.rst
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
* Jacques-Etienne Baudoux (BCIM) <je@bcim.be> |
2 changes: 2 additions & 0 deletions
2
procurement_auto_create_group_by_product/readme/DESCRIPTION.rst
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
Allow to have one picking per product by using a procurement group per product | ||
during the procurement run. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
from . import test_auto_create_by_product |
145 changes: 145 additions & 0 deletions
145
procurement_auto_create_group_by_product/tests/test_auto_create_by_product.py
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,145 @@ | ||
# Copyright 2023 Jacques-Etienne Baudoux (BCIM) <je@bcim.be> | ||
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). | ||
|
||
import psycopg2 | ||
|
||
from odoo import api, registry, tools | ||
from odoo.service.model import PG_CONCURRENCY_ERRORS_TO_RETRY | ||
|
||
from odoo.addons.procurement_auto_create_group.tests.test_auto_create import ( | ||
TestProcurementAutoCreateGroup, | ||
) | ||
|
||
|
||
class TestProcurementAutoCreateGroupByProduct(TestProcurementAutoCreateGroup): | ||
def test_pull_push_auto_create_group_not_by_product(self): | ||
"""Test pull flow that without option to group by product""" | ||
self.pull_push_rule_auto.auto_create_group_by_product = False | ||
# Behavior should be the same | ||
super( | ||
TestProcurementAutoCreateGroupByProduct, self | ||
).test_02_pull_push_auto_create_group() | ||
|
||
def test_pull_push_auto_create_group_by_product(self): | ||
"""Test pull flow that with option to group by product""" | ||
self.pull_push_rule_auto.auto_create_group_by_product = True | ||
move = self.move_obj.search([("product_id", "=", self.prod_auto_pull_push.id)]) | ||
self.assertFalse(move) | ||
group = self.group_obj.search( | ||
[("product_id", "=", self.prod_auto_pull_push.id)] | ||
) | ||
self.assertFalse(move) | ||
self._procure(self.prod_auto_pull_push) | ||
move = self.move_obj.search([("product_id", "=", self.prod_auto_pull_push.id)]) | ||
self.assertTrue(move) | ||
self.assertTrue(move.group_id, "Procurement Group not assigned.") | ||
self.assertEqual( | ||
move.group_id.product_id, | ||
self.prod_auto_pull_push, | ||
"Procurement Group product missing.", | ||
) | ||
self.assertEqual( | ||
move.product_uom_qty, | ||
5.0, | ||
"Move invalid quantity.", | ||
) | ||
self._procure(self.prod_auto_pull_push) | ||
group = self.group_obj.search( | ||
[("product_id", "=", self.prod_auto_pull_push.id)] | ||
) | ||
self.assertEqual( | ||
len(group), | ||
1, | ||
"Procurement Group per product should be unique.", | ||
) | ||
# The second move should be merged with the previous one | ||
self.assertEqual( | ||
move.product_uom_qty, | ||
10.0, | ||
"Move invalid quantity.", | ||
) | ||
|
||
def test_push_auto_create_group_not_by_product(self): | ||
"""Test push flow that without option to group by product""" | ||
self.push_rule_auto.auto_create_group_by_product = False | ||
super( | ||
TestProcurementAutoCreateGroupByProduct, self | ||
).test_05_push_auto_create_group() | ||
|
||
def test_push_auto_create_group_by_product(self): | ||
"""Test push flow that with option to group by product""" | ||
self.push_rule_auto.auto_create_group_by_product = True | ||
move = self.move_obj.search( | ||
[ | ||
("product_id", "=", self.prod_auto_push.id), | ||
("location_dest_id", "=", self.loc_components.id), | ||
] | ||
) | ||
self.assertFalse(move) | ||
self._push_trigger(self.prod_auto_push) | ||
move = self.move_obj.search( | ||
[ | ||
("product_id", "=", self.prod_auto_push.id), | ||
("location_dest_id", "=", self.loc_components.id), | ||
] | ||
) | ||
self.assertTrue(move) | ||
self.assertTrue(move.group_id, "Procurement Group not assigned.") | ||
self.assertEqual( | ||
move.group_id.product_id, | ||
self.prod_auto_push, | ||
"Procurement Group product missing.", | ||
) | ||
self._push_trigger(self.prod_auto_push) | ||
group = self.group_obj.search([("product_id", "=", self.prod_auto_push.id)]) | ||
self.assertEqual( | ||
len(group), | ||
1, | ||
"Procurement Group per product should be unique.", | ||
) | ||
move = self.move_obj.search( | ||
[ | ||
("product_id", "=", self.prod_auto_push.id), | ||
("location_dest_id", "=", self.loc_components.id), | ||
] | ||
) | ||
self.assertEqual( | ||
len(move), | ||
1, | ||
"Invalid amount of moves.", | ||
) | ||
self.assertEqual( | ||
move.group_id.product_id, | ||
self.prod_auto_push, | ||
"Procurement Group product missing.", | ||
) | ||
|
||
def test_concurrent_procurement_group_creation(self): | ||
"""Check for the same product, no multiple procurement groups are created.""" | ||
rule = self.pull_push_rule_auto | ||
rule.auto_create_group_by_product = True | ||
product = self.prod_auto_pull_push | ||
# Check that no procurement group exist for the product | ||
self.assertFalse(product.auto_create_procurement_group_ids) | ||
# So create one and an adisory lock will be created | ||
rule._get_auto_procurement_group(product) | ||
self.assertTrue(product.auto_create_procurement_group_ids) | ||
# Use another transaction to test the advisory lock | ||
with registry(self.env.cr.dbname).cursor() as new_cr: | ||
new_env = api.Environment(new_cr, self.env.uid, self.env.context) | ||
new_env["product.product"].invalidate_cache( | ||
["auto_create_procurement_group_ids"], | ||
[ | ||
product.id, | ||
], | ||
) | ||
rule2 = new_env["stock.rule"].browse(rule.id) | ||
rule2.auto_create_group_by_product = True | ||
product2 = new_env["product.product"].browse(product.id) | ||
self.assertFalse(product2.auto_create_procurement_group_ids) | ||
with self.assertRaises(psycopg2.OperationalError) as cm, tools.mute_logger( | ||
"odoo.sql_db" | ||
): | ||
rule2._get_auto_procurement_group(product2) | ||
self.assertTrue(cm.exception.pgcode in PG_CONCURRENCY_ERRORS_TO_RETRY) | ||
new_cr.rollback() |
Oops, something went wrong.