OdooLS detects your Odoo setup from `odools.toml` configuration files. They use [TOML](https://toml.io/) syntax and work the same across every supported IDE (VS Code, PyCharm, Zed), so a config can be shared across a team. You can run OdooLS with **no config at all**, a **single minimal file**, or a **layered multi-profile setup** — pick the level that fits your project. # Quick start ## Zero config Open a workspace and OdooLS auto-detects your setup, creating a profile named `"default"`. See [Auto-detection](#auto-detection) for the exact rules — in short, it looks for the Odoo sources and any addon directories in (or just below) your workspace folders. This is enough for the common case: open a `src/` folder containing `odoo/`, `enterprise/`, and `design-themes/`, and everything is found automatically. It works the same if you open `odoo/`, `enterprise/`, and `design-themes/` as separate workspace folders — OdooLS scans every folder you open. ## A minimal config file Auto-detection has limits: maybe you opened only an addons directory, want a specific Python interpreter, or your sources live outside the workspace. In that case, create an `odools.toml` in your workspace **root**: ```toml [[config]] name = "My Config" odoo_path = "/home/odoo/community" addons_paths = ["/home/odoo/enterprise"] python_path = "/home/odoo/venv/bin/python" ``` Each `[[config]]` block is a **profile**. Anything you don't set is auto-detected or falls back to its default. > **Duplicate workspace folder names are allowed**, but you can't target them with `${workspaceFolder:NAME}` — that pattern only resolves names that are unique, and is skipped for any name shared by several folders. All folders are still scanned regardless. # Configuration reference | Key | Type | Default | Description | |---|---|---|---| | `name` | `string` | `"default"` | Profile name, used to switch profiles in your IDE. | | `extends` | `string` | — | Inherit from another profile (see [Profiles](#profiles-and-inheritance)). | | `odoo_path` | `string` | auto-detected | Odoo source path (must contain `odoo/release.py`). | | `addons_paths` | `string[]` | auto-detected | Addon directories to load. | | `addons_merge` | `"merge"` \| `"override"` | `"merge"` | How `addons_paths` combines with a parent profile. | | `python_path` | `string` | detected `python3` | Python interpreter path, or a command name like `"python3"`. | | `stdlib` | `string` | — | Custom typeshed/stdlib path (end it with `/`). | | `additional_stubs` | `string[]` | — | Extra type-stub directories. | | `additional_stubs_merge` | `"merge"` \| `"override"` | `"merge"` | How `additional_stubs` combines with a parent profile. | | `additional_languages` | `string[]` | — | Extra i18n language codes, e.g. `["fr_BE", "es"]`. Base codes are added automatically (`"fr_BE"` also adds `"fr"`). Always merged with parents. | | `file_cache` | `bool` | `true` | Cache workspace files on disk. | | `diag_missing_imports` | `"all"` \| `"only_odoo"` \| `"none"` | `"all"` | Which missing-import diagnostics to report. | | `ac_filter_model_names` | `bool` | `true` | Autocomplete only model names from module dependencies. | | `auto_refresh_delay` | `integer` | `1000` | Delay (ms) before refreshing after a file change. Clamped to 1000–15000. | | `no_typeshed_stubs` | `bool` | `false` | Disable the built-in typeshed stubs. | | `diagnostic_settings` | `table` | — | Override the severity of individual diagnostics (see [Diagnostics](#diagnostics)). | | `diagnostic_filters` | `table[]` | — | Show/hide diagnostics per file (see [Diagnostics](#diagnostics)). | | `tsserver_command` | `string` | `"tsserver"` | Provide custom command to run tsserver (OdooLS > 1.5.0). | | `disable_semantic_tokens_python` | `bool` | `false` | Disable semantic token for python files (OdooLS > 1.5.2). | | `disable_semantic_tokens_javascript` | `bool` | `false` | Disable semantic token for javascript files (OdooLS > 1.5.2). | | `disable_semantic_tokens_xml` | `bool` | `false` | Disable semantic token for xmlfiles (OdooLS > 1.5.2). | | `disable_javascript` | `bool` | `false` | Turn off frontend-asset processing entirely: no tsserver process, and JS/TS files and Owl XML templates from manifest asset bundles are not parsed, so no related diagnostics or features (OdooLS >1.5.0). | | `ts_check` | `bool` | `false` | Turn off tsserver diagnostics in Js files. This is equivalent to checkJS in a tsconfig.js (OdooLS >1.5.0). | | `"$version"`, `"$base"` | `string` | — | Variables for paths, used by [versioning](#working-with-multiple-versions). Must be quoted (TOML keys with `$` require it). | **Paths accept variables.** In any path value you can use: - `${userHome}` — your home directory - `${workspaceFolder}` — the current workspace folder - `${workspaceFolder:NAME}` — the folder named `NAME` (dropped, not fatal, if it matches zero or several folders — recorded so your IDE's config panel can flag it) **Invalid values are dropped, not fatal.** A path that doesn't exist or fails validation (an `odoo_path` without `odoo/release.py`, an addon entry with no modules) is dropped — but recorded so your IDE's config panel can flag it. Check the server logs if a path isn't taking effect. **Some changes restart the server.** Editing `odoo_path`, `addons_paths`, `python_path`, `additional_stubs`, `stdlib`, `no_typeshed_stubs`, `tsserver_command`, `disable_javascript`, or a version (`$version`/`$base`) triggers a full restart. Everything else applies live. # Auto-detection OdooLS infers `odoo_path` and `addons_paths` from your workspace folders. **User-set values always win** — auto-detection only runs for a field you didn't set. > **New in 1.5.0:** auto-detection now searches *below* your workspace folders — immediate children for `odoo_path`, and a recursive scan for `addons_paths`. Earlier versions only checked the workspace folder roots themselves. ## Odoo path A directory is a valid Odoo path if it contains `odoo/release.py`. When `odoo_path` is unset, OdooLS checks: 1. each workspace folder **root**, then 2. their **immediate children** (one level deep) — only if no root matched. > **Error:** if more than one folder or child qualifies, OdooLS reports _"More than one workspace folder or subfolder is a valid odoo_path"_ — set `odoo_path` explicitly to resolve it. ## Addon paths A directory is a valid addon path if it directly contains at least one **module** (a subdirectory with a `__manifest__.py`). When `addons_paths` is unset, OdooLS: - includes each workspace folder **root** that is itself an addon path, and - **recursively scans** each folder (up to 10 levels deep) for more addon paths. When a directory qualifies, OdooLS stops descending into it (its children are modules, not further roots). Modules, hidden directories, and noise like `node_modules`, `__pycache__`, `venv`, and `dist` are never scanned. ### Combining manual and detected paths Setting `addons_paths` to **any** value — even `[]` — turns off auto-detection. To keep both, add the `$autoDetectAddons` token to your list: ```toml addons_paths = ["/path/to/custom/addons", "$autoDetectAddons"] ``` The token is replaced with the auto-detected paths and merged with the ones you wrote. # Multiple config files and profiles ## Where OdooLS reads config from OdooLS builds your configuration from several sources, **in this order**: 1. **An explicit config file**, if you set one — processed first. This is a single file outside the workspace tree: - **CLI:** pass `--config-path=PATH` (and optionally `--selected-config=NAME`) - **VS Code:** set `Odoo.serverConfigPath` (in the settings UI or `settings.json`) - **Zed:** set it under `"lsp"/"odoo"/"binary"/"arguments"` — see the [Zed README](https://github.com/odoo/odoo-zed/blob/release/README.md) PyCharm doesn't currently expose this — it lets you pick a profile from the Odoo status bar, but not point at a config file outside the workspace. 2. **Each workspace folder**, by walking **up** its directory tree and loading every `odools.toml` found. These sources are **merged**, not overriding — the explicit file doesn't replace your workspace files, and being processed first doesn't make it win. When the same profile appears in more than one source, list fields are unioned and scalar fields must agree (a conflict is an error — see [Across multiple workspace folders](#across-multiple-workspace-folders)). ## Configuration file priority within a workspace When walking up a single workspace folder's tree, every `odools.toml` is merged with the **closest file winning**: - **scalar fields** (e.g. `python_path`): the closest config wins, - **list fields** (e.g. `addons_paths`): combined per the [merge rules](#how-values-are-merged). Relative paths resolve against the directory of the file they appear in. This lets you keep generic settings high up (e.g. in your home directory) and override them per project. ## Profiles and inheritance Each `[[config]]` block is a profile. With several profiles, your IDE lets you switch between them. A profile can inherit from another with `extends`: ```toml [[config]] name = "project A" extends = "base_setup" addons_paths = ["./project_a"] ``` The parent can live in the same file or any `odools.toml` further up the tree. `extends` is resolved per workspace folder (after merging that folder's files), then the results from different workspace folders are merged. > **Errors:** extending a profile that doesn't exist is an error, and circular chains (A extends B, B extends A) are detected and rejected. ### How values are merged When a child inherits from a parent: - **Scalars** (`python_path`, `file_cache`, …): child wins; if the child doesn't set it, the parent's value is used. - **`addons_paths` / `additional_stubs`**: controlled by their `*_merge` field — `"merge"` (default) concatenates parent + child, `"override"` keeps only the child's list. - **`additional_languages`**: always concatenated (no override). - **`diagnostic_settings`**: merged per code — child overrides the parent for the same code. - **`diagnostic_filters`**: accumulated — both parent and child filters apply. ### Across multiple workspace folders When the same profile name appears in several workspace folders, they merge — but scalars must agree: - **list fields** are unioned, - **scalar fields** must hold the same value everywhere, or OdooLS reports _"Conflict detected in 'profile' for key 'field': 'value1' vs 'value2'."_ ## Example ``` 📁 home/user/ ├── 📄 odools.toml ├── 📁 community/ (odoo source) └── 📁 my_addons/ ├── 📄 odools.toml ├── 📁 project_a/ └── 📁 project_b/ ``` `/home/user/odools.toml` — shared base: ```toml [[config]] name = "base_setup" file_cache = false odoo_path = "./community" ``` `/home/user/my_addons/odools.toml` — one profile per project: ```toml [[config]] name = "project A" extends = "base_setup" addons_paths = ["./project_a"] [[config]] name = "project B" extends = "base_setup" addons_paths = ["./project_b"] ``` Open `my_addons` and pick "project A" or "project B" — no need to open `community` separately. To skip the manual choice, use a dynamic path instead: ```toml [[config]] name = "Current project" extends = "base_setup" addons_paths = ["${workspaceFolder}"] ``` Now any folder you open directly under `my_addons` is loaded as its addon path automatically. # Working with multiple versions If you keep several Odoo versions side by side, OdooLS can handle versions for you with two mechanisms. Use one or the other — if both are set, **detection wins** (it runs first and overrides `$version`). In both, `${version}` and `${base}` are path variables that expand to the values of `$version` and `$base`. > **Tip:** `$version` can also point at a `__manifest__.py` — OdooLS reads its `version` field and keeps the `major.minor` part (e.g. `"18.0.1.0.0"` → `"18.0"`). ## Splitting: one profile per version directory `${splitVersion}` scans a directory for version-numbered subdirectories and creates one profile per version, so you don't duplicate profiles by hand. ``` 📁 home/user/my_addons/project_a/ ├── 📁 17.0/ └── 📁 18.0/ ``` ```toml [[config]] name = "Current project" "$version" = "${workspaceFolder}/${splitVersion}" addons_paths = ["${workspaceFolder}/${version}"] ``` OdooLS scans the resolved directory for version-named subdirectories (matching `^(\D+~)?\d+\.\d+$`, so `17.0`, `18.0`, `saas~16.1`, …) and generates a profile per match, named `{original}-{version}` (e.g. `Current project-17.0`, `Current project-18.0`). The original profile becomes a template. Open `project_a` and pick the version you want. ## Detection: read the version from the path `${detectVersion}` reads the version out of the workspace path instead of splitting. Put it at the end of `$base`; OdooLS strips the token, matches the rest against the open folder's path, and takes the next path component as the version. ``` 📁 home/user/ ├── 📁 17.0/ │ ├── 📁 odoo/ │ └── 📁 enterprise/ └── 📁 18.0/ ├── 📁 odoo/ └── 📁 enterprise/ ``` ```toml [[config]] name = "default" "$base" = "/home/user/${detectVersion}" odoo_path = "${base}/odoo" addons_paths = ["${base}/enterprise"] ``` Open any folder under `/home/user/17.0/` or `/home/user/18.0/` (however deeply nested) and OdooLS resolves `${base}` and `${version}` to that version automatically. # Diagnostics ## Changing severity Override the severity of individual diagnostics — or disable them — with `diagnostic_settings`: ```toml [config.diagnostic_settings] OLS03001 = "Info" OLS02001 = "Disabled" ``` Allowed values (case-sensitive): `Error`, `Warning`, `Info`, `Hint`, `Disabled`. Across inheritance, child settings override parent settings for the same code. The full list of codes is in [diagnostic_codes_list.rs](https://github.com/odoo/odoo-ls/blob/release/server/src/core/diagnostic_codes_list.rs). ## Filtering by file `diagnostic_filters` controls which diagnostics appear in which files. Add one or more `[[config.diagnostic_filters]]` blocks: ```toml [[config.diagnostic_filters]] codes = ["OLS.*"] paths = ["**/my_folder_1/*", "**/account*/*"] path_type = "notin" ``` | Key | Type | Required | Description | |---|---|---|---| | `paths` | `string[]` | **yes** | [Glob patterns](https://docs.rs/glob/0.3.3/glob/struct.Pattern.html) for matching files. Supports template variables (`${workspaceFolder}`, `${version}`, …). | | `codes` | `string[]` | no | Regexes matching diagnostic codes to filter. Invalid regexes fail config loading. | | `types` | `string[]` | no | Severities to filter: `"Error"`, `"Warning"`, `"Info"`, `"Hint"`. (`"Disabled"` is not valid here.) | | `path_type` | `"in"` \| `"notin"` | no | Apply the filter inside (`in`, default) or outside (`notin`) the matched paths. | - `path_type = "in"`: hide the matching diagnostics **in** the matched files. - `path_type = "notin"`: hide them **everywhere except** the matched files. So the example above hides `OLS.*` diagnostics everywhere except `my_folder_1` and `account*` modules — i.e. only those show OLS diagnostics. Filters accumulate across inheritance: parent and child filters all apply. ### Notes about diagnostic codes Diagnostic codes exported by OdooLS are under three formats: - Python diagnostics are under the format `OLSxxxxxx` where x is the specific error code ([See codes from OdooLS](https://github.com/odoo/odoo-ls/blob/release/server/src/core/diagnostic_codes_list.rs)) - OXC diagnostics for Javascript files are under the format `oxc_XXX` where XXX is the code from OXC ([See Rules from OXC](https://oxc.rs/docs/guide/usage/linter/rules.html)). Example: `oxc_eslint(no-unused-vars)` - Typescript diagnostics for Javascript and owl-xml files are under the format `tsserver_XX` where XX is the code from tsserver ([See errors from tsserver](https://github.com/microsoft/TypeScript/blob/main/src/compiler/diagnosticMessages.json)). Example: tsserver_7006