From d05d80c4dff0bb1f78cb9a2d0940487a2656b12c Mon Sep 17 00:00:00 2001 From: metatroncubeswdev Date: Tue, 18 Aug 2026 00:53:17 -0400 Subject: [PATCH] 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 --- addons/tncsc_deployment/__init__.py | 11 ++ addons/tncsc_deployment/__manifest__.py | 12 +- addons/tncsc_deployment/data/ir_cron.xml | 23 +++ .../data/mail_template_overrides.xml | 59 +++++++ .../tncsc_deployment/data/membership_data.xml | 59 +++++++ .../data/res_company_data.xml | 21 +++ addons/tncsc_deployment/data/theme_data.xml | 26 +++ addons/tncsc_deployment/models/__init__.py | 1 + addons/tncsc_deployment/models/res_company.py | 105 ++++++++++++ .../tncsc_deployment/security/tncsc_roles.xml | 69 ++++++++ addons/tncsc_deployment/tests/__init__.py | 1 + .../tests/test_tncsc_deployment.py | 85 ++++++++++ .../tncsc_deployment/views/website_pages.xml | 160 ++++++++++++++++++ deploy/odoo.conf | 2 +- 14 files changed, 632 insertions(+), 2 deletions(-) create mode 100644 addons/tncsc_deployment/data/ir_cron.xml create mode 100644 addons/tncsc_deployment/data/mail_template_overrides.xml create mode 100644 addons/tncsc_deployment/data/membership_data.xml create mode 100644 addons/tncsc_deployment/data/res_company_data.xml create mode 100644 addons/tncsc_deployment/data/theme_data.xml create mode 100644 addons/tncsc_deployment/models/__init__.py create mode 100644 addons/tncsc_deployment/models/res_company.py create mode 100644 addons/tncsc_deployment/security/tncsc_roles.xml create mode 100644 addons/tncsc_deployment/tests/__init__.py create mode 100644 addons/tncsc_deployment/tests/test_tncsc_deployment.py create mode 100644 addons/tncsc_deployment/views/website_pages.xml diff --git a/addons/tncsc_deployment/__init__.py b/addons/tncsc_deployment/__init__.py index e69de29..7fb25fd 100644 --- a/addons/tncsc_deployment/__init__.py +++ b/addons/tncsc_deployment/__init__.py @@ -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() diff --git a/addons/tncsc_deployment/__manifest__.py b/addons/tncsc_deployment/__manifest__.py index 397417b..8c33aa2 100644 --- a/addons/tncsc_deployment/__manifest__.py +++ b/addons/tncsc_deployment/__manifest__.py @@ -34,9 +34,19 @@ that applies to the community_* product modules. 'community_benefits', 'community_interac', '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': [], 'application': False, 'installable': True, + 'post_init_hook': 'post_init_hook', } diff --git a/addons/tncsc_deployment/data/ir_cron.xml b/addons/tncsc_deployment/data/ir_cron.xml new file mode 100644 index 0000000..5c07fbe --- /dev/null +++ b/addons/tncsc_deployment/data/ir_cron.xml @@ -0,0 +1,23 @@ + + + + + + TNCSC: Verify Accounting Setup + + code + model.search([])._tncsc_setup_accounting() + 1 + days + + + + diff --git a/addons/tncsc_deployment/data/mail_template_overrides.xml b/addons/tncsc_deployment/data/mail_template_overrides.xml new file mode 100644 index 0000000..44e0993 --- /dev/null +++ b/addons/tncsc_deployment/data/mail_template_overrides.xml @@ -0,0 +1,59 @@ + + + + + + TNCSC membership renewal due | TNCSC உறுப்பினர் புதுப்பித்தல் தேவை + +
+

English

+

Dear Member,

+

+ Your Tamil Nadu Cultural Society of Canada membership + () + expires on . +

+

Please renew soon to keep your membership active and avoid a lapse in benefits.

+
+

தமிழ்

+

அன்புள்ள உறுப்பினர்,

+

