feat(tncsc_deployment): TNCSC client configuration (Phase 7)

Data-only deployment layer seeding TNCSC's branding (navy/orange/electric-
blue from the plan), 5 membership tiers in CAD (Individual $50, Family $80,
Student $20, Senior $30, Life $500 - the plan's own example figures;
update via Settings once TNCSC confirms real pricing), the
'TNCSC-{year}-{seq}' member-ID format, a Canadian (Ontario/HST) chart of
accounts via l10n_ca plus non-profit-specific accounts (Membership Dues/
Event/Sponsorship/School Fees/Donations Revenue, Deferred Event Revenue
liability, a Stripe clearing account) and a Donations journal, bilingual
EN/Tamil overrides of two membership email templates (Tamil text is a
best-effort draft only, explicitly flagged as needing native-speaker
review before go-live), five placeholder website pages (Home/About/Tamil
School/Sponsors/Contact), and TNCSC-named role groups (Board Admin,
Treasurer, Events Officer, School Coordinator, Teacher, Classifieds
Moderator) that imply the existing generic product-layer groups rather
than defining new permission logic.

Chasing the chart-of-accounts setup down to a genuinely working state
took real digging: setting company.country_id on a brand-new company
auto-schedules this Odoo build's own chart-template installer via a
precommit hook (res.company.install_l10n_modules), which races an
explicit try_loading('ca_2023', ...) call made in the same install
transaction and silently replaces its result afterwards (reverting
currency to USD, chart_template to 'generic_coa', and deleting the custom
accounts) - confirmed via raw SQL checks that the correct state exists
right up until the post_init_hook transaction commits, and is gone by the
time the install process exits. Since the precommit auto-trigger only
ever fires once per company (guarded by chart_template being unset), a
second call from a separate transaction is immune to the race. The fix:
the actual setup logic lives in an idempotent res.company._tncsc_setup_
accounting() method (models/res_company.py - a narrow, documented
exception to "no models" in the deployment layer, since cramming this
into a sandboxed ir.cron code string wasn't practical), called as a
best-effort from post_init_hook and guaranteed by a daily safety-net cron
que runs in its own transaction. Also hit two smaller, separate bugs on
the way: ir.cron code execution forbids direct attribute assignment
(STORE_ATTR) in its sandbox, and Html config_parameter fields must not be
wrapped in CDATA in data XML.

Verified end-to-end on a genuinely fresh database (not the long-lived dev
DB, which already has posted entries and correctly refuses a currency
change): installed tncsc_deployment alone, pulling in all 8 product
modules plus l10n_ca as dependencies, confirmed the post-install state via
raw SQL, manually triggered the safety-net cron and confirmed it reached
the fully-correct state (CAD, ca_2023, 355 accounts including all 7
custom ones, the Donations journal, tier products linked to the dues
account), confirmed the cron is idempotent on a second run, and ran the
full test suite (9/9 passing) with the same deterministic setup called
from the test transaction directly.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
metatroncubeswdev 2026-08-18 00:53:17 -04:00
parent b94ee06d3a
commit d05d80c4df
14 changed files with 632 additions and 2 deletions

View File

@ -0,0 +1,11 @@
from . import models
def post_init_hook(env):
"""Best-effort immediate attempt at the accounting bootstrap. The daily
cron in data/ir_cron.xml (calling the same idempotent method from a
separate transaction) is what guarantees eventual correctness - see
models/res_company.py for why a single call from within this hook's own
transaction isn't reliable on this Odoo build.
"""
env.company._tncsc_setup_accounting()

View File

@ -34,9 +34,19 @@ that applies to the community_* product modules.
'community_benefits', 'community_benefits',
'community_interac', 'community_interac',
'community_portal', 'community_portal',
'l10n_ca',
],
'data': [
'security/tncsc_roles.xml',
'data/res_company_data.xml',
'data/theme_data.xml',
'data/membership_data.xml',
'data/mail_template_overrides.xml',
'data/ir_cron.xml',
'views/website_pages.xml',
], ],
'data': [],
'demo': [], 'demo': [],
'application': False, 'application': False,
'installable': True, 'installable': True,
'post_init_hook': 'post_init_hook',
} }

