mirror of
https://github.com/bringout/oca-ocb-core.git
synced 2026-04-20 03:32:00 +02:00
18.0 vanilla
This commit is contained in:
parent
d72e748793
commit
0a7ae8db93
337 changed files with 399651 additions and 232598 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
82
odoo-bringout-oca-ocb-base/odoo/addons/base/tests/config/cli
Normal file
82
odoo-bringout-oca-ocb-base/odoo/addons/base/tests/config/cli
Normal 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
|
||||
|
|
@ -0,0 +1 @@
|
|||
[options]
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -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'})
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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')
|
||||
|
|
|
|||
698
odoo-bringout-oca-ocb-base/odoo/addons/base/tests/test_groups.py
Normal file
698
odoo-bringout-oca-ocb-base/odoo/addons/base/tests/test_groups.py
Normal 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)]
|
||||
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
|
@ -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'))
|
||||
|
|
|
|||
|
|
@ -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({
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -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'],
|
||||
),
|
||||
])
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
"""
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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'",
|
||||
])
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
|
|
|
|||
|
|
@ -42,6 +42,7 @@ class TestModuleManifest(BaseCase):
|
|||
'auto_install': False,
|
||||
'bootstrap': False,
|
||||
'category': 'Uncategorized',
|
||||
'cloc_exclude': [],
|
||||
'configurator_snippets': {},
|
||||
'countries': [],
|
||||
'data': [],
|
||||
|
|
|
|||
|
|
@ -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}')
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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']
|
||||
|
|
|
|||
|
|
@ -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")')
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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]
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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]
|
||||
|
||||
|
|
|
|||
|
|
@ -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')
|
||||
|
||||
|
|
|
|||
|
|
@ -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')
|
||||
|
|
|
|||
|
|
@ -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)])
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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', '')])
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
||||
|
|
|
|||
|
|
@ -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')
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
|
|
|
|||
|
|
@ -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&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="{view0.id}" '
|
||||
'data-oe-field="arch_db" '
|
||||
'data-oe-translation-state="to_translate" '
|
||||
f'data-oe-translation-initial-sha="{sha256(terms_en2[0].encode()).hexdigest()}"'
|
||||
f'data-oe-translation-source-sha="{sha256(terms_en2[0].encode()).hexdigest()}"'
|
||||
'>'
|
||||
f'{terms_en2[0]}'
|
||||
'</span>"'
|
||||
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -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__)
|
||||
|
||||
|
|
|
|||
|
|
@ -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, {})
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
@ -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
|
|
@ -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()
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue