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) <noreply@anthropic.com>
2798 lines
113 KiB
Markdown
2798 lines
113 KiB
Markdown
# 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 `<odoo><data noupdate="0">` 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 '<?xml version="1.0" encoding="utf-8"?>\n<odoo>\n <data noupdate="0">\n </data>\n</odoo>\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
|
||
<?xml version="1.0" encoding="utf-8"?>
|
||
<odoo>
|
||
<data noupdate="0">
|
||
|
||
<!-- ============================================================
|
||
1xxxxx ASSETS
|
||
============================================================ -->
|
||
|
||
<!-- 115xxx Due From Shareholder / Associated Corps -->
|
||
<record id="acct_115100" model="account.account">
|
||
<field name="code">115100</field>
|
||
<field name="name">Due From Shareholder — Gurpreet</field>
|
||
<field name="account_type">asset_current</field>
|
||
<field name="reconcile" eval="True"/>
|
||
</record>
|
||
|
||
<record id="acct_115900" model="account.account">
|
||
<field name="code">115900</field>
|
||
<field name="name">Due From Associated Corporations</field>
|
||
<field name="account_type">asset_current</field>
|
||
<field name="reconcile" eval="True"/>
|
||
</record>
|
||
|
||
<!-- 118xxx Tax Assets -->
|
||
<record id="acct_118100" model="account.account">
|
||
<field name="code">118100</field>
|
||
<field name="name">HST/GST Input Tax Credit (ITC) Receivable</field>
|
||
<field name="account_type">asset_current</field>
|
||
</record>
|
||
|
||
<record id="acct_118200" model="account.account">
|
||
<field name="code">118200</field>
|
||
<field name="name">HST/GST Instalments Paid</field>
|
||
<field name="account_type">asset_current</field>
|
||
</record>
|
||
|
||
<record id="acct_118300" model="account.account">
|
||
<field name="code">118300</field>
|
||
<field name="name">QST Input Tax Refund Receivable</field>
|
||
<field name="account_type">asset_current</field>
|
||
</record>
|
||
|
||
<!-- 151xxx Capital Assets — Cost -->
|
||
<record id="acct_151100" model="account.account">
|
||
<field name="code">151100</field>
|
||
<field name="name">Computer Hardware & Equipment (CCA Class 50)</field>
|
||
<field name="account_type">asset_fixed</field>
|
||
</record>
|
||
|
||
<record id="acct_151200" model="account.account">
|
||
<field name="code">151200</field>
|
||
<field name="name">Office Furniture & Equipment (CCA Class 8)</field>
|
||
<field name="account_type">asset_fixed</field>
|
||
</record>
|
||
|
||
<record id="acct_151300" model="account.account">
|
||
<field name="code">151300</field>
|
||
<field name="name">Vehicles (CCA Class 10/10.1)</field>
|
||
<field name="account_type">asset_fixed</field>
|
||
</record>
|
||
|
||
<record id="acct_151400" model="account.account">
|
||
<field name="code">151400</field>
|
||
<field name="name">Leasehold Improvements (CCA Class 13)</field>
|
||
<field name="account_type">asset_fixed</field>
|
||
</record>
|
||
|
||
<record id="acct_151500" model="account.account">
|
||
<field name="code">151500</field>
|
||
<field name="name">Acquired Software & Intangibles (CCA Class 14.1)</field>
|
||
<field name="account_type">asset_fixed</field>
|
||
</record>
|
||
|
||
<record id="acct_151600" model="account.account">
|
||
<field name="code">151600</field>
|
||
<field name="name">Tools & Small Equipment <$500 (CCA Class 12)</field>
|
||
<field name="account_type">asset_fixed</field>
|
||
</record>
|
||
|
||
<!-- 154xxx Accumulated Depreciation (contra) -->
|
||
<record id="acct_154100" model="account.account">
|
||
<field name="code">154100</field>
|
||
<field name="name">Acc. Depreciation — Computer Hardware</field>
|
||
<field name="account_type">asset_fixed</field>
|
||
</record>
|
||
|
||
<record id="acct_154200" model="account.account">
|
||
<field name="code">154200</field>
|
||
<field name="name">Acc. Depreciation — Office Furniture</field>
|
||
<field name="account_type">asset_fixed</field>
|
||
</record>
|
||
|
||
<record id="acct_154300" model="account.account">
|
||
<field name="code">154300</field>
|
||
<field name="name">Acc. Depreciation — Vehicles</field>
|
||
<field name="account_type">asset_fixed</field>
|
||
</record>
|
||
|
||
<record id="acct_154400" model="account.account">
|
||
<field name="code">154400</field>
|
||
<field name="name">Acc. Depreciation — Leasehold Improvements</field>
|
||
<field name="account_type">asset_fixed</field>
|
||
</record>
|
||
|
||
<record id="acct_154500" model="account.account">
|
||
<field name="code">154500</field>
|
||
<field name="name">Acc. Depreciation — Acquired Software</field>
|
||
<field name="account_type">asset_fixed</field>
|
||
</record>
|
||
|
||
</data>
|
||
</odoo>
|
||
```
|
||
|
||
- [ ] **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 `</data>` closing tag**
|
||
|
||
Add the following block just before the `</data>` line in `01_account_account.xml`:
|
||
```xml
|
||
<!-- ============================================================
|
||
2xxxxx LIABILITIES
|
||
============================================================ -->
|
||
|
||
<!-- 213xxx Tax Liabilities -->
|
||
<record id="acct_213100" model="account.account">
|
||
<field name="code">213100</field>
|
||
<field name="name">HST/GST Collected on Sales</field>
|
||
<field name="account_type">liability_current</field>
|
||
</record>
|
||
|
||
<record id="acct_213500" model="account.account">
|
||
<field name="code">213500</field>
|
||
<field name="name">QST Collected on Sales</field>
|
||
<field name="account_type">liability_current</field>
|
||
</record>
|
||
|
||
<record id="acct_214100" model="account.account">
|
||
<field name="code">214100</field>
|
||
<field name="name">Net HST/GST Payable</field>
|
||
<field name="account_type">liability_current</field>
|
||
</record>
|
||
|
||
<!-- 215xxx Source Deductions Payable (T4 payroll) -->
|
||
<record id="acct_215100" model="account.account">
|
||
<field name="code">215100</field>
|
||
<field name="name">Source Deductions Payable — Federal Tax</field>
|
||
<field name="account_type">liability_current</field>
|
||
</record>
|
||
|
||
<record id="acct_215200" model="account.account">
|
||
<field name="code">215200</field>
|
||
<field name="name">Source Deductions Payable — CPP</field>
|
||
<field name="account_type">liability_current</field>
|
||
</record>
|
||
|
||
<record id="acct_215300" model="account.account">
|
||
<field name="code">215300</field>
|
||
<field name="name">Source Deductions Payable — EI</field>
|
||
<field name="account_type">liability_current</field>
|
||
</record>
|
||
|
||
<!-- 216xxx Corporate Income Tax -->
|
||
<record id="acct_216100" model="account.account">
|
||
<field name="code">216100</field>
|
||
<field name="name">Corporate Income Tax — Federal Payable</field>
|
||
<field name="account_type">liability_current</field>
|
||
</record>
|
||
|
||
<record id="acct_216200" model="account.account">
|
||
<field name="code">216200</field>
|
||
<field name="name">Corporate Income Tax — Provincial Payable</field>
|
||
<field name="account_type">liability_current</field>
|
||
</record>
|
||
|
||
<record id="acct_216300" model="account.account">
|
||
<field name="code">216300</field>
|
||
<field name="name">Corporate Tax Instalments Paid</field>
|
||
<field name="account_type">asset_current</field>
|
||
</record>
|
||
|
||
<!-- 221xxx Due To Shareholder -->
|
||
<record id="acct_221100" model="account.account">
|
||
<field name="code">221100</field>
|
||
<field name="name">Due To Shareholder — Gurpreet (short-term)</field>
|
||
<field name="account_type">liability_current</field>
|
||
<field name="reconcile" eval="True"/>
|
||
</record>
|
||
|
||
<record id="acct_221200" model="account.account">
|
||
<field name="code">221200</field>
|
||
<field name="name">Shareholder Loan — Gurpreet (long-term)</field>
|
||
<field name="account_type">liability_non_current</field>
|
||
<field name="reconcile" eval="True"/>
|
||
</record>
|
||
|
||
<!-- 222xxx Due To Associated Corps (intercompany loans only) -->
|
||
<record id="acct_222900" model="account.account">
|
||
<field name="code">222900</field>
|
||
<field name="name">Due To Associated Corporations</field>
|
||
<field name="account_type">liability_current</field>
|
||
<field name="reconcile" eval="True"/>
|
||
</record>
|
||
```
|
||
|
||
- [ ] **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 `</data>`**
|
||
|
||
Add this block before `</data>`:
|
||
```xml
|
||
<!-- ============================================================
|
||
3xxxxx EQUITY
|
||
============================================================ -->
|
||
|
||
<record id="acct_311100" model="account.account">
|
||
<field name="code">311100</field>
|
||
<field name="name">Share Capital — Common Shares</field>
|
||
<field name="account_type">equity</field>
|
||
</record>
|
||
|
||
<record id="acct_311200" model="account.account">
|
||
<field name="code">311200</field>
|
||
<field name="name">Share Capital — Preferred Shares</field>
|
||
<field name="account_type">equity</field>
|
||
</record>
|
||
|
||
<record id="acct_311300" model="account.account">
|
||
<field name="code">311300</field>
|
||
<field name="name">Contributed Surplus</field>
|
||
<field name="account_type">equity</field>
|
||
</record>
|
||
|
||
<record id="acct_321100" model="account.account">
|
||
<field name="code">321100</field>
|
||
<field name="name">Retained Earnings — Current Year</field>
|
||
<field name="account_type">equity</field>
|
||
</record>
|
||
|
||
<record id="acct_321200" model="account.account">
|
||
<field name="code">321200</field>
|
||
<field name="name">Retained Earnings — Prior Years</field>
|
||
<field name="account_type">equity</field>
|
||
</record>
|
||
|
||
<record id="acct_321900" model="account.account">
|
||
<field name="code">321900</field>
|
||
<field name="name">Dividends Declared</field>
|
||
<field name="account_type">equity</field>
|
||
</record>
|
||
```
|
||
|
||
- [ ] **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 `</data>`**
|
||
|
||
Add this block before `</data>`:
|
||
```xml
|
||
<!-- ============================================================
|
||
4xxxxx REVENUE (one account per service line; jurisdiction via tax)
|
||
============================================================ -->
|
||
|
||
<!-- Recurring Revenue -->
|
||
<record id="acct_411100" model="account.account">
|
||
<field name="code">411100</field>
|
||
<field name="name">SaaS Subscription Revenue</field>
|
||
<field name="account_type">income</field>
|
||
</record>
|
||
<record id="acct_411200" model="account.account">
|
||
<field name="code">411200</field>
|
||
<field name="name">Hosting & Infrastructure Revenue</field>
|
||
<field name="account_type">income</field>
|
||
</record>
|
||
<record id="acct_411300" model="account.account">
|
||
<field name="code">411300</field>
|
||
<field name="name">Support & Maintenance Contracts Revenue</field>
|
||
<field name="account_type">income</field>
|
||
</record>
|
||
<record id="acct_411400" model="account.account">
|
||
<field name="code">411400</field>
|
||
<field name="name">Domain/SSL/Renewal Pass-through Revenue</field>
|
||
<field name="account_type">income</field>
|
||
</record>
|
||
<record id="acct_411500" model="account.account">
|
||
<field name="code">411500</field>
|
||
<field name="name">Setup / Onboarding Fees Revenue</field>
|
||
<field name="account_type">income</field>
|
||
</record>
|
||
|
||
<!-- Project Revenue -->
|
||
<record id="acct_412100" model="account.account">
|
||
<field name="code">412100</field>
|
||
<field name="name">Custom Software Development Revenue</field>
|
||
<field name="account_type">income</field>
|
||
</record>
|
||
<record id="acct_412200" model="account.account">
|
||
<field name="code">412200</field>
|
||
<field name="name">Custom Web Application Development Revenue</field>
|
||
<field name="account_type">income</field>
|
||
</record>
|
||
<record id="acct_412300" model="account.account">
|
||
<field name="code">412300</field>
|
||
<field name="name">Custom Website Development Revenue</field>
|
||
<field name="account_type">income</field>
|
||
</record>
|
||
<record id="acct_412400" model="account.account">
|
||
<field name="code">412400</field>
|
||
<field name="name">ERP Implementation & Customization Revenue</field>
|
||
<field name="account_type">income</field>
|
||
</record>
|
||
<record id="acct_412500" model="account.account">
|
||
<field name="code">412500</field>
|
||
<field name="name">Mobile App Development Revenue</field>
|
||
<field name="account_type">income</field>
|
||
</record>
|
||
<record id="acct_412600" model="account.account">
|
||
<field name="code">412600</field>
|
||
<field name="name">Business App / Integration Revenue</field>
|
||
<field name="account_type">income</field>
|
||
</record>
|
||
|
||
<!-- Services -->
|
||
<record id="acct_413100" model="account.account">
|
||
<field name="code">413100</field>
|
||
<field name="name">Consulting & Advisory Revenue</field>
|
||
<field name="account_type">income</field>
|
||
</record>
|
||
<record id="acct_413200" model="account.account">
|
||
<field name="code">413200</field>
|
||
<field name="name">Training & Workshops Revenue</field>
|
||
<field name="account_type">income</field>
|
||
</record>
|
||
<record id="acct_413300" model="account.account">
|
||
<field name="code">413300</field>
|
||
<field name="name">Technical Support — Per-incident / Hourly Revenue</field>
|
||
<field name="account_type">income</field>
|
||
</record>
|
||
|
||
<!-- Reseller -->
|
||
<record id="acct_414100" model="account.account">
|
||
<field name="code">414100</field>
|
||
<field name="name">Third-party Software Resale Revenue</field>
|
||
<field name="account_type">income</field>
|
||
</record>
|
||
<record id="acct_414200" model="account.account">
|
||
<field name="code">414200</field>
|
||
<field name="name">Hardware Resale Revenue</field>
|
||
<field name="account_type">income</field>
|
||
</record>
|
||
|
||
<!-- Adjustments (contra) -->
|
||
<record id="acct_419100" model="account.account">
|
||
<field name="code">419100</field>
|
||
<field name="name">Sales Discounts</field>
|
||
<field name="account_type">income</field>
|
||
</record>
|
||
<record id="acct_419200" model="account.account">
|
||
<field name="code">419200</field>
|
||
<field name="name">Sales Returns & Refunds</field>
|
||
<field name="account_type">income</field>
|
||
</record>
|
||
<record id="acct_419300" model="account.account">
|
||
<field name="code">419300</field>
|
||
<field name="name">Bad Debt Recovery</field>
|
||
<field name="account_type">income_other</field>
|
||
</record>
|
||
```
|
||
|
||
- [ ] **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 `</data>`**
|
||
|
||
Add this block:
|
||
```xml
|
||
<!-- ============================================================
|
||
5xxxxx DIRECT COSTS (COGS)
|
||
============================================================ -->
|
||
|
||
<!-- Infrastructure & Hosting -->
|
||
<record id="acct_511100" model="account.account">
|
||
<field name="code">511100</field>
|
||
<field name="name">Cloud Infrastructure (AWS, Hetzner, OVH, DigitalOcean, Linode)</field>
|
||
<field name="account_type">expense_direct_cost</field>
|
||
</record>
|
||
<record id="acct_511110" model="account.account">
|
||
<field name="code">511110</field>
|
||
<field name="name">CDN & Edge Services (Cloudflare, Fastly)</field>
|
||
<field name="account_type">expense_direct_cost</field>
|
||
</record>
|
||
<record id="acct_511120" model="account.account">
|
||
<field name="code">511120</field>
|
||
<field name="name">Backup & Storage Services</field>
|
||
<field name="account_type">expense_direct_cost</field>
|
||
</record>
|
||
<record id="acct_511130" model="account.account">
|
||
<field name="code">511130</field>
|
||
<field name="name">Database & Backend Services (Supabase, hosted Postgres, Redis)</field>
|
||
<field name="account_type">expense_direct_cost</field>
|
||
</record>
|
||
<record id="acct_511140" model="account.account">
|
||
<field name="code">511140</field>
|
||
<field name="name">Monitoring & Observability (customer-facing only)</field>
|
||
<field name="account_type">expense_direct_cost</field>
|
||
</record>
|
||
<record id="acct_511150" model="account.account">
|
||
<field name="code">511150</field>
|
||
<field name="name">SSL Certificates & Domains (wholesale for resale)</field>
|
||
<field name="account_type">expense_direct_cost</field>
|
||
</record>
|
||
<record id="acct_511160" model="account.account">
|
||
<field name="code">511160</field>
|
||
<field name="name">DNS & Email Hosting (wholesale for resale)</field>
|
||
<field name="account_type">expense_direct_cost</field>
|
||
</record>
|
||
|
||
<!-- Third-party APIs -->
|
||
<record id="acct_511200" model="account.account">
|
||
<field name="code">511200</field>
|
||
<field name="name">Third-party API Costs (Twilio, SendGrid, OpenAI)</field>
|
||
<field name="account_type">expense_direct_cost</field>
|
||
</record>
|
||
<record id="acct_511210" model="account.account">
|
||
<field name="code">511210</field>
|
||
<field name="name">Per-customer Licensing & Royalties</field>
|
||
<field name="account_type">expense_direct_cost</field>
|
||
</record>
|
||
|
||
<!-- Project Direct Costs -->
|
||
<record id="acct_512100" model="account.account">
|
||
<field name="code">512100</field>
|
||
<field name="name">Subcontracted Labour — Canadian (T4A) — SR&ED-eligible</field>
|
||
<field name="account_type">expense_direct_cost</field>
|
||
</record>
|
||
<record id="acct_512110" model="account.account">
|
||
<field name="code">512110</field>
|
||
<field name="name">Subcontracted Labour — Foreign — NOT SR&ED-eligible</field>
|
||
<field name="account_type">expense_direct_cost</field>
|
||
</record>
|
||
<record id="acct_512200" model="account.account">
|
||
<field name="code">512200</field>
|
||
<field name="name">Project-specific Software & Licenses</field>
|
||
<field name="account_type">expense_direct_cost</field>
|
||
</record>
|
||
<record id="acct_512300" model="account.account">
|
||
<field name="code">512300</field>
|
||
<field name="name">Project Travel & Onsite (rebilled)</field>
|
||
<field name="account_type">expense_direct_cost</field>
|
||
</record>
|
||
<record id="acct_512400" model="account.account">
|
||
<field name="code">512400</field>
|
||
<field name="name">Project Hardware (passed through)</field>
|
||
<field name="account_type">expense_direct_cost</field>
|
||
</record>
|
||
|
||
<!-- Resold Goods -->
|
||
<record id="acct_513100" model="account.account">
|
||
<field name="code">513100</field>
|
||
<field name="name">Cost of Software Resold</field>
|
||
<field name="account_type">expense_direct_cost</field>
|
||
</record>
|
||
<record id="acct_513200" model="account.account">
|
||
<field name="code">513200</field>
|
||
<field name="name">Cost of Hardware Resold</field>
|
||
<field name="account_type">expense_direct_cost</field>
|
||
</record>
|
||
|
||
<!-- Adjustments -->
|
||
<record id="acct_519100" model="account.account">
|
||
<field name="code">519100</field>
|
||
<field name="name">COGS Adjustments / Write-offs</field>
|
||
<field name="account_type">expense_direct_cost</field>
|
||
</record>
|
||
```
|
||
|
||
- [ ] **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 `</data>`**
|
||
|
||
Add this block:
|
||
```xml
|
||
<!-- ============================================================
|
||
6xxxxx OPERATING EXPENSES
|
||
============================================================ -->
|
||
|
||
<!-- 611xxx Personnel — Internal Staff (T4) -->
|
||
<record id="acct_611100" model="account.account">
|
||
<field name="code">611100</field>
|
||
<field name="name">Salaries & Wages — Development (SR&ED-eligible)</field>
|
||
<field name="account_type">expense</field>
|
||
</record>
|
||
<record id="acct_611200" model="account.account">
|
||
<field name="code">611200</field>
|
||
<field name="name">Salaries & Wages — Sales & Marketing</field>
|
||
<field name="account_type">expense</field>
|
||
</record>
|
||
<record id="acct_611300" model="account.account">
|
||
<field name="code">611300</field>
|
||
<field name="name">Salaries & Wages — Admin & Operations</field>
|
||
<field name="account_type">expense</field>
|
||
</record>
|
||
<record id="acct_611400" model="account.account">
|
||
<field name="code">611400</field>
|
||
<field name="name">Salary — Shareholder/Officer (Gurpreet)</field>
|
||
<field name="account_type">expense</field>
|
||
</record>
|
||
<record id="acct_611500" model="account.account">
|
||
<field name="code">611500</field>
|
||
<field name="name">Employer CPP / QPP Contributions</field>
|
||
<field name="account_type">expense</field>
|
||
</record>
|
||
<record id="acct_611600" model="account.account">
|
||
<field name="code">611600</field>
|
||
<field name="name">Employer EI Premiums</field>
|
||
<field name="account_type">expense</field>
|
||
</record>
|
||
<record id="acct_611700" model="account.account">
|
||
<field name="code">611700</field>
|
||
<field name="name">Employer Health Tax (EHT/QHST)</field>
|
||
<field name="account_type">expense</field>
|
||
</record>
|
||
<record id="acct_611800" model="account.account">
|
||
<field name="code">611800</field>
|
||
<field name="name">WCB / WSIB Premiums</field>
|
||
<field name="account_type">expense</field>
|
||
</record>
|
||
<record id="acct_611900" model="account.account">
|
||
<field name="code">611900</field>
|
||
<field name="name">Employee Benefits (health, dental, group)</field>
|
||
<field name="account_type">expense</field>
|
||
</record>
|
||
<record id="acct_611950" model="account.account">
|
||
<field name="code">611950</field>
|
||
<field name="name">Bonuses & Incentives</field>
|
||
<field name="account_type">expense</field>
|
||
</record>
|
||
<record id="acct_611960" model="account.account">
|
||
<field name="code">611960</field>
|
||
<field name="name">Vacation Pay Accrual</field>
|
||
<field name="account_type">expense</field>
|
||
</record>
|
||
|
||
<!-- 612xxx Personnel — Contract (non-project) -->
|
||
<record id="acct_612100" model="account.account">
|
||
<field name="code">612100</field>
|
||
<field name="name">Contract Labour — Canadian (admin/marketing/freelance)</field>
|
||
<field name="account_type">expense</field>
|
||
</record>
|
||
<record id="acct_612200" model="account.account">
|
||
<field name="code">612200</field>
|
||
<field name="name">Contract Labour — Foreign</field>
|
||
<field name="account_type">expense</field>
|
||
</record>
|
||
|
||
<!-- 621xxx Office & Facilities -->
|
||
<record id="acct_621100" model="account.account">
|
||
<field name="code">621100</field>
|
||
<field name="name">Rent — Commercial Office</field>
|
||
<field name="account_type">expense</field>
|
||
</record>
|
||
<record id="acct_621200" model="account.account">
|
||
<field name="code">621200</field>
|
||
<field name="name">Home Office — Business Portion</field>
|
||
<field name="account_type">expense</field>
|
||
</record>
|
||
<record id="acct_621300" model="account.account">
|
||
<field name="code">621300</field>
|
||
<field name="name">Utilities — Commercial</field>
|
||
<field name="account_type">expense</field>
|
||
</record>
|
||
<record id="acct_621400" model="account.account">
|
||
<field name="code">621400</field>
|
||
<field name="name">Internet & Phone — Business</field>
|
||
<field name="account_type">expense</field>
|
||
</record>
|
||
<record id="acct_621500" model="account.account">
|
||
<field name="code">621500</field>
|
||
<field name="name">Office Supplies & Consumables</field>
|
||
<field name="account_type">expense</field>
|
||
</record>
|
||
<record id="acct_621600" model="account.account">
|
||
<field name="code">621600</field>
|
||
<field name="name">Cleaning & Maintenance</field>
|
||
<field name="account_type">expense</field>
|
||
</record>
|
||
<record id="acct_621700" model="account.account">
|
||
<field name="code">621700</field>
|
||
<field name="name">Office Snacks & Refreshments</field>
|
||
<field name="account_type">expense</field>
|
||
</record>
|
||
|
||
<!-- 631xxx Technology — Operating -->
|
||
<record id="acct_631100" model="account.account">
|
||
<field name="code">631100</field>
|
||
<field name="name">Software — Productivity (M365, Slack, Notion, Linear, GitHub)</field>
|
||
<field name="account_type">expense</field>
|
||
</record>
|
||
<record id="acct_631200" model="account.account">
|
||
<field name="code">631200</field>
|
||
<field name="name">Software — Development Tools (Cursor, Figma, IDEs)</field>
|
||
<field name="account_type">expense</field>
|
||
</record>
|
||
<record id="acct_631300" model="account.account">
|
||
<field name="code">631300</field>
|
||
<field name="name">Software — Internal Infrastructure</field>
|
||
<field name="account_type">expense</field>
|
||
</record>
|
||
<record id="acct_631400" model="account.account">
|
||
<field name="code">631400</field>
|
||
<field name="name">Software — Security & IT</field>
|
||
<field name="account_type">expense</field>
|
||
</record>
|
||
<record id="acct_631500" model="account.account">
|
||
<field name="code">631500</field>
|
||
<field name="name">Software — Sales & Marketing</field>
|
||
<field name="account_type">expense</field>
|
||
</record>
|
||
|
||
<!-- 641xxx Marketing & Sales -->
|
||
<record id="acct_641100" model="account.account">
|
||
<field name="code">641100</field>
|
||
<field name="name">Advertising — Digital Ads</field>
|
||
<field name="account_type">expense</field>
|
||
</record>
|
||
<record id="acct_641200" model="account.account">
|
||
<field name="code">641200</field>
|
||
<field name="name">Advertising — Content / SEO</field>
|
||
<field name="account_type">expense</field>
|
||
</record>
|
||
<record id="acct_641300" model="account.account">
|
||
<field name="code">641300</field>
|
||
<field name="name">Trade Shows & Conferences</field>
|
||
<field name="account_type">expense</field>
|
||
</record>
|
||
<record id="acct_641400" model="account.account">
|
||
<field name="code">641400</field>
|
||
<field name="name">Promotional Items / Branded Swag</field>
|
||
<field name="account_type">expense</field>
|
||
</record>
|
||
<record id="acct_641500" model="account.account">
|
||
<field name="code">641500</field>
|
||
<field name="name">Website — Own (nexasystems.ca)</field>
|
||
<field name="account_type">expense</field>
|
||
</record>
|
||
|
||
<!-- 651xxx Professional Fees -->
|
||
<record id="acct_651100" model="account.account">
|
||
<field name="code">651100</field>
|
||
<field name="name">Legal Fees — General</field>
|
||
<field name="account_type">expense</field>
|
||
</record>
|
||
<record id="acct_651200" model="account.account">
|
||
<field name="code">651200</field>
|
||
<field name="name">Accounting & Bookkeeping</field>
|
||
<field name="account_type">expense</field>
|
||
</record>
|
||
<record id="acct_651300" model="account.account">
|
||
<field name="code">651300</field>
|
||
<field name="name">Tax Preparation (T2, T1, GST/HST)</field>
|
||
<field name="account_type">expense</field>
|
||
</record>
|
||
<record id="acct_651400" model="account.account">
|
||
<field name="code">651400</field>
|
||
<field name="name">Business Consulting</field>
|
||
<field name="account_type">expense</field>
|
||
</record>
|
||
|
||
<!-- 661xxx Insurance -->
|
||
<record id="acct_661100" model="account.account">
|
||
<field name="code">661100</field>
|
||
<field name="name">Insurance — Commercial General Liability</field>
|
||
<field name="account_type">expense</field>
|
||
</record>
|
||
<record id="acct_661200" model="account.account">
|
||
<field name="code">661200</field>
|
||
<field name="name">Insurance — Professional Liability / E&O</field>
|
||
<field name="account_type">expense</field>
|
||
</record>
|
||
<record id="acct_661300" model="account.account">
|
||
<field name="code">661300</field>
|
||
<field name="name">Insurance — Cyber Liability</field>
|
||
<field name="account_type">expense</field>
|
||
</record>
|
||
<record id="acct_661400" model="account.account">
|
||
<field name="code">661400</field>
|
||
<field name="name">Insurance — Property</field>
|
||
<field name="account_type">expense</field>
|
||
</record>
|
||
<record id="acct_661500" model="account.account">
|
||
<field name="code">661500</field>
|
||
<field name="name">Insurance — Directors & Officers</field>
|
||
<field name="account_type">expense</field>
|
||
</record>
|
||
|
||
<!-- 671xxx Travel & Entertainment -->
|
||
<record id="acct_671100" model="account.account">
|
||
<field name="code">671100</field>
|
||
<field name="name">Travel — Flights, Hotels, Ground Transport</field>
|
||
<field name="account_type">expense</field>
|
||
</record>
|
||
<record id="acct_671200" model="account.account">
|
||
<field name="code">671200</field>
|
||
<field name="name">Meals & Entertainment — 50% Deductible</field>
|
||
<field name="account_type">expense</field>
|
||
</record>
|
||
<record id="acct_671300" model="account.account">
|
||
<field name="code">671300</field>
|
||
<field name="name">Vehicle — Operating (gas, insurance, repairs, parking)</field>
|
||
<field name="account_type">expense</field>
|
||
</record>
|
||
<record id="acct_671400" model="account.account">
|
||
<field name="code">671400</field>
|
||
<field name="name">Mileage Reimbursement — Personal Vehicle</field>
|
||
<field name="account_type">expense</field>
|
||
</record>
|
||
|
||
<!-- 681xxx Training & Development -->
|
||
<record id="acct_681100" model="account.account">
|
||
<field name="code">681100</field>
|
||
<field name="name">Conferences & Seminars (registration)</field>
|
||
<field name="account_type">expense</field>
|
||
</record>
|
||
<record id="acct_681200" model="account.account">
|
||
<field name="code">681200</field>
|
||
<field name="name">Courses & Certifications</field>
|
||
<field name="account_type">expense</field>
|
||
</record>
|
||
<record id="acct_681300" model="account.account">
|
||
<field name="code">681300</field>
|
||
<field name="name">Books & Publications</field>
|
||
<field name="account_type">expense</field>
|
||
</record>
|
||
<record id="acct_681400" model="account.account">
|
||
<field name="code">681400</field>
|
||
<field name="name">Professional Memberships & Dues</field>
|
||
<field name="account_type">expense</field>
|
||
</record>
|
||
|
||
<!-- 691xxx Banking & Finance -->
|
||
<record id="acct_691100" model="account.account">
|
||
<field name="code">691100</field>
|
||
<field name="name">Bank Service Charges</field>
|
||
<field name="account_type">expense</field>
|
||
</record>
|
||
<record id="acct_691200" model="account.account">
|
||
<field name="code">691200</field>
|
||
<field name="name">Merchant Processing Fees (Stripe, PayPal, Square)</field>
|
||
<field name="account_type">expense</field>
|
||
</record>
|
||
<record id="acct_691300" model="account.account">
|
||
<field name="code">691300</field>
|
||
<field name="name">Wire Transfer & FX Fees</field>
|
||
<field name="account_type">expense</field>
|
||
</record>
|
||
<record id="acct_691400" model="account.account">
|
||
<field name="code">691400</field>
|
||
<field name="name">Interest Expense — Bank Loans / LOC</field>
|
||
<field name="account_type">expense</field>
|
||
</record>
|
||
<record id="acct_691500" model="account.account">
|
||
<field name="code">691500</field>
|
||
<field name="name">Interest Expense — Credit Cards</field>
|
||
<field name="account_type">expense</field>
|
||
</record>
|
||
<record id="acct_691600" model="account.account">
|
||
<field name="code">691600</field>
|
||
<field name="name">Late Payment Penalties — Non-deductible</field>
|
||
<field name="account_type">expense</field>
|
||
</record>
|
||
|
||
<!-- 699xxx Other -->
|
||
<record id="acct_699100" model="account.account">
|
||
<field name="code">699100</field>
|
||
<field name="name">Bad Debt Expense</field>
|
||
<field name="account_type">expense</field>
|
||
</record>
|
||
<record id="acct_699200" model="account.account">
|
||
<field name="code">699200</field>
|
||
<field name="name">Donations & Sponsorships (deductible)</field>
|
||
<field name="account_type">expense</field>
|
||
</record>
|
||
<record id="acct_699300" model="account.account">
|
||
<field name="code">699300</field>
|
||
<field name="name">Penalties & Fines — Non-deductible</field>
|
||
<field name="account_type">expense</field>
|
||
</record>
|
||
<record id="acct_699400" model="account.account">
|
||
<field name="code">699400</field>
|
||
<field name="name">Realized FX Losses</field>
|
||
<field name="account_type">expense</field>
|
||
</record>
|
||
<record id="acct_699500" model="account.account">
|
||
<field name="code">699500</field>
|
||
<field name="name">Depreciation / CCA Expense</field>
|
||
<field name="account_type">expense</field>
|
||
</record>
|
||
```
|
||
|
||
- [ ] **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
|
||
<?xml version="1.0" encoding="utf-8"?>
|
||
<odoo>
|
||
<data noupdate="0">
|
||
|
||
<record id="plan_project" model="account.analytic.plan">
|
||
<field name="name">Project</field>
|
||
<field name="default_applicability">mandatory</field>
|
||
</record>
|
||
|
||
<record id="plan_department" model="account.analytic.plan">
|
||
<field name="name">Department</field>
|
||
<field name="default_applicability">mandatory</field>
|
||
</record>
|
||
|
||
<record id="plan_sred_tag" model="account.analytic.plan">
|
||
<field name="name">SR&ED Tag</field>
|
||
<field name="default_applicability">optional</field>
|
||
</record>
|
||
|
||
</data>
|
||
</odoo>
|
||
```
|
||
|
||
- [ ] **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
|
||
<?xml version="1.0" encoding="utf-8"?>
|
||
<odoo>
|
||
<data noupdate="0">
|
||
|
||
<!-- Departments -->
|
||
<record id="aa_dept_dev" model="account.analytic.account">
|
||
<field name="name">Development</field>
|
||
<field name="code">DEPT-DEV</field>
|
||
<field name="plan_id" ref="plan_department"/>
|
||
</record>
|
||
|
||
<record id="aa_dept_sales" model="account.analytic.account">
|
||
<field name="name">Sales & Marketing</field>
|
||
<field name="code">DEPT-SALES</field>
|
||
<field name="plan_id" ref="plan_department"/>
|
||
</record>
|
||
|
||
<record id="aa_dept_admin" model="account.analytic.account">
|
||
<field name="name">Admin & Operations</field>
|
||
<field name="code">DEPT-ADMIN</field>
|
||
<field name="plan_id" ref="plan_department"/>
|
||
</record>
|
||
|
||
<record id="aa_dept_hosting" model="account.analytic.account">
|
||
<field name="name">Hosting Operations</field>
|
||
<field name="code">DEPT-HOSTING</field>
|
||
<field name="plan_id" ref="plan_department"/>
|
||
</record>
|
||
|
||
<!-- SR&ED tag values (one analytic account per tag) -->
|
||
<record id="aa_sred_t4_dev" model="account.analytic.account">
|
||
<field name="name">T4 Dev Salary — full proxy</field>
|
||
<field name="code">SRED-T4-DEV-SALARY</field>
|
||
<field name="plan_id" ref="plan_sred_tag"/>
|
||
</record>
|
||
|
||
<record id="aa_sred_specified" model="account.analytic.account">
|
||
<field name="name">Specified Employee Salary — 75% cap</field>
|
||
<field name="code">SRED-SPECIFIED-EMPLOYEE</field>
|
||
<field name="plan_id" ref="plan_sred_tag"/>
|
||
</record>
|
||
|
||
<record id="aa_sred_contr_ca_arm" model="account.analytic.account">
|
||
<field name="name">Contractor CA Arm's Length — 80% eligible</field>
|
||
<field name="code">SRED-CONTRACTOR-CA-ARM-LENGTH</field>
|
||
<field name="plan_id" ref="plan_sred_tag"/>
|
||
</record>
|
||
|
||
<record id="aa_sred_contr_ca_naf" model="account.analytic.account">
|
||
<field name="name">Contractor CA Non-Arm's Length</field>
|
||
<field name="code">SRED-CONTRACTOR-CA-NON-ARM-LENGTH</field>
|
||
<field name="plan_id" ref="plan_sred_tag"/>
|
||
</record>
|
||
|
||
<record id="aa_sred_materials" model="account.analytic.account">
|
||
<field name="name">Materials Consumed in R&D</field>
|
||
<field name="code">SRED-MATERIALS-CONSUMED</field>
|
||
<field name="plan_id" ref="plan_sred_tag"/>
|
||
</record>
|
||
|
||
<record id="aa_sred_overhead_basis" model="account.analytic.account">
|
||
<field name="name">Overhead Proxy Basis (direct labour basis)</field>
|
||
<field name="code">SRED-OVERHEAD-PROXY-BASIS</field>
|
||
<field name="plan_id" ref="plan_sred_tag"/>
|
||
</record>
|
||
|
||
<record id="aa_sred_not_eligible" model="account.analytic.account">
|
||
<field name="name">Not Eligible (default)</field>
|
||
<field name="code">NOT-ELIGIBLE</field>
|
||
<field name="plan_id" ref="plan_sred_tag"/>
|
||
</record>
|
||
|
||
</data>
|
||
</odoo>
|
||
```
|
||
|
||
- [ ] **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
|
||
<?xml version="1.0" encoding="utf-8"?>
|
||
<odoo>
|
||
<data noupdate="0">
|
||
|
||
<!-- Helper note: tax substitutions use xmlids from l10n_ca module.
|
||
If those xmlids change between Odoo versions, regenerate this file. -->
|
||
|
||
<record id="fp_ca_ontario" model="account.fiscal.position">
|
||
<field name="name">CA — Ontario (Default)</field>
|
||
<field name="auto_apply" eval="True"/>
|
||
<field name="country_id" ref="base.ca"/>
|
||
<field name="state_ids" eval="[(6, 0, [ref('base.state_ca_on')])]"/>
|
||
</record>
|
||
|
||
<record id="fp_ca_atlantic" model="account.fiscal.position">
|
||
<field name="name">CA — Atlantic (HST 15%)</field>
|
||
<field name="auto_apply" eval="True"/>
|
||
<field name="country_id" ref="base.ca"/>
|
||
<field name="state_ids" eval="[(6, 0, [ref('base.state_ca_nb'), ref('base.state_ca_ns'), ref('base.state_ca_pe'), ref('base.state_ca_nl')])]"/>
|
||
</record>
|
||
|
||
<record id="fp_ca_quebec" model="account.fiscal.position">
|
||
<field name="name">CA — Quebec (GST + QST)</field>
|
||
<field name="auto_apply" eval="True"/>
|
||
<field name="country_id" ref="base.ca"/>
|
||
<field name="state_ids" eval="[(6, 0, [ref('base.state_ca_qc')])]"/>
|
||
</record>
|
||
|
||
<record id="fp_ca_bc" model="account.fiscal.position">
|
||
<field name="name">CA — British Columbia (GST 5%, PST per-product)</field>
|
||
<field name="auto_apply" eval="True"/>
|
||
<field name="country_id" ref="base.ca"/>
|
||
<field name="state_ids" eval="[(6, 0, [ref('base.state_ca_bc')])]"/>
|
||
</record>
|
||
|
||
<record id="fp_ca_prairies_territories" model="account.fiscal.position">
|
||
<field name="name">CA — Prairies / Territories (GST 5% only)</field>
|
||
<field name="auto_apply" eval="True"/>
|
||
<field name="country_id" ref="base.ca"/>
|
||
<field name="state_ids" eval="[(6, 0, [ref('base.state_ca_ab'), ref('base.state_ca_mb'), ref('base.state_ca_sk'), ref('base.state_ca_yt'), ref('base.state_ca_nt'), ref('base.state_ca_nu')])]"/>
|
||
</record>
|
||
|
||
<record id="fp_export_us" model="account.fiscal.position">
|
||
<field name="name">Export — United States (Zero-rated)</field>
|
||
<field name="auto_apply" eval="True"/>
|
||
<field name="country_id" ref="base.us"/>
|
||
</record>
|
||
|
||
<record id="fp_export_intl" model="account.fiscal.position">
|
||
<field name="name">Export — International (Zero-rated)</field>
|
||
<field name="auto_apply" eval="False"/>
|
||
<field name="note">Manually applied for non-CA/non-US customers. Auto-apply by country group requires custom rule.</field>
|
||
</record>
|
||
|
||
<record id="fp_tax_exempt" model="account.fiscal.position">
|
||
<field name="name">Tax Exempt (cert-holder)</field>
|
||
<field name="auto_apply" eval="False"/>
|
||
<field name="note">Apply manually to customers with valid exemption certificate on file. Document the certificate in the partner's notes.</field>
|
||
</record>
|
||
|
||
</data>
|
||
</odoo>
|
||
```
|
||
|
||
- [ ] **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
|
||
<?xml version="1.0" encoding="utf-8"?>
|
||
<odoo>
|
||
<data noupdate="0">
|
||
|
||
<!-- Parent: Services -->
|
||
<record id="pc_services" model="product.category">
|
||
<field name="name">Services</field>
|
||
</record>
|
||
|
||
<!-- Children, each with default income account -->
|
||
<record id="pc_saas" model="product.category">
|
||
<field name="name">SaaS Subscription</field>
|
||
<field name="parent_id" ref="pc_services"/>
|
||
<field name="property_account_income_categ_id" ref="acct_411100"/>
|
||
</record>
|
||
|
||
<record id="pc_hosting" model="product.category">
|
||
<field name="name">Hosting</field>
|
||
<field name="parent_id" ref="pc_services"/>
|
||
<field name="property_account_income_categ_id" ref="acct_411200"/>
|
||
</record>
|
||
|
||
<record id="pc_support" model="product.category">
|
||
<field name="name">Support Contract</field>
|
||
<field name="parent_id" ref="pc_services"/>
|
||
<field name="property_account_income_categ_id" ref="acct_411300"/>
|
||
</record>
|
||
|
||
<record id="pc_setup_fee" model="product.category">
|
||
<field name="name">Setup Fee</field>
|
||
<field name="parent_id" ref="pc_services"/>
|
||
<field name="property_account_income_categ_id" ref="acct_411500"/>
|
||
</record>
|
||
|
||
<record id="pc_custom_software" model="product.category">
|
||
<field name="name">Custom Software Development</field>
|
||
<field name="parent_id" ref="pc_services"/>
|
||
<field name="property_account_income_categ_id" ref="acct_412100"/>
|
||
</record>
|
||
|
||
<record id="pc_webapp" model="product.category">
|
||
<field name="name">Custom Web App Development</field>
|
||
<field name="parent_id" ref="pc_services"/>
|
||
<field name="property_account_income_categ_id" ref="acct_412200"/>
|
||
</record>
|
||
|
||
<record id="pc_website" model="product.category">
|
||
<field name="name">Custom Website Development</field>
|
||
<field name="parent_id" ref="pc_services"/>
|
||
<field name="property_account_income_categ_id" ref="acct_412300"/>
|
||
</record>
|
||
|
||
<record id="pc_erp" model="product.category">
|
||
<field name="name">ERP Implementation</field>
|
||
<field name="parent_id" ref="pc_services"/>
|
||
<field name="property_account_income_categ_id" ref="acct_412400"/>
|
||
</record>
|
||
|
||
<record id="pc_consulting" model="product.category">
|
||
<field name="name">Consulting & Advisory</field>
|
||
<field name="parent_id" ref="pc_services"/>
|
||
<field name="property_account_income_categ_id" ref="acct_413100"/>
|
||
</record>
|
||
|
||
<record id="pc_training" model="product.category">
|
||
<field name="name">Training</field>
|
||
<field name="parent_id" ref="pc_services"/>
|
||
<field name="property_account_income_categ_id" ref="acct_413200"/>
|
||
</record>
|
||
|
||
<!-- Parent: Resale -->
|
||
<record id="pc_resale" model="product.category">
|
||
<field name="name">Resale</field>
|
||
</record>
|
||
|
||
<record id="pc_resale_software" model="product.category">
|
||
<field name="name">Software Resale</field>
|
||
<field name="parent_id" ref="pc_resale"/>
|
||
<field name="property_account_income_categ_id" ref="acct_414100"/>
|
||
<field name="property_account_expense_categ_id" ref="acct_513100"/>
|
||
</record>
|
||
|
||
<record id="pc_resale_hardware" model="product.category">
|
||
<field name="name">Hardware Resale</field>
|
||
<field name="parent_id" ref="pc_resale"/>
|
||
<field name="property_account_income_categ_id" ref="acct_414200"/>
|
||
<field name="property_account_expense_categ_id" ref="acct_513200"/>
|
||
</record>
|
||
|
||
</data>
|
||
</odoo>
|
||
```
|
||
|
||
- [ ] **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
|
||
<?xml version="1.0" encoding="utf-8"?>
|
||
<odoo>
|
||
<data noupdate="0">
|
||
|
||
<record id="rp_associated_tag" model="res.partner.category">
|
||
<field name="name">RP-Associated</field>
|
||
<field name="color">3</field>
|
||
<field name="parent_id" eval="False"/>
|
||
</record>
|
||
|
||
</data>
|
||
</odoo>
|
||
```
|
||
|
||
- [ ] **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
|
||
<?xml version="1.0" encoding="utf-8"?>
|
||
<odoo>
|
||
<data noupdate="0">
|
||
|
||
<record id="partner_westin_healthcare" model="res.partner">
|
||
<field name="name">Westin Healthcare Inc</field>
|
||
<field name="is_company" eval="True"/>
|
||
<field name="company_type">company</field>
|
||
<field name="customer_rank">1</field>
|
||
<field name="supplier_rank">1</field>
|
||
<field name="country_id" ref="base.ca"/>
|
||
<field name="state_id" ref="base.state_ca_on"/>
|
||
<field name="category_id" eval="[(6, 0, [ref('rp_associated_tag')])]"/>
|
||
<field name="property_account_position_id" ref="fp_ca_ontario"/>
|
||
<field name="comment">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).</field>
|
||
</record>
|
||
|
||
<record id="partner_divine_mobility" model="res.partner">
|
||
<field name="name">Divine Mobility Inc</field>
|
||
<field name="is_company" eval="True"/>
|
||
<field name="company_type">company</field>
|
||
<field name="customer_rank">1</field>
|
||
<field name="supplier_rank">1</field>
|
||
<field name="country_id" ref="base.ca"/>
|
||
<field name="state_id" ref="base.state_ca_on"/>
|
||
<field name="category_id" eval="[(6, 0, [ref('rp_associated_tag')])]"/>
|
||
<field name="property_account_position_id" ref="fp_ca_ontario"/>
|
||
<field name="comment">Associated corporation under common control with Nexa Systems Inc (Gurpreet, owner). See Westin Healthcare Inc for compliance notes.</field>
|
||
</record>
|
||
|
||
</data>
|
||
</odoo>
|
||
```
|
||
|
||
- [ ] **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
|
||
<?xml version="1.0" encoding="utf-8"?>
|
||
<odoo>
|
||
<data noupdate="0">
|
||
|
||
<!-- AWS / Amazon Web Services -->
|
||
<record id="rule_aws" model="account.reconcile.model">
|
||
<field name="name">AWS / Amazon Web Services → Cloud Infrastructure</field>
|
||
<field name="rule_type">writeoff_suggestion</field>
|
||
<field name="match_label">contains</field>
|
||
<field name="match_label_param">AMAZON WEB SERVICES</field>
|
||
<field name="line_ids" eval="[(0, 0, {
|
||
'account_id': ref('acct_511100'),
|
||
'amount_type': 'percentage',
|
||
'amount_string': '100',
|
||
'label': 'AWS — Cloud Infrastructure',
|
||
})]"/>
|
||
</record>
|
||
|
||
<!-- Hetzner / OVH / DigitalOcean / Linode -->
|
||
<record id="rule_hetzner" model="account.reconcile.model">
|
||
<field name="name">Hetzner → Cloud Infrastructure</field>
|
||
<field name="rule_type">writeoff_suggestion</field>
|
||
<field name="match_label">contains</field>
|
||
<field name="match_label_param">HETZNER</field>
|
||
<field name="line_ids" eval="[(0, 0, {
|
||
'account_id': ref('acct_511100'),
|
||
'amount_type': 'percentage',
|
||
'amount_string': '100',
|
||
'label': 'Hetzner — Cloud Infrastructure',
|
||
})]"/>
|
||
</record>
|
||
|
||
<record id="rule_digitalocean" model="account.reconcile.model">
|
||
<field name="name">DigitalOcean → Cloud Infrastructure</field>
|
||
<field name="rule_type">writeoff_suggestion</field>
|
||
<field name="match_label">contains</field>
|
||
<field name="match_label_param">DIGITALOCEAN</field>
|
||
<field name="line_ids" eval="[(0, 0, {
|
||
'account_id': ref('acct_511100'),
|
||
'amount_type': 'percentage',
|
||
'amount_string': '100',
|
||
'label': 'DigitalOcean — Cloud Infrastructure',
|
||
})]"/>
|
||
</record>
|
||
|
||
<!-- Cloudflare → CDN -->
|
||
<record id="rule_cloudflare" model="account.reconcile.model">
|
||
<field name="name">Cloudflare → CDN & Edge</field>
|
||
<field name="rule_type">writeoff_suggestion</field>
|
||
<field name="match_label">contains</field>
|
||
<field name="match_label_param">CLOUDFLARE</field>
|
||
<field name="line_ids" eval="[(0, 0, {
|
||
'account_id': ref('acct_511110'),
|
||
'amount_type': 'percentage',
|
||
'amount_string': '100',
|
||
'label': 'Cloudflare — CDN & Edge',
|
||
})]"/>
|
||
</record>
|
||
|
||
<!-- GitHub → Dev Tools -->
|
||
<record id="rule_github" model="account.reconcile.model">
|
||
<field name="name">GitHub → Software (Dev Tools)</field>
|
||
<field name="rule_type">writeoff_suggestion</field>
|
||
<field name="match_label">contains</field>
|
||
<field name="match_label_param">GITHUB</field>
|
||
<field name="line_ids" eval="[(0, 0, {
|
||
'account_id': ref('acct_631200'),
|
||
'amount_type': 'percentage',
|
||
'amount_string': '100',
|
||
'label': 'GitHub — Software (Dev Tools)',
|
||
})]"/>
|
||
</record>
|
||
|
||
<!-- Microsoft / M365 → Productivity -->
|
||
<record id="rule_microsoft" model="account.reconcile.model">
|
||
<field name="name">Microsoft / M365 → Software (Productivity)</field>
|
||
<field name="rule_type">writeoff_suggestion</field>
|
||
<field name="match_label">contains</field>
|
||
<field name="match_label_param">MICROSOFT</field>
|
||
<field name="line_ids" eval="[(0, 0, {
|
||
'account_id': ref('acct_631100'),
|
||
'amount_type': 'percentage',
|
||
'amount_string': '100',
|
||
'label': 'Microsoft — Software (Productivity)',
|
||
})]"/>
|
||
</record>
|
||
|
||
<!-- Stripe fee → Merchant Processing -->
|
||
<record id="rule_stripe_fee" model="account.reconcile.model">
|
||
<field name="name">Stripe fee → Merchant Processing</field>
|
||
<field name="rule_type">writeoff_suggestion</field>
|
||
<field name="match_label">contains</field>
|
||
<field name="match_label_param">STRIPE FEE</field>
|
||
<field name="line_ids" eval="[(0, 0, {
|
||
'account_id': ref('acct_691200'),
|
||
'amount_type': 'percentage',
|
||
'amount_string': '100',
|
||
'label': 'Stripe — Merchant Processing',
|
||
})]"/>
|
||
</record>
|
||
|
||
<!-- Google Ads → Advertising Digital -->
|
||
<record id="rule_google_ads" model="account.reconcile.model">
|
||
<field name="name">Google Ads → Advertising (Digital)</field>
|
||
<field name="rule_type">writeoff_suggestion</field>
|
||
<field name="match_label">contains</field>
|
||
<field name="match_label_param">GOOGLE ADS</field>
|
||
<field name="line_ids" eval="[(0, 0, {
|
||
'account_id': ref('acct_641100'),
|
||
'amount_type': 'percentage',
|
||
'amount_string': '100',
|
||
'label': 'Google Ads — Advertising (Digital)',
|
||
})]"/>
|
||
</record>
|
||
|
||
</data>
|
||
</odoo>
|
||
```
|
||
|
||
- [ ] **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 `<record id="acct_XXXXXX" model="account.account">` 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_<date>.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).
|