View File

@ -0,0 +1,23 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<data noupdate="1">
<!-- Safety net for a not-fully-isolated Odoo bootstrap quirk on fresh
databases: this Odoo build's own chart-template auto-install can
race with post_init_hook's explicit try_loading() call within
the same install transaction and silently replace its result
afterwards (see models/res_company.py for the full explanation).
A call from this cron's own separate transaction is immune to
that race. _tncsc_setup_accounting() is idempotent - it does
nothing once already correct - so running it daily forever is
harmless self-healing, not just a one-time fix. -->
<record id="ir_cron_tncsc_setup_accounting" model="ir.cron">
<field name="name">TNCSC: Verify Accounting Setup</field>
<field name="model_id" ref="base.model_res_company"/>
<field name="state">code</field>
<field name="code">model.search([])._tncsc_setup_accounting()</field>
<field name="interval_number">1</field>
<field name="interval_type">days</field>
<field name="active" eval="True"/>
</record>
</data>
</odoo>

View File

@ -0,0 +1,59 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo noupdate="1">
<!-- Bilingual (EN/Tamil) overrides of the community_membership templates,
demonstrating the pattern: TNCSC voice, org name, both languages.
The Tamil text below is a best-effort placeholder draft only - it
has NOT been reviewed by a native Tamil speaker and must be
corrected before this goes live. -->
<record id="community_membership.mail_template_membership_renewal_due" model="mail.template">
<field name="subject">TNCSC membership renewal due | 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>
Your Tamil Nadu Cultural Society of Canada membership
<t t-if="object.membership_member_id">(<t t-out="object.membership_member_id"/>)</t>
expires on <t t-out="format_date(object.membership_expiry)"/>.
</p>
<p>Please renew soon to keep your membership active and avoid a lapse in benefits.</p>
<hr/>
<p><strong>தமிழ்</strong></p>
<p>அன்புள்ள <t t-out="object.name or ''">உறுப்பினர்</t>,</p>
<p>
கனடா தமிழ்நாடு பண்பாட்டு சங்கத்தில் உங்கள் உறுப்பினர் காலம்
<t t-out="format_date(object.membership_expiry)"/> அன்று
காலாவதியாகிறது.
</p>
<p>உங்கள் உறுப்பினர் தகுதியைத் தொடர தயவுசெய்து விரைவில் புதுப்பிக்கவும்.</p>
</div>
</field>
</record>
<record id="community_membership.mail_template_membership_expired" model="mail.template">
<field name="subject">Your TNCSC membership has expired | 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>
Your Tamil Nadu Cultural Society of Canada membership
<t t-if="object.membership_member_id">(<t t-out="object.membership_member_id"/>)</t>
expired on <t t-out="format_date(object.membership_expiry)"/>.
</p>
<p>A renewal invoice has been prepared for you. Please get in touch to reactivate your membership.</p>
<hr/>
<p><strong>தமிழ்</strong></p>
<p>அன்புள்ள <t t-out="object.name or ''">உறுப்பினர்</t>,</p>
<p>
கனடா தமிழ்நாடு பண்பாட்டு சங்கத்தில் உங்கள் உறுப்பினர் தகுதி
<t t-out="format_date(object.membership_expiry)"/> அன்று
காலாவதியானது.
</p>
<p>உங்களுக்கான புதுப்பித்தல் விலைப்பட்டியல் தயாரிக்கப்பட்டுள்ளது. உறுப்பினர் தகுதியை மீண்டும்
பெற எங்களைத் தொடர்பு கொள்ளவும்.</p>
</div>
</field>
</record>
</odoo>

View File

