odoo-agent-cli 0.2.0__tar.gz → 0.4.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 → odoo_agent_cli-0.4.0}/.gitignore +3 -0
- {odoo_agent_cli-0.2.0 → odoo_agent_cli-0.4.0}/PKG-INFO +61 -8
- {odoo_agent_cli-0.2.0 → odoo_agent_cli-0.4.0}/README.md +60 -7
- {odoo_agent_cli-0.2.0 → odoo_agent_cli-0.4.0}/SKILL.md +65 -4
- {odoo_agent_cli-0.2.0 → odoo_agent_cli-0.4.0}/pyproject.toml +16 -1
- {odoo_agent_cli-0.2.0 → odoo_agent_cli-0.4.0}/src/odoocli/AGENT_GUIDE.md +65 -4
- {odoo_agent_cli-0.2.0 → odoo_agent_cli-0.4.0}/src/odoocli/_version.py +1 -1
- odoo_agent_cli-0.4.0/src/odoocli/aliases.py +199 -0
- odoo_agent_cli-0.4.0/src/odoocli/cli/alias_cmd.py +41 -0
- {odoo_agent_cli-0.2.0 → odoo_agent_cli-0.4.0}/src/odoocli/cli/app.py +151 -5
- odoo_agent_cli-0.4.0/src/odoocli/cli/cache_cmds.py +72 -0
- {odoo_agent_cli-0.2.0 → odoo_agent_cli-0.4.0}/src/odoocli/cli/read_cmds.py +101 -19
- {odoo_agent_cli-0.2.0 → odoo_agent_cli-0.4.0}/src/odoocli/cli/write_cmds.py +33 -21
- {odoo_agent_cli-0.2.0 → odoo_agent_cli-0.4.0}/src/odoocli/domain.py +58 -7
- odoo_agent_cli-0.4.0/src/odoocli/schema.py +362 -0
- {odoo_agent_cli-0.2.0 → odoo_agent_cli-0.4.0}/LICENSE +0 -0
- {odoo_agent_cli-0.2.0 → odoo_agent_cli-0.4.0}/src/odoocli/__init__.py +0 -0
- {odoo_agent_cli-0.2.0 → odoo_agent_cli-0.4.0}/src/odoocli/__main__.py +0 -0
- {odoo_agent_cli-0.2.0 → odoo_agent_cli-0.4.0}/src/odoocli/cli/__init__.py +0 -0
- {odoo_agent_cli-0.2.0 → odoo_agent_cli-0.4.0}/src/odoocli/cli/guide_cmd.py +0 -0
- {odoo_agent_cli-0.2.0 → odoo_agent_cli-0.4.0}/src/odoocli/cli/output.py +0 -0
- {odoo_agent_cli-0.2.0 → odoo_agent_cli-0.4.0}/src/odoocli/cli/profile_cmds.py +0 -0
- {odoo_agent_cli-0.2.0 → odoo_agent_cli-0.4.0}/src/odoocli/cli/values.py +0 -0
- {odoo_agent_cli-0.2.0 → odoo_agent_cli-0.4.0}/src/odoocli/client.py +0 -0
- {odoo_agent_cli-0.2.0 → odoo_agent_cli-0.4.0}/src/odoocli/config.py +0 -0
- {odoo_agent_cli-0.2.0 → odoo_agent_cli-0.4.0}/src/odoocli/errors.py +0 -0
- {odoo_agent_cli-0.2.0 → odoo_agent_cli-0.4.0}/src/odoocli/lenient.py +0 -0
- {odoo_agent_cli-0.2.0 → odoo_agent_cli-0.4.0}/src/odoocli/py.typed +0 -0
- {odoo_agent_cli-0.2.0 → odoo_agent_cli-0.4.0}/src/odoocli/security.py +0 -0
- {odoo_agent_cli-0.2.0 → odoo_agent_cli-0.4.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.4.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,13 +53,20 @@ 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
|
|
|
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
|
+
|
|
63
70
|
Prefer named connections? They live in a `0600` TOML file:
|
|
64
71
|
|
|
65
72
|
```bash
|
|
@@ -102,6 +109,7 @@ Values of fields named like `password`, `api_key`, `secret` are `[redacted]` unl
|
|
|
102
109
|
| `--company ID`, `--lang CODE`, `--context JSON` | Odoo context merged into every call |
|
|
103
110
|
| `--insecure` | skip TLS verification (self-signed on-prem) |
|
|
104
111
|
| `--no-redact`, `--include-sensitive` | lift the two output/model guards |
|
|
112
|
+
| `--no-validate` | skip the field-name check against the model schema |
|
|
105
113
|
| `--debug` | one JSON line per RPC on stderr (method, id, duration, retries) |
|
|
106
114
|
| `--verbose` | include Odoo's server traceback in error output |
|
|
107
115
|
|
|
@@ -122,13 +130,51 @@ Values of fields named like `password`, `api_key`, `secret` are `[redacted]` unl
|
|
|
122
130
|
|
|
123
131
|
Values: `true/false/null`, integers, floats, JSON lists or objects, quoted strings, else text.
|
|
124
132
|
|
|
133
|
+
A bare word is looked up as a preset for the model: `-w overdue`, `-w unpaid`, `-w draft`,
|
|
134
|
+
`-w confirmed`, `-w archived`. `odoo alias MODEL --presets` lists the ones that apply. A bare
|
|
135
|
+
word that is not a preset exits 2 listing the ones that are.
|
|
136
|
+
|
|
137
|
+
## Names and typos
|
|
138
|
+
|
|
139
|
+
Read commands accept an alias in place of a technical model name (`invoices`, `customers`,
|
|
140
|
+
`quotes`, `pickings`, ...); `odoo alias` prints the table and needs no connection. Write
|
|
141
|
+
commands refuse an alias that carries a filter, so a vendor bill cannot be filed as a
|
|
142
|
+
customer invoice by accident.
|
|
143
|
+
|
|
144
|
+
Field names are checked against the model schema before the call, which costs nothing after
|
|
145
|
+
the first lookup:
|
|
146
|
+
|
|
147
|
+
```console
|
|
148
|
+
$ odoo search partners -w mobil=+32
|
|
149
|
+
{"error": {"code": "unknown_field", "message": "res.partner has no field 'mobil'. Did you mean 'mobile'? (also: phone)"}}
|
|
150
|
+
$ echo $?
|
|
151
|
+
2
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
`fields_get` is read once per model and cached under `~/.cache/odoo-cli` for 24 hours
|
|
155
|
+
(`odoo cache path|list|clear`, `ODOO_CACHE_DIR`, `ODOO_SCHEMA_TTL`). Dotted paths are
|
|
156
|
+
followed across relations. A computed non-stored field used in `-w` or `--order` warns on
|
|
157
|
+
stderr rather than failing. Validation only rejects a name it has positively read from the
|
|
158
|
+
server: when the schema is unreadable it stays quiet, so it can never refuse wrongly.
|
|
159
|
+
`--no-validate` or `ODOO_NO_VALIDATE=1` turns it off.
|
|
160
|
+
|
|
161
|
+
## Aggregates
|
|
162
|
+
|
|
163
|
+
```bash
|
|
164
|
+
odoo group invoices -w overdue --by partner_id --sum amount_residual \
|
|
165
|
+
--order "amount_residual desc" --limit 10
|
|
166
|
+
odoo group invoices --by invoice_date:month --sum amount_total
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
`read_group` under the hood: counts and totals per group without pulling the records.
|
|
170
|
+
|
|
125
171
|
## Writes
|
|
126
172
|
|
|
127
173
|
Off by default. Enable per connection with `allow_writes = true` on the profile
|
|
128
174
|
(`odoo profile add ... --allow-writes`) or `ODOO_ALLOW_WRITES=1`.
|
|
129
175
|
|
|
130
176
|
```bash
|
|
131
|
-
odoo create crm.lead -v name="Website inquiry" -v partner_id=42 --dry-run #
|
|
177
|
+
odoo create crm.lead -v name="Website inquiry" -v partner_id=42 --dry-run # payload only, exit 0
|
|
132
178
|
odoo create crm.lead -v name="Website inquiry" -v partner_id=42 # prints the new id
|
|
133
179
|
odoo write res.partner 42,43 -v active=false
|
|
134
180
|
odoo unlink res.partner 99 --yes # --yes required
|
|
@@ -190,8 +236,11 @@ repair loop in `odoocli.lenient`.
|
|
|
190
236
|
|
|
191
237
|
```bash
|
|
192
238
|
uv sync --group dev
|
|
239
|
+
uvx pre-commit install # ruff, mypy and hygiene hooks on commit
|
|
193
240
|
uv run pytest # unit tests, mocked JSON-RPC
|
|
194
241
|
uv run ruff check && uv run mypy
|
|
242
|
+
uv run pytest --cov --cov-report=term-missing # gated at 93% in CI
|
|
243
|
+
uv run --group docs mkdocs serve # the documentation site
|
|
195
244
|
|
|
196
245
|
ODOO_VERSION=17.0 scripts/start-odoo.sh # throwaway Odoo in Docker
|
|
197
246
|
ODOO_URL=http://localhost:8069 ODOO_DB=test ODOO_LOGIN=admin ODOO_API_KEY=admin \
|
|
@@ -199,9 +248,13 @@ ODOO_ALLOW_WRITES=1 uv run pytest -m integration -o addopts=""
|
|
|
199
248
|
docker compose -f docker/odoo-compose.yml down -v
|
|
200
249
|
```
|
|
201
250
|
|
|
202
|
-
CI runs the unit suite on
|
|
203
|
-
`main`, tags and manual dispatch. Releases are
|
|
204
|
-
trusted publishing
|
|
251
|
+
CI runs the unit suite on Python 3.11-3.13 and builds the docs on every PR; the integration
|
|
252
|
+
matrix (Odoo 17.0, 18.0, 19.0) runs on `main`, tags and manual dispatch. Releases are
|
|
253
|
+
published to PyPI on `v*` tags through trusted publishing, with every action pinned to a
|
|
254
|
+
commit digest.
|
|
255
|
+
|
|
256
|
+
`AGENTS.md` is the specification: layering, the contracts that may not change silently, and
|
|
257
|
+
the definition of done. Read it before changing anything.
|
|
205
258
|
|
|
206
259
|
## License
|
|
207
260
|
|
|
@@ -26,13 +26,20 @@ 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
|
|
|
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
|
+
|
|
36
43
|
Prefer named connections? They live in a `0600` TOML file:
|
|
37
44
|
|
|
38
45
|
```bash
|
|
@@ -75,6 +82,7 @@ Values of fields named like `password`, `api_key`, `secret` are `[redacted]` unl
|
|
|
75
82
|
| `--company ID`, `--lang CODE`, `--context JSON` | Odoo context merged into every call |
|
|
76
83
|
| `--insecure` | skip TLS verification (self-signed on-prem) |
|
|
77
84
|
| `--no-redact`, `--include-sensitive` | lift the two output/model guards |
|
|
85
|
+
| `--no-validate` | skip the field-name check against the model schema |
|
|
78
86
|
| `--debug` | one JSON line per RPC on stderr (method, id, duration, retries) |
|
|
79
87
|
| `--verbose` | include Odoo's server traceback in error output |
|
|
80
88
|
|
|
@@ -95,13 +103,51 @@ Values of fields named like `password`, `api_key`, `secret` are `[redacted]` unl
|
|
|
95
103
|
|
|
96
104
|
Values: `true/false/null`, integers, floats, JSON lists or objects, quoted strings, else text.
|
|
97
105
|
|
|
106
|
+
A bare word is looked up as a preset for the model: `-w overdue`, `-w unpaid`, `-w draft`,
|
|
107
|
+
`-w confirmed`, `-w archived`. `odoo alias MODEL --presets` lists the ones that apply. A bare
|
|
108
|
+
word that is not a preset exits 2 listing the ones that are.
|
|
109
|
+
|
|
110
|
+
## Names and typos
|
|
111
|
+
|
|
112
|
+
Read commands accept an alias in place of a technical model name (`invoices`, `customers`,
|
|
113
|
+
`quotes`, `pickings`, ...); `odoo alias` prints the table and needs no connection. Write
|
|
114
|
+
commands refuse an alias that carries a filter, so a vendor bill cannot be filed as a
|
|
115
|
+
customer invoice by accident.
|
|
116
|
+
|
|
117
|
+
Field names are checked against the model schema before the call, which costs nothing after
|
|
118
|
+
the first lookup:
|
|
119
|
+
|
|
120
|
+
```console
|
|
121
|
+
$ odoo search partners -w mobil=+32
|
|
122
|
+
{"error": {"code": "unknown_field", "message": "res.partner has no field 'mobil'. Did you mean 'mobile'? (also: phone)"}}
|
|
123
|
+
$ echo $?
|
|
124
|
+
2
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
`fields_get` is read once per model and cached under `~/.cache/odoo-cli` for 24 hours
|
|
128
|
+
(`odoo cache path|list|clear`, `ODOO_CACHE_DIR`, `ODOO_SCHEMA_TTL`). Dotted paths are
|
|
129
|
+
followed across relations. A computed non-stored field used in `-w` or `--order` warns on
|
|
130
|
+
stderr rather than failing. Validation only rejects a name it has positively read from the
|
|
131
|
+
server: when the schema is unreadable it stays quiet, so it can never refuse wrongly.
|
|
132
|
+
`--no-validate` or `ODOO_NO_VALIDATE=1` turns it off.
|
|
133
|
+
|
|
134
|
+
## Aggregates
|
|
135
|
+
|
|
136
|
+
```bash
|
|
137
|
+
odoo group invoices -w overdue --by partner_id --sum amount_residual \
|
|
138
|
+
--order "amount_residual desc" --limit 10
|
|
139
|
+
odoo group invoices --by invoice_date:month --sum amount_total
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
`read_group` under the hood: counts and totals per group without pulling the records.
|
|
143
|
+
|
|
98
144
|
## Writes
|
|
99
145
|
|
|
100
146
|
Off by default. Enable per connection with `allow_writes = true` on the profile
|
|
101
147
|
(`odoo profile add ... --allow-writes`) or `ODOO_ALLOW_WRITES=1`.
|
|
102
148
|
|
|
103
149
|
```bash
|
|
104
|
-
odoo create crm.lead -v name="Website inquiry" -v partner_id=42 --dry-run #
|
|
150
|
+
odoo create crm.lead -v name="Website inquiry" -v partner_id=42 --dry-run # payload only, exit 0
|
|
105
151
|
odoo create crm.lead -v name="Website inquiry" -v partner_id=42 # prints the new id
|
|
106
152
|
odoo write res.partner 42,43 -v active=false
|
|
107
153
|
odoo unlink res.partner 99 --yes # --yes required
|
|
@@ -163,8 +209,11 @@ repair loop in `odoocli.lenient`.
|
|
|
163
209
|
|
|
164
210
|
```bash
|
|
165
211
|
uv sync --group dev
|
|
212
|
+
uvx pre-commit install # ruff, mypy and hygiene hooks on commit
|
|
166
213
|
uv run pytest # unit tests, mocked JSON-RPC
|
|
167
214
|
uv run ruff check && uv run mypy
|
|
215
|
+
uv run pytest --cov --cov-report=term-missing # gated at 93% in CI
|
|
216
|
+
uv run --group docs mkdocs serve # the documentation site
|
|
168
217
|
|
|
169
218
|
ODOO_VERSION=17.0 scripts/start-odoo.sh # throwaway Odoo in Docker
|
|
170
219
|
ODOO_URL=http://localhost:8069 ODOO_DB=test ODOO_LOGIN=admin ODOO_API_KEY=admin \
|
|
@@ -172,9 +221,13 @@ ODOO_ALLOW_WRITES=1 uv run pytest -m integration -o addopts=""
|
|
|
172
221
|
docker compose -f docker/odoo-compose.yml down -v
|
|
173
222
|
```
|
|
174
223
|
|
|
175
|
-
CI runs the unit suite on
|
|
176
|
-
`main`, tags and manual dispatch. Releases are
|
|
177
|
-
trusted publishing
|
|
224
|
+
CI runs the unit suite on Python 3.11-3.13 and builds the docs on every PR; the integration
|
|
225
|
+
matrix (Odoo 17.0, 18.0, 19.0) runs on `main`, tags and manual dispatch. Releases are
|
|
226
|
+
published to PyPI on `v*` tags through trusted publishing, with every action pinned to a
|
|
227
|
+
commit digest.
|
|
228
|
+
|
|
229
|
+
`AGENTS.md` is the specification: layering, the contracts that may not change silently, and
|
|
230
|
+
the definition of done. Read it before changing anything.
|
|
178
231
|
|
|
179
232
|
## License
|
|
180
233
|
|
|
@@ -36,6 +36,52 @@ work on every command:
|
|
|
36
36
|
--context '{"tz": "Europe/Brussels"}' any other key, merged with the flags above
|
|
37
37
|
```
|
|
38
38
|
|
|
39
|
+
## Business names instead of technical ones
|
|
40
|
+
|
|
41
|
+
Read commands accept an alias in place of a model name, and a preset name in place of a
|
|
42
|
+
condition. Both expand to an ordinary domain before anything is sent, so the result has
|
|
43
|
+
exactly the same shape.
|
|
44
|
+
|
|
45
|
+
```
|
|
46
|
+
odoo alias every alias, its model and its filter (no connection needed)
|
|
47
|
+
odoo alias invoices --presets the presets that apply to that model
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
```
|
|
51
|
+
odoo search invoices -w overdue same as
|
|
52
|
+
odoo search account.move -w move_type=out_invoice -w payment_state=not_paid \
|
|
53
|
+
-w invoice_date_due<TODAY
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
Aliases include `partners`, `customers`, `suppliers`, `invoices`, `bills`, `credit-notes`,
|
|
57
|
+
`orders`, `quotes`, `opportunities`, `products`, `pickings`, `users`, `employees`, `tasks`.
|
|
58
|
+
Presets include `overdue`, `unpaid`, `paid`, `draft`, `posted`, `cancelled`, `confirmed`,
|
|
59
|
+
`this-month`, `this-year`, `ready`, `done`, and `archived` / `active` on any model.
|
|
60
|
+
|
|
61
|
+
Write commands refuse an alias that carries a filter (exit 2, `alias_not_writable`): use the
|
|
62
|
+
technical name and set the discriminator field yourself, so a vendor bill cannot be filed as
|
|
63
|
+
a customer invoice by accident.
|
|
64
|
+
|
|
65
|
+
## Field names are checked before the call
|
|
66
|
+
|
|
67
|
+
A misspelled field costs no round trip. The CLI reads the model schema once with
|
|
68
|
+
`fields_get`, caches it for 24 hours, and checks every name used in `-w`, `--fields`,
|
|
69
|
+
`--order`, `-v` and `--by`:
|
|
70
|
+
|
|
71
|
+
```
|
|
72
|
+
$ odoo search partners -w mobil=+32
|
|
73
|
+
{"error": {"code": "unknown_field", "message": "res.partner has no field 'mobil'.
|
|
74
|
+
Did you mean 'mobile'? (also: phone)"}} exit 2, nothing was sent
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
Dotted paths are followed across relations, and the error names the model where the path
|
|
78
|
+
actually broke. Using a computed non-stored field in `-w` or `--order` produces a
|
|
79
|
+
`field_not_stored` warning on stderr and the call still goes out.
|
|
80
|
+
|
|
81
|
+
Validation only ever rejects a name it has positively read from the server: if the schema
|
|
82
|
+
cannot be read, it stays quiet. Turn it off with `--no-validate` or `ODOO_NO_VALIDATE=1`.
|
|
83
|
+
Refresh it after a module install or an Odoo upgrade with `odoo cache clear`.
|
|
84
|
+
|
|
39
85
|
## Output contract
|
|
40
86
|
|
|
41
87
|
- Piped or captured: raw Odoo JSON, exactly what `search_read`, `read` or `fields_get` return.
|
|
@@ -59,8 +105,16 @@ odoo search MODEL [-w COND]... [--domain JSON] [--fields a,b] [--limit N] [--off
|
|
|
59
105
|
[--order "x desc"] [--all] [--ids-only] [--lenient-fields]
|
|
60
106
|
odoo count MODEL [-w COND]... [--domain JSON]
|
|
61
107
|
odoo read MODEL ID [ID...] [--fields a,b]
|
|
108
|
+
odoo group MODEL --by FIELD[,FIELD] [--sum a,b] [--avg a,b] [-w COND]... [--limit N]
|
|
109
|
+
odoo alias [NAME] [--presets] aliases and presets, offline
|
|
110
|
+
odoo cache path|list|clear the schema cache
|
|
62
111
|
```
|
|
63
112
|
|
|
113
|
+
`MODEL` accepts an alias on every read command. `odoo group` runs `read_group`, so you
|
|
114
|
+
get counts and totals per group without pulling the records:
|
|
115
|
+
`odoo group invoices -w overdue --by partner_id --sum amount_residual`.
|
|
116
|
+
Grouping keys accept a date granularity: `--by invoice_date:month`.
|
|
117
|
+
|
|
64
118
|
Conditions (`-w`, repeatable, AND-ed together, combined with `--domain`):
|
|
65
119
|
|
|
66
120
|
```
|
|
@@ -84,7 +138,8 @@ odoo unlink MODEL IDS --yes [--dry-run]
|
|
|
84
138
|
odoo call MODEL METHOD [--ids 1,2] [--args JSON] [--kwargs JSON] [--yes] [--dry-run]
|
|
85
139
|
```
|
|
86
140
|
|
|
87
|
-
- `--dry-run` prints the exact payload and exits 0 without
|
|
141
|
+
- `--dry-run` prints the exact payload and exits 0 without writing anything. It does read
|
|
142
|
+
the model schema to check your field names, so a typo is caught there too. Use it first.
|
|
88
143
|
- `unlink` and any `call` to a non read-only method need `--yes` (or `ODOO_ASSUME_YES=1`).
|
|
89
144
|
- `call` on read-only methods (`name_search`, `read_group`, `default_get`, ...) needs
|
|
90
145
|
neither `allow_writes` nor `--yes`.
|
|
@@ -103,8 +158,9 @@ One2many and many2many fields take Odoo commands, written as JSON in `-v` or `--
|
|
|
103
158
|
|
|
104
159
|
## Working method that avoids most failures
|
|
105
160
|
|
|
106
|
-
1. Unknown model? `odoo
|
|
107
|
-
and `selection`
|
|
161
|
+
1. Unknown model? Try `odoo alias` first, then `odoo models --like word`. Then
|
|
162
|
+
`odoo fields MODEL`, which shows `type`, `required`, `store`, `relation` and `selection`
|
|
163
|
+
values.
|
|
108
164
|
2. Only filter or order on fields with `store: true`. Computed non-stored fields
|
|
109
165
|
(`qty_available`, `amount_to_invoice`, ...) can be read but not searched; Odoo answers
|
|
110
166
|
"Cannot convert ... to SQL". Read them and filter client-side.
|
|
@@ -119,7 +175,8 @@ One2many and many2many fields take Odoo commands, written as JSON in `-v` or `--
|
|
|
119
175
|
became `company_ids`). If a field is rejected, `odoo fields` is the truth.
|
|
120
176
|
`--lenient-fields` on `search` removes rejected fields and retries, with a warning on
|
|
121
177
|
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`.
|
|
178
|
+
8. Never guess a model name: `odoo alias`, then `odoo models --like invoice`. A dotless
|
|
179
|
+
name close to a known alias is reported as a typo instead of being sent.
|
|
123
180
|
9. Sensitive models (`ir.config_parameter`, `ir.mail_server`, `res.users.apikeys`, `ir.cron`,
|
|
124
181
|
`ir.actions.server`, ...) are refused unless `--include-sensitive`.
|
|
125
182
|
10. A record you know exists but cannot find is usually archived (`--include-archived`) or in
|
|
@@ -131,6 +188,10 @@ One2many and many2many fields take Odoo commands, written as JSON in `-v` or `--
|
|
|
131
188
|
## Recipes
|
|
132
189
|
|
|
133
190
|
```
|
|
191
|
+
odoo search customers -w country_id.code=BE --fields name,email,vat --limit 20
|
|
192
|
+
odoo count invoices -w overdue
|
|
193
|
+
odoo group invoices -w overdue --by partner_id --sum amount_residual --order "amount_residual desc" --limit 10
|
|
194
|
+
odoo group invoices --by invoice_date:month --sum amount_total
|
|
134
195
|
odoo search res.partner -w is_company=true -w country_id.code=BE --fields name,email,vat --limit 20
|
|
135
196
|
odoo search sale.order -w state=sale -w date_order>=2026-01-01 --fields name,partner_id,amount_total --order "amount_total desc"
|
|
136
197
|
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.4.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,12 @@ dev = [
|
|
|
44
44
|
"respx>=0.21",
|
|
45
45
|
"ruff>=0.6",
|
|
46
46
|
"mypy>=1.11",
|
|
47
|
+
"pytest-cov>=5",
|
|
48
|
+
]
|
|
49
|
+
docs = [
|
|
50
|
+
"mkdocs>=1.6",
|
|
51
|
+
"mkdocs-material>=9.5",
|
|
52
|
+
"mkdocstrings[python]>=0.26",
|
|
47
53
|
]
|
|
48
54
|
|
|
49
55
|
[tool.hatch.build.targets.wheel]
|
|
@@ -76,6 +82,15 @@ warn_unreachable = true
|
|
|
76
82
|
module = ["respx", "respx.*"]
|
|
77
83
|
ignore_missing_imports = true
|
|
78
84
|
|
|
85
|
+
[tool.coverage.run]
|
|
86
|
+
source = ["odoocli"]
|
|
87
|
+
branch = true
|
|
88
|
+
|
|
89
|
+
[tool.coverage.report]
|
|
90
|
+
show_missing = true
|
|
91
|
+
fail_under = 93
|
|
92
|
+
exclude_also = ["if TYPE_CHECKING:", "raise AssertionError\\(\"unreachable\"\\)"]
|
|
93
|
+
|
|
79
94
|
[tool.pytest.ini_options]
|
|
80
95
|
asyncio_mode = "auto"
|
|
81
96
|
testpaths = ["tests"]
|
|
@@ -31,6 +31,52 @@ work on every command:
|
|
|
31
31
|
--context '{"tz": "Europe/Brussels"}' any other key, merged with the flags above
|
|
32
32
|
```
|
|
33
33
|
|
|
34
|
+
## Business names instead of technical ones
|
|
35
|
+
|
|
36
|
+
Read commands accept an alias in place of a model name, and a preset name in place of a
|
|
37
|
+
condition. Both expand to an ordinary domain before anything is sent, so the result has
|
|
38
|
+
exactly the same shape.
|
|
39
|
+
|
|
40
|
+
```
|
|
41
|
+
odoo alias every alias, its model and its filter (no connection needed)
|
|
42
|
+
odoo alias invoices --presets the presets that apply to that model
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
```
|
|
46
|
+
odoo search invoices -w overdue same as
|
|
47
|
+
odoo search account.move -w move_type=out_invoice -w payment_state=not_paid \
|
|
48
|
+
-w invoice_date_due<TODAY
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
Aliases include `partners`, `customers`, `suppliers`, `invoices`, `bills`, `credit-notes`,
|
|
52
|
+
`orders`, `quotes`, `opportunities`, `products`, `pickings`, `users`, `employees`, `tasks`.
|
|
53
|
+
Presets include `overdue`, `unpaid`, `paid`, `draft`, `posted`, `cancelled`, `confirmed`,
|
|
54
|
+
`this-month`, `this-year`, `ready`, `done`, and `archived` / `active` on any model.
|
|
55
|
+
|
|
56
|
+
Write commands refuse an alias that carries a filter (exit 2, `alias_not_writable`): use the
|
|
57
|
+
technical name and set the discriminator field yourself, so a vendor bill cannot be filed as
|
|
58
|
+
a customer invoice by accident.
|
|
59
|
+
|
|
60
|
+
## Field names are checked before the call
|
|
61
|
+
|
|
62
|
+
A misspelled field costs no round trip. The CLI reads the model schema once with
|
|
63
|
+
`fields_get`, caches it for 24 hours, and checks every name used in `-w`, `--fields`,
|
|
64
|
+
`--order`, `-v` and `--by`:
|
|
65
|
+
|
|
66
|
+
```
|
|
67
|
+
$ odoo search partners -w mobil=+32
|
|
68
|
+
{"error": {"code": "unknown_field", "message": "res.partner has no field 'mobil'.
|
|
69
|
+
Did you mean 'mobile'? (also: phone)"}} exit 2, nothing was sent
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
Dotted paths are followed across relations, and the error names the model where the path
|
|
73
|
+
actually broke. Using a computed non-stored field in `-w` or `--order` produces a
|
|
74
|
+
`field_not_stored` warning on stderr and the call still goes out.
|
|
75
|
+
|
|
76
|
+
Validation only ever rejects a name it has positively read from the server: if the schema
|
|
77
|
+
cannot be read, it stays quiet. Turn it off with `--no-validate` or `ODOO_NO_VALIDATE=1`.
|
|
78
|
+
Refresh it after a module install or an Odoo upgrade with `odoo cache clear`.
|
|
79
|
+
|
|
34
80
|
## Output contract
|
|
35
81
|
|
|
36
82
|
- Piped or captured: raw Odoo JSON, exactly what `search_read`, `read` or `fields_get` return.
|
|
@@ -54,8 +100,16 @@ odoo search MODEL [-w COND]... [--domain JSON] [--fields a,b] [--limit N] [--off
|
|
|
54
100
|
[--order "x desc"] [--all] [--ids-only] [--lenient-fields]
|
|
55
101
|
odoo count MODEL [-w COND]... [--domain JSON]
|
|
56
102
|
odoo read MODEL ID [ID...] [--fields a,b]
|
|
103
|
+
odoo group MODEL --by FIELD[,FIELD] [--sum a,b] [--avg a,b] [-w COND]... [--limit N]
|
|
104
|
+
odoo alias [NAME] [--presets] aliases and presets, offline
|
|
105
|
+
odoo cache path|list|clear the schema cache
|
|
57
106
|
```
|
|
58
107
|
|
|
108
|
+
`MODEL` accepts an alias on every read command. `odoo group` runs `read_group`, so you
|
|
109
|
+
get counts and totals per group without pulling the records:
|
|
110
|
+
`odoo group invoices -w overdue --by partner_id --sum amount_residual`.
|
|
111
|
+
Grouping keys accept a date granularity: `--by invoice_date:month`.
|
|
112
|
+
|
|
59
113
|
Conditions (`-w`, repeatable, AND-ed together, combined with `--domain`):
|
|
60
114
|
|
|
61
115
|
```
|
|
@@ -79,7 +133,8 @@ odoo unlink MODEL IDS --yes [--dry-run]
|
|
|
79
133
|
odoo call MODEL METHOD [--ids 1,2] [--args JSON] [--kwargs JSON] [--yes] [--dry-run]
|
|
80
134
|
```
|
|
81
135
|
|
|
82
|
-
- `--dry-run` prints the exact payload and exits 0 without
|
|
136
|
+
- `--dry-run` prints the exact payload and exits 0 without writing anything. It does read
|
|
137
|
+
the model schema to check your field names, so a typo is caught there too. Use it first.
|
|
83
138
|
- `unlink` and any `call` to a non read-only method need `--yes` (or `ODOO_ASSUME_YES=1`).
|
|
84
139
|
- `call` on read-only methods (`name_search`, `read_group`, `default_get`, ...) needs
|
|
85
140
|
neither `allow_writes` nor `--yes`.
|
|
@@ -98,8 +153,9 @@ One2many and many2many fields take Odoo commands, written as JSON in `-v` or `--
|
|
|
98
153
|
|
|
99
154
|
## Working method that avoids most failures
|
|
100
155
|
|
|
101
|
-
1. Unknown model? `odoo
|
|
102
|
-
and `selection`
|
|
156
|
+
1. Unknown model? Try `odoo alias` first, then `odoo models --like word`. Then
|
|
157
|
+
`odoo fields MODEL`, which shows `type`, `required`, `store`, `relation` and `selection`
|
|
158
|
+
values.
|
|
103
159
|
2. Only filter or order on fields with `store: true`. Computed non-stored fields
|
|
104
160
|
(`qty_available`, `amount_to_invoice`, ...) can be read but not searched; Odoo answers
|
|
105
161
|
"Cannot convert ... to SQL". Read them and filter client-side.
|
|
@@ -114,7 +170,8 @@ One2many and many2many fields take Odoo commands, written as JSON in `-v` or `--
|
|
|
114
170
|
became `company_ids`). If a field is rejected, `odoo fields` is the truth.
|
|
115
171
|
`--lenient-fields` on `search` removes rejected fields and retries, with a warning on
|
|
116
172
|
stderr; only use it for exploration, never in a script that relies on the result.
|
|
117
|
-
8. Never guess a model name: `odoo models --like invoice`.
|
|
173
|
+
8. Never guess a model name: `odoo alias`, then `odoo models --like invoice`. A dotless
|
|
174
|
+
name close to a known alias is reported as a typo instead of being sent.
|
|
118
175
|
9. Sensitive models (`ir.config_parameter`, `ir.mail_server`, `res.users.apikeys`, `ir.cron`,
|
|
119
176
|
`ir.actions.server`, ...) are refused unless `--include-sensitive`.
|
|
120
177
|
10. A record you know exists but cannot find is usually archived (`--include-archived`) or in
|
|
@@ -126,6 +183,10 @@ One2many and many2many fields take Odoo commands, written as JSON in `-v` or `--
|
|
|
126
183
|
## Recipes
|
|
127
184
|
|
|
128
185
|
```
|
|
186
|
+
odoo search customers -w country_id.code=BE --fields name,email,vat --limit 20
|
|
187
|
+
odoo count invoices -w overdue
|
|
188
|
+
odoo group invoices -w overdue --by partner_id --sum amount_residual --order "amount_residual desc" --limit 10
|
|
189
|
+
odoo group invoices --by invoice_date:month --sum amount_total
|
|
129
190
|
odoo search res.partner -w is_company=true -w country_id.code=BE --fields name,email,vat --limit 20
|
|
130
191
|
odoo search sale.order -w state=sale -w date_order>=2026-01-01 --fields name,partner_id,amount_total --order "amount_total desc"
|
|
131
192
|
odoo count account.move -w move_type=out_invoice -w payment_state=not_paid -w invoice_date_due<2026-09-01
|