[ADD] membership_status_in_portal: portal membership status badge
Add a generic module displaying the connected portal user's membership status in the account sidebar, below the "Edit information" link. The native Odoo membership label (``res.partner.membership_state`` from the ``membership`` module) is shown as a colored badge: - green (``text-bg-success``) for free / paid members; - yellow (``text-bg-warning``) for waiting / invoiced members; - red (``text-bg-danger``) otherwise. Color, icon and label are computed in the QWeb template; no model or field is added. Includes HttpCase tests covering member and non-member rendering and asserting no action button is rendered.
This commit is contained in:
2
membership_status_in_portal/.gitignore
vendored
Normal file
2
membership_status_in_portal/.gitignore
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
*.*~
|
||||
*pyc
|
||||
54
membership_status_in_portal/README.rst
Normal file
54
membership_status_in_portal/README.rst
Normal file
@@ -0,0 +1,54 @@
|
||||
==========================
|
||||
membership_status_in_portal
|
||||
==========================
|
||||
|
||||
Display the membership status of the connected user in the portal.
|
||||
|
||||
Description
|
||||
===========
|
||||
|
||||
In the portal account sidebar (all ``/my`` pages, including ``/my/home``),
|
||||
this module shows the native Odoo membership status of the connected user
|
||||
(``res.partner.membership_state`` provided by the ``membership`` module).
|
||||
|
||||
No registration or renewal button is displayed: non-members only see their
|
||||
status.
|
||||
|
||||
Installation
|
||||
============
|
||||
|
||||
Use Odoo normal module installation procedure to install
|
||||
``membership_status_in_portal``.
|
||||
|
||||
Known issues / Roadmap
|
||||
======================
|
||||
|
||||
None yet.
|
||||
|
||||
Bug Tracker
|
||||
===========
|
||||
|
||||
Bugs are tracked on `our issues website <https://git.elabore.coop/elabore/member-tools/issues>`_. 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.
|
||||
0
membership_status_in_portal/__init__.py
Normal file
0
membership_status_in_portal/__init__.py
Normal file
23
membership_status_in_portal/__manifest__.py
Normal file
23
membership_status_in_portal/__manifest__.py
Normal file
@@ -0,0 +1,23 @@
|
||||
# Copyright 2026 Stéphan Sainléger (Elabore)
|
||||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
|
||||
|
||||
{
|
||||
"name": "membership_status_in_portal",
|
||||
"version": "18.0.1.0.0",
|
||||
"author": "Elabore",
|
||||
"maintainer": "Elabore",
|
||||
"website": "https://git.elabore.coop/elabore/member-tools",
|
||||
"license": "AGPL-3",
|
||||
"category": "Website",
|
||||
"summary": "Display the membership status of portal users",
|
||||
"depends": [
|
||||
"portal",
|
||||
"membership",
|
||||
],
|
||||
"data": [
|
||||
"views/portal_sidebar.xml",
|
||||
],
|
||||
"installable": True,
|
||||
"auto_install": False,
|
||||
"application": False,
|
||||
}
|
||||
1
membership_status_in_portal/tests/__init__.py
Normal file
1
membership_status_in_portal/tests/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
from . import test_portal_sidebar
|
||||
66
membership_status_in_portal/tests/test_portal_sidebar.py
Normal file
66
membership_status_in_portal/tests/test_portal_sidebar.py
Normal file
@@ -0,0 +1,66 @@
|
||||
import re
|
||||
|
||||
from odoo.tests import HttpCase, tagged
|
||||
|
||||
|
||||
@tagged("post_install", "-at_install")
|
||||
class MembershipStatusPortalCase(HttpCase):
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super().setUpClass()
|
||||
cls.portal_group = cls.env.ref("base.group_portal")
|
||||
|
||||
def _create_portal_user(self, login, **partner_vals):
|
||||
partner = self.env["res.partner"].create(
|
||||
{
|
||||
"name": login,
|
||||
"lang": self.env.lang or "en_US",
|
||||
**partner_vals,
|
||||
}
|
||||
)
|
||||
return self.env["res.users"].create(
|
||||
{
|
||||
"name": login,
|
||||
"login": login,
|
||||
"password": login,
|
||||
"partner_id": partner.id,
|
||||
"groups_id": [(6, 0, [self.portal_group.id])],
|
||||
}
|
||||
)
|
||||
|
||||
def _membership_label(self, partner):
|
||||
selection = self.env["res.partner"].fields_get(["membership_state"])[
|
||||
"membership_state"
|
||||
]["selection"]
|
||||
return dict(selection)[partner.membership_state]
|
||||
|
||||
def _open_my_home(self, user):
|
||||
self.authenticate(user.login, user.password)
|
||||
response = self.url_open("/my/home")
|
||||
self.assertEqual(response.status_code, 200)
|
||||
return response.text
|
||||
|
||||
def _extract_membership_block(self, html):
|
||||
match = re.search(r'<div class="o_membership_state.*?</div>', html, re.DOTALL)
|
||||
self.assertIsNotNone(match, "membership status block not found on /my/home")
|
||||
return match.group(0)
|
||||
|
||||
def test_non_member_sees_status_without_button(self):
|
||||
user = self._create_portal_user("nonmember")
|
||||
self.assertEqual(user.partner_id.membership_state, "none")
|
||||
|
||||
block = self._extract_membership_block(self._open_my_home(user))
|
||||
|
||||
self.assertIn(self._membership_label(user.partner_id), block)
|
||||
self.assertNotIn("<button", block)
|
||||
self.assertNotIn("<a ", block)
|
||||
|
||||
def test_member_sees_status_without_button(self):
|
||||
user = self._create_portal_user("member", free_member=True)
|
||||
self.assertEqual(user.partner_id.membership_state, "free")
|
||||
|
||||
block = self._extract_membership_block(self._open_my_home(user))
|
||||
|
||||
self.assertIn(self._membership_label(user.partner_id), block)
|
||||
self.assertNotIn("<button", block)
|
||||
self.assertNotIn("<a ", block)
|
||||
34
membership_status_in_portal/views/portal_sidebar.xml
Normal file
34
membership_status_in_portal/views/portal_sidebar.xml
Normal file
@@ -0,0 +1,34 @@
|
||||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<odoo>
|
||||
<template
|
||||
id="portal_side_content_membership_status"
|
||||
name="Portal Sidebar: Membership Status"
|
||||
inherit_id="portal.side_content"
|
||||
priority="40"
|
||||
>
|
||||
<xpath expr="//a[@href='/my/account']" position="after">
|
||||
<div class="o_membership_state mt-3">
|
||||
<t t-set="state" t-value="user_id.partner_id.membership_state" />
|
||||
<t
|
||||
t-set="state_label"
|
||||
t-value="dict(user_id.partner_id.fields_get(['membership_state'])['membership_state']['selection']).get(state)"
|
||||
/>
|
||||
<t
|
||||
t-set="state_color"
|
||||
t-value="{'free': 'success', 'paid': 'success', 'invoiced': 'warning', 'waiting': 'warning'}.get(state, 'danger')"
|
||||
/>
|
||||
<t
|
||||
t-set="state_icon"
|
||||
t-value="{'free': 'fa-check', 'paid': 'fa-check', 'invoiced': 'fa-clock-o', 'waiting': 'fa-clock-o'}.get(state, 'fa-remove')"
|
||||
/>
|
||||
<span
|
||||
t-attf-class="badge rounded-pill text-bg-#{state_color} fs-6"
|
||||
t-att-title="state_label"
|
||||
>
|
||||
<i t-attf-class="fa fa-fw #{state_icon}" role="img" />
|
||||
<span t-out="state_label" class="ms-1" />
|
||||
</span>
|
||||
</div>
|
||||
</xpath>
|
||||
</template>
|
||||
</odoo>
|
||||
Reference in New Issue
Block a user