@ -0,0 +1,59 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo noupdate="1">
<!-- Membership settings -->
<record id="config_parameter_membership_org_name" model="ir.config_parameter">
<field name="key">community_membership.org_name</field>
<field name="value">Tamil Nadu Cultural Society of Canada</field>
</record>
<record id="config_parameter_membership_id_format" model="ir.config_parameter">
<field name="key">community_membership.member_id_format</field>
<field name="value">TNCSC-{year}-{seq}</field>
</record>
<record id="config_parameter_membership_renewal_offsets" model="ir.config_parameter">
<field name="key">community_membership.renewal_offsets</field>
<field name="value">30,14,7</field>
</record>
<!-- Membership tiers (CAD) - figures from the implementation plan;
update via Settings once TNCSC confirms final pricing. -->
<record id="membership_tier_individual" model="community.membership.tier">
<field name="name">Individual</field>
<field name="code">IND</field>
<field name="price">50.0</field>
<field name="currency_id" ref="base.CAD"/>
<field name="period">annual</field>
<field name="sequence">10</field>
</record>
<record id="membership_tier_family" model="community.membership.tier">
<field name="name">Family</field>
<field name="code">FAM</field>
<field name="price">80.0</field>
<field name="currency_id" ref="base.CAD"/>
<field name="period">annual</field>
<field name="sequence">20</field>
</record>
<record id="membership_tier_student" model="community.membership.tier">
<field name="name">Student</field>
<field name="code">STU</field>
<field name="price">20.0</field>
<field name="currency_id" ref="base.CAD"/>
<field name="period">annual</field>
<field name="sequence">30</field>
</record>
<record id="membership_tier_senior" model="community.membership.tier">
<field name="name">Senior</field>
<field name="code">SEN</field>
<field name="price">30.0</field>
<field name="currency_id" ref="base.CAD"/>
<field name="period">annual</field>
<field name="sequence">40</field>
</record>
<record id="membership_tier_life" model="community.membership.tier">
<field name="name">Life</field>
<field name="code">LIFE</field>
<field name="price">500.0</field>
<field name="currency_id" ref="base.CAD"/>
<field name="period">one_time</field>
<field name="sequence">50</field>
</record>
</odoo>

View File

@ -0,0 +1,21 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record id="base.CAD" model="res.currency">
<field name="active">True</field>
</record>
<!-- country_id/state_id/currency_id are deliberately NOT set here: writing
country_id on a res.company with no chart_template yet auto-schedules
Odoo's own chart-template installer via a precommit hook
(install_l10n_modules), which races with this module's own explicit
try_loading('ca_2023', ...) call in post_init_hook and can leave the
company on the wrong currency. Those three fields are set together
with chart_template_load=True in post_init_hook instead, which
suppresses that auto-trigger so only our explicit load runs. -->
<record id="base.main_company" model="res.company">
<field name="name">Tamil Nadu Cultural Society of Canada</field>
<field name="email">info@tncsc.example.org</field>
<field name="website">https://www.tncsc.example.org</field>
</record>
</odoo>

View File

@ -0,0 +1,26 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo noupdate="1">
<!-- Colours from the implementation plan. Logo is intentionally left
unset here - upload the real TNCSC logo via Settings > Theme once
the asset is available; there is no placeholder image shipped. -->
<record id="config_parameter_theme_primary" model="ir.config_parameter">
<field name="key">community_theme_base.primary_color</field>
<field name="value">#05091E</field>
</record>
<record id="config_parameter_theme_secondary" model="ir.config_parameter">
<field name="key">community_theme_base.secondary_color</field>
<field name="value">#F97316</field>
</record>
<record id="config_parameter_theme_accent" model="ir.config_parameter">
<field name="key">community_theme_base.accent_color</field>
<field name="value">#0EA5FF</field>
</record>
<record id="config_parameter_theme_heading_font" model="ir.config_parameter">
<field name="key">community_theme_base.heading_font</field>
<field name="value">Inter, sans-serif</field>
</record>
<record id="config_parameter_theme_body_font" model="ir.config_parameter">
<field name="key">community_theme_base.body_font</field>
<field name="value">Inter, sans-serif</field>
</record>
</odoo>

View File

@ -0,0 +1 @@
from . import res_company

View File

