Merge PR #1821 into 14.0

Signed-off-by jbaudoux
This commit is contained in:
OCA-git-bot
2024-05-22 09:21:24 +00:00
20 changed files with 358 additions and 18 deletions

View File

@@ -13,13 +13,6 @@ class ProcurementGroup(models.Model):
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

View File

@@ -16,15 +16,18 @@ class StockRule(models.Model):
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."))

View File

@@ -25,7 +25,7 @@ class TestProcurementAutoCreateGroup(TransactionCase):
# 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,
@@ -54,7 +54,7 @@ class TestProcurementAutoCreateGroup(TransactionCase):
}
)
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,
@@ -114,8 +114,12 @@ class TestProcurementAutoCreateGroup(TransactionCase):
}
)
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(
@@ -170,8 +174,10 @@ class TestProcurementAutoCreateGroup(TransactionCase):
[("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):
@@ -189,7 +195,7 @@ class TestProcurementAutoCreateGroup(TransactionCase):
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()

View File

@@ -0,0 +1 @@
will be generated by the boat

View File

@@ -0,0 +1 @@
from . import models

View File

@@ -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,
}

View File

@@ -0,0 +1,4 @@
from . import stock_rule
from . import stock_move
from . import procurement_group
from . import product_product

View File

@@ -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)

View File

@@ -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"
)

View File

@@ -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

View File

@@ -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

View File

@@ -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'.

View File

@@ -0,0 +1 @@
* Jacques-Etienne Baudoux (BCIM) <je@bcim.be>

View File

@@ -0,0 +1,2 @@
Allow to have one picking per product by using a procurement group per product
during the procurement run.

View File

@@ -0,0 +1 @@
from . import test_auto_create_by_product

View File

@@ -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()

View File

@@ -0,0 +1,13 @@
<?xml version="1.0" encoding="utf-8" ?>
<odoo>
<record id="procurement_group_form_view" model="ir.ui.view">
<field name="name">procurement_auto_create_group_by_product</field>
<field name="model">procurement.group</field>
<field name="inherit_id" ref="stock.procurement_group_form_view" />
<field name="arch" type="xml">
<field name="move_type" position="after">
<field name="product_id" />
</field>
</field>
</record>
</odoo>

View File

@@ -0,0 +1,33 @@
<?xml version="1.0" encoding="utf-8" ?>
<odoo>
<record model="ir.ui.view" id="view_stock_rule_form">
<field
name="name"
>stock.rule.form - procurement_auto_create_group_by_product</field>
<field name="model">stock.rule</field>
<field
name="inherit_id"
ref="procurement_auto_create_group.view_stock_rule_form"
/>
<field name="arch" type="xml">
<xpath
expr="//group[@name='propagation_group']/field[@name='auto_create_group']"
position="after"
>
<field
name="auto_create_group_by_product"
attrs="{'invisible':['|', ('auto_create_group','!=',True),('group_propagation_option','!=','propagate')]}"
/>
</xpath>
<xpath
expr="//group[field[@name='auto']]/field[@name='auto_create_group']"
position="after"
>
<field
name="auto_create_group_by_product"
attrs="{'invisible':['|',('auto_create_group','!=',True),('action','!=','push')]}"
/>
</xpath>
</field>
</record>
</odoo>

View File

@@ -0,0 +1 @@
../../../../procurement_auto_create_group_by_product

View File

@@ -0,0 +1,6 @@
import setuptools
setuptools.setup(
setup_requires=['setuptools-odoo'],
odoo_addon=True,
)