diff --git a/agreement_portal/README.rst b/agreement_portal/README.rst new file mode 100644 index 000000000..37973bee6 --- /dev/null +++ b/agreement_portal/README.rst @@ -0,0 +1,111 @@ +================ +Agreement Portal +================ + +.. + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! This file is generated by oca-gen-addon-readme !! + !! changes will be overwritten. !! + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! source digest: sha256:ace66f4efb7ad30070b685116e28cabe368de238715ffc058769ec6aea8c2e40 + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + +.. |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%2Fagreement-lightgray.png?logo=github + :target: https://github.com/OCA/agreement/tree/18.0/agreement_portal + :alt: OCA/agreement +.. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png + :target: https://translation.odoo-community.org/projects/agreement-18-0/agreement-18-0-agreement_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/agreement&target_branch=18.0 + :alt: Try me on Runboat + +|badge1| |badge2| |badge3| |badge4| |badge5| + +This module lets portal users see the agreements linked to their +commercial partner (or agreements they follow) in the "My Account" area +of the portal. + +It adds: + +- an **Agreements** card on the portal home page with a record counter, +- a paginated, sortable list at ``/my/agreements``, +- a detail page per agreement, shareable with a portal access token. + +Only the fields of the base ``agreement`` module are shown (name, +reference, start and end dates, partner). Extensions displaying fields +from other agreement modules can inherit the ``portal_agreement_page`` +template. + +**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 **Agreements** card + lists the agreements whose partner belongs to the user's company, + plus any agreement the user follows. +3. Internal users can share a direct link with a token from the + agreement 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 +- Víctor Martínez (portal controller patterns from Tecnativa portal + modules) +- 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/agreement `_ project on GitHub. + +You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute. diff --git a/agreement_portal/__init__.py b/agreement_portal/__init__.py new file mode 100644 index 000000000..91c5580fe --- /dev/null +++ b/agreement_portal/__init__.py @@ -0,0 +1,2 @@ +from . import controllers +from . import models diff --git a/agreement_portal/__manifest__.py b/agreement_portal/__manifest__.py new file mode 100644 index 000000000..c4f3e2895 --- /dev/null +++ b/agreement_portal/__manifest__.py @@ -0,0 +1,19 @@ +# Copyright (C) 2022 Rafnix Guzman +# Copyright (C) 2026 Pop Solutions +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). +{ + "name": "Agreement Portal", + "summary": "Let portal users see their agreements", + "version": "18.0.1.0.0", + "license": "AGPL-3", + "website": "https://github.com/OCA/agreement", + "category": "Contract", + "author": "Pop Solutions, Odoo Community Association (OCA)", + "maintainers": ["marcos-mendez"], + "depends": ["portal", "agreement"], + "data": [ + "security/ir.model.access.csv", + "security/agreement_security.xml", + "views/agreement_portal_templates.xml", + ], +} diff --git a/agreement_portal/controllers/__init__.py b/agreement_portal/controllers/__init__.py new file mode 100644 index 000000000..12a7e529b --- /dev/null +++ b/agreement_portal/controllers/__init__.py @@ -0,0 +1 @@ +from . import main diff --git a/agreement_portal/controllers/main.py b/agreement_portal/controllers/main.py new file mode 100644 index 000000000..3c454c64f --- /dev/null +++ b/agreement_portal/controllers/main.py @@ -0,0 +1,98 @@ +# Copyright 2020-2022 Tecnativa - Víctor Martínez +# Copyright (C) 2022 Rafnix Guzman +# 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 PortalAgreement(CustomerPortal): + def _prepare_home_portal_values(self, counters): + values = super()._prepare_home_portal_values(counters) + if "agreement_count" in counters: + agreement_model = request.env["agreement"] + agreement_count = ( + agreement_model.search_count([]) + if agreement_model.has_access("read") + else 0 + ) + values["agreement_count"] = agreement_count + return values + + def _agreement_get_page_view_values(self, agreement, access_token, **kwargs): + values = { + "page_name": "Agreements", + "agreement": agreement, + } + return self._get_page_view_values( + agreement, access_token, values, "my_agreements_history", False, **kwargs + ) + + def _get_filter_domain(self, kw): + return [] + + @http.route( + ["/my/agreements", "/my/agreements/page/"], + type="http", + auth="user", + website=True, + ) + def portal_my_agreements(self, page=1, sortby=None, **kw): + values = self._prepare_portal_layout_values() + agreement_model = request.env["agreement"] + if not agreement_model.has_access("read"): + return request.redirect("/my") + domain = self._get_filter_domain(kw) + searchbar_sortings = { + "name": {"label": _("Name"), "order": "name desc"}, + "code": {"label": _("Reference"), "order": "code desc"}, + } + if not sortby: + sortby = "name" + order = searchbar_sortings[sortby]["order"] + agreement_count = agreement_model.search_count(domain) + pager = portal_pager( + url="/my/agreements", + url_args={"sortby": sortby}, + total=agreement_count, + page=page, + step=self._items_per_page, + ) + agreements = agreement_model.search( + domain, order=order, limit=self._items_per_page, offset=pager["offset"] + ) + request.session["my_agreements_history"] = agreements.ids[:100] + values.update( + { + "agreements": agreements, + "page_name": "Agreements", + "pager": pager, + "default_url": "/my/agreements", + "searchbar_sortings": searchbar_sortings, + "sortby": sortby, + } + ) + return request.render("agreement_portal.portal_my_agreements", values) + + @http.route( + ["/my/agreements/"], + type="http", + auth="public", + website=True, + ) + def portal_my_agreement_detail(self, agreement_id, access_token=None, **kw): + try: + agreement_sudo = self._document_check_access( + "agreement", agreement_id, access_token + ) + except (AccessError, MissingError): + return request.redirect("/my") + values = self._agreement_get_page_view_values( + agreement_sudo, access_token, **kw + ) + return request.render("agreement_portal.portal_agreement_page", values) diff --git a/agreement_portal/models/__init__.py b/agreement_portal/models/__init__.py new file mode 100644 index 000000000..51f4021fd --- /dev/null +++ b/agreement_portal/models/__init__.py @@ -0,0 +1 @@ +from . import agreement diff --git a/agreement_portal/models/agreement.py b/agreement_portal/models/agreement.py new file mode 100644 index 000000000..bf077344e --- /dev/null +++ b/agreement_portal/models/agreement.py @@ -0,0 +1,15 @@ +# Copyright (C) 2022 Rafnix Guzman +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from odoo import models + + +class Agreement(models.Model): + _name = "agreement" + _inherit = ["agreement", "portal.mixin"] + + def _compute_access_url(self): + res = super()._compute_access_url() + for record in self: + record.access_url = f"/my/agreements/{record.id}" + return res diff --git a/agreement_portal/pyproject.toml b/agreement_portal/pyproject.toml new file mode 100644 index 000000000..4231d0ccc --- /dev/null +++ b/agreement_portal/pyproject.toml @@ -0,0 +1,3 @@ +[build-system] +requires = ["whool"] +build-backend = "whool.buildapi" diff --git a/agreement_portal/readme/CONTRIBUTORS.md b/agreement_portal/readme/CONTRIBUTORS.md new file mode 100644 index 000000000..0d69342f6 --- /dev/null +++ b/agreement_portal/readme/CONTRIBUTORS.md @@ -0,0 +1,3 @@ +- Rafnix Guzman \ +- Víctor Martínez (portal controller patterns from Tecnativa portal modules) +- Marcos Mendez \ diff --git a/agreement_portal/readme/DESCRIPTION.md b/agreement_portal/readme/DESCRIPTION.md new file mode 100644 index 000000000..5b1f45dd0 --- /dev/null +++ b/agreement_portal/readme/DESCRIPTION.md @@ -0,0 +1,12 @@ +This module lets portal users see the agreements linked to their commercial +partner (or agreements they follow) in the "My Account" area of the portal. + +It adds: + +- an **Agreements** card on the portal home page with a record counter, +- a paginated, sortable list at `/my/agreements`, +- a detail page per agreement, shareable with a portal access token. + +Only the fields of the base `agreement` module are shown (name, reference, +start and end dates, partner). Extensions displaying fields from other +agreement modules can inherit the `portal_agreement_page` template. diff --git a/agreement_portal/readme/USAGE.md b/agreement_portal/readme/USAGE.md new file mode 100644 index 000000000..6beeb63ba --- /dev/null +++ b/agreement_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 **Agreements** card lists the + agreements whose partner belongs to the user's company, plus any agreement + the user follows. +3. Internal users can share a direct link with a token from the agreement form + ("Share" behavior provided by `portal.mixin`). diff --git a/agreement_portal/security/agreement_security.xml b/agreement_portal/security/agreement_security.xml new file mode 100644 index 000000000..c7717c081 --- /dev/null +++ b/agreement_portal/security/agreement_security.xml @@ -0,0 +1,14 @@ + + + + Agreement: portal users see their own + + + ['|', + ('partner_id', 'child_of', user.partner_id.commercial_partner_id.id), + ('message_partner_ids', 'in', [user.partner_id.id]), + ] + + + + diff --git a/agreement_portal/security/ir.model.access.csv b/agreement_portal/security/ir.model.access.csv new file mode 100644 index 000000000..3a7220f71 --- /dev/null +++ b/agreement_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_agreement_portal,Agreement portal user,agreement.model_agreement,base.group_portal,1,0,0,0 diff --git a/agreement_portal/static/description/index.html b/agreement_portal/static/description/index.html new file mode 100644 index 000000000..49163513d --- /dev/null +++ b/agreement_portal/static/description/index.html @@ -0,0 +1,453 @@ + + + + + +Agreement Portal + + + +
+