@ -0,0 +1,105 @@
from odoo import models
CUSTOM_ACCOUNT_CODE_START = 800001
REVENUE_ACCOUNT_SPECS = [
('Membership Dues Revenue', 'income'),
('Event Revenue', 'income'),
('Sponsorship Revenue', 'income'),
('School Fees Revenue', 'income'),
('Donations Revenue', 'income_other'),
]
class ResCompany(models.Model):
"""Deliberate, narrow exception to "no models" in the deployment layer:
this is bootstrap-only glue (idempotent, TNCSC-specific, never called by
any product module), not a reusable feature. It exists because this
Odoo build's own chart-template auto-install (triggered the first time a
fresh company's country_id is set, before this module's data even loads)
races with an explicit try_loading() call made in the same install
transaction, and can silently replace it afterwards. A second call from
a separate transaction (the daily cron in data/ir_cron.xml) is immune to
that race, since the auto-trigger only fires once, for a company that
has never had a chart_template set at all.
"""
_inherit = 'res.company'
def _tncsc_setup_accounting(self):
for company in self:
company._tncsc_fix_chart_template()
company._tncsc_create_custom_accounts()
def _tncsc_fix_chart_template(self):
self.ensure_one()
env = self.env
cad = env.ref('base.CAD')
if self.currency_id == cad and self.chart_template == 'ca_2023':
return
self.with_context(chart_template_load=True).write({
'currency_id': cad.id,
'country_id': env.ref('base.ca').id,
'state_id': env.ref('base.state_ca_on').id,
})
env['account.chart.template'].try_loading('ca_2023', self, install_demo=False)
if self.currency_id != cad:
self.currency_id = cad
def _tncsc_create_custom_accounts(self):
self.ensure_one()
env = self.env
Account = env['account.account']
if Account.search_count([('company_ids', 'in', self.id), ('name', '=', 'Membership Dues Revenue')]):
return
def _next_free_code(start):
code = start
while Account.search_count([('company_ids', 'in', self.id), ('code', '=', str(code))]):
code += 1
return str(code)
accounts = {}
next_code = CUSTOM_ACCOUNT_CODE_START
for name, account_type in REVENUE_ACCOUNT_SPECS:
code = _next_free_code(next_code)
next_code = int(code) + 1
accounts[name] = Account.create({
'name': name,
'code': code,
'account_type': account_type,
'company_ids': [(6, 0, [self.id])],
})
deferred_code = _next_free_code(next_code)
next_code = int(deferred_code) + 1
Account.create({
'name': 'Deferred Event Revenue',
'code': deferred_code,
'account_type': 'liability_current',
'company_ids': [(6, 0, [self.id])],
})
stripe_code = _next_free_code(next_code)
Account.create({
'name': 'Stripe Clearing Account',
'code': stripe_code,
'account_type': 'asset_current',
'company_ids': [(6, 0, [self.id])],
})
if not env['account.journal'].search_count([('code', '=', 'DON'), ('company_id', '=', self.id)]):
env['account.journal'].create({
'name': 'Donations',
'code': 'DON',
'type': 'sale',
'company_id': self.id,
'default_account_id': accounts['Donations Revenue'].id,
})
tiers = env['community.membership.tier'].search([])
for tier in tiers:
if tier.product_id:
tier.product_id.property_account_income_id = accounts['Membership Dues Revenue']

View File

