18.0 vanilla

This commit is contained in:
Ernad Husremovic 2025-10-03 18:06:50 +02:00
parent d72e748793
commit 0a7ae8db93
337 changed files with 399651 additions and 232598 deletions

View file

@ -15,7 +15,9 @@ from . import test_expression
from . import test_float
from . import test_format_address_mixin
from . import test_func
from . import test_groups
from . import test_http_case
from . import test_i18n
from . import test_image
from . import test_avatar_mixin
from . import test_ir_actions
@ -29,6 +31,7 @@ from . import test_ir_model
from . import test_ir_module
from . import test_ir_sequence
from . import test_ir_sequence_date_range
from . import test_ir_embedded_actions
from . import test_ir_default
from . import test_mail
from . import test_menu
@ -49,6 +52,7 @@ from . import test_sql
from . import test_translate
from . import test_tz
# from . import test_uninstall # loop
from . import test_upgrade_code
from . import test_user_has_group
from . import test_views
from . import test_xmlrpc
@ -57,6 +61,7 @@ from . import test_res_currency
from . import test_res_country
from . import test_res_partner
from . import test_res_partner_bank
from . import test_res_partner_merge
from . import test_res_users
from . import test_reports
from . import test_test_retry
@ -70,4 +75,5 @@ from . import test_pdf
from . import test_neutralize
from . import test_config_parameter
from . import test_ir_module_category
from . import test_configmanager
from . import test_num2words_ar

View file

