From odoo-dev-toolkit
Write Odoo 18 Python code — models, fields, methods, controllers, wizards, computed fields, constraints, and API decorators. Use this skill when the user wants to write or modify Odoo Python code. Trigger on: "model", "controller", "wizard", "computed field", "onchange", "constraint", "Odoo Python", "API endpoint", "Odoo method", "business logic", "napsat model", "vytvořit model", "Odoo kód", "Python pro Odoo", "kontroler", "wizard", "průvodce", "vypočítané pole", "business logika", "Odoo API".
How this skill is triggered — by the user, by Claude, or both
Slash command
/odoo-dev-toolkit:odoo-pythonThe summary Claude sees in its skill listing — used to decide when to auto-load this skill
Write production-ready Odoo 18 Python code for models, controllers, wizards, and business logic. Every piece of code follows Odoo 18 conventions and PEP 8.
Write production-ready Odoo 18 Python code for models, controllers, wizards, and business logic. Every piece of code follows Odoo 18 conventions and PEP 8.
Read references/python-patterns.md for detailed code patterns before writing.
Use the latest ORM patterns:
Command constants for One2many/Many2many writes (not tuples)invisible/readonly/required in views (no attrs dict)@api.depends for computed fields, @api.onchange sparingly@api.constrains for validationAlways consider:
sudo() only when explicitly needed, with clear comments whycheck_access_rights / check_access_rule when bypassing ORMbrowse in loops — use search + batch processingread_group for aggregations instead of reading all recordsfields parameter in search_readwith_context(prefetch_fields=False) for large batch operationsfrom odoo import models, fields, api, _
from odoo.exceptions import UserError, ValidationError
class ModelName(models.Model):
_name = 'module.model.name'
_description = 'Human Readable Name'
_inherit = ['mail.thread', 'mail.activity.mixin']
_order = 'date desc, id desc'
_rec_name = 'name'
name = fields.Char(string="Name", required=True, tracking=True)
active = fields.Boolean(default=True)
state = fields.Selection([
('draft', 'Draft'),
('confirmed', 'Confirmed'),
('done', 'Done'),
('cancel', 'Cancelled'),
], string="Status", default='draft', required=True, tracking=True)
# ... more fields
class SaleOrder(models.Model):
_inherit = 'sale.order'
custom_field = fields.Char(string="Custom Field")
def action_confirm(self):
"""Override to add custom logic on confirmation."""
res = super().action_confirm()
for order in self:
# custom logic
pass
return res
class ModelWizard(models.TransientModel):
_name = 'module.model.wizard'
_description = 'Wizard Description'
# Wizard fields
date_from = fields.Date(required=True, default=fields.Date.today)
partner_id = fields.Many2one('res.partner', required=True)
def action_process(self):
"""Main wizard action."""
self.ensure_one()
# process logic
return {'type': 'ir.actions.act_window_close'}
| Field | Python | Common Parameters |
|---|---|---|
Char | fields.Char() | size, trim, translate |
Text | fields.Text() | translate |
Html | fields.Html() | sanitize, translate |
Integer | fields.Integer() | group_operator |
Float | fields.Float() | digits, group_operator |
Monetary | fields.Monetary() | currency_field (default: currency_id) |
Boolean | fields.Boolean() | |
Date | fields.Date() | |
Datetime | fields.Datetime() | |
Selection | fields.Selection() | selection (list of tuples) |
Many2one | fields.Many2one() | comodel_name, ondelete, domain |
One2many | fields.One2many() | comodel_name, inverse_name |
Many2many | fields.Many2many() | comodel_name, relation, column1, column2 |
Binary | fields.Binary() | attachment |
Image | fields.Image() | max_width, max_height |
fields.Char(
string="Display Label", # UI label
required=True, # mandatory
readonly=True, # read-only
index=True, # database index
default="value", # static default
default=lambda self: ..., # dynamic default
help="Tooltip text", # help tooltip
tracking=True, # track changes in chatter
copy=False, # don't copy on duplicate
groups="base.group_system", # field-level access
company_dependent=True, # multi-company field
translate=True, # translatable
)
amount_total = fields.Float(
string="Total",
compute='_compute_amount_total',
store=True, # stored = updated on dependency change
readonly=True,
)
@api.depends('line_ids.subtotal', 'discount')
def _compute_amount_total(self):
for record in self:
record.amount_total = sum(record.line_ids.mapped('subtotal')) - record.discount
name_display = fields.Char(compute='_compute_name_display', inverse='_inverse_name_display')
def _compute_name_display(self):
for rec in self:
rec.name_display = f"{rec.first_name} {rec.last_name}"
def _inverse_name_display(self):
for rec in self:
parts = (rec.name_display or '').split(' ', 1)
rec.first_name = parts[0]
rec.last_name = parts[1] if len(parts) > 1 else ''
from odoo import http
from odoo.http import request
class CustomController(http.Controller):
@http.route('/custom/endpoint', type='http', auth='user', website=True)
def custom_page(self, **kwargs):
"""Render a website page."""
values = {
'records': request.env['model.name'].search([]),
}
return request.render('module_name.template_name', values)
@http.route('/api/custom', type='json', auth='user', methods=['POST'])
def api_endpoint(self, **kwargs):
"""JSON API endpoint."""
data = request.jsonrequest
# process data
return {'status': 'ok', 'result': ...}
@http.route('/custom/download', type='http', auth='user')
def download_report(self, record_id, **kwargs):
"""File download endpoint."""
record = request.env['model.name'].browse(int(record_id))
record.check_access_rights('read')
record.check_access_rule('read')
# generate file content
return request.make_response(
file_content,
headers=[
('Content-Type', 'application/pdf'),
('Content-Disposition', 'attachment; filename="report.pdf"'),
]
)
| Parameter | Options | Default |
|---|---|---|
type | 'http', 'json' | 'http' |
auth | 'user', 'public', 'none' | 'user' |
methods | ['GET'], ['POST'], ['GET', 'POST'] | All methods |
website | True, False | False |
csrf | True, False | True |
cors | '*' or specific origin | None |
from odoo.exceptions import ValidationError
@api.constrains('date_start', 'date_end')
def _check_dates(self):
for record in self:
if record.date_end and record.date_start and record.date_end < record.date_start:
raise ValidationError(_("End date must be after start date."))
_sql_constraints = [
('name_unique', 'UNIQUE(name, company_id)',
'Name must be unique per company!'),
('amount_positive', 'CHECK(amount >= 0)',
'Amount must be positive!'),
]
from odoo import Command
# In Odoo 18, use Command constants:
record.write({
'line_ids': [
Command.create({'name': 'New Line', 'quantity': 1}), # (0, 0, vals)
Command.update(line_id, {'quantity': 5}), # (1, id, vals)
Command.delete(line_id), # (2, id)
Command.unlink(line_id), # (3, id) - M2M only
Command.link(line_id), # (4, id) - M2M only
Command.clear(), # (5,)
Command.set([id1, id2, id3]), # (6, 0, ids) - M2M
]
})
Save Python files in the appropriate directory:
models/model_name.pycontrollers/main.pywizards/wizard_name.pyAlways update the corresponding __init__.py to import new files.
Provides behavioral guidelines to reduce common LLM coding mistakes, focusing on simplicity, surgical changes, assumption surfacing, and verifiable success criteria.
Searches, retrieves, and installs Agent Skills from prompts.chat registry using MCP tools like search_skills and get_skill. Activates for finding skills, browsing catalogs, or extending Claude.
Creates, edits, and optimizes skills for Claude Code, including drafting, evaluating with test prompts, iterating on performance, and improving skill descriptions for better triggering accuracy.
npx claudepluginhub michalvarys/claude-plugins --plugin odoo-dev-toolkit