Skip to content

Commit 937a838

Browse files
committed
[WIP] membership changes
1 parent 5c09bc6 commit 937a838

9 files changed

+207
-39
lines changed
+17
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
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=05, 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+
</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_group_voting_member"
51+
parent="membership_group.membership_group_reporting_menu"
52+
/>
53+
3354
</odoo>

membership_group/models/membership_group.py

+17-5
Original file line numberDiff line numberDiff line change
@@ -28,25 +28,37 @@ class MembershipGroup(models.Model):
2828
)
2929
partner_ids_count = fields.Integer("# of Members", compute="_compute_partner_ids")
3030

31+
termination_cycle = fields.Boolean(
32+
help="""
33+
Members from a group with a termination cycle will be
34+
removed from the group on the termination date",
35+
""",
36+
)
37+
next_termination_date = fields.Date(
38+
help="Next termination date for members of this group",
39+
)
40+
3141
@api.depends("name", "parent_id.complete_name")
3242
def _compute_complete_name(self):
3343
for group in self:
3444
if group.parent_id:
35-
group.complete_name = "%s / %s" % (
36-
group.parent_id.complete_name,
37-
group.name,
38-
)
45+
group.complete_name = f"{group.parent_id.complete_name} / {group.name}"
3946
else:
4047
group.complete_name = group.name
4148

4249
@api.depends(
43-
"membership_group_member_ids", "membership_group_member_ids.partner_id"
50+
"membership_group_member_ids",
51+
"membership_group_member_ids.partner_id",
4452
)
4553
def _compute_partner_ids(self):
4654
for group in self:
4755
group.partner_ids = group.membership_group_member_ids.mapped("partner_id")
4856
group.partner_ids_count = len(group.partner_ids)
4957

58+
def _has_termination_cycle(self):
59+
self.ensure_one()
60+
return bool(self.termination_cycle and self.next_termination_date)
61+
5062
def action_open_partner_view(self):
5163
action_name = "membership.action_membership_members"
5264
action_vals = self.env["ir.actions.act_window"]._for_xml_id(action_name)

membership_group/models/membership_group_member.py

+48-2
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,15 @@
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()
12+
can_vote = fields.Boolean()
1113
type = fields.Selection(
1214
[
1315
("follower", "Follower"),
@@ -18,11 +20,55 @@ class MembershipGroupMember(models.Model):
1820
("committee", "Committee"),
1921
],
2022
)
23+
date_from = fields.Date(
24+
default=fields.Date.context_today,
25+
help="Start date of the membership",
26+
)
27+
date_to = fields.Date(
28+
compute="_compute_date_to",
29+
store=True,
30+
readonly=False,
31+
precompute=True,
32+
help="Planned end date of the membership",
33+
)
34+
date_end = fields.Date(
35+
help="End date of the membership",
36+
)
37+
can_revoke_membership = fields.Boolean(compute="_compute_can_revoke_membership")
2138

2239
_sql_constraints = [
2340
(
2441
"partner_group_uniq",
25-
"unique(partner_id, group_id)",
42+
"unique(active, partner_id, group_id)",
2643
"Member already exists for this group!",
2744
)
2845
]
46+
47+
@api.depends("group_id")
48+
def _compute_date_to(self):
49+
for record in self:
50+
if (
51+
not record.date_to
52+
and record.group_id
53+
and record.group_id._has_termination_cycle()
54+
):
55+
record.date_to = record.group_id.next_termination_date
56+
57+
@api.depends("date_to")
58+
def _compute_can_revoke_membership(self):
59+
for record in self:
60+
record.can_revoke_membership = record._can_revoke_membership()
61+
62+
def _can_revoke_membership(self):
63+
self.ensure_one()
64+
return self.date_to < fields.Date.today() if self.date_to else True
65+
66+
def action_revoke_membership(self):
67+
if active_records := self.filtered(lambda x: x.active):
68+
active_records.active = False
69+
active_records.date_end = fields.Date.today()
70+
return True
71+
72+
@api.model
73+
def _cron_revoke_membership(self):
74+
self.search([("date_to", "<=", fields.date.today())]).action_revoke_membership()

membership_group/security/ir.model.access.csv

+1-1
Original file line numberDiff line numberDiff line change
@@ -4,4 +4,4 @@ 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

