Skip to content

Commit

Permalink
Merge PR #1821 into 14.0
Browse files Browse the repository at this point in the history
Signed-off-by jbaudoux
  • Loading branch information
OCA-git-bot committed May 22, 2024
2 parents de4aaf2 + 6572251 commit c521e0b
Show file tree
Hide file tree
Showing 20 changed files with 358 additions and 18 deletions.
11 changes: 2 additions & 9 deletions procurement_auto_create_group/models/procurement_group.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,6 @@ def _get_rule(self, product_id, location_id, values):
rule = super()._get_rule(product_id, location_id, values)
# If there isn't a date planned in the values it means that this
# method has been called outside of a procurement process.
if (
rule
and not values.get("group_id")
and rule.auto_create_group
and values.get("date_planned")
):
group_data = rule._prepare_auto_procurement_group_data()
group = self.env["procurement.group"].create(group_data)
values["group_id"] = group
if rule and rule.auto_create_group and values.get("date_planned"):
values["group_id"] = rule._get_auto_procurement_group(product_id)
return rule
9 changes: 6 additions & 3 deletions procurement_auto_create_group/models/stock_rule.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,15 +16,18 @@ def _onchange_group_propagation_option(self):
if self.group_propagation_option != "propagate":
self.auto_create_group = False

def _get_auto_procurement_group(self, product):
group_data = self._prepare_auto_procurement_group_data(product)
return self.env["procurement.group"].create(group_data)

def _push_prepare_move_copy_values(self, move_to_copy, new_date):
new_move_vals = super()._push_prepare_move_copy_values(move_to_copy, new_date)
if self.auto_create_group:
group_data = self._prepare_auto_procurement_group_data()
group = self.env["procurement.group"].create(group_data)
group = self._get_auto_procurement_group(move_to_copy.product_id)
new_move_vals["group_id"] = group.id
return new_move_vals

def _prepare_auto_procurement_group_data(self):
def _prepare_auto_procurement_group_data(self, product):
name = self.env["ir.sequence"].next_by_code("procurement.group") or False
if not name:
raise UserError(_("No sequence defined for procurement group."))
Expand Down
18 changes: 12 additions & 6 deletions procurement_auto_create_group/tests/test_auto_create.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ def setUp(self):

# Create rules and routes:
pull_push_route_auto = self.route_obj.create({"name": "Auto Create Group"})
self.rule_1 = self.rule_obj.create(
self.pull_push_rule_auto = self.rule_obj.create(
{
"name": "rule with autocreate",
"route_id": pull_push_route_auto.id,
Expand Down Expand Up @@ -54,7 +54,7 @@ def setUp(self):
}
)
push_route_auto = self.route_obj.create({"name": "Auto Create Group"})
self.rule_1 = self.rule_obj.create(
self.push_rule_auto = self.rule_obj.create(
{
"name": "route_auto",
"location_src_id": self.location.id,
Expand Down Expand Up @@ -114,8 +114,12 @@ def setUp(self):
}
)

self.group = self.group_obj.create({"name": "SO0001"})

def _procure(self, product):
values = {}
values = {
"group_id": self.group,
}
self.group_obj.run(
[
self.env["procurement.group"].Procurement(
Expand Down Expand Up @@ -170,8 +174,10 @@ def test_01_pull_push_no_auto_create_group(self):
[("product_id", "=", self.prod_no_auto_pull_push.id)]
)
self.assertTrue(move)
self.assertFalse(
move.group_id, "Procurement Group should not have been assigned."
self.assertEqual(
move.group_id,
self.group,
"Procurement Group should not have been assigned.",
)

def test_02_pull_push_auto_create_group(self):
Expand All @@ -189,7 +195,7 @@ def test_02_pull_push_auto_create_group(self):

def test_03_onchange_method(self):
"""Test onchange method for stock rule."""
proc_rule = self.rule_1
proc_rule = self.push_rule_auto
self.assertTrue(proc_rule.auto_create_group)
proc_rule.write({"group_propagation_option": "none"})
proc_rule._onchange_group_propagation_option()
Expand Down
1 change: 1 addition & 0 deletions procurement_auto_create_group_by_product/README.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
will be generated by the boat
1 change: 1 addition & 0 deletions procurement_auto_create_group_by_product/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
from . import models
17 changes: 17 additions & 0 deletions procurement_auto_create_group_by_product/__manifest__.py
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,
}
4 changes: 4 additions & 0 deletions procurement_auto_create_group_by_product/models/__init__.py
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
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 procurement_auto_create_group_by_product/models/product_product.py
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 procurement_auto_create_group_by_product/models/stock_move.py
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 procurement_auto_create_group_by_product/models/stock_rule.py
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 procurement_auto_create_group_by_product/readme/CONFIGURE.rst
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'.
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
* Jacques-Etienne Baudoux (BCIM) <je@bcim.be>
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.
1 change: 1 addition & 0 deletions procurement_auto_create_group_by_product/tests/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
from . import test_auto_create_by_product
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()
Loading

0 comments on commit c521e0b

Please sign in to comment.