Skip to content

Commit

Permalink
[IMP] stock_storage_type: Allow to restrict some putaway sequence
Browse files Browse the repository at this point in the history
fixes OCA#343
  • Loading branch information
lmignon authored and alexandregaldeano committed Jan 17, 2025
1 parent 6e6e47b commit 179b66f
Show file tree
Hide file tree
Showing 11 changed files with 318 additions and 0 deletions.
1 change: 1 addition & 0 deletions stock_storage_type/__manifest__.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
"views/stock_package_storage_type.xml",
"views/stock_quant_package.xml",
"views/stock_storage_location_sequence.xml",
"views/stock_storage_location_sequence_cond.xml",
"views/storage_type_menus.xml",
],
"demo": [
Expand Down
1 change: 1 addition & 0 deletions stock_storage_type/models/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,4 @@
from . import stock_quant
from . import stock_quant_package
from . import stock_storage_location_sequence
from . import stock_storage_location_sequence_cond
2 changes: 2 additions & 0 deletions stock_storage_type/models/stock_location.py
Original file line number Diff line number Diff line change
Expand Up @@ -356,6 +356,8 @@ def _get_pack_putaway_strategy(self, putaway_location, quant, product):
return dest_location

for package_sequence in package_locations:
if not package_sequence.can_be_applied(putaway_location, quant, product):
continue
pref_loc = package_sequence.location_id
storage_locations = pref_loc.get_storage_locations(products=product)
_logger.debug("Storage locations selected: %s" % storage_locations)
Expand Down
13 changes: 13 additions & 0 deletions stock_storage_type/models/stock_storage_location_sequence.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,11 @@ class StockStorageLocationSequence(models.Model):
location_putaway_strategy = fields.Selection(
related="location_id.pack_putaway_strategy"
)
location_sequence_cond_ids = fields.Many2many(
string="Conditions",
comodel_name="stock.storage.location.sequence.cond",
relation="stock_location_sequence_cond_rel",
)

def _format_package_storage_type_message(self, last=False):
self.ensure_one()
Expand Down Expand Up @@ -84,3 +89,11 @@ def button_show_locations(self):
),
]
return action

def can_be_applied(self, putaway_location, quant, product):
"""Check if conditions are met."""
self.ensure_one()
for cond in self.location_sequence_cond_ids:
if not cond.evaluate(self, putaway_location, quant, product):
return False
return True
147 changes: 147 additions & 0 deletions stock_storage_type/models/stock_storage_location_sequence_cond.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
# Copyright 2022 ACSONE SA/NV
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
import logging
import textwrap

from odoo import _, api, exceptions, fields, models
from odoo.tools import safe_eval

_logger = logging.getLogger(__name__)


class StockStorageLocationSequenceCond(models.Model):

_name = "stock.storage.location.sequence.cond"
_description = "Stock Storage Location Sequence Condition"

name = fields.Char(required=True)

condition_type = fields.Selection(
selection=[("code", "Execute code")], default="code", required=True
)
code_snippet = fields.Text(required=True)
code_snippet_docs = fields.Text(
compute="_compute_code_snippet_docs",
default=lambda self: self._default_code_snippet_docs(),
)

active = fields.Boolean(
default=True,
)

_sql_constraints = [
(
"name",
"EXCLUDE (name WITH =) WHERE (active = True)",
"Stock storage location sequence condition name must be unique",
)
]

def _compute_code_snippet_docs(self):
for rec in self:
rec.code_snippet_docs = textwrap.dedent(rec._default_code_snippet_docs())

@api.constrains("condition_type", "code_snippet")
def _check_condition_type_code(self):
for rec in self.filtered(lambda c: c.condition_type == "code"):
if not rec._code_snippet_valued():
raise exceptions.UserError(
_(
"Condition type is set to `Code`: you must provide a piece of code"
)
)

def _default_code_snippet_docs(self):
return """
Available vars:
* storage_location_sequence
* condition
* putaway_location
* quant
* product
* env
* datetime
* dateutil
* time
* user
* exceptions
Must initialize a boolean 'result' variable set to True when condition is met
"""

