From 5dda7e791f7a9467a04e2c846d8501279212734f Mon Sep 17 00:00:00 2001 From: sbejaoui Date: Thu, 10 Aug 2023 16:13:26 +0200 Subject: [PATCH 01/20] [16.0][ADD] - shopfloor_packing --- .eslintignore | 1 + .../odoo/addons/shopfloor_mobile_packing | 1 + setup/shopfloor_mobile_packing/setup.py | 6 + .../odoo/addons/shopfloor_packing | 1 + setup/shopfloor_packing/setup.py | 6 + shopfloor_mobile_packing/README.rst | 73 +++ shopfloor_mobile_packing/__init__.py | 0 shopfloor_mobile_packing/__manifest__.py | 18 + .../readme/CONTRIBUTORS.rst | 1 + .../readme/DESCRIPTION.rst | 1 + .../static/description/index.html | 419 +++++++++++++++++ .../static/src/js/cluster-picking.js | 86 ++++ .../src/js/components/pack_picking_detail.js | 94 ++++ .../static/src/js/i18n/en.json | 6 + .../static/src/js/i18n/fr.json | 6 + .../static/src/js/translation_registry.js | 10 + shopfloor_mobile_packing/templates/assets.xml | 27 ++ shopfloor_packing/README.rst | 79 ++++ shopfloor_packing/__init__.py | 3 + shopfloor_packing/__manifest__.py | 20 + shopfloor_packing/actions/__init__.py | 5 + shopfloor_packing/actions/data.py | 14 + shopfloor_packing/actions/data_detail.py | 32 ++ shopfloor_packing/actions/message.py | 49 ++ shopfloor_packing/actions/schema.py | 14 + shopfloor_packing/actions/schema_detail.py | 48 ++ shopfloor_packing/i18n/fr_BE.po | 95 ++++ shopfloor_packing/models/__init__.py | 3 + shopfloor_packing/models/shopfloor_menu.py | 15 + shopfloor_packing/models/stock_picking.py | 61 +++ .../models/stock_picking_batch.py | 21 + shopfloor_packing/readme/CONTRIBUTORS.rst | 1 + shopfloor_packing/readme/DESCRIPTION.rst | 2 + shopfloor_packing/readme/USAGE.rst | 1 + shopfloor_packing/services/__init__.py | 1 + shopfloor_packing/services/cluster_picking.py | 266 +++++++++++ .../static/description/index.html | 425 ++++++++++++++++++ shopfloor_packing/tests/__init__.py | 2 + .../test_cluster_picking_pack_picking.py | 342 ++++++++++++++ .../tests/test_cluster_picking_unload.py | 50 +++ shopfloor_packing/views/shopfloor_menu.xml | 23 + shopfloor_packing/views/stock_picking.xml | 18 + 42 files changed, 2346 insertions(+) create mode 100644 .eslintignore create mode 120000 setup/shopfloor_mobile_packing/odoo/addons/shopfloor_mobile_packing create mode 100644 setup/shopfloor_mobile_packing/setup.py create mode 120000 setup/shopfloor_packing/odoo/addons/shopfloor_packing create mode 100644 setup/shopfloor_packing/setup.py create mode 100644 shopfloor_mobile_packing/README.rst create mode 100644 shopfloor_mobile_packing/__init__.py create mode 100644 shopfloor_mobile_packing/__manifest__.py create mode 100644 shopfloor_mobile_packing/readme/CONTRIBUTORS.rst create mode 100644 shopfloor_mobile_packing/readme/DESCRIPTION.rst create mode 100644 shopfloor_mobile_packing/static/description/index.html create mode 100644 shopfloor_mobile_packing/static/src/js/cluster-picking.js create mode 100644 shopfloor_mobile_packing/static/src/js/components/pack_picking_detail.js create mode 100644 shopfloor_mobile_packing/static/src/js/i18n/en.json create mode 100644 shopfloor_mobile_packing/static/src/js/i18n/fr.json create mode 100644 shopfloor_mobile_packing/static/src/js/translation_registry.js create mode 100644 shopfloor_mobile_packing/templates/assets.xml create mode 100644 shopfloor_packing/README.rst create mode 100644 shopfloor_packing/__init__.py create mode 100644 shopfloor_packing/__manifest__.py create mode 100644 shopfloor_packing/actions/__init__.py create mode 100644 shopfloor_packing/actions/data.py create mode 100644 shopfloor_packing/actions/data_detail.py create mode 100644 shopfloor_packing/actions/message.py create mode 100644 shopfloor_packing/actions/schema.py create mode 100644 shopfloor_packing/actions/schema_detail.py create mode 100644 shopfloor_packing/i18n/fr_BE.po create mode 100644 shopfloor_packing/models/__init__.py create mode 100644 shopfloor_packing/models/shopfloor_menu.py create mode 100644 shopfloor_packing/models/stock_picking.py create mode 100644 shopfloor_packing/models/stock_picking_batch.py create mode 100644 shopfloor_packing/readme/CONTRIBUTORS.rst create mode 100644 shopfloor_packing/readme/DESCRIPTION.rst create mode 100644 shopfloor_packing/readme/USAGE.rst create mode 100644 shopfloor_packing/services/__init__.py create mode 100644 shopfloor_packing/services/cluster_picking.py create mode 100644 shopfloor_packing/static/description/index.html create mode 100644 shopfloor_packing/tests/__init__.py create mode 100644 shopfloor_packing/tests/test_cluster_picking_pack_picking.py create mode 100644 shopfloor_packing/tests/test_cluster_picking_unload.py create mode 100644 shopfloor_packing/views/shopfloor_menu.xml create mode 100644 shopfloor_packing/views/stock_picking.xml diff --git a/.eslintignore b/.eslintignore new file mode 100644 index 00000000000..c18d2841f0b --- /dev/null +++ b/.eslintignore @@ -0,0 +1 @@ +shopfloor_mobile_packing/static/src/js diff --git a/setup/shopfloor_mobile_packing/odoo/addons/shopfloor_mobile_packing b/setup/shopfloor_mobile_packing/odoo/addons/shopfloor_mobile_packing new file mode 120000 index 00000000000..0069ba2870a --- /dev/null +++ b/setup/shopfloor_mobile_packing/odoo/addons/shopfloor_mobile_packing @@ -0,0 +1 @@ +../../../../shopfloor_mobile_packing \ No newline at end of file diff --git a/setup/shopfloor_mobile_packing/setup.py b/setup/shopfloor_mobile_packing/setup.py new file mode 100644 index 00000000000..28c57bb6403 --- /dev/null +++ b/setup/shopfloor_mobile_packing/setup.py @@ -0,0 +1,6 @@ +import setuptools + +setuptools.setup( + setup_requires=['setuptools-odoo'], + odoo_addon=True, +) diff --git a/setup/shopfloor_packing/odoo/addons/shopfloor_packing b/setup/shopfloor_packing/odoo/addons/shopfloor_packing new file mode 120000 index 00000000000..a02fb6c0aaf --- /dev/null +++ b/setup/shopfloor_packing/odoo/addons/shopfloor_packing @@ -0,0 +1 @@ +../../../../shopfloor_packing \ No newline at end of file diff --git a/setup/shopfloor_packing/setup.py b/setup/shopfloor_packing/setup.py new file mode 100644 index 00000000000..28c57bb6403 --- /dev/null +++ b/setup/shopfloor_packing/setup.py @@ -0,0 +1,6 @@ +import setuptools + +setuptools.setup( + setup_requires=['setuptools-odoo'], + odoo_addon=True, +) diff --git a/shopfloor_mobile_packing/README.rst b/shopfloor_mobile_packing/README.rst new file mode 100644 index 00000000000..c61602c7f8d --- /dev/null +++ b/shopfloor_mobile_packing/README.rst @@ -0,0 +1,73 @@ +======================== +Shopfloor Mobile Packing +======================== + +.. !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! This file is generated by oca-gen-addon-readme !! + !! changes will be overwritten. !! + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + +.. |badge1| image:: https://img.shields.io/badge/maturity-Beta-yellow.png + :target: https://odoo-community.org/page/development-status + :alt: Beta +.. |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/16.0/shopfloor_mobile_packing + :alt: OCA/wms +.. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png + :target: https://translation.odoo-community.org/projects/wms-16-0/wms-16-0-shopfloor_mobile_packing + :alt: Translate me on Weblate +.. |badge5| image:: https://img.shields.io/badge/runboat-Try%20me-875A7B.png + :target: https://runboat.odoo-community.org/webui/builds.html?repo=OCA/wms&target_branch=16.0 + :alt: Try me on Runboat + +|badge1| |badge2| |badge3| |badge4| |badge5| + +Shopfloor mobile extension for packing operation into cluster picking. + +**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 +~~~~~~~ + +* ACSONE SA/NV + +Contributors +~~~~~~~~~~~~ + +* Souheil Bejaoui + +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_mobile_packing/__init__.py b/shopfloor_mobile_packing/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/shopfloor_mobile_packing/__manifest__.py b/shopfloor_mobile_packing/__manifest__.py new file mode 100644 index 00000000000..b421f7e36ca --- /dev/null +++ b/shopfloor_mobile_packing/__manifest__.py @@ -0,0 +1,18 @@ +# Copyright 2021 ACSONE SA/NV +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +{ + "name": "Shopfloor Mobile Packing", + "version": "16.0.1.0.0", + "summary": """ + Shopfloor mobile extension for packing operation into cluster picking + """, + "author": "ACSONE SA/NV,Odoo Community Association (OCA)", + "website": "https://github.com/OCA/wms", + "category": "Stock Management", + "depends": ["shopfloor_mobile", "shopfloor_packing"], + "data": ["templates/assets.xml"], + "installable": True, + "license": "AGPL-3", + "application": False, +} diff --git a/shopfloor_mobile_packing/readme/CONTRIBUTORS.rst b/shopfloor_mobile_packing/readme/CONTRIBUTORS.rst new file mode 100644 index 00000000000..50e6298db56 --- /dev/null +++ b/shopfloor_mobile_packing/readme/CONTRIBUTORS.rst @@ -0,0 +1 @@ +* Souheil Bejaoui diff --git a/shopfloor_mobile_packing/readme/DESCRIPTION.rst b/shopfloor_mobile_packing/readme/DESCRIPTION.rst new file mode 100644 index 00000000000..a5c474fc17e --- /dev/null +++ b/shopfloor_mobile_packing/readme/DESCRIPTION.rst @@ -0,0 +1 @@ +Shopfloor mobile extension for packing operation into cluster picking. diff --git a/shopfloor_mobile_packing/static/description/index.html b/shopfloor_mobile_packing/static/description/index.html new file mode 100644 index 00000000000..49445c23b4d --- /dev/null +++ b/shopfloor_mobile_packing/static/description/index.html @@ -0,0 +1,419 @@ + + + + + + +Shopfloor Mobile Packing + + + +
+

