#!/usr/bin/env python3 """Classify every OEEL-original module by 'community-safety'. For each /__manifest__.py in enterprise-addons/, determine: class: - safe : depends only on OCB modules, AND its Python/XML doesn't reference other OEEL-original modules - keep : depends on OCB + some OEEL modules but those OEEL deps also depend on OCB (chain) — i.e. we can ship as a bundle - doomed : depends on an Enterprise-only feature that OCB lacks (studio, sign, planning, documents, spreadsheet, voip, ...) """ from __future__ import annotations import ast import os import re import json from pathlib import Path OEEL_ROOT = Path("/root/Documents/trae_projects/one_b_os/enterprise-addons") OCB_PATH = Path("/tmp/odoo_ocb/addons") ENTERPRISE_REPO_URL = ( "https://raw.githubusercontent.com/odoo/odoo/19.0/addons" ) OCB_MODULES: set[str] = set() for p in OCB_PATH.iterdir(): if p.is_dir() and (p / "__manifest__.py").is_file(): OCB_MODULES.add(p.name) OEEL_MODULES: set[str] = set() for p in OEEL_ROOT.iterdir(): if p.is_dir() and (p / "__manifest__.py").is_file(): OEEL_MODULES.add(p.name) # Truly Enterprise-locked feature modules (no OCB equivalent). # Decided heuristically: modules whose filename or purpose has no Community # counterpart, AND which are referenced by many other modules. TRULY_ENTERPRISE = { # Application-level whole apps with no OCB equivalent "web_studio", "website_studio", "documents", "documents_spreadsheet", "spreadsheet", "spreadsheet_edition", "spreadsheet_dashboard", "spreadsheet_enterprise", "sign", "sign_itsme", "planning", "appointment", "voip", "voip_onsip", "knowledge", "website_knowledge", "ai", "whatsapp", "marketing_automation", "approvals", "approvals_purchase", "frontdesk", "helpdesk", "helpdesk_*", "hr_appraisal", "hr_payroll", "timesheet_grid", "project_enterprise", "website_helpdesk", "industry_fsm", "worksheet", "event_social", "event_enterprise", "delivery_fedex", "delivery_dhl", "delivery_bpost", "delivery_easypost", "delivery_sendcloud", "delivery_usps", "delivery_ups", "delivery_starshipit", "delivery_envia", "delivery_shiprocket", "sale_subscription", "sale_renting", "account_accountant", "account_avatax", "account_sepa_direct_debit", "account_budget", "account_batch_payment", "account_followup", "account_asset", "account_reports", "account_saft", "account_extract", "account_invoice_extract", "account_bank_statement_extract", "hr_attendance_gantt", "hr_gantt", "hr_holidays_gantt", "web_gantt", "web_grid", "web_map", "web_cohort", "sale_temporal", "sale_amazon", "sale_shopee", "sale_ebay", "website_sale_renting", "crm_enterprise", "crm_helpdesk", "iap_alternative", "iap_credit", "stock_barcode", "stock_fleet_enterprise", "mail_*", # mail_enterprise, mail_gantt, ... "data_recycle", "project_forecast", "website_helpdesk_knowledge", "ai_*", "barcodes_gs1_nomenclature", "l10n_*:hr_payroll", # payroll localization "test_*", } def parse_manifest(path: Path) -> dict | None: src = path.read_text(encoding="utf-8") try: tree = ast.parse(src) except SyntaxError: return None for node in ast.walk(tree): if isinstance(node, ast.Dict): d = {} for k, v in zip(node.keys, node.values): if isinstance(k, ast.Constant): d.setdefault(k.value, _to_py(v)) return d return None def _to_py(node): if isinstance(node, ast.Constant): return node.value if isinstance(node, ast.List): return [_to_py(e) for e in node.elts] if isinstance(node, ast.Tuple): return tuple(_to_py(e) for e in node.elts) if isinstance(node, ast.Dict): return { _to_py(k): _to_py(v) for k, v in zip(node.keys, node.values) } return None def get_depends(manifest: dict | None) -> list[str]: if not manifest: return [] d = manifest.get("depends", []) return list(d) if isinstance(d, list) else [] # --- Scan Python files for cross-module imports/reference. --- IMPORT_RE = re.compile(r"from\s+odoo\.addons\.([\w.]+)\s+import") ENV_RE = re.compile(r"self\.env\[?\s*['\"]([\w.]+)['\"]\s*\]?") def module_requires_oeel(mod: str) -> list[str]: """Return list of OEEL-original modules referenced in code/XML.""" base = OEEL_ROOT / mod requires: set[str] = set() for r, _, files in os.walk(base): for fn in files: if not fn.endswith((".py", ".xml")): continue try: src = (Path(r) / fn).read_text( encoding="utf-8", errors="ignore" ) except OSError: continue for m in IMPORT_RE.finditer(src): target = m.group(1).split(".")[0] if target in OEEL_MODULES and target != mod: requires.add(target) for m in ENV_RE.finditer(src): target = m.group(1) if "." in target: root = target.split(".")[0] else: root = target # Most models live under their own module's namespace. # Rough heuristic: if the model name starts with a # likely-OEEL word the module already needs an OEEL module. return sorted(requires) # --- Classify. --- def categorize(mod: str) -> tuple[str, list[str]]: manifest_path = OEEL_ROOT / mod / "__manifest__.py" manifest = parse_manifest(manifest_path) deps = get_depends(manifest) # Deps that point outside OCB non_ocb_deps = [d for d in deps if d not in OCB_MODULES] # Truly enterprise-only dependency doomed = [d for d in non_ocb_deps if d in TRULY_ENTERPRISE] if doomed: return "doomed", non_ocb_deps code_oeel = module_requires_oeel(mod) if not non_ocb_deps and not code_oeel: return "safe", [] if non_ocb_deps or code_oeel: return "keep", sorted(set(non_ocb_deps + code_oeel)) return "doomed", non_ocb_deps def main(): buckets: dict[str, list[tuple[str, list[str]]]] = { "safe": [], "keep": [], "doomed": [], } for mod in sorted(OEEL_MODULES): cls, deps = categorize(mod) buckets[cls].append((mod, deps)) summary = { cls: {"count": len(v), "samples": v[:5], "total": len(v)} for cls, v in buckets.items() } print(json.dumps(summary, indent=2)) out = Path("/tmp/classification.txt") with out.open("w") as f: for cls, items in buckets.items(): f.write(f"=== {cls} ({len(items)}) ===\n") for m, deps in items: f.write(f" {m}: {deps}\n") print(f"\nFull classification: {out}") if __name__ == "__main__": main()