-
You're on the Waitlist
-
The class is currently full. We'll notify you as soon as a seat opens up.
+
+
+
+
+
+
+
+
+
+
+
+
+ Placed on Waitlist
+
+ The selected class is currently full. We have placed
+ on the waitlist for
+ .
+ You will be notified immediately when a seat opens up.
+
+
+
+
+
+
+
+
+ Registration Complete!
+
+ Congratulations! is successfully enrolled in
+ .
+
+
+
+
+
What's Next:
+
+ - Confirmation Receipt: An enrollment confirmation has been sent to your email.
+ - Classroom Access: Log in to your portal anytime to view class schedule, instructors, and student attendance.
+ - School Portal: Access your parent dashboard at /my/school.
+
+
+
+
+
+
-
-
-
-
Registration Complete
-
is enrolled in
-
.
-
-
+
diff --git a/addons/community_theme_base/static/description/banner.png b/addons/community_theme_base/static/description/banner.png
new file mode 100644
index 0000000..775c93d
Binary files /dev/null and b/addons/community_theme_base/static/description/banner.png differ
diff --git a/addons/community_theme_base/static/description/icon.png b/addons/community_theme_base/static/description/icon.png
new file mode 100644
index 0000000..88e3da8
Binary files /dev/null and b/addons/community_theme_base/static/description/icon.png differ
diff --git a/addons/event_qr_ticketing/static/description/banner.png b/addons/event_qr_ticketing/static/description/banner.png
new file mode 100644
index 0000000..5ed5530
Binary files /dev/null and b/addons/event_qr_ticketing/static/description/banner.png differ
diff --git a/addons/event_qr_ticketing/static/description/icon.png b/addons/event_qr_ticketing/static/description/icon.png
new file mode 100644
index 0000000..546f833
Binary files /dev/null and b/addons/event_qr_ticketing/static/description/icon.png differ
diff --git a/addons/tncsc_deployment/__init__.py b/addons/tncsc_deployment/__init__.py
index 0bee969..858964f 100644
--- a/addons/tncsc_deployment/__init__.py
+++ b/addons/tncsc_deployment/__init__.py
@@ -12,6 +12,8 @@ def post_init_hook(env):
env.company._tncsc_setup_accounting()
_remove_stock_homepage_page(env)
_fix_website_menus(env)
+ env['ir.config_parameter'].sudo().set_param('web.base.url', 'https://tncsc.odoo.thedomainnest.com')
+ env['ir.config_parameter'].sudo().set_param('web.base.url.freeze', 'True')
def _remove_stock_homepage_page(env):
diff --git a/addons/tncsc_deployment/__manifest__.py b/addons/tncsc_deployment/__manifest__.py
index a62e4f9..8393777 100644
--- a/addons/tncsc_deployment/__manifest__.py
+++ b/addons/tncsc_deployment/__manifest__.py
@@ -1,6 +1,6 @@
{
'name': 'TNCSC Deployment',
- 'version': '19.0.1.2.0',
+ 'version': '19.0.1.3.0',
'category': 'Hidden',
'summary': 'Tamil Nadu Cultural Society of Canada - client configuration for the Community OS suite',
'description': """
diff --git a/addons/tncsc_deployment/controllers/__init__.py b/addons/tncsc_deployment/controllers/__init__.py
index 9c98204..64220b0 100644
--- a/addons/tncsc_deployment/controllers/__init__.py
+++ b/addons/tncsc_deployment/controllers/__init__.py
@@ -1,3 +1,4 @@
# -*- coding: utf-8 -*-
from . import main
from . import dashboard
+from . import membership
diff --git a/addons/tncsc_deployment/controllers/membership.py b/addons/tncsc_deployment/controllers/membership.py
new file mode 100644
index 0000000..63c8923
--- /dev/null
+++ b/addons/tncsc_deployment/controllers/membership.py
@@ -0,0 +1,295 @@
+# -*- coding: utf-8 -*-
+import base64
+import json
+import logging
+from datetime import date
+
+from odoo import fields, http
+from odoo.http import request
+
+_logger = logging.getLogger(__name__)
+
+ELIGIBILITY_KEYS = (
+ 'tncsc_born_tamil_nadu',
+ 'tncsc_pio_oci',
+ 'tncsc_tamil_speaker_resident',
+ 'tncsc_family_tamil_indian',
+ 'tncsc_other_citizenship',
+)
+
+MONTH_OPTIONS = [
+ ('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 TNCSCMembershipController(http.Controller):
+
+ @http.route('/membership/check_username', type='jsonrpc', auth='public', methods=['POST'], website=True)
+ def check_username(self, username=None, **kwargs):
+ if not username:
+ return {'available': False, 'message': 'Username is required.'}
+ username = username.strip()
+ if len(username) < 3:
+ return {'available': False, 'message': 'Username must be at least 3 characters.'}
+
+ count = request.env['res.users'].sudo().search_count([('login', '=', username)])
+ if count > 0:
+ return {'available': False, 'message': 'Username is already taken.'}
+ return {'available': True, 'message': 'Username is available.'}
+
+ @http.route('/membership/check_email', type='jsonrpc', auth='public', methods=['POST'], website=True)
+ def check_email(self, email=None, **kwargs):
+ if not email:
+ return {'available': False, 'message': 'Email is required.'}
+ email = email.strip()
+ if '@' not in email or '.' not in email:
+ return {'available': False, 'message': 'Please enter a valid email address.'}
+
+ user_count = request.env['res.users'].sudo().search_count([('email', '=', email)])
+ partner_count = request.env['res.partner'].sudo().search_count([
+ ('email', '=', email),
+ ('membership_tier_id', '!=', False),
+ ])
+ if user_count > 0 or partner_count > 0:
+ return {'available': False, 'message': 'An account with this email already exists.'}
+ return {'available': True, 'message': 'Email is available.'}
+
+ def _get_form_context(self, selected_code=None, errors=None, form_data=None):
+ Tier = request.env['community.membership.tier'].sudo()
+ tiers = Tier.search([('active', '=', True)], order='sequence, id')
+
+ selected_tier = None
+ if selected_code:
+ selected_tier = tiers.filtered(lambda t: t.code == selected_code)
+ if not selected_tier and selected_code.isdigit():
+ selected_tier = tiers.filtered(lambda t: t.id == int(selected_code))
+ if selected_tier:
+ selected_tier = selected_tier[0]
+ if not selected_tier and tiers:
+ selected_tier = tiers[0]
+
+ countries = request.env['res.country'].sudo().search([], order='name asc')
+ ca_country = request.env.ref('base.ca', raise_if_not_found=False)
+ if not ca_country:
+ ca_country = request.env['res.country'].sudo().search([('code', '=', 'CA')], limit=1)
+
+ current_year = fields.Date.today().year
+ birth_years = list(range(current_year - 14, current_year - 100, -1))
+
+ return {
+ 'tiers': tiers,
+ 'selected_tier': selected_tier,
+ 'countries': countries,
+ 'default_country_id': ca_country.id if ca_country else False,
+ 'birth_months': MONTH_OPTIONS,
+ 'birth_years': birth_years,
+ 'errors': errors or [],
+ 'form_data': form_data or {},
+ }
+
+ @http.route(['/membership/join'], type='http', auth='public', methods=['GET', 'POST'], website=True, sitemap=True)
+ def membership_join(self, **post):
+ if request.httprequest.method == 'GET':
+ tier_code = post.get('tier') or 'FAM'
+ values = self._get_form_context(selected_code=tier_code)
+ return request.render('tncsc_deployment.page_membership_join', values)
+
+ # Handle POST submission
+ errors = []
+ tier_id = post.get('tier_id')
+ username = (post.get('username') or '').strip()
+ email = (post.get('email') or '').strip().lower()
+ password = post.get('password') or ''
+ confirm_password = post.get('confirm_password') or ''
+ first_name = (post.get('first_name') or '').strip()
+ last_name = (post.get('last_name') or '').strip()
+ phone = (post.get('phone') or '').strip()
+ street = (post.get('street') or '').strip()
+ city = (post.get('city') or '').strip()
+ zip_code = (post.get('zip') or '').strip()
+ country_id = post.get('country_id')
+ birth_month = post.get('birth_month') or ''
+ birth_year = post.get('birth_year') or ''
+ terms_accepted = bool(post.get('membership_terms_accepted'))
+ coupon_code = (post.get('coupon_code') or '').strip()
+
+ tier = None
+ if tier_id:
+ tier = request.env['community.membership.tier'].sudo().browse(int(tier_id))
+ if not tier.exists():
+ tier = None
+
+ if not tier:
+ errors.append('Please select a valid membership plan.')
+
+ if not username or len(username) < 3:
+ errors.append('Username must be at least 3 characters long.')
+ elif request.env['res.users'].sudo().search_count([('login', '=', username)]) > 0:
+ errors.append(f'The username "{username}" is already taken. Please choose another username.')
+
+ if not email or '@' not in email:
+ errors.append('Please provide a valid email address.')
+ else:
+ existing_user = request.env['res.users'].sudo().search_count([('email', '=', email)])
+ existing_partner = request.env['res.partner'].sudo().search_count([
+ ('email', '=', email),
+ ('membership_tier_id', '!=', False),
+ ])
+ if existing_user > 0 or existing_partner > 0:
+ errors.append(f'An account or membership application with email "{email}" already exists.')
+
+ if not password or len(password) < 6:
+ errors.append('Password must be at least 6 characters long.')
+ elif password != confirm_password:
+ errors.append('Passwords do not match.')
+
+ if not first_name:
+ errors.append('First Name is required.')
+ if not last_name:
+ errors.append('Last Name is required.')
+
+ if not terms_accepted:
+ errors.append('You must accept the TNCSC Constitution and Membership Terms to proceed.')
+
+ # Minimum age validation if tier requires it
+ today = date.today()
+ calculated_age = None
+ if birth_year and birth_year.isdigit():
+ b_year = int(birth_year)
+ b_month = int(birth_month) if birth_month and birth_month.isdigit() else 1
+ calculated_age = today.year - b_year
+ if today.month < b_month:
+ calculated_age -= 1
+
+ if tier and tier.min_age and calculated_age < tier.min_age:
+ errors.append(
+ f'The {tier.name} plan requires a minimum age of {tier.min_age} years '
+ f'(calculated age: {calculated_age}).'
+ )
+
+ # File uploads
+ avatar_file = request.httprequest.files.get('avatar_file')
+ proof_file = request.httprequest.files.get('proof_file')
+
+ if tier and tier.requires_proof and not proof_file:
+ # Alert requirement if mandatory
+ pass
+
+ if errors:
+ values = self._get_form_context(
+ selected_code=tier.code if tier else post.get('tier_id'),
+ errors=errors,
+ form_data=post,
+ )
+ return request.render('tncsc_deployment.page_membership_join', values)
+
+ # Create Member Partner
+ partner_vals = {
+ 'first_name': first_name,
+ 'last_name': last_name,
+ 'name': f"{first_name} {last_name}".strip(),
+ 'email': email,
+ 'phone': phone,
+ 'street': street,
+ 'city': city,
+ 'zip': zip_code,
+ 'country_id': int(country_id) if country_id and country_id.isdigit() else False,
+ 'birth_month': str(birth_month) if birth_month else False,
+ 'birth_year': int(birth_year) if birth_year and birth_year.isdigit() else False,
+ 'membership_tier_id': tier.id,
+ 'membership_approval_state': 'pending',
+ 'membership_terms_accepted': True,
+ 'company_id': request.env.company.id,
+ }
+
+ # TNCSC Eligibility Answers
+ for k in ELIGIBILITY_KEYS:
+ ans = post.get(k)
+ if ans in ('yes', 'no'):
+ partner_vals[k] = ans
+
+ if avatar_file and avatar_file.filename:
+ try:
+ partner_vals['image_1920'] = base64.b64encode(avatar_file.read())
+ except Exception as e:
+ _logger.warning("Failed to read uploaded avatar: %s", e)
+
+ if proof_file and proof_file.filename:
+ try:
+ partner_vals['membership_proof_document'] = base64.b64encode(proof_file.read())
+ partner_vals['membership_proof_filename'] = proof_file.filename
+ except Exception as e:
+ _logger.warning("Failed to read proof document: %s", e)
+
+ partner = request.env['res.partner'].sudo().create(partner_vals)
+
+ # Create Portal User
+ try:
+ portal_group = request.env.ref('base.group_portal')
+ user_vals = {
+ 'name': partner.name,
+ 'login': username,
+ 'email': email,
+ 'partner_id': partner.id,
+ 'groups_id': [(6, 0, [portal_group.id])],
+ }
+ user = request.env['res.users'].sudo().create(user_vals)
+ user.sudo().password = password
+ except Exception as e:
+ _logger.exception("Failed to create portal user for new member: %s", e)
+
+ # Process Family Members if applicable
+ if tier.allows_family_members:
+ fam_first_names = request.httprequest.form.getlist('family_first_name[]')
+ fam_last_names = request.httprequest.form.getlist('family_last_name[]')
+ fam_emails = request.httprequest.form.getlist('family_email[]')
+ fam_phones = request.httprequest.form.getlist('family_phone[]')
+
+ for idx in range(len(fam_first_names)):
+ f_first = (fam_first_names[idx] or '').strip()
+ f_last = (fam_last_names[idx] or '').strip()
+ if f_first or f_last:
+ f_email = (fam_emails[idx] or '').strip() if idx < len(fam_emails) else ''
+ f_phone = (fam_phones[idx] or '').strip() if idx < len(fam_phones) else ''
+ request.env['res.partner'].sudo().create({
+ 'first_name': f_first,
+ 'last_name': f_last,
+ 'name': f"{f_first} {f_last}".strip(),
+ 'email': f_email or False,
+ 'phone': f_phone or False,
+ 'family_head_id': partner.id,
+ 'company_id': request.env.company.id,
+ })
+
+ # Post signup note on partner chatter
+ coupon_note = f"
Coupon Applied: {coupon_code}" if coupon_code else ""
+ partner.sudo().message_post(
+ body=(
+ f"
Online Membership Application Submitted"
+ f"
Tier: {tier.name} ({tier.code}) - ${tier.price}
"
+ f"
Username: {username}
"
+ f"
Status: Pending Board Approval"
+ f"{coupon_note}"
+ ),
+ subtype_xmlid='mail.mt_note',
+ )
+
+ # Send application received confirmation email
+ try:
+ template = request.env.ref('community_membership.mail_template_membership_application_received', raise_if_not_found=False)
+ if template:
+ template.sudo().send_mail(partner.id, force_send=True)
+ except Exception as e:
+ _logger.warning("Failed to send application confirmation email: %s", e)
+
+ return request.render('tncsc_deployment.page_membership_thank_you', {
+ 'partner': partner,
+ 'tier': tier,
+ 'username': username,
+ })
+
+ @http.route('/membership/thank-you', type='http', auth='public', website=True)
+ def membership_thank_you(self, **kw):
+ return request.render('tncsc_deployment.page_membership_thank_you', {})
diff --git a/addons/tncsc_deployment/data/mail_template_overrides.xml b/addons/tncsc_deployment/data/mail_template_overrides.xml
index 44e0993..5e8e70a 100644
--- a/addons/tncsc_deployment/data/mail_template_overrides.xml
+++ b/addons/tncsc_deployment/data/mail_template_overrides.xml
@@ -53,6 +53,63 @@
உங்களுக்கான புதுப்பித்தல் விலைப்பட்டியல் தயாரிக்கப்பட்டுள்ளது. உறுப்பினர் தகுதியை மீண்டும்
பெற எங்களைத் தொடர்பு கொள்ளவும்.
+
+
+
+
+
+
+
diff --git a/addons/tncsc_deployment/migrations/19.0.1.3.0/pre-migrate.py b/addons/tncsc_deployment/migrations/19.0.1.3.0/pre-migrate.py
new file mode 100644
index 0000000..354f5d0
--- /dev/null
+++ b/addons/tncsc_deployment/migrations/19.0.1.3.0/pre-migrate.py
@@ -0,0 +1,16 @@
+"""Reload the seeded website pages from views/website_pages.xml.
+
+That file is noupdate="1" so edits made in the website editor survive
+upgrades, but it also means upgrades never deliver template fixes or new pages
+(here: /membership/join, /membership/thank-you, dynamic event links, and captcha fixes).
+Loading it once in 'init' mode rewrites the existing page views.
+
+Runs before the module's data is loaded.
+"""
+from odoo import SUPERUSER_ID, api
+from odoo.tools import convert_file
+
+
+def migrate(cr, version):
+ env = api.Environment(cr, SUPERUSER_ID, {})
+ convert_file(env, 'tncsc_deployment', 'views/website_pages.xml', {}, mode='init', noupdate=True)
diff --git a/addons/tncsc_deployment/static/description/banner.png b/addons/tncsc_deployment/static/description/banner.png
new file mode 100644
index 0000000..f1d274c
Binary files /dev/null and b/addons/tncsc_deployment/static/description/banner.png differ
diff --git a/addons/tncsc_deployment/static/description/icon.png b/addons/tncsc_deployment/static/description/icon.png
new file mode 100644
index 0000000..b7da2f2
Binary files /dev/null and b/addons/tncsc_deployment/static/description/icon.png differ
diff --git a/addons/tncsc_deployment/static/src/scss/website_navigation.scss b/addons/tncsc_deployment/static/src/scss/website_navigation.scss
index 366d044..a3b9abc 100644
--- a/addons/tncsc_deployment/static/src/scss/website_navigation.scss
+++ b/addons/tncsc_deployment/static/src/scss/website_navigation.scss
@@ -5,28 +5,32 @@
display: none !important;
}
-.tncsc-hero-nav .tncsc-member-login {
+.tncsc-member-login {
display: inline-flex;
align-items: center;
justify-content: center;
gap: 8px;
- padding: 8px 12px;
- border: 1px solid #D89C43;
- border-radius: 6px;
- background: #FFFCF7;
- color: #87530D;
+ padding: 7px 16px;
+ border: 1px solid #d19b33 !important;
+ border-radius: 20px !important;
+ background: linear-gradient(135deg, #e5a93c 0%, #d19b33 100%) !important;
+ color: #120735 !important;
font-size: 14px;
- font-weight: 600;
- text-decoration: none;
+ font-weight: 700 !important;
+ text-decoration: none !important;
white-space: nowrap;
+ box-shadow: 0 2px 8px rgba(229, 169, 60, 0.3);
+ transition: all 0.2s ease;
&:hover {
- background: #FFF0D6;
- color: #643D09;
+ background: linear-gradient(135deg, #f3c669 0%, #e5a93c 100%) !important;
+ color: #120735 !important;
+ transform: translateY(-1px);
+ box-shadow: 0 4px 12px rgba(229, 169, 60, 0.45);
}
&:focus-visible {
- outline: 3px solid #D89C43;
+ outline: 3px solid #e5a93c;
outline-offset: 3px;
}
}
diff --git a/addons/tncsc_deployment/tests/test_tncsc_deployment.py b/addons/tncsc_deployment/tests/test_tncsc_deployment.py
index bb8688b..ae75cc8 100644
--- a/addons/tncsc_deployment/tests/test_tncsc_deployment.py
+++ b/addons/tncsc_deployment/tests/test_tncsc_deployment.py
@@ -106,3 +106,46 @@ class TestTncscDeployment(TransactionCase):
self.env.ref('community_interac.group_interac_verifier'),
treasurer.implied_ids,
)
+
+ def test_membership_join_pages_published(self):
+ urls = ('/membership/join', '/membership/thank-you')
+ for url in urls:
+ page = self.env['website.page'].search([('url', '=', url)], limit=1)
+ self.assertTrue(page, f"Expected a website.page for {url}")
+ self.assertTrue(page.is_published)
+
+ def test_membership_approval_workflow_end_to_end(self):
+ tier = self.env['community.membership.tier'].search([('code', '=', 'FAM')], limit=1)
+ partner = self.env['res.partner'].create({
+ 'first_name': 'Meena',
+ 'last_name': 'Sundaram',
+ 'email': 'meena.test@example.com',
+ 'membership_tier_id': tier.id,
+ 'membership_approval_state': 'pending',
+ })
+ self.assertEqual(partner.membership_approval_state, 'pending')
+ self.assertEqual(partner.membership_state, 'none')
+
+ # Approve
+ partner.action_approve_membership()
+ self.assertEqual(partner.membership_approval_state, 'approved')
+ self.assertEqual(partner.membership_state, 'active')
+ self.assertTrue(partner.membership_start)
+ self.assertTrue(partner.membership_expiry)
+ self.assertTrue(partner.membership_member_id)
+
+ # Reject workflow on another partner
+ partner_rej = self.env['res.partner'].create({
+ 'first_name': 'Raj',
+ 'last_name': 'Kumar',
+ 'email': 'raj.test@example.com',
+ 'membership_tier_id': tier.id,
+ 'membership_approval_state': 'pending',
+ })
+ partner_rej.action_reject_membership()
+ self.assertEqual(partner_rej.membership_approval_state, 'rejected')
+ self.assertEqual(partner_rej.membership_state, 'none')
+
+ def test_base_url_configured(self):
+ base_url = self.env['ir.config_parameter'].sudo().get_param('web.base.url')
+ self.assertEqual(base_url, 'https://tncsc.odoo.thedomainnest.com')
diff --git a/addons/tncsc_deployment/views/browser_branding.xml b/addons/tncsc_deployment/views/browser_branding.xml
index 716743b..b370e62 100644
--- a/addons/tncsc_deployment/views/browser_branding.xml
+++ b/addons/tncsc_deployment/views/browser_branding.xml
@@ -10,9 +10,12 @@
'/membership': 'Membership | TNCSC',
'/membership/benefits': 'Membership Benefits | TNCSC',
'/membership/registration': 'Membership Registration | TNCSC',
+ '/membership/join': 'Join Membership | TNCSC',
'/event': 'Events | TNCSC',
- '/product/tncsc-summer-picnic-2026': 'Summer Picnic 2026 | TNCSC',
'/tamil-school': 'Tamil School | TNCSC',
+ '/school/register': 'Tamil School Registration | TNCSC',
+ '/school/register/student': 'Tamil School Registration - Student | TNCSC',
+ '/school/register/class': 'Tamil School Registration - Class | TNCSC',
'/sponsors': 'Sponsors | TNCSC',
'/contact': 'Contact Us | TNCSC',
'/my': 'Member Portal | TNCSC',
diff --git a/addons/tncsc_deployment/views/website_header_overrides.xml b/addons/tncsc_deployment/views/website_header_overrides.xml
index cd05db0..cd04956 100644
--- a/addons/tncsc_deployment/views/website_header_overrides.xml
+++ b/addons/tncsc_deployment/views/website_header_overrides.xml
@@ -1,7 +1,7 @@