# Odoo 19 (Enterprise) — field names & footguns Hard-won notes from a real v19 demo build. Check these before you waste a build cycle. Always confirm the major version first: `version()["version"]`. ## Renamed core fields (these break v17/v18 code) | Concept | v17/18 | **v19** | |---|---|---| | User's groups | `groups_id` | **`group_ids`** | | Group's app category | `category_id` on `res.groups` | **removed** — use `privilege_id` (model `res.groups.privilege`), or just omit | | (so in security XML) | `` | `` | A custom module that sets `category_id` on `res.groups` or `groups_id` on `res.users` will fail to install on v19 with `Invalid field ... / Cannot ...`. Drop the category or use `privilege_id`. ## fields_get returns null attributes over RPC (some v19 builds) `fields_get([], {"attributes": ["string","type"]})` may return each field with `string=None`. The **keys (technical names) are still correct** — match on keys, not labels. (`fields_named()` in `odoo_connect.py` does this.) ## Product model (v19) - Goods that track stock: `type="consu"` **plus** `is_storable=True` (the old `type="product"` is gone). - Weight: `product.template.weight` (kg by default). ## Credit limit (Accounting) - Native behaviour is **WARNING ONLY** — it does NOT block SO confirmation. A hard block needs a Studio approval rule or a custom `action_confirm` override. - Enable: `res.config.settings.account_use_credit_limit = True` (+ `account_default_credit_limit`). Apply via `create` then `execute`. - Per-customer: `res.partner.use_partner_credit_limit = True`, `credit_limit = `. **These are non-stored** — you cannot `search` on `use_partner_credit_limit` (Postgres "not stored" error); read it per-record instead. - `res.partner.credit` = current Total Receivable (computed from posted AR moves). Pre-stage it by posting an unpaid customer invoice. ## Fleet load-by-weight / Dispatch (v19) — IS native, but gated - Native `fleet` alone = vehicle cost/maintenance; **no capacity fields**. - Install **`stock_fleet`** ("Stock Transport"). It adds the dispatch fields to `stock.picking.batch`: `vehicle_id`, `vehicle_category_id`, `vehicle_weight_capacity`, `used_weight_percentage`, `estimated_shipping_weight`, `has_dispatch_management`. - Capacity lives on **`fleet.vehicle.model.category`** → fields `weight_capacity`, `volume_capacity`. Create a category with capacity, assign it to the vehicle (`fleet.vehicle.category_id`), then a batch with that vehicle computes `used_weight_percentage`. - Demo flow: confirm SOs → deliveries → create `stock.picking.batch` with `vehicle_id` + `picking_ids` → read `used_weight_percentage`. ## Currency - New databases ship most currencies **inactive**. Find with `search([["name","=","EGP"]], context={"active_test": False})`, set `active=True`, then `res.company.currency_id`. - Change company currency **BEFORE** posting any invoices/journal entries — Odoo blocks the switch once entries exist. ## Stock seeding (so deliveries go Ready) - Create `stock.quant` with `inventory_quantity` and `context={"inventory_mode": True}`, then call `action_apply_inventory` with the same context. The apply may warn harmlessly if already applied; check by reserving (`stock.picking.action_assign` → state `assigned`). ## State fields are read-only in practice - `state` on `sale.order` / `account.move` / `stock.picking` is writable over RPC and doing so is always wrong: it skips the workflow. Confirming an SO with `write` creates no delivery and no invoice basis; posting a move with `write` creates no journal entries and no sequence number. - Use the button method: `action_confirm`, `action_post` / `button_draft` / `button_cancel`, `action_assign` + `button_validate`, `button_confirm` (PO). - `stock.picking.button_validate` can return a wizard dict (`stock.backorder.confirmation`, immediate transfer) instead of `True` — create the wizard from `res_model`/`context` and call its `process` button. ## RPC helper footgun - v19's API is **JSON-2** (`POST /json/2//`, `Authorization: bearer `). `/xmlrpc`, `/xmlrpc/2` and `/jsonrpc` still work but are deprecated and go away in Odoo 22. - **No positional arguments exist.** ids go in the `ids` body key, everything else under the ORM parameter's own name: `domain`, `fields`, `vals` (write), `vals_list` (create), `context`, `limit`. Wrong name = `TypeError` from the server, not a silent misread — but guessing still costs a round trip. Every model+method signature for YOUR database is published at `/doc`. - The old execute_kw footgun (a context dict passed positionally landing in `offset` → `psycopg2 can't adapt type 'dict'`) is gone with the endpoint. If you see it, you are on legacy code. - **One call, one transaction.** Committed on success, discarded on error, and nothing chains across calls. Prefer a single method that does the whole job (`search_read`, `action_confirm`) over a sequence you orchestrate client-side. - API keys expire — three months maximum, one day if you generated it interactively. Check the expiry before demo day. - Odoo Online exposes the external API on **Custom** plans only (not One App Free, not Standard). - **Batch by widening calls, not by bundling them.** JSON-2 has no multi-call envelope, but every ORM method is set-based: `create(vals_list=[...])` takes a list and returns ids in order, `write`/`read`/`action_post` take an id list, `search_read` replaces search+read in one transaction. `get_or_create_many()` in `odoo_connect.py` does the batched get-or-create. A batched `create` fails as a unit — keep the loop where you need per-record error isolation. ## Module install over RPC - `x("ir.module.module","button_immediate_install", ids)`. Heavy — let it run minutes (`x()` defaults to a 600s timeout for this reason). On a failed XML-data load it rolls back cleanly (module stays uninstalled); fix and retry. - After deploying a custom module to git (Odoo.sh), wait for the rebuild, then `x("ir.module.module","update_list")` and search for it before installing.