diff --git a/fieldservice_equipment_calibration/README.rst b/fieldservice_equipment_calibration/README.rst new file mode 100644 index 0000000000..d510834a72 --- /dev/null +++ b/fieldservice_equipment_calibration/README.rst @@ -0,0 +1,103 @@ +===================================== +Field Service - Equipment Calibration +===================================== + +.. + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! This file is generated by oca-gen-addon-readme !! + !! changes will be overwritten. !! + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! source digest: sha256:1d8e9aba350f66b184958c23db8284bef6abaf54ee5bc60d9fe38afe2605aea5 + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + +.. |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/18.0/fieldservice_equipment_calibration + :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-18-0/field-service-18-0-fieldservice_equipment_calibration + :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=18.0 + :alt: Try me on Runboat + +|badge1| |badge2| |badge3| |badge4| |badge5| + +Track calibration certificates of your field service equipments. + +Each certificate records a validity period (initial and expiration +dates), an attached certificate file and free-form notes, with full +chatter/activity tracking. Certificates are listed on a **Calibration** +tab of the equipment form and under *Field Service > Master Data > +Calibration Certificates*. + +Archiving a certificate (e.g. when superseded by a new calibration) +keeps it available for traceability without cluttering the active list. + +**Table of contents** + +.. contents:: + :local: + +Usage +===== + +1. Open an equipment (*Field Service > Master Data > Equipments*). +2. On the **Calibration** tab, add a certificate with its validity dates + and attach the certificate file. +3. Follow expirations from *Field Service > Master Data > Calibration + Certificates* (filter or group by expiration date). + +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 +------- + +* Pop Solutions + +Contributors +------------ + +- Rafnix Guzman +- Marcos Mendez + +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-marcos-mendez| image:: https://github.com/marcos-mendez.png?size=40px + :target: https://github.com/marcos-mendez + :alt: marcos-mendez + +Current `maintainer `__: + +|maintainer-marcos-mendez| + +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_equipment_calibration/__init__.py b/fieldservice_equipment_calibration/__init__.py new file mode 100644 index 0000000000..0650744f6b --- /dev/null +++ b/fieldservice_equipment_calibration/__init__.py @@ -0,0 +1 @@ +from . import models diff --git a/fieldservice_equipment_calibration/__manifest__.py b/fieldservice_equipment_calibration/__manifest__.py new file mode 100644 index 0000000000..fa3943cd08 --- /dev/null +++ b/fieldservice_equipment_calibration/__manifest__.py @@ -0,0 +1,19 @@ +# Copyright (C) 2022 Rafnix Guzman +# Copyright (C) 2026 Pop Solutions +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). +{ + "name": "Field Service - Equipment Calibration", + "summary": "Track calibration certificates of field service equipments", + "version": "18.0.1.0.0", + "license": "AGPL-3", + "website": "https://github.com/OCA/field-service", + "category": "Field Service", + "author": "Pop Solutions, Odoo Community Association (OCA)", + "maintainers": ["marcos-mendez"], + "depends": ["fieldservice"], + "data": [ + "security/ir.model.access.csv", + "views/fsm_calibration_certificate_views.xml", + "views/fsm_equipment_views.xml", + ], +} diff --git a/fieldservice_equipment_calibration/models/__init__.py b/fieldservice_equipment_calibration/models/__init__.py new file mode 100644 index 0000000000..d4a519cbf5 --- /dev/null +++ b/fieldservice_equipment_calibration/models/__init__.py @@ -0,0 +1,2 @@ +from . import fsm_calibration_certificate +from . import fsm_equipment diff --git a/fieldservice_equipment_calibration/models/fsm_calibration_certificate.py b/fieldservice_equipment_calibration/models/fsm_calibration_certificate.py new file mode 100644 index 0000000000..f8ae3992e9 --- /dev/null +++ b/fieldservice_equipment_calibration/models/fsm_calibration_certificate.py @@ -0,0 +1,20 @@ +# Copyright (C) 2022 Rafnix Guzman +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from odoo import fields, models + + +class FsmCalibrationCertificate(models.Model): + _name = "fsm.calibration.certificate" + _description = "Calibration Certificate" + _inherit = ["mail.thread", "mail.activity.mixin"] + + name = fields.Char(required=True, tracking=True) + initial_date = fields.Date(tracking=True) + expiration_date = fields.Date(tracking=True) + notes = fields.Text() + fsm_equipment_id = fields.Many2one( + "fsm.equipment", string="Equipment", tracking=True + ) + certificate_file = fields.Binary(tracking=True, attachment=True) + active = fields.Boolean(default=True, tracking=True) diff --git a/fieldservice_equipment_calibration/models/fsm_equipment.py b/fieldservice_equipment_calibration/models/fsm_equipment.py new file mode 100644 index 0000000000..03b64d09bf --- /dev/null +++ b/fieldservice_equipment_calibration/models/fsm_equipment.py @@ -0,0 +1,14 @@ +# Copyright (C) 2022 Rafnix Guzman +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from odoo import fields, models + + +class FsmEquipment(models.Model): + _inherit = "fsm.equipment" + + fsm_calibration_certificate_ids = fields.One2many( + "fsm.calibration.certificate", + "fsm_equipment_id", + string="Calibration Certificates", + ) diff --git a/fieldservice_equipment_calibration/pyproject.toml b/fieldservice_equipment_calibration/pyproject.toml new file mode 100644 index 0000000000..4231d0cccb --- /dev/null +++ b/fieldservice_equipment_calibration/pyproject.toml @@ -0,0 +1,3 @@ +[build-system] +requires = ["whool"] +build-backend = "whool.buildapi" diff --git a/fieldservice_equipment_calibration/readme/CONTRIBUTORS.md b/fieldservice_equipment_calibration/readme/CONTRIBUTORS.md new file mode 100644 index 0000000000..c2dba6666f --- /dev/null +++ b/fieldservice_equipment_calibration/readme/CONTRIBUTORS.md @@ -0,0 +1,2 @@ +- Rafnix Guzman \ +- Marcos Mendez \ diff --git a/fieldservice_equipment_calibration/readme/DESCRIPTION.md b/fieldservice_equipment_calibration/readme/DESCRIPTION.md new file mode 100644 index 0000000000..69fd010f89 --- /dev/null +++ b/fieldservice_equipment_calibration/readme/DESCRIPTION.md @@ -0,0 +1,9 @@ +Track calibration certificates of your field service equipments. + +Each certificate records a validity period (initial and expiration dates), an +attached certificate file and free-form notes, with full chatter/activity +tracking. Certificates are listed on a **Calibration** tab of the equipment +form and under *Field Service > Master Data > Calibration Certificates*. + +Archiving a certificate (e.g. when superseded by a new calibration) keeps it +available for traceability without cluttering the active list. diff --git a/fieldservice_equipment_calibration/readme/USAGE.md b/fieldservice_equipment_calibration/readme/USAGE.md new file mode 100644 index 0000000000..78f534eb49 --- /dev/null +++ b/fieldservice_equipment_calibration/readme/USAGE.md @@ -0,0 +1,5 @@ +1. Open an equipment (*Field Service > Master Data > Equipments*). +2. On the **Calibration** tab, add a certificate with its validity dates and + attach the certificate file. +3. Follow expirations from *Field Service > Master Data > Calibration + Certificates* (filter or group by expiration date). diff --git a/fieldservice_equipment_calibration/security/ir.model.access.csv b/fieldservice_equipment_calibration/security/ir.model.access.csv new file mode 100644 index 0000000000..bc7502704b --- /dev/null +++ b/fieldservice_equipment_calibration/security/ir.model.access.csv @@ -0,0 +1,3 @@ +id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink +access_fsm_calibration_certificate_fsm_user,fsm.calibration.certificate fsm user,model_fsm_calibration_certificate,fieldservice.group_fsm_user_own,1,0,0,0 +access_fsm_calibration_certificate_fsm_manager,fsm.calibration.certificate fsm manager,model_fsm_calibration_certificate,fieldservice.group_fsm_manager,1,1,1,1 diff --git a/fieldservice_equipment_calibration/static/description/index.html b/fieldservice_equipment_calibration/static/description/index.html new file mode 100644 index 0000000000..86a2ce20cd --- /dev/null +++ b/fieldservice_equipment_calibration/static/description/index.html @@ -0,0 +1,444 @@ + + + + + +Field Service - Equipment Calibration + + + +
+

