diff --git a/.gitignore b/.gitignore
index 918e572..a6d9a58 100644
--- a/.gitignore
+++ b/.gitignore
@@ -17,3 +17,6 @@ data/raw/*
*.csv.bak
.env
+
+# Local MCP config (contains credentials)
+.mcp.json
diff --git a/addons/community_membership/CHANGELOG.rst b/addons/community_membership/CHANGELOG.rst
index dc462ac..7a72843 100644
--- a/addons/community_membership/CHANGELOG.rst
+++ b/addons/community_membership/CHANGELOG.rst
@@ -1,6 +1,15 @@
Changelog
=========
+19.0.1.1.0 (2026-09-27)
+~~~~~~~~~~~~~~~~~~~~~~~~
+* Tiers: fixed-term period (``term_years``), minimum age, proof-document
+ flag, family-member flag and a signup description.
+* Members: first/last name, month and year of birth with computed age,
+ board approval state (approve/reject), terms acceptance, proof-of-
+ eligibility upload and an inline family members list. Birth date and
+ proof document are visible to Membership Managers only.
+
19.0.1.0.0 (2026-08-17)
~~~~~~~~~~~~~~~~~~~~~~~~
* Initial release: configurable membership tiers, ``res.partner`` extension
diff --git a/addons/community_membership/__manifest__.py b/addons/community_membership/__manifest__.py
index 8a47cb2..c5c38a5 100644
--- a/addons/community_membership/__manifest__.py
+++ b/addons/community_membership/__manifest__.py
@@ -1,6 +1,6 @@
{
'name': 'Community OS - Membership',
- 'version': '19.0.1.0.0',
+ 'version': '19.0.1.1.0',
'category': 'Sales/Members',
'summary': 'Configurable membership tiers, family grouping, renewals, QR membership card and self-service portal',
'description': """
diff --git a/addons/community_membership/models/membership_tier.py b/addons/community_membership/models/membership_tier.py
index 550396a..8ed1e50 100644
--- a/addons/community_membership/models/membership_tier.py
+++ b/addons/community_membership/models/membership_tier.py
@@ -1,3 +1,5 @@
+from dateutil.relativedelta import relativedelta
+
from odoo import api, fields, models
PERIOD_DURATION_DAYS = {
@@ -24,9 +26,30 @@ class CommunityMembershipTier(models.Model):
('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 "
@@ -42,6 +65,10 @@ class CommunityMembershipTier(models.Model):
_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):
@@ -78,3 +105,11 @@ class CommunityMembershipTier(models.Model):
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
diff --git a/addons/community_membership/models/res_partner.py b/addons/community_membership/models/res_partner.py
index 5ab581f..9ba2e66 100644
--- a/addons/community_membership/models/res_partner.py
+++ b/addons/community_membership/models/res_partner.py
@@ -2,13 +2,20 @@ import base64
import io
import qrcode
-from dateutil.relativedelta import relativedelta
-from odoo import api, fields, models
+from odoo import _, api, fields, models
+from odoo.exceptions import ValidationError
MEMBER_ID_SEQUENCE_CODE = 'community.membership.member_id'
DEFAULT_MEMBER_ID_FORMAT = 'MEM-{year}-{seq}'
DEFAULT_RENEWAL_OFFSETS = '30,14,7'
+MANAGER_GROUP = 'community_membership.group_membership_manager'
+
+BIRTH_MONTHS = [
+ ('1', 'January'), ('2', 'February'), ('3', 'March'), ('4', 'April'),
+ ('5', 'May'), ('6', 'June'), ('7', 'July'), ('8', 'August'),
+ ('9', 'September'), ('10', 'October'), ('11', 'November'), ('12', 'December'),
+]
class ResPartner(models.Model):
@@ -35,11 +42,98 @@ class ResPartner(models.Model):
domain=[('membership_tier_id', '!=', False)],
help="If this member belongs to a family membership, the primary member it is grouped under.",
)
+ family_member_ids = fields.One2many(
+ 'res.partner', 'family_head_id', string='Family Members',
+ help="Spouse, children or other people covered by this member's family membership.",
+ )
+ first_name = fields.Char(string='First Name')
+ last_name = fields.Char(string='Last Name')
+ # Only month and year of birth are collected, never the full date.
+ birth_month = fields.Selection(BIRTH_MONTHS, string='Birth Month', groups=MANAGER_GROUP)
+ birth_year = fields.Integer(string='Birth Year', groups=MANAGER_GROUP)
+ member_age = fields.Integer(
+ string='Age', compute='_compute_member_age', groups=MANAGER_GROUP,
+ help="Age in whole years, worked out from the birth month and year.",
+ )
+ membership_approval_state = fields.Selection(
+ [
+ ('pending', 'Pending Approval'),
+ ('approved', 'Approved'),
+ ('rejected', 'Rejected'),
+ ],
+ string='Board Approval', copy=False, tracking=True,
+ help="Every new membership is subject to approval by the Board of Directors.",
+ )
+ membership_terms_accepted = fields.Boolean(string='Accepted Terms & Conditions', copy=False)
+ membership_proof_document = fields.Binary(
+ string='Proof of Eligibility', attachment=True, copy=False, groups=MANAGER_GROUP,
+ help="Document confirming eligibility for the tier, e.g. proof of age or full-time student status.",
+ )
+ membership_proof_filename = fields.Char(string='Proof File Name', copy=False, groups=MANAGER_GROUP)
is_volunteer = fields.Boolean(string='Volunteer')
volunteer_hours = fields.Float(string='Volunteer Hours')
membership_org_name = fields.Char(compute='_compute_membership_org_name')
membership_card_qr = fields.Binary(compute='_compute_membership_card_qr')
+ @api.depends('birth_month', 'birth_year')
+ def _compute_member_age(self):
+ today = fields.Date.context_today(self)
+ for partner in self:
+ partner.member_age = partner._age_on(today)
+
+ def _age_on(self, day):
+ """Whole years of age on `day`, or 0 when the birth month or year is unknown."""
+ self.ensure_one()
+ if not self.birth_month or not self.birth_year:
+ return 0
+ age = day.year - self.birth_year
+ if day.month < int(self.birth_month):
+ age -= 1
+ return max(age, 0)
+
+ @api.model_create_multi
+ def create(self, vals_list):
+ for vals in vals_list:
+ if not vals.get('name'):
+ full_name = ' '.join(part for part in (vals.get('first_name'), vals.get('last_name')) if part)
+ if full_name:
+ vals['name'] = full_name
+ return super().create(vals_list)
+
+ @api.onchange('first_name', 'last_name')
+ def _onchange_first_last_name(self):
+ full_name = ' '.join(part for part in (self.first_name, self.last_name) if part)
+ if full_name:
+ self.name = full_name
+
+ @api.constrains('birth_year')
+ def _check_birth_year(self):
+ this_year = fields.Date.context_today(self).year
+ for partner in self.sudo():
+ if partner.birth_year and not 1900 <= partner.birth_year <= this_year:
+ raise ValidationError(_("Birth year must be between 1900 and %s.", this_year))
+
+ @api.constrains('membership_tier_id', 'birth_month', 'birth_year')
+ def _check_tier_minimum_age(self):
+ today = fields.Date.context_today(self)
+ for partner in self.sudo():
+ tier = partner.membership_tier_id
+ if not tier.min_age or not partner.birth_year:
+ continue
+ if partner._age_on(today) < tier.min_age:
+ raise ValidationError(_(
+ "%(name)s is too young for the %(tier)s tier, which requires age %(age)s or older.",
+ name=partner.display_name, tier=tier.name, age=tier.min_age,
+ ))
+
+ def action_approve_membership(self):
+ self.write({'membership_approval_state': 'approved'})
+ return True
+
+ def action_reject_membership(self):
+ self.write({'membership_approval_state': 'rejected'})
+ return True
+
def _compute_membership_org_name(self):
org_name = self.env['ir.config_parameter'].sudo().get_param('community_membership.org_name') or ''
for partner in self:
@@ -74,9 +168,9 @@ class ResPartner(models.Model):
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)
+ expiry = partner.membership_tier_id.get_expiry_date(today)
+ if expiry:
+ vals['membership_expiry'] = expiry
partner.write(vals)
return True
diff --git a/addons/community_membership/tests/__init__.py b/addons/community_membership/tests/__init__.py
index 65832d0..1f7bdc6 100644
--- a/addons/community_membership/tests/__init__.py
+++ b/addons/community_membership/tests/__init__.py
@@ -1,3 +1,4 @@
from . import test_membership
from . import test_membership_renewal
from . import test_membership_verify
+from . import test_membership_member_details
diff --git a/addons/community_membership/tests/test_membership.py b/addons/community_membership/tests/test_membership.py
index dfbe7d2..555f26f 100644
--- a/addons/community_membership/tests/test_membership.py
+++ b/addons/community_membership/tests/test_membership.py
@@ -7,7 +7,7 @@ class TestMembership(TransactionCase):
def test_tier_creates_product(self):
tier = self.env['community.membership.tier'].create({
'name': 'Individual',
- 'code': 'IND',
+ 'code': 'TEST-IND',
'price': 50.0,
'period': 'annual',
})
@@ -15,6 +15,10 @@ class TestMembership(TransactionCase):
self.assertEqual(tier.product_id.list_price, 50.0)
def test_member_id_generated_on_activation(self):
+ # A deployment module may seed its own format; pin the product default.
+ self.env['ir.config_parameter'].sudo().set_param(
+ 'community_membership.member_id_format', 'MEM-{year}-{seq}'
+ )
tier = self.env['community.membership.tier'].create({
'name': 'Individual',
'code': 'IND2',
@@ -39,7 +43,7 @@ class TestMembership(TransactionCase):
def test_one_time_tier_never_expires(self):
tier = self.env['community.membership.tier'].create({
'name': 'Lifetime',
- 'code': 'LIFE',
+ 'code': 'TEST-LIFE',
'price': 500.0,
'period': 'one_time',
})
diff --git a/addons/community_membership/tests/test_membership_member_details.py b/addons/community_membership/tests/test_membership_member_details.py
new file mode 100644
index 0000000..0afaf7b
--- /dev/null
+++ b/addons/community_membership/tests/test_membership_member_details.py
@@ -0,0 +1,58 @@
+from datetime import date
+
+from dateutil.relativedelta import relativedelta
+
+from odoo.exceptions import ValidationError
+from odoo.tests.common import TransactionCase, tagged
+
+
+@tagged('post_install', '-at_install')
+class TestMembershipMemberDetails(TransactionCase):
+
+ def _tier(self, code, **values):
+ return self.env['community.membership.tier'].create({
+ 'name': code, 'code': code, 'price': 10.0, 'period': 'annual', **values,
+ })
+
+ def test_fixed_term_tier_expires_after_term_years(self):
+ tier = self._tier('DET-10Y', period='fixed_term', term_years=10)
+ partner = self.env['res.partner'].create({'name': 'Ten Year Member', 'membership_tier_id': tier.id})
+ partner.action_activate_membership()
+ self.assertEqual(partner.membership_expiry, partner.membership_start + relativedelta(years=10))
+
+ def test_name_built_from_first_and_last_name(self):
+ partner = self.env['res.partner'].create({'first_name': 'Arun', 'last_name': 'Kumar'})
+ self.assertEqual(partner.name, 'Arun Kumar')
+
+ def test_age_counts_birth_month(self):
+ partner = self.env['res.partner'].create({'name': 'Aged Member', 'birth_month': '6', 'birth_year': 1960})
+ self.assertEqual(partner._age_on(date(2026, 5, 31)), 65)
+ self.assertEqual(partner._age_on(date(2026, 6, 1)), 66)
+
+ def test_tier_minimum_age_enforced(self):
+ tier = self._tier('DET-SEN', min_age=65)
+ this_year = date.today().year
+ with self.assertRaises(ValidationError):
+ self.env['res.partner'].create({
+ 'name': 'Too Young', 'membership_tier_id': tier.id,
+ 'birth_month': '1', 'birth_year': this_year - 30,
+ })
+ # Unknown birth year is allowed; the board can check documents instead.
+ self.env['res.partner'].create({'name': 'Unknown Age', 'membership_tier_id': tier.id})
+
+ def test_birth_year_must_be_plausible(self):
+ with self.assertRaises(ValidationError):
+ self.env['res.partner'].create({'name': 'Bad Year', 'birth_year': 1850})
+
+ def test_family_members_listed_on_head(self):
+ tier = self._tier('DET-FAM', allows_family_members=True)
+ head = self.env['res.partner'].create({'name': 'Family Head', 'membership_tier_id': tier.id})
+ spouse = self.env['res.partner'].create({'name': 'Spouse', 'family_head_id': head.id})
+ self.assertEqual(head.family_member_ids, spouse)
+
+ def test_board_approval(self):
+ partner = self.env['res.partner'].create({
+ 'name': 'Applicant', 'membership_approval_state': 'pending',
+ })
+ partner.action_approve_membership()
+ self.assertEqual(partner.membership_approval_state, 'approved')
diff --git a/addons/community_membership/views/membership_tier_views.xml b/addons/community_membership/views/membership_tier_views.xml
index 011fb21..35b14e4 100644
--- a/addons/community_membership/views/membership_tier_views.xml
+++ b/addons/community_membership/views/membership_tier_views.xml
@@ -11,6 +11,8 @@
+
+
@@ -32,6 +34,8 @@
+
@@ -39,6 +43,17 @@
+
+
+
+
+
+
+
+
+
+
diff --git a/addons/community_membership/views/res_partner_views.xml b/addons/community_membership/views/res_partner_views.xml
index 1f568fa..f6d6fbf 100644
--- a/addons/community_membership/views/res_partner_views.xml
+++ b/addons/community_membership/views/res_partner_views.xml
@@ -8,22 +8,67 @@
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
+
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
@@ -53,6 +98,8 @@
+
diff --git a/addons/tncsc_deployment/__manifest__.py b/addons/tncsc_deployment/__manifest__.py
index af2888e..ac01fec 100644
--- a/addons/tncsc_deployment/__manifest__.py
+++ b/addons/tncsc_deployment/__manifest__.py
@@ -1,6 +1,6 @@
{
'name': 'TNCSC Deployment',
- 'version': '19.0.1.0.0',
+ 'version': '19.0.1.1.0',
'category': 'Hidden',
'summary': 'Tamil Nadu Cultural Society of Canada - client configuration for the Community OS suite',
'description': """
@@ -44,6 +44,7 @@ that applies to the community_* product modules.
'data/mail_template_overrides.xml',
'data/events_data.xml',
'data/ir_cron.xml',
+ 'views/res_partner_views.xml',
'views/website_pages.xml',
'views/website_header_overrides.xml',
'views/browser_branding.xml',
diff --git a/addons/tncsc_deployment/data/membership_data.xml b/addons/tncsc_deployment/data/membership_data.xml
index 2ad02e8..d46cf08 100644
--- a/addons/tncsc_deployment/data/membership_data.xml
+++ b/addons/tncsc_deployment/data/membership_data.xml
@@ -10,46 +10,68 @@
-
-
- Individual
- IND
- 50.0
-
- annual
- 10
-
+
- Family
+ Annual/Family
FAM
- 80.0
-
- annual
- 20
-
-
- Student
- STU
- 20.0
-
- annual
- 30
-
-
- Senior
- SEN
30.0
annual
- 40
+
+ Covers the member, his/her spouse and kids living at the same address.
+ 10
+
+
+ Long-Term Membership
+ LONG
+ 250.0
+
+ fixed_term
+ 10
+ 20
- Life
+ Life Term Membership
LIFE
500.0
one_time
+ 30
+
+
+ Senior Annual Membership
+ SEN
+ 15.0
+
+ annual
+ 65
+
+ For seniors over 65 years. Documents may be requested to confirm.
+ 40
+
+
+ Senior Life Term
+ SENLIFE
+ 250.0
+
+ one_time
+ 60
+
+ Life term membership for seniors aged 60 and over. Documents may be requested to confirm.
50
+
+ Student Annual Membership
+ STU
+ 10.0
+
+ annual
+
+ For full-time students. Documents may be requested to confirm.
+ 60
+
diff --git a/addons/tncsc_deployment/migrations/19.0.1.1.0/post-migrate.py b/addons/tncsc_deployment/migrations/19.0.1.1.0/post-migrate.py
new file mode 100644
index 0000000..bb37648
--- /dev/null
+++ b/addons/tncsc_deployment/migrations/19.0.1.1.0/post-migrate.py
@@ -0,0 +1,42 @@
+"""Bring an existing database's membership tiers in line with the six plans
+on tncsc.com/membership-registration.
+
+data/membership_data.xml is noupdate="1", so an upgrade creates the new
+tiers (Long-Term, Senior Life Term) but leaves existing ones untouched.
+This script rewrites those, and archives the old "Individual" tier, which
+TNCSC doesn't offer. Archiving, not deleting, keeps it on any member
+already holding it.
+"""
+from odoo import SUPERUSER_ID, api
+
+TIER_VALUES = {
+ 'membership_tier_family': {
+ 'name': 'Annual/Family', 'price': 30.0, 'period': 'annual', 'sequence': 10,
+ 'allows_family_members': True, 'min_age': 0, 'requires_proof': False,
+ 'description': 'Covers the member, his/her spouse and kids living at the same address.',
+ },
+ 'membership_tier_life': {
+ 'name': 'Life Term Membership', 'price': 500.0, 'period': 'one_time', 'sequence': 30,
+ },
+ 'membership_tier_senior': {
+ 'name': 'Senior Annual Membership', 'price': 15.0, 'period': 'annual', 'sequence': 40,
+ 'min_age': 65, 'requires_proof': True,
+ 'description': 'For seniors over 65 years. Documents may be requested to confirm.',
+ },
+ 'membership_tier_student': {
+ 'name': 'Student Annual Membership', 'price': 10.0, 'period': 'annual', 'sequence': 60,
+ 'requires_proof': True,
+ 'description': 'For full-time students. Documents may be requested to confirm.',
+ },
+}
+
+
+def migrate(cr, version):
+ env = api.Environment(cr, SUPERUSER_ID, {})
+ for xmlid, values in TIER_VALUES.items():
+ tier = env.ref(f'tncsc_deployment.{xmlid}', raise_if_not_found=False)
+ if tier:
+ tier.write(values)
+ individual = env.ref('tncsc_deployment.membership_tier_individual', raise_if_not_found=False)
+ if individual:
+ individual.active = False
diff --git a/addons/tncsc_deployment/models/__init__.py b/addons/tncsc_deployment/models/__init__.py
index aff44f3..b067a53 100644
--- a/addons/tncsc_deployment/models/__init__.py
+++ b/addons/tncsc_deployment/models/__init__.py
@@ -1 +1,2 @@
from . import res_company
+from . import res_partner
diff --git a/addons/tncsc_deployment/models/res_partner.py b/addons/tncsc_deployment/models/res_partner.py
new file mode 100644
index 0000000..a3f36c4
--- /dev/null
+++ b/addons/tncsc_deployment/models/res_partner.py
@@ -0,0 +1,36 @@
+from odoo import fields, models
+
+MANAGER_GROUP = 'community_membership.group_membership_manager'
+YES_NO = [('yes', 'Yes'), ('no', 'No')]
+
+
+class ResPartner(models.Model):
+ """TNCSC membership eligibility answers, asked on the registration form.
+
+ Stored as Yes/No selections rather than booleans so an unanswered
+ question stays distinguishable from a "No".
+ """
+ _inherit = 'res.partner'
+
+ tncsc_born_tamil_nadu = fields.Selection(
+ YES_NO, string='Born in / Origin of Tamil Nadu', groups=MANAGER_GROUP,
+ help='Are you "born in Tamil Nadu, India" or born in any part of India but origin of Tamil Nadu?',
+ )
+ tncsc_pio_oci = fields.Selection(
+ YES_NO, string='Holds / Qualifies for PIO or OCI', groups=MANAGER_GROUP,
+ help='Are you the holder of Indian PIO/OCI or qualified to obtain one?',
+ )
+ tncsc_tamil_speaker_resident = fields.Selection(
+ YES_NO, string='Speaks Tamil, Lived in Tamil Nadu', groups=MANAGER_GROUP,
+ help='Do you speak Tamil and ever lived in Tamil Nadu, India with Indian citizenship?',
+ )
+ tncsc_family_tamil_indian = fields.Selection(
+ YES_NO, string='Spouse / Parent Tamil & Indian', groups=MANAGER_GROUP,
+ help='Does your spouse or either of your parents speak Tamil and have Indian citizenship, '
+ 'or ever had Indian citizenship before migrating to Canada?',
+ )
+ tncsc_other_citizenship = fields.Selection(
+ YES_NO, string='Former Citizenship (Neighbouring Country)', groups=MANAGER_GROUP,
+ help='Had you ever been a citizen of Afghanistan, Bangladesh, Bhutan, China, Nepal, '
+ 'Pakistan or Sri Lanka?',
+ )
diff --git a/addons/tncsc_deployment/tests/test_tncsc_deployment.py b/addons/tncsc_deployment/tests/test_tncsc_deployment.py
index a6c24d5..bb8688b 100644
--- a/addons/tncsc_deployment/tests/test_tncsc_deployment.py
+++ b/addons/tncsc_deployment/tests/test_tncsc_deployment.py
@@ -25,19 +25,37 @@ class TestTncscDeployment(TransactionCase):
self.assertEqual(get_param('community_membership.org_name'), 'Tamil Nadu Cultural Society of Canada')
self.assertEqual(get_param('community_membership.member_id_format'), 'TNCSC-{year}-{seq}')
- def test_five_membership_tiers_seeded(self):
+ def test_six_membership_tiers_seeded(self):
tiers = self.env['community.membership.tier'].search([])
- self.assertEqual(len(tiers), 5)
+ self.assertEqual(len(tiers), 6)
by_code = {tier.code: tier for tier in tiers}
- self.assertEqual(by_code['IND'].price, 50.0)
- self.assertEqual(by_code['FAM'].price, 80.0)
- self.assertEqual(by_code['STU'].price, 20.0)
- self.assertEqual(by_code['SEN'].price, 30.0)
+ self.assertEqual(by_code['FAM'].price, 30.0)
+ self.assertTrue(by_code['FAM'].allows_family_members)
+ self.assertEqual(by_code['LONG'].price, 250.0)
+ self.assertEqual((by_code['LONG'].period, by_code['LONG'].term_years), ('fixed_term', 10))
self.assertEqual(by_code['LIFE'].price, 500.0)
+ self.assertEqual(by_code['SEN'].price, 15.0)
+ self.assertEqual(by_code['SEN'].min_age, 65)
+ self.assertEqual(by_code['SENLIFE'].price, 250.0)
+ self.assertEqual((by_code['SENLIFE'].period, by_code['SENLIFE'].min_age), ('one_time', 60))
+ self.assertEqual(by_code['STU'].price, 10.0)
+ self.assertTrue(by_code['STU'].requires_proof)
for tier in tiers:
self.assertEqual(tier.currency_id.name, 'CAD')
self.assertTrue(tier.product_id, "Every seeded tier should still auto-create its product")
+ def test_eligibility_answers_stored(self):
+ partner = self.env['res.partner'].create({
+ 'first_name': 'Kavya',
+ 'last_name': 'Raman',
+ 'tncsc_born_tamil_nadu': 'yes',
+ 'tncsc_other_citizenship': 'no',
+ })
+ self.assertEqual(partner.name, 'Kavya Raman')
+ self.assertEqual(partner.tncsc_born_tamil_nadu, 'yes')
+ self.assertEqual(partner.tncsc_other_citizenship, 'no')
+ self.assertFalse(partner.tncsc_pio_oci, "Unanswered questions should stay empty, not 'no'")
+
def test_chart_of_accounts_loaded(self):
company = self.env.company
self.assertEqual(company.currency_id.name, 'CAD')
diff --git a/addons/tncsc_deployment/views/res_partner_views.xml b/addons/tncsc_deployment/views/res_partner_views.xml
new file mode 100644
index 0000000..fa68638
--- /dev/null
+++ b/addons/tncsc_deployment/views/res_partner_views.xml
@@ -0,0 +1,21 @@
+
+
+
+ res.partner.form.tncsc.eligibility
+ res.partner
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/scripts/migrate_wp_members.py b/scripts/migrate_wp_members.py
index 4b7d5a6..b52b53f 100644
--- a/scripts/migrate_wp_members.py
+++ b/scripts/migrate_wp_members.py
@@ -17,8 +17,9 @@ Behaviour, confirmed with TNCSC before writing this:
- Rows whose email is on the @abbuzz.com test/spam domain are skipped
entirely (not real members).
- "Level" maps to a tier code: Annual/Family -> FAM, Long-Term
- Membership -> LIFE, Student Annual Membership -> STU, Senior Annual
- Membership -> SEN. Blank Level, or a comma-separated multi-value Level
+ Membership -> LONG, Life Term Membership -> LIFE, Student Annual
+ Membership -> STU, Senior Annual Membership -> SEN, Senior Life Term ->
+ SENLIFE. Blank Level, or a comma-separated multi-value Level
(ambiguous - can't tell which one is current), is imported as a
contact with NO tier set and flagged 'needs_review' in the report -
never auto-guessed.
@@ -70,9 +71,11 @@ COL_JOIN_DATE = 30
LEVEL_TO_TIER_CODE = {
'Annual/Family': 'FAM',
- 'Long-Term Membership': 'LIFE',
+ 'Long-Term Membership': 'LONG',
+ 'Life Term Membership': 'LIFE',
'Student Annual Membership': 'STU',
'Senior Annual Membership': 'SEN',
+ 'Senior Life Term': 'SENLIFE',
}