Skip to content

Commit

Permalink
shopfloor: Add option to sort move lines on processing of cluster pic…
Browse files Browse the repository at this point in the history
…king
  • Loading branch information
grindtildeath committed Jan 7, 2025
1 parent cd82181 commit 55fa1e3
Show file tree
Hide file tree
Showing 4 changed files with 179 additions and 11 deletions.
15 changes: 15 additions & 0 deletions shopfloor/migrations/14.0.5.0.0/post-migration.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
# Copyright 2024 Camptocamp SA
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl)
import json

from openupgradelib import openupgrade


@openupgrade.migrate()
def migrate(env, version):
scenario = env.ref("shopfloor.scenario_cluster_picking")
options = scenario.options
options["allow_move_line_processing_sort_order"] = True
scenario.write(
{"options_edit": json.dumps(options or {}, indent=4, sort_keys=True)}
)
62 changes: 62 additions & 0 deletions shopfloor/models/shopfloor_menu.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
# Copyright 2020 Camptocamp SA (http://www.camptocamp.com)
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
from odoo import _, api, exceptions, fields, models
from odoo.tools.safe_eval import test_python_expr

PICK_PACK_SAME_TIME_HELP = """
If you tick this box, while picking goods from a location
Expand Down Expand Up @@ -53,6 +54,30 @@
to scan a destination package.
"""

MOVE_LINE_PROCESSING_SORT_ORDER_HELP = (
"By priority & location:\n"
"> each line is sorted by priority and then by location to perform a smart path in"
" the warehouse.\n"
"By location grouped by product:\n"
"> in case of multiple move lines for the same product, break the sorting to"
" finalize a started product by processing all other move lines for that product"
" in order to group a product on a picking device. When stacking products on a"
" pallet, this prevents to spread a same product at different level on the stack."
)

CUSTOM_CODE_DEFAULT = (
"# Available variables:\n"
"# - env: Odoo Environment on which the action is triggered\n"
"# - time, datetime, dateutil, timezone: useful Python libraries\n"
"# - records: lines to sort\n"
"# - default_filter_func: default filtering function\n"
"#\n"
"# To provide the order for stock.move.line records to be processed, define eg.:\n"
"# move_lines = records.filtered(default_filter_func).sorted()\n"
"\n"
"\n"
)


class ShopfloorMenu(models.Model):
_inherit = "shopfloor.menu"
Expand Down Expand Up @@ -226,6 +251,26 @@ class ShopfloorMenu(models.Model):
allow_alternative_destination_package_is_possible = fields.Boolean(
compute="_compute_allow_alternative_destination_package_is_possible"
)
move_line_processing_sort_order_is_possible = fields.Boolean(
compute="_compute_move_line_processing_sort_order_is_possible"
)
move_line_processing_sort_order = fields.Selection(
selection=[
("location", "Location"),
("location_grouped_product", "Location Grouping products"),
("custom_code", "Custom code"),
],
string="Sort method used when processing move lines",
default="location",
required=True,
help=MOVE_LINE_PROCESSING_SORT_ORDER_HELP,
)

move_line_processing_sort_order_custom_code = fields.Text(
string="Custom sort key code",
default=CUSTOM_CODE_DEFAULT,
help="Python code to sort move lines.",
)

@api.onchange("unload_package_at_destination")
def _onchange_unload_package_at_destination(self):
Expand Down Expand Up @@ -455,3 +500,20 @@ def _compute_allow_alternative_destination_package_is_possible(self):
menu.allow_alternative_destination_package_is_possible = (
menu.scenario_id.has_option("allow_alternative_destination_package")
)

@api.depends("scenario_id")
def _compute_move_line_processing_sort_order_is_possible(self):
for menu in self:
menu.move_line_processing_sort_order_is_possible = (

Check warning on line 507 in shopfloor/models/shopfloor_menu.py

View check run for this annotation

Codecov / codecov/patch

shopfloor/models/shopfloor_menu.py#L507

Added line #L507 was not covered by tests
menu.scenario_id.has_option("allow_move_line_processing_sort_order")
)

@api.constrains("move_line_processing_sort_order_custom_code")
def _check_python_code(self):
for menu in self.sudo().filtered("move_line_processing_sort_order_custom_code"):
msg = test_python_expr(
expr=menu.move_line_processing_sort_order_custom_code.strip(),
mode="exec",
)
if msg:
raise exceptions.ValidationError(msg)

Check warning on line 519 in shopfloor/models/shopfloor_menu.py

View check run for this annotation

Codecov / codecov/patch

shopfloor/models/shopfloor_menu.py#L519

Added line #L519 was not covered by tests
93 changes: 82 additions & 11 deletions shopfloor/services/cluster_picking.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,16 @@
# Copyright 2020-2022 Jacques-Etienne Baudoux (BCIM) <[email protected]>
# Copyright 2023 Michael Tietz (MT Software) <[email protected]>
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
from odoo import _, fields
from pytz import timezone

from odoo import _, exceptions, fields
from odoo.osv import expression
from odoo.tools.safe_eval import (
datetime as safe_datetime,
dateutil as safe_dateutil,
safe_eval,
time as safe_time,
)

