summaryrefslogtreecommitdiff
path: root/odoo
diff options
context:
space:
mode:
authorChristophe Simonis <chs@odoo.com>2019-09-20 14:16:48 +0000
committerChristophe Simonis <chs@odoo.com>2019-09-20 14:33:54 +0000
commit58a83d12228fbbd4e9dde542e133d264204ee0f3 (patch)
tree60aaf45e3281e88fddc58fea8fe75de0ad4e2f46 /odoo
parentf6da4d57bcc8e19ae0bd6d571926706bee46e08f (diff)
parent4a1321bc99dd7d8d153aceb2d0076161ce28bcd7 (diff)
[MERGE] forward port branch saas-12.4 up to 4a1321bc99d
closes odoo/odoo#37127 Signed-off-by: Christophe Simonis <chs@odoo.com>
Diffstat (limited to 'odoo')
-rw-r--r--odoo/addons/base/models/ir_mail_server.py2
-rw-r--r--odoo/addons/base/models/ir_qweb_fields.py6
-rw-r--r--odoo/addons/base/models/res_users.py37
-rw-r--r--odoo/addons/base/tests/test_ir_sequence_date_range.py2
-rw-r--r--odoo/addons/base/tests/test_user_has_group.py18
-rw-r--r--odoo/addons/base/views/ir_qweb_widget_templates.xml4
-rw-r--r--odoo/addons/base/views/res_partner_views.xml2
-rw-r--r--odoo/addons/test_testing_utilities/tests/test_methods.py6
-rw-r--r--odoo/models.py2
-rw-r--r--odoo/tests/common.py122
10 files changed, 138 insertions, 63 deletions
diff --git a/odoo/addons/base/models/ir_mail_server.py b/odoo/addons/base/models/ir_mail_server.py
index ee8944083b6..195f899172d 100644
--- a/odoo/addons/base/models/ir_mail_server.py
+++ b/odoo/addons/base/models/ir_mail_server.py
@@ -126,7 +126,7 @@ def encode_rfc2822_address_header(header_text):
return formataddr((name, email))
except UnicodeEncodeError:
_logger.warning(_('Failed to encode the address %s\n'
- 'from mail header:\n%s') % addr, header_text)
+ 'from mail header:\n%s') % (addr, header_text))
return ""
addresses = getaddresses([pycompat.to_text(ustr(header_text))])
diff --git a/odoo/addons/base/models/ir_qweb_fields.py b/odoo/addons/base/models/ir_qweb_fields.py
index 8f64560203c..2cc77269e27 100644
--- a/odoo/addons/base/models/ir_qweb_fields.py
+++ b/odoo/addons/base/models/ir_qweb_fields.py
@@ -601,7 +601,7 @@ class BarcodeConverter(models.AbstractModel):
def get_available_options(self):
options = super(BarcodeConverter, self).get_available_options()
options.update(
- type=dict(type='string', string=_('Barcode type'), description=_('Barcode type, eg: UPCA, EAN13, Code128'), default_value='Code128'),
+ symbology=dict(type='string', string=_('Barcode symbology'), description=_('Barcode type, eg: UPCA, EAN13, Code128'), default_value='Code128'),
width=dict(type='integer', string=_('Width'), default_value=600),
height=dict(type='integer', string=_('Height'), default_value=100),
humanreadable=dict(type='integer', string=_('Human Readable'), default_value=0),
@@ -610,9 +610,9 @@ class BarcodeConverter(models.AbstractModel):
@api.model
def value_to_html(self, value, options=None):
- barcode_type = options.get('type', 'Code128')
+ barcode_symbology = options.get('symbology', 'Code128')
barcode = self.env['ir.actions.report'].barcode(
- barcode_type,
+ barcode_symbology,
value,
**{key: value for key, value in options.items() if key in ['width', 'height', 'humanreadable']})
return u'<img src="data:png;base64,%s">' % base64.b64encode(barcode).decode('ascii')
diff --git a/odoo/addons/base/models/res_users.py b/odoo/addons/base/models/res_users.py
index ea8a05811f3..66b0b2f3e49 100644
--- a/odoo/addons/base/models/res_users.py
+++ b/odoo/addons/base/models/res_users.py
@@ -400,10 +400,42 @@ class Users(models.Model):
@api.constrains('groups_id')
def _check_one_user_type(self):
- for user in self:
- if len(user.groups_id.filtered(lambda x: x.category_id.xml_id == 'base.module_category_user_type')) > 1:
+ """We check that no users are both portal and users (same with public).
+ This could typically happen because of implied groups.
+ """
+ user_types_category = self.env.ref('base.module_category_user_type', raise_if_not_found=False)
+ user_types_groups = self.env['res.groups'].search(
+ [('category_id', '=', user_types_category.id)]) if user_types_category else False
+ if user_types_groups: # needed at install
+ if self._has_multiple_groups(user_types_groups.ids):
raise ValidationError(_('The user cannot have more than one user types.'))
+ def _has_multiple_groups(self, group_ids):
+ """The method is not fast if the list of ids is very long;
+ so we rather check all users than limit to the size of the group
+ :param group_ids: list of group ids
+ :return: boolean: is there at least a user in at least 2 of the provided groups
+ """
+ if group_ids:
+ args = [tuple(group_ids)]
+ if len(self.ids) == 1:
+ where_clause = "AND r.uid = %s"
+ args.append(self.id)
+ else:
+ where_clause = "" # default; we check ALL users (actually pretty efficient)
+ query = """
+ SELECT 1 FROM res_groups_users_rel WHERE EXISTS(
+ SELECT r.uid
+ FROM res_groups_users_rel r
+ WHERE r.gid IN %s""" + where_clause + """
+ GROUP BY r.uid HAVING COUNT(r.gid) > 1
+ )
+ """
+ self.env.cr.execute(query, args)
+ return bool(self.env.cr.fetchall())
+ else:
+ return False
+
def toggle_active(self):
for user in self:
if not user.active and not user.partner_id.active:
@@ -939,6 +971,7 @@ class GroupsImplied(models.Model):
JOIN group_imply i ON (r.gid = i.hid)
WHERE i.gid = %(gid)s
""", dict(gid=group.id))
+ self._check_one_user_type()
return res
class UsersImplied(models.Model):
diff --git a/odoo/addons/base/tests/test_ir_sequence_date_range.py b/odoo/addons/base/tests/test_ir_sequence_date_range.py
index 08729f3a072..43e218f1189 100644
--- a/odoo/addons/base/tests/test_ir_sequence_date_range.py
+++ b/odoo/addons/base/tests/test_ir_sequence_date_range.py
@@ -24,7 +24,7 @@ class TestIrSequenceDateRangeStandard(SingleTransactionCase):
year = date.today().year - 1
january = lambda d: date(year, 1, d)
- seq16 = self.env['ir.sequence'].with_context({'ir_sequence_date': january(16)})
+ seq16 = self.env['ir.sequence'].with_context(ir_sequence_date=january(16))
n = seq16.next_by_code('test_sequence_date_range')
self.assertEqual(n, '1')
n = seq16.next_by_code('test_sequence_date_range')
diff --git a/odoo/addons/base/tests/test_user_has_group.py b/odoo/addons/base/tests/test_user_has_group.py
index 61f6efaecfb..4f5b2bc61dd 100644
--- a/odoo/addons/base/tests/test_user_has_group.py
+++ b/odoo/addons/base/tests/test_user_has_group.py
@@ -143,3 +143,21 @@ class TestHasGroup(TransactionCase):
})
with self.assertRaises(ValidationError):
self.grp_internal.users = [(4, test_user.id)]
+
+ def test_two_user_types_implied_groups(self):
+ """Contrarily to test_two_user_types, we simply add an implied_id to a group.
+ This will trigger the addition of the relevant users to the relevant groups;
+ if, say, this was done in SQL and thus bypassing the ORM, it would bypass the constraints
+ and thus give us a case uncovered by the aforementioned test.
+ """
+ grp_test = self.env["res.groups"].create(
+ {"name": "test", "implied_ids": [(6, 0, [self.grp_internal.id])]})
+
+ test_user = self.env['res.users'].create({
+ 'login': 'test_user_portal',
+ 'name': "Test User with one user types",
+ 'groups_id': [(6, 0, [grp_test.id])]
+ })
+
+ with self.assertRaises(ValidationError):
+ grp_test.write({'implied_ids': [(4, self.grp_portal.id)]})
diff --git a/odoo/addons/base/views/ir_qweb_widget_templates.xml b/odoo/addons/base/views/ir_qweb_widget_templates.xml
index 47bf04a62ff..4f141cadb71 100644
--- a/odoo/addons/base/views/ir_qweb_widget_templates.xml
+++ b/odoo/addons/base/views/ir_qweb_widget_templates.xml
@@ -13,7 +13,7 @@
</div>
</template>
<template id="contact">
- <address t-ignore="true" class="d-inline-block mb-0" itemscope="itemscope" itemtype="http://schema.org/Organization">
+ <address t-ignore="true" class="mb-0" itemscope="itemscope" itemtype="http://schema.org/Organization">
<div t-if="not (('name' in fields) or (address and 'address' in fields) or (city and 'city' in fields) or (mobile and 'mobile' in fields) or (website and 'website' in fields) or (email and 'email' in fields))" class="css_non_editable_mode_hidden">
--<span class="text-muted" t-esc="name"/>--
</div>
@@ -23,7 +23,7 @@
<div itemprop="address" itemscope="itemscope" itemtype="http://schema.org/PostalAddress">
<div t-if="address and 'address' in fields" class="d-flex align-items-baseline">
<i t-if="not options.get('no_marker')" class="fa fa-map-marker fa-fw" role="img" aria-label="Address" title="Address"/>
- <span itemprop="streetAddress" t-raw="address.replace('\n', options.get('no_tag_br') and ', ' or ('&lt;br/&gt;%s' % ('' if options.get('no_marker') else '')))"/>
+ <div class="w-100" itemprop="streetAddress" t-raw="address.replace('\n', options.get('no_tag_br') and ', ' or ('&lt;br/&gt;%s' % ('' if options.get('no_marker') else '')))"/>
</div>
<div t-if="city and 'city' in fields" class="d-flex align-items-baseline">
<i t-if="not options.get('no_marker')" class="fa fa-map-marker fa-fw" role="img" aria-label="Address" title="Address"/>
diff --git a/odoo/addons/base/views/res_partner_views.xml b/odoo/addons/base/views/res_partner_views.xml
index 96b2f0dcb3c..aa8e65f5508 100644
--- a/odoo/addons/base/views/res_partner_views.xml
+++ b/odoo/addons/base/views/res_partner_views.xml
@@ -524,7 +524,7 @@
<img class="o_kanban_image_inner_pic" t-if="record.parent_id.raw_value" t-att-alt="record.parent_id.value" t-att-src="kanban_image('res.partner', 'image_128', record.parent_id.raw_value)"/>
</div>
</t>
- <t t-else="">
+ <t t-elif="record.image_128.raw_value">
<t t-set="placeholder" t-value="'/base/static/img/company_image.png'"/>
<div class="o_kanban_image_fill_left o_kanban_image_full" t-attf-style="background-image: url(#{kanban_image('res.partner', 'image_128', record.id.raw_value, placeholder)})" role="img"/>
</t>
diff --git a/odoo/addons/test_testing_utilities/tests/test_methods.py b/odoo/addons/test_testing_utilities/tests/test_methods.py
index 031d3a5e2ec..2a783aa2166 100644
--- a/odoo/addons/test_testing_utilities/tests/test_methods.py
+++ b/odoo/addons/test_testing_utilities/tests/test_methods.py
@@ -11,9 +11,11 @@ class TestBasic(common.TransactionCase):
Y3 = {'f1': "Y", 'f2': 3}
records = self.env['test_testing_utilities.a'].create([X1, Y2])
- # match in any order
self.assertRecordValues(records, [X1, Y2])
- self.assertRecordValues(records, [Y2, X1])
+
+ with self.assertRaises(AssertionError):
+ # order should match
+ self.assertRecordValues(records, [Y2, X1])
# fail if wrong size
with self.assertRaises(AssertionError):
diff --git a/odoo/models.py b/odoo/models.py
index 92860de3c41..5ba3b364a9e 100644
--- a/odoo/models.py
+++ b/odoo/models.py
@@ -5560,7 +5560,7 @@ Record ids: %(records)s
real_records = self - new_records
records = model.browse()
if real_records:
- records |= model.search([(key.name, 'in', real_records.ids)])
+ records |= model.search([(key.name, 'in', real_records.ids)], order='id')
if new_records:
cache_records = self.env.cache.get_records(model, key)
records |= cache_records.filtered(lambda r: set(r[key.name]._ids) & set(self._ids))
diff --git a/odoo/tests/common.py b/odoo/tests/common.py
index e02b174652b..706626394db 100644
--- a/odoo/tests/common.py
+++ b/odoo/tests/common.py
@@ -23,6 +23,7 @@ import tempfile
import threading
import time
import unittest
+import difflib
import werkzeug.urls
from contextlib import contextmanager
from datetime import datetime, date
@@ -315,74 +316,95 @@ class BaseCase(TreeCase, MetaCase('DummyCase', (object,), {})):
:param expected_values: List of dicts expected to be exactly matched in records
'''
- def _compare_candidate(record, candidate):
- ''' Return True if the candidate matches the given record '''
- for field_name in candidate.keys():
+ def _compare_candidate(record, candidate, field_names):
+ ''' Compare all the values in `candidate` with a record.
+ :param record: record being compared
+ :param candidate: dict of values to compare
+ :return: A dictionary will encountered difference in values.
+ '''
+ diff = {}
+ for field_name in field_names:
record_value = record[field_name]
- candidate_value = candidate[field_name]
field = record._fields[field_name]
field_type = field.type
if field_type == 'monetary':
# Compare monetary field.
currency_field_name = record._fields[field_name].currency_field
record_currency = record[currency_field_name]
- if record_currency.compare_amounts(candidate_value, record_value)\
- if record_currency else candidate_value != record_value:
- return False
+ if field_name not in candidate:
+ diff[field_name] = (record_value, None)
+ elif record_currency:
+ if record_currency.compare_amounts(candidate[field_name], record_value):
+ diff[field_name] = (record_value, record_currency.round(candidate[field_name]))
+ elif candidate[field_name] != record_value:
+ diff[field_name] = (record_value, candidate[field_name])
elif field_type == 'float' and field.get_digits(record.env):
prec = field.get_digits(record.env)[1]
- if float_compare(candidate_value, record_value, precision_digits=prec) != 0:
- return False
+ if float_compare(candidate[field_name], record_value, precision_digits=prec) != 0:
+ diff[field_name] = (record_value, candidate[field_name])
elif field_type in ('one2many', 'many2many'):
# Compare x2many relational fields.
# Empty comparison must be an empty list to be True.
- if set(record_value.ids) != set(candidate_value):
- return False
+ if field_name not in candidate:
+ diff[field_name] = (sorted(record_value.ids), None)
+ elif set(record_value.ids) != set(candidate[field_name]):
+ diff[field_name] = (sorted(record_value.ids), sorted(candidate[field_name]))
elif field_type == 'many2one':
# Compare many2one relational fields.
# Every falsy value is allowed to compare with an empty record.
- if (record_value or candidate_value) and record_value.id != candidate_value:
- return False
- elif (candidate_value or record_value) and record_value != candidate_value:
+ if field_name not in candidate:
+ diff[field_name] = (record_value.id, None)
+ elif (record_value or candidate[field_name]) and record_value.id != candidate[field_name]:
+ diff[field_name] = (record_value.id, candidate[field_name])
+ else:
# Compare others fields if not both interpreted as falsy values.
- return False
- return True
-
- def _repr_field_value(record, field_name):
- record_value = record[field_name]
- field_type = record._fields[field_name].type
- if field_type == 'monetary':
- currency_field_name = record._fields[field_name].currency_field
- record_currency = record[currency_field_name]
- return record_currency and record_currency.round(record_value) or record_value
- elif field_type in ('one2many', 'many2many'):
- return set(record_value.ids)
- elif field_type == 'many2one':
- return record_value.id
- else:
- return record_value
-
- def _format_message(records, expected_values):
- ''' Return a formatted representation of records/expected_values. '''
- all_records_values = [{key: _repr_field_value(record, key) for key in expected_values[0]} for record in records]
- msg1 = '\n'.join(pprint.pformat(dic) for dic in all_records_values)
- msg2 = '\n'.join(pprint.pformat(dic) for dic in expected_values)
- return 'Current values:\n\n%s\n\nExpected values:\n\n%s' % (msg1, msg2)
+ if field_name not in candidate:
+ diff[field_name] = (record_value, None)
+ elif (candidate[field_name] or record_value) and record_value != candidate[field_name]:
+ diff[field_name] = (record_value, candidate[field_name])
+ return diff
+
+ # Compare records with candidates.
+ different_values = []
+ field_names = list(expected_values[0].keys())
+ for index, record in enumerate(records):
+ is_additional_record = index >= len(expected_values)
+ candidate = {} if is_additional_record else expected_values[index]
+ diff = _compare_candidate(record, candidate, field_names)
+ if diff:
+ different_values.append((index, 'additional_record' if is_additional_record else 'regular_diff', diff))
+ for index in range(len(records), len(expected_values)):
+ diff = {}
+ for field_name in field_names:
+ diff[field_name] = (None, expected_values[index][field_name])
+ different_values.append((index, 'missing_record', diff))
+
+ # Build error message.
+ if not different_values:
+ return
- # if the length or both things to compare is different, we can already tell they're not equal
+ errors = ['The records and expected_values do not match.']
if len(records) != len(expected_values):
- msg = 'Wrong number of records to compare: %d != %d.\n\n' % (len(records), len(expected_values))
- self.fail(msg + _format_message(records, expected_values))
-
- candidates = list(expected_values)
- for index, record in enumerate(records):
- for candidate_index, candidate in enumerate(candidates):
- if _compare_candidate(record, candidate):
- candidates.pop(candidate_index)
- break
- else:
- msg = 'Record doesn\'t match expected values at index %d.\n\n' % index
- self.fail(msg + _format_message(records, expected_values))
+ errors.append('Wrong number of records to compare: %d records versus %d expected values.' % (len(records), len(expected_values)))
+
+ for index, diff_type, diff in different_values:
+ if diff_type == 'regular_diff':
+ errors.append('\n==== Differences at index %s ====' % index)
+ record_diff = ['%s:%s' % (k, v[0]) for k, v in diff.items()]
+ candidate_diff = ['%s:%s' % (k, v[1]) for k, v in diff.items()]
+ errors.append('\n'.join(difflib.unified_diff(record_diff, candidate_diff)))
+ elif diff_type == 'additional_record':
+ errors += [
+ '\n==== Additional record ====',
+ pprint.pformat(dict((k, v[0]) for k, v in diff.items())),
+ ]
+ elif diff_type == 'missing_record':
+ errors += [
+ '\n==== Missing record ====',
+ pprint.pformat(dict((k, v[1]) for k, v in diff.items())),
+ ]
+
+ self.fail('\n'.join(errors))
def shortDescription(self):
return None