Skip to content

Commit 6c40c93

Browse files
committed
[WIP] membership changes
1 parent 5c09bc6 commit 6c40c93

21 files changed

+418
-41
lines changed

membership_group/__init__.py

+1
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
11
from . import models
2+
from . import wizard

membership_group/__manifest__.py

+8
Original file line numberDiff line numberDiff line change
@@ -12,12 +12,20 @@
1212
"membership",
1313
],
1414
"data": [
15+
"data/ir_cron_data.xml",
1516
"security/ir.model.access.csv",
1617
"views/membership_group_member_view.xml",
1718
"views/membership_group_view.xml",
19+
"views/membership_vote_snapshot_view.xml",
1820
"views/res_partner_view.xml",
21+
"wizard/membership_vote_history_view.xml",
1922
"menuitems.xml",
2023
],
24+
"assets": {
25+
"web.assets_backend": [
26+
"membership_group/static/src/views/**/*",
27+
],
28+
},
2129
"demo": [
2230
"data/membership_group_demo.xml",
2331
"data/res_partner_demo.xml",
+30
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
<?xml version="1.0" encoding="utf-8"?>
2+
<odoo noupdate="1">
3+
4+
<record id="ir_cron_revoke_memberships" model="ir.cron">
5+
<field name="name">Membership: Revoke expired memberships</field>
6+
<field name="interval_number">1</field>
7+
<field name="interval_type">days</field>
8+
<field name="nextcall"
9+
eval="(DateTime.now().replace(hour=5, minute=0, second=0) + timedelta(days=1)).strftime('%Y-%m-%d %H:%M:%S')" />
10+
<field name="numbercall">-1</field>
11+
<field name="model_id" ref="model_membership_group_member" />
12+
<field name="state">code</field>
13+
<field name="code">model._cron_revoke_membership()</field>
14+
<field name="active" eval="True" />
15+
</record>
16+
17+
<record id="ir_cron_revoke_memberships" model="ir.cron">
18+
<field name="name">Membership: Revoke expired memberships</field>
19+
<field name="interval_number">1</field>
20+
<field name="interval_type">days</field>
21+
<field name="nextcall"
22+
eval="(DateTime.now().replace(hour=5, minute=0, second=0) + timedelta(days=1)).strftime('%Y-%m-%d %H:%M:%S')" />
23+
<field name="numbercall">-1</field>
24+
<field name="model_id" ref="model_membership_group_member" />
25+
<field name="state">code</field>
26+
<field name="code">model._cron_revoke_membership()</field>
27+
<field name="active" eval="True" />
28+
</record>
29+
30+
</odoo>

membership_group/menuitems.xml

+22-1
Original file line numberDiff line numberDiff line change
@@ -25,9 +25,30 @@
2525

2626
<menuitem
2727
id="membership_group_membership_menu"
28-
name="Membership Group Members"
28+
name="Current Members"
2929
action="action_membership_group_member"
3030
parent="membership_group.membership_group_reporting_menu"
3131
/>
3232

33+
<menuitem
34+
id="membership_group_future_membership_menu"
35+
name="Future Members"
36+
action="action_membership_group_future_member"
37+
parent="membership_group.membership_group_reporting_menu"
38+
/>
39+
40+
<menuitem
41+
id="membership_group_past_membership_menu"
42+
name="Past Members"
43+
action="action_membership_group_past_member"
44+
parent="membership_group.membership_group_reporting_menu"
45+
/>
46+
47+
<menuitem
48+
id="membership_group_voting_membership_menu"
49+
name="Voting Members"
50+
action="action_membership_vote_snapshot"
51+
parent="membership_group.membership_group_reporting_menu"
52+
/>
53+
3354
</odoo>

membership_group/models/__init__.py

+1
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
from . import membership_group
22
from . import membership_group_member
3+
from . import membership_vote_snapshot
34
from . import res_partner

membership_group/models/membership_group.py

+28-6
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
from odoo import api, fields, models
1+
from odoo import _, api, fields, models
2+
from odoo.exceptions import ValidationError
23

34

45
class MembershipGroup(models.Model):
@@ -28,25 +29,46 @@ class MembershipGroup(models.Model):
2829
)
2930
partner_ids_count = fields.Integer("# of Members", compute="_compute_partner_ids")
3031

32+
termination_cycle = fields.Boolean(
33+
help="""
34+
Members from a group with a termination cycle will be
35+
removed from the group on the termination date",
36+
""",
37+
)
38+
next_termination_date = fields.Date(
39+
help="Next termination date for members of this group",
40+
)
41+
voting_group = fields.Boolean(copy=False)
42+
43+
@api.constrains("voting_group")
44+
def _check_voting_group(self):
45+
"""Only allow one voting group"""
46+
if voting_groups := self.search([("voting_group", "=", True)]):
47+
if not voting_groups or len(voting_groups) == 1:
48+
return
49+
raise ValidationError(_("Only one voting group is allowed"))
50+
3151
@api.depends("name", "parent_id.complete_name")
3252
def _compute_complete_name(self):
3353
for group in self:
3454
if group.parent_id:
35-
group.complete_name = "%s / %s" % (
36-
group.parent_id.complete_name,
37-
group.name,
38-
)
55+
group.complete_name = f"{group.parent_id.complete_name} / {group.name}"
3956
else:
4057
group.complete_name = group.name
4158

