From d283df6f9e35177f1380a27e190a94a34b787e50 Mon Sep 17 00:00:00 2001 From: Simone Orsi Date: Tue, 2 Feb 2021 10:11:19 +0100 Subject: [PATCH 001/111] Add shopfloor_base (split from shopfloor) --- shopfloor_base/README.rst | 193 ++++++++++++ shopfloor_base/__init__.py | 4 + shopfloor_base/__manifest__.py | 37 +++ shopfloor_base/actions/__init__.py | 32 ++ shopfloor_base/actions/base_action.py | 18 ++ shopfloor_base/actions/data.py | 40 +++ shopfloor_base/actions/message.py | 42 +++ shopfloor_base/actions/savepoint.py | 46 +++ shopfloor_base/actions/search.py | 15 + shopfloor_base/controllers/__init__.py | 1 + shopfloor_base/controllers/main.py | 11 + shopfloor_base/data/module_category_data.xml | 7 + shopfloor_base/demo/auth_api_key_demo.xml | 7 + shopfloor_base/demo/shopfloor_menu_demo.xml | 6 + .../demo/shopfloor_profile_demo.xml | 9 + shopfloor_base/models/__init__.py | 2 + shopfloor_base/models/shopfloor_menu.py | 14 + shopfloor_base/models/shopfloor_profile.py | 17 ++ shopfloor_base/readme/CONFIGURE.rst | 38 +++ shopfloor_base/readme/CONTRIBUTORS.rst | 12 + shopfloor_base/readme/CREDITS.rst | 5 + shopfloor_base/readme/DESCRIPTION.rst | 14 + shopfloor_base/readme/HISTORY.rst | 4 + shopfloor_base/readme/ROADMAP.rst | 2 + shopfloor_base/readme/USAGE.rst | 6 + shopfloor_base/security/groups.xml | 21 ++ shopfloor_base/security/ir.model.access.csv | 5 + shopfloor_base/services/__init__.py | 14 + shopfloor_base/services/app.py | 61 ++++ shopfloor_base/services/forms/__init__.py | 1 + shopfloor_base/services/forms/form_mixin.py | 85 ++++++ shopfloor_base/services/menu.py | 118 ++++++++ shopfloor_base/services/profile.py | 80 +++++ shopfloor_base/services/scan_anything.py | 151 ++++++++++ shopfloor_base/services/schema.py | 58 ++++ shopfloor_base/services/service.py | 283 ++++++++++++++++++ shopfloor_base/services/user.py | 84 ++++++ shopfloor_base/services/validator.py | 249 +++++++++++++++ shopfloor_base/tests/__init__.py | 7 + shopfloor_base/tests/common.py | 155 ++++++++++ shopfloor_base/tests/common_misc.py | 53 ++++ shopfloor_base/tests/test_actions_data.py | 31 ++ shopfloor_base/tests/test_app.py | 31 ++ shopfloor_base/tests/test_menu.py | 27 ++ shopfloor_base/tests/test_openapi.py | 24 ++ shopfloor_base/tests/test_profile.py | 26 ++ shopfloor_base/tests/test_scan_anything.py | 37 +++ shopfloor_base/tests/test_user.py | 47 +++ shopfloor_base/utils.py | 23 ++ shopfloor_base/views/menus.xml | 29 ++ shopfloor_base/views/shopfloor_menu.xml | 79 +++++ .../views/shopfloor_profile_views.xml | 57 ++++ 52 files changed, 2418 insertions(+) create mode 100644 shopfloor_base/README.rst create mode 100644 shopfloor_base/__init__.py create mode 100644 shopfloor_base/__manifest__.py create mode 100644 shopfloor_base/actions/__init__.py create mode 100644 shopfloor_base/actions/base_action.py create mode 100644 shopfloor_base/actions/data.py create mode 100644 shopfloor_base/actions/message.py create mode 100644 shopfloor_base/actions/savepoint.py create mode 100644 shopfloor_base/actions/search.py create mode 100644 shopfloor_base/controllers/__init__.py create mode 100644 shopfloor_base/controllers/main.py create mode 100644 shopfloor_base/data/module_category_data.xml create mode 100644 shopfloor_base/demo/auth_api_key_demo.xml create mode 100644 shopfloor_base/demo/shopfloor_menu_demo.xml create mode 100644 shopfloor_base/demo/shopfloor_profile_demo.xml create mode 100644 shopfloor_base/models/__init__.py create mode 100644 shopfloor_base/models/shopfloor_menu.py create mode 100644 shopfloor_base/models/shopfloor_profile.py create mode 100644 shopfloor_base/readme/CONFIGURE.rst create mode 100644 shopfloor_base/readme/CONTRIBUTORS.rst create mode 100644 shopfloor_base/readme/CREDITS.rst create mode 100644 shopfloor_base/readme/DESCRIPTION.rst create mode 100644 shopfloor_base/readme/HISTORY.rst create mode 100644 shopfloor_base/readme/ROADMAP.rst create mode 100644 shopfloor_base/readme/USAGE.rst create mode 100644 shopfloor_base/security/groups.xml create mode 100644 shopfloor_base/security/ir.model.access.csv create mode 100644 shopfloor_base/services/__init__.py create mode 100644 shopfloor_base/services/app.py create mode 100644 shopfloor_base/services/forms/__init__.py create mode 100644 shopfloor_base/services/forms/form_mixin.py create mode 100644 shopfloor_base/services/menu.py create mode 100644 shopfloor_base/services/profile.py create mode 100644 shopfloor_base/services/scan_anything.py create mode 100644 shopfloor_base/services/schema.py create mode 100644 shopfloor_base/services/service.py create mode 100644 shopfloor_base/services/user.py create mode 100644 shopfloor_base/services/validator.py create mode 100644 shopfloor_base/tests/__init__.py create mode 100644 shopfloor_base/tests/common.py create mode 100644 shopfloor_base/tests/common_misc.py create mode 100644 shopfloor_base/tests/test_actions_data.py create mode 100644 shopfloor_base/tests/test_app.py create mode 100644 shopfloor_base/tests/test_menu.py create mode 100644 shopfloor_base/tests/test_openapi.py create mode 100644 shopfloor_base/tests/test_profile.py create mode 100644 shopfloor_base/tests/test_scan_anything.py create mode 100644 shopfloor_base/tests/test_user.py create mode 100644 shopfloor_base/utils.py create mode 100644 shopfloor_base/views/menus.xml create mode 100644 shopfloor_base/views/shopfloor_menu.xml create mode 100644 shopfloor_base/views/shopfloor_profile_views.xml diff --git a/shopfloor_base/README.rst b/shopfloor_base/README.rst new file mode 100644 index 0000000000..497f2adb9a --- /dev/null +++ b/shopfloor_base/README.rst @@ -0,0 +1,193 @@ +======================================= +TODO: Shopfloor BASE FIXME AFTER SPLIT! +======================================= + +.. !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! This file is generated by oca-gen-addon-readme !! + !! changes will be overwritten. !! + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + +.. |badge1| image:: https://img.shields.io/badge/maturity-Alpha-red.png + :target: https://odoo-community.org/page/development-status + :alt: Alpha +.. |badge2| image:: https://img.shields.io/badge/licence-AGPL--3-blue.png + :target: http://www.gnu.org/licenses/agpl-3.0-standalone.html + :alt: License: AGPL-3 +.. |badge3| image:: https://img.shields.io/badge/github-OCA%2Fwms-lightgray.png?logo=github + :target: https://github.com/OCA/wms/tree/13.0/shopfloor + :alt: OCA/wms +.. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png + :target: https://translation.odoo-community.org/projects/wms-13-0/wms-13-0-shopfloor + :alt: Translate me on Weblate +.. |badge5| image:: https://img.shields.io/badge/runbot-Try%20me-875A7B.png + :target: https://runbot.odoo-community.org/runbot/285/13.0 + :alt: Try me on Runbot + +|badge1| |badge2| |badge3| |badge4| |badge5| + +Shopfloor is a barcode scanner application for internal warehouse operations. + +The application supports scenarios, to relate to Operation Types: + +* Cluster Picking +* Zone Picking +* Checkout/Packing +* Delivery +* Location Content Transfer +* Single Pack Transfer + +This module provides REST APIs to support the scenarios. It needs a frontend +to consume the backend APIs and provide screens for users on barcode devices. +A default front-end application is provided by ``shopfloor_mobile``. + +.. IMPORTANT:: + This is an alpha version, the data model and design can change at any time without warning. + Only for development or testing purpose, do not use in production. + `More details on development status `_ + +**Table of contents** + +.. contents:: + :local: + +Configuration +============= + +Profiles +~~~~~~~~ + +In Inventory / Configuration / Shopfloor / Profiles. + +The profiles are used to restrict which menus are shown on the frontend +application. When a user logs in the scanner application, they have to +select their profile, so the correct menus are shown. + +Menus +~~~~~ + +In Inventory / Configuration / Shopfloor / Menus. + +The menus are displayed on the frontend application and store the configuration +of the scenarios. Each menu must use a scenario and defines which Operation Types +they are allowed to process. + +Their profile will restrict the visibility to the profile chosen on the device. +If a menu has no profile, it is shown in every profile. + +Some scenarios may have additional options, which are explained in tooltips. + +Logs retention +~~~~~~~~~~~~~~ + +Logs are kept in database for every REST requests made by a client application. +They can be used for debugging and monitoring of the activity. + +The Logs menu is shown only with Developer tools (``?debug=1``) activated. + +By default, Shopfloor logs are kept 30 days. +You can change the duration of the retention by changing the System Parameter +``shopfloor.log.retention.days``. + +If the value is set to 0, the logs are not stored at all. + +Logged data is: request URL and method, parameters, headers, result or error. + +Usage +===== + +An API key is created in the Demo data (for development), using +the Demo user. The key to use in the HTTP header ``API-KEY`` is: 72B044F7AC780DAC + +Curl example:: + + curl -X POST "http://localhost:8069/shopfloor/user/menu" -H "accept: */*" -H "Content-Type: application/json" -H "API-KEY: 72B044F7AC780DAC" + +Known issues / Roadmap +====================== + +* improve documentation +* split out scenario components to their own modules + +Changelog +========= + +13.0.1.0.0 +~~~~~~~~~~ + +First official version. + +Bug Tracker +=========== + +Bugs are tracked on `GitHub Issues `_. +In case of trouble, please check there if your issue has already been reported. +If you spotted it first, help us smashing it by providing a detailed and welcomed +`feedback `_. + +Do not contact contributors directly about support or help with technical issues. + +Credits +======= + +Authors +~~~~~~~ + +* Camptocamp +* BCIM +* Akretion + +Contributors +~~~~~~~~~~~~ + +* Guewen Baconnier +* Simone Orsi +* Sébastien Alix +* Alexandre Fayolle +* Benoit Guillot +* Thierry Ducrest + +Design +~~~~~~ + +* Joël Grand-Guillaume +* Jacques-Etienne Baudoux + +Other credits +~~~~~~~~~~~~~ + +**Financial support** + +* Cosanum +* Camptocamp R&D +* Akretion R&D + +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-guewen| image:: https://github.com/guewen.png?size=40px + :target: https://github.com/guewen + :alt: guewen +.. |maintainer-simahawk| image:: https://github.com/simahawk.png?size=40px + :target: https://github.com/simahawk + :alt: simahawk +.. |maintainer-sebalix| image:: https://github.com/sebalix.png?size=40px + :target: https://github.com/sebalix + :alt: sebalix + +Current `maintainers `__: + +|maintainer-guewen| |maintainer-simahawk| |maintainer-sebalix| + +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_base/__init__.py b/shopfloor_base/__init__.py new file mode 100644 index 0000000000..6a34e5681f --- /dev/null +++ b/shopfloor_base/__init__.py @@ -0,0 +1,4 @@ +from . import controllers +from . import models +from . import actions +from . import services diff --git a/shopfloor_base/__manifest__.py b/shopfloor_base/__manifest__.py new file mode 100644 index 0000000000..939d9be33a --- /dev/null +++ b/shopfloor_base/__manifest__.py @@ -0,0 +1,37 @@ +# Copyright 2020 Camptocamp SA (http://www.camptocamp.com) +# Copyright 2020 Akretion (http://www.akretion.com) +# Copyright 2020 BCIM +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). + +{ + "name": "Shopfloor Base", + "summary": "Core module for creating mobile apps", + "version": "13.0.1.0.0", + "development_status": "Alpha", + "category": "Inventory", + "website": "https://github.com/OCA/wms", + "author": "Camptocamp, BCIM, Akretion, Odoo Community Association (OCA)", + "maintainers": ["guewen", "simahawk", "sebalix"], + "license": "AGPL-3", + "application": True, + "depends": [ + "base_jsonify", + "base_rest", + "rest_log", + "base_sparse_field", + "auth_api_key", + ], + "data": [ + "data/module_category_data.xml", + "security/groups.xml", + "security/ir.model.access.csv", + "views/shopfloor_menu.xml", + "views/shopfloor_profile_views.xml", + "views/menus.xml", + ], + "demo": [ + "demo/auth_api_key_demo.xml", + "demo/shopfloor_menu_demo.xml", + "demo/shopfloor_profile_demo.xml", + ], +} diff --git a/shopfloor_base/actions/__init__.py b/shopfloor_base/actions/__init__.py new file mode 100644 index 0000000000..ec054bf649 --- /dev/null +++ b/shopfloor_base/actions/__init__.py @@ -0,0 +1,32 @@ +""" +Support actions available from any Service Components. + +To use an Action Component, a Service component + +Difference with Service components: + +* Public methods of a Service Components are exposed in the REST API, + Action Components are never exposed + +An Action component can be get from Service or Action Components using +``self.actions_for(usage)``. + +The goal of the Action Components is to share common actions +and processes between Services, avoid having too much logic in +Services. + +""" +from . import base_action +from . import data +from . import message +from . import search +from . import savepoint + +# TODO: kept in shopfloor -> review if these must stay there +# from . import change_package_lot +# from . import data_detail +# from . import completion_info +# from . import location_content_transfer_sorter +# from . import move_line_search +# from . import stock +# from . import inventory diff --git a/shopfloor_base/actions/base_action.py b/shopfloor_base/actions/base_action.py new file mode 100644 index 0000000000..fb7eb2ae12 --- /dev/null +++ b/shopfloor_base/actions/base_action.py @@ -0,0 +1,18 @@ +# Copyright 2020 Camptocamp SA (http://www.camptocamp.com) +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). +from odoo.addons.component.core import AbstractComponent + + +class ShopFloorProcessAction(AbstractComponent): + """Base Component for actions""" + + _name = "shopfloor.process.action" + _collection = "shopfloor.action" + _usage = "actions" + + def actions_for(self, usage): + return self.component(usage=usage) + + @property + def msg_store(self): + return self.actions_for("message") diff --git a/shopfloor_base/actions/data.py b/shopfloor_base/actions/data.py new file mode 100644 index 0000000000..98722840c3 --- /dev/null +++ b/shopfloor_base/actions/data.py @@ -0,0 +1,40 @@ +# Copyright 2020 Camptocamp SA (http://www.camptocamp.com) +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). +from odoo.addons.component.core import Component + +from ..utils import ensure_model + + +class DataAction(Component): + """Provide methods to share data structures + + The methods should be used in Service Components, so we try to + have similar data structures across scenarios. + """ + + _name = "shopfloor.data.action" + _inherit = "shopfloor.process.action" + _usage = "data" + + def _jsonify(self, recordset, parser, multi=False, **kw): + res = recordset.jsonify(parser) + if not multi: + return res[0] if res else None + return res + + def _simple_record_parser(self): + return ["id", "name"] + + def _select_value_to_label(self, rec, fname): + return rec._fields[fname].convert_to_export(rec[fname], rec) + + @ensure_model("res.partner") + def partner(self, record, **kw): + return self._jsonify(record, self._partner_parser, **kw) + + def partners(self, record, **kw): + return self.partner(record, multi=True) + + @property + def _partner_parser(self): + return self._simple_record_parser() diff --git a/shopfloor_base/actions/message.py b/shopfloor_base/actions/message.py new file mode 100644 index 0000000000..33702d19d9 --- /dev/null +++ b/shopfloor_base/actions/message.py @@ -0,0 +1,42 @@ +# Copyright 2020 Camptocamp SA (http://www.camptocamp.com) +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). +# from odoo import _ + +from odoo.addons.component.core import Component + + +class MessageAction(Component): + """Provide message templates + + The methods should be used in Service Components, in order to share as much + as possible the messages for similar events. + + Before adding a message, please look if no message already exists, + and consider making an existing message more generic. + """ + + _name = "shopfloor.message.action" + _inherit = "shopfloor.process.action" + _usage = "message" + + # TODO: we should probably have `shopfloor.message` records + # and here we can simply lookup for a message using its identifier + # Eg: + # + # def _get_message(self, key): + # domain = [ + # ("type", "=", "action"), + # ("key", "=", key), + # ] + # return self.env["shopfloor.message"].search(domain) + # + # def message(self, key, **kw): + # msg = self._get_message(key) + # return { + # "type": msg.type, + # "body": msg.body % kw + # } + # + # then all depending modules can simply create records they need + # instea of overriding and polluting the component. + # Additional goodie: users can edit messages via UI. diff --git a/shopfloor_base/actions/savepoint.py b/shopfloor_base/actions/savepoint.py new file mode 100644 index 0000000000..96daa8fe61 --- /dev/null +++ b/shopfloor_base/actions/savepoint.py @@ -0,0 +1,46 @@ +# Copyright 2020 Camptocamp SA (http://www.camptocamp.com) +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). +import uuid + +from psycopg2 import sql + +from odoo.sql_db import clear_env, flush_env + +from odoo.addons.component.core import Component + + +class SavepointBuilder(Component): + """Return a new Savepoint instance""" + + _name = "shopfloor.savepoint.action" + _inherit = "shopfloor.process.action" + _usage = "savepoint" + + def new(self): + return Savepoint(self.env.cr) + + +class Savepoint(object): + """Wrapper for SQL Savepoint + + Close to "cr.savepoint()" context manager but this class gives more control + over when the release/rollback are called. + """ + + def __init__(self, cr): + self._cr = cr + self.name = uuid.uuid1().hex + flush_env(self._cr, clear=False) + self._execute("SAVEPOINT {}") + + def rollback(self): + clear_env(self._cr) + self._execute("ROLLBACK TO SAVEPOINT {}") + + def release(self): + flush_env(self._cr, clear=False) + self._execute("RELEASE SAVEPOINT {}") + + def _execute(self, query): + # pylint: disable=sql-injection + self._cr.execute(sql.SQL(query).format(sql.Identifier(self.name))) diff --git a/shopfloor_base/actions/search.py b/shopfloor_base/actions/search.py new file mode 100644 index 0000000000..d963555bb3 --- /dev/null +++ b/shopfloor_base/actions/search.py @@ -0,0 +1,15 @@ +# Copyright 2020 Camptocamp SA (http://www.camptocamp.com) +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). +from odoo.addons.component.core import Component + + +class SearchAction(Component): + """Provide methods to search records from scanner + + The methods should be used in Service Components, so a search will always + have the same result in all scenarios. + """ + + _name = "shopfloor.search.action" + _inherit = "shopfloor.process.action" + _usage = "search" diff --git a/shopfloor_base/controllers/__init__.py b/shopfloor_base/controllers/__init__.py new file mode 100644 index 0000000000..12a7e529b6 --- /dev/null +++ b/shopfloor_base/controllers/__init__.py @@ -0,0 +1 @@ +from . import main diff --git a/shopfloor_base/controllers/main.py b/shopfloor_base/controllers/main.py new file mode 100644 index 0000000000..218e08e3b7 --- /dev/null +++ b/shopfloor_base/controllers/main.py @@ -0,0 +1,11 @@ +# Copyright 2020 Camptocamp SA (http://www.camptocamp.com) +# Copyright 2020 Akretion (http://www.akretion.com) +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). + +from odoo.addons.base_rest.controllers import main + + +class ShopfloorController(main.RestController): + _root_path = "/shopfloor/" + _collection_name = "shopfloor.service" + _default_auth = "api_key" diff --git a/shopfloor_base/data/module_category_data.xml b/shopfloor_base/data/module_category_data.xml new file mode 100644 index 0000000000..2f5a178be9 --- /dev/null +++ b/shopfloor_base/data/module_category_data.xml @@ -0,0 +1,7 @@ + + + + Shopfloor + 90 + + diff --git a/shopfloor_base/demo/auth_api_key_demo.xml b/shopfloor_base/demo/auth_api_key_demo.xml new file mode 100644 index 0000000000..7858d1d044 --- /dev/null +++ b/shopfloor_base/demo/auth_api_key_demo.xml @@ -0,0 +1,7 @@ + + + Demo + + 72B044F7AC780DAC + + diff --git a/shopfloor_base/demo/shopfloor_menu_demo.xml b/shopfloor_base/demo/shopfloor_menu_demo.xml new file mode 100644 index 0000000000..0adfe6e7c1 --- /dev/null +++ b/shopfloor_base/demo/shopfloor_menu_demo.xml @@ -0,0 +1,6 @@ + + + Simple Thing To Do + 20 + + diff --git a/shopfloor_base/demo/shopfloor_profile_demo.xml b/shopfloor_base/demo/shopfloor_profile_demo.xml new file mode 100644 index 0000000000..8c96f116b4 --- /dev/null +++ b/shopfloor_base/demo/shopfloor_profile_demo.xml @@ -0,0 +1,9 @@ + + + + Demo Profile 1 + + + Demo Profile 2 + + diff --git a/shopfloor_base/models/__init__.py b/shopfloor_base/models/__init__.py new file mode 100644 index 0000000000..bfa2f6eda4 --- /dev/null +++ b/shopfloor_base/models/__init__.py @@ -0,0 +1,2 @@ +from . import shopfloor_menu +from . import shopfloor_profile diff --git a/shopfloor_base/models/shopfloor_menu.py b/shopfloor_base/models/shopfloor_menu.py new file mode 100644 index 0000000000..792d7a1139 --- /dev/null +++ b/shopfloor_base/models/shopfloor_menu.py @@ -0,0 +1,14 @@ +# Copyright 2020 Camptocamp SA (http://www.camptocamp.com) +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). +from odoo import fields, models + + +class ShopfloorMenu(models.Model): + _name = "shopfloor.menu" + _description = "Menu displayed in the scanner application" + _order = "sequence" + + name = fields.Char(required=True, translate=True) + sequence = fields.Integer() + scenario = fields.Char() + active = fields.Boolean(default=True) diff --git a/shopfloor_base/models/shopfloor_profile.py b/shopfloor_base/models/shopfloor_profile.py new file mode 100644 index 0000000000..060df4355e --- /dev/null +++ b/shopfloor_base/models/shopfloor_profile.py @@ -0,0 +1,17 @@ +# Copyright 2020 Camptocamp SA (http://www.camptocamp.com) +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). +from odoo import fields, models + + +class ShopfloorProfile(models.Model): + _name = "shopfloor.profile" + _description = "Shopfloor profile settings" + + name = fields.Char(required=True) + menu_ids = fields.One2many( + comodel_name="shopfloor.menu", + inverse_name="profile_id", + string="Menus", + help="Menus visible for this profile", + ) + active = fields.Boolean(default=True) diff --git a/shopfloor_base/readme/CONFIGURE.rst b/shopfloor_base/readme/CONFIGURE.rst new file mode 100644 index 0000000000..92102a8b42 --- /dev/null +++ b/shopfloor_base/readme/CONFIGURE.rst @@ -0,0 +1,38 @@ +Profiles +~~~~~~~~ + +In Inventory / Configuration / Shopfloor / Profiles. + +The profiles are used to restrict which menus are shown on the frontend +application. When a user logs in the scanner application, they have to +select their profile, so the correct menus are shown. + +Menus +~~~~~ + +In Inventory / Configuration / Shopfloor / Menus. + +The menus are displayed on the frontend application and store the configuration +of the scenarios. Each menu must use a scenario and defines which Operation Types +they are allowed to process. + +Their profile will restrict the visibility to the profile chosen on the device. +If a menu has no profile, it is shown in every profile. + +Some scenarios may have additional options, which are explained in tooltips. + +Logs retention +~~~~~~~~~~~~~~ + +Logs are kept in database for every REST requests made by a client application. +They can be used for debugging and monitoring of the activity. + +The Logs menu is shown only with Developer tools (``?debug=1``) activated. + +By default, Shopfloor logs are kept 30 days. +You can change the duration of the retention by changing the System Parameter +``shopfloor.log.retention.days``. + +If the value is set to 0, the logs are not stored at all. + +Logged data is: request URL and method, parameters, headers, result or error. diff --git a/shopfloor_base/readme/CONTRIBUTORS.rst b/shopfloor_base/readme/CONTRIBUTORS.rst new file mode 100644 index 0000000000..4d4d24ed1b --- /dev/null +++ b/shopfloor_base/readme/CONTRIBUTORS.rst @@ -0,0 +1,12 @@ +* Guewen Baconnier +* Simone Orsi +* Sébastien Alix +* Alexandre Fayolle +* Benoit Guillot +* Thierry Ducrest + +Design +~~~~~~ + +* Joël Grand-Guillaume +* Jacques-Etienne Baudoux diff --git a/shopfloor_base/readme/CREDITS.rst b/shopfloor_base/readme/CREDITS.rst new file mode 100644 index 0000000000..dd789bcc35 --- /dev/null +++ b/shopfloor_base/readme/CREDITS.rst @@ -0,0 +1,5 @@ +**Financial support** + +* Cosanum +* Camptocamp R&D +* Akretion R&D diff --git a/shopfloor_base/readme/DESCRIPTION.rst b/shopfloor_base/readme/DESCRIPTION.rst new file mode 100644 index 0000000000..6242bc91fc --- /dev/null +++ b/shopfloor_base/readme/DESCRIPTION.rst @@ -0,0 +1,14 @@ +Shopfloor is a barcode scanner application for internal warehouse operations. + +The application supports scenarios, to relate to Operation Types: + +* Cluster Picking +* Zone Picking +* Checkout/Packing +* Delivery +* Location Content Transfer +* Single Pack Transfer + +This module provides REST APIs to support the scenarios. It needs a frontend +to consume the backend APIs and provide screens for users on barcode devices. +A default front-end application is provided by ``shopfloor_mobile``. diff --git a/shopfloor_base/readme/HISTORY.rst b/shopfloor_base/readme/HISTORY.rst new file mode 100644 index 0000000000..dc27ee2605 --- /dev/null +++ b/shopfloor_base/readme/HISTORY.rst @@ -0,0 +1,4 @@ +13.0.1.0.0 +~~~~~~~~~~ + +First official version. diff --git a/shopfloor_base/readme/ROADMAP.rst b/shopfloor_base/readme/ROADMAP.rst new file mode 100644 index 0000000000..7f274ec28f --- /dev/null +++ b/shopfloor_base/readme/ROADMAP.rst @@ -0,0 +1,2 @@ +* improve documentation +* split out scenario components to their own modules diff --git a/shopfloor_base/readme/USAGE.rst b/shopfloor_base/readme/USAGE.rst new file mode 100644 index 0000000000..9f4832dd09 --- /dev/null +++ b/shopfloor_base/readme/USAGE.rst @@ -0,0 +1,6 @@ +An API key is created in the Demo data (for development), using +the Demo user. The key to use in the HTTP header ``API-KEY`` is: 72B044F7AC780DAC + +Curl example:: + + curl -X POST "http://localhost:8069/shopfloor/user/menu" -H "accept: */*" -H "Content-Type: application/json" -H "API-KEY: 72B044F7AC780DAC" diff --git a/shopfloor_base/security/groups.xml b/shopfloor_base/security/groups.xml new file mode 100644 index 0000000000..baedacaa99 --- /dev/null +++ b/shopfloor_base/security/groups.xml @@ -0,0 +1,21 @@ + + + + + Shopfloor User + + + + Shopfloor Manager + + + + + diff --git a/shopfloor_base/security/ir.model.access.csv b/shopfloor_base/security/ir.model.access.csv new file mode 100644 index 0000000000..6558c139bb --- /dev/null +++ b/shopfloor_base/security/ir.model.access.csv @@ -0,0 +1,5 @@ +"id","name","model_id/id","group_id/id","perm_read","perm_write","perm_create","perm_unlink" +"access_shopfloor_menu_users","shopfloor menu","model_shopfloor_menu","shopfloor_base.group_shopfloor_user",1,0,0,0 +"access_shopfloor_menu_manager","shopfloor menu manager","model_shopfloor_menu","shopfloor_base.group_shopfloor_manager",1,1,1,1 +"access_shopfloor_profile_users","shopfloor profile","model_shopfloor_profile","shopfloor_base.group_shopfloor_user",1,0,0,0 +"access_shopfloor_profile_manager","shopfloor profile manager","model_shopfloor_profile","shopfloor_base.group_shopfloor_manager",1,1,1,1 diff --git a/shopfloor_base/services/__init__.py b/shopfloor_base/services/__init__.py new file mode 100644 index 0000000000..0ae4a9285e --- /dev/null +++ b/shopfloor_base/services/__init__.py @@ -0,0 +1,14 @@ +# core classes +from . import service +from . import schema +from . import validator + +# generic services +from . import app +from . import user +from . import menu +from . import profile +from . import scan_anything + +# forms +from . import forms diff --git a/shopfloor_base/services/app.py b/shopfloor_base/services/app.py new file mode 100644 index 0000000000..ecbc8f21c0 --- /dev/null +++ b/shopfloor_base/services/app.py @@ -0,0 +1,61 @@ +# Copyright 2020 Camptocamp SA (http://www.camptocamp.com) +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). +from odoo.addons.component.core import Component + + +class ShopfloorApp(Component): + """Generic endpoints for the Application.""" + + _inherit = "base.shopfloor.service" + _name = "shopfloor.app" + _usage = "app" + _description = __doc__ + + # TODO: maybe rename to `config` or `app_config` + # as this is not related to current user conf + def user_config(self): + profiles_comp = self.component("profile") + profiles = profiles_comp._to_json(profiles_comp._search()) + user_comp = self.component("user") + user_info = user_comp._user_info() + return self._response(data={"profiles": profiles, "user_info": user_info}) + + +class ShopfloorAppValidator(Component): + """Validators for the Application endpoints""" + + _inherit = "base.shopfloor.validator" + _name = "shopfloor.app.validator" + _usage = "app.validator" + + def user_config(self): + return {} + + +class ShopfloorAppValidatorResponse(Component): + """Validators for the Application endpoints responses""" + + _inherit = "base.shopfloor.validator.response" + _name = "shopfloor.app.validator.response" + _usage = "app.validator.response" + + def user_config(self): + profile_return_validator = self.component("profile.validator.response") + user_return_validator = self.component("user.validator.response") + return self._response_schema( + { + "profiles": { + "type": "list", + "required": True, + "schema": { + "type": "dict", + "schema": profile_return_validator._record_schema, + }, + }, + "user_info": { + "type": "dict", + "required": True, + "schema": user_return_validator._user_info_schema(), + }, + } + ) diff --git a/shopfloor_base/services/forms/__init__.py b/shopfloor_base/services/forms/__init__.py new file mode 100644 index 0000000000..19f4552043 --- /dev/null +++ b/shopfloor_base/services/forms/__init__.py @@ -0,0 +1 @@ +from . import form_mixin diff --git a/shopfloor_base/services/forms/form_mixin.py b/shopfloor_base/services/forms/form_mixin.py new file mode 100644 index 0000000000..1389082c80 --- /dev/null +++ b/shopfloor_base/services/forms/form_mixin.py @@ -0,0 +1,85 @@ +# Copyright 2020 Camptocamp SA (http://www.camptocamp.com) +# @author Simone Orsi +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). +from odoo import _ + +from odoo.addons.component.core import AbstractComponent + + +class ShopfloorFormMixin(AbstractComponent): + """Allow to edit records. + """ + + _inherit = "base.shopfloor.service" + _name = "shopfloor.form.mixin" + _usage = "form_mixin" + _description = __doc__ + _expose_model = "" + _requires_header_profile = True + _requires_header_menu = False + + def get(self, _id): + record = self._get(_id) + return self._response_for_form(record) + + def update(self, _id, **params): + record = self._get(_id) + record.write(self._prepare_params(params, mode="update")) + return self._response_for_form(record, message=self._msg_record_updated(record)) + + def _response_for_form(self, record, **kw): + record_data = self._record_data(record) + form_data = self._form_data(record) + return self._response(data={"record": record_data, "form": form_data}, **kw) + + def _record_data(self, record): + raise NotImplementedError() + + def _form_data(self, record): + raise NotImplementedError() + + def _prepare_params(self, params, mode="update"): + return params + + def _msg_record_updated(self, record): + model = self.env["ir.model"]._get(record._name) + body = _("%s updated.") % model.name + return {"message_type": "info", "body": body} + + +class ShopfloorFormMixinValidator(AbstractComponent): + """Validators for the ShopfloorFormMixin endpoints""" + + _inherit = "base.shopfloor.validator" + _name = "shopfloor.form.validator.mixin" + _usage = "form_mixin.validator" + + def get(self): + raise NotImplementedError() + + def update(self): + raise NotImplementedError() + + +class ShopfloorFormMixinValidatorResponse(AbstractComponent): + """Validators for the ShopfloorFormMixin endpoints responses""" + + _inherit = "base.shopfloor.validator.response" + _name = "shopfloor.form.validator.response.mixin" + _usage = "form_mixin.validator.response" + + def get(self): + schema = { + "record": self.schemas._schema_dict_of(self._record_schema()), + "form": self.schemas._schema_dict_of(self._form_schema()), + } + return self._response_schema(schema) + + def update(self): + return self.get() + + def _record_schema(self): + raise NotImplementedError() + + def _form_schema(self): + raise NotImplementedError() diff --git a/shopfloor_base/services/menu.py b/shopfloor_base/services/menu.py new file mode 100644 index 0000000000..9c9e169a96 --- /dev/null +++ b/shopfloor_base/services/menu.py @@ -0,0 +1,118 @@ +# Copyright 2020 Camptocamp SA (http://www.camptocamp.com) +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). +from odoo.osv import expression + +from odoo.addons.base_rest.components.service import to_int +from odoo.addons.component.core import Component + + +class ShopfloorMenu(Component): + """ + Menu Structure for the client application. + + The list of menus is restricted by the profiles. + A menu without profile is shown in every profiles. + """ + + _inherit = "base.shopfloor.service" + _name = "shopfloor.menu" + _usage = "menu" + _expose_model = "shopfloor.menu" + _description = __doc__ + + def _get_base_search_domain(self): + base_domain = super()._get_base_search_domain() + return expression.AND( + [ + base_domain, + [ + "|", + ("profile_id", "=", False), + ("profile_id", "=", self.work.profile.id), + ], + ] + ) + + def _search(self, name_fragment=None): + if not self.work.profile: + # we need to know the profile to load menus + return self.env["shopfloor.menu"].browse() + domain = self._get_base_search_domain() + if name_fragment: + domain.append(("name", "ilike", name_fragment)) + records = self.env[self._expose_model].search(domain) + return records + + def search(self, name_fragment=None): + """List available menu entries for current profile""" + records = self._search(name_fragment=name_fragment) + return self._response( + data={"size": len(records), "records": self._to_json(records)} + ) + + def _convert_one_record(self, record): + values = record.jsonify(self._one_record_parser, one=True) + counters = self._get_move_line_counters(record) + values.update(counters) + return values + + def _get_move_line_counters(self, record): + """Lookup for all lines per menu item and compute counters. + """ + # TODO: maybe to be improved w/ raw SQL as this run for each menu item + # and it's called every time the menu is opened/gets refreshed + move_line_search = self.actions_for( + "search_move_line", picking_types=record.picking_type_ids + ) + locations = record.picking_type_ids.mapped("default_location_src_id") + lines_per_menu = move_line_search.search_move_lines_by_location(locations) + return move_line_search.counters_for_lines(lines_per_menu) + + @property + def _one_record_parser(self): + return [ + "id", + "name", + "scenario", + ("picking_type_ids:picking_types", ["id", "name"]), + ] + + +class ShopfloorMenuValidator(Component): + """Validators for the Menu endpoints""" + + _inherit = "base.shopfloor.validator" + _name = "shopfloor.menu.validator" + _usage = "menu.validator" + + def search(self): + return { + "name_fragment": {"type": "string", "nullable": True, "required": False} + } + + +class ShopfloorMenuValidatorResponse(Component): + """Validators for the Menu endpoints responses""" + + _inherit = "base.shopfloor.validator.response" + _name = "shopfloor.menu.validator.response" + _usage = "menu.validator.response" + + def return_search(self): + record_schema = self._record_schema + return self._response_schema( + { + "size": {"coerce": to_int, "required": True, "type": "integer"}, + "records": self.schemas._schema_list_of(record_schema), + } + ) + + @property + def _record_schema(self): + schema = { + "id": {"coerce": to_int, "required": True, "type": "integer"}, + "name": {"type": "string", "nullable": False, "required": True}, + "scenario": {"type": "string", "nullable": False, "required": True}, + } + schema.update(self.schemas.menu_item_counters()) + return schema diff --git a/shopfloor_base/services/profile.py b/shopfloor_base/services/profile.py new file mode 100644 index 0000000000..6724ecaca1 --- /dev/null +++ b/shopfloor_base/services/profile.py @@ -0,0 +1,80 @@ +# Copyright 2020 Camptocamp SA (http://www.camptocamp.com) +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). +from odoo.addons.base_rest.components.service import to_int +from odoo.addons.component.core import Component + + +class ShopfloorProfile(Component): + """ + Profile storing the configuration for the interaction from the client. + + A client application must use a profile, passed to every request in the + HTTP header HTTP_SERVICE_CTX_PROFILE_ID. + + Only stock managers should be allowed to change the profile for a device. + """ + + _inherit = "base.shopfloor.service" + _name = "shopfloor.profile" + _usage = "profile" + _expose_model = "shopfloor.profile" + _description = __doc__ + + def _search(self, name_fragment=None): + domain = self._get_base_search_domain() + if name_fragment: + domain.append(("name", "ilike", name_fragment)) + records = self.env[self._expose_model].search(domain) + return records + + def search(self, name_fragment=None): + """List available profiles""" + records = self._search(name_fragment=name_fragment) + return self._response( + data={"size": len(records), "records": self._to_json(records)} + ) + + def _convert_one_record(self, record): + return { + "id": record.id, + "name": record.name, + } + + +class ShopfloorProfileValidator(Component): + """Validators for the Profile endpoints""" + + _inherit = "base.shopfloor.validator" + _name = "shopfloor.profile.validator" + _usage = "profile.validator" + + def search(self): + return { + "name_fragment": {"type": "string", "nullable": True, "required": False} + } + + +class ShopfloorProfileValidatorResponse(Component): + """Validators for the Profile endpoints responses""" + + _inherit = "base.shopfloor.validator.response" + _name = "shopfloor.profile.validator.response" + _usage = "profile.validator.response" + + def search(self): + return self._response_schema( + { + "size": {"coerce": to_int, "required": True, "type": "integer"}, + "records": { + "type": "list", + "schema": {"type": "dict", "schema": self._record_schema}, + }, + } + ) + + @property + def _record_schema(self): + return { + "id": {"coerce": to_int, "required": True, "type": "integer"}, + "name": {"type": "string", "nullable": False, "required": True}, + } diff --git a/shopfloor_base/services/scan_anything.py b/shopfloor_base/services/scan_anything.py new file mode 100644 index 0000000000..a943fc5c65 --- /dev/null +++ b/shopfloor_base/services/scan_anything.py @@ -0,0 +1,151 @@ +# Copyright 2020 Camptocamp SA (http://www.camptocamp.com) +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). +from odoo import _ + +from odoo.addons.component.core import Component + + +class ShopfloorScanAnything(Component): + """Endpoints to scan any record. + + Supported types of records (models): + + * location (stock.location) + * package (stock.quant.package) + * product (product.product) + * lot (stock.production.lot) + * transfer (stock.picking) + + NOTE for swagger docs: using `anyof_schema` for `record` response key + does not work in swagger UI. Hence, you won't see any detail. + + Issue: https://github.com/swagger-api/swagger-ui/issues/3803 + PR: https://github.com/swagger-api/swagger-ui/pull/5530 + """ + + _inherit = "base.shopfloor.service" + _name = "shopfloor.scan.anything" + _usage = "scan_anything" + _description = __doc__ + + def scan(self, identifier): + # TODO: shall we add constrains by profile etc? + data = {} + tried = [] + record = None + for rec_type, finder, converter, __ in self._scan_handlers(): + tried.append(rec_type) + record = finder(identifier) + if record: + data.update( + { + "identifier": identifier, + "record": converter(record), + "type": rec_type, + } + ) + break + if not record: + return self._response_for_not_found(tried) + return self._response_for_found(data) + + def _response_for_found(self, data): + return self._response(data=data) + + def _response_for_not_found(self, tried): + message = { + "body": _( + "Record not found.\n" "We've tried with the following types: {}" + ).format(", ".join(tried)), + "message_type": "error", + } + return self._response(message=message) + + def _scan_handlers(self): + """Return a tuple of tuples describing handlers for scan requests. + + Tuple schema: + + 0. record type + 1. finder + 2. json detail converter + 3. detail schema validator + + """ + search = self.actions_for("search") + schema = self.component(usage="schema_detail") + return ( + ( + "location", + search.location_from_scan, + self.data_detail.location_detail, + schema.location_detail, + ), + ( + "package", + search.package_from_scan, + self.data_detail.package_detail, + schema.package_detail, + ), + ( + "product", + search.product_from_scan, + self.data_detail.product_detail, + schema.product_detail, + ), + ( + "lot", + search.lot_from_scan, + self.data_detail.lot_detail, + schema.lot_detail, + ), + ( + "transfer", + search.picking_from_scan, + self.data_detail.picking_detail, + schema.picking_detail, + ), + ) + + +class ShopfloorScanAnythingValidator(Component): + """Validators for the Application endpoints""" + + _inherit = "base.shopfloor.validator" + _name = "shopfloor.scan_anything.validator" + _usage = "scan_anything.validator" + + def scan(self): + return { + "identifier": {"type": "string", "nullable": False, "required": True}, + } + + +class ShopfloorScanAnythingValidatorResponse(Component): + """Validators for the scan anything endpoints responses""" + + _inherit = "base.shopfloor.validator.response" + _name = "shopfloor.scan_anything.validator.response" + _usage = "scan_anything.validator.response" + + def scan(self): + scan_service = self.component(usage="scan_anything") + allowed_types = [x[0] for x in scan_service._scan_handlers()] + allowed_schemas = [x[-1]() for x in scan_service._scan_handlers()] + data_schema = { + "identifier": {"type": "string", "nullable": True, "required": False}, + "type": { + "type": "string", + "nullable": True, + "required": False, + "allowed": allowed_types, + }, + "record": { + "type": "dict", + "required": False, + "nullable": True, + "anyof_schema": allowed_schemas, + "dependencies": ["identifier", "type"], + }, + } + return self._response_schema(data_schema) diff --git a/shopfloor_base/services/schema.py b/shopfloor_base/services/schema.py new file mode 100644 index 0000000000..4509609c8d --- /dev/null +++ b/shopfloor_base/services/schema.py @@ -0,0 +1,58 @@ +# Copyright 2020 Camptocamp SA (http://www.camptocamp.com) +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). +from odoo.addons.component.core import Component + + +class BaseShopfloorSchemaResponse(Component): + """Provide methods to share schema structures + + The methods should be used in Service Components, so we try to + have similar schema structures across scenarios. + """ + + _inherit = "base.rest.service" + _name = "base.shopfloor.schemas" + _collection = "shopfloor.service" + _usage = "schema" + _is_rest_service_component = False + + def _schema_list_of(self, schema, **kw): + schema = { + "type": "list", + "nullable": True, + "required": True, + "schema": {"type": "dict", "schema": schema}, + } + schema.update(kw) + return schema + + def _simple_record(self, **kw): + schema = { + "id": {"required": True, "type": "integer"}, + "name": {"type": "string", "nullable": False, "required": True}, + } + schema.update(kw) + return schema + + def _schema_dict_of(self, schema, **kw): + schema = { + "type": "dict", + "nullable": True, + "required": True, + "schema": schema, + } + schema.update(kw) + return schema + + def _schema_search_results_of(self, schema, **kw): + return { + "size": {"required": True, "type": "integer"}, + "records": { + "type": "list", + "required": True, + "schema": {"type": "dict", "schema": schema}, + }, + } + + def menu_item_counters(self, **kw): + return {} diff --git a/shopfloor_base/services/service.py b/shopfloor_base/services/service.py new file mode 100644 index 0000000000..bb61ba2c64 --- /dev/null +++ b/shopfloor_base/services/service.py @@ -0,0 +1,283 @@ +# Copyright 2020 Camptocamp SA (http://www.camptocamp.com) +# Copyright 2020 Akretion (http://www.akretion.com) +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). + +from werkzeug.exceptions import BadRequest + +from odoo.http import request +from odoo.tools import DotDict + +from odoo.addons.base_rest.controllers.main import _PseudoCollection +from odoo.addons.component.core import AbstractComponent, WorkContext + + +class BaseShopfloorService(AbstractComponent): + """Base class for REST services""" + + _inherit = "base.rest.service" + _name = "base.shopfloor.service" + _collection = "shopfloor.service" + _actions_collection_name = "shopfloor.action" + _expose_model = None + + def dispatch(self, method_name, *args, params=None): + self._validate_headers_update_work_context(request, method_name) + return super().dispatch(method_name, *args, params=params) + + def _get_base_search_domain(self): + return [] + + def _convert_one_record(self, record): + """To implement in service Components""" + return {} + + def _to_json(self, records): + res = [] + for record in records: + res.append(self._convert_one_record(record)) + return res + + def _response( + self, base_response=None, data=None, next_state=None, message=None, popup=None + ): + """Base "envelope" for the responses + + All the keys are optional. + + :param base_response: optional dictionary of values to extend + (typically already created by a call to _response()) + :param data: dictionary of values, when a next_state is provided, + the data is enclosed in a key of the same name (to support polymorphism + in the schema) + :param next_state: string describing the next state that the client + application must reach + :param message: dictionary for the message to show in the client + application (see ``_response_schema`` for the keys) + :param popup: dictionary for a popup to show in the client application + (see ``_response_schema`` for the keys). The popup is displayed before + reaching the next state. + """ + if base_response: + response = base_response.copy() + else: + response = {} + if next_state: + # data for a state is always enclosed in a key with the name + # of the state, so an endpoint can return to different states + # that need different data: the schema can be different for + # every state this way + response.update( + { + # ensure we have an empty dict when the state + # does not need any data, so the client does not need + # to check this + "data": {next_state: data or {}}, + "next_state": next_state, + } + ) + + elif data: + response["data"] = data + + if message: + response["message"] = message + + if popup: + response["popup"] = popup + + return response + + _requires_header_menu = False + _requires_header_profile = False + + def _get_openapi_default_parameters(self): + defaults = super()._get_openapi_default_parameters() + # Normal users can't read an API key, ignore it using sudo() only + # because it's a demo key. + demo_api_key = self.env.ref("shopfloor.api_key_demo", raise_if_not_found=False) + if demo_api_key: + demo_api_key = demo_api_key.sudo() + + service_params = [ + { + "name": "API-KEY", + "in": "header", + "description": "API key for Authorization", + "required": True, + "schema": {"type": "string"}, + "style": "simple", + "value": demo_api_key.key if demo_api_key else "", + }, + ] + if self._requires_header_menu: + # Try to first the first menu that implements the current service. + # Not all usages have a process, in that case, we'll set the first + # menu found + menu = self.env["shopfloor.menu"].search( + [("scenario", "=", self._usage)], limit=1 + ) + if not menu: + menu = self.env["shopfloor.menu"].search([], limit=1) + service_params.append( + { + "name": "SERVICE_CTX_MENU_ID", + "in": "header", + "description": "ID of the current menu", + "required": True, + "schema": {"type": "integer"}, + "style": "simple", + "value": menu.id, + } + ) + if self._requires_header_profile: + profile = self.env["shopfloor.profile"].search([], limit=1) + service_params.append( + { + "name": "SERVICE_CTX_PROFILE_ID", + "in": "header", + "description": "ID of the current profile", + "required": True, + "schema": {"type": "integer"}, + "style": "simple", + "value": profile.id, + } + ), + defaults.extend(service_params) + return defaults + + @property + def actions_collection(self): + return _PseudoCollection(self._actions_collection_name, self.env) + + def actions_for(self, usage, propagate_kwargs=None, **kw): + """Return an Action Component for a usage + + Action Components are the components supporting the business logic of + the processes, so we can limit the code in Services to the minimum and + share methods. + """ + propagate_kwargs = self.work._propagate_kwargs[:] + (propagate_kwargs or []) + # propagate custom arguments (such as menu ID/profile ID) + kwargs = { + attr_name: getattr(self.work, attr_name) + for attr_name in propagate_kwargs + if attr_name not in ("collection", "components_registry") + and hasattr(self.work, attr_name) + } + kwargs.update(kw) + work = WorkContext(collection=self.actions_collection, **kwargs) + return work.component(usage=usage) + + # FIXME: this is not used anymore by base_rest -> + # all public/private methods are exposed + def _is_public_api_method(self, method_name): + # do not "hide" the "actions_for" method as internal since, we'll use + # it in components, so exclude it from the rest API + if method_name == "actions_for": + return False + return super()._is_public_api_method(method_name) + + @property + def data(self): + return self.actions_for("data") + + @property + def data_detail(self): + return self.actions_for("data_detail") + + @property + def msg_store(self): + return self.actions_for("message") + + # TODO: maybe to be proposed to base_rest + # TODO: add tests + def _validate_headers_update_work_context(self, request, method_name): + """Validate request and update context per service. + + Our services may require extra headers. + The service component is loaded after the ctx has been initialized + hence we need an hook were we can validate by component/service + if the request is compliant with what we need (eg: missing header) + """ + if self.env.context.get("_service_skip_request_validation"): + return + extra_work_ctx = {} + headers = request.httprequest.environ + for rule, active in self._validation_rules: + if callable(active): + active = active(request, method_name) + if not active: + continue + header_name, coerce_func, ctx_value_handler_name, mandatory = rule + try: + header_value = coerce_func(headers.get(header_name)) + except (TypeError, ValueError) as err: + if not mandatory: + continue + raise BadRequest( + "{} header validation error: {}".format(header_name, str(err)) + ) + ctx_value_handler = getattr(self, ctx_value_handler_name) + dest_key, value = ctx_value_handler(header_value) + if not value: + raise BadRequest("{} header value lookup error".format(header_name)) + extra_work_ctx[dest_key] = value + for k, v in extra_work_ctx.items(): + setattr(self.work, k, v) + + @property + def _validation_rules(self): + return ( + # rule to apply, active flag + (self.MENU_ID_HEADER_RULE, self._requires_header_menu), + (self.PROFILE_ID_HEADER_RULE, self._requires_header_profile), + ) + + MENU_ID_HEADER_RULE = ( + # header name, coerce func, ctx handler, mandatory + "HTTP_SERVICE_CTX_MENU_ID", + int, + "_work_ctx_get_menu_id", + True, + ) + PROFILE_ID_HEADER_RULE = ( + # header name, coerce func, ctx value handler, mandatory + "HTTP_SERVICE_CTX_PROFILE_ID", + int, + "_work_ctx_get_profile_id", + True, + ) + + def _work_ctx_get_menu_id(self, rec_id): + return "menu", self.env["shopfloor.menu"].browse(rec_id).exists() + + def _work_ctx_get_profile_id(self, rec_id): + return "profile", self.env["shopfloor.profile"].browse(rec_id).exists() + + _options = {} + + @property + def options(self): + """Compute options for current service. + + If the service has a menu, options coming from the menu are injected. + """ + if self._options: + return self._options + + options = {} + if self._requires_header_menu and getattr(self.work, "menu", None): + options = self.work.menu.options or {} + options.update(getattr(self.work, "options", {})) + self._options = DotDict(options) + return self._options + + +class BaseShopfloorProcess(AbstractComponent): + """Base class for process rest service""" + + _inherit = "base.shopfloor.service" + _name = "base.shopfloor.process" + + _requires_header_menu = True + _requires_header_profile = True diff --git a/shopfloor_base/services/user.py b/shopfloor_base/services/user.py new file mode 100644 index 0000000000..ceed40c9ea --- /dev/null +++ b/shopfloor_base/services/user.py @@ -0,0 +1,84 @@ +# Copyright 2020 Camptocamp SA (http://www.camptocamp.com) +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). +from odoo.addons.base_rest.components.service import to_int +from odoo.addons.component.core import Component + + +class ShopfloorUser(Component): + """Generic endpoints for user specific info.""" + + _inherit = "base.shopfloor.service" + _name = "shopfloor.user" + _usage = "user" + _description = __doc__ + _requires_header_profile = True + + def menu(self): + menu_comp = self.component("menu") + menus = menu_comp._to_json(menu_comp._search()) + return self._response(data={"menus": menus}) + + # TODO: this endpoint does not require profile header + def user_info(self): + return self._response(data={"user_info": self._user_info()}) + + def _user_info(self): + return self.env.user.jsonify(self._user_info_parser, one=True) + + @property + def _user_info_parser(self): + return ["id", "name"] + + +class ShopfloorUserValidator(Component): + """Validators for the User endpoints""" + + _inherit = "base.shopfloor.validator" + _name = "shopfloor.user.validator" + _usage = "user.validator" + + def menu(self): + return {} + + def user_info(self): + return {} + + +class ShopfloorUserValidatorResponse(Component): + """Validators for the User endpoints responses""" + + _inherit = "base.shopfloor.validator.response" + _name = "shopfloor.user.validator.response" + _usage = "user.validator.response" + + def menu(self): + menu_return_validator = self.component("menu.validator.response") + return self._response_schema( + { + "menus": { + "type": "list", + "required": True, + "schema": { + "type": "dict", + "schema": menu_return_validator._record_schema, + }, + }, + } + ) + + def user_info(self): + return self._response_schema( + { + "user_info": { + "type": "dict", + "required": True, + "schema": self._user_info_schema(), + } + } + ) + + def _user_info_schema(self): + return { + "id": {"coerce": to_int, "required": True, "type": "integer"}, + "name": {"type": "string", "nullable": False, "required": True}, + } diff --git a/shopfloor_base/services/validator.py b/shopfloor_base/services/validator.py new file mode 100644 index 0000000000..ce3a9eb8c6 --- /dev/null +++ b/shopfloor_base/services/validator.py @@ -0,0 +1,249 @@ +# Copyright 2020-2021 Camptocamp SA (http://www.camptocamp.com) +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). +import logging + +from odoo.addons.component.core import AbstractComponent, Component +from odoo.addons.component.exception import NoComponentError + +_logger = logging.getLogger(__name__) + + +class ShopfloorRestCerberusValidator(Component): + """Customize the handling of validators + + In the initial implementation of rest_api, the schema validators + had to be returned by methods in the same service as the method, named + after the endpoint's method with a prefix: "_validator_" or + "_validator_return_". + + As we have a lot of endpoints methods in some services, we extracted + the validator methods in dedicated components with + "base.shopfloor.validator" and "base.shopfloor.validator.response" usages, + and methods of the same name as the endpoint's method. + + With the new API, endpoints are decorated with "@restapi.method" and the + validator is defined there. Example: + + @restapi.method( + [(["//get", "/"], "GET")], + input_param=restapi.CerberusValidator("_get_partner_input_schema"), + output_param=restapi.CerberusValidator("_get_partner_output_schema"), + auth="public", + ) + + The schema is get by calling the method "_get..." on the service. + + For backward compatilibity, base_rest patches the methods not decorated + and sets the "input_param" and "output_param" to call the + "_validator_" or "_validator_return_": + + https://github.com/OCA/rest-framework/blob/abd74cd7241d3b93054825cc3e41cb7b693c9000/base_rest/models/rest_service_registration.py#L240-L250 # noqa + + The following change in base_rest allows to customize the way the validator + handler is get: https://github.com/OCA/rest-framework/pull/99 + + This is what is used here to delegate to our ".validator" and + ".validator.response" components. + """ + + _name = "shopfloor.rest.cerberus.validator" + _inherit = "base.rest.cerberus.validator" + _usage = "cerberus.validator" + _collection = "shopfloor.service" + _is_rest_service_component = False + + def _get_validator_component(self, service, method_name, direction): + assert direction in ("input", "output") + if direction == "input": + suffix = "validator" + method_name = method_name.replace("_validator_", "") + else: + suffix = "validator.response" + method_name = method_name.replace("_validator_return_", "") + validator_component = self.component( + usage="{}.{}".format(service._usage, suffix) + ) + return validator_component, method_name + + def get_validator_handler(self, service, method_name, direction): + """Get the validator handler for a method + + By default, it returns the method on the current service instance. It + can be customized to delegate the validators to another component. + """ + try: + validator_component, method_name = self._get_validator_component( + service, method_name, direction + ) + except NoComponentError: + _logger.warning("no component found for %s method %s", service, method_name) + return {} + + try: + return getattr(validator_component, method_name) + except AttributeError: + _logger.warning( + "no validator method found for %s method %s", service, method_name + ) + return {} + + def has_validator_handler(self, service, method_name, direction): + """Return if the service has a validator handler for a method + + By default, it returns True if the the method exists on the service. It + can be customized to delegate the validators to another component. + """ + try: + validator_component, method_name = self._get_validator_component( + service, method_name, direction + ) + except NoComponentError: + return False + return hasattr(validator_component, method_name) + + +class BaseShopfloorValidator(AbstractComponent): + """Base class for Validators""" + + _inherit = "base.rest.service" + _name = "base.shopfloor.validator" + _collection = "shopfloor.service" + _is_rest_service_component = False + + +class BaseShopfloorValidatorResponse(AbstractComponent): + """Base class for Validator for Responses + + When an endpoint returns data for a state, the data is enclosed + in a key with the same name as the state, this is in order to support + polymorphism in schemas (an endpoint being able to return different data + depending on the next state). + + General idea of a schema for a method that changes state (data may vary, + in this example, next_state will be one of "confirm_start", "start", + "scan_location"): + + { + message { + message_type* string + message* string + } + next_state string + data { + confirm_start {...} + start {...} + scan_location {...} + } + } + + General idea of a schema for a generic method (data may vary): + + { + message { + message_type* string + message* string + } + data { + size* integer + records* integer + } + } + + """ + + _inherit = "base.rest.service" + _name = "base.shopfloor.validator.response" + _collection = "shopfloor.service" + _is_rest_service_component = False + + # 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 + to the next state. + """ + return {} + + @property + def schemas(self): + return self.component(usage="schema") + + @property + def schemas_detail(self): + return self.component(usage="schema_detail") + + def _response_schema(self, data_schema=None, next_states=None): + """Schema for the return validator + + Must be used for the schema of all responses. + The "data" part can be customized and is optional, + it must be a dictionary. + + next_states is a list of allowed states to which the client + can transition. The schema of the data needed for every state + of the list must be defined in the ``_states`` method. + + The initial state does not need to be included in the list, it + is implicit as we assume that any state can go back to the initial + state in case of unrecoverable error. + """ + response_schema = { + "message": { + "type": "dict", + "required": False, + "schema": { + "message_type": { + "type": "string", + "required": True, + "allowed": ["info", "warning", "error", "success"], + }, + "body": {"type": "string", "required": True}, + }, + }, + "popup": { + "type": "dict", + "required": False, + "schema": {"body": {"type": "string", "required": True}}, + }, + "log_entry_url": {"type": "string", "required": False}, + } + if not data_schema: + data_schema = {} + + # TODO: shall we keep `next_state` as part of base module? + # In theory the next state is what leads users to the next step. + if next_states: + 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) + ) + + data_schema = data_schema.copy() + data_schema.update( + { + state: {"type": "dict", "schema": states_schemas[state]} + for state in next_states + } + ) + response_schema["next_state"] = {"type": "string", "required": False} + + response_schema["data"] = { + "type": "dict", + "required": False, + "schema": data_schema, + } + return response_schema diff --git a/shopfloor_base/tests/__init__.py b/shopfloor_base/tests/__init__.py new file mode 100644 index 0000000000..7aa9206af5 --- /dev/null +++ b/shopfloor_base/tests/__init__.py @@ -0,0 +1,7 @@ +from . import test_app +from . import test_user +from . import test_menu +from . import test_openapi +from . import test_profile +from . import test_actions_data +from . import test_scan_anything diff --git a/shopfloor_base/tests/common.py b/shopfloor_base/tests/common.py new file mode 100644 index 0000000000..f8fce485d7 --- /dev/null +++ b/shopfloor_base/tests/common.py @@ -0,0 +1,155 @@ +# Copyright 2020 Camptocamp SA (http://www.camptocamp.com) +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). +from contextlib import contextmanager +from pprint import pformat + +from odoo.tests.common import SavepointCase + +from odoo.addons.base_rest.controllers.main import _PseudoCollection +from odoo.addons.base_rest.tests.common import RegistryMixin +from odoo.addons.component.core import WorkContext +from odoo.addons.component.tests.common import ComponentMixin + + +class AnyObject: + def __repr__(self): + return "ANY" + + def __deepcopy__(self, memodict=None): + return self + + def __copy__(self): + return self + + def __eq__(self, other): + return True + + +class CommonCase(SavepointCase, RegistryMixin, ComponentMixin): + """Base class for writing Shopfloor tests + + All tests are run as normal stock user by default, to check that all the + services work without manager permissions. + + The consequences on writing tests: + + * Records created or written in a test setup must use sudo() + if the user has no permission on these models. + * Tests setUps should not extend setUpClass but setUpClassVars + and setUpClassBaseData, which already have an environment using + the stock user. + * Be wary of creating records before setUpClassUsers is called, because + it their "env.user" would be admin and could lead to inconsistencies + in tests. + + This class provides several helpers which are used throughout all the tests. + """ + + # by default disable tracking suite-wise, it's a time saver :) + tracking_disable = True + + ANY = AnyObject() # allow accepting anything in assert_response() + + maxDiff = None + + @contextmanager + def work_on_services(self, env=None, **params): + params = params or {} + collection = _PseudoCollection("shopfloor.service", env or self.env) + yield WorkContext( + model_name="rest.service.registration", collection=collection, **params + ) + + @contextmanager + def work_on_actions(self, **params): + params = params or {} + collection = _PseudoCollection("shopfloor.action", self.env) + yield WorkContext( + model_name="rest.service.registration", collection=collection, **params + ) + + # pylint: disable=method-required-super + # super is called "the old-style way" to call both super classes in the + # order we want + def setUp(self): + # Have to initialize both odoo env and stuff + + # the Component registry of the mixin + SavepointCase.setUp(self) + ComponentMixin.setUp(self) + + @classmethod + def setUpClass(cls): + super(CommonCase, cls).setUpClass() + cls.env = cls.env( + context=dict( + cls.env.context, + tracking_disable=cls.tracking_disable, + _service_skip_request_validation=True, + ) + ) + + cls.setUpComponent() + cls.setUpRegistry() + cls.setUpClassUsers() + cls.setUpClassVars() + cls.setUpClassBaseData() + + with cls.work_on_actions(cls) as work: + cls.data = work.component(usage="data") + with cls.work_on_actions(cls) as work: + cls.msg_store = work.component(usage="message") + with cls.work_on_services(cls) as work: + cls.schema = work.component(usage="schema") + + @classmethod + def setUpClassUsers(cls): + Users = cls.env["res.users"].with_context( + {"no_reset_password": True, "mail_create_nosubscribe": True} + ) + cls.shopfloor_user = Users.create(cls._shopfloor_user_values()) + cls.env = cls.env(user=cls.shopfloor_user) + + @classmethod + def _shopfloor_user_values(cls): + return { + "name": "Pauline Poivraisselle", + "login": "pauline2", + "email": "p.p@example.com", + "groups_id": [ + (6, 0, [cls.env.ref("shopfloor_base.group_shopfloor_user").id]) + ], + } + + @classmethod + def setUpClassVars(cls): + pass + + @classmethod + def setUpClassBaseData(cls): + pass + + def assert_response( + self, response, next_state=None, message=None, data=None, popup=None + ): + """Assert a response from the webservice + + The data and message dictionaries can use ``self.ANY`` to accept any + value. + """ + expected = {} + if message: + expected["message"] = message + if popup: + expected["popup"] = popup + if next_state: + expected.update( + {"next_state": next_state, "data": {next_state: data or {}}} + ) + elif data: + expected["data"] = data + self.assertDictEqual( + response, + expected, + "\n\nActual:\n%s" + "\n\nExpected:\n%s" % (pformat(response), pformat(expected)), + ) diff --git a/shopfloor_base/tests/common_misc.py b/shopfloor_base/tests/common_misc.py new file mode 100644 index 0000000000..2e3eebd436 --- /dev/null +++ b/shopfloor_base/tests/common_misc.py @@ -0,0 +1,53 @@ +# Copyright 2020 Camptocamp SA (http://www.camptocamp.com) +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). +import logging + +from .common import CommonCase + +_logger = logging.getLogger(__name__) + + +try: + from cerberus import Validator +except ImportError: + _logger.debug("Can not import cerberus") + + +class ActionsDataCaseBase(CommonCase): + @classmethod + def setUpClassBaseData(cls): + super().setUpClassBaseData() + cls.partner = cls.env.ref("base.res_partner_12") + + def assert_schema(self, schema, data): + validator = Validator(schema) + self.assertTrue(validator.validate(data), validator.errors) + + +class CommonMenuCase(CommonCase): + @classmethod + def setUpClassVars(cls, *args, **kwargs): + super().setUpClassVars(*args, **kwargs) + cls.profile = cls.env.ref("shopfloor_base.profile_demo_2") + + def setUp(self): + super().setUp() + with self.work_on_services(profile=self.profile) as work: + self.service = work.component(usage="menu") + + def _assert_menu_response(self, response, menus, **kw): + self.assert_response( + response, + data={ + "size": len(menus), + "records": [self._data_for_menu_item(menu, **kw) for menu in menus], + }, + ) + + def _data_for_menu_item(self, menu, **kw): + data = { + "id": menu.id, + "name": menu.name, + "scenario": menu.scenario, + } + return data diff --git a/shopfloor_base/tests/test_actions_data.py b/shopfloor_base/tests/test_actions_data.py new file mode 100644 index 0000000000..42b3357333 --- /dev/null +++ b/shopfloor_base/tests/test_actions_data.py @@ -0,0 +1,31 @@ +# Copyright 2020 Camptocamp SA (http://www.camptocamp.com) +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). +import logging + +from .common import CommonCase + +_logger = logging.getLogger(__name__) + + +try: + from cerberus import Validator +except ImportError: + _logger.debug("Can not import cerberus") + + +class ActionsDataCaseBase(CommonCase): + @classmethod + def setUpClassBaseData(cls): + super().setUpClassBaseData() + cls.partner = cls.env.ref("base.res_partner_12") + + def assert_schema(self, schema, data): + validator = Validator(schema) + self.assertTrue(validator.validate(data), validator.errors) + + +class ActionsDataCase(ActionsDataCaseBase): + def test_data_partner(self): + data = self.data.partner(self.partner) + self.assert_schema(self.schema._simple_record(), data) + self.assertDictEqual(data, {"id": self.partner.id, "name": self.partner.name}) diff --git a/shopfloor_base/tests/test_app.py b/shopfloor_base/tests/test_app.py new file mode 100644 index 0000000000..ef66d91a20 --- /dev/null +++ b/shopfloor_base/tests/test_app.py @@ -0,0 +1,31 @@ +# Copyright 2020 Camptocamp SA (http://www.camptocamp.com) +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). +from .common import CommonCase + + +class AppCase(CommonCase): + @classmethod + def setUpClassVars(cls): + super().setUpClassVars() + cls.profile = cls.env.ref("shopfloor_base.profile_demo_1") + cls.profile2 = cls.env.ref("shopfloor_base.profile_demo_2") + + def setUp(self): + super().setUp() + with self.work_on_services(profile=self.profile) as work: + self.service = work.component(usage="app") + + def test_user_config(self): + """Request /app/user_config""" + # Simulate the client asking the configuration + response = self.service.dispatch("user_config") + profiles = self.env["shopfloor.profile"].search([]) + self.assert_response( + response, + data={ + "profiles": [ + {"id": profile.id, "name": profile.name} for profile in profiles + ], + "user_info": {"id": self.env.user.id, "name": self.env.user.name}, + }, + ) diff --git a/shopfloor_base/tests/test_menu.py b/shopfloor_base/tests/test_menu.py new file mode 100644 index 0000000000..d8f0e183c2 --- /dev/null +++ b/shopfloor_base/tests/test_menu.py @@ -0,0 +1,27 @@ +# Copyright 2020 Camptocamp SA (http://www.camptocamp.com) +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). + +from .common_misc import CommonMenuCase + + +class MenuCase(CommonMenuCase): + def test_menu_search(self): + """Request /menu/search""" + # Simulate the client searching menus + response = self.service.dispatch("search") + menus = self.env["shopfloor.menu"].search([]) + self._assert_menu_response(response, menus) + + def test_menu_search_restricted(self): + """Request /menu/search with profile attributions""" + # Simulate the client searching menus + menus = self.env["shopfloor.menu"].sudo().search([]) + menus_without_profile = menus[0:2] + # these menus should now be hidden for the current profile + other_profile = self.env.ref("shopfloor_base.profile_demo_1") + menus_without_profile.profile_id = other_profile + + response = self.service.dispatch("search") + + my_menus = menus - menus_without_profile + self._assert_menu_response(response, my_menus) diff --git a/shopfloor_base/tests/test_openapi.py b/shopfloor_base/tests/test_openapi.py new file mode 100644 index 0000000000..b2c6f76c74 --- /dev/null +++ b/shopfloor_base/tests/test_openapi.py @@ -0,0 +1,24 @@ +# Copyright 2020 Camptocamp SA (http://www.camptocamp.com) +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). + +from .common import CommonCase + + +class TestOpenAPICommonCase(CommonCase): + @classmethod + def setUpClassVars(cls, *args, **kwargs): + super().setUpClassVars(*args, **kwargs) + + # we don't really care about which menu and profile we use + # to read the OpenAPI specs + cls.menu = cls.env.ref("shopfloor.shopfloor_menu_delivery") + cls.profile = cls.env.ref("shopfloor_base.profile_demo_2") + + def test_openapi(self): + with self.work_on_services(menu=self.menu, profile=self.profile) as work: + services = work.many_components() + for service in services: + if not service._is_rest_service_component: + continue + # will raise if it fails to generate the openapi specs + service.to_openapi() diff --git a/shopfloor_base/tests/test_profile.py b/shopfloor_base/tests/test_profile.py new file mode 100644 index 0000000000..a7fc094ad6 --- /dev/null +++ b/shopfloor_base/tests/test_profile.py @@ -0,0 +1,26 @@ +# Copyright 2020 Camptocamp SA (http://www.camptocamp.com) +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). + +from .common import CommonCase + + +class ProfileCase(CommonCase): + def setUp(self): + super().setUp() + with self.work_on_services() as work: + self.service = work.component(usage="profile") + + def test_profile_search(self): + """Request /profile/search""" + # Simulate the client searching profiles + response = self.service.dispatch("search") + self.assert_response( + response, + data={ + "size": 2, + "records": [ + {"id": self.ANY, "name": "Highbay Truck"}, + {"id": self.ANY, "name": "Shelf 1"}, + ], + }, + ) diff --git a/shopfloor_base/tests/test_scan_anything.py b/shopfloor_base/tests/test_scan_anything.py new file mode 100644 index 0000000000..300a626fb0 --- /dev/null +++ b/shopfloor_base/tests/test_scan_anything.py @@ -0,0 +1,37 @@ +# # Copyright 2020 Camptocamp SA (http://www.camptocamp.com) +# # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). + +# from .common import CommonCase + + +# class ScanAnythingCase(CommonCase): +# def setUp(self): +# super().setUp() +# with self.work_on_services() as work: +# self.service = work.component(usage="scan_anything") + +# def _test_response_ok(self, rec_type, data, identifier): +# params = {"identifier": identifier} +# response = self.service.dispatch("scan", params=params) +# self.assert_response( +# response, data={"type": rec_type, +# "identifier": identifier, "record": data}, +# ) + +# def _test_response_ko(self, identifier, tried=None): +# tried = tried or [x[0] for x in self.service._scan_handlers()] +# params = {"identifier": identifier} +# response = self.service.dispatch("scan", params=params) +# message = response["message"] +# self.assertEqual(message["message_type"], "error") +# self.assertIn("Record not found", message["body"]) +# for rec_type in tried: +# self.assertIn(rec_type, message["body"]) + +# def test_scan_product(self): +# record = self.product_b +# record.barcode = "PROD-B" +# rec_type = "product" +# identifier = record.barcode +# data = self.data_detail.product_detail(record) +# self._test_response_ok(rec_type, data, identifier) diff --git a/shopfloor_base/tests/test_user.py b/shopfloor_base/tests/test_user.py new file mode 100644 index 0000000000..230d4e1f22 --- /dev/null +++ b/shopfloor_base/tests/test_user.py @@ -0,0 +1,47 @@ +# Copyright 2020 Camptocamp SA (http://www.camptocamp.com) +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). +from .common_misc import CommonMenuCase + + +class UserCase(CommonMenuCase): + @classmethod + def setUpClassVars(cls, *args, **kwargs): + super().setUpClassVars(*args, **kwargs) + cls.profile = cls.env.ref("shopfloor_base.profile_demo_1") + cls.profile2 = cls.env.ref("shopfloor_base.profile_demo_2") + + def setUp(self): + super().setUp() + with self.work_on_services(profile=self.profile) as work: + self.service = work.component(usage="user") + + def test_menu_no_profile(self): + """Request /user/menu""" + # Simulate the client asking the menu + response = self.service.dispatch("menu") + menus = self.env["shopfloor.menu"].search([]) + self.assert_response( + response, + data={"menus": [self._data_for_menu_item(menu) for menu in menus]}, + ) + + def test_menu_by_profile(self): + """Request /user/menu w/ a specific profile""" + # Simulate the client asking the menu + menus = self.env["shopfloor.menu"].sudo().search([]) + menu = menus[0] + menu.profile_id = self.profile + (menus - menu).profile_id = self.profile2 + + response = self.service.dispatch("menu") + self.assert_response( + response, data={"menus": [self._data_for_menu_item(menu)]}, + ) + + def test_user_info(self): + """Request /user/user_info""" + response = self.service.dispatch("user_info") + self.assert_response( + response, + data={"user_info": {"id": self.env.user.id, "name": self.env.user.name}}, + ) diff --git a/shopfloor_base/utils.py b/shopfloor_base/utils.py new file mode 100644 index 0000000000..3ed7bf77fb --- /dev/null +++ b/shopfloor_base/utils.py @@ -0,0 +1,23 @@ +# Copyright 2021 Camptocamp SA (http://www.camptocamp.com) +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). +# @author Simone Orsi +from functools import wraps + + +def ensure_model(model_name): + """Decorator to ensure data method is called w/ the right recordset.""" + + def _ensure_model(func): + @wraps(func) + def wrapped(*args, **kwargs): + # 1st arg is `self` + record = args[1] + if record is not None: + assert ( + record._name == model_name + ), f"Expected model: {model_name}. Got: {record._name}" + return func(*args, **kwargs) + + return wrapped + + return _ensure_model diff --git a/shopfloor_base/views/menus.xml b/shopfloor_base/views/menus.xml new file mode 100644 index 0000000000..14d3a28386 --- /dev/null +++ b/shopfloor_base/views/menus.xml @@ -0,0 +1,29 @@ + + + + + + + + diff --git a/shopfloor_base/views/shopfloor_menu.xml b/shopfloor_base/views/shopfloor_menu.xml new file mode 100644 index 0000000000..f8feb4b6ea --- /dev/null +++ b/shopfloor_base/views/shopfloor_menu.xml @@ -0,0 +1,79 @@ + + + + shopfloor menu tree + shopfloor.menu + + + + + + + + + + + shopfloor menu form + shopfloor.menu + +
+ + + +
+
+
+ + shopfloor menu search + shopfloor.menu + + + + + + + + + + + + + + + + Menus + shopfloor.menu + ir.actions.act_window + tree,form + +
diff --git a/shopfloor_base/views/shopfloor_profile_views.xml b/shopfloor_base/views/shopfloor_profile_views.xml new file mode 100644 index 0000000000..1e2d9beee9 --- /dev/null +++ b/shopfloor_base/views/shopfloor_profile_views.xml @@ -0,0 +1,57 @@ + + + + shopfloor.profile tree + shopfloor.profile + + + + + + + + shopfloor.profile form + shopfloor.profile + +
+ + + + + + + + + + + +
+
+
+ + shopfloor.profile search + shopfloor.profile + + + + + + + + + + Profiles + shopfloor.profile + ir.actions.act_window + tree,form + +
From b80b7f0c3f9f918cfaedd8de2645eeb580cacfdc Mon Sep 17 00:00:00 2001 From: Simone Orsi Date: Tue, 2 Feb 2021 10:30:55 +0100 Subject: [PATCH 002/111] shopfloor_base: add shopfloor.scenario --- shopfloor_base/__manifest__.py | 3 + shopfloor_base/demo/shopfloor_menu_demo.xml | 1 + .../demo/shopfloor_scenario_demo.xml | 6 ++ shopfloor_base/models/__init__.py | 1 + shopfloor_base/models/shopfloor_menu.py | 10 ++- shopfloor_base/models/shopfloor_scenario.py | 70 +++++++++++++++++++ shopfloor_base/security/ir.model.access.csv | 2 + shopfloor_base/services/service.py | 2 +- shopfloor_base/tests/__init__.py | 1 + shopfloor_base/tests/common.py | 12 ++++ shopfloor_base/tests/test_shopfloor_models.py | 42 +++++++++++ shopfloor_base/views/menus.xml | 6 ++ shopfloor_base/views/shopfloor_menu.xml | 7 +- .../views/shopfloor_scenario_views.xml | 52 ++++++++++++++ 14 files changed, 210 insertions(+), 5 deletions(-) create mode 100644 shopfloor_base/demo/shopfloor_scenario_demo.xml create mode 100644 shopfloor_base/models/shopfloor_scenario.py create mode 100644 shopfloor_base/tests/test_shopfloor_models.py create mode 100644 shopfloor_base/views/shopfloor_scenario_views.xml diff --git a/shopfloor_base/__manifest__.py b/shopfloor_base/__manifest__.py index 939d9be33a..d1d27a8b7f 100644 --- a/shopfloor_base/__manifest__.py +++ b/shopfloor_base/__manifest__.py @@ -26,12 +26,15 @@ "security/groups.xml", "security/ir.model.access.csv", "views/shopfloor_menu.xml", + "views/shopfloor_scenario_views.xml", "views/shopfloor_profile_views.xml", "views/menus.xml", ], "demo": [ "demo/auth_api_key_demo.xml", + "demo/shopfloor_scenario_demo.xml", "demo/shopfloor_menu_demo.xml", "demo/shopfloor_profile_demo.xml", ], + "external_dependencies": {"python": ["pyyaml"]}, } diff --git a/shopfloor_base/demo/shopfloor_menu_demo.xml b/shopfloor_base/demo/shopfloor_menu_demo.xml index 0adfe6e7c1..2e26e1d3f2 100644 --- a/shopfloor_base/demo/shopfloor_menu_demo.xml +++ b/shopfloor_base/demo/shopfloor_menu_demo.xml @@ -2,5 +2,6 @@ Simple Thing To Do 20 + diff --git a/shopfloor_base/demo/shopfloor_scenario_demo.xml b/shopfloor_base/demo/shopfloor_scenario_demo.xml new file mode 100644 index 0000000000..38c25b44f8 --- /dev/null +++ b/shopfloor_base/demo/shopfloor_scenario_demo.xml @@ -0,0 +1,6 @@ + + + Demo scenario 1 + demo_scenario_1 + + diff --git a/shopfloor_base/models/__init__.py b/shopfloor_base/models/__init__.py index bfa2f6eda4..df7a330e39 100644 --- a/shopfloor_base/models/__init__.py +++ b/shopfloor_base/models/__init__.py @@ -1,2 +1,3 @@ from . import shopfloor_menu from . import shopfloor_profile +from . import shopfloor_scenario diff --git a/shopfloor_base/models/shopfloor_menu.py b/shopfloor_base/models/shopfloor_menu.py index 792d7a1139..c844dfffcd 100644 --- a/shopfloor_base/models/shopfloor_menu.py +++ b/shopfloor_base/models/shopfloor_menu.py @@ -1,4 +1,6 @@ # Copyright 2020 Camptocamp SA (http://www.camptocamp.com) +# Copyright 2021 ACSONE SA/NV (http://www.camptocamp.com) +# @author Simone Orsi # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). from odoo import fields, models @@ -10,5 +12,11 @@ class ShopfloorMenu(models.Model): name = fields.Char(required=True, translate=True) sequence = fields.Integer() - scenario = fields.Char() + profile_id = fields.Many2one( + "shopfloor.profile", string="Profile", help="Visible on this profile only" + ) + scenario_id = fields.Many2one( + comodel_name="shopfloor.scenario", required=True, ondelete="cascade" + ) + scenario = fields.Char(related="scenario_id.key") active = fields.Boolean(default=True) diff --git a/shopfloor_base/models/shopfloor_scenario.py b/shopfloor_base/models/shopfloor_scenario.py new file mode 100644 index 0000000000..b468ddbfc6 --- /dev/null +++ b/shopfloor_base/models/shopfloor_scenario.py @@ -0,0 +1,70 @@ +# Copyright 2021 ACSONE SA/NV (http://www.camptocamp.com) +# @author Simone Orsi +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). +import logging + +from odoo import api, fields, models + +from odoo.addons.base_sparse_field.models.fields import Serialized +from odoo.addons.http_routing.models.ir_http import slugify + +_logger = logging.getLogger(__name__) + +try: + import yaml +except ImportError: + _logger.debug("`yaml` lib is missing") + + +class ShopfloorScenario(models.Model): + _name = "shopfloor.scenario" + _description = "Shopfloor Scenario" + + name = fields.Char(required=True, translate=True) + key = fields.Char(required=True) + options = Serialized(compute="_compute_options", default={}) + options_edit = fields.Char(help="Configure options via YAML") + + _sql_constraints = [("key", "unique(key)", "Scenario key must be unique")] + + @api.depends("options_edit") + def _compute_options(self): + for rec in self: + rec.options = rec._load_options() + + def _load_options(self): + return yaml.safe_load(self.options_edit or "") or {} + + @api.onchange("name") + def _onchange_name_for_key(self): + # Keep this specific name for the method to avoid possible overrides + # of existing `_onchange_name` methods + if self.name and not self.key: + self.key = self.name + + @api.onchange("key") + def _onchange_key(self): + if self.key: + # make sure is normalized + self.key = self._normalize_key(self.key) + + @api.model + def create(self, vals): + self._handle_key(vals) + return super().create(vals) + + def write(self, vals): + self._handle_key(vals) + return super().write(vals) + + def _handle_key(self, vals): + # make sure technical names are always there + if not vals.get("key") and vals.get("name"): + vals["key"] = self._normalize_key(vals["name"]) + + @staticmethod + def _normalize_key(name): + return slugify(name).replace("-", "_") + + def has_option(self, key): + return self.options.get(key, False) diff --git a/shopfloor_base/security/ir.model.access.csv b/shopfloor_base/security/ir.model.access.csv index 6558c139bb..b8d48e0f40 100644 --- a/shopfloor_base/security/ir.model.access.csv +++ b/shopfloor_base/security/ir.model.access.csv @@ -3,3 +3,5 @@ "access_shopfloor_menu_manager","shopfloor menu manager","model_shopfloor_menu","shopfloor_base.group_shopfloor_manager",1,1,1,1 "access_shopfloor_profile_users","shopfloor profile","model_shopfloor_profile","shopfloor_base.group_shopfloor_user",1,0,0,0 "access_shopfloor_profile_manager","shopfloor profile manager","model_shopfloor_profile","shopfloor_base.group_shopfloor_manager",1,1,1,1 +"access_shopfloor_scenario_users","shopfloor scenario","model_shopfloor_scenario","shopfloor_base.group_shopfloor_user",1,0,0,0 +"access_shopfloor_scenario_manager","shopfloor scenario manager","model_shopfloor_scenario","shopfloor_base.group_shopfloor_manager",1,1,1,1 diff --git a/shopfloor_base/services/service.py b/shopfloor_base/services/service.py index bb61ba2c64..f85ac9cef2 100644 --- a/shopfloor_base/services/service.py +++ b/shopfloor_base/services/service.py @@ -267,7 +267,7 @@ def options(self): options = {} if self._requires_header_menu and getattr(self.work, "menu", None): - options = self.work.menu.options or {} + options = self.work.menu.scenario_id.options or {} options.update(getattr(self.work, "options", {})) self._options = DotDict(options) return self._options diff --git a/shopfloor_base/tests/__init__.py b/shopfloor_base/tests/__init__.py index 7aa9206af5..3a04abeb37 100644 --- a/shopfloor_base/tests/__init__.py +++ b/shopfloor_base/tests/__init__.py @@ -1,3 +1,4 @@ +from . import test_shopfloor_models from . import test_app from . import test_user from . import test_menu diff --git a/shopfloor_base/tests/common.py b/shopfloor_base/tests/common.py index f8fce485d7..7634394968 100644 --- a/shopfloor_base/tests/common.py +++ b/shopfloor_base/tests/common.py @@ -107,6 +107,7 @@ def setUpClassUsers(cls): {"no_reset_password": True, "mail_create_nosubscribe": True} ) cls.shopfloor_user = Users.create(cls._shopfloor_user_values()) + cls.shopfloor_manager = Users.create(cls._shopfloor_manager_values()) cls.env = cls.env(user=cls.shopfloor_user) @classmethod @@ -120,6 +121,17 @@ def _shopfloor_user_values(cls): ], } + @classmethod + def _shopfloor_manager_values(cls): + return { + "name": "Johnny Manager", + "login": "jmanager", + "email": "jmanager@example.com", + "groups_id": [ + (6, 0, [cls.env.ref("shopfloor_base.group_shopfloor_manager").id]) + ], + } + @classmethod def setUpClassVars(cls): pass diff --git a/shopfloor_base/tests/test_shopfloor_models.py b/shopfloor_base/tests/test_shopfloor_models.py new file mode 100644 index 0000000000..b96c8a3916 --- /dev/null +++ b/shopfloor_base/tests/test_shopfloor_models.py @@ -0,0 +1,42 @@ +# Copyright 2021 ACSONE SA/NV (http://www.camptocamp.com) +# @author Simone Orsi +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). + +from odoo.tests.common import Form + +from .common import CommonCase + + +class TestShopfloorRecords(CommonCase): + @classmethod + def setUpClassUsers(cls): + super().setUpClassUsers() + cls.env = cls.env(user=cls.shopfloor_manager) + + def test_scenario(self): + rec = self.env["shopfloor.scenario"].create( + { + "name": "New Scenario", + "options_edit": """ +opt1: true +opt2: false +opt3: + nested: true + """, + } + ) + self.assertEqual(rec.key, "new_scenario") + # fmt: off + self.assertEqual(rec.options, { + "opt1": True, + "opt2": False, + "opt3": { + "nested": True, + }, + }) + # fmt: on + with Form(self.env["shopfloor.scenario"]) as form: + form.name = "Test Onchange" + self.assertEqual(form.key, "test_onchange") + + # TODO: test other records (menu, profile) diff --git a/shopfloor_base/views/menus.xml b/shopfloor_base/views/menus.xml index 14d3a28386..3549599553 100644 --- a/shopfloor_base/views/menus.xml +++ b/shopfloor_base/views/menus.xml @@ -18,6 +18,12 @@ parent="menu_shopfloor_root" sequence="20" /> + + - - + [('request_url', 'like', '%shopfloor%')] + + + + + /> From 5d4b3075bd2270b9a6510946de4435c4489c2dc5 Mon Sep 17 00:00:00 2001 From: Simone Orsi Date: Tue, 16 Feb 2021 08:22:50 +0100 Subject: [PATCH 016/111] shopfloor_base: pure json for scenario options edit --- shopfloor_base/__manifest__.py | 1 - shopfloor_base/models/shopfloor_scenario.py | 23 +++++++++++------ shopfloor_base/tests/test_shopfloor_models.py | 25 ++++++++++++++----- .../views/shopfloor_scenario_views.xml | 8 +++--- 4 files changed, 37 insertions(+), 20 deletions(-) diff --git a/shopfloor_base/__manifest__.py b/shopfloor_base/__manifest__.py index adf5a351ed..30859c1943 100644 --- a/shopfloor_base/__manifest__.py +++ b/shopfloor_base/__manifest__.py @@ -36,6 +36,5 @@ "demo/shopfloor_menu_demo.xml", "demo/shopfloor_profile_demo.xml", ], - "external_dependencies": {"python": ["pyyaml"]}, "post_init_hook": "post_init_hook", } diff --git a/shopfloor_base/models/shopfloor_scenario.py b/shopfloor_base/models/shopfloor_scenario.py index a11dc810fd..9bc72b3647 100644 --- a/shopfloor_base/models/shopfloor_scenario.py +++ b/shopfloor_base/models/shopfloor_scenario.py @@ -1,6 +1,7 @@ # Copyright 2021 ACSONE SA/NV (http://www.acsone.eu) # @author Simone Orsi # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). +import json import logging from odoo import api, fields, models @@ -10,20 +11,21 @@ _logger = logging.getLogger(__name__) -try: - import yaml -except ImportError: - _logger.debug("`yaml` lib is missing") - class ShopfloorScenario(models.Model): _name = "shopfloor.scenario" _description = "Shopfloor Scenario" name = fields.Char(required=True, translate=True) - key = fields.Char(required=True) + key = fields.Char( + required=True, + help="Identify scenario univocally. " + "This value must match a service component's `usage`.", + ) options = Serialized(compute="_compute_options", default={}) - options_edit = fields.Char(help="Configure options via YAML") + options_edit = fields.Text( + help="Configure options via JSON", inverse="_inverse_options_edit" + ) _sql_constraints = [("key", "unique(key)", "Scenario key must be unique")] @@ -32,8 +34,13 @@ def _compute_options(self): for rec in self: rec.options = rec._load_options() + def _inverse_options_edit(self): + for rec in self: + # Make sure options_edit is always readable + rec.options_edit = json.dumps(rec.options or {}, indent=4, sort_keys=True) + def _load_options(self): - return yaml.safe_load(self.options_edit or "") or {} + return json.loads(self.options_edit or "{}") @api.onchange("name") def _onchange_name_for_key(self): diff --git a/shopfloor_base/tests/test_shopfloor_models.py b/shopfloor_base/tests/test_shopfloor_models.py index dd6d24d4c4..ed6b76129c 100644 --- a/shopfloor_base/tests/test_shopfloor_models.py +++ b/shopfloor_base/tests/test_shopfloor_models.py @@ -1,6 +1,11 @@ # Copyright 2021 ACSONE SA/NV (http://www.acsone.eu) # @author Simone Orsi # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). +<<<<<<< HEAD +======= + +import json +>>>>>>> e127e37f... fixup! shopfloor_base: pure json for scenario options edit from odoo.tests.common import Form @@ -18,23 +23,31 @@ def test_scenario(self): { "name": "New Scenario", "options_edit": """ -opt1: true -opt2: false -opt3: - nested: true +{ + "opt1": true, + "opt2": false, + "opt3": + { + "nested": true + } +} """, } ) self.assertEqual(rec.key, "new_scenario") # fmt: off - self.assertEqual(rec.options, { + expected = { "opt1": True, "opt2": False, "opt3": { "nested": True, }, - }) + } # fmt: on + self.assertEqual(rec.options, expected) + self.assertEqual( + rec.options_edit, json.dumps(expected, indent=4, sort_keys=True) + ) with Form(self.env["shopfloor.scenario"]) as form: form.name = "Test Onchange" self.assertEqual(form.key, "test_onchange") diff --git a/shopfloor_base/views/shopfloor_scenario_views.xml b/shopfloor_base/views/shopfloor_scenario_views.xml index 980df1792e..8a93f320b8 100644 --- a/shopfloor_base/views/shopfloor_scenario_views.xml +++ b/shopfloor_base/views/shopfloor_scenario_views.xml @@ -22,11 +22,9 @@ - + + + From a2d74e599be3472e87c6550127f27c5f72a76b67 Mon Sep 17 00:00:00 2001 From: Simone Orsi Date: Tue, 16 Feb 2021 08:31:12 +0100 Subject: [PATCH 017/111] shopfloor_base: update README --- shopfloor_base/README.rst | 66 +-- shopfloor_base/readme/CONFIGURE.rst | 36 +- shopfloor_base/readme/DESCRIPTION.rst | 15 +- shopfloor_base/readme/ROADMAP.rst | 1 - shopfloor_base/static/description/index.html | 521 +++++++++++++++++++ 5 files changed, 570 insertions(+), 69 deletions(-) create mode 100644 shopfloor_base/static/description/index.html diff --git a/shopfloor_base/README.rst b/shopfloor_base/README.rst index 497f2adb9a..819946310d 100644 --- a/shopfloor_base/README.rst +++ b/shopfloor_base/README.rst @@ -1,6 +1,6 @@ -======================================= -TODO: Shopfloor BASE FIXME AFTER SPLIT! -======================================= +============== +Shopfloor Base +============== .. !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! !! This file is generated by oca-gen-addon-readme !! @@ -14,10 +14,10 @@ TODO: Shopfloor BASE FIXME AFTER SPLIT! :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/13.0/shopfloor + :target: https://github.com/OCA/wms/tree/13.0/shopfloor_base :alt: OCA/wms .. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png - :target: https://translation.odoo-community.org/projects/wms-13-0/wms-13-0-shopfloor + :target: https://translation.odoo-community.org/projects/wms-13-0/wms-13-0-shopfloor_base :alt: Translate me on Weblate .. |badge5| image:: https://img.shields.io/badge/runbot-Try%20me-875A7B.png :target: https://runbot.odoo-community.org/runbot/285/13.0 @@ -25,20 +25,11 @@ TODO: Shopfloor BASE FIXME AFTER SPLIT! |badge1| |badge2| |badge3| |badge4| |badge5| -Shopfloor is a barcode scanner application for internal warehouse operations. +Shopfloor is a barcode scanner application. -The application supports scenarios, to relate to Operation Types: - -* Cluster Picking -* Zone Picking -* Checkout/Packing -* Delivery -* Location Content Transfer -* Single Pack Transfer - -This module provides REST APIs to support the scenarios. It needs a frontend +This module provides REST APIs to support scenario. It needs a frontend to consume the backend APIs and provide screens for users on barcode devices. -A default front-end application is provided by ``shopfloor_mobile``. +A default front-end application is provided by ``shopfloor_mobile_base``. .. IMPORTANT:: This is an alpha version, the data model and design can change at any time without warning. @@ -53,10 +44,16 @@ A default front-end application is provided by ``shopfloor_mobile``. Configuration ============= +Shopfloor config menu +~~~~~~~~~~~~~~~~~~~~~ + +In the main menu (or home screen) click on "Shopfloor". + + Profiles ~~~~~~~~ -In Inventory / Configuration / Shopfloor / Profiles. +In Shopfloor / Profiles. The profiles are used to restrict which menus are shown on the frontend application. When a user logs in the scanner application, they have to @@ -65,32 +62,26 @@ select their profile, so the correct menus are shown. Menus ~~~~~ -In Inventory / Configuration / Shopfloor / Menus. +In Shopfloor / Menus. -The menus are displayed on the frontend application and store the configuration -of the scenarios. Each menu must use a scenario and defines which Operation Types -they are allowed to process. +The menus are displayed on the frontend application. +The configuration may come from the menu itself +and/or from the scenario linked to it. Their profile will restrict the visibility to the profile chosen on the device. If a menu has no profile, it is shown in every profile. -Some scenarios may have additional options, which are explained in tooltips. - -Logs retention -~~~~~~~~~~~~~~ +Some scenario may have additional options. -Logs are kept in database for every REST requests made by a client application. -They can be used for debugging and monitoring of the activity. -The Logs menu is shown only with Developer tools (``?debug=1``) activated. - -By default, Shopfloor logs are kept 30 days. -You can change the duration of the retention by changing the System Parameter -``shopfloor.log.retention.days``. +Scenario +~~~~~~~~ -If the value is set to 0, the logs are not stored at all. +In Shopfloor / Scenario. -Logged data is: request URL and method, parameters, headers, result or error. +A Scenario represents a flow (or more basically "something to do" with the app. +Each scenario must have a name and a unique key. +The key must match a registered shopfloor service component. Usage ===== @@ -106,7 +97,6 @@ Known issues / Roadmap ====================== * improve documentation -* split out scenario components to their own modules Changelog ========= @@ -122,7 +112,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 -`feedback `_. +`feedback `_. Do not contact contributors directly about support or help with technical issues. @@ -188,6 +178,6 @@ Current `maintainers `__: |maintainer-guewen| |maintainer-simahawk| |maintainer-sebalix| -This module is part of the `OCA/wms `_ project on GitHub. +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_base/readme/CONFIGURE.rst b/shopfloor_base/readme/CONFIGURE.rst index 92102a8b42..029fdce00b 100644 --- a/shopfloor_base/readme/CONFIGURE.rst +++ b/shopfloor_base/readme/CONFIGURE.rst @@ -1,7 +1,13 @@ +Shopfloor config menu +~~~~~~~~~~~~~~~~~~~~~ + +In the main menu (or home screen) click on "Shopfloor". + + Profiles ~~~~~~~~ -In Inventory / Configuration / Shopfloor / Profiles. +In Shopfloor / Profiles. The profiles are used to restrict which menus are shown on the frontend application. When a user logs in the scanner application, they have to @@ -10,29 +16,23 @@ select their profile, so the correct menus are shown. Menus ~~~~~ -In Inventory / Configuration / Shopfloor / Menus. +In Shopfloor / Menus. -The menus are displayed on the frontend application and store the configuration -of the scenarios. Each menu must use a scenario and defines which Operation Types -they are allowed to process. +The menus are displayed on the frontend application. +The configuration may come from the menu itself +and/or from the scenario linked to it. Their profile will restrict the visibility to the profile chosen on the device. If a menu has no profile, it is shown in every profile. -Some scenarios may have additional options, which are explained in tooltips. +Some scenario may have additional options. -Logs retention -~~~~~~~~~~~~~~ -Logs are kept in database for every REST requests made by a client application. -They can be used for debugging and monitoring of the activity. - -The Logs menu is shown only with Developer tools (``?debug=1``) activated. - -By default, Shopfloor logs are kept 30 days. -You can change the duration of the retention by changing the System Parameter -``shopfloor.log.retention.days``. +Scenario +~~~~~~~~ -If the value is set to 0, the logs are not stored at all. +In Shopfloor / Scenario. -Logged data is: request URL and method, parameters, headers, result or error. +A Scenario represents a flow (or more basically "something to do" with the app. +Each scenario must have a name and a unique key. +The key must match a registered shopfloor service component. diff --git a/shopfloor_base/readme/DESCRIPTION.rst b/shopfloor_base/readme/DESCRIPTION.rst index 6242bc91fc..6d24f63c12 100644 --- a/shopfloor_base/readme/DESCRIPTION.rst +++ b/shopfloor_base/readme/DESCRIPTION.rst @@ -1,14 +1,5 @@ -Shopfloor is a barcode scanner application for internal warehouse operations. +Shopfloor is a barcode scanner application. -The application supports scenarios, to relate to Operation Types: - -* Cluster Picking -* Zone Picking -* Checkout/Packing -* Delivery -* Location Content Transfer -* Single Pack Transfer - -This module provides REST APIs to support the scenarios. It needs a frontend +This module provides REST APIs to support scenario. It needs a frontend to consume the backend APIs and provide screens for users on barcode devices. -A default front-end application is provided by ``shopfloor_mobile``. +A default front-end application is provided by ``shopfloor_mobile_base``. diff --git a/shopfloor_base/readme/ROADMAP.rst b/shopfloor_base/readme/ROADMAP.rst index 7f274ec28f..b2b740e6df 100644 --- a/shopfloor_base/readme/ROADMAP.rst +++ b/shopfloor_base/readme/ROADMAP.rst @@ -1,2 +1 @@ * improve documentation -* split out scenario components to their own modules diff --git a/shopfloor_base/static/description/index.html b/shopfloor_base/static/description/index.html new file mode 100644 index 0000000000..a978954013 --- /dev/null +++ b/shopfloor_base/static/description/index.html @@ -0,0 +1,521 @@ + + + + + + +Shopfloor Base + + + +
+

