Skip to content

Commit

Permalink
Merge PR #80 into 13.0
Browse files Browse the repository at this point in the history
Signed-off-by simahawk
  • Loading branch information
OCA-git-bot committed Nov 27, 2020
2 parents dbad7fe + f04ca8f commit ca10b47
Show file tree
Hide file tree
Showing 29 changed files with 44,079 additions and 60,654 deletions.
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": "13.0.1.3.7",
"version": "13.0.1.4.0",
"development_status": "Alpha",
"category": "Inventory",
"website": "https://github.com/OCA/wms",
Expand Down
1 change: 1 addition & 0 deletions shopfloor/actions/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,3 +26,4 @@
from . import search
from . import inventory
from . import savepoint
from . import move_line_search
105 changes: 105 additions & 0 deletions shopfloor/actions/move_line_search.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
# Copyright 2020 Camptocamp SA (http://www.camptocamp.com)
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
from odoo.addons.component.core import Component


class MoveLineSearch(Component):
"""Provide methods to search move line records.
The methods should be used in Service Components, so a search will always
have the same result in all scenarios.
"""

_name = "shopfloor.search.move.line"
_inherit = "shopfloor.process.action"
_usage = "search_move_line"

@property
def picking_types(self):
return getattr(
self.work, "picking_types", self.env["stock.picking.type"].browse()
)

def _search_move_lines_by_location_domain(
self,
locations,
picking_type=None,
package=None,
product=None,
lot=None,
match_user=False,
):
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)]
if package:
domain += [("package_id", "=", package.id)]
if product:
domain += [("product_id", "=", product.id)]
if lot:
domain += [("lot_id", "=", lot.id)]
if match_user:
domain += [
"|",
("shopfloor_user_id", "=", False),
("shopfloor_user_id", "=", self.env.uid),
]
return domain

def search_move_lines_by_location(
self,
locations,
picking_type=None,
package=None,
product=None,
lot=None,
order="priority",
match_user=False,
sort_keys_func=None,
):
"""Find lines that potentially need work in given locations."""
move_lines = self.env["stock.move.line"].search(
self._search_move_lines_by_location_domain(
locations, picking_type, package, product, lot, match_user=match_user
)
)
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):
"""Return a sorting function to order lines."""

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

def counters_for_lines(self, lines, priority_selection=("2", "3")):
# Not using mapped/filtered to support simple lists and generators
priority_lines = [
x for x in lines if x.picking_id.priority in priority_selection
]
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}),
}
18 changes: 11 additions & 7 deletions shopfloor/actions/search.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,27 +17,31 @@ class SearchAction(Component):
def location_from_scan(self, barcode):
if not barcode:
return self.env["stock.location"].browse()
return self.env["stock.location"].search([("barcode", "=", barcode)])
return self.env["stock.location"].search([("barcode", "=", barcode)], limit=1)

def package_from_scan(self, barcode):
return self.env["stock.quant.package"].search([("name", "=", barcode)])
return self.env["stock.quant.package"].search([("name", "=", barcode)], limit=1)

def picking_from_scan(self, barcode):
return self.env["stock.picking"].search([("name", "=", barcode)])
return self.env["stock.picking"].search([("name", "=", barcode)], limit=1)

def product_from_scan(self, barcode):
product = self.env["product.product"].search([("barcode", "=", barcode)])
product = self.env["product.product"].search(
[("barcode", "=", barcode)], limit=1
)
if not product:
packaging = self.env["product.packaging"].search(
[("product_id", "!=", False), ("barcode", "=", barcode)]
[("product_id", "!=", False), ("barcode", "=", barcode)], limit=1
)
product = packaging.product_id
return product

def lot_from_scan(self, barcode):
return self.env["stock.production.lot"].search([("name", "=", barcode)])
return self.env["stock.production.lot"].search(
[("name", "=", barcode)], limit=1
)