membership_group/tests/test_membership_group.py

+36
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import freezegun
12
from odoo.tests import common
23

34

@@ -84,3 +85,38 @@ def test_04_action_open_membership_group_view(self):
8485
],
8586
)
8687
self.assertEqual(res["res_id"], self.group_1.id)
88+
89+
def test_05_membership_group_with_revoke_date(self):
90+
group_1_with_termination = self.env["membership.group"].create(
91+
{
92+
"name": "Test Group 1 with termination",
93+
"termination_cycle": True,
94+
"next_termination_date": "2025-06-01",
95+
}
96+
)
97+
member_group_termination = self.env["membership.group.member"].create(
98+
{
99+
"partner_id": self.partner_1.id,
100+
"group_id": group_1_with_termination.id,
101+
}
102+
)
103+
104+
self.assertEqual(
105+
member_group_termination.date_to,
106+
group_1_with_termination.next_termination_date,
107+
)
108+
self.assertTrue(member_group_termination.active)
109+
110+
with freezegun.freeze_time("2025-05-01"):
111+
self.env["membership.group.member"]._cron_revoke_membership()
112+
113+
self.assertTrue(member_group_termination.active)
114+
115+
with freezegun.freeze_time("2025-06-01"):
116+
self.env["membership.group.member"]._cron_revoke_membership()
117+
118+
self.assertFalse(member_group_termination.active)
119+
self.assertEqual(
120+
str(member_group_termination.date_end),
121+
"2025-06-01",
122+
)
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,22 @@
1-
<?xml version="1.0" encoding="utf-8" ?>
1+
<?xml version="1.0" encoding="utf-8"?>
22
<odoo>
33

44
<record id="membership_group_member_view_tree" model="ir.ui.view">
55
<field name="model">membership.group.member</field>
66
<field name="arch" type="xml">
77
<tree editable="bottom">
8+
<field name="active" invisible="1" />
89
<field name="partner_id" />
910
<field name="group_id" />
10-
<field name="type" />
11-
<field name="wants_to_collaborate" />
11+
<field name="date_from" optional="show" />
12+
<field name="date_to" optional="show" />
13+
<field name="date_end" optional="hide" />
14+
<field name="type" optional="hide" />
15+
<field name="wants_to_collaborate" optional="hide" />
16+
<field name="can_vote" widget="boolean_toggle"
17+
readonly="context.get('membership_remove_options', False)" optional="hide" />
18+
<button type="object" name="action_revoke_membership" string="Revoke"
19+
icon="fa-trash" attrs="{'invisible': [('active','=', False)]}" />
1220
</tree>
1321
</field>
1422
</record>
@@ -17,32 +25,67 @@
1725
<field name="model">membership.group.member</field>
1826
<field name="arch" type="xml">
1927
<pivot string="Membership Group Members" disable_linking="True">
20-
<field name="partner_id" type="row"/>
21-
<field name="group_id" type="row"/>
22-
<field name="type" type="col"/>
28+
<field name="partner_id" type="row" />
29+
<field name="group_id" type="row" />
30+
<field name="type" type="col" />
2331
</pivot>
2432
</field>
2533
</record>
2634

27-
<record id="membership_group_member_view_search" model="ir.ui.view">
35+
<record id="membership_group_member_view_search" model="ir.ui.view">
2836
<field name="model">membership.group.member</field>
2937
<field name="arch" type="xml">
3038
<search>
3139
<field name="partner_id" />
3240
<field name="group_id" />
3341
<field name="type" />
42+
<filter name="show_voters" string="Can Vote" domain="[('can_vote', '=', True)]" />
43+
<filter name="show_collaborators" string="Want to Collaborate"
44+
domain="[('wants_to_collaborate', '=', True)]" />
45+
<separator />
3446
<group expand="0" name="claims" string="Group By">
35-
<filter string="Partner" name="partner_id" domain="[]" help="Partner" context="{'group_by':'partner_id'}" />
36-
<filter string="Group" name="group_id" domain="[]" help="Group" context="{'group_by':'group_id'}" />
37-
<filter string="Type" name="type" domain="[]" help="Group" context="{'group_by':'type'}" />
47+
<filter string="Partner" name="groupby_partner_id" domain="[]" help="Partner"
48+
context="{'group_by':'partner_id'}" />
49+
<filter string="Group" name="groupby_group_id" domain="[]" help="Group"
50+
context="{'group_by':'group_id'}" />
51+
<filter string="Type" name="groupby_type" domain="[]" help="Group"
52+
context="{'group_by':'type'}" />
3853
</group>
3954
</search>
4055
</field>
4156
</record>
4257