@ -1,13 +1,12 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
import threading
from contextlib import contextmanager
from unittest.mock import patch, Mock
from odoo.tests.common import TransactionCase, HttpCase
from odoo import Command
from odoo import Command, modules
from odoo.tests.common import new_test_user, TransactionCase, HttpCase
from odoo.tools.mail import email_split_and_format
DISABLED_MAIL_CONTEXT = {
'tracking_disable': True,
@ -27,12 +26,72 @@ class BaseCommon(TransactionCase):
# Mail logic won't be tested by default in other modules.
# Mail API overrides should be tested with dedicated tests on purpose
# Hack to use with_context and avoid manual context dict modification
cls.env = cls.env['base'].with_context(**DISABLED_MAIL_CONTEXT).env
cls.env = cls.env['base'].with_context(**cls.default_env_context()).env
independent_user = cls.setup_independent_user()
if independent_user:
cls.env = cls.env(user=independent_user)
cls.user = cls.env.user
independent_company = cls.setup_independent_company()
if independent_company:
# avoid using the context to assign companies
cls.env.user.company_id = independent_company
cls.env.user.company_ids = [Command.set(independent_company.ids)]
else:
cls.setup_main_company()
# Make sure all class variables have the same env.
# Do not specify any class variables before the env changes.
cls.company = cls.env.company
cls.currency = cls.env.company.currency_id
cls.partner = cls.env['res.partner'].create({
'name': 'Test Partner',
})
cls.currency = cls.env.company.currency_id
cls.group_portal = cls.env.ref('base.group_portal')
cls.group_user = cls.env.ref('base.group_user')
cls.group_system = cls.env.ref('base.group_system')
@classmethod
def default_env_context(cls):
""" To Override to reactivate the tracking """
return {**DISABLED_MAIL_CONTEXT}
@classmethod
def setup_other_currency(cls, code, **kwargs):
rates = kwargs.pop('rates', [])
currency = cls.env['res.currency'].with_context(active_test=False).search([('name', '=', code)], limit=1)
currency.rate_ids.unlink()
currency.write({
'active': True,
'rate_ids': [Command.create(
{
'name': rate_date,
'rate': rate,
'company_id': cls.env.company.id,
}
) for rate_date, rate in rates],
**kwargs,
})
return currency
@classmethod
def setup_independent_company(cls, **kwargs):
return None
@classmethod
def setup_independent_user(cls):
return None
@classmethod
def get_default_groups(cls):
return cls.env['res.users']._default_groups()
@classmethod
def setup_main_company(cls, currency_code='USD'):
cls._use_currency(cls.env.company, currency_code)
@classmethod
def _enable_currency(cls, currency_code):
@ -43,40 +102,58 @@ class BaseCommon(TransactionCase):
return currency
@classmethod
def _use_currency(cls, currency_code):
def _use_currency(cls, company, currency_code):
# Enforce constant currency
currency = cls._enable_currency(currency_code)
if not cls.env.company.currency_id == currency:
if company.currency_id != currency:
cls.env.transaction.cache.set(cls.env.company, type(cls.env.company).currency_id, currency.id, dirty=True)
# this is equivalent to cls.env.company.currency_id = currency but without triggering buisness code checks.
# The value is added in cache, and the cache value is set as dirty so that that
# the value will be written to the database on next flush.
# this was needed because some journal entries may exist when running tests, especially l10n demo data.
@classmethod
def _create_partner(cls, **create_values):
return cls.env['res.partner'].create({
'name': "Test Partner",
'company_id': False,
**create_values,
})
@classmethod
def _create_company(cls, **create_values):
company = cls.env['res.company'].create({
'name': "Test Company",
**create_values,
})
cls.env.user.company_ids = [Command.link(company.id)]
# cls.env.context['allowed_company_ids'].append(company.id)
return company
@classmethod
def _create_new_internal_user(cls, **kwargs):
return new_test_user(
cls.env,
**({'login': 'internal_user'} | kwargs),
)
@classmethod
def _create_new_portal_user(cls, **kwargs):
return new_test_user(
cls.env,
groups='base.group_portal',
**({'login': 'portal_user'} | kwargs),
)
class BaseUsersCommon(BaseCommon):
@classmethod
def setUpClass(cls):
super().setUpClass()
cls.group_portal = cls.env.ref('base.group_portal')
cls.group_user = cls.env.ref('base.group_user')
cls.user_portal = cls.env['res.users'].create({
'name': 'Test Portal User',
'login': 'portal_user',
'password': 'portal_user',
'email': 'portal_user@gladys.portal',
'groups_id': [Command.set([cls.group_portal.id])],
})
cls.user_internal = cls.env['res.users'].create({
'name': 'Test Internal User',
'login': 'internal_user',
'password': 'internal_user',
'email': 'mark.brown23@example.com',
'groups_id': [Command.set([cls.group_user.id])],
})
cls.user_portal = cls._create_new_portal_user()
cls.user_internal = cls._create_new_internal_user()
class TransactionCaseWithUserDemo(TransactionCase):
@ -323,19 +400,15 @@ class MockSmtplibCase:
def send_message(self, message, smtp_from, smtp_to_list):
origin.emails.append({
'smtp_from': smtp_from,
'smtp_to_list': smtp_to_list,
# message
'message': message.as_string(),
'msg_cc': message['Cc'],
'msg_from': message['From'],
'from_filter': self.from_filter,
})
def sendmail(self, smtp_from, smtp_to_list, message_str, mail_options):
origin.emails.append({
'msg_from_fmt': email_split_and_format(message['From'])[0],
'msg_to': message['To'],
# smtp
'smtp_from': smtp_from,
'smtp_to_list': smtp_to_list,
'message': message_str,
'msg_from': None, # to fix if necessary
'from_filter': self.from_filter,
})
@ -370,7 +443,7 @@ class MockSmtplibCase:
with patch('smtplib.SMTP_SSL', side_effect=lambda *args, **kwargs: self.testing_smtp_session), \
patch('smtplib.SMTP', side_effect=lambda *args, **kwargs: self.testing_smtp_session), \
patch.object(type(IrMailServer), '_is_test_mode', lambda self: False), \
patch.object(modules.module, 'current_test', False), \
patch.object(type(IrMailServer), 'connect', mock_function(connect_origin)) as connect_mocked, \
patch.object(type(IrMailServer), '_find_mail_server', mock_function(find_mail_server_origin)) as find_mail_server_mocked:
self.connect_mocked = connect_mocked.mock
@ -378,23 +451,27 @@ class MockSmtplibCase:
yield
def _build_email(self, mail_from, return_path=None, **kwargs):
headers = {'Return-Path': return_path} if return_path else {}
headers.update(**kwargs.pop('headers', {}))
return self.env['ir.mail_server'].build_email(
mail_from,
kwargs.pop('email_to', 'dest@example-é.com'),
kwargs.pop('subject', 'subject'),
kwargs.pop('body', 'body'),
headers={'Return-Path': return_path} if return_path else None,
headers=headers,
**kwargs,
)
def _send_email(self, msg, smtp_session):
with patch.object(threading.current_thread(), 'testing', False):
with patch.object(modules.module, 'current_test', False):
self.env['ir.mail_server'].send_email(msg, smtp_session=smtp_session)
return smtp_session.messages.pop()
def assertSMTPEmailsSent(self, smtp_from=None, smtp_to_list=None, message_from=None,
def assertSMTPEmailsSent(self, smtp_from=None, smtp_to_list=None,
message_from=None, msg_from=None,
mail_server=None, from_filter=None,
emails_count=1):
emails_count=1,
msg_cc_lst=None, msg_to_lst=None):
"""Check that the given email has been sent. If one of the parameter is
None it is just ignored and not used to retrieve the email.
@ -406,40 +483,47 @@ class MockSmtplibCase:
:param from_filter: from_filter of the <ir.mail_server> used to send the
email. False means 'match everything';'
:param emails_count: the number of emails which should match the condition
:param msg_cc: optional check msg_cc value of email;
:param msg_to: optional check msg_to value of email;
:return: True if at least one email has been found with those parameters
"""
if from_filter is not None and mail_server:
raise ValueError('Invalid usage: use either from_filter either mail_server')
if from_filter is None and mail_server is not None:
from_filter = mail_server.from_filter
matching_emails = filter(
matching_emails = list(filter(
lambda email:
(smtp_from is None or smtp_from == email['smtp_from'])
and (smtp_to_list is None or smtp_to_list == email['smtp_to_list'])
and (message_from is None or 'From: %s' % message_from in email['message'])
# might have header being name <email> instead of "name" <email>, to check
and (msg_from is None or (msg_from == email['msg_from'] or msg_from == email['msg_from_fmt']))
and (from_filter is None or from_filter == email['from_filter']),
self.emails,
)
))
debug_info = ''
matching_emails_count = len(list(matching_emails))
matching_emails_count = len(matching_emails)
if matching_emails_count != emails_count:
emails_from = []
for email in self.emails:
from_found = next((
line.split('From:')[1].strip() for line in email['message'].splitlines()
if line.startswith('From:')), '')
emails_from.append(from_found)
debug_info = '\n'.join(
f"SMTP-From: {email['smtp_from']}, SMTP-To: {email['smtp_to_list']}, Msg-From: {email_msg_from}, From_filter: {email['from_filter']})"
for email, email_msg_from in zip(self.emails, emails_from)
f"SMTP-From: {email['smtp_from']}, SMTP-To: {email['smtp_to_list']}, "
f"Msg-From: {email['msg_from']}, From_filter: {email['from_filter']})"
for email in self.emails
)
self.assertEqual(
matching_emails_count, emails_count,
msg=f'Incorrect emails sent: {matching_emails_count} found, {emails_count} expected'
f'\nConditions\nSMTP-From: {smtp_from}, SMTP-To: {smtp_to_list}, Msg-From: {message_from}, From_filter: {from_filter}'
f'\nConditions\nSMTP-From: {smtp_from}, SMTP-To: {smtp_to_list}, Msg-From: {message_from or msg_from}, From_filter: {from_filter}'
f'\nNot found in\n{debug_info}'
)
if msg_to_lst is not None:
for email in matching_emails:
self.assertListEqual(sorted(email_split_and_format(email['msg_to'])), sorted(msg_to_lst))
if msg_cc_lst is not None:
for email in matching_emails:
self.assertListEqual(sorted(email_split_and_format(email['msg_cc'])), sorted(msg_cc_lst))
@classmethod
def _init_mail_gateway(cls):

View file

@ -0,0 +1,65 @@
[options]
admin_passwd = admin
csv_internal_sep = ,
db_host = False
db_maxconn = 64
db_name = False
db_password = False
db_port = False
db_sslmode = prefer
db_template = template0
db_user = False
dbfilter =
demo = {}
email_from = False
from_filter = False
geoip_database = /usr/share/GeoIP/GeoLite2-City.mmdb
gevent_port = 8072
http_enable = True
http_interface =
http_port = 8069
import_partial =
limit_memory_hard = 2684354560
limit_memory_soft = 2147483648
limit_request = 65536
limit_time_cpu = 60
limit_time_real = 120
limit_time_real_cron = -1
list_db = True
log_db = False
log_db_level = warning
log_handler = :INFO
log_level = info
logfile =
max_cron_threads = 2
osv_memory_age_limit = False
osv_memory_count_limit = 0
pg_path =
pidfile =
proxy_mode = False
reportgz = False
screencasts =
screenshots = /tmp/odoo_tests
server_wide_modules = base,web
smtp_password = False
smtp_port = 25
smtp_server = localhost
smtp_ssl = False
smtp_ssl_certificate_filename = False
smtp_ssl_private_key_filename = False
smtp_user = False
syslog = False
test_enable = False
test_file =
test_tags = None
transient_age_limit = 1.0
translate_modules = ['all']
unaccent = False
upgrade_path =
websocket_keep_alive_timeout = 3600
websocket_rate_limit_burst = 10
websocket_rate_limit_delay = 0.2
without_demo = False
workers = 0
x_sendfile = False

View file

@ -0,0 +1,82 @@
--init stock,hr
--update account,website
--without-demo rigolo
--import-partial /tmp/import-partial
--pidfile /tmp/pidfile
--load base,mail
--data-dir /tmp/data-dir
--no-http
--http-interface 10.0.0.254
--http-port 6942
--gevent-port 8012
--proxy-mode
--x-sendfile
--db-filter .*
--test-file /tmp/file-file
--test-tags :TestMantra.test_is_extra_mile_done
--screencasts /tmp/screencasts
--screenshots /tmp/screenshots
--logfile /tmp/odoo.log
--log-handler odoo.tools.config:DEBUG
--log-handler :WARNING
--log-web
--log-sql
--log-db logdb
--log-db-level debug
--log-level debug
--email-from admin@example.com
--from-filter .*
--smtp smtp.localhost
--smtp-port 1299
--smtp-ssl
--smtp-user spongebob
--smtp-password Tigrou0072
--smtp-ssl-certificate-filename /tmp/tlscert
--smtp-ssl-private-key-filename /tmp/tlskey
--database horizon
--db_user kiwi
--db_password Tigrou0073
--pg_path /tmp/pg_path
--db_host db.localhost
--db_port 4269
--db_sslmode verify-full
--db_maxconn 42
--db_maxconn_gevent 100
--db-template backup1706
--db_replica_host db2.localhost
--db_replica_port 2038
--load-language fr_FR
--language fr_FR
--i18n-export /tmp/translate_out.csv
--i18n-import /tmp/translate_in.csv
--i18n-overwrite
--modules stock,hr,mail
--no-database-list
--dev xml,reload
--shell-interface ipython
--stop-after-init
--osv-memory-count-limit 71
--transient-age-limit 4
--max-cron-threads 4
--unaccent
--geoip-city-db /tmp/city.db
--geoip-country-db /tmp/country.db
--workers 92
--limit-memory-soft 1048576
--limit-memory-soft-gevent 1048577
--limit-memory-hard 1048578
--limit-memory-hard-gevent 1048579
--limit-time-cpu 60
--limit-time-real 61
--limit-time-real-cron 62
--limit-request 100

View file

@ -0,0 +1 @@
[options]

View file

@ -0,0 +1,109 @@
[options]
# options not exposed on the command line
admin_passwd = Tigrou007
csv_internal_sep = @
publisher_warranty_url = http://example.com
reportgz = True
root_path = /tmp/root_path
websocket_rate_limit_burst = 1
websocket_rate_limit_delay = 2
websocket_keep_alive_timeout = 600
# common
config = /tmp/config
save = True
init = stock,hr
update = account,website
without_demo = True
import_partial = /tmp/import-partial
pidfile = /tmp/pidfile
addons_path = /tmp/odoo
upgrade_path = /tmp/upgrade
pre_upgrade_scripts = /tmp/pre-custom.py
server_wide_modules = base,mail
data_dir = /tmp/data-dir
# HTTP
http_interface = 10.0.0.254
http_port = 6942
gevent_port = 8012
http_enable = False
proxy_mode = True
x_sendfile = True
# web
dbfilter = .*
# testing
test_file = /tmp/file-file
test_enable = True
test_tags = :TestMantra.test_is_extra_mile_done
screencasts = /tmp/screencasts
screenshots = /tmp/screenshots
# logging
logfile = /tmp/odoo.log
syslog = False
log_handler = :DEBUG
log_db = logdb
log_db_level = debug
log_level = debug
# SMTP
email_from = admin@example.com
from_filter = .*
smtp_server = smtp.localhost
smtp_port = 1299
smtp_ssl = True
smtp_user = spongebob
smtp_password = Tigrou0072
smtp_ssl_certificate_filename = /tmp/tlscert
smtp_ssl_private_key_filename = /tmp/tlskey
# database
db_name = horizon
db_user = kiwi
db_password = Tigrou0073
pg_path = /tmp/pg_path
db_host = db.localhost
db_port = 4269
db_sslmode = verify-full
db_maxconn = 42
db_maxconn_gevent = 100
db_template = backup1706
db_replica_host = db2.localhost
db_replica_port = 2038
# i18n
load_language = fr_FR
language = fr_FR
translate_out = /tmp/translate_out.csv
translate_in = /tmp/translate_in.csv
overwrite_existing_translations = True
translate_modules = stock,hr,mail
# security
list_db = False
# advanced
dev_mode = xml
shell_interface = ipython
stop_after_init = True
osv_memory_count_limit = 71
transient_age_limit = 4.0
max_cron_threads = 4
limit_time_worker_cron = 600
unaccent = True
geoip_city_db = /tmp/city.db
geoip_country_db = /tmp/country.db
# multiprocessing
workers = 92
limit_memory_soft = 1048576
limit_memory_soft_gevent = 1048577
limit_memory_hard = 1048578
limit_memory_hard_gevent = 1048579
limit_time_cpu = 60
limit_time_real = 61
limit_time_real_cron = 62
limit_request = 100

View file

@ -0,0 +1,72 @@
[options]
addons_path = {root_path}/odoo/addons,{root_path}/addons
admin_passwd = admin
csv_internal_sep = ,
data_dir = {homedir}/.local/share/Odoo
db_host = False
db_maxconn = 64
db_maxconn_gevent = False
db_name = False
db_password = False
db_port = False
db_replica_host = False
db_replica_port = False
db_sslmode = prefer
db_template = template0
db_user = False
dbfilter =
email_from = False
from_filter = False
geoip_city_db = /usr/share/GeoIP/GeoLite2-City.mmdb
geoip_country_db = /usr/share/GeoIP/GeoLite2-Country.mmdb
gevent_port = 8072
http_enable = True
http_interface =
http_port = 8069
import_partial =
limit_memory_hard = 2684354560
limit_memory_hard_gevent = False
limit_memory_soft = 2147483648
limit_memory_soft_gevent = False
limit_request = 65536
limit_time_cpu = 60
limit_time_real = 120
limit_time_real_cron = -1
limit_time_worker_cron = 0
list_db = True
log_db = False
log_db_level = warning
log_handler = :INFO
log_level = info
logfile =
max_cron_threads = 2
osv_memory_count_limit = 0
pg_path =
pidfile =
pre_upgrade_scripts =
proxy_mode = False
reportgz = False
screencasts =
screenshots = /tmp/odoo_tests
server_wide_modules = base,web
smtp_password = False
smtp_port = 25
smtp_server = localhost
smtp_ssl = False
smtp_ssl_certificate_filename = False
smtp_ssl_private_key_filename = False
smtp_user = False
syslog = False
test_enable = False
test_file =
test_tags = None
transient_age_limit = 1.0
translate_modules = ['all']
unaccent = False
upgrade_path =
websocket_keep_alive_timeout = 3600
websocket_rate_limit_burst = 10
websocket_rate_limit_delay = 0.2
without_demo = False
workers = 0
x_sendfile = False

View file

@ -9,19 +9,30 @@ from odoo.tests.common import TransactionCase
from odoo.tools.misc import mute_logger
from odoo import Command
# test group that demo user should not have
GROUP_SYSTEM = 'base.group_system'
class TestACL(TransactionCaseWithUserDemo):
def setUp(self):
super(TestACL, self).setUp()
self.erp_system_group = self.env.ref(GROUP_SYSTEM)
@classmethod
def setUpClass(cls):
super().setUpClass()
cls.TEST_GROUP = 'base.base_test_group'
cls.test_group = cls.env['res.groups'].create({
'name': 'test with implied user',
'implied_ids': [Command.link(cls.env.ref('base.group_user').id)]
})
cls.env["ir.model.data"].create({
"module": "base",
"name": "base_test_group",
"model": "res.groups",
"res_id": cls.test_group.id,
})
def _set_field_groups(self, model, field_name, groups):
field = model._fields[field_name]
self.patch(field, 'groups', groups)
self.env.invalidate_all()
self.env.registry.clear_cache('templates')
def test_field_visibility_restriction(self):
"""Check that model-level ``groups`` parameter effectively restricts access to that
@ -29,35 +40,28 @@ class TestACL(TransactionCaseWithUserDemo):
currency = self.env['res.currency'].with_user(self.user_demo)
# Add a view that adds a label for the field we are going to check
extension = self.env["ir.ui.view"].create({
primary = self.env["ir.ui.view"].create({
"name": "Add separate label for decimal_places",
"model": "res.currency",
"inherit_id": self.env.ref("base.view_currency_form").id,
"arch": """
<data>
<field name="decimal_places" position="attributes">
<attribute name="nolabel">1</attribute>
</field>
<field name="decimal_places" position="before">
"type": "form",
"priority": 1,
"arch": """<form>
<group>
<group string="Price Accuracy">
<field name="rounding"/>
<label for="decimal_places"/>
</field>
</data>
""",
<field name="decimal_places" nolabel="1"/>
</group>
</group>
</form>""",
})
currency = currency.with_context(check_view_ids=extension.ids)
# Verify the test environment first
original_fields = currency.fields_get([])
with self.debug_mode():
# <group groups="base.group_no_one">
# <group string="Price Accuracy">
# <field name="rounding"/>
# <field name="decimal_places"/>
# </group>
form_view = currency.get_view(False, 'form')
form_view = currency.get_view(primary.id, 'form')
view_arch = etree.fromstring(form_view.get('arch'))
has_group_system = self.user_demo.has_group(GROUP_SYSTEM)
self.assertFalse(has_group_system, "`demo` user should not belong to the restricted group before the test")
has_group_test = self.user_demo.has_group(self.TEST_GROUP)
self.assertFalse(has_group_test, "`demo` user should not belong to the restricted group before the test")
self.assertIn('decimal_places', original_fields, "'decimal_places' field must be properly visible before the test")
self.assertNotEqual(view_arch.xpath("//field[@name='decimal_places'][@nolabel='1']"), [],
"Field 'decimal_places' must be found in view definition before the test")
@ -65,10 +69,10 @@ class TestACL(TransactionCaseWithUserDemo):
"Label for 'decimal_places' must be found in view definition before the test")
# restrict access to the field and check it's gone
self._set_field_groups(currency, 'decimal_places', GROUP_SYSTEM)
self._set_field_groups(currency, 'decimal_places', self.TEST_GROUP)
fields = currency.fields_get([])
form_view = currency.get_view(False, 'form')
form_view = currency.get_view(primary.id, 'form')
view_arch = etree.fromstring(form_view.get('arch'))
self.assertNotIn('decimal_places', fields, "'decimal_places' field should be gone")
self.assertEqual(view_arch.xpath("//field[@name='decimal_places']"), [],
@ -77,13 +81,12 @@ class TestACL(TransactionCaseWithUserDemo):
"Label for 'decimal_places' must not be found in view definition")
# Make demo user a member of the restricted group and check that the field is back
self.erp_system_group.users += self.user_demo
has_group_system = self.user_demo.has_group(GROUP_SYSTEM)
self.test_group.users += self.user_demo
has_group_test = self.user_demo.has_group(self.TEST_GROUP)
fields = currency.fields_get([])
with self.debug_mode():
form_view = currency.get_view(False, 'form')
form_view = currency.get_view(primary.id, 'form')
view_arch = etree.fromstring(form_view.get('arch'))
self.assertTrue(has_group_system, "`demo` user should now belong to the restricted group")
self.assertTrue(has_group_test, "`demo` user should now belong to the restricted group")
self.assertIn('decimal_places', fields, "'decimal_places' field must be properly visible again")
self.assertNotEqual(view_arch.xpath("//field[@name='decimal_places']"), [],
"Field 'decimal_places' must be found in view definition again")
@ -96,13 +99,13 @@ class TestACL(TransactionCaseWithUserDemo):
partner = self.env['res.partner'].browse(1).with_user(self.user_demo)
# Verify the test environment first
has_group_system = self.user_demo.has_group(GROUP_SYSTEM)
self.assertFalse(has_group_system, "`demo` user should not belong to the restricted group")
has_group_test = self.user_demo.has_group(self.TEST_GROUP)
self.assertFalse(has_group_test, "`demo` user should not belong to the restricted group")
self.assertTrue(partner.read(['bank_ids']))
self.assertTrue(partner.write({'bank_ids': []}))
# Now restrict access to the field and check it's forbidden
self._set_field_groups(partner, 'bank_ids', GROUP_SYSTEM)
self._set_field_groups(partner, 'bank_ids', self.TEST_GROUP)
with self.assertRaises(AccessError):
partner.search_fetch([], ['bank_ids'])
@ -114,9 +117,9 @@ class TestACL(TransactionCaseWithUserDemo):
partner.write({'bank_ids': []})
# Add the restricted group, and check that it works again
self.erp_system_group.users += self.user_demo
has_group_system = self.user_demo.has_group(GROUP_SYSTEM)
self.assertTrue(has_group_system, "`demo` user should now belong to the restricted group")
self.test_group.users += self.user_demo
has_group_test = self.user_demo.has_group(self.TEST_GROUP)
self.assertTrue(has_group_test, "`demo` user should now belong to the restricted group")
self.assertTrue(partner.read(['bank_ids']))
self.assertTrue(partner.write({'bank_ids': []}))
@ -125,9 +128,8 @@ class TestACL(TransactionCaseWithUserDemo):
"""Test access to records having restricted fields"""
# Invalidate cache to avoid restricted value to be available
# in the cache
self.env.invalidate_all()
partner = self.env['res.partner'].with_user(self.user_demo)
self._set_field_groups(partner, 'email', GROUP_SYSTEM)
self._set_field_groups(partner, 'email', self.TEST_GROUP)
# accessing fields must no raise exceptions...
partner = partner.search([], limit=1)
@ -146,11 +148,11 @@ class TestACL(TransactionCaseWithUserDemo):
company_view = company.get_view(False, 'form')
view_arch = etree.fromstring(company_view['arch'])
# demo not part of the group_system, create edit and delete must be False
# demo not part of the group_test, create edit and delete must be False
for method in methods:
self.assertEqual(view_arch.get(method), 'False')
# demo part of the group_system, create edit and delete must not be specified
# demo part of the group_test, create edit and delete must not be specified
company = self.env['res.company'].with_user(self.env.ref("base.user_admin"))
company_view = company.get_view(False, 'form')
view_arch = etree.fromstring(company_view['arch'])
@ -178,14 +180,15 @@ class TestACL(TransactionCaseWithUserDemo):
self.assertEqual(field_node[0].get('can_' + method), 'True')
def test_get_views_fields(self):
""" Tests fields restricted to group_system are not passed when calling `get_views` as demo
""" Tests fields restricted to group_test are not passed when calling `get_views` as demo
but the same fields are well passed when calling `get_views` as admin"""
Partner = self.env['res.partner']
self._set_field_groups(Partner, 'email', GROUP_SYSTEM)
self._set_field_groups(Partner, 'email', self.TEST_GROUP)
views = Partner.with_user(self.user_demo).get_views([(False, 'form')])
self.assertFalse('email' in views['models']['res.partner'])
views = Partner.with_user(self.env.ref("base.user_admin")).get_views([(False, 'form')])
self.assertTrue('email' in views['models']['res.partner'])
self.assertFalse('email' in views['models']['res.partner']["fields"])
self.user_demo.groups_id = [Command.link(self.test_group.id)]
views = Partner.with_user(self.user_demo).get_views([(False, 'form')])
self.assertTrue('email' in views['models']['res.partner']["fields"])
class TestIrRule(TransactionCaseWithUserDemo):

View file

@ -214,17 +214,6 @@ class TestAPI(SavepointCaseWithUserDemo):
with self.assertRaises(AccessError):
demo_partner.company_id.name
@mute_logger('odoo.models')
def test_55_environment_lang(self):
""" Check the record env.lang behavior """
partner = self.partner_demo
self.env['res.lang']._activate_lang('fr_FR')
self.assertEqual(partner.with_context(lang=None).env.lang, None, 'None lang context should have None env.lang')
self.assertEqual(partner.with_context(lang='en_US').env.lang, 'en_US', 'en_US active lang context should have en_US env.lang')
self.assertEqual(partner.with_context(lang='fr_FR').env.lang, 'fr_FR', 'fr_FR active lang context should have fr_FR env.lang')
self.assertEqual(partner.with_context(lang='nl_NL').env.lang, None, 'Inactive lang context lang should have None env.lang')
self.assertEqual(partner.with_context(lang='Dummy').env.lang, None, 'Ilegal lang context should have None env.lang')
def test_56_environment_uid_origin(self):
"""Check the expected behavior of `env.uid_origin`"""
user_demo = self.user_demo

View file

@ -2,7 +2,7 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo.tests.common import TransactionCase
from odoo.tools import check_barcode_encoding, get_barcode_check_digit
from odoo.tools.barcode import check_barcode_encoding, get_barcode_check_digit
class TestBarcode(TransactionCase):

View file

@ -180,22 +180,22 @@ class TestGroups(TransactionCase):
groups = all_groups.search([('full_name', 'like', '/')])
self.assertTrue(groups, "did not match search for '/'")
def test_res_group_recursion(self):
def test_res_group_has_cycle(self):
# four groups with no cycle, check them all together
a = self.env['res.groups'].create({'name': 'A'})
b = self.env['res.groups'].create({'name': 'B'})
c = self.env['res.groups'].create({'name': 'G', 'implied_ids': [Command.set((a + b).ids)]})
d = self.env['res.groups'].create({'name': 'D', 'implied_ids': [Command.set(c.ids)]})
self.assertTrue((a + b + c + d)._check_m2m_recursion('implied_ids'))
self.assertFalse((a + b + c + d)._has_cycle('implied_ids'))
# create a cycle and check
a.implied_ids = d
self.assertFalse(a._check_m2m_recursion('implied_ids'))
self.assertTrue(a._has_cycle('implied_ids'))
def test_res_group_copy(self):
a = self.env['res.groups'].with_context(lang='en_US').create({'name': 'A'})
b = a.copy()
self.assertFalse(a.name == b.name)
self.assertNotEqual(a.name, b.name)
def test_apply_groups(self):
a = self.env['res.groups'].create({'name': 'A'})

View file

@ -239,13 +239,7 @@ class TestClocParser(TransactionCase):
py_count = cl.parse_py(PY_TEST_NO_RETURN)
self.assertEqual(py_count, (2, 2))
py_count = cl.parse_py(PY_TEST)
if self._python_version >= (3, 8, 0):
# Multi line str lineno return the begining of the str
# in python 3.8, it result in a different count for
# multi str used in expressions
self.assertEqual(py_count, (7, 16))
else:
self.assertEqual(py_count, (8, 16))
self.assertEqual(py_count, (7, 16))
js_count = cl.parse_js(JS_TEST)
self.assertEqual(js_count, (10, 17))
css_count = cl.parse_css(CSS_TEST)

View file

@ -0,0 +1,534 @@
import unittest
import odoo
from odoo.tests import TransactionCase
from odoo.tools import file_path, file_open, file_open_temporary_directory
from odoo.tools.config import conf, configmanager, _get_default_datadir
IS_POSIX = 'workers' in odoo.tools.config.options
ROOT_PATH = odoo.tools.config.options['root_path'].removesuffix('/odoo')
class TestConfigManager(TransactionCase):
maxDiff = None
def setUp(self):
super().setUp()
# _parse_config() as the side-effect of changing those two
# values, make sure the original value is restored at the end.
self.patch(conf, 'addons_paths', odoo.conf.addons_paths)
self.patch(conf, 'server_wide_modules', odoo.conf.server_wide_modules)
def test_01_default_config(self):
config = configmanager(fname=file_path('base/tests/config/empty.conf'))
default_values = {
# options not exposed on the command line
'admin_passwd': 'admin',
'csv_internal_sep': ',',
'publisher_warranty_url': 'http://services.odoo.com/publisher-warranty/',
'reportgz': False,
'root_path': f'{ROOT_PATH}/odoo',
'websocket_rate_limit_burst': 10,
'websocket_rate_limit_delay': 0.2,
'websocket_keep_alive_timeout': 3600,
# common
'config': None,
'save': None,
'init': {},
'update': {},
'without_demo': False,
'demo': {},
'import_partial': '',
'pidfile': '',
'addons_path': f'{ROOT_PATH}/odoo/addons,{ROOT_PATH}/addons',
'upgrade_path': '',
'pre_upgrade_scripts': '',
'server_wide_modules': 'base,web',
'data_dir': _get_default_datadir(),
# HTTP
'http_interface': '',
'http_port': 8069,
'gevent_port': 8072,
'http_enable': True,
'proxy_mode': False,
'x_sendfile': False,
# web
'dbfilter': '',
# testing
'test_file': '',
'test_enable': False,
'test_tags': None,
'screencasts': '',
'screenshots': '/tmp/odoo_tests',
# logging
'logfile': '',
'syslog': False,
'log_handler': [':INFO'],
'log_db': False,
'log_db_level': 'warning',
'log_level': 'info',
# SMTP
'email_from': False,
'from_filter': False,
'smtp_server': 'localhost',
'smtp_port': 25,
'smtp_ssl': False,
'smtp_user': False,
'smtp_password': False,
'smtp_ssl_certificate_filename': False,
'smtp_ssl_private_key_filename': False,
# database
'db_name': False,
'db_user': False,
'db_password': False,
'pg_path': '',
'db_host': False,
'db_port': False,
'db_sslmode': 'prefer',
'db_maxconn': 64,
'db_maxconn_gevent': False,
'db_template': 'template0',
'db_replica_host': False,
'db_replica_port': False,
# i18n
'load_language': None,
'language': None,
'translate_out': '',
'translate_in': '',
'overwrite_existing_translations': False,
'translate_modules': ['all'],
# security
'list_db': True,
# advanced
'dev_mode': [],
'shell_interface': None,
'stop_after_init': False,
'osv_memory_count_limit': 0,
'transient_age_limit': 1.0,
'max_cron_threads': 2,
'limit_time_worker_cron': 0,
'unaccent': False,
'geoip_city_db': '/usr/share/GeoIP/GeoLite2-City.mmdb',
'geoip_country_db': '/usr/share/GeoIP/GeoLite2-Country.mmdb',
}
if IS_POSIX:
# multiprocessing
default_values.update(
{
'workers': 0,
'limit_memory_soft': 2048 * 1024 * 1024,
'limit_memory_soft_gevent': False,
'limit_memory_hard': 2560 * 1024 * 1024,
'limit_memory_hard_gevent': False,
'limit_time_cpu': 60,
'limit_time_real': 120,
'limit_time_real_cron': -1,
'limit_request': 2**16,
}
)
config._parse_config()
self.assertEqual(config.options, default_values, "Options don't match")
def test_02_config_file(self):
values = {
# options not exposed on the command line
'admin_passwd': 'Tigrou007',
'csv_internal_sep': '@',
'publisher_warranty_url': 'http://example.com', # blacklist for save, read from the config file
'reportgz': True,
'root_path': f'{ROOT_PATH}/odoo', # blacklist for save, ignored from the config file
'websocket_rate_limit_burst': '1',
'websocket_rate_limit_delay': '2',
'websocket_keep_alive_timeout': '600',
# common
'config': '/tmp/config', # blacklist for save, read from the config file
'save': True, # blacklist for save, read from the config file
'init': {}, # blacklist for save, ignored from the config file
'update': {}, # blacklist for save, ignored from the config file
'without_demo': True,
'demo': {}, # blacklist for save, ignored from the config file
'import_partial': '/tmp/import-partial',
'pidfile': '/tmp/pidfile',
'addons_path': '/tmp/odoo',
'upgrade_path': '/tmp/upgrade',
'pre_upgrade_scripts': '/tmp/pre-custom.py',
'server_wide_modules': 'base,mail',
'data_dir': '/tmp/data-dir',
# HTTP
'http_interface': '10.0.0.254',
'http_port': 6942,
'gevent_port': 8012,
'http_enable': False,
'proxy_mode': True,
'x_sendfile': True,
# web
'dbfilter': '.*',
# testing
'test_file': '/tmp/file-file',
'test_enable': True,
'test_tags': ':TestMantra.test_is_extra_mile_done',
'screencasts': '/tmp/screencasts',
'screenshots': '/tmp/screenshots',
# logging
'logfile': '/tmp/odoo.log',
'syslog': False,
'log_handler': [':DEBUG'],
'log_db': 'logdb',
'log_db_level': 'debug',
'log_level': 'debug',
# SMTP
'email_from': 'admin@example.com',
'from_filter': '.*',
'smtp_server': 'smtp.localhost',
'smtp_port': 1299,
'smtp_ssl': True,
'smtp_user': 'spongebob',
'smtp_password': 'Tigrou0072',
'smtp_ssl_certificate_filename': '/tmp/tlscert',
'smtp_ssl_private_key_filename': '/tmp/tlskey',
# database
'db_name': 'horizon',
'db_user': 'kiwi',
'db_password': 'Tigrou0073',
'pg_path': '/tmp/pg_path',
'db_host': 'db.localhost',
'db_port': 4269,
'db_sslmode': 'verify-full',
'db_maxconn': 42,
'db_maxconn_gevent': 100,
'db_template': 'backup1706',
'db_replica_host': 'db2.localhost',
'db_replica_port': 2038,
# i18n
'load_language': 'fr_FR', # blacklist for save, read from the config file
'language': 'fr_FR', # blacklist for save, read from the config file
'translate_out': '/tmp/translate_out.csv', # blacklist for save, read from the config file
'translate_in': '/tmp/translate_in.csv', # blacklist for save, read from the config file
'overwrite_existing_translations': True, # blacklist for save, read from the config file
'translate_modules': ['all'], # ignored from the config file
# security
'list_db': False,
# advanced
'dev_mode': [], # blacklist for save, ignored from the config file
'shell_interface': 'ipython', # blacklist for save, read from the config file
'stop_after_init': True, # blacklist for save, read from the config file
'osv_memory_count_limit': 71,
'transient_age_limit': 4.0,
'max_cron_threads': 4,
'limit_time_worker_cron': 600,
'unaccent': True,
'geoip_city_db': '/tmp/city.db',
'geoip_country_db': '/tmp/country.db',
}
if IS_POSIX:
# multiprocessing
values.update(
{
'workers': 92,
'limit_memory_soft': 1048576,
'limit_memory_soft_gevent': 1048577,
'limit_memory_hard': 1048578,
'limit_memory_hard_gevent': 1048579,
'limit_time_cpu': 60,
'limit_time_real': 61,
'limit_time_real_cron': 62,
'limit_request': 100,
}
)
config_path = file_path('base/tests/config/non_default.conf')
config = configmanager(fname=config_path)
self.assertEqual(config.rcfile, config_path, "Config file path doesn't match")
config._parse_config()
self.assertEqual(config.options, values, "Options don't match")
self.assertEqual(config.rcfile, config_path)
self.assertNotEqual(config.rcfile, config['config']) # funny
@unittest.skipIf(not IS_POSIX, 'this test is POSIX only')
def test_03_save_default_options(self):
with file_open_temporary_directory(self.env) as temp_dir:
config_path = f'{temp_dir}/save.conf'
config = configmanager(fname=config_path)
config._parse_config(['--config', config_path, '--save'])
with (file_open(config_path, env=self.env) as config_file,
file_open('base/tests/config/save_posix.conf', env=self.env) as save_file):
config_content = config_file.read().rstrip()
save_content = save_file.read().format(
root_path=ROOT_PATH,
homedir=config._normalize('~'),
empty_dict=r'{}',
)
self.assertEqual(config_content.splitlines(), save_content.splitlines())
def test_04_odoo16_config_file(self):
# test that loading the Odoo 16.0 generated default config works
# with a modern version
config = configmanager(fname=file_path('base/tests/config/16.0.conf'))
assert_options = {
# options taken from the configuration file
'admin_passwd': 'admin',
'csv_internal_sep': ',',
'db_host': False,
'db_maxconn': 64,
'db_name': False,
'db_password': False,
'db_port': False,
'db_sslmode': 'prefer',
'db_template': 'template0',
'db_user': False,
'dbfilter': '',
'demo': {},
'email_from': False,
'geoip_city_db': '/usr/share/GeoIP/GeoLite2-City.mmdb',
'http_enable': True,
'http_interface': '',
'http_port': 8069,
'import_partial': '',
'list_db': True,
'load_language': None,
'log_db': False,
'log_db_level': 'warning',
'log_handler': [':INFO'],
'log_level': 'info',
'logfile': '',
'max_cron_threads': 2,
'osv_memory_count_limit': 0,
'overwrite_existing_translations': False,
'pg_path': '',
'pidfile': '',
'proxy_mode': False,
'reportgz': False,
'screencasts': '',
'screenshots': '/tmp/odoo_tests',
'server_wide_modules': 'base,web',
'smtp_password': False,
'smtp_port': 25,
'smtp_server': 'localhost',
'smtp_ssl': False,
'smtp_user': False,
'syslog': False,
'test_enable': False,
'test_file': '',
'test_tags': None,
'transient_age_limit': 1.0,
'translate_modules': ['all'],
'unaccent': False,
'update': {},
'upgrade_path': '',
'pre_upgrade_scripts': '',
'without_demo': False,
# options that are not taken from the file (also in 14.0)
'addons_path': f'{ROOT_PATH}/odoo/addons,{ROOT_PATH}/addons',
'config': None,
'data_dir': _get_default_datadir(),
'dev_mode': [],
'init': {},
'language': None,
'publisher_warranty_url': 'http://services.odoo.com/publisher-warranty/',
'save': None,
'shell_interface': None,
'stop_after_init': False,
'root_path': f'{ROOT_PATH}/odoo',
'translate_in': '',
'translate_out': '',
# new options since 14.0
'db_maxconn_gevent': False,
'db_replica_host': False,
'db_replica_port': False,
'geoip_country_db': '/usr/share/GeoIP/GeoLite2-Country.mmdb',
'from_filter': False,
'gevent_port': 8072,
'smtp_ssl_certificate_filename': False,
'smtp_ssl_private_key_filename': False,
'websocket_keep_alive_timeout': '3600',
'websocket_rate_limit_burst': '10',
'websocket_rate_limit_delay': '0.2',
'x_sendfile': False,
'limit_time_worker_cron': 0,
}
if IS_POSIX:
# multiprocessing
assert_options.update(
{
'workers': 0,
'limit_memory_soft': 2048 * 1024 * 1024,
'limit_memory_soft_gevent': False,
'limit_memory_hard': 2560 * 1024 * 1024,
'limit_memory_hard_gevent': False,
'limit_time_cpu': 60,
'limit_time_real': 120,
'limit_time_real_cron': -1,
'limit_request': 1 << 16,
}
)
config._parse_config()
with self.assertNoLogs('py.warnings'):
config._warn_deprecated_options()
self.assertEqual(config.options, assert_options, "Options don't match")
def test_05_repeat_parse_config(self):
"""Emulate multiple calls to parse_config()"""
config = configmanager()
config._parse_config()
config._warn_deprecated_options()
config._parse_config()
config._warn_deprecated_options()
def test_06_cli(self):
config = configmanager(fname=file_path('base/tests/config/empty.conf'))
with file_open('base/tests/config/cli') as file:
config._parse_config(file.read().split())
values = {
# options not exposed on the command line
'admin_passwd': 'admin',
'csv_internal_sep': ',',
'publisher_warranty_url': 'http://services.odoo.com/publisher-warranty/',
'reportgz': False,
'root_path': f'{ROOT_PATH}/odoo',
'websocket_rate_limit_burst': 10,
'websocket_rate_limit_delay': .2,
'websocket_keep_alive_timeout': 3600,
# common
'config': None,
'save': None,
'init': {'hr': 1, 'stock': 1},
'update': {'account': 1, 'website': 1},
'without_demo': 'rigolo',
'demo': {},
'import_partial': '/tmp/import-partial',
'pidfile': '/tmp/pidfile',
'addons_path': f'{ROOT_PATH}/odoo/addons,{ROOT_PATH}/addons',
'upgrade_path': '',
'pre_upgrade_scripts': '',
'server_wide_modules': 'base,mail',
'data_dir': '/tmp/data-dir',
# HTTP
'http_interface': '10.0.0.254',
'http_port': 6942,
'gevent_port': 8012,
'http_enable': False,
'proxy_mode': True,
'x_sendfile': True,
# web
'dbfilter': '.*',
# testing
'test_file': '/tmp/file-file',
'test_enable': True,
'test_tags': ':TestMantra.test_is_extra_mile_done',
'screencasts': '/tmp/screencasts',
'screenshots': '/tmp/screenshots',
# logging
'logfile': '/tmp/odoo.log',
'syslog': False,
'log_handler': [
':INFO',
'odoo.tools.config:DEBUG',
':WARNING',
'odoo.http:DEBUG',
'odoo.sql_db:DEBUG',
],
'log_db': 'logdb',
'log_db_level': 'debug',
'log_level': 'debug',
# SMTP
'email_from': 'admin@example.com',
'from_filter': '.*',
'smtp_server': 'smtp.localhost',
'smtp_port': 1299,
'smtp_ssl': True,
'smtp_user': 'spongebob',
'smtp_password': 'Tigrou0072',
'smtp_ssl_certificate_filename': '/tmp/tlscert',
'smtp_ssl_private_key_filename': '/tmp/tlskey',
# database
'db_name': 'horizon',
'db_user': 'kiwi',
'db_password': 'Tigrou0073',
'pg_path': '/tmp/pg_path',
'db_host': 'db.localhost',
'db_port': 4269,
'db_sslmode': 'verify-full',
'db_maxconn': 42,
'db_maxconn_gevent': 100,
'db_template': 'backup1706',
'db_replica_host': 'db2.localhost',
'db_replica_port': 2038,
# i18n
'load_language': 'fr_FR',
'language': 'fr_FR',
'translate_out': '/tmp/translate_out.csv',
'translate_in': '/tmp/translate_in.csv',
'overwrite_existing_translations': True,
'translate_modules': ['hr', 'mail', 'stock'],
# security
'list_db': False,
# advanced
'dev_mode': ['xml', 'reload'],
'shell_interface': 'ipython',
'stop_after_init': True,
'osv_memory_count_limit': 71,
'transient_age_limit': 4.0,
'max_cron_threads': 4,
'limit_time_worker_cron': 0,
'unaccent': True,
'geoip_city_db': '/tmp/city.db',
'geoip_country_db': '/tmp/country.db',
}
if IS_POSIX:
# multiprocessing
values.update(
{
'workers': 92,
'limit_memory_soft': 1048576,
'limit_memory_soft_gevent': 1048577,
'limit_memory_hard': 1048578,
'limit_memory_hard_gevent': 1048579,
'limit_time_cpu': 60,
'limit_time_real': 61,
'limit_time_real_cron': 62,
'limit_request': 100,
}
)
self.assertEqual(config.options, values)

View file

@ -1,21 +1,25 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
import json
import logging
from functools import partial
from unittest.mock import patch
import psycopg2
from psycopg2.extensions import ISOLATION_LEVEL_REPEATABLE_READ
import odoo
from odoo.modules.registry import Registry
from odoo.sql_db import db_connect, TestCursor
from odoo.tests import common
from odoo.tests.common import BaseCase
from odoo.tests.common import BaseCase, HttpCase
from odoo.tools.misc import config
ADMIN_USER_ID = common.ADMIN_USER_ID
def registry():
return odoo.registry(common.get_db_name())
return Registry(common.get_db_name())
class TestRealCursor(BaseCase):
@ -47,6 +51,78 @@ class TestRealCursor(BaseCase):
with registry().cursor() as cr:
self.assertEqual(cr.connection.isolation_level, ISOLATION_LEVEL_REPEATABLE_READ)
def test_connection_readonly(self):
# even without db_replica, we expect the connection to be readonly for consistency
registry_ = registry()
with registry_.cursor(readonly=False) as cr:
cr.execute('SHOW transaction_read_only')
self.assertEqual(cr.fetchone(), ('off',))
self.assertFalse(cr._cnx.readonly)
with registry_.cursor(readonly=True) as cr:
cr.execute('SHOW transaction_read_only')
self.assertEqual(cr.fetchone(), ('on',))
self.assertTrue(cr._cnx.readonly)
class TestHTTPCursor(HttpCase):
def test_cursor_keeps_readwriteness(self):
with self.env.registry.cursor(readonly=False) as cr:
self.assertFalse(cr.readonly)
cr.execute("SELECT 1")
cr.rollback()
self.assertFalse(cr.readonly)
cr.execute("SELECT 1")
cr.commit()
self.assertFalse(cr.readonly)
with self.env.registry.cursor(readonly=True) as cr:
self.assertTrue(cr.readonly)
cr.execute("SELECT 1")
cr.rollback()
self.assertTrue(cr.readonly)
cr.execute("SELECT 1")
cr.commit()
self.assertTrue(cr.readonly)
def test_call_kw_readonly(self):
self.authenticate('admin', 'admin')
self.env.user.partner_id.id
# a generic patcher to check if the method was called with a readonly cursor or not.
def return_readonly(self, *args, **kwargs):
return ['ok', self.env.cr.readonly]
with patch.object(type(self.env['res.partner']), 'read', return_readonly):
result_read = self.url_open('/web/dataset/call_kw', data=json.dumps({
"params": {
'model': 'res.partner',
'method': 'read',
'args': [self.env.user.partner_id.id, ['name']],
'kwargs': {},
},
}), headers={"Content-Type": "application/json"})
self.assertEqual(result_read.status_code, 200)
ok, readonly = result_read.json()['result']
self.assertEqual(ok, 'ok')
self.assertEqual(readonly, True, 'Call to read are expecte to be read only')
with patch.object(type(self.env['res.partner']), 'write', return_readonly):
result_write = self.url_open('/web/dataset/call_kw', data=json.dumps({
"params": {
'model': 'res.partner',
'method': 'write',
'args': [self.env.user.partner_id.id, {'name': 'Urgo'}],
'kwargs': {},
},
}), headers={"Content-Type": "application/json"})
self.assertEqual(result_write.status_code, 200)
ok, readonly = result_write.json()['result']
self.assertEqual(ok, 'ok')
self.assertEqual(readonly, False, 'Call to write are expecte to be read write')
class TestTestCursor(common.TransactionCase):
def setUp(self):
super().setUp()
@ -258,24 +334,55 @@ class TestCursorHooks(common.TransactionCase):
class TestCursorHooksTransactionCaseCleanup(common.TransactionCase):
"""Check savepoint cases handle commit hooks properly."""
def test_isolation_first(self):
def mutate_second_test_ref():
for name in ['precommit', 'postcommit', 'prerollback', 'postrollback']:
del self.env.cr.precommit.data.get(f'test_cursor_hooks_savepoint_case_cleanup_test_second_{name}', [''])[0]
self.env.cr.precommit.add(mutate_second_test_ref)
@staticmethod
def initial_callback():
pass
def test_isolation_second(self):
references = [['not_empty']] * 4
cr = self.env.cr
commit_callbacks = [cr.precommit, cr.postcommit, cr.prerollback, cr.postrollback]
callback_names = ['precommit', 'postcommit', 'prerollback', 'postrollback']
@staticmethod
def other_callback():
pass
for callback_name, callbacks, reference in zip(callback_names, commit_callbacks, references):
callbacks.data.setdefault(f"test_cursor_hooks_savepoint_case_cleanup_test_second_{callback_name}", reference)
@classmethod
def setUpClass(cls):
super().setUpClass()
for callback in commit_callbacks:
cr = cls.env.cr
cls.callback_names = ['precommit', 'postcommit', 'prerollback', 'postrollback']
cls.callbacks = [cr.precommit, cr.postcommit, cr.prerollback, cr.postrollback]
for callback, name in zip(cls.callbacks, cls.callback_names):
callback.data[f'test_cursor_hooks_{name}'] = ['keep']
callback.add(cls.initial_callback)
def assertHookData(self):
for callback, name in zip(self.callbacks, self.callback_names):
self.assertEqual(
callback.data[f'test_cursor_hooks_{name}'],
['keep'],
f"{name} failed to clean up between transaction tests"
)
self.assertIn(self.initial_callback, callback._funcs)
self.assertNotIn(self.other_callback, callback._funcs)
def test_1_isolation(self):
self.assertHookData()
for callback, name in zip(self.callbacks, self.callback_names):
callback.data[f'test_cursor_hooks_{name}'].append("don't keep")
callback.add(self.other_callback)
def test_2_isolation(self):
self.assertHookData()
for callback in self.callbacks:
callback.run()
for callback_name, reference in zip(callback_names, references):
self.assertTrue(bool(reference), f"{callback_name} failed to clean up between transaction tests")
self.assertTrue(reference[0] == 'not_empty', f"{callback_name} failed to clean up between transaction tests")
def test_3_isolation(self):
self.assertHookData()
for callback in self.callbacks:
callback.clear()
def test_4_isolation(self):
self.assertHookData()
self.env.cr.clear()
def test_5_isolation(self):
self.assertHookData()

View file

@ -31,8 +31,7 @@ class TestModelDeprecations(TransactionCase):
def test_name_get(self):
for model_name, Model in self.registry.items():
with self.subTest(model=model_name):
# name_get should exist but define by BaseModel
module = inspect.getmodule(Model.name_get)
if module.__name__ == 'odoo.models':
if not hasattr(Model, 'name_get'):
continue
module = inspect.getmodule(Model.name_get)
self.fail(f"Deprecated name_get method found on {model_name} in {module.__name__}, you should override `_compute_display_name` instead")

View file

@ -9,15 +9,50 @@ from unittest.mock import patch
import psycopg2
from odoo.addons.base.tests.common import SavepointCaseWithUserDemo
from odoo.fields import Date
from odoo.models import BaseModel
from odoo.tests.common import BaseCase, TransactionCase
from odoo.tools import mute_logger
from odoo.osv import expression
from odoo import Command
class TestExpression(SavepointCaseWithUserDemo):
class TransactionExpressionCase(TransactionCase):
def _search(self, model, domain, init_domain=None, test_complement=True):
sql = model.search(domain, order="id")
init_domain = init_domain or []
init_search = model.search(init_domain, order="id")
fil = init_search.filtered_domain(domain)
self.assertEqual(sql._ids, fil._ids, f"filtered_domain do not match SQL search for domain: {domain}")
if test_complement and domain:
# testing complement when asked, skip trivial the case where domain is TRUE
def inverse(domain):
"""Return the complement of the given domain"""
return expression.distribute_not(['!', *expression.normalize_domain(domain)])
# test whether the result of the search and the complement are equal to the universe
complement_domain = inverse(domain)
if init_domain:
# the init_search is not TRUE
# first, check the complement with a single search; include inactive records for the complement
cpl = model.with_context(active_test=False).search(complement_domain, order="id")
uni = model.with_context(active_test=False).search([], order="id")
self.assertEqual(sorted(sql._ids + cpl._ids), uni.ids, f"{domain} and {complement_domain} don't cover all records (search all)")
# second, for the rest of the check, limit the serach with init_domain
complement_domain = ['&', *expression.normalize_domain(init_domain), *complement_domain]
# general case where the universe is init_search
cpl = self._search(
model,
complement_domain,
init_domain=init_domain,
test_complement=False,
)
uni = init_search
self.assertEqual(sorted(sql._ids + cpl._ids), uni.ids, f"{domain} and {complement_domain} don't cover all records")
return sql
class TestExpression(SavepointCaseWithUserDemo, TransactionExpressionCase):
@classmethod
def setUpClass(cls):
@ -25,12 +60,6 @@ class TestExpression(SavepointCaseWithUserDemo):
cls._load_partners_set()
cls.env['res.currency'].with_context({'active_test': False}).search([('name', 'in', ['EUR', 'USD'])]).write({'active': True})
def _search(self, model, domain, init_domain=None):
sql = model.search(domain, order="id")
fil = model.search(init_domain or [], order="id").filtered_domain(domain)
self.assertEqual(sql._ids, fil._ids, f"filtered_domain do not match SQL search for domain: {domain}")
return sql
def test_00_in_not_in_m2m(self):
# Create 4 partners with no category, or one or two categories (out of two categories).
categories = self.env['res.partner.category']
@ -350,8 +379,9 @@ class TestExpression(SavepointCaseWithUserDemo):
# check that multi-level expressions with negative op work
all_partners = self._search(Partner, [('company_id', '!=', False)])
# FP Note: filtered_domain differs
res_partners = Partner.search([('company_id.partner_id', 'not in', [])])
# check with empty list
# TODO complement does not work
res_partners = self._search(Partner, [('company_id.partner_id', 'not in', [])], test_complement=False)
self.assertEqual(all_partners, res_partners, "not in [] fails")
# Test the '(not) like/in' behavior. res.partner and its parent_id
@ -501,6 +531,53 @@ class TestExpression(SavepointCaseWithUserDemo):
partners = self._search(Partner, [('child_ids.city', '=', 'foo')])
self.assertFalse(partners)
def test_15_o2m_subselect(self):
Partner = self.env['res.partner']
state_us_1 = self.env.ref('base.state_us_1')
state_us_2 = self.env.ref('base.state_us_2')
state_us_3 = self.env.ref('base.state_us_3')
partners = Partner.create(
[
{
"name": "Partner A",
"child_ids": [
(0, 0, {"name": "Child A1", "state_id": state_us_1.id}),
(0, 0, {"name": "Child A2", "state_id": state_us_2.id}),
(0, 0, {"name": "Child A2", "state_id": state_us_3.id}),
]
},
{
"name": "Partner B",
"child_ids": [
(0, 0, {"name": "Child B1", "state_id": state_us_1.id}),
]
},
{
"name": "Partner C",
"child_ids": [
(0, 0, {"name": "Child C2", "state_id": state_us_2.id}),
(0, 0, {"name": "Child C3", "state_id": state_us_3.id}),
]
},
{
"name": "Partner D",
"state_id": state_us_1.id,
}
]
)
partner_a, partner_b, partner_c, __ = partners
init_domain = [("id", "in", partners.ids)]
# find partners with children in state_us_1
domain = init_domain + [("child_ids.state_id", "=", state_us_1.id)]
result = self._search(Partner, domain, init_domain)
self.assertEqual(result, partner_a + partner_b)
# find partners with children in other states than state_us_1
domain = init_domain + [("child_ids.state_id", "!=", state_us_1.id)]
result = self._search(Partner, domain, init_domain)
self.assertEqual(result, partner_a + partner_c)
def test_15_equivalent_one2many_1(self):
Company = self.env['res.company']
company3 = Company.create({'name': 'Acme 3'})
@ -720,24 +797,31 @@ class TestExpression(SavepointCaseWithUserDemo):
self.assertEqual(expression.distribute_not(source), expect,
"distribute_not on long expression applied wrongly")
def test_40_negating_traversal(self):
domain = ['!', ('a.b', '=', 4)]
self.assertEqual(expression.distribute_not(domain), domain,
"distribute_not must not distribute the operator on domain traversal")
def test_accent(self):
if not self.registry.has_unaccent:
raise unittest.SkipTest("unaccent not enabled")
Model = self.env['res.partner.category']
helen = Model.create({'name': 'Hélène'})
self.assertEqual(helen, Model.search([('name', 'ilike', 'Helene')]))
self.assertEqual(helen, Model.search([('name', 'ilike', 'hélène')]))
self.assertEqual(helen, Model.search([('name', '=ilike', 'Hel%')]))
self.assertEqual(helen, Model.search([('name', '=ilike', 'hél%')]))
self.assertNotIn(helen, Model.search([('name', 'not ilike', 'Helene')]))
self.assertNotIn(helen, Model.search([('name', 'not ilike', 'hélène')]))
self.assertEqual(helen, self._search(Model, [('name', 'ilike', 'Helene')]))
self.assertEqual(helen, self._search(Model, [('name', 'ilike', 'hélène')]))
self.assertEqual(helen, self._search(Model, [('name', '=ilike', 'Hel%')]))
self.assertEqual(helen, self._search(Model, [('name', '=ilike', 'hél%')]))
self.assertNotIn(helen, self._search(Model, [('name', 'not ilike', 'Helene')]))
self.assertNotIn(helen, self._search(Model, [('name', 'not ilike', 'hélène')]))
# =like and like should be case and accent sensitive
self.assertEqual(helen, Model.search([('name', '=like', 'Hél%')]))
self.assertNotIn(helen, Model.search([('name', '=like', 'Hel%')]))
self.assertEqual(helen, Model.search([('name', 'like', 'élè')]))
self.assertNotIn(helen, Model.search([('name', 'like', 'ele')]))
self.assertEqual(helen, self._search(Model, [('name', '=like', 'Hél%')]))
self.assertNotIn(helen, self._search(Model, [('name', '=like', 'Hel%')]))
self.assertEqual(helen, self._search(Model, [('name', 'like', 'élè')]))
self.assertNotIn(helen, self._search(Model, [('name', 'like', 'ele')]))
self.assertNotIn(helen, self._search(Model, [('name', 'not ilike', 'ele')]))
self.assertNotIn(helen, self._search(Model, [('name', 'not ilike', 'élè')]))
hermione, nicostratus = Model.create([
{'name': 'Hermione', 'parent_id': helen.id},
@ -745,11 +829,11 @@ class TestExpression(SavepointCaseWithUserDemo):
])
self.assertEqual(nicostratus.parent_path, f'{helen.id}/{nicostratus.id}/')
with patch('odoo.osv.expression.get_unaccent_wrapper') as w:
with patch.object(self.env.registry, 'unaccent') as w:
w().side_effect = lambda x: x
rs = Model.search([('parent_path', 'like', f'{helen.id}/%')], order='id asc')
rs = Model.search([('parent_path', '=like', f'{helen.id}/%')], order='id asc')
self.assertEqual(rs, helen | hermione | nicostratus)
# the result of `get_unaccent_wrapper()` is the wrapper and that's
# the result of `unaccent()` is the wrapper and that's
# what should not be called
w().assert_not_called()
@ -799,30 +883,44 @@ class TestExpression(SavepointCaseWithUserDemo):
countries = self._search(Country, [('name', '=ilike', 'z%')])
self.assertTrue(len(countries) == 2, "Must match only countries with names starting with Z (currently 2)")
def test_like_filtered(self):
Model = self.env['res.partner.category']
record = Model.create({'name': '[default] _*%'})
record_pct = Model.create({'name': '5%'})
self.assertIn(record, self._search(Model, [('name', 'like', r'[default]')]))
self.assertIn(record, self._search(Model, [('name', 'like', r'\_*')]))
self.assertIn(record, self._search(Model, [('name', 'like', r'[_ef')]))
self.assertIn(record, self._search(Model, [('name', 'like', r'[%]')]))
self.assertIn(record, self._search(Model, [('name', 'ilike', r'DEF')]))
self.assertIn(record, self._search(Model, [('name', '=like', r'%\%')]))
self.assertIn(record_pct, self._search(Model, [('name', '=like', r'%\%')]))
def test_like_cast(self):
Model = self.env['res.partner.category']
record = Model.create({'name': 'XY', 'color': 42})
self.assertIn(record, Model.search([('name', 'like', 'X')]))
self.assertIn(record, Model.search([('name', 'ilike', 'X')]))
self.assertIn(record, Model.search([('name', 'not like', 'Z')]))
self.assertIn(record, Model.search([('name', 'not ilike', 'Z')]))
self.assertIn(record, self._search(Model, [('name', 'like', 'X')]))
self.assertIn(record, self._search(Model, [('name', 'ilike', 'X')]))
self.assertIn(record, self._search(Model, [('name', 'not like', 'Z')]))
self.assertIn(record, self._search(Model, [('name', 'not ilike', 'Z')]))
self.assertNotIn(record, Model.search([('name', 'like', 'Z')]))
self.assertNotIn(record, Model.search([('name', 'ilike', 'Z')]))
self.assertNotIn(record, Model.search([('name', 'not like', 'X')]))
self.assertNotIn(record, Model.search([('name', 'not ilike', 'X')]))
self.assertNotIn(record, self._search(Model, [('name', 'like', 'Z')]))
self.assertNotIn(record, self._search(Model, [('name', 'ilike', 'Z')]))
self.assertNotIn(record, self._search(Model, [('name', 'not like', 'X')]))
self.assertNotIn(record, self._search(Model, [('name', 'not ilike', 'X')]))
# like, ilike, not like, not ilike convert their lhs to str
self.assertIn(record, Model.search([('color', 'like', '4')]))
self.assertIn(record, Model.search([('color', 'ilike', '4')]))
self.assertIn(record, Model.search([('color', 'not like', '3')]))
self.assertIn(record, Model.search([('color', 'not ilike', '3')]))
self.assertIn(record, self._search(Model, [('color', 'like', '4')]))
self.assertIn(record, self._search(Model, [('color', 'ilike', '4')]))
self.assertIn(record, self._search(Model, [('color', 'not like', '3')]))
self.assertIn(record, self._search(Model, [('color', 'not ilike', '3')]))
self.assertNotIn(record, Model.search([('color', 'like', '3')]))
self.assertNotIn(record, Model.search([('color', 'ilike', '3')]))
self.assertNotIn(record, Model.search([('color', 'not like', '4')]))
self.assertNotIn(record, Model.search([('color', 'not ilike', '4')]))
self.assertNotIn(record, self._search(Model, [('color', 'like', '3')]))
self.assertNotIn(record, self._search(Model, [('color', 'ilike', '3')]))
self.assertNotIn(record, self._search(Model, [('color', 'not like', '4')]))
self.assertNotIn(record, self._search(Model, [('color', 'not ilike', '4')]))
# =like and =ilike don't work on non-character fields
with mute_logger('odoo.sql_db'), self.assertRaises(psycopg2.Error):
@ -830,6 +928,42 @@ class TestExpression(SavepointCaseWithUserDemo):
with self.assertRaises(ValueError):
Model.search([('name', '=', 'X'), ('color', '=like', '4%')])
def test_like_complement_m2o_access(self):
Model = self.env['res.partner']
parent1, parent2 = Model.create([{'name': 'Parent 1'}, {'name': 'Parent 2'}])
child1, child2 = Model.create([
{'name': 'Child 1', 'parent_id': parent1.id},
{'name': 'Child 2', 'parent_id': parent2.id},
])
other = Model.create({'name': 'other'})
partners = parent1 + parent2 + child1 + child2 + other
# replace all ir.rules by one global rule to prevent access to parent1
self.env['ir.rule'].search([]).unlink()
self.env['ir.rule'].create([{
'name': 'partners rule',
'model_id': self.env['ir.model']._get('res.partner').id,
'domain_force': str([('id', 'not in', parent1.ids)]),
}])
# search for children, bypassing access rights
found = self._search(
Model,
[('parent_id', 'like', 'Parent'), ('id', 'in', partners.ids)],
[('id', 'in', partners.ids)],
)
self.assertEqual(found, child1 + child2)
# search for children with opposite condition and access rights; we find
# all except parent1 (no access) and child2(parent matches 'Parent')
partners.invalidate_recordset() # avoid cache poisoning
found = self._search(
Model.with_user(self.user_demo),
[('parent_id', 'not like', 'Parent'), ('id', 'in', partners.ids)],
[('id', 'in', partners.ids)],
)
self.assertEqual(found, partners - (parent1 + child2))
def test_translate_search(self):
Country = self.env['res.country']
belgium = self.env.ref('base.be')
@ -933,6 +1067,9 @@ class TestExpression(SavepointCaseWithUserDemo):
false = expression.FALSE_DOMAIN
true = expression.TRUE_DOMAIN
normal = [('foo', '=', 'bar')]
# OR and AND with empty list should return their unit value
self.assertEqual(expression.OR([]), false)
self.assertEqual(expression.AND([]), true)
# OR with single FALSE_LEAF
expr = expression.OR([false])
self.assertEqual(expr, false)
@ -957,6 +1094,11 @@ class TestExpression(SavepointCaseWithUserDemo):
# AND with OR with single FALSE_LEAF and normal leaf
expr = expression.AND([expression.OR([false]), normal])
self.assertEqual(expr, false)
# empty domain inside the list should be treated as true
expr = expression.AND([[], normal])
self.assertEqual(expr, normal)
expr = expression.OR([[], normal])
self.assertEqual(expr, true)
def test_filtered_domain_order(self):
domain = [('name', 'ilike', 'a')]
@ -998,7 +1140,7 @@ class TestExpression(SavepointCaseWithUserDemo):
self.assertEqual(other_partners, all_partner - partner)
class TestExpression2(TransactionCase):
class TestExpression2(TransactionExpressionCase):
def test_long_table_alias(self):
# To test the 64 characters limit for table aliases in PostgreSQL
@ -1007,7 +1149,7 @@ class TestExpression2(TransactionCase):
self.env['res.users'].search([('name', '=', 'test')])
class TestAutoJoin(TransactionCase):
class TestAutoJoin(TransactionExpressionCase):
def test_auto_join(self):
# Get models
@ -1018,9 +1160,11 @@ class TestAutoJoin(TransactionCase):
# Get test columns
def patch_auto_join(model, fname, value):
self.patch(model._fields[fname], 'auto_join', value)
model.invalidate_model([fname])
def patch_domain(model, fname, value):
self.patch(model._fields[fname], 'domain', value)
model.invalidate_model([fname])
# Get country/state data
Country = self.env['res.country']
@ -1054,28 +1198,28 @@ class TestAutoJoin(TransactionCase):
name_test = '12'
# Do: one2many without _auto_join
partners = partner_obj.search([('bank_ids.sanitized_acc_number', 'like', name_test)])
partners = self._search(partner_obj, [('bank_ids.sanitized_acc_number', 'like', name_test)])
self.assertEqual(partners, p_aa,
"_auto_join off: ('bank_ids.sanitized_acc_number', 'like', '..'): incorrect result")
partners = partner_obj.search(['|', ('name', 'like', 'C'), ('bank_ids.sanitized_acc_number', 'like', name_test)])
partners = self._search(partner_obj, ['|', ('name', 'like', 'C'), ('bank_ids.sanitized_acc_number', 'like', name_test)])
self.assertIn(p_aa, partners,
"_auto_join off: '|', ('name', 'like', 'C'), ('bank_ids.sanitized_acc_number', 'like', '..'): incorrect result")
self.assertIn(p_c, partners,
"_auto_join off: '|', ('name', 'like', 'C'), ('bank_ids.sanitized_acc_number', 'like', '..'): incorrect result")
# Do: cascaded one2many without _auto_join
partners = partner_obj.search([('child_ids.bank_ids.id', 'in', [b_aa.id, b_ba.id])])
partners = self._search(partner_obj, [('child_ids.bank_ids.id', 'in', [b_aa.id, b_ba.id])])
self.assertEqual(partners, p_a + p_b,
"_auto_join off: ('child_ids.bank_ids.id', 'in', [..]): incorrect result")
# Do: one2many with _auto_join
patch_auto_join(partner_obj, 'bank_ids', True)
partners = partner_obj.search([('bank_ids.sanitized_acc_number', 'like', name_test)])
partners = self._search(partner_obj, [('bank_ids.sanitized_acc_number', 'like', name_test)])
self.assertEqual(partners, p_aa,
"_auto_join on: ('bank_ids.sanitized_acc_number', 'like', '..') incorrect result")
partners = partner_obj.search(['|', ('name', 'like', 'C'), ('bank_ids.sanitized_acc_number', 'like', name_test)])
partners = self._search(partner_obj, ['|', ('name', 'like', 'C'), ('bank_ids.sanitized_acc_number', 'like', name_test)])
self.assertIn(p_aa, partners,
"_auto_join on: '|', ('name', 'like', 'C'), ('bank_ids.sanitized_acc_number', 'like', '..'): incorrect result")
self.assertIn(p_c, partners,
@ -1083,14 +1227,14 @@ class TestAutoJoin(TransactionCase):
# Do: one2many with _auto_join, test final leaf is an id
bank_ids = [b_aa.id, b_ab.id]
partners = partner_obj.search([('bank_ids.id', 'in', bank_ids)])
partners = self._search(partner_obj, [('bank_ids.id', 'in', bank_ids)])
self.assertEqual(partners, p_aa + p_ab,
"_auto_join on: ('bank_ids.id', 'in', [..]) incorrect result")
# Do: 2 cascaded one2many with _auto_join, test final leaf is an id
patch_auto_join(partner_obj, 'child_ids', True)
bank_ids = [b_aa.id, b_ba.id]
partners = partner_obj.search([('child_ids.bank_ids.id', 'in', bank_ids)])
partners = self._search(partner_obj, [('child_ids.bank_ids.id', 'in', bank_ids)])
self.assertEqual(partners, p_a + p_b,
"_auto_join on: ('child_ids.bank_ids.id', 'not in', [..]): incorrect result")
@ -1100,35 +1244,35 @@ class TestAutoJoin(TransactionCase):
name_test = 'US'
# Do: many2one without _auto_join
partners = partner_obj.search([('state_id.country_id.code', 'like', name_test)])
partners = self._search(partner_obj, [('state_id.country_id.code', 'like', name_test)])
self.assertLessEqual(p_a + p_b + p_aa + p_ab + p_ba, partners,
"_auto_join off: ('state_id.country_id.code', 'like', '..') incorrect result")
partners = partner_obj.search(['|', ('state_id.code', '=', states[0].code), ('name', 'like', 'C')])
partners = self._search(partner_obj, ['|', ('state_id.code', '=', states[0].code), ('name', 'like', 'C')])
self.assertIn(p_a, partners, '_auto_join off: disjunction incorrect result')
self.assertIn(p_c, partners, '_auto_join off: disjunction incorrect result')
# Do: many2one with 1 _auto_join on the first many2one
patch_auto_join(partner_obj, 'state_id', True)
partners = partner_obj.search([('state_id.country_id.code', 'like', name_test)])
partners = self._search(partner_obj, [('state_id.country_id.code', 'like', name_test)])
self.assertLessEqual(p_a + p_b + p_aa + p_ab + p_ba, partners,
"_auto_join on for state_id: ('state_id.country_id.code', 'like', '..') incorrect result")
partners = partner_obj.search(['|', ('state_id.code', '=', states[0].code), ('name', 'like', 'C')])
partners = self._search(partner_obj, ['|', ('state_id.code', '=', states[0].code), ('name', 'like', 'C')])
self.assertIn(p_a, partners, '_auto_join: disjunction incorrect result')
self.assertIn(p_c, partners, '_auto_join: disjunction incorrect result')
# Do: many2one with 1 _auto_join on the second many2one
patch_auto_join(partner_obj, 'state_id', False)
patch_auto_join(state_obj, 'country_id', True)
partners = partner_obj.search([('state_id.country_id.code', 'like', name_test)])
partners = self._search(partner_obj, [('state_id.country_id.code', 'like', name_test)])
self.assertLessEqual(p_a + p_b + p_aa + p_ab + p_ba, partners,
"_auto_join on for country_id: ('state_id.country_id.code', 'like', '..') incorrect result")
# Do: many2one with 2 _auto_join
patch_auto_join(partner_obj, 'state_id', True)
patch_auto_join(state_obj, 'country_id', True)
partners = partner_obj.search([('state_id.country_id.code', 'like', name_test)])
partners = self._search(partner_obj, [('state_id.country_id.code', 'like', name_test)])
self.assertLessEqual(p_a + p_b + p_aa + p_ab + p_ba, partners,
"_auto_join on: ('state_id.country_id.code', 'like', '..') incorrect result")
@ -1142,14 +1286,14 @@ class TestAutoJoin(TransactionCase):
patch_domain(partner_obj, 'bank_ids', [('sanitized_acc_number', 'like', '2')])
# Do: 2 cascaded one2many with _auto_join, test final leaf is an id
partners = partner_obj.search(['&', (1, '=', 1), ('child_ids.bank_ids.id', 'in', [b_aa.id, b_ba.id])])
partners = self._search(partner_obj, ['&', (1, '=', 1), ('child_ids.bank_ids.id', 'in', [b_aa.id, b_ba.id])])
self.assertLessEqual(p_a, partners,
"_auto_join on one2many with domains incorrect result")
self.assertFalse((p_ab + p_ba) & partners,
"_auto_join on one2many with domains incorrect result")
patch_domain(partner_obj, 'child_ids', lambda self: [('name', '=', '__%s' % self._name)])
partners = partner_obj.search(['&', (1, '=', 1), ('child_ids.bank_ids.id', 'in', [b_aa.id, b_ba.id])])
partners = self._search(partner_obj, ['&', (1, '=', 1), ('child_ids.bank_ids.id', 'in', [b_aa.id, b_ba.id])])
self.assertFalse(partners,
"_auto_join on one2many with domains incorrect result")
@ -1166,7 +1310,7 @@ class TestAutoJoin(TransactionCase):
patch_domain(partner_obj, 'bank_ids', [])
# Do: ('child_ids.state_id.country_id.code', 'like', '..') without _auto_join
partners = partner_obj.search([('child_ids.state_id.country_id.code', 'like', name_test)])
partners = self._search(partner_obj, [('child_ids.state_id.country_id.code', 'like', name_test)])
self.assertLessEqual(p_a + p_b, partners,
"_auto_join off: ('child_ids.state_id.country_id.code', 'like', '..') incorrect result")
@ -1174,7 +1318,8 @@ class TestAutoJoin(TransactionCase):
patch_auto_join(partner_obj, 'child_ids', True)
patch_auto_join(partner_obj, 'state_id', True)
patch_auto_join(state_obj, 'country_id', True)
partners = partner_obj.search([('child_ids.state_id.country_id.code', 'like', name_test)])
# TODO complement does not work
partners = self._search(partner_obj, [('child_ids.state_id.country_id.code', 'like', name_test)], test_complement=False)
self.assertLessEqual(p_a + p_b, partners,
"_auto_join on: ('child_ids.state_id.country_id.code', 'like', '..') incorrect result")
@ -1219,7 +1364,7 @@ class TestQueries(TransactionCase):
WHERE (
(
("res_partner"."active" = %s) AND
("res_partner"."name"::text LIKE %s)
("res_partner"."name" LIKE %s)
) AND (
("res_partner"."title" = %s) OR (
("res_partner"."ref" != %s) OR
@ -1238,19 +1383,38 @@ class TestQueries(TransactionCase):
with self.assertQueries(['''
SELECT "res_partner"."id"
FROM "res_partner"
WHERE (("res_partner"."active" = %s) AND ("res_partner"."name"::text LIKE %s))
ORDER BY "res_partner"."complete_name"asc,"res_partner"."id"desc
WHERE (("res_partner"."active" = %s) AND ("res_partner"."name" LIKE %s))
ORDER BY "res_partner"."complete_name" ASC,"res_partner"."id" DESC
''']):
Model.search([('name', 'like', 'foo')])
with self.assertQueries(['''
SELECT "res_partner"."id"
FROM "res_partner"
WHERE (("res_partner"."active" = %s) AND ("res_partner"."name"::text LIKE %s))
WHERE (("res_partner"."active" = %s) AND ("res_partner"."name" LIKE %s))
ORDER BY "res_partner"."id"
''']):
Model.search([('name', 'like', 'foo')], order='id')
with self.assertQueries(['''
SELECT "res_partner"."id"
FROM "res_partner"
WHERE (("res_partner"."active" = %s) AND ("res_partner"."name" LIKE %s))
ORDER BY "res_partner"."company_id"
''']):
Model.search([('name', 'like', 'foo')], order='company_id.id')
with self.assertQueries(['''
SELECT "res_partner"."id"
FROM "res_partner"
WHERE (("res_partner"."active" = %s) AND ("res_partner"."name" LIKE %s))
ORDER BY "res_partner"."company_id" DESC
''']):
Model.search([('name', 'like', 'foo')], order='company_id.id DESC')
with self.assertRaises(ValueError):
Model.search([('name', 'like', 'foo')], order='company_id.name')
def test_count(self):
Model = self.env['res.partner']
Model.search([('name', 'like', 'foo')])
@ -1258,7 +1422,7 @@ class TestQueries(TransactionCase):
with self.assertQueries(['''
SELECT COUNT(*)
FROM "res_partner"
WHERE (("res_partner"."active" = %s) AND ("res_partner"."name"::text LIKE %s))
WHERE (("res_partner"."active" = %s) AND ("res_partner"."name" LIKE %s))
''']):
Model.search_count([('name', 'like', 'foo')])
@ -1269,7 +1433,7 @@ class TestQueries(TransactionCase):
with self.assertQueries(['''
SELECT COUNT(*) FROM (
SELECT FROM "res_partner"
WHERE (("res_partner"."active" = %s) AND ("res_partner"."name"::text LIKE %s))
WHERE (("res_partner"."active" = %s) AND ("res_partner"."name" LIKE %s))
LIMIT %s
) t
''']):
@ -1283,7 +1447,7 @@ class TestQueries(TransactionCase):
with self.assertQueries(['''
SELECT "res_partner_title"."id"
FROM "res_partner_title"
WHERE (COALESCE("res_partner_title"."name"->>%s, "res_partner_title"."name"->>%s) like %s)
WHERE (COALESCE("res_partner_title"."name"->>%s, "res_partner_title"."name"->>%s) LIKE %s)
ORDER BY COALESCE("res_partner_title"."name"->>%s, "res_partner_title"."name"->>%s)
''']):
Model.search([('name', 'like', 'foo')])
@ -1332,7 +1496,7 @@ class TestQueries(TransactionCase):
FROM "ir_model"
WHERE (
("ir_model"."name"->>%s ILIKE %s)
OR ("ir_model"."model"::text ILIKE %s)
OR ("ir_model"."model" ILIKE %s)
)
ORDER BY "ir_model"."model"
LIMIT %s
@ -1343,8 +1507,8 @@ class TestQueries(TransactionCase):
SELECT "ir_model"."id", "ir_model"."name"->>%s
FROM "ir_model"
WHERE (
("ir_model"."name" is NULL OR "ir_model"."name"->>%s not ilike %s)
AND (("ir_model"."model"::text NOT ILIKE %s) OR "ir_model"."model" IS NULL)
(("ir_model"."name"->>%s NOT ILIKE %s) OR "ir_model"."name"->>%s IS NULL)
AND (("ir_model"."model" NOT ILIKE %s) OR "ir_model"."model" IS NULL)
)
ORDER BY "ir_model"."model"
LIMIT %s
@ -1365,7 +1529,7 @@ class TestMany2one(TransactionCase):
FROM "res_users"
LEFT JOIN "res_partner" AS "res_users__partner_id" ON
("res_users"."partner_id" = "res_users__partner_id"."id")
WHERE ("res_users__partner_id"."name"::text LIKE %s)
WHERE ("res_users__partner_id"."name" LIKE %s)
ORDER BY "res_users__partner_id"."name", "res_users"."login"
''']):
self.User.search([('name', 'like', 'foo')])
@ -1377,7 +1541,7 @@ class TestMany2one(TransactionCase):
FROM "res_users"
LEFT JOIN "res_partner" AS "res_users__partner_id" ON
("res_users"."partner_id" = "res_users__partner_id"."id")
WHERE ("res_users__partner_id"."name"::text LIKE %s)
WHERE ("res_users__partner_id"."name" LIKE %s)
ORDER BY "res_users__partner_id"."name", "res_users"."login"
''']):
self.User.search([('partner_id.name', 'like', 'foo')])
@ -1400,7 +1564,7 @@ class TestMany2one(TransactionCase):
WHERE ("res_partner"."company_id" IN (
SELECT "res_company"."id"
FROM "res_company"
WHERE ("res_company"."name"::text LIKE %s)
WHERE ("res_company"."name" LIKE %s)
))
ORDER BY "res_partner"."complete_name"asc,"res_partner"."id"desc
''']):
@ -1415,7 +1579,7 @@ class TestMany2one(TransactionCase):
WHERE ("res_company"."partner_id" IN (
SELECT "res_partner"."id"
FROM "res_partner"
WHERE ("res_partner"."name"::text LIKE %s)
WHERE ("res_partner"."name" LIKE %s)
))
))
ORDER BY "res_partner"."complete_name"asc,"res_partner"."id"desc
@ -1428,11 +1592,11 @@ class TestMany2one(TransactionCase):
WHERE (("res_partner"."company_id" IN (
SELECT "res_company"."id"
FROM "res_company"
WHERE ("res_company"."name"::text LIKE %s)
WHERE ("res_company"."name" LIKE %s)
)) OR ("res_partner"."country_id" IN (
SELECT "res_country"."id"
FROM "res_country"
WHERE ("res_country"."code"::text LIKE %s)
WHERE ("res_country"."code" LIKE %s)
)))
ORDER BY "res_partner"."complete_name"asc,"res_partner"."id"desc
''']):
@ -1442,6 +1606,20 @@ class TestMany2one(TransactionCase):
('country_id.code', 'like', 'BE'),
])
def test_complement_regular(self):
self.Partner.search(['!', ('company_id.name', 'like', self.company.name)])
with self.assertQueries(['''
SELECT "res_partner"."id"
FROM "res_partner"
WHERE (("res_partner"."company_id" NOT IN (
SELECT "res_company"."id"
FROM "res_company"
WHERE ("res_company"."name" LIKE %s)
)) OR "res_partner"."company_id" IS NULL)
ORDER BY "res_partner"."complete_name"asc,"res_partner"."id"desc
''']):
self.Partner.search(['!', ('company_id.name', 'like', self.company.name)])
def test_explicit_subquery(self):
self.Partner.search([('company_id.name', 'like', self.company.name)])
@ -1451,7 +1629,7 @@ class TestMany2one(TransactionCase):
WHERE ("res_partner"."company_id" IN (
SELECT "res_company"."id"
FROM "res_company"
WHERE (("res_company"."active" = %s) AND ("res_company"."name"::text LIKE %s))
WHERE (("res_company"."active" = %s) AND ("res_company"."name" LIKE %s))
))
ORDER BY "res_partner"."complete_name"asc,"res_partner"."id"desc
''']):
@ -1465,7 +1643,7 @@ class TestMany2one(TransactionCase):
WHERE ("res_partner"."company_id" IN (
SELECT "res_company"."id"
FROM "res_company"
WHERE (("res_company"."active" = %s) AND ("res_company"."name"::text LIKE %s))
WHERE (("res_company"."active" = %s) AND ("res_company"."name" LIKE %s))
ORDER BY "res_company"."id"
LIMIT %s
))
@ -1478,7 +1656,7 @@ class TestMany2one(TransactionCase):
with self.assertQueries(['''
SELECT "res_company"."id"
FROM "res_company"
WHERE (("res_company"."active" = %s) AND ("res_company"."name"::text LIKE %s))
WHERE (("res_company"."active" = %s) AND ("res_company"."name" LIKE %s))
ORDER BY "res_company"."id"
''', '''
SELECT "res_partner"."id"
@ -1494,7 +1672,7 @@ class TestMany2one(TransactionCase):
with self.assertQueries(['''
SELECT "res_company"."id"
FROM "res_company"
WHERE (("res_company"."active" = %s) AND ("res_company"."name"::text LIKE %s))
WHERE (("res_company"."active" = %s) AND ("res_company"."name" LIKE %s))
ORDER BY "res_company"."id"
''', '''
SELECT "res_partner"."id"
@ -1513,7 +1691,7 @@ class TestMany2one(TransactionCase):
WHERE ("res_partner"."company_id" IN (
SELECT "res_company"."id"
FROM "res_company"
WHERE (("res_company"."active" = %s) AND ("res_company"."name"::text LIKE %s))
WHERE (("res_company"."active" = %s) AND ("res_company"."name" LIKE %s))
))
ORDER BY "res_partner"."complete_name"asc,"res_partner"."id"desc
''']):
@ -1531,7 +1709,7 @@ class TestMany2one(TransactionCase):
FROM "res_partner"
LEFT JOIN "res_company" AS "res_partner__company_id" ON
("res_partner"."company_id" = "res_partner__company_id"."id")
WHERE ("res_partner__company_id"."name"::text LIKE %s)
WHERE ("res_partner__company_id"."name" LIKE %s)
ORDER BY "res_partner"."complete_name"asc,"res_partner"."id"desc
''']):
self.Partner.search([('company_id.name', 'like', self.company.name)])
@ -1544,7 +1722,7 @@ class TestMany2one(TransactionCase):
WHERE ("res_partner__company_id"."partner_id" IN (
SELECT "res_partner"."id"
FROM "res_partner"
WHERE ("res_partner"."name"::text LIKE %s)
WHERE ("res_partner"."name" LIKE %s)
))
ORDER BY "res_partner"."complete_name"asc,"res_partner"."id"desc
''']):
@ -1563,7 +1741,7 @@ class TestMany2one(TransactionCase):
FROM "res_company"
LEFT JOIN "res_partner" AS "res_company__partner_id" ON
("res_company"."partner_id" = "res_company__partner_id"."id")
WHERE ("res_company__partner_id"."name"::text LIKE %s)
WHERE ("res_company__partner_id"."name" LIKE %s)
))
ORDER BY "res_partner"."complete_name"asc,"res_partner"."id"desc
''']):
@ -1581,7 +1759,7 @@ class TestMany2one(TransactionCase):
("res_partner"."company_id" = "res_partner__company_id"."id")
LEFT JOIN "res_partner" AS "res_partner__company_id__partner_id" ON
("res_partner__company_id"."partner_id" = "res_partner__company_id__partner_id"."id")
WHERE ("res_partner__company_id__partner_id"."name"::text LIKE %s)
WHERE ("res_partner__company_id__partner_id"."name" LIKE %s)
ORDER BY "res_partner"."complete_name"asc,"res_partner"."id"desc
''']):
self.Partner.search([('company_id.partner_id.name', 'like', self.company.name)])
@ -1602,8 +1780,8 @@ class TestMany2one(TransactionCase):
("res_partner"."country_id" = "res_partner__country_id"."id")
LEFT JOIN "res_company" AS "res_partner__company_id" ON
("res_partner"."company_id" = "res_partner__company_id"."id")
WHERE (("res_partner__company_id"."name"::text LIKE %s)
OR ("res_partner__country_id"."code"::text LIKE %s))
WHERE (("res_partner__company_id"."name" LIKE %s)
OR ("res_partner__country_id"."code" LIKE %s))
ORDER BY "res_partner"."complete_name"asc,"res_partner"."id"desc
''']):
self.Partner.search([
@ -1621,7 +1799,7 @@ class TestMany2one(TransactionCase):
WHERE ("res_partner"."company_id" IN (
SELECT "res_company"."id"
FROM "res_company"
WHERE ("res_company"."name"::text LIKE %s)
WHERE ("res_company"."name" LIKE %s)
))
ORDER BY "res_partner"."complete_name"asc,"res_partner"."id"desc
''']):
@ -1630,15 +1808,29 @@ class TestMany2one(TransactionCase):
with self.assertQueries(['''
SELECT "res_partner"."id"
FROM "res_partner"
WHERE (("res_partner"."company_id" IN (
WHERE (("res_partner"."company_id" NOT IN (
SELECT "res_company"."id"
FROM "res_company"
WHERE (("res_company"."name"::text not like %s) OR "res_company"."name" IS NULL))
) OR "res_partner"."company_id" IS NULL)
WHERE ("res_company"."name" LIKE %s)
)) OR "res_partner"."company_id" IS NULL)
ORDER BY "res_partner"."complete_name"asc,"res_partner"."id"desc
''']):
self.Partner.search([('company_id', 'not like', "blablabla")])
def test_name_search_undefined(self):
"""Check that if the _rec_name is not defined, we do not restrict anything.
This way the model continues to work in the web interface inside many2one fields.
"""
PartnerClass = self.env.registry['res.partner']
with (
patch.object(PartnerClass, '_rec_name', ''),
patch.object(PartnerClass, '_rec_names_search', []),
mute_logger('odoo.models'),
):
self.assertGreater(len(self.Partner.name_search()), 0)
self.assertGreater(len(self.Partner.name_search('test')), 0)
class TestOne2many(TransactionCase):
def setUp(self):
@ -1676,7 +1868,7 @@ class TestOne2many(TransactionCase):
WHERE ("res_partner"."id" IN (
SELECT "res_partner_bank"."partner_id"
FROM "res_partner_bank"
WHERE ("res_partner_bank"."sanitized_acc_number"::text LIKE %s)
WHERE ("res_partner_bank"."sanitized_acc_number" LIKE %s)
))
ORDER BY "res_partner"."complete_name"asc,"res_partner"."id"desc
''']):
@ -1688,11 +1880,14 @@ class TestOne2many(TransactionCase):
WHERE ("res_partner"."id" IN (
SELECT "res_partner"."parent_id"
FROM "res_partner"
WHERE ("res_partner"."id" IN (
SELECT "res_partner_bank"."partner_id"
FROM "res_partner_bank"
WHERE ("res_partner_bank"."sanitized_acc_number"::text LIKE %s)
)) AND "res_partner"."parent_id" IS NOT NULL
WHERE (
("res_partner"."active" = TRUE)
AND ("res_partner"."id" IN (
SELECT "res_partner_bank"."partner_id"
FROM "res_partner_bank"
WHERE ("res_partner_bank"."sanitized_acc_number" LIKE %s)
))
) AND "res_partner"."parent_id" IS NOT NULL
))
ORDER BY "res_partner"."complete_name"asc,"res_partner"."id"desc
''']):
@ -1723,7 +1918,7 @@ class TestOne2many(TransactionCase):
WHERE ("res_partner"."id" IN (
SELECT "res_partner_bank"."partner_id"
FROM "res_partner_bank"
WHERE ("res_partner_bank"."sanitized_acc_number"::text LIKE %s)
WHERE ("res_partner_bank"."sanitized_acc_number" LIKE %s)
))
ORDER BY "res_partner"."complete_name"asc,"res_partner"."id"desc
''']):
@ -1735,11 +1930,11 @@ class TestOne2many(TransactionCase):
WHERE (("res_partner"."id" IN (
SELECT "res_partner_bank"."partner_id"
FROM "res_partner_bank"
WHERE ("res_partner_bank"."sanitized_acc_number"::text LIKE %s)
WHERE ("res_partner_bank"."sanitized_acc_number" LIKE %s)
)) AND ("res_partner"."id" IN (
SELECT "res_partner_bank"."partner_id"
FROM "res_partner_bank"
WHERE ("res_partner_bank"."sanitized_acc_number"::text LIKE %s)
WHERE ("res_partner_bank"."sanitized_acc_number" LIKE %s)
)))
ORDER BY "res_partner"."complete_name"asc,"res_partner"."id"desc
''']):
@ -1757,7 +1952,7 @@ class TestOne2many(TransactionCase):
WHERE (("res_partner"."active" = TRUE) AND ("res_partner"."id" IN
(SELECT "res_partner_bank"."partner_id"
FROM "res_partner_bank"
WHERE ("res_partner_bank"."sanitized_acc_number"::text LIKE %s)))
WHERE ("res_partner_bank"."sanitized_acc_number" LIKE %s)))
)))
ORDER BY "res_partner"."complete_name"asc,"res_partner"."id"desc
''']):
@ -1784,7 +1979,7 @@ class TestOne2many(TransactionCase):
WHERE ((
"res_partner_bank"."id" IN %s
) AND (
"res_partner_bank"."sanitized_acc_number"::text LIKE %s
"res_partner_bank"."sanitized_acc_number" LIKE %s
))
)
))
@ -1812,7 +2007,7 @@ class TestOne2many(TransactionCase):
WHERE ((
"res_partner"."active" = TRUE
) AND (
"res_partner__state_id__country_id"."code"::text LIKE %s
"res_partner__state_id__country_id"."code" LIKE %s
))
))
ORDER BY "res_partner"."complete_name"asc,"res_partner"."id"desc
@ -1828,7 +2023,7 @@ class TestOne2many(TransactionCase):
WHERE ("res_partner"."id" IN (
SELECT "res_partner_bank"."partner_id"
FROM "res_partner_bank"
WHERE ("res_partner_bank"."sanitized_acc_number"::text LIKE %s)
WHERE ("res_partner_bank"."sanitized_acc_number" LIKE %s)
))
ORDER BY "res_partner"."complete_name"asc,"res_partner"."id"desc
''']):
@ -1889,7 +2084,6 @@ class TestMany2many(TransactionCase):
''']):
self.User.search([('groups_id', 'in', group.ids)], order='id')
group_color = group.color
with self.assertQueries(['''
SELECT "res_users"."id"
FROM "res_users"
@ -1916,7 +2110,7 @@ class TestMany2many(TransactionCase):
)
ORDER BY "res_users"."id"
''']):
self.User.search([('groups_id.color', '=', group_color)], order='id')
self.User.search([('groups_id.color', '=', 1)], order='id')
with self.assertQueries(['''
SELECT "res_users"."id"
@ -1933,7 +2127,7 @@ class TestMany2many(TransactionCase):
AND "res_groups__rule_groups"."rule_group_id" IN (
SELECT "ir_rule"."id"
FROM "ir_rule"
WHERE ("ir_rule"."name"::text LIKE %s)
WHERE ("ir_rule"."name" LIKE %s)
)
)
)
@ -1959,7 +2153,7 @@ class TestMany2many(TransactionCase):
AND "res_users__company_ids"."cid" IN (
SELECT "res_company"."id"
FROM "res_company"
WHERE ("res_company"."name"::text LIKE %s)
WHERE ("res_company"."name" LIKE %s)
)
)
ORDER BY "res_users"."id"

