From a4dc58a2b16b1adaaaee6e2d562b395b9568efc3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phan=20Sainl=C3=A9ger?= Date: Thu, 10 Nov 2022 00:30:28 +0100 Subject: [PATCH] [ADD] partner_profiles_portal: create add-on --- partner_profiles_portal/.gitignore | 2 + partner_profiles_portal/README.rst | 43 ++++ partner_profiles_portal/__init__.py | 4 + partner_profiles_portal/__manifest__.py | 41 ++++ .../controllers/__init__.py | 4 + .../controllers/portal_my_profiles.py | 91 +++++++++ .../controllers/portal_partner_profile.py | 130 ++++++++++++ partner_profiles_portal/i18n/README | 1 + partner_profiles_portal/models/__init__.py | 3 + partner_profiles_portal/models/res_partner.py | 50 +++++ .../security/members_security.xml | 13 ++ .../views/portal_home_template.xml | 24 +++ .../views/portal_my_profiles_template.xml | 97 +++++++++ .../views/portal_partner_profile_template.xml | 189 ++++++++++++++++++ .../views/res_partner_view.xml | 32 +++ 15 files changed, 724 insertions(+) create mode 100644 partner_profiles_portal/.gitignore create mode 100644 partner_profiles_portal/README.rst create mode 100644 partner_profiles_portal/__init__.py create mode 100644 partner_profiles_portal/__manifest__.py create mode 100644 partner_profiles_portal/controllers/__init__.py create mode 100644 partner_profiles_portal/controllers/portal_my_profiles.py create mode 100644 partner_profiles_portal/controllers/portal_partner_profile.py create mode 100644 partner_profiles_portal/i18n/README create mode 100644 partner_profiles_portal/models/__init__.py create mode 100644 partner_profiles_portal/models/res_partner.py create mode 100644 partner_profiles_portal/security/members_security.xml create mode 100644 partner_profiles_portal/views/portal_home_template.xml create mode 100644 partner_profiles_portal/views/portal_my_profiles_template.xml create mode 100644 partner_profiles_portal/views/portal_partner_profile_template.xml create mode 100644 partner_profiles_portal/views/res_partner_view.xml diff --git a/partner_profiles_portal/.gitignore b/partner_profiles_portal/.gitignore new file mode 100644 index 0000000..6da5887 --- /dev/null +++ b/partner_profiles_portal/.gitignore @@ -0,0 +1,2 @@ +*.*~ +*pyc diff --git a/partner_profiles_portal/README.rst b/partner_profiles_portal/README.rst new file mode 100644 index 0000000..267d172 --- /dev/null +++ b/partner_profiles_portal/README.rst @@ -0,0 +1,43 @@ +=============== +partner_profiles_portal +=============== + +Provide portal pages and forms to manage partner's profiles from portal home space. + +Installation +============ + +Use Odoo normal module installation procedure to install +``partner_profiles_portal``. + +Known issues / Roadmap +====================== + +None yet. +Bug Tracker +=========== + +Bugs are tracked on `our issues website `_. In case of +trouble, please check there if your issue has already been +reported. If you spotted it first, help us smashing it by providing a +detailed and welcomed feedback. + +Credits +======= + +Contributors +------------ + +* Stéphan Sainléger + +Funders +------- + +The development of this module has been financially supported by: +* Elabore (https://elabore.coop) + + +Maintainer +---------- + +This module is maintained by Elabore. \ No newline at end of file diff --git a/partner_profiles_portal/__init__.py b/partner_profiles_portal/__init__.py new file mode 100644 index 0000000..c3d410e --- /dev/null +++ b/partner_profiles_portal/__init__.py @@ -0,0 +1,4 @@ +# -*- coding: utf-8 -*- + +from . import models +from . import controllers diff --git a/partner_profiles_portal/__manifest__.py b/partner_profiles_portal/__manifest__.py new file mode 100644 index 0000000..3b6089d --- /dev/null +++ b/partner_profiles_portal/__manifest__.py @@ -0,0 +1,41 @@ +# Copyright 2022 Stéphan Sainléger (Elabore) +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +{ + "name": "partner_profiles_portal", + "version": "12.0.1.0.0", + "author": "Elabore", + "website": "https://elabore.coop", + "maintainer": "Stéphan Sainléger", + "license": "AGPL-3", + "category": "Tools", + "summary": "Provide portal pages and forms to manage partner's profiles from portal home space.", + # any module necessary for this one to work correctly + "depends": [ + "base", + "partner_profiles", + "portal", + "website", + ], + "qweb": [], + "external_dependencies": { + "python": [], + }, + # always loaded + "data": [ + "security/members_security.xml", + "views/portal_home_template.xml", + "views/portal_my_profiles_template.xml", + "views/portal_partner_profile_template.xml", + "views/res_partner_view.xml", + ], + # only loaded in demonstration mode + "demo": [], + "js": [], + "css": [], + "installable": True, + # Install this module automatically if all dependency have been previously + # and independently installed. Used for synergetic or glue modules. + "auto_install": False, + "application": False, +} \ No newline at end of file diff --git a/partner_profiles_portal/controllers/__init__.py b/partner_profiles_portal/controllers/__init__.py new file mode 100644 index 0000000..6042fcc --- /dev/null +++ b/partner_profiles_portal/controllers/__init__.py @@ -0,0 +1,4 @@ +# -*- coding: utf-8 -*- + +from . import portal_my_profiles +from . import portal_partner_profile \ No newline at end of file diff --git a/partner_profiles_portal/controllers/portal_my_profiles.py b/partner_profiles_portal/controllers/portal_my_profiles.py new file mode 100644 index 0000000..5a7cd65 --- /dev/null +++ b/partner_profiles_portal/controllers/portal_my_profiles.py @@ -0,0 +1,91 @@ +# Copyright 2020 Lokavaluto () +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). +from odoo import http, _ +from odoo.http import request +from odoo.addons.portal.controllers.portal import CustomerPortal, pager as portal_pager + + +class CustomerPortalMyProfiles(CustomerPortal): + + def _get_domain_my_profiles(self, user): + if user.partner_id.other_contact_ids: + main_profile_ids = user.partner_id.other_contact_ids.filtered( + "edit_structure_main_profile" + ).mapped("parent_id") + public_profile_ids = user.partner_id.other_contact_ids.filtered( + "edit_structure_public_profile" + ).mapped("parent_id.public_profile_id") + return [ + "|", + "|", + ("contact_id", "=", user.partner_id.id), + ("id", "in", main_profile_ids.ids), + ("id", "in", public_profile_ids.ids), + ] + else: + return [("contact_id", "=", user.partner_id.id)] + + def _prepare_portal_layout_values(self): + values = super(CustomerPortalMyProfiles, self)._prepare_portal_layout_values() + values["profile_count"] = request.env["res.partner"].search_count( + self._get_domain_my_profiles(request.env.user) + ) + return values + + @http.route( + ["/my/profiles", "/my/profiles/page/"], + type="http", + auth="user", + website=True, + ) + def portal_my_profiles( + self, page=1, date_begin=None, date_end=None, sortby=None, **kw + ): + values = self._prepare_portal_layout_values() + profile = request.env["res.partner"] + domain = self._get_domain_my_profiles(request.env.user) + + searchbar_sortings = { + "name": {"label": _("Name"), "order": "name"}, + "partner_profile": {"label": _("Profile Type"), "order": "partner_profile"}, + "parent_id": {"label": _("Company"), "order": "parent_id"}, + } + if not sortby: + sortby = "name" + order = searchbar_sortings[sortby]["order"] + + # archive groups - Default Group By 'create_date' + archive_groups = self._get_archive_groups("res.partner", domain) + + # profiles count + profile_count = profile.search_count(domain) + # pager + pager = portal_pager( + url="/my/profiles", + url_args={"sortby": sortby}, + total=profile_count, + page=page, + step=self._items_per_page, + ) + + # content according to pager and archive selected + profiles = profile.search( + domain, + order=order, + limit=self._items_per_page, + offset=pager["offset"], + ) + request.session["my_profiles_history"] = profiles.ids[:100] + + values.update( + { + "profiles": profiles, + "page_name": "profile", + "archive_groups": archive_groups, + "default_url": "/my/profiles", + "pager": pager, + "searchbar_sortings": searchbar_sortings, + "sortby": sortby, + } + ) + return request.render("partner_profiles_portal.portal_my_profiles", values) \ No newline at end of file diff --git a/partner_profiles_portal/controllers/portal_partner_profile.py b/partner_profiles_portal/controllers/portal_partner_profile.py new file mode 100644 index 0000000..74ffa57 --- /dev/null +++ b/partner_profiles_portal/controllers/portal_partner_profile.py @@ -0,0 +1,130 @@ +# Copyright 2020 Lokavaluto () +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). +from odoo import http, tools, _ +from odoo.exceptions import AccessError, MissingError +from odoo.http import request +from odoo.addons.portal.controllers.portal import CustomerPortal + + +class CustomerPortalPartnerProfile(CustomerPortal): + + def _profile_get_page_view_values(self, profile, access_token, **kwargs): + values = { + "page_name": "profile", + "profile": profile, + } + return self._get_page_view_values( + profile, access_token, values, "my_profiles_history", False, **kwargs + ) + + def _details_profile_form_validate(self, data, profile_id): + error = dict() + error_message = [] + # nickname uniqueness + if data.get("nickname") and request.env["res.partner"].sudo().search( + [ + ("name", "=", data.get("nickname")), + ("partner_profile.ref", "=", "partner_profile_public"), + ("id", "!=", profile_id), + ] + ): + error["nickname"] = "error" + error_message.append( + _("This nickname is already used, please find an other idea.") + ) + + # email validation + if data.get("email") and not tools.single_email_re.match(data.get("email")): + error["email"] = "error" + error_message.append( + _("Invalid Email! Please enter a valid email address.") + ) + return error, error_message + + def _get_profile_fields(self): + fields = [ + "nickname", + "function", + "phone", + "mobile", + "email", + "website_url", + "street", + "street2", + "city", + "country_id", + "zipcode", + ] + return fields + + def _get_page_saving_values(self, profile, kw): + profile_fields = self._get_profile_fields() + values = {key: kw[key] for key in profile_fields if key in kw} + values.update( + { + "name": values.pop("nickname", profile.name), + "zip": values.pop("zipcode", ""), + "website": values.pop("website_url", ""), + } + ) + return values + + def _get_page_opening_values(self): + # Just retrieve the values to display for Selection fields + countries = request.env["res.country"].sudo().search([]) + values = { + "countries": countries, + } + return values + + @http.route( + ["/my/profile/", "/my/profile/save"], + type="http", + auth="user", + website=True, + ) + def portal_my_profile( + self, profile_id=None, access_token=None, redirect=None, **kw + ): + # The following condition is to transform profile_id to an int, as it is sent as a string from the templace "portal_my_profile" + # TODO: find a better way to retrieve the profile_id at form submit step + if not isinstance(profile_id, int): + profile_id = int(profile_id) + + # Check that the user has the right to see this profile + try: + profile_sudo = self._document_check_access( + "res.partner", profile_id, access_token + ) + except (AccessError, MissingError): + return request.redirect("/my/profiles") + + values = self._profile_get_page_view_values(profile_sudo, access_token, **kw) + values.update( + { + "error": {}, + "error_message": [], + } + ) + if kw and request.httprequest.method == "POST": + # the user has clicked in the Save button to save new data + error, error_message = self._details_profile_form_validate(kw, profile_id) + values.update({"error": error, "error_message": error_message}) + values.update(kw) + if not error: + profile = request.env["res.partner"].browse(profile_id) + values = self._get_page_saving_values(profile, kw) + profile.sudo().write(values) + if redirect: + return request.redirect(redirect) + return request.redirect("/my/profiles") + + # This is just the form page opening. We send all the data needed for the form fields + values.update(self._get_page_opening_values()) + values.update( + { + "profile_id": profile_id, # Sent in order to retrieve it at submit time + "redirect": redirect + } + ) + return request.render("partner_profiles_portal.portal_my_profile", values) diff --git a/partner_profiles_portal/i18n/README b/partner_profiles_portal/i18n/README new file mode 100644 index 0000000..62197a1 --- /dev/null +++ b/partner_profiles_portal/i18n/README @@ -0,0 +1 @@ +This directory should contain the *.po for Odoo translation. diff --git a/partner_profiles_portal/models/__init__.py b/partner_profiles_portal/models/__init__.py new file mode 100644 index 0000000..cd5f4d8 --- /dev/null +++ b/partner_profiles_portal/models/__init__.py @@ -0,0 +1,3 @@ +# -*- coding: utf-8 -*- + +from . import res_partner \ No newline at end of file diff --git a/partner_profiles_portal/models/res_partner.py b/partner_profiles_portal/models/res_partner.py new file mode 100644 index 0000000..a1ff012 --- /dev/null +++ b/partner_profiles_portal/models/res_partner.py @@ -0,0 +1,50 @@ +# Copyright 2022 Elabore (https://elabore.coop) +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +import logging +from odoo import _, api, fields, models + +_logger = logging.getLogger(__name__) + + +class res_partner(models.Model): + _inherit = "res.partner" + + edit_structure_main_profile = fields.Boolean( + string=_("Manage structure's main profile") + ) + edit_structure_public_profile = fields.Boolean( + string=_("Manage structure's public profile") + ) + can_edit_main_profile_ids = fields.Many2many( + "res.partner", + relation="res_partner_main_profile_rel", + column1="partner_id", + column2="profile_id", + store=True, + compute="_compute_can_edit", + string="Can edit main profile", + ) + can_edit_public_profile_ids = fields.Many2many( + "res.partner", + relation="res_partner_public_profile_rel", + column1="partner_id", + column2="profile_id", + store=True, + compute="_compute_can_edit", + string="Can edit public profile", + ) + + @api.depends( + "other_contact_ids", + "other_contact_ids.edit_structure_main_profile", + "other_contact_ids.edit_structure_public_profile", + ) + def _compute_can_edit(self): + for partner in self: + partner.can_edit_main_profile_ids = partner.child_ids.filtered( + "edit_structure_main_profile" + ).mapped("contact_id") + partner.can_edit_public_profile_ids = partner.child_ids.filtered( + "edit_structure_public_profile" + ).mapped("contact_id") \ No newline at end of file diff --git a/partner_profiles_portal/security/members_security.xml b/partner_profiles_portal/security/members_security.xml new file mode 100644 index 0000000..a0fdde0 --- /dev/null +++ b/partner_profiles_portal/security/members_security.xml @@ -0,0 +1,13 @@ + + + + res_partner: portal: read/write access on my profiles + + ['|','|',('contact_id', '=', user.partner_id.id), + ('can_edit_main_profile_ids', 'in', [user.partner_id.id]), + ('can_edit_public_profile_ids', 'in', [user.partner_id.id])] + + + + + \ No newline at end of file diff --git a/partner_profiles_portal/views/portal_home_template.xml b/partner_profiles_portal/views/portal_home_template.xml new file mode 100644 index 0000000..3d14b73 --- /dev/null +++ b/partner_profiles_portal/views/portal_home_template.xml @@ -0,0 +1,24 @@ + + + + \ No newline at end of file diff --git a/partner_profiles_portal/views/portal_my_profiles_template.xml b/partner_profiles_portal/views/portal_my_profiles_template.xml new file mode 100644 index 0000000..cc6f84f --- /dev/null +++ b/partner_profiles_portal/views/portal_my_profiles_template.xml @@ -0,0 +1,97 @@ + + + + \ No newline at end of file diff --git a/partner_profiles_portal/views/portal_partner_profile_template.xml b/partner_profiles_portal/views/portal_partner_profile_template.xml new file mode 100644 index 0000000..298136d --- /dev/null +++ b/partner_profiles_portal/views/portal_partner_profile_template.xml @@ -0,0 +1,189 @@ + + + + \ No newline at end of file diff --git a/partner_profiles_portal/views/res_partner_view.xml b/partner_profiles_portal/views/res_partner_view.xml new file mode 100644 index 0000000..0904fc1 --- /dev/null +++ b/partner_profiles_portal/views/res_partner_view.xml @@ -0,0 +1,32 @@ + + + + + Partner Profiles Form View + res.partner + + 99 + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file