diff options
Diffstat (limited to 'odoo/addons/test_new_api/models/test_new_api.py')
| -rw-r--r-- | odoo/addons/test_new_api/models/test_new_api.py | 766 |
1 files changed, 766 insertions, 0 deletions
diff --git a/odoo/addons/test_new_api/models/test_new_api.py b/odoo/addons/test_new_api/models/test_new_api.py new file mode 100644 index 00000000000..3f45466efa3 --- /dev/null +++ b/odoo/addons/test_new_api/models/test_new_api.py @@ -0,0 +1,766 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +import datetime + +from odoo import models, fields, api, _ +from odoo.exceptions import AccessError, ValidationError + + +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) + root_categ = fields.Many2one(_name, compute='_compute_root_categ') + display_name = fields.Char(compute='_compute_display_name', inverse='_inverse_display_name') + 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 + + 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 _read(self, 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(Category, self)._read(fields) + + +class Discussion(models.Model): + _name = 'test_new_api.discussion' + _description = 'Test New API Discussion' + + name = fields.Char(string='Title', required=True, + help="General description of what this discussion is about.") + 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)]) + + 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() + 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() + + @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.participants: + raise ValidationError(_("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 + 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 [('id', 'in', ids)] + + @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') + + +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') + + @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 + + +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('res.partner') + 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() + + +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) + 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' + + 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) + + 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() + + +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) + + @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') + +class ComputeProtected(models.Model): + _name = 'test_new_api.compute.protected' + _description = 'Test New API Compute Protected' + + 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 = 'Test New API Compute Inversse' + + foo = fields.Char() + bar = fields.Char(compute='_compute_bar', inverse='_inverse_bar', store=True) + + @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 + + +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 CompanyDependent(models.Model): + _name = 'test_new_api.company' + _description = 'Test New API Company' + + foo = fields.Char(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) + + +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') + display_name = fields.Char(compute='_compute_display_name', store=True) + + @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 + + +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 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) + + @api.depends('foo') + def _compute_bar(self): + for record in self: + record.bar = record.foo + + @api.depends('active', 'foo') + def _compute_baz(self): + for record in self: + if record.active: + record.baz = record.foo + + +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) + + +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) + + +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') + + +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 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')]) + + +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 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, create=False): + if not self: + return + comodel = self.env[self.res_model] + if 'res_id' in fnames and 'attachment_ids' in comodel: + field = comodel._fields['attachment_ids'] + record = comodel.browse(self.res_id) + self.env.cache.invalidate([(field, record._ids)]) + record.modified(['attachment_ids']) + return super(Attachment, self).modified(fnames, create) + + +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', required=True) + + +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', required=True) + 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', required=True) + 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' + + 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') + 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') + + +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') + + @api.depends('model_ids') + def _compute_model_ids_count(self): + for rec in self: + rec.model_ids_count = len(rec.model_ids) |