@ -0,0 +1,69 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo noupdate="1">
<!-- TNCSC-branded role names. Each is a thin wrapper implying the
generic product-layer groups - the actual access control logic
lives entirely in the community_* modules; these just give board
members role labels they recognize in Settings > Users. -->
<record id="module_category_tncsc_roles" model="ir.module.category">
<field name="name">TNCSC Roles</field>
<field name="sequence">5</field>
</record>
<record id="privilege_tncsc_roles" model="res.groups.privilege">
<field name="name">TNCSC Roles</field>
<field name="category_id" ref="module_category_tncsc_roles"/>
</record>
<record id="group_tncsc_board_admin" model="res.groups">
<field name="name">Board Admin</field>
<field name="privilege_id" ref="privilege_tncsc_roles"/>
<field name="comment">Full access across membership, school, classifieds, benefits, events, and Interac payments.</field>
<field name="implied_ids" eval="[
(4, ref('community_membership.group_membership_manager')),
(4, ref('community_school.group_school_coordinator')),
(4, ref('community_classifieds.group_classifieds_moderator')),
(4, ref('community_benefits.group_benefits_manager')),
(4, ref('community_interac.group_interac_verifier')),
(4, ref('event.group_event_manager')),
]"/>
</record>
<record id="group_tncsc_treasurer" model="res.groups">
<field name="name">Treasurer</field>
<field name="privilege_id" ref="privilege_tncsc_roles"/>
<field name="comment">Confirms Interac e-Transfer payments and manages accounting.</field>
<field name="implied_ids" eval="[
(4, ref('community_interac.group_interac_verifier')),
(4, ref('account.group_account_invoice')),
]"/>
</record>
<record id="group_tncsc_events_officer" model="res.groups">
<field name="name">Events Officer</field>
<field name="privilege_id" ref="privilege_tncsc_roles"/>
<field name="comment">Manages events and staffs the check-in desk.</field>
<field name="implied_ids" eval="[(4, ref('event.group_event_manager'))]"/>
</record>
<record id="group_tncsc_school_coordinator" model="res.groups">
<field name="name">School Coordinator</field>
<field name="privilege_id" ref="privilege_tncsc_roles"/>
<field name="comment">Manages Tamil School terms, classes, students, and enrollment.</field>
<field name="implied_ids" eval="[(4, ref('community_school.group_school_coordinator'))]"/>
</record>
<record id="group_tncsc_teacher" model="res.groups">
<field name="name">Teacher</field>
<field name="privilege_id" ref="privilege_tncsc_roles"/>
<field name="comment">Takes attendance for their own Tamil School classes.</field>
<field name="implied_ids" eval="[(4, ref('community_school.group_school_teacher'))]"/>
</record>
<record id="group_tncsc_classifieds_moderator" model="res.groups">
<field name="name">Classifieds Moderator</field>
<field name="privilege_id" ref="privilege_tncsc_roles"/>
<field name="comment">Reviews and publishes classified listings.</field>
<field name="implied_ids" eval="[(4, ref('community_classifieds.group_classifieds_moderator'))]"/>
</record>
</odoo>

View File

@ -0,0 +1 @@
from . import test_tncsc_deployment

View File

@ -0,0 +1,85 @@
from odoo.tests.common import TransactionCase, tagged
@tagged('post_install', '-at_install')
class TestTncscDeployment(TransactionCase):
def setUp(self):
super().setUp()
# Deterministic setup for testing: this Odoo build's own chart-
# template auto-install can race with post_init_hook's call to this
# same method within the install transaction (see
# models/res_company.py) - calling it again here, synchronously,
# from the test's own transaction avoids the test suite being
# sensitive to that race. The method is idempotent either way.
self.env.company._tncsc_setup_accounting()
def test_branding_config_seeded(self):
get_param = self.env['ir.config_parameter'].sudo().get_param
self.assertEqual(get_param('community_theme_base.primary_color'), '#05091E')
self.assertEqual(get_param('community_theme_base.secondary_color'), '#F97316')
self.assertEqual(get_param('community_theme_base.accent_color'), '#0EA5FF')
def test_membership_settings_seeded(self):
get_param = self.env['ir.config_parameter'].sudo().get_param
self.assertEqual(get_param('community_membership.org_name'), 'Tamil Nadu Cultural Society of Canada')
self.assertEqual(get_param('community_membership.member_id_format'), 'TNCSC-{year}-{seq}')
def test_five_membership_tiers_seeded(self):
tiers = self.env['community.membership.tier'].search([])
self.assertEqual(len(tiers), 5)
by_code = {tier.code: tier for tier in tiers}
self.assertEqual(by_code['IND'].price, 50.0)
self.assertEqual(by_code['FAM'].price, 80.0)
self.assertEqual(by_code['STU'].price, 20.0)
self.assertEqual(by_code['SEN'].price, 30.0)
self.assertEqual(by_code['LIFE'].price, 500.0)
for tier in tiers:
self.assertEqual(tier.currency_id.name, 'CAD')
self.assertTrue(tier.product_id, "Every seeded tier should still auto-create its product")
def test_chart_of_accounts_loaded(self):
company = self.env.company
self.assertEqual(company.currency_id.name, 'CAD')
self.assertEqual(company.country_id.code, 'CA')
# The Canadian template seeds thousands of accounts; a small sanity count is enough.
account_count = self.env['account.account'].search_count([('company_ids', 'in', company.id)])
self.assertGreater(account_count, 100)
def test_custom_nonprofit_accounts_created(self):
Account = self.env['account.account']
names = {'Membership Dues Revenue', 'Event Revenue', 'Sponsorship Revenue',
'School Fees Revenue', 'Donations Revenue', 'Deferred Event Revenue',
'Stripe Clearing Account'}
for name in names:
account = Account.search([('name', '=', name)], limit=1)
self.assertTrue(account, f"Expected custom account '{name}' to exist")
def test_donations_journal_created(self):
journal = self.env['account.journal'].search([('code', '=', 'DON')], limit=1)
self.assertTrue(journal)
self.assertEqual(journal.type, 'sale')
def test_membership_tier_products_use_dues_account(self):
dues_account = self.env['account.account'].search([('name', '=', 'Membership Dues Revenue')], limit=1)
tiers = self.env['community.membership.tier'].search([])
for tier in tiers:
self.assertEqual(tier.product_id.property_account_income_id, dues_account)
def test_website_pages_published(self):
for url in ('/', '/about', '/tamil-school', '/sponsors', '/contact'):
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_tncsc_role_groups_exist_and_imply_product_groups(self):
board_admin = self.env.ref('tncsc_deployment.group_tncsc_board_admin')
self.assertIn(
self.env.ref('community_membership.group_membership_manager'),
board_admin.implied_ids,
)
treasurer = self.env.ref('tncsc_deployment.group_tncsc_treasurer')
self.assertIn(
self.env.ref('community_interac.group_interac_verifier'),
treasurer.implied_ids,
)

