add TNCSC deployment module, community add-ons, role guides, and website navigation overrides
BIN
addons/community_benefits/static/description/banner.png
Normal file
|
After Width: | Height: | Size: 19 KiB |
BIN
addons/community_benefits/static/description/icon.png
Normal file
|
After Width: | Height: | Size: 1.7 KiB |
BIN
addons/community_classifieds/static/description/banner.png
Normal file
|
After Width: | Height: | Size: 19 KiB |
BIN
addons/community_classifieds/static/description/icon.png
Normal file
|
After Width: | Height: | Size: 1.7 KiB |
BIN
addons/community_interac/static/description/banner.png
Normal file
|
After Width: | Height: | Size: 19 KiB |
BIN
addons/community_interac/static/description/icon.png
Normal file
|
After Width: | Height: | Size: 1.7 KiB |
@ -65,5 +65,79 @@
|
|||||||
</field>
|
</field>
|
||||||
</record>
|
</record>
|
||||||
|
|
||||||
|
<record id="mail_template_membership_application_received" model="mail.template">
|
||||||
|
<field name="name">Membership: Application Received</field>
|
||||||
|
<field name="model_id" ref="base.model_res_partner"/>
|
||||||
|
<field name="subject">Your {{ object.membership_org_name or 'organization' }} membership application has been received</field>
|
||||||
|
<field name="partner_to">{{ object.id }}</field>
|
||||||
|
<field name="lang">{{ object.lang }}</field>
|
||||||
|
<field name="auto_delete" eval="True"/>
|
||||||
|
<field name="body_html" type="html">
|
||||||
|
<div style="margin: 0px; padding: 0px; font-size: 13px;">
|
||||||
|
<p>Dear <t t-out="object.name or ''">Applicant</t>,</p>
|
||||||
|
<p>
|
||||||
|
Thank you for applying for membership with <t t-out="object.membership_org_name or 'our organization'"/>!
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
Selected Plan: <strong><t t-out="object.membership_tier_id.name or 'Membership'"/></strong>
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
Your application is currently under review by the Board of Directors. You will be notified as soon as a decision is made.
|
||||||
|
</p>
|
||||||
|
<p>Thank you for your interest and support!</p>
|
||||||
|
</div>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<record id="mail_template_membership_approved" model="mail.template">
|
||||||
|
<field name="name">Membership: Application Approved</field>
|
||||||
|
<field name="model_id" ref="base.model_res_partner"/>
|
||||||
|
<field name="subject">Welcome! Your {{ object.membership_org_name or 'organization' }} membership has been approved</field>
|
||||||
|
<field name="partner_to">{{ object.id }}</field>
|
||||||
|
<field name="lang">{{ object.lang }}</field>
|
||||||
|
<field name="auto_delete" eval="True"/>
|
||||||
|
<field name="body_html" type="html">
|
||||||
|
<div style="margin: 0px; padding: 0px; font-size: 13px;">
|
||||||
|
<p>Dear <t t-out="object.name or ''">Member</t>,</p>
|
||||||
|
<p>
|
||||||
|
We are pleased to inform you that your membership application for
|
||||||
|
<strong><t t-out="object.membership_tier_id.name or 'Membership'"/></strong>
|
||||||
|
with <t t-out="object.membership_org_name or 'our organization'"/> has been approved by the Board of Directors!
|
||||||
|
</p>
|
||||||
|
<t t-if="object.membership_member_id">
|
||||||
|
<p>Your Member ID: <strong><t t-out="object.membership_member_id"/></strong></p>
|
||||||
|
</t>
|
||||||
|
<p>You can access your member portal and download your digital membership card by logging in at our website.</p>
|
||||||
|
<p>Welcome to our community!</p>
|
||||||
|
</div>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<record id="mail_template_membership_rejected" model="mail.template">
|
||||||
|
<field name="name">Membership: Application Rejected</field>
|
||||||
|
<field name="model_id" ref="base.model_res_partner"/>
|
||||||
|
<field name="subject">Update on your {{ object.membership_org_name or 'organization' }} membership application</field>
|
||||||
|
<field name="partner_to">{{ object.id }}</field>
|
||||||
|
<field name="lang">{{ object.lang }}</field>
|
||||||
|
<field name="auto_delete" eval="True"/>
|
||||||
|
<field name="body_html" type="html">
|
||||||
|
<div style="margin: 0px; padding: 0px; font-size: 13px;">
|
||||||
|
<p>Dear <t t-out="object.name or ''">Applicant</t>,</p>
|
||||||
|
<p>
|
||||||
|
Thank you for your interest in joining <t t-out="object.membership_org_name or 'our organization'"/>.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
After careful review, the Board of Directors was unable to approve your application at this time.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
If any fees were paid during application submission, a full refund will be processed promptly.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
If you have questions or believe this was in error, please contact our support team.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
</data>
|
</data>
|
||||||
</odoo>
|
</odoo>
|
||||||
|
|||||||
@ -88,14 +88,16 @@ class CommunityMembershipTier(models.Model):
|
|||||||
|
|
||||||
def _membership_product_values(self):
|
def _membership_product_values(self):
|
||||||
self.ensure_one()
|
self.ensure_one()
|
||||||
return {
|
vals = {
|
||||||
'name': self.name,
|
'name': self.name,
|
||||||
'list_price': self.price,
|
'list_price': self.price,
|
||||||
'type': 'service',
|
'type': 'service',
|
||||||
'sale_ok': True,
|
'sale_ok': True,
|
||||||
'purchase_ok': False,
|
'purchase_ok': False,
|
||||||
'invoice_policy': 'order',
|
|
||||||
}
|
}
|
||||||
|
if 'invoice_policy' in self.env['product.product']._fields:
|
||||||
|
vals['invoice_policy'] = 'order'
|
||||||
|
return vals
|
||||||
|
|
||||||
def _create_membership_product(self):
|
def _create_membership_product(self):
|
||||||
self.ensure_one()
|
self.ensure_one()
|
||||||
|
|||||||
@ -128,10 +128,31 @@ class ResPartner(models.Model):
|
|||||||
|
|
||||||
def action_approve_membership(self):
|
def action_approve_membership(self):
|
||||||
self.write({'membership_approval_state': 'approved'})
|
self.write({'membership_approval_state': 'approved'})
|
||||||
|
template = self.env.ref('community_membership.mail_template_membership_approved', raise_if_not_found=False)
|
||||||
|
for partner in self:
|
||||||
|
if partner.membership_state in ('none', 'invoiced') and partner.membership_tier_id:
|
||||||
|
partner.action_activate_membership()
|
||||||
|
if template and partner.email:
|
||||||
|
template.send_mail(partner.id, force_send=True)
|
||||||
|
if hasattr(partner, 'message_post'):
|
||||||
|
partner.message_post(body=_("Membership application approved by the Board of Directors."))
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def action_reject_membership(self):
|
def action_reject_membership(self):
|
||||||
self.write({'membership_approval_state': 'rejected'})
|
self.write({'membership_approval_state': 'rejected'})
|
||||||
|
template = self.env.ref('community_membership.mail_template_membership_rejected', raise_if_not_found=False)
|
||||||
|
for partner in self:
|
||||||
|
draft_invoices = self.env['account.move'].sudo().search([
|
||||||
|
('partner_id', '=', partner.id),
|
||||||
|
('move_type', '=', 'out_invoice'),
|
||||||
|
('state', '=', 'draft'),
|
||||||
|
])
|
||||||
|
if draft_invoices:
|
||||||
|
draft_invoices.button_cancel()
|
||||||
|
if template and partner.email:
|
||||||
|
template.send_mail(partner.id, force_send=True)
|
||||||
|
if hasattr(partner, 'message_post'):
|
||||||
|
partner.message_post(body=_("Membership application rejected by the Board of Directors."))
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def _compute_membership_org_name(self):
|
def _compute_membership_org_name(self):
|
||||||
|
|||||||
BIN
addons/community_membership/static/description/banner.png
Normal file
|
After Width: | Height: | Size: 19 KiB |
BIN
addons/community_membership/static/description/icon.png
Normal file
|
After Width: | Height: | Size: 1.7 KiB |
BIN
addons/community_portal/static/description/banner.png
Normal file
|
After Width: | Height: | Size: 19 KiB |
BIN
addons/community_portal/static/description/icon.png
Normal file
|
After Width: | Height: | Size: 1.6 KiB |
@ -14,7 +14,7 @@ class TestPortalDashboard(HttpCase):
|
|||||||
|
|
||||||
def test_portal_home_renders_without_error(self):
|
def test_portal_home_renders_without_error(self):
|
||||||
self.authenticate('admin', 'admin')
|
self.authenticate('admin', 'admin')
|
||||||
response = self.url_open('/my')
|
response = self.url_open('/my', timeout=30)
|
||||||
self.assertEqual(response.status_code, 200)
|
self.assertEqual(response.status_code, 200)
|
||||||
|
|
||||||
def test_dashboard_only_shows_cards_for_installed_modules(self):
|
def test_dashboard_only_shows_cards_for_installed_modules(self):
|
||||||
@ -25,7 +25,7 @@ class TestPortalDashboard(HttpCase):
|
|||||||
]).mapped('name'))
|
]).mapped('name'))
|
||||||
|
|
||||||
self.authenticate('admin', 'admin')
|
self.authenticate('admin', 'admin')
|
||||||
response = self.url_open('/my')
|
response = self.url_open('/my', timeout=30)
|
||||||
self.assertEqual(response.status_code, 200)
|
self.assertEqual(response.status_code, 200)
|
||||||
content = response.content.decode()
|
content = response.content.decode()
|
||||||
|
|
||||||
|
|||||||
@ -64,14 +64,16 @@ class CommunitySchoolClass(models.Model):
|
|||||||
self.ensure_one()
|
self.ensure_one()
|
||||||
if self.fee_product_id:
|
if self.fee_product_id:
|
||||||
return self.fee_product_id
|
return self.fee_product_id
|
||||||
product = self.env['product.product'].create({
|
vals = {
|
||||||
'name': f"School Fee - {self.name}",
|
'name': f"School Fee - {self.name}",
|
||||||
'list_price': self.fee,
|
'list_price': self.fee,
|
||||||
'type': 'service',
|
'type': 'service',
|
||||||
'sale_ok': True,
|
'sale_ok': True,
|
||||||
'purchase_ok': False,
|
'purchase_ok': False,
|
||||||
'invoice_policy': 'order',
|
}
|
||||||
})
|
if 'invoice_policy' in self.env['product.product']._fields:
|
||||||
|
vals['invoice_policy'] = 'order'
|
||||||
|
product = self.env['product.product'].create(vals)
|
||||||
self.fee_product_id = product.id
|
self.fee_product_id = product.id
|
||||||
return product
|
return product
|
||||||
|
|
||||||
|
|||||||
BIN
addons/community_school/static/description/banner.png
Normal file
|
After Width: | Height: | Size: 19 KiB |
BIN
addons/community_school/static/description/icon.png
Normal file
|
After Width: | Height: | Size: 1.8 KiB |
@ -1,100 +1,539 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<odoo>
|
<odoo>
|
||||||
|
<!-- Shared Stepper Template -->
|
||||||
|
<template id="school_registration_stepper" name="School Registration Stepper">
|
||||||
|
<div class="school-stepper mb-4">
|
||||||
|
<div t-attf-class="step-item #{'active' if step == 1 else ('completed' if step > 1 else '')}">
|
||||||
|
<div class="step-circle">
|
||||||
|
<t t-if="step > 1"><i class="fa fa-check"></i></t>
|
||||||
|
<t t-else="">1</t>
|
||||||
|
</div>
|
||||||
|
<div class="step-label">Parent / Guardian</div>
|
||||||
|
</div>
|
||||||
|
<div t-attf-class="step-item #{'active' if step == 2 else ('completed' if step > 2 else '')}">
|
||||||
|
<div class="step-circle">
|
||||||
|
<t t-if="step > 2"><i class="fa fa-check"></i></t>
|
||||||
|
<t t-else="">2</t>
|
||||||
|
</div>
|
||||||
|
<div class="step-label">Student Details</div>
|
||||||
|
</div>
|
||||||
|
<div t-attf-class="step-item #{'active' if step == 3 else ('completed' if step > 3 else '')}">
|
||||||
|
<div class="step-circle">
|
||||||
|
<t t-if="step > 3"><i class="fa fa-check"></i></t>
|
||||||
|
<t t-else="">3</t>
|
||||||
|
</div>
|
||||||
|
<div class="step-label">Class Selection</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<!-- Shared Styles -->
|
||||||
|
<template id="school_registration_styles" name="School Registration Styles">
|
||||||
|
<style>
|
||||||
|
.school-reg-wrap {
|
||||||
|
background-color: #f7f8fc;
|
||||||
|
min-height: 85vh;
|
||||||
|
padding-bottom: 80px;
|
||||||
|
font-family: var(--community-font, 'Outfit', 'Inter', sans-serif);
|
||||||
|
}
|
||||||
|
.school-reg-hero {
|
||||||
|
background: linear-gradient(135deg, #120735 0%, #1e1048 50%, #2a165f 100%);
|
||||||
|
color: #ffffff;
|
||||||
|
padding: 50px 20px 65px 20px;
|
||||||
|
text-align: center;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
.school-reg-hero::after {
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
height: 4px;
|
||||||
|
background: linear-gradient(90deg, #e5a93c, #f3c669, #e5a93c);
|
||||||
|
}
|
||||||
|
.school-reg-card {
|
||||||
|
background: #ffffff;
|
||||||
|
border-radius: 18px;
|
||||||
|
border: 1px solid #e2e8f0;
|
||||||
|
box-shadow: 0 15px 40px rgba(18, 7, 53, 0.08);
|
||||||
|
padding: 42px 48px 48px 48px;
|
||||||
|
margin-top: -35px;
|
||||||
|
margin-bottom: 60px;
|
||||||
|
position: relative;
|
||||||
|
z-index: 10;
|
||||||
|
max-width: 640px;
|
||||||
|
margin-left: auto;
|
||||||
|
margin-right: auto;
|
||||||
|
}
|
||||||
|
@media (max-width: 576px) {
|
||||||
|
.school-reg-card {
|
||||||
|
padding: 28px 20px 32px 20px;
|
||||||
|
margin-top: -25px;
|
||||||
|
}
|
||||||
|
.btn-school-next, .btn-school-back {
|
||||||
|
width: 100%;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/* Stepper */
|
||||||
|
.school-stepper {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
position: relative;
|
||||||
|
margin-bottom: 32px;
|
||||||
|
}
|
||||||
|
.school-stepper::before {
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
top: 21px;
|
||||||
|
left: 40px;
|
||||||
|
right: 40px;
|
||||||
|
height: 3px;
|
||||||
|
background: #e2e8f0;
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
.step-item {
|
||||||
|
position: relative;
|
||||||
|
z-index: 2;
|
||||||
|
text-align: center;
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
.step-circle {
|
||||||
|
width: 42px;
|
||||||
|
height: 42px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: #ffffff;
|
||||||
|
border: 3px solid #cbd5e1;
|
||||||
|
color: #64748b;
|
||||||
|
font-weight: 700;
|
||||||
|
font-size: 1rem;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
margin: 0 auto 8px auto;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
.step-item.active .step-circle {
|
||||||
|
border-color: #e5a93c;
|
||||||
|
background: #120735;
|
||||||
|
color: #e5a93c;
|
||||||
|
box-shadow: 0 0 0 4px rgba(229, 169, 60, 0.2);
|
||||||
|
}
|
||||||
|
.step-item.completed .step-circle {
|
||||||
|
border-color: #10b981;
|
||||||
|
background: #10b981;
|
||||||
|
color: #ffffff;
|
||||||
|
}
|
||||||
|
.step-label {
|
||||||
|
font-size: 0.8rem;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #64748b;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.5px;
|
||||||
|
}
|
||||||
|
.step-item.active .step-label {
|
||||||
|
color: #120735;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
.school-section-title {
|
||||||
|
font-size: 1.25rem;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #120735;
|
||||||
|
border-bottom: 2px solid #edf2f7;
|
||||||
|
padding-bottom: 12px;
|
||||||
|
margin-bottom: 24px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
.form-label {
|
||||||
|
font-weight: 600;
|
||||||
|
color: #1e293b;
|
||||||
|
font-size: 0.95rem;
|
||||||
|
margin-bottom: 6px;
|
||||||
|
}
|
||||||
|
.input-group-text {
|
||||||
|
background-color: #f8fafc;
|
||||||
|
border-color: #cbd5e1;
|
||||||
|
color: #64748b;
|
||||||
|
padding-left: 14px;
|
||||||
|
padding-right: 14px;
|
||||||
|
border-top-left-radius: 8px;
|
||||||
|
border-bottom-left-radius: 8px;
|
||||||
|
}
|
||||||
|
.form-control, .form-select {
|
||||||
|
border-color: #cbd5e1;
|
||||||
|
padding: 11px 14px;
|
||||||
|
border-radius: 8px;
|
||||||
|
font-size: 0.95rem;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
}
|
||||||
|
.form-control:focus, .form-select:focus {
|
||||||
|
border-color: #e5a93c;
|
||||||
|
box-shadow: 0 0 0 3px rgba(229, 169, 60, 0.2);
|
||||||
|
}
|
||||||
|
.btn-school-next {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
background: linear-gradient(135deg, #e5a93c 0%, #d19b33 100%);
|
||||||
|
color: #120735;
|
||||||
|
font-weight: 700;
|
||||||
|
font-size: 1.05rem;
|
||||||
|
padding: 13px 38px;
|
||||||
|
border-radius: 30px;
|
||||||
|
border: none;
|
||||||
|
box-shadow: 0 6px 18px rgba(229, 169, 60, 0.35);
|
||||||
|
transition: all 0.25s ease;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
.btn-school-next:hover {
|
||||||
|
background: linear-gradient(135deg, #f3c669 0%, #e5a93c 100%);
|
||||||
|
transform: translateY(-2px);
|
||||||
|
box-shadow: 0 10px 22px rgba(229, 169, 60, 0.45);
|
||||||
|
color: #120735;
|
||||||
|
}
|
||||||
|
.btn-school-back {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
border-radius: 30px;
|
||||||
|
padding: 12px 26px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #475569;
|
||||||
|
border: 1px solid #cbd5e1;
|
||||||
|
background: #ffffff;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
.btn-school-back:hover {
|
||||||
|
background: #f1f5f9;
|
||||||
|
color: #0f172a;
|
||||||
|
}
|
||||||
|
.class-card {
|
||||||
|
border: 2px solid #e2e8f0;
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 16px 20px;
|
||||||
|
margin-bottom: 14px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
background: #ffffff;
|
||||||
|
}
|
||||||
|
.class-card:hover {
|
||||||
|
border-color: #e5a93c;
|
||||||
|
background-color: #fefcf7;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<!-- STEP 1: PARENT / GUARDIAN -->
|
||||||
<template id="registration_step_parent" name="School Registration: Parent">
|
<template id="registration_step_parent" name="School Registration: Parent">
|
||||||
<t t-call="website.layout">
|
<t t-call="website.layout">
|
||||||
<div class="container" style="max-width: 480px; margin-top: 24px; margin-bottom: 60px;">
|
<t t-call="community_school.school_registration_styles"/>
|
||||||
<h2>Register - Step 1 of 3: Parent/Guardian</h2>
|
<div id="wrap" class="school-reg-wrap">
|
||||||
<form method="POST" t-attf-action="/school/register">
|
<!-- Hero Header -->
|
||||||
<input type="hidden" name="csrf_token" t-att-value="request.csrf_token()"/>
|
<div class="school-reg-hero">
|
||||||
<div class="mb-3">
|
<div class="container">
|
||||||
<label class="form-label">Full Name</label>
|
<span class="badge px-3 py-2 rounded-pill fw-bold mb-3" style="background-color: #e5a93c; color: #120735;">
|
||||||
<input type="text" class="form-control" name="name" required="required"/>
|
LANGUAGE SCHOOL ADMISSIONS
|
||||||
|
</span>
|
||||||
|
<h1 class="display-5 fw-bold text-white mb-2">School Student Registration</h1>
|
||||||
|
<p class="fs-5 mb-0" style="color: #c9c3ec;">Step 1 of 3: Parent or Guardian Information</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="mb-3">
|
</div>
|
||||||
<label class="form-label">Email</label>
|
|
||||||
<input type="email" class="form-control" name="email" required="required"/>
|
<!-- Registration Card -->
|
||||||
|
<div class="container">
|
||||||
|
<div class="school-reg-card">
|
||||||
|
<t t-call="community_school.school_registration_stepper">
|
||||||
|
<t t-set="step" t-value="1"/>
|
||||||
|
</t>
|
||||||
|
|
||||||
|
<div class="school-section-title">
|
||||||
|
<i class="fa fa-user-circle text-warning"></i>
|
||||||
|
<span>Parent / Guardian Information</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<form method="POST" t-attf-action="/school/register">
|
||||||
|
<input type="hidden" name="csrf_token" t-att-value="request.csrf_token()"/>
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label">Full Name <span class="text-danger">*</span></label>
|
||||||
|
<div class="input-group">
|
||||||
|
<span class="input-group-text"><i class="fa fa-user"></i></span>
|
||||||
|
<input type="text" class="form-control" name="name" required="required" placeholder="e.g. Anand Natarajan"/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label">Email Address <span class="text-danger">*</span></label>
|
||||||
|
<div class="input-group">
|
||||||
|
<span class="input-group-text"><i class="fa fa-envelope"></i></span>
|
||||||
|
<input type="email" class="form-control" name="email" required="required" placeholder="e.g. anand@example.com"/>
|
||||||
|
</div>
|
||||||
|
<small class="text-muted">Enrollment notices, invoices, and class reminders will be sent to this email.</small>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-4">
|
||||||
|
<label class="form-label">Phone Number</label>
|
||||||
|
<div class="input-group">
|
||||||
|
<span class="input-group-text"><i class="fa fa-phone"></i></span>
|
||||||
|
<input type="tel" class="form-control" name="phone" placeholder="e.g. +1 416-555-0199"/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="d-flex justify-content-end align-items-center pt-3 pb-2">
|
||||||
|
<button type="submit" class="btn btn-school-next">
|
||||||
|
Continue to Student Info <i class="fa fa-arrow-right ms-2"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
</div>
|
</div>
|
||||||
<div class="mb-3">
|
</div>
|
||||||
<label class="form-label">Phone</label>
|
|
||||||
<input type="text" class="form-control" name="phone"/>
|
|
||||||
</div>
|
|
||||||
<button type="submit" class="btn btn-primary">Continue</button>
|
|
||||||
</form>
|
|
||||||
</div>
|
</div>
|
||||||
</t>
|
</t>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
<!-- STEP 2: STUDENT -->
|
||||||
<template id="registration_step_student" name="School Registration: Student">
|
<template id="registration_step_student" name="School Registration: Student">
|
||||||
<t t-call="website.layout">
|
<t t-call="website.layout">
|
||||||
<div class="container" style="max-width: 480px; margin-top: 24px; margin-bottom: 60px;">
|
<t t-call="community_school.school_registration_styles"/>
|
||||||
<h2>Register - Step 2 of 3: Student</h2>
|
<div id="wrap" class="school-reg-wrap">
|
||||||
<form method="POST" t-attf-action="/school/register/student">
|
<!-- Hero Header -->
|
||||||
<input type="hidden" name="csrf_token" t-att-value="request.csrf_token()"/>
|
<div class="school-reg-hero">
|
||||||
<div class="mb-3">
|
<div class="container">
|
||||||
<label class="form-label">Student Full Name</label>
|
<span class="badge px-3 py-2 rounded-pill fw-bold mb-3" style="background-color: #e5a93c; color: #120735;">
|
||||||
<input type="text" class="form-control" name="name" required="required"/>
|
LANGUAGE SCHOOL ADMISSIONS
|
||||||
|
</span>
|
||||||
|
<h1 class="display-5 fw-bold text-white mb-2">School Student Registration</h1>
|
||||||
|
<p class="fs-5 mb-0" style="color: #c9c3ec;">Step 2 of 3: Student Details & Emergency Contact</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="mb-3">
|
</div>
|
||||||
<label class="form-label">Date of Birth</label>
|
|
||||||
<input type="date" class="form-control" name="date_of_birth"/>
|
<!-- Registration Card -->
|
||||||
|
<div class="container">
|
||||||
|
<div class="school-reg-card">
|
||||||
|
<t t-call="community_school.school_registration_stepper">
|
||||||
|
<t t-set="step" t-value="2"/>
|
||||||
|
</t>
|
||||||
|
|
||||||
|
<div class="school-section-title">
|
||||||
|
<i class="fa fa-graduation-cap text-warning"></i>
|
||||||
|
<span>Student Information</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<form method="POST" t-attf-action="/school/register/student">
|
||||||
|
<input type="hidden" name="csrf_token" t-att-value="request.csrf_token()"/>
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label">Student Full Name <span class="text-danger">*</span></label>
|
||||||
|
<div class="input-group">
|
||||||
|
<span class="input-group-text"><i class="fa fa-id-card"></i></span>
|
||||||
|
<input type="text" class="form-control" name="name" required="required" placeholder="e.g. Kavin Anand"/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label">Date of Birth</label>
|
||||||
|
<div class="input-group">
|
||||||
|
<span class="input-group-text"><i class="fa fa-calendar"></i></span>
|
||||||
|
<input type="date" class="form-control" name="date_of_birth"/>
|
||||||
|
</div>
|
||||||
|
<small class="text-muted">Used to match the student to their appropriate age or grade level.</small>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label">Health & Medical Notes</label>
|
||||||
|
<div class="input-group">
|
||||||
|
<span class="input-group-text"><i class="fa fa-medkit"></i></span>
|
||||||
|
<textarea class="form-control" name="health_notes" rows="2" placeholder="Allergies, medical conditions, or special requirements (optional)"/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="school-section-title mt-4">
|
||||||
|
<i class="fa fa-shield text-warning"></i>
|
||||||
|
<span>Emergency Contact</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label">Emergency Contact Name</label>
|
||||||
|
<div class="input-group">
|
||||||
|
<span class="input-group-text"><i class="fa fa-user-plus"></i></span>
|
||||||
|
<input type="text" class="form-control" name="emergency_contact_name" placeholder="Contact person if parent is unreachable"/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-4">
|
||||||
|
<label class="form-label">Emergency Contact Phone</label>
|
||||||
|
<div class="input-group">
|
||||||
|
<span class="input-group-text"><i class="fa fa-phone-square"></i></span>
|
||||||
|
<input type="tel" class="form-control" name="emergency_contact_phone" placeholder="e.g. +1 416-555-0188"/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="d-flex flex-wrap justify-content-between align-items-center gap-3 pt-3 pb-1">
|
||||||
|
<a href="/school/register" class="btn btn-school-back">
|
||||||
|
<i class="fa fa-arrow-left me-1"></i> Back
|
||||||
|
</a>
|
||||||
|
<button type="submit" class="btn btn-school-next">
|
||||||
|
Continue to Class Selection <i class="fa fa-arrow-right ms-2"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
</div>
|
</div>
|
||||||
<div class="mb-3">
|
</div>
|
||||||
<label class="form-label">Health Notes</label>
|
|
||||||
<textarea class="form-control" name="health_notes"/>
|
|
||||||
</div>
|
|
||||||
<div class="mb-3">
|
|
||||||
<label class="form-label">Emergency Contact Name</label>
|
|
||||||
<input type="text" class="form-control" name="emergency_contact_name"/>
|
|
||||||
</div>
|
|
||||||
<div class="mb-3">
|
|
||||||
<label class="form-label">Emergency Contact Phone</label>
|
|
||||||
<input type="text" class="form-control" name="emergency_contact_phone"/>
|
|
||||||
</div>
|
|
||||||
<button type="submit" class="btn btn-primary">Continue</button>
|
|
||||||
</form>
|
|
||||||
</div>
|
</div>
|
||||||
</t>
|
</t>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
<!-- STEP 3: CLASS SELECTION -->
|
||||||
<template id="registration_step_class" name="School Registration: Class">
|
<template id="registration_step_class" name="School Registration: Class">
|
||||||
<t t-call="website.layout">
|
<t t-call="website.layout">
|
||||||
<div class="container" style="max-width: 480px; margin-top: 24px; margin-bottom: 60px;">
|
<t t-call="community_school.school_registration_styles"/>
|
||||||
<h2>Register - Step 3 of 3: Class</h2>
|
<div id="wrap" class="school-reg-wrap">
|
||||||
<form method="POST" t-attf-action="/school/register/class">
|
<!-- Hero Header -->
|
||||||
<input type="hidden" name="csrf_token" t-att-value="request.csrf_token()"/>
|
<div class="school-reg-hero">
|
||||||
<div class="mb-3">
|
<div class="container">
|
||||||
<label class="form-label">Class</label>
|
<span class="badge px-3 py-2 rounded-pill fw-bold mb-3" style="background-color: #e5a93c; color: #120735;">
|
||||||
<select class="form-select" name="class_id" required="required">
|
LANGUAGE SCHOOL ADMISSIONS
|
||||||
<t t-foreach="classes" t-as="klass">
|
</span>
|
||||||
<option t-att-value="klass.id">
|
<h1 class="display-5 fw-bold text-white mb-2">School Student Registration</h1>
|
||||||
<t t-out="klass.name"/> (<t t-out="klass.enrolled_count"/>/<t t-out="klass.max_students"/> seats)
|
<p class="fs-5 mb-0" style="color: #c9c3ec;">Step 3 of 3: Select Class & Complete Registration</p>
|
||||||
<t t-if="klass.fee"> - <t t-out="klass.fee"/></t>
|
|
||||||
</option>
|
|
||||||
</t>
|
|
||||||
</select>
|
|
||||||
</div>
|
</div>
|
||||||
<button type="submit" class="btn btn-primary">Complete Registration</button>
|
</div>
|
||||||
</form>
|
|
||||||
|
<!-- Registration Card -->
|
||||||
|
<div class="container">
|
||||||
|
<div class="school-reg-card">
|
||||||
|
<t t-call="community_school.school_registration_stepper">
|
||||||
|
<t t-set="step" t-value="3"/>
|
||||||
|
</t>
|
||||||
|
|
||||||
|
<div class="school-section-title">
|
||||||
|
<i class="fa fa-book text-warning"></i>
|
||||||
|
<span>Select Class</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<form method="POST" t-attf-action="/school/register/class">
|
||||||
|
<input type="hidden" name="csrf_token" t-att-value="request.csrf_token()"/>
|
||||||
|
|
||||||
|
<div class="mb-4">
|
||||||
|
<label class="form-label d-block mb-3">Available Classes <span class="text-danger">*</span></label>
|
||||||
|
|
||||||
|
<t t-foreach="classes" t-as="klass">
|
||||||
|
<div class="class-card">
|
||||||
|
<div class="form-check d-flex justify-content-between align-items-center">
|
||||||
|
<div>
|
||||||
|
<input class="form-check-input me-2" type="radio" name="class_id"
|
||||||
|
t-att-value="klass.id" t-att-id="'class_' + str(klass.id)" required="required"/>
|
||||||
|
<label class="form-check-label fw-bold text-dark" t-att-for="'class_' + str(klass.id)">
|
||||||
|
<t t-out="klass.name"/>
|
||||||
|
</label>
|
||||||
|
<div class="small text-muted ps-4">
|
||||||
|
Seats: <t t-out="klass.enrolled_count"/> / <t t-out="klass.max_students"/>
|
||||||
|
<t t-if="klass.enrolled_count >= klass.max_students">
|
||||||
|
<span class="badge bg-warning text-dark ms-2">Waitlist</span>
|
||||||
|
</t>
|
||||||
|
<t t-else="">
|
||||||
|
<span class="badge bg-success ms-2">Available</span>
|
||||||
|
</t>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="text-end">
|
||||||
|
<t t-if="klass.fee">
|
||||||
|
<span class="fw-bold text-primary fs-5">$<t t-out="'%g' % klass.fee"/></span>
|
||||||
|
</t>
|
||||||
|
<t t-else="">
|
||||||
|
<span class="badge bg-light text-secondary border">Free</span>
|
||||||
|
</t>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</t>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="p-3 bg-light rounded-3 border mb-4">
|
||||||
|
<small class="text-muted d-block">
|
||||||
|
<i class="fa fa-info-circle text-primary me-1"></i>
|
||||||
|
Upon submission, an enrollment record will be created and an invoice/receipt will be generated. You will be able to view classroom details in your Member Portal.
|
||||||
|
</small>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="d-flex flex-wrap justify-content-between align-items-center gap-3 pt-3 pb-1">
|
||||||
|
<a href="/school/register/student" class="btn btn-school-back">
|
||||||
|
<i class="fa fa-arrow-left me-1"></i> Back
|
||||||
|
</a>
|
||||||
|
<button type="submit" class="btn btn-school-next">
|
||||||
|
Complete Registration <i class="fa fa-check ms-2"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</t>
|
</t>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
<!-- STEP 4: DONE / CONFIRMATION -->
|
||||||
<template id="registration_done" name="School Registration: Done">
|
<template id="registration_done" name="School Registration: Done">
|
||||||
<t t-call="website.layout">
|
<t t-call="website.layout">
|
||||||
<div class="container" style="max-width: 480px; margin-top: 60px; margin-bottom: 60px;">
|
<t t-call="community_school.school_registration_styles"/>
|
||||||
<t t-if="waitlisted">
|
<div id="wrap" class="school-reg-wrap py-5">
|
||||||
<div class="alert alert-warning">
|
<div class="container py-4">
|
||||||
<h2>You're on the Waitlist</h2>
|
<div class="row justify-content-center">
|
||||||
<p>The class is currently full. We'll notify you as soon as a seat opens up.</p>
|
<div class="col-md-8 col-lg-7">
|
||||||
|
<div class="card border-0 shadow-sm rounded-4 p-4 p-md-5 text-center bg-white">
|
||||||
|
<t t-if="waitlisted">
|
||||||
|
<div class="mb-4">
|
||||||
|
<span class="d-inline-flex align-items-center justify-content-center rounded-circle"
|
||||||
|
style="width: 80px; height: 80px; background-color: #fff8e1; color: #f59e0b; font-size: 2.5rem;">
|
||||||
|
<i class="fa fa-clock-o"></i>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<h2 class="fw-bold mb-2" style="color: #120735;">Placed on Waitlist</h2>
|
||||||
|
<p class="text-secondary mb-4">
|
||||||
|
The selected class is currently full. We have placed
|
||||||
|
<b class="text-dark" t-out="enrollment.student_id.partner_id.name"/> on the waitlist for
|
||||||
|
<b class="text-primary" t-out="enrollment.class_id.name"/>.
|
||||||
|
You will be notified immediately when a seat opens up.
|
||||||
|
</p>
|
||||||
|
</t>
|
||||||
|
<t t-else="">
|
||||||
|
<div class="mb-4">
|
||||||
|
<span class="d-inline-flex align-items-center justify-content-center rounded-circle"
|
||||||
|
style="width: 80px; height: 80px; background-color: #e8f5e9; color: #2e7d32; font-size: 2.5rem;">
|
||||||
|
<i class="fa fa-check"></i>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<h2 class="fw-bold mb-2" style="color: #120735;">Registration Complete!</h2>
|
||||||
|
<p class="text-secondary mb-4">
|
||||||
|
Congratulations! <b class="text-dark" t-out="enrollment.student_id.partner_id.name"/> is successfully enrolled in
|
||||||
|
<b class="text-primary" t-out="enrollment.class_id.name"/>.
|
||||||
|
</p>
|
||||||
|
</t>
|
||||||
|
|
||||||
|
<div class="p-3 bg-light rounded-3 border text-start mb-4">
|
||||||
|
<h6 class="fw-bold mb-2 text-dark"><i class="fa fa-info-circle text-primary me-2"></i>What's Next:</h6>
|
||||||
|
<ul class="text-muted small mb-0 ps-3">
|
||||||
|
<li class="mb-1"><b>Confirmation Receipt:</b> An enrollment confirmation has been sent to your email.</li>
|
||||||
|
<li class="mb-1"><b>Classroom Access:</b> Log in to your portal anytime to view class schedule, instructors, and student attendance.</li>
|
||||||
|
<li><b>School Portal:</b> Access your parent dashboard at <a href="/my/school">/my/school</a>.</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="d-flex flex-wrap gap-3 justify-content-center">
|
||||||
|
<a href="/" class="btn btn-outline-secondary px-4 py-2 rounded-pill">
|
||||||
|
<i class="fa fa-home me-2"></i>Return to Home
|
||||||
|
</a>
|
||||||
|
<a href="/my/school" class="btn px-4 py-2 rounded-pill fw-bold text-white" style="background-color: #120735;">
|
||||||
|
<i class="fa fa-graduation-cap me-2"></i>View Student Portal
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</t>
|
</div>
|
||||||
<t t-else="">
|
|
||||||
<div class="alert alert-success">
|
|
||||||
<h2>Registration Complete</h2>
|
|
||||||
<p t-out="enrollment.student_id.partner_id.name"/> is enrolled in
|
|
||||||
<span t-out="enrollment.class_id.name"/>.
|
|
||||||
</div>
|
|
||||||
</t>
|
|
||||||
</div>
|
</div>
|
||||||
</t>
|
</t>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
BIN
addons/community_theme_base/static/description/banner.png
Normal file
|
After Width: | Height: | Size: 19 KiB |
BIN
addons/community_theme_base/static/description/icon.png
Normal file
|
After Width: | Height: | Size: 1.7 KiB |
BIN
addons/event_qr_ticketing/static/description/banner.png
Normal file
|
After Width: | Height: | Size: 20 KiB |
BIN
addons/event_qr_ticketing/static/description/icon.png
Normal file
|
After Width: | Height: | Size: 2.0 KiB |
@ -12,6 +12,8 @@ def post_init_hook(env):
|
|||||||
env.company._tncsc_setup_accounting()
|
env.company._tncsc_setup_accounting()
|
||||||
_remove_stock_homepage_page(env)
|
_remove_stock_homepage_page(env)
|
||||||
_fix_website_menus(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):
|
def _remove_stock_homepage_page(env):
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
'name': 'TNCSC Deployment',
|
'name': 'TNCSC Deployment',
|
||||||
'version': '19.0.1.2.0',
|
'version': '19.0.1.3.0',
|
||||||
'category': 'Hidden',
|
'category': 'Hidden',
|
||||||
'summary': 'Tamil Nadu Cultural Society of Canada - client configuration for the Community OS suite',
|
'summary': 'Tamil Nadu Cultural Society of Canada - client configuration for the Community OS suite',
|
||||||
'description': """
|
'description': """
|
||||||
|
|||||||
@ -1,3 +1,4 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
from . import main
|
from . import main
|
||||||
from . import dashboard
|
from . import dashboard
|
||||||
|
from . import membership
|
||||||
|
|||||||
295
addons/tncsc_deployment/controllers/membership.py
Normal file
@ -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"<br/><b>Coupon Applied:</b> {coupon_code}" if coupon_code else ""
|
||||||
|
partner.sudo().message_post(
|
||||||
|
body=(
|
||||||
|
f"<b>Online Membership Application Submitted</b><br/>"
|
||||||
|
f"<b>Tier:</b> {tier.name} ({tier.code}) - ${tier.price}<br/>"
|
||||||
|
f"<b>Username:</b> {username}<br/>"
|
||||||
|
f"<b>Status:</b> 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', {})
|
||||||
@ -53,6 +53,63 @@
|
|||||||
</p>
|
</p>
|
||||||
<p>உங்களுக்கான புதுப்பித்தல் விலைப்பட்டியல் தயாரிக்கப்பட்டுள்ளது. உறுப்பினர் தகுதியை மீண்டும்
|
<p>உங்களுக்கான புதுப்பித்தல் விலைப்பட்டியல் தயாரிக்கப்பட்டுள்ளது. உறுப்பினர் தகுதியை மீண்டும்
|
||||||
பெற எங்களைத் தொடர்பு கொள்ளவும்.</p>
|
பெற எங்களைத் தொடர்பு கொள்ளவும்.</p>
|
||||||
|
</div>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<record id="community_membership.mail_template_membership_approved" model="mail.template">
|
||||||
|
<field name="subject">TNCSC Membership Approved! Welcome | TNCSC உறுப்பினர் சேர்க்கை அங்கீகரிக்கப்பட்டது</field>
|
||||||
|
<field name="body_html" type="html">
|
||||||
|
<div style="margin: 0px; padding: 0px; font-size: 13px;">
|
||||||
|
<p><strong>English</strong></p>
|
||||||
|
<p>Dear <t t-out="object.name or ''">Member</t>,</p>
|
||||||
|
<p>
|
||||||
|
We are thrilled to welcome you to the Tamil Nadu Cultural Society of Canada!
|
||||||
|
Your membership application for
|
||||||
|
<strong><t t-out="object.membership_tier_id.name or 'Membership'"/></strong>
|
||||||
|
has been approved by the Board of Directors.
|
||||||
|
</p>
|
||||||
|
<t t-if="object.membership_member_id">
|
||||||
|
<p>Your Member ID: <strong><t t-out="object.membership_member_id"/></strong></p>
|
||||||
|
</t>
|
||||||
|
<p>You can access your member portal, download your digital membership card, and access community events through our website.</p>
|
||||||
|
<hr/>
|
||||||
|
<p><strong>தமிழ்</strong></p>
|
||||||
|
<p>அன்புள்ள <t t-out="object.name or ''">உறுப்பினர்</t>,</p>
|
||||||
|
<p>
|
||||||
|
கனடா தமிழ்நாடு பண்பாட்டு சங்கத்திற்கு உங்களை அன்போடு வரவேற்கிறோம்!
|
||||||
|
உங்கள் <strong><t t-out="object.membership_tier_id.name or 'உறுப்பினர்'"/></strong> விண்ணப்பம்
|
||||||
|
இயக்குனர் குழுவால் அங்கீகரிக்கப்பட்டுள்ளது.
|
||||||
|
</p>
|
||||||
|
<t t-if="object.membership_member_id">
|
||||||
|
<p>உங்கள் உறுப்பினர் எண்: <strong><t t-out="object.membership_member_id"/></strong></p>
|
||||||
|
</t>
|
||||||
|
<p>நமது இணையதளம் மூலம் உறுப்பினர் போர்ட்டலை அணுகி உங்கள் டிஜிட்டல் அட்டை மற்றும் நிகழ்வு விவரங்களைப் பெறலாம்.</p>
|
||||||
|
</div>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<record id="community_membership.mail_template_membership_rejected" model="mail.template">
|
||||||
|
<field name="subject">TNCSC Membership Application Update | TNCSC உறுப்பினர் விண்ணப்ப தகவல்</field>
|
||||||
|
<field name="body_html" type="html">
|
||||||
|
<div style="margin: 0px; padding: 0px; font-size: 13px;">
|
||||||
|
<p><strong>English</strong></p>
|
||||||
|
<p>Dear <t t-out="object.name or ''">Applicant</t>,</p>
|
||||||
|
<p>
|
||||||
|
Thank you for your application to join the Tamil Nadu Cultural Society of Canada.
|
||||||
|
After careful review, the Board of Directors was unable to approve your application at this time.
|
||||||
|
</p>
|
||||||
|
<p>If any registration fees were collected, a full refund will be processed promptly.</p>
|
||||||
|
<p>If you have any questions or require clarification, please reach out to us at support@tncsc.ca.</p>
|
||||||
|
<hr/>
|
||||||
|
<p><strong>தமிழ்</strong></p>
|
||||||
|
<p>அன்புள்ள <t t-out="object.name or ''">விண்ணப்பதாரர்</t>,</p>
|
||||||
|
<p>
|
||||||
|
கனடா தமிழ்நாடு பண்பாட்டு சங்கத்தில் இணைய விண்ணப்பித்தமைக்கு நன்றி.
|
||||||
|
விண்ணப்ப பரிசீலனைக்குப் பிறகு, தற்போதைக்கு தங்கள் விண்ணப்பத்தை ஏற்க இயலவில்லை என்பதைத் தெரிவித்துக் கொள்கிறோம்.
|
||||||
|
</p>
|
||||||
|
<p>கட்டணம் ஏதேனும் செலுத்தப்பட்டிருந்தால், அது முழுமையாக திருப்பித் தரப்படும்.</p>
|
||||||
|
<p>தொடர்புக்கு: support@tncsc.ca</p>
|
||||||
</div>
|
</div>
|
||||||
</field>
|
</field>
|
||||||
</record>
|
</record>
|
||||||
|
|||||||
16
addons/tncsc_deployment/migrations/19.0.1.3.0/pre-migrate.py
Normal file
@ -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)
|
||||||
BIN
addons/tncsc_deployment/static/description/banner.png
Normal file
|
After Width: | Height: | Size: 19 KiB |
BIN
addons/tncsc_deployment/static/description/icon.png
Normal file
|
After Width: | Height: | Size: 1.8 KiB |
@ -5,28 +5,32 @@
|
|||||||
display: none !important;
|
display: none !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tncsc-hero-nav .tncsc-member-login {
|
.tncsc-member-login {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
padding: 8px 12px;
|
padding: 7px 16px;
|
||||||
border: 1px solid #D89C43;
|
border: 1px solid #d19b33 !important;
|
||||||
border-radius: 6px;
|
border-radius: 20px !important;
|
||||||
background: #FFFCF7;
|
background: linear-gradient(135deg, #e5a93c 0%, #d19b33 100%) !important;
|
||||||
color: #87530D;
|
color: #120735 !important;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
font-weight: 600;
|
font-weight: 700 !important;
|
||||||
text-decoration: none;
|
text-decoration: none !important;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
|
box-shadow: 0 2px 8px rgba(229, 169, 60, 0.3);
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
background: #FFF0D6;
|
background: linear-gradient(135deg, #f3c669 0%, #e5a93c 100%) !important;
|
||||||
color: #643D09;
|
color: #120735 !important;
|
||||||
|
transform: translateY(-1px);
|
||||||
|
box-shadow: 0 4px 12px rgba(229, 169, 60, 0.45);
|
||||||
}
|
}
|
||||||
|
|
||||||
&:focus-visible {
|
&:focus-visible {
|
||||||
outline: 3px solid #D89C43;
|
outline: 3px solid #e5a93c;
|
||||||
outline-offset: 3px;
|
outline-offset: 3px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -106,3 +106,46 @@ class TestTncscDeployment(TransactionCase):
|
|||||||
self.env.ref('community_interac.group_interac_verifier'),
|
self.env.ref('community_interac.group_interac_verifier'),
|
||||||
treasurer.implied_ids,
|
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')
|
||||||
|
|||||||
@ -10,9 +10,12 @@
|
|||||||
'/membership': 'Membership | TNCSC',
|
'/membership': 'Membership | TNCSC',
|
||||||
'/membership/benefits': 'Membership Benefits | TNCSC',
|
'/membership/benefits': 'Membership Benefits | TNCSC',
|
||||||
'/membership/registration': 'Membership Registration | TNCSC',
|
'/membership/registration': 'Membership Registration | TNCSC',
|
||||||
|
'/membership/join': 'Join Membership | TNCSC',
|
||||||
'/event': 'Events | TNCSC',
|
'/event': 'Events | TNCSC',
|
||||||
'/product/tncsc-summer-picnic-2026': 'Summer Picnic 2026 | TNCSC',
|
|
||||||
'/tamil-school': 'Tamil School | 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',
|
'/sponsors': 'Sponsors | TNCSC',
|
||||||
'/contact': 'Contact Us | TNCSC',
|
'/contact': 'Contact Us | TNCSC',
|
||||||
'/my': 'Member Portal | TNCSC',
|
'/my': 'Member Portal | TNCSC',
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<odoo>
|
<odoo>
|
||||||
<template id="website_member_login_button" name="TNCSC Member Login Button">
|
<template id="website_member_login_button" name="TNCSC Member Login Button">
|
||||||
<a href="/web/login?redirect=/my" class="tncsc-member-login">
|
<a href="/web/login?redirect=/my" class="btn btn-warning btn-sm rounded-pill px-3 fw-bold text-dark tncsc-member-login" style="background: linear-gradient(135deg, #e5a93c 0%, #d19b33 100%) !important; color: #120735 !important; font-weight: 700 !important; border: 1px solid #d19b33 !important; text-decoration: none !important; display: inline-flex; align-items: center; gap: 6px; box-shadow: 0 2px 8px rgba(229, 169, 60, 0.3);">
|
||||||
<i class="fa fa-user" aria-hidden="true"/> Member Login
|
<i class="fa fa-user" aria-hidden="true"/> Member Login
|
||||||
</a>
|
</a>
|
||||||
</template>
|
</template>
|
||||||
@ -97,15 +97,12 @@
|
|||||||
</xpath>
|
</xpath>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<template id="view_page_product_summer_picnic_custom_header_only"
|
<template id="view_page_membership_join_custom_header_only"
|
||||||
inherit_id="tncsc_deployment.view_page_product_summer_picnic"
|
inherit_id="tncsc_deployment.view_page_membership_join"
|
||||||
name="Use TNCSC navigation without the default website header">
|
name="Use TNCSC navigation without the default website header">
|
||||||
<xpath expr="//t[@t-call='website.layout']" position="inside">
|
<xpath expr="//t[@t-call='website.layout']" position="inside">
|
||||||
<t t-set="no_header" t-value="True"/>
|
<t t-set="no_header" t-value="True"/>
|
||||||
</xpath>
|
</xpath>
|
||||||
<xpath expr="//div[hasclass('tncsc-hero-nav')]//a[@href='/contact']" position="after">
|
|
||||||
<t t-call="tncsc_deployment.website_member_login_button"/>
|
|
||||||
</xpath>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<template id="view_page_tamil_school_custom_header_only"
|
<template id="view_page_tamil_school_custom_header_only"
|
||||||
@ -129,6 +126,67 @@
|
|||||||
<t t-call="tncsc_deployment.website_member_login_button"/>
|
<t t-call="tncsc_deployment.website_member_login_button"/>
|
||||||
</xpath>
|
</xpath>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
<!-- School Registration Navigation & Branding -->
|
||||||
|
<template id="tncsc_school_reg_header_nav" name="TNCSC School Registration Navigation">
|
||||||
|
<div class="tncsc-top-header" style="background-color: #120735; padding: 14px 20px; text-align: center;">
|
||||||
|
<h1 style="font-size: 1.55rem; font-weight: 700; color: #ffffff; margin: 0; line-height: 1.2;">தமிழ்நாடு கலாச்சார சங்கம்</h1>
|
||||||
|
<p style="font-size: 1.1rem; font-weight: 400; color: #B8B2E6; margin: 2px 0 0 0;">Tamil Nadu Cultural Society of Canada</p>
|
||||||
|
</div>
|
||||||
|
<nav class="navbar navbar-expand-lg navbar-dark" style="background-color: #1c0d48; border-bottom: 2px solid #e5a93c; padding: 8px 24px;">
|
||||||
|
<div class="container-fluid">
|
||||||
|
<a class="navbar-brand d-flex align-items-center gap-2" href="/">
|
||||||
|
<img src="/tncsc_deployment/static/src/img/brand/logo.png" alt="TNCSC Logo" style="height: 38px; width: auto;"/>
|
||||||
|
<span class="fw-bold text-white small d-none d-sm-inline">TNCSC</span>
|
||||||
|
</a>
|
||||||
|
<div class="d-flex align-items-center gap-2 ms-auto">
|
||||||
|
<a href="/tamil-school" class="btn btn-outline-light btn-sm rounded-pill px-3">
|
||||||
|
<i class="fa fa-graduation-cap me-1"></i> Tamil School
|
||||||
|
</a>
|
||||||
|
<a href="/membership/registration" class="btn btn-outline-warning btn-sm rounded-pill px-3">
|
||||||
|
<i class="fa fa-id-card me-1"></i> Membership
|
||||||
|
</a>
|
||||||
|
<t t-call="tncsc_deployment.website_member_login_button"/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template id="view_school_reg_step_parent_header" inherit_id="community_school.registration_step_parent" name="TNCSC School Reg Header Parent">
|
||||||
|
<xpath expr="//t[@t-call='website.layout']" position="inside">
|
||||||
|
<t t-set="no_header" t-value="True"/>
|
||||||
|
</xpath>
|
||||||
|
<xpath expr="//div[@id='wrap']" position="before">
|
||||||
|
<t t-call="tncsc_deployment.tncsc_school_reg_header_nav"/>
|
||||||
|
</xpath>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template id="view_school_reg_step_student_header" inherit_id="community_school.registration_step_student" name="TNCSC School Reg Header Student">
|
||||||
|
<xpath expr="//t[@t-call='website.layout']" position="inside">
|
||||||
|
<t t-set="no_header" t-value="True"/>
|
||||||
|
</xpath>
|
||||||
|
<xpath expr="//div[@id='wrap']" position="before">
|
||||||
|
<t t-call="tncsc_deployment.tncsc_school_reg_header_nav"/>
|
||||||
|
</xpath>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template id="view_school_reg_step_class_header" inherit_id="community_school.registration_step_class" name="TNCSC School Reg Header Class">
|
||||||
|
<xpath expr="//t[@t-call='website.layout']" position="inside">
|
||||||
|
<t t-set="no_header" t-value="True"/>
|
||||||
|
</xpath>
|
||||||
|
<xpath expr="//div[@id='wrap']" position="before">
|
||||||
|
<t t-call="tncsc_deployment.tncsc_school_reg_header_nav"/>
|
||||||
|
</xpath>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template id="view_school_reg_done_header" inherit_id="community_school.registration_done" name="TNCSC School Reg Header Done">
|
||||||
|
<xpath expr="//t[@t-call='website.layout']" position="inside">
|
||||||
|
<t t-set="no_header" t-value="True"/>
|
||||||
|
</xpath>
|
||||||
|
<xpath expr="//div[@id='wrap']" position="before">
|
||||||
|
<t t-call="tncsc_deployment.tncsc_school_reg_header_nav"/>
|
||||||
|
</xpath>
|
||||||
|
</template>
|
||||||
<!-- Odoo's stock footer (shown on built-in pages such as event
|
<!-- Odoo's stock footer (shown on built-in pages such as event
|
||||||
registration, login and school registration) ships demo content:
|
registration, login and school registration) ships demo content:
|
||||||
dead "#" links, info@yourcompany.example.com, a 555 phone number and
|
dead "#" links, info@yourcompany.example.com, a 555 phone number and
|
||||||
@ -136,47 +194,51 @@
|
|||||||
website.footer_custom (16) makes this replace it even where the
|
website.footer_custom (16) makes this replace it even where the
|
||||||
website editor has made a website-specific copy of that view. -->
|
website editor has made a website-specific copy of that view. -->
|
||||||
<template id="tncsc_footer" inherit_id="website.layout" name="TNCSC Footer" priority="90">
|
<template id="tncsc_footer" inherit_id="website.layout" name="TNCSC Footer" priority="90">
|
||||||
|
<xpath expr="//footer[@id='bottom']" position="attributes">
|
||||||
|
<attribute name="style">background-color: #120735 !important; color: #ffffff;</attribute>
|
||||||
|
<attribute name="class" remove="bg-light" separator=" "/>
|
||||||
|
</xpath>
|
||||||
<xpath expr="//div[@id='footer']" position="replace">
|
<xpath expr="//div[@id='footer']" position="replace">
|
||||||
<div id="footer" class="oe_structure oe_structure_solo text-break" t-if="not no_footer">
|
<div id="footer" class="oe_structure oe_structure_solo text-break" t-if="not no_footer" style="background-color: #120735; color: #ffffff;">
|
||||||
<section class="pt40 pb16">
|
<section class="pt48 pb32" style="background-color: #120735; color: #cbd5e1;">
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-lg-3 pt24 pb24">
|
<div class="col-lg-3 pt16 pb16">
|
||||||
<h5>Useful Links</h5>
|
<h5 style="color: #e5a93c; font-weight: 700; font-size: 1.15rem; margin-bottom: 18px; letter-spacing: 0.5px;">Useful Links</h5>
|
||||||
<ul class="list-unstyled">
|
<ul class="list-unstyled">
|
||||||
<li><a href="/">Home</a></li>
|
<li class="mb-2"><a href="/" style="color: #cbd5e1; text-decoration: none; transition: color 0.2s ease;">Home</a></li>
|
||||||
<li><a href="/about">About Us</a></li>
|
<li class="mb-2"><a href="/about" style="color: #cbd5e1; text-decoration: none; transition: color 0.2s ease;">About Us</a></li>
|
||||||
<li><a href="/event">Events</a></li>
|
<li class="mb-2"><a href="/event" style="color: #cbd5e1; text-decoration: none; transition: color 0.2s ease;">Events</a></li>
|
||||||
<li><a href="/membership/registration">Membership</a></li>
|
<li class="mb-2"><a href="/membership/registration" style="color: #cbd5e1; text-decoration: none; transition: color 0.2s ease;">Membership</a></li>
|
||||||
<li><a href="/tamil-school">Tamil Class</a></li>
|
<li class="mb-2"><a href="/tamil-school" style="color: #cbd5e1; text-decoration: none; transition: color 0.2s ease;">Tamil Class</a></li>
|
||||||
<li><a href="/contact">Contact</a></li>
|
<li class="mb-2"><a href="/contact" style="color: #cbd5e1; text-decoration: none; transition: color 0.2s ease;">Contact</a></li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-lg-5 pt24 pb24">
|
<div class="col-lg-5 pt16 pb16">
|
||||||
<h5>About TNCSC</h5>
|
<h5 style="color: #e5a93c; font-weight: 700; font-size: 1.15rem; margin-bottom: 18px; letter-spacing: 0.5px;">About TNCSC</h5>
|
||||||
<p>A not-for-profit Ontario corporation serving Tamils of Indian origin in Canada
|
<p style="color: #cbd5e1; font-size: 0.95rem; line-height: 1.7;">A not-for-profit Ontario corporation serving Tamils of Indian origin in Canada
|
||||||
since 1997, preserving and promoting our language, heritage and culture.</p>
|
since 1997, preserving and promoting our language, heritage and culture.</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-lg-3 offset-lg-1 pt24 pb24">
|
<div class="col-lg-3 offset-lg-1 pt16 pb16">
|
||||||
<h5>Connect with us</h5>
|
<h5 style="color: #e5a93c; font-weight: 700; font-size: 1.15rem; margin-bottom: 18px; letter-spacing: 0.5px;">Connect with us</h5>
|
||||||
<ul class="list-unstyled">
|
<ul class="list-unstyled mb-4">
|
||||||
<li><i class="fa fa-comment fa-fw me-2"/><a href="/contact">Contact us</a></li>
|
<li class="mb-2"><i class="fa fa-comment fa-fw me-2" style="color: #e5a93c;"/><a href="/contact" style="color: #cbd5e1; text-decoration: none;">Contact us</a></li>
|
||||||
<li t-if="res_company.email">
|
<li t-if="res_company.email" class="mb-2">
|
||||||
<i class="fa fa-envelope fa-fw me-2"/><a t-attf-href="mailto:#{res_company.email}" t-out="res_company.email"/>
|
<i class="fa fa-envelope fa-fw me-2" style="color: #e5a93c;"/><a t-attf-href="mailto:#{res_company.email}" t-out="res_company.email" style="color: #cbd5e1; text-decoration: none;"/>
|
||||||
</li>
|
</li>
|
||||||
<li t-if="res_company.phone">
|
<li t-if="res_company.phone" class="mb-2">
|
||||||
<i class="fa fa-phone fa-fw me-2"/><a t-attf-href="tel:#{res_company.phone}" t-out="res_company.phone"/>
|
<i class="fa fa-phone fa-fw me-2" style="color: #e5a93c;"/><a t-attf-href="tel:#{res_company.phone}" t-out="res_company.phone" style="color: #cbd5e1; text-decoration: none;"/>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
<div class="s_social_media text-start">
|
<div class="s_social_media text-start d-flex gap-3">
|
||||||
<a t-if="website.social_facebook" href="/website/social/facebook" target="_blank" rel="noopener" aria-label="Facebook">
|
<a t-if="website.social_facebook" href="/website/social/facebook" target="_blank" rel="noopener" aria-label="Facebook" style="color: #e5a93c; font-size: 1.3rem;">
|
||||||
<i class="fa fa-facebook rounded-circle shadow-sm"/>
|
<i class="fa fa-facebook"/>
|
||||||
</a>
|
</a>
|
||||||
<a t-if="website.social_youtube" href="/website/social/youtube" target="_blank" rel="noopener" aria-label="YouTube">
|
<a t-if="website.social_youtube" href="/website/social/youtube" target="_blank" rel="noopener" aria-label="YouTube" style="color: #e5a93c; font-size: 1.3rem;">
|
||||||
<i class="fa fa-youtube-play rounded-circle shadow-sm"/>
|
<i class="fa fa-youtube-play"/>
|
||||||
</a>
|
</a>
|
||||||
<a t-if="website.social_instagram" href="/website/social/instagram" target="_blank" rel="noopener" aria-label="Instagram">
|
<a t-if="website.social_instagram" href="/website/social/instagram" target="_blank" rel="noopener" aria-label="Instagram" style="color: #e5a93c; font-size: 1.3rem;">
|
||||||
<i class="fa fa-instagram rounded-circle shadow-sm"/>
|
<i class="fa fa-instagram"/>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -185,6 +247,20 @@
|
|||||||
</section>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
</xpath>
|
</xpath>
|
||||||
|
<xpath expr="//div[hasclass('o_footer_copyright')]" position="replace">
|
||||||
|
<div class="o_footer_copyright" style="background-color: #0c0424 !important; border-top: 1px solid rgba(255, 255, 255, 0.08); padding: 18px 0; color: #94a3b8;">
|
||||||
|
<div class="container">
|
||||||
|
<div class="row align-items-center row-gap-2">
|
||||||
|
<div class="col-md-8 text-center text-md-start small">
|
||||||
|
<span>Copyright &copy; 2026 Tamil Nadu Cultural Society of Canada. All rights reserved.</span>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-4 text-center text-md-end small">
|
||||||
|
<span style="color: #e5a93c; font-weight: 600;">Preserving Tamil Culture Since 1997</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</xpath>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<!-- Odoo's stock header (on built-in pages such as login, event and
|
<!-- Odoo's stock header (on built-in pages such as login, event and
|
||||||
|
|||||||
@ -6,6 +6,7 @@ db_user = odoo
|
|||||||
db_password = odoo
|
db_password = odoo
|
||||||
addons_path = /mnt/extra-addons
|
addons_path = /mnt/extra-addons
|
||||||
dev_mode = reload,qweb,werkzeug,xml
|
dev_mode = reload,qweb,werkzeug,xml
|
||||||
|
db_name = TNCSC
|
||||||
list_db = True
|
list_db = True
|
||||||
proxy_mode = False
|
proxy_mode = False
|
||||||
logfile =
|
logfile =
|
||||||
|
|||||||
67
docs/role_guides/admin_guide.md
Normal file
@ -0,0 +1,67 @@
|
|||||||
|
# TNCSC Community OS — Administrator Guide
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
This guide is for TNCSC System Administrators, Board Members, and Membership Managers managing the Tamil Nadu Cultural Society of Canada's digital platform on Odoo 19.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. Membership Management
|
||||||
|
|
||||||
|
### Reviewing Applications
|
||||||
|
1. Navigate to **Community OS > Members**.
|
||||||
|
2. Filter by **Pending Approval** in the search bar.
|
||||||
|
3. Open a member record to inspect their application:
|
||||||
|
- **Personal Details**: First Name, Last Name, Phone, Email, Address.
|
||||||
|
- **Eligibility Questions**: Check answers to the 5 TNCSC Constitution eligibility questions (Tamil origin, PIO/OCI status, citizenship history).
|
||||||
|
- **Age & Birth Info**: Check computed age (worked out from birth month and year).
|
||||||
|
- **Uploaded Proof**: If a Senior or Student tier was requested, review the attached `Proof of Eligibility` document in the chatter or membership tab.
|
||||||
|
- **Family Members**: For Annual/Family memberships, view child records linked under `Family Members`.
|
||||||
|
|
||||||
|
### Approving or Rejecting
|
||||||
|
- Click **Approve**:
|
||||||
|
- Automatically transitions `Board Approval` to **Approved**.
|
||||||
|
- Sets membership state to **Active** and sets the start and expiry dates based on the tier.
|
||||||
|
- Automatically dispatches the bilingual **TNCSC Membership Approved** email containing their Member ID and portal access information.
|
||||||
|
- Logs approval details in the chatter.
|
||||||
|
- Click **Reject**:
|
||||||
|
- Sets `Board Approval` to **Rejected**.
|
||||||
|
- Cancels any draft membership dues invoice.
|
||||||
|
- Sends the **TNCSC Membership Rejected** notification email with instructions on refund/appeals.
|
||||||
|
- Logs the rejection in chatter.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. Event Management & Ticketing
|
||||||
|
|
||||||
|
1. Navigate to **Community OS > Events**.
|
||||||
|
2. Create or open an event (e.g. Tamil New Year, Picnic, Pongal Celebration).
|
||||||
|
3. **Hero & Website Display**:
|
||||||
|
- Ensure `Website Published` is checked.
|
||||||
|
- The public website dynamically links to the earliest upcoming event for "Book Now" and lists all upcoming events at `/event`.
|
||||||
|
4. **QR Code Attendance**:
|
||||||
|
- Attendees receive a QR code badge/ticket.
|
||||||
|
- Volunteers can scan attendees at the entrance using any mobile device camera or Odoo barcode scanner to validate registration.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. Website Administration & CMS
|
||||||
|
|
||||||
|
1. **Pages**:
|
||||||
|
- `/` — Homepage featuring upcoming events, announcements, and quick links.
|
||||||
|
- `/about` & `/about/board-of-directors` — Organization mission, history, and committee members.
|
||||||
|
- `/membership/registration` — Plan comparison and details.
|
||||||
|
- `/membership/join` — Public interactive membership signup application with real-time validation.
|
||||||
|
- `/event` — Dynamic events catalog.
|
||||||
|
- `/tamil-school` — Tamil class information and enrollment.
|
||||||
|
- `/contact` — Contact details with spam-protected inquiry form.
|
||||||
|
2. **Editing Content**:
|
||||||
|
- Log in as an administrator and click the **Edit** button in the website top bar to make visual changes.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. System Security & User Roles
|
||||||
|
|
||||||
|
- **Portal Users**: Regular members, students, and parents. Can only view their own membership card, profile, events, and family school records.
|
||||||
|
- **Membership Managers**: Access to sensitive applicant data (DOB, proof documents, eligibility answers, board approval buttons).
|
||||||
|
- **School Teachers**: Access to classroom rosters, attendance sheets, and student grades.
|
||||||
|
- **Treasurers / Accountants**: Access to invoices, bank journals, Interac e-Transfer reconciliations, and financial reports.
|
||||||
49
docs/role_guides/parent_guide.md
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
# TNCSC Community Portal — Member & Parent Guide
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
This guide is for TNCSC members, parents of Tamil School students, and event attendees using the self-service web portal.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. Accessing the Member Portal
|
||||||
|
|
||||||
|
1. Visit `https://tncsc.odoo.thedomainnest.com/my` (or click **Member Login** in the top navigation).
|
||||||
|
2. Enter your registered **Username / Email** and **Password** chosen during registration.
|
||||||
|
3. Once logged in, your personal dashboard displays:
|
||||||
|
- Your Active Membership Tier & Expiry Date
|
||||||
|
- Digital Membership Card with Scannable QR Code
|
||||||
|
- Event Registrations & QR Tickets
|
||||||
|
- Children Enrolled in Tamil School
|
||||||
|
- Invoices & Payment History
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. Digital Membership Card
|
||||||
|
|
||||||
|
1. On your portal home page (`/my`), click **View Membership Card**.
|
||||||
|
2. Your digital card includes:
|
||||||
|
- Your full name and Member ID (e.g. `TNCSC-2026-0042`)
|
||||||
|
- Membership tier (e.g. Annual/Family, Life Term, Senior)
|
||||||
|
- QR code for entry verification at community events
|
||||||
|
- Expiration date (or *Lifetime*)
|
||||||
|
3. You can screenshot or save the card to your smartphone for instant check-in.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. Tamil School Student Portal
|
||||||
|
|
||||||
|
1. Navigate to **My Account > Tamil School** (`/my/school`).
|
||||||
|
2. View each enrolled child:
|
||||||
|
- Class Level & Instructor
|
||||||
|
- Attendance Records & Progress
|
||||||
|
- Schedule & Classroom location
|
||||||
|
3. Update emergency contacts or submit absence notes directly.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. Event Registration & Tickets
|
||||||
|
|
||||||
|
1. Browse upcoming cultural festivals and picnics at `/event`.
|
||||||
|
2. Click **Register** on an event to claim tickets for yourself and family members.
|
||||||
|
3. Access your tickets anytime under **My Account > Event Tickets**.
|
||||||
|
4. Show the ticket QR code at the event entrance for expedited check-in.
|
||||||
41
docs/role_guides/teacher_guide.md
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
# TNCSC Tamil School — Teacher & Instructor Guide
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
This guide is for Tamil language and cultural instructors conducting classes at the TNCSC Tamil School.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. Accessing Your Classes
|
||||||
|
|
||||||
|
1. Log in to the TNCSC platform at `https://tncsc.odoo.thedomainnest.com` (or local portal).
|
||||||
|
2. Navigate to **Community OS > Tamil School > Classes**.
|
||||||
|
3. Open your assigned class (e.g. *Kindergarten Tamil*, *Level 1 Tamil*, *Advanced Tamil Literature*).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. Managing Class Rosters & Students
|
||||||
|
|
||||||
|
1. In the class form, review the **Enrolled Students** list.
|
||||||
|
2. Each student card displays:
|
||||||
|
- Student Name & Tamil Name
|
||||||
|
- Age / Date of Birth
|
||||||
|
- Parent / Guardian Contact (Email & Phone)
|
||||||
|
- Emergency Contact details
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. Daily Attendance
|
||||||
|
|
||||||
|
1. Under the class view, select **Take Attendance** for the current session date.
|
||||||
|
2. Mark each student as:
|
||||||
|
- **Present**
|
||||||
|
- **Absent**
|
||||||
|
- **Excused / Late**
|
||||||
|
3. Save the attendance record. Parents can see their child's attendance history in real time via their member portal.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. Class Announcements & Syllabus
|
||||||
|
|
||||||
|
1. Post homework, lesson plans, and reading assignments directly in the class chatter.
|
||||||
|
2. Parents and students will receive email notifications with class updates.
|
||||||
49
docs/role_guides/treasurer_guide.md
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
# TNCSC Community OS — Treasurer & Finance Guide
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
This guide is for the TNCSC Treasurer and finance team responsible for managing membership dues, event ticketing revenue, Tamil school tuition fees, donations, and Canadian non-profit compliance.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. Membership Dues & Invoicing
|
||||||
|
|
||||||
|
### Automated Invoicing
|
||||||
|
- When an applicant submits a membership registration or is approved, an invoice is generated based on their chosen tier:
|
||||||
|
- **Annual/Family (FAM)**: $30 CAD / 1 year
|
||||||
|
- **Long-Term (LONG)**: $250 CAD / 10 years
|
||||||
|
- **Life Term (LIFE)**: $500 CAD / Lifetime
|
||||||
|
- **Senior Annual (SEN)**: $15 CAD / 1 year
|
||||||
|
- **Senior Life (SENLIFE)**: $250 CAD / Lifetime
|
||||||
|
- **Student Annual (STU)**: $10 CAD / 1 year
|
||||||
|
|
||||||
|
### Reviewing Dues Invoices
|
||||||
|
1. Navigate to **Accounting > Customers > Invoices**.
|
||||||
|
2. Filter by status: `Draft`, `Posted`, or `Paid`.
|
||||||
|
3. Invoices are tagged to the member partner and default to the Canadian Chart of Accounts revenue account for Membership Dues.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. Interac e-Transfer Reconciliations
|
||||||
|
|
||||||
|
TNCSC accepts e-Transfers through `community_interac`.
|
||||||
|
1. Members send Interac payments to `finance@tncsc.ca` (or configured company email).
|
||||||
|
2. When the bank statement or notification is received:
|
||||||
|
- Match the transaction reference or applicant name in **Accounting > Bank and Cash**.
|
||||||
|
- Reconcile the payment against the open customer invoice.
|
||||||
|
- Once marked Paid, the membership state automatically advances to `Active`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. Event Revenue & Ticketing
|
||||||
|
|
||||||
|
1. Event registrations (such as picnics or banquets) automatically generate sales records and invoices if fee-based.
|
||||||
|
2. Invoices can be reviewed under **Accounting > Invoices** filtered by Event tag.
|
||||||
|
3. Refunds for canceled events or rejected applicants can be processed via **Credit Note** directly on the invoice.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. Financial Reporting & Non-Profit Filings
|
||||||
|
|
||||||
|
1. **Balance Sheet**: **Accounting > Reporting > Balance Sheet**.
|
||||||
|
2. **Profit and Loss (Income Statement)**: **Accounting > Reporting > Profit and Loss**.
|
||||||
|
3. **General Ledger**: Exportable for Canadian CRA T3010 / T2 non-profit returns and annual general meeting (AGM) presentations.
|
||||||
@ -10,8 +10,8 @@ import re
|
|||||||
import sys
|
import sys
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
REPO_ROOT = Path(__file__).resolve().parent.parent
|
REPO_ROOT = Path(__file__).resolve().parent.parent if Path(__file__).name != '<stdin>' else Path('/mnt/extra-addons').parent
|
||||||
ADDONS_DIR = REPO_ROOT / "addons"
|
ADDONS_DIR = Path('/mnt/extra-addons') if Path('/mnt/extra-addons').exists() else (REPO_ROOT / "addons")
|
||||||
|
|
||||||
BANNED_WORDS = [
|
BANNED_WORDS = [
|
||||||
"tncsc",
|
"tncsc",
|
||||||
|
|||||||
@ -9,8 +9,8 @@ import ast
|
|||||||
import sys
|
import sys
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
REPO_ROOT = Path(__file__).resolve().parent.parent
|
REPO_ROOT = Path(__file__).resolve().parent.parent if Path(__file__).name != '<stdin>' else Path('/mnt/extra-addons').parent
|
||||||
ADDONS_DIR = REPO_ROOT / "addons"
|
ADDONS_DIR = Path('/mnt/extra-addons') if Path('/mnt/extra-addons').exists() else (REPO_ROOT / "addons")
|
||||||
|
|
||||||
REQUIRED_KEYS = ["name", "version", "author", "website", "license", "category", "summary", "depends"]
|
REQUIRED_KEYS = ["name", "version", "author", "website", "license", "category", "summary", "depends"]
|
||||||
PRODUCT_ONLY_KEYS = ["price", "currency", "images"]
|
PRODUCT_ONLY_KEYS = ["price", "currency", "images"]
|
||||||
|
|||||||
175
scripts/generate_module_assets.py
Normal file
@ -0,0 +1,175 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Generate professional icon.png and banner.png assets for Community OS modules."""
|
||||||
|
import os
|
||||||
|
from PIL import Image, ImageDraw, ImageFont
|
||||||
|
|
||||||
|
MODULES = [
|
||||||
|
{
|
||||||
|
'dir': 'community_membership',
|
||||||
|
'title': 'Community Membership',
|
||||||
|
'subtitle': 'Tiers, Terms, Expiry, Family Plans & Digital Cards',
|
||||||
|
'color_primary': (18, 7, 53), # Deep Navy
|
||||||
|
'color_secondary': (229, 169, 60), # Gold
|
||||||
|
'badge': 'MEMBERSHIP',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'dir': 'community_school',
|
||||||
|
'title': 'Community School',
|
||||||
|
'subtitle': 'Classrooms, Tamil School Rosters, Attendance & Teachers',
|
||||||
|
'color_primary': (15, 60, 45), # Deep Emerald
|
||||||
|
'color_secondary': (229, 169, 60), # Gold
|
||||||
|
'badge': 'TAMIL SCHOOL',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'dir': 'event_qr_ticketing',
|
||||||
|
'title': 'Event QR Ticketing',
|
||||||
|
'subtitle': 'Fast QR Code Check-in, Attendee Badges & Scanner',
|
||||||
|
'color_primary': (40, 15, 80), # Deep Violet
|
||||||
|
'color_secondary': (243, 198, 105),# Light Gold
|
||||||
|
'badge': 'EVENTS & QR',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'dir': 'community_portal',
|
||||||
|
'title': 'Community Portal',
|
||||||
|
'subtitle': 'Self-Service Member Portal, Digital Cards & School Roster',
|
||||||
|
'color_primary': (20, 30, 75), # Deep Indigo
|
||||||
|
'color_secondary': (70, 190, 200), # Teal
|
||||||
|
'badge': 'PORTAL',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'dir': 'community_benefits',
|
||||||
|
'title': 'Community Benefits',
|
||||||
|
'subtitle': 'Member Discounts, Partner Perks & Entitlement Verification',
|
||||||
|
'color_primary': (25, 45, 100), # Royal Blue
|
||||||
|
'color_secondary': (229, 169, 60), # Gold
|
||||||
|
'badge': 'BENEFITS',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'dir': 'community_classifieds',
|
||||||
|
'title': 'Community Classifieds',
|
||||||
|
'subtitle': 'Community Marketplace, Volunteer Listings & Housing',
|
||||||
|
'color_primary': (20, 70, 70), # Dark Teal
|
||||||
|
'color_secondary': (255, 140, 60), # Tangerine
|
||||||
|
'badge': 'CLASSIFIEDS',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'dir': 'community_interac',
|
||||||
|
'title': 'Community Interac',
|
||||||
|
'subtitle': 'Canadian Interac e-Transfer Direct Payment Provider',
|
||||||
|
'color_primary': (80, 50, 10), # Dark Bronze
|
||||||
|
'color_secondary': (255, 190, 40), # Amber
|
||||||
|
'badge': 'INTERAC CAD',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'dir': 'community_theme_base',
|
||||||
|
'title': 'Community Theme Base',
|
||||||
|
'subtitle': 'Unified Theme Tokens, Typography, Colors & Branding',
|
||||||
|
'color_primary': (35, 25, 60), # Deep Slate Purple
|
||||||
|
'color_secondary': (180, 140, 230),# Lavender
|
||||||
|
'badge': 'THEME BASE',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'dir': 'tncsc_deployment',
|
||||||
|
'title': 'TNCSC Deployment',
|
||||||
|
'subtitle': 'Tamil Nadu Cultural Society of Canada Production Platform',
|
||||||
|
'color_primary': (18, 7, 53), # TNCSC Navy
|
||||||
|
'color_secondary': (229, 169, 60), # TNCSC Gold
|
||||||
|
'badge': 'TNCSC CANADA',
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def make_gradient(width, height, color1, color2):
|
||||||
|
base = Image.new('RGB', (width, height), color1)
|
||||||
|
top = Image.new('RGB', (width, height), color2)
|
||||||
|
mask = Image.new('L', (width, height))
|
||||||
|
mask_data = []
|
||||||
|
for y in range(height):
|
||||||
|
for x in range(width):
|
||||||
|
mask_data.append(int(255 * (x / width * 0.5 + y / height * 0.5)))
|
||||||
|
mask.putdata(mask_data)
|
||||||
|
base.paste(top, (0, 0), mask)
|
||||||
|
return base
|
||||||
|
|
||||||
|
|
||||||
|
def generate_icon(mod, target_dir):
|
||||||
|
width, height = 128, 128
|
||||||
|
c1 = mod['color_primary']
|
||||||
|
c2 = tuple(min(255, c + 35) for c in c1)
|
||||||
|
im = make_gradient(width, height, c1, c2)
|
||||||
|
draw = ImageDraw.Draw(im)
|
||||||
|
|
||||||
|
# Gold accent border
|
||||||
|
gold = mod['color_secondary']
|
||||||
|
draw.rounded_rectangle([4, 4, width - 4, height - 4], radius=24, outline=gold, width=3)
|
||||||
|
|
||||||
|
# Center monogram/symbol
|
||||||
|
words = mod['badge'].split()
|
||||||
|
monogram = "".join(w[0] for w in words[:3])
|
||||||
|
|
||||||
|
# Inner badge pill
|
||||||
|
draw.rounded_rectangle([20, 46, width - 20, 82], radius=8, fill=gold)
|
||||||
|
draw.text((width // 2, 64), monogram, fill=c1, anchor="mm")
|
||||||
|
|
||||||
|
# Bottom bar
|
||||||
|
draw.line([30, 100, width - 30, 100], fill=gold, width=2)
|
||||||
|
|
||||||
|
os.makedirs(target_dir, exist_ok=True)
|
||||||
|
icon_path = os.path.join(target_dir, 'icon.png')
|
||||||
|
im.save(icon_path, 'PNG')
|
||||||
|
print(f"Generated {icon_path}")
|
||||||
|
|
||||||
|
|
||||||
|
def generate_banner(mod, target_dir):
|
||||||
|
width, height = 800, 400
|
||||||
|
c1 = mod['color_primary']
|
||||||
|
c2 = tuple(min(255, c + 25) for c in c1)
|
||||||
|
im = make_gradient(width, height, c1, c2)
|
||||||
|
draw = ImageDraw.Draw(im)
|
||||||
|
|
||||||
|
gold = mod['color_secondary']
|
||||||
|
|
||||||
|
# Elegant decorative borders
|
||||||
|
draw.rectangle([0, 0, width, 8], fill=gold)
|
||||||
|
draw.rectangle([0, height - 8, width, height], fill=gold)
|
||||||
|
|
||||||
|
# Subtle geometric grid / card background
|
||||||
|
draw.rounded_rectangle([40, 40, width - 40, height - 40], radius=16, outline=gold, width=2)
|
||||||
|
draw.rounded_rectangle([48, 48, width - 48, height - 48], radius=12, outline=(60, 60, 80), width=1)
|
||||||
|
|
||||||
|
# Badge Pill at top
|
||||||
|
badge_text = f"★ {mod['badge']} MODULE ★"
|
||||||
|
draw.rounded_rectangle([280, 70, 520, 102], radius=16, fill=gold)
|
||||||
|
draw.text((400, 86), badge_text, fill=c1, anchor="mm")
|
||||||
|
|
||||||
|
# Main Title
|
||||||
|
draw.text((400, 170), mod['title'], fill=(255, 255, 255), anchor="mm")
|
||||||
|
|
||||||
|
# Gold separator line
|
||||||
|
draw.line([250, 220, 550, 220], fill=gold, width=3)
|
||||||
|
|
||||||
|
# Subtitle
|
||||||
|
draw.text((400, 260), mod['subtitle'], fill=(210, 215, 230), anchor="mm")
|
||||||
|
|
||||||
|
# Footer attribution
|
||||||
|
draw.text((400, 330), "Community OS • Odoo 19 Edition", fill=gold, anchor="mm")
|
||||||
|
|
||||||
|
os.makedirs(target_dir, exist_ok=True)
|
||||||
|
banner_path = os.path.join(target_dir, 'banner.png')
|
||||||
|
im.save(banner_path, 'PNG')
|
||||||
|
print(f"Generated {banner_path}")
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
if os.path.exists('/mnt/extra-addons'):
|
||||||
|
root = '/mnt/extra-addons'
|
||||||
|
else:
|
||||||
|
root = os.path.abspath(os.path.join(os.path.dirname(__file__), '..', 'addons'))
|
||||||
|
for mod in MODULES:
|
||||||
|
desc_dir = os.path.join(root, mod['dir'], 'static', 'description')
|
||||||
|
generate_icon(mod, desc_dir)
|
||||||
|
generate_banner(mod, desc_dir)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
main()
|
||||||