feat(nexa_coa_setup): hard-delete unused accounts
Adds _delete_unused_accounts hook that hard-deletes (not archives) every account that's safe to remove — not owned by nexa_coa_setup AND not referenced by: - account.move.line postings - account.tax.repartition.line - account.journal default/suspense/profit/loss accounts - account.fiscal.position.account substitution maps - product.category and product.template JSONB property_account_* fields - res.partner JSONB property_account_payable_id/receivable_id - res.company exchange/transfer/POS receivable accounts Tries bulk unlink first; falls back to per-record if a batch fails so the rest still get cleaned. Result on staging: 554 -> 172 total accounts (deleted 382). The 31 still archived are blocked by references (historical postings, tax repartition links, bank journal defaults, etc.) — left as archived so they're hidden from dropdowns but preserve audit history. Verified all 4 test invoices still post correctly (ON 113, US 100, QC 114.98, Westin intercompany 169.50). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -82,10 +82,140 @@ def post_init_hook(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
|
||||
|
||||
Reference in New Issue
Block a user