Field Service - Equipment Calibration

+ + +

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

+

Track calibration certificates of your field service equipments.

+

Each certificate records a validity period (initial and expiration +dates), an attached certificate file and free-form notes, with full +chatter/activity tracking. Certificates are listed on a Calibration +tab of the equipment form and under Field Service > Master Data > +Calibration Certificates.

+

Archiving a certificate (e.g. when superseded by a new calibration) +keeps it available for traceability without cluttering the active list.

+

Table of contents

+ +
+

Usage

+
    +
  1. Open an equipment (Field Service > Master Data > Equipments).
  2. +
  3. On the Calibration tab, add a certificate with its validity dates +and attach the certificate file.
  4. +
  5. Follow expirations from Field Service > Master Data > Calibration +Certificates (filter or group by expiration date).
  6. +
+
+
+

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

+
    +
  • Pop Solutions
  • +
+
+
+

Contributors

+ +
+
+

Maintainers

+

This module is maintained by the OCA.

+ +Odoo Community Association + +

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

+

Current maintainer:

+

marcos-mendez

+

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_equipment_calibration/tests/__init__.py b/fieldservice_equipment_calibration/tests/__init__.py new file mode 100644 index 0000000000..48fbeea7d0 --- /dev/null +++ b/fieldservice_equipment_calibration/tests/__init__.py @@ -0,0 +1 @@ +from . import test_calibration_certificate diff --git a/fieldservice_equipment_calibration/tests/test_calibration_certificate.py b/fieldservice_equipment_calibration/tests/test_calibration_certificate.py new file mode 100644 index 0000000000..d579ca6bab --- /dev/null +++ b/fieldservice_equipment_calibration/tests/test_calibration_certificate.py @@ -0,0 +1,41 @@ +# Copyright (C) 2026 Pop Solutions +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from odoo.tests.common import TransactionCase + + +class TestCalibrationCertificate(TransactionCase): + @classmethod + def setUpClass(cls): + super().setUpClass() + cls.equipment = cls.env["fsm.equipment"].create( + {"name": "Test Calibrated Equipment"} + ) + + def test_certificate_lifecycle(self): + certificate = self.env["fsm.calibration.certificate"].create( + { + "name": "CAL-2026-001", + "initial_date": "2026-01-01", + "expiration_date": "2027-01-01", + "fsm_equipment_id": self.equipment.id, + } + ) + self.assertTrue(certificate.active) + self.assertEqual(self.equipment.fsm_calibration_certificate_ids, certificate) + + def test_archived_certificate_leaves_active_list(self): + certificate = self.env["fsm.calibration.certificate"].create( + { + "name": "CAL-2026-002", + "fsm_equipment_id": self.equipment.id, + } + ) + certificate.active = False + self.assertNotIn(certificate, self.equipment.fsm_calibration_certificate_ids) + self.assertIn( + certificate, + self.env["fsm.calibration.certificate"] + .with_context(active_test=False) + .search([("fsm_equipment_id", "=", self.equipment.id)]), + ) diff --git a/fieldservice_equipment_calibration/views/fsm_calibration_certificate_views.xml b/fieldservice_equipment_calibration/views/fsm_calibration_certificate_views.xml new file mode 100644 index 0000000000..b057a2e55c --- /dev/null +++ b/fieldservice_equipment_calibration/views/fsm_calibration_certificate_views.xml @@ -0,0 +1,60 @@ + + + + fsm.calibration.certificate.list + fsm.calibration.certificate + + + + + + + + + + + fsm.calibration.certificate.form + fsm.calibration.certificate + +
+ + + + + + + + + + + + + + + + +
+
+ + Calibration Certificates + fsm.calibration.certificate + list,form + +

+ Create a new calibration certificate. +