Shopfloor Mobile Packing

+ + +

Beta License: AGPL-3 OCA/wms Translate me on Weblate Try me on Runboat

+

Shopfloor mobile extension for packing operation into cluster picking.

+

Table of contents

+ +
+

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

+
    +
  • ACSONE SA/NV
  • +
+
+
+

Contributors

+ +
+
+

Maintainers

+

This module is maintained by the OCA.

+Odoo Community Association +

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_mobile_packing/static/src/js/cluster-picking.js b/shopfloor_mobile_packing/static/src/js/cluster-picking.js new file mode 100644 index 00000000000..cb9ed73a115 --- /dev/null +++ b/shopfloor_mobile_packing/static/src/js/cluster-picking.js @@ -0,0 +1,86 @@ +/** + * Copyright 2021 ACSONE SA/NV + * 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"; + +const registry_key = "cluster_picking"; +const ClusterPickingBase = process_registry.get(registry_key); + +let template = ClusterPickingBase.component.template; +ClusterPickingBase.component.template = template.replace( + "", + ` + + + +` +); + +// Keep the pointer to the orginal method +let data_result_method = ClusterPickingBase.component.data; + +ClusterPickingBase.component.computed.searchbar_input_type = function () { + if (this.state_is("pack_picking_put_in_pack")) { + return "number"; + } + return "text"; +}; + +// Replace the data method with our new method to add +// our new state +let component = ClusterPickingBase.component; +let data = function () { + // we must bin the original method to this to put it into + // the object context + let result = data_result_method.bind(this)(); + // add our new state + result.states.pack_picking_put_in_pack = { + display_info: { + title: this.$t("cluster_picking.pack_picking_put_in_pack.title"), + scan_placeholder: this.$t( + "cluster_picking.pack_picking_put_in_pack.scan_placeholder" + ), + }, + on_scan: (scanned) => { + let endpoint, endpoint_data; + const data = this.state.data; + endpoint = "put_in_pack"; + endpoint_data = { + picking_batch_id: this.current_batch().id, + picking_id: data.id, + nbr_packages: parseInt(scanned.text, 10), + }; + this.wait_call(this.odoo.call(endpoint, endpoint_data)); + }, + }; + result.states.pack_picking_scan_pack = { + display_info: { + title: this.$t("cluster_picking.pack_picking_scan_pack.title"), + scan_placeholder: this.$t( + "cluster_picking.pack_picking_scan_pack.scan_placeholder" + ), + }, + on_scan: (scanned) => { + let endpoint, endpoint_data; + const data = this.state.data; + endpoint = "scan_packing_to_pack"; + endpoint_data = { + picking_batch_id: this.current_batch().id, + picking_id: data.id, + barcode: scanned.text, + }; + this.wait_call(this.odoo.call(endpoint, endpoint_data)); + }, + }; + return result; +}; + +ClusterPickingBase.component.data = data; diff --git a/shopfloor_mobile_packing/static/src/js/components/pack_picking_detail.js b/shopfloor_mobile_packing/static/src/js/components/pack_picking_detail.js new file mode 100644 index 00000000000..89827dfb46a --- /dev/null +++ b/shopfloor_mobile_packing/static/src/js/components/pack_picking_detail.js @@ -0,0 +1,94 @@ +/** + * Copyright 2021 ACSONE SA/NV + * License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). + */ + +/* eslint-disable strict */ +Vue.component("pack-picking-detail", { + props: ["record"], + methods: { + move_lines_color_klass(rec) { + let line = rec; + if (line._is_group) { + line = line.records[0]; + } + let klass = ""; + if (this.record.scanned_packs.includes(line.package_dest.id)) { + klass = "done screen_step_done lighten-1"; + } else { + klass = "not-done screen_step_todo lighten-1"; + } + return "move-line-" + klass; + }, + line_list_options() { + return { + card_klass: "loud-labels", + key_title: "", + showCounters: false, + list_item_options: { + fields: this.line_list_fields(), + list_item_klass_maker: this.move_lines_color_klass, + }, + }; + }, + line_list_fields() { + self = this; + return [ + { + path: "product.display_name", + action_val_path: "product.default_code", + klass: "loud", + }, + { + path: "package_src.name", + label: "Pack", + action_val_path: "package_src.name", + }, + {path: "lot.name", label: "Lot", action_val_path: "lot.name"}, + { + path: "qty_done", + label: "Qty", + render_component: "packaging-qty-picker-display", + render_props: function (record) { + return self.utils.wms.move_line_qty_picker_props(record, { + qtyInit: record.qty_done, + }); + }, + }, + ]; + }, + grouped_lines() { + const groups = this.utils.wms.group_by_pack( + this.record.move_lines.filter((op) => { + if (op.package_dest != null && op.package_dest.is_internal) { + return op; + } + }) + ); + const self = this; + _.forEach(groups, function (item) { + item.group_color = self.record.scanned_packs.includes(item.pack.id) + ? self.utils.colors.color_for("screen_step_done") + : self.utils.colors.color_for("screen_step_todo"); + }); + return groups; + }, + }, + template: ` +
+ + +
+ {{ record.name }} : {{ record.partner.name }} +
+
+
+ +
+ +`, +}); diff --git a/shopfloor_mobile_packing/static/src/js/i18n/en.json b/shopfloor_mobile_packing/static/src/js/i18n/en.json new file mode 100644 index 00000000000..440c46a98aa --- /dev/null +++ b/shopfloor_mobile_packing/static/src/js/i18n/en.json @@ -0,0 +1,6 @@ +{ + "cluster_picking.pack_picking_put_in_pack.title": "Packing: Enter the number of boxes used.", + "cluster_picking.pack_picking_put_in_pack.scan_placeholder": "Number of boxes used", + "cluster_picking.pack_picking_scan_pack.title": "Packing: Validate bin to pack.", + "cluster_picking.pack_picking_scan_pack.scan_placeholder": "Bin barcode " +} diff --git a/shopfloor_mobile_packing/static/src/js/i18n/fr.json b/shopfloor_mobile_packing/static/src/js/i18n/fr.json new file mode 100644 index 00000000000..cad33dee5dc --- /dev/null +++ b/shopfloor_mobile_packing/static/src/js/i18n/fr.json @@ -0,0 +1,6 @@ +{ + "cluster_picking.pack_picking_put_in_pack.title": "Emballage: Nombre de colis.", + "cluster_picking.pack_picking_put_in_pack.scan_placeholder": "Nombre de colis créés?", + "cluster_picking.pack_picking_scan_pack.title": "Emballage: Validation emplacement à emballer.", + "cluster_picking.pack_picking_scan_pack.scan_placeholder": "Code-barre emplacement charriot " +} diff --git a/shopfloor_mobile_packing/static/src/js/translation_registry.js b/shopfloor_mobile_packing/static/src/js/translation_registry.js new file mode 100644 index 00000000000..c8e11f06f69 --- /dev/null +++ b/shopfloor_mobile_packing/static/src/js/translation_registry.js @@ -0,0 +1,10 @@ +import {translation_registry} from "/shopfloor_mobile_base/static/wms/src/services/translation_registry.js"; + +translation_registry.load( + "fr-FR", + "/shopfloor_mobile_packing/static/src/js/i18n/fr.json" +); +translation_registry.load( + "en-US", + "/shopfloor_mobile_packing/static/src/js/i18n/en.json" +); diff --git a/shopfloor_mobile_packing/templates/assets.xml b/shopfloor_mobile_packing/templates/assets.xml new file mode 100644 index 00000000000..9ff6f2e2a63 --- /dev/null +++ b/shopfloor_mobile_packing/templates/assets.xml @@ -0,0 +1,27 @@ + + + + + diff --git a/shopfloor_packing/README.rst b/shopfloor_packing/README.rst new file mode 100644 index 00000000000..4eb9c969189 --- /dev/null +++ b/shopfloor_packing/README.rst @@ -0,0 +1,79 @@ +================= +Shopfloor Packing +================= + +.. !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! This file is generated by oca-gen-addon-readme !! + !! changes will be overwritten. !! + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + +.. |badge1| image:: https://img.shields.io/badge/maturity-Beta-yellow.png + :target: https://odoo-community.org/page/development-status + :alt: Beta +.. |badge2| image:: https://img.shields.io/badge/licence-LGPL--3-blue.png + :target: http://www.gnu.org/licenses/lgpl-3.0-standalone.html + :alt: License: LGPL-3 +.. |badge3| image:: https://img.shields.io/badge/github-OCA%2Fwms-lightgray.png?logo=github + :target: https://github.com/OCA/wms/tree/16.0/shopfloor_packing + :alt: OCA/wms +.. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png + :target: https://translation.odoo-community.org/projects/wms-16-0/wms-16-0-shopfloor_packing + :alt: Translate me on Weblate +.. |badge5| image:: https://img.shields.io/badge/runboat-Try%20me-875A7B.png + :target: https://runboat.odoo-community.org/webui/builds.html?repo=OCA/wms&target_branch=16.0 + :alt: Try me on Runboat + +|badge1| |badge2| |badge3| |badge4| |badge5| + +This module adds a new configuration option to the "cluster picking" +scenario, enabling the packing of pickings into packages. + +**Table of contents** + +.. contents:: + :local: + +Usage +===== + +Go to the "cluster picking" scenario and check "Pack pickings". + +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 +~~~~~~~ + +* ACSONE SA/NV + +Contributors +~~~~~~~~~~~~ + +* Souheil Bejaoui + +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_packing/__init__.py b/shopfloor_packing/__init__.py new file mode 100644 index 00000000000..c6efb50ad34 --- /dev/null +++ b/shopfloor_packing/__init__.py @@ -0,0 +1,3 @@ +from . import actions +from . import services +from . import models diff --git a/shopfloor_packing/__manifest__.py b/shopfloor_packing/__manifest__.py new file mode 100644 index 00000000000..96342a8a663 --- /dev/null +++ b/shopfloor_packing/__manifest__.py @@ -0,0 +1,20 @@ +# Copyright 2021 ACSONE SA/NV +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +{ + "name": "Shopfloor Packing", + "version": "16.0.1.0.0", + "summary": """ Manage Packing into cluster picking""", + "author": "ACSONE SA/NV,Odoo Community Association (OCA)", + "website": "https://github.com/OCA/wms", + "category": "Stock Management", + "depends": [ + "shopfloor", + "internal_stock_quant_package", + "delivery_package_type_number_parcels", + ], + "data": ["views/shopfloor_menu.xml", "views/stock_picking.xml"], + "installable": True, + "license": "AGPL-3", + "application": False, +} diff --git a/shopfloor_packing/actions/__init__.py b/shopfloor_packing/actions/__init__.py new file mode 100644 index 00000000000..5e5999a6633 --- /dev/null +++ b/shopfloor_packing/actions/__init__.py @@ -0,0 +1,5 @@ +from . import data +from . import data_detail +from . import schema +from . import schema_detail +from . import message diff --git a/shopfloor_packing/actions/data.py b/shopfloor_packing/actions/data.py new file mode 100644 index 00000000000..e8d036e7c9e --- /dev/null +++ b/shopfloor_packing/actions/data.py @@ -0,0 +1,14 @@ +# Copyright 2021 ACSONE SA/NV +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from odoo.addons.component.core import Component + + +class DataAction(Component): + _inherit = "shopfloor.data.action" + + @property + def _package_parser(self): + res = super()._package_parser + res.append("is_internal") + return res diff --git a/shopfloor_packing/actions/data_detail.py b/shopfloor_packing/actions/data_detail.py new file mode 100644 index 00000000000..a3dc4262417 --- /dev/null +++ b/shopfloor_packing/actions/data_detail.py @@ -0,0 +1,32 @@ +# Copyright 2021 ACSONE SA/NV +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from odoo.addons.component.core import Component +from odoo.addons.shopfloor_base.utils import ensure_model + + +class DataDetailAction(Component): + _inherit = "shopfloor.data.detail.action" + + @ensure_model("stock.picking") + def pack_picking_detail(self, record, **kw): + return { + "id": record.id, + "name": record.name, + "partner": {"id": record.partner_id.id, "name": record.partner_id.name}, + "scanned_packs": list(record._packing_scanned_packs), + "move_lines": [ + self._pack_picking_move_lines_detail(ml) for ml in record.move_line_ids + ], + } + + def _pack_picking_move_lines_detail(self, record): + return { + "id": record.id, + "qty_done": record.qty_done, + "product": self.product( + record.product_id or record.package_id.single_product_id + ), + "package_src": self.package(record.package_id, record.picking_id), + "package_dest": self.package(record.result_package_id, record.picking_id), + } diff --git a/shopfloor_packing/actions/message.py b/shopfloor_packing/actions/message.py new file mode 100644 index 00000000000..b8de0f846a7 --- /dev/null +++ b/shopfloor_packing/actions/message.py @@ -0,0 +1,49 @@ +# Copyright 2021 ACSONE SA/NV +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). +from odoo import _ + +from odoo.addons.component.core import Component + + +class MessageAction(Component): + _inherit = "shopfloor.message.action" + + def stock_picking_already_packed(self, picking): + return { + "message_type": "warning", + "body": _("Transfer {} already packed.").format(picking.name), + } + + def nbr_packages_must_be_greated_than_zero(self): + return { + "message_type": "error", + "body": _("The number of packages must be greater than 0."), + } + + def notable_to_put_in_pack(self, picking): + return { + "message_type": "error", + "body": _("Not able to put in pack transfer {}.").format(picking.name), + } + + def bin_should_be_internal(self, package): + return { + "message_type": "error", + "body": _("The scanned package '{}' must be internal.").format( + package.name + ), + } + + def bin_is_for_another_picking(self, package): + return { + "message_type": "error", + "body": _("The scanned package '{}' is for an other picking.").format( + package.name + ), + } + + def stock_picking_packed_successfully(self, picking): + return { + "message_type": "success", + "body": _("Transfer {} has been packed successfully.").format(picking.name), + } diff --git a/shopfloor_packing/actions/schema.py b/shopfloor_packing/actions/schema.py new file mode 100644 index 00000000000..a59a1685169 --- /dev/null +++ b/shopfloor_packing/actions/schema.py @@ -0,0 +1,14 @@ +# Copyright 2021 ACSONE SA/NV +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from odoo.addons.component.core import Component + + +class ShopfloorSchemaAction(Component): + + _inherit = "shopfloor.schema.action" + + def package(self, with_packaging=False): + schema = super().package(with_packaging=with_packaging) + schema["is_internal"] = {"required": False, "type": "boolean"} + return schema diff --git a/shopfloor_packing/actions/schema_detail.py b/shopfloor_packing/actions/schema_detail.py new file mode 100644 index 00000000000..ce5725e298a --- /dev/null +++ b/shopfloor_packing/actions/schema_detail.py @@ -0,0 +1,48 @@ +# Copyright 2021 ACSONE SA/NV +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from odoo.addons.component.core import Component + + +class ShopfloorSchemaDetailAction(Component): + _inherit = "shopfloor.schema.detail.action" + + def pack_picking_detail(self): + schema = { + "id": {"required": True, "type": "integer"}, + "name": {"type": "string", "required": True, "nullable": False}, + "partner": { + "type": "dict", + "required": True, + "nullable": False, + "schema": { + "id": {"required": True, "type": "integer"}, + "name": {"type": "string", "required": True, "nullable": False}, + }, + }, + "scanned_packs": {"type": "list", "schema": {"type": "integer"}}, + "move_lines": { + "type": "list", + "schema": { + "type": "dict", + "schema": { + "id": {"required": True, "type": "integer"}, + "qty_done": {"type": "float", "required": True}, + "lot": { + "type": "dict", + "required": False, + "nullable": True, + "schema": self.lot(), + }, + "package_dest": self._schema_dict_of( + self.package(with_packaging=False), required=False + ), + "package_src": self._schema_dict_of( + self.package(with_packaging=False), required=False + ), + "product": self._schema_dict_of(self.product()), + }, + }, + }, + } + return schema diff --git a/shopfloor_packing/i18n/fr_BE.po b/shopfloor_packing/i18n/fr_BE.po new file mode 100644 index 00000000000..16ba5fa6704 --- /dev/null +++ b/shopfloor_packing/i18n/fr_BE.po @@ -0,0 +1,95 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * alc_shopfloor_packing +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 10.0+e\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2021-12-09 07:21+0000\n" +"PO-Revision-Date: 2021-12-09 07:21+0000\n" +"Last-Translator: <>\n" +"Language-Team: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Plural-Forms: \n" + +#. module: alc_shopfloor_packing +#: model:ir.model.fields,help:alc_shopfloor_packing.field_stock_picking_is_shopfloor_packing_todo +#: model:ir.model.fields,help:alc_shopfloor_packing.field_stock_picking_wave_is_shopfloor_packing_todo +msgid "If set, some operations need to be packed by the shopdloor operator" +msgstr "Si coché, le contenu des bacs doit encore être mis en boîte par l'opérateur Shofloorr" + +#. module: alc_shopfloor_packing +#: model:ir.model.fields,help:alc_shopfloor_packing.field_shopfloor_menu_pack_pickings +msgid "If you tick this box, all the picked item will be put in pack before the transfer." +msgstr "Si cochée, les produits devront être mis en boîte à la fin de la préparation." + +#. module: alc_shopfloor_packing +#: model:ir.model,name:alc_shopfloor_packing.model_shopfloor_menu +msgid "Menu displayed in the scanner application" +msgstr "Menu affiché dans l'application du scanner" + +#. module: alc_shopfloor_packing +#: code:addons/alc_shopfloor_packing/actions/message.py:27 +#, python-format +msgid "Not able to put in pack transfer {}." +msgstr "Impossible d'emballer le transfert {}." + +#. module: alc_shopfloor_packing +#: model:ir.model.fields,field_description:alc_shopfloor_packing.field_stock_picking_is_shopfloor_packing_todo +#: model:ir.model.fields,field_description:alc_shopfloor_packing.field_stock_picking_wave_is_shopfloor_packing_todo +msgid "Operations need to be packed" +msgstr "Des produits doivent être emballés" + +#. module: alc_shopfloor_packing +#: model:ir.model.fields,field_description:alc_shopfloor_packing.field_shopfloor_menu_pack_pickings +msgid "Pack pickings" +msgstr "Emballer les préparations" + +#. module: alc_shopfloor_packing +#: code:addons/alc_shopfloor_packing/models/shopfloor_menu.py:33 +#, python-format +msgid "Pack pickings is not allowed for menu {}." +msgstr "L'emballage des préparations n'est pas supporté par le menu {}." + +#. module: alc_shopfloor_packing +#: model:ir.model.fields,field_description:alc_shopfloor_packing.field_shopfloor_menu_pack_pickings_is_possible +msgid "Pack pickings is possible" +msgstr "L'emballage des préparations n'est pas possible" + +#. module: alc_shopfloor_packing +#: model:ir.model,name:alc_shopfloor_packing.model_stock_picking_wave +msgid "Picking Wave" +msgstr "Vague de préparation" + +#. module: alc_shopfloor_packing +#: code:addons/alc_shopfloor_packing/actions/message.py:21 +#, python-format +msgid "The number of packages must be greater than 0." +msgstr "Le nombre de boîtes doit est > 0." + +#. module: alc_shopfloor_packing +#: code:addons/alc_shopfloor_packing/actions/message.py:33 +#, python-format +msgid "The scanned package '{}' must be internal." +msgstr "The scanned package '{}' must be internal." + +#. module: alc_shopfloor_packing +#: model:ir.model,name:alc_shopfloor_packing.model_stock_picking +msgid "Transfer" +msgstr "Transfert" + +#. module: alc_shopfloor_packing +#: code:addons/alc_shopfloor_packing/actions/message.py:15 +#, python-format +msgid "Transfer {} already packed." +msgstr "La préparation {} est déjà emballée." + + +#. module: alc_shopfloor_packing +#: code:addons/alc_shopfloor_packing/actions/message.py:49 +#, python-format +msgid "Transfer {} has been packed successfully." +msgstr "La préparation {} a été emballée avec succès." diff --git a/shopfloor_packing/models/__init__.py b/shopfloor_packing/models/__init__.py new file mode 100644 index 00000000000..c2c0fc473ab --- /dev/null +++ b/shopfloor_packing/models/__init__.py @@ -0,0 +1,3 @@ +from . import stock_picking +from . import stock_picking_batch +from . import shopfloor_menu diff --git a/shopfloor_packing/models/shopfloor_menu.py b/shopfloor_packing/models/shopfloor_menu.py new file mode 100644 index 00000000000..7786b41946c --- /dev/null +++ b/shopfloor_packing/models/shopfloor_menu.py @@ -0,0 +1,15 @@ +# Copyright 2021 ACSONE SA/NV +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from odoo import fields, models + + +class ShopfloorMenu(models.Model): + _inherit = "shopfloor.menu" + + pack_pickings = fields.Boolean( + string="Pack pickings", + default=False, + help="If you tick this box, all the picked item will be put in pack" + " before the transfer.", + ) diff --git a/shopfloor_packing/models/stock_picking.py b/shopfloor_packing/models/stock_picking.py new file mode 100644 index 00000000000..26ba0a02005 --- /dev/null +++ b/shopfloor_packing/models/stock_picking.py @@ -0,0 +1,61 @@ +# Copyright 2021 ACSONE SA/NV +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +import json + +from odoo import api, fields, models + + +class StockPicking(models.Model): + _inherit = "stock.picking" + + is_shopfloor_packing_todo = fields.Boolean( + "Operations need to be packed", + help="If set, some operations need to be packed by the shopdloor operator", + compute="_compute_is_shopfloor_packing_todo", + ) + + packing_scanned_packs_str = fields.Char( + help="Technical field to know which pack has been scanned into the put in " + "pack process" + ) + + def _get_packages_to_pack(self): + self.ensure_one() + return self.mapped("move_line_ids.result_package_id").filtered("is_internal") + + @api.depends("move_line_ids", "move_line_ids.result_package_id") + def _compute_is_shopfloor_packing_todo(self): + for rec in self: + rec.is_shopfloor_packing_todo = False + for move_line in rec.move_line_ids: + if ( + move_line.result_package_id + and move_line.result_package_id.is_internal + ): + rec.is_shopfloor_packing_todo = True + break + + @property + def _packing_scanned_packs(self): + return set(json.loads(self.packing_scanned_packs_str or "[]")) + + def _set_packing_scanned_packs(self, packing_scanned_packs): + scanned_packs = list(packing_scanned_packs) if packing_scanned_packs else [] + self.packing_scanned_packs_str = json.dumps(scanned_packs) + + def _set_packing_pack_scanned(self, pack_id): + self.ensure_one() + self._set_packing_scanned_packs(self._packing_scanned_packs | {pack_id}) + + def _is_packing_pack_scanned(self, pack_id): + self.ensure_one() + return pack_id in self._packing_scanned_packs + + def _reset_packing_packs_scanned(self): + for rec in self: + rec._set_packing_scanned_packs({}) + + def is_shopfloor_packing_pack_to_scan(self): + self.ensure_one() + return set(self._get_packages_to_pack().ids) != self._packing_scanned_packs diff --git a/shopfloor_packing/models/stock_picking_batch.py b/shopfloor_packing/models/stock_picking_batch.py new file mode 100644 index 00000000000..e8db2539e75 --- /dev/null +++ b/shopfloor_packing/models/stock_picking_batch.py @@ -0,0 +1,21 @@ +# Copyright 2021 ACSONE SA/NV +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from odoo import api, fields, models + + +class StockPickingBatch(models.Model): + _inherit = "stock.picking.batch" + + is_shopfloor_packing_todo = fields.Boolean( + "Operations need to be packed", + help="If set, some operations need to be packed by the shopdloor operator", + compute="_compute_is_shopfloor_packing_todo", + ) + + @api.depends("picking_ids", "picking_ids.is_shopfloor_packing_todo") + def _compute_is_shopfloor_packing_todo(self): + for rec in self: + rec.is_shopfloor_packing_todo = any( + rec.picking_ids.mapped("is_shopfloor_packing_todo") + ) diff --git a/shopfloor_packing/readme/CONTRIBUTORS.rst b/shopfloor_packing/readme/CONTRIBUTORS.rst new file mode 100644 index 00000000000..50e6298db56 --- /dev/null +++ b/shopfloor_packing/readme/CONTRIBUTORS.rst @@ -0,0 +1 @@ +* Souheil Bejaoui diff --git a/shopfloor_packing/readme/DESCRIPTION.rst b/shopfloor_packing/readme/DESCRIPTION.rst new file mode 100644 index 00000000000..9ab2e76bb2f --- /dev/null +++ b/shopfloor_packing/readme/DESCRIPTION.rst @@ -0,0 +1,2 @@ +This module adds a new configuration option to the "cluster picking" +scenario, enabling the packing of pickings into packages. diff --git a/shopfloor_packing/readme/USAGE.rst b/shopfloor_packing/readme/USAGE.rst new file mode 100644 index 00000000000..904eb9ccaa0 --- /dev/null +++ b/shopfloor_packing/readme/USAGE.rst @@ -0,0 +1 @@ +Go to the "cluster picking" scenario and check "Pack pickings". diff --git a/shopfloor_packing/services/__init__.py b/shopfloor_packing/services/__init__.py new file mode 100644 index 00000000000..b2faa17efc3 --- /dev/null +++ b/shopfloor_packing/services/__init__.py @@ -0,0 +1 @@ +from . import cluster_picking diff --git a/shopfloor_packing/services/cluster_picking.py b/shopfloor_packing/services/cluster_picking.py new file mode 100644 index 00000000000..39cd8f80167 --- /dev/null +++ b/shopfloor_packing/services/cluster_picking.py @@ -0,0 +1,266 @@ +# Copyright 2021 ACSONE SA/NV (https://www.acsone.eu) +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). + +from odoo import fields + +from odoo.addons.base_rest.components.service import to_int +from odoo.addons.component.core import Component + + +class ClusterPicking(Component): + _inherit = "shopfloor.cluster.picking" + + def _last_picked_line(self, picking): + # a complete override to add a condition on internal package + return fields.first( + picking.move_line_ids.filtered( + lambda l: l.qty_done > 0 + and l.result_package_id.is_internal + # if we are moving the entire package, we shouldn't + # add stuff inside it, it's not a new package + and l.package_id != l.result_package_id + ).sorted(key="write_date", reverse=True) + ) + + def _get_next_picking_to_pack(self, batch): + """ + Return a picking not yet packed. + + The returned picking is the first + one into the list of picking not yet packed (is_shopfloor_packing_todo=True). + nbr_packages + """ + pickings_to_pack = batch.picking_ids.filtered( + lambda p: p.is_shopfloor_packing_todo + ) + move_lines = pickings_to_pack.mapped("move_line_ids") + move_lines = move_lines.filtered( + lambda ml: ml.result_package_id.is_internal + ).sorted(key=lambda ml: ml.result_package_id.name) + return move_lines[0].picking_id + + def _response_pack_picking_put_in_pack(self, picking, message=None): + data = self.data_detail.pack_picking_detail(picking) + return self._response( + next_state="pack_picking_put_in_pack", data=data, message=message + ) + + def _response_pack_picking_scan_pack(self, picking, message=None): + data = self.data_detail.pack_picking_detail(picking) + return self._response( + next_state="pack_picking_scan_pack", data=data, message=message + ) + + def scan_destination_pack(self, picking_batch_id, move_line_id, barcode, quantity): + search = self._actions_for("search") + bin_package = search.package_from_scan(barcode) + + if bin_package and not bin_package.is_internal: + batch = self.env["stock.picking.batch"].browse(picking_batch_id) + if not batch.exists(): + return self._response_batch_does_not_exist() + move_line = self.env["stock.move.line"].browse(move_line_id) + if not move_line.exists(): + return self._pick_next_line( + batch, message=self.msg_store.operation_not_found() + ) + return self._response_for_scan_destination( + move_line, message=self.msg_store.bin_should_be_internal(bin_package) + ) + return super().scan_destination_pack( + picking_batch_id, move_line_id, barcode, quantity + ) + + def scan_packing_to_pack(self, picking_batch_id, picking_id, barcode): + batch = self.env["stock.picking.batch"].browse(picking_batch_id) + if not batch.exists(): + return self._response_batch_does_not_exist() + picking = batch.picking_ids.filtered( + lambda p, picking_id=picking_id: p.id == picking_id + ) + if not picking: + return self._prepare_pack_picking( + batch, + message=self.msg_store.stock_picking_not_found(), + ) + if not picking.is_shopfloor_packing_todo: + return self._prepare_pack_picking( + batch, + message=self.msg_store.stock_picking_already_packed(picking), + ) + + search = self._actions_for("search") + bin_package = search.package_from_scan(barcode) + + if not bin_package: + return self._prepare_pack_picking( + batch, message=self.msg_store.bin_not_found_for_barcode(barcode) + ) + if not bin_package.is_internal: + return self._prepare_pack_picking( + batch, message=self.msg_store.bin_should_be_internal(bin_package) + ) + if bin_package not in picking.mapped("move_line_ids.result_package_id"): + return self._prepare_pack_picking( + batch, message=self.msg_store.bin_is_for_another_picking(bin_package) + ) + + picking._set_packing_pack_scanned(bin_package.id) + return self._prepare_pack_picking( + batch, + ) + + def _prepare_pack_picking(self, batch, message=None): + picking = self._get_next_picking_to_pack(batch) + if picking.is_shopfloor_packing_pack_to_scan(): + return self._response_pack_picking_scan_pack(picking, message=message) + return self._response_pack_picking_put_in_pack(picking, message=message) + + def prepare_unload(self, picking_batch_id): + # before initializing the unloading phase we put picking in pack if + # required by the scenario + batch = self.env["stock.picking.batch"].browse(picking_batch_id) + if not batch.exists(): + return self._response_batch_does_not_exist() + if not self.work.menu.pack_pickings or not batch.is_shopfloor_packing_todo: + return super().prepare_unload(picking_batch_id) + return self._prepare_pack_picking(batch) + + def put_in_pack(self, picking_batch_id, picking_id, nbr_packages): + batch = self.env["stock.picking.batch"].browse(picking_batch_id) + if not batch.exists(): + return self._response_batch_does_not_exist() + picking = batch.picking_ids.filtered( + lambda p, picking_id=picking_id: p.id == picking_id + ) + if not picking: + return self._response_put_in_pack( + picking_batch_id, + message=self.msg_store.stock_picking_not_found(), + ) + if not picking.is_shopfloor_packing_todo: + return self._response_put_in_pack( + picking_batch_id, + message=self.msg_store.stock_picking_already_packed(picking), + ) + if nbr_packages <= 0: + return self._response_put_in_pack( + picking_batch_id, + message=self.msg_store.nbr_packages_must_be_greated_than_zero(), + ) + savepoint = self._actions_for("savepoint").new() + pack = self._put_in_pack(picking, nbr_packages) + picking._reset_packing_packs_scanned() + if not pack: + savepoint.rollback() + return self._response_put_in_pack( + picking_batch_id, + message=self.msg_store.notable_to_put_in_pack(picking), + ) + self._postprocess_put_in_pack(picking, pack) + return self._response_put_in_pack( + picking_batch_id, + message=self.msg_store.stock_picking_packed_successfully(picking), + ) + + def _postprocess_put_in_pack(self, picking, pack): + """Override this method to include post-processing logic for the new package, + such as printing..""" + return + + def _put_in_pack(self, picking, number_of_parcels): + move_lines_to_pack = picking.move_line_ids.filtered( + lambda l: l.result_package_id and l.result_package_id.is_internal + ) + pack = picking._put_in_pack(move_lines_to_pack) + if ( + isinstance(pack, dict) + and pack.get("res_model") == "stock.quant.package" + and pack.get("res_id") + ): + pack = self.env["stock.quant.package"].browse(pack.get("res_id")) + if isinstance(pack, self.env["stock.quant.package"].__class__): + pack.number_of_parcels = number_of_parcels + return pack + + def _response_put_in_pack(self, picking_batch_id, message=None): + res = self.prepare_unload(picking_batch_id) + if message: + res["message"] = message + return res + + +class ShopfloorClusterPickingValidator(Component): + """Validators for the Cluster Picking endpoints.""" + + _inherit = "shopfloor.cluster_picking.validator" + + def put_in_pack(self): + return { + "picking_batch_id": { + "coerce": to_int, + "required": True, + "type": "integer", + }, + "picking_id": {"coerce": to_int, "required": True, "type": "integer"}, + "nbr_packages": {"coerce": to_int, "required": True, "type": "integer"}, + } + + def scan_packing_to_pack(self): + return { + "picking_batch_id": { + "coerce": to_int, + "required": True, + "type": "integer", + }, + "picking_id": {"coerce": to_int, "required": True, "type": "integer"}, + "barcode": {"required": True, "type": "string"}, + } + + +class ShopfloorClusterPickingValidatorResponse(Component): + """Validators for the Cluster Picking endpoints responses.""" + + _inherit = "shopfloor.cluster_picking.validator.response" + + def _states(self): + states = super()._states() + states["pack_picking_put_in_pack"] = self.schemas_detail.pack_picking_detail() + states["pack_picking_scan_pack"] = self.schemas_detail.pack_picking_detail() + return states + + @property + def _schema_pack_picking(self): + schema = self.schemas_detail.pack_picking_detail() + return {"type": "dict", "nullable": True, "schema": schema} + + def prepare_unload(self): + res = super().prepare_unload() + res["data"]["schema"]["pack_picking_put_in_pack"] = self._schema_pack_picking + res["data"]["schema"]["pack_picking_scan_pack"] = self._schema_pack_picking + return res + + def put_in_pack(self): + return self.prepare_unload() + + def confirm_start(self): + res = super().confirm_start() + res["data"]["schema"]["pack_picking_put_in_pack"] = self._schema_pack_picking + res["data"]["schema"]["pack_picking_scan_pack"] = self._schema_pack_picking + return res + + def scan_destination_pack(self): + res = super().scan_destination_pack() + res["data"]["schema"]["pack_picking_put_in_pack"] = self._schema_pack_picking + res["data"]["schema"]["pack_picking_scan_pack"] = self._schema_pack_picking + return res + + def scan_packing_to_pack(self): + return self._response_schema( + next_states={ + "unload_all", + "unload_single", + "pack_picking_put_in_pack", + "pack_picking_scan_pack", + } + ) diff --git a/shopfloor_packing/static/description/index.html b/shopfloor_packing/static/description/index.html new file mode 100644 index 00000000000..f0ba9735645 --- /dev/null +++ b/shopfloor_packing/static/description/index.html @@ -0,0 +1,425 @@ + + + + + + +Shopfloor Packing + + + +
+