+ கனடா தமிழ்நாடு பண்பாட்டு சங்கத்தில் உங்கள் உறுப்பினர் காலம் + அன்று + காலாவதியாகிறது. +

+

உங்கள் உறுப்பினர் தகுதியைத் தொடர தயவுசெய்து விரைவில் புதுப்பிக்கவும்.

+
+
+
+ + + Your TNCSC membership has expired | TNCSC உறுப்பினர் காலாவதியானது + +
+

English

+

Dear Member,

+

+ Your Tamil Nadu Cultural Society of Canada membership + () + expired on . +

+

A renewal invoice has been prepared for you. Please get in touch to reactivate your membership.

+
+

தமிழ்

+

அன்புள்ள உறுப்பினர்,

+

+ கனடா தமிழ்நாடு பண்பாட்டு சங்கத்தில் உங்கள் உறுப்பினர் தகுதி + அன்று + காலாவதியானது. +

+

உங்களுக்கான புதுப்பித்தல் விலைப்பட்டியல் தயாரிக்கப்பட்டுள்ளது. உறுப்பினர் தகுதியை மீண்டும் + பெற எங்களைத் தொடர்பு கொள்ளவும்.

+
+
+
+
diff --git a/addons/tncsc_deployment/data/membership_data.xml b/addons/tncsc_deployment/data/membership_data.xml new file mode 100644 index 0000000..3661f11 --- /dev/null +++ b/addons/tncsc_deployment/data/membership_data.xml @@ -0,0 +1,59 @@ + + + + + community_membership.org_name + Tamil Nadu Cultural Society of Canada + + + community_membership.member_id_format + TNCSC-{year}-{seq} + + + community_membership.renewal_offsets + 30,14,7 + + + + + Individual + IND + 50.0 + + annual + 10 + + + Family + FAM + 80.0 + + annual + 20 + + + Student + STU + 20.0 + + annual + 30 + + + Senior + SEN + 30.0 + + annual + 40 + + + Life + LIFE + 500.0 + + one_time + 50 + + diff --git a/addons/tncsc_deployment/data/res_company_data.xml b/addons/tncsc_deployment/data/res_company_data.xml new file mode 100644 index 0000000..558adfa --- /dev/null +++ b/addons/tncsc_deployment/data/res_company_data.xml @@ -0,0 +1,21 @@ + + + + True + + + + + Tamil Nadu Cultural Society of Canada + info@tncsc.example.org + https://www.tncsc.example.org + + + diff --git a/addons/tncsc_deployment/data/theme_data.xml b/addons/tncsc_deployment/data/theme_data.xml new file mode 100644 index 0000000..c9ee456 --- /dev/null +++ b/addons/tncsc_deployment/data/theme_data.xml @@ -0,0 +1,26 @@ + + + + + community_theme_base.primary_color + #05091E + + + community_theme_base.secondary_color + #F97316 + + + community_theme_base.accent_color + #0EA5FF + + + community_theme_base.heading_font + Inter, sans-serif + + + community_theme_base.body_font + Inter, sans-serif + + diff --git a/addons/tncsc_deployment/models/__init__.py b/addons/tncsc_deployment/models/__init__.py new file mode 100644 index 0000000..aff44f3 --- /dev/null +++ b/addons/tncsc_deployment/models/__init__.py @@ -0,0 +1 @@ +from . import res_company diff --git a/addons/tncsc_deployment/models/res_company.py b/addons/tncsc_deployment/models/res_company.py new file mode 100644 index 0000000..9601e95 --- /dev/null +++ b/addons/tncsc_deployment/models/res_company.py @@ -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'] diff --git a/addons/tncsc_deployment/security/tncsc_roles.xml b/addons/tncsc_deployment/security/tncsc_roles.xml new file mode 100644 index 0000000..c8b34de --- /dev/null +++ b/addons/tncsc_deployment/security/tncsc_roles.xml @@ -0,0 +1,69 @@ + + + + + + TNCSC Roles + 5 + + + + TNCSC Roles + + + + + Board Admin + + Full access across membership, school, classifieds, benefits, events, and Interac payments. + + + + + Treasurer + + Confirms Interac e-Transfer payments and manages accounting. + + + + + Events Officer + + Manages events and staffs the check-in desk. + + + + + School Coordinator + + Manages Tamil School terms, classes, students, and enrollment. + + + + + Teacher + + Takes attendance for their own Tamil School classes. + + + + + Classifieds Moderator + + Reviews and publishes classified listings. + + + diff --git a/addons/tncsc_deployment/tests/__init__.py b/addons/tncsc_deployment/tests/__init__.py new file mode 100644 index 0000000..8b559c8 --- /dev/null +++ b/addons/tncsc_deployment/tests/__init__.py @@ -0,0 +1 @@ +from . import test_tncsc_deployment diff --git a/addons/tncsc_deployment/tests/test_tncsc_deployment.py b/addons/tncsc_deployment/tests/test_tncsc_deployment.py new file mode 100644 index 0000000..7ae1894 --- /dev/null +++ b/addons/tncsc_deployment/tests/test_tncsc_deployment.py @@ -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, + ) diff --git a/addons/tncsc_deployment/views/website_pages.xml b/addons/tncsc_deployment/views/website_pages.xml new file mode 100644 index 0000000..a6e2904 --- /dev/null +++ b/addons/tncsc_deployment/views/website_pages.xml @@ -0,0 +1,160 @@ + + + + + + TNCSC Home + qweb + tncsc_deployment.page_home + + + +
+