from odoo.addons.base_rest.components.service import to_bool, to_int
from odoo.addons.component.core import Component
Expand Down Expand Up @@ -362,17 +370,80 @@ def _lines_for_picking_batch(self, picking_batch, filter_func=lambda x: x):
# after ALL the other lines in the batch are processed.
return lines.sorted(key=self._sort_key_lines)

@staticmethod
def _lines_filtering(line):
return (
line.state in ("assigned", "partially_available")
# On 'StockPicking.action_assign()', result_package_id is set to
# the same package as 'package_id'. Here, we need to exclude lines
# that were already put into a bin, i.e. the destination package
# is different.
and (
not line.result_package_id or line.result_package_id == line.package_id
)
)

def _group_by_product(self, lines):
grouped_line_ids = []
product_ids_checked = set()

Check warning on line 388 in shopfloor/services/cluster_picking.py

View check run for this annotation

Codecov / codecov/patch

shopfloor/services/cluster_picking.py#L387-L388

Added lines #L387 - L388 were not covered by tests
for line in lines:
if line.product_id.id not in product_ids_checked:
same_product_line_ids = lines.filtered(
lambda x: x.product_id == line.product_id
).ids
grouped_line_ids.extend(same_product_line_ids)
product_ids_checked.add(line.product_id.id)
lines = self.env["stock.move.line"].browse(grouped_line_ids)
return lines

Check warning on line 397 in shopfloor/services/cluster_picking.py

View check run for this annotation

Codecov / codecov/patch

shopfloor/services/cluster_picking.py#L394-L397

Added lines #L394 - L397 were not covered by tests

def _lines_to_pick(self, picking_batch):
return self._lines_for_picking_batch(
picking_batch,
filter_func=lambda l: (
l.state in ("assigned", "partially_available")
# On 'StockPicking.action_assign()', result_package_id is set to
# the same package as 'package_id'. Here, we need to exclude lines
# that were already put into a bin, i.e. the destination package
# is different.
and (not l.result_package_id or l.result_package_id == l.package_id)
),
order = self.work.menu.move_line_processing_sort_order
if order == "location":
lines = self._lines_for_picking_batch(
picking_batch, filter_func=self._lines_filtering
)
elif order == "location_grouped_product":
# we need to call _lines_for_picking_batch
# without passing a filter_func so that the ordering is computed
# taking into account all lines in the batch,
# including those that have already been processed.
lines = self._lines_for_picking_batch(
picking_batch,
filter_func=lambda x: x,
)
lines = self._group_by_product(lines).filtered(self._lines_filtering)

Check warning on line 414 in shopfloor/services/cluster_picking.py

View check run for this annotation

Codecov / codecov/patch

shopfloor/services/cluster_picking.py#L414

Added line #L414 was not covered by tests
elif order == "custom_code":
lines = self._eval_custom_code(picking_batch)

Check warning on line 416 in shopfloor/services/cluster_picking.py

View check run for this annotation

Codecov / codecov/patch

shopfloor/services/cluster_picking.py#L416

Added line #L416 was not covered by tests
return lines

def _eval_context(self, move_lines):
return {

Check warning on line 420 in shopfloor/services/cluster_picking.py

View check run for this annotation

Codecov / codecov/patch

shopfloor/services/cluster_picking.py#L420

Added line #L420 was not covered by tests
"uid": self.env.uid,
"user": self.env.user,
"time": safe_time,
"datetime": safe_datetime,
"dateutil": safe_dateutil,
"timezone": timezone,
# orm
"env": self.env,
# record
"records": move_lines,
# filter
"default_filter_func": self._lines_filtering,
}

def _eval_custom_code(self, picking_batch):
expr = self.work.menu.move_line_processing_sort_order_custom_code
move_lines = picking_batch.mapped("picking_ids.move_line_ids")
eval_context = self._eval_context(move_lines)
try:
safe_eval(expr, eval_context, mode="exec", nocopy=True)
except Exception as err:
raise exceptions.UserError(

Check warning on line 442 in shopfloor/services/cluster_picking.py

View check run for this annotation

Codecov / codecov/patch

shopfloor/services/cluster_picking.py#L436-L442

Added lines #L436 - L442 were not covered by tests
_("Error when evaluating the move lines sorting code:\n %s") % (err)
)
return eval_context.get(

Check warning on line 445 in shopfloor/services/cluster_picking.py

View check run for this annotation

Codecov / codecov/patch

shopfloor/services/cluster_picking.py#L445

Added line #L445 was not covered by tests
"move_lines", move_lines.filtered(self._lines_filtering)
)

def _last_picked_line(self, picking):
Expand Down
20 changes: 20 additions & 0 deletions shopfloor/views/shopfloor_menu.xml
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,26 @@
/>
<field name="allow_alternative_destination_package" />
</group>
<group
name="move_line_processing_sort_order"
attrs="{'invisible': [('move_line_processing_sort_order_is_possible', '=', False)]}"
>
<field
name="move_line_processing_sort_order_is_possible"
invisible="1"
/>
<field name="move_line_processing_sort_order" />
</group>
</group>
<group name="options" position="after">
<group name="move_line_processing_sort_order_custom_code">
<field
name="move_line_processing_sort_order_custom_code"
attrs="{'invisible': [('move_line_processing_sort_order', '!=', 'custom_code')]}"
widget="ace"
options="{'mode': 'python'}"
/>
</group>
</group>
</field>
</record>
Expand Down

0 comments on commit 55fa1e3

Please sign in to comment.