Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[16.0] shopfloor: location_content_transfer: get_work additional domain and sort key. #917

Merged
2 changes: 1 addition & 1 deletion shopfloor/__manifest__.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
{
"name": "Shopfloor",
"summary": "manage warehouse operations with barcode scanners",
"version": "16.0.2.2.0",
"version": "16.0.2.2.1",
"development_status": "Beta",
"category": "Inventory",
"website": "https://github.com/OCA/wms",
Expand Down
158 changes: 125 additions & 33 deletions shopfloor/actions/move_line_search.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
# Copyright 2020 Camptocamp SA (http://www.camptocamp.com)
# Copyright 2024 ACSONE SA/NV
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
from odoo.tools import safe_eval

from odoo.addons.component.core import Component


Expand All @@ -20,9 +23,42 @@
self.work, "picking_types", self.env["stock.picking.type"].browse()
)

def _search_move_lines_by_location_domain(
@property
def additional_domain(self):
return getattr(self.work, "additional_domain", [])

@property
def sort_order(self):
return getattr(self.work, "sort_order", "priority")

@property
def sort_order_custom_code(self):
return getattr(self.work, "sort_order_custom_code", None)

def _get_additional_domain_eval_context(self):
"""Prepare the context used when evaluating the additional domain
:returns: dict -- evaluation context given to safe_eval
"""
return {
"datetime": safe_eval.datetime,
"dateutil": safe_eval.dateutil,
"time": safe_eval.time,
"uid": self.env.uid,
"user": self.env.user,
}

def _sort_key_custom_code_eval_context(self, line):
return {
"line": line,
"key": None,
"get_sort_key_priority": self._sort_key_move_lines_priority,
"get_sort_key_location": self._sort_key_move_lines_location,
"get_sort_key_assigned_to_current_user": self._sort_key_assigned_to_current_user,
}

def _search_move_lines_domain(
self,
locations,
locations=None,
picking_type=None,
package=None,
product=None,
Expand All @@ -32,48 +68,61 @@
# When True, adds the package in the domain even if the package is False
enforce_empty_package=False,
):
"""Return a domain to search move lines.

Be careful on the use of the picking_type parameter. This paramater can take
a recordset or None as value. The interpretation of the value is as follows:
* If picking_type is None, the domain will be filtered on all picking types
defined in the work. (most probably those defined on the menu)
* If picking_type is a recordset, the domain will be filtered on the given
picking types if the recordset is not empty. If the recordset is empty,
the domain will not be filtered on any picking type.
"""
domain = [
("location_id", "child_of", locations.ids),
("qty_done", "=", 0),
("state", "in", ("assigned", "partially_available")),
]
if picking_type:
# auto_join in place for this field
domain += [("picking_id.picking_type_id", "=", picking_type.id)]
elif self.picking_types:
domain += [("picking_id.picking_type_id", "in", self.picking_types.ids)]
picking_types = picking_type if picking_type is not None else self.picking_types
if picking_types:
domain += [("picking_id.picking_type_id", "in", picking_types.ids)]
locations = locations or picking_types.default_location_src_id
if locations:
domain += [("location_id", "child_of", locations.ids)]
if package or package is not None and enforce_empty_package:
domain += [("package_id", "=", package.id if package else False)]
if product:
domain += [("product_id", "=", product.id)]
if lot:
domain += [("lot_id", "=", lot.id)]
if match_user:
# we only want to see the lines assigned to the current user
domain += [
"|",
("shopfloor_user_id", "=", False),
("shopfloor_user_id", "=", self.env.uid),
("shopfloor_user_id", "in", (False, self.env.uid)),
("picking_id.user_id", "in", (False, self.env.uid)),
]
if picking_ready:
domain += [("picking_id.state", "=", "assigned")]
if self.additional_domain:
eval_context = self._get_additional_domain_eval_context()
domain += safe_eval.safe_eval(self.additional_domain, eval_context)
return domain

def search_move_lines_by_location(
def search_move_lines(
self,
locations,
locations=None,
picking_type=None,
package=None,
product=None,
lot=None,
order="priority",
order=None,
match_user=False,
sort_keys_func=None,
picking_ready=True,
enforce_empty_package=False,
):
"""Find lines that potentially need work in given locations."""
move_lines = self.env["stock.move.line"].search(
self._search_move_lines_by_location_domain(
self._search_move_lines_domain(
locations,
picking_type,
package,
Expand All @@ -84,36 +133,79 @@
enforce_empty_package=enforce_empty_package,
)
)
order = order or self.sort_order
sort_keys_func = sort_keys_func or self._sort_key_move_lines(order)
move_lines = move_lines.sorted(sort_keys_func)
return move_lines

@staticmethod
def _sort_key_move_lines(order):
def _sort_key_move_lines(self, order=None):
"""Return a sorting function to order lines."""
if order is None:
return lambda line: tuple()

if order == "priority":
# make prority negative to keep sorting ascending
return lambda line: (
-int(line.move_id.priority or "0"),
line.move_id.date,
line.move_id.id,
)
elif order == "location":
return lambda line: (
line.location_id.shopfloor_picking_sequence or "",
line.location_id.name,
line.move_id.date,
line.move_id.id,
)
return lambda line: line
return self._sort_key_move_lines_priority

if order == "location":
return self._sort_key_move_lines_location

if order == "custom_code":
return self._sort_key_custom_code

if order == "assigned_to_current_user":
return self._sort_key_assigned_to_current_user

raise ValueError(f"Unknown order '{order}'")

Check warning on line 158 in shopfloor/actions/move_line_search.py

View check run for this annotation

Codecov / codecov/patch

shopfloor/actions/move_line_search.py#L158

Added line #L158 was not covered by tests

def _sort_key_move_lines_priority(self, line):
# make prority negative to keep sorting ascending
return self._sort_key_assigned_to_current_user(line) + (
-int(line.move_id.priority or "0"),
line.move_id.date,
line.move_id.id,
)

def _sort_key_move_lines_location(self, line):
return self._sort_key_assigned_to_current_user(line) + (
line.location_id.shopfloor_picking_sequence or "",
line.location_id.name,
line.move_id.date,
line.move_id.id,
)

def _sort_key_assigned_to_current_user(self, line):
user_id = line.shopfloor_user_id.id or line.picking_id.user_id.id or None
# Determine sort priority
# Priority 0: Assigned to the current user
# Priority 1: Not assigned to any user
# Priority 2: Assigned to a different user
if user_id == self.env.uid:
priority = 0
elif user_id is None:
priority = 1
else:
priority = 2
jbaudoux marked this conversation as resolved.
Show resolved Hide resolved
return (priority,)

def _sort_key_custom_code(self, line):
context = self._sort_key_custom_code_eval_context(line)
safe_eval.safe_eval(
self.sort_order_custom_code, context, mode="exec", nocopy=True
)
return context["key"]

def counters_for_lines(self, lines):
# Not using mapped/filtered to support simple lists and generators
priority_lines = [x for x in lines if int(x.picking_id.priority or "0") > 0]
priority_lines = set()
priority_pickings = set()
for line in lines:
if int(line.move_id.priority or "0") > 0:
priority_lines.add(line)
if int(line.picking_id.priority or "0") > 0:
priority_pickings.add(line.picking_id)
return {
"lines_count": len(lines),
"picking_count": len({x.picking_id.id for x in lines}),
"priority_lines_count": len(priority_lines),
"priority_picking_count": len({x.picking_id.id for x in priority_lines}),
"priority_picking_count": len(priority_pickings),
}
9 changes: 7 additions & 2 deletions shopfloor/data/shopfloor_scenario_data.xml
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,9 @@
"multiple_move_single_pack": true,
"no_prefill_qty": true,
"scan_location_or_pack_first": true,
"allow_alternative_destination_package": true
"allow_alternative_destination_package": true,
"allow_move_line_search_sort_order": false,
"allow_move_line_search_additional_domain": true
}
</field>
</record>
Expand Down Expand Up @@ -68,7 +70,10 @@
"allow_unreserve_other_moves": true,
"allow_ignore_no_putaway_available": true,
"allow_get_work": true,
"no_prefill_qty": true
"no_prefill_qty": true,
"allow_move_line_search_sort_order": true,
"allow_move_line_search_additional_domain": true

}
</field>
</record>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
# Copyright 2024 ACSONE SA/NV (http://acsone.eu)
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).