Tamil Nadu Cultural Society of Canada

+

Celebrating Tamil language, arts, and culture in Canada.

+

Content to be finalized by TNCSC - this is a placeholder home page + generated by the deployment layer.

+ Register for Tamil School + Browse Classifieds +
+
+
+
+
+ + / + + True + + + + + TNCSC About + qweb + tncsc_deployment.page_about + + + +
+

About Us

+

Placeholder content - to be filled in with TNCSC's real history, mission, + and board information.

+
+
+
+
+
+ + /about + + True + + + + + TNCSC Tamil School + qweb + tncsc_deployment.page_tamil_school + + + +
+

Tamil School

+

Weekend Tamil language classes for children and youth. See current terms, + levels, and class schedules below.

+ Register +
+
+
+
+
+ + /tamil-school + + True + + + + + TNCSC Sponsors + qweb + tncsc_deployment.page_sponsors + + + +
+

Our Sponsors

+

Placeholder content - sponsor logos and tiers to be added.

+ See Member Benefits +
+
+
+
+
+ + /sponsors + + True + + + + + TNCSC Contact + qweb + tncsc_deployment.page_contact + + + +
+

Contact Us

+

Email:

+

Placeholder content - add a real address, phone number, and contact form.

+
+
+
+
+
+ + /contact + + True + + + + + About + /about + + 10 + + + Tamil School + /tamil-school + + 20 + + + Classifieds + /classifieds + + 30 + + + Benefits + /benefits + + 40 + + + Sponsors + /sponsors + + 50 + + + Contact + /contact + + 60 + +
diff --git a/deploy/odoo.conf b/deploy/odoo.conf index 544df98..8043337 100644 --- a/deploy/odoo.conf +++ b/deploy/odoo.conf @@ -1,5 +1,5 @@ [options] -admin_passwd = $pbkdf2-sha512$600000$l5KS8v6/977XOgcAIITwvg$F92iQue/QiuEv1Jgf8xefn.ikVIanCsopqLdhpWbFal1DnM7UqCRtZyPwFr4DsVL5hHEOuFcfkrRH5Wv6sDSuQ +admin_passwd = $pbkdf2-sha512$600000$h9A6h9Cac04pBaDUGoOwFg$FrvE9uDPOH6MSH6kkZvcRdsNDnnghqvWQtnjdSNGo8w0R139ubAGlyf.YVE3Zvd9Z9kTG5Twvg5yIjfgSIAV7w db_host = db db_port = 5432 db_user = odoo