diff --git a/fieldservice_portal/README.rst b/fieldservice_portal/README.rst new file mode 100644 index 0000000000..62b7b3f5f5 --- /dev/null +++ b/fieldservice_portal/README.rst @@ -0,0 +1,90 @@ +====================== +Field Service - Portal +====================== + +.. + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! This file is generated by oca-gen-addon-readme !! + !! changes will be overwritten. !! + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! source digest: sha256:bfddb8307746f6f3179ded15fd2c7b9a6622c6e21d24ae73c9aece10a7f455f6 + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + +.. |badge1| image:: https://img.shields.io/badge/maturity-Beta-yellow.png + :target: https://odoo-community.org/page/development-status + :alt: Beta +.. |badge2| image:: https://img.shields.io/badge/licence-AGPL--3-blue.png + :target: http://www.gnu.org/licenses/agpl-3.0-standalone.html + :alt: License: AGPL-3 +.. |badge3| image:: https://img.shields.io/badge/github-OCA%2Ffield--service-lightgray.png?logo=github + :target: https://github.com/OCA/field-service/tree/16.0/fieldservice_portal + :alt: OCA/field-service +.. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png + :target: https://translation.odoo-community.org/projects/field-service-16-0/field-service-16-0-fieldservice_portal + :alt: Translate me on Weblate +.. |badge5| image:: https://img.shields.io/badge/runboat-Try%20me-875A7B.png + :target: https://runboat.odoo-community.org/builds?repo=OCA/field-service&target_branch=16.0 + :alt: Try me on Runboat + +|badge1| |badge2| |badge3| |badge4| |badge5| + +Bridge module between fieldservice and portal that allows portal users +to monitor work orders related to their locations. + +**Table of contents** + +.. contents:: + :local: + +Bug Tracker +=========== + +Bugs are tracked on `GitHub Issues `_. +In case of trouble, please check there if your issue has already been reported. +If you spotted it first, help us to smash it by providing a detailed and welcomed +`feedback `_. + +Do not contact contributors directly about support or help with technical issues. + +Credits +======= + +Authors +~~~~~~~ + +* PyTech SRL + +Contributors +~~~~~~~~~~~~ + +* `PyTech SRL `_: + + * Alessio Renda + +Maintainers +~~~~~~~~~~~ + +This module is maintained by the OCA. + +.. image:: https://odoo-community.org/logo.png + :alt: Odoo Community Association + :target: https://odoo-community.org + +OCA, or the Odoo Community Association, is a nonprofit organization whose +mission is to support the collaborative development of Odoo features and +promote its widespread use. + +.. |maintainer-aleuffre| image:: https://github.com/aleuffre.png?size=40px + :target: https://github.com/aleuffre + :alt: aleuffre +.. |maintainer-renda-dev| image:: https://github.com/renda-dev.png?size=40px + :target: https://github.com/renda-dev + :alt: renda-dev + +Current `maintainers `__: + +|maintainer-aleuffre| |maintainer-renda-dev| + +This module is part of the `OCA/field-service `_ project on GitHub. + +You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute. diff --git a/fieldservice_portal/__init__.py b/fieldservice_portal/__init__.py new file mode 100644 index 0000000000..e046e49fbe --- /dev/null +++ b/fieldservice_portal/__init__.py @@ -0,0 +1 @@ +from . import controllers diff --git a/fieldservice_portal/__manifest__.py b/fieldservice_portal/__manifest__.py new file mode 100644 index 0000000000..b159c19e6a --- /dev/null +++ b/fieldservice_portal/__manifest__.py @@ -0,0 +1,33 @@ +{ + "name": "Field Service - Portal", + "version": "16.0.1.0.0", + "summary": """ + Bridge module between fieldservice and portal. + """, + "depends": [ + "fieldservice", + "portal", + ], + "author": "PyTech SRL, Odoo Community Association (OCA)", + "maintainers": ["aleuffre", "renda-dev"], + "website": "https://github.com/OCA/field-service", + "category": "Field Service", + "license": "AGPL-3", + "data": [ + "security/ir.model.access.csv", + "security/portal_security.xml", + "views/fsm_order_template.xml", + "views/portal_template.xml", + ], + "demo": [ + "demo/fsm_location_demo.xml", + "demo/fsm_order_demo.xml", + ], + "assets": { + "web.assets_frontend": [ + "fieldservice_portal/static/src/js/fsm_order_portal.js", + ], + }, + "installable": True, + "application": False, +} diff --git a/fieldservice_portal/controllers/__init__.py b/fieldservice_portal/controllers/__init__.py new file mode 100644 index 0000000000..d09bc16471 --- /dev/null +++ b/fieldservice_portal/controllers/__init__.py @@ -0,0 +1 @@ +from . import fsm_order_portal diff --git a/fieldservice_portal/controllers/fsm_order_portal.py b/fieldservice_portal/controllers/fsm_order_portal.py new file mode 100644 index 0000000000..8bf1be9b50 --- /dev/null +++ b/fieldservice_portal/controllers/fsm_order_portal.py @@ -0,0 +1,212 @@ +from collections import OrderedDict +from operator import itemgetter + +from odoo import _, http +from odoo.exceptions import AccessError +from odoo.http import request +from odoo.osv.expression import OR +from odoo.tools import groupby as groupbyelem + +from odoo.addons.portal.controllers.portal import CustomerPortal, pager as portal_pager + + +class CustomerPortal(CustomerPortal): + def _prepare_home_portal_values(self, counters): + values = super()._prepare_home_portal_values(counters) + if "fsm_order_count" in counters: + fsm_order_count = ( + request.env["fsm.order"].search_count([]) + if request.env["fsm.order"].check_access_rights( + "read", raise_exception=False + ) + else 0 + ) + values["fsm_order_count"] = fsm_order_count + return values + + def _fsm_order_check_access(self, order_id): + order = request.env["fsm.order"].browse([order_id]) + + try: + order.check_access_rights("read") + order.check_access_rule("read") + except AccessError: + raise + return order.sudo() + + def fsm_order_get_page_view_values(self, order, **kwargs): + values = { + "page_name": "fsm_order", + "order": order, + } + + if kwargs.get("error"): + values["error"] = kwargs["error"] + if kwargs.get("warning"): + values["warning"] = kwargs["warning"] + if kwargs.get("success"): + values["success"] = kwargs["success"] + + return values + + @http.route( + ["/my/fsm_orders", "/my/fsm_orders/page/"], + type="http", + auth="user", + website=True, + ) + def portal_my_fsm_orders( + self, + page=1, + date_begin=None, + date_end=None, + sortby=None, + filterby=None, + groupby=None, + search=None, + search_in="all", + **kw + ): + values = self._prepare_portal_layout_values() + FsmOrder = request.env["fsm.order"] + domain = [] + + searchbar_sortings = { + "date": {"label": _("Newest"), "order": "request_early desc"}, + "name": {"label": _("Name"), "order": "name"}, + "stage": {"label": _("Stage"), "order": "stage_id"}, + "location": {"label": _("Location"), "order": "location_id"}, + "type": {"label": _("Type"), "order": "type"}, + } + + searchbar_groupby = { + "none": {"input": "none", "label": _("None")}, + "location_id": {"input": "location", "label": _("Location")}, + "ticket_id": {"input": "ticket", "label": _("Ticket")}, + "stage_id": {"input": "stage", "label": _("Stage")}, + "type": {"input": "type", "label": _("Type")}, + } + + # search input (text) + searchbar_inputs = OrderedDict( + ( + ("all", {"input": "all", "label": _("Search in All")}), + ("name", {"input": "name", "label": _("Search in WO Number")}), + ( + "description", + { + "input": "description", + "label": _("Search in Description"), + }, + ), + ( + "location_id.name", + { + "input": "location", + "label": _("Search in Location Numbers"), + }, + ), + ) + ) + + if search and search_in: + search_domain = [] + for search_property in [ + k + for (k, v) in searchbar_inputs.items() + if search_in in (v["input"], "all") and k != "all" + ]: + search_domain = OR( + [search_domain, [(search_property, "ilike", search)]] + ) + domain += search_domain + + # search filters (by stage) + searchbar_filters = OrderedDict( + ( + str(stage.name), + { + "label": stage.name, + "domain": [("stage_id", "=", stage.id)], + }, + ) + for stage in request.env["fsm.stage"].search([("stage_type", "=", "order")]) + ) + searchbar_filters.update( + { + "all": {"label": _("All"), "domain": []}, + "open": {"label": _("Open"), "domain": [("is_closed", "=", False)]}, + } + ) + + # default group by value + if not groupby: + groupby = "location_id" + # default sort by order + if not sortby: + sortby = "date" + order = searchbar_sortings[sortby]["order"] + # default filter by value + if not filterby: + filterby = "open" + domain += searchbar_filters[filterby]["domain"] + + # count for pager + fsm_order_count = FsmOrder.search_count(domain) + # pager + pager = portal_pager( + url="/my/fsm_orders", + url_args={}, + total=fsm_order_count, + page=page, + step=self._items_per_page, + ) + # content according to pager and archive selected + fsm_orders = FsmOrder.search( + domain, + order=order, + limit=self._items_per_page, + offset=pager["offset"], + ) + + if groupby == "none": + grouped_orders = [fsm_orders] if fsm_orders else [] + else: + grouped_orders = [ + FsmOrder.concat(*g) + for k, g in groupbyelem(fsm_orders, itemgetter(groupby)) + ] + + values.update( + { + "date": date_begin, + "grouped_orders": grouped_orders, + "page_name": "fsm_order", + "pager": pager, + "default_url": "/my/fsm_orders", + "searchbar_sortings": searchbar_sortings, + "searchbar_groupby": searchbar_groupby, + "searchbar_inputs": searchbar_inputs, + "search_in": search_in, + "sortby": sortby, + "groupby": groupby, + "searchbar_filters": searchbar_filters, + "filterby": filterby, + } + ) + return request.render("fieldservice_portal.portal_my_fsm_orders", values) + + @http.route( + ["/my/fsm_order/"], + type="http", + website=True, + ) + def portal_my_fsm_order(self, order_id=None, **kw): + try: + order_sudo = self._fsm_order_check_access(order_id) + except AccessError: + return request.redirect("/my") + values = self.fsm_order_get_page_view_values(order_sudo, **kw) + return request.render( + "fieldservice_portal.portal_fieldservice_order_page", values + ) diff --git a/fieldservice_portal/demo/fsm_location_demo.xml b/fieldservice_portal/demo/fsm_location_demo.xml new file mode 100644 index 0000000000..d2137b34d0 --- /dev/null +++ b/fieldservice_portal/demo/fsm_location_demo.xml @@ -0,0 +1,6 @@ + + + + + + diff --git a/fieldservice_portal/demo/fsm_order_demo.xml b/fieldservice_portal/demo/fsm_order_demo.xml new file mode 100644 index 0000000000..0bde574bd9 --- /dev/null +++ b/fieldservice_portal/demo/fsm_order_demo.xml @@ -0,0 +1,8 @@ + + + + Demo Order + Description for the new demo order + + + diff --git a/fieldservice_portal/readme/CONTRIBUTORS.rst b/fieldservice_portal/readme/CONTRIBUTORS.rst new file mode 100644 index 0000000000..e4ec89c4ce --- /dev/null +++ b/fieldservice_portal/readme/CONTRIBUTORS.rst @@ -0,0 +1,3 @@ +* `PyTech SRL `_: + + * Alessio Renda diff --git a/fieldservice_portal/readme/DESCRIPTION.rst b/fieldservice_portal/readme/DESCRIPTION.rst new file mode 100644 index 0000000000..13440bbbf7 --- /dev/null +++ b/fieldservice_portal/readme/DESCRIPTION.rst @@ -0,0 +1,2 @@ +Bridge module between fieldservice and portal that allows portal users +to monitor work orders related to their locations. diff --git a/fieldservice_portal/security/ir.model.access.csv b/fieldservice_portal/security/ir.model.access.csv new file mode 100644 index 0000000000..4161fb7bcf --- /dev/null +++ b/fieldservice_portal/security/ir.model.access.csv @@ -0,0 +1,4 @@ +id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink +access_fsm_order_portal,fieldservice.order.portal,fieldservice.model_fsm_order,base.group_portal,1,0,0,0 +access_fsm_stage_portal,fieldservice.stage.portal,fieldservice.model_fsm_stage,base.group_portal,1,0,0,0 +access_fsm_order_type_portal,fieldservice.order.type.portal,fieldservice.model_fsm_order_type,base.group_portal,1,0,0,0 diff --git a/fieldservice_portal/security/portal_security.xml b/fieldservice_portal/security/portal_security.xml new file mode 100644 index 0000000000..9e3dea0ccf --- /dev/null +++ b/fieldservice_portal/security/portal_security.xml @@ -0,0 +1,36 @@ + + + + + Portal Personal Orders + + ['|','|', + ('location_id.owner_id', 'child_of', [user.commercial_partner_id.id]), + ('location_id.contact_id', 'child_of', [user.commercial_partner_id.id]), + ('message_partner_ids','child_of',[user.commercial_partner_id.id])] + + + + + Portal Personal Locations + + ['|','|', + ('owner_id', 'child_of', [user.commercial_partner_id.id]), + ('contact_id', 'child_of', [user.commercial_partner_id.id]), + ('message_partner_ids','child_of',[user.commercial_partner_id.id])] + + + + + Portal Personal Locations + + ['&', + ('service_location_id', '!=', False), + '|','|', + ('service_location_id.owner_id', 'child_of', [user.commercial_partner_id.id]), + ('service_location_id.contact_id', 'child_of', [user.commercial_partner_id.id]), + ('service_location_id.message_partner_ids','child_of', [user.commercial_partner_id.id])] + + + + diff --git a/fieldservice_portal/static/description/index.html b/fieldservice_portal/static/description/index.html new file mode 100644 index 0000000000..509e880adb --- /dev/null +++ b/fieldservice_portal/static/description/index.html @@ -0,0 +1,432 @@ + + + + + +Field Service - Portal + + + +
+

