vanilla 18.0

This commit is contained in:
Ernad Husremovic 2025-10-08 10:48:09 +02:00
parent 5454004ff9
commit d7f6d2725e
979 changed files with 428093 additions and 0 deletions

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

@ -0,0 +1,50 @@
import io
import json
from lxml import etree
from zipfile import ZipFile
from odoo import http
from odoo.tests.common import HttpCase
class TestPivotExport(HttpCase):
def test_export_xlsx_with_integer_column(self):
""" Test the export_xlsx method of the pivot controller with int columns """
self.authenticate('admin', 'admin')
jdata = {
'title': 'Sales Analysis',
'model': 'sale.report',
'measure_count': 1,
'origin_count': 1,
'col_group_headers': [
[{'title': 500, 'width': 1, 'height': 1}],
],
'measure_headers': [],
'origin_headers': [],
'rows': [
{'title': 1, 'indent': 0, 'values': [{'value': 42}]},
],
}
response = self.url_open(
'/web/pivot/export_xlsx',
data={
'data': json.dumps(jdata),
'csrf_token': http.Request.csrf_token(self),
},
)
response.raise_for_status()
zip_file = ZipFile(io.BytesIO(response.content))
with zip_file.open('xl/worksheets/sheet1.xml') as file:
sheet_tree = etree.parse(file)
xml_data = {}
for c in sheet_tree.iterfind('.//{http://schemas.openxmlformats.org/spreadsheetml/2006/main}c'):
cell_ref = c.attrib['r']
value = c.findtext('{http://schemas.openxmlformats.org/spreadsheetml/2006/main}v')
xml_data[cell_ref] = value
self.assertEqual(xml_data['B1'], '500')
self.assertEqual(xml_data['A2'], '0')
self.assertEqual(xml_data['B2'], '42')

View file

@ -0,0 +1,61 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo.tests import Form, 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")
claude = self.users[4]
user_ids = [id_ for id_, __ in ResUsers.with_user(claude).name_search('', limit=2)]
self.assertEqual(claude.id, user_ids[0], "The current user, Claude, should be the first in the result.")
self.assertNotEqual(claude.id, user_ids[1], "The current user, Claude, should not appear twice in the result")
user_ids = [id_ for id_, __ in ResUsers.with_user(claude).name_search('', limit=5)]
self.assertEqual(len(user_ids), len(set(user_ids)), "Some user(s), appear multiple times in the result")
def test_change_password(self):
'''
We should be able to change user password without any issue
'''
user_internal = self.env['res.users'].create({
'name': 'Internal',
'login': 'user_internal',
'password': 'password',
'groups_id': [self.env.ref('base.group_user').id],
})
with Form(self.env['change.password.wizard'].with_context(active_model='res.users', active_ids=user_internal.ids), view='base.change_password_wizard_view') as form:
with form.user_ids.edit(0) as line:
line.new_passwd = 'bla'
rec = form.save()
rec.change_password_button()

View file

@ -0,0 +1,84 @@
from odoo.tests.common import TransactionCase
from odoo.addons.web.controllers.utils import get_action_triples, get_action
class TestWebRouter(TransactionCase):
def test_router_get_action_exist(self):
ir_cron_act = self.env.ref('base.ir_cron_act')
valid_actions = [
f'action-{ir_cron_act.id}', # record id
'action-base.ir_cron_act', # xml id
'm-ir.cron', # m- model name (for website)
'ir.cron', # dotted model name
'crons', # action path
]
for action in valid_actions:
with self.subTest(action=action):
self.assertEqual(get_action(self.env, action), ir_cron_act)
def test_router_get_action_missing(self):
Actions = self.env['ir.actions.actions']
missing_actions = [
'action-999999999',
'action-base.idontexist',
'm-base', # abstract model
'm-idontexist',
'base.idontexist',
'idontexist',
]
for action in missing_actions:
with self.subTest(action=action):
self.assertEqual(get_action(self.env, action), Actions)
def test_router_get_action_triples_exist(self):
base = self.env['ir.module.module'].search([('name', '=', 'base')])
user = self.env.user
ir_cron_act = self.env.ref('base.ir_cron_act')
matrix = {
# single action
f'action-{ir_cron_act.id}': [(None, ir_cron_act, None)],
'action-base.ir_cron_act': [(None, ir_cron_act, None)],
'm-ir.cron': [(None, ir_cron_act, None)],
'ir.cron': [(None, ir_cron_act, None)],
'crons': [(None, ir_cron_act, None)],
# multiple actions, all are accessible by clicking in the web client
# Apps > Base > Module info
f'apps/{base.id}/ir.module.module/{base.id}': [
(None, self.env.ref('base.open_module_tree'), base.id),
(base.id, self.env.ref('base.open_module_tree'), base.id)],
# Settings > Users & Companies > Users > Marc Demo > Related Partner
f'users/{user.id}/res.partner/{user.partner_id.id}': [
(None, self.env.ref('base.action_res_users'), user.id),
(user.id, self.env.ref('base.action_partner_form'), user.partner_id.id)],
# Settings > Users & Companies > Users > Marc Demo > Access Right > TOTP
f'users/{user.id}/ir.model.access/ir.model.access/146': [
(None, self.env.ref('base.action_res_users'), user.id),
(user.id, self.env.ref('base.ir_access_act'), None),
(user.id, self.env.ref('base.ir_access_act'), 146),
]
}
for path, triples in matrix.items():
with self.subTest(path=path):
self.assertEqual(list(get_action_triples(self.env, path)), triples)
def test_router_get_action_triples_missing(self):
# single unknown action
missing_actions = [
'action-999999999',
'action-base.idontexist',
'm-base',
'm-idontexist',
'base.idontexist',
'idontexist',
]
for action in missing_actions:
with self.subTest(path=action):
with self.assertRaises(ValueError) as capture:
all(get_action_triples(self.env, action))
self.assertEqual(capture.exception.args[0],
f"expected action at word 0 but found “{action}")

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,27 @@
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):
res_partner_model_id = self.env["ir.model"].search([("model", "=", "res.partner")]).id
self.env["ir.model.fields"].create({
"name": "x_date",
"ttype": "date",
"model": "res.partner",
"model_id": res_partner_model_id,
})
first, second = self.env["res.partner"].create([
{
"name": "first",
"x_date": fields.Date.to_date("2021-06-01")
},
{
"name": "second",
"x_date": fields.Date.to_date("2021-07-01")
}
])
groups = self.env["res.partner"].web_read_group([["id", "in", [first.id, second.id]]], [], groupby=["x_date"], limit=1)
self.assertEqual(groups["length"], 2)

View file

@ -0,0 +1,26 @@
# 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()
def test_web_route_redirect_param_legacy(self):
# This test is for legacy routes with /web and fragement
web_response = self.url_open('/web#cids=1&action=887&menu_id=124')
web_response.raise_for_status()
response_url_query = url_parse(web_response.url).query
self.assertEqual(response_url_query, 'redirect=%2Fweb%3F')
def test_web_route_redirect_param(self):
# This test if for the new routes with /odoo, pathname and query params
web_response = self.url_open('/odoo/action-887?cids=1')
web_response.raise_for_status()
response_url_query = url_parse(web_response.url).query
self.assertEqual(response_url_query, 'redirect=%2Fodoo%2Faction-887%3Fcids%3D1')

View file

@ -0,0 +1,109 @@
# -*- 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"], "/odoo")
self.assertEqual(data["start_url"], "/odoo")
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("/odoo?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"], "/odoo")
self.assertEqual(data["start_url"], "/odoo")
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_webmanifest_scoped(self):
response = self.url_open("/web/manifest.scoped_app_manifest?app_id=test&path=/test&app_name=Test")
self.assertEqual(response.status_code, 200)
self.assertEqual(response.headers["Content-Type"], "application/manifest+json")
data = response.json()
self.assertEqual(data["name"], "Test")
self.assertEqual(data["scope"], "/test")
self.assertEqual(data["start_url"], "/test")
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': 'any', '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"], "/odoo")
def test_offline_url(self):
"""
This route returns the offline page
"""
response = self.url_open("/odoo/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("/odoo")
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.",
)