summaryrefslogtreecommitdiff
path: root/odoo
diff options
context:
space:
mode:
authorChristophe Simonis <chs@odoo.com>2019-02-21 15:37:19 +0100
committerChristophe Simonis <chs@odoo.com>2019-02-21 15:37:19 +0100
commit1d2b6f1bef6af13245d29d56cbb5f9d61f9ca038 (patch)
tree8ea1d203aef83d287de9ec578f81bd64eedb7359 /odoo
parentf945b1bcc21621cfc056e40f4d66b7181d19239e (diff)
parent84143a34b313c01b0b37b7d7246ce3d5c9476002 (diff)
[MERGE] forward port branch 12.0 up to 84143a34b31
Diffstat (limited to 'odoo')
-rw-r--r--odoo/addons/base/data/res.lang.csv4
-rw-r--r--odoo/addons/base/models/ir_actions_report.py5
-rw-r--r--odoo/addons/base/models/ir_ui_view.py18
-rw-r--r--odoo/addons/base/models/res_country.py2
-rw-r--r--odoo/addons/base/models/res_partner.py6
-rw-r--r--odoo/addons/base/tests/test_views.py60
-rw-r--r--odoo/addons/test_new_api/tests/test_new_fields.py50
-rw-r--r--odoo/addons/test_testing_utilities/menu.xml13
-rw-r--r--odoo/addons/test_testing_utilities/models.py5
-rw-r--r--odoo/addons/test_testing_utilities/tests/test_form_impl.py26
-rw-r--r--odoo/api.py9
-rw-r--r--odoo/fields.py22
-rw-r--r--odoo/models.py62
-rw-r--r--odoo/tests/common.py61
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