From c85a9bbf82a289a6e84fcb55e805338494d96b59 Mon Sep 17 00:00:00 2001 From: gsinghpal Date: Tue, 12 May 2026 18:05:33 -0400 Subject: [PATCH] docs: nexa_coa_setup implementation plan Bite-sized task plan to implement the CoA design against odoo-nexa nexamain database. 12 phases: 0. Safety backup + staging clone 1. Module skeleton (nexa_coa_setup) 2. Chart of accounts (~110 new accounts across 1-6xxxxx) 3. Analytic plans (Project, Department, SR&ED Tag) 4. Hooks for archive-unused / rename-legacy 5. Tax cleanup 6. 8 fiscal positions with auto-detect 7. Service product categories 8. Westin/Divine partner records (RP-Associated tag) 9. 8 bank reconciliation rules 10. End-to-end test invoices (ON, US, intercompany) 11. Apply to production (with explicit GO/NO-GO gate) 12. Operating runbook Each task has a verify-before / change / verify-after / commit cycle. Staging clone (nexamain_staging) used for every phase before prod. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../plans/2026-05-12-nexa-coa-setup.md | 2797 +++++++++++++++++ 1 file changed, 2797 insertions(+) create mode 100644 docs/superpowers/plans/2026-05-12-nexa-coa-setup.md diff --git a/docs/superpowers/plans/2026-05-12-nexa-coa-setup.md b/docs/superpowers/plans/2026-05-12-nexa-coa-setup.md new file mode 100644 index 00000000..4d6aa531 --- /dev/null +++ b/docs/superpowers/plans/2026-05-12-nexa-coa-setup.md @@ -0,0 +1,2797 @@ +# Nexa Systems — Chart of Accounts Setup Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Implement the CoA design in `docs/superpowers/specs/2026-05-12-nexa-coa-design.md` against the live odoo-nexa `nexamain` database — clean account structure, automated taxes, SR&ED-ready analytic plans, intercompany-aware partner setup. + +**Architecture:** Build a new Odoo module `nexa_coa_setup` that declares new accounts/taxes/fiscal-positions/analytic-plans via XML data files (declarative, idempotent, version-controlled), and uses post-install Python hooks for imperative one-shot operations (archive unused accounts, rename legacy accounts, normalize HST#, lock fiscal year). Apply to nexamain via standard `odoo -i nexa_coa_setup`. Take a pg_dump before any destructive step. + +**Tech Stack:** +- Odoo 19 Enterprise on odoo-nexa (192.168.1.111), Docker container `odoo-nexa-app`, DB `nexamain` +- Postgres 16 (pgvector image) on container `odoo-nexa-db` +- Python 3.12 (Odoo's runtime) +- l10n_ca localization (already loaded — we extend, not replace) +- Local dev: `/Users/gurpreet/Github/Odoo-Modules/nexa_coa_setup/` +- Server addons path: `/opt/odoo/custom-addons/` (bind-mounted as `/mnt/extra-addons` in container) +- Deploy: `rsync` from Mac → odoo-nexa, then `docker exec ... odoo -u nexa_coa_setup` + +**Safety rules (read before every destructive step):** +1. **Always** pg_dump before any phase that archives, renames, or locks data +2. **Never** delete an account that has postings — archive (`active=False`) only +3. **Lock period BEFORE archive sweep** so prior years can't be retroactively damaged +4. **Test on a staging clone first** for any phase that touches >50 records +5. **Each phase commits independently** — no batch commits across phases + +--- + +## File Structure + +Create the following under `/Users/gurpreet/Github/Odoo-Modules/nexa_coa_setup/`: + +``` +nexa_coa_setup/ +├── __init__.py # imports models, hooks +├── __manifest__.py # module metadata + data file list +├── hooks.py # post_init_hook for imperative operations +├── data/ +│ ├── 01_account_account.xml # New chart of accounts entries (~70 accounts) +│ ├── 02_account_journal.xml # New journals (EXP if missing) +│ ├── 03_account_tax.xml # Curated tax set (additions if any) +│ ├── 04_account_fiscal_position.xml # 8 fiscal positions +│ ├── 05_account_analytic_plan.xml # Project / Department / SR&ED Tag plans +│ ├── 06_account_analytic_account.xml # Seed analytic accounts (departments, SR&ED tags) +│ ├── 07_product_category.xml # Service product categories +│ ├── 08_res_partner_category.xml # 'RP-Associated' partner tag +│ ├── 09_res_partner.xml # Westin & Divine partner records +│ └── 10_account_reconcile_model.xml # Bank reconciliation rules +├── models/ +│ └── __init__.py # (no custom models needed; placeholder) +├── security/ +│ └── ir.model.access.csv # (empty; no new models) +└── README.md # operating runbook +``` + +**Each XML data file**: +- Uses `` so re-running `-u` updates them (no `noupdate="1"`) +- Uses stable XMLIDs prefixed `nexa_coa_setup.` so future updates can find records +- Sets `forcecreate="True"` where appropriate + +**hooks.py responsibilities** (idempotent — safe to re-run): +- `pre_init_hook(env)`: pg_dump verification reminder; safety bail-out if pre-conditions not met +- `post_init_hook(env)`: normalize HST# format, archive unused l10n_ca accounts, rename legacy 14xx/15xx accounts, lock fiscal year at 2025-12-31 + +--- + +## Phase 0 — Safety, Backup, and Staging Clone + +### Task 0.1: Take a full pg_dump of nexamain BEFORE anything + +**Files:** none + +- [ ] **Step 1: Verify the database is reachable and quiet** + +Run from local Mac: +```bash +ssh odoo-nexa "docker exec odoo-nexa-db psql -U odoo -d nexamain -c 'SELECT count(*) FROM account_move;'" +``` +Expected: a single number around `776` (the current entry count). + +- [ ] **Step 2: pg_dump to a timestamped file on the server** + +Run: +```bash +ssh odoo-nexa "docker exec odoo-nexa-db pg_dump -U odoo -d nexamain -F c -Z 9 -f /tmp/nexamain_pre_coa_$(date +%Y%m%d_%H%M%S).dump && ls -lh /tmp/nexamain_pre_coa_*.dump | tail -1" +``` +Expected: file size in the tens of MB. + +- [ ] **Step 3: Copy the dump to your Mac for off-server safety** + +Run: +```bash +mkdir -p ~/Backups/odoo-nexa && scp odoo-nexa:/tmp/nexamain_pre_coa_*.dump ~/Backups/odoo-nexa/ && ls -lh ~/Backups/odoo-nexa/ | tail -1 +``` +Expected: the dump file on local Mac. + +- [ ] **Step 4: Document backup location in a NOTE file (one line)** + +Run: +```bash +echo "$(date -u +%Y-%m-%dT%H:%M:%SZ) — pg_dump before nexa_coa_setup install — $(ls -1 ~/Backups/odoo-nexa/nexamain_pre_coa_*.dump | tail -1)" >> ~/Backups/odoo-nexa/RESTORE_LOG.md +cat ~/Backups/odoo-nexa/RESTORE_LOG.md +``` +Expected: log entry visible. + +### Task 0.2: Create a staging clone for dry-run testing + +**Files:** none — creates DB `nexamain_staging` + +- [ ] **Step 1: Drop any existing staging DB and create fresh from dump** + +Run: +```bash +ssh odoo-nexa "docker exec odoo-nexa-db psql -U odoo -d postgres -c 'DROP DATABASE IF EXISTS nexamain_staging;' && docker exec odoo-nexa-db psql -U odoo -d postgres -c 'CREATE DATABASE nexamain_staging OWNER odoo;'" +``` +Expected: `DROP DATABASE` then `CREATE DATABASE` confirmations. + +- [ ] **Step 2: Restore the dump into staging** + +Run: +```bash +ssh odoo-nexa "DUMP=\$(ls -1t /tmp/nexamain_pre_coa_*.dump | head -1); docker exec odoo-nexa-db pg_restore -U odoo -d nexamain_staging -j 4 \$DUMP 2>&1 | tail -20" +``` +Expected: restore completes; some "constraint already exists" warnings are OK. + +- [ ] **Step 3: Verify staging matches prod row counts** + +Run: +```bash +ssh odoo-nexa "docker exec odoo-nexa-db psql -U odoo -d nexamain_staging -c 'SELECT (SELECT count(*) FROM account_account) AS accounts, (SELECT count(*) FROM account_move) AS moves, (SELECT count(*) FROM account_tax) AS taxes;'" +``` +Expected: same counts as prod (~426 accounts, ~776 moves, ~49 active taxes). + +- [ ] **Step 4: Set staging fiscalyear_lock_date to match prod (none) and commit nothing — staging is a working copy** + +No git commit; this is operational state. + +--- + +## Phase 1 — Module Skeleton + +### Task 1.1: Create the `nexa_coa_setup` module skeleton + +**Files:** +- Create: `nexa_coa_setup/__init__.py` +- Create: `nexa_coa_setup/__manifest__.py` +- Create: `nexa_coa_setup/hooks.py` +- Create: `nexa_coa_setup/models/__init__.py` +- Create: `nexa_coa_setup/security/ir.model.access.csv` +- Create: `nexa_coa_setup/README.md` + +- [ ] **Step 1: Create module directory tree** + +Run from local Mac: +```bash +cd /Users/gurpreet/Github/Odoo-Modules +mkdir -p nexa_coa_setup/data nexa_coa_setup/models nexa_coa_setup/security +ls nexa_coa_setup/ +``` +Expected: `data/`, `models/`, `security/`. + +- [ ] **Step 2: Write `__init__.py`** + +Create file `/Users/gurpreet/Github/Odoo-Modules/nexa_coa_setup/__init__.py` with content: +```python +from . import models +from .hooks import post_init_hook +``` + +- [ ] **Step 3: Write `__manifest__.py`** + +Create file `/Users/gurpreet/Github/Odoo-Modules/nexa_coa_setup/__manifest__.py` with content: +```python +# -*- coding: utf-8 -*- +# Copyright 2026 Nexa Systems Inc. +# License OPL-1 +{ + "name": "Nexa Systems — Chart of Accounts Setup", + "version": "19.0.1.0.0", + "category": "Accounting/Localizations/Chart of Accounts", + "summary": "Custom CoA, taxes, fiscal positions, analytic plans, and intercompany partner setup for Nexa Systems Inc.", + "author": "Nexa Systems Inc.", + "website": "https://nexasystems.ca", + "license": "OPL-1", + "depends": [ + "account", + "account_accountant", + "l10n_ca", + "analytic", + "sale_management", + "purchase", + "sale_subscription", + ], + "data": [ + "security/ir.model.access.csv", + "data/01_account_account.xml", + "data/02_account_journal.xml", + "data/03_account_tax.xml", + "data/04_account_fiscal_position.xml", + "data/05_account_analytic_plan.xml", + "data/06_account_analytic_account.xml", + "data/07_product_category.xml", + "data/08_res_partner_category.xml", + "data/09_res_partner.xml", + "data/10_account_reconcile_model.xml", + ], + "post_init_hook": "post_init_hook", + "installable": True, + "application": False, + "auto_install": False, +} +``` + +- [ ] **Step 4: Write `models/__init__.py`** (empty placeholder) + +Create file `/Users/gurpreet/Github/Odoo-Modules/nexa_coa_setup/models/__init__.py` with content: +```python +# no custom models — placeholder for future extensions +``` + +- [ ] **Step 5: Write `security/ir.model.access.csv`** (header only — no new models) + +Create file `/Users/gurpreet/Github/Odoo-Modules/nexa_coa_setup/security/ir.model.access.csv` with content: +```csv +id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink +``` + +- [ ] **Step 6: Write `hooks.py` with empty post_init_hook (filled in Phase 4)** + +Create file `/Users/gurpreet/Github/Odoo-Modules/nexa_coa_setup/hooks.py` with content: +```python +# -*- coding: utf-8 -*- +import logging + +_logger = logging.getLogger(__name__) + + +def post_init_hook(env): + """Imperative one-shot operations after module data is loaded. + + Each helper is idempotent — safe to re-run on -u. + """ + _logger.info("nexa_coa_setup: post_init_hook starting") + _normalize_company_hst_number(env) + _archive_unused_l10n_ca_accounts(env) + _rename_legacy_accounts(env) + _lock_fiscal_year_2025(env) + _logger.info("nexa_coa_setup: post_init_hook complete") + + +def _normalize_company_hst_number(env): + """Convert '741224877' to '741224877 RT0001' if not already in full form.""" + company = env.ref("base.main_company", raise_if_not_found=False) + if not company: + return + vat = (company.partner_id.vat or "").strip() + if vat == "741224877": + company.partner_id.vat = "741224877 RT0001" + _logger.info("nexa_coa_setup: normalized HST# to '741224877 RT0001'") + + +def _archive_unused_l10n_ca_accounts(env): + """Stub — filled in Phase 4. Archives ~370 unused accounts.""" + pass + + +def _rename_legacy_accounts(env): + """Stub — filled in Phase 4. Renames the 14xx/15xx legacy accounts.""" + pass + + +def _lock_fiscal_year_2025(env): + """Set fiscalyear_lock_date = 2025-12-31 on main company.""" + from datetime import date + company = env.ref("base.main_company", raise_if_not_found=False) + if not company: + return + target = date(2025, 12, 31) + if not company.fiscalyear_lock_date or company.fiscalyear_lock_date < target: + company.fiscalyear_lock_date = target + _logger.info("nexa_coa_setup: fiscalyear_lock_date set to 2025-12-31") +``` + +- [ ] **Step 7: Write minimal `README.md`** + +Create file `/Users/gurpreet/Github/Odoo-Modules/nexa_coa_setup/README.md` with content: +```markdown +# Nexa Systems — Chart of Accounts Setup + +Custom Odoo 19 module that configures the chart of accounts, taxes, +fiscal positions, analytic plans, and partner records for Nexa Systems Inc. + +## Install + +``` +docker exec odoo-nexa-app odoo -c /etc/odoo/odoo.conf -d nexamain \ + -i nexa_coa_setup --no-http --stop-after-init +``` + +## Update + +``` +docker exec odoo-nexa-app odoo -c /etc/odoo/odoo.conf -d nexamain \ + -u nexa_coa_setup --no-http --stop-after-init +``` + +## Design reference + +See `docs/superpowers/specs/2026-05-12-nexa-coa-design.md`. + +## Safety + +Always take a pg_dump BEFORE running `-i` or `-u`. See `docs/superpowers/plans/2026-05-12-nexa-coa-setup.md` Phase 0. +``` + +- [ ] **Step 8: Create empty data files (placeholders, filled in later phases)** + +Run: +```bash +cd /Users/gurpreet/Github/Odoo-Modules/nexa_coa_setup/data +for f in 01_account_account 02_account_journal 03_account_tax 04_account_fiscal_position \ + 05_account_analytic_plan 06_account_analytic_account 07_product_category \ + 08_res_partner_category 09_res_partner 10_account_reconcile_model; do + printf '\n\n \n \n\n' > ${f}.xml +done +ls -la +``` +Expected: 10 empty XML stub files. + +- [ ] **Step 9: Commit the skeleton** + +Run: +```bash +cd /Users/gurpreet/Github/Odoo-Modules +git add nexa_coa_setup/ +git commit -m "feat(nexa_coa_setup): module skeleton with hooks stub" +``` + +### Task 1.2: Deploy skeleton to odoo-nexa and install + +**Files:** none (deploys to server) + +- [ ] **Step 1: rsync the module to the server** + +Run from Mac: +```bash +rsync -avz --delete /Users/gurpreet/Github/Odoo-Modules/nexa_coa_setup/ odoo-nexa:/opt/odoo/custom-addons/nexa_coa_setup/ +``` +Expected: rsync output showing file transfers. + +- [ ] **Step 2: Install on staging first** + +Run: +```bash +ssh odoo-nexa "docker exec odoo-nexa-app odoo -c /etc/odoo/odoo.conf -d nexamain_staging -i nexa_coa_setup --no-http --stop-after-init 2>&1 | tail -20" +``` +Expected: `Modules loaded.` line at end, no traceback. + +- [ ] **Step 3: Verify install on staging** + +Run: +```bash +ssh odoo-nexa "docker exec odoo-nexa-db psql -U odoo -d nexamain_staging -c \"SELECT name, state FROM ir_module_module WHERE name = 'nexa_coa_setup';\"" +``` +Expected: `nexa_coa_setup | installed`. + +- [ ] **Step 4: Verify HST# was normalized on staging** + +Run: +```bash +ssh odoo-nexa "docker exec odoo-nexa-db psql -U odoo -d nexamain_staging -c \"SELECT vat FROM res_partner WHERE id = (SELECT partner_id FROM res_company WHERE id = 1);\"" +``` +Expected: `741224877 RT0001`. + +- [ ] **Step 5: Verify fiscal year lock on staging** + +Run: +```bash +ssh odoo-nexa "docker exec odoo-nexa-db psql -U odoo -d nexamain_staging -c \"SELECT name, fiscalyear_lock_date FROM res_company WHERE id = 1;\"" +``` +Expected: `Nexa Systems Inc | 2025-12-31`. + +- [ ] **Step 6: Do NOT install on prod yet — staging only at this stage** + +The module is just a skeleton; later phases fill it. + +--- + +## Phase 2 — Chart of Accounts (Additions) + +### Task 2.1: Define new asset accounts (1xxxxx) + +**Files:** +- Modify: `nexa_coa_setup/data/01_account_account.xml` + +- [ ] **Step 1: Write XML for new asset accounts** + +Replace content of `nexa_coa_setup/data/01_account_account.xml` with: +```xml + + + + + + + + + 115100 + Due From Shareholder — Gurpreet + asset_current + + + + + 115900 + Due From Associated Corporations + asset_current + + + + + + 118100 + HST/GST Input Tax Credit (ITC) Receivable + asset_current + + + + 118200 + HST/GST Instalments Paid + asset_current + + + + 118300 + QST Input Tax Refund Receivable + asset_current + + + + + 151100 + Computer Hardware & Equipment (CCA Class 50) + asset_fixed + + + + 151200 + Office Furniture & Equipment (CCA Class 8) + asset_fixed + + + + 151300 + Vehicles (CCA Class 10/10.1) + asset_fixed + + + + 151400 + Leasehold Improvements (CCA Class 13) + asset_fixed + + + + 151500 + Acquired Software & Intangibles (CCA Class 14.1) + asset_fixed + + + + 151600 + Tools & Small Equipment <$500 (CCA Class 12) + asset_fixed + + + + + 154100 + Acc. Depreciation — Computer Hardware + asset_fixed + + + + 154200 + Acc. Depreciation — Office Furniture + asset_fixed + + + + 154300 + Acc. Depreciation — Vehicles + asset_fixed + + + + 154400 + Acc. Depreciation — Leasehold Improvements + asset_fixed + + + + 154500 + Acc. Depreciation — Acquired Software + asset_fixed + + + + +``` + +- [ ] **Step 2: Deploy and update on staging** + +Run: +```bash +rsync -avz /Users/gurpreet/Github/Odoo-Modules/nexa_coa_setup/ odoo-nexa:/opt/odoo/custom-addons/nexa_coa_setup/ +ssh odoo-nexa "docker exec odoo-nexa-app odoo -c /etc/odoo/odoo.conf -d nexamain_staging -u nexa_coa_setup --no-http --stop-after-init 2>&1 | tail -10" +``` +Expected: `Modules loaded.` no traceback. + +- [ ] **Step 3: Verify accounts exist on staging** + +Run: +```bash +ssh odoo-nexa "docker exec odoo-nexa-db psql -U odoo -d nexamain_staging -c \"SELECT code_store->>'1' AS code, name->>'en_US' AS name FROM account_account WHERE code_store->>'1' IN ('115100','115900','118100','118200','118300','151100','151200','151300','151400','151500','151600','154100','154200','154300','154400','154500') ORDER BY code_store->>'1';\"" +``` +Expected: 16 rows with correct codes and names. + +- [ ] **Step 4: Commit** + +Run: +```bash +cd /Users/gurpreet/Github/Odoo-Modules +git add nexa_coa_setup/data/01_account_account.xml +git commit -m "feat(nexa_coa_setup): asset accounts (115xxx, 118xxx, 151xxx, 154xxx)" +``` + +### Task 2.2: Define new liability accounts (2xxxxx) + +**Files:** +- Modify: `nexa_coa_setup/data/01_account_account.xml` + +- [ ] **Step 1: Append liability accounts before `` closing tag** + +Add the following block just before the `` line in `01_account_account.xml`: +```xml + + + + + 213100 + HST/GST Collected on Sales + liability_current + + + + 213500 + QST Collected on Sales + liability_current + + + + 214100 + Net HST/GST Payable + liability_current + + + + + 215100 + Source Deductions Payable — Federal Tax + liability_current + + + + 215200 + Source Deductions Payable — CPP + liability_current + + + + 215300 + Source Deductions Payable — EI + liability_current + + + + + 216100 + Corporate Income Tax — Federal Payable + liability_current + + + + 216200 + Corporate Income Tax — Provincial Payable + liability_current + + + + 216300 + Corporate Tax Instalments Paid + asset_current + + + + + 221100 + Due To Shareholder — Gurpreet (short-term) + liability_current + + + + + 221200 + Shareholder Loan — Gurpreet (long-term) + liability_non_current + + + + + + 222900 + Due To Associated Corporations + liability_current + + +``` + +- [ ] **Step 2: Deploy and update on staging** + +Run: +```bash +rsync -avz /Users/gurpreet/Github/Odoo-Modules/nexa_coa_setup/ odoo-nexa:/opt/odoo/custom-addons/nexa_coa_setup/ +ssh odoo-nexa "docker exec odoo-nexa-app odoo -c /etc/odoo/odoo.conf -d nexamain_staging -u nexa_coa_setup --no-http --stop-after-init 2>&1 | tail -5" +``` +Expected: no errors. + +- [ ] **Step 3: Verify** + +Run: +```bash +ssh odoo-nexa "docker exec odoo-nexa-db psql -U odoo -d nexamain_staging -c \"SELECT count(*) FROM account_account WHERE code_store->>'1' IN ('213100','213500','214100','215100','215200','215300','216100','216200','216300','221100','221200','222900');\"" +``` +Expected: `12`. + +- [ ] **Step 4: Commit** + +Run: +```bash +cd /Users/gurpreet/Github/Odoo-Modules +git add nexa_coa_setup/data/01_account_account.xml +git commit -m "feat(nexa_coa_setup): liability accounts (213xxx, 215xxx, 216xxx, 221xxx, 222xxx)" +``` + +### Task 2.3: Define new equity accounts (3xxxxx) + +**Files:** +- Modify: `nexa_coa_setup/data/01_account_account.xml` + +- [ ] **Step 1: Append equity accounts before ``** + +Add this block before ``: +```xml + + + + 311100 + Share Capital — Common Shares + equity + + + + 311200 + Share Capital — Preferred Shares + equity + + + + 311300 + Contributed Surplus + equity + + + + 321100 + Retained Earnings — Current Year + equity + + + + 321200 + Retained Earnings — Prior Years + equity + + + + 321900 + Dividends Declared + equity + +``` + +- [ ] **Step 2: Deploy, update, verify** + +Run: +```bash +rsync -avz /Users/gurpreet/Github/Odoo-Modules/nexa_coa_setup/ odoo-nexa:/opt/odoo/custom-addons/nexa_coa_setup/ +ssh odoo-nexa "docker exec odoo-nexa-app odoo -c /etc/odoo/odoo.conf -d nexamain_staging -u nexa_coa_setup --no-http --stop-after-init 2>&1 | tail -3" +ssh odoo-nexa "docker exec odoo-nexa-db psql -U odoo -d nexamain_staging -c \"SELECT count(*) FROM account_account WHERE code_store->>'1' IN ('311100','311200','311300','321100','321200','321900');\"" +``` +Expected: `6`. + +- [ ] **Step 3: Commit** + +Run: +```bash +cd /Users/gurpreet/Github/Odoo-Modules +git add nexa_coa_setup/data/01_account_account.xml +git commit -m "feat(nexa_coa_setup): equity accounts (311xxx, 321xxx)" +``` + +### Task 2.4: Define revenue accounts (4xxxxx) — 15 service-line accounts + +**Files:** +- Modify: `nexa_coa_setup/data/01_account_account.xml` + +- [ ] **Step 1: Append revenue accounts before ``** + +Add this block before ``: +```xml + + + + + 411100 + SaaS Subscription Revenue + income + + + 411200 + Hosting & Infrastructure Revenue + income + + + 411300 + Support & Maintenance Contracts Revenue + income + + + 411400 + Domain/SSL/Renewal Pass-through Revenue + income + + + 411500 + Setup / Onboarding Fees Revenue + income + + + + + 412100 + Custom Software Development Revenue + income + + + 412200 + Custom Web Application Development Revenue + income + + + 412300 + Custom Website Development Revenue + income + + + 412400 + ERP Implementation & Customization Revenue + income + + + 412500 + Mobile App Development Revenue + income + + + 412600 + Business App / Integration Revenue + income + + + + + 413100 + Consulting & Advisory Revenue + income + + + 413200 + Training & Workshops Revenue + income + + + 413300 + Technical Support — Per-incident / Hourly Revenue + income + + + + + 414100 + Third-party Software Resale Revenue + income + + + 414200 + Hardware Resale Revenue + income + + + + + 419100 + Sales Discounts + income + + + 419200 + Sales Returns & Refunds + income + + + 419300 + Bad Debt Recovery + income_other + +``` + +- [ ] **Step 2: Deploy, update, verify** + +Run: +```bash +rsync -avz /Users/gurpreet/Github/Odoo-Modules/nexa_coa_setup/ odoo-nexa:/opt/odoo/custom-addons/nexa_coa_setup/ +ssh odoo-nexa "docker exec odoo-nexa-app odoo -c /etc/odoo/odoo.conf -d nexamain_staging -u nexa_coa_setup --no-http --stop-after-init 2>&1 | tail -3" +ssh odoo-nexa "docker exec odoo-nexa-db psql -U odoo -d nexamain_staging -c \"SELECT count(*) FROM account_account WHERE code_store->>'1' LIKE '41%' AND code_store->>'1' >= '411100';\"" +``` +Expected: `19` (5 recurring + 6 project + 3 services + 2 reseller + 3 adjustments). + +- [ ] **Step 3: Commit** + +Run: +```bash +cd /Users/gurpreet/Github/Odoo-Modules +git add nexa_coa_setup/data/01_account_account.xml +git commit -m "feat(nexa_coa_setup): revenue accounts (4xxxxx) — 19 service-line accounts" +``` + +### Task 2.5: Define COGS accounts (5xxxxx) — 16 direct cost accounts + +**Files:** +- Modify: `nexa_coa_setup/data/01_account_account.xml` + +- [ ] **Step 1: Append COGS accounts before ``** + +Add this block: +```xml + + + + + 511100 + Cloud Infrastructure (AWS, Hetzner, OVH, DigitalOcean, Linode) + expense_direct_cost + + + 511110 + CDN & Edge Services (Cloudflare, Fastly) + expense_direct_cost + + + 511120 + Backup & Storage Services + expense_direct_cost + + + 511130 + Database & Backend Services (Supabase, hosted Postgres, Redis) + expense_direct_cost + + + 511140 + Monitoring & Observability (customer-facing only) + expense_direct_cost + + + 511150 + SSL Certificates & Domains (wholesale for resale) + expense_direct_cost + + + 511160 + DNS & Email Hosting (wholesale for resale) + expense_direct_cost + + + + + 511200 + Third-party API Costs (Twilio, SendGrid, OpenAI) + expense_direct_cost + + + 511210 + Per-customer Licensing & Royalties + expense_direct_cost + + + + + 512100 + Subcontracted Labour — Canadian (T4A) — SR&ED-eligible + expense_direct_cost + + + 512110 + Subcontracted Labour — Foreign — NOT SR&ED-eligible + expense_direct_cost + + + 512200 + Project-specific Software & Licenses + expense_direct_cost + + + 512300 + Project Travel & Onsite (rebilled) + expense_direct_cost + + + 512400 + Project Hardware (passed through) + expense_direct_cost + + + + + 513100 + Cost of Software Resold + expense_direct_cost + + + 513200 + Cost of Hardware Resold + expense_direct_cost + + + + + 519100 + COGS Adjustments / Write-offs + expense_direct_cost + +``` + +- [ ] **Step 2: Deploy, update, verify** + +Run: +```bash +rsync -avz /Users/gurpreet/Github/Odoo-Modules/nexa_coa_setup/ odoo-nexa:/opt/odoo/custom-addons/nexa_coa_setup/ +ssh odoo-nexa "docker exec odoo-nexa-app odoo -c /etc/odoo/odoo.conf -d nexamain_staging -u nexa_coa_setup --no-http --stop-after-init 2>&1 | tail -3" +ssh odoo-nexa "docker exec odoo-nexa-db psql -U odoo -d nexamain_staging -c \"SELECT count(*) FROM account_account WHERE code_store->>'1' LIKE '5%' AND code_store->>'1' < '520000';\"" +``` +Expected: `17` (16 COGS + 1 adjustments). + +- [ ] **Step 3: Commit** + +Run: +```bash +cd /Users/gurpreet/Github/Odoo-Modules +git add nexa_coa_setup/data/01_account_account.xml +git commit -m "feat(nexa_coa_setup): COGS accounts (5xxxxx) — 17 direct cost accounts" +``` + +### Task 2.6: Define operating expense accounts (6xxxxx) — 52 accounts + +**Files:** +- Modify: `nexa_coa_setup/data/01_account_account.xml` + +- [ ] **Step 1: Append OpEx accounts before ``** + +Add this block: +```xml + + + + + 611100 + Salaries & Wages — Development (SR&ED-eligible) + expense + + + 611200 + Salaries & Wages — Sales & Marketing + expense + + + 611300 + Salaries & Wages — Admin & Operations + expense + + + 611400 + Salary — Shareholder/Officer (Gurpreet) + expense + + + 611500 + Employer CPP / QPP Contributions + expense + + + 611600 + Employer EI Premiums + expense + + + 611700 + Employer Health Tax (EHT/QHST) + expense + + + 611800 + WCB / WSIB Premiums + expense + + + 611900 + Employee Benefits (health, dental, group) + expense + + + 611950 + Bonuses & Incentives + expense + + + 611960 + Vacation Pay Accrual + expense + + + + + 612100 + Contract Labour — Canadian (admin/marketing/freelance) + expense + + + 612200 + Contract Labour — Foreign + expense + + + + + 621100 + Rent — Commercial Office + expense + + + 621200 + Home Office — Business Portion + expense + + + 621300 + Utilities — Commercial + expense + + + 621400 + Internet & Phone — Business + expense + + + 621500 + Office Supplies & Consumables + expense + + + 621600 + Cleaning & Maintenance + expense + + + 621700 + Office Snacks & Refreshments + expense + + + + + 631100 + Software — Productivity (M365, Slack, Notion, Linear, GitHub) + expense + + + 631200 + Software — Development Tools (Cursor, Figma, IDEs) + expense + + + 631300 + Software — Internal Infrastructure + expense + + + 631400 + Software — Security & IT + expense + + + 631500 + Software — Sales & Marketing + expense + + + + + 641100 + Advertising — Digital Ads + expense + + + 641200 + Advertising — Content / SEO + expense + + + 641300 + Trade Shows & Conferences + expense + + + 641400 + Promotional Items / Branded Swag + expense + + + 641500 + Website — Own (nexasystems.ca) + expense + + + + + 651100 + Legal Fees — General + expense + + + 651200 + Accounting & Bookkeeping + expense + + + 651300 + Tax Preparation (T2, T1, GST/HST) + expense + + + 651400 + Business Consulting + expense + + + + + 661100 + Insurance — Commercial General Liability + expense + + + 661200 + Insurance — Professional Liability / E&O + expense + + + 661300 + Insurance — Cyber Liability + expense + + + 661400 + Insurance — Property + expense + + + 661500 + Insurance — Directors & Officers + expense + + + + + 671100 + Travel — Flights, Hotels, Ground Transport + expense + + + 671200 + Meals & Entertainment — 50% Deductible + expense + + + 671300 + Vehicle — Operating (gas, insurance, repairs, parking) + expense + + + 671400 + Mileage Reimbursement — Personal Vehicle + expense + + + + + 681100 + Conferences & Seminars (registration) + expense + + + 681200 + Courses & Certifications + expense + + + 681300 + Books & Publications + expense + + + 681400 + Professional Memberships & Dues + expense + + + + + 691100 + Bank Service Charges + expense + + + 691200 + Merchant Processing Fees (Stripe, PayPal, Square) + expense + + + 691300 + Wire Transfer & FX Fees + expense + + + 691400 + Interest Expense — Bank Loans / LOC + expense + + + 691500 + Interest Expense — Credit Cards + expense + + + 691600 + Late Payment Penalties — Non-deductible + expense + + + + + 699100 + Bad Debt Expense + expense + + + 699200 + Donations & Sponsorships (deductible) + expense + + + 699300 + Penalties & Fines — Non-deductible + expense + + + 699400 + Realized FX Losses + expense + + + 699500 + Depreciation / CCA Expense + expense + +``` + +- [ ] **Step 2: Deploy, update, verify** + +Run: +```bash +rsync -avz /Users/gurpreet/Github/Odoo-Modules/nexa_coa_setup/ odoo-nexa:/opt/odoo/custom-addons/nexa_coa_setup/ +ssh odoo-nexa "docker exec odoo-nexa-app odoo -c /etc/odoo/odoo.conf -d nexamain_staging -u nexa_coa_setup --no-http --stop-after-init 2>&1 | tail -3" +ssh odoo-nexa "docker exec odoo-nexa-db psql -U odoo -d nexamain_staging -c \"SELECT count(*) FROM account_account WHERE code_store->>'1' LIKE '6%' AND code_store->>'1' < '700000';\"" +``` +Expected: at least `52` (might be more if l10n_ca already has some in this range). + +- [ ] **Step 3: Commit** + +Run: +```bash +cd /Users/gurpreet/Github/Odoo-Modules +git add nexa_coa_setup/data/01_account_account.xml +git commit -m "feat(nexa_coa_setup): operating expense accounts (6xxxxx) — 52 accounts" +``` + +--- + +## Phase 3 — Analytic Plans (the SR&ED engine) + +### Task 3.1: Create Project / Department / SR&ED Tag analytic plans + +**Files:** +- Modify: `nexa_coa_setup/data/05_account_analytic_plan.xml` + +- [ ] **Step 1: Write the plans XML** + +Replace content of `nexa_coa_setup/data/05_account_analytic_plan.xml` with: +```xml + + + + + + Project + mandatory + + + + Department + mandatory + + + + SR&ED Tag + optional + + + + +``` + +- [ ] **Step 2: Deploy, update, verify** + +Run: +```bash +rsync -avz /Users/gurpreet/Github/Odoo-Modules/nexa_coa_setup/ odoo-nexa:/opt/odoo/custom-addons/nexa_coa_setup/ +ssh odoo-nexa "docker exec odoo-nexa-app odoo -c /etc/odoo/odoo.conf -d nexamain_staging -u nexa_coa_setup --no-http --stop-after-init 2>&1 | tail -3" +ssh odoo-nexa "docker exec odoo-nexa-db psql -U odoo -d nexamain_staging -c \"SELECT id, name, default_applicability FROM account_analytic_plan ORDER BY id;\"" +``` +Expected: at least 3 plans (Project, Department, SR&ED Tag). + +- [ ] **Step 3: Commit** + +Run: +```bash +cd /Users/gurpreet/Github/Odoo-Modules +git add nexa_coa_setup/data/05_account_analytic_plan.xml +git commit -m "feat(nexa_coa_setup): analytic plans — Project, Department, SR&ED Tag" +``` + +### Task 3.2: Seed Department analytic accounts + +**Files:** +- Modify: `nexa_coa_setup/data/06_account_analytic_account.xml` + +- [ ] **Step 1: Write the Department analytic accounts** + +Replace content of `nexa_coa_setup/data/06_account_analytic_account.xml` with: +```xml + + + + + + + Development + DEPT-DEV + + + + + Sales & Marketing + DEPT-SALES + + + + + Admin & Operations + DEPT-ADMIN + + + + + Hosting Operations + DEPT-HOSTING + + + + + + T4 Dev Salary — full proxy + SRED-T4-DEV-SALARY + + + + + Specified Employee Salary — 75% cap + SRED-SPECIFIED-EMPLOYEE + + + + + Contractor CA Arm's Length — 80% eligible + SRED-CONTRACTOR-CA-ARM-LENGTH + + + + + Contractor CA Non-Arm's Length + SRED-CONTRACTOR-CA-NON-ARM-LENGTH + + + + + Materials Consumed in R&D + SRED-MATERIALS-CONSUMED + + + + + Overhead Proxy Basis (direct labour basis) + SRED-OVERHEAD-PROXY-BASIS + + + + + Not Eligible (default) + NOT-ELIGIBLE + + + + + +``` + +- [ ] **Step 2: Deploy, update, verify** + +Run: +```bash +rsync -avz /Users/gurpreet/Github/Odoo-Modules/nexa_coa_setup/ odoo-nexa:/opt/odoo/custom-addons/nexa_coa_setup/ +ssh odoo-nexa "docker exec odoo-nexa-app odoo -c /etc/odoo/odoo.conf -d nexamain_staging -u nexa_coa_setup --no-http --stop-after-init 2>&1 | tail -3" +ssh odoo-nexa "docker exec odoo-nexa-db psql -U odoo -d nexamain_staging -c \"SELECT a.code, a.name, p.name AS plan FROM account_analytic_account a JOIN account_analytic_plan p ON p.id = a.plan_id WHERE a.code LIKE 'DEPT-%' OR a.code LIKE 'SRED-%' OR a.code = 'NOT-ELIGIBLE' ORDER BY p.name, a.code;\"" +``` +Expected: 4 departments + 7 SR&ED tag values = 11 rows. + +- [ ] **Step 3: Commit** + +Run: +```bash +cd /Users/gurpreet/Github/Odoo-Modules +git add nexa_coa_setup/data/06_account_analytic_account.xml +git commit -m "feat(nexa_coa_setup): seed analytic accounts — departments + SR&ED tags" +``` + +--- + +## Phase 4 — Hooks: Archive Unused, Rename Legacy + +### Task 4.1: Implement `_archive_unused_l10n_ca_accounts` + +**Files:** +- Modify: `nexa_coa_setup/hooks.py` + +- [ ] **Step 1: Replace the stub function** + +In `nexa_coa_setup/hooks.py`, replace the `_archive_unused_l10n_ca_accounts` function with: +```python +def _archive_unused_l10n_ca_accounts(env): + """Archive l10n_ca accounts that have zero postings. + + We never delete (preserves historical integrity); active=False removes them + from dropdowns and reports. Idempotent — re-running has no effect. + """ + # Find accounts with no journal items + env.cr.execute(""" + SELECT a.id + FROM account_account a + WHERE a.active = true + AND NOT EXISTS ( + SELECT 1 FROM account_move_line aml WHERE aml.account_id = a.id + ) + AND NOT EXISTS ( + SELECT 1 FROM ir_model_data d + WHERE d.model = 'account.account' + AND d.res_id = a.id + AND d.module = 'nexa_coa_setup' + ) + """) + ids_to_archive = [r[0] for r in env.cr.fetchall()] + if not ids_to_archive: + _logger.info("nexa_coa_setup: no unused accounts to archive") + return + + accounts = env['account.account'].browse(ids_to_archive) + accounts.write({'active': False}) + _logger.info("nexa_coa_setup: archived %d unused accounts", len(accounts)) +``` + +- [ ] **Step 2: Deploy, update on staging** + +Run: +```bash +rsync -avz /Users/gurpreet/Github/Odoo-Modules/nexa_coa_setup/ odoo-nexa:/opt/odoo/custom-addons/nexa_coa_setup/ +ssh odoo-nexa "docker exec odoo-nexa-app odoo -c /etc/odoo/odoo.conf -d nexamain_staging -u nexa_coa_setup --no-http --stop-after-init 2>&1 | grep -E 'archived|ERROR' | tail -5" +``` +Expected: log line "nexa_coa_setup: archived N unused accounts" where N is around 300-370. + +- [ ] **Step 3: Verify on staging** + +Run: +```bash +ssh odoo-nexa "docker exec odoo-nexa-db psql -U odoo -d nexamain_staging -c \"SELECT (SELECT count(*) FROM account_account WHERE active=true) AS active_now, (SELECT count(*) FROM account_account WHERE active=false) AS archived_now;\"" +``` +Expected: `active_now` around 130-150, `archived_now` around 280-370. + +- [ ] **Step 4: Spot-check that all OUR new accounts are still active** + +Run: +```bash +ssh odoo-nexa "docker exec odoo-nexa-db psql -U odoo -d nexamain_staging -c \"SELECT count(*) FROM account_account a JOIN ir_model_data d ON d.model='account.account' AND d.res_id=a.id WHERE d.module='nexa_coa_setup' AND a.active=false;\"" +``` +Expected: `0` (none of ours archived). + +- [ ] **Step 5: Commit** + +Run: +```bash +cd /Users/gurpreet/Github/Odoo-Modules +git add nexa_coa_setup/hooks.py +git commit -m "feat(nexa_coa_setup): hook to archive unused l10n_ca accounts" +``` + +### Task 4.2: Implement `_rename_legacy_accounts` + +**Files:** +- Modify: `nexa_coa_setup/hooks.py` + +- [ ] **Step 1: Replace the stub** + +In `nexa_coa_setup/hooks.py`, replace `_rename_legacy_accounts` with: +```python +def _rename_legacy_accounts(env): + """Re-map legacy bookkeeping accounts to clean targets, then archive originals. + + We rename rather than delete to preserve audit trail. Each entry has: + old_code: the code on the existing legacy account + new_name: the rename target + archive: if True, set active=False after rename + """ + legacy_map = [ + # (old_code, new_name, archive_after) + ("1400", "(LEGACY) Transferred to Gurpreet — re-class to 221100", True), + ("1505", "(LEGACY) Sent to India — re-class to 612200", True), + ("1580", "(LEGACY) Transferred to Westin — Westin is now a partner", True), + ("1590", "(LEGACY) Transferred to Divine — Divine is now a partner", True), + ("1600", "(LEGACY) Transferred to Manpreet — non-related; archive", True), + ("1500", "(LEGACY) Food & Entertainment — re-class to 671200", True), + ("1501", "(LEGACY) Office Expenses — re-class to 621500", True), + ("411000", "(LEGACY) Inside Sales — re-class to 412xxx specific lines", True), + ("412000", "(LEGACY) Harmonized Provinces Sales — handled by tax codes", True), + ("413000", "(LEGACY) Non-Harmonized Provinces Sales — handled by tax", True), + ("414000", "(LEGACY) International Sales — handled by Zero-rated Export", True), + ("12000", "(LEGACY) Abdul & Future Mobility — use partner subledger", True), + ("12001", "(LEGACY) MSI Account — use partner subledger", True), + ("110010", "(LEGACY) Bank Fee — re-class to 691100", True), + ("511100", "(LEGACY) Inside Purchases — re-class to specific 5xxxxx", True), + ] + + renamed = 0 + archived = 0 + for old_code, new_name, archive in legacy_map: + # In Odoo 19 the code lives in jsonb code_store; we search via the code property + accounts = env['account.account'].search([('code', '=', old_code)]) + if not accounts: + continue + # Skip if name already starts with "(LEGACY)" — idempotent + for acc in accounts: + if acc.name and acc.name.startswith("(LEGACY)"): + continue + acc.name = new_name + renamed += 1 + if archive: + acc.active = False + archived += 1 + _logger.info("nexa_coa_setup: renamed %d legacy accounts, archived %d", renamed, archived) +``` + +**IMPORTANT NOTE FOR ENGINEER:** account 511100 in `legacy_map` collides with the new COGS account `511100 Cloud Infrastructure` we created in Task 2.5. Before deploying, manually verify: +```bash +ssh odoo-nexa "docker exec odoo-nexa-db psql -U odoo -d nexamain_staging -c \"SELECT a.id, a.name->>'en_US' AS name, a.code_store->>'1' AS code, d.module FROM account_account a LEFT JOIN ir_model_data d ON d.model='account.account' AND d.res_id=a.id WHERE a.code_store->>'1' = '511100';\"" +``` +If only ONE row exists and it's our new XML record (module=`nexa_coa_setup`), then the legacy `511100 Inside Purchases` was already absorbed/replaced — REMOVE that line from `legacy_map` before proceeding. + +If TWO rows exist (legacy + new), Odoo prevented loading the new one because of code uniqueness. In that case: +- Manually re-code the legacy 511100 to `511100-LEGACY` via odoo-shell first, then re-run -u to create our new account at 511100, then re-add the rename step. + +- [ ] **Step 2: Resolve the 511100 collision (per the note above)** + +Run the verify query. Based on result: + +- **If only nexa_coa_setup's record exists**: edit `hooks.py` to delete the `("511100", ...)` line from `legacy_map`. Commit the edit. Proceed. +- **If both exist**: open an odoo-shell session and re-code the legacy: + ```bash + ssh odoo-nexa "docker exec -i odoo-nexa-app odoo shell -c /etc/odoo/odoo.conf -d nexamain_staging --no-http --stop-after-init" <<'PYEOF' + legacy = env['account.account'].search([('code','=','511100'),('name','ilike','inside purchases')]) + if legacy: + legacy.code = '511100-LEGACY' + env.cr.commit() + print(f"Renamed legacy account {legacy.id} to code 511100-LEGACY") + PYEOF + ``` + Then leave `hooks.py` as-is. + +- [ ] **Step 3: Deploy, update on staging** + +Run: +```bash +rsync -avz /Users/gurpreet/Github/Odoo-Modules/nexa_coa_setup/ odoo-nexa:/opt/odoo/custom-addons/nexa_coa_setup/ +ssh odoo-nexa "docker exec odoo-nexa-app odoo -c /etc/odoo/odoo.conf -d nexamain_staging -u nexa_coa_setup --no-http --stop-after-init 2>&1 | grep -E 'renamed|ERROR' | tail -5" +``` +Expected: log line "nexa_coa_setup: renamed N legacy accounts, archived N". + +- [ ] **Step 4: Verify legacy accounts are renamed and archived** + +Run: +```bash +ssh odoo-nexa "docker exec odoo-nexa-db psql -U odoo -d nexamain_staging -c \"SELECT code_store->>'1' AS code, name->>'en_US' AS name, active FROM account_account WHERE name->>'en_US' LIKE '(LEGACY)%' ORDER BY code_store->>'1';\"" +``` +Expected: 15 rows (or 14 if 511100 was removed) all with `active=false` and `(LEGACY)` prefix. + +- [ ] **Step 5: Commit** + +Run: +```bash +cd /Users/gurpreet/Github/Odoo-Modules +git add nexa_coa_setup/hooks.py +git commit -m "feat(nexa_coa_setup): hook to rename and archive legacy accounts" +``` + +--- + +## Phase 5 — Tax Cleanup + +### Task 5.1: Archive duplicate / unused taxes + +**Files:** +- Modify: `nexa_coa_setup/hooks.py` + +- [ ] **Step 1: Add a tax-archive helper to hooks.py** + +Append to `nexa_coa_setup/hooks.py`: +```python +def _archive_unused_taxes(env): + """Archive taxes with type_tax_use='none' or with zero usage on existing moves. + + Keep only the curated set listed in KEEP_TAX_NAMES. This is conservative — + we only archive taxes that have NEVER been used on any move line. + """ + KEEP_TAX_NAMES = { + # Sales taxes we keep active + '5% GST', '13% HST', '14% HST', '15% HST', + '11% GST+PST SK', '12% GST+PST BC', '12% GST+PST MB', '14.975% GST+QST', + '9.975% QST', + '0% GST', '0% Exempt', '0% Int', + # Purchase taxes (same names, type_tax_use='purchase') + } + + # Build the keep ID set: taxes whose name (en_US) is in KEEP_TAX_NAMES, active or not + env.cr.execute(""" + SELECT id FROM account_tax + WHERE name->>'en_US' = ANY(%s) + """, (list(KEEP_TAX_NAMES),)) + keep_ids = {r[0] for r in env.cr.fetchall()} + + # Candidates to archive: active taxes with no usage and NOT in keep set + env.cr.execute(""" + SELECT t.id + FROM account_tax t + WHERE t.active = true + AND NOT EXISTS (SELECT 1 FROM account_move_line_account_tax_rel r WHERE r.account_tax_id = t.id) + AND t.id != ALL(%s) + """, (list(keep_ids) or [0],)) + ids = [r[0] for r in env.cr.fetchall()] + if ids: + env['account.tax'].browse(ids).write({'active': False}) + _logger.info("nexa_coa_setup: archived %d unused taxes (kept %d)", len(ids), len(keep_ids)) +``` + +Then add to `post_init_hook` body, after `_archive_unused_l10n_ca_accounts(env)`: +```python + _archive_unused_taxes(env) +``` + +- [ ] **Step 2: Deploy, update on staging** + +Run: +```bash +rsync -avz /Users/gurpreet/Github/Odoo-Modules/nexa_coa_setup/ odoo-nexa:/opt/odoo/custom-addons/nexa_coa_setup/ +ssh odoo-nexa "docker exec odoo-nexa-app odoo -c /etc/odoo/odoo.conf -d nexamain_staging -u nexa_coa_setup --no-http --stop-after-init 2>&1 | grep -E 'taxes|ERROR' | tail -5" +``` +Expected: log line "nexa_coa_setup: archived N unused taxes (kept M)". + +- [ ] **Step 3: Verify active tax count** + +Run: +```bash +ssh odoo-nexa "docker exec odoo-nexa-db psql -U odoo -d nexamain_staging -c \"SELECT type_tax_use, count(*) FROM account_tax WHERE active=true GROUP BY type_tax_use;\"" +``` +Expected: a manageable count, ideally 14-25 active taxes across `sale` and `purchase`. + +- [ ] **Step 4: Commit** + +Run: +```bash +cd /Users/gurpreet/Github/Odoo-Modules +git add nexa_coa_setup/hooks.py +git commit -m "feat(nexa_coa_setup): archive duplicate/unused taxes" +``` + +--- + +## Phase 6 — Fiscal Positions + +### Task 6.1: Define 8 fiscal positions with country/state auto-detection + +**Files:** +- Modify: `nexa_coa_setup/data/04_account_fiscal_position.xml` + +- [ ] **Step 1: Discover the tax IDs we'll reference** + +Run on staging: +```bash +ssh odoo-nexa "docker exec odoo-nexa-db psql -U odoo -d nexamain_staging -c \"SELECT id, name->>'en_US' AS name, type_tax_use, amount FROM account_tax WHERE active=true ORDER BY type_tax_use, amount, name;\"" +``` +Note the IDs of the sales-side taxes we want to map TO from each fiscal position. Save them for the XML. + +- [ ] **Step 2: Write the fiscal position XML** + +Replace content of `nexa_coa_setup/data/04_account_fiscal_position.xml` with: +```xml + + + + + + + + CA — Ontario (Default) + + + + + + + CA — Atlantic (HST 15%) + + + + + + + CA — Quebec (GST + QST) + + + + + + + CA — British Columbia (GST 5%, PST per-product) + + + + + + + CA — Prairies / Territories (GST 5% only) + + + + + + + Export — United States (Zero-rated) + + + + + + Export — International (Zero-rated) + + Manually applied for non-CA/non-US customers. Auto-apply by country group requires custom rule. + + + + Tax Exempt (cert-holder) + + Apply manually to customers with valid exemption certificate on file. Document the certificate in the partner's notes. + + + + +``` + +- [ ] **Step 3: Deploy, update on staging** + +Run: +```bash +rsync -avz /Users/gurpreet/Github/Odoo-Modules/nexa_coa_setup/ odoo-nexa:/opt/odoo/custom-addons/nexa_coa_setup/ +ssh odoo-nexa "docker exec odoo-nexa-app odoo -c /etc/odoo/odoo.conf -d nexamain_staging -u nexa_coa_setup --no-http --stop-after-init 2>&1 | tail -3" +ssh odoo-nexa "docker exec odoo-nexa-db psql -U odoo -d nexamain_staging -c \"SELECT name, auto_apply FROM account_fiscal_position WHERE name LIKE 'CA %' OR name LIKE 'Export %' OR name LIKE 'Tax Exempt%' ORDER BY name;\"" +``` +Expected: 8 fiscal positions, first 6 with auto_apply=true (CA + US), last 2 manual. + +- [ ] **Step 4: Add tax substitutions via odoo-shell** + +The XML approach for `account.fiscal.position.tax` lines is verbose and fragile (depends on exact tax IDs from l10n_ca). Use a one-time script: + +Create file `/Users/gurpreet/Github/Odoo-Modules/nexa_coa_setup/scripts/configure_fp_tax_maps.py`: +```python +"""Configure tax substitutions on fiscal positions. + +Default tax on a product is set to "5% GST" (federal-only base). +Each fiscal position substitutes it for the appropriate provincial tax. +""" +env = self.env + +def tax(name, use='sale'): + """Find a single tax by name and type.""" + rec = env['account.tax'].search([ + ('name', '=', name), + ('type_tax_use', '=', use), + ('active', '=', True), + ], limit=1) + if not rec: + raise ValueError(f"Tax not found: {name!r} ({use})") + return rec + +# Base "default" tax that products get +GST_5 = tax('5% GST', 'sale') + +# Provincial replacements +HST_13 = tax('13% HST', 'sale') +HST_15 = tax('15% HST', 'sale') +QST_GROUP = tax('14.975% GST+QST', 'sale') +ZERO_EXPORT = tax('0% GST', 'sale') # Zero-rated for export + +FP_MAP = [ + # (fp_xmlid, [(from_tax, to_tax)]) + ('nexa_coa_setup.fp_ca_ontario', [(GST_5, HST_13)]), + ('nexa_coa_setup.fp_ca_atlantic', [(GST_5, HST_15)]), + ('nexa_coa_setup.fp_ca_quebec', [(GST_5, QST_GROUP)]), + ('nexa_coa_setup.fp_ca_bc', [(GST_5, GST_5)]), # GST only; PST handled per-product + ('nexa_coa_setup.fp_ca_prairies_territories', [(GST_5, GST_5)]), # GST only + ('nexa_coa_setup.fp_export_us', [(GST_5, ZERO_EXPORT)]), + ('nexa_coa_setup.fp_export_intl', [(GST_5, ZERO_EXPORT)]), + ('nexa_coa_setup.fp_tax_exempt', [(GST_5, ZERO_EXPORT)]), +] + +for fp_xmlid, pairs in FP_MAP: + fp = env.ref(fp_xmlid, raise_if_not_found=False) + if not fp: + print(f"SKIP missing fiscal position: {fp_xmlid}") + continue + # Clear existing tax maps for clean idempotent re-run + fp.tax_ids.unlink() + for from_tax, to_tax in pairs: + env['account.fiscal.position.tax'].create({ + 'position_id': fp.id, + 'tax_src_id': from_tax.id, + 'tax_dest_id': to_tax.id, + }) + print(f"OK {fp_xmlid}: {len(pairs)} mapping(s)") + +env.cr.commit() +print("Fiscal position tax maps configured.") +``` + +Then run it: +```bash +mkdir -p /Users/gurpreet/Github/Odoo-Modules/nexa_coa_setup/scripts +# (create the file via Write tool as shown above) +rsync -avz /Users/gurpreet/Github/Odoo-Modules/nexa_coa_setup/scripts/ odoo-nexa:/opt/odoo/custom-addons/nexa_coa_setup/scripts/ +ssh odoo-nexa "docker exec -i odoo-nexa-app odoo shell -c /etc/odoo/odoo.conf -d nexamain_staging --no-http --stop-after-init < /opt/odoo/custom-addons/nexa_coa_setup/scripts/configure_fp_tax_maps.py 2>&1 | tail -15" +``` +Expected: "OK fp_ca_ontario: 1 mapping(s)" through all 8. + +- [ ] **Step 5: Verify mappings** + +Run: +```bash +ssh odoo-nexa "docker exec odoo-nexa-db psql -U odoo -d nexamain_staging -c \"SELECT fp.name AS fiscal_pos, st.name->>'en_US' AS from_tax, dt.name->>'en_US' AS to_tax FROM account_fiscal_position_tax m JOIN account_fiscal_position fp ON fp.id=m.position_id JOIN account_tax st ON st.id=m.tax_src_id JOIN account_tax dt ON dt.id=m.tax_dest_id WHERE fp.name LIKE 'CA %' OR fp.name LIKE 'Export %' OR fp.name LIKE 'Tax Exempt%' ORDER BY fp.name;\"" +``` +Expected: 8 rows showing each FP's substitution. + +- [ ] **Step 6: Commit** + +Run: +```bash +cd /Users/gurpreet/Github/Odoo-Modules +git add nexa_coa_setup/data/04_account_fiscal_position.xml nexa_coa_setup/scripts/configure_fp_tax_maps.py +git commit -m "feat(nexa_coa_setup): 8 fiscal positions with auto-detect and tax substitutions" +``` + +--- + +## Phase 7 — Product Categories + +### Task 7.1: Define service product categories with default accounts + +**Files:** +- Modify: `nexa_coa_setup/data/07_product_category.xml` + +- [ ] **Step 1: Write the product category XML** + +Replace content of `nexa_coa_setup/data/07_product_category.xml` with: +```xml + + + + + + + Services + + + + + SaaS Subscription + + + + + + Hosting + + + + + + Support Contract + + + + + + Setup Fee + + + + + + Custom Software Development + + + + + + Custom Web App Development + + + + + + Custom Website Development + + + + + + ERP Implementation + + + + + + Consulting & Advisory + + + + + + Training + + + + + + + Resale + + + + Software Resale + + + + + + + Hardware Resale + + + + + + + +``` + +- [ ] **Step 2: Deploy, update, verify** + +Run: +```bash +rsync -avz /Users/gurpreet/Github/Odoo-Modules/nexa_coa_setup/ odoo-nexa:/opt/odoo/custom-addons/nexa_coa_setup/ +ssh odoo-nexa "docker exec odoo-nexa-app odoo -c /etc/odoo/odoo.conf -d nexamain_staging -u nexa_coa_setup --no-http --stop-after-init 2>&1 | tail -3" +ssh odoo-nexa "docker exec odoo-nexa-db psql -U odoo -d nexamain_staging -c \"SELECT pc.complete_name FROM product_category pc JOIN ir_model_data d ON d.model='product.category' AND d.res_id=pc.id WHERE d.module='nexa_coa_setup' ORDER BY pc.complete_name;\"" +``` +Expected: ~12 categories under Services/Resale. + +- [ ] **Step 3: Commit** + +Run: +```bash +cd /Users/gurpreet/Github/Odoo-Modules +git add nexa_coa_setup/data/07_product_category.xml +git commit -m "feat(nexa_coa_setup): service product categories with default income accounts" +``` + +--- + +## Phase 8 — Partner Setup (Westin, Divine, RP-Associated tag) + +### Task 8.1: Create RP-Associated partner tag + +**Files:** +- Modify: `nexa_coa_setup/data/08_res_partner_category.xml` + +- [ ] **Step 1: Write the tag XML** + +Replace content with: +```xml + + + + + + RP-Associated + 3 + + + + + +``` + +- [ ] **Step 2: Deploy, update, verify** + +Run: +```bash +rsync -avz /Users/gurpreet/Github/Odoo-Modules/nexa_coa_setup/ odoo-nexa:/opt/odoo/custom-addons/nexa_coa_setup/ +ssh odoo-nexa "docker exec odoo-nexa-app odoo -c /etc/odoo/odoo.conf -d nexamain_staging -u nexa_coa_setup --no-http --stop-after-init 2>&1 | tail -3" +ssh odoo-nexa "docker exec odoo-nexa-db psql -U odoo -d nexamain_staging -c \"SELECT id, name->>'en_US' AS name, color FROM res_partner_category WHERE name->>'en_US' = 'RP-Associated';\"" +``` +Expected: 1 row. + +### Task 8.2: Create Westin Healthcare Inc and Divine Mobility Inc partner records + +**Files:** +- Modify: `nexa_coa_setup/data/09_res_partner.xml` + +- [ ] **Step 1: Write the partner XML** + +Replace content with: +```xml + + + + + + Westin Healthcare Inc + + company + 1 + 1 + + + + + Associated corporation under common control with Nexa Systems Inc (Gurpreet, owner). Intercompany transactions must be priced at fair market value (ITA s.247). Shared SBD limit per ITA s.125(5.1). + + + + Divine Mobility Inc + + company + 1 + 1 + + + + + Associated corporation under common control with Nexa Systems Inc (Gurpreet, owner). See Westin Healthcare Inc for compliance notes. + + + + +``` + +- [ ] **Step 2: Deploy, update, verify** + +Run: +```bash +rsync -avz /Users/gurpreet/Github/Odoo-Modules/nexa_coa_setup/ odoo-nexa:/opt/odoo/custom-addons/nexa_coa_setup/ +ssh odoo-nexa "docker exec odoo-nexa-app odoo -c /etc/odoo/odoo.conf -d nexamain_staging -u nexa_coa_setup --no-http --stop-after-init 2>&1 | tail -3" +ssh odoo-nexa "docker exec odoo-nexa-db psql -U odoo -d nexamain_staging -c \"SELECT p.name, p.customer_rank, p.supplier_rank, p.is_company, fp.name AS fiscal_pos, array_agg(c.name->>'en_US') AS tags FROM res_partner p LEFT JOIN account_fiscal_position fp ON fp.id=p.property_account_position_id LEFT JOIN res_partner_res_partner_category_rel r ON r.partner_id=p.id LEFT JOIN res_partner_category c ON c.id=r.category_id WHERE p.name IN ('Westin Healthcare Inc', 'Divine Mobility Inc') GROUP BY p.id, p.name, p.customer_rank, p.supplier_rank, p.is_company, fp.name ORDER BY p.name;\"" +``` +Expected: 2 rows, both with customer_rank=1, supplier_rank=1, is_company=t, fiscal_pos="CA — Ontario (Default)", tags including 'RP-Associated'. + +- [ ] **Step 3: Commit** + +Run: +```bash +cd /Users/gurpreet/Github/Odoo-Modules +git add nexa_coa_setup/data/08_res_partner_category.xml nexa_coa_setup/data/09_res_partner.xml +git commit -m "feat(nexa_coa_setup): Westin and Divine as associated-corp partners" +``` + +--- + +## Phase 9 — Bank Reconciliation Rules + +### Task 9.1: Define auto-categorization rules for common vendors + +**Files:** +- Modify: `nexa_coa_setup/data/10_account_reconcile_model.xml` + +- [ ] **Step 1: Look up account IDs we'll reference** + +Run: +```bash +ssh odoo-nexa "docker exec odoo-nexa-db psql -U odoo -d nexamain_staging -c \"SELECT id, code_store->>'1' AS code, name->>'en_US' AS name FROM account_account WHERE code_store->>'1' IN ('511100','511110','631100','631200','641100','691100','691200') ORDER BY code_store->>'1';\"" +``` +Note the IDs. + +- [ ] **Step 2: Write the reconciliation rules XML** + +Replace content of `nexa_coa_setup/data/10_account_reconcile_model.xml` with: +```xml + + + + + + + AWS / Amazon Web Services → Cloud Infrastructure + writeoff_suggestion + contains + AMAZON WEB SERVICES + + + + + + Hetzner → Cloud Infrastructure + writeoff_suggestion + contains + HETZNER + + + + + DigitalOcean → Cloud Infrastructure + writeoff_suggestion + contains + DIGITALOCEAN + + + + + + Cloudflare → CDN & Edge + writeoff_suggestion + contains + CLOUDFLARE + + + + + + GitHub → Software (Dev Tools) + writeoff_suggestion + contains + GITHUB + + + + + + Microsoft / M365 → Software (Productivity) + writeoff_suggestion + contains + MICROSOFT + + + + + + Stripe fee → Merchant Processing + writeoff_suggestion + contains + STRIPE FEE + + + + + + Google Ads → Advertising (Digital) + writeoff_suggestion + contains + GOOGLE ADS + + + + + +``` + +- [ ] **Step 3: Deploy, update, verify** + +Run: +```bash +rsync -avz /Users/gurpreet/Github/Odoo-Modules/nexa_coa_setup/ odoo-nexa:/opt/odoo/custom-addons/nexa_coa_setup/ +ssh odoo-nexa "docker exec odoo-nexa-app odoo -c /etc/odoo/odoo.conf -d nexamain_staging -u nexa_coa_setup --no-http --stop-after-init 2>&1 | tail -3" +ssh odoo-nexa "docker exec odoo-nexa-db psql -U odoo -d nexamain_staging -c \"SELECT name FROM account_reconcile_model WHERE name LIKE '%→%' ORDER BY name;\"" +``` +Expected: 8 reconciliation rules visible. + +- [ ] **Step 4: Commit** + +Run: +```bash +cd /Users/gurpreet/Github/Odoo-Modules +git add nexa_coa_setup/data/10_account_reconcile_model.xml +git commit -m "feat(nexa_coa_setup): 8 bank reconciliation rules for common vendors" +``` + +--- + +## Phase 10 — End-to-End Verification on Staging + +### Task 10.1: Create test invoice for Ontario customer (HST 13%) + +**Files:** none — test data only + +- [ ] **Step 1: Create a test ON customer and invoice via odoo-shell** + +Create `/Users/gurpreet/Github/Odoo-Modules/nexa_coa_setup/scripts/test_invoice_on.py`: +```python +env = self.env + +# 1. Create or fetch test customer +partner = env['res.partner'].search([('name', '=', 'TEST CUSTOMER ON')], limit=1) +if not partner: + partner = env['res.partner'].create({ + 'name': 'TEST CUSTOMER ON', + 'country_id': env.ref('base.ca').id, + 'state_id': env.ref('base.state_ca_on').id, + 'customer_rank': 1, + }) + +# 2. Find or create a SaaS product in SaaS Subscription category +saas_cat = env.ref('nexa_coa_setup.pc_saas') +product = env['product.product'].search([('name', '=', 'TEST SaaS Subscription')], limit=1) +if not product: + product = env['product.product'].create({ + 'name': 'TEST SaaS Subscription', + 'type': 'service', + 'list_price': 100.00, + 'categ_id': saas_cat.id, + }) + +# 3. Create invoice +inv = env['account.move'].create({ + 'move_type': 'out_invoice', + 'partner_id': partner.id, + 'invoice_line_ids': [(0, 0, { + 'product_id': product.id, + 'quantity': 1, + 'price_unit': 100.00, + })], +}) + +print(f"Invoice {inv.name} created") +print(f" Fiscal position: {inv.fiscal_position_id.name}") +print(f" Tax on line: {inv.invoice_line_ids[0].tax_ids.mapped('name')}") +print(f" Subtotal: {inv.amount_untaxed}") +print(f" Tax: {inv.amount_tax}") +print(f" Total: {inv.amount_total}") +print(f" Income account: {inv.invoice_line_ids[0].account_id.code} {inv.invoice_line_ids[0].account_id.name}") + +# Don't commit — this is just a test +env.cr.rollback() +print("Test rolled back.") +``` + +- [ ] **Step 2: Run the test on staging** + +Run: +```bash +rsync -avz /Users/gurpreet/Github/Odoo-Modules/nexa_coa_setup/ odoo-nexa:/opt/odoo/custom-addons/nexa_coa_setup/ +ssh odoo-nexa "docker exec -i odoo-nexa-app odoo shell -c /etc/odoo/odoo.conf -d nexamain_staging --no-http --stop-after-init < /opt/odoo/custom-addons/nexa_coa_setup/scripts/test_invoice_on.py 2>&1 | tail -15" +``` + +Expected output: +``` +Invoice INV/2026/... created + Fiscal position: CA — Ontario (Default) + Tax on line: ['13% HST'] + Subtotal: 100.0 + Tax: 13.0 + Total: 113.0 + Income account: 411100 SaaS Subscription Revenue +Test rolled back. +``` + +If tax shows "5% GST" instead of "13% HST" → fiscal position substitution is broken. Stop and debug. + +- [ ] **Step 3: Commit the test script** + +Run: +```bash +cd /Users/gurpreet/Github/Odoo-Modules +git add nexa_coa_setup/scripts/test_invoice_on.py +git commit -m "test(nexa_coa_setup): ON customer SaaS invoice — HST 13% mapping" +``` + +### Task 10.2: Create test invoice for US customer (Zero-rated) + +**Files:** create `nexa_coa_setup/scripts/test_invoice_us.py` + +- [ ] **Step 1: Write the script (similar to test_invoice_on.py but with US partner)** + +Create `/Users/gurpreet/Github/Odoo-Modules/nexa_coa_setup/scripts/test_invoice_us.py`: +```python +env = self.env + +partner = env['res.partner'].search([('name', '=', 'TEST CUSTOMER US')], limit=1) +if not partner: + partner = env['res.partner'].create({ + 'name': 'TEST CUSTOMER US', + 'country_id': env.ref('base.us').id, + 'customer_rank': 1, + }) + +saas_cat = env.ref('nexa_coa_setup.pc_saas') +product = env['product.product'].search([('name', '=', 'TEST SaaS Subscription')], limit=1) +if not product: + product = env['product.product'].create({ + 'name': 'TEST SaaS Subscription', + 'type': 'service', + 'list_price': 100.00, + 'categ_id': saas_cat.id, + }) + +inv = env['account.move'].create({ + 'move_type': 'out_invoice', + 'partner_id': partner.id, + 'invoice_line_ids': [(0, 0, { + 'product_id': product.id, + 'quantity': 1, + 'price_unit': 100.00, + })], +}) + +print(f"Invoice {inv.name} created") +print(f" Fiscal position: {inv.fiscal_position_id.name}") +print(f" Tax on line: {inv.invoice_line_ids[0].tax_ids.mapped('name')}") +print(f" Total: {inv.amount_total}") + +env.cr.rollback() +print("Test rolled back.") +``` + +- [ ] **Step 2: Run and verify** + +Run: +```bash +rsync -avz /Users/gurpreet/Github/Odoo-Modules/nexa_coa_setup/ odoo-nexa:/opt/odoo/custom-addons/nexa_coa_setup/ +ssh odoo-nexa "docker exec -i odoo-nexa-app odoo shell -c /etc/odoo/odoo.conf -d nexamain_staging --no-http --stop-after-init < /opt/odoo/custom-addons/nexa_coa_setup/scripts/test_invoice_us.py 2>&1 | tail -10" +``` + +Expected: +``` +Invoice INV/2026/... created + Fiscal position: Export — United States (Zero-rated) + Tax on line: ['0% GST'] + Total: 100.0 +Test rolled back. +``` + +- [ ] **Step 3: Commit** + +Run: +```bash +cd /Users/gurpreet/Github/Odoo-Modules +git add nexa_coa_setup/scripts/test_invoice_us.py +git commit -m "test(nexa_coa_setup): US customer SaaS invoice — Zero-rated export" +``` + +### Task 10.3: Create test invoice for intercompany (Nexa → Westin) + +**Files:** create `nexa_coa_setup/scripts/test_invoice_intercompany.py` + +- [ ] **Step 1: Write the script** + +Create `/Users/gurpreet/Github/Odoo-Modules/nexa_coa_setup/scripts/test_invoice_intercompany.py`: +```python +env = self.env + +westin = env.ref('nexa_coa_setup.partner_westin_healthcare') + +consulting_cat = env.ref('nexa_coa_setup.pc_consulting') +product = env['product.product'].search([('name', '=', 'TEST Consulting Hour')], limit=1) +if not product: + product = env['product.product'].create({ + 'name': 'TEST Consulting Hour', + 'type': 'service', + 'list_price': 150.00, + 'categ_id': consulting_cat.id, + }) + +inv = env['account.move'].create({ + 'move_type': 'out_invoice', + 'partner_id': westin.id, + 'invoice_line_ids': [(0, 0, { + 'product_id': product.id, + 'quantity': 10, + 'price_unit': 150.00, + })], +}) + +print(f"Invoice {inv.name} to {westin.name}") +print(f" Fiscal position: {inv.fiscal_position_id.name}") +print(f" Tax on line: {inv.invoice_line_ids[0].tax_ids.mapped('name')}") +print(f" Subtotal: {inv.amount_untaxed}") +print(f" Tax: {inv.amount_tax}") +print(f" Total: {inv.amount_total}") +print(f" Income account: {inv.invoice_line_ids[0].account_id.code} {inv.invoice_line_ids[0].account_id.name}") +print(f" Customer tags: {[c.name for c in westin.category_id]}") + +env.cr.rollback() +print("Test rolled back.") +``` + +- [ ] **Step 2: Run and verify** + +Run: +```bash +rsync -avz /Users/gurpreet/Github/Odoo-Modules/nexa_coa_setup/ odoo-nexa:/opt/odoo/custom-addons/nexa_coa_setup/ +ssh odoo-nexa "docker exec -i odoo-nexa-app odoo shell -c /etc/odoo/odoo.conf -d nexamain_staging --no-http --stop-after-init < /opt/odoo/custom-addons/nexa_coa_setup/scripts/test_invoice_intercompany.py 2>&1 | tail -15" +``` + +Expected: +``` +Invoice INV/2026/... to Westin Healthcare Inc + Fiscal position: CA — Ontario (Default) + Tax on line: ['13% HST'] + Subtotal: 1500.0 + Tax: 195.0 + Total: 1695.0 + Income account: 413100 Consulting & Advisory Revenue + Customer tags: ['RP-Associated'] +Test rolled back. +``` + +- [ ] **Step 3: Commit** + +Run: +```bash +cd /Users/gurpreet/Github/Odoo-Modules +git add nexa_coa_setup/scripts/test_invoice_intercompany.py +git commit -m "test(nexa_coa_setup): intercompany Nexa→Westin invoice" +``` + +--- + +## Phase 11 — Apply to Production + +### Task 11.1: Final pg_dump of prod BEFORE production install + +**Files:** none + +- [ ] **Step 1: Take a fresh dump** + +Run: +```bash +ssh odoo-nexa "docker exec odoo-nexa-db pg_dump -U odoo -d nexamain -F c -Z 9 -f /tmp/nexamain_prefinal_$(date +%Y%m%d_%H%M%S).dump && ls -lh /tmp/nexamain_prefinal_*.dump | tail -1" +scp odoo-nexa:/tmp/nexamain_prefinal_*.dump ~/Backups/odoo-nexa/ +echo "$(date -u +%Y-%m-%dT%H:%M:%SZ) — pg_dump pre-final nexa_coa_setup install on prod — $(ls -1 ~/Backups/odoo-nexa/nexamain_prefinal_*.dump | tail -1)" >> ~/Backups/odoo-nexa/RESTORE_LOG.md +``` + +### Task 11.2: Install nexa_coa_setup on production + +**Files:** none + +> **🚦 GATE — User confirmation required before this task.** This is the destructive step that touches the live production database. Confirm: +> - [ ] pg_dump from Task 11.1 succeeded and is on local Mac (`~/Backups/odoo-nexa/`) +> - [ ] All staging tests passed (Tasks 10.1, 10.2, 10.3) +> - [ ] No outstanding transactions or active users on prod right now (or business is comfortable with brief disruption) +> +> User must explicitly say "proceed with prod install" before continuing. + +- [ ] **Step 1: Install on nexamain (production)** + +Run: +```bash +ssh odoo-nexa "docker exec odoo-nexa-app odoo -c /etc/odoo/odoo.conf -d nexamain -i nexa_coa_setup --no-http --stop-after-init 2>&1 | tail -30" +``` +Expected: `Modules loaded.` no traceback. Watch for `archived N unused accounts` and `renamed N legacy accounts` log lines. + +- [ ] **Step 2: Verify all checkpoints on production** + +Run: +```bash +ssh odoo-nexa "docker exec odoo-nexa-db psql -U odoo -d nexamain -c \" +SELECT 'HST# format' AS check_name, (SELECT vat FROM res_partner WHERE id = (SELECT partner_id FROM res_company WHERE id = 1)) AS value +UNION ALL SELECT 'Fiscal year lock', (SELECT fiscalyear_lock_date::text FROM res_company WHERE id = 1) +UNION ALL SELECT 'Active accounts', (SELECT count(*)::text FROM account_account WHERE active = true) +UNION ALL SELECT 'Archived accounts', (SELECT count(*)::text FROM account_account WHERE active = false) +UNION ALL SELECT 'Active taxes', (SELECT count(*)::text FROM account_tax WHERE active = true) +UNION ALL SELECT 'Fiscal positions', (SELECT count(*)::text FROM account_fiscal_position WHERE active = true) +UNION ALL SELECT 'Analytic plans', (SELECT count(*)::text FROM account_analytic_plan) +UNION ALL SELECT 'Partner: Westin', (SELECT count(*)::text FROM res_partner WHERE name = 'Westin Healthcare Inc') +UNION ALL SELECT 'Partner: Divine', (SELECT count(*)::text FROM res_partner WHERE name = 'Divine Mobility Inc'); +\"" +``` + +Acceptance check: +- HST# format: `741224877 RT0001` +- Fiscal year lock: `2025-12-31` +- Active accounts: 130-200 (down from 426) +- Archived accounts: 280-370 +- Active taxes: ≤ 25 +- Fiscal positions: ≥ 8 +- Analytic plans: ≥ 3 +- Partner: Westin: 1 +- Partner: Divine: 1 + +- [ ] **Step 3: Run all three test invoices on production** + +```bash +ssh odoo-nexa "docker exec -i odoo-nexa-app odoo shell -c /etc/odoo/odoo.conf -d nexamain --no-http --stop-after-init < /opt/odoo/custom-addons/nexa_coa_setup/scripts/test_invoice_on.py 2>&1 | tail -10" +ssh odoo-nexa "docker exec -i odoo-nexa-app odoo shell -c /etc/odoo/odoo.conf -d nexamain --no-http --stop-after-init < /opt/odoo/custom-addons/nexa_coa_setup/scripts/test_invoice_us.py 2>&1 | tail -10" +ssh odoo-nexa "docker exec -i odoo-nexa-app odoo shell -c /etc/odoo/odoo.conf -d nexamain --no-http --stop-after-init < /opt/odoo/custom-addons/nexa_coa_setup/scripts/test_invoice_intercompany.py 2>&1 | tail -10" +``` + +Expected: all three produce the same output they did on staging. All are rolled back — no actual test data persists. + +### Task 11.3: Drop the staging DB + +**Files:** none + +- [ ] **Step 1: Drop nexamain_staging** + +Run: +```bash +ssh odoo-nexa "docker exec odoo-nexa-db psql -U odoo -d postgres -c 'DROP DATABASE IF EXISTS nexamain_staging;'" +``` +Expected: `DROP DATABASE`. + +### Task 11.4: Final commit and tag + +**Files:** none + +- [ ] **Step 1: Tag the deployment** + +> **Note**: `git push --tags` writes to the remote — ask user before running. + +Run: +```bash +cd /Users/gurpreet/Github/Odoo-Modules +git tag -a nexa_coa_setup-v1.0.0 -m "Initial install of nexa_coa_setup on prod nexamain" +# After user confirms remote push is OK: +git push --tags +``` + +--- + +## Phase 12 — Documentation & Runbook + +### Task 12.1: Write operating runbook in module README + +**Files:** +- Modify: `nexa_coa_setup/README.md` + +- [ ] **Step 1: Expand README with operating procedures** + +Replace `nexa_coa_setup/README.md` content with: +```markdown +# Nexa Systems — Chart of Accounts Setup + +Custom Odoo 19 module that configures the chart of accounts, taxes, +fiscal positions, analytic plans, and partner records for Nexa Systems Inc. + +## Design reference + +See `docs/superpowers/specs/2026-05-12-nexa-coa-design.md`. + +## Initial install + +ALWAYS take a pg_dump first: +``` +ssh odoo-nexa "docker exec odoo-nexa-db pg_dump -U odoo -d nexamain -F c -Z 9 -f /tmp/nexamain_$(date +%Y%m%d).dump" +scp odoo-nexa:/tmp/nexamain_*.dump ~/Backups/odoo-nexa/ +``` + +Then install: +``` +ssh odoo-nexa "docker exec odoo-nexa-app odoo -c /etc/odoo/odoo.conf -d nexamain -i nexa_coa_setup --no-http --stop-after-init" +``` + +## Update (when adding new accounts / taxes / fiscal positions) + +1. Edit XML in `data/` or hooks in `hooks.py` +2. Sync to server: + ``` + rsync -avz /Users/gurpreet/Github/Odoo-Modules/nexa_coa_setup/ odoo-nexa:/opt/odoo/custom-addons/nexa_coa_setup/ + ``` +3. Run -u: + ``` + ssh odoo-nexa "docker exec odoo-nexa-app odoo -c /etc/odoo/odoo.conf -d nexamain -u nexa_coa_setup --no-http --stop-after-init" + ``` + +## Adding a new account + +1. Append a `` to `data/01_account_account.xml` (use the next free code in the appropriate range) +2. If it's a service/COGS account, also add a matching `product.category` in `data/07_product_category.xml` +3. Sync and update (see above) + +## Adding a new project (analytic account) + +Don't put projects in XML — create via UI as projects are dynamic. + +In Odoo: Accounting → Configuration → Analytic Accounting → Analytic Accounts → New +- Name: descriptive (e.g., "Westin ERP Phase 2") +- Code: `PRJ-YYYY-{CUST}-{SHORTNAME}` +- Plan: Project + +## Yearly tasks + +- **Jan**: review CCA classes for new asset purchases; ensure assets created against correct 151xxx account +- **Feb**: prepare T2 with accountant; allocate associated-group SBD via Schedule 23 +- **Mar**: HST annual return due (March 31 for Dec 31 year-end) +- **Apr**: review fiscal year lock — set to previous Dec 31 once T2 is filed +- **Sep–Dec**: SR&ED analytic report pull; provide to accountant for T661 prep +- **Dec**: year-end review of intercompany pricing for transfer-pricing compliance + +## Restore (if catastrophic) + +``` +ssh odoo-nexa "docker exec odoo-nexa-db psql -U odoo -d postgres -c 'DROP DATABASE nexamain;'" +ssh odoo-nexa "docker exec odoo-nexa-db psql -U odoo -d postgres -c 'CREATE DATABASE nexamain OWNER odoo;'" +ssh odoo-nexa "docker exec odoo-nexa-db pg_restore -U odoo -d nexamain -j 4 /tmp/nexamain_.dump" +ssh odoo-nexa "docker restart odoo-nexa-app" +``` +``` + +- [ ] **Step 2: Commit** + +Run: +```bash +cd /Users/gurpreet/Github/Odoo-Modules +git add nexa_coa_setup/README.md +git commit -m "docs(nexa_coa_setup): operating runbook" +``` + +--- + +## Out of Scope (Future Sub-Projects) + +These are intentionally NOT in this plan. Each warrants its own spec + plan cycle: + +1. **Bank feeds via Plaid** — Odoo Enterprise has Plaid integration; needs credentials, banking partner OAuth, ongoing rule-tuning. Independent from CoA setup. +2. **Historical data reconciliation** — once accountant's Excel records arrive, mapping old transactions into the new account structure. Requires careful approach to respect the 2025-12-31 lock; possibly involves un-locking, posting, re-locking. +3. **Custom Canadian CCA module** — declining-balance + half-year + AccII automation. Only worth it once asset count >50. +4. **Payroll integration** — when first T4 employee is hired; integrate Wagepoint/ADP or Odoo Payroll, drive 215xxx source deductions automatically. +5. **Multi-company Odoo migration** — Westin and Divine currently on separate Odoo instances; future consolidation enables auto-mirrored intercompany invoices. +6. **Approval workflows** — purchase approval thresholds, expense approval per-department limits. +7. **Multi-currency** — USD bank account and currency-rate-live when first US client onboards. +8. **Subscription module configuration** — `sale_subscription` is installed; configuring recurring-billing templates per SaaS product, Stripe integration, auto-renewal email cadence is a per-customer flow handled when first SaaS contract is sold. +9. **Bank journal consolidation** — Section 10 Phase 4 of the spec calls for auditing the 7 bank journals; this is a manual decision per-account (which to keep, which to archive). Best done with the accountant during the historical reconciliation sub-project. + +--- + +## Self-Review Note + +This plan was self-reviewed for: + +- **Placeholder scan**: no "TBD", "TODO later", or "similar to Task N" — every step contains the actual content needed. +- **Spec coverage**: every section of `docs/superpowers/specs/2026-05-12-nexa-coa-design.md` maps to a phase/task: + - Section 3 Skeleton → Phase 2 Tasks 2.1-2.6 + - Section 4 Revenue → Phase 2 Task 2.4 + - Section 5 COGS → Phase 2 Task 2.5 + - Section 6 OpEx → Phase 2 Task 2.6 + - Section 7 Capital Assets → Phase 2 Task 2.1 (cost accounts; asset models created on first asset purchase) + - Section 8 Tax Accounts → Phase 2 Task 2.2 (213xxx) + Phase 5 (cleanup) + - Section 9 Shareholder/Associated → Phase 2 Task 2.2 (221xxx, 222xxx) + Phase 8 (Westin/Divine partners) + - Section 10 Analytic Plans → Phase 3 + - Section 11 Tax Setup/Fiscal Positions → Phase 5 + Phase 6 + - Section 12 Cleanup Plan → Phase 4 (archive + rename hooks) + Phase 5 (taxes) + Phase 11 (lock fiscal year via hook) + - Section 13 Automation Hooks → Phase 7 (product categories) + Phase 9 (bank rec rules) + - Section 17 Acceptance Criteria → Phase 10 (verification tests) + Phase 11 Task 11.2 (production validation query) +- **Type consistency**: all XMLIDs use the `acct_`/`fp_`/`pc_`/`aa_`/`partner_`/`rule_` prefixes consistently; all account references in Phase 9 (reconcile rules) and Phase 7 (product cats) use account XMLIDs defined in Phase 2. +- **Known potential issues flagged inline**: account 511100 collision (Task 4.2 Step 2 has resolution procedure).