El Framework ORM de Odoo
El ORM de Odoo 20 representa los modelos mediante recordsets y aplica los permisos del usuario. El ejemplo define todos los modelos y campos que usarán las siguientes lecciones; sus textos base están en inglés para preparar las traducciones.
1. Modelos completos de biblioteca #
Fichero models/biblioteca.py:
1from odoo import api, fields, models, _ 2from odoo.exceptions import ValidationError 3 4 5class LibraryTag(models.Model): 6 _name = 'library.tag' 7 _description = 'Book tag' 8 9 name = fields.Char(required=True) 10 11 12class LibraryBook(models.Model): 13 _name = 'library.book' 14 _description = 'Book' 15 _order = 'name, id' 16 17 name = fields.Char(string='Title', required=True, index=True) 18 author = fields.Char(string='Author') 19 isbn = fields.Char(string='ISBN', size=13) 20 description = fields.Text(string='Synopsis') 21 pages = fields.Integer(string='Pages', default=0) 22 company_id = fields.Many2one('res.company', required=True, 23 default=lambda self: self.env.company) 24 currency_id = fields.Many2one(related='company_id.currency_id') 25 price = fields.Monetary(string='Price', currency_field='currency_id') 26 discounted_price = fields.Monetary(compute='_compute_discounted_price', 27 currency_field='currency_id', store=True) 28 publication_date = fields.Date() 29 state = fields.Selection([ 30 ('draft', 'Draft'), ('available', 'Available'), 31 ('borrowed', 'Borrowed'), ('lost', 'Lost'), 32 ], default='draft', required=True) 33 is_available = fields.Boolean(compute='_compute_is_available', store=True) 34 priority = fields.Selection([ 35 ('low', 'Low'), ('medium', 'Medium'), ('high', 'High'), 36 ], default='low', required=True) 37 partner_id = fields.Many2one('res.partner', string='Publisher', ondelete='restrict') 38 tag_ids = fields.Many2many('library.tag') 39 loan_ids = fields.One2many('library.loan', 'book_id') 40 41 @api.depends('price') 42 def _compute_discounted_price(self): 43 for record in self: 44 record.discounted_price = record.price * 0.90 45 46 @api.depends('state') 47 def _compute_is_available(self): 48 for record in self: 49 record.is_available = record.state == 'available' 50 51 @api.model_create_multi 52 def create(self, vals_list): 53 for vals in vals_list: 54 if not vals.get('name'): 55 vals['name'] = _('Untitled book') 56 return super().create(vals_list) 57 58 @api.constrains('pages') 59 def _check_pages(self): 60 for record in self: 61 if record.pages < 0: 62 raise ValidationError(_('Pages cannot be negative.')) 63 64 def action_make_available(self): 65 self.write({'state': 'available'}) 66 67 68class LibraryLoan(models.Model): 69 _name = 'library.loan' 70 _description = 'Book loan' 71 72 name = fields.Char(string='Reference', required=True, default='/') 73 partner_id = fields.Many2one('res.partner', string='Partner', required=True, 74 ondelete='restrict') 75 book_id = fields.Many2one('library.book', string='Book', required=True, 76 ondelete='restrict') 77 return_date = fields.Date(string='Return date') 78 company_id = fields.Many2one(related='book_id.company_id', store=True) 79 state = fields.Selection([ 80 ('draft', 'Draft'), ('active', 'Active'), ('returned', 'Returned'), 81 ], default='draft', required=True)
Many2one enlaza un registro; One2many necesita el campo inverso real (book_id); Many2many utiliza una tabla de relación. Los modelos referenciados deben existir al cargar el registro de modelos.
El campo is_available deriva de state, evitando dos fuentes de verdad. pages=0 representa una cantidad aún desconocida; los valores negativos se rechazan. Cada método compute asigna un valor para cada registro, también cuando faltan datos.
2. Consultar y modificar recordsets #
Ejecuta estos fragmentos en odoo shell sobre tu base de desarrollo, donde env está disponible:
1books = env['library.book'].search([ 2 ('state', '=', 'available'), ('price', '<', 50), 3]) 4discounted = env['library.book'].search([ 5 '|', ('price', '<', 20), ('state', '=', 'draft'), 6]) 7book = env['library.book'].browse(1).exists() 8if book: 9 book.write({'state': 'available'})
Los operadores lógicos van en notación prefija: | precede a sus dos operandos. browse() construye un recordset; exists() comprueba la existencia del registro. search() aplica filtros y devuelve registros accesibles.
3. Relaciones con Command #
1from odoo.fields import Command 2 3book = env['library.book'].search([], limit=1) 4if book: 5 book.write({'tag_ids': [Command.create({'name': 'Python'})]})
Command.link(id) vincula un registro existente; Command.unlink(id) elimina la relación. En un One2many, este último puede borrar el hijo si el campo inverso tiene ondelete='cascade'; en los demás casos se desvincula cuando el inverso lo permite. Command.delete(id) intenta borrar el registro relacionado, sujeto a permisos y referencias.
@api.model_create_multi permite crear lotes. @api.model sigue recibiendo self: indica que su contenido como recordset no es relevante, no crea un método estático. @api.constrains valida en el servidor cuando se modifican los campos declarados; no es una restricción SQL ni sustituye todas las comprobaciones del flujo de negocio.