vanilla 17.0

This commit is contained in:
Ernad Husremovic 2025-10-08 10:47:08 +02:00
parent d72e748793
commit a9bcec8e91
1986 changed files with 1613876 additions and 568976 deletions

View file

@ -9,6 +9,7 @@ from . import test_menu
from . import test_click_everywhere
from . import test_base_document_layout
from . import test_load_menus
from . import test_partner
from . import test_profiler
from . import test_session_info
from . import test_read_progress_bar
@ -16,6 +17,11 @@ from . import test_assets
from . import test_assets_xml
from . import test_login
from . import test_web_search_read
from . import test_web_read_group
from . import test_domain
from . import test_translate
from . import test_web_redirect
from . import test_res_users
from . import test_webmanifest
from . import test_ir_qweb
from . import test_reports

View file

@ -17,26 +17,31 @@ _logger = logging.getLogger(__name__)
class TestAssetsGenerateTimeCommon(odoo.tests.TransactionCase):
def generate_bundles(self):
self.env['ir.attachment'].search([('url', '=like', '/web/assets/%')]).unlink() # delete existing attachement
def generate_bundles(self, unlink=True):
if unlink:
self.env['ir.attachment'].search([('url', '=like', '/web/assets/%')]).unlink() # delete existing attachement
installed_module_names = self.env['ir.module.module'].search([('state', '=', 'installed')]).mapped('name')
bundles = {
key
for module in installed_module_names
for key in get_manifest(module)['assets']
for key in get_manifest(module).get('assets', [])
}
for bundle in bundles:
for bundle_name in bundles:
with mute_logger('odoo.addons.base.models.assetsbundle'):
for assets_type in 'css', 'js':
try:
start_t = time.time()
css = assets_type == 'css'
js = assets_type == 'js'
self.env['ir.qweb']._generate_asset_nodes(bundle, css=css, js=js)
yield (f'{bundle}.{assets_type}', time.time() - start_t)
bundle = self.env['ir.qweb']._get_asset_bundle(bundle_name, css=css, js=js)
if assets_type == 'css' and bundle.stylesheets:
bundle.css()
if assets_type == 'js' and bundle.javascripts:
bundle.js()
yield (f'{bundle_name}.{assets_type}', time.time() - start_t)
except ValueError:
_logger.info('Error detected while generating bundle %r %s', bundle, assets_type)
_logger.info('Error detected while generating bundle %r %s', bundle_name, assets_type)
@odoo.tests.tagged('post_install', '-at_install', 'assets_bundle')
@ -47,9 +52,33 @@ class TestLogsAssetsGenerateTime(TestAssetsGenerateTimeCommon):
The purpose of this test is to monitor the time of assets bundle generation.
This is not meant to test the generation failure, hence the try/except and the mute logger.
"""
for bundle, duration in self.generate_bundles():
for bundle, duration in list(self.generate_bundles()):
_logger.info('Bundle %r generated in %.2fs', bundle, duration)
def test_logs_assets_check_time(self):
"""
The purpose of this test is to monitor the time of assets bundle generation.
This is not meant to test the generation failure, hence the try/except and the mute logger.
"""
start = time.time()
for bundle, duration in self.generate_bundles(False):
_logger.info('Bundle %r checked in %.2fs', bundle, duration)
duration = time.time() - start
_logger.info('All bundle checked in %.2fs', duration)
@odoo.tests.tagged('post_install', '-at_install', '-standard', 'test_assets')
class TestPregenerateTime(HttpCase):
def test_logs_pregenerate_time(self):
self.env['ir.qweb']._pregenerate_assets_bundles()
start = time.time()
self.env.registry.clear_cache()
self.env.cache.invalidate()
with self.profile(collectors=['sql', odoo.tools.profiler.PeriodicCollector(interval=0.01)], disable_gc=True):
self.env['ir.qweb']._pregenerate_assets_bundles()
duration = time.time() - start
_logger.info('All bundle checked in %.2fs', duration)
@odoo.tests.tagged('post_install', '-at_install', '-standard', 'assets_bundle')
class TestAssetsGenerateTime(TestAssetsGenerateTimeCommon):
@ -73,6 +102,7 @@ class TestAssetsGenerateTime(TestAssetsGenerateTimeCommon):
class TestLoad(HttpCase):
def test_assets_already_exists(self):
self.authenticate('admin', 'admin')
# TODO xdo adapt this test. url open won't generate attachment anymore even if not pregenerated
_save_attachment = odoo.addons.base.models.assetsbundle.AssetsBundle.save_attachment
def save_attachment(bundle, extension, content):

View file

@ -63,19 +63,13 @@ class TestStaticInheritanceCommon(odoo.tests.TransactionCase):
def renderBundle(self, debug=False):
files = []
for url in self.template_files:
atype = 'text/xml'
if '.js' in url:
atype = 'text/javascript'
files.append({
'atype': atype,
'url': url,
'filename': url,
'content': None,
'media': None,
})
asset = AssetsBundle('web.test_bundle', files, env=self.env, css=False, js=True)
# to_node return the files descriptions and generate attachments.
asset.to_node(css=False, js=False, debug=debug and 'assets' or '')
asset = AssetsBundle('web.test_bundle', files, env=self.env, css=False, js=True, debug_assets=debug)
content = asset.xml(show_inherit_info=debug)
return f'<templates xml:space="preserve">\n{content}\n</templates>'

View file

@ -2,7 +2,10 @@
import logging
import odoo.tests
from datetime import datetime
from odoo.addons.base.tests.common import HttpCaseWithUserDemo
from dateutil.relativedelta import relativedelta
_logger = logging.getLogger(__name__)
@ -15,12 +18,11 @@ class TestMenusAdmin(odoo.tests.HttpCase):
for app_id in menus['root']['children']:
with self.subTest(app=menus[app_id]['name']):
_logger.runbot('Testing %s', menus[app_id]['name'])
self.browser_js("/web", "odoo.__DEBUG__.services['web.clickEverywhere']('%s');" % menus[app_id]['xmlid'], "odoo.isReady === true", login="admin", timeout=600)
self.terminate_browser()
self.browser_js("/web", "odoo.loader.modules.get('@web/webclient/clickbot/clickbot_loader').startClickEverywhere('%s');" % menus[app_id]['xmlid'], "odoo.isReady === true", login="admin", timeout=1200)
@odoo.tests.tagged('click_all', 'post_install', '-at_install', '-standard')
class TestMenusDemo(odoo.tests.HttpCase):
class TestMenusDemo(HttpCaseWithUserDemo):
allow_end_on_form = True
def test_01_click_everywhere_as_demo(self):
user_demo = self.env.ref("base.user_demo")
@ -28,16 +30,35 @@ class TestMenusDemo(odoo.tests.HttpCase):
for app_id in menus['root']['children']:
with self.subTest(app=menus[app_id]['name']):
_logger.runbot('Testing %s', menus[app_id]['name'])
self.browser_js("/web", "odoo.__DEBUG__.services['web.clickEverywhere']('%s');" % menus[app_id]['xmlid'], "odoo.isReady === true", login="demo", timeout=600)
self.terminate_browser()
self.browser_js("/web", "odoo.loader.modules.get('@web/webclient/clickbot/clickbot_loader').startClickEverywhere('%s');" % menus[app_id]['xmlid'], "odoo.isReady === true", login="demo", timeout=1200)
@odoo.tests.tagged('post_install', '-at_install')
class TestMenusAdminLight(odoo.tests.HttpCase):
allow_end_on_form = True
def test_01_click_apps_menus_as_admin(self):
self.browser_js("/web", "odoo.__DEBUG__.services['web.clickEverywhere'](undefined, true);", "odoo.isReady === true", login="admin", timeout=120)
# Due to action_pos_preparation_display_kitchen_display, cliking on the "Kitchen Display"
# menuitem could open the UI display, which will break the crawler tests as there is no
# way for the tour to be executed, leading to a timeout
if 'pos_preparation_display.display' in self.env:
self.env['pos_preparation_display.display'].create({
'name': 'Super Smart Kitchen Display',
})
# There is a bug when we go the Field Service app (without any demo data) and we
# click on the Studio button. It seems the fake group generated containing one record
# to be used in the KanbanEditorRenderer has groupByField to undefined
# (I guess it is because there is no group by?) and we got an error at this line
# because we assume groupByField is defined.
if 'project.task' in self.env and 'is_fsm' in self.env['project.task']:
self.env['project.task'].create({
'name': 'Zizizbroken',
'project_id': self.env.ref('industry_fsm.fsm_project').id,
'user_ids': [(4, self.env.ref('base.user_admin').id)],
'date_deadline': datetime.now() + relativedelta(hour=12),
'planned_date_begin': datetime.now() + relativedelta(hour=10),
})
self.browser_js("/web", "odoo.loader.modules.get('@web/webclient/clickbot/clickbot_loader').startClickEverywhere(undefined, true);", "odoo.isReady === true", login="admin", timeout=120)
@odoo.tests.tagged('post_install', '-at_install',)
@odoo.tests.tagged('post_install', '-at_install')
class TestMenusDemoLight(HttpCaseWithUserDemo):
allow_end_on_form = True
@ -47,4 +68,4 @@ class TestMenusDemoLight(HttpCaseWithUserDemo):
group_website_designer = self.env.ref('website.group_website_designer', raise_if_not_found=False)
if group_website_designer:
self.env.ref('base.group_user').write({"implied_ids": [(4, group_website_designer.id)]})
self.browser_js("/web", "odoo.__DEBUG__.services['web.clickEverywhere'](undefined, true);", "odoo.isReady === true", login="demo", timeout=120)
self.browser_js("/web", "odoo.loader.modules.get('@web/webclient/clickbot/clickbot_loader').startClickEverywhere(undefined, true);", "odoo.isReady === true", login="demo", timeout=120)

View file

@ -4,6 +4,7 @@
import operator
import re
import secrets
from io import BytesIO
from unittest.mock import patch
import requests
@ -68,6 +69,14 @@ class TestDatabaseOperations(BaseCase):
def assertDbs(self, dbs):
self.assertEqual(self.list_dbs_filtered() - self.base_databases, set(dbs))
def url_open_drop(self, dbname):
res = self.session.post(self.url('/web/database/drop'), data={
'master_pwd': self.password,
'name': dbname,
}, allow_redirects=False)
res.raise_for_status()
return res
def test_database_creation(self):
# check verify_admin_password patch
self.assertTrue(odoo.tools.config.verify_admin_password(self.password))
@ -88,10 +97,7 @@ class TestDatabaseOperations(BaseCase):
self.assertDbs([test_db_name])
# delete the created database
res = self.session.post(self.url('/web/database/drop'), data={
'master_pwd': self.password,
'name': test_db_name,
}, allow_redirects=False)
res = self.url_open_drop(test_db_name)
self.assertEqual(res.status_code, 303)
self.assertIn('/web/database/manager', res.headers['Location'])
self.assertDbs([])
@ -110,10 +116,70 @@ class TestDatabaseOperations(BaseCase):
self.assertDbs([test_db_name])
# delete the created database
res = self.session.post(self.url('/web/database/drop'), data={
'master_pwd': self.password,
'name': test_db_name,
}, allow_redirects=False)
self.assertEqual(res.status_code, 303)
res = self.url_open_drop(test_db_name)
self.assertIn('/web/database/manager', res.headers['Location'])
self.assertDbs([])
def test_database_restore(self):
test_db_name = self.db_name + '-test-database-restore'
self.assertNotIn(test_db_name, self.list_dbs_filtered())
# backup the current database inside a temporary zip file
res = self.session.post(
self.url('/web/database/backup'),
data={
'master_pwd': self.password,
'name': self.db_name,
},
allow_redirects=False,
stream=True,
)
res.raise_for_status()
datetime_pattern = r'\d\d\d\d-\d\d-\d\d_\d\d-\d\d-\d\d'
self.assertRegex(
res.headers.get('Content-Disposition'),
fr"attachment; filename\*=UTF-8''{self.db_name}_{datetime_pattern}\.zip"
)
backup_file = BytesIO()
backup_file.write(res.content)
self.assertGreater(backup_file.tell(), 0, "The backup seems corrupted")
# upload the backup under a new name (create a duplicate)
with self.subTest(DEFAULT_MAX_CONTENT_LENGTH=None), \
patch.object(odoo.http, 'DEFAULT_MAX_CONTENT_LENGTH', None):
backup_file.seek(0)
self.session.post(
self.url('/web/database/restore'),
data={
'master_pwd': self.password,
'name': test_db_name,
'copy': True,
},
files={
'backup_file': backup_file,
},
allow_redirects=False
).raise_for_status()
self.assertDbs([test_db_name])
self.url_open_drop(test_db_name)
# upload the backup again, this time simulating that the file is
# too large under the default size limit, the default size limit
# shouldn't apply to /web/database URLs
with self.subTest(DEFAULT_MAX_CONTENT_LENGTH=1024), \
patch.object(odoo.http, 'DEFAULT_MAX_CONTENT_LENGTH', 1024):
backup_file.seek(0)
self.session.post(
self.url('/web/database/restore'),
data={
'master_pwd': self.password,
'name': test_db_name,
'copy': True,
},
files={
'backup_file': backup_file,
},
allow_redirects=False
).raise_for_status()
self.assertDbs([test_db_name])
self.url_open_drop(test_db_name)

View file

@ -63,3 +63,12 @@ class IrModelAccessTest(TransactionCase):
# non existent model comes after existent model
result = self.env['ir.model'].display_name_for(["res.company", "unexistent"])
self.assertEqual(result, [{"display_name": "Companies", "model": "res.company"}, {"display_name": "unexistent", "model": "unexistent"}])
# transient models
result = self.env['ir.model'].display_name_for(["res.company", "base.language.export"])
self.assertEqual(result, [{"display_name": "Companies", "model": "res.company"}, {"display_name": "base.language.export", "model": "base.language.export"}])
# do not return results for transient models
result = self.env['ir.model'].get_available_models()
result = {values["model"] for values in result}
self.assertIn("res.company", result)
self.assertNotIn("base.language.export", result)

View file

@ -1,5 +1,8 @@
import base64
from lxml import etree
from odoo.tests.common import TransactionCase
from odoo.tools.mimetypes import guess_mimetype
class TestIrQweb(TransactionCase):
def test_image_field(self):
@ -28,3 +31,44 @@ class TestIrQweb(TransactionCase):
self.assertTrue(img.get("src").startswith("/web/image"))
self.assertEqual(img.get("class"), "img img-fluid")
self.assertEqual(img.get("alt"), "test image partner")
def test_image_field_webp(self):
webp = "UklGRsCpAQBXRUJQVlA4WAoAAAAQAAAAGAQA/wMAQUxQSMywAAAdNANp22T779/0RUREkvqLOTPesG1T21jatpLTSbpXQzTMEw3zWMM81jCPnWG2fTM7vpndvpkd38y2758Y+6a/Ld/Mt3zzT/XwzCKlV0Ooo61UpZIsKLjKc98R"
webp_decoded = base64.b64decode(webp)
self.assertEqual(guess_mimetype(webp_decoded), "image/webp")
view = self.env["ir.ui.view"].create({
"key": "web.test_qweb",
"type": "qweb",
"arch": """<t t-name="test_qweb">
<span t-field="record.flag_image" t-options-widget="'image'" t-options-qweb_img_raw_data="is_raw_image" />
</t>"""
})
lang_record = self.env["res.lang"].create({
"name": "test lang",
"flag_image": webp,
"code": "TEST"
})
attachment = self.env["ir.attachment"].search([
("res_model", "=", "res.lang"),
("res_id", '=', lang_record.id),
("res_field", "=", "flag_image")
])
jpeg_attach = self.env["ir.attachment"].create({
"name": "webpcopy.jpg",
"res_model": "ir.attachment",
"res_id": attachment.id,
"datas": "iVBORw0KGgoAAAANSUhEUgAAAAIAAAACCAIAAAD91JpzAAAAF0lEQVR4nGJxKFrEwMDAxAAGgAAAAP//D+IBWx9K7TUAAAAASUVORK5CYII="
})
jpeg_datas = jpeg_attach.datas
html = view.with_context(webp_as_jpg=False)._render_template(view.id, {"is_raw_image": True, "record": lang_record})
tree = etree.fromstring(html)
img = tree.find("img")
self.assertEqual(img.get("src"), "data:image/webp;base64,%s" % webp)
html = view.with_context(webp_as_jpg=True)._render_template(view.id, {"is_raw_image": True, "record": lang_record})
tree = etree.fromstring(html)
img = tree.find("img")
self.assertEqual(img.get("src"), "data:image/png;base64,%s" % jpeg_datas.decode())

View file

@ -2,7 +2,10 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
import re
from contextlib import suppress
import odoo.tests
from odoo.tools.misc import file_open
from werkzeug.urls import url_quote_plus
RE_ONLY = re.compile(r'QUnit\.(only|debug)\(')
@ -106,11 +109,12 @@ class WebSuite(WebsuiteCommon):
for asset in assets:
filename = asset['filename']
if not filename or asset['atype'] != 'text/javascript':
if not filename.endswith('.js'):
continue
with open(filename, 'rb') as fp:
if RE_ONLY.search(fp.read().decode('utf-8')):
self.fail("`QUnit.only()` or `QUnit.debug()` used in file %r" % asset['url'])
with suppress(FileNotFoundError):
with file_open(filename, 'rb', filter_ext=('.js',)) as fp:
if RE_ONLY.search(fp.read().decode('utf-8')):
self.fail("`QUnit.only()` or `QUnit.debug()` used in file %r" % asset['url'])
@odoo.tests.tagged('post_install', '-at_install')

View file

@ -17,7 +17,6 @@ class LoadMenusTests(HttpCase):
def test_load_menus(self):
menu_loaded = self.url_open("/web/webclient/load_menus/1234")
expected = {
str(self.menu.id): {
"actionID": False,
@ -28,6 +27,7 @@ class LoadMenusTests(HttpCase):
"name": "test_menu",
"webIcon": False,
"webIconData": False,
"webIconDataMimetype": False,
"xmlid": ""
},
"root": {
@ -41,6 +41,7 @@ class LoadMenusTests(HttpCase):
"name": "root",
"webIcon": None,
"webIconData": None,
"webIconDataMimetype": None,
"xmlid": "",
"backgroundImage": None,
}

View file

@ -0,0 +1,117 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
import io
import logging
import unittest
import zipfile
from odoo.fields import Command
from odoo.tests.common import HttpCase, tagged
from base64 import b64decode
from odoo.tools import mute_logger
_logger = logging.getLogger(__name__)
try:
import vobject
except ImportError:
_logger.warning("`vobject` Python module not found, vcard file generation disabled. Consider installing this module if you want to generate vcard files")
vobject = None
@tagged('-at_install', 'post_install')
class TestPartnerVCard(HttpCase):
def setUp(self):
super().setUp()
if not vobject:
raise unittest.SkipTest("Skip tests when `vobject` Python module is not found.")
self.partners = self.env['res.partner'].create([{
'name': 'John Doe',
'email': 'john.doe@test.example.com',
'mobile': '+1 202 555 0888',
'phone': '+1 202 555 0122',
'function': 'Painter',
'street': 'Cookieville Minimum-Security Orphanarium',
'city': 'New York',
'country_id': self.env.ref('base.us').id,
'zip': '97648',
'website': 'https://test.exemple.com',
}, {
'name': 'shut',
'email': 'shut@test.example.com',
'mobile': '+1 202 555 0999',
'phone': '+1 202 555 0123',
'function': 'Developer',
'street': 'Donutville Maximum-Security Orphanarium',
'city': 'Washington DC',
'country_id': self.env.ref('base.us').id,
'zip': '97649',
'website': 'https://test.example.com',
'child_ids': [
Command.create({'type': 'other'})
]
}])
self.authenticate("admin", "admin")
def check_vcard_contents(self, vcard, partner):
self.assertEqual(vcard.contents["n"][0].value.family, partner.name, "Vcard should have the same name")
self.assertEqual(vcard.contents["adr"][0].value.street, partner.street, "Vcard should have the same street")
self.assertEqual(vcard.contents["adr"][0].value.city, partner.city, "Vcard should have the same city")
self.assertEqual(vcard.contents["adr"][0].value.code, partner.zip, "Vcard should have the same zip")
self.assertEqual(vcard.contents["adr"][0].value.country, self.env.ref('base.us').name, "Vcard should have the same country")
self.assertEqual(vcard.contents["email"][0].value, partner.email, "Vcard should have the same email")
self.assertEqual(vcard.contents["url"][0].value, partner.website, "Vcard should have the same website")
self.assertEqual(vcard.contents["tel"][0].params['TYPE'], ["work"], "Vcard should have the same phone")
self.assertEqual(vcard.contents["tel"][0].value, partner.phone, "Vcard should have the same phone")
self.assertEqual(vcard.contents["tel"][1].params['TYPE'], ["cell"], "Vcard should have the same mobile")
self.assertEqual(vcard.contents["tel"][1].value, partner.mobile, "Vcard should have the same mobile")
self.assertEqual(vcard.contents["title"][0].value, partner.function, "Vcard should have the same function")
self.assertEqual(len(vcard.contents['photo'][0].value), len(b64decode(partner.avatar_512)), "Vcard should have the same photo")
def test_fetch_single_partner_vcard(self):
res = self.url_open('/web_enterprise/partner/%d/vcard' % self.partners[0].id)
vcard = vobject.readOne(res.text)
self.check_vcard_contents(vcard, self.partners[0])
def test_fetch_multiple_partners_vcard(self):
res = self.url_open('/web/partner/vcard?partner_ids=%s,%s'
% (self.partners[0].id, self.partners[1].id))
with io.BytesIO(res.content) as buffer:
with zipfile.ZipFile(buffer, 'r') as zipf:
vcfFileList = zipf.namelist()
for i, vcfFile in enumerate(vcfFileList):
vcardFile = zipf.read(vcfFile).decode()
self.check_vcard_contents(vobject.readOne(vcardFile), self.partners[i])
@unittest.skip
def test_not_exist_partner_vcard(self):
partner_id = self.partner.id
self.partner.unlink()
res = self.url_open('/web/partner/%d/vcard' % partner_id)
self.assertEqual(res.status_code, 404)
def test_check_partner_access_for_user(self):
self.env['res.users'].create({
'groups_id': [Command.set([self.env.ref('base.group_public').id])],
'name': 'Test User',
'login': 'testuser',
'password': 'testuser',
})
self.authenticate('testuser', 'testuser')
with mute_logger('odoo.http'): # mute 403 warning
res = self.url_open('/web/partner/vcard?partner_ids=%s,%s' %
(self.partners[0].id, self.partners[1].id))
self.assertEqual(res.status_code, 403)
def test_fetch_single_partner_vcard_without_name(self):
"""
Test to fetch a vcard of a partner create through
child of another partner without name
"""
partner = self.partners[1].child_ids[0]
res = self.url_open('/web/partner/vcard?partner_ids=%s' % partner.id)
vcard = vobject.readOne(res.text)
self.assertEqual(vcard.contents["n"][0].value.family, partner.complete_name, "Vcard will have the complete name when it dosen't have name")

View file

@ -86,24 +86,24 @@ class TestReadProgressBar(common.TransactionCase):
c1, c2, c3 = self.env['res.country'].search([], limit=3)
self.env['x_progressbar'].create([
# week 21
{'x_country_id': c1.id, 'x_date': '2021-05-20', 'x_state': 'foo'},
{'x_country_id': c1.id, 'x_date': '2021-05-21', 'x_state': 'foo'},
{'x_country_id': c1.id, 'x_date': '2021-05-22', 'x_state': 'foo'},
{'x_country_id': c1.id, 'x_date': '2021-05-23', 'x_state': 'bar'},
# week 22
{'x_country_id': c1.id, 'x_date': '2021-05-24', 'x_state': 'baz'},
{'x_country_id': c2.id, 'x_date': '2021-05-25', 'x_state': 'foo'},
{'x_country_id': c2.id, 'x_date': '2021-05-26', 'x_state': 'bar'},
{'x_country_id': c2.id, 'x_date': '2021-05-27', 'x_state': 'bar'},
{'x_country_id': c2.id, 'x_date': '2021-05-28', 'x_state': 'baz'},
{'x_country_id': c2.id, 'x_date': '2021-05-29', 'x_state': 'baz'},
{'x_country_id': c3.id, 'x_date': '2021-05-30', 'x_state': 'foo'},
# week 23
{'x_country_id': c3.id, 'x_date': '2021-05-31', 'x_state': 'foo'},
{'x_country_id': c3.id, 'x_date': '2021-06-01', 'x_state': 'baz'},
{'x_country_id': c3.id, 'x_date': '2021-06-02', 'x_state': 'baz'},
{'x_country_id': c3.id, 'x_date': '2021-06-03', 'x_state': 'baz'},
# week 53 2018 / week 1 2019
{'x_country_id': c1.id, 'x_date': '2019-01-01', 'x_state': 'foo'},
{'x_country_id': c1.id, 'x_date': '2019-01-02', 'x_state': 'foo'},
{'x_country_id': c1.id, 'x_date': '2019-01-03', 'x_state': 'foo'},
{'x_country_id': c1.id, 'x_date': '2019-01-04', 'x_state': 'bar'},
{'x_country_id': c1.id, 'x_date': '2019-01-05', 'x_state': 'baz'},
# week 2 2019
{'x_country_id': c2.id, 'x_date': '2019-01-06', 'x_state': 'foo'},
{'x_country_id': c2.id, 'x_date': '2019-01-07', 'x_state': 'bar'},
{'x_country_id': c2.id, 'x_date': '2019-01-08', 'x_state': 'bar'},
{'x_country_id': c2.id, 'x_date': '2019-01-09', 'x_state': 'baz'},
{'x_country_id': c3.id, 'x_date': '2019-01-10', 'x_state': 'baz'},
{'x_country_id': c3.id, 'x_date': '2019-01-11', 'x_state': 'foo'},
{'x_country_id': c3.id, 'x_date': '2019-01-12', 'x_state': 'foo'},
# week 3 2019
{'x_country_id': c3.id, 'x_date': '2019-01-13', 'x_state': 'baz'},
{'x_country_id': c3.id, 'x_date': '2019-01-14', 'x_state': 'baz'},
{'x_country_id': c3.id, 'x_date': '2019-01-15', 'x_state': 'baz'},
])
progress_bar = {
@ -113,16 +113,16 @@ class TestReadProgressBar(common.TransactionCase):
result = self.env['x_progressbar'].read_progress_bar([], 'x_country_id', progress_bar)
self.assertEqual(result, {
c1.display_name: {'foo': 3, 'bar': 1, 'baz': 1},
c2.display_name: {'foo': 1, 'bar': 2, 'baz': 2},
c3.display_name: {'foo': 2, 'bar': 0, 'baz': 3},
c2.display_name: {'foo': 1, 'bar': 2, 'baz': 1},
c3.display_name: {'foo': 2, 'bar': 0, 'baz': 4},
})
# check date aggregation and format
result = self.env['x_progressbar'].read_progress_bar([], 'x_date:week', progress_bar)
self.assertEqual(result, {
'W21 2021': {'foo': 3, 'bar': 1, 'baz': 0},
'W22 2021': {'foo': 2, 'bar': 2, 'baz': 3},
'W23 2021': {'foo': 1, 'bar': 0, 'baz': 3},
'W1 2019': {'foo': 3, 'bar': 1, 'baz': 1},
'W2 2019': {'foo': 3, 'bar': 2, 'baz': 2},
'W3 2019': {'foo': 0, 'bar': 0, 'baz': 3},
})
# add a computed field on model
@ -146,13 +146,14 @@ class TestReadProgressBar(common.TransactionCase):
result = self.env['x_progressbar'].read_progress_bar([], 'x_country_id', progress_bar)
self.assertEqual(result, {
c1.display_name: {'foo': 3, 'bar': 1, 'baz': 1},
c2.display_name: {'foo': 1, 'bar': 2, 'baz': 2},
c3.display_name: {'foo': 2, 'bar': 0, 'baz': 3},
c2.display_name: {'foo': 1, 'bar': 2, 'baz': 1},
c3.display_name: {'foo': 2, 'bar': 0, 'baz': 4},
})
result = self.env['x_progressbar'].read_progress_bar([], 'x_date:week', progress_bar)
self.assertEqual(result, {
'W21 2021': {'foo': 3, 'bar': 1, 'baz': 0},
'W22 2021': {'foo': 2, 'bar': 2, 'baz': 3},
'W23 2021': {'foo': 1, 'bar': 0, 'baz': 3},
# first week is not the same as above, but that seems acceptable...
'W1 2019': {'foo': 3, 'bar': 1, 'baz': 1},
'W2 2019': {'foo': 3, 'bar': 2, 'baz': 2},
'W3 2019': {'foo': 0, 'bar': 0, 'baz': 3},
})

View file

@ -0,0 +1,39 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo.tests import TransactionCase
class TestResUsers(TransactionCase):
@classmethod
def setUpClass(cls):
super().setUpClass()
cls.users = cls.env["res.users"].create([
{'name': 'Jean', 'login': 'jean@mail.com', 'password': 'jean@mail.com'},
{'name': 'Jean-Paul', 'login': 'jean-paul@mail.com', 'password': 'jean-paul@mail.com'},
{'name': 'Jean-Jacques', 'login': 'jean-jacques@mail.com', 'password': 'jean-jacques@mail.com'},
{'name': 'Georges', 'login': 'georges@mail.com', 'password': 'georges@mail.com'},
{'name': 'Claude', 'login': 'claude@mail.com', 'password': 'claude@mail.com'},
{'name': 'Pascal', 'login': 'pascal@mail.com', 'password': 'pascal@mail.com'},
])
def test_name_search(self):
"""
Test name search with self assign feature
The self assign feature is present only when a limit is present,
which is the case with the public name_search by default
"""
ResUsers = self.env['res.users']
jean = self.users[0]
user_ids = [id_ for id_, __ in ResUsers.with_user(jean).name_search('')]
self.assertEqual(jean.id, user_ids[0], "The current user, Jean, should be the first in the result.")
user_ids = [id_ for id_, __ in ResUsers.with_user(jean).name_search('Claude')]
self.assertNotIn(jean.id, user_ids, "The current user, Jean, should not be in the result because his name does not fit the condition.")
pascal = self.users[-1]
user_ids = [id_ for id_, __ in ResUsers.with_user(pascal).name_search('')]
self.assertEqual(pascal.id, user_ids[0], "The current user, Pascal, should be the first in the result.")
user_ids = [id_ for id_, __ in ResUsers.with_user(pascal).name_search('', limit=3)]
self.assertEqual(pascal.id, user_ids[0], "The current user, Pascal, should be the first in the result.")
self.assertEqual(len(user_ids), 3, "The number of results found should still respect the limit set.")
jean_paul = self.users[1]
user_ids = [id_ for id_, __ in ResUsers.with_user(jean_paul).name_search('Jean')]
self.assertEqual(jean_paul.id, user_ids[0], "The current user, Jean-Paul, should be the first in the result")

View file

@ -14,7 +14,11 @@ class TestSessionInfo(common.HttpCase):
cls.company_a = cls.env['res.company'].create({'name': "A"})
cls.company_b = cls.env['res.company'].create({'name': "B"})
cls.company_c = cls.env['res.company'].create({'name': "C"})
cls.companies = [cls.company_a, cls.company_b, cls.company_c]
cls.company_b_branch = cls.env['res.company'].create({'name': "B Branch", 'parent_id': cls.company_b.id})
cls.company_c_branch = cls.env['res.company'].create({'name': "C Branch", 'parent_id': cls.company_c.id})
cls.company_c_branch_branch = cls.env['res.company'].create({'name': "C Branch Branch", 'parent_id': cls.company_c_branch.id})
cls.allowed_companies = cls.company_a + cls.company_b_branch + cls.company_c + cls.company_c_branch_branch
cls.disallowed_ancestor_companies = cls.company_b + cls.company_c_branch
cls.user_password = "info"
cls.user = common.new_test_user(
@ -25,7 +29,7 @@ class TestSessionInfo(common.HttpCase):
tz="UTC")
cls.user.write({
'company_id': cls.company_a.id,
'company_ids': [Command.set([company.id for company in cls.companies])],
'company_ids': [Command.set(cls.allowed_companies.ids)],
})
cls.payload = json.dumps(dict(jsonrpc="2.0", method="call", id=str(uuid4())))
@ -47,11 +51,25 @@ class TestSessionInfo(common.HttpCase):
'id': company.id,
'name': company.name,
'sequence': company.sequence,
} for company in self.companies
'child_ids': company.child_ids.ids,
'parent_id': company.parent_id.id,
} for company in self.allowed_companies
}
expected_disallowed_ancestor_companies = {
str(company.id): {
'id': company.id,
'name': company.name,
'sequence': company.sequence,
'child_ids': company.child_ids.ids,
'parent_id': company.parent_id.id,
} for company in self.disallowed_ancestor_companies
}
expected_user_companies = {
'current_company': self.company_a.id,
'allowed_companies': expected_allowed_companies,
'disallowed_ancestor_companies': expected_disallowed_ancestor_companies,
}
self.assertEqual(
result['user_companies'],
@ -63,12 +81,3 @@ class TestSessionInfo(common.HttpCase):
response = self.url_open("/web/session/modules", data=self.payload, headers=self.headers)
data = response.json()
self.assertTrue(isinstance(data['result'], list))
def test_load_polish_lang(self):
# Regression test, making sure languages without thousand separators
# work correctly
lang_pl = self.env['res.lang']._activate_lang('pl_PL')
self.user.lang = lang_pl.code
self.authenticate(self.user.login, self.user_password)
res = self.url_open('/web')
res.raise_for_status()

View file

@ -0,0 +1,42 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo.tests.common import TransactionCase
class TestTranslationOverride(TransactionCase):
@classmethod
def setUpClass(cls):
super().setUpClass()
cls.category = cls.env['res.partner.category'].create({'name': 'Reblochon'})
cls.custom = cls.env['ir.model.fields'].create({
'name': 'x_html_test',
'ttype': 'html',
'model_id': cls.category.id,
'translate': True,
})
def test_web_override_translations(self):
self.env['res.lang']._activate_lang('fr_FR')
categoryEN = self.category.with_context(lang='en_US')
categoryFR = self.category.with_context(lang='fr_FR')
customEN = self.custom.with_context(lang='en_US')
customFR = self.custom.with_context(lang='fr_FR')
self.category.web_override_translations({'name': 'commonName'})
self.assertEqual(categoryEN.name, 'commonName')
self.assertEqual(categoryFR.name, 'commonName')
# cannot void translations (incluiding en_US)
self.category.web_override_translations({'name': False})
self.assertEqual(categoryEN.name, 'commonName')
self.assertEqual(categoryFR.name, 'commonName')
# empty str is a valid translation
self.category.web_override_translations({'name': ''})
self.assertEqual(categoryEN.name, '')
self.assertEqual(categoryFR.name, '')
# translated html fields are not changed
self.custom.web_override_translations({'name': '<div>dont</div><div>change</div>'})
self.assertEqual(customEN.name, 'x_html_test')
self.assertEqual(customFR.name, 'x_html_test')

View file

@ -0,0 +1,20 @@
from odoo import fields
from odoo.tests import common
@common.tagged('post_install', '-at_install')
class TestWebReadGroup(common.TransactionCase):
def test_web_read_group_with_date_groupby_and_limit(self):
first, second = self.env["res.partner"].create([
{
"name": "first",
"date": fields.Date.to_date("2021-06-01")
},
{
"name": "second",
"date": fields.Date.to_date("2021-07-01")
}
])
groups = self.env["res.partner"].web_read_group([["id", "in", [first.id, second.id]]], [], groupby=["date"], limit=1)
self.assertEqual(groups["length"], 2)

View file

@ -0,0 +1,25 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from werkzeug.urls import url_parse
from odoo.tests.common import HttpCase
class TestWebRedirect(HttpCase):
def setUp(self):
super().setUp()
self.encoded_url_query = "redirect=web%23cids%3D1%26action%3Dmenu"
def test_root_route_redirect_param(self):
web_response = self.url_open(f"/?{self.encoded_url_query}")
web_response.raise_for_status()
response_url_query = url_parse(web_response.url).query
self.assertEqual(response_url_query, self.encoded_url_query)
def test_web_route_redirect_param(self):
web_response = self.url_open(f"/web?{self.encoded_url_query}")
web_response.raise_for_status()
response_url_query = url_parse(web_response.url).query
self.assertEqual(response_url_query, self.encoded_url_query)

View file

@ -12,7 +12,8 @@ class TestWebSearchRead(common.TransactionCase):
cls.ResCurrency = cls.env['res.currency'].with_context(active_test=False)
cls.max = cls.ResCurrency.search_count([])
def assert_web_search_read(self, expected_length, expected_records_length, expected_search_count_called=True, **kwargs):
def assert_web_search_read(self, expected_length, expected_records_length, expected_search_count_called=True,
**kwargs):
original_search_count = self.ResCurrency.search_count
search_count_called = [False]
@ -21,13 +22,13 @@ class TestWebSearchRead(common.TransactionCase):
return original_search_count(*method_args, **method_kwargs)
with patch('odoo.addons.base.models.res_currency.Currency.search_count', new=search_count):
results = self.ResCurrency.web_search_read(domain=[], fields=['id'], **kwargs)
results = self.ResCurrency.web_search_read(domain=[], specification={'id':{}}, **kwargs)
self.assertEqual(results['length'], expected_length)
self.assertEqual(len(results['records']), expected_records_length)
self.assertEqual(search_count_called[0], expected_search_count_called)
def test_web_search_read(self):
def test_unity_web_search_read(self):
self.assert_web_search_read(self.max, self.max, expected_search_count_called=False)
self.assert_web_search_read(self.max, 2, limit=2)
self.assert_web_search_read(self.max, 2, limit=2, offset=10)

View file

@ -0,0 +1,92 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo.addons.base.tests.common import HttpCaseWithUserDemo
from odoo.tests.common import tagged
@tagged("-at_install", "post_install")
class WebManifestRoutesTest(HttpCaseWithUserDemo):
"""
This test suite is used to request the routes used by the PWA backend implementation
"""
def test_webmanifest(self):
"""
This route returns a well formed backend's WebManifest
"""
self.authenticate("admin", "admin")
response = self.url_open("/web/manifest.webmanifest")
self.assertEqual(response.status_code, 200)
self.assertEqual(response.headers["Content-Type"], "application/manifest+json")
data = response.json()
self.assertEqual(data["name"], "Odoo")
self.assertEqual(data["scope"], "/web")
self.assertEqual(data["start_url"], "/web")
self.assertEqual(data["display"], "standalone")
self.assertEqual(data["background_color"], "#714B67")
self.assertEqual(data["theme_color"], "#714B67")
self.assertEqual(data["prefer_related_applications"], False)
self.assertCountEqual(data["icons"], [
{'src': '/web/static/img/odoo-icon-192x192.png', 'sizes': '192x192', 'type': 'image/png'},
{'src': '/web/static/img/odoo-icon-512x512.png', 'sizes': '512x512', 'type': 'image/png'}
])
self.assertGreaterEqual(len(data["shortcuts"]), 0)
for shortcut in data["shortcuts"]:
self.assertGreater(len(shortcut["name"]), 0)
self.assertGreater(len(shortcut["description"]), 0)
self.assertGreater(len(shortcut["icons"]), 0)
self.assertTrue(shortcut["url"].startswith("/web#menu_id="))
def test_webmanifest_unauthenticated(self):
"""
This route returns a well formed backend's WebManifest
"""
response = self.url_open("/web/manifest.webmanifest")
self.assertEqual(response.status_code, 200)
self.assertEqual(response.headers["Content-Type"], "application/manifest+json")
data = response.json()
self.assertEqual(data["name"], "Odoo")
self.assertEqual(data["scope"], "/web")
self.assertEqual(data["start_url"], "/web")
self.assertEqual(data["display"], "standalone")
self.assertEqual(data["background_color"], "#714B67")
self.assertEqual(data["theme_color"], "#714B67")
self.assertEqual(data["prefer_related_applications"], False)
self.assertCountEqual(data["icons"], [
{'src': '/web/static/img/odoo-icon-192x192.png', 'sizes': '192x192', 'type': 'image/png'},
{'src': '/web/static/img/odoo-icon-512x512.png', 'sizes': '512x512', 'type': 'image/png'}
])
self.assertEqual(len(data["shortcuts"]), 0)
def test_serviceworker(self):
"""
This route returns a JavaScript's ServiceWorker
"""
response = self.url_open("/web/service-worker.js")
self.assertEqual(response.status_code, 200)
self.assertEqual(response.headers["Content-Type"], "text/javascript")
self.assertEqual(response.headers["Service-Worker-Allowed"], "/web")
def test_offline_url(self):
"""
This route returns the offline page
"""
response = self.url_open("/web/offline")
self.assertEqual(response.status_code, 200)
self.assertEqual(response.headers["Content-Type"], "text/html; charset=utf-8")
def test_apple_touch_icon(self):
"""
This request tests the presence of an apple-touch-icon image route for the PWA icon and
its presence from the head of the document.
"""
self.authenticate("demo", "demo")
response = self.url_open("/web/static/img/odoo-icon-ios.png")
self.assertEqual(response.status_code, 200)
document = self.url_open("/web")
self.assertIn(
'<link rel="apple-touch-icon" href="/web/static/img/odoo-icon-ios.png"/>', document.text,
"Icon for iOS is present in the head of the document.",
)