19.0 vanilla

This commit is contained in:
Ernad Husremovic 2026-03-09 09:32:39 +01:00
parent 38c6088dcc
commit d9452d2060
243 changed files with 30797 additions and 10815 deletions

View file

@ -15,6 +15,10 @@ installed.""",
'website_blog',
'website_event_sale',
'website_slides',
'website_livechat',
'website_crm_iap_reveal',
'website_sale_comparison',
'website_sale_wishlist',
],
'installable': True,
'assets': {
@ -22,5 +26,6 @@ installed.""",
'test_website_modules/static/tests/**/*',
],
},
'author': 'Odoo S.A.',
'license': 'LGPL-3',
}

View file

@ -1,106 +1,130 @@
odoo.define('test_website_modules.tour.configurator_flow', function (require) {
'use strict';
import { registry } from "@web/core/registry";
const tour = require('web_tour.tour');
const wTourUtils = require('website.tour_utils');
tour.register('configurator_flow', {
test: true,
url: '/web#action=website.action_website_configuration',
},
[
{
content: "click on create new website",
trigger: 'button[name="action_website_create_new"]',
}, {
content: "insert website name",
trigger: '[name="name"] input',
run: 'text Website Test',
}, {
content: "validate the website creation modal",
trigger: 'button.btn-primary',
},
// Configurator first screen
{
content: "click next",
trigger: 'button.o_configurator_show',
timeout: 20000, /* previous step create a new website, this could take a long time */
},
// Description screen
{
content: "select a website type",
trigger: 'a.o_change_website_type',
}, {
content: "insert a website industry",
trigger: '.o_configurator_industry input',
run: 'text ab',
}, {
content: "select a website industry from the autocomplete",
trigger: '.o_configurator_industry ul li a',
}, {
content: "select an objective",
trigger: '.o_configurator_purpose_dd a',
}, {
content: "choose from the objective list",
trigger: 'a.o_change_website_purpose',
},
// Palette screen
{
content: "chose a palette card",
trigger: '.palette_card',
},
// Features screen
{
content: "select Pricing",
trigger: '.card:contains("Pricing")',
}, {
content: "Events should be selected (module already installed)",
extra_trigger: '.card.border-success:contains("Pricing")',
trigger: '.card.card_installed:contains("Events")',
run: function () {}, // it's a check
}, {
content: "Slides should be selected (module already installed)",
trigger: '.card.card_installed:contains("eLearning")',
run: function () {}, // it's a check
}, {
content: "Success Stories (Blog) and News (Blog) should be selected (module already installed)",
extra_trigger: '.card.card_installed:contains("Success Stories")',
trigger: '.card.card_installed:contains("News")',
run: function () {}, // it's a check
}, {
content: "Click on build my website",
trigger: 'button.btn-primary',
}, {
content: "Loader should be shown",
trigger: '.o_website_loader_container',
run: function () {}, // it's a check
}, {
content: "Wait untill the configurator is finished",
trigger: '#oe_snippets.o_loaded',
timeout: 30000,
},
...wTourUtils.clickOnSave(),
{
content: "check menu and footer links are correct",
trigger: 'body:not(.editor_enable)', // edit mode left
run: function () {
const $iframe = this.$anchor.find('iframe.o_iframe:not(.o_ignore_in_tour)');
for (const menu of ['Home', 'Events', 'Courses', 'Pricing', 'News', 'Success Stories', 'Contact us']) {
if (!$iframe.contents().find(`#top_menu a:contains(${menu})`).length) {
console.error(`Missing ${menu} menu. It should have been created by the configurator.`);
}
}
for (const url of ['/', '/event', '/slides', '/pricing', '/blog/', '/blog/', '/contactus']) {
if (!$iframe.contents().find(`#top_menu a[href^='${url}']`).length) {
console.error(`Missing ${url} menu URL. It should have been created by the configurator.`);
}
}
for (const link of ['Privacy Policy', 'Contact us']) {
if (!$iframe.contents().find(`#footer ul a:contains(${link})`).length) {
console.error(`Missing ${link} footer link. It should have been created by the configurator.`);
}
}
registry.category("web_tour.tours").add("configurator_flow", {
url: "/odoo/action-website.action_website_configuration",
steps: () => [
{
content: "click on create new website",
trigger: 'button[name="action_website_create_new"]',
run: "click",
},
},
]);
{
content: "insert website name",
trigger: '[name="name"] input',
run: "edit Website Test",
},
{
content: "validate the website creation modal",
trigger: 'button.btn-primary:contains("Create")',
run: "click",
expectUnloadPage: true,
},
// Configurator first screen
{
content: "click next",
trigger: "button.o_configurator_show",
run: "click",
timeout: 20000, /* previous step create a new website, this could take a long time */
},
// Description screen
{
content: "select a website type",
trigger: "button.o_change_website_type",
run: "click",
},
{
content: "insert a website industry",
trigger: ".o_configurator_industry input",
run: "edit ab",
},
{
content: "select a website industry from the autocomplete",
trigger: ".o_configurator_industry ul li a",
run: "click",
},
{
content: "choose from the objective list",
trigger: "button.o_change_website_purpose",
run: "click",
},
// Palette screen
{
content: "chose a palette card",
trigger: ".palette_card",
run: "click",
},
// Features screen
{
content: "select Pricing",
trigger: '.card:contains("Pricing")',
run: "click",
},
{
trigger: '.card.border-success:contains("Pricing")',
},
{
content: "Events should be selected (module already installed)",
trigger: '.card.card_installed:contains("Events")',
},
{
content: "Slides should be selected (module already installed)",
trigger: '.card.card_installed:contains("eLearning")',
},
{
trigger: '.card.card_installed:contains("Success Stories")',
},
{
content:
"Success Stories (Blog) and News (Blog) should be selected (module already installed)",
trigger: '.card.card_installed:contains("News")',
},
{
content: "Click on build my website",
trigger: "button.btn-primary",
run: "click",
},
// Online catalog screen
{
content: "Choose a shop page style",
trigger: ".o_configurator_screen:contains(online catalog) .button_area",
run: "click",
},
// Product page Screen
{
content: "Choose a product page style",
trigger: ".o_configurator_screen:contains(product page) .button_area",
run: "click",
},
{
content: "Loader should be shown",
trigger: ".o_website_loader_container",
expectUnloadPage: true,
},
{
content: "Wait until the configurator is finished",
trigger: ":iframe [data-view-xmlid='website.homepage']",
timeout: 30000,
},
{
content: "check menu and footer links are correct",
trigger: "body:not(.editor_enable)", // edit mode left
},
...["Contact us", "Privacy Policy"].map((menu) => ({
content: `Check footer menu ${menu} is there`,
trigger: `:iframe footer a:contains(${menu})`,
})),
...["Home", "Events", "Courses", "Pricing", "News", "Success Stories", "Contact us"].map(
(menu) => ({
content: `Check menu ${menu} is there`,
trigger: `:iframe .top_menu a:contains(${menu}):not(:visible)`,
})
),
...["/", "/event", "/slides", "/pricing", "/blog/", "/blog/", "/contactus"].map((url) => ({
content: `Check url ${url} is there`,
trigger: `:iframe .top_menu a[href^='${url}']:not(:visible)`,
})),
{
trigger: ":iframe h1:contains(your journey starts here)",
},
],
});

View file

@ -2,3 +2,5 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from . import test_configurator
from . import test_controllers
from . import test_performance

View file

@ -14,4 +14,4 @@ class TestConfigurator(TestConfiguratorCommon):
group_order_template = self.env.ref('sale_management.group_sale_order_template', raise_if_not_found=False)
if group_order_template:
self.env.ref('base.group_user').write({"implied_ids": [(4, group_order_template.id)]})
self.start_tour('/web#action=website.action_website_configuration', 'configurator_flow', login="admin")
self.start_tour('/odoo/action-website.action_website_configuration', 'configurator_flow', login="admin")

View file

@ -0,0 +1,151 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from base64 import b64encode
from odoo import Command, tests
from odoo.tools import mute_logger
from odoo.tools.json import scriptsafe as json_safe
from odoo.addons.base.tests.common import HttpCaseWithUserDemo, HttpCaseWithUserPortal
@tests.tagged('-at_install', 'post_install')
class TestWebEditorController(HttpCaseWithUserDemo, HttpCaseWithUserPortal):
def test_modify_image(self):
gif_base64 = b"R0lGODdhAQABAIAAAP///////ywAAAAAAQABAAACAkQBADs="
attachment = self.env['ir.attachment'].create({
'name': 'test.gif',
'mimetype': 'image/gif',
'datas': gif_base64,
'public': True,
'res_model': 'ir.ui.view',
'res_id': 0,
})
def modify(login, name, expect_fail=False):
self.authenticate(login, login)
svg = b'<svg viewBox="0 0 400 400"><!-- %s --><image url="data:image/gif;base64,%s" /></svg>' % (name.encode('ascii'), gif_base64)
params = {
'name': name,
'mimetype': 'image/svg+xml',
'data': b64encode(svg).decode('ascii'),
}
if attachment.res_id:
params['res_model'] = attachment.res_model
params['res_id'] = attachment.res_id
response = self.url_open(
f'/html_editor/modify_image/{attachment.id}',
headers={'Content-Type': 'application/json'},
data=json_safe.dumps({
"params": params,
}),
)
self.assertEqual(200, response.status_code, "Expect response")
if expect_fail:
return json_safe.loads(response.content)
content = json_safe.loads(response.content)
self.assertFalse(content.get('error'), "An error should not happen")
url = content.get('result')
self.assertTrue(url.partition('?unique=')[0].endswith(name), "Expect name in URL")
response = self.url_open(url)
self.assertEqual(200, response.status_code, "Expect response")
self.assertTrue('image/svg+xml' in response.headers.get('Content-Type'), "Expect SVG mimetype")
self.assertEqual(svg, response.content, "Expect unchanged SVG")
return True
# Admin can modify page
modify('admin', 'page-admin.gif')
# Base user cannot modify page
self.user_demo.write({
'group_ids': [
Command.clear(),
Command.link(self.env.ref('base.group_user').id),
],
})
with mute_logger('odoo.http'):
json = modify('demo', 'page-demofail.gif', True)
self.assertFalse(json.get('result'), "Expect no URL when called with insufficient rights")
# Restricted editor with event right cannot modify page
self.user_demo.write({
'group_ids': [
Command.clear(),
Command.link(self.env.ref('base.group_user').id),
Command.link(self.env.ref('website.group_website_restricted_editor').id),
Command.link(self.env.ref('event.group_event_manager').id),
],
})
with mute_logger('odoo.http'):
json = modify('demo', 'page-demofail2.gif', True)
self.assertFalse(json.get('result'), "Expect no URL when called with insufficient rights")
# Website designer can modify page
self.user_demo.write({
'group_ids': [
Command.clear(),
Command.link(self.env.ref('base.group_user').id),
Command.link(self.env.ref('website.group_website_designer').id),
],
})
modify('demo', 'page-demo.gif')
# Website designer can modify url attachment (for e.g. unsplash)
attachment.url = '/page-logo-unique.gif'
modify('demo', 'page-logo-unique.gif')
attachment.url = False # Reset previous value
event = self.env['event.event'].create({'name': 'test event'})
attachment.res_model = 'event.event'
attachment.res_id = event.id
# Admin can modify event
modify('admin', 'event-admin.gif')
# Base user cannot modify event
self.user_demo.write({
'group_ids': [
Command.clear(),
Command.link(self.env.ref('base.group_user').id),
],
})
with mute_logger('odoo.http'):
json = modify('demo', 'event-demofail.gif', True)
self.assertFalse(json.get('result'), "Expect no URL when called with insufficient rights")
# Restricted editor with sales rights cannot modify event
self.user_demo.write({
'group_ids': [
Command.clear(),
Command.link(self.env.ref('base.group_user').id),
Command.link(self.env.ref('website.group_website_restricted_editor').id),
Command.link(self.env.ref('sales_team.group_sale_manager').id),
],
})
with mute_logger('odoo.http'):
json = modify('demo', 'event-demofail2.gif', True)
self.assertFalse(json.get('result'), "Expect no URL when called with insufficient rights")
# Restricted editor with event rights can modify event
self.user_demo.write({
'group_ids': [
Command.clear(),
Command.link(self.env.ref('base.group_user').id),
Command.link(self.env.ref('website.group_website_restricted_editor').id),
Command.link(self.env.ref('event.group_event_manager').id),
],
})
modify('demo', 'event-demo.gif')
# Website designer cannot modify event
self.user_demo.write({
'group_ids': [
Command.clear(),
Command.link(self.env.ref('base.group_user').id),
Command.link(self.env.ref('website.group_website_designer').id),
],
})
with mute_logger('odoo.http'):
json = modify('demo', 'event-demofail3.gif', True)
self.assertFalse(json.get('result'), "Expect no URL when called with insufficient rights")

View file

@ -0,0 +1,382 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
import base64
import io
import re
from PIL import Image
from unittest.mock import patch
import odoo
from odoo.fields import Command
from odoo.tests import tagged
from odoo.addons.website.tests.test_performance import TestWebsitePerformanceCommon
from odoo.addons.website_sale.tests.common import WebsiteSaleCommon
from odoo.addons.website_sale.tests.test_website_sale_pricelist import TestWebsitePriceList
class TestWebsiteAllPerformance(TestWebsitePerformanceCommon, TestWebsitePriceList, WebsiteSaleCommon):
@classmethod
def setUpClass(cls):
super().setUpClass()
# Attachment needed for the replacement of images
cls.env['ir.attachment'].create({
'public': True,
'name': 's_default_image.jpg',
'type': 'url',
'url': f'{cls.base_url()}/web/image/website.s_banner_default_image.jpg',
})
cats = cls.env['product.public.category'].create([{
'name': 'Level 0',
}, {
'name': 'Level 1',
}, {
'name': 'Level 2',
}])
cats[2].parent_id = cats[1].id
cats[1].parent_id = cats[0].id
# First image (blue) for the template.
color_blue = '#4169E1'
name_blue = 'Royal Blue'
# Red for the variant.
color_red = '#CD5C5C'
name_red = 'Indian Red'
# Create the color attribute.
cls.product_attribute = cls.env['product.attribute'].create({
'name': 'Beautiful Color',
'display_type': 'color',
})
# create the color attribute values
cls.attr_values = cls.env['product.attribute.value'].create([{
'name': name_blue,
'attribute_id': cls.product_attribute.id,
'html_color': color_blue,
'sequence': 1,
}, {
'name': name_red,
'attribute_id': cls.product_attribute.id,
'html_color': color_red,
'sequence': 2,
},
])
# first image (blue) for the template
f = io.BytesIO()
Image.new('RGB', (1920, 1080), '#4169E1').save(f, 'JPEG')
f.seek(0)
blue_image = base64.b64encode(f.read())
# second image (red) for the variant 1
f = io.BytesIO()
Image.new('RGB', (800, 500), '#FF69E1').save(f, 'JPEG')
f.seek(0)
red_image = base64.b64encode(f.read())
cls.productA = cls.env['product.product'].create({
'name': 'Product A',
'list_price': 100,
'sale_ok': True,
'website_published': True,
'website_sequence': 1,
'public_categ_ids': [Command.link(cats[2].id)],
})
cls.productB = cls.env['product.product'].create({
'name': 'Product B',
'list_price': 100,
'sale_ok': True,
'website_published': True,
'image_1920': red_image,
'website_sequence': -10,
})
cls.templateC = cls.env['product.template'].create({
'name': 'Test Remove Image',
'image_1920': blue_image,
'website_sequence': -10,
'public_categ_ids': [Command.link(cats[1].id)],
})
cls.productC = cls.templateC.product_variant_ids[0]
cls.productC.write({
'name': 'Product C',
'list_price': 100,
'sale_ok': True,
'website_published': True,
})
cls.product_images = cls.env['product.image'].with_context(default_product_tmpl_id=cls.productC.product_tmpl_id.id).create([{
'name': 'Template image',
'image_1920': blue_image,
}, {
'name': 'Variant image',
'image_1920': red_image,
'product_variant_id': cls.productC.id,
}])
for i in range(20):
template = cls.env['product.template'].create({
'name': f'Product test {i}',
'list_price': 100,
'sale_ok': True,
'image_1920': red_image,
'website_sequence': -9,
'attribute_line_ids': [
Command.create({
'attribute_id': cls.product_attribute.id,
'value_ids': [Command.set(cls.product_attribute.value_ids.ids)],
}),
],
})
variant = template.product_variant_ids[0]
if i % 5:
variant.website_published = True
if i % 4:
variant.website_sequence = -7
images = [{
'name': 'Template image',
'image_1920': blue_image,
}]
if i % 2:
images.append({
'name': 'Variant image',
'image_1920': red_image,
'product_variant_id': variant.id,
})
cls.env['product.image'].create(images)
fpos = cls.env["account.fiscal.position"].create({
'name': 'Fiscal Position BE',
'country_id': cls.env.ref('base.be').id,
'auto_apply': True,
'sequence': -1,
})
usd = cls.env.ref('base.USD')
cls.env['product.pricelist'].create({
'name': 'Custom pricelist (TEST)',
'currency_id': usd.id,
'sequence': -1,
'item_ids': [(0, 0, {
'base': 'list_price',
'applied_on': '1_product',
'product_tmpl_id': cls.templateC.id,
'price_discount': 20,
'min_quantity': 2,
'compute_price': 'formula',
})],
})
tax_group = cls.env['account.tax.group'].create({'name': 'Test 6%'})
cls.env['account.tax'].create({
'name': "Test 6%",
'fiscal_position_ids': fpos,
'amount': 6,
'price_include_override': 'tax_included',
'type_tax_use': 'sale',
'amount_type': 'percent',
'country_id': cls.env.ref('base.us').id,
'tax_group_id': tax_group.id,
})
def setUp(self):
super().setUp()
self.session = None
self.env['website'].search([]).channel_id = False
def _get_cart_quantity(self):
return int(re.search(
r'my_cart_quantity.*?>([0-9.]+)</sup>',
self.url_open(self.page.url).text
).group(1))
def test_website_user_id_public_user(self):
origin_serve_page = odoo.addons.website.models.ir_http.IrHttp._serve_page
def _serve_page():
request = odoo.http.request
self.assertTrue(request.env['website'].search([]).user_id._is_public(), 'The public user of the website is not public!')
self.assertTrue(request.env.user._is_public(), 'The visitor should not be logged')
return origin_serve_page()
with patch('odoo.addons.website.models.ir_http.IrHttp._serve_page', wraps=_serve_page) as mocked:
self.url_open(self.page.url)
mocked.assert_called_once()
def test_perf_sql_queries_page(self):
self.set_registry_readonly_mode(True)
self.page.track = False
# self.url_open('/web/set_profiling?profile=1&execution_context_qweb=1')
self.assertEqual(self._get_cart_quantity(), 0)
origin_allow_to_use_cache = odoo.addons.website.models.website_page.WebsitePage._allow_to_use_cache
def _allow_to_use_cache(request):
can_use = origin_allow_to_use_cache(request.env['website.page'], request)
self.assertTrue(can_use, 'The homepage should be cached for the public user')
with patch('odoo.addons.website.models.website_page.WebsitePage._allow_to_use_cache', wraps=_allow_to_use_cache) as mocked:
self.url_open(self.page.url)
mocked.assert_called_once()
select_tables_perf = {
# website queries
'orm_signaling_registry': 1,
'ir_attachment': 1,
# website_livechat _post_process_response_from_cache queries
'website': 1,
# website_crm_iap_reveal _serve_page queries
'website_visitor': 1,
}
expected_query_count = sum(select_tables_perf.values())
self._check_url_hot_query(self.page.url, expected_query_count, select_tables_perf, {})
self.url_open('/shop/cart/add', json={
"id": 0,
"jsonrpc": "2.0",
"method": "call",
"params": {
"product_template_id": self.productC.product_tmpl_id.id,
"product_id": self.productC.id,
"quantity": 1,
"uom_id": 1,
"product_custom_attribute_values": [],
"no_variant_attribute_value_ids": [],
"linked_products": []
}
})
self.assertEqual(self._get_cart_quantity(), 1)
select_tables_perf = {
# website queries
'orm_signaling_registry': 1,
'ir_attachment': 1,
# website_livechat _post_process_response_from_cache queries
'website': 1,
# website_crm_iap_reveal _serve_page queries
'website_visitor': 1,
}
expected_query_count = sum(select_tables_perf.values())
self._check_url_hot_query(self.page.url, expected_query_count, select_tables_perf, {})
self.url_open('/shop/cart/update', json={
"id": 0,
"jsonrpc": "2.0",
"method": "call",
"params": {
"line_id": self.env['sale.order'].search([], limit=1).order_line.id,
"product_id": self.productC.id,
"quantity": 0
}
})
self.assertEqual(self._get_cart_quantity(), 0)
select_tables_perf = {
# website queries
'orm_signaling_registry': 1,
'ir_attachment': 1,
# website_livechat _post_process_response_from_cache queries
'website': 1,
# website_crm_iap_reveal _serve_page queries
'website_visitor': 1,
}
expected_query_count = sum(select_tables_perf.values())
self._check_url_hot_query(self.page.url, expected_query_count, select_tables_perf, {})
def _get_queries_shop(self):
html = self.url_open('/shop').text
self.assertIn(f'<img src="/web/image/product.product/{self.productC.id}/', html)
self.assertIn(f'<img src="/web/image/product.template/{self.productA.product_tmpl_id.id}/', html)
self.assertIn(f'<img src="/web/image/product.image/{self.product_images.ids[1]}/', html)
query_count = 51 # To increase this number you must ask the permission to al
queries = {
'orm_signaling_registry': 1,
'website': 2,
'res_company': 2,
'product_pricelist': 4,
'product_template': 5,
'product_tag': 1,
'product_public_category': 6,
'product_product': 1,
'product_template_attribute_line': 3,
'res_users': 1,
'res_partner': 2,
'product_category': 1,
'product_pricelist_item': 2,
'account_tax': 1,
'res_currency': 1,
'account_account_tag': 1,
'product_ribbon': 1,
'product_attribute_value': 3,
'product_attribute': 1,
'ir_attachment': 4,
'product_image': 3,
'product_template_attribute_value': 1,
'ir_ui_view': 2,
'website_menu': 1,
'website_page': 1,
}
addons = tuple(self.env.registry._init_modules) + (self.env.context.get('install_module'),)
if 'website_helpdesk' in addons:
query_count += 1
queries['helpdesk_team'] = 1
if 'website_sale_subscription' in addons:
query_count += 1
queries['product_product'] += 1
tax = self.env.ref('account.1_sale_tax_template', raise_if_not_found=False)
if tax and tax.name == '15%':
query_count += 2
queries['account_tax_repartition_line'] = 2
if self._has_demo_data():
query_count += 5
queries['product_template'] += 1
queries['product_product'] += 2
queries['ir_attachment'] += 1
queries['product_ribbon'] += 1
else:
query_count += 3
queries['product_template_attribute_value'] += 3
# To increase the query count you must ask the permission to al
return query_count, queries
def _has_demo_data(self):
return bool(self.env['ir.module.module'].search_count([('demo', '=', True)]))
def test_perf_sql_queries_shop(self):
# To increase the query count you must ask the permission to al
query_count, queries = self._get_queries_shop()
if self._has_demo_data():
query_count += 5
queries['account_tax'] += 1
queries['account_account_tag'] += 2
queries['product_template_attribute_value'] += 2
self.assertEqual(sum(queries.values()), query_count, 'Please learn to count.')
self._check_url_hot_query('/shop', query_count, queries)
@tagged('post_install', '-at_install')
class TestWebsiteAllPerformanceShop(TestWebsiteAllPerformance):
def test_perf_sql_queries_shop(self):
# To increase the query count you must ask the permission to al
query_count, queries = self._get_queries_shop()
query_count += 3
queries['account_tax'] += 1
queries['account_account_tag'] += 2
if self._has_demo_data():
query_count += 2
queries['product_template_attribute_value'] += 2
self.assertEqual(sum(queries.values()), query_count, 'Please learn to count.')
self._check_url_hot_query('/shop', query_count, queries)