diff options
| author | Christophe Simonis <chs@odoo.com> | 2019-02-21 15:37:19 +0100 |
|---|---|---|
| committer | Christophe Simonis <chs@odoo.com> | 2019-02-21 15:37:19 +0100 |
| commit | 1d2b6f1bef6af13245d29d56cbb5f9d61f9ca038 (patch) | |
| tree | 8ea1d203aef83d287de9ec578f81bd64eedb7359 /odoo | |
| parent | f945b1bcc21621cfc056e40f4d66b7181d19239e (diff) | |
| parent | 84143a34b313c01b0b37b7d7246ce3d5c9476002 (diff) | |
[MERGE] forward port branch 12.0 up to 84143a34b31
Diffstat (limited to 'odoo')
| -rw-r--r-- | odoo/addons/base/data/res.lang.csv | 4 | ||||
| -rw-r--r-- | odoo/addons/base/models/ir_actions_report.py | 5 | ||||
| -rw-r--r-- | odoo/addons/base/models/ir_ui_view.py | 18 | ||||
| -rw-r--r-- | odoo/addons/base/models/res_country.py | 2 | ||||
| -rw-r--r-- | odoo/addons/base/models/res_partner.py | 6 | ||||
| -rw-r--r-- | odoo/addons/base/tests/test_views.py | 60 | ||||
| -rw-r--r-- | odoo/addons/test_new_api/tests/test_new_fields.py | 50 | ||||
| -rw-r--r-- | odoo/addons/test_testing_utilities/menu.xml | 13 | ||||
| -rw-r--r-- | odoo/addons/test_testing_utilities/models.py | 5 | ||||
| -rw-r--r-- | odoo/addons/test_testing_utilities/tests/test_form_impl.py | 26 | ||||
| -rw-r--r-- | odoo/api.py | 9 | ||||
| -rw-r--r-- | odoo/fields.py | 22 | ||||
| -rw-r--r-- | odoo/models.py | 62 | ||||
| -rw-r--r-- | odoo/tests/common.py | 61 |
14 files changed, 275 insertions, 68 deletions
diff --git a/odoo/addons/base/data/res.lang.csv b/odoo/addons/base/data/res.lang.csv index 326448849a0..9fc3aa1c861 100644 --- a/odoo/addons/base/data/res.lang.csv +++ b/odoo/addons/base/data/res.lang.csv @@ -1,8 +1,10 @@ "id","name","code","iso_code","direction","grouping","decimal_point","thousands_sep","date_format","time_format","translatable","week_start" "base.lang_en","English","en_US","en","Left-to-Right","[3,0]",".",",","%m/%d/%Y","%H:%M:%S","True","7" "base.lang_am_ET","Amharic / አምሃርኛ","am_ET","am_ET","Left-to-Right","[3,0]",".",",","%d/%m/%Y","%I:%M:%S","True","7" -"base.lang_ar_SY","Arabic / الْعَرَبيّة","ar_SY","ar","Right-to-Left","[3,0]",".",",","%d %b, %Y","%I:%M:%S","True","6" +"base.lang_ar","Arabic / الْعَرَبيّة","ar_AA","ar","Right-to-Left","[3,0]",".",",","%d %b, %Y","%I:%M:%S","True","6" +"base.lang_ar_SY","Arabic (Syria) / الْعَرَبيّة","ar_SY","ar_SY","Right-to-Left","[3,0]",".",",","%d %b, %Y","%I:%M:%S","True","6" "base.lang_eu_ES","Basque / Euskara","eu_ES","eu_ES","Left-to-Right","[]",",",,"%a, %Y.eko %bren %da","%H:%M:%S","True","1" +"base.lang_bn_IN","Bengali / বাংলা","bn_IN","bn_IN","Left-to-Right","[]",",",,"%A %d %b %Y","%I:%M:%S","True","1" "base.lang_bs_BA","Bosnian / bosanski jezik","bs_BA","bs","Left-to-Right","[]",",",,"%d.%m.%Y","%H:%M:%S","True","7" "base.lang_bg","Bulgarian / български език","bg_BG","bg","Left-to-Right","[3,0]",",",,"%d.%m.%Y","%H,%M,%S","True","1" "base.lang_ca_ES","Catalan / Català","ca_ES","ca_ES","Left-to-Right","[3,0]",",",".","%d/%m/%Y","%H:%M:%S","True","1" diff --git a/odoo/addons/base/models/ir_actions_report.py b/odoo/addons/base/models/ir_actions_report.py index 095dcabcf1c..4136b4348f6 100644 --- a/odoo/addons/base/models/ir_actions_report.py +++ b/odoo/addons/base/models/ir_actions_report.py @@ -467,7 +467,10 @@ class IrActionsReport(models.Model): ) return barcode.asString('png') except (ValueError, AttributeError): - raise ValueError("Cannot convert into barcode.") + if barcode_type == 'Code128': + raise ValueError("Cannot convert into barcode.") + else: + return self.barcode('Code128', value, width=width, height=height, humanreadable=humanreadable) @api.multi def render_template(self, template, values=None): diff --git a/odoo/addons/base/models/ir_ui_view.py b/odoo/addons/base/models/ir_ui_view.py index 5fd437e9498..b2e11f85623 100644 --- a/odoo/addons/base/models/ir_ui_view.py +++ b/odoo/addons/base/models/ir_ui_view.py @@ -251,7 +251,7 @@ actual arch. for view in self: arch_fs = None xml_id = view.xml_id or view.key - if 'xml' in config['dev_mode'] and view.arch_fs and xml_id: + if (self._context.get('read_arch_from_file') or 'xml' in config['dev_mode']) and view.arch_fs and xml_id: # It is safe to split on / herebelow because arch_fs is explicitely stored with '/' fullpath = get_resource_path(*view.arch_fs.split('/')) if fullpath: @@ -766,19 +766,20 @@ actual arch. [view_data] = root.read(fields=fields) view_arch = etree.fromstring(view_data['arch'].encode('utf-8')) if not root.inherit_id: + if self._context.get('inherit_branding'): + view_arch.attrib.update({ + 'data-oe-model': 'ir.ui.view', + 'data-oe-id': str(root.id), + 'data-oe-field': 'arch', + }) arch_tree = view_arch else: + if self._context.get('inherit_branding'): + self.inherit_branding(view_arch, root.id, root.id) parent_view = root.inherit_id.read_combined(fields=fields) arch_tree = etree.fromstring(parent_view['arch']) arch_tree = self.apply_inheritance_specs(arch_tree, view_arch, parent_view['id']) - if self._context.get('inherit_branding'): - arch_tree.attrib.update({ - 'data-oe-model': 'ir.ui.view', - 'data-oe-id': str(root.id), - 'data-oe-field': 'arch', - }) - # and apply inheritance arch = self.apply_view_inheritance(arch_tree, root.id, self.model) @@ -1196,6 +1197,7 @@ actual arch. def _contains_branded(self, node): return node.tag == 't'\ or 't-raw' in node.attrib\ + or 't-call' in node.attrib\ or any(self.is_node_branded(child) for child in node.iterdescendants()) def _pop_view_branding(self, element): diff --git a/odoo/addons/base/models/res_country.py b/odoo/addons/base/models/res_country.py index fe86446bfbc..8fd1f978ed0 100644 --- a/odoo/addons/base/models/res_country.py +++ b/odoo/addons/base/models/res_country.py @@ -121,7 +121,7 @@ class CountryState(models.Model): @api.model def _name_search(self, name, args=None, operator='ilike', limit=100, name_get_uid=None): args = args or [] - if 'country_id' in self.env.context: + if self.env.context.get('country_id'): args = expression.AND([args, [('country_id', '=', self.env.context.get('country_id'))]]) if operator == 'ilike' and not (name or '').strip(): diff --git a/odoo/addons/base/models/res_partner.py b/odoo/addons/base/models/res_partner.py index c558c8f7920..885a3c6bf09 100644 --- a/odoo/addons/base/models/res_partner.py +++ b/odoo/addons/base/models/res_partner.py @@ -867,6 +867,12 @@ class Partner(models.Model): def _get_country_name(self): return self.country_id.name or '' + @api.multi + def get_base_url(self): + """Get the base URL for the current partner.""" + self.ensure_one() + return self.env['ir.config_parameter'].sudo().get_param('web.base.url') + class ResPartnerIndustry(models.Model): _description = 'Industry' diff --git a/odoo/addons/base/tests/test_views.py b/odoo/addons/base/tests/test_views.py index 0a1f3060762..cb16873b1a3 100644 --- a/odoo/addons/base/tests/test_views.py +++ b/odoo/addons/base/tests/test_views.py @@ -674,6 +674,51 @@ class TestTemplating(ViewCase): second.get('data-oe-id'), "second should come from the extension view") + def test_branding_primary_inherit(self): + view1 = self.View.create({ + 'name': "Base view", + 'type': 'qweb', + 'arch': """<root> + <item order="1"/> + </root> + """ + }) + view2 = self.View.create({ + 'name': "Extension", + 'type': 'qweb', + 'mode': 'primary', + 'inherit_id': view1.id, + 'arch': """<xpath expr="//item" position="after"> + <item order="2"/> + </xpath> + """ + }) + + arch_string = view2.with_context(inherit_branding=True).read_combined(['arch'])['arch'] + + arch = etree.fromstring(arch_string) + self.View.distribute_branding(arch) + + [initial] = arch.xpath('//item[@order=1]') + self.assertEqual( + initial.get('data-oe-id'), + str(view1.id), + "initial should come from the root view") + self.assertEqual( + initial.get('data-oe-xpath'), + '/root[1]/item[1]', + "initial's xpath should be within the inherited view only") + + [second] = arch.xpath('//item[@order=2]') + self.assertEqual( + second.get('data-oe-id'), + str(view2.id), + "second should come from the extension view") + self.assertEqual( + second.get('data-oe-xpath'), + '/xpath/item', + "second xpath should be on the inheriting view only") + def test_branding_distribute_inner(self): """ Checks that the branding is correctly distributed within a view extension @@ -724,6 +769,21 @@ class TestTemplating(ViewCase): ) ) + def test_call_no_branding(self): + view = self.View.create({ + 'name': "Base View", + 'type': 'qweb', + 'arch': """<root> + <item><span t-call="foo"/></item> + </root>""", + }) + + arch_string = view.with_context(inherit_branding=True).read_combined(['arch'])['arch'] + arch = etree.fromstring(arch_string) + self.View.distribute_branding(arch) + + self.assertEqual(arch, E.root(E.item(E.span({'t-call': "foo"})))) + def test_esc_no_branding(self): view = self.View.create({ 'name': "Base View", diff --git a/odoo/addons/test_new_api/tests/test_new_fields.py b/odoo/addons/test_new_api/tests/test_new_fields.py index 704c85a9dd7..64bf5da075a 100644 --- a/odoo/addons/test_new_api/tests/test_new_fields.py +++ b/odoo/addons/test_new_api/tests/test_new_fields.py @@ -369,6 +369,26 @@ class TestFields(common.TransactionCase): self.assertEqual(record.bar3, 'C') self.assertCountEqual(log, ['compute']) + def test_13_inverse_access(self): + """ test access rights on inverse fields """ + foo = self.env['test_new_api.category'].create({'name': 'Foo'}) + user = self.env['res.users'].create({'name': 'Foo', 'login': 'foo'}) + self.assertFalse(user.has_group('base.group_system')) + # add group on non-stored inverse field + self.patch(type(foo).display_name, 'groups', 'base.group_system') + with self.assertRaises(AccessError): + foo.sudo(user).display_name = 'Forbidden' + + def test_13_inverse_access(self): + """ test access rights on inverse fields """ + foo = self.env['test_new_api.category'].create({'name': 'Foo'}) + user = self.env['res.users'].create({'name': 'Foo', 'login': 'foo'}) + self.assertFalse(user.has_group('base.group_system')) + # add group on non-stored inverse field + self.patch(type(foo).display_name, 'groups', 'base.group_system') + with self.assertRaises(AccessError): + foo.sudo(user).display_name = 'Forbidden' + def test_14_search(self): """ test search on computed fields """ discussion = self.env.ref('test_new_api.discussion_0') @@ -771,6 +791,10 @@ class TestFields(common.TransactionCase): self.env['ir.property'].create({'name': 'foo', 'fields_id': field_tag_id.id, 'value': tag0, 'type': 'many2one'}) + # assumption: users don't have access to 'ir.property' + accesses = self.env['ir.model.access'].search([('model_id.model', '=', 'ir.property')]) + accesses.write(dict.fromkeys(['perm_read', 'perm_write', 'perm_create', 'perm_unlink'], False)) + # create/modify a record, and check the value for each user record = self.env['test_new_api.company'].create({ 'foo': 'main', @@ -824,6 +848,13 @@ class TestFields(common.TransactionCase): self.assertEqual(record.sudo(user1).foo, False) self.assertEqual(record.sudo(user2).foo, 'default') + # set field with 'force_company' in context + record.sudo(user0).with_context(force_company=company1.id).foo = 'beta' + record.invalidate_cache() + self.assertEqual(record.sudo(user0).foo, 'main') + self.assertEqual(record.sudo(user1).foo, 'beta') + self.assertEqual(record.sudo(user2).foo, 'default') + # create company record and attribute company_record = self.env['test_new_api.company'].create({'foo': 'ABC'}) attribute_record = self.env['test_new_api.company.attr'].create({ @@ -843,6 +874,25 @@ class TestFields(common.TransactionCase): self.assertEqual(attribute_record.bar, 'DEFDEF') self.assertFalse(self.env.has_todo()) + # add group on company-dependent field + self.assertFalse(user0.has_group('base.group_system')) + self.patch(type(record).foo, 'groups', 'base.group_system') + with self.assertRaises(AccessError): + record.sudo(user0).foo = 'forbidden' + + user0.write({'groups_id': [(4, self.env.ref('base.group_system').id)]}) + record.sudo(user0).foo = 'yes we can' + + # add ir.rule to prevent access on record + self.assertTrue(user0.has_group('base.group_user')) + rule = self.env['ir.rule'].create({ + 'model_id': self.env['ir.model']._get_id(record._name), + 'groups': [self.env.ref('base.group_user').id], + 'domain_force': str([('id', '!=', record.id)]), + }) + with self.assertRaises(AccessError): + record.sudo(user0).foo = 'forbidden' + def test_30_read(self): """ test computed fields as returned by read(). """ discussion = self.env.ref('test_new_api.discussion_0') diff --git a/odoo/addons/test_testing_utilities/menu.xml b/odoo/addons/test_testing_utilities/menu.xml index 99e409708c2..3ca18ef2a50 100644 --- a/odoo/addons/test_testing_utilities/menu.xml +++ b/odoo/addons/test_testing_utilities/menu.xml @@ -10,6 +10,17 @@ name="O2M onchange test thing" action="action_test_o2m_onchange"/> + <record id="action_test_m2o_onchange" model="ir.actions.act_window"> + <field name="name">test_default_and_onchange</field> + <field name="type">ir.actions.act_window</field> + <field name="res_model">test_testing_utilities.d</field> + <field name="view_mode">tree,form</field> + </record> + + <menuitem id="menu_m2o" parent="base.menu_tests" + name="test_default_and_onchange" + action="action_test_m2o_onchange"/> + <record id="o2m_parent" model="ir.ui.view"> <field name="name">Parent External Form</field> <field name="priority">1</field> @@ -98,7 +109,7 @@ <field name="arch" type="xml"> <form> <field name="f1"/> - <field name="f2" attrs="{'readonly': [['f1', '=', 63], ['f3', '=', 5]]}"/> + <field name="f2" attrs="{'readonly': [['f1', '=', '63'], ['f3', '=', 5]]}"/> <field name="f3"/> <field name="f4"/> <field name="f5" readonly="1"/> diff --git a/odoo/addons/test_testing_utilities/models.py b/odoo/addons/test_testing_utilities/models.py index 98a9fea7e7f..ffb27cd08d8 100644 --- a/odoo/addons/test_testing_utilities/models.py +++ b/odoo/addons/test_testing_utilities/models.py @@ -6,7 +6,7 @@ class A(models.Model): _name = 'test_testing_utilities.a' _description = 'Testing Utilities A' - f1 = fields.Integer(required=True) + f1 = fields.Char(required=True) f2 = fields.Integer(default=42) f3 = fields.Integer() f4 = fields.Integer(compute='_compute_f4') @@ -22,7 +22,7 @@ class A(models.Model): @api.depends('f1', 'f2') def _compute_f4(self): for r in self: - r.f4 = r.f2 / (r.f1 or 1) + r.f4 = r.f2 / (int(r.f1) or 1) class B(models.Model): _name = 'test_testing_utilities.readonly' @@ -154,7 +154,6 @@ class O2MSub(models.Model): @api.onchange('has_parent') def _onchange_has_parent(self): - self.has_parent = bool(self.parent_id) if self.has_parent: self.value = self.parent_id.value diff --git a/odoo/addons/test_testing_utilities/tests/test_form_impl.py b/odoo/addons/test_testing_utilities/tests/test_form_impl.py index e2f0d0d89d3..344ae62f4d2 100644 --- a/odoo/addons/test_testing_utilities/tests/test_form_impl.py +++ b/odoo/addons/test_testing_utilities/tests/test_form_impl.py @@ -24,7 +24,7 @@ class TestBasic(TransactionCase): self.assertEqual(f.f3, 21) self.assertEqual(f.f4, 42) - f.f1 = 4 + f.f1 = '4' self.assertEqual(f.f2, 42) self.assertEqual(f.f3, 21) self.assertEqual(f.f4, 10) @@ -36,7 +36,7 @@ class TestBasic(TransactionCase): r = f.save() self.assertEqual( (r.f1, r.f2, r.f3, r.f4), - (4, 8, 4, 2), + ('4', 8, 4, 2), ) def test_required(self): @@ -45,12 +45,12 @@ class TestBasic(TransactionCase): with self.assertRaisesRegexp(AssertionError, 'f1 is a required field'): f.save() # set f1 and unset f2 => should work - f.f1 = 1 + f.f1 = '1' f.f2 = False r = f.save() self.assertEqual( (r.f1, r.f2, r.f3, r.f4), - (1, 0, 0, 0) + ('1', 0, 0, 0) ) def test_readonly(self): @@ -61,7 +61,7 @@ class TestBasic(TransactionCase): f = Form(self.env['test_testing_utilities.readonly']) with self.assertRaises(AssertionError): - f.f1 = 5 + f.f1 = '5' with self.assertRaises(AssertionError): f.f2 = 42 @@ -70,7 +70,7 @@ class TestBasic(TransactionCase): """ f = Form(self.env['test_testing_utilities.a'], view='test_testing_utilities.non_normalized_attrs') - f.f1 = 1 + f.f1 = '1' f.f2 = 987 self.assertEqual(f.f5, 987) self.assertEqual(f.f6, 987) @@ -86,7 +86,7 @@ class TestBasic(TransactionCase): # not readonly yet, should work f.f2 = 5 # make f2 readonly - f.f1 = 63 + f.f1 = '63' f.f3 = 5 with self.assertRaises(AssertionError): f.f2 = 6 @@ -101,9 +101,9 @@ class TestM2O(TransactionCase): f = Form(self.env['test_testing_utilities.d']) - self.assertEqual( - f.f, a, - "The default value for the m2o should be the first Sub record" + self.assertFalse( + f.f, + "The default value gets overridden by the onchange" ) f.f2 = "B" self.assertEqual( @@ -271,6 +271,7 @@ class TestO2M(TransactionCase): [get(s) for s in r.subs], [("2", 2, 2), ("2", 2, 2)] ) + self.assertEqual(r.v, 5) with Form(r, view='test_testing_utilities.o2m_parent') as f: with f.subs.new() as sub: @@ -286,6 +287,7 @@ class TestO2M(TransactionCase): [get(s) for s in r.subs], [("2", 2, 2), ("5", 5, 5), ("2", 2, 2)] ) + self.assertEqual(r.v, 10) with Form(r, view='test_testing_utilities.o2m_parent') as f, \ f.subs.edit(index=0) as sub,\ @@ -439,12 +441,12 @@ class TestEdition(TransactionCase): """ def test_trivial(self): r = self.env['test_testing_utilities.a'].create({ - 'f1': 5, + 'f1': '5', }) with Form(r) as f: self.assertEqual(f.id, r.id) - self.assertEqual(f.f1, 5) + self.assertEqual(f.f1, '5') self.assertEqual(f.f4, 8) f.f2 = 5 diff --git a/odoo/api.py b/odoo/api.py index dffbc0e633a..b22d9ac2476 100644 --- a/odoo/api.py +++ b/odoo/api.py @@ -1036,7 +1036,7 @@ class Cache(object): """ Return the value of ``field`` for ``record``. """ key = record.env.cache_key(field) try: - value = self._data[key][field][record.id] + value = self._data[key][field][record._ids[0]] except KeyError: raise CacheMiss(record, field) @@ -1045,7 +1045,12 @@ class Cache(object): def set(self, record, field, value): """ Set the value of ``field`` for ``record``. """ key = record.env.cache_key(field) - self._data[key][field][record.id] = value + self._data[key][field][record._ids[0]] = value + + def update(self, records, field, values): + """ Set the values of ``field`` for several ``records``. """ + key = records.env.cache_key(field) + self._data[key][field].update(zip(records._ids, values)) def remove(self, record, field): """ Remove the value of ``field`` for ``record``. """ diff --git a/odoo/fields.py b/odoo/fields.py index a0a08631c52..75f6097d1aa 100644 --- a/odoo/fields.py +++ b/odoo/fields.py @@ -643,13 +643,25 @@ class Field(MetaField('DummyField', (object,), {})): return model.env['ir.property'].get(self.name, self.model_name) def _compute_company_dependent(self, records): - Property = records.env['ir.property'] + # read property as superuser, as the current user may not have access + context = records.env.context + if 'force_company' not in context: + field_id = records.env['ir.model.fields']._get_id(self.model_name, self.name) + company = records.env['res.company']._company_default_get(self.model_name, field_id) + context = dict(context, force_company=company.id) + Property = records.env(user=SUPERUSER_ID, context=context)['ir.property'] values = Property.get_multi(self.name, self.model_name, records.ids) for record in records: record[self.name] = values.get(record.id) def _inverse_company_dependent(self, records): - Property = records.env['ir.property'] + # update property as superuser, as the current user may not have access + context = records.env.context + if 'force_company' not in context: + field_id = records.env['ir.model.fields']._get_id(self.model_name, self.name) + company = records.env['res.company']._company_default_get(self.model_name, field_id) + context = dict(context, force_company=company.id) + Property = records.env(user=SUPERUSER_ID, context=context)['ir.property'] values = { record.id: self.convert_to_write(record[self.name], record) for record in records @@ -2127,7 +2139,8 @@ class Many2one(_Relational): return () def convert_to_record(self, value, record): - return record.env[self.comodel_name]._browse(value, record.env, record._prefetch) + # use registry to avoid creating a recordset for the model + return record.env.registry[self.comodel_name]._browse(value, record.env, record._prefetch) def convert_to_read(self, value, record, use_name_get=True): if use_name_get and value: @@ -2247,7 +2260,8 @@ class _RelationalMulti(_Relational): raise ValueError("Wrong value for %s: %s" % (self, value)) def convert_to_record(self, value, record): - return record.env[self.comodel_name]._browse(value, record.env, record._prefetch) + # use registry to avoid creating a recordset for the model + return record.env.registry[self.comodel_name]._browse(value, record.env, record._prefetch) def convert_to_read(self, value, record, use_name_get=True): return value.ids diff --git a/odoo/models.py b/odoo/models.py index 9104406195b..b936cf8ae02 100644 --- a/odoo/models.py +++ b/odoo/models.py @@ -2779,20 +2779,22 @@ class BaseModel(MetaModel('DummyModel', (object,), {'_register': False})): # retrieve results from records; this takes values from the cache and # computes remaining fields self = self.with_prefetch(self._prefetch.copy()) - data = {record: {'id': record.id} for record in self} - missing = set() + data = [(record, {'id': record._ids[0]}) for record in self] use_name_get = (load == '_classic_read') for name in (stored + inherited + computed): convert = self._fields[name].convert_to_read # restrict the prefetching of self's model to self; this avoids # computing fields on a larger recordset than self self._prefetch[self._name] = set(self._ids) - for record in self: + for record, vals in data: + # missing records have their vals empty + if not vals: + continue try: - data[record][name] = convert(record[name], record, use_name_get) + vals[name] = convert(record[name], record, use_name_get) except MissingError: - missing.add(record) - result = [data[record] for record in self if record not in missing] + vals.clear() + result = [vals for record, vals in data if vals] return result @@ -2887,36 +2889,46 @@ class BaseModel(MetaModel('DummyModel', (object,), {'_register': False})): res = 'pg_size_pretty(length(%s)::bigint)' % res return '%s as "%s"' % (res, col) + # selected fields are: 'id' followed by fields_pre qual_names = [qualify(name) for name in [self._fields['id']] + fields_pre] # determine the actual query to execute from_clause, where_clause, params = query.get_sql() query_str = "SELECT %s FROM %s WHERE %s" % (",".join(qual_names), from_clause, where_clause) - result = [] + # fetch one list of record values per field + field_values_list = [[] for name in qual_names] param_pos = params.index(param_ids) for sub_ids in cr.split_for_in_conditions(self.ids): params[param_pos] = tuple(sub_ids) cr.execute(query_str, params) - result.extend(cr.dictfetchall()) + for row in cr.fetchall(): + for values, val in zip(field_values_list, row): + values.append(val) - ids = [vals['id'] for vals in result] + ids = field_values_list.pop(0) fetched = self.browse(ids) if ids: # translate the fields if necessary if context.get('lang'): - for field in fields_pre: + for field, values in zip(fields_pre, field_values_list): if not field.inherited and callable(field.translate): name = field.name translate = field.get_trans_func(fetched) - for vals in result: - vals[name] = translate(vals['id'], vals[name]) + for index in range(len(ids)): + values[index] = translate(ids[index], values[index]) # store result in cache - for vals in result: - record = self.browse(vals.pop('id'), self._prefetch) - record._cache.update(record._convert_to_cache(vals, validate=False)) + target = self.browse([], self._prefetch) + for field, values in zip(fields_pre, field_values_list): + convert = field.convert_to_cache + # Note that the target record passed to convert below is empty. + # This does not harm in practice, as it is only used in Monetary + # fields for rounding the value. As the value comes straight + # from the database, it is expected to be rounded already. + values = [convert(value, target, validate=False) for value in values] + self.env.cache.update(fetched, field, values) # determine the fields that must be processed now; # for the sake of simplicity, we ignore inherited fields @@ -3278,6 +3290,9 @@ class BaseModel(MetaModel('DummyModel', (object,), {'_register': False})): with self.env.protecting(protected_fields, self): # write stored fields with (low-level) method _write if store_vals or inverse_vals or inherited_vals: + # if log_access is enabled, this updates 'write_date' and + # 'write_uid' and check access rules, even when old_vals is + # empty self._write(store_vals) # update parent records (after possibly updating parent fields) @@ -3294,6 +3309,8 @@ class BaseModel(MetaModel('DummyModel', (object,), {'_register': False})): self.env[model_name].browse(parent_ids).write(parent_vals) if inverse_vals: + self.check_field_access_rights('write', list(inverse_vals)) + self.modified(set(inverse_vals) - set(store_vals)) # group fields by inverse method (to call it once), and order @@ -4624,7 +4641,7 @@ class BaseModel(MetaModel('DummyModel', (object,), {'_register': False})): # @classmethod - def _browse(cls, ids, env, prefetch=None): + def _browse(cls, ids, env, prefetch=None, add_prefetch=True): """ Create a recordset instance. :param ids: a tuple of record ids @@ -4637,7 +4654,8 @@ class BaseModel(MetaModel('DummyModel', (object,), {'_register': False})): if prefetch is None: prefetch = defaultdict(set) # {model_name: set(ids)} records._prefetch = prefetch - prefetch[cls._name].update(ids) + if add_prefetch: + prefetch[cls._name].update(ids) return records def browse(self, arg=None, prefetch=None): @@ -4676,9 +4694,13 @@ class BaseModel(MetaModel('DummyModel', (object,), {'_register': False})): """ Verifies that the current recorset holds a single record. Raises an exception otherwise. """ - if len(self) == 1: + try: + # unpack to ensure there is only one value is faster than len when true and + # has a significant impact as this check is largely called + _id, = self._ids return self - raise ValueError("Expected singleton: %s" % self) + except ValueError: + raise ValueError("Expected singleton: %s" % self) def with_env(self, env): """ Returns a new version of this recordset attached to the provided @@ -4944,7 +4966,7 @@ class BaseModel(MetaModel('DummyModel', (object,), {'_register': False})): def __iter__(self): """ Return an iterator over ``self``. """ for id in self._ids: - yield self._browse((id,), self.env, self._prefetch) + yield self._browse((id,), self.env, self._prefetch, False) def __contains__(self, item): """ Test whether ``item`` (record or field name) is an element of ``self``. diff --git a/odoo/tests/common.py b/odoo/tests/common.py index c7bbd0cebb7..9072cb14c10 100644 --- a/odoo/tests/common.py +++ b/odoo/tests/common.py @@ -1198,6 +1198,11 @@ class Form(object): if fields[k]['type'] == 'one2many': # o2m default gets a (6) at the start, makes no sense return [c for c in v if c[0] != 6] + elif fields[k]['type'] == 'datetime' and isinstance(v, datetime): + return odoo.fields.Datetime.to_string(v) + elif fields[k]['type'] == 'date' and isinstance(v, date): + return odoo.fields.Datetime.to_string(v) + return v defaults = { k: cleanup(k, v) @@ -1213,9 +1218,12 @@ class Form(object): vals[k] = [(6, False, [])] elif type_ == 'one2many': vals[k] = [] + elif type_ in ('integer', 'float'): + vals[k] = 0 - # TODO: check that only fields with default values should be sent - self._perform_onchange(list(defaults.keys())) + # on creation, every field is considered changed by the client + # apparently + self._perform_onchange(list(vals.keys() - {'id'})) def _init_from_values(self, values): self._values.update( @@ -1369,20 +1377,22 @@ class Form(object): node = self._get_node(f) if not node.get('force_save'): continue - # TODO: filter out (1, _, {}) from o2m values + values[f] = v return values def _perform_onchange(self, fields): assert isinstance(fields, list) - - # marks any onchange source as changed (default_get or explicit set) + # marks any onchange source as changed self._changed.update(fields) - result = self._model.onchange( - self._onchange_values(), - fields, - self._view['onchange'], - ) + + # skip calling onchange() if there's no trigger on any of the changed + # fields + spec = self._view['onchange'] + if not any(spec[f] for f in fields): + return + + result = self._model.onchange(self._onchange_values(), fields, spec) if result.get('warning'): _logger.getChild('onchange').warn("%(title)s %(message)s" % result.get('warning')) values = result.get('value', {}) @@ -1398,7 +1408,18 @@ class Form(object): ) def _onchange_values(self): - return dict(self._values) + f = self._view['fields'] + values = {} + for k, v in self._values.items(): + if f[k]['type'] == 'one2many': + # web client sends a 4 for unmodified o2m rows + values[k] = [ + (4, rid, False) if (c == 1 and not vs) else (c, rid, vs) + for (c, rid, vs) in v + ] + else: + values[k] = v + return values def _cleanup_onchange(self, descr, value, current): if descr['type'] == 'many2one': @@ -1412,6 +1433,7 @@ class Form(object): return [] v = [] + c = {t[1]: t[2] for t in current if t[0] == 1} # which view should this be??? subfields = descr['views']['edition']['fields'] for command in value: @@ -1424,7 +1446,10 @@ class Form(object): for k, v in command[2].items() if k in subfields })) - # TODO: should reuse existing values if not 5? + elif command[0] == 4: + v.append((1, command[1], c.get(command[1], {}))) + elif command[0] == 5: + v = [] return v elif descr['type'] == 'many2many': # onchange result is a bunch of commands, normalize to single 6 @@ -1444,7 +1469,7 @@ class Form(object): else: raise ValueError( "Unsupported M2M command %d" % command[0]) - return [(6, 0, ids)] + return [(6, False, ids)] return value @@ -1508,7 +1533,6 @@ class O2MForm(Form): continue # if self._get_modifier(f, 'readonly'): # continue - # TODO: filter out (1, _, {}) from o2m values values[f] = v return values @@ -1541,7 +1565,10 @@ class O2MProxy(X2MProxy): elif command == 2: pass else: - raise AssertionError("O2M proxy only supports commands 0, 1 and 2") + raise AssertionError("O2M proxy only supports commands 0, 1 and 2, found %s" % command) + + def __len__(self): + return len(self._records) @property def _model(self): @@ -1703,6 +1730,10 @@ def record_to_values(fields, record): v = [(6, 0, v.ids)] elif descr['type'] == 'one2many': v = [(1, r.id, {}) for r in v] + elif descr['type'] == 'datetime' and isinstance(v, datetime): + v = odoo.fields.Datetime.to_string(v) + elif descr['type'] == 'date' and isinstance(v, date): + v = odoo.fields.Date.to_string(v) r[f] = v return r |