View File

@ -0,0 +1,160 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo noupdate="1">
<!-- Home -->
<record id="view_page_home" model="ir.ui.view">
<field name="name">TNCSC Home</field>
<field name="type">qweb</field>
<field name="key">tncsc_deployment.page_home</field>
<field name="arch" type="xml">
<t t-name="tncsc_deployment.page_home">
<t t-call="website.layout">
<div class="container" style="margin-top: 40px; margin-bottom: 60px;">
<h1>Tamil Nadu Cultural Society of Canada</h1>
<p class="lead">Celebrating Tamil language, arts, and culture in Canada.</p>
<p>Content to be finalized by TNCSC - this is a placeholder home page
generated by the deployment layer.</p>
<a href="/school/register" class="btn btn-primary me-2">Register for Tamil School</a>
<a href="/classifieds" class="btn btn-outline-secondary">Browse Classifieds</a>
</div>
</t>
</t>
</field>
</record>
<record id="page_home" model="website.page">
<field name="url">/</field>
<field name="view_id" ref="view_page_home"/>
<field name="is_published">True</field>
</record>
<!-- About -->
<record id="view_page_about" model="ir.ui.view">
<field name="name">TNCSC About</field>
<field name="type">qweb</field>
<field name="key">tncsc_deployment.page_about</field>
<field name="arch" type="xml">
<t t-name="tncsc_deployment.page_about">
<t t-call="website.layout">
<div class="container" style="margin-top: 40px; margin-bottom: 60px;">
<h1>About Us</h1>
<p>Placeholder content - to be filled in with TNCSC's real history, mission,
and board information.</p>
</div>
</t>
</t>
</field>
</record>
<record id="page_about" model="website.page">
<field name="url">/about</field>
<field name="view_id" ref="view_page_about"/>
<field name="is_published">True</field>
</record>
<!-- Tamil School -->
<record id="view_page_tamil_school" model="ir.ui.view">
<field name="name">TNCSC Tamil School</field>
<field name="type">qweb</field>
<field name="key">tncsc_deployment.page_tamil_school</field>
<field name="arch" type="xml">
<t t-name="tncsc_deployment.page_tamil_school">
<t t-call="website.layout">
<div class="container" style="margin-top: 40px; margin-bottom: 60px;">
<h1>Tamil School</h1>
<p>Weekend Tamil language classes for children and youth. See current terms,
levels, and class schedules below.</p>
<a href="/school/register" class="btn btn-primary">Register</a>
</div>
</t>
</t>
</field>
</record>
<record id="page_tamil_school" model="website.page">
<field name="url">/tamil-school</field>
<field name="view_id" ref="view_page_tamil_school"/>
<field name="is_published">True</field>
</record>
<!-- Sponsors -->
<record id="view_page_sponsors" model="ir.ui.view">
<field name="name">TNCSC Sponsors</field>
<field name="type">qweb</field>
<field name="key">tncsc_deployment.page_sponsors</field>
<field name="arch" type="xml">
<t t-name="tncsc_deployment.page_sponsors">
<t t-call="website.layout">
<div class="container" style="margin-top: 40px; margin-bottom: 60px;">
<h1>Our Sponsors</h1>
<p>Placeholder content - sponsor logos and tiers to be added.</p>
<a href="/benefits" class="btn btn-outline-secondary">See Member Benefits</a>
</div>
</t>
</t>
</field>
</record>
<record id="page_sponsors" model="website.page">
<field name="url">/sponsors</field>
<field name="view_id" ref="view_page_sponsors"/>
<field name="is_published">True</field>
</record>
<!-- Contact -->
<record id="view_page_contact" model="ir.ui.view">
<field name="name">TNCSC Contact</field>
<field name="type">qweb</field>
<field name="key">tncsc_deployment.page_contact</field>
<field name="arch" type="xml">
<t t-name="tncsc_deployment.page_contact">
<t t-call="website.layout">
<div class="container" style="margin-top: 40px; margin-bottom: 60px;">
<h1>Contact Us</h1>
<p>Email: <t t-out="env.company.email or ''"/></p>
<p>Placeholder content - add a real address, phone number, and contact form.</p>
</div>
</t>
</t>
</field>
</record>
<record id="page_contact" model="website.page">
<field name="url">/contact</field>
<field name="view_id" ref="view_page_contact"/>
<field name="is_published">True</field>
</record>
<!-- Main nav menu entries -->
<record id="menu_about" model="website.menu">
<field name="name">About</field>
<field name="url">/about</field>
<field name="parent_id" ref="website.main_menu"/>
<field name="sequence">10</field>
</record>
<record id="menu_tamil_school" model="website.menu">
<field name="name">Tamil School</field>
<field name="url">/tamil-school</field>
<field name="parent_id" ref="website.main_menu"/>
<field name="sequence">20</field>
</record>
<record id="menu_classifieds" model="website.menu">
<field name="name">Classifieds</field>
<field name="url">/classifieds</field>
<field name="parent_id" ref="website.main_menu"/>
<field name="sequence">30</field>
</record>
<record id="menu_benefits" model="website.menu">
<field name="name">Benefits</field>
<field name="url">/benefits</field>
<field name="parent_id" ref="website.main_menu"/>
<field name="sequence">40</field>
</record>
<record id="menu_sponsors" model="website.menu">
<field name="name">Sponsors</field>
<field name="url">/sponsors</field>
<field name="parent_id" ref="website.main_menu"/>
<field name="sequence">50</field>
</record>
<record id="menu_contact" model="website.menu">
<field name="name">Contact</field>
<field name="url">/contact</field>
<field name="parent_id" ref="website.main_menu"/>
<field name="sequence">60</field>
</record>
</odoo>

View File

@ -1,5 +1,5 @@
[options] [options]
admin_passwd = $pbkdf2-sha512$600000$l5KS8v6/977XOgcAIITwvg$F92iQue/QiuEv1Jgf8xefn.ikVIanCsopqLdhpWbFal1DnM7UqCRtZyPwFr4DsVL5hHEOuFcfkrRH5Wv6sDSuQ admin_passwd = $pbkdf2-sha512$600000$h9A6h9Cac04pBaDUGoOwFg$FrvE9uDPOH6MSH6kkZvcRdsNDnnghqvWQtnjdSNGo8w0R139ubAGlyf.YVE3Zvd9Z9kTG5Twvg5yIjfgSIAV7w
db_host = db db_host = db
db_port = 5432 db_port = 5432
db_user = odoo db_user = odoo