diff --git a/setup/shopfloor_mobile_printing_base/odoo/addons/shopfloor_mobile_printing_base b/setup/shopfloor_mobile_printing_base/odoo/addons/shopfloor_mobile_printing_base new file mode 120000 index 00000000000..b901e6e2cd7 --- /dev/null +++ b/setup/shopfloor_mobile_printing_base/odoo/addons/shopfloor_mobile_printing_base @@ -0,0 +1 @@ +../../../../shopfloor_mobile_printing_base \ No newline at end of file diff --git a/setup/shopfloor_mobile_printing_base/setup.py b/setup/shopfloor_mobile_printing_base/setup.py new file mode 100644 index 00000000000..28c57bb6403 --- /dev/null +++ b/setup/shopfloor_mobile_printing_base/setup.py @@ -0,0 +1,6 @@ +import setuptools + +setuptools.setup( + setup_requires=['setuptools-odoo'], + odoo_addon=True, +) diff --git a/setup/shopfloor_printing_base/odoo/addons/shopfloor_printing_base b/setup/shopfloor_printing_base/odoo/addons/shopfloor_printing_base new file mode 120000 index 00000000000..517df3dbf62 --- /dev/null +++ b/setup/shopfloor_printing_base/odoo/addons/shopfloor_printing_base @@ -0,0 +1 @@ +../../../../shopfloor_printing_base \ No newline at end of file diff --git a/setup/shopfloor_printing_base/setup.py b/setup/shopfloor_printing_base/setup.py new file mode 100644 index 00000000000..28c57bb6403 --- /dev/null +++ b/setup/shopfloor_printing_base/setup.py @@ -0,0 +1,6 @@ +import setuptools + +setuptools.setup( + setup_requires=['setuptools-odoo'], + odoo_addon=True, +) diff --git a/shopfloor_base/services/service.py b/shopfloor_base/services/service.py index d537e323b01..e287764e6ed 100644 --- a/shopfloor_base/services/service.py +++ b/shopfloor_base/services/service.py @@ -23,11 +23,15 @@ class BaseShopfloorService(AbstractComponent): _collection = "shopfloor.app" _expose_model = None - def __init__(self, work_context): - super().__init__(work_context) + @property + def _menu(self): + # User private attributes to not mess up w/ public endpoints + return getattr(self.work, "menu", self.env["shopfloor.menu"]) + + @property + def _profile(self): # User private attributes to not mess up w/ public endpoints - self._profile = getattr(self.work, "profile", self.env["shopfloor.profile"]) - self._menu = getattr(self.work, "menu", self.env["shopfloor.menu"]) + return getattr(self.work, "profile", self.env["shopfloor.profile"]) def _get_api_spec(self, **params): return ShopfloorRestServiceAPISpec(self, **params) diff --git a/shopfloor_base/services/validator.py b/shopfloor_base/services/validator.py index 0d06162e0e0..c275cbc4bdb 100644 --- a/shopfloor_base/services/validator.py +++ b/shopfloor_base/services/validator.py @@ -162,10 +162,9 @@ class BaseShopfloorValidatorResponse(AbstractComponent): # Initial state of a workflow _start_state = "start" - def _states(self): - """List of possible next states - - With the schema of the data send to the client to transition + def _states(self) -> dict: + """Returns a dict mapping next states with the schema + of the data sent to the client to transition to the next state. """ return {} @@ -181,6 +180,31 @@ def schemas(self): def schemas_detail(self): return self._actions_for("schema_detail") + def _get_global_fields_schemas(self) -> dict: + """Returns schemas of fields to be added in all next states data schemas""" + return {} + + def _validate_next_states(self, next_states: set, states_schemas: dict): + if self._start_state not in states_schemas: + raise ValueError( + "the _start_state is {} but this state does not exist" + ", you may want to change the property's value".format( + self._start_state + ) + ) + unknown_states = set(next_states) - states_schemas.keys() + if unknown_states: + raise ValueError( + "states {!r} are not defined in _states".format(unknown_states) + ) + + def _add_global_fields_schemas(self, states_schemas: dict) -> dict: + "Modifies the 'states_schemas' dict to add the schemas of the global fields" + global_fields_schemas = self._get_global_fields_schemas() + for data_schema in states_schemas.values(): + data_schema.update(global_fields_schemas) + return states_schemas + def _response_schema(self, data_schema=None, next_states=None): """Schema for the return validator @@ -225,19 +249,8 @@ def _response_schema(self, data_schema=None, next_states=None): next_states = set(next_states) next_states.add(self._start_state) states_schemas = self._states() - if self._start_state not in states_schemas: - raise ValueError( - "the _start_state is {} but this state does not exist" - ", you may want to change the property's value".format( - self._start_state - ) - ) - unknown_states = set(next_states) - states_schemas.keys() - if unknown_states: - raise ValueError( - "states {!r} are not defined in _states".format(unknown_states) - ) - + self._validate_next_states(next_states, states_schemas) + states_schemas = self._add_global_fields_schemas(states_schemas) data_schema = data_schema.copy() data_schema.update( { diff --git a/shopfloor_mobile_printing_base/README.rst b/shopfloor_mobile_printing_base/README.rst new file mode 100644 index 00000000000..d41508e08d7 --- /dev/null +++ b/shopfloor_mobile_printing_base/README.rst @@ -0,0 +1,119 @@ +============================== +Shopfloor Mobile Printing Base +============================== + +.. + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! This file is generated by oca-gen-addon-readme !! + !! changes will be overwritten. !! + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! source digest: sha256:87d0a3ca476eeb63945ef15e1ba78c79e0384b64eb7cfb1071f1724cbb7746c8 + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + +.. |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_printing_base + :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_printing_base + :alt: Translate me on Weblate +.. |badge5| image:: https://img.shields.io/badge/runboat-Try%20me-875A7B.png + :target: https://runboat.odoo-community.org/builds?repo=OCA/wms&target_branch=16.0 + :alt: Try me on Runboat + +|badge1| |badge2| |badge3| |badge4| |badge5| + +This module allows to use a new Vue component that allows to send print +commands with an amount of copies. + +**Table of contents** + +.. contents:: + :local: + +Usage +===== + +Use this in your component template: + +```` + +Implement the ``print_labels()`` (or whatever you call it) method on +your component level: + +:: + + const Reception = process_registry.extend("reception", { + template: new_template, + "methods._get_states": function () { + let states = _get_states.bind(this)(); + const set_destination = states.set_destination; + + const self = this; + set_destination.print_labels = function (quantity) { + self.wait_call( + self.odoo.call("print_labels", { + picking_id: self.state.data.picking.id, + selected_line_id: self.state.data.selected_move_line[0].id, + quantity: quantity, + }) + ); + }; + return states; + }, + }); + +See ``shopfloor_printing_base`` for backend implementation. + +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 to smash 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 +------------ + +- Denis Roussel denis.roussel@acsone.eu + +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. + +.. |maintainer-rousseldenis| image:: https://github.com/rousseldenis.png?size=40px + :target: https://github.com/rousseldenis + :alt: rousseldenis + +Current `maintainer `__: + +|maintainer-rousseldenis| + +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_printing_base/__init__.py b/shopfloor_mobile_printing_base/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/shopfloor_mobile_printing_base/__manifest__.py b/shopfloor_mobile_printing_base/__manifest__.py new file mode 100644 index 00000000000..4a717c185b2 --- /dev/null +++ b/shopfloor_mobile_printing_base/__manifest__.py @@ -0,0 +1,16 @@ +# Copyright 2025 ACSONE SA/NV +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +{ + "name": "Shopfloor Mobile Printing Base", + "summary": """This module adds the widget to print in shopfloor""", + "version": "16.0.1.0.0", + "license": "AGPL-3", + "author": "ACSONE SA/NV,Odoo Community Association (OCA)", + "maintainers": ["rousseldenis"], + "website": "https://github.com/OCA/wms", + "depends": [ + "shopfloor_mobile_base", + ], + "data": ["templates/assets.xml"], +} diff --git a/shopfloor_mobile_printing_base/readme/CONTRIBUTORS.md b/shopfloor_mobile_printing_base/readme/CONTRIBUTORS.md new file mode 100644 index 00000000000..4e7e6847269 --- /dev/null +++ b/shopfloor_mobile_printing_base/readme/CONTRIBUTORS.md @@ -0,0 +1 @@ +- Denis Roussel diff --git a/shopfloor_mobile_printing_base/readme/DESCRIPTION.md b/shopfloor_mobile_printing_base/readme/DESCRIPTION.md new file mode 100644 index 00000000000..2d0307ecf67 --- /dev/null +++ b/shopfloor_mobile_printing_base/readme/DESCRIPTION.md @@ -0,0 +1,2 @@ +This module allows to use a new Vue component that allows to send print commands +with an amount of copies. diff --git a/shopfloor_mobile_printing_base/readme/USAGE.md b/shopfloor_mobile_printing_base/readme/USAGE.md new file mode 100644 index 00000000000..a30e278bcc7 --- /dev/null +++ b/shopfloor_mobile_printing_base/readme/USAGE.md @@ -0,0 +1,29 @@ +Use this in your component template: + +` ` + +Implement the `print_labels()` (or whatever you call it) method on your component level: + +``` +const Reception = process_registry.extend("reception", { + template: new_template, + "methods._get_states": function () { + let states = _get_states.bind(this)(); + const set_destination = states.set_destination; + + const self = this; + set_destination.print_labels = function (quantity) { + self.wait_call( + self.odoo.call("print_labels", { + picking_id: self.state.data.picking.id, + selected_line_id: self.state.data.selected_move_line[0].id, + quantity: quantity, + }) + ); + }; + return states; + }, +}); +``` + +See `shopfloor_printing_base` for backend implementation. diff --git a/shopfloor_mobile_printing_base/static/description/icon.png b/shopfloor_mobile_printing_base/static/description/icon.png new file mode 100644 index 00000000000..3a0328b516c Binary files /dev/null and b/shopfloor_mobile_printing_base/static/description/icon.png differ diff --git a/shopfloor_mobile_printing_base/static/description/index.html b/shopfloor_mobile_printing_base/static/description/index.html new file mode 100644 index 00000000000..c77b06c69a0 --- /dev/null +++ b/shopfloor_mobile_printing_base/static/description/index.html @@ -0,0 +1,456 @@ + + + + + +Shopfloor Mobile Printing Base + + + +
+

