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.
Files changed (30) hide show
  1. {odoo_agent_cli-0.2.0 → odoo_agent_cli-0.4.0}/.gitignore +3 -0
  2. {odoo_agent_cli-0.2.0 → odoo_agent_cli-0.4.0}/PKG-INFO +61 -8
  3. {odoo_agent_cli-0.2.0 → odoo_agent_cli-0.4.0}/README.md +60 -7
  4. {odoo_agent_cli-0.2.0 → odoo_agent_cli-0.4.0}/SKILL.md +65 -4
  5. {odoo_agent_cli-0.2.0 → odoo_agent_cli-0.4.0}/pyproject.toml +16 -1
  6. {odoo_agent_cli-0.2.0 → odoo_agent_cli-0.4.0}/src/odoocli/AGENT_GUIDE.md +65 -4
  7. {odoo_agent_cli-0.2.0 → odoo_agent_cli-0.4.0}/src/odoocli/_version.py +1 -1
  8. odoo_agent_cli-0.4.0/src/odoocli/aliases.py +199 -0
  9. odoo_agent_cli-0.4.0/src/odoocli/cli/alias_cmd.py +41 -0
  10. {odoo_agent_cli-0.2.0 → odoo_agent_cli-0.4.0}/src/odoocli/cli/app.py +151 -5
  11. odoo_agent_cli-0.4.0/src/odoocli/cli/cache_cmds.py +72 -0
  12. {odoo_agent_cli-0.2.0 → odoo_agent_cli-0.4.0}/src/odoocli/cli/read_cmds.py +101 -19
  13. {odoo_agent_cli-0.2.0 → odoo_agent_cli-0.4.0}/src/odoocli/cli/write_cmds.py +33 -21
  14. {odoo_agent_cli-0.2.0 → odoo_agent_cli-0.4.0}/src/odoocli/domain.py +58 -7
  15. odoo_agent_cli-0.4.0/src/odoocli/schema.py +362 -0
  16. {odoo_agent_cli-0.2.0 → odoo_agent_cli-0.4.0}/LICENSE +0 -0
  17. {odoo_agent_cli-0.2.0 → odoo_agent_cli-0.4.0}/src/odoocli/__init__.py +0 -0
  18. {odoo_agent_cli-0.2.0 → odoo_agent_cli-0.4.0}/src/odoocli/__main__.py +0 -0
  19. {odoo_agent_cli-0.2.0 → odoo_agent_cli-0.4.0}/src/odoocli/cli/__init__.py +0 -0
  20. {odoo_agent_cli-0.2.0 → odoo_agent_cli-0.4.0}/src/odoocli/cli/guide_cmd.py +0 -0
  21. {odoo_agent_cli-0.2.0 → odoo_agent_cli-0.4.0}/src/odoocli/cli/output.py +0 -0
  22. {odoo_agent_cli-0.2.0 → odoo_agent_cli-0.4.0}/src/odoocli/cli/profile_cmds.py +0 -0
  23. {odoo_agent_cli-0.2.0 → odoo_agent_cli-0.4.0}/src/odoocli/cli/values.py +0 -0
  24. {odoo_agent_cli-0.2.0 → odoo_agent_cli-0.4.0}/src/odoocli/client.py +0 -0
  25. {odoo_agent_cli-0.2.0 → odoo_agent_cli-0.4.0}/src/odoocli/config.py +0 -0
  26. {odoo_agent_cli-0.2.0 → odoo_agent_cli-0.4.0}/src/odoocli/errors.py +0 -0
  27. {odoo_agent_cli-0.2.0 → odoo_agent_cli-0.4.0}/src/odoocli/lenient.py +0 -0
  28. {odoo_agent_cli-0.2.0 → odoo_agent_cli-0.4.0}/src/odoocli/py.typed +0 -0
  29. {odoo_agent_cli-0.2.0 → odoo_agent_cli-0.4.0}/src/odoocli/security.py +0 -0
  30. {odoo_agent_cli-0.2.0 → odoo_agent_cli-0.4.0}/src/odoocli/sync.py +0 -0
@@ -11,3 +11,6 @@ build/
11
11
  htmlcov/
12
12
  .env
13
13
  .DS_Store
14
+
15
+ # mkdocs build output
16
+ site/
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: odoo-agent-cli
3
- Version: 0.2.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 models --like invoice # find the right technical name
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 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 \
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 # shows payload, exit 0
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 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.
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 models --like invoice # find the right technical name
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 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 \
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 # shows payload, exit 0
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 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.
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 calling Odoo. Use it first.
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 fields MODEL` first. It shows `type`, `required`, `store`, `relation`
107
- and `selection` values.
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.2.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 calling Odoo. Use it first.
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 fields MODEL` first. It shows `type`, `required`, `store`, `relation`
102
- and `selection` values.
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
@@ -1,3 +1,3 @@
1
1
  """Single source of the package version (kept import-free to avoid cycles)."""
2
2
 
3
- __version__ = "0.2.0"
3
+ __version__ = "0.4.0"