Compare commits

..

14 Commits

Author SHA1 Message Date
Stéphan Sainléger
f6baf4d1a2 [FIX] finalize_db: detach `ir_filters pointing to deleted ir_actions`
OpenUpgrade migrations can delete and recreate ``ir_actions`` records with
new ids, leaving ``ir_filters.action_id`` (custom user filters) pointing to
a now-missing action. Opening the custom filters list then fails with
"Record does not exist or has been deleted (ir.actions.actions(<id>,))"
when Odoo resolves the ``action_id`` many2one ``display_name``.

Detach such filters (set ``action_id = NULL``) as a final, version-agnostic
cleanup step: the filter stays usable, only the broken action link is
dropped. Placed in ``finalize_db.sh`` so the check runs whatever the target
version, with an integrated re-count that warns if orphans remain.
2026-07-07 22:06:58 +02:00
Stéphan Sainléger
81e036cd4a [FIX] finalize_db: only reset `to upgrade modules, not to install`
The pre-shell state reset in ``scripts/finalize_db.sh`` forced both
``to upgrade`` and ``to install`` modules to ``installed``. Forcing
``to install`` to ``installed`` makes Odoo skip their install scripts
entirely (tables, ``noupdate`` data, init hooks never run), leaving
ghost "installed but empty" modules that the final ``-u all`` cannot
recover.

Restrict the reset to ``to upgrade`` only, whose real update is honored
by the controlled ``-u all`` at the end of the script. Also drop the
dead ``NOT IN (... state = 'uninstalled')`` sub-query: ``name`` is
unique so a module never has two states, making the filter a no-op.

A commented-out ``SELECT`` is added to trace pending-upgrade modules if
the trailing ``-u all`` is ever removed.
2026-07-03 23:06:12 +02:00
Stéphan Sainléger
aa48543a12 [FIX] remove useless and destructive website template cleaning 2026-07-03 22:47:32 +02:00
Stéphan Sainléger
9cbb5eaff2 [IMP] add project_deadline in "merged in core" modules in 15.0 2026-07-03 22:43:16 +02:00
Stéphan Sainléger
ef75d4a5b1 [IMP] add migration log file
to store all the migration logs. File reset at each migration.
2026-06-05 22:31:39 +02:00
Stéphan Sainléger
d2c4ec6de5 [IMP] add --resume-from option
to resume the migration process from a step wehre the database and
filestore are correct.
2026-06-05 21:13:05 +02:00
Stéphan Sainléger
e27c309d06 [IMP] add hr_expense_report_merge_attachment in "merged in core" modules in 18.0 2026-06-05 21:13:05 +02:00
Stéphan Sainléger
8ea746a4d1 [IMP] delete res.settings views in 18.0 pre-update to rebuild them latter 2026-06-05 21:13:05 +02:00
Stéphan Sainléger
778730e396 [IMP] manage the sequence issues in stock picking and pos orders in v18 post-upgrade 2026-06-05 21:13:05 +02:00
Stéphan Sainléger
969414e649 [IMP] detect missing pos.order hashes and regenerate them 2026-05-28 11:45:18 +02:00
Stéphan Sainléger
f9d678508b [IMP] add Odoo addons known changes 2026-05-28 11:45:18 +02:00
Stéphan Sainléger
55218946c5 [NEW] add detection of obsolete, Odoo core integrated or renamed addons 2026-05-28 11:45:18 +02:00
Stéphan Sainléger
25fff3da5d [IMP] add correction on company dependent fields not migrated by OpenUpgrade 2026-05-15 21:52:52 +02:00
Stéphan Sainléger
c59439aa8d [IMP] add migrations script to use bank-payment-alternative addons in 18.0 2026-05-15 21:52:52 +02:00
15 changed files with 8 additions and 1247 deletions

2
.gitignore vendored
View File

@@ -1,4 +1,2 @@
final_404_addons final_404_addons
migration.log migration.log
__pycache__/
*.pyc

View File

