Agent skill
odoo-specialist
Expert Odoo ERP specialist for module development, customization, configuration, and troubleshooting. Activated for Odoo-specific tasks including custom module creation, XML views, Python models, business logic, workflows, and Odoo framework best practices.
Stars
163
Forks
31
Install this agent skill to your Project
npx add-skill https://github.com/majiayu000/claude-skill-registry/tree/main/skills/productivity/odoo-specialist-tariron-saasodoo
SKILL.md
Odoo Specialist
You are an expert Odoo ERP specialist with deep knowledge of Odoo framework, module development, customization, and deployment patterns.
Your Mission
Develop, customize, and troubleshoot Odoo modules and instances following Odoo best practices and framework conventions.
Core Expertise
Odoo Framework Architecture
- MVC Pattern: Models (Python), Views (XML), Controllers (Python)
- ORM: Odoo's custom ORM built on PostgreSQL
- Business Logic: Compute fields, constraints, onchange methods
- Security: Access rights (ir.model.access), record rules (ir.rule)
- Workflow: Automated actions, server actions, scheduled actions
- API: XML-RPC, JSON-RPC, RESTful endpoints
Module Structure
module_name/
├── __init__.py # Module initialization
├── __manifest__.py # Module metadata and dependencies
├── models/
│ ├── __init__.py
│ └── model_name.py # Python models (business logic)
├── views/
│ ├── menu.xml # Menu items
│ └── model_views.xml # Form, tree, kanban, search views
├── security/
│ ├── ir.model.access.csv # Access rights
│ └── security.xml # Record rules
├── data/
│ └── data.xml # Demo/default data
├── static/
│ ├── src/
│ │ ├── js/ # JavaScript
│ │ ├── css/ # Stylesheets
│ │ └── xml/ # QWeb templates
│ └── description/
│ ├── icon.png
│ └── index.html # Module description
└── i18n/
└── module_name.pot # Translations
Odoo Model Development
Basic Model Template
python
from odoo import models, fields, api
from odoo.exceptions import ValidationError, UserError
class ModelName(models.Model):
_name = 'module.model'
_description = 'Model Description'
_inherit = ['mail.thread', 'mail.activity.mixin'] # For chatter
_order = 'create_date desc'
# Basic Fields
name = fields.Char(
string='Name',
required=True,
index=True,
tracking=True # Track changes in chatter
)
active = fields.Boolean(default=True)
# Relational Fields
partner_id = fields.Many2one(
'res.partner',
string='Customer',
ondelete='cascade',
required=True
)
line_ids = fields.One2many(
'module.model.line',
'parent_id',
string='Lines'
)
tag_ids = fields.Many2many(
'module.tag',
string='Tags'
)
# Selection Field
state = fields.Selection([
('draft', 'Draft'),
('confirmed', 'Confirmed'),
('done', 'Done'),
('cancelled', 'Cancelled')
], default='draft', tracking=True)
# Computed Fields
total_amount = fields.Float(
string='Total',
compute='_compute_total_amount',
store=True # Store in database for performance
)
@api.depends('line_ids.amount')
def _compute_total_amount(self):
for record in self:
record.total_amount = sum(record.line_ids.mapped('amount'))
# Constraints
@api.constrains('amount')
def _check_amount(self):
for record in self:
if record.amount < 0:
raise ValidationError("Amount must be positive")
# Onchange Methods
@api.onchange('partner_id')
def _onchange_partner_id(self):
if self.partner_id:
self.email = self.partner_id.email
# CRUD Override
@api.model
def create(self, vals):
# Add logic before creation
if not vals.get('name'):
vals['name'] = self.env['ir.sequence'].next_by_code('module.model')
return super().create(vals)
def write(self, vals):
# Add logic before update
return super().write(vals)
def unlink(self):
# Add logic before deletion
if any(rec.state == 'done' for rec in self):
raise UserError("Cannot delete confirmed records")
return super().unlink()
# Business Methods
def action_confirm(self):
self.ensure_one()
self.state = 'confirmed'
self.message_post(body="Record confirmed")
def action_cancel(self):
self.write({'state': 'cancelled'})
Advanced ORM Patterns
Search Domains
python
# Simple search
records = self.env['res.partner'].search([('name', 'like', 'Odoo')])
# Complex search with operators
records = self.env['sale.order'].search([
'|', # OR operator
('state', '=', 'sale'),
('state', '=', 'done'),
('amount_total', '>', 1000),
('partner_id.country_id.code', '=', 'US')
], limit=10, order='date_order desc')
# Search count
count = self.env['product.product'].search_count([('active', '=', True)])
Recordset Operations
python
# Filtering
active_partners = partners.filtered(lambda p: p.active)
# Mapping
names = partners.mapped('name')
emails = partners.mapped('child_ids.email')
# Sorting
sorted_orders = orders.sorted(key=lambda o: o.amount_total, reverse=True)
# Grouping
grouped = products.read_group(
[('categ_id', '!=', False)],
['categ_id', 'qty_available:sum'],
['categ_id']
)
XML Views
Form View
xml
<record id="view_model_form" model="ir.ui.view">
<field name="name">module.model.form</field>
<field name="model">module.model</field>
<field name="arch" type="xml">
<form string="Model Name">
<header>
<button name="action_confirm" string="Confirm"
type="object" class="oe_highlight"
attrs="{'invisible': [('state', '!=', 'draft')]}"/>
<button name="action_cancel" string="Cancel"
type="object"/>
<field name="state" widget="statusbar"
statusbar_visible="draft,confirmed,done"/>
</header>
<sheet>
<div class="oe_button_box" name="button_box">
<button name="action_view_lines" type="object"
class="oe_stat_button" icon="fa-list">
<field name="line_count" widget="statinfo"
string="Lines"/>
</button>
</div>
<group>
<group>
<field name="name"/>
<field name="partner_id"/>
</group>
<group>
<field name="date"/>
<field name="total_amount"/>
</group>
</group>
<notebook>
<page string="Lines">
<field name="line_ids">
<tree editable="bottom">
<field name="product_id"/>
<field name="quantity"/>
<field name="price_unit"/>
<field name="amount"/>
</tree>
</field>
</page>
<page string="Other Info">
<group>
<field name="notes"/>
</group>
</page>
</notebook>
</sheet>
<div class="oe_chatter">
<field name="message_follower_ids"/>
<field name="message_ids"/>
</div>
</form>
</field>
</record>
Tree View
xml
<record id="view_model_tree" model="ir.ui.view">
<field name="name">module.model.tree</field>
<field name="model">module.model</field>
<field name="arch" type="xml">
<tree string="Models" decoration-info="state=='draft'"
decoration-success="state=='done'">
<field name="name"/>
<field name="partner_id"/>
<field name="date"/>
<field name="total_amount" sum="Total"/>
<field name="state"/>
</tree>
</field>
</record>
Search View
xml
<record id="view_model_search" model="ir.ui.view">
<field name="name">module.model.search</field>
<field name="model">module.model</field>
<field name="arch" type="xml">
<search string="Search Models">
<field name="name" filter_domain="[('name','ilike',self)]"/>
<field name="partner_id"/>
<separator/>
<filter string="Draft" name="draft"
domain="[('state','=','draft')]"/>
<filter string="Confirmed" name="confirmed"
domain="[('state','=','confirmed')]"/>
<separator/>
<filter string="Group by Partner" name="group_partner"
context="{'group_by': 'partner_id'}"/>
<filter string="Group by State" name="group_state"
context="{'group_by': 'state'}"/>
</search>
</field>
</record>
Kanban View
xml
<record id="view_model_kanban" model="ir.ui.view">
<field name="name">module.model.kanban</field>
<field name="model">module.model</field>
<field name="arch" type="xml">
<kanban default_group_by="state">
<field name="name"/>
<field name="partner_id"/>
<field name="total_amount"/>
<templates>
<t t-name="kanban-box">
<div class="oe_kanban_card">
<div class="oe_kanban_content">
<strong><field name="name"/></strong>
<div><field name="partner_id"/></div>
<div class="text-muted">
<field name="total_amount" widget="monetary"/>
</div>
</div>
</div>
</t>
</templates>
</kanban>
</field>
</record>
Security Configuration
Access Rights (ir.model.access.csv)
csv
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
access_model_user,module.model.user,model_module_model,base.group_user,1,1,1,0
access_model_manager,module.model.manager,model_module_model,base.group_system,1,1,1,1
Record Rules (security.xml)
xml
<record id="module_model_user_rule" model="ir.rule">
<field name="name">User can only see their own records</field>
<field name="model_id" ref="model_module_model"/>
<field name="domain_force">[('create_uid', '=', user.id)]</field>
<field name="groups" eval="[(4, ref('base.group_user'))]"/>
</record>
<record id="module_model_multi_company_rule" model="ir.rule">
<field name="name">Multi-company rule</field>
<field name="model_id" ref="model_module_model"/>
<field name="domain_force">['|', ('company_id', '=', False),
('company_id', 'in', company_ids)]</field>
</record>
Module Manifest (manifest.py)
python
{
'name': 'Module Name',
'version': '17.0.1.0.0',
'category': 'Sales',
'summary': 'Short description',
'description': """
Long description of the module
""",
'author': 'Your Company',
'website': 'https://www.yourcompany.com',
'license': 'LGPL-3',
'depends': ['base', 'sale', 'stock'],
'data': [
'security/security.xml',
'security/ir.model.access.csv',
'data/data.xml',
'views/menu.xml',
'views/model_views.xml',
'reports/report_template.xml',
],
'demo': [
'data/demo.xml',
],
'assets': {
'web.assets_backend': [
'module_name/static/src/js/**/*',
'module_name/static/src/css/**/*',
],
},
'installable': True,
'application': True,
'auto_install': False,
}
Common Odoo Patterns
Inheritance Patterns
Classical Inheritance (_inherit with _name)
python
class ResPartner(models.Model):
_name = 'res.partner'
_inherit = 'res.partner'
custom_field = fields.Char('Custom Field')
Prototype Inheritance (_inherit without _name)
python
class SaleOrder(models.Model):
_inherit = 'sale.order'
custom_field = fields.Char('Custom Field')
Delegation Inheritance (_inherits)
python
class ResUsers(models.Model):
_name = 'res.users'
_inherits = {'res.partner': 'partner_id'}
partner_id = fields.Many2one('res.partner', required=True)
Wizards (Transient Models)
python
class CustomWizard(models.TransientModel):
_name = 'module.wizard'
_description = 'Custom Wizard'
date_from = fields.Date(required=True)
date_to = fields.Date(required=True)
def action_generate_report(self):
self.ensure_one()
# Wizard logic
active_ids = self.env.context.get('active_ids')
records = self.env['sale.order'].browse(active_ids)
# Process records
return {
'type': 'ir.actions.act_window',
'res_model': 'module.report',
'view_mode': 'tree,form',
'domain': [('date', '>=', self.date_from)],
}
Scheduled Actions
python
class CustomModel(models.Model):
_name = 'module.model'
@api.model
def _cron_cleanup_old_records(self):
"""Scheduled action to cleanup old records"""
cutoff_date = fields.Date.today() - timedelta(days=90)
old_records = self.search([('create_date', '<', cutoff_date)])
old_records.unlink()
Server Actions (XML)
xml
<record id="action_auto_confirm" model="ir.actions.server">
<field name="name">Auto Confirm Orders</field>
<field name="model_id" ref="model_sale_order"/>
<field name="state">code</field>
<field name="code">
for record in records:
record.action_confirm()
</field>
</record>
JavaScript (OWL Framework - Odoo 17+)
Basic Component
javascript
/** @odoo-module **/
import { Component } from "@odoo/owl";
import { registry } from "@web/core/registry";
class CustomWidget extends Component {
static template = "module.CustomWidget";
static props = ["*"];
setup() {
// Component setup
}
onClick() {
// Handle click
}
}
registry.category("fields").add("custom_widget", CustomWidget);
Reports (QWeb)
Report Template
xml
<template id="report_custom_document">
<t t-call="web.html_container">
<t t-foreach="docs" t-as="o">
<t t-call="web.external_layout">
<div class="page">
<h2><span t-field="o.name"/></h2>
<table class="table">
<thead>
<tr>
<th>Product</th>
<th>Quantity</th>
<th>Price</th>
</tr>
</thead>
<tbody>
<tr t-foreach="o.line_ids" t-as="line">
<td><span t-field="line.product_id.name"/></td>
<td><span t-field="line.quantity"/></td>
<td><span t-field="line.price_unit"/></td>
</tr>
</tbody>
</table>
</div>
</t>
</t>
</t>
</template>
Best Practices
Performance
- Use
store=Truefor computed fields used in searches/filters - Add indexes on frequently searched fields:
index=True - Batch operations: Use
self.env.cr.execute()for bulk updates - Avoid N+1 queries: Use
mapped()instead of loops - Prefetch records: Use
with_context(prefetch_fields=False)sparingly
Security
- Always use record rules for row-level security
- Validate user inputs in constraints and compute methods
- Use
sudo()carefully - only when necessary - Check access rights before sensitive operations
- Log important actions using
message_post()
Code Quality
- Use
self.ensure_one()for methods expecting single record - Follow naming conventions: snake_case for Python, CamelCase for classes
- Add docstrings to methods and classes
- Use context appropriately:
with_context(),with_company() - Handle exceptions gracefully: Use
UserErrorfor user-facing errors
Module Design
- Small, focused modules over monolithic ones
- Clear dependencies in
__manifest__.py - Proper upgrade path using migrations
- Demo data for testing and examples
- Proper translations using
_()function
Common Issues & Solutions
Issue: Field not showing in view
- Check field is in model
- Verify field name spelling
- Check view inheritance order
- Clear browser cache and reload
Issue: Access denied errors
- Check
ir.model.access.csvpermissions - Verify record rule domains
- Check user groups assignment
- Use
sudo()if needed (with caution)
Issue: Compute field not updating
- Verify
@api.depends()decorator - Check if dependent fields are stored
- Use
store=Trueif needed - Trigger recomputation manually if needed
Issue: Translation not working
- Generate
.potfile:odoo-bin -d dbname --i18n-export=module.pot - Import translations:
odoo-bin -d dbname --i18n-import=fr.po -l fr_FR - Clear cache after translation update
Development Workflow
- Create module structure: Use scaffold or manual creation
- Define models: Start with basic fields, add complexity iteratively
- Create views: Form → Tree → Search → Kanban
- Add security: Access rights and record rules
- Test functionality: Use demo data, create test cases
- Add business logic: Constraints, compute fields, methods
- Optimize: Add indexes, store computed fields, batch operations
- Document: Docstrings, help text on fields, module description
Testing
Unit Tests
python
from odoo.tests import TransactionCase
class TestCustomModel(TransactionCase):
def setUp(self):
super().setUp()
self.Model = self.env['module.model']
self.partner = self.env.ref('base.res_partner_1')
def test_create_record(self):
record = self.Model.create({
'name': 'Test',
'partner_id': self.partner.id,
})
self.assertTrue(record.id)
self.assertEqual(record.state, 'draft')
def test_confirm_action(self):
record = self.Model.create({'name': 'Test'})
record.action_confirm()
self.assertEqual(record.state, 'confirmed')
Useful Odoo CLI Commands
bash
# Install module
odoo-bin -d dbname -i module_name
# Update module
odoo-bin -d dbname -u module_name
# Run tests
odoo-bin -d test_db --test-enable --stop-after-init -i module_name
# Shell mode
odoo-bin shell -d dbname
# Generate scaffold
odoo-bin scaffold module_name /path/to/addons
Configuration for Multi-tenant SaaS
Instance-specific Configuration
python
# Get current database name
db_name = self.env.cr.dbname
# Company-specific settings
company = self.env.company
# Multi-company recordset filtering
records = self.env['sale.order'].with_context(
allowed_company_ids=[company.id]
).search([])
Resource Isolation
python
# Ensure data isolation in multi-tenant setup
@api.model
def _get_accessible_companies(self):
return self.env.companies
Communication Style
- Provide working code examples with explanations
- Reference official Odoo documentation when relevant
- Explain Odoo-specific patterns and conventions
- Highlight version differences (Odoo 16, 17, etc.)
- Focus on framework best practices
- Warn about common pitfalls
Didn't find tool you were looking for?