Field Service - Portal

+ + +

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

+

Bridge module between fieldservice and portal that allows portal users +to monitor work orders related to their locations.

+

Table of contents

+ +
+

Bug Tracker

+

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

+

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

+
+
+

Credits

+
+

Authors

+
    +
  • PyTech SRL
  • +
+
+
+

Contributors

+ +
+
+

Maintainers

+

This module is maintained by the OCA.

+ +Odoo Community Association + +

OCA, or the Odoo Community Association, is a nonprofit organization whose +mission is to support the collaborative development of Odoo features and +promote its widespread use.

+

Current maintainers:

+

aleuffre renda-dev

+

This module is part of the OCA/field-service project on GitHub.

+

You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute.

+
+
+
+ + diff --git a/fieldservice_portal/static/src/js/fsm_order_portal.esm.js b/fieldservice_portal/static/src/js/fsm_order_portal.esm.js new file mode 100644 index 0000000000..8788bd77e1 --- /dev/null +++ b/fieldservice_portal/static/src/js/fsm_order_portal.esm.js @@ -0,0 +1,13 @@ +/** @odoo-module */ + +import "portal.portal"; // Force dependencies +import publicWidget from "web.public.widget"; + +publicWidget.registry.PortalHomeCounters.include({ + /** + * @override + */ + _getCountersAlwaysDisplayed() { + return this._super(...arguments).concat(["fsm_order_count"]); + }, +}); diff --git a/fieldservice_portal/tests/__init__.py b/fieldservice_portal/tests/__init__.py new file mode 100644 index 0000000000..8307da4c18 --- /dev/null +++ b/fieldservice_portal/tests/__init__.py @@ -0,0 +1 @@ +from . import test_portal diff --git a/fieldservice_portal/tests/test_portal.py b/fieldservice_portal/tests/test_portal.py new file mode 100644 index 0000000000..dfa3881861 --- /dev/null +++ b/fieldservice_portal/tests/test_portal.py @@ -0,0 +1,171 @@ +import json + +from odoo.exceptions import AccessError +from odoo.http import Request +from odoo.tests.common import HttpCase, TransactionCase, tagged + + +@tagged("post_install", "-at_install") +class TestUsersHttp(HttpCase, TransactionCase): + def test_fsm_order_portal(self): + # Accessing work order of the portal user through route APIs available + login = "portal" + self.authenticate(login, login) + response = self.url_open( + "/my/fsm_orders", + data={ + "validation": login, + "password": login, + "csrf_token": Request.csrf_token(self), + }, + ) + + # Check successful response from API + self.assertEqual(response.status_code, 200) + + login = "demo" + self.authenticate(login, login) + response = self.url_open( + "/my/fsm_orders", + data={ + "validation": login, + "password": login, + "csrf_token": Request.csrf_token(self), + }, + ) + + # Check Forbidden response from API + self.assertEqual(response.status_code, 403) + + def test_fsm_order_access(self): + order_id = self.env["fsm.order"].search([])[0].id + login = "portal" + self.authenticate(login, login) + response = self.url_open( + "/my/fsm_order/" + str(order_id), + data={ + "validation": login, + "password": login, + "csrf_token": Request.csrf_token(self), + }, + ) + self.assertEqual(response.status_code, 200) + + def test_fsm_order_access_denied(self): + # create a Res Partner to be converted to FSM Location/Person + test_loc_partner = self.env["res.partner"].create( + {"name": "Test Loc Partner", "phone": "ABC", "email": "tlp@email.com"} + ) + # create FSM Location and assign it to different user other than Portal User + test_location = self.env["fsm.location"].create( + { + "name": "Test Location No Portal User", + "phone": "123", + "email": "tp@email.com", + "customer_id": test_loc_partner.id, + "partner_id": test_loc_partner.id, + "owner_id": test_loc_partner.id, + } + ) + order = self.env["fsm.order"].create( + { + "location_id": test_location.id, + } + ) + + # Trying to access fsm_order which is not + # assigned to Portal User to check access error + login = "portal" + self.authenticate(login, login) + self.url_open( + "/my/fsm_order/" + str(order.id), + data={ + "validation": login, + "password": login, + "csrf_token": Request.csrf_token(self), + }, + ) + self.assertRaises( + AccessError, msg="Access Denied by record rules for operation: read" + ) + + def test_fsm_order_kw_usage(self): + order_id = self.env["fsm.order"].search([])[0].id + # Trying to access fsm_order url + # with query parameters + login = "portal" + self.authenticate(login, login) + response = self.url_open( + "/my/fsm_order/" + str(order_id) + "?success='success'", + data={ + "validation": login, + "password": login, + "csrf_token": Request.csrf_token(self), + }, + ) + self.assertEqual(response.status_code, 200) + + def test_fsm_no_fsm_order_present(self): + # Trying to filter fsm_orders based on filter + login = "portal" + self.authenticate(login, login) + response = self.url_open( + "/my/fsm_orders?groupby=none&filterby=Completed&page=1&search_in=&search=", + data={ + "validation": login, + "password": login, + "csrf_token": Request.csrf_token(self), + }, + ) + self.assertEqual(response.status_code, 200) + self.assertNotIn("", response.text) + self.assertIn("