4358
<record id="action_membership_group_member" model="ir.actions.act_window">
44-
<field name="name">Membership Group Members</field>
59+
<field name="name">Current Members</field>
60+
<field name="res_model">membership.group.member</field>
61+
<field name="view_mode">tree,pivot</field>
62+
</record>
63+
64+
<record id="action_membership_group_future_member" model="ir.actions.act_window">
65+
<field name="name">Future Members</field>
66+
<field name="res_model">membership.group.member</field>
67+
<field name="view_mode">tree,pivot</field>
68+
<field name="context">{'active_test': False, 'create': False, 'edit': False}</field>
69+
<field name="domain">[('active', '=', False), ('date_from', '&gt;',
70+
context_today().strftime('%Y-%m-%d'))]</field>
71+
</record>
72+
73+
<record id="action_membership_group_past_member" model="ir.actions.act_window">
74+
<field name="name">Past Members</field>
4575
<field name="res_model">membership.group.member</field>
4676
<field name="view_mode">tree,pivot</field>
77+
<field name="context">{'active_test': False, 'create': False, 'edit': False,
78+
'membership_remove_options': True}</field>
79+
<field name="domain">[('active', '=', False), ('date_end', '!=', False)]</field>
4780
</record>
81+
82+
<record id="action_membership_group_voting_member" model="ir.actions.act_window">
83+
<field name="name">Voting Members</field>
84+
<field name="res_model">membership.group.member</field>
85+
<field name="view_mode">tree,pivot</field>
86+
<field name="context">{'create': False, 'edit': False, 'membership_remove_options': True,
87+
'search_default_groupby_group_id': 1}</field>
88+
<field name="domain">[('can_vote', '=', True)]</field>
89+
</record>
90+
4891
</odoo>

membership_group/views/membership_group_view.xml

+10-11
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
<?xml version="1.0" encoding="utf-8" ?>
1+
<?xml version="1.0" encoding="utf-8"?>
22
<odoo>
33

44
<record id="membership_group_view_tree" model="ir.ui.view">
@@ -48,20 +48,19 @@
4848
</group>
4949
</group>
5050
<group name="form_body" />
51-
5251
<notebook>
5352
<page string="Membership" name="membership">
54-
<field name="membership_group_member_ids">
55-
<tree editable="top">
56-
<field
57-
name="partner_id"
58-
domain="[('membership_state', 'in', ['invoiced', 'paid', 'free'])]"
59-
/>
60-
</tree>
61-
</field>
53+
<field name="membership_group_member_ids"/>
6254
</page>
6355
<page string="Subgroups">
64-
<field name="child_ids"/>
56+
<field name="child_ids" />
57+
</page>
58+
<page string="Configuration">
59+
<group>
60+
<field name="termination_cycle" widget="boolean_toggle" />
61+
<field name="next_termination_date"
62+
attrs="{'required': [('termination_cycle','=', True)], 'invisible': [('termination_cycle', '=', False)]}" />
63+
</group>
6564
</page>
6665
</notebook>
6766
</sheet>

membership_group/views/res_partner_view.xml

+2-8
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
<?xml version="1.0" encoding="utf-8" ?>
1+
<?xml version="1.0" encoding="utf-8"?>
22
<odoo>
33

44
<record id="view_partner_form" model="ir.ui.view">
@@ -22,13 +22,7 @@
2222
</xpath>
2323
<xpath expr="//field[@name='member_lines']" position="after">
2424
<group>
25-
<field name="membership_group_member_ids" string="Groups" colspan="4">
26-
<tree editable="bottom">
27-
<field name="group_id"/>
28-
<field name="type" />
29-
<field name="wants_to_collaborate" />
30-
</tree>
31-
</field>
25+
<field name="membership_group_member_ids" string="Groups" colspan="2" />
3226
</group>
3327
</xpath>
3428
</field>

0 commit comments

Comments
 (0)