import json
import logging

from odoo import SUPERUSER_ID, api

_logger = logging.getLogger(__name__)


def migrate(cr, version):
if not version:
return
env = api.Environment(cr, SUPERUSER_ID, {})
zone_picking = env.ref("shopfloor.scenario_zone_picking")
_update_scenario_options(zone_picking, sort_order=False, additional_domain=True)
location_content_transfer = env.ref("shopfloor.scenario_location_content_transfer")
_update_scenario_options(
location_content_transfer, sort_order=True, additional_domain=True
)


def _update_scenario_options(scenario, sort_order=True, additional_domain=True):
options = scenario.options
options["allow_move_line_search_sort_order"] = sort_order
options["allow_move_line_search_additional_domain"] = additional_domain
options_edit = json.dumps(options or {}, indent=4, sort_keys=True)
scenario.write({"options_edit": options_edit})
_logger.info(
"Option allow_alternative_destination_package added to scenario %s",
scenario.name,
)
85 changes: 85 additions & 0 deletions shopfloor/models/shopfloor_menu.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
# Copyright 2020 Camptocamp SA (http://www.camptocamp.com)
# Copyright 2024 ACSONE SA/NV (http://www.acsone.eu)
# 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 @@ -227,6 +229,28 @@
compute="_compute_allow_alternative_destination_package_is_possible"
)