+
+
+ +
diff --git a/fieldservice_equipment_calibration/views/fsm_equipment_views.xml b/fieldservice_equipment_calibration/views/fsm_equipment_views.xml new file mode 100644 index 0000000000..094269e9c2 --- /dev/null +++ b/fieldservice_equipment_calibration/views/fsm_equipment_views.xml @@ -0,0 +1,21 @@ + + + + fsm.equipment.form.calibration + fsm.equipment + + + + + + + + + + + + + + + + diff --git a/fieldservice_equipment_calibration_portal/README.rst b/fieldservice_equipment_calibration_portal/README.rst new file mode 100644 index 0000000000..2df0c1e1a7 --- /dev/null +++ b/fieldservice_equipment_calibration_portal/README.rst @@ -0,0 +1,93 @@ +============================================ +Field Service - Equipment Calibration Portal +============================================ + +.. + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! This file is generated by oca-gen-addon-readme !! + !! changes will be overwritten. !! + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! source digest: sha256:8fdf4e36da9868296af4ecafd30cba71de89710f464a5f77dd10d2428162df77 + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + +.. |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/18.0/fieldservice_equipment_calibration_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-18-0/field-service-18-0-fieldservice_equipment_calibration_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=18.0 + :alt: Try me on Runboat + +|badge1| |badge2| |badge3| |badge4| |badge5| + +Show and download the calibration certificates of an equipment on its +portal page. + +**Table of contents** + +.. contents:: + :local: + +Usage +===== + +Install the module: the equipment portal page gains a "Calibration +Certificates" section with download links. Downloads honor the same +access rules as the page (portal user, access token or the Public +Equipment Pages setting). + +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 +------- + +* Pop Solutions + +Contributors +------------ + +- Marcos Mendez marcos@popsolutions.co + +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-marcos-mendez| image:: https://github.com/marcos-mendez.png?size=40px + :target: https://github.com/marcos-mendez + :alt: marcos-mendez + +Current `maintainer `__: + +|maintainer-marcos-mendez| + +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_equipment_calibration_portal/__init__.py b/fieldservice_equipment_calibration_portal/__init__.py new file mode 100644 index 0000000000..e046e49fbe --- /dev/null +++ b/fieldservice_equipment_calibration_portal/__init__.py @@ -0,0 +1 @@ +from . import controllers diff --git a/fieldservice_equipment_calibration_portal/__manifest__.py b/fieldservice_equipment_calibration_portal/__manifest__.py new file mode 100644 index 0000000000..a627c57856 --- /dev/null +++ b/fieldservice_equipment_calibration_portal/__manifest__.py @@ -0,0 +1,15 @@ +# Copyright (C) 2026 Pop Solutions +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). +{ + "name": "Field Service - Equipment Calibration Portal", + "summary": "Show and download equipment calibration certificates " + "on the portal page", + "version": "18.0.1.0.0", + "license": "AGPL-3", + "website": "https://github.com/OCA/field-service", + "category": "Field Service", + "author": "Pop Solutions, Odoo Community Association (OCA)", + "maintainers": ["marcos-mendez"], + "depends": ["fieldservice_equipment_portal", "fieldservice_equipment_calibration"], + "data": ["views/portal_templates.xml"], +} diff --git a/fieldservice_equipment_calibration_portal/controllers/__init__.py b/fieldservice_equipment_calibration_portal/controllers/__init__.py new file mode 100644 index 0000000000..12a7e529b6 --- /dev/null +++ b/fieldservice_equipment_calibration_portal/controllers/__init__.py @@ -0,0 +1 @@ +from . import main diff --git a/fieldservice_equipment_calibration_portal/controllers/main.py b/fieldservice_equipment_calibration_portal/controllers/main.py new file mode 100644 index 0000000000..52cc76d738 --- /dev/null +++ b/fieldservice_equipment_calibration_portal/controllers/main.py @@ -0,0 +1,44 @@ +# Copyright (C) 2026 Pop Solutions +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from odoo import http +from odoo.exceptions import AccessError, MissingError +from odoo.http import request + +from odoo.addons.fieldservice_equipment_portal.controllers.main import PortalEquipment + + +class PortalEquipmentCalibration(PortalEquipment): + @http.route( + "/my/equipments/calibration//download", + type="http", + auth="public", + website=True, + ) + def portal_equipment_calibration_download( + self, certificate_id, access_token=None, **kw + ): + """Certificate download honoring the same access rules as the + equipment page (portal user / access token / public setting).""" + certificate = ( + request.env["fsm.calibration.certificate"] + .sudo() + .browse(certificate_id) + .exists() + ) + if not certificate or not certificate.certificate_file: + raise request.not_found() + try: + self._document_check_access( + "fsm.equipment", certificate.fsm_equipment_id.id, access_token + ) + except (AccessError, MissingError): + if not self._equipment_public_access_enabled(): + return request.redirect("/my") + return ( + request.env["ir.binary"] + ._get_stream_from( + certificate, "certificate_file", filename=f"{certificate.name}.pdf" + ) + .get_response(as_attachment=True) + ) diff --git a/fieldservice_equipment_calibration_portal/pyproject.toml b/fieldservice_equipment_calibration_portal/pyproject.toml new file mode 100644 index 0000000000..4231d0cccb --- /dev/null +++ b/fieldservice_equipment_calibration_portal/pyproject.toml @@ -0,0 +1,3 @@ +[build-system] +requires = ["whool"] +build-backend = "whool.buildapi" diff --git a/fieldservice_equipment_calibration_portal/readme/CONTRIBUTORS.md b/fieldservice_equipment_calibration_portal/readme/CONTRIBUTORS.md new file mode 100644 index 0000000000..d7c7c24430 --- /dev/null +++ b/fieldservice_equipment_calibration_portal/readme/CONTRIBUTORS.md @@ -0,0 +1 @@ +- Marcos Mendez diff --git a/fieldservice_equipment_calibration_portal/readme/DESCRIPTION.md b/fieldservice_equipment_calibration_portal/readme/DESCRIPTION.md new file mode 100644 index 0000000000..102ec7bf5d --- /dev/null +++ b/fieldservice_equipment_calibration_portal/readme/DESCRIPTION.md @@ -0,0 +1 @@ +Show and download the calibration certificates of an equipment on its portal page. diff --git a/fieldservice_equipment_calibration_portal/readme/USAGE.md b/fieldservice_equipment_calibration_portal/readme/USAGE.md new file mode 100644 index 0000000000..bb6edfe9b2 --- /dev/null +++ b/fieldservice_equipment_calibration_portal/readme/USAGE.md @@ -0,0 +1 @@ +Install the module: the equipment portal page gains a "Calibration Certificates" section with download links. Downloads honor the same access rules as the page (portal user, access token or the Public Equipment Pages setting). diff --git a/fieldservice_equipment_calibration_portal/static/description/index.html b/fieldservice_equipment_calibration_portal/static/description/index.html new file mode 100644 index 0000000000..17e7c6f0b9 --- /dev/null +++ b/fieldservice_equipment_calibration_portal/static/description/index.html @@ -0,0 +1,434 @@ + + + + + +Field Service - Equipment Calibration Portal + + + +
+

Field Service - Equipment Calibration Portal

+ + +

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

+

Show and download the calibration certificates of an equipment on its +portal page.

+

Table of contents

+ +
+

Usage

+

Install the module: the equipment portal page gains a “Calibration +Certificates” section with download links. Downloads honor the same +access rules as the page (portal user, access token or the Public +Equipment Pages setting).

+
+
+

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

+
    +
  • Pop Solutions
  • +
+
+
+

Contributors

+ +
+
+

Maintainers

+

This module is maintained by the OCA.

+ +Odoo Community Association + +

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

+

Current maintainer:

+

marcos-mendez

