Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[REF] membership changes #80

Open
wants to merge 1 commit into
base: 16.0
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions membership_group/__init__.py
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
from . import models
from . import wizard
8 changes: 8 additions & 0 deletions membership_group/__manifest__.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,20 @@
"membership",
],
"data": [
"data/ir_cron_data.xml",
"security/ir.model.access.csv",
"views/membership_group_member_view.xml",
"views/membership_group_view.xml",
"views/membership_vote_snapshot_view.xml",
"views/res_partner_view.xml",
"wizard/membership_vote_history_view.xml",
"menuitems.xml",
],
"assets": {
"web.assets_backend": [
"membership_group/static/src/views/**/*",
],
},
"demo": [
"data/membership_group_demo.xml",
"data/res_partner_demo.xml",
Expand Down
30 changes: 30 additions & 0 deletions membership_group/data/ir_cron_data.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo noupdate="1">

<record id="ir_cron_revoke_memberships" model="ir.cron">
<field name="name">Membership: Revoke expired memberships</field>
<field name="interval_number">1</field>
<field name="interval_type">days</field>
<field name="nextcall"
eval="(DateTime.now().replace(hour=5, minute=0, second=0) + timedelta(days=1)).strftime('%Y-%m-%d %H:%M:%S')" />
<field name="numbercall">-1</field>
<field name="model_id" ref="model_membership_group_member" />
<field name="state">code</field>
<field name="code">model._cron_revoke_membership()</field>
<field name="active" eval="True" />
</record>

<record id="ir_cron_revoke_memberships" model="ir.cron">
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Duplicate of the above

<field name="name">Membership: Revoke expired memberships</field>
<field name="interval_number">1</field>
<field name="interval_type">days</field>
<field name="nextcall"
eval="(DateTime.now().replace(hour=5, minute=0, second=0) + timedelta(days=1)).strftime('%Y-%m-%d %H:%M:%S')" />
<field name="numbercall">-1</field>
<field name="model_id" ref="model_membership_group_member" />
<field name="state">code</field>
<field name="code">model._cron_revoke_membership()</field>
<field name="active" eval="True" />
</record>

</odoo>
23 changes: 22 additions & 1 deletion membership_group/menuitems.xml
Original file line number Diff line number Diff line change
Expand Up @@ -25,9 +25,30 @@

<menuitem
id="membership_group_membership_menu"
name="Membership Group Members"
name="Current Members"
action="action_membership_group_member"
parent="membership_group.membership_group_reporting_menu"
/>

<menuitem
id="membership_group_future_membership_menu"
name="Future Members"
action="action_membership_group_future_member"
parent="membership_group.membership_group_reporting_menu"
/>

<menuitem
id="membership_group_past_membership_menu"
name="Past Members"
action="action_membership_group_past_member"
parent="membership_group.membership_group_reporting_menu"
/>

<menuitem
id="membership_group_voting_membership_menu"
name="Voting Members"
action="action_membership_vote_snapshot"
parent="membership_group.membership_group_reporting_menu"
/>

</odoo>
1 change: 1 addition & 0 deletions membership_group/models/__init__.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
from . import membership_group
from . import membership_group_member
from . import membership_vote_snapshot
from . import res_partner
34 changes: 28 additions & 6 deletions membership_group/models/membership_group.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
from odoo import api, fields, models
from odoo import _, api, fields, models
from odoo.exceptions import ValidationError


class MembershipGroup(models.Model):
Expand Down Expand Up @@ -28,25 +29,46 @@ class MembershipGroup(models.Model):
)
partner_ids_count = fields.Integer("# of Members", compute="_compute_partner_ids")

termination_cycle = fields.Boolean(
help="""
Members from a group with a termination cycle will be
removed from the group on the termination date",
""",
)
next_termination_date = fields.Date(
help="Next termination date for members of this group",
)
voting_group = fields.Boolean(copy=False)

@api.constrains("voting_group")
def _check_voting_group(self):
"""Only allow one voting group"""
if voting_groups := self.search([("voting_group", "=", True)]):
if not voting_groups or len(voting_groups) == 1:
return
raise ValidationError(_("Only one voting group is allowed"))

@api.depends("name", "parent_id.complete_name")
def _compute_complete_name(self):
for group in self:
if group.parent_id:
group.complete_name = "%s / %s" % (
group.parent_id.complete_name,
group.name,
)
group.complete_name = f"{group.parent_id.complete_name} / {group.name}"
else:
group.complete_name = group.name

@api.depends(
"membership_group_member_ids", "membership_group_member_ids.partner_id"
"membership_group_member_ids",
"membership_group_member_ids.partner_id",
)
def _compute_partner_ids(self):
for group in self:
group.partner_ids = group.membership_group_member_ids.mapped("partner_id")
group.partner_ids_count = len(group.partner_ids)

def _has_termination_cycle(self):
self.ensure_one()
return bool(self.termination_cycle and self.next_termination_date)

def action_open_partner_view(self):
action_name = "membership.action_membership_members"
action_vals = self.env["ir.actions.act_window"]._for_xml_id(action_name)
Expand Down
49 changes: 47 additions & 2 deletions membership_group/models/membership_group_member.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
from odoo import fields, models
from odoo import api, fields, models


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

active = fields.Boolean(default=True)
partner_id = fields.Many2one("res.partner", required=True, ondelete="cascade")
group_id = fields.Many2one("membership.group", required=True, ondelete="cascade")
wants_to_collaborate = fields.Boolean()
Expand All @@ -18,11 +19,55 @@ class MembershipGroupMember(models.Model):
("committee", "Committee"),
],
)
date_from = fields.Date(
default=fields.Date.context_today,
help="Start date of the membership",
)
date_to = fields.Date(
compute="_compute_date_to",
store=True,
readonly=False,
precompute=True,
help="Planned end date of the membership",
)
date_end = fields.Date(
help="End date of the membership",
)
can_revoke_membership = fields.Boolean(compute="_compute_can_revoke_membership")

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure what this field is used for atm. Left over for development?

_sql_constraints = [
(
"partner_group_uniq",
"unique(partner_id, group_id)",
"unique(active, partner_id, group_id)",
"Member already exists for this group!",
)
]

@api.depends("group_id")
def _compute_date_to(self):
for record in self:
if (
not record.date_to
and record.group_id
and record.group_id._has_termination_cycle()
):
record.date_to = record.group_id.next_termination_date

@api.depends("date_to")
def _compute_can_revoke_membership(self):
for record in self:
record.can_revoke_membership = record._can_revoke_membership()

def _can_revoke_membership(self):
self.ensure_one()
return self.date_to < fields.Date.today() if self.date_to else True

def action_revoke_membership(self):
if active_records := self.filtered(lambda x: x.active):
active_records.active = False
active_records.date_end = fields.Date.today()
return True

@api.model
def _cron_revoke_membership(self):
self.search([("date_to", "<=", fields.date.today())]).action_revoke_membership()
36 changes: 36 additions & 0 deletions membership_group/models/membership_vote_snapshot.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
from odoo import api, fields, models


class MembershipVoteSnapshot(models.Model):
_name = "membership.vote.snapshot"
_description = "Membership Vote Snapshot"
rec_name = "partner_id"

partner_id = fields.Many2one(
"res.partner",
required=True,
readonly=True,
ondelete="restrict",
)
date = fields.Date(
required=True,
readonly=True,
)

def action_voting_at_date(self):
return {
"res_model": "membership.vote.history",
"views": [[False, "form"]],
"target": "new",
"type": "ir.actions.act_window",
}

@api.model
def action_create_can_vote_snapshot(self):
voting_members = self.env["res.partner"].search(
[("member_can_vote", "=", True)]
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When I have voting members in the future or in the past it doesn't work. Is it maybe easier to have a domain which uses date_from and date_end / date_to on membership.group.member? And then in membership.vote.history.open_at_date set the selected date as context variable.
Or maybe make member_can_vote dependent on context (api.depends_context)

)
today = fields.Date.today()
values = [{"partner_id": member.id, "date": today} for member in voting_members]
self.search([("date", "=", today)]).sudo().unlink()
self.sudo().create(values)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you remove the sudo here and instead use ir.model.access. I think we can give full rights to the user and add delete="false" create="false" on the tree for ux

16 changes: 15 additions & 1 deletion membership_group/models/res_partner.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,14 @@ class ResPartner(models.Model):
store=True,
)
membership_group_ids_count = fields.Integer(
string="# of Groups", compute="_compute_membership_group_ids", store=True
string="# of Groups",
compute="_compute_membership_group_ids",
store=True,
)
member_can_vote = fields.Boolean(
compute="_compute_member_can_vote",
store=True,
string="Can Vote",
)

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

@api.depends("membership_group_ids", "membership_group_ids.voting_group")
def _compute_member_can_vote(self):
for partner in self:
partner.member_can_vote = bool(
partner.membership_group_ids.filtered("voting_group")
)

def action_open_membership_group_view(self):
action_name = "membership_group.membership_group_action"
action_vals = self.env["ir.actions.act_window"]._for_xml_id(action_name)
Expand Down
4 changes: 3 additions & 1 deletion membership_group/security/ir.model.access.csv
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,6 @@ access_membership_group_portal,access_membership_group_portal,model_membership_g
access_membership_group,access_membership_group,model_membership_group,base.group_user,1,1,1,1
access_membership_group_member_public,access_membership_group_member_public,model_membership_group_member,base.group_public,1,0,0,0
access_membership_group_member_portal,access_membership_group_member_portal,model_membership_group_member,base.group_portal,1,0,0,0
access_membership_group_member,access_membership_group_member,model_membership_group_member,base.group_user,1,1,1,1
access_membership_group_member,access_membership_group_member,model_membership_group_member,base.group_user,1,1,1,0
access_membership_vote_snapshot,access_membership_vote_snapshot,model_membership_vote_snapshot,base.group_user,1,0,0,0
access_membership_vote_history,access_membership_vote_history,model_membership_vote_history,base.group_user,1,1,1,0
15 changes: 15 additions & 0 deletions membership_group/static/src/views/list/membership_report_list.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<?xml version="1.0" encoding="utf-8"?>
<templates id="template" xml:space="preserve">

<t t-name="MembershipReport.Buttons" t-inherit="web.ListView.Buttons" t-inherit-mode="primary" owl="1">
<xpath expr="(//div/*)[last()]" position="after">
<button type="button" class="btn btn-primary me-2" t-on-click="onClickMembershipVoteAtDate">
Voting at Date
</button>
<button type="button" class="btn btn-secondary me-2" t-on-click="onClickMembershipVoteRefresh">
Refresh
</button>
</xpath>
</t>

</templates>
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
/** @odoo-module */

import {ListController} from "@web/views/list/list_controller";

export class MembershipReportListController extends ListController {
async onClickMembershipVoteAtDate() {
const context = {
active_model: this.props.resModel,
};
this.actionService.doAction({
res_model: "membership.vote.history",
views: [[false, "form"]],
target: "new",
type: "ir.actions.act_window",
context,
});
}

async onClickMembershipVoteRefresh() {
await this.orm.call("membership.vote.snapshot", "action_create_can_vote_snapshot", [], {});
this.actionService.doAction({
type: "ir.actions.client",
tag: "reload",
});
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
/** @odoo-module */

import {listView} from "@web/views/list/list_view";
import {MembershipReportListController} from "./membership_report_list_controller";
import {registry} from "@web/core/registry";

export const MembershipReportListView = {
...listView,
Controller: MembershipReportListController,
buttonTemplate: "MembershipReport.Buttons",
};

registry.category("views").add("membership_report_list", MembershipReportListView);
37 changes: 37 additions & 0 deletions membership_group/tests/test_membership_group.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import freezegun

from odoo.tests import common


Expand Down Expand Up @@ -84,3 +86,38 @@ def test_04_action_open_membership_group_view(self):
],
)
self.assertEqual(res["res_id"], self.group_1.id)

def test_05_membership_group_with_revoke_date(self):
group_1_with_termination = self.env["membership.group"].create(
{
"name": "Test Group 1 with termination",
"termination_cycle": True,
"next_termination_date": "2025-06-01",
}
)
member_group_termination = self.env["membership.group.member"].create(
{
"partner_id": self.partner_1.id,
"group_id": group_1_with_termination.id,
}
)

self.assertEqual(
member_group_termination.date_to,
group_1_with_termination.next_termination_date,
)
self.assertTrue(member_group_termination.active)

with freezegun.freeze_time("2025-05-01"):
self.env["membership.group.member"]._cron_revoke_membership()

self.assertTrue(member_group_termination.active)

with freezegun.freeze_time("2025-06-01"):
self.env["membership.group.member"]._cron_revoke_membership()

self.assertFalse(member_group_termination.active)
self.assertEqual(
str(member_group_termination.date_end),
"2025-06-01",
)
Loading
Loading