Shopfloor Mobile Printing Base

+ + +

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

+

This module allows to use a new Vue component that allows to send print +commands with an amount of copies.

+

Table of contents

+ +
+

Usage

+

Use this in your component template:

+

<label-printer v-on:print_labels="state.print_labels($event)" buttonLabel="<The label>"/>

+

Implement the print_labels() (or whatever you call it) method on +your component level:

+
+const Reception = process_registry.extend("reception", {
+    template: new_template,
+    "methods._get_states": function () {
+        let states = _get_states.bind(this)();
+        const set_destination = states.set_destination;
+
+        const self = this;
+        set_destination.print_labels = function (quantity) {
+            self.wait_call(
+            self.odoo.call("print_labels", {
+                picking_id: self.state.data.picking.id,
+                selected_line_id: self.state.data.selected_move_line[0].id,
+                quantity: quantity,
+            })
+            );
+        };
+        return states;
+    },
+});
+
+

See shopfloor_printing_base for backend implementation.

+
+
+

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 to smash 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.

+

Current maintainer:

+

rousseldenis

+

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_printing_base/static/src/components/printing-widget.js b/shopfloor_mobile_printing_base/static/src/components/printing-widget.js new file mode 100644 index 00000000000..ffbeca9ca11 --- /dev/null +++ b/shopfloor_mobile_printing_base/static/src/components/printing-widget.js @@ -0,0 +1,105 @@ +/** + * Copyright 2020 Camptocamp SA (http://www.camptocamp.com) + * @author Simone Orsi + * Copyright 2025 ACSONE SA/NV (https://acsone.eu) + * License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). + */ + +export var LabelPrinterWidget = Vue.component("label-printer", { + template: ` + + + Label Printing + + +
+ + mdi-minus + + + + + + mdi-plus + +
+ + {{ get_label() }} +
+`, + props: { + input_type: { + type: String, + default: "text", // Avoid default browser spinner + }, + editable: { + type: Boolean, + default: true, + }, + show_init_value: { + type: Boolean, + default: false, + }, + select_value_on_load: { + type: Boolean, + default: true, + }, + step: { + type: Number, + default: 1, + }, + buttonLabel: { + type: String, + default: "Print", + }, + min: { + type: Number, + default: 1, + }, + mode: { + type: String, + default: "text-only", + }, + }, + data: function () { + return { + value: 1, + original_value: 0, + + // Data validation for the number input field + numberRules: [ + (v) => !!v || "Required", + (v) => Number.isInteger(Number(v)) || "Must be a number", + ], + }; + }, + methods: { + get_label: function () { + return this.buttonLabel; + }, + increase: function () { + if (this.max == undefined || this.value < this.max) { + this.value += this.step; + } + }, + decrease: function () { + if (this.value > this.min) { + const new_val = this.value - this.step; + this.value = new_val >= this.min ? new_val : this.min; + } + }, + }, + watch: { + value: { + handler: function (newVal, oldVal) { + this.value = newVal; + this.$emit("input", this.value); + }, + }, + }, +}); diff --git a/shopfloor_mobile_printing_base/static/src/css/main.css b/shopfloor_mobile_printing_base/static/src/css/main.css new file mode 100644 index 00000000000..4508fe3569b --- /dev/null +++ b/shopfloor_mobile_printing_base/static/src/css/main.css @@ -0,0 +1,3 @@ +.centered-input input { + text-align: center; +} diff --git a/shopfloor_mobile_printing_base/static/src/i18n/i18n.de.js b/shopfloor_mobile_printing_base/static/src/i18n/i18n.de.js new file mode 100644 index 00000000000..1fb94566a5b --- /dev/null +++ b/shopfloor_mobile_printing_base/static/src/i18n/i18n.de.js @@ -0,0 +1,112 @@ +/** + * Copyright 2026 ACSONE SA/NV (https://acsone.eu) + * License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). + */ + +import {translation_registry} from "../services/translation_registry.js"; + +const messages_de = { + screen: { + login: { + title: "Login", + action: { + login: "Login", + }, + error: { + login_invalid: "Ungültige Anmeldeinformationen", + }, + }, + home: { + title: "Startseite", + main_title: "Startseite", + version: "Version:", + action: { + nuke_data_and_reload: + "Erzwingen das erneute Laden von Daten und aktualisieren", + }, + }, + scan_anything: { + name: "Gescannt", + title: "Gescannt {what}", + scan_placeholder: "Alles scannen", + }, + settings: { + title: "Einstellungen", + home: { + name: "Einstellungen", + title: "Einstellungen", + }, + language: { + name: "Sprache", + title: "Sprache auswählen", + }, + profile: { + name: "Profil", + title: "Wähle Profil", + profile_updated: "Profil aktualisiert", + }, + fullscreen: { + enter: "Vollbildmodus", + exit: "Vollbild beenden", + }, + }, + }, + app: { + profile_not_configured: + "Profil noch nicht konfiguriert. Bitte wählen Sie eine aus.", + profile_configure: "Profil konfigurieren", + loading: "Wird geladen...", + action: { + logout: "Ausloggen", + }, + nav: { + scenario: "Szenario:", + op_types: "Op Typen:", + }, + log_entry_link: "Logeintrag ansehen / teilen", + running_env: { + prod: "Produktion", + integration: "Integration", + staging: "Staging", + test: "Test", + dev: "Entwicklung", + }, + report_issue: { + action_txt: "Benötigen Sie Unterstützung?", + mail: { + subject: "Ich benötige Unterstützung mit der {app_name} app", + info_warning_msg: + "BITTE ÄNDERN SIE DIE FOLGENDE INFORMATION/NACHRICHT NICHT", + }, + }, + }, + language: { + name: { + English: "Englisch", + French: "Französisch", + German: "Deutsch", + }, + }, + btn: { + back: { + title: "Zurück", + }, + confirm: { + title: "Bestätigen", + }, + ok: { + title: "Ok", + }, + reset: { + title: "Zurücksetzen", + }, + cancel: { + title: "Absagen", + }, + reload_config: { + title: "Konfiguration und Menü neu laden", + }, + }, +}; + +translation_registry.add("de-DE", messages_de); diff --git a/shopfloor_mobile_printing_base/static/src/i18n/i18n.en.js b/shopfloor_mobile_printing_base/static/src/i18n/i18n.en.js new file mode 100644 index 00000000000..6cf7872af85 --- /dev/null +++ b/shopfloor_mobile_printing_base/static/src/i18n/i18n.en.js @@ -0,0 +1,135 @@ +/** + * Copyright 2026 ACSONE SA/NV (https://acsone.eu) + * License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). + */ + +import {translation_registry} from "../services/translation_registry.js"; + +const messages_en = { + screen: { + login: { + title: "Login", + action: { + login: "Login", + }, + error: { + login_invalid: "Login failed. Invalid credentials", + }, + }, + home: { + title: "Home", + main_title: "Home", + version: "Version:", + action: { + nuke_data_and_reload: "Force reload data and refresh", + }, + }, + scan_anything: { + name: "Scan", + title: "Scan {what}", + scan_placeholder: "Scan anything", + }, + settings: { + title: "Settings", + home: { + name: "Settings", + title: "Settings", + }, + language: { + name: "Language", + title: "Select language", + }, + profile: { + name: "Profile", + title: "Select profile", + profile_updated: "Profile updated", + }, + fullscreen: { + enter: "Go fullscreen", + exit: "Exit fullscreen", + }, + }, + }, + app: { + profile_not_configured: "Profile not configured yet. Please select one.", + profile_configure: "Configure profile", + loading: "Loading...", + action: { + logout: "Logout", + }, + nav: { + scenario: "Scenario:", + op_types: "Op Types:", + }, + log_entry_link: "View / share log entry", + running_env: { + prod: "Production", + integration: "Integration", + staging: "Staging", + test: "Test", + dev: "Development", + }, + report_issue: { + action_txt: "Need help?", + mail: { + subject: "I need help with the {app_name} app", + info_warning_msg: "PLEASE, DO NOT ALTER THE INFO BELOW", + }, + }, + }, + language: { + name: { + English: "English", + French: "French", + German: "German", + }, + }, + btn: { + back: { + title: "Back", + }, + confirm: { + title: "Confirm", + }, + ok: { + title: "Ok", + }, + reset: { + title: "Reset", + }, + cancel: { + title: "Cancel", + }, + reload_config: { + title: "Reload config and menu", + }, + }, + misc: { + // TODO: split out WMS messages + btn_get_work: "Get work", + btn_manual_selection: "Manual selection", + stock_zero_check: { + confirm_stock_zero: "Confirm stock = 0", + confirm_stock_not_zero: "Declare stock not empty", + }, + actions_popup: { + btn_action: "Action", + }, + lines_count: "{priority_lines_count}/{lines_count}", + lines_count_extended: "{priority_lines_count}/{lines_count} position(s)", + picking_count: "{priority_picking_count}/{picking_count}", + picking_count_extended: "{priority_picking_count}/{picking_count} picking(s)", + }, + list: { + no_items: "No item to list.", + }, + select: { + no_items: "No item to select.", + }, + order_lines_by: { + priority: "Order by priority", + location: "Order by location", + }, +}; + +translation_registry.add("en-US", messages_en); diff --git a/shopfloor_mobile_printing_base/static/src/i18n/i18n.fr.js b/shopfloor_mobile_printing_base/static/src/i18n/i18n.fr.js new file mode 100644 index 00000000000..2e8a14d8b3f --- /dev/null +++ b/shopfloor_mobile_printing_base/static/src/i18n/i18n.fr.js @@ -0,0 +1,111 @@ +/** + * Copyright 2026 ACSONE SA/NV (https://acsone.eu) + * License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). + */ + +import {translation_registry} from "../services/translation_registry.js"; + +const messages_fr = { + screen: { + login: { + title: "Se connecter", + action: { + login: "Se connecter", + }, + error: { + login_invalid: "Iformations d'identification invalides", + }, + }, + home: { + title: "Accueil", + main_title: "Accueil", + version: "Version:", + action: { + nuke_data_and_reload: "Forcer rechargement des données et actualiser", + }, + }, + scan_anything: { + name: "Scanner", + title: "Scannez {what}", + scan_placeholder: "Scannez quelque chose", + }, + settings: { + title: "Paramètres", + home: { + name: "Réglages", + title: "Réglages", + }, + language: { + name: "Langue", + title: "Choisir la langue", + }, + profile: { + name: "Profil", + title: "Choisissez un profil", + profile_updated: "Profil mis à jour", + }, + fullscreen: { + enter: "Plein écran", + exit: "Quitter plein écran", + }, + }, + }, + app: { + profile_not_configured: + "Profil pas encore configuré. S'il vous plait sélectionner en un.", + profile_configure: "Configurer le profil", + loading: "Chargement en cours...", + action: { + logout: "Se déconnecter", + }, + nav: { + scenario: "Scénario:", + op_types: "Op types:", + }, + log_entry_link: "Afficher / partager l'entrée du journal", + running_env: { + prod: "Production", + integration: "Intégration", + staging: "Staging", + test: "Test", + dev: "Développement", + }, + report_issue: { + action_txt: "Besoin d'aide?", + mail: { + subject: "J'ai besoin d'aide avec l'application {app_name}", + info_warning_msg: + "VEUILLEZ NE PAS MODIFIER LES INFORMATIONS CI-DESSOUS", + }, + }, + }, + language: { + name: { + English: "Anglais", + French: "Français", + German: "Allemand", + }, + }, + btn: { + back: { + title: "Retour", + }, + confirm: { + title: "Confirmer", + }, + ok: { + title: "Ok", + }, + reset: { + title: "Réinitialiser", + }, + cancel: { + title: "Annuler", + }, + reload_config: { + title: "Recharger la configuration et le menu", + }, + }, +}; + +translation_registry.add("fr-FR", messages_fr); diff --git a/shopfloor_mobile_printing_base/static/src/scenarios/mixins.js b/shopfloor_mobile_printing_base/static/src/scenarios/mixins.js new file mode 100644 index 00000000000..a3c0e53382f --- /dev/null +++ b/shopfloor_mobile_printing_base/static/src/scenarios/mixins.js @@ -0,0 +1,12 @@ +/** + * Copyright 2025 ACSONE SA/NV + * License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). + */ + +import {ScenarioBaseMixin} from "/shopfloor_mobile_base/static/wms/src/scenario/mixins.js"; + +const methods = ScenarioBaseMixin.methods; + +methods._print_label_allowed = function () { + return this.state.data && this.state.data.allow_print_label === true; +}; diff --git a/shopfloor_mobile_printing_base/templates/assets.xml b/shopfloor_mobile_printing_base/templates/assets.xml new file mode 100644 index 00000000000..f3d4c9580d2 --- /dev/null +++ b/shopfloor_mobile_printing_base/templates/assets.xml @@ -0,0 +1,33 @@ + + + + + diff --git a/shopfloor_printing_base/README.rst b/shopfloor_printing_base/README.rst new file mode 100644 index 00000000000..dfe90fdb4a3 --- /dev/null +++ b/shopfloor_printing_base/README.rst @@ -0,0 +1,91 @@ +======================= +Shopfloor Printing Base +======================= + +.. + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! This file is generated by oca-gen-addon-readme !! + !! changes will be overwritten. !! + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! source digest: sha256:354423e30ff349592c2784af634e650eb78822978d7b6f5a1bc0dd1ca9737b29 + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + +.. |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_printing_base + :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_printing_base + :alt: Translate me on Weblate +.. |badge5| image:: https://img.shields.io/badge/runboat-Try%20me-875A7B.png + :target: https://runboat.odoo-community.org/builds?repo=OCA/wms&target_branch=16.0 + :alt: Try me on Runboat + +|badge1| |badge2| |badge3| |badge4| |badge5| + +This module allows to print reports (labels) in shpofloor flows. + +**Table of contents** + +.. contents:: + :local: + +Configuration +============= + +- Go to Shopfloor App menus +- Select the menu you want to configure +- Select the report you want to use in the 'Print Report for Labels' + +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 to smash 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 +------------ + +- Denis Roussel denis.roussel@acsone.eu + +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. + +.. |maintainer-rousseldenis| image:: https://github.com/rousseldenis.png?size=40px + :target: https://github.com/rousseldenis + :alt: rousseldenis + +Current `maintainer `__: + +|maintainer-rousseldenis| + +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_printing_base/__init__.py b/shopfloor_printing_base/__init__.py new file mode 100644 index 00000000000..0aa8326d5ca --- /dev/null +++ b/shopfloor_printing_base/__init__.py @@ -0,0 +1 @@ +from . import actions, components, models, services diff --git a/shopfloor_printing_base/__manifest__.py b/shopfloor_printing_base/__manifest__.py new file mode 100644 index 00000000000..5cb426f819e --- /dev/null +++ b/shopfloor_printing_base/__manifest__.py @@ -0,0 +1,20 @@ +# Copyright 2025 ACSONE SA/NV +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +{ + "name": "Shopfloor Printing Base", + "summary": """This module allows to provide base method to send printings""", + "version": "16.0.1.0.0", + "license": "AGPL-3", + "author": "ACSONE SA/NV,Odoo Community Association (OCA)", + "maintainers": ["rousseldenis"], + "website": "https://github.com/OCA/wms", + "depends": [ + "shopfloor_base", + "shopfloor_mobile_base", + "base_report_to_label_printer", + ], + "data": [ + "views/shopfloor_menu.xml", + ], +} diff --git a/shopfloor_printing_base/actions/__init__.py b/shopfloor_printing_base/actions/__init__.py new file mode 100644 index 00000000000..51a1e70dbdf --- /dev/null +++ b/shopfloor_printing_base/actions/__init__.py @@ -0,0 +1 @@ +from . import message diff --git a/shopfloor_printing_base/actions/message.py b/shopfloor_printing_base/actions/message.py new file mode 100644 index 00000000000..0e61068a862 --- /dev/null +++ b/shopfloor_printing_base/actions/message.py @@ -0,0 +1,24 @@ +# Copyright 2025 ACSONE SA/NV (https://acsone.eu) +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). +from odoo import _ + +from odoo.addons.component.core import Component + + +class MessageAction(Component): + _inherit = "shopfloor.message.action" + + def print_job_sent(self): + return {"message_type": "success", "body": _("Print job sent")} + + def print_error(self): + return {"message_type": "warning", "body": _("Printing error")} + + def print_no_report(self): + return { + "message_type": "warning", + "body": _( + "No report found to be printed. Check your scenario menu " + "configuration with your Administrator!" + ), + } diff --git a/shopfloor_printing_base/components/__init__.py b/shopfloor_printing_base/components/__init__.py new file mode 100644 index 00000000000..672ecddfd01 --- /dev/null +++ b/shopfloor_printing_base/components/__init__.py @@ -0,0 +1 @@ +from . import printing diff --git a/shopfloor_printing_base/components/printing.py b/shopfloor_printing_base/components/printing.py new file mode 100644 index 00000000000..3e8910fd096 --- /dev/null +++ b/shopfloor_printing_base/components/printing.py @@ -0,0 +1,44 @@ +# Copyright 2025 ACSONE SA/NV +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). +from odoo.addons.component.components.base import AbstractComponent +from odoo.addons.shopfloor_base.actions.base_action import get_actions_for + + +class ShopFloorPrintingAction(AbstractComponent): + """Base Component for actions""" + + _name = "shopfloor.printing.action" + _inherit = "shopfloor.process.action" + _collection = "shopfloor.printing" + + @property + def msg_store(self): + return get_actions_for(self, "message") + + @property + def report_to_print(self): + """ + Returns the report to print + """ + return self.work.menu.label_print_report_id.sudo() + + def print(self, record_ids, quantity=1, **kwargs) -> dict: + """ + Print the current report defined on menu level with the + defined quantity. + + return: A message dictionary + """ + report = self.report_to_print + message = dict() + if not report: + message = self.msg_store.print_no_report() + return message + result = report.print_document_client_action( + record_ids, **{"quantity": quantity} + ) + if result: + message = self.msg_store.print_job_sent() + else: + message = self.msg_store.print_error() + return message diff --git a/shopfloor_printing_base/models/__init__.py b/shopfloor_printing_base/models/__init__.py new file mode 100644 index 00000000000..8bd3d5195ca --- /dev/null +++ b/shopfloor_printing_base/models/__init__.py @@ -0,0 +1 @@ +from . import shopfloor_menu diff --git a/shopfloor_printing_base/models/shopfloor_menu.py b/shopfloor_printing_base/models/shopfloor_menu.py new file mode 100644 index 00000000000..9dcb302a6ad --- /dev/null +++ b/shopfloor_printing_base/models/shopfloor_menu.py @@ -0,0 +1,16 @@ +# Copyright 2025 ACSONE SA/NV +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). +from odoo import fields, models + + +class ShopfloorMenu(models.Model): + _inherit = "shopfloor.menu" + + label_print_report_id = fields.Many2one( + string="Print Report for Labels", + comodel_name="ir.actions.report", + domain=[("label", "=", True)], + help="Choose here the report to print. " + "Only reports with 'label' field checked will be available.", + ) + display_print_label_button = fields.Boolean() diff --git a/shopfloor_printing_base/readme/CONFIGURE.md b/shopfloor_printing_base/readme/CONFIGURE.md new file mode 100644 index 00000000000..17d65a206b6 --- /dev/null +++ b/shopfloor_printing_base/readme/CONFIGURE.md @@ -0,0 +1,3 @@ +- Go to Shopfloor App menus +- Select the menu you want to configure +- Select the report you want to use in the 'Print Report for Labels' diff --git a/shopfloor_printing_base/readme/CONTRIBUTORS.md b/shopfloor_printing_base/readme/CONTRIBUTORS.md new file mode 100644 index 00000000000..4e7e6847269 --- /dev/null +++ b/shopfloor_printing_base/readme/CONTRIBUTORS.md @@ -0,0 +1 @@ +- Denis Roussel diff --git a/shopfloor_printing_base/readme/DESCRIPTION.md b/shopfloor_printing_base/readme/DESCRIPTION.md new file mode 100644 index 00000000000..d9d31221a04 --- /dev/null +++ b/shopfloor_printing_base/readme/DESCRIPTION.md @@ -0,0 +1 @@ +This module allows to print reports (labels) in shpofloor flows. diff --git a/shopfloor_printing_base/services/__init__.py b/shopfloor_printing_base/services/__init__.py new file mode 100644 index 00000000000..c77c34ece6c --- /dev/null +++ b/shopfloor_printing_base/services/__init__.py @@ -0,0 +1,2 @@ +from . import service +from . import validator diff --git a/shopfloor_printing_base/services/service.py b/shopfloor_printing_base/services/service.py new file mode 100644 index 00000000000..d31d34c332c --- /dev/null +++ b/shopfloor_printing_base/services/service.py @@ -0,0 +1,36 @@ +# Copyright 2020 Camptocamp SA (http://www.camptocamp.com) +# Copyright 2020 Akretion (http://www.akretion.com) +# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html). + +from odoo.addons.component.core import AbstractComponent + + +class BaseShopfloorService(AbstractComponent): + """Base class for REST services""" + + _inherit = "base.shopfloor.service" + + def _printing_for(self, usage): + """ + Return the good printing component for + the current usage. + """ + printings = self.work.components_registry.lookup( + collection_name="shopfloor.printing", usage=usage + ) + return printings[0](self.work) + + def _response( + self, base_response=None, data=None, next_state=None, message=None, popup=None + ): + if self._menu.display_print_label_button: + data = data or {} + data["allow_print_label"] = True + + return super()._response( + base_response=base_response, + data=data, + next_state=next_state, + message=message, + popup=popup, + ) diff --git a/shopfloor_printing_base/services/validator.py b/shopfloor_printing_base/services/validator.py new file mode 100644 index 00000000000..a51cf10262d --- /dev/null +++ b/shopfloor_printing_base/services/validator.py @@ -0,0 +1,17 @@ +from odoo.addons.component.core import AbstractComponent + + +class BaseShopfloorValidatorResponse(AbstractComponent): + _inherit = "base.shopfloor.validator.response" + + def _get_global_fields_schemas(self) -> dict: + res = super()._get_global_fields_schemas() + res.update( + { + "allow_print_label": { + "type": "boolean", + "nullable": True, + } + } + ) + return res diff --git a/shopfloor_printing_base/static/description/icon.png b/shopfloor_printing_base/static/description/icon.png new file mode 100644 index 00000000000..3a0328b516c Binary files /dev/null and b/shopfloor_printing_base/static/description/icon.png differ diff --git a/shopfloor_printing_base/static/description/index.html b/shopfloor_printing_base/static/description/index.html new file mode 100644 index 00000000000..a7c6cbc614d --- /dev/null +++ b/shopfloor_printing_base/static/description/index.html @@ -0,0 +1,434 @@ + + + + + +Shopfloor Printing Base + + + +
+

Shopfloor Printing Base

+ + +

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

+

This module allows to print reports (labels) in shpofloor flows.

+

Table of contents

+ +
+

Configuration

+
    +
  • Go to Shopfloor App menus
  • +
  • Select the menu you want to configure
  • +
  • Select the report you want to use in the ‘Print Report for Labels’
  • +
+
+
+

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 to smash 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.

+

Current maintainer:

+

rousseldenis

+

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_printing_base/tests/__init__.py b/shopfloor_printing_base/tests/__init__.py new file mode 100644 index 00000000000..72ebd9fd4a5 --- /dev/null +++ b/shopfloor_printing_base/tests/__init__.py @@ -0,0 +1 @@ +from . import test_printing diff --git a/shopfloor_printing_base/tests/access.xml b/shopfloor_printing_base/tests/access.xml new file mode 100644 index 00000000000..265578d39bc --- /dev/null +++ b/shopfloor_printing_base/tests/access.xml @@ -0,0 +1,17 @@ + + + + shopfloor_test + + + + + + + + + diff --git a/shopfloor_printing_base/tests/models.py b/shopfloor_printing_base/tests/models.py new file mode 100644 index 00000000000..dac14debd09 --- /dev/null +++ b/shopfloor_printing_base/tests/models.py @@ -0,0 +1,48 @@ +# Copyright 2025 ACSONE SA/NV +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). +from odoo import models + +from odoo.addons.base_rest.components.service import to_int +from odoo.addons.component.core import Component + + +class ShopFloorPrintingAction(Component): + """Base Component for actions""" + + _name = "shopfloor.test.printing.action" + _inherit = "shopfloor.printing.action" + _collection = "shopfloor.printing" + _usage = "test" + + +class ShopfloorTestFlow(Component): + """ + Test Shopfloor Flow + """ + + _inherit = "base.shopfloor.process" + _name = "shopfloor.test.flow" + _collection = "shopfloor.action" + _apply_on = "shopfloor.test.model" + _usage = "test" + _description = __doc__ + + def test(self): + return self._response(data={"id": id}) + + +class ShopfloorTestValidator(Component): + """Validators for the Test endpoints""" + + _inherit = "base.shopfloor.validator" + _name = "shopfloor.test.validator" + _usage = "test.validator" + + def test(self): + return {"id": {"coerce": to_int, "required": True, "type": "integer"}} + + +class ShopfloorTestModel(models.Model): + + _name = "shopfloor.test.model" + _description = "Test Model" diff --git a/shopfloor_printing_base/tests/report.xml b/shopfloor_printing_base/tests/report.xml new file mode 100644 index 00000000000..f73b16103dc --- /dev/null +++ b/shopfloor_printing_base/tests/report.xml @@ -0,0 +1,22 @@ + + + + + + Test + shopfloor.test.model + qweb-pdf + shopfloor_printing_base.report_saleorder_document + shopfloor_printing_base.report_saleorder_document + + diff --git a/shopfloor_printing_base/tests/test_printing.py b/shopfloor_printing_base/tests/test_printing.py new file mode 100644 index 00000000000..1e23fb8a68a --- /dev/null +++ b/shopfloor_printing_base/tests/test_printing.py @@ -0,0 +1,156 @@ +# Copyright 2025 ACSONE SA/NV +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl) +# pylint: disable=missing-return +from contextlib import contextmanager +from unittest import mock + +from odoo_test_helper.fake_model_loader import FakeModelLoader + +from odoo.modules.module import get_resource_path +from odoo.tools import convert_file + +from odoo.addons.base_report_to_printer.models.printing_printer import PrintingPrinter +from odoo.addons.base_rest.controllers.main import _PseudoCollection +from odoo.addons.component.core import WorkContext +from odoo.addons.shopfloor_base.tests.common import CommonCase as BaseCommonCase + + +class TestPrinting(BaseCommonCase): + @classmethod + def setUpClass(cls): + super().setUpClass() + printer_server = cls.env["printing.server"].sudo().create({}) + cls.printer = ( + cls.env["printing.printer"] + .sudo() + .create( + { + "name": "TEST", + "system_name": "TEST", + "server_id": printer_server.id, + } + ) + ) + cls.env.user.printing_printer_id = cls.printer + cls.menu = cls.env.ref("shopfloor_base.shopfloor_menu_demo_1") + from .models import ( + ShopFloorPrintingAction, + ShopfloorTestFlow, + ShopfloorTestValidator, + ) + + ShopFloorPrintingAction._build_component(cls._components_registry) + ShopfloorTestFlow._build_component(cls._components_registry) + ShopfloorTestValidator._build_component(cls._components_registry) + + cls.loader = FakeModelLoader(cls.env, cls.__module__) + cls.loader.backup_registry() + + # The fake class is imported here !! After the backup_registry + from .models import ShopfloorTestModel + + cls.loader.update_registry((ShopfloorTestModel,)) + this_module = "shopfloor_printing_base" + convert_file( + cls.env.cr, + "shopfloor_printing_base", + get_resource_path(this_module, "tests/report.xml"), + {}, + mode="init", + noupdate=False, + kind="test", + ) + convert_file( + cls.env.cr, + "shopfloor_printing_base", + get_resource_path(this_module, "tests/access.xml"), + {}, + mode="init", + noupdate=False, + kind="test", + ) + + @classmethod + def tearDownClass(cls): + cls.loader.restore_registry() + super().tearDownClass() + + @contextmanager + def _work_on_actions(self, **params): + params = params or {} + collection = _PseudoCollection("shopfloor.action", self.env) + yield WorkContext( + model_name="shopfloor.test.model", + components_registry=self._components_registry, + collection=collection, + **params + ) + + def setUp(self): + super().setUp() + # Get a base scenario + with self._work_on_actions(menu=self.menu) as work_action: + self.service = work_action.component(usage="test") + self.test_record = self.env["shopfloor.test.model"].sudo().create({}) + + def test_print_no_report(self): + with mock.patch.object(PrintingPrinter, "print_document") as mock_print: + printing = self.service._printing_for("test") + mock_print.return_value = False + response = printing.print( + record_ids=self.test_record.ids, + quantity=1, + ) + mock_print.assert_not_called() + self.assertDictEqual( + { + "message_type": "warning", + "body": "No report found to be printed. Check your " + "scenario menu configuration with your Administrator!", + }, + response, + ) + + def test_print_report(self): + self.service.work.menu.sudo().label_print_report_id = self.env.ref( + "shopfloor_printing_base.report_test_document" + ) + with mock.patch.object(PrintingPrinter, "print_document") as mock_print: + printing = self.service._printing_for("test") + + # Document is printed + mock_print.return_value = True + response = printing.print( + record_ids=self.test_record.ids, + quantity=1, + ) + mock_print.assert_called() + self.assertDictEqual( + {"message_type": "success", "body": "Print job sent"}, + response, + ) + + def test_print_report_error(self): + self.service.work.menu.sudo().label_print_report_id = self.env.ref( + "shopfloor_printing_base.report_test_document" + ) + with mock.patch.object(PrintingPrinter, "print_document") as mock_print: + printing = self.service._printing_for("test") + + # Document is not printed + mock_print.return_value = False + response = printing.print( + record_ids=self.test_record.ids, + quantity=1, + ) + mock_print.assert_called() + self.assertDictEqual( + {"message_type": "warning", "body": "Printing error"}, + response, + ) + + def test_response(self): + self.service.work.menu.sudo().display_print_label_button = True + response = self.service._response(data={"id": id}) + allow = response.get("data", {}).get("allow_print_label") + self.assertTrue(allow) diff --git a/shopfloor_printing_base/views/shopfloor_menu.xml b/shopfloor_printing_base/views/shopfloor_menu.xml new file mode 100644 index 00000000000..feaf78f1fa6 --- /dev/null +++ b/shopfloor_printing_base/views/shopfloor_menu.xml @@ -0,0 +1,19 @@ + + + + + shopfloor.menu + + + + + + + + + + + + + diff --git a/test-requirements.txt b/test-requirements.txt index 689482e20df..0ae2d91d39c 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -1,2 +1,4 @@ vcrpy-unittest odoo_test_helper + +odoo-addon-base-report-to-printer @ git+https://github.com/OCA/report-print-send@refs/pull/413/head#subdirectory=setup/base_report_to_printer