4259
@api.depends(
43-
"membership_group_member_ids", "membership_group_member_ids.partner_id"
60+
"membership_group_member_ids",
61+
"membership_group_member_ids.partner_id",
4462
)
4563
def _compute_partner_ids(self):
4664
for group in self:
4765
group.partner_ids = group.membership_group_member_ids.mapped("partner_id")
4866
group.partner_ids_count = len(group.partner_ids)
4967

68+
def _has_termination_cycle(self):
69+
self.ensure_one()
70+
return bool(self.termination_cycle and self.next_termination_date)
71+
5072
def action_open_partner_view(self):
5173
action_name = "membership.action_membership_members"
5274
action_vals = self.env["ir.actions.act_window"]._for_xml_id(action_name)

membership_group/models/membership_group_member.py

+47-2
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
1-
from odoo import fields, models
1+
from odoo import api, fields, models
22

33

44
class MembershipGroupMember(models.Model):
55
_name = "membership.group.member"
66
_description = "Membership Group Member"
77

8+
active = fields.Boolean(default=True)
89
partner_id = fields.Many2one("res.partner", required=True, ondelete="cascade")
910
group_id = fields.Many2one("membership.group", required=True, ondelete="cascade")
1011
wants_to_collaborate = fields.Boolean()
@@ -18,11 +19,55 @@ class MembershipGroupMember(models.Model):
1819
("committee", "Committee"),
1920
],
2021
)
22+
date_from = fields.Date(
23+
default=fields.Date.context_today,
24+
help="Start date of the membership",
25+
)
26+
date_to = fields.Date(
27+
compute="_compute_date_to",
28+
store=True,
29+
readonly=False,
30+
precompute=True,
31+
help="Planned end date of the membership",
32+
)
33+
date_end = fields.Date(
34+
help="End date of the membership",
35+
)
36+
can_revoke_membership = fields.Boolean(compute="_compute_can_revoke_membership")
2137

2238
_sql_constraints = [
2339
(
2440
"partner_group_uniq",
25-
"unique(partner_id, group_id)",
41+
"unique(active, partner_id, group_id)",
2642
"Member already exists for this group!",
2743
)
2844
]
45+
46+
@api.depends("group_id")
47+
def _compute_date_to(self):
48+
for record in self:
49+
if (
50+
not record.date_to
51+
and record.group_id
52+
and record.group_id._has_termination_cycle()
53+
):
54+
record.date_to = record.group_id.next_termination_date
55+
56+
@api.depends("date_to")
57+
def _compute_can_revoke_membership(self):
58+
for record in self:
59+
record.can_revoke_membership = record._can_revoke_membership()
60+
61+
def _can_revoke_membership(self):
62+
self.ensure_one()
63+
return self.date_to < fields.Date.today() if self.date_to else True
64+
65+
def action_revoke_membership(self):
66+
if active_records := self.filtered(lambda x: x.active):
67+
active_records.active = False
68+
active_records.date_end = fields.Date.today()
69+
return True
70+
71+
@api.model
72+
def _cron_revoke_membership(self):
73+
self.search([("date_to", "<=", fields.date.today())]).action_revoke_membership()
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
from odoo import api, fields, models
2+
3+
4+
class MembershipVoteSnapshot(models.Model):
5+
_name = "membership.vote.snapshot"
6+
_description = "Membership Vote Snapshot"
7+
rec_name = "partner_id"
8+
9+
partner_id = fields.Many2one(
10+
"res.partner",
11+
required=True,
12+
readonly=True,
13+
ondelete="restrict",
14+
)
15+
date = fields.Date(
16+
required=True,
17+
readonly=True,
18+
)
19+
20+
def action_voting_at_date(self):
21+
return {
22+
"res_model": "membership.vote.history",
23+
"views": [[False, "form"]],
24+
"target": "new",
25+
"type": "ir.actions.act_window",
26+
}
27+
28+
@api.model
29+
def action_create_can_vote_snapshot(self):
30+
voting_members = self.env["res.partner"].search(
31+
[("member_can_vote", "=", True)]
32+
)
33+
today = fields.Date.today()
34+
values = [{"partner_id": member.id, "date": today} for member in voting_members]
35+
self.search([("date", "=", today)]).sudo().unlink()
36+
self.sudo().create(values)

membership_group/models/res_partner.py

+15-1
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,14 @@ class ResPartner(models.Model):
1414
store=True,
1515
)
1616
membership_group_ids_count = fields.Integer(
17-
string="# of Groups", compute="_compute_membership_group_ids", store=True
17+
string="# of Groups",
18+
compute="_compute_membership_group_ids",
19+
store=True,
20+
)
21+
member_can_vote = fields.Boolean(
22+
compute="_compute_member_can_vote",
23+
store=True,
24+
string="Can Vote",
1825
)
1926

