From 32315a2dbad89e9cd8335cca4145dc012761750e Mon Sep 17 00:00:00 2001 From: Mathieu Benoit Date: Mon, 30 Mar 2026 14:46:56 -0400 Subject: [PATCH] [ADD] installation: add optional extra modules (CybroOdoo) CybroOdoo repos are large and slow to clone, making them unsuitable for default installation. Moves them to opt-in per-version extra manifests, introduces .erplibre-state.json to track installation options per Odoo version, and surfaces the choice in the TODO CLI sub-menu. Switch auto-detects extra from state and warns when no state is recorded. Generated by Claude Code 2.1.87 model claude-sonnet-4-6 Co-Authored-By: Mathieu Benoit --- conf/make.installation.Makefile | 32 +++++ manifest/git_manifest_extra_odoo12.0.xml | 12 ++ manifest/git_manifest_extra_odoo13.0.xml | 12 ++ manifest/git_manifest_extra_odoo14.0.xml | 12 ++ manifest/git_manifest_extra_odoo15.0.xml | 12 ++ manifest/git_manifest_extra_odoo16.0.xml | 21 +++ manifest/git_manifest_extra_odoo17.0.xml | 20 +++ manifest/git_manifest_extra_odoo18.0.xml | 21 +++ manifest/git_manifest_odoo12.0.xml | 7 - manifest/git_manifest_odoo13.0.xml | 7 - manifest/git_manifest_odoo14.0.xml | 7 - manifest/git_manifest_odoo15.0.xml | 7 - manifest/git_manifest_odoo16.0.xml | 15 --- manifest/git_manifest_odoo17.0.xml | 15 --- manifest/git_manifest_odoo18.0.xml | 15 --- script/git/git_merge_repo_manifest.py | 20 +++ script/todo/todo.py | 31 +++++ script/todo/todo_i18n.py | 12 ++ script/version/erplibre_state.py | 161 +++++++++++++++++++++++ script/version/update_env_version.py | 57 +++++++- 20 files changed, 420 insertions(+), 76 deletions(-) create mode 100644 manifest/git_manifest_extra_odoo12.0.xml create mode 100644 manifest/git_manifest_extra_odoo13.0.xml create mode 100644 manifest/git_manifest_extra_odoo14.0.xml create mode 100644 manifest/git_manifest_extra_odoo15.0.xml create mode 100644 manifest/git_manifest_extra_odoo16.0.xml create mode 100644 manifest/git_manifest_extra_odoo17.0.xml create mode 100644 manifest/git_manifest_extra_odoo18.0.xml create mode 100644 script/version/erplibre_state.py diff --git a/conf/make.installation.Makefile b/conf/make.installation.Makefile index ca3e4a4..7365faf 100644 --- a/conf/make.installation.Makefile +++ b/conf/make.installation.Makefile @@ -113,6 +113,38 @@ switch_odoo_12_update: ./script/version/update_env_version.py --erplibre_version odoo12.0_python3.7.17 --switch --switch_update ./script/make.sh config_gen_all +#################### +# INSTALL WITH EXTRA (CybroOdoo) +#################### + +.PHONY: install_odoo_18_with_extra +install_odoo_18_with_extra: + ./script/version/update_env_version.py --erplibre_version odoo18.0_python3.12.10 --install_dev --with_extra + +.PHONY: install_odoo_17_with_extra +install_odoo_17_with_extra: + ./script/version/update_env_version.py --erplibre_version odoo17.0_python3.10.18 --install_dev --with_extra + +.PHONY: install_odoo_16_with_extra +install_odoo_16_with_extra: + ./script/version/update_env_version.py --erplibre_version odoo16.0_python3.10.18 --install_dev --with_extra + +.PHONY: install_odoo_15_with_extra +install_odoo_15_with_extra: + ./script/version/update_env_version.py --erplibre_version odoo15.0_python3.8.20 --install_dev --with_extra + +.PHONY: install_odoo_14_with_extra +install_odoo_14_with_extra: + ./script/version/update_env_version.py --erplibre_version odoo14.0_python3.8.20 --install_dev --with_extra + +.PHONY: install_odoo_13_with_extra +install_odoo_13_with_extra: + ./script/version/update_env_version.py --erplibre_version odoo13.0_python3.7.17 --install_dev --with_extra + +.PHONY: install_odoo_12_with_extra +install_odoo_12_with_extra: + ./script/version/update_env_version.py --erplibre_version odoo12.0_python3.7.17 --install_dev --with_extra + .PHONY: install_odoo_all_version install_odoo_all_version: ./script/make.sh install_odoo_18 diff --git a/manifest/git_manifest_extra_odoo12.0.xml b/manifest/git_manifest_extra_odoo12.0.xml new file mode 100644 index 0000000..9e51d4f --- /dev/null +++ b/manifest/git_manifest_extra_odoo12.0.xml @@ -0,0 +1,12 @@ + + + + + + diff --git a/manifest/git_manifest_extra_odoo13.0.xml b/manifest/git_manifest_extra_odoo13.0.xml new file mode 100644 index 0000000..c7a74a4 --- /dev/null +++ b/manifest/git_manifest_extra_odoo13.0.xml @@ -0,0 +1,12 @@ + + + + + + diff --git a/manifest/git_manifest_extra_odoo14.0.xml b/manifest/git_manifest_extra_odoo14.0.xml new file mode 100644 index 0000000..94cd0fb --- /dev/null +++ b/manifest/git_manifest_extra_odoo14.0.xml @@ -0,0 +1,12 @@ + + + + + + diff --git a/manifest/git_manifest_extra_odoo15.0.xml b/manifest/git_manifest_extra_odoo15.0.xml new file mode 100644 index 0000000..c096a17 --- /dev/null +++ b/manifest/git_manifest_extra_odoo15.0.xml @@ -0,0 +1,12 @@ + + + + + + diff --git a/manifest/git_manifest_extra_odoo16.0.xml b/manifest/git_manifest_extra_odoo16.0.xml new file mode 100644 index 0000000..6be7a49 --- /dev/null +++ b/manifest/git_manifest_extra_odoo16.0.xml @@ -0,0 +1,21 @@ + + + + + + + + diff --git a/manifest/git_manifest_extra_odoo17.0.xml b/manifest/git_manifest_extra_odoo17.0.xml new file mode 100644 index 0000000..d18e6fb --- /dev/null +++ b/manifest/git_manifest_extra_odoo17.0.xml @@ -0,0 +1,20 @@ + + + + + + + diff --git a/manifest/git_manifest_extra_odoo18.0.xml b/manifest/git_manifest_extra_odoo18.0.xml new file mode 100644 index 0000000..c41344b --- /dev/null +++ b/manifest/git_manifest_extra_odoo18.0.xml @@ -0,0 +1,21 @@ + + + + + + + + diff --git a/manifest/git_manifest_odoo12.0.xml b/manifest/git_manifest_odoo12.0.xml index 4af28fa..08c03df 100644 --- a/manifest/git_manifest_odoo12.0.xml +++ b/manifest/git_manifest_odoo12.0.xml @@ -26,13 +26,6 @@ - - - - - - - - - - dict: + """Return the current state, or an empty state if the file does not exist.""" + if not os.path.isfile(STATE_FILE): + return _deep_copy(_EMPTY_STATE) + try: + with open(STATE_FILE, "r", encoding="utf-8") as f: + return json.load(f) + except (json.JSONDecodeError, OSError) as e: + _logger.warning(f"Cannot read {STATE_FILE}: {e}. Using empty state.") + return _deep_copy(_EMPTY_STATE) + + +def write_state(state: dict) -> None: + """Persist state to .erplibre-state.json.""" + with open(STATE_FILE, "w", encoding="utf-8") as f: + json.dump(state, f, indent=4, ensure_ascii=False) + f.write("\n") + + +def set_version_installed( + odoo_version: str, + extra: bool = False, + python: str = None, + poetry: str = None, +) -> None: + """Record that an Odoo version has been installed (or reinstalled).""" + state = read_state() + entry = state["odoo_versions"].get(odoo_version, _deep_copy(_EMPTY_VERSION_ENTRY)) + entry["installed"] = True + entry["extra"] = extra + if python: + entry["python"] = python + if poetry: + entry["poetry"] = poetry + entry["installed_at"] = str(date.today()) + state["odoo_versions"][odoo_version] = entry + state["current_odoo_version"] = odoo_version + write_state(state) + _logger.info( + f"State updated: odoo {odoo_version} installed" + f" (extra={extra}, python={python}, poetry={poetry})" + ) + + +def set_version_switched(odoo_version: str) -> None: + """Record that the workspace was switched to an Odoo version.""" + state = read_state() + entry = state["odoo_versions"].get(odoo_version, _deep_copy(_EMPTY_VERSION_ENTRY)) + entry["switched_at"] = str(date.today()) + state["odoo_versions"][odoo_version] = entry + state["current_odoo_version"] = odoo_version + write_state(state) + + +def set_mobile_active(active: bool) -> None: + """Record whether the mobile project is currently synced.""" + state = read_state() + state["mobile"]["active"] = active + if active: + state["mobile"]["installed_at"] = str(date.today()) + write_state(state) + _logger.info(f"State updated: mobile active={active}") + + +def get_version_extra(odoo_version: str) -> bool: + """Return True if the given Odoo version was installed with extra modules.""" + state = read_state() + entry = state["odoo_versions"].get(odoo_version) + if entry is None: + return False + return bool(entry.get("extra", False)) + + +def get_version_installed(odoo_version: str) -> bool: + """Return True if the given Odoo version has an installation record.""" + state = read_state() + entry = state["odoo_versions"].get(odoo_version) + if entry is None: + return False + return bool(entry.get("installed", False)) + + +def get_current_version() -> str | None: + """Return the currently active Odoo version, or None.""" + return read_state().get("current_odoo_version") + + +def get_mobile_active() -> bool: + """Return True if mobile is recorded as active.""" + return bool(read_state().get("mobile", {}).get("active", False)) + + +def print_state() -> None: + """Log a human-readable summary of the current state.""" + state = read_state() + current = state.get("current_odoo_version") or "unknown" + mobile = state.get("mobile", {}) + mobile_status = "active" if mobile.get("active") else "inactive" + + _logger.info(f"Current Odoo version : {current}") + _logger.info(f"Mobile context : {mobile_status}") + + versions = state.get("odoo_versions", {}) + if versions: + installed = [ + f"{v}{' +extra' if d.get('extra') else ''}" + for v, d in sorted(versions.items()) + if d.get("installed") + ] + if installed: + _logger.info(f"Installed versions : {', '.join(installed)}") + else: + _logger.info("Installed versions : none recorded") + else: + _logger.info("Installed versions : none recorded") + + +def _deep_copy(d: dict) -> dict: + """Simple deep copy for plain dicts/lists (no external deps).""" + return json.loads(json.dumps(d)) diff --git a/script/version/update_env_version.py b/script/version/update_env_version.py index ece8d3f..154913b 100755 --- a/script/version/update_env_version.py +++ b/script/version/update_env_version.py @@ -12,6 +12,21 @@ import subprocess import sys import time +# erplibre_state is in the same directory; import lazily to avoid hard failure +# when the script runs outside the project root (e.g. during bare install). +try: + from script.version.erplibre_state import ( + get_version_extra, + get_version_installed, + print_state, + set_version_installed, + set_version_switched, + ) + + _STATE_AVAILABLE = True +except ImportError: + _STATE_AVAILABLE = False + logging.basicConfig(level=os.environ.get("LOGLEVEL", "INFO")) _logger = logging.getLogger(__name__) @@ -124,6 +139,14 @@ def get_config(): action="store_true", help="Force all, include force_install, force_repo.", ) + parser.add_argument( + "--with_extra", + action="store_true", + help=( + "Install or switch with extra modules (e.g. CybroOdoo)." + " State is saved in .erplibre-state.json." + ), + ) args = parser.parse_args() if args.force: @@ -200,6 +223,11 @@ class Update: _logger.info( f"Mobile context: {'active (' + MOBILE_PATH + ')' if self.mobile_active else 'inactive'}" ) + if _STATE_AVAILABLE: + extra = get_version_extra(self.odoo_version) + _logger.info( + f"Extra modules (CybroOdoo): {'active' if extra else 'inactive'}" + ) erplibre_version_to_search = ERPLIBRE_TEMPLATE_VERSION % ( self.odoo_version, self.python_version, @@ -413,15 +441,38 @@ class Update: if self.config.install_dev: _logger.info("Installation.") status = self.install_erplibre() + if _STATE_AVAILABLE and not status: + set_version_installed( + self.new_version_odoo, + extra=bool(self.config.with_extra), + python=self.new_version_python, + poetry=self.new_version_poetry, + ) elif ( self.config.is_in_switch and self.config.is_in_switch_force_update ): _logger.info("Switch") self.execute_log.append(f"System update") - status = os.system( - "./script/manifest/update_manifest_local_dev.sh" - ) + # Auto-detect extra from state, warn if no state recorded + with_extra = bool(self.config.with_extra) + if _STATE_AVAILABLE and not with_extra: + if not get_version_installed(self.new_version_odoo): + _logger.warning( + f"No installation state found for odoo" + f" {self.new_version_odoo}. Proceeding without" + f" extra modules (CybroOdoo). To install with" + f" extra: make" + f" install_odoo_{self.new_version_odoo.replace('.', '')}_with_extra" + ) + else: + with_extra = get_version_extra(self.new_version_odoo) + manifest_script = "./script/manifest/update_manifest_local_dev.sh" + if with_extra: + manifest_script += " --with_extra" + status = os.system(manifest_script) + if _STATE_AVAILABLE and not status: + set_version_switched(self.new_version_odoo) # To support multiple addons directory, remove TEMP # for addons_path in os.listdir("."):