From 752ea676c3eb8db88d9429d314ec059dff01a60a Mon Sep 17 00:00:00 2001 From: Thierry Ducrest Date: Wed, 7 Jun 2023 14:27:22 +0200 Subject: [PATCH 1/7] shopfloor_reception: add hook for reception package dimension --- .../docs/reception_sequence_graph.mermaid | 22 +++++++++---------- shopfloor_reception/services/reception.py | 8 +++++-- 2 files changed, 17 insertions(+), 13 deletions(-) diff --git a/shopfloor_reception/docs/reception_sequence_graph.mermaid b/shopfloor_reception/docs/reception_sequence_graph.mermaid index ea0c509bb2..097559c87c 100644 --- a/shopfloor_reception/docs/reception_sequence_graph.mermaid +++ b/shopfloor_reception/docs/reception_sequence_graph.mermaid @@ -1,7 +1,7 @@ %%{init: {'theme': 'neutral' } }%% sequenceDiagram participant select_document - participant select_line + participant select_move participant set_lot participant set_quantity participant set_destination @@ -10,19 +10,19 @@ sequenceDiagram note left of select_document: scan_document(barcode) select_document ->> select_document: Error: barcode not found select_document ->> select_document: Multiple picking matching the product / packaging barcode - select_document ->> select_line: Picking scanned, one has been found + select_document ->> select_move: Picking scanned, one has been found select_document ->> set_lot: Packaging / Product has been scanned, single correspondance. Tracked product select_document ->> set_quantity: Packaging / Product has been scanned, single correspondance. Not tracked product end rect rgb(100, 250, 170) - note left of select_line: scan_line(picking_id, barcode) - select_line ->> select_line: Error: barcode not found - select_line ->> set_lot: Packaging / Product has been scanned, single correspondance. Tracked product - select_line ->> set_quantity: Packaging / Product has been scanned, single correspondance. Not tracked product + note left of select_move: scan_line(picking_id, barcode) + select_move ->> select_move: Error: barcode not found + select_move ->> set_lot: Packaging / Product has been scanned, single correspondance. Tracked product + select_move ->> set_quantity: Packaging / Product has been scanned, single correspondance. Not tracked product end rect rgb(250, 220, 200) note left of set_lot: set_lot(picking_id, select_line_ids, lot_name=None, expiration_date=None) - set_lot ->> select_line: User clicked on back + set_lot ->> select_move: User clicked on back set_lot ->> set_lot: Barcode not found. Ask user to create one from barcode set_lot ->> set_lot: expiration_date has been set on the selected line set_lot ->> set_lot: lot_it has been set on the selected line @@ -37,8 +37,8 @@ sequenceDiagram set_quantity ->> set_quantity: Error: User tried to scan a package with a non valid location set_quantity ->> set_quantity: Error: User tried to scan a non valid location set_quantity ->> set_quantity: Warning: User scanned an unknown barcode. Ask to create a package - set_quantity ->> select_line: User scanned a package with a valid location - set_quantity ->> select_line: User scanned a valid location + set_quantity ->> select_move: User scanned a package with a valid location + set_quantity ->> select_move: User scanned a valid location set_quantity ->> set_destination: User scanner a package with no location note right of set_quantity: process_with_new_pack(picking_id, select_line_ids) set_quantity ->> set_destination: User confirmed the creation of a new package @@ -51,11 +51,11 @@ sequenceDiagram note left of set_destination: set_destination(picking_id, selected_line_ids, location_id, confirmation=False) set_destination ->> set_destination: Warning: User scanned a child location of the picking type. Ask for confirmation set_destination ->> set_destination: Error: User tried to scan a non-valid location - set_destination ->> select_line: User scanned a child location of the move's dest location + set_destination ->> select_move: User scanned a child location of the move's dest location end rect rgb(250, 150, 150) note left of select_dest_package: select_dest_package(picking_id, selected_line_ids, location_id, confirmation=False) - select_dest_package ->> select_line: User scanned a valid package + select_dest_package ->> select_move: User scanned a valid package select_dest_package ->> select_dest_package: Warning: User scanned an unknown barcode. Confirm to create one. select_dest_package ->> select_dest_package: Error: User scanned a non-empty package end diff --git a/shopfloor_reception/services/reception.py b/shopfloor_reception/services/reception.py index f1d421deb2..f0e9570a9a 100644 --- a/shopfloor_reception/services/reception.py +++ b/shopfloor_reception/services/reception.py @@ -272,7 +272,7 @@ def _scan_line__assign_user(self, picking, line, qty_done): self._assign_user_to_line(line) line.qty_done += qty_done if product.tracking not in ("lot", "serial") or (line.lot_id or line.lot_name): - return self._response_for_set_quantity(picking, line) + return self._before_state__set_quantity(picking, line) return self._response_for_set_lot(picking, line) def _select_line__filter_lines_by_packaging__return(self, lines, packaging): @@ -782,6 +782,10 @@ def _align_product_uom_qties(self, move): for line in lines: line.product_uom_qty = line.qty_done + remaining_todo + def _before_state__set_quantity(self, picking, line, message=None): + # Used by inherting module see shopfloor_reception_packaging_dimension + return self._response_for_set_quantity(picking, line, message=message) + def _response_for_set_quantity( self, picking, line, message=None, asking_confirmation=False ): @@ -1043,7 +1047,7 @@ def set_lot_confirm_action(self, picking_id, selected_line_id): message = self._check_expiry_date(selected_line) if message: return self._response_for_set_lot(picking, selected_line, message=message) - return self._response_for_set_quantity(picking, selected_line) + return self._before_state__set_quantity(picking, selected_line) def _check_expiry_date(self, line): use_expiration_date = ( From f9c4219643987cc140de6b238e2d5771155764b8 Mon Sep 17 00:00:00 2001 From: Thierry Ducrest Date: Wed, 7 Jun 2023 05:20:10 +0200 Subject: [PATCH 2/7] shopfloor: add packaging detail action --- shopfloor/actions/data_detail.py | 20 +++++++++++++++++++ shopfloor/actions/schema_detail.py | 22 +++++++++++++-------- shopfloor/tests/test_actions_data_base.py | 14 +++++++++++++ shopfloor/tests/test_actions_data_detail.py | 7 ++++--- 4 files changed, 52 insertions(+), 11 deletions(-) diff --git a/shopfloor/actions/data_detail.py b/shopfloor/actions/data_detail.py index b5836db245..b804baf12e 100644 --- a/shopfloor/actions/data_detail.py +++ b/shopfloor/actions/data_detail.py @@ -153,3 +153,23 @@ def _product_supplierinfo_parser(self): "product_name", "product_code", ] + + @ensure_model("product.packaging") + def packaging_detail(self, record, **kw): + return self._jsonify( + record.with_context(packaging=record.id), + self._packaging_detail_parser, + **kw + ) + + @property + def _packaging_detail_parser(self): + return self._packaging_parser + [ + "packaging_length:length", + "width", + "height", + "max_weight", + "length_uom_name:length_uom", + "weight_uom_name:weight_uom", + "barcode:barcode", + ] diff --git a/shopfloor/actions/schema_detail.py b/shopfloor/actions/schema_detail.py index 99b164651a..f21726f80f 100644 --- a/shopfloor/actions/schema_detail.py +++ b/shopfloor/actions/schema_detail.py @@ -90,11 +90,17 @@ def product_supplierinfo(self): ) return schema - # TODO - # def packaging_detail(self): - # schema = self.packaging() - # schema.update( - # { - # } - # ) - # return schema + def packaging_detail(self): + schema = self.packaging() + schema.update( + { + "length": {"type": "float", "nullable": True, "required": False}, + "width": {"type": "float", "nullable": True, "required": False}, + "height": {"type": "float", "nullable": True, "required": False}, + "max_weight": {"type": "float", "nullable": True, "required": False}, + "length_uom": {"type": "string", "nullable": True, "required": False}, + "weight_uom": {"type": "string", "nullable": True, "required": False}, + "barcode": {"type": "string", "nullable": True, "required": False}, + } + ) + return schema diff --git a/shopfloor/tests/test_actions_data_base.py b/shopfloor/tests/test_actions_data_base.py index 29f0572d92..f96d432a96 100644 --- a/shopfloor/tests/test_actions_data_base.py +++ b/shopfloor/tests/test_actions_data_base.py @@ -242,3 +242,17 @@ def _expected_product_detail(self, record, **kw): } ) return dict(**self._expected_product(record), **detail) + + def _expected_packaging_detail(self, record, **kw): + return dict( + **self._expected_packaging(record), + **{ + "length": record.packaging_length, + "width": record.width, + "height": record.height, + "max_weight": record.max_weight, + "length_uom": record.length_uom_name, + "weight_uom": record.weight_uom_name, + "barcode": record.barcode, + } + ) diff --git a/shopfloor/tests/test_actions_data_detail.py b/shopfloor/tests/test_actions_data_detail.py index 737be45a6e..9f543db95c 100644 --- a/shopfloor/tests/test_actions_data_detail.py +++ b/shopfloor/tests/test_actions_data_detail.py @@ -32,9 +32,10 @@ def test_data_location(self): ) def test_data_packaging(self): - data = self.data_detail.packaging(self.packaging) - self.assert_schema(self.schema_detail.packaging(), data) - self.assertDictEqual(data, self._expected_packaging(self.packaging)) + self.packaging.barcode = "barcode" + data = self.data_detail.packaging_detail(self.packaging) + self.assert_schema(self.schema_detail.packaging_detail(), data) + self.assertDictEqual(data, self._expected_packaging_detail(self.packaging)) def test_data_lot(self): lot = self.env["stock.production.lot"].create( From 3d2ee65a52cd68258b858cf9fc77e44689c40f2f Mon Sep 17 00:00:00 2001 From: Thierry Ducrest Date: Wed, 7 Jun 2023 04:25:43 +0200 Subject: [PATCH 3/7] shopfloor_reception_mobile: imp states inheritance From 5ab5fa8b108d0370e5756b2f51a8a01e1e0dc6b6 Mon Sep 17 00:00:00 2001 From: Thierry Ducrest Date: Thu, 8 Jun 2023 09:45:36 +0200 Subject: [PATCH 4/7] Add shopfloor_reception_packaging_dimension --- .../shopfloor_reception_packaging_dimension | 1 + .../setup.py | 6 + shopfloor/actions/message.py | 6 + .../__init__.py | 3 + .../__manifest__.py | 16 ++ .../hooks.py | 40 ++++ .../models/__init__.py | 1 + .../models/shopfloor_menu.py | 24 +++ .../readme/CONTRIBUTORS.rst | 1 + .../readme/DESCRIPTION.rst | 5 + .../services/__init__.py | 1 + .../services/reception.py | 197 ++++++++++++++++++ .../tests/__init__.py | 1 + .../tests/test_set_package_dimension.py | 160 ++++++++++++++ .../views/shopfloor_menu.xml | 20 ++ 15 files changed, 482 insertions(+) create mode 120000 setup/shopfloor_reception_packaging_dimension/odoo/addons/shopfloor_reception_packaging_dimension create mode 100644 setup/shopfloor_reception_packaging_dimension/setup.py create mode 100644 shopfloor_reception_packaging_dimension/__init__.py create mode 100644 shopfloor_reception_packaging_dimension/__manifest__.py create mode 100644 shopfloor_reception_packaging_dimension/hooks.py create mode 100644 shopfloor_reception_packaging_dimension/models/__init__.py create mode 100644 shopfloor_reception_packaging_dimension/models/shopfloor_menu.py create mode 100644 shopfloor_reception_packaging_dimension/readme/CONTRIBUTORS.rst create mode 100644 shopfloor_reception_packaging_dimension/readme/DESCRIPTION.rst create mode 100644 shopfloor_reception_packaging_dimension/services/__init__.py create mode 100644 shopfloor_reception_packaging_dimension/services/reception.py create mode 100644 shopfloor_reception_packaging_dimension/tests/__init__.py create mode 100644 shopfloor_reception_packaging_dimension/tests/test_set_package_dimension.py create mode 100644 shopfloor_reception_packaging_dimension/views/shopfloor_menu.xml diff --git a/setup/shopfloor_reception_packaging_dimension/odoo/addons/shopfloor_reception_packaging_dimension b/setup/shopfloor_reception_packaging_dimension/odoo/addons/shopfloor_reception_packaging_dimension new file mode 120000 index 0000000000..9ff2c52e7a --- /dev/null +++ b/setup/shopfloor_reception_packaging_dimension/odoo/addons/shopfloor_reception_packaging_dimension @@ -0,0 +1 @@ +../../../../shopfloor_reception_packaging_dimension \ No newline at end of file diff --git a/setup/shopfloor_reception_packaging_dimension/setup.py b/setup/shopfloor_reception_packaging_dimension/setup.py new file mode 100644 index 0000000000..28c57bb640 --- /dev/null +++ b/setup/shopfloor_reception_packaging_dimension/setup.py @@ -0,0 +1,6 @@ +import setuptools + +setuptools.setup( + setup_requires=['setuptools-odoo'], + odoo_addon=True, +) diff --git a/shopfloor/actions/message.py b/shopfloor/actions/message.py index d7b36db0da..01d9bda072 100644 --- a/shopfloor/actions/message.py +++ b/shopfloor/actions/message.py @@ -463,6 +463,12 @@ def packaging_not_found_in_picking(self): "body": _("Packaging not found in the current transfer."), } + def packaging_dimension_updated(self, packaging): + return { + "message_type": "success", + "body": _("Packaging {} dimension updated.").format(packaging.name), + } + def expiration_date_missing(self): return { "message_type": "error", diff --git a/shopfloor_reception_packaging_dimension/__init__.py b/shopfloor_reception_packaging_dimension/__init__.py new file mode 100644 index 0000000000..e7386302bb --- /dev/null +++ b/shopfloor_reception_packaging_dimension/__init__.py @@ -0,0 +1,3 @@ +from .hooks import post_init_hook, uninstall_hook +from . import models +from . import services diff --git a/shopfloor_reception_packaging_dimension/__manifest__.py b/shopfloor_reception_packaging_dimension/__manifest__.py new file mode 100644 index 0000000000..08566851e0 --- /dev/null +++ b/shopfloor_reception_packaging_dimension/__manifest__.py @@ -0,0 +1,16 @@ +{ + "name": "Shopfloor Reception Packaging Dimension", + "summary": "Collect Packaging Dimension from the Reception scenario", + "version": "14.0.1.0.0", + "development_status": "Beta", + "category": "Inventory", + "website": "https://github.com/OCA/wms", + "author": "Camptocamp, Odoo Community Association (OCA)", + "maintainers": ["TDu"], + "license": "AGPL-3", + "installable": True, + "depends": ["shopfloor_reception"], + "data": ["views/shopfloor_menu.xml"], + "post_init_hook": "post_init_hook", + "uninstall_hook": "uninstall_hook", +} diff --git a/shopfloor_reception_packaging_dimension/hooks.py b/shopfloor_reception_packaging_dimension/hooks.py new file mode 100644 index 0000000000..00c46969c7 --- /dev/null +++ b/shopfloor_reception_packaging_dimension/hooks.py @@ -0,0 +1,40 @@ +# Copyright 2023 Camptocamp SA (http://www.camptocamp.com) +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). + +import json +import logging + +from odoo import SUPERUSER_ID, api + +from odoo.addons.shopfloor_base.utils import purge_endpoints, register_new_services + +from .services.reception import Reception as Service + +_logger = logging.getLogger(__file__) + + +def post_init_hook(cr, registry): + _logger.info("Add set packaging dimension option on reception scenario") + env = api.Environment(cr, SUPERUSER_ID, {}) + scenario = env.ref("shopfloor_reception.scenario_reception") + options = scenario.options + options.update({"set_packaging_dimension": True}) + scenario.options_edit = json.dumps(options) + # The service imported is extending an existing component + # As it is a simple python import the odoo inheritance is not working + # So it needs to be fix + Service._usage = "reception" + Service._name = "shopfloor.reception" + register_new_services(env, Service) + + +def uninstall_hook(cr, registry): + _logger.info("Remove set packaging dimension option on reception scenario") + env = api.Environment(cr, SUPERUSER_ID, {}) + scenario = env.ref("shopfloor_reception.scenario_reception") + options = scenario.options + if "set_packaging_dimension" in options.keys(): + options.pop("set_packaging_dimension") + scenario.options_edit = json.dumps(options) + Service._usage = "reception" + purge_endpoints(env, Service._usage, endpoint="set_packaging_dimension") diff --git a/shopfloor_reception_packaging_dimension/models/__init__.py b/shopfloor_reception_packaging_dimension/models/__init__.py new file mode 100644 index 0000000000..8bd3d5195c --- /dev/null +++ b/shopfloor_reception_packaging_dimension/models/__init__.py @@ -0,0 +1 @@ +from . import shopfloor_menu diff --git a/shopfloor_reception_packaging_dimension/models/shopfloor_menu.py b/shopfloor_reception_packaging_dimension/models/shopfloor_menu.py new file mode 100644 index 0000000000..6cf71a58de --- /dev/null +++ b/shopfloor_reception_packaging_dimension/models/shopfloor_menu.py @@ -0,0 +1,24 @@ +# Copyright 2023 Camptocamp SA (http://www.camptocamp.com) +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). +from odoo import api, fields, models + + +class ShopfloorMenu(models.Model): + _inherit = "shopfloor.menu" + + set_packaging_dimension_is_possible = fields.Boolean( + compute="_compute_set_packaging_dimension_is_possible" + ) + set_packaging_dimension = fields.Boolean( + string="Set packaging dimension", + default=False, + help="If for the product being processed, its related packaging " + "dimension are not set, ask to fill them up.", + ) + + @api.depends("scenario_id") + def _compute_set_packaging_dimension_is_possible(self): + for menu in self: + menu.set_packaging_dimension_is_possible = menu.scenario_id.has_option( + "set_packaging_dimension" + ) diff --git a/shopfloor_reception_packaging_dimension/readme/CONTRIBUTORS.rst b/shopfloor_reception_packaging_dimension/readme/CONTRIBUTORS.rst new file mode 100644 index 0000000000..0dd376faec --- /dev/null +++ b/shopfloor_reception_packaging_dimension/readme/CONTRIBUTORS.rst @@ -0,0 +1 @@ +* Thierry Ducrest diff --git a/shopfloor_reception_packaging_dimension/readme/DESCRIPTION.rst b/shopfloor_reception_packaging_dimension/readme/DESCRIPTION.rst new file mode 100644 index 0000000000..fe5faf19ae --- /dev/null +++ b/shopfloor_reception_packaging_dimension/readme/DESCRIPTION.rst @@ -0,0 +1,5 @@ +This module adds an option to the reception scenario. +When activated. Before setting the quantity for the reception, +if there is product packaging related to the product received with +missing information, the user will be presented with a screen +(for each packaging) proposing to update the missing information. diff --git a/shopfloor_reception_packaging_dimension/services/__init__.py b/shopfloor_reception_packaging_dimension/services/__init__.py new file mode 100644 index 0000000000..aa19bba8ce --- /dev/null +++ b/shopfloor_reception_packaging_dimension/services/__init__.py @@ -0,0 +1 @@ +from . import reception diff --git a/shopfloor_reception_packaging_dimension/services/reception.py b/shopfloor_reception_packaging_dimension/services/reception.py new file mode 100644 index 0000000000..ccb43afa54 --- /dev/null +++ b/shopfloor_reception_packaging_dimension/services/reception.py @@ -0,0 +1,197 @@ +# Copyright 2023 Camptocamp SA +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl) + +from odoo.addons.base_rest.components.service import to_int +from odoo.addons.component.core import Component +from odoo.addons.shopfloor.utils import to_float + + +class Reception(Component): + _inherit = "shopfloor.reception" + + packaging_update_done = False + + def _before_state__set_quantity(self, picking, line, message=None): + """Show the packaging dimension screen before the set quantity screen.""" + if self.work.menu.set_packaging_dimension and not self.packaging_update_done: + packaging = self._get_next_packaging_to_set_dimension(line.product_id) + if packaging: + return self._response_for_set_packaging_dimension( + picking, line, packaging, message=message + ) + return super()._before_state__set_quantity(picking, line, message=message) + + def _get_next_packaging_to_set_dimension(self, product, previous_packaging=None): + """Return for a product the next packaging needing dimension to be set.""" + next_packaging_id = previous_packaging.id + 1 if previous_packaging else 0 + domain = [ + ("product_id", "=", product.id), + ("id", ">=", next_packaging_id), + "|", + "|", + "|", + "|", + "|", + "|", + "|", + "|", + "|", + "|", + ("packaging_length", "=", 0), + ("packaging_length", "=", False), + ("width", "=", 0), + ("width", "=", False), + ("height", "=", 0), + ("height", "=", False), + ("max_weight", "=", 0), + ("max_weight", "=", False), + ("qty", "=", 0), + ("qty", "=", False), + ("barcode", "=", False), + ] + return self.env["product.packaging"].search(domain, order="id", limit=1) + + def _response_for_set_packaging_dimension( + self, picking, line, packaging, message=None + ): + return self._response( + next_state="set_packaging_dimension", + data={ + "picking": self.data.picking(picking), + "selected_move_line": self.data.move_line(line), + "packaging": self.data_detail.packaging_detail(packaging), + }, + message=message, + ) + + def set_packaging_dimension( + self, picking_id, selected_line_id, packaging_id, cancel=False, **kwargs + ): + """Set the dimension on a product packaging. + + If the user cancel the dimension update we still propose the next + possible packgaging. + + Transitions: + - set_packaging_dimension: if more packaging needs dimension + - set_quantity: otherwise + """ + picking = self.env["stock.picking"].browse(picking_id) + selected_line = self.env["stock.move.line"].browse(selected_line_id) + packaging = self.env["product.packaging"].sudo().browse(packaging_id) + message = None + next_packaging = None + if not packaging: + message = self.msg_store.record_not_found() + elif not cancel and self._check_dimension_to_update(kwargs): + self._update_packaging_dimension(packaging, kwargs) + message = self.msg_store.packaging_dimension_updated(packaging) + + if packaging: + next_packaging = self._get_next_packaging_to_set_dimension( + selected_line.product_id, packaging + ) + if next_packaging: + return self._response_for_set_packaging_dimension( + picking, selected_line, next_packaging, message=message + ) + self.packaging_update_done = True + return self._before_state__set_quantity(picking, selected_line, message=message) + + def _check_dimension_to_update(self, dimensions): + """Return True if there is any dimension that needs to be updated on the packaging.""" + return any([value is not None for key, value in dimensions.items()]) + + def _get_dimension_fields_conversion_map(self): + return {"length": "packaging_length"} + + def _update_packaging_dimension(self, packaging, dimensions_to_update): + """Update dimension on the packaging.""" + fields_conv_map = self._get_dimension_fields_conversion_map() + for dimension, value in dimensions_to_update.items(): + if value is not None: + dimension = fields_conv_map.get(dimension, dimension) + packaging[dimension] = value + + +class ShopfloorReceptionValidator(Component): + _inherit = "shopfloor.reception.validator" + + def set_packaging_dimension(self): + return { + "picking_id": {"coerce": to_int, "required": True, "type": "integer"}, + "selected_line_id": { + "coerce": to_int, + "required": True, + "type": "integer", + }, + "packaging_id": {"coerce": to_int, "required": True, "type": "integer"}, + "height": { + "coerce": to_float, + "required": False, + "type": "float", + "nullable": True, + }, + "length": { + "coerce": to_float, + "required": False, + "type": "float", + "nullable": True, + }, + "width": { + "coerce": to_float, + "required": False, + "type": "float", + "nullable": True, + }, + "max_weight": { + "coerce": to_float, + "required": False, + "type": "float", + "nullable": True, + }, + "shipping_weight": { + "coerce": to_float, + "required": False, + "type": "float", + "nullable": True, + }, + "qty": { + "coerce": to_float, + "required": False, + "type": "float", + "nullable": True, + }, + "barcode": {"type": "string", "required": False, "nullable": True}, + "cancel": {"type": "boolean"}, + } + + +class ShopfloorReceptionValidatorResponse(Component): + _inherit = "shopfloor.reception.validator.response" + + def _states(self): + res = super()._states() + res.update({"set_packaging_dimension": self._schema_set_packaging_dimension}) + return res + + def _scan_line_next_states(self): + res = super()._scan_line_next_states() + res.update({"set_packaging_dimension"}) + return res + + def _set_lot_confirm_action_next_states(self): + res = super()._set_lot_confirm_action_next_states() + res.update({"set_packaging_dimension"}) + return res + + @property + def _schema_set_packaging_dimension(self): + return { + "picking": {"type": "dict", "schema": self.schemas.picking()}, + "selected_move_line": {"type": "dict", "schema": self.schemas.move_line()}, + "packaging": { + "type": "dict", + "schema": self.schemas_detail.packaging_detail(), + }, + } diff --git a/shopfloor_reception_packaging_dimension/tests/__init__.py b/shopfloor_reception_packaging_dimension/tests/__init__.py new file mode 100644 index 0000000000..2c166ca4fd --- /dev/null +++ b/shopfloor_reception_packaging_dimension/tests/__init__.py @@ -0,0 +1 @@ +from . import test_set_package_dimension diff --git a/shopfloor_reception_packaging_dimension/tests/test_set_package_dimension.py b/shopfloor_reception_packaging_dimension/tests/test_set_package_dimension.py new file mode 100644 index 0000000000..52248ee2a1 --- /dev/null +++ b/shopfloor_reception_packaging_dimension/tests/test_set_package_dimension.py @@ -0,0 +1,160 @@ +# Copyright 2023 Camptocamp SA +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl) + +from odoo.addons.shopfloor_reception.tests.common import CommonCase + + +class TestSetPackDimension(CommonCase): + @classmethod + def setUpClassBaseData(cls): + super().setUpClassBaseData() + # Activate the option to use the module + cls.menu.sudo().set_packaging_dimension = True + cls.picking = cls._create_picking( + lines=[(cls.product_a, 10), (cls.product_b, 10), (cls.product_c, 10)] + ) + # Picking has 3 products + # Product A with one packaging + # Product B with no packaging + cls.product_b.packaging_ids = [(5, 0, 0)] + # Product C with 2 packaging + cls.product_c_packaging_2 = ( + cls.env["product.packaging"] + .sudo() + .create( + { + "name": "Big Box", + "product_id": cls.product_c.id, + "barcode": "ProductCBigBox", + "qty": 6, + } + ) + ) + + cls.line_with_packaging = cls.picking.move_line_ids[0] + cls.line_without_packaging = cls.picking.move_line_ids[1] + + def _assert_response_set_dimension( + self, response, picking, line, packaging, message=None + ): + data = { + "picking": self.data.picking(picking), + "selected_move_line": self.data.move_line(line), + "packaging": self.data_detail.packaging_detail(packaging), + } + self.assert_response( + response, + next_state="set_packaging_dimension", + data=data, + message=message, + ) + + def test_scan_product_ask_for_dimension(self): + self.product_a.tracking = "none" + # self._add_package(self.picking) + self.assertTrue(self.product_a.packaging_ids) + response = self.service.dispatch( + "scan_line", + params={ + "picking_id": self.picking.id, + "barcode": self.product_a.barcode, + }, + ) + self.data.picking(self.picking) + selected_move_line = self.picking.move_line_ids.filtered( + lambda l: l.product_id == self.product_a + ) + self._assert_response_set_dimension( + response, self.picking, selected_move_line, self.product_a_packaging + ) + + # TODO : Have questions on how the lot information is commited by the frontend + # + # def test_scan_lot_ask_for_dimension(self): + # self.product_a.tracking = "none" + # self.assertTrue(self.product_a.packaging_ids) + # response = self.service.dispatch( + # "set_lot_confirmation_action", + # params={ + # "picking_id": self.picking.id, + # "barcode": self.product_a.barcode, + # }, + # ) + # self.data.picking(self.picking) + # selected_move_line = self.picking.move_line_ids.filtered( + # lambda l: l.product_id == self.product_a + # ) + # self._assert_response_set_dimension( + # response, self.picking, selected_move_line, self.product_a_packaging + # ) + + def test_set_packaging_dimension(self): + selected_move_line = self.picking.move_line_ids.filtered( + lambda l: l.product_id == self.product_a + ) + self.service.dispatch( + "set_packaging_dimension", + params={ + "picking_id": self.picking.id, + "selected_line_id": selected_move_line.id, + "packaging_id": self.product_a_packaging.id, + "height": 55, + "qty": 34, + "barcode": "barcode", + }, + ) + self.assertEqual(self.product_a_packaging.height, 55) + self.assertEqual(self.product_a_packaging.barcode, "barcode") + self.assertEqual(self.product_a_packaging.qty, 34) + + def test_set_multiple_packaging_dimension(self): + line = self.picking.move_line_ids.filtered( + lambda l: l.product_id == self.product_c + ) + # Set the weight but other dimension are required + self.product_c_packaging_2.max_weight = 200 + response = self.service.dispatch( + "set_packaging_dimension", + params={ + "picking_id": self.picking.id, + "selected_line_id": line.id, + "packaging_id": self.product_c_packaging.id, + "height": 55, + "length": 233, + }, + ) + self.assertEqual(self.product_c_packaging.height, 55) + self.assertEqual(self.product_c_packaging.packaging_length, 233) + self._assert_response_set_dimension( + response, + self.picking, + line, + self.product_c_packaging_2, + message=self.msg_store.packaging_dimension_updated( + self.product_c_packaging + ), + ) + response = self.service.dispatch( + "set_packaging_dimension", + params={ + "picking_id": self.picking.id, + "selected_line_id": line.id, + "packaging_id": self.product_c_packaging_2.id, + "height": 200, + "max_weight": 1000, + }, + ) + self.assertEqual(self.product_c_packaging_2.height, 200) + self.assertEqual(self.product_c_packaging_2.max_weight, 1000) + self.assert_response( + response, + next_state="set_quantity", + data={ + "picking": self.data.picking(self.picking), + "selected_move_line": self.data.move_lines(line), + "confirmation_required": False, + }, + message=self.msg_store.packaging_dimension_updated( + self.product_c_packaging_2 + ), + ) diff --git a/shopfloor_reception_packaging_dimension/views/shopfloor_menu.xml b/shopfloor_reception_packaging_dimension/views/shopfloor_menu.xml new file mode 100644 index 0000000000..9c0f650240 --- /dev/null +++ b/shopfloor_reception_packaging_dimension/views/shopfloor_menu.xml @@ -0,0 +1,20 @@ + + + + shopfloor.menu + + + + + + + + + + + + + From 273c525a75ed5bdb3742a004bfdc63a578730a1c Mon Sep 17 00:00:00 2001 From: Thierry Ducrest Date: Wed, 7 Jun 2023 05:18:46 +0200 Subject: [PATCH 5/7] Add shopfloor_reception_packaging_dimension_mobile --- ...floor_reception_packaging_dimension_mobile | 1 + .../setup.py | 6 + .../README.rst | 79 +++++++++ .../__init__.py | 0 .../__manifest__.py | 19 +++ .../readme/CONTRIBUTORS.rst | 1 + .../readme/DESCRIPTION.rst | 4 + .../scenario/reception_packaging_dimension.js | 150 ++++++++++++++++++ .../templates/assets.xml | 25 +++ 9 files changed, 285 insertions(+) create mode 120000 setup/shopfloor_reception_packaging_dimension_mobile/odoo/addons/shopfloor_reception_packaging_dimension_mobile create mode 100644 setup/shopfloor_reception_packaging_dimension_mobile/setup.py create mode 100644 shopfloor_reception_packaging_dimension_mobile/README.rst create mode 100644 shopfloor_reception_packaging_dimension_mobile/__init__.py create mode 100644 shopfloor_reception_packaging_dimension_mobile/__manifest__.py create mode 100644 shopfloor_reception_packaging_dimension_mobile/readme/CONTRIBUTORS.rst create mode 100644 shopfloor_reception_packaging_dimension_mobile/readme/DESCRIPTION.rst create mode 100644 shopfloor_reception_packaging_dimension_mobile/static/src/scenario/reception_packaging_dimension.js create mode 100644 shopfloor_reception_packaging_dimension_mobile/templates/assets.xml diff --git a/setup/shopfloor_reception_packaging_dimension_mobile/odoo/addons/shopfloor_reception_packaging_dimension_mobile b/setup/shopfloor_reception_packaging_dimension_mobile/odoo/addons/shopfloor_reception_packaging_dimension_mobile new file mode 120000 index 0000000000..a9ab8c21f8 --- /dev/null +++ b/setup/shopfloor_reception_packaging_dimension_mobile/odoo/addons/shopfloor_reception_packaging_dimension_mobile @@ -0,0 +1 @@ +../../../../shopfloor_reception_packaging_dimension_mobile \ No newline at end of file diff --git a/setup/shopfloor_reception_packaging_dimension_mobile/setup.py b/setup/shopfloor_reception_packaging_dimension_mobile/setup.py new file mode 100644 index 0000000000..28c57bb640 --- /dev/null +++ b/setup/shopfloor_reception_packaging_dimension_mobile/setup.py @@ -0,0 +1,6 @@ +import setuptools + +setuptools.setup( + setup_requires=['setuptools-odoo'], + odoo_addon=True, +) diff --git a/shopfloor_reception_packaging_dimension_mobile/README.rst b/shopfloor_reception_packaging_dimension_mobile/README.rst new file mode 100644 index 0000000000..326943dfe9 --- /dev/null +++ b/shopfloor_reception_packaging_dimension_mobile/README.rst @@ -0,0 +1,79 @@ +============================================= +Shopfloor Checkout Package Measurement Mobile +============================================= + +.. !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! This file is generated by oca-gen-addon-readme !! + !! changes will be overwritten. !! + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + +.. |badge1| image:: https://img.shields.io/badge/maturity-Alpha-red.png + :target: https://odoo-community.org/page/development-status + :alt: Alpha +.. |badge2| image:: https://img.shields.io/badge/licence-AGPL--3-blue.png + :target: http://www.gnu.org/licenses/agpl-3.0-standalone.html + :alt: License: AGPL-3 +.. |badge3| image:: https://img.shields.io/badge/github-OCA%2Fwms-lightgray.png?logo=github + :target: https://github.com/OCA/wms/tree/14.0/shopfloor_checkout_package_measurement_mobile + :alt: OCA/wms +.. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png + :target: https://translation.odoo-community.org/projects/wms-14-0/wms-14-0-shopfloor_checkout_package_measurement_mobile + :alt: Translate me on Weblate +.. |badge5| image:: https://img.shields.io/badge/runbot-Try%20me-875A7B.png + :target: https://runbot.odoo-community.org/runbot/285/14.0 + :alt: Try me on Runbot + +|badge1| |badge2| |badge3| |badge4| |badge5| + +This module adds the front end part for the `shopfloor_checkout_package_measurement` module. +Allowing to set package measurements from the mobile shopfloor application. + +.. IMPORTANT:: + This is an alpha version, the data model and design can change at any time without warning. + Only for development or testing purpose, do not use in production. + `More details on development status `_ + +**Table of contents** + +.. contents:: + :local: + +Bug Tracker +=========== + +Bugs are tracked on `GitHub Issues `_. +In case of trouble, please check there if your issue has already been reported. +If you spotted it first, help us smashing it by providing a detailed and welcomed +`feedback `_. + +Do not contact contributors directly about support or help with technical issues. + +Credits +======= + +Authors +~~~~~~~ + +* Camptocamp + +Contributors +~~~~~~~~~~~~ + +* Thierry Ducrest + +Maintainers +~~~~~~~~~~~ + +This module is maintained by the OCA. + +.. image:: https://odoo-community.org/logo.png + :alt: Odoo Community Association + :target: https://odoo-community.org + +OCA, or the Odoo Community Association, is a nonprofit organization whose +mission is to support the collaborative development of Odoo features and +promote its widespread use. + +This module is part of the `OCA/wms `_ project on GitHub. + +You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute. diff --git a/shopfloor_reception_packaging_dimension_mobile/__init__.py b/shopfloor_reception_packaging_dimension_mobile/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/shopfloor_reception_packaging_dimension_mobile/__manifest__.py b/shopfloor_reception_packaging_dimension_mobile/__manifest__.py new file mode 100644 index 0000000000..503602e737 --- /dev/null +++ b/shopfloor_reception_packaging_dimension_mobile/__manifest__.py @@ -0,0 +1,19 @@ +# Copyright 2023 Camptocamp SA +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl) + +{ + "name": "Shopfloor Reception Packaging Dimension Mobile", + "summary": "Frontend for the packaging dimension on reception scenario", + "version": "14.0.1.0.0", + "development_status": "Alpha", + "category": "Inventory", + "website": "https://github.com/OCA/wms", + "author": "Camptocamp, Odoo Community Association (OCA)", + "maintainers": ["TDu"], + "license": "AGPL-3", + "depends": [ + "shopfloor_reception_mobile", + "shopfloor_reception_packaging_dimension", + ], + "data": ["templates/assets.xml"], +} diff --git a/shopfloor_reception_packaging_dimension_mobile/readme/CONTRIBUTORS.rst b/shopfloor_reception_packaging_dimension_mobile/readme/CONTRIBUTORS.rst new file mode 100644 index 0000000000..0dd376faec --- /dev/null +++ b/shopfloor_reception_packaging_dimension_mobile/readme/CONTRIBUTORS.rst @@ -0,0 +1 @@ +* Thierry Ducrest diff --git a/shopfloor_reception_packaging_dimension_mobile/readme/DESCRIPTION.rst b/shopfloor_reception_packaging_dimension_mobile/readme/DESCRIPTION.rst new file mode 100644 index 0000000000..ab7978b561 --- /dev/null +++ b/shopfloor_reception_packaging_dimension_mobile/readme/DESCRIPTION.rst @@ -0,0 +1,4 @@ +This module adds the front end part for the `shopfloor_reception_packaging_dimension` +module. Allowing to set dimension on packaging related to the product being processed, +if they are not set already. +The option needs to be enable on the shopfloor menu. diff --git a/shopfloor_reception_packaging_dimension_mobile/static/src/scenario/reception_packaging_dimension.js b/shopfloor_reception_packaging_dimension_mobile/static/src/scenario/reception_packaging_dimension.js new file mode 100644 index 0000000000..dc75b3f975 --- /dev/null +++ b/shopfloor_reception_packaging_dimension_mobile/static/src/scenario/reception_packaging_dimension.js @@ -0,0 +1,150 @@ +/** + * Copyright 2023 Camptocamp SA (http://www.camptocamp.com) + * License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). + */ + +import {process_registry} from "/shopfloor_mobile_base/static/wms/src/services/process_registry.js"; + +import {reception_states} from "/shopfloor_reception_mobile/static/src/scenario/reception_states.js"; + +// Get the original template of the reception scenario +const reception_scenario = process_registry.get("reception"); +const template = reception_scenario.component.template; +// And inject the new state template (for this module) into it +const pos = template.indexOf(""); + +const new_template = + template.substring(0, pos) + + ` +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + Done + + + + + + Skip + + + +
+
+ +` + + template.substring(pos); + +// Extend the reception scenario with : +// - the new patched template +// - the js code for the new state +const ReceptionPackageDimension = process_registry.extend("reception", { + template: new_template, + "methods._get_states": function () { + let states = reception_states.bind(this)(); + states["set_packaging_dimension"] = { + display_info: { + title: "Set packaging dimension", + }, + events: { + go_back: "on_back", + }, + get_payload_set_packaging_dimension: () => { + let values = { + picking_id: this.state.data.picking.id, + selected_line_id: this.state.data.selected_move_line.id, + packaging_id: this.state.data.packaging.id, + }; + const measurements = [ + "length", + "width", + "height", + "max_weight", + "qty", + "barcode", + ]; + for (const measurement of measurements) { + values[measurement] = this.state.data.packaging[measurement]; + } + return values; + }, + on_skip: () => { + const payload = this.state.get_payload_set_packaging_dimension(); + payload["cancel"] = true; + this.wait_call(this.odoo.call("set_packaging_dimension", payload)); + }, + on_done: () => { + const payload = this.state.get_payload_set_packaging_dimension(); + this.wait_call(this.odoo.call("set_packaging_dimension", payload)); + }, + }; + return states; + }, +}); + +process_registry.replace("reception", ReceptionPackageDimension); diff --git a/shopfloor_reception_packaging_dimension_mobile/templates/assets.xml b/shopfloor_reception_packaging_dimension_mobile/templates/assets.xml new file mode 100644 index 0000000000..000d0fa522 --- /dev/null +++ b/shopfloor_reception_packaging_dimension_mobile/templates/assets.xml @@ -0,0 +1,25 @@ + + + + + + + From 0271954766d3629571532bd4e6ef8c07f384bbb8 Mon Sep 17 00:00:00 2001 From: Thierry Ducrest Date: Tue, 11 Jul 2023 08:06:26 +0200 Subject: [PATCH 6/7] sh_reception_packaging_dimension: refactor to allow module to extend --- .../services/reception.py | 44 +++++++++---------- .../tests/test_set_package_dimension.py | 39 ++++++++-------- 2 files changed, 42 insertions(+), 41 deletions(-) diff --git a/shopfloor_reception_packaging_dimension/services/reception.py b/shopfloor_reception_packaging_dimension/services/reception.py index ccb43afa54..b87458df11 100644 --- a/shopfloor_reception_packaging_dimension/services/reception.py +++ b/shopfloor_reception_packaging_dimension/services/reception.py @@ -1,6 +1,8 @@ # Copyright 2023 Camptocamp SA # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl) +from odoo.osv import expression + from odoo.addons.base_rest.components.service import to_int from odoo.addons.component.core import Component from odoo.addons.shopfloor.utils import to_float @@ -21,34 +23,32 @@ def _before_state__set_quantity(self, picking, line, message=None): ) return super()._before_state__set_quantity(picking, line, message=message) + def _get_domain_packaging_needs_dimension(self): + return expression.OR( + [ + [("packaging_length", "=", 0)], + [("packaging_length", "=", False)], + [("width", "=", 0)], + [("width", "=", False)], + [("height", "=", 0)], + [("height", "=", False)], + [("max_weight", "=", 0)], + [("max_weight", "=", False)], + [("qty", "=", 0)], + [("qty", "=", False)], + [("barcode", "=", False)], + ] + ) + def _get_next_packaging_to_set_dimension(self, product, previous_packaging=None): """Return for a product the next packaging needing dimension to be set.""" next_packaging_id = previous_packaging.id + 1 if previous_packaging else 0 - domain = [ + domain_dimension = self._get_domain_packaging_needs_dimension() + domain_packaging_id = [ ("product_id", "=", product.id), ("id", ">=", next_packaging_id), - "|", - "|", - "|", - "|", - "|", - "|", - "|", - "|", - "|", - "|", - ("packaging_length", "=", 0), - ("packaging_length", "=", False), - ("width", "=", 0), - ("width", "=", False), - ("height", "=", 0), - ("height", "=", False), - ("max_weight", "=", 0), - ("max_weight", "=", False), - ("qty", "=", 0), - ("qty", "=", False), - ("barcode", "=", False), ] + domain = expression.AND([domain_packaging_id, domain_dimension]) return self.env["product.packaging"].search(domain, order="id", limit=1) def _response_for_set_packaging_dimension( diff --git a/shopfloor_reception_packaging_dimension/tests/test_set_package_dimension.py b/shopfloor_reception_packaging_dimension/tests/test_set_package_dimension.py index 52248ee2a1..64c6fae553 100644 --- a/shopfloor_reception_packaging_dimension/tests/test_set_package_dimension.py +++ b/shopfloor_reception_packaging_dimension/tests/test_set_package_dimension.py @@ -68,25 +68,26 @@ def test_scan_product_ask_for_dimension(self): response, self.picking, selected_move_line, self.product_a_packaging ) - # TODO : Have questions on how the lot information is commited by the frontend - # - # def test_scan_lot_ask_for_dimension(self): - # self.product_a.tracking = "none" - # self.assertTrue(self.product_a.packaging_ids) - # response = self.service.dispatch( - # "set_lot_confirmation_action", - # params={ - # "picking_id": self.picking.id, - # "barcode": self.product_a.barcode, - # }, - # ) - # self.data.picking(self.picking) - # selected_move_line = self.picking.move_line_ids.filtered( - # lambda l: l.product_id == self.product_a - # ) - # self._assert_response_set_dimension( - # response, self.picking, selected_move_line, self.product_a_packaging - # ) + def test_scan_lot_ask_for_dimension(self): + self.product_a.tracking = "none" + selected_move_line = self.picking.move_line_ids.filtered( + lambda l: l.product_id == self.product_a + ) + self.assertTrue(self.product_a.packaging_ids) + response = self.service.dispatch( + "set_lot_confirm_action", + params={ + "picking_id": self.picking.id, + "selected_line_id": selected_move_line.id, + }, + ) + self.data.picking(self.picking) + selected_move_line = self.picking.move_line_ids.filtered( + lambda l: l.product_id == self.product_a + ) + self._assert_response_set_dimension( + response, self.picking, selected_move_line, self.product_a_packaging + ) def test_set_packaging_dimension(self): selected_move_line = self.picking.move_line_ids.filtered( From 1d73bae9def71e9484f1a376ca74e0a1adf40377 Mon Sep 17 00:00:00 2001 From: Thierry Ducrest Date: Tue, 11 Jul 2023 13:53:58 +0200 Subject: [PATCH 7/7] sh_reception_packaging_dimension_mobile: refactor for extensibility --- .../scenario/reception_packaging_dimension.js | 23 +++++++------------ 1 file changed, 8 insertions(+), 15 deletions(-) diff --git a/shopfloor_reception_packaging_dimension_mobile/static/src/scenario/reception_packaging_dimension.js b/shopfloor_reception_packaging_dimension_mobile/static/src/scenario/reception_packaging_dimension.js index dc75b3f975..980adbbb2a 100644 --- a/shopfloor_reception_packaging_dimension_mobile/static/src/scenario/reception_packaging_dimension.js +++ b/shopfloor_reception_packaging_dimension_mobile/static/src/scenario/reception_packaging_dimension.js @@ -5,14 +5,12 @@ import {process_registry} from "/shopfloor_mobile_base/static/wms/src/services/process_registry.js"; -import {reception_states} from "/shopfloor_reception_mobile/static/src/scenario/reception_states.js"; - -// Get the original template of the reception scenario const reception_scenario = process_registry.get("reception"); +const _get_states = reception_scenario.component.methods._get_states; +// Get the original template of the reception scenario const template = reception_scenario.component.template; // And inject the new state template (for this module) into it const pos = template.indexOf(""); - const new_template = template.substring(0, pos) + ` @@ -77,7 +75,7 @@ const new_template = v-model="state.data.packaging.max_weight" > - + @@ -105,8 +103,11 @@ const new_template = // - the js code for the new state const ReceptionPackageDimension = process_registry.extend("reception", { template: new_template, + "methods.get_packaging_measurements": function () { + return ["length", "width", "height", "max_weight", "qty", "barcode"]; + }, "methods._get_states": function () { - let states = reception_states.bind(this)(); + let states = _get_states.bind(this)(); states["set_packaging_dimension"] = { display_info: { title: "Set packaging dimension", @@ -120,15 +121,7 @@ const ReceptionPackageDimension = process_registry.extend("reception", { selected_line_id: this.state.data.selected_move_line.id, packaging_id: this.state.data.packaging.id, }; - const measurements = [ - "length", - "width", - "height", - "max_weight", - "qty", - "barcode", - ]; - for (const measurement of measurements) { + for (const measurement of this.get_packaging_measurements()) { values[measurement] = this.state.data.packaging[measurement]; } return values;