Compare commits
14 Commits
7a02382623
...
d7bbeb49b7
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d7bbeb49b7 | ||
|
|
2737bc481c | ||
|
|
0e595e6129 | ||
|
|
a0f783ab14 | ||
|
|
82a13b2ce5 | ||
|
|
0230670bdc | ||
|
|
86e89ca419 | ||
|
|
749c0335fa | ||
|
|
092423d7de | ||
|
|
9c52fac9ba | ||
|
|
d2f8934a53 | ||
|
|
113427f7e2 | ||
|
|
3559eb1fd5 | ||
|
|
9f28dce160 |
@@ -3,7 +3,7 @@
|
||||
# License OPL-1 (Odoo Proprietary License v1.0)
|
||||
{
|
||||
'name': 'Fusion Plating — Reports',
|
||||
'version': '19.0.10.3.0',
|
||||
'version': '19.0.10.4.0',
|
||||
'category': 'Manufacturing/Plating',
|
||||
'summary': 'PDF reports for Fusion Plating: quote, SO, WO, packing, BoL, CoC, invoice, receipt, quality + compliance.',
|
||||
'depends': [
|
||||
|
||||
@@ -70,9 +70,12 @@
|
||||
and _so.x_fc_internal_note.striptags()[:100])
|
||||
or '-'"/>
|
||||
<!-- Inline the QR as base64 data URI so wkhtmltopdf doesn't need
|
||||
to fetch /report/barcode/ over the network during rendering. -->
|
||||
to fetch /report/barcode/ over the network during rendering.
|
||||
Generated at 600x600 (was 300x300) so it down-scales rather
|
||||
than up-scales at print time — much sharper on Brother /
|
||||
Zebra thermal printers where small QRs were dropping rows. -->
|
||||
<t t-set="_qr_src" t-value="env['ir.actions.report'].barcode_data_uri(
|
||||
'QR', _scan_url, width=300, height=300)"/>
|
||||
'QR', _scan_url, width=600, height=600)"/>
|
||||
|
||||
<style>
|
||||
@page { margin: 0; size: 152mm 102mm; }
|
||||
@@ -97,11 +100,14 @@
|
||||
page-break-after: always;
|
||||
page-break-inside: avoid;
|
||||
}
|
||||
/* ---- HEADER band — grew to 40% to fit 2x WO# + logo + bigger QR. */
|
||||
/* ---- HEADER band — grew to 44% (was 40%) to fit a bigger QR
|
||||
for thermal-printer scan reliability. The body band is now
|
||||
56% but each row stays roughly the same height because
|
||||
we tightened the Notes row at the bottom. */
|
||||
.fp-sticker-head-wrap {
|
||||
position: absolute;
|
||||
left: 0; right: 0; top: 0;
|
||||
height: 40%;
|
||||
height: 44%;
|
||||
border-bottom: 2px solid #000;
|
||||
box-sizing: border-box;
|
||||
padding: 0;
|
||||
@@ -168,26 +174,32 @@
|
||||
render the image larger than the wrapper and offset it so
|
||||
the wrapper clips that border out. ---------------------- */
|
||||
.fp-sticker-qr-wrap {
|
||||
width: 380px;
|
||||
height: 380px;
|
||||
width: 460px;
|
||||
height: 460px;
|
||||
display: inline-block;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
.fp-sticker-qr {
|
||||
width: 510px;
|
||||
height: 510px;
|
||||
width: 620px;
|
||||
height: 620px;
|
||||
position: absolute;
|
||||
top: -65px;
|
||||
left: -65px;
|
||||
top: -80px;
|
||||
left: -80px;
|
||||
margin: 0;
|
||||
display: block;
|
||||
}
|
||||
/* ---- BODY band (7 rows, each 14.28% of the band) ---- */
|
||||
/* ---- BODY band — first 6 rows are equal-height; the
|
||||
final Notes row takes less. Header moved down to 44%
|
||||
so this band is 56%; the row-height split below brings
|
||||
the 6 main rows to ~9.4mm each (slightly more than the
|
||||
old equal-split 8.7mm despite the smaller band). Notes
|
||||
drops to ~6mm — enough for 1-2 lines of the bigger
|
||||
text below. ---- */
|
||||
.fp-sticker-body-wrap {
|
||||
position: absolute;
|
||||
left: 0; right: 0;
|
||||
top: 40%; bottom: 0;
|
||||
top: 44%; bottom: 0;
|
||||
}
|
||||
table.fp-sticker-body {
|
||||
width: 100%;
|
||||
@@ -195,14 +207,15 @@
|
||||
table-layout: fixed;
|
||||
border-collapse: collapse;
|
||||
}
|
||||
table.fp-sticker-body tr { height: 14.28%; }
|
||||
table.fp-sticker-body tr.fp-sticker-row-main { height: 15%; }
|
||||
table.fp-sticker-body tr.fp-sticker-row-notes { height: 10%; }
|
||||
table.fp-sticker-body tr + tr td { border-top: 1px solid #000; }
|
||||
col.fp-col-label { width: 32%; }
|
||||
col.fp-col-value { width: 68%; }
|
||||
table.fp-sticker-body td {
|
||||
vertical-align: middle;
|
||||
padding: 0 14px;
|
||||
font-size: 38pt;
|
||||
font-size: 44pt;
|
||||
line-height: 1.1;
|
||||
}
|
||||
td.fp-sticker-label {
|
||||
@@ -217,7 +230,10 @@
|
||||
white-space: nowrap;
|
||||
}
|
||||
.fp-sticker-strong { font-weight: 700; }
|
||||
.fp-sticker-muted { color: #555; font-size: 28pt; }
|
||||
.fp-sticker-muted { color: #555; font-size: 32pt; }
|
||||
/* Notes row uses a slightly smaller font so 1-2 lines fit
|
||||
in the trimmed-down height. -------------------------- */
|
||||
tr.fp-sticker-row-notes td { font-size: 32pt; }
|
||||
</style>
|
||||
|
||||
<div class="fp-sticker">
|
||||
@@ -269,7 +285,7 @@
|
||||
<col class="fp-col-label"/>
|
||||
<col class="fp-col-value"/>
|
||||
</colgroup>
|
||||
<tr>
|
||||
<tr class="fp-sticker-row-main">
|
||||
<td class="fp-sticker-label">PO (RO):</td>
|
||||
<td class="fp-sticker-value">
|
||||
<span class="fp-sticker-strong"
|
||||
@@ -281,13 +297,13 @@
|
||||
</t>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<tr class="fp-sticker-row-main">
|
||||
<td class="fp-sticker-label">Customer:</td>
|
||||
<td class="fp-sticker-value">
|
||||
<span t-esc="_partner_name"/>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<tr class="fp-sticker-row-main">
|
||||
<td class="fp-sticker-label">Process:</td>
|
||||
<td class="fp-sticker-value">
|
||||
<t t-if="_process">
|
||||
@@ -299,7 +315,7 @@
|
||||
<t t-else="">-</t>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<tr class="fp-sticker-row-main">
|
||||
<td class="fp-sticker-label">Part Number:</td>
|
||||
<td class="fp-sticker-value">
|
||||
<t t-if="_part">
|
||||
@@ -323,7 +339,7 @@
|
||||
<t t-else="">-</t>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<tr class="fp-sticker-row-main">
|
||||
<td class="fp-sticker-label">Due Date:</td>
|
||||
<td class="fp-sticker-value">
|
||||
<t t-if="_due">
|
||||
@@ -332,7 +348,7 @@
|
||||
<t t-else="">-</t>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<tr class="fp-sticker-row-main">
|
||||
<td class="fp-sticker-label">Qty:</td>
|
||||
<td class="fp-sticker-value">
|
||||
<span class="fp-sticker-strong">
|
||||
@@ -340,7 +356,7 @@
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<tr class="fp-sticker-row-notes">
|
||||
<td class="fp-sticker-label">Notes:</td>
|
||||
<td class="fp-sticker-value">
|
||||
<t t-esc="_internal_note"/>
|
||||
|
||||
@@ -1,26 +1,261 @@
|
||||
# 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.
|
||||
Odoo 19 module that installs and maintains Nexa Systems Inc's chart of
|
||||
accounts, taxes, fiscal positions, analytic plans, partner records, and
|
||||
seeded standard products.
|
||||
|
||||
## Install
|
||||
- **Design reference**: `docs/superpowers/specs/2026-05-12-nexa-coa-design.md`
|
||||
- **Implementation plan**: `docs/superpowers/plans/2026-05-12-nexa-coa-setup.md`
|
||||
- **Target**: odoo-nexa (192.168.1.111), database `nexamain`
|
||||
|
||||
```
|
||||
docker exec odoo-nexa-app odoo -c /etc/odoo/odoo.conf -d nexamain \
|
||||
-i nexa_coa_setup --no-http --stop-after-init
|
||||
---
|
||||
|
||||
## Chart of accounts at a glance
|
||||
|
||||
| Range | Purpose | Examples |
|
||||
|---|---|---|
|
||||
| `1010-1090` | Cash & bank | 1010 Scotia 9309, 1030 BMO, 1040 RBC, 1070 Scotia CC, 1090 Suspense |
|
||||
| `1100-1130` | Accounts receivable | 1100 Customers, 1120 Due From Shareholder, 1130 Due From Associated Corps |
|
||||
| `1210-1230` | Tax assets | 1210 HST/GST ITC Receivable, 1220 Instalments, 1230 QST Refund |
|
||||
| `1510-1750` | Capital assets + accumulated depreciation | 1510 Computers (CCA 50), 1550 Software (CCA 14.1) |
|
||||
| `2010-2130` | Trade AP + sales tax | 2010 Vendors, 2110 HST Collected, 2120 QST Collected, 2130 Net Payable |
|
||||
| `2210-2330` | Payroll + corporate tax payable | 2210 Fed Tax W/H, 2220 CPP, 2230 EI |
|
||||
| `2510-2590` | Shareholder + intercompany | 2510 Due To Shareholder, 2520 Long-term Loan, 2590 Due To Associated |
|
||||
| `3010-3590` | Equity + retained earnings | 3010 Common Shares, 3510 RE Current Year, 3590 Dividends Declared |
|
||||
| `4010-4050` | Recurring revenue | 4010 SaaS, 4020 Hosting, 4030 Support, 4040 Domain Pass-through, 4050 Setup |
|
||||
| `4110-4160` | Project revenue | 4110 Custom Software, 4120 Web App, 4130 Website, 4140 ERP, 4150 Mobile |
|
||||
| `4210-4230` | Services revenue (hourly) | 4210 Consulting, 4220 Training, 4230 Tech Support |
|
||||
| `4310-4320` | Reseller revenue | 4310 Software resale, 4320 Hardware resale |
|
||||
| `4910-4930` | Revenue adjustments | 4910 Discounts, 4920 Returns, 4930 Bad Debt Recovery |
|
||||
| `5010-5910` | Direct costs (COGS) | 5010 Cloud Infrastructure, 5210 Sub Labour CA (SR&ED-eligible) |
|
||||
| `6010-6092` | Personnel — T4 employees | 6010 Dev salaries (SR&ED proxy base), 6040 Owner salary |
|
||||
| `6110-6120` | Contract labour (non-project) | 6110 Canadian, 6120 Foreign |
|
||||
| `6210-6270` | Office & facilities | 6210 Rent, 6220 Home Office, 6240 Internet & Phone |
|
||||
| `6310-6350` | Software subs (internal) | 6310 Productivity, 6320 Dev Tools, 6340 Security |
|
||||
| `6410-6450` | Marketing & sales | 6410 Digital Ads, 6450 Own Website |
|
||||
| `6510-6540` | Professional fees | 6510 Legal, 6520 Accounting, 6530 Tax Prep |
|
||||
| `6610-6650` | Insurance | 6620 Professional Liability / E&O, 6630 Cyber |
|
||||
| `6710-6740` | Travel & entertainment | 6710 Travel, 6720 M&E (50% deductible) |
|
||||
| `6810-6840` | Training & development | |
|
||||
| `6910-6960` | Banking & finance | 6910 Bank fees, 6920 Stripe/merchant, 6960 Late penalties (non-deductible) |
|
||||
| `7010-7050` | Other | 7010 Bad Debt, 7020 Donations, 7030 Fines (non-deductible), 7050 Depreciation |
|
||||
|
||||
---
|
||||
|
||||
## Standard products (seeded)
|
||||
|
||||
All 14 starter products auto-route to the right income account via their
|
||||
category. Use as-is, duplicate to create variants, or override prices per
|
||||
customer/quote.
|
||||
|
||||
| SKU | Description | Default | Income → |
|
||||
|---|---|---|---|
|
||||
| `SAAS-BASIC` | SaaS Subscription — Basic | per-customer | 4010 |
|
||||
| `HOST-S/M/L` | Hosting Small/Medium/Large | $49/$149/$299 mo | 4020 |
|
||||
| `SUPPORT-RET` | Support Retainer (4 hrs/mo) | $640 mo | 4030 |
|
||||
| `SETUP-FEE` | Setup / Onboarding Fee | $500 | 4050 |
|
||||
| `DEV-SOFTWARE` | Custom Software Dev | $160 / hr | 4110 |
|
||||
| `DEV-WEBAPP` | Custom Web App Dev | $160 / hr | 4120 |
|
||||
| `DEV-WEBSITE` | Custom Website Dev | $160 / hr | 4130 |
|
||||
| `ERP-IMPL` | ERP Implementation | $175 / hr | 4140 |
|
||||
| `CONSULT` | Consulting & Advisory | $200 / hr | 4210 |
|
||||
| `TRAINING` | Training & Workshop | $120 / hr | 4220 |
|
||||
| `TECH-SUPPORT` | Technical Support — Hourly | $160 / hr | 4230 |
|
||||
| `RESALE-SW` | Software License (template) | per-quote | 4310 |
|
||||
|
||||
Product file uses `noupdate=1` so your price/description edits persist
|
||||
across module updates.
|
||||
|
||||
---
|
||||
|
||||
## Fiscal positions (automatic tax handling)
|
||||
|
||||
Tax routing is determined by customer billing address — you don't pick
|
||||
the tax manually on invoices.
|
||||
|
||||
| Position | Auto-applies to | Sales tax |
|
||||
|---|---|---|
|
||||
| CA — Ontario (Default) | ON customers | 13% HST |
|
||||
| CA — Atlantic | NB, NS, PE, NL | 15% HST |
|
||||
| CA — Quebec | QC | 5% GST + 9.975% QST |
|
||||
| CA — BC | BC | 5% GST (PST handled per-product if applicable) |
|
||||
| CA — Prairies / Territories | AB, MB, SK, YT, NT, NU | 5% GST |
|
||||
| Export — US | All US customers | 0% (Zero-rated) |
|
||||
| Export — International | Manual | 0% (Zero-rated) |
|
||||
| Tax Exempt | Manual (cert-holders) | 0% |
|
||||
|
||||
---
|
||||
|
||||
## Analytic plans
|
||||
|
||||
Three orthogonal tagging plans on every transaction:
|
||||
|
||||
| Plan | Tags | Used for |
|
||||
|---|---|---|
|
||||
| **Customer Project** | One per engagement (e.g. `PRJ-2026-WESTIN-ERP`) | Project P&L, billable-hour realization, WIP |
|
||||
| **Department** | DEPT-DEV, DEPT-SALES, DEPT-ADMIN, DEPT-HOSTING | Cost allocation, departmental P&L |
|
||||
| **SR&ED Tag** | SRED-T4-DEV-SALARY, SRED-SPECIFIED-EMPLOYEE, SRED-CONTRACTOR-CA-ARM-LENGTH, SRED-CONTRACTOR-CA-NON-ARM-LENGTH, SRED-MATERIALS-CONSUMED, SRED-OVERHEAD-PROXY-BASIS, NOT-ELIGIBLE | T661 SR&ED filing |
|
||||
|
||||
---
|
||||
|
||||
## Install / update
|
||||
|
||||
**Always pg_dump first.**
|
||||
|
||||
```bash
|
||||
ssh odoo-nexa "STAMP=\$(date +%Y%m%d_%H%M%S) && \
|
||||
docker exec odoo-nexa-db pg_dump -U odoo -d nexamain -F c -Z 9 \
|
||||
> /tmp/nexamain_\${STAMP}.dump"
|
||||
scp odoo-nexa:/tmp/nexamain_*.dump ~/Backups/odoo-nexa/
|
||||
```
|
||||
|
||||
## Update
|
||||
**Deploy module changes** (Mac → server; rsync isn't available, use tar):
|
||||
|
||||
```
|
||||
docker exec odoo-nexa-app odoo -c /etc/odoo/odoo.conf -d nexamain \
|
||||
-u nexa_coa_setup --no-http --stop-after-init
|
||||
```bash
|
||||
cd /Users/gurpreet/Github/Odoo-Modules && \
|
||||
tar czf - nexa_coa_setup | \
|
||||
ssh odoo-nexa "cd /opt/odoo/custom-addons && \
|
||||
sudo rm -rf nexa_coa_setup && sudo tar xzf - && \
|
||||
sudo chown -R 1000:1000 nexa_coa_setup"
|
||||
```
|
||||
|
||||
## Design reference
|
||||
**Install (first time on a database)**:
|
||||
|
||||
See `docs/superpowers/specs/2026-05-12-nexa-coa-design.md`.
|
||||
```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"
|
||||
```
|
||||
|
||||
## Safety
|
||||
**Update (after editing XML or hooks)**:
|
||||
|
||||
Always take a pg_dump BEFORE running `-i` or `-u`. See `docs/superpowers/plans/2026-05-12-nexa-coa-setup.md` Phase 0.
|
||||
```bash
|
||||
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"
|
||||
```
|
||||
|
||||
**Restore from backup** (only if catastrophic):
|
||||
|
||||
```bash
|
||||
DUMP=~/Backups/odoo-nexa/nexamain_<timestamp>.dump
|
||||
scp "$DUMP" odoo-nexa:/tmp/
|
||||
ssh odoo-nexa "docker exec odoo-nexa-db psql -U odoo -d postgres -c \
|
||||
'DROP DATABASE nexamain;' && \
|
||||
docker exec odoo-nexa-db psql -U odoo -d postgres -c \
|
||||
'CREATE DATABASE nexamain OWNER odoo;' && \
|
||||
cat $DUMP | docker exec -i odoo-nexa-db pg_restore -U odoo -d nexamain && \
|
||||
docker restart odoo-nexa-app"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Yearly close calendar
|
||||
|
||||
Nexa is an annual HST filer and an annual T2 filer (fiscal year-end Dec 31).
|
||||
|
||||
| When | What | Notes |
|
||||
|---|---|---|
|
||||
| **Jan** | Confirm asset purchases for the year, assign correct CCA class accounts (1510-1560) | Class 50 hardware @ 55% DB (AccII = 82.5% Y1 through 2027). Class 14.1 software @ 100% Y1. |
|
||||
| **Jan-Feb** | Pull SR&ED analytic report. Filter on SR&ED Tag analytic plan. Export. | Hand off to accountant for T661 prep. |
|
||||
| **Mar 31** | **HST/GST annual return due**. Line 105 = sum of 2110, Line 108 = sum of 1210 ITC. | Pay any net tax + first quarterly instalment if prior year > $3k. |
|
||||
| **Mar 31** | **T2 balance due** (CCPC). | T2 return itself is due June 30. |
|
||||
| **Mar-Apr** | Set fiscalyear_lock_date to prior Dec 31 via Accounting > Configuration > Settings > Lock Dates. | Prevents accidental back-dating into closed period. |
|
||||
| **Jun 30** | **T2 corporate income tax return due**. Schedule 23 allocates SBD + SR&ED expenditure limits across Nexa / Westin / Divine. | Associated-group calculation. |
|
||||
| **Quarterly** | Pay HST instalments (if prior net tax ≥ $3k). | Track via 1220 Instalments Paid. |
|
||||
| **Sep-Dec** | Yearly review of intercompany pricing (Nexa ↔ Westin / Divine). | Transfer-pricing compliance — must be fair market value. |
|
||||
|
||||
---
|
||||
|
||||
## Common tasks
|
||||
|
||||
### Add a new GL account
|
||||
|
||||
1. Append a `<record id="acct_NNNN" model="account.account">` to `data/01_account_account.xml` using the next free 4-digit code in the relevant range.
|
||||
2. Deploy + `-u` (see Install/Update above).
|
||||
3. Verify: `psql -d nexamain -c "SELECT code, name FROM ... WHERE code = 'NNNN';"`
|
||||
|
||||
### Add a new product
|
||||
|
||||
For ad-hoc / per-customer items, just create via UI:
|
||||
- Sales > Products > New → set name, price, **Product Category** (this drives the income account).
|
||||
- The fiscal position auto-applies tax when invoicing.
|
||||
|
||||
For new standard catalog items, append to `data/11_products.xml` (uses `noupdate=1` so a re-install won't overwrite UI edits).
|
||||
|
||||
### Add a new analytic account (project, department, SR&ED tag)
|
||||
|
||||
Projects are dynamic — create via UI:
|
||||
- Accounting > Configuration > Analytic Accounting > Analytic Accounts > New
|
||||
- Code: `PRJ-{YYYY}-{CUST}-{SHORTNAME}` (e.g. `PRJ-2026-WESTIN-ERP-PHASE2`)
|
||||
- Plan: **Customer Project**
|
||||
|
||||
For departments or new SR&ED tags, edit `data/06_account_analytic_account.xml` + deploy + `-u`.
|
||||
|
||||
### Set the fiscal year lock
|
||||
|
||||
Accounting > Configuration > Settings > Lock Dates > set `Lock Date` to Dec 31 of the prior fiscal year. Requires:
|
||||
- All bank statement lines for the period reconciled / cleared / deleted
|
||||
- Final accountant adjustments posted
|
||||
|
||||
The `post_init_hook` attempts this automatically at install but tolerates failure (logs a warning instead) — set it manually once the books are clean.
|
||||
|
||||
### Reclassify a wrongly-categorized historical invoice line
|
||||
|
||||
Two ways:
|
||||
- **In place (no PDF change)**: SQL `UPDATE account_move_line SET account_id = <new_id> WHERE id = <line_id>;`. Used in `scripts/fix_invoice_1127.py` and `scripts/reclass_historical_411000.py` as references.
|
||||
- **Proper way (audit-trail clean)**: Reset invoice to draft, edit, re-post. Requires admin permissions + un-reconciling any payment first.
|
||||
|
||||
### Pull SR&ED data at year-end
|
||||
|
||||
```sql
|
||||
-- Eligible salaries summary
|
||||
SELECT aa.code, aa.name, ROUND(SUM(aml.debit - aml.credit)::numeric, 2) AS amount
|
||||
FROM account_move_line aml
|
||||
JOIN account_analytic_line aal ON aal.move_line_id = aml.id
|
||||
JOIN account_analytic_account aa ON aa.id = aal.account_id
|
||||
JOIN account_analytic_plan ap ON ap.id = aa.plan_id
|
||||
WHERE ap.name = 'SR&ED Tag'
|
||||
AND aml.date BETWEEN '2026-01-01' AND '2026-12-31'
|
||||
AND aa.code <> 'NOT-ELIGIBLE'
|
||||
GROUP BY aa.code, aa.name
|
||||
ORDER BY aa.code;
|
||||
```
|
||||
|
||||
Plus pull eligible contractor invoices: customer = Nexa, vendor in Canada (arm's length), tagged `SRED-CONTRACTOR-CA-ARM-LENGTH`.
|
||||
|
||||
---
|
||||
|
||||
## Compliance flags (do not forget)
|
||||
|
||||
- **Associated corporations**: Westin Healthcare Inc + Divine Mobility Inc share Nexa's $500k SBD limit and $3M SR&ED expenditure limit. **Annual Schedule 23 filing** allocates these across the group.
|
||||
- **Subsection 15(2)**: Shareholder loans outstanding > 1 year past fiscal year-end become **taxable to Gurpreet personally**. Track 2510 (short-term) vs 2520 (long-term commercial loan).
|
||||
- **Transfer pricing (s.247)**: Nexa invoicing Westin / Divine must be at fair market value. Document the methodology; penalty is 10% of any adjustment.
|
||||
- **GST/HST cadence**: currently annual. Once Nexa-only revenue clears **$1.5M**, CRA auto-moves you to quarterly filing.
|
||||
- **QC QST registration**: required if Nexa has any QC customers and revenue > $30k. Separate from federal CRA registration.
|
||||
- **Quick Method GST/HST**: **likely unavailable** — the $400k threshold is checked against the associated-group total (Nexa + Westin + Divine combined).
|
||||
- **Specified employee SR&ED cap**: Gurpreet's SR&ED-eligible salary is capped at 75% of basic salary (no bonuses), and only counts if paid via T4 payroll.
|
||||
|
||||
---
|
||||
|
||||
## Implementation scripts (one-shot, idempotent)
|
||||
|
||||
All under `scripts/` — preserved for audit reference; not run by the module
|
||||
itself. Already executed against prod.
|
||||
|
||||
| Script | What it did |
|
||||
|---|---|
|
||||
| `fix_gl_codes.py` | Renumbered 119100/119900 → 115200/115900 and 511105 → 511100 |
|
||||
| `convert_to_4digit.py` | Renumbered 128 Nexa accounts from 6-digit l10n_ca style to clean 4-digit |
|
||||
| `convert_l10nca_to_4digit.py` | Renumbered 12 retained l10n_ca legacy accounts (Bank/AR/AP/HST) to 4-digit |
|
||||
| `fix_invoice_1127.py` | Reclassified Entech invoice 1127's 17 lines + reassigned 14 product templates to proper categories |
|
||||
| `reclass_historical_411000.py` | Batch-reclassified 200 historical invoice lines from legacy 411000 by keyword rules |
|
||||
| `test_invoices.py` | End-to-end invoice tests (ON HST, US zero-rated, QC GST+QST, intercompany) |
|
||||
|
||||
---
|
||||
|
||||
## Open items (manual follow-ups)
|
||||
|
||||
- [ ] Set fiscal year lock at 2025-12-31 once unreconciled bank statement lines are cleared
|
||||
- [ ] Verify HST# format display on customer invoice PDFs (stored as `741224877RT0001`; some Canadian accountants prefer `741224877 RT0001` with space — change via UI if needed)
|
||||
- [ ] When first T4 employee is hired: configure payroll (Wagepoint / ADP / Odoo Payroll), set up source deduction journal automation, decide on payroll cadence
|
||||
- [ ] When first USD invoice arrives: add USD bank account (1020 reserved), enable currency_rate_live cron, configure multi-currency on company
|
||||
- [ ] When asset count grows: install custom CCA module OR keep maintaining CCA schedule via accountant's spreadsheet
|
||||
|
||||
@@ -30,6 +30,7 @@
|
||||
"data/08_res_partner_category.xml",
|
||||
"data/09_res_partner.xml",
|
||||
"data/10_account_reconcile_model.xml",
|
||||
"data/11_products.xml",
|
||||
],
|
||||
"pre_init_hook": "pre_init_hook",
|
||||
"post_init_hook": "post_init_hook",
|
||||
|
||||
@@ -8,101 +8,105 @@
|
||||
|
||||
<!-- NOTE: 115100 is l10n_ca "Customers Account" (240 postings — AR control) — kept as l10n_ca.
|
||||
115110 is l10n_ca "Customers Account (PoS)" — kept.
|
||||
Nexa intercompany receivables live in the 119xxx range to avoid all collisions. -->
|
||||
Nexa intercompany receivables sit at 115200 / 115900 (freed when their
|
||||
unused l10n_ca defaults — Mortgage Loans — were deleted during install).
|
||||
XMLIDs (acct_119100, acct_119900) preserved from initial install where
|
||||
these accounts were at codes 119100/119900 — codes updated in-place via
|
||||
scripts/fix_gl_codes.py without rewriting ir.model.data. -->
|
||||
<record id="acct_119100" model="account.account">
|
||||
<field name="code">119100</field>
|
||||
<field name="code">1120</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_119900" model="account.account">
|
||||
<field name="code">119900</field>
|
||||
<field name="code">1130</field>
|
||||
<field name="name">Due From Associated Corporations</field>
|
||||
<field name="account_type">asset_current</field>
|
||||
<field name="reconcile" eval="True"/>
|
||||
</record>
|
||||
|
||||
<record id="acct_118100" model="account.account">
|
||||
<field name="code">118100</field>
|
||||
<field name="code">1210</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="code">1220</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="code">1230</field>
|
||||
<field name="name">QST Input Tax Refund Receivable</field>
|
||||
<field name="account_type">asset_current</field>
|
||||
</record>
|
||||
|
||||
<record id="acct_151100" model="account.account">
|
||||
<field name="code">151100</field>
|
||||
<field name="code">1510</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="code">1520</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="code">1530</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="code">1540</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="code">1550</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="code">1560</field>
|
||||
<field name="name">Tools & Small Equipment <$500 (CCA Class 12)</field>
|
||||
<field name="account_type">asset_fixed</field>
|
||||
</record>
|
||||
|
||||
<record id="acct_154100" model="account.account">
|
||||
<field name="code">154100</field>
|
||||
<field name="code">1710</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="code">1720</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="code">1730</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="code">1740</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="code">1750</field>
|
||||
<field name="name">Acc. Depreciation — Acquired Software</field>
|
||||
<field name="account_type">asset_fixed</field>
|
||||
</record>
|
||||
@@ -112,75 +116,75 @@
|
||||
<!-- ============================================================ -->
|
||||
|
||||
<record id="acct_213100" model="account.account">
|
||||
<field name="code">213100</field>
|
||||
<field name="code">2110</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="code">2120</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="code">2130</field>
|
||||
<field name="name">Net HST/GST Payable</field>
|
||||
<field name="account_type">liability_current</field>
|
||||
</record>
|
||||
|
||||
<record id="acct_215100" model="account.account">
|
||||
<field name="code">215100</field>
|
||||
<field name="code">2210</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="code">2220</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="code">2230</field>
|
||||
<field name="name">Source Deductions Payable — EI</field>
|
||||
<field name="account_type">liability_current</field>
|
||||
</record>
|
||||
|
||||
<record id="acct_216100" model="account.account">
|
||||
<field name="code">216100</field>
|
||||
<field name="code">2310</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="code">2320</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="code">2330</field>
|
||||
<field name="name">Corporate Tax Instalments Paid</field>
|
||||
<field name="account_type">asset_current</field>
|
||||
</record>
|
||||
|
||||
<record id="acct_221100" model="account.account">
|
||||
<field name="code">221100</field>
|
||||
<field name="code">2510</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="code">2520</field>
|
||||
<field name="name">Shareholder Loan — Gurpreet (long-term)</field>
|
||||
<field name="account_type">liability_non_current</field>
|
||||
<field name="reconcile" eval="True"/>
|
||||
</record>
|
||||
|
||||
<record id="acct_222900" model="account.account">
|
||||
<field name="code">222900</field>
|
||||
<field name="code">2590</field>
|
||||
<field name="name">Due To Associated Corporations</field>
|
||||
<field name="account_type">liability_current</field>
|
||||
<field name="reconcile" eval="True"/>
|
||||
@@ -191,37 +195,37 @@
|
||||
<!-- ============================================================ -->
|
||||
|
||||
<record id="acct_311100" model="account.account">
|
||||
<field name="code">311100</field>
|
||||
<field name="code">3010</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="code">3020</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="code">3030</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="code">3510</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="code">3520</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="code">3590</field>
|
||||
<field name="name">Dividends Declared</field>
|
||||
<field name="account_type">equity</field>
|
||||
</record>
|
||||
@@ -231,115 +235,115 @@
|
||||
<!-- ============================================================ -->
|
||||
|
||||
<record id="acct_411100" model="account.account">
|
||||
<field name="code">411100</field>
|
||||
<field name="code">4010</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="code">4020</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="code">4030</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="code">4040</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="code">4050</field>
|
||||
<field name="name">Setup / Onboarding Fees Revenue</field>
|
||||
<field name="account_type">income</field>
|
||||
</record>
|
||||
|
||||
<record id="acct_412100" model="account.account">
|
||||
<field name="code">412100</field>
|
||||
<field name="code">4110</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="code">4120</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="code">4130</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="code">4140</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="code">4150</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="code">4160</field>
|
||||
<field name="name">Business App / Integration Revenue</field>
|
||||
<field name="account_type">income</field>
|
||||
</record>
|
||||
|
||||
<record id="acct_413100" model="account.account">
|
||||
<field name="code">413100</field>
|
||||
<field name="code">4210</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="code">4220</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="code">4230</field>
|
||||
<field name="name">Technical Support — Per-incident / Hourly Revenue</field>
|
||||
<field name="account_type">income</field>
|
||||
</record>
|
||||
|
||||
<record id="acct_414100" model="account.account">
|
||||
<field name="code">414100</field>
|
||||
<field name="code">4310</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="code">4320</field>
|
||||
<field name="name">Hardware Resale Revenue</field>
|
||||
<field name="account_type">income</field>
|
||||
</record>
|
||||
|
||||
<record id="acct_419100" model="account.account">
|
||||
<field name="code">419100</field>
|
||||
<field name="code">4910</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="code">4920</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="code">4930</field>
|
||||
<field name="name">Bad Debt Recovery</field>
|
||||
<field name="account_type">income_other</field>
|
||||
</record>
|
||||
@@ -348,106 +352,108 @@
|
||||
<!-- 5xxxxx — DIRECT COSTS (COGS) -->
|
||||
<!-- ============================================================ -->
|
||||
|
||||
<!-- NOTE: 511100 was "Inside Purchases" in l10n_ca (1 posting from legacy bookkeeping) — kept as l10n_ca.
|
||||
Cloud Infrastructure sits at 511105 to avoid collision. -->
|
||||
<!-- NOTE: legacy l10n_ca "Inside Purchases" at 511100 (had 1 historical
|
||||
posting) was renamed to 511100.OLD via scripts/fix_gl_codes.py —
|
||||
Cloud Infrastructure now claims the clean 511100 code. XMLID
|
||||
acct_511105 preserved from initial install. -->
|
||||
<record id="acct_511105" model="account.account">
|
||||
<field name="code">511105</field>
|
||||
<field name="code">5010</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="code">5020</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="code">5030</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="code">5040</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="code">5050</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="code">5060</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="code">5070</field>
|
||||
<field name="name">DNS & Email Hosting (wholesale for resale)</field>
|
||||
<field name="account_type">expense_direct_cost</field>
|
||||
</record>
|
||||
|
||||
<record id="acct_511200" model="account.account">
|
||||
<field name="code">511200</field>
|
||||
<field name="code">5110</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="code">5120</field>
|
||||
<field name="name">Per-customer Licensing & Royalties</field>
|
||||
<field name="account_type">expense_direct_cost</field>
|
||||
</record>
|
||||
|
||||
<record id="acct_512100" model="account.account">
|
||||
<field name="code">512100</field>
|
||||
<field name="code">5210</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="code">5220</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="code">5230</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="code">5240</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="code">5250</field>
|
||||
<field name="name">Project Hardware (passed through)</field>
|
||||
<field name="account_type">expense_direct_cost</field>
|
||||
</record>
|
||||
|
||||
<record id="acct_513100" model="account.account">
|
||||
<field name="code">513100</field>
|
||||
<field name="code">5310</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="code">5320</field>
|
||||
<field name="name">Cost of Hardware Resold</field>
|
||||
<field name="account_type">expense_direct_cost</field>
|
||||
</record>
|
||||
|
||||
<record id="acct_519100" model="account.account">
|
||||
<field name="code">519100</field>
|
||||
<field name="code">5910</field>
|
||||
<field name="name">COGS Adjustments / Write-offs</field>
|
||||
<field name="account_type">expense_direct_cost</field>
|
||||
</record>
|
||||
@@ -457,349 +463,349 @@
|
||||
<!-- ============================================================ -->
|
||||
|
||||
<record id="acct_611100" model="account.account">
|
||||
<field name="code">611100</field>
|
||||
<field name="code">6010</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="code">6020</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="code">6030</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="code">6040</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="code">6050</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="code">6060</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="code">6070</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="code">6080</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="code">6090</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="code">6091</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="code">6092</field>
|
||||
<field name="name">Vacation Pay Accrual</field>
|
||||
<field name="account_type">expense</field>
|
||||
</record>
|
||||
|
||||
<record id="acct_612100" model="account.account">
|
||||
<field name="code">612100</field>
|
||||
<field name="code">6110</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="code">6120</field>
|
||||
<field name="name">Contract Labour — Foreign</field>
|
||||
<field name="account_type">expense</field>
|
||||
</record>
|
||||
|
||||
<record id="acct_621100" model="account.account">
|
||||
<field name="code">621100</field>
|
||||
<field name="code">6210</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="code">6220</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="code">6230</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="code">6240</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="code">6250</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="code">6260</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="code">6270</field>
|
||||
<field name="name">Office Snacks & Refreshments</field>
|
||||
<field name="account_type">expense</field>
|
||||
</record>
|
||||
|
||||
<record id="acct_631100" model="account.account">
|
||||
<field name="code">631100</field>
|
||||
<field name="code">6310</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="code">6320</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="code">6330</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="code">6340</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="code">6350</field>
|
||||
<field name="name">Software — Sales & Marketing</field>
|
||||
<field name="account_type">expense</field>
|
||||
</record>
|
||||
|
||||
<record id="acct_641100" model="account.account">
|
||||
<field name="code">641100</field>
|
||||
<field name="code">6410</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="code">6420</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="code">6430</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="code">6440</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="code">6450</field>
|
||||
<field name="name">Website — Own (nexasystems.ca)</field>
|
||||
<field name="account_type">expense</field>
|
||||
</record>
|
||||
|
||||
<record id="acct_651100" model="account.account">
|
||||
<field name="code">651100</field>
|
||||
<field name="code">6510</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="code">6520</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="code">6530</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="code">6540</field>
|
||||
<field name="name">Business Consulting</field>
|
||||
<field name="account_type">expense</field>
|
||||
</record>
|
||||
|
||||
<record id="acct_661100" model="account.account">
|
||||
<field name="code">661100</field>
|
||||
<field name="code">6610</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="code">6620</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="code">6630</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="code">6640</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="code">6650</field>
|
||||
<field name="name">Insurance — Directors & Officers</field>
|
||||
<field name="account_type">expense</field>
|
||||
</record>
|
||||
|
||||
<record id="acct_671100" model="account.account">
|
||||
<field name="code">671100</field>
|
||||
<field name="code">6710</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="code">6720</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="code">6730</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="code">6740</field>
|
||||
<field name="name">Mileage Reimbursement — Personal Vehicle</field>
|
||||
<field name="account_type">expense</field>
|
||||
</record>
|
||||
|
||||
<record id="acct_681100" model="account.account">
|
||||
<field name="code">681100</field>
|
||||
<field name="code">6810</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="code">6820</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="code">6830</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="code">6840</field>
|
||||
<field name="name">Professional Memberships & Dues</field>
|
||||
<field name="account_type">expense</field>
|
||||
</record>
|
||||
|
||||
<record id="acct_691100" model="account.account">
|
||||
<field name="code">691100</field>
|
||||
<field name="code">6910</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="code">6920</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="code">6930</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="code">6940</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="code">6950</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="code">6960</field>
|
||||
<field name="name">Late Payment Penalties — Non-deductible</field>
|
||||
<field name="account_type">expense</field>
|
||||
</record>
|
||||
|
||||
<record id="acct_699100" model="account.account">
|
||||
<field name="code">699100</field>
|
||||
<field name="code">7010</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="code">7020</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="code">7030</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="code">7040</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="code">7050</field>
|
||||
<field name="name">Depreciation / CCA Expense</field>
|
||||
<field name="account_type">expense</field>
|
||||
</record>
|
||||
|
||||
@@ -1,5 +1,64 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<data noupdate="0">
|
||||
|
||||
<!-- Tax substitutions on each fiscal position are configured by the
|
||||
post_init_hook (_configure_fiscal_position_tax_maps) because the
|
||||
source/destination tax IDs are resolved at runtime from the
|
||||
curated active tax set. -->
|
||||
|
||||
<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 apply for non-CA / non-US customers. Auto-apply by country-group requires a 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 a valid exemption certificate on file. Record certificate details in the partner notes.</field>
|
||||
</record>
|
||||
|
||||
</data>
|
||||
</odoo>
|
||||
|
||||
@@ -1,5 +1,94 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<data noupdate="0">
|
||||
|
||||
<record id="pc_services" model="product.category">
|
||||
<field name="name">Services</field>
|
||||
</record>
|
||||
|
||||
<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_tech_support" model="product.category">
|
||||
<field name="name">Technical Support — Hourly</field>
|
||||
<field name="parent_id" ref="pc_services"/>
|
||||
<field name="property_account_income_categ_id" ref="acct_413300"/>
|
||||
</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>
|
||||
|
||||
<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>
|
||||
|
||||
@@ -1,5 +1,12 @@
|
||||
<?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>
|
||||
|
||||
@@ -1,5 +1,32 @@
|
||||
<?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>
|
||||
|
||||
@@ -1,5 +1,110 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<data noupdate="0">
|
||||
|
||||
<record id="rule_aws" model="account.reconcile.model">
|
||||
<field name="name">AWS / Amazon Web Services → Cloud Infrastructure</field>
|
||||
<field name="trigger">manual</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_511105'),
|
||||
'amount_type': 'percentage',
|
||||
'amount_string': '100',
|
||||
'label': 'AWS — Cloud Infrastructure',
|
||||
})]"/>
|
||||
</record>
|
||||
|
||||
<record id="rule_hetzner" model="account.reconcile.model">
|
||||
<field name="name">Hetzner → Cloud Infrastructure</field>
|
||||
<field name="trigger">manual</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_511105'),
|
||||
'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="trigger">manual</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_511105'),
|
||||
'amount_type': 'percentage',
|
||||
'amount_string': '100',
|
||||
'label': 'DigitalOcean — Cloud Infrastructure',
|
||||
})]"/>
|
||||
</record>
|
||||
|
||||
<record id="rule_cloudflare" model="account.reconcile.model">
|
||||
<field name="name">Cloudflare → CDN & Edge</field>
|
||||
<field name="trigger">manual</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>
|
||||
|
||||
<record id="rule_github" model="account.reconcile.model">
|
||||
<field name="name">GitHub → Software (Dev Tools)</field>
|
||||
<field name="trigger">manual</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>
|
||||
|
||||
<record id="rule_microsoft" model="account.reconcile.model">
|
||||
<field name="name">Microsoft / M365 → Software (Productivity)</field>
|
||||
<field name="trigger">manual</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>
|
||||
|
||||
<record id="rule_stripe_fee" model="account.reconcile.model">
|
||||
<field name="name">Stripe fee → Merchant Processing</field>
|
||||
<field name="trigger">manual</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>
|
||||
|
||||
<record id="rule_google_ads" model="account.reconcile.model">
|
||||
<field name="name">Google Ads → Advertising (Digital)</field>
|
||||
<field name="trigger">manual</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>
|
||||
|
||||
158
nexa_coa_setup/data/11_products.xml
Normal file
158
nexa_coa_setup/data/11_products.xml
Normal file
@@ -0,0 +1,158 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<data noupdate="1">
|
||||
|
||||
<!-- noupdate=1: products are seeded on first install but NEVER overwritten
|
||||
on later -u, so user price/description edits stick. -->
|
||||
|
||||
<!-- ============================================================ -->
|
||||
<!-- Recurring Revenue (4010-4050) -->
|
||||
<!-- ============================================================ -->
|
||||
|
||||
<record id="prod_saas_basic" model="product.template">
|
||||
<field name="name">SaaS Subscription — Basic</field>
|
||||
<field name="default_code">SAAS-BASIC</field>
|
||||
<field name="type">service</field>
|
||||
<field name="list_price">0.00</field>
|
||||
<field name="categ_id" ref="pc_saas"/>
|
||||
<field name="description_sale">Basic SaaS subscription tier. Set monthly price per customer.</field>
|
||||
</record>
|
||||
|
||||
<record id="prod_hosting_small" model="product.template">
|
||||
<field name="name">Hosting — Small (1 vCPU / 2GB RAM / 50GB SSD)</field>
|
||||
<field name="default_code">HOST-S</field>
|
||||
<field name="type">service</field>
|
||||
<field name="list_price">49.00</field>
|
||||
<field name="categ_id" ref="pc_hosting"/>
|
||||
<field name="description_sale">Small managed hosting tier. Suitable for small websites, dev/staging environments.</field>
|
||||
</record>
|
||||
|
||||
<record id="prod_hosting_medium" model="product.template">
|
||||
<field name="name">Hosting — Medium (2 vCPU / 8GB RAM / 200GB SSD)</field>
|
||||
<field name="default_code">HOST-M</field>
|
||||
<field name="type">service</field>
|
||||
<field name="list_price">149.00</field>
|
||||
<field name="categ_id" ref="pc_hosting"/>
|
||||
<field name="description_sale">Medium managed hosting tier. Production websites, small apps.</field>
|
||||
</record>
|
||||
|
||||
<record id="prod_hosting_large" model="product.template">
|
||||
<field name="name">Hosting — Large (4 vCPU / 16GB RAM / 500GB SSD)</field>
|
||||
<field name="default_code">HOST-L</field>
|
||||
<field name="type">service</field>
|
||||
<field name="list_price">299.00</field>
|
||||
<field name="categ_id" ref="pc_hosting"/>
|
||||
<field name="description_sale">Large managed hosting tier. Production SaaS, heavier workloads.</field>
|
||||
</record>
|
||||
|
||||
<record id="prod_support_retainer" model="product.template">
|
||||
<field name="name">Support Contract — Monthly Retainer (4 hrs)</field>
|
||||
<field name="default_code">SUPPORT-RET</field>
|
||||
<field name="type">service</field>
|
||||
<field name="list_price">640.00</field>
|
||||
<field name="categ_id" ref="pc_support"/>
|
||||
<field name="description_sale">4 hours/month support retainer. Unused hours expire monthly. Overage billed at standard rate.</field>
|
||||
</record>
|
||||
|
||||
<record id="prod_setup_fee" model="product.template">
|
||||
<field name="name">Setup / Onboarding Fee</field>
|
||||
<field name="default_code">SETUP-FEE</field>
|
||||
<field name="type">service</field>
|
||||
<field name="list_price">500.00</field>
|
||||
<field name="categ_id" ref="pc_setup_fee"/>
|
||||
<field name="description_sale">One-time setup & onboarding fee for new SaaS / hosting / support customers.</field>
|
||||
</record>
|
||||
|
||||
<!-- ============================================================ -->
|
||||
<!-- Project Revenue (4110-4160) — hourly -->
|
||||
<!-- ============================================================ -->
|
||||
|
||||
<record id="prod_dev_software" model="product.template">
|
||||
<field name="name">Custom Software Development — Hourly</field>
|
||||
<field name="default_code">DEV-SOFTWARE</field>
|
||||
<field name="type">service</field>
|
||||
<field name="list_price">160.00</field>
|
||||
<field name="uom_id" ref="uom.product_uom_hour"/>
|
||||
<field name="categ_id" ref="pc_custom_software"/>
|
||||
<field name="description_sale">Custom software development billed hourly. Quote on scope; final billing on actual hours.</field>
|
||||
</record>
|
||||
|
||||
<record id="prod_dev_webapp" model="product.template">
|
||||
<field name="name">Custom Web Application Development — Hourly</field>
|
||||
<field name="default_code">DEV-WEBAPP</field>
|
||||
<field name="type">service</field>
|
||||
<field name="list_price">160.00</field>
|
||||
<field name="uom_id" ref="uom.product_uom_hour"/>
|
||||
<field name="categ_id" ref="pc_webapp"/>
|
||||
<field name="description_sale">Custom web application development (frontend + backend) billed hourly.</field>
|
||||
</record>
|
||||
|
||||
<record id="prod_dev_website" model="product.template">
|
||||
<field name="name">Custom Website Development — Hourly</field>
|
||||
<field name="default_code">DEV-WEBSITE</field>
|
||||
<field name="type">service</field>
|
||||
<field name="list_price">160.00</field>
|
||||
<field name="uom_id" ref="uom.product_uom_hour"/>
|
||||
<field name="categ_id" ref="pc_website"/>
|
||||
<field name="description_sale">Custom website development (corporate sites, marketing sites) billed hourly.</field>
|
||||
</record>
|
||||
|
||||
<record id="prod_erp_impl" model="product.template">
|
||||
<field name="name">ERP Implementation & Customization — Hourly</field>
|
||||
<field name="default_code">ERP-IMPL</field>
|
||||
<field name="type">service</field>
|
||||
<field name="list_price">175.00</field>
|
||||
<field name="uom_id" ref="uom.product_uom_hour"/>
|
||||
<field name="categ_id" ref="pc_erp"/>
|
||||
<field name="description_sale">Odoo / ERP implementation, customization, module development billed hourly.</field>
|
||||
</record>
|
||||
|
||||
<!-- ============================================================ -->
|
||||
<!-- Services Revenue (4210-4230) — hourly -->
|
||||
<!-- ============================================================ -->
|
||||
|
||||
<record id="prod_consulting" model="product.template">
|
||||
<field name="name">Consulting & Advisory — Hourly</field>
|
||||
<field name="default_code">CONSULT</field>
|
||||
<field name="type">service</field>
|
||||
<field name="list_price">200.00</field>
|
||||
<field name="uom_id" ref="uom.product_uom_hour"/>
|
||||
<field name="categ_id" ref="pc_consulting"/>
|
||||
<field name="description_sale">Architecture, strategy, technology advisory billed hourly.</field>
|
||||
</record>
|
||||
|
||||
<record id="prod_training" model="product.template">
|
||||
<field name="name">Training & Workshop — Hourly</field>
|
||||
<field name="default_code">TRAINING</field>
|
||||
<field name="type">service</field>
|
||||
<field name="list_price">120.00</field>
|
||||
<field name="uom_id" ref="uom.product_uom_hour"/>
|
||||
<field name="categ_id" ref="pc_training"/>
|
||||
<field name="description_sale">Custom training, workshops, knowledge transfer sessions billed hourly.</field>
|
||||
</record>
|
||||
|
||||
<record id="prod_tech_support" model="product.template">
|
||||
<field name="name">Technical Support — Per-incident / Hourly</field>
|
||||
<field name="default_code">TECH-SUPPORT</field>
|
||||
<field name="type">service</field>
|
||||
<field name="list_price">160.00</field>
|
||||
<field name="uom_id" ref="uom.product_uom_hour"/>
|
||||
<field name="categ_id" ref="pc_tech_support"/>
|
||||
<field name="description_sale">Ad-hoc technical support, troubleshooting, infrastructure setup billed hourly.</field>
|
||||
</record>
|
||||
|
||||
<!-- ============================================================ -->
|
||||
<!-- Reseller-side placeholders (4310 / 4320) -->
|
||||
<!-- ============================================================ -->
|
||||
|
||||
<record id="prod_resale_software_template" model="product.template">
|
||||
<field name="name">Third-party Software License (template)</field>
|
||||
<field name="default_code">RESALE-SW</field>
|
||||
<field name="type">service</field>
|
||||
<field name="list_price">0.00</field>
|
||||
<field name="categ_id" ref="pc_resale_software"/>
|
||||
<field name="description_sale">Template for reselling third-party software licenses (M365, Adobe, etc.). Duplicate & set price per item.</field>
|
||||
</record>
|
||||
|
||||
</data>
|
||||
</odoo>
|
||||
@@ -4,14 +4,12 @@ import logging
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
# l10n_ca account codes that collide with the Nexa CoA design and that
|
||||
# l10n_ca pre-loads with 'income_other'/'expense'/etc. types we don't want.
|
||||
# Each of these is checked at pre_init: if it has zero postings we suffix
|
||||
# its code with '.OLD' and archive it so our XML can claim the code.
|
||||
# Codes with postings are LEFT ALONE — we renumbered the Nexa code instead
|
||||
# (115100 stays as l10n_ca 'Customers Account' AR; Nexa shareholder receivable
|
||||
# moved to 119100. 511100 stays as l10n_ca 'Inside Purchases'; Nexa Cloud
|
||||
# Infrastructure moved to 511105).
|
||||
# l10n_ca account codes that collide with the Nexa CoA design. Each is
|
||||
# checked at pre_init: if it has zero postings we suffix its code with
|
||||
# '.OLD' and archive it so our XML can claim the code.
|
||||
# Codes with postings are LEFT ALONE — we renumbered the Nexa account.
|
||||
# Currently 115100 stays as l10n_ca 'Customers Account' (240 postings, AR
|
||||
# control) — Nexa shareholder receivable sits at 115200 instead.
|
||||
_L10N_CA_COLLISION_CODES = [
|
||||
"118100", "118200", "118300",
|
||||
"213100", "214100",
|
||||
@@ -19,12 +17,18 @@ _L10N_CA_COLLISION_CODES = [
|
||||
"311100", "311200", "311300",
|
||||
"411100", "411200", "411300",
|
||||
"413100", "413200", "413300",
|
||||
"511110", "511120", "511130", "511140", "511200", "511210",
|
||||
"511100", "511110", "511120", "511130", "511140", "511200", "511210",
|
||||
"512100", "512110", "512200",
|
||||
"611100", "611200", "611300",
|
||||
"612100", "612200",
|
||||
]
|
||||
|
||||
# Codes that MUST be cleared even if they have postings (force-suffix to .OLD).
|
||||
# Use sparingly — historical reports lose the original name. Only for codes
|
||||
# where the Nexa account at that code is the canonical one going forward and
|
||||
# any prior posting is a misclassification the user will re-class later.
|
||||
_L10N_CA_FORCE_CLEAR_CODES = {"511100"}
|
||||
|
||||
|
||||
def pre_init_hook(env):
|
||||
"""Run BEFORE XML data is loaded. Clear l10n_ca account codes that would
|
||||
@@ -47,7 +51,8 @@ def _clear_l10n_ca_collisions(env):
|
||||
not_found += 1
|
||||
continue
|
||||
usage = env["account.move.line"].search_count([("account_id", "=", acc.id)])
|
||||
if usage > 0:
|
||||
force = code in _L10N_CA_FORCE_CLEAR_CODES
|
||||
if usage > 0 and not force:
|
||||
_logger.info(
|
||||
"nexa_coa_setup: keeping l10n_ca account %s (%s) — %d postings exist",
|
||||
code, acc.name, usage,
|
||||
@@ -64,6 +69,11 @@ def _clear_l10n_ca_collisions(env):
|
||||
"active": False,
|
||||
})
|
||||
cleared += 1
|
||||
if force and usage > 0:
|
||||
_logger.info(
|
||||
"nexa_coa_setup: force-cleared %s despite %d postings (in FORCE_CLEAR set)",
|
||||
code, usage,
|
||||
)
|
||||
_logger.info(
|
||||
"nexa_coa_setup: collision sweep — cleared %d, kept-with-postings %d, not-found %d",
|
||||
cleared, kept_with_postings, not_found,
|
||||
@@ -79,10 +89,353 @@ def post_init_hook(env):
|
||||
_normalize_company_hst_number(env)
|
||||
_archive_unused_l10n_ca_accounts(env)
|
||||
_rename_legacy_accounts(env)
|
||||
_archive_unused_taxes(env)
|
||||
_migrate_tax_repartition_accounts(env)
|
||||
_configure_fiscal_position_tax_maps(env)
|
||||
_delete_unused_accounts(env)
|
||||
_lock_fiscal_year_2025(env)
|
||||
_logger.info("nexa_coa_setup: post_init_hook complete")
|
||||
|
||||
|
||||
def _delete_unused_accounts(env):
|
||||
"""Hard-delete every account that's safe to remove.
|
||||
|
||||
'Safe' = not owned by nexa_coa_setup AND not referenced by:
|
||||
- account.move.line (posted entries)
|
||||
- account.tax.repartition.line
|
||||
- account.journal default/suspense/profit/loss accounts
|
||||
- account.fiscal.position.account maps
|
||||
- product.category property_account_income_categ_id / property_account_expense_categ_id
|
||||
- product.template property_account_income_id / property_account_expense_id
|
||||
- res.partner property_account_payable_id / property_account_receivable_id
|
||||
- res.company income_currency_exchange_account_id / expense_currency_exchange_account_id /
|
||||
transfer_account_id
|
||||
|
||||
Properties in Odoo 19 are stored as JSONB (e.g. {"1": 1234}) per company.
|
||||
|
||||
Tries unlink in batches; if a batch fails, falls back to per-record so the
|
||||
rest still get cleaned. Logs successes and skipped (still-referenced) ids.
|
||||
"""
|
||||
# Collect every account-id we must keep
|
||||
keep = set()
|
||||
|
||||
# 1. Nexa-owned
|
||||
env.cr.execute(
|
||||
"SELECT res_id FROM ir_model_data WHERE model='account.account' AND module='nexa_coa_setup'"
|
||||
)
|
||||
keep.update(r[0] for r in env.cr.fetchall())
|
||||
|
||||
# 2. Anything with posted move lines
|
||||
env.cr.execute(
|
||||
"SELECT DISTINCT account_id FROM account_move_line WHERE account_id IS NOT NULL"
|
||||
)
|
||||
keep.update(r[0] for r in env.cr.fetchall())
|
||||
|
||||
# 3. Tax repartition lines
|
||||
env.cr.execute(
|
||||
"SELECT DISTINCT account_id FROM account_tax_repartition_line WHERE account_id IS NOT NULL"
|
||||
)
|
||||
keep.update(r[0] for r in env.cr.fetchall())
|
||||
|
||||
# 4. Journal default/suspense/profit/loss accounts
|
||||
env.cr.execute("""
|
||||
SELECT default_account_id FROM account_journal WHERE default_account_id IS NOT NULL
|
||||
UNION SELECT suspense_account_id FROM account_journal WHERE suspense_account_id IS NOT NULL
|
||||
UNION SELECT profit_account_id FROM account_journal WHERE profit_account_id IS NOT NULL
|
||||
UNION SELECT loss_account_id FROM account_journal WHERE loss_account_id IS NOT NULL
|
||||
""")
|
||||
keep.update(r[0] for r in env.cr.fetchall())
|
||||
|
||||
# 5. Fiscal position account substitution maps
|
||||
env.cr.execute(
|
||||
"SELECT account_src_id FROM account_fiscal_position_account WHERE account_src_id IS NOT NULL "
|
||||
"UNION SELECT account_dest_id FROM account_fiscal_position_account WHERE account_dest_id IS NOT NULL"
|
||||
)
|
||||
keep.update(r[0] for r in env.cr.fetchall())
|
||||
|
||||
# 6. JSONB property fields on product_category and product_template
|
||||
for table, col in [
|
||||
("product_category", "property_account_income_categ_id"),
|
||||
("product_category", "property_account_expense_categ_id"),
|
||||
("product_category", "property_stock_valuation_account_id"),
|
||||
("product_category", "property_price_difference_account_id"),
|
||||
("product_template", "property_account_income_id"),
|
||||
("product_template", "property_account_expense_id"),
|
||||
]:
|
||||
env.cr.execute(
|
||||
f"SELECT DISTINCT (jsonb_each_text({col})).value::int "
|
||||
f"FROM {table} WHERE {col} IS NOT NULL"
|
||||
)
|
||||
keep.update(r[0] for r in env.cr.fetchall() if r[0])
|
||||
|
||||
# 7. JSONB property fields on res_partner
|
||||
for col in ("property_account_payable_id", "property_account_receivable_id"):
|
||||
env.cr.execute(
|
||||
f"SELECT DISTINCT (jsonb_each_text({col})).value::int "
|
||||
f"FROM res_partner WHERE {col} IS NOT NULL"
|
||||
)
|
||||
keep.update(r[0] for r in env.cr.fetchall() if r[0])
|
||||
|
||||
# 8. Company exchange/transfer accounts (regular int columns, not JSONB)
|
||||
env.cr.execute("""
|
||||
SELECT income_currency_exchange_account_id FROM res_company
|
||||
WHERE income_currency_exchange_account_id IS NOT NULL
|
||||
UNION SELECT expense_currency_exchange_account_id FROM res_company
|
||||
WHERE expense_currency_exchange_account_id IS NOT NULL
|
||||
UNION SELECT transfer_account_id FROM res_company
|
||||
WHERE transfer_account_id IS NOT NULL
|
||||
UNION SELECT account_default_pos_receivable_account_id FROM res_company
|
||||
WHERE account_default_pos_receivable_account_id IS NOT NULL
|
||||
""")
|
||||
keep.update(r[0] for r in env.cr.fetchall())
|
||||
|
||||
# Pick deletion candidates
|
||||
candidates = env["account.account"].with_context(active_test=False).search([
|
||||
("id", "not in", list(keep) or [0]),
|
||||
])
|
||||
if not candidates:
|
||||
_logger.info("nexa_coa_setup: no accounts to delete")
|
||||
return
|
||||
|
||||
_logger.info(
|
||||
"nexa_coa_setup: attempting to delete %d unused accounts (keeping %d protected)",
|
||||
len(candidates), len(keep),
|
||||
)
|
||||
|
||||
# Try bulk first, fall back to per-record on error
|
||||
deleted = 0
|
||||
skipped = 0
|
||||
try:
|
||||
with env.cr.savepoint():
|
||||
candidates.unlink()
|
||||
deleted = len(candidates)
|
||||
except Exception:
|
||||
# Per-record fallback
|
||||
for acc in candidates:
|
||||
try:
|
||||
with env.cr.savepoint():
|
||||
acc.unlink()
|
||||
deleted += 1
|
||||
except Exception as e:
|
||||
_logger.debug(
|
||||
"nexa_coa_setup: cannot delete %s (%s): %s",
|
||||
acc.code, acc.name, e,
|
||||
)
|
||||
skipped += 1
|
||||
|
||||
_logger.info("nexa_coa_setup: deleted %d accounts, skipped %d (still referenced)", deleted, skipped)
|
||||
|
||||
|
||||
# Map of legacy l10n_ca account codes (some now suffixed '.OLD', some still
|
||||
# raw codes that were archived) → Nexa consolidated tax accounts.
|
||||
# Used by _migrate_tax_repartition_accounts to repoint tax repartition lines
|
||||
# at the new accounts, so when an invoice creates tax journal items they hit
|
||||
# 118100 (ITC) and 213100 (HST collected) instead of the archived legacy ones.
|
||||
_TAX_REPARTITION_REMAP = {
|
||||
# ITC / receivable side → Nexa 1210 HST/GST ITC Receivable (or 1230 QST refund)
|
||||
"118100.OLD": "1210",
|
||||
"118200.OLD": "1230",
|
||||
"118300.OLD": "1210",
|
||||
"118400": "1210",
|
||||
"118500": "1210",
|
||||
# Payable / collected side → Nexa 2110 HST/GST Collected (or 2120 QST collected)
|
||||
"231000": "2110",
|
||||
"232000": "2120",
|
||||
"233000": "2110",
|
||||
"234000": "2110",
|
||||
"235000": "2110",
|
||||
}
|
||||
|
||||
|
||||
def _migrate_tax_repartition_accounts(env):
|
||||
"""Repoint account_tax_repartition_line.account_id from legacy l10n_ca
|
||||
accounts to Nexa's consolidated tax accounts (118100 ITC, 213100 HST
|
||||
collected, 213500 QST collected). Without this, posting an invoice that
|
||||
uses an active tax (e.g., 13% HST) fails because the repartition still
|
||||
points at archived accounts.
|
||||
|
||||
Idempotent: only touches repartition lines whose current account is in
|
||||
the legacy set.
|
||||
"""
|
||||
legacy_codes = list(_TAX_REPARTITION_REMAP.keys())
|
||||
legacy_accounts = env["account.account"].with_context(active_test=False).search([
|
||||
("code", "in", legacy_codes),
|
||||
])
|
||||
if not legacy_accounts:
|
||||
_logger.info("nexa_coa_setup: no legacy tax accounts found; repartition migration skipped")
|
||||
return
|
||||
|
||||
# Build target ID lookup (target accounts must exist and be active)
|
||||
target_ids = {}
|
||||
for legacy_code, target_code in _TAX_REPARTITION_REMAP.items():
|
||||
target = env["account.account"].search([("code", "=", target_code), ("active", "=", True)], limit=1)
|
||||
if target:
|
||||
target_ids[legacy_code] = target.id
|
||||
|
||||
migrated = 0
|
||||
for legacy in legacy_accounts:
|
||||
target_id = target_ids.get(legacy.code)
|
||||
if not target_id:
|
||||
continue
|
||||
rep_lines = env["account.tax.repartition.line"].search([("account_id", "=", legacy.id)])
|
||||
if rep_lines:
|
||||
rep_lines.write({"account_id": target_id})
|
||||
migrated += len(rep_lines)
|
||||
_logger.info("nexa_coa_setup: migrated %d tax repartition lines to Nexa accounts", migrated)
|
||||
|
||||
|
||||
def _configure_fiscal_position_tax_maps(env):
|
||||
"""Configure default Ontario taxes on the company + tax substitution maps
|
||||
on each Nexa fiscal position.
|
||||
|
||||
Design: Nexa is Ontario-based, so:
|
||||
- Default sale tax = '13% HST' (sale)
|
||||
- Default purchase tax = '13% HST' (purchase)
|
||||
- Fiscal positions substitute OUT to other rates per customer location:
|
||||
ON → no substitution (already 13%)
|
||||
Atlantic → 15% HST
|
||||
Quebec → 14.975% GST+QST
|
||||
BC/Prairies/Territories → 5% GST only
|
||||
US/Export/Exempt → 0% GST
|
||||
|
||||
Odoo 19 fiscal position model:
|
||||
- account.fiscal.position has tax_ids (M2M) — destination taxes
|
||||
- account.tax has original_tax_ids (M2M) — source taxes it replaces
|
||||
|
||||
Idempotent: writes are 'replace' style so re-running cleans up prior runs.
|
||||
"""
|
||||
def find_tax(name, use):
|
||||
return env["account.tax"].search(
|
||||
[("name", "=", name), ("type_tax_use", "=", use), ("active", "=", True)],
|
||||
limit=1,
|
||||
)
|
||||
|
||||
HST_13_sale = find_tax("13% HST", "sale")
|
||||
HST_13_purchase = find_tax("13% HST", "purchase")
|
||||
HST_15_sale = find_tax("15% HST", "sale")
|
||||
HST_15_purchase = find_tax("15% HST", "purchase")
|
||||
QST_sale = find_tax("14.975% GST+QST", "sale")
|
||||
QST_purchase = find_tax("14.975% GST+QST", "purchase")
|
||||
GST_5_sale = find_tax("5% GST", "sale")
|
||||
GST_5_purchase = find_tax("5% GST", "purchase")
|
||||
ZERO_sale = find_tax("0% GST", "sale")
|
||||
ZERO_purchase = find_tax("0% GST", "purchase")
|
||||
|
||||
# Set company-default sale/purchase taxes to Ontario HST 13% (the clean
|
||||
# named ones, not the l10n_ca 'HST for sales - 13%' legacy).
|
||||
company = env.ref("base.main_company", raise_if_not_found=False)
|
||||
if company:
|
||||
if HST_13_sale:
|
||||
company.account_sale_tax_id = HST_13_sale.id
|
||||
if HST_13_purchase:
|
||||
company.account_purchase_tax_id = HST_13_purchase.id
|
||||
_logger.info(
|
||||
"nexa_coa_setup: set company default sale tax = %s, purchase tax = %s",
|
||||
HST_13_sale.name if HST_13_sale else "(none)",
|
||||
HST_13_purchase.name if HST_13_purchase else "(none)",
|
||||
)
|
||||
|
||||
# Each entry: (fp_xmlid, [(source, destination), ...])
|
||||
# Note: Odoo 19's account.fiscal.position.map_tax treats EMPTY tax_ids as
|
||||
# "remove ALL taxes" (tax-unit semantics). To get the legacy "pass-through"
|
||||
# behavior we have to put at least one destination tax in tax_ids — even
|
||||
# if no substitution is needed. We use a self-mapping (13% HST → 13% HST)
|
||||
# which is filtered out below as no-op but still populates tax_ids.
|
||||
fp_map = [
|
||||
# Ontario is the home: pass-through via self-mapping placeholder
|
||||
("nexa_coa_setup.fp_ca_ontario", [
|
||||
(HST_13_sale, HST_13_sale),
|
||||
(HST_13_purchase, HST_13_purchase),
|
||||
]),
|
||||
("nexa_coa_setup.fp_ca_atlantic", [
|
||||
(HST_13_sale, HST_15_sale),
|
||||
(HST_13_purchase, HST_15_purchase),
|
||||
]),
|
||||
("nexa_coa_setup.fp_ca_quebec", [
|
||||
(HST_13_sale, QST_sale),
|
||||
(HST_13_purchase, QST_purchase),
|
||||
]),
|
||||
("nexa_coa_setup.fp_ca_bc", [
|
||||
(HST_13_sale, GST_5_sale),
|
||||
(HST_13_purchase, GST_5_purchase),
|
||||
]),
|
||||
("nexa_coa_setup.fp_ca_prairies_territories", [
|
||||
(HST_13_sale, GST_5_sale),
|
||||
(HST_13_purchase, GST_5_purchase),
|
||||
]),
|
||||
("nexa_coa_setup.fp_export_us", [
|
||||
(HST_13_sale, ZERO_sale),
|
||||
]),
|
||||
("nexa_coa_setup.fp_export_intl", [
|
||||
(HST_13_sale, ZERO_sale),
|
||||
]),
|
||||
("nexa_coa_setup.fp_tax_exempt", [
|
||||
(HST_13_sale, ZERO_sale),
|
||||
]),
|
||||
]
|
||||
|
||||
configured = 0
|
||||
for fp_xmlid, pairs in fp_map:
|
||||
fp = env.ref(fp_xmlid, raise_if_not_found=False)
|
||||
if not fp:
|
||||
_logger.warning("nexa_coa_setup: fiscal position not found: %s", fp_xmlid)
|
||||
continue
|
||||
fp.tax_ids = [(5, 0, 0)]
|
||||
for src, dst in pairs:
|
||||
if not src or not dst:
|
||||
continue
|
||||
# Always add dst to fp.tax_ids so the FP isn't "empty"
|
||||
# (Odoo 19 treats empty tax_ids as remove-all-taxes).
|
||||
fp.tax_ids = [(4, dst.id, 0)]
|
||||
# If src == dst, no original_tax_ids link needed — the map_tax
|
||||
# fallback returns the source unchanged when no mapping is found.
|
||||
if src.id != dst.id and src.id not in dst.original_tax_ids.ids:
|
||||
dst.original_tax_ids = [(4, src.id, 0)]
|
||||
configured += 1
|
||||
_logger.info("nexa_coa_setup: configured tax maps on %d fiscal positions", configured)
|
||||
|
||||
|
||||
# Tax names to keep ACTIVE (covers GST/HST/QST/PST across provinces + zero-rated
|
||||
# export + exempt). Everything else gets archived if it has zero usage on
|
||||
# existing journal entries.
|
||||
_KEEP_TAX_NAMES = {
|
||||
"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", "7% PST BC", "8% PST MB", "6% PST SK", "5% PST SK",
|
||||
"0% GST", "0% Exempt", "0% Int",
|
||||
}
|
||||
|
||||
|
||||
def _archive_unused_taxes(env):
|
||||
"""Archive active taxes whose name is NOT in _KEEP_TAX_NAMES AND that
|
||||
have no usage on existing move lines. Preserves audit trail for historical
|
||||
moves; just hides duplicates and unused defaults from the active set.
|
||||
"""
|
||||
keep_names = list(_KEEP_TAX_NAMES)
|
||||
env.cr.execute(
|
||||
"""
|
||||
SELECT t.id
|
||||
FROM account_tax t
|
||||
WHERE t.active = true
|
||||
AND COALESCE(t.name->>'en_US', '') != ALL(%s)
|
||||
AND NOT EXISTS (
|
||||
SELECT 1 FROM account_move_line_account_tax_rel r
|
||||
WHERE r.account_tax_id = t.id
|
||||
)
|
||||
""",
|
||||
(keep_names,),
|
||||
)
|
||||
ids = [r[0] for r in env.cr.fetchall()]
|
||||
if not ids:
|
||||
_logger.info("nexa_coa_setup: no unused taxes to archive")
|
||||
return
|
||||
env["account.tax"].browse(ids).write({"active": False})
|
||||
_logger.info(
|
||||
"nexa_coa_setup: archived %d unused taxes (kept set: %d names)",
|
||||
len(ids), len(keep_names),
|
||||
)
|
||||
|
||||
|
||||
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)
|
||||
@@ -95,13 +448,81 @@ def _normalize_company_hst_number(env):
|
||||
|
||||
|
||||
def _archive_unused_l10n_ca_accounts(env):
|
||||
"""Stub — filled in Phase 4. Archives ~370 unused accounts."""
|
||||
pass
|
||||
"""Archive l10n_ca accounts that have zero postings and don't belong to
|
||||
nexa_coa_setup. Preserves history (active=False, never delete).
|
||||
|
||||
Idempotent: re-running has no effect on already-archived accounts.
|
||||
"""
|
||||
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 = [r[0] for r in env.cr.fetchall()]
|
||||
if not ids:
|
||||
_logger.info("nexa_coa_setup: no unused accounts to archive")
|
||||
return
|
||||
env["account.account"].browse(ids).write({"active": False})
|
||||
_logger.info("nexa_coa_setup: archived %d unused accounts", len(ids))
|
||||
|
||||
|
||||
# Legacy accounts (from Gurpreet's prior bookkeeping) to rename + archive.
|
||||
# These all have postings, so we mark them "(LEGACY)" so they stop appearing
|
||||
# in regular dropdowns but their history is preserved for future
|
||||
# accountant-driven reconciliation.
|
||||
_LEGACY_RENAMES = [
|
||||
# (code, new_name, archive_after)
|
||||
("1400", "(LEGACY) Transferred to Gurpreet — re-class to 2510", True),
|
||||
("1505", "(LEGACY) Sent to India — re-class to 6120", 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 6720", True),
|
||||
("1501", "(LEGACY) Office Expenses — re-class to 6250", True),
|
||||
("411000", "(LEGACY) Inside Sales — re-class to 4xxx 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 6910", True),
|
||||
]
|
||||
|
||||
|
||||
def _rename_legacy_accounts(env):
|
||||
"""Stub — filled in Phase 4. Renames the 14xx/15xx legacy accounts."""
|
||||
pass
|
||||
"""Rename + archive the legacy accounts from prior bookkeeping.
|
||||
|
||||
Idempotent: accounts already prefixed with '(LEGACY)' are skipped.
|
||||
"""
|
||||
renamed = 0
|
||||
archived = 0
|
||||
for code, new_name, archive in _LEGACY_RENAMES:
|
||||
# active_test=False so we also rename accounts that were already
|
||||
# archived by _archive_unused_l10n_ca_accounts (e.g., 413000 sales bucket).
|
||||
accs = env["account.account"].with_context(active_test=False).search([("code", "=", code)])
|
||||
for acc in accs:
|
||||
if (acc.name or "").startswith("(LEGACY)"):
|
||||
continue
|
||||
acc.name = new_name
|
||||
renamed += 1
|
||||
if archive and acc.active:
|
||||
acc.active = False
|
||||
archived += 1
|
||||
_logger.info(
|
||||
"nexa_coa_setup: renamed %d legacy accounts, archived %d",
|
||||
renamed, archived,
|
||||
)
|
||||
|
||||
|
||||
def _lock_fiscal_year_2025(env):
|
||||
|
||||
34
nexa_coa_setup/scripts/convert_l10nca_to_4digit.py
Normal file
34
nexa_coa_setup/scripts/convert_l10nca_to_4digit.py
Normal file
@@ -0,0 +1,34 @@
|
||||
"""Rename the l10n_ca bank/AR/AP/tax legacy accounts to 4-digit codes."""
|
||||
LEGACY_MAP = [
|
||||
# (old_code, new_code, expected_name_substring)
|
||||
("112005", "1010", "Scotia Current"), # Primary operating (most-used)
|
||||
("112004", "1030", "BMO"), # BMO bank
|
||||
("112007", "1040", "RBC"), # RBC bank (NOT visa)
|
||||
("112008", "1070", "Scotia Credit Card"), # Scotia CC
|
||||
("112006", "1071", "RBC VISA"), # RBC visa
|
||||
("112002", "1080", "Outstanding Receipts"), # In-transit receipts
|
||||
("112003", "1081", "Outstanding Payments"), # In-transit payments
|
||||
("112001", "1090", "Bank Suspense"), # Catch-all suspense
|
||||
("115100", "1100", "Customers Account"), # AR control
|
||||
("118310", "1215", "HST receivable - 13%"), # Legacy 13% HST receivable (near new 1210)
|
||||
("211100", "2010", "Vendors Account"), # AP control
|
||||
("213310", "2115", "HST to pay - 13%"), # Legacy 13% HST collected (near new 2110)
|
||||
]
|
||||
ok = 0; skipped = 0; missing = 0
|
||||
for old, new, hint in LEGACY_MAP:
|
||||
acc = env['account.account'].search([('code', '=', old)], limit=1)
|
||||
if not acc:
|
||||
print(f"MISS {old} -> {new}: not found")
|
||||
missing += 1; continue
|
||||
if hint.lower() not in (acc.name or '').lower():
|
||||
print(f"SKIP {old} -> {new}: name '{acc.name}' doesn't match '{hint}'")
|
||||
skipped += 1; continue
|
||||
conflict = env['account.account'].with_context(active_test=False).search([('code', '=', new)], limit=1)
|
||||
if conflict:
|
||||
print(f"SKIP {old} -> {new}: target occupied by {conflict.name}")
|
||||
skipped += 1; continue
|
||||
acc.code = new
|
||||
ok += 1
|
||||
print(f"OK {old} -> {new}: {acc.name}")
|
||||
print(f">>> Renumbered {ok}, skipped {skipped}, missing {missing} of {len(LEGACY_MAP)}")
|
||||
env.cr.commit()
|
||||
171
nexa_coa_setup/scripts/convert_to_4digit.py
Normal file
171
nexa_coa_setup/scripts/convert_to_4digit.py
Normal file
@@ -0,0 +1,171 @@
|
||||
"""Map current Nexa CoA codes (6-digit) to a clean 4-digit scheme.
|
||||
Run on prod via odoo-shell. Updates account_account.code in place."""
|
||||
|
||||
# (current_code, new_code, expected_name_substring_for_safety)
|
||||
CODE_MAP = [
|
||||
# 1xxx ASSETS
|
||||
("115200", "1120", "Due From Shareholder"),
|
||||
("115900", "1130", "Due From Associated Corporations"),
|
||||
("118100", "1210", "Input Tax Credit"),
|
||||
("118200", "1220", "Instalments Paid"),
|
||||
("118300", "1230", "QST Input Tax Refund"),
|
||||
("151100", "1510", "Computer Hardware"),
|
||||
("151200", "1520", "Office Furniture"),
|
||||
("151300", "1530", "Vehicles"),
|
||||
("151400", "1540", "Leasehold Improvements"),
|
||||
("151500", "1550", "Acquired Software"),
|
||||
("151600", "1560", "Tools"),
|
||||
("154100", "1710", "Acc. Depreciation — Computer"),
|
||||
("154200", "1720", "Acc. Depreciation — Office"),
|
||||
("154300", "1730", "Acc. Depreciation — Vehicles"),
|
||||
("154400", "1740", "Acc. Depreciation — Leasehold"),
|
||||
("154500", "1750", "Acc. Depreciation — Acquired"),
|
||||
|
||||
# 2xxx LIABILITIES
|
||||
("213100", "2110", "HST/GST Collected"),
|
||||
("213500", "2120", "QST Collected"),
|
||||
("214100", "2130", "Net HST/GST Payable"),
|
||||
("215100", "2210", "Source Deductions Payable — Federal"),
|
||||
("215200", "2220", "Source Deductions Payable — CPP"),
|
||||
("215300", "2230", "Source Deductions Payable — EI"),
|
||||
("216100", "2310", "Federal Payable"),
|
||||
("216200", "2320", "Provincial Payable"),
|
||||
("216300", "2330", "Tax Instalments Paid"),
|
||||
("221100", "2510", "Due To Shareholder"),
|
||||
("221200", "2520", "Shareholder Loan"),
|
||||
("222900", "2590", "Due To Associated Corporations"),
|
||||
|
||||
# 3xxx EQUITY
|
||||
("311100", "3010", "Common Shares"),
|
||||
("311200", "3020", "Preferred Shares"),
|
||||
("311300", "3030", "Contributed Surplus"),
|
||||
("321100", "3510", "Retained Earnings — Current"),
|
||||
("321200", "3520", "Retained Earnings — Prior"),
|
||||
("321900", "3590", "Dividends Declared"),
|
||||
|
||||
# 4xxx REVENUE
|
||||
("411100", "4010", "SaaS Subscription"),
|
||||
("411200", "4020", "Hosting"),
|
||||
("411300", "4030", "Support"),
|
||||
("411400", "4040", "Domain/SSL"),
|
||||
("411500", "4050", "Setup"),
|
||||
("412100", "4110", "Custom Software Development"),
|
||||
("412200", "4120", "Custom Web Application"),
|
||||
("412300", "4130", "Custom Website"),
|
||||
("412400", "4140", "ERP Implementation"),
|
||||
("412500", "4150", "Mobile App"),
|
||||
("412600", "4160", "Business App"),
|
||||
("413100", "4210", "Consulting"),
|
||||
("413200", "4220", "Training"),
|
||||
("413300", "4230", "Technical Support — Per-incident"),
|
||||
("414100", "4310", "Third-party Software Resale"),
|
||||
("414200", "4320", "Hardware Resale"),
|
||||
("419100", "4910", "Sales Discounts"),
|
||||
("419200", "4920", "Sales Returns"),
|
||||
("419300", "4930", "Bad Debt Recovery"),
|
||||
|
||||
# 5xxx COGS
|
||||
("511100", "5010", "Cloud Infrastructure"),
|
||||
("511110", "5020", "CDN"),
|
||||
("511120", "5030", "Backup"),
|
||||
("511130", "5040", "Database"),
|
||||
("511140", "5050", "Monitoring"),
|
||||
("511150", "5060", "SSL"),
|
||||
("511160", "5070", "DNS"),
|
||||
("511200", "5110", "Third-party API"),
|
||||
("511210", "5120", "Per-customer Licensing"),
|
||||
("512100", "5210", "Subcontracted Labour — Canadian"),
|
||||
("512110", "5220", "Subcontracted Labour — Foreign"),
|
||||
("512200", "5230", "Project-specific Software"),
|
||||
("512300", "5240", "Project Travel"),
|
||||
("512400", "5250", "Project Hardware"),
|
||||
("513100", "5310", "Cost of Software Resold"),
|
||||
("513200", "5320", "Cost of Hardware Resold"),
|
||||
("519100", "5910", "COGS Adjustments"),
|
||||
|
||||
# 6xxx OPERATING EXPENSES
|
||||
("611100", "6010", "Salaries & Wages — Development"),
|
||||
("611200", "6020", "Salaries & Wages — Sales"),
|
||||
("611300", "6030", "Salaries & Wages — Admin"),
|
||||
("611400", "6040", "Salary — Shareholder"),
|
||||
("611500", "6050", "Employer CPP"),
|
||||
("611600", "6060", "Employer EI"),
|
||||
("611700", "6070", "Employer Health Tax"),
|
||||
("611800", "6080", "WCB"),
|
||||
("611900", "6090", "Employee Benefits"),
|
||||
("611950", "6091", "Bonuses"),
|
||||
("611960", "6092", "Vacation Pay"),
|
||||
("612100", "6110", "Contract Labour — Canadian"),
|
||||
("612200", "6120", "Contract Labour — Foreign"),
|
||||
("621100", "6210", "Rent"),
|
||||
("621200", "6220", "Home Office"),
|
||||
("621300", "6230", "Utilities"),
|
||||
("621400", "6240", "Internet"),
|
||||
("621500", "6250", "Office Supplies"),
|
||||
("621600", "6260", "Cleaning"),
|
||||
("621700", "6270", "Office Snacks"),
|
||||
("631100", "6310", "Software — Productivity"),
|
||||
("631200", "6320", "Software — Development Tools"),
|
||||
("631300", "6330", "Software — Internal Infrastructure"),
|
||||
("631400", "6340", "Software — Security"),
|
||||
("631500", "6350", "Software — Sales"),
|
||||
("641100", "6410", "Advertising — Digital"),
|
||||
("641200", "6420", "Advertising — Content"),
|
||||
("641300", "6430", "Trade Shows"),
|
||||
("641400", "6440", "Promotional"),
|
||||
("641500", "6450", "Website — Own"),
|
||||
("651100", "6510", "Legal Fees"),
|
||||
("651200", "6520", "Accounting"),
|
||||
("651300", "6530", "Tax Preparation"),
|
||||
("651400", "6540", "Business Consulting"),
|
||||
("661100", "6610", "Insurance — Commercial General"),
|
||||
("661200", "6620", "Insurance — Professional Liability"),
|
||||
("661300", "6630", "Insurance — Cyber"),
|
||||
("661400", "6640", "Insurance — Property"),
|
||||
("661500", "6650", "Insurance — Directors"),
|
||||
("671100", "6710", "Travel"),
|
||||
("671200", "6720", "Meals"),
|
||||
("671300", "6730", "Vehicle — Operating"),
|
||||
("671400", "6740", "Mileage"),
|
||||
("681100", "6810", "Conferences"),
|
||||
("681200", "6820", "Courses"),
|
||||
("681300", "6830", "Books"),
|
||||
("681400", "6840", "Professional Memberships"),
|
||||
("691100", "6910", "Bank Service Charges"),
|
||||
("691200", "6920", "Merchant Processing"),
|
||||
("691300", "6930", "Wire Transfer"),
|
||||
("691400", "6940", "Interest Expense — Bank"),
|
||||
("691500", "6950", "Interest Expense — Credit Cards"),
|
||||
("691600", "6960", "Late Payment Penalties"),
|
||||
|
||||
# 7xxx OTHER (was 699xxx)
|
||||
("699100", "7010", "Bad Debt Expense"),
|
||||
("699200", "7020", "Donations"),
|
||||
("699300", "7030", "Penalties & Fines"),
|
||||
("699400", "7040", "Realized FX Losses"),
|
||||
("699500", "7050", "Depreciation"),
|
||||
]
|
||||
|
||||
ok = 0
|
||||
skipped = 0
|
||||
missing = 0
|
||||
for old_code, new_code, expected_name_part in CODE_MAP:
|
||||
acc = env['account.account'].search([('code', '=', old_code)], limit=1)
|
||||
if not acc:
|
||||
print(f"MISS {old_code} → {new_code}: not found")
|
||||
missing += 1
|
||||
continue
|
||||
if expected_name_part.lower() not in (acc.name or '').lower():
|
||||
print(f"SKIP {old_code} → {new_code}: name '{acc.name}' doesn't contain '{expected_name_part}'")
|
||||
skipped += 1
|
||||
continue
|
||||
# Check target code is free
|
||||
conflict = env['account.account'].with_context(active_test=False).search([('code', '=', new_code)], limit=1)
|
||||
if conflict:
|
||||
print(f"SKIP {old_code} → {new_code}: target occupied by {conflict.name}")
|
||||
skipped += 1
|
||||
continue
|
||||
acc.code = new_code
|
||||
ok += 1
|
||||
print(f">>> Renumbered {ok}, skipped {skipped}, missing {missing} of {len(CODE_MAP)}")
|
||||
env.cr.commit()
|
||||
40
nexa_coa_setup/scripts/fix_gl_codes.py
Normal file
40
nexa_coa_setup/scripts/fix_gl_codes.py
Normal file
@@ -0,0 +1,40 @@
|
||||
"""Renumber a few oddly-coded accounts to cleaner positions.
|
||||
Idempotent — checks before each move."""
|
||||
moves = [
|
||||
# (current_code, target_code, expected_name_substr)
|
||||
('119100', '115200', 'Due From Shareholder'),
|
||||
('119900', '115900', 'Due From Associated Corporations'),
|
||||
]
|
||||
print(">>> Easy renames (no postings)")
|
||||
for old, new, name_hint in moves:
|
||||
acc = env['account.account'].search([('code', '=', old)], limit=1)
|
||||
if not acc:
|
||||
print(f"SKIP {old}: not found")
|
||||
continue
|
||||
if name_hint.lower() not in (acc.name or '').lower():
|
||||
print(f"SKIP {old}: name doesn't match expected '{name_hint}' (got '{acc.name}')")
|
||||
continue
|
||||
conflict = env['account.account'].with_context(active_test=False).search([('code', '=', new)], limit=1)
|
||||
if conflict:
|
||||
print(f"SKIP {old}->{new}: target code occupied by {conflict.name}")
|
||||
continue
|
||||
acc.code = new
|
||||
print(f"OK {old} -> {new}: {acc.name}")
|
||||
|
||||
# The 511100 swap — rename legacy first, then renumber ours
|
||||
print(">>> 511105 -> 511100 swap")
|
||||
legacy = env['account.account'].with_context(active_test=False).search([('code', '=', '511100'), ('name', 'ilike', 'inside purchases')], limit=1)
|
||||
ours = env['account.account'].search([('code', '=', '511105'), ('name', 'ilike', 'cloud infrastructure')], limit=1)
|
||||
if not legacy:
|
||||
print("legacy 511100 not found (already moved?)")
|
||||
elif not ours:
|
||||
print("our 511105 not found (already renamed?)")
|
||||
else:
|
||||
# Rename legacy first to free the code
|
||||
legacy.write({'code': '511100.OLD', 'name': f"(l10n_ca LEGACY) {legacy.name}", 'active': False})
|
||||
ours.code = '511100'
|
||||
print(f"OK legacy 511100 -> 511100.OLD ({legacy.name})")
|
||||
print(f"OK 511105 -> 511100 ({ours.name})")
|
||||
|
||||
env.cr.commit()
|
||||
print(">>> done <<<")
|
||||
46
nexa_coa_setup/scripts/fix_invoice_1127.py
Normal file
46
nexa_coa_setup/scripts/fix_invoice_1127.py
Normal file
@@ -0,0 +1,46 @@
|
||||
"""Fix invoice 1127 + assign products to proper Nexa categories."""
|
||||
|
||||
# 1. Get target account IDs
|
||||
acct_4320 = env['account.account'].search([('code', '=', '4320')], limit=1) # Hardware Resale
|
||||
acct_4230 = env['account.account'].search([('code', '=', '4230')], limit=1) # Tech Support hourly
|
||||
print(f"4320 Hardware Resale id={acct_4320.id}; 4230 Tech Support id={acct_4230.id}")
|
||||
|
||||
# 2. Repoint invoice 1127's lines
|
||||
inv = env['account.move'].browse(62485)
|
||||
print(f"Invoice {inv.name} (id={inv.id}) - {inv.partner_id.name}")
|
||||
HW_CODES = {'P620','CUSPC','RTX5090','DRW','N6100','WD8TB','NV3500','UDM-PRO','ST4000VN008','12U-600','18U-600','2UD15','UBCP004'}
|
||||
SVC_CODES = {'SETUP'}
|
||||
|
||||
# Note: posted invoice lines are protected; we update the journal items directly via SQL
|
||||
hw_changed = sv_changed = 0
|
||||
for line in inv.line_ids:
|
||||
if not line.product_id:
|
||||
continue
|
||||
sku = line.product_id.default_code
|
||||
if sku in HW_CODES and line.account_id != acct_4320:
|
||||
env.cr.execute("UPDATE account_move_line SET account_id = %s WHERE id = %s",
|
||||
(acct_4320.id, line.id))
|
||||
hw_changed += 1
|
||||
elif sku in SVC_CODES and line.account_id != acct_4230:
|
||||
env.cr.execute("UPDATE account_move_line SET account_id = %s WHERE id = %s",
|
||||
(acct_4230.id, line.id))
|
||||
sv_changed += 1
|
||||
print(f"Repointed {hw_changed} hardware lines -> 4320, {sv_changed} service lines -> 4230")
|
||||
|
||||
# 3. Reassign product categories
|
||||
pc_hardware = env.ref('nexa_coa_setup.pc_resale_hardware')
|
||||
pc_techsupport = env.ref('nexa_coa_setup.pc_tech_support')
|
||||
print(f"Hardware category id={pc_hardware.id}; Tech Support category id={pc_techsupport.id}")
|
||||
|
||||
prods_changed = 0
|
||||
for prod in env['product.product'].search([('default_code', 'in', list(HW_CODES | SVC_CODES))]):
|
||||
sku = prod.default_code
|
||||
target = pc_hardware if sku in HW_CODES else pc_techsupport
|
||||
if prod.product_tmpl_id.categ_id != target:
|
||||
prod.product_tmpl_id.categ_id = target.id
|
||||
prods_changed += 1
|
||||
print(f" {sku} {prod.name} -> category '{target.name}'")
|
||||
print(f"Reassigned {prods_changed} product templates")
|
||||
|
||||
env.cr.commit()
|
||||
print(">>> done <<<")
|
||||
62
nexa_coa_setup/scripts/reclass_historical_411000.py
Normal file
62
nexa_coa_setup/scripts/reclass_historical_411000.py
Normal file
@@ -0,0 +1,62 @@
|
||||
"""Reclassify all 200 lines on legacy 411000 to proper Nexa accounts based on
|
||||
keyword rules. Run on prod via odoo-shell."""
|
||||
|
||||
acct_4030 = env['account.account'].search([('code', '=', '4030')], limit=1).id # Support & Maintenance
|
||||
acct_4230 = env['account.account'].search([('code', '=', '4230')], limit=1).id # Tech Support hourly
|
||||
acct_4320 = env['account.account'].search([('code', '=', '4320')], limit=1).id # Hardware Resale
|
||||
legacy_acct = env['account.account'].with_context(active_test=False).search([('code', '=', '411000')], limit=1).id
|
||||
|
||||
# (priority, regex pattern (case-insensitive), target_account_id, label)
|
||||
import re
|
||||
RULES = [
|
||||
('Computer & Server Maintenance', acct_4030, 'Support & Maintenance'),
|
||||
('Server Backup & Monitoring', acct_4030, 'Support & Maintenance'),
|
||||
('Membership Fee', acct_4030, 'Support & Maintenance (membership)'),
|
||||
('[CUSTCOMP]', acct_4320, 'Hardware Resale (custom PC)'),
|
||||
('Custom Computer', acct_4320, 'Hardware Resale (custom PC)'),
|
||||
('ustom Computer', acct_4320, 'Hardware Resale (typo'), # the 'ustom Computer' typo entry
|
||||
('HP Desk Computer', acct_4320, 'Hardware Resale (HP desktop)'),
|
||||
('Server 2019', acct_4320, 'Hardware Resale (Windows Server license)'),
|
||||
('Server Rack', acct_4320, 'Hardware Resale (rack)'),
|
||||
('16 Port POE', acct_4320, 'Hardware Resale (switch)'),
|
||||
('CPU:', acct_4320, 'Hardware Resale (custom build)'),
|
||||
('Cleaning Supplies', acct_4320, 'Hardware Resale (consumables)'),
|
||||
('ONSITE-', acct_4230, 'Tech Support — onsite'),
|
||||
('OFFSITE-', acct_4230, 'Tech Support — offsite'),
|
||||
('Onsite Sever Setup', acct_4230, 'Tech Support — setup'),
|
||||
('Server Setup', acct_4230, 'Tech Support — setup'),
|
||||
('Wiring for', acct_4230, 'Tech Support — installation'),
|
||||
]
|
||||
|
||||
# Find all lines on 411000
|
||||
env.cr.execute("""
|
||||
SELECT aml.id, aml.name, aml.credit
|
||||
FROM account_move_line aml
|
||||
JOIN account_move m ON m.id = aml.move_id
|
||||
WHERE aml.account_id = %s AND m.move_type = 'out_invoice'
|
||||
""", (legacy_acct,))
|
||||
lines = env.cr.fetchall()
|
||||
|
||||
bucket = {acct_4030: 0, acct_4230: 0, acct_4320: 0}
|
||||
unmatched = []
|
||||
for line_id, line_name, credit in lines:
|
||||
name = (line_name or '').strip()
|
||||
matched = False
|
||||
for pattern, target, label in RULES:
|
||||
if pattern.lower() in name.lower():
|
||||
env.cr.execute("UPDATE account_move_line SET account_id = %s WHERE id = %s",
|
||||
(target, line_id))
|
||||
bucket[target] += 1
|
||||
matched = True
|
||||
break
|
||||
if not matched:
|
||||
unmatched.append((line_id, name[:60], credit))
|
||||
|
||||
print(f"Reclassified {bucket[acct_4030]} lines -> 4030 Support & Maintenance")
|
||||
print(f"Reclassified {bucket[acct_4230]} lines -> 4230 Tech Support — Hourly")
|
||||
print(f"Reclassified {bucket[acct_4320]} lines -> 4320 Hardware Resale")
|
||||
print(f"Unmatched: {len(unmatched)}")
|
||||
for u in unmatched[:20]:
|
||||
print(f" id={u[0]} amount={u[2]} name={u[1]!r}")
|
||||
env.cr.commit()
|
||||
print(">>> done <<<")
|
||||
59
nexa_coa_setup/scripts/test_invoices.py
Normal file
59
nexa_coa_setup/scripts/test_invoices.py
Normal file
@@ -0,0 +1,59 @@
|
||||
"""Test invoices using Form to emulate UI onchange behavior."""
|
||||
from odoo.tests.common import Form
|
||||
import json
|
||||
|
||||
results = []
|
||||
|
||||
def test_invoice(label, partner_vals, product_name, product_cat_xmlid, price=100.0):
|
||||
env.cr.execute("SAVEPOINT test")
|
||||
try:
|
||||
partner = env['res.partner'].search([('name', '=', partner_vals['name'])], limit=1)
|
||||
if not partner:
|
||||
partner = env['res.partner'].create(partner_vals)
|
||||
cat = env.ref(product_cat_xmlid)
|
||||
product = env['product.product'].search([('name', '=', product_name)], limit=1)
|
||||
if not product:
|
||||
product = env['product.product'].create({
|
||||
'name': product_name, 'type': 'service',
|
||||
'list_price': price, 'categ_id': cat.id,
|
||||
})
|
||||
with Form(env['account.move'].with_context(default_move_type='out_invoice')) as inv_form:
|
||||
inv_form.partner_id = partner
|
||||
with inv_form.invoice_line_ids.new() as line:
|
||||
line.product_id = product
|
||||
line.quantity = 1
|
||||
line.price_unit = price
|
||||
inv = inv_form.save()
|
||||
line = inv.invoice_line_ids[0]
|
||||
results.append({
|
||||
'label': label,
|
||||
'fiscal_position': inv.fiscal_position_id.name or '(none)',
|
||||
'taxes': line.tax_ids.mapped('name'),
|
||||
'income_account': f"{line.account_id.code} {line.account_id.name}",
|
||||
'subtotal': inv.amount_untaxed,
|
||||
'tax_total': inv.amount_tax,
|
||||
'total': inv.amount_total,
|
||||
'partner_tags': [c.name for c in partner.category_id],
|
||||
})
|
||||
finally:
|
||||
env.cr.execute("ROLLBACK TO SAVEPOINT test")
|
||||
|
||||
test_invoice("Ontario (HST 13%)",
|
||||
{'name': 'TEST CUST ON', 'country_id': env.ref('base.ca').id, 'state_id': env.ref('base.state_ca_on').id, 'customer_rank': 1},
|
||||
'TEST SaaS', 'nexa_coa_setup.pc_saas', 100.0)
|
||||
|
||||
test_invoice("US (Zero-rated)",
|
||||
{'name': 'TEST CUST US', 'country_id': env.ref('base.us').id, 'customer_rank': 1},
|
||||
'TEST SaaS', 'nexa_coa_setup.pc_saas', 100.0)
|
||||
|
||||
test_invoice("Quebec (GST+QST)",
|
||||
{'name': 'TEST CUST QC', 'country_id': env.ref('base.ca').id, 'state_id': env.ref('base.state_ca_qc').id, 'customer_rank': 1},
|
||||
'TEST SaaS', 'nexa_coa_setup.pc_saas', 100.0)
|
||||
|
||||
test_invoice("Intercompany -> Westin",
|
||||
{'name': 'Westin Healthcare Inc'},
|
||||
'TEST Consulting', 'nexa_coa_setup.pc_consulting', 150.0)
|
||||
|
||||
for r in results:
|
||||
print("---")
|
||||
print(json.dumps(r, indent=2, default=str))
|
||||
Reference in New Issue
Block a user