odoo-agent-cli 0.2.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- odoo_agent_cli-0.2.0/.gitignore +13 -0
- odoo_agent_cli-0.2.0/LICENSE +21 -0
- odoo_agent_cli-0.2.0/PKG-INFO +208 -0
- odoo_agent_cli-0.2.0/README.md +181 -0
- odoo_agent_cli-0.2.0/SKILL.md +144 -0
- odoo_agent_cli-0.2.0/pyproject.toml +83 -0
- odoo_agent_cli-0.2.0/src/odoocli/AGENT_GUIDE.md +139 -0
- odoo_agent_cli-0.2.0/src/odoocli/__init__.py +29 -0
- odoo_agent_cli-0.2.0/src/odoocli/__main__.py +5 -0
- odoo_agent_cli-0.2.0/src/odoocli/_version.py +3 -0
- odoo_agent_cli-0.2.0/src/odoocli/cli/__init__.py +0 -0
- odoo_agent_cli-0.2.0/src/odoocli/cli/app.py +337 -0
- odoo_agent_cli-0.2.0/src/odoocli/cli/guide_cmd.py +19 -0
- odoo_agent_cli-0.2.0/src/odoocli/cli/output.py +105 -0
- odoo_agent_cli-0.2.0/src/odoocli/cli/profile_cmds.py +116 -0
- odoo_agent_cli-0.2.0/src/odoocli/cli/read_cmds.py +223 -0
- odoo_agent_cli-0.2.0/src/odoocli/cli/values.py +51 -0
- odoo_agent_cli-0.2.0/src/odoocli/cli/write_cmds.py +178 -0
- odoo_agent_cli-0.2.0/src/odoocli/client.py +312 -0
- odoo_agent_cli-0.2.0/src/odoocli/config.py +175 -0
- odoo_agent_cli-0.2.0/src/odoocli/domain.py +208 -0
- odoo_agent_cli-0.2.0/src/odoocli/errors.py +99 -0
- odoo_agent_cli-0.2.0/src/odoocli/lenient.py +93 -0
- odoo_agent_cli-0.2.0/src/odoocli/py.typed +0 -0
- odoo_agent_cli-0.2.0/src/odoocli/security.py +83 -0
- odoo_agent_cli-0.2.0/src/odoocli/sync.py +126 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Organize-IT
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: odoo-agent-cli
|
|
3
|
+
Version: 0.2.0
|
|
4
|
+
Summary: Odoo JSON-RPC CLI and Python client built for AI agents and scripts.
|
|
5
|
+
Project-URL: Homepage, https://github.com/Organize-IT/odoo-cli
|
|
6
|
+
Project-URL: Issues, https://github.com/Organize-IT/odoo-cli/issues
|
|
7
|
+
Author-email: Organize-IT <maxime@organizeit.be>
|
|
8
|
+
License-Expression: MIT
|
|
9
|
+
License-File: LICENSE
|
|
10
|
+
Keywords: agents,cli,erp,json-rpc,odoo
|
|
11
|
+
Classifier: Development Status :: 4 - Beta
|
|
12
|
+
Classifier: Environment :: Console
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
15
|
+
Classifier: Programming Language :: Python :: 3
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
19
|
+
Classifier: Topic :: Office/Business :: Financial :: Accounting
|
|
20
|
+
Classifier: Topic :: Software Development :: Libraries
|
|
21
|
+
Requires-Python: >=3.11
|
|
22
|
+
Requires-Dist: httpx>=0.27
|
|
23
|
+
Requires-Dist: rich>=13
|
|
24
|
+
Requires-Dist: tomli-w>=1.0
|
|
25
|
+
Requires-Dist: typer>=0.12
|
|
26
|
+
Description-Content-Type: text/markdown
|
|
27
|
+
|
|
28
|
+
# odoo-agent-cli
|
|
29
|
+
|
|
30
|
+
Odoo JSON-RPC command line tool and Python client, built for AI agents and scripts.
|
|
31
|
+
|
|
32
|
+
`odoo search res.partner -w is_company=true --fields name,email` prints exactly what Odoo
|
|
33
|
+
returns. stdout is data only, stderr is diagnostics, exit codes mean something, and nothing
|
|
34
|
+
writes to your ERP unless you switched writes on for that connection.
|
|
35
|
+
|
|
36
|
+
Extracted from the connector that powers [UpBoard.ai](https://upboard.ai). Not affiliated with
|
|
37
|
+
Odoo S.A.
|
|
38
|
+
|
|
39
|
+
## Install
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
uv tool install odoo-agent-cli # or: pipx install odoo-agent-cli
|
|
43
|
+
odoo --version
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
Requires Python 3.11+. Works with Odoo 17, 18 and 19 (integration-tested in CI), and should
|
|
47
|
+
work with any version exposing `/jsonrpc` with API keys (14+).
|
|
48
|
+
|
|
49
|
+
## 60 seconds
|
|
50
|
+
|
|
51
|
+
```bash
|
|
52
|
+
export ODOO_URL=https://mycompany.odoo.com ODOO_DB=mycompany \
|
|
53
|
+
ODOO_LOGIN=bot@mycompany.com ODOO_API_KEY=... # API key or password
|
|
54
|
+
|
|
55
|
+
odoo info # version, uid, connection source
|
|
56
|
+
odoo models --like invoice # find the right technical name
|
|
57
|
+
odoo fields account.move --stored # what you can filter and order on
|
|
58
|
+
odoo count account.move -w move_type=out_invoice -w payment_state=not_paid
|
|
59
|
+
odoo search account.move -w move_type=out_invoice -w invoice_date_due<2026-09-01 \
|
|
60
|
+
--fields name,partner_id,amount_residual --order "invoice_date_due" --limit 20
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
Prefer named connections? They live in a `0600` TOML file:
|
|
64
|
+
|
|
65
|
+
```bash
|
|
66
|
+
odoo profile add acme --url https://acme.odoo.com --db acme --login bot@acme.com \
|
|
67
|
+
--api-key-env ACME_ODOO_KEY --test
|
|
68
|
+
odoo -p acme search res.partner -w name~acme
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
## Connection resolution
|
|
72
|
+
|
|
73
|
+
First match wins, and the CLI never prompts:
|
|
74
|
+
|
|
75
|
+
1. `--profile NAME` or `ODOO_PROFILE=NAME`
|
|
76
|
+
2. `ODOO_URL`, `ODOO_DB`, `ODOO_LOGIN`, `ODOO_API_KEY`
|
|
77
|
+
3. a profile named `default`
|
|
78
|
+
|
|
79
|
+
Nothing found: exit code 3 with a message listing those three ways. A profile stores the key
|
|
80
|
+
(`--api-key`) or points to an env var (`--api-key-env`). `odoo profile path` shows the file.
|
|
81
|
+
|
|
82
|
+
## Output contract
|
|
83
|
+
|
|
84
|
+
| Situation | stdout | stderr | exit |
|
|
85
|
+
|---|---|---|---|
|
|
86
|
+
| piped / captured | raw Odoo JSON (`--format json` by default) | | 0 |
|
|
87
|
+
| terminal | table (`--format table`), `jsonl` and `csv` available | | 0 |
|
|
88
|
+
| Odoo raised | | `{"error": {"code", "message", "odoo": {...}}}` | 1 |
|
|
89
|
+
| bad arguments | | `{"error": ...}` | 2 |
|
|
90
|
+
| connection, auth, no profile | | `{"error": ...}` | 3 |
|
|
91
|
+
| refused by a guard | | `{"error": ...}` | 4 |
|
|
92
|
+
| write executed | result | `{"write": {"model", "method", "ids", "fields"}}` | 0 |
|
|
93
|
+
|
|
94
|
+
Data is never humanised: many2one fields stay `[id, "name"]`, empty values stay `false`.
|
|
95
|
+
Values of fields named like `password`, `api_key`, `secret` are `[redacted]` unless
|
|
96
|
+
`--no-redact`. Global options are accepted anywhere on the command line:
|
|
97
|
+
|
|
98
|
+
| Option | Effect |
|
|
99
|
+
|---|---|
|
|
100
|
+
| `--profile/-p NAME`, `--format/-f FMT`, `--timeout S` | connection, output, per-call timeout |
|
|
101
|
+
| `--include-archived` | context `active_test=false`: searches also return archived records |
|
|
102
|
+
| `--company ID`, `--lang CODE`, `--context JSON` | Odoo context merged into every call |
|
|
103
|
+
| `--insecure` | skip TLS verification (self-signed on-prem) |
|
|
104
|
+
| `--no-redact`, `--include-sensitive` | lift the two output/model guards |
|
|
105
|
+
| `--debug` | one JSON line per RPC on stderr (method, id, duration, retries) |
|
|
106
|
+
| `--verbose` | include Odoo's server traceback in error output |
|
|
107
|
+
|
|
108
|
+
## Conditions
|
|
109
|
+
|
|
110
|
+
`-w` is repeatable and AND-ed; `--domain` takes a raw Odoo domain (use it for OR).
|
|
111
|
+
|
|
112
|
+
| `-w` | Odoo leaf |
|
|
113
|
+
|---|---|
|
|
114
|
+
| `is_company=true` | `["is_company", "=", true]` |
|
|
115
|
+
| `amount_total>=1000` | `["amount_total", ">=", 1000]` |
|
|
116
|
+
| `name~acme` / `name!~acme` | `["name", "ilike", "acme"]` / `not ilike` |
|
|
117
|
+
| `state in draft,sent` | `["state", "in", ["draft", "sent"]]` |
|
|
118
|
+
| `email=null` | `["email", "=", false]` |
|
|
119
|
+
| `partner_id.country_id.code=BE` | `["partner_id.country_id.code", "=", "BE"]` |
|
|
120
|
+
| `tag_ids in [1,2]` | `["tag_ids", "in", [1, 2]]` |
|
|
121
|
+
| `parent_id child_of 5` | `["parent_id", "child_of", 5]` |
|
|
122
|
+
|
|
123
|
+
Values: `true/false/null`, integers, floats, JSON lists or objects, quoted strings, else text.
|
|
124
|
+
|
|
125
|
+
## Writes
|
|
126
|
+
|
|
127
|
+
Off by default. Enable per connection with `allow_writes = true` on the profile
|
|
128
|
+
(`odoo profile add ... --allow-writes`) or `ODOO_ALLOW_WRITES=1`.
|
|
129
|
+
|
|
130
|
+
```bash
|
|
131
|
+
odoo create crm.lead -v name="Website inquiry" -v partner_id=42 --dry-run # shows payload, exit 0
|
|
132
|
+
odoo create crm.lead -v name="Website inquiry" -v partner_id=42 # prints the new id
|
|
133
|
+
odoo write res.partner 42,43 -v active=false
|
|
134
|
+
odoo unlink res.partner 99 --yes # --yes required
|
|
135
|
+
odoo call sale.order action_confirm --ids 12 --yes # any method
|
|
136
|
+
odoo call res.partner name_search --args '["acme"]' --kwargs '{"limit": 5}' # read-only: no guard
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
`unlink` and any `call` to a non read-only method need `--yes` or `ODOO_ASSUME_YES=1`.
|
|
140
|
+
Sensitive models (`ir.config_parameter`, `ir.mail_server`, `res.users.apikeys`, `ir.cron`, ...)
|
|
141
|
+
are refused unless `--include-sensitive`.
|
|
142
|
+
|
|
143
|
+
## For AI agents
|
|
144
|
+
|
|
145
|
+
- `odoo agent-guide` prints the working method, pitfalls (non-stored fields, version drift,
|
|
146
|
+
many2one shapes) and recipes.
|
|
147
|
+
- The same text ships as an [Agent Skill](https://github.com/Organize-IT/odoo-cli/blob/main/SKILL.md):
|
|
148
|
+
`npx skills add Organize-IT/odoo-cli`.
|
|
149
|
+
- `odoo search ... --lenient-fields` removes fields Odoo rejects and retries, with a warning on
|
|
150
|
+
stderr. Exploration only.
|
|
151
|
+
|
|
152
|
+
## Library
|
|
153
|
+
|
|
154
|
+
The distribution is `odoo-agent-cli`; the import name is `odoocli`.
|
|
155
|
+
|
|
156
|
+
```python
|
|
157
|
+
from odoocli import OdooClient
|
|
158
|
+
|
|
159
|
+
with OdooClient("https://acme.odoo.com", "acme", "bot@acme.com", "api-key") as odoo:
|
|
160
|
+
overdue = odoo.search_read(
|
|
161
|
+
"account.move",
|
|
162
|
+
[["move_type", "=", "out_invoice"], ["payment_state", "=", "not_paid"]],
|
|
163
|
+
["name", "partner_id", "amount_residual"],
|
|
164
|
+
limit=50,
|
|
165
|
+
order="invoice_date_due",
|
|
166
|
+
)
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
```python
|
|
170
|
+
from odoocli import AsyncOdooClient, OdooAccessError
|
|
171
|
+
|
|
172
|
+
async with AsyncOdooClient(url, db, login, key) as odoo:
|
|
173
|
+
try:
|
|
174
|
+
new_id = await odoo.create("res.partner", {"name": "Acme"})
|
|
175
|
+
except OdooAccessError as e:
|
|
176
|
+
print(e.code, e.message, e.data)
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
Exceptions: `OdooError` (base, `.code`, `.message`, `.data`), `OdooConnectionError`,
|
|
180
|
+
`OdooAuthError`, `OdooAccessError`, `OdooValidationError`, `OdooMissingError`.
|
|
181
|
+
|
|
182
|
+
Both clients accept `context={...}` (merged into every call; a per-call `context=` keyword
|
|
183
|
+
wins), `verify_ssl=False` and `max_retries`. HTTP 429 is always retried with backoff and
|
|
184
|
+
`Retry-After`; network errors, timeouts and HTTP 5xx are retried only for calls that cannot
|
|
185
|
+
change data, so a `create` that timed out is never replayed. Logs go to the `odoocli.rpc`
|
|
186
|
+
logger. Domain helpers live in `odoocli.domain`, guards in `odoocli.security`, the opt-in
|
|
187
|
+
repair loop in `odoocli.lenient`.
|
|
188
|
+
|
|
189
|
+
## Development
|
|
190
|
+
|
|
191
|
+
```bash
|
|
192
|
+
uv sync --group dev
|
|
193
|
+
uv run pytest # unit tests, mocked JSON-RPC
|
|
194
|
+
uv run ruff check && uv run mypy
|
|
195
|
+
|
|
196
|
+
ODOO_VERSION=17.0 scripts/start-odoo.sh # throwaway Odoo in Docker
|
|
197
|
+
ODOO_URL=http://localhost:8069 ODOO_DB=test ODOO_LOGIN=admin ODOO_API_KEY=admin \
|
|
198
|
+
ODOO_ALLOW_WRITES=1 uv run pytest -m integration -o addopts=""
|
|
199
|
+
docker compose -f docker/odoo-compose.yml down -v
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
CI runs the unit suite on every PR and the integration matrix (Odoo 17.0, 18.0, 19.0) on
|
|
203
|
+
`main`, tags and manual dispatch. Releases are published to PyPI on `v*` tags through
|
|
204
|
+
trusted publishing.
|
|
205
|
+
|
|
206
|
+
## License
|
|
207
|
+
|
|
208
|
+
MIT. Odoo is a trademark of Odoo S.A.; this project is independent.
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
# odoo-agent-cli
|
|
2
|
+
|
|
3
|
+
Odoo JSON-RPC command line tool and Python client, built for AI agents and scripts.
|
|
4
|
+
|
|
5
|
+
`odoo search res.partner -w is_company=true --fields name,email` prints exactly what Odoo
|
|
6
|
+
returns. stdout is data only, stderr is diagnostics, exit codes mean something, and nothing
|
|
7
|
+
writes to your ERP unless you switched writes on for that connection.
|
|
8
|
+
|
|
9
|
+
Extracted from the connector that powers [UpBoard.ai](https://upboard.ai). Not affiliated with
|
|
10
|
+
Odoo S.A.
|
|
11
|
+
|
|
12
|
+
## Install
|
|
13
|
+
|
|
14
|
+
```bash
|
|
15
|
+
uv tool install odoo-agent-cli # or: pipx install odoo-agent-cli
|
|
16
|
+
odoo --version
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
Requires Python 3.11+. Works with Odoo 17, 18 and 19 (integration-tested in CI), and should
|
|
20
|
+
work with any version exposing `/jsonrpc` with API keys (14+).
|
|
21
|
+
|
|
22
|
+
## 60 seconds
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
export ODOO_URL=https://mycompany.odoo.com ODOO_DB=mycompany \
|
|
26
|
+
ODOO_LOGIN=bot@mycompany.com ODOO_API_KEY=... # API key or password
|
|
27
|
+
|
|
28
|
+
odoo info # version, uid, connection source
|
|
29
|
+
odoo models --like invoice # find the right technical name
|
|
30
|
+
odoo fields account.move --stored # what you can filter and order on
|
|
31
|
+
odoo count account.move -w move_type=out_invoice -w payment_state=not_paid
|
|
32
|
+
odoo search account.move -w move_type=out_invoice -w invoice_date_due<2026-09-01 \
|
|
33
|
+
--fields name,partner_id,amount_residual --order "invoice_date_due" --limit 20
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
Prefer named connections? They live in a `0600` TOML file:
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
odoo profile add acme --url https://acme.odoo.com --db acme --login bot@acme.com \
|
|
40
|
+
--api-key-env ACME_ODOO_KEY --test
|
|
41
|
+
odoo -p acme search res.partner -w name~acme
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
## Connection resolution
|
|
45
|
+
|
|
46
|
+
First match wins, and the CLI never prompts:
|
|
47
|
+
|
|
48
|
+
1. `--profile NAME` or `ODOO_PROFILE=NAME`
|
|
49
|
+
2. `ODOO_URL`, `ODOO_DB`, `ODOO_LOGIN`, `ODOO_API_KEY`
|
|
50
|
+
3. a profile named `default`
|
|
51
|
+
|
|
52
|
+
Nothing found: exit code 3 with a message listing those three ways. A profile stores the key
|
|
53
|
+
(`--api-key`) or points to an env var (`--api-key-env`). `odoo profile path` shows the file.
|
|
54
|
+
|
|
55
|
+
## Output contract
|
|
56
|
+
|
|
57
|
+
| Situation | stdout | stderr | exit |
|
|
58
|
+
|---|---|---|---|
|
|
59
|
+
| piped / captured | raw Odoo JSON (`--format json` by default) | | 0 |
|
|
60
|
+
| terminal | table (`--format table`), `jsonl` and `csv` available | | 0 |
|
|
61
|
+
| Odoo raised | | `{"error": {"code", "message", "odoo": {...}}}` | 1 |
|
|
62
|
+
| bad arguments | | `{"error": ...}` | 2 |
|
|
63
|
+
| connection, auth, no profile | | `{"error": ...}` | 3 |
|
|
64
|
+
| refused by a guard | | `{"error": ...}` | 4 |
|
|
65
|
+
| write executed | result | `{"write": {"model", "method", "ids", "fields"}}` | 0 |
|
|
66
|
+
|
|
67
|
+
Data is never humanised: many2one fields stay `[id, "name"]`, empty values stay `false`.
|
|
68
|
+
Values of fields named like `password`, `api_key`, `secret` are `[redacted]` unless
|
|
69
|
+
`--no-redact`. Global options are accepted anywhere on the command line:
|
|
70
|
+
|
|
71
|
+
| Option | Effect |
|
|
72
|
+
|---|---|
|
|
73
|
+
| `--profile/-p NAME`, `--format/-f FMT`, `--timeout S` | connection, output, per-call timeout |
|
|
74
|
+
| `--include-archived` | context `active_test=false`: searches also return archived records |
|
|
75
|
+
| `--company ID`, `--lang CODE`, `--context JSON` | Odoo context merged into every call |
|
|
76
|
+
| `--insecure` | skip TLS verification (self-signed on-prem) |
|
|
77
|
+
| `--no-redact`, `--include-sensitive` | lift the two output/model guards |
|
|
78
|
+
| `--debug` | one JSON line per RPC on stderr (method, id, duration, retries) |
|
|
79
|
+
| `--verbose` | include Odoo's server traceback in error output |
|
|
80
|
+
|
|
81
|
+
## Conditions
|
|
82
|
+
|
|
83
|
+
`-w` is repeatable and AND-ed; `--domain` takes a raw Odoo domain (use it for OR).
|
|
84
|
+
|
|
85
|
+
| `-w` | Odoo leaf |
|
|
86
|
+
|---|---|
|
|
87
|
+
| `is_company=true` | `["is_company", "=", true]` |
|
|
88
|
+
| `amount_total>=1000` | `["amount_total", ">=", 1000]` |
|
|
89
|
+
| `name~acme` / `name!~acme` | `["name", "ilike", "acme"]` / `not ilike` |
|
|
90
|
+
| `state in draft,sent` | `["state", "in", ["draft", "sent"]]` |
|
|
91
|
+
| `email=null` | `["email", "=", false]` |
|
|
92
|
+
| `partner_id.country_id.code=BE` | `["partner_id.country_id.code", "=", "BE"]` |
|
|
93
|
+
| `tag_ids in [1,2]` | `["tag_ids", "in", [1, 2]]` |
|
|
94
|
+
| `parent_id child_of 5` | `["parent_id", "child_of", 5]` |
|
|
95
|
+
|
|
96
|
+
Values: `true/false/null`, integers, floats, JSON lists or objects, quoted strings, else text.
|
|
97
|
+
|
|
98
|
+
## Writes
|
|
99
|
+
|
|
100
|
+
Off by default. Enable per connection with `allow_writes = true` on the profile
|
|
101
|
+
(`odoo profile add ... --allow-writes`) or `ODOO_ALLOW_WRITES=1`.
|
|
102
|
+
|
|
103
|
+
```bash
|
|
104
|
+
odoo create crm.lead -v name="Website inquiry" -v partner_id=42 --dry-run # shows payload, exit 0
|
|
105
|
+
odoo create crm.lead -v name="Website inquiry" -v partner_id=42 # prints the new id
|
|
106
|
+
odoo write res.partner 42,43 -v active=false
|
|
107
|
+
odoo unlink res.partner 99 --yes # --yes required
|
|
108
|
+
odoo call sale.order action_confirm --ids 12 --yes # any method
|
|
109
|
+
odoo call res.partner name_search --args '["acme"]' --kwargs '{"limit": 5}' # read-only: no guard
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
`unlink` and any `call` to a non read-only method need `--yes` or `ODOO_ASSUME_YES=1`.
|
|
113
|
+
Sensitive models (`ir.config_parameter`, `ir.mail_server`, `res.users.apikeys`, `ir.cron`, ...)
|
|
114
|
+
are refused unless `--include-sensitive`.
|
|
115
|
+
|
|
116
|
+
## For AI agents
|
|
117
|
+
|
|
118
|
+
- `odoo agent-guide` prints the working method, pitfalls (non-stored fields, version drift,
|
|
119
|
+
many2one shapes) and recipes.
|
|
120
|
+
- The same text ships as an [Agent Skill](https://github.com/Organize-IT/odoo-cli/blob/main/SKILL.md):
|
|
121
|
+
`npx skills add Organize-IT/odoo-cli`.
|
|
122
|
+
- `odoo search ... --lenient-fields` removes fields Odoo rejects and retries, with a warning on
|
|
123
|
+
stderr. Exploration only.
|
|
124
|
+
|
|
125
|
+
## Library
|
|
126
|
+
|
|
127
|
+
The distribution is `odoo-agent-cli`; the import name is `odoocli`.
|
|
128
|
+
|
|
129
|
+
```python
|
|
130
|
+
from odoocli import OdooClient
|
|
131
|
+
|
|
132
|
+
with OdooClient("https://acme.odoo.com", "acme", "bot@acme.com", "api-key") as odoo:
|
|
133
|
+
overdue = odoo.search_read(
|
|
134
|
+
"account.move",
|
|
135
|
+
[["move_type", "=", "out_invoice"], ["payment_state", "=", "not_paid"]],
|
|
136
|
+
["name", "partner_id", "amount_residual"],
|
|
137
|
+
limit=50,
|
|
138
|
+
order="invoice_date_due",
|
|
139
|
+
)
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
```python
|
|
143
|
+
from odoocli import AsyncOdooClient, OdooAccessError
|
|
144
|
+
|
|
145
|
+
async with AsyncOdooClient(url, db, login, key) as odoo:
|
|
146
|
+
try:
|
|
147
|
+
new_id = await odoo.create("res.partner", {"name": "Acme"})
|
|
148
|
+
except OdooAccessError as e:
|
|
149
|
+
print(e.code, e.message, e.data)
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
Exceptions: `OdooError` (base, `.code`, `.message`, `.data`), `OdooConnectionError`,
|
|
153
|
+
`OdooAuthError`, `OdooAccessError`, `OdooValidationError`, `OdooMissingError`.
|
|
154
|
+
|
|
155
|
+
Both clients accept `context={...}` (merged into every call; a per-call `context=` keyword
|
|
156
|
+
wins), `verify_ssl=False` and `max_retries`. HTTP 429 is always retried with backoff and
|
|
157
|
+
`Retry-After`; network errors, timeouts and HTTP 5xx are retried only for calls that cannot
|
|
158
|
+
change data, so a `create` that timed out is never replayed. Logs go to the `odoocli.rpc`
|
|
159
|
+
logger. Domain helpers live in `odoocli.domain`, guards in `odoocli.security`, the opt-in
|
|
160
|
+
repair loop in `odoocli.lenient`.
|
|
161
|
+
|
|
162
|
+
## Development
|
|
163
|
+
|
|
164
|
+
```bash
|
|
165
|
+
uv sync --group dev
|
|
166
|
+
uv run pytest # unit tests, mocked JSON-RPC
|
|
167
|
+
uv run ruff check && uv run mypy
|
|
168
|
+
|
|
169
|
+
ODOO_VERSION=17.0 scripts/start-odoo.sh # throwaway Odoo in Docker
|
|
170
|
+
ODOO_URL=http://localhost:8069 ODOO_DB=test ODOO_LOGIN=admin ODOO_API_KEY=admin \
|
|
171
|
+
ODOO_ALLOW_WRITES=1 uv run pytest -m integration -o addopts=""
|
|
172
|
+
docker compose -f docker/odoo-compose.yml down -v
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
CI runs the unit suite on every PR and the integration matrix (Odoo 17.0, 18.0, 19.0) on
|
|
176
|
+
`main`, tags and manual dispatch. Releases are published to PyPI on `v*` tags through
|
|
177
|
+
trusted publishing.
|
|
178
|
+
|
|
179
|
+
## License
|
|
180
|
+
|
|
181
|
+
MIT. Odoo is a trademark of Odoo S.A.; this project is independent.
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: odoo-cli
|
|
3
|
+
description: Use when a task needs to read or change data in an Odoo ERP (partners, invoices, orders, stock, any model) through the `odoo` command line tool from the odoo-agent-cli package (import name odoocli). Covers connection setup, domain syntax, guarded writes, output contract and version-drift pitfalls.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# odoo CLI guide for AI agents
|
|
7
|
+
|
|
8
|
+
`odoo` talks to one Odoo database over JSON-RPC. It is built so that a program can drive it:
|
|
9
|
+
stdout is data only, stderr is diagnostics, exit codes mean something.
|
|
10
|
+
|
|
11
|
+
## Connection
|
|
12
|
+
|
|
13
|
+
Resolution order, first match wins:
|
|
14
|
+
|
|
15
|
+
1. `--profile NAME` or `ODOO_PROFILE=NAME`, defined with
|
|
16
|
+
`odoo profile add NAME --url https://... --db DB --login USER --api-key KEY`
|
|
17
|
+
2. Environment: `ODOO_URL`, `ODOO_DB`, `ODOO_LOGIN`, `ODOO_API_KEY`
|
|
18
|
+
3. A profile named `default`
|
|
19
|
+
|
|
20
|
+
Nothing resolved: exit code 3 and a message listing these three ways. The CLI never prompts.
|
|
21
|
+
`ODOO_API_KEY` accepts an Odoo API key (preferred) or the user's password.
|
|
22
|
+
Check a connection with `odoo info`. Self-signed on-prem server: `--insecure`
|
|
23
|
+
(or `odoo profile add ... --no-verify-ssl`).
|
|
24
|
+
|
|
25
|
+
## Context: archived records, company, language
|
|
26
|
+
|
|
27
|
+
Odoo hides archived records (`active = false`) from every search unless the context says
|
|
28
|
+
otherwise, and multi-company data depends on the company in context. These global flags
|
|
29
|
+
work on every command:
|
|
30
|
+
|
|
31
|
+
```
|
|
32
|
+
--include-archived context active_test=false: search also returns archived records
|
|
33
|
+
--company 3 context allowed_company_ids=[3]
|
|
34
|
+
--lang fr_BE labels and selection values in that language (must be installed
|
|
35
|
+
in Odoo: 18+ answers "Invalid language code" otherwise)
|
|
36
|
+
--context '{"tz": "Europe/Brussels"}' any other key, merged with the flags above
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
## Output contract
|
|
40
|
+
|
|
41
|
+
- Piped or captured: raw Odoo JSON, exactly what `search_read`, `read` or `fields_get` return.
|
|
42
|
+
Many2one fields are `[id, "display name"]`, empty values are `false`, never `null`.
|
|
43
|
+
- On a terminal: a table. Force a format with `--format json|jsonl|table|csv`.
|
|
44
|
+
Use `--format jsonl` for large result sets.
|
|
45
|
+
- Errors: one JSON object on stderr, `{"error": {"code": ..., "message": ..., "odoo": {...}}}`.
|
|
46
|
+
- Warnings and write logs: one JSON object per line on stderr.
|
|
47
|
+
- Exit codes: `0` ok, `1` Odoo raised, `2` bad usage, `3` connection or authentication,
|
|
48
|
+
`4` refused by a guard (writes disabled, missing `--yes`, sensitive model).
|
|
49
|
+
- Values of fields named like `password`, `api_key`, `secret` are replaced by `[redacted]`
|
|
50
|
+
unless `--no-redact`.
|
|
51
|
+
|
|
52
|
+
## Read commands
|
|
53
|
+
|
|
54
|
+
```
|
|
55
|
+
odoo info [--modules] server version, uid, optional installed modules
|
|
56
|
+
odoo models [--like sale] list models (technical name, label)
|
|
57
|
+
odoo fields MODEL [--type many2one] [--stored] [--search text] [--all-attributes]
|
|
58
|
+
odoo search MODEL [-w COND]... [--domain JSON] [--fields a,b] [--limit N] [--offset N]
|
|
59
|
+
[--order "x desc"] [--all] [--ids-only] [--lenient-fields]
|
|
60
|
+
odoo count MODEL [-w COND]... [--domain JSON]
|
|
61
|
+
odoo read MODEL ID [ID...] [--fields a,b]
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
Conditions (`-w`, repeatable, AND-ed together, combined with `--domain`):
|
|
65
|
+
|
|
66
|
+
```
|
|
67
|
+
-w is_company=true -w amount_total>=1000 -w name~acme (ilike)
|
|
68
|
+
-w name!~test -w state in draft,sent -w state not in done,cancel
|
|
69
|
+
-w email=null -w partner_id.country_id.code=BE
|
|
70
|
+
-w tag_ids in [1,2] -w parent_id child_of 5
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
For OR, use `--domain` with Odoo prefix notation: `--domain '["|",["a","=",1],["b","=",2]]'`.
|
|
74
|
+
|
|
75
|
+
## Write commands
|
|
76
|
+
|
|
77
|
+
Writes only exist when the profile has `allow_writes = true` or `ODOO_ALLOW_WRITES=1`.
|
|
78
|
+
Otherwise exit 4.
|
|
79
|
+
|
|
80
|
+
```
|
|
81
|
+
odoo create MODEL -v name=Acme -v is_company=true [--values JSON] [--dry-run]
|
|
82
|
+
odoo write MODEL IDS -v field=value... [--values JSON] [--dry-run]
|
|
83
|
+
odoo unlink MODEL IDS --yes [--dry-run]
|
|
84
|
+
odoo call MODEL METHOD [--ids 1,2] [--args JSON] [--kwargs JSON] [--yes] [--dry-run]
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
- `--dry-run` prints the exact payload and exits 0 without calling Odoo. Use it first.
|
|
88
|
+
- `unlink` and any `call` to a non read-only method need `--yes` (or `ODOO_ASSUME_YES=1`).
|
|
89
|
+
- `call` on read-only methods (`name_search`, `read_group`, `default_get`, ...) needs
|
|
90
|
+
neither `allow_writes` nor `--yes`.
|
|
91
|
+
- Every executed write logs one line on stderr:
|
|
92
|
+
`{"write": {"model": ..., "method": ..., "ids": [...], "fields": [...]}}`.
|
|
93
|
+
|
|
94
|
+
One2many and many2many fields take Odoo commands, written as JSON in `-v` or `--values`:
|
|
95
|
+
|
|
96
|
+
```
|
|
97
|
+
-v 'order_line=[[0,0,{"product_id":7,"product_uom_qty":2}]]' create a line
|
|
98
|
+
-v 'tag_ids=[[4,12]]' link id 12
|
|
99
|
+
-v 'tag_ids=[[6,0,[12,13]]]' replace with ids 12 and 13
|
|
100
|
+
-v 'tag_ids=[[3,12]]' unlink id 12 (keep record)
|
|
101
|
+
-v 'order_line=[[2,55]]' delete line 55
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
## Working method that avoids most failures
|
|
105
|
+
|
|
106
|
+
1. Unknown model? `odoo fields MODEL` first. It shows `type`, `required`, `store`, `relation`
|
|
107
|
+
and `selection` values.
|
|
108
|
+
2. Only filter or order on fields with `store: true`. Computed non-stored fields
|
|
109
|
+
(`qty_available`, `amount_to_invoice`, ...) can be read but not searched; Odoo answers
|
|
110
|
+
"Cannot convert ... to SQL". Read them and filter client-side.
|
|
111
|
+
3. `odoo count` before a wide `odoo search`. Default `--limit` is 80. `--all` paginates
|
|
112
|
+
everything; prefer `--format jsonl` with it. `--ids-only` when you only need ids.
|
|
113
|
+
4. Ask only for the fields you need with `--fields`. `search` without `--fields` returns
|
|
114
|
+
every field, which is slow and noisy.
|
|
115
|
+
5. Many2one values come back as `[id, name]`. Filter on them with the id
|
|
116
|
+
(`-w partner_id=42`) or through a related field (`-w partner_id.name~acme`).
|
|
117
|
+
6. Dates are strings, `YYYY-MM-DD` or `YYYY-MM-DD HH:MM:SS` in UTC.
|
|
118
|
+
7. Field names drift between Odoo 17, 18 and 19 (for example `account.account.company_id`
|
|
119
|
+
became `company_ids`). If a field is rejected, `odoo fields` is the truth.
|
|
120
|
+
`--lenient-fields` on `search` removes rejected fields and retries, with a warning on
|
|
121
|
+
stderr; only use it for exploration, never in a script that relies on the result.
|
|
122
|
+
8. Never guess a model name: `odoo models --like invoice`.
|
|
123
|
+
9. Sensitive models (`ir.config_parameter`, `ir.mail_server`, `res.users.apikeys`, `ir.cron`,
|
|
124
|
+
`ir.actions.server`, ...) are refused unless `--include-sensitive`.
|
|
125
|
+
10. A record you know exists but cannot find is usually archived (`--include-archived`) or in
|
|
126
|
+
another company (`--company`). `odoo read` exits 1 with `missing_record` in that case.
|
|
127
|
+
11. Something odd on the wire? `--debug` logs every RPC call (method, duration, retries) as
|
|
128
|
+
JSON lines on stderr. Network errors and HTTP 5xx are retried for reads only; a write that
|
|
129
|
+
timed out is reported, never replayed.
|
|
130
|
+
|
|
131
|
+
## Recipes
|
|
132
|
+
|
|
133
|
+
```
|
|
134
|
+
odoo search res.partner -w is_company=true -w country_id.code=BE --fields name,email,vat --limit 20
|
|
135
|
+
odoo search sale.order -w state=sale -w date_order>=2026-01-01 --fields name,partner_id,amount_total --order "amount_total desc"
|
|
136
|
+
odoo count account.move -w move_type=out_invoice -w payment_state=not_paid -w invoice_date_due<2026-09-01
|
|
137
|
+
odoo search product.product --fields name,qty_available --all --format jsonl | jq -c 'select(.qty_available < 0)'
|
|
138
|
+
odoo call res.partner name_search --args '["acme"]' --kwargs '{"limit": 5}'
|
|
139
|
+
odoo call account.move read_group --kwargs '{"domain": [["move_type","=","out_invoice"]], "fields": ["amount_total:sum"], "groupby": ["partner_id"]}'
|
|
140
|
+
odoo create crm.lead -v name="Website inquiry" -v partner_id=42 --dry-run
|
|
141
|
+
odoo call sale.order action_confirm --ids 12 --yes
|
|
142
|
+
odoo call sale.order action_cancel --ids 12 --yes --context '{"disable_cancel_warning": true}'
|
|
143
|
+
odoo search res.partner -w name~acme --include-archived --ids-only
|
|
144
|
+
```
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling>=1.25"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "odoo-agent-cli"
|
|
7
|
+
version = "0.2.0"
|
|
8
|
+
description = "Odoo JSON-RPC CLI and Python client built for AI agents and scripts."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = "MIT"
|
|
11
|
+
requires-python = ">=3.11"
|
|
12
|
+
authors = [{ name = "Organize-IT", email = "maxime@organizeit.be" }]
|
|
13
|
+
keywords = ["odoo", "cli", "json-rpc", "agents", "erp"]
|
|
14
|
+
classifiers = [
|
|
15
|
+
"Development Status :: 4 - Beta",
|
|
16
|
+
"Environment :: Console",
|
|
17
|
+
"Intended Audience :: Developers",
|
|
18
|
+
"License :: OSI Approved :: MIT License",
|
|
19
|
+
"Programming Language :: Python :: 3",
|
|
20
|
+
"Programming Language :: Python :: 3.11",
|
|
21
|
+
"Programming Language :: Python :: 3.12",
|
|
22
|
+
"Programming Language :: Python :: 3.13",
|
|
23
|
+
"Topic :: Office/Business :: Financial :: Accounting",
|
|
24
|
+
"Topic :: Software Development :: Libraries",
|
|
25
|
+
]
|
|
26
|
+
dependencies = [
|
|
27
|
+
"httpx>=0.27",
|
|
28
|
+
"typer>=0.12",
|
|
29
|
+
"rich>=13",
|
|
30
|
+
"tomli-w>=1.0",
|
|
31
|
+
]
|
|
32
|
+
|
|
33
|
+
[project.urls]
|
|
34
|
+
Homepage = "https://github.com/Organize-IT/odoo-cli"
|
|
35
|
+
Issues = "https://github.com/Organize-IT/odoo-cli/issues"
|
|
36
|
+
|
|
37
|
+
[project.scripts]
|
|
38
|
+
odoo = "odoocli.cli.app:main"
|
|
39
|
+
|
|
40
|
+
[dependency-groups]
|
|
41
|
+
dev = [
|
|
42
|
+
"pytest>=8",
|
|
43
|
+
"pytest-asyncio>=0.24",
|
|
44
|
+
"respx>=0.21",
|
|
45
|
+
"ruff>=0.6",
|
|
46
|
+
"mypy>=1.11",
|
|
47
|
+
]
|
|
48
|
+
|
|
49
|
+
[tool.hatch.build.targets.wheel]
|
|
50
|
+
packages = ["src/odoocli"]
|
|
51
|
+
|
|
52
|
+
[tool.hatch.build.targets.sdist]
|
|
53
|
+
include = ["src/odoocli", "README.md", "LICENSE", "SKILL.md"]
|
|
54
|
+
|
|
55
|
+
[tool.ruff]
|
|
56
|
+
line-length = 100
|
|
57
|
+
target-version = "py311"
|
|
58
|
+
src = ["src", "tests"]
|
|
59
|
+
|
|
60
|
+
[tool.ruff.lint]
|
|
61
|
+
select = ["E", "W", "F", "I", "B", "C4", "UP", "ARG", "SIM"]
|
|
62
|
+
|
|
63
|
+
[tool.ruff.lint.flake8-bugbear]
|
|
64
|
+
extend-immutable-calls = ["typer.Argument", "typer.Option"]
|
|
65
|
+
|
|
66
|
+
[tool.ruff.lint.per-file-ignores]
|
|
67
|
+
"tests/**" = ["ARG001"] # pytest fixtures are used for their side effects
|
|
68
|
+
|
|
69
|
+
[tool.mypy]
|
|
70
|
+
python_version = "3.11"
|
|
71
|
+
strict = true
|
|
72
|
+
files = ["src", "tests"]
|
|
73
|
+
warn_unreachable = true
|
|
74
|
+
|
|
75
|
+
[[tool.mypy.overrides]]
|
|
76
|
+
module = ["respx", "respx.*"]
|
|
77
|
+
ignore_missing_imports = true
|
|
78
|
+
|
|
79
|
+
[tool.pytest.ini_options]
|
|
80
|
+
asyncio_mode = "auto"
|
|
81
|
+
testpaths = ["tests"]
|
|
82
|
+
markers = ["integration: needs a live Odoo (ODOO_URL, ODOO_DB, ODOO_LOGIN, ODOO_API_KEY)"]
|
|
83
|
+
addopts = "-m 'not integration'"
|