diff --git a/versions/18.0/pre_upgrade.sh b/versions/18.0/pre_upgrade.sh index 05ae24c..2f63d87 100755 --- a/versions/18.0/pre_upgrade.sh +++ b/versions/18.0/pre_upgrade.sh @@ -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._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 diff --git a/versions/18.0/scripts/l10n_fr_account/18.0.2.2/post-migration-0k-fix-property-account-xmlids.py b/versions/18.0/scripts/l10n_fr_account/18.0.2.2/post-migration-0k-fix-property-account-xmlids.py new file mode 100644 index 0000000..296c9fa --- /dev/null +++ b/versions/18.0/scripts/l10n_fr_account/18.0.2.2/post-migration-0k-fix-property-account-xmlids.py @@ -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._" 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._" 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, + )