mirror of
https://github.com/bringout/oca-ocb-core.git
synced 2026-04-20 17:31:59 +02:00
19.0 vanilla
This commit is contained in:
parent
d1963a3c3a
commit
2d3ee4855a
7430 changed files with 2687981 additions and 2965473 deletions
|
|
@ -24,6 +24,7 @@ from . import test_install
|
|||
from . import test_avatar_mixin
|
||||
from . import test_init
|
||||
from . import test_ir_actions
|
||||
from . import test_ir_asset
|
||||
from . import test_ir_attachment
|
||||
from . import test_ir_cron
|
||||
from . import test_ir_filters
|
||||
|
|
|
|||
|
|
@ -0,0 +1,35 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
|
||||
<asset id="base.test_asset_tag_aaa" name="Test asset tag (init active => keep active => update active)">
|
||||
<bundle>test_asset_bundle</bundle>
|
||||
<path>base/tests/something.scss</path>
|
||||
</asset>
|
||||
<asset id="base.test_asset_tag_aii" name="Test asset tag (init active => make inactive => update inactive)">
|
||||
<bundle>test_asset_bundle</bundle>
|
||||
<path>base/tests/something.scss</path>
|
||||
</asset>
|
||||
<asset id="base.test_asset_tag_aia" name="Test asset tag (init active => make inactive => update active)">
|
||||
<bundle>test_asset_bundle</bundle>
|
||||
<path>base/tests/something.scss</path>
|
||||
<field name="active">True</field> <!-- Take into account during update -->
|
||||
</asset>
|
||||
<asset id="base.test_asset_tag_iii" name="Test asset tag (init inactive => keep inactive => update inactive)" active="False">
|
||||
<bundle>test_asset_bundle</bundle>
|
||||
<path>base/tests/something.scss</path>
|
||||
</asset>
|
||||
<asset id="base.test_asset_tag_iaa" name="Test asset tag (init inactive => make active => update active)" active="False">
|
||||
<bundle>test_asset_bundle</bundle>
|
||||
<path>base/tests/something.scss</path>
|
||||
</asset>
|
||||
<asset id="base.test_asset_tag_prepend" name="Test asset tag with directive">
|
||||
<bundle directive="prepend">test_asset_bundle</bundle>
|
||||
<path>base/tests/something.scss</path>
|
||||
</asset>
|
||||
<asset id="base.test_asset_tag_extra" name="Test asset tag with extra field">
|
||||
<bundle>test_asset_bundle</bundle>
|
||||
<path>base/tests/something.scss</path>
|
||||
<field name="sequence" eval="17"/>
|
||||
</asset>
|
||||
|
||||
</odoo>
|
||||
|
|
@ -89,7 +89,7 @@ class BaseCommon(TransactionCase):
|
|||
|
||||
@classmethod
|
||||
def get_default_groups(cls):
|
||||
return cls.env['res.users']._default_groups()
|
||||
return cls.env.ref('base.group_user')
|
||||
|
||||
@classmethod
|
||||
def setup_main_company(cls, currency_code='USD'):
|
||||
|
|
@ -256,7 +256,7 @@ class SavepointCaseWithUserDemo(TransactionCase):
|
|||
'name': 'Austin Kennedy', # Tom Ruiz
|
||||
})],
|
||||
}, {
|
||||
'name': 'Pepper Street', # 'Deco Addict',
|
||||
'name': 'Pepper Street', # 'Acme Corporation',
|
||||
'state_id': cls.env.ref('base.state_us_2').id,
|
||||
'child_ids': [Command.create({
|
||||
'name': 'Liam King', # 'Douglas Fletcher',
|
||||
|
|
|
|||
|
|
@ -49,6 +49,7 @@ reportgz = False
|
|||
screencasts =
|
||||
screenshots = /tmp/odoo_tests
|
||||
server_wide_modules = base,rpc,web
|
||||
skip_auto_install = False
|
||||
smtp_password =
|
||||
smtp_port = 25
|
||||
smtp_server = localhost
|
||||
|
|
|
|||
|
|
@ -159,6 +159,32 @@ class TestParentStore(TransactionCase):
|
|||
self.assertEqual(len(old_struct), 4, "After duplication, previous record must have old childs records only")
|
||||
self.assertFalse(new_struct & old_struct, "After duplication, nodes should not be mixed")
|
||||
|
||||
def test_missing_parent(self):
|
||||
""" Missing parent id should not raise an error. """
|
||||
# Missing parent with _parent_store
|
||||
new_cat0 = self.cat0.copy()
|
||||
records = new_cat0.search([('parent_id', 'parent_of', 999999999)])
|
||||
self.assertEqual(len(records), 0)
|
||||
|
||||
# Missing parent without _parent_store
|
||||
category = self.env['res.partner.category']
|
||||
self.patch(self.env.registry['res.partner.category'], '_parent_store', False)
|
||||
records = category.search([('parent_id', 'child_of', 999999999)])
|
||||
self.assertEqual(len(records), 0)
|
||||
|
||||
def test_missing_child(self):
|
||||
""" Missing child id should not raise an error. """
|
||||
# Missing child with _parent_store
|
||||
new_cat0 = self.cat0.copy()
|
||||
records = new_cat0.search([('parent_id', 'child_of', 999999999)])
|
||||
self.assertEqual(len(records), 0)
|
||||
|
||||
# Missing child without _parent_store
|
||||
category = self.env['res.partner.category']
|
||||
self.patch(self.env.registry['res.partner.category'], '_parent_store', False)
|
||||
records = category.search([('parent_id', 'child_of', 999999999)])
|
||||
self.assertEqual(len(records), 0)
|
||||
|
||||
def test_duplicate_children_01(self):
|
||||
""" Duplicate the children then reassign them to the new parent (1st method). """
|
||||
new_cat1 = self.cat1.copy()
|
||||
|
|
|
|||
|
|
@ -1,15 +1,12 @@
|
|||
import io
|
||||
import os
|
||||
import re
|
||||
import subprocess as sp
|
||||
import sys
|
||||
import textwrap
|
||||
import time
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
|
||||
from odoo.cli.command import commands, load_addons_commands, load_internal_commands
|
||||
from odoo.tests import BaseCase, TransactionCase
|
||||
from odoo.tests import BaseCase
|
||||
from odoo.tools import config, file_path
|
||||
|
||||
|
||||
|
|
@ -132,55 +129,3 @@ class TestCommand(BaseCase):
|
|||
# we skip local variables as they differ based on configuration (e.g.: if a database is specified or not)
|
||||
lines = [line for line in shell.stdout.read().splitlines() if line.startswith('>>>')]
|
||||
self.assertEqual(lines, [">>> Hello from Python!", '>>> '])
|
||||
|
||||
|
||||
class TestCommandUsingDb(TestCommand, TransactionCase):
|
||||
|
||||
@unittest.skipIf(
|
||||
os.name != 'posix' and sys.version_info < (3, 12),
|
||||
"os.set_blocking on files only available in windows starting 3.12",
|
||||
)
|
||||
def test_i18n_export(self):
|
||||
# i18n export is a process that takes a long time to run, we are
|
||||
# not interrested in running it in full, we are only interrested
|
||||
# in making sure it starts correctly.
|
||||
#
|
||||
# This test only asserts the first few lines and then SIGTERM
|
||||
# the process. We took the challenge to write a cross-platform
|
||||
# test, the lack of a select-like API for Windows makes the code
|
||||
# a bit complicated. Sorry :/
|
||||
|
||||
expected_text = textwrap.dedent("""\
|
||||
# Translation of Odoo Server.
|
||||
# This file contains the translation of the following modules:
|
||||
# \t* base
|
||||
""").encode()
|
||||
|
||||
proc = self.popen_command(
|
||||
'i18n', 'export', '-d', self.env.cr.dbname, '-o', '-', 'base',
|
||||
# ensure we get a io.FileIO and not a buffered or text shit
|
||||
text=False, bufsize=0,
|
||||
)
|
||||
|
||||
# Feed the buffer for maximum 15 seconds.
|
||||
buffer = io.BytesIO()
|
||||
timeout = time.monotonic() + 15
|
||||
os.set_blocking(proc.stdout.fileno(), False)
|
||||
while buffer.tell() < len(expected_text) and time.monotonic() < timeout:
|
||||
if chunk := proc.stdout.read(len(expected_text) - buffer.tell()):
|
||||
buffer.write(chunk)
|
||||
else:
|
||||
# would had loved to use select() for its timeout, but
|
||||
# select doesn't work on files on windows, use a flat
|
||||
# sleep instead: not great, not terrible.
|
||||
time.sleep(.1)
|
||||
|
||||
proc.terminate()
|
||||
try:
|
||||
proc.wait(timeout=5)
|
||||
except sp.TimeoutExpired:
|
||||
proc.kill()
|
||||
raise
|
||||
|
||||
self.assertEqual(buffer.getvalue(), expected_text,
|
||||
"The subprocess did not write the prelude in under 15 seconds.")
|
||||
|
|
|
|||
|
|
@ -152,6 +152,7 @@ class TestConfigManager(TransactionCase):
|
|||
|
||||
# advanced
|
||||
'dev_mode': [],
|
||||
'skip_auto_install': False,
|
||||
'stop_after_init': False,
|
||||
'osv_memory_count_limit': 0,
|
||||
'transient_age_limit': 1.0,
|
||||
|
|
@ -270,6 +271,7 @@ class TestConfigManager(TransactionCase):
|
|||
# advanced
|
||||
'dev_mode': ['xml'], # blacklist for save, read from the config file
|
||||
'stop_after_init': False,
|
||||
'skip_auto_install': False,
|
||||
'osv_memory_count_limit': 71,
|
||||
'transient_age_limit': 4.0,
|
||||
'max_cron_threads': 4,
|
||||
|
|
@ -390,6 +392,7 @@ class TestConfigManager(TransactionCase):
|
|||
'init': {},
|
||||
'publisher_warranty_url': 'http://services.odoo.com/publisher-warranty/',
|
||||
'save': False,
|
||||
'skip_auto_install': False,
|
||||
'stop_after_init': False,
|
||||
|
||||
# undocummented options
|
||||
|
|
@ -570,6 +573,7 @@ class TestConfigManager(TransactionCase):
|
|||
|
||||
# advanced
|
||||
'dev_mode': ['xml', 'reload'],
|
||||
'skip_auto_install': False,
|
||||
'stop_after_init': True,
|
||||
'osv_memory_count_limit': 71,
|
||||
'transient_age_limit': 4.0,
|
||||
|
|
@ -629,6 +633,7 @@ class TestConfigManager(TransactionCase):
|
|||
'upgrade_path': [],
|
||||
'pre_upgrade_scripts': [],
|
||||
'server_wide_modules': ['web', 'base', 'mail'],
|
||||
'skip_auto_install': False,
|
||||
'data_dir': '/tmp/data-dir',
|
||||
|
||||
# HTTP
|
||||
|
|
|
|||
|
|
@ -1236,14 +1236,14 @@ class TestExpression(SavepointCaseWithUserDemo, TransactionExpressionCase):
|
|||
|
||||
# indirect search via m2o
|
||||
Partner = self.env['res.partner']
|
||||
deco_addict = self._search(Partner, [('name', '=', 'Pepper Street')])
|
||||
acme_corp = self._search(Partner, [('name', '=', 'Pepper Street')])
|
||||
|
||||
not_be = self._search(Partner, [('country_id', '!=', 'Belgium')])
|
||||
self.assertNotIn(deco_addict, not_be)
|
||||
self.assertNotIn(acme_corp, not_be)
|
||||
|
||||
Partner = Partner.with_context(lang='fr_FR')
|
||||
not_be = self._search(Partner, [('country_id', '!=', 'Belgique')])
|
||||
self.assertNotIn(deco_addict, not_be)
|
||||
self.assertNotIn(acme_corp, not_be)
|
||||
|
||||
def test_or_with_implicit_and(self):
|
||||
# Check that when using expression.OR on a list of domains with at least one
|
||||
|
|
@ -1793,36 +1793,78 @@ class TestQueries(TransactionCase):
|
|||
''']):
|
||||
Model.search([])
|
||||
|
||||
@mute_logger('odoo.models.unlink')
|
||||
def test_access_rules_active_test(self):
|
||||
Model = self.env['res.partner'].with_user(self.env.ref('base.user_admin'))
|
||||
self.env['ir.rule'].search([]).unlink()
|
||||
PartnerCateg = self.env['res.partner.category']
|
||||
|
||||
model_id = self.env['ir.model']._get('res.partner.category').id
|
||||
self.env['ir.rule'].search([('model_id', '=', model_id)]).unlink()
|
||||
self.env['ir.rule'].create([{
|
||||
'name': 'partner users rule',
|
||||
'model_id': self.env['ir.model']._get('res.partner').id,
|
||||
'domain_force': str([('user_ids.login', 'like', '%@%')]),
|
||||
'name': 'categ childs rule',
|
||||
'model_id': model_id,
|
||||
'domain_force': str([('child_ids', 'not any', [('name', 'ilike', 'private')])]),
|
||||
}, {
|
||||
'name': 'partners rule',
|
||||
'model_id': self.env['ir.model']._get('res.partner').id,
|
||||
'domain_force': str([('commercial_partner_id.name', '=', 'John')]),
|
||||
'name': 'categ rule',
|
||||
'model_id': model_id,
|
||||
'domain_force': str([('parent_id.name', 'ilike', 'public')]),
|
||||
}])
|
||||
Model.search([])
|
||||
|
||||
pub_active, pub_inactive, pri_active, pri_inactive = PartnerCateg.create([
|
||||
{'name': 'public active', 'active': True},
|
||||
{'name': 'public inactive', 'active': False},
|
||||
{'name': 'private active', 'active': True},
|
||||
{'name': 'private inactive', 'active': False},
|
||||
])
|
||||
|
||||
accessible_records = PartnerCateg.create([
|
||||
{'name': 'a1', 'parent_id': pub_active.id},
|
||||
{'name': 'a2', 'parent_id': pub_inactive.id},
|
||||
{'name': 'a3', 'parent_id': pub_active.id, 'child_ids': [Command.create({'name': 'not PRI'})]},
|
||||
])
|
||||
inaccessible_records = PartnerCateg.create([
|
||||
{'name': 'ua1'}, # No public parent
|
||||
{'name': 'ua2', 'parent_id': pri_active.id},
|
||||
{'name': 'ua3', 'parent_id': pub_active.id, 'child_ids': [Command.link(pri_active.id)]},
|
||||
{'name': 'ua4', 'parent_id': pub_active.id, 'child_ids': [Command.link(pri_inactive.id)]},
|
||||
])
|
||||
records = accessible_records + inaccessible_records
|
||||
domain = [('id', 'in', records.ids)]
|
||||
|
||||
PartnerCateg = PartnerCateg.with_user(self.env.ref('base.user_admin'))
|
||||
PartnerCateg.search(domain) # warmup
|
||||
|
||||
with self.assertQueries(['''
|
||||
SELECT "res_partner"."id"
|
||||
FROM "res_partner"
|
||||
LEFT JOIN "res_partner" AS "res_partner__commercial_partner_id"
|
||||
ON ("res_partner"."commercial_partner_id" = "res_partner__commercial_partner_id"."id")
|
||||
WHERE "res_partner"."active" IS TRUE AND (
|
||||
("res_partner"."commercial_partner_id" IS NOT NULL AND "res_partner__commercial_partner_id"."name" IN %s)
|
||||
AND EXISTS(SELECT FROM (
|
||||
SELECT "res_users"."partner_id" AS __inverse
|
||||
FROM "res_users"
|
||||
WHERE "res_users"."login" LIKE %s
|
||||
) AS __sub WHERE __inverse = "res_partner"."id")
|
||||
)
|
||||
ORDER BY "res_partner"."complete_name" ASC, "res_partner"."id" DESC
|
||||
SELECT "res_partner_category"."id"
|
||||
FROM "res_partner_category"
|
||||
LEFT JOIN "res_partner_category" AS "res_partner_category__parent_id" ON (
|
||||
"res_partner_category"."parent_id" = "res_partner_category__parent_id"."id")
|
||||
WHERE ("res_partner_category"."active" IS TRUE AND "res_partner_category"."id" IN %s)
|
||||
AND (NOT EXISTS(
|
||||
SELECT FROM (
|
||||
SELECT "res_partner_category"."parent_id" AS __inverse
|
||||
FROM "res_partner_category"
|
||||
WHERE
|
||||
(
|
||||
"res_partner_category"."name" ->> %s ILIKE %s
|
||||
AND "res_partner_category"."parent_id" IS NOT NULL
|
||||
)
|
||||
) AS __sub
|
||||
WHERE __inverse = "res_partner_category"."id"
|
||||
)
|
||||
AND (
|
||||
"res_partner_category"."parent_id" IS NOT NULL
|
||||
AND "res_partner_category__parent_id"."name" ->> %s ILIKE %s
|
||||
)
|
||||
)
|
||||
ORDER BY "res_partner_category"."name" ->> %s, "res_partner_category"."id"
|
||||
''']):
|
||||
Model.search([])
|
||||
records_search = PartnerCateg.search(domain)
|
||||
|
||||
self.assertEqual(records_search, accessible_records)
|
||||
self.assertEqual(
|
||||
records.with_user(self.env.ref('base.user_admin'))._filtered_access('read'),
|
||||
accessible_records,
|
||||
)
|
||||
|
||||
def test_access_rules_active_test_neg(self):
|
||||
Model = self.env['res.partner'].with_user(self.env.ref('base.user_admin'))
|
||||
|
|
|
|||
|
|
@ -221,9 +221,10 @@ class TestRequestRemainingAfterFirstCheck(TestRequestRemainingCommon):
|
|||
s.get(self.base_url() + "/web/concurrent", timeout=10)
|
||||
|
||||
type(self).thread_a = threading.Thread(target=late_request_thread)
|
||||
main_lock = self.main_lock
|
||||
self.thread_a.start()
|
||||
# we need to ensure that the first check is made and that we are aquiring the lock
|
||||
self.main_lock.acquire()
|
||||
main_lock.acquire()
|
||||
|
||||
def assertCanOpenTestCursor(self):
|
||||
super().assertCanOpenTestCursor()
|
||||
|
|
|
|||
|
|
@ -70,6 +70,28 @@ class TestIntervals(TransactionCase):
|
|||
[(0, 5), (12, 13), (20, 22), (23, 24)],
|
||||
)
|
||||
|
||||
def test_keep_distinct(self):
|
||||
""" Test merge operations between two Intervals
|
||||
instances with different _keep_distinct flags.
|
||||
"""
|
||||
|
||||
A = Intervals(self.ints([(0, 10)]), keep_distinct=False)
|
||||
B = Intervals(self.ints([(-5, 5), (5, 15)]), keep_distinct=True)
|
||||
|
||||
C = A & B
|
||||
# The _keep_distinct flag must be the same as the left one
|
||||
self.assertFalse(C._keep_distinct)
|
||||
self.assertEqual(len(C), 1)
|
||||
self.assertEqual(list(C), self.ints([(0, 10)]))
|
||||
|
||||
# If, as a result of the above operation, C has _keep_distinct = False
|
||||
# but is not preserving its _items, the following operation must raise
|
||||
# an error
|
||||
D = Intervals()
|
||||
C = C - D
|
||||
self.assertFalse(C._keep_distinct)
|
||||
self.assertEqual(C._items, self.ints([(0, 10)]))
|
||||
|
||||
|
||||
class TestUtils(TransactionCase):
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,77 @@
|
|||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo.tests.common import TransactionCase, tagged
|
||||
from odoo.tools import convert_file
|
||||
from odoo.tools.misc import file_path
|
||||
|
||||
|
||||
@tagged('-at_install', 'post_install')
|
||||
class TestAsset(TransactionCase):
|
||||
|
||||
def test_asset_tag(self):
|
||||
"""
|
||||
Verify that assets defined with the <asset> tag are properly imported.
|
||||
"""
|
||||
# Load new records
|
||||
convert_file(
|
||||
self.env, 'base',
|
||||
file_path('base/tests/asset_tag.xml'),
|
||||
{}, 'init', False,
|
||||
)
|
||||
active_keep_asset = self.env.ref('base.test_asset_tag_aaa')
|
||||
inactive_keep_asset = self.env.ref('base.test_asset_tag_iii')
|
||||
active_switch_asset_reset = self.env.ref('base.test_asset_tag_aia')
|
||||
active_switch_asset_ignore = self.env.ref('base.test_asset_tag_aii')
|
||||
inactive_switch_asset = self.env.ref('base.test_asset_tag_iaa')
|
||||
prepend_asset = self.env.ref('base.test_asset_tag_prepend')
|
||||
asset_with_extra_field = self.env.ref('base.test_asset_tag_extra')
|
||||
|
||||
# Verify initial load
|
||||
self.assertEqual(prepend_asset._name, 'ir.asset', 'Model should be ir.asset')
|
||||
self.assertEqual(prepend_asset.name, 'Test asset tag with directive', 'Name not loaded')
|
||||
self.assertEqual(prepend_asset.directive, 'prepend', 'Directive not loaded')
|
||||
self.assertEqual(prepend_asset.bundle, 'test_asset_bundle', 'Bundle not loaded')
|
||||
self.assertEqual(prepend_asset.path, 'base/tests/something.scss', 'Path not loaded')
|
||||
self.assertEqual(asset_with_extra_field.sequence, 17, 'Sequence not loaded')
|
||||
self.assertTrue(active_keep_asset.active, 'Should be active')
|
||||
self.assertTrue(active_switch_asset_reset.active, 'Should be active')
|
||||
self.assertTrue(active_switch_asset_ignore.active, 'Should be active')
|
||||
self.assertFalse(inactive_keep_asset.active, 'Should be inactive')
|
||||
self.assertFalse(inactive_switch_asset.active, 'Should be inactive')
|
||||
|
||||
# Patch records
|
||||
prepend_asset.name = 'changed'
|
||||
prepend_asset.directive = 'append'
|
||||
prepend_asset.bundle = 'changed'
|
||||
prepend_asset.path = 'base/tests/changed.scss'
|
||||
asset_with_extra_field.sequence = 3
|
||||
active_switch_asset_reset.active = False
|
||||
active_switch_asset_ignore.active = False
|
||||
inactive_switch_asset.active = True
|
||||
|
||||
# Update records
|
||||
convert_file(
|
||||
self.env, 'base',
|
||||
file_path('base/tests/asset_tag.xml'),
|
||||
{
|
||||
'base.test_asset_tag_aaa': active_keep_asset.id,
|
||||
'base.test_asset_tag_iii': inactive_keep_asset.id,
|
||||
'base.test_asset_tag_aia': active_switch_asset_reset.id,
|
||||
'base.test_asset_tag_aii': active_switch_asset_ignore.id,
|
||||
'base.test_asset_tag_iaa': inactive_switch_asset.id,
|
||||
'base.test_asset_tag_prepend': prepend_asset.id,
|
||||
'base.test_asset_tag_extra': asset_with_extra_field.id,
|
||||
}, 'update', False,
|
||||
)
|
||||
|
||||
# Verify updated load
|
||||
self.assertEqual(prepend_asset.name, 'Test asset tag with directive', 'Name not restored')
|
||||
self.assertEqual(prepend_asset.directive, 'prepend', 'Directive not restored')
|
||||
self.assertEqual(prepend_asset.bundle, 'test_asset_bundle', 'Bundle not restored')
|
||||
self.assertEqual(prepend_asset.path, 'base/tests/something.scss', 'Path not restored')
|
||||
self.assertEqual(asset_with_extra_field.sequence, 17, 'Sequence not restored')
|
||||
self.assertTrue(active_keep_asset.active, 'Should be active')
|
||||
self.assertTrue(active_switch_asset_reset.active, 'Should be reset to active')
|
||||
self.assertFalse(active_switch_asset_ignore.active, 'Should be kept inactive')
|
||||
self.assertFalse(inactive_keep_asset.active, 'Should be inactive')
|
||||
self.assertTrue(inactive_switch_asset.active, 'Should be kept active')
|
||||
|
|
@ -114,6 +114,21 @@ class TestIrCron(TransactionCase, CronMixinCase):
|
|||
self.assertEqual(self.cron.lastcall, fields.Datetime.now())
|
||||
self.assertEqual(self.partner.name, 'You have been CRONWNED')
|
||||
|
||||
def test_cron_direct_trigger_exception(self):
|
||||
self.cron.code = textwrap.dedent("raise UserError('oops')")
|
||||
with (
|
||||
self.enter_registry_test_mode(),
|
||||
self.assertLogs('odoo.addons.base.models.ir_cron', 40), # logging.ERROR
|
||||
self.registry.cursor() as cron_cr,
|
||||
):
|
||||
action = self.cron.with_env(self.env(cr=cron_cr)).method_direct_trigger()
|
||||
|
||||
self.assertNotEqual(action, True)
|
||||
action_params = action.pop('params')
|
||||
self.assertEqual(action, {'type': 'ir.actions.client', 'tag': 'display_exception'})
|
||||
self.assertEqual(list(action_params), ['code', 'message', 'data'])
|
||||
self.assertEqual(list(action_params['data']), ['name', 'message', 'arguments', 'context', 'debug'])
|
||||
|
||||
def test_cron_no_job_ready(self):
|
||||
self.cron.nextcall = fields.Datetime.now() + timedelta(days=1)
|
||||
self.cron.flush_recordset()
|
||||
|
|
|
|||
|
|
@ -475,7 +475,7 @@ class TestIrMailServer(TransactionCase, MockSmtplibCase):
|
|||
)
|
||||
|
||||
def test_eml_attachment_encoding(self):
|
||||
"""Test that message/rfc822 attachments are encoded using 7bit, 8bit, or binary encoding."""
|
||||
"""Test that message/rfc822 attachments are encoded using 7bit, 8bit, or binary encoding per RFC."""
|
||||
IrMailServer = self.env['ir.mail_server']
|
||||
|
||||
# Create a sample .eml file content
|
||||
|
|
@ -491,12 +491,43 @@ class TestIrMailServer(TransactionCase, MockSmtplibCase):
|
|||
attachments=attachments,
|
||||
)
|
||||
|
||||
# Verify that the attachment is correctly encoded
|
||||
acceptable_encodings = {'7bit', '8bit', 'binary'}
|
||||
found_rfc822_part = False
|
||||
|
||||
for part in message.iter_attachments():
|
||||
if part.get_content_type() == 'message/rfc822':
|
||||
found_rfc822_part = True
|
||||
# Get Content-Transfer-Encoding, defaulting to '7bit' if not present (per RFC)
|
||||
encoding = part.get('Content-Transfer-Encoding', '7bit').lower()
|
||||
|
||||
self.assertIn(
|
||||
part.get('Content-Transfer-Encoding'),
|
||||
encoding,
|
||||
acceptable_encodings,
|
||||
"The message/rfc822 attachment should be encoded using 7bit, 8bit, or binary encoding.",
|
||||
f"RFC violation: message/rfc822 attachment has Content-Transfer-Encoding '{encoding}'. "
|
||||
f"Only 7bit, 8bit, or binary encoding is permitted per RFC 2046 Section 5.2.1."
|
||||
)
|
||||
|
||||
self.assertTrue(found_rfc822_part, "No message/rfc822 attachment found in the built email")
|
||||
|
||||
def test_eml_message_serialization_with_non_ascii(self):
|
||||
"""Ensure an email with a message/rfc822 attachment containing non-ASCII chars can be serialized."""
|
||||
IrMailServer = self.env['ir.mail_server']
|
||||
|
||||
# .eml content with non-ASCII character
|
||||
eml_content = "From: user@example.com\nTo: user2@example.com\nSubject: Test\n\nBody with é"
|
||||
attachments = [('test.eml', eml_content.encode(), 'message/rfc822')]
|
||||
|
||||
message = IrMailServer._build_email__(
|
||||
email_from='john.doe@from.example.com',
|
||||
email_to='destinataire@to.example.com',
|
||||
subject='Serialization test',
|
||||
body='This email contains a .eml attachment.',
|
||||
attachments=attachments,
|
||||
)
|
||||
|
||||
try:
|
||||
serialized = message.as_string().encode('utf-8')
|
||||
except UnicodeEncodeError as e:
|
||||
raise AssertionError("Email with non-ASCII .eml attachment could not be serialized") from e
|
||||
|
||||
self.assertIsInstance(serialized, bytes)
|
||||
|
|
|
|||
|
|
@ -27,6 +27,7 @@ except ImportError:
|
|||
aiosmtpd = None
|
||||
|
||||
|
||||
SMTP_TIMEOUT = 5
|
||||
PASSWORD = 'secretpassword'
|
||||
_openssl = shutil.which('openssl')
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
|
@ -68,7 +69,7 @@ class Certificate:
|
|||
@unittest.skipUnless(aiosmtpd, "aiosmtpd couldn't be imported")
|
||||
@unittest.skipUnless(_openssl, "openssl not found in path")
|
||||
# fail fast for timeout errors
|
||||
@patch('odoo.addons.base.models.ir_mail_server.SMTP_TIMEOUT', .1)
|
||||
@patch('odoo.addons.base.models.ir_mail_server.SMTP_TIMEOUT', SMTP_TIMEOUT)
|
||||
# prevent the CLI from interfering with the tests
|
||||
@patch.dict(config.options, {'smtp_server': ''})
|
||||
class TestIrMailServerSMTPD(TransactionCaseWithUserDemo):
|
||||
|
|
@ -146,8 +147,8 @@ class TestIrMailServerSMTPD(TransactionCaseWithUserDemo):
|
|||
# when resolving "localhost" (so stupid), use the following to
|
||||
# force aiosmtpd/odoo to bind/connect to a fixed ipv4 OR ipv6
|
||||
# address.
|
||||
family, _, cls.port = _find_free_local_address()
|
||||
cls.localhost = getaddrinfo('localhost', cls.port, family)
|
||||
family, addr, cls.port = _find_free_local_address()
|
||||
cls.localhost = getaddrinfo(addr, cls.port, family)
|
||||
cls.startClassPatcher(patch('socket.getaddrinfo', cls.getaddrinfo))
|
||||
|
||||
def setUp(self):
|
||||
|
|
@ -268,13 +269,14 @@ class TestIrMailServerSMTPD(TransactionCaseWithUserDemo):
|
|||
'smtp_ssl_private_key': private_key,
|
||||
})
|
||||
if error_pattern:
|
||||
with self.assertRaises(UserError) as error_capture:
|
||||
timeout = .1 if 'timed out' in error_pattern else SMTP_TIMEOUT
|
||||
with self.assertRaises(UserError) as error_capture, \
|
||||
patch('odoo.addons.base.models.ir_mail_server.SMTP_TIMEOUT', timeout):
|
||||
mail_server.test_smtp_connection()
|
||||
self.assertRegex(error_capture.exception.args[0], error_pattern)
|
||||
else:
|
||||
mail_server.test_smtp_connection()
|
||||
|
||||
|
||||
def test_authentication_login_matrix(self):
|
||||
"""
|
||||
Connect to a server that is authenticating users via a login/pwd
|
||||
|
|
@ -318,7 +320,9 @@ class TestIrMailServerSMTPD(TransactionCaseWithUserDemo):
|
|||
password=password):
|
||||
with self.start_smtpd(encryption, ssl_context, auth_required):
|
||||
if error_pattern:
|
||||
with self.assertRaises(UserError) as capture:
|
||||
timeout = .1 if 'timed out' in error_pattern else SMTP_TIMEOUT
|
||||
with self.assertRaises(UserError) as capture, \
|
||||
patch('odoo.addons.base.models.ir_mail_server.SMTP_TIMEOUT', timeout):
|
||||
mail_server.test_smtp_connection()
|
||||
self.assertRegex(capture.exception.args[0], error_pattern)
|
||||
else:
|
||||
|
|
@ -374,7 +378,9 @@ class TestIrMailServerSMTPD(TransactionCaseWithUserDemo):
|
|||
client_encryption=client_encryption):
|
||||
mail_server.smtp_encryption = client_encryption
|
||||
with self.start_smtpd(server_encryption, ssl_context, auth_required=False):
|
||||
with self.assertRaises(UserError) as capture:
|
||||
timeout = .1 if 'timed out' in error_pattern else SMTP_TIMEOUT
|
||||
with self.assertRaises(UserError) as capture, \
|
||||
patch('odoo.addons.base.models.ir_mail_server.SMTP_TIMEOUT', timeout):
|
||||
mail_server.test_smtp_connection()
|
||||
self.assertRegex(capture.exception.args[0], error_pattern)
|
||||
|
||||
|
|
|
|||
|
|
@ -221,7 +221,7 @@ class TestIrSequenceGenerate(BaseCase):
|
|||
isoyear, isoweek, weekday = datetime.now().isocalendar()
|
||||
self.assertEqual(
|
||||
env['ir.sequence'].next_by_code('test_sequence_type_9'),
|
||||
f"{isoyear}/{isoyear % 100}/1/{isoweek}/{weekday % 7}",
|
||||
f"{isoyear}/{isoyear % 100:02d}/1/{isoweek:02d}/{weekday % 7}",
|
||||
)
|
||||
|
||||
def test_ir_sequence_suffix(self):
|
||||
|
|
|
|||
|
|
@ -402,13 +402,15 @@ class TestHtmlTools(BaseCase):
|
|||
|
||||
def test_plaintext2html(self):
|
||||
cases = [
|
||||
("First \nSecond \nThird\n \nParagraph\n\r--\nSignature paragraph", 'div',
|
||||
("First \nSecond \nThird\n \nParagraph\n\r--\nSignature paragraph", 'div', True,
|
||||
"<div><p>First <br/>Second <br/>Third</p><p>Paragraph</p><p>--<br/>Signature paragraph</p></div>"),
|
||||
("First<p>It should be escaped</p>\nSignature", False,
|
||||
"<p>First<p>It should be escaped</p><br/>Signature</p>")
|
||||
("First<p>It should be escaped</p>\nSignature", False, True,
|
||||
"<p>First<p>It should be escaped</p><br/>Signature</p>"),
|
||||
("First \nSecond \nThird", False, False,
|
||||
"First <br/>Second <br/>Third"),
|
||||
]
|
||||
for content, container_tag, expected in cases:
|
||||
html = plaintext2html(content, container_tag)
|
||||
for content, container_tag, with_paragraph, expected in cases:
|
||||
html = plaintext2html(content, container_tag, with_paragraph)
|
||||
self.assertEqual(html, expected, 'plaintext2html is broken')
|
||||
|
||||
def test_html_html_to_inner_content(self):
|
||||
|
|
|
|||
|
|
@ -94,7 +94,7 @@ class TestModuleManifest(BaseCase):
|
|||
file.write(str({'name': f'Temp {self.module_name}'}))
|
||||
with self.assertLogs('odoo.modules.module', 'WARNING') as capture:
|
||||
manifest = Manifest.for_addon(self.module_name)
|
||||
manifest.manifest_cached
|
||||
manifest.raw_value('') # parse the manifest
|
||||
self.assertEqual(manifest['license'], 'LGPL-3')
|
||||
self.assertEqual(manifest['author'], '')
|
||||
self.assertIn("Missing `author` key", capture.output[0])
|
||||
|
|
|
|||
|
|
@ -58,6 +58,43 @@ class TestQwebFieldInteger(common.TransactionCase):
|
|||
"125.125k"
|
||||
)
|
||||
|
||||
|
||||
class TestQwebFieldFloatConverter(common.TransactionCase):
|
||||
def value_to_html(self, value, options=None):
|
||||
options = options or {}
|
||||
return self.env['ir.qweb.field.float'].value_to_html(value, options)
|
||||
|
||||
def test_float_value_to_html_no_precision(self):
|
||||
self.assertEqual(self.value_to_html(3), '3.0')
|
||||
self.assertEqual(self.value_to_html(3.1), '3.1')
|
||||
self.assertEqual(self.value_to_html(3.1231239), '3.123124')
|
||||
|
||||
def test_float_value_to_html_with_precision(self):
|
||||
options = {'precision': 3}
|
||||
self.assertEqual(self.value_to_html(3, options), '3.000')
|
||||
self.assertEqual(self.value_to_html(3.1, options), '3.100')
|
||||
self.assertEqual(self.value_to_html(3.123, options), '3.123')
|
||||
self.assertEqual(self.value_to_html(3.1239, options), '3.124')
|
||||
|
||||
def test_float_value_to_html_with_min_precision(self):
|
||||
options = {'min_precision': 3}
|
||||
self.assertEqual(self.value_to_html(0, options), '0.000')
|
||||
self.assertEqual(self.value_to_html(3, options), '3.000')
|
||||
self.assertEqual(self.value_to_html(3.1, options), '3.100')
|
||||
self.assertEqual(self.value_to_html(3.123, options), '3.123')
|
||||
self.assertEqual(self.value_to_html(3.1239, options), '3.1239')
|
||||
self.assertEqual(self.value_to_html(3.1231239, options), '3.123124')
|
||||
self.assertEqual(self.value_to_html(1234567890.1234567890, options), '1,234,567,890.12346')
|
||||
|
||||
def test_float_value_to_html_with_precision_and_min_precision(self):
|
||||
options = {'min_precision': 3, 'precision': 4}
|
||||
self.assertEqual(self.value_to_html(3, options), '3.000')
|
||||
self.assertEqual(self.value_to_html(3.1, options), '3.100')
|
||||
self.assertEqual(self.value_to_html(3.123, options), '3.123')
|
||||
self.assertEqual(self.value_to_html(3.1239, options), '3.1239')
|
||||
self.assertEqual(self.value_to_html(3.12349, options), '3.1235')
|
||||
|
||||
|
||||
class TestQwebFieldContact(common.TransactionCase):
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
|
|
@ -86,3 +123,53 @@ class TestQwebFieldContact(common.TransactionCase):
|
|||
self.assertIn(self.partner.website, result)
|
||||
self.assertNotIn(self.partner.phone, result)
|
||||
self.assertIn('itemprop="telephone"', result, "Empty telephone itemprop should be added to prevent issue with iOS Safari")
|
||||
|
||||
|
||||
class TestQwebFieldOne2Many(common.TransactionCase):
|
||||
def value_to_html(self, value, options=None):
|
||||
options = options or {}
|
||||
return self.env['ir.qweb.field.one2many'].value_to_html(value, options)
|
||||
|
||||
def test_one2many_empty(self):
|
||||
partner = self.env['res.partner'].create({'name': 'Test Parent'})
|
||||
self.assertFalse(self.value_to_html(partner.child_ids))
|
||||
|
||||
def test_one2many_with_values(self):
|
||||
parent = self.env['res.partner'].create({'name': 'Parent'})
|
||||
self.env['res.partner'].create({'name': 'Child', 'parent_id': parent.id})
|
||||
self.assertEqual(self.value_to_html(parent.child_ids), "Parent, Child")
|
||||
|
||||
|
||||
class TestQwebFieldMany2Many(common.TransactionCase):
|
||||
def value_to_html(self, value, options=None):
|
||||
options = options or {}
|
||||
return self.env['ir.qweb.field.many2many'].value_to_html(value, options)
|
||||
|
||||
def test_many2many_empty(self):
|
||||
user = self.env['res.users'].create({'name': 'UserTest', 'login': 'usertest@example.com', 'group_ids': None})
|
||||
self.assertFalse(self.value_to_html(user.group_ids))
|
||||
|
||||
def test_many2many_with_values(self):
|
||||
user = self.env['res.users'].create({
|
||||
'name': 'User2',
|
||||
'login': 'user2@example.com',
|
||||
})
|
||||
self.assertEqual(
|
||||
self.value_to_html(user.all_group_ids[:2].sorted()),
|
||||
'Role / User, Technical Features',
|
||||
)
|
||||
|
||||
|
||||
class TestQwebFieldMany2One(common.TransactionCase):
|
||||
def value_to_html(self, value, options=None):
|
||||
options = options or {}
|
||||
return self.env['ir.qweb.field.many2one'].value_to_html(value, options)
|
||||
|
||||
def test_many2one_empty(self):
|
||||
partner = self.env['res.partner'].create({'name': 'Lonely'})
|
||||
self.assertFalse(self.value_to_html(partner.parent_id))
|
||||
|
||||
def test_many2one_with_value(self):
|
||||
parent = self.env['res.partner'].create({'name': 'BigBoss'})
|
||||
child = self.env['res.partner'].create({'name': 'Minion', 'parent_id': parent.id})
|
||||
self.assertEqual(self.value_to_html(child.parent_id), 'BigBoss')
|
||||
|
|
|
|||
|
|
@ -30,6 +30,7 @@ class TestReports(odoo.tests.TransactionCase):
|
|||
'account.report_original_vendor_bill': [('move_type', 'in', ('in_invoice', 'in_receipt'))],
|
||||
'account.report_invoice_with_payments': invoice_domain,
|
||||
'account.report_invoice': invoice_domain,
|
||||
'account_edi_ubl_cii.account_invoices_generated_by_odoo': [('move_type', 'in', ('in_invoice', 'in_refund'))],
|
||||
'l10n_th.report_commercial_invoice': invoice_domain,
|
||||
}
|
||||
extra_data_reports = {
|
||||
|
|
@ -561,6 +562,46 @@ class TestReportsRendering(TestReportsRenderingCommon):
|
|||
pages_contents = [[elem[1] for elem in page] for page in pages]
|
||||
self.assertEqual(pages_contents, expected_pages_contents)
|
||||
|
||||
def test_report_specific_paperformat_args(self):
|
||||
"""
|
||||
Verify that the values defined in `specific_paperformat_args` take
|
||||
precedence over those in the paperformat when building the wkhtmltopdf
|
||||
command arguments.
|
||||
"""
|
||||
command_args = self.env['ir.actions.report']._build_wkhtmltopdf_args(
|
||||
self.env['report.paperformat'].new({
|
||||
'format': 'A4',
|
||||
'margin_top': 25,
|
||||
'margin_left': 50,
|
||||
'margin_bottom': 75,
|
||||
'margin_right': 100,
|
||||
'dpi': 90,
|
||||
'header_spacing': 125,
|
||||
'orientation': 'portrait'
|
||||
}),
|
||||
landscape=None,
|
||||
specific_paperformat_args={
|
||||
'data-report-landscape': True,
|
||||
'data-report-margin-top': 0,
|
||||
'data-report-margin-bottom': 0,
|
||||
'data-report-header-spacing': 0,
|
||||
'data-report-dpi': 96
|
||||
})
|
||||
self.assertEqual(command_args, [
|
||||
'--disable-local-file-access',
|
||||
'--quiet',
|
||||
'--page-size', 'A4',
|
||||
'--margin-top', '0',
|
||||
'--dpi', '96',
|
||||
'--zoom', '1.0',
|
||||
'--header-spacing', '0',
|
||||
'--margin-left', '50.0',
|
||||
'--margin-bottom', '0',
|
||||
'--margin-right', '100.0',
|
||||
'--javascript-delay', '1000',
|
||||
'--orientation', 'landscape',
|
||||
])
|
||||
|
||||
|
||||
@odoo.tests.tagged('post_install', '-at_install', '-standard', 'pdf_rendering')
|
||||
class TestReportsRenderingLimitations(TestReportsRenderingCommon):
|
||||
|
|
|
|||
|
|
@ -1,8 +1,10 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from unittest.mock import patch
|
||||
|
||||
from odoo.exceptions import ValidationError
|
||||
from odoo.tests.common import TransactionCase
|
||||
from odoo.tests.common import Command, TransactionCase
|
||||
|
||||
|
||||
class TestCompany(TransactionCase):
|
||||
|
|
@ -54,3 +56,13 @@ class TestCompany(TransactionCase):
|
|||
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)
|
||||
|
||||
def test_write_company_root_delegated_field_names(self):
|
||||
self.env['res.company'].with_context(default_parent_id=self.env.company.id).create({'name': 'foo'})
|
||||
new_currency = self.env['res.currency'].create({
|
||||
'name': 'AAA',
|
||||
'symbol': 'AAA',
|
||||
'rate_ids': [Command.create({'name': '2009-09-09', 'rate': 1})]
|
||||
})
|
||||
with patch('odoo.addons.base.models.res_company.ResCompany._get_company_root_delegated_field_names', return_value=["currency_id", "zip"]):
|
||||
self.env.company.write({'currency_id': new_currency.id, 'zip': '12345'})
|
||||
|
|
|
|||
|
|
@ -492,6 +492,13 @@ class TestPartnerAddressCompany(TransactionCase):
|
|||
self.assertFalse(ct1_1.vat)
|
||||
self.assertEqual(inv_1.street, 'Invoice Child Street', 'Should take parent address')
|
||||
self.assertFalse(inv_1.vat)
|
||||
# test it also works with default_parent_id value in context
|
||||
# also ensure it works directly on a non-empty recordset
|
||||
inv_2 = (ct1_1 | inv_1).with_context(default_parent_id=inv.id).create({
|
||||
'name': 'Address, Child of Invoice',
|
||||
})
|
||||
self.assertEqual(inv_2.street, 'Invoice Child Street', 'Should take parent address')
|
||||
self.assertFalse(inv_2.vat)
|
||||
|
||||
# sync P1 with parent, check address is update + other fields in write kept
|
||||
ct1_phone = '+320455999999'
|
||||
|
|
@ -1105,8 +1112,7 @@ class TestPartnerForm(TransactionCase):
|
|||
def test_lang_computation_form_view(self):
|
||||
""" Check computation of lang: coming from installed languages, forced
|
||||
default value and propagation from parent."""
|
||||
default_lang_info = self.env['res.lang'].get_installed()[0]
|
||||
default_lang_code = default_lang_info[0]
|
||||
default_lang_code = self.env['ir.default']._get('res.partner', 'lang') or False
|
||||
self.assertNotEqual(default_lang_code, 'de_DE') # should not be the case, just to ease test
|
||||
self.assertNotEqual(default_lang_code, 'fr_FR') # should not be the case, just to ease test
|
||||
|
||||
|
|
|
|||
|
|
@ -31,6 +31,7 @@ class TestResPartnerBank(SavepointCaseWithUserDemo):
|
|||
|
||||
# sanitaze the acc_number
|
||||
sanitized_acc_number = 'BE001251882303'
|
||||
self.assertEqual(partner_bank.sanitized_acc_number, sanitized_acc_number)
|
||||
vals = partner_bank_model.search(
|
||||
[('acc_number', '=', sanitized_acc_number)])
|
||||
self.assertEqual(1, len(vals))
|
||||
|
|
@ -49,3 +50,111 @@ class TestResPartnerBank(SavepointCaseWithUserDemo):
|
|||
vals = partner_bank_model.search(
|
||||
[('acc_number', '=', acc_number.lower())])
|
||||
self.assertEqual(1, len(vals))
|
||||
|
||||
# updating the sanitized value will also update the acc_number
|
||||
partner_bank.write({'sanitized_acc_number': 'BE001251882303WRONG'})
|
||||
self.assertEqual(partner_bank.acc_number, partner_bank.sanitized_acc_number)
|
||||
|
||||
def test_find_or_create_bank_account_create(self):
|
||||
partner = self.env['res.partner'].create({'name': 'partner name'})
|
||||
found_bank = self.env['res.partner.bank']._find_or_create_bank_account(
|
||||
account_number='account number',
|
||||
partner=partner,
|
||||
company=self.env.company,
|
||||
)
|
||||
# The bank didn't exist, we should create it
|
||||
self.assertRecordValues(found_bank, [{
|
||||
'acc_number': 'account number',
|
||||
'partner_id': partner.id,
|
||||
'company_id': False,
|
||||
'active': True,
|
||||
}])
|
||||
|
||||
def test_find_or_create_bank_account_find_active(self):
|
||||
partner = self.env['res.partner'].create({'name': 'partner name'})
|
||||
bank = self.env['res.partner.bank'].create({
|
||||
'acc_number': 'account number',
|
||||
'partner_id': partner.id,
|
||||
'company_id': False,
|
||||
'active': True,
|
||||
})
|
||||
found_bank = self.env['res.partner.bank']._find_or_create_bank_account(
|
||||
account_number='account number',
|
||||
partner=partner,
|
||||
company=self.env.company,
|
||||
)
|
||||
# The bank exists and is active, we should not create a new one
|
||||
self.assertEqual(bank, found_bank)
|
||||
|
||||
def test_find_or_create_bank_account_find_inactive(self):
|
||||
partner = self.env['res.partner'].create({'name': 'partner name'})
|
||||
self.env['res.partner.bank'].create({
|
||||
'acc_number': 'account number',
|
||||
'partner_id': partner.id,
|
||||
'company_id': False,
|
||||
'active': False,
|
||||
})
|
||||
found_bank = self.env['res.partner.bank']._find_or_create_bank_account(
|
||||
account_number='account number',
|
||||
partner=partner,
|
||||
company=self.env.company,
|
||||
)
|
||||
# The bank exists but is inactive, we should neither create a new one, neither return it
|
||||
self.assertFalse(found_bank)
|
||||
|
||||
def test_find_or_create_bank_account_find_parent(self):
|
||||
partner = self.env['res.partner'].create({'name': 'partner name'})
|
||||
contact = self.env['res.partner'].create({'name': 'contact', 'parent_id': partner.id})
|
||||
partner_bank = self.env['res.partner.bank'].create({
|
||||
'acc_number': 'account number',
|
||||
'partner_id': partner.id,
|
||||
})
|
||||
# Only the bank on the commercial partner exists
|
||||
found_bank = self.env['res.partner.bank']._find_or_create_bank_account(
|
||||
account_number='account number',
|
||||
partner=partner,
|
||||
company=self.env.company,
|
||||
)
|
||||
self.assertEqual(partner_bank, found_bank)
|
||||
|
||||
found_bank = self.env['res.partner.bank']._find_or_create_bank_account(
|
||||
account_number='account number',
|
||||
partner=contact,
|
||||
company=self.env.company,
|
||||
)
|
||||
self.assertEqual(partner_bank, found_bank)
|
||||
|
||||
# Now the bank exists on both partners
|
||||
contact_bank = self.env['res.partner.bank'].create({
|
||||
'acc_number': 'account number',
|
||||
'partner_id': contact.id,
|
||||
})
|
||||
found_bank = self.env['res.partner.bank']._find_or_create_bank_account(
|
||||
account_number='account number',
|
||||
partner=partner,
|
||||
company=self.env.company,
|
||||
)
|
||||
self.assertEqual(partner_bank, found_bank)
|
||||
|
||||
found_bank = self.env['res.partner.bank']._find_or_create_bank_account(
|
||||
account_number='account number',
|
||||
partner=contact,
|
||||
company=self.env.company,
|
||||
)
|
||||
self.assertEqual(contact_bank, found_bank)
|
||||
|
||||
# Only the bank on the contact exists
|
||||
partner_bank.unlink()
|
||||
found_bank = self.env['res.partner.bank']._find_or_create_bank_account(
|
||||
account_number='account number',
|
||||
partner=partner,
|
||||
company=self.env.company,
|
||||
)
|
||||
self.assertEqual(contact_bank, found_bank)
|
||||
|
||||
found_bank = self.env['res.partner.bank']._find_or_create_bank_account(
|
||||
account_number='account number',
|
||||
partner=contact,
|
||||
company=self.env.company,
|
||||
)
|
||||
self.assertEqual(contact_bank, found_bank)
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from datetime import datetime, timedelta
|
||||
from types import SimpleNamespace
|
||||
from unittest.mock import patch
|
||||
|
||||
|
|
@ -221,6 +222,19 @@ class TestUsers(UsersCommonCase):
|
|||
self.assertTrue(portal_partner_2.exists(), 'Should have kept the partner')
|
||||
self.assertEqual(asked_deletion_2.state, 'fail', 'Should have marked the deletion as failed')
|
||||
|
||||
def test_delete_public_user(self):
|
||||
"""Test that the public user cannot be deleted."""
|
||||
public_user = self.env.ref('base.public_user')
|
||||
public_partner = public_user.partner_id
|
||||
|
||||
# Attempt to delete the public user
|
||||
with self.assertRaises(UserError, msg="Public user should not be deletable"):
|
||||
public_user.unlink()
|
||||
|
||||
# Ensure the public user still exists and is inactive
|
||||
self.assertTrue(public_user.exists() and not public_user.active, "Public user should still exist and be inactive")
|
||||
self.assertTrue(public_partner.exists() and not public_partner.active, "Public partner should still exist and be inactive")
|
||||
|
||||
def test_user_home_action_restriction(self):
|
||||
test_user = new_test_user(self.env, 'hello world')
|
||||
|
||||
|
|
@ -428,6 +442,28 @@ class TestUsers2(UsersCommonCase):
|
|||
with self.assertRaises(ValidationError, msg="The user cannot be at the same time in groups: ['Membre', 'Portal', 'Foo / Small user group']"):
|
||||
user_form.save()
|
||||
|
||||
def test_view_group_hierarchy(self):
|
||||
"""Test that the group hierarchy shows up in the correct language of the user."""
|
||||
self.env['res.lang']._activate_lang('fr_FR')
|
||||
group_system = self.env.ref('base.group_system')
|
||||
group_system.with_context(lang='fr_FR').name = 'Administrateur'
|
||||
|
||||
view_group_hierarchy_en = self.env['res.groups']._get_view_group_hierarchy()
|
||||
view_group_hierarchy_fr = self.env['res.groups'].with_context(lang='fr_FR')._get_view_group_hierarchy()
|
||||
self.assertNotEqual(view_group_hierarchy_en['groups'][group_system.id]['name'], 'Administrateur')
|
||||
self.assertEqual(view_group_hierarchy_fr['groups'][group_system.id]['name'], 'Administrateur')
|
||||
|
||||
# Should work the other way around too
|
||||
self.env.registry.clear_cache('groups')
|
||||
view_group_hierarchy_fr = self.env['res.groups'].with_context(lang='fr_FR')._get_view_group_hierarchy()
|
||||
view_group_hierarchy_en = self.env['res.groups']._get_view_group_hierarchy()
|
||||
self.assertNotEqual(view_group_hierarchy_en['groups'][group_system.id]['name'], 'Administrateur')
|
||||
self.assertEqual(view_group_hierarchy_fr['groups'][group_system.id]['name'], 'Administrateur')
|
||||
|
||||
with patch('odoo.addons.base.models.res_groups.ResGroups._get_view_group_hierarchy') as mock:
|
||||
self.user_portal_1.copy_data()
|
||||
self.assertFalse(mock.called)
|
||||
|
||||
@users('portal_1')
|
||||
@mute_logger('odoo.addons.base.models.ir_model')
|
||||
def test_self_writeable_fields(self):
|
||||
|
|
@ -495,6 +531,94 @@ class TestUsers2(UsersCommonCase):
|
|||
"group_ids": [Command.link(contact_creation_group.id)],
|
||||
})
|
||||
|
||||
def test_portal_user_manager_access(self):
|
||||
# groups
|
||||
group_portal = self.env.ref('base.group_portal')
|
||||
group_user = self.env.ref('base.group_user')
|
||||
group_partner_manager = self.env.ref('base.group_partner_manager')
|
||||
group_portal_user_manager = self.env['res.groups'].create({
|
||||
'name': 'Portal User Manager',
|
||||
'user_ids': [],
|
||||
})
|
||||
|
||||
# ACL
|
||||
self.env['ir.model.access'].create({
|
||||
'name': 'Allow user profile update',
|
||||
'model_id': self.env['ir.model']._get('res.users').id,
|
||||
'group_id': group_portal_user_manager.id,
|
||||
'perm_write': True,
|
||||
})
|
||||
|
||||
# Rules
|
||||
self.env['ir.rule'].create({
|
||||
'name': 'Allow updates by Portal Managers on PORTAL users (only)',
|
||||
'model_id': self.env['ir.model']._get('res.users').id,
|
||||
'groups': [group_portal_user_manager.id],
|
||||
'domain_force': [('share', '=', True)],
|
||||
'perm_write': True,
|
||||
})
|
||||
|
||||
# Users
|
||||
portal_user_manager = self.env['res.users'].create({
|
||||
'name': 'Portal User Manager',
|
||||
'login': 'maintainer',
|
||||
'password': 'password',
|
||||
'group_ids': [group_user.id, group_partner_manager.id, group_portal_user_manager.id],
|
||||
})
|
||||
user = self.env['res.users'].create({
|
||||
'name': 'User',
|
||||
'login': 'user_',
|
||||
'password': 'password',
|
||||
'group_ids': [group_user.id, group_partner_manager.id],
|
||||
})
|
||||
portal = self.env['res.users'].create({
|
||||
'name': 'Portal',
|
||||
'login': 'portal_',
|
||||
'password': 'password',
|
||||
'group_ids': [group_portal.id],
|
||||
})
|
||||
|
||||
# A UPM cannot update the user profile of another USER
|
||||
with self.assertRaises(AccessError):
|
||||
user.with_user(portal_user_manager).write({
|
||||
'name': 'New name for you'
|
||||
})
|
||||
# A UPM can update the user profile of a PORTAL user
|
||||
portal.with_user(portal_user_manager).write({
|
||||
'name': 'New name for you'
|
||||
})
|
||||
|
||||
# A UPM cannot update the partner profile of another USER
|
||||
with self.assertRaises(AccessError):
|
||||
user.partner_id.with_user(portal_user_manager).write({
|
||||
'name': 'New name for you'
|
||||
})
|
||||
# A UPM can update the partner profile of a PORTAL user
|
||||
portal.partner_id.with_user(portal_user_manager).write({
|
||||
'name': 'New name for you'
|
||||
})
|
||||
|
||||
# A USER cannot update the user profile of another USER
|
||||
with self.assertRaises(AccessError):
|
||||
self.user_internal.with_user(user).write({
|
||||
'name': 'New name for you'
|
||||
})
|
||||
# A USER cannot update the user profile of a PORTAL user
|
||||
with self.assertRaises(AccessError):
|
||||
portal.with_user(user).write({
|
||||
'name': 'New name for you'
|
||||
})
|
||||
|
||||
# A USER cannot update the partner profile of another USER
|
||||
with self.assertRaises(AccessError):
|
||||
self.user_internal.partner_id.with_user(user).write({
|
||||
'name': 'New name for you'
|
||||
})
|
||||
# A USER can update the partner profile of a PORTAL user
|
||||
portal.partner_id.with_user(user).write({
|
||||
'name': 'New name for you'
|
||||
})
|
||||
|
||||
|
||||
class TestUsersTweaks(TransactionCase):
|
||||
def test_superuser(self):
|
||||
|
|
@ -517,10 +641,10 @@ class TestUsersIdentitycheck(HttpCase):
|
|||
self.env.user.password = "admin@odoo"
|
||||
|
||||
# Create a first session that will be used to revoke other sessions
|
||||
session = self.authenticate('admin', 'admin@odoo')
|
||||
session = self.authenticate('admin', 'admin@odoo', session_extra={'_trace_disable': False})
|
||||
|
||||
# Create a second session that will be used to check it has been revoked
|
||||
self.authenticate('admin', 'admin@odoo')
|
||||
self.authenticate('admin', 'admin@odoo', session_extra={'_trace_disable': False})
|
||||
# Test the session is valid
|
||||
# Valid session -> not redirected from /web to /web/login
|
||||
self.assertTrue(self.url_open('/web').url.endswith('/web'))
|
||||
|
|
@ -545,3 +669,77 @@ class TestUsersIdentitycheck(HttpCase):
|
|||
|
||||
# In addition, the password must have been emptied from the wizard
|
||||
self.assertFalse(user_identity_check.password)
|
||||
|
||||
|
||||
@tagged('post_install', '-at_install')
|
||||
class TestApiKeys(UsersCommonCase):
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super().setUpClass()
|
||||
|
||||
cls.env['ir.config_parameter'].set_param('base.enable_programmatic_api_keys', 1)
|
||||
UsersApiKeys = cls.env['res.users.apikeys'].with_user(cls.user_internal)
|
||||
cls.tomorrow = datetime.now() + timedelta(days=1)
|
||||
cls.unscoped_key = UsersApiKeys._generate(None, 'Key without a scope', cls.tomorrow)
|
||||
cls.scoped_key = UsersApiKeys._generate('scope', 'Key with a scope', cls.tomorrow)
|
||||
|
||||
def test_programmatic_apikey_management_is_deactivated_by_default(self):
|
||||
self.env['ir.config_parameter'].set_param('base.enable_programmatic_api_keys', None)
|
||||
|
||||
# Attempting to create a key raises an error
|
||||
with self.assertRaisesRegex(UserError, 'Programmatic API keys are not enabled'):
|
||||
self.env['res.users.apikeys'].with_user(self.user_internal).generate(
|
||||
self.unscoped_key, None, 'Another key without a scope', self.tomorrow)
|
||||
|
||||
# Attempting to revoke a key raises an error
|
||||
with self.assertRaisesRegex(UserError, 'Programmatic API keys are not enabled'):
|
||||
self.env['res.users.apikeys'].with_user(self.user_internal).revoke(self.unscoped_key)
|
||||
|
||||
def test_generate_apikey_is_limited(self):
|
||||
# create 8 new keys, which makes 10 keys in total for user_internal
|
||||
for i in range(8):
|
||||
self.env['res.users.apikeys'].with_user(self.user_internal).generate(
|
||||
self.unscoped_key, None, 'Another key without a scope', self.tomorrow)
|
||||
|
||||
with self.assertRaisesRegex(UserError, 'Limit of 10 API keys is reached'):
|
||||
self.env['res.users.apikeys'].with_user(self.user_internal).generate(
|
||||
self.unscoped_key, None, 'Another key without a scope', self.tomorrow)
|
||||
|
||||
# This ICP can change the limit
|
||||
self.env['ir.config_parameter'].set_param('base.programmatic_api_keys_limit', 11)
|
||||
self.env['res.users.apikeys'].with_user(self.user_internal).generate(
|
||||
self.unscoped_key, None, 'Another key without a scope', self.tomorrow)
|
||||
|
||||
def test_generate_apikey_raises_when_creating_unscoped_key_from_scoped_key(self):
|
||||
# Creating an unscoped key from a scoped key raises an error
|
||||
with self.assertRaisesRegex(UserError, 'The provided API key is invalid or does not belong to the current user'):
|
||||
self.env['res.users.apikeys'].with_user(self.user_internal).generate(
|
||||
self.scoped_key, None, 'Another key without a scope', self.tomorrow)
|
||||
|
||||
def test_generate_apikey_raises_when_creating_key_from_differently_scoped_key(self):
|
||||
# Creating a key with a different scope raises an error
|
||||
with self.assertRaisesRegex(UserError, 'The provided API key is invalid or does not belong to the current user'):
|
||||
self.env['res.users.apikeys'].with_user(self.user_internal).generate(
|
||||
self.scoped_key, 'other', 'Another key with another scope', self.tomorrow)
|
||||
|
||||
def test_generate_apikey_accepts_creating_key_from_identically_scoped_key(self):
|
||||
# Creating a key with the same scope doesn't raise
|
||||
self.env['res.users.apikeys'].with_user(self.user_internal).generate(
|
||||
self.scoped_key, 'scope', 'Another key with a scope', self.tomorrow)
|
||||
|
||||
def test_generate_apikey_accepts_creating_scoped_key_from_unscoped_key(self):
|
||||
# Creating a key with a scope from an unscoped key doesn't raise
|
||||
self.env['res.users.apikeys'].with_user(self.user_internal).generate(
|
||||
self.unscoped_key, 'scope', 'Another key with a scope', self.tomorrow)
|
||||
|
||||
def test_generate_apikey_accepts_creating_unscoped_key_from_unscoped_key(self):
|
||||
# Creating an unscoped key from another unscoped key doesn't raise
|
||||
self.env['res.users.apikeys'].with_user(self.user_internal).generate(
|
||||
self.unscoped_key, None, 'Another key without a scope', self.tomorrow)
|
||||
|
||||
def test_generate_apikey_checks_ownership(self):
|
||||
# Check that an API key cannot be generated from another user's API key
|
||||
with self.assertRaisesRegex(UserError, 'The provided API key is invalid or does not belong to the current user'):
|
||||
self.env['res.users.apikeys'].with_user(SUPERUSER_ID).generate(
|
||||
self.unscoped_key, None, 'Another key without a scope', self.tomorrow)
|
||||
|
|
|
|||
|
|
@ -49,6 +49,28 @@ class TestRetry(TestRetryCommon):
|
|||
self.assertEqual(tests_run_count, self.count)
|
||||
|
||||
|
||||
@tagged('test_retry', 'test_retry_success')
|
||||
class TestRetryTraceback(TestRetryCommon):
|
||||
""" Check some tests behaviour when ODOO_TEST_FAILURE_RETRIES is set"""
|
||||
|
||||
def test_retry_traceback_success(self):
|
||||
tests_run_count = self.get_tests_run_count()
|
||||
self.update_count()
|
||||
if tests_run_count != self.count:
|
||||
_logger.error('Traceback (most recent call last):\n')
|
||||
self.assertEqual(tests_run_count, self.count)
|
||||
|
||||
|
||||
@tagged('test_retry', 'test_retry_success')
|
||||
class TestRetryTracebackArg(TestRetryCommon):
|
||||
def test_retry_traceback_args_success(self):
|
||||
tests_run_count = self.get_tests_run_count()
|
||||
self.update_count()
|
||||
if tests_run_count != self.count:
|
||||
_logger.error('%s', 'Traceback (most recent call last):\n')
|
||||
self.assertEqual(tests_run_count, self.count)
|
||||
|
||||
|
||||
@tagged('-standard', 'test_retry', 'test_retry_failures')
|
||||
class TestRetryFailures(TestRetryCommon):
|
||||
def test_retry_failure_assert(self):
|
||||
|
|
|
|||
|
|
@ -153,6 +153,74 @@ class TranslationToolsTestCase(BaseCase):
|
|||
self.assertEqual(result, source)
|
||||
self.assertItemsEqual(terms, ['Form stuff'])
|
||||
|
||||
def test_translate_xml_o_translate_inline_on_block(self):
|
||||
""" Test xml_translate() with non-inline elements with o_translate_inline. """
|
||||
terms = []
|
||||
source = """<div>
|
||||
<h1 class="o_translate_inline">Blah</h1>more text
|
||||
<h1 class="o_translate_inline" t-if="True">Other Blah</h1>even more text
|
||||
</div>"""
|
||||
result = xml_translate(terms.append, source)
|
||||
self.assertEqual(result, source)
|
||||
self.assertItemsEqual(terms,
|
||||
['<h1 class="o_translate_inline">Blah</h1>more text', 'Other Blah', 'even more text'])
|
||||
|
||||
def test_translate_xml_o_translate_inline_on_parent(self):
|
||||
""" Test xml_translate() with non-inline elements inside o_translate_inline. """
|
||||
terms = []
|
||||
source = """<div>
|
||||
<span class="o_translate_inline">Blah<h1>more text</h1></span>
|
||||
<span class="o_translate_inline">Other Blah<h1 t-if="True">even more text</h1></span>
|
||||
</div>"""
|
||||
result = xml_translate(terms.append, source)
|
||||
self.assertEqual(result, source)
|
||||
self.assertItemsEqual(terms,
|
||||
['<span class="o_translate_inline">Blah<h1>more text</h1></span>', 'Other Blah', 'even more text'])
|
||||
|
||||
def test_translate_xml_highlight(self):
|
||||
""" Test xml_translate() with highlight span (with o_translate_inline). """
|
||||
terms = []
|
||||
source = """<div>
|
||||
<span class="o_text_highlight o_translate_inline">
|
||||
<a>solo link</a>
|
||||
</span>
|
||||
</div>
|
||||
<div>
|
||||
<span class="o_text_highlight o_translate_inline">
|
||||
<span>Here is a <a>nested link</a> in highlight</span>
|
||||
</span>
|
||||
</div>"""
|
||||
result = xml_translate(terms.append, source)
|
||||
self.assertEqual(result, source)
|
||||
self.assertItemsEqual(terms, ["""<span class="o_text_highlight o_translate_inline">
|
||||
<a>solo link</a>
|
||||
</span>""", """<span class="o_text_highlight o_translate_inline">
|
||||
<span>Here is a <a>nested link</a> in highlight</span>
|
||||
</span>"""])
|
||||
|
||||
def test_translate_xml_o_translate_inline_with_groups(self):
|
||||
""" Test xml_translate() with groups attribute and with o_translate_inline. """
|
||||
terms = []
|
||||
source = """<div>
|
||||
<a class="o_translate_inline" href="#" groups="anyone">Skip</a>
|
||||
</div>"""
|
||||
result = xml_translate(terms.append, source)
|
||||
self.assertEqual(result, source)
|
||||
self.assertItemsEqual(terms, ['Skip'])
|
||||
|
||||
def test_translate_xml_groups(self):
|
||||
""" Test xml_translate() with groups attributes. """
|
||||
terms = []
|
||||
source = """<t t-name="stuff">
|
||||
stuff before
|
||||
<span groups="anyone"/>
|
||||
stuff after
|
||||
</t>"""
|
||||
result = xml_translate(terms.append, source)
|
||||
self.assertEqual(result, source)
|
||||
self.assertItemsEqual(terms,
|
||||
['stuff before', 'stuff after'])
|
||||
|
||||
def test_translate_xml_t(self):
|
||||
""" Test xml_translate() with t-* attributes. """
|
||||
terms = []
|
||||
|
|
@ -1521,6 +1589,29 @@ class TestXMLTranslation(TransactionCase):
|
|||
f'arch_db for {lang} should be {archf2} when check_translations'
|
||||
)
|
||||
|
||||
def test_t_call_no_normal_attribute_translation(self):
|
||||
self.env['ir.ui.view'].create({
|
||||
'type': 'qweb',
|
||||
'key': 'test',
|
||||
'arch': '<button><t t-out="placeholder"/></button>',
|
||||
})
|
||||
view0 = self.env['ir.ui.view'].with_context(lang='fr_FR', edit_translations=True).create({
|
||||
'type': 'qweb',
|
||||
'arch': '<t t-call="test" placeholder="hello"/>',
|
||||
})
|
||||
self.assertEqual(view0._render_template(view0.id, {'hello': 'world'}), '<button>world</button>')
|
||||
self.assertEqual(view0.arch_db, '<t t-call="test" placeholder="hello"/>')
|
||||
|
||||
view0.arch = '<t t-call="test" placeholder.translate="hello"/>'
|
||||
translate_node = (
|
||||
f'<span data-oe-model="ir.ui.view" data-oe-id="{view0.id}"'
|
||||
' data-oe-field="arch_db" data-oe-translation-state="to_translate"'
|
||||
' data-oe-translation-source-sha="2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824">hello</span>'
|
||||
)
|
||||
self.assertEqual(view0._render_template(view0.id), f'<button>{translate_node}</button>')
|
||||
translate_attr = translate_node.replace("<", "<").replace(">", ">").replace('"', """)
|
||||
self.assertEqual(view0.arch_db, f'<t t-call="test" placeholder.translate="{translate_attr}"/>')
|
||||
|
||||
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>'
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ from lxml.builder import E
|
|||
from psycopg2 import IntegrityError
|
||||
from psycopg2.extras import Json
|
||||
|
||||
from odoo.exceptions import AccessError, ValidationError
|
||||
from odoo.exceptions import AccessError, UserError, ValidationError
|
||||
from odoo.tests import common, tagged
|
||||
from odoo.addons.base.tests.common import TransactionCaseWithUserDemo
|
||||
from odoo.tools import mute_logger, view_validation, safe_eval
|
||||
|
|
@ -1892,6 +1892,7 @@ class TestTemplating(ViewCase):
|
|||
""")
|
||||
self.assertEqual(arch, expected)
|
||||
|
||||
|
||||
@tagged('post_install', '-at_install')
|
||||
class TestViews(ViewCase):
|
||||
|
||||
|
|
@ -2229,6 +2230,25 @@ class TestViews(ViewCase):
|
|||
'inherit_id': False,
|
||||
})
|
||||
|
||||
def test_xml_editor_rejects_encoding_declaration(self):
|
||||
"""Must raise a UserError when encoding declaration is included."""
|
||||
with self.assertRaises(UserError):
|
||||
self.View.create({
|
||||
'name': 'encoding_declaration_view',
|
||||
'arch_base': "<?xml version='1.0' encoding='utf-8'?>",
|
||||
'inherit_id': False,
|
||||
})
|
||||
|
||||
view = self.assertValid("<form string='Test'></form>", name="test_xml_encoding_view")
|
||||
for field in ("arch", "arch_base"):
|
||||
with self.subTest(field=field):
|
||||
original_value = view[field]
|
||||
|
||||
with self.assertRaises(UserError):
|
||||
view.write({field: "<?xml version='1.0' encoding='utf-8'?><form/>"})
|
||||
|
||||
self.assertXMLEqual(view[field], original_value)
|
||||
|
||||
def test_context_in_view(self):
|
||||
arch = """
|
||||
<form string="View">
|
||||
|
|
@ -2695,6 +2715,28 @@ class TestViews(ViewCase):
|
|||
self.assertValid(arch % 'base.group_no_one')
|
||||
self.assertWarning(arch % 'base.dummy')
|
||||
|
||||
def test_groups_field_removed(self):
|
||||
view = self.View.create({
|
||||
'name': 'valid view',
|
||||
'model': 'ir.ui.view',
|
||||
'arch': """
|
||||
<form string="View">
|
||||
<span class="oe_inline" invisible="0 == 0">
|
||||
(<field name="name" groups="base.group_portal"/>)
|
||||
</span>
|
||||
</form>
|
||||
""",
|
||||
})
|
||||
arch = self.View.get_views([(view.id, view.type)])['views']['form']['arch']
|
||||
|
||||
self.assertEqual(arch, """
|
||||
<form string="View">
|
||||
<span class="oe_inline" invisible="0 == 0">
|
||||
()
|
||||
</span>
|
||||
</form>
|
||||
""".strip())
|
||||
|
||||
def test_attrs_groups_behavior(self):
|
||||
view = self.View.create({
|
||||
'name': 'foo',
|
||||
|
|
@ -6090,3 +6132,20 @@ class ViewModifiers(ViewCase):
|
|||
self.assertFalse(tree.xpath('//div[@id="foo"]'))
|
||||
self.assertTrue(tree.xpath('//div[@id="bar"]'))
|
||||
self.assertFalse(tree.xpath('//div[@id="stuff"]'))
|
||||
|
||||
def test_create_inherit_view_with_xpath_without_expr(self):
|
||||
"""Test that creating inherited view containing <xpath> node without the 'expr' attribute."""
|
||||
|
||||
parent_view = self.env.ref('base.view_partner_form')
|
||||
inherit_arch = """
|
||||
<xpath position="replace">
|
||||
<field name="name"/>
|
||||
</xpath>
|
||||
"""
|
||||
|
||||
with self.assertRaises(ValidationError):
|
||||
self.env['ir.ui.view'].create({
|
||||
'name': 'test.xpath.without.expr',
|
||||
'inherit_id': parent_view.id,
|
||||
'arch': inherit_arch,
|
||||
})
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue