feat(community_membership): tier model, res.partner extension, settings (Session 1-A)
Adds the membership foundation: community.membership.tier (admin-configurable
tiers, each auto-creating a linked invoicing product), res.partner membership
fields (member ID, tier, state, family grouping, volunteer tracking), and a
res.config.settings page (org name, member-ID format, renewal offsets, portal
toggle) - all backed by ir.config_parameter so nothing is hardcoded.
Member IDs are generated on activation via a configurable format string
({year}/{seq}) resolved against an ir.sequence. Adds the Membership Manager
group/menu and views (tier list/form, partner form tab, list columns, search
filters, settings page).
Along the way, hit two real Odoo 19 API changes vs. older tutorials/docs:
res.groups.category_id was replaced by privilege_id -> res.groups.privilege,
and module settings pages now use the <app>/<block>/<setting> pattern
inherited from base.res_config_settings_view_form rather than raw divs.
Verified by upgrading the module against a live Odoo 19 + Postgres 16
container: installs clean, all 4 tests pass (tier->product creation, member
ID generation on activation, one-time/lifetime tiers never expire,
configurable ID format), and the module still installs cleanly for its
dependents (community_benefits, tncsc_deployment).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
parent
a848373429
commit
ae324c30f9
@ -0,0 +1 @@
|
|||||||
|
from . import models
|
||||||
@ -29,7 +29,15 @@ configuration record managed by the deployment layer.
|
|||||||
'portal',
|
'portal',
|
||||||
'website',
|
'website',
|
||||||
],
|
],
|
||||||
'data': [],
|
'data': [
|
||||||
|
'security/membership_security.xml',
|
||||||
|
'security/ir.model.access.csv',
|
||||||
|
'data/ir_sequence.xml',
|
||||||
|
'views/membership_tier_views.xml',
|
||||||
|
'views/res_partner_views.xml',
|
||||||
|
'views/res_config_settings_views.xml',
|
||||||
|
'views/membership_menus.xml',
|
||||||
|
],
|
||||||
'demo': [],
|
'demo': [],
|
||||||
'images': ['static/description/banner.png'],
|
'images': ['static/description/banner.png'],
|
||||||
'application': True,
|
'application': True,
|
||||||
|
|||||||
10
addons/community_membership/data/ir_sequence.xml
Normal file
10
addons/community_membership/data/ir_sequence.xml
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<odoo noupdate="1">
|
||||||
|
<record id="seq_membership_member_id" model="ir.sequence">
|
||||||
|
<field name="name">Membership Member ID</field>
|
||||||
|
<field name="code">community.membership.member_id</field>
|
||||||
|
<field name="padding">4</field>
|
||||||
|
<field name="number_increment">1</field>
|
||||||
|
<field name="implementation">no_gap</field>
|
||||||
|
</record>
|
||||||
|
</odoo>
|
||||||
3
addons/community_membership/models/__init__.py
Normal file
3
addons/community_membership/models/__init__.py
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
from . import membership_tier
|
||||||
|
from . import res_partner
|
||||||
|
from . import res_config_settings
|
||||||
80
addons/community_membership/models/membership_tier.py
Normal file
80
addons/community_membership/models/membership_tier.py
Normal file
@ -0,0 +1,80 @@
|
|||||||
|
from odoo import api, fields, models
|
||||||
|
|
||||||
|
PERIOD_DURATION_DAYS = {
|
||||||
|
'annual': 365,
|
||||||
|
'monthly': 30,
|
||||||
|
'one_time': None, # never expires
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class CommunityMembershipTier(models.Model):
|
||||||
|
_name = 'community.membership.tier'
|
||||||
|
_description = 'Membership Tier'
|
||||||
|
_order = 'sequence, id'
|
||||||
|
|
||||||
|
name = fields.Char(required=True, translate=True)
|
||||||
|
code = fields.Char(required=True, help="Short internal code, e.g. 'IND', 'FAM'.")
|
||||||
|
price = fields.Monetary(currency_field='currency_id', required=True)
|
||||||
|
currency_id = fields.Many2one(
|
||||||
|
'res.currency', required=True,
|
||||||
|
default=lambda self: self.env.company.currency_id,
|
||||||
|
)
|
||||||
|
period = fields.Selection(
|
||||||
|
[
|
||||||
|
('annual', 'Annual'),
|
||||||
|
('one_time', 'One-time (lifetime)'),
|
||||||
|
('monthly', 'Monthly'),
|
||||||
|
],
|
||||||
|
default='annual', required=True,
|
||||||
|
)
|
||||||
|
member_state_logic = fields.Text(
|
||||||
|
string='State Notes',
|
||||||
|
help="Free-text notes for admins on any special membership-state handling "
|
||||||
|
"this tier needs (e.g. lifetime tiers never move to 'expired').",
|
||||||
|
)
|
||||||
|
sequence = fields.Integer(default=10)
|
||||||
|
product_id = fields.Many2one(
|
||||||
|
'product.product', string='Membership Product', readonly=True, copy=False,
|
||||||
|
help="Auto-created service product used to invoice this tier's dues.",
|
||||||
|
)
|
||||||
|
active = fields.Boolean(default=True)
|
||||||
|
|
||||||
|
_code_uniq = models.Constraint(
|
||||||
|
'unique(code)', 'A membership tier with this code already exists.',
|
||||||
|
)
|
||||||
|
|
||||||
|
@api.model_create_multi
|
||||||
|
def create(self, vals_list):
|
||||||
|
tiers = super().create(vals_list)
|
||||||
|
for tier in tiers:
|
||||||
|
if not tier.product_id:
|
||||||
|
tier.product_id = tier._create_membership_product()
|
||||||
|
return tiers
|
||||||
|
|
||||||
|
def write(self, vals):
|
||||||
|
res = super().write(vals)
|
||||||
|
if 'price' in vals or 'name' in vals:
|
||||||
|
for tier in self:
|
||||||
|
if tier.product_id:
|
||||||
|
tier.product_id.write(tier._membership_product_values())
|
||||||
|
return res
|
||||||
|
|
||||||
|
def _membership_product_values(self):
|
||||||
|
self.ensure_one()
|
||||||
|
return {
|
||||||
|
'name': self.name,
|
||||||
|
'list_price': self.price,
|
||||||
|
'type': 'service',
|
||||||
|
'sale_ok': True,
|
||||||
|
'purchase_ok': False,
|
||||||
|
'invoice_policy': 'order',
|
||||||
|
}
|
||||||
|
|
||||||
|
def _create_membership_product(self):
|
||||||
|
self.ensure_one()
|
||||||
|
product = self.env['product.product'].create(self._membership_product_values())
|
||||||
|
return product
|
||||||
|
|
||||||
|
def duration_days(self):
|
||||||
|
self.ensure_one()
|
||||||
|
return PERIOD_DURATION_DAYS.get(self.period)
|
||||||
29
addons/community_membership/models/res_config_settings.py
Normal file
29
addons/community_membership/models/res_config_settings.py
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
from odoo import fields, models
|
||||||
|
|
||||||
|
|
||||||
|
class ResConfigSettings(models.TransientModel):
|
||||||
|
_inherit = 'res.config.settings'
|
||||||
|
|
||||||
|
membership_org_name = fields.Char(
|
||||||
|
string='Organization Display Name',
|
||||||
|
config_parameter='community_membership.org_name',
|
||||||
|
help="Used in membership emails, the QR card, and the verification page. "
|
||||||
|
"Never hardcode an organization name in module code - set it here.",
|
||||||
|
)
|
||||||
|
membership_member_id_format = fields.Char(
|
||||||
|
string='Member ID Format',
|
||||||
|
config_parameter='community_membership.member_id_format',
|
||||||
|
default='MEM-{year}-{seq}',
|
||||||
|
help="Placeholders: {year} = activation year, {seq} = sequence number.",
|
||||||
|
)
|
||||||
|
membership_renewal_offsets = fields.Char(
|
||||||
|
string='Renewal Reminder Offsets (days)',
|
||||||
|
config_parameter='community_membership.renewal_offsets',
|
||||||
|
default='30,14,7',
|
||||||
|
help="Comma-separated list of days-before-expiry to send renewal reminders.",
|
||||||
|
)
|
||||||
|
membership_portal_enabled = fields.Boolean(
|
||||||
|
string='Enable Member Portal',
|
||||||
|
config_parameter='community_membership.portal_enabled',
|
||||||
|
default=True,
|
||||||
|
)
|
||||||
63
addons/community_membership/models/res_partner.py
Normal file
63
addons/community_membership/models/res_partner.py
Normal file
@ -0,0 +1,63 @@
|
|||||||
|
from dateutil.relativedelta import relativedelta
|
||||||
|
|
||||||
|
from odoo import api, fields, models
|
||||||
|
|
||||||
|
MEMBER_ID_SEQUENCE_CODE = 'community.membership.member_id'
|
||||||
|
DEFAULT_MEMBER_ID_FORMAT = 'MEM-{year}-{seq}'
|
||||||
|
|
||||||
|
|
||||||
|
class ResPartner(models.Model):
|
||||||
|
_inherit = 'res.partner'
|
||||||
|
|
||||||
|
membership_member_id = fields.Char(
|
||||||
|
string='Member ID', copy=False, index=True, readonly=True,
|
||||||
|
)
|
||||||
|
membership_tier_id = fields.Many2one('community.membership.tier', string='Membership Tier')
|
||||||
|
membership_state = fields.Selection(
|
||||||
|
[
|
||||||
|
('none', 'Not a Member'),
|
||||||
|
('invoiced', 'Invoiced'),
|
||||||
|
('active', 'Active'),
|
||||||
|
('renewal_due', 'Renewal Due'),
|
||||||
|
('expired', 'Expired'),
|
||||||
|
],
|
||||||
|
default='none', copy=False, tracking=True,
|
||||||
|
)
|
||||||
|
membership_start = fields.Date(copy=False)
|
||||||
|
membership_expiry = fields.Date(copy=False)
|
||||||
|
family_head_id = fields.Many2one(
|
||||||
|
'res.partner', string='Family Head',
|
||||||
|
domain=[('membership_tier_id', '!=', False)],
|
||||||
|
help="If this member belongs to a family membership, the primary member it is grouped under.",
|
||||||
|
)
|
||||||
|
is_volunteer = fields.Boolean(string='Volunteer')
|
||||||
|
volunteer_hours = fields.Float(string='Volunteer Hours')
|
||||||
|
|
||||||
|
def action_activate_membership(self):
|
||||||
|
"""Transition to 'active', generating a member ID and expiry if needed."""
|
||||||
|
today = fields.Date.context_today(self)
|
||||||
|
for partner in self:
|
||||||
|
if not partner.membership_tier_id:
|
||||||
|
continue
|
||||||
|
vals = {'membership_state': 'active'}
|
||||||
|
if not partner.membership_member_id:
|
||||||
|
vals['membership_member_id'] = partner._generate_member_id()
|
||||||
|
if not partner.membership_start:
|
||||||
|
vals['membership_start'] = today
|
||||||
|
duration_days = partner.membership_tier_id.duration_days()
|
||||||
|
if duration_days:
|
||||||
|
vals['membership_expiry'] = today + relativedelta(days=duration_days)
|
||||||
|
partner.write(vals)
|
||||||
|
return True
|
||||||
|
|
||||||
|
def _generate_member_id(self):
|
||||||
|
self.ensure_one()
|
||||||
|
get_param = self.env['ir.config_parameter'].sudo().get_param
|
||||||
|
member_id_format = get_param('community_membership.member_id_format', DEFAULT_MEMBER_ID_FORMAT)
|
||||||
|
seq_number = self.env['ir.sequence'].next_by_code(MEMBER_ID_SEQUENCE_CODE) or '1'
|
||||||
|
year = fields.Date.context_today(self).year
|
||||||
|
return (
|
||||||
|
member_id_format
|
||||||
|
.replace('{year}', str(year))
|
||||||
|
.replace('{seq}', seq_number)
|
||||||
|
)
|
||||||
@ -1 +1,4 @@
|
|||||||
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
|
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
|
||||||
|
access_community_membership_tier_manager,community.membership.tier manager,model_community_membership_tier,group_membership_manager,1,1,1,1
|
||||||
|
access_community_membership_tier_user,community.membership.tier internal read,model_community_membership_tier,base.group_user,1,0,0,0
|
||||||
|
access_community_membership_tier_portal,community.membership.tier portal read,model_community_membership_tier,base.group_portal,1,0,0,0
|
||||||
|
|||||||
|
19
addons/community_membership/security/membership_security.xml
Normal file
19
addons/community_membership/security/membership_security.xml
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<odoo>
|
||||||
|
<record id="module_category_membership" model="ir.module.category">
|
||||||
|
<field name="name">Membership</field>
|
||||||
|
<field name="sequence">20</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<record id="privilege_membership" model="res.groups.privilege">
|
||||||
|
<field name="name">Membership</field>
|
||||||
|
<field name="category_id" ref="module_category_membership"/>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<record id="group_membership_manager" model="res.groups">
|
||||||
|
<field name="name">Membership Manager</field>
|
||||||
|
<field name="privilege_id" ref="privilege_membership"/>
|
||||||
|
<field name="implied_ids" eval="[(4, ref('base.group_user'))]"/>
|
||||||
|
<field name="comment">Can manage membership tiers, member records, and renewal settings.</field>
|
||||||
|
</record>
|
||||||
|
</odoo>
|
||||||
@ -0,0 +1 @@
|
|||||||
|
from . import test_membership
|
||||||
69
addons/community_membership/tests/test_membership.py
Normal file
69
addons/community_membership/tests/test_membership.py
Normal file
@ -0,0 +1,69 @@
|
|||||||
|
from odoo.tests.common import TransactionCase, tagged
|
||||||
|
|
||||||
|
|
||||||
|
@tagged('post_install', '-at_install')
|
||||||
|
class TestMembership(TransactionCase):
|
||||||
|
|
||||||
|
def test_tier_creates_product(self):
|
||||||
|
tier = self.env['community.membership.tier'].create({
|
||||||
|
'name': 'Individual',
|
||||||
|
'code': 'IND',
|
||||||
|
'price': 50.0,
|
||||||
|
'period': 'annual',
|
||||||
|
})
|
||||||
|
self.assertTrue(tier.product_id, "Creating a tier should auto-create a membership product")
|
||||||
|
self.assertEqual(tier.product_id.list_price, 50.0)
|
||||||
|
|
||||||
|
def test_member_id_generated_on_activation(self):
|
||||||
|
tier = self.env['community.membership.tier'].create({
|
||||||
|
'name': 'Individual',
|
||||||
|
'code': 'IND2',
|
||||||
|
'price': 50.0,
|
||||||
|
'period': 'annual',
|
||||||
|
})
|
||||||
|
partner = self.env['res.partner'].create({
|
||||||
|
'name': 'Jane Member',
|
||||||
|
'membership_tier_id': tier.id,
|
||||||
|
})
|
||||||
|
self.assertFalse(partner.membership_member_id)
|
||||||
|
self.assertEqual(partner.membership_state, 'none')
|
||||||
|
|
||||||
|
partner.action_activate_membership()
|
||||||
|
|
||||||
|
self.assertEqual(partner.membership_state, 'active')
|
||||||
|
self.assertTrue(partner.membership_member_id, "Member ID should be generated on activation")
|
||||||
|
self.assertTrue(partner.membership_member_id.startswith('MEM-'))
|
||||||
|
self.assertTrue(partner.membership_start)
|
||||||
|
self.assertTrue(partner.membership_expiry, "Annual tier should compute an expiry date")
|
||||||
|
|
||||||
|
def test_one_time_tier_never_expires(self):
|
||||||
|
tier = self.env['community.membership.tier'].create({
|
||||||
|
'name': 'Lifetime',
|
||||||
|
'code': 'LIFE',
|
||||||
|
'price': 500.0,
|
||||||
|
'period': 'one_time',
|
||||||
|
})
|
||||||
|
partner = self.env['res.partner'].create({
|
||||||
|
'name': 'Life Member',
|
||||||
|
'membership_tier_id': tier.id,
|
||||||
|
})
|
||||||
|
partner.action_activate_membership()
|
||||||
|
self.assertEqual(partner.membership_state, 'active')
|
||||||
|
self.assertFalse(partner.membership_expiry, "One-time/lifetime tier should not set an expiry")
|
||||||
|
|
||||||
|
def test_member_id_format_is_configurable(self):
|
||||||
|
self.env['ir.config_parameter'].sudo().set_param(
|
||||||
|
'community_membership.member_id_format', 'TESTORG-{year}-{seq}'
|
||||||
|
)
|
||||||
|
tier = self.env['community.membership.tier'].create({
|
||||||
|
'name': 'Individual',
|
||||||
|
'code': 'IND3',
|
||||||
|
'price': 50.0,
|
||||||
|
'period': 'annual',
|
||||||
|
})
|
||||||
|
partner = self.env['res.partner'].create({
|
||||||
|
'name': 'Custom Format Member',
|
||||||
|
'membership_tier_id': tier.id,
|
||||||
|
})
|
||||||
|
partner.action_activate_membership()
|
||||||
|
self.assertTrue(partner.membership_member_id.startswith('TESTORG-'))
|
||||||
17
addons/community_membership/views/membership_menus.xml
Normal file
17
addons/community_membership/views/membership_menus.xml
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<odoo>
|
||||||
|
<menuitem id="menu_membership_root" name="Membership" sequence="25"
|
||||||
|
groups="community_membership.group_membership_manager"/>
|
||||||
|
|
||||||
|
<menuitem id="menu_membership_members" name="Members"
|
||||||
|
parent="menu_membership_root" action="action_membership_members" sequence="10"/>
|
||||||
|
|
||||||
|
<menuitem id="menu_membership_configuration" name="Configuration"
|
||||||
|
parent="menu_membership_root" sequence="90"/>
|
||||||
|
|
||||||
|
<menuitem id="menu_membership_tiers" name="Membership Tiers"
|
||||||
|
parent="menu_membership_configuration" action="action_membership_tier" sequence="10"/>
|
||||||
|
|
||||||
|
<menuitem id="menu_membership_settings" name="Settings"
|
||||||
|
parent="menu_membership_configuration" action="action_membership_config_settings" sequence="90"/>
|
||||||
|
</odoo>
|
||||||
55
addons/community_membership/views/membership_tier_views.xml
Normal file
55
addons/community_membership/views/membership_tier_views.xml
Normal file
@ -0,0 +1,55 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<odoo>
|
||||||
|
<record id="view_membership_tier_list" model="ir.ui.view">
|
||||||
|
<field name="name">community.membership.tier.list</field>
|
||||||
|
<field name="model">community.membership.tier</field>
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
<list string="Membership Tiers">
|
||||||
|
<field name="sequence" widget="handle"/>
|
||||||
|
<field name="name"/>
|
||||||
|
<field name="code"/>
|
||||||
|
<field name="price" widget="monetary"/>
|
||||||
|
<field name="currency_id" column_invisible="1"/>
|
||||||
|
<field name="period"/>
|
||||||
|
<field name="active" column_invisible="1"/>
|
||||||
|
</list>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<record id="view_membership_tier_form" model="ir.ui.view">
|
||||||
|
<field name="name">community.membership.tier.form</field>
|
||||||
|
<field name="model">community.membership.tier</field>
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
<form string="Membership Tier">
|
||||||
|
<sheet>
|
||||||
|
<div class="oe_title">
|
||||||
|
<label for="name"/>
|
||||||
|
<h1><field name="name" placeholder="e.g. Individual"/></h1>
|
||||||
|
</div>
|
||||||
|
<group>
|
||||||
|
<group>
|
||||||
|
<field name="code"/>
|
||||||
|
<field name="price" widget="monetary"/>
|
||||||
|
<field name="currency_id" groups="base.group_multi_currency"/>
|
||||||
|
<field name="period"/>
|
||||||
|
</group>
|
||||||
|
<group>
|
||||||
|
<field name="sequence"/>
|
||||||
|
<field name="active"/>
|
||||||
|
<field name="product_id" readonly="1"/>
|
||||||
|
</group>
|
||||||
|
</group>
|
||||||
|
<group string="Notes">
|
||||||
|
<field name="member_state_logic" nolabel="1" placeholder="Optional admin notes on state handling for this tier..."/>
|
||||||
|
</group>
|
||||||
|
</sheet>
|
||||||
|
</form>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<record id="action_membership_tier" model="ir.actions.act_window">
|
||||||
|
<field name="name">Membership Tiers</field>
|
||||||
|
<field name="res_model">community.membership.tier</field>
|
||||||
|
<field name="view_mode">list,form</field>
|
||||||
|
</record>
|
||||||
|
</odoo>
|
||||||
@ -0,0 +1,40 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<odoo>
|
||||||
|
<record id="res_config_settings_view_form_membership" model="ir.ui.view">
|
||||||
|
<field name="name">res.config.settings.view.form.membership</field>
|
||||||
|
<field name="model">res.config.settings</field>
|
||||||
|
<field name="inherit_id" ref="base.res_config_settings_view_form"/>
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
<xpath expr="//form" position="inside">
|
||||||
|
<app data-string="Membership" string="Membership" name="community_membership"
|
||||||
|
groups="community_membership.group_membership_manager">
|
||||||
|
<block title="Membership" id="membership_settings">
|
||||||
|
<setting id="membership_org_name_setting" string="Organization Name"
|
||||||
|
help="Used in membership emails, the QR card, and the verification page">
|
||||||
|
<field name="membership_org_name" placeholder="e.g. My Community Organization"/>
|
||||||
|
</setting>
|
||||||
|
<setting id="membership_member_id_format_setting" string="Member ID Format"
|
||||||
|
help="Placeholders: {year}, {seq}">
|
||||||
|
<field name="membership_member_id_format"/>
|
||||||
|
</setting>
|
||||||
|
<setting id="membership_renewal_offsets_setting" string="Renewal Reminder Offsets"
|
||||||
|
help="Comma-separated days-before-expiry to send renewal reminders">
|
||||||
|
<field name="membership_renewal_offsets"/>
|
||||||
|
</setting>
|
||||||
|
<setting id="membership_portal_enabled_setting" string="Member Portal">
|
||||||
|
<field name="membership_portal_enabled"/>
|
||||||
|
</setting>
|
||||||
|
</block>
|
||||||
|
</app>
|
||||||
|
</xpath>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<record id="action_membership_config_settings" model="ir.actions.act_window">
|
||||||
|
<field name="name">Membership Settings</field>
|
||||||
|
<field name="res_model">res.config.settings</field>
|
||||||
|
<field name="view_mode">form</field>
|
||||||
|
<field name="target">current</field>
|
||||||
|
<field name="context">{'module': 'community_membership'}</field>
|
||||||
|
</record>
|
||||||
|
</odoo>
|
||||||
71
addons/community_membership/views/res_partner_views.xml
Normal file
71
addons/community_membership/views/res_partner_views.xml
Normal file
@ -0,0 +1,71 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<odoo>
|
||||||
|
<record id="view_partner_form_membership" model="ir.ui.view">
|
||||||
|
<field name="name">res.partner.form.membership</field>
|
||||||
|
<field name="model">res.partner</field>
|
||||||
|
<field name="inherit_id" ref="base.view_partner_form"/>
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
<notebook position="inside">
|
||||||
|
<page string="Membership" name="membership">
|
||||||
|
<group>
|
||||||
|
<group>
|
||||||
|
<field name="membership_member_id"/>
|
||||||
|
<field name="membership_tier_id"/>
|
||||||
|
<field name="membership_state"/>
|
||||||
|
<field name="family_head_id"/>
|
||||||
|
</group>
|
||||||
|
<group>
|
||||||
|
<field name="membership_start"/>
|
||||||
|
<field name="membership_expiry"/>
|
||||||
|
<field name="is_volunteer"/>
|
||||||
|
<field name="volunteer_hours" invisible="not is_volunteer"/>
|
||||||
|
</group>
|
||||||
|
</group>
|
||||||
|
<button name="action_activate_membership" type="object"
|
||||||
|
string="Activate Membership" class="btn-primary"
|
||||||
|
invisible="membership_state == 'active'"/>
|
||||||
|
</page>
|
||||||
|
</notebook>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<record id="view_partner_list_membership" model="ir.ui.view">
|
||||||
|
<field name="name">res.partner.list.membership</field>
|
||||||
|
<field name="model">res.partner</field>
|
||||||
|
<field name="inherit_id" ref="base.view_partner_tree"/>
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
<field name="display_name" position="after">
|
||||||
|
<field name="membership_member_id" optional="show"/>
|
||||||
|
<field name="membership_tier_id" optional="show"/>
|
||||||
|
<field name="membership_state" optional="show"/>
|
||||||
|
</field>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<record id="view_partner_kanban_membership" model="ir.ui.view">
|
||||||
|
<field name="name">res.partner.kanban.membership.search</field>
|
||||||
|
<field name="model">res.partner</field>
|
||||||
|
<field name="inherit_id" ref="base.view_res_partner_filter"/>
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
<filter name="type_person" position="after">
|
||||||
|
<separator/>
|
||||||
|
<filter string="Members" name="is_member" domain="[('membership_state', '!=', 'none')]"/>
|
||||||
|
<filter string="Active Members" name="membership_active" domain="[('membership_state', '=', 'active')]"/>
|
||||||
|
<filter string="Renewal Due" name="membership_renewal_due" domain="[('membership_state', '=', 'renewal_due')]"/>
|
||||||
|
<filter string="Expired Members" name="membership_expired" domain="[('membership_state', '=', 'expired')]"/>
|
||||||
|
<group name="group_by" position="inside">
|
||||||
|
<filter string="Membership Tier" name="group_by_tier" context="{'group_by': 'membership_tier_id'}"/>
|
||||||
|
<filter string="Membership State" name="group_by_state" context="{'group_by': 'membership_state'}"/>
|
||||||
|
</group>
|
||||||
|
</filter>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<record id="action_membership_members" model="ir.actions.act_window">
|
||||||
|
<field name="name">Members</field>
|
||||||
|
<field name="res_model">res.partner</field>
|
||||||
|
<field name="view_mode">list,kanban,form</field>
|
||||||
|
<field name="domain">[('membership_state', '!=', 'none')]</field>
|
||||||
|
<field name="context">{'default_membership_state': 'invoiced'}</field>
|
||||||
|
</record>
|
||||||
|
</odoo>
|
||||||
Loading…
x
Reference in New Issue
Block a user