There are no Work Orders in your account.

", response.text) + + def test_fsm_order_filter_usage(self): + # Trying to filter fsm_orders based on filter, group and sort + login = "portal" + self.authenticate(login, login) + response = self.url_open( + "/my/fsm_orders?groupby=stage_id&filterby=New&sortby=location", + data={ + "validation": login, + "password": login, + "csrf_token": Request.csrf_token(self), + }, + ) + self.assertEqual(response.status_code, 200) + self.assertIn("", response.text) + self.assertIn("Demo Order", response.text) + + def test_fsm_orders_portal_home(self): + login = "portal" + self.authenticate(login, login) + response = self.url_open( + "/my/home", + data={ + "validation": login, + "password": login, + "csrf_token": Request.csrf_token(self), + }, + ) + self.assertEqual(response.status_code, 200) + self.assertIn("FSM Orders", response.text) + + def test_fsm_orders_count(self): + login = "portal" + self.authenticate(login, login) + response = self.url_open( + "/my/counters", + data=json.dumps( + { + "validation": login, + "password": login, + "csrf_token": Request.csrf_token(self), + "params": { + "counters": "fsm_order_count", + }, + } + ).encode(), + headers={"Content-Type": "application/json"}, + ).json() + self.assertEqual(response["result"]["fsm_order_count"], 1) diff --git a/fieldservice_portal/views/fsm_order_template.xml b/fieldservice_portal/views/fsm_order_template.xml new file mode 100644 index 0000000000..00a88e50d9 --- /dev/null +++ b/fieldservice_portal/views/fsm_order_template.xml @@ -0,0 +1,148 @@ + + + + + + + diff --git a/fieldservice_portal/views/portal_template.xml b/fieldservice_portal/views/portal_template.xml new file mode 100644 index 0000000000..03c9d4d17b --- /dev/null +++ b/fieldservice_portal/views/portal_template.xml @@ -0,0 +1,41 @@ + + + + + + diff --git a/setup/fieldservice_portal/odoo/addons/fieldservice_portal b/setup/fieldservice_portal/odoo/addons/fieldservice_portal new file mode 120000 index 0000000000..02f7e77a7f --- /dev/null +++ b/setup/fieldservice_portal/odoo/addons/fieldservice_portal @@ -0,0 +1 @@ +../../../../fieldservice_portal \ No newline at end of file diff --git a/setup/fieldservice_portal/setup.py b/setup/fieldservice_portal/setup.py new file mode 100644 index 0000000000..28c57bb640 --- /dev/null +++ b/setup/fieldservice_portal/setup.py @@ -0,0 +1,6 @@ +import setuptools + +setuptools.setup( + setup_requires=['setuptools-odoo'], + odoo_addon=True, +)