def _get_code_snippet_eval_context(
self, storage_location_sequence, putaway_location, quant, product
):
"""Prepare the context used when evaluating python code
:returns: dict -- evaluation context given to safe_eval
"""
self.ensure_one()
return {
"env": self.env,
"user": self.env.user,
"condition": self,
"putaway_location": putaway_location,
"quant": quant,
"product": product,
"datetime": safe_eval.datetime,
"dateutil": safe_eval.dateutil,
"time": safe_eval.time,
"storage_location_sequence": storage_location_sequence,
"exceptions": safe_eval.wrap_module(
exceptions, ["UserError", "ValidationError"]
),
}

def _exec_code(self, storage_location_sequence, putaway_location, quant, product):
self.ensure_one()
if not self._code_snippet_valued():
return False
eval_ctx = self._get_code_snippet_eval_context(
storage_location_sequence, putaway_location, quant, product
)
snippet = self.code_snippet
safe_eval.safe_eval(snippet, eval_ctx, mode="exec", nocopy=True)
result = eval_ctx.get("result")
if not isinstance(result, bool):
raise exceptions.UserError(
_("code_snippet should return boolean value into `result` variable.")
)
if not result:
_logger.debug(
"Condition %s not met:\n"
"* putaway sequence: %s\n"
"* putaway location: %s\n"
"* quant: %s\n"
"* product: %s\n"
% (
self.name,
storage_location_sequence.id,
putaway_location.name,
quant.id,
product.display_name,
)
)
return result

def _code_snippet_valued(self):
self.ensure_one()
snippet = self.code_snippet or ""
return bool(
[
not line.startswith("#")
for line in (snippet.splitlines())
if line.strip("")
]
)

def evaluate(self, storage_location_sequence, putaway_location, quant, product):
self.ensure_one()
if self.condition_type == "code":
return self._exec_code(
storage_location_sequence, putaway_location, quant, product
)
condition_type = self.condition_type
raise exceptions.UserError(
_(f"Not able to evaluate condition of type {condition_type}")
)
1 change: 1 addition & 0 deletions stock_storage_type/readme/CONTRIBUTORS.rst
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,4 @@
* Guewen Baconnier <guewen.baconnier@camptocamp.com>
* Raphaël Reverdy <raphael.reverdy@akretion.com>
* Jacques-Etienne Baudoux <je@bcim.be>
* Laurent Mignon <laurent.mignon@acsone.eu>
2 changes: 2 additions & 0 deletions stock_storage_type/security/ir.model.access.csv
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,5 @@ access_stock_location_package_storage_type_rel_user,access_stock_location_packag
access_stock_location_package_storage_type_rel_manager,access_stock_location_package_storage_type_rel_manager,model_stock_location_package_storage_type_rel,base.group_user,1,1,1,1
access_stock_storage_location_sequence_user,access_stock_storage_location_sequence_user,model_stock_storage_location_sequence,base.group_user,1,0,0,0
access_stock_storage_location_sequence_manager,access_stock_storage_location_sequence_manager,model_stock_storage_location_sequence,stock.group_stock_manager,1,1,1,1
access_stock_storage_location_sequence_cond_user,access_stock_storage_location_sequence_cond_user,model_stock_storage_location_sequence_cond,base.group_user,1,0,0,0
access_stock_storage_location_sequence_cond_manager,access_stock_storage_location_sequence_cond_manager,model_stock_storage_location_sequence_cond,stock.group_stock_manager,1,1,1,1
73 changes: 73 additions & 0 deletions stock_storage_type/tests/test_storage_type_putaway_strategy.py
Original file line number Diff line number Diff line change
Expand Up @@ -495,3 +495,76 @@ def test_storage_strategy_none_in_sequence_to_fixes(self):
package_level.location_dest_id,
self.cardboxes_bin_4_location,
)

def test_storage_strategy_sequence_condition(self):
"""If a condition is not met on storage location sequence, it's ignored"""
move = self._create_single_move(self.product)
move._assign_picking()
original_location_dest = move.location_dest_id
package = self.env["stock.quant.package"].create(
{"product_packaging_id": self.product_lot_cardbox_product_packaging.id}
)
self._update_qty_in_location(
move.location_id, move.product_id, move.product_qty, package=package
)

