# -*- coding: utf-8 -*- # Part of Odoo. See LICENSE file for full copyright and licensing details. import datetime import logging from odoo import Command, api, fields, models from odoo.exceptions import AccessError, ValidationError from odoo.tools import SQL from odoo.tools.float_utils import float_round from odoo.tools.translate import html_translate _logger = logging.getLogger('precompute_setter') class Category(models.Model): _name = 'test_new_api.category' _description = 'Test New API Category' _order = 'name' _parent_store = True _parent_name = 'parent' name = fields.Char(required=True) color = fields.Integer('Color Index') parent = fields.Many2one('test_new_api.category', ondelete='cascade') parent_path = fields.Char(index=True) depth = fields.Integer(compute="_compute_depth") root_categ = fields.Many2one(_name, compute='_compute_root_categ') display_name = fields.Char( compute='_compute_display_name', inverse='_inverse_display_name', search='_search_display_name', recursive=True, ) dummy = fields.Char(store=False) discussions = fields.Many2many('test_new_api.discussion', 'test_new_api_discussion_category', 'category', 'discussion') _sql_constraints = [ ('positive_color', 'CHECK(color >= 0)', 'The color code must be positive!') ] @api.depends('name', 'parent.display_name') # this definition is recursive def _compute_display_name(self): for cat in self: if cat.parent: cat.display_name = cat.parent.display_name + ' / ' + cat.name else: cat.display_name = cat.name @api.depends('parent') def _compute_root_categ(self): for cat in self: current = cat while current.parent: current = current.parent cat.root_categ = current @api.depends('parent_path') def _compute_depth(self): for cat in self: cat.depth = cat.parent_path.count('/') - 1 def _inverse_display_name(self): for cat in self: names = cat.display_name.split('/') # determine sequence of categories categories = [] for name in names[:-1]: category = self.search([('name', 'ilike', name.strip())]) categories.append(category[0]) categories.append(cat) # assign parents following sequence for parent, child in zip(categories, categories[1:]): if parent and child: child.parent = parent # assign name of last category, and reassign display_name (to normalize it) cat.name = names[-1].strip() def _fetch_query(self, query, fields): # DLE P45: `test_31_prefetch`, # with self.assertRaises(AccessError): # cat1.name if self.search_count([('id', 'in', self._ids), ('name', '=', 'NOACCESS')]): raise AccessError('Sorry') return super()._fetch_query(query, fields) class Discussion(models.Model): _name = 'test_new_api.discussion' _description = 'Test New API Discussion' name = fields.Char(string='Title', required=True, help="Description of discussion.") moderator = fields.Many2one('res.users') categories = fields.Many2many('test_new_api.category', 'test_new_api_discussion_category', 'discussion', 'category') participants = fields.Many2many('res.users', context={'active_test': False}) messages = fields.One2many('test_new_api.message', 'discussion', copy=True) message_concat = fields.Text(string='Message concatenate') important_messages = fields.One2many('test_new_api.message', 'discussion', domain=[('important', '=', True)]) very_important_messages = fields.One2many( 'test_new_api.message', 'discussion', domain=lambda self: self._domain_very_important()) emails = fields.One2many('test_new_api.emailmessage', 'discussion') important_emails = fields.One2many('test_new_api.emailmessage', 'discussion', domain=[('important', '=', True)]) history = fields.Json('History', default={'delete_messages': []}) attributes_definition = fields.PropertiesDefinition('Message Properties') # see message@attributes def _domain_very_important(self): """Ensure computed O2M domains work as expected.""" return [("important", "=", True)] @api.onchange('name') def _onchange_name(self): # test onchange modifying one2many field values if self.env.context.get('generate_dummy_message') and self.name == '{generate_dummy_message}': # update body of existings messages and emails for message in self.messages: message.body = 'not last dummy message' for message in self.important_messages: message.body = 'not last dummy message' # add new dummy message message_vals = self.messages._add_missing_default_values({'body': 'dummy message', 'important': True}) self.messages |= self.messages.new(message_vals) self.important_messages |= self.messages.new(message_vals) @api.onchange('moderator') def _onchange_moderator(self): self.participants |= self.moderator @api.onchange('messages') def _onchange_messages(self): self.message_concat = "\n".join(["%s:%s" % (m.name, m.body) for m in self.messages]) class Message(models.Model): _name = 'test_new_api.message' _description = 'Test New API Message' discussion = fields.Many2one('test_new_api.discussion', ondelete='cascade') body = fields.Text(index='trigram') author = fields.Many2one('res.users', default=lambda self: self.env.user) name = fields.Char(string='Title', compute='_compute_name', store=True) display_name = fields.Char(string='Abstract', compute='_compute_display_name') size = fields.Integer(compute='_compute_size', search='_search_size') double_size = fields.Integer(compute='_compute_double_size') discussion_name = fields.Char(related='discussion.name', string="Discussion Name", readonly=False) author_partner = fields.Many2one( 'res.partner', compute='_compute_author_partner', search='_search_author_partner') important = fields.Boolean() label = fields.Char(translate=True) priority = fields.Integer() active = fields.Boolean(default=True) has_important_sibling = fields.Boolean(compute='_compute_has_important_sibling') attributes = fields.Properties( string='Properties', definition='discussion.attributes_definition', ) @api.depends('discussion.messages.important') def _compute_has_important_sibling(self): for record in self: siblings = record.discussion.with_context(active_test=False).messages - record record.has_important_sibling = any(siblings.mapped('important')) @api.constrains('author', 'discussion') def _check_author(self): for message in self.with_context(active_test=False): if message.discussion and message.author not in message.discussion.sudo().participants: raise ValidationError(self.env._("Author must be among the discussion participants.")) @api.depends('author.name', 'discussion.name') def _compute_name(self): for message in self: message.name = self._context.get('compute_name', "[%s] %s" % (message.discussion.name or '', message.author.name or '')) @api.constrains('name') def _check_name(self): # dummy constraint to check on computed field for message in self: if message.name.startswith("[X]"): raise ValidationError("No way!") @api.depends('author.name', 'discussion.name', 'body') def _compute_display_name(self): for message in self: stuff = "[%s] %s: %s" % (message.author.name, message.discussion.name or '', message.body or '') message.display_name = stuff[:80] @api.depends('body') def _compute_size(self): for message in self: message.size = len(message.body or '') def _search_size(self, operator, value): if operator not in ('=', '!=', '<', '<=', '>', '>=', 'in', 'not in'): return [] # retrieve all the messages that match with a specific SQL query self.flush_model(['body']) query = """SELECT id FROM "%s" WHERE char_length("body") %s %%s""" % \ (self._table, operator) self.env.cr.execute(query, (value,)) ids = [t[0] for t in self.env.cr.fetchall()] # return domain with an implicit AND return [('id', 'in', ids), (1, '=', 1)] @api.depends('size') def _compute_double_size(self): for message in self: # This illustrates a subtle situation: message.double_size depends # on message.size. When the latter is computed, message.size is # assigned, which would normally invalidate message.double_size. # However, this may not happen while message.double_size is being # computed: the last statement below would fail, because # message.double_size would be undefined. message.double_size = 0 size = message.size message.double_size = message.double_size + size @api.depends('author', 'author.partner_id') def _compute_author_partner(self): for message in self: message.author_partner = message.author.partner_id @api.model def _search_author_partner(self, operator, value): return [('author.partner_id', operator, value)] def write(self, vals): if 'priority' in vals: vals['priority'] = 5 return super().write(vals) class EmailMessage(models.Model): _name = 'test_new_api.emailmessage' _description = 'Test New API Email Message' _inherits = {'test_new_api.message': 'message'} message = fields.Many2one('test_new_api.message', 'Message', required=True, ondelete='cascade') email_to = fields.Char('To') active = fields.Boolean('Active Message', related='message.active', store=True, related_sudo=False) class DiscussionPartner(models.Model): """ Simplified model for partners. Having a specific model avoids all the overrides from other modules that may change which fields are being read, how many queries it takes to use that model, etc. """ _name = 'test_new_api.partner' _description = 'Discussion Partner' name = fields.Char(string='Name') class Multi(models.Model): """ Model for testing multiple onchange methods in cascade that modify a one2many field several times. """ _name = 'test_new_api.multi' _description = 'Test New API Multi' name = fields.Char(related='partner.name', readonly=True) partner = fields.Many2one('res.partner') lines = fields.One2many('test_new_api.multi.line', 'multi') partners = fields.One2many(related='partner.child_ids') tags = fields.Many2many('test_new_api.multi.tag', domain=[('name', 'ilike', 'a')]) @api.onchange('name') def _onchange_name(self): for line in self.lines: line.name = self.name @api.onchange('partner') def _onchange_partner(self): for line in self.lines: line.partner = self.partner @api.onchange('tags') def _onchange_tags(self): for line in self.lines: line.tags |= self.tags class MultiLine(models.Model): _name = 'test_new_api.multi.line' _description = 'Test New API Multi Line' multi = fields.Many2one('test_new_api.multi', ondelete='cascade') name = fields.Char() partner = fields.Many2one(related='multi.partner', store=True) tags = fields.Many2many('test_new_api.multi.tag') class MultiLine2(models.Model): _name = 'test_new_api.multi.line2' _inherit = 'test_new_api.multi.line' _description = 'Test New API Multi Line 2' class MultiTag(models.Model): _name = 'test_new_api.multi.tag' _description = 'Test New API Multi Tag' name = fields.Char() @api.depends('name') @api.depends_context('special_tag') def _compute_display_name(self): for record in self: name = record.name if name and self.env.context.get('special_tag'): name += "!" record.display_name = name or "" class Edition(models.Model): _name = 'test_new_api.creativework.edition' _description = 'Test New API Creative Work Edition' name = fields.Char() res_id = fields.Integer(required=True) res_model_id = fields.Many2one('ir.model', required=True, ondelete='cascade') res_model = fields.Char(related='res_model_id.model', store=True, readonly=False) class Book(models.Model): _name = 'test_new_api.creativework.book' _description = 'Test New API Creative Work Book' name = fields.Char() editions = fields.One2many( 'test_new_api.creativework.edition', 'res_id', domain=[('res_model', '=', _name)] ) class Movie(models.Model): _name = 'test_new_api.creativework.movie' _description = 'Test New API Creative Work Movie' name = fields.Char() editions = fields.One2many( 'test_new_api.creativework.edition', 'res_id', domain=[('res_model', '=', _name)] ) class MixedModel(models.Model): _name = 'test_new_api.mixed' _description = 'Test New API Mixed' foo = fields.Char() text = fields.Text() truth = fields.Boolean() count = fields.Integer() number = fields.Float(digits=(10, 2), default=3.14) number2 = fields.Float(digits='New API Precision') date = fields.Date() moment = fields.Datetime() now = fields.Datetime(compute='_compute_now') lang = fields.Selection(string='Language', selection='_get_lang') reference = fields.Reference(string='Related Document', selection='_reference_models') comment1 = fields.Html(sanitize=False) comment2 = fields.Html(sanitize_attributes=True, strip_classes=False) comment3 = fields.Html(sanitize_attributes=True, strip_classes=True) comment4 = fields.Html(sanitize_attributes=True, strip_style=True) comment5 = fields.Html(sanitize_overridable=True, sanitize_attributes=False) currency_id = fields.Many2one('res.currency', default=lambda self: self.env.ref('base.EUR')) amount = fields.Monetary() def _compute_now(self): # this is a non-stored computed field without dependencies for message in self: message.now = fields.Datetime.now() @api.model def _get_lang(self): return self.env['res.lang'].get_installed() @api.model def _reference_models(self): models = self.env['ir.model'].sudo().search([('state', '!=', 'manual')]) return [(model.model, model.name) for model in models if not model.model.startswith('ir.')] class BoolModel(models.Model): _name = 'domain.bool' _description = 'Boolean Domain' bool_true = fields.Boolean('b1', default=True) bool_false = fields.Boolean('b2', default=False) bool_undefined = fields.Boolean('b3') class Foo(models.Model): _name = 'test_new_api.foo' _description = 'Test New API Foo' name = fields.Char() value1 = fields.Integer(change_default=True) value2 = fields.Integer() text = fields.Char(trim=False) class Bar(models.Model): _name = 'test_new_api.bar' _description = 'Test New API Bar' name = fields.Char() foo = fields.Many2one('test_new_api.foo', compute='_compute_foo', search='_search_foo') value1 = fields.Integer(related='foo.value1', readonly=False) value2 = fields.Integer(related='foo.value2', readonly=False) text1 = fields.Char('Text1', related='foo.text', readonly=False) text2 = fields.Char('Text2', related='foo.text', readonly=False, trim=True) @api.depends('name') def _compute_foo(self): for bar in self: bar.foo = self.env['test_new_api.foo'].search([('name', '=', bar.name)], limit=1) def _search_foo(self, operator, value): assert operator == 'in' records = self.env['test_new_api.foo'].browse(value) return [('name', 'in', records.mapped('name'))] class Related(models.Model): _name = 'test_new_api.related' _description = 'Test New API Related' name = fields.Char() # related fields with a single field related_name = fields.Char(related='name', string='A related on Name', readonly=False) related_related_name = fields.Char(related='related_name', string='A related on a related on Name', readonly=False) message = fields.Many2one('test_new_api.message') message_name = fields.Text(related="message.body", related_sudo=False, string='Message Body') message_currency = fields.Many2one(related="message.author", string='Message Author') foo_id = fields.Many2one('test_new_api.related_foo') foo_name = fields.Char('foo_name', related='foo_id.name', related_sudo=False) foo_name_sudo = fields.Char('foo_name_sudo', related='foo_id.name', related_sudo=True) foo_bar_name = fields.Char('foo_bar_name', related='foo_id.bar_id.name', related_sudo=False) foo_bar_name_sudo = fields.Char('foo_bar_name_sudo', related='foo_id.bar_id.name', related_sudo=True) foo_id_bar_name = fields.Char('foo_id_bar_name', related='foo_id.bar_name', related_sudo=False) foo_bar_id = fields.Many2one(string='foo_bar_id', related='foo_id.bar_id', related_sudo=False) foo_bar_id_name = fields.Char('foo_bar_id_name', related='foo_bar_id.name', related_sudo=False) foo_bar_sudo_id = fields.Many2one(string='foo_bar_sudo_id', related='foo_id.bar_id', related_sudo=True) foo_bar_sudo_id_name = fields.Char('foo_bar_sudo_id_name', related='foo_bar_sudo_id.name', related_sudo=False) class RelatedFoo(models.Model): _name = _description = 'test_new_api.related_foo' name = fields.Char() bar_id = fields.Many2one('test_new_api.related_bar') bar_name = fields.Char('bar_name', related='bar_id.name', related_sudo=False) class RelatedBar(models.Model): _name = _description = 'test_new_api.related_bar' name = fields.Char() class RelatedInherits(models.Model): _name = _description = 'test_new_api.related_inherits' _inherits = {'test_new_api.related': 'base_id'} base_id = fields.Many2one('test_new_api.related', required=True, ondelete='cascade') class ComputeReadonly(models.Model): _name = 'test_new_api.compute.readonly' _description = 'Model with a computed readonly field' foo = fields.Char(default='') bar = fields.Char(compute='_compute_bar', store=True) @api.depends('foo') def _compute_bar(self): for record in self: record.bar = record.foo class ComputeInverse(models.Model): _name = 'test_new_api.compute.inverse' _description = 'Model with a computed inversed field' foo = fields.Char() bar = fields.Char(compute='_compute_bar', inverse='_inverse_bar', store=True) baz = fields.Char() @api.depends('foo') def _compute_bar(self): self._context.get('log', []).append('compute') for record in self: record.bar = record.foo def _inverse_bar(self): self._context.get('log', []).append('inverse') for record in self: record.foo = record.bar @api.constrains('bar', 'baz') def _check_constraint(self): if self._context.get('log_constraint'): self._context.get('log', []).append('constraint') class ComputeSudo(models.Model): _name = 'test_new_api.compute.sudo' _description = 'Model with a compute_sudo field' name_for_uid = fields.Char(compute='_compute_name_for_uid', compute_sudo=True) @api.depends_context('uid') def _compute_name_for_uid(self): for record in self: record.name_for_uid = self.env.user.name class MultiComputeInverse(models.Model): """ Model with the same inverse method for several fields. """ _name = 'test_new_api.multi_compute_inverse' _description = 'Test New API Multi Compute Inverse' foo = fields.Char(default='', required=True) bar1 = fields.Char(compute='_compute_bars', inverse='_inverse_bar1', store=True) bar2 = fields.Char(compute='_compute_bars', inverse='_inverse_bar23', store=True) bar3 = fields.Char(compute='_compute_bars', inverse='_inverse_bar23', store=True) @api.depends('foo') def _compute_bars(self): self._context.get('log', []).append('compute') for record in self: substrs = record.foo.split('/') + ['', '', ''] record.bar1, record.bar2, record.bar3 = substrs[:3] def _inverse_bar1(self): self._context.get('log', []).append('inverse1') for record in self: record.write({'foo': '/'.join([record.bar1, record.bar2, record.bar3])}) def _inverse_bar23(self): self._context.get('log', []).append('inverse23') for record in self: record.write({'foo': '/'.join([record.bar1, record.bar2, record.bar3])}) class Move(models.Model): _name = 'test_new_api.move' _description = 'Move' line_ids = fields.One2many('test_new_api.move_line', 'move_id', domain=[('visible', '=', True)]) quantity = fields.Integer(compute='_compute_quantity', store=True) tag_id = fields.Many2one('test_new_api.multi.tag') tag_name = fields.Char(related='tag_id.name') tag_repeat = fields.Integer() tag_string = fields.Char(compute='_compute_tag_string') # This field can fool the ORM during onchanges! When editing a payment # record, modified fields are assigned to the parent record. When # determining the dependent records, the ORM looks for the payments related # to this record by the field `move_id`. As this field is an inverse of # `move_id`, it uses it. If that field was not initialized properly, the # ORM determines its value to be... empty (instead of the payment record.) payment_ids = fields.One2many('test_new_api.payment', 'move_id') @api.depends('line_ids.quantity') def _compute_quantity(self): for record in self: record.quantity = sum(line.quantity for line in record.line_ids) @api.depends('tag_name', 'tag_repeat') def _compute_tag_string(self): for record in self: record.tag_string = (record.tag_name or "") * record.tag_repeat class MoveLine(models.Model): _name = 'test_new_api.move_line' _description = 'Move Line' move_id = fields.Many2one('test_new_api.move', required=True, ondelete='cascade') visible = fields.Boolean(default=True) quantity = fields.Integer() class Payment(models.Model): _name = 'test_new_api.payment' _description = 'Payment inherits from Move' _inherits = {'test_new_api.move': 'move_id'} move_id = fields.Many2one('test_new_api.move', required=True, ondelete='cascade') class Order(models.Model): _name = _description = 'test_new_api.order' line_ids = fields.One2many('test_new_api.order.line', 'order_id') line_short_field_name = fields.Integer(index=True) class OrderLine(models.Model): _name = _description = 'test_new_api.order.line' order_id = fields.Many2one('test_new_api.order', required=True, ondelete='cascade') product = fields.Char() reward = fields.Boolean() short_field_name = fields.Integer(index=True) very_very_very_very_very_long_field_name_1 = fields.Integer(index=True) very_very_very_very_very_long_field_name_2 = fields.Integer(index=True) has_been_rewarded = fields.Char(compute='_compute_has_been_rewarded', store=True) @api.depends('reward') def _compute_has_been_rewarded(self): for rec in self: if rec.reward: rec.has_been_rewarded = 'Yes' def unlink(self): # also delete associated reward lines reward_lines = [ other_line for line in self if not line.reward for other_line in line.order_id.line_ids if other_line.reward and other_line.product == line.product ] self = self.union(*reward_lines) return super().unlink() class CompanyDependent(models.Model): _name = 'test_new_api.company' _description = 'Test New API Company' foo = fields.Char(company_dependent=True) text = fields.Text(company_dependent=True) date = fields.Date(company_dependent=True) moment = fields.Datetime(company_dependent=True) tag_id = fields.Many2one('test_new_api.multi.tag', company_dependent=True) truth = fields.Boolean(company_dependent=True) count = fields.Integer(company_dependent=True) phi = fields.Float(company_dependent=True, digits=(2, 5)) html1 = fields.Html(company_dependent=True, sanitize=False) html2 = fields.Html(company_dependent=True, sanitize_attributes=True, strip_classes=True, strip_style=True) company_id = fields.Many2one('res.company', company_dependent=True) # child_of and parent_of is optimized partner_id = fields.Many2one('res.partner', company_dependent=True) class CompanyDependentAttribute(models.Model): _name = 'test_new_api.company.attr' _description = 'Test New API Company Attribute' company = fields.Many2one('test_new_api.company') quantity = fields.Integer() bar = fields.Char(compute='_compute_bar', store=True) @api.depends('quantity', 'company.foo') def _compute_bar(self): for record in self: record.bar = (record.company.foo or '') * record.quantity class ComputeRecursive(models.Model): _name = 'test_new_api.recursive' _description = 'Test New API Recursive' name = fields.Char(required=True) parent = fields.Many2one('test_new_api.recursive', ondelete='cascade') full_name = fields.Char(compute='_compute_full_name', recursive=True) display_name = fields.Char(compute='_compute_display_name', recursive=True, store=True) context_dependent_name = fields.Char(compute='_compute_context_dependent_name', recursive=True) @api.depends('name', 'parent.full_name') def _compute_full_name(self): for rec in self: if rec.parent: rec.full_name = rec.parent.full_name + " / " + rec.name else: rec.full_name = rec.name @api.depends('name', 'parent.display_name') def _compute_display_name(self): for rec in self: if rec.parent: rec.display_name = rec.parent.display_name + " / " + rec.name else: rec.display_name = rec.name # This field is recursive, non-stored and context-dependent. Its purpose is # to reproduce a bug in modified(), which might not detect that the field # is present in cache if it has values in another context. @api.depends_context('bozo') @api.depends('name', 'parent.context_dependent_name') def _compute_context_dependent_name(self): for rec in self: if rec.parent: rec.context_dependent_name = rec.parent.context_dependent_name + " / " + rec.name else: rec.context_dependent_name = rec.name class ComputeRecursiveTree(models.Model): _name = 'test_new_api.recursive.tree' _description = 'Test New API Recursive with one2many field' name = fields.Char(required=True) parent_id = fields.Many2one('test_new_api.recursive.tree', ondelete='cascade') children_ids = fields.One2many('test_new_api.recursive.tree', 'parent_id') display_name = fields.Char(compute='_compute_display_name', recursive=True, store=True) @api.depends('name', 'children_ids.display_name') def _compute_display_name(self): for rec in self: children_names = rec.mapped('children_ids.display_name') rec.display_name = '%s(%s)' % (rec.name, ', '.join(children_names)) class ComputeRecursiveOrder(models.Model): _name = _description = 'test_new_api.recursive.order' value = fields.Integer() class ComputeRecursiveLine(models.Model): _name = _description = 'test_new_api.recursive.line' order_id = fields.Many2one('test_new_api.recursive.order') task_ids = fields.One2many('test_new_api.recursive.task', 'line_id') task_number = fields.Integer(compute='_compute_task_number', store=True) # line.task_number indirectly depends on recursive field task.line_id, and # is triggered by the recursion in modified() on field task.line_id @api.depends('task_ids') def _compute_task_number(self): for record in self: record.task_number = len(record.task_ids) class ComputeRecursiveTask(models.Model): _name = _description = 'test_new_api.recursive.task' value = fields.Integer() line_id = fields.Many2one('test_new_api.recursive.line', compute='_compute_line_id', recursive=True, store=True) # the recursive nature of task.line_id is a bit artificial, but it makes # line.task_number be triggered by a recursive call in modified() @api.depends('value', 'line_id.order_id.value') def _compute_line_id(self): # this assignment forces the new value of record.line_id to be dirty in cache self.line_id = False for record in self: domain = [('order_id.value', '=', record.value)] record.line_id = record.line_id.search(domain, order='id desc', limit=1) class ComputeCascade(models.Model): _name = 'test_new_api.cascade' _description = 'Test New API Cascade' foo = fields.Char() bar = fields.Char(compute='_compute_bar') # depends on foo baz = fields.Char(compute='_compute_baz', store=True) # depends on bar @api.depends('foo') def _compute_bar(self): for record in self: record.bar = "[%s]" % (record.foo or "") @api.depends('bar') def _compute_baz(self): for record in self: record.baz = "<%s>" % (record.bar or "") class ComputeReadWrite(models.Model): _name = 'test_new_api.compute.readwrite' _description = 'Model with a computed non-readonly field' foo = fields.Char() bar = fields.Char(compute='_compute_bar', store=True, readonly=False) @api.depends('foo') def _compute_bar(self): for record in self: record.bar = record.foo class ComputeOnchange(models.Model): _name = 'test_new_api.compute.onchange' _description = "Compute method as an onchange" active = fields.Boolean() foo = fields.Char() bar = fields.Char(compute='_compute_bar', store=True) baz = fields.Char(compute='_compute_baz', store=True, readonly=False) count = fields.Integer(default=0) line_ids = fields.One2many( 'test_new_api.compute.onchange.line', 'record_id', compute='_compute_line_ids', store=True, readonly=False ) tag_ids = fields.Many2many( 'test_new_api.multi.tag', compute='_compute_tag_ids', store=True, readonly=False, ) @api.onchange('foo') def _onchange_foo(self): self.count += 1 @api.depends('foo') def _compute_bar(self): for record in self: record.bar = (record.foo or "") + "r" @api.depends('active', 'foo') def _compute_baz(self): for record in self: if record.active: record.baz = (record.foo or "") + "z" @api.depends('foo') def _compute_line_ids(self): for record in self: if not record.foo: continue if any(line.foo == record.foo for line in record.line_ids): continue # add a line with the same value as 'foo' record.line_ids = [Command.create({'foo': record.foo})] @api.depends('foo') def _compute_tag_ids(self): Tag = self.env['test_new_api.multi.tag'] for record in self: if record.foo: record.tag_ids = Tag.search([('name', '=', record.foo)]) def copy_data(self, default=None): vals_list = super().copy_data(default=default) return [dict(vals, foo=self.env._("%s (copy)", record.foo)) for record, vals in zip(self, vals_list)] class ComputeOnchangeLine(models.Model): _name = 'test_new_api.compute.onchange.line' _description = "Line-like model for test_new_api.compute.onchange" record_id = fields.Many2one('test_new_api.compute.onchange', ondelete='cascade') foo = fields.Char() bar = fields.Char(compute='_compute_bar') @api.depends('foo') def _compute_bar(self): for line in self: line.bar = (line.foo or "") + "r" class ComputeDynamicDepends(models.Model): _name = 'test_new_api.compute.dynamic.depends' _description = "Computed field with dynamic dependencies" name1 = fields.Char() name2 = fields.Char() name3 = fields.Char() full_name = fields.Char(compute='_compute_full_name') def _get_full_name_fields(self): # the fields to use are stored in a config parameter depends = self.env['ir.config_parameter'].get_param('test_new_api.full_name', '') return depends.split(',') if depends else [] @api.depends(lambda self: self._get_full_name_fields()) def _compute_full_name(self): fnames = self._get_full_name_fields() for record in self: record.full_name = ", ".join(filter(None, (record[fname] for fname in fnames))) class ComputeUnassigned(models.Model): _name = 'test_new_api.compute.unassigned' _description = "Model with computed fields left unassigned" foo = fields.Char() bar = fields.Char(compute='_compute_bar') bare = fields.Char(compute='_compute_bare', readonly=False) bars = fields.Char(compute='_compute_bars', store=True) bares = fields.Char(compute='_compute_bares', readonly=False, store=True) @api.depends('foo') def _compute_bar(self): for record in self: if record.foo == "assign": record.bar = record.foo @api.depends('foo') def _compute_bare(self): for record in self: if record.foo == "assign": record.bare = record.foo @api.depends('foo') def _compute_bars(self): for record in self: if record.foo == "assign": record.bars = record.foo @api.depends('foo') def _compute_bares(self): for record in self: if record.foo == "assign": record.bares = record.foo class ComputeOne2many(models.Model): _name = 'test_new_api.one2many' _description = "A computed editable one2many field with a domain" name = fields.Char() line_ids = fields.One2many( 'test_new_api.one2many.line', 'container_id', compute='_compute_line_ids', store=True, readonly=False, domain=[('count', '>', 0)], ) @api.depends('name') def _compute_line_ids(self): # increment counter of line with the same name, or create a new line for record in self: if not record.name: continue for line in record.line_ids: if line.name == record.name: line.count += 1 break else: record.line_ids = [(0, 0, {'name': record.name})] class ComputeOne2manyLine(models.Model): _name = 'test_new_api.one2many.line' _description = "Line of a computed one2many" name = fields.Char() count = fields.Integer(default=1) container_id = fields.Many2one('test_new_api.one2many', required=True) class ModelBinary(models.Model): _name = 'test_new_api.model_binary' _description = 'Test Image field' binary = fields.Binary() binary_related_store = fields.Binary("Binary Related Store", related='binary', store=True, readonly=False) binary_related_no_store = fields.Binary("Binary Related No Store", related='binary', store=False, readonly=False) binary_computed = fields.Binary(compute='_compute_binary') @api.depends('binary') def _compute_binary(self): # arbitrary value: 'bin_size' must have no effect for record in self: record.binary_computed = [(record.id, bool(record.binary))] class ModelImage(models.Model): _name = 'test_new_api.model_image' _description = 'Test Image field' name = fields.Char(required=True) image = fields.Image() image_512 = fields.Image("Image 512", related='image', max_width=512, max_height=512, store=True, readonly=False) image_256 = fields.Image("Image 256", related='image', max_width=256, max_height=256, store=False, readonly=False) image_128 = fields.Image("Image 128", max_width=128, max_height=128) image_64 = fields.Image("Image 64", related='image', max_width=64, max_height=64, store=True, attachment=False, readonly=False) class BinarySvg(models.Model): _name = 'test_new_api.binary_svg' _description = 'Test SVG upload' name = fields.Char(required=True) image_attachment = fields.Binary(attachment=True) image_wo_attachment = fields.Binary(attachment=False) image_wo_attachment_related = fields.Binary( "image wo attachment", related="image_wo_attachment", store=True, attachment=False, ) class MonetaryBase(models.Model): _name = 'test_new_api.monetary_base' _description = 'Monetary Base' base_currency_id = fields.Many2one('res.currency') amount = fields.Monetary(currency_field='base_currency_id') class MonetaryRelated(models.Model): _name = 'test_new_api.monetary_related' _description = 'Monetary Related' monetary_id = fields.Many2one('test_new_api.monetary_base') currency_id = fields.Many2one('res.currency', related='monetary_id.base_currency_id') amount = fields.Monetary(related='monetary_id.amount') total = fields.Monetary() class MonetaryCustom(models.Model): _name = 'test_new_api.monetary_custom' _description = 'Monetary Related Custom' monetary_id = fields.Many2one('test_new_api.monetary_base') x_currency_id = fields.Many2one('res.currency', related='monetary_id.base_currency_id') x_amount = fields.Monetary(related='monetary_id.amount') class MonetaryInherits(models.Model): _name = 'test_new_api.monetary_inherits' _description = 'Monetary Inherits' _inherits = {'test_new_api.monetary_base': 'monetary_id'} monetary_id = fields.Many2one('test_new_api.monetary_base', required=True, ondelete='cascade') currency_id = fields.Many2one('res.currency') class MonetaryOrder(models.Model): _name = 'test_new_api.monetary_order' _description = 'Sales Order' currency_id = fields.Many2one('res.currency') line_ids = fields.One2many('test_new_api.monetary_order_line', 'order_id') total = fields.Monetary(compute='_compute_total', store=True) @api.depends('line_ids.subtotal') def _compute_total(self): for record in self: record.total = sum(line.subtotal for line in record.line_ids) class MonetaryOrderLine(models.Model): _name = 'test_new_api.monetary_order_line' _description = 'Sales Order Line' order_id = fields.Many2one('test_new_api.monetary_order', required=True, ondelete='cascade') subtotal = fields.Float(digits=(10, 2)) class FieldWithCaps(models.Model): _name = 'test_new_api.field_with_caps' _description = 'Model with field defined with capital letters' pArTneR_321_id = fields.Many2one('res.partner') class Selection(models.Model): _name = 'test_new_api.selection' _description = "Selection" state = fields.Selection([('foo', 'Foo'), ('bar', 'Bar')]) other = fields.Selection([('foo', 'Foo'), ('bar', 'Bar')]) class RequiredM2O(models.Model): _name = 'test_new_api.req_m2o' _description = 'Required Many2one' foo = fields.Many2one('res.currency', required=True, ondelete='cascade') bar = fields.Many2one('res.country', required=True) class RequiredM2OTransient(models.TransientModel): _name = 'test_new_api.req_m2o_transient' _description = 'Transient Model with Required Many2one' foo = fields.Many2one('res.currency', required=True, ondelete='restrict') bar = fields.Many2one('res.country', required=True) class TestTransient(models.TransientModel): _name = 'test_new_api.transient_model' _description = 'Transient Model' class Attachment(models.Model): _name = 'test_new_api.attachment' _description = 'Attachment' res_model = fields.Char(required=True) res_id = fields.Integer(required=True) name = fields.Char(compute='_compute_name', compute_sudo=True, store=True) @api.depends('res_model', 'res_id') def _compute_name(self): for rec in self: rec.name = self.env[rec.res_model].browse(rec.res_id).display_name # DLE P55: `test_cache_invalidation` def modified(self, fnames, *args, **kwargs): if not self: return comodel = self.env[self.res_model] if 'res_id' in fnames and 'attachment_ids' in comodel: record = comodel.browse(self.res_id) record.invalidate_recordset(['attachment_ids']) record.modified(['attachment_ids']) return super(Attachment, self).modified(fnames, *args, **kwargs) class AttachmentHost(models.Model): _name = 'test_new_api.attachment.host' _description = 'Attachment Host' attachment_ids = fields.One2many( 'test_new_api.attachment', 'res_id', auto_join=True, domain=lambda self: [('res_model', '=', self._name)], ) class DecimalPrecisionTestModel(models.Model): _name = 'decimal.precision.test' _description = 'Decimal Precision Test' float = fields.Float() float_2 = fields.Float(digits=(16, 2)) float_4 = fields.Float(digits=(16, 4)) class ModelA(models.Model): _name = 'test_new_api.model_a' _description = 'Model A' name = fields.Char() a_restricted_b_ids = fields.Many2many('test_new_api.model_b', relation='rel_model_a_model_b_1') b_restricted_b_ids = fields.Many2many('test_new_api.model_b', relation='rel_model_a_model_b_2', ondelete='restrict') class ModelB(models.Model): _name = 'test_new_api.model_b' _description = 'Model B' name = fields.Char() a_restricted_a_ids = fields.Many2many('test_new_api.model_a', relation='rel_model_a_model_b_1', ondelete='restrict') b_restricted_a_ids = fields.Many2many('test_new_api.model_a', relation='rel_model_a_model_b_2') class ModelParent(models.Model): _name = 'test_new_api.model_parent' _description = 'Model Multicompany parent' name = fields.Char() company_id = fields.Many2one('res.company') class ModelChild(models.Model): _name = 'test_new_api.model_child' _description = 'Model Multicompany child' _check_company_auto = True name = fields.Char() company_id = fields.Many2one('res.company') parent_id = fields.Many2one('test_new_api.model_parent', check_company=True) class ModelChildNoCheck(models.Model): _name = 'test_new_api.model_child_nocheck' _description = 'Model Multicompany child' _check_company_auto = True name = fields.Char() company_id = fields.Many2one('res.company') parent_id = fields.Many2one('test_new_api.model_parent', check_company=False) # model with explicit and stored field 'display_name' class Display(models.Model): _name = 'test_new_api.display' _description = 'Model that overrides display_name' display_name = fields.Char(compute='_compute_display_name', store=True) def _compute_display_name(self): for record in self: record.display_name = 'My id is %s' % (record.id) # abstract model with automatic and non-stored field 'display_name' class Mixin(models.AbstractModel): _name = 'test_new_api.mixin' _description = 'Dummy mixin model' # in this model extension, the field 'display_name' should not be inherited from # 'test_new_api.mixin' class ExtendedDisplay(models.Model): _name = 'test_new_api.display' _inherit = ['test_new_api.mixin', 'test_new_api.display'] class ModelActiveField(models.Model): _name = 'test_new_api.model_active_field' _description = 'A model with active field' name = fields.Char() active = fields.Boolean(default=True) parent_id = fields.Many2one('test_new_api.model_active_field') children_ids = fields.One2many('test_new_api.model_active_field', 'parent_id') all_children_ids = fields.One2many('test_new_api.model_active_field', 'parent_id', context={'active_test': False}) active_children_ids = fields.One2many('test_new_api.model_active_field', 'parent_id', context={'active_test': True}) relatives_ids = fields.Many2many( 'test_new_api.model_active_field', 'model_active_field_relatives_rel', 'source_id', 'dest_id', ) all_relatives_ids = fields.Many2many( 'test_new_api.model_active_field', 'model_active_field_relatives_rel', 'source_id', 'dest_id', context={'active_test': False}, ) parent_active = fields.Boolean(string='Active Parent', related='parent_id.active', store=True) class ModelMany2oneReference(models.Model): _name = 'test_new_api.model_many2one_reference' _description = 'dummy m2oref model' res_model = fields.Char('Resource Model') res_id = fields.Many2oneReference('Resource ID', model_field='res_model') const = fields.Boolean(default=True) class InverseM2oRef(models.Model): _name = 'test_new_api.inverse_m2o_ref' _description = 'dummy m2oref inverse model' model_ids = fields.One2many( 'test_new_api.model_many2one_reference', 'res_id', string="Models", ) model_ids_count = fields.Integer("Count", compute='_compute_model_ids_count') model_computed_ids = fields.One2many( 'test_new_api.model_many2one_reference', string="Models Computed", compute='_compute_model_computed_ids', ) @api.depends('model_ids') def _compute_model_ids_count(self): for rec in self: rec.model_ids_count = len(rec.model_ids) def _compute_model_computed_ids(self): self.model_computed_ids = [] class ModelChildM2o(models.Model): _name = 'test_new_api.model_child_m2o' _description = 'dummy model with override write and ValidationError' name = fields.Char('Name') parent_id = fields.Many2one('test_new_api.model_parent_m2o', ondelete='cascade') size1 = fields.Integer(compute='_compute_sizes', store=True) size2 = fields.Integer(compute='_compute_sizes', store=True) cost = fields.Integer(compute='_compute_cost', store=True, readonly=False) @api.depends('parent_id.name') def _compute_sizes(self): for record in self: record.size1 = len(record.parent_id.name) record.size2 = len(record.parent_id.name) @api.depends('name') def _compute_cost(self): for record in self: record.cost = len(record.name) def write(self, vals): res = super(ModelChildM2o, self).write(vals) if self.name == 'A': raise ValidationError('the first existing child should not be changed when adding a new child to the parent') return res class ModelParentM2o(models.Model): _name = 'test_new_api.model_parent_m2o' _description = 'dummy model with multiple childs' name = fields.Char('Name') child_ids = fields.One2many('test_new_api.model_child_m2o', 'parent_id', string="Children") cost = fields.Integer(compute='_compute_cost', store=True) @api.depends('child_ids.cost') def _compute_cost(self): for record in self: record.cost = sum(child.cost for child in record.child_ids) class Country(models.Model): _name = 'test_new_api.country' _description = 'Country, ordered by name' _order = 'name, id' name = fields.Char() class City(models.Model): _name = 'test_new_api.city' _description = 'City, ordered by country then name' _order = 'country_id, name, id' name = fields.Char() country_id = fields.Many2one('test_new_api.country') # abstract model with a selection field class StateMixin(models.AbstractModel): _name = 'test_new_api.state_mixin' _description = 'Dummy state mixin model' state = fields.Selection([ ('draft', 'Draft'), ('confirmed', 'Confirmed'), ('done', 'Done'), ]) class SelectionBase(models.Model): _name = 'test_new_api.model_selection_base' _description = "Model with a base selection field" my_selection = fields.Selection([ ('foo', "Foo"), ('bar', "Bar"), ]) class SelectionBaseNullExplicit(models.Model): _inherit = 'test_new_api.model_selection_base' _description = "Model with a selection field extension with ondelete null" my_selection = fields.Selection(selection_add=[ ('quux', "Quux"), ], ondelete={'quux': 'set null'}) class SelectionBaseNullImplicit(models.Model): _inherit = 'test_new_api.model_selection_base' _description = "Model with a selection field extension without ondelete" my_selection = fields.Selection(selection_add=[ ('ham', "Ham"), ]) class SelectionRelated(models.Model): _name = 'test_new_api.model_selection_related' _description = "Model with a related selection field" selection_id = fields.Many2one( comodel_name='test_new_api.model_selection_base', required=True, ) related_selection = fields.Selection( related='selection_id.my_selection', ) class SelectionRelatedUpdatable(models.Model): _name = 'test_new_api.model_selection_related_updatable' _description = "Model with an updatable related selection field" selection_id = fields.Many2one( comodel_name='test_new_api.model_selection_base', required=True, ) related_selection = fields.Selection( related='selection_id.my_selection', readonly=False, ) class SelectionRequired(models.Model): _name = 'test_new_api.model_selection_required' _description = "Model with a required selection field" active = fields.Boolean(default=True) my_selection = fields.Selection([ ('foo', "Foo"), ('bar', "Bar"), ], required=True, default='foo') class SelectionRequiredDefault(models.Model): _inherit = 'test_new_api.model_selection_required' _description = "Model with a selection field extension with ondelete default" my_selection = fields.Selection(selection_add=[ ('baz', "Baz"), ], ondelete={'baz': 'set default'}) class SelectionRequiredCascade(models.Model): _inherit = 'test_new_api.model_selection_required' _description = "Model with a selection field extension with ondelete cascade" my_selection = fields.Selection(selection_add=[ ('eggs', "Eggs"), ], ondelete={'eggs': 'cascade'}) class SelectionRequiredLiteral(models.Model): _inherit = 'test_new_api.model_selection_required' _description = "Model with a selection field extension with ondelete set