metatroncubeswdev 657792b268
Some checks failed
CI / Brand-leak check (push) Has been cancelled
CI / flake8 / pylint-odoo / manifest completeness (push) Has been cancelled
CI / Install all modules with --test-enable (push) Has been cancelled
Add member application fields and the six real TNCSC membership plans
community_membership:
- Tiers get a fixed-term period (term_years), minimum age, a
  proof-document flag, a family-member flag and a signup description.
- Members get first/last name (composing the contact name), month and
  year of birth with computed age, board approval state with
  approve/reject buttons, terms acceptance, a proof-of-eligibility upload
  and an inline family members list. Birth date and proof document are
  restricted to Membership Managers.
- Constraints: plausible birth year; a tier's minimum age is enforced
  when the birth year is known.
- Fix pre-existing tests that collided with deployment-seeded tier codes
  and member ID format.

tncsc_deployment:
- Five Yes/No eligibility questions from the tncsc.com registration form
  (manager-only), shown in an Eligibility section of the member form.
- Tiers now match tncsc.com/membership-registration: Annual/Family $30,
  Long-Term (10 yr) $250, Life $500, Senior Annual (65+) $15, Senior Life
  (60+) $250, Student Annual $10 (CAD).
- 19.0.1.1.0 post-migrate rewrites existing tiers (noupdate data) and
  archives the unused Individual tier.

scripts/migrate_wp_members.py maps WordPress levels to the new LONG and
SENLIFE tiers. .mcp.json (holds credentials) is now gitignored.

Verified on the local Docker stack: -u on tncsc_site ran the migration
cleanly, and a fresh install with --test-enable passed 32/32 tests for
community_membership and tncsc_deployment.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-27 10:46:31 -04:00

116 lines
4.1 KiB
Python

from dateutil.relativedelta import relativedelta
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'),
('fixed_term', 'Fixed term (years)'),
],
default='annual', required=True,
)
term_years = fields.Integer(
string='Term (Years)', default=1,
help="Length of the membership in years, for fixed-term tiers (e.g. 10 for a long-term membership).",
)
description = fields.Text(
translate=True,
help="Who the tier is for and what it covers, shown to applicants on the signup page.",
)
min_age = fields.Integer(
string='Minimum Age',
help="Applicants must be at least this old to choose this tier. 0 means no age limit.",
)
requires_proof = fields.Boolean(
string='Requires Proof Document',
help="Applicants may be asked to upload proof of eligibility (e.g. age or student status).",
)
allows_family_members = fields.Boolean(
string='Includes Family Members',
help="The membership also covers family members, who can be listed on the member's record.",
)
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.',
)
_term_years_positive = models.Constraint(
"CHECK(period != 'fixed_term' OR term_years > 0)",
'A fixed-term tier needs a term of at least one year.',
)
@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)
def get_expiry_date(self, start):
"""Expiry for a membership of this tier starting on `start`, or False if it never expires."""
self.ensure_one()
if self.period == 'fixed_term':
return start + relativedelta(years=self.term_years)
duration_days = self.duration_days()
return start + relativedelta(days=duration_days) if duration_days else False