@@ -39,11 +39,10 @@ cd 0k-odoo-upgrade
├── lib/ ├── lib/
│ ├── common.sh # Shared bash functions │ ├── common.sh # Shared bash functions
│ └── python/ # Python utility scripts │ └── python/ # Python utility scripts
│ ├── check_views.py # View analysis (pre-migration) │ ├── check_views.py # View analysis (pre-migration)
│ ├── validate_views.py # View validation (post-migration) │ ├── validate_views.py # View validation (post-migration)
│ ├── fix_duplicated_views.py # Fix duplicated views │ ├── fix_duplicated_views.py # Fix duplicated views
── dedup_unique_constraints.py # Dedup soon-to-be-unique fields (pre-migration) ── cleanup_modules.py # Obsolete module cleanup
│ └── cleanup_modules.py # Obsolete module cleanup
├── scripts/ ├── scripts/
│ ├── prepare_db.sh # Database preparation before migration │ ├── prepare_db.sh # Database preparation before migration
@@ -85,8 +84,6 @@ The script performs a **step-by-step migration** between each major version. For
3. **Database Preparation** (`scripts/prepare_db.sh`) 3. **Database Preparation** (`scripts/prepare_db.sh`)
- Neutralization: disable mail servers and cron jobs - Neutralization: disable mail servers and cron jobs
- Detection of installed modules missing in the target version - Detection of installed modules missing in the target version
- Deduplication of fields that become `UNIQUE` in a traversed version
(see [Unique Constraints](#unique-constraints-new_unique_constraints))
- View state verification - View state verification
- User confirmation prompt - User confirmation prompt
@@ -359,48 +356,6 @@ Version scripts have access to functions defined in `lib/common.sh`:
| `log_info`, `log_warn`, `log_error` | Logging functions | | `log_info`, `log_warn`, `log_error` | Logging functions |
| `log_step "title"` | Display a section header | | `log_step "title"` | Display a section header |
### Unique Constraints (`new_unique_constraints`)
Some Odoo versions add a `UNIQUE` constraint on a field that previously allowed
duplicates. A well-known case is `utm.source.name` in 16.0. When the migration
rebuilds the model, PostgreSQL tries to create the unique index and aborts the
whole registry load if the legacy data contains duplicates:
```
psycopg2.errors.UniqueViolation: could not create unique index "utm_source_unique_name"
DETAIL: Key (name)=(Webinaire gouvernance (copie)) is duplicated.
```
To prevent this, declare the newly-introduced unique constraints in the
`known_changes.yaml` of the version that introduces them, under the
`new_unique_constraints` key. Only `model` and `fields` are needed — the table,
the column type and the translatable (jsonb) flag are resolved automatically
from the ORM:
```yaml
new_unique_constraints:
- model: utm.source
fields: [name]
- model: utm.medium
fields: [name]
- model: utm.campaign
fields: [name]
```
During `prepare_db.sh`, the `dedup_unique_constraints.py` checker aggregates the
declarations of every traversed version and, on the **source** database:
- detects duplicate groups (for translatable/jsonb fields it compares the
indexed `en_US` value, not the raw jsonb);
- keeps the record with the smallest `id` untouched and renames each other
duplicate by appending ` [<id>]` to every language key (jsonb) or to the
plain value (varchar) — all foreign keys are preserved;
- writes a timestamped report to
`/tmp/dedup_unique_constraints_<source_db>_<timestamp>.json`.
Renamed records are summarized at the end of `migration.log` (see the final
`UPGRADE PROCESS ENDED WITH SUCCESS` section).
### Adding a New Version ### Adding a New Version
To add support for a new version (e.g., 19.0): To add support for a new version (e.g., 19.0):
@@ -413,8 +368,6 @@ cp versions/18.0/*.sh versions/19.0/
# - Change references from ou18 → ou19 # - Change references from ou18 → ou19
# - Change the port from -p 8018:8069 → -p 8019:8069 # - Change the port from -p 8018:8069 → -p 8019:8069
# - Add SQL fixes specific to this migration # - Add SQL fixes specific to this migration
# - Declare any new UNIQUE constraints in versions/19.0/known_changes.yaml
# under `new_unique_constraints` (see "Unique Constraints" above)
``` ```
## Troubleshooting ## Troubleshooting

View File

@@ -82,9 +82,6 @@ ou18:
- "--log-level=debug" - "--log-level=debug"
- "--limit-time-cpu=1000000" - "--limit-time-cpu=1000000"
- "--limit-time-real=1000000" - "--limit-time-real=1000000"
volumes:
- /home/stephan/dev/Odoo/0k-odoo-upgrade/versions/18.0/addons/:/opt/odoo/auto/mig:rw
- /home/stephan/dev/Odoo/0k-odoo-upgrade/versions/18.0/scripts/:/opt/odoo/auto/upgrade:rw
options: options:
workers: 0 workers: 0

View File

@@ -100,27 +100,6 @@ exec_python_script_in_odoo_shell() {
run_compose --debug run "$service_name" shell -d "$db_name" --no-http --stop-after-init < "$python_script" run_compose --debug run "$service_name" shell -d "$db_name" --no-http --stop-after-init < "$python_script"
} }
# Same as exec_python_script_in_odoo_shell but prepends a Python preamble read
# from stdin (heredoc). Useful to inject environment values into the Odoo shell
# when the compose wrapper does not reliably propagate host env vars into the
# container. The preamble runs before the script body in the same interpreter.
#
# Usage:
# exec_python_script_in_odoo_shell_with_preamble svc db /path/script.py <<'PY'
# import os; os.environ['FOO'] = 'bar'
# PY
exec_python_script_in_odoo_shell_with_preamble() {
local service_name="$1"
local db_name="$2"
local python_script="$3"
local preamble
preamble="$(cat)"
{ printf '%s\n' "$preamble"; cat "$python_script"; } \
| run_compose --debug run "$service_name" shell -d "$db_name" --no-http --stop-after-init
}
# Classifies missing modules into 4 categories based on the known_changes.yaml # Classifies missing modules into 4 categories based on the known_changes.yaml
# files from each traversed version (from ORIGIN_VERSION+1 to FINAL_VERSION). # files from each traversed version (from ORIGIN_VERSION+1 to FINAL_VERSION).
# The following global arrays are populated: # The following global arrays are populated:
@@ -196,50 +175,8 @@ classify_missing_addons() {
done done
} }
# Aggregates the `new_unique_constraints` sections from the known_changes.yaml
# files of every traversed version (from ORIGIN_VERSION+1 to FINAL_VERSION) into
# a single JSON array printed on stdout, e.g.:
# [{"model":"utm.source","fields":["name"]},{"model":"utm.medium","fields":["name"]}]
# Duplicate (model, fields) entries are collapsed. Prints "[]" when none.
#
# Prerequisites: ORIGIN_VERSION and FINAL_VERSION must be exported.
collect_new_unique_constraints() {
local versions_path="${PROJECT_ROOT}/versions"
local v
local -a json_parts=()
local -A seen=()
for v in $(seq $((ORIGIN_VERSION + 1)) "$FINAL_VERSION"); do
local yaml_file="${versions_path}/${v}.0/known_changes.yaml"
[[ -f "$yaml_file" ]] || continue
local count
count=$(yq '.new_unique_constraints | length' "$yaml_file" 2>/dev/null)
[[ "$count" =~ ^[0-9]+$ && "$count" -gt 0 ]] || continue
local i
for ((i = 0; i < count; i++)); do
local model fields entry
model=$(yq -o=json ".new_unique_constraints[$i].model" "$yaml_file" 2>/dev/null)
fields=$(yq -o=json -I=0 ".new_unique_constraints[$i].fields" "$yaml_file" 2>/dev/null)
[[ -n "$model" && "$model" != "null" ]] || continue
[[ -n "$fields" && "$fields" != "null" ]] || continue
entry="{\"model\":${model},\"fields\":${fields}}"
if [[ -z "${seen[$entry]:-}" ]]; then
seen["$entry"]=1
json_parts+=("$entry")
fi
done
done
local IFS=','
printf '[%s]' "${json_parts[*]}"
}
export PROJECT_ROOT DATASTORE_PATH FILESTORE_SUBPATH export PROJECT_ROOT DATASTORE_PATH FILESTORE_SUBPATH
export -f log_info log_warn log_error log_step confirm_or_exit export -f log_info log_warn log_error log_step confirm_or_exit
export -f check_required_commands export -f check_required_commands
export -f query_postgres_container copy_database copy_filestore run_compose exec_python_script_in_odoo_shell export -f query_postgres_container copy_database copy_filestore run_compose exec_python_script_in_odoo_shell
export -f exec_python_script_in_odoo_shell_with_preamble
export -f classify_missing_addons export -f classify_missing_addons
export -f collect_new_unique_constraints

View File

@@ -1,304 +0,0 @@
#!/usr/bin/env python3
"""
Pre-Migration Unique Constraint Deduplication Script for Odoo
Some Odoo versions introduce new UNIQUE constraints on fields that previously
allowed duplicates (e.g. utm.source.name in 16.0). When the migration rebuilds
the model, the unique index creation raises
`psycopg2.errors.UniqueViolation: could not create unique index ...`
and aborts the whole registry load.
This script runs BEFORE the migration loop, on the SOURCE database, and renames
duplicate records so the target-version unique index can be built successfully.
For each declared constraint (model + fields), it:
- resolves the table, column type and translatable flag from the ORM;
- detects duplicate groups (for translatable/jsonb fields the comparison is
done on the value actually indexed by Odoo, i.e. the `en_US` key);
- keeps the record with the smallest id untouched and renames every other
duplicate by appending ` [<id>]` to ALL language keys (jsonb) or to the
plain value (varchar), preserving all foreign keys.
Constraints to check are passed as JSON via the DEDUP_UNIQUE_CONSTRAINTS
environment variable, e.g.:
[{"model": "utm.source", "fields": ["name"]}, ...]
A machine-readable report of every rename is written to
/tmp/dedup_unique_constraints_<db>_<timestamp>.json when DEDUP_REPORT is set.
Usage:
compose run <service> shell -d <db> --no-http --stop-after-init \
< dedup_unique_constraints.py
Exit codes:
0 - Completed (duplicates, if any, were deduplicated)
2 - Technical failure (see traceback)
"""
import os
import sys
import json
from datetime import datetime
from collections import defaultdict
class Colors:
RED = '\033[91m'
GREEN = '\033[92m'
YELLOW = '\033[93m'
BLUE = '\033[94m'
BOLD = '\033[1m'
END = '\033[0m'
def _c(color, text):
return f"{color}{text}{Colors.END}"
def load_constraints():
"""Load the constraints catalog from the environment."""
raw = os.environ.get('DEDUP_UNIQUE_CONSTRAINTS', '').strip()
if not raw:
return []
try:
data = json.loads(raw)
except json.JSONDecodeError as exc:
raise ValueError(f"DEDUP_UNIQUE_CONSTRAINTS is not valid JSON: {exc}")
if not isinstance(data, list):
raise ValueError("DEDUP_UNIQUE_CONSTRAINTS must be a JSON list")
return data
def resolve_constraint(env, spec):
"""Resolve a {model, fields} spec into concrete DB metadata.
Returns a dict with table, list of (field_name, column, is_jsonb) or
None if the model/fields are not present in this database (e.g. module
not installed in the source version).
"""
model_name = spec.get('model')
field_names = spec.get('fields') or []
if not model_name or not field_names:
return None
if model_name not in env:
return None
model = env[model_name]
table = model._table
# The physical table must exist in the source DB.
env.cr.execute("SELECT to_regclass(%s)", (table,))
if not env.cr.fetchone()[0]:
return None
columns = []
for fname in field_names:
field = model._fields.get(fname)
if field is None:
return None
column = field.name
# Column must physically exist (skip non-stored/computed fields).
# We rely on the ACTUAL PostgreSQL column type, not the ORM `translate`
# attribute: in the source version a translatable field may still be a
# plain `character varying` column (it only becomes `jsonb` during the
# migration to the version that introduces translation storage).
env.cr.execute(
"""
SELECT data_type FROM information_schema.columns
WHERE table_name = %s AND column_name = %s
""",
(table, column),
)
row = env.cr.fetchone()
if not row:
return None
data_type = row[0]
columns.append({
'field': fname,
'column': column,
'is_jsonb': data_type == 'jsonb',
})
return {'model': model_name, 'table': table, 'columns': columns}
def key_sql(column, is_jsonb):
"""SQL expression yielding the value Odoo compares for the unique index.
For jsonb (translated) columns Odoo indexes the value; the semantically
relevant duplicate is on the `en_US` key. For plain columns, the value.
"""
if is_jsonb:
return f"({column} ->> 'en_US')"
return column
def find_duplicate_groups(env, meta):
"""Return duplicate groups as list of dicts: {value, ids (sorted)}."""
table = meta['table']
key_exprs = [key_sql(c['column'], c['is_jsonb']) for c in meta['columns']]
key_concat = " || '\x1f' || ".join(f"COALESCE({e}, '')" for e in key_exprs)
query = f"""
SELECT {key_concat} AS dup_key, array_agg(id ORDER BY id) AS ids
FROM {table}
GROUP BY {key_concat}
HAVING COUNT(*) > 1
"""
env.cr.execute(query)
groups = []
for dup_key, ids in env.cr.fetchall():
groups.append({'value': dup_key, 'ids': list(ids)})
return groups
def rename_record(env, meta, record_id):
"""Suffix ` [<id>]` on every language key (jsonb) / plain value (varchar).
Returns a list of {field, old_value, new_value} describing the change.
"""
table = meta['table']
changes = []
suffix = f" [{record_id}]"
for col in meta['columns']:
column = col['column']
env.cr.execute(
f"SELECT {column} FROM {table} WHERE id = %s", (record_id,)
)
row = env.cr.fetchone()
if not row:
continue
old_value = row[0]
if col['is_jsonb']:
# jsonb: append suffix to each language value
if not isinstance(old_value, dict):
# psycopg may already decode jsonb to dict; if not, load it.
try:
old_value = json.loads(old_value) if old_value else {}
except (TypeError, ValueError):
old_value = {}
new_value = {
lang: (val or '') + suffix for lang, val in old_value.items()
}
env.cr.execute(
f"UPDATE {table} SET {column} = %s::jsonb WHERE id = %s",
(json.dumps(new_value), record_id),
)
else:
new_value = (old_value or '') + suffix
env.cr.execute(
f"UPDATE {table} SET {column} = %s WHERE id = %s",
(new_value, record_id),
)
changes.append({
'field': col['field'],
'old_value': old_value,
'new_value': new_value,
})
return changes
def main():
print("\n" + "=" * 80)
print(_c(Colors.BOLD, "PRE-MIGRATION - UNIQUE CONSTRAINT DEDUPLICATION"))
print("=" * 80 + "\n")
try:
specs = load_constraints()
except ValueError as exc:
print(_c(Colors.RED, f"[ERROR] {exc}"))
sys.exit(2)
if not specs:
print(_c(Colors.YELLOW, "No unique constraints declared. Nothing to do."))
return
renamed_records = [] # flat report entries
checked = 0
skipped = []
for spec in specs:
meta = resolve_constraint(env, spec)
label = f"{spec.get('model')}({', '.join(spec.get('fields', []))})"
if meta is None:
skipped.append(label)
print(_c(Colors.YELLOW, f"[SKIP] {label} - not present in this database"))
continue
checked += 1
groups = find_duplicate_groups(env, meta)
if not groups:
print(_c(Colors.GREEN, f"[OK] {label} - no duplicates"))
continue
total_dups = sum(len(g['ids']) - 1 for g in groups)
print(_c(Colors.BOLD,
f"[FIX] {label} - {len(groups)} duplicate group(s), "
f"{total_dups} record(s) to rename"))
for group in groups:
ids = group['ids']
keep_id, dup_ids = ids[0], ids[1:]
print(f" value={group['value']!r} keep id={keep_id} "
f"rename ids={dup_ids}")
for dup_id in dup_ids:
changes = rename_record(env, meta, dup_id)
for ch in changes:
renamed_records.append({
'model': meta['model'],
'table': meta['table'],
'id': dup_id,
'field': ch['field'],
'old_value': ch['old_value'],
'new_value': ch['new_value'],
})
if renamed_records:
env.cr.commit()
# ---- Summary ---------------------------------------------------------
print("\n" + "=" * 80)
print(_c(Colors.BOLD, "DEDUPLICATION SUMMARY"))
print("=" * 80)
print(f" Constraints checked ... {checked}")
print(f" Constraints skipped ... {len(skipped)}")
print(f" Records renamed ....... {len(renamed_records)}")
if renamed_records:
print("")
for r in renamed_records:
old_display = r['old_value']
new_display = r['new_value']
print(f" - {r['model']} #{r['id']} [{r['field']}]: "
f"{old_display!r} -> {new_display!r}")
print("=" * 80 + "\n")
# ---- JSON report -----------------------------------------------------
if os.environ.get('DEDUP_REPORT') and renamed_records:
ts = datetime.now().strftime('%Y%m%d_%H%M%S')
db = env.cr.dbname
path = f"/tmp/dedup_unique_constraints_{db}_{ts}.json"
report = {
'type': 'dedup_unique_constraints',
'timestamp': datetime.now().isoformat(),
'database': db,
'constraints_checked': checked,
'constraints_skipped': skipped,
'renamed_count': len(renamed_records),
'renamed': renamed_records,
}
with open(path, 'w') as fh:
json.dump(report, fh, indent=2, default=str)
print(_c(Colors.BLUE, f"[REPORT] JSON written to {path}"))
try:
main()
except Exception as exc: # noqa: BLE001
print(_c(Colors.RED, f"[ERROR] Deduplication script failed: {exc}"))
import traceback
traceback.print_exc()
sys.exit(2)

View File

@@ -87,30 +87,6 @@ else
fi fi
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
#############################################
## Deduplicate soon-to-be-unique fields ##
#############################################
# Some target versions add UNIQUE constraints on fields that allowed duplicates
# in the source version (e.g. utm.source.name in 16.0). Left untouched, the
# unique index creation aborts the migration with a UniqueViolation. We rename
# duplicates in the source DB now, before the migration loop.
log_step "UNIQUE CONSTRAINTS DEDUP"
UNIQUE_CONSTRAINTS_JSON=$(collect_new_unique_constraints)
if [[ "$UNIQUE_CONSTRAINTS_JSON" == "[]" || -z "$UNIQUE_CONSTRAINTS_JSON" ]]; then
log_info "No new unique constraints declared for the traversed versions — skipping."
else
log_info "Checking unique constraints: $UNIQUE_CONSTRAINTS_JSON"
DEDUP_SCRIPT="${SCRIPT_DIR}/lib/python/dedup_unique_constraints.py"
# Inject the catalog via a Python preamble because the compose wrapper does
# not reliably propagate host environment variables into the container.
exec_python_script_in_odoo_shell_with_preamble "$DB_NAME" "$DB_NAME" "$DEDUP_SCRIPT" <<PY
import os
os.environ['DEDUP_UNIQUE_CONSTRAINTS'] = ${UNIQUE_CONSTRAINTS_JSON@Q}
os.environ['DEDUP_REPORT'] = '1'
PY
fi
PYTHON_SCRIPT="${SCRIPT_DIR}/lib/python/check_views.py" PYTHON_SCRIPT="${SCRIPT_DIR}/lib/python/check_views.py"
echo "Check views with script $PYTHON_SCRIPT ..." echo "Check views with script $PYTHON_SCRIPT ..."
exec_python_script_in_odoo_shell "$DB_NAME" "$DB_NAME" "$PYTHON_SCRIPT" exec_python_script_in_odoo_shell "$DB_NAME" "$DB_NAME" "$PYTHON_SCRIPT"

View File

@@ -232,20 +232,4 @@ log_step "POST-UPGRADE PROCESSES"
"${SCRIPT_DIR}/scripts/finalize_db.sh" "$FINALE_DB_NAME" "$FINALE_SERVICE_NAME" "${SCRIPT_DIR}/scripts/finalize_db.sh" "$FINALE_DB_NAME" "$FINALE_SERVICE_NAME"
log_step "UPGRADE PROCESS ENDED WITH SUCCESS" log_step "UPGRADE PROCESS ENDED WITH SUCCESS"
# Report any records that were renamed to satisfy new unique constraints.
# The deduplication runs on the source DB (ou${ORIGIN_VERSION}) during
# prepare_db.sh and writes a timestamped JSON report to /tmp.
readarray -t dedup_reports < <(ls -1t /tmp/dedup_unique_constraints_"${COPY_DB_NAME}"_*.json 2>/dev/null || true)
if [[ ${#dedup_reports[@]} -gt 0 ]]; then
latest_report="${dedup_reports[0]}"
renamed_count=$(yq -p=json '.renamed_count' "$latest_report" 2>/dev/null || echo "?")
log_info "Unique-constraint deduplication: ${renamed_count} record(s) renamed in the source DB before migration."
log_info "Details: ${latest_report}"
if [[ "$renamed_count" =~ ^[0-9]+$ && "$renamed_count" -gt 0 ]]; then
yq -p=json -r '.renamed[] | " - \(.model) #\(.id) [\(.field)]: \(.old_value) -> \(.new_value)"' \
"$latest_report" 2>/dev/null || true
fi
fi
log_info "Full logs available at: ${LOG_FILE}" log_info "Full logs available at: ${LOG_FILE}"

View File

@@ -21,12 +21,3 @@ renamed:
new: server_action_mass_edit new: server_action_mass_edit
- old: crm_project - old: crm_project
new: crm_lead_to_task new: crm_lead_to_task
# Unique constraints newly introduced in 16.0
new_unique_constraints:
- model: utm.source
fields: [name]
- model: utm.medium
fields: [name]
- model: utm.campaign
fields: [name]

View File

@@ -51,58 +51,6 @@ EOF
echo "SQL command = $PRE_MIGRATE_SQL_3" echo "SQL command = $PRE_MIGRATE_SQL_3"
query_postgres_container "$PRE_MIGRATE_SQL_3" ou17 || exit 1 query_postgres_container "$PRE_MIGRATE_SQL_3" ou17 || exit 1
# ────────────────────────────────────────────────────────────
# Remove project_list's ir.actions.act_window.view records
#
# `project_list` (OCA) is not ported to 17.0 and is classified as
# merged_in_core: in 17.0 the core `project` module natively adds the
# kanban/tree views to the "Projects" actions (act_window_id 149 & 760).
# 17.0 also introduces the unique constraint
# `act_window_view_unique_mode_per_action = unique(act_window_id, view_mode)`.
#
# When core reloads project_project_views.xml, it INSERTs its own
# (act_window_id, view_mode='kanban') rows, which collide with the rows still
# owned by project_list -> UniqueViolation, aborting the registry load:
#
# ERROR: duplicate key value violates unique constraint
# "act_window_view_unique_mode_per_action"
# DETAIL: Key (act_window_id, view_mode)=(760, kanban) already exists.
#
# We delete project_list's act_window_view rows (and their ir_model_data) so
# core can recreate its canonical rows. Verified against the 17.0 OpenUpgrade
# scripts: none reference project_list xmlids nor remap these records, so this
# is safe. Scoped to ir.actions.act_window.view only. Idempotent.
# ────────────────────────────────────────────────────────────
PRE_MIGRATE_SQL_4=$(cat <<'EOF'
DO $$
DECLARE
deleted_count INTEGER;
BEGIN
WITH targets AS (
SELECT d.id AS imd_id, d.res_id AS awv_id
FROM ir_model_data d
WHERE d.module = 'project_list'
AND d.model = 'ir.actions.act_window.view'
),
del_views AS (
DELETE FROM ir_act_window_view
WHERE id IN (SELECT awv_id FROM targets)
RETURNING id
),
del_imd AS (
DELETE FROM ir_model_data
WHERE id IN (SELECT imd_id FROM targets)
RETURNING id
)
SELECT count(*) INTO deleted_count FROM del_views;
RAISE NOTICE 'Removed % project_list act_window_view record(s) to avoid act_window_view_unique_mode_per_action collision', deleted_count;
END $$;
EOF
)
echo "SQL command = $PRE_MIGRATE_SQL_4"
query_postgres_container "$PRE_MIGRATE_SQL_4" ou17 || exit 1
# Copy filestores # Copy filestores
copy_filestore ou16 ou16 ou17 ou17 || exit 1 copy_filestore ou16 ou16 ou17 ou17 || exit 1

View File

@@ -121,6 +121,9 @@ EOF
echo "Fixing company-dependent columns for Odoo 18..." echo "Fixing company-dependent columns for Odoo 18..."
query_postgres_container "$COMPANY_DEPENDENT_FIX_SQL" ou18 || exit 1 query_postgres_container "$COMPANY_DEPENDENT_FIX_SQL" ou18 || exit 1
EOF
)
# Execute SQL pre-migration commands # Execute SQL pre-migration commands
PRE_MIGRATE_SQL=$(cat <<'EOF' PRE_MIGRATE_SQL=$(cat <<'EOF'
UPDATE account_analytic_plan SET default_applicability=NULL WHERE default_applicability='optional'; UPDATE account_analytic_plan SET default_applicability=NULL WHERE default_applicability='optional';
@@ -131,13 +134,6 @@ EOF
echo "SQL command = $PRE_MIGRATE_SQL" echo "SQL command = $PRE_MIGRATE_SQL"
query_postgres_container "$PRE_MIGRATE_SQL" ou18 || exit 1 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 filestores
copy_filestore ou17 ou17 ou18 ou18 || exit 1 copy_filestore ou17 ou17 ou18 ou18 || exit 1

View File

@@ -1,33 +0,0 @@
<?xml version='1.0' encoding='utf-8'?>
<!--
0k / Élabore customization of the French VAT report (l10n_fr_account).
Adds an editable "adjustment" expression on box 25 (VAT credit) and
integrates it into the box 25 balance formula. Historically this lived in a
full copy of l10n_fr_account/data/tax_report_data.xml that shadowed the core
module; it is now applied the OpenUpgrade way, via load_data() from
post-migration.py, without duplicating the core module.
Loaded with mode="init"; records use the module's own xml ids
(l10n_fr_account.*). tax_report_25_formula already exists in core and is
updated here; tax_report_25_adjustment is new and gets created.
-->
<odoo>
<!-- Update the box 25 balance formula to include the adjustment -->
<record id="tax_report_25_formula" model="account.report.expression">
<field name="report_line_id" ref="l10n_fr_account.tax_report_25"/>
<field name="label">balance</field>
<field name="engine">aggregation</field>
<field name="formula">box_23.balance - box_16.balance + box_25.adjustment</field>
<field name="subformula">if_above(EUR(0))</field>
</record>
<!-- New editable external "adjustment" expression on box 25 -->
<record id="tax_report_25_adjustment" model="account.report.expression">
<field name="report_line_id" ref="l10n_fr_account.tax_report_25"/>
<field name="label">adjustment</field>
<field name="engine">external</field>
<field name="formula">sum</field>
<field name="subformula">editable;rounding=0</field>
</record>
</odoo>

View File

@@ -1,195 +0,0 @@
# 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,
)

View File

@@ -1,20 +0,0 @@
# 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). It runs IN ADDITION to the
# native openupgrade_scripts post-migration.py of l10n_fr_account/18.0.2.2.
#
# Loads the 0k customization of the French VAT report (box 25 "adjustment")
# the OpenUpgrade way, without shadowing the core l10n_fr_account module.
from openupgradelib import openupgrade
@openupgrade.migrate()
def migrate(env, version):
# mode="init" (re)creates records marked noupdate that the normal upgrade
# mechanism would skip; it updates existing ones (tax_report_25_formula)
# and creates new ones (tax_report_25_adjustment).
openupgrade.load_data(
env, "l10n_fr_account", "18.0.2.2/noupdate_changes.xml"
)

View File

@@ -1,460 +0,0 @@
# 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). It runs IN ADDITION to the
# native openupgrade_scripts pre-migration.py of l10n_fr_account/18.0.2.2:
# the framework appends every upgrade path, so this script does not override
# nor duplicate the native one.
from openupgradelib import openupgrade
@openupgrade.migrate()
def migrate(env, version):
"""Fix orphan account.report.expression records before XML data loading.
During migration from V17, some account.report.expression records lose
their ir_model_data entries. When l10n_fr_account loads tax_report_data.xml
in V18, it tries to INSERT these records again, violating the unique
constraint account_report_expression_line_label_uniq (report_line_id, label).
Fix 1: Re-insert missing ir_model_data entries so Odoo performs UPDATE
instead of INSERT when loading the XML data.
Fix 2: Delete orphan expressions that no longer exist in V18 XML
(box_X5, box_Y4, box_Y5, box_Z5 now use aggregation_formula field).
"""
cr = env.cr
# (expr_xmlid, line_xmlid, label)
# Generated from l10n_fr_account/data/tax_report_data.xml (18.0)
expressions = [
("tax_report_A1_tag", "tax_report_A1", "balance"),
("tax_report_A1_tag_rounded", "tax_report_A1", "balance_rounded"),
("tax_report_A1_balance_from_tags", "tax_report_A1", "balance_from_tags"),
("tax_report_A1_adjustment", "tax_report_A1", "adjustment"),
("tax_report_A2_tag", "tax_report_A2", "balance"),
("tax_report_A2_tag_rounded", "tax_report_A2", "balance_rounded"),
("tax_report_A2_balance_from_tags", "tax_report_A2", "balance_from_tags"),
("tax_report_A2_adjustment", "tax_report_A2", "adjustment"),
("tax_report_A3_tag", "tax_report_A3", "balance"),
("tax_report_A3_tag_rounded", "tax_report_A3", "balance_rounded"),
("tax_report_A3_balance_from_tags", "tax_report_A3", "balance_from_tags"),
("tax_report_A3_adjustment", "tax_report_A3", "adjustment"),
("tax_report_A4_tag", "tax_report_A4", "balance"),
("tax_report_A4_tag_rounded", "tax_report_A4", "balance_rounded"),
("tax_report_A4_balance_from_tags", "tax_report_A4", "balance_from_tags"),
("tax_report_A4_adjustment", "tax_report_A4", "adjustment"),
("tax_report_A5_tag", "tax_report_A5", "balance"),
("tax_report_A5_tag_rounded", "tax_report_A5", "balance_rounded"),
("tax_report_A5_balance_from_tags", "tax_report_A5", "balance_from_tags"),
("tax_report_A5_adjustment", "tax_report_A5", "adjustment"),
("tax_report_B1_tag", "tax_report_B1", "balance"),
("tax_report_B1_tag_rounded", "tax_report_B1", "balance_rounded"),
("tax_report_B1_balance_from_tags", "tax_report_B1", "balance_from_tags"),
("tax_report_B1_adjustment", "tax_report_B1", "adjustment"),
("tax_report_B2_tag", "tax_report_B2", "balance"),
("tax_report_B2_tag_rounded", "tax_report_B2", "balance_rounded"),
("tax_report_B2_balance_from_tags", "tax_report_B2", "balance_from_tags"),
("tax_report_B2_adjustment", "tax_report_B2", "adjustment"),
("tax_report_B3_tag", "tax_report_B3", "balance"),
("tax_report_B3_tag_rounded", "tax_report_B3", "balance_rounded"),
("tax_report_B3_balance_from_tags", "tax_report_B3", "balance_from_tags"),
("tax_report_B3_adjustment", "tax_report_B3", "adjustment"),
("tax_report_B4_tag", "tax_report_B4", "balance"),
("tax_report_B4_tag_rounded", "tax_report_B4", "balance_rounded"),
("tax_report_B4_balance_from_tags", "tax_report_B4", "balance_from_tags"),
("tax_report_B4_adjustment", "tax_report_B4", "adjustment"),
("tax_report_B5_tag", "tax_report_B5", "balance"),
("tax_report_B5_tag_rounded", "tax_report_B5", "balance_rounded"),
("tax_report_B5_balance_from_tags", "tax_report_B5", "balance_from_tags"),
("tax_report_B5_adjustment", "tax_report_B5", "adjustment"),
("tax_report_E1_tag", "tax_report_E1", "balance"),
("tax_report_E1_tag_rounded", "tax_report_E1", "balance_rounded"),
("tax_report_E1_balance_from_tags", "tax_report_E1", "balance_from_tags"),
("tax_report_E1_adjustment", "tax_report_E1", "adjustment"),
("tax_report_E2_tag", "tax_report_E2", "balance"),
("tax_report_E2_tag_rounded", "tax_report_E2", "balance_rounded"),
("tax_report_E2_balance_from_tags", "tax_report_E2", "balance_from_tags"),
("tax_report_E2_adjustment", "tax_report_E2", "adjustment"),
("tax_report_E3_tag", "tax_report_E3", "balance"),
("tax_report_E3_tag_rounded", "tax_report_E3", "balance_rounded"),
("tax_report_E3_balance_from_tags", "tax_report_E3", "balance_from_tags"),
("tax_report_E3_adjustment", "tax_report_E3", "adjustment"),
("tax_report_E4_tag", "tax_report_E4", "balance"),
("tax_report_E4_tag_rounded", "tax_report_E4", "balance_rounded"),
("tax_report_E4_balance_from_tags", "tax_report_E4", "balance_from_tags"),
("tax_report_E4_adjustment", "tax_report_E4", "adjustment"),
("tax_report_E5_tag", "tax_report_E5", "balance"),
("tax_report_E5_tag_rounded", "tax_report_E5", "balance_rounded"),
("tax_report_E5_balance_from_tags", "tax_report_E5", "balance_from_tags"),
("tax_report_E5_adjustment", "tax_report_E5", "adjustment"),
("tax_report_E6_tag", "tax_report_E6", "balance"),
("tax_report_E6_tag_rounded", "tax_report_E6", "balance_rounded"),
("tax_report_E6_balance_from_tags", "tax_report_E6", "balance_from_tags"),
("tax_report_E6_adjustment", "tax_report_E6", "adjustment"),
("tax_report_F1_tag", "tax_report_F1", "balance"),
("tax_report_F1_tag_rounded", "tax_report_F1", "balance_rounded"),
("tax_report_F1_balance_from_tags", "tax_report_F1", "balance_from_tags"),
("tax_report_F1_adjustment", "tax_report_F1", "adjustment"),
("tax_report_F2_tag", "tax_report_F2", "balance"),
("tax_report_F2_tag_rounded", "tax_report_F2", "balance_rounded"),
("tax_report_F2_balance_from_tags", "tax_report_F2", "balance_from_tags"),
("tax_report_F2_adjustment", "tax_report_F2", "adjustment"),
("tax_report_F3_tag", "tax_report_F3", "balance"),
("tax_report_F3_tag_rounded", "tax_report_F3", "balance_rounded"),
("tax_report_F3_balance_from_tags", "tax_report_F3", "balance_from_tags"),
("tax_report_F3_adjustment", "tax_report_F3", "adjustment"),
("tax_report_F4_tag", "tax_report_F4", "balance"),
("tax_report_F4_tag_rounded", "tax_report_F4", "balance_rounded"),
("tax_report_F4_balance_from_tags", "tax_report_F4", "balance_from_tags"),
("tax_report_F4_adjustment", "tax_report_F4", "adjustment"),
("tax_report_F5_tag", "tax_report_F5", "balance"),
("tax_report_F5_tag_rounded", "tax_report_F5", "balance_rounded"),
("tax_report_F5_balance_from_tags", "tax_report_F5", "balance_from_tags"),
("tax_report_F5_adjustment", "tax_report_F5", "adjustment"),
("tax_report_F6_tag", "tax_report_F6", "balance"),
("tax_report_F6_tag_rounded", "tax_report_F6", "balance_rounded"),
("tax_report_F6_balance_from_tags", "tax_report_F6", "balance_from_tags"),
("tax_report_F6_adjustment", "tax_report_F6", "adjustment"),
("tax_report_F7_tag", "tax_report_F7", "balance"),
("tax_report_F7_tag_rounded", "tax_report_F7", "balance_rounded"),
("tax_report_F7_balance_from_tags", "tax_report_F7", "balance_from_tags"),
("tax_report_F7_adjustment", "tax_report_F7", "adjustment"),
("tax_report_F8_tag", "tax_report_F8", "balance"),
("tax_report_F8_tag_rounded", "tax_report_F8", "balance_rounded"),
("tax_report_F8_balance_from_tags", "tax_report_F8", "balance_from_tags"),
("tax_report_F8_adjustment", "tax_report_F8", "adjustment"),
("tax_report_F9_tag", "tax_report_F9", "balance"),
("tax_report_F9_tag_rounded", "tax_report_F9", "balance_rounded"),
("tax_report_F9_balance_from_tags", "tax_report_F9", "balance_from_tags"),
("tax_report_F9_adjustment", "tax_report_F9", "adjustment"),
("tax_report_08_base_tag", "tax_report_08_base", "balance"),
("tax_report_08_base_tag_rounded", "tax_report_08_base", "balance_rounded"),
("tax_report_08_base_balance_from_tags", "tax_report_08_base", "balance_from_tags"),
("tax_report_08_base_adjustment", "tax_report_08_base", "adjustment"),
("tax_report_08_taxe_tag", "tax_report_08_taxe", "balance"),
("tax_report_08_taxe_tag_rounded", "tax_report_08_taxe", "balance_rounded"),
("tax_report_08_taxe_tag_no_rounding", "tax_report_08_taxe", "balance_from_tags"),
("tax_report_09_base_tag", "tax_report_09_base", "balance"),
("tax_report_09_base_tag_rounded", "tax_report_09_base", "balance_rounded"),
("tax_report_09_base_balance_from_tags", "tax_report_09_base", "balance_from_tags"),
("tax_report_09_base_adjustment", "tax_report_09_base", "adjustment"),
("tax_report_09_taxe_tag", "tax_report_09_taxe", "balance"),
("tax_report_09_taxe_tag_rounded", "tax_report_09_taxe", "balance_rounded"),
("tax_report_09_taxe_tag_no_rounding", "tax_report_09_taxe", "balance_from_tags"),
("tax_report_9B_base_tag", "tax_report_9B_base", "balance"),
("tax_report_9B_base_tag_rounded", "tax_report_9B_base", "balance_rounded"),
("tax_report_9B_base_balance_from_tags", "tax_report_9B_base", "balance_from_tags"),
("tax_report_9B_base_adjustment", "tax_report_9B_base", "adjustment"),
("tax_report_9B_taxe_tag", "tax_report_9B_taxe", "balance"),
("tax_report_9B_taxe_tag_rounded", "tax_report_9B_taxe", "balance_rounded"),
("tax_report_9B_taxe_tag_no_rounding", "tax_report_9B_taxe", "balance_from_tags"),
("tax_report_10_base_tag", "tax_report_10_base", "balance"),
("tax_report_10_base_tag_rounded", "tax_report_10_base", "balance_rounded"),
("tax_report_10_base_balance_from_tags", "tax_report_10_base", "balance_from_tags"),
("tax_report_10_base_adjustment", "tax_report_10_base", "adjustment"),
("tax_report_10_taxe_tag", "tax_report_10_taxe", "balance"),
("tax_report_10_taxe_tag_balance", "tax_report_10_taxe", "balance_rounded"),
("tax_report_10_taxe_tag_no_rounding", "tax_report_10_taxe", "balance_from_tags"),
("tax_report_11_base_tag", "tax_report_11_base", "balance"),
("tax_report_11_base_tag_rounded", "tax_report_11_base", "balance_rounded"),
("tax_report_11_base_balance_from_tags", "tax_report_11_base", "balance_from_tags"),
("tax_report_11_base_adjustment", "tax_report_11_base", "adjustment"),
("tax_report_11_taxe_tag", "tax_report_11_taxe", "balance"),
("tax_report_11_taxe_tag_rounded", "tax_report_11_taxe", "balance_rounded"),
("tax_report_11_taxe_tag_no_rounding", "tax_report_11_taxe", "balance_from_tags"),
("tax_report_T1_base_tag", "tax_report_T1_base", "balance"),
("tax_report_T1_base_tag_rounded", "tax_report_T1_base", "balance_rounded"),
("tax_report_T1_base_balance_from_tags", "tax_report_T1_base", "balance_from_tags"),
("tax_report_T1_base_adjustment", "tax_report_T1_base", "adjustment"),
("tax_report_T1_taxe_tag", "tax_report_T1_taxe", "balance"),
("tax_report_T1_taxe_tag_rounded", "tax_report_T1_taxe", "balance_rounded"),
("tax_report_T1_taxe_tag_no_rounding", "tax_report_T1_taxe", "balance_from_tags"),
("tax_report_T2_base_tag", "tax_report_T2_base", "balance"),
("tax_report_T2_base_tag_rounded", "tax_report_T2_base", "balance_rounded"),
("tax_report_T2_base_balance_from_tags", "tax_report_T2_base", "balance_from_tags"),
("tax_report_T2_base_adjustment", "tax_report_T2_base", "adjustment"),
("tax_report_T2_taxe_tag", "tax_report_T2_taxe", "balance"),
("tax_report_T2_taxe_tag_rounded", "tax_report_T2_taxe", "balance_rounded"),
("tax_report_T2_taxe_tag_no_rounding", "tax_report_T2_taxe", "balance_from_tags"),
("tax_report_T3_base_tag", "tax_report_T3_base", "balance"),
("tax_report_T3_base_tag_rounded", "tax_report_T3_base", "balance_rounded"),
("tax_report_T3_base_balance_from_tags", "tax_report_T3_base", "balance_from_tags"),
("tax_report_T3_base_adjustment", "tax_report_T3_base", "adjustment"),
("tax_report_T3_taxe_tag", "tax_report_T3_taxe", "balance"),
("tax_report_T3_taxe_tag_rounded", "tax_report_T3_taxe", "balance_rounded"),
("tax_report_T3_taxe_tag_no_rounding", "tax_report_T3_taxe", "balance_from_tags"),
("tax_report_T4_base_tag", "tax_report_T4_base", "balance"),
("tax_report_T4_base_tag_rounded", "tax_report_T4_base", "balance_rounded"),
("tax_report_T4_base_balance_from_tags", "tax_report_T4_base", "balance_from_tags"),
("tax_report_T4_base_adjustment", "tax_report_T4_base", "adjustment"),
("tax_report_T4_taxe_tag", "tax_report_T4_taxe", "balance"),
("tax_report_T4_taxe_tag_balance", "tax_report_T4_taxe", "balance_rounded"),
("tax_report_T4_taxe_tag_no_rounding", "tax_report_T4_taxe", "balance_from_tags"),
("tax_report_T5_base_tag", "tax_report_T5_base", "balance"),
("tax_report_T5_base_tag_rounded", "tax_report_T5_base", "balance_rounded"),
("tax_report_T5_base_balance_from_tags", "tax_report_T5_base", "balance_from_tags"),
("tax_report_T5_base_adjustment", "tax_report_T5_base", "adjustment"),
("tax_report_T5_taxe_tag", "tax_report_T5_taxe", "balance"),
("tax_report_T5_taxe_tag_rounded", "tax_report_T5_taxe", "balance_rounded"),
("tax_report_T5_taxe_tag_no_rounding", "tax_report_T5_taxe", "balance_from_tags"),
("tax_report_T6_base_tag", "tax_report_T6_base", "balance"),
("tax_report_T6_base_tag_balance", "tax_report_T6_base", "balance_rounded"),
("tax_report_T6_base_balance_from_tags", "tax_report_T6_base", "balance_from_tags"),
("tax_report_T6_base_adjustment", "tax_report_T6_base", "adjustment"),
("tax_report_T6_taxe_tag", "tax_report_T6_taxe", "balance"),
("tax_report_T6_taxe_tag_balance", "tax_report_T6_taxe", "balance_rounded"),
("tax_report_T6_taxe_tag_no_rounding", "tax_report_T6_taxe", "balance_from_tags"),
("tax_report_T7_base_tag", "tax_report_T7_base", "balance"),
("tax_report_T7_base_tag_rounded", "tax_report_T7_base", "balance_rounded"),
("tax_report_T7_base_balance_from_tags", "tax_report_T7_base", "balance_from_tags"),
("tax_report_T7_base_adjustment", "tax_report_T7_base", "adjustment"),
("tax_report_T7_taxe_tag", "tax_report_T7_taxe", "balance"),
("tax_report_T7_taxe_balance_rounded", "tax_report_T7_taxe", "balance_rounded"),
("tax_report_T7_taxe_balance_from_tags", "tax_report_T7_taxe", "balance_from_tags"),
("tax_report_13_base_tag", "tax_report_13_base", "balance"),
("tax_report_13_base_tag_rounded", "tax_report_13_base", "balance_rounded"),
("tax_report_13_base_balance_from_tags", "tax_report_13_base", "balance_from_tags"),
("tax_report_13_base_adjustment", "tax_report_13_base", "adjustment"),
("tax_report_13_taxe_tag", "tax_report_13_taxe", "balance"),
("tax_report_13_taxe_balance_rounded", "tax_report_13_taxe", "balance_rounded"),
("tax_report_13_taxe_balance_from_tags", "tax_report_13_taxe", "balance_from_tags"),
("tax_report_14_base_tag", "tax_report_14_base", "balance"),
("tax_report_14_base_tag_rounded", "tax_report_14_base", "balance_rounded"),
("tax_report_14_base_balance_from_tags", "tax_report_14_base", "balance_from_tags"),
("tax_report_14_base_adjustment", "tax_report_14_base", "adjustment"),
("tax_report_14_taxe_tag", "tax_report_14_taxe", "balance"),
("tax_report_14_taxe_balance_rounded", "tax_report_14_taxe", "balance_rounded"),
("tax_report_14_taxe_balance_from_tags", "tax_report_14_taxe", "balance_from_tags"),
("tax_report_P1_base_tag", "tax_report_P1_base", "balance"),
("tax_report_P1_base_tag_rounded", "tax_report_P1_base", "balance_rounded"),
("tax_report_P1_base_balance_from_tags", "tax_report_P1_base", "balance_from_tags"),
("tax_report_P1_base_adjustment", "tax_report_P1_base", "adjustment"),
("tax_report_P1_taxe_tag", "tax_report_P1_taxe", "balance"),
("tax_report_P1_taxe_tag_rounded", "tax_report_P1_taxe", "balance_rounded"),
("tax_report_P1_taxe_tag_no_rounding", "tax_report_P1_taxe", "balance_from_tags"),
("tax_report_P2_base_tag", "tax_report_P2_base", "balance"),
("tax_report_P2_base_tag_rounded", "tax_report_P2_base", "balance_rounded"),
("tax_report_P2_base_balance_from_tags", "tax_report_P2_base", "balance_from_tags"),
("tax_report_P2_base_adjustment", "tax_report_P2_base", "adjustment"),
("tax_report_P2_taxe_tag", "tax_report_P2_taxe", "balance"),
("tax_report_P2_taxe_tag_rounded", "tax_report_P2_taxe", "balance_rounded"),
("tax_report_P2_taxe_tag_no_rounding", "tax_report_P2_taxe", "balance_from_tags"),
("tax_report_I1_base_tag", "tax_report_I1_base", "balance"),
("tax_report_I1_base_tag_rounded", "tax_report_I1_base", "balance_rounded"),
("tax_report_I1_base_balance_from_tags", "tax_report_I1_base", "balance_from_tags"),
("tax_report_I1_base_adjustment", "tax_report_I1_base", "adjustment"),
("tax_report_I1_taxe_tag", "tax_report_I1_taxe", "balance"),
("tax_report_I1_taxe_tag_rounded", "tax_report_I1_taxe", "balance_rounded"),
("tax_report_I1_taxe_tag_no_rounding", "tax_report_I1_taxe", "balance_from_tags"),
("tax_report_I2_base_tag", "tax_report_I2_base", "balance"),
("tax_report_I2_base_tag_rounded", "tax_report_I2_base", "balance_rounded"),
("tax_report_I2_base_balance_from_tags", "tax_report_I2_base", "balance_from_tags"),
("tax_report_I2_base_adjustment", "tax_report_I2_base", "adjustment"),
("tax_report_I2_taxe_tag", "tax_report_I2_taxe", "balance"),
("tax_report_I2_taxe_tag_rounded", "tax_report_I2_taxe", "balance_rounded"),
("tax_report_I2_taxe_tag_no_rounding", "tax_report_I2_taxe", "balance_from_tags"),
("tax_report_I3_base_tag", "tax_report_I3_base", "balance"),
("tax_report_I3_base_tag_rounded", "tax_report_I3_base", "balance_rounded"),
("tax_report_I3_base_balance_from_tags", "tax_report_I3_base", "balance_from_tags"),
("tax_report_I3_base_adjustment", "tax_report_I3_base", "adjustment"),
("tax_report_I3_taxe_tag", "tax_report_I3_taxe", "balance"),
("tax_report_I3_taxe_tag_rounded", "tax_report_I3_taxe", "balance_rounded"),
("tax_report_I3_taxe_tag_no_rounding", "tax_report_I3_taxe", "balance_from_tags"),
("tax_report_I4_base_tag", "tax_report_I4_base", "balance"),
("tax_report_I4_base_tag_rounded", "tax_report_I4_base", "balance_rounded"),
("tax_report_I4_base_balance_from_tags", "tax_report_I4_base", "balance_from_tags"),
("tax_report_I4_base_adjustment", "tax_report_I4_base", "adjustment"),
("tax_report_I4_taxe_tag", "tax_report_I4_taxe", "balance"),
("tax_report_I4_taxe_tag_rounded", "tax_report_I4_taxe", "balance_rounded"),
("tax_report_I4_taxe_tag_no_rounding", "tax_report_I4_taxe", "balance_from_tags"),
("tax_report_I5_base_tag", "tax_report_I5_base", "balance"),
("tax_report_I5_base_tag_rounded", "tax_report_I5_base", "balance_rounded"),
("tax_report_I5_base_balance_from_tags", "tax_report_I5_base", "balance_from_tags"),
("tax_report_I5_base_adjustment", "tax_report_I5_base", "adjustment"),
("tax_report_I5_taxe_tag", "tax_report_I5_taxe", "balance"),
("tax_report_I5_taxe_tag_rounded", "tax_report_I5_taxe", "balance_rounded"),
("tax_report_I5_taxe_tag_no_rounding", "tax_report_I5_taxe", "balance_from_tags"),
("tax_report_I6_base_tag", "tax_report_I6_base", "balance"),
("tax_report_I6_base_tag_rounded", "tax_report_I6_base", "balance_rounded"),
("tax_report_I6_base_balance_from_tags", "tax_report_I6_base", "balance_from_tags"),
("tax_report_I6_base_adjustment", "tax_report_I6_base", "adjustment"),
("tax_report_I6_taxe_tag", "tax_report_I6_taxe", "balance"),
("tax_report_I6_taxe_tag_rounded", "tax_report_I6_taxe", "balance_rounded"),
("tax_report_I6_taxe_tag_no_rounding", "tax_report_I6_taxe", "balance_from_tags"),
("tax_report_15_tag", "tax_report_15", "balance"),
("tax_report_15_balance_rounded", "tax_report_15", "balance_rounded"),
("tax_report_15_balance_from_tags", "tax_report_15", "balance_from_tags"),
("tax_report_15_1_tag", "tax_report_15_1", "balance"),
("tax_report_15_1_balance_rounded", "tax_report_15_1", "balance_rounded"),
("tax_report_15_1_balance_from_tags", "tax_report_15_1", "balance_from_tags"),
("tax_report_15_2_tag", "tax_report_15_2", "balance"),
("tax_report_15_2_balance_rounded", "tax_report_15_2", "balance_rounded"),
("tax_report_15_2_balance_from_tags", "tax_report_15_2", "balance_from_tags"),
("tax_report_5B_tag", "tax_report_5B", "balance"),
("tax_report_5B_balance_rounded", "tax_report_5B", "balance_rounded"),
("tax_report_5B_balance_from_tags", "tax_report_5B", "balance_from_tags"),
("tax_report_16_formula", "tax_report_16", "balance"),
("tax_report_17_tag", "tax_report_17", "balance"),
("tax_report_17_balance_rounded", "tax_report_17", "balance_rounded"),
("tax_report_17_balance_from_tags", "tax_report_17", "balance_from_tags"),
("tax_report_18_tag", "tax_report_18", "balance"),
("tax_report_18_balance_rounded", "tax_report_18", "balance_rounded"),
("tax_report_18_balance_from_tags", "tax_report_18", "balance_from_tags"),
("tax_report_19_tag", "tax_report_19", "balance"),
("tax_report_19_balance_rounded", "tax_report_19", "balance_rounded"),
("tax_report_19_balance_from_tags", "tax_report_19", "balance_from_tags"),
("tax_report_20_tag", "tax_report_20", "balance"),
("tax_report_20_balance_rounded", "tax_report_20", "balance_rounded"),
("tax_report_20_balance_from_tags", "tax_report_20", "balance_from_tags"),
("tax_report_21_tag", "tax_report_21", "balance"),
("tax_report_21_balance_from_tags", "tax_report_21", "balance_from_tags"),
("tax_report_22_applied_carryover", "tax_report_22", "_applied_carryover_balance"),
("tax_report_22_tag", "tax_report_22", "tag"),
("tax_report_22_balance_rounded", "tax_report_22", "balance_rounded"),
("tax_report_22_balance", "tax_report_22", "balance"),
("tax_report_2C_tag", "tax_report_2C", "balance"),
("tax_report_2C_balance_rounded", "tax_report_2C", "balance_rounded"),
("tax_report_2C_balance_from_tags", "tax_report_2C", "balance_from_tags"),
("tax_report_22A_tag", "tax_report_22A", "balance"),
("tax_report_22A_balance_rounded", "tax_report_22A", "balance_rounded"),
("tax_report_22A_balance_from_tags", "tax_report_22A", "balance_from_tags"),
("tax_report_23_formula", "tax_report_23", "balance"),
("tax_report_24_tag", "tax_report_24", "balance"),
("tax_report_24_balance_rounded", "tax_report_24", "balance_rounded"),
("tax_report_24_balance_from_tags", "tax_report_24", "balance_from_tags"),
("tax_report_2E_tag", "tax_report_2E", "balance"),
("tax_report_2E_balance_rounded", "tax_report_2E", "balance_rounded"),
("tax_report_2E_balance_from_tags", "tax_report_2E", "balance_from_tags"),
("tax_report_25_formula", "tax_report_25", "balance"),
("tax_report_25_adjustment", "tax_report_25", "adjustment"),
("tax_report_td_formula", "tax_report_TD", "balance"),
("tax_report_TICFE_tag", "tax_report_TICFE", "balance"),
("tax_report_TICGN_tag", "tax_report_TICGN", "balance"),
("tax_report_TICC_tag", "tax_report_TICC", "balance"),
("tax_report_TIC_total_formula", "tax_report_TIC_total", "balance"),
("tax_report_X1_tag", "tax_report_X1", "balance"),
("tax_report_X1_balance_rounded", "tax_report_X1", "balance_rounded"),
("tax_report_X1_balance_from_tags", "tax_report_X1", "balance_from_tags"),
("tax_report_X2_tag", "tax_report_X2", "balance"),
("tax_report_X2_balance_rounded", "tax_report_X2", "balance_rounded"),
("tax_report_X2_balance_from_tags", "tax_report_X2", "balance_from_tags"),
("tax_report_X3_tag", "tax_report_X3", "balance"),
("tax_report_X3_balance_rounded", "tax_report_X3", "balance_rounded"),
("tax_report_X3_balance_from_tags", "tax_report_X3", "balance_from_tags"),
("tax_report_X4_formula", "tax_report_X4", "balance"),
("tax_report_Y1_formula", "tax_report_Y1", "balance"),
("tax_report_Y2_formula", "tax_report_Y2", "balance"),
("tax_report_Y3_formula", "tax_report_Y3", "balance"),
("tax_report_Z1_tag", "tax_report_Z1", "balance"),
("tax_report_Z1_balance_rounded", "tax_report_Z1", "balance_rounded"),
("tax_report_Z1_balance_from_tags", "tax_report_Z1", "balance_from_tags"),
("tax_report_Z2_tag", "tax_report_Z2", "balance"),
("tax_report_Z2_balance_rounded", "tax_report_Z2", "balance_rounded"),
("tax_report_Z2_balance_from_tags", "tax_report_Z2", "balance_from_tags"),
("tax_report_Z3_tag", "tax_report_Z3", "balance"),
("tax_report_Z3_balance_rounded", "tax_report_Z3", "balance_rounded"),
("tax_report_Z3_balance_from_tags", "tax_report_Z3", "balance_from_tags"),
("tax_report_Z4_formula", "tax_report_Z4", "balance"),
("tax_report_26_external_tag", "tax_report_26_external", "balance"),
("tax_report_AA_tag", "tax_report_AA", "balance"),
("tax_report_AA_balance_rounded", "tax_report_AA", "balance_rounded"),
("tax_report_AA_balance_from_tags", "tax_report_AA", "balance_from_tags"),
("tax_report_27_formula", "tax_report_27", "balance"),
("tax_report_27_formula_temp", "tax_report_27", "_balance_temp"),
("tax_report_27_carryover", "tax_report_27", "_carryover_balance"),
("tax_report_28_formula", "tax_report_28", "balance"),
("tax_report_29_tag", "tax_report_29", "balance"),
("tax_report_29_balance_rounded", "tax_report_29", "balance_rounded"),
("tax_report_29_balance_from_tags", "tax_report_29", "balance_from_tags"),
("tax_report_32_formula", "tax_report_32", "balance"),
]
# Fix 1: insert missing ir_model_data entries for orphan expressions
cr.execute(
"""
SELECT imd.name, imd.res_id
FROM ir_model_data imd
WHERE imd.module = 'l10n_fr_account'
AND imd.model = 'account.report.line'
"""
)
line_xmlid_to_id = {row[0]: row[1] for row in cr.fetchall()}
cr.execute(
"""
SELECT res_id FROM ir_model_data
WHERE model = 'account.report.expression'
"""
)
known_expr_ids = {row[0] for row in cr.fetchall()}
inserted = 0
for expr_xmlid, line_xmlid, label in expressions:
line_id = line_xmlid_to_id.get(line_xmlid)
if not line_id:
continue
cr.execute(
"""
SELECT id FROM account_report_expression
WHERE report_line_id = %s AND label = %s
""",
(line_id, label),
)
row = cr.fetchone()
if not row:
continue
expr_id = row[0]
if expr_id in known_expr_ids:
continue
cr.execute(
"""
INSERT INTO ir_model_data (name, module, model, res_id, noupdate, write_date, create_date)
VALUES (%s, 'l10n_fr_account', 'account.report.expression', %s, false, NOW(), NOW())
ON CONFLICT DO NOTHING
""",
(expr_xmlid, expr_id),
)
known_expr_ids.add(expr_id)
inserted += 1
if inserted:
print(
f" [l10n_fr_account] Inserted {inserted} missing ir_model_data entries"
" for orphan expressions"
)
# Fix 2: delete orphan expressions obsolete in V18
# box_X5, box_Y4, box_Y5, box_Z5 now use aggregation_formula field
# instead of expression_ids, so their V17 expressions are obsolete.
obsolete_line_xmlids = [
"tax_report_X5",
"tax_report_Y4",
"tax_report_Y5",
"tax_report_Z5",
]
cr.execute(
"""
DELETE FROM account_report_expression
WHERE report_line_id IN (
SELECT res_id FROM ir_model_data
WHERE module = 'l10n_fr_account'
AND model = 'account.report.line'
AND name = ANY(%s)
)
AND id NOT IN (
SELECT res_id FROM ir_model_data
WHERE model = 'account.report.expression'
)
""",
(obsolete_line_xmlids,),
)
deleted = cr.rowcount
if deleted:
print(
f" [l10n_fr_account] Deleted {deleted} obsolete orphan expressions"
" (X5/Y4/Y5/Z5)"
)

View File

@@ -1,11 +1,4 @@
#!/bin/bash #!/bin/bash
set -euo pipefail set -euo pipefail
# --upgrade-path lists BOTH our custom migration scripts and OpenUpgrade's run_compose run -p 8018:8069 ou18 --config=/opt/odoo/auto/odoo.conf --stop-after-init -u all --workers 0 --log-level=debug --max-cron-threads=0 --limit-time-real=10000 --database=ou18 --load=base,web,openupgrade_framework
# native ones. Passing --upgrade-path overrides config['upgrade_path'], and
# openupgrade_framework only sets that default when it is empty; so we must
# include the native openupgrade_scripts/scripts path explicitly, otherwise the
# native migration scripts would no longer run. Odoo appends every listed path
# to odoo.upgrade.__path__ and concatenates (does not override) the scripts
# found per module/version, so our script runs IN ADDITION to the native ones.
run_compose run -p 8018:8069 ou18 --config=/opt/odoo/auto/odoo.conf --stop-after-init -u all --workers 0 --log-level=debug --max-cron-threads=0 --limit-time-real=10000 --database=ou18 --load=base,web,openupgrade_framework --addons-path=/opt/odoo/auto/mig,/opt/odoo/auto/addons --upgrade-path=/opt/odoo/auto/upgrade,/opt/odoo/auto/addons/openupgrade_scripts/scripts