# -*- 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', {})