# configure a new sequence with none in the parent location
self.cardboxes_package_storage_type.storage_location_sequence_ids.unlink()
self.warehouse.lot_stock_id.pack_putaway_strategy = "none"
self.warehouse.lot_stock_id.location_storage_type_ids = (
self.cardboxes_location_storage_type
)
condition = self.env["stock.storage.location.sequence.cond"].create(
{"name": "Always False", "code_snippet": "result = False"}
)
self.none_sequence = self.env["stock.storage.location.sequence"].create(
{
"package_storage_type_id": self.cardboxes_package_storage_type.id,
"location_id": self.warehouse.lot_stock_id.id,
"sequence": 1,
"location_sequence_cond_ids": [(6, 0, condition.ids)],
}
)
self.env["stock.storage.location.sequence"].create(
{
"package_storage_type_id": self.cardboxes_package_storage_type.id,
"location_id": self.cardboxes_location.id,
"sequence": 2,
}
)

move._action_assign()
move_line = move.move_line_ids
package_level = move_line.package_level_id

self.assertIn(
package_level.location_dest_id,
self.cardboxes_location.child_ids,
"the move line's destination must go into the cardbox location"
" since the the first sequence is ignored due to the False"
" condition on it",
)

# if we update the condition to always be True, reset the
# location_dest on the package_level and reapply the put away strategy
# the move line's destination must be in Stock as we have a 'none'
# strategy the first putaway sequence
condition.code_snippet = "result = True"
package_level.location_dest_id = original_location_dest.id
package_level.recompute_pack_putaway()

self.assertEqual(
package_level.location_dest_id,
self.warehouse.lot_stock_id,
"the move line's destination must stay in Stock as we have"
" a 'none' strategy on it and it is in the sequence",
)

package_level.location_dest_id = self.cardboxes_location
# if we reapply the strategy, it should now apply the ordered
# location of the cardbox location
package_level.recompute_pack_putaway()

self.assertTrue(
package_level.location_dest_id in self.cardboxes_location.child_ids
)
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
/>
<field name="location_id" />
<field name="location_putaway_strategy" />
<field name="location_sequence_cond_ids" widget="many2many_tags" />
<button
string="Show locations"
name="button_show_locations"
Expand Down
70 changes: 70 additions & 0 deletions stock_storage_type/views/stock_storage_location_sequence_cond.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
<?xml version="1.0" encoding="utf-8" ?>
<!-- Copyright 2022 ACSONE SA/NV
License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). -->
<odoo>

<record model="ir.ui.view" id="stock_storage_location_sequence_cond_form_view">
<field
name="name"
>stock.storage.location.sequence.cond.form (in stock_storage_type)</field>
<field name="model">stock.storage.location.sequence.cond</field>
<field name="arch" type="xml">
<form>
<header />
<field name="active" invisible="1" />
<widget
name="web_ribbon"
title="Archived"
bg_color="bg-danger"
attrs="{'invisible': [('active', '=', True)]}"
/>
<sheet>
<div name="title" class="oe_title">
<label for="name" class="oe_edit_only" />
<h1>
<field name="name" />
</h1>
</div>
<group name="main">
<field name="condition_type" />
</group>
<field
name="code_snippet_docs"
attrs="{'invisible': [('condition_type', '!=', 'code')]}"
/>
<field
name="code_snippet"
widget="ace"
attrs="{'invisible': [('condition_type', '!=', 'code')]}"
/>
</sheet>
</form>
</field>
</record>


<record model="ir.ui.view" id="stock_storage_location_sequence_cond_tree_view">
<field
name="name"
>stock.storage.location.sequence.cond.tree (in stock_storage_type)</field>
<field name="model">stock.storage.location.sequence.cond</field>
<field name="arch" type="xml">
<tree>
<field name="name" />
</tree>
</field>
</record>

<record
model="ir.actions.act_window"
id="stock_storage_location_sequence_cond_act_window"
>
<field name="name">Stock Storage Location Sequence Conditions</field>
<field name="res_model">stock.storage.location.sequence.cond</field>
<field name="view_mode">tree,form</field>
<field name="domain">[]</field>
<field name="context">{}</field>
</record>


</odoo>
7 changes: 7 additions & 0 deletions stock_storage_type/views/storage_type_menus.xml
Original file line number Diff line number Diff line change
Expand Up @@ -28,4 +28,11 @@
parent="storage_type_menu"
sequence="12"
/>
<menuitem
id="stock_storage_location_sequence_cond_menu"
action="stock_storage_location_sequence_cond_act_window"
name="Storage Location Sequence Conditions"
parent="storage_type_menu"
sequence="13"
/>
</odoo>

0 comments on commit 179b66f

Please sign in to comment.