move_line_search_additional_domain_is_possible = fields.Boolean(
compute="_compute_move_line_search_additional_domain_is_possible"
)
move_line_search_additional_domain = fields.Char(
string="Additional domain used when searching move lines"
)
move_line_search_sort_order_is_possible = fields.Boolean(
compute="_compute_move_line_search_sort_order_is_possible"
)
move_line_search_sort_order = fields.Selection(
selection=[
("priority", "Priority"),
("location", "Location"),
("custom_code", "Custom code"),
],
string="Sort method used when searching move lines",
default="priority",
)
move_line_search_sort_order_custom_code = fields.Text(
string="Custom sort key code", help="Python code to sort move lines. "
)

@api.onchange("unload_package_at_destination")
def _onchange_unload_package_at_destination(self):
# Uncheck pick_pack_same_time when unload_package_at_destination is set to True
Expand Down Expand Up @@ -458,3 +482,64 @@
menu.allow_alternative_destination_package_is_possible = (
menu.scenario_id.has_option("allow_alternative_destination_package")
)

@api.depends("scenario_id")
def _compute_move_line_search_additional_domain_is_possible(self):
for menu in self:
menu.move_line_search_additional_domain_is_possible = (

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

View check run for this annotation

Codecov / codecov/patch

shopfloor/models/shopfloor_menu.py#L489

Added line #L489 was not covered by tests
menu.scenario_id.has_option("allow_move_line_search_additional_domain")
)

@api.depends("scenario_id")
def _compute_move_line_search_sort_order_is_possible(self):
for menu in self:
menu.move_line_search_sort_order_is_possible = menu.scenario_id.has_option(

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

View check run for this annotation

Codecov / codecov/patch

shopfloor/models/shopfloor_menu.py#L496

Added line #L496 was not covered by tests
"allow_move_line_search_sort_order"
)

@api.constrains(
"move_line_search_sort_order", "move_line_search_sort_order_custom_code"
)
def _check_move_line_search_sort_order_custom_code(self):
for menu in self:
if (
menu.move_line_search_sort_order == "custom_code"
and not menu.move_line_search_sort_order_custom_code
):
raise exceptions.ValidationError(
_(
"Custom sort key code is required when 'Custom code' is selected."
)
)
if (
menu.move_line_search_sort_order != "custom_code"
and menu.move_line_search_sort_order_custom_code
):
raise exceptions.ValidationError(

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

View check run for this annotation

Codecov / codecov/patch

shopfloor/models/shopfloor_menu.py#L518

Added line #L518 was not covered by tests
_(
"Custom sort key code is only allowed when 'Custom code' is selected."
)
)
code = (
menu.move_line_search_sort_order_custom_code
and menu.move_line_search_sort_order_custom_code.strip()
)
if code:
msg = test_python_expr(code, mode="exec")
if msg:
raise exceptions.ValidationError(msg)

@api.model_create_multi
def create(self, vals_list):
for vals in vals_list:
if vals.get("move_line_search_sort_order", "") != "custom_code":
vals["move_line_search_sort_order_custom_code"] = ""
return super().create(vals_list)

def write(self, vals):
if (
"move_line_search_sort_order" in vals
and vals["move_line_search_sort_order"] != "custom_code"
):
vals["move_line_search_sort_order_custom_code"] = ""
return super().write(vals)
Loading
Loading