2025-10-31 01:10:54 -04:00
|
|
|
|
#!/usr/bin/env python3
|
2026-03-11 23:15:07 -04:00
|
|
|
|
# © 2021-2026 TechnoLibre (http://www.technolibre.ca)
|
2025-10-31 01:10:54 -04:00
|
|
|
|
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl)
|
|
|
|
|
|
|
|
|
|
|
|
import datetime
|
|
|
|
|
|
import json
|
|
|
|
|
|
import logging
|
|
|
|
|
|
import os
|
|
|
|
|
|
import shutil
|
|
|
|
|
|
import sys
|
|
|
|
|
|
import zipfile
|
|
|
|
|
|
from uuid import uuid4
|
|
|
|
|
|
|
|
|
|
|
|
import click
|
|
|
|
|
|
import todo_file_browser
|
|
|
|
|
|
|
2026-07-30 23:00:12 -04:00
|
|
|
|
from script.todo.version_manager import get_odoo_version
|
|
|
|
|
|
|
[IMP] migration: show the real state of each step and replay from any of them
The resume menu exposed internal key names: « Reuse database without state_4 »
tells nobody what will happen, and the most useful option -- keep the prepared
database, redo the version bumps -- was the least readable of the four.
It now prints where the migration actually stands: the zip, the database, the
target, and one line per step with its state. Step 4 reports how many version
bumps are migrated and names them, so « 0/6 · 13 14 15 16 17 18 » replaces a
list index nobody could interpret.
The four numbered options become:
[c] continue where it stopped (was: press enter)
[0-4] replay from that step (new: rewind to any step)
[n] new migration, erase everything (was: [1])
[r] keep the zip, ask everything (was: [4])
Rewinding drops « state_* » from the chosen step onward and keeps the rest:
config_* answers, the zip and the target version are decisions, not progress,
so they are no longer re-asked. state_0_search_missing_module is still forced
back to False, as the previous code did: it fills an in-memory dict the later
steps rely on. Old [2] is now « replay from 0 », old [3] « replay from 4 ».
First use of t() in this file; the new strings are translated in todo_i18n.py.
Verified against the real progression of the 12.0 -> 18.0 migration and a
synthetic one: for every step, the kept keys are exactly those of the earlier
steps, config and target survive, and the module search is forced again.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-08-01 02:53:17 -04:00
|
|
|
|
try:
|
|
|
|
|
|
from script.todo.todo_i18n import t
|
|
|
|
|
|
except Exception: # pragma: no cover - fallback when i18n is unavailable
|
|
|
|
|
|
|
|
|
|
|
|
def t(key: str) -> str:
|
|
|
|
|
|
return key
|
|
|
|
|
|
|
|
|
|
|
|
|
2025-10-31 01:10:54 -04:00
|
|
|
|
new_path = os.path.normpath(
|
|
|
|
|
|
os.path.join(os.path.dirname(__file__), "..", "..")
|
|
|
|
|
|
)
|
|
|
|
|
|
sys.path.append(new_path)
|
|
|
|
|
|
|
2026-02-13 01:27:38 -05:00
|
|
|
|
from script.execute import execute
|
2025-10-31 01:10:54 -04:00
|
|
|
|
from script.git.git_tool import GitTool
|
|
|
|
|
|
|
|
|
|
|
|
_logger = logging.getLogger(__name__)
|
|
|
|
|
|
|
|
|
|
|
|
PYTHON_BIN = ".venv.erplibre/bin/python3"
|
|
|
|
|
|
UPGRADE_DATABASE_CONFIG_LOG = ".venv.erplibre/odoo_database_migration_log.json"
|
|
|
|
|
|
# UPGRADE_MODULE_CONFIG_LOG = ".venv.erplibre/odoo_module_migration_log.json"
|
|
|
|
|
|
VENV_NAME_MODULE_MIGRATOR = ".venv"
|
|
|
|
|
|
LST_PATH_OCA_ODOO_MODULE_MIGRATOR = ["script", "OCA_odoo-module-migrator"]
|
|
|
|
|
|
PATH_OCA_ODOO_MODULE_MIGRATOR = "./" + "/".join(
|
|
|
|
|
|
LST_PATH_OCA_ODOO_MODULE_MIGRATOR
|
|
|
|
|
|
)
|
|
|
|
|
|
PATH_VENV_MODULE_MIGRATOR = os.path.join(
|
|
|
|
|
|
PATH_OCA_ODOO_MODULE_MIGRATOR, VENV_NAME_MODULE_MIGRATOR
|
|
|
|
|
|
)
|
|
|
|
|
|
PATH_SOURCE_VENV_MODULE_MIGRATOR = os.path.join(
|
|
|
|
|
|
PATH_VENV_MODULE_MIGRATOR, "bin", "activate"
|
|
|
|
|
|
)
|
|
|
|
|
|
FILENAME_ODOO_VERSION = ".odoo-version"
|
|
|
|
|
|
LOCAL_MANIFEST = os.path.join(
|
|
|
|
|
|
".repo", "local_manifests", "erplibre_manifest.xml"
|
|
|
|
|
|
)
|
[IMP] migration: per-database uninstall list, with a stated reason
Which modules must be uninstalled before a version bump depends on the data of
one specific database, so that list does not belong to a shared versioned file.
- Read private/odoo/migration/<database>/uninstall_module_list_odooXX0_to_odooYY0.txt
first, then the shared versioned defaults under script/odoo/migration/, and
merge them (duplicates dropped). private/ mirrors script/, the convention
already used by script/todo/todo.json -> private/todo/todo_override.json.
- Fix the parser. It was « f.readline().split() »: only the FIRST line was kept,
so a multi-line list was silently truncated, and a comma-separated list turned
into one bogus module name. On a file starting with a comment it returned the
words of that comment as module names. It now reads every line and accepts
commas, several names per line, blank lines and comments.
- Support a « # reason » justification per module, printed before uninstalling;
a module with no reason is flagged. Removing a module must stay reviewable.
- Ignore private/odoo/ in git: these lists describe one database.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-31 23:25:43 -04:00
|
|
|
|
# Module lists for a version bump. The shared, versioned defaults live under
|
|
|
|
|
|
# script/; the per-database lists live under private/ (mirroring script/, like
|
|
|
|
|
|
# script/todo/todo.json -> private/todo/todo_override.json). Which modules must
|
|
|
|
|
|
# be dropped depends on the database, so that choice is never versioned.
|
|
|
|
|
|
PATH_MIGRATION_GLOBAL = os.path.join("script", "odoo", "migration")
|
|
|
|
|
|
PATH_MIGRATION_PRIVATE = os.path.join("private", "odoo", "migration")
|
[IMP] migration: show the real state of each step and replay from any of them
The resume menu exposed internal key names: « Reuse database without state_4 »
tells nobody what will happen, and the most useful option -- keep the prepared
database, redo the version bumps -- was the least readable of the four.
It now prints where the migration actually stands: the zip, the database, the
target, and one line per step with its state. Step 4 reports how many version
bumps are migrated and names them, so « 0/6 · 13 14 15 16 17 18 » replaces a
list index nobody could interpret.
The four numbered options become:
[c] continue where it stopped (was: press enter)
[0-4] replay from that step (new: rewind to any step)
[n] new migration, erase everything (was: [1])
[r] keep the zip, ask everything (was: [4])
Rewinding drops « state_* » from the chosen step onward and keeps the rest:
config_* answers, the zip and the target version are decisions, not progress,
so they are no longer re-asked. state_0_search_missing_module is still forced
back to False, as the previous code did: it fills an in-memory dict the later
steps rely on. Old [2] is now « replay from 0 », old [3] « replay from 4 ».
First use of t() in this file; the new strings are translated in todo_i18n.py.
Verified against the real progression of the 12.0 -> 18.0 migration and a
synthetic one: for every step, the kept keys are exactly those of the earlier
steps, config and target survive, and the module search is forced again.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-08-01 02:53:17 -04:00
|
|
|
|
# Steps of the migration, in order. Each one owns the progression keys prefixed
|
|
|
|
|
|
# with « state_<index> ». Rewinding to a step drops its keys and every later
|
|
|
|
|
|
# one, so the run replays from there. Labels go through t(): the key IS the
|
|
|
|
|
|
# English string, as everywhere else in this project.
|
|
|
|
|
|
MIGRATION_STEP = [
|
|
|
|
|
|
(0, "Prepare the environment"),
|
|
|
|
|
|
(1, "Restore and neutralize the database"),
|
|
|
|
|
|
(2, "Update all addons"),
|
|
|
|
|
|
(3, "Clean up before data migration"),
|
|
|
|
|
|
(4, "Upgrade version by version (OpenUpgrade)"),
|
|
|
|
|
|
]
|
2025-10-31 01:10:54 -04:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class TodoUpgrade:
|
|
|
|
|
|
def __init__(self, todo):
|
|
|
|
|
|
self.file_path = None
|
|
|
|
|
|
self.dir_path = None
|
|
|
|
|
|
self.todo = todo
|
|
|
|
|
|
self.dct_progression = {}
|
|
|
|
|
|
self.lst_command_executed = []
|
|
|
|
|
|
self.dct_module_per_version = {}
|
|
|
|
|
|
self.dct_module_per_dct_version_path = {}
|
2026-02-13 01:27:38 -05:00
|
|
|
|
self.execute = execute.Execute()
|
2025-10-31 01:10:54 -04:00
|
|
|
|
|
|
|
|
|
|
def write_config(self):
|
|
|
|
|
|
if "date_create" not in self.dct_progression.keys():
|
|
|
|
|
|
self.dct_progression["date_create"] = str(datetime.datetime.now())
|
|
|
|
|
|
self.dct_progression["date_update"] = str(datetime.datetime.now())
|
|
|
|
|
|
# Always put command_executed at the end
|
|
|
|
|
|
if "command_executed" in self.dct_progression.keys():
|
|
|
|
|
|
value = self.dct_progression["command_executed"]
|
|
|
|
|
|
del self.dct_progression["command_executed"]
|
|
|
|
|
|
self.dct_progression["command_executed"] = value
|
|
|
|
|
|
with open(UPGRADE_DATABASE_CONFIG_LOG, "w") as f:
|
|
|
|
|
|
json.dump(self.dct_progression, f, indent=4)
|
|
|
|
|
|
|
[IMP] migration: show the real state of each step and replay from any of them
The resume menu exposed internal key names: « Reuse database without state_4 »
tells nobody what will happen, and the most useful option -- keep the prepared
database, redo the version bumps -- was the least readable of the four.
It now prints where the migration actually stands: the zip, the database, the
target, and one line per step with its state. Step 4 reports how many version
bumps are migrated and names them, so « 0/6 · 13 14 15 16 17 18 » replaces a
list index nobody could interpret.
The four numbered options become:
[c] continue where it stopped (was: press enter)
[0-4] replay from that step (new: rewind to any step)
[n] new migration, erase everything (was: [1])
[r] keep the zip, ask everything (was: [4])
Rewinding drops « state_* » from the chosen step onward and keeps the rest:
config_* answers, the zip and the target version are decisions, not progress,
so they are no longer re-asked. state_0_search_missing_module is still forced
back to False, as the previous code did: it fills an in-memory dict the later
steps rely on. Old [2] is now « replay from 0 », old [3] « replay from 4 ».
First use of t() in this file; the new strings are translated in todo_i18n.py.
Verified against the real progression of the 12.0 -> 18.0 migration and a
synthetic one: for every step, the kept keys are exactly those of the earlier
steps, config and target survive, and the module search is forced again.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-08-01 02:53:17 -04:00
|
|
|
|
@staticmethod
|
|
|
|
|
|
def read_progression():
|
|
|
|
|
|
"""Return the saved progression, or an empty dict if unreadable."""
|
|
|
|
|
|
try:
|
|
|
|
|
|
with open(UPGRADE_DATABASE_CONFIG_LOG, "r") as f:
|
|
|
|
|
|
return json.load(f)
|
|
|
|
|
|
except (json.decoder.JSONDecodeError, OSError):
|
|
|
|
|
|
print(
|
|
|
|
|
|
f"⚠️ {t('The progression file is invalid, ignoring it')}:"
|
|
|
|
|
|
f" {UPGRADE_DATABASE_CONFIG_LOG}"
|
|
|
|
|
|
)
|
|
|
|
|
|
return {}
|
|
|
|
|
|
|
|
|
|
|
|
@staticmethod
|
|
|
|
|
|
def step_status(dct_progression, step):
|
|
|
|
|
|
"""Return (icon, detail) telling how far a migration step went.
|
|
|
|
|
|
|
|
|
|
|
|
Steps 0 to 3 are plain booleans. Step 4 keeps one list per version, so
|
|
|
|
|
|
its detail reports which version bumps are already migrated.
|
|
|
|
|
|
"""
|
|
|
|
|
|
prefix = f"state_{step}_"
|
|
|
|
|
|
dct_flag = {
|
|
|
|
|
|
key: value
|
|
|
|
|
|
for key, value in dct_progression.items()
|
|
|
|
|
|
if key.startswith(prefix)
|
|
|
|
|
|
}
|
|
|
|
|
|
if not dct_flag:
|
|
|
|
|
|
return "⬜", t("not started")
|
|
|
|
|
|
|
|
|
|
|
|
if step == 4:
|
|
|
|
|
|
lst_done = dct_progression.get("state_4_upgrade_odoo_lst") or []
|
|
|
|
|
|
# The data migration list only appears once a bump succeeds, so the
|
|
|
|
|
|
# number of bumps comes from any per-version list (« *_odoo_lst »).
|
|
|
|
|
|
total = max(
|
|
|
|
|
|
[
|
|
|
|
|
|
len(value)
|
|
|
|
|
|
for key, value in dct_progression.items()
|
|
|
|
|
|
if key.startswith("state_4_")
|
|
|
|
|
|
and key.endswith("_odoo_lst")
|
|
|
|
|
|
and isinstance(value, list)
|
|
|
|
|
|
]
|
|
|
|
|
|
or [0]
|
|
|
|
|
|
)
|
|
|
|
|
|
done = sum(1 for item in lst_done if item)
|
|
|
|
|
|
detail = f"{done}/{total} " + t("version bumps migrated")
|
|
|
|
|
|
# Name the versions when the target is known: the list ends on the
|
|
|
|
|
|
# target, so the first bump is target - total + 1.
|
|
|
|
|
|
try:
|
|
|
|
|
|
last = int(float(dct_progression["target_odoo_version"]))
|
|
|
|
|
|
detail += " · " + " ".join(
|
|
|
|
|
|
f"{last - total + 1 + i}"
|
|
|
|
|
|
f"{'✓' if i < len(lst_done) and lst_done[i] else ''}"
|
|
|
|
|
|
for i in range(total)
|
|
|
|
|
|
)
|
|
|
|
|
|
except (KeyError, TypeError, ValueError):
|
|
|
|
|
|
pass
|
|
|
|
|
|
return ("✅" if total and done == total else "⏳"), detail
|
|
|
|
|
|
|
|
|
|
|
|
if all(dct_flag.values()):
|
|
|
|
|
|
return "✅", t("done")
|
|
|
|
|
|
return "⏳", t("partially done")
|
|
|
|
|
|
|
[ADD] migration: a TUI resume screen, same first question as the prompt
The migration now opens with the same choice the QEMU deployment does — TUI
or line-by-line prompts — settled in advance by TODO > Configuration if you
want it to be. Choosing the TUI loads the saved progression and asks the very
same first question: where does this migration stand, and where do we resume?
prompt_resume was one function that rendered, read and decided at once, so a
second view would have meant a second copy of the decision. It is now three:
resume_context() the progression -> plain data (file, database, target,
steps with their icon and detail, version bumps)
print_resume() renders it on the terminal
apply_resume_answer() answer -> (progression, changed)
The TUI returns the SAME answer strings as the prompt — c, n, r, q, 0..4,
4.<version> — so apply_resume_answer stays the only place that decides what a
choice means. Neither view can drift into describing the migration
differently, because both render the same context.
In the TUI the steps are a table and the version bumps a list: Enter on either
replays from there, which is what « [0-4] » and « [4.N] » meant in text. The
cursor opens on the first unfinished step and on the first unmigrated version
— where it stopped is where one usually wants to act.
Both views gain « q », quit without doing anything. A TUI needs Escape to do
something sane, and an escape hatch present in only one of the two views is
exactly the kind of divergence this split exists to prevent. execute_odoo_
upgrade returns immediately on it, writing nothing.
Verified on a 12->18 progression with steps 0-3 done and 2 of 6 bumps
migrated: identical context feeding both views, Enter on step 2 giving « 2 »,
Enter on the 15 bump giving « 4.15 », the c/n/r/q/Escape shortcuts, and every
answer producing the same progression through both paths — « 2 » leaving only
the state of steps 0 and 1, « 4.15 » resetting the clone list from the third
bump on so the half-migrated intermediate database gets rebuilt. « q » checked
to leave the progression file byte-identical.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-08-01 06:38:31 -04:00
|
|
|
|
def resume_context(self, old_dct_progression):
|
|
|
|
|
|
"""Everything the resume screen shows, as plain data.
|
[IMP] migration: show the real state of each step and replay from any of them
The resume menu exposed internal key names: « Reuse database without state_4 »
tells nobody what will happen, and the most useful option -- keep the prepared
database, redo the version bumps -- was the least readable of the four.
It now prints where the migration actually stands: the zip, the database, the
target, and one line per step with its state. Step 4 reports how many version
bumps are migrated and names them, so « 0/6 · 13 14 15 16 17 18 » replaces a
list index nobody could interpret.
The four numbered options become:
[c] continue where it stopped (was: press enter)
[0-4] replay from that step (new: rewind to any step)
[n] new migration, erase everything (was: [1])
[r] keep the zip, ask everything (was: [4])
Rewinding drops « state_* » from the chosen step onward and keeps the rest:
config_* answers, the zip and the target version are decisions, not progress,
so they are no longer re-asked. state_0_search_missing_module is still forced
back to False, as the previous code did: it fills an in-memory dict the later
steps rely on. Old [2] is now « replay from 0 », old [3] « replay from 4 ».
First use of t() in this file; the new strings are translated in todo_i18n.py.
Verified against the real progression of the 12.0 -> 18.0 migration and a
synthetic one: for every step, the kept keys are exactly those of the earlier
steps, config and target survive, and the module search is forced again.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-08-01 02:53:17 -04:00
|
|
|
|
|
[ADD] migration: a TUI resume screen, same first question as the prompt
The migration now opens with the same choice the QEMU deployment does — TUI
or line-by-line prompts — settled in advance by TODO > Configuration if you
want it to be. Choosing the TUI loads the saved progression and asks the very
same first question: where does this migration stand, and where do we resume?
prompt_resume was one function that rendered, read and decided at once, so a
second view would have meant a second copy of the decision. It is now three:
resume_context() the progression -> plain data (file, database, target,
steps with their icon and detail, version bumps)
print_resume() renders it on the terminal
apply_resume_answer() answer -> (progression, changed)
The TUI returns the SAME answer strings as the prompt — c, n, r, q, 0..4,
4.<version> — so apply_resume_answer stays the only place that decides what a
choice means. Neither view can drift into describing the migration
differently, because both render the same context.
In the TUI the steps are a table and the version bumps a list: Enter on either
replays from there, which is what « [0-4] » and « [4.N] » meant in text. The
cursor opens on the first unfinished step and on the first unmigrated version
— where it stopped is where one usually wants to act.
Both views gain « q », quit without doing anything. A TUI needs Escape to do
something sane, and an escape hatch present in only one of the two views is
exactly the kind of divergence this split exists to prevent. execute_odoo_
upgrade returns immediately on it, writing nothing.
Verified on a 12->18 progression with steps 0-3 done and 2 of 6 bumps
migrated: identical context feeding both views, Enter on step 2 giving « 2 »,
Enter on the 15 bump giving « 4.15 », the c/n/r/q/Escape shortcuts, and every
answer producing the same progression through both paths — « 2 » leaving only
the state of steps 0 and 1, « 4.15 » resetting the clone list from the third
bump on so the half-migrated intermediate database gets rebuilt. « q » checked
to leave the progression file byte-identical.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-08-01 06:38:31 -04:00
|
|
|
|
No I/O: the line-by-line prompt and the TUI both render THIS, so the
|
|
|
|
|
|
two can never describe the migration differently.
|
[IMP] migration: show the real state of each step and replay from any of them
The resume menu exposed internal key names: « Reuse database without state_4 »
tells nobody what will happen, and the most useful option -- keep the prepared
database, redo the version bumps -- was the least readable of the four.
It now prints where the migration actually stands: the zip, the database, the
target, and one line per step with its state. Step 4 reports how many version
bumps are migrated and names them, so « 0/6 · 13 14 15 16 17 18 » replaces a
list index nobody could interpret.
The four numbered options become:
[c] continue where it stopped (was: press enter)
[0-4] replay from that step (new: rewind to any step)
[n] new migration, erase everything (was: [1])
[r] keep the zip, ask everything (was: [4])
Rewinding drops « state_* » from the chosen step onward and keeps the rest:
config_* answers, the zip and the target version are decisions, not progress,
so they are no longer re-asked. state_0_search_missing_module is still forced
back to False, as the previous code did: it fills an in-memory dict the later
steps rely on. Old [2] is now « replay from 0 », old [3] « replay from 4 ».
First use of t() in this file; the new strings are translated in todo_i18n.py.
Verified against the real progression of the 12.0 -> 18.0 migration and a
synthetic one: for every step, the kept keys are exactly those of the earlier
steps, config and target survive, and the module search is forced again.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-08-01 02:53:17 -04:00
|
|
|
|
"""
|
|
|
|
|
|
migration_file = old_dct_progression.get("migration_file") or "?"
|
[ADD] migration: a TUI resume screen, same first question as the prompt
The migration now opens with the same choice the QEMU deployment does — TUI
or line-by-line prompts — settled in advance by TODO > Configuration if you
want it to be. Choosing the TUI loads the saved progression and asks the very
same first question: where does this migration stand, and where do we resume?
prompt_resume was one function that rendered, read and decided at once, so a
second view would have meant a second copy of the decision. It is now three:
resume_context() the progression -> plain data (file, database, target,
steps with their icon and detail, version bumps)
print_resume() renders it on the terminal
apply_resume_answer() answer -> (progression, changed)
The TUI returns the SAME answer strings as the prompt — c, n, r, q, 0..4,
4.<version> — so apply_resume_answer stays the only place that decides what a
choice means. Neither view can drift into describing the migration
differently, because both render the same context.
In the TUI the steps are a table and the version bumps a list: Enter on either
replays from there, which is what « [0-4] » and « [4.N] » meant in text. The
cursor opens on the first unfinished step and on the first unmigrated version
— where it stopped is where one usually wants to act.
Both views gain « q », quit without doing anything. A TUI needs Escape to do
something sane, and an escape hatch present in only one of the two views is
exactly the kind of divergence this split exists to prevent. execute_odoo_
upgrade returns immediately on it, writing nothing.
Verified on a 12->18 progression with steps 0-3 done and 2 of 6 bumps
migrated: identical context feeding both views, Enter on step 2 giving « 2 »,
Enter on the 15 bump giving « 4.15 », the c/n/r/q/Escape shortcuts, and every
answer producing the same progression through both paths — « 2 » leaving only
the state of steps 0 and 1, « 4.15 » resetting the clone list from the third
bump on so the half-migrated intermediate database gets rebuilt. « q » checked
to leave the progression file byte-identical.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-08-01 06:38:31 -04:00
|
|
|
|
steps = []
|
|
|
|
|
|
for step, label in MIGRATION_STEP:
|
|
|
|
|
|
icon, detail = self.step_status(old_dct_progression, step)
|
|
|
|
|
|
steps.append(
|
|
|
|
|
|
{
|
|
|
|
|
|
"step": step,
|
|
|
|
|
|
"icon": icon,
|
|
|
|
|
|
"label": t(label),
|
|
|
|
|
|
"detail": detail,
|
|
|
|
|
|
}
|
|
|
|
|
|
)
|
|
|
|
|
|
lst_version = self.version_bumps(old_dct_progression)
|
|
|
|
|
|
done = old_dct_progression.get("state_4_upgrade_odoo_lst") or []
|
|
|
|
|
|
return {
|
|
|
|
|
|
"file": os.path.basename(migration_file),
|
|
|
|
|
|
"database": old_dct_progression.get("config_database_name") or "?",
|
|
|
|
|
|
"target": old_dct_progression.get("target_odoo_version") or "?",
|
|
|
|
|
|
"started": old_dct_progression.get("date_create") or "?",
|
|
|
|
|
|
"steps": steps,
|
|
|
|
|
|
"versions": [
|
|
|
|
|
|
{
|
|
|
|
|
|
"version": version,
|
|
|
|
|
|
"done": bool(i < len(done) and done[i]),
|
|
|
|
|
|
}
|
|
|
|
|
|
for i, version in enumerate(lst_version)
|
|
|
|
|
|
],
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
@staticmethod
|
|
|
|
|
|
def print_resume(ctx):
|
|
|
|
|
|
"""Render the resume screen on the terminal."""
|
[IMP] migration: show the real state of each step and replay from any of them
The resume menu exposed internal key names: « Reuse database without state_4 »
tells nobody what will happen, and the most useful option -- keep the prepared
database, redo the version bumps -- was the least readable of the four.
It now prints where the migration actually stands: the zip, the database, the
target, and one line per step with its state. Step 4 reports how many version
bumps are migrated and names them, so « 0/6 · 13 14 15 16 17 18 » replaces a
list index nobody could interpret.
The four numbered options become:
[c] continue where it stopped (was: press enter)
[0-4] replay from that step (new: rewind to any step)
[n] new migration, erase everything (was: [1])
[r] keep the zip, ask everything (was: [4])
Rewinding drops « state_* » from the chosen step onward and keeps the rest:
config_* answers, the zip and the target version are decisions, not progress,
so they are no longer re-asked. state_0_search_missing_module is still forced
back to False, as the previous code did: it fills an in-memory dict the later
steps rely on. Old [2] is now « replay from 0 », old [3] « replay from 4 ».
First use of t() in this file; the new strings are translated in todo_i18n.py.
Verified against the real progression of the 12.0 -> 18.0 migration and a
synthetic one: for every step, the kept keys are exactly those of the earlier
steps, config and target survive, and the module search is forced again.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-08-01 02:53:17 -04:00
|
|
|
|
print()
|
|
|
|
|
|
print(f"📍 {t('Migration in progress')}")
|
|
|
|
|
|
# Pad in code, not in the translations: the labels differ in length
|
|
|
|
|
|
# between languages and a hardcoded padding misaligns the colons.
|
[ADD] migration: a TUI resume screen, same first question as the prompt
The migration now opens with the same choice the QEMU deployment does — TUI
or line-by-line prompts — settled in advance by TODO > Configuration if you
want it to be. Choosing the TUI loads the saved progression and asks the very
same first question: where does this migration stand, and where do we resume?
prompt_resume was one function that rendered, read and decided at once, so a
second view would have meant a second copy of the decision. It is now three:
resume_context() the progression -> plain data (file, database, target,
steps with their icon and detail, version bumps)
print_resume() renders it on the terminal
apply_resume_answer() answer -> (progression, changed)
The TUI returns the SAME answer strings as the prompt — c, n, r, q, 0..4,
4.<version> — so apply_resume_answer stays the only place that decides what a
choice means. Neither view can drift into describing the migration
differently, because both render the same context.
In the TUI the steps are a table and the version bumps a list: Enter on either
replays from there, which is what « [0-4] » and « [4.N] » meant in text. The
cursor opens on the first unfinished step and on the first unmigrated version
— where it stopped is where one usually wants to act.
Both views gain « q », quit without doing anything. A TUI needs Escape to do
something sane, and an escape hatch present in only one of the two views is
exactly the kind of divergence this split exists to prevent. execute_odoo_
upgrade returns immediately on it, writing nothing.
Verified on a 12->18 progression with steps 0-3 done and 2 of 6 bumps
migrated: identical context feeding both views, Enter on step 2 giving « 2 »,
Enter on the 15 bump giving « 4.15 », the c/n/r/q/Escape shortcuts, and every
answer producing the same progression through both paths — « 2 » leaving only
the state of steps 0 and 1, « 4.15 » resetting the clone list from the third
bump on so the half-migrated intermediate database gets rebuilt. « q » checked
to leave the progression file byte-identical.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-08-01 06:38:31 -04:00
|
|
|
|
print(f" {t('File'):<9}: {ctx['file']}")
|
[IMP] migration: show the real state of each step and replay from any of them
The resume menu exposed internal key names: « Reuse database without state_4 »
tells nobody what will happen, and the most useful option -- keep the prepared
database, redo the version bumps -- was the least readable of the four.
It now prints where the migration actually stands: the zip, the database, the
target, and one line per step with its state. Step 4 reports how many version
bumps are migrated and names them, so « 0/6 · 13 14 15 16 17 18 » replaces a
list index nobody could interpret.
The four numbered options become:
[c] continue where it stopped (was: press enter)
[0-4] replay from that step (new: rewind to any step)
[n] new migration, erase everything (was: [1])
[r] keep the zip, ask everything (was: [4])
Rewinding drops « state_* » from the chosen step onward and keeps the rest:
config_* answers, the zip and the target version are decisions, not progress,
so they are no longer re-asked. state_0_search_missing_module is still forced
back to False, as the previous code did: it fills an in-memory dict the later
steps rely on. Old [2] is now « replay from 0 », old [3] « replay from 4 ».
First use of t() in this file; the new strings are translated in todo_i18n.py.
Verified against the real progression of the 12.0 -> 18.0 migration and a
synthetic one: for every step, the kept keys are exactly those of the earlier
steps, config and target survive, and the module search is forced again.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-08-01 02:53:17 -04:00
|
|
|
|
print(
|
[ADD] migration: a TUI resume screen, same first question as the prompt
The migration now opens with the same choice the QEMU deployment does — TUI
or line-by-line prompts — settled in advance by TODO > Configuration if you
want it to be. Choosing the TUI loads the saved progression and asks the very
same first question: where does this migration stand, and where do we resume?
prompt_resume was one function that rendered, read and decided at once, so a
second view would have meant a second copy of the decision. It is now three:
resume_context() the progression -> plain data (file, database, target,
steps with their icon and detail, version bumps)
print_resume() renders it on the terminal
apply_resume_answer() answer -> (progression, changed)
The TUI returns the SAME answer strings as the prompt — c, n, r, q, 0..4,
4.<version> — so apply_resume_answer stays the only place that decides what a
choice means. Neither view can drift into describing the migration
differently, because both render the same context.
In the TUI the steps are a table and the version bumps a list: Enter on either
replays from there, which is what « [0-4] » and « [4.N] » meant in text. The
cursor opens on the first unfinished step and on the first unmigrated version
— where it stopped is where one usually wants to act.
Both views gain « q », quit without doing anything. A TUI needs Escape to do
something sane, and an escape hatch present in only one of the two views is
exactly the kind of divergence this split exists to prevent. execute_odoo_
upgrade returns immediately on it, writing nothing.
Verified on a 12->18 progression with steps 0-3 done and 2 of 6 bumps
migrated: identical context feeding both views, Enter on step 2 giving « 2 »,
Enter on the 15 bump giving « 4.15 », the c/n/r/q/Escape shortcuts, and every
answer producing the same progression through both paths — « 2 » leaving only
the state of steps 0 and 1, « 4.15 » resetting the clone list from the third
bump on so the half-migrated intermediate database gets rebuilt. « q » checked
to leave the progression file byte-identical.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-08-01 06:38:31 -04:00
|
|
|
|
f" {t('Database'):<9}: {ctx['database']}"
|
|
|
|
|
|
f" · {t('Target')} : {ctx['target']}"
|
[IMP] migration: show the real state of each step and replay from any of them
The resume menu exposed internal key names: « Reuse database without state_4 »
tells nobody what will happen, and the most useful option -- keep the prepared
database, redo the version bumps -- was the least readable of the four.
It now prints where the migration actually stands: the zip, the database, the
target, and one line per step with its state. Step 4 reports how many version
bumps are migrated and names them, so « 0/6 · 13 14 15 16 17 18 » replaces a
list index nobody could interpret.
The four numbered options become:
[c] continue where it stopped (was: press enter)
[0-4] replay from that step (new: rewind to any step)
[n] new migration, erase everything (was: [1])
[r] keep the zip, ask everything (was: [4])
Rewinding drops « state_* » from the chosen step onward and keeps the rest:
config_* answers, the zip and the target version are decisions, not progress,
so they are no longer re-asked. state_0_search_missing_module is still forced
back to False, as the previous code did: it fills an in-memory dict the later
steps rely on. Old [2] is now « replay from 0 », old [3] « replay from 4 ».
First use of t() in this file; the new strings are translated in todo_i18n.py.
Verified against the real progression of the 12.0 -> 18.0 migration and a
synthetic one: for every step, the kept keys are exactly those of the earlier
steps, config and target survive, and the module search is forced again.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-08-01 02:53:17 -04:00
|
|
|
|
)
|
[ADD] migration: a TUI resume screen, same first question as the prompt
The migration now opens with the same choice the QEMU deployment does — TUI
or line-by-line prompts — settled in advance by TODO > Configuration if you
want it to be. Choosing the TUI loads the saved progression and asks the very
same first question: where does this migration stand, and where do we resume?
prompt_resume was one function that rendered, read and decided at once, so a
second view would have meant a second copy of the decision. It is now three:
resume_context() the progression -> plain data (file, database, target,
steps with their icon and detail, version bumps)
print_resume() renders it on the terminal
apply_resume_answer() answer -> (progression, changed)
The TUI returns the SAME answer strings as the prompt — c, n, r, q, 0..4,
4.<version> — so apply_resume_answer stays the only place that decides what a
choice means. Neither view can drift into describing the migration
differently, because both render the same context.
In the TUI the steps are a table and the version bumps a list: Enter on either
replays from there, which is what « [0-4] » and « [4.N] » meant in text. The
cursor opens on the first unfinished step and on the first unmigrated version
— where it stopped is where one usually wants to act.
Both views gain « q », quit without doing anything. A TUI needs Escape to do
something sane, and an escape hatch present in only one of the two views is
exactly the kind of divergence this split exists to prevent. execute_odoo_
upgrade returns immediately on it, writing nothing.
Verified on a 12->18 progression with steps 0-3 done and 2 of 6 bumps
migrated: identical context feeding both views, Enter on step 2 giving « 2 »,
Enter on the 15 bump giving « 4.15 », the c/n/r/q/Escape shortcuts, and every
answer producing the same progression through both paths — « 2 » leaving only
the state of steps 0 and 1, « 4.15 » resetting the clone list from the third
bump on so the half-migrated intermediate database gets rebuilt. « q » checked
to leave the progression file byte-identical.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-08-01 06:38:31 -04:00
|
|
|
|
print(f" {t('Started'):<9}: {ctx['started']}")
|
[IMP] migration: show the real state of each step and replay from any of them
The resume menu exposed internal key names: « Reuse database without state_4 »
tells nobody what will happen, and the most useful option -- keep the prepared
database, redo the version bumps -- was the least readable of the four.
It now prints where the migration actually stands: the zip, the database, the
target, and one line per step with its state. Step 4 reports how many version
bumps are migrated and names them, so « 0/6 · 13 14 15 16 17 18 » replaces a
list index nobody could interpret.
The four numbered options become:
[c] continue where it stopped (was: press enter)
[0-4] replay from that step (new: rewind to any step)
[n] new migration, erase everything (was: [1])
[r] keep the zip, ask everything (was: [4])
Rewinding drops « state_* » from the chosen step onward and keeps the rest:
config_* answers, the zip and the target version are decisions, not progress,
so they are no longer re-asked. state_0_search_missing_module is still forced
back to False, as the previous code did: it fills an in-memory dict the later
steps rely on. Old [2] is now « replay from 0 », old [3] « replay from 4 ».
First use of t() in this file; the new strings are translated in todo_i18n.py.
Verified against the real progression of the 12.0 -> 18.0 migration and a
synthetic one: for every step, the kept keys are exactly those of the earlier
steps, config and target survive, and the module search is forced again.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-08-01 02:53:17 -04:00
|
|
|
|
print()
|
|
|
|
|
|
print(f" {t('Steps')} :")
|
[ADD] migration: a TUI resume screen, same first question as the prompt
The migration now opens with the same choice the QEMU deployment does — TUI
or line-by-line prompts — settled in advance by TODO > Configuration if you
want it to be. Choosing the TUI loads the saved progression and asks the very
same first question: where does this migration stand, and where do we resume?
prompt_resume was one function that rendered, read and decided at once, so a
second view would have meant a second copy of the decision. It is now three:
resume_context() the progression -> plain data (file, database, target,
steps with their icon and detail, version bumps)
print_resume() renders it on the terminal
apply_resume_answer() answer -> (progression, changed)
The TUI returns the SAME answer strings as the prompt — c, n, r, q, 0..4,
4.<version> — so apply_resume_answer stays the only place that decides what a
choice means. Neither view can drift into describing the migration
differently, because both render the same context.
In the TUI the steps are a table and the version bumps a list: Enter on either
replays from there, which is what « [0-4] » and « [4.N] » meant in text. The
cursor opens on the first unfinished step and on the first unmigrated version
— where it stopped is where one usually wants to act.
Both views gain « q », quit without doing anything. A TUI needs Escape to do
something sane, and an escape hatch present in only one of the two views is
exactly the kind of divergence this split exists to prevent. execute_odoo_
upgrade returns immediately on it, writing nothing.
Verified on a 12->18 progression with steps 0-3 done and 2 of 6 bumps
migrated: identical context feeding both views, Enter on step 2 giving « 2 »,
Enter on the 15 bump giving « 4.15 », the c/n/r/q/Escape shortcuts, and every
answer producing the same progression through both paths — « 2 » leaving only
the state of steps 0 and 1, « 4.15 » resetting the clone list from the third
bump on so the half-migrated intermediate database gets rebuilt. « q » checked
to leave the progression file byte-identical.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-08-01 06:38:31 -04:00
|
|
|
|
for item in ctx["steps"]:
|
|
|
|
|
|
print(
|
|
|
|
|
|
f" [{item['step']}] {item['icon']} "
|
|
|
|
|
|
f"{item['label']:<44} {item['detail']}"
|
|
|
|
|
|
)
|
[IMP] migration: show the real state of each step and replay from any of them
The resume menu exposed internal key names: « Reuse database without state_4 »
tells nobody what will happen, and the most useful option -- keep the prepared
database, redo the version bumps -- was the least readable of the four.
It now prints where the migration actually stands: the zip, the database, the
target, and one line per step with its state. Step 4 reports how many version
bumps are migrated and names them, so « 0/6 · 13 14 15 16 17 18 » replaces a
list index nobody could interpret.
The four numbered options become:
[c] continue where it stopped (was: press enter)
[0-4] replay from that step (new: rewind to any step)
[n] new migration, erase everything (was: [1])
[r] keep the zip, ask everything (was: [4])
Rewinding drops « state_* » from the chosen step onward and keeps the rest:
config_* answers, the zip and the target version are decisions, not progress,
so they are no longer re-asked. state_0_search_missing_module is still forced
back to False, as the previous code did: it fills an in-memory dict the later
steps rely on. Old [2] is now « replay from 0 », old [3] « replay from 4 ».
First use of t() in this file; the new strings are translated in todo_i18n.py.
Verified against the real progression of the 12.0 -> 18.0 migration and a
synthetic one: for every step, the kept keys are exactly those of the earlier
steps, config and target survive, and the module search is forced again.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-08-01 02:53:17 -04:00
|
|
|
|
print()
|
|
|
|
|
|
print(f" [c] {t('Continue where it stopped')}")
|
|
|
|
|
|
print(
|
|
|
|
|
|
f" [0-4] {t('Replay from that step')}"
|
|
|
|
|
|
f" ({t('erases the progression of that step and the next ones')})"
|
|
|
|
|
|
)
|
[ADD] migration: a TUI resume screen, same first question as the prompt
The migration now opens with the same choice the QEMU deployment does — TUI
or line-by-line prompts — settled in advance by TODO > Configuration if you
want it to be. Choosing the TUI loads the saved progression and asks the very
same first question: where does this migration stand, and where do we resume?
prompt_resume was one function that rendered, read and decided at once, so a
second view would have meant a second copy of the decision. It is now three:
resume_context() the progression -> plain data (file, database, target,
steps with their icon and detail, version bumps)
print_resume() renders it on the terminal
apply_resume_answer() answer -> (progression, changed)
The TUI returns the SAME answer strings as the prompt — c, n, r, q, 0..4,
4.<version> — so apply_resume_answer stays the only place that decides what a
choice means. Neither view can drift into describing the migration
differently, because both render the same context.
In the TUI the steps are a table and the version bumps a list: Enter on either
replays from there, which is what « [0-4] » and « [4.N] » meant in text. The
cursor opens on the first unfinished step and on the first unmigrated version
— where it stopped is where one usually wants to act.
Both views gain « q », quit without doing anything. A TUI needs Escape to do
something sane, and an escape hatch present in only one of the two views is
exactly the kind of divergence this split exists to prevent. execute_odoo_
upgrade returns immediately on it, writing nothing.
Verified on a 12->18 progression with steps 0-3 done and 2 of 6 bumps
migrated: identical context feeding both views, Enter on step 2 giving « 2 »,
Enter on the 15 bump giving « 4.15 », the c/n/r/q/Escape shortcuts, and every
answer producing the same progression through both paths — « 2 » leaving only
the state of steps 0 and 1, « 4.15 » resetting the clone list from the third
bump on so the half-migrated intermediate database gets rebuilt. « q » checked
to leave the progression file byte-identical.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-08-01 06:38:31 -04:00
|
|
|
|
if ctx["versions"]:
|
|
|
|
|
|
versions = "/".join(str(v["version"]) for v in ctx["versions"])
|
[IMP] migration: replay a single version bump, and stop offering a doomed retry
When OpenUpgrade failed, todo_upgrade_execute offered the generic
« [1] to redo the command ». Replaying it re-ran OpenUpgrade on the database it
had just half migrated, which never recovers. That call now passes
wait_at_error=False, and the failure message explains that the clone step has
been reset so a relaunch DROPS and REBUILDS the intermediate database from the
previous version.
The resume menu gains « [4.N] »: replay the step 4 loop from one version bump.
Only the per-version lists are trimmed from that index, so earlier bumps stay
migrated and steps 0 to 3 are untouched. Resetting the clone entry is the whole
point: the intermediate database of a failed bump must be rebuilt, not upgraded
again. « [4] » still replays every bump.
The per-bump uninstall files were also read at step 1 only, for the source
version, so uninstall_module_list_odoo130_to_odoo140.txt existed in name but
was never read. The step 4 loop now reads the file of the bump it is about to
perform, merged with the answers already stored in the progression.
Verified against the real progression (13 and 14 migrated): the menu offers
13/14/15/16/17/18, and replaying from 14 turns every per-version list from
[True, True, False...] into [True, False, ...] while state_4_reach_open_upgrade
and steps 0-3 survive. A 13->14 list of 4 modules is read with its reasons.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-08-01 04:46:17 -04:00
|
|
|
|
print(
|
|
|
|
|
|
f" [4.N] {t('Replay the upgrade from version N')}"
|
[ADD] migration: a TUI resume screen, same first question as the prompt
The migration now opens with the same choice the QEMU deployment does — TUI
or line-by-line prompts — settled in advance by TODO > Configuration if you
want it to be. Choosing the TUI loads the saved progression and asks the very
same first question: where does this migration stand, and where do we resume?
prompt_resume was one function that rendered, read and decided at once, so a
second view would have meant a second copy of the decision. It is now three:
resume_context() the progression -> plain data (file, database, target,
steps with their icon and detail, version bumps)
print_resume() renders it on the terminal
apply_resume_answer() answer -> (progression, changed)
The TUI returns the SAME answer strings as the prompt — c, n, r, q, 0..4,
4.<version> — so apply_resume_answer stays the only place that decides what a
choice means. Neither view can drift into describing the migration
differently, because both render the same context.
In the TUI the steps are a table and the version bumps a list: Enter on either
replays from there, which is what « [0-4] » and « [4.N] » meant in text. The
cursor opens on the first unfinished step and on the first unmigrated version
— where it stopped is where one usually wants to act.
Both views gain « q », quit without doing anything. A TUI needs Escape to do
something sane, and an escape hatch present in only one of the two views is
exactly the kind of divergence this split exists to prevent. execute_odoo_
upgrade returns immediately on it, writing nothing.
Verified on a 12->18 progression with steps 0-3 done and 2 of 6 bumps
migrated: identical context feeding both views, Enter on step 2 giving « 2 »,
Enter on the 15 bump giving « 4.15 », the c/n/r/q/Escape shortcuts, and every
answer producing the same progression through both paths — « 2 » leaving only
the state of steps 0 and 1, « 4.15 » resetting the clone list from the third
bump on so the half-migrated intermediate database gets rebuilt. « q » checked
to leave the progression file byte-identical.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-08-01 06:38:31 -04:00
|
|
|
|
f" ({versions}) —"
|
[IMP] migration: replay a single version bump, and stop offering a doomed retry
When OpenUpgrade failed, todo_upgrade_execute offered the generic
« [1] to redo the command ». Replaying it re-ran OpenUpgrade on the database it
had just half migrated, which never recovers. That call now passes
wait_at_error=False, and the failure message explains that the clone step has
been reset so a relaunch DROPS and REBUILDS the intermediate database from the
previous version.
The resume menu gains « [4.N] »: replay the step 4 loop from one version bump.
Only the per-version lists are trimmed from that index, so earlier bumps stay
migrated and steps 0 to 3 are untouched. Resetting the clone entry is the whole
point: the intermediate database of a failed bump must be rebuilt, not upgraded
again. « [4] » still replays every bump.
The per-bump uninstall files were also read at step 1 only, for the source
version, so uninstall_module_list_odoo130_to_odoo140.txt existed in name but
was never read. The step 4 loop now reads the file of the bump it is about to
perform, merged with the answers already stored in the progression.
Verified against the real progression (13 and 14 migrated): the menu offers
13/14/15/16/17/18, and replaying from 14 turns every per-version list from
[True, True, False...] into [True, False, ...] while state_4_reach_open_upgrade
and steps 0-3 survive. A 13->14 list of 4 modules is read with its reasons.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-08-01 04:46:17 -04:00
|
|
|
|
f" {t('rebuilds the intermediate database')}"
|
|
|
|
|
|
)
|
[IMP] migration: show the real state of each step and replay from any of them
The resume menu exposed internal key names: « Reuse database without state_4 »
tells nobody what will happen, and the most useful option -- keep the prepared
database, redo the version bumps -- was the least readable of the four.
It now prints where the migration actually stands: the zip, the database, the
target, and one line per step with its state. Step 4 reports how many version
bumps are migrated and names them, so « 0/6 · 13 14 15 16 17 18 » replaces a
list index nobody could interpret.
The four numbered options become:
[c] continue where it stopped (was: press enter)
[0-4] replay from that step (new: rewind to any step)
[n] new migration, erase everything (was: [1])
[r] keep the zip, ask everything (was: [4])
Rewinding drops « state_* » from the chosen step onward and keeps the rest:
config_* answers, the zip and the target version are decisions, not progress,
so they are no longer re-asked. state_0_search_missing_module is still forced
back to False, as the previous code did: it fills an in-memory dict the later
steps rely on. Old [2] is now « replay from 0 », old [3] « replay from 4 ».
First use of t() in this file; the new strings are translated in todo_i18n.py.
Verified against the real progression of the 12.0 -> 18.0 migration and a
synthetic one: for every step, the kept keys are exactly those of the earlier
steps, config and target survive, and the module search is forced again.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-08-01 02:53:17 -04:00
|
|
|
|
print(f" [n] {t('New migration, erase everything')}")
|
|
|
|
|
|
print(f" [r] {t('Keep the zip only, ask every question again')}")
|
[ADD] migration: a TUI resume screen, same first question as the prompt
The migration now opens with the same choice the QEMU deployment does — TUI
or line-by-line prompts — settled in advance by TODO > Configuration if you
want it to be. Choosing the TUI loads the saved progression and asks the very
same first question: where does this migration stand, and where do we resume?
prompt_resume was one function that rendered, read and decided at once, so a
second view would have meant a second copy of the decision. It is now three:
resume_context() the progression -> plain data (file, database, target,
steps with their icon and detail, version bumps)
print_resume() renders it on the terminal
apply_resume_answer() answer -> (progression, changed)
The TUI returns the SAME answer strings as the prompt — c, n, r, q, 0..4,
4.<version> — so apply_resume_answer stays the only place that decides what a
choice means. Neither view can drift into describing the migration
differently, because both render the same context.
In the TUI the steps are a table and the version bumps a list: Enter on either
replays from there, which is what « [0-4] » and « [4.N] » meant in text. The
cursor opens on the first unfinished step and on the first unmigrated version
— where it stopped is where one usually wants to act.
Both views gain « q », quit without doing anything. A TUI needs Escape to do
something sane, and an escape hatch present in only one of the two views is
exactly the kind of divergence this split exists to prevent. execute_odoo_
upgrade returns immediately on it, writing nothing.
Verified on a 12->18 progression with steps 0-3 done and 2 of 6 bumps
migrated: identical context feeding both views, Enter on step 2 giving « 2 »,
Enter on the 15 bump giving « 4.15 », the c/n/r/q/Escape shortcuts, and every
answer producing the same progression through both paths — « 2 » leaving only
the state of steps 0 and 1, « 4.15 » resetting the clone list from the third
bump on so the half-migrated intermediate database gets rebuilt. « q » checked
to leave the progression file byte-identical.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-08-01 06:38:31 -04:00
|
|
|
|
print(f" [q] {t('Quit without doing anything')}")
|
|
|
|
|
|
|
|
|
|
|
|
def apply_resume_answer(self, old_dct_progression, answer, ctx):
|
|
|
|
|
|
"""Turn the answer into (progression, changed), or None to quit.
|
|
|
|
|
|
|
|
|
|
|
|
THE decision point, shared by both interfaces: the TUI returns the
|
|
|
|
|
|
same answer strings as the prompt, so this logic is written once.
|
|
|
|
|
|
"""
|
|
|
|
|
|
answer = (answer or "").strip().lower()
|
|
|
|
|
|
lst_version = [v["version"] for v in ctx["versions"]]
|
[IMP] migration: show the real state of each step and replay from any of them
The resume menu exposed internal key names: « Reuse database without state_4 »
tells nobody what will happen, and the most useful option -- keep the prepared
database, redo the version bumps -- was the least readable of the four.
It now prints where the migration actually stands: the zip, the database, the
target, and one line per step with its state. Step 4 reports how many version
bumps are migrated and names them, so « 0/6 · 13 14 15 16 17 18 » replaces a
list index nobody could interpret.
The four numbered options become:
[c] continue where it stopped (was: press enter)
[0-4] replay from that step (new: rewind to any step)
[n] new migration, erase everything (was: [1])
[r] keep the zip, ask everything (was: [4])
Rewinding drops « state_* » from the chosen step onward and keeps the rest:
config_* answers, the zip and the target version are decisions, not progress,
so they are no longer re-asked. state_0_search_missing_module is still forced
back to False, as the previous code did: it fills an in-memory dict the later
steps rely on. Old [2] is now « replay from 0 », old [3] « replay from 4 ».
First use of t() in this file; the new strings are translated in todo_i18n.py.
Verified against the real progression of the 12.0 -> 18.0 migration and a
synthetic one: for every step, the kept keys are exactly those of the earlier
steps, config and target survive, and the module search is forced again.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-08-01 02:53:17 -04:00
|
|
|
|
|
|
|
|
|
|
if answer in ("", "c"):
|
|
|
|
|
|
return old_dct_progression, False
|
[ADD] migration: a TUI resume screen, same first question as the prompt
The migration now opens with the same choice the QEMU deployment does — TUI
or line-by-line prompts — settled in advance by TODO > Configuration if you
want it to be. Choosing the TUI loads the saved progression and asks the very
same first question: where does this migration stand, and where do we resume?
prompt_resume was one function that rendered, read and decided at once, so a
second view would have meant a second copy of the decision. It is now three:
resume_context() the progression -> plain data (file, database, target,
steps with their icon and detail, version bumps)
print_resume() renders it on the terminal
apply_resume_answer() answer -> (progression, changed)
The TUI returns the SAME answer strings as the prompt — c, n, r, q, 0..4,
4.<version> — so apply_resume_answer stays the only place that decides what a
choice means. Neither view can drift into describing the migration
differently, because both render the same context.
In the TUI the steps are a table and the version bumps a list: Enter on either
replays from there, which is what « [0-4] » and « [4.N] » meant in text. The
cursor opens on the first unfinished step and on the first unmigrated version
— where it stopped is where one usually wants to act.
Both views gain « q », quit without doing anything. A TUI needs Escape to do
something sane, and an escape hatch present in only one of the two views is
exactly the kind of divergence this split exists to prevent. execute_odoo_
upgrade returns immediately on it, writing nothing.
Verified on a 12->18 progression with steps 0-3 done and 2 of 6 bumps
migrated: identical context feeding both views, Enter on step 2 giving « 2 »,
Enter on the 15 bump giving « 4.15 », the c/n/r/q/Escape shortcuts, and every
answer producing the same progression through both paths — « 2 » leaving only
the state of steps 0 and 1, « 4.15 » resetting the clone list from the third
bump on so the half-migrated intermediate database gets rebuilt. « q » checked
to leave the progression file byte-identical.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-08-01 06:38:31 -04:00
|
|
|
|
if answer == "q":
|
|
|
|
|
|
return None
|
[IMP] migration: show the real state of each step and replay from any of them
The resume menu exposed internal key names: « Reuse database without state_4 »
tells nobody what will happen, and the most useful option -- keep the prepared
database, redo the version bumps -- was the least readable of the four.
It now prints where the migration actually stands: the zip, the database, the
target, and one line per step with its state. Step 4 reports how many version
bumps are migrated and names them, so « 0/6 · 13 14 15 16 17 18 » replaces a
list index nobody could interpret.
The four numbered options become:
[c] continue where it stopped (was: press enter)
[0-4] replay from that step (new: rewind to any step)
[n] new migration, erase everything (was: [1])
[r] keep the zip, ask everything (was: [4])
Rewinding drops « state_* » from the chosen step onward and keeps the rest:
config_* answers, the zip and the target version are decisions, not progress,
so they are no longer re-asked. state_0_search_missing_module is still forced
back to False, as the previous code did: it fills an in-memory dict the later
steps rely on. Old [2] is now « replay from 0 », old [3] « replay from 4 ».
First use of t() in this file; the new strings are translated in todo_i18n.py.
Verified against the real progression of the 12.0 -> 18.0 migration and a
synthetic one: for every step, the kept keys are exactly those of the earlier
steps, config and target survive, and the module search is forced again.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-08-01 02:53:17 -04:00
|
|
|
|
if answer == "n":
|
|
|
|
|
|
return {}, True
|
|
|
|
|
|
if answer == "r":
|
|
|
|
|
|
return {
|
|
|
|
|
|
"migration_file": old_dct_progression.get("migration_file"),
|
|
|
|
|
|
"date_create": old_dct_progression.get("date_create"),
|
|
|
|
|
|
}, True
|
[IMP] migration: replay a single version bump, and stop offering a doomed retry
When OpenUpgrade failed, todo_upgrade_execute offered the generic
« [1] to redo the command ». Replaying it re-ran OpenUpgrade on the database it
had just half migrated, which never recovers. That call now passes
wait_at_error=False, and the failure message explains that the clone step has
been reset so a relaunch DROPS and REBUILDS the intermediate database from the
previous version.
The resume menu gains « [4.N] »: replay the step 4 loop from one version bump.
Only the per-version lists are trimmed from that index, so earlier bumps stay
migrated and steps 0 to 3 are untouched. Resetting the clone entry is the whole
point: the intermediate database of a failed bump must be rebuilt, not upgraded
again. « [4] » still replays every bump.
The per-bump uninstall files were also read at step 1 only, for the source
version, so uninstall_module_list_odoo130_to_odoo140.txt existed in name but
was never read. The step 4 loop now reads the file of the bump it is about to
perform, merged with the answers already stored in the progression.
Verified against the real progression (13 and 14 migrated): the menu offers
13/14/15/16/17/18, and replaying from 14 turns every per-version list from
[True, True, False...] into [True, False, ...] while state_4_reach_open_upgrade
and steps 0-3 survive. A 13->14 list of 4 modules is read with its reasons.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-08-01 04:46:17 -04:00
|
|
|
|
if answer.startswith("4.") and lst_version:
|
|
|
|
|
|
target = answer.split(".", 1)[1].strip()
|
|
|
|
|
|
if target.isdigit() and int(target) in lst_version:
|
|
|
|
|
|
return (
|
|
|
|
|
|
self.rewind_version_bump(
|
|
|
|
|
|
old_dct_progression, lst_version.index(int(target))
|
|
|
|
|
|
),
|
|
|
|
|
|
True,
|
|
|
|
|
|
)
|
|
|
|
|
|
print(f"⚠️ {t('Unknown version')} : {target}")
|
|
|
|
|
|
return old_dct_progression, False
|
[IMP] migration: show the real state of each step and replay from any of them
The resume menu exposed internal key names: « Reuse database without state_4 »
tells nobody what will happen, and the most useful option -- keep the prepared
database, redo the version bumps -- was the least readable of the four.
It now prints where the migration actually stands: the zip, the database, the
target, and one line per step with its state. Step 4 reports how many version
bumps are migrated and names them, so « 0/6 · 13 14 15 16 17 18 » replaces a
list index nobody could interpret.
The four numbered options become:
[c] continue where it stopped (was: press enter)
[0-4] replay from that step (new: rewind to any step)
[n] new migration, erase everything (was: [1])
[r] keep the zip, ask everything (was: [4])
Rewinding drops « state_* » from the chosen step onward and keeps the rest:
config_* answers, the zip and the target version are decisions, not progress,
so they are no longer re-asked. state_0_search_missing_module is still forced
back to False, as the previous code did: it fills an in-memory dict the later
steps rely on. Old [2] is now « replay from 0 », old [3] « replay from 4 ».
First use of t() in this file; the new strings are translated in todo_i18n.py.
Verified against the real progression of the 12.0 -> 18.0 migration and a
synthetic one: for every step, the kept keys are exactly those of the earlier
steps, config and target survive, and the module search is forced again.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-08-01 02:53:17 -04:00
|
|
|
|
if answer.isdigit() and 0 <= int(answer) <= MIGRATION_STEP[-1][0]:
|
|
|
|
|
|
return (
|
|
|
|
|
|
self.rewind_progression(old_dct_progression, int(answer)),
|
|
|
|
|
|
True,
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
print(f"⚠️ {t('Unknown choice, continuing where it stopped')}.")
|
|
|
|
|
|
return old_dct_progression, False
|
|
|
|
|
|
|
[ADD] migration: a TUI resume screen, same first question as the prompt
The migration now opens with the same choice the QEMU deployment does — TUI
or line-by-line prompts — settled in advance by TODO > Configuration if you
want it to be. Choosing the TUI loads the saved progression and asks the very
same first question: where does this migration stand, and where do we resume?
prompt_resume was one function that rendered, read and decided at once, so a
second view would have meant a second copy of the decision. It is now three:
resume_context() the progression -> plain data (file, database, target,
steps with their icon and detail, version bumps)
print_resume() renders it on the terminal
apply_resume_answer() answer -> (progression, changed)
The TUI returns the SAME answer strings as the prompt — c, n, r, q, 0..4,
4.<version> — so apply_resume_answer stays the only place that decides what a
choice means. Neither view can drift into describing the migration
differently, because both render the same context.
In the TUI the steps are a table and the version bumps a list: Enter on either
replays from there, which is what « [0-4] » and « [4.N] » meant in text. The
cursor opens on the first unfinished step and on the first unmigrated version
— where it stopped is where one usually wants to act.
Both views gain « q », quit without doing anything. A TUI needs Escape to do
something sane, and an escape hatch present in only one of the two views is
exactly the kind of divergence this split exists to prevent. execute_odoo_
upgrade returns immediately on it, writing nothing.
Verified on a 12->18 progression with steps 0-3 done and 2 of 6 bumps
migrated: identical context feeding both views, Enter on step 2 giving « 2 »,
Enter on the 15 bump giving « 4.15 », the c/n/r/q/Escape shortcuts, and every
answer producing the same progression through both paths — « 2 » leaving only
the state of steps 0 and 1, « 4.15 » resetting the clone list from the third
bump on so the half-migrated intermediate database gets rebuilt. « q » checked
to leave the progression file byte-identical.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-08-01 06:38:31 -04:00
|
|
|
|
def prompt_resume(self, old_dct_progression, use_tui=False):
|
|
|
|
|
|
"""Show where the migration stands and ask what to do next.
|
|
|
|
|
|
|
|
|
|
|
|
Returns (progression, changed), or None if the user quits. The old
|
|
|
|
|
|
menu exposed internal key names (« Reuse database without state_4 »),
|
|
|
|
|
|
which said nothing about what would happen; this shows the real state
|
|
|
|
|
|
of every step and lets the user replay from any of them.
|
|
|
|
|
|
"""
|
|
|
|
|
|
ctx = self.resume_context(old_dct_progression)
|
|
|
|
|
|
answer = None
|
|
|
|
|
|
if use_tui:
|
|
|
|
|
|
answer = self.resume_tui(ctx)
|
|
|
|
|
|
if answer is None:
|
|
|
|
|
|
self.print_resume(ctx)
|
|
|
|
|
|
answer = input(f"💬 {t('Your choice')} : ")
|
|
|
|
|
|
return self.apply_resume_answer(old_dct_progression, answer, ctx)
|
|
|
|
|
|
|
|
|
|
|
|
@staticmethod
|
|
|
|
|
|
def ask_ui():
|
|
|
|
|
|
"""Interface of the migration: TUI or line-by-line prompts.
|
|
|
|
|
|
|
|
|
|
|
|
The preference can settle it in advance (TODO > Configuration);
|
|
|
|
|
|
« ask » asks. Same contract as the QEMU deployment.
|
|
|
|
|
|
"""
|
|
|
|
|
|
try:
|
|
|
|
|
|
from script.todo import todo_prefs
|
|
|
|
|
|
|
|
|
|
|
|
pref = todo_prefs.get("migration_ui")
|
|
|
|
|
|
except Exception:
|
|
|
|
|
|
pref = "ask"
|
|
|
|
|
|
if pref in ("tui", "cli"):
|
|
|
|
|
|
return pref
|
|
|
|
|
|
print(f"\n{t('Interface:')}")
|
|
|
|
|
|
print(f" [1] {t('TUI form')} *")
|
|
|
|
|
|
print(f" [2] {t('Classic questions (line by line)')}")
|
|
|
|
|
|
print(f" {t('(change the default in TODO > Configuration)')}")
|
|
|
|
|
|
answer = input(t("Choice (1-2, default 1): ")).strip()
|
|
|
|
|
|
return "cli" if answer == "2" else "tui"
|
|
|
|
|
|
|
|
|
|
|
|
@staticmethod
|
|
|
|
|
|
def resume_tui(ctx):
|
|
|
|
|
|
"""Resume screen as a TUI. Returns the SAME answer strings as the
|
|
|
|
|
|
prompt, or None when textual is missing (fall back to the prompt)."""
|
|
|
|
|
|
try:
|
|
|
|
|
|
from script.todo.migration_form import run_resume_tui
|
|
|
|
|
|
|
|
|
|
|
|
return run_resume_tui(ctx)
|
|
|
|
|
|
except ImportError:
|
|
|
|
|
|
print(t("Install textual for the dashboard (pip)."))
|
|
|
|
|
|
return None
|
|
|
|
|
|
|
[IMP] migration: replay a single version bump, and stop offering a doomed retry
When OpenUpgrade failed, todo_upgrade_execute offered the generic
« [1] to redo the command ». Replaying it re-ran OpenUpgrade on the database it
had just half migrated, which never recovers. That call now passes
wait_at_error=False, and the failure message explains that the clone step has
been reset so a relaunch DROPS and REBUILDS the intermediate database from the
previous version.
The resume menu gains « [4.N] »: replay the step 4 loop from one version bump.
Only the per-version lists are trimmed from that index, so earlier bumps stay
migrated and steps 0 to 3 are untouched. Resetting the clone entry is the whole
point: the intermediate database of a failed bump must be rebuilt, not upgraded
again. « [4] » still replays every bump.
The per-bump uninstall files were also read at step 1 only, for the source
version, so uninstall_module_list_odoo130_to_odoo140.txt existed in name but
was never read. The step 4 loop now reads the file of the bump it is about to
perform, merged with the answers already stored in the progression.
Verified against the real progression (13 and 14 migrated): the menu offers
13/14/15/16/17/18, and replaying from 14 turns every per-version list from
[True, True, False...] into [True, False, ...] while state_4_reach_open_upgrade
and steps 0-3 survive. A 13->14 list of 4 modules is read with its reasons.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-08-01 04:46:17 -04:00
|
|
|
|
@staticmethod
|
|
|
|
|
|
def version_bumps(dct_progression):
|
|
|
|
|
|
"""Odoo versions the step 4 loop walks through, e.g. [13, 14, ..., 18].
|
|
|
|
|
|
|
|
|
|
|
|
The per-version lists all end on the target, so the first bump is
|
|
|
|
|
|
« target - len + 1 ». Returns [] when step 4 has not started.
|
|
|
|
|
|
"""
|
|
|
|
|
|
total = max(
|
|
|
|
|
|
[
|
|
|
|
|
|
len(value)
|
|
|
|
|
|
for key, value in dct_progression.items()
|
|
|
|
|
|
if key.startswith("state_4_")
|
|
|
|
|
|
and key.endswith("_odoo_lst")
|
|
|
|
|
|
and isinstance(value, list)
|
|
|
|
|
|
]
|
|
|
|
|
|
or [0]
|
|
|
|
|
|
)
|
|
|
|
|
|
if not total:
|
|
|
|
|
|
return []
|
|
|
|
|
|
try:
|
|
|
|
|
|
last = int(float(dct_progression["target_odoo_version"]))
|
|
|
|
|
|
except (KeyError, TypeError, ValueError):
|
|
|
|
|
|
return []
|
|
|
|
|
|
return list(range(last - total + 1, last + 1))
|
|
|
|
|
|
|
|
|
|
|
|
@staticmethod
|
|
|
|
|
|
def rewind_version_bump(old_dct_progression, index):
|
|
|
|
|
|
"""Replay the step 4 loop from one version bump onwards.
|
|
|
|
|
|
|
|
|
|
|
|
Only the per-version lists are trimmed from `index`: earlier bumps stay
|
|
|
|
|
|
migrated and steps 0 to 3 are untouched. Resetting the clone entry is
|
|
|
|
|
|
the point — the intermediate database of a failed bump is half
|
|
|
|
|
|
migrated, so it must be dropped and rebuilt from the previous version
|
|
|
|
|
|
rather than upgraded again.
|
|
|
|
|
|
"""
|
|
|
|
|
|
dct_kept = dict(old_dct_progression)
|
|
|
|
|
|
for key, value in old_dct_progression.items():
|
|
|
|
|
|
if (
|
|
|
|
|
|
key.startswith("state_4_")
|
|
|
|
|
|
and isinstance(value, list)
|
|
|
|
|
|
and key.endswith(("_odoo_lst", "_module"))
|
|
|
|
|
|
):
|
|
|
|
|
|
dct_kept[key] = [
|
|
|
|
|
|
item if i < index else False
|
|
|
|
|
|
for i, item in enumerate(value)
|
|
|
|
|
|
]
|
|
|
|
|
|
return dct_kept
|
|
|
|
|
|
|
[IMP] migration: show the real state of each step and replay from any of them
The resume menu exposed internal key names: « Reuse database without state_4 »
tells nobody what will happen, and the most useful option -- keep the prepared
database, redo the version bumps -- was the least readable of the four.
It now prints where the migration actually stands: the zip, the database, the
target, and one line per step with its state. Step 4 reports how many version
bumps are migrated and names them, so « 0/6 · 13 14 15 16 17 18 » replaces a
list index nobody could interpret.
The four numbered options become:
[c] continue where it stopped (was: press enter)
[0-4] replay from that step (new: rewind to any step)
[n] new migration, erase everything (was: [1])
[r] keep the zip, ask everything (was: [4])
Rewinding drops « state_* » from the chosen step onward and keeps the rest:
config_* answers, the zip and the target version are decisions, not progress,
so they are no longer re-asked. state_0_search_missing_module is still forced
back to False, as the previous code did: it fills an in-memory dict the later
steps rely on. Old [2] is now « replay from 0 », old [3] « replay from 4 ».
First use of t() in this file; the new strings are translated in todo_i18n.py.
Verified against the real progression of the 12.0 -> 18.0 migration and a
synthetic one: for every step, the kept keys are exactly those of the earlier
steps, config and target survive, and the module search is forced again.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-08-01 02:53:17 -04:00
|
|
|
|
@staticmethod
|
|
|
|
|
|
def rewind_progression(old_dct_progression, step):
|
|
|
|
|
|
"""Drop the progression of `step` and of every later step.
|
|
|
|
|
|
|
|
|
|
|
|
Configuration answers (config_*), the zip and the target version are
|
|
|
|
|
|
kept: they are decisions, not progress. Only « state_* » is rewound.
|
|
|
|
|
|
"""
|
|
|
|
|
|
dct_kept = {}
|
|
|
|
|
|
for key, value in old_dct_progression.items():
|
|
|
|
|
|
if not key.startswith("state_"):
|
|
|
|
|
|
dct_kept[key] = value
|
|
|
|
|
|
continue
|
|
|
|
|
|
index = key[len("state_") :].split("_", 1)[0]
|
|
|
|
|
|
if index.isdigit() and int(index) < step:
|
|
|
|
|
|
dct_kept[key] = value
|
|
|
|
|
|
# The module search fills an in-memory dict the later steps rely on;
|
|
|
|
|
|
# it must run again even when step 0 itself is kept.
|
|
|
|
|
|
dct_kept["state_0_search_missing_module"] = False
|
|
|
|
|
|
print(
|
|
|
|
|
|
f"⏪ {t('Replaying from step')} {step} —"
|
|
|
|
|
|
f" {t(dict(MIGRATION_STEP)[step])}"
|
|
|
|
|
|
)
|
|
|
|
|
|
return dct_kept
|
|
|
|
|
|
|
2025-10-31 01:10:54 -04:00
|
|
|
|
def on_file_selected(self, file_path):
|
|
|
|
|
|
self.file_path = file_path
|
|
|
|
|
|
todo_file_browser.exit_program()
|
|
|
|
|
|
|
|
|
|
|
|
def on_dir_selected(self, dir_path):
|
|
|
|
|
|
self.dir_path = dir_path
|
|
|
|
|
|
todo_file_browser.exit_program()
|
|
|
|
|
|
|
|
|
|
|
|
def execute_module_upgrade(self):
|
|
|
|
|
|
print("Welcome to Odoo module upgrade processus with ERPLibre 🤖")
|
|
|
|
|
|
|
2025-11-11 00:05:33 -05:00
|
|
|
|
if os.path.exists(UPGRADE_DATABASE_CONFIG_LOG):
|
|
|
|
|
|
with open(UPGRADE_DATABASE_CONFIG_LOG, "r") as f:
|
|
|
|
|
|
try:
|
|
|
|
|
|
old_dct_progression = json.load(f)
|
|
|
|
|
|
self.dct_progression = old_dct_progression
|
|
|
|
|
|
self.lst_command_executed = old_dct_progression.get(
|
|
|
|
|
|
"command_executed"
|
|
|
|
|
|
)
|
|
|
|
|
|
except json.decoder.JSONDecodeError:
|
|
|
|
|
|
print(
|
|
|
|
|
|
f'⚠️ The config file "{UPGRADE_DATABASE_CONFIG_LOG}" is invalid, ignore it.'
|
|
|
|
|
|
)
|
2025-10-31 01:10:54 -04:00
|
|
|
|
|
|
|
|
|
|
print(
|
|
|
|
|
|
"Migrate a directory repo to migrate all module, or select a directory module."
|
|
|
|
|
|
)
|
|
|
|
|
|
print("[m] Migrate one module")
|
|
|
|
|
|
print("[r] Migrate one repo")
|
|
|
|
|
|
print("[] Directory browser")
|
|
|
|
|
|
|
|
|
|
|
|
lst_dir_path = []
|
|
|
|
|
|
from_version = 0
|
|
|
|
|
|
to_version = 0
|
|
|
|
|
|
|
|
|
|
|
|
status = input("💬 What do you choose : ").strip().lower()
|
|
|
|
|
|
if status == "m":
|
|
|
|
|
|
print("[p] Path (default)")
|
|
|
|
|
|
print("[n] Name")
|
|
|
|
|
|
|
|
|
|
|
|
status = input("💬 What do you choose : ").strip().lower()
|
|
|
|
|
|
if status == "n":
|
|
|
|
|
|
module_name = input("💬 Module name : ").strip().lower()
|
|
|
|
|
|
from_version = int(
|
|
|
|
|
|
input("💬 From odoo version (12 to 18) : ").strip()
|
|
|
|
|
|
)
|
|
|
|
|
|
to_version = int(
|
|
|
|
|
|
input("💬 To odoo version (12 to 18) : ").strip()
|
|
|
|
|
|
)
|
|
|
|
|
|
self.switch_odoo(from_version)
|
|
|
|
|
|
# TODO detect path
|
|
|
|
|
|
(
|
|
|
|
|
|
lst_module_missing,
|
|
|
|
|
|
lst_module_duplicate,
|
|
|
|
|
|
lst_module_exist,
|
|
|
|
|
|
lst_module_error,
|
|
|
|
|
|
) = self.check_addons_exist([module_name], get_all_info=True)
|
|
|
|
|
|
if (
|
|
|
|
|
|
lst_module_missing
|
|
|
|
|
|
or lst_module_duplicate
|
|
|
|
|
|
or lst_module_error
|
|
|
|
|
|
):
|
|
|
|
|
|
if lst_module_missing:
|
|
|
|
|
|
print(f"Missing list : {lst_module_missing}")
|
|
|
|
|
|
if lst_module_duplicate:
|
|
|
|
|
|
print(f"Duplicate list : {lst_module_duplicate}")
|
|
|
|
|
|
if lst_module_error:
|
|
|
|
|
|
print(f"Error list : {lst_module_error}")
|
|
|
|
|
|
return
|
|
|
|
|
|
lst_dir_path.extend(lst_module_exist)
|
|
|
|
|
|
else:
|
|
|
|
|
|
self.dir_path = input("💬 Path : ").strip()
|
|
|
|
|
|
elif status == "r":
|
|
|
|
|
|
self.dir_path = input("💬 Path : ").strip()
|
|
|
|
|
|
else:
|
|
|
|
|
|
self.dir_path = None
|
|
|
|
|
|
|
|
|
|
|
|
if not self.dir_path and not lst_dir_path:
|
|
|
|
|
|
initial_dir = os.getcwd()
|
|
|
|
|
|
|
|
|
|
|
|
file_browser = todo_file_browser.FileBrowser(
|
|
|
|
|
|
initial_dir, self.on_dir_selected, open_dir=True
|
|
|
|
|
|
)
|
|
|
|
|
|
file_browser.run_main_frame()
|
|
|
|
|
|
|
|
|
|
|
|
if not lst_dir_path and not os.path.exists(self.dir_path):
|
|
|
|
|
|
_logger.error(f"Path '{self.dir_path}' not exists.")
|
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
|
|
if not from_version:
|
|
|
|
|
|
from_version = int(
|
|
|
|
|
|
input("💬 From odoo version (12 to 18) : ").strip()
|
|
|
|
|
|
)
|
|
|
|
|
|
if not to_version:
|
|
|
|
|
|
to_version = int(input("💬 To odoo version (12 to 18) : ").strip())
|
|
|
|
|
|
|
|
|
|
|
|
# TODO ask option direct migration
|
|
|
|
|
|
|
|
|
|
|
|
lst_version_to_update = [
|
|
|
|
|
|
(a, a + 1) for a in range(from_version, to_version)
|
|
|
|
|
|
]
|
|
|
|
|
|
# lst_version_to_update = [(from_version, to_version)]
|
|
|
|
|
|
# for actual_version in range(from_version, to_version):
|
|
|
|
|
|
# next_version = actual_version + 1
|
|
|
|
|
|
for actual_version, next_version in lst_version_to_update:
|
|
|
|
|
|
set_path_migrate_addons = set()
|
|
|
|
|
|
|
|
|
|
|
|
if not lst_dir_path:
|
|
|
|
|
|
if os.path.exists(
|
|
|
|
|
|
os.path.join(self.dir_path, "__manifest__.py")
|
|
|
|
|
|
):
|
|
|
|
|
|
lst_dir_path = [
|
|
|
|
|
|
[os.path.basename(self.dir_path), self.dir_path]
|
|
|
|
|
|
]
|
|
|
|
|
|
else:
|
|
|
|
|
|
lst_dir_path = [
|
|
|
|
|
|
[a, os.path.join(self.dir_path, a)]
|
|
|
|
|
|
for a in os.listdir(self.dir_path)
|
|
|
|
|
|
if os.path.exists(
|
|
|
|
|
|
os.path.join(self.dir_path, a, "__manifest__.py")
|
|
|
|
|
|
)
|
|
|
|
|
|
]
|
|
|
|
|
|
|
|
|
|
|
|
lst_path_git_clone_migrate = []
|
|
|
|
|
|
lst_module_to_migrate_all = []
|
|
|
|
|
|
for dir_path in lst_dir_path:
|
|
|
|
|
|
source_manifest_path = os.path.join(
|
|
|
|
|
|
dir_path[1], "__manifest__.py"
|
|
|
|
|
|
).replace(f"odoo{from_version}.0", f"odoo{actual_version}.0")
|
|
|
|
|
|
|
|
|
|
|
|
is_dir_module = os.path.exists(source_manifest_path)
|
|
|
|
|
|
if not is_dir_module:
|
|
|
|
|
|
continue
|
|
|
|
|
|
source_module_path = dir_path[1].replace(
|
|
|
|
|
|
f"odoo{from_version}.0", f"odoo{actual_version}.0"
|
|
|
|
|
|
)
|
|
|
|
|
|
source_addons_path = os.path.dirname(dir_path[1]).replace(
|
|
|
|
|
|
f"odoo{from_version}.0", f"odoo{actual_version}.0"
|
|
|
|
|
|
)
|
|
|
|
|
|
target_module_path = source_module_path.replace(
|
|
|
|
|
|
f"odoo{actual_version}.0", f"odoo{next_version}.0"
|
|
|
|
|
|
)
|
|
|
|
|
|
target_manifest_path = source_manifest_path.replace(
|
|
|
|
|
|
f"odoo{actual_version}.0", f"odoo{next_version}.0"
|
|
|
|
|
|
)
|
|
|
|
|
|
target_addons_path = source_addons_path.replace(
|
|
|
|
|
|
f"odoo{actual_version}.0", f"odoo{next_version}.0"
|
|
|
|
|
|
)
|
|
|
|
|
|
dct_module = {
|
|
|
|
|
|
"source_module_path": source_module_path,
|
|
|
|
|
|
"source_manifest_path": source_manifest_path,
|
|
|
|
|
|
"source_addons_path": source_addons_path,
|
|
|
|
|
|
"target_module_path": target_module_path,
|
|
|
|
|
|
"target_manifest_path": target_manifest_path,
|
|
|
|
|
|
"target_addons_path": target_addons_path,
|
|
|
|
|
|
"module_name": dir_path[0],
|
|
|
|
|
|
"source_version_odoo": actual_version,
|
|
|
|
|
|
"target_version_odoo": next_version,
|
|
|
|
|
|
}
|
|
|
|
|
|
lst_module_to_migrate_all.append(dct_module)
|
|
|
|
|
|
set_path_migrate_addons.add(target_addons_path)
|
|
|
|
|
|
# lst_path_git_clone_migrate.append(target_addons_path)
|
|
|
|
|
|
# TODO auto detect version from
|
|
|
|
|
|
# TODO if detect from directory, check from repo list
|
|
|
|
|
|
# TODO detect from manifest version
|
|
|
|
|
|
|
|
|
|
|
|
self.switch_odoo(next_version)
|
|
|
|
|
|
self.install_OCA_odoo_module_migrator()
|
|
|
|
|
|
|
|
|
|
|
|
self.internal_module_upgrade(
|
|
|
|
|
|
next_version,
|
|
|
|
|
|
lst_module_to_migrate_all,
|
|
|
|
|
|
lst_path_git_clone_migrate,
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
for commit_path in set_path_migrate_addons:
|
|
|
|
|
|
cmd = f"./script/code/git_commit_migration_addons_path.py --path {commit_path} --odoo_version {next_version}.0"
|
|
|
|
|
|
self.todo_upgrade_execute(cmd)
|
|
|
|
|
|
print(set_path_migrate_addons)
|
|
|
|
|
|
status = input(
|
|
|
|
|
|
f"💬 Please validate git commit on repos, press to continue : "
|
|
|
|
|
|
).strip()
|
|
|
|
|
|
|
|
|
|
|
|
def internal_module_upgrade(
|
|
|
|
|
|
self,
|
|
|
|
|
|
next_version,
|
|
|
|
|
|
lst_module_to_migrate_all,
|
|
|
|
|
|
lst_path_git_clone_migrate,
|
|
|
|
|
|
):
|
|
|
|
|
|
has_cmd = False
|
|
|
|
|
|
cmd_parallel = "parallel :::"
|
|
|
|
|
|
for dct_module in lst_module_to_migrate_all:
|
|
|
|
|
|
target_addons_path = dct_module.get("target_addons_path")
|
|
|
|
|
|
source_addons_path = dct_module.get("source_addons_path")
|
|
|
|
|
|
module_name = dct_module.get("module_name")
|
|
|
|
|
|
source_version_odoo = f'{dct_module.get("source_version_odoo")}.0'
|
|
|
|
|
|
target_version_odoo = f'{dct_module.get("target_version_odoo")}.0'
|
|
|
|
|
|
source_module_path_to_copy = dct_module.get("source_module_path")
|
|
|
|
|
|
# Prepare git environment for target
|
|
|
|
|
|
if target_addons_path not in lst_path_git_clone_migrate:
|
|
|
|
|
|
lst_path_git_clone_migrate.append(target_addons_path)
|
|
|
|
|
|
self.check_and_clone_source_to_target_migration_code(
|
|
|
|
|
|
next_version,
|
|
|
|
|
|
source_addons_path,
|
|
|
|
|
|
target_addons_path,
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
cmd_migration = (
|
|
|
|
|
|
f"echo 'odoo_module_migrate {module_name}' && "
|
|
|
|
|
|
f"cp -r {source_module_path_to_copy} {target_addons_path} && "
|
|
|
|
|
|
f"cd {PATH_OCA_ODOO_MODULE_MIGRATOR} && "
|
|
|
|
|
|
f"source {VENV_NAME_MODULE_MIGRATOR}/bin/activate && "
|
|
|
|
|
|
f"python -m odoo_module_migrate --directory {target_addons_path} --modules {module_name} "
|
|
|
|
|
|
f"--init-version-name {source_version_odoo} --target-version-name {target_version_odoo} "
|
|
|
|
|
|
f"--no-commit && "
|
|
|
|
|
|
f"cd ~- "
|
|
|
|
|
|
# f"cp -r {source_module_path_to_copy} {target_module_path_to_copy} && "
|
|
|
|
|
|
# f"cd {target_module_path_to_copy} && git commit -am '[MIG] {module_name}: Migration to {target_version_odoo}' && cd ~-"
|
|
|
|
|
|
)
|
|
|
|
|
|
cmd_parallel += f' "{cmd_migration}"'
|
|
|
|
|
|
has_cmd = True
|
|
|
|
|
|
|
|
|
|
|
|
if lst_module_to_migrate_all:
|
|
|
|
|
|
if has_cmd:
|
|
|
|
|
|
self.todo_upgrade_execute(cmd_parallel)
|
|
|
|
|
|
print("List of path with migrate code :")
|
|
|
|
|
|
print(lst_path_git_clone_migrate)
|
|
|
|
|
|
print("ℹ To show repo status :\nmake repo_show_status")
|
|
|
|
|
|
input("💬 Check migration code, press to continue : ")
|
|
|
|
|
|
|
|
|
|
|
|
# source_module_path = dct_module_result.get(
|
|
|
|
|
|
# "source_module_path"
|
|
|
|
|
|
# )
|
|
|
|
|
|
# if not source_module_path:
|
|
|
|
|
|
# _logger.error(
|
|
|
|
|
|
# f"Missing source module path '{source_module_path}'"
|
|
|
|
|
|
# )
|
|
|
|
|
|
# else:
|
|
|
|
|
|
# if os.path.exists(
|
|
|
|
|
|
# os.path.join(source_module_path, ".git")
|
|
|
|
|
|
# ):
|
|
|
|
|
|
# self.todo_upgrade_execute(
|
|
|
|
|
|
# f"cd '{source_module_path}' && git stash && cd ~-",
|
|
|
|
|
|
# )
|
|
|
|
|
|
#
|
|
|
|
|
|
# target_module_path = dct_module_result.get(
|
|
|
|
|
|
# "target_module_path"
|
|
|
|
|
|
# )
|
|
|
|
|
|
# if not target_module_path:
|
|
|
|
|
|
# _logger.error(
|
|
|
|
|
|
# f"Missing target module path '{target_module_path}'"
|
|
|
|
|
|
# )
|
|
|
|
|
|
# else:
|
|
|
|
|
|
# # TODO check if has file to commit
|
|
|
|
|
|
# self.todo_upgrade_execute(
|
|
|
|
|
|
# f"cd '{target_module_path}' && git commit -am '[MIG] {len(lst_module_to_migrate_all)} modules: Migration to {next_version}' && cd ~-",
|
|
|
|
|
|
# )
|
|
|
|
|
|
|
|
|
|
|
|
# TODO copie to next odoo version
|
|
|
|
|
|
# do commit and continue
|
|
|
|
|
|
# continue migration to loop
|
|
|
|
|
|
|
|
|
|
|
|
if next_version in [18]:
|
|
|
|
|
|
# TODO need odoo 18, validate python version without switch
|
|
|
|
|
|
status = input(
|
|
|
|
|
|
f"💬 Please validate repo is ready to run upgrade views_migration_18, press to continue : "
|
|
|
|
|
|
).strip()
|
|
|
|
|
|
# Apply modification with views_migration_18
|
|
|
|
|
|
has_cmd = False
|
|
|
|
|
|
# cmd_serial = ""
|
|
|
|
|
|
cmd_parallel = "parallel :::"
|
|
|
|
|
|
for path_git_clone_migrate in lst_path_git_clone_migrate:
|
|
|
|
|
|
cmd_migration = (
|
|
|
|
|
|
f"echo 'views_migration_18 {path_git_clone_migrate}' && "
|
|
|
|
|
|
f"./.venv.odoo18.0_python3.12.10/bin/python ./script/code/odoo_upgrade_code_with_dir_module.py --path {path_git_clone_migrate}"
|
|
|
|
|
|
)
|
|
|
|
|
|
cmd_parallel += f' "{cmd_migration}"'
|
|
|
|
|
|
# cmd_serial += f"{cmd_migration};"
|
|
|
|
|
|
has_cmd = True
|
|
|
|
|
|
|
|
|
|
|
|
if has_cmd:
|
|
|
|
|
|
# self.todo_upgrade_execute(
|
|
|
|
|
|
# cmd_serial
|
|
|
|
|
|
# )
|
|
|
|
|
|
self.todo_upgrade_execute(cmd_parallel)
|
|
|
|
|
|
print("List of module with migration 18 :")
|
|
|
|
|
|
print(lst_module_to_migrate_all)
|
|
|
|
|
|
print("ℹ To show repo status :\nmake repo_show_status")
|
|
|
|
|
|
input("💬 Check migration 18 code, press to continue : ")
|
|
|
|
|
|
|
|
|
|
|
|
if next_version == 17:
|
|
|
|
|
|
status = input(
|
|
|
|
|
|
f"💬 Please validate repo is ready to run upgrade views_migration_17, press to continue : "
|
|
|
|
|
|
).strip()
|
|
|
|
|
|
# Apply modification with views_migration_17
|
|
|
|
|
|
has_cmd = False
|
|
|
|
|
|
cmd_serial = ""
|
|
|
|
|
|
cmd_parallel = "parallel :::"
|
|
|
|
|
|
for dct_module in lst_module_to_migrate_all:
|
|
|
|
|
|
database_migration_17_name = (
|
|
|
|
|
|
f"migration_odoo_{next_version}_{str(uuid4())[:6]}"
|
|
|
|
|
|
)
|
|
|
|
|
|
module_name = dct_module.get("module_name")
|
|
|
|
|
|
cmd_migration = (
|
|
|
|
|
|
f"echo 'views_migration_17 {module_name}' && "
|
|
|
|
|
|
f"./run.sh -d {database_migration_17_name} -i {module_name} --load=base,web,views_migration_17 --dev upgrade --no-http --stop-after-init"
|
|
|
|
|
|
)
|
|
|
|
|
|
cmd_parallel += f' "{cmd_migration}"'
|
|
|
|
|
|
cmd_serial += f"{cmd_migration};"
|
|
|
|
|
|
has_cmd = True
|
|
|
|
|
|
|
|
|
|
|
|
if has_cmd:
|
|
|
|
|
|
# self.todo_upgrade_execute(
|
|
|
|
|
|
# cmd_serial
|
|
|
|
|
|
# )
|
|
|
|
|
|
self.todo_upgrade_execute(cmd_parallel)
|
|
|
|
|
|
print("List of module with migration 17 :")
|
|
|
|
|
|
print(lst_module_to_migrate_all)
|
|
|
|
|
|
print("ℹ To show repo status :\nmake repo_show_status")
|
|
|
|
|
|
input("💬 Check migration 17 code, press to continue : ")
|
|
|
|
|
|
|
|
|
|
|
|
def execute_odoo_upgrade(self):
|
|
|
|
|
|
# TODO update dev environment for git project
|
|
|
|
|
|
# TODO Redeploy new production after upgrade
|
|
|
|
|
|
# 2 upgrades version = 5 environnement. 0-prod init, 1-dev init, 2-dev01, 3-dev02, 4-prod final
|
|
|
|
|
|
print("Welcome to Odoo database upgrade processus with ERPLibre 🤖")
|
|
|
|
|
|
self.lst_command_executed = []
|
|
|
|
|
|
self.dct_module_per_version = {}
|
|
|
|
|
|
self.dct_module_per_dct_version_path = {}
|
|
|
|
|
|
default_database_name = "test"
|
|
|
|
|
|
|
[ADD] migration: a TUI resume screen, same first question as the prompt
The migration now opens with the same choice the QEMU deployment does — TUI
or line-by-line prompts — settled in advance by TODO > Configuration if you
want it to be. Choosing the TUI loads the saved progression and asks the very
same first question: where does this migration stand, and where do we resume?
prompt_resume was one function that rendered, read and decided at once, so a
second view would have meant a second copy of the decision. It is now three:
resume_context() the progression -> plain data (file, database, target,
steps with their icon and detail, version bumps)
print_resume() renders it on the terminal
apply_resume_answer() answer -> (progression, changed)
The TUI returns the SAME answer strings as the prompt — c, n, r, q, 0..4,
4.<version> — so apply_resume_answer stays the only place that decides what a
choice means. Neither view can drift into describing the migration
differently, because both render the same context.
In the TUI the steps are a table and the version bumps a list: Enter on either
replays from there, which is what « [0-4] » and « [4.N] » meant in text. The
cursor opens on the first unfinished step and on the first unmigrated version
— where it stopped is where one usually wants to act.
Both views gain « q », quit without doing anything. A TUI needs Escape to do
something sane, and an escape hatch present in only one of the two views is
exactly the kind of divergence this split exists to prevent. execute_odoo_
upgrade returns immediately on it, writing nothing.
Verified on a 12->18 progression with steps 0-3 done and 2 of 6 bumps
migrated: identical context feeding both views, Enter on step 2 giving « 2 »,
Enter on the 15 bump giving « 4.15 », the c/n/r/q/Escape shortcuts, and every
answer producing the same progression through both paths — « 2 » leaving only
the state of steps 0 and 1, « 4.15 » resetting the clone list from the third
bump on so the half-migrated intermediate database gets rebuilt. « q » checked
to leave the progression file byte-identical.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-08-01 06:38:31 -04:00
|
|
|
|
use_tui = self.ask_ui() == "tui"
|
|
|
|
|
|
|
2025-10-31 01:10:54 -04:00
|
|
|
|
if os.path.exists(UPGRADE_DATABASE_CONFIG_LOG):
|
[IMP] migration: show the real state of each step and replay from any of them
The resume menu exposed internal key names: « Reuse database without state_4 »
tells nobody what will happen, and the most useful option -- keep the prepared
database, redo the version bumps -- was the least readable of the four.
It now prints where the migration actually stands: the zip, the database, the
target, and one line per step with its state. Step 4 reports how many version
bumps are migrated and names them, so « 0/6 · 13 14 15 16 17 18 » replaces a
list index nobody could interpret.
The four numbered options become:
[c] continue where it stopped (was: press enter)
[0-4] replay from that step (new: rewind to any step)
[n] new migration, erase everything (was: [1])
[r] keep the zip, ask everything (was: [4])
Rewinding drops « state_* » from the chosen step onward and keeps the rest:
config_* answers, the zip and the target version are decisions, not progress,
so they are no longer re-asked. state_0_search_missing_module is still forced
back to False, as the previous code did: it fills an in-memory dict the later
steps rely on. Old [2] is now « replay from 0 », old [3] « replay from 4 ».
First use of t() in this file; the new strings are translated in todo_i18n.py.
Verified against the real progression of the 12.0 -> 18.0 migration and a
synthetic one: for every step, the kept keys are exactly those of the earlier
steps, config and target survive, and the module search is forced again.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-08-01 02:53:17 -04:00
|
|
|
|
old_dct_progression = self.read_progression()
|
|
|
|
|
|
if old_dct_progression:
|
[ADD] migration: a TUI resume screen, same first question as the prompt
The migration now opens with the same choice the QEMU deployment does — TUI
or line-by-line prompts — settled in advance by TODO > Configuration if you
want it to be. Choosing the TUI loads the saved progression and asks the very
same first question: where does this migration stand, and where do we resume?
prompt_resume was one function that rendered, read and decided at once, so a
second view would have meant a second copy of the decision. It is now three:
resume_context() the progression -> plain data (file, database, target,
steps with their icon and detail, version bumps)
print_resume() renders it on the terminal
apply_resume_answer() answer -> (progression, changed)
The TUI returns the SAME answer strings as the prompt — c, n, r, q, 0..4,
4.<version> — so apply_resume_answer stays the only place that decides what a
choice means. Neither view can drift into describing the migration
differently, because both render the same context.
In the TUI the steps are a table and the version bumps a list: Enter on either
replays from there, which is what « [0-4] » and « [4.N] » meant in text. The
cursor opens on the first unfinished step and on the first unmigrated version
— where it stopped is where one usually wants to act.
Both views gain « q », quit without doing anything. A TUI needs Escape to do
something sane, and an escape hatch present in only one of the two views is
exactly the kind of divergence this split exists to prevent. execute_odoo_
upgrade returns immediately on it, writing nothing.
Verified on a 12->18 progression with steps 0-3 done and 2 of 6 bumps
migrated: identical context feeding both views, Enter on step 2 giving « 2 »,
Enter on the 15 bump giving « 4.15 », the c/n/r/q/Escape shortcuts, and every
answer producing the same progression through both paths — « 2 » leaving only
the state of steps 0 and 1, « 4.15 » resetting the clone list from the third
bump on so the half-migrated intermediate database gets rebuilt. « q » checked
to leave the progression file byte-identical.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-08-01 06:38:31 -04:00
|
|
|
|
resumed = self.prompt_resume(old_dct_progression, use_tui)
|
|
|
|
|
|
if resumed is None:
|
|
|
|
|
|
return
|
|
|
|
|
|
self.dct_progression, changed = resumed
|
[IMP] migration: show the real state of each step and replay from any of them
The resume menu exposed internal key names: « Reuse database without state_4 »
tells nobody what will happen, and the most useful option -- keep the prepared
database, redo the version bumps -- was the least readable of the four.
It now prints where the migration actually stands: the zip, the database, the
target, and one line per step with its state. Step 4 reports how many version
bumps are migrated and names them, so « 0/6 · 13 14 15 16 17 18 » replaces a
list index nobody could interpret.
The four numbered options become:
[c] continue where it stopped (was: press enter)
[0-4] replay from that step (new: rewind to any step)
[n] new migration, erase everything (was: [1])
[r] keep the zip, ask everything (was: [4])
Rewinding drops « state_* » from the chosen step onward and keeps the rest:
config_* answers, the zip and the target version are decisions, not progress,
so they are no longer re-asked. state_0_search_missing_module is still forced
back to False, as the previous code did: it fills an in-memory dict the later
steps rely on. Old [2] is now « replay from 0 », old [3] « replay from 4 ».
First use of t() in this file; the new strings are translated in todo_i18n.py.
Verified against the real progression of the 12.0 -> 18.0 migration and a
synthetic one: for every step, the kept keys are exactly those of the earlier
steps, config and target survive, and the module search is forced again.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-08-01 02:53:17 -04:00
|
|
|
|
if changed:
|
|
|
|
|
|
self.write_config()
|
[ADD] migration: a TUI resume screen, same first question as the prompt
The migration now opens with the same choice the QEMU deployment does — TUI
or line-by-line prompts — settled in advance by TODO > Configuration if you
want it to be. Choosing the TUI loads the saved progression and asks the very
same first question: where does this migration stand, and where do we resume?
prompt_resume was one function that rendered, read and decided at once, so a
second view would have meant a second copy of the decision. It is now three:
resume_context() the progression -> plain data (file, database, target,
steps with their icon and detail, version bumps)
print_resume() renders it on the terminal
apply_resume_answer() answer -> (progression, changed)
The TUI returns the SAME answer strings as the prompt — c, n, r, q, 0..4,
4.<version> — so apply_resume_answer stays the only place that decides what a
choice means. Neither view can drift into describing the migration
differently, because both render the same context.
In the TUI the steps are a table and the version bumps a list: Enter on either
replays from there, which is what « [0-4] » and « [4.N] » meant in text. The
cursor opens on the first unfinished step and on the first unmigrated version
— where it stopped is where one usually wants to act.
Both views gain « q », quit without doing anything. A TUI needs Escape to do
something sane, and an escape hatch present in only one of the two views is
exactly the kind of divergence this split exists to prevent. execute_odoo_
upgrade returns immediately on it, writing nothing.
Verified on a 12->18 progression with steps 0-3 done and 2 of 6 bumps
migrated: identical context feeding both views, Enter on step 2 giving « 2 »,
Enter on the 15 bump giving « 4.15 », the c/n/r/q/Escape shortcuts, and every
answer producing the same progression through both paths — « 2 » leaving only
the state of steps 0 and 1, « 4.15 » resetting the clone list from the third
bump on so the half-migrated intermediate database gets rebuilt. « q » checked
to leave the progression file byte-identical.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-08-01 06:38:31 -04:00
|
|
|
|
elif use_tui:
|
|
|
|
|
|
print(f"ℹ️ {t('No migration in progress to resume.')}")
|
|
|
|
|
|
elif use_tui:
|
|
|
|
|
|
print(f"ℹ️ {t('No migration in progress to resume.')}")
|
2025-10-31 01:10:54 -04:00
|
|
|
|
|
|
|
|
|
|
if "migration_file" in self.dct_progression:
|
|
|
|
|
|
self.file_path = self.dct_progression["migration_file"]
|
|
|
|
|
|
else:
|
|
|
|
|
|
print("")
|
|
|
|
|
|
print("Select the zip file of you database backup.")
|
|
|
|
|
|
|
|
|
|
|
|
self.file_path = input(
|
|
|
|
|
|
"💬 Give the path of file, or empty to use a File Browser, or type 'remote' to download from production : "
|
|
|
|
|
|
)
|
|
|
|
|
|
if not self.file_path.strip():
|
|
|
|
|
|
self.file_path = None
|
|
|
|
|
|
if not self.file_path:
|
|
|
|
|
|
initial_dir = os.path.join(os.getcwd(), "image_db")
|
|
|
|
|
|
file_browser = todo_file_browser.FileBrowser(
|
|
|
|
|
|
initial_dir, self.on_file_selected
|
|
|
|
|
|
)
|
|
|
|
|
|
file_browser.run_main_frame()
|
|
|
|
|
|
elif self.file_path == "remote":
|
|
|
|
|
|
status, self.file_path, default_database_name = (
|
2026-05-20 02:54:29 -04:00
|
|
|
|
self.todo.db_manager.download_database_backup_cli()
|
2025-10-31 01:10:54 -04:00
|
|
|
|
)
|
|
|
|
|
|
if status:
|
|
|
|
|
|
_logger.error(
|
|
|
|
|
|
"Cannot retrieve database from remote, please retry migration."
|
|
|
|
|
|
)
|
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
|
|
self.dct_progression["migration_file"] = self.file_path
|
|
|
|
|
|
self.write_config()
|
|
|
|
|
|
|
|
|
|
|
|
print(f"✅ Open file {self.file_path}")
|
|
|
|
|
|
with zipfile.ZipFile(self.file_path, "r") as zip_ref:
|
|
|
|
|
|
manifest_file_1 = zip_ref.open("manifest.json")
|
|
|
|
|
|
json_manifest_file_1 = json.load(manifest_file_1)
|
|
|
|
|
|
odoo_actual_version = json_manifest_file_1.get("version")
|
|
|
|
|
|
print(f"✅ Detect version Odoo CE '{odoo_actual_version}'.")
|
|
|
|
|
|
|
|
|
|
|
|
# print("What is your actual Odoo version?")
|
|
|
|
|
|
lst_version, lst_version_installed, odoo_installed_version = (
|
2026-03-26 15:45:44 -04:00
|
|
|
|
get_odoo_version()
|
2025-10-31 01:10:54 -04:00
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
lst_odoo_version = [
|
|
|
|
|
|
{"prompt_description": a.get("odoo_version")}
|
|
|
|
|
|
for a in lst_version
|
|
|
|
|
|
if float(a.get("odoo_version")) > float(odoo_actual_version)
|
|
|
|
|
|
]
|
|
|
|
|
|
help_info = self.todo.fill_help_info(lst_odoo_version)
|
|
|
|
|
|
|
|
|
|
|
|
if "target_odoo_version" in self.dct_progression:
|
|
|
|
|
|
odoo_target_version = self.dct_progression["target_odoo_version"]
|
|
|
|
|
|
else:
|
|
|
|
|
|
print("💬 Which version do you want to upgrade to?")
|
|
|
|
|
|
odoo_target_version = None
|
|
|
|
|
|
cmd_no_found = True
|
|
|
|
|
|
while cmd_no_found:
|
|
|
|
|
|
status = click.prompt(help_info)
|
|
|
|
|
|
try:
|
|
|
|
|
|
int_cmd = int(status)
|
|
|
|
|
|
if 0 < int_cmd <= len(lst_odoo_version):
|
|
|
|
|
|
cmd_no_found = False
|
|
|
|
|
|
odoo_target_version = lst_odoo_version[
|
|
|
|
|
|
int_cmd - 1
|
|
|
|
|
|
].get("prompt_description")
|
|
|
|
|
|
except ValueError:
|
|
|
|
|
|
pass
|
|
|
|
|
|
if cmd_no_found:
|
|
|
|
|
|
print("Commande non trouvée 🤖!")
|
|
|
|
|
|
|
|
|
|
|
|
self.dct_progression["target_odoo_version"] = odoo_target_version
|
|
|
|
|
|
self.write_config()
|
|
|
|
|
|
|
|
|
|
|
|
# Search nb diff to use range
|
|
|
|
|
|
start_version = int(float(odoo_actual_version))
|
|
|
|
|
|
end_version = int(float(odoo_target_version))
|
|
|
|
|
|
range_version = range(start_version, end_version)
|
|
|
|
|
|
lst_module = sorted(
|
|
|
|
|
|
list(set(json_manifest_file_1.get("modules").keys()))
|
|
|
|
|
|
)
|
|
|
|
|
|
self.dct_module_per_version[start_version] = lst_module
|
|
|
|
|
|
self.dct_progression["dct_module_per_version"] = (
|
|
|
|
|
|
self.dct_module_per_version
|
|
|
|
|
|
)
|
|
|
|
|
|
self.dct_progression["lst_module_per_version_origin"] = lst_module
|
|
|
|
|
|
# TODO need support minor version, example 18.2, the .2 (no need for OCE OCB)
|
|
|
|
|
|
|
|
|
|
|
|
print("✨ Show documentation version :")
|
|
|
|
|
|
# TODO Generate it locally and show it if asked
|
|
|
|
|
|
|
|
|
|
|
|
for next_version in range_version:
|
|
|
|
|
|
print(
|
|
|
|
|
|
f"https://oca.github.io/OpenUpgrade/coverage_analysis/modules{next_version*10}-{(next_version+1)*10}.html"
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
# ⚠️ ℹ 💬 ❗ 🔷 ✨ 🟦 🔹 🔵 ⟳ ⧖ ⚙ ✔ ✅ ❌ ⏵ ⏸ ⏹ ◆ ◇ … ➤ ⚑ ★ ☆ ☰ ⬍ ⍟ ⊗ ⌘ ⏻ ⍰
|
|
|
|
|
|
msg = "0 - Inspect zip"
|
|
|
|
|
|
print(f"🔷 {msg}")
|
|
|
|
|
|
self.add_comment_progression(msg)
|
|
|
|
|
|
print("✅ -> Search odoo version")
|
|
|
|
|
|
print("✅ -> Find good environment, read the .zip file")
|
|
|
|
|
|
|
|
|
|
|
|
is_state_4_reach_open_upgrade = self.dct_progression.get(
|
|
|
|
|
|
"state_4_reach_open_upgrade"
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
if not is_state_4_reach_open_upgrade and not self.dct_progression.get(
|
|
|
|
|
|
"state_0_install_odoo"
|
|
|
|
|
|
):
|
|
|
|
|
|
lst_diff_version = sorted(
|
|
|
|
|
|
list(
|
|
|
|
|
|
set([f"odoo{a}.0" for a in range_version]).difference(
|
|
|
|
|
|
set(lst_version_installed)
|
|
|
|
|
|
)
|
|
|
|
|
|
)
|
|
|
|
|
|
)
|
|
|
|
|
|
for odoo_version_to_install in lst_diff_version:
|
|
|
|
|
|
iter_range_version = odoo_version_to_install.replace(
|
|
|
|
|
|
"odoo", ""
|
|
|
|
|
|
).replace(".0", "")
|
|
|
|
|
|
want_continue = input(
|
|
|
|
|
|
f"💬 Would you like to install '{odoo_version_to_install}' (y/Y) : "
|
|
|
|
|
|
)
|
|
|
|
|
|
if want_continue.strip().lower() != "y":
|
|
|
|
|
|
return
|
|
|
|
|
|
self.todo_upgrade_execute(
|
|
|
|
|
|
f"make install_odoo_{iter_range_version}"
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
if not os.path.isfile(FILENAME_ODOO_VERSION):
|
|
|
|
|
|
print(
|
|
|
|
|
|
"⚠️ You need an installed system before continue, check your Odoo installation."
|
|
|
|
|
|
)
|
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
|
|
self.dct_progression["state_0_install_odoo"] = True
|
|
|
|
|
|
self.write_config()
|
|
|
|
|
|
# not self.dct_progression.get("state_0_switch_odoo")
|
|
|
|
|
|
if not is_state_4_reach_open_upgrade:
|
|
|
|
|
|
self.switch_odoo(odoo_actual_version)
|
|
|
|
|
|
# self.dct_progression["state_0_switch_odoo"] = True
|
|
|
|
|
|
# self.write_config()
|
|
|
|
|
|
|
|
|
|
|
|
print("✅ -> Install environment if missing")
|
|
|
|
|
|
|
|
|
|
|
|
if not self.dct_progression.get("state_0_search_missing_module"):
|
|
|
|
|
|
self.switch_odoo(odoo_actual_version)
|
|
|
|
|
|
dct_bd_modules = json_manifest_file_1.get("modules")
|
|
|
|
|
|
lst_module_to_check = [a for a in dct_bd_modules.keys()]
|
|
|
|
|
|
(
|
|
|
|
|
|
lst_module_missing,
|
|
|
|
|
|
lst_module_duplicate,
|
|
|
|
|
|
lst_module_exist,
|
|
|
|
|
|
lst_module_error,
|
|
|
|
|
|
) = self.check_addons_exist(lst_module_to_check, get_all_info=True)
|
|
|
|
|
|
if not lst_module_missing:
|
|
|
|
|
|
lst_module_missing = []
|
|
|
|
|
|
dct_module_exist = {}
|
|
|
|
|
|
if not lst_module_exist:
|
|
|
|
|
|
lst_module_exist = []
|
|
|
|
|
|
else:
|
|
|
|
|
|
for item_lst_module_exist in lst_module_exist:
|
|
|
|
|
|
dct_module_exist[item_lst_module_exist[0]] = (
|
|
|
|
|
|
item_lst_module_exist[1].replace(os.getcwd(), ".")
|
|
|
|
|
|
)
|
|
|
|
|
|
if not lst_module_duplicate:
|
|
|
|
|
|
lst_module_duplicate = []
|
|
|
|
|
|
|
|
|
|
|
|
lst_module_missing = sorted(list(set(lst_module_missing)))
|
|
|
|
|
|
self.dct_progression["len_lst_module_missing"] = len(
|
|
|
|
|
|
lst_module_missing
|
|
|
|
|
|
)
|
|
|
|
|
|
self.dct_progression["lst_module_missing"] = lst_module_missing
|
|
|
|
|
|
self.dct_progression["len_dct_module_exist"] = len(
|
|
|
|
|
|
lst_module_exist
|
|
|
|
|
|
)
|
|
|
|
|
|
self.dct_progression["dct_module_exist"] = dct_module_exist
|
|
|
|
|
|
self.dct_progression["len_lst_module_duplicate"] = len(
|
|
|
|
|
|
lst_module_duplicate
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
self.dct_progression["lst_module_duplicate"] = lst_module_duplicate
|
|
|
|
|
|
self.write_config()
|
|
|
|
|
|
if lst_module_missing or lst_module_duplicate:
|
|
|
|
|
|
print("Cannot setup environment to begin.")
|
|
|
|
|
|
if lst_module_missing:
|
|
|
|
|
|
print("Missing module :")
|
|
|
|
|
|
print(lst_module_missing)
|
|
|
|
|
|
if lst_module_duplicate:
|
|
|
|
|
|
print("Duplicate module :")
|
|
|
|
|
|
print(lst_module_duplicate)
|
|
|
|
|
|
want_continue = input(
|
|
|
|
|
|
"💬 Detect error missing/duplicate module init, do you want to continue? (Y/N): "
|
|
|
|
|
|
)
|
|
|
|
|
|
if want_continue.strip().lower() != "y":
|
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
|
|
self.dct_progression["state_0_search_missing_module"] = True
|
|
|
|
|
|
self.write_config()
|
|
|
|
|
|
else:
|
|
|
|
|
|
# TODO fill from config
|
|
|
|
|
|
lst_module_missing = []
|
|
|
|
|
|
|
|
|
|
|
|
print("✅ -> Search missing module")
|
|
|
|
|
|
|
|
|
|
|
|
print(
|
|
|
|
|
|
"❌ -> Install missing module, do a research or ask to uninstall it (can break data)"
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
msg = "1 - Import database from zip"
|
|
|
|
|
|
print(f"🔷 {msg}")
|
|
|
|
|
|
self.add_comment_progression(msg)
|
|
|
|
|
|
|
|
|
|
|
|
database_name = self.dct_progression.get("config_database_name")
|
|
|
|
|
|
if not database_name:
|
|
|
|
|
|
database_name = (
|
|
|
|
|
|
input(
|
|
|
|
|
|
f"💬 Witch database name do you want to work with? Default ({default_database_name}) : "
|
|
|
|
|
|
).strip()
|
|
|
|
|
|
or default_database_name
|
|
|
|
|
|
)
|
|
|
|
|
|
self.dct_progression["config_database_name"] = database_name
|
|
|
|
|
|
self.write_config()
|
|
|
|
|
|
|
|
|
|
|
|
do_neutralize = False
|
|
|
|
|
|
if not self.dct_progression.get("state_1_neutralize_database"):
|
|
|
|
|
|
print("[1] Ignore neutralize database")
|
|
|
|
|
|
wait_continue = (
|
|
|
|
|
|
input("💬 Neutralize database, press to continue : ")
|
|
|
|
|
|
.strip()
|
|
|
|
|
|
.lower()
|
|
|
|
|
|
)
|
|
|
|
|
|
if wait_continue != "1":
|
|
|
|
|
|
do_neutralize = True
|
|
|
|
|
|
database_name += "_neutralize"
|
|
|
|
|
|
self.dct_progression["config_database_name"] = database_name
|
|
|
|
|
|
self.write_config()
|
|
|
|
|
|
|
|
|
|
|
|
print(f"★ Work with database '{database_name}'")
|
|
|
|
|
|
|
|
|
|
|
|
if not self.dct_progression.get("state_1_restore_database"):
|
|
|
|
|
|
file_name = os.path.basename(self.file_path)
|
|
|
|
|
|
image_db_file_path = os.path.join("image_db", file_name)
|
2026-01-05 07:52:30 -05:00
|
|
|
|
str_will_copy = (
|
|
|
|
|
|
f"🤖 will copy '{self.file_path}' to '{image_db_file_path}'"
|
|
|
|
|
|
)
|
|
|
|
|
|
if not shutil._samefile(self.file_path, image_db_file_path):
|
|
|
|
|
|
do_copy = False
|
|
|
|
|
|
if os.path.exists(image_db_file_path):
|
2025-10-31 01:10:54 -04:00
|
|
|
|
status_overwrite_image_db = input(
|
2026-01-05 07:52:30 -05:00
|
|
|
|
f"{str_will_copy}, "
|
2025-10-31 01:10:54 -04:00
|
|
|
|
f"a file already exist, do you want to continue (y/Y) : "
|
|
|
|
|
|
).strip()
|
2026-01-05 07:52:30 -05:00
|
|
|
|
if status_overwrite_image_db.lower() == "y":
|
|
|
|
|
|
do_copy = True
|
|
|
|
|
|
os.remove(image_db_file_path)
|
|
|
|
|
|
else:
|
|
|
|
|
|
print(str_will_copy)
|
|
|
|
|
|
do_copy = True
|
|
|
|
|
|
|
|
|
|
|
|
if do_copy:
|
2025-10-31 01:10:54 -04:00
|
|
|
|
shutil.copy(self.file_path, image_db_file_path)
|
|
|
|
|
|
|
2026-02-22 12:14:07 -05:00
|
|
|
|
neutralize_arg = ""
|
|
|
|
|
|
if start_version >= 16 and do_neutralize:
|
|
|
|
|
|
neutralize_arg = " --neutralize"
|
|
|
|
|
|
|
2025-10-31 01:10:54 -04:00
|
|
|
|
status, cmd_executed = self.todo_upgrade_execute(
|
2026-02-22 12:14:07 -05:00
|
|
|
|
f"./script/database/db_restore.py --database {database_name} --image {file_name} --ignore_cache{neutralize_arg}",
|
2025-10-31 01:10:54 -04:00
|
|
|
|
single_source_odoo=True,
|
|
|
|
|
|
)
|
|
|
|
|
|
if not status:
|
|
|
|
|
|
self.dct_progression["state_1_restore_database"] = True
|
|
|
|
|
|
self.write_config()
|
|
|
|
|
|
|
|
|
|
|
|
print("✅ -> Restore database")
|
2026-07-31 00:34:32 -04:00
|
|
|
|
already_update_state_1 = False
|
2025-10-31 01:10:54 -04:00
|
|
|
|
|
2026-07-30 23:00:12 -04:00
|
|
|
|
if not self.dct_progression.get("state_1_update_all"):
|
|
|
|
|
|
print(
|
|
|
|
|
|
"[1] Update all addons before neutralize (already neutralize by odoo if supported)"
|
|
|
|
|
|
)
|
|
|
|
|
|
wait_continue = (
|
|
|
|
|
|
input(
|
|
|
|
|
|
"💬 Do you need to upgrade before a database neutralization, press to ignore : "
|
|
|
|
|
|
)
|
|
|
|
|
|
.strip()
|
|
|
|
|
|
.lower()
|
|
|
|
|
|
)
|
|
|
|
|
|
if wait_continue == "1":
|
|
|
|
|
|
status, cmd_executed = self.todo_upgrade_execute(
|
|
|
|
|
|
f"./script/addons/update_addons_all.sh {database_name}",
|
|
|
|
|
|
single_source_odoo=True,
|
|
|
|
|
|
)
|
2026-07-31 00:34:32 -04:00
|
|
|
|
|
2026-07-30 23:00:12 -04:00
|
|
|
|
if not status:
|
2026-07-31 00:34:32 -04:00
|
|
|
|
already_update_state_1 = True
|
2026-07-30 23:00:12 -04:00
|
|
|
|
self.dct_progression["state_1_update_all"] = True
|
|
|
|
|
|
self.write_config()
|
|
|
|
|
|
|
|
|
|
|
|
print("✅ -> Update database before neutralize by module")
|
|
|
|
|
|
|
2025-10-31 01:10:54 -04:00
|
|
|
|
print("✅ -> Neutralize database")
|
|
|
|
|
|
if do_neutralize:
|
|
|
|
|
|
status, cmd_executed = self.todo_upgrade_execute(
|
|
|
|
|
|
f"./script/addons/update_prod_to_dev.sh {database_name}",
|
|
|
|
|
|
single_source_odoo=True,
|
|
|
|
|
|
)
|
2026-02-22 12:14:07 -05:00
|
|
|
|
|
|
|
|
|
|
# TODO exécuter next line si status != 0 et log contient
|
|
|
|
|
|
# psycopg2.errors.UndefinedTable: relation "discuss_channel" does not exist
|
|
|
|
|
|
# LIGNE 1 : SELECT "discuss_channel"."id" FROM "discuss_channel" WHERE (...
|
|
|
|
|
|
# source ./.venv.odoo18.0_python3.12.10/bin/activate && cat script/postgresql/migration/fix_migration_postgresql_17_to_postgresql_18_module_mail_nov_2025.py | ./odoo18.0/odoo/odoo-bin shell -d ripbylop_stage_prod_17_nov_2025
|
|
|
|
|
|
# psycopg2.errors.ForeignKeyViolation: insert or update on table "discuss_channel_member" violates foreign key constraint "discuss_channel_member_channel_id_fkey"
|
|
|
|
|
|
# DÉTAIL : Key (channel_id)=(20) is not present in table "discuss_channel".
|
|
|
|
|
|
# ./script/database/migrate/process_backup_file.py --path_backup_zip image_db/db.zip --path_output_zip image_db/dbFIX.zip --word_to_delete discuss_channel_channel_type_not_null
|
|
|
|
|
|
# Puis faire un retry de la commande, sinon rien
|
|
|
|
|
|
|
2026-07-31 23:22:02 -04:00
|
|
|
|
# Only record the step when it actually succeeded. The previous
|
|
|
|
|
|
# unconditional assignment made the test below dead code: a failed
|
|
|
|
|
|
# neutralization was remembered as done and skipped on resume.
|
2025-10-31 01:10:54 -04:00
|
|
|
|
if not status:
|
|
|
|
|
|
self.dct_progression["state_1_neutralize_database"] = True
|
|
|
|
|
|
self.write_config()
|
|
|
|
|
|
else:
|
|
|
|
|
|
self.dct_progression["state_1_neutralize_database"] = True
|
|
|
|
|
|
self.write_config()
|
|
|
|
|
|
|
|
|
|
|
|
config_state_1_uninstall_module = self.dct_progression.get(
|
|
|
|
|
|
"config_state_1_uninstall_module"
|
|
|
|
|
|
)
|
|
|
|
|
|
config_state_1_install_module = self.dct_progression.get(
|
|
|
|
|
|
"config_state_1_install_module"
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
if not is_state_4_reach_open_upgrade:
|
[IMP] migration: per-database uninstall list, with a stated reason
Which modules must be uninstalled before a version bump depends on the data of
one specific database, so that list does not belong to a shared versioned file.
- Read private/odoo/migration/<database>/uninstall_module_list_odooXX0_to_odooYY0.txt
first, then the shared versioned defaults under script/odoo/migration/, and
merge them (duplicates dropped). private/ mirrors script/, the convention
already used by script/todo/todo.json -> private/todo/todo_override.json.
- Fix the parser. It was « f.readline().split() »: only the FIRST line was kept,
so a multi-line list was silently truncated, and a comma-separated list turned
into one bogus module name. On a file starting with a comment it returned the
words of that comment as module names. It now reads every line and accepts
commas, several names per line, blank lines and comments.
- Support a « # reason » justification per module, printed before uninstalling;
a module with no reason is flagged. Removing a module must stay reviewable.
- Ignore private/odoo/ in git: these lists describe one database.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-31 23:25:43 -04:00
|
|
|
|
lst_module_to_uninstall, lst_uninstall_reason = (
|
|
|
|
|
|
self.read_uninstall_module_list(start_version, database_name)
|
2025-10-31 01:10:54 -04:00
|
|
|
|
)
|
[IMP] migration: per-database uninstall list, with a stated reason
Which modules must be uninstalled before a version bump depends on the data of
one specific database, so that list does not belong to a shared versioned file.
- Read private/odoo/migration/<database>/uninstall_module_list_odooXX0_to_odooYY0.txt
first, then the shared versioned defaults under script/odoo/migration/, and
merge them (duplicates dropped). private/ mirrors script/, the convention
already used by script/todo/todo.json -> private/todo/todo_override.json.
- Fix the parser. It was « f.readline().split() »: only the FIRST line was kept,
so a multi-line list was silently truncated, and a comma-separated list turned
into one bogus module name. On a file starting with a comment it returned the
words of that comment as module names. It now reads every line and accepts
commas, several names per line, blank lines and comments.
- Support a « # reason » justification per module, printed before uninstalling;
a module with no reason is flagged. Removing a module must stay reviewable.
- Ignore private/odoo/ in git: these lists describe one database.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-31 23:25:43 -04:00
|
|
|
|
if lst_uninstall_reason:
|
|
|
|
|
|
print("✨ Modules to uninstall before migration :")
|
[IMP] migration: replay a single version bump, and stop offering a doomed retry
When OpenUpgrade failed, todo_upgrade_execute offered the generic
« [1] to redo the command ». Replaying it re-ran OpenUpgrade on the database it
had just half migrated, which never recovers. That call now passes
wait_at_error=False, and the failure message explains that the clone step has
been reset so a relaunch DROPS and REBUILDS the intermediate database from the
previous version.
The resume menu gains « [4.N] »: replay the step 4 loop from one version bump.
Only the per-version lists are trimmed from that index, so earlier bumps stay
migrated and steps 0 to 3 are untouched. Resetting the clone entry is the whole
point: the intermediate database of a failed bump must be rebuilt, not upgraded
again. « [4] » still replays every bump.
The per-bump uninstall files were also read at step 1 only, for the source
version, so uninstall_module_list_odoo130_to_odoo140.txt existed in name but
was never read. The step 4 loop now reads the file of the bump it is about to
perform, merged with the answers already stored in the progression.
Verified against the real progression (13 and 14 migrated): the menu offers
13/14/15/16/17/18, and replaying from 14 turns every per-version list from
[True, True, False...] into [True, False, ...] while state_4_reach_open_upgrade
and steps 0-3 survive. A 13->14 list of 4 modules is read with its reasons.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-08-01 04:46:17 -04:00
|
|
|
|
self.print_uninstall_reason(lst_uninstall_reason)
|
2025-10-31 01:10:54 -04:00
|
|
|
|
|
|
|
|
|
|
if config_state_1_uninstall_module:
|
|
|
|
|
|
lst_module_to_uninstall = (
|
|
|
|
|
|
lst_module_to_uninstall + config_state_1_uninstall_module
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
if lst_module_to_uninstall:
|
|
|
|
|
|
self.uninstall_from_database(
|
|
|
|
|
|
lst_module_to_uninstall, database_name, start_version
|
|
|
|
|
|
)
|
|
|
|
|
|
self.dct_progression["state_1_uninstall_module"] = True
|
|
|
|
|
|
self.write_config()
|
|
|
|
|
|
|
|
|
|
|
|
self.dct_progression["config_state_1_uninstall_module"] = (
|
|
|
|
|
|
config_state_1_uninstall_module
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
self.write_config()
|
|
|
|
|
|
|
|
|
|
|
|
print("✅ -> Uninstall module")
|
|
|
|
|
|
|
|
|
|
|
|
print("✅ -> install module")
|
|
|
|
|
|
if not is_state_4_reach_open_upgrade:
|
|
|
|
|
|
lst_module_to_install = []
|
|
|
|
|
|
if config_state_1_install_module:
|
|
|
|
|
|
lst_module_to_install = (
|
|
|
|
|
|
lst_module_to_install + config_state_1_install_module
|
|
|
|
|
|
)
|
|
|
|
|
|
if lst_module_to_install:
|
|
|
|
|
|
self.install_from_database(
|
|
|
|
|
|
lst_module_to_install, database_name, start_version
|
|
|
|
|
|
)
|
|
|
|
|
|
self.dct_progression["state_1_install_module"] = True
|
|
|
|
|
|
self.write_config()
|
|
|
|
|
|
self.dct_progression["config_state_1_install_module"] = (
|
|
|
|
|
|
config_state_1_install_module
|
|
|
|
|
|
)
|
|
|
|
|
|
self.write_config()
|
|
|
|
|
|
|
|
|
|
|
|
msg = "2 - Succeed update all addons"
|
|
|
|
|
|
print(f"🔷 {msg}")
|
|
|
|
|
|
self.add_comment_progression(msg)
|
|
|
|
|
|
|
2026-07-31 23:28:54 -04:00
|
|
|
|
if (
|
|
|
|
|
|
not self.dct_progression.get("state_2_update_all")
|
|
|
|
|
|
and not already_update_state_1
|
|
|
|
|
|
):
|
2025-10-31 01:10:54 -04:00
|
|
|
|
status, cmd_executed = self.todo_upgrade_execute(
|
|
|
|
|
|
f"./script/addons/update_addons_all.sh {database_name}",
|
|
|
|
|
|
single_source_odoo=True,
|
|
|
|
|
|
)
|
|
|
|
|
|
if not status:
|
|
|
|
|
|
self.dct_progression["state_2_update_all"] = True
|
|
|
|
|
|
self.write_config()
|
|
|
|
|
|
|
2026-07-31 23:28:54 -04:00
|
|
|
|
# Predict the website COW views that the NEXT version bump will break.
|
|
|
|
|
|
# A copy-on-write view freezes the structure of the module view it was
|
|
|
|
|
|
# copied from; when that module view changes mode between two versions,
|
|
|
|
|
|
# the copy keeps an arch written for the old mode and the upgrade dies
|
|
|
|
|
|
# on « Element ... cannot be located in parent view ». Reporting it here
|
|
|
|
|
|
# -- before hours of migration -- leaves time to arbitrate.
|
|
|
|
|
|
# Only the next bump can be predicted: the modes in database describe
|
|
|
|
|
|
# the current version.
|
|
|
|
|
|
self.todo_upgrade_execute(
|
|
|
|
|
|
f"{PYTHON_BIN} ./script/odoo/migration/check_cow_views.py"
|
|
|
|
|
|
f" -d {database_name} -t odoo{start_version + 1}.0",
|
|
|
|
|
|
wait_at_error=False,
|
|
|
|
|
|
)
|
|
|
|
|
|
|
2025-10-31 01:10:54 -04:00
|
|
|
|
msg = "3 - Clean up database before data migration"
|
|
|
|
|
|
print(f"🔷 {msg}")
|
|
|
|
|
|
self.add_comment_progression(msg)
|
|
|
|
|
|
|
|
|
|
|
|
if not self.dct_progression.get("state_3_install_clean_database"):
|
|
|
|
|
|
status, cmd_executed = self.todo_upgrade_execute(
|
|
|
|
|
|
f"./script/addons/install_addons.sh {database_name} database_cleanup",
|
|
|
|
|
|
single_source_odoo=True,
|
|
|
|
|
|
)
|
|
|
|
|
|
if not status:
|
|
|
|
|
|
self.dct_progression["state_3_install_clean_database"] = True
|
|
|
|
|
|
self.write_config()
|
|
|
|
|
|
|
|
|
|
|
|
if not self.dct_progression.get("state_3_clean_database"):
|
|
|
|
|
|
print(
|
|
|
|
|
|
"✨ Aller dans «configuration/Technique/Nettoyage.../Purger» les modules obsolètes"
|
|
|
|
|
|
)
|
|
|
|
|
|
status = input(
|
|
|
|
|
|
"💬 Did you finish to clean database? Press y/Y to open server with selenium, else ignore it : "
|
|
|
|
|
|
).strip()
|
|
|
|
|
|
|
|
|
|
|
|
if status.lower().strip() == "y":
|
|
|
|
|
|
self.todo.prompt_execute_selenium_and_run_db(database_name)
|
|
|
|
|
|
status = input("💬 Press to continue state.3 : ").strip()
|
|
|
|
|
|
|
|
|
|
|
|
self.dct_progression["state_3_clean_database"] = True
|
|
|
|
|
|
self.write_config()
|
|
|
|
|
|
|
|
|
|
|
|
self.install_OCA_odoo_module_migrator()
|
|
|
|
|
|
|
|
|
|
|
|
msg = "4 - Upgrade version with OpenUpgrade"
|
|
|
|
|
|
print(f"🔷 {msg}")
|
|
|
|
|
|
self.add_comment_progression(msg)
|
|
|
|
|
|
|
|
|
|
|
|
self.dct_progression["state_4_reach_open_upgrade"] = True
|
|
|
|
|
|
self.write_config()
|
|
|
|
|
|
lst_next_version = [
|
|
|
|
|
|
a for a in range(start_version + 1, end_version + 1)
|
|
|
|
|
|
]
|
|
|
|
|
|
lst_database_name_upgrade = [
|
|
|
|
|
|
f"{database_name}_upgrade_{str(a)}" for a in lst_next_version
|
|
|
|
|
|
]
|
|
|
|
|
|
# Setup lst_switch_odoo
|
|
|
|
|
|
lst_clone_odoo = self.dct_progression.get(
|
|
|
|
|
|
"state_4_clone_odoo_lst", [False] * len(lst_next_version)
|
|
|
|
|
|
)
|
|
|
|
|
|
lst_switch_odoo = self.dct_progression.get(
|
|
|
|
|
|
"state_4_switch_odoo_lst", [False] * len(lst_next_version)
|
|
|
|
|
|
)
|
|
|
|
|
|
lst_module_migrate_odoo = self.dct_progression.get(
|
|
|
|
|
|
"state_4_module_migrate_odoo_lst", [False] * len(lst_next_version)
|
|
|
|
|
|
)
|
|
|
|
|
|
lst_module_uninstall_module = self.dct_progression.get(
|
|
|
|
|
|
"state_4_uninstall_module", [False] * len(lst_next_version)
|
|
|
|
|
|
)
|
|
|
|
|
|
lst_module_install_module = self.dct_progression.get(
|
|
|
|
|
|
"state_4_install_module", [False] * len(lst_next_version)
|
|
|
|
|
|
)
|
|
|
|
|
|
lst_module_search_missing_module = self.dct_progression.get(
|
|
|
|
|
|
"state_4_search_missing_module", [False] * len(lst_next_version)
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
nb_missing_value_switch_odoo = abs(
|
|
|
|
|
|
len(lst_switch_odoo) - len(lst_next_version)
|
|
|
|
|
|
)
|
|
|
|
|
|
if nb_missing_value_switch_odoo:
|
|
|
|
|
|
lst_switch_odoo += [False] * nb_missing_value_switch_odoo
|
|
|
|
|
|
|
|
|
|
|
|
# Setup lst_upgrade_odoo
|
|
|
|
|
|
lst_upgrade_odoo = self.dct_progression.get(
|
|
|
|
|
|
"state_4_upgrade_odoo_lst", [[]] * len(lst_next_version)
|
|
|
|
|
|
)
|
|
|
|
|
|
lst_fix_migration_odoo = self.dct_progression.get(
|
|
|
|
|
|
"state_4_fix_migration_odoo_lst", [[]] * len(lst_next_version)
|
|
|
|
|
|
)
|
|
|
|
|
|
nb_missing_value_upgrade_odoo = abs(
|
|
|
|
|
|
len(lst_upgrade_odoo) - len(lst_next_version)
|
|
|
|
|
|
)
|
|
|
|
|
|
if nb_missing_value_upgrade_odoo:
|
|
|
|
|
|
lst_upgrade_odoo += [[]] * nb_missing_value_upgrade_odoo
|
|
|
|
|
|
|
|
|
|
|
|
database_name_upgrade = None
|
|
|
|
|
|
lst_module_missing_next_version = []
|
|
|
|
|
|
lst_module_to_delete = []
|
|
|
|
|
|
lst_module_to_delete_last_version = []
|
|
|
|
|
|
for index, next_version in enumerate(lst_next_version):
|
|
|
|
|
|
# Reinit the list
|
|
|
|
|
|
lst_module_missing_last_version = lst_module_missing_next_version[
|
|
|
|
|
|
:
|
|
|
|
|
|
]
|
|
|
|
|
|
lst_module_to_delete_last_version.extend(lst_module_to_delete)
|
|
|
|
|
|
lst_module_to_delete = []
|
|
|
|
|
|
|
|
|
|
|
|
msg = f"4.{index} - Ready to work with version {next_version}"
|
|
|
|
|
|
self.add_comment_progression(msg)
|
|
|
|
|
|
|
|
|
|
|
|
option_comment = 0
|
|
|
|
|
|
msg = f"4.{index}.{chr(option_comment + 65)} - Search updated module list to next version"
|
|
|
|
|
|
self.add_comment_progression(msg)
|
|
|
|
|
|
|
|
|
|
|
|
if not database_name_upgrade:
|
|
|
|
|
|
last_database_name = database_name
|
|
|
|
|
|
else:
|
|
|
|
|
|
last_database_name = database_name_upgrade
|
|
|
|
|
|
database_name_upgrade = lst_database_name_upgrade[index]
|
|
|
|
|
|
lst_module_to_uninstall = []
|
|
|
|
|
|
lst_module_to_install = []
|
|
|
|
|
|
lst_module_to_analyse = self.get_rename_module(
|
|
|
|
|
|
self.dct_module_per_version[next_version - 1],
|
|
|
|
|
|
next_version,
|
|
|
|
|
|
)
|
|
|
|
|
|
self.dct_module_per_version[next_version] = sorted(
|
|
|
|
|
|
list(set(lst_module_to_analyse))
|
|
|
|
|
|
)
|
|
|
|
|
|
self.dct_progression["dct_module_per_version"] = (
|
|
|
|
|
|
self.dct_module_per_version
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
option_comment += 1
|
|
|
|
|
|
msg = f"4.{index}.{chr(option_comment + 65)} - Clone Odoo"
|
|
|
|
|
|
self.add_comment_progression(msg)
|
|
|
|
|
|
|
|
|
|
|
|
if not os.path.exists(f"odoo{next_version}.0/addons/addons"):
|
|
|
|
|
|
os.makedirs(f"odoo{next_version}.0/addons/addons")
|
|
|
|
|
|
|
|
|
|
|
|
if not lst_clone_odoo[index]:
|
|
|
|
|
|
self.switch_odoo(next_version - 1)
|
|
|
|
|
|
|
|
|
|
|
|
print(
|
|
|
|
|
|
f"⧖ -> Clone to odoo.'{next_version}', from '{database_name}' to '{database_name_upgrade}'."
|
|
|
|
|
|
)
|
|
|
|
|
|
# Delete if exist database
|
|
|
|
|
|
self.todo_upgrade_execute(
|
|
|
|
|
|
f"./script/database/db_restore.py -d {database_name_upgrade} --only_drop",
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
# Duplicate database
|
|
|
|
|
|
cmd_clone_database = f"./odoo_bin.sh db --clone --from_database {last_database_name} --database {database_name_upgrade}"
|
2026-07-31 23:22:02 -04:00
|
|
|
|
status, cmd_executed = self.todo_upgrade_execute(
|
|
|
|
|
|
cmd_clone_database
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
# Everything downstream runs against this clone: if it failed,
|
|
|
|
|
|
# do not mark it done (a rerun would skip the clone and migrate
|
|
|
|
|
|
# a missing or truncated database).
|
|
|
|
|
|
if status:
|
|
|
|
|
|
print(
|
|
|
|
|
|
f"❌ -> Clone to Odoo{next_version} FAILED (status"
|
|
|
|
|
|
f" {status}). Stopping: '{database_name_upgrade}' is"
|
|
|
|
|
|
" not usable."
|
|
|
|
|
|
)
|
|
|
|
|
|
return
|
2025-10-31 01:10:54 -04:00
|
|
|
|
|
|
|
|
|
|
lst_clone_odoo[index] = True
|
|
|
|
|
|
self.dct_progression["state_4_clone_odoo_lst"] = lst_clone_odoo
|
|
|
|
|
|
self.write_config()
|
|
|
|
|
|
print(f"✅ -> Clone Odoo{next_version} done")
|
|
|
|
|
|
else:
|
|
|
|
|
|
print(f"✅ -> Clone Odoo{next_version} - nothing")
|
|
|
|
|
|
|
|
|
|
|
|
option_comment += 1
|
|
|
|
|
|
msg = f"4.{index}.{chr(option_comment + 65)} - Uninstall module"
|
|
|
|
|
|
self.add_comment_progression(msg)
|
|
|
|
|
|
|
|
|
|
|
|
config_state_4_uninstall_module = self.dct_progression.get(
|
|
|
|
|
|
"config_state_4_uninstall_module",
|
|
|
|
|
|
[False] * len(lst_next_version),
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
if not lst_module_uninstall_module[index]:
|
[IMP] migration: replay a single version bump, and stop offering a doomed retry
When OpenUpgrade failed, todo_upgrade_execute offered the generic
« [1] to redo the command ». Replaying it re-ran OpenUpgrade on the database it
had just half migrated, which never recovers. That call now passes
wait_at_error=False, and the failure message explains that the clone step has
been reset so a relaunch DROPS and REBUILDS the intermediate database from the
previous version.
The resume menu gains « [4.N] »: replay the step 4 loop from one version bump.
Only the per-version lists are trimmed from that index, so earlier bumps stay
migrated and steps 0 to 3 are untouched. Resetting the clone entry is the whole
point: the intermediate database of a failed bump must be rebuilt, not upgraded
again. « [4] » still replays every bump.
The per-bump uninstall files were also read at step 1 only, for the source
version, so uninstall_module_list_odoo130_to_odoo140.txt existed in name but
was never read. The step 4 loop now reads the file of the bump it is about to
perform, merged with the answers already stored in the progression.
Verified against the real progression (13 and 14 migrated): the menu offers
13/14/15/16/17/18, and replaying from 14 turns every per-version list from
[True, True, False...] into [True, False, ...] while state_4_reach_open_upgrade
and steps 0-3 survive. A 13->14 list of 4 modules is read with its reasons.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-08-01 04:46:17 -04:00
|
|
|
|
lst_module_to_uninstall = (
|
|
|
|
|
|
config_state_4_uninstall_module[index] or []
|
|
|
|
|
|
)
|
|
|
|
|
|
# Same file convention as step 1, one file per version bump:
|
|
|
|
|
|
# uninstall_module_list_odoo130_to_odoo140.txt is read HERE,
|
|
|
|
|
|
# right before the 13 -> 14 data migration. Without this the
|
|
|
|
|
|
# per-bump files existed in name only and were never read.
|
|
|
|
|
|
lst_file, lst_detail = self.read_uninstall_module_list(
|
|
|
|
|
|
next_version - 1, database_name
|
|
|
|
|
|
)
|
|
|
|
|
|
if lst_detail:
|
|
|
|
|
|
print(
|
|
|
|
|
|
f"✨ Modules to uninstall before Odoo{next_version} :"
|
|
|
|
|
|
)
|
|
|
|
|
|
self.print_uninstall_reason(lst_detail)
|
|
|
|
|
|
lst_module_to_uninstall = list(
|
|
|
|
|
|
dict.fromkeys(list(lst_module_to_uninstall) + lst_file)
|
|
|
|
|
|
)
|
2025-10-31 01:10:54 -04:00
|
|
|
|
|
|
|
|
|
|
if lst_module_to_uninstall:
|
|
|
|
|
|
self.uninstall_from_database(
|
|
|
|
|
|
lst_module_to_uninstall,
|
|
|
|
|
|
database_name_upgrade,
|
|
|
|
|
|
next_version - 1,
|
|
|
|
|
|
)
|
|
|
|
|
|
lst_module_uninstall_module[index] = True
|
|
|
|
|
|
self.dct_progression["state_4_module_migrate_odoo_lst"] = (
|
|
|
|
|
|
lst_module_uninstall_module
|
|
|
|
|
|
)
|
|
|
|
|
|
self.write_config()
|
|
|
|
|
|
|
|
|
|
|
|
self.dct_progression["config_state_4_uninstall_module"] = (
|
|
|
|
|
|
config_state_4_uninstall_module
|
|
|
|
|
|
)
|
|
|
|
|
|
self.dct_progression["state_4_uninstall_module"] = (
|
|
|
|
|
|
lst_module_uninstall_module
|
|
|
|
|
|
)
|
|
|
|
|
|
self.write_config()
|
|
|
|
|
|
|
|
|
|
|
|
option_comment += 1
|
|
|
|
|
|
msg = f"4.{index}.{chr(option_comment + 65)} - Install module"
|
|
|
|
|
|
self.add_comment_progression(msg)
|
|
|
|
|
|
|
|
|
|
|
|
config_state_4_install_module = self.dct_progression.get(
|
|
|
|
|
|
"config_state_4_install_module",
|
|
|
|
|
|
[False] * len(lst_next_version),
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
# Special case to install module to fix migration
|
|
|
|
|
|
if (
|
|
|
|
|
|
next_version == 13
|
|
|
|
|
|
and "dms" in lst_module_to_analyse
|
|
|
|
|
|
and "muk_dms"
|
|
|
|
|
|
not in self.dct_progression.get("dct_module_exist", {}).keys()
|
|
|
|
|
|
):
|
|
|
|
|
|
# Force install dms into odoo 12
|
|
|
|
|
|
if not config_state_4_install_module[index]:
|
|
|
|
|
|
config_state_4_install_module[index] = ["dms"]
|
|
|
|
|
|
elif "dms" not in config_state_4_install_module[index]:
|
|
|
|
|
|
config_state_4_install_module[index].append("dms")
|
|
|
|
|
|
|
|
|
|
|
|
if not lst_module_install_module[index]:
|
|
|
|
|
|
lst_module_to_install = config_state_4_install_module[index]
|
|
|
|
|
|
if not lst_module_to_install:
|
|
|
|
|
|
lst_module_to_install = []
|
|
|
|
|
|
|
|
|
|
|
|
if lst_module_to_install:
|
|
|
|
|
|
self.install_from_database(
|
|
|
|
|
|
lst_module_to_install,
|
|
|
|
|
|
database_name_upgrade,
|
|
|
|
|
|
next_version - 1,
|
|
|
|
|
|
)
|
|
|
|
|
|
lst_module_install_module[index] = True
|
|
|
|
|
|
self.dct_progression["state_4_module_migrate_odoo_lst"] = (
|
|
|
|
|
|
lst_module_install_module
|
|
|
|
|
|
)
|
|
|
|
|
|
self.write_config()
|
|
|
|
|
|
|
|
|
|
|
|
self.dct_progression["config_state_4_install_module"] = (
|
|
|
|
|
|
config_state_4_install_module
|
|
|
|
|
|
)
|
|
|
|
|
|
self.dct_progression["state_4_install_module"] = (
|
|
|
|
|
|
lst_module_install_module
|
|
|
|
|
|
)
|
|
|
|
|
|
self.write_config()
|
|
|
|
|
|
|
|
|
|
|
|
option_comment += 1
|
|
|
|
|
|
msg = f"4.{index}.{chr(option_comment + 65)} - Switch Odoo"
|
|
|
|
|
|
self.add_comment_progression(msg)
|
|
|
|
|
|
|
|
|
|
|
|
if not lst_switch_odoo[index]:
|
|
|
|
|
|
self.switch_odoo(next_version)
|
|
|
|
|
|
lst_switch_odoo[index] = True
|
|
|
|
|
|
self.dct_progression["state_4_switch_odoo_lst"] = (
|
|
|
|
|
|
lst_switch_odoo
|
|
|
|
|
|
)
|
|
|
|
|
|
self.write_config()
|
|
|
|
|
|
print(f"✅ -> Switch Odoo{next_version} done with update")
|
|
|
|
|
|
else:
|
|
|
|
|
|
print(f"✅ -> Switch Odoo{next_version} - nothing")
|
|
|
|
|
|
|
|
|
|
|
|
lst_state_4_module_migrate_code = self.dct_progression.get(
|
|
|
|
|
|
"config_state_4_module_to_migrate_code",
|
|
|
|
|
|
[[]] * len(lst_next_version),
|
|
|
|
|
|
)
|
|
|
|
|
|
if (
|
|
|
|
|
|
"config_state_4_module_to_migrate_code"
|
|
|
|
|
|
not in self.dct_progression.keys()
|
|
|
|
|
|
):
|
|
|
|
|
|
self.dct_progression[
|
|
|
|
|
|
"config_state_4_module_to_migrate_code"
|
|
|
|
|
|
] = lst_state_4_module_migrate_code
|
|
|
|
|
|
lst_module_to_migrate = lst_state_4_module_migrate_code[index]
|
|
|
|
|
|
|
|
|
|
|
|
option_comment += 1
|
|
|
|
|
|
msg = (
|
|
|
|
|
|
f"4.{index}.{chr(option_comment + 65)} - Search missing module"
|
|
|
|
|
|
)
|
|
|
|
|
|
self.add_comment_progression(msg)
|
|
|
|
|
|
|
|
|
|
|
|
if not lst_module_search_missing_module[index]:
|
|
|
|
|
|
lst_module_to_analyse_updated = []
|
|
|
|
|
|
for bd_module in lst_module_to_analyse:
|
|
|
|
|
|
if (
|
|
|
|
|
|
lst_module_to_uninstall
|
|
|
|
|
|
and bd_module in lst_module_to_uninstall
|
|
|
|
|
|
):
|
|
|
|
|
|
# Ignore check if uninstall before
|
|
|
|
|
|
continue
|
|
|
|
|
|
lst_module_to_analyse_updated.append(bd_module)
|
|
|
|
|
|
|
|
|
|
|
|
# TODO remove from list past module deleted
|
|
|
|
|
|
lst_module_to_check = [
|
|
|
|
|
|
a
|
|
|
|
|
|
for a in lst_module_to_analyse_updated
|
|
|
|
|
|
if a not in lst_module_to_delete_last_version
|
|
|
|
|
|
]
|
|
|
|
|
|
(
|
|
|
|
|
|
lst_module_missing_next_version,
|
|
|
|
|
|
lst_module_duplicate_next_version,
|
|
|
|
|
|
) = self.check_addons_exist(lst_module_to_check)
|
|
|
|
|
|
|
|
|
|
|
|
lst_module_missing_next_version = sorted(
|
|
|
|
|
|
list(set(lst_module_missing_next_version))
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
self.dct_progression["state_4_len_lst_module_missing"] = len(
|
|
|
|
|
|
lst_module_missing_next_version
|
|
|
|
|
|
)
|
|
|
|
|
|
self.dct_progression["state_4_lst_module_missing"] = (
|
|
|
|
|
|
lst_module_missing_next_version
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
lst_module_duplicate = sorted(
|
|
|
|
|
|
list(set(lst_module_duplicate_next_version))
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
self.dct_progression["state_4_len_lst_module_duplicate"] = len(
|
|
|
|
|
|
lst_module_duplicate
|
|
|
|
|
|
)
|
|
|
|
|
|
self.dct_progression["state_4_lst_module_duplicate"] = (
|
|
|
|
|
|
lst_module_duplicate
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
if lst_module_duplicate:
|
|
|
|
|
|
print(f"Duplicate module into odoo{next_version} : ")
|
|
|
|
|
|
print(lst_module_duplicate)
|
|
|
|
|
|
input(
|
|
|
|
|
|
f"💬 Detect error duplicate module, manage this problem manually and press to continue."
|
|
|
|
|
|
)
|
|
|
|
|
|
# if lst_module_missing_next_version and not lst_module_to_migrate:
|
|
|
|
|
|
if lst_module_missing_next_version:
|
|
|
|
|
|
# TODO support when lst_module_to_migrate is fill
|
|
|
|
|
|
lst_module_to_migrate = []
|
|
|
|
|
|
print(
|
|
|
|
|
|
f"👹 Detect error missing module, missing module into odoo{next_version} :"
|
|
|
|
|
|
)
|
|
|
|
|
|
for index_missing_module, module_missing in enumerate(
|
|
|
|
|
|
lst_module_missing_next_version
|
|
|
|
|
|
):
|
|
|
|
|
|
old_path = self.dct_progression.get(
|
|
|
|
|
|
"dct_module_exist", {}
|
|
|
|
|
|
).get(module_missing)
|
|
|
|
|
|
print(
|
|
|
|
|
|
f"[{index_missing_module}] {module_missing} - {old_path}"
|
|
|
|
|
|
)
|
|
|
|
|
|
print("[a] All list above")
|
|
|
|
|
|
print("[e] Add extra custom")
|
|
|
|
|
|
|
|
|
|
|
|
want_continue = (
|
|
|
|
|
|
input(
|
|
|
|
|
|
f"💬 Enumerate missing module separate by coma to delete it"
|
|
|
|
|
|
f". The others will be migrate : "
|
|
|
|
|
|
)
|
|
|
|
|
|
.strip()
|
|
|
|
|
|
.lower()
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
is_delete_all = False
|
|
|
|
|
|
|
|
|
|
|
|
if want_continue:
|
|
|
|
|
|
lst_want_continue = [
|
|
|
|
|
|
a.strip() for a in want_continue.split(",")
|
|
|
|
|
|
]
|
|
|
|
|
|
if "a" in lst_want_continue:
|
|
|
|
|
|
is_delete_all = True
|
|
|
|
|
|
lst_module_to_delete = [
|
|
|
|
|
|
lst_module_missing_next_version[a]
|
|
|
|
|
|
for a in range(
|
|
|
|
|
|
len(lst_module_missing_next_version)
|
|
|
|
|
|
)
|
|
|
|
|
|
]
|
|
|
|
|
|
else:
|
|
|
|
|
|
# TODO show error if the index is wrong
|
|
|
|
|
|
lst_want_continue_number = [
|
|
|
|
|
|
int(a)
|
|
|
|
|
|
for a in lst_want_continue
|
|
|
|
|
|
if a.isdigit()
|
|
|
|
|
|
]
|
|
|
|
|
|
lst_module_to_delete = [
|
|
|
|
|
|
lst_module_missing_next_version[a]
|
|
|
|
|
|
for a in lst_want_continue_number
|
|
|
|
|
|
if 0
|
|
|
|
|
|
<= a
|
|
|
|
|
|
< len(lst_module_missing_next_version)
|
|
|
|
|
|
]
|
|
|
|
|
|
if len(lst_module_to_delete) == len(
|
|
|
|
|
|
lst_module_missing_next_version
|
|
|
|
|
|
):
|
|
|
|
|
|
is_delete_all = True
|
|
|
|
|
|
|
|
|
|
|
|
if next_version == 15:
|
|
|
|
|
|
lst_module_to_delete.append("users_default_groups")
|
|
|
|
|
|
lst_module_to_delete.append(
|
|
|
|
|
|
"web_editor_backend_context"
|
|
|
|
|
|
)
|
|
|
|
|
|
lst_module_to_delete.append(
|
|
|
|
|
|
"website_google_analytics_fixed"
|
|
|
|
|
|
)
|
|
|
|
|
|
elif next_version == 17:
|
|
|
|
|
|
lst_module_to_delete.append(
|
|
|
|
|
|
"export_delete_login_log"
|
|
|
|
|
|
)
|
|
|
|
|
|
elif next_version == 18:
|
|
|
|
|
|
lst_module_to_delete.append(
|
|
|
|
|
|
"base_attachment_object_storage"
|
|
|
|
|
|
)
|
|
|
|
|
|
lst_module_to_delete.append(
|
|
|
|
|
|
"user_password_strength"
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
if "e" in lst_want_continue:
|
|
|
|
|
|
want_continue = (
|
|
|
|
|
|
input(
|
|
|
|
|
|
f"💬 Enumerate module name to delete, separate by coma : "
|
|
|
|
|
|
)
|
|
|
|
|
|
.strip()
|
|
|
|
|
|
.lower()
|
|
|
|
|
|
)
|
|
|
|
|
|
lst_to_extend = [
|
|
|
|
|
|
a.strip() for a in want_continue.split(",")
|
|
|
|
|
|
]
|
|
|
|
|
|
lst_module_to_delete.extend(lst_to_extend)
|
|
|
|
|
|
|
|
|
|
|
|
if lst_module_to_delete:
|
|
|
|
|
|
msg = f"4.{index}.{chr(option_comment + 65)}.option - Choose delete missing module"
|
|
|
|
|
|
self.add_comment_progression(msg)
|
|
|
|
|
|
|
|
|
|
|
|
self.switch_odoo(next_version - 1)
|
|
|
|
|
|
|
|
|
|
|
|
if lst_module_to_delete:
|
|
|
|
|
|
# Delete if exist database
|
|
|
|
|
|
self.todo_upgrade_execute(
|
|
|
|
|
|
f"./script/database/db_restore.py -d {database_name_upgrade} --only_drop",
|
|
|
|
|
|
)
|
|
|
|
|
|
# Duplicate database
|
|
|
|
|
|
cmd_clone_database = f"./odoo_bin.sh db --clone --from_database {last_database_name} --database {database_name_upgrade}"
|
|
|
|
|
|
self.todo_upgrade_execute(cmd_clone_database)
|
|
|
|
|
|
self.uninstall_from_database(
|
|
|
|
|
|
lst_module_to_delete,
|
|
|
|
|
|
database_name_upgrade,
|
|
|
|
|
|
next_version,
|
|
|
|
|
|
)
|
|
|
|
|
|
self.install_from_database(
|
|
|
|
|
|
lst_module_to_install,
|
|
|
|
|
|
database_name_upgrade,
|
|
|
|
|
|
next_version - 1,
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
if not is_delete_all:
|
|
|
|
|
|
msg = f"4.{index}.{chr(option_comment + 65)}.option - Choose auto-fix (not implemented yet)"
|
|
|
|
|
|
self.add_comment_progression(msg)
|
|
|
|
|
|
|
|
|
|
|
|
lst_module_to_migrate_code = set(
|
|
|
|
|
|
lst_module_missing_next_version
|
|
|
|
|
|
) - set(lst_module_to_delete)
|
|
|
|
|
|
(
|
|
|
|
|
|
lst_module_missing_last,
|
|
|
|
|
|
lst_module_duplicate_last,
|
|
|
|
|
|
lst_module_exist_last,
|
|
|
|
|
|
lst_module_error_last,
|
|
|
|
|
|
) = self.check_addons_exist(
|
|
|
|
|
|
lst_module_to_migrate_code, get_all_info=True
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
if lst_module_missing_last:
|
|
|
|
|
|
print(
|
|
|
|
|
|
f"Error missing module : {lst_module_missing_last}"
|
|
|
|
|
|
)
|
|
|
|
|
|
if lst_module_duplicate_last:
|
|
|
|
|
|
print(
|
|
|
|
|
|
f"Error duplicate module : {lst_module_duplicate_last}"
|
|
|
|
|
|
)
|
|
|
|
|
|
if lst_module_error_last:
|
|
|
|
|
|
print(
|
|
|
|
|
|
f"Error error module : {lst_module_error_last}"
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
if lst_module_exist_last:
|
|
|
|
|
|
odoo_name_last_version = (
|
|
|
|
|
|
f"odoo{next_version - 1}.0"
|
|
|
|
|
|
)
|
|
|
|
|
|
odoo_name_actual_version = f"odoo{next_version}.0"
|
|
|
|
|
|
for (
|
|
|
|
|
|
module_name,
|
|
|
|
|
|
module_path,
|
|
|
|
|
|
) in lst_module_exist_last:
|
|
|
|
|
|
module_dir_path = os.path.dirname(module_path)
|
|
|
|
|
|
module_dir_path_new_version = (
|
|
|
|
|
|
module_dir_path.replace(
|
|
|
|
|
|
odoo_name_last_version,
|
|
|
|
|
|
odoo_name_actual_version,
|
|
|
|
|
|
)
|
|
|
|
|
|
)
|
|
|
|
|
|
module_dir_path_manifest = os.path.join(
|
|
|
|
|
|
module_path, "__manifest__.py"
|
|
|
|
|
|
)
|
|
|
|
|
|
module_dir_new_version = os.path.join(
|
|
|
|
|
|
module_dir_path_new_version, module_name
|
|
|
|
|
|
)
|
|
|
|
|
|
module_dir_new_version_manifest = os.path.join(
|
|
|
|
|
|
module_dir_new_version, "__manifest__.py"
|
|
|
|
|
|
)
|
|
|
|
|
|
dct_module_to_migrate_module = {
|
|
|
|
|
|
"source_module_path": module_path,
|
|
|
|
|
|
"source_manifest_path": module_dir_path_manifest,
|
|
|
|
|
|
"source_addons_path": module_dir_path,
|
|
|
|
|
|
"target_module_path": module_dir_new_version,
|
|
|
|
|
|
"target_manifest_path": module_dir_new_version_manifest,
|
|
|
|
|
|
"target_addons_path": module_dir_path_new_version,
|
|
|
|
|
|
"module_name": module_name,
|
|
|
|
|
|
"source_version_odoo": next_version - 1,
|
|
|
|
|
|
"target_version_odoo": next_version,
|
|
|
|
|
|
}
|
|
|
|
|
|
# TODO move this into config
|
|
|
|
|
|
lst_module_to_migrate.append(
|
|
|
|
|
|
dct_module_to_migrate_module
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
self.dct_progression[
|
|
|
|
|
|
"config_state_4_module_to_migrate_code"
|
|
|
|
|
|
][index] = lst_module_to_migrate
|
|
|
|
|
|
self.write_config()
|
|
|
|
|
|
|
|
|
|
|
|
self.switch_odoo(next_version)
|
|
|
|
|
|
# TODO auto-fix
|
|
|
|
|
|
# TODO try to migrate module, find in previous version, application la migration vers une nouvelle version
|
|
|
|
|
|
# TODO ajouté menu todo qui permet de faire une migration d'un module et migrer le générateur de code.
|
|
|
|
|
|
# TODO when check module, reminder provenance
|
|
|
|
|
|
# TODO implement asyncio instead of parallel
|
|
|
|
|
|
# TODO detect when duplicate path module ou module manquant, prendre décision qui ont efface si dupliqué
|
|
|
|
|
|
# TODO pourquoi web_ir_actions_act_multi est doublé dans odoo 13
|
|
|
|
|
|
|
|
|
|
|
|
lst_module_search_missing_module[index] = True
|
|
|
|
|
|
self.dct_progression["state_4_search_missing_module"] = (
|
|
|
|
|
|
lst_module_search_missing_module
|
|
|
|
|
|
)
|
|
|
|
|
|
self.write_config()
|
|
|
|
|
|
option_comment += 1
|
|
|
|
|
|
msg = f"4.{index}.{chr(option_comment + 65)} - Migrate module"
|
|
|
|
|
|
self.add_comment_progression(msg)
|
|
|
|
|
|
lst_path_git_clone_migrate = []
|
|
|
|
|
|
|
|
|
|
|
|
if not lst_module_migrate_odoo[index]:
|
|
|
|
|
|
# TODO Searching module
|
|
|
|
|
|
# search addons/addons
|
|
|
|
|
|
# search read manifest and detect branch difference, manifest into private?
|
|
|
|
|
|
# Extract module name and run migration to another list
|
|
|
|
|
|
# Maybe check if already exist and show list or continue with overwrite
|
|
|
|
|
|
# Expliquer pourquoi on ne fait pas le oca-port, c'est
|
|
|
|
|
|
|
|
|
|
|
|
config_migrate_repo = self.dct_progression.get(
|
|
|
|
|
|
"config_migrate_repo", False
|
|
|
|
|
|
)
|
|
|
|
|
|
self.dct_progression["config_migrate_repo"] = (
|
|
|
|
|
|
config_migrate_repo
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
if config_migrate_repo:
|
|
|
|
|
|
dct_module_result = self.search_module_to_move(
|
|
|
|
|
|
next_version - 1, next_version
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
# TODO code migration
|
|
|
|
|
|
# git stash
|
|
|
|
|
|
# call odoo-module-migrate, without commit
|
|
|
|
|
|
|
|
|
|
|
|
source_module_path = dct_module_result.get(
|
|
|
|
|
|
"source_module_path"
|
|
|
|
|
|
)
|
|
|
|
|
|
if not source_module_path:
|
|
|
|
|
|
_logger.error(
|
|
|
|
|
|
f"Missing source module path '{source_module_path}'"
|
|
|
|
|
|
)
|
|
|
|
|
|
else:
|
|
|
|
|
|
if os.path.exists(
|
|
|
|
|
|
os.path.join(source_module_path, ".git")
|
|
|
|
|
|
):
|
|
|
|
|
|
self.todo_upgrade_execute(
|
|
|
|
|
|
f"cd '{source_module_path}' && git stash && cd ~-"
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
target_module_path = dct_module_result.get(
|
|
|
|
|
|
"target_module_path"
|
|
|
|
|
|
)
|
|
|
|
|
|
if not target_module_path:
|
|
|
|
|
|
_logger.error(
|
|
|
|
|
|
f"Missing target module path '{target_module_path}'"
|
|
|
|
|
|
)
|
|
|
|
|
|
else:
|
|
|
|
|
|
if os.path.exists(
|
|
|
|
|
|
os.path.join(target_module_path, ".git")
|
|
|
|
|
|
):
|
|
|
|
|
|
self.todo_upgrade_execute(
|
|
|
|
|
|
f"cd '{target_module_path}' && git stash && cd ~-"
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
lst_module_to_migrate_all = dct_module_result.get(
|
|
|
|
|
|
"lst_module", []
|
|
|
|
|
|
)
|
|
|
|
|
|
else:
|
|
|
|
|
|
lst_module_to_migrate_all = []
|
|
|
|
|
|
|
|
|
|
|
|
# TODO remove duplicate au lieu d'extend
|
|
|
|
|
|
lst_module_to_migrate_all.extend(lst_module_to_migrate)
|
|
|
|
|
|
|
|
|
|
|
|
self.internal_module_upgrade(
|
|
|
|
|
|
next_version,
|
|
|
|
|
|
lst_module_to_migrate_all,
|
|
|
|
|
|
lst_path_git_clone_migrate,
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
lst_module_migrate_odoo[index] = True
|
|
|
|
|
|
self.dct_progression["state_4_module_migrate_odoo_lst"] = (
|
|
|
|
|
|
lst_module_migrate_odoo
|
|
|
|
|
|
)
|
|
|
|
|
|
self.write_config()
|
|
|
|
|
|
|
|
|
|
|
|
print(f"✅ -> Module upgrade Odoo{next_version} done")
|
|
|
|
|
|
else:
|
|
|
|
|
|
print(f"✅ -> Module upgrade Odoo{next_version} - nothing")
|
|
|
|
|
|
|
|
|
|
|
|
option_comment += 1
|
|
|
|
|
|
msg = f"4.{index}.{chr(option_comment + 65)} - Fix migrate code"
|
|
|
|
|
|
self.add_comment_progression(msg)
|
|
|
|
|
|
|
|
|
|
|
|
if not lst_fix_migration_odoo[index]:
|
|
|
|
|
|
print("")
|
[FIX] migration 13->14: hand group_fiscal_year over to om_account_accountant
The 13.0 -> 14.0 data migration died on:
duplicate key value violates unique constraint "res_groups_name_uniq"
Key (category_id, name)=(9, Allow to define fiscal years of more or less
than a year) already exists
In 13.0 that security group is declared by the core « account » module, so the
database holds it as account.group_fiscal_year. In 14.0 core account no longer
declares it and om_account_accountant (odoomates) does. Owning no XML id of its
own, the module tries to CREATE the group and collides with the existing row.
Renaming the XML id makes Odoo UPDATE the existing record instead. The record
id is untouched, so user assignments, access rights and record rules pointing
at the group survive -- on the reference database it holds none, but the fix
must not depend on that.
The fix hook also learns a « .sql » flavour, run through psql. The « .py »
flavour is piped into « odoo<target>-bin shell », which requires loading a
not-yet-migrated database with the TARGET version's registry -- precisely what
is failing at that point. A pure SQL fix needs no ORM and no registry.
Verified end to end on a throwaway copy of the stuck database: the statement
renames one row, a second run changes nothing, and the full 13->14 OpenUpgrade
then completes (« Modules loaded. », base at 14.0.1.3, zero
res_groups_name_uniq error).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-08-01 04:21:06 -04:00
|
|
|
|
stem = os.path.join(
|
|
|
|
|
|
PATH_MIGRATION_GLOBAL,
|
|
|
|
|
|
f"fix_migration_odoo{(next_version - 1) * 10}"
|
|
|
|
|
|
f"_to_odoo{next_version * 10}",
|
2025-10-31 01:10:54 -04:00
|
|
|
|
)
|
[FIX] migration 13->14: hand group_fiscal_year over to om_account_accountant
The 13.0 -> 14.0 data migration died on:
duplicate key value violates unique constraint "res_groups_name_uniq"
Key (category_id, name)=(9, Allow to define fiscal years of more or less
than a year) already exists
In 13.0 that security group is declared by the core « account » module, so the
database holds it as account.group_fiscal_year. In 14.0 core account no longer
declares it and om_account_accountant (odoomates) does. Owning no XML id of its
own, the module tries to CREATE the group and collides with the existing row.
Renaming the XML id makes Odoo UPDATE the existing record instead. The record
id is untouched, so user assignments, access rights and record rules pointing
at the group survive -- on the reference database it holds none, but the fix
must not depend on that.
The fix hook also learns a « .sql » flavour, run through psql. The « .py »
flavour is piped into « odoo<target>-bin shell », which requires loading a
not-yet-migrated database with the TARGET version's registry -- precisely what
is failing at that point. A pure SQL fix needs no ORM and no registry.
Verified end to end on a throwaway copy of the stuck database: the statement
renames one row, a second run changes nothing, and the full 13->14 OpenUpgrade
then completes (« Modules loaded. », base at 14.0.1.3, zero
res_groups_name_uniq error).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-08-01 04:21:06 -04:00
|
|
|
|
# Two flavours. « .sql » runs through psql: no Odoo registry,
|
|
|
|
|
|
# so it works on a database not yet migrated -- exactly when
|
|
|
|
|
|
# loading it with the TARGET version's code would fail. « .py »
|
|
|
|
|
|
# is piped into the Odoo shell when the ORM is really needed.
|
|
|
|
|
|
file_path_fix_migration = ""
|
|
|
|
|
|
cmd_fix_migration = ""
|
|
|
|
|
|
if os.path.exists(f"{stem}.sql"):
|
|
|
|
|
|
file_path_fix_migration = f"{stem}.sql"
|
|
|
|
|
|
cmd_fix_migration = (
|
|
|
|
|
|
f"psql -v ON_ERROR_STOP=1 -d {database_name_upgrade}"
|
|
|
|
|
|
f" -f ./{file_path_fix_migration}"
|
|
|
|
|
|
)
|
|
|
|
|
|
elif os.path.exists(f"{stem}.py"):
|
|
|
|
|
|
file_path_fix_migration = f"{stem}.py"
|
|
|
|
|
|
cmd_fix_migration = (
|
|
|
|
|
|
f"cat ./{file_path_fix_migration} |"
|
|
|
|
|
|
f" ./odoo{next_version}.0/odoo/odoo-bin shell"
|
|
|
|
|
|
f" -d {database_name_upgrade}"
|
|
|
|
|
|
)
|
|
|
|
|
|
if file_path_fix_migration:
|
2026-07-31 23:22:02 -04:00
|
|
|
|
status, cmd_executed = self.todo_upgrade_execute(
|
[FIX] migration 13->14: hand group_fiscal_year over to om_account_accountant
The 13.0 -> 14.0 data migration died on:
duplicate key value violates unique constraint "res_groups_name_uniq"
Key (category_id, name)=(9, Allow to define fiscal years of more or less
than a year) already exists
In 13.0 that security group is declared by the core « account » module, so the
database holds it as account.group_fiscal_year. In 14.0 core account no longer
declares it and om_account_accountant (odoomates) does. Owning no XML id of its
own, the module tries to CREATE the group and collides with the existing row.
Renaming the XML id makes Odoo UPDATE the existing record instead. The record
id is untouched, so user assignments, access rights and record rules pointing
at the group survive -- on the reference database it holds none, but the fix
must not depend on that.
The fix hook also learns a « .sql » flavour, run through psql. The « .py »
flavour is piped into « odoo<target>-bin shell », which requires loading a
not-yet-migrated database with the TARGET version's registry -- precisely what
is failing at that point. A pure SQL fix needs no ORM and no registry.
Verified end to end on a throwaway copy of the stuck database: the statement
renames one row, a second run changes nothing, and the full 13->14 OpenUpgrade
then completes (« Modules loaded. », base at 14.0.1.3, zero
res_groups_name_uniq error).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-08-01 04:21:06 -04:00
|
|
|
|
cmd_fix_migration,
|
2025-10-31 01:10:54 -04:00
|
|
|
|
single_source_odoo=True,
|
|
|
|
|
|
)
|
|
|
|
|
|
|
2026-07-31 23:22:02 -04:00
|
|
|
|
# A fix that did not run must not be recorded as applied,
|
|
|
|
|
|
# otherwise the rerun skips it and OpenUpgrade hits the very
|
|
|
|
|
|
# problem the fix exists to prevent.
|
|
|
|
|
|
if status:
|
|
|
|
|
|
print(
|
|
|
|
|
|
f"❌ -> Fix migration Odoo{next_version} FAILED"
|
|
|
|
|
|
f" (status {status}):"
|
|
|
|
|
|
f" {file_path_fix_migration}"
|
|
|
|
|
|
)
|
|
|
|
|
|
return
|
|
|
|
|
|
|
2025-10-31 01:10:54 -04:00
|
|
|
|
lst_fix_migration_odoo[index] = file_path_fix_migration
|
|
|
|
|
|
self.dct_progression["state_4_fix_migration_odoo_lst"] = (
|
|
|
|
|
|
lst_fix_migration_odoo
|
|
|
|
|
|
)
|
|
|
|
|
|
self.write_config()
|
|
|
|
|
|
print(f"✅ -> Fix migration Odoo{next_version} done")
|
|
|
|
|
|
else:
|
|
|
|
|
|
print(
|
|
|
|
|
|
f"✅ -> Fix migration Odoo{next_version} - no fix to execute"
|
|
|
|
|
|
)
|
|
|
|
|
|
else:
|
|
|
|
|
|
print(f"✅ -> Fix migration Odoo{next_version} - nothing")
|
|
|
|
|
|
|
|
|
|
|
|
for path_git_clone_migrate in lst_path_git_clone_migrate:
|
|
|
|
|
|
cmd = f"./script/code/git_commit_migration_addons_path.py --path {path_git_clone_migrate} --odoo_version {next_version}.0"
|
|
|
|
|
|
self.todo_upgrade_execute(cmd)
|
|
|
|
|
|
|
|
|
|
|
|
option_comment += 1
|
|
|
|
|
|
msg = f"4.{index}.{chr(option_comment + 65)} - Migrate database"
|
|
|
|
|
|
self.add_comment_progression(msg)
|
|
|
|
|
|
|
|
|
|
|
|
if not lst_upgrade_odoo[index]:
|
|
|
|
|
|
path_addons_openupgrade = os.path.join(
|
|
|
|
|
|
os.getcwd(), f"odoo{next_version}.0", "OCA_OpenUpgrade"
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
# Update config with OCA_OpenUpgrade
|
|
|
|
|
|
ignore_path = (
|
|
|
|
|
|
"--ignore-odoo-path " if next_version <= 13 else ""
|
|
|
|
|
|
)
|
|
|
|
|
|
extra_addons_path_extra = (
|
|
|
|
|
|
f",{path_addons_openupgrade}/addons,{path_addons_openupgrade}/odoo/addons"
|
|
|
|
|
|
if next_version <= 13
|
|
|
|
|
|
else ""
|
|
|
|
|
|
)
|
|
|
|
|
|
cmd_update_config = (
|
|
|
|
|
|
f"./script/git/git_repo_update_group.py {ignore_path}"
|
|
|
|
|
|
f"--extra-addons-path {path_addons_openupgrade}{extra_addons_path_extra} "
|
|
|
|
|
|
f"&& ./script/generate_config.sh"
|
|
|
|
|
|
)
|
|
|
|
|
|
self.todo_upgrade_execute(cmd_update_config)
|
|
|
|
|
|
|
|
|
|
|
|
print("🚸 Please, validate commits after code migration.")
|
|
|
|
|
|
print("ℹ To show repo status :\nmake repo_show_status")
|
|
|
|
|
|
print(
|
|
|
|
|
|
f"🚸 Please, validate this path into config.conf : '{path_addons_openupgrade}'."
|
|
|
|
|
|
)
|
|
|
|
|
|
status = input(f"💬 Press to continue {msg} : ").strip()
|
|
|
|
|
|
# The technique change at version 14
|
|
|
|
|
|
if next_version <= 13:
|
|
|
|
|
|
erplibre_version = self.install_OCA_openupgrade(
|
|
|
|
|
|
next_version
|
|
|
|
|
|
)
|
|
|
|
|
|
cmd_upgrade = f".venv.{erplibre_version}/bin/python ./odoo{next_version}.0/OCA_OpenUpgrade/odoo-bin -c ./config.conf --update all --no-http --stop-after-init -d {database_name_upgrade}"
|
|
|
|
|
|
else:
|
|
|
|
|
|
cmd_upgrade = f"./run.sh --upgrade-path=./odoo{next_version}.0/OCA_OpenUpgrade/openupgrade_scripts/scripts --update all -c config.conf --stop-after-init --no-http --load=base,web,openupgrade_framework -d {database_name_upgrade}"
|
|
|
|
|
|
lst_upgrade_odoo[index] = cmd_upgrade
|
|
|
|
|
|
|
[ADD] migration: snapshot and diff the website COW views at each version bump
A version bump rewrites website views without announcing any of it, so "the
site looks wrong" after a migration is currently unanswerable.
snapshot_cow_views.py records every website_id view (key, mode, inherit_id,
active, arch and its md5) and diffs two snapshots. Rows come back as JSON
straight from Postgres because an arch holds newlines and pipes; the column
list is intersected with information_schema, since ir_ui_view does not expose
the same columns from 12.0 to 18.0. Snapshots hold customer template content,
so they go under private/ and stay out of git.
todo_upgrade.py takes one before and one after each OpenUpgrade run, then
prints the diff. Both are non-blocking: forensic material must never stop an
upgrade.
Measured on the real 12.0 -> 13.0 jump, the diff shows what no log reported:
71 -> 68 copies, 16 deleted and 13 created. portal.frontend_layout is not
converted but DELETED (id 2670) and RECREATED (id 3397), with its children
re-parented from one to the other. The four theme_technolibre copies,
muk_web_branding, project_agile and erplibre_website_snippets_basic_html are
dropped outright, and website_crm.contactus_thanks is renamed to
website_form.contactus_thanks.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-08-01 00:58:54 -04:00
|
|
|
|
# Record the website COW views before the data migration. The
|
|
|
|
|
|
# upgrade silently deletes and recreates copies (measured on
|
|
|
|
|
|
# 12->13: 16 copies dropped, 13 created, children re-parented),
|
|
|
|
|
|
# and rewrites the arch of many others. Without a before/after
|
|
|
|
|
|
# record, "the site looks wrong" cannot be investigated.
|
|
|
|
|
|
self.snapshot_cow_views(
|
|
|
|
|
|
database_name_upgrade, f"before_{next_version}"
|
|
|
|
|
|
)
|
|
|
|
|
|
|
[ADD] migration: neutralize the COW views that would break a version bump
key is the only thing pairing a website copy with the module view it came
from. A copy whose key matches nothing is never paired, so it never receives
the new inherit_id, never changes shape, and never joins a view combination.
Renaming the key is therefore enough to take it out of the way:
UPDATE ir_ui_view SET key = '<prefix>.' || key, active = false
active = false alone would NOT work: an inactive copy keeping the same key
still shadows the module view. Nothing is deleted either, so inherit_id
ondelete='restrict' and the website_page foreign keys are never touched, and
the old arch stays in database as a readable archive. --restore undoes it.
Written in plain psql on purpose: it runs on a database not yet migrated,
where starting an Odoo shell of the target version is not guaranteed. It is a
systematic step driven by the detector, valid for every bump, so it lives in
the loop rather than in a per-version fix_migration file.
Offered rather than forced, since those copies carry real customizations.
Verified on a throwaway copy: dry-run writes nothing; apply renames the key and
deactivates while keeping the 4058-char arch; a second apply is a no-op;
restore returns the row to its exact initial state.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-08-01 01:04:38 -04:00
|
|
|
|
# Take the copies that cannot survive this bump out of the way,
|
|
|
|
|
|
# otherwise the data migration dies on them. Offered, not
|
|
|
|
|
|
# forced: their arch is a real customization.
|
|
|
|
|
|
self.neutralize_cow_views(database_name_upgrade, next_version)
|
|
|
|
|
|
|
[IMP] migration: replay a single version bump, and stop offering a doomed retry
When OpenUpgrade failed, todo_upgrade_execute offered the generic
« [1] to redo the command ». Replaying it re-ran OpenUpgrade on the database it
had just half migrated, which never recovers. That call now passes
wait_at_error=False, and the failure message explains that the clone step has
been reset so a relaunch DROPS and REBUILDS the intermediate database from the
previous version.
The resume menu gains « [4.N] »: replay the step 4 loop from one version bump.
Only the per-version lists are trimmed from that index, so earlier bumps stay
migrated and steps 0 to 3 are untouched. Resetting the clone entry is the whole
point: the intermediate database of a failed bump must be rebuilt, not upgraded
again. « [4] » still replays every bump.
The per-bump uninstall files were also read at step 1 only, for the source
version, so uninstall_module_list_odoo130_to_odoo140.txt existed in name but
was never read. The step 4 loop now reads the file of the bump it is about to
perform, merged with the answers already stored in the progression.
Verified against the real progression (13 and 14 migrated): the menu offers
13/14/15/16/17/18, and replaying from 14 turns every per-version list from
[True, True, False...] into [True, False, ...] while state_4_reach_open_upgrade
and steps 0-3 survive. A 13->14 list of 4 modules is read with its reasons.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-08-01 04:46:17 -04:00
|
|
|
|
# wait_at_error=False on purpose: the generic « [1] to redo the
|
|
|
|
|
|
# command » would replay OpenUpgrade on a database it has just
|
|
|
|
|
|
# half migrated, which never recovers. The failure is handled
|
|
|
|
|
|
# below by dropping the clone flag so the replay REBUILDS the
|
|
|
|
|
|
# intermediate database from the previous version.
|
2026-07-31 23:22:02 -04:00
|
|
|
|
status, cmd_executed = self.todo_upgrade_execute(
|
2025-10-31 01:10:54 -04:00
|
|
|
|
cmd_upgrade,
|
|
|
|
|
|
new_env={
|
|
|
|
|
|
"OPENUPGRADE_TARGET_VERSION": f"{next_version}.0"
|
|
|
|
|
|
},
|
[IMP] migration: replay a single version bump, and stop offering a doomed retry
When OpenUpgrade failed, todo_upgrade_execute offered the generic
« [1] to redo the command ». Replaying it re-ran OpenUpgrade on the database it
had just half migrated, which never recovers. That call now passes
wait_at_error=False, and the failure message explains that the clone step has
been reset so a relaunch DROPS and REBUILDS the intermediate database from the
previous version.
The resume menu gains « [4.N] »: replay the step 4 loop from one version bump.
Only the per-version lists are trimmed from that index, so earlier bumps stay
migrated and steps 0 to 3 are untouched. Resetting the clone entry is the whole
point: the intermediate database of a failed bump must be rebuilt, not upgraded
again. « [4] » still replays every bump.
The per-bump uninstall files were also read at step 1 only, for the source
version, so uninstall_module_list_odoo130_to_odoo140.txt existed in name but
was never read. The step 4 loop now reads the file of the bump it is about to
perform, merged with the answers already stored in the progression.
Verified against the real progression (13 and 14 migrated): the menu offers
13/14/15/16/17/18, and replaying from 14 turns every per-version list from
[True, True, False...] into [True, False, ...] while state_4_reach_open_upgrade
and steps 0-3 survive. A 13->14 list of 4 modules is read with its reasons.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-08-01 04:46:17 -04:00
|
|
|
|
wait_at_error=False,
|
2025-10-31 01:10:54 -04:00
|
|
|
|
)
|
2026-07-31 23:22:02 -04:00
|
|
|
|
|
|
|
|
|
|
# This is THE data migration. Recording it as done when it
|
|
|
|
|
|
# failed used to send the loop to the next version on top of a
|
|
|
|
|
|
# half-migrated database. Stop here instead: the state stays
|
|
|
|
|
|
# unset, so a rerun replays this version.
|
|
|
|
|
|
if status:
|
2026-08-01 02:38:54 -04:00
|
|
|
|
# The intermediate database is now half migrated and must
|
|
|
|
|
|
# not be reused: drop the clone flag so the rerun rebuilds
|
|
|
|
|
|
# it from the pristine source. Without this the replay
|
|
|
|
|
|
# would restart OpenUpgrade on top of the broken clone.
|
|
|
|
|
|
lst_clone_odoo[index] = False
|
|
|
|
|
|
self.dct_progression["state_4_clone_odoo_lst"] = (
|
|
|
|
|
|
lst_clone_odoo
|
|
|
|
|
|
)
|
|
|
|
|
|
self.write_config()
|
2026-07-31 23:22:02 -04:00
|
|
|
|
print(
|
[IMP] migration: replay a single version bump, and stop offering a doomed retry
When OpenUpgrade failed, todo_upgrade_execute offered the generic
« [1] to redo the command ». Replaying it re-ran OpenUpgrade on the database it
had just half migrated, which never recovers. That call now passes
wait_at_error=False, and the failure message explains that the clone step has
been reset so a relaunch DROPS and REBUILDS the intermediate database from the
previous version.
The resume menu gains « [4.N] »: replay the step 4 loop from one version bump.
Only the per-version lists are trimmed from that index, so earlier bumps stay
migrated and steps 0 to 3 are untouched. Resetting the clone entry is the whole
point: the intermediate database of a failed bump must be rebuilt, not upgraded
again. « [4] » still replays every bump.
The per-bump uninstall files were also read at step 1 only, for the source
version, so uninstall_module_list_odoo130_to_odoo140.txt existed in name but
was never read. The step 4 loop now reads the file of the bump it is about to
perform, merged with the answers already stored in the progression.
Verified against the real progression (13 and 14 migrated): the menu offers
13/14/15/16/17/18, and replaying from 14 turns every per-version list from
[True, True, False...] into [True, False, ...] while state_4_reach_open_upgrade
and steps 0-3 survive. A 13->14 list of 4 modules is read with its reasons.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-08-01 04:46:17 -04:00
|
|
|
|
f"\n❌ -> Database migration to Odoo{next_version}"
|
|
|
|
|
|
f" FAILED (status {status}).\n"
|
|
|
|
|
|
f" '{database_name_upgrade}' is now half migrated:"
|
|
|
|
|
|
" replaying the command on it would never recover, so"
|
|
|
|
|
|
" it is NOT offered.\n"
|
|
|
|
|
|
" The clone step has been reset. Fix the cause, then"
|
|
|
|
|
|
" relaunch the migration and answer [c] (continue):"
|
|
|
|
|
|
f" '{database_name_upgrade}' will be dropped and"
|
|
|
|
|
|
" rebuilt from the previous version before retrying."
|
2026-07-31 23:22:02 -04:00
|
|
|
|
)
|
|
|
|
|
|
return
|
2025-10-31 01:10:54 -04:00
|
|
|
|
|
[ADD] migration: snapshot and diff the website COW views at each version bump
A version bump rewrites website views without announcing any of it, so "the
site looks wrong" after a migration is currently unanswerable.
snapshot_cow_views.py records every website_id view (key, mode, inherit_id,
active, arch and its md5) and diffs two snapshots. Rows come back as JSON
straight from Postgres because an arch holds newlines and pipes; the column
list is intersected with information_schema, since ir_ui_view does not expose
the same columns from 12.0 to 18.0. Snapshots hold customer template content,
so they go under private/ and stay out of git.
todo_upgrade.py takes one before and one after each OpenUpgrade run, then
prints the diff. Both are non-blocking: forensic material must never stop an
upgrade.
Measured on the real 12.0 -> 13.0 jump, the diff shows what no log reported:
71 -> 68 copies, 16 deleted and 13 created. portal.frontend_layout is not
converted but DELETED (id 2670) and RECREATED (id 3397), with its children
re-parented from one to the other. The four theme_technolibre copies,
muk_web_branding, project_agile and erplibre_website_snippets_basic_html are
dropped outright, and website_crm.contactus_thanks is renamed to
website_form.contactus_thanks.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-08-01 00:58:54 -04:00
|
|
|
|
self.snapshot_cow_views(
|
|
|
|
|
|
database_name_upgrade, f"after_{next_version}"
|
|
|
|
|
|
)
|
|
|
|
|
|
self.diff_cow_views(
|
|
|
|
|
|
database_name_upgrade,
|
|
|
|
|
|
f"before_{next_version}",
|
|
|
|
|
|
f"after_{next_version}",
|
|
|
|
|
|
)
|
|
|
|
|
|
|
2025-10-31 01:10:54 -04:00
|
|
|
|
self.dct_progression["state_4_upgrade_odoo_lst"] = (
|
|
|
|
|
|
lst_upgrade_odoo
|
|
|
|
|
|
)
|
|
|
|
|
|
self.write_config()
|
|
|
|
|
|
|
|
|
|
|
|
str_wait_next_version = (
|
|
|
|
|
|
" (or wait next version 🤖)"
|
|
|
|
|
|
if next_version != lst_next_version[-1]
|
|
|
|
|
|
else ""
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
status = (
|
|
|
|
|
|
input(
|
|
|
|
|
|
f"💬 Do you want to upgrade all{str_wait_next_version}? Press y/Y to upgrade all addons database : "
|
|
|
|
|
|
)
|
|
|
|
|
|
.strip()
|
|
|
|
|
|
.lower()
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
if status == "y":
|
|
|
|
|
|
self.todo_upgrade_execute(
|
|
|
|
|
|
f"./script/addons/update_addons_all.sh {database_name_upgrade}",
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
print(f"✅ -> Database upgrade Odoo{next_version} done")
|
|
|
|
|
|
|
|
|
|
|
|
# Update config without OCA_OpenUpgrade
|
|
|
|
|
|
cmd_update_config = f"./script/git/git_repo_update_group.py && ./script/generate_config.sh"
|
|
|
|
|
|
self.todo_upgrade_execute(cmd_update_config)
|
|
|
|
|
|
print("[y] Open server with Selenium")
|
|
|
|
|
|
status = (
|
|
|
|
|
|
input(
|
|
|
|
|
|
"💬 Do you want to test this upgrade? Choose or press to ignore it : "
|
|
|
|
|
|
)
|
|
|
|
|
|
.strip()
|
|
|
|
|
|
.lower()
|
|
|
|
|
|
)
|
|
|
|
|
|
"make repo_show_status"
|
|
|
|
|
|
if status == "y":
|
|
|
|
|
|
self.todo.prompt_execute_selenium_and_run_db(
|
|
|
|
|
|
database_name_upgrade
|
|
|
|
|
|
)
|
|
|
|
|
|
status = input(
|
|
|
|
|
|
f"💬 Press to continue 4.{index} : "
|
|
|
|
|
|
).strip()
|
|
|
|
|
|
else:
|
|
|
|
|
|
print(f"✅ -> Database upgrade Odoo{next_version} - nothing")
|
|
|
|
|
|
|
|
|
|
|
|
#
|
|
|
|
|
|
# waiting_input = input("💬 Press any keyboard key to continue...")
|
|
|
|
|
|
print("")
|
|
|
|
|
|
|
|
|
|
|
|
msg = "5 - Cleaning up database after upgrade"
|
|
|
|
|
|
print(f"🔷 {msg}")
|
|
|
|
|
|
self.add_comment_progression(msg)
|
|
|
|
|
|
|
|
|
|
|
|
print(
|
|
|
|
|
|
"✨ Re-update i18n, purger data, tables (except mail_test and mail_test_full)"
|
|
|
|
|
|
)
|
|
|
|
|
|
# waiting_input = input("💬print Press any keyboard key to continue...")
|
|
|
|
|
|
msg = "6 - Migration finished"
|
|
|
|
|
|
print(f"🔷 {msg}")
|
|
|
|
|
|
self.add_comment_progression(msg)
|
|
|
|
|
|
|
|
|
|
|
|
cmd_backup_template = f"./odoo_bin.sh db --backup --database {database_name_upgrade} --restore_image"
|
|
|
|
|
|
cmd_backup = f"{cmd_backup_template} {database_name_upgrade}_finish_{datetime.datetime.now().strftime('%Y%m%d%H%M%S')}"
|
|
|
|
|
|
print(f"✨ Can execute backup creation :\n{cmd_backup}")
|
|
|
|
|
|
status = input(
|
|
|
|
|
|
"💬 Press y/Y or write filename.zip to export or enter to continue : "
|
|
|
|
|
|
).strip()
|
|
|
|
|
|
if status.lower():
|
|
|
|
|
|
if status.lower() != "y":
|
|
|
|
|
|
cmd_backup = f"{cmd_backup_template} {status}"
|
|
|
|
|
|
self.todo_upgrade_execute(cmd_backup)
|
|
|
|
|
|
|
|
|
|
|
|
status = input("💬 Test the migration, press y/Y : ")
|
|
|
|
|
|
if status.lower().strip() == "y":
|
|
|
|
|
|
self.todo.prompt_execute_selenium_and_run_db(database_name_upgrade)
|
|
|
|
|
|
|
|
|
|
|
|
def get_rename_module(self, lst_module, next_version):
|
|
|
|
|
|
path_search = f"odoo{next_version}.0/OCA_OpenUpgrade/"
|
|
|
|
|
|
status, cmd_executed, lst_output = self.todo_upgrade_execute(
|
|
|
|
|
|
f"find {path_search} -name apriori.py",
|
|
|
|
|
|
get_output=True,
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
if not lst_output:
|
|
|
|
|
|
_logger.error(
|
|
|
|
|
|
f"Cannot find renamed module script apriori.py into path '{path_search}'"
|
|
|
|
|
|
)
|
|
|
|
|
|
return lst_module
|
|
|
|
|
|
apriory_py = lst_output[0].strip()
|
|
|
|
|
|
|
|
|
|
|
|
with open(apriory_py, "r") as f:
|
|
|
|
|
|
file_content = f.read()
|
|
|
|
|
|
|
|
|
|
|
|
data_vars = {}
|
|
|
|
|
|
exec(file_content, data_vars)
|
|
|
|
|
|
renamed_modules = data_vars.get("renamed_modules", {})
|
|
|
|
|
|
merged_modules = data_vars.get("merged_modules", {})
|
|
|
|
|
|
deleted_modules = data_vars.get("deleted_modules", [])
|
|
|
|
|
|
|
|
|
|
|
|
lst_index_to_delete = []
|
|
|
|
|
|
for index, module in enumerate(lst_module):
|
|
|
|
|
|
renamed_module = renamed_modules.get(module)
|
|
|
|
|
|
merged_module = merged_modules.get(module)
|
|
|
|
|
|
if renamed_module:
|
|
|
|
|
|
lst_module[index] = renamed_module
|
|
|
|
|
|
if merged_module:
|
|
|
|
|
|
lst_module[index] = merged_module
|
|
|
|
|
|
if module in deleted_modules:
|
|
|
|
|
|
lst_index_to_delete.append(index)
|
|
|
|
|
|
for index_to_delete in lst_index_to_delete[::-1]:
|
|
|
|
|
|
lst_module.pop(index_to_delete)
|
|
|
|
|
|
return list(set(lst_module))
|
|
|
|
|
|
|
|
|
|
|
|
def search_module_to_move(self, source_version_odoo, target_version_odoo):
|
|
|
|
|
|
lst_module = []
|
|
|
|
|
|
# lst_path_to_check = [
|
|
|
|
|
|
# os.path.join(f"odoo{actual_version_odoo}", "addons"),
|
|
|
|
|
|
# os.path.join(f"odoo{target_version_odoo}", "addons"),
|
|
|
|
|
|
# os.path.join(f"private", "addons"),
|
|
|
|
|
|
# ]
|
|
|
|
|
|
source_path_to_check = os.path.join(
|
|
|
|
|
|
f"odoo{source_version_odoo}.0", "addons", "addons"
|
|
|
|
|
|
)
|
|
|
|
|
|
target_path_to_check = os.path.join(
|
|
|
|
|
|
f"odoo{target_version_odoo}.0", "addons", "addons"
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
# Search
|
|
|
|
|
|
is_moving_git = False
|
|
|
|
|
|
if os.path.exists(source_path_to_check):
|
|
|
|
|
|
if os.path.exists(os.path.join(source_path_to_check, ".git")):
|
|
|
|
|
|
is_moving_git = True
|
|
|
|
|
|
if not os.path.exists(target_path_to_check):
|
|
|
|
|
|
shutil.copytree(source_path_to_check, target_path_to_check)
|
|
|
|
|
|
# if not is_moving_git:
|
|
|
|
|
|
# os.mkdir(path_to_check_target)
|
|
|
|
|
|
# else:
|
|
|
|
|
|
# # TODO clone
|
|
|
|
|
|
# pass
|
|
|
|
|
|
|
|
|
|
|
|
if not is_moving_git:
|
|
|
|
|
|
# TODO do something
|
|
|
|
|
|
# Time to compare
|
|
|
|
|
|
os.listdir(source_path_to_check)
|
|
|
|
|
|
|
|
|
|
|
|
if os.path.exists(target_path_to_check):
|
|
|
|
|
|
for dir_name in os.listdir(target_path_to_check):
|
|
|
|
|
|
source_module_path = os.path.join(
|
|
|
|
|
|
source_path_to_check, dir_name
|
|
|
|
|
|
)
|
|
|
|
|
|
source_manifest_path = os.path.join(
|
|
|
|
|
|
source_module_path, "__manifest__.py"
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
target_module_path = os.path.join(
|
|
|
|
|
|
target_path_to_check, dir_name
|
|
|
|
|
|
)
|
|
|
|
|
|
target_manifest_path = os.path.join(
|
|
|
|
|
|
target_module_path, "__manifest__.py"
|
|
|
|
|
|
)
|
|
|
|
|
|
if os.path.exists(target_manifest_path):
|
|
|
|
|
|
# TODO remove from list when module already exist in version 15
|
|
|
|
|
|
dct_module = {
|
|
|
|
|
|
"source_module_path": source_module_path,
|
|
|
|
|
|
"source_manifest_path": source_manifest_path,
|
|
|
|
|
|
"source_addons_path": source_path_to_check,
|
|
|
|
|
|
"target_module_path": target_module_path,
|
|
|
|
|
|
"target_manifest_path": target_manifest_path,
|
|
|
|
|
|
"target_addons_path": target_path_to_check,
|
|
|
|
|
|
"module_name": dir_name,
|
|
|
|
|
|
"source_version_odoo": source_version_odoo,
|
|
|
|
|
|
"target_version_odoo": target_version_odoo,
|
|
|
|
|
|
}
|
|
|
|
|
|
lst_module.append(dct_module)
|
|
|
|
|
|
|
|
|
|
|
|
dct_module = {
|
|
|
|
|
|
"lst_module": lst_module,
|
|
|
|
|
|
"source_module_path": source_path_to_check,
|
|
|
|
|
|
"target_module_path": target_path_to_check,
|
|
|
|
|
|
}
|
|
|
|
|
|
return dct_module
|
|
|
|
|
|
|
[ADD] migration: snapshot and diff the website COW views at each version bump
A version bump rewrites website views without announcing any of it, so "the
site looks wrong" after a migration is currently unanswerable.
snapshot_cow_views.py records every website_id view (key, mode, inherit_id,
active, arch and its md5) and diffs two snapshots. Rows come back as JSON
straight from Postgres because an arch holds newlines and pipes; the column
list is intersected with information_schema, since ir_ui_view does not expose
the same columns from 12.0 to 18.0. Snapshots hold customer template content,
so they go under private/ and stay out of git.
todo_upgrade.py takes one before and one after each OpenUpgrade run, then
prints the diff. Both are non-blocking: forensic material must never stop an
upgrade.
Measured on the real 12.0 -> 13.0 jump, the diff shows what no log reported:
71 -> 68 copies, 16 deleted and 13 created. portal.frontend_layout is not
converted but DELETED (id 2670) and RECREATED (id 3397), with its children
re-parented from one to the other. The four theme_technolibre copies,
muk_web_branding, project_agile and erplibre_website_snippets_basic_html are
dropped outright, and website_crm.contactus_thanks is renamed to
website_form.contactus_thanks.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-08-01 00:58:54 -04:00
|
|
|
|
def snapshot_cow_views(self, database_name, label):
|
|
|
|
|
|
"""Record the website COW views of a database under private/.
|
|
|
|
|
|
|
|
|
|
|
|
Never blocks the migration: a snapshot is forensic material, its
|
|
|
|
|
|
absence must not stop an upgrade.
|
|
|
|
|
|
"""
|
|
|
|
|
|
self.todo_upgrade_execute(
|
|
|
|
|
|
f"{PYTHON_BIN} ./script/odoo/migration/snapshot_cow_views.py"
|
|
|
|
|
|
f" -d {database_name} -l {label}",
|
|
|
|
|
|
wait_at_error=False,
|
|
|
|
|
|
)
|
|
|
|
|
|
|
[ADD] migration: neutralize the COW views that would break a version bump
key is the only thing pairing a website copy with the module view it came
from. A copy whose key matches nothing is never paired, so it never receives
the new inherit_id, never changes shape, and never joins a view combination.
Renaming the key is therefore enough to take it out of the way:
UPDATE ir_ui_view SET key = '<prefix>.' || key, active = false
active = false alone would NOT work: an inactive copy keeping the same key
still shadows the module view. Nothing is deleted either, so inherit_id
ondelete='restrict' and the website_page foreign keys are never touched, and
the old arch stays in database as a readable archive. --restore undoes it.
Written in plain psql on purpose: it runs on a database not yet migrated,
where starting an Odoo shell of the target version is not guaranteed. It is a
systematic step driven by the detector, valid for every bump, so it lives in
the loop rather than in a per-version fix_migration file.
Offered rather than forced, since those copies carry real customizations.
Verified on a throwaway copy: dry-run writes nothing; apply renames the key and
deactivates while keeping the 4058-char arch; a second apply is a no-op;
restore returns the row to its exact initial state.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-08-01 01:04:38 -04:00
|
|
|
|
def neutralize_cow_views(self, database_name, next_version):
|
|
|
|
|
|
"""Offer to neutralize the COW views that would break this bump.
|
|
|
|
|
|
|
|
|
|
|
|
Renaming their key unpairs them from the module view, so the upgrade
|
|
|
|
|
|
stops choking on them. Nothing is deleted and the operation is
|
|
|
|
|
|
reversible (neutralize_cow_views.py --restore), but the choice belongs
|
|
|
|
|
|
to the user: those copies carry real customizations.
|
|
|
|
|
|
"""
|
|
|
|
|
|
cmd = (
|
|
|
|
|
|
f"{PYTHON_BIN} ./script/odoo/migration/neutralize_cow_views.py"
|
|
|
|
|
|
f" -d {database_name} -t odoo{next_version}.0"
|
|
|
|
|
|
)
|
|
|
|
|
|
status, cmd_executed, output = self.todo_upgrade_execute(
|
|
|
|
|
|
cmd, get_output=True, wait_at_error=False
|
|
|
|
|
|
)
|
|
|
|
|
|
if "No website COW view to neutralize" in "\n".join(output or []):
|
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
|
|
answer = (
|
|
|
|
|
|
input(
|
|
|
|
|
|
"💬 Neutralize these copies so the upgrade can proceed?"
|
|
|
|
|
|
" Their arch is kept and the change is reversible."
|
|
|
|
|
|
" (Y/n) : "
|
|
|
|
|
|
)
|
|
|
|
|
|
.strip()
|
|
|
|
|
|
.lower()
|
|
|
|
|
|
)
|
|
|
|
|
|
if answer == "n":
|
|
|
|
|
|
print(
|
|
|
|
|
|
"⚠️ -> Skipped. The data migration will very likely stop on"
|
|
|
|
|
|
" these views."
|
|
|
|
|
|
)
|
|
|
|
|
|
return
|
|
|
|
|
|
self.todo_upgrade_execute(f"{cmd} --apply", wait_at_error=False)
|
|
|
|
|
|
|
[ADD] migration: snapshot and diff the website COW views at each version bump
A version bump rewrites website views without announcing any of it, so "the
site looks wrong" after a migration is currently unanswerable.
snapshot_cow_views.py records every website_id view (key, mode, inherit_id,
active, arch and its md5) and diffs two snapshots. Rows come back as JSON
straight from Postgres because an arch holds newlines and pipes; the column
list is intersected with information_schema, since ir_ui_view does not expose
the same columns from 12.0 to 18.0. Snapshots hold customer template content,
so they go under private/ and stay out of git.
todo_upgrade.py takes one before and one after each OpenUpgrade run, then
prints the diff. Both are non-blocking: forensic material must never stop an
upgrade.
Measured on the real 12.0 -> 13.0 jump, the diff shows what no log reported:
71 -> 68 copies, 16 deleted and 13 created. portal.frontend_layout is not
converted but DELETED (id 2670) and RECREATED (id 3397), with its children
re-parented from one to the other. The four theme_technolibre copies,
muk_web_branding, project_agile and erplibre_website_snippets_basic_html are
dropped outright, and website_crm.contactus_thanks is renamed to
website_form.contactus_thanks.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-08-01 00:58:54 -04:00
|
|
|
|
def diff_cow_views(self, database_name, label_before, label_after):
|
|
|
|
|
|
"""Print what the version bump did to the website COW views."""
|
|
|
|
|
|
directory = os.path.join(
|
|
|
|
|
|
PATH_MIGRATION_PRIVATE, database_name, "cow_snapshots"
|
|
|
|
|
|
)
|
|
|
|
|
|
path_before = os.path.join(directory, f"{label_before}.json")
|
|
|
|
|
|
path_after = os.path.join(directory, f"{label_after}.json")
|
|
|
|
|
|
if not (os.path.exists(path_before) and os.path.exists(path_after)):
|
|
|
|
|
|
return
|
|
|
|
|
|
self.todo_upgrade_execute(
|
|
|
|
|
|
f"{PYTHON_BIN} ./script/odoo/migration/snapshot_cow_views.py"
|
|
|
|
|
|
f" --diff {path_before} {path_after}",
|
|
|
|
|
|
wait_at_error=False,
|
|
|
|
|
|
)
|
|
|
|
|
|
|
[IMP] migration: per-database uninstall list, with a stated reason
Which modules must be uninstalled before a version bump depends on the data of
one specific database, so that list does not belong to a shared versioned file.
- Read private/odoo/migration/<database>/uninstall_module_list_odooXX0_to_odooYY0.txt
first, then the shared versioned defaults under script/odoo/migration/, and
merge them (duplicates dropped). private/ mirrors script/, the convention
already used by script/todo/todo.json -> private/todo/todo_override.json.
- Fix the parser. It was « f.readline().split() »: only the FIRST line was kept,
so a multi-line list was silently truncated, and a comma-separated list turned
into one bogus module name. On a file starting with a comment it returned the
words of that comment as module names. It now reads every line and accepts
commas, several names per line, blank lines and comments.
- Support a « # reason » justification per module, printed before uninstalling;
a module with no reason is flagged. Removing a module must stay reviewable.
- Ignore private/odoo/ in git: these lists describe one database.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-31 23:25:43 -04:00
|
|
|
|
@staticmethod
|
|
|
|
|
|
def parse_module_list_file(file_path):
|
|
|
|
|
|
"""Read a module list file, return [(module, reason), ...].
|
|
|
|
|
|
|
|
|
|
|
|
Accepted syntax, one module per line with an optional justification:
|
|
|
|
|
|
|
|
|
|
|
|
queue_job # blocks 12->13, trigger queue_job_notify
|
|
|
|
|
|
mgmtsystem_hazard # not ported to 13.0
|
|
|
|
|
|
|
|
|
|
|
|
Commas and several names per line are also accepted, so a list copied
|
|
|
|
|
|
from a command line works as-is. Blank lines and full-line comments are
|
|
|
|
|
|
ignored.
|
|
|
|
|
|
|
|
|
|
|
|
The previous parser was « f.readline().split() »: it kept only the FIRST
|
|
|
|
|
|
line, so a multi-line list was silently truncated, and a comma-separated
|
|
|
|
|
|
list collapsed into one bogus module name.
|
|
|
|
|
|
"""
|
|
|
|
|
|
lst_module = []
|
|
|
|
|
|
with open(file_path, "r", encoding="utf-8") as f:
|
|
|
|
|
|
for line in f:
|
|
|
|
|
|
content, _, reason = line.partition("#")
|
|
|
|
|
|
for module_name in content.replace(",", " ").split():
|
|
|
|
|
|
lst_module.append((module_name, reason.strip()))
|
|
|
|
|
|
return lst_module
|
|
|
|
|
|
|
[IMP] migration: replay a single version bump, and stop offering a doomed retry
When OpenUpgrade failed, todo_upgrade_execute offered the generic
« [1] to redo the command ». Replaying it re-ran OpenUpgrade on the database it
had just half migrated, which never recovers. That call now passes
wait_at_error=False, and the failure message explains that the clone step has
been reset so a relaunch DROPS and REBUILDS the intermediate database from the
previous version.
The resume menu gains « [4.N] »: replay the step 4 loop from one version bump.
Only the per-version lists are trimmed from that index, so earlier bumps stay
migrated and steps 0 to 3 are untouched. Resetting the clone entry is the whole
point: the intermediate database of a failed bump must be rebuilt, not upgraded
again. « [4] » still replays every bump.
The per-bump uninstall files were also read at step 1 only, for the source
version, so uninstall_module_list_odoo130_to_odoo140.txt existed in name but
was never read. The step 4 loop now reads the file of the bump it is about to
perform, merged with the answers already stored in the progression.
Verified against the real progression (13 and 14 migrated): the menu offers
13/14/15/16/17/18, and replaying from 14 turns every per-version list from
[True, True, False...] into [True, False, ...] while state_4_reach_open_upgrade
and steps 0-3 survive. A 13->14 list of 4 modules is read with its reasons.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-08-01 04:46:17 -04:00
|
|
|
|
@staticmethod
|
|
|
|
|
|
def print_uninstall_reason(lst_detail):
|
|
|
|
|
|
"""Show WHY each module goes away.
|
|
|
|
|
|
|
|
|
|
|
|
Which modules must be dropped depends on the database, and a removal
|
|
|
|
|
|
without a stated reason is a decision nobody can review later.
|
|
|
|
|
|
"""
|
|
|
|
|
|
for name, reason, origin in lst_detail:
|
|
|
|
|
|
print(
|
|
|
|
|
|
f" - {name}"
|
|
|
|
|
|
+ (f" — {reason}" if reason else " — ⚠️ no reason given")
|
|
|
|
|
|
+ f" [{origin}]"
|
|
|
|
|
|
)
|
|
|
|
|
|
|
[IMP] migration: per-database uninstall list, with a stated reason
Which modules must be uninstalled before a version bump depends on the data of
one specific database, so that list does not belong to a shared versioned file.
- Read private/odoo/migration/<database>/uninstall_module_list_odooXX0_to_odooYY0.txt
first, then the shared versioned defaults under script/odoo/migration/, and
merge them (duplicates dropped). private/ mirrors script/, the convention
already used by script/todo/todo.json -> private/todo/todo_override.json.
- Fix the parser. It was « f.readline().split() »: only the FIRST line was kept,
so a multi-line list was silently truncated, and a comma-separated list turned
into one bogus module name. On a file starting with a comment it returned the
words of that comment as module names. It now reads every line and accepts
commas, several names per line, blank lines and comments.
- Support a « # reason » justification per module, printed before uninstalling;
a module with no reason is flagged. Removing a module must stay reviewable.
- Ignore private/odoo/ in git: these lists describe one database.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-31 23:25:43 -04:00
|
|
|
|
def read_uninstall_module_list(self, start_version, database_name):
|
|
|
|
|
|
"""Modules to uninstall before migrating start_version -> next.
|
|
|
|
|
|
|
|
|
|
|
|
Reads the per-database private list first, then the shared versioned
|
|
|
|
|
|
defaults; duplicates are dropped, keeping the first occurrence.
|
|
|
|
|
|
|
|
|
|
|
|
Returns (lst_module, lst_detail) where lst_detail carries
|
|
|
|
|
|
(module, reason, origin_file) so the caller can justify each removal.
|
|
|
|
|
|
"""
|
|
|
|
|
|
file_name = (
|
|
|
|
|
|
f"uninstall_module_list_odoo{start_version * 10}"
|
|
|
|
|
|
f"_to_odoo{(start_version + 1) * 10}.txt"
|
|
|
|
|
|
)
|
|
|
|
|
|
lst_path = [
|
|
|
|
|
|
os.path.join(PATH_MIGRATION_PRIVATE, database_name, file_name),
|
|
|
|
|
|
os.path.join(PATH_MIGRATION_GLOBAL, file_name),
|
|
|
|
|
|
]
|
|
|
|
|
|
|
|
|
|
|
|
lst_module = []
|
|
|
|
|
|
lst_detail = []
|
|
|
|
|
|
for file_path in lst_path:
|
|
|
|
|
|
if not os.path.exists(file_path):
|
|
|
|
|
|
continue
|
|
|
|
|
|
for module_name, reason in self.parse_module_list_file(file_path):
|
|
|
|
|
|
if module_name in lst_module:
|
|
|
|
|
|
continue
|
|
|
|
|
|
lst_module.append(module_name)
|
|
|
|
|
|
lst_detail.append((module_name, reason, file_path))
|
|
|
|
|
|
return lst_module, lst_detail
|
|
|
|
|
|
|
[FIX] migration: tell modules with no code from those to uninstall
Uninstalling before the 13->14 bump asked for 15 modules; 12 of them no longer
exist in the 13.0 addons path. check_addons_exist.py rejects the command on
the first missing name, so the uninstall aborted whole — and the 3 modules
that WERE there stayed installed, blocking the bump for a reason that had
nothing to do with them.
The list is now split before the script is called, using check_addons_exist
which was already there and simply never consulted at this point. « Missing »
means the addons path has no code for it, not that the database lacks it —
the distinction is the whole point, since Odoo cannot uninstall a module whose
code is gone.
When some are missing, a choice is offered rather than a failure:
[1] uninstall the present ones, skip the missing (default)
[2] try the whole list anyway (it will fail)
[3] uninstall nothing, continue
Only what is really uninstalled is subtracted from the per-version module
list, so a module left in place stays counted as installed — which it is. The
missing names are written to the progression comments, so what was skipped is
still on record afterwards.
Verified on the exact list from the failure: 3 present and 12 missing,
option 1 issuing a command with only the 3 and leaving the 12 counted as
installed, option 2 sending all 15, option 3 running nothing.
Also swapped entries 4 and 5 of the Deploy menu — QEMU/KVM now sits at [4],
NTFY at [5].
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-08-01 07:16:26 -04:00
|
|
|
|
def split_present_missing(self, lst_module):
|
|
|
|
|
|
"""Split a module list into (present, missing) against the ACTIVE code.
|
|
|
|
|
|
|
|
|
|
|
|
« Missing » means the addons path no longer holds the module — not
|
|
|
|
|
|
that it is absent from the database. The distinction matters because
|
|
|
|
|
|
check_addons_exist.py refuses the WHOLE uninstall for a single missing
|
|
|
|
|
|
name, so the modules that ARE there never get uninstalled either.
|
|
|
|
|
|
"""
|
|
|
|
|
|
lst_missing, _lst_duplicate = self.check_addons_exist(lst_module)
|
|
|
|
|
|
set_missing = set(lst_missing or [])
|
|
|
|
|
|
return (
|
|
|
|
|
|
[name for name in lst_module if name not in set_missing],
|
|
|
|
|
|
[name for name in lst_module if name in set_missing],
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
def prompt_uninstall_missing(self, lst_present, lst_missing):
|
|
|
|
|
|
"""Ask what to do when part of the list has no code left.
|
|
|
|
|
|
Returns the list to actually uninstall (possibly empty)."""
|
|
|
|
|
|
print()
|
|
|
|
|
|
print(
|
|
|
|
|
|
f"⚠️ {len(lst_missing)} "
|
|
|
|
|
|
f"{t('modules of the list have no code in the active Odoo:')}"
|
|
|
|
|
|
)
|
|
|
|
|
|
for name in lst_missing:
|
|
|
|
|
|
print(f" {name}")
|
|
|
|
|
|
print(f" {t('Odoo cannot uninstall a module whose code is gone;')}")
|
|
|
|
|
|
print(f" {t('one of them fails the whole uninstall command.')}")
|
|
|
|
|
|
print()
|
|
|
|
|
|
if lst_present:
|
|
|
|
|
|
print(
|
|
|
|
|
|
f" {len(lst_present)} "
|
|
|
|
|
|
f"{t('are present and can be uninstalled:')}"
|
|
|
|
|
|
)
|
|
|
|
|
|
for name in lst_present:
|
|
|
|
|
|
print(f" {name}")
|
|
|
|
|
|
else:
|
|
|
|
|
|
print(f" {t('No module of the list is present.')}")
|
|
|
|
|
|
print()
|
|
|
|
|
|
if lst_present:
|
|
|
|
|
|
print(
|
|
|
|
|
|
f" [1] {t('Uninstall the present ones, skip the missing')}"
|
|
|
|
|
|
" *"
|
|
|
|
|
|
)
|
|
|
|
|
|
print(f" [2] {t('Try the whole list anyway (it will fail)')}")
|
|
|
|
|
|
print(f" [3] {t('Uninstall nothing, continue')}")
|
|
|
|
|
|
answer = input(f"💬 {t('Your choice')} : ").strip()
|
|
|
|
|
|
if answer == "2":
|
|
|
|
|
|
return lst_present + lst_missing
|
|
|
|
|
|
if answer == "3" or not lst_present:
|
|
|
|
|
|
return []
|
|
|
|
|
|
return lst_present
|
|
|
|
|
|
|
2025-10-31 01:10:54 -04:00
|
|
|
|
def uninstall_from_database(
|
|
|
|
|
|
self, lst_module_to_uninstall, database_name, actual_version
|
|
|
|
|
|
):
|
|
|
|
|
|
if not lst_module_to_uninstall:
|
|
|
|
|
|
return
|
[FIX] migration: tell modules with no code from those to uninstall
Uninstalling before the 13->14 bump asked for 15 modules; 12 of them no longer
exist in the 13.0 addons path. check_addons_exist.py rejects the command on
the first missing name, so the uninstall aborted whole — and the 3 modules
that WERE there stayed installed, blocking the bump for a reason that had
nothing to do with them.
The list is now split before the script is called, using check_addons_exist
which was already there and simply never consulted at this point. « Missing »
means the addons path has no code for it, not that the database lacks it —
the distinction is the whole point, since Odoo cannot uninstall a module whose
code is gone.
When some are missing, a choice is offered rather than a failure:
[1] uninstall the present ones, skip the missing (default)
[2] try the whole list anyway (it will fail)
[3] uninstall nothing, continue
Only what is really uninstalled is subtracted from the per-version module
list, so a module left in place stays counted as installed — which it is. The
missing names are written to the progression comments, so what was skipped is
still on record afterwards.
Verified on the exact list from the failure: 3 present and 12 missing,
option 1 issuing a command with only the 3 and leaving the 12 counted as
installed, option 2 sending all 15, option 3 running nothing.
Also swapped entries 4 and 5 of the Deploy menu — QEMU/KVM now sits at [4],
NTFY at [5].
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-08-01 07:16:26 -04:00
|
|
|
|
# Sort out what the active code still holds BEFORE calling the script:
|
|
|
|
|
|
# it aborts on the first missing name and takes the rest down with it.
|
|
|
|
|
|
lst_present, lst_missing = self.split_present_missing(
|
|
|
|
|
|
lst_module_to_uninstall
|
|
|
|
|
|
)
|
|
|
|
|
|
if lst_missing:
|
|
|
|
|
|
self.add_comment_progression(
|
|
|
|
|
|
"uninstall - no code for: " + ", ".join(lst_missing)
|
|
|
|
|
|
)
|
|
|
|
|
|
lst_module_to_uninstall = self.prompt_uninstall_missing(
|
|
|
|
|
|
lst_present, lst_missing
|
|
|
|
|
|
)
|
|
|
|
|
|
if not lst_module_to_uninstall:
|
|
|
|
|
|
print(f"⏭ {t('Nothing uninstalled.')}")
|
|
|
|
|
|
return
|
2025-10-31 01:10:54 -04:00
|
|
|
|
uninstall_module = ",".join(lst_module_to_uninstall)
|
|
|
|
|
|
self.todo_upgrade_execute(
|
|
|
|
|
|
f"./script/addons/uninstall_addons.sh {database_name} {uninstall_module}",
|
|
|
|
|
|
single_source_odoo=True,
|
|
|
|
|
|
)
|
|
|
|
|
|
|
[FIX] migration: tell modules with no code from those to uninstall
Uninstalling before the 13->14 bump asked for 15 modules; 12 of them no longer
exist in the 13.0 addons path. check_addons_exist.py rejects the command on
the first missing name, so the uninstall aborted whole — and the 3 modules
that WERE there stayed installed, blocking the bump for a reason that had
nothing to do with them.
The list is now split before the script is called, using check_addons_exist
which was already there and simply never consulted at this point. « Missing »
means the addons path has no code for it, not that the database lacks it —
the distinction is the whole point, since Odoo cannot uninstall a module whose
code is gone.
When some are missing, a choice is offered rather than a failure:
[1] uninstall the present ones, skip the missing (default)
[2] try the whole list anyway (it will fail)
[3] uninstall nothing, continue
Only what is really uninstalled is subtracted from the per-version module
list, so a module left in place stays counted as installed — which it is. The
missing names are written to the progression comments, so what was skipped is
still on record afterwards.
Verified on the exact list from the failure: 3 present and 12 missing,
option 1 issuing a command with only the 3 and leaving the 12 counted as
installed, option 2 sending all 15, option 3 running nothing.
Also swapped entries 4 and 5 of the Deploy menu — QEMU/KVM now sits at [4],
NTFY at [5].
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-08-01 07:16:26 -04:00
|
|
|
|
# Update list installed module — only what was REALLY uninstalled, so
|
|
|
|
|
|
# a module left in place stays counted as installed.
|
2025-10-31 01:10:54 -04:00
|
|
|
|
self.dct_module_per_version[actual_version] = sorted(
|
|
|
|
|
|
list(
|
|
|
|
|
|
set(self.dct_module_per_version[actual_version])
|
|
|
|
|
|
- set(lst_module_to_uninstall)
|
|
|
|
|
|
)
|
|
|
|
|
|
)
|
|
|
|
|
|
self.dct_progression["dct_module_per_version"] = (
|
|
|
|
|
|
self.dct_module_per_version
|
|
|
|
|
|
)
|
|
|
|
|
|
self.write_config()
|
|
|
|
|
|
|
|
|
|
|
|
def install_from_database(
|
|
|
|
|
|
self, lst_module_to_install, database_name, actual_version
|
|
|
|
|
|
):
|
|
|
|
|
|
if not lst_module_to_install:
|
|
|
|
|
|
return
|
|
|
|
|
|
install_module = ",".join(lst_module_to_install)
|
|
|
|
|
|
self.todo_upgrade_execute(
|
|
|
|
|
|
f"./script/addons/install_addons.sh {database_name} {install_module}",
|
|
|
|
|
|
single_source_odoo=True,
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
# Update list installed module
|
|
|
|
|
|
self.dct_module_per_version[actual_version] = sorted(
|
|
|
|
|
|
list(
|
|
|
|
|
|
set(
|
|
|
|
|
|
self.dct_module_per_version[actual_version]
|
|
|
|
|
|
+ lst_module_to_install
|
|
|
|
|
|
)
|
|
|
|
|
|
)
|
|
|
|
|
|
)
|
|
|
|
|
|
self.dct_progression["dct_module_per_version"] = (
|
|
|
|
|
|
self.dct_module_per_version
|
|
|
|
|
|
)
|
|
|
|
|
|
self.write_config()
|
|
|
|
|
|
|
|
|
|
|
|
def check_addons_exist(
|
|
|
|
|
|
self, lst_module_to_check, ignore_error=True, get_all_info=False
|
|
|
|
|
|
):
|
|
|
|
|
|
str_module_to_check = ",".join(sorted(lst_module_to_check))
|
|
|
|
|
|
status, cmd_executed, dct_output = self.todo_upgrade_execute(
|
|
|
|
|
|
f"{PYTHON_BIN} ./script/addons/check_addons_exist.py --format_json --output_json -m {str_module_to_check}",
|
|
|
|
|
|
get_output=True,
|
|
|
|
|
|
output_is_json=True,
|
|
|
|
|
|
wait_at_error=not ignore_error,
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
lst_module_missing = dct_output.get("missing")
|
|
|
|
|
|
lst_module_duplicate = dct_output.get("duplicate")
|
|
|
|
|
|
if get_all_info:
|
|
|
|
|
|
lst_module_error = dct_output.get("error")
|
|
|
|
|
|
lst_module_exist = dct_output.get("exist")
|
|
|
|
|
|
return (
|
|
|
|
|
|
lst_module_missing,
|
|
|
|
|
|
lst_module_duplicate,
|
|
|
|
|
|
lst_module_exist,
|
|
|
|
|
|
lst_module_error,
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
return lst_module_missing, lst_module_duplicate
|
|
|
|
|
|
|
|
|
|
|
|
def switch_odoo(self, odoo_version):
|
|
|
|
|
|
int_odoo_version = int(float(odoo_version))
|
|
|
|
|
|
|
|
|
|
|
|
# Expect odoo_version like 12.0
|
|
|
|
|
|
lst_version, lst_version_installed, odoo_installed_version = (
|
2026-03-26 15:45:44 -04:00
|
|
|
|
get_odoo_version()
|
2025-10-31 01:10:54 -04:00
|
|
|
|
)
|
|
|
|
|
|
if odoo_installed_version != f"odoo{int_odoo_version}.0":
|
|
|
|
|
|
print(
|
|
|
|
|
|
f"⧖ -> Was '{odoo_installed_version}', Switch to odoo{int_odoo_version}.0"
|
|
|
|
|
|
)
|
|
|
|
|
|
self.todo_upgrade_execute(f"make switch_odoo_{int_odoo_version}")
|
|
|
|
|
|
self.todo_upgrade_execute("make config_gen_all")
|
|
|
|
|
|
|
|
|
|
|
|
def install_OCA_odoo_module_migrator(self):
|
|
|
|
|
|
if not os.path.exists(PATH_VENV_MODULE_MIGRATOR):
|
|
|
|
|
|
self.todo_upgrade_execute(
|
|
|
|
|
|
f"cd {PATH_OCA_ODOO_MODULE_MIGRATOR} && python -m venv {VENV_NAME_MODULE_MIGRATOR} && source {VENV_NAME_MODULE_MIGRATOR}/bin/activate && pip3 install -r requirements.txt"
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
def install_OCA_openupgrade(self, next_version):
|
|
|
|
|
|
# TODO install odoorpc==0.7.0
|
|
|
|
|
|
# openupgradelib
|
|
|
|
|
|
# openupgrade_path = f"odoo{next_version}.0/OCA_OpenUpgrade"
|
|
|
|
|
|
# venv_oca_path = f"{openupgrade_path}/.venv"
|
|
|
|
|
|
# if os.path.exists(venv_oca_path):
|
|
|
|
|
|
# return
|
|
|
|
|
|
lst_version, lst_version_installed, odoo_installed_version = (
|
2026-03-26 15:45:44 -04:00
|
|
|
|
get_odoo_version()
|
2025-10-31 01:10:54 -04:00
|
|
|
|
)
|
|
|
|
|
|
extract_version = f"{next_version}.0"
|
|
|
|
|
|
dct_erplibre_info = [
|
|
|
|
|
|
a for a in lst_version if a.get("odoo_version") == extract_version
|
|
|
|
|
|
]
|
|
|
|
|
|
if not dct_erplibre_info:
|
|
|
|
|
|
raise Exception(f"Cannot extract {extract_version}")
|
|
|
|
|
|
dct_erplibre_info = dct_erplibre_info[0]
|
|
|
|
|
|
erplibre_version = dct_erplibre_info.get("erplibre_version")
|
|
|
|
|
|
# self.todo_upgrade_execute(
|
|
|
|
|
|
# f".venv.{erplibre_version}/bin/python -m venv {venv_oca_path} && {venv_oca_path}/bin/pip3 install -r {openupgrade_path}/requirements.txt"
|
|
|
|
|
|
# )
|
|
|
|
|
|
self.todo_upgrade_execute(
|
|
|
|
|
|
f".venv.{erplibre_version}/bin/pip install odoorpc==0.7.0"
|
|
|
|
|
|
)
|
|
|
|
|
|
self.todo_upgrade_execute(
|
|
|
|
|
|
f".venv.{erplibre_version}/bin/pip install openupgradelib"
|
|
|
|
|
|
)
|
|
|
|
|
|
return erplibre_version
|
|
|
|
|
|
|
|
|
|
|
|
def todo_upgrade_execute(
|
|
|
|
|
|
self,
|
|
|
|
|
|
cmd,
|
|
|
|
|
|
single_source_odoo=False,
|
|
|
|
|
|
new_env=None,
|
|
|
|
|
|
quiet=False,
|
|
|
|
|
|
get_output=False,
|
|
|
|
|
|
output_is_json=False,
|
|
|
|
|
|
wait_at_error=True,
|
|
|
|
|
|
):
|
|
|
|
|
|
if output_is_json and not get_output:
|
|
|
|
|
|
get_output = True
|
|
|
|
|
|
output = None
|
|
|
|
|
|
if get_output:
|
2026-02-13 01:27:38 -05:00
|
|
|
|
status, cmd_executed, output = self.execute.exec_command_live(
|
2025-10-31 01:10:54 -04:00
|
|
|
|
cmd,
|
|
|
|
|
|
source_erplibre=False,
|
|
|
|
|
|
single_source_odoo=single_source_odoo,
|
|
|
|
|
|
new_env=new_env,
|
|
|
|
|
|
return_status_and_output_and_command=True,
|
|
|
|
|
|
quiet=quiet,
|
|
|
|
|
|
)
|
|
|
|
|
|
else:
|
2026-02-13 01:27:38 -05:00
|
|
|
|
status, cmd_executed = self.execute.exec_command_live(
|
2025-10-31 01:10:54 -04:00
|
|
|
|
cmd,
|
|
|
|
|
|
source_erplibre=False,
|
|
|
|
|
|
single_source_odoo=single_source_odoo,
|
|
|
|
|
|
new_env=new_env,
|
|
|
|
|
|
return_status_and_command=True,
|
|
|
|
|
|
quiet=quiet,
|
|
|
|
|
|
)
|
|
|
|
|
|
self.lst_command_executed.append(cmd_executed)
|
|
|
|
|
|
self.dct_progression["command_executed"] = self.lst_command_executed
|
|
|
|
|
|
self.write_config()
|
2026-07-31 23:22:02 -04:00
|
|
|
|
# None means « the command never reported a status » -> treat it as a
|
|
|
|
|
|
# failure, never as a success (defence in depth: exec_command_live now
|
|
|
|
|
|
# always sets one, but a silent None must not skip this prompt).
|
|
|
|
|
|
if (status is None or status) and wait_at_error:
|
2025-10-31 01:10:54 -04:00
|
|
|
|
print("[1] to redo the command")
|
|
|
|
|
|
wait_status = (
|
|
|
|
|
|
input(
|
|
|
|
|
|
"💬 Error detected, press to continue or ctrl+c to stop : "
|
|
|
|
|
|
)
|
|
|
|
|
|
.strip()
|
|
|
|
|
|
.lower()
|
|
|
|
|
|
)
|
2026-02-22 12:14:07 -05:00
|
|
|
|
|
|
|
|
|
|
# psycopg2.errors.UndefinedTable: relation "discuss_channel" does not exist
|
|
|
|
|
|
# LIGNE 1 : SELECT "discuss_channel"."id" FROM "discuss_channel" WHERE (...
|
|
|
|
|
|
|
2025-10-31 01:10:54 -04:00
|
|
|
|
if wait_status == "1":
|
|
|
|
|
|
return self.todo_upgrade_execute(
|
|
|
|
|
|
cmd,
|
|
|
|
|
|
single_source_odoo=single_source_odoo,
|
|
|
|
|
|
new_env=new_env,
|
|
|
|
|
|
quiet=quiet,
|
|
|
|
|
|
get_output=get_output,
|
|
|
|
|
|
output_is_json=output_is_json,
|
|
|
|
|
|
wait_at_error=wait_at_error,
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
if get_output:
|
|
|
|
|
|
if output_is_json:
|
|
|
|
|
|
str_output = json.loads("".join(output))
|
|
|
|
|
|
return status, cmd_executed, str_output
|
|
|
|
|
|
return status, cmd_executed, output
|
|
|
|
|
|
return status, cmd_executed
|
|
|
|
|
|
|
|
|
|
|
|
def check_and_clone_source_to_target_migration_code(
|
|
|
|
|
|
self, next_version, source_addons_path, target_addons_path
|
|
|
|
|
|
):
|
|
|
|
|
|
if not os.path.exists(os.path.join(source_addons_path, ".git")):
|
|
|
|
|
|
return
|
|
|
|
|
|
if os.path.exists(os.path.join(target_addons_path, ".git")):
|
|
|
|
|
|
return
|
|
|
|
|
|
# if not os.path.exists(target_addons_path):
|
|
|
|
|
|
# cmd_mkdir = f"mkdir -p {target_addons_path}"
|
|
|
|
|
|
# status, cmd_executed, lst_output = self.todo_upgrade_execute(
|
|
|
|
|
|
# cmd_mkdir,
|
|
|
|
|
|
# get_output=True,
|
|
|
|
|
|
# )
|
|
|
|
|
|
# return
|
|
|
|
|
|
source_dir_name = os.path.basename(source_addons_path)
|
|
|
|
|
|
# Clone a project for next version
|
|
|
|
|
|
# Get actual branch
|
|
|
|
|
|
cmd_git_clone_migrate_source = (
|
|
|
|
|
|
f"cd {source_addons_path} && "
|
|
|
|
|
|
f"git branch --show-current && "
|
|
|
|
|
|
f"cd ~-"
|
|
|
|
|
|
)
|
|
|
|
|
|
status, cmd_executed, lst_output = self.todo_upgrade_execute(
|
|
|
|
|
|
cmd_git_clone_migrate_source,
|
|
|
|
|
|
get_output=True,
|
|
|
|
|
|
)
|
|
|
|
|
|
branch_source = lst_output[0].strip() if len(lst_output) else ""
|
|
|
|
|
|
if not branch_source:
|
|
|
|
|
|
# Get branch from repo
|
|
|
|
|
|
branch_source = self.get_branch_name_from_local_manifest(
|
|
|
|
|
|
source_addons_path, f"{next_version}.0"
|
|
|
|
|
|
)
|
|
|
|
|
|
branch_target = branch_source.replace(
|
|
|
|
|
|
str(next_version - 1), str(next_version)
|
|
|
|
|
|
)
|
|
|
|
|
|
# Get remote branch for actual version
|
|
|
|
|
|
cmd_git_clone_migrate_source_same_target = (
|
|
|
|
|
|
f"cd {source_addons_path} "
|
|
|
|
|
|
f"&& git fetch --all "
|
|
|
|
|
|
f'&& git branch -vv | grep "{branch_source}" '
|
|
|
|
|
|
f"&& cd ~-"
|
|
|
|
|
|
)
|
|
|
|
|
|
cmd_git_clone_migrate_source_same_target_remote_only = (
|
|
|
|
|
|
f"cd {source_addons_path} && git remote && cd ~-"
|
|
|
|
|
|
)
|
|
|
|
|
|
status, cmd_executed, lst_output = self.todo_upgrade_execute(
|
|
|
|
|
|
cmd_git_clone_migrate_source_same_target,
|
|
|
|
|
|
get_output=True,
|
|
|
|
|
|
wait_at_error=False,
|
|
|
|
|
|
)
|
|
|
|
|
|
if status == 1:
|
|
|
|
|
|
status, cmd_executed, lst_output = self.todo_upgrade_execute(
|
|
|
|
|
|
cmd_git_clone_migrate_source_same_target_remote_only,
|
|
|
|
|
|
get_output=True,
|
|
|
|
|
|
)
|
|
|
|
|
|
if lst_output:
|
|
|
|
|
|
remote = lst_output[0].strip()
|
|
|
|
|
|
# TODO write this modification into repo manifest
|
|
|
|
|
|
else:
|
|
|
|
|
|
remote = input(
|
|
|
|
|
|
"👹 BUG, you need to push last branch. Please write the remote/ :"
|
|
|
|
|
|
)
|
|
|
|
|
|
else:
|
|
|
|
|
|
local_branch, remote, remote_branch = (
|
|
|
|
|
|
self.get_local_branch_remote_actual_branch_git(
|
|
|
|
|
|
lst_output,
|
|
|
|
|
|
)
|
|
|
|
|
|
)
|
|
|
|
|
|
# Get remote branch for next version
|
|
|
|
|
|
remote_branch_target = f"{remote}/{branch_target}"
|
|
|
|
|
|
cmd_git_clone_migrate_source_same_target = (
|
|
|
|
|
|
f"cd {source_addons_path} "
|
|
|
|
|
|
f"&& git fetch --all "
|
|
|
|
|
|
f'&& git branch --remotes -vv | grep "{remote_branch_target} " '
|
|
|
|
|
|
f"&& cd ~-"
|
|
|
|
|
|
)
|
|
|
|
|
|
status, cmd_executed, lst_output = self.todo_upgrade_execute(
|
|
|
|
|
|
cmd_git_clone_migrate_source_same_target,
|
|
|
|
|
|
get_output=True,
|
|
|
|
|
|
wait_at_error=False,
|
|
|
|
|
|
)
|
|
|
|
|
|
has_existing_target_branch = any([a.strip() for a in lst_output])
|
|
|
|
|
|
|
|
|
|
|
|
# TODO check config if path is added
|
|
|
|
|
|
# Get remote branch address
|
|
|
|
|
|
cmd_remote_address = (
|
|
|
|
|
|
f"cd {source_addons_path} "
|
|
|
|
|
|
f"&& git remote get-url {remote} "
|
|
|
|
|
|
f"&& cd ~-"
|
|
|
|
|
|
)
|
|
|
|
|
|
status, cmd_executed, lst_output = self.todo_upgrade_execute(
|
|
|
|
|
|
cmd_remote_address,
|
|
|
|
|
|
get_output=True,
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
remote_address = lst_output[0].strip()
|
|
|
|
|
|
|
|
|
|
|
|
# TODO some time, the clone has error, need to repeat
|
|
|
|
|
|
branch_to_clone = (
|
|
|
|
|
|
branch_target if has_existing_target_branch else branch_source
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
cmd_git_clone = (
|
|
|
|
|
|
f"cd {os.path.dirname(target_addons_path)} "
|
|
|
|
|
|
f"&& git clone {remote_address} {source_dir_name} -b {branch_to_clone} && cd ~-"
|
|
|
|
|
|
)
|
|
|
|
|
|
status, cmd_executed, lst_output = self.todo_upgrade_execute(
|
|
|
|
|
|
cmd_git_clone,
|
|
|
|
|
|
get_output=True,
|
|
|
|
|
|
)
|
|
|
|
|
|
if not has_existing_target_branch:
|
|
|
|
|
|
cmd_git_clone = (
|
|
|
|
|
|
f"cd {target_addons_path} "
|
2026-06-29 03:22:58 -04:00
|
|
|
|
f"&& if git rev-parse --verify --quiet refs/heads/{branch_target}; then "
|
|
|
|
|
|
f"git checkout {branch_target}; else git checkout -b {branch_target}; fi "
|
|
|
|
|
|
f"&& cd ~-"
|
2025-10-31 01:10:54 -04:00
|
|
|
|
)
|
|
|
|
|
|
status, cmd_executed, lst_output = self.todo_upgrade_execute(
|
|
|
|
|
|
cmd_git_clone,
|
|
|
|
|
|
get_output=True,
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
def get_branch_name_from_local_manifest(self, addons_path, default_branch):
|
|
|
|
|
|
relative_addons_path = addons_path.replace(os.getcwd() + "/", "")
|
|
|
|
|
|
git_tool = GitTool()
|
|
|
|
|
|
dct_remote, dct_project, default_remote = (
|
|
|
|
|
|
git_tool.get_manifest_xml_info(filename=LOCAL_MANIFEST)
|
|
|
|
|
|
)
|
|
|
|
|
|
for dct_repo in dct_project.values():
|
|
|
|
|
|
path = dct_repo.get("@path")
|
|
|
|
|
|
if path == relative_addons_path:
|
|
|
|
|
|
revision = dct_repo.get("@revision")
|
|
|
|
|
|
return revision
|
|
|
|
|
|
return default_branch
|
|
|
|
|
|
|
|
|
|
|
|
def get_local_branch_remote_actual_branch_git(self, lst_output):
|
|
|
|
|
|
for line in lst_output:
|
|
|
|
|
|
# The current branch is marked with an asterisk (*) at the start of the line
|
|
|
|
|
|
if not line.strip().startswith("*"):
|
|
|
|
|
|
continue
|
|
|
|
|
|
# Split the line to isolate the remote and remote branch name
|
|
|
|
|
|
parts = line.split()
|
|
|
|
|
|
if len(parts) >= 4:
|
|
|
|
|
|
# The remote and remote branch are in the 4th part, e.g., '[origin/main]'
|
|
|
|
|
|
remote_info = parts[3].strip("[]")
|
|
|
|
|
|
# Split 'origin/main' into 'origin' and 'main'
|
|
|
|
|
|
# remote, remote_branch = remote_info.split("/", 1)
|
|
|
|
|
|
try:
|
|
|
|
|
|
remote, remote_branch = remote_info.split("/", 1)
|
|
|
|
|
|
except ValueError as e:
|
|
|
|
|
|
# # TODO this means no remote, take default one? or last one?
|
|
|
|
|
|
# # TODO supporter la migration 17 vers 18
|
|
|
|
|
|
# print("👹 BUG, you need to push ")
|
|
|
|
|
|
# TODO search remote and associate branch
|
|
|
|
|
|
value = input(
|
|
|
|
|
|
"👹 BUG, you need to push last branch. Please write the remote/branch_name :"
|
|
|
|
|
|
)
|
|
|
|
|
|
remote, remote_branch = value.split("/", 1)
|
|
|
|
|
|
return parts[1], remote, remote_branch
|
|
|
|
|
|
return None, None, None
|
|
|
|
|
|
|
|
|
|
|
|
def add_comment_progression(self, comment):
|
|
|
|
|
|
comment_to_add = f"# {comment}"
|
|
|
|
|
|
self.lst_command_executed.append(comment_to_add)
|
|
|
|
|
|
self.dct_progression["command_executed"] = self.lst_command_executed
|
|
|
|
|
|
self.write_config()
|