odoo-agent-cli 0.3.0__tar.gz → 0.5.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.3.0 → odoo_agent_cli-0.5.0}/.gitignore +3 -0
- {odoo_agent_cli-0.3.0 → odoo_agent_cli-0.5.0}/PKG-INFO +97 -12
- {odoo_agent_cli-0.3.0 → odoo_agent_cli-0.5.0}/README.md +96 -11
- {odoo_agent_cli-0.3.0 → odoo_agent_cli-0.5.0}/SKILL.md +72 -7
- {odoo_agent_cli-0.3.0 → odoo_agent_cli-0.5.0}/pyproject.toml +17 -1
- {odoo_agent_cli-0.3.0 → odoo_agent_cli-0.5.0}/src/odoocli/AGENT_GUIDE.md +72 -7
- {odoo_agent_cli-0.3.0 → odoo_agent_cli-0.5.0}/src/odoocli/__init__.py +2 -0
- {odoo_agent_cli-0.3.0 → odoo_agent_cli-0.5.0}/src/odoocli/_version.py +1 -1
- odoo_agent_cli-0.5.0/src/odoocli/aliases.py +310 -0
- odoo_agent_cli-0.5.0/src/odoocli/cli/alias_cmd.py +41 -0
- {odoo_agent_cli-0.3.0 → odoo_agent_cli-0.5.0}/src/odoocli/cli/app.py +174 -5
- odoo_agent_cli-0.5.0/src/odoocli/cli/cache_cmds.py +72 -0
- {odoo_agent_cli-0.3.0 → odoo_agent_cli-0.5.0}/src/odoocli/cli/profile_cmds.py +39 -5
- {odoo_agent_cli-0.3.0 → odoo_agent_cli-0.5.0}/src/odoocli/cli/read_cmds.py +121 -20
- {odoo_agent_cli-0.3.0 → odoo_agent_cli-0.5.0}/src/odoocli/cli/write_cmds.py +33 -21
- {odoo_agent_cli-0.3.0 → odoo_agent_cli-0.5.0}/src/odoocli/config.py +33 -4
- {odoo_agent_cli-0.3.0 → odoo_agent_cli-0.5.0}/src/odoocli/domain.py +88 -14
- {odoo_agent_cli-0.3.0 → odoo_agent_cli-0.5.0}/src/odoocli/errors.py +11 -0
- odoo_agent_cli-0.5.0/src/odoocli/schema.py +362 -0
- {odoo_agent_cli-0.3.0 → odoo_agent_cli-0.5.0}/LICENSE +0 -0
- {odoo_agent_cli-0.3.0 → odoo_agent_cli-0.5.0}/src/odoocli/__main__.py +0 -0
- {odoo_agent_cli-0.3.0 → odoo_agent_cli-0.5.0}/src/odoocli/cli/__init__.py +0 -0
- {odoo_agent_cli-0.3.0 → odoo_agent_cli-0.5.0}/src/odoocli/cli/guide_cmd.py +0 -0
- {odoo_agent_cli-0.3.0 → odoo_agent_cli-0.5.0}/src/odoocli/cli/output.py +0 -0
- {odoo_agent_cli-0.3.0 → odoo_agent_cli-0.5.0}/src/odoocli/cli/values.py +0 -0
- {odoo_agent_cli-0.3.0 → odoo_agent_cli-0.5.0}/src/odoocli/client.py +0 -0
- {odoo_agent_cli-0.3.0 → odoo_agent_cli-0.5.0}/src/odoocli/lenient.py +0 -0
- {odoo_agent_cli-0.3.0 → odoo_agent_cli-0.5.0}/src/odoocli/py.typed +0 -0
- {odoo_agent_cli-0.3.0 → odoo_agent_cli-0.5.0}/src/odoocli/security.py +0 -0
- {odoo_agent_cli-0.3.0 → odoo_agent_cli-0.5.0}/src/odoocli/sync.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: odoo-agent-cli
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.5.0
|
|
4
4
|
Summary: Odoo JSON-RPC CLI and Python client built for AI agents and scripts.
|
|
5
5
|
Project-URL: Homepage, https://github.com/Organize-IT/odoo-cli
|
|
6
6
|
Project-URL: Issues, https://github.com/Organize-IT/odoo-cli/issues
|
|
@@ -53,14 +53,22 @@ export ODOO_URL=https://mycompany.odoo.com ODOO_DB=mycompany \
|
|
|
53
53
|
ODOO_LOGIN=bot@mycompany.com ODOO_API_KEY=... # API key or password
|
|
54
54
|
|
|
55
55
|
odoo info # version, uid, connection source
|
|
56
|
-
odoo
|
|
56
|
+
odoo alias # business names for models, and presets
|
|
57
|
+
odoo models --like invoice # or find the technical name yourself
|
|
57
58
|
odoo fields account.move --stored # what you can filter and order on
|
|
58
|
-
odoo count
|
|
59
|
-
odoo search
|
|
59
|
+
odoo count invoices -w overdue
|
|
60
|
+
odoo search invoices -w overdue \
|
|
60
61
|
--fields name,partner_id,amount_residual --order "invoice_date_due" --limit 20
|
|
62
|
+
odoo group invoices -w unpaid --by partner_id --sum amount_residual
|
|
61
63
|
```
|
|
62
64
|
|
|
63
|
-
|
|
65
|
+
`invoices` is `account.move` filtered on `move_type = out_invoice`, and `overdue` is
|
|
66
|
+
`payment_state = not_paid` past its due date. Both expand to an ordinary domain before
|
|
67
|
+
anything is sent, and the technical names keep working. Full documentation:
|
|
68
|
+
[Organize-IT.github.io/odoo-cli](https://github.com/Organize-IT/odoo-cli/tree/main/docs).
|
|
69
|
+
|
|
70
|
+
Prefer named connections? They live in a TOML file written owner-only where the platform
|
|
71
|
+
can enforce that — `odoo profile path --check` says whether it can:
|
|
64
72
|
|
|
65
73
|
```bash
|
|
66
74
|
odoo profile add acme --url https://acme.odoo.com --db acme --login bot@acme.com \
|
|
@@ -77,7 +85,10 @@ First match wins, and the CLI never prompts:
|
|
|
77
85
|
3. a profile named `default`
|
|
78
86
|
|
|
79
87
|
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
|
|
88
|
+
(`--api-key`) or points to an env var (`--api-key-env`). `odoo profile path` shows the file,
|
|
89
|
+
`--check` reports what its permissions are actually worth: mode 600 means owner-only on
|
|
90
|
+
POSIX and nothing at all on Windows, where `chmod` only toggles a read-only attribute. On
|
|
91
|
+
such a platform, prefer `--api-key-env` and keep the key in your secret manager.
|
|
81
92
|
|
|
82
93
|
## Output contract
|
|
83
94
|
|
|
@@ -89,6 +100,7 @@ Nothing found: exit code 3 with a message listing those three ways. A profile st
|
|
|
89
100
|
| bad arguments | | `{"error": ...}` | 2 |
|
|
90
101
|
| connection, auth, no profile | | `{"error": ...}` | 3 |
|
|
91
102
|
| refused by a guard | | `{"error": ...}` | 4 |
|
|
103
|
+
| query repaired to run | rows | `{"error": ...}` | 5 |
|
|
92
104
|
| write executed | result | `{"write": {"model", "method", "ids", "fields"}}` | 0 |
|
|
93
105
|
|
|
94
106
|
Data is never humanised: many2one fields stay `[id, "name"]`, empty values stay `false`.
|
|
@@ -102,6 +114,7 @@ Values of fields named like `password`, `api_key`, `secret` are `[redacted]` unl
|
|
|
102
114
|
| `--company ID`, `--lang CODE`, `--context JSON` | Odoo context merged into every call |
|
|
103
115
|
| `--insecure` | skip TLS verification (self-signed on-prem) |
|
|
104
116
|
| `--no-redact`, `--include-sensitive` | lift the two output/model guards |
|
|
117
|
+
| `--no-validate` | skip the field-name check against the model schema |
|
|
105
118
|
| `--debug` | one JSON line per RPC on stderr (method, id, duration, retries) |
|
|
106
119
|
| `--verbose` | include Odoo's server traceback in error output |
|
|
107
120
|
|
|
@@ -122,13 +135,76 @@ Values of fields named like `password`, `api_key`, `secret` are `[redacted]` unl
|
|
|
122
135
|
|
|
123
136
|
Values: `true/false/null`, integers, floats, JSON lists or objects, quoted strings, else text.
|
|
124
137
|
|
|
138
|
+
A bare word is looked up as a preset for the model: `-w overdue`, `-w unpaid`, `-w draft`,
|
|
139
|
+
`-w confirmed`, `-w archived`. `odoo alias MODEL --presets` lists the ones that apply. A bare
|
|
140
|
+
word that is not a preset exits 2 listing the ones that are.
|
|
141
|
+
|
|
142
|
+
## Your own names
|
|
143
|
+
|
|
144
|
+
The shipped table covers what most tenants call things. A profile file adds the rest, and a
|
|
145
|
+
user entry replaces a built-in of the same name — your tenant knows its vocabulary better than
|
|
146
|
+
this tool does:
|
|
147
|
+
|
|
148
|
+
```toml
|
|
149
|
+
[aliases.subscriptions]
|
|
150
|
+
model = "sale.subscription"
|
|
151
|
+
domain = [["stage_category", "=", "progress"]]
|
|
152
|
+
help = "Running subscriptions"
|
|
153
|
+
|
|
154
|
+
[aliases.invoices] # replaces the built-in
|
|
155
|
+
model = "account.move"
|
|
156
|
+
domain = [["move_type", "=", "out_invoice"], ["company_id", "=", 3]]
|
|
157
|
+
|
|
158
|
+
[presets.mine]
|
|
159
|
+
domain = [["user_id", "=", 7]]
|
|
160
|
+
models = ["crm.lead", "sale.order"]
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
`odoo alias` marks each entry `builtin` or `config`. A malformed table fails the command with
|
|
164
|
+
exit 2 instead of being skipped: a filter you believe is applied and is not is exactly what
|
|
165
|
+
this mechanism exists to avoid. Dynamic dates work too — `@today`, `@month-start`, `@year-start`.
|
|
166
|
+
|
|
167
|
+
## Names and typos
|
|
168
|
+
|
|
169
|
+
Read commands accept an alias in place of a technical model name (`invoices`, `customers`,
|
|
170
|
+
`quotes`, `pickings`, ...); `odoo alias` prints the table and needs no connection. Write
|
|
171
|
+
commands refuse an alias that carries a filter, so a vendor bill cannot be filed as a
|
|
172
|
+
customer invoice by accident.
|
|
173
|
+
|
|
174
|
+
Field names are checked against the model schema before the call, which costs nothing after
|
|
175
|
+
the first lookup:
|
|
176
|
+
|
|
177
|
+
```console
|
|
178
|
+
$ odoo search partners -w mobil=+32
|
|
179
|
+
{"error": {"code": "unknown_field", "message": "res.partner has no field 'mobil'. Did you mean 'mobile'? (also: phone)"}}
|
|
180
|
+
$ echo $?
|
|
181
|
+
2
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
`fields_get` is read once per model and cached under `~/.cache/odoo-cli` for 24 hours
|
|
185
|
+
(`odoo cache path|list|clear`, `ODOO_CACHE_DIR`, `ODOO_SCHEMA_TTL`). Dotted paths are
|
|
186
|
+
followed across relations. A computed non-stored field used in `-w` or `--order` warns on
|
|
187
|
+
stderr rather than failing. Validation only rejects a name it has positively read from the
|
|
188
|
+
server: when the schema is unreadable it stays quiet, so it can never refuse wrongly.
|
|
189
|
+
`--no-validate` or `ODOO_NO_VALIDATE=1` turns it off.
|
|
190
|
+
|
|
191
|
+
## Aggregates
|
|
192
|
+
|
|
193
|
+
```bash
|
|
194
|
+
odoo group invoices -w overdue --by partner_id --sum amount_residual \
|
|
195
|
+
--order "amount_residual desc" --limit 10
|
|
196
|
+
odoo group invoices --by invoice_date:month --sum amount_total
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
`read_group` under the hood: counts and totals per group without pulling the records.
|
|
200
|
+
|
|
125
201
|
## Writes
|
|
126
202
|
|
|
127
203
|
Off by default. Enable per connection with `allow_writes = true` on the profile
|
|
128
204
|
(`odoo profile add ... --allow-writes`) or `ODOO_ALLOW_WRITES=1`.
|
|
129
205
|
|
|
130
206
|
```bash
|
|
131
|
-
odoo create crm.lead -v name="Website inquiry" -v partner_id=42 --dry-run #
|
|
207
|
+
odoo create crm.lead -v name="Website inquiry" -v partner_id=42 --dry-run # payload only, exit 0
|
|
132
208
|
odoo create crm.lead -v name="Website inquiry" -v partner_id=42 # prints the new id
|
|
133
209
|
odoo write res.partner 42,43 -v active=false
|
|
134
210
|
odoo unlink res.partner 99 --yes # --yes required
|
|
@@ -146,8 +222,9 @@ are refused unless `--include-sensitive`.
|
|
|
146
222
|
many2one shapes) and recipes.
|
|
147
223
|
- The same text ships as an [Agent Skill](https://github.com/Organize-IT/odoo-cli/blob/main/SKILL.md):
|
|
148
224
|
`npx skills add Organize-IT/odoo-cli`.
|
|
149
|
-
- `odoo search ... --lenient-fields` removes fields Odoo rejects and retries
|
|
150
|
-
|
|
225
|
+
- `odoo search ... --lenient-fields` removes fields Odoo rejects and retries. It prints the
|
|
226
|
+
rows, then exits **5**: they answer a wider question than the one you asked. Exploration
|
|
227
|
+
stays comfortable; a script that checks its exit codes cannot be fooled by it.
|
|
151
228
|
|
|
152
229
|
## Library
|
|
153
230
|
|
|
@@ -190,8 +267,11 @@ repair loop in `odoocli.lenient`.
|
|
|
190
267
|
|
|
191
268
|
```bash
|
|
192
269
|
uv sync --group dev
|
|
270
|
+
uvx pre-commit install # ruff, mypy and hygiene hooks on commit
|
|
193
271
|
uv run pytest # unit tests, mocked JSON-RPC
|
|
194
272
|
uv run ruff check && uv run mypy
|
|
273
|
+
uv run pytest --cov --cov-report=term-missing # gated at 93% in CI
|
|
274
|
+
uv run --group docs mkdocs serve # the documentation site
|
|
195
275
|
|
|
196
276
|
ODOO_VERSION=17.0 scripts/start-odoo.sh # throwaway Odoo in Docker
|
|
197
277
|
ODOO_URL=http://localhost:8069 ODOO_DB=test ODOO_LOGIN=admin ODOO_API_KEY=admin \
|
|
@@ -199,9 +279,14 @@ ODOO_ALLOW_WRITES=1 uv run pytest -m integration -o addopts=""
|
|
|
199
279
|
docker compose -f docker/odoo-compose.yml down -v
|
|
200
280
|
```
|
|
201
281
|
|
|
202
|
-
CI runs the unit suite on
|
|
203
|
-
`main`, tags and manual dispatch. Releases are
|
|
204
|
-
trusted publishing
|
|
282
|
+
CI runs the unit suite on Python 3.11-3.13 and builds the docs on every PR; the integration
|
|
283
|
+
matrix (Odoo 17.0, 18.0, 19.0) runs on `main`, tags and manual dispatch. Releases are
|
|
284
|
+
published to PyPI on `v*` tags through trusted publishing, with every action pinned to a
|
|
285
|
+
commit digest.
|
|
286
|
+
|
|
287
|
+
`AGENTS.md` is the specification: layering, the contracts that may not change silently, and
|
|
288
|
+
the definition of done. [docs/decisions/](docs/decisions/) records the decisions with a real
|
|
289
|
+
trade-off behind them. Read both before changing anything.
|
|
205
290
|
|
|
206
291
|
## License
|
|
207
292
|
|
|
@@ -26,14 +26,22 @@ export ODOO_URL=https://mycompany.odoo.com ODOO_DB=mycompany \
|
|
|
26
26
|
ODOO_LOGIN=bot@mycompany.com ODOO_API_KEY=... # API key or password
|
|
27
27
|
|
|
28
28
|
odoo info # version, uid, connection source
|
|
29
|
-
odoo
|
|
29
|
+
odoo alias # business names for models, and presets
|
|
30
|
+
odoo models --like invoice # or find the technical name yourself
|
|
30
31
|
odoo fields account.move --stored # what you can filter and order on
|
|
31
|
-
odoo count
|
|
32
|
-
odoo search
|
|
32
|
+
odoo count invoices -w overdue
|
|
33
|
+
odoo search invoices -w overdue \
|
|
33
34
|
--fields name,partner_id,amount_residual --order "invoice_date_due" --limit 20
|
|
35
|
+
odoo group invoices -w unpaid --by partner_id --sum amount_residual
|
|
34
36
|
```
|
|
35
37
|
|
|
36
|
-
|
|
38
|
+
`invoices` is `account.move` filtered on `move_type = out_invoice`, and `overdue` is
|
|
39
|
+
`payment_state = not_paid` past its due date. Both expand to an ordinary domain before
|
|
40
|
+
anything is sent, and the technical names keep working. Full documentation:
|
|
41
|
+
[Organize-IT.github.io/odoo-cli](https://github.com/Organize-IT/odoo-cli/tree/main/docs).
|
|
42
|
+
|
|
43
|
+
Prefer named connections? They live in a TOML file written owner-only where the platform
|
|
44
|
+
can enforce that — `odoo profile path --check` says whether it can:
|
|
37
45
|
|
|
38
46
|
```bash
|
|
39
47
|
odoo profile add acme --url https://acme.odoo.com --db acme --login bot@acme.com \
|
|
@@ -50,7 +58,10 @@ First match wins, and the CLI never prompts:
|
|
|
50
58
|
3. a profile named `default`
|
|
51
59
|
|
|
52
60
|
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
|
|
61
|
+
(`--api-key`) or points to an env var (`--api-key-env`). `odoo profile path` shows the file,
|
|
62
|
+
`--check` reports what its permissions are actually worth: mode 600 means owner-only on
|
|
63
|
+
POSIX and nothing at all on Windows, where `chmod` only toggles a read-only attribute. On
|
|
64
|
+
such a platform, prefer `--api-key-env` and keep the key in your secret manager.
|
|
54
65
|
|
|
55
66
|
## Output contract
|
|
56
67
|
|
|
@@ -62,6 +73,7 @@ Nothing found: exit code 3 with a message listing those three ways. A profile st
|
|
|
62
73
|
| bad arguments | | `{"error": ...}` | 2 |
|
|
63
74
|
| connection, auth, no profile | | `{"error": ...}` | 3 |
|
|
64
75
|
| refused by a guard | | `{"error": ...}` | 4 |
|
|
76
|
+
| query repaired to run | rows | `{"error": ...}` | 5 |
|
|
65
77
|
| write executed | result | `{"write": {"model", "method", "ids", "fields"}}` | 0 |
|
|
66
78
|
|
|
67
79
|
Data is never humanised: many2one fields stay `[id, "name"]`, empty values stay `false`.
|
|
@@ -75,6 +87,7 @@ Values of fields named like `password`, `api_key`, `secret` are `[redacted]` unl
|
|
|
75
87
|
| `--company ID`, `--lang CODE`, `--context JSON` | Odoo context merged into every call |
|
|
76
88
|
| `--insecure` | skip TLS verification (self-signed on-prem) |
|
|
77
89
|
| `--no-redact`, `--include-sensitive` | lift the two output/model guards |
|
|
90
|
+
| `--no-validate` | skip the field-name check against the model schema |
|
|
78
91
|
| `--debug` | one JSON line per RPC on stderr (method, id, duration, retries) |
|
|
79
92
|
| `--verbose` | include Odoo's server traceback in error output |
|
|
80
93
|
|
|
@@ -95,13 +108,76 @@ Values of fields named like `password`, `api_key`, `secret` are `[redacted]` unl
|
|
|
95
108
|
|
|
96
109
|
Values: `true/false/null`, integers, floats, JSON lists or objects, quoted strings, else text.
|
|
97
110
|
|
|
111
|
+
A bare word is looked up as a preset for the model: `-w overdue`, `-w unpaid`, `-w draft`,
|
|
112
|
+
`-w confirmed`, `-w archived`. `odoo alias MODEL --presets` lists the ones that apply. A bare
|
|
113
|
+
word that is not a preset exits 2 listing the ones that are.
|
|
114
|
+
|
|
115
|
+
## Your own names
|
|
116
|
+
|
|
117
|
+
The shipped table covers what most tenants call things. A profile file adds the rest, and a
|
|
118
|
+
user entry replaces a built-in of the same name — your tenant knows its vocabulary better than
|
|
119
|
+
this tool does:
|
|
120
|
+
|
|
121
|
+
```toml
|
|
122
|
+
[aliases.subscriptions]
|
|
123
|
+
model = "sale.subscription"
|
|
124
|
+
domain = [["stage_category", "=", "progress"]]
|
|
125
|
+
help = "Running subscriptions"
|
|
126
|
+
|
|
127
|
+
[aliases.invoices] # replaces the built-in
|
|
128
|
+
model = "account.move"
|
|
129
|
+
domain = [["move_type", "=", "out_invoice"], ["company_id", "=", 3]]
|
|
130
|
+
|
|
131
|
+
[presets.mine]
|
|
132
|
+
domain = [["user_id", "=", 7]]
|
|
133
|
+
models = ["crm.lead", "sale.order"]
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
`odoo alias` marks each entry `builtin` or `config`. A malformed table fails the command with
|
|
137
|
+
exit 2 instead of being skipped: a filter you believe is applied and is not is exactly what
|
|
138
|
+
this mechanism exists to avoid. Dynamic dates work too — `@today`, `@month-start`, `@year-start`.
|
|
139
|
+
|
|
140
|
+
## Names and typos
|
|
141
|
+
|
|
142
|
+
Read commands accept an alias in place of a technical model name (`invoices`, `customers`,
|
|
143
|
+
`quotes`, `pickings`, ...); `odoo alias` prints the table and needs no connection. Write
|
|
144
|
+
commands refuse an alias that carries a filter, so a vendor bill cannot be filed as a
|
|
145
|
+
customer invoice by accident.
|
|
146
|
+
|
|
147
|
+
Field names are checked against the model schema before the call, which costs nothing after
|
|
148
|
+
the first lookup:
|
|
149
|
+
|
|
150
|
+
```console
|
|
151
|
+
$ odoo search partners -w mobil=+32
|
|
152
|
+
{"error": {"code": "unknown_field", "message": "res.partner has no field 'mobil'. Did you mean 'mobile'? (also: phone)"}}
|
|
153
|
+
$ echo $?
|
|
154
|
+
2
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
`fields_get` is read once per model and cached under `~/.cache/odoo-cli` for 24 hours
|
|
158
|
+
(`odoo cache path|list|clear`, `ODOO_CACHE_DIR`, `ODOO_SCHEMA_TTL`). Dotted paths are
|
|
159
|
+
followed across relations. A computed non-stored field used in `-w` or `--order` warns on
|
|
160
|
+
stderr rather than failing. Validation only rejects a name it has positively read from the
|
|
161
|
+
server: when the schema is unreadable it stays quiet, so it can never refuse wrongly.
|
|
162
|
+
`--no-validate` or `ODOO_NO_VALIDATE=1` turns it off.
|
|
163
|
+
|
|
164
|
+
## Aggregates
|
|
165
|
+
|
|
166
|
+
```bash
|
|
167
|
+
odoo group invoices -w overdue --by partner_id --sum amount_residual \
|
|
168
|
+
--order "amount_residual desc" --limit 10
|
|
169
|
+
odoo group invoices --by invoice_date:month --sum amount_total
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
`read_group` under the hood: counts and totals per group without pulling the records.
|
|
173
|
+
|
|
98
174
|
## Writes
|
|
99
175
|
|
|
100
176
|
Off by default. Enable per connection with `allow_writes = true` on the profile
|
|
101
177
|
(`odoo profile add ... --allow-writes`) or `ODOO_ALLOW_WRITES=1`.
|
|
102
178
|
|
|
103
179
|
```bash
|
|
104
|
-
odoo create crm.lead -v name="Website inquiry" -v partner_id=42 --dry-run #
|
|
180
|
+
odoo create crm.lead -v name="Website inquiry" -v partner_id=42 --dry-run # payload only, exit 0
|
|
105
181
|
odoo create crm.lead -v name="Website inquiry" -v partner_id=42 # prints the new id
|
|
106
182
|
odoo write res.partner 42,43 -v active=false
|
|
107
183
|
odoo unlink res.partner 99 --yes # --yes required
|
|
@@ -119,8 +195,9 @@ are refused unless `--include-sensitive`.
|
|
|
119
195
|
many2one shapes) and recipes.
|
|
120
196
|
- The same text ships as an [Agent Skill](https://github.com/Organize-IT/odoo-cli/blob/main/SKILL.md):
|
|
121
197
|
`npx skills add Organize-IT/odoo-cli`.
|
|
122
|
-
- `odoo search ... --lenient-fields` removes fields Odoo rejects and retries
|
|
123
|
-
|
|
198
|
+
- `odoo search ... --lenient-fields` removes fields Odoo rejects and retries. It prints the
|
|
199
|
+
rows, then exits **5**: they answer a wider question than the one you asked. Exploration
|
|
200
|
+
stays comfortable; a script that checks its exit codes cannot be fooled by it.
|
|
124
201
|
|
|
125
202
|
## Library
|
|
126
203
|
|
|
@@ -163,8 +240,11 @@ repair loop in `odoocli.lenient`.
|
|
|
163
240
|
|
|
164
241
|
```bash
|
|
165
242
|
uv sync --group dev
|
|
243
|
+
uvx pre-commit install # ruff, mypy and hygiene hooks on commit
|
|
166
244
|
uv run pytest # unit tests, mocked JSON-RPC
|
|
167
245
|
uv run ruff check && uv run mypy
|
|
246
|
+
uv run pytest --cov --cov-report=term-missing # gated at 93% in CI
|
|
247
|
+
uv run --group docs mkdocs serve # the documentation site
|
|
168
248
|
|
|
169
249
|
ODOO_VERSION=17.0 scripts/start-odoo.sh # throwaway Odoo in Docker
|
|
170
250
|
ODOO_URL=http://localhost:8069 ODOO_DB=test ODOO_LOGIN=admin ODOO_API_KEY=admin \
|
|
@@ -172,9 +252,14 @@ ODOO_ALLOW_WRITES=1 uv run pytest -m integration -o addopts=""
|
|
|
172
252
|
docker compose -f docker/odoo-compose.yml down -v
|
|
173
253
|
```
|
|
174
254
|
|
|
175
|
-
CI runs the unit suite on
|
|
176
|
-
`main`, tags and manual dispatch. Releases are
|
|
177
|
-
trusted publishing
|
|
255
|
+
CI runs the unit suite on Python 3.11-3.13 and builds the docs on every PR; the integration
|
|
256
|
+
matrix (Odoo 17.0, 18.0, 19.0) runs on `main`, tags and manual dispatch. Releases are
|
|
257
|
+
published to PyPI on `v*` tags through trusted publishing, with every action pinned to a
|
|
258
|
+
commit digest.
|
|
259
|
+
|
|
260
|
+
`AGENTS.md` is the specification: layering, the contracts that may not change silently, and
|
|
261
|
+
the definition of done. [docs/decisions/](docs/decisions/) records the decisions with a real
|
|
262
|
+
trade-off behind them. Read both before changing anything.
|
|
178
263
|
|
|
179
264
|
## License
|
|
180
265
|
|
|
@@ -18,6 +18,8 @@ Resolution order, first match wins:
|
|
|
18
18
|
3. A profile named `default`
|
|
19
19
|
|
|
20
20
|
Nothing resolved: exit code 3 and a message listing these three ways. The CLI never prompts.
|
|
21
|
+
`odoo profile path --check` says whether the stored key is really owner-only on this
|
|
22
|
+
platform; on Windows it is not, so prefer `--api-key-env` there.
|
|
21
23
|
`ODOO_API_KEY` accepts an Odoo API key (preferred) or the user's password.
|
|
22
24
|
Check a connection with `odoo info`. Self-signed on-prem server: `--insecure`
|
|
23
25
|
(or `odoo profile add ... --no-verify-ssl`).
|
|
@@ -36,6 +38,52 @@ work on every command:
|
|
|
36
38
|
--context '{"tz": "Europe/Brussels"}' any other key, merged with the flags above
|
|
37
39
|
```
|
|
38
40
|
|
|
41
|
+
## Business names instead of technical ones
|
|
42
|
+
|
|
43
|
+
Read commands accept an alias in place of a model name, and a preset name in place of a
|
|
44
|
+
condition. Both expand to an ordinary domain before anything is sent, so the result has
|
|
45
|
+
exactly the same shape.
|
|
46
|
+
|
|
47
|
+
```
|
|
48
|
+
odoo alias every alias, its model and its filter (no connection needed)
|
|
49
|
+
odoo alias invoices --presets the presets that apply to that model
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
```
|
|
53
|
+
odoo search invoices -w overdue same as
|
|
54
|
+
odoo search account.move -w move_type=out_invoice -w payment_state=not_paid \
|
|
55
|
+
-w invoice_date_due<TODAY
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
Aliases include `partners`, `customers`, `suppliers`, `invoices`, `bills`, `credit-notes`,
|
|
59
|
+
`orders`, `quotes`, `opportunities`, `products`, `pickings`, `users`, `employees`, `tasks`.
|
|
60
|
+
Presets include `overdue`, `unpaid`, `paid`, `draft`, `posted`, `cancelled`, `confirmed`,
|
|
61
|
+
`this-month`, `this-year`, `ready`, `done`, and `archived` / `active` on any model.
|
|
62
|
+
|
|
63
|
+
Write commands refuse an alias that carries a filter (exit 2, `alias_not_writable`): use the
|
|
64
|
+
technical name and set the discriminator field yourself, so a vendor bill cannot be filed as
|
|
65
|
+
a customer invoice by accident.
|
|
66
|
+
|
|
67
|
+
## Field names are checked before the call
|
|
68
|
+
|
|
69
|
+
A misspelled field costs no round trip. The CLI reads the model schema once with
|
|
70
|
+
`fields_get`, caches it for 24 hours, and checks every name used in `-w`, `--fields`,
|
|
71
|
+
`--order`, `-v` and `--by`:
|
|
72
|
+
|
|
73
|
+
```
|
|
74
|
+
$ odoo search partners -w mobil=+32
|
|
75
|
+
{"error": {"code": "unknown_field", "message": "res.partner has no field 'mobil'.
|
|
76
|
+
Did you mean 'mobile'? (also: phone)"}} exit 2, nothing was sent
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
Dotted paths are followed across relations, and the error names the model where the path
|
|
80
|
+
actually broke. Using a computed non-stored field in `-w` or `--order` produces a
|
|
81
|
+
`field_not_stored` warning on stderr and the call still goes out.
|
|
82
|
+
|
|
83
|
+
Validation only ever rejects a name it has positively read from the server: if the schema
|
|
84
|
+
cannot be read, it stays quiet. Turn it off with `--no-validate` or `ODOO_NO_VALIDATE=1`.
|
|
85
|
+
Refresh it after a module install or an Odoo upgrade with `odoo cache clear`.
|
|
86
|
+
|
|
39
87
|
## Output contract
|
|
40
88
|
|
|
41
89
|
- Piped or captured: raw Odoo JSON, exactly what `search_read`, `read` or `fields_get` return.
|
|
@@ -45,7 +93,8 @@ work on every command:
|
|
|
45
93
|
- Errors: one JSON object on stderr, `{"error": {"code": ..., "message": ..., "odoo": {...}}}`.
|
|
46
94
|
- Warnings and write logs: one JSON object per line on stderr.
|
|
47
95
|
- 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)
|
|
96
|
+
`4` refused by a guard (writes disabled, missing `--yes`, sensitive model), `5` the query
|
|
97
|
+
was repaired to make it run, so the rows answer a wider question than the one you asked.
|
|
49
98
|
- Values of fields named like `password`, `api_key`, `secret` are replaced by `[redacted]`
|
|
50
99
|
unless `--no-redact`.
|
|
51
100
|
|
|
@@ -59,8 +108,16 @@ odoo search MODEL [-w COND]... [--domain JSON] [--fields a,b] [--limit N] [--off
|
|
|
59
108
|
[--order "x desc"] [--all] [--ids-only] [--lenient-fields]
|
|
60
109
|
odoo count MODEL [-w COND]... [--domain JSON]
|
|
61
110
|
odoo read MODEL ID [ID...] [--fields a,b]
|
|
111
|
+
odoo group MODEL --by FIELD[,FIELD] [--sum a,b] [--avg a,b] [-w COND]... [--limit N]
|
|
112
|
+
odoo alias [NAME] [--presets] aliases and presets, offline
|
|
113
|
+
odoo cache path|list|clear the schema cache
|
|
62
114
|
```
|
|
63
115
|
|
|
116
|
+
`MODEL` accepts an alias on every read command. `odoo group` runs `read_group`, so you
|
|
117
|
+
get counts and totals per group without pulling the records:
|
|
118
|
+
`odoo group invoices -w overdue --by partner_id --sum amount_residual`.
|
|
119
|
+
Grouping keys accept a date granularity: `--by invoice_date:month`.
|
|
120
|
+
|
|
64
121
|
Conditions (`-w`, repeatable, AND-ed together, combined with `--domain`):
|
|
65
122
|
|
|
66
123
|
```
|
|
@@ -84,7 +141,8 @@ odoo unlink MODEL IDS --yes [--dry-run]
|
|
|
84
141
|
odoo call MODEL METHOD [--ids 1,2] [--args JSON] [--kwargs JSON] [--yes] [--dry-run]
|
|
85
142
|
```
|
|
86
143
|
|
|
87
|
-
- `--dry-run` prints the exact payload and exits 0 without
|
|
144
|
+
- `--dry-run` prints the exact payload and exits 0 without writing anything. It does read
|
|
145
|
+
the model schema to check your field names, so a typo is caught there too. Use it first.
|
|
88
146
|
- `unlink` and any `call` to a non read-only method need `--yes` (or `ODOO_ASSUME_YES=1`).
|
|
89
147
|
- `call` on read-only methods (`name_search`, `read_group`, `default_get`, ...) needs
|
|
90
148
|
neither `allow_writes` nor `--yes`.
|
|
@@ -103,8 +161,9 @@ One2many and many2many fields take Odoo commands, written as JSON in `-v` or `--
|
|
|
103
161
|
|
|
104
162
|
## Working method that avoids most failures
|
|
105
163
|
|
|
106
|
-
1. Unknown model? `odoo
|
|
107
|
-
and `selection`
|
|
164
|
+
1. Unknown model? Try `odoo alias` first, then `odoo models --like word`. Then
|
|
165
|
+
`odoo fields MODEL`, which shows `type`, `required`, `store`, `relation` and `selection`
|
|
166
|
+
values.
|
|
108
167
|
2. Only filter or order on fields with `store: true`. Computed non-stored fields
|
|
109
168
|
(`qty_available`, `amount_to_invoice`, ...) can be read but not searched; Odoo answers
|
|
110
169
|
"Cannot convert ... to SQL". Read them and filter client-side.
|
|
@@ -117,9 +176,11 @@ One2many and many2many fields take Odoo commands, written as JSON in `-v` or `--
|
|
|
117
176
|
6. Dates are strings, `YYYY-MM-DD` or `YYYY-MM-DD HH:MM:SS` in UTC.
|
|
118
177
|
7. Field names drift between Odoo 17, 18 and 19 (for example `account.account.company_id`
|
|
119
178
|
became `company_ids`). If a field is rejected, `odoo fields` is the truth.
|
|
120
|
-
`--lenient-fields` on `search` removes rejected fields and retries
|
|
121
|
-
|
|
122
|
-
|
|
179
|
+
`--lenient-fields` on `search` removes rejected fields and retries. It prints the rows and
|
|
180
|
+
then exits 5, because they answer a wider question than the one you asked. Use it to
|
|
181
|
+
explore; if you keep it in a script, check the exit code.
|
|
182
|
+
8. Never guess a model name: `odoo alias`, then `odoo models --like invoice`. A dotless
|
|
183
|
+
name close to a known alias is reported as a typo instead of being sent.
|
|
123
184
|
9. Sensitive models (`ir.config_parameter`, `ir.mail_server`, `res.users.apikeys`, `ir.cron`,
|
|
124
185
|
`ir.actions.server`, ...) are refused unless `--include-sensitive`.
|
|
125
186
|
10. A record you know exists but cannot find is usually archived (`--include-archived`) or in
|
|
@@ -131,6 +192,10 @@ One2many and many2many fields take Odoo commands, written as JSON in `-v` or `--
|
|
|
131
192
|
## Recipes
|
|
132
193
|
|
|
133
194
|
```
|
|
195
|
+
odoo search customers -w country_id.code=BE --fields name,email,vat --limit 20
|
|
196
|
+
odoo count invoices -w overdue
|
|
197
|
+
odoo group invoices -w overdue --by partner_id --sum amount_residual --order "amount_residual desc" --limit 10
|
|
198
|
+
odoo group invoices --by invoice_date:month --sum amount_total
|
|
134
199
|
odoo search res.partner -w is_company=true -w country_id.code=BE --fields name,email,vat --limit 20
|
|
135
200
|
odoo search sale.order -w state=sale -w date_order>=2026-01-01 --fields name,partner_id,amount_total --order "amount_total desc"
|
|
136
201
|
odoo count account.move -w move_type=out_invoice -w payment_state=not_paid -w invoice_date_due<2026-09-01
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "odoo-agent-cli"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.5.0"
|
|
8
8
|
description = "Odoo JSON-RPC CLI and Python client built for AI agents and scripts."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = "MIT"
|
|
@@ -44,6 +44,13 @@ dev = [
|
|
|
44
44
|
"respx>=0.21",
|
|
45
45
|
"ruff>=0.6",
|
|
46
46
|
"mypy>=1.11",
|
|
47
|
+
"pytest-cov>=5",
|
|
48
|
+
"hypothesis>=6.168.0",
|
|
49
|
+
]
|
|
50
|
+
docs = [
|
|
51
|
+
"mkdocs>=1.6",
|
|
52
|
+
"mkdocs-material>=9.5",
|
|
53
|
+
"mkdocstrings[python]>=0.26",
|
|
47
54
|
]
|
|
48
55
|
|
|
49
56
|
[tool.hatch.build.targets.wheel]
|
|
@@ -76,6 +83,15 @@ warn_unreachable = true
|
|
|
76
83
|
module = ["respx", "respx.*"]
|
|
77
84
|
ignore_missing_imports = true
|
|
78
85
|
|
|
86
|
+
[tool.coverage.run]
|
|
87
|
+
source = ["odoocli"]
|
|
88
|
+
branch = true
|
|
89
|
+
|
|
90
|
+
[tool.coverage.report]
|
|
91
|
+
show_missing = true
|
|
92
|
+
fail_under = 93
|
|
93
|
+
exclude_also = ["if TYPE_CHECKING:", "raise AssertionError\\(\"unreachable\"\\)"]
|
|
94
|
+
|
|
79
95
|
[tool.pytest.ini_options]
|
|
80
96
|
asyncio_mode = "auto"
|
|
81
97
|
testpaths = ["tests"]
|