Shopfloor Packing

+ + +

Beta License: LGPL-3 OCA/wms Translate me on Weblate Try me on Runboat

+

This module adds a new configuration option to the “cluster picking” +scenario, enabling the packing of pickings into packages.

+

Table of contents

+ +
+

Usage

+

Go to the “cluster picking” scenario and check “Pack pickings”.

+
+
+

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

+
    +
  • ACSONE SA/NV
  • +
+
+
+

Contributors

+ +
+
+

Maintainers

+

This module is maintained by the OCA.

+Odoo Community Association +

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_packing/tests/__init__.py b/shopfloor_packing/tests/__init__.py new file mode 100644 index 00000000000..e84b11d98c7 --- /dev/null +++ b/shopfloor_packing/tests/__init__.py @@ -0,0 +1,2 @@ +from . import test_cluster_picking_pack_picking +from . import test_cluster_picking_unload diff --git a/shopfloor_packing/tests/test_cluster_picking_pack_picking.py b/shopfloor_packing/tests/test_cluster_picking_pack_picking.py new file mode 100644 index 00000000000..2f2097827ff --- /dev/null +++ b/shopfloor_packing/tests/test_cluster_picking_pack_picking.py @@ -0,0 +1,342 @@ +# Copyright 2021 ACSONE SA/NV +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from odoo.addons.shopfloor.tests.test_cluster_picking_unload import ( + ClusterPickingUnloadingCommonCase, +) + + +# pylint: disable=missing-return +class ClusterPickingUnloadPackingCommonCase(ClusterPickingUnloadingCommonCase): + @classmethod + def setUpClassBaseData(cls, *args, **kwargs): + super().setUpClassBaseData(*args, **kwargs) + cls.bin1.write({"name": "bin1", "is_internal": True}) + cls.bin2.write({"name": "bin2", "is_internal": True}) + cls.menu.sudo().pack_pickings = True + + +class TestClusterPickingPrepareUnload(ClusterPickingUnloadPackingCommonCase): + def test_scan_destination_pack_bin_not_internal(self): + """Scan a destination package that is not an internal package.""" + self.bin2.is_internal = False + move_line = self.move_lines[0] + response = self.service.dispatch( + "scan_destination_pack", + params={ + "picking_batch_id": self.batch.id, + "move_line_id": move_line.id, + # this bin is used for the other picking + "barcode": self.bin2.name, + "quantity": move_line.reserved_qty, + }, + ) + self.assert_response( + response, + next_state="scan_destination", + data=self._line_data(move_line), + message=self.service.msg_store.bin_should_be_internal(self.bin2), + ) + + def test_prepare_unload_all_same_dest(self): + move_lines = self.move_lines + self._set_dest_package_and_done(move_lines[:1], self.bin2) + self._set_dest_package_and_done(move_lines[1:], self.bin1) + move_lines.write({"location_dest_id": self.packing_location.id}) + response = self.service.dispatch( + "prepare_unload", params={"picking_batch_id": self.batch.id} + ) + location = self.packing_location + # The first bin to process is bin1 we should therefore scan the bin 1 + # to pack and put in pack + picking = move_lines[-1].picking_id + data = self.data_detail.pack_picking_detail(picking) + self.assert_response( + response, + next_state="pack_picking_scan_pack", + data=data, + ) + # we scan the pack + response = self.service.dispatch( + "scan_packing_to_pack", + params={ + "picking_batch_id": self.batch.id, + "picking_id": picking.id, + "barcode": self.bin1.name, + }, + ) + data = self.data_detail.pack_picking_detail(picking) + self.assert_response( + response, + next_state="pack_picking_put_in_pack", + data=data, + ) + # we process to the put in pack + response = self.service.dispatch( + "put_in_pack", + params={ + "picking_batch_id": self.batch.id, + "picking_id": picking.id, + "nbr_packages": 4, + }, + ) + message = self.service.msg_store.stock_picking_packed_successfully(picking) + result_package = picking.move_line_ids.mapped("result_package_id") + self.assertEqual(len(result_package), 1) + self.assertEqual(result_package[0].number_of_parcels, 4) + + picking = move_lines[0].picking_id + data = self.data_detail.pack_picking_detail(picking) + # message = self.service.msg_store.stock_picking_packed_successfully(picking) + self.assert_response( + response, next_state="pack_picking_scan_pack", data=data, message=message + ) + # we scan the pack + response = self.service.dispatch( + "scan_packing_to_pack", + params={ + "picking_batch_id": self.batch.id, + "picking_id": picking.id, + "barcode": self.bin2.name, + }, + ) + data = self.data_detail.pack_picking_detail(picking) + self.assert_response( + response, + next_state="pack_picking_put_in_pack", + data=data, + ) + # we process to the put in pack + response = self.service.dispatch( + "put_in_pack", + params={ + "picking_batch_id": self.batch.id, + "picking_id": picking.id, + "nbr_packages": 2, + }, + ) + data = self._data_for_batch(self.batch, location) + message = self.service.msg_store.stock_picking_packed_successfully(picking) + self.assert_response( + response, next_state="unload_all", data=data, message=message + ) + + result_package = picking.move_line_ids.mapped("result_package_id") + self.assertEqual(len(result_package), 1) + self.assertEqual(result_package[0].number_of_parcels, 2) + + def test_prepare_unload_different_dest(self): + """All move lines have different destination locations.""" + move_lines = self.move_lines + self._set_dest_package_and_done(move_lines[:1], self.bin2) + self._set_dest_package_and_done(move_lines[1:], self.bin1) + move_lines[:1].write({"location_dest_id": self.packing_a_location.id}) + move_lines[1:].write({"location_dest_id": self.packing_b_location.id}) + response = self.service.dispatch( + "prepare_unload", params={"picking_batch_id": self.batch.id} + ) + # The first bin to process is bin1 we should therefore a pack_picking + # step with the picking info of the last move_line + picking = move_lines[-1].picking_id + data = self.data_detail.pack_picking_detail(picking) + self.assert_response( + response, + next_state="pack_picking_scan_pack", + data=data, + ) + # we scan the pack + response = self.service.dispatch( + "scan_packing_to_pack", + params={ + "picking_batch_id": self.batch.id, + "picking_id": picking.id, + "barcode": self.bin1.name, + }, + ) + data = self.data_detail.pack_picking_detail(picking) + self.assert_response( + response, + next_state="pack_picking_put_in_pack", + data=data, + ) + # we process to the put in pack + response = self.service.dispatch( + "put_in_pack", + params={ + "picking_batch_id": self.batch.id, + "picking_id": picking.id, + "nbr_packages": 4, + }, + ) + + message = self.service.msg_store.stock_picking_packed_successfully(picking) + + # next picking.. + picking = move_lines[0].picking_id + data = self.data_detail.pack_picking_detail(picking) + self.assert_response( + response, next_state="pack_picking_scan_pack", data=data, message=message + ) + # we scan the pack + response = self.service.dispatch( + "scan_packing_to_pack", + params={ + "picking_batch_id": self.batch.id, + "picking_id": picking.id, + "barcode": self.bin2.name, + }, + ) + data = self.data_detail.pack_picking_detail(picking) + self.assert_response( + response, + next_state="pack_picking_put_in_pack", + data=data, + ) + # we process to the put in pack + response = self.service.dispatch( + "put_in_pack", + params={ + "picking_batch_id": self.batch.id, + "picking_id": picking.id, + "nbr_packages": 2, + }, + ) + # Since the last move_line has been put in pack first, the first pack + # to unload is the one from the last move_line + new_bin = move_lines[-1].result_package_id + location = move_lines[-1].location_dest_id + data = self._data_for_batch(self.batch, location, pack=new_bin) + message = self.service.msg_store.stock_picking_packed_successfully(picking) + self.assert_response( + response, next_state="unload_single", data=data, message=message + ) + + def test_prepare_full_bin_unload(self): + # process one move_line and call unload + # the unload should return a pack_picking state + # and once processed continue with next move_lines + move_lines = self.move_lines + self._set_dest_package_and_done(move_lines[0], self.bin1) + move_lines.write({"location_dest_id": self.packing_location.id}) + response = self.service.dispatch( + "prepare_unload", params={"picking_batch_id": self.batch.id} + ) + # step with the picking info of the last move_line + picking = move_lines[0].picking_id + data = self.data_detail.pack_picking_detail(picking) + self.assert_response( + response, + next_state="pack_picking_scan_pack", + data=data, + ) + # we scan the pack and process to the put in pack + response = self.service.dispatch( + "scan_packing_to_pack", + params={ + "picking_batch_id": self.batch.id, + "picking_id": picking.id, + "barcode": self.bin1.name, + }, + ) + data = self.data_detail.pack_picking_detail(picking) + self.assert_response( + response, + next_state="pack_picking_put_in_pack", + data=data, + ) + response = self.service.dispatch( + "put_in_pack", + params={ + "picking_batch_id": self.batch.id, + "picking_id": picking.id, + "nbr_packages": 4, + }, + ) + result_package = picking.move_line_ids.mapped("result_package_id") + self.assertEqual(len(result_package), 1) + self.assertEqual(result_package[0].number_of_parcels, 4) + + # now we must unload + location = move_lines[0].location_dest_id + data = self._data_for_batch(self.batch, location) + self.assert_response( + response, + next_state="unload_all", + data=data, + message=self.service.msg_store.stock_picking_packed_successfully(picking), + ) + response = self.service.dispatch( + "set_destination_all", + params={ + "picking_batch_id": self.batch.id, + "barcode": self.packing_location.barcode, + }, + ) + + # once the unload is done, we must process the others move_lines + move_line = self.service._next_line_for_pick(self.batch) + while move_line: + picking = move_line.picking_id + self.assertEqual(response["next_state"], "start_line") + response = self.service.dispatch( + "scan_destination_pack", + params={ + "picking_batch_id": self.batch.id, + "move_line_id": move_line.id, + "barcode": self.bin1.name, + "quantity": move_line.reserved_uom_qty, + }, + ) + move_line = self.service._next_line_for_pick(self.batch) + + # everything is processed, we should put in pack... + data = self.data_detail.pack_picking_detail(picking) + self.assert_response( + response, + next_state="pack_picking_scan_pack", + data=data, + ) + # we scan the pack and process to the put in pack + response = self.service.dispatch( + "scan_packing_to_pack", + params={ + "picking_batch_id": self.batch.id, + "picking_id": picking.id, + "barcode": self.bin1.name, + }, + ) + data = self.data_detail.pack_picking_detail(picking) + self.assert_response( + response, + next_state="pack_picking_put_in_pack", + data=data, + ) + response = self.service.dispatch( + "put_in_pack", + params={ + "picking_batch_id": self.batch.id, + "picking_id": picking.id, + "nbr_packages": 2, + }, + ) + data = self._data_for_batch(self.batch, location) + self.assert_response( + response, + next_state="unload_all", + data=data, + message=self.service.msg_store.stock_picking_packed_successfully(picking), + ) + + result_package = picking.move_line_ids.mapped("result_package_id") + self.assertEqual(len(result_package), 1) + self.assertEqual(result_package[0].number_of_parcels, 2) + + def test_response_for_scan_destination(self): + """Check that non internal package are not proposed as package_dest.""" + line1 = self.two_lines_picking.move_line_ids[0] + # we already scan and put the first line in bin1 + self._set_dest_package_and_done(line1, self.bin1) + self.bin1.is_internal = False + self.assertFalse(self.service._last_picked_line(line1.picking_id)) + response = self.service._response_for_scan_destination(line1) + self.assertFalse(response["data"]["scan_destination"]["package_dest"]) diff --git a/shopfloor_packing/tests/test_cluster_picking_unload.py b/shopfloor_packing/tests/test_cluster_picking_unload.py new file mode 100644 index 00000000000..e81895089bb --- /dev/null +++ b/shopfloor_packing/tests/test_cluster_picking_unload.py @@ -0,0 +1,50 @@ +# Copyright 2020 Camptocamp SA (http://www.camptocamp.com) +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). + +from odoo.addons.shopfloor.tests.test_cluster_picking_unload import ( + ClusterPickingPrepareUnloadCase, + ClusterPickingSetDestinationAllCase, + ClusterPickingUnloadScanDestinationCase, + ClusterPickingUnloadScanPackCase, + ClusterPickingUnloadSplitCase, +) + + +class ClusterPickingPrepareUnloadCase2(ClusterPickingPrepareUnloadCase): + """ + Ensure that the normal unload process is preserved if the pack_pickings option. + + is not activated on the menu (default) + """ + + +class ClusterPickingSetDestinationAllCase2(ClusterPickingSetDestinationAllCase): + """ + Ensure that the normal unload process is preserved if the pack_pickings option. + + is not activated on the menu (default) + """ + + +class ClusterPickingUnloadSplitCase2(ClusterPickingUnloadSplitCase): + """ + Ensure that the normal unload process is preserved if the pack_pickings option. + + is not activated on the menu (default) + """ + + +class ClusterPickingUnloadScanPackCase2(ClusterPickingUnloadScanPackCase): + """ + Ensure that the normal unload process is preserved if the pack_pickings option. + + is not activated on the menu (default) + """ + + +class ClusterPickingUnloadScanDestinationCase2(ClusterPickingUnloadScanDestinationCase): + """ + Ensure that the normal unload process is preserved if the pack_pickings option. + + is not activated on the menu (default) + """ diff --git a/shopfloor_packing/views/shopfloor_menu.xml b/shopfloor_packing/views/shopfloor_menu.xml new file mode 100644 index 00000000000..d24718b4c04 --- /dev/null +++ b/shopfloor_packing/views/shopfloor_menu.xml @@ -0,0 +1,23 @@ + + + + + + shopfloor.menu + + + + + + + + + + + + diff --git a/shopfloor_packing/views/stock_picking.xml b/shopfloor_packing/views/stock_picking.xml new file mode 100644 index 00000000000..49e153660da --- /dev/null +++ b/shopfloor_packing/views/stock_picking.xml @@ -0,0 +1,18 @@ + + + + + + stock.picking + + + + + + + + + + + From 7658948c0e55b7583824e9f96aa950f9bbeae6db Mon Sep 17 00:00:00 2001 From: sbejaoui Date: Wed, 26 Jun 2024 21:18:26 +0200 Subject: [PATCH 02/20] [IMP] description --- shopfloor_mobile_packing/README.rst | 11 +++-- .../static/description/index.html | 33 ++++++------- shopfloor_packing/README.rst | 29 +++++++---- shopfloor_packing/readme/DESCRIPTION.rst | 12 ++++- .../static/description/index.html | 48 +++++++++++-------- 5 files changed, 82 insertions(+), 51 deletions(-) diff --git a/shopfloor_mobile_packing/README.rst b/shopfloor_mobile_packing/README.rst index c61602c7f8d..14492bdcfe0 100644 --- a/shopfloor_mobile_packing/README.rst +++ b/shopfloor_mobile_packing/README.rst @@ -2,10 +2,13 @@ Shopfloor Mobile Packing ======================== -.. !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! +.. + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! !! This file is generated by oca-gen-addon-readme !! !! changes will be overwritten. !! !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! source digest: sha256:b7902164a2332698048e39e0a5f28cb8ed1e741c6e20def9237892380b2c7333 + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! .. |badge1| image:: https://img.shields.io/badge/maturity-Beta-yellow.png :target: https://odoo-community.org/page/development-status @@ -20,10 +23,10 @@ Shopfloor Mobile Packing :target: https://translation.odoo-community.org/projects/wms-16-0/wms-16-0-shopfloor_mobile_packing :alt: Translate me on Weblate .. |badge5| image:: https://img.shields.io/badge/runboat-Try%20me-875A7B.png - :target: https://runboat.odoo-community.org/webui/builds.html?repo=OCA/wms&target_branch=16.0 + :target: https://runboat.odoo-community.org/builds?repo=OCA/wms&target_branch=16.0 :alt: Try me on Runboat -|badge1| |badge2| |badge3| |badge4| |badge5| +|badge1| |badge2| |badge3| |badge4| |badge5| Shopfloor mobile extension for packing operation into cluster picking. @@ -37,7 +40,7 @@ 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 +If you spotted it first, help us to smash it by providing a detailed and welcomed `feedback `_. Do not contact contributors directly about support or help with technical issues. diff --git a/shopfloor_mobile_packing/static/description/index.html b/shopfloor_mobile_packing/static/description/index.html index 49445c23b4d..a2ea1c4619b 100644 --- a/shopfloor_mobile_packing/static/description/index.html +++ b/shopfloor_mobile_packing/static/description/index.html @@ -1,20 +1,19 @@ - - + Shopfloor Mobile Packing