+

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_equipment_calibration_portal/tests/__init__.py b/fieldservice_equipment_calibration_portal/tests/__init__.py new file mode 100644 index 0000000000..287ace8fb8 --- /dev/null +++ b/fieldservice_equipment_calibration_portal/tests/__init__.py @@ -0,0 +1 @@ +from . import test_calibration_portal diff --git a/fieldservice_equipment_calibration_portal/tests/test_calibration_portal.py b/fieldservice_equipment_calibration_portal/tests/test_calibration_portal.py new file mode 100644 index 0000000000..f8c1b5409d --- /dev/null +++ b/fieldservice_equipment_calibration_portal/tests/test_calibration_portal.py @@ -0,0 +1,75 @@ +# Copyright (C) 2026 Pop Solutions +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +import base64 + +from odoo.tests import HttpCase, tagged + + +@tagged("post_install", "-at_install") +class TestEquipmentCalibrationPortal(HttpCase): + @classmethod + def setUpClass(cls): + super().setUpClass() + cls.partner = cls.env["res.partner"].create({"name": "Calibration Owner"}) + cls.portal_user = ( + cls.env["res.users"] + .with_context(no_reset_password=True) + .create( + { + "login": "portal-calibration@test.example.com", + "name": "Portal Calibration User", + "partner_id": cls.partner.id, + "groups_id": [(6, 0, [cls.env.ref("base.group_portal").id])], + "password": "portal-calibration", + } + ) + ) + cls.equipment = cls.env["fsm.equipment"].create( + {"name": "Calibrated Equipment", "owned_by_id": cls.partner.id} + ) + cls.certificate = cls.env["fsm.calibration.certificate"].create( + { + "name": "CAL-2026-01", + "fsm_equipment_id": cls.equipment.id, + "certificate_file": base64.b64encode(b"%PDF-1.4 cal test"), + } + ) + + def test_page_lists_certificates(self): + self.authenticate("portal-calibration@test.example.com", "portal-calibration") + response = self.url_open(f"/my/equipments/{self.equipment.id}") + self.assertEqual(response.status_code, 200) + self.assertIn(b"Calibration Certificates", response.content) + self.assertIn(b"CAL-2026-01", response.content) + + def test_portal_user_downloads_certificate(self): + self.authenticate("portal-calibration@test.example.com", "portal-calibration") + response = self.url_open( + f"/my/equipments/calibration/{self.certificate.id}/download" + ) + self.assertEqual(response.status_code, 200) + self.assertIn(b"%PDF-1.4", response.content) + + def test_anonymous_download_respects_public_setting(self): + response = self.url_open( + f"/my/equipments/calibration/{self.certificate.id}/download" + ) + self.assertNotIn(b"%PDF-1.4", response.content) + self.env["ir.config_parameter"].sudo().set_param( + "fieldservice_equipment_portal.public_access", "True" + ) + response = self.url_open( + f"/my/equipments/calibration/{self.certificate.id}/download" + ) + self.assertEqual(response.status_code, 200) + self.assertIn(b"%PDF-1.4", response.content) + + def test_download_missing_certificate_404(self): + response = self.url_open("/my/equipments/calibration/99999999/download") + self.assertEqual(response.status_code, 404) + empty = self.env["fsm.calibration.certificate"].create( + {"name": "SEM-ARQUIVO", "fsm_equipment_id": self.equipment.id} + ) + response = self.url_open(f"/my/equipments/calibration/{empty.id}/download") + self.assertEqual(response.status_code, 404) diff --git a/fieldservice_equipment_calibration_portal/views/portal_templates.xml b/fieldservice_equipment_calibration_portal/views/portal_templates.xml new file mode 100644 index 0000000000..039f83286d --- /dev/null +++ b/fieldservice_equipment_calibration_portal/views/portal_templates.xml @@ -0,0 +1,31 @@ + + + + diff --git a/fieldservice_equipment_portal/README.rst b/fieldservice_equipment_portal/README.rst new file mode 100644 index 0000000000..d20971f534 --- /dev/null +++ b/fieldservice_equipment_portal/README.rst @@ -0,0 +1,107 @@ +================================ +Field Service - Equipment Portal +================================ + +.. + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! This file is generated by oca-gen-addon-readme !! + !! changes will be overwritten. !! + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! source digest: sha256:e182c368f118fa42632876a887e15e29cc69f5cba71e5f84ba91ca683894b6d9 + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + +.. |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/18.0/fieldservice_equipment_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-18-0/field-service-18-0-fieldservice_equipment_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=18.0 + :alt: Try me on Runboat + +|badge1| |badge2| |badge3| |badge4| |badge5| + +This module lets portal users see the field service equipments owned or +managed by their commercial partner (or equipments they follow) in the +"My Account" area of the portal. + +It adds: + +- an **Equipments** card on the portal home page with a record counter, +- a paginated list at ``/my/equipments``, +- a detail page per equipment (product, location, owner, notes), + shareable with a portal access token — a stable URL well suited as a + QR code target on physical equipment labels (see + ``fieldservice_equipment_portal_qrcode``). + +**Table of contents** + +.. contents:: + :local: + +Usage +===== + +1. Grant a customer contact portal access (Settings > Users, "Grant + portal access"). +2. As that portal user, open **My Account**: an **Equipments** card + lists the equipments whose owner or manager belongs to the user's + company, plus any equipment the user follows. +3. Internal users can share a direct link with a token from the + equipment form ("Share" behavior provided by ``portal.mixin``). + +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 +------- + +* Pop Solutions + +Contributors +------------ + +- Rafnix Guzman +- Marcos Mendez + +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-marcos-mendez| image:: https://github.com/marcos-mendez.png?size=40px + :target: https://github.com/marcos-mendez + :alt: marcos-mendez + +Current `maintainer `__: + +|maintainer-marcos-mendez| + +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_equipment_portal/__init__.py b/fieldservice_equipment_portal/__init__.py new file mode 100644 index 0000000000..91c5580fed --- /dev/null +++ b/fieldservice_equipment_portal/__init__.py @@ -0,0 +1,2 @@ +from . import controllers +from . import models diff --git a/fieldservice_equipment_portal/__manifest__.py b/fieldservice_equipment_portal/__manifest__.py new file mode 100644 index 0000000000..17d46156db --- /dev/null +++ b/fieldservice_equipment_portal/__manifest__.py @@ -0,0 +1,19 @@ +# Copyright (C) 2026 Pop Solutions +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). +{ + "name": "Field Service - Equipment Portal", + "summary": "Let portal users see their field service equipments", + "version": "18.0.1.2.0", + "license": "AGPL-3", + "website": "https://github.com/OCA/field-service", + "category": "Field Service", + "author": "Pop Solutions, Odoo Community Association (OCA)", + "maintainers": ["marcos-mendez"], + "depends": ["portal", "fieldservice"], + "data": [ + "security/ir.model.access.csv", + "security/fsm_equipment_security.xml", + "views/portal_templates.xml", + "views/res_config_settings.xml", + ], +} diff --git a/fieldservice_equipment_portal/controllers/__init__.py b/fieldservice_equipment_portal/controllers/__init__.py new file mode 100644 index 0000000000..12a7e529b6 --- /dev/null +++ b/fieldservice_equipment_portal/controllers/__init__.py @@ -0,0 +1 @@ +from . import main diff --git a/fieldservice_equipment_portal/controllers/main.py b/fieldservice_equipment_portal/controllers/main.py new file mode 100644 index 0000000000..d80af49617 --- /dev/null +++ b/fieldservice_equipment_portal/controllers/main.py @@ -0,0 +1,116 @@ +# Copyright (C) 2026 Pop Solutions +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from odoo import _, http +from odoo.exceptions import AccessError, MissingError +from odoo.http import request + +from odoo.addons.portal.controllers.portal import CustomerPortal +from odoo.addons.portal.controllers.portal import pager as portal_pager + + +class PortalEquipment(CustomerPortal): + def _prepare_home_portal_values(self, counters): + values = super()._prepare_home_portal_values(counters) + if "equipment_count" in counters: + equipment_model = request.env["fsm.equipment"] + equipment_count = ( + equipment_model.search_count([]) + if equipment_model.has_access("read") + else 0 + ) + values["equipment_count"] = equipment_count + return values + + def _equipment_get_page_view_values(self, equipment, access_token, **kwargs): + values = { + "page_name": "Equipments", + "equipment": equipment, + # fsm.equipment only gains product_id with fieldservice_equipment_stock + "equipment_has_product": "product_id" in equipment._fields, + } + return self._get_page_view_values( + equipment, access_token, values, "my_equipments_history", False, **kwargs + ) + + def _equipment_public_access_enabled(self): + return bool( + request.env["ir.config_parameter"] + .sudo() + .get_param("fieldservice_equipment_portal.public_access") + ) + + def _get_filter_domain(self, kw): + return [] + + @http.route( + ["/my/equipments", "/my/equipments/page/"], + type="http", + auth="user", + website=True, + ) + def portal_my_equipments(self, page=1, sortby=None, **kw): + values = self._prepare_portal_layout_values() + equipment_model = request.env["fsm.equipment"] + if not equipment_model.has_access("read"): + return request.redirect("/my") + domain = self._get_filter_domain(kw) + searchbar_sortings = { + "name": {"label": _("Name"), "order": "name desc"}, + } + if not sortby: + sortby = "name" + order = searchbar_sortings[sortby]["order"] + equipment_count = equipment_model.search_count(domain) + pager = portal_pager( + url="/my/equipments", + url_args={"sortby": sortby}, + total=equipment_count, + page=page, + step=self._items_per_page, + ) + equipments = equipment_model.search( + domain, order=order, limit=self._items_per_page, offset=pager["offset"] + ) + request.session["my_equipments_history"] = equipments.ids[:100] + values.update( + { + "equipments": equipments, + "page_name": "Equipments", + "pager": pager, + "default_url": "/my/equipments", + "searchbar_sortings": searchbar_sortings, + "sortby": sortby, + } + ) + return request.render( + "fieldservice_equipment_portal.portal_my_equipments", values + ) + + @http.route( + ["/my/equipments/"], + type="http", + auth="public", + website=True, + ) + def portal_my_equipment_detail(self, equipment_id, access_token=None, **kw): + try: + equipment_sudo = self._document_check_access( + "fsm.equipment", equipment_id, access_token + ) + except (AccessError, MissingError): + # Printed QR codes carry no access token: when the public access + # option is enabled, the page is readable without logging in. + if not self._equipment_public_access_enabled(): + return request.redirect("/my") + equipment_sudo = ( + request.env["fsm.equipment"].sudo().browse(equipment_id).exists() + ) + if not equipment_sudo: + return request.redirect("/my") + values = self._equipment_get_page_view_values( + equipment_sudo, access_token, **kw + ) + return request.render( + "fieldservice_equipment_portal.portal_equipment_page", values + ) diff --git a/fieldservice_equipment_portal/models/__init__.py b/fieldservice_equipment_portal/models/__init__.py new file mode 100644 index 0000000000..4246a8e195 --- /dev/null +++ b/fieldservice_equipment_portal/models/__init__.py @@ -0,0 +1,2 @@ +from . import fsm_equipment +from . import res_config_settings diff --git a/fieldservice_equipment_portal/models/fsm_equipment.py b/fieldservice_equipment_portal/models/fsm_equipment.py new file mode 100644 index 0000000000..2cc83bb4b4 --- /dev/null +++ b/fieldservice_equipment_portal/models/fsm_equipment.py @@ -0,0 +1,15 @@ +# Copyright (C) 2026 Pop Solutions +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from odoo import models + + +class FsmEquipment(models.Model): + _name = "fsm.equipment" + _inherit = ["fsm.equipment", "portal.mixin"] + + def _compute_access_url(self): + res = super()._compute_access_url() + for record in self: + record.access_url = f"/my/equipments/{record.id}" + return res diff --git a/fieldservice_equipment_portal/models/res_config_settings.py b/fieldservice_equipment_portal/models/res_config_settings.py new file mode 100644 index 0000000000..97669937d2 --- /dev/null +++ b/fieldservice_equipment_portal/models/res_config_settings.py @@ -0,0 +1,15 @@ +# Copyright (C) 2026 Pop Solutions +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from odoo import fields, models + + +class ResConfigSettings(models.TransientModel): + _inherit = "res.config.settings" + + fsm_equipment_portal_public = fields.Boolean( + string="Public Equipment Pages", + config_parameter="fieldservice_equipment_portal.public_access", + help="Allow anyone with an equipment page link (e.g. scanned from a " + "printed QR code) to view the equipment page without logging in.", + ) diff --git a/fieldservice_equipment_portal/pyproject.toml b/fieldservice_equipment_portal/pyproject.toml new file mode 100644 index 0000000000..4231d0cccb --- /dev/null +++ b/fieldservice_equipment_portal/pyproject.toml @@ -0,0 +1,3 @@ +[build-system] +requires = ["whool"] +build-backend = "whool.buildapi" diff --git a/fieldservice_equipment_portal/readme/CONTRIBUTORS.md b/fieldservice_equipment_portal/readme/CONTRIBUTORS.md new file mode 100644 index 0000000000..c2dba6666f --- /dev/null +++ b/fieldservice_equipment_portal/readme/CONTRIBUTORS.md @@ -0,0 +1,2 @@ +- Rafnix Guzman \ +- Marcos Mendez \ diff --git a/fieldservice_equipment_portal/readme/DESCRIPTION.md b/fieldservice_equipment_portal/readme/DESCRIPTION.md new file mode 100644 index 0000000000..e3a69041e3 --- /dev/null +++ b/fieldservice_equipment_portal/readme/DESCRIPTION.md @@ -0,0 +1,11 @@ +This module lets portal users see the field service equipments owned or +managed by their commercial partner (or equipments they follow) in the +"My Account" area of the portal. + +It adds: + +- an **Equipments** card on the portal home page with a record counter, +- a paginated list at `/my/equipments`, +- a detail page per equipment (product, location, owner, notes), shareable + with a portal access token — a stable URL well suited as a QR code target + on physical equipment labels (see `fieldservice_equipment_portal_qrcode`). diff --git a/fieldservice_equipment_portal/readme/USAGE.md b/fieldservice_equipment_portal/readme/USAGE.md new file mode 100644 index 0000000000..d0349c27ac --- /dev/null +++ b/fieldservice_equipment_portal/readme/USAGE.md @@ -0,0 +1,7 @@ +1. Grant a customer contact portal access (Settings > Users, "Grant portal + access"). +2. As that portal user, open **My Account**: an **Equipments** card lists the + equipments whose owner or manager belongs to the user's company, plus any + equipment the user follows. +3. Internal users can share a direct link with a token from the equipment + form ("Share" behavior provided by `portal.mixin`). diff --git a/fieldservice_equipment_portal/security/fsm_equipment_security.xml b/fieldservice_equipment_portal/security/fsm_equipment_security.xml new file mode 100644 index 0000000000..7fdc789ae2 --- /dev/null +++ b/fieldservice_equipment_portal/security/fsm_equipment_security.xml @@ -0,0 +1,15 @@ + + + + Equipment: portal users see their own + + + ['|', '|', + ('owned_by_id', 'child_of', user.partner_id.commercial_partner_id.id), + ('managed_by_id', 'child_of', user.partner_id.commercial_partner_id.id), + ('message_partner_ids', 'in', [user.partner_id.id]), + ] + + + + diff --git a/fieldservice_equipment_portal/security/ir.model.access.csv b/fieldservice_equipment_portal/security/ir.model.access.csv new file mode 100644 index 0000000000..80b27edb06 --- /dev/null +++ b/fieldservice_equipment_portal/security/ir.model.access.csv @@ -0,0 +1,2 @@ +id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink +access_fsm_equipment_portal,fsm.equipment portal user,fieldservice.model_fsm_equipment,base.group_portal,1,0,0,0 diff --git a/fieldservice_equipment_portal/static/description/index.html b/fieldservice_equipment_portal/static/description/index.html new file mode 100644 index 0000000000..61f76f1b55 --- /dev/null +++ b/fieldservice_equipment_portal/static/description/index.html @@ -0,0 +1,450 @@ + + + + + +Field Service - Equipment Portal + + + +
+

