The JSON config loader keeps backend POS configuration in a single declarative document. Apply the JSON and the database converges on the described state: records are created, updated, or deleted to match, and the most recently applied document is replayed automatically on module install and upgrade so configuration survives migrations. Implemented inDocumentation Index
Fetch the complete documentation index at: https://docs.laportenard.com/llms.txt
Use this file to discover all available pages before exploring further.
nu_restaurant_pos/lib/config_loader.py and nu_restaurant_pos/models/nu_pos_config_sync.py.
What it manages
| Section | Odoo model | Match key |
|---|---|---|
printers | nu.pos.printer | key (loader-owned xmlid) |
print_routes | nu.pos.print.route | key (loader-owned xmlid) |
discount_presets | nu.pos.discount.preset | key (loader-owned xmlid) |
void_reasons | nu.pos.void.reason | key (loader-owned xmlid) |
special_request_tags | nu.pos.special.request.tag | key (loader-owned xmlid) |
side_dish_groups | nu.pos.side.dish.group | key (loader-owned xmlid) |
picking_sale_reasons | nu.pos.picking.sale.reason | key (loader-owned xmlid) |
shared_configs | nu.pos.config | name + company |
pos_configs | pos.config (nu_* fields only) | name + optional company |
Catalog records (the first seven sections) and shared configs are matched by xmlids stamped under the synthetic module
__nu_pos_config_json__. The loader only ever sees records it owns — anything created manually in the Odoo UI is invisible to it and will never be modified or deleted.Document shape
Keys vs xmlids
key— a string you choose. Stable within a section, scoped to the loader. Used to reference sibling records inside the document (e.g.print_routes.printerreferences aprinterskey).- External xmlid — full
module.namereference to a record outside the JSON (companies, accounts, journals, POS categories, products, users, floors, tags, fiscal positions).
Apply, dry-run, export
Three entry points, all gated bybase.group_system:
Admin form
Settings → POS → JSON Config Sync. Upload the file, then click Dry-run preview to see the create/update/delete plan, or Apply for the real run. Export current state downloads the current DB as JSON.
Active attachment & upgrade replay
action_apply (and the programmatic equivalents) persists the raw JSON as a single ir.attachment named active_config.json attached to the nu.pos.config.sync model with res_id=0. The post-init hook and the version migration script both call apply_active_attachment(), which:
- Loads the active attachment (no-op if none exists).
- Runs the loader against the live database.
- Logs the resulting plan.
Execution order
The loader runs everything inside a single savepoint:- Validate — shape, required keys, selection values, all external xmlids resolve.
- Plan — diff current xmlid-owned records against the JSON to count create/update/delete per section.
- Catalog upserts — printers, routes, presets, void reasons, tags, side-dish groups, picking-sale reasons.
- Shared configs (pass 1) — everything except terminal-dependent fields (
cashier_pos_configs,shared_floors). pos.configupdates — link terminals to theirnu_shared_config_idand applynu_*fields.- Shared configs (pass 2) — terminal-dependent fields, now that the terminal linkage exists.
- Catalog orphan deletes — remove loader-owned records not present in the JSON.
- Shared-config deletes — remove loader-owned
nu.pos.configrecords not present in the JSON, only if nopos.configreferences them.
shared_floors require linked terminals, which don’t exist until step 5.
Safety rules
Terminals are never created or deleted
pos_configs is update-only. An entry whose name doesn’t match an existing pos.config is skipped and recorded as a warning. The loader never creates or removes physical terminals.Hand-managed records are untouchable
A printer (or any catalog record) created in the Odoo UI has no xmlid under
__nu_pos_config_json__. The loader doesn’t see it, doesn’t update it, doesn’t delete it.Shared configs linked to terminals are not deleted
If a JSON-removed shared config still has a
pos_config_ids link, the loader skips the delete and emits a warning instead of breaking running terminals.Pre-flight references all resolve
Every xmlid in the document is checked before any write. A single bad reference fails the whole document; nothing partially applies.
Plan output
Bothaction_dry_run and action_apply return a plan dict and render it in the Last result panel. Example:
Authoring workflow
A typical lifecycle for managing config in a deployment:Bootstrap from the live database
Click Export current state to download a JSON snapshot of the current configuration. Commit it to your config repo.
Edit declaratively
Add, remove, or change entries. Keys must be stable — renaming a
key is treated as a delete + create.Dry-run
Upload and click Dry-run preview. Review the create/update/delete counts and any warnings before applying.
Adding a new field
To add a new field to one of the managed models:- Add the field to the model in
nu_restaurant_pos/models/. - Map it in
_vals_from_entry()(write) and_export_catalog_entry()(read) inconfig_loader.py, or in the correspondingSHARED_*tuple fornu.pos.configandPOS_CONFIG_FIELDSforpos.config. - If the field references another record by xmlid, add it to
_validate_references()so pre-flight catches bad references. - If the field is a
Selection, add it to_validate_selections(). - Add a test in
nu_restaurant_pos/tests/test_config_loader.py.
Catalog sections that need cross-record references (like
special_request_tags.parent or side_dish_groups.lines.product) are written in two passes inside the loader. Follow the existing patterns in _upsert_special_request_tags and _upsert_side_dish_groups rather than inlining the logic.Adding a new section
Adding a brand-new catalog section requires more than_vals_from_entry:
- Add the section name to
ALLOWED_TOP_LEVELandCATALOG_SECTIONS. - Map it to its model in
CATALOG_MODELS. - Implement
_vals_from_entryand_export_catalog_entrycases. - Add reference and selection validators if the section has them.
- If the section needs multi-pass writes (e.g. self-references), add a dedicated
_upsert_*function and dispatch to it from_execute().
Reference
- Loader:
nu_restaurant_pos/lib/config_loader.py - UI wizard + replay:
nu_restaurant_pos/models/nu_pos_config_sync.py - Form view & menu:
nu_restaurant_pos/views/nu_pos_config_sync_views.xml - Post-init hook:
nu_restaurant_pos/__init__.py(_post_init_apply_config) - Upgrade replay:
nu_restaurant_pos/migrations/12.0.1.14.0/post-migrate.py - Tests:
nu_restaurant_pos/tests/test_config_loader.py