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>
81 lines
2.6 KiB
Python
81 lines
2.6 KiB
Python
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)
|