Field Service - Equipment Portal

+ + +

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

+

This module lets portal users see the field service equipments owned or +managed by their commercial partner (or equipments they follow) in the +“My Account” area of the portal.

+

It adds:

+
    +
  • an Equipments card on the portal home page with a record counter,
  • +
  • a paginated list at /my/equipments,
  • +
  • a detail page per equipment (product, location, owner, notes), +shareable with a portal access token — a stable URL well suited as a +QR code target on physical equipment labels (see +fieldservice_equipment_portal_qrcode).
  • +
+

Table of contents

+ +
+

Usage

+
    +
  1. Grant a customer contact portal access (Settings > Users, “Grant +portal access”).
  2. +
  3. As that portal user, open My Account: an Equipments card +lists the equipments whose owner or manager belongs to the user’s +company, plus any equipment the user follows.
  4. +
  5. Internal users can share a direct link with a token from the +equipment form (“Share” behavior provided by portal.mixin).
  6. +
+
+
+

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

+
    +
  • Pop Solutions
  • +
+
+
+

Contributors

+ +
+
+

Maintainers

+

This module is maintained by the OCA.

+ +Odoo Community Association + +

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

+

Current maintainer:

+

marcos-mendez

+

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_equipment_portal/tests/__init__.py b/fieldservice_equipment_portal/tests/__init__.py new file mode 100644 index 0000000000..6381ded8ad --- /dev/null +++ b/fieldservice_equipment_portal/tests/__init__.py @@ -0,0 +1 @@ +from . import test_equipment_portal diff --git a/fieldservice_equipment_portal/tests/test_equipment_portal.py b/fieldservice_equipment_portal/tests/test_equipment_portal.py new file mode 100644 index 0000000000..f7f801fff5 --- /dev/null +++ b/fieldservice_equipment_portal/tests/test_equipment_portal.py @@ -0,0 +1,121 @@ +# Copyright (C) 2026 Pop Solutions +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from odoo.tests import HttpCase, tagged + + +@tagged("post_install", "-at_install") +class TestEquipmentPortal(HttpCase): + @classmethod + def setUpClass(cls): + super().setUpClass() + cls.partner = cls.env["res.partner"].create({"name": "Equipment Owner"}) + cls.other_partner = cls.env["res.partner"].create({"name": "Other Owner"}) + cls.portal_user = ( + cls.env["res.users"] + .with_context(no_reset_password=True) + .create( + { + "login": "portal-equipment@test.example.com", + "name": "Portal Equipment User", + "partner_id": cls.partner.id, + "groups_id": [(6, 0, [cls.env.ref("base.group_portal").id])], + "password": "portal-equipment", + } + ) + ) + cls.equipment = cls.env["fsm.equipment"].create( + {"name": "My Portal Equipment", "owned_by_id": cls.partner.id} + ) + cls.other_equipment = cls.env["fsm.equipment"].create( + {"name": "Foreign Equipment", "owned_by_id": cls.other_partner.id} + ) + + def test_access_url(self): + self.assertEqual( + self.equipment.access_url, f"/my/equipments/{self.equipment.id}" + ) + + def test_portal_list_shows_own_equipments_only(self): + self.authenticate("portal-equipment@test.example.com", "portal-equipment") + response = self.url_open("/my/equipments") + self.assertEqual(response.status_code, 200) + self.assertIn(b"My Portal Equipment", response.content) + self.assertNotIn(b"Foreign Equipment", response.content) + + def test_portal_detail_page(self): + self.authenticate("portal-equipment@test.example.com", "portal-equipment") + response = self.url_open(f"/my/equipments/{self.equipment.id}") + self.assertEqual(response.status_code, 200) + self.assertIn(b"My Portal Equipment", response.content) + + def test_portal_detail_denied_without_access(self): + self.authenticate("portal-equipment@test.example.com", "portal-equipment") + response = self.url_open(f"/my/equipments/{self.other_equipment.id}") + self.assertNotIn(b"Foreign Equipment", response.content) + + def test_portal_detail_with_access_token(self): + self.other_equipment._portal_ensure_token() + response = self.url_open( + f"/my/equipments/{self.other_equipment.id}" + f"?access_token={self.other_equipment.access_token}" + ) + self.assertEqual(response.status_code, 200) + self.assertIn(b"Foreign Equipment", response.content) + + def test_home_counter(self): + self.authenticate("portal-equipment@test.example.com", "portal-equipment") + result = self.make_jsonrpc_request( + "/my/counters", {"counters": ["equipment_count"]} + ) + self.assertEqual(result["equipment_count"], 1) + + def test_list_and_counter_without_read_access(self): + self.env.ref( + "fieldservice_equipment_portal.access_fsm_equipment_portal" + ).perm_read = False + self.authenticate("portal-equipment@test.example.com", "portal-equipment") + response = self.url_open("/my/equipments") + self.assertTrue(response.url.rstrip("/").endswith("/my")) + result = self.make_jsonrpc_request( + "/my/counters", {"counters": ["equipment_count"]} + ) + self.assertEqual(result["equipment_count"], 0) + + def test_anonymous_denied_when_public_access_disabled(self): + response = self.url_open(f"/my/equipments/{self.equipment.id}") + self.assertNotIn(b"My Portal Equipment", response.content) + + def test_anonymous_allowed_when_public_access_enabled(self): + self.env["ir.config_parameter"].sudo().set_param( + "fieldservice_equipment_portal.public_access", "True" + ) + response = self.url_open(f"/my/equipments/{self.equipment.id}") + self.assertEqual(response.status_code, 200) + self.assertIn(b"My Portal Equipment", response.content) + + def test_anonymous_unknown_equipment_redirects_when_public(self): + self.env["ir.config_parameter"].sudo().set_param( + "fieldservice_equipment_portal.public_access", "True" + ) + response = self.url_open("/my/equipments/99999999") + self.assertTrue( + "/web/login" in response.url or response.url.rstrip("/").endswith("/my") + ) + + def test_public_setting_roundtrip(self): + settings = self.env["res.config.settings"].create( + {"fsm_equipment_portal_public": True} + ) + settings.execute() + self.assertTrue( + self.env["ir.config_parameter"] + .sudo() + .get_param("fieldservice_equipment_portal.public_access") + ) + + def test_sort_by_name(self): + self.authenticate("portal-equipment@test.example.com", "portal-equipment") + response = self.url_open("/my/equipments?sortby=name") + self.assertEqual(response.status_code, 200) + self.assertIn(b"My Portal Equipment", response.content) diff --git a/fieldservice_equipment_portal/views/portal_templates.xml b/fieldservice_equipment_portal/views/portal_templates.xml new file mode 100644 index 0000000000..af8ce9c67c --- /dev/null +++ b/fieldservice_equipment_portal/views/portal_templates.xml @@ -0,0 +1,104 @@ + + + + + + + diff --git a/fieldservice_equipment_portal/views/res_config_settings.xml b/fieldservice_equipment_portal/views/res_config_settings.xml new file mode 100644 index 0000000000..2be7a0d24e --- /dev/null +++ b/fieldservice_equipment_portal/views/res_config_settings.xml @@ -0,0 +1,23 @@ + + + + res.config.settings.view.form.fsm.equipment.portal + res.config.settings + + + + + + + + + + + + diff --git a/fieldservice_equipment_portal_qrcode/README.rst b/fieldservice_equipment_portal_qrcode/README.rst new file mode 100644 index 0000000000..edd102d2c4 --- /dev/null +++ b/fieldservice_equipment_portal_qrcode/README.rst @@ -0,0 +1,101 @@ +=============================================== +Field Service - Equipment Portal QR Code Labels +=============================================== + +.. + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! This file is generated by oca-gen-addon-readme !! + !! changes will be overwritten. !! + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! source digest: sha256:b27e1043a40c11ed2e1ab7d7d1991f47f09c64617e9575780223f7d319c4d754 + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + +.. |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/18.0/fieldservice_equipment_portal_qrcode + :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-18-0/field-service-18-0-fieldservice_equipment_portal_qrcode + :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=18.0 + :alt: Try me on Runboat + +|badge1| |badge2| |badge3| |badge4| |badge5| + +Print 60x40mm QR code labels for field service equipments. The QR code +points to the equipment's portal page +(``fieldservice_equipment_portal``), so anyone scanning the label on the +physical asset lands on its page — with a portal access token the page +can even be shared with people outside Odoo. + +The label shows the company (or a dedicated "labels logo" configurable +on the company form), the equipment name and its serial number. + +**Table of contents** + +.. contents:: + :local: + +Usage +===== + +1. Optionally set a **Labels logo** on the company form (falls back to + the company logo). +2. Open an equipment and use *Print > Equipment QR Label (PDF)*. +3. Stick the label on the physical equipment: scanning the QR opens the + equipment's portal page. + +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 +------- + +* Pop Solutions + +Contributors +------------ + +- Rafnix Guzman +- Marcos Mendez + +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-marcos-mendez| image:: https://github.com/marcos-mendez.png?size=40px + :target: https://github.com/marcos-mendez + :alt: marcos-mendez + +Current `maintainer `__: + +|maintainer-marcos-mendez| + +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_equipment_portal_qrcode/__init__.py b/fieldservice_equipment_portal_qrcode/__init__.py new file mode 100644 index 0000000000..91c5580fed --- /dev/null +++ b/fieldservice_equipment_portal_qrcode/__init__.py @@ -0,0 +1,2 @@ +from . import controllers +from . import models diff --git a/fieldservice_equipment_portal_qrcode/__manifest__.py b/fieldservice_equipment_portal_qrcode/__manifest__.py new file mode 100644 index 0000000000..a42b2981a9 --- /dev/null +++ b/fieldservice_equipment_portal_qrcode/__manifest__.py @@ -0,0 +1,20 @@ +# Copyright (C) 2022 Rafnix Guzman +# Copyright (C) 2026 Pop Solutions +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). +{ + "name": "Field Service - Equipment Portal QR Code Labels", + "summary": "Print QR code labels linking equipments to their portal page", + "version": "18.0.1.1.0", + "license": "AGPL-3", + "website": "https://github.com/OCA/field-service", + "category": "Field Service", + "author": "Pop Solutions, Odoo Community Association (OCA)", + "maintainers": ["marcos-mendez"], + "depends": ["fieldservice_equipment_portal", "fieldservice_equipment_stock"], + "external_dependencies": {"python": ["qrcode"]}, + "data": [ + "views/res_company_views.xml", + "reports/equipment_label_report.xml", + "reports/equipment_label_templates.xml", + ], +} diff --git a/fieldservice_equipment_portal_qrcode/controllers/__init__.py b/fieldservice_equipment_portal_qrcode/controllers/__init__.py new file mode 100644 index 0000000000..12a7e529b6 --- /dev/null +++ b/fieldservice_equipment_portal_qrcode/controllers/__init__.py @@ -0,0 +1 @@ +from . import main diff --git a/fieldservice_equipment_portal_qrcode/controllers/main.py b/fieldservice_equipment_portal_qrcode/controllers/main.py new file mode 100644 index 0000000000..a3641746c1 --- /dev/null +++ b/fieldservice_equipment_portal_qrcode/controllers/main.py @@ -0,0 +1,26 @@ +# Copyright (C) 2026 Pop Solutions +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from odoo import http +from odoo.http import request + + +class PortalEquipmentSerial(http.Controller): + @http.route( + "/my/equipments/serial/", + type="http", + auth="public", + website=True, + ) + def portal_equipment_by_serial(self, serial, **kw): + """Resolve an equipment by its serial number and redirect to its + page. Keeps printed QR labels stable even if the equipment id + changes; access control happens on the target page.""" + equipment = ( + request.env["fsm.equipment"] + .sudo() + .search([("lot_id.name", "=ilike", serial)], limit=1) + ) + if not equipment: + raise request.not_found() + return request.redirect(f"/my/equipments/{equipment.id}", code=301) diff --git a/fieldservice_equipment_portal_qrcode/models/__init__.py b/fieldservice_equipment_portal_qrcode/models/__init__.py new file mode 100644 index 0000000000..eac8f2f81d --- /dev/null +++ b/fieldservice_equipment_portal_qrcode/models/__init__.py @@ -0,0 +1,2 @@ +from . import fsm_equipment +from . import res_company diff --git a/fieldservice_equipment_portal_qrcode/models/fsm_equipment.py b/fieldservice_equipment_portal_qrcode/models/fsm_equipment.py new file mode 100644 index 0000000000..4f19da0170 --- /dev/null +++ b/fieldservice_equipment_portal_qrcode/models/fsm_equipment.py @@ -0,0 +1,41 @@ +# Copyright (C) 2022 Rafnix Guzman +# Copyright (C) 2026 Pop Solutions +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +import base64 +import io + +import qrcode + +from odoo import api, fields, models + + +class FsmEquipment(models.Model): + _inherit = "fsm.equipment" + + qrcode = fields.Binary( + compute="_compute_qrcode", + attachment=False, + readonly=True, + help="QR code pointing to this equipment's portal page.", + ) + + def _get_qrcode_url(self): + self.ensure_one() + return f"{self.get_base_url()}{self.access_url}" + + @api.depends("lot_id", "product_id") + def _compute_qrcode(self): + for equipment in self: + if not equipment.id: + equipment.qrcode = False + continue + equipment.qrcode = equipment._generate_qrcode_from_url( + equipment._get_qrcode_url() + ) + + @api.model + def _generate_qrcode_from_url(self, url): + data = io.BytesIO() + qrcode.make(url, box_size=8, border=0).save(data, format="PNG") + return base64.b64encode(data.getvalue()).decode() diff --git a/fieldservice_equipment_portal_qrcode/models/res_company.py b/fieldservice_equipment_portal_qrcode/models/res_company.py new file mode 100644 index 0000000000..f6b57ce625 --- /dev/null +++ b/fieldservice_equipment_portal_qrcode/models/res_company.py @@ -0,0 +1,16 @@ +# Copyright (C) 2022 Rafnix Guzman +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from odoo import fields, models + + +class ResCompany(models.Model): + _inherit = "res.company" + + labels_logo = fields.Image( + "Labels logo", + max_width=512, + max_height=512, + help="Logo printed on equipment QR labels. Falls back to the " + "company logo when empty.", + ) diff --git a/fieldservice_equipment_portal_qrcode/pyproject.toml b/fieldservice_equipment_portal_qrcode/pyproject.toml new file mode 100644 index 0000000000..4231d0cccb --- /dev/null +++ b/fieldservice_equipment_portal_qrcode/pyproject.toml @@ -0,0 +1,3 @@ +[build-system] +requires = ["whool"] +build-backend = "whool.buildapi" diff --git a/fieldservice_equipment_portal_qrcode/readme/CONTRIBUTORS.md b/fieldservice_equipment_portal_qrcode/readme/CONTRIBUTORS.md new file mode 100644 index 0000000000..c2dba6666f --- /dev/null +++ b/fieldservice_equipment_portal_qrcode/readme/CONTRIBUTORS.md @@ -0,0 +1,2 @@ +- Rafnix Guzman \ +- Marcos Mendez \ diff --git a/fieldservice_equipment_portal_qrcode/readme/DESCRIPTION.md b/fieldservice_equipment_portal_qrcode/readme/DESCRIPTION.md new file mode 100644 index 0000000000..528a6e3221 --- /dev/null +++ b/fieldservice_equipment_portal_qrcode/readme/DESCRIPTION.md @@ -0,0 +1,7 @@ +Print 60x40mm QR code labels for field service equipments. The QR code +points to the equipment's portal page (`fieldservice_equipment_portal`), so +anyone scanning the label on the physical asset lands on its page — with a +portal access token the page can even be shared with people outside Odoo. + +The label shows the company (or a dedicated "labels logo" configurable on +the company form), the equipment name and its serial number. diff --git a/fieldservice_equipment_portal_qrcode/readme/USAGE.md b/fieldservice_equipment_portal_qrcode/readme/USAGE.md new file mode 100644 index 0000000000..82f6c2e6e6 --- /dev/null +++ b/fieldservice_equipment_portal_qrcode/readme/USAGE.md @@ -0,0 +1,5 @@ +1. Optionally set a **Labels logo** on the company form (falls back to the + company logo). +2. Open an equipment and use *Print > Equipment QR Label (PDF)*. +3. Stick the label on the physical equipment: scanning the QR opens the + equipment's portal page. diff --git a/fieldservice_equipment_portal_qrcode/reports/equipment_label_report.xml b/fieldservice_equipment_portal_qrcode/reports/equipment_label_report.xml new file mode 100644 index 0000000000..9cd77dad41 --- /dev/null +++ b/fieldservice_equipment_portal_qrcode/reports/equipment_label_report.xml @@ -0,0 +1,34 @@ + + + + Equipment Label 60x40mm + custom + 40 + 60 + Portrait + 2 + 2 + 2 + 2 + + 0 + 80 + + + Equipment QR Label (PDF) + fsm.equipment + qweb-pdf + fieldservice_equipment_portal_qrcode.report_equipment_qr_label + fieldservice_equipment_portal_qrcode.report_equipment_qr_label + + 'Equipment QR Label - %s' % (object.lot_id.name or object.name) + + report + + diff --git a/fieldservice_equipment_portal_qrcode/reports/equipment_label_templates.xml b/fieldservice_equipment_portal_qrcode/reports/equipment_label_templates.xml new file mode 100644 index 0000000000..fe2fb5463a --- /dev/null +++ b/fieldservice_equipment_portal_qrcode/reports/equipment_label_templates.xml @@ -0,0 +1,41 @@ + + + + diff --git a/fieldservice_equipment_portal_qrcode/static/description/index.html b/fieldservice_equipment_portal_qrcode/static/description/index.html new file mode 100644 index 0000000000..a588941f1b --- /dev/null +++ b/fieldservice_equipment_portal_qrcode/static/description/index.html @@ -0,0 +1,443 @@ + + + + + +Field Service - Equipment Portal QR Code Labels + + + +
+

