Skip to main content

odoo-orm-patterns

Odoo 18 ORM operations, recordsets, domains, and compute patterns

Quellinformationen

Repository
Insightpulseai/odoo
Letzte Quellaktivität
9. April 2026 um 11:24
Erkannte Sprache von SKILL.md
Englisch
Sterne
6
Forks
2

Installationsoptionen

Standardmäßig ist der Prompt ausgewählt, der zuerst die Quelle prüft. Sie können zu einem direkten Befehl wechseln oder eine lokale Kopie herunterladen.

Quelldateien prüfen

Lesen Sie SKILL.md und alle von SkillsMP angezeigten Begleitdateien, bevor Sie sich für eine Installation entscheiden.

SKILL.md wird angezeigt

SKILL.md
Quellanweisungen · Schreibgeschützte Vorschau
name
odoo_orm_patterns
description
Odoo 18 ORM operations, recordsets, domains, and compute patterns
category
backend
priority
critical
version
1.0
# ORM Patterns ## Recordset Operations ### Read Operations ```python # Browse by ID (returns recordset, no DB query until field access) record = self.env['res.partner'].browse(42) records = self.env['res.partner'].browse([1, 2, 3]) # Search (returns recordset) partners = self.env['res.partner'].search([ ('is_company', '=', True), ('country_id.code', '=', 'PH'), ], limit=10, order='name ASC') # Search and read (returns list of dicts — use for reporting/export only) data = self.env['res.partner'].search_read( domain=[('is_company', '=', True)], fields=['name', 'email', 'phone'], limit=10, ) # Count count = self.env['res.partner'].search_count([ ('is_company', '=', True), ]) ``` ### Write Operations ```python # Create single record partner = self.env['res.partner'].create({ 'name': 'Test Partner', 'email': 'test@example.com', }) # Create multiple records (batch — preferred) partners = self.env['res.partner'].create([ {'name': 'Partner A'}, {'name': 'Partner B'}, ]) # Update partner.write({'phone': '+63 2 1234 5678'}) # Delete partner.unlink() # Copy (duplicate with defaults) new_partner = partner.copy(default={'name': 'Copy of Partner'}) ``` ### Recordset Methods ```python # Filter (returns subset matching predicate) active_partners = partners.filtered(lambda p: p.active and p.email) # Also accepts string for simple field truthy check active_partners = partners.filtered('active') # Map (returns recordset for relational fields, list for others) emails = partners.mapped('email') # ['a@b.com', 'c@d.com'] countries = partners.mapped('country_id') # recordset of res.country child_emails = partners.mapped('child_ids.email') # dotted path traversal # Sort sorted_partners = partners.sorted(key=lambda p: p.name) sorted_partners = partners.sorted('name', reverse=True) # Exists (filter out deleted records) if record.exists(): record.do_something() # Set operations combined = records_a | records_b # union common = records_a & records_b # intersection diff = records_a - records_b # difference # Iteration for partner in partners: partner.do_something() # each iteration: single-record recordset # Length and membership len(partners) # number of records partner in partners # membership test ``` **NEVER** use list comprehensions on recordsets when `filtered` or `mapped` can do the job: ```python # WRONG emails = [p.email for p in partners if p.email] # CORRECT emails = partners.filtered('email').mapped('email') ``` ## Domain Syntax Domains are lists of criteria, using Polish (prefix) notation for operators. ### Basic Criteria ```python # Simple comparisons [('field', '=', value)] [('field', '!=', value)] [('field', '<', value)] [('field', '>', value)] [('field', '<=', value)] [('field', '>=', value)] # String matching [('name', 'like', 'test')] # case-sensitive, %test% [('name', 'ilike', 'test')] # case-insensitive, %test% [('name', '=like', 'test%')] # case-sensitive, exact pattern [('name', '=ilike', 'Test%')] # case-insensitive, exact pattern # List membership [('state', 'in', ['draft', 'confirmed'])] [('state', 'not in', ['cancelled'])] # Hierarchy [('category_id', 'child_of', parent_id)] [('category_id', 'parent_of', child_id)] ``` ### Boolean Operators Default operator between criteria is `&` (AND). Use prefix notation: ```python # AND (default — implicit between all criteria) [('state', '=', 'draft'), ('active', '=', True)] # Equivalent to: ['&', ('state', '=', 'draft'), ('active', '=', True)] # OR ['|', ('state', '=', 'draft'), ('state', '=', 'confirmed')] # NOT ['!', ('active', '=', True)] # Complex: (state = draft OR state = confirmed) AND active = True ['|', ('state', '=', 'draft'), ('state', '=', 'confirmed'), ('active', '=', True)] # Complex: state = draft AND (type = sale OR type = purchase) [('state', '=', 'draft'), '|', ('type', '=', 'sale'), ('type', '=', 'purchase')] ``` ### Dotted Paths (Related Fields) ```python # Traverse relational fields with dots [('partner_id.country_id.code', '=', 'PH')] [('order_line.product_id.categ_id', 'child_of', category_id)] ``` ## Compute Fields ### Basic Compute ```python total = fields.Float( string='Total', compute='_compute_total', store=True, # persist to DB (required for search/group_by) readonly=True, # default for compute, explicit for clarity ) @api.depends('line_ids.price', 'line_ids.qty') def _compute_total(self): for record in self: record.total = sum( line.price * line.qty for line in record.line_ids ) ``` ### Compute with Inverse ```python display_name = fields.Char( compute='_compute_display_name', inverse='_inverse_display_name', store=True, ) @api.depends('first_name', 'last_name') def _compute_display_name(self): for record in self: record.display_name = f'{record.first_name} {record.last_name}' def _inverse_display_name(self): for record in self: parts = (record.display_name or '').split(' ', 1) record.first_name = parts[0] record.last_name = parts[1] if len(parts) > 1 else '' ``` ### Compute Rules ```python # CORRECT: Always loop over self @api.depends('amount', 'tax_rate') def _compute_tax(self): for record in self: record.tax_amount = record.amount * record.tax_rate # WRONG: Assigning without loop (breaks on multi-record recordsets) @api.depends('amount', 'tax_rate') def _compute_tax(self): self.tax_amount = self.amount * self.tax_rate # CRASHES on multi-record ``` - `@api.depends()` is mandatory for stored compute fields - `@api.depends()` triggers recomputation when listed fields change - For non-stored compute fields, `@api.depends()` is optional but recommended - `@api.depends_context('key')` for context-dependent computes (e.g., language, company) ## Context Context is an immutable dictionary carried through the ORM call chain. ### Reading Context ```python # Current context ctx = self.env.context # Specific key lang = self.env.context.get('lang', 'en_US') active_id = self.env.context.get('active_id') active_ids = self.env.context.get('active_ids', []) active_model = self.env.context.get('active_model') ``` ### Modifying Context ```python # CORRECT: with_context returns a NEW recordset with modified context records = self.with_context(tracking_disable=True) records.write({'name': 'Updated'}) # Add to existing context records = self.with_context(**self.env.context, custom_key='value') # WRONG: Never modify context dict directly self.env.context['key'] = 'value' # FAILS — context is frozen ``` ### Common Context Keys | Key | Purpose | |-----|---------| | `no_reset_password` | Skip password reset email on user create | | `mail_create_nosubscribe` | Don't subscribe creator to record | | `mail_notrack` | Disable field tracking (chatter) | | `tracking_disable` | Disable all tracking and mail | | `active_test` | `False` to include archived records in search | | `default_<field>` | Set default value for field | | `search_default_<filter>` | Activate a search filter | | `allowed_company_ids` | List of companies user can access | | `force_company` | Force specific company context | ## Command Tuples (One2many / Many2many Writes) When writing to `One2many` or `Many2many` fields, use command tuples: ```python # (0, 0, vals) — Create a new record and link it record.write({'line_ids': [(0, 0, {'name': 'New Line', 'amount': 100})]}) # (1, id, vals) — Update an existing linked record record.write({'line_ids': [(1, line_id, {'amount': 200})]}) # (2, id, 0) — Delete the linked record from DB record.write({'line_ids': [(2, line_id, 0)]}) # (3, id, 0) — Unlink (M2M only: remove relation, keep record) record.write({'tag_ids': [(3, tag_id, 0)]}) # (4, id, 0) — Link existing record (M2M: add relation) record.write({'tag_ids': [(4, tag_id, 0)]}) # (5, 0, 0) — Clear all links (M2M: unlink all, O2M: delete all) record.write({'tag_ids': [(5, 0, 0)]}) # (6, 0, [ids]) — Replace: clear all, then link these IDs record.write({'tag_ids': [(6, 0, [tag1_id, tag2_id])]}) ``` ### Quick Reference Table | Command | Args | Effect | O2M | M2M | |---------|------|--------|-----|-----| | `(0, 0, vals)` | dict | Create + link | Yes | Yes | | `(1, id, vals)` | id, dict | Update linked | Yes | Yes | | `(2, id, 0)` | id | Delete from DB | Yes | Yes | | `(3, id, 0)` | id | Remove link only | No | Yes | | `(4, id, 0)` | id | Add link | No | Yes | | `(5, 0, 0)` | — | Clear all | Yes | Yes | | `(6, 0, ids)` | id list | Replace all | No | Yes | ### Combining Commands
Auf GitHub ansehen
Diese SKILL.md ist sehr gross, daher zeigt SkillsMP hier nur den ersten Abschnitt. Auf GitHub ansehen