Shopfloor Base

+ + +

Alpha License: AGPL-3 OCA/wms Translate me on Weblate Try me on Runbot

+

Shopfloor is a barcode scanner application.

+

This module provides REST APIs to support scenario. It needs a frontend +to consume the backend APIs and provide screens for users on barcode devices. +A default front-end application is provided by shopfloor_mobile_base.

+
+

Important

+

This is an alpha version, the data model and design can change at any time without warning. +Only for development or testing purpose, do not use in production. +More details on development status

+
+

Table of contents

+ +
+

Configuration

+
+

Shopfloor config menu

+

In the main menu (or home screen) click on “Shopfloor”.

+
+
+

Profiles

+

In Shopfloor / Profiles.

+

The profiles are used to restrict which menus are shown on the frontend +application. When a user logs in the scanner application, they have to +select their profile, so the correct menus are shown.

+
+ +
+

Scenario

+

In Shopfloor / Scenario.

+

A Scenario represents a flow (or more basically “something to do” with the app. +Each scenario must have a name and a unique key. +The key must match a registered shopfloor service component.

+
+
+
+

Usage

+

An API key is created in the Demo data (for development), using +the Demo user. The key to use in the HTTP header API-KEY is: 72B044F7AC780DAC

+

Curl example:

+
+curl -X POST "http://localhost:8069/shopfloor/user/menu" -H  "accept: */*" -H  "Content-Type: application/json" -H "API-KEY: 72B044F7AC780DAC"
+
+
+
+

Known issues / Roadmap

+
    +
  • improve documentation
  • +
+
+
+

Changelog

+
+

13.0.1.0.0

+

First official version.

+
+
+
+

Bug Tracker

+

Bugs are tracked on GitHub Issues. +In case of trouble, please check there if your issue has already been reported. +If you spotted it first, help us smashing it by providing a detailed and welcomed +feedback.

+

Do not contact contributors directly about support or help with technical issues.

+
+
+

Credits

+
+

Authors

+
    +
  • Camptocamp
  • +
  • BCIM
  • +
  • Akretion
  • +
+
+
+

Contributors

+ +
+
+

Design

+ +
+
+

Other credits

+

Financial support

+
    +
  • Cosanum
  • +
  • Camptocamp R&D
  • +
  • Akretion R&D
  • +
+
+
+

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 maintainers:

+

guewen simahawk sebalix

+

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.

+
+
+
+ + From e047a1373ef6022f27bed175c360252003ee0a4b Mon Sep 17 00:00:00 2001 From: Simone Orsi Date: Tue, 16 Feb 2021 08:41:30 +0100 Subject: [PATCH 018/111] shopfloor_base: drop dependency on rest_log --- shopfloor_base/__manifest__.py | 8 +------- shopfloor_base/security/groups.xml | 4 ---- shopfloor_base/views/menus.xml | 17 ----------------- 3 files changed, 1 insertion(+), 28 deletions(-) diff --git a/shopfloor_base/__manifest__.py b/shopfloor_base/__manifest__.py index 30859c1943..986b72630f 100644 --- a/shopfloor_base/__manifest__.py +++ b/shopfloor_base/__manifest__.py @@ -14,13 +14,7 @@ "maintainers": ["guewen", "simahawk", "sebalix"], "license": "AGPL-3", "application": True, - "depends": [ - "base_jsonify", - "base_rest", - "rest_log", - "base_sparse_field", - "auth_api_key", - ], + "depends": ["base_jsonify", "base_rest", "base_sparse_field", "auth_api_key"], "data": [ "data/module_category_data.xml", "security/groups.xml", diff --git a/shopfloor_base/security/groups.xml b/shopfloor_base/security/groups.xml index baedacaa99..5be736bdab 100644 --- a/shopfloor_base/security/groups.xml +++ b/shopfloor_base/security/groups.xml @@ -13,9 +13,5 @@ name="users" eval="[(4, ref('base.user_root')), (4, ref('base.user_admin'))]" /> - diff --git a/shopfloor_base/views/menus.xml b/shopfloor_base/views/menus.xml index 7167fe8cd4..26f5c3dcb0 100644 --- a/shopfloor_base/views/menus.xml +++ b/shopfloor_base/views/menus.xml @@ -24,21 +24,4 @@ sequence="30" /> - - Logs - rest.log - ir.actions.act_window - tree,form - - [('request_url', 'like', '%shopfloor%')] - - - - From 1d31fe3cbb37769b0bbe6c7f24f0b714dcd2d89e Mon Sep 17 00:00:00 2001 From: Simone Orsi Date: Tue, 16 Feb 2021 08:42:03 +0100 Subject: [PATCH 019/111] shopfloor_base: re-license to LGPL --- shopfloor_base/README.rst | 6 +++--- shopfloor_base/__manifest__.py | 4 ++-- shopfloor_base/actions/base_action.py | 2 +- shopfloor_base/actions/data.py | 2 +- shopfloor_base/actions/data_detail.py | 2 +- shopfloor_base/actions/message.py | 2 +- shopfloor_base/actions/savepoint.py | 2 +- shopfloor_base/actions/schema.py | 2 +- shopfloor_base/actions/schema_detail.py | 2 +- shopfloor_base/actions/search.py | 2 +- shopfloor_base/controllers/main.py | 2 +- shopfloor_base/hooks.py | 2 +- shopfloor_base/models/shopfloor_menu.py | 2 +- shopfloor_base/models/shopfloor_profile.py | 2 +- shopfloor_base/models/shopfloor_scenario.py | 2 +- shopfloor_base/security/groups.xml | 2 +- shopfloor_base/services/app.py | 2 +- shopfloor_base/services/forms/form_mixin.py | 2 +- shopfloor_base/services/menu.py | 2 +- shopfloor_base/services/profile.py | 2 +- shopfloor_base/services/scan_anything.py | 2 +- shopfloor_base/services/service.py | 2 +- shopfloor_base/services/user.py | 2 +- shopfloor_base/services/validator.py | 2 +- shopfloor_base/static/description/index.html | 2 +- shopfloor_base/tests/common.py | 2 +- shopfloor_base/tests/common_misc.py | 2 +- shopfloor_base/tests/test_actions_data.py | 2 +- shopfloor_base/tests/test_app.py | 2 +- shopfloor_base/tests/test_menu.py | 2 +- shopfloor_base/tests/test_openapi.py | 2 +- shopfloor_base/tests/test_profile.py | 2 +- shopfloor_base/tests/test_scan_anything.py | 2 +- shopfloor_base/tests/test_shopfloor_models.py | 6 +----- shopfloor_base/tests/test_user.py | 2 +- shopfloor_base/utils.py | 2 +- 36 files changed, 39 insertions(+), 43 deletions(-) diff --git a/shopfloor_base/README.rst b/shopfloor_base/README.rst index 819946310d..e8e9c0cae3 100644 --- a/shopfloor_base/README.rst +++ b/shopfloor_base/README.rst @@ -10,9 +10,9 @@ Shopfloor Base .. |badge1| image:: https://img.shields.io/badge/maturity-Alpha-red.png :target: https://odoo-community.org/page/development-status :alt: Alpha -.. |badge2| image:: https://img.shields.io/badge/licence-AGPL--3-blue.png - :target: http://www.gnu.org/licenses/agpl-3.0-standalone.html - :alt: License: AGPL-3 +.. |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/13.0/shopfloor_base :alt: OCA/wms diff --git a/shopfloor_base/__manifest__.py b/shopfloor_base/__manifest__.py index 986b72630f..1e89ebb6f5 100644 --- a/shopfloor_base/__manifest__.py +++ b/shopfloor_base/__manifest__.py @@ -1,7 +1,7 @@ # Copyright 2020 Camptocamp SA (http://www.camptocamp.com) # Copyright 2020 Akretion (http://www.akretion.com) # Copyright 2020 BCIM -# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). +# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html). { "name": "Shopfloor Base", @@ -12,7 +12,7 @@ "website": "https://github.com/OCA/wms", "author": "Camptocamp, BCIM, Akretion, Odoo Community Association (OCA)", "maintainers": ["guewen", "simahawk", "sebalix"], - "license": "AGPL-3", + "license": "LGPL-3", "application": True, "depends": ["base_jsonify", "base_rest", "base_sparse_field", "auth_api_key"], "data": [ diff --git a/shopfloor_base/actions/base_action.py b/shopfloor_base/actions/base_action.py index c9d2c596f5..2e44fdf1d6 100644 --- a/shopfloor_base/actions/base_action.py +++ b/shopfloor_base/actions/base_action.py @@ -1,7 +1,7 @@ # Copyright 2020 Camptocamp SA (http://www.camptocamp.com) # Copyright 2021 ACSONE SA/NV (http://www.acsone.eu) # @author Simone Orsi -# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). +# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html). from odoo.addons.base_rest.controllers.main import _PseudoCollection from odoo.addons.component.core import AbstractComponent, WorkContext diff --git a/shopfloor_base/actions/data.py b/shopfloor_base/actions/data.py index 98722840c3..8b02ff0bd6 100644 --- a/shopfloor_base/actions/data.py +++ b/shopfloor_base/actions/data.py @@ -1,5 +1,5 @@ # Copyright 2020 Camptocamp SA (http://www.camptocamp.com) -# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). +# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html). from odoo.addons.component.core import Component from ..utils import ensure_model diff --git a/shopfloor_base/actions/data_detail.py b/shopfloor_base/actions/data_detail.py index 111beb9226..6b764dd26b 100644 --- a/shopfloor_base/actions/data_detail.py +++ b/shopfloor_base/actions/data_detail.py @@ -1,5 +1,5 @@ # Copyright 2020 Camptocamp SA (http://www.camptocamp.com) -# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). +# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html). from odoo.addons.component.core import Component diff --git a/shopfloor_base/actions/message.py b/shopfloor_base/actions/message.py index 33702d19d9..96cea7f2c5 100644 --- a/shopfloor_base/actions/message.py +++ b/shopfloor_base/actions/message.py @@ -1,5 +1,5 @@ # Copyright 2020 Camptocamp SA (http://www.camptocamp.com) -# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). +# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html). # from odoo import _ from odoo.addons.component.core import Component diff --git a/shopfloor_base/actions/savepoint.py b/shopfloor_base/actions/savepoint.py index 96daa8fe61..5c4b40d724 100644 --- a/shopfloor_base/actions/savepoint.py +++ b/shopfloor_base/actions/savepoint.py @@ -1,5 +1,5 @@ # Copyright 2020 Camptocamp SA (http://www.camptocamp.com) -# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). +# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html). import uuid from psycopg2 import sql diff --git a/shopfloor_base/actions/schema.py b/shopfloor_base/actions/schema.py index ddf57cd5c0..02c8e76996 100644 --- a/shopfloor_base/actions/schema.py +++ b/shopfloor_base/actions/schema.py @@ -1,7 +1,7 @@ # Copyright 2020 Camptocamp SA (http://www.camptocamp.com) # Copyright 2021 ACSONE SA/NV (http://www.acsone.eu) # @author Simone Orsi -# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). +# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html). from odoo.addons.component.core import Component diff --git a/shopfloor_base/actions/schema_detail.py b/shopfloor_base/actions/schema_detail.py index b919eb1f35..b1afb2c29a 100644 --- a/shopfloor_base/actions/schema_detail.py +++ b/shopfloor_base/actions/schema_detail.py @@ -1,5 +1,5 @@ # Copyright 2020 Camptocamp SA (http://www.camptocamp.com) -# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). +# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html). from odoo.addons.component.core import Component diff --git a/shopfloor_base/actions/search.py b/shopfloor_base/actions/search.py index d963555bb3..c87b5fbdeb 100644 --- a/shopfloor_base/actions/search.py +++ b/shopfloor_base/actions/search.py @@ -1,5 +1,5 @@ # Copyright 2020 Camptocamp SA (http://www.camptocamp.com) -# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). +# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html). from odoo.addons.component.core import Component diff --git a/shopfloor_base/controllers/main.py b/shopfloor_base/controllers/main.py index 218e08e3b7..f7bc009ec5 100644 --- a/shopfloor_base/controllers/main.py +++ b/shopfloor_base/controllers/main.py @@ -1,6 +1,6 @@ # Copyright 2020 Camptocamp SA (http://www.camptocamp.com) # Copyright 2020 Akretion (http://www.akretion.com) -# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). +# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html). from odoo.addons.base_rest.controllers import main diff --git a/shopfloor_base/hooks.py b/shopfloor_base/hooks.py index 02cead2908..3a2a8967a6 100644 --- a/shopfloor_base/hooks.py +++ b/shopfloor_base/hooks.py @@ -1,5 +1,5 @@ # Copyright 2021 ACSONE SA/NV -# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). +# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html). from odoo import SUPERUSER_ID, api diff --git a/shopfloor_base/models/shopfloor_menu.py b/shopfloor_base/models/shopfloor_menu.py index 9ebec2b4b1..328cb554c5 100644 --- a/shopfloor_base/models/shopfloor_menu.py +++ b/shopfloor_base/models/shopfloor_menu.py @@ -1,7 +1,7 @@ # Copyright 2020 Camptocamp SA (http://www.camptocamp.com) # Copyright 2021 ACSONE SA/NV (http://www.acsone.eu) # @author Simone Orsi -# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). +# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html). from odoo import fields, models diff --git a/shopfloor_base/models/shopfloor_profile.py b/shopfloor_base/models/shopfloor_profile.py index 060df4355e..f0ff70ff03 100644 --- a/shopfloor_base/models/shopfloor_profile.py +++ b/shopfloor_base/models/shopfloor_profile.py @@ -1,5 +1,5 @@ # Copyright 2020 Camptocamp SA (http://www.camptocamp.com) -# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). +# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html). from odoo import fields, models diff --git a/shopfloor_base/models/shopfloor_scenario.py b/shopfloor_base/models/shopfloor_scenario.py index 9bc72b3647..a5b7c7a790 100644 --- a/shopfloor_base/models/shopfloor_scenario.py +++ b/shopfloor_base/models/shopfloor_scenario.py @@ -1,6 +1,6 @@ # Copyright 2021 ACSONE SA/NV (http://www.acsone.eu) # @author Simone Orsi -# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). +# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html). import json import logging diff --git a/shopfloor_base/security/groups.xml b/shopfloor_base/security/groups.xml index 5be736bdab..db2a0e5800 100644 --- a/shopfloor_base/security/groups.xml +++ b/shopfloor_base/security/groups.xml @@ -1,6 +1,6 @@ + License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). --> Shopfloor User diff --git a/shopfloor_base/services/app.py b/shopfloor_base/services/app.py index ecbc8f21c0..1387c749ee 100644 --- a/shopfloor_base/services/app.py +++ b/shopfloor_base/services/app.py @@ -1,5 +1,5 @@ # Copyright 2020 Camptocamp SA (http://www.camptocamp.com) -# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). +# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html). from odoo.addons.component.core import Component diff --git a/shopfloor_base/services/forms/form_mixin.py b/shopfloor_base/services/forms/form_mixin.py index 1389082c80..11592a3cc7 100644 --- a/shopfloor_base/services/forms/form_mixin.py +++ b/shopfloor_base/services/forms/form_mixin.py @@ -1,6 +1,6 @@ # Copyright 2020 Camptocamp SA (http://www.camptocamp.com) # @author Simone Orsi -# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). +# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html). from odoo import _ from odoo.addons.component.core import AbstractComponent diff --git a/shopfloor_base/services/menu.py b/shopfloor_base/services/menu.py index d823cbba12..bd57626141 100644 --- a/shopfloor_base/services/menu.py +++ b/shopfloor_base/services/menu.py @@ -1,5 +1,5 @@ # Copyright 2020 Camptocamp SA (http://www.camptocamp.com) -# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). +# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html). from odoo.osv import expression from odoo.addons.base_rest.components.service import to_int diff --git a/shopfloor_base/services/profile.py b/shopfloor_base/services/profile.py index 6724ecaca1..aea52239da 100644 --- a/shopfloor_base/services/profile.py +++ b/shopfloor_base/services/profile.py @@ -1,5 +1,5 @@ # Copyright 2020 Camptocamp SA (http://www.camptocamp.com) -# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). +# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html). from odoo.addons.base_rest.components.service import to_int from odoo.addons.component.core import Component diff --git a/shopfloor_base/services/scan_anything.py b/shopfloor_base/services/scan_anything.py index 8b82702b47..978f1fd6ce 100644 --- a/shopfloor_base/services/scan_anything.py +++ b/shopfloor_base/services/scan_anything.py @@ -1,7 +1,7 @@ # Copyright 2020 Camptocamp SA (http://www.camptocamp.com) # Copyright 2021 ACSONE SA/NV (http://www.acsone.eu) # @author Simone Orsi -# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). +# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html). from odoo import _ from odoo.addons.component.core import AbstractComponent, Component diff --git a/shopfloor_base/services/service.py b/shopfloor_base/services/service.py index e47d7152f2..0b0c5a8c1f 100644 --- a/shopfloor_base/services/service.py +++ b/shopfloor_base/services/service.py @@ -1,6 +1,6 @@ # Copyright 2020 Camptocamp SA (http://www.camptocamp.com) # Copyright 2020 Akretion (http://www.akretion.com) -# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). +# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html). from werkzeug.exceptions import BadRequest diff --git a/shopfloor_base/services/user.py b/shopfloor_base/services/user.py index ceed40c9ea..e30b1bfc2e 100644 --- a/shopfloor_base/services/user.py +++ b/shopfloor_base/services/user.py @@ -1,5 +1,5 @@ # Copyright 2020 Camptocamp SA (http://www.camptocamp.com) -# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). +# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html). from odoo.addons.base_rest.components.service import to_int from odoo.addons.component.core import Component diff --git a/shopfloor_base/services/validator.py b/shopfloor_base/services/validator.py index ed5cf007f4..93848e32ab 100644 --- a/shopfloor_base/services/validator.py +++ b/shopfloor_base/services/validator.py @@ -1,5 +1,5 @@ # Copyright 2020-2021 Camptocamp SA (http://www.camptocamp.com) -# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). +# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html). import logging from odoo.addons.component.core import AbstractComponent, Component diff --git a/shopfloor_base/static/description/index.html b/shopfloor_base/static/description/index.html index a978954013..d91ef0e767 100644 --- a/shopfloor_base/static/description/index.html +++ b/shopfloor_base/static/description/index.html @@ -367,7 +367,7 @@

Shopfloor Base

!! This file is generated by oca-gen-addon-readme !! !! changes will be overwritten. !! !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! --> -

Alpha License: AGPL-3 OCA/wms Translate me on Weblate Try me on Runbot

+

Alpha License: LGPL-3 OCA/wms Translate me on Weblate Try me on Runbot

Shopfloor is a barcode scanner application.

This module provides REST APIs to support scenario. It needs a frontend to consume the backend APIs and provide screens for users on barcode devices. diff --git a/shopfloor_base/tests/common.py b/shopfloor_base/tests/common.py index d5492e859b..512ebde780 100644 --- a/shopfloor_base/tests/common.py +++ b/shopfloor_base/tests/common.py @@ -1,5 +1,5 @@ # Copyright 2020 Camptocamp SA (http://www.camptocamp.com) -# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). +# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html). from contextlib import contextmanager from pprint import pformat diff --git a/shopfloor_base/tests/common_misc.py b/shopfloor_base/tests/common_misc.py index deac93e946..12e5a8686d 100644 --- a/shopfloor_base/tests/common_misc.py +++ b/shopfloor_base/tests/common_misc.py @@ -1,5 +1,5 @@ # Copyright 2020 Camptocamp SA (http://www.camptocamp.com) -# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). +# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html). import logging _logger = logging.getLogger(__name__) diff --git a/shopfloor_base/tests/test_actions_data.py b/shopfloor_base/tests/test_actions_data.py index 04864889d3..c30ffce908 100644 --- a/shopfloor_base/tests/test_actions_data.py +++ b/shopfloor_base/tests/test_actions_data.py @@ -1,5 +1,5 @@ # Copyright 2020 Camptocamp SA (http://www.camptocamp.com) -# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). +# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html). from .common import CommonCase from .common_misc import ActionsDataTestMixin diff --git a/shopfloor_base/tests/test_app.py b/shopfloor_base/tests/test_app.py index ef66d91a20..0d461a12a5 100644 --- a/shopfloor_base/tests/test_app.py +++ b/shopfloor_base/tests/test_app.py @@ -1,5 +1,5 @@ # Copyright 2020 Camptocamp SA (http://www.camptocamp.com) -# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). +# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html). from .common import CommonCase diff --git a/shopfloor_base/tests/test_menu.py b/shopfloor_base/tests/test_menu.py index abf403d7d8..83a2ae2a55 100644 --- a/shopfloor_base/tests/test_menu.py +++ b/shopfloor_base/tests/test_menu.py @@ -1,5 +1,5 @@ # Copyright 2020 Camptocamp SA (http://www.camptocamp.com) -# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). +# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html). from .common import CommonCase from .common_misc import MenuTestMixin diff --git a/shopfloor_base/tests/test_openapi.py b/shopfloor_base/tests/test_openapi.py index a4e9de18eb..4ceab2a16d 100644 --- a/shopfloor_base/tests/test_openapi.py +++ b/shopfloor_base/tests/test_openapi.py @@ -1,5 +1,5 @@ # Copyright 2020 Camptocamp SA (http://www.camptocamp.com) -# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). +# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html). from .common import CommonCase from .common_misc import OpenAPITestMixin diff --git a/shopfloor_base/tests/test_profile.py b/shopfloor_base/tests/test_profile.py index 4509f70b69..9069514956 100644 --- a/shopfloor_base/tests/test_profile.py +++ b/shopfloor_base/tests/test_profile.py @@ -1,5 +1,5 @@ # Copyright 2020 Camptocamp SA (http://www.camptocamp.com) -# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). +# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html). from .common import CommonCase diff --git a/shopfloor_base/tests/test_scan_anything.py b/shopfloor_base/tests/test_scan_anything.py index 8fba7efc5f..bbddda871c 100644 --- a/shopfloor_base/tests/test_scan_anything.py +++ b/shopfloor_base/tests/test_scan_anything.py @@ -1,7 +1,7 @@ # Copyright 2020 Camptocamp SA (http://www.camptocamp.com) # Copyright 2021 ACSONE SA/NV (http://www.acsone.eu) # @author Simone Orsi -# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). +# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html). from odoo.addons.component.core import Component diff --git a/shopfloor_base/tests/test_shopfloor_models.py b/shopfloor_base/tests/test_shopfloor_models.py index ed6b76129c..895d7e9f13 100644 --- a/shopfloor_base/tests/test_shopfloor_models.py +++ b/shopfloor_base/tests/test_shopfloor_models.py @@ -1,11 +1,7 @@ # Copyright 2021 ACSONE SA/NV (http://www.acsone.eu) # @author Simone Orsi -# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). -<<<<<<< HEAD -======= - +# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html). import json ->>>>>>> e127e37f... fixup! shopfloor_base: pure json for scenario options edit from odoo.tests.common import Form diff --git a/shopfloor_base/tests/test_user.py b/shopfloor_base/tests/test_user.py index c6043b512a..35ca2c6372 100644 --- a/shopfloor_base/tests/test_user.py +++ b/shopfloor_base/tests/test_user.py @@ -1,5 +1,5 @@ # Copyright 2020 Camptocamp SA (http://www.camptocamp.com) -# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). +# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html). from .common import CommonCase from .common_misc import MenuTestMixin diff --git a/shopfloor_base/utils.py b/shopfloor_base/utils.py index 3ed7bf77fb..48654e57b0 100644 --- a/shopfloor_base/utils.py +++ b/shopfloor_base/utils.py @@ -1,5 +1,5 @@ # Copyright 2021 Camptocamp SA (http://www.camptocamp.com) -# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). +# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html). # @author Simone Orsi from functools import wraps From 64b12370cc4b1e8c828ca02ba3be765bc88820f5 Mon Sep 17 00:00:00 2001 From: Simone Orsi Date: Wed, 17 Feb 2021 17:07:08 +0100 Subject: [PATCH 020/111] shopfloor_base: fix group menu by scenario --- shopfloor_base/views/shopfloor_menu.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/shopfloor_base/views/shopfloor_menu.xml b/shopfloor_base/views/shopfloor_menu.xml index 4561c90c51..5ffa2f165e 100644 --- a/shopfloor_base/views/shopfloor_menu.xml +++ b/shopfloor_base/views/shopfloor_menu.xml @@ -65,7 +65,7 @@ string="Scenario" name="groupby_scenario" domain="[]" - context="{'group_by': 'scenario'}" + context="{'group_by': 'scenario_id'}" /> From 7bdd28f031c4190dcf59a1b2e7dd7a0cf52ba61d Mon Sep 17 00:00:00 2001 From: Simone Orsi Date: Wed, 17 Feb 2021 17:07:33 +0100 Subject: [PATCH 021/111] shopfloor_base: add TODO for scenario model --- shopfloor_base/models/shopfloor_scenario.py | 3 +++ shopfloor_base/readme/ROADMAP.rst | 1 + 2 files changed, 4 insertions(+) diff --git a/shopfloor_base/models/shopfloor_scenario.py b/shopfloor_base/models/shopfloor_scenario.py index a5b7c7a790..c06a86f76a 100644 --- a/shopfloor_base/models/shopfloor_scenario.py +++ b/shopfloor_base/models/shopfloor_scenario.py @@ -17,6 +17,9 @@ class ShopfloorScenario(models.Model): _description = "Shopfloor Scenario" name = fields.Char(required=True, translate=True) + # TODO: make it readonly in UI? + # Make it a Selection field? + # Normally this will be used only by dev implementing new scenario. key = fields.Char( required=True, help="Identify scenario univocally. " diff --git a/shopfloor_base/readme/ROADMAP.rst b/shopfloor_base/readme/ROADMAP.rst index b2b740e6df..47cd17a12f 100644 --- a/shopfloor_base/readme/ROADMAP.rst +++ b/shopfloor_base/readme/ROADMAP.rst @@ -1 +1,2 @@ * improve documentation +* change shopfloor.scenario.key to selection? See comment in model From 86a760db898f8c6056f4041419e6ee5b4eb40d88 Mon Sep 17 00:00:00 2001 From: Simone Orsi Date: Thu, 18 Feb 2021 15:34:24 +0100 Subject: [PATCH 022/111] shopfloor_base: log what done via hooks --- shopfloor_base/hooks.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/shopfloor_base/hooks.py b/shopfloor_base/hooks.py index 3a2a8967a6..59ccff8f1b 100644 --- a/shopfloor_base/hooks.py +++ b/shopfloor_base/hooks.py @@ -1,9 +1,15 @@ # Copyright 2021 ACSONE SA/NV # License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html). + +import logging + from odoo import SUPERUSER_ID, api +_logger = logging.getLogger(__name__) + def post_init_hook(cr, registry): + _logger.info("Linking existing menu items to their scenario") env = api.Environment(cr, SUPERUSER_ID, {}) # Ensure scenario are linked to menu items env["shopfloor.menu"].with_context(active_test=False).search( From a8224df4ccc97449d82ed5d55785ab84e4bb65cd Mon Sep 17 00:00:00 2001 From: OCA-git-bot Date: Wed, 24 Feb 2021 11:01:40 +0000 Subject: [PATCH 023/111] [UPD] README.rst --- shopfloor_base/README.rst | 3 ++- shopfloor_base/static/description/index.html | 1 + 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/shopfloor_base/README.rst b/shopfloor_base/README.rst index e8e9c0cae3..e8f40436d1 100644 --- a/shopfloor_base/README.rst +++ b/shopfloor_base/README.rst @@ -80,7 +80,7 @@ Scenario In Shopfloor / Scenario. A Scenario represents a flow (or more basically "something to do" with the app. -Each scenario must have a name and a unique key. +Each scenario must have a name and a unique key. The key must match a registered shopfloor service component. Usage @@ -97,6 +97,7 @@ Known issues / Roadmap ====================== * improve documentation +* change shopfloor.scenario.key to selection? See comment in model Changelog ========= diff --git a/shopfloor_base/static/description/index.html b/shopfloor_base/static/description/index.html index d91ef0e767..c7fd954067 100644 --- a/shopfloor_base/static/description/index.html +++ b/shopfloor_base/static/description/index.html @@ -449,6 +449,7 @@

Usage

Known issues / Roadmap

  • improve documentation
  • +
  • change shopfloor.scenario.key to selection? See comment in model
From 729994b1357ac503e2c2a56775b3dfc7b91ef76b Mon Sep 17 00:00:00 2001 From: OCA-git-bot Date: Wed, 24 Feb 2021 11:01:41 +0000 Subject: [PATCH 024/111] [ADD] icon.png --- shopfloor_base/static/description/icon.png | Bin 0 -> 9455 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 shopfloor_base/static/description/icon.png diff --git a/shopfloor_base/static/description/icon.png b/shopfloor_base/static/description/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..3a0328b516c4980e8e44cdb63fd945757ddd132d GIT binary patch literal 9455 zcmW++2RxMjAAjx~&dlBk9S+%}OXg)AGE&Cb*&}d0jUxM@u(PQx^-s)697TX`ehR4?GS^qbkof1cslKgkU)h65qZ9Oc=ml_0temigYLJfnz{IDzUf>bGs4N!v3=Z3jMq&A#7%rM5eQ#dc?k~! zVpnB`o+K7|Al`Q_U;eD$B zfJtP*jH`siUq~{KE)`jP2|#TUEFGRryE2`i0**z#*^6~AI|YzIWy$Cu#CSLW3q=GA z6`?GZymC;dCPk~rBS%eCb`5OLr;RUZ;D`}um=H)BfVIq%7VhiMr)_#G0N#zrNH|__ zc+blN2UAB0=617@>_u;MPHN;P;N#YoE=)R#i$k_`UAA>WWCcEVMh~L_ zj--gtp&|K1#58Yz*AHCTMziU1Jzt_jG0I@qAOHsk$2}yTmVkBp_eHuY$A9)>P6o~I z%aQ?!(GqeQ-Y+b0I(m9pwgi(IIZZzsbMv+9w{PFtd_<_(LA~0H(xz{=FhLB@(1&qHA5EJw1>>=%q2f&^X>IQ{!GJ4e9U z&KlB)z(84HmNgm2hg2C0>WM{E(DdPr+EeU_N@57;PC2&DmGFW_9kP&%?X4}+xWi)( z;)z%wI5>D4a*5XwD)P--sPkoY(a~WBw;E~AW`Yue4kFa^LM3X`8x|}ZUeMnqr}>kH zG%WWW>3ml$Yez?i%)2pbKPI7?5o?hydokgQyZsNEr{a|mLdt;X2TX(#B1j35xPnPW z*bMSSOauW>o;*=kO8ojw91VX!qoOQb)zHJ!odWB}d+*K?#sY_jqPdg{Sm2HdYzdEx zOGVPhVRTGPtv0o}RfVP;Nd(|CB)I;*t&QO8h zFfekr30S!-LHmV_Su-W+rEwYXJ^;6&3|L$mMC8*bQptyOo9;>Qb9Q9`ySe3%V$A*9 zeKEe+b0{#KWGp$F+tga)0RtI)nhMa-K@JS}2krK~n8vJ=Ngm?R!9G<~RyuU0d?nz# z-5EK$o(!F?hmX*2Yt6+coY`6jGbb7tF#6nHA zuKk=GGJ;ZwON1iAfG$E#Y7MnZVmrY|j0eVI(DN_MNFJmyZ|;w4tf@=CCDZ#5N_0K= z$;R~bbk?}TpfDjfB&aiQ$VA}s?P}xPERJG{kxk5~R`iRS(SK5d+Xs9swCozZISbnS zk!)I0>t=A<-^z(cmSFz3=jZ23u13X><0b)P)^1T_))Kr`e!-pb#q&J*Q`p+B6la%C zuVl&0duN<;uOsB3%T9Fp8t{ED108<+W(nOZd?gDnfNBC3>M8WE61$So|P zVvqH0SNtDTcsUdzaMDpT=Ty0pDHHNL@Z0w$Y`XO z2M-_r1S+GaH%pz#Uy0*w$Vdl=X=rQXEzO}d6J^R6zjM1u&c9vYLvLp?W7w(?np9x1 zE_0JSAJCPB%i7p*Wvg)pn5T`8k3-uR?*NT|J`eS#_#54p>!p(mLDvmc-3o0mX*mp_ zN*AeS<>#^-{S%W<*mz^!X$w_2dHWpcJ6^j64qFBft-o}o_Vx80o0>}Du;>kLts;$8 zC`7q$QI(dKYG`Wa8#wl@V4jVWBRGQ@1dr-hstpQL)Tl+aqVpGpbSfN>5i&QMXfiZ> zaA?T1VGe?rpQ@;+pkrVdd{klI&jVS@I5_iz!=UMpTsa~mBga?1r}aRBm1WS;TT*s0f0lY=JBl66Upy)-k4J}lh=P^8(SXk~0xW=T9v*B|gzIhN z>qsO7dFd~mgxAy4V?&)=5ieYq?zi?ZEoj)&2o)RLy=@hbCRcfT5jigwtQGE{L*8<@Yd{zg;CsL5mvzfDY}P-wos_6PfprFVaeqNE%h zKZhLtcQld;ZD+>=nqN~>GvROfueSzJD&BE*}XfU|H&(FssBqY=hPCt`d zH?@s2>I(|;fcW&YM6#V#!kUIP8$Nkdh0A(bEVj``-AAyYgwY~jB zT|I7Bf@%;7aL7Wf4dZ%VqF$eiaC38OV6oy3Z#TER2G+fOCd9Iaoy6aLYbPTN{XRPz z;U!V|vBf%H!}52L2gH_+j;`bTcQRXB+y9onc^wLm5wi3-Be}U>k_u>2Eg$=k!(l@I zcCg+flakT2Nej3i0yn+g+}%NYb?ta;R?(g5SnwsQ49U8Wng8d|{B+lyRcEDvR3+`O{zfmrmvFrL6acVP%yG98X zo&+VBg@px@i)%o?dG(`T;n*$S5*rnyiR#=wW}}GsAcfyQpE|>a{=$Hjg=-*_K;UtD z#z-)AXwSRY?OPefw^iI+ z)AXz#PfEjlwTes|_{sB?4(O@fg0AJ^g8gP}ex9Ucf*@_^J(s_5jJV}c)s$`Myn|Kd z$6>}#q^n{4vN@+Os$m7KV+`}c%4)4pv@06af4-x5#wj!KKb%caK{A&Y#Rfs z-po?Dcb1({W=6FKIUirH&(yg=*6aLCekcKwyfK^JN5{wcA3nhO(o}SK#!CINhI`-I z1)6&n7O&ZmyFMuNwvEic#IiOAwNkR=u5it{B9n2sAJV5pNhar=j5`*N!Na;c7g!l$ z3aYBqUkqqTJ=Re-;)s!EOeij=7SQZ3Hq}ZRds%IM*PtM$wV z@;rlc*NRK7i3y5BETSKuumEN`Xu_8GP1Ri=OKQ$@I^ko8>H6)4rjiG5{VBM>B|%`&&s^)jS|-_95&yc=GqjNo{zFkw%%HHhS~e=s zD#sfS+-?*t|J!+ozP6KvtOl!R)@@-z24}`9{QaVLD^9VCSR2b`b!KC#o;Ki<+wXB6 zx3&O0LOWcg4&rv4QG0)4yb}7BFSEg~=IR5#ZRj8kg}dS7_V&^%#Do==#`u zpy6{ox?jWuR(;pg+f@mT>#HGWHAJRRDDDv~@(IDw&R>9643kK#HN`!1vBJHnC+RM&yIh8{gG2q zA%e*U3|N0XSRa~oX-3EAneep)@{h2vvd3Xvy$7og(sayr@95+e6~Xvi1tUqnIxoIH zVWo*OwYElb#uyW{Imam6f2rGbjR!Y3`#gPqkv57dB6K^wRGxc9B(t|aYDGS=m$&S!NmCtrMMaUg(c zc2qC=2Z`EEFMW-me5B)24AqF*bV5Dr-M5ig(l-WPS%CgaPzs6p_gnCIvTJ=Y<6!gT zVt@AfYCzjjsMEGi=rDQHo0yc;HqoRNnNFeWZgcm?f;cp(6CNylj36DoL(?TS7eU#+ z7&mfr#y))+CJOXQKUMZ7QIdS9@#-}7y2K1{8)cCt0~-X0O!O?Qx#E4Og+;A2SjalQ zs7r?qn0H044=sDN$SRG$arw~n=+T_DNdSrarmu)V6@|?1-ZB#hRn`uilTGPJ@fqEy zGt(f0B+^JDP&f=r{#Y_wi#AVDf-y!RIXU^0jXsFpf>=Ji*TeqSY!H~AMbJdCGLhC) zn7Rx+sXw6uYj;WRYrLd^5IZq@6JI1C^YkgnedZEYy<&4(z%Q$5yv#Boo{AH8n$a zhb4Y3PWdr269&?V%uI$xMcUrMzl=;w<_nm*qr=c3Rl@i5wWB;e-`t7D&c-mcQl7x! zZWB`UGcw=Y2=}~wzrfLx=uet<;m3~=8I~ZRuzvMQUQdr+yTV|ATf1Uuomr__nDf=X zZ3WYJtHp_ri(}SQAPjv+Y+0=fH4krOP@S&=zZ-t1jW1o@}z;xk8 z(Nz1co&El^HK^NrhVHa-_;&88vTU>_J33=%{if;BEY*J#1n59=07jrGQ#IP>@u#3A z;!q+E1Rj3ZJ+!4bq9F8PXJ@yMgZL;>&gYA0%_Kbi8?S=XGM~dnQZQ!yBSgcZhY96H zrWnU;k)qy`rX&&xlDyA%(a1Hhi5CWkmg(`Gb%m(HKi-7Z!LKGRP_B8@`7&hdDy5n= z`OIxqxiVfX@OX1p(mQu>0Ai*v_cTMiw4qRt3~NBvr9oBy0)r>w3p~V0SCm=An6@3n)>@z!|o-$HvDK z|3D2ZMJkLE5loMKl6R^ez@Zz%S$&mbeoqH5`Bb){Ei21q&VP)hWS2tjShfFtGE+$z zzCR$P#uktu+#!w)cX!lWN1XU%K-r=s{|j?)Akf@q#3b#{6cZCuJ~gCxuMXRmI$nGtnH+-h z+GEi!*X=AP<|fG`1>MBdTb?28JYc=fGvAi2I<$B(rs$;eoJCyR6_bc~p!XR@O-+sD z=eH`-ye})I5ic1eL~TDmtfJ|8`0VJ*Yr=hNCd)G1p2MMz4C3^Mj?7;!w|Ly%JqmuW zlIEW^Ft%z?*|fpXda>Jr^1noFZEwFgVV%|*XhH@acv8rdGxeEX{M$(vG{Zw+x(ei@ zmfXb22}8-?Fi`vo-YVrTH*C?a8%M=Hv9MqVH7H^J$KsD?>!SFZ;ZsvnHr_gn=7acz z#W?0eCdVhVMWN12VV^$>WlQ?f;P^{(&pYTops|btm6aj>_Uz+hqpGwB)vWp0Cf5y< zft8-je~nn?W11plq}N)4A{l8I7$!ks_x$PXW-2XaRFswX_BnF{R#6YIwMhAgd5F9X zGmwdadS6(a^fjHtXg8=l?Rc0Sm%hk6E9!5cLVloEy4eh(=FwgP`)~I^5~pBEWo+F6 zSf2ncyMurJN91#cJTy_u8Y}@%!bq1RkGC~-bV@SXRd4F{R-*V`bS+6;W5vZ(&+I<9$;-V|eNfLa5n-6% z2(}&uGRF;p92eS*sE*oR$@pexaqr*meB)VhmIg@h{uzkk$9~qh#cHhw#>O%)b@+(| z^IQgqzuj~Sk(J;swEM-3TrJAPCq9k^^^`q{IItKBRXYe}e0Tdr=Huf7da3$l4PdpwWDop%^}n;dD#K4s#DYA8SHZ z&1!riV4W4R7R#C))JH1~axJ)RYnM$$lIR%6fIVA@zV{XVyx}C+a-Dt8Y9M)^KU0+H zR4IUb2CJ{Hg>CuaXtD50jB(_Tcx=Z$^WYu2u5kubqmwp%drJ6 z?Fo40g!Qd<-l=TQxqHEOuPX0;^z7iX?Ke^a%XT<13TA^5`4Xcw6D@Ur&VT&CUe0d} z1GjOVF1^L@>O)l@?bD~$wzgf(nxX1OGD8fEV?TdJcZc2KoUe|oP1#=$$7ee|xbY)A zDZq+cuTpc(fFdj^=!;{k03C69lMQ(|>uhRfRu%+!k&YOi-3|1QKB z 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 Xviia!U7SGha1wx#SCgwmn*{w2TRX*I literal 0 HcmV?d00001 From d12610854043f908f946188aefad4a70e613711f Mon Sep 17 00:00:00 2001 From: oca-travis Date: Wed, 24 Feb 2021 11:25:06 +0000 Subject: [PATCH 025/111] [UPD] Update shopfloor_base.pot --- shopfloor_base/i18n/shopfloor_base.pot | 234 +++++++++++++++++++++++++ 1 file changed, 234 insertions(+) create mode 100644 shopfloor_base/i18n/shopfloor_base.pot diff --git a/shopfloor_base/i18n/shopfloor_base.pot b/shopfloor_base/i18n/shopfloor_base.pot new file mode 100644 index 0000000000..2f4e69b4ac --- /dev/null +++ b/shopfloor_base/i18n/shopfloor_base.pot @@ -0,0 +1,234 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * shopfloor_base +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 13.0\n" +"Report-Msgid-Bugs-To: \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: shopfloor_base +#: code:addons/shopfloor_base/services/forms/form_mixin.py:0 +#, python-format +msgid "%s updated." +msgstr "" + +#. module: shopfloor_base +#: model:ir.model.fields,field_description:shopfloor_base.field_shopfloor_menu__active +#: model:ir.model.fields,field_description:shopfloor_base.field_shopfloor_profile__active +msgid "Active" +msgstr "" + +#. module: shopfloor_base +#: model_terms:ir.ui.view,arch_db:shopfloor_base.shopfloor_menu_form_view +#: model_terms:ir.ui.view,arch_db:shopfloor_base.shopfloor_menu_search_view +#: model_terms:ir.ui.view,arch_db:shopfloor_base.shopfloor_profile_form_view +#: model_terms:ir.ui.view,arch_db:shopfloor_base.shopfloor_profile_search_view +msgid "Archived" +msgstr "" + +#. module: shopfloor_base +#: model:ir.model.fields,help:shopfloor_base.field_shopfloor_scenario__options_edit +msgid "Configure options via JSON" +msgstr "" + +#. module: shopfloor_base +#: model:ir.model.fields,field_description:shopfloor_base.field_shopfloor_menu__create_uid +#: model:ir.model.fields,field_description:shopfloor_base.field_shopfloor_profile__create_uid +#: model:ir.model.fields,field_description:shopfloor_base.field_shopfloor_scenario__create_uid +msgid "Created by" +msgstr "" + +#. module: shopfloor_base +#: model:ir.model.fields,field_description:shopfloor_base.field_shopfloor_menu__create_date +#: model:ir.model.fields,field_description:shopfloor_base.field_shopfloor_profile__create_date +#: model:ir.model.fields,field_description:shopfloor_base.field_shopfloor_scenario__create_date +msgid "Created on" +msgstr "" + +#. module: shopfloor_base +#: model:shopfloor.scenario,name:shopfloor_base.shopfloor_scenario_demo_1 +msgid "Demo scenario 1" +msgstr "" + +#. module: shopfloor_base +#: model:ir.model.fields,field_description:shopfloor_base.field_shopfloor_menu__display_name +#: model:ir.model.fields,field_description:shopfloor_base.field_shopfloor_profile__display_name +#: model:ir.model.fields,field_description:shopfloor_base.field_shopfloor_scenario__display_name +msgid "Display Name" +msgstr "" + +#. module: shopfloor_base +#: model_terms:ir.ui.view,arch_db:shopfloor_base.shopfloor_menu_search_view +msgid "Group By" +msgstr "" + +#. module: shopfloor_base +#: model:ir.model.fields,field_description:shopfloor_base.field_shopfloor_menu__id +#: model:ir.model.fields,field_description:shopfloor_base.field_shopfloor_profile__id +#: model:ir.model.fields,field_description:shopfloor_base.field_shopfloor_scenario__id +msgid "ID" +msgstr "" + +#. module: shopfloor_base +#: model:ir.model.fields,help:shopfloor_base.field_shopfloor_scenario__key +msgid "" +"Identify scenario univocally. This value must match a service component's " +"`usage`." +msgstr "" + +#. module: shopfloor_base +#: model:ir.model.fields,field_description:shopfloor_base.field_shopfloor_scenario__key +msgid "Key" +msgstr "" + +#. module: shopfloor_base +#: model:ir.model.fields,field_description:shopfloor_base.field_shopfloor_menu____last_update +#: model:ir.model.fields,field_description:shopfloor_base.field_shopfloor_profile____last_update +#: model:ir.model.fields,field_description:shopfloor_base.field_shopfloor_scenario____last_update +msgid "Last Modified on" +msgstr "" + +#. module: shopfloor_base +#: model:ir.model.fields,field_description:shopfloor_base.field_shopfloor_menu__write_uid +#: model:ir.model.fields,field_description:shopfloor_base.field_shopfloor_profile__write_uid +#: model:ir.model.fields,field_description:shopfloor_base.field_shopfloor_scenario__write_uid +msgid "Last Updated by" +msgstr "" + +#. module: shopfloor_base +#: model:ir.model.fields,field_description:shopfloor_base.field_shopfloor_menu__write_date +#: model:ir.model.fields,field_description:shopfloor_base.field_shopfloor_profile__write_date +#: model:ir.model.fields,field_description:shopfloor_base.field_shopfloor_scenario__write_date +msgid "Last Updated on" +msgstr "" + +#. module: shopfloor_base +#: model:ir.model.fields,field_description:shopfloor_base.field_shopfloor_menu__scenario +msgid "Legacy scenario field" +msgstr "" + +#. module: shopfloor_base +#: model_terms:ir.ui.view,arch_db:shopfloor_base.shopfloor_menu_form_view +msgid "Menu Options" +msgstr "" + +#. module: shopfloor_base +#: model:ir.model,name:shopfloor_base.model_shopfloor_menu +msgid "Menu displayed in the scanner application" +msgstr "" + +#. module: shopfloor_base +#: model:ir.actions.act_window,name:shopfloor_base.action_shopfloor_menu +#: model:ir.model.fields,field_description:shopfloor_base.field_shopfloor_profile__menu_ids +#: model:ir.ui.menu,name:shopfloor_base.menu_action_shopfloor_menu +msgid "Menus" +msgstr "" + +#. module: shopfloor_base +#: model:ir.model.fields,help:shopfloor_base.field_shopfloor_profile__menu_ids +msgid "Menus visible for this profile" +msgstr "" + +#. module: shopfloor_base +#: model:ir.model.fields,field_description:shopfloor_base.field_shopfloor_menu__name +#: model:ir.model.fields,field_description:shopfloor_base.field_shopfloor_profile__name +#: model:ir.model.fields,field_description:shopfloor_base.field_shopfloor_scenario__name +msgid "Name" +msgstr "" + +#. module: shopfloor_base +#: model:ir.model.fields,field_description:shopfloor_base.field_shopfloor_scenario__options +msgid "Options" +msgstr "" + +#. module: shopfloor_base +#: model:ir.model.fields,field_description:shopfloor_base.field_shopfloor_scenario__options_edit +msgid "Options Edit" +msgstr "" + +#. module: shopfloor_base +#: model:ir.model.fields,field_description:shopfloor_base.field_shopfloor_menu__profile_id +#: model_terms:ir.ui.view,arch_db:shopfloor_base.shopfloor_menu_search_view +msgid "Profile" +msgstr "" + +#. module: shopfloor_base +#: model:ir.actions.act_window,name:shopfloor_base.action_shopfloor_profile +#: model:ir.ui.menu,name:shopfloor_base.menu_action_shopfloor_profile +msgid "Profiles" +msgstr "" + +#. module: shopfloor_base +#: code:addons/shopfloor_base/services/scan_anything.py:0 +#, python-format +msgid "" +"Record not found.\n" +"We've tried with the following types: {}" +msgstr "" + +#. module: shopfloor_base +#: model:ir.actions.act_window,name:shopfloor_base.action_shopfloor_scenario +#: model:ir.model.fields,field_description:shopfloor_base.field_shopfloor_menu__scenario_id +#: model:ir.ui.menu,name:shopfloor_base.menu_action_shopfloor_scenario +#: model_terms:ir.ui.view,arch_db:shopfloor_base.shopfloor_menu_search_view +msgid "Scenario" +msgstr "" + +#. module: shopfloor_base +#: model:ir.model.constraint,message:shopfloor_base.constraint_shopfloor_scenario_key +msgid "Scenario key must be unique" +msgstr "" + +#. module: shopfloor_base +#: model:ir.model.fields,field_description:shopfloor_base.field_shopfloor_menu__sequence +msgid "Sequence" +msgstr "" + +#. module: shopfloor_base +#: model:ir.module.category,name:shopfloor_base.module_category_shopfloor +#: model:ir.ui.menu,name:shopfloor_base.menu_shopfloor_root +msgid "Shopfloor" +msgstr "" + +#. module: shopfloor_base +#: model:res.groups,name:shopfloor_base.group_shopfloor_manager +msgid "Shopfloor Manager" +msgstr "" + +#. module: shopfloor_base +#: model:ir.model,name:shopfloor_base.model_shopfloor_scenario +msgid "Shopfloor Scenario" +msgstr "" + +#. module: shopfloor_base +#: model:res.groups,name:shopfloor_base.group_shopfloor_user +msgid "Shopfloor User" +msgstr "" + +#. module: shopfloor_base +#: model:ir.model,name:shopfloor_base.model_shopfloor_profile +msgid "Shopfloor profile settings" +msgstr "" + +#. module: shopfloor_base +#: model:shopfloor.menu,name:shopfloor_base.shopfloor_menu_demo_1 +msgid "Simple Thing To Do" +msgstr "" + +#. module: shopfloor_base +#: code:addons/shopfloor_base/services/service.py:0 +#, python-format +msgid "The record %s %s does not exist" +msgstr "" + +#. module: shopfloor_base +#: model:ir.model.fields,help:shopfloor_base.field_shopfloor_menu__profile_id +msgid "Visible on this profile only" +msgstr "" From 387e9883c3646f767b7b9ae214764c99e5ad8ad5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Alix?= Date: Thu, 25 Feb 2021 16:10:31 +0100 Subject: [PATCH 026/111] shopfloor_base: use partner.display_name --- shopfloor_base/actions/data.py | 2 +- shopfloor_base/tests/test_actions_data.py | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/shopfloor_base/actions/data.py b/shopfloor_base/actions/data.py index 8b02ff0bd6..69180746dd 100644 --- a/shopfloor_base/actions/data.py +++ b/shopfloor_base/actions/data.py @@ -37,4 +37,4 @@ def partners(self, record, **kw): @property def _partner_parser(self): - return self._simple_record_parser() + return ["id", "display_name:name"] diff --git a/shopfloor_base/tests/test_actions_data.py b/shopfloor_base/tests/test_actions_data.py index c30ffce908..9efdf16e9a 100644 --- a/shopfloor_base/tests/test_actions_data.py +++ b/shopfloor_base/tests/test_actions_data.py @@ -13,4 +13,6 @@ def setUpClassBaseData(cls): def test_data_partner(self): data = self.data.partner(self.partner) self.assert_schema(self.schema._simple_record(), data) - self.assertDictEqual(data, {"id": self.partner.id, "name": self.partner.name}) + self.assertDictEqual( + data, {"id": self.partner.id, "name": self.partner.display_name} + ) From a66a7f4b9b888ceaebbb3970916de96ca8bf1a49 Mon Sep 17 00:00:00 2001 From: OCA-git-bot Date: Mon, 1 Mar 2021 15:20:12 +0000 Subject: [PATCH 027/111] shopfloor_base 13.0.1.1.0 --- shopfloor_base/__manifest__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/shopfloor_base/__manifest__.py b/shopfloor_base/__manifest__.py index 1e89ebb6f5..bc75679d9c 100644 --- a/shopfloor_base/__manifest__.py +++ b/shopfloor_base/__manifest__.py @@ -6,7 +6,7 @@ { "name": "Shopfloor Base", "summary": "Core module for creating mobile apps", - "version": "13.0.1.0.0", + "version": "13.0.1.1.0", "development_status": "Alpha", "category": "Inventory", "website": "https://github.com/OCA/wms", From f964186e3d843dbf17df3033ec27fa5fa958f194 Mon Sep 17 00:00:00 2001 From: hparfr Date: Fri, 5 Mar 2021 15:47:05 +0100 Subject: [PATCH 028/111] [MIG] shopfloor_base: Migration to 14.0 --- shopfloor_base/__manifest__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/shopfloor_base/__manifest__.py b/shopfloor_base/__manifest__.py index bc75679d9c..2df6dc6854 100644 --- a/shopfloor_base/__manifest__.py +++ b/shopfloor_base/__manifest__.py @@ -6,7 +6,7 @@ { "name": "Shopfloor Base", "summary": "Core module for creating mobile apps", - "version": "13.0.1.1.0", + "version": "14.0.1.0.0", "development_status": "Alpha", "category": "Inventory", "website": "https://github.com/OCA/wms", From 17f050ce0ebec97d0ce41986e64605cff0701825 Mon Sep 17 00:00:00 2001 From: hparfr Date: Fri, 5 Mar 2021 15:49:30 +0100 Subject: [PATCH 029/111] [IMP] shopfloor_base: black, isort, prettier --- shopfloor_base/actions/data_detail.py | 3 +-- shopfloor_base/actions/schema_detail.py | 3 +-- shopfloor_base/services/forms/form_mixin.py | 3 +-- shopfloor_base/services/scan_anything.py | 18 ++++++------------ shopfloor_base/tests/common_misc.py | 3 ++- shopfloor_base/tests/test_user.py | 3 ++- 6 files changed, 13 insertions(+), 20 deletions(-) diff --git a/shopfloor_base/actions/data_detail.py b/shopfloor_base/actions/data_detail.py index 6b764dd26b..a02fbd10b7 100644 --- a/shopfloor_base/actions/data_detail.py +++ b/shopfloor_base/actions/data_detail.py @@ -5,8 +5,7 @@ class DataDetailAction(Component): - """Provide extra data on top of data action. - """ + """Provide extra data on top of data action.""" _name = "shopfloor.data.detail.action" _inherit = "shopfloor.data.action" diff --git a/shopfloor_base/actions/schema_detail.py b/shopfloor_base/actions/schema_detail.py index b1afb2c29a..905ba6cc1e 100644 --- a/shopfloor_base/actions/schema_detail.py +++ b/shopfloor_base/actions/schema_detail.py @@ -4,8 +4,7 @@ class SchemaDetailAction(Component): - """Provide advanced details. - """ + """Provide advanced details.""" _inherit = "shopfloor.schema.action" _name = "shopfloor.schema.detail.action" diff --git a/shopfloor_base/services/forms/form_mixin.py b/shopfloor_base/services/forms/form_mixin.py index 11592a3cc7..559739a5fd 100644 --- a/shopfloor_base/services/forms/form_mixin.py +++ b/shopfloor_base/services/forms/form_mixin.py @@ -7,8 +7,7 @@ class ShopfloorFormMixin(AbstractComponent): - """Allow to edit records. - """ + """Allow to edit records.""" _inherit = "base.shopfloor.service" _name = "shopfloor.form.mixin" diff --git a/shopfloor_base/services/scan_anything.py b/shopfloor_base/services/scan_anything.py index 978f1fd6ce..10e7767542 100644 --- a/shopfloor_base/services/scan_anything.py +++ b/shopfloor_base/services/scan_anything.py @@ -66,14 +66,12 @@ def _response_for_not_found(self, tried): return self._response(message=message) def _scan_handlers(self): - """Return components to handle scan requests. - """ + """Return components to handle scan requests.""" return self.many_components(usage="scan_anything.handler") class ShopfloorScanAnythingHandler(AbstractComponent): - """Handle record search for ScanAnything service. - """ + """Handle record search for ScanAnything service.""" _name = "shopfloor.scan.anything.handler" _usage = "scan_anything.handler" @@ -103,25 +101,21 @@ def _search(self): @property def record_type(self): - """Return unique record type for this handler - """ + """Return unique record type for this handler""" raise NotImplementedError() def search(self, identifier): - """Find and return Odoo record. - """ + """Find and return Odoo record.""" raise NotImplementedError() @property def converter(self): - """Return data converter to json. - """ + """Return data converter to json.""" raise NotImplementedError() @property def schema(self): - """Return schema to validate record converter. - """ + """Return schema to validate record converter.""" raise NotImplementedError() diff --git a/shopfloor_base/tests/common_misc.py b/shopfloor_base/tests/common_misc.py index 12e5a8686d..72b0977073 100644 --- a/shopfloor_base/tests/common_misc.py +++ b/shopfloor_base/tests/common_misc.py @@ -60,7 +60,8 @@ def _test_response_ok(self, rec_type, data, identifier, record_types=None): params = {"identifier": identifier, "record_types": record_types} response = service.dispatch("scan", params=params) self.assert_response( - response, data={"type": rec_type, "identifier": identifier, "record": data}, + response, + data={"type": rec_type, "identifier": identifier, "record": data}, ) def _test_response_ko(self, identifier, record_types=None): diff --git a/shopfloor_base/tests/test_user.py b/shopfloor_base/tests/test_user.py index 35ca2c6372..5f63b3599e 100644 --- a/shopfloor_base/tests/test_user.py +++ b/shopfloor_base/tests/test_user.py @@ -36,7 +36,8 @@ def test_menu_by_profile(self): response = self.service.dispatch("menu") self.assert_response( - response, data={"menus": [self._data_for_menu_item(menu)]}, + response, + data={"menus": [self._data_for_menu_item(menu)]}, ) def test_user_info(self): From 17dbf105eff61ee6e241d334557ac98b404e80a3 Mon Sep 17 00:00:00 2001 From: OCA-git-bot Date: Fri, 5 Mar 2021 15:37:46 +0000 Subject: [PATCH 030/111] [UPD] README.rst --- shopfloor_base/README.rst | 10 +++++----- shopfloor_base/static/description/index.html | 6 +++--- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/shopfloor_base/README.rst b/shopfloor_base/README.rst index e8f40436d1..b7d430801d 100644 --- a/shopfloor_base/README.rst +++ b/shopfloor_base/README.rst @@ -14,13 +14,13 @@ Shopfloor Base :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/13.0/shopfloor_base + :target: https://github.com/OCA/wms/tree/14.0/shopfloor_base :alt: OCA/wms .. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png - :target: https://translation.odoo-community.org/projects/wms-13-0/wms-13-0-shopfloor_base + :target: https://translation.odoo-community.org/projects/wms-14-0/wms-14-0-shopfloor_base :alt: Translate me on Weblate .. |badge5| image:: https://img.shields.io/badge/runbot-Try%20me-875A7B.png - :target: https://runbot.odoo-community.org/runbot/285/13.0 + :target: https://runbot.odoo-community.org/runbot/285/14.0 :alt: Try me on Runbot |badge1| |badge2| |badge3| |badge4| |badge5| @@ -113,7 +113,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 -`feedback `_. +`feedback `_. Do not contact contributors directly about support or help with technical issues. @@ -179,6 +179,6 @@ Current `maintainers `__: |maintainer-guewen| |maintainer-simahawk| |maintainer-sebalix| -This module is part of the `OCA/wms `_ project on GitHub. +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_base/static/description/index.html b/shopfloor_base/static/description/index.html index c7fd954067..e8b66de109 100644 --- a/shopfloor_base/static/description/index.html +++ b/shopfloor_base/static/description/index.html @@ -367,7 +367,7 @@