def generic_packaging_from_scan(self, barcode):
return self.env["product.packaging"].search(
[("barcode", "=", barcode), ("product_id", "=", False)]
[("barcode", "=", barcode), ("product_id", "=", False)], limit=1
)
2 changes: 1 addition & 1 deletion shopfloor/models/shopfloor_log.py
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@ def _get_exception_severity_mapping(self):
if not exc_name or not severity:
raise ValueError
except ValueError:
_logger.exception(
_logger.info(
"Could not convert System Parameter"
" 'shopfloor.log.severity.exception.mapping' to mapping."
" The following rule will be ignored: %s",
Expand Down
51 changes: 31 additions & 20 deletions shopfloor/services/menu.py
Original file line number Diff line number Diff line change
Expand Up @@ -59,16 +59,31 @@ def search(self, name_fragment=None):
)

def _convert_one_record(self, record):
# TODO: use `jsonify`
return {
"id": record.id,
"name": record.name,
"scenario": record.scenario,
"picking_types": [
{"id": picking_type.id, "name": picking_type.name}
for picking_type in record.picking_type_ids
],
}
values = record.jsonify(self._one_record_parser, one=True)
counters = self._get_move_line_counters(record)
values.update(counters)
return values

def _get_move_line_counters(self, record):
"""Lookup for all lines per menu item and compute counters.
"""
# TODO: maybe to be improved w/ raw SQL as this run for each menu item
# and it's called every time the menu is opened/gets refreshed
move_line_search = self.actions_for(
"search_move_line", picking_types=record.picking_type_ids
)
locations = record.picking_type_ids.mapped("default_location_src_id")
lines_per_menu = move_line_search.search_move_lines_by_location(locations)
return move_line_search.counters_for_lines(lines_per_menu)

@property
def _one_record_parser(self):
return [
"id",
"name",
"scenario",
("picking_type_ids:picking_types", ["id", "name"]),
]


class ShopfloorMenuValidator(Component):
Expand All @@ -92,28 +107,24 @@ class ShopfloorMenuValidatorResponse(Component):
_usage = "menu.validator.response"

def return_search(self):
record_schema = self._record_schema
return self._response_schema(
{
"size": {"coerce": to_int, "required": True, "type": "integer"},
"records": {
"type": "list",
"required": True,
"schema": {"type": "dict", "schema": self._record_schema},
},
"records": self.schemas._schema_list_of(record_schema),
}
)

@property
def _record_schema(self):
return {
schema = {
"id": {"coerce": to_int, "required": True, "type": "integer"},
"name": {"type": "string", "nullable": False, "required": True},
"scenario": {"type": "string", "nullable": False, "required": True},
"picking_types": {
"type": "list",
"schema": {"type": "dict", "schema": self._picking_type_schema},
},
"picking_types": self.schemas._schema_list_of(self._picking_type_schema),
}
schema.update(self.schemas.move_lines_counters())
return schema

@property
def _picking_type_schema(self):
Expand Down
8 changes: 8 additions & 0 deletions shopfloor/services/schema.py
Original file line number Diff line number Diff line change
Expand Up @@ -186,3 +186,11 @@ def picking_type(self):
"id": {"required": True, "type": "integer"},
"name": {"type": "string", "nullable": False, "required": True},
}

def move_lines_counters(self):
return {
"lines_count": {"type": "float", "required": True},
"picking_count": {"type": "float", "required": True},
"priority_lines_count": {"type": "float", "required": True},
"priority_picking_count": {"type": "float", "required": True},
}
33 changes: 27 additions & 6 deletions shopfloor/services/service.py
Original file line number Diff line number Diff line change
Expand Up @@ -320,19 +320,22 @@ def _get_openapi_default_parameters(self):
def actions_collection(self):
return _PseudoCollection(self._actions_collection_name, self.env)

def actions_for(self, usage):
def actions_for(self, usage, propagate_kwargs=None, **kw):
"""Return an Action Component for a usage
Action Components are the components supporting the business logic of
the processes, so we can limit the code in Services to the minimum and
share methods.
"""
propagate_kwargs = self.work._propagate_kwargs[:] + (propagate_kwargs or [])
# propagate custom arguments (such as menu ID/profile ID)
kwargs = {
attr_name: getattr(self.work, attr_name)
for attr_name in self.work._propagate_kwargs
for attr_name in propagate_kwargs
if attr_name not in ("collection", "components_registry")
and hasattr(self.work, attr_name)
}
kwargs.update(kw)
work = WorkContext(collection=self.actions_collection, **kwargs)
return work.component(usage=usage)

Expand All @@ -355,6 +358,11 @@ def data_detail(self):
def msg_store(self):
return self.actions_for("message")

@property
def search_move_line(self):
# TODO: propagating `picking_types` should probably be default
return self.actions_for("search_move_line", propagate_kwargs=["picking_types"])

# TODO: maybe to be proposed to base_rest
# TODO: add tests
def _validate_headers_update_work_context(self, request, method_name):
Expand Down Expand Up @@ -424,22 +432,35 @@ class BaseShopfloorProcess(AbstractComponent):
_requires_header_menu = True
_requires_header_profile = True

@property
def picking_types(self):
def _get_process_picking_types(self):
"""Return picking types for the menu and profile"""
# TODO make this a lazy property or computed field avoid running the
# filter every time?
picking_types = self.work.menu.picking_type_ids.filtered(
lambda pt: not pt.warehouse_id
or pt.warehouse_id == self.work.profile.warehouse_id
)
if not picking_types:
return picking_types

@property
def picking_types(self):
if not hasattr(self.work, "picking_types"):
self.work.picking_types = self._get_process_picking_types()
if not self.work.picking_types:
raise exceptions.UserError(
_("No operation types configured on menu {} for warehouse {}.").format(
self.work.menu.name, self.work.profile.warehouse_id.display_name
)
)
return picking_types
return self.work.picking_types

@property
def search_move_line(self):
# TODO: picking types should be set somehow straight in the work context
# by `_validate_headers_update_work_context` in this way
# we can remove this override and the need to call `_get_process_picking_types`
# every time.
return self.actions_for("search_move_line", picking_types=self.picking_types)

def _check_picking_status(self, pickings):
"""Check if given pickings can be processed.
Expand Down
Loading

0 comments on commit ca10b47

Please sign in to comment.