Field Service - Equipment Portal QR Code Labels

+ + +

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

+

Print 60x40mm QR code labels for field service equipments. The QR code +points to the equipment’s portal page +(fieldservice_equipment_portal), so anyone scanning the label on the +physical asset lands on its page — with a portal access token the page +can even be shared with people outside Odoo.

+

The label shows the company (or a dedicated “labels logo” configurable +on the company form), the equipment name and its serial number.

+

Table of contents

+ +
+

Usage

+
    +
  1. Optionally set a Labels logo on the company form (falls back to +the company logo).
  2. +
  3. Open an equipment and use Print > Equipment QR Label (PDF).
  4. +
  5. Stick the label on the physical equipment: scanning the QR opens the +equipment’s portal page.
  6. +
+
+
+

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

+
    +
  • Pop Solutions
  • +
+
+
+

Contributors

+ +
+
+

Maintainers

+

This module is maintained by the OCA.

+ +Odoo Community Association + +

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

+

Current maintainer:

+

marcos-mendez

+

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_equipment_portal_qrcode/tests/__init__.py b/fieldservice_equipment_portal_qrcode/tests/__init__.py new file mode 100644 index 0000000000..af77a790d9 --- /dev/null +++ b/fieldservice_equipment_portal_qrcode/tests/__init__.py @@ -0,0 +1,2 @@ +from . import test_equipment_qrcode +from . import test_serial_route diff --git a/fieldservice_equipment_portal_qrcode/tests/test_equipment_qrcode.py b/fieldservice_equipment_portal_qrcode/tests/test_equipment_qrcode.py new file mode 100644 index 0000000000..7a53e5f14c --- /dev/null +++ b/fieldservice_equipment_portal_qrcode/tests/test_equipment_qrcode.py @@ -0,0 +1,33 @@ +# Copyright (C) 2026 Pop Solutions +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +import base64 + +from odoo.tests.common import TransactionCase + + +class TestEquipmentQrcode(TransactionCase): + @classmethod + def setUpClass(cls): + super().setUpClass() + cls.equipment = cls.env["fsm.equipment"].create({"name": "QR Equipment"}) + + def test_qrcode_url_targets_portal_page(self): + url = self.equipment._get_qrcode_url() + self.assertTrue(url.endswith(f"/my/equipments/{self.equipment.id}")) + + def test_qrcode_is_png(self): + png = base64.b64decode(self.equipment.qrcode) + self.assertTrue(png.startswith(b"\x89PNG")) + + def test_qrcode_empty_for_new_records(self): + new_equipment = self.env["fsm.equipment"].new({"name": "Virtual"}) + self.assertFalse(new_equipment.qrcode) + + def test_label_report_renders(self): + html = self.env["ir.actions.report"]._render_qweb_html( + "fieldservice_equipment_portal_qrcode.action_report_equipment_qr_label", + self.equipment.ids, + )[0] + self.assertIn(b"QR Equipment", html) + self.assertIn(b"data:image/png;base64", html) diff --git a/fieldservice_equipment_portal_qrcode/tests/test_serial_route.py b/fieldservice_equipment_portal_qrcode/tests/test_serial_route.py new file mode 100644 index 0000000000..2ad74a265a --- /dev/null +++ b/fieldservice_equipment_portal_qrcode/tests/test_serial_route.py @@ -0,0 +1,41 @@ +# Copyright (C) 2026 Pop Solutions +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from odoo.tests import HttpCase, tagged + + +@tagged("post_install", "-at_install") +class TestEquipmentSerialRoute(HttpCase): + @classmethod + def setUpClass(cls): + super().setUpClass() + cls.product = cls.env["product.product"].create( + { + "name": "Serial Device", + "is_storable": True, + "tracking": "serial", + } + ) + cls.lot = cls.env["stock.lot"].create( + {"name": "SER-0099", "product_id": cls.product.id} + ) + cls.equipment = cls.env["fsm.equipment"].create( + { + "name": "Serial Equipment", + "product_id": cls.product.id, + "lot_id": cls.lot.id, + } + ) + + def test_serial_route_redirects_to_equipment_page(self): + self.env["ir.config_parameter"].sudo().set_param( + "fieldservice_equipment_portal.public_access", "True" + ) + response = self.url_open("/my/equipments/serial/SER-0099") + self.assertEqual(response.status_code, 200) + self.assertTrue(response.url.endswith(f"/my/equipments/{self.equipment.id}")) + self.assertIn(b"Serial Equipment", response.content) + + def test_serial_route_unknown_serial_404(self): + response = self.url_open("/my/equipments/serial/NAO-EXISTE") + self.assertEqual(response.status_code, 404) diff --git a/fieldservice_equipment_portal_qrcode/views/res_company_views.xml b/fieldservice_equipment_portal_qrcode/views/res_company_views.xml new file mode 100644 index 0000000000..5ce25822d7 --- /dev/null +++ b/fieldservice_equipment_portal_qrcode/views/res_company_views.xml @@ -0,0 +1,15 @@ + + + + diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000000..0f5aa6b2a7 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,2 @@ +# generated from manifests external_dependencies +qrcode