- Home
- Skills
- Code Quality & Review
- Master Odoo — Version-Aware Odoo Developer (17–20)
Works with the AI Agents you already use
Master Odoo — Version-Aware Odoo Developer (17–20)
Get correct Odoo 17, 18, 19 and 20 code for the exact version you work on — and catch outdated patterns before they break your module.
Free
Master Odoo — Version-Aware Odoo Developer (17–20)
Example session with this skill installed
In my Odoo 19 module service_desk, add a Service Request model: a unique reference, a customer, a priority, a status (New → In Progress → Done) with buttons, a chatter, list / form / search views, a menu, and access rights for internal users (only managers can delete).
- Read your context and instructions
- Compiled the master odoo
- Generated the data export
Target series detected: 19.0 (from __manifest__.py). Delta deltas/odoo-19.md applied.
Plan
- Model
service.requestwithmail.thread+mail.activity.mixin(chatter and tracking). - Reference generated by an
ir.sequence(SR/2026/00001), unique in the database. - Two access levels: internal users (read, write, create) and a Service Desk Manager group (+ delete).
- Views: list, form with a status bar and action buttons, search with filters and group-by, menu.
19.0-specific choices
| Need | 19.0 way | Why |
|---|---|---|
| Unique reference | models.Constraint | _sql_constraints is ignored in 19.0: the constraint would silently not exist |
| List view | <list> | <tree> is not a view type since 18.0 |
| Chatter | <chatter/> | 18.0+ markup |
| Search group-by | bare <group> | expand / string make the view fail to load in 19.0 |
__manifest__.py
{
'name': 'Service Desk',
'version': '19.0.1.0.0',
'summary': 'Track customer service requests',
'author': 'Your Company',
'license': 'LGPL-3',
'depends': ['mail'],
'data': [
'security/security.xml',
'security/ir.model.access.csv',
'data/sequence.xml',
'views/service_request_views.xml',
],
'installable': True,
'application': True,
}
models/service_request.py
from odoo import _, api, fields, models
class ServiceRequest(models.Model):
_name = 'service.request'
_description = 'Service Request'
_inherit = ['mail.thread', 'mail.activity.mixin']
_order = 'priority desc, id desc'
name = fields.Char(string='Reference', required=True, readonly=True, copy=False,
default=lambda self: _('New'))
partner_id = fields.Many2one('res.partner', string='Customer', required=True, tracking=True)
priority = fields.Selection([('0', 'Normal'), ('1', 'High'), ('2', 'Urgent')],
default='0', tracking=True)
state = fields.Selection([('new', 'New'), ('in_progress', 'In Progress'), ('done', 'Done')],
default='new', required=True, tracking=True)
description = fields.Html()
# 19.0: _sql_constraints is ignored (warning only) — models.Constraint creates
# service_request_name_unique in the database.
_name_unique = models.Constraint('UNIQUE(name)', "This reference already exists.")
@api.model_create_multi
def create(self, vals_list):
for vals in vals_list:
if vals.get('name', _('New')) == _('New'):
vals['name'] = self.env['ir.sequence'].next_by_code('service.request') or _('New')
return super().create(vals_list)
def action_start(self):
self.write({'state': 'in_progress'})
def action_done(self):
self.write({'state': 'done'})
security/security.xml
<odoo>
<record id="group_service_manager" model="res.groups">
<field name="name">Service Desk Manager</field>
<field name="implied_ids" eval="[Command.link(ref('base.group_user'))]"/>
</record>
</odoo>
security/ir.model.access.csv
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
access_service_request_user,service.request user,model_service_request,base.group_user,1,1,1,0
access_service_request_manager,service.request manager,model_service_request,group_service_manager,1,1,1,1
data/sequence.xml
<odoo noupdate="1">
<record id="seq_service_request" model="ir.sequence">
<field name="name">Service Request</field>
<field name="code">service.request</field>
<field name="prefix">SR/%(year)s/</field>
<field name="padding">5</field>
</record>
</odoo>
views/service_request_views.xml
<odoo>
<record id="service_request_view_list" model="ir.ui.view">
<field name="name">service.request.list</field>
<field name="model">service.request</field>
<field name="arch" type="xml">
<list decoration-danger="priority == '2'" decoration-muted="state == 'done'">
<field name="name"/>
<field name="partner_id"/>
<field name="priority" widget="priority"/>
<field name="state" widget="badge"/>
</list>
</field>
</record>
<record id="service_request_view_form" model="ir.ui.view">
<field name="name">service.request.form</field>
<field name="model">service.request</field>
<field name="arch" type="xml">
<form>
<header>
<button name="action_start" type="object" string="Start" class="btn-primary"
invisible="state != 'new'"/>
<button name="action_done" type="object" string="Mark as Done" class="btn-primary"
invisible="state != 'in_progress'"/>
<field name="state" widget="statusbar"/>
</header>
<sheet>
<div class="oe_title"><h1><field name="name"/></h1></div>
<group>
<field name="partner_id"/>
<field name="priority" widget="priority"/>
</group>
<field name="description" placeholder="Describe the request..."/>
</sheet>
<chatter/>
</form>
</field>
</record>
<record id="service_request_view_search" model="ir.ui.view">
<field name="name">service.request.search</field>
<field name="model">service.request</field>
<field name="arch" type="xml">
<search>
<field name="name"/>
<field name="partner_id"/>
<filter name="open" string="Open" domain="[('state', '!=', 'done')]"/>
<filter name="urgent" string="Urgent" domain="[('priority', '=', '2')]"/>
<group>
<filter name="group_state" string="Status" context="{'group_by': 'state'}"/>
<filter name="group_partner" string="Customer" context="{'group_by': 'partner_id'}"/>
</group>
</search>
</field>
</record>
<record id="service_request_action" model="ir.actions.act_window">
<field name="name">Service Requests</field>
<field name="res_model">service.request</field>
<field name="view_mode">list,form</field>
<field name="context">{'search_default_open': 1}</field>
</record>
<menuitem id="service_desk_menu_root" name="Service Desk" sequence="40"/>
<menuitem id="service_request_menu" parent="service_desk_menu_root" action="service_request_action" sequence="10"/>
</odoo>
(plus __init__.py → from . import models and models/__init__.py → from . import service_request)
Check
$ python3 scripts/check_module.py service_desk
== service_desk (series 19.0) — 0 error(s), 0 warning(s)
Next steps
- Update the module (
-u service_desk) and assign the Service Desk Manager group to the right users. - Add tests: reference uniqueness (
IntegrityErroron a duplicate) and access rights (a non-manager cannot delete). - If several companies use the desk, add
company_idand a multi-company access rule.
access.csv
CSV · data export
Example file from a real run - the skill writes it into your workspace.
Connects securely to your tools. The creator never sees your data.
About this skill
Stop AI assistants from writing Odoo code for the wrong version
Odoo changes its conventions at every release, and most AI assistants mix them up. They write
<tree> on 18.0, name_get() on 18.0, _sql_constraints on 19.0 — where Odoo silently skips the
constraint — or get_param() on 20.0, which installs fine and crashes on first use.
Master Odoo makes your agent write code for one target version at a time — 17.0, 18.0, 19.0 or 20.0 — with conventions checked against Odoo's own source code.
What you get
- The right version, every time: the skill reads the target version from your module's manifest before writing anything, and never mixes the conventions of two versions.
- Complete, consistent modules: models and fields, XML views, security, data files, scheduled actions, controllers, QWeb reports, mixins, translations, OWL components, website themes and tests.
- Secure by default: access rights always included, record rules, safe SQL, escaped QWeb output, CSRF-aware controllers.
- A built-in code check: 77 verified rules scan your module for its version and point to the exact file, line and fix for every outdated pattern.
- No invented APIs: when something is uncertain for a version, the agent says so instead of guessing.
What changes most — and what the skill handles
| 17.0 | 18.0 | 19.0 | 20.0 | |
|---|---|---|---|---|
| List view | <tree> | <list> | <list> | <list> |
| SQL constraints | _sql_constraints | _sql_constraints | models.Constraint | models.Constraint |
| Access rights | ir.model.access.csv | same | same (group_ids) | ir.access.csv |
| System parameters | get_param | get_param | get_param | get_str / get_int… |
| Kanban card / chatter | kanban-box / oe_chatter | card / <chatter/> | same | same |
| OWL | 2 | 2 | 2 | 3 |
Verified, not guessed
Every version-specific fact was checked on the official odoo/odoo source code of each version, and
each version was exercised on real Odoo installations.
What's inside
SKILL.md— the protocol your agent follows.deltas/— one file per version (17.0 → 20.0), authoritative over the general guides.reference/— 16 focused guides: ORM, fields, views, security, manifest, decorators, controllers, data, actions, reports, mixins, tests, transactions, translation, performance, OWL.scripts/check_module.py— read-only code check (Python 3.8+, standard library only).
Works with
Any agent that supports the open SKILL.md standard: Claude Code, Codex CLI, Cursor, Gemini CLI and more. The code check needs an agent that can run shell commands; without it, the agent still applies every rule from the guides.
Not included
Migrating a module from one version to another, and server setup or deployment.
Why this beats prompting it yourself
A general-purpose agent mixes the conventions it has seen across Odoo versions: it writes <tree>
on 18.0, _sql_constraints on 19.0 or get_param() on 20.0 — code that looks right and fails
silently or at load time. This skill pins your agent to the version in your manifest, gives it one
verified rule set per version, and checks the result before you install it.
Known limitations
- Odoo 17.0 to 20.0 only; older versions are refused rather than guessed.
- No migration between versions, and no server setup or deployment.
- The code check works on patterns: it can, rarely, flag a line that is correct in context — each finding explains why, so you can decide.
- No Enterprise source code is included; Enterprise-only views and modules are not covered.
How to use
Skills on Agensi run inside your AI agent. Copy the prompt, paste it into your agent, and it connects to Agensi and installs this skill for you. If a payment is needed, your agent shows it to you first.
Reviews
No reviews yet
Be one of the first to try it. Every listed skill passes our trust checks below.
Security scanned
Passed our 8-point scan before listing
Fresh listing
Recently published to Agensi
Free forever
No account required to browse
Trust & safety
Security scanned
Verified clean today
- Free to download with an account
Requires