The core script ``l10n_fr_account/migrations/2.1/end-migrate_update_taxes.py``
calls ``account.chart.template.try_loading('fr', company)``. During
``_post_load_data``, Odoo 18 resolves the French template's default accounts via
``self.ref()`` WITHOUT ``raise_if_not_found=False`` (chart_template.py:711/714
and the ``_get_property_accounts`` loop at :746). If any expected
``account.<company>_<key>`` xml-id is missing, it raises
``ValueError: External ID not found`` and aborts the registry load.
This happens when a database customized its chart of accounts: the generic PCG
account was deleted (e.g. 607000) or recreated without its xml-id (e.g. 707000).
Add a generic ORM ``post-migration`` script that, for every company with a
``chart_template``, reads the expected account xml-ids from the template API
(``_get_property_accounts`` / ``_get_chart_template_data`` /
``_get_account_account``) and recreates any missing ``account.<company>_<key>``
``ir.model.data`` pointing to the best matching account: exact code, else the
company account sharing the longest common code prefix, else a non-blocking
warning. It runs at stage ``post`` (before the native ``end`` script) and only
creates missing xml-ids — no account is modified. Idempotent, generic across
all migrated FR databases.
Remove the previous hardcoded 607/707 SQL block from
``versions/18.0/pre_upgrade.sh`` now superseded by this script.
196 lines
7.7 KiB
Python
196 lines
7.7 KiB
Python
# Copyright 0k / Élabore
|
|
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
|
|
#
|
|
# Custom OpenUpgrade migration script, injected through a dedicated
|
|
# --upgrade-path (see versions/18.0/upgrade.sh). Runs at stage "post" of
|
|
# l10n_fr_account/18.0.2.2, i.e. BEFORE the global "end" stage where the native
|
|
# l10n_fr_account/migrations/2.1/end-migrate_update_taxes.py runs.
|
|
#
|
|
# Problem
|
|
# -------
|
|
# end-migrate_update_taxes.py calls account.chart.template.try_loading('fr', ...).
|
|
# During _post_load_data(), Odoo 18 resolves the French template's default
|
|
# accounts via self.ref() WITHOUT raise_if_not_found=False, at three places:
|
|
# - property_account_income_categ_id (pcg_707_account) -> chart_template.py:711
|
|
# - property_account_expense_categ_id (pcg_607_account) -> chart_template.py:714
|
|
# - every _get_property_accounts()/additional_properties account -> :746
|
|
# If any expected account xml-id "account.<company>_<key>" is missing, it raises:
|
|
# ValueError: External ID not found in the system: account.1_pcg_607_account
|
|
# which aborts the whole registry load.
|
|
#
|
|
# This happens when a database customized its chart of accounts: the generic PCG
|
|
# account was deleted (e.g. 607000) or recreated without its xml-id (e.g. 707000).
|
|
#
|
|
# Fix (generic, not hardcoded to 607/707)
|
|
# ---------------------------------------
|
|
# For every company with a chart_template, we read the list of account xml-ids
|
|
# the template expects (dynamically, from the template ORM API), and for each one
|
|
# that is missing in the DB we recreate the "account.<company>_<key>" ir.model.data
|
|
# pointing to the best matching real account:
|
|
# 1. the account whose code matches exactly the template account code,
|
|
# 2. otherwise the company account whose code shares the longest common prefix
|
|
# with the template code (e.g. 607000 -> 607100),
|
|
# 3. otherwise nothing (logged as a warning; never blocks).
|
|
# No account is modified; only missing xml-ids are (re)created. Idempotent.
|
|
import logging
|
|
|
|
from openupgradelib import openupgrade
|
|
|
|
_logger = logging.getLogger(__name__)
|
|
|
|
|
|
def _collect_property_account_xmlids(template, code, accounts_csv):
|
|
"""Return the set of template account xml-ids referenced as property accounts.
|
|
|
|
Sources:
|
|
- template._get_property_accounts({}) (receivable, payable, expense, income, ...)
|
|
- the template_data dict (property_account_*_id keys)
|
|
- additional_properties declared on res.company template data
|
|
Only keep values that are real account xml-ids of this template, i.e. present
|
|
in accounts_csv (this filters out journals and dotted external ids).
|
|
"""
|
|
xmlids = set()
|
|
|
|
# 1. Base property accounts (model-level defaults)
|
|
try:
|
|
prop_fields = template._get_property_accounts({})
|
|
except Exception as exc: # noqa: BLE001
|
|
_logger.warning("Could not read _get_property_accounts: %s", exc)
|
|
prop_fields = {}
|
|
|
|
# 2. Full template data: template_data holds property_account_*_id values,
|
|
# res.company holds additional_properties (currency exchange, deferral...).
|
|
try:
|
|
tdata = template._get_chart_template_data(code)
|
|
except Exception as exc: # noqa: BLE001
|
|
_logger.warning("Could not read _get_chart_template_data(%s): %s", code, exc)
|
|
tdata = {}
|
|
|
|
candidate_values = []
|
|
|
|
template_data = tdata.get("template_data", {}) if tdata else {}
|
|
for key, value in template_data.items():
|
|
if key in prop_fields or key.startswith("property_"):
|
|
candidate_values.append(value)
|
|
|
|
# additional_properties live inside the res.company template values
|
|
res_company = tdata.get("res.company", {}) if tdata else {}
|
|
for _company_key, company_vals in res_company.items():
|
|
if not isinstance(company_vals, dict):
|
|
continue
|
|
add_props = company_vals.get("additional_properties")
|
|
if isinstance(add_props, dict):
|
|
candidate_values.extend(add_props.values())
|
|
|
|
# Keep only plain (non-dotted) xml-ids that are actual accounts of the template
|
|
for value in candidate_values:
|
|
if isinstance(value, str) and value and "." not in value and value in accounts_csv:
|
|
xmlids.add(value)
|
|
|
|
return xmlids
|
|
|
|
|
|
def _find_best_account(env, company, target_code):
|
|
"""Return the best account record for a template account code, or None.
|
|
|
|
1. exact code match for the company;
|
|
2. otherwise the company account whose code shares the longest common prefix
|
|
with target_code; ties are broken by smallest code (deterministic).
|
|
3. otherwise None.
|
|
|
|
The exact fallback account matters little functionally: it is only used as a
|
|
journal's default_account_id / an ir.default, which stays user-overridable.
|
|
The goal is to provide a valid account of the right family so that
|
|
try_loading() no longer crashes on a missing external id.
|
|
"""
|
|
if not target_code:
|
|
return None
|
|
|
|
Account = env["account.account"]
|
|
company_domain = [("company_ids", "in", company.id)]
|
|
|
|
exact = Account.search(company_domain + [("code", "=", target_code)], limit=1)
|
|
if exact:
|
|
return exact
|
|
|
|
# Longest common prefix fallback; ties broken by smallest code.
|
|
candidates = Account.search(company_domain)
|
|
best = None
|
|
best_key = None # (prefix_len, negated-code-for-min) -> maximize
|
|
for acc in candidates:
|
|
code = acc.code or ""
|
|
common = 0
|
|
for a, b in zip(code, target_code):
|
|
if a != b:
|
|
break
|
|
common += 1
|
|
if common == 0:
|
|
continue
|
|
key = (common, tuple(-ord(c) for c in code)) # longer prefix, then smallest code
|
|
if best_key is None or key > best_key:
|
|
best = acc
|
|
best_key = key
|
|
return best
|
|
|
|
|
|
@openupgrade.migrate()
|
|
def migrate(env, version):
|
|
template = env["account.chart.template"]
|
|
companies = env["res.company"].search([("chart_template", "!=", False)])
|
|
|
|
created = 0
|
|
for company in companies:
|
|
code = company.chart_template
|
|
tmpl = template.with_company(company)
|
|
|
|
try:
|
|
accounts_csv = tmpl._get_account_account(code)
|
|
except Exception as exc: # noqa: BLE001
|
|
_logger.warning(
|
|
"Company %s (%s): cannot read template accounts: %s",
|
|
company.id, code, exc,
|
|
)
|
|
continue
|
|
|
|
expected = _collect_property_account_xmlids(tmpl, code, accounts_csv)
|
|
|
|
for tmpl_xmlid in sorted(expected):
|
|
full = f"account.{company.id}_{tmpl_xmlid}"
|
|
if env.ref(full, raise_if_not_found=False):
|
|
continue
|
|
if company.parent_ids:
|
|
parent_full = (
|
|
f"account.{company.parent_ids[0].id}_{tmpl_xmlid}"
|
|
)
|
|
if env.ref(parent_full, raise_if_not_found=False):
|
|
continue
|
|
|
|
target_code = accounts_csv.get(tmpl_xmlid, {}).get("code")
|
|
account = _find_best_account(env, company, target_code)
|
|
if not account:
|
|
_logger.warning(
|
|
"Company %s: no account found for template xml-id %s "
|
|
"(code %s); skipping",
|
|
company.id, tmpl_xmlid, target_code,
|
|
)
|
|
continue
|
|
|
|
env["ir.model.data"].create({
|
|
"module": "account",
|
|
"name": f"{company.id}_{tmpl_xmlid}",
|
|
"model": "account.account",
|
|
"res_id": account.id,
|
|
"noupdate": True,
|
|
})
|
|
created += 1
|
|
_logger.info(
|
|
"Company %s: created xml-id %s -> account %s (%s) for "
|
|
"template code %s",
|
|
company.id, full, account.id, account.code, target_code,
|
|
)
|
|
|
|
_logger.info(
|
|
"[l10n_fr_account] property-account xml-id fix: %s xml-id(s) created",
|
|
created,
|
|
)
|