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("."):