View file

@ -1,4 +1,3 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from math import log10
@ -14,21 +13,25 @@ class TestFloatPrecision(TransactionCase):
""" Test rounding methods with 2 digits. """
currency = self.env.ref('base.EUR')
def try_round(amount, expected):
digits = max(0, -int(log10(currency.rounding)))
result = float_repr(currency.round(amount), precision_digits=digits)
def try_round(amount, expected, digits=2, method='HALF-UP'):
value = float_round(amount, precision_digits=digits, rounding_method=method)
result = float_repr(value, precision_digits=digits)
self.assertEqual(result, expected, 'Rounding error: got %s, expected %s' % (result, expected))
try_round(2.674,'2.67')
try_round(2.675,'2.68') # in Python 2.7.2, round(2.675,2) gives 2.67
try_round(-2.675,'-2.68') # in Python 2.7.2, round(2.675,2) gives 2.67
try_round(0.001,'0.00')
try_round(-0.001,'-0.00')
try_round(-0.001, '0.00')
try_round(0.0049,'0.00') # 0.0049 is closer to 0 than to 0.01, so should round down
try_round(0.005,'0.01') # the rule is to round half away from zero
try_round(-0.005,'-0.01') # the rule is to round half away from zero
try_round(6.6 * 0.175, '1.16') # 6.6 * 0.175 is rounded to 1.15 with epsilon = 53
try_round(-6.6 * 0.175, '-1.16')
try_round(5.015, '5.02', method='HALF-EVEN')
try_round(5.025, '5.02', method='HALF-EVEN')
try_round(-5.015, '-5.02', method='HALF-EVEN')
try_round(-5.025, '-5.02', method='HALF-EVEN')
def try_zero(amount, expected):
self.assertEqual(currency.is_zero(amount), expected,
@ -78,7 +81,7 @@ class TestFloatPrecision(TransactionCase):
try_round(2.6744, '2.674')
try_round(-2.6744, '-2.674')
try_round(0.0004, '0.000')
try_round(-0.0004, '-0.000')
try_round(-0.0004, '0.000')
try_round(357.4555, '357.456')
try_round(-357.4555, '-357.456')
try_round(457.4554, '457.455')
@ -92,7 +95,7 @@ class TestFloatPrecision(TransactionCase):
try_round(2.6744, '2.674', method='HALF-DOWN')
try_round(-2.6744, '-2.674', method='HALF-DOWN')
try_round(0.0004, '0.000', method='HALF-DOWN')
try_round(-0.0004, '-0.000', method='HALF-DOWN')
try_round(-0.0004, '0.000', method='HALF-DOWN')
try_round(357.4555, '357.455', method='HALF-DOWN')
try_round(-357.4555, '-357.455', method='HALF-DOWN')
try_round(457.4554, '457.455', method='HALF-DOWN')
@ -106,9 +109,9 @@ class TestFloatPrecision(TransactionCase):
try_round(2.6744, '2.674', method='HALF-EVEN')
try_round(-2.6744, '-2.674', method='HALF-EVEN')
try_round(0.0004, '0.000', method='HALF-EVEN')
try_round(-0.0004, '-0.000', method='HALF-EVEN')
try_round(357.4555, '357.455', method='HALF-EVEN')
try_round(-357.4555, '-357.455', method='HALF-EVEN')
try_round(-0.0004, '0.000', method='HALF-EVEN')
try_round(357.4555, '357.456', method='HALF-EVEN')
try_round(-357.4555, '-357.456', method='HALF-EVEN')
try_round(457.4554, '457.455', method='HALF-EVEN')
try_round(-457.4554, '-457.455', method='HALF-EVEN')
@ -225,7 +228,7 @@ class TestFloatPrecision(TransactionCase):
try_split(2.675, ('2', '68'), float_split_str) # in Python 2.7.2, round(2.675,2) gives 2.67
try_split(-2.675, ('-2', '68'), float_split_str) # in Python 2.7.2, round(2.675,2) gives 2.67
try_split(0.001, ('0', '00'), float_split_str)
try_split(-0.001, ('-0', '00'), float_split_str)
try_split(-0.001, ('0', '00'), float_split_str)
try_split(42, ('42', '00'), float_split_str)
try_split(0.1, ('0', '10'), float_split_str)
try_split(13.0, ('13', ''), float_split_str, rounding=0)
@ -263,12 +266,21 @@ class TestFloatPrecision(TransactionCase):
with self.assertRaises(AssertionError):
float_round(0.01, precision_digits=3, precision_rounding=0.01)
with self.assertRaises(AssertionError):
float_round(-1.0, precision_digits=0, precision_rounding=0.1)
with self.assertRaises(AssertionError):
float_round(1.25, precision_rounding=0.0)
with self.assertRaises(AssertionError):
float_round(1.25, precision_rounding=-0.1)
with self.assertRaises(AssertionError):
float_round(1.25, precision_digits=-1)
with self.assertRaises(AssertionError):
float_round(1.25, precision_digits=0.5)
def test_amount_to_text_10(self):
""" verify that amount_to_text works as expected """
currency = self.env.ref('base.EUR')

View file

@ -0,0 +1,698 @@
from odoo import Command
from odoo.exceptions import ValidationError
from odoo.tests import common
from odoo.tools import SetDefinitions
@common.tagged('at_install', 'groups')
class TestGroupsObject(common.BaseCase):
@classmethod
def setUpClass(cls):
super().setUpClass()
cls.definitions = SetDefinitions({
1: {'ref': 'A'},
2: {'ref': 'A1', 'supersets': [1]}, # A1 <= A
3: {'ref': 'A11', 'supersets': [2]}, # A11 <= A1
4: {'ref': 'A2', 'supersets': [1]}, # A2 <= A
5: {'ref': 'A21', 'supersets': [4]}, # A21 <= A2
6: {'ref': 'A22', 'supersets': [4]}, # A22 <= A2
7: {'ref': 'B'},
8: {'ref': 'B1', 'supersets': [7]}, # B1 <= B
9: {'ref': 'B11', 'supersets': [8]}, # B11 <= B1
10: {'ref': 'B2', 'supersets': [7]}, # B2 <= B
11: {'ref': 'BX',
'supersets': [7], # BX <= B
'disjoints': [8, 10]}, # BX disjoint from B1, B2
12: {'ref': 'A1B1', 'supersets': [2, 8]}, # A1B1 <= A1, B1
13: {'ref': 'C'},
14: {'ref': 'D', 'disjoints': [1, 7]}, # D disjoint from A, B
15: {'ref': 'E', 'disjoints': [1, 7, 14]}, # E disjoint from A, B, D
16: {'ref': 'E1', 'supersets': [15]}, # E1 <= E (and thus disjoint from A, B, D)
})
def test_groups_1_base(self):
A = self.definitions.parse('A')
B = self.definitions.parse('B')
B1 = self.definitions.parse('B1')
self.assertTrue(hash(A), "'Group object must be hashable'")
self.assertEqual(str(A), "'A'")
self.assertEqual(str(B), "'B'")
self.assertEqual(str(B1), "'B1'")
def test_groups_2_and(self):
A = self.definitions.parse('A')
A1 = self.definitions.parse('A1')
B = self.definitions.parse('B')
B1 = self.definitions.parse('B1')
B11 = self.definitions.parse('B11')
BX = self.definitions.parse('BX')
universe = self.definitions.universe
empty = self.definitions.empty
self.assertEqual(str(A & B), "'A' & 'B'")
self.assertEqual(str(B & A), "'A' & 'B'")
self.assertEqual(str(B & BX), "'BX'")
self.assertEqual(str(B1 & BX), "~*")
self.assertEqual(str(B11 & BX), "~*")
self.assertEqual(str(empty & empty), "~*")
self.assertEqual(str(A & universe), "'A'")
self.assertEqual(str(A & empty), "~*")
self.assertEqual(str(A1 & ~A), "~*")
self.assertEqual(str(A & A1 & universe), "'A1'")
def test_groups_3_or(self):
A = self.definitions.parse('A')
A1 = self.definitions.parse('A1')
B = self.definitions.parse('B')
B1 = self.definitions.parse('B1')
B11 = self.definitions.parse('B11')
B2 = self.definitions.parse('B2')
BX = self.definitions.parse('BX')
universe = self.definitions.universe
empty = self.definitions.empty
self.assertEqual(str(A | A), "'A'")
self.assertEqual(str(A | B), "'A' | 'B'")
self.assertEqual(str(A1 | A), "'A'")
self.assertEqual(str(A | A1), "'A'")
self.assertEqual(str(A | B1), "'A' | 'B1'")
self.assertEqual(str(B | A), "'A' | 'B'")
self.assertEqual(str(B | BX), "'B'")
self.assertEqual(str(B1 | BX), "'B1' | 'BX'")
self.assertEqual(str(B11 | BX), "'B11' | 'BX'")
self.assertEqual(str(empty | empty), "~*")
self.assertEqual(str(A | B11 | B2), "'A' | 'B11' | 'B2'")
self.assertEqual(str(A | B2 | B11), "'A' | 'B11' | 'B2'")
self.assertEqual(str(A | empty), "'A'")
self.assertEqual(str(A | universe), "*")
self.assertEqual(str((A | A1) | empty), "'A'")
def test_groups_3_or_and(self):
A = self.definitions.parse('A')
A1 = self.definitions.parse('A1')
A2 = self.definitions.parse('A2')
B1 = self.definitions.parse('B1')
B2 = self.definitions.parse('B2')
universe = self.definitions.universe
empty = self.definitions.empty
self.assertEqual(str((A & B1) | B2), "('A' & 'B1') | 'B2'")
self.assertEqual(str(A | B1 & B2), "'A' | ('B1' & 'B2')")
self.assertEqual(str(A | A1 & universe), "'A'")
self.assertEqual(str((A1 | A2) & (B1 | B2)), "('A1' & 'B1') | ('A1' & 'B2') | ('A2' & 'B1') | ('A2' & 'B2')")
self.assertEqual(str(A | (A1 | empty)), "'A'")
self.assertEqual(str((A & A1) | empty), "'A1'")
self.assertEqual(str(A & (A1 | empty)), "'A1'")
def test_groups_4_gt_lt(self):
A = self.definitions.parse('A')
A1 = self.definitions.parse('A1')
A11 = self.definitions.parse('A11')
A2 = self.definitions.parse('A2')
A21 = self.definitions.parse('A21')
B = self.definitions.parse('B')
B1 = self.definitions.parse('B1')
B11 = self.definitions.parse('B11')
B2 = self.definitions.parse('B2')
A1B1 = self.definitions.parse('A1B1')
self.assertEqual(A == A, True)
self.assertEqual(A == B, False)
self.assertEqual(A >= A1, True)
self.assertEqual(A >= A, True)
self.assertEqual((A & B) >= B, False)
self.assertEqual(B1 >= A1B1, True)
self.assertEqual(B1 >= (A1 | A1B1), False) # noqa: SIM300
self.assertEqual(B >= (A & B), True) # noqa: SIM300
self.assertEqual(A > B, False)
self.assertEqual(A > A1, True)
self.assertEqual(A1 > A, False)
self.assertEqual(A > A, False)
self.assertEqual(A > A11, True)
self.assertEqual(A > A2, True)
self.assertEqual(A > A21, True)
self.assertEqual(A1 > A11, True)
self.assertEqual(A2 > A11, False)
self.assertEqual(A2 > A21, True)
self.assertEqual(A > B1, False)
self.assertEqual(A > B11, False)
self.assertEqual(A > B2, False)
self.assertEqual(A <= A, True)
self.assertEqual(A1 <= A, True)
self.assertEqual((A & B) <= B, True)
self.assertEqual((A & B) <= A, True)
self.assertEqual(B1 <= (A1 | A1B1), False) # noqa: SIM300
self.assertEqual(B <= (A & B), False) # noqa: SIM300
self.assertEqual(A <= (A & B), False) # noqa: SIM300
self.assertEqual(A <= (A | B), True) # noqa: SIM300
self.assertEqual(A < B, False)
self.assertEqual(A < A1, False)
self.assertEqual(A1 < A, True)
self.assertEqual(A < A1, False)
self.assertEqual(A < A11, False)
self.assertEqual(A < A2, False)
self.assertEqual(A < A21, False)
self.assertEqual(A < B1, False)
self.assertEqual(A < B11, False)
self.assertEqual(A < B2, False)
self.assertEqual(A < (A | B), True) # noqa: SIM300
def test_groups_5_invert(self):
A = self.definitions.parse('A')
A1 = self.definitions.parse('A1')
A2 = self.definitions.parse('A2')
B = self.definitions.parse('B')
B1 = self.definitions.parse('B1')
B11 = self.definitions.parse('B11')
B2 = self.definitions.parse('B2')
BX = self.definitions.parse('BX')
universe = self.definitions.universe
empty = self.definitions.empty
self.assertEqual(str(~A), "~'A'")
self.assertEqual(str(~A1), "~'A1'")
self.assertEqual(str(~B), "~'B'")
self.assertEqual(str(~universe), "~*")
self.assertEqual(str(~empty), "*")
self.assertEqual(str(~(A & B)), "~'A' | ~'B'")
self.assertEqual(str(~(A | B)), "~'A' & ~'B'")
self.assertEqual(str(~A & ~A1), "~'A'")
self.assertEqual(str(A | ~A), "*")
self.assertEqual(str(~A | ~A1), "~'A1'")
self.assertEqual(str(~(A | A1)), "~'A'")
self.assertEqual(~(A | A1), ~A & ~A1)
self.assertEqual(str(~(A & A1)), "~'A1'")
self.assertEqual(~(A & A1), ~A | ~A1)
self.assertEqual(str(~(~B1 & ~B2)), "'B1' | 'B2'")
self.assertEqual(str(A & ~A), "~*")
self.assertEqual(str(A & ~A1), "'A' & ~'A1'")
self.assertEqual(str(~A & A), "~*")
self.assertEqual(str(~A & A1), "~*")
self.assertEqual(str(~A1 & A), "'A' & ~'A1'")
self.assertEqual(str(B11 & ~BX), "'B11'")
self.assertEqual(str(~B1 & BX), "'BX'")
self.assertEqual(str(~B11 & BX), "'BX'")
self.assertEqual(str(~((A & B1) | B2)), "(~'A' & ~'B2') | (~'B1' & ~'B2')")
self.assertEqual(str(~(A | (B1 & B2))), "(~'A' & ~'B1') | (~'A' & ~'B2')")
self.assertEqual(str(~(A | (B2 & B1))), "(~'A' & ~'B1') | (~'A' & ~'B2')")
self.assertEqual(str(~((A1 & A2) | (B1 & B2))), "(~'A1' & ~'B1') | (~'A1' & ~'B2') | (~'A2' & ~'B1') | (~'A2' & ~'B2')")
self.assertEqual(str(~A & ~B2), "~'A' & ~'B2'")
self.assertEqual(str(~(~B1 & ~B2)), "'B1' | 'B2'")
self.assertEqual(str(~((A & B) | A1)), "~'A' | (~'A1' & ~'B')")
self.assertEqual(str(~(~A | (~A1 & ~B))), "('A' & 'B') | 'A1'")
self.assertEqual(str(~~((A & B) | A1)), "('A' & 'B') | 'A1'")
def test_groups_6_invert_gt_lt(self):
A = self.definitions.parse('A')
A1 = self.definitions.parse('A1')
self.assertEqual(A < A1, False)
self.assertEqual(~A < ~A1, True)
self.assertEqual(A > A1, True)
self.assertEqual(~A > ~A1, False)
self.assertEqual(~A1 > ~A, True)
self.assertEqual(A < ~A, False) # noqa: SIM300
self.assertEqual(A < ~A1, False) # noqa: SIM300
self.assertEqual(~A < ~A, False)
self.assertEqual(~A < ~A1, True)
def test_groups_7_various(self):
A = self.definitions.parse('A')
A1 = self.definitions.parse('A1')
B = self.definitions.parse('B')
self.assertEqual(str(~A & (A | B)), "~'A' & 'B'")
self.assertEqual(str(A1 & B & ~A), "~*")
self.assertEqual(str(A1 & ~A & B), "~*")
self.assertEqual(str(~A1 & A & B), "'A' & ~'A1' & 'B'")
def test_groups_8_reduce(self):
A = self.definitions.parse('A')
A1 = self.definitions.parse('A1')
A11 = self.definitions.parse('A11')
B = self.definitions.parse('B')
B1 = self.definitions.parse('B1')
B2 = self.definitions.parse('B2')
universe = self.definitions.universe
empty = self.definitions.empty
self.assertEqual(str((A | B) & B), "'B'")
self.assertEqual(str((A & B) | (A & ~B)), "'A'")
self.assertEqual(str((A & B1 & B2) | (A & B1 & ~B2)), "'A' & 'B1'")
self.assertEqual(str((A & ~B2 & B1) | (A & B1 & B2)), "'A' & 'B1'")
self.assertEqual(str((A & B1 & ~B2) | (A & ~B1 & B2)), "('A' & 'B1' & ~'B2') | ('A' & ~'B1' & 'B2')")
self.assertEqual(str(((B2 & A1) | (B2 & A1 & A11)) | ((B2 & A11) | (~B2 & A1) | (~B2 & A1 & A11))), "'A1'")
self.assertEqual(str(~(((B2 & A1) | (B2 & A1 & A11)) | ((B2 & A11) | (~B2 & A1) | (~B2 & A1 & A11)))), "~'A1'")
self.assertEqual(str(~((~A & B) | (A & B) | (A & ~B))), "~'A' & ~'B'")
self.assertEqual(str((~A & ~B2) & (B1 | B2)), "~'A' & 'B1' & ~'B2'")
self.assertEqual(str((~A & ~B2) & ~(~B1 & ~B2)), "~'A' & 'B1' & ~'B2'")
self.assertEqual(str(~A & ~B2 & universe), "~'A' & ~'B2'")
self.assertEqual(str((~A & ~B2 & universe) & ~(~B1 & ~B2)), "~'A' & 'B1' & ~'B2'")
self.assertEqual(str((~A & ~B2 & empty) & ~(~B1 & ~B2)), "~*")
self.assertEqual(str((~A & ~B2) & ~(~B1 & ~B2 & empty)), "~'A' & ~'B2'")
self.assertEqual(str((~A & B1 & A) & B), "~*")
def test_groups_9_distinct(self):
A = self.definitions.parse('A')
A1 = self.definitions.parse('A1')
A11 = self.definitions.parse('A11')
A1B1 = self.definitions.parse('A1B1')
B = self.definitions.parse('B')
B1 = self.definitions.parse('B1')
B11 = self.definitions.parse('B11')
E = self.definitions.parse('E')
E1 = self.definitions.parse('E1')
self.assertEqual(A <= E, False)
self.assertEqual(A >= E, False)
self.assertEqual(A <= ~E, True) # noqa: SIM300
self.assertEqual(A >= ~E, False) # noqa: SIM300
self.assertEqual(A11 <= ~E, True) # noqa: SIM300
self.assertEqual(A11 >= ~E, False) # noqa: SIM300
self.assertEqual(~A >= E, True)
self.assertEqual(~A11 >= E, True)
self.assertEqual(~A >= ~E, False)
self.assertEqual(~A11 >= ~E, False)
self.assertEqual(A <= E1, False)
self.assertEqual(A >= E1, False)
self.assertEqual(A <= ~E1, True) # noqa: SIM300
self.assertEqual(A >= ~E1, False) # noqa: SIM300
self.assertEqual(A11 <= ~E1, True) # noqa: SIM300
self.assertEqual(A11 >= ~E1, False) # noqa: SIM300
self.assertEqual(~A >= E1, True)
self.assertEqual(~A11 >= E1, True)
self.assertEqual(~A >= ~E1, False)
self.assertEqual(~A <= ~E1, False)
self.assertEqual(~A11 >= ~E1, False)
self.assertEqual(str(B11 & ~E), "'B11'")
self.assertEqual(str(~A11 | E), "~'A11'")
self.assertEqual(str(~(A1 & A11 & ~E)), "~'A11'")
self.assertEqual(str(B1 & E), "~*")
self.assertEqual(str(B11 & E), "~*")
self.assertEqual(str(B1 | E), "'B1' | 'E'")
self.assertEqual(str((B1 & E) | A1B1), "'A1B1'")
self.assertEqual(str(A1 & A11 & ~E), "'A11'")
self.assertEqual(str(~E & (E | B)), "'B'")
self.assertEqual(str((~E & E) | B), "'B'")
def test_groups_10_hudge_combine(self):
A1 = self.definitions.parse('A1')
A11 = self.definitions.parse('A11')
B = self.definitions.parse('B')
B1 = self.definitions.parse('B1')
B2 = self.definitions.parse('B2')
A1B1 = self.definitions.parse('A1B1')
C = self.definitions.parse('C')
D = self.definitions.parse('D')
E = self.definitions.parse('E')
Z1 = C | B2 | A1 | A11
Z2 = (C) | (C & B2) | (C & B2 & A1) | (C & B2 & A11) | (C & ~B2) | (C & ~B2 & A1)
Z3 = (C & ~B2 & A11) | (C & A1) | (C & A1 & B1) | (C & A11) | (C & A11 & B1) | (C & B1)
Z4 = (B2 & A1) | (B2 & A1 & A11) | (B2 & A11) | (~B2 & A1) | (~B2 & A1 & A11)
Z5 = (~B2 & A11) | (A1) | (A1 & A11) | (A1 & A11 & B1) | (A1 & B1) | (A11) | (A11 & B1)
group1 = Z1 & (Z2 | Z3 | Z4 | Z5)
self.assertEqual(str(group1), "'A1' | 'C'")
self.assertEqual(str(~group1), "~'A1' & ~'C'")
self.assertEqual(str(~~group1), "'A1' | 'C'")
self.assertEqual(str((~group1).invert_intersect(~A1)), "~'C'")
self.assertEqual(str(group1 & B), "('A1' & 'B') | ('B' & 'C')")
self.assertEqual(str(~(group1 & B)), "(~'A1' & ~'C') | ~'B'")
self.assertEqual(str(~~(group1 & B)), "('A1' & 'B') | ('B' & 'C')")
self.assertEqual(str((group1 & B).invert_intersect(B)), "'A1' | 'C'")
self.assertFalse((group1 & B).invert_intersect(A1))
self.assertEqual(str(A1 & D), "~*")
self.assertEqual(str(group1 & (C | B | D)), "('A1' & 'B') | 'C'")
self.assertEqual(str(~(group1 & (C | B | D))), "(~'A1' & ~'C') | (~'B' & ~'C')")
group2 = (B1 | D) & (A1B1 | (A1B1 & D) | (A1B1 & D & E) | (A1B1 & E) | E)
self.assertEqual(str(group2), "'A1B1'")
def test_groups_11_invert_intersect(self):
A = self.definitions.parse('A')
A1 = self.definitions.parse('A1')
A11 = self.definitions.parse('A11')
A2 = self.definitions.parse('A2')
A21 = self.definitions.parse('A21')
A22 = self.definitions.parse('A22')
B = self.definitions.parse('B')
B1 = self.definitions.parse('B1')
B2 = self.definitions.parse('B2')
D = self.definitions.parse('D')
self.assertEqual(str((A1 & A2).invert_intersect(A2)), "'A1'")
self.assertEqual(str((A1 & B1 | A1 & B2).invert_intersect(A1)), "'B1' | 'B2'")
self.assertEqual(str((A1 & B1 | A1 & B2 | A1 & A2).invert_intersect(A1)), "'A2' | 'B1' | 'B2'")
self.assertEqual(str((A1 & B1 | A2 & B1).invert_intersect(A1 | A2)), "'B1'")
self.assertEqual(str((A1 & B1 | A1 & B2 | A2 & B1 | A2 & B2).invert_intersect(A1 | A2)), "'B1' | 'B2'")
self.assertEqual(A.invert_intersect(A | B), None)
self.assertEqual(A.invert_intersect(A1 | A2), None)
self.assertEqual(A.invert_intersect(A | D), None)
tests = [
(A2, A1),
(B1 | B2, A1),
(A2 | B1 | B2, A1),
(B1, A1 | A2),
(B1 | B2, A1 | A2),
(B1 & B2, A1),
(A2 & B1 & B2, A1),
(B1 & B2, A1 | A2),
(A1, B1 & B2),
(A1 | A2, B1 & B2),
(A1, A2 | B1 & B2),
(A11 | A21, A22 | B1 & B2),
(A11 & A21, A22 | B1 & B2),
(A, A1 | B),
(A1 | B, A),
]
for a, b in tests:
self.assertEqual(str((a & b).invert_intersect(b)), str(a), f'Should invert_intersect: {a & b}\nby: ({b})')
def test_groups_matches(self):
A = self.definitions.parse('A')
A1 = self.definitions.parse('A1')
A11 = self.definitions.parse('A11')
B = self.definitions.parse('B')
C = self.definitions.parse('C')
D = self.definitions.parse('D')
matching = [
(A, {1, 13}),
(A, {1, 2, 3, 13}),
(A1, {1, 2, 13}),
(A11, {1, 2, 3, 13}),
(A | B, {1, 13}),
(B | C, {1, 13}),
(A1 | B, {1, 2, 13}),
(A11 | B, {1, 2, 3, 13}),
((A11 | B) & ~D, {1, 2, 3, 13}),
(A & ~A11, {1, 13}),
(A & ~A11, {1, 2, 13}),
]
for spec, group_ids in matching:
self.assertTrue(
spec.matches(group_ids),
f"user with groups {self.definitions.from_ids(group_ids, keep_subsets=True)} should match {spec}",
)
non_matching = [
(A, {13}),
(A1, {13}),
(A11, {13}),
(A | B, {13}),
(A & ~C, {13}),
(A & ~B & ~C, {13}),
((A11 | B) & ~C, {1, 2, 3, 13}),
(A & ~A11, {1, 2, 3, 13}),
]
for spec, group_ids in non_matching:
self.assertFalse(
spec.matches(group_ids),
f"user with groups {self.definitions.from_ids(group_ids, keep_subsets=True)} should not match {spec}",
)
def test_groups_unknown(self):
A = self.definitions.parse('A')
U1 = self.definitions.parse('unknown.group1', raise_if_not_found=False)
U2 = self.definitions.parse('unknown.group2', raise_if_not_found=False)
self.assertEqual(U1, U1)
self.assertNotEqual(U1, U2)
self.assertEqual(A | U1, U1 | A)
self.assertEqual(U1 | U2, U2 | U1)
self.assertEqual(A & U1, U1 & A)
self.assertEqual(U1 & U2, U2 & U1)
self.assertEqual(A | U1 | U2, A | U1 | U2)
self.assertEqual(A | U2 | U1, A | U1 | U2)
self.assertEqual(U1 | A | U2, A | U1 | U2)
self.assertEqual(U1 | A | U2, A | U1 | U2)
self.assertEqual(U2 | A | U1, A | U1 | U2)
self.assertEqual(U2 | U1 | A, A | U1 | U2)
self.assertEqual(A & U1 & U2, A & U1 & U2)
self.assertEqual(A & U2 & U1, A & U1 & U2)
self.assertEqual(U1 & A & U2, A & U1 & U2)
self.assertEqual(U1 & A & U2, A & U1 & U2)
self.assertEqual(U2 & A & U1, A & U1 & U2)
self.assertEqual(U2 & U1 & A, A & U1 & U2)
def test_groups_key(self):
A = self.definitions.parse('A')
B = self.definitions.parse('B')
C = self.definitions.parse('C')
U = self.definitions.parse('unknown.group', raise_if_not_found=False)
test_cases = [
A,
A | B,
A & B,
A & ~B,
(A | B) & ~C,
U,
A | U | B,
]
for groups in test_cases:
self.assertIsInstance(groups.key, str)
groups1 = self.definitions.from_key(groups.key)
self.assertEqual(groups1, groups)
self.assertEqual(groups1.key, groups.key)
@common.tagged('at_install', 'groups')
class TestGroupsOdoo(common.TransactionCase):
@classmethod
def setUpClass(cls):
super().setUpClass()
cls.test_group = cls.env['res.groups'].create({
'name': 'test with implied user',
'implied_ids': [Command.link(cls.env.ref('base.group_user').id)]
})
cls.env["ir.model.data"].create({
"module": "base",
"name": "base_test_group",
"model": "res.groups",
"res_id": cls.test_group.id,
})
cls.definitions = cls.env['res.groups']._get_group_definitions()
def parse_repr(self, group_repr):
""" Return the group object from the string (given by the repr of the group object).
:param group_repr: str
Use | (union) and & (intersection) separator like the python object.
intersection it's apply before union.
Can use an invertion with ~.
"""
if not group_repr:
return self.definitions.universe
res = None
for union in group_repr.split('|'):
union = union.strip()
intersection = None
if union.startswith('(') and union.endswith(')'):
union = union[1:-1]
for xmlid in union.split('&'):
xmlid = xmlid.strip()
leaf = ~self.definitions.parse(xmlid[1:]) if xmlid.startswith('~') else self.definitions.parse(xmlid)
if intersection is None:
intersection = leaf
else:
intersection &= leaf
if intersection is None:
return self.definitions.universe
elif res is None:
res = intersection
else:
res |= intersection
return self.definitions.empty if res is None else res
def test_groups_1_base(self):
parse = self.definitions.parse
self.assertEqual(str(parse('base.group_user') & parse('base.group_user')), "'base.group_user'")
self.assertEqual(str(parse('base.group_user') & parse('base.group_system')), "'base.group_system'")
self.assertEqual(str(parse('base.group_system') & parse('base.group_user')), "'base.group_system'")
self.assertEqual(str(parse('base.group_erp_manager') & parse('base.group_system')), "'base.group_system'")
self.assertEqual(str(parse('base.group_system') & parse('base.group_allow_export')), "'base.group_system' & 'base.group_allow_export'")
self.assertEqual(str(parse('base.group_user') | parse('base.group_user')), "'base.group_user'")
self.assertEqual(str(parse('base.group_user') | parse('base.group_system')), "'base.group_user'")
self.assertEqual(str(parse('base.group_system') | parse('base.group_public')), "'base.group_system' | 'base.group_public'")
self.assertEqual(parse('base.group_system') < parse('base.group_erp_manager'), True)
self.assertEqual(parse('base.group_system') < parse('base.group_sanitize_override'), True)
self.assertEqual(parse('base.group_erp_manager') < parse('base.group_user'), True)
self.assertEqual(parse('!base.group_portal') < parse('!base.group_public'), False)
self.assertEqual(parse('base.base_test_group') == parse('base.base_test_group'), True)
self.assertEqual(parse('base.group_system') <= parse('base.group_system'), True)
self.assertEqual(parse('base.group_public') <= parse('base.group_system'), False) # None ?
self.assertEqual(parse('base.group_user') <= parse('base.group_system'), False)
self.assertEqual(parse('base.group_system') <= parse('base.group_user'), True)
self.assertEqual(parse('base.group_user') <= parse('base.group_portal'), False)
self.assertEqual(parse('!base.group_portal') <= parse('!base.group_public'), False)
def test_groups_2_from_commat_separator(self):
parse = self.definitions.parse
self.assertEqual(str(parse('base.group_user,base.group_system') & parse('base.group_system')), "'base.group_system'")
self.assertEqual(str(parse('base.group_user,base.group_erp_manager') & parse('base.group_system')), "'base.group_system'")
self.assertEqual(str(parse('base.group_user,base.group_portal') & parse('base.group_portal')), "'base.group_portal'")
self.assertEqual(str(parse('base.group_user,base.group_portal,base.group_public,base.group_multi_company') & parse('base.group_portal,base.group_public')), "'base.group_portal' | 'base.group_public'")
self.assertEqual(str(parse('base.group_system,base.base_test_group') & parse('base.group_user')), "'base.group_system' | 'base.base_test_group'")
self.assertEqual(str(parse('base.group_system,base.group_portal') & parse('base.group_user')), "'base.group_system'")
self.assertEqual(str(parse('base.group_user') & parse('!base.group_portal,base.group_system')), "'base.group_system'")
self.assertEqual(str(parse('!base.group_portal') & parse('base.group_portal,base.group_system')), "'base.group_system'")
self.assertEqual(str(parse('base.group_portal,!base.group_user') & parse('base.group_user')), "~*")
self.assertEqual(str(parse('!base.group_user') & parse('base.group_portal,base.group_user')), "'base.group_portal'")
self.assertEqual(str(parse('base.group_user') & parse('base.group_portal,!base.group_user')), "~*")
self.assertEqual(str(parse('!base.group_user') & parse('base.group_portal,!base.group_system')), "'base.group_portal'")
self.assertEqual(str(parse('!base.group_user,base.group_allow_export') & parse('base.group_allow_export,!base.group_system')), "~'base.group_user' & 'base.group_allow_export'")
self.assertEqual(str(parse('!base.group_user,base.group_portal') & parse('base.group_portal,!base.group_system')), "'base.group_portal'")
self.assertEqual(str(parse('!*') & parse('base.group_portal')), "~*")
self.assertEqual(str(parse('*') & parse('base.group_portal')), "'base.group_portal'")
self.assertEqual(str(parse('base.group_user,!base.group_system') & parse('base.group_erp_manager,base.group_portal')), "'base.group_erp_manager' & ~'base.group_system'")
self.assertEqual(str(parse('base.group_user,!base.group_system') & parse('base.group_portal,base.group_erp_manager')), "'base.group_erp_manager' & ~'base.group_system'")
self.assertEqual(str(parse('base.group_user') & parse('base.group_portal,base.group_erp_manager,!base.group_system')), "'base.group_erp_manager' & ~'base.group_system'")
self.assertEqual(str(parse('base.group_user') & parse('base.group_portal,base.group_system')), "'base.group_system'")
self.assertEqual(str(parse('base.group_user,base.group_system') & parse('base.group_portal,base.group_system')), "'base.group_system'")
self.assertEqual(str(parse('base.group_user') & parse('base.group_portal,base.group_erp_manager')), "'base.group_erp_manager'")
self.assertEqual(str(parse('base.group_user') & parse('base.group_portal,!base.group_system')), "~*")
self.assertEqual(str(parse('base.group_user,base.group_system') & parse('base.group_system,base.group_portal')), "'base.group_system'")
self.assertEqual(str(parse('base.group_user') & parse('base.group_system,base.group_portal')), "'base.group_system'")
self.assertEqual(str(parse('base.group_user,base.group_system') & parse('base.group_allow_export')), "'base.group_user' & 'base.group_allow_export'")
self.assertEqual(str(parse('base.group_user,base.group_erp_manager') | parse('base.group_system')), "'base.group_user'")
self.assertEqual(str(parse('base.group_user') | parse('base.group_portal,base.group_system')), "'base.group_user' | 'base.group_portal'")
self.assertEqual(str(parse('!*') | parse('base.group_user')), "'base.group_user'")
self.assertEqual(str(parse('base.group_user') | parse('!*')), "'base.group_user'")
self.assertEqual(str(parse('!*') | parse('base.group_user,base.group_portal')), "'base.group_user' | 'base.group_portal'")
self.assertEqual(str(parse('*') | parse('base.group_user')), "*")
self.assertEqual(str(parse('base.group_user') | parse('*')), "*")
self.assertEqual(str(parse('base.group_user,base.group_erp_manager') | parse('base.group_system,base.group_public')), "'base.group_user' | 'base.group_public'")
self.assertEqual(parse('base.group_system') < parse('base.group_erp_manager,base.group_sanitize_override'), True)
self.assertEqual(parse('!base.group_public,!base.group_portal') < parse('!base.group_public'), True)
self.assertEqual(parse('base.group_system,base.base_test_group') == parse('base.group_system,base.base_test_group'), True)
self.assertEqual(parse('base.group_system,base.base_test_group') == parse('base.base_test_group,base.group_system'), True)
self.assertEqual(parse('base.group_system,base.base_test_group') == parse('base.base_test_group,base.group_public'), False)
self.assertEqual(parse('base.group_system,base.base_test_group') == parse('base.base_test_group'), False)
self.assertEqual(parse('base.group_user') <= parse('base.group_system,base.group_public'), False)
self.assertEqual(parse('base.group_system') <= parse('base.group_user,base.group_public'), True)
self.assertEqual(parse('base.group_public') <= parse('base.group_system,base.group_public'), True)
self.assertEqual(parse('base.group_system,base.group_public') <= parse('base.group_system,base.group_public'), True)
self.assertEqual(parse('base.group_system,base.group_public') <= parse('base.group_user,base.group_public'), True)
self.assertEqual(parse('base.group_system,!base.group_public') <= parse('base.group_system'), True)
self.assertEqual(parse('base.group_system,!base.group_allow_export') <= parse('base.group_system'), True)
self.assertEqual(parse('base.group_system') <= parse('base.group_system,!base.group_allow_export'), False)
self.assertEqual(parse('base.group_system') <= parse('base.group_system,!base.group_public'), True)
self.assertEqual(parse('base.group_system') == parse('base.group_system,!base.group_public'), True)
self.assertEqual(parse('!base.group_public,!base.group_portal') <= parse('!base.group_public'), True)
self.assertEqual(parse('base.group_user,!base.group_allow_export') <= parse('base.group_user,!base.group_system,!base.group_allow_export'), False)
self.assertEqual(parse('base.group_system,!base.group_portal,!base.group_public') <= parse('base.group_system,!base.group_public'), True)
def test_groups_3_from_ref(self):
parse = self.parse_repr
self.assertEqual(str(parse('base.group_user & base.group_portal | base.group_user & ~base.group_system') & parse('base.group_public')), "~*")
self.assertEqual(str(parse('base.group_user & base.group_portal | base.group_user & ~base.group_system') & parse('~base.group_user')), "~*")
self.assertEqual(str(parse('base.group_user & base.group_portal | base.group_user & ~base.group_system') & parse('~base.group_user & base.group_portal')), "~*")
self.assertEqual(str(parse('base.group_user & base.group_portal | base.group_user & base.group_system') & parse('base.group_user & ~base.group_portal')), "'base.group_system'")
self.assertEqual(str(parse('base.group_public & base.group_erp_manager | base.group_public & base.group_portal') & parse('*')), "~*")
self.assertEqual(str(parse('base.group_system & base.group_allow_export') & parse('base.group_portal | base.group_system')), "'base.group_system' & 'base.group_allow_export'")
self.assertEqual(str(parse('base.group_portal & base.group_erp_manager') | parse('base.group_erp_manager')), "'base.group_erp_manager'")
self.assertEqual(parse('base.group_system & base.group_allow_export') < parse('base.group_system'), True)
self.assertEqual(parse('base.base_test_group') == parse('base.base_test_group & base.group_user'), True)
self.assertEqual(parse('base.group_system | base.base_test_group') == parse('base.group_system & base.group_user | base.base_test_group & base.group_user'), True)
self.assertEqual(parse('base.group_public & base.group_allow_export') <= parse('base.group_public'), True)
self.assertEqual(parse('base.group_public') <= parse('base.group_public & base.group_allow_export'), False)
self.assertEqual(parse('base.group_public & base.group_user') <= parse('base.group_portal'), True)
self.assertEqual(parse('base.group_public & base.group_user') <= parse('base.group_public | base.group_user'), True)
self.assertEqual(parse('base.group_public & base.group_system') <= parse('base.group_user'), True)
self.assertEqual(parse('base.group_public & base.group_system') <= parse('base.group_portal | base.group_user'), True)
self.assertEqual(parse('base.group_public & base.group_allow_export') <= parse('~base.group_public'), False)
self.assertEqual(parse('base.group_portal & base.group_public | base.group_system & base.group_public') <= parse('base.group_public'), True)
self.assertEqual(parse('base.group_portal & base.group_user | base.group_system & base.group_user') <= parse('base.group_user'), True)
self.assertEqual(parse('base.group_portal & base.group_system | base.group_user & base.group_system') <= parse('base.group_system'), True)
self.assertEqual(parse('base.group_portal & base.group_user | base.group_user & base.group_user') <= parse('base.group_user'), True)
self.assertEqual(parse('base.group_portal & base.group_user | base.group_user & base.group_user') <= parse('base.group_user'), True)
self.assertEqual(parse('base.group_public') <= parse('base.group_portal & base.group_public | base.group_system & base.group_public'), False)
self.assertEqual(parse('base.group_user & base.group_allow_export') <= parse('base.group_user & base.group_system & base.group_allow_export'), False)
self.assertEqual(parse('base.group_system & base.group_allow_export') <= parse('base.group_user & base.group_system & base.group_allow_export'), True)
self.assertEqual(parse('base.group_system & base.group_allow_export') <= parse('base.group_system'), True)
self.assertEqual(parse('base.group_public') >= parse('base.group_portal & base.group_public | base.group_system & base.group_public'), True)
self.assertEqual(parse('base.group_user & base.group_public') >= parse('base.group_user & base.group_portal & base.group_public | base.group_user & base.group_system & base.group_public'), True)
self.assertEqual(parse('base.group_system & base.group_allow_export') >= parse('base.group_system'), False)
self.assertEqual(parse('base.group_system & base.group_allow_export') > parse('base.group_system'), False)
def test_groups_4_full_empty(self):
user_group_ids = self.env.user._get_group_ids()
self.assertFalse(self.definitions.parse('base.group_public').matches(user_group_ids))
self.assertTrue(self.definitions.parse('*').matches(user_group_ids))
self.assertFalse((~self.definitions.parse('*')).matches(user_group_ids))
def test_groups_5_contains_user(self):
# user is included into the defined group of users
user = self.env['res.users'].create({
'name': 'A User',
'login': 'a_user',
'email': 'a@user.com',
})
tests = [
# group on the user, # groups access, access
('base.group_public', 'base.group_system | base.group_public', True),
('base.group_public,base.group_allow_export', 'base.group_user | base.group_public', True),
('base.group_public', 'base.group_system & base.group_public', False),
('base.group_public', 'base.group_system | base.group_portal', False),
('base.group_public', 'base.group_system & base.group_portal', False),
('base.group_system', 'base.group_system | base.group_public', True),
('base.group_system', 'base.group_system & base.group_public', False),
('base.group_system', 'base.group_user | base.group_system', True),
('base.group_system', 'base.group_user & base.group_system', True),
('base.group_public', 'base.group_user | base.group_system', False),
('base.group_public', 'base.group_user & base.group_system', False),
('base.group_system', 'base.group_system & ~base.group_user', False),
('base.group_portal', 'base.group_system & ~base.group_user', False),
('base.group_user', 'base.group_user & ~base.group_system', True),
('base.group_user', '~base.group_system & base.group_user', True),
('base.group_system', 'base.group_user & ~base.group_system', False),
('base.group_portal', 'base.group_portal & ~base.group_user', True),
('base.group_system', '~base.group_system & base.group_user', False),
('base.group_system', '~base.group_system & ~base.group_user', False),
('base.group_user', 'base.group_user & base.group_sanitize_override & base.group_allow_export', False),
('base.group_system', 'base.group_user & base.group_sanitize_override & base.group_allow_export', False),
('base.group_system,base.group_allow_export', 'base.group_user & base.group_sanitize_override & base.group_allow_export', True),
('base.group_user,base.group_sanitize_override,base.group_allow_export', 'base.group_user & base.group_sanitize_override & base.group_allow_export', True),
('base.group_user', 'base.group_erp_manager | base.group_multi_company', False),
('base.group_user,base.group_erp_manager', 'base.group_erp_manager | base.group_multi_company', True),
]
for user_groups, groups, result in tests:
user.groups_id = [(6, 0, [self.env.ref(xmlid).id for xmlid in user_groups.split(',')])]
self.assertEqual(self.parse_repr(groups).matches(user._get_group_ids()), result, f'User ({user_groups!r}) should {"" if result else "not "}have access to groups: ({groups!r})')
def test_groups_6_distinct(self):
user = self.env['res.users'].create({
'name': 'A User',
'login': 'a_user',
'email': 'a@user.com',
'groups_id': self.env.ref('base.group_user').ids,
})
with self.assertRaisesRegex(ValidationError, "The user cannot have more than one user types."):
user.groups_id = [(4, self.env.ref('base.group_public').id)]
with self.assertRaisesRegex(ValidationError, "The user cannot have more than one user types."):
user.groups_id = [(4, self.env.ref('base.group_portal').id)]

View file

@ -1,12 +1,12 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
import logging
import threading
from unittest.mock import patch
from odoo.http import Controller, request, route
from odoo.tests.common import ChromeBrowser, HttpCase, tagged
from odoo.tools import config, logging
from odoo.tools import config
_logger = logging.getLogger(__name__)
@ -52,10 +52,27 @@ class TestHttpCase(HttpCase):
text = log.split('.browser:', 1)[1]
if text == 'test successful':
continue
if text.startswith('heap '):
continue
self.assertEqual(text, "Object(custom=Object, value=1, description='dummy')")
console_log_count += 1
self.assertEqual(console_log_count, 1)
@tagged('-at_install', 'post_install')
class TestRunbotLog(HttpCase):
def test_runbot_js_log(self):
"""Test that a ChromeBrowser console.dir is handled server side as a log of level RUNBOT."""
log_message = 'this is a small test'
with self.assertLogs() as log_catcher:
self.browser_js("about:blank", f"console.runbot = console.dir; console.runbot('{log_message}'); console.log('test successful');")
found = False
for record in log_catcher.records:
if record.message == log_message:
self.assertEqual(record.levelno, logging.RUNBOT)
self.assertTrue(record.name.endswith('browser'))
found = True
self.assertTrue(found, "Runbot log not found")
@tagged('-at_install', 'post_install')
class TestChromeBrowser(HttpCase):

View file

@ -0,0 +1,33 @@
from odoo.tests import TransactionCase
from odoo.tools import format_list, py_to_js_locale
class I18nTest(TransactionCase):
def test_format_list(self):
lang = self.env["res.lang"]
formatted_text = format_list(self.env, ["Mario", "Luigi"])
self.assertEqual(formatted_text, "Mario and Luigi", "Should default to English.")
formatted_text = format_list(self.env, ["To be", "Not to be"], "or")
self.assertEqual(formatted_text, "To be or Not to be", "Should take the style into account.")
lang._activate_lang("fr_FR")
formatted_text = format_list(lang.with_context(lang="fr_FR").env, ["Athos", "Porthos", "Aramis"])
self.assertEqual(formatted_text, "Athos, Porthos et Aramis", "Should use the language of the user.")
formatted_text = format_list(
lang.with_context(lang="en_US").env, ["Athos", "Porthos", "Aramis"], lang_code="fr_FR",
)
self.assertEqual(formatted_text, "Athos, Porthos et Aramis", "Should use the chosen language.")
def test_py_to_js_locale(self):
self.assertEqual(py_to_js_locale("tg"), "tg")
self.assertEqual(py_to_js_locale("kab"), "kab")
self.assertEqual(py_to_js_locale("fr_BE"), "fr-BE")
self.assertEqual(py_to_js_locale("es_419"), "es-419")
self.assertEqual(py_to_js_locale("sr@latin"), "sr-Latn")
self.assertEqual(py_to_js_locale("sr@Cyrl"), "sr-Cyrl")
self.assertEqual(py_to_js_locale("sr_RS@latin"), "sr-Latn-RS")
self.assertEqual(py_to_js_locale("fr-TG"), "fr-TG")

View file

@ -3,11 +3,10 @@
import base64
import io
import binascii
from PIL import Image, ImageDraw, PngImagePlugin
from odoo import tools
from odoo.tools import image as tools
from odoo.exceptions import UserError
from odoo.tests.common import TransactionCase
@ -369,4 +368,4 @@ class TestImage(TransactionCase):
"""converts to RGB when saving as JPEG"""
image1 = Image.new('P', (1, 1), color='red')
image2 = Image.new('RGB', (1, 1), color='red')
self.assertEqual(tools.image.image_apply_opt(image1, 'JPEG'), tools.image.image_apply_opt(image2, 'JPEG'))
self.assertEqual(tools.image_apply_opt(image1, 'JPEG'), tools.image_apply_opt(image2, 'JPEG'))

View file

@ -478,7 +478,7 @@ ZeroDivisionError: division by zero""" % self.test_server_action.id
def test_80_permission(self):
self.action.write({
'state': 'code',
'code': """record.write({'date': datetime.date.today()})""",
'code': """record.write({'name': str(datetime.date.today())})""",
})
user_demo = self.user_demo
@ -486,10 +486,10 @@ ZeroDivisionError: division by zero""" % self.test_server_action.id
# can write on contact partner
self.test_partner.type = "contact"
self.test_partner.with_user(user_demo.id).check_access_rule("write")
self.test_partner.with_user(user_demo.id).check_access("write")
self_demo.with_context(self.context).run()
self.assertEqual(self.test_partner.date, date.today())
self.assertEqual(self.test_partner.name, str(date.today()))
def test_90_webhook(self):
self.action.write({
@ -573,7 +573,7 @@ class TestCommonCustomFields(common.TransactionCase):
return self.env['ir.ui.view'].create({
'name': 'yet another view',
'model': self.MODEL,
'arch': '<tree string="X"><field name="%s"/></tree>' % name,
'arch': '<list string="X"><field name="%s"/></list>' % name,
})
@ -765,7 +765,7 @@ class TestCustomFields(TestCommonCustomFields):
# create a non-computed field, and assert how many queries it takes
model_id = self.env['ir.model']._get_id('res.partner')
query_count = 48
query_count = 49
with self.assertQueryCount(query_count):
self.env.registry.clear_cache()
self.env['ir.model.fields'].create({

View file

@ -11,7 +11,8 @@ from PIL import Image
import odoo
from odoo.exceptions import AccessError
from odoo.addons.base.tests.common import TransactionCaseWithUserDemo
from odoo.tools import image_to_base64, mute_logger
from odoo.tools import mute_logger
from odoo.tools.image import image_to_base64
HASH_SPLIT = 2 # FIXME: testing implementations detail is not a good idea
@ -247,20 +248,15 @@ class TestIrAttachment(TransactionCaseWithUserDemo):
self.assertFalse(os.path.isfile(store_path), 'file removed')
def test_13_rollback(self):
self.registry.enter_test_mode(self.cr)
self.addCleanup(self.registry.leave_test_mode)
self.cr = self.registry.cursor()
self.addCleanup(self.cr.close)
self.env = odoo.api.Environment(self.cr, odoo.SUPERUSER_ID, {})
savepoint = self.cr.savepoint()
# the data needs to be unique so that no other attachment link
# the file so that the gc removes it
unique_blob = os.urandom(16)
a1 = self.Attachment.create({'name': 'a1', 'raw': unique_blob})
a1 = self.env['ir.attachment'].create({'name': 'a1', 'raw': unique_blob})
store_path = os.path.join(self.filestore, a1.store_fname)
self.assertTrue(os.path.isfile(store_path), 'file exists')
self.env.cr.rollback()
self.Attachment._gc_file_store_unsafe()
savepoint.rollback()
self.env['ir.attachment']._gc_file_store_unsafe()
self.assertFalse(os.path.isfile(store_path), 'file removed')
def test_14_invalid_mimetype_with_correct_file_extension_no_post_processing(self):

View file

@ -1,19 +1,19 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
import collections
# ruff: noqa: E201, E272, E301, E306
import secrets
import textwrap
import threading
from concurrent.futures import ThreadPoolExecutor
from contextlib import closing
from datetime import timedelta
from unittest.mock import call, patch
from unittest.mock import patch
from freezegun import freeze_time
import odoo
from odoo import api, fields
from odoo.tests.common import BaseCase, TransactionCase, RecordCapturer, get_db_name, tagged
from odoo import fields
from odoo.tests.common import TransactionCase, RecordCapturer
from odoo.tools import mute_logger
from odoo.addons.base.models.ir_cron import MIN_FAILURE_COUNT_BEFORE_DEACTIVATION, MIN_DELTA_BEFORE_DEACTIVATION
class CronMixinCase:
@ -51,8 +51,6 @@ class CronMixinCase:
'active': True,
'interval_number': 1,
'interval_type': 'days',
'numbercall': -1,
'doall': False,
'nextcall': fields.Datetime.now() + timedelta(hours=1),
'lastcall': False,
'priority': priority,
@ -78,11 +76,13 @@ class TestIrCron(TransactionCase, CronMixinCase):
cls.partner = cls.env['res.partner'].create(cls._get_partner_data(cls.env))
def setUp(self):
super().setUp()
self.partner.write(self._get_partner_data(self.env))
self.cron.write(self._get_cron_data(self.env))
self.env['ir.cron.trigger'].search(
[('cron_id', '=', self.cron.id)]
).unlink()
domain = [('cron_id', '=', self.cron.id)]
self.env['ir.cron.trigger'].search(domain).unlink()
self.env['ir.cron.progress'].search(domain).unlink()
def test_cron_direct_trigger(self):
self.cron.code = textwrap.dedent(f"""\
@ -122,19 +122,7 @@ class TestIrCron(TransactionCase, CronMixinCase):
def test_cron_unactive_never_ready(self):
self.cron.active = False
self.cron.nextcall = fields.Datetime.now()
self.cron._trigger()
self.cron.flush_recordset()
self.env['ir.cron.trigger'].flush_model()
ready_jobs = self.registry['ir.cron']._get_all_ready_jobs(self.cr)
self.assertNotIn(self.cron.id, [job['id'] for job in ready_jobs])
def test_cron_numbercall0_never_ready(self):
self.cron.numbercall = 0
self.cron.nextcall = fields.Datetime.now()
self.cron._trigger()
self.cron.flush_recordset()
self.env['ir.cron.trigger'].flush_model()
self.env.flush_all()
ready_jobs = self.registry['ir.cron']._get_all_ready_jobs(self.cr)
self.assertNotIn(self.cron.id, [job['id'] for job in ready_jobs])
@ -205,202 +193,356 @@ class TestIrCron(TransactionCase, CronMixinCase):
self.assertTrue(capture.records, "trigger should has been kept")
def test_cron_process_job(self):
Progress = self.env['ir.cron.progress']
default_progress_values = {'done': 0, 'remaining': 0, 'timed_out_counter': 0}
ten_days_ago = fields.Datetime.now() - MIN_DELTA_BEFORE_DEACTIVATION - timedelta(days=2)
almost_failed = MIN_FAILURE_COUNT_BEFORE_DEACTIVATION - 1
Setup = collections.namedtuple('Setup', ['doall', 'numbercall', 'missedcall', 'trigger'])
Expect = collections.namedtuple('Expect', ['call_count', 'call_left', 'active'])
def nothing(cron):
state = {'call_count': 0}
def f(self):
state['call_count'] += 1
return f, state
matrix = [
(Setup(doall=False, numbercall=-1, missedcall=2, trigger=False),
Expect(call_count=1, call_left=-1, active=True)),
(Setup(doall=True, numbercall=-1, missedcall=2, trigger=False),
Expect(call_count=2, call_left=-1, active=True)),
(Setup(doall=False, numbercall=3, missedcall=2, trigger=False),
Expect(call_count=1, call_left=2, active=True)),
(Setup(doall=True, numbercall=3, missedcall=2, trigger=False),
Expect(call_count=2, call_left=1, active=True)),
(Setup(doall=True, numbercall=3, missedcall=4, trigger=False),
Expect(call_count=3, call_left=0, active=False)),
(Setup(doall=True, numbercall=3, missedcall=0, trigger=True),
Expect(call_count=1, call_left=2, active=True)),
def eleven_success(cron):
state = {'call_count': 0}
CALL_TARGET = 11
def f(self):
state['call_count'] += 1
self.env['ir.cron']._notify_progress(
done=1,
remaining=CALL_TARGET - state['call_count']
)
return f, state
def five_success(cron):
state = {'call_count': 0}
CALL_TARGET = 5
def f(self):
state['call_count'] += 1
self.env['ir.cron']._notify_progress(
done=1,
remaining=CALL_TARGET - state['call_count']
)
return f, state
def failure(cron):
state = {'call_count': 0}
def f(self):
state['call_count'] += 1
raise ValueError
return f, state
def failure_partial(cron):
state = {'call_count': 0}
CALL_TARGET = 5
def f(self):
state['call_count'] += 1
self.env['ir.cron']._notify_progress(
done=1,
remaining=CALL_TARGET - state['call_count']
)
self.env.cr.commit()
raise ValueError
return f, state
def failure_fully(cron):
state = {'call_count': 0}
def f(self):
state['call_count'] += 1
self.env['ir.cron']._notify_progress(done=1, remaining=0)
self.env.cr.commit()
raise ValueError
return f, state
CASES = [
# IN | OUT
# callback, curr_failures, trigger, call_count, done_count, fail_count, active,
( nothing, 0, False, 1, 0, 0, True),
( nothing, almost_failed, False, 1, 0, 0, True),
( eleven_success, 0, True, 10, 10, 0, True),
( eleven_success, almost_failed, True, 10, 10, 0, True),
( five_success, 0, False, 5, 5, 0, True),
( five_success, almost_failed, False, 5, 5, 0, True),
( failure, 0, False, 1, 0, 1, True),
( failure, almost_failed, False, 1, 0, 0, False),
(failure_partial, 0, False, 5, 5, 1, True),
(failure_partial, almost_failed, False, 5, 5, 0, False),
( failure_fully, 0, False, 1, 1, 1, True),
( failure_fully, almost_failed, False, 1, 1, 0, False),
]
for setup, expect in matrix:
with self.subTest(setup=setup, expect=expect):
for cb, curr_failures, trigger, call_count, done_count, fail_count, active in CASES:
with self.subTest(cb=cb, failure=curr_failures), closing(self.cr.savepoint()):
self.cron.write({
'active': True,
'doall': setup.doall,
'numbercall': setup.numbercall,
'nextcall': fields.Datetime.now() - timedelta(days=setup.missedcall - 1),
'failure_count': curr_failures,
'first_failure_date': ten_days_ago if curr_failures else None
})
with self.capture_triggers(self.cron.id) as capture:
if setup.trigger:
if trigger:
self.cron._trigger()
self.cron.flush_recordset()
capture.records.flush_recordset()
self.env.flush_all()
self.registry.enter_test_mode(self.cr)
cb, state = cb(self.cron)
try:
with patch.object(self.registry['ir.cron'], '_callback') as callback:
with mute_logger('odoo.addons.base.models.ir_cron'),\
patch.object(self.registry['ir.actions.server'], 'run', cb):
self.registry['ir.cron']._process_job(
self.registry.db_name,
self.registry.cursor(),
self.cron.read(load=None)[0]
{**self.cron.read(load=None)[0], **default_progress_values}
)
finally:
self.registry.leave_test_mode()
self.cron.invalidate_recordset()
capture.records.invalidate_recordset()
self.assertEqual(callback.call_count, expect.call_count)
self.assertEqual(self.cron.numbercall, expect.call_left)
self.assertEqual(self.cron.active, expect.active)
self.assertEqual(self.cron.lastcall, fields.Datetime.now())
self.assertEqual(self.cron.nextcall, fields.Datetime.now() + timedelta(days=1))
self.assertEqual(self.env['ir.cron.trigger'].search_count([
('cron_id', '=', self.cron.id),
('call_at', '<=', fields.Datetime.now())]
), 0)
self.assertEqual(self.cron.id in [job['id'] for job in self.cron._get_all_ready_jobs(self.env.cr)], trigger)
self.assertEqual(state['call_count'], call_count)
self.assertEqual(Progress.search_count([('cron_id', '=', self.cron.id), ('done', '=', 1)]), done_count)
self.assertEqual(self.cron.failure_count, fail_count)
self.assertEqual(self.cron.active, active)
def test_cron_null_interval(self):
self.cron.interval_number = 0
self.cron.flush_recordset()
with self.assertLogs('odoo.addons.base.models.ir_cron', 'ERROR'):
self.cron._process_job(get_db_name(), self.env.cr, self.cron.read(load=False)[0])
self.cron.invalidate_recordset(['active'])
def test_cron_retrigger(self):
Trigger = self.env['ir.cron.trigger']
Progress = self.env['ir.cron.progress']
default_progress_values = {'done': 0, 'remaining': 0, 'timed_out_counter': 0}
def make_run(cron):
state = {'call_count': 0}
CALL_TARGET = 11
def run(self):
state['call_count'] += 1
self.env['ir.cron']._notify_progress(done=1, remaining=CALL_TARGET - state['call_count'])
return run, state
self.cron._trigger()
self.env.flush_all()
self.registry.enter_test_mode(self.cr)
mocked_run, mocked_run_state = make_run(self.cron)
try:
with patch.object(self.registry['ir.actions.server'], 'run', mocked_run):
self.registry['ir.cron']._process_job(
self.registry.db_name,
self.registry.cursor(),
{**self.cron.read(load=None)[0], **default_progress_values}
)
finally:
self.registry.leave_test_mode()
self.assertEqual(
mocked_run_state['call_count'], 10,
'`run` should have been called ten times',
)
self.assertEqual(
Progress.search_count([('done', '=', 1), ('cron_id', '=', self.cron.id)]), 10,
'There should be 10 progress log for this cron',
)
self.assertEqual(
Trigger.search_count([('cron_id', '=', self.cron.id)]), 1,
"One trigger should have been kept",
)
self.env.flush_all()
self.registry.enter_test_mode(self.cr)
try:
with patch.object(self.registry['ir.actions.server'], 'run', mocked_run):
self.registry['ir.cron']._process_job(
self.registry.db_name,
self.registry.cursor(),
{**self.cron.read(load=None)[0], **default_progress_values}
)
finally:
self.registry.leave_test_mode()
ready_jobs = self.registry['ir.cron']._get_all_ready_jobs(self.cr)
self.assertNotIn(
self.cron.id, [job['id'] for job in ready_jobs],
'The cron has finished executing'
)
self.assertEqual(
mocked_run_state['call_count'], 10 + 1,
'`run` should have been called one additional time',
)
self.assertEqual(
Progress.search_count([('done', '=', 1), ('cron_id', '=', self.cron.id)]), 11,
'There should be 11 progress log for this cron',
)
def test_cron_failed_increase(self):
self.cron._trigger()
self.env.flush_all()
self.registry.enter_test_mode(self.cr)
default_progress = {'done': 0, 'remaining': 0, 'timed_out_counter': 0}
try:
with (
patch.object(self.registry['ir.cron'], '_callback', side_effect=Exception),
patch.object(self.registry['ir.cron'], '_notify_admin') as notify,
):
self.registry['ir.cron']._process_job(
self.registry.db_name,
self.registry.cursor(),
{**self.cron.read(load=None)[0], **default_progress}
)
finally:
self.registry.leave_test_mode()
self.env.invalidate_all()
self.assertEqual(self.cron.failure_count, 1, 'The cron should have failed once')
self.assertEqual(self.cron.active, True, 'The cron should still be active')
self.assertFalse(notify.called)
self.cron.failure_count = 4
self.cron._trigger()
self.env.flush_all()
self.registry.enter_test_mode(self.cr)
try:
with (
patch.object(self.registry['ir.cron'], '_callback', side_effect=Exception),
patch.object(self.registry['ir.cron'], '_notify_admin') as notify,
):
self.registry['ir.cron']._process_job(
self.registry.db_name,
self.registry.cursor(),
{**self.cron.read(load=None)[0], **default_progress}
)
finally:
self.registry.leave_test_mode()
self.env.invalidate_all()
self.assertEqual(self.cron.failure_count, 5, 'The cron should have failed one more time but not reset (due to time)')
self.assertEqual(self.cron.active, True, 'The cron should not have been deactivated due to time constraint')
self.assertFalse(notify.called)
self.cron.failure_count = 4
self.cron.first_failure_date = fields.Datetime.now() - timedelta(days=8)
self.cron._trigger()
self.env.flush_all()
self.registry.enter_test_mode(self.cr)
try:
with (
patch.object(self.registry['ir.cron'], '_callback', side_effect=Exception),
patch.object(self.registry['ir.cron'], '_notify_admin') as notify,
):
self.registry['ir.cron']._process_job(
self.registry.db_name,
self.registry.cursor(),
{**self.cron.read(load=None)[0], **default_progress}
)
finally:
self.registry.leave_test_mode()
self.env.invalidate_all()
self.assertEqual(self.cron.failure_count, 0, 'The cron should have failed one more time and reset to 0')
self.assertEqual(self.cron.active, False, 'The cron should have been deactivated after 5 failures')
self.assertTrue(notify.called)
def test_cron_timeout_failure(self):
self.cron._trigger()
progress = self.env['ir.cron.progress'].create([{
'cron_id': self.cron.id,
'remaining': 0,
'done': 0,
'timed_out_counter': 3,
}])
self.env.flush_all()
self.registry.enter_test_mode(self.cr)
try:
with mute_logger('odoo.addons.base.models.ir_cron'):
self.registry['ir.cron']._process_job(
self.registry.db_name,
self.registry.cursor(),
{**progress.read(fields=['done', 'remaining', 'timed_out_counter'], load=None)[0], 'progress_id': progress.id, **self.cron.read(load=None)[0]}
)
finally:
self.registry.leave_test_mode()
self.env.invalidate_all()
self.assertEqual(self.cron.failure_count, 1, 'The cron should have failed once')
self.assertEqual(self.cron.active, True, 'The cron should still be active')
self.cron._trigger()
self.registry.enter_test_mode(self.cr)
try:
self.registry['ir.cron']._process_job(
self.registry.db_name,
self.registry.cursor(),
{**progress.read(fields=['done', 'remaining', 'timed_out_counter'], load=None)[0], 'progress_id': progress.id, **self.cron.read(load=None)[0]}
)
finally:
self.registry.leave_test_mode()
self.env.invalidate_all()
self.assertEqual(self.cron.failure_count, 0, 'The cron should have succeeded and reset the counter')
def test_cron_timeout_success(self):
self.cron._trigger()
progress = self.env['ir.cron.progress'].create([{
'cron_id': self.cron.id,
'remaining': 0,
'done': 0,
'timed_out_counter': 3,
}])
self.env.flush_all()
self.registry.enter_test_mode(self.cr)
try:
with mute_logger('odoo.addons.base.models.ir_cron'):
self.registry['ir.cron']._process_job(
self.registry.db_name,
self.registry.cursor(),
{**progress.read(fields=['done', 'remaining', 'timed_out_counter'], load=None)[0], 'progress_id': progress.id, **self.cron.read(load=None)[0]}
)
finally:
self.registry.leave_test_mode()
self.env.invalidate_all()
self.assertEqual(self.cron.failure_count, 1, 'The cron should have failed once')
self.assertEqual(self.cron.active, True, 'The cron should still be active')
self.cron._trigger()
self.registry.enter_test_mode(self.cr)
try:
self.registry['ir.cron']._process_job(
self.registry.db_name,
self.registry.cursor(),
{**progress.read(fields=['done', 'remaining', 'timed_out_counter'], load=None)[0], 'progress_id': progress.id, **self.cron.read(load=None)[0]}
)
finally:
self.registry.leave_test_mode()
self.env.invalidate_all()
self.assertEqual(self.cron.failure_count, 0, 'The cron should have succeeded and reset the counter')
def test_acquire_processed_job(self):
# Test acquire job on already processed jobs
job = self.env['ir.cron']._acquire_one_job(self.cr, self.cron.id)
self.assertEqual(job, None, "No error should be thrown, job should just be none")
def test_cron_deactivate(self):
default_progress_values = {'done': 0, 'remaining': 0, 'timed_out_counter': 0}
def mocked_run(self):
self.env['ir.cron']._notify_progress(done=1, remaining=0, deactivate=True)
self.cron._trigger()
self.env.flush_all()
self.registry.enter_test_mode(self.cr)
try:
with patch.object(self.registry['ir.actions.server'], 'run', mocked_run):
self.registry['ir.cron']._process_job(
self.registry.db_name,
self.registry.cursor(),
{**self.cron.read(load=None)[0], **default_progress_values}
)
finally:
self.registry.leave_test_mode()
self.env.invalidate_all()
self.assertFalse(self.cron.active)
@tagged('-standard', '-at_install', 'post_install', 'database_breaking')
class TestIrCronConcurrent(BaseCase, CronMixinCase):
@classmethod
def setUpClass(cls):
super().setUpClass()
# Keep a reference on the real cron methods, those without patch
cls.registry = odoo.registry(get_db_name())
cls.cron_process_job = cls.registry['ir.cron']._process_job
cls.cron_process_jobs = cls.registry['ir.cron']._process_jobs
cls.cron_get_all_ready_jobs = cls.registry['ir.cron']._get_all_ready_jobs
cls.cron_acquire_one_job = cls.registry['ir.cron']._acquire_one_job
cls.cron_callback = cls.registry['ir.cron']._callback
def setUp(self):
super().setUp()
with self.registry.cursor() as cr:
env = api.Environment(cr, odoo.SUPERUSER_ID, {})
env['ir.cron'].search([]).unlink()
env['ir.cron.trigger'].search([]).unlink()
self.cron1_data = env['ir.cron'].create(self._get_cron_data(env, priority=1)).read(load=None)[0]
self.cron2_data = env['ir.cron'].create(self._get_cron_data(env, priority=2)).read(load=None)[0]
self.partner_data = env['res.partner'].create(self._get_partner_data(env)).read(load=None)[0]
self.cron_ids = [self.cron1_data['id'], self.cron2_data['id']]
def test_cron_concurrency_1(self):
"""
Two cron threads "th1" and "th2" wake up at the same time and
see two jobs "job1" and "job2" that are ready (setup).
Th1 acquire job1, before it can process and release its job, th2
acquire a job too (setup). Th2 shouldn't be able to acquire job1
as another thread is processing it, it should skips job1 and
should acquire job2 instead (test). Both thread then process
their job, update its `nextcall` and release it (setup).
All the threads update and release their job before any thread
attempt to acquire another job. (setup)
The two thread each attempt to acquire a new job (setup), they
should both fail to acquire any as each job's nextcall is in the
future* (test).
*actually, in their own transaction, the other job's nextcall is
still "in the past" but any attempt to use that information
would result in a serialization error. This tests ensure that
that serialization error is correctly handled and ignored.
"""
lock = threading.Lock()
barrier = threading.Barrier(2)
###
# Setup
###
# Watchdog, if a thread was waiting at the barrier when the
# other exited, it receives a BrokenBarrierError and exits too.
def process_jobs(*args, **kwargs):
try:
self.cron_process_jobs(*args, **kwargs)
finally:
barrier.reset()
# The two threads get the same list of jobs
def get_all_ready_jobs(*args, **kwargs):
jobs = self.cron_get_all_ready_jobs(*args, **kwargs)
barrier.wait()
return jobs
# When a thread acquire a job, it processes it till the end
# before another thread can acquire one.
def acquire_one_job(*args, **kwargs):
lock.acquire(timeout=1)
try:
with mute_logger('odoo.sql_db'):
job = self.cron_acquire_one_job(*args, **kwargs)
except Exception:
lock.release()
raise
if not job:
lock.release()
return job
# When a thread is done processing its job, it waits for the
# other thread to catch up.
def process_job(*args, **kwargs):
try:
return_value = self.cron_process_job(*args, **kwargs)
finally:
lock.release()
barrier.wait(timeout=1)
return return_value
# Set 2 jobs ready, process them in 2 different threads.
with self.registry.cursor() as cr:
env = api.Environment(cr, odoo.SUPERUSER_ID, {})
env['ir.cron'].browse(self.cron_ids).write({
'nextcall': fields.Datetime.now() - timedelta(hours=1)
})
###
# Run
###
with patch.object(self.registry['ir.cron'], '_process_jobs', process_jobs), \
patch.object(self.registry['ir.cron'], '_get_all_ready_jobs', get_all_ready_jobs), \
patch.object(self.registry['ir.cron'], '_acquire_one_job', acquire_one_job), \
patch.object(self.registry['ir.cron'], '_process_job', process_job), \
patch.object(self.registry['ir.cron'], '_callback') as callback, \
ThreadPoolExecutor(max_workers=2) as executor:
fut1 = executor.submit(self.registry['ir.cron']._process_jobs, self.registry.db_name)
fut2 = executor.submit(self.registry['ir.cron']._process_jobs, self.registry.db_name)
fut1.result(timeout=2)
fut2.result(timeout=2)
###
# Validation
###
self.assertEqual(len(callback.call_args_list), 2, 'Two jobs must have been processed.')
self.assertEqual(callback.call_args_list, [
call(
self.cron1_data['name'],
self.cron1_data['ir_actions_server_id'],
self.cron1_data['id'],
),
call(
self.cron2_data['name'],
self.cron2_data['ir_actions_server_id'],
self.cron2_data['id'],
),
])

View file

@ -89,7 +89,7 @@ class TestIrDefault(TransactionCase):
with self.assertRaises(ValidationError):
IrDefault.set('res.partner', 'unknown_field', 42)
with self.assertRaises(ValidationError):
IrDefault.set('res.partner', 'lang', 'some_LANG')
IrDefault.set('res.partner', 'type', 'invalid_type')
with self.assertRaises(ValidationError):
IrDefault.set('res.partner', 'partner_latitude', 'foo')
with self.assertRaises(ValidationError):

View file

@ -0,0 +1,145 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo.exceptions import UserError
from odoo.addons.base.tests.common import TransactionCaseWithUserDemo
class TestEmbeddedActionsBase(TransactionCaseWithUserDemo):
@classmethod
def setUpClass(cls):
super().setUpClass()
cls.test_partner = cls.env['res.partner'].create({
'city': 'OrigCity',
'email': 'test.partner@test.example.com',
'name': 'TestingPartner',
'employee': True,
})
cls.context = {
'active_model': 'res.partner',
'active_id': cls.test_partner.id,
}
# create parent action
cls.parent_action = cls.env['ir.actions.act_window'].create({
'name': 'ParentAction',
'res_model': 'res.partner',
})
# create actions
cls.action_1 = cls.env['ir.actions.act_window'].create({
'name': 'Action1',
'res_model': 'res.partner',
})
# create actions
cls.action_2 = cls.env['ir.actions.act_window'].create({
'name': 'Action2',
'res_model': 'res.partner',
})
# create embedded actions
cls.embedded_action_1 = cls.env['ir.embedded.actions'].create({
'name': 'EmbeddedAction1',
'parent_res_model': 'res.partner',
'parent_action_id': cls.parent_action.id,
'action_id': cls.action_1.id,
})
# create embedded actions
cls.embedded_action_2 = cls.env['ir.embedded.actions'].create({
'name': 'EmbeddedAction1',
'parent_res_model': 'res.partner',
'parent_action_id': cls.parent_action.id,
'action_id': cls.action_2.id,
})
def get_embedded_actions_ids(self, parent_action):
return parent_action.with_context(self.context).read()[0]['embedded_action_ids']
def test_parent_has_embedded_actions(self):
res = self.get_embedded_actions_ids(self.parent_action)
self.assertEqual(len(res), 2, "There should be 2 embedded records linked to the parent action")
self.assertTrue(self.embedded_action_1.id in res and self.embedded_action_2.id in res, "The correct embedded actions\
should be in embedded_actions")
def test_cannot_delete_default_embedded_action(self):
return
def test_can_delete_custom_embedded_action(self):
embedded_action_custo = self.env['ir.embedded.actions'].create({
'name': 'EmbeddedActionCusto',
'parent_res_model': 'res.partner',
'parent_action_id': self.parent_action.id,
'action_id': self.action_2.id,
})
try:
embedded_action_custo.unlink()
except UserError:
self.assertTrue(False)
def test_domain_on_embedded_action(self):
test_partner_custo = self.env['res.partner'].create({
'city': 'CustoCity',
'email': 'test.partner@test.example.com',
'name': 'CustomPartner',
'employee': False,
})
self.context = {
'active_model': 'res.partner',
'active_id': test_partner_custo.id,
}
embedded_action_custo = self.env['ir.embedded.actions'].create({
'name': 'EmbeddedActionCusto',
'parent_res_model': 'res.partner',
'parent_action_id': self.parent_action.id,
'action_id': self.action_2.id,
'domain': [('employee', '=', True)]
})
res = self.get_embedded_actions_ids(self.parent_action)
self.assertTrue(embedded_action_custo.id not in res, "The embedded action not respecting the domain should\
not be returned in the read method")
def test_groups_on_embedded_action(self):
arbitrary_group = self.env['res.groups'].create({
'name': 'arbitrary_group',
'implied_ids': [(6, 0, [self.ref('base.group_user')])],
})
embedded_action_custo = self.env['ir.embedded.actions'].create({
'name': 'EmbeddedActionCusto',
'parent_res_model': 'res.partner',
'parent_action_id': self.parent_action.id,
'action_id': self.action_2.id,
'groups_ids': [(6, 0, [arbitrary_group.id])]
})
res = self.get_embedded_actions_ids(self.parent_action)
self.assertEqual(len(res), 2, "There should be 2 embedded records linked to the parent action")
self.assertTrue(self.embedded_action_1.id in res and self.embedded_action_2.id in res, "The correct embedded actions\
should be in embedded_actions")
self.env.user.write({'groups_id': [(4, arbitrary_group.id)]})
res = self.get_embedded_actions_ids(self.parent_action)
self.assertEqual(len(res), 3, "There should be 3 embedded records linked to the parent action")
self.assertTrue(self.embedded_action_1.id in res and self.embedded_action_2.id in res and embedded_action_custo.id in res, "The correct embedded actions\
should be in embedded_actions")
def test_create_embedded_action_with_action_and_python_method(self):
embedded_action1, embedded_action2 = self.env['ir.embedded.actions'].create([
{
'name': 'EmbeddedActionCustom',
'action_id': self.action_2.id,
'parent_action_id': self.parent_action.id,
'parent_res_model': 'res.partner',
'python_method': "action_python_method",
},
{
'name': 'EmbeddedActionCustom2',
'action_id': self.action_2.id,
'python_method': "",
'parent_action_id': self.parent_action.id,
'parent_res_model': 'res.partner',
}
])
self.assertEqual(embedded_action1.python_method, "action_python_method")
self.assertFalse(embedded_action1.action_id)
self.assertEqual(embedded_action2.action_id, self.env['ir.actions.actions'].browse(self.action_2.id))
self.assertFalse(embedded_action2.python_method)

View file

@ -15,9 +15,10 @@ def noid(seq):
for d in seq:
d.pop('id', None)
d.pop('action_id', None)
d.pop('embedded_action_id', None)
d.pop('embedded_parent_res_id', None)
return seq
class FiltersCase(TransactionCaseWithUserDemo):
def setUp(self):
super(FiltersCase, self).setUp()
@ -26,8 +27,7 @@ class FiltersCase(TransactionCaseWithUserDemo):
def build(self, model, *args):
Model = self.env[model].with_user(ADMIN_USER_ID)
for vals in args:
Model.create(vals)
Model.create(args)
class TestGetFilters(FiltersCase):
@ -101,7 +101,7 @@ class TestOwnDefaults(FiltersCase):
self.assertItemsEqual(noid(filters), [
dict(name='a', user_id=self.USER_NG, is_default=True,
domain='[]', context='{}', sort='[]')
domain='[]', context='{}', sort='[]'),
])
def test_new_filter_not_default(self):
@ -277,25 +277,6 @@ class TestGlobalDefaults(FiltersCase):
])
class TestReadGroup(TransactionCase):
"""Test function read_group with groupby on a many2one field to a model
(in test, "user_id" to "res.users") which is ordered by an inherited not stored field (in
test, "name" inherited from "res.partners").
"""
def test_read_group_1(self):
Users = self.env['res.users']
self.assertEqual(Users._order, "name, login", "Model res.users must be ordered by name, login")
self.assertFalse(Users._fields['name'].store, "Field name is not stored in res.users")
Filters = self.env['ir.filters']
filter_a = Filters.create(dict(name="Filter_A", model_id="ir.filters"))
filter_b = Filters.create(dict(name="Filter_B", model_id="ir.filters"))
filter_b.write(dict(user_id=False))
res = Filters.read_group([], ['name', 'user_id'], ['user_id'])
self.assertTrue(any(val['user_id'] == False for val in res), "At least one group must contain val['user_id'] == False.")
@tagged('post_install', '-at_install', 'migration')
class TestAllFilters(TransactionCase):
def check_filter(self, name, model, domain, fields, groupby, order, context):
@ -328,3 +309,89 @@ class TestAllFilters(TransactionCase):
order=','.join(ast.literal_eval(filter_.sort)),
context=context,
)
class TestEmbeddedFilters(FiltersCase):
def setUp(self):
super(FiltersCase, self).setUp()
self.USER_NG = self.env['res.users'].name_search('demo')[0]
self.USER_ID = self.USER_NG[0]
self.parent_action = self.env['ir.actions.act_window'].create({
'name': 'ParentAction',
'res_model': 'res.partner',
})
self.action_1 = self.env['ir.actions.act_window'].create({
'name': 'Action1',
'res_model': 'res.partner',
})
self.embedded_action_1 = self.env['ir.embedded.actions'].create({
'name': 'EmbeddedAction1',
'parent_res_model': 'res.partner',
'parent_action_id': self.parent_action.id,
'action_id': self.action_1.id,
})
self.embedded_action_2 = self.env['ir.embedded.actions'].create({
'name': 'EmbeddedAction2',
'parent_res_model': 'res.partner',
'parent_action_id': self.parent_action.id,
'action_id': self.action_1.id,
})
def test_global_filters_with_embedded_action(self):
Filters = self.env['ir.filters'].with_user(self.USER_ID)
Filters.create_or_replace({
'name': 'a',
'model_id': 'ir.filters',
'user_id': False,
'is_default': True,
'embedded_action_id': self.embedded_action_1.id,
'embedded_parent_res_id': 1
})
Filters.create_or_replace({
'name': 'b',
'model_id': 'ir.filters',
'user_id': self.USER_ID,
'is_default': False,
'embedded_action_id': self.embedded_action_2.id,
'embedded_parent_res_id': 1
})
# If embedded_action_id and embedded_parent_res_id are set, should return the corresponding filter
filters = self.env['ir.filters'].with_user(self.USER_ID).get_filters('ir.filters', embedded_action_id=self.embedded_action_1.id, embedded_parent_res_id=1)
self.assertItemsEqual(noid(filters), [dict(name='a', is_default=True, user_id=False, domain='[]', context='{}', sort='[]')])
# Check that the filter is correctly linked to one embedded_parent_res_id and is not returned if another one is set
filters = self.env['ir.filters'].with_user(self.USER_ID).get_filters('ir.filters', embedded_action_id=self.embedded_action_1.id, embedded_parent_res_id=2)
self.assertItemsEqual(noid(filters), [])
# Check that a shared filter can be fetched with another user
filters = self.env['ir.filters'].with_user(ADMIN_USER_ID).get_filters('ir.filters', embedded_action_id=self.embedded_action_1.id, embedded_parent_res_id=1)
self.assertItemsEqual(noid(filters), [dict(name='a', is_default=True, user_id=False, domain='[]', context='{}', sort='[]')])
# If embedded_action_id and embedded_parent_res_id are not set, should return no filters
filters = self.env['ir.filters'].with_user(self.USER_ID).get_filters('ir.filters')
self.assertItemsEqual(noid(filters), [])
def test_global_filters_with_no_embedded_action(self):
Filters = self.env['ir.filters'].with_user(self.USER_ID)
filter_a = Filters.create_or_replace({
'name': 'a',
'model_id': 'ir.filters',
'user_id': False,
'is_default': True,
'embedded_action_id': False,
'embedded_parent_res_id': 0,
})
filter_b = Filters.create_or_replace({
'name': 'b',
'model_id': 'ir.filters',
'user_id': self.USER_ID,
'is_default': True,
'embedded_action_id': False,
'embedded_parent_res_id': 1,
})
self.assertFalse(filter_a.embedded_action_id)
self.assertFalse(filter_a.embedded_parent_res_id)
self.assertFalse(filter_b.embedded_action_id)
self.assertFalse(filter_b.embedded_parent_res_id)

View file

@ -6,6 +6,8 @@ import email.policy
from unittest.mock import patch
import psycopg2.errors
from odoo import tools
from odoo.addons.base.tests import test_mail_examples
from odoo.addons.base.tests.common import MockSmtplibCase
@ -124,18 +126,27 @@ class TestIrMailServer(TransactionCase, MockSmtplibCase):
subject='Subject',
subtype='html',
)
body_alternative = False
body_alternative = None
for part in message.walk():
if part.get_content_maintype() == 'multipart':
continue # skip container
if part.get_content_type() == 'text/plain':
if not part.get_payload():
continue
body_alternative = tools.ustr(part.get_content())
# remove ending new lines as it just adds noise
body_alternative = body_alternative.strip('\n')
body_alternative = part.get_content().rstrip('\n')
self.assertEqual(body_alternative, expected)
@mute_logger('odoo.sql_db')
def test_mail_server_auth_cert_requires_tls(self):
with self.assertRaises(psycopg2.errors.CheckViolation):
self.env['ir.mail_server'].create({
'name': 'test',
'smtp_host': 'smtp_host',
'smtp_encryption': 'none',
'smtp_authentication': 'certificate',
})
@users('admin')
def test_mail_server_get_test_email_from(self):
""" Test the email used to test the mail server connection. Check

View file

@ -11,8 +11,9 @@ from pathlib import Path
from unittest.mock import patch
from socket import getaddrinfo # keep a reference on the non-patched function
from odoo import modules
from odoo.exceptions import UserError
from odoo.tools import file_path, mute_logger
from odoo.tools import config, file_path, mute_logger
from .common import TransactionCaseWithUserDemo
try:
@ -62,7 +63,7 @@ class Certificate:
# fail fast for timeout errors
@patch('odoo.addons.base.models.ir_mail_server.SMTP_TIMEOUT', .1)
# prevent the CLI from interfering with the tests
@patch.dict('odoo.tools.config.options', {'smtp_server': ''})
@patch.dict(config.options, {'smtp_server': ''})
class TestIrMailServerSMTPD(TransactionCaseWithUserDemo):
@classmethod
def setUpClass(cls):
@ -134,14 +135,6 @@ class TestIrMailServerSMTPD(TransactionCaseWithUserDemo):
patcher.start()
cls.addClassCleanup(patcher.stop)
# reactivate sending emails during this test suite, make sure
# NOT TO send emails using another ir.mail_server than the one
# created in setUp!
patcher = patch.object(cls.registry['ir.mail_server'], '_is_test_mode')
mock = patcher.start()
mock.return_value = False
cls.addClassCleanup(patcher.stop)
# fix runbot, docker uses a single ipv4 stack but it gives ::1
# when resolving "localhost" (so stupid), use the following to
# force aiosmtpd/odoo to bind/connect to a fixed ipv4 OR ipv6
@ -150,6 +143,15 @@ class TestIrMailServerSMTPD(TransactionCaseWithUserDemo):
cls.localhost = getaddrinfo('localhost', cls.port, family)
cls.startClassPatcher(patch('socket.getaddrinfo', cls.getaddrinfo))
def setUp(self):
super().setUp()
# reactivate sending emails during this test suite, make sure
# NOT TO send emails using another ir.mail_server than the one
# created in setUp!
patcher = patch.object(modules.module, 'current_test', False)
patcher.start()
self.addCleanup(patcher.stop)
@classmethod
def getaddrinfo(cls, host, port, *args, **kwargs):
"""

View file

@ -5,7 +5,7 @@ from psycopg2 import IntegrityError
from psycopg2.errors import NotNullViolation
from odoo.exceptions import ValidationError
from odoo.tests.common import Form, TransactionCase, HttpCase, tagged
from odoo.tests import Form, TransactionCase, HttpCase, tagged
from odoo.tools import mute_logger
from odoo import Command
@ -528,10 +528,10 @@ class TestIrModelFieldsTranslation(HttpCase):
field = self.env['ir.model.fields'].search([('model_id.model', '=', 'res.users'), ('name', '=', 'login')])
self.assertEqual(field.with_context(lang='en_US').field_description, 'Login')
# check the name column of res.users is displayed as 'Login'
self.start_tour("/web", 'ir_model_fields_translation_en_tour', login="admin")
self.start_tour("/odoo", 'ir_model_fields_translation_en_tour', login="admin")
field.update_field_translations('field_description', {'en_US': 'Login2'})
# check the name column of res.users is displayed as 'Login2'
self.start_tour("/web", 'ir_model_fields_translation_en_tour2', login="admin")
self.start_tour("/odoo", 'ir_model_fields_translation_en_tour2', login="admin")
# modify fr_FR translation
self.env['res.lang']._activate_lang('fr_FR')
@ -541,7 +541,31 @@ class TestIrModelFieldsTranslation(HttpCase):
admin = self.env['res.users'].search([('login', '=', 'admin')], limit=1)
admin.lang = 'fr_FR'
# check the name column of res.users is displayed as 'Identifiant'
self.start_tour("/web", 'ir_model_fields_translation_fr_tour', login="admin")
self.start_tour("/odoo", 'ir_model_fields_translation_fr_tour', login="admin")
field.update_field_translations('field_description', {'fr_FR': 'Identifiant2'})
# check the name column of res.users is displayed as 'Identifiant2'
self.start_tour("/web", 'ir_model_fields_translation_fr_tour2', login="admin")
self.start_tour("/odoo", 'ir_model_fields_translation_fr_tour2', login="admin")
class TestIrModelInherit(TransactionCase):
def test_inherit(self):
imi = self.env["ir.model.inherit"].search([("model_id.model", "=", "ir.actions.server")])
self.assertEqual(len(imi), 1)
self.assertEqual(imi.parent_id.model, "ir.actions.actions")
self.assertFalse(imi.parent_field_id)
def test_inherits(self):
imi = self.env["ir.model.inherit"].search(
[("model_id.model", "=", "res.users"), ("parent_field_id", "!=", False)]
)
self.assertEqual(len(imi), 1)
self.assertEqual(imi.parent_id.model, "res.partner")
self.assertEqual(imi.parent_field_id.name, "partner_id")
def test_delegate_field(self):
imi = self.env["ir.model.inherit"].search(
[("model_id.model", "=", "ir.cron"), ("parent_field_id", "!=", False)]
)
self.assertEqual(len(imi), 1)
self.assertEqual(imi.parent_id.model, "ir.actions.server")
self.assertEqual(imi.parent_field_id.name, "ir_actions_server_id")

View file

@ -4,22 +4,24 @@
from contextlib import contextmanager
import psycopg2
import psycopg2.errorcodes
import psycopg2.errors
import odoo
from odoo.exceptions import UserError
from odoo.modules.registry import Registry
from odoo.tests import common
from odoo.tests.common import BaseCase
from odoo.tools.misc import mute_logger
ADMIN_USER_ID = common.ADMIN_USER_ID
@contextmanager
def environment():
""" Return an environment with a new cursor for the current database; the
cursor is committed and closed after the context block.
"""
registry = odoo.registry(common.get_db_name())
registry = Registry(common.get_db_name())
with registry.cursor() as cr:
yield odoo.api.Environment(cr, ADMIN_USER_ID, {})
@ -92,15 +94,13 @@ class TestIrSequenceNoGap(BaseCase):
""" Try to draw a number from two transactions.
This is expected to not work.
"""
with environment() as env0:
with environment() as env1:
# NOTE: The error has to be an OperationalError
# s.t. the automatic request retry (service/model.py) works.
with self.assertRaises(psycopg2.OperationalError) as e:
n0 = env0['ir.sequence'].next_by_code('test_sequence_type_2')
self.assertTrue(n0)
n1 = env1['ir.sequence'].next_by_code('test_sequence_type_2')
self.assertEqual(e.exception.pgcode, psycopg2.errorcodes.LOCK_NOT_AVAILABLE, msg="postgresql returned an incorrect errcode")
with environment() as env0, environment() as env1:
# NOTE: The error has to be an OperationalError
# s.t. the automatic request retry (service/model.py) works.
with self.assertRaises(psycopg2.errors.LockNotAvailable, msg="postgresql returned an incorrect errcode"):
n0 = env0['ir.sequence'].next_by_code('test_sequence_type_2')
self.assertTrue(n0)
env1['ir.sequence'].next_by_code('test_sequence_type_2')
@classmethod
def tearDownClass(cls):

View file

@ -9,12 +9,14 @@ from odoo.addons.base.models.ir_mail_server import extract_rfc2822_addresses
from odoo.addons.base.models.ir_qweb_fields import nl2br_enclose
from odoo.tests import tagged
from odoo.tests.common import BaseCase
from odoo.tools import (
is_html_empty, html_to_inner_content, html_sanitize, append_content_to_html, plaintext2html,
from odoo.tools import misc
from odoo.tools.mail import (
is_html_empty, html2plaintext, html_to_inner_content, html_sanitize, append_content_to_html, plaintext2html,
email_domain_normalize, email_normalize, email_re,
email_split, email_split_and_format, email_split_tuples,
single_email_re, html2plaintext,
misc, formataddr, email_anonymize,
single_email_re,
formataddr,
email_anonymize,
prepend_html_content,
)
@ -312,6 +314,28 @@ class TestSanitizer(BaseCase):
for text in in_lst:
self.assertIn(text, new_html)
def test_quote_signature_container_propagation(self):
"""Test that applying normalization twice doesn't quote more than wanted."""
# quote signature with bare signature in main block
bare_signature_body = (
"<div>"
"<div><p>Hello</p><p>Here is your document</p></div>"
"<div>--<br>Mark Demo</div>"
"<div class=\"bg-300\"></div>"
"</div>"
)
expected_result = (
"<div data-o-mail-quote-container=\"1\">"
"<div><p>Hello</p><p>Here is your document</p></div>"
"<div data-o-mail-quote=\"1\">--<br data-o-mail-quote=\"1\">Mark Demo</div>"
"<div class=\"bg-300\" data-o-mail-quote=\"1\"></div>"
"</div>"
)
sanitized_once = html_sanitize(bare_signature_body)
sanitized_twice = html_sanitize(sanitized_once)
self.assertEqual(sanitized_once, expected_result)
self.assertEqual(sanitized_twice, expected_result)
def test_quote_gmail(self):
html = html_sanitize(test_mail_examples.GMAIL_1)
for ext in test_mail_examples.GMAIL_1_IN:
@ -886,7 +910,7 @@ class TestMailTools(BaseCase):
""" Test mail utility methods. """
def test_html2plaintext(self):
self.assertEqual(html2plaintext(False), 'False')
self.assertEqual(html2plaintext(False), '')
self.assertEqual(html2plaintext('\t'), '')
self.assertEqual(html2plaintext(' '), '')
self.assertEqual(html2plaintext("""<h1>Title</h1>

View file

@ -1,5 +1,4 @@
import base64
import unittest
try:
import magic
@ -7,7 +6,7 @@ except ImportError:
magic = None
from odoo.tests.common import BaseCase
from odoo.tools.mimetypes import get_extension, guess_mimetype
from odoo.tools.mimetypes import fix_filename_extension, get_extension, guess_mimetype
PNG = b'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAIAAACQd1PeAAAADElEQVQI12P4//8/AAX+Av7czFnnAAAAAElFTkSuQmCC'
GIF = b"R0lGODdhAQABAIAAAP///////ywAAAAAAQABAAACAkQBADs="
@ -59,6 +58,10 @@ XML = b"""<?xml version='1.0' encoding='utf-8'?>
</Document>
"""
TXT = b"""\
Hello world!
"""
class test_guess_mimetype(BaseCase):
def test_default_mimetype_empty(self):
@ -121,14 +124,18 @@ class test_guess_mimetype(BaseCase):
self.assertEqual(mimetype, 'application/zip')
def test_mimetype_xml(self):
expected_mimetype = 'application/xml' if magic is None else 'text/xml'
mimetype = guess_mimetype(XML, default='test')
self.assertEqual(mimetype, expected_mimetype)
self.assertIn(mimetype, ('application/xml', 'text/xml'))
def test_mimetype_txt(self):
mimetype = guess_mimetype(TXT, default='test')
self.assertEqual(mimetype, 'text/plain')
def test_mimetype_get_extension(self):
self.assertEqual(get_extension('filename.Abc'), '.abc')
self.assertEqual(get_extension('filename.scss'), '.scss')
self.assertEqual(get_extension('filename.torrent'), '.torrent')
self.assertEqual(get_extension('filename.ab_c'), '.ab_c')
self.assertEqual(get_extension('.htaccess'), '')
# enough to suppose that extension is present and don't suffix the filename
self.assertEqual(get_extension('filename.tar.gz'), '.gz')
@ -137,3 +144,23 @@ class test_guess_mimetype(BaseCase):
self.assertEqual(get_extension('filename.not_alnum'), '')
self.assertEqual(get_extension('filename.with space'), '')
self.assertEqual(get_extension('filename.notAnExtension'), '')
def test_mimetype_fix_extension(self):
fix = fix_filename_extension
self.assertEqual(fix('words.txt', 'text/plain'), 'words.txt')
self.assertEqual(fix('image.jpg', 'image/jpeg'), 'image.jpg')
self.assertEqual(fix('image.jpeg', 'image/jpeg'), 'image.jpeg')
self.assertEqual(fix('sheet.xls', 'application/vnd.ms-excel'), 'sheet.xls')
self.assertEqual(fix('sheet.xls', 'application/CDFV2'), 'sheet.xls')
xlsx_mime = 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
self.assertEqual(fix('sheet.xlsx', xlsx_mime), 'sheet.xlsx')
self.assertEqual(fix('sheet.xlsx', 'application/zip'), 'sheet.xlsx')
with self.assertLogs('odoo.tools.mimetypes', 'WARNING') as capture:
self.assertEqual(fix('image.txt', 'image/jpeg'), 'image.txt.jpg')
self.assertEqual(fix('words.jpg', 'text/plain'), 'words.jpg.txt')
self.assertEqual(capture.output, [
"WARNING:odoo.tools.mimetypes:File 'image.txt' has an invalid "
"extension for mimetype 'image/jpeg', adding '.jpg'",
"WARNING:odoo.tools.mimetypes:File 'words.jpg' has an invalid "
"extension for mimetype 'text/plain', adding '.txt'",
])

View file

@ -1,6 +1,7 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
import base64
import datetime
from dateutil.relativedelta import relativedelta
import os.path
@ -14,42 +15,11 @@ from odoo.tools import (
merge_sequences,
misc,
remove_accents,
validate_url,
)
from odoo.tools.mail import validate_url
from odoo.tests.common import TransactionCase, BaseCase
class TestCountingStream(BaseCase):
def test_empty_stream(self):
s = misc.CountingStream(iter([]))
self.assertEqual(s.index, -1)
self.assertIsNone(next(s, None))
self.assertEqual(s.index, 0)
def test_single(self):
s = misc.CountingStream(range(1))
self.assertEqual(s.index, -1)
self.assertEqual(next(s, None), 0)
self.assertIsNone(next(s, None))
self.assertEqual(s.index, 1)
def test_full(self):
s = misc.CountingStream(range(42))
for _ in s:
pass
self.assertEqual(s.index, 42)
def test_repeated(self):
""" Once the CountingStream has stopped iterating, the index should not
increase anymore (the internal state should not be allowed to change)
"""
s = misc.CountingStream(iter([]))
self.assertIsNone(next(s, None))
self.assertEqual(s.index, 0)
self.assertIsNone(next(s, None))
self.assertEqual(s.index, 0)
class TestMergeSequences(BaseCase):
def test_merge_sequences(self):
# base case
@ -628,3 +598,35 @@ class TestUrlValidate(BaseCase):
self.assertEqual(validate_url('/index.html'), 'http:///index.html')
self.assertEqual(validate_url('?debug=1'), 'http://?debug=1')
self.assertEqual(validate_url('#model=project.task&id=3603607'), 'http://#model=project.task&id=3603607')
class TestMiscToken(TransactionCase):
def test_expired_token(self):
payload = {'test': True, 'value': 123456, 'some_string': 'hello', 'some_dict': {'name': 'New Dict'}}
expiration = datetime.datetime.now() - datetime.timedelta(days=1)
token = misc.hash_sign(self.env, 'test', payload, expiration=expiration)
self.assertIsNone(misc.verify_hash_signed(self.env, 'test', token))
def test_long_payload(self):
payload = {'test': True, 'value':123456, 'some_string': 'hello', 'some_dict': {'name': 'New Dict'}}
token = misc.hash_sign(self.env, 'test', payload, expiration_hours=24)
self.assertEqual(misc.verify_hash_signed(self.env, 'test', token), payload)
def test_None_payload(self):
with self.assertRaises(Exception):
misc.hash_sign(self.env, 'test', None, expiration_hours=24)
def test_list_payload(self):
payload = ["str1", "str2", "str3", 4, 5]
token = misc.hash_sign(self.env, 'test', payload, expiration_hours=24)
self.assertEqual(misc.verify_hash_signed(self.env, 'test', token), payload)
def test_modified_payload(self):
payload = ["str1", "str2", "str3", 4, 5]
token = base64.urlsafe_b64decode(misc.hash_sign(self.env, 'test', payload, expiration_hours=24) + '===')
new_timestamp = datetime.datetime.now() + datetime.timedelta(days=7)
new_timestamp = int(new_timestamp.timestamp())
new_timestamp = new_timestamp.to_bytes(8, byteorder='little')
token = base64.urlsafe_b64encode(token[:1] + new_timestamp + token[9:]).decode()
self.assertIsNone(misc.verify_hash_signed(self.env, 'test', token))

View file

@ -42,6 +42,7 @@ class TestModuleManifest(BaseCase):
'auto_install': False,
'bootstrap': False,
'category': 'Uncategorized',
'cloc_exclude': [],
'configurator_snippets': {},
'countries': [],
'data': [],

View file

@ -1,12 +1,8 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from collections import defaultdict
import psycopg2
from odoo.exceptions import AccessError, MissingError
from odoo.tests.common import TransactionCase
from odoo.exceptions import AccessError
from odoo.tests.common import TransactionCase, tagged
from odoo.tools import mute_logger
from odoo import Command
@ -156,64 +152,6 @@ class TestORM(TransactionCase):
recs = partner.browse([0])
self.assertFalse(recs.exists())
def test_groupby_date(self):
partners_data = dict(
A='2012-11-19',
B='2012-12-17',
C='2012-12-31',
D='2013-01-07',
E='2013-01-14',
F='2013-01-28',
G='2013-02-11',
)
partner_ids = []
partner_ids_by_day = defaultdict(list)
partner_ids_by_month = defaultdict(list)
partner_ids_by_year = defaultdict(list)
partners = self.env['res.partner']
for name, date in partners_data.items():
p = partners.create(dict(name=name, date=date))
partner_ids.append(p.id)
partner_ids_by_day[date].append(p.id)
partner_ids_by_month[date.rsplit('-', 1)[0]].append(p.id)
partner_ids_by_year[date.split('-', 1)[0]].append(p.id)
def read_group(interval):
domain = [('id', 'in', partner_ids)]
result = {}
for grp in partners.read_group(domain, ['date'], ['date:' + interval]):
result[grp['date:' + interval]] = partners.search(grp['__domain'])
return result
self.assertEqual(len(read_group('day')), len(partner_ids_by_day))
self.assertEqual(len(read_group('month')), len(partner_ids_by_month))
self.assertEqual(len(read_group('year')), len(partner_ids_by_year))
res = partners.read_group([('id', 'in', partner_ids)], ['date'],
['date:month', 'date:day'], lazy=False)
self.assertEqual(len(res), len(partner_ids))
# combine groupby and orderby
months = ['February 2013', 'January 2013', 'December 2012', 'November 2012']
res = partners.read_group([('id', 'in', partner_ids)], ['date'],
groupby=['date:month'], orderby='date:month DESC')
self.assertEqual([item['date:month'] for item in res], months)
# order by date should reorder by date:month
res = partners.read_group([('id', 'in', partner_ids)], ['date'],
groupby=['date:month'], orderby='date DESC')
self.assertEqual([item['date:month'] for item in res], months)
# order by date should reorder by date:day
days = ['11 Feb 2013', '28 Jan 2013', '14 Jan 2013', '07 Jan 2013',
'31 Dec 2012', '17 Dec 2012', '19 Nov 2012']
res = partners.read_group([('id', 'in', partner_ids)], ['date'],
groupby=['date:month', 'date:day'],
orderby='date DESC', lazy=False)
self.assertEqual([item['date:day'] for item in res], days)
def test_write_duplicate(self):
p1 = self.env['res.partner'].create({'name': 'W'})
(p1 + p1).write({'name': 'X'})
@ -234,28 +172,6 @@ class TestORM(TransactionCase):
group_user.write({'users': [Command.unlink(user.id)]})
self.assertTrue(user.share)
@mute_logger('odoo.models')
def test_unlink_with_property(self):
""" Verify that unlink removes the related ir.property as unprivileged user """
user = self.env['res.users'].create({
'name': 'Justine Bridou',
'login': 'saucisson',
'groups_id': [Command.set([self.ref('base.group_partner_manager')])],
})
p1 = self.env['res.partner'].with_user(user).create({'name': 'Zorro'})
self.env['ir.property'].with_user(user)._set_multi("ref", "res.partner", {p1.id: "Nain poilu"})
p1_prop = self.env['ir.property'].with_user(user)._get("ref", "res.partner", res_id=p1.id)
self.assertEqual(
p1_prop, "Nain poilu", 'p1_prop should have been created')
# Unlink with unprivileged user
p1.unlink()
# ir.property is deleted
p1_prop = self.env['ir.property'].with_user(user)._get("ref", "res.partner", res_id=p1.id)
self.assertEqual(
p1_prop, False, 'p1_prop should have been deleted')
def test_create_multi(self):
""" create for multiple records """
# assumption: 'res.bank' does not override 'create'
@ -401,3 +317,35 @@ class TestInherits(TransactionCase):
user.write({'image_1920': 'R0lGODlhAQABAIAAAP///////yH5BAEKAAEALAAAAAABAAEAAAICTAEAOw=='})
write_date_after = user.write_date
self.assertNotEqual(write_date_before, write_date_after)
@tagged('post_install', '-at_install')
class TestCompanyDependent(TransactionCase):
def test_orm_ondelete_restrict(self):
# model_A
# | field_a company dependent many2one is
# | company dependent many2one stored as jsonb and doesn't
# | (ondelete='restrict') have db ON DELETE action
# v
# model_B
# | field_b if a row for model_B is deleted
# | many2one (ondelete='cascade') because of ON DELETE CASCADE,
# v model_A will reference a deleted
# model_C row and logically be NULL when read
#
# this test asks you to move the
# ON DELETE CASCADE logic of model_B
# to ORM and remove ondelete='cascade'
for model in self.env.registry.values():
for field in model._fields.values():
if field.company_dependent and field.type == 'many2one' and field.ondelete.lower() == 'restrict':
for comodel_field in self.env[field.comodel_name]._fields.values():
self.assertFalse(
comodel_field.type == 'many2one' and comodel_field.ondelete == 'cascade',
(f'when a row for {comodel_field.comodel_name} is deleted, a row for {comodel_field.model_name} '
f'may also be deleted for sake of on delete cascade field {comodel_field}, which will '
f'bypass the ORM ondelete="restrict" check for a company dependent many2one field {field}. '
f'Please override the unlink method of {comodel_field.comodel_name} and do the ORM on '
f'delete cascade logic and remove/override the ondelete="cascade" of {comodel_field}')
)

View file

@ -1,11 +1,28 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo.tests.common import TransactionCase
from odoo.tools import get_cache_key_counter
from odoo.tests.common import TransactionCase, tagged
from odoo.tools.cache import get_cache_key_counter
from threading import Thread, Barrier
class TestOrmcache(TransactionCase):
@tagged('-at_install', 'post_install')
class TestOrmCache(TransactionCase):
@classmethod
def setUpClass(cls):
super().setUpClass()
if cls.registry.registry_invalidated:
raise AssertionError('Registry should not be invalidated when starting this test')
if cls.registry.cache_invalidated:
raise AssertionError('Cache should not be invalidated when starting this test')
# this test verifies the actual side effects of signaling changes
cls._signal_changes_patcher.stop()
# if something invalidate the cache or registry before test_signaling_01_multiple,
# the test may fail the first time but succeed on retry
# disabling autoretry to avoid hidding "real" errrors
cls._retry = False
def test_ormcache(self):
""" Test the effectiveness of the ormcache() decorator. """
IMD = self.env['ir.model.data']

View file

@ -2,7 +2,7 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo.tests.common import BaseCase, TransactionCase
from odoo.tools import Query
from odoo.tools import Query, SQL
class QueryTestCase(BaseCase):
@ -18,10 +18,10 @@ class QueryTestCase(BaseCase):
alias = query.left_join("product_product", "user_id", "res_user", "id", "user_id")
self.assertEqual(alias, 'product_product__user_id')
from_clause, where_clause, where_params = query.get_sql()
self.assertEqual(from_clause,
self.assertEqual(query.from_clause.code,
'"product_product", "product_template" JOIN "product_category" AS "product_template__categ_id" ON ("product_template"."categ_id" = "product_template__categ_id"."id") LEFT JOIN "res_user" AS "product_product__user_id" ON ("product_product"."user_id" = "product_product__user_id"."id")')
self.assertEqual(where_clause, "product_product.template_id = product_template.id")
self.assertEqual(query.where_clause.code,
"product_product.template_id = product_template.id")
def test_query_chained_explicit_joins(self):
query = Query(None, 'product_product')
@ -34,10 +34,10 @@ class QueryTestCase(BaseCase):
alias = query.left_join("product_template__categ_id", "user_id", "res_user", "id", "user_id")
self.assertEqual(alias, 'product_template__categ_id__user_id')
from_clause, where_clause, where_params = query.get_sql()
self.assertEqual(from_clause,
self.assertEqual(query.from_clause.code,
'"product_product", "product_template" JOIN "product_category" AS "product_template__categ_id" ON ("product_template"."categ_id" = "product_template__categ_id"."id") LEFT JOIN "res_user" AS "product_template__categ_id__user_id" ON ("product_template__categ_id"."user_id" = "product_template__categ_id__user_id"."id")')
self.assertEqual(where_clause, "product_product.template_id = product_template.id")
self.assertEqual(query.where_clause.code,
"product_product.template_id = product_template.id")
def test_mixed_query_chained_explicit_implicit_joins(self):
query = Query(None, 'product_product')
@ -53,10 +53,10 @@ class QueryTestCase(BaseCase):
query.add_table('account_account')
query.add_where("product_category.expense_account_id = account_account.id")
from_clause, where_clause, where_params = query.get_sql()
self.assertEqual(from_clause,
self.assertEqual(query.from_clause.code,
'"product_product", "product_template", "account_account" JOIN "product_category" AS "product_template__categ_id" ON ("product_template"."categ_id" = "product_template__categ_id"."id") LEFT JOIN "res_user" AS "product_template__categ_id__user_id" ON ("product_template__categ_id"."user_id" = "product_template__categ_id__user_id"."id")')
self.assertEqual(where_clause, "product_product.template_id = product_template.id AND product_category.expense_account_id = account_account.id")
self.assertEqual(query.where_clause.code,
"product_product.template_id = product_template.id AND product_category.expense_account_id = account_account.id")
def test_raise_missing_lhs(self):
query = Query(None, 'product_product')
@ -83,21 +83,21 @@ class QueryTestCase(BaseCase):
def test_table_expression(self):
query = Query(None, 'foo')
from_clause, where_clause, where_params = query.get_sql()
from_clause = query.from_clause.code
self.assertEqual(from_clause, '"foo"')
query = Query(None, 'bar', 'SELECT id FROM foo')
from_clause, where_clause, where_params = query.get_sql()
query = Query(None, 'bar', SQL('(SELECT id FROM foo)'))
from_clause = query.from_clause.code
self.assertEqual(from_clause, '(SELECT id FROM foo) AS "bar"')
query = Query(None, 'foo')
query.add_table('bar', 'SELECT id FROM foo')
from_clause, where_clause, where_params = query.get_sql()
query.add_table('bar', SQL('(SELECT id FROM foo)'))
from_clause = query.from_clause.code
self.assertEqual(from_clause, '"foo", (SELECT id FROM foo) AS "bar"')
query = Query(None, 'foo')
query.join('foo', 'bar_id', 'SELECT id FROM foo', 'id', 'bar')
from_clause, where_clause, where_params = query.get_sql()
query.join('foo', 'bar_id', SQL('(SELECT id FROM foo)'), 'id', 'bar')
from_clause = query.from_clause.code
self.assertEqual(from_clause, '"foo" JOIN (SELECT id FROM foo) AS "foo__bar" ON ("foo"."bar_id" = "foo__bar"."id")')

View file

@ -4,6 +4,7 @@
from odoo.tests.common import TransactionCase
from odoo.tools import pdf
from odoo.tools.misc import file_open
from odoo.tools.pdf import reshape_text
import io
@ -96,3 +97,26 @@ class TestPdf(TransactionCase):
def tearDown(self):
super().tearDown()
self.minimal_reader_buffer.close()
def test_reshaping_non_arabic_text(self):
"""
Test that reshaper doesn't alter non-Arabic text.
"""
english_text = "Hello, I'm just an English text"
processed_text = reshape_text(english_text)
self.assertEqual(english_text, processed_text, "English text shouldn't be altered.")
brazilian_text = "Ayrton Senna foi o melhor piloto de Formula 1 que já existiu"
processed_brazilian_text = reshape_text(brazilian_text)
self.assertEqual(brazilian_text, processed_brazilian_text, "Brazilian text shouldn't be altered.")
def test_reshaping_arabic_text(self):
"""
Test reshaping is applied properly on Arabic text.
"""
text = "بث مباشر"
processed_text = reshape_text(text)
expected_shapes = ['', '', '', '', '', ' ', '', '']
for i, expected_shape in enumerate(expected_shapes):
self.assertEqual(processed_text[i], expected_shape)

View file

@ -455,24 +455,45 @@ class TestReportsRendering(TestReportsRenderingCommon):
pdf_content = self.create_pdf(page_content=page_content)
pages = self._parse_pdf(pdf_content)
self.assertEqual(len(pages), 4, '4 pages are expected, 2 per record (you may ensure `nb_lines` has a correct value to generate an oveflow)')
page_break_at = int(pages[1][2][1].split('\n')[0]) # This element should be the first line, 61 when this test was written
self.assertEqual(len(pages), 6,
'6 pages are expected, 3 per record (you may ensure `nb_lines` has a correct value to generate an oveflow)')
first_page_break_at = int(
pages[1][2][1].split('\n')[0]) # This element should be the first line, 61 when this test was written
second_page_break_at = int(pages[2][2][1].split('\n')[0])
# There is some inconsistency caused by the pdfminer library when \n are placed, to be sure we don't have issues
# We put one element per line
pages_contents = []
for page in pages:
page_content = []
for elem in page:
if '\n' in elem[1]:
page_content.extend(elem[1].split('\n'))
else:
page_content.append(elem[1])
pages_contents.append(page_content)
expected_pages_contents = []
# Thoses changes are needed to format the page content and the expected page the same due to the inconsistency
# With the pdfminer library
for partner in self.partners:
expected_pages_contents.append([
'LTFigure', #logo
'Some header Text',
f'Name: {partner.name}\n' + '\n'.join([str(i) for i in range(page_break_at)]),
f'Footer for {partner.name} Page: 1 / 2',
def create_page_content(start, end, page_number, include_name=False):
content = [
'LTFigure', # logo
'Some header Text',
]
if include_name:
content.append(f'Name: {partner.name}')
content.extend([str(i) for i in range(start, end)])
content.append(f'Footer for {partner.name} Page: {page_number} / 3')
return content
expected_pages_contents.extend([
create_page_content(0, first_page_break_at, 1, include_name=True),
create_page_content(first_page_break_at, second_page_break_at, 2),
create_page_content(second_page_break_at, nb_lines, 3)
])
expected_pages_contents.append([
'LTFigure', #logo
'Some header Text',
'\n'.join([str(i) for i in range(page_break_at, nb_lines)]),
f'Footer for {partner.name} Page: 2 / 2',
])
pages_contents = [[elem[1] for elem in page] for page in pages]
self.assertEqual(pages_contents, expected_pages_contents)
def test_thead_tbody_repeat(self):
@ -497,8 +518,11 @@ class TestReportsRendering(TestReportsRenderingCommon):
pdf_content = self.create_pdf(page_content=page_content)
pages = self._parse_pdf(pdf_content)
self.assertEqual(len(pages), 4, '4 pages are expected, 2 per record (you may ensure `nb_lines` has a correct value to generate an oveflow)')
page_break_at = int(pages[1][5][1]) # This element should be the first line of the table, 28 when this test was written
self.assertEqual(len(pages), 6, '6 pages are expected, 3 per record (you may ensure `nb_lines` has a correct value to generate an oveflow)')
# This element should be the first line of the table, 28 when this test was written
first_page_break_at = int(pages[1][5][1])
second_page_break_at = int(pages[2][5][1])
def expected_table(start, end):
table = ['T1', 'T2', 'T3'] # thead
@ -512,14 +536,20 @@ class TestReportsRendering(TestReportsRenderingCommon):
expected_pages_contents.append([
'LTFigure', #logo
'Some header Text',
* expected_table(0, page_break_at),
f'Footer for {partner.name} Page: 1 / 2',
* expected_table(0, first_page_break_at),
f'Footer for {partner.name} Page: 1 / 3',
])
expected_pages_contents.append([
'LTFigure', #logo
'Some header Text',
* expected_table(page_break_at, nb_lines),
f'Footer for {partner.name} Page: 2 / 2',
* expected_table(first_page_break_at, second_page_break_at),
f'Footer for {partner.name} Page: 2 / 3',
])
expected_pages_contents.append([
'LTFigure', # logo
'Some header Text',
*expected_table(second_page_break_at, nb_lines),
f'Footer for {partner.name} Page: 3 / 3',
])
pages_contents = [[elem[1] for elem in page] for page in pages]

View file

@ -1,7 +1,7 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import Command
from odoo.exceptions import UserError, ValidationError
from odoo.exceptions import ValidationError
from odoo.tests.common import TransactionCase
@ -51,20 +51,6 @@ class TestCompany(TransactionCase):
company.partner_id.image_1920 = "R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7"
self.assertFalse(company.uses_default_logo)
def test_unlink_company_with_children(self):
"""Ensure that companies with child companies cannot be deleted."""
parent_company = self.env['res.company'].create({
'name': 'Parent Company',
'child_ids': [
Command.create({'name': 'Child Company'}),
],
})
with self.assertRaises(UserError):
parent_company.unlink()
self.assertTrue(parent_company.exists())
def test_create_branch_with_default_parent_id(self):
branch = self.env['res.company'].with_context(default_parent_id=self.env.company.id).create({'name': 'Branch Company'})
self.assertFalse(branch.partner_id.parent_id)

View file

@ -6,7 +6,7 @@ from lxml import etree
import logging
from odoo import exceptions, Command
from odoo.tests.common import Form, TransactionCase, tagged
from odoo.tests import Form, TransactionCase, tagged
_logger = logging.getLogger(__name__)
@ -251,8 +251,7 @@ class TestResConfigExecute(TransactionCase):
forbidden_models_fields = defaultdict(set)
for model in models_to_check:
has_read_access = self.env[model].with_user(user).check_access_rights(
'read', raise_exception=False)
has_read_access = self.env[model].with_user(user).has_access('read')
if not has_read_access:
forbidden_models_fields[model] = models_to_check[model]

View file

@ -15,11 +15,11 @@ class TestResCurrency(TransactionCase):
{'name': 'bar', 'currency_id': self.env.ref('base.USD').id},
])
for company, expected_currency in [(company_foo, 'EUR'), (company_bar, 'USD')]:
for model, view_type in [('res.currency', 'form'), ('res.currency.rate', 'tree')]:
for model, view_type in [('res.currency', 'form'), ('res.currency.rate', 'list')]:
arch = self.env[model].with_company(company).get_view(view_type=view_type)['arch']
tree = etree.fromstring(arch)
node_company_rate = tree.xpath('//field[@name="company_rate"]')[0]
node_inverse_company_rate = tree.xpath('//field[@name="inverse_company_rate"]')[0]
node_company_rate = tree.find('.//field[@name="company_rate"]')
node_inverse_company_rate = tree.find('.//field[@name="inverse_company_rate"]')
self.assertEqual(node_company_rate.get('string'), f'Unit per {expected_currency}')
self.assertEqual(node_inverse_company_rate.get('string'), f'{expected_currency} per Unit')

View file

@ -59,3 +59,109 @@ class test_res_lang(TransactionCase):
with self.assertRaises(UserError):
language.active = False
def test_get_data(self):
ResLang = self.env['res.lang']
en_id = ResLang._activate_lang('en_US').id
en_url_code = ResLang.browse(en_id).url_code
fr_id = ResLang._activate_lang('fr_FR').id
fr_direction = ResLang.browse(fr_id).direction
fr_data = ResLang._get_data(id=fr_id)
dummy_data = ResLang._get_data(id=0)
# test __eq__
self.env.registry.clear_cache()
self.assertEqual(ResLang._get_data(id=fr_id), fr_data)
self.assertEqual(ResLang._get_data(id=0), dummy_data)
# test __bool__
# data for an active language
self.assertTrue(ResLang._get_data(code='en_US'))
# data for an inactive language
self.assertFalse(ResLang._get_data(code='nl_NL'))
# data for an invalid dummy language
self.assertFalse(ResLang._get_data(code='dummy'))
# test dict conversion
self.assertEqual(
dict(ResLang._get_data(id=fr_id)),
ResLang.browse(fr_id).read(ResLang.CACHED_FIELDS)[0]
)
self.assertEqual(
dict(ResLang._get_data(id=0)),
dict.fromkeys(ResLang.CACHED_FIELDS, False)
)
# test performance
self.env.cache.clear()
self.env.registry.clear_cache()
# 1 query for res_lang +
# 1 query for ir_attachment to compute `flag_image_url`
with self.assertQueryCount(2):
# get cached field value for an active language
self.assertEqual(ResLang._get_data(code='en_US').url_code, en_url_code)
# get another cached field value for another active language
self.assertEqual(ResLang._get_data(code='fr_FR').direction, fr_direction)
# get field value for an inactive language
self.assertEqual(ResLang._get_data(code='nl_NL').direction, False)
# get field value for a dummy language
self.assertEqual(ResLang._get_data(code='dummy').direction, False)
# test programming error
with self.assertRaises(AttributeError):
# raise error for querying a not cached field of an active language
ResLang._get_data(code='en_US').flag_image
with self.assertRaises(AttributeError):
# raise error for querying a not cached field of an inactive language
ResLang._get_data(code='nl_NL').flag_image
with self.assertRaises(AttributeError):
# raise error for querying a not cached field of the dummy language
ResLang._get_data(code='dummy').flag_image
def test_lang_url_code_shortening(self):
# Setup and initial checks
ResLang = self.env['res.lang']
es_ES = self.env.ref('base.lang_es')
self.assertFalse(es_ES.active)
self.assertEqual(es_ES.url_code, 'es_ES')
es_419 = self.env.ref('base.lang_es_419')
self.assertFalse(es_419.active)
self.assertEqual(es_419.url_code, 'es')
# Activating es_ES should give it the url_code 'es' (short version) and
# es_419 should have its url_code changed from 'es' to 'es_419'
ResLang._activate_lang('es_ES')
self.assertEqual(es_419.url_code, 'es_419')
self.assertEqual(es_ES.url_code, 'es')
# Activating es_419 should not set it's url_code back to 'es'
ResLang._activate_lang('es_419')
self.assertEqual(es_419.url_code, 'es_419')
self.assertEqual(es_ES.url_code, 'es')
# Disabling both 'es' languages and activating 'es_419' should set its
# url_code back to 'es' since that short version is now 'available'
(es_419 + es_ES).write({'active': False})
ResLang._activate_lang('es_419')
self.assertEqual(es_419.url_code, 'es')
self.assertEqual(es_ES.url_code, 'es_ES')
# Now, special case if one day a lang receive a short code as default
# `code`, it's not the case as of today but there is plan to make it
# happen for `es_419`, the code is already ready for it.
self.env.cr.execute(f""" UPDATE res_lang SET code = 'es' where id = {es_419.id}""")
self.env.invalidate_all()
self.assertEqual(es_419.code, 'es')
(es_419 + es_ES).write({'active': False})
ResLang._activate_lang('es_419')
self.assertEqual(es_419.url_code, 'es')
self.assertEqual(es_ES.url_code, 'es_ES')
es_419.active = False
ResLang._activate_lang('es_ES')
self.assertEqual(es_419.url_code, 'es')
# es_ES can't have its url_code shortened because there is no
# possibility to replace 'es' url_code from 'es_419' if we change its
# code from 'es_419' to 'es' in the future
self.assertEqual(es_ES.url_code, 'es_ES')
# Another special case, /my is reserved to portal controller
my_MM = ResLang._activate_lang('my_MM')
self.assertEqual(my_MM.url_code, 'mya')

View file

@ -4,13 +4,13 @@
from contextlib import contextmanager
from unittest.mock import patch
from odoo import Command
from odoo import Command, models
from odoo.addons.base.models.ir_mail_server import extract_rfc2822_addresses
from odoo.addons.base.models.res_partner import Partner
from odoo.addons.base.tests.common import TransactionCaseWithUserDemo
from odoo.exceptions import AccessError, RedirectWarning, UserError, ValidationError
from odoo.tests import Form
from odoo.tests.common import tagged, TransactionCase
from odoo.tests.common import new_test_user, tagged, TransactionCase, users
# samples use effective TLDs from the Mozilla public suffix
# list at http://publicsuffix.org
@ -296,7 +296,7 @@ class TestPartner(TransactionCaseWithUserDemo):
# Check a partner may be searched when current user has no access but sudo is used
public_user = self.env.ref('base.public_user')
with self.assertRaises(AccessError):
test_partner.with_user(public_user).check_access_rule('read')
test_partner.with_user(public_user).check_access('read')
ns_res = self.env['res.partner'].with_user(public_user).sudo().name_search('Vlad', args=[('user_ids.email', 'ilike', 'vlad')])
self.assertEqual(set(i[0] for i in ns_res), set(test_user.partner_id.ids))
@ -312,133 +312,6 @@ class TestPartner(TransactionCaseWithUserDemo):
"'Destination Contact' name should contain db ID in brackets"
)
def test_partner_merge_null_company_property(self):
""" Check that partner with null company ir.property can be merged """
partners = self.env['res.partner'].create([
{'name': 'no barcode partner'},
{'name': 'partner1', 'barcode': 'partner1'},
{'name': 'partner2', 'barcode': 'partner2'},
])
with self.assertLogs(level='ERROR'):
partner_merge_wizard = self.env['base.partner.merge.automatic.wizard']._merge(
partners.ids,
partners[0],
)
self.assertFalse(partners[0].barcode)
partners = self.env['res.partner'].create([
{'name': 'partner1', 'barcode': 'partner1'},
{'name': 'partner2', 'barcode': 'partner2'},
])
self.env['ir.property'].search([
('res_id', 'in', ["res.partner,{}".format(p.id) for p in partners])
]).company_id = None
partner_merge_wizard = self.env['base.partner.merge.automatic.wizard']._merge(
partners.ids,
partners[0],
)
def test_read_group(self):
title_sir = self.env['res.partner.title'].create({'name': 'Sir...'})
title_lady = self.env['res.partner.title'].create({'name': 'Lady...'})
user_vals_list = [
{'name': 'Alice', 'login': 'alice', 'color': 1, 'function': 'Friend', 'date': '2015-03-28', 'title': title_lady.id},
{'name': 'Alice', 'login': 'alice2', 'color': 0, 'function': 'Friend', 'date': '2015-01-28', 'title': title_lady.id},
{'name': 'Bob', 'login': 'bob', 'color': 2, 'function': 'Friend', 'date': '2015-03-02', 'title': title_sir.id},
{'name': 'Eve', 'login': 'eve', 'color': 3, 'function': 'Eavesdropper', 'date': '2015-03-20', 'title': title_lady.id},
{'name': 'Nab', 'login': 'nab', 'color': -3, 'function': '5$ Wrench', 'date': '2014-09-10', 'title': title_sir.id},
{'name': 'Nab', 'login': 'nab-she', 'color': 6, 'function': '5$ Wrench', 'date': '2014-01-02', 'title': title_lady.id},
]
res_users = self.env['res.users']
users = res_users.create(user_vals_list)
domain = [('id', 'in', users.ids)]
# group on local char field without domain and without active_test (-> empty WHERE clause)
groups_data = res_users.with_context(active_test=False).read_group([], fields=['login'], groupby=['login'], orderby='login DESC')
self.assertGreater(len(groups_data), 6, "Incorrect number of results when grouping on a field")
# group on local char field with limit
groups_data = res_users.read_group(domain, fields=['login'], groupby=['login'], orderby='login DESC', limit=3, offset=3)
self.assertEqual(len(groups_data), 3, "Incorrect number of results when grouping on a field with limit")
self.assertEqual([g['login'] for g in groups_data], ['bob', 'alice2', 'alice'], 'Result mismatch')
# group on inherited char field, aggregate on int field (second groupby ignored on purpose)
groups_data = res_users.read_group(domain, fields=['name', 'color', 'function'], groupby=['function', 'login'])
self.assertEqual(len(groups_data), 3, "Incorrect number of results when grouping on a field")
self.assertEqual(['5$ Wrench', 'Eavesdropper', 'Friend'], [g['function'] for g in groups_data], 'incorrect read_group order')
for group_data in groups_data:
self.assertIn('color', group_data, "Aggregated data for the column 'color' is not present in read_group return values")
self.assertEqual(group_data['color'], 3, "Incorrect sum for aggregated data for the column 'color'")
# group on inherited char field, reverse order
groups_data = res_users.read_group(domain, fields=['name', 'color'], groupby='name', orderby='name DESC')
self.assertEqual([g['name'] for g in groups_data], ['Nab', 'Eve', 'Bob', 'Alice'], 'Incorrect ordering of the list')
# group on int field, default ordering
groups_data = res_users.read_group(domain, fields=['color'], groupby='color')
self.assertEqual([g['color'] for g in groups_data], [-3, 0, 1, 2, 3, 6], 'Incorrect ordering of the list')
# multi group, second level is int field, should still be summed in first level grouping
groups_data = res_users.read_group(domain, fields=['name', 'color'], groupby=['name', 'color'], orderby='name DESC')
self.assertEqual([g['name'] for g in groups_data], ['Nab', 'Eve', 'Bob', 'Alice'], 'Incorrect ordering of the list')
self.assertEqual([g['color'] for g in groups_data], [3, 3, 2, 1], 'Incorrect ordering of the list')
# group on inherited char field, multiple orders with directions
groups_data = res_users.read_group(domain, fields=['name', 'color'], groupby='name', orderby='color DESC, name')
self.assertEqual(len(groups_data), 4, "Incorrect number of results when grouping on a field")
self.assertEqual([g['name'] for g in groups_data], ['Eve', 'Nab', 'Bob', 'Alice'], 'Incorrect ordering of the list')
self.assertEqual([g['name_count'] for g in groups_data], [1, 2, 1, 2], 'Incorrect number of results')
# group on inherited date column (res_partner.date) -> Year-Month, default ordering
groups_data = res_users.read_group(domain, fields=['function', 'color', 'date'], groupby=['date'])
self.assertEqual(len(groups_data), 4, "Incorrect number of results when grouping on a field")
self.assertEqual([g['date'] for g in groups_data], ['January 2014', 'September 2014', 'January 2015', 'March 2015'], 'Incorrect ordering of the list')
self.assertEqual([g['date_count'] for g in groups_data], [1, 1, 1, 3], 'Incorrect number of results')
# group on inherited date column (res_partner.date) specifying the :year -> Year default ordering
groups_data = res_users.read_group(domain, fields=['function', 'color', 'date'], groupby=['date:year'])
self.assertEqual(len(groups_data), 2, "Incorrect number of results when grouping on a field")
self.assertEqual([g['date:year'] for g in groups_data], ['2014', '2015'], 'Incorrect ordering of the list')
self.assertEqual([g['date_count'] for g in groups_data], [2, 4], 'Incorrect number of results')
# group on inherited date column (res_partner.date) -> Year-Month, custom order
groups_data = res_users.read_group(domain, fields=['function', 'color', 'date'], groupby=['date'], orderby='date DESC')
self.assertEqual(len(groups_data), 4, "Incorrect number of results when grouping on a field")
self.assertEqual([g['date'] for g in groups_data], ['March 2015', 'January 2015', 'September 2014', 'January 2014'], 'Incorrect ordering of the list')
self.assertEqual([g['date_count'] for g in groups_data], [3, 1, 1, 1], 'Incorrect number of results')
# group on inherited many2one (res_partner.title), default order
groups_data = res_users.read_group(domain, fields=['function', 'color', 'title'], groupby=['title'])
self.assertEqual(len(groups_data), 2, "Incorrect number of results when grouping on a field")
# m2o is returned as a (id, label) pair
self.assertEqual([g['title'] for g in groups_data], [(title_lady.id, 'Lady...'), (title_sir.id, 'Sir...')], 'Incorrect ordering of the list')
self.assertEqual([g['title_count'] for g in groups_data], [4, 2], 'Incorrect number of results')
self.assertEqual([g['color'] for g in groups_data], [10, -1], 'Incorrect aggregation of int column')
# group on inherited many2one (res_partner.title), reversed natural order
groups_data = res_users.read_group(domain, fields=['function', 'color', 'title'], groupby=['title'], orderby="title desc")
self.assertEqual(len(groups_data), 2, "Incorrect number of results when grouping on a field")
# m2o is returned as a (id, label) pair
self.assertEqual([(title_sir.id, 'Sir...'), (title_lady.id, 'Lady...')], [g['title'] for g in groups_data], 'Incorrect ordering of the list')
self.assertEqual([g['title_count'] for g in groups_data], [2, 4], 'Incorrect number of results')
self.assertEqual([g['color'] for g in groups_data], [-1, 10], 'Incorrect aggregation of int column')
# group on inherited many2one (res_partner.title), multiple orders with m2o in second position
groups_data = res_users.read_group(domain, fields=['function', 'color', 'title'], groupby=['title'], orderby="color desc, title desc")
self.assertEqual(len(groups_data), 2, "Incorrect number of results when grouping on a field")
# m2o is returned as a (id, label) pair
self.assertEqual([g['title'] for g in groups_data], [(title_lady.id, 'Lady...'), (title_sir.id, 'Sir...')], 'Incorrect ordering of the result')
self.assertEqual([g['title_count'] for g in groups_data], [4, 2], 'Incorrect number of results')
self.assertEqual([g['color'] for g in groups_data], [10, -1], 'Incorrect aggregation of int column')
# group on inherited many2one (res_partner.title), ordered by other inherited field (color)
groups_data = res_users.read_group(domain, fields=['function', 'color', 'title'], groupby=['title'], orderby='color')
self.assertEqual(len(groups_data), 2, "Incorrect number of results when grouping on a field")
# m2o is returned as a (id, label) pair
self.assertEqual([g['title'] for g in groups_data], [(title_sir.id, 'Sir...'), (title_lady.id, 'Lady...')], 'Incorrect ordering of the list')
self.assertEqual([g['title_count'] for g in groups_data], [2, 4], 'Incorrect number of results')
self.assertEqual([g['color'] for g in groups_data], [-1, 10], 'Incorrect aggregation of int column')
def test_display_name_translation(self):
self.env['res.lang']._activate_lang('fr_FR')
self.env.ref('base.module_base')._update_translations(['fr_FR'])
@ -463,68 +336,261 @@ class TestPartner(TransactionCaseWithUserDemo):
@tagged('res_partner')
class TestPartnerAddressCompany(TransactionCase):
def test_address(self):
res_partner = self.env['res.partner']
ghoststep = res_partner.create({
'name': 'GhostStep',
'is_company': True,
'street': 'Main Street, 10',
'phone': '123456789',
@classmethod
def setUpClass(cls):
super().setUpClass()
# test user
cls.test_user = new_test_user(
cls.env,
email='emp@test.mycompany.com',
groups='base.group_user,base.group_partner_manager',
login='employee',
name='Employee',
password='employee',
)
# test addresses
cls.base_address_fields = {'street', 'street2', 'zip', 'city', 'state_id', 'country_id'}
cls.test_country_state = cls.env['res.country.state'].create([
{
'code': 'OD',
'country_id': cls.env.ref('base.be').id,
'name': 'Odoo Province',
},
])
cls.test_industries = cls.env['res.partner.industry'].create([
{'name': 'Balto Impersonators'},
{'name': 'Floppy Advisors'},
{'name': 'Both of the above'},
])
cls.test_address_values_cmp, cls.test_address_values_2_cmp, cls.test_address_values_3_cmp = [
{
'city': 'Ramillies',
'country_id': cls.env.ref('base.be'),
'state_id': cls.test_country_state,
'street': 'Test Street',
'street2': '10 F',
'zip': '1367',
}, {
'city': 'Ramillies 2',
'country_id': cls.env.ref('base.us'),
'state_id': cls.env['res.country.state'],
'street': 'Another Street',
'street2': False,
'zip': '013670',
}, {
'city': 'Totally Not Ramillies',
'country_id': cls.env.ref('base.be'),
'state_id': cls.test_country_state,
'street': 'Third Street',
'street2': 'Without number',
'zip': '1367#Corgi',
},
]
cls.test_address_values, cls.test_address_values_2, cls.test_address_values_3 = [
{fname: value.id if isinstance(value, models.Model) else value for fname, value in values.items()}
for values in (cls.test_address_values_cmp, cls.test_address_values_2_cmp, cls.test_address_values_3_cmp)
]
# pre-existing data
cls.test_parent = cls.env['res.partner'].create({
'company_registry': '0477472701',
'email': 'info@ghoststep.com',
'industry_id': cls.test_industries[0].id,
'is_company': True,
'name': 'GhostStep',
'phone': '+32455001122',
'vat': 'BE0477472701',
'type': 'contact',
**cls.test_address_values,
})
cls.existing = cls.env['res.partner'].create({
'name': 'Existing Contact',
'parent_id': cls.test_parent.id,
})
p1 = res_partner.browse(res_partner.name_create('Denis Bladesmith <denis.bladesmith@ghoststep.com>')[0])
self.assertEqual(p1.type, 'contact', 'Default type must be "contact"')
p1phone = '123456789#34'
p1.write({'phone': p1phone,
'parent_id': ghoststep.id})
self.assertEqual(p1.street, ghoststep.street, 'Address fields must be synced')
self.assertEqual(p1.phone, p1phone, 'Phone should be preserved after address sync')
self.assertEqual(p1.type, 'contact', 'Type should be preserved after address sync')
self.assertEqual(p1.email, 'denis.bladesmith@ghoststep.com', 'Email should be preserved after sync')
# turn off sync
p1street = 'Different street, 42'
p1.write({'street': p1street,
'type': 'invoice'})
self.assertEqual(p1.street, p1street, 'Address fields must not be synced after turning sync off')
self.assertNotEqual(ghoststep.street, p1street, 'Parent address must never be touched')
@users('employee')
def test_address(self):
# check initial data
for fname, fvalue in self.test_address_values_cmp.items():
self.assertEqual(self.existing[fname], fvalue)
# turn on sync again
p1.write({'type': 'contact'})
self.assertEqual(p1.street, ghoststep.street, 'Address fields must be synced again')
self.assertEqual(p1.phone, p1phone, 'Phone should be preserved after address sync')
self.assertEqual(p1.type, 'contact', 'Type should be preserved after address sync')
self.assertEqual(p1.email, 'denis.bladesmith@ghoststep.com', 'Email should be preserved after sync')
# future new child
ct1 = self.env['res.partner'].browse(
self.env['res.partner'].name_create('Denis Bladesmith <denis.bladesmith@ghoststep.com>')[0]
)
self.assertEqual(ct1.type, 'contact', 'Default type must be "contact"')
# Modify parent, sync to children
ghoststreet = 'South Street, 25'
ghoststep.write({'street': ghoststreet})
self.assertEqual(p1.street, ghoststreet, 'Address fields must be synced automatically')
self.assertEqual(p1.phone, p1phone, 'Phone should not be synced')
self.assertEqual(p1.email, 'denis.bladesmith@ghoststep.com', 'Email should be preserved after sync')
ct2, inv, deli, other = self.env['res.partner'].create([
{
'name': 'Address, Future Sibling of P1',
**self.test_address_values_3,
}, {
'name': 'Invoice Child',
'street': 'Invoice Child Street',
'type': 'invoice',
}, {
'name': 'Delivery Child',
'street': 'Delivery Child Street',
'type': 'delivery',
}, {
'name': 'Other Child',
'street': 'Other Child Street',
'type': 'other',
},
])
ct1_1, inv_1 = self.env['res.partner'].create([
{
'name': 'Address, Child of P1',
'parent_id': ct1.id,
}, {
'name': 'Address, Child of Invoice',
'parent_id': inv.id,
},
])
# check creation values
for fname in self.base_address_fields:
self.assertFalse(ct1_1[fname])
self.assertFalse(ct1_1.vat)
self.assertEqual(inv_1.street, 'Invoice Child Street', 'Should take parent address')
self.assertFalse(inv_1.vat)
p1street = 'My Street, 11'
p1.write({'street': p1street})
self.assertEqual(ghoststep.street, ghoststreet, 'Touching contact should never alter parent')
# sync P1 with parent, check address is update + other fields in write kept
ct1_phone = '+320455999999'
ct1.write({
'phone': ct1_phone,
'parent_id': self.test_parent.id,
})
for fname, fvalue in self.test_address_values_cmp.items():
self.assertEqual(ct1[fname], fvalue)
# Note: update is done only for direct children of parent
self.assertFalse(ct1_1[fname], 'Descendants are not updated, only direct children')
self.assertEqual(ct1.email, 'denis.bladesmith@ghoststep.com', 'Email should be preserved after sync')
self.assertEqual(ct1.phone, ct1_phone, 'Phone should be preserved after address sync')
self.assertEqual(ct1.type, 'contact', 'Type should be preserved after address sync')
self.assertEqual(ct1.vat, 'BE0477472701', 'VAT should come from parent')
self.assertEqual(ct1.industry_id, self.test_industries[0], 'Industry should come from parent')
self.assertEqual(ct1.company_registry, '0477472701', 'Company registry should come from parent')
# turn off sync: do what you want
ct1_street = 'Different street, 42'
ct1.write({
'street': ct1_street,
'state_id': False,
'type': 'invoice',
})
self.assertEqual(ct1.street, ct1_street, 'Address fields must not be synced after turning sync off')
self.assertEqual(ct1.zip, '1367', 'Address fields not changed in write should have kept their value')
for fname in self.base_address_fields:
# Note: only updated values are sync
if fname == 'street':
self.assertEqual(ct1_1[fname], ct1_street)
else:
self.assertFalse(ct1_1[fname])
self.assertEqual(ct1.type, 'invoice')
self.assertEqual(ct1.parent_id, self.test_parent, 'Changing address should not break hierarchy')
self.assertNotEqual(self.test_parent.street, ct1_street, 'Parent address must not be touched')
# turn on sync again: should reset address to parent
ct1.write({'type': 'contact'})
for fname, fvalue in self.test_address_values_cmp.items():
self.assertEqual(ct1[fname], fvalue)
# Note: update is done only for direct children of parent
if fname == 'street':
self.assertEqual(ct1_1[fname], ct1_street)
else:
self.assertFalse(ct1_1[fname])
self.assertEqual(ct1.type, 'contact', 'Type should be preserved after address sync')
# set P2 as sibling of P1 -> should update address
ct2.write({'parent_id': self.test_parent.id})
for fname, fvalue in self.test_address_values_cmp.items():
self.assertEqual(ct2[fname], fvalue)
# DOWNSTREAM: parent -> children
# ------------------------------------------------------------
self.test_parent.write(self.test_address_values_2)
for fname, fvalue in self.test_address_values_2_cmp.items():
self.assertEqual(ct1[fname], fvalue)
self.assertEqual(ct2[fname], fvalue)
self.assertEqual(self.existing[fname], fvalue)
# but child of P3 is not updated, as only 1 level is updated
for fname in self.base_address_fields:
if fname == 'street':
self.assertEqual(ct1_1[fname], ct1_street, 'Updated only through P1 direct update')
else:
self.assertFalse(ct1_1[fname], 'Still holding base creation values, no descendants update')
# and not-contacts are not updated
for child in inv, deli, other:
self.assertEqual(child.street, f'{child.name} Street', 'Should not be updated')
# UPSTREAM: child -> parent update: not done currently, consider contact is readonly
# ------------------------------------------------------------
ct1.write(self.test_address_values_3)
for fname, fvalue in self.test_address_values_2_cmp.items():
self.assertEqual(self.test_parent[fname], fvalue)
self.assertEqual(ct2[fname], fvalue)
self.assertEqual(self.existing[fname], fvalue)
for fname, fvalue in self.test_address_values_3_cmp.items():
self.assertEqual(ct1[fname], fvalue)
self.assertEqual(ct1_1[fname], fvalue)
@users('employee')
def test_address_first_contact_sync(self):
""" Test initial creation of company/contact pair where contact address gets copied to
company """
res_partner = self.env['res.partner']
ironshield = res_partner.browse(res_partner.name_create('IronShield')[0])
self.assertFalse(ironshield.is_company, 'Partners are not companies by default')
self.assertEqual(ironshield.type, 'contact', 'Default type must be "contact"')
ironshield.write({'type': 'contact'})
p1 = res_partner.create({
'name': 'Isen Hardearth',
'street': 'Strongarm Avenue, 12',
'parent_id': ironshield.id,
})
self.assertEqual(p1.type, 'contact', 'Default type must be "contact", not the copied parent type')
self.assertEqual(ironshield.street, p1.street, 'Address fields should be copied to company')
(
void_parent_ct, void_parent_comp, full_parent_ct, full_parent_comp,
void_parent_withparent, full_parent_withparent,
) = self.env['res.partner'].create([
{ # contact parents
'name': 'Void Ct',
'is_company': False,
}, {
'name': 'Void Comp',
'is_company': True,
}, { # company parents
'name': 'Full Ct',
'is_company': False,
**self.test_address_values_2,
}, {
'name': 'Full Comp',
'is_company': False,
**self.test_address_values_2,
}, { # parent being itself a child of another partner
'name': 'Void Ct With Parent',
'parent_id': self.test_parent.id,
}, {
'name': 'Full Ct With Parent',
'parent_id': self.test_parent.id,
**self.test_address_values_2,
},
])
for parent in (void_parent_ct + void_parent_comp + full_parent_ct + full_parent_comp):
with self.subTest(parent_name=parent.name):
p1 = self.env['res.partner'].create(dict(
{
'name': 'Micheline Brutijus',
'parent_id': parent.id,
}, **self.test_address_values_3)
)
self.assertEqual(p1.type, 'contact', 'Default type must be "contact", not the copied parent type')
if parent in (void_parent_ct, void_parent_comp):
for fname, fvalue in self.test_address_values_3_cmp.items():
self.assertEqual(p1[fname], fvalue, 'Creation value taken')
self.assertEqual(parent[fname], fvalue, 'Should sync void parent to first contact')
elif parent in (full_parent_ct, full_parent_comp):
for fname, fvalue in self.test_address_values_2_cmp.items():
self.assertEqual(p1[fname], fvalue, 'Parent wins over creation values')
self.assertEqual(parent[fname], fvalue, 'Should not sync parent with address to first contact')
elif parent == full_parent_withparent:
for fname, fvalue in self.test_address_values_cmp.items():
self.assertEqual(p1[fname], fvalue)
self.assertEqual(parent[fname], fvalue, 'Should not sync parent that is not root to first contact')
elif parent == void_parent_withparent:
for fname, fvalue in self.test_address_values_cmp.items():
self.assertEqual(p1[fname], fvalue)
self.assertFalse(parent[fname], 'Should not sync parent that is not root to first contact, event when void')
def test_address_get(self):
""" Test address_get address resolution mechanism: it should first go down through descendants,
@ -653,76 +719,98 @@ class TestPartnerAddressCompany(TransactionCase):
def test_commercial_field_sync(self):
"""Check if commercial fields are synced properly: testing with VAT field"""
Partner = self.env['res.partner']
company_1 = Partner.create({'name': 'company 1', 'is_company': True, 'vat': 'BE0123456789'})
company_2 = Partner.create({'name': 'company 2', 'is_company': True, 'vat': 'BE9876543210'})
company_1, company_2 = self.env['res.partner'].create([
{
'company_registry': '123456789',
'industry_id': self.test_industries[0].id,
'is_company': True,
'name': 'company 1',
'vat': 'BE013456789',
}, {
'company_registry': '9876543210',
'industry_id': self.test_industries[0].id,
'is_company': True,
'name': 'company 2',
'vat': 'BE9876543210',
},
])
partner = Partner.create({'name': 'someone', 'is_company': False, 'parent_id': company_1.id})
Partner.flush_recordset()
self.assertEqual(partner.vat, company_1.vat, "VAT should be inherited from the company 1")
contact = self.env['res.partner'].create({'name': 'someone', 'is_company': False, 'parent_id': company_1.id})
self.assertEqual(contact.commercial_partner_id, company_1, "Commercial partner should be recomputed")
for fname in ('company_registry', 'industry_id', 'vat'):
self.assertEqual(contact[fname], company_1[fname], "Commercial field should be inherited from the company 1")
# create a delivery address for the partner
delivery = Partner.create({'name': 'somewhere', 'type': 'delivery', 'parent_id': partner.id})
self.assertEqual(delivery.commercial_partner_id.id, company_1.id, "Commercial partner should be recomputed")
self.assertEqual(delivery.vat, company_1.vat, "VAT should be inherited from the company 1")
# create a delivery address and a child for the partner
contact_dlr = self.env['res.partner'].create({'name': 'somewhere', 'type': 'delivery', 'parent_id': contact.id})
self.assertEqual(contact_dlr.commercial_partner_id, company_1, "Commercial partner should be recomputed")
for fname in ('company_registry', 'industry_id', 'vat'):
self.assertEqual(contact_dlr[fname], company_1[fname], "Commercial field should be inherited from the company 1")
contact_ct = self.env['res.partner'].create({'name': 'child someone', 'parent_id': contact.id})
self.assertEqual(contact_dlr.commercial_partner_id, company_1, "Commercial partner should be recomputed")
for fname in ('company_registry', 'industry_id', 'vat'):
self.assertEqual(contact_dlr[fname], company_1[fname], "Commercial field should be inherited from the company 1")
# move the partner to another company
partner.write({'parent_id': company_2.id})
partner.flush_recordset()
self.assertEqual(partner.commercial_partner_id.id, company_2.id, "Commercial partner should be recomputed")
self.assertEqual(partner.vat, company_2.vat, "VAT should be inherited from the company 2")
self.assertEqual(delivery.commercial_partner_id.id, company_2.id, "Commercial partner should be recomputed on delivery")
self.assertEqual(delivery.vat, company_2.vat, "VAT should be inherited from the company 2 to delivery")
contact.write({'parent_id': company_2.id})
self.assertEqual(contact.commercial_partner_id, company_2, "Commercial partner should be recomputed")
for fname in ('company_registry', 'industry_id', 'vat'):
self.assertEqual(contact[fname], company_2[fname], "Commercial field should be inherited from the company 2")
self.assertEqual(contact_dlr.commercial_partner_id, company_2, "Commercial partner should be recomputed on delivery")
for fname in ('company_registry', 'industry_id', 'vat'):
self.assertEqual(contact_dlr[fname], company_2[fname], "Commecial field should be inherited from the company 2 to delivery")
self.assertEqual(contact_ct.commercial_partner_id, company_2, "Commercial partner should be recomputed on delivery")
for fname in ('company_registry', 'industry_id', 'vat'):
self.assertEqual(contact_ct[fname], company_2[fname], "Commecial field should be inherited from the company 2 to delivery")
def test_commercial_sync(self):
res_partner = self.env['res.partner']
p0 = res_partner.create({'name': 'Sigurd Sunknife',
'email': 'ssunknife@gmail.com'})
sunhelm = res_partner.create({'name': 'Sunhelm',
'is_company': True,
'street': 'Rainbow Street, 13',
'phone': '1122334455',
'email': 'info@sunhelm.com',
'vat': 'BE0477472701',
'child_ids': [Command.link(p0.id),
Command.create({'name': 'Alrik Greenthorn',
'email': 'agr@sunhelm.com'})]})
p1 = res_partner.create({'name': 'Otto Blackwood',
'email': 'otto.blackwood@sunhelm.com',
'parent_id': sunhelm.id})
p11 = res_partner.create({'name': 'Gini Graywool',
'email': 'ggr@sunhelm.com',
'parent_id': p1.id})
p2 = res_partner.search([('email', '=', 'agr@sunhelm.com')], limit=1)
sunhelm.write({'child_ids': [Command.create({'name': 'Ulrik Greenthorn',
'email': 'ugr@sunhelm.com'})]})
p3 = res_partner.search([('email', '=', 'ugr@sunhelm.com')], limit=1)
# check using embedded 2many commands
company_2.write({'child_ids': [(0, 0, {'name': 'Alrik Greenthorn', 'email': 'agr@sunhelm.com'})]})
contact2 = self.env['res.partner'].search([('email', '=', 'agr@sunhelm.com')])
for fname in ('company_registry', 'industry_id', 'vat'):
self.assertEqual(contact2[fname], company_2[fname], "Commercial field should be inherited from the company 2")
for p in (p0, p1, p11, p2, p3):
self.assertEqual(p.commercial_partner_id, sunhelm, 'Incorrect commercial entity resolution')
self.assertEqual(p.vat, sunhelm.vat, 'Commercial fields must be automatically synced')
sunhelmvat = 'BE0123456749'
sunhelm.write({'vat': sunhelmvat})
for p in (p0, p1, p11, p2, p3):
self.assertEqual(p.vat, sunhelmvat, 'Commercial fields must be automatically and recursively synced')
# DOWNSTREAM update to descendants
company_2.write({'company_registry': 'new', 'industry_id': self.test_industries[1].id, 'vat': 'BEnew'})
for partner in contact + contact_dlr + contact_ct + contact2:
for fname, fvalue in (('company_registry', 'new'), ('industry_id', self.test_industries[1]), ('vat', 'BEnew')):
self.assertEqual(partner[fname], fvalue, "Commercial field should be updated from the company 2")
p1vat = 'BE0987654394'
p1.write({'vat': p1vat})
for p in (sunhelm, p0, p11, p2, p3):
self.assertEqual(p.vat, sunhelmvat, 'Sync to children should only work downstream and on commercial entities')
# UPSTREAM: not supported (but desyncs it)
contactvat = 'BE445566'
contact.write({'vat': contactvat})
for partner in company_2 + contact_dlr + contact_ct + contact2:
self.assertEqual(partner.vat, 'BEnew', 'Sync to children should only work downstream and on commercial entities')
for partner in contact:
self.assertEqual(partner.vat, contactvat, 'Sync to children should only work downstream and on commercial entities')
# MISC PARENT MANIPULATION
# promote p1 to commercial entity
p1.write({'parent_id': sunhelm.id,
'is_company': True,
'name': 'Sunhelm Subsidiary'})
self.assertEqual(p1.vat, p1vat, 'Setting is_company should stop auto-sync of commercial fields')
self.assertEqual(p1.commercial_partner_id, p1, 'Incorrect commercial entity resolution after setting is_company')
contact.write({
'parent_id': company_1.id,
'is_company': True,
'name': 'Sunhelm Subsidiary',
})
self.assertEqual(contact.vat, contactvat, 'Setting is_company should stop auto-sync of commercial fields')
self.assertEqual(contact.commercial_partner_id, contact, 'Incorrect commercial entity resolution after setting is_company')
self.assertEqual(company_1.vat, 'BE013456789', 'Should not impact parent')
self.assertEqual(contact_dlr.vat, 'BEnew', 'Promotion not propagated')
self.assertEqual(contact_ct.vat, 'BEnew', 'Promotion not propagated')
# change parent of commercial entity
(contact_dlr + contact_ct).write({'vat': contactvat})
contact.write({'parent_id': company_2.id})
self.assertEqual(contact.vat, contactvat, 'Setting is_company should stop auto-sync of commercial fields')
self.assertEqual(contact.commercial_partner_id, contact, 'Incorrect commercial entity resolution after setting is_company')
self.assertEqual(company_2.vat, 'BEnew', 'Should not impact parent')
self.assertEqual(contact_dlr.vat, contactvat, 'Parent company stop auto sync')
self.assertEqual(contact_ct.vat, contactvat, 'Parent company stop auto sync')
# writing on parent should not touch child commercial entities
sunhelmvat2 = 'BE0112233453'
sunhelm.write({'vat': sunhelmvat2})
self.assertEqual(p1.vat, p1vat, 'Setting is_company should stop auto-sync of commercial fields')
self.assertEqual(p0.vat, sunhelmvat2, 'Commercial fields must be automatically synced')
company_2.write({'vat': sunhelmvat2})
for partner in contact + contact_ct + contact_dlr:
self.assertEqual(contact.vat, contactvat, 'Setting is_company should stop auto-sync of commercial fields')
for partner in contact2:
self.assertEqual(partner.vat, sunhelmvat2, 'Commercial fields must be automatically synced')
def test_company_dependent_commercial_sync(self):
ResPartner = self.env['res.partner']
@ -754,11 +842,6 @@ class TestPartnerAddressCompany(TransactionCase):
self.assertEqual(child_address.with_company(company_1).barcode, 'Company 1')
self.assertEqual(child_address.with_company(company_2).barcode, 'Company 2')
child_address.parent_id = False
# Reassigning a parent (or a new one) shouldn't fail
child_address.parent_id = test_partner_company.id
def test_company_change_propagation(self):
""" Check propagation of company_id across children """
User = self.env['res.users']
@ -890,6 +973,7 @@ class TestPartnerForm(TransactionCase):
child.name = "Second Child"
self.assertEqual(child.lang, 'fr_FR', "Child contact's lang should be the same as its parent.")
partner = partner_form.save()
self.assertEqual(partner.child_ids.mapped('lang'), ['de_DE', 'fr_FR'])
# check final values (kept from form input)
self.assertEqual(partner.lang, 'fr_FR')
@ -928,8 +1012,11 @@ class TestPartnerRecursion(TransactionCase):
self.p3 = res_partner.create({'name': 'Elmtree Grand-Child 1.1', 'parent_id': self.p2.id})
def test_100_res_partner_recursion(self):
self.assertTrue(self.p3._check_recursion())
self.assertTrue((self.p1 + self.p2 + self.p3)._check_recursion())
self.assertFalse(self.p3._has_cycle())
self.assertFalse((self.p1 + self.p2 + self.p3)._has_cycle())
# special case: empty recordsets don't lead to cycles
self.assertFalse(self.env['res.partner']._has_cycle())
# split 101, 102, 103 tests to force SQL rollback between them
@ -952,6 +1039,11 @@ class TestPartnerRecursion(TransactionCase):
self.p2.write({'child_ids': [Command.update(self.p3.id, {'parent_id': p3b.id}),
Command.update(p3b.id, {'parent_id': self.p3.id})]})
def test_105_res_partner_recursion(self):
with self.assertRaises(ValidationError):
# p3 -> p2 -> p1 -> p2
(self.p3 + self.p1).parent_id = self.p2
def test_110_res_partner_recursion_multi_update(self):
""" multi-write on several partners in same hierarchy must not trigger a false cycle detection """
ps = self.p1 + self.p2 + self.p3
@ -964,3 +1056,13 @@ class TestPartnerRecursion(TransactionCase):
self.p1.parent_id = self.p2
with self.assertRaises(ValidationError):
(self.p3|self.p2).write({'parent_id': self.p1.id})
@tagged('res_partner')
class TestPartnerCategory(TransactionCase):
def test_name_search(self):
category = self.env['res.partner.category'].create({'name': 'buggy_test'})
result = self.env['res.partner.category'].name_search('buggy_test')
self.assertEqual(len(result), 1)
self.assertEqual(result, [(category.id, category.display_name)])

View file

@ -0,0 +1,101 @@
from odoo.tests.common import TransactionCase
class TestMergePartner(TransactionCase):
def setUp(self):
super().setUp()
self.Partner = self.env['res.partner']
self.Bank = self.env['res.partner.bank']
# Create partners
self.partner1 = self.Partner.create({'name': 'Partner 1', 'email': 'partner1@example.com'})
self.partner2 = self.Partner.create({'name': 'Partner 2', 'email': 'partner2@example.com'})
self.partner3 = self.Partner.create({'name': 'Partner 3', 'email': 'partner3@example.com'})
# Create bank accounts
self.bank1 = self.Bank.create({'acc_number': '12345', 'partner_id': self.partner1.id})
self.bank2 = self.Bank.create({'acc_number': '54321', 'partner_id': self.partner2.id})
self.bank3 = self.Bank.create({'acc_number': '12345', 'partner_id': self.partner3.id}) # Duplicate account number
# Create references
self.attachment1 = self.env['ir.attachment'].create({
'name': 'Attachment 1',
'res_model': 'res.partner',
'res_id': self.partner1.id,
})
self.attachment2 = self.env['ir.attachment'].create({
'name': 'Attachment 2',
'res_model': 'res.partner',
'res_id': self.partner2.id,
})
self.attachment_bank1 = self.env['ir.attachment'].create({
'name': 'Attachment Bank 1',
'res_model': 'res.partner.bank',
'res_id': self.bank1.id,
})
self.attachment_bank2 = self.env['ir.attachment'].create({
'name': 'Attachment Bank 2',
'res_model': 'res.partner.bank',
'res_id': self.bank2.id,
})
self.attachment_bank3 = self.env['ir.attachment'].create({
'name': 'Attachment Bank 2',
'res_model': 'res.partner.bank',
'res_id': self.bank3.id,
})
def test_merge_partners_without_bank_accounts(self):
""" Test merging partners without any bank accounts """
partner4 = self.Partner.create({'name': 'Partner 4', 'email': 'partner4@example.com'})
partner5 = self.Partner.create({'name': 'Partner 5', 'email': 'partner5@example.com'})
wizard = self.env['base.partner.merge.automatic.wizard'].create({})
wizard._merge([partner4.id, partner5.id], partner4)
self.assertFalse(partner5.exists(), "Source partner should be deleted after merge")
self.assertTrue(partner4.exists(), "Destination partner should exist after merge")
def test_merge_partners_with_unique_bank_accounts(self):
""" Test merging partners with unique bank accounts """
wizard = self.env['base.partner.merge.automatic.wizard'].create({})
wizard._merge([self.partner1.id, self.partner2.id], self.partner1)
self.assertFalse(self.partner2.exists(), "Source partner should be deleted after merge")
self.assertTrue(self.partner1.exists(), "Destination partner should exist after merge")
self.assertEqual(self.bank1.partner_id, self.partner1, "Bank account should belong to destination partner")
self.assertEqual(self.bank2.partner_id, self.partner1, "Bank account should be reassigned to destination partner")
def test_merge_partners_with_duplicate_bank_accounts(self):
""" Test merging partners with duplicate bank accounts among themselves """
wizard = self.env['base.partner.merge.automatic.wizard'].create({})
src_partners = self.partner1 + self.partner3
wizard._merge((src_partners + self.partner2).ids, self.partner2)
self.assertFalse(src_partners.exists(), "Source partners should be deleted after merge")
self.assertTrue(self.partner2.exists(), "Destination partner should exist after merge")
self.assertRecordValues(self.partner2.bank_ids, [
{'acc_number': '12345'},
{'acc_number': '54321'},
])
self.assertEqual(self.attachment_bank1.res_id, self.bank1.id, "Bank attachment should remain linked to the correct bank account")
self.assertEqual(self.attachment_bank3.res_id, self.bank1.id, "Bank attachment should be reassigned to the correct bank account")
def test_merge_partners_with_duplicate_bank_accounts_with_destination(self):
""" Test merging partners with duplicate bank accounts with the destination partner """
wizard = self.env['base.partner.merge.automatic.wizard'].create({})
wizard._merge([self.partner1.id, self.partner3.id], self.partner1)
self.assertFalse(self.partner3.exists(), "Source partner should be deleted after merge")
self.assertTrue(self.partner1.exists(), "Destination partner should exist after merge")
self.assertEqual(len(self.partner1.bank_ids), 1, "There should be a single bank account after merge")
self.assertIn(self.bank1, self.partner1.bank_ids, "The original bank account of the destination partner should remain")
self.assertFalse(self.bank3.exists(), "The duplicate bank account should have been deleted.")
def test_merge_partners_with_references(self):
""" Test merging partners with references """
wizard = self.env['base.partner.merge.automatic.wizard'].create({})
wizard._merge([self.partner1.id, self.partner2.id], self.partner1)
self.assertFalse(self.partner2.exists(), "Source partner should be deleted after merge")
self.assertTrue(self.partner1.exists(), "Destination partner should exist after merge")
self.assertEqual(self.attachment1.res_id, self.partner1.id, "Attachment should be linked to the destination partner")
self.assertEqual(self.attachment2.res_id, self.partner1.id, "Attachment should be reassigned to the destination partner")

View file

@ -7,8 +7,8 @@ from unittest.mock import patch
from odoo import SUPERUSER_ID
from odoo.addons.base.models.res_users import is_selection_groups, get_selection_groups, name_selection_groups
from odoo.exceptions import UserError, ValidationError
from odoo.service import security
from odoo.tests.common import Form, TransactionCase, new_test_user, tagged, HttpCase, users
from odoo.http import _request_stack
from odoo.tests import Form, TransactionCase, new_test_user, tagged, HttpCase, users
from odoo.tools import mute_logger
@ -241,6 +241,30 @@ class TestUsers(TransactionCase):
@tagged('post_install', '-at_install')
class TestUsers2(TransactionCase):
@classmethod
def setUpClass(cls):
super().setUpClass()
cls.user_employee = cls.env['res.users'].create({
'name': 'employee',
'login': 'employee',
'groups_id': cls.env.ref('base.group_user'),
'tz': 'UTC',
})
def test_change_user_login(self):
""" Check that partner email is updated when changing user's login """
User = self.env['res.users']
with Form(User, view='base.view_users_form') as UserForm:
UserForm.name = "Test User"
UserForm.login = "test-user1"
self.assertFalse(UserForm.email)
UserForm.login = "test-user1@mycompany.example.org"
self.assertEqual(
UserForm.email, "test-user1@mycompany.example.org",
"Setting a valid email as login should update the partner's email"
)
def test_reified_groups(self):
""" The groups handler doesn't use the "real" view with pseudo-fields
@ -321,10 +345,8 @@ class TestUsers2(TransactionCase):
if fname.startswith(('in_group_', 'sel_groups_'))
)
# check that the reified field name has no effect in fields
res_with_reified = User.read_group([], fnames + [reified_fname], ['company_id'])
res_without_reified = User.read_group([], fnames, ['company_id'])
self.assertEqual(res_with_reified, res_without_reified, "Reified fields should be ignored in read_group")
# check that the reified field name is not aggregable
self.assertFalse(User.fields_get([reified_fname], ['aggregator'])[reified_fname].get('aggregator'))
# check that the reified fields are not considered invalid in search_read
# and are ignored
@ -371,6 +393,21 @@ class TestUsers2(TransactionCase):
self.env['res.groups']._update_user_groups_view()
@users('employee')
def test_self_readable_writeable_fields_preferences_form(self):
"""Test that a field protected by a `groups='...'` with a group the user doesn't belong to
but part of the `SELF_WRITEABLE_FIELDS` is shown in the user profile preferences form and is editable"""
my_user = self.env['res.users'].browse(self.env.user.id)
self.assertIn(
'email',
my_user.SELF_WRITEABLE_FIELDS,
"This test doesn't make sense if not tested on a field part of the SELF_WRITEABLE_FIELDS"
)
self.patch(self.env.registry['res.users']._fields['email'], 'groups', 'base.group_system')
with Form(my_user, view='base.view_users_form_simple_modif') as UserForm:
UserForm.email = "foo@bar.com"
self.assertEqual(my_user.email, "foo@bar.com")
@tagged('post_install', '-at_install', 'res_groups')
class TestUsersGroupWarning(TransactionCase):
@ -569,27 +606,33 @@ class TestUsersIdentitycheck(HttpCase):
"""
# Change the password to 8 characters for security reasons
self.env.user.password = "admin@odoo"
# Create a session
# Create a first session that will be used to revoke other sessions
session = self.authenticate('admin', 'admin@odoo')
# Create a second session that will be used to check it has been revoked
self.authenticate('admin', 'admin@odoo')
# Test the session is valid
self.assertTrue(security.check_session(session, self.env))
# Valid session -> not redirected from /web to /web/login
self.assertTrue(self.url_open('/web').url.endswith('/web'))
# Push a fake request to the request stack, because @check_identity requires a request.
# Use the first session created above, used to invalid other sessions than itself.
_request_stack.push(SimpleNamespace(session=session, env=self.env))
self.addCleanup(_request_stack.pop)
# The user clicks the button logout from all devices from his profile
action = self.env.user.action_revoke_all_devices()
# The form of the wizard opens with a new record
form = Form(self.env[action['res_model']].create({}), action['view_id'])
# The form of the check identity wizard opens
form = Form(self.env[action['res_model']].browse(action['res_id']), action.get('view_id'))
# The user fills his password
form.password = 'admin@odoo'
# The user clicks the button "Log out from all devices", which triggers a save then a call to the button method
user_identity_check = form.save()
user_identity_check.revoke_all_devices()
action = user_identity_check.run_check()
# Test the session is no longer valid
self.assertFalse(security.check_session(session, self.env))
# Invalid session -> redirected from /web to /web/login
self.assertTrue(self.url_open('/web').url.endswith('/web/login'))
self.assertTrue(self.url_open('/web').url.endswith('/web/login?redirect=%2Fweb%3F'))
# In addition, the wizard must have been deleted
self.assertFalse(user_identity_check.exists())
# In addition, the password must have been emptied from the wizard
self.assertFalse(user_identity_check.password)

View file

@ -239,6 +239,8 @@ class test_search(TransactionCase):
# test that a custom field x_active filters like active
# we take the model res.country as a test model as it is included in base and does
# not have an active field
self.addCleanup(self.registry.reset_changes) # reset the registry to avoid polluting other tests
model_country = self.env['res.country']
self.assertNotIn('active', model_country._fields) # just in case someone adds the active field in the model
self.env['ir.model.fields'].create({
@ -286,8 +288,19 @@ class test_search(TransactionCase):
self.assertEqual(len(partners) + count_partner_before, Partner.search_count([]))
self.assertEqual(3, Partner.search_count([], limit=3))
def test_22_large_domain(self):
""" Ensure search and its unerlying SQL mechanism is able to handle large domains"""
N = 9500
domain = ['|'] * (N - 1) + [('login', '=', 'admin')] * N
self.env['res.users'].search(domain)
def test_22_like_folding(self):
Model = self.env['res.country']
with self.assertQueries(["""
SELECT "res_country"."id"
FROM "res_country"
WHERE TRUE
ORDER BY "res_country"."name"->>%s
""", """
SELECT "res_country"."id"
FROM "res_country"
WHERE FALSE
ORDER BY "res_country"."name"->>%s
"""]):
Model.search([('code', 'ilike', '')])
Model.search([('code', 'not ilike', '')])

View file

@ -59,7 +59,7 @@ class TestSQL(BaseCase):
def test_sql_idempotence(self):
sql1 = SQL("SELECT id FROM table WHERE foo=%s AND bar=%s", 42, 'baz')
sql2 = SQL(sql1)
self.assertIs(sql1, sql2)
self.assertEqual(sql1, sql2)
def test_sql_unpacking(self):
sql = SQL("SELECT id FROM table WHERE foo=%s AND bar=%s", 42, 'baz')
@ -174,4 +174,3 @@ class TestSqlTools(TransactionCase):
# ensure the definitions match
db_definition = sql.constraint_definition(self.env.cr, 'res_bank', 'test_constraint_dummy')
self.assertEqual(definition, db_definition)

View file

@ -1,29 +1,45 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo.tests import BaseCase, tagged
from odoo.tests import BaseCase, TransactionCase, tagged, BaseCase
from odoo.tests.common import _logger as test_logger
import logging
import os
from unittest.mock import patch
_logger = logging.getLogger(__name__)
class TestRetryCommon(BaseCase):
@classmethod
def setUpClass(cls):
super().setUpClass()
original_runbot = test_logger.runbot
# lower 25 to info to avoid spaming builds with test logs
def runbot(message, *args):
if message.startswith('Retrying'):
return test_logger.info(message, *args)
return original_runbot(message, *args)
patcher = patch.object(test_logger, 'runbot', runbot)
cls.startClassPatcher(patcher)
def get_tests_run_count(self):
return int(os.environ.get('ODOO_TEST_FAILURE_RETRIES', 0)) + 1
return BaseCase._tests_run_count
def update_count(self):
self.count = getattr(self, 'count', 0) + 1
@tagged('-standard', 'test_retry', 'test_retry_success')
@tagged('test_retry', 'test_retry_success')
class TestRetry(TestRetryCommon):
""" Check some tests behaviour when ODOO_TEST_FAILURE_RETRIES is set"""
def test_log_levels(self):
_logger.debug('test debug')
_logger.info('test info')
_logger.runbot('test 25')
def test_retry_success(self):
tests_run_count = self.get_tests_run_count()
@ -42,7 +58,45 @@ class TestRetryFailures(TestRetryCommon):
_logger.error('Failure')
@tagged('-standard', 'test_retry', 'test_retry_success')
@tagged('test_retry', 'test_retry_success')
class TestRetryRollbackedCursor(TestRetryCommon, TransactionCase):
def test_broken_cursor(self):
tests_run_count = self.get_tests_run_count()
self.update_count()
if tests_run_count != self.count:
self.env.cr.rollback()
@tagged('test_retry', 'test_retry_success')
class TestRetryCommitedCursor(TestRetryCommon, TransactionCase):
def test_broken_cursor(self):
tests_run_count = self.get_tests_run_count()
self.update_count()
if tests_run_count != self.count:
self.env.cr.commit()
@tagged('test_retry', 'test_retry_success')
class TestRetryRollbackedCursorError(TestRetryCommon, TransactionCase):
def test_broken_cursor(self):
tests_run_count = self.get_tests_run_count()
self.update_count()
if tests_run_count != self.count:
self.env.cr.rollback()
raise Exception('a')
@tagged('test_retry', 'test_retry_success')
class TestRetryCommitedCursorError(TestRetryCommon, TransactionCase):
def test_broken_cursor(self):
tests_run_count = self.get_tests_run_count()
self.update_count()
if tests_run_count != self.count:
self.env.cr.commit()
raise Exception('a')
@tagged('test_retry', 'test_retry_success')
class TestRetrySubtest(TestRetryCommon):
def test_retry_subtest_success_one(self):
@ -80,3 +134,29 @@ class TestRetrySubtestFailures(TestRetryCommon):
with self.subTest():
_logger.error('Failure')
self.assertFalse(1 == 1)
@tagged('-standard', 'test_retry', 'test_retry_disable')
class TestRetry1Disable(TestRetryCommon):
def test_retry_0_retry_success(self):
tests_run_count = self.get_tests_run_count()
self.update_count()
if tests_run_count != self.count:
raise Exception('Should success on retry')
def test_retry_1_fails(self):
raise Exception('Should fail twice')
def test_retry_2_fails(self):
raise Exception('Should fail without retry 1')
def test_retry_3_fails(self):
raise Exception('Should fail without retry 2')
@tagged('-standard', 'test_retry', 'test_retry_disable')
class TestRetry2Disable(TestRetryCommon):
def test_retry_second_class_fails(self):
raise Exception('Should fail without retry other class')

View file

@ -5,7 +5,6 @@ import contextlib
import difflib
import logging
import re
import sys
from contextlib import contextmanager
from pathlib import PurePath
from unittest import SkipTest, skip
@ -20,14 +19,12 @@ _logger = logging.getLogger(__name__)
from odoo.tests import MetaCase
if sys.version_info >= (3, 8):
# this is mainly to ensure that simple tests will continue to work even if BaseCase should be used
# this only works if doClassCleanup is available on testCase because of the vendoring of suite.py.
# this test will only work in python 3.8 +
class TestTestSuite(TestCase, metaclass=MetaCase):
# this is mainly to ensure that simple tests will continue to work even if BaseCase should be used
# this only works if doClassCleanup is available on testCase because of the vendoring of suite.py.
class TestTestSuite(TestCase, metaclass=MetaCase):
def test_test_suite(self):
""" Check that OdooSuite handles unittest.TestCase correctly. """
def test_test_suite(self):
""" Check that OdooSuite handles unittest.TestCase correctly. """
def get_method_additional_tags(self, method):
return []
@ -145,6 +142,11 @@ class TestRunnerLoggingCommon(TransactionCase):
class TestRunnerLogging(TestRunnerLoggingCommon):
def setUp(self):
old_level = _logger.level
_logger.setLevel(logging.INFO)
self.addCleanup(_logger.setLevel, old_level)
return super().setUp()
def test_has_add_error(self):
self.assertTrue(hasattr(self, '_addError'))
@ -328,9 +330,6 @@ Traceback (most recent call last):
self.fail(msg % (login, count, expected, funcname, filename, linenum))
AssertionError: Query count more than expected for user __system__: 1 > 0 in test_assertQueryCount at base/tests/test_test_suite.py:$line
''')
if self._python_version < (3, 10, 0):
message = message.replace("with self.assertQueryCount(system=0):", "self.env.cr.execute('SELECT 1')")
self.expected_logs = [
(logging.INFO, '=' * 70),
(logging.ERROR, message),

View file

@ -197,7 +197,7 @@ class TranslationToolsTestCase(BaseCase):
source = """<t t-name="stuff">
<ul class="nav navbar-nav">
<li class="nav-item">
<a class="nav-link oe_menu_leaf" href="/web#menu_id=42&amp;action=54">
<a class="nav-link oe_menu_leaf" href="/odoo/action-54?menu_id=42">
<span class="oe_menu_text">Blah</span>
</a>
</li>
@ -340,7 +340,7 @@ class TestLanguageInstall(TransactionCase):
# running the wizard calls _load_module_terms() to load PO files
loaded = []
def _load_module_terms(self, modules, langs, overwrite=False):
def _load_module_terms(self, modules, langs, overwrite=False, imported_module=False):
loaded.append((modules, langs, overwrite))
with patch('odoo.addons.base.models.ir_module.Module._load_module_terms', _load_module_terms):
@ -366,7 +366,6 @@ class TestTranslation(TransactionCase):
def setUpClass(cls):
super().setUpClass()
cls.env['res.lang']._activate_lang('fr_FR')
cls.env.ref('base.module_base')._update_translations(['fr_FR'])
cls.customers = cls.env['res.partner.category'].create({'name': 'Customers'})
cls.customers_xml_id = cls.customers.export_data(['id']).get('datas')[0][0]
@ -382,6 +381,43 @@ class TestTranslation(TransactionCase):
translation_importer.load(f, 'po', 'fr_FR')
translation_importer.save(overwrite=True)
def test_101_translation_read(self):
""" Check the record env.lang behavior """
category = self.customers
self.env['res.lang']._activate_lang('fr_FR')
self.env['res.lang']._activate_lang('nl_NL')
category.with_context(lang='nl_NL').name = 'Klanten'
self.env.ref('base.lang_nl').active = False
category.invalidate_recordset()
self.assertEqual(category.with_context(lang=None).name, 'Customers')
category.invalidate_recordset()
self.assertEqual(category.with_context(lang='en_US').name, 'Customers')
category.invalidate_recordset()
self.assertEqual(category.with_context(lang='fr_FR').name, 'Clients')
with self.assertRaises(UserError):
# inactive language
category.with_context(lang='nl_NL').name
with self.assertRaises(UserError):
# non-existing language
category.with_context(lang='Dummy').name
with self.assertRaises(UserError):
# technical langauge starts with '_'
category.with_context(lang='_en_US').name
with self.assertRaises(UserError):
# SQL injection language
category.with_context(lang="'', NOW(").name
# lang as en_US and None are always readable even when en_US is not activated
self.env['res.partner'].with_context(active_test=False).search([]).write({'lang': 'fr_FR'})
self.env.ref('base.lang_en').active = False
category.invalidate_recordset()
self.assertEqual(category.with_context(lang=None).name, 'Customers')
category.invalidate_recordset()
self.assertEqual(category.with_context(lang='en_US').name, 'Customers')
def test_101_create_translated_record(self):
category = self.customers.with_context({})
self.assertEqual(category.name, 'Customers', "Error in basic name")
@ -508,16 +544,12 @@ class TestTranslation(TransactionCase):
self.assertTrue(self.env.ref('base.lang_fr').active)
category_fr = category_en.with_context(lang='fr_FR')
self.assertFalse(self.env.ref('base.lang_zh_CN').active)
category_zh = category_en.with_context(lang='zh_CN')
self.env['res.partner'].with_context(active_test=False).search([]).write({'lang': 'fr_FR'})
self.env.ref('base.lang_en').active = False
category_fr.with_context(prefetch_langs=True).name
category_nl.name
category_en.name
category_zh.name
category_fr.invalidate_recordset()
with self.assertQueryCount(1):
@ -526,7 +558,6 @@ class TestTranslation(TransactionCase):
with self.assertQueryCount(0):
self.assertEqual(category_nl.name, 'Klanten')
self.assertEqual(category_en.name, 'Customers')
self.assertEqual(category_zh.name, 'Customers')
# TODO Currently, the unique constraint doesn't work for translatable field
@ -580,6 +611,24 @@ class TestTranslationWrite(TransactionCase):
category3.with_context(lang='en_US').name = 'English 2'
self.assertEqual(category3.with_context(lang='fr_FR').name, 'French 2')
with self.assertRaises(UserError):
# Illegal context lang starts with "_" should raise UserError
category.with_context(lang='_en_US').name = '_Customers'
def test_01_invalid_lang(self):
self.env['res.lang']._activate_lang('nl_NL')
self.category.with_context(lang='nl_NL').name = 'Reblochon nl_NL'
self.env.ref('base.lang_nl').active = False
# [inactive_lang, non_existing_lang, technical_lang, sql_injection_lang]
langs = ['nl_NL', 'Dummy', '_en_US', "'', NOW("]
for lang in langs:
with self.assertRaises(UserError):
self.category.with_context(lang=lang).name = 'new value'
with self.assertRaises(UserError):
self.category.with_context(lang=lang).create({'name': 'new value'})
def test_03_fr_single(self):
self.env['res.lang']._activate_lang('fr_FR')
self.env['res.partner'].with_context(active_test=False).search([]).write({'lang': 'fr_FR'})
@ -872,7 +921,7 @@ class TestTranslationWrite(TransactionCase):
# check that get_views() also returns the expected label
info = model.get_views([(False, 'form')])
self.assertEqual(info['models'][model._name]['name']['string'], LABEL)
self.assertEqual(info['models'][model._name]["fields"]['name']['string'], LABEL)
class TestXMLTranslation(TransactionCase):
@ -881,7 +930,6 @@ class TestXMLTranslation(TransactionCase):
super().setUpClass()
cls.env['res.lang']._activate_lang('fr_FR')
cls.env['res.lang']._activate_lang('nl_NL')
cls.env.ref('base.module_base')._update_translations(['fr_FR', 'nl_NL'])
def create_view(self, archf, terms, **kwargs):
view = self.env['ir.ui.view'].create({
@ -1305,7 +1353,7 @@ class TestXMLTranslation(TransactionCase):
view.update_field_translations('arch_db', {
'en_US': {'Fork': 'Fork2'},
'fr_FR': {'Fourchette': 'Fourchette2'}
'fr_FR': {'Fork': 'Fourchette2'}
})
self.assertEqual(view.arch_db, '<form string="X">Bread and cheese<div>Fork2</div></form>')
@ -1332,7 +1380,7 @@ class TestXMLTranslation(TransactionCase):
def test_delay_translations(self):
archf = '<form string="%s"><div>%s</div><div>%s</div></form>'
terms_en = ('Knife', 'Fork', 'Spoon')
terms_fr = ('Couteau', 'Fourchette', 'Cuiller')
terms_fr = ('Couteau', '<i>Fourchette</i>', 'Cuiller')
view0 = self.create_view(archf, terms_en, fr_FR=terms_fr)
archf2 = '<form string="%s"><p>%s</p><div>%s</div></form>'
@ -1362,7 +1410,7 @@ class TestXMLTranslation(TransactionCase):
f'data-oe-id=&quot;{view0.id}&quot; '
'data-oe-field=&quot;arch_db&quot; '
'data-oe-translation-state=&quot;to_translate&quot; '
f'data-oe-translation-initial-sha=&quot;{sha256(terms_en2[0].encode()).hexdigest()}&quot;'
f'data-oe-translation-source-sha=&quot;{sha256(terms_en2[0].encode()).hexdigest()}&quot;'
'&gt;'
f'{terms_en2[0]}'
'&lt;/span&gt;"'
@ -1374,7 +1422,7 @@ class TestXMLTranslation(TransactionCase):
f'data-oe-id="{view0.id}" '
'data-oe-field="arch_db" '
'data-oe-translation-state="translated" '
f'data-oe-translation-initial-sha="{sha256(terms_fr[1].encode()).hexdigest()}"'
f'data-oe-translation-source-sha="{sha256(terms_en2[1].encode()).hexdigest()}"'
'>'
f'{terms_fr[1]}'
'</span>'
@ -1386,7 +1434,7 @@ class TestXMLTranslation(TransactionCase):
f'data-oe-id="{view0.id}" '
'data-oe-field="arch_db" '
'data-oe-translation-state="translated" '
f'data-oe-translation-initial-sha="{sha256(terms_fr[2].encode()).hexdigest()}"'
f'data-oe-translation-source-sha="{sha256(terms_en2[2].encode()).hexdigest()}"'
'>'
f'{terms_fr[2]}'
'</span>'
@ -1440,6 +1488,257 @@ class TestXMLTranslation(TransactionCase):
f'arch_db for {lang} should be {archf2} when check_translations'
)
def test_update_field_translations_source_lang(self):
""" call update_field_translations with source_lang """
archf = '<form string="%s"><div>%s</div><div>%s</div></form>'
terms_en = ('Knife', 'Fork', 'Spoon')
view1 = self.create_view(archf, terms_en)
# update when fr_FR is not in the jsonb
# the fr_FR source value falls back to the en_US value
view1.update_field_translations('arch_db', {
'fr_FR': {
'Knife': 'Couteau',
'Fork': 'Fourchette'
},
}, source_lang='fr_FR')
self.assertEqual(view1.with_context(lang='en_US').arch_db, archf % ('Knife', 'Fork', 'Spoon'))
self.assertEqual(view1.with_context(lang='fr_FR').arch_db, archf % ('Couteau', 'Fourchette', 'Spoon'))
view1.update_field_translations('arch_db', {
'en_US': {
'Couteau': 'knife',
'Fourchette': 'fork',
'Spoon': 'spoon'
},
'fr_FR': {
'Spoon': 'Cuiller'
},
}, source_lang='fr_FR')
self.assertEqual(view1.with_context(lang='en_US').arch_db, archf % ('knife', 'fork', 'spoon'))
self.assertEqual(view1.with_context(lang='fr_FR').arch_db, archf % ('Couteau', 'Fourchette', 'Cuiller'))
def test_update_field_translations_empty_str(self):
""" translate a value to empty will fall back it to the source """
archf = '<form string="%s"><div>%s</div><div>%s</div></form>'
terms_en = ('Knife', 'Fork', 'Spoon')
terms_fr = ('Couteau', 'Fourchette', 'Cuiller')
view1 = self.create_view(archf, terms_en, fr_FR=terms_fr)
view1.update_field_translations('arch_db', {
'en_US': {
'Fork': 'fork',
'Spoon': ''
},
'fr_FR': {
'Knife': '',
'Fork': False
}
})
self.assertEqual(view1.with_context(lang='en_US').arch_db, archf % ('Knife', 'fork', 'Spoon'))
self.assertEqual(view1.with_context(lang='fr_FR').arch_db, archf % ('Knife', 'Fork', 'Cuiller'))
def test_update_field_translations_partially(self):
# partially translate en_US
xml = '<form><div>%s</div><div>%s</div></form>'
# xml developed in fr_FR
view1 = self.env['ir.ui.view'].with_context(lang='fr_FR').create({
'name': 'view_1',
'model': 'res.partner',
'arch': xml % ('Pomme', 'Poire') # with typo
}) # with typo
# jsonb column value:
# {
# "en_US": "<form><div>Pomme</div><div>Banane</div></form>",
# "fr_FR": "<form><div>Pomme</div><div>Banane</div></form>"
# }
view1_us = view1.with_context(lang='en_US')
view1_fr = view1.with_context(lang='fr_FR')
view1.update_field_translations('arch_db', {'en_US': {'Pomme': 'Apple'}})
self.assertEqual(view1_us.arch_db, xml % ('Apple', 'Poire'))
self.assertEqual(view1_fr.arch_db, xml % ('Pomme', 'Poire'))
view1.update_field_translations('arch_db', {'en_US': {'Poire': 'Pear'}})
self.assertEqual(view1_us.arch_db, xml % ('Apple', 'Pear')) # all en_US terms should be translated
self.assertEqual(view1_fr.arch_db, xml % ('Pomme', 'Poire')) # fr_FR shouldn't be changed
def test_update_field_translations_typofix(self):
# as a side effect of the behavior in test_update_field_translations_partially
# term update in one language won't be populated to other translated language values
self.env['res.lang']._activate_lang('en_GB')
# fix typo / update terms in en_US
xml = '<form><div>%s</div><div>%s</div><div>%s</div></form>'
# xml developed in en_GB
view1 = self.env['ir.ui.view'].with_context(lang='en_GB').create({
'name': 'view_1',
'model': 'res.partner',
'arch': xml % ('Footbell', 'Clbus', 'Rakning') # with typo
})
view1.update_field_translations('arch_db', {'en_US': {'Footbell': 'SocceR'}}) # still with a typo
# jsonb column value:
# {
# "en_US": "<form><div>SocceR</div><div>Clbus</div><div>Rakning</div></form>",
# "en_GB": "<form><div>Footbell</div><div>Clbus</div><div>Rakning</div></form>"
# }
view1_us = view1.with_context(lang='en_US')
view1_gb = view1.with_context(lang='en_GB')
view1_fr = view1.with_context(lang='fr_FR')
# fix "Clbus" in en_GB
view1.update_field_translations('arch_db', {'en_GB': {'Clbus': 'Clubs'}})
self.assertEqual(view1_us.arch_db, xml % ('SocceR', 'Clbus', 'Rakning')) # nothing should be fixed in en_US
self.assertEqual(view1_gb.arch_db, xml % ('Footbell', 'Clubs', 'Rakning')) # "Clbus" should be fixed in en_GB
self.assertEqual(view1_fr.arch_db, xml % ('SocceR', 'Clbus', 'Rakning')) # fr_FR should fall back to en_US
# fix "SocceR" in en_US and "Footbell" in en_GB
view1.update_field_translations('arch_db', {'en_US': {'SocceR': 'Soccer'}, 'en_GB': {'SocceR': 'Football'}}) # always use old_en_terms to update
self.assertEqual(view1_us.arch_db, xml % ('Soccer', 'Clbus', 'Rakning')) # "SocceR" should be fixed in en_US
self.assertEqual(view1_gb.arch_db, xml % ('Football', 'Clubs', 'Rakning')) # "Clbus" should be fixed in en_GB
self.assertEqual(view1_fr.arch_db, xml % ('Soccer', 'Clbus', 'Rakning'))
# fix "Rakning" in en_US
view1.update_field_translations('arch_db', {'en_US': {'Rakning': 'Ranking'}})
self.assertEqual(view1_us.arch_db, xml % ('Soccer', 'Clbus', 'Ranking')) # "Rakning" should be fixed in en_US
self.assertEqual(view1_gb.arch_db, xml % ('Football', 'Clubs', 'Rakning')) # "Rakning" isn't fixed in en_GB
self.assertEqual(view1_us.arch_db, xml % ('Soccer', 'Clbus', 'Ranking')) # fr_FR should fall back to en_US
class TestXMLDuplicateTranslations(TransactionCase):
"""
duplicate translations are not supported
this test is only used to describe all tricky behaviours
"""
@classmethod
def setUpClass(cls):
super().setUpClass()
cls.env['res.lang']._activate_lang('fr_FR')
cls.env['res.lang']._activate_lang('es_ES')
cls.xml = '<form><div>%s</div><div>%s</div></form>'
cls.view1 = cls.env['ir.ui.view'].with_context(lang='fr_FR').create({
'name': 'view_1',
'model': 'res.partner',
'arch': cls.xml % ('un étudiant', 'une étudiante')
})
# jsonb column value:
# {
# "en_US": "<form><div>un étudiant</div><div>une étudiante</div></form>",
# "fr_FR": "<form><div>un étudiant</div><div>une étudiante</div></form>",
# }
cls.view1_en = cls.view1.with_context(lang='en_US')
cls.view1_fr = cls.view1.with_context(lang='fr_FR')
cls.view1_es = cls.view1.with_context(lang='es_ES')
# translate 2 fr_FR terms to one en_US term
cls.view1.update_field_translations('arch_db', {
'en_US': {'un étudiant': 'a student', 'une étudiante': 'a student'},
'es_ES': {'un étudiant': 'un estudiante', 'une étudiante': 'una estudiante'}
})
# intuitive behaviour
def test_update_field_translations_result(self):
# the field value for en_US has 2 same terms
self.assertEqual(self.view1_en.arch_db, self.xml % ('a student', 'a student'))
# the field value for fr_FR has two different terms
self.assertHTMLEqual(self.view1_fr.arch_db, self.xml % ('un étudiant', 'une étudiante'))
# the field value for es_SP has two different terms
self.assertHTMLEqual(self.view1_es.arch_db, self.xml % ('un estudiante', 'una estudiante'))
# jsonb column value:
# {
# "en_US": "<form><div>a student</div><div>a student</div></form>",
# "fr_FR": "<form><div>un étudiant</div><div>une étudiante</div></form>",
# "es_ES": "<form><div>un estudiante</div><div>una estudiante</div></form>"
# }
# tricky behaviour
def test_update_field_translations_again(self):
# confirm translation for es_SP
self.view1.update_field_translations('arch_db', {'es_ES': {}})
self.assertEqual(self.view1_en.arch_db, self.xml % ('a student', 'a student'))
self.assertEqual(self.view1_fr.arch_db, self.xml % ('un étudiant', 'une étudiante'))
self.assertEqual(self.view1_es.arch_db, self.xml % ('un estudiante', 'una estudiante'))
# capitalize the en_US
self.view1.update_field_translations('arch_db', {'en_US': {'a student': 'A STUDENT'}})
self.assertEqual(self.view1_en.arch_db, self.xml % ('A STUDENT', 'A STUDENT'))
self.assertEqual(self.view1_fr.arch_db, self.xml % ('un étudiant', 'une étudiante'))
self.assertEqual(self.view1_es.arch_db, self.xml % ('un estudiante', 'una estudiante'))
# capitalize 'un étudiant' in fr_FR only
self.view1.update_field_translations('arch_db', {'fr_FR': {'A STUDENT': 'UNE ÉTUDIANTE'}})
self.assertEqual(self.view1_en.arch_db, self.xml % ('A STUDENT', 'A STUDENT'))
self.assertEqual(self.view1_fr.arch_db, self.xml % ('UNE ÉTUDIANTE', 'UNE ÉTUDIANTE')) # 'un étudiant' is dropped
self.assertEqual(self.view1_es.arch_db, self.xml % ('un estudiante', 'una estudiante'))
# tricky behaviour
def test_get_field_translations(self):
""" open the translation dialog from the form view
the field value and the translation dialog / translation mapping are
not consistent
"""
# there is only one term for fr_FR in the translation dialog
# {'lang': 'fr_FR', 'src': 'a student', 'value': 'un étudiant'} is missing
# because the fr_FR term 'une étudiante' appears after the 'un étudiant' and overwrites the translation mapping
# same for the es_SP
self.assertItemsEqual(self.view1.get_field_translations('arch_db')[0], [
{'lang': 'en_US', 'source': 'a student', 'value': ''},
{'lang': 'fr_FR', 'source': 'a student', 'value': 'une étudiante'},
{'lang': 'es_ES', 'source': 'a student', 'value': 'una estudiante'}
])
# tricky behaviour
def test_write(self):
""" add one more div without changing any term
translations are lost when 2 other language terms are translated to the
same term in the current language value
"""
# write in fr_FR
new_xml1 = '<form><div>%s</div><div>%s</div><div/></form>'
self.view1_fr.arch = new_xml1 % ('un étudiant', 'une étudiante')
self.assertEqual(self.view1_en.arch_db, new_xml1 % ('a student', 'a student'))
self.assertEqual(self.view1_fr.arch_db, new_xml1 % ('un étudiant', 'une étudiante'))
self.assertEqual(self.view1_es.arch_db, new_xml1 % ('un estudiante', 'una estudiante'))
# write in en_US
new_xml2 = '<form><div>%s</div><div>%s</div><div/><div/></form>'
self.view1_en.arch = new_xml2 % ('a student', 'a student')
self.assertEqual(self.view1_en.arch_db, new_xml2 % ('a student', 'a student'))
self.assertEqual(self.view1_fr.arch_db, new_xml2 % ('une étudiante', 'une étudiante')) # 'un étudiant' is dropped
self.assertEqual(self.view1_es.arch_db, new_xml2 % ('una estudiante', 'una estudiante')) # 'un estudiante' is dropped
# tricky behaviour
def test_copy(self):
""" copy record
translations are lost when 2 other language terms are translated to the
same term in the current language value
"""
# copy translated field means
# 1. create with the current language value
# 2. use the translation mapping from current_lang_terms to other_lang_terms to update_field_translations
# copy the record in fr_FR
view1_fr_copy = self.view1_fr.copy({'name': 'view1_fr_copy'})
# view1_en_copy.update_field_translations('arch_db', {
# 'en_US': {'un étudiant': 'a student', 'une étudiante': 'a student'},
# 'es_ES': {'un étudiant': 'un estudiante', 'une étudiante': 'una estudiante'},
# })
# is called when copy
self.assertEqual(view1_fr_copy.with_context(lang='en_US').arch_db, self.xml % ('a student', 'a student'))
self.assertEqual(view1_fr_copy.arch_db, self.xml % ('un étudiant', 'une étudiante'))
self.assertEqual(view1_fr_copy.with_context(lang='es_ES').arch_db, self.xml % ('un estudiante', 'una estudiante'))
# copy the record in en_US
view1_en_copy = self.view1_en.copy({'name': 'view1_us_copy'})
# view1_en_copy.update_field_translations('arch_db', {
# 'fr_FR': {'a student': 'une étudiante'},
# 'es_ES': {'a student': ''una estudiante'},
# })
# is called when copy
self.assertEqual(view1_en_copy.arch_db, self.xml % ('a student', 'a student'))
self.assertEqual(view1_en_copy.with_context(lang='fr_FR').arch_db, self.xml % ('une étudiante', 'une étudiante')) # 'un étudiant' is dropped
self.assertEqual(view1_en_copy.with_context(lang='es_ES').arch_db, self.xml % ('una estudiante', 'una estudiante')) # 'un estudiante' is dropped
class TestHTMLTranslation(TransactionCase):
def test_write_non_existing(self):

View file

@ -4,7 +4,7 @@ import pytz
from unittest.mock import patch
from odoo.tests.common import TransactionCase
from odoo.tools._monkeypatches_pytz import _tz_mapping
from odoo._monkeypatches.pytz import _tz_mapping
_logger = logging.getLogger(__name__)

View file

@ -6,7 +6,7 @@
from contextlib import contextmanager
import unittest
from odoo import api, registry, SUPERUSER_ID
from odoo import api, SUPERUSER_ID
from odoo.tests import common
from odoo.tests.common import BaseCase
@ -18,7 +18,7 @@ def environment():
""" Return an environment with a new cursor for the current database; the
cursor is committed and closed after the context block.
"""
reg = registry(common.get_db_name())
reg = Registry(common.get_db_name())
with reg.cursor() as cr:
yield api.Environment(cr, SUPERUSER_ID, {})

View file

@ -0,0 +1,49 @@
import subprocess as sp
import sys
from os.path import join as opj, realpath
from odoo.tools import config
from odoo.tests import BaseCase
class TestCommand(BaseCase):
@classmethod
def setUpClass(cls):
super().setUpClass()
cls.odoo_bin = realpath(opj(__file__, '../../../../../odoo-bin'))
def run_command(self, *args, check=True, capture_output=True, text=True, **kwargs):
return sp.run(
[
sys.executable,
self.odoo_bin,
f'--addons-path={config["addons_path"]}',
*args,
],
capture_output=capture_output,
check=check,
text=text,
**kwargs
)
def test_upgrade_code_example(self):
proc = self.run_command('upgrade_code', '--script', '17.5-00-example', '--dry-run')
self.assertFalse(proc.stdout, "there should be no file modified by the example script")
self.assertFalse(proc.stderr)
def test_upgrade_code_help(self):
proc = self.run_command('upgrade_code', '--help')
self.assertIn("usage: ", proc.stdout)
self.assertIn("Rewrite the entire source code", proc.stdout)
self.assertFalse(proc.stderr)
def test_upgrade_code_standalone(self):
from odoo.cli import upgrade_code # noqa: PLC0415
proc = sp.run(
[sys.executable, upgrade_code.__file__, '--help'],
check=True, capture_output=True, text=True
)
self.assertIn("usage: ", proc.stdout)
self.assertIn("Rewrite the entire source code", proc.stdout)
self.assertFalse(proc.stderr)

View file

@ -2,7 +2,7 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo.tests.common import TransactionCase
from odoo.exceptions import ValidationError
from odoo.exceptions import AccessError, ValidationError
from odoo import Command
@ -33,13 +33,13 @@ class TestHasGroup(TransactionCase):
self.grp_public = self.env.ref(self.grp_public_xml_id)
def test_env_uid(self):
Users = self.env['res.users'].with_user(self.test_user)
Partner = self.env['res.partner'].with_user(self.test_user)
self.assertTrue(
Users.has_group(self.group0),
Partner.env.user.has_group(self.group0),
"the test user should belong to group0"
)
self.assertFalse(
Users.has_group(self.group1),
Partner.env.user.has_group(self.group1),
"the test user should *not* belong to group1"
)
@ -53,6 +53,19 @@ class TestHasGroup(TransactionCase):
"the test user shoudl not belong to group1"
)
def test_other_user(self):
internal_user = self.test_user.copy({'groups_id': self.grp_internal})
internal_user = internal_user.with_user(internal_user)
test_user = self.env['res.users'].with_user(self.test_user).browse(self.test_user.id)
test_user.has_group(self.group0)
with self.assertRaises(AccessError):
test_user.browse(internal_user.id).has_group(self.group0)
test_user.sudo().browse(internal_user.id).has_group(self.group0)
internal_user.has_group(self.group0)
internal_user.browse(test_user.id).has_group(self.group0)
def test_portal_creation(self):
"""Here we check that portal user creation fails if it tries to create a user
who would also have group_user by implied_group.
@ -288,21 +301,30 @@ class TestHasGroup(TransactionCase):
def populate_cache():
self.test_user.has_group('test_user_has_group.group0')
self.assertTrue(self.registry._Registry__caches['default'], "user.has_group cache must be populated")
self.assertTrue(self.registry._Registry__caches['default'], "user._has_group cache must be populated")
populate_cache()
self.env.ref(self.group0).write({"share": True})
self.assertFalse(self.registry._Registry__caches['default'], "Writing on group must invalidate user.has_group cache")
self.assertFalse(self.registry._Registry__caches['default'], "Writing on group must invalidate user._has_group cache")
populate_cache()
# call_cache_clearing_methods is called in res.groups.write to invalidate
# cache before calling its parent class method (`odoo.models.Model.write`)
# as explain in the `res.group.write` comment.
# This verifies that calling `call_cache_clearing_methods()` invalidates
# the ormcache of method `user.has_group()`
# the ormcache of method `user._has_group()`
self.env['ir.model.access'].call_cache_clearing_methods()
self.assertFalse(
self.registry._Registry__caches['default'],
"call_cache_clearing_methods() must invalidate user.has_group cache"
"call_cache_clearing_methods() must invalidate user._has_group cache"
)
def test_has_group_with_new_id(self):
user = self.env['res.users'].new({'partner_id': self.test_user.partner_id.id})
self.assertEqual(user.has_group(self.group0), False)
self.assertEqual(user.has_group(self.group1), False)
user2 = self.env['res.users'].new({'partner_id': self.test_user.partner_id.id}, origin=self.test_user)
self.assertEqual(user2.has_group(self.group0), True)
self.assertEqual(user2.has_group(self.group1), False)

File diff suppressed because it is too large Load diff

View file

@ -1,8 +1,8 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
import collections
import datetime
import time
from xmlrpc.client import Binary
from odoo.exceptions import AccessDenied, AccessError
from odoo.http import _request_stack
@ -12,6 +12,7 @@ import odoo.tools
from odoo.tests import common
from odoo.service import common as auth, model
from odoo.tools import DotDict
from odoo.api import call_kw
@common.tagged('post_install', '-at_install')
@ -42,6 +43,28 @@ class TestXMLRPC(common.HttpCase):
ids = o.execute(db_name, self.admin_uid, 'admin', 'ir.model', 'search', [], {})
self.assertIsInstance(ids, list)
def test_xmlrpc_datetime(self):
""" Test that native datetime can be sent over xmlrpc
"""
m = self.env.ref('base.model_res_device_log')
self.env['ir.model.access'].create({
'name': "w/e",
'model_id': m.id,
'perm_read': True,
'perm_create': True,
})
now = datetime.datetime.now()
ids = self.xmlrpc(
'res.device.log', 'create',
{'session_identifier': "abc", 'first_activity': now}
)
[r] = self.xmlrpc(
'res.device.log', 'read',
ids, ['first_activity'],
)
self.assertEqual(r['first_activity'], now.isoformat(" ", "seconds"))
def test_xmlrpc_read_group(self):
groups = self.xmlrpc_object.execute(
common.get_db_name(), self.admin_uid, 'admin',
@ -108,7 +131,7 @@ class TestXMLRPC(common.HttpCase):
)
def _json_call(self, *args):
self.opener.post("http://%s:%s/jsonrpc" % (common.HOST, odoo.tools.config['http_port']), json={
self.opener.post(f"{self.base_url()}/jsonrpc", json={
'jsonrpc': '2.0',
'id': None,
'method': 'call',
@ -153,6 +176,7 @@ class TestAPIKeys(common.HttpCase):
'cookies': {},
'args': {},
}),
'cookies': {},
# bypass check_identity flow
'session': {'identity-check-last': time.time()},
'geoip': {},
@ -200,6 +224,14 @@ class TestAPIKeys(common.HttpCase):
])
self.assertEqual(ctx['tz'], 'Australia/Eucla')
api_key = call_kw(
model=self.env['res.users.apikeys.description'],
name='create',
args=[{'name': 'Name of the key'}],
kwargs={}
)
self.assertTrue(isinstance(api_key, int))
def test_delete(self):
env = self.env(user=self._user)
env['res.users.apikeys.description'].create({'name': 'b',}).make_key()