Shopfloor Base

!! This file is generated by oca-gen-addon-readme !! !! changes will be overwritten. !! !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! --> -

Alpha License: LGPL-3 OCA/wms Translate me on Weblate Try me on Runbot

+

Alpha License: LGPL-3 OCA/wms Translate me on Weblate Try me on Runbot

Shopfloor is a barcode scanner application.

This module provides REST APIs to support scenario. It needs a frontend to consume the backend APIs and provide screens for users on barcode devices. @@ -464,7 +464,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 -feedback.

+feedback.

Do not contact contributors directly about support or help with technical issues.

@@ -513,7 +513,7 @@

Maintainers

promote its widespread use.

Current maintainers:

guewen simahawk sebalix

-

This module is part of the OCA/wms project on GitHub.

+

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.

From f8ee12287db3ceee39b28916aa99ab7a84ade913 Mon Sep 17 00:00:00 2001 From: Ignacio Buioli Date: Wed, 31 Mar 2021 02:57:15 +0000 Subject: [PATCH 031/111] Added translation using Weblate (Spanish (Argentina)) --- shopfloor_base/i18n/es_AR.po | 235 +++++++++++++++++++++++++++++++++++ 1 file changed, 235 insertions(+) create mode 100644 shopfloor_base/i18n/es_AR.po diff --git a/shopfloor_base/i18n/es_AR.po b/shopfloor_base/i18n/es_AR.po new file mode 100644 index 0000000000..1b8ef60428 --- /dev/null +++ b/shopfloor_base/i18n/es_AR.po @@ -0,0 +1,235 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * shopfloor_base +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 13.0\n" +"Report-Msgid-Bugs-To: \n" +"Last-Translator: Automatically generated\n" +"Language-Team: none\n" +"Language: es_AR\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Plural-Forms: nplurals=2; plural=n != 1;\n" + +#. module: shopfloor_base +#: code:addons/shopfloor_base/services/forms/form_mixin.py:0 +#, python-format +msgid "%s updated." +msgstr "" + +#. module: shopfloor_base +#: model:ir.model.fields,field_description:shopfloor_base.field_shopfloor_menu__active +#: model:ir.model.fields,field_description:shopfloor_base.field_shopfloor_profile__active +msgid "Active" +msgstr "" + +#. module: shopfloor_base +#: model_terms:ir.ui.view,arch_db:shopfloor_base.shopfloor_menu_form_view +#: model_terms:ir.ui.view,arch_db:shopfloor_base.shopfloor_menu_search_view +#: model_terms:ir.ui.view,arch_db:shopfloor_base.shopfloor_profile_form_view +#: model_terms:ir.ui.view,arch_db:shopfloor_base.shopfloor_profile_search_view +msgid "Archived" +msgstr "" + +#. module: shopfloor_base +#: model:ir.model.fields,help:shopfloor_base.field_shopfloor_scenario__options_edit +msgid "Configure options via JSON" +msgstr "" + +#. module: shopfloor_base +#: model:ir.model.fields,field_description:shopfloor_base.field_shopfloor_menu__create_uid +#: model:ir.model.fields,field_description:shopfloor_base.field_shopfloor_profile__create_uid +#: model:ir.model.fields,field_description:shopfloor_base.field_shopfloor_scenario__create_uid +msgid "Created by" +msgstr "" + +#. module: shopfloor_base +#: model:ir.model.fields,field_description:shopfloor_base.field_shopfloor_menu__create_date +#: model:ir.model.fields,field_description:shopfloor_base.field_shopfloor_profile__create_date +#: model:ir.model.fields,field_description:shopfloor_base.field_shopfloor_scenario__create_date +msgid "Created on" +msgstr "" + +#. module: shopfloor_base +#: model:shopfloor.scenario,name:shopfloor_base.shopfloor_scenario_demo_1 +msgid "Demo scenario 1" +msgstr "" + +#. module: shopfloor_base +#: model:ir.model.fields,field_description:shopfloor_base.field_shopfloor_menu__display_name +#: model:ir.model.fields,field_description:shopfloor_base.field_shopfloor_profile__display_name +#: model:ir.model.fields,field_description:shopfloor_base.field_shopfloor_scenario__display_name +msgid "Display Name" +msgstr "" + +#. module: shopfloor_base +#: model_terms:ir.ui.view,arch_db:shopfloor_base.shopfloor_menu_search_view +msgid "Group By" +msgstr "" + +#. module: shopfloor_base +#: model:ir.model.fields,field_description:shopfloor_base.field_shopfloor_menu__id +#: model:ir.model.fields,field_description:shopfloor_base.field_shopfloor_profile__id +#: model:ir.model.fields,field_description:shopfloor_base.field_shopfloor_scenario__id +msgid "ID" +msgstr "" + +#. module: shopfloor_base +#: model:ir.model.fields,help:shopfloor_base.field_shopfloor_scenario__key +msgid "" +"Identify scenario univocally. This value must match a service component's " +"`usage`." +msgstr "" + +#. module: shopfloor_base +#: model:ir.model.fields,field_description:shopfloor_base.field_shopfloor_scenario__key +msgid "Key" +msgstr "" + +#. module: shopfloor_base +#: model:ir.model.fields,field_description:shopfloor_base.field_shopfloor_menu____last_update +#: model:ir.model.fields,field_description:shopfloor_base.field_shopfloor_profile____last_update +#: model:ir.model.fields,field_description:shopfloor_base.field_shopfloor_scenario____last_update +msgid "Last Modified on" +msgstr "" + +#. module: shopfloor_base +#: model:ir.model.fields,field_description:shopfloor_base.field_shopfloor_menu__write_uid +#: model:ir.model.fields,field_description:shopfloor_base.field_shopfloor_profile__write_uid +#: model:ir.model.fields,field_description:shopfloor_base.field_shopfloor_scenario__write_uid +msgid "Last Updated by" +msgstr "" + +#. module: shopfloor_base +#: model:ir.model.fields,field_description:shopfloor_base.field_shopfloor_menu__write_date +#: model:ir.model.fields,field_description:shopfloor_base.field_shopfloor_profile__write_date +#: model:ir.model.fields,field_description:shopfloor_base.field_shopfloor_scenario__write_date +msgid "Last Updated on" +msgstr "" + +#. module: shopfloor_base +#: model:ir.model.fields,field_description:shopfloor_base.field_shopfloor_menu__scenario +msgid "Legacy scenario field" +msgstr "" + +#. module: shopfloor_base +#: model_terms:ir.ui.view,arch_db:shopfloor_base.shopfloor_menu_form_view +msgid "Menu Options" +msgstr "" + +#. module: shopfloor_base +#: model:ir.model,name:shopfloor_base.model_shopfloor_menu +msgid "Menu displayed in the scanner application" +msgstr "" + +#. module: shopfloor_base +#: model:ir.actions.act_window,name:shopfloor_base.action_shopfloor_menu +#: model:ir.model.fields,field_description:shopfloor_base.field_shopfloor_profile__menu_ids +#: model:ir.ui.menu,name:shopfloor_base.menu_action_shopfloor_menu +msgid "Menus" +msgstr "" + +#. module: shopfloor_base +#: model:ir.model.fields,help:shopfloor_base.field_shopfloor_profile__menu_ids +msgid "Menus visible for this profile" +msgstr "" + +#. module: shopfloor_base +#: model:ir.model.fields,field_description:shopfloor_base.field_shopfloor_menu__name +#: model:ir.model.fields,field_description:shopfloor_base.field_shopfloor_profile__name +#: model:ir.model.fields,field_description:shopfloor_base.field_shopfloor_scenario__name +msgid "Name" +msgstr "" + +#. module: shopfloor_base +#: model:ir.model.fields,field_description:shopfloor_base.field_shopfloor_scenario__options +msgid "Options" +msgstr "" + +#. module: shopfloor_base +#: model:ir.model.fields,field_description:shopfloor_base.field_shopfloor_scenario__options_edit +msgid "Options Edit" +msgstr "" + +#. module: shopfloor_base +#: model:ir.model.fields,field_description:shopfloor_base.field_shopfloor_menu__profile_id +#: model_terms:ir.ui.view,arch_db:shopfloor_base.shopfloor_menu_search_view +msgid "Profile" +msgstr "" + +#. module: shopfloor_base +#: model:ir.actions.act_window,name:shopfloor_base.action_shopfloor_profile +#: model:ir.ui.menu,name:shopfloor_base.menu_action_shopfloor_profile +msgid "Profiles" +msgstr "" + +#. module: shopfloor_base +#: code:addons/shopfloor_base/services/scan_anything.py:0 +#, python-format +msgid "" +"Record not found.\n" +"We've tried with the following types: {}" +msgstr "" + +#. module: shopfloor_base +#: model:ir.actions.act_window,name:shopfloor_base.action_shopfloor_scenario +#: model:ir.model.fields,field_description:shopfloor_base.field_shopfloor_menu__scenario_id +#: model:ir.ui.menu,name:shopfloor_base.menu_action_shopfloor_scenario +#: model_terms:ir.ui.view,arch_db:shopfloor_base.shopfloor_menu_search_view +msgid "Scenario" +msgstr "" + +#. module: shopfloor_base +#: model:ir.model.constraint,message:shopfloor_base.constraint_shopfloor_scenario_key +msgid "Scenario key must be unique" +msgstr "" + +#. module: shopfloor_base +#: model:ir.model.fields,field_description:shopfloor_base.field_shopfloor_menu__sequence +msgid "Sequence" +msgstr "" + +#. module: shopfloor_base +#: model:ir.module.category,name:shopfloor_base.module_category_shopfloor +#: model:ir.ui.menu,name:shopfloor_base.menu_shopfloor_root +msgid "Shopfloor" +msgstr "" + +#. module: shopfloor_base +#: model:res.groups,name:shopfloor_base.group_shopfloor_manager +msgid "Shopfloor Manager" +msgstr "" + +#. module: shopfloor_base +#: model:ir.model,name:shopfloor_base.model_shopfloor_scenario +msgid "Shopfloor Scenario" +msgstr "" + +#. module: shopfloor_base +#: model:res.groups,name:shopfloor_base.group_shopfloor_user +msgid "Shopfloor User" +msgstr "" + +#. module: shopfloor_base +#: model:ir.model,name:shopfloor_base.model_shopfloor_profile +msgid "Shopfloor profile settings" +msgstr "" + +#. module: shopfloor_base +#: model:shopfloor.menu,name:shopfloor_base.shopfloor_menu_demo_1 +msgid "Simple Thing To Do" +msgstr "" + +#. module: shopfloor_base +#: code:addons/shopfloor_base/services/service.py:0 +#, python-format +msgid "The record %s %s does not exist" +msgstr "" + +#. module: shopfloor_base +#: model:ir.model.fields,help:shopfloor_base.field_shopfloor_menu__profile_id +msgid "Visible on this profile only" +msgstr "" From 2b62d6851037f0a664342b5f3ced6dd3f6d2b130 Mon Sep 17 00:00:00 2001 From: Ignacio Buioli Date: Wed, 31 Mar 2021 03:01:38 +0000 Subject: [PATCH 032/111] Translated using Weblate (Spanish (Argentina)) Currently translated at 100.0% (37 of 37 strings) Translation: wms-14.0/wms-14.0-shopfloor_base Translate-URL: https://translation.odoo-community.org/projects/wms-14-0/wms-14-0-shopfloor_base/es_AR/ --- shopfloor_base/i18n/es_AR.po | 78 +++++++++++++++++++----------------- 1 file changed, 42 insertions(+), 36 deletions(-) diff --git a/shopfloor_base/i18n/es_AR.po b/shopfloor_base/i18n/es_AR.po index 1b8ef60428..d167422a67 100644 --- a/shopfloor_base/i18n/es_AR.po +++ b/shopfloor_base/i18n/es_AR.po @@ -6,25 +6,27 @@ msgid "" msgstr "" "Project-Id-Version: Odoo Server 13.0\n" "Report-Msgid-Bugs-To: \n" -"Last-Translator: Automatically generated\n" +"PO-Revision-Date: 2021-03-31 05:46+0000\n" +"Last-Translator: Ignacio Buioli \n" "Language-Team: none\n" "Language: es_AR\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: \n" "Plural-Forms: nplurals=2; plural=n != 1;\n" +"X-Generator: Weblate 4.3.2\n" #. module: shopfloor_base #: code:addons/shopfloor_base/services/forms/form_mixin.py:0 #, python-format msgid "%s updated." -msgstr "" +msgstr "%s actualizado." #. module: shopfloor_base #: model:ir.model.fields,field_description:shopfloor_base.field_shopfloor_menu__active #: model:ir.model.fields,field_description:shopfloor_base.field_shopfloor_profile__active msgid "Active" -msgstr "" +msgstr "Activo" #. module: shopfloor_base #: model_terms:ir.ui.view,arch_db:shopfloor_base.shopfloor_menu_form_view @@ -32,50 +34,50 @@ msgstr "" #: model_terms:ir.ui.view,arch_db:shopfloor_base.shopfloor_profile_form_view #: model_terms:ir.ui.view,arch_db:shopfloor_base.shopfloor_profile_search_view msgid "Archived" -msgstr "" +msgstr "Archivado" #. module: shopfloor_base #: model:ir.model.fields,help:shopfloor_base.field_shopfloor_scenario__options_edit msgid "Configure options via JSON" -msgstr "" +msgstr "Configurar opciones vía JSON" #. module: shopfloor_base #: model:ir.model.fields,field_description:shopfloor_base.field_shopfloor_menu__create_uid #: model:ir.model.fields,field_description:shopfloor_base.field_shopfloor_profile__create_uid #: model:ir.model.fields,field_description:shopfloor_base.field_shopfloor_scenario__create_uid msgid "Created by" -msgstr "" +msgstr "Creado por" #. module: shopfloor_base #: model:ir.model.fields,field_description:shopfloor_base.field_shopfloor_menu__create_date #: model:ir.model.fields,field_description:shopfloor_base.field_shopfloor_profile__create_date #: model:ir.model.fields,field_description:shopfloor_base.field_shopfloor_scenario__create_date msgid "Created on" -msgstr "" +msgstr "Creado el" #. module: shopfloor_base #: model:shopfloor.scenario,name:shopfloor_base.shopfloor_scenario_demo_1 msgid "Demo scenario 1" -msgstr "" +msgstr "Escenario de demostración 1" #. module: shopfloor_base #: model:ir.model.fields,field_description:shopfloor_base.field_shopfloor_menu__display_name #: model:ir.model.fields,field_description:shopfloor_base.field_shopfloor_profile__display_name #: model:ir.model.fields,field_description:shopfloor_base.field_shopfloor_scenario__display_name msgid "Display Name" -msgstr "" +msgstr "Mostrar Nombre" #. module: shopfloor_base #: model_terms:ir.ui.view,arch_db:shopfloor_base.shopfloor_menu_search_view msgid "Group By" -msgstr "" +msgstr "Agrupar por" #. module: shopfloor_base #: model:ir.model.fields,field_description:shopfloor_base.field_shopfloor_menu__id #: model:ir.model.fields,field_description:shopfloor_base.field_shopfloor_profile__id #: model:ir.model.fields,field_description:shopfloor_base.field_shopfloor_scenario__id msgid "ID" -msgstr "" +msgstr "ID" #. module: shopfloor_base #: model:ir.model.fields,help:shopfloor_base.field_shopfloor_scenario__key @@ -83,88 +85,90 @@ msgid "" "Identify scenario univocally. This value must match a service component's " "`usage`." msgstr "" +"Identifique el escenario de manera unívoca. Este valor debe coincidir con el " +"\"uso\" de un componente de servicio." #. module: shopfloor_base #: model:ir.model.fields,field_description:shopfloor_base.field_shopfloor_scenario__key msgid "Key" -msgstr "" +msgstr "Clave" #. module: shopfloor_base #: model:ir.model.fields,field_description:shopfloor_base.field_shopfloor_menu____last_update #: model:ir.model.fields,field_description:shopfloor_base.field_shopfloor_profile____last_update #: model:ir.model.fields,field_description:shopfloor_base.field_shopfloor_scenario____last_update msgid "Last Modified on" -msgstr "" +msgstr "Última Modificación el" #. module: shopfloor_base #: model:ir.model.fields,field_description:shopfloor_base.field_shopfloor_menu__write_uid #: model:ir.model.fields,field_description:shopfloor_base.field_shopfloor_profile__write_uid #: model:ir.model.fields,field_description:shopfloor_base.field_shopfloor_scenario__write_uid msgid "Last Updated by" -msgstr "" +msgstr "Última Actualización por" #. module: shopfloor_base #: model:ir.model.fields,field_description:shopfloor_base.field_shopfloor_menu__write_date #: model:ir.model.fields,field_description:shopfloor_base.field_shopfloor_profile__write_date #: model:ir.model.fields,field_description:shopfloor_base.field_shopfloor_scenario__write_date msgid "Last Updated on" -msgstr "" +msgstr "Última Actualización el" #. module: shopfloor_base #: model:ir.model.fields,field_description:shopfloor_base.field_shopfloor_menu__scenario msgid "Legacy scenario field" -msgstr "" +msgstr "Campo de escenario heredado" #. module: shopfloor_base #: model_terms:ir.ui.view,arch_db:shopfloor_base.shopfloor_menu_form_view msgid "Menu Options" -msgstr "" +msgstr "Opciones del Menú" #. module: shopfloor_base #: model:ir.model,name:shopfloor_base.model_shopfloor_menu msgid "Menu displayed in the scanner application" -msgstr "" +msgstr "Menú mostrado en la aplicación de escaner" #. module: shopfloor_base #: model:ir.actions.act_window,name:shopfloor_base.action_shopfloor_menu #: model:ir.model.fields,field_description:shopfloor_base.field_shopfloor_profile__menu_ids #: model:ir.ui.menu,name:shopfloor_base.menu_action_shopfloor_menu msgid "Menus" -msgstr "" +msgstr "Menús" #. module: shopfloor_base #: model:ir.model.fields,help:shopfloor_base.field_shopfloor_profile__menu_ids msgid "Menus visible for this profile" -msgstr "" +msgstr "Menús visibles para este perfil" #. module: shopfloor_base #: model:ir.model.fields,field_description:shopfloor_base.field_shopfloor_menu__name #: model:ir.model.fields,field_description:shopfloor_base.field_shopfloor_profile__name #: model:ir.model.fields,field_description:shopfloor_base.field_shopfloor_scenario__name msgid "Name" -msgstr "" +msgstr "Nombre" #. module: shopfloor_base #: model:ir.model.fields,field_description:shopfloor_base.field_shopfloor_scenario__options msgid "Options" -msgstr "" +msgstr "Opciones" #. module: shopfloor_base #: model:ir.model.fields,field_description:shopfloor_base.field_shopfloor_scenario__options_edit msgid "Options Edit" -msgstr "" +msgstr "Editar Opciones" #. module: shopfloor_base #: model:ir.model.fields,field_description:shopfloor_base.field_shopfloor_menu__profile_id #: model_terms:ir.ui.view,arch_db:shopfloor_base.shopfloor_menu_search_view msgid "Profile" -msgstr "" +msgstr "Perfil" #. module: shopfloor_base #: model:ir.actions.act_window,name:shopfloor_base.action_shopfloor_profile #: model:ir.ui.menu,name:shopfloor_base.menu_action_shopfloor_profile msgid "Profiles" -msgstr "" +msgstr "Perfiles" #. module: shopfloor_base #: code:addons/shopfloor_base/services/scan_anything.py:0 @@ -173,6 +177,8 @@ msgid "" "Record not found.\n" "We've tried with the following types: {}" msgstr "" +"Registro no encontrado.\n" +"Hemos tratado con los siguientes tipos: {}" #. module: shopfloor_base #: model:ir.actions.act_window,name:shopfloor_base.action_shopfloor_scenario @@ -180,56 +186,56 @@ msgstr "" #: model:ir.ui.menu,name:shopfloor_base.menu_action_shopfloor_scenario #: model_terms:ir.ui.view,arch_db:shopfloor_base.shopfloor_menu_search_view msgid "Scenario" -msgstr "" +msgstr "Escenario" #. module: shopfloor_base #: model:ir.model.constraint,message:shopfloor_base.constraint_shopfloor_scenario_key msgid "Scenario key must be unique" -msgstr "" +msgstr "Clave de Escenario debe ser única" #. module: shopfloor_base #: model:ir.model.fields,field_description:shopfloor_base.field_shopfloor_menu__sequence msgid "Sequence" -msgstr "" +msgstr "Secuencia" #. module: shopfloor_base #: model:ir.module.category,name:shopfloor_base.module_category_shopfloor #: model:ir.ui.menu,name:shopfloor_base.menu_shopfloor_root msgid "Shopfloor" -msgstr "" +msgstr "Taller" #. module: shopfloor_base #: model:res.groups,name:shopfloor_base.group_shopfloor_manager msgid "Shopfloor Manager" -msgstr "" +msgstr "Gerente del Taller" #. module: shopfloor_base #: model:ir.model,name:shopfloor_base.model_shopfloor_scenario msgid "Shopfloor Scenario" -msgstr "" +msgstr "Escenario del Taller" #. module: shopfloor_base #: model:res.groups,name:shopfloor_base.group_shopfloor_user msgid "Shopfloor User" -msgstr "" +msgstr "Usuario del Taller" #. module: shopfloor_base #: model:ir.model,name:shopfloor_base.model_shopfloor_profile msgid "Shopfloor profile settings" -msgstr "" +msgstr "Ajustes del perfil de taller" #. module: shopfloor_base #: model:shopfloor.menu,name:shopfloor_base.shopfloor_menu_demo_1 msgid "Simple Thing To Do" -msgstr "" +msgstr "Cosas Simples Para Hacer" #. module: shopfloor_base #: code:addons/shopfloor_base/services/service.py:0 #, python-format msgid "The record %s %s does not exist" -msgstr "" +msgstr "El registro %s %s no existe" #. module: shopfloor_base #: model:ir.model.fields,help:shopfloor_base.field_shopfloor_menu__profile_id msgid "Visible on this profile only" -msgstr "" +msgstr "Visible solo en este perfil" From bcb61b348e0be5a7067b5b607447c21fe6a87773 Mon Sep 17 00:00:00 2001 From: Simone Orsi Date: Fri, 26 Feb 2021 16:06:26 +0100 Subject: [PATCH 033/111] shopfloor_base: give demo user shopfloor permission --- shopfloor_base/__manifest__.py | 1 + shopfloor_base/demo/res_users_demo.xml | 8 ++++++++ 2 files changed, 9 insertions(+) create mode 100644 shopfloor_base/demo/res_users_demo.xml diff --git a/shopfloor_base/__manifest__.py b/shopfloor_base/__manifest__.py index 2df6dc6854..5e75c581b1 100644 --- a/shopfloor_base/__manifest__.py +++ b/shopfloor_base/__manifest__.py @@ -25,6 +25,7 @@ "views/menus.xml", ], "demo": [ + "demo/res_users_demo.xml", "demo/auth_api_key_demo.xml", "demo/shopfloor_scenario_demo.xml", "demo/shopfloor_menu_demo.xml", diff --git a/shopfloor_base/demo/res_users_demo.xml b/shopfloor_base/demo/res_users_demo.xml new file mode 100644 index 0000000000..3e2e461b3d --- /dev/null +++ b/shopfloor_base/demo/res_users_demo.xml @@ -0,0 +1,8 @@ + + + + + From 311a2f1b5ceacd3bbc5f7d94c0aa82c1c80822e9 Mon Sep 17 00:00:00 2001 From: Simone Orsi Date: Fri, 26 Feb 2021 16:07:12 +0100 Subject: [PATCH 034/111] shopfloor_base: add generic msg for rec not found --- shopfloor_base/actions/message.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/shopfloor_base/actions/message.py b/shopfloor_base/actions/message.py index 96cea7f2c5..4c8e111772 100644 --- a/shopfloor_base/actions/message.py +++ b/shopfloor_base/actions/message.py @@ -1,6 +1,6 @@ # Copyright 2020 Camptocamp SA (http://www.camptocamp.com) # License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html). -# from odoo import _ +from odoo import _ from odoo.addons.component.core import Component @@ -19,6 +19,12 @@ class MessageAction(Component): _inherit = "shopfloor.process.action" _usage = "message" + def generic_record_not_found(self): + return { + "message_type": "error", + "body": _("Record not found."), + } + # TODO: we should probably have `shopfloor.message` records # and here we can simply lookup for a message using its identifier # Eg: From 6397dbc4f20b05d17ad7936611b3c0b908842cf1 Mon Sep 17 00:00:00 2001 From: Simone Orsi Date: Fri, 26 Feb 2021 16:08:04 +0100 Subject: [PATCH 035/111] shopfloor_base: service add properties for schema --- shopfloor_base/services/service.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/shopfloor_base/services/service.py b/shopfloor_base/services/service.py index 0b0c5a8c1f..21a9ce240a 100644 --- a/shopfloor_base/services/service.py +++ b/shopfloor_base/services/service.py @@ -169,6 +169,14 @@ def data(self): def data_detail(self): return self._actions_for("data_detail") + @property + def schema(self): + return self._actions_for("schema") + + @property + def schema_detail(self): + return self._actions_for("schema_detail") + @property def msg_store(self): return self._actions_for("message") From 862d72a53c36fca75b14fa6a924109167ac9e0a5 Mon Sep 17 00:00:00 2001 From: Simone Orsi Date: Fri, 26 Feb 2021 16:08:36 +0100 Subject: [PATCH 036/111] shopfloor_base: validator return None if not found --- shopfloor_base/services/validator.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/shopfloor_base/services/validator.py b/shopfloor_base/services/validator.py index 93848e32ab..9691c3cfa0 100644 --- a/shopfloor_base/services/validator.py +++ b/shopfloor_base/services/validator.py @@ -79,7 +79,7 @@ def get_validator_handler(self, service, method_name, direction): ) except NoComponentError: _logger.warning("no component found for %s method %s", service, method_name) - return {} + return None try: return getattr(validator_component, method_name) @@ -87,7 +87,7 @@ def get_validator_handler(self, service, method_name, direction): _logger.warning( "no validator method found for %s method %s", service, method_name ) - return {} + return None def has_validator_handler(self, service, method_name, direction): """Return if the service has a validator handler for a method From e9f98f31a5a87163c0df9bfcef5fdb61982122b0 Mon Sep 17 00:00:00 2001 From: Simone Orsi Date: Wed, 3 Mar 2021 11:16:46 +0100 Subject: [PATCH 037/111] shopfloor_base: add ACSONE to credits --- shopfloor_base/readme/CREDITS.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/shopfloor_base/readme/CREDITS.rst b/shopfloor_base/readme/CREDITS.rst index dd789bcc35..a7aeb40b73 100644 --- a/shopfloor_base/readme/CREDITS.rst +++ b/shopfloor_base/readme/CREDITS.rst @@ -3,3 +3,4 @@ * Cosanum * Camptocamp R&D * Akretion R&D +* ACSONE R&D From c95dc25afa92654dfe9619881ded48401806144b Mon Sep 17 00:00:00 2001 From: Simone Orsi Date: Mon, 8 Mar 2021 14:43:40 +0100 Subject: [PATCH 038/111] shofloor: make menu picking type info not mandatory --- shopfloor_base/services/menu.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/shopfloor_base/services/menu.py b/shopfloor_base/services/menu.py index bd57626141..b57290ea03 100644 --- a/shopfloor_base/services/menu.py +++ b/shopfloor_base/services/menu.py @@ -51,10 +51,10 @@ def search(self, name_fragment=None): ) def _convert_one_record(self, record): - values = record.jsonify(self._one_record_parser(), one=True) + values = record.jsonify(self._one_record_parser(record), one=True) return values - def _one_record_parser(self): + def _one_record_parser(self, record): return [ "id", "name", From b825f290207aded748290520755b31fbfb070829 Mon Sep 17 00:00:00 2001 From: Simone Orsi Date: Mon, 8 Mar 2021 14:44:48 +0100 Subject: [PATCH 039/111] shopfloor: isolate menu items in tests --- shopfloor_base/tests/test_user.py | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/shopfloor_base/tests/test_user.py b/shopfloor_base/tests/test_user.py index 5f63b3599e..4aca43bc96 100644 --- a/shopfloor_base/tests/test_user.py +++ b/shopfloor_base/tests/test_user.py @@ -8,8 +8,17 @@ class UserCase(CommonCase, MenuTestMixin): @classmethod def setUpClassVars(cls, *args, **kwargs): super().setUpClassVars(*args, **kwargs) - cls.profile = cls.env.ref("shopfloor_base.profile_demo_1") - cls.profile2 = cls.env.ref("shopfloor_base.profile_demo_2") + ref = cls.env.ref + profile1 = ref("shopfloor_base.profile_demo_1") + cls.profile = profile1.sudo().copy() + cls.profile2 = ref("shopfloor_base.profile_demo_2") + menu_xid_pref = "shopfloor_base.shopfloor_menu_" + cls.menu_items = ref(menu_xid_pref + "demo_1") + # Isolate menu items + cls.menu_items.sudo().write({"profile_id": cls.profile.id}) + cls.env["shopfloor.menu"].search( + [("id", "not in", cls.menu_items.ids)] + ).sudo().write({"profile_id": profile1.id}) def setUp(self): super().setUp() @@ -20,7 +29,7 @@ def test_menu_no_profile(self): """Request /user/menu""" # Simulate the client asking the menu response = self.service.dispatch("menu") - menus = self.env["shopfloor.menu"].search([]) + menus = self.menu_items self.assert_response( response, data={"menus": [self._data_for_menu_item(menu) for menu in menus]}, @@ -29,7 +38,7 @@ def test_menu_no_profile(self): def test_menu_by_profile(self): """Request /user/menu w/ a specific profile""" # Simulate the client asking the menu - menus = self.env["shopfloor.menu"].sudo().search([]) + menus = self.menu_items.sudo() menu = menus[0] menu.profile_id = self.profile (menus - menu).profile_id = self.profile2 From 230b1864526eba514714e3b7e8f18e741cc902fd Mon Sep 17 00:00:00 2001 From: OCA-git-bot Date: Tue, 20 Apr 2021 09:03:10 +0000 Subject: [PATCH 040/111] [UPD] README.rst --- shopfloor_base/README.rst | 1 + shopfloor_base/static/description/index.html | 1 + 2 files changed, 2 insertions(+) diff --git a/shopfloor_base/README.rst b/shopfloor_base/README.rst index b7d430801d..3f4952b7ac 100644 --- a/shopfloor_base/README.rst +++ b/shopfloor_base/README.rst @@ -151,6 +151,7 @@ Other credits * Cosanum * Camptocamp R&D * Akretion R&D +* ACSONE R&D Maintainers ~~~~~~~~~~~ diff --git a/shopfloor_base/static/description/index.html b/shopfloor_base/static/description/index.html index e8b66de109..defe354010 100644 --- a/shopfloor_base/static/description/index.html +++ b/shopfloor_base/static/description/index.html @@ -502,6 +502,7 @@

Other credits

  • Cosanum
  • Camptocamp R&D
  • Akretion R&D
  • +
  • ACSONE R&D
  • From ea7e7b1ef152bd182b6bb9e7e70a8f9bad94f418 Mon Sep 17 00:00:00 2001 From: OCA-git-bot Date: Tue, 20 Apr 2021 09:03:11 +0000 Subject: [PATCH 041/111] shopfloor_base 14.0.1.0.1 --- shopfloor_base/__manifest__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/shopfloor_base/__manifest__.py b/shopfloor_base/__manifest__.py index 5e75c581b1..f61954c78d 100644 --- a/shopfloor_base/__manifest__.py +++ b/shopfloor_base/__manifest__.py @@ -6,7 +6,7 @@ { "name": "Shopfloor Base", "summary": "Core module for creating mobile apps", - "version": "14.0.1.0.0", + "version": "14.0.1.0.1", "development_status": "Alpha", "category": "Inventory", "website": "https://github.com/OCA/wms", From b8bc764adbcd6b583e67259751e0454db225cffb Mon Sep 17 00:00:00 2001 From: oca-travis Date: Mon, 31 May 2021 15:18:52 +0000 Subject: [PATCH 042/111] [UPD] Update shopfloor_base.pot --- shopfloor_base/i18n/shopfloor_base.pot | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/shopfloor_base/i18n/shopfloor_base.pot b/shopfloor_base/i18n/shopfloor_base.pot index 2f4e69b4ac..89a4c1a82f 100644 --- a/shopfloor_base/i18n/shopfloor_base.pot +++ b/shopfloor_base/i18n/shopfloor_base.pot @@ -4,7 +4,7 @@ # msgid "" msgstr "" -"Project-Id-Version: Odoo Server 13.0\n" +"Project-Id-Version: Odoo Server 14.0\n" "Report-Msgid-Bugs-To: \n" "Last-Translator: \n" "Language-Team: \n" @@ -165,6 +165,12 @@ msgstr "" msgid "Profiles" msgstr "" +#. module: shopfloor_base +#: code:addons/shopfloor_base/actions/message.py:0 +#, python-format +msgid "Record not found." +msgstr "" + #. module: shopfloor_base #: code:addons/shopfloor_base/services/scan_anything.py:0 #, python-format From de29598ea6bca9afa580d216153857c0bbf40823 Mon Sep 17 00:00:00 2001 From: OCA Transbot Date: Mon, 31 May 2021 15:22:28 +0000 Subject: [PATCH 043/111] Update translation files Updated by "Update PO files to match POT (msgmerge)" hook in Weblate. Translation: wms-14.0/wms-14.0-shopfloor_base Translate-URL: https://translation.odoo-community.org/projects/wms-14-0/wms-14-0-shopfloor_base/ --- shopfloor_base/i18n/es_AR.po | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/shopfloor_base/i18n/es_AR.po b/shopfloor_base/i18n/es_AR.po index d167422a67..f4a481c574 100644 --- a/shopfloor_base/i18n/es_AR.po +++ b/shopfloor_base/i18n/es_AR.po @@ -170,6 +170,12 @@ msgstr "Perfil" msgid "Profiles" msgstr "Perfiles" +#. module: shopfloor_base +#: code:addons/shopfloor_base/actions/message.py:0 +#, python-format +msgid "Record not found." +msgstr "" + #. module: shopfloor_base #: code:addons/shopfloor_base/services/scan_anything.py:0 #, python-format From 26db75cc6c44b7b58502846322ccac1af4221a42 Mon Sep 17 00:00:00 2001 From: Ignacio Buioli Date: Sat, 5 Jun 2021 02:17:55 +0000 Subject: [PATCH 044/111] Translated using Weblate (Spanish (Argentina)) Currently translated at 100.0% (38 of 38 strings) Translation: wms-14.0/wms-14.0-shopfloor_base Translate-URL: https://translation.odoo-community.org/projects/wms-14-0/wms-14-0-shopfloor_base/es_AR/ --- shopfloor_base/i18n/es_AR.po | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/shopfloor_base/i18n/es_AR.po b/shopfloor_base/i18n/es_AR.po index f4a481c574..05323c8c7f 100644 --- a/shopfloor_base/i18n/es_AR.po +++ b/shopfloor_base/i18n/es_AR.po @@ -6,7 +6,7 @@ msgid "" msgstr "" "Project-Id-Version: Odoo Server 13.0\n" "Report-Msgid-Bugs-To: \n" -"PO-Revision-Date: 2021-03-31 05:46+0000\n" +"PO-Revision-Date: 2021-06-05 04:48+0000\n" "Last-Translator: Ignacio Buioli \n" "Language-Team: none\n" "Language: es_AR\n" @@ -174,7 +174,7 @@ msgstr "Perfiles" #: code:addons/shopfloor_base/actions/message.py:0 #, python-format msgid "Record not found." -msgstr "" +msgstr "Registro no encontrado." #. module: shopfloor_base #: code:addons/shopfloor_base/services/scan_anything.py:0 From 9c14a23501703140edc7432dd048a7c2f42784e0 Mon Sep 17 00:00:00 2001 From: Simone Orsi Date: Thu, 14 Oct 2021 16:50:31 +0200 Subject: [PATCH 045/111] shopfloor_base: adapt tests to base_rest change --- shopfloor_base/tests/common.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/shopfloor_base/tests/common.py b/shopfloor_base/tests/common.py index 512ebde780..71394df168 100644 --- a/shopfloor_base/tests/common.py +++ b/shopfloor_base/tests/common.py @@ -3,6 +3,8 @@ from contextlib import contextmanager from pprint import pformat +import mock + from odoo.tests.common import SavepointCase from odoo.addons.base_rest.controllers.main import _PseudoCollection @@ -57,7 +59,13 @@ def work_on_services(self, env=None, **params): params = params or {} collection = _PseudoCollection("shopfloor.service", env or self.env) yield WorkContext( - model_name="rest.service.registration", collection=collection, **params + model_name="rest.service.registration", + collection=collection, + # No need for a real request mock + # as we don't deal w/ real request for testing + # but base_rest context provider needs it. + request=mock.Mock(), + **params ) @contextmanager From c37b162f91e9400795a524f46f5f588167c21a8c Mon Sep 17 00:00:00 2001 From: OCA-git-bot Date: Thu, 14 Oct 2021 15:53:21 +0000 Subject: [PATCH 046/111] shopfloor_base 14.0.1.0.2 --- shopfloor_base/__manifest__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/shopfloor_base/__manifest__.py b/shopfloor_base/__manifest__.py index f61954c78d..f64fcaa0df 100644 --- a/shopfloor_base/__manifest__.py +++ b/shopfloor_base/__manifest__.py @@ -6,7 +6,7 @@ { "name": "Shopfloor Base", "summary": "Core module for creating mobile apps", - "version": "14.0.1.0.1", + "version": "14.0.1.0.2", "development_status": "Alpha", "category": "Inventory", "website": "https://github.com/OCA/wms", From bdcc58a4cc9026adac906a83511cf675083adbd9 Mon Sep 17 00:00:00 2001 From: Simone Orsi Date: Wed, 17 Nov 2021 15:37:09 +0100 Subject: [PATCH 047/111] shopfloor_base: unify exposed model usage --- shopfloor_base/services/menu.py | 2 +- shopfloor_base/services/profile.py | 2 +- shopfloor_base/services/service.py | 8 +++++++- 3 files changed, 9 insertions(+), 3 deletions(-) diff --git a/shopfloor_base/services/menu.py b/shopfloor_base/services/menu.py index b57290ea03..4f0dbd86c5 100644 --- a/shopfloor_base/services/menu.py +++ b/shopfloor_base/services/menu.py @@ -40,7 +40,7 @@ def _search(self, name_fragment=None): domain = self._get_base_search_domain() if name_fragment: domain.append(("name", "ilike", name_fragment)) - records = self.env[self._expose_model].search(domain) + records = self._exposed_model.search(domain) return records def search(self, name_fragment=None): diff --git a/shopfloor_base/services/profile.py b/shopfloor_base/services/profile.py index aea52239da..4d02830b32 100644 --- a/shopfloor_base/services/profile.py +++ b/shopfloor_base/services/profile.py @@ -24,7 +24,7 @@ def _search(self, name_fragment=None): domain = self._get_base_search_domain() if name_fragment: domain.append(("name", "ilike", name_fragment)) - records = self.env[self._expose_model].search(domain) + records = self._exposed_model.search(domain) return records def search(self, name_fragment=None): diff --git a/shopfloor_base/services/service.py b/shopfloor_base/services/service.py index 21a9ce240a..88caa9eb7c 100644 --- a/shopfloor_base/services/service.py +++ b/shopfloor_base/services/service.py @@ -29,10 +29,16 @@ def dispatch(self, method_name, *args, params=None): def _actions_for(self, usage, **kw): return get_actions_for(self, usage, **kw) + @property + def _exposed_model(self): + # Use `.get` to avoid failure on `inspect.getmembers` call + # which load this on services w/out a model. + return self.env.get(self._expose_model) + def _get(self, _id): domain = expression.normalize_domain(self._get_base_search_domain()) domain = expression.AND([domain, [("id", "=", _id)]]) - record = self.env[self._expose_model].search(domain) + record = self._exposed_model.search(domain) if not record: raise exceptions.MissingError( _("The record %s %s does not exist") % (self._expose_model, _id) From 1067acc736428369b33f18d2c19f4711660b8e1d Mon Sep 17 00:00:00 2001 From: Simone Orsi Date: Wed, 17 Nov 2021 15:37:23 +0100 Subject: [PATCH 048/111] sf_base: profiles and menu load w/out permission check Use sudo because we don't care if the current user can see menu items / profiles or not. They should always be loaded by the app. --- shopfloor_base/services/menu.py | 7 +++++++ shopfloor_base/services/profile.py | 7 +++++++ 2 files changed, 14 insertions(+) diff --git a/shopfloor_base/services/menu.py b/shopfloor_base/services/menu.py index 4f0dbd86c5..ce220c0274 100644 --- a/shopfloor_base/services/menu.py +++ b/shopfloor_base/services/menu.py @@ -20,6 +20,13 @@ class ShopfloorMenu(Component): _expose_model = "shopfloor.menu" _description = __doc__ + @property + def _exposed_model(self): + # Use sudo because we don't care + # if the current user can see menu items or not. + # They should always be loaded by the app. + return super()._exposed_model.sudo() + def _get_base_search_domain(self): base_domain = super()._get_base_search_domain() return expression.AND( diff --git a/shopfloor_base/services/profile.py b/shopfloor_base/services/profile.py index 4d02830b32..3c0ea4e437 100644 --- a/shopfloor_base/services/profile.py +++ b/shopfloor_base/services/profile.py @@ -20,6 +20,13 @@ class ShopfloorProfile(Component): _expose_model = "shopfloor.profile" _description = __doc__ + @property + def _exposed_model(self): + # Use sudo because we don't care + # if the current user can see profiles or not. + # They should always be loaded by the app. + return super()._exposed_model.sudo() + def _search(self, name_fragment=None): domain = self._get_base_search_domain() if name_fragment: From f6f48d22d3d132e70e149d6bb83fa89862a5495d Mon Sep 17 00:00:00 2001 From: OCA-git-bot Date: Wed, 17 Nov 2021 16:53:12 +0000 Subject: [PATCH 049/111] shopfloor_base 14.0.1.1.0 --- shopfloor_base/__manifest__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/shopfloor_base/__manifest__.py b/shopfloor_base/__manifest__.py index f64fcaa0df..5ea1c299b8 100644 --- a/shopfloor_base/__manifest__.py +++ b/shopfloor_base/__manifest__.py @@ -6,7 +6,7 @@ { "name": "Shopfloor Base", "summary": "Core module for creating mobile apps", - "version": "14.0.1.0.2", + "version": "14.0.1.1.0", "development_status": "Alpha", "category": "Inventory", "website": "https://github.com/OCA/wms", From 701e8e8c6590a6a6a111220763461ece8700bc37 Mon Sep 17 00:00:00 2001 From: Simone Orsi Date: Tue, 18 Jan 2022 15:46:26 +0100 Subject: [PATCH 050/111] shopfloor_base: dev status -> Beta --- shopfloor_base/__manifest__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/shopfloor_base/__manifest__.py b/shopfloor_base/__manifest__.py index 5ea1c299b8..264d6e5012 100644 --- a/shopfloor_base/__manifest__.py +++ b/shopfloor_base/__manifest__.py @@ -7,7 +7,7 @@ "name": "Shopfloor Base", "summary": "Core module for creating mobile apps", "version": "14.0.1.1.0", - "development_status": "Alpha", + "development_status": "Beta", "category": "Inventory", "website": "https://github.com/OCA/wms", "author": "Camptocamp, BCIM, Akretion, Odoo Community Association (OCA)", From ff65f85a6c312c4a6e0741ca49aa3229e43da938 Mon Sep 17 00:00:00 2001 From: OCA-git-bot Date: Wed, 19 Jan 2022 10:12:10 +0000 Subject: [PATCH 051/111] [UPD] README.rst --- shopfloor_base/README.rst | 9 ++------- shopfloor_base/static/description/index.html | 8 +------- 2 files changed, 3 insertions(+), 14 deletions(-) diff --git a/shopfloor_base/README.rst b/shopfloor_base/README.rst index 3f4952b7ac..42cc4bb137 100644 --- a/shopfloor_base/README.rst +++ b/shopfloor_base/README.rst @@ -7,9 +7,9 @@ Shopfloor Base !! changes will be overwritten. !! !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! -.. |badge1| image:: https://img.shields.io/badge/maturity-Alpha-red.png +.. |badge1| image:: https://img.shields.io/badge/maturity-Beta-yellow.png :target: https://odoo-community.org/page/development-status - :alt: Alpha + :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 @@ -31,11 +31,6 @@ This module provides REST APIs to support scenario. It needs a frontend to consume the backend APIs and provide screens for users on barcode devices. A default front-end application is provided by ``shopfloor_mobile_base``. -.. IMPORTANT:: - This is an alpha version, the data model and design can change at any time without warning. - Only for development or testing purpose, do not use in production. - `More details on development status `_ - **Table of contents** .. contents:: diff --git a/shopfloor_base/static/description/index.html b/shopfloor_base/static/description/index.html index defe354010..4e7b4438e1 100644 --- a/shopfloor_base/static/description/index.html +++ b/shopfloor_base/static/description/index.html @@ -367,17 +367,11 @@

    Shopfloor Base

    !! This file is generated by oca-gen-addon-readme !! !! changes will be overwritten. !! !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! --> -

    Alpha License: LGPL-3 OCA/wms Translate me on Weblate Try me on Runbot

    +

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

    Shopfloor is a barcode scanner application.

    This module provides REST APIs to support scenario. It needs a frontend to consume the backend APIs and provide screens for users on barcode devices. A default front-end application is provided by shopfloor_mobile_base.

    -
    -

    Important

    -

    This is an alpha version, the data model and design can change at any time without warning. -Only for development or testing purpose, do not use in production. -More details on development status

    -

    Table of contents

      From 8a5d09f8d2f89fcacd0606e0f116f3cb60e4aca1 Mon Sep 17 00:00:00 2001 From: OCA-git-bot Date: Wed, 19 Jan 2022 10:12:18 +0000 Subject: [PATCH 052/111] shopfloor_base 14.0.1.2.0 --- shopfloor_base/__manifest__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/shopfloor_base/__manifest__.py b/shopfloor_base/__manifest__.py index 264d6e5012..3f55a83a65 100644 --- a/shopfloor_base/__manifest__.py +++ b/shopfloor_base/__manifest__.py @@ -6,7 +6,7 @@ { "name": "Shopfloor Base", "summary": "Core module for creating mobile apps", - "version": "14.0.1.1.0", + "version": "14.0.1.2.0", "development_status": "Beta", "category": "Inventory", "website": "https://github.com/OCA/wms", From 78313e92e385b79df1b63394ac44260b7699863a Mon Sep 17 00:00:00 2001 From: Pierre Verkest Date: Wed, 3 Nov 2021 11:15:37 +0100 Subject: [PATCH 053/111] [IMP] shopfloor_base: add app menu icon --- shopfloor_base/readme/CREDITS.rst | 4 ++++ shopfloor_base/static/description/icon.png | Bin 9455 -> 15812 bytes shopfloor_base/views/menus.xml | 10 ++++++---- 3 files changed, 10 insertions(+), 4 deletions(-) diff --git a/shopfloor_base/readme/CREDITS.rst b/shopfloor_base/readme/CREDITS.rst index a7aeb40b73..b2141e8f1a 100644 --- a/shopfloor_base/readme/CREDITS.rst +++ b/shopfloor_base/readme/CREDITS.rst @@ -4,3 +4,7 @@ * Camptocamp R&D * Akretion R&D * ACSONE R&D + +**Icons** + +* Tablet app icon by Gregor Cresnar from the Noun Project diff --git a/shopfloor_base/static/description/icon.png b/shopfloor_base/static/description/icon.png index 3a0328b516c4980e8e44cdb63fd945757ddd132d..9e308a5440ef1a5865c06d41069f2ae987965050 100644 GIT binary patch literal 15812 zcmV;#Jv+jQP)r004{#1^@s6quw!j000*sdQ@0+Qek%> zaB^>EX>4U6ba`-PAZ2)IW&i+q+O3*tavVF7g#Y6dbA-eW90#k}++dDBpChwaWQls+ zJvO2wl37_;B0N0Yk!9BZ{P#Bh#h>K8Ga=@ZYDyP>LJie5ekj-d=l*W5aQ?q9wdXba z^SUYHdBJhX^WERSruE$4xE|kc$l!**{UG2HA4|&#OS$hrJ8iSuuJe7^ z!4iX8F7w^FK@N1S`0T11UMUFrvfE`}{WQLijX~Btm)+M|?3uTo`GzgbJS}%R%V5gi zA7AFTi~pCeuYvZ#fwfn8;xv?CCrq;pWlsNd7YVWZiD^9XJzwSynA=wPp8eYDA5*|}^jaV+Ggv2y2w>tw*j z=%zKB+<2czPH~!>_hftSbN~prPkUSlQxEKyt{EnYu$C< zLytZ6+)J;$4d64vh$D?W%BZ7FH~pmiXPVi}GV5&1E}*o+iYu+W%Brhvu(tgUJMOge zF1zmbl{Nd6?T@V4kIemZ*6fotWh|Wg@;hr>&iOTj6`e%ojEuQp$#_-<0JKxiu3|%Q z%A9g`RYy@oN7igqa;8WS>TpX^^4qHiA znd$0;r4gg6HBYlEEzgwZ9E>>fhhbG2fK z2F>Vs7k7s|n+tnwvJi;r)vMdDOZIhF1QcLo)clOu%?|A~LFeb9fv;cY+|xDcohJRB zbhqeU^P0TsSu!M+|MkS*H^Ub^Ql0I(*?ep!`oHeQ*%oXDtgoTImJM*6`5A=wszOR& zt;8Rp^`qx&Q7U>X@9e(zZR0NVwMp`N1aHOW6sIc#>gzkMYcH4j$aEy?X(kB5))-oE z)?Eu(rJ~A~xULJ38o%Jr2?3VMy&fSoC}##pgp#e+$>IP~$`qRo+G2*Loqm?6Ip5vY zOkj;o^634&vg)gyE0Q}DP58KjFkfdVsUrq;)0y|Mub0LZzD60llk`P<+$ARdvinUl zqSAU^YT35u%0vYFJz20Y!aWHi($Qk2+E+`(7V5<1T89gX}$?Fh;1L;p+?#o zefW$f@FI@x19e_)UwtnhY1HvZoQ6E{Ezyqx$zna5;1-CJ_0^Swsr*#>LO#sl<7&Cs zg-W?HWVHzPo)IBYQ{Kb5-3mBxXqv(+k;JZ;TV333qCM}-$l~4l2;24Vw^_}|OeZpo z0W@i`Ldm7#s`1MAOeXF#Ro0DjG_MtB;IMmTef7OQJLqfbEq*{t@4LdiCv$JA?HKPl z&UEa_Zj^joF`nDfeb+_qSSlKI#Yt(bGsBt8y!S-tI^wHzmuC0Q#@1N%h)ryKF;p7U zj#5XZc`QI*DWvW_M7H}d~ih-yr9N7 zr(MWJrv?-*0uE>>!7555*S9I31nLhexATE5Rrh+TQc zjRrN$dogU?Z@QS@BzoI}CpRX5rpaZR+g_baq+l1+2Ysz)K@`{CxkN)N&iCw0!R)Be z6MiV@$My2|XZwo1Nh`8M8QVix@4f7GQa~l-L0RsZ02g^&5@rR_;uL)U?9mxmX-io+JE^ox zq0TxEruv*0@24kCM~5%k0#)sIMKjFQOV|h-O-3DJ{FdK?Y8pLVbck`StNlDsg1G*J7z)!La!9`%|i7-2+njO`Xw+2St?{Nu@gIKjISkc5Dfe}z*B@ZS+|=l`93 zI*qf222mU%BkrqXdIEt=83Ql0u9Vuf#L-k9I3^Udb#g;AAhm+o*`kNY-Db8hH`$;r zePTChtEAzDMTKT*wb9vGs@H5|>fLrWgl8QI@NP*$8pK}4f}&WS`Xb?MLA$3>U{BIBfwWd=|aLP|Bkt{(%JdLvOy(P9cp6d?(5ijtCe9R7cNc zU?OO+*c@6D@Kv`JAnCNT!Axr}2xl|@q(?zWd5*JrY#htLfh{Unkzyivy|X}{8aTsf z;o^!NARF`3R?gItTW}3phLy$u6E5PteSlnwqJ?k6xd3Pwe)X^$1Z+?7&|A!4J62e_ z;)4yoHN-7Nu-pNSf_b9oq5=!D!5DWQAj2)E*i63zt#T7%KE>81{gr}ed!$E{S6dHD z&VOjR!2L+jVl7lbTW=<-h#Wf!iUpMgEg~Lns(hD8s`Hzo^(fjskYPu2AUH}hvdv$L z`T0IoCS!e;2{`)ox|QA#9}W9Q{p>gtx8KSqe3p+p5SKzR^O}4G>QH3Y-&A)05JX(>YGq4&)KUJP9ytNrQ&VF!WSr z>xjA3>HUzsha$)i48jyS>!blT7Ukp)BvZt{W03$O_$-4sEh@TP(gS!S6eot+q}nVK zw^?pf956&G%Y(pP=a>QML6ddqA! z{Z$AGKVD(_b4Gm5N{L~|T6BpGx(l;aIxh0~@&L{Eg<7CUb`Hx)_>QzQWrOBS37P1e zyK-5ojJk%pC8B1G=rd8p=Q9vK?k<>=sHgQ*3_|d(ncfbqkcUXYjo@#>ne-$JWp?4C zs7eeS;K4+B8V-G$k^gKs<6FNWFOJS~5fdOR6>hF_YKMg%ugFq(TgLLOA98{cPXloK zoME*d{IPw|CpbA>#he162kEFj`0roC4nhUlhq03G4^i|{@5+A9vvOGOz9@fO{VXH3ur$?wQ{44DP@r3Pf zEIML%blwpKW3_CdA8_W-r+1;2PMyOu*k8H?D1i#-P=rw32VL0I)GM-PZ0}~ko}v@l zGIi1J5s)z3=3Y;+UP2m9kVDKTPw`6`^WiUIbKz0cwe5Cqq8>PS8S zq>MVQ73;$i7pe-DSTM;fXbJQI5CQGdfHOO^N!>&ayyMqgxCW+@&Y$$Ywqb$?anlA#~6{K&O(kd0s)FTutpp*Ym5{U`&gGWewm zI^n&Q|6N`}CvY#igLJ*Ska`eI#P!`BazW=OZs`dO1v09YxhjL)m@`k36>^?V0?p%K z91WpcB4sOAP>zxsV8=?p#3;FwYhtOT=Ulq4xa=myft5tIgn`qiG6H(I-L-FXswwNl zD(Z*~xtv18=2XJqz*&%;53RUtHpK(3sYRj)^3!R`M52vcE}HHnFmz80T}A5fcAOv- zv*ZpoJrwylTqq{2On@K_)@jgEe$)0OSoNg=u>TpQ+$p(ytC!6%ImLW@Xn$q2qj%T7V$u-qWo$rT=ds!yI@|dP$nIv~) znhuWaIT2aM>pzVQ0%8?E8d-Yk?^BjhBT+6}eN+O^b=6Zkr-sYQEMJyyr`+6+x9d2# z=*7Eb`O~t@_nuXLtxbB~)3MU6RlXP1-L)u1(@$LPw#E04#wG8l>dNBJcg6OPzBRTp zz8w0xkUPV|Tj)g9n2Hyw#w{pEfD_WF02HN+7zNrsgc0z?I-c&lZioeb(@!smtwEc` zBLh6q6Roea_9!4AI~(4kyRk}ldhU(JM1`UeDwRa~5qm{qC|n&8bBU4nRN10WQ0=VP z&xB4`F_Vym`fJTRN7`+;i~q+TF-MJM^I_`ESVLJ zSWViLQAOUNIeRYemQrzWnlYN2F_RYO!KpR+~r6VC|K5J(^wewr)jDIK{^GEH( z3~MgK#1;bSfzWAN15g zCw#8zk=;OcBl?AtW>%T-0qJ3P1X*4wFLOc(# z08`{w+3&;3P3_zM>b29yb<@&I68T?HGWVYZizM1Ctjhr^R9j`<&2Sb!kZ=iOUOE`b z36h}z{Muj#hX-W3BuW(>w>j*xmmJoHF6kV|Tb=C7i#Tja)6p~6qmILy8)HRzbbKmd zh{ti!&8ZDNhunI8W7}zuB>>{PL@MuT?S5!rQzGB!_c9RcLI^(NV-V8j&oMsp>*#8G z&2Mxb-#tzSzVzT=qAc=xct$l4G{FgyI;ZyU`Jd^}HLDGHgH4`#*yH|ud;d1|kF9^3 znew}IRKHC;JoC!}U`I%z>Qvq8!0kNVFNzHd&>84wwrT%FGj+mIw5ov+v{CYf@_r{XV7tJfW?>sF7&v7oXC` z>LvYQ0^MYN>5)O}26^zh8^yQc5w!^PG4u$tEZ9fqEdViK9_gP!?gC{$su`JvV;h%| zH;mewKGvqA^Qa@Vl32sT=@5rwZsZ^$LnjCHO|K`BgAgzfY@*MqY(4bT#~%{nLeKY+ zQ+m3pvs%fGsGbGreZ;+ltW&y_T(G!ExUm{IngoKcO6Olv9RyqfL-f#X8^4I(C5Z<~ z(Ccgjd9$RzlNT0J~7QzXDSY-3k}k#oxc^So{iP&E_yX;sPHzg zgEn+F%A^I52U#HGiD}E=V~FUn<3)z2BdT!ML_!}?fIF!FFAzMQ&VYvgM#Eph0H?2o zSG3PL=5>=_OV|D6^K{y4BH;9i9k6pgqp^`V$*mm#Qr6QV8cal&QrG()l{ftuTRQ|k1VX3 z^--e+K7#0>6Np5q(@}T0=>xr(&Lepb64q%;I52lVlN&IPxw$3WQ|S?-4(zcdMGtmX zy0*^y&Os7uh_@ZA>3#oM9Hz(U7$8NKc*gE{Bp{CiBe(t;<<^cig{h10xZy~g>oiF_l)|i z2~Dko#-f3iy1-d`zF|p&27a6Nn4NOK=oDI-Ghch_*k&B|YVs{(P7T zkl!zzU((P)SyxDdd-7;Z`~~WL z1_u$R!TfteJj;N>aCUu$OGm^ao&1*-Rph6oGGUmSe}wuS zAX-j@ChQXg#E`}~4Eb|a@m^K_eN~iEj}*_Loqp-Iiuk(^#-BNr%M(m(nn;ra&;bY zCKq4xy~ASexYU1+%T`?CzT;xuYxmzP!rV~)=hejPAZPz=HCl}MSF36Ibf*lZ3t#d1 zZCVopY5{4y;*i%_Zo9GD+Eo4dq!>z=@#fH32zvLz7AAhl7%ht|KqYM#@Hu8whu4`a zV`i|#JJS8JtGUCg&@aN9s7R0K88lo4n_zXDPVHbE(*D0PuZfMb&_MVA000JJOGiWi zbpVzCq~KI{a{vGU32;bRa{vG?BLDy{BLR4&KXw2B00(qQO+^Rg3j+!aGKS41Gynh~ z07*naRCwC$eOYrHSC-y)Zf4eoeP0NGAPJG;B1)8ck$ODc(=*ZT@QfWDesYBU;0L=O z9pUk#|Ac?QMEJ=Mwj=DZ!yWd_xINuHqgHpTMNwQt5+uP5#10T!p-@$sd5<4*sm!X( ztjw$hprlkl1X-2ax!*neLIBAA5!v1X{pg?lM0@AE*GKX_h4Wf2^L<5Ae<+A9kf1RJ zasUX77LM?fnWN9+6u07(56i8+WWaMIr$^|g+=Px11g@RRWoGPvdC4@)Ul~>XSI5QD zlSeo2Z~x*i|BqgLy)W%=N&rj%1OU-DIw=0dpZ~OX`rPbQEvNls67*wTnsd6T=S?Y9 zW>y%WCU1?Duh0Z2Ei8AM6FK^RoZ{WW$;Rnu$!X53(SracBB=;P&!}2i6Y2_>|Hdfm zKihcm>h}Nmi~myGUS2o7Kv8>#`;~wF@Bhu<=Z40u&N5t7VQeiuM`n=3nfrZvO3m{ty3gcXw^u1RxbH zo6-LHzx!9cQ|D*@7@G3uWwY{8MX%;1OG=7joHCHS=|?x;S~8O%&5X%%Mp2b>xqNkLA_tiq?-&V}(N4jZfZ^Dapiko!iIo=#_nNkYLJg%zh zXrZ_8q;gzcTYd6^#q}S&KauUp{?n>yM1$mnM9x=#|DV2>*G)sMnAPtnIqfGO{Ko`qx)6AR1Kh_G_E0^BCmJwPu`$tvNm}0Q# z+;&gTO}ee0Fn>Mup#Zv5)ikCc=*Fe*TpJLIrhLTA*|Z?xH)kKdQToYgpqgZ<3Qf7* zGt@sMq`)i#N@qaQ8WFo`5`Pu+lMcMK0ztFTM6Zy9L0C?=4uFK;u-(sJkvC}@YZH=q zgaxuHfD9}be(a02<6B=HZt{jGvMKcx06EE070i6YbNP;fqT?Ar42mvDUUz6&-&25KC#4B<-=|bE<8_Cjr3if~|371KdsMIo0 zQsjZP1tjd*N?iK|iV*0{7BHCaLr*pjP0@T8Zi_!By+YI$oxqePO4Ty)J^~-HAt+aT;wx3oE@3Q!o*oj4UZw8$wLt$<)WTmPlsfQvR=XF-Zq}B zJ;&14GWN?y*1d`S+@-5}ILFgbP6{MoMn03rjk(Ks@7xtk42~j~$wH98SM`w0m>HSG z+}Je6o;}2;Pw!y2dOv_`0&yVObm{t=OS;=p%9^v z&0}VC3i*tcJNC(wTR5ncERJ$YEF%$bMcWIKn(|fyi9&?=ku&)A!u78WNPG)H0weuH z_+a53TpT+K6#~rCrcw&dz+DAO0(V;ykgyknY;%(wE=`}u*ue1D4kTW%4EGP=>g+}I zsyVPJE%Cv1bc-W)+Kd#@Di=w^Nov2dd-6cO4-v_(RY)njXNrY$6iy6~ zL`rJ_O4)-JX3Gp5?j1l+t_MPV1C)F=hw*`7C=f8JRTvcuEM6<7Th>kjWwuc&wE_~~ zC$^5_fJG4Do5KQ&5W@D{GW4nry;22xIZGGCPGUbwdjR5BWZCh*bNKGi4IR2}v_Sb& zs*flNGFrwLp765_cICUF8z3TRSqnsA@6@R%y4r4lM1sVlpVS1N2P}1mDJ8bHxA5fo z5{gH~^y0bB!AXhUrDNL{g=`Mz&z{5d0{>}e`Ar1dhQ~GD3L?;Kl5d}es0@j%$uPaF4hwpt4fA+&4 zV`^;D&so^R9hd>K(5_c?FaR~Hbyc5J+Xaxsw!7;e2;R0*N^I}$;L8__Fjads~RDZDshk7IxtCV;!kk8or2U5pQpK~>cz zB(pAN=sG}vn(>3BlgV3Z*8x&z^@JUgC3I6q)ieMi5K)a2kOLGB<_={4Yu&D<5V=Ec z)2Wcfp+=t37}%u%(Ln(UJb^q7Z45S1)pbag4P1*n0pz;O3Yb-k!`6ePQ|qU;o=%xI zkF)s|2m}%YLBb-G4Gj^`C@K7Bv8))K!g#4R=+2?>F5%Mk`>oO?Y zfSKD0mT03RarC%7AZd6I3SU$r0t8uhydbwjP@ttij@aQy!^guULRby^p+oU43-;+i z)Tr+`?f|x8I27Svq)_e`3>NC3dqF)hfF$4mf-F8vf?PjCJtc|aiUhwo9!y(``Y=*P zyJDiA%dpL5Sb~`BzcWIm9tHhMfuhl@8rH7QsGx+LC2lIHdRhR&fu@D6n+WoV90Iu< zC5UbNp%S!Uajc%O-N}e#LrK{=pt~I>9;-ydWVDonmPvP1l+uzCje>T(kcvjCPLShh0WwJJ2Zdjy_T~ z;E5=3>n8Jl zYQj(gC2KQL<$#3&C8GwK&PnT;MArclVljliyDP)VU8{5;X+epigy4vycC}G>2fF|) zuozXNEm7yLrIWRsrB%NJ+W-<*kdO!jD%-w}Fu6q`KAULF*)|g~npI;sB=hTJF|`@n zD=l>c2KmKM8Y!Z71t6W!aVQQu3fEVhqmapAc4QJcE$73aWQpy=U2Gk^hG7~FoFx?} zNeZl(+=L=r4Gh54&6af*^|XM5n>ev3F-SNZBz8nNqiA^d?A!Q9-}z(Yv;H=_k|iE3 zFX3-K|9dRIUia0pk|!XUh$C_uqk$qVAc|oXSz^cni=wHn<1(&((ovwa6C^bjUFR$# zO7yl|x7`MEJ-D%O4Wol2&;la2*UnwW!xvAm@_GX%EVnx-1y>-d^C)rxKS}ue#Q{mZ z&cZHdp_At*96#f_X{@K=GeQ%+qmhX90AV|`u8_%NZv0FjNB}@nRV$&$j?Y*Mw0#GP zDT0U+sGzO8bvR-JF{93OrdZsh?LbL2@+6I7Lcv1Ckq&~$USL2s_4}Y z+##TJs})v*G`H`|$16-Isi%WA080s52V|bbSxoNsJL-p?`4|T z0|h=G1M9n+xWDueTf5u-g6j+-=PIU#C$&C9x9KElmMxA9`-C91lf*wi;o z6IH#6O0|NjUWMU^jRe;ymX7en;ywKC;Vm3hOYqhX`*v>%UsCvHNx?}x_ok4Zwx(gg z^ngX}E)TIqTpIb*P@>_Fok(UZ@2um&^T(K-nn6YzX!;fBpg+3 z&C%d;n6)K>1WdzxL-J7?+$g~p%I9(z=pTqxnz?_tj~6S;SbX*vzkl!r)?RPIl%|J> z0;Byy_~7k#aryj3oS&P=@W4>0%iry5GCYRds~n1zlIuBk4O;>{ZR=vN3y!B-BmqgZ zTdDyurfGHuC~a4eIe3EzAuu;~1`9Luk=|A*C3f~+8SNQVTBEEa=eSG-tw=p?74n}b-^2Lmc%Uh+sT#(I$1pWGj^*t&=+dm?5abuV zp|wd(7!5^<4NeV`NFUOv=OFNO#vniIWB@regWgOYgN1&a8Jj{Omk+&;ZWwsB{2afz zcN6P}J64YoZ#KVO3gT@yVf~%3TRFsUAAF9{;Su!r_Mwn31ag(3{z2S$>l(KAU*q8T z5XWW(CB0(xyk~!R1LB0eS~yO=ZQ2?kYG22twgaSDK1#?@&~lg@979G?F*`Pm8yDZf zP~QOhdio;M28yL39>08w7dxv~9W&MHn7xHqPA0qiPIe8>=HY8ReD(+z=NFLA1xKE< zS_an^E@OUb78SjUrPb%S`|KgM4qjv9;58~n)z|VPN&n_%wQHQxEz>lhNUavBw~Kqk z9dW^y8~y6sMf{VGeuSQU0ji=RpUs7e8}GBOZmi+<(tQ|^AW>I+=&c#D9c1KdW+Ndb zH5YubxLhMn*%drbZ7+cE?mXx&IW$=+n-}`?U|LT;QCNfWXmN5Nn{{0 zBCXSNo_b;c;pAVm@?2HSj!j{BU?@^)4SME-;vv@dw_T7}bl?Uf*;mx zKm!m&3O}J_2}7E8wO$=agzyx;JIbunaXvuiX92*m(N6C?!2pyDXA?X+6jzgt>~57kO72(URxt?0e6ixWAlz*1&~ z06|&~mO3$&%J0r`$JI15b~zd(+!`lwfzobswiEGZ_Up5$z$}QM2n9k2NR~D6<5)Xh z(Zk-sO6>cb1ROocs}o19_bn;{ziY8y;;MCUCsKx?!MFtzdb51?xMT ziGz(N4HBotqI!}XB(9|PIuWXdj@wK3K?b8I--C&fF9e7bLu#|A{1PhoWq&1snFpL4(Bio15cNq$J&r47+*`bl zTleo^d20pBd+U${upp2k?2b{%h}f*iEUx0(z)hNfB$avGzS3%LSxkWltN^-{*edT~ zWBE0DGC9o6%wcL`5B3jtCI6=~qsqq}%({sQLngm^Mc;~`pR1F>XUo7I~PAzyx-uAF| z@J;O)l4TwW7Rhds|puU%}+)IPxLEK}9H-9-TxXm&g3%48C}L z56?DVVQqiQN?LLN0>W6|5a!3u;N69*n4g@*$iNV^hzVuKd06*#xml7|cg%cmiPoe0d>KQ7|g>PuW*csmp* zfHnSPq<;tlJ$;xNpT_dW3YOPba9l0B3!K$7Opi|D?9?oV`v;+^TI6&xW=5r2#n#?7 zN_yFPTu}!SPpeR;=p9L!0(BoGiFA~c^P+eORM&AyczU%8fsy_}oEe`)M$5$DPy|iY zFgh@dk^UiEJO8#@TWo!1buthnVma4{2>DzNGZWJodNGLY!`E&nOA2z(5!LGZl9+v| z3)Ittqzypf?qZC*O>8{+3K4SIEZ$wXimMBk(NicS5NXyTW<(|yz@m;^Hix(8-@@wl z27a@63nl&7=hSn%L)r>*lI|eXly(vMyu>YmB&EYG-3-DYP;mU!^z2Hqs`haI9eSm9cFG3NDf6#-VK(I$Hve!xpofMW1B$OZ&A-V<* zj{=wUO0}*_n4g+MM%8eVM@GwFergui&tFDH(`vCLs#};CL~xtp zQ2RMa(pZ(+Eq$Yv&<2D2{ZTv)DwW9DlUl4?qn3;QjC6+{{^IGg$yuVD6xJh-a^!V|8;K{k?rScjhem`}&d3 zHBKE=z8S9rC(j#8zBiNR5P=j!|T z@cOqgIW`Vv;Q7i+{PO0n@#V@RRN1f-y(w6bH*|_@j|-vQAfa{wg}XcFDL{h(AP6O+ zf?H3&geDY}j*p=V1^1sW;_vSK7CT4#Fj=BlJjP~m7nx@dv9`62pHBQs42G1SGBdV! zcksV{`B%8LbPpA2xLja)Zv%&)e~hEj5w5>=6@mmlzVkcWeYuDV3<$dvq}>VwCH|l_ zPU4ias3!^z&s^UGUbk8~h1V*u70e@}j9-2EDL#LE7tD;qatS(2Yc>uG%Sp$K3LDrz zK7?-Q4eoI-H+lN%IbLk9paKI>{F!ijl_LJ{7a!wSw|@%~0y>$1N^ZT7XPYIWPwDu(}f!6A2PSk3DF^O6Bs@2j()*yI^RR|0NaurgP&0wyT#9>MKSUUP>~dpM5mzi{hnB!gMv$=VD2x4-_MDC7%1_AXgs zb!Qz*Tgza(nvm)SV7ej*wcJ`K4azrBl_-&Ql046XsN@6T`s64c<$t>HlOS%RUZV4#d0sfQ3NcMvzbe3QJ6j1i)I>KTt1xtQCtq|W!y*kG1hi}pl}faw$`Ng;YVI2=0$Feso5vH>9V3CjT(vG>2tZ0y_+ zm&>r<`5OR^hDdZopfV3@zB(;$feO6L+J>%)#5kA--$&pCefy4{`01(v6+ zg<};tfU$CAJy~buDM1YYN;pj^mM+n<|D=l`v4PUbhtl2om$jN>&#^HKXNCMkq)axS+inOP3jwc3wm7`mA{2%HuqDLYrU zJ2vQRXHhS|?M64=FE}Qwe_91aKOK?3!B>Lb;Png1uICpGleN6h<6nEOU57o*nVBK% zKw{K@Z+LQryUj^r>nPkY7Qx=tZ}^H30zLU2j1P=pV}Az*8?|+Y*LOkgP{mLpg^ZGo zmTe(Zwd$L-G;k6MyoWb1>AZj<1V;OZ(cfeeSxfG@R3M3=%ywLqa9t8g=wK_t$lwsZ zd*ud%BCvb7Z+TrkyuZOq8){l|3PMrB394Zi_K!7pTXQkcn=9bGg{znvnSc`2IFQoG zob(_GiXNj&M?^3DuxIGW6>#msWlW7uqEtSP?~fn?wZI!Yzgeuc$Z1)O^beu0(0htF zN!O#tXgRsX=`UZ%=970RqGSul%+O5(QkoDXpsH3mDX2q0u>mbw=?B*(kaPwpslmY^ z5`q$shfjK#ritByJ*;f5qIh(KTrP(*6Vn(S9Et2+a2k(xgT#?~>Pd2puG`rGhcj*C zliQ!-=Ht8AD;}UX--CBAT*mjVe+Sd!lU*w;i$mHdk8~$EiQ}&rMgE_HP%r&ZOcW4E{u z!#l>M!+w$=koepcfq>`)Fv27Z5hOD<_O@|wbO;%fIPH_^0vRo1wfS;8OWXZZv`pl* zjt=owlq<*B+}nmJO;6;yvwo5&kXS(J!ft7-XeOV;WoGOi9bkKJ2Zq^Qpky=+lOy98 z=^b?2We@=ag+5G=Od#i(mfjVe$&}dH+r{SI4osFFt)$Us)Crx0;_`@3?VRWcF4RM4J7#_`zq8QsSd`zJ<}D5lesQu%4hQ3WoXzG0@lV_MH`ix&lc4;(D(vYJ#5&;l|L-QDacmGpt zz23n)7cOIZd=mY=ec?_wPNTe0eEP_%O>qacZqX3$AB*R4ACwAg|m5EDvf6*pggiHEPAV0LH{V}m21 zLkfJoMv&A=MZU&7^~7Jx4P;`!-5;?|j&f>B6FUdHc)7KLo#O*Y!5~$!%U8Yi z%i&f}r#Xqsb-V52;s#f}H!oFytcC-S1RUtc_+n)d^1102X!$3xGvc(T3TH}2Y z$U0AvyQqmBIzdj-aiBzcGu^5_kBgegYC-@3K@67m5Cb9*WuXflrf&Gs3Y(=qHV6q{ zE(`qQYrSQl?4(-O-h~=a$MHop)rmp!*A00~L25G&YZJ;ExJj2m(srPvadi`@mXO3Y zXK~Wkm;p!#637SvLkJkUkx=yxPE2lSo9=Dv>C_VY)SuwyxC~B2day3xcKykRo6jW^f`D`PEHomc8!fC_$c2|H}r5vxs6e zYPMiCCDmIuF}9A#5kkPI>NT)%anXJ6ZT0bGA5Q0<@ffsMXy`&DkczC#UY% zCalN+f?yg3n)JYH{t1N}C&8W)ie=cu(p6A5A@vl3q-*AJ5@)FoFm?UgypR?OLCtBP zD#0`wCx3>|v`I_YW}fDB9|m%e6I5J7qsP*ex=@6Q&ZH5i2aE?KVQnr12~frwOkf%& zBFiQiF^Z@t()&uyyuXwNU?&s1iF=+jr72Cx!ZeHnJ{9sO_??*8-xJ^SLqrgYfSS>u zs7i1HV2>pgpMcaj!?D>! zz>v~34~?pB2%}BV ziko{yQ93AoVpR15oRIiw1swG{hy@nanl;>Df<}WR>7soNKmwzp7mp81w^tv%+!wFc zHtv-UiZ@MD>Te47Xi!qQUJR)aO7o$Sq0jIQNMM>$FC8A;+*#YY|LW26vck-{!SP`u zlgnOFGwL9T&XnbGyR%z3dO?H`77RGiso#JEfRxPTV(I1X+V+2X^t(Hs@2+eeD~GT5 zd$Rf5exbkDR5RKIMOFJm8^FSyNPFUB3nPTED^Fy+)4u@;q?BAfI$q!3eEnZv+Rgh)yl?o>`(%FB`+g=I$b!m_V9NSGN$ z)sW@l@vHrfoxfaJeDSjfzy4y?IIdO!=n8TZL=&?^_8NkufeP_qn{;fzPNj& zlF%Uz!YyLUNhkF5B}uesQB98#=e)WbP{Ip@UNh~pcu334reQ*_RLx4Mymz#J_}Sk2 z_J4nI_sL%^{`SsF_29Sypb9`I07C#W0J2I(%}!pJ>7T!T@%+HV$UFJo!qsd~?xLcp zqe6%t5V|ufmfL>`mJHJ{AWfNwz?5!j!q4qgn4l?3Y+xaVG`VOR#!jVFesp|Px__{_ zd-vtN$4eVaD+gxPr~q(&bOO)|K(oP;0U@hsYG!b1EI)H;etd9pcv8z{`xI5l5s})q zmFxgGnq5Yk5~gX!FP9}sxvrElW4=ikvrT3O0nC!6X_{rdq95*W?yj#aKHuD5-##{V zqY8$@NvZ&J00sfb!3qv)9>8b-R1j1mQUE9b2{eu>c*73HH;1n&ngk%3*#yG?V0ggc z{1^aC)&8d0jUxM@u(PQx^-s)697TX`ehR4?GS^qbkof1cslKgkU)h65qZ9Oc=ml_0temigYLJfnz{IDzUf>bGs4N!v3=Z3jMq&A#7%rM5eQ#dc?k~! zVpnB`o+K7|Al`Q_U;eD$B zfJtP*jH`siUq~{KE)`jP2|#TUEFGRryE2`i0**z#*^6~AI|YzIWy$Cu#CSLW3q=GA z6`?GZymC;dCPk~rBS%eCb`5OLr;RUZ;D`}um=H)BfVIq%7VhiMr)_#G0N#zrNH|__ zc+blN2UAB0=617@>_u;MPHN;P;N#YoE=)R#i$k_`UAA>WWCcEVMh~L_ zj--gtp&|K1#58Yz*AHCTMziU1Jzt_jG0I@qAOHsk$2}yTmVkBp_eHuY$A9)>P6o~I z%aQ?!(GqeQ-Y+b0I(m9pwgi(IIZZzsbMv+9w{PFtd_<_(LA~0H(xz{=FhLB@(1&qHA5EJw1>>=%q2f&^X>IQ{!GJ4e9U z&KlB)z(84HmNgm2hg2C0>WM{E(DdPr+EeU_N@57;PC2&DmGFW_9kP&%?X4}+xWi)( z;)z%wI5>D4a*5XwD)P--sPkoY(a~WBw;E~AW`Yue4kFa^LM3X`8x|}ZUeMnqr}>kH zG%WWW>3ml$Yez?i%)2pbKPI7?5o?hydokgQyZsNEr{a|mLdt;X2TX(#B1j35xPnPW z*bMSSOauW>o;*=kO8ojw91VX!qoOQb)zHJ!odWB}d+*K?#sY_jqPdg{Sm2HdYzdEx zOGVPhVRTGPtv0o}RfVP;Nd(|CB)I;*t&QO8h zFfekr30S!-LHmV_Su-W+rEwYXJ^;6&3|L$mMC8*bQptyOo9;>Qb9Q9`ySe3%V$A*9 zeKEe+b0{#KWGp$F+tga)0RtI)nhMa-K@JS}2krK~n8vJ=Ngm?R!9G<~RyuU0d?nz# z-5EK$o(!F?hmX*2Yt6+coY`6jGbb7tF#6nHA zuKk=GGJ;ZwON1iAfG$E#Y7MnZVmrY|j0eVI(DN_MNFJmyZ|;w4tf@=CCDZ#5N_0K= z$;R~bbk?}TpfDjfB&aiQ$VA}s?P}xPERJG{kxk5~R`iRS(SK5d+Xs9swCozZISbnS zk!)I0>t=A<-^z(cmSFz3=jZ23u13X><0b)P)^1T_))Kr`e!-pb#q&J*Q`p+B6la%C zuVl&0duN<;uOsB3%T9Fp8t{ED108<+W(nOZd?gDnfNBC3>M8WE61$So|P zVvqH0SNtDTcsUdzaMDpT=Ty0pDHHNL@Z0w$Y`XO z2M-_r1S+GaH%pz#Uy0*w$Vdl=X=rQXEzO}d6J^R6zjM1u&c9vYLvLp?W7w(?np9x1 zE_0JSAJCPB%i7p*Wvg)pn5T`8k3-uR?*NT|J`eS#_#54p>!p(mLDvmc-3o0mX*mp_ zN*AeS<>#^-{S%W<*mz^!X$w_2dHWpcJ6^j64qFBft-o}o_Vx80o0>}Du;>kLts;$8 zC`7q$QI(dKYG`Wa8#wl@V4jVWBRGQ@1dr-hstpQL)Tl+aqVpGpbSfN>5i&QMXfiZ> zaA?T1VGe?rpQ@;+pkrVdd{klI&jVS@I5_iz!=UMpTsa~mBga?1r}aRBm1WS;TT*s0f0lY=JBl66Upy)-k4J}lh=P^8(SXk~0xW=T9v*B|gzIhN z>qsO7dFd~mgxAy4V?&)=5ieYq?zi?ZEoj)&2o)RLy=@hbCRcfT5jigwtQGE{L*8<@Yd{zg;CsL5mvzfDY}P-wos_6PfprFVaeqNE%h zKZhLtcQld;ZD+>=nqN~>GvROfueSzJD&BE*}XfU|H&(FssBqY=hPCt`d zH?@s2>I(|;fcW&YM6#V#!kUIP8$Nkdh0A(bEVj``-AAyYgwY~jB zT|I7Bf@%;7aL7Wf4dZ%VqF$eiaC38OV6oy3Z#TER2G+fOCd9Iaoy6aLYbPTN{XRPz z;U!V|vBf%H!}52L2gH_+j;`bTcQRXB+y9onc^wLm5wi3-Be}U>k_u>2Eg$=k!(l@I zcCg+flakT2Nej3i0yn+g+}%NYb?ta;R?(g5SnwsQ49U8Wng8d|{B+lyRcEDvR3+`O{zfmrmvFrL6acVP%yG98X zo&+VBg@px@i)%o?dG(`T;n*$S5*rnyiR#=wW}}GsAcfyQpE|>a{=$Hjg=-*_K;UtD z#z-)AXwSRY?OPefw^iI+ z)AXz#PfEjlwTes|_{sB?4(O@fg0AJ^g8gP}ex9Ucf*@_^J(s_5jJV}c)s$`Myn|Kd z$6>}#q^n{4vN@+Os$m7KV+`}c%4)4pv@06af4-x5#wj!KKb%caK{A&Y#Rfs z-po?Dcb1({W=6FKIUirH&(yg=*6aLCekcKwyfK^JN5{wcA3nhO(o}SK#!CINhI`-I z1)6&n7O&ZmyFMuNwvEic#IiOAwNkR=u5it{B9n2sAJV5pNhar=j5`*N!Na;c7g!l$ z3aYBqUkqqTJ=Re-;)s!EOeij=7SQZ3Hq}ZRds%IM*PtM$wV z@;rlc*NRK7i3y5BETSKuumEN`Xu_8GP1Ri=OKQ$@I^ko8>H6)4rjiG5{VBM>B|%`&&s^)jS|-_95&yc=GqjNo{zFkw%%HHhS~e=s zD#sfS+-?*t|J!+ozP6KvtOl!R)@@-z24}`9{QaVLD^9VCSR2b`b!KC#o;Ki<+wXB6 zx3&O0LOWcg4&rv4QG0)4yb}7BFSEg~=IR5#ZRj8kg}dS7_V&^%#Do==#`u zpy6{ox?jWuR(;pg+f@mT>#HGWHAJRRDDDv~@(IDw&R>9643kK#HN`!1vBJHnC+RM&yIh8{gG2q zA%e*U3|N0XSRa~oX-3EAneep)@{h2vvd3Xvy$7og(sayr@95+e6~Xvi1tUqnIxoIH zVWo*OwYElb#uyW{Imam6f2rGbjR!Y3`#gPqkv57dB6K^wRGxc9B(t|aYDGS=m$&S!NmCtrMMaUg(c zc2qC=2Z`EEFMW-me5B)24AqF*bV5Dr-M5ig(l-WPS%CgaPzs6p_gnCIvTJ=Y<6!gT zVt@AfYCzjjsMEGi=rDQHo0yc;HqoRNnNFeWZgcm?f;cp(6CNylj36DoL(?TS7eU#+ z7&mfr#y))+CJOXQKUMZ7QIdS9@#-}7y2K1{8)cCt0~-X0O!O?Qx#E4Og+;A2SjalQ zs7r?qn0H044=sDN$SRG$arw~n=+T_DNdSrarmu)V6@|?1-ZB#hRn`uilTGPJ@fqEy zGt(f0B+^JDP&f=r{#Y_wi#AVDf-y!RIXU^0jXsFpf>=Ji*TeqSY!H~AMbJdCGLhC) zn7Rx+sXw6uYj;WRYrLd^5IZq@6JI1C^YkgnedZEYy<&4(z%Q$5yv#Boo{AH8n$a zhb4Y3PWdr269&?V%uI$xMcUrMzl=;w<_nm*qr=c3Rl@i5wWB;e-`t7D&c-mcQl7x! zZWB`UGcw=Y2=}~wzrfLx=uet<;m3~=8I~ZRuzvMQUQdr+yTV|ATf1Uuomr__nDf=X zZ3WYJtHp_ri(}SQAPjv+Y+0=fH4krOP@S&=zZ-t1jW1o@}z;xk8 z(Nz1co&El^HK^NrhVHa-_;&88vTU>_J33=%{if;BEY*J#1n59=07jrGQ#IP>@u#3A z;!q+E1Rj3ZJ+!4bq9F8PXJ@yMgZL;>&gYA0%_Kbi8?S=XGM~dnQZQ!yBSgcZhY96H zrWnU;k)qy`rX&&xlDyA%(a1Hhi5CWkmg(`Gb%m(HKi-7Z!LKGRP_B8@`7&hdDy5n= z`OIxqxiVfX@OX1p(mQu>0Ai*v_cTMiw4qRt3~NBvr9oBy0)r>w3p~V0SCm=An6@3n)>@z!|o-$HvDK z|3D2ZMJkLE5loMKl6R^ez@Zz%S$&mbeoqH5`Bb){Ei21q&VP)hWS2tjShfFtGE+$z zzCR$P#uktu+#!w)cX!lWN1XU%K-r=s{|j?)Akf@q#3b#{6cZCuJ~gCxuMXRmI$nGtnH+-h z+GEi!*X=AP<|fG`1>MBdTb?28JYc=fGvAi2I<$B(rs$;eoJCyR6_bc~p!XR@O-+sD z=eH`-ye})I5ic1eL~TDmtfJ|8`0VJ*Yr=hNCd)G1p2MMz4C3^Mj?7;!w|Ly%JqmuW zlIEW^Ft%z?*|fpXda>Jr^1noFZEwFgVV%|*XhH@acv8rdGxeEX{M$(vG{Zw+x(ei@ zmfXb22}8-?Fi`vo-YVrTH*C?a8%M=Hv9MqVH7H^J$KsD?>!SFZ;ZsvnHr_gn=7acz z#W?0eCdVhVMWN12VV^$>WlQ?f;P^{(&pYTops|btm6aj>_Uz+hqpGwB)vWp0Cf5y< zft8-je~nn?W11plq}N)4A{l8I7$!ks_x$PXW-2XaRFswX_BnF{R#6YIwMhAgd5F9X zGmwdadS6(a^fjHtXg8=l?Rc0Sm%hk6E9!5cLVloEy4eh(=FwgP`)~I^5~pBEWo+F6 zSf2ncyMurJN91#cJTy_u8Y}@%!bq1RkGC~-bV@SXRd4F{R-*V`bS+6;W5vZ(&+I<9$;-V|eNfLa5n-6% z2(}&uGRF;p92eS*sE*oR$@pexaqr*meB)VhmIg@h{uzkk$9~qh#cHhw#>O%)b@+(| z^IQgqzuj~Sk(J;swEM-3TrJAPCq9k^^^`q{IItKBRXYe}e0Tdr=Huf7da3$l4PdpwWDop%^}n;dD#K4s#DYA8SHZ z&1!riV4W4R7R#C))JH1~axJ)RYnM$$lIR%6fIVA@zV{XVyx}C+a-Dt8Y9M)^KU0+H zR4IUb2CJ{Hg>CuaXtD50jB(_Tcx=Z$^WYu2u5kubqmwp%drJ6 z?Fo40g!Qd<-l=TQxqHEOuPX0;^z7iX?Ke^a%XT<13TA^5`4Xcw6D@Ur&VT&CUe0d} z1GjOVF1^L@>O)l@?bD~$wzgf(nxX1OGD8fEV?TdJcZc2KoUe|oP1#=$$7ee|xbY)A zDZq+cuTpc(fFdj^=!;{k03C69lMQ(|>uhRfRu%+!k&YOi-3|1QKB z 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 Xviia!U7SGha1wx#SCgwmn*{w2TRX*I diff --git a/shopfloor_base/views/menus.xml b/shopfloor_base/views/menus.xml index 26f5c3dcb0..6c1691286c 100644 --- a/shopfloor_base/views/menus.xml +++ b/shopfloor_base/views/menus.xml @@ -1,10 +1,12 @@ - - - - + Date: Tue, 15 Feb 2022 16:29:26 +0000 Subject: [PATCH 054/111] [UPD] README.rst --- shopfloor_base/README.rst | 4 ++++ shopfloor_base/static/description/index.html | 4 ++++ 2 files changed, 8 insertions(+) diff --git a/shopfloor_base/README.rst b/shopfloor_base/README.rst index 42cc4bb137..5be83027f1 100644 --- a/shopfloor_base/README.rst +++ b/shopfloor_base/README.rst @@ -148,6 +148,10 @@ Other credits * Akretion R&D * ACSONE R&D +**Icons** + +* Tablet app icon by Gregor Cresnar from the Noun Project + Maintainers ~~~~~~~~~~~ diff --git a/shopfloor_base/static/description/index.html b/shopfloor_base/static/description/index.html index 4e7b4438e1..506c27ac1a 100644 --- a/shopfloor_base/static/description/index.html +++ b/shopfloor_base/static/description/index.html @@ -498,6 +498,10 @@

      Other credits

    • Akretion R&D
    • ACSONE R&D
    +

    Icons

    +
      +
    • Tablet app icon by Gregor Cresnar from the Noun Project
    • +

    Maintainers

    From b22940e134a4d4f7afad3bf8fc445f2e881eb695 Mon Sep 17 00:00:00 2001 From: OCA-git-bot Date: Tue, 15 Feb 2022 16:29:34 +0000 Subject: [PATCH 055/111] shopfloor_base 14.0.1.2.1 --- shopfloor_base/__manifest__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/shopfloor_base/__manifest__.py b/shopfloor_base/__manifest__.py index 3f55a83a65..e9063f1de4 100644 --- a/shopfloor_base/__manifest__.py +++ b/shopfloor_base/__manifest__.py @@ -6,7 +6,7 @@ { "name": "Shopfloor Base", "summary": "Core module for creating mobile apps", - "version": "14.0.1.2.0", + "version": "14.0.1.2.1", "development_status": "Beta", "category": "Inventory", "website": "https://github.com/OCA/wms", From e593a3ad51bfc8f50820f9888e7ce5e69b6ecf3b Mon Sep 17 00:00:00 2001 From: Pierre Verkest Date: Wed, 16 Feb 2022 19:40:24 +0100 Subject: [PATCH 056/111] [14.0] shopfloor_base: add icon.svg file used to generate .png file --- shopfloor_base/static/description/icon.png | Bin 15812 -> 5216 bytes shopfloor_base/static/description/icon.svg | 119 +++++++++++++++++++++ 2 files changed, 119 insertions(+) create mode 100644 shopfloor_base/static/description/icon.svg diff --git a/shopfloor_base/static/description/icon.png b/shopfloor_base/static/description/icon.png index 9e308a5440ef1a5865c06d41069f2ae987965050..5df155ee5a2e8e33c1b8b31ce0e3060d867ec0c0 100644 GIT binary patch literal 5216 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 + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + From f0e3e889bdd6e6bb4ea737e6cc87bf92290ec1bf Mon Sep 17 00:00:00 2001 From: OCA-git-bot Date: Fri, 11 Mar 2022 16:53:01 +0000 Subject: [PATCH 057/111] shopfloor_base 14.0.1.2.2 --- shopfloor_base/__manifest__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/shopfloor_base/__manifest__.py b/shopfloor_base/__manifest__.py index e9063f1de4..910d84da57 100644 --- a/shopfloor_base/__manifest__.py +++ b/shopfloor_base/__manifest__.py @@ -6,7 +6,7 @@ { "name": "Shopfloor Base", "summary": "Core module for creating mobile apps", - "version": "14.0.1.2.1", + "version": "14.0.1.2.2", "development_status": "Beta", "category": "Inventory", "website": "https://github.com/OCA/wms", From 53a56342f7340c8aa410c6045489d137ee27d730 Mon Sep 17 00:00:00 2001 From: Simone Orsi Date: Thu, 14 Oct 2021 17:50:22 +0200 Subject: [PATCH 058/111] shopfloor_base: add app backend This is the base to provide specific settings by app. For instance, today you cannot control which users can use the app or provide any extra global settings without adding a specific parameters. Opens the door to better management of the app and to manage multiple apps on the same instance. --- shopfloor_base/__manifest__.py | 1 + shopfloor_base/controllers/main.py | 8 ++- shopfloor_base/models/__init__.py | 2 + shopfloor_base/models/ir_http.py | 44 +++++++++++++ shopfloor_base/models/shopfloor_app.py | 47 ++++++++++++++ shopfloor_base/security/ir.model.access.csv | 2 + shopfloor_base/services/app.py | 6 +- shopfloor_base/services/menu.py | 6 +- shopfloor_base/services/profile.py | 6 +- shopfloor_base/services/scan_anything.py | 2 +- shopfloor_base/services/service.py | 2 +- shopfloor_base/services/validator.py | 6 +- shopfloor_base/tests/common.py | 3 +- shopfloor_base/views/menus.xml | 6 ++ shopfloor_base/views/shopfloor_app.xml | 68 +++++++++++++++++++++ 15 files changed, 191 insertions(+), 18 deletions(-) create mode 100644 shopfloor_base/models/ir_http.py create mode 100644 shopfloor_base/models/shopfloor_app.py create mode 100644 shopfloor_base/views/shopfloor_app.xml diff --git a/shopfloor_base/__manifest__.py b/shopfloor_base/__manifest__.py index 910d84da57..ef269d5c79 100644 --- a/shopfloor_base/__manifest__.py +++ b/shopfloor_base/__manifest__.py @@ -19,6 +19,7 @@ "data/module_category_data.xml", "security/groups.xml", "security/ir.model.access.csv", + "views/shopfloor_app.xml", "views/shopfloor_menu.xml", "views/shopfloor_scenario_views.xml", "views/shopfloor_profile_views.xml", diff --git a/shopfloor_base/controllers/main.py b/shopfloor_base/controllers/main.py index f7bc009ec5..e25f81f122 100644 --- a/shopfloor_base/controllers/main.py +++ b/shopfloor_base/controllers/main.py @@ -6,6 +6,8 @@ class ShopfloorController(main.RestController): - _root_path = "/shopfloor/" - _collection_name = "shopfloor.service" - _default_auth = "api_key" + _root_path = "/shopfloor//" + # Useless when calling via real request + # as the collection will come from model converter + _collection_name = "shopfloor.app" + _default_auth = "user" diff --git a/shopfloor_base/models/__init__.py b/shopfloor_base/models/__init__.py index df7a330e39..31edaf457b 100644 --- a/shopfloor_base/models/__init__.py +++ b/shopfloor_base/models/__init__.py @@ -1,3 +1,5 @@ +from . import shopfloor_app from . import shopfloor_menu from . import shopfloor_profile from . import shopfloor_scenario +from . import ir_http diff --git a/shopfloor_base/models/ir_http.py b/shopfloor_base/models/ir_http.py new file mode 100644 index 0000000000..07c7eca97f --- /dev/null +++ b/shopfloor_base/models/ir_http.py @@ -0,0 +1,44 @@ +# Copyright 2021 Camptocamp SA (http://www.camptocamp.com) +# @author Simone Orsi +# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html). +import werkzeug + +from odoo import api, models +from odoo.http import request + +from odoo.addons.base.models.ir_http import RequestUID + + +class TechNameConverter(werkzeug.routing.BaseConverter): + """Record converter via tech_name field. + + Similar to the standard model converter but uses the `tech_name` field + of a model to retrieve the record. + """ + + def __init__(self, url_map, model=False): + super().__init__(url_map) + self.model = model + + def to_python(self, value): + # First lookup for the query + query = ( + request.env[self.model].sudo()._search([("tech_name", "=", value)], limit=1) + ) + # Then browse the record w/ proper environment (as per ModelConverter) + _uid = RequestUID(value=value, converter=self) + env = api.Environment(request.cr, _uid, request.context) + return env[self.model].browse(query) + + def to_url(self, value): + return value.tech_name + + +class IrHttp(models.AbstractModel): + _inherit = "ir.http" + + @classmethod + def _get_converters(cls): + converters = super()._get_converters() + converters["tech_name"] = TechNameConverter + return converters diff --git a/shopfloor_base/models/shopfloor_app.py b/shopfloor_base/models/shopfloor_app.py new file mode 100644 index 0000000000..cb514c731e --- /dev/null +++ b/shopfloor_base/models/shopfloor_app.py @@ -0,0 +1,47 @@ +# Copyright 2021 Camptocamp SA (http://www.camptocamp.com) +# @author Simone Orsi +# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html). +from odoo import fields, models + + +class ShopfloorApp(models.Model): + """Backend for a Shopfloor app.""" + + _name = "shopfloor.app" + _inherit = "collection.base" + _description = "A Shopfloor application" + + # TODO: attach and load menu items and other records + # from the specific shopfloor.app (aka current collection) + name = fields.Char(required=True, translate=True) + short_name = fields.Char(required=True, translate=True) + # Unique name + tech_name = fields.Char(required=True) + active = fields.Boolean(default=True) + api_route = fields.Char( + compute="_compute_api_route", + help="Base route for endpoints attached to this app.", + ) + url = fields.Char(compute="_compute_url", help="Public URL to use the app.") + + _sql_constraints = [("tech_name", "unique(tech_name)", "tech_name must be unique")] + + _api_route_path = "/shopfloor/" + + def _compute_api_route(self): + for rec in self: + rec.api_route = rec._api_route_path + rec.tech_name + + _base_url_path = "/shopfloor/app/" + + def _compute_url(self): + for rec in self: + rec.url = rec._base_url_path + rec.tech_name + + def action_open_app(self): + return { + "type": "ir.actions.act_url", + "name": self.name, + "url": self.url, + "target": "new", + } diff --git a/shopfloor_base/security/ir.model.access.csv b/shopfloor_base/security/ir.model.access.csv index b8d48e0f40..45768938f6 100644 --- a/shopfloor_base/security/ir.model.access.csv +++ b/shopfloor_base/security/ir.model.access.csv @@ -5,3 +5,5 @@ "access_shopfloor_profile_manager","shopfloor profile manager","model_shopfloor_profile","shopfloor_base.group_shopfloor_manager",1,1,1,1 "access_shopfloor_scenario_users","shopfloor scenario","model_shopfloor_scenario","shopfloor_base.group_shopfloor_user",1,0,0,0 "access_shopfloor_scenario_manager","shopfloor scenario manager","model_shopfloor_scenario","shopfloor_base.group_shopfloor_manager",1,1,1,1 +"access_shopfloor_app_users","shopfloor app","model_shopfloor_app","",1,0,0,0 +"access_shopfloor_app_manager","shopfloor app manager","model_shopfloor_app","shopfloor_base.group_shopfloor_manager",1,1,1,1 diff --git a/shopfloor_base/services/app.py b/shopfloor_base/services/app.py index 1387c749ee..412100c925 100644 --- a/shopfloor_base/services/app.py +++ b/shopfloor_base/services/app.py @@ -7,7 +7,7 @@ class ShopfloorApp(Component): """Generic endpoints for the Application.""" _inherit = "base.shopfloor.service" - _name = "shopfloor.app" + _name = "shopfloor.service.app" _usage = "app" _description = __doc__ @@ -25,7 +25,7 @@ class ShopfloorAppValidator(Component): """Validators for the Application endpoints""" _inherit = "base.shopfloor.validator" - _name = "shopfloor.app.validator" + _name = "shopfloor.service.app.validator" _usage = "app.validator" def user_config(self): @@ -36,7 +36,7 @@ class ShopfloorAppValidatorResponse(Component): """Validators for the Application endpoints responses""" _inherit = "base.shopfloor.validator.response" - _name = "shopfloor.app.validator.response" + _name = "shopfloor.service.app.validator.response" _usage = "app.validator.response" def user_config(self): diff --git a/shopfloor_base/services/menu.py b/shopfloor_base/services/menu.py index ce220c0274..97e6444f5c 100644 --- a/shopfloor_base/services/menu.py +++ b/shopfloor_base/services/menu.py @@ -15,7 +15,7 @@ class ShopfloorMenu(Component): """ _inherit = "base.shopfloor.service" - _name = "shopfloor.menu" + _name = "shopfloor.service.menu" _usage = "menu" _expose_model = "shopfloor.menu" _description = __doc__ @@ -73,7 +73,7 @@ class ShopfloorMenuValidator(Component): """Validators for the Menu endpoints""" _inherit = "base.shopfloor.validator" - _name = "shopfloor.menu.validator" + _name = "shopfloor.service.menu.validator" _usage = "menu.validator" def search(self): @@ -86,7 +86,7 @@ class ShopfloorMenuValidatorResponse(Component): """Validators for the Menu endpoints responses""" _inherit = "base.shopfloor.validator.response" - _name = "shopfloor.menu.validator.response" + _name = "shopfloor.service.menu.validator.response" _usage = "menu.validator.response" def return_search(self): diff --git a/shopfloor_base/services/profile.py b/shopfloor_base/services/profile.py index 3c0ea4e437..793c0e3a26 100644 --- a/shopfloor_base/services/profile.py +++ b/shopfloor_base/services/profile.py @@ -15,7 +15,7 @@ class ShopfloorProfile(Component): """ _inherit = "base.shopfloor.service" - _name = "shopfloor.profile" + _name = "shopfloor.service.profile" _usage = "profile" _expose_model = "shopfloor.profile" _description = __doc__ @@ -52,7 +52,7 @@ class ShopfloorProfileValidator(Component): """Validators for the Profile endpoints""" _inherit = "base.shopfloor.validator" - _name = "shopfloor.profile.validator" + _name = "shopfloor.service.profile.validator" _usage = "profile.validator" def search(self): @@ -65,7 +65,7 @@ class ShopfloorProfileValidatorResponse(Component): """Validators for the Profile endpoints responses""" _inherit = "base.shopfloor.validator.response" - _name = "shopfloor.profile.validator.response" + _name = "shopfloor.service.profile.validator.response" _usage = "profile.validator.response" def search(self): diff --git a/shopfloor_base/services/scan_anything.py b/shopfloor_base/services/scan_anything.py index 10e7767542..fa76759803 100644 --- a/shopfloor_base/services/scan_anything.py +++ b/shopfloor_base/services/scan_anything.py @@ -76,7 +76,7 @@ class ShopfloorScanAnythingHandler(AbstractComponent): _name = "shopfloor.scan.anything.handler" _usage = "scan_anything.handler" _description = __doc__ - _collection = "shopfloor.service" + _collection = "shopfloor.app" _actions_collection_name = "shopfloor.action" @property diff --git a/shopfloor_base/services/service.py b/shopfloor_base/services/service.py index 88caa9eb7c..ad7bf78d73 100644 --- a/shopfloor_base/services/service.py +++ b/shopfloor_base/services/service.py @@ -19,7 +19,7 @@ class BaseShopfloorService(AbstractComponent): _inherit = "base.rest.service" _name = "base.shopfloor.service" - _collection = "shopfloor.service" + _collection = "shopfloor.app" _expose_model = None def dispatch(self, method_name, *args, params=None): diff --git a/shopfloor_base/services/validator.py b/shopfloor_base/services/validator.py index 9691c3cfa0..330fb80c62 100644 --- a/shopfloor_base/services/validator.py +++ b/shopfloor_base/services/validator.py @@ -51,7 +51,7 @@ class ShopfloorRestCerberusValidator(Component): _name = "shopfloor.rest.cerberus.validator" _inherit = "base.rest.cerberus.validator" _usage = "cerberus.validator" - _collection = "shopfloor.service" + _collection = "shopfloor.app" _is_rest_service_component = False def _get_validator_component(self, service, method_name, direction): @@ -109,7 +109,7 @@ class BaseShopfloorValidator(AbstractComponent): _inherit = "base.rest.service" _name = "base.shopfloor.validator" - _collection = "shopfloor.service" + _collection = "shopfloor.app" _is_rest_service_component = False @@ -155,7 +155,7 @@ class BaseShopfloorValidatorResponse(AbstractComponent): _inherit = "base.rest.service" _name = "base.shopfloor.validator.response" - _collection = "shopfloor.service" + _collection = "shopfloor.app" _is_rest_service_component = False # Initial state of a workflow diff --git a/shopfloor_base/tests/common.py b/shopfloor_base/tests/common.py index 71394df168..0eec979fcf 100644 --- a/shopfloor_base/tests/common.py +++ b/shopfloor_base/tests/common.py @@ -57,7 +57,8 @@ class CommonCase(SavepointCase, RegistryMixin, ComponentMixin): @contextmanager def work_on_services(self, env=None, **params): params = params or {} - collection = _PseudoCollection("shopfloor.service", env or self.env) + # TODO: test specific shopfloor.app record + collection = _PseudoCollection("shopfloor.app", env or self.env) yield WorkContext( model_name="rest.service.registration", collection=collection, diff --git a/shopfloor_base/views/menus.xml b/shopfloor_base/views/menus.xml index 6c1691286c..d3c9ae05db 100644 --- a/shopfloor_base/views/menus.xml +++ b/shopfloor_base/views/menus.xml @@ -7,6 +7,12 @@ sequence="300" web_icon="shopfloor_base,static/description/icon.png" /> + + + + shopfloor app tree + shopfloor.app + + + + + + + + + + shopfloor app form + shopfloor.app + +
    + + + +
    + -
    From 0185b70e27a38756e8f3c30bf49eff56fe51c03a Mon Sep 17 00:00:00 2001 From: Simone Orsi Date: Sat, 20 Nov 2021 21:27:35 +0100 Subject: [PATCH 072/111] shopfloor_base: apps menu 1st --- shopfloor_base/views/menus.xml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/shopfloor_base/views/menus.xml b/shopfloor_base/views/menus.xml index d3c9ae05db..174af907ce 100644 --- a/shopfloor_base/views/menus.xml +++ b/shopfloor_base/views/menus.xml @@ -11,25 +11,25 @@ action="action_shopfloor_app" id="menu_action_shopfloor_app" parent="menu_shopfloor_root" - sequence="30" + sequence="10" />
    From 61d3de3142cb788b866c7cf8b6c925793d7954c8 Mon Sep 17 00:00:00 2001 From: Simone Orsi Date: Sat, 20 Nov 2021 21:28:12 +0100 Subject: [PATCH 073/111] shopfloor_base: fix test service setup --- shopfloor_base/tests/common.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/shopfloor_base/tests/common.py b/shopfloor_base/tests/common.py index 2dc0520554..314daa8354 100644 --- a/shopfloor_base/tests/common.py +++ b/shopfloor_base/tests/common.py @@ -55,12 +55,11 @@ class CommonCase(SavepointCase, RegistryMixin, ComponentMixin): maxDiff = None @contextmanager - def work_on_services(self, env=None, **params): + def work_on_services(self, collection=None, env=None, **params): params = params or {} - collection = _PseudoCollection("shopfloor.app", env or self.env) yield WorkContext( model_name="rest.service.registration", - collection=collection, + collection=collection or self.shopfloor_app, # No need for a real request mock # as we don't deal w/ real request for testing # but base_rest context provider needs it. @@ -105,6 +104,9 @@ def setUpClass(cls): ) ) + cls.shopfloor_app = cls.env["shopfloor.app"].search( + [("tech_name", "=", "default")], limit=1 + ) cls.setUpComponent() cls.setUpRegistry() cls.setUpClassUsers() From 44962183bf43fa5344e02cff9d5298716754b801 Mon Sep 17 00:00:00 2001 From: Simone Orsi Date: Fri, 26 Nov 2021 14:02:18 +0100 Subject: [PATCH 074/111] shopfloor_base: get rid of api key left overs --- shopfloor_base/__manifest__.py | 3 --- shopfloor_base/data/shopfloor_app_data.xml | 7 ------- shopfloor_base/demo/auth_api_key_demo.xml | 7 ------- 3 files changed, 17 deletions(-) delete mode 100644 shopfloor_base/data/shopfloor_app_data.xml delete mode 100644 shopfloor_base/demo/auth_api_key_demo.xml diff --git a/shopfloor_base/__manifest__.py b/shopfloor_base/__manifest__.py index b14f6f33b7..dcbd0fe661 100644 --- a/shopfloor_base/__manifest__.py +++ b/shopfloor_base/__manifest__.py @@ -18,12 +18,10 @@ "base_jsonify", "base_rest", "base_sparse_field", - "auth_api_key", # FIXME: remove "endpoint_route_handler", ], "data": [ "data/module_category_data.xml", - "data/shopfloor_app_data.xml", "security/groups.xml", "security/ir.model.access.csv", "views/shopfloor_app.xml", @@ -34,7 +32,6 @@ ], "demo": [ "demo/res_users_demo.xml", - "demo/auth_api_key_demo.xml", "demo/shopfloor_scenario_demo.xml", "demo/shopfloor_menu_demo.xml", "demo/shopfloor_profile_demo.xml", diff --git a/shopfloor_base/data/shopfloor_app_data.xml b/shopfloor_base/data/shopfloor_app_data.xml deleted file mode 100644 index dcb8dd3f68..0000000000 --- a/shopfloor_base/data/shopfloor_app_data.xml +++ /dev/null @@ -1,7 +0,0 @@ - - - Shopfloor - default - default - - diff --git a/shopfloor_base/demo/auth_api_key_demo.xml b/shopfloor_base/demo/auth_api_key_demo.xml deleted file mode 100644 index 7858d1d044..0000000000 --- a/shopfloor_base/demo/auth_api_key_demo.xml +++ /dev/null @@ -1,7 +0,0 @@ - - - Demo - - 72B044F7AC780DAC - - From bd60bcd589352bf9a0e42477ddaf1df92daaaa21 Mon Sep 17 00:00:00 2001 From: Simone Orsi Date: Fri, 26 Nov 2021 14:05:35 +0100 Subject: [PATCH 075/111] shopflor.app: add category --- shopfloor_base/models/shopfloor_app.py | 5 ++++- shopfloor_base/views/shopfloor_app.xml | 11 +++++++++++ 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/shopfloor_base/models/shopfloor_app.py b/shopfloor_base/models/shopfloor_app.py index 0569c50172..073bf12afb 100644 --- a/shopfloor_base/models/shopfloor_app.py +++ b/shopfloor_base/models/shopfloor_app.py @@ -36,10 +36,13 @@ class ShopfloorApp(models.Model): _description = "A Shopfloor application" name = fields.Char(required=True, translate=True) - short_name = fields.Char(required=True, translate=True) + short_name = fields.Char( + required=True, translate=True, help="Needed for app manifest" + ) # Unique name tech_name = fields.Char(required=True, index=True) active = fields.Boolean(default=True) + category = fields.Selection(selection=[("", "None")]) api_route = fields.Char( compute="_compute_api_route", compute_sudo=True, diff --git a/shopfloor_base/views/shopfloor_app.xml b/shopfloor_base/views/shopfloor_app.xml index 1f86896fba..3bee71487c 100644 --- a/shopfloor_base/views/shopfloor_app.xml +++ b/shopfloor_base/views/shopfloor_app.xml @@ -46,6 +46,7 @@ + @@ -74,6 +75,16 @@ + + + + + Date: Fri, 26 Nov 2021 16:17:27 +0100 Subject: [PATCH 076/111] shopfloor.app: add btn to view menu items --- shopfloor_base/models/shopfloor_app.py | 10 ++++++++++ shopfloor_base/views/shopfloor_app.xml | 6 ++++++ 2 files changed, 16 insertions(+) diff --git a/shopfloor_base/models/shopfloor_app.py b/shopfloor_base/models/shopfloor_app.py index 073bf12afb..2d6587dc75 100644 --- a/shopfloor_base/models/shopfloor_app.py +++ b/shopfloor_base/models/shopfloor_app.py @@ -130,6 +130,16 @@ def action_open_app_docs(self): "target": "new", } + def action_view_menu_items(self): + xid = "shopfloor_base.action_shopfloor_menu" + action = self.env["ir.actions.act_window"]._for_xml_id(xid) + action["domain"] = [ + "|", + ("id", "in", self.profile_ids.menu_ids.ids), + ("profile_id", "=", False), + ] + return action + # TODO: move to shopfloor_app_base? or just `app_base`? @api.model_create_multi def create(self, vals_list): diff --git a/shopfloor_base/views/shopfloor_app.xml b/shopfloor_base/views/shopfloor_app.xml index 3bee71487c..e81f9b503f 100644 --- a/shopfloor_base/views/shopfloor_app.xml +++ b/shopfloor_base/views/shopfloor_app.xml @@ -56,6 +56,12 @@ +