[FIX] l10n_fr_account: recreate missing property-account xml-ids in 18.0
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.
This commit is contained in:
@@ -131,6 +131,13 @@ EOF
|
||||
echo "SQL command = $PRE_MIGRATE_SQL"
|
||||
query_postgres_container "$PRE_MIGRATE_SQL" ou18 || exit 1
|
||||
|
||||
# NOTE: Missing generic PCG account xml-ids expected by l10n_fr_account (e.g.
|
||||
# account.<company>_pcg_607_account / _pcg_707_account) are recreated generically
|
||||
# by the ORM post-migration script
|
||||
# versions/18.0/scripts/l10n_fr_account/18.0.2.2/post-migration-0k-fix-property-account-xmlids.py
|
||||
# which runs at stage "post" of l10n_fr_account, before the native "end" script
|
||||
# end-migrate_update_taxes.py that would otherwise crash on the missing xml-id.
|
||||
|
||||
# Copy filestores
|
||||
copy_filestore ou17 ou17 ou18 ou18 || exit 1
|
||||
|
||||
|
||||
@@ -0,0 +1,195 @@
|
||||
# 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,
|
||||
)
|
||||
Reference in New Issue
Block a user