metatroncubeswdev ae324c30f9 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>
2026-08-17 20:03:10 -04:00

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)