2027
@api.depends("membership_group_member_ids", "membership_group_member_ids.group_id")
@@ -25,6 +32,13 @@ def _compute_membership_group_ids(self):
2532
)
2633
partner.membership_group_ids_count = len(partner.membership_group_ids)
2734

35+
@api.depends("membership_group_ids", "membership_group_ids.voting_group")
36+
def _compute_member_can_vote(self):
37+
for partner in self:
38+
partner.member_can_vote = bool(
39+
partner.membership_group_ids.filtered("voting_group")
40+
)
41+
2842
def action_open_membership_group_view(self):
2943
action_name = "membership_group.membership_group_action"
3044
action_vals = self.env["ir.actions.act_window"]._for_xml_id(action_name)

membership_group/security/ir.model.access.csv

+3-1
Original file line numberDiff line numberDiff line change
@@ -4,4 +4,6 @@ access_membership_group_portal,access_membership_group_portal,model_membership_g
44
access_membership_group,access_membership_group,model_membership_group,base.group_user,1,1,1,1
55
access_membership_group_member_public,access_membership_group_member_public,model_membership_group_member,base.group_public,1,0,0,0
66
access_membership_group_member_portal,access_membership_group_member_portal,model_membership_group_member,base.group_portal,1,0,0,0
7-
access_membership_group_member,access_membership_group_member,model_membership_group_member,base.group_user,1,1,1,1
7+
access_membership_group_member,access_membership_group_member,model_membership_group_member,base.group_user,1,1,1,0
8+
access_membership_vote_snapshot,access_membership_vote_snapshot,model_membership_vote_snapshot,base.group_user,1,0,0,0
9+
access_membership_vote_history,access_membership_vote_history,model_membership_vote_history,base.group_user,1,1,1,0
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
<?xml version="1.0" encoding="utf-8"?>
2+
<templates id="template" xml:space="preserve">
3+
4+
<t t-name="MembershipReport.Buttons" t-inherit="web.ListView.Buttons" t-inherit-mode="primary" owl="1">
5+
<xpath expr="(//div/*)[last()]" position="after">
6+
<button type="button" class="btn btn-primary me-2" t-on-click="onClickMembershipVoteAtDate">
7+
Voting at Date
8+
</button>
9+
<button type="button" class="btn btn-secondary me-2" t-on-click="onClickMembershipVoteRefresh">
10+
Refresh
11+
</button>
12+
</xpath>
13+
</t>
14+
15+
</templates>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
/** @odoo-module */
2+
3+
import {ListController} from "@web/views/list/list_controller";
4+
5+
export class MembershipReportListController extends ListController {
6+
async onClickMembershipVoteAtDate() {
7+
const context = {
8+
active_model: this.props.resModel,
9+
};
10+
this.actionService.doAction({
11+
res_model: "membership.vote.history",
12+
views: [[false, "form"]],
13+
target: "new",
14+
type: "ir.actions.act_window",
15+
context,
16+
});
17+
}
18+
19+
async onClickMembershipVoteRefresh() {
20+
await this.orm.call("membership.vote.snapshot", "action_create_can_vote_snapshot", [], {});
21+
this.actionService.doAction({
22+
type: "ir.actions.client",
23+
tag: "reload",
24+
});
25+
}
26+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
/** @odoo-module */
2+
3+
import {listView} from "@web/views/list/list_view";
4+
import {MembershipReportListController} from "./membership_report_list_controller";
5+
import {registry} from "@web/core/registry";
6+
7+
export const MembershipReportListView = {
8+
...listView,
9+
Controller: MembershipReportListController,
10+
buttonTemplate: "MembershipReport.Buttons",
11+
};
12+
13+
registry.category("views").add("membership_report_list", MembershipReportListView);

membership_group/tests/test_membership_group.py

+37
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import freezegun
2+
13
from odoo.tests import common
24

35

@@ -84,3 +86,38 @@ def test_04_action_open_membership_group_view(self):
8486
],
8587
)
8688
self.assertEqual(res["res_id"], self.group_1.id)
89+
90+
def test_05_membership_group_with_revoke_date(self):
91+
group_1_with_termination = self.env["membership.group"].create(
92+
{
93+
"name": "Test Group 1 with termination",
94+
"termination_cycle": True,
95+
"next_termination_date": "2025-06-01",
96+
}
97+
)
98+
member_group_termination = self.env["membership.group.member"].create(
99+
{
100+
"partner_id": self.partner_1.id,
101+
"group_id": group_1_with_termination.id,
102+
}
103+
)
104+
105+
self.assertEqual(
106+
member_group_termination.date_to,
107+
group_1_with_termination.next_termination_date,
108+
)
109+
self.assertTrue(member_group_termination.active)
110+
111+
with freezegun.freeze_time("2025-05-01"):
112+
self.env["membership.group.member"]._cron_revoke_membership()
113+
114+
self.assertTrue(member_group_termination.active)
115+
116+
with freezegun.freeze_time("2025-06-01"):
117+
self.env["membership.group.member"]._cron_revoke_membership()
118+
119+
self.assertFalse(member_group_termination.active)
120+
self.assertEqual(
121+
str(member_group_termination.date_end),
122+
"2025-06-01",
123+
)

0 commit comments

Comments
 (0)