_archive_unused_l10n_ca_accounts: archives every active account that has zero postings and doesn't belong to nexa_coa_setup. Sweeps ~280 unused l10n_ca defaults from 426 to 141 active. _rename_legacy_accounts: marks 14 legacy bookkeeping codes with a '(LEGACY)' prefix indicating the new account they map to, and archives them. Uses active_test=False so already-archived accounts also get the prefix for future readability. Both idempotent — re-running on -u or via odoo-shell has no effect on already-processed records. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
203 lines
8.1 KiB
Python
203 lines
8.1 KiB
Python
# -*- coding: utf-8 -*-
|
|
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_COLLISION_CODES = [
|
|
"118100", "118200", "118300",
|
|
"213100", "214100",
|
|
"221200",
|
|
"311100", "311200", "311300",
|
|
"411100", "411200", "411300",
|
|
"413100", "413200", "413300",
|
|
"511110", "511120", "511130", "511140", "511200", "511210",
|
|
"512100", "512110", "512200",
|
|
"611100", "611200", "611300",
|
|
"612100", "612200",
|
|
]
|
|
|
|
|
|
def pre_init_hook(env):
|
|
"""Run BEFORE XML data is loaded. Clear l10n_ca account codes that would
|
|
collide with Nexa's chart of accounts."""
|
|
_logger.info("nexa_coa_setup: pre_init_hook starting")
|
|
_clear_l10n_ca_collisions(env)
|
|
_logger.info("nexa_coa_setup: pre_init_hook complete")
|
|
|
|
|
|
def _clear_l10n_ca_collisions(env):
|
|
"""For each colliding code: if it has zero postings, rename to NNNNNN.OLD
|
|
and set inactive. If it has postings, leave alone (Nexa code was renumbered
|
|
in the XML to avoid the conflict)."""
|
|
cleared = 0
|
|
kept_with_postings = 0
|
|
not_found = 0
|
|
for code in _L10N_CA_COLLISION_CODES:
|
|
acc = env["account.account"].search([("code", "=", code)], limit=1)
|
|
if not acc:
|
|
not_found += 1
|
|
continue
|
|
usage = env["account.move.line"].search_count([("account_id", "=", acc.id)])
|
|
if usage > 0:
|
|
_logger.info(
|
|
"nexa_coa_setup: keeping l10n_ca account %s (%s) — %d postings exist",
|
|
code, acc.name, usage,
|
|
)
|
|
kept_with_postings += 1
|
|
continue
|
|
new_code = f"{code}.OLD"
|
|
# Skip if already suffixed (idempotency)
|
|
if acc.code.endswith(".OLD"):
|
|
continue
|
|
acc.write({
|
|
"code": new_code,
|
|
"name": f"(l10n_ca LEGACY) {acc.name or acc.display_name}",
|
|
"active": False,
|
|
})
|
|
cleared += 1
|
|
_logger.info(
|
|
"nexa_coa_setup: collision sweep — cleared %d, kept-with-postings %d, not-found %d",
|
|
cleared, kept_with_postings, not_found,
|
|
)
|
|
|
|
|
|
def post_init_hook(env):
|
|
"""Imperative one-shot operations after module data is loaded.
|
|
|
|
Each helper is idempotent — safe to re-run on -u.
|
|
"""
|
|
_logger.info("nexa_coa_setup: post_init_hook starting")
|
|
_normalize_company_hst_number(env)
|
|
_archive_unused_l10n_ca_accounts(env)
|
|
_rename_legacy_accounts(env)
|
|
_lock_fiscal_year_2025(env)
|
|
_logger.info("nexa_coa_setup: post_init_hook complete")
|
|
|
|
|
|
def _normalize_company_hst_number(env):
|
|
"""Convert '741224877' to '741224877 RT0001' if not already in full form."""
|
|
company = env.ref("base.main_company", raise_if_not_found=False)
|
|
if not company:
|
|
return
|
|
vat = (company.partner_id.vat or "").strip()
|
|
if vat == "741224877":
|
|
company.partner_id.vat = "741224877 RT0001"
|
|
_logger.info("nexa_coa_setup: normalized HST# to '741224877 RT0001'")
|
|
|
|
|
|
def _archive_unused_l10n_ca_accounts(env):
|
|
"""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 221100", True),
|
|
("1505", "(LEGACY) Sent to India — re-class to 612200", True),
|
|
("1580", "(LEGACY) Transferred to Westin — Westin is now a partner", True),
|
|
("1590", "(LEGACY) Transferred to Divine — Divine is now a partner", True),
|
|
("1600", "(LEGACY) Transferred to Manpreet — non-related; archive", True),
|
|
("1500", "(LEGACY) Food & Entertainment — re-class to 671200", True),
|
|
("1501", "(LEGACY) Office Expenses — re-class to 621500", True),
|
|
("411000", "(LEGACY) Inside Sales — re-class to 412xxx specific lines", True),
|
|
("412000", "(LEGACY) Harmonized Provinces Sales — handled by tax codes", True),
|
|
("413000", "(LEGACY) Non-Harmonized Provinces Sales — handled by tax", True),
|
|
("414000", "(LEGACY) International Sales — handled by Zero-rated Export", True),
|
|
("12000", "(LEGACY) Abdul & Future Mobility — use partner subledger", True),
|
|
("12001", "(LEGACY) MSI Account — use partner subledger", True),
|
|
("110010", "(LEGACY) Bank Fee — re-class to 691100", True),
|
|
]
|
|
|
|
|
|
def _rename_legacy_accounts(env):
|
|
"""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):
|
|
"""Try to set fiscalyear_lock_date = 2025-12-31 on main company.
|
|
|
|
If Odoo blocks the lock because unreconciled bank statement lines or other
|
|
open items exist in the period, log a clear warning and continue. The user
|
|
can set the lock manually via Accounting > Configuration > Settings > Lock
|
|
Dates once those items are cleaned up.
|
|
"""
|
|
from datetime import date
|
|
from odoo.exceptions import RedirectWarning, UserError, ValidationError
|
|
company = env.ref("base.main_company", raise_if_not_found=False)
|
|
if not company:
|
|
return
|
|
target = date(2025, 12, 31)
|
|
if company.fiscalyear_lock_date and company.fiscalyear_lock_date >= target:
|
|
_logger.info("nexa_coa_setup: fiscalyear_lock_date already at or after 2025-12-31")
|
|
return
|
|
try:
|
|
company.fiscalyear_lock_date = target
|
|
_logger.info("nexa_coa_setup: fiscalyear_lock_date set to 2025-12-31")
|
|
except (RedirectWarning, UserError, ValidationError) as exc:
|
|
_logger.warning(
|
|
"nexa_coa_setup: could not auto-lock fiscal year 2025-12-31. "
|
|
"Reason: %s. Set the lock manually via Accounting > Configuration > "
|
|
"Settings > Lock Dates after the unreconciled items in the period "
|
|
"are cleaned up.",
|
|
exc,
|
|
)
|