Agreement Portal

+ + +

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

+

This module lets portal users see the agreements linked to their +commercial partner (or agreements they follow) in the “My Account” area +of the portal.

+

It adds:

+
    +
  • an Agreements card on the portal home page with a record counter,
  • +
  • a paginated, sortable list at /my/agreements,
  • +
  • a detail page per agreement, shareable with a portal access token.
  • +
+

Only the fields of the base agreement module are shown (name, +reference, start and end dates, partner). Extensions displaying fields +from other agreement modules can inherit the portal_agreement_page +template.

+

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 Agreements card +lists the agreements whose partner belongs to the user’s company, +plus any agreement the user follows.
  4. +
  5. Internal users can share a direct link with a token from the +agreement 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

+
    +
  • Rafnix Guzman <rafnixg@gmail.com>
  • +
  • Víctor Martínez (portal controller patterns from Tecnativa portal +modules)
  • +
  • Marcos Mendez <m@pop.coop>
  • +
+
+
+

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/agreement project on GitHub.

+

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

+
+
+
+ + diff --git a/agreement_portal/tests/__init__.py b/agreement_portal/tests/__init__.py new file mode 100644 index 000000000..721c6f48b --- /dev/null +++ b/agreement_portal/tests/__init__.py @@ -0,0 +1 @@ +from . import test_agreement_portal diff --git a/agreement_portal/tests/test_agreement_portal.py b/agreement_portal/tests/test_agreement_portal.py new file mode 100644 index 000000000..cd3a0f452 --- /dev/null +++ b/agreement_portal/tests/test_agreement_portal.py @@ -0,0 +1,95 @@ +# Copyright (C) 2026 Pop Solutions +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from odoo.tests import HttpCase, tagged + + +@tagged("post_install", "-at_install") +class TestAgreementPortal(HttpCase): + @classmethod + def setUpClass(cls): + super().setUpClass() + cls.partner = cls.env["res.partner"].create({"name": "Portal Partner"}) + cls.other_partner = cls.env["res.partner"].create({"name": "Other Partner"}) + cls.portal_user = ( + cls.env["res.users"] + .with_context(no_reset_password=True) + .create( + { + "login": "portal-agreement@test.example.com", + "name": "Portal Agreement User", + "partner_id": cls.partner.id, + "groups_id": [(6, 0, [cls.env.ref("base.group_portal").id])], + "password": "portal-agreement", + } + ) + ) + cls.agreement = cls.env["agreement"].create( + { + "name": "My Portal Agreement", + "code": "PORTAL-1", + "partner_id": cls.partner.id, + } + ) + cls.other_agreement = cls.env["agreement"].create( + { + "name": "Foreign Agreement", + "code": "PORTAL-2", + "partner_id": cls.other_partner.id, + } + ) + + def test_access_url(self): + self.assertEqual( + self.agreement.access_url, f"/my/agreements/{self.agreement.id}" + ) + + def test_portal_list_shows_own_agreements_only(self): + self.authenticate("portal-agreement@test.example.com", "portal-agreement") + response = self.url_open("/my/agreements") + self.assertEqual(response.status_code, 200) + self.assertIn(b"My Portal Agreement", response.content) + self.assertNotIn(b"Foreign Agreement", response.content) + + def test_portal_detail_page(self): + self.authenticate("portal-agreement@test.example.com", "portal-agreement") + response = self.url_open(f"/my/agreements/{self.agreement.id}") + self.assertEqual(response.status_code, 200) + self.assertIn(b"My Portal Agreement", response.content) + + def test_portal_detail_denied_without_access(self): + self.authenticate("portal-agreement@test.example.com", "portal-agreement") + response = self.url_open(f"/my/agreements/{self.other_agreement.id}") + self.assertNotIn(b"Foreign Agreement", response.content) + + def test_home_counter(self): + self.authenticate("portal-agreement@test.example.com", "portal-agreement") + result = self.make_jsonrpc_request( + "/my/counters", {"counters": ["agreement_count"]} + ) + self.assertEqual(result["agreement_count"], 1) + + def test_list_and_counter_without_read_access(self): + self.env.ref("agreement_portal.access_agreement_portal").perm_read = False + self.authenticate("portal-agreement@test.example.com", "portal-agreement") + response = self.url_open("/my/agreements") + self.assertTrue(response.url.rstrip("/").endswith("/my")) + result = self.make_jsonrpc_request( + "/my/counters", {"counters": ["agreement_count"]} + ) + self.assertEqual(result["agreement_count"], 0) + + def test_sort_by_reference(self): + self.authenticate("portal-agreement@test.example.com", "portal-agreement") + response = self.url_open("/my/agreements?sortby=code") + self.assertEqual(response.status_code, 200) + self.assertIn(b"My Portal Agreement", response.content) + + def test_portal_detail_with_access_token(self): + self.other_agreement._portal_ensure_token() + response = self.url_open( + f"/my/agreements/{self.other_agreement.id}" + f"?access_token={self.other_agreement.access_token}" + ) + self.assertEqual(response.status_code, 200) + self.assertIn(b"Foreign Agreement", response.content) diff --git a/agreement_portal/views/agreement_portal_templates.xml b/agreement_portal/views/agreement_portal_templates.xml new file mode 100644 index 000000000..c89340f38 --- /dev/null +++ b/agreement_portal/views/agreement_portal_templates.xml @@ -0,0 +1,119 @@ + + + + + + +