Odoo js
Odoo development skills for AI coding agents — a Claude Code plugin (also Codex & Gemini CLI) packaging PSDU principles for models, views, security, migrations, tests & code review. Principles, not rules.
npx -y skills add assoumaaa/odoo-superpowers --skill odoo-jsAssembled from the repository path, not quoted from the project. Check it against their README if it does not work.
2 things to look at
- no licenseNo license file was found in the repository. Code published without one is not open source by default, so using it at work is a question for whoever answers licensing questions where you are.
- 1 stars1 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.
What its author says it does
Copied from the file, not written here
Use when writing, reviewing, or migrating any JavaScript, OWL component, QWeb/OWL template, or SCSS under static/src in an Odoo module — POS overrides, backend widgets, website/public widgets, services. Carries PSAE review-derived principles for OWL correctness, the patch() discipline, template conventions, frontend JS style, and cross-version upgrade gotchas, plus references for exact patterns. Invoke before writing anything under static/src/.
SKILL.md
9.0 KB, as published. Nobody here has run it
Odoo JS
Write the frontend an experienced PSAE reviewer wouldn't comment on — so the review is about the feature, not the patch mechanics or the let that should have been a const. These are stances to carry, not a checklist to run: the exact forms live in references/, pulled only when you need the fact. When a task skips the planning pass (odoo-grill), these reflexes carry the design alone.
The single most repeated finding isn't a bug — it's reaching around the framework: querying the DOM instead of reading state, copy-pasting a standard method instead of calling super, reassigning X.props instead of patch-ing it. OWL gives you a declarative tool for almost everything you reach for imperatively; the stances below are mostly that one instinct applied.
The stances
Extend with patch(), and always reach super
Customisation is patch() — never reassignment, Component.extend, or the old Registries API — because it's the only form that unpatches cleanly and composes with other modules' patches; and X.prototype.method.call(this) is not a back door to "the original," since patch() mutated the prototype, so it hits the patched method anyway while looking like a bypass. Patch the layer you mean: prototype (methods, setup), the class itself (static members like template), and the props object are separate objects, so patching the prototype to add a static prop misses. Above all, find a way to call super: copy-pasting an inherited method's whole body to change three lines is the most dangerous frontend pattern, because the day Odoo hotfixes that method every customer not on your fork silently loses it — guard your special case, then return super.method(...arguments) for the rest. Don't override a pure getter to bolt on a side-effect (put the mutation in its own method), and change props plus the template before reaching for a whole new Component subclass — that subclass is a maintenance liability you rarely need. (Exact forms, super in async patches, version-renamed methods: references/patching-services.md.)
Think in state and getters, not the DOM
A value you can derive from state, props, or record fields is a getter — not a value frozen into this.x in setup() (stale the moment state changes) and not a no-arg method (a getter with worse ergonomics). Getters recompute on every render and never go stale; "make this a getter" is the single most repeated component-level comment, and it applies to patched POS models (PosOrder, PosOrderline) exactly as to screen components. Push a complex t-if/t-att-class expression into a getter too, so the template stays declarative and the logic stays testable. The DOM itself is OWL's to own: no document.getElementById, no $(...), no jQuery — reach an element with useRef+t-ref and react to events with t-on-click, never a manual addEventListener, because a query from the component root starts too high and breaks once two instances exist. And side-effects belong in lifecycle hooks, not setup()'s body — async load in onWillStart, DOM-dependent work in onMounted, cleanup in onWillDestroy; never override the whole setup() to inject one line, since hooks from multiple patches stack while a full override clobbers. (references/owl-components.md.)
Put service-needing logic where its dependencies live
Acquire each dependency once with useService (or the domain hook like usePos() when one exists), not this.env.services.x scattered through methods — and check what the core component already assigns before re-acquiring, or it's dead code (TicketScreen already exposes this.dialog). Services never travel as arguments: a method on a POS record that needs orm, notifications, or env.utils is in the wrong layer, and passing them in from the caller (order.applySettlement({ orm, pos })) just hides that. The fix is layering — records keep pure data mutations; shared service-needing business logic goes on the store (patch(PosStore.prototype), where this.data, this.models, this.env already live, as core pos_loyalty does it); the component stays a thin UI orchestrator passing plain data. Call models through orm, reserving bare rpc(route, params) for actual controller routes. A user-triggered call that can fail needs a visible failure — surface an error dialog, never let a rejected promise vanish — and fire independent calls with Promise.all rather than awaiting in sequence. Load POS master data through the Python loader so it rides POS's one RPC, not a per-screen orm.call. (references/patching-services.md.)
Keep templates declarative, styles in classes
The template renders state; it neither computes nor stores. Use t-out, never the deprecated t-esc. Conditional classes take the object form ({ 'text-danger': hasError }), not a ternary that concatenates class strings — and a getter returning that object reads even better. Hide a standard element with d-none, never t-if="False" or position="replace": deleting a node another module's xpath depends on breaks that module silently, so add the class via position="attributes" and insert your variant position="after" (a component tag can't take d-none — its class becomes a prop — so use t-if there instead). xpath on structure, not text: hasclass('foo') over an exact-string @class='foo', always with t-inherit-mode set. No inline style= and no data-* on OWL templates — inline style isn't cached and costs refresh time, and a data-* attribute almost always means you're about to read it back out of the DOM, which means it should have been state. For styling, reach for an existing Bootstrap utility first, write SCSS only when none fits, never inline (and Bootstrap's grid isn't loaded in POS — row/col-* do nothing there). (references/templates-styling.md.)
The Odoo-specific JS reflexes
General JS style is the formatter's job and a competent baseline; these are the few reflexes that are Odoo-specific or are traps the framework sets. Files are ES6 modules — import { x }, never require or odoo.define (an /** @odoo-module **/ header opts a file into the module system on versions that still need it). Prefix a variable with $ only when it actually holds a jQuery object, never a plain HTMLElement. Every user-facing string passes through _t(), or it can't be translated. Extract with map then fold with reduce — one job per stage; a single callback that does both draws the stock "map + reduce" comment — and index a repeated lookup into a Map rather than nesting an O(n²) search. The trap carried over from Python: an empty array is truthy in JS, so if (list) doesn't test emptiness — use list.length. (references/js-conventions.md.)
JS upgrades break silently — read both versions first
JS is where upgrades break quietly: import paths, registry categories, component names, and bundle names all move between major versions with no error — just a feature that stops working — so read the component in both the old and the target source before touching any JS file in a migration (→ odoo-migrations). The recurring traps: publicWidget is gone in v19 → the Interaction class (the events hash becomes dynamicContent/addListener, async work moves from start() into willStart(), and there's no super). A core method you wrap may have switched jQuery→native and now throws where it used to no-op — v18 $('sel').append(x) on an empty match vs v19 querySelector('sel').append throwing on null — so guard the container before delegating through; asset/patch order won't fix it. And deep state mutation may be missed by OWL 3's batched scheduler (v19): reassign this.state.config = { ...this.state.config, key: x } rather than writing this.state.config.key = x in place.
References (consult for the fact, don't memorize)
| Need the exact... | Read |
|---|---|
OWL component anatomy — getters vs state, useRef/t-ref, lifecycle hooks, prop shapes | references/owl-components.md |
patch() rules (prototype/static/props), calling super, services, ORM/RPC, registries | references/patching-services.md |
QWeb/OWL template directives, conditional classes, inheritance (t-inherit, xpath, d-none), SCSS | references/templates-styling.md |
JS style — const/naming/imports, array idioms, guards, async, _t | references/js-conventions.md |
For where frontend files live and how assets are declared in the manifest, see odoo-module-development. For XML backend views (not OWL templates), see odoo-xml-conventions. For the Python side of a POS data loader or controller, see odoo-python.