From 745670806777df58e5048181a315f4f73ec189e5 Mon Sep 17 00:00:00 2001 From: Alexandre Fayolle Date: Mon, 20 Jan 2020 15:58:16 +0100 Subject: [PATCH 001/986] add empty files --- shopfloor/__init__.py | 0 shopfloor/__manifest__.py | 0 shopfloor/models/__init__.py | 0 3 files changed, 0 insertions(+), 0 deletions(-) create mode 100644 shopfloor/__init__.py create mode 100644 shopfloor/__manifest__.py create mode 100644 shopfloor/models/__init__.py diff --git a/shopfloor/__init__.py b/shopfloor/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/shopfloor/__manifest__.py b/shopfloor/__manifest__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/shopfloor/models/__init__.py b/shopfloor/models/__init__.py new file mode 100644 index 00000000000..e69de29bb2d From 678755f1ebf5e5bf91e551bd29ebd7bd45fd2f31 Mon Sep 17 00:00:00 2001 From: Alexandre Fayolle Date: Mon, 20 Jan 2020 16:08:05 +0100 Subject: [PATCH 002/986] add some boilerplate --- oca_dependencies.txt | 1 + shopfloor/__init__.py | 1 + shopfloor/__manifest__.py | 20 ++++++++++++++++++++ shopfloor/readme/CONFIGURE.rst | 1 + shopfloor/readme/CONTRIBUTORS.rst | 3 +++ shopfloor/readme/DESCRIPTION.rst | 1 + shopfloor/readme/HISTORY.rst | 4 ++++ shopfloor/readme/ROADMAP.rst | 1 + shopfloor/readme/USAGE.rst | 5 +++++ 9 files changed, 37 insertions(+) create mode 100644 oca_dependencies.txt create mode 100644 shopfloor/readme/CONFIGURE.rst create mode 100644 shopfloor/readme/CONTRIBUTORS.rst create mode 100644 shopfloor/readme/DESCRIPTION.rst create mode 100644 shopfloor/readme/HISTORY.rst create mode 100644 shopfloor/readme/ROADMAP.rst create mode 100644 shopfloor/readme/USAGE.rst diff --git a/oca_dependencies.txt b/oca_dependencies.txt new file mode 100644 index 00000000000..b56ff1aa4e9 --- /dev/null +++ b/oca_dependencies.txt @@ -0,0 +1 @@ +rest-framework diff --git a/shopfloor/__init__.py b/shopfloor/__init__.py index e69de29bb2d..0650744f6bc 100644 --- a/shopfloor/__init__.py +++ b/shopfloor/__init__.py @@ -0,0 +1 @@ +from . import models diff --git a/shopfloor/__manifest__.py b/shopfloor/__manifest__.py index e69de29bb2d..5cc45996a68 100644 --- a/shopfloor/__manifest__.py +++ b/shopfloor/__manifest__.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +# © 2020 Camptocamp, Akretion, BCIM +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). + +{ + "name": "Shopfloor", + "summary": "manage warehouse operations with barcode scanners", + "version": "13.0.1.0.0", + "category": "Inventory", + "website": "https://odoo-community.org", + "author": "Akretion, BCIM, Camptocamp, Odoo Community Association (OCA)", + "licence": "AGPL-3", + "application": True, + "depends": [ + "stock", + "base_rest", + ], + "data": [ + ], +} diff --git a/shopfloor/readme/CONFIGURE.rst b/shopfloor/readme/CONFIGURE.rst new file mode 100644 index 00000000000..8442c179fd5 --- /dev/null +++ b/shopfloor/readme/CONFIGURE.rst @@ -0,0 +1 @@ +writeme diff --git a/shopfloor/readme/CONTRIBUTORS.rst b/shopfloor/readme/CONTRIBUTORS.rst new file mode 100644 index 00000000000..7a55e5ba991 --- /dev/null +++ b/shopfloor/readme/CONTRIBUTORS.rst @@ -0,0 +1,3 @@ +* Alexandre Fayolle + + ADD YOURSELF diff --git a/shopfloor/readme/DESCRIPTION.rst b/shopfloor/readme/DESCRIPTION.rst new file mode 100644 index 00000000000..756a0db67d6 --- /dev/null +++ b/shopfloor/readme/DESCRIPTION.rst @@ -0,0 +1 @@ +write me diff --git a/shopfloor/readme/HISTORY.rst b/shopfloor/readme/HISTORY.rst new file mode 100644 index 00000000000..dc27ee26059 --- /dev/null +++ b/shopfloor/readme/HISTORY.rst @@ -0,0 +1,4 @@ +13.0.1.0.0 +~~~~~~~~~~ + +First official version. diff --git a/shopfloor/readme/ROADMAP.rst b/shopfloor/readme/ROADMAP.rst new file mode 100644 index 00000000000..756a0db67d6 --- /dev/null +++ b/shopfloor/readme/ROADMAP.rst @@ -0,0 +1 @@ +write me diff --git a/shopfloor/readme/USAGE.rst b/shopfloor/readme/USAGE.rst new file mode 100644 index 00000000000..098af7f8dfd --- /dev/null +++ b/shopfloor/readme/USAGE.rst @@ -0,0 +1,5 @@ +To add your own REST service you must provides at least 2 classes. + +* A Component providing the business logic of your service, +* A Controller to register your service. +write me From 350b4c4b77d3dfef82add35c921562ae4379f4ee Mon Sep 17 00:00:00 2001 From: Alexandre Fayolle Date: Mon, 20 Jan 2020 16:37:49 +0100 Subject: [PATCH 003/986] more boilerplate --- shopfloor/__manifest__.py | 1 + shopfloor/controllers/__init__.py | 1 + shopfloor/controllers/main.py | 3 +++ 3 files changed, 5 insertions(+) create mode 100644 shopfloor/controllers/__init__.py create mode 100644 shopfloor/controllers/main.py diff --git a/shopfloor/__manifest__.py b/shopfloor/__manifest__.py index 5cc45996a68..c0d07303fe0 100644 --- a/shopfloor/__manifest__.py +++ b/shopfloor/__manifest__.py @@ -6,6 +6,7 @@ "name": "Shopfloor", "summary": "manage warehouse operations with barcode scanners", "version": "13.0.1.0.0", + "development_status": "Alpha", "category": "Inventory", "website": "https://odoo-community.org", "author": "Akretion, BCIM, Camptocamp, Odoo Community Association (OCA)", diff --git a/shopfloor/controllers/__init__.py b/shopfloor/controllers/__init__.py new file mode 100644 index 00000000000..12a7e529b67 --- /dev/null +++ b/shopfloor/controllers/__init__.py @@ -0,0 +1 @@ +from . import main diff --git a/shopfloor/controllers/main.py b/shopfloor/controllers/main.py new file mode 100644 index 00000000000..1a5fe43b700 --- /dev/null +++ b/shopfloor/controllers/main.py @@ -0,0 +1,3 @@ +from odoo import http + + From 14fb280b52b7a5fb05363244bc014ca63c5f5166 Mon Sep 17 00:00:00 2001 From: Guewen Baconnier Date: Mon, 20 Jan 2020 17:07:55 +0100 Subject: [PATCH 004/986] Add setuptools files --- setup/.setuptools-odoo-make-default-ignore | 2 ++ setup/README | 2 ++ setup/shopfloor/odoo/addons/shopfloor | 1 + setup/shopfloor/setup.py | 6 ++++++ 4 files changed, 11 insertions(+) create mode 100644 setup/.setuptools-odoo-make-default-ignore create mode 100644 setup/README create mode 120000 setup/shopfloor/odoo/addons/shopfloor create mode 100644 setup/shopfloor/setup.py diff --git a/setup/.setuptools-odoo-make-default-ignore b/setup/.setuptools-odoo-make-default-ignore new file mode 100644 index 00000000000..207e615334b --- /dev/null +++ b/setup/.setuptools-odoo-make-default-ignore @@ -0,0 +1,2 @@ +# addons listed in this file are ignored by +# setuptools-odoo-make-default (one addon per line) diff --git a/setup/README b/setup/README new file mode 100644 index 00000000000..a63d633e863 --- /dev/null +++ b/setup/README @@ -0,0 +1,2 @@ +To learn more about this directory, please visit +https://pypi.python.org/pypi/setuptools-odoo diff --git a/setup/shopfloor/odoo/addons/shopfloor b/setup/shopfloor/odoo/addons/shopfloor new file mode 120000 index 00000000000..bfab73c253e --- /dev/null +++ b/setup/shopfloor/odoo/addons/shopfloor @@ -0,0 +1 @@ +../../../../shopfloor \ No newline at end of file diff --git a/setup/shopfloor/setup.py b/setup/shopfloor/setup.py new file mode 100644 index 00000000000..28c57bb6403 --- /dev/null +++ b/setup/shopfloor/setup.py @@ -0,0 +1,6 @@ +import setuptools + +setuptools.setup( + setup_requires=['setuptools-odoo'], + odoo_addon=True, +) From 1463b31654e43de7aa3b7210f9ddcb3bb635c553 Mon Sep 17 00:00:00 2001 From: Alexandre Fayolle Date: Mon, 20 Jan 2020 17:11:52 +0100 Subject: [PATCH 005/986] add some models --- shopfloor/__manifest__.py | 5 +++ shopfloor/models/__init__.py | 3 ++ shopfloor/models/res_users.py | 13 +++++++ shopfloor/models/shopfloor_group.py | 14 ++++++++ shopfloor/models/shopfloor_menu.py | 15 ++++++++ shopfloor/security/ir.model.access.csv | 5 +++ shopfloor/views/menus.xml | 6 ++++ shopfloor/views/res_users.xml | 24 +++++++++++++ shopfloor/views/shopfloor_group.xml | 47 ++++++++++++++++++++++++++ shopfloor/views/shopfloor_menu.xml | 29 ++++++++++++++++ 10 files changed, 161 insertions(+) create mode 100644 shopfloor/models/res_users.py create mode 100644 shopfloor/models/shopfloor_group.py create mode 100644 shopfloor/models/shopfloor_menu.py create mode 100644 shopfloor/security/ir.model.access.csv create mode 100644 shopfloor/views/menus.xml create mode 100644 shopfloor/views/res_users.xml create mode 100644 shopfloor/views/shopfloor_group.xml create mode 100644 shopfloor/views/shopfloor_menu.xml diff --git a/shopfloor/__manifest__.py b/shopfloor/__manifest__.py index c0d07303fe0..a6b85e88b15 100644 --- a/shopfloor/__manifest__.py +++ b/shopfloor/__manifest__.py @@ -17,5 +17,10 @@ "base_rest", ], "data": [ + "security/ir.model.access.csv", + "views/res_users.xml", + "views/shopfloor_group.xml", + "views/shopfloor_menu.xml", + "views/menus.xml", ], } diff --git a/shopfloor/models/__init__.py b/shopfloor/models/__init__.py index e69de29bb2d..d8addf9144d 100644 --- a/shopfloor/models/__init__.py +++ b/shopfloor/models/__init__.py @@ -0,0 +1,3 @@ +from . import shopfloor_menu +from . import shopfloor_group +from . import res_users diff --git a/shopfloor/models/res_users.py b/shopfloor/models/res_users.py new file mode 100644 index 00000000000..fe7db83dd45 --- /dev/null +++ b/shopfloor/models/res_users.py @@ -0,0 +1,13 @@ +from odoo import fields, models + + +class ResUsers(models.Model): + _inherit = "res.users" + + shopfloor_group_ids = fields.Many2many( + 'shopfloor.group', + string="Shopfloor groups" + ) + shopfloor_current_process = fields.Char() + shopfloor_last_call = fields.Char() + shopfloor_picking_id = fields.Many2one('stock.picking') diff --git a/shopfloor/models/shopfloor_group.py b/shopfloor/models/shopfloor_group.py new file mode 100644 index 00000000000..9e3ebb6b16b --- /dev/null +++ b/shopfloor/models/shopfloor_group.py @@ -0,0 +1,14 @@ +from odoo import fields, models + + +class ShopfloorGroup(models.Model): + _name = "shopfloor.group" + _description = "Shopfloor group, governs which menu items are visible" + + name = fields.Char(required=True) + user_ids = fields.Many2many("res.users", string="Members") + menu_ids = fields.Many2many( + 'shopfloor.menu', + string="Menus", + help="Can see these menus", + ) diff --git a/shopfloor/models/shopfloor_menu.py b/shopfloor/models/shopfloor_menu.py new file mode 100644 index 00000000000..1e823b4e256 --- /dev/null +++ b/shopfloor/models/shopfloor_menu.py @@ -0,0 +1,15 @@ +from odoo import fields, models + + +class ShopfloorMenu(models.Model): + _name = 'shopfloor.menu' + _description = "Menu displayed in the scanner application" + _order = 'sequence' + + name = fields.Char(translate=True) + sequence = fields.Integer() + group_ids = fields.Many2many( + 'shopfloor.group', + string="Groups", + help="visible for these groups", + ) diff --git a/shopfloor/security/ir.model.access.csv b/shopfloor/security/ir.model.access.csv new file mode 100644 index 00000000000..5ca90ecc73f --- /dev/null +++ b/shopfloor/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",,1,0,, +"access_shopfloor_menu_stock_manager","shopfloor menu inventory manager","model_shopfloor_menu","stock.group_stock_manager,1,1,1,1 +"access_shopfloor_group_users","shopfloor group","model_shopfloor_group",,1,0,, +"access_shopfloor_group_stock_manager","shopfloor group inventory manager","model_shopfloor_group","stock.group_stock_manager,1,1,1,1 diff --git a/shopfloor/views/menus.xml b/shopfloor/views/menus.xml new file mode 100644 index 00000000000..c4a8cbb2f31 --- /dev/null +++ b/shopfloor/views/menus.xml @@ -0,0 +1,6 @@ + +< + + + + diff --git a/shopfloor/views/res_users.xml b/shopfloor/views/res_users.xml new file mode 100644 index 00000000000..2bf16a31f61 --- /dev/null +++ b/shopfloor/views/res_users.xml @@ -0,0 +1,24 @@ + + + + + Res users Shopfloor + + + + + + + + + + + + + + + + + + + diff --git a/shopfloor/views/shopfloor_group.xml b/shopfloor/views/shopfloor_group.xml new file mode 100644 index 00000000000..1aedf5f4a0a --- /dev/null +++ b/shopfloor/views/shopfloor_group.xml @@ -0,0 +1,47 @@ + + + + shopfloor group tree + + + + + + + + + + shopfloor group form + +
+ + + + + + + + + + + +
+
+
+ + + shopfloor group search + + + + + + + + + Groups + ir.actions.act_window + tree,form + + +
diff --git a/shopfloor/views/shopfloor_menu.xml b/shopfloor/views/shopfloor_menu.xml new file mode 100644 index 00000000000..9c50eafa7e7 --- /dev/null +++ b/shopfloor/views/shopfloor_menu.xml @@ -0,0 +1,29 @@ + + + + + shopfloor menu tree + + + + + + + + + + + shopfloor menu search + + + + + + + + + Menus + ir.actions.act_window + tree + + From 79d29af82090876013e0c9eea221b83fdd8ae01e Mon Sep 17 00:00:00 2001 From: Guewen Baconnier Date: Mon, 20 Jan 2020 17:20:56 +0100 Subject: [PATCH 006/986] fixup! add some models --- shopfloor/security/ir.model.access.csv | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/shopfloor/security/ir.model.access.csv b/shopfloor/security/ir.model.access.csv index 5ca90ecc73f..5b805f475d9 100644 --- a/shopfloor/security/ir.model.access.csv +++ b/shopfloor/security/ir.model.access.csv @@ -1,5 +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",,1,0,, -"access_shopfloor_menu_stock_manager","shopfloor menu inventory manager","model_shopfloor_menu","stock.group_stock_manager,1,1,1,1 -"access_shopfloor_group_users","shopfloor group","model_shopfloor_group",,1,0,, -"access_shopfloor_group_stock_manager","shopfloor group inventory manager","model_shopfloor_group","stock.group_stock_manager,1,1,1,1 +"access_shopfloor_menu_users","shopfloor menu","model_shopfloor_menu",,1,0,0,0 +"access_shopfloor_menu_stock_manager","shopfloor menu inventory manager","model_shopfloor_menu","stock.group_stock_manager",1,1,1,1 +"access_shopfloor_group_users","shopfloor group","model_shopfloor_group",,1,0,0,0 +"access_shopfloor_group_stock_manager","shopfloor group inventory manager","model_shopfloor_group","stock.group_stock_manager",1,1,1,1 From bd4d915c7cad8fef22a8a19564d2c48a550a387a Mon Sep 17 00:00:00 2001 From: Benoit Date: Mon, 20 Jan 2020 17:23:12 +0100 Subject: [PATCH 007/986] add start controllers and services --- shopfloor/__init__.py | 2 ++ shopfloor/controllers/main.py | 10 +++++++++- shopfloor/services/__init__.py | 1 + shopfloor/services/shopfloor_service.py | 26 +++++++++++++++++++++++++ 4 files changed, 38 insertions(+), 1 deletion(-) create mode 100644 shopfloor/services/__init__.py create mode 100644 shopfloor/services/shopfloor_service.py diff --git a/shopfloor/__init__.py b/shopfloor/__init__.py index 0650744f6bc..c312a8487c5 100644 --- a/shopfloor/__init__.py +++ b/shopfloor/__init__.py @@ -1 +1,3 @@ +from . import controllers from . import models +from . import services diff --git a/shopfloor/controllers/main.py b/shopfloor/controllers/main.py index 1a5fe43b700..94b9d46c509 100644 --- a/shopfloor/controllers/main.py +++ b/shopfloor/controllers/main.py @@ -1,3 +1,11 @@ -from odoo import http +from odoo.addons.base_rest.controllers import main +import json + + +class ShopfloorController(main.RestController): + _root_path = "/shopfloor/" + _collection_name = "shopfloor.services" + _default_auth = "api_key" + diff --git a/shopfloor/services/__init__.py b/shopfloor/services/__init__.py new file mode 100644 index 00000000000..86d4e03e8fa --- /dev/null +++ b/shopfloor/services/__init__.py @@ -0,0 +1 @@ +from . shopfloor_service diff --git a/shopfloor/services/shopfloor_service.py b/shopfloor/services/shopfloor_service.py new file mode 100644 index 00000000000..18146f34619 --- /dev/null +++ b/shopfloor/services/shopfloor_service.py @@ -0,0 +1,26 @@ +from odoo.addons.component.core import Component + + +class ShopfloorService(Component): + _inherit = "base.rest.service" + _name = "shopfloor.service" + _collection = "shopfloor.services" + _usage = "shopfloor" + + def get_pack(self, pack_name): + """ + Get pack informations + """ + pack = self.env['stock.quant.package'].search([('name', '=', pack_name)]) + return self._to_json(pack) + + def _validator_search(self): + return {"pack_name": {"type": "string", "nullable": False, "required": True}} + + def _to_json(self, pack): + res = { + "id": pack.id, + "name": pack.name, + "location": pack.location_id.name, + } + return res From 07721ec5ba3cce48ab6b54496bb0f76c0719753c Mon Sep 17 00:00:00 2001 From: Alexandre Fayolle Date: Mon, 20 Jan 2020 17:28:09 +0100 Subject: [PATCH 008/986] fixup! add some models --- shopfloor/views/res_users.xml | 1 + shopfloor/views/shopfloor_group.xml | 4 ++++ shopfloor/views/shopfloor_menu.xml | 3 +++ 3 files changed, 8 insertions(+) diff --git a/shopfloor/views/res_users.xml b/shopfloor/views/res_users.xml index 2bf16a31f61..7215a23cb60 100644 --- a/shopfloor/views/res_users.xml +++ b/shopfloor/views/res_users.xml @@ -3,6 +3,7 @@ Res users Shopfloor + res.users diff --git a/shopfloor/views/shopfloor_group.xml b/shopfloor/views/shopfloor_group.xml index 1aedf5f4a0a..212f672858a 100644 --- a/shopfloor/views/shopfloor_group.xml +++ b/shopfloor/views/shopfloor_group.xml @@ -2,6 +2,7 @@ shopfloor group tree + shopfloor.group @@ -12,6 +13,7 @@ shopfloor group form + shopfloor.group
@@ -31,6 +33,7 @@ shopfloor group search + shopfloor.group @@ -40,6 +43,7 @@ Groups + shopfloor.group ir.actions.act_window tree,form diff --git a/shopfloor/views/shopfloor_menu.xml b/shopfloor/views/shopfloor_menu.xml index 9c50eafa7e7..461eda3a9fd 100644 --- a/shopfloor/views/shopfloor_menu.xml +++ b/shopfloor/views/shopfloor_menu.xml @@ -3,6 +3,7 @@ shopfloor menu tree + shopfloor.menu @@ -14,6 +15,7 @@ shopfloor menu search + shopfloor.menu @@ -23,6 +25,7 @@ Menus + shopfloor.menu ir.actions.act_window tree From 5da9f544b3774a4fbfdefd5d8931fc76dc96e866 Mon Sep 17 00:00:00 2001 From: Alexandre Fayolle Date: Mon, 20 Jan 2020 17:30:24 +0100 Subject: [PATCH 009/986] fixup! fixup! add some models --- shopfloor/views/shopfloor_group.xml | 6 +++--- shopfloor/views/shopfloor_menu.xml | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/shopfloor/views/shopfloor_group.xml b/shopfloor/views/shopfloor_group.xml index 212f672858a..f705515e5c1 100644 --- a/shopfloor/views/shopfloor_group.xml +++ b/shopfloor/views/shopfloor_group.xml @@ -1,6 +1,6 @@ - + shopfloor group tree shopfloor.group @@ -11,7 +11,7 @@ - + shopfloor group form shopfloor.group @@ -31,7 +31,7 @@ - + shopfloor group search shopfloor.group diff --git a/shopfloor/views/shopfloor_menu.xml b/shopfloor/views/shopfloor_menu.xml index 461eda3a9fd..b7648b2b120 100644 --- a/shopfloor/views/shopfloor_menu.xml +++ b/shopfloor/views/shopfloor_menu.xml @@ -1,7 +1,7 @@ - + shopfloor menu tree shopfloor.menu @@ -13,7 +13,7 @@ - + shopfloor menu search shopfloor.menu From 50fde5487709e8f70d8acee6efd30cb209a282f6 Mon Sep 17 00:00:00 2001 From: Benoit Date: Mon, 20 Jan 2020 17:32:59 +0100 Subject: [PATCH 010/986] fixup! typo --- shopfloor/services/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/shopfloor/services/__init__.py b/shopfloor/services/__init__.py index 86d4e03e8fa..5f9782abe91 100644 --- a/shopfloor/services/__init__.py +++ b/shopfloor/services/__init__.py @@ -1 +1 @@ -from . shopfloor_service +from . import shopfloor_service From 9eaa7a7df6000917a871ad0ceb1241c05e0b4885 Mon Sep 17 00:00:00 2001 From: Alexandre Fayolle Date: Mon, 20 Jan 2020 17:36:28 +0100 Subject: [PATCH 011/986] fixup! fixup! fixup! add some models --- shopfloor/views/menus.xml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/shopfloor/views/menus.xml b/shopfloor/views/menus.xml index c4a8cbb2f31..e79ab726b9f 100644 --- a/shopfloor/views/menus.xml +++ b/shopfloor/views/menus.xml @@ -1,6 +1,6 @@ -< - - - + + + + From e638afe78103a2ef84da77aa228e094e9ec9199e Mon Sep 17 00:00:00 2001 From: Alexandre Fayolle Date: Mon, 20 Jan 2020 17:40:09 +0100 Subject: [PATCH 012/986] fixup! fixup! fixup! fixup! add some models --- shopfloor/views/menus.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/shopfloor/views/menus.xml b/shopfloor/views/menus.xml index e79ab726b9f..e29c43aa408 100644 --- a/shopfloor/views/menus.xml +++ b/shopfloor/views/menus.xml @@ -2,5 +2,5 @@ - + From ea434c0714b03090f18b0fc9ce27469a28d1efdd Mon Sep 17 00:00:00 2001 From: Alexandre Fayolle Date: Mon, 20 Jan 2020 17:42:23 +0100 Subject: [PATCH 013/986] fixup! fixup! fixup! fixup! fixup! add some models --- shopfloor/views/menus.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/shopfloor/views/menus.xml b/shopfloor/views/menus.xml index e29c43aa408..487df55ccf8 100644 --- a/shopfloor/views/menus.xml +++ b/shopfloor/views/menus.xml @@ -2,5 +2,5 @@ - + From 2492be39422a445c9d97b349364fc5c3b2d75e64 Mon Sep 17 00:00:00 2001 From: Guewen Baconnier Date: Tue, 21 Jan 2020 09:27:41 +0100 Subject: [PATCH 014/986] Add dependency on auth_api_key Required to identify the access from the JS client. The module is stored in the repository: OCA/server-auth. --- oca_dependencies.txt | 1 + shopfloor/__manifest__.py | 4 ++++ shopfloor/demo/auth_api_key_demo.xml | 9 +++++++++ shopfloor/readme/USAGE.rst | 7 ++----- 4 files changed, 16 insertions(+), 5 deletions(-) create mode 100644 shopfloor/demo/auth_api_key_demo.xml diff --git a/oca_dependencies.txt b/oca_dependencies.txt index b56ff1aa4e9..533ba46e90d 100644 --- a/oca_dependencies.txt +++ b/oca_dependencies.txt @@ -1 +1,2 @@ rest-framework +server-auth diff --git a/shopfloor/__manifest__.py b/shopfloor/__manifest__.py index a6b85e88b15..45b15a0aab5 100644 --- a/shopfloor/__manifest__.py +++ b/shopfloor/__manifest__.py @@ -15,6 +15,7 @@ "depends": [ "stock", "base_rest", + "auth_api_key", ], "data": [ "security/ir.model.access.csv", @@ -23,4 +24,7 @@ "views/shopfloor_menu.xml", "views/menus.xml", ], + "demo": [ + "demo/auth_api_key_demo.xml", + ] } diff --git a/shopfloor/demo/auth_api_key_demo.xml b/shopfloor/demo/auth_api_key_demo.xml new file mode 100644 index 00000000000..800aec5f0b8 --- /dev/null +++ b/shopfloor/demo/auth_api_key_demo.xml @@ -0,0 +1,9 @@ + + + + Demo + + 72B044F7AC780DAC + + + diff --git a/shopfloor/readme/USAGE.rst b/shopfloor/readme/USAGE.rst index 098af7f8dfd..a83522c0be6 100644 --- a/shopfloor/readme/USAGE.rst +++ b/shopfloor/readme/USAGE.rst @@ -1,5 +1,2 @@ -To add your own REST service you must provides at least 2 classes. - -* A Component providing the business logic of your service, -* A Controller to register your service. -write me +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 From 44a07747e4152540a4b309c9a4198905fa561ba1 Mon Sep 17 00:00:00 2001 From: Guewen Baconnier Date: Tue, 21 Jan 2020 09:42:45 +0100 Subject: [PATCH 015/986] fixup! Add dependency on auth_api_key --- shopfloor/readme/USAGE.rst | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/shopfloor/readme/USAGE.rst b/shopfloor/readme/USAGE.rst index a83522c0be6..2d5767567e5 100644 --- a/shopfloor/readme/USAGE.rst +++ b/shopfloor/readme/USAGE.rst @@ -1,2 +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/shopfloor/get_pack" -H "accept: */*" -H "Content-Type: application/json" -H "API-KEY: 72B044F7AC780DAC" -d "{\"pack_name\":\"string\"}" From 1098c6d4b8db09e6e57d88b0739d0b2943bb02c2 Mon Sep 17 00:00:00 2001 From: Guewen Baconnier Date: Tue, 21 Jan 2020 09:42:35 +0100 Subject: [PATCH 016/986] fixup! add start controllers and services --- shopfloor/services/shopfloor_service.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/shopfloor/services/shopfloor_service.py b/shopfloor/services/shopfloor_service.py index 18146f34619..7db5a5c4b7c 100644 --- a/shopfloor/services/shopfloor_service.py +++ b/shopfloor/services/shopfloor_service.py @@ -14,7 +14,7 @@ def get_pack(self, pack_name): pack = self.env['stock.quant.package'].search([('name', '=', pack_name)]) return self._to_json(pack) - def _validator_search(self): + def _validator_get_pack(self): return {"pack_name": {"type": "string", "nullable": False, "required": True}} def _to_json(self, pack): From 80149cd46c0779c3cc016613fcd495c18d116a94 Mon Sep 17 00:00:00 2001 From: Alexandre Fayolle Date: Tue, 21 Jan 2020 11:05:24 +0100 Subject: [PATCH 017/986] [REF] rename shopfloor.group -> shopfloor.operation.group --- shopfloor/__manifest__.py | 2 +- shopfloor/models/__init__.py | 2 +- shopfloor/models/res_users.py | 6 ++--- shopfloor/models/shopfloor_menu.py | 4 ++-- ..._group.py => shopfloor_operation_group.py} | 6 ++--- shopfloor/security/ir.model.access.csv | 4 ++-- shopfloor/views/menus.xml | 2 +- shopfloor/views/res_users.xml | 2 +- shopfloor/views/shopfloor_menu.xml | 2 +- ...roup.xml => shopfloor_operation_group.xml} | 24 +++++++++---------- 10 files changed, 27 insertions(+), 27 deletions(-) rename shopfloor/models/{shopfloor_group.py => shopfloor_operation_group.py} (60%) rename shopfloor/views/{shopfloor_group.xml => shopfloor_operation_group.xml} (57%) diff --git a/shopfloor/__manifest__.py b/shopfloor/__manifest__.py index 45b15a0aab5..653d4f64911 100644 --- a/shopfloor/__manifest__.py +++ b/shopfloor/__manifest__.py @@ -20,7 +20,7 @@ "data": [ "security/ir.model.access.csv", "views/res_users.xml", - "views/shopfloor_group.xml", + "views/shopfloor_operation_group.xml", "views/shopfloor_menu.xml", "views/menus.xml", ], diff --git a/shopfloor/models/__init__.py b/shopfloor/models/__init__.py index d8addf9144d..6dca41f10ca 100644 --- a/shopfloor/models/__init__.py +++ b/shopfloor/models/__init__.py @@ -1,3 +1,3 @@ from . import shopfloor_menu -from . import shopfloor_group +from . import shopfloor_operation_group from . import res_users diff --git a/shopfloor/models/res_users.py b/shopfloor/models/res_users.py index fe7db83dd45..5b6514def6d 100644 --- a/shopfloor/models/res_users.py +++ b/shopfloor/models/res_users.py @@ -4,9 +4,9 @@ class ResUsers(models.Model): _inherit = "res.users" - shopfloor_group_ids = fields.Many2many( - 'shopfloor.group', - string="Shopfloor groups" + shopfloor_operation_group_ids = fields.Many2many( + 'shopfloor.operation.group', + string="Shopfloor operation groups" ) shopfloor_current_process = fields.Char() shopfloor_last_call = fields.Char() diff --git a/shopfloor/models/shopfloor_menu.py b/shopfloor/models/shopfloor_menu.py index 1e823b4e256..8e62c40d0ef 100644 --- a/shopfloor/models/shopfloor_menu.py +++ b/shopfloor/models/shopfloor_menu.py @@ -8,8 +8,8 @@ class ShopfloorMenu(models.Model): name = fields.Char(translate=True) sequence = fields.Integer() - group_ids = fields.Many2many( - 'shopfloor.group', + operation_group_ids = fields.Many2many( + 'shopfloor.operation.group', string="Groups", help="visible for these groups", ) diff --git a/shopfloor/models/shopfloor_group.py b/shopfloor/models/shopfloor_operation_group.py similarity index 60% rename from shopfloor/models/shopfloor_group.py rename to shopfloor/models/shopfloor_operation_group.py index 9e3ebb6b16b..1d78aeeb832 100644 --- a/shopfloor/models/shopfloor_group.py +++ b/shopfloor/models/shopfloor_operation_group.py @@ -1,9 +1,9 @@ from odoo import fields, models -class ShopfloorGroup(models.Model): - _name = "shopfloor.group" - _description = "Shopfloor group, governs which menu items are visible" +class ShopfloorOperationGroup(models.Model): + _name = "shopfloor.operation.group" + _description = "Shopfloor operation group, governs which menu items are visible" name = fields.Char(required=True) user_ids = fields.Many2many("res.users", string="Members") diff --git a/shopfloor/security/ir.model.access.csv b/shopfloor/security/ir.model.access.csv index 5b805f475d9..6c0c0ddd14d 100644 --- a/shopfloor/security/ir.model.access.csv +++ b/shopfloor/security/ir.model.access.csv @@ -1,5 +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",,1,0,0,0 "access_shopfloor_menu_stock_manager","shopfloor menu inventory manager","model_shopfloor_menu","stock.group_stock_manager",1,1,1,1 -"access_shopfloor_group_users","shopfloor group","model_shopfloor_group",,1,0,0,0 -"access_shopfloor_group_stock_manager","shopfloor group inventory manager","model_shopfloor_group","stock.group_stock_manager",1,1,1,1 +"access_shopfloor_operation_group_users","shopfloor operation group","model_shopfloor_operation_group",,1,0,0,0 +"access_shopfloor_operation_group_stock_manager","shopfloor operation group inventory manager","model_shopfloor_operation_group","stock.group_stock_manager",1,1,1,1 diff --git a/shopfloor/views/menus.xml b/shopfloor/views/menus.xml index 487df55ccf8..3acc388d270 100644 --- a/shopfloor/views/menus.xml +++ b/shopfloor/views/menus.xml @@ -2,5 +2,5 @@ - + diff --git a/shopfloor/views/res_users.xml b/shopfloor/views/res_users.xml index 7215a23cb60..b1ce6c1e56d 100644 --- a/shopfloor/views/res_users.xml +++ b/shopfloor/views/res_users.xml @@ -10,7 +10,7 @@ - + diff --git a/shopfloor/views/shopfloor_menu.xml b/shopfloor/views/shopfloor_menu.xml index b7648b2b120..c9845601b00 100644 --- a/shopfloor/views/shopfloor_menu.xml +++ b/shopfloor/views/shopfloor_menu.xml @@ -8,7 +8,7 @@ - + diff --git a/shopfloor/views/shopfloor_group.xml b/shopfloor/views/shopfloor_operation_group.xml similarity index 57% rename from shopfloor/views/shopfloor_group.xml rename to shopfloor/views/shopfloor_operation_group.xml index f705515e5c1..28fdc3ecc36 100644 --- a/shopfloor/views/shopfloor_group.xml +++ b/shopfloor/views/shopfloor_operation_group.xml @@ -1,8 +1,8 @@ - - shopfloor group tree - shopfloor.group + + shopfloor operation group tree + shopfloor.operation.group @@ -11,9 +11,9 @@ - - shopfloor group form - shopfloor.group + + shopfloor operation group form + shopfloor.operation.group @@ -31,9 +31,9 @@ - - shopfloor group search - shopfloor.group + + shopfloor operation group search + shopfloor.operation.group @@ -41,9 +41,9 @@ - - Groups - shopfloor.group + + Operation Groups + shopfloor.operation.group ir.actions.act_window tree,form From 0fd3c1f352ad9c8d2ece1f39407fa9f812de5831 Mon Sep 17 00:00:00 2001 From: Alexandre Fayolle Date: Tue, 21 Jan 2020 11:22:55 +0100 Subject: [PATCH 018/986] [IMP] add shopfloor.process --- shopfloor/__manifest__.py | 2 ++ shopfloor/models/__init__.py | 2 ++ shopfloor/models/shopfloor_menu.py | 1 + shopfloor/models/shopfloor_process.py | 11 ++++++ shopfloor/models/stock_picking_type.py | 7 ++++ shopfloor/views/menus.xml | 1 + shopfloor/views/shopfloor_menu.xml | 1 + shopfloor/views/shopfloor_process.xml | 50 ++++++++++++++++++++++++++ shopfloor/views/stock_picking_type.xml | 13 +++++++ 9 files changed, 88 insertions(+) create mode 100644 shopfloor/models/shopfloor_process.py create mode 100644 shopfloor/models/stock_picking_type.py create mode 100644 shopfloor/views/shopfloor_process.xml create mode 100644 shopfloor/views/stock_picking_type.xml diff --git a/shopfloor/__manifest__.py b/shopfloor/__manifest__.py index 653d4f64911..7cbf3e87cd4 100644 --- a/shopfloor/__manifest__.py +++ b/shopfloor/__manifest__.py @@ -22,6 +22,8 @@ "views/res_users.xml", "views/shopfloor_operation_group.xml", "views/shopfloor_menu.xml", + "views/shopfloor_process.xml", + "views/stock_picking_type.xml", "views/menus.xml", ], "demo": [ diff --git a/shopfloor/models/__init__.py b/shopfloor/models/__init__.py index 6dca41f10ca..15fe538e931 100644 --- a/shopfloor/models/__init__.py +++ b/shopfloor/models/__init__.py @@ -1,3 +1,5 @@ from . import shopfloor_menu from . import shopfloor_operation_group +from . import shopfloor_process from . import res_users +from . import stock_picking_type diff --git a/shopfloor/models/shopfloor_menu.py b/shopfloor/models/shopfloor_menu.py index 8e62c40d0ef..48f836bd371 100644 --- a/shopfloor/models/shopfloor_menu.py +++ b/shopfloor/models/shopfloor_menu.py @@ -13,3 +13,4 @@ class ShopfloorMenu(models.Model): string="Groups", help="visible for these groups", ) + process_id = fields.Many2one('shopfloor.process', name="Process") diff --git a/shopfloor/models/shopfloor_process.py b/shopfloor/models/shopfloor_process.py new file mode 100644 index 00000000000..accbc02155b --- /dev/null +++ b/shopfloor/models/shopfloor_process.py @@ -0,0 +1,11 @@ +from odoo import fields, models + + +class ShopfloorProcess(models.Model): + _name = "shopfloor.process" + _description = "a process to be run from the scanners" + + name = fields.Char(required=True) + picking_type_ids = fields.One2many( + 'stock.picking.type', 'process_id', string="Operation types" + ) diff --git a/shopfloor/models/stock_picking_type.py b/shopfloor/models/stock_picking_type.py new file mode 100644 index 00000000000..206b68c7fe4 --- /dev/null +++ b/shopfloor/models/stock_picking_type.py @@ -0,0 +1,7 @@ +from odoo import fields, models + + +class StockPickingType(models.Model): + _inherit = 'stock.picking.type' + + process_id = fields.Many2one('shopfloor.process', string="Process") diff --git a/shopfloor/views/menus.xml b/shopfloor/views/menus.xml index 3acc388d270..4fba7ea4281 100644 --- a/shopfloor/views/menus.xml +++ b/shopfloor/views/menus.xml @@ -3,4 +3,5 @@ + diff --git a/shopfloor/views/shopfloor_menu.xml b/shopfloor/views/shopfloor_menu.xml index c9845601b00..016b872f25a 100644 --- a/shopfloor/views/shopfloor_menu.xml +++ b/shopfloor/views/shopfloor_menu.xml @@ -9,6 +9,7 @@ + diff --git a/shopfloor/views/shopfloor_process.xml b/shopfloor/views/shopfloor_process.xml new file mode 100644 index 00000000000..67afa35bc8f --- /dev/null +++ b/shopfloor/views/shopfloor_process.xml @@ -0,0 +1,50 @@ + + + + shopfloor process tree + shopfloor.process + + + + + + + + + + shopfloor process form + shopfloor.process + + + + + + + + + + + + + + + + + + shopfloor process search + shopfloor.process + + + + + + + + + Processes + shopfloor.process + ir.actions.act_window + tree,form + + + diff --git a/shopfloor/views/stock_picking_type.xml b/shopfloor/views/stock_picking_type.xml new file mode 100644 index 00000000000..1ccb8c591bf --- /dev/null +++ b/shopfloor/views/stock_picking_type.xml @@ -0,0 +1,13 @@ + + + + Operation Types + stock.picking.type + + + + + + + + From 93e8ee3e188537fec7e0c2d0e99de8c1e2bef44a Mon Sep 17 00:00:00 2001 From: Guewen Baconnier Date: Tue, 21 Jan 2020 12:00:20 +0100 Subject: [PATCH 019/986] Add shopfloor device, with the associated service Adds a base component for rest to share things amongst service components. The service for packs still needs to be adapted. --- shopfloor/__manifest__.py | 2 +- shopfloor/models/__init__.py | 2 +- shopfloor/models/res_users.py | 13 ----- shopfloor/models/shopfloor_device.py | 25 +++++++++ shopfloor/security/ir.model.access.csv | 2 + shopfloor/services/__init__.py | 3 ++ shopfloor/services/device.py | 33 ++++++++++++ shopfloor/services/service.py | 54 +++++++++++++++++++ shopfloor/services/shopfloor_service.py | 4 +- shopfloor/views/menus.xml | 1 + shopfloor/views/res_users.xml | 25 --------- shopfloor/views/shopfloor_device_views.xml | 61 ++++++++++++++++++++++ 12 files changed, 183 insertions(+), 42 deletions(-) delete mode 100644 shopfloor/models/res_users.py create mode 100644 shopfloor/models/shopfloor_device.py create mode 100644 shopfloor/services/device.py create mode 100644 shopfloor/services/service.py delete mode 100644 shopfloor/views/res_users.xml create mode 100644 shopfloor/views/shopfloor_device_views.xml diff --git a/shopfloor/__manifest__.py b/shopfloor/__manifest__.py index 7cbf3e87cd4..7bae4764a91 100644 --- a/shopfloor/__manifest__.py +++ b/shopfloor/__manifest__.py @@ -19,11 +19,11 @@ ], "data": [ "security/ir.model.access.csv", - "views/res_users.xml", "views/shopfloor_operation_group.xml", "views/shopfloor_menu.xml", "views/shopfloor_process.xml", "views/stock_picking_type.xml", + "views/shopfloor_device_views.xml", "views/menus.xml", ], "demo": [ diff --git a/shopfloor/models/__init__.py b/shopfloor/models/__init__.py index 15fe538e931..7bfd287ebaf 100644 --- a/shopfloor/models/__init__.py +++ b/shopfloor/models/__init__.py @@ -1,5 +1,5 @@ from . import shopfloor_menu from . import shopfloor_operation_group from . import shopfloor_process -from . import res_users from . import stock_picking_type +from . import shopfloor_device diff --git a/shopfloor/models/res_users.py b/shopfloor/models/res_users.py deleted file mode 100644 index 5b6514def6d..00000000000 --- a/shopfloor/models/res_users.py +++ /dev/null @@ -1,13 +0,0 @@ -from odoo import fields, models - - -class ResUsers(models.Model): - _inherit = "res.users" - - shopfloor_operation_group_ids = fields.Many2many( - 'shopfloor.operation.group', - string="Shopfloor operation groups" - ) - shopfloor_current_process = fields.Char() - shopfloor_last_call = fields.Char() - shopfloor_picking_id = fields.Many2one('stock.picking') diff --git a/shopfloor/models/shopfloor_device.py b/shopfloor/models/shopfloor_device.py new file mode 100644 index 00000000000..986367f936c --- /dev/null +++ b/shopfloor/models/shopfloor_device.py @@ -0,0 +1,25 @@ +from odoo import fields, models + + +class ShopfloorDevice(models.Model): + _name = "shopfloor.device" + _description = "Shopfloor device settings" + + name = fields.Char(required=True) + warehouse_id = fields.Many2one( + "stock.warehouse", + required=True, + ) + shopfloor_operation_group_ids = fields.Many2many( + "shopfloor.operation.group", + string="Shopfloor Operation Groups" + ) + user_id = fields.Many2one( + "res.users", + help="Optional user using the device. The device will" + "use this configuration when the users logs in the client " + "application." + ) + shopfloor_current_process = fields.Char(readonly=True) + shopfloor_last_call = fields.Char(readonly=True) + shopfloor_picking_id = fields.Many2one('stock.picking', readonly=True) diff --git a/shopfloor/security/ir.model.access.csv b/shopfloor/security/ir.model.access.csv index 6c0c0ddd14d..2726088b885 100644 --- a/shopfloor/security/ir.model.access.csv +++ b/shopfloor/security/ir.model.access.csv @@ -3,3 +3,5 @@ "access_shopfloor_menu_stock_manager","shopfloor menu inventory manager","model_shopfloor_menu","stock.group_stock_manager",1,1,1,1 "access_shopfloor_operation_group_users","shopfloor operation group","model_shopfloor_operation_group",,1,0,0,0 "access_shopfloor_operation_group_stock_manager","shopfloor operation group inventory manager","model_shopfloor_operation_group","stock.group_stock_manager",1,1,1,1 +"access_shopfloor_device_users","shopfloor device","model_shopfloor_device",,1,0,0,0 +"access_shopfloor_device_stock_manager","shopfloor device inventory manager","model_shopfloor_device","stock.group_stock_manager",1,1,1,1 diff --git a/shopfloor/services/__init__.py b/shopfloor/services/__init__.py index 5f9782abe91..7369a5602f7 100644 --- a/shopfloor/services/__init__.py +++ b/shopfloor/services/__init__.py @@ -1 +1,4 @@ +from . import service +# TODO rename file shop_floor_service to pack from . import shopfloor_service +from . import device diff --git a/shopfloor/services/device.py b/shopfloor/services/device.py new file mode 100644 index 00000000000..74857cd9334 --- /dev/null +++ b/shopfloor/services/device.py @@ -0,0 +1,33 @@ +from odoo.addons.component.core import Component + + +class ShopfloorDevice(Component): + _inherit = "base.shopfloor.service" + _name = "shopfloor.device" + _usage = "device" + _expose_model = "shopfloor.device" + + def search(self, name_fragment=None): + # TODO filter on shopfloor.group or user in override of _get_base_search_domain + domain = self._get_base_search_domain() + if name_fragment: + domain.append(("name", "ilike", name_fragment)) + records = self.env[self._expose_model].search(domain) + return {"size": len(records), "data": self._to_json(records)} + + def _validator_search(self): + return { + "name_fragment": { + "type": "string", + "nullable": True, + "required": False, + } + } + + def _convert_one_record(self, record): + return { + "id": record.id, + "name": record.name, + "warehouse_id": record.warehouse_id.id, + "warehouse": record.warehouse_id.name, + } diff --git a/shopfloor/services/service.py b/shopfloor/services/service.py new file mode 100644 index 00000000000..f52894f558a --- /dev/null +++ b/shopfloor/services/service.py @@ -0,0 +1,54 @@ +from odoo import _ +from odoo.osv import expression +from odoo.addons.component.core import AbstractComponent +from odoo.exceptions import MissingError + + +class BaseShopfloorService(AbstractComponent): + _inherit = "base.rest.service" + _name = "base.shopfloor.service" + _collection = "shopfloor.services" + _expose_model = None + + 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) + if not record: + raise MissingError( + _("The record %s %s does not exist") + % (self._expose_model, _id) + ) + else: + return record + + def _get_base_search_domain(self): + return [] + + def _convert_one_record(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 _get_openapi_default_parameters(self): + defaults = super()._get_openapi_default_parameters() + demo_api_key = self.env.ref( + "shopfloor.api_key_demo", raise_if_not_found=False + ).key + defaults.append( + { + "name": "API-KEY", + "in": "header", + "description": "API key for Authorization", + "required": True, + "schema": {"type": "string"}, + "style": "simple", + "value": demo_api_key, + } + ) + return defaults diff --git a/shopfloor/services/shopfloor_service.py b/shopfloor/services/shopfloor_service.py index 7db5a5c4b7c..c3e68528b06 100644 --- a/shopfloor/services/shopfloor_service.py +++ b/shopfloor/services/shopfloor_service.py @@ -1,10 +1,10 @@ from odoo.addons.component.core import Component +# TODO move in a pack service class ShopfloorService(Component): - _inherit = "base.rest.service" + _inherit = "base.shopfloor.service" _name = "shopfloor.service" - _collection = "shopfloor.services" _usage = "shopfloor" def get_pack(self, pack_name): diff --git a/shopfloor/views/menus.xml b/shopfloor/views/menus.xml index 4fba7ea4281..f6bc4f105da 100644 --- a/shopfloor/views/menus.xml +++ b/shopfloor/views/menus.xml @@ -4,4 +4,5 @@ +
diff --git a/shopfloor/views/res_users.xml b/shopfloor/views/res_users.xml deleted file mode 100644 index b1ce6c1e56d..00000000000 --- a/shopfloor/views/res_users.xml +++ /dev/null @@ -1,25 +0,0 @@ - - - - - Res users Shopfloor - res.users - - - - - - - - - - - - - - - - - - - diff --git a/shopfloor/views/shopfloor_device_views.xml b/shopfloor/views/shopfloor_device_views.xml new file mode 100644 index 00000000000..2f92f309280 --- /dev/null +++ b/shopfloor/views/shopfloor_device_views.xml @@ -0,0 +1,61 @@ + + + + shopfloor.device tree + shopfloor.device + + + + + + + + + + + shopfloor.device form + shopfloor.device + +
+ + + + + + + + + + + + + + + + + +
+
+
+ + + shopfloor.device search + shopfloor.device + + + + + + + + + + + + Devices + shopfloor.device + ir.actions.act_window + tree,form + + +
From 6caccfd800ad79c960ec4db5a9e68b6b8a001b9a Mon Sep 17 00:00:00 2001 From: Guewen Baconnier Date: Tue, 21 Jan 2020 13:55:19 +0100 Subject: [PATCH 020/986] fixup! [IMP] add shopfloor.process --- shopfloor/security/ir.model.access.csv | 1 + 1 file changed, 1 insertion(+) diff --git a/shopfloor/security/ir.model.access.csv b/shopfloor/security/ir.model.access.csv index 2726088b885..6db6c867996 100644 --- a/shopfloor/security/ir.model.access.csv +++ b/shopfloor/security/ir.model.access.csv @@ -5,3 +5,4 @@ "access_shopfloor_operation_group_stock_manager","shopfloor operation group inventory manager","model_shopfloor_operation_group","stock.group_stock_manager",1,1,1,1 "access_shopfloor_device_users","shopfloor device","model_shopfloor_device",,1,0,0,0 "access_shopfloor_device_stock_manager","shopfloor device inventory manager","model_shopfloor_device","stock.group_stock_manager",1,1,1,1 +"access_shopfloor_process_users","shopfloor process","model_shopfloor_process",,1,0,0,0 From 65d172aab37af9145f4574a3d723636e3171ed7f Mon Sep 17 00:00:00 2001 From: Guewen Baconnier Date: Tue, 21 Jan 2020 14:09:58 +0100 Subject: [PATCH 021/986] Add device restriction per user or operation group * If a user is assigned on a device, this user can use only this device: the API returns only this record. * If a user is not assigned on any device, the devices are filtered by the operation groups: no group on a device means everyone can use it, otherwise the user must be in at least one the group assigned to the device. --- shopfloor/models/__init__.py | 1 + shopfloor/models/res_users.py | 12 ++++++++++++ shopfloor/models/shopfloor_device.py | 14 +++++++++++++- shopfloor/services/device.py | 14 +++++++++++++- shopfloor/views/shopfloor_device_views.xml | 3 ++- 5 files changed, 41 insertions(+), 3 deletions(-) create mode 100644 shopfloor/models/res_users.py diff --git a/shopfloor/models/__init__.py b/shopfloor/models/__init__.py index 7bfd287ebaf..68782904869 100644 --- a/shopfloor/models/__init__.py +++ b/shopfloor/models/__init__.py @@ -3,3 +3,4 @@ from . import shopfloor_process from . import stock_picking_type from . import shopfloor_device +from . import res_users diff --git a/shopfloor/models/res_users.py b/shopfloor/models/res_users.py new file mode 100644 index 00000000000..e86bb27d2cc --- /dev/null +++ b/shopfloor/models/res_users.py @@ -0,0 +1,12 @@ +from odoo import fields, models + + +class ResUsers(models.Model): + _inherit = "res.users" + + # in practice, it's a one2one + shopfloor_device_ids = fields.One2many( + comodel_name="shopfloor.device", + inverse_name="user_id", + readonly=True, + ) diff --git a/shopfloor/models/shopfloor_device.py b/shopfloor/models/shopfloor_device.py index 986367f936c..f3f8ffc56b3 100644 --- a/shopfloor/models/shopfloor_device.py +++ b/shopfloor/models/shopfloor_device.py @@ -1,4 +1,4 @@ -from odoo import fields, models +from odoo import api, fields, models class ShopfloorDevice(models.Model): @@ -9,6 +9,7 @@ class ShopfloorDevice(models.Model): warehouse_id = fields.Many2one( "stock.warehouse", required=True, + default=lambda self: self._default_warehouse_id(), ) shopfloor_operation_group_ids = fields.Many2many( "shopfloor.operation.group", @@ -16,6 +17,7 @@ class ShopfloorDevice(models.Model): ) user_id = fields.Many2one( "res.users", + copy=False, help="Optional user using the device. The device will" "use this configuration when the users logs in the client " "application." @@ -23,3 +25,13 @@ class ShopfloorDevice(models.Model): shopfloor_current_process = fields.Char(readonly=True) shopfloor_last_call = fields.Char(readonly=True) shopfloor_picking_id = fields.Many2one('stock.picking', readonly=True) + + _sql_constraints = [ + ('user_id_uniq', 'unique(user_id)', 'A user can be assigned to only one device.'), + ] + + @api.model + def _default_warehouse_id(self): + wh = self.env['stock.warehouse'].search([]) + if len(wh) == 1: + return wh diff --git a/shopfloor/services/device.py b/shopfloor/services/device.py index 74857cd9334..5edd4a565b8 100644 --- a/shopfloor/services/device.py +++ b/shopfloor/services/device.py @@ -1,3 +1,4 @@ +from odoo import fields from odoo.addons.component.core import Component @@ -8,13 +9,24 @@ class ShopfloorDevice(Component): _expose_model = "shopfloor.device" def search(self, name_fragment=None): - # TODO filter on shopfloor.group or user in override of _get_base_search_domain domain = self._get_base_search_domain() if name_fragment: domain.append(("name", "ilike", name_fragment)) records = self.env[self._expose_model].search(domain) return {"size": len(records), "data": self._to_json(records)} + def _get_base_search_domain(self): + # shopfloor_device_ids is a one2one + user = self.env.user + assigned_device = fields.first(user.shopfloor_device_ids) + if assigned_device: + return [("id", "=", assigned_device.id)] + return [ + "|", + ("shopfloor_operation_group_ids", "=", False), + ("shopfloor_operation_group_ids.user_ids", "=", user.id), + ] + def _validator_search(self): return { "name_fragment": { diff --git a/shopfloor/views/shopfloor_device_views.xml b/shopfloor/views/shopfloor_device_views.xml index 2f92f309280..d1805604c79 100644 --- a/shopfloor/views/shopfloor_device_views.xml +++ b/shopfloor/views/shopfloor_device_views.xml @@ -4,9 +4,10 @@ shopfloor.device tree shopfloor.device - + + From 5c8dde6391d159cd0c735877dcef70354dfea688 Mon Sep 17 00:00:00 2001 From: Guewen Baconnier Date: Tue, 21 Jan 2020 14:36:06 +0100 Subject: [PATCH 022/986] Ignore validation of search return --- shopfloor/services/device.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/shopfloor/services/device.py b/shopfloor/services/device.py index 5edd4a565b8..f92d1a18d11 100644 --- a/shopfloor/services/device.py +++ b/shopfloor/services/device.py @@ -1,6 +1,8 @@ from odoo import fields from odoo.addons.component.core import Component +from odoo.addons.base_rest.components.service import skip_secure_response + class ShopfloorDevice(Component): _inherit = "base.shopfloor.service" @@ -8,6 +10,7 @@ class ShopfloorDevice(Component): _usage = "device" _expose_model = "shopfloor.device" + @skip_secure_response def search(self, name_fragment=None): domain = self._get_base_search_domain() if name_fragment: From b939da752760cbc6160b00e41ba4e0b242ef2440 Mon Sep 17 00:00:00 2001 From: Benoit Date: Tue, 21 Jan 2020 15:14:56 +0100 Subject: [PATCH 023/986] improve putaway service methods and add first version of tests --- shopfloor/services/shopfloor_service.py | 67 +++++++++++++++++++++++++ shopfloor/tests/__init__.py | 1 + shopfloor/tests/common.py | 17 +++++++ shopfloor/tests/test_putaway.py | 56 +++++++++++++++++++++ 4 files changed, 141 insertions(+) create mode 100644 shopfloor/tests/__init__.py create mode 100644 shopfloor/tests/common.py create mode 100644 shopfloor/tests/test_putaway.py diff --git a/shopfloor/services/shopfloor_service.py b/shopfloor/services/shopfloor_service.py index c3e68528b06..3e14289e332 100644 --- a/shopfloor/services/shopfloor_service.py +++ b/shopfloor/services/shopfloor_service.py @@ -1,4 +1,5 @@ from odoo.addons.component.core import Component +from odoo.addons.base_rest.components.service import to_int # TODO move in a pack service @@ -7,6 +8,72 @@ class ShopfloorService(Component): _name = "shopfloor.service" _usage = "shopfloor" + def scan_pack(self, pack_name): + pack = self.env['stock.quant.package'].search([('name', '=', pack_name)]) + company = self.env.user.company_id # FIXME add logic to get proper company + # FIXME add logic to get proper warehouse + warehouse = self.env['stock.warehouse'].search([])[0] + picking_type = warehouse.int_type_id # FIXME add logic to get picking type properly + product = pack.quant_ids[0].product_id # FIXME we consider only one product per pack + move_vals = { + 'picking_type_id': picking_type.id, + 'product_id': product.id, + 'location_id': pack.location_id.id, + 'location_dest_id': picking_type.default_location_dest_id.id, + 'name': product.name, + 'product_uom': product.uom_id.id, + 'product_uom_qty': pack.quant_ids[0].quantity, + 'company_id': company.id, + } + move = self.env['stock.move'].create(move_vals) + move._action_confirm() + pack_level = self.env['stock.package_level'].create({ + 'package_id': pack.id, + 'move_ids': [(6, 0, [move.id])], + 'company_id': company.id, + }) + move.picking_id.action_assign() + return_vals = { + 'name': pack.name, + 'location_name': pack.location_id.name, + 'location_dest_name': move.move_line_ids[0].location_dest_id.name, + 'product_name': move.name, + 'picking_name': move.picking_id.name, + 'location_id': pack.location_id.id, + 'location_dest_id': move.move_line_ids[0].location_dest_id.id, + 'move_id': move.id, +# 'allow_change_destination': True, #TODO + } + return return_vals + + def validate(self, move_id, location_name): + move = self.env['stock.move'].browse(move_id) + dest_location = self.env['stock.location'].search([('name', '=', location_name)]) + if move.move_line_ids[0].location_dest_id.id != dest_location.id: + move.move_line_ids[0].location_dest_id = dest_location.id + move.move_line_ids[0].qty_done = move.move_line_ids[0].product_uom_qty + move.picking_id.button_validate() + return True + + def cancel(self, move_id): + move = self.env['stock.move'].browse(move_id) + move.picking_id.cancel() + return True + + def _validator_validate(self): + return { + "move_id": {"coerce": to_int, "required": True, "type": "integer"}, + "location_name": {"type": "string", "nullable": False, "required": True}, + } + + def _validator_scan_pack(self): + return {"pack_name": {"type": "string", "nullable": False, "required": True}} + + def _validator_cancel(self): + return { + "move_id": {"coerce": to_int, "required": True, "type": "integer"}, + } + def get_pack(self, pack_name): """ Get pack informations diff --git a/shopfloor/tests/__init__.py b/shopfloor/tests/__init__.py new file mode 100644 index 00000000000..3a80e1ce963 --- /dev/null +++ b/shopfloor/tests/__init__.py @@ -0,0 +1 @@ +from . import test_putaway diff --git a/shopfloor/tests/common.py b/shopfloor/tests/common.py new file mode 100644 index 00000000000..f85f3c3f947 --- /dev/null +++ b/shopfloor/tests/common.py @@ -0,0 +1,17 @@ +from contextlib import contextmanager +from odoo.addons.base_rest.controllers.main import _PseudoCollection +from odoo.addons.component.core import WorkContext +from odoo.tests.common import SavepointCase + + +class CommonCase(SavepointCase): + + @contextmanager + def work_on_services(self, **params): + params = params or {} + collection = _PseudoCollection("shopfloor.service", self.env) + yield WorkContext( + model_name="rest.service.registration", + collection=collection, + **params + ) diff --git a/shopfloor/tests/test_putaway.py b/shopfloor/tests/test_putaway.py new file mode 100644 index 00000000000..45b87743d67 --- /dev/null +++ b/shopfloor/tests/test_putaway.py @@ -0,0 +1,56 @@ +from .common import CommonCase + + +class PutawayCase(CommonCase): + + def setUp(self, *args, **kwargs): + super(PutawayCase, self).setUp(*args, **kwargs) + in_location = self.env.ref('stock.stock_location_company').child_ids[0] + stock_location = self.env.ref('stock.stock_location_stock') + self.productA = self.env['product.product'].create({'name': 'Product A', 'type': 'product'}) + self.packA = self.env['stock.quant.package'].create({ + 'location_id': in_location.id + }) + self.quantA = self.env['stock.quant'].create({ + 'product_id': self.productA.id, + 'location_id': in_location.id, + 'quantity': 1, + 'package_id': self.packA.id, + }) + self.env['stock.putaway.rule'].create({ + 'product_id': self.productA.id, + 'location_in_id': stock_location.id, + 'location_out_id': stock_location.child_ids[0].id, + }) + with self.work_on_services( + ) as work: + self.service = work.component(usage="shopfloor") + + def test_scan_pack(self): + pack_name = self.packA.name + params = { + 'pack_name': pack_name, + } + response = self.service.dispatch("scan_pack", params=params) + move_id = response['move_id'] + params ={ + 'move_id': move_id, + 'location_name': response['location_dest_name'], + } + location_dest_id = self.env['stock.location'].search([ + ('name', '=', params['location_name']) + ]).id + new_loc_quant = self.env['stock.quant'].search([ + ('product_id', '=', self.productA.id), + ('location_id', '=', location_dest_id) + ]) + self.assertFalse(new_loc_quant) + response = self.service.dispatch("validate", params=params) + new_loc_quant = self.env['stock.quant'].search([ + ('product_id', '=', self.productA.id), + ('location_id', '=', location_dest_id) + ]) + move = self.env['stock.move'].browse(move_id) + self.assertEquals(move.state, 'done') + self.assertEquals(self.quantA.quantity, 0) + self.assertEquals(new_loc_quant.quantity, move.product_uom_qty) From 8377a66bb43c5dcee541efa71b036578bc3ae9a8 Mon Sep 17 00:00:00 2001 From: Guewen Baconnier Date: Tue, 21 Jan 2020 15:16:51 +0100 Subject: [PATCH 024/986] Add demo data for groups and devices --- shopfloor/__manifest__.py | 2 ++ shopfloor/demo/shopfloor_device_demo.xml | 21 +++++++++++++++++++ .../demo/shopfloor_operation_group_demo.xml | 8 +++++++ shopfloor/models/shopfloor_device.py | 4 +++- 4 files changed, 34 insertions(+), 1 deletion(-) create mode 100644 shopfloor/demo/shopfloor_device_demo.xml create mode 100644 shopfloor/demo/shopfloor_operation_group_demo.xml diff --git a/shopfloor/__manifest__.py b/shopfloor/__manifest__.py index 7bae4764a91..5110d5a2eb4 100644 --- a/shopfloor/__manifest__.py +++ b/shopfloor/__manifest__.py @@ -28,5 +28,7 @@ ], "demo": [ "demo/auth_api_key_demo.xml", + "demo/shopfloor_operation_group_demo.xml", + "demo/shopfloor_device_demo.xml", ] } diff --git a/shopfloor/demo/shopfloor_device_demo.xml b/shopfloor/demo/shopfloor_device_demo.xml new file mode 100644 index 00000000000..bd9ed3f2507 --- /dev/null +++ b/shopfloor/demo/shopfloor_device_demo.xml @@ -0,0 +1,21 @@ + + + + Highbay Truck 1 + + + + + + Highbay Truck 2 + + + + + + Shelf 1 + + + + + diff --git a/shopfloor/demo/shopfloor_operation_group_demo.xml b/shopfloor/demo/shopfloor_operation_group_demo.xml new file mode 100644 index 00000000000..4f8104efb52 --- /dev/null +++ b/shopfloor/demo/shopfloor_operation_group_demo.xml @@ -0,0 +1,8 @@ + + + + HighBay + + + + diff --git a/shopfloor/models/shopfloor_device.py b/shopfloor/models/shopfloor_device.py index f3f8ffc56b3..bd32e5c2e19 100644 --- a/shopfloor/models/shopfloor_device.py +++ b/shopfloor/models/shopfloor_device.py @@ -13,7 +13,9 @@ class ShopfloorDevice(models.Model): ) shopfloor_operation_group_ids = fields.Many2many( "shopfloor.operation.group", - string="Shopfloor Operation Groups" + string="Shopfloor Operation Groups", + help="When unset, all users can use the device. When set," + "only users belonging to at least one group can use the device.", ) user_id = fields.Many2one( "res.users", From 96bb0c9339cebdb2c6fd4bda9d972fc01d46c2ba Mon Sep 17 00:00:00 2001 From: Guewen Baconnier Date: Tue, 21 Jan 2020 15:36:12 +0100 Subject: [PATCH 025/986] Add demo data for menus and processes --- shopfloor/__manifest__.py | 2 ++ shopfloor/demo/shopfloor_menu_demo.xml | 9 +++++ shopfloor/demo/shopfloor_process_demo.xml | 7 ++++ shopfloor/models/shopfloor_device.py | 1 + shopfloor/services/__init__.py | 1 + shopfloor/services/menu.py | 41 +++++++++++++++++++++++ 6 files changed, 61 insertions(+) create mode 100644 shopfloor/demo/shopfloor_menu_demo.xml create mode 100644 shopfloor/demo/shopfloor_process_demo.xml create mode 100644 shopfloor/services/menu.py diff --git a/shopfloor/__manifest__.py b/shopfloor/__manifest__.py index 5110d5a2eb4..f94a70f42e9 100644 --- a/shopfloor/__manifest__.py +++ b/shopfloor/__manifest__.py @@ -28,6 +28,8 @@ ], "demo": [ "demo/auth_api_key_demo.xml", + "demo/shopfloor_process_demo.xml", + "demo/shopfloor_menu_demo.xml", "demo/shopfloor_operation_group_demo.xml", "demo/shopfloor_device_demo.xml", ] diff --git a/shopfloor/demo/shopfloor_menu_demo.xml b/shopfloor/demo/shopfloor_menu_demo.xml new file mode 100644 index 00000000000..7d643a0e1f9 --- /dev/null +++ b/shopfloor/demo/shopfloor_menu_demo.xml @@ -0,0 +1,9 @@ + + + + Put-Away Reach Truck + 10 + + + + diff --git a/shopfloor/demo/shopfloor_process_demo.xml b/shopfloor/demo/shopfloor_process_demo.xml new file mode 100644 index 00000000000..30964d7b23c --- /dev/null +++ b/shopfloor/demo/shopfloor_process_demo.xml @@ -0,0 +1,7 @@ + + + + Put-Away Reach Truck + + + diff --git a/shopfloor/models/shopfloor_device.py b/shopfloor/models/shopfloor_device.py index bd32e5c2e19..ab47e8ea2f8 100644 --- a/shopfloor/models/shopfloor_device.py +++ b/shopfloor/models/shopfloor_device.py @@ -11,6 +11,7 @@ class ShopfloorDevice(models.Model): required=True, default=lambda self: self._default_warehouse_id(), ) + # TODO: remove shopfloor_ prefix shopfloor_operation_group_ids = fields.Many2many( "shopfloor.operation.group", string="Shopfloor Operation Groups", diff --git a/shopfloor/services/__init__.py b/shopfloor/services/__init__.py index 7369a5602f7..60ec641820f 100644 --- a/shopfloor/services/__init__.py +++ b/shopfloor/services/__init__.py @@ -2,3 +2,4 @@ # TODO rename file shop_floor_service to pack from . import shopfloor_service from . import device +from . import menu diff --git a/shopfloor/services/menu.py b/shopfloor/services/menu.py new file mode 100644 index 00000000000..c61f5c66b63 --- /dev/null +++ b/shopfloor/services/menu.py @@ -0,0 +1,41 @@ +from odoo.addons.component.core import Component + +from odoo.addons.base_rest.components.service import skip_secure_response + + +class ShopfloorMenu(Component): + _inherit = "base.shopfloor.service" + _name = "shopfloor.menu" + _usage = "menu" + _expose_model = "shopfloor.menu" + + @skip_secure_response + 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 {"size": len(records), "data": self._to_json(records)} + + def _get_base_search_domain(self): + user = self.env.user + return [ + "|", + ("operation_group_ids", "=", False), + ("operation_group_ids.user_ids", "=", user.id), + ] + + def _validator_search(self): + return { + "name_fragment": { + "type": "string", + "nullable": True, + "required": False, + } + } + + def _convert_one_record(self, record): + return { + "id": record.id, + "name": record.name, + } From 8613953862d6578476d2022344e4430e248322dc Mon Sep 17 00:00:00 2001 From: Guewen Baconnier Date: Tue, 21 Jan 2020 15:43:13 +0100 Subject: [PATCH 026/986] Rename field (sorry, you'll have to rebuild your db) --- shopfloor/models/shopfloor_device.py | 3 +-- shopfloor/services/device.py | 4 ++-- shopfloor/views/shopfloor_device_views.xml | 6 +++--- 3 files changed, 6 insertions(+), 7 deletions(-) diff --git a/shopfloor/models/shopfloor_device.py b/shopfloor/models/shopfloor_device.py index ab47e8ea2f8..f84c78322e3 100644 --- a/shopfloor/models/shopfloor_device.py +++ b/shopfloor/models/shopfloor_device.py @@ -11,8 +11,7 @@ class ShopfloorDevice(models.Model): required=True, default=lambda self: self._default_warehouse_id(), ) - # TODO: remove shopfloor_ prefix - shopfloor_operation_group_ids = fields.Many2many( + operation_group_ids = fields.Many2many( "shopfloor.operation.group", string="Shopfloor Operation Groups", help="When unset, all users can use the device. When set," diff --git a/shopfloor/services/device.py b/shopfloor/services/device.py index f92d1a18d11..c53060838e9 100644 --- a/shopfloor/services/device.py +++ b/shopfloor/services/device.py @@ -26,8 +26,8 @@ def _get_base_search_domain(self): return [("id", "=", assigned_device.id)] return [ "|", - ("shopfloor_operation_group_ids", "=", False), - ("shopfloor_operation_group_ids.user_ids", "=", user.id), + ("operation_group_ids", "=", False), + ("operation_group_ids.user_ids", "=", user.id), ] def _validator_search(self): diff --git a/shopfloor/views/shopfloor_device_views.xml b/shopfloor/views/shopfloor_device_views.xml index d1805604c79..1f6dc4cc251 100644 --- a/shopfloor/views/shopfloor_device_views.xml +++ b/shopfloor/views/shopfloor_device_views.xml @@ -7,7 +7,7 @@ - +
@@ -25,7 +25,7 @@ - + @@ -47,7 +47,7 @@ - +
From ed60994b4bd66a8e7fe29a8cb8150a8f013dcb48 Mon Sep 17 00:00:00 2001 From: Benoit Date: Tue, 21 Jan 2020 16:03:21 +0100 Subject: [PATCH 027/986] [IMP] add validator return for search shopfloor device --- shopfloor/services/device.py | 31 +++++++++++++++++++++++++++---- 1 file changed, 27 insertions(+), 4 deletions(-) diff --git a/shopfloor/services/device.py b/shopfloor/services/device.py index c53060838e9..8e6ede204a8 100644 --- a/shopfloor/services/device.py +++ b/shopfloor/services/device.py @@ -1,7 +1,7 @@ from odoo import fields from odoo.addons.component.core import Component -from odoo.addons.base_rest.components.service import skip_secure_response +from odoo.addons.base_rest.components.service import to_int class ShopfloorDevice(Component): @@ -10,7 +10,6 @@ class ShopfloorDevice(Component): _usage = "device" _expose_model = "shopfloor.device" - @skip_secure_response def search(self, name_fragment=None): domain = self._get_base_search_domain() if name_fragment: @@ -39,10 +38,34 @@ def _validator_search(self): } } + def _validator_return_search(self): + return { + "size": {"coerce": to_int, "required": True, "type": "integer"}, + "data": { + "type": "list", + "schema": { + "type": "dict", + "schema": { + "id": {"coerce": to_int, "required": True, "type": "integer"}, + "name": {"type": "string", "nullable": False, "required": True}, + "warehouse": { + "type": "dict", + "schema": { + "id": {"coerce": to_int, "required": True, "type": "integer"}, + "name": {"type": "string", "nullable": False, "required": True}, + } + } + } + } + } + } + def _convert_one_record(self, record): return { "id": record.id, "name": record.name, - "warehouse_id": record.warehouse_id.id, - "warehouse": record.warehouse_id.name, + "warehouse": { + "id": record.warehouse_id.id, + "name": record.warehouse_id.name, + } } From 3b94b07875a00980897c0a0f932637f40ea91433 Mon Sep 17 00:00:00 2001 From: Guewen Baconnier Date: Tue, 21 Jan 2020 16:16:30 +0100 Subject: [PATCH 028/986] Add schema validation for menu /search output --- .isort.cfg | 2 +- shopfloor/services/menu.py | 42 ++++++++++++++++++++++---------------- 2 files changed, 25 insertions(+), 19 deletions(-) diff --git a/.isort.cfg b/.isort.cfg index 5751c40dd2b..98b216f744d 100644 --- a/.isort.cfg +++ b/.isort.cfg @@ -9,4 +9,4 @@ line_length=88 known_odoo=odoo known_odoo_addons=odoo.addons sections=FUTURE,STDLIB,THIRDPARTY,ODOO,ODOO_ADDONS,FIRSTPARTY,LOCALFOLDER -known_third_party= +known_third_party=setuptools diff --git a/shopfloor/services/menu.py b/shopfloor/services/menu.py index c61f5c66b63..c4270d1b9a5 100644 --- a/shopfloor/services/menu.py +++ b/shopfloor/services/menu.py @@ -1,7 +1,6 @@ +from odoo.addons.base_rest.components.service import to_int from odoo.addons.component.core import Component -from odoo.addons.base_rest.components.service import skip_secure_response - class ShopfloorMenu(Component): _inherit = "base.shopfloor.service" @@ -9,14 +8,6 @@ class ShopfloorMenu(Component): _usage = "menu" _expose_model = "shopfloor.menu" - @skip_secure_response - 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 {"size": len(records), "data": self._to_json(records)} - def _get_base_search_domain(self): user = self.env.user return [ @@ -25,17 +16,32 @@ def _get_base_search_domain(self): ("operation_group_ids.user_ids", "=", user.id), ] + 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 {"size": len(records), "data": self._to_json(records)} + def _validator_search(self): return { - "name_fragment": { - "type": "string", - "nullable": True, - "required": False, - } + "name_fragment": {"type": "string", "nullable": True, "required": False} } - def _convert_one_record(self, record): + def _validator_return_search(self): return { - "id": record.id, - "name": record.name, + "size": {"coerce": to_int, "required": True, "type": "integer"}, + "data": { + "type": "list", + "schema": { + "type": "dict", + "schema": { + "id": {"coerce": to_int, "required": True, "type": "integer"}, + "name": {"type": "string", "nullable": False, "required": True}, + }, + }, + }, } + + def _convert_one_record(self, record): + return {"id": record.id, "name": record.name} From a1d34092663074e7fbf886b13772c2255b8801e6 Mon Sep 17 00:00:00 2001 From: Benoit Date: Tue, 21 Jan 2020 16:24:28 +0100 Subject: [PATCH 029/986] add process name and menu in request header --- shopfloor/controllers/main.py | 22 +++++++++++++++++++++- shopfloor/services/service.py | 22 ++++++++++++++++++++-- 2 files changed, 41 insertions(+), 3 deletions(-) diff --git a/shopfloor/controllers/main.py b/shopfloor/controllers/main.py index 94b9d46c509..4a95642ab99 100644 --- a/shopfloor/controllers/main.py +++ b/shopfloor/controllers/main.py @@ -1,5 +1,6 @@ from odoo.addons.base_rest.controllers import main -import json +from odoo.exceptions import MissingError +from odoo.http import request class ShopfloorController(main.RestController): @@ -7,5 +8,24 @@ class ShopfloorController(main.RestController): _collection_name = "shopfloor.services" _default_auth = "api_key" + @classmethod + def _get_process_from_headers(cls, headers): + process_name = headers.get("HTTP_SERVICE_CTX_PROCESS_NAME") + return process_name + @classmethod + def _get_process_menu_from_headers(cls, headers): + process_menu = headers.get("HTTP_SERVICE_CTX_PROCESS_MENU") + return process_menu + def _get_component_context(self): + """ + This method adds the component context: + * the process name + * the process menu + """ + res = super(ShopfloorController, self)._get_component_context() + headers = request.httprequest.environ + res["process_name"] = self._get_process_from_headers(headers) + res["process_menu"] = self._get_process_menu_from_headers(headers) + return res diff --git a/shopfloor/services/service.py b/shopfloor/services/service.py index f52894f558a..7d6cdb05dde 100644 --- a/shopfloor/services/service.py +++ b/shopfloor/services/service.py @@ -40,7 +40,7 @@ def _get_openapi_default_parameters(self): demo_api_key = self.env.ref( "shopfloor.api_key_demo", raise_if_not_found=False ).key - defaults.append( + defaults.extend([ { "name": "API-KEY", "in": "header", @@ -49,6 +49,24 @@ def _get_openapi_default_parameters(self): "schema": {"type": "string"}, "style": "simple", "value": demo_api_key, + }, + { + "name": "SERVICE_CTX_PROCESS_NAME", + "in": "header", + "description": "Name of the current process", + "required": True, + "schema": {"type": "string"}, + "style": "simple", + "value": "Put-Away Reach Truck", + }, + { + "name": "SERVICE_CTX_PROCESS_MENU", + "in": "header", + "description": "Name of the current process menu", + "required": True, + "schema": {"type": "string"}, + "style": "simple", + "value": "Put-Away Reach Truck", } - ) + ]) return defaults From 54004c5e253bf091b6071aa0e503879586ffe94c Mon Sep 17 00:00:00 2001 From: Guewen Baconnier Date: Tue, 21 Jan 2020 16:46:15 +0100 Subject: [PATCH 030/986] Extract pack service in rest api --- shopfloor/services/__init__.py | 3 +- shopfloor/services/pack.py | 124 ++++++++++++++++++++++++ shopfloor/services/shopfloor_service.py | 93 ------------------ 3 files changed, 125 insertions(+), 95 deletions(-) create mode 100644 shopfloor/services/pack.py delete mode 100644 shopfloor/services/shopfloor_service.py diff --git a/shopfloor/services/__init__.py b/shopfloor/services/__init__.py index 60ec641820f..1fa6f94a433 100644 --- a/shopfloor/services/__init__.py +++ b/shopfloor/services/__init__.py @@ -1,5 +1,4 @@ from . import service -# TODO rename file shop_floor_service to pack -from . import shopfloor_service from . import device from . import menu +from . import pack diff --git a/shopfloor/services/pack.py b/shopfloor/services/pack.py new file mode 100644 index 00000000000..e0dcd1980e3 --- /dev/null +++ b/shopfloor/services/pack.py @@ -0,0 +1,124 @@ +from odoo.addons.base_rest.components.service import to_int +from odoo.addons.component.core import Component + + +class ShopfloorPack(Component): + _inherit = "base.shopfloor.service" + _name = "shopfloor.pack" + _usage = "pack" + + def scan(self, pack_name): + pack = self.env["stock.quant.package"].search([("name", "=", pack_name)]) + company = self.env.user.company_id # FIXME add logic to get proper company + # FIXME add logic to get proper warehouse + warehouse = self.env["stock.warehouse"].search([])[0] + picking_type = ( + warehouse.int_type_id + ) # FIXME add logic to get picking type properly + product = pack.quant_ids[ + 0 + ].product_id # FIXME we consider only one product per pack + move_vals = { + "picking_type_id": picking_type.id, + "product_id": product.id, + "location_id": pack.location_id.id, + "location_dest_id": picking_type.default_location_dest_id.id, + "name": product.name, + "product_uom": product.uom_id.id, + "product_uom_qty": pack.quant_ids[0].quantity, + "company_id": company.id, + } + move = self.env["stock.move"].create(move_vals) + move._action_confirm() + self.env["stock.package_level"].create( + { + "package_id": pack.id, + "move_ids": [(6, 0, [move.id])], + "company_id": company.id, + } + ) + move.picking_id.action_assign() + return_vals = { + "name": pack.name, + "location_name": pack.location_id.name, + "location_dest_name": move.move_line_ids[0].location_dest_id.name, + "product_name": move.name, + "picking_name": move.picking_id.name, + "location_id": pack.location_id.id, + "location_dest_id": move.move_line_ids[0].location_dest_id.id, + "move_id": move.id, + # 'allow_change_destination': True, #TODO + } + return return_vals + + def validate(self, move_id, location_name): + move = self.env["stock.move"].browse(move_id) + dest_location = self.env["stock.location"].search( + [("name", "=", location_name)] + ) + if move.move_line_ids[0].location_dest_id.id != dest_location.id: + move.move_line_ids[0].location_dest_id = dest_location.id + move.move_line_ids[0].qty_done = move.move_line_ids[0].product_uom_qty + move.picking_id.button_validate() + return True + + def cancel(self, move_id): + move = self.env["stock.move"].browse(move_id) + move.picking_id.cancel() + return True + + def _validator_cancel(self): + return {"move_id": {"coerce": to_int, "required": True, "type": "integer"}} + + def _validator_validate(self): + return { + "move_id": {"coerce": to_int, "required": True, "type": "integer"}, + "location_name": {"type": "string", "nullable": False, "required": True}, + } + + def _validator_scan(self): + return {"pack_name": {"type": "string", "nullable": False, "required": True}} + + def _validator_return_scan(self): + return {"data": self._record_return_schema} + + def get_by_name(self, pack_name): + """ + Get pack informations + """ + pack = self.env["stock.quant.package"].search( + [("name", "=", pack_name)], + # TODO, is it what we want? error if not found? + limit=1, + ) + return self._to_json(pack)[:1] + + def _validator_get_by_name(self): + return {"pack_name": {"type": "string", "nullable": False, "required": True}} + + def _validator_return_get_by_name(self): + return {"data": self._record_return_schema} + + def _convert_one_record(self, record): + return { + "id": record.id, + "name": record.name, + "location": {"id": record.location_id.id, "name": record.location_id.name}, + } + + @property + def _record_return_schema(self): + return { + "type": "dict", + "schema": { + "id": {"coerce": to_int, "required": True, "type": "integer"}, + "name": {"type": "string", "nullable": False, "required": True}, + "location": { + "type": "dict", + "schema": { + "id": {"coerce": to_int, "required": True, "type": "integer"}, + "name": {"type": "string", "nullable": False, "required": True}, + }, + }, + }, + } diff --git a/shopfloor/services/shopfloor_service.py b/shopfloor/services/shopfloor_service.py deleted file mode 100644 index 3e14289e332..00000000000 --- a/shopfloor/services/shopfloor_service.py +++ /dev/null @@ -1,93 +0,0 @@ -from odoo.addons.component.core import Component -from odoo.addons.base_rest.components.service import to_int - - -# TODO move in a pack service -class ShopfloorService(Component): - _inherit = "base.shopfloor.service" - _name = "shopfloor.service" - _usage = "shopfloor" - - def scan_pack(self, pack_name): - pack = self.env['stock.quant.package'].search([('name', '=', pack_name)]) - company = self.env.user.company_id # FIXME add logic to get proper company - # FIXME add logic to get proper warehouse - warehouse = self.env['stock.warehouse'].search([])[0] - picking_type = warehouse.int_type_id # FIXME add logic to get picking type properly - product = pack.quant_ids[0].product_id # FIXME we consider only one product per pack - move_vals = { - 'picking_type_id': picking_type.id, - 'product_id': product.id, - 'location_id': pack.location_id.id, - 'location_dest_id': picking_type.default_location_dest_id.id, - 'name': product.name, - 'product_uom': product.uom_id.id, - 'product_uom_qty': pack.quant_ids[0].quantity, - 'company_id': company.id, - } - move = self.env['stock.move'].create(move_vals) - move._action_confirm() - pack_level = self.env['stock.package_level'].create({ - 'package_id': pack.id, - 'move_ids': [(6, 0, [move.id])], - 'company_id': company.id, - }) - move.picking_id.action_assign() - return_vals = { - 'name': pack.name, - 'location_name': pack.location_id.name, - 'location_dest_name': move.move_line_ids[0].location_dest_id.name, - 'product_name': move.name, - 'picking_name': move.picking_id.name, - 'location_id': pack.location_id.id, - 'location_dest_id': move.move_line_ids[0].location_dest_id.id, - 'move_id': move.id, -# 'allow_change_destination': True, #TODO - } - return return_vals - - def validate(self, move_id, location_name): - move = self.env['stock.move'].browse(move_id) - dest_location = self.env['stock.location'].search([('name', '=', location_name)]) - if move.move_line_ids[0].location_dest_id.id != dest_location.id: - move.move_line_ids[0].location_dest_id = dest_location.id - move.move_line_ids[0].qty_done = move.move_line_ids[0].product_uom_qty - move.picking_id.button_validate() - return True - - def cancel(self, move_id): - move = self.env['stock.move'].browse(move_id) - move.picking_id.cancel() - return True - - def _validator_validate(self): - return { - "move_id": {"coerce": to_int, "required": True, "type": "integer"}, - "location_name": {"type": "string", "nullable": False, "required": True}, - } - - def _validator_scan_pack(self): - return {"pack_name": {"type": "string", "nullable": False, "required": True}} - - def _validator_cancel(self): - return { - "move_id": {"coerce": to_int, "required": True, "type": "integer"}, - } - - def get_pack(self, pack_name): - """ - Get pack informations - """ - pack = self.env['stock.quant.package'].search([('name', '=', pack_name)]) - return self._to_json(pack) - - def _validator_get_pack(self): - return {"pack_name": {"type": "string", "nullable": False, "required": True}} - - def _to_json(self, pack): - res = { - "id": pack.id, - "name": pack.name, - "location": pack.location_id.name, - } - return res From 1db37ed7a3d914b434ab4aac1248caaf3ae92910 Mon Sep 17 00:00:00 2001 From: Guewen Baconnier Date: Tue, 21 Jan 2020 16:48:38 +0100 Subject: [PATCH 031/986] Add docstrings on rest methods (they are shown in swagger) --- shopfloor/services/device.py | 33 +++++++++++++++++++-------------- shopfloor/services/menu.py | 1 + shopfloor/services/pack.py | 1 + 3 files changed, 21 insertions(+), 14 deletions(-) diff --git a/shopfloor/services/device.py b/shopfloor/services/device.py index 8e6ede204a8..b2609f4245f 100644 --- a/shopfloor/services/device.py +++ b/shopfloor/services/device.py @@ -1,7 +1,7 @@ from odoo import fields -from odoo.addons.component.core import Component from odoo.addons.base_rest.components.service import to_int +from odoo.addons.component.core import Component class ShopfloorDevice(Component): @@ -11,6 +11,7 @@ class ShopfloorDevice(Component): _expose_model = "shopfloor.device" def search(self, name_fragment=None): + """List available devices for current user""" domain = self._get_base_search_domain() if name_fragment: domain.append(("name", "ilike", name_fragment)) @@ -31,11 +32,7 @@ def _get_base_search_domain(self): def _validator_search(self): return { - "name_fragment": { - "type": "string", - "nullable": True, - "required": False, - } + "name_fragment": {"type": "string", "nullable": True, "required": False} } def _validator_return_search(self): @@ -51,13 +48,21 @@ def _validator_return_search(self): "warehouse": { "type": "dict", "schema": { - "id": {"coerce": to_int, "required": True, "type": "integer"}, - "name": {"type": "string", "nullable": False, "required": True}, - } - } - } - } - } + "id": { + "coerce": to_int, + "required": True, + "type": "integer", + }, + "name": { + "type": "string", + "nullable": False, + "required": True, + }, + }, + }, + }, + }, + }, } def _convert_one_record(self, record): @@ -67,5 +72,5 @@ def _convert_one_record(self, record): "warehouse": { "id": record.warehouse_id.id, "name": record.warehouse_id.name, - } + }, } diff --git a/shopfloor/services/menu.py b/shopfloor/services/menu.py index c4270d1b9a5..3fdf5182a09 100644 --- a/shopfloor/services/menu.py +++ b/shopfloor/services/menu.py @@ -17,6 +17,7 @@ def _get_base_search_domain(self): ] def search(self, name_fragment=None): + """List available menu entries for current user""" domain = self._get_base_search_domain() if name_fragment: domain.append(("name", "ilike", name_fragment)) diff --git a/shopfloor/services/pack.py b/shopfloor/services/pack.py index e0dcd1980e3..bac63d79bbf 100644 --- a/shopfloor/services/pack.py +++ b/shopfloor/services/pack.py @@ -8,6 +8,7 @@ class ShopfloorPack(Component): _usage = "pack" def scan(self, pack_name): + """Scan a pack barcode""" pack = self.env["stock.quant.package"].search([("name", "=", pack_name)]) company = self.env.user.company_id # FIXME add logic to get proper company # FIXME add logic to get proper warehouse From 6236dc8486e211e1411066bd2c1778ff14318dec Mon Sep 17 00:00:00 2001 From: Benoit Date: Tue, 21 Jan 2020 16:56:31 +0100 Subject: [PATCH 032/986] ref header params make it extendable --- shopfloor/controllers/main.py | 16 ++++------------ 1 file changed, 4 insertions(+), 12 deletions(-) diff --git a/shopfloor/controllers/main.py b/shopfloor/controllers/main.py index 4a95642ab99..5909552712b 100644 --- a/shopfloor/controllers/main.py +++ b/shopfloor/controllers/main.py @@ -8,16 +8,6 @@ class ShopfloorController(main.RestController): _collection_name = "shopfloor.services" _default_auth = "api_key" - @classmethod - def _get_process_from_headers(cls, headers): - process_name = headers.get("HTTP_SERVICE_CTX_PROCESS_NAME") - return process_name - - @classmethod - def _get_process_menu_from_headers(cls, headers): - process_menu = headers.get("HTTP_SERVICE_CTX_PROCESS_MENU") - return process_menu - def _get_component_context(self): """ This method adds the component context: @@ -26,6 +16,8 @@ def _get_component_context(self): """ res = super(ShopfloorController, self)._get_component_context() headers = request.httprequest.environ - res["process_name"] = self._get_process_from_headers(headers) - res["process_menu"] = self._get_process_menu_from_headers(headers) + for k, v in headers.items(): + if k.startswith('HTTP_SERVICE_CTX_'): + key_name = k[17:].lower() + res[key_name] = v return res From 247ee82867122f5067f0de319e79a26faaf34164 Mon Sep 17 00:00:00 2001 From: Guewen Baconnier Date: Tue, 21 Jan 2020 17:14:42 +0100 Subject: [PATCH 033/986] Fix service test --- shopfloor/controllers/main.py | 6 +-- shopfloor/services/pack.py | 7 ++- shopfloor/services/service.py | 70 +++++++++++++------------ shopfloor/tests/test_putaway.py | 92 ++++++++++++++++++--------------- 4 files changed, 91 insertions(+), 84 deletions(-) diff --git a/shopfloor/controllers/main.py b/shopfloor/controllers/main.py index 5909552712b..4df8196e6ea 100644 --- a/shopfloor/controllers/main.py +++ b/shopfloor/controllers/main.py @@ -1,11 +1,11 @@ -from odoo.addons.base_rest.controllers import main -from odoo.exceptions import MissingError from odoo.http import request +from odoo.addons.base_rest.controllers import main + class ShopfloorController(main.RestController): _root_path = "/shopfloor/" - _collection_name = "shopfloor.services" + _collection_name = "shopfloor.service" _default_auth = "api_key" def _get_component_context(self): diff --git a/shopfloor/services/pack.py b/shopfloor/services/pack.py index bac63d79bbf..bf098599a30 100644 --- a/shopfloor/services/pack.py +++ b/shopfloor/services/pack.py @@ -1,4 +1,4 @@ -from odoo.addons.base_rest.components.service import to_int +from odoo.addons.base_rest.components.service import skip_secure_response, to_int from odoo.addons.component.core import Component @@ -7,6 +7,8 @@ class ShopfloorPack(Component): _name = "shopfloor.pack" _usage = "pack" + # TODO define the return schema and add the validator method + @skip_secure_response def scan(self, pack_name): """Scan a pack barcode""" pack = self.env["stock.quant.package"].search([("name", "=", pack_name)]) @@ -80,9 +82,6 @@ def _validator_validate(self): def _validator_scan(self): return {"pack_name": {"type": "string", "nullable": False, "required": True}} - def _validator_return_scan(self): - return {"data": self._record_return_schema} - def get_by_name(self, pack_name): """ Get pack informations diff --git a/shopfloor/services/service.py b/shopfloor/services/service.py index 7d6cdb05dde..d19ba3db273 100644 --- a/shopfloor/services/service.py +++ b/shopfloor/services/service.py @@ -1,13 +1,14 @@ from odoo import _ +from odoo.exceptions import MissingError from odoo.osv import expression + from odoo.addons.component.core import AbstractComponent -from odoo.exceptions import MissingError class BaseShopfloorService(AbstractComponent): _inherit = "base.rest.service" _name = "base.shopfloor.service" - _collection = "shopfloor.services" + _collection = "shopfloor.service" _expose_model = None def _get(self, _id): @@ -16,8 +17,7 @@ def _get(self, _id): record = self.env[self._expose_model].search(domain) if not record: raise MissingError( - _("The record %s %s does not exist") - % (self._expose_model, _id) + _("The record %s %s does not exist") % (self._expose_model, _id) ) else: return record @@ -25,7 +25,7 @@ def _get(self, _id): def _get_base_search_domain(self): return [] - def _convert_one_record(record): + def _convert_one_record(self, record): """To implement in service Components""" return {} @@ -40,33 +40,35 @@ def _get_openapi_default_parameters(self): demo_api_key = self.env.ref( "shopfloor.api_key_demo", raise_if_not_found=False ).key - defaults.extend([ - { - "name": "API-KEY", - "in": "header", - "description": "API key for Authorization", - "required": True, - "schema": {"type": "string"}, - "style": "simple", - "value": demo_api_key, - }, - { - "name": "SERVICE_CTX_PROCESS_NAME", - "in": "header", - "description": "Name of the current process", - "required": True, - "schema": {"type": "string"}, - "style": "simple", - "value": "Put-Away Reach Truck", - }, - { - "name": "SERVICE_CTX_PROCESS_MENU", - "in": "header", - "description": "Name of the current process menu", - "required": True, - "schema": {"type": "string"}, - "style": "simple", - "value": "Put-Away Reach Truck", - } - ]) + defaults.extend( + [ + { + "name": "API-KEY", + "in": "header", + "description": "API key for Authorization", + "required": True, + "schema": {"type": "string"}, + "style": "simple", + "value": demo_api_key, + }, + { + "name": "SERVICE_CTX_PROCESS_NAME", + "in": "header", + "description": "Name of the current process", + "required": True, + "schema": {"type": "string"}, + "style": "simple", + "value": "Put-Away Reach Truck", + }, + { + "name": "SERVICE_CTX_PROCESS_MENU", + "in": "header", + "description": "Name of the current process menu", + "required": True, + "schema": {"type": "string"}, + "style": "simple", + "value": "Put-Away Reach Truck", + }, + ] + ) return defaults diff --git a/shopfloor/tests/test_putaway.py b/shopfloor/tests/test_putaway.py index 45b87743d67..53c325cb0c4 100644 --- a/shopfloor/tests/test_putaway.py +++ b/shopfloor/tests/test_putaway.py @@ -2,55 +2,61 @@ class PutawayCase(CommonCase): - def setUp(self, *args, **kwargs): super(PutawayCase, self).setUp(*args, **kwargs) - in_location = self.env.ref('stock.stock_location_company').child_ids[0] - stock_location = self.env.ref('stock.stock_location_stock') - self.productA = self.env['product.product'].create({'name': 'Product A', 'type': 'product'}) - self.packA = self.env['stock.quant.package'].create({ - 'location_id': in_location.id - }) - self.quantA = self.env['stock.quant'].create({ - 'product_id': self.productA.id, - 'location_id': in_location.id, - 'quantity': 1, - 'package_id': self.packA.id, - }) - self.env['stock.putaway.rule'].create({ - 'product_id': self.productA.id, - 'location_in_id': stock_location.id, - 'location_out_id': stock_location.child_ids[0].id, - }) - with self.work_on_services( - ) as work: - self.service = work.component(usage="shopfloor") + in_location = self.env.ref("stock.stock_location_company").child_ids[0] + stock_location = self.env.ref("stock.stock_location_stock") + self.productA = self.env["product.product"].create( + {"name": "Product A", "type": "product"} + ) + self.packA = self.env["stock.quant.package"].create( + {"location_id": in_location.id} + ) + self.quantA = self.env["stock.quant"].create( + { + "product_id": self.productA.id, + "location_id": in_location.id, + "quantity": 1, + "package_id": self.packA.id, + } + ) + self.env["stock.putaway.rule"].create( + { + "product_id": self.productA.id, + "location_in_id": stock_location.id, + "location_out_id": stock_location.child_ids[0].id, + } + ) + + with self.work_on_services() as work: + self.service = work.component(usage="pack") def test_scan_pack(self): pack_name = self.packA.name - params = { - 'pack_name': pack_name, - } - response = self.service.dispatch("scan_pack", params=params) - move_id = response['move_id'] - params ={ - 'move_id': move_id, - 'location_name': response['location_dest_name'], - } - location_dest_id = self.env['stock.location'].search([ - ('name', '=', params['location_name']) - ]).id - new_loc_quant = self.env['stock.quant'].search([ - ('product_id', '=', self.productA.id), - ('location_id', '=', location_dest_id) - ]) + params = {"pack_name": pack_name} + response = self.service.dispatch("scan", params=params) + move_id = response["move_id"] + params = {"move_id": move_id, "location_name": response["location_dest_name"]} + location_dest_id = ( + self.env["stock.location"] + .search([("name", "=", params["location_name"])]) + .id + ) + new_loc_quant = self.env["stock.quant"].search( + [ + ("product_id", "=", self.productA.id), + ("location_id", "=", location_dest_id), + ] + ) self.assertFalse(new_loc_quant) response = self.service.dispatch("validate", params=params) - new_loc_quant = self.env['stock.quant'].search([ - ('product_id', '=', self.productA.id), - ('location_id', '=', location_dest_id) - ]) - move = self.env['stock.move'].browse(move_id) - self.assertEquals(move.state, 'done') + new_loc_quant = self.env["stock.quant"].search( + [ + ("product_id", "=", self.productA.id), + ("location_id", "=", location_dest_id), + ] + ) + move = self.env["stock.move"].browse(move_id) + self.assertEquals(move.state, "done") self.assertEquals(self.quantA.quantity, 0) self.assertEquals(new_loc_quant.quantity, move.product_uom_qty) From 0b7669569af1b4f658c9f1cc0b8218b93bf88c19 Mon Sep 17 00:00:00 2001 From: Guewen Baconnier Date: Tue, 21 Jan 2020 17:20:33 +0100 Subject: [PATCH 034/986] Apply pre-commit run -a --- shopfloor/__manifest__.py | 11 +++-------- shopfloor/controllers/main.py | 2 +- shopfloor/models/res_users.py | 4 +--- shopfloor/models/shopfloor_device.py | 12 ++++++++---- shopfloor/models/shopfloor_menu.py | 10 ++++------ shopfloor/models/shopfloor_operation_group.py | 4 +--- shopfloor/models/shopfloor_process.py | 2 +- shopfloor/models/stock_picking_type.py | 4 ++-- shopfloor/tests/common.py | 9 ++++----- 9 files changed, 25 insertions(+), 33 deletions(-) diff --git a/shopfloor/__manifest__.py b/shopfloor/__manifest__.py index f94a70f42e9..b4043333aa6 100644 --- a/shopfloor/__manifest__.py +++ b/shopfloor/__manifest__.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # © 2020 Camptocamp, Akretion, BCIM # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). @@ -10,13 +9,9 @@ "category": "Inventory", "website": "https://odoo-community.org", "author": "Akretion, BCIM, Camptocamp, Odoo Community Association (OCA)", - "licence": "AGPL-3", + "license": "AGPL-3", "application": True, - "depends": [ - "stock", - "base_rest", - "auth_api_key", - ], + "depends": ["stock", "base_rest", "auth_api_key"], "data": [ "security/ir.model.access.csv", "views/shopfloor_operation_group.xml", @@ -32,5 +27,5 @@ "demo/shopfloor_menu_demo.xml", "demo/shopfloor_operation_group_demo.xml", "demo/shopfloor_device_demo.xml", - ] + ], } diff --git a/shopfloor/controllers/main.py b/shopfloor/controllers/main.py index 4df8196e6ea..34be9aae064 100644 --- a/shopfloor/controllers/main.py +++ b/shopfloor/controllers/main.py @@ -17,7 +17,7 @@ def _get_component_context(self): res = super(ShopfloorController, self)._get_component_context() headers = request.httprequest.environ for k, v in headers.items(): - if k.startswith('HTTP_SERVICE_CTX_'): + if k.startswith("HTTP_SERVICE_CTX_"): key_name = k[17:].lower() res[key_name] = v return res diff --git a/shopfloor/models/res_users.py b/shopfloor/models/res_users.py index e86bb27d2cc..6838b582ee9 100644 --- a/shopfloor/models/res_users.py +++ b/shopfloor/models/res_users.py @@ -6,7 +6,5 @@ class ResUsers(models.Model): # in practice, it's a one2one shopfloor_device_ids = fields.One2many( - comodel_name="shopfloor.device", - inverse_name="user_id", - readonly=True, + comodel_name="shopfloor.device", inverse_name="user_id", readonly=True ) diff --git a/shopfloor/models/shopfloor_device.py b/shopfloor/models/shopfloor_device.py index f84c78322e3..25acbe2dc6c 100644 --- a/shopfloor/models/shopfloor_device.py +++ b/shopfloor/models/shopfloor_device.py @@ -22,18 +22,22 @@ class ShopfloorDevice(models.Model): copy=False, help="Optional user using the device. The device will" "use this configuration when the users logs in the client " - "application." + "application.", ) shopfloor_current_process = fields.Char(readonly=True) shopfloor_last_call = fields.Char(readonly=True) - shopfloor_picking_id = fields.Many2one('stock.picking', readonly=True) + shopfloor_picking_id = fields.Many2one("stock.picking", readonly=True) _sql_constraints = [ - ('user_id_uniq', 'unique(user_id)', 'A user can be assigned to only one device.'), + ( + "user_id_uniq", + "unique(user_id)", + "A user can be assigned to only one device.", + ) ] @api.model def _default_warehouse_id(self): - wh = self.env['stock.warehouse'].search([]) + wh = self.env["stock.warehouse"].search([]) if len(wh) == 1: return wh diff --git a/shopfloor/models/shopfloor_menu.py b/shopfloor/models/shopfloor_menu.py index 48f836bd371..f95d37c382a 100644 --- a/shopfloor/models/shopfloor_menu.py +++ b/shopfloor/models/shopfloor_menu.py @@ -2,15 +2,13 @@ class ShopfloorMenu(models.Model): - _name = 'shopfloor.menu' + _name = "shopfloor.menu" _description = "Menu displayed in the scanner application" - _order = 'sequence' + _order = "sequence" name = fields.Char(translate=True) sequence = fields.Integer() operation_group_ids = fields.Many2many( - 'shopfloor.operation.group', - string="Groups", - help="visible for these groups", + "shopfloor.operation.group", string="Groups", help="visible for these groups" ) - process_id = fields.Many2one('shopfloor.process', name="Process") + process_id = fields.Many2one("shopfloor.process", name="Process") diff --git a/shopfloor/models/shopfloor_operation_group.py b/shopfloor/models/shopfloor_operation_group.py index 1d78aeeb832..0dc47fe272a 100644 --- a/shopfloor/models/shopfloor_operation_group.py +++ b/shopfloor/models/shopfloor_operation_group.py @@ -8,7 +8,5 @@ class ShopfloorOperationGroup(models.Model): name = fields.Char(required=True) user_ids = fields.Many2many("res.users", string="Members") menu_ids = fields.Many2many( - 'shopfloor.menu', - string="Menus", - help="Can see these menus", + "shopfloor.menu", string="Menus", help="Can see these menus" ) diff --git a/shopfloor/models/shopfloor_process.py b/shopfloor/models/shopfloor_process.py index accbc02155b..b1daa88f4df 100644 --- a/shopfloor/models/shopfloor_process.py +++ b/shopfloor/models/shopfloor_process.py @@ -7,5 +7,5 @@ class ShopfloorProcess(models.Model): name = fields.Char(required=True) picking_type_ids = fields.One2many( - 'stock.picking.type', 'process_id', string="Operation types" + "stock.picking.type", "process_id", string="Operation types" ) diff --git a/shopfloor/models/stock_picking_type.py b/shopfloor/models/stock_picking_type.py index 206b68c7fe4..808bf505526 100644 --- a/shopfloor/models/stock_picking_type.py +++ b/shopfloor/models/stock_picking_type.py @@ -2,6 +2,6 @@ class StockPickingType(models.Model): - _inherit = 'stock.picking.type' + _inherit = "stock.picking.type" - process_id = fields.Many2one('shopfloor.process', string="Process") + process_id = fields.Many2one("shopfloor.process", string="Process") diff --git a/shopfloor/tests/common.py b/shopfloor/tests/common.py index f85f3c3f947..5ae4ec521d0 100644 --- a/shopfloor/tests/common.py +++ b/shopfloor/tests/common.py @@ -1,17 +1,16 @@ from contextlib import contextmanager + +from odoo.tests.common import SavepointCase + from odoo.addons.base_rest.controllers.main import _PseudoCollection from odoo.addons.component.core import WorkContext -from odoo.tests.common import SavepointCase class CommonCase(SavepointCase): - @contextmanager def work_on_services(self, **params): params = params or {} collection = _PseudoCollection("shopfloor.service", self.env) yield WorkContext( - model_name="rest.service.registration", - collection=collection, - **params + model_name="rest.service.registration", collection=collection, **params ) From c17a936f65e6a5b8bb68f4009186915d52900528 Mon Sep 17 00:00:00 2001 From: Guewen Baconnier Date: Wed, 22 Jan 2020 12:54:45 +0100 Subject: [PATCH 035/986] Use expression.AND for combining domains --- shopfloor/services/device.py | 20 ++++++++++++++------ shopfloor/services/menu.py | 18 +++++++++++++----- 2 files changed, 27 insertions(+), 11 deletions(-) diff --git a/shopfloor/services/device.py b/shopfloor/services/device.py index b2609f4245f..f3605592bc5 100644 --- a/shopfloor/services/device.py +++ b/shopfloor/services/device.py @@ -1,4 +1,5 @@ from odoo import fields +from odoo.osv import expression from odoo.addons.base_rest.components.service import to_int from odoo.addons.component.core import Component @@ -20,15 +21,22 @@ def search(self, name_fragment=None): def _get_base_search_domain(self): # shopfloor_device_ids is a one2one + base_domain = super()._get_base_search_domain() user = self.env.user assigned_device = fields.first(user.shopfloor_device_ids) if assigned_device: - return [("id", "=", assigned_device.id)] - return [ - "|", - ("operation_group_ids", "=", False), - ("operation_group_ids.user_ids", "=", user.id), - ] + return expression.AND([base_domain, [("id", "=", assigned_device.id)]]) + + return expression.AND( + [ + base_domain, + [ + "|", + ("operation_group_ids", "=", False), + ("operation_group_ids.user_ids", "=", user.id), + ], + ] + ) def _validator_search(self): return { diff --git a/shopfloor/services/menu.py b/shopfloor/services/menu.py index 3fdf5182a09..8f8b8631402 100644 --- a/shopfloor/services/menu.py +++ b/shopfloor/services/menu.py @@ -1,3 +1,5 @@ +from odoo.osv import expression + from odoo.addons.base_rest.components.service import to_int from odoo.addons.component.core import Component @@ -9,12 +11,18 @@ class ShopfloorMenu(Component): _expose_model = "shopfloor.menu" def _get_base_search_domain(self): + base_domain = super()._get_base_search_domain() user = self.env.user - return [ - "|", - ("operation_group_ids", "=", False), - ("operation_group_ids.user_ids", "=", user.id), - ] + return expression.AND( + [ + base_domain, + [ + "|", + ("operation_group_ids", "=", False), + ("operation_group_ids.user_ids", "=", user.id), + ], + ] + ) def search(self, name_fragment=None): """List available menu entries for current user""" From a60a7fa0f7cc9fdb65148a0049266cccf9525dd3 Mon Sep 17 00:00:00 2001 From: Guewen Baconnier Date: Wed, 22 Jan 2020 12:55:13 +0100 Subject: [PATCH 036/986] Add listing of locations --- shopfloor/services/__init__.py | 1 + shopfloor/services/location.py | 70 ++++++++++++++++++++++++++++++++++ 2 files changed, 71 insertions(+) create mode 100644 shopfloor/services/location.py diff --git a/shopfloor/services/__init__.py b/shopfloor/services/__init__.py index 1fa6f94a433..7a379146daa 100644 --- a/shopfloor/services/__init__.py +++ b/shopfloor/services/__init__.py @@ -2,3 +2,4 @@ from . import device from . import menu from . import pack +from . import location diff --git a/shopfloor/services/location.py b/shopfloor/services/location.py new file mode 100644 index 00000000000..2ed25b05a1b --- /dev/null +++ b/shopfloor/services/location.py @@ -0,0 +1,70 @@ +from odoo.osv import expression + +from odoo.addons.base_rest.components.service import to_int +from odoo.addons.component.core import Component + + +class ShopfloorLocation(Component): + _inherit = "base.shopfloor.service" + _name = "shopfloor.location" + _usage = "location" + _expose_model = "stock.location" + + def search(self, name_fragment=None): + """List available devices for current user""" + domain = self._get_base_search_domain() + if name_fragment: + domain = expression.AND( + [ + domain, + [ + "|", + ("name", "ilike", name_fragment), + ("barcode", "ilike", name_fragment), + ], + ] + ) + records = self.env[self._expose_model].search(domain) + return {"size": len(records), "data": self._to_json(records)} + + def _get_base_search_domain(self): + # TODO add filter on warehouse of the device + return super()._get_base_search_domain() + + def _validator_search(self): + return { + "name_fragment": {"type": "string", "nullable": True, "required": False} + } + + def _validator_return_search(self): + return { + "size": {"coerce": to_int, "required": True, "type": "integer"}, + "data": { + "type": "list", + "schema": { + "type": "dict", + "schema": { + "id": {"coerce": to_int, "required": True, "type": "integer"}, + "name": {"type": "string", "nullable": False, "required": True}, + "complete_name": { + "type": "string", + "nullable": False, + "required": True, + }, + "barcode": { + "type": "string", + "nullable": False, + "required": False, + }, + }, + }, + }, + } + + def _convert_one_record(self, record): + return { + "id": record.id, + "name": record.name, + "complete_name": record.complete_name, + "barcode": record.barcode or "", + } From 01623d59efd14079c723a2800a9cc08cfafe08f0 Mon Sep 17 00:00:00 2001 From: Guewen Baconnier Date: Wed, 22 Jan 2020 13:20:09 +0100 Subject: [PATCH 037/986] Rename 'device' to 'profile' We don't want to force having one device record per hardware device, since adding a new scanner would require to create a new device on Odoo. Instead, we prefer to have profiles, which has to be selected at loading of the client application. The profile will hold the configuration for the interactions (warehouse, but maybe later printer, ...). The allowed profiles can be restricted to groups. A user can be forced to use a profile. --- shopfloor/__manifest__.py | 4 +-- ...ce_demo.xml => shopfloor_profile_demo.xml} | 6 ++-- shopfloor/models/__init__.py | 2 +- shopfloor/models/res_users.py | 4 +-- ...opfloor_device.py => shopfloor_profile.py} | 20 +++++------- shopfloor/security/ir.model.access.csv | 4 +-- shopfloor/services/__init__.py | 2 +- shopfloor/services/location.py | 4 +-- shopfloor/services/{device.py => profile.py} | 31 +++++++++++++------ shopfloor/views/menus.xml | 2 +- ..._views.xml => shopfloor_profile_views.xml} | 29 +++++++---------- 11 files changed, 56 insertions(+), 52 deletions(-) rename shopfloor/demo/{shopfloor_device_demo.xml => shopfloor_profile_demo.xml} (69%) rename shopfloor/models/{shopfloor_device.py => shopfloor_profile.py} (56%) rename shopfloor/services/{device.py => profile.py} (75%) rename shopfloor/views/{shopfloor_device_views.xml => shopfloor_profile_views.xml} (59%) diff --git a/shopfloor/__manifest__.py b/shopfloor/__manifest__.py index b4043333aa6..bfd999f5f00 100644 --- a/shopfloor/__manifest__.py +++ b/shopfloor/__manifest__.py @@ -18,7 +18,7 @@ "views/shopfloor_menu.xml", "views/shopfloor_process.xml", "views/stock_picking_type.xml", - "views/shopfloor_device_views.xml", + "views/shopfloor_profile_views.xml", "views/menus.xml", ], "demo": [ @@ -26,6 +26,6 @@ "demo/shopfloor_process_demo.xml", "demo/shopfloor_menu_demo.xml", "demo/shopfloor_operation_group_demo.xml", - "demo/shopfloor_device_demo.xml", + "demo/shopfloor_profile_demo.xml", ], } diff --git a/shopfloor/demo/shopfloor_device_demo.xml b/shopfloor/demo/shopfloor_profile_demo.xml similarity index 69% rename from shopfloor/demo/shopfloor_device_demo.xml rename to shopfloor/demo/shopfloor_profile_demo.xml index bd9ed3f2507..918ad669e2b 100644 --- a/shopfloor/demo/shopfloor_device_demo.xml +++ b/shopfloor/demo/shopfloor_profile_demo.xml @@ -1,18 +1,18 @@ - + Highbay Truck 1 - + Highbay Truck 2 - + Shelf 1 diff --git a/shopfloor/models/__init__.py b/shopfloor/models/__init__.py index 68782904869..1aa3c9fbc8a 100644 --- a/shopfloor/models/__init__.py +++ b/shopfloor/models/__init__.py @@ -2,5 +2,5 @@ from . import shopfloor_operation_group from . import shopfloor_process from . import stock_picking_type -from . import shopfloor_device +from . import shopfloor_profile from . import res_users diff --git a/shopfloor/models/res_users.py b/shopfloor/models/res_users.py index 6838b582ee9..8c136f725e7 100644 --- a/shopfloor/models/res_users.py +++ b/shopfloor/models/res_users.py @@ -5,6 +5,6 @@ class ResUsers(models.Model): _inherit = "res.users" # in practice, it's a one2one - shopfloor_device_ids = fields.One2many( - comodel_name="shopfloor.device", inverse_name="user_id", readonly=True + shopfloor_profile_ids = fields.One2many( + comodel_name="shopfloor.profile", inverse_name="user_id", readonly=True ) diff --git a/shopfloor/models/shopfloor_device.py b/shopfloor/models/shopfloor_profile.py similarity index 56% rename from shopfloor/models/shopfloor_device.py rename to shopfloor/models/shopfloor_profile.py index 25acbe2dc6c..f18d813b31b 100644 --- a/shopfloor/models/shopfloor_device.py +++ b/shopfloor/models/shopfloor_profile.py @@ -1,9 +1,9 @@ from odoo import api, fields, models -class ShopfloorDevice(models.Model): - _name = "shopfloor.device" - _description = "Shopfloor device settings" +class ShopfloorProfile(models.Model): + _name = "shopfloor.profile" + _description = "Shopfloor profile settings" name = fields.Char(required=True) warehouse_id = fields.Many2one( @@ -14,25 +14,21 @@ class ShopfloorDevice(models.Model): operation_group_ids = fields.Many2many( "shopfloor.operation.group", string="Shopfloor Operation Groups", - help="When unset, all users can use the device. When set," - "only users belonging to at least one group can use the device.", + help="When unset, all users can use the profile. When set," + "only users belonging to at least one group can use the profile.", ) user_id = fields.Many2one( "res.users", copy=False, - help="Optional user using the device. The device will" - "use this configuration when the users logs in the client " - "application.", + help="Optional user using the profile. When a profile has a" + "user assigned to it, the user is not allowed to use another profile.", ) - shopfloor_current_process = fields.Char(readonly=True) - shopfloor_last_call = fields.Char(readonly=True) - shopfloor_picking_id = fields.Many2one("stock.picking", readonly=True) _sql_constraints = [ ( "user_id_uniq", "unique(user_id)", - "A user can be assigned to only one device.", + "A user can be assigned to only one profile.", ) ] diff --git a/shopfloor/security/ir.model.access.csv b/shopfloor/security/ir.model.access.csv index 6db6c867996..a616c579eec 100644 --- a/shopfloor/security/ir.model.access.csv +++ b/shopfloor/security/ir.model.access.csv @@ -3,6 +3,6 @@ "access_shopfloor_menu_stock_manager","shopfloor menu inventory manager","model_shopfloor_menu","stock.group_stock_manager",1,1,1,1 "access_shopfloor_operation_group_users","shopfloor operation group","model_shopfloor_operation_group",,1,0,0,0 "access_shopfloor_operation_group_stock_manager","shopfloor operation group inventory manager","model_shopfloor_operation_group","stock.group_stock_manager",1,1,1,1 -"access_shopfloor_device_users","shopfloor device","model_shopfloor_device",,1,0,0,0 -"access_shopfloor_device_stock_manager","shopfloor device inventory manager","model_shopfloor_device","stock.group_stock_manager",1,1,1,1 +"access_shopfloor_profile_users","shopfloor profile","model_shopfloor_profile",,1,0,0,0 +"access_shopfloor_profile_stock_manager","shopfloor profile inventory manager","model_shopfloor_profile","stock.group_stock_manager",1,1,1,1 "access_shopfloor_process_users","shopfloor process","model_shopfloor_process",,1,0,0,0 diff --git a/shopfloor/services/__init__.py b/shopfloor/services/__init__.py index 7a379146daa..f72a9992432 100644 --- a/shopfloor/services/__init__.py +++ b/shopfloor/services/__init__.py @@ -1,5 +1,5 @@ from . import service -from . import device +from . import profile from . import menu from . import pack from . import location diff --git a/shopfloor/services/location.py b/shopfloor/services/location.py index 2ed25b05a1b..8f65a1febb4 100644 --- a/shopfloor/services/location.py +++ b/shopfloor/services/location.py @@ -11,7 +11,7 @@ class ShopfloorLocation(Component): _expose_model = "stock.location" def search(self, name_fragment=None): - """List available devices for current user""" + """List available locations for current user""" domain = self._get_base_search_domain() if name_fragment: domain = expression.AND( @@ -28,7 +28,7 @@ def search(self, name_fragment=None): return {"size": len(records), "data": self._to_json(records)} def _get_base_search_domain(self): - # TODO add filter on warehouse of the device + # TODO add filter on warehouse of the current profile return super()._get_base_search_domain() def _validator_search(self): diff --git a/shopfloor/services/device.py b/shopfloor/services/profile.py similarity index 75% rename from shopfloor/services/device.py rename to shopfloor/services/profile.py index f3605592bc5..001a31a52e8 100644 --- a/shopfloor/services/device.py +++ b/shopfloor/services/profile.py @@ -5,14 +5,27 @@ from odoo.addons.component.core import Component -class ShopfloorDevice(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 (TODO put the name of the header). + + The list of profiles available for a user is restricted by 2 things: + + * If the profile has operation groups, the profile can be used only + if the user is at least in one of these groups. + * If the user has an assigned profile, the user can use only this profile. + + """ + _inherit = "base.shopfloor.service" - _name = "shopfloor.device" - _usage = "device" - _expose_model = "shopfloor.device" + _name = "shopfloor.profile" + _usage = "profile" + _expose_model = "shopfloor.profile" def search(self, name_fragment=None): - """List available devices for current user""" + """List available profiles for current user""" domain = self._get_base_search_domain() if name_fragment: domain.append(("name", "ilike", name_fragment)) @@ -20,12 +33,12 @@ def search(self, name_fragment=None): return {"size": len(records), "data": self._to_json(records)} def _get_base_search_domain(self): - # shopfloor_device_ids is a one2one + # shopfloor_profile_ids is a one2one in practice. base_domain = super()._get_base_search_domain() user = self.env.user - assigned_device = fields.first(user.shopfloor_device_ids) - if assigned_device: - return expression.AND([base_domain, [("id", "=", assigned_device.id)]]) + assigned_profile = fields.first(user.shopfloor_profile_ids) + if assigned_profile: + return expression.AND([base_domain, [("id", "=", assigned_profile.id)]]) return expression.AND( [ diff --git a/shopfloor/views/menus.xml b/shopfloor/views/menus.xml index f6bc4f105da..71f19f9f78f 100644 --- a/shopfloor/views/menus.xml +++ b/shopfloor/views/menus.xml @@ -4,5 +4,5 @@ - + diff --git a/shopfloor/views/shopfloor_device_views.xml b/shopfloor/views/shopfloor_profile_views.xml similarity index 59% rename from shopfloor/views/shopfloor_device_views.xml rename to shopfloor/views/shopfloor_profile_views.xml index 1f6dc4cc251..26720fae2bb 100644 --- a/shopfloor/views/shopfloor_device_views.xml +++ b/shopfloor/views/shopfloor_profile_views.xml @@ -1,8 +1,8 @@ - - shopfloor.device tree - shopfloor.device + + shopfloor.profile tree + shopfloor.profile @@ -13,9 +13,9 @@ - - shopfloor.device form - shopfloor.device + + shopfloor.profile form + shopfloor.profile
@@ -28,20 +28,15 @@ - - - - -
- - shopfloor.device search - shopfloor.device + + shopfloor.profile search + shopfloor.profile @@ -52,9 +47,9 @@ - - Devices - shopfloor.device + + Profiles + shopfloor.profile ir.actions.act_window tree,form From 09c9de59ccb93300f8f4d4fd2828e7ef6680f697 Mon Sep 17 00:00:00 2001 From: Guewen Baconnier Date: Wed, 22 Jan 2020 13:24:02 +0100 Subject: [PATCH 038/986] Fix travis, server_environment requires a running_env --- .travis.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.travis.yml b/.travis.yml index 7048b77b57e..761b593248a 100644 --- a/.travis.yml +++ b/.travis.yml @@ -40,6 +40,8 @@ install: - git clone --depth=1 https://github.com/OCA/maintainer-quality-tools.git ${HOME}/maintainer-quality-tools - export PATH=${HOME}/maintainer-quality-tools/travis:${PATH} - travis_install_nightly + # Requirements to test server_environment modules + - printf '[options]\n\nrunning_env = dev\n' > ${HOME}/.openerp_serverrc script: - travis_run_tests From a0e8cae73d3dd25444ac13fbe9fe42184dd8defd Mon Sep 17 00:00:00 2001 From: Guewen Baconnier Date: Wed, 22 Jan 2020 13:41:36 +0100 Subject: [PATCH 039/986] Fix initialization of tests Components have to be initalized (when running pytest-odoo, we don't need it, but to run odoo tests, we have to use the ComponentMixin to initialize the system). --- shopfloor/tests/common.py | 24 +++++++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/shopfloor/tests/common.py b/shopfloor/tests/common.py index 5ae4ec521d0..1f14f5ac93e 100644 --- a/shopfloor/tests/common.py +++ b/shopfloor/tests/common.py @@ -4,9 +4,14 @@ from odoo.addons.base_rest.controllers.main import _PseudoCollection from odoo.addons.component.core import WorkContext +from odoo.addons.component.tests.common import ComponentMixin -class CommonCase(SavepointCase): +class CommonCase(SavepointCase, ComponentMixin): + + # by default disable tracking suite-wise, it's a time saver :) + tracking_disable = True + @contextmanager def work_on_services(self, **params): params = params or {} @@ -14,3 +19,20 @@ def work_on_services(self, **params): 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) + ) + cls.setUpComponent() From 901975383adacd2f961ded7ae250ac3a5ce720dc Mon Sep 17 00:00:00 2001 From: Guewen Baconnier Date: Wed, 22 Jan 2020 15:48:07 +0100 Subject: [PATCH 040/986] Add description on services (displayed on swagger) --- shopfloor/services/location.py | 3 +++ shopfloor/services/menu.py | 9 +++++++++ shopfloor/services/pack.py | 3 +++ shopfloor/services/profile.py | 5 +++-- shopfloor/services/service.py | 2 ++ 5 files changed, 20 insertions(+), 2 deletions(-) diff --git a/shopfloor/services/location.py b/shopfloor/services/location.py index 8f65a1febb4..17aac7be05c 100644 --- a/shopfloor/services/location.py +++ b/shopfloor/services/location.py @@ -5,10 +5,13 @@ class ShopfloorLocation(Component): + """Expose Stock Locations data for the current warehouse.""" + _inherit = "base.shopfloor.service" _name = "shopfloor.location" _usage = "location" _expose_model = "stock.location" + _description = __doc__ def search(self, name_fragment=None): """List available locations for current user""" diff --git a/shopfloor/services/menu.py b/shopfloor/services/menu.py index 8f8b8631402..e9d7fbfc50d 100644 --- a/shopfloor/services/menu.py +++ b/shopfloor/services/menu.py @@ -5,10 +5,19 @@ class ShopfloorMenu(Component): + """ + Menu Structure for the client application. + + The list of menus is restricted by the operation groups. A menu without + groups is visible for all users, a menu with group(s) is visible if the + user is in at least one of the groups. + """ + _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() diff --git a/shopfloor/services/pack.py b/shopfloor/services/pack.py index bf098599a30..32f7a1b20fa 100644 --- a/shopfloor/services/pack.py +++ b/shopfloor/services/pack.py @@ -3,9 +3,12 @@ class ShopfloorPack(Component): + """Expose data about Stock Quant Packages""" + _inherit = "base.shopfloor.service" _name = "shopfloor.pack" _usage = "pack" + _description = __doc__ # TODO define the return schema and add the validator method @skip_secure_response diff --git a/shopfloor/services/profile.py b/shopfloor/services/profile.py index 001a31a52e8..92f9d3eac32 100644 --- a/shopfloor/services/profile.py +++ b/shopfloor/services/profile.py @@ -6,7 +6,8 @@ class ShopfloorProfile(Component): - """Profile storing the configuration for the interaction from the client. + """ + 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 (TODO put the name of the header). @@ -16,13 +17,13 @@ class ShopfloorProfile(Component): * If the profile has operation groups, the profile can be used only if the user is at least in one of these groups. * If the user has an assigned profile, the user can use only this profile. - """ _inherit = "base.shopfloor.service" _name = "shopfloor.profile" _usage = "profile" _expose_model = "shopfloor.profile" + _description = __doc__ def search(self, name_fragment=None): """List available profiles for current user""" diff --git a/shopfloor/services/service.py b/shopfloor/services/service.py index d19ba3db273..d48cfe3f781 100644 --- a/shopfloor/services/service.py +++ b/shopfloor/services/service.py @@ -6,6 +6,8 @@ class BaseShopfloorService(AbstractComponent): + """Base class for REST services""" + _inherit = "base.rest.service" _name = "base.shopfloor.service" _collection = "shopfloor.service" From abd1bddb36d5684944e521b9b2a188b3b9615d68 Mon Sep 17 00:00:00 2001 From: Guewen Baconnier Date: Wed, 22 Jan 2020 16:50:27 +0100 Subject: [PATCH 041/986] Add a code for the process The code will be used for: * Customize the view depending of the code (for instance, we could have a field "allow to replace a lot" on a "Single Pack Transfer" process, and a second "Single Pack Transfer" without allowing this) * the code is the identifier of the method in the REST API (/shopfloor//) --- shopfloor/demo/shopfloor_menu_demo.xml | 6 ++++++ shopfloor/demo/shopfloor_process_demo.xml | 6 ++++++ shopfloor/demo/shopfloor_profile_demo.xml | 10 ++-------- shopfloor/models/shopfloor_menu.py | 3 ++- shopfloor/models/shopfloor_process.py | 8 ++++++++ shopfloor/services/__init__.py | 2 ++ shopfloor/services/menu.py | 7 ++++++- shopfloor/services/single_pack_putaway.py | 10 ++++++++++ shopfloor/services/single_pack_transfer.py | 10 ++++++++++ shopfloor/views/shopfloor_process.xml | 3 +++ 10 files changed, 55 insertions(+), 10 deletions(-) create mode 100644 shopfloor/services/single_pack_putaway.py create mode 100644 shopfloor/services/single_pack_transfer.py diff --git a/shopfloor/demo/shopfloor_menu_demo.xml b/shopfloor/demo/shopfloor_menu_demo.xml index 7d643a0e1f9..6d0d511f791 100644 --- a/shopfloor/demo/shopfloor_menu_demo.xml +++ b/shopfloor/demo/shopfloor_menu_demo.xml @@ -6,4 +6,10 @@ + + Single Pallet Transfer + 20 + + +
diff --git a/shopfloor/demo/shopfloor_process_demo.xml b/shopfloor/demo/shopfloor_process_demo.xml index 30964d7b23c..3f869cb4579 100644 --- a/shopfloor/demo/shopfloor_process_demo.xml +++ b/shopfloor/demo/shopfloor_process_demo.xml @@ -2,6 +2,12 @@ Put-Away Reach Truck + single_pack_putaway + + + + Single Pallet Transfer + single_pack_transfer diff --git a/shopfloor/demo/shopfloor_profile_demo.xml b/shopfloor/demo/shopfloor_profile_demo.xml index 918ad669e2b..5ef9df4179d 100644 --- a/shopfloor/demo/shopfloor_profile_demo.xml +++ b/shopfloor/demo/shopfloor_profile_demo.xml @@ -1,13 +1,7 @@ - - Highbay Truck 1 - - - - - - Highbay Truck 2 + + Highbay Truck diff --git a/shopfloor/models/shopfloor_menu.py b/shopfloor/models/shopfloor_menu.py index f95d37c382a..0f0a56e2bbd 100644 --- a/shopfloor/models/shopfloor_menu.py +++ b/shopfloor/models/shopfloor_menu.py @@ -11,4 +11,5 @@ class ShopfloorMenu(models.Model): operation_group_ids = fields.Many2many( "shopfloor.operation.group", string="Groups", help="visible for these groups" ) - process_id = fields.Many2one("shopfloor.process", name="Process") + process_id = fields.Many2one("shopfloor.process", name="Process", required=True) + process_code = fields.Selection(related="process_id.code", readonly=True) diff --git a/shopfloor/models/shopfloor_process.py b/shopfloor/models/shopfloor_process.py index b1daa88f4df..db4759c2d01 100644 --- a/shopfloor/models/shopfloor_process.py +++ b/shopfloor/models/shopfloor_process.py @@ -6,6 +6,14 @@ class ShopfloorProcess(models.Model): _description = "a process to be run from the scanners" name = fields.Char(required=True) + code = fields.Selection(selection="_selection_code", required=True) picking_type_ids = fields.One2many( "stock.picking.type", "process_id", string="Operation types" ) + + def _selection_code(self): + return [ + # these must match a REST service + ("single_pack_putaway", "Single Pack Put-away"), + ("single_pack_transfer", "Single Pack Transfer"), + ] diff --git a/shopfloor/services/__init__.py b/shopfloor/services/__init__.py index f72a9992432..bb82a1df2e5 100644 --- a/shopfloor/services/__init__.py +++ b/shopfloor/services/__init__.py @@ -3,3 +3,5 @@ from . import menu from . import pack from . import location +from . import single_pack_putaway +from . import single_pack_transfer diff --git a/shopfloor/services/menu.py b/shopfloor/services/menu.py index e9d7fbfc50d..220003a5599 100644 --- a/shopfloor/services/menu.py +++ b/shopfloor/services/menu.py @@ -56,10 +56,15 @@ def _validator_return_search(self): "schema": { "id": {"coerce": to_int, "required": True, "type": "integer"}, "name": {"type": "string", "nullable": False, "required": True}, + "process": { + "type": "string", + "nullable": False, + "required": True, + }, }, }, }, } def _convert_one_record(self, record): - return {"id": record.id, "name": record.name} + return {"id": record.id, "name": record.name, "process": record.process_code} diff --git a/shopfloor/services/single_pack_putaway.py b/shopfloor/services/single_pack_putaway.py new file mode 100644 index 00000000000..f46024bd0dc --- /dev/null +++ b/shopfloor/services/single_pack_putaway.py @@ -0,0 +1,10 @@ +from odoo.addons.component.core import Component + + +class SinglePackPutaway(Component): + """Methods for the Single Pack Put-Away Process""" + + _inherit = "base.shopfloor.service" + _name = "shopfloor.single.pack.putaway" + _usage = "single_pack_putaway" + _description = __doc__ diff --git a/shopfloor/services/single_pack_transfer.py b/shopfloor/services/single_pack_transfer.py new file mode 100644 index 00000000000..a22659a1cb7 --- /dev/null +++ b/shopfloor/services/single_pack_transfer.py @@ -0,0 +1,10 @@ +from odoo.addons.component.core import Component + + +class SinglePackTransfer(Component): + """Methods for the Single Pack Transfer Process""" + + _inherit = "base.shopfloor.service" + _name = "shopfloor.single.pack.transfer" + _usage = "single_pack_transfer" + _description = __doc__ diff --git a/shopfloor/views/shopfloor_process.xml b/shopfloor/views/shopfloor_process.xml index 67afa35bc8f..db58ed0b343 100644 --- a/shopfloor/views/shopfloor_process.xml +++ b/shopfloor/views/shopfloor_process.xml @@ -6,6 +6,7 @@ + @@ -20,6 +21,7 @@ + @@ -36,6 +38,7 @@ + From 2b03ed5624dfe3b49aacb77034296425bf594b1f Mon Sep 17 00:00:00 2001 From: Guewen Baconnier Date: Wed, 22 Jan 2020 17:27:15 +0100 Subject: [PATCH 042/986] Rework HTTP Header parameters * We need the menu id and the profile id, from there, we can find the configuration for the processes * Get recordsets in the work context, so we can conveniently use them from service components (and no actual SELECT is issued until we use them) * Return 400 BadRequest if a parameter is missing, but do not check existence of the record to avoid queries before we actually need the records, considering we won't need them for every request (a method toinspect a pack for instance does not care about the menu or the profile) --- .isort.cfg | 2 +- shopfloor/controllers/main.py | 23 +++++++++++++++++------ shopfloor/services/service.py | 16 ++++++++-------- 3 files changed, 26 insertions(+), 15 deletions(-) diff --git a/.isort.cfg b/.isort.cfg index 98b216f744d..4d3705d124e 100644 --- a/.isort.cfg +++ b/.isort.cfg @@ -9,4 +9,4 @@ line_length=88 known_odoo=odoo known_odoo_addons=odoo.addons sections=FUTURE,STDLIB,THIRDPARTY,ODOO,ODOO_ADDONS,FIRSTPARTY,LOCALFOLDER -known_third_party=setuptools +known_third_party=setuptools,werkzeug diff --git a/shopfloor/controllers/main.py b/shopfloor/controllers/main.py index 34be9aae064..297b0e40332 100644 --- a/shopfloor/controllers/main.py +++ b/shopfloor/controllers/main.py @@ -1,3 +1,5 @@ +from werkzeug.exceptions import BadRequest + from odoo.http import request from odoo.addons.base_rest.controllers import main @@ -11,13 +13,22 @@ class ShopfloorController(main.RestController): def _get_component_context(self): """ This method adds the component context: - * the process name - * the process menu + * the shopfloor menu in ``self.work.menu`` from the service Components + * the shopfloor profile in ``self.work.profile`` from the service + Components """ res = super(ShopfloorController, self)._get_component_context() headers = request.httprequest.environ - for k, v in headers.items(): - if k.startswith("HTTP_SERVICE_CTX_"): - key_name = k[17:].lower() - res[key_name] = v + try: + menu_id = int(headers.get("HTTP_SERVICE_CTX_MENU_ID")) + except (TypeError, ValueError): + raise BadRequest("HTTP_SERVICE_CTX_MENU_ID must be set with an integer") + res["menu"] = request.env["shopfloor.menu"].browse(menu_id) + + try: + profile_id = int(headers.get("HTTP_SERVICE_CTX_PROFILE_ID")) + except (TypeError, ValueError): + raise BadRequest("HTTP_SERVICE_CTX_PROFILE_ID must be set with an integer") + res["profile"] = request.env["shopfloor.profile"].browse(profile_id) + return res diff --git a/shopfloor/services/service.py b/shopfloor/services/service.py index d48cfe3f781..2363b60af48 100644 --- a/shopfloor/services/service.py +++ b/shopfloor/services/service.py @@ -54,22 +54,22 @@ def _get_openapi_default_parameters(self): "value": demo_api_key, }, { - "name": "SERVICE_CTX_PROCESS_NAME", + "name": "SERVICE_CTX_MENU_ID", "in": "header", - "description": "Name of the current process", + "description": "ID of the current menu", "required": True, - "schema": {"type": "string"}, + "schema": {"type": "integer"}, "style": "simple", - "value": "Put-Away Reach Truck", + "value": "1", }, { - "name": "SERVICE_CTX_PROCESS_MENU", + "name": "SERVICE_CTX_PROFILE_ID", "in": "header", - "description": "Name of the current process menu", + "description": "ID of the current profile", "required": True, - "schema": {"type": "string"}, + "schema": {"type": "integer"}, "style": "simple", - "value": "Put-Away Reach Truck", + "value": "1", }, ] ) From ee57d9c9844c8a5e5c52ffbe714ae1287942324e Mon Sep 17 00:00:00 2001 From: Benoit Date: Thu, 23 Jan 2020 10:04:30 +0100 Subject: [PATCH 043/986] refactor of pack scan service need to be improved again --- shopfloor/services/pack.py | 257 ++++++++++++++++++++++++++++---- shopfloor/tests/test_putaway.py | 17 ++- 2 files changed, 242 insertions(+), 32 deletions(-) diff --git a/shopfloor/services/pack.py b/shopfloor/services/pack.py index 32f7a1b20fa..9cca11de74e 100644 --- a/shopfloor/services/pack.py +++ b/shopfloor/services/pack.py @@ -12,15 +12,88 @@ class ShopfloorPack(Component): # TODO define the return schema and add the validator method @skip_secure_response - def scan(self, pack_name): + def scan(self, barcode): """Scan a pack barcode""" - pack = self.env["stock.quant.package"].search([("name", "=", pack_name)]) + company = self.env.user.company_id # FIXME add logic to get proper company # FIXME add logic to get proper warehouse warehouse = self.env["stock.warehouse"].search([])[0] picking_type = ( warehouse.int_type_id ) # FIXME add logic to get picking type properly + + # TODO define on what we search (pack name, pack barcode ...) + pack = self.env["stock.quant.package"].search([("name", "=", barcode)]) + if not pack: + return { + "success": False, + "code": "not_found", + "message": { + "title": "Pack not found", + "body": "the pack %s doesn't exists" % barcode, + }, + } + allowed_locations = self.env["stock.location"].search( + [("id", "child_of", picking_type.default_location_src_id.id)] + ) + if pack.location_id not in allowed_locations: + return { + "success": False, + "code": "forbidden", + "message": { + "title": "do not process", + "body": "pack %s is not in %s location" + % (barcode, picking_type.default_location_src_id.name), + }, + } + quantity = pack.quant_ids[0].quantity + existing_operations = self.env["stock.move.line"].search( + [("qty_done", "=", quantity), ("package_id", "=", pack.id)] + ) + if ( + existing_operations + and existing_operations[0].picking_id.picking_type_id != picking_type + ): + return { + "success": False, + "code": "forbidden", + "message": { + "title": "do not process", + "body": "An operation exists in %s %s. " + "You cannot process it with this shopfloor process." + % ( + existing_operations[0].picking_id.picking_type_id.name, + existing_operations[0].picking_id.name, + ), + }, + } + elif existing_operations: + move = existing_operations.move_id + return { + "data": { + "id": move.move_line_ids[0].package_level_id.id, + "location_src": { + "id": pack.location_id.id, + "name": pack.location_id.name, + }, + "location_dst": { + "id": move.move_line_ids[0].location_dest_id.id, + "name": move.move_line_ids[0].location_dest_id.name, + }, + "product": { + "id": move.product_id.name, + "name": move.product_id.name, + }, + "picking": {"id": move.picking_id.id, "name": move.picking_id.name}, + }, + "success": False, + "code": "need_confirmation", + "message": { + "title": "Already started", + "body": "Operation already running. " + "Would you like to take it over ?", + }, + } product = pack.quant_ids[ 0 ].product_id # FIXME we consider only one product per pack @@ -45,45 +118,179 @@ def scan(self, pack_name): ) move.picking_id.action_assign() return_vals = { - "name": pack.name, - "location_name": pack.location_id.name, - "location_dest_name": move.move_line_ids[0].location_dest_id.name, - "product_name": move.name, - "picking_name": move.picking_id.name, - "location_id": pack.location_id.id, - "location_dest_id": move.move_line_ids[0].location_dest_id.id, - "move_id": move.id, - # 'allow_change_destination': True, #TODO + "success": True, + "data": { + "id": move.move_line_ids[0].package_level_id.id, + "location_src": { + "id": pack.location_id.id, + "name": pack.location_id.name, + }, + "location_dst": { + "id": move.move_line_ids[0].location_dest_id.id, + "name": move.move_line_ids[0].location_dest_id.name, + }, + "product": {"id": move.product_id.name, "name": move.product_id.name}, + "picking": {"id": move.picking_id.id, "name": move.picking_id.name}, + }, } return return_vals - def validate(self, move_id, location_name): - move = self.env["stock.move"].browse(move_id) + def validate(self, package_level_id, location_name, confirmation=False): + package = self.env["stock.package_level"].browse(package_level_id) + move = package.move_line_ids[0].move_id dest_location = self.env["stock.location"].search( [("name", "=", location_name)] ) - if move.move_line_ids[0].location_dest_id.id != dest_location.id: - move.move_line_ids[0].location_dest_id = dest_location.id - move.move_line_ids[0].qty_done = move.move_line_ids[0].product_uom_qty - move.picking_id.button_validate() - return True + move_dest_location = move.move_line_ids[0].location_dest_id + allowed_locations = self.env["stock.location"].search( + [ + ( + "id", + "child_of", + move.picking_id.picking_type_id.default_location_dest_id.id, + ) + ] + ) + zone_locations = self.env["stock.location"].search( + [("id", "child_of", move_dest_location.id)] + ) + if dest_location not in allowed_locations: + return { + "success": False, + "code": "forbidden", + "message": {"title": "Forbidden", "body": "You cannot place it here"}, + } + elif ( + dest_location in allowed_locations + and dest_location not in zone_locations + and confirmation + ): + return { + "success": False, + "code": "need_confirmation", + "message": {"title": "Confirm", "body": "Are you sure ?"}, + } + if move.state == "cancel": + return { + "success": False, + "code": "restart", + "message": { + "title": "Restart", + "body": "Restart the operation someone has canceld it.", + }, + } + move.move_line_ids[0].location_dest_id = dest_location.id + move._action_done() + return {"success": True} - def cancel(self, move_id): - move = self.env["stock.move"].browse(move_id) - move.picking_id.cancel() - return True + def cancel(self, package_level_id): + package = self.env["stock.package_level"].browse(package_level_id) + package.move_ids[0].cancel() + return {"success": True} def _validator_cancel(self): - return {"move_id": {"coerce": to_int, "required": True, "type": "integer"}} + return { + "package_level_id": {"coerce": to_int, "required": True, "type": "integer"} + } def _validator_validate(self): return { - "move_id": {"coerce": to_int, "required": True, "type": "integer"}, + "package_level_id": {"coerce": to_int, "required": True, "type": "integer"}, "location_name": {"type": "string", "nullable": False, "required": True}, } + def _validator_return_validate(self): + return { + "success": {"type": "boolean", "nullable": True, "required": True}, + "code": {"type": "string", "nullable": True, "required": False}, + "message": { + "type": "dict", + "schema": { + "body": {"type": "string", "nullable": False, "required": True} + }, + }, + } + def _validator_scan(self): - return {"pack_name": {"type": "string", "nullable": False, "required": True}} + return {"barcode": {"type": "string", "nullable": False, "required": True}} + + def _validator_return_scan(self): + return { + "success": {"type": "boolean", "nullable": True, "required": True}, + "code": {"type": "string", "nullable": True, "required": False}, + "message": { + "type": "dict", + "schema": { + "body": {"type": "string", "nullable": False, "required": True} + }, + }, + "data": { + "type": "dict", + "schema": { + "id": {"coerce": to_int, "required": True, "type": "integer"}, + "location_src": { + "type": "dict", + "schema": { + "id": { + "coerce": to_int, + "required": True, + "type": "integer", + }, + "name": { + "type": "string", + "nullable": False, + "required": True, + }, + }, + }, + "location_dst": { + "type": "dict", + "schema": { + "id": { + "coerce": to_int, + "required": True, + "type": "integer", + }, + "name": { + "type": "string", + "nullable": False, + "required": True, + }, + }, + }, + "product": { + "type": "dict", + "schema": { + "id": { + "coerce": to_int, + "required": True, + "type": "integer", + }, + "name": { + "type": "string", + "nullable": False, + "required": True, + }, + }, + }, + "picking": { + "type": "dict", + "schema": { + "id": { + "coerce": to_int, + "required": True, + "type": "integer", + }, + "name": { + "type": "string", + "nullable": False, + "required": True, + }, + }, + }, + }, + }, + } def get_by_name(self, pack_name): """ diff --git a/shopfloor/tests/test_putaway.py b/shopfloor/tests/test_putaway.py index 53c325cb0c4..5b09c332413 100644 --- a/shopfloor/tests/test_putaway.py +++ b/shopfloor/tests/test_putaway.py @@ -4,18 +4,17 @@ class PutawayCase(CommonCase): def setUp(self, *args, **kwargs): super(PutawayCase, self).setUp(*args, **kwargs) - in_location = self.env.ref("stock.stock_location_company").child_ids[0] stock_location = self.env.ref("stock.stock_location_stock") self.productA = self.env["product.product"].create( {"name": "Product A", "type": "product"} ) self.packA = self.env["stock.quant.package"].create( - {"location_id": in_location.id} + {"location_id": stock_location.id} ) self.quantA = self.env["stock.quant"].create( { "product_id": self.productA.id, - "location_id": in_location.id, + "location_id": stock_location.id, "quantity": 1, "package_id": self.packA.id, } @@ -32,11 +31,15 @@ def setUp(self, *args, **kwargs): self.service = work.component(usage="pack") def test_scan_pack(self): - pack_name = self.packA.name - params = {"pack_name": pack_name} + barcode = self.packA.name + params = {"barcode": barcode} response = self.service.dispatch("scan", params=params) - move_id = response["move_id"] - params = {"move_id": move_id, "location_name": response["location_dest_name"]} + package_level = self.env["stock.package_level"].browse(response["data"]["id"]) + move_id = package_level.move_line_ids[0].move_id.id + params = { + "package_level_id": package_level.id, + "location_name": response["data"]["location_dst"]["name"], + } location_dest_id = ( self.env["stock.location"] .search([("name", "=", params["location_name"])]) From ec18a254924714ec943ad3ad9b5e09b366403d59 Mon Sep 17 00:00:00 2001 From: Benoit Date: Thu, 23 Jan 2020 10:13:44 +0100 Subject: [PATCH 044/986] move single pack putaway service --- shopfloor/services/pack.py | 284 +----------------- shopfloor/services/single_pack_putaway.py | 281 +++++++++++++++++ ...putaway.py => test_single_pack_putaway.py} | 0 3 files changed, 282 insertions(+), 283 deletions(-) rename shopfloor/tests/{test_putaway.py => test_single_pack_putaway.py} (100%) diff --git a/shopfloor/services/pack.py b/shopfloor/services/pack.py index 9cca11de74e..46b70147646 100644 --- a/shopfloor/services/pack.py +++ b/shopfloor/services/pack.py @@ -1,4 +1,4 @@ -from odoo.addons.base_rest.components.service import skip_secure_response, to_int +from odoo.addons.base_rest.components.service import to_int from odoo.addons.component.core import Component @@ -10,288 +10,6 @@ class ShopfloorPack(Component): _usage = "pack" _description = __doc__ - # TODO define the return schema and add the validator method - @skip_secure_response - def scan(self, barcode): - """Scan a pack barcode""" - - company = self.env.user.company_id # FIXME add logic to get proper company - # FIXME add logic to get proper warehouse - warehouse = self.env["stock.warehouse"].search([])[0] - picking_type = ( - warehouse.int_type_id - ) # FIXME add logic to get picking type properly - - # TODO define on what we search (pack name, pack barcode ...) - pack = self.env["stock.quant.package"].search([("name", "=", barcode)]) - if not pack: - return { - "success": False, - "code": "not_found", - "message": { - "title": "Pack not found", - "body": "the pack %s doesn't exists" % barcode, - }, - } - allowed_locations = self.env["stock.location"].search( - [("id", "child_of", picking_type.default_location_src_id.id)] - ) - if pack.location_id not in allowed_locations: - return { - "success": False, - "code": "forbidden", - "message": { - "title": "do not process", - "body": "pack %s is not in %s location" - % (barcode, picking_type.default_location_src_id.name), - }, - } - quantity = pack.quant_ids[0].quantity - existing_operations = self.env["stock.move.line"].search( - [("qty_done", "=", quantity), ("package_id", "=", pack.id)] - ) - if ( - existing_operations - and existing_operations[0].picking_id.picking_type_id != picking_type - ): - return { - "success": False, - "code": "forbidden", - "message": { - "title": "do not process", - "body": "An operation exists in %s %s. " - "You cannot process it with this shopfloor process." - % ( - existing_operations[0].picking_id.picking_type_id.name, - existing_operations[0].picking_id.name, - ), - }, - } - elif existing_operations: - move = existing_operations.move_id - return { - "data": { - "id": move.move_line_ids[0].package_level_id.id, - "location_src": { - "id": pack.location_id.id, - "name": pack.location_id.name, - }, - "location_dst": { - "id": move.move_line_ids[0].location_dest_id.id, - "name": move.move_line_ids[0].location_dest_id.name, - }, - "product": { - "id": move.product_id.name, - "name": move.product_id.name, - }, - "picking": {"id": move.picking_id.id, "name": move.picking_id.name}, - }, - "success": False, - "code": "need_confirmation", - "message": { - "title": "Already started", - "body": "Operation already running. " - "Would you like to take it over ?", - }, - } - product = pack.quant_ids[ - 0 - ].product_id # FIXME we consider only one product per pack - move_vals = { - "picking_type_id": picking_type.id, - "product_id": product.id, - "location_id": pack.location_id.id, - "location_dest_id": picking_type.default_location_dest_id.id, - "name": product.name, - "product_uom": product.uom_id.id, - "product_uom_qty": pack.quant_ids[0].quantity, - "company_id": company.id, - } - move = self.env["stock.move"].create(move_vals) - move._action_confirm() - self.env["stock.package_level"].create( - { - "package_id": pack.id, - "move_ids": [(6, 0, [move.id])], - "company_id": company.id, - } - ) - move.picking_id.action_assign() - return_vals = { - "success": True, - "data": { - "id": move.move_line_ids[0].package_level_id.id, - "location_src": { - "id": pack.location_id.id, - "name": pack.location_id.name, - }, - "location_dst": { - "id": move.move_line_ids[0].location_dest_id.id, - "name": move.move_line_ids[0].location_dest_id.name, - }, - "product": {"id": move.product_id.name, "name": move.product_id.name}, - "picking": {"id": move.picking_id.id, "name": move.picking_id.name}, - }, - } - return return_vals - - def validate(self, package_level_id, location_name, confirmation=False): - package = self.env["stock.package_level"].browse(package_level_id) - move = package.move_line_ids[0].move_id - dest_location = self.env["stock.location"].search( - [("name", "=", location_name)] - ) - move_dest_location = move.move_line_ids[0].location_dest_id - allowed_locations = self.env["stock.location"].search( - [ - ( - "id", - "child_of", - move.picking_id.picking_type_id.default_location_dest_id.id, - ) - ] - ) - zone_locations = self.env["stock.location"].search( - [("id", "child_of", move_dest_location.id)] - ) - if dest_location not in allowed_locations: - return { - "success": False, - "code": "forbidden", - "message": {"title": "Forbidden", "body": "You cannot place it here"}, - } - elif ( - dest_location in allowed_locations - and dest_location not in zone_locations - and confirmation - ): - return { - "success": False, - "code": "need_confirmation", - "message": {"title": "Confirm", "body": "Are you sure ?"}, - } - if move.state == "cancel": - return { - "success": False, - "code": "restart", - "message": { - "title": "Restart", - "body": "Restart the operation someone has canceld it.", - }, - } - move.move_line_ids[0].location_dest_id = dest_location.id - move._action_done() - return {"success": True} - - def cancel(self, package_level_id): - package = self.env["stock.package_level"].browse(package_level_id) - package.move_ids[0].cancel() - return {"success": True} - - def _validator_cancel(self): - return { - "package_level_id": {"coerce": to_int, "required": True, "type": "integer"} - } - - def _validator_validate(self): - return { - "package_level_id": {"coerce": to_int, "required": True, "type": "integer"}, - "location_name": {"type": "string", "nullable": False, "required": True}, - } - - def _validator_return_validate(self): - return { - "success": {"type": "boolean", "nullable": True, "required": True}, - "code": {"type": "string", "nullable": True, "required": False}, - "message": { - "type": "dict", - "schema": { - "body": {"type": "string", "nullable": False, "required": True} - }, - }, - } - - def _validator_scan(self): - return {"barcode": {"type": "string", "nullable": False, "required": True}} - - def _validator_return_scan(self): - return { - "success": {"type": "boolean", "nullable": True, "required": True}, - "code": {"type": "string", "nullable": True, "required": False}, - "message": { - "type": "dict", - "schema": { - "body": {"type": "string", "nullable": False, "required": True} - }, - }, - "data": { - "type": "dict", - "schema": { - "id": {"coerce": to_int, "required": True, "type": "integer"}, - "location_src": { - "type": "dict", - "schema": { - "id": { - "coerce": to_int, - "required": True, - "type": "integer", - }, - "name": { - "type": "string", - "nullable": False, - "required": True, - }, - }, - }, - "location_dst": { - "type": "dict", - "schema": { - "id": { - "coerce": to_int, - "required": True, - "type": "integer", - }, - "name": { - "type": "string", - "nullable": False, - "required": True, - }, - }, - }, - "product": { - "type": "dict", - "schema": { - "id": { - "coerce": to_int, - "required": True, - "type": "integer", - }, - "name": { - "type": "string", - "nullable": False, - "required": True, - }, - }, - }, - "picking": { - "type": "dict", - "schema": { - "id": { - "coerce": to_int, - "required": True, - "type": "integer", - }, - "name": { - "type": "string", - "nullable": False, - "required": True, - }, - }, - }, - }, - }, - } - def get_by_name(self, pack_name): """ Get pack informations diff --git a/shopfloor/services/single_pack_putaway.py b/shopfloor/services/single_pack_putaway.py index f46024bd0dc..3c68068915c 100644 --- a/shopfloor/services/single_pack_putaway.py +++ b/shopfloor/services/single_pack_putaway.py @@ -1,3 +1,4 @@ +from odoo.addons.base_rest.components.service import to_int from odoo.addons.component.core import Component @@ -8,3 +9,283 @@ class SinglePackPutaway(Component): _name = "shopfloor.single.pack.putaway" _usage = "single_pack_putaway" _description = __doc__ + + def scan(self, barcode): + """Scan a pack barcode""" + + company = self.env.user.company_id # FIXME add logic to get proper company + # FIXME add logic to get proper warehouse + warehouse = self.env["stock.warehouse"].search([])[0] + picking_type = ( + warehouse.int_type_id + ) # FIXME add logic to get picking type properly + + # TODO define on what we search (pack name, pack barcode ...) + pack = self.env["stock.quant.package"].search([("name", "=", barcode)]) + if not pack: + return { + "success": False, + "code": "not_found", + "message": { + "title": "Pack not found", + "body": "the pack %s doesn't exists" % barcode, + }, + } + allowed_locations = self.env["stock.location"].search( + [("id", "child_of", picking_type.default_location_src_id.id)] + ) + if pack.location_id not in allowed_locations: + return { + "success": False, + "code": "forbidden", + "message": { + "title": "do not process", + "body": "pack %s is not in %s location" + % (barcode, picking_type.default_location_src_id.name), + }, + } + quantity = pack.quant_ids[0].quantity + existing_operations = self.env["stock.move.line"].search( + [("qty_done", "=", quantity), ("package_id", "=", pack.id)] + ) + if ( + existing_operations + and existing_operations[0].picking_id.picking_type_id != picking_type + ): + return { + "success": False, + "code": "forbidden", + "message": { + "title": "do not process", + "body": "An operation exists in %s %s. " + "You cannot process it with this shopfloor process." + % ( + existing_operations[0].picking_id.picking_type_id.name, + existing_operations[0].picking_id.name, + ), + }, + } + elif existing_operations: + move = existing_operations.move_id + return { + "data": { + "id": move.move_line_ids[0].package_level_id.id, + "location_src": { + "id": pack.location_id.id, + "name": pack.location_id.name, + }, + "location_dst": { + "id": move.move_line_ids[0].location_dest_id.id, + "name": move.move_line_ids[0].location_dest_id.name, + }, + "product": { + "id": move.product_id.name, + "name": move.product_id.name, + }, + "picking": {"id": move.picking_id.id, "name": move.picking_id.name}, + }, + "success": False, + "code": "need_confirmation", + "message": { + "title": "Already started", + "body": "Operation already running. " + "Would you like to take it over ?", + }, + } + product = pack.quant_ids[ + 0 + ].product_id # FIXME we consider only one product per pack + move_vals = { + "picking_type_id": picking_type.id, + "product_id": product.id, + "location_id": pack.location_id.id, + "location_dest_id": picking_type.default_location_dest_id.id, + "name": product.name, + "product_uom": product.uom_id.id, + "product_uom_qty": pack.quant_ids[0].quantity, + "company_id": company.id, + } + move = self.env["stock.move"].create(move_vals) + move._action_confirm() + self.env["stock.package_level"].create( + { + "package_id": pack.id, + "move_ids": [(6, 0, [move.id])], + "company_id": company.id, + } + ) + move.picking_id.action_assign() + return_vals = { + "success": True, + "data": { + "id": move.move_line_ids[0].package_level_id.id, + "location_src": { + "id": pack.location_id.id, + "name": pack.location_id.name, + }, + "location_dst": { + "id": move.move_line_ids[0].location_dest_id.id, + "name": move.move_line_ids[0].location_dest_id.name, + }, + "product": {"id": move.product_id.name, "name": move.product_id.name}, + "picking": {"id": move.picking_id.id, "name": move.picking_id.name}, + }, + } + return return_vals + + def validate(self, package_level_id, location_name, confirmation=False): + package = self.env["stock.package_level"].browse(package_level_id) + move = package.move_line_ids[0].move_id + dest_location = self.env["stock.location"].search( + [("name", "=", location_name)] + ) + move_dest_location = move.move_line_ids[0].location_dest_id + allowed_locations = self.env["stock.location"].search( + [ + ( + "id", + "child_of", + move.picking_id.picking_type_id.default_location_dest_id.id, + ) + ] + ) + zone_locations = self.env["stock.location"].search( + [("id", "child_of", move_dest_location.id)] + ) + if dest_location not in allowed_locations: + return { + "success": False, + "code": "forbidden", + "message": {"title": "Forbidden", "body": "You cannot place it here"}, + } + elif ( + dest_location in allowed_locations + and dest_location not in zone_locations + and confirmation + ): + return { + "success": False, + "code": "need_confirmation", + "message": {"title": "Confirm", "body": "Are you sure ?"}, + } + if move.state == "cancel": + return { + "success": False, + "code": "restart", + "message": { + "title": "Restart", + "body": "Restart the operation someone has canceld it.", + }, + } + move.move_line_ids[0].location_dest_id = dest_location.id + move._action_done() + return {"success": True} + + def cancel(self, package_level_id): + package = self.env["stock.package_level"].browse(package_level_id) + package.move_ids[0].cancel() + return {"success": True} + + def _validator_cancel(self): + return { + "package_level_id": {"coerce": to_int, "required": True, "type": "integer"} + } + + def _validator_validate(self): + return { + "package_level_id": {"coerce": to_int, "required": True, "type": "integer"}, + "location_name": {"type": "string", "nullable": False, "required": True}, + } + + def _validator_return_validate(self): + return { + "success": {"type": "boolean", "nullable": True, "required": True}, + "code": {"type": "string", "nullable": True, "required": False}, + "message": { + "type": "dict", + "schema": { + "body": {"type": "string", "nullable": False, "required": True} + }, + }, + } + + def _validator_scan(self): + return {"barcode": {"type": "string", "nullable": False, "required": True}} + + def _validator_return_scan(self): + return { + "success": {"type": "boolean", "nullable": True, "required": True}, + "code": {"type": "string", "nullable": True, "required": False}, + "message": { + "type": "dict", + "schema": { + "body": {"type": "string", "nullable": False, "required": True} + }, + }, + "data": { + "type": "dict", + "schema": { + "id": {"coerce": to_int, "required": True, "type": "integer"}, + "location_src": { + "type": "dict", + "schema": { + "id": { + "coerce": to_int, + "required": True, + "type": "integer", + }, + "name": { + "type": "string", + "nullable": False, + "required": True, + }, + }, + }, + "location_dst": { + "type": "dict", + "schema": { + "id": { + "coerce": to_int, + "required": True, + "type": "integer", + }, + "name": { + "type": "string", + "nullable": False, + "required": True, + }, + }, + }, + "product": { + "type": "dict", + "schema": { + "id": { + "coerce": to_int, + "required": True, + "type": "integer", + }, + "name": { + "type": "string", + "nullable": False, + "required": True, + }, + }, + }, + "picking": { + "type": "dict", + "schema": { + "id": { + "coerce": to_int, + "required": True, + "type": "integer", + }, + "name": { + "type": "string", + "nullable": False, + "required": True, + }, + }, + }, + }, + }, + } diff --git a/shopfloor/tests/test_putaway.py b/shopfloor/tests/test_single_pack_putaway.py similarity index 100% rename from shopfloor/tests/test_putaway.py rename to shopfloor/tests/test_single_pack_putaway.py From b1544bb411a695e911adf33248ecd19087aebe29 Mon Sep 17 00:00:00 2001 From: Guewen Baconnier Date: Thu, 23 Jan 2020 11:41:42 +0100 Subject: [PATCH 045/986] Use a unified response format (use self._response_schema() now) --- shopfloor/services/location.py | 50 ++++++---- shopfloor/services/menu.py | 41 +++++--- shopfloor/services/pack.py | 19 ++-- shopfloor/services/profile.py | 56 ++++++----- shopfloor/services/service.py | 21 +++++ shopfloor/services/single_pack_putaway.py | 110 ++++++---------------- 6 files changed, 147 insertions(+), 150 deletions(-) diff --git a/shopfloor/services/location.py b/shopfloor/services/location.py index 17aac7be05c..2a3f5ea3557 100644 --- a/shopfloor/services/location.py +++ b/shopfloor/services/location.py @@ -28,7 +28,7 @@ def search(self, name_fragment=None): ] ) records = self.env[self._expose_model].search(domain) - return {"size": len(records), "data": self._to_json(records)} + return {"data": {"size": len(records), "records": self._to_json(records)}} def _get_base_search_domain(self): # TODO add filter on warehouse of the current profile @@ -40,29 +40,39 @@ def _validator_search(self): } def _validator_return_search(self): - return { - "size": {"coerce": to_int, "required": True, "type": "integer"}, - "data": { - "type": "list", - "schema": { - "type": "dict", + return self._response_schema( + { + "size": {"coerce": to_int, "required": True, "type": "integer"}, + "records": { + "type": "list", "schema": { - "id": {"coerce": to_int, "required": True, "type": "integer"}, - "name": {"type": "string", "nullable": False, "required": True}, - "complete_name": { - "type": "string", - "nullable": False, - "required": True, - }, - "barcode": { - "type": "string", - "nullable": False, - "required": False, + "type": "dict", + "schema": { + "id": { + "coerce": to_int, + "required": True, + "type": "integer", + }, + "name": { + "type": "string", + "nullable": False, + "required": True, + }, + "complete_name": { + "type": "string", + "nullable": False, + "required": True, + }, + "barcode": { + "type": "string", + "nullable": False, + "required": False, + }, }, }, }, - }, - } + } + ) def _convert_one_record(self, record): return { diff --git a/shopfloor/services/menu.py b/shopfloor/services/menu.py index 220003a5599..374772d9c28 100644 --- a/shopfloor/services/menu.py +++ b/shopfloor/services/menu.py @@ -39,7 +39,7 @@ def search(self, name_fragment=None): if name_fragment: domain.append(("name", "ilike", name_fragment)) records = self.env[self._expose_model].search(domain) - return {"size": len(records), "data": self._to_json(records)} + return {"data": {"size": len(records), "records": self._to_json(records)}} def _validator_search(self): return { @@ -47,24 +47,35 @@ def _validator_search(self): } def _validator_return_search(self): - return { - "size": {"coerce": to_int, "required": True, "type": "integer"}, - "data": { - "type": "list", - "schema": { - "type": "dict", + return self._response_schema( + { + "size": {"coerce": to_int, "required": True, "type": "integer"}, + "records": { + "type": "list", + "required": True, "schema": { - "id": {"coerce": to_int, "required": True, "type": "integer"}, - "name": {"type": "string", "nullable": False, "required": True}, - "process": { - "type": "string", - "nullable": False, - "required": True, + "type": "dict", + "schema": { + "id": { + "coerce": to_int, + "required": True, + "type": "integer", + }, + "name": { + "type": "string", + "nullable": False, + "required": True, + }, + "process": { + "type": "string", + "nullable": False, + "required": True, + }, }, }, }, - }, - } + } + ) def _convert_one_record(self, record): return {"id": record.id, "name": record.name, "process": record.process_code} diff --git a/shopfloor/services/pack.py b/shopfloor/services/pack.py index 46b70147646..2f8a591621a 100644 --- a/shopfloor/services/pack.py +++ b/shopfloor/services/pack.py @@ -25,7 +25,7 @@ def _validator_get_by_name(self): return {"pack_name": {"type": "string", "nullable": False, "required": True}} def _validator_return_get_by_name(self): - return {"data": self._record_return_schema} + return self._response_schema(self._record_return_schema) def _convert_one_record(self, record): return { @@ -37,16 +37,13 @@ def _convert_one_record(self, record): @property def _record_return_schema(self): return { - "type": "dict", - "schema": { - "id": {"coerce": to_int, "required": True, "type": "integer"}, - "name": {"type": "string", "nullable": False, "required": True}, - "location": { - "type": "dict", - "schema": { - "id": {"coerce": to_int, "required": True, "type": "integer"}, - "name": {"type": "string", "nullable": False, "required": True}, - }, + "id": {"coerce": to_int, "required": True, "type": "integer"}, + "name": {"type": "string", "nullable": False, "required": True}, + "location": { + "type": "dict", + "schema": { + "id": {"coerce": to_int, "required": True, "type": "integer"}, + "name": {"type": "string", "nullable": False, "required": True}, }, }, } diff --git a/shopfloor/services/profile.py b/shopfloor/services/profile.py index 92f9d3eac32..2ccf78be0fe 100644 --- a/shopfloor/services/profile.py +++ b/shopfloor/services/profile.py @@ -31,7 +31,7 @@ def search(self, name_fragment=None): if name_fragment: domain.append(("name", "ilike", name_fragment)) records = self.env[self._expose_model].search(domain) - return {"size": len(records), "data": self._to_json(records)} + return {"data": {"size": len(records), "records": self._to_json(records)}} def _get_base_search_domain(self): # shopfloor_profile_ids is a one2one in practice. @@ -58,34 +58,44 @@ def _validator_search(self): } def _validator_return_search(self): - return { - "size": {"coerce": to_int, "required": True, "type": "integer"}, - "data": { - "type": "list", - "schema": { - "type": "dict", + return self._response_schema( + { + "size": {"coerce": to_int, "required": True, "type": "integer"}, + "records": { + "type": "list", "schema": { - "id": {"coerce": to_int, "required": True, "type": "integer"}, - "name": {"type": "string", "nullable": False, "required": True}, - "warehouse": { - "type": "dict", - "schema": { - "id": { - "coerce": to_int, - "required": True, - "type": "integer", - }, - "name": { - "type": "string", - "nullable": False, - "required": True, + "type": "dict", + "schema": { + "id": { + "coerce": to_int, + "required": True, + "type": "integer", + }, + "name": { + "type": "string", + "nullable": False, + "required": True, + }, + "warehouse": { + "type": "dict", + "schema": { + "id": { + "coerce": to_int, + "required": True, + "type": "integer", + }, + "name": { + "type": "string", + "nullable": False, + "required": True, + }, }, }, }, }, }, - }, - } + } + ) def _convert_one_record(self, record): return { diff --git a/shopfloor/services/service.py b/shopfloor/services/service.py index 2363b60af48..91620efc21b 100644 --- a/shopfloor/services/service.py +++ b/shopfloor/services/service.py @@ -37,6 +37,27 @@ def _to_json(self, records): res.append(self._convert_one_record(record)) return res + def _response_schema(self, data_schema=None): + if not data_schema: + data_schema = {} + return { + "data": {"type": "dict", "required": False, "schema": data_schema}, + "state": {"type": "string", "required": False}, + "message": { + "type": "dict", + "required": False, + "schema": { + "message_type": { + "type": "string", + "required": True, + "allowed": ["info", "warning", "error"], + }, + "title": {"type": "string", "required": False}, + "message": {"type": "string", "required": True}, + }, + }, + } + def _get_openapi_default_parameters(self): defaults = super()._get_openapi_default_parameters() demo_api_key = self.env.ref( diff --git a/shopfloor/services/single_pack_putaway.py b/shopfloor/services/single_pack_putaway.py index 3c68068915c..a5a982c370d 100644 --- a/shopfloor/services/single_pack_putaway.py +++ b/shopfloor/services/single_pack_putaway.py @@ -198,94 +198,42 @@ def _validator_validate(self): } def _validator_return_validate(self): - return { - "success": {"type": "boolean", "nullable": True, "required": True}, - "code": {"type": "string", "nullable": True, "required": False}, - "message": { - "type": "dict", - "schema": { - "body": {"type": "string", "nullable": False, "required": True} - }, - }, - } + return self._response_schema() def _validator_scan(self): return {"barcode": {"type": "string", "nullable": False, "required": True}} def _validator_return_scan(self): - return { - "success": {"type": "boolean", "nullable": True, "required": True}, - "code": {"type": "string", "nullable": True, "required": False}, - "message": { - "type": "dict", - "schema": { - "body": {"type": "string", "nullable": False, "required": True} - }, - }, - "data": { - "type": "dict", - "schema": { - "id": {"coerce": to_int, "required": True, "type": "integer"}, - "location_src": { - "type": "dict", - "schema": { - "id": { - "coerce": to_int, - "required": True, - "type": "integer", - }, - "name": { - "type": "string", - "nullable": False, - "required": True, - }, - }, + return self._response_schema( + { + "id": {"coerce": to_int, "required": True, "type": "integer"}, + "location_src": { + "type": "dict", + "schema": { + "id": {"coerce": to_int, "required": True, "type": "integer"}, + "name": {"type": "string", "nullable": False, "required": True}, }, - "location_dst": { - "type": "dict", - "schema": { - "id": { - "coerce": to_int, - "required": True, - "type": "integer", - }, - "name": { - "type": "string", - "nullable": False, - "required": True, - }, - }, + }, + "location_dst": { + "type": "dict", + "schema": { + "id": {"coerce": to_int, "required": True, "type": "integer"}, + "name": {"type": "string", "nullable": False, "required": True}, }, - "product": { - "type": "dict", - "schema": { - "id": { - "coerce": to_int, - "required": True, - "type": "integer", - }, - "name": { - "type": "string", - "nullable": False, - "required": True, - }, - }, + }, + "product": { + "type": "dict", + "schema": { + "id": {"coerce": to_int, "required": True, "type": "integer"}, + "name": {"type": "string", "nullable": False, "required": True}, }, - "picking": { - "type": "dict", - "schema": { - "id": { - "coerce": to_int, - "required": True, - "type": "integer", - }, - "name": { - "type": "string", - "nullable": False, - "required": True, - }, - }, + }, + "picking": { + "type": "dict", + "schema": { + "id": {"coerce": to_int, "required": True, "type": "integer"}, + "name": {"type": "string", "nullable": False, "required": True}, }, }, - }, - } + } + ) From 528c4382f4590aa71d57aa184cf46dbfb159edcf Mon Sep 17 00:00:00 2001 From: Guewen Baconnier Date: Thu, 23 Jan 2020 12:01:51 +0100 Subject: [PATCH 046/986] Add method to generate the response body (use self._response()) --- shopfloor/services/location.py | 4 +++- shopfloor/services/menu.py | 4 +++- shopfloor/services/pack.py | 2 +- shopfloor/services/profile.py | 4 +++- shopfloor/services/service.py | 26 ++++++++++++++++++++++++++ 5 files changed, 36 insertions(+), 4 deletions(-) diff --git a/shopfloor/services/location.py b/shopfloor/services/location.py index 2a3f5ea3557..a7898691201 100644 --- a/shopfloor/services/location.py +++ b/shopfloor/services/location.py @@ -28,7 +28,9 @@ def search(self, name_fragment=None): ] ) records = self.env[self._expose_model].search(domain) - return {"data": {"size": len(records), "records": self._to_json(records)}} + return self._response( + data={"size": len(records), "records": self._to_json(records)} + ) def _get_base_search_domain(self): # TODO add filter on warehouse of the current profile diff --git a/shopfloor/services/menu.py b/shopfloor/services/menu.py index 374772d9c28..33ea065b67d 100644 --- a/shopfloor/services/menu.py +++ b/shopfloor/services/menu.py @@ -39,7 +39,9 @@ def search(self, name_fragment=None): if name_fragment: domain.append(("name", "ilike", name_fragment)) records = self.env[self._expose_model].search(domain) - return {"data": {"size": len(records), "records": self._to_json(records)}} + return self._response( + data={"size": len(records), "records": self._to_json(records)} + ) def _validator_search(self): return { diff --git a/shopfloor/services/pack.py b/shopfloor/services/pack.py index 2f8a591621a..e13ea02c5ef 100644 --- a/shopfloor/services/pack.py +++ b/shopfloor/services/pack.py @@ -19,7 +19,7 @@ def get_by_name(self, pack_name): # TODO, is it what we want? error if not found? limit=1, ) - return self._to_json(pack)[:1] + return self._response(data=self._to_json(pack)[:1]) def _validator_get_by_name(self): return {"pack_name": {"type": "string", "nullable": False, "required": True}} diff --git a/shopfloor/services/profile.py b/shopfloor/services/profile.py index 2ccf78be0fe..86e2ed1175a 100644 --- a/shopfloor/services/profile.py +++ b/shopfloor/services/profile.py @@ -31,7 +31,9 @@ def search(self, name_fragment=None): if name_fragment: domain.append(("name", "ilike", name_fragment)) records = self.env[self._expose_model].search(domain) - return {"data": {"size": len(records), "records": self._to_json(records)}} + return self._response( + data={"size": len(records), "records": self._to_json(records)} + ) def _get_base_search_domain(self): # shopfloor_profile_ids is a one2one in practice. diff --git a/shopfloor/services/service.py b/shopfloor/services/service.py index 91620efc21b..d96822cb43b 100644 --- a/shopfloor/services/service.py +++ b/shopfloor/services/service.py @@ -37,7 +37,33 @@ def _to_json(self, records): res.append(self._convert_one_record(record)) return res + def _response(self, data=None, state=None, message=None): + """Base "envelope" for the responses + + All the keys are optional. + + :param data: dictionary of values + :param 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) + """ + response = {} + if data: + response["data"] = data + if state: + response["state"] = state + if message: + response["message"] = message + return response + def _response_schema(self, data_schema=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. + """ if not data_schema: data_schema = {} return { From bb92121906543b2301668b877b77be0ae3e74291 Mon Sep 17 00:00:00 2001 From: Benoit Date: Thu, 23 Jan 2020 11:53:04 +0100 Subject: [PATCH 047/986] fix tests --- shopfloor/services/single_pack_putaway.py | 14 +++++++++++--- shopfloor/tests/__init__.py | 2 +- shopfloor/tests/test_single_pack_putaway.py | 6 +++--- 3 files changed, 15 insertions(+), 7 deletions(-) diff --git a/shopfloor/services/single_pack_putaway.py b/shopfloor/services/single_pack_putaway.py index a5a982c370d..9192526ebc6 100644 --- a/shopfloor/services/single_pack_putaway.py +++ b/shopfloor/services/single_pack_putaway.py @@ -95,11 +95,12 @@ def scan(self, barcode): product = pack.quant_ids[ 0 ].product_id # FIXME we consider only one product per pack + default_location_dest = picking_type.default_location_dest_id move_vals = { "picking_type_id": picking_type.id, "product_id": product.id, "location_id": pack.location_id.id, - "location_dest_id": picking_type.default_location_dest_id.id, + "location_dest_id": default_location_dest.id, "name": product.name, "product_uom": product.uom_id.id, "product_uom_qty": pack.quant_ids[0].quantity, @@ -107,11 +108,18 @@ def scan(self, barcode): } move = self.env["stock.move"].create(move_vals) move._action_confirm() + location_dest_id = ( + default_location_dest._get_putaway_strategy(product).id + or default_location_dest.id + ) self.env["stock.package_level"].create( { "package_id": pack.id, - "move_ids": [(6, 0, [move.id])], + "move_ids": [(4, move.id)], "company_id": company.id, + "is_done": True, + "location_id": pack.location_id.id, + "location_dest_id": location_dest_id, } ) move.picking_id.action_assign() @@ -127,7 +135,7 @@ def scan(self, barcode): "id": move.move_line_ids[0].location_dest_id.id, "name": move.move_line_ids[0].location_dest_id.name, }, - "product": {"id": move.product_id.name, "name": move.product_id.name}, + "product": {"id": move.product_id.id, "name": move.product_id.name}, "picking": {"id": move.picking_id.id, "name": move.picking_id.name}, }, } diff --git a/shopfloor/tests/__init__.py b/shopfloor/tests/__init__.py index 3a80e1ce963..188a3788ab0 100644 --- a/shopfloor/tests/__init__.py +++ b/shopfloor/tests/__init__.py @@ -1 +1 @@ -from . import test_putaway +from . import test_single_pack_putaway diff --git a/shopfloor/tests/test_single_pack_putaway.py b/shopfloor/tests/test_single_pack_putaway.py index 5b09c332413..82a8e6c6c29 100644 --- a/shopfloor/tests/test_single_pack_putaway.py +++ b/shopfloor/tests/test_single_pack_putaway.py @@ -5,6 +5,7 @@ class PutawayCase(CommonCase): def setUp(self, *args, **kwargs): super(PutawayCase, self).setUp(*args, **kwargs) stock_location = self.env.ref("stock.stock_location_stock") + out_location = stock_location.child_ids[1] self.productA = self.env["product.product"].create( {"name": "Product A", "type": "product"} ) @@ -23,12 +24,11 @@ def setUp(self, *args, **kwargs): { "product_id": self.productA.id, "location_in_id": stock_location.id, - "location_out_id": stock_location.child_ids[0].id, + "location_out_id": out_location.id, } ) - with self.work_on_services() as work: - self.service = work.component(usage="pack") + self.service = work.component(usage="single_pack_putaway") def test_scan_pack(self): barcode = self.packA.name From fde60c69b60b2dcd6eb5f31ea237c0ceb89e26ff Mon Sep 17 00:00:00 2001 From: Benoit Date: Thu, 23 Jan 2020 12:20:53 +0100 Subject: [PATCH 048/986] fix return format --- shopfloor/services/single_pack_putaway.py | 78 +++++++++++++++-------- 1 file changed, 51 insertions(+), 27 deletions(-) diff --git a/shopfloor/services/single_pack_putaway.py b/shopfloor/services/single_pack_putaway.py index 9192526ebc6..18de0687df6 100644 --- a/shopfloor/services/single_pack_putaway.py +++ b/shopfloor/services/single_pack_putaway.py @@ -24,11 +24,11 @@ def scan(self, barcode): pack = self.env["stock.quant.package"].search([("name", "=", barcode)]) if not pack: return { - "success": False, - "code": "not_found", + "state": "start", "message": { + "message_type": "error", "title": "Pack not found", - "body": "the pack %s doesn't exists" % barcode, + "message": "the pack %s doesn't exists" % barcode, }, } allowed_locations = self.env["stock.location"].search( @@ -36,11 +36,11 @@ def scan(self, barcode): ) if pack.location_id not in allowed_locations: return { - "success": False, - "code": "forbidden", + "state": "start", "message": { + "message_type": "error", "title": "do not process", - "body": "pack %s is not in %s location" + "message": "pack %s is not in %s location" % (barcode, picking_type.default_location_src_id.name), }, } @@ -53,11 +53,11 @@ def scan(self, barcode): and existing_operations[0].picking_id.picking_type_id != picking_type ): return { - "success": False, - "code": "forbidden", + "state": "start", "message": { + "message_type": "error", "title": "do not process", - "body": "An operation exists in %s %s. " + "message": "An operation exists in %s %s. " "You cannot process it with this shopfloor process." % ( existing_operations[0].picking_id.picking_type_id.name, @@ -84,11 +84,11 @@ def scan(self, barcode): }, "picking": {"id": move.picking_id.id, "name": move.picking_id.name}, }, - "success": False, - "code": "need_confirmation", + "state": "confirm_start", "message": { + "message_type": "warning", "title": "Already started", - "body": "Operation already running. " + "message": "Operation already running. " "Would you like to take it over ?", }, } @@ -107,7 +107,7 @@ def scan(self, barcode): "company_id": company.id, } move = self.env["stock.move"].create(move_vals) - move._action_confirm() + move._action_confirm(merge=False) location_dest_id = ( default_location_dest._get_putaway_strategy(product).id or default_location_dest.id @@ -123,8 +123,13 @@ def scan(self, barcode): } ) move.picking_id.action_assign() - return_vals = { - "success": True, + return { + "state": "scan_location", + "message": { + "message_type": "info", + "title": "Start", + "message": "The move is ready, you can scan the destination location.", + }, "data": { "id": move.move_line_ids[0].package_level_id.id, "location_src": { @@ -139,7 +144,6 @@ def scan(self, barcode): "picking": {"id": move.picking_id.id, "name": move.picking_id.name}, }, } - return return_vals def validate(self, package_level_id, location_name, confirmation=False): package = self.env["stock.package_level"].browse(package_level_id) @@ -162,9 +166,12 @@ def validate(self, package_level_id, location_name, confirmation=False): ) if dest_location not in allowed_locations: return { - "success": False, - "code": "forbidden", - "message": {"title": "Forbidden", "body": "You cannot place it here"}, + "state": "scan_location", + "message": { + "message_type": "error", + "title": "Forbidden", + "message": "You cannot place it here", + }, } elif ( dest_location in allowed_locations @@ -172,27 +179,44 @@ def validate(self, package_level_id, location_name, confirmation=False): and confirmation ): return { - "success": False, - "code": "need_confirmation", - "message": {"title": "Confirm", "body": "Are you sure ?"}, + "state": "confirm_location", + "message": { + "message_type": "warning", + "title": "Confirm", + "message": "Are you sure ?", + }, } if move.state == "cancel": return { - "success": False, - "code": "restart", + "state": "start", "message": { + "message_type": "warning", "title": "Restart", - "body": "Restart the operation someone has canceld it.", + "message": "Restart the operation someone has canceled it.", }, } move.move_line_ids[0].location_dest_id = dest_location.id move._action_done() - return {"success": True} + return { + "state": "start", + "message": { + "message_type": "info", + "title": "Start", + "message": "The pack has been moved, you can scan a new pack.", + }, + } def cancel(self, package_level_id): package = self.env["stock.package_level"].browse(package_level_id) package.move_ids[0].cancel() - return {"success": True} + return { + "state": "start", + "message": { + "message_type": "info", + "title": "Start", + "message": "The move has been canceled, you can scan a new pack.", + }, + } def _validator_cancel(self): return { From 3ba43de8f2a0bb7e527e324be36019542a6391b0 Mon Sep 17 00:00:00 2001 From: Guewen Baconnier Date: Thu, 23 Jan 2020 13:30:37 +0100 Subject: [PATCH 049/986] Add translations gettext on messages --- shopfloor/services/single_pack_putaway.py | 144 ++++++++++++---------- 1 file changed, 80 insertions(+), 64 deletions(-) diff --git a/shopfloor/services/single_pack_putaway.py b/shopfloor/services/single_pack_putaway.py index 18de0687df6..80d20d5a3dd 100644 --- a/shopfloor/services/single_pack_putaway.py +++ b/shopfloor/services/single_pack_putaway.py @@ -1,3 +1,5 @@ +from odoo import _ + from odoo.addons.base_rest.components.service import to_int from odoo.addons.component.core import Component @@ -23,27 +25,27 @@ def scan(self, barcode): # TODO define on what we search (pack name, pack barcode ...) pack = self.env["stock.quant.package"].search([("name", "=", barcode)]) if not pack: - return { - "state": "start", - "message": { + return self._response( + state="start", + message={ "message_type": "error", - "title": "Pack not found", - "message": "the pack %s doesn't exists" % barcode, + "title": _("Pack not found"), + "message": _("The pack %s doesn't exist") % barcode, }, - } + ) allowed_locations = self.env["stock.location"].search( [("id", "child_of", picking_type.default_location_src_id.id)] ) if pack.location_id not in allowed_locations: - return { - "state": "start", - "message": { + return self._response( + state="start", + message={ "message_type": "error", - "title": "do not process", - "message": "pack %s is not in %s location" + "title": _("Cannot proceed"), + "message": _("pack %s is not in %s location") % (barcode, picking_type.default_location_src_id.name), }, - } + ) quantity = pack.quant_ids[0].quantity existing_operations = self.env["stock.move.line"].search( [("qty_done", "=", quantity), ("package_id", "=", pack.id)] @@ -52,23 +54,25 @@ def scan(self, barcode): existing_operations and existing_operations[0].picking_id.picking_type_id != picking_type ): - return { - "state": "start", - "message": { + return self._response( + state="start", + message={ "message_type": "error", - "title": "do not process", - "message": "An operation exists in %s %s. " - "You cannot process it with this shopfloor process." + "title": _("Cannot proceed"), + "message": _( + "An operation exists in %s %s. " + "You cannot process it with this shopfloor process." + ) % ( existing_operations[0].picking_id.picking_type_id.name, existing_operations[0].picking_id.name, ), }, - } + ) elif existing_operations: move = existing_operations.move_id - return { - "data": { + return self._response( + data={ "id": move.move_line_ids[0].package_level_id.id, "location_src": { "id": pack.location_id.id, @@ -84,14 +88,15 @@ def scan(self, barcode): }, "picking": {"id": move.picking_id.id, "name": move.picking_id.name}, }, - "state": "confirm_start", - "message": { + state="confirm_start", + message={ "message_type": "warning", - "title": "Already started", - "message": "Operation already running. " - "Would you like to take it over ?", + "title": _("Already started"), + "message": _( + "Operation already running. " "Would you like to take it over ?" + ), }, - } + ) product = pack.quant_ids[ 0 ].product_id # FIXME we consider only one product per pack @@ -123,14 +128,16 @@ def scan(self, barcode): } ) move.picking_id.action_assign() - return { - "state": "scan_location", - "message": { + return self._response( + state="scan_location", + message={ "message_type": "info", - "title": "Start", - "message": "The move is ready, you can scan the destination location.", + "title": _("Start"), + "message": _( + "The move is ready, you can scan the destination location." + ), }, - "data": { + data={ "id": move.move_line_ids[0].package_level_id.id, "location_src": { "id": pack.location_id.id, @@ -143,7 +150,7 @@ def scan(self, barcode): "product": {"id": move.product_id.id, "name": move.product_id.name}, "picking": {"id": move.picking_id.id, "name": move.picking_id.name}, }, - } + ) def validate(self, package_level_id, location_name, confirmation=False): package = self.env["stock.package_level"].browse(package_level_id) @@ -165,58 +172,67 @@ def validate(self, package_level_id, location_name, confirmation=False): [("id", "child_of", move_dest_location.id)] ) if dest_location not in allowed_locations: - return { - "state": "scan_location", - "message": { + return self._response( + state="scan_location", + message={ "message_type": "error", - "title": "Forbidden", - "message": "You cannot place it here", + "title": _("Forbidden"), + "message": _("You cannot place it here"), }, - } + ) elif ( dest_location in allowed_locations and dest_location not in zone_locations and confirmation ): - return { - "state": "confirm_location", - "message": { + return self._response( + state="confirm_location", + message={ "message_type": "warning", - "title": "Confirm", - "message": "Are you sure ?", + "title": _("Confirm"), + "message": _("Are you sure?"), }, - } + ) if move.state == "cancel": - return { - "state": "start", - "message": { + return self._response( + state="start", + message={ "message_type": "warning", - "title": "Restart", - "message": "Restart the operation someone has canceled it.", + "title": _("Restart"), + "message": _("Restart the operation, someone has canceled it."), }, - } + ) move.move_line_ids[0].location_dest_id = dest_location.id move._action_done() - return { - "state": "start", - "message": { + return self._response( + state="start", + message={ "message_type": "info", - "title": "Start", - "message": "The pack has been moved, you can scan a new pack.", + "title": _("Start"), + "message": _("The pack has been moved, you can scan a new pack."), }, - } + ) def cancel(self, package_level_id): package = self.env["stock.package_level"].browse(package_level_id) + if not package.exists(): + return self._response( + state="start", + message={ + "message_type": "error", + "title": _("Start again"), + "message": _("This operation does not exist anymore."), + }, + ) package.move_ids[0].cancel() - return { - "state": "start", - "message": { + return self._response( + state="start", + message={ "message_type": "info", - "title": "Start", - "message": "The move has been canceled, you can scan a new pack.", + "title": _("Start"), + "message": _("The move has been canceled, you can scan a new pack."), }, - } + ) def _validator_cancel(self): return { From 87f06162e6ae2572b318a268de5b4f73db88c517 Mon Sep 17 00:00:00 2001 From: Benoit Date: Thu, 23 Jan 2020 14:09:48 +0100 Subject: [PATCH 050/986] use location barcode instead of name --- shopfloor/services/single_pack_putaway.py | 49 ++++++++++----------- shopfloor/tests/test_single_pack_putaway.py | 22 +++++---- 2 files changed, 37 insertions(+), 34 deletions(-) diff --git a/shopfloor/services/single_pack_putaway.py b/shopfloor/services/single_pack_putaway.py index 80d20d5a3dd..52f3994b3ec 100644 --- a/shopfloor/services/single_pack_putaway.py +++ b/shopfloor/services/single_pack_putaway.py @@ -152,11 +152,20 @@ def scan(self, barcode): }, ) - def validate(self, package_level_id, location_name, confirmation=False): + def validate(self, package_level_id, location_barcode, confirmation=False): package = self.env["stock.package_level"].browse(package_level_id) move = package.move_line_ids[0].move_id + if move.state == "cancel": + return self._response( + state="start", + message={ + "message_type": "warning", + "title": _("Restart"), + "message": _("Restart the operation, someone has canceled it."), + }, + ) dest_location = self.env["stock.location"].search( - [("name", "=", location_name)] + [("barcode", "=", location_barcode)] ) move_dest_location = move.move_line_ids[0].location_dest_id allowed_locations = self.env["stock.location"].search( @@ -180,28 +189,18 @@ def validate(self, package_level_id, location_name, confirmation=False): "message": _("You cannot place it here"), }, ) - elif ( - dest_location in allowed_locations - and dest_location not in zone_locations - and confirmation - ): - return self._response( - state="confirm_location", - message={ - "message_type": "warning", - "title": _("Confirm"), - "message": _("Are you sure?"), - }, - ) - if move.state == "cancel": - return self._response( - state="start", - message={ - "message_type": "warning", - "title": _("Restart"), - "message": _("Restart the operation, someone has canceled it."), - }, - ) + elif dest_location in allowed_locations and dest_location not in zone_locations: + if confirmation: + move.location_dest_id = dest_location.id + else: + return self._response( + state="confirm_location", + message={ + "message_type": "warning", + "title": _("Confirm"), + "message": _("Are you sure?"), + }, + ) move.move_line_ids[0].location_dest_id = dest_location.id move._action_done() return self._response( @@ -242,7 +241,7 @@ def _validator_cancel(self): def _validator_validate(self): return { "package_level_id": {"coerce": to_int, "required": True, "type": "integer"}, - "location_name": {"type": "string", "nullable": False, "required": True}, + "location_barcode": {"type": "string", "nullable": False, "required": True}, } def _validator_return_validate(self): diff --git a/shopfloor/tests/test_single_pack_putaway.py b/shopfloor/tests/test_single_pack_putaway.py index 82a8e6c6c29..7d2057b370a 100644 --- a/shopfloor/tests/test_single_pack_putaway.py +++ b/shopfloor/tests/test_single_pack_putaway.py @@ -5,7 +5,13 @@ class PutawayCase(CommonCase): def setUp(self, *args, **kwargs): super(PutawayCase, self).setUp(*args, **kwargs) stock_location = self.env.ref("stock.stock_location_stock") - out_location = stock_location.child_ids[1] + out_location = self.env["stock.location"].search( + [ + ("location_id", "=", stock_location.id), + ("barcode", "!=", False), + ("usage", "=", "internal"), + ] + )[0] self.productA = self.env["product.product"].create( {"name": "Product A", "type": "product"} ) @@ -36,19 +42,17 @@ def test_scan_pack(self): response = self.service.dispatch("scan", params=params) package_level = self.env["stock.package_level"].browse(response["data"]["id"]) move_id = package_level.move_line_ids[0].move_id.id + location_dest = self.env["stock.location"].browse( + response["data"]["location_dst"]["id"] + ) params = { "package_level_id": package_level.id, - "location_name": response["data"]["location_dst"]["name"], + "location_barcode": location_dest.barcode, } - location_dest_id = ( - self.env["stock.location"] - .search([("name", "=", params["location_name"])]) - .id - ) new_loc_quant = self.env["stock.quant"].search( [ ("product_id", "=", self.productA.id), - ("location_id", "=", location_dest_id), + ("location_id", "=", location_dest.id), ] ) self.assertFalse(new_loc_quant) @@ -56,7 +60,7 @@ def test_scan_pack(self): new_loc_quant = self.env["stock.quant"].search( [ ("product_id", "=", self.productA.id), - ("location_id", "=", location_dest_id), + ("location_id", "=", location_dest.id), ] ) move = self.env["stock.move"].browse(move_id) From d3be660171a0a7dd36b0339f0bd286988932d667 Mon Sep 17 00:00:00 2001 From: Guewen Baconnier Date: Thu, 23 Jan 2020 14:39:48 +0100 Subject: [PATCH 051/986] Add Actions Components to share business logic Service Components are methods exposed to the REST API. They need to share common methods for their business logic, so enter the Actions Components to hold them. Alternatively, we could have put the methods directly on the Models, but at least here we don't crowd the models with many methods. To create a new Action Component, the bare minimum is: from odoo.addons.component.core import Component class StockPackageLevelAction(Component): _name = "shopfloor.stock.package_level.action" _inherit = "shopfloor.process.action" _apply_on = "stock.package_level" def hello(self): return "world" And to use it from any Service or Action component: self.actions_for("stock.package_level").hello() --- shopfloor/__init__.py | 1 + shopfloor/actions/__init__.py | 21 ++++ shopfloor/actions/base_action.py | 10 ++ shopfloor/actions/stock_package_level.py | 10 ++ shopfloor/services/service.py | 34 +++++- shopfloor/services/single_pack_putaway.py | 122 +++++++++++++++++---- shopfloor/services/single_pack_transfer.py | 12 ++ 7 files changed, 187 insertions(+), 23 deletions(-) create mode 100644 shopfloor/actions/__init__.py create mode 100644 shopfloor/actions/base_action.py create mode 100644 shopfloor/actions/stock_package_level.py diff --git a/shopfloor/__init__.py b/shopfloor/__init__.py index c312a8487c5..6a34e5681f2 100644 --- a/shopfloor/__init__.py +++ b/shopfloor/__init__.py @@ -1,3 +1,4 @@ from . import controllers from . import models +from . import actions from . import services diff --git a/shopfloor/actions/__init__.py b/shopfloor/actions/__init__.py new file mode 100644 index 00000000000..b9b5c7e5511 --- /dev/null +++ b/shopfloor/actions/__init__.py @@ -0,0 +1,21 @@ +""" +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 +* Action Components will generally have an existing Odoo model name + +An Action component can be get from Service or Action Components using +``self.actions_for(model_name)``. + +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 stock_package_level diff --git a/shopfloor/actions/base_action.py b/shopfloor/actions/base_action.py new file mode 100644 index 00000000000..e7d5bbda5b9 --- /dev/null +++ b/shopfloor/actions/base_action.py @@ -0,0 +1,10 @@ +from odoo.addons.component.core import AbstractComponent + + +class ShopFloorProcessAction(AbstractComponent): + _name = "shopfloor.process.action" + _collection = "shopfloor.action" + _usage = "actions" + + def actions_for(self, model_name): + return self.component(usage="actions", model_name=model_name) diff --git a/shopfloor/actions/stock_package_level.py b/shopfloor/actions/stock_package_level.py new file mode 100644 index 00000000000..dd48a12da67 --- /dev/null +++ b/shopfloor/actions/stock_package_level.py @@ -0,0 +1,10 @@ +from odoo.addons.component.core import Component + + +class StockPackageLevelAction(Component): + _name = "shopfloor.stock.package_level.action" + _inherit = "shopfloor.process.action" + _apply_on = "stock.package_level" + + def hello(self): + return "world" diff --git a/shopfloor/services/service.py b/shopfloor/services/service.py index d96822cb43b..f9bfd90518c 100644 --- a/shopfloor/services/service.py +++ b/shopfloor/services/service.py @@ -2,7 +2,8 @@ from odoo.exceptions import MissingError from odoo.osv import expression -from odoo.addons.component.core import AbstractComponent +from odoo.addons.base_rest.controllers.main import _PseudoCollection +from odoo.addons.component.core import AbstractComponent, WorkContext class BaseShopfloorService(AbstractComponent): @@ -11,6 +12,7 @@ class BaseShopfloorService(AbstractComponent): _inherit = "base.rest.service" _name = "base.shopfloor.service" _collection = "shopfloor.service" + _actions_collection_name = "shopfloor.action" _expose_model = None def _get(self, _id): @@ -121,3 +123,33 @@ def _get_openapi_default_parameters(self): ] ) return defaults + + @property + def actions_collection(self): + return _PseudoCollection(self._actions_collection_name, self.env) + + def actions_for(self, model_name): + """Return an Action Component for the model + + 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 custom arguments (such as menu ID/profile ID) + kwargs = { + attr_name: getattr(self.work, attr_name) + for attr_name in self.work._propagate_kwargs + if attr_name + not in ("collection", "model_name", "components_registry", "model_name") + } + work = WorkContext( + model_name=model_name, collection=self.actions_collection, **kwargs + ) + return work.component(usage="actions") + + 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) diff --git a/shopfloor/services/single_pack_putaway.py b/shopfloor/services/single_pack_putaway.py index 52f3994b3ec..cbf6154eb33 100644 --- a/shopfloor/services/single_pack_putaway.py +++ b/shopfloor/services/single_pack_putaway.py @@ -15,7 +15,9 @@ class SinglePackPutaway(Component): def scan(self, barcode): """Scan a pack barcode""" - company = self.env.user.company_id # FIXME add logic to get proper company + company = ( + self.env.user.company_id + ) # FIXME add logic to get proper company # FIXME add logic to get proper warehouse warehouse = self.env["stock.warehouse"].search([])[0] picking_type = ( @@ -52,7 +54,8 @@ def scan(self, barcode): ) if ( existing_operations - and existing_operations[0].picking_id.picking_type_id != picking_type + and existing_operations[0].picking_id.picking_type_id + != picking_type ): return self._response( state="start", @@ -86,14 +89,18 @@ def scan(self, barcode): "id": move.product_id.name, "name": move.product_id.name, }, - "picking": {"id": move.picking_id.id, "name": move.picking_id.name}, + "picking": { + "id": move.picking_id.id, + "name": move.picking_id.name, + }, }, state="confirm_start", message={ "message_type": "warning", "title": _("Already started"), "message": _( - "Operation already running. " "Would you like to take it over ?" + "Operation already running. " + "Would you like to take it over ?" ), }, ) @@ -147,13 +154,29 @@ def scan(self, barcode): "id": move.move_line_ids[0].location_dest_id.id, "name": move.move_line_ids[0].location_dest_id.name, }, - "product": {"id": move.product_id.id, "name": move.product_id.name}, - "picking": {"id": move.picking_id.id, "name": move.picking_id.name}, + "product": { + "id": move.product_id.id, + "name": move.product_id.name, + }, + "picking": { + "id": move.picking_id.id, + "name": move.picking_id.name, + }, }, ) def validate(self, package_level_id, location_barcode, confirmation=False): + """Validate the transfer""" package = self.env["stock.package_level"].browse(package_level_id) + if not package.exists(): + return self._response( + state="start", + message={ + "message_type": "error", + "title": _("Start again"), + "message": _("This operation does not exist anymore."), + }, + ) move = package.move_line_ids[0].move_id if move.state == "cancel": return self._response( @@ -161,7 +184,9 @@ def validate(self, package_level_id, location_barcode, confirmation=False): message={ "message_type": "warning", "title": _("Restart"), - "message": _("Restart the operation, someone has canceled it."), + "message": _( + "Restart the operation, someone has canceled it." + ), }, ) dest_location = self.env["stock.location"].search( @@ -189,7 +214,10 @@ def validate(self, package_level_id, location_barcode, confirmation=False): "message": _("You cannot place it here"), }, ) - elif dest_location in allowed_locations and dest_location not in zone_locations: + elif ( + dest_location in allowed_locations + and dest_location not in zone_locations + ): if confirmation: move.location_dest_id = dest_location.id else: @@ -208,7 +236,9 @@ def validate(self, package_level_id, location_barcode, confirmation=False): message={ "message_type": "info", "title": _("Start"), - "message": _("The pack has been moved, you can scan a new pack."), + "message": _( + "The pack has been moved, you can scan a new pack." + ), }, ) @@ -229,26 +259,42 @@ def cancel(self, package_level_id): message={ "message_type": "info", "title": _("Start"), - "message": _("The move has been canceled, you can scan a new pack."), + "message": _( + "The move has been canceled, you can scan a new pack." + ), }, ) def _validator_cancel(self): return { - "package_level_id": {"coerce": to_int, "required": True, "type": "integer"} + "package_level_id": { + "coerce": to_int, + "required": True, + "type": "integer", + } } def _validator_validate(self): return { - "package_level_id": {"coerce": to_int, "required": True, "type": "integer"}, - "location_barcode": {"type": "string", "nullable": False, "required": True}, + "package_level_id": { + "coerce": to_int, + "required": True, + "type": "integer", + }, + "location_barcode": { + "type": "string", + "nullable": False, + "required": True, + }, } def _validator_return_validate(self): return self._response_schema() def _validator_scan(self): - return {"barcode": {"type": "string", "nullable": False, "required": True}} + return { + "barcode": {"type": "string", "nullable": False, "required": True} + } def _validator_return_scan(self): return self._response_schema( @@ -257,29 +303,61 @@ def _validator_return_scan(self): "location_src": { "type": "dict", "schema": { - "id": {"coerce": to_int, "required": True, "type": "integer"}, - "name": {"type": "string", "nullable": False, "required": True}, + "id": { + "coerce": to_int, + "required": True, + "type": "integer", + }, + "name": { + "type": "string", + "nullable": False, + "required": True, + }, }, }, "location_dst": { "type": "dict", "schema": { - "id": {"coerce": to_int, "required": True, "type": "integer"}, - "name": {"type": "string", "nullable": False, "required": True}, + "id": { + "coerce": to_int, + "required": True, + "type": "integer", + }, + "name": { + "type": "string", + "nullable": False, + "required": True, + }, }, }, "product": { "type": "dict", "schema": { - "id": {"coerce": to_int, "required": True, "type": "integer"}, - "name": {"type": "string", "nullable": False, "required": True}, + "id": { + "coerce": to_int, + "required": True, + "type": "integer", + }, + "name": { + "type": "string", + "nullable": False, + "required": True, + }, }, }, "picking": { "type": "dict", "schema": { - "id": {"coerce": to_int, "required": True, "type": "integer"}, - "name": {"type": "string", "nullable": False, "required": True}, + "id": { + "coerce": to_int, + "required": True, + "type": "integer", + }, + "name": { + "type": "string", + "nullable": False, + "required": True, + }, }, }, } diff --git a/shopfloor/services/single_pack_transfer.py b/shopfloor/services/single_pack_transfer.py index a22659a1cb7..e29fdbf56cc 100644 --- a/shopfloor/services/single_pack_transfer.py +++ b/shopfloor/services/single_pack_transfer.py @@ -1,3 +1,4 @@ +from odoo.addons.base_rest.components.service import to_int from odoo.addons.component.core import Component @@ -8,3 +9,14 @@ class SinglePackTransfer(Component): _name = "shopfloor.single.pack.transfer" _usage = "single_pack_transfer" _description = __doc__ + + def validate(self, package_level_id, location_name, confirmation=False): + """Validate the transfer""" + return self.actions_for("stock.package_level").hello() + + def _validator_validate(self): + return { + "package_level_id": {"coerce": to_int, "required": True, "type": "integer"}, + "location_name": {"type": "string", "nullable": False, "required": True}, + "confirmation": {"type": "boolean", "required": False}, + } From 6cec401db86280723124600c8a6e479fe61e3f1c Mon Sep 17 00:00:00 2001 From: Guewen Baconnier Date: Thu, 23 Jan 2020 15:11:17 +0100 Subject: [PATCH 052/986] Add /app/load_config endpoint (load menus+profiles at once) --- shopfloor/services/__init__.py | 1 + shopfloor/services/app.py | 42 ++++++++++++++++++++++++++ shopfloor/services/menu.py | 41 ++++++++++--------------- shopfloor/services/profile.py | 55 ++++++++++++++-------------------- 4 files changed, 81 insertions(+), 58 deletions(-) create mode 100644 shopfloor/services/app.py diff --git a/shopfloor/services/__init__.py b/shopfloor/services/__init__.py index bb82a1df2e5..5eb564a6a69 100644 --- a/shopfloor/services/__init__.py +++ b/shopfloor/services/__init__.py @@ -1,4 +1,5 @@ from . import service +from . import app from . import profile from . import menu from . import pack diff --git a/shopfloor/services/app.py b/shopfloor/services/app.py new file mode 100644 index 00000000000..ed1bbf4880e --- /dev/null +++ b/shopfloor/services/app.py @@ -0,0 +1,42 @@ +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__ + + def user_config(self): + menus = self.component("menu")._search() + profiles = self.component("profile")._search() + return self._response(data={"menus": menus, "profiles": profiles}) + + def _validator_user_config(self): + return {} + + def _validator_return_user_config(self): + menu_service = self.component("menu") + profile_service = self.component("profile") + return self._response_schema( + { + "menus": { + "type": "list", + "required": True, + "schema": { + "type": "dict", + "schema": menu_service._record_return_schema, + }, + }, + "profiles": { + "type": "list", + "required": True, + "schema": { + "type": "dict", + "schema": profile_service._record_return_schema, + }, + }, + } + ) diff --git a/shopfloor/services/menu.py b/shopfloor/services/menu.py index 33ea065b67d..96847ce7c43 100644 --- a/shopfloor/services/menu.py +++ b/shopfloor/services/menu.py @@ -33,15 +33,17 @@ def _get_base_search_domain(self): ] ) - def search(self, name_fragment=None): - """List available menu entries for current user""" + 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 self._response( - data={"size": len(records), "records": self._to_json(records)} - ) + return self._to_json(records) + + def search(self, name_fragment=None): + """List available menu entries for current user""" + json_records = self._search(name_fragment=name_fragment) + return self._response(data={"size": len(json_records), "records": json_records}) def _validator_search(self): return { @@ -55,29 +57,18 @@ def _validator_return_search(self): "records": { "type": "list", "required": True, - "schema": { - "type": "dict", - "schema": { - "id": { - "coerce": to_int, - "required": True, - "type": "integer", - }, - "name": { - "type": "string", - "nullable": False, - "required": True, - }, - "process": { - "type": "string", - "nullable": False, - "required": True, - }, - }, - }, + "schema": {"type": "dict", "schema": self._record_return_schema}, }, } ) + @property + def _record_return_schema(self): + return { + "id": {"coerce": to_int, "required": True, "type": "integer"}, + "name": {"type": "string", "nullable": False, "required": True}, + "process": {"type": "string", "nullable": False, "required": True}, + } + def _convert_one_record(self, record): return {"id": record.id, "name": record.name, "process": record.process_code} diff --git a/shopfloor/services/profile.py b/shopfloor/services/profile.py index 86e2ed1175a..9f59ef0706f 100644 --- a/shopfloor/services/profile.py +++ b/shopfloor/services/profile.py @@ -25,14 +25,18 @@ class ShopfloorProfile(Component): _expose_model = "shopfloor.profile" _description = __doc__ - def search(self, name_fragment=None): - """List available profiles for current user""" + 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 self._to_json(records) + + def search(self, name_fragment=None): + """List available profiles for current user""" + json_records = self._search(name_fragment=name_fragment) return self._response( - data={"size": len(records), "records": self._to_json(records)} + data={"size": len(json_records), "records": self._to_json(json_records)} ) def _get_base_search_domain(self): @@ -65,40 +69,25 @@ def _validator_return_search(self): "size": {"coerce": to_int, "required": True, "type": "integer"}, "records": { "type": "list", - "schema": { - "type": "dict", - "schema": { - "id": { - "coerce": to_int, - "required": True, - "type": "integer", - }, - "name": { - "type": "string", - "nullable": False, - "required": True, - }, - "warehouse": { - "type": "dict", - "schema": { - "id": { - "coerce": to_int, - "required": True, - "type": "integer", - }, - "name": { - "type": "string", - "nullable": False, - "required": True, - }, - }, - }, - }, - }, + "schema": {"type": "dict", "schema": self._record_return_schema}, }, } ) + @property + def _record_return_schema(self): + return { + "id": {"coerce": to_int, "required": True, "type": "integer"}, + "name": {"type": "string", "nullable": False, "required": True}, + "warehouse": { + "type": "dict", + "schema": { + "id": {"coerce": to_int, "required": True, "type": "integer"}, + "name": {"type": "string", "nullable": False, "required": True}, + }, + }, + } + def _convert_one_record(self, record): return { "id": record.id, From a1f883fe8d15fd7571ac6e8bc99936f2cd668a8a Mon Sep 17 00:00:00 2001 From: Guewen Baconnier Date: Thu, 23 Jan 2020 15:14:20 +0100 Subject: [PATCH 053/986] pre-commit run -a --- shopfloor/services/single_pack_putaway.py | 112 +++++----------------- 1 file changed, 22 insertions(+), 90 deletions(-) diff --git a/shopfloor/services/single_pack_putaway.py b/shopfloor/services/single_pack_putaway.py index cbf6154eb33..b22e7797486 100644 --- a/shopfloor/services/single_pack_putaway.py +++ b/shopfloor/services/single_pack_putaway.py @@ -15,9 +15,7 @@ class SinglePackPutaway(Component): def scan(self, barcode): """Scan a pack barcode""" - company = ( - self.env.user.company_id - ) # FIXME add logic to get proper company + company = self.env.user.company_id # FIXME add logic to get proper company # FIXME add logic to get proper warehouse warehouse = self.env["stock.warehouse"].search([])[0] picking_type = ( @@ -54,8 +52,7 @@ def scan(self, barcode): ) if ( existing_operations - and existing_operations[0].picking_id.picking_type_id - != picking_type + and existing_operations[0].picking_id.picking_type_id != picking_type ): return self._response( state="start", @@ -89,18 +86,14 @@ def scan(self, barcode): "id": move.product_id.name, "name": move.product_id.name, }, - "picking": { - "id": move.picking_id.id, - "name": move.picking_id.name, - }, + "picking": {"id": move.picking_id.id, "name": move.picking_id.name}, }, state="confirm_start", message={ "message_type": "warning", "title": _("Already started"), "message": _( - "Operation already running. " - "Would you like to take it over ?" + "Operation already running. " "Would you like to take it over ?" ), }, ) @@ -154,14 +147,8 @@ def scan(self, barcode): "id": move.move_line_ids[0].location_dest_id.id, "name": move.move_line_ids[0].location_dest_id.name, }, - "product": { - "id": move.product_id.id, - "name": move.product_id.name, - }, - "picking": { - "id": move.picking_id.id, - "name": move.picking_id.name, - }, + "product": {"id": move.product_id.id, "name": move.product_id.name}, + "picking": {"id": move.picking_id.id, "name": move.picking_id.name}, }, ) @@ -184,9 +171,7 @@ def validate(self, package_level_id, location_barcode, confirmation=False): message={ "message_type": "warning", "title": _("Restart"), - "message": _( - "Restart the operation, someone has canceled it." - ), + "message": _("Restart the operation, someone has canceled it."), }, ) dest_location = self.env["stock.location"].search( @@ -214,10 +199,7 @@ def validate(self, package_level_id, location_barcode, confirmation=False): "message": _("You cannot place it here"), }, ) - elif ( - dest_location in allowed_locations - and dest_location not in zone_locations - ): + elif dest_location in allowed_locations and dest_location not in zone_locations: if confirmation: move.location_dest_id = dest_location.id else: @@ -236,9 +218,7 @@ def validate(self, package_level_id, location_barcode, confirmation=False): message={ "message_type": "info", "title": _("Start"), - "message": _( - "The pack has been moved, you can scan a new pack." - ), + "message": _("The pack has been moved, you can scan a new pack."), }, ) @@ -259,42 +239,26 @@ def cancel(self, package_level_id): message={ "message_type": "info", "title": _("Start"), - "message": _( - "The move has been canceled, you can scan a new pack." - ), + "message": _("The move has been canceled, you can scan a new pack."), }, ) def _validator_cancel(self): return { - "package_level_id": { - "coerce": to_int, - "required": True, - "type": "integer", - } + "package_level_id": {"coerce": to_int, "required": True, "type": "integer"} } def _validator_validate(self): return { - "package_level_id": { - "coerce": to_int, - "required": True, - "type": "integer", - }, - "location_barcode": { - "type": "string", - "nullable": False, - "required": True, - }, + "package_level_id": {"coerce": to_int, "required": True, "type": "integer"}, + "location_barcode": {"type": "string", "nullable": False, "required": True}, } def _validator_return_validate(self): return self._response_schema() def _validator_scan(self): - return { - "barcode": {"type": "string", "nullable": False, "required": True} - } + return {"barcode": {"type": "string", "nullable": False, "required": True}} def _validator_return_scan(self): return self._response_schema( @@ -303,61 +267,29 @@ def _validator_return_scan(self): "location_src": { "type": "dict", "schema": { - "id": { - "coerce": to_int, - "required": True, - "type": "integer", - }, - "name": { - "type": "string", - "nullable": False, - "required": True, - }, + "id": {"coerce": to_int, "required": True, "type": "integer"}, + "name": {"type": "string", "nullable": False, "required": True}, }, }, "location_dst": { "type": "dict", "schema": { - "id": { - "coerce": to_int, - "required": True, - "type": "integer", - }, - "name": { - "type": "string", - "nullable": False, - "required": True, - }, + "id": {"coerce": to_int, "required": True, "type": "integer"}, + "name": {"type": "string", "nullable": False, "required": True}, }, }, "product": { "type": "dict", "schema": { - "id": { - "coerce": to_int, - "required": True, - "type": "integer", - }, - "name": { - "type": "string", - "nullable": False, - "required": True, - }, + "id": {"coerce": to_int, "required": True, "type": "integer"}, + "name": {"type": "string", "nullable": False, "required": True}, }, }, "picking": { "type": "dict", "schema": { - "id": { - "coerce": to_int, - "required": True, - "type": "integer", - }, - "name": { - "type": "string", - "nullable": False, - "required": True, - }, + "id": {"coerce": to_int, "required": True, "type": "integer"}, + "name": {"type": "string", "nullable": False, "required": True}, }, }, } From 4a66bcc67c6bc570d1483e2b714262044660f586 Mon Sep 17 00:00:00 2001 From: Benoit Date: Thu, 23 Jan 2020 15:19:35 +0100 Subject: [PATCH 054/986] get picking type properly from menu and profile --- shopfloor/services/service.py | 9 +++++++++ shopfloor/services/single_pack_putaway.py | 21 +++++++++++++-------- shopfloor/tests/test_single_pack_putaway.py | 5 ++++- 3 files changed, 26 insertions(+), 9 deletions(-) diff --git a/shopfloor/services/service.py b/shopfloor/services/service.py index f9bfd90518c..7b47800d1b3 100644 --- a/shopfloor/services/service.py +++ b/shopfloor/services/service.py @@ -15,6 +15,15 @@ class BaseShopfloorService(AbstractComponent): _actions_collection_name = "shopfloor.action" _expose_model = None + @property + def picking_types(self): + """ + Get the current picking type based on the menu and the warehouse of the profile. + """ + return self.work.menu.process_id.picking_type_ids.filtered( + lambda p: p.warehouse_id == self.work.profile.warehouse_id + ) + def _get(self, _id): domain = expression.normalize_domain(self._get_base_search_domain()) domain = expression.AND([domain, [("id", "=", _id)]]) diff --git a/shopfloor/services/single_pack_putaway.py b/shopfloor/services/single_pack_putaway.py index b22e7797486..38dbea6ce09 100644 --- a/shopfloor/services/single_pack_putaway.py +++ b/shopfloor/services/single_pack_putaway.py @@ -14,14 +14,19 @@ class SinglePackPutaway(Component): def scan(self, barcode): """Scan a pack barcode""" - + picking_type = self.picking_types + if len(picking_type) > 1: + return self._response( + state="start", + message={ + "message_type": "error", + "title": _("Configuration error"), + "message": _( + "Several picking types found for this menu and profile" + ), + }, + ) company = self.env.user.company_id # FIXME add logic to get proper company - # FIXME add logic to get proper warehouse - warehouse = self.env["stock.warehouse"].search([])[0] - picking_type = ( - warehouse.int_type_id - ) # FIXME add logic to get picking type properly - # TODO define on what we search (pack name, pack barcode ...) pack = self.env["stock.quant.package"].search([("name", "=", barcode)]) if not pack: @@ -93,7 +98,7 @@ def scan(self, barcode): "message_type": "warning", "title": _("Already started"), "message": _( - "Operation already running. " "Would you like to take it over ?" + "Operation already running. Would you like to take it over ?" ), }, ) diff --git a/shopfloor/tests/test_single_pack_putaway.py b/shopfloor/tests/test_single_pack_putaway.py index 7d2057b370a..7e72d52939d 100644 --- a/shopfloor/tests/test_single_pack_putaway.py +++ b/shopfloor/tests/test_single_pack_putaway.py @@ -33,7 +33,10 @@ def setUp(self, *args, **kwargs): "location_out_id": out_location.id, } ) - with self.work_on_services() as work: + menu = self.env.ref("shopfloor.shopfloor_menu_put_away_reach_truck") + profile = self.env.ref("shopfloor.shopfloor_profile_shelf_1_demo") + profile.warehouse_id.int_type_id.process_id = menu.process_id.id + with self.work_on_services(menu=menu, profile=profile) as work: self.service = work.component(usage="single_pack_putaway") def test_scan_pack(self): From 9c90a2ed907170eb399d6226431b1eaa428c12f1 Mon Sep 17 00:00:00 2001 From: hparfr Date: Tue, 21 Jan 2020 09:21:52 +0100 Subject: [PATCH 055/986] Initial commit --- mobile_app/__init__.py | 0 mobile_app/__manifest__.py | 14 + mobile_app/static/src/wms/.gitignore | 21 + mobile_app/static/src/wms/index.html | 25 + mobile_app/static/src/wms/src/assets/logo.png | Bin 0 -> 6849 bytes .../wms/src/components/searchbar/searchbar.js | 29 + mobile_app/static/src/wms/src/main.js | 14 + mobile_app/static/src/wms/src/vendor/vue.js | 11965 ++++++++++++++++ 8 files changed, 12068 insertions(+) create mode 100644 mobile_app/__init__.py create mode 100644 mobile_app/__manifest__.py create mode 100644 mobile_app/static/src/wms/.gitignore create mode 100644 mobile_app/static/src/wms/index.html create mode 100644 mobile_app/static/src/wms/src/assets/logo.png create mode 100644 mobile_app/static/src/wms/src/components/searchbar/searchbar.js create mode 100644 mobile_app/static/src/wms/src/main.js create mode 100644 mobile_app/static/src/wms/src/vendor/vue.js diff --git a/mobile_app/__init__.py b/mobile_app/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/mobile_app/__manifest__.py b/mobile_app/__manifest__.py new file mode 100644 index 00000000000..b1124c51a13 --- /dev/null +++ b/mobile_app/__manifest__.py @@ -0,0 +1,14 @@ +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). + +{ + "name": "Mobbile app for WMS", + "summary": ".", + "version": "13.0.1.0.0", + "development_status": "Alpha", + "depends": ["stock"], + "author": "Akretion, Odoo Community Association (OCA)", + "website": "https://github.com/OCA/stock-logistics-warehouse", + "category": "Warehouse Management", + "license": "AGPL-3", + "installable": True, +} diff --git a/mobile_app/static/src/wms/.gitignore b/mobile_app/static/src/wms/.gitignore new file mode 100644 index 00000000000..a0dddc6fb8c --- /dev/null +++ b/mobile_app/static/src/wms/.gitignore @@ -0,0 +1,21 @@ +.DS_Store +node_modules +/dist + +# local env files +.env.local +.env.*.local + +# Log files +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# Editor directories and files +.idea +.vscode +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? diff --git a/mobile_app/static/src/wms/index.html b/mobile_app/static/src/wms/index.html new file mode 100644 index 00000000000..5297071270b --- /dev/null +++ b/mobile_app/static/src/wms/index.html @@ -0,0 +1,25 @@ + + + + + + + + + + + WMS + + + +
+ ici lasearch +
+ {{ txt }} +
+
+ + + diff --git a/mobile_app/static/src/wms/src/assets/logo.png b/mobile_app/static/src/wms/src/assets/logo.png new file mode 100644 index 0000000000000000000000000000000000000000..f3d2503fc2a44b5053b0837ebea6e87a2d339a43 GIT binary patch literal 6849 zcmaKRcUV(fvo}bjDT-7nLI_nlK}sT_69H+`qzVWDA|yaU?}j417wLi^B1KB1SLsC& zL0ag7$U(XW5YR7p&Ux?sP$d4lvMt8C^+TcQu4F zQqv!UF!I+kw)c0jhd6+g6oCr9P?7)?!qX1ui*iL{p}sKCAGuJ{{W)0z1pLF|=>h}& zt(2Lr0Z`2ig8<5i%Zk}cO5Fm=LByqGWaS`oqChZdEFmc`0hSb#gg|Aap^{+WKOYcj zHjINK)KDG%&s?Mt4CL(T=?;~U@bU2x_mLKN!#GJuK_CzbNw5SMEJorG!}_5;?R>@1 zSl)jns3WlU7^J%=(hUtfmuUCU&C3%8B5C^f5>W2Cy8jW3#{Od{lF1}|?c61##3dzA zsPlFG;l_FzBK}8>|H_Ru_H#!_7$UH4UKo3lKOA}g1(R&|e@}GINYVzX?q=_WLZCgh z)L|eJMce`D0EIwgRaNETDsr+?vQknSGAi=7H00r`QnI%oQnFxm`G2umXso9l+8*&Q z7WqF|$p49js$mdzo^BXpH#gURy=UO;=IMrYc5?@+sR4y_?d*~0^YP7d+y0{}0)zBM zIKVM(DBvICK#~7N0a+PY6)7;u=dutmNqK3AlsrUU9U`d;msiucB_|8|2kY=(7XA;G zwDA8AR)VCA#JOkxm#6oHNS^YVuOU;8p$N)2{`;oF|rQ?B~K$%rHDxXs+_G zF5|-uqHZvSzq}L;5Kcy_P+x0${33}Ofb6+TX&=y;;PkEOpz%+_bCw_{<&~ zeLV|!bP%l1qxywfVr9Z9JI+++EO^x>ZuCK);=$VIG1`kxK8F2M8AdC$iOe3cj1fo(ce4l-9 z7*zKy3={MixvUk=enQE;ED~7tv%qh&3lR<0m??@w{ILF|e#QOyPkFYK!&Up7xWNtL zOW%1QMC<3o;G9_S1;NkPB6bqbCOjeztEc6TsBM<(q9((JKiH{01+Ud=uw9B@{;(JJ z-DxI2*{pMq`q1RQc;V8@gYAY44Z!%#W~M9pRxI(R?SJ7sy7em=Z5DbuDlr@*q|25V)($-f}9c#?D%dU^RS<(wz?{P zFFHtCab*!rl(~j@0(Nadvwg8q|4!}L^>d?0al6}Rrv9$0M#^&@zjbfJy_n!%mVHK4 z6pLRIQ^Uq~dnyy$`ay51Us6WaP%&O;@49m&{G3z7xV3dLtt1VTOMYl3UW~Rm{Eq4m zF?Zl_v;?7EFx1_+#WFUXxcK78IV)FO>42@cm@}2I%pVbZqQ}3;p;sDIm&knay03a^ zn$5}Q$G!@fTwD$e(x-~aWP0h+4NRz$KlnO_H2c< z(XX#lPuW_%H#Q+c&(nRyX1-IadKR-%$4FYC0fsCmL9ky3 zKpxyjd^JFR+vg2!=HWf}2Z?@Td`0EG`kU?{8zKrvtsm)|7>pPk9nu@2^z96aU2<#` z2QhvH5w&V;wER?mopu+nqu*n8p~(%QkwSs&*0eJwa zMXR05`OSFpfyRb!Y_+H@O%Y z0=K^y6B8Gcbl?SA)qMP3Z+=C(?8zL@=74R=EVnE?vY!1BQy2@q*RUgRx4yJ$k}MnL zs!?74QciNb-LcG*&o<9=DSL>1n}ZNd)w1z3-0Pd^4ED1{qd=9|!!N?xnXjM!EuylY z5=!H>&hSofh8V?Jofyd!h`xDI1fYAuV(sZwwN~{$a}MX^=+0TH*SFp$vyxmUv7C*W zv^3Gl0+eTFgBi3FVD;$nhcp)ka*4gSskYIqQ&+M}xP9yLAkWzBI^I%zR^l1e?bW_6 zIn{mo{dD=)9@V?s^fa55jh78rP*Ze<3`tRCN4*mpO$@7a^*2B*7N_|A(Ve2VB|)_o z$=#_=aBkhe(ifX}MLT()@5?OV+~7cXC3r!%{QJxriXo9I%*3q4KT4Xxzyd{ z9;_%=W%q!Vw$Z7F3lUnY+1HZ*lO;4;VR2+i4+D(m#01OYq|L_fbnT;KN<^dkkCwtd zF7n+O7KvAw8c`JUh6LmeIrk4`F3o|AagKSMK3))_5Cv~y2Bb2!Ibg9BO7Vkz?pAYX zoI=B}+$R22&IL`NCYUYjrdhwjnMx_v=-Qcx-jmtN>!Zqf|n1^SWrHy zK|MwJ?Z#^>)rfT5YSY{qjZ&`Fjd;^vv&gF-Yj6$9-Dy$<6zeP4s+78gS2|t%Z309b z0^fp~ue_}i`U9j!<|qF92_3oB09NqgAoehQ`)<)dSfKoJl_A6Ec#*Mx9Cpd-p#$Ez z={AM*r-bQs6*z$!*VA4|QE7bf@-4vb?Q+pPKLkY2{yKsw{&udv_2v8{Dbd zm~8VAv!G~s)`O3|Q6vFUV%8%+?ZSVUa(;fhPNg#vab@J*9XE4#D%)$UU-T5`fwjz! z6&gA^`OGu6aUk{l*h9eB?opVdrHK>Q@U>&JQ_2pR%}TyOXGq_6s56_`U(WoOaAb+K zXQr#6H}>a-GYs9^bGP2Y&hSP5gEtW+GVC4=wy0wQk=~%CSXj=GH6q z-T#s!BV`xZVxm{~jr_ezYRpqqIcXC=Oq`b{lu`Rt(IYr4B91hhVC?yg{ol4WUr3v9 zOAk2LG>CIECZ-WIs0$N}F#eoIUEtZudc7DPYIjzGqDLWk_A4#(LgacooD z2K4IWs@N`Bddm-{%oy}!k0^i6Yh)uJ1S*90>|bm3TOZxcV|ywHUb(+CeX-o1|LTZM zwU>dY3R&U)T(}5#Neh?-CWT~@{6Ke@sI)uSuzoah8COy)w)B)aslJmp`WUcjdia-0 zl2Y}&L~XfA`uYQboAJ1;J{XLhYjH){cObH3FDva+^8ioOQy%Z=xyjGLmWMrzfFoH; zEi3AG`_v+%)&lDJE;iJWJDI@-X9K5O)LD~j*PBe(wu+|%ar~C+LK1+-+lK=t# z+Xc+J7qp~5q=B~rD!x78)?1+KUIbYr^5rcl&tB-cTtj+e%{gpZZ4G~6r15+d|J(ky zjg@@UzMW0k9@S#W(1H{u;Nq(7llJbq;;4t$awM;l&(2s+$l!Ay9^Ge|34CVhr7|BG z?dAR83smef^frq9V(OH+a+ki#q&-7TkWfFM=5bsGbU(8mC;>QTCWL5ydz9s6k@?+V zcjiH`VI=59P-(-DWXZ~5DH>B^_H~;4$)KUhnmGo*G!Tq8^LjfUDO)lASN*=#AY_yS zqW9UX(VOCO&p@kHdUUgsBO0KhXxn1sprK5h8}+>IhX(nSXZKwlNsjk^M|RAaqmCZB zHBolOHYBas@&{PT=R+?d8pZu zUHfyucQ`(umXSW7o?HQ3H21M`ZJal+%*)SH1B1j6rxTlG3hx1IGJN^M7{$j(9V;MZ zRKybgVuxKo#XVM+?*yTy{W+XHaU5Jbt-UG33x{u(N-2wmw;zzPH&4DE103HV@ER86 z|FZEmQb|&1s5#`$4!Cm}&`^{(4V}OP$bk`}v6q6rm;P!H)W|2i^e{7lTk2W@jo_9q z*aw|U7#+g59Fv(5qI`#O-qPj#@_P>PC#I(GSp3DLv7x-dmYK=C7lPF8a)bxb=@)B1 zUZ`EqpXV2dR}B&r`uM}N(TS99ZT0UB%IN|0H%DcVO#T%L_chrgn#m6%x4KE*IMfjX zJ%4veCEqbXZ`H`F_+fELMC@wuy_ch%t*+Z+1I}wN#C+dRrf2X{1C8=yZ_%Pt6wL_~ zZ2NN-hXOT4P4n$QFO7yYHS-4wF1Xfr-meG9Pn;uK51?hfel`d38k{W)F*|gJLT2#T z<~>spMu4(mul-8Q3*pf=N4DcI)zzjqAgbE2eOT7~&f1W3VsdD44Ffe;3mJp-V@8UC z)|qnPc12o~$X-+U@L_lWqv-RtvB~%hLF($%Ew5w>^NR82qC_0FB z)=hP1-OEx?lLi#jnLzH}a;Nvr@JDO-zQWd}#k^an$Kwml;MrD&)sC5b`s0ZkVyPkb zt}-jOq^%_9>YZe7Y}PhW{a)c39G`kg(P4@kxjcYfgB4XOOcmezdUI7j-!gs7oAo2o zx(Ph{G+YZ`a%~kzK!HTAA5NXE-7vOFRr5oqY$rH>WI6SFvWmahFav!CfRMM3%8J&c z*p+%|-fNS_@QrFr(at!JY9jCg9F-%5{nb5Bo~z@Y9m&SHYV`49GAJjA5h~h4(G!Se zZmK{Bo7ivCfvl}@A-ptkFGcWXAzj3xfl{evi-OG(TaCn1FAHxRc{}B|x+Ua1D=I6M z!C^ZIvK6aS_c&(=OQDZfm>O`Nxsw{ta&yiYPA~@e#c%N>>#rq)k6Aru-qD4(D^v)y z*>Rs;YUbD1S8^D(ps6Jbj0K3wJw>L4m)0e(6Pee3Y?gy9i0^bZO?$*sv+xKV?WBlh zAp*;v6w!a8;A7sLB*g-^<$Z4L7|5jXxxP1}hQZ<55f9<^KJ>^mKlWSGaLcO0=$jem zWyZkRwe~u{{tU63DlCaS9$Y4CP4f?+wwa(&1ou)b>72ydrFvm`Rj-0`kBJgK@nd(*Eh!(NC{F-@=FnF&Y!q`7){YsLLHf0_B6aHc# z>WIuHTyJwIH{BJ4)2RtEauC7Yq7Cytc|S)4^*t8Va3HR zg=~sN^tp9re@w=GTx$;zOWMjcg-7X3Wk^N$n;&Kf1RgVG2}2L-(0o)54C509C&77i zrjSi{X*WV=%C17((N^6R4Ya*4#6s_L99RtQ>m(%#nQ#wrRC8Y%yxkH;d!MdY+Tw@r zjpSnK`;C-U{ATcgaxoEpP0Gf+tx);buOMlK=01D|J+ROu37qc*rD(w`#O=3*O*w9?biwNoq3WN1`&Wp8TvKj3C z3HR9ssH7a&Vr<6waJrU zdLg!ieYz%U^bmpn%;(V%%ugMk92&?_XX1K@mwnVSE6!&%P%Wdi7_h`CpScvspMx?N zQUR>oadnG17#hNc$pkTp+9lW+MBKHRZ~74XWUryd)4yd zj98$%XmIL4(9OnoeO5Fnyn&fpQ9b0h4e6EHHw*l68j;>(ya`g^S&y2{O8U>1*>4zR zq*WSI_2o$CHQ?x0!wl9bpx|Cm2+kFMR)oMud1%n2=qn5nE&t@Fgr#=Zv2?}wtEz^T z9rrj=?IH*qI5{G@Rn&}^Z{+TW}mQeb9=8b<_a`&Cm#n%n~ zU47MvCBsdXFB1+adOO)03+nczfWa#vwk#r{o{dF)QWya9v2nv43Zp3%Ps}($lA02*_g25t;|T{A5snSY?3A zrRQ~(Ygh_ebltHo1VCbJb*eOAr;4cnlXLvI>*$-#AVsGg6B1r7@;g^L zFlJ_th0vxO7;-opU@WAFe;<}?!2q?RBrFK5U{*ai@NLKZ^};Ul}beukveh?TQn;$%9=R+DX07m82gP$=}Uo_%&ngV`}Hyv8g{u z3SWzTGV|cwQuFIs7ZDOqO_fGf8Q`8MwL}eUp>q?4eqCmOTcwQuXtQckPy|4F1on8l zP*h>d+cH#XQf|+6c|S{7SF(Lg>bR~l(0uY?O{OEVlaxa5@e%T&xju=o1`=OD#qc16 zSvyH*my(dcp6~VqR;o(#@m44Lug@~_qw+HA=mS#Z^4reBy8iV?H~I;{LQWk3aKK8$bLRyt$g?- { //for async simulation + var result = lookup[this.entered]; + console.log('odoo has answered', result); + this.$emit('found', result); //talk to parent + this.reset(); + }); + }, + reset: function () { + this.entered = ''; + } + }, + + template: '
' +}) diff --git a/mobile_app/static/src/wms/src/main.js b/mobile_app/static/src/wms/src/main.js new file mode 100644 index 00000000000..5b2903d3769 --- /dev/null +++ b/mobile_app/static/src/wms/src/main.js @@ -0,0 +1,14 @@ +//import searchbar from 'components/searchbar/searchbar.js' + +var vue = new Vue({ + el: '#app', + data: { + 'txt': 'search something' + }, + methods: { + showFound: function(fff) { + console.log('found: ', fff); + this.txt = fff; + } + } +}) diff --git a/mobile_app/static/src/wms/src/vendor/vue.js b/mobile_app/static/src/wms/src/vendor/vue.js new file mode 100644 index 00000000000..e22cf13003c --- /dev/null +++ b/mobile_app/static/src/wms/src/vendor/vue.js @@ -0,0 +1,11965 @@ +/*! + * Vue.js v2.6.11 + * (c) 2014-2019 Evan You + * Released under the MIT License. + */ +(function (global, factory) { + typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() : + typeof define === 'function' && define.amd ? define(factory) : + (global = global || self, global.Vue = factory()); +}(this, function () { 'use strict'; + + /* */ + + var emptyObject = Object.freeze({}); + + // These helpers produce better VM code in JS engines due to their + // explicitness and function inlining. + function isUndef (v) { + return v === undefined || v === null + } + + function isDef (v) { + return v !== undefined && v !== null + } + + function isTrue (v) { + return v === true + } + + function isFalse (v) { + return v === false + } + + /** + * Check if value is primitive. + */ + function isPrimitive (value) { + return ( + typeof value === 'string' || + typeof value === 'number' || + // $flow-disable-line + typeof value === 'symbol' || + typeof value === 'boolean' + ) + } + + /** + * Quick object check - this is primarily used to tell + * Objects from primitive values when we know the value + * is a JSON-compliant type. + */ + function isObject (obj) { + return obj !== null && typeof obj === 'object' + } + + /** + * Get the raw type string of a value, e.g., [object Object]. + */ + var _toString = Object.prototype.toString; + + function toRawType (value) { + return _toString.call(value).slice(8, -1) + } + + /** + * Strict object type check. Only returns true + * for plain JavaScript objects. + */ + function isPlainObject (obj) { + return _toString.call(obj) === '[object Object]' + } + + function isRegExp (v) { + return _toString.call(v) === '[object RegExp]' + } + + /** + * Check if val is a valid array index. + */ + function isValidArrayIndex (val) { + var n = parseFloat(String(val)); + return n >= 0 && Math.floor(n) === n && isFinite(val) + } + + function isPromise (val) { + return ( + isDef(val) && + typeof val.then === 'function' && + typeof val.catch === 'function' + ) + } + + /** + * Convert a value to a string that is actually rendered. + */ + function toString (val) { + return val == null + ? '' + : Array.isArray(val) || (isPlainObject(val) && val.toString === _toString) + ? JSON.stringify(val, null, 2) + : String(val) + } + + /** + * Convert an input value to a number for persistence. + * If the conversion fails, return original string. + */ + function toNumber (val) { + var n = parseFloat(val); + return isNaN(n) ? val : n + } + + /** + * Make a map and return a function for checking if a key + * is in that map. + */ + function makeMap ( + str, + expectsLowerCase + ) { + var map = Object.create(null); + var list = str.split(','); + for (var i = 0; i < list.length; i++) { + map[list[i]] = true; + } + return expectsLowerCase + ? function (val) { return map[val.toLowerCase()]; } + : function (val) { return map[val]; } + } + + /** + * Check if a tag is a built-in tag. + */ + var isBuiltInTag = makeMap('slot,component', true); + + /** + * Check if an attribute is a reserved attribute. + */ + var isReservedAttribute = makeMap('key,ref,slot,slot-scope,is'); + + /** + * Remove an item from an array. + */ + function remove (arr, item) { + if (arr.length) { + var index = arr.indexOf(item); + if (index > -1) { + return arr.splice(index, 1) + } + } + } + + /** + * Check whether an object has the property. + */ + var hasOwnProperty = Object.prototype.hasOwnProperty; + function hasOwn (obj, key) { + return hasOwnProperty.call(obj, key) + } + + /** + * Create a cached version of a pure function. + */ + function cached (fn) { + var cache = Object.create(null); + return (function cachedFn (str) { + var hit = cache[str]; + return hit || (cache[str] = fn(str)) + }) + } + + /** + * Camelize a hyphen-delimited string. + */ + var camelizeRE = /-(\w)/g; + var camelize = cached(function (str) { + return str.replace(camelizeRE, function (_, c) { return c ? c.toUpperCase() : ''; }) + }); + + /** + * Capitalize a string. + */ + var capitalize = cached(function (str) { + return str.charAt(0).toUpperCase() + str.slice(1) + }); + + /** + * Hyphenate a camelCase string. + */ + var hyphenateRE = /\B([A-Z])/g; + var hyphenate = cached(function (str) { + return str.replace(hyphenateRE, '-$1').toLowerCase() + }); + + /** + * Simple bind polyfill for environments that do not support it, + * e.g., PhantomJS 1.x. Technically, we don't need this anymore + * since native bind is now performant enough in most browsers. + * But removing it would mean breaking code that was able to run in + * PhantomJS 1.x, so this must be kept for backward compatibility. + */ + + /* istanbul ignore next */ + function polyfillBind (fn, ctx) { + function boundFn (a) { + var l = arguments.length; + return l + ? l > 1 + ? fn.apply(ctx, arguments) + : fn.call(ctx, a) + : fn.call(ctx) + } + + boundFn._length = fn.length; + return boundFn + } + + function nativeBind (fn, ctx) { + return fn.bind(ctx) + } + + var bind = Function.prototype.bind + ? nativeBind + : polyfillBind; + + /** + * Convert an Array-like object to a real Array. + */ + function toArray (list, start) { + start = start || 0; + var i = list.length - start; + var ret = new Array(i); + while (i--) { + ret[i] = list[i + start]; + } + return ret + } + + /** + * Mix properties into target object. + */ + function extend (to, _from) { + for (var key in _from) { + to[key] = _from[key]; + } + return to + } + + /** + * Merge an Array of Objects into a single Object. + */ + function toObject (arr) { + var res = {}; + for (var i = 0; i < arr.length; i++) { + if (arr[i]) { + extend(res, arr[i]); + } + } + return res + } + + /* eslint-disable no-unused-vars */ + + /** + * Perform no operation. + * Stubbing args to make Flow happy without leaving useless transpiled code + * with ...rest (https://flow.org/blog/2017/05/07/Strict-Function-Call-Arity/). + */ + function noop (a, b, c) {} + + /** + * Always return false. + */ + var no = function (a, b, c) { return false; }; + + /* eslint-enable no-unused-vars */ + + /** + * Return the same value. + */ + var identity = function (_) { return _; }; + + /** + * Generate a string containing static keys from compiler modules. + */ + function genStaticKeys (modules) { + return modules.reduce(function (keys, m) { + return keys.concat(m.staticKeys || []) + }, []).join(',') + } + + /** + * Check if two values are loosely equal - that is, + * if they are plain objects, do they have the same shape? + */ + function looseEqual (a, b) { + if (a === b) { return true } + var isObjectA = isObject(a); + var isObjectB = isObject(b); + if (isObjectA && isObjectB) { + try { + var isArrayA = Array.isArray(a); + var isArrayB = Array.isArray(b); + if (isArrayA && isArrayB) { + return a.length === b.length && a.every(function (e, i) { + return looseEqual(e, b[i]) + }) + } else if (a instanceof Date && b instanceof Date) { + return a.getTime() === b.getTime() + } else if (!isArrayA && !isArrayB) { + var keysA = Object.keys(a); + var keysB = Object.keys(b); + return keysA.length === keysB.length && keysA.every(function (key) { + return looseEqual(a[key], b[key]) + }) + } else { + /* istanbul ignore next */ + return false + } + } catch (e) { + /* istanbul ignore next */ + return false + } + } else if (!isObjectA && !isObjectB) { + return String(a) === String(b) + } else { + return false + } + } + + /** + * Return the first index at which a loosely equal value can be + * found in the array (if value is a plain object, the array must + * contain an object of the same shape), or -1 if it is not present. + */ + function looseIndexOf (arr, val) { + for (var i = 0; i < arr.length; i++) { + if (looseEqual(arr[i], val)) { return i } + } + return -1 + } + + /** + * Ensure a function is called only once. + */ + function once (fn) { + var called = false; + return function () { + if (!called) { + called = true; + fn.apply(this, arguments); + } + } + } + + var SSR_ATTR = 'data-server-rendered'; + + var ASSET_TYPES = [ + 'component', + 'directive', + 'filter' + ]; + + var LIFECYCLE_HOOKS = [ + 'beforeCreate', + 'created', + 'beforeMount', + 'mounted', + 'beforeUpdate', + 'updated', + 'beforeDestroy', + 'destroyed', + 'activated', + 'deactivated', + 'errorCaptured', + 'serverPrefetch' + ]; + + /* */ + + + + var config = ({ + /** + * Option merge strategies (used in core/util/options) + */ + // $flow-disable-line + optionMergeStrategies: Object.create(null), + + /** + * Whether to suppress warnings. + */ + silent: false, + + /** + * Show production mode tip message on boot? + */ + productionTip: "development" !== 'production', + + /** + * Whether to enable devtools + */ + devtools: "development" !== 'production', + + /** + * Whether to record perf + */ + performance: false, + + /** + * Error handler for watcher errors + */ + errorHandler: null, + + /** + * Warn handler for watcher warns + */ + warnHandler: null, + + /** + * Ignore certain custom elements + */ + ignoredElements: [], + + /** + * Custom user key aliases for v-on + */ + // $flow-disable-line + keyCodes: Object.create(null), + + /** + * Check if a tag is reserved so that it cannot be registered as a + * component. This is platform-dependent and may be overwritten. + */ + isReservedTag: no, + + /** + * Check if an attribute is reserved so that it cannot be used as a component + * prop. This is platform-dependent and may be overwritten. + */ + isReservedAttr: no, + + /** + * Check if a tag is an unknown element. + * Platform-dependent. + */ + isUnknownElement: no, + + /** + * Get the namespace of an element + */ + getTagNamespace: noop, + + /** + * Parse the real tag name for the specific platform. + */ + parsePlatformTagName: identity, + + /** + * Check if an attribute must be bound using property, e.g. value + * Platform-dependent. + */ + mustUseProp: no, + + /** + * Perform updates asynchronously. Intended to be used by Vue Test Utils + * This will significantly reduce performance if set to false. + */ + async: true, + + /** + * Exposed for legacy reasons + */ + _lifecycleHooks: LIFECYCLE_HOOKS + }); + + /* */ + + /** + * unicode letters used for parsing html tags, component names and property paths. + * using https://www.w3.org/TR/html53/semantics-scripting.html#potentialcustomelementname + * skipping \u10000-\uEFFFF due to it freezing up PhantomJS + */ + var unicodeRegExp = /a-zA-Z\u00B7\u00C0-\u00D6\u00D8-\u00F6\u00F8-\u037D\u037F-\u1FFF\u200C-\u200D\u203F-\u2040\u2070-\u218F\u2C00-\u2FEF\u3001-\uD7FF\uF900-\uFDCF\uFDF0-\uFFFD/; + + /** + * Check if a string starts with $ or _ + */ + function isReserved (str) { + var c = (str + '').charCodeAt(0); + return c === 0x24 || c === 0x5F + } + + /** + * Define a property. + */ + function def (obj, key, val, enumerable) { + Object.defineProperty(obj, key, { + value: val, + enumerable: !!enumerable, + writable: true, + configurable: true + }); + } + + /** + * Parse simple path. + */ + var bailRE = new RegExp(("[^" + (unicodeRegExp.source) + ".$_\\d]")); + function parsePath (path) { + if (bailRE.test(path)) { + return + } + var segments = path.split('.'); + return function (obj) { + for (var i = 0; i < segments.length; i++) { + if (!obj) { return } + obj = obj[segments[i]]; + } + return obj + } + } + + /* */ + + // can we use __proto__? + var hasProto = '__proto__' in {}; + + // Browser environment sniffing + var inBrowser = typeof window !== 'undefined'; + var inWeex = typeof WXEnvironment !== 'undefined' && !!WXEnvironment.platform; + var weexPlatform = inWeex && WXEnvironment.platform.toLowerCase(); + var UA = inBrowser && window.navigator.userAgent.toLowerCase(); + var isIE = UA && /msie|trident/.test(UA); + var isIE9 = UA && UA.indexOf('msie 9.0') > 0; + var isEdge = UA && UA.indexOf('edge/') > 0; + var isAndroid = (UA && UA.indexOf('android') > 0) || (weexPlatform === 'android'); + var isIOS = (UA && /iphone|ipad|ipod|ios/.test(UA)) || (weexPlatform === 'ios'); + var isChrome = UA && /chrome\/\d+/.test(UA) && !isEdge; + var isPhantomJS = UA && /phantomjs/.test(UA); + var isFF = UA && UA.match(/firefox\/(\d+)/); + + // Firefox has a "watch" function on Object.prototype... + var nativeWatch = ({}).watch; + + var supportsPassive = false; + if (inBrowser) { + try { + var opts = {}; + Object.defineProperty(opts, 'passive', ({ + get: function get () { + /* istanbul ignore next */ + supportsPassive = true; + } + })); // https://github.com/facebook/flow/issues/285 + window.addEventListener('test-passive', null, opts); + } catch (e) {} + } + + // this needs to be lazy-evaled because vue may be required before + // vue-server-renderer can set VUE_ENV + var _isServer; + var isServerRendering = function () { + if (_isServer === undefined) { + /* istanbul ignore if */ + if (!inBrowser && !inWeex && typeof global !== 'undefined') { + // detect presence of vue-server-renderer and avoid + // Webpack shimming the process + _isServer = global['process'] && global['process'].env.VUE_ENV === 'server'; + } else { + _isServer = false; + } + } + return _isServer + }; + + // detect devtools + var devtools = inBrowser && window.__VUE_DEVTOOLS_GLOBAL_HOOK__; + + /* istanbul ignore next */ + function isNative (Ctor) { + return typeof Ctor === 'function' && /native code/.test(Ctor.toString()) + } + + var hasSymbol = + typeof Symbol !== 'undefined' && isNative(Symbol) && + typeof Reflect !== 'undefined' && isNative(Reflect.ownKeys); + + var _Set; + /* istanbul ignore if */ // $flow-disable-line + if (typeof Set !== 'undefined' && isNative(Set)) { + // use native Set when available. + _Set = Set; + } else { + // a non-standard Set polyfill that only works with primitive keys. + _Set = /*@__PURE__*/(function () { + function Set () { + this.set = Object.create(null); + } + Set.prototype.has = function has (key) { + return this.set[key] === true + }; + Set.prototype.add = function add (key) { + this.set[key] = true; + }; + Set.prototype.clear = function clear () { + this.set = Object.create(null); + }; + + return Set; + }()); + } + + /* */ + + var warn = noop; + var tip = noop; + var generateComponentTrace = (noop); // work around flow check + var formatComponentName = (noop); + + { + var hasConsole = typeof console !== 'undefined'; + var classifyRE = /(?:^|[-_])(\w)/g; + var classify = function (str) { return str + .replace(classifyRE, function (c) { return c.toUpperCase(); }) + .replace(/[-_]/g, ''); }; + + warn = function (msg, vm) { + var trace = vm ? generateComponentTrace(vm) : ''; + + if (config.warnHandler) { + config.warnHandler.call(null, msg, vm, trace); + } else if (hasConsole && (!config.silent)) { + console.error(("[Vue warn]: " + msg + trace)); + } + }; + + tip = function (msg, vm) { + if (hasConsole && (!config.silent)) { + console.warn("[Vue tip]: " + msg + ( + vm ? generateComponentTrace(vm) : '' + )); + } + }; + + formatComponentName = function (vm, includeFile) { + if (vm.$root === vm) { + return '' + } + var options = typeof vm === 'function' && vm.cid != null + ? vm.options + : vm._isVue + ? vm.$options || vm.constructor.options + : vm; + var name = options.name || options._componentTag; + var file = options.__file; + if (!name && file) { + var match = file.match(/([^/\\]+)\.vue$/); + name = match && match[1]; + } + + return ( + (name ? ("<" + (classify(name)) + ">") : "") + + (file && includeFile !== false ? (" at " + file) : '') + ) + }; + + var repeat = function (str, n) { + var res = ''; + while (n) { + if (n % 2 === 1) { res += str; } + if (n > 1) { str += str; } + n >>= 1; + } + return res + }; + + generateComponentTrace = function (vm) { + if (vm._isVue && vm.$parent) { + var tree = []; + var currentRecursiveSequence = 0; + while (vm) { + if (tree.length > 0) { + var last = tree[tree.length - 1]; + if (last.constructor === vm.constructor) { + currentRecursiveSequence++; + vm = vm.$parent; + continue + } else if (currentRecursiveSequence > 0) { + tree[tree.length - 1] = [last, currentRecursiveSequence]; + currentRecursiveSequence = 0; + } + } + tree.push(vm); + vm = vm.$parent; + } + return '\n\nfound in\n\n' + tree + .map(function (vm, i) { return ("" + (i === 0 ? '---> ' : repeat(' ', 5 + i * 2)) + (Array.isArray(vm) + ? ((formatComponentName(vm[0])) + "... (" + (vm[1]) + " recursive calls)") + : formatComponentName(vm))); }) + .join('\n') + } else { + return ("\n\n(found in " + (formatComponentName(vm)) + ")") + } + }; + } + + /* */ + + var uid = 0; + + /** + * A dep is an observable that can have multiple + * directives subscribing to it. + */ + var Dep = function Dep () { + this.id = uid++; + this.subs = []; + }; + + Dep.prototype.addSub = function addSub (sub) { + this.subs.push(sub); + }; + + Dep.prototype.removeSub = function removeSub (sub) { + remove(this.subs, sub); + }; + + Dep.prototype.depend = function depend () { + if (Dep.target) { + Dep.target.addDep(this); + } + }; + + Dep.prototype.notify = function notify () { + // stabilize the subscriber list first + var subs = this.subs.slice(); + if (!config.async) { + // subs aren't sorted in scheduler if not running async + // we need to sort them now to make sure they fire in correct + // order + subs.sort(function (a, b) { return a.id - b.id; }); + } + for (var i = 0, l = subs.length; i < l; i++) { + subs[i].update(); + } + }; + + // The current target watcher being evaluated. + // This is globally unique because only one watcher + // can be evaluated at a time. + Dep.target = null; + var targetStack = []; + + function pushTarget (target) { + targetStack.push(target); + Dep.target = target; + } + + function popTarget () { + targetStack.pop(); + Dep.target = targetStack[targetStack.length - 1]; + } + + /* */ + + var VNode = function VNode ( + tag, + data, + children, + text, + elm, + context, + componentOptions, + asyncFactory + ) { + this.tag = tag; + this.data = data; + this.children = children; + this.text = text; + this.elm = elm; + this.ns = undefined; + this.context = context; + this.fnContext = undefined; + this.fnOptions = undefined; + this.fnScopeId = undefined; + this.key = data && data.key; + this.componentOptions = componentOptions; + this.componentInstance = undefined; + this.parent = undefined; + this.raw = false; + this.isStatic = false; + this.isRootInsert = true; + this.isComment = false; + this.isCloned = false; + this.isOnce = false; + this.asyncFactory = asyncFactory; + this.asyncMeta = undefined; + this.isAsyncPlaceholder = false; + }; + + var prototypeAccessors = { child: { configurable: true } }; + + // DEPRECATED: alias for componentInstance for backwards compat. + /* istanbul ignore next */ + prototypeAccessors.child.get = function () { + return this.componentInstance + }; + + Object.defineProperties( VNode.prototype, prototypeAccessors ); + + var createEmptyVNode = function (text) { + if ( text === void 0 ) text = ''; + + var node = new VNode(); + node.text = text; + node.isComment = true; + return node + }; + + function createTextVNode (val) { + return new VNode(undefined, undefined, undefined, String(val)) + } + + // optimized shallow clone + // used for static nodes and slot nodes because they may be reused across + // multiple renders, cloning them avoids errors when DOM manipulations rely + // on their elm reference. + function cloneVNode (vnode) { + var cloned = new VNode( + vnode.tag, + vnode.data, + // #7975 + // clone children array to avoid mutating original in case of cloning + // a child. + vnode.children && vnode.children.slice(), + vnode.text, + vnode.elm, + vnode.context, + vnode.componentOptions, + vnode.asyncFactory + ); + cloned.ns = vnode.ns; + cloned.isStatic = vnode.isStatic; + cloned.key = vnode.key; + cloned.isComment = vnode.isComment; + cloned.fnContext = vnode.fnContext; + cloned.fnOptions = vnode.fnOptions; + cloned.fnScopeId = vnode.fnScopeId; + cloned.asyncMeta = vnode.asyncMeta; + cloned.isCloned = true; + return cloned + } + + /* + * not type checking this file because flow doesn't play well with + * dynamically accessing methods on Array prototype + */ + + var arrayProto = Array.prototype; + var arrayMethods = Object.create(arrayProto); + + var methodsToPatch = [ + 'push', + 'pop', + 'shift', + 'unshift', + 'splice', + 'sort', + 'reverse' + ]; + + /** + * Intercept mutating methods and emit events + */ + methodsToPatch.forEach(function (method) { + // cache original method + var original = arrayProto[method]; + def(arrayMethods, method, function mutator () { + var args = [], len = arguments.length; + while ( len-- ) args[ len ] = arguments[ len ]; + + var result = original.apply(this, args); + var ob = this.__ob__; + var inserted; + switch (method) { + case 'push': + case 'unshift': + inserted = args; + break + case 'splice': + inserted = args.slice(2); + break + } + if (inserted) { ob.observeArray(inserted); } + // notify change + ob.dep.notify(); + return result + }); + }); + + /* */ + + var arrayKeys = Object.getOwnPropertyNames(arrayMethods); + + /** + * In some cases we may want to disable observation inside a component's + * update computation. + */ + var shouldObserve = true; + + function toggleObserving (value) { + shouldObserve = value; + } + + /** + * Observer class that is attached to each observed + * object. Once attached, the observer converts the target + * object's property keys into getter/setters that + * collect dependencies and dispatch updates. + */ + var Observer = function Observer (value) { + this.value = value; + this.dep = new Dep(); + this.vmCount = 0; + def(value, '__ob__', this); + if (Array.isArray(value)) { + if (hasProto) { + protoAugment(value, arrayMethods); + } else { + copyAugment(value, arrayMethods, arrayKeys); + } + this.observeArray(value); + } else { + this.walk(value); + } + }; + + /** + * Walk through all properties and convert them into + * getter/setters. This method should only be called when + * value type is Object. + */ + Observer.prototype.walk = function walk (obj) { + var keys = Object.keys(obj); + for (var i = 0; i < keys.length; i++) { + defineReactive$$1(obj, keys[i]); + } + }; + + /** + * Observe a list of Array items. + */ + Observer.prototype.observeArray = function observeArray (items) { + for (var i = 0, l = items.length; i < l; i++) { + observe(items[i]); + } + }; + + // helpers + + /** + * Augment a target Object or Array by intercepting + * the prototype chain using __proto__ + */ + function protoAugment (target, src) { + /* eslint-disable no-proto */ + target.__proto__ = src; + /* eslint-enable no-proto */ + } + + /** + * Augment a target Object or Array by defining + * hidden properties. + */ + /* istanbul ignore next */ + function copyAugment (target, src, keys) { + for (var i = 0, l = keys.length; i < l; i++) { + var key = keys[i]; + def(target, key, src[key]); + } + } + + /** + * Attempt to create an observer instance for a value, + * returns the new observer if successfully observed, + * or the existing observer if the value already has one. + */ + function observe (value, asRootData) { + if (!isObject(value) || value instanceof VNode) { + return + } + var ob; + if (hasOwn(value, '__ob__') && value.__ob__ instanceof Observer) { + ob = value.__ob__; + } else if ( + shouldObserve && + !isServerRendering() && + (Array.isArray(value) || isPlainObject(value)) && + Object.isExtensible(value) && + !value._isVue + ) { + ob = new Observer(value); + } + if (asRootData && ob) { + ob.vmCount++; + } + return ob + } + + /** + * Define a reactive property on an Object. + */ + function defineReactive$$1 ( + obj, + key, + val, + customSetter, + shallow + ) { + var dep = new Dep(); + + var property = Object.getOwnPropertyDescriptor(obj, key); + if (property && property.configurable === false) { + return + } + + // cater for pre-defined getter/setters + var getter = property && property.get; + var setter = property && property.set; + if ((!getter || setter) && arguments.length === 2) { + val = obj[key]; + } + + var childOb = !shallow && observe(val); + Object.defineProperty(obj, key, { + enumerable: true, + configurable: true, + get: function reactiveGetter () { + var value = getter ? getter.call(obj) : val; + if (Dep.target) { + dep.depend(); + if (childOb) { + childOb.dep.depend(); + if (Array.isArray(value)) { + dependArray(value); + } + } + } + return value + }, + set: function reactiveSetter (newVal) { + var value = getter ? getter.call(obj) : val; + /* eslint-disable no-self-compare */ + if (newVal === value || (newVal !== newVal && value !== value)) { + return + } + /* eslint-enable no-self-compare */ + if (customSetter) { + customSetter(); + } + // #7981: for accessor properties without setter + if (getter && !setter) { return } + if (setter) { + setter.call(obj, newVal); + } else { + val = newVal; + } + childOb = !shallow && observe(newVal); + dep.notify(); + } + }); + } + + /** + * Set a property on an object. Adds the new property and + * triggers change notification if the property doesn't + * already exist. + */ + function set (target, key, val) { + if (isUndef(target) || isPrimitive(target) + ) { + warn(("Cannot set reactive property on undefined, null, or primitive value: " + ((target)))); + } + if (Array.isArray(target) && isValidArrayIndex(key)) { + target.length = Math.max(target.length, key); + target.splice(key, 1, val); + return val + } + if (key in target && !(key in Object.prototype)) { + target[key] = val; + return val + } + var ob = (target).__ob__; + if (target._isVue || (ob && ob.vmCount)) { + warn( + 'Avoid adding reactive properties to a Vue instance or its root $data ' + + 'at runtime - declare it upfront in the data option.' + ); + return val + } + if (!ob) { + target[key] = val; + return val + } + defineReactive$$1(ob.value, key, val); + ob.dep.notify(); + return val + } + + /** + * Delete a property and trigger change if necessary. + */ + function del (target, key) { + if (isUndef(target) || isPrimitive(target) + ) { + warn(("Cannot delete reactive property on undefined, null, or primitive value: " + ((target)))); + } + if (Array.isArray(target) && isValidArrayIndex(key)) { + target.splice(key, 1); + return + } + var ob = (target).__ob__; + if (target._isVue || (ob && ob.vmCount)) { + warn( + 'Avoid deleting properties on a Vue instance or its root $data ' + + '- just set it to null.' + ); + return + } + if (!hasOwn(target, key)) { + return + } + delete target[key]; + if (!ob) { + return + } + ob.dep.notify(); + } + + /** + * Collect dependencies on array elements when the array is touched, since + * we cannot intercept array element access like property getters. + */ + function dependArray (value) { + for (var e = (void 0), i = 0, l = value.length; i < l; i++) { + e = value[i]; + e && e.__ob__ && e.__ob__.dep.depend(); + if (Array.isArray(e)) { + dependArray(e); + } + } + } + + /* */ + + /** + * Option overwriting strategies are functions that handle + * how to merge a parent option value and a child option + * value into the final value. + */ + var strats = config.optionMergeStrategies; + + /** + * Options with restrictions + */ + { + strats.el = strats.propsData = function (parent, child, vm, key) { + if (!vm) { + warn( + "option \"" + key + "\" can only be used during instance " + + 'creation with the `new` keyword.' + ); + } + return defaultStrat(parent, child) + }; + } + + /** + * Helper that recursively merges two data objects together. + */ + function mergeData (to, from) { + if (!from) { return to } + var key, toVal, fromVal; + + var keys = hasSymbol + ? Reflect.ownKeys(from) + : Object.keys(from); + + for (var i = 0; i < keys.length; i++) { + key = keys[i]; + // in case the object is already observed... + if (key === '__ob__') { continue } + toVal = to[key]; + fromVal = from[key]; + if (!hasOwn(to, key)) { + set(to, key, fromVal); + } else if ( + toVal !== fromVal && + isPlainObject(toVal) && + isPlainObject(fromVal) + ) { + mergeData(toVal, fromVal); + } + } + return to + } + + /** + * Data + */ + function mergeDataOrFn ( + parentVal, + childVal, + vm + ) { + if (!vm) { + // in a Vue.extend merge, both should be functions + if (!childVal) { + return parentVal + } + if (!parentVal) { + return childVal + } + // when parentVal & childVal are both present, + // we need to return a function that returns the + // merged result of both functions... no need to + // check if parentVal is a function here because + // it has to be a function to pass previous merges. + return function mergedDataFn () { + return mergeData( + typeof childVal === 'function' ? childVal.call(this, this) : childVal, + typeof parentVal === 'function' ? parentVal.call(this, this) : parentVal + ) + } + } else { + return function mergedInstanceDataFn () { + // instance merge + var instanceData = typeof childVal === 'function' + ? childVal.call(vm, vm) + : childVal; + var defaultData = typeof parentVal === 'function' + ? parentVal.call(vm, vm) + : parentVal; + if (instanceData) { + return mergeData(instanceData, defaultData) + } else { + return defaultData + } + } + } + } + + strats.data = function ( + parentVal, + childVal, + vm + ) { + if (!vm) { + if (childVal && typeof childVal !== 'function') { + warn( + 'The "data" option should be a function ' + + 'that returns a per-instance value in component ' + + 'definitions.', + vm + ); + + return parentVal + } + return mergeDataOrFn(parentVal, childVal) + } + + return mergeDataOrFn(parentVal, childVal, vm) + }; + + /** + * Hooks and props are merged as arrays. + */ + function mergeHook ( + parentVal, + childVal + ) { + var res = childVal + ? parentVal + ? parentVal.concat(childVal) + : Array.isArray(childVal) + ? childVal + : [childVal] + : parentVal; + return res + ? dedupeHooks(res) + : res + } + + function dedupeHooks (hooks) { + var res = []; + for (var i = 0; i < hooks.length; i++) { + if (res.indexOf(hooks[i]) === -1) { + res.push(hooks[i]); + } + } + return res + } + + LIFECYCLE_HOOKS.forEach(function (hook) { + strats[hook] = mergeHook; + }); + + /** + * Assets + * + * When a vm is present (instance creation), we need to do + * a three-way merge between constructor options, instance + * options and parent options. + */ + function mergeAssets ( + parentVal, + childVal, + vm, + key + ) { + var res = Object.create(parentVal || null); + if (childVal) { + assertObjectType(key, childVal, vm); + return extend(res, childVal) + } else { + return res + } + } + + ASSET_TYPES.forEach(function (type) { + strats[type + 's'] = mergeAssets; + }); + + /** + * Watchers. + * + * Watchers hashes should not overwrite one + * another, so we merge them as arrays. + */ + strats.watch = function ( + parentVal, + childVal, + vm, + key + ) { + // work around Firefox's Object.prototype.watch... + if (parentVal === nativeWatch) { parentVal = undefined; } + if (childVal === nativeWatch) { childVal = undefined; } + /* istanbul ignore if */ + if (!childVal) { return Object.create(parentVal || null) } + { + assertObjectType(key, childVal, vm); + } + if (!parentVal) { return childVal } + var ret = {}; + extend(ret, parentVal); + for (var key$1 in childVal) { + var parent = ret[key$1]; + var child = childVal[key$1]; + if (parent && !Array.isArray(parent)) { + parent = [parent]; + } + ret[key$1] = parent + ? parent.concat(child) + : Array.isArray(child) ? child : [child]; + } + return ret + }; + + /** + * Other object hashes. + */ + strats.props = + strats.methods = + strats.inject = + strats.computed = function ( + parentVal, + childVal, + vm, + key + ) { + if (childVal && "development" !== 'production') { + assertObjectType(key, childVal, vm); + } + if (!parentVal) { return childVal } + var ret = Object.create(null); + extend(ret, parentVal); + if (childVal) { extend(ret, childVal); } + return ret + }; + strats.provide = mergeDataOrFn; + + /** + * Default strategy. + */ + var defaultStrat = function (parentVal, childVal) { + return childVal === undefined + ? parentVal + : childVal + }; + + /** + * Validate component names + */ + function checkComponents (options) { + for (var key in options.components) { + validateComponentName(key); + } + } + + function validateComponentName (name) { + if (!new RegExp(("^[a-zA-Z][\\-\\.0-9_" + (unicodeRegExp.source) + "]*$")).test(name)) { + warn( + 'Invalid component name: "' + name + '". Component names ' + + 'should conform to valid custom element name in html5 specification.' + ); + } + if (isBuiltInTag(name) || config.isReservedTag(name)) { + warn( + 'Do not use built-in or reserved HTML elements as component ' + + 'id: ' + name + ); + } + } + + /** + * Ensure all props option syntax are normalized into the + * Object-based format. + */ + function normalizeProps (options, vm) { + var props = options.props; + if (!props) { return } + var res = {}; + var i, val, name; + if (Array.isArray(props)) { + i = props.length; + while (i--) { + val = props[i]; + if (typeof val === 'string') { + name = camelize(val); + res[name] = { type: null }; + } else { + warn('props must be strings when using array syntax.'); + } + } + } else if (isPlainObject(props)) { + for (var key in props) { + val = props[key]; + name = camelize(key); + res[name] = isPlainObject(val) + ? val + : { type: val }; + } + } else { + warn( + "Invalid value for option \"props\": expected an Array or an Object, " + + "but got " + (toRawType(props)) + ".", + vm + ); + } + options.props = res; + } + + /** + * Normalize all injections into Object-based format + */ + function normalizeInject (options, vm) { + var inject = options.inject; + if (!inject) { return } + var normalized = options.inject = {}; + if (Array.isArray(inject)) { + for (var i = 0; i < inject.length; i++) { + normalized[inject[i]] = { from: inject[i] }; + } + } else if (isPlainObject(inject)) { + for (var key in inject) { + var val = inject[key]; + normalized[key] = isPlainObject(val) + ? extend({ from: key }, val) + : { from: val }; + } + } else { + warn( + "Invalid value for option \"inject\": expected an Array or an Object, " + + "but got " + (toRawType(inject)) + ".", + vm + ); + } + } + + /** + * Normalize raw function directives into object format. + */ + function normalizeDirectives (options) { + var dirs = options.directives; + if (dirs) { + for (var key in dirs) { + var def$$1 = dirs[key]; + if (typeof def$$1 === 'function') { + dirs[key] = { bind: def$$1, update: def$$1 }; + } + } + } + } + + function assertObjectType (name, value, vm) { + if (!isPlainObject(value)) { + warn( + "Invalid value for option \"" + name + "\": expected an Object, " + + "but got " + (toRawType(value)) + ".", + vm + ); + } + } + + /** + * Merge two option objects into a new one. + * Core utility used in both instantiation and inheritance. + */ + function mergeOptions ( + parent, + child, + vm + ) { + { + checkComponents(child); + } + + if (typeof child === 'function') { + child = child.options; + } + + normalizeProps(child, vm); + normalizeInject(child, vm); + normalizeDirectives(child); + + // Apply extends and mixins on the child options, + // but only if it is a raw options object that isn't + // the result of another mergeOptions call. + // Only merged options has the _base property. + if (!child._base) { + if (child.extends) { + parent = mergeOptions(parent, child.extends, vm); + } + if (child.mixins) { + for (var i = 0, l = child.mixins.length; i < l; i++) { + parent = mergeOptions(parent, child.mixins[i], vm); + } + } + } + + var options = {}; + var key; + for (key in parent) { + mergeField(key); + } + for (key in child) { + if (!hasOwn(parent, key)) { + mergeField(key); + } + } + function mergeField (key) { + var strat = strats[key] || defaultStrat; + options[key] = strat(parent[key], child[key], vm, key); + } + return options + } + + /** + * Resolve an asset. + * This function is used because child instances need access + * to assets defined in its ancestor chain. + */ + function resolveAsset ( + options, + type, + id, + warnMissing + ) { + /* istanbul ignore if */ + if (typeof id !== 'string') { + return + } + var assets = options[type]; + // check local registration variations first + if (hasOwn(assets, id)) { return assets[id] } + var camelizedId = camelize(id); + if (hasOwn(assets, camelizedId)) { return assets[camelizedId] } + var PascalCaseId = capitalize(camelizedId); + if (hasOwn(assets, PascalCaseId)) { return assets[PascalCaseId] } + // fallback to prototype chain + var res = assets[id] || assets[camelizedId] || assets[PascalCaseId]; + if (warnMissing && !res) { + warn( + 'Failed to resolve ' + type.slice(0, -1) + ': ' + id, + options + ); + } + return res + } + + /* */ + + + + function validateProp ( + key, + propOptions, + propsData, + vm + ) { + var prop = propOptions[key]; + var absent = !hasOwn(propsData, key); + var value = propsData[key]; + // boolean casting + var booleanIndex = getTypeIndex(Boolean, prop.type); + if (booleanIndex > -1) { + if (absent && !hasOwn(prop, 'default')) { + value = false; + } else if (value === '' || value === hyphenate(key)) { + // only cast empty string / same name to boolean if + // boolean has higher priority + var stringIndex = getTypeIndex(String, prop.type); + if (stringIndex < 0 || booleanIndex < stringIndex) { + value = true; + } + } + } + // check default value + if (value === undefined) { + value = getPropDefaultValue(vm, prop, key); + // since the default value is a fresh copy, + // make sure to observe it. + var prevShouldObserve = shouldObserve; + toggleObserving(true); + observe(value); + toggleObserving(prevShouldObserve); + } + { + assertProp(prop, key, value, vm, absent); + } + return value + } + + /** + * Get the default value of a prop. + */ + function getPropDefaultValue (vm, prop, key) { + // no default, return undefined + if (!hasOwn(prop, 'default')) { + return undefined + } + var def = prop.default; + // warn against non-factory defaults for Object & Array + if (isObject(def)) { + warn( + 'Invalid default value for prop "' + key + '": ' + + 'Props with type Object/Array must use a factory function ' + + 'to return the default value.', + vm + ); + } + // the raw prop value was also undefined from previous render, + // return previous default value to avoid unnecessary watcher trigger + if (vm && vm.$options.propsData && + vm.$options.propsData[key] === undefined && + vm._props[key] !== undefined + ) { + return vm._props[key] + } + // call factory function for non-Function types + // a value is Function if its prototype is function even across different execution context + return typeof def === 'function' && getType(prop.type) !== 'Function' + ? def.call(vm) + : def + } + + /** + * Assert whether a prop is valid. + */ + function assertProp ( + prop, + name, + value, + vm, + absent + ) { + if (prop.required && absent) { + warn( + 'Missing required prop: "' + name + '"', + vm + ); + return + } + if (value == null && !prop.required) { + return + } + var type = prop.type; + var valid = !type || type === true; + var expectedTypes = []; + if (type) { + if (!Array.isArray(type)) { + type = [type]; + } + for (var i = 0; i < type.length && !valid; i++) { + var assertedType = assertType(value, type[i]); + expectedTypes.push(assertedType.expectedType || ''); + valid = assertedType.valid; + } + } + + if (!valid) { + warn( + getInvalidTypeMessage(name, value, expectedTypes), + vm + ); + return + } + var validator = prop.validator; + if (validator) { + if (!validator(value)) { + warn( + 'Invalid prop: custom validator check failed for prop "' + name + '".', + vm + ); + } + } + } + + var simpleCheckRE = /^(String|Number|Boolean|Function|Symbol)$/; + + function assertType (value, type) { + var valid; + var expectedType = getType(type); + if (simpleCheckRE.test(expectedType)) { + var t = typeof value; + valid = t === expectedType.toLowerCase(); + // for primitive wrapper objects + if (!valid && t === 'object') { + valid = value instanceof type; + } + } else if (expectedType === 'Object') { + valid = isPlainObject(value); + } else if (expectedType === 'Array') { + valid = Array.isArray(value); + } else { + valid = value instanceof type; + } + return { + valid: valid, + expectedType: expectedType + } + } + + /** + * Use function string name to check built-in types, + * because a simple equality check will fail when running + * across different vms / iframes. + */ + function getType (fn) { + var match = fn && fn.toString().match(/^\s*function (\w+)/); + return match ? match[1] : '' + } + + function isSameType (a, b) { + return getType(a) === getType(b) + } + + function getTypeIndex (type, expectedTypes) { + if (!Array.isArray(expectedTypes)) { + return isSameType(expectedTypes, type) ? 0 : -1 + } + for (var i = 0, len = expectedTypes.length; i < len; i++) { + if (isSameType(expectedTypes[i], type)) { + return i + } + } + return -1 + } + + function getInvalidTypeMessage (name, value, expectedTypes) { + var message = "Invalid prop: type check failed for prop \"" + name + "\"." + + " Expected " + (expectedTypes.map(capitalize).join(', ')); + var expectedType = expectedTypes[0]; + var receivedType = toRawType(value); + var expectedValue = styleValue(value, expectedType); + var receivedValue = styleValue(value, receivedType); + // check if we need to specify expected value + if (expectedTypes.length === 1 && + isExplicable(expectedType) && + !isBoolean(expectedType, receivedType)) { + message += " with value " + expectedValue; + } + message += ", got " + receivedType + " "; + // check if we need to specify received value + if (isExplicable(receivedType)) { + message += "with value " + receivedValue + "."; + } + return message + } + + function styleValue (value, type) { + if (type === 'String') { + return ("\"" + value + "\"") + } else if (type === 'Number') { + return ("" + (Number(value))) + } else { + return ("" + value) + } + } + + function isExplicable (value) { + var explicitTypes = ['string', 'number', 'boolean']; + return explicitTypes.some(function (elem) { return value.toLowerCase() === elem; }) + } + + function isBoolean () { + var args = [], len = arguments.length; + while ( len-- ) args[ len ] = arguments[ len ]; + + return args.some(function (elem) { return elem.toLowerCase() === 'boolean'; }) + } + + /* */ + + function handleError (err, vm, info) { + // Deactivate deps tracking while processing error handler to avoid possible infinite rendering. + // See: https://github.com/vuejs/vuex/issues/1505 + pushTarget(); + try { + if (vm) { + var cur = vm; + while ((cur = cur.$parent)) { + var hooks = cur.$options.errorCaptured; + if (hooks) { + for (var i = 0; i < hooks.length; i++) { + try { + var capture = hooks[i].call(cur, err, vm, info) === false; + if (capture) { return } + } catch (e) { + globalHandleError(e, cur, 'errorCaptured hook'); + } + } + } + } + } + globalHandleError(err, vm, info); + } finally { + popTarget(); + } + } + + function invokeWithErrorHandling ( + handler, + context, + args, + vm, + info + ) { + var res; + try { + res = args ? handler.apply(context, args) : handler.call(context); + if (res && !res._isVue && isPromise(res) && !res._handled) { + res.catch(function (e) { return handleError(e, vm, info + " (Promise/async)"); }); + // issue #9511 + // avoid catch triggering multiple times when nested calls + res._handled = true; + } + } catch (e) { + handleError(e, vm, info); + } + return res + } + + function globalHandleError (err, vm, info) { + if (config.errorHandler) { + try { + return config.errorHandler.call(null, err, vm, info) + } catch (e) { + // if the user intentionally throws the original error in the handler, + // do not log it twice + if (e !== err) { + logError(e, null, 'config.errorHandler'); + } + } + } + logError(err, vm, info); + } + + function logError (err, vm, info) { + { + warn(("Error in " + info + ": \"" + (err.toString()) + "\""), vm); + } + /* istanbul ignore else */ + if ((inBrowser || inWeex) && typeof console !== 'undefined') { + console.error(err); + } else { + throw err + } + } + + /* */ + + var isUsingMicroTask = false; + + var callbacks = []; + var pending = false; + + function flushCallbacks () { + pending = false; + var copies = callbacks.slice(0); + callbacks.length = 0; + for (var i = 0; i < copies.length; i++) { + copies[i](); + } + } + + // Here we have async deferring wrappers using microtasks. + // In 2.5 we used (macro) tasks (in combination with microtasks). + // However, it has subtle problems when state is changed right before repaint + // (e.g. #6813, out-in transitions). + // Also, using (macro) tasks in event handler would cause some weird behaviors + // that cannot be circumvented (e.g. #7109, #7153, #7546, #7834, #8109). + // So we now use microtasks everywhere, again. + // A major drawback of this tradeoff is that there are some scenarios + // where microtasks have too high a priority and fire in between supposedly + // sequential events (e.g. #4521, #6690, which have workarounds) + // or even between bubbling of the same event (#6566). + var timerFunc; + + // The nextTick behavior leverages the microtask queue, which can be accessed + // via either native Promise.then or MutationObserver. + // MutationObserver has wider support, however it is seriously bugged in + // UIWebView in iOS >= 9.3.3 when triggered in touch event handlers. It + // completely stops working after triggering a few times... so, if native + // Promise is available, we will use it: + /* istanbul ignore next, $flow-disable-line */ + if (typeof Promise !== 'undefined' && isNative(Promise)) { + var p = Promise.resolve(); + timerFunc = function () { + p.then(flushCallbacks); + // In problematic UIWebViews, Promise.then doesn't completely break, but + // it can get stuck in a weird state where callbacks are pushed into the + // microtask queue but the queue isn't being flushed, until the browser + // needs to do some other work, e.g. handle a timer. Therefore we can + // "force" the microtask queue to be flushed by adding an empty timer. + if (isIOS) { setTimeout(noop); } + }; + isUsingMicroTask = true; + } else if (!isIE && typeof MutationObserver !== 'undefined' && ( + isNative(MutationObserver) || + // PhantomJS and iOS 7.x + MutationObserver.toString() === '[object MutationObserverConstructor]' + )) { + // Use MutationObserver where native Promise is not available, + // e.g. PhantomJS, iOS7, Android 4.4 + // (#6466 MutationObserver is unreliable in IE11) + var counter = 1; + var observer = new MutationObserver(flushCallbacks); + var textNode = document.createTextNode(String(counter)); + observer.observe(textNode, { + characterData: true + }); + timerFunc = function () { + counter = (counter + 1) % 2; + textNode.data = String(counter); + }; + isUsingMicroTask = true; + } else if (typeof setImmediate !== 'undefined' && isNative(setImmediate)) { + // Fallback to setImmediate. + // Technically it leverages the (macro) task queue, + // but it is still a better choice than setTimeout. + timerFunc = function () { + setImmediate(flushCallbacks); + }; + } else { + // Fallback to setTimeout. + timerFunc = function () { + setTimeout(flushCallbacks, 0); + }; + } + + function nextTick (cb, ctx) { + var _resolve; + callbacks.push(function () { + if (cb) { + try { + cb.call(ctx); + } catch (e) { + handleError(e, ctx, 'nextTick'); + } + } else if (_resolve) { + _resolve(ctx); + } + }); + if (!pending) { + pending = true; + timerFunc(); + } + // $flow-disable-line + if (!cb && typeof Promise !== 'undefined') { + return new Promise(function (resolve) { + _resolve = resolve; + }) + } + } + + /* */ + + var mark; + var measure; + + { + var perf = inBrowser && window.performance; + /* istanbul ignore if */ + if ( + perf && + perf.mark && + perf.measure && + perf.clearMarks && + perf.clearMeasures + ) { + mark = function (tag) { return perf.mark(tag); }; + measure = function (name, startTag, endTag) { + perf.measure(name, startTag, endTag); + perf.clearMarks(startTag); + perf.clearMarks(endTag); + // perf.clearMeasures(name) + }; + } + } + + /* not type checking this file because flow doesn't play well with Proxy */ + + var initProxy; + + { + var allowedGlobals = makeMap( + 'Infinity,undefined,NaN,isFinite,isNaN,' + + 'parseFloat,parseInt,decodeURI,decodeURIComponent,encodeURI,encodeURIComponent,' + + 'Math,Number,Date,Array,Object,Boolean,String,RegExp,Map,Set,JSON,Intl,' + + 'require' // for Webpack/Browserify + ); + + var warnNonPresent = function (target, key) { + warn( + "Property or method \"" + key + "\" is not defined on the instance but " + + 'referenced during render. Make sure that this property is reactive, ' + + 'either in the data option, or for class-based components, by ' + + 'initializing the property. ' + + 'See: https://vuejs.org/v2/guide/reactivity.html#Declaring-Reactive-Properties.', + target + ); + }; + + var warnReservedPrefix = function (target, key) { + warn( + "Property \"" + key + "\" must be accessed with \"$data." + key + "\" because " + + 'properties starting with "$" or "_" are not proxied in the Vue instance to ' + + 'prevent conflicts with Vue internals. ' + + 'See: https://vuejs.org/v2/api/#data', + target + ); + }; + + var hasProxy = + typeof Proxy !== 'undefined' && isNative(Proxy); + + if (hasProxy) { + var isBuiltInModifier = makeMap('stop,prevent,self,ctrl,shift,alt,meta,exact'); + config.keyCodes = new Proxy(config.keyCodes, { + set: function set (target, key, value) { + if (isBuiltInModifier(key)) { + warn(("Avoid overwriting built-in modifier in config.keyCodes: ." + key)); + return false + } else { + target[key] = value; + return true + } + } + }); + } + + var hasHandler = { + has: function has (target, key) { + var has = key in target; + var isAllowed = allowedGlobals(key) || + (typeof key === 'string' && key.charAt(0) === '_' && !(key in target.$data)); + if (!has && !isAllowed) { + if (key in target.$data) { warnReservedPrefix(target, key); } + else { warnNonPresent(target, key); } + } + return has || !isAllowed + } + }; + + var getHandler = { + get: function get (target, key) { + if (typeof key === 'string' && !(key in target)) { + if (key in target.$data) { warnReservedPrefix(target, key); } + else { warnNonPresent(target, key); } + } + return target[key] + } + }; + + initProxy = function initProxy (vm) { + if (hasProxy) { + // determine which proxy handler to use + var options = vm.$options; + var handlers = options.render && options.render._withStripped + ? getHandler + : hasHandler; + vm._renderProxy = new Proxy(vm, handlers); + } else { + vm._renderProxy = vm; + } + }; + } + + /* */ + + var seenObjects = new _Set(); + + /** + * Recursively traverse an object to evoke all converted + * getters, so that every nested property inside the object + * is collected as a "deep" dependency. + */ + function traverse (val) { + _traverse(val, seenObjects); + seenObjects.clear(); + } + + function _traverse (val, seen) { + var i, keys; + var isA = Array.isArray(val); + if ((!isA && !isObject(val)) || Object.isFrozen(val) || val instanceof VNode) { + return + } + if (val.__ob__) { + var depId = val.__ob__.dep.id; + if (seen.has(depId)) { + return + } + seen.add(depId); + } + if (isA) { + i = val.length; + while (i--) { _traverse(val[i], seen); } + } else { + keys = Object.keys(val); + i = keys.length; + while (i--) { _traverse(val[keys[i]], seen); } + } + } + + /* */ + + var normalizeEvent = cached(function (name) { + var passive = name.charAt(0) === '&'; + name = passive ? name.slice(1) : name; + var once$$1 = name.charAt(0) === '~'; // Prefixed last, checked first + name = once$$1 ? name.slice(1) : name; + var capture = name.charAt(0) === '!'; + name = capture ? name.slice(1) : name; + return { + name: name, + once: once$$1, + capture: capture, + passive: passive + } + }); + + function createFnInvoker (fns, vm) { + function invoker () { + var arguments$1 = arguments; + + var fns = invoker.fns; + if (Array.isArray(fns)) { + var cloned = fns.slice(); + for (var i = 0; i < cloned.length; i++) { + invokeWithErrorHandling(cloned[i], null, arguments$1, vm, "v-on handler"); + } + } else { + // return handler return value for single handlers + return invokeWithErrorHandling(fns, null, arguments, vm, "v-on handler") + } + } + invoker.fns = fns; + return invoker + } + + function updateListeners ( + on, + oldOn, + add, + remove$$1, + createOnceHandler, + vm + ) { + var name, def$$1, cur, old, event; + for (name in on) { + def$$1 = cur = on[name]; + old = oldOn[name]; + event = normalizeEvent(name); + if (isUndef(cur)) { + warn( + "Invalid handler for event \"" + (event.name) + "\": got " + String(cur), + vm + ); + } else if (isUndef(old)) { + if (isUndef(cur.fns)) { + cur = on[name] = createFnInvoker(cur, vm); + } + if (isTrue(event.once)) { + cur = on[name] = createOnceHandler(event.name, cur, event.capture); + } + add(event.name, cur, event.capture, event.passive, event.params); + } else if (cur !== old) { + old.fns = cur; + on[name] = old; + } + } + for (name in oldOn) { + if (isUndef(on[name])) { + event = normalizeEvent(name); + remove$$1(event.name, oldOn[name], event.capture); + } + } + } + + /* */ + + function mergeVNodeHook (def, hookKey, hook) { + if (def instanceof VNode) { + def = def.data.hook || (def.data.hook = {}); + } + var invoker; + var oldHook = def[hookKey]; + + function wrappedHook () { + hook.apply(this, arguments); + // important: remove merged hook to ensure it's called only once + // and prevent memory leak + remove(invoker.fns, wrappedHook); + } + + if (isUndef(oldHook)) { + // no existing hook + invoker = createFnInvoker([wrappedHook]); + } else { + /* istanbul ignore if */ + if (isDef(oldHook.fns) && isTrue(oldHook.merged)) { + // already a merged invoker + invoker = oldHook; + invoker.fns.push(wrappedHook); + } else { + // existing plain hook + invoker = createFnInvoker([oldHook, wrappedHook]); + } + } + + invoker.merged = true; + def[hookKey] = invoker; + } + + /* */ + + function extractPropsFromVNodeData ( + data, + Ctor, + tag + ) { + // we are only extracting raw values here. + // validation and default values are handled in the child + // component itself. + var propOptions = Ctor.options.props; + if (isUndef(propOptions)) { + return + } + var res = {}; + var attrs = data.attrs; + var props = data.props; + if (isDef(attrs) || isDef(props)) { + for (var key in propOptions) { + var altKey = hyphenate(key); + { + var keyInLowerCase = key.toLowerCase(); + if ( + key !== keyInLowerCase && + attrs && hasOwn(attrs, keyInLowerCase) + ) { + tip( + "Prop \"" + keyInLowerCase + "\" is passed to component " + + (formatComponentName(tag || Ctor)) + ", but the declared prop name is" + + " \"" + key + "\". " + + "Note that HTML attributes are case-insensitive and camelCased " + + "props need to use their kebab-case equivalents when using in-DOM " + + "templates. You should probably use \"" + altKey + "\" instead of \"" + key + "\"." + ); + } + } + checkProp(res, props, key, altKey, true) || + checkProp(res, attrs, key, altKey, false); + } + } + return res + } + + function checkProp ( + res, + hash, + key, + altKey, + preserve + ) { + if (isDef(hash)) { + if (hasOwn(hash, key)) { + res[key] = hash[key]; + if (!preserve) { + delete hash[key]; + } + return true + } else if (hasOwn(hash, altKey)) { + res[key] = hash[altKey]; + if (!preserve) { + delete hash[altKey]; + } + return true + } + } + return false + } + + /* */ + + // The template compiler attempts to minimize the need for normalization by + // statically analyzing the template at compile time. + // + // For plain HTML markup, normalization can be completely skipped because the + // generated render function is guaranteed to return Array. There are + // two cases where extra normalization is needed: + + // 1. When the children contains components - because a functional component + // may return an Array instead of a single root. In this case, just a simple + // normalization is needed - if any child is an Array, we flatten the whole + // thing with Array.prototype.concat. It is guaranteed to be only 1-level deep + // because functional components already normalize their own children. + function simpleNormalizeChildren (children) { + for (var i = 0; i < children.length; i++) { + if (Array.isArray(children[i])) { + return Array.prototype.concat.apply([], children) + } + } + return children + } + + // 2. When the children contains constructs that always generated nested Arrays, + // e.g.