[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