add website_membership_associate
This commit is contained in:
parent
40d7cc8772
commit
31d53c428c
12 changed files with 837 additions and 0 deletions
54
website_membership_associate/README.md
Normal file
54
website_membership_associate/README.md
Normal file
|
|
@ -0,0 +1,54 @@
|
||||||
|
# Website Membership Associate
|
||||||
|
|
||||||
|
Show associate members in the public `/members` directory of `website_membership`.
|
||||||
|
|
||||||
|
## Problem
|
||||||
|
|
||||||
|
The standard `website_membership` module builds the public members directory from
|
||||||
|
`membership.membership_line` records. Partners that are linked as associate
|
||||||
|
members (`res.partner.associate_member`) inherit the membership status of their
|
||||||
|
host partner and do not own a membership line, so they are not shown in the
|
||||||
|
public directory even when they are published.
|
||||||
|
|
||||||
|
## Solution
|
||||||
|
|
||||||
|
The module follows the same pattern as OCA's
|
||||||
|
`website_membership_non_paid_member`:
|
||||||
|
|
||||||
|
- The `members()` controller injects the context key
|
||||||
|
`include_associate_members=True` and delegates pagination, grouping and search
|
||||||
|
to the standard `website_membership` controller.
|
||||||
|
- `membership.membership_line._search()` detects that context key and widens the
|
||||||
|
domain so that a host is returned when the active country or search filters
|
||||||
|
match one of its published associates.
|
||||||
|
- The inherited controller post-processes the rendered context to add the
|
||||||
|
current page's associates under the same membership group (or under
|
||||||
|
"Free Members") as their host, keep associates under their own country in the
|
||||||
|
country sidebar, hide hosts that only appear because an associate matched the
|
||||||
|
active country/search filter, and include published associate companies in the
|
||||||
|
Google Map markers.
|
||||||
|
|
||||||
|
No fake membership lines are created and no core or OCA addon is modified.
|
||||||
|
|
||||||
|
## Supported features
|
||||||
|
|
||||||
|
- Pagination over hosts (`_references_per_page`) with associates rendered on the
|
||||||
|
same page.
|
||||||
|
- Grouping by membership product and "Free Members".
|
||||||
|
- Country grouping and filtering (associates are counted under their own
|
||||||
|
country).
|
||||||
|
- Name / website description search.
|
||||||
|
- Google Map markers (when the optional view is enabled).
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
No configuration is required. Install the module and publish the partners that
|
||||||
|
must appear on the website.
|
||||||
|
|
||||||
|
## Development notes
|
||||||
|
|
||||||
|
- All business logic is in Python controllers/models; no QWeb logic is added.
|
||||||
|
- No source code of `website_membership`, `membership` or other core/OCA addons
|
||||||
|
is modified.
|
||||||
|
- No fake membership lines are created for associate members.
|
||||||
|
- Only one level of association (`associate_member`) is supported.
|
||||||
4
website_membership_associate/__init__.py
Normal file
4
website_membership_associate/__init__.py
Normal file
|
|
@ -0,0 +1,4 @@
|
||||||
|
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
|
||||||
|
|
||||||
|
from . import controllers
|
||||||
|
from . import models
|
||||||
17
website_membership_associate/__manifest__.py
Normal file
17
website_membership_associate/__manifest__.py
Normal file
|
|
@ -0,0 +1,17 @@
|
||||||
|
# Copyright 2026 Criptomart
|
||||||
|
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
|
||||||
|
|
||||||
|
{
|
||||||
|
"name": "Website Membership Associate",
|
||||||
|
"version": "18.0.1.1.0",
|
||||||
|
"category": "Website/Membership",
|
||||||
|
"summary": "Show associate members in the public /members directory.",
|
||||||
|
"author": "Criptomart",
|
||||||
|
"website": "https://git.criptomart.net/criptomart/addons-cm",
|
||||||
|
"license": "AGPL-3",
|
||||||
|
"depends": [
|
||||||
|
"membership",
|
||||||
|
"website_membership",
|
||||||
|
],
|
||||||
|
"data": [],
|
||||||
|
}
|
||||||
3
website_membership_associate/controllers/__init__.py
Normal file
3
website_membership_associate/controllers/__init__.py
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
|
||||||
|
|
||||||
|
from . import main
|
||||||
293
website_membership_associate/controllers/main.py
Normal file
293
website_membership_associate/controllers/main.py
Normal file
|
|
@ -0,0 +1,293 @@
|
||||||
|
# Copyright 2026 Criptomart
|
||||||
|
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
|
||||||
|
|
||||||
|
from odoo import fields, http
|
||||||
|
from odoo.http import request
|
||||||
|
from odoo.tools.translate import _
|
||||||
|
|
||||||
|
from odoo.addons.website_membership.controllers.main import (
|
||||||
|
WebsiteMembership as BaseWebsiteMembership,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class WebsiteMembership(BaseWebsiteMembership):
|
||||||
|
@http.route()
|
||||||
|
def members(
|
||||||
|
self, membership_id=None, country_name=None, country_id=0, page=1, **post
|
||||||
|
):
|
||||||
|
request.env = request.env(
|
||||||
|
context=dict(request.env.context, include_associate_members=True)
|
||||||
|
)
|
||||||
|
response = super().members(
|
||||||
|
membership_id=membership_id,
|
||||||
|
country_name=country_name,
|
||||||
|
country_id=country_id,
|
||||||
|
page=page,
|
||||||
|
**post,
|
||||||
|
)
|
||||||
|
self._add_associate_members(response, membership_id, country_id, page, post)
|
||||||
|
return response
|
||||||
|
|
||||||
|
def _add_associate_members(
|
||||||
|
self, response, membership_id, country_id, page, post
|
||||||
|
):
|
||||||
|
values = response.qcontext
|
||||||
|
memberships_partner_ids = values.get("memberships_partner_ids", {})
|
||||||
|
if not memberships_partner_ids:
|
||||||
|
return
|
||||||
|
|
||||||
|
post_name = post.get("search") or post.get("name", "")
|
||||||
|
host_keys = {}
|
||||||
|
for key, ids in memberships_partner_ids.items():
|
||||||
|
for partner_id in ids:
|
||||||
|
host_keys.setdefault(partner_id, []).append(key)
|
||||||
|
host_ids = set(host_keys.keys())
|
||||||
|
hosts = {
|
||||||
|
pid: partner
|
||||||
|
for pid, partner in values.get("partners", {}).items()
|
||||||
|
if pid in host_ids
|
||||||
|
}
|
||||||
|
|
||||||
|
associates = (
|
||||||
|
request.env["membership.membership_line"]
|
||||||
|
.sudo()
|
||||||
|
._get_associate_partners(
|
||||||
|
tuple(host_ids), country_id=country_id, post_name=post_name
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
new_memberships_partner_ids = self._filter_displayed_hosts(
|
||||||
|
memberships_partner_ids, hosts, country_id, post_name
|
||||||
|
)
|
||||||
|
for associate in associates:
|
||||||
|
keys = host_keys.get(associate.associate_member.id)
|
||||||
|
if not keys:
|
||||||
|
continue
|
||||||
|
for key in keys:
|
||||||
|
if associate.id not in new_memberships_partner_ids[key]:
|
||||||
|
new_memberships_partner_ids[key].append(associate.id)
|
||||||
|
values["memberships_partner_ids"] = {
|
||||||
|
key: ids
|
||||||
|
for key, ids in new_memberships_partner_ids.items()
|
||||||
|
if ids
|
||||||
|
}
|
||||||
|
|
||||||
|
displayed_ids = set(
|
||||||
|
pid
|
||||||
|
for ids in values["memberships_partner_ids"].values()
|
||||||
|
for pid in ids
|
||||||
|
)
|
||||||
|
values["partners"] = {
|
||||||
|
p.id: p
|
||||||
|
for p in request.env["res.partner"]
|
||||||
|
.sudo()
|
||||||
|
.browse(list(displayed_ids))
|
||||||
|
}
|
||||||
|
|
||||||
|
all_host_ids, all_free_host_ids, all_associates = (
|
||||||
|
self._get_all_visible_ids(membership_id, country_id, post_name)
|
||||||
|
)
|
||||||
|
self._recompute_countries(
|
||||||
|
values, all_host_ids, all_free_host_ids, all_associates, country_id
|
||||||
|
)
|
||||||
|
self._update_google_map(values, country_id, post_name)
|
||||||
|
self._update_pager(
|
||||||
|
values,
|
||||||
|
membership_id,
|
||||||
|
country_id,
|
||||||
|
page,
|
||||||
|
post,
|
||||||
|
all_host_ids,
|
||||||
|
all_free_host_ids,
|
||||||
|
)
|
||||||
|
|
||||||
|
def _filter_displayed_hosts(
|
||||||
|
self, memberships_partner_ids, hosts, country_id, post_name
|
||||||
|
):
|
||||||
|
return {
|
||||||
|
key: [
|
||||||
|
pid
|
||||||
|
for pid in ids
|
||||||
|
if pid not in hosts
|
||||||
|
or self._partner_matches_filters(hosts[pid], country_id, post_name)
|
||||||
|
]
|
||||||
|
for key, ids in memberships_partner_ids.items()
|
||||||
|
}
|
||||||
|
|
||||||
|
def _partner_matches_filters(self, partner, country_id, post_name):
|
||||||
|
if country_id and partner.country_id.id != country_id:
|
||||||
|
return False
|
||||||
|
if post_name and not self._partner_matches_search(partner, post_name):
|
||||||
|
return False
|
||||||
|
return True
|
||||||
|
|
||||||
|
def _partner_matches_search(self, partner, post_name):
|
||||||
|
term = post_name.lower()
|
||||||
|
if term in (partner.name or "").lower():
|
||||||
|
return True
|
||||||
|
if term in (partner.website_description or "").lower():
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
def _get_all_visible_ids(self, membership_id, country_id, post_name):
|
||||||
|
free_selected = membership_id in (None, "free")
|
||||||
|
if membership_id == "free":
|
||||||
|
all_host_ids = set()
|
||||||
|
else:
|
||||||
|
all_host_ids = self._get_all_host_ids(
|
||||||
|
membership_id, country_id, post_name
|
||||||
|
)
|
||||||
|
all_free_host_ids = (
|
||||||
|
self._get_all_free_host_ids(country_id, post_name)
|
||||||
|
if free_selected
|
||||||
|
else set()
|
||||||
|
)
|
||||||
|
all_associates = (
|
||||||
|
request.env["membership.membership_line"]
|
||||||
|
.sudo()
|
||||||
|
._get_associate_partners(
|
||||||
|
tuple(all_host_ids | all_free_host_ids),
|
||||||
|
country_id=country_id,
|
||||||
|
post_name=post_name,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return all_host_ids, all_free_host_ids, all_associates
|
||||||
|
|
||||||
|
def _get_all_host_ids(self, membership_id, country_id, post_name):
|
||||||
|
Product = request.env["product.product"]
|
||||||
|
MembershipLine = request.env["membership.membership_line"]
|
||||||
|
today = fields.Date.today()
|
||||||
|
products = Product.sudo().search([("membership", "=", True)])
|
||||||
|
domain = [
|
||||||
|
("partner.website_published", "=", True),
|
||||||
|
("state", "=", "paid"),
|
||||||
|
("date_to", ">=", today),
|
||||||
|
("date_from", "<=", today),
|
||||||
|
("membership_id", "in", products.ids),
|
||||||
|
]
|
||||||
|
if membership_id and membership_id != "free":
|
||||||
|
domain.append(("membership_id", "=", int(membership_id)))
|
||||||
|
if post_name:
|
||||||
|
domain += [
|
||||||
|
"|",
|
||||||
|
("partner.name", "ilike", post_name),
|
||||||
|
("partner.website_description", "ilike", post_name),
|
||||||
|
]
|
||||||
|
if country_id:
|
||||||
|
domain.append(("partner.country_id", "=", country_id))
|
||||||
|
lines = MembershipLine.sudo().search(domain)
|
||||||
|
return set(lines.partner.ids)
|
||||||
|
|
||||||
|
def _get_all_free_host_ids(self, country_id, post_name):
|
||||||
|
domain = [
|
||||||
|
("membership_state", "=", "free"),
|
||||||
|
("website_published", "=", True),
|
||||||
|
]
|
||||||
|
if post_name:
|
||||||
|
domain += [
|
||||||
|
"|",
|
||||||
|
("name", "ilike", post_name),
|
||||||
|
("website_description", "ilike", post_name),
|
||||||
|
]
|
||||||
|
if country_id:
|
||||||
|
domain.append(("country_id", "=", country_id))
|
||||||
|
return set(request.env["res.partner"].sudo().search(domain).ids)
|
||||||
|
|
||||||
|
def _recompute_countries(
|
||||||
|
self, values, all_host_ids, all_free_host_ids, all_associates, country_id
|
||||||
|
):
|
||||||
|
visible_ids = all_host_ids | all_free_host_ids | set(all_associates.ids)
|
||||||
|
values["countries"] = self._build_countries(visible_ids, country_id)
|
||||||
|
|
||||||
|
def _build_countries(self, visible_ids, country_id):
|
||||||
|
Partner = request.env["res.partner"]
|
||||||
|
Country = request.env["res.country"]
|
||||||
|
current_country = None
|
||||||
|
countries = Partner.sudo().read_group(
|
||||||
|
[("id", "in", list(visible_ids))],
|
||||||
|
["__count"],
|
||||||
|
groupby="country_id",
|
||||||
|
)
|
||||||
|
countries_total = sum(
|
||||||
|
country_dict["country_id_count"] for country_dict in countries
|
||||||
|
)
|
||||||
|
if country_id:
|
||||||
|
current_country = Country.browse(country_id).read(["id", "name"])[0]
|
||||||
|
if not any(
|
||||||
|
x["country_id"] and x["country_id"][0] == country_id
|
||||||
|
for x in countries
|
||||||
|
):
|
||||||
|
countries.append(
|
||||||
|
{
|
||||||
|
"country_id_count": 0,
|
||||||
|
"country_id": (country_id, current_country["name"]),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
countries = [d for d in countries if d["country_id"]]
|
||||||
|
countries.sort(key=lambda d: d["country_id"][1])
|
||||||
|
countries.insert(
|
||||||
|
0,
|
||||||
|
{
|
||||||
|
"country_id_count": countries_total,
|
||||||
|
"country_id": (0, _("All Countries")),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
return countries
|
||||||
|
|
||||||
|
def _update_google_map(self, values, country_id, post_name):
|
||||||
|
map_ids_str = values.get("google_map_partner_ids", "")
|
||||||
|
if not map_ids_str:
|
||||||
|
return
|
||||||
|
map_ids = [int(pid) for pid in map_ids_str.split(",") if pid]
|
||||||
|
if not map_ids:
|
||||||
|
return
|
||||||
|
partners = request.env["res.partner"].sudo().browse(map_ids)
|
||||||
|
if country_id:
|
||||||
|
partners = partners.filtered(
|
||||||
|
lambda p: p.country_id.id == country_id
|
||||||
|
)
|
||||||
|
if post_name:
|
||||||
|
partners = partners.filtered(
|
||||||
|
lambda p: self._partner_matches_search(p, post_name)
|
||||||
|
)
|
||||||
|
values["google_map_partner_ids"] = ",".join(
|
||||||
|
str(pid) for pid in partners.ids
|
||||||
|
)
|
||||||
|
|
||||||
|
def _update_pager(
|
||||||
|
self,
|
||||||
|
values,
|
||||||
|
membership_id,
|
||||||
|
country_id,
|
||||||
|
page,
|
||||||
|
post,
|
||||||
|
all_host_ids,
|
||||||
|
all_free_host_ids,
|
||||||
|
):
|
||||||
|
post_name = post.get("search") or post.get("name", "")
|
||||||
|
if membership_id == "free":
|
||||||
|
displayed_host_ids = set()
|
||||||
|
else:
|
||||||
|
host_records = request.env["res.partner"].sudo().browse(
|
||||||
|
list(all_host_ids)
|
||||||
|
)
|
||||||
|
displayed_host_ids = {
|
||||||
|
host.id
|
||||||
|
for host in host_records
|
||||||
|
if self._partner_matches_filters(host, country_id, post_name)
|
||||||
|
}
|
||||||
|
count_members = len(displayed_host_ids) + len(all_free_host_ids)
|
||||||
|
limit = self._references_per_page
|
||||||
|
base_url = "/members%s%s" % (
|
||||||
|
"/association/%s" % membership_id if membership_id else "",
|
||||||
|
"/country/%s" % country_id if country_id else "",
|
||||||
|
)
|
||||||
|
values["pager"] = request.website.pager(
|
||||||
|
url=base_url,
|
||||||
|
total=count_members,
|
||||||
|
page=page,
|
||||||
|
step=limit,
|
||||||
|
scope=7,
|
||||||
|
url_args=post,
|
||||||
|
)
|
||||||
|
values["search_count"] = count_members
|
||||||
31
website_membership_associate/i18n/es.po
Normal file
31
website_membership_associate/i18n/es.po
Normal file
|
|
@ -0,0 +1,31 @@
|
||||||
|
# Translation of Odoo Server.
|
||||||
|
# This file contains the translation of the following modules:
|
||||||
|
# * website_membership_associate
|
||||||
|
#
|
||||||
|
msgid ""
|
||||||
|
msgstr ""
|
||||||
|
"Project-Id-Version: Odoo Server 18.0\n"
|
||||||
|
"Report-Msgid-Bugs-To: \n"
|
||||||
|
"POT-Creation-Date: 2026-07-27 00:00+0000\n"
|
||||||
|
"PO-Revision-Date: 2026-07-27 00:00+0000\n"
|
||||||
|
"Last-Translator: \n"
|
||||||
|
"Language-Team: \n"
|
||||||
|
"MIME-Version: 1.0\n"
|
||||||
|
"Content-Type: text/plain; charset=UTF-8\n"
|
||||||
|
"Content-Transfer-Encoding: \n"
|
||||||
|
"Plural-Forms: \n"
|
||||||
|
"Language: es\n"
|
||||||
|
|
||||||
|
#. module: website_membership_associate
|
||||||
|
#. odoo-python
|
||||||
|
#: code:addons/website_membership_associate/controllers/main.py:0
|
||||||
|
#, python-format
|
||||||
|
msgid "All Countries"
|
||||||
|
msgstr "Todos los países"
|
||||||
|
|
||||||
|
#. module: website_membership_associate
|
||||||
|
#. odoo-python
|
||||||
|
#: code:addons/website_membership_associate/controllers/main.py:0
|
||||||
|
#, python-format
|
||||||
|
msgid "Free Members"
|
||||||
|
msgstr "Miembros gratuitos"
|
||||||
31
website_membership_associate/i18n/eu.po
Normal file
31
website_membership_associate/i18n/eu.po
Normal file
|
|
@ -0,0 +1,31 @@
|
||||||
|
# Translation of Odoo Server.
|
||||||
|
# This file contains the translation of the following modules:
|
||||||
|
# * website_membership_associate
|
||||||
|
#
|
||||||
|
msgid ""
|
||||||
|
msgstr ""
|
||||||
|
"Project-Id-Version: Odoo Server 18.0\n"
|
||||||
|
"Report-Msgid-Bugs-To: \n"
|
||||||
|
"POT-Creation-Date: 2026-07-27 00:00+0000\n"
|
||||||
|
"PO-Revision-Date: 2026-07-27 00:00+0000\n"
|
||||||
|
"Last-Translator: \n"
|
||||||
|
"Language-Team: \n"
|
||||||
|
"MIME-Version: 1.0\n"
|
||||||
|
"Content-Type: text/plain; charset=UTF-8\n"
|
||||||
|
"Content-Transfer-Encoding: \n"
|
||||||
|
"Plural-Forms: \n"
|
||||||
|
"Language: eu\n"
|
||||||
|
|
||||||
|
#. module: website_membership_associate
|
||||||
|
#. odoo-python
|
||||||
|
#: code:addons/website_membership_associate/controllers/main.py:0
|
||||||
|
#, python-format
|
||||||
|
msgid "All Countries"
|
||||||
|
msgstr "Herrialde guztiak"
|
||||||
|
|
||||||
|
#. module: website_membership_associate
|
||||||
|
#. odoo-python
|
||||||
|
#: code:addons/website_membership_associate/controllers/main.py:0
|
||||||
|
#, python-format
|
||||||
|
msgid "Free Members"
|
||||||
|
msgstr "Bazkide libreak"
|
||||||
|
|
@ -0,0 +1,30 @@
|
||||||
|
# Translation of Odoo Server.
|
||||||
|
# This file contains the translation of the following modules:
|
||||||
|
# * website_membership_associate
|
||||||
|
#
|
||||||
|
msgid ""
|
||||||
|
msgstr ""
|
||||||
|
"Project-Id-Version: Odoo Server 18.0\n"
|
||||||
|
"Report-Msgid-Bugs-To: \n"
|
||||||
|
"POT-Creation-Date: 2026-07-27 00:00+0000\n"
|
||||||
|
"PO-Revision-Date: 2026-07-27 00:00+0000\n"
|
||||||
|
"Last-Translator: \n"
|
||||||
|
"Language-Team: \n"
|
||||||
|
"MIME-Version: 1.0\n"
|
||||||
|
"Content-Type: text/plain; charset=UTF-8\n"
|
||||||
|
"Content-Transfer-Encoding: \n"
|
||||||
|
"Plural-Forms: \n"
|
||||||
|
|
||||||
|
#. module: website_membership_associate
|
||||||
|
#. odoo-python
|
||||||
|
#: code:addons/website_membership_associate/controllers/main.py:0
|
||||||
|
#, python-format
|
||||||
|
msgid "All Countries"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#. module: website_membership_associate
|
||||||
|
#. odoo-python
|
||||||
|
#: code:addons/website_membership_associate/controllers/main.py:0
|
||||||
|
#, python-format
|
||||||
|
msgid "Free Members"
|
||||||
|
msgstr ""
|
||||||
3
website_membership_associate/models/__init__.py
Normal file
3
website_membership_associate/models/__init__.py
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
|
||||||
|
|
||||||
|
from . import membership
|
||||||
91
website_membership_associate/models/membership.py
Normal file
91
website_membership_associate/models/membership.py
Normal file
|
|
@ -0,0 +1,91 @@
|
||||||
|
# Copyright 2026 Criptomart
|
||||||
|
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
|
||||||
|
|
||||||
|
from odoo import api, models
|
||||||
|
from odoo.osv import expression
|
||||||
|
|
||||||
|
|
||||||
|
class MembershipLine(models.Model):
|
||||||
|
_inherit = "membership.membership_line"
|
||||||
|
|
||||||
|
def _search(self, domain, offset=0, limit=None, order=None):
|
||||||
|
if self.env.context.get("include_associate_members"):
|
||||||
|
domain = self._expand_domain_for_associates(domain)
|
||||||
|
return super()._search(domain, offset=offset, limit=limit, order=order)
|
||||||
|
|
||||||
|
def _expand_domain_for_associates(self, domain):
|
||||||
|
country_id = None
|
||||||
|
post_name = None
|
||||||
|
for leaf in domain:
|
||||||
|
if isinstance(leaf, (list, tuple)) and len(leaf) == 3:
|
||||||
|
field, operator, value = leaf
|
||||||
|
if field == "partner.country_id" and operator == "=":
|
||||||
|
country_id = value
|
||||||
|
elif (
|
||||||
|
field in ("partner.name", "partner.website_description")
|
||||||
|
and operator == "ilike"
|
||||||
|
):
|
||||||
|
post_name = value
|
||||||
|
if country_id is None and not post_name:
|
||||||
|
return domain
|
||||||
|
associate_domain = [
|
||||||
|
("associate_member", "!=", False),
|
||||||
|
("website_published", "=", True),
|
||||||
|
]
|
||||||
|
if country_id is not None:
|
||||||
|
associate_domain.append(("country_id", "=", country_id))
|
||||||
|
if post_name:
|
||||||
|
associate_domain += [
|
||||||
|
"|",
|
||||||
|
("name", "ilike", post_name),
|
||||||
|
("website_description", "ilike", post_name),
|
||||||
|
]
|
||||||
|
host_ids = (
|
||||||
|
self.env["res.partner"]
|
||||||
|
.sudo()
|
||||||
|
.search(associate_domain)
|
||||||
|
.associate_member.ids
|
||||||
|
)
|
||||||
|
if not host_ids:
|
||||||
|
return domain
|
||||||
|
return expression.OR([domain, [("partner", "in", host_ids)]])
|
||||||
|
|
||||||
|
@api.model
|
||||||
|
def _get_associate_partners(
|
||||||
|
self, host_partner_ids, country_id=None, post_name=None
|
||||||
|
):
|
||||||
|
if not host_partner_ids:
|
||||||
|
return self.env["res.partner"]
|
||||||
|
domain = [
|
||||||
|
("associate_member", "in", tuple(host_partner_ids)),
|
||||||
|
("website_published", "=", True),
|
||||||
|
]
|
||||||
|
if country_id:
|
||||||
|
domain.append(("country_id", "=", country_id))
|
||||||
|
if post_name:
|
||||||
|
domain += [
|
||||||
|
"|",
|
||||||
|
("name", "ilike", post_name),
|
||||||
|
("website_description", "ilike", post_name),
|
||||||
|
]
|
||||||
|
return self.env["res.partner"].sudo().search(domain)
|
||||||
|
|
||||||
|
def _get_published_companies(self, limit=None):
|
||||||
|
if not self.ids:
|
||||||
|
return []
|
||||||
|
limit_clause = "" if limit is None else " LIMIT %d" % limit
|
||||||
|
self.env.cr.execute(
|
||||||
|
"""
|
||||||
|
SELECT DISTINCT p.id
|
||||||
|
FROM res_partner p
|
||||||
|
INNER JOIN membership_membership_line m
|
||||||
|
ON (p.id = m.partner OR p.associate_member = m.partner)
|
||||||
|
WHERE p.is_published
|
||||||
|
AND p.is_company
|
||||||
|
AND m.id IN %s
|
||||||
|
ORDER BY p.id
|
||||||
|
"""
|
||||||
|
+ limit_clause,
|
||||||
|
(tuple(self.ids),),
|
||||||
|
)
|
||||||
|
return [row[0] for row in self.env.cr.fetchall()]
|
||||||
3
website_membership_associate/tests/__init__.py
Normal file
3
website_membership_associate/tests/__init__.py
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
|
||||||
|
|
||||||
|
from . import test_website_membership_associate
|
||||||
|
|
@ -0,0 +1,277 @@
|
||||||
|
# Copyright 2026 Criptomart
|
||||||
|
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
|
||||||
|
|
||||||
|
from odoo import fields
|
||||||
|
from odoo.tests import HttpCase, tagged
|
||||||
|
|
||||||
|
from odoo.addons.membership.tests.common import TestMembershipCommon
|
||||||
|
|
||||||
|
|
||||||
|
@tagged("post_install", "-at_install")
|
||||||
|
class TestWebsiteMembershipAssociate(HttpCase, TestMembershipCommon):
|
||||||
|
@classmethod
|
||||||
|
def setUpClass(cls):
|
||||||
|
super().setUpClass()
|
||||||
|
cls.website = cls.env.ref("website.default_website")
|
||||||
|
cls.country_us = cls.env.ref("base.us")
|
||||||
|
cls.country_be = cls.env.ref("base.be")
|
||||||
|
|
||||||
|
cls.membership_1.write({"website_published": True})
|
||||||
|
cls.partner_1.write(
|
||||||
|
{
|
||||||
|
"website_published": True,
|
||||||
|
"is_company": True,
|
||||||
|
"country_id": cls.country_us.id,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
invoice = cls.partner_1.create_membership_invoice(cls.membership_1, 75.0)
|
||||||
|
invoice.action_post()
|
||||||
|
cls.env["account.payment.register"].with_context(
|
||||||
|
active_model="account.move", active_ids=invoice.ids
|
||||||
|
).create(
|
||||||
|
{
|
||||||
|
"amount": 86.25,
|
||||||
|
"payment_method_line_id": cls.inbound_payment_method_line.id,
|
||||||
|
}
|
||||||
|
)._create_payments()
|
||||||
|
cls.partner_1._compute_membership_state()
|
||||||
|
|
||||||
|
cls.associate = cls.env["res.partner"].create(
|
||||||
|
{
|
||||||
|
"name": "Associate Paid",
|
||||||
|
"associate_member": cls.partner_1.id,
|
||||||
|
"website_published": True,
|
||||||
|
"is_company": True,
|
||||||
|
"country_id": cls.country_be.id,
|
||||||
|
"website_description": "Sustainable farming",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
cls.partner_2.write(
|
||||||
|
{
|
||||||
|
"website_published": True,
|
||||||
|
"is_company": True,
|
||||||
|
"country_id": cls.country_us.id,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
cls.associate_free = cls.env["res.partner"].create(
|
||||||
|
{
|
||||||
|
"name": "Associate Free",
|
||||||
|
"associate_member": cls.partner_2.id,
|
||||||
|
"website_published": True,
|
||||||
|
"is_company": True,
|
||||||
|
"country_id": cls.country_us.id,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_host_membership_state_is_paid(self):
|
||||||
|
self.assertEqual(self.partner_1.membership_state, "paid")
|
||||||
|
|
||||||
|
def test_members_page_lists_host_and_associate(self):
|
||||||
|
response = self.url_open("/members")
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
self.assertIn(self.partner_1.name, response.text)
|
||||||
|
self.assertIn(self.associate.name, response.text)
|
||||||
|
|
||||||
|
def test_membership_filter_includes_associate(self):
|
||||||
|
response = self.url_open("/members/association/%d" % self.membership_1.id)
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
self.assertIn(self.partner_1.name, response.text)
|
||||||
|
self.assertIn(self.associate.name, response.text)
|
||||||
|
|
||||||
|
def test_free_membership_filter_includes_associate(self):
|
||||||
|
response = self.url_open("/members/association/free")
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
self.assertIn(self.partner_2.name, response.text)
|
||||||
|
self.assertIn(self.associate_free.name, response.text)
|
||||||
|
|
||||||
|
def test_country_filter_counts_associate(self):
|
||||||
|
response = self.url_open("/members/country/%d" % self.country_be.id)
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
self.assertIn(self.associate.name, response.text)
|
||||||
|
self.assertNotIn(self.partner_1.name, response.text)
|
||||||
|
self.assertNotIn(self.partner_2.name, response.text)
|
||||||
|
self.assertNotIn(self.associate_free.name, response.text)
|
||||||
|
|
||||||
|
def test_search_finds_associate(self):
|
||||||
|
response = self.url_open("/members?search=Sustainable+farming")
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
self.assertIn(self.associate.name, response.text)
|
||||||
|
self.assertNotIn(self.partner_1.name, response.text)
|
||||||
|
|
||||||
|
def test_published_companies_map_includes_associate(self):
|
||||||
|
host_line = self.partner_1.member_lines[0]
|
||||||
|
companies = host_line._get_published_companies(limit=2000)
|
||||||
|
self.assertIn(self.partner_1.id, companies)
|
||||||
|
self.assertIn(self.associate.id, companies)
|
||||||
|
|
||||||
|
|
||||||
|
@tagged("post_install", "-at_install")
|
||||||
|
class TestWebsiteMembershipAssociatePagination(HttpCase, TestMembershipCommon):
|
||||||
|
@classmethod
|
||||||
|
def setUpClass(cls):
|
||||||
|
super().setUpClass()
|
||||||
|
cls.website = cls.env.ref("website.default_website")
|
||||||
|
cls.country_es = cls.env.ref("base.es")
|
||||||
|
cls.membership_1.write({"website_published": True})
|
||||||
|
|
||||||
|
host_vals = [
|
||||||
|
{
|
||||||
|
"name": "Host Page %d" % i,
|
||||||
|
"website_published": True,
|
||||||
|
"is_company": True,
|
||||||
|
"country_id": cls.country_es.id,
|
||||||
|
}
|
||||||
|
for i in range(20)
|
||||||
|
]
|
||||||
|
cls.paginated_hosts = cls.env["res.partner"].create(host_vals)
|
||||||
|
invoices = cls.paginated_hosts.create_membership_invoice(
|
||||||
|
cls.membership_1, 75.0
|
||||||
|
)
|
||||||
|
invoices.action_post()
|
||||||
|
cls.env["account.payment.register"].with_context(
|
||||||
|
active_model="account.move", active_ids=invoices.ids
|
||||||
|
).create(
|
||||||
|
{
|
||||||
|
"amount": sum(invoices.mapped("amount_total")),
|
||||||
|
"payment_method_line_id": cls.inbound_payment_method_line.id,
|
||||||
|
}
|
||||||
|
)._create_payments()
|
||||||
|
cls.paginated_hosts._compute_membership_state()
|
||||||
|
|
||||||
|
associate_vals = [
|
||||||
|
{
|
||||||
|
"name": "Associate Page %d" % i,
|
||||||
|
"associate_member": cls.paginated_hosts[i].id,
|
||||||
|
"website_published": True,
|
||||||
|
"is_company": True,
|
||||||
|
"country_id": cls.country_es.id,
|
||||||
|
}
|
||||||
|
for i in range(20)
|
||||||
|
]
|
||||||
|
cls.paginated_associates = cls.env["res.partner"].create(associate_vals)
|
||||||
|
|
||||||
|
def test_pagination_hosts_plus_associates(self):
|
||||||
|
response = self.url_open("/members/association/%d" % self.membership_1.id)
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
# Page 1 contains all 20 hosts plus their 20 associates.
|
||||||
|
self.assertIn("Host Page 0", response.text)
|
||||||
|
self.assertIn("Host Page 19", response.text)
|
||||||
|
self.assertIn("Associate Page 0", response.text)
|
||||||
|
self.assertIn("Associate Page 19", response.text)
|
||||||
|
# No pagination placeholder from a second page is expected.
|
||||||
|
self.assertNotIn("Host Page 20", response.text)
|
||||||
|
self.assertNotIn("Associate Page 20", response.text)
|
||||||
|
|
||||||
|
|
||||||
|
@tagged("post_install", "-at_install")
|
||||||
|
class TestMembershipLineAssociate(TestMembershipCommon):
|
||||||
|
@classmethod
|
||||||
|
def setUpClass(cls):
|
||||||
|
super().setUpClass()
|
||||||
|
cls.country_us = cls.env.ref("base.us")
|
||||||
|
cls.country_be = cls.env.ref("base.be")
|
||||||
|
|
||||||
|
cls.membership_1.write({"website_published": True})
|
||||||
|
cls.partner_1.write(
|
||||||
|
{
|
||||||
|
"website_published": True,
|
||||||
|
"is_company": True,
|
||||||
|
"country_id": cls.country_us.id,
|
||||||
|
"website_description": "Host description",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
invoice = cls.partner_1.create_membership_invoice(cls.membership_1, 75.0)
|
||||||
|
invoice.action_post()
|
||||||
|
cls.env["account.payment.register"].with_context(
|
||||||
|
active_model="account.move", active_ids=invoice.ids
|
||||||
|
).create(
|
||||||
|
{
|
||||||
|
"amount": invoice.amount_total,
|
||||||
|
"payment_method_line_id": cls.inbound_payment_method_line.id,
|
||||||
|
}
|
||||||
|
)._create_payments()
|
||||||
|
cls.partner_1._compute_membership_state()
|
||||||
|
|
||||||
|
cls.associate = cls.env["res.partner"].create(
|
||||||
|
{
|
||||||
|
"name": "Associate Paid",
|
||||||
|
"associate_member": cls.partner_1.id,
|
||||||
|
"website_published": True,
|
||||||
|
"is_company": True,
|
||||||
|
"country_id": cls.country_be.id,
|
||||||
|
"website_description": "Sustainable farming",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
def _base_domain(self):
|
||||||
|
today = fields.Date.today()
|
||||||
|
return [
|
||||||
|
("partner.website_published", "=", True),
|
||||||
|
("state", "=", "paid"),
|
||||||
|
("date_to", ">=", today),
|
||||||
|
("date_from", "<=", today),
|
||||||
|
("membership_id", "in", self.membership_1.ids),
|
||||||
|
]
|
||||||
|
|
||||||
|
def test_search_excludes_host_when_only_associate_matches_without_context(self):
|
||||||
|
domain = self._base_domain()
|
||||||
|
domain += [
|
||||||
|
"|",
|
||||||
|
("partner.name", "ilike", "Sustainable farming"),
|
||||||
|
("partner.website_description", "ilike", "Sustainable farming"),
|
||||||
|
]
|
||||||
|
lines = self.env["membership.membership_line"].search(domain)
|
||||||
|
self.assertNotIn(self.partner_1.id, lines.partner.ids)
|
||||||
|
|
||||||
|
def test_search_includes_host_when_associate_name_matches(self):
|
||||||
|
domain = self._base_domain()
|
||||||
|
domain += [
|
||||||
|
"|",
|
||||||
|
("partner.name", "ilike", "Sustainable farming"),
|
||||||
|
("partner.website_description", "ilike", "Sustainable farming"),
|
||||||
|
]
|
||||||
|
lines = (
|
||||||
|
self.env["membership.membership_line"]
|
||||||
|
.with_context(include_associate_members=True)
|
||||||
|
.search(domain)
|
||||||
|
)
|
||||||
|
self.assertIn(self.partner_1.id, lines.partner.ids)
|
||||||
|
|
||||||
|
def test_search_includes_host_when_associate_country_matches(self):
|
||||||
|
domain = self._base_domain()
|
||||||
|
domain.append(("partner.country_id", "=", self.country_be.id))
|
||||||
|
lines = (
|
||||||
|
self.env["membership.membership_line"]
|
||||||
|
.with_context(include_associate_members=True)
|
||||||
|
.search(domain)
|
||||||
|
)
|
||||||
|
self.assertIn(self.partner_1.id, lines.partner.ids)
|
||||||
|
|
||||||
|
def test_get_associate_partners_filters_by_country(self):
|
||||||
|
MembershipLine = self.env["membership.membership_line"]
|
||||||
|
match = MembershipLine._get_associate_partners(
|
||||||
|
(self.partner_1.id,), country_id=self.country_be.id
|
||||||
|
)
|
||||||
|
self.assertEqual(match, self.associate)
|
||||||
|
no_match = MembershipLine._get_associate_partners(
|
||||||
|
(self.partner_1.id,), country_id=self.country_us.id
|
||||||
|
)
|
||||||
|
self.assertFalse(no_match)
|
||||||
|
|
||||||
|
def test_get_associate_partners_filters_by_search(self):
|
||||||
|
MembershipLine = self.env["membership.membership_line"]
|
||||||
|
match = MembershipLine._get_associate_partners(
|
||||||
|
(self.partner_1.id,), post_name="Sustainable"
|
||||||
|
)
|
||||||
|
self.assertEqual(match, self.associate)
|
||||||
|
no_match = MembershipLine._get_associate_partners(
|
||||||
|
(self.partner_1.id,), post_name="Nonexistent"
|
||||||
|
)
|
||||||
|
self.assertFalse(no_match)
|
||||||
|
|
||||||
|
def test_published_companies_includes_associate(self):
|
||||||
|
host_line = self.partner_1.member_lines[0]
|
||||||
|
companies = host_line._get_published_companies(limit=2000)
|
||||||
|
self.assertIn(self.partner_1.id, companies)
|
||||||
|
self.assertIn(self.associate.id, companies)
|
||||||
Loading…
Add table
Add a link
Reference in a new issue