合并企业版代码(未测试,先提交到测试分支)
This commit is contained in:
6
web_studio/tests/__init__.py
Normal file
6
web_studio/tests/__init__.py
Normal file
@@ -0,0 +1,6 @@
|
||||
from . import test_approval
|
||||
from . import test_ir_model
|
||||
from . import test_report_editor
|
||||
from . import test_ui
|
||||
from . import test_view_normalization
|
||||
from . import test_view_editor
|
||||
466
web_studio/tests/test_approval.py
Normal file
466
web_studio/tests/test_approval.py
Normal file
@@ -0,0 +1,466 @@
|
||||
from psycopg2 import IntegrityError
|
||||
|
||||
from odoo.addons.mail.tests.common import mail_new_test_user
|
||||
from odoo.exceptions import AccessError, UserError, ValidationError
|
||||
from odoo.tests.common import TransactionCase
|
||||
from odoo.tools import mute_logger
|
||||
|
||||
class TestStudioApproval(TransactionCase):
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super().setUpClass()
|
||||
creation_context = {
|
||||
"studio": True,
|
||||
'no_reset_password': True,
|
||||
'mail_create_nosubscribe': True,
|
||||
'mail_create_nolog': True
|
||||
}
|
||||
# setup 2 users with custom groups
|
||||
cls.group_user = cls.env['res.groups'].with_context(**creation_context).create({
|
||||
'name': 'Approval User',
|
||||
'implied_ids': [(4, cls.env.ref('base.group_user').id)],
|
||||
})
|
||||
cls.group_manager = cls.env['res.groups'].with_context(**creation_context).create({
|
||||
'name': 'Approval Manager',
|
||||
'implied_ids': [(4, cls.group_user.id)],
|
||||
})
|
||||
cls.user = mail_new_test_user(
|
||||
cls.env, login='Employee',
|
||||
groups="base.group_user,base.group_partner_manager", context=creation_context)
|
||||
cls.manager = mail_new_test_user(
|
||||
cls.env, login='Manager',
|
||||
groups="base.group_user,base.group_partner_manager", context=creation_context)
|
||||
cls.user.write({
|
||||
'groups_id': [(4, cls.group_user.id)]
|
||||
})
|
||||
cls.manager.write({
|
||||
'groups_id': [(4, cls.group_manager.id)]
|
||||
})
|
||||
cls.record = cls.user.partner_id
|
||||
# setup validation rules; inactive by default, they'll get
|
||||
# activated in the tests when they're needed
|
||||
# i'll use the 'open_parent' method on partners because why not
|
||||
partner_model = cls.env.ref('base.model_res_partner')
|
||||
cls.MODEL = 'res.partner'
|
||||
cls.METHOD = 'open_parent'
|
||||
cls.rule = cls.env['studio.approval.rule'].create({
|
||||
'active': False,
|
||||
'model_id': partner_model.id,
|
||||
'method': cls.METHOD,
|
||||
'message': "You didn't say the magic word!",
|
||||
'group_id': cls.group_manager.id,
|
||||
})
|
||||
cls.rule_with_domain = cls.env['studio.approval.rule'].create({
|
||||
'active': False,
|
||||
'model_id': partner_model.id,
|
||||
'method': cls.METHOD,
|
||||
'message': "You didn't say the magic word!",
|
||||
'group_id': cls.group_manager.id,
|
||||
'domain': '[("is_company", "=", True)]',
|
||||
})
|
||||
cls.rule_exclusive = cls.env['studio.approval.rule'].create({
|
||||
'active': False,
|
||||
'model_id': partner_model.id,
|
||||
'method': cls.METHOD,
|
||||
'message': "You didn't say the magic word!",
|
||||
'group_id': cls.group_manager.id,
|
||||
'exclusive_user': True,
|
||||
})
|
||||
cls.rule_exclusive_with_domain = cls.env['studio.approval.rule'].create({
|
||||
'active': False,
|
||||
'model_id': partner_model.id,
|
||||
'method': cls.METHOD,
|
||||
'message': "You didn't say the magic word!",
|
||||
'group_id': cls.group_manager.id,
|
||||
'domain': '[("is_company", "=", True)]',
|
||||
'exclusive_user': True,
|
||||
})
|
||||
|
||||
def test_00_constraints(self):
|
||||
"""Check that constraints on the model apply as expected."""
|
||||
self.rule.active = True
|
||||
# check that approval rules on non-existing methods are not allowed
|
||||
with self.assertRaises(ValidationError, msg="Shouldn't have approval on non-existing method"):
|
||||
self.rule.method = 'atomize'
|
||||
# check that there cannot be 2 entries for the same rule+record
|
||||
self.rule.with_user(self.manager).set_approval(res_id=self.record.id, approved=False)
|
||||
with mute_logger('odoo.sql_db'):
|
||||
with self.assertRaises(IntegrityError, msg="Shouldn't have 2 entries for the same rule+record"):
|
||||
with self.cr.savepoint():
|
||||
self.env['studio.approval.entry'].with_user(self.manager).create({
|
||||
'rule_id': self.rule.id,
|
||||
'user_id': self.manager.id,
|
||||
'res_id': self.record.id,
|
||||
})
|
||||
# check that modifying forbidden fields is prevented when entries exist
|
||||
with self.assertRaises(UserError):
|
||||
self.rule.method = 'unlink'
|
||||
with self.assertRaises(UserError):
|
||||
self.rule.group_id = self.env.ref('base.group_user')
|
||||
with self.assertRaises(UserError):
|
||||
self.rule.model_id = self.env.ref('base.model_res_partner_bank')
|
||||
with self.assertRaises(UserError):
|
||||
self.rule.action_id = self.env['ir.actions.actions'].search([], limit=1)
|
||||
# check that deleting a rule that has entries is prevented
|
||||
with self.assertRaises(UserError):
|
||||
self.rule.unlink()
|
||||
|
||||
def test_01_single_rule(self):
|
||||
""" - normal user can't validate
|
||||
- normal user can't proceed
|
||||
- admin user can validate
|
||||
- normal user can proceed after that
|
||||
"""
|
||||
rule = self.rule
|
||||
rule.active = True
|
||||
with self.assertRaises(UserError, msg="Should'nt validate without required group"):
|
||||
rule.with_user(self.user).set_approval(res_id=self.record.id, approved=True)
|
||||
approval_result = self.env['studio.approval.rule'].with_user(self.user).check_approval(
|
||||
model=self.MODEL,
|
||||
res_id=self.record.id,
|
||||
method=self.METHOD,
|
||||
action_id=False)
|
||||
self.assertFalse(approval_result.get('approved'), "Shouldn't have approved automatically")
|
||||
rule.with_user(self.manager).set_approval(res_id=self.record.id, approved=True)
|
||||
approval_result = self.env['studio.approval.rule'].with_user(self.user).check_approval(
|
||||
model=self.MODEL,
|
||||
res_id=self.record.id,
|
||||
method=self.METHOD,
|
||||
action_id=False)
|
||||
self.assertTrue(approval_result.get('approved'), "Should be able to proceed after manager approval")
|
||||
|
||||
def test_02_single_rule_on_action(self):
|
||||
""" Same as previous test, but with approval on an action instead of a method."""
|
||||
rule = self.rule
|
||||
# there's no constraint on actions, since any action could be set
|
||||
# in the form view's arch; take a random action
|
||||
ACTION = self.env.ref('base.action_partner_form')
|
||||
rule.write({
|
||||
'active': True,
|
||||
'method': False,
|
||||
'action_id': ACTION.id,
|
||||
})
|
||||
with self.assertRaises(UserError, msg="Should'nt validate without required group"):
|
||||
rule.with_user(self.user).set_approval(res_id=self.record.id, approved=True)
|
||||
approval_result = self.env['studio.approval.rule'].with_user(self.user).check_approval(
|
||||
model=self.MODEL,
|
||||
res_id=self.record.id,
|
||||
method=False,
|
||||
action_id=ACTION.id)
|
||||
self.assertFalse(approval_result.get('approved'), "Shouldn't have approved automatically")
|
||||
rule.with_user(self.manager).set_approval(res_id=self.record.id, approved=True)
|
||||
approval_result = self.env['studio.approval.rule'].with_user(self.user).check_approval(
|
||||
model=self.MODEL,
|
||||
res_id=self.record.id,
|
||||
method=False,
|
||||
action_id=ACTION.id)
|
||||
self.assertTrue(approval_result.get('approved'), "Should be able to proceed after manager approval")
|
||||
|
||||
def test_03_single_rule_with_domain(self):
|
||||
""" - rule not triggered if no domain match
|
||||
- rule triggered if domain match
|
||||
"""
|
||||
rule = self.rule_with_domain
|
||||
rule.active = True
|
||||
matching_record = self.env.ref('base.main_company').partner_id
|
||||
non_matching_record = self.record
|
||||
approval_result = self.env['studio.approval.rule'].with_user(self.user).check_approval(
|
||||
model=self.MODEL,
|
||||
res_id=matching_record.id,
|
||||
method=self.METHOD,
|
||||
action_id=False)
|
||||
self.assertFalse(approval_result.get('approved'), "Shouldn't be able to proceed on record that matches the rule's domain")
|
||||
approval_result = self.env['studio.approval.rule'].with_user(self.user).check_approval(
|
||||
model=self.MODEL,
|
||||
res_id=non_matching_record.id,
|
||||
method=self.METHOD,
|
||||
action_id=False)
|
||||
self.assertTrue(approval_result.get('approved'), "Should be able to proceed on record that doesn't match the rule's domain")
|
||||
|
||||
def test_04_rule_rejection(self):
|
||||
""" - admin rejects rule
|
||||
- normal user can't proceed
|
||||
- admin user can't proceed
|
||||
- admin approves rule
|
||||
- normal user can proceed
|
||||
"""
|
||||
rule = self.rule
|
||||
rule.active = True
|
||||
rule.with_user(self.manager).set_approval(res_id=self.record.id, approved=False)
|
||||
approval_result = self.env['studio.approval.rule'].with_user(self.user).check_approval(
|
||||
model=self.MODEL,
|
||||
res_id=self.record.id,
|
||||
method=self.METHOD,
|
||||
action_id=False)
|
||||
self.assertFalse(approval_result.get('approved'), "User shouldn't be able to proceed following manager rejection")
|
||||
approval_result = self.env['studio.approval.rule'].with_user(self.manager).check_approval(
|
||||
model=self.MODEL,
|
||||
res_id=self.record.id,
|
||||
method=self.METHOD,
|
||||
action_id=False)
|
||||
self.assertFalse(approval_result.get('approved'), "Manager shouldn't be able to proceed following own rejection")
|
||||
rule.with_user(self.manager).delete_approval(res_id=self.record.id)
|
||||
rule.with_user(self.manager).set_approval(res_id=self.record.id, approved=True)
|
||||
approval_result = self.env['studio.approval.rule'].with_user(self.user).check_approval(
|
||||
model=self.MODEL,
|
||||
res_id=self.record.id,
|
||||
method=self.METHOD,
|
||||
action_id=False)
|
||||
self.assertTrue(approval_result.get('approved'), "Should be able to proceed after manager changed their mind")
|
||||
|
||||
def test_05_different_users(self):
|
||||
""" - user cannot proceed
|
||||
- admin cannot proceed
|
||||
- admin can validate their rule and proceed
|
||||
- user can approve their rule and proceed after manager approval
|
||||
"""
|
||||
# set the base rule for users and the 'exlusive_user' rules for admin
|
||||
self.rule.active = True
|
||||
self.rule.group_id = self.group_user
|
||||
self.rule_exclusive.active = True
|
||||
user_rule = self.rule
|
||||
manager_rule = self.rule_exclusive
|
||||
approval_result = self.env['studio.approval.rule'].with_user(self.user).check_approval(
|
||||
model=self.MODEL,
|
||||
res_id=self.record.id,
|
||||
method=self.METHOD,
|
||||
action_id=False)
|
||||
self.assertFalse(approval_result.get('approved'), "User shouldn't be able to proceed")
|
||||
# cancel the user's approval which was implicitely done in the previous call
|
||||
user_rule.with_user(self.user).delete_approval(res_id=self.record.id)
|
||||
approval_result = self.env['studio.approval.rule'].with_user(self.manager).check_approval(
|
||||
model=self.MODEL,
|
||||
res_id=self.record.id,
|
||||
method=self.METHOD,
|
||||
action_id=False)
|
||||
self.assertFalse(approval_result.get('approved'), "Manager shouldn't be able to proceed")
|
||||
approval_info = self.env['studio.approval.rule'].with_user(self.manager).get_approval_spec(
|
||||
model=self.MODEL,
|
||||
res_id=self.record.id,
|
||||
method=self.METHOD,
|
||||
action_id=False
|
||||
)
|
||||
manager_entry = list(filter(lambda e: e['user_id'][0] == self.manager.id, approval_info['entries']))
|
||||
self.assertEqual(len(manager_entry), 1, "Only one rule should have been validated by the manager")
|
||||
approval_result = self.env['studio.approval.rule'].with_user(self.user).check_approval(
|
||||
model=self.MODEL,
|
||||
res_id=self.record.id,
|
||||
method=self.METHOD,
|
||||
action_id=False)
|
||||
self.assertTrue(approval_result.get('approved'), "Should be able to proceed after everybody approved")
|
||||
|
||||
def test_06_security(self):
|
||||
""" - user cannot remove entry from admin
|
||||
- admin cannot remove entry from user
|
||||
- rule already validated/rejected doesn't accept new validation
|
||||
"""
|
||||
# set the base rule for users and the 'exlusive_user' rules for admin
|
||||
self.rule.active = True
|
||||
self.rule.group_id = self.group_user
|
||||
self.rule_exclusive.active = True
|
||||
user_rule = self.rule
|
||||
manager_rule = self.rule_exclusive
|
||||
user_rule.with_user(self.user).set_approval(res_id=self.record.id, approved=False)
|
||||
manager_rule.with_user(self.manager).set_approval(res_id=self.record.id, approved=False)
|
||||
with self.assertRaises(UserError, msg="Shouldn't be able to cancel approval of someone else"):
|
||||
user_rule.with_user(self.manager).delete_approval(res_id=self.record.id)
|
||||
with self.assertRaises(UserError, msg="Shouldn't be able to create a second entry for the same record+rule"):
|
||||
user_rule.with_user(self.manager).set_approval(res_id=self.record.id, approved=True)
|
||||
self.env.flush_all()
|
||||
with self.assertRaises(UserError, msg="Shouldn't be able to cancel approval of someone else"):
|
||||
manager_rule.with_user(self.user).delete_approval(res_id=self.record.id)
|
||||
with self.assertRaises(UserError, msg="Shouldn't be able to create a second entry for the same record+rule"):
|
||||
manager_rule.with_user(self.user).set_approval(res_id=self.record.id, approved=True)
|
||||
self.env.flush_all()
|
||||
|
||||
def test_07_forbidden_record(self):
|
||||
"""Getting/setting approval on records to which you don't have access."""
|
||||
MODEL = 'res.company'
|
||||
METHOD = 'create'
|
||||
self.rule.write({
|
||||
'active': True,
|
||||
'method': METHOD,
|
||||
'model_id': self.env.ref('base.model_res_company').id,
|
||||
})
|
||||
main_company = self.manager.company_id
|
||||
alternate_company = self.env['res.company'].create({'name': 'SomeCompany'})
|
||||
# I don't need to assert anything: raise = failure
|
||||
self.env['studio.approval.rule'].with_user(self.manager).get_approval_spec(
|
||||
model=MODEL,
|
||||
res_id=main_company.id,
|
||||
method=METHOD,
|
||||
action_id=False
|
||||
)
|
||||
with self.assertRaises(AccessError, msg="Shouldn't be able to get approval spec on record I can't read"):
|
||||
self.env['studio.approval.rule'].with_user(self.manager).get_approval_spec(
|
||||
model=MODEL,
|
||||
res_id=alternate_company.id,
|
||||
method=METHOD,
|
||||
action_id=False
|
||||
)
|
||||
with self.assertRaises(AccessError, msg="Shouldn't be able to set approval on record I can't write on"):
|
||||
self.rule.with_user(self.manager).set_approval(res_id=main_company.id, approved=True)
|
||||
|
||||
def test_08_archive(self):
|
||||
"""Archiving of approvals should be applied even with active_test disabled."""
|
||||
# set the base rule for users and the 'exclusive_user' rules for admin
|
||||
self.rule.active = True
|
||||
self.rule.group_id = self.group_user
|
||||
self.rule_exclusive.active = True
|
||||
manager_rule = self.rule_exclusive
|
||||
approval_result = self.env['studio.approval.rule'].with_user(self.manager).check_approval(
|
||||
model=self.MODEL,
|
||||
res_id=self.record.id,
|
||||
method=self.METHOD,
|
||||
action_id=False)
|
||||
self.assertFalse(approval_result.get('approved'), "Manager shouldn't be able to proceed")
|
||||
manager_rule.active = False
|
||||
approval_result = self.env['studio.approval.rule'].with_context(active_test=False).with_user(self.manager).check_approval(
|
||||
model=self.MODEL,
|
||||
res_id=self.record.id,
|
||||
method=self.METHOD,
|
||||
action_id=False)
|
||||
self.assertTrue(approval_result.get('approved'), "Manager should be able to proceed with archived rule even with active_test disabled")
|
||||
|
||||
def test_09_archive_reverse(self):
|
||||
"""Archiving of rules should not prevent rules with 'exclusive_user' from working."""
|
||||
# set the base rule for users and the 'exclusive_user' rules for admin
|
||||
self.rule.active = True
|
||||
self.rule.group_id = self.group_user
|
||||
self.rule_exclusive.active = True
|
||||
non_exlusive_rule = self.rule
|
||||
# validate a rule that is not exclusive then archive it
|
||||
non_exlusive_rule.with_user(self.manager).set_approval(res_id=self.record.id, approved=True)
|
||||
non_exlusive_rule.active = False
|
||||
# try to approve an exclusive rule which is still remaining
|
||||
approval_result = self.env['studio.approval.rule'].with_user(self.manager).check_approval(
|
||||
model=self.MODEL,
|
||||
res_id=self.record.id,
|
||||
method=self.METHOD,
|
||||
action_id=False)
|
||||
self.assertTrue(approval_result.get('approved'), "should be able to proceed with an exclusive rule if another entry was archived")
|
||||
|
||||
def test_10_exclusive_collision(self):
|
||||
"""Test that exclusive rules for different methods does not interact unexpectdedly."""
|
||||
# set the base rule for users and the 'exlusive_user' rules for admin
|
||||
self.rule.active = True
|
||||
self.rule.group_id = self.group_user
|
||||
self.rule_exclusive.active = True
|
||||
self.rule_exclusive.group_id = self.group_user
|
||||
other_exclusive_rule = self.env['studio.approval.rule'].create({
|
||||
'active': True,
|
||||
'model_id': self.rule_exclusive.model_id.id,
|
||||
'method': 'unlink',
|
||||
'message': "You didn't say the magic word!",
|
||||
'group_id': self.group_user.id,
|
||||
'exclusive_user': True,
|
||||
})
|
||||
approval_result = self.env['studio.approval.rule'].with_user(self.user).check_approval(
|
||||
model=self.MODEL,
|
||||
res_id=self.record.id,
|
||||
method=self.METHOD,
|
||||
action_id=False)
|
||||
self.assertFalse(approval_result.get('approved'), "User shouldn't be able to proceed")
|
||||
approval_result = self.env['studio.approval.rule'].with_user(self.user).check_approval(
|
||||
model=self.MODEL,
|
||||
res_id=self.record.id,
|
||||
method='unlink',
|
||||
action_id=False)
|
||||
# check that the rule on 'unlink' is not prevented by another entry
|
||||
# for a rule that is not related to the same action/method
|
||||
self.assertTrue(approval_result.get('approved'), "User should be able to unlink")
|
||||
|
||||
def test_11_approval_activity(self):
|
||||
"""Test the integration between approvals and next activities"""
|
||||
self.rule.active = True
|
||||
self.rule.responsible_id = self.manager
|
||||
# generate a next activity for the rule's responsible by asking for approval
|
||||
self.env['studio.approval.rule'].with_user(self.user).check_approval(
|
||||
model=self.MODEL,
|
||||
res_id=self.record.id,
|
||||
method=self.METHOD,
|
||||
action_id=False)
|
||||
approval_request = self.env['studio.approval.request'].search(
|
||||
[('rule_id', '=', self.rule.id), ('res_id', '=', self.record.id)])
|
||||
self.assertEqual(len(approval_request), 1, "There should be exactly one approval request")
|
||||
activity = approval_request.mail_activity_id
|
||||
# mark the activity as done, the approval should go through and the request should be deleted
|
||||
activity.with_user(self.manager).action_done()
|
||||
approval_result = self.env['studio.approval.rule'].with_user(self.user).check_approval(
|
||||
model=self.MODEL,
|
||||
res_id=self.record.id,
|
||||
method=self.METHOD,
|
||||
action_id=False)
|
||||
self.assertTrue(approval_result.get('approved'),
|
||||
"The approval should have been granted upon validation of the activity")
|
||||
self.assertFalse(approval_request.exists(),
|
||||
"The approval request should have been deleted upon the activity's confirmation")
|
||||
|
||||
def test_12_approval_activity_spoof(self):
|
||||
"""Test that validating an approval activity as another user will not leak approval rights"""
|
||||
self.rule.active = True
|
||||
self.rule.responsible_id = self.manager
|
||||
# generate a next activity for the rule's responsible by asking for approval
|
||||
self.env['studio.approval.rule'].with_user(self.user).check_approval(
|
||||
model=self.MODEL,
|
||||
res_id=self.record.id,
|
||||
method=self.METHOD,
|
||||
action_id=False)
|
||||
approval_request = self.env['studio.approval.request'].search(
|
||||
[('rule_id', '=', self.rule.id), ('res_id', '=', self.record.id)])
|
||||
activity = approval_request.mail_activity_id
|
||||
# mark the manager's activity as done with the non-manager user
|
||||
# the approval should *not* go through and the request should be deleted (and no errors should be raised)
|
||||
activity.with_user(self.user).action_done()
|
||||
approval_result = self.env['studio.approval.rule'].with_user(self.user).check_approval(
|
||||
model=self.MODEL,
|
||||
res_id=self.record.id,
|
||||
method=self.METHOD,
|
||||
action_id=False)
|
||||
self.assertFalse(approval_result.get('approved'),
|
||||
"The approval should not have been granted upon validation of the activity by anohter user")
|
||||
self.assertFalse(approval_request.exists(),
|
||||
"The approval request should have been deleted upon the activity's confirmation")
|
||||
|
||||
def test_13_approval_activity_dismissal(self):
|
||||
"""Test that granting approval unlinks the activity that was created for that purpose"""
|
||||
self.rule.active = True
|
||||
self.rule.responsible_id = self.manager
|
||||
# generate a next activity for the rule's responsible by asking for approval
|
||||
self.env['studio.approval.rule'].with_user(self.user).check_approval(
|
||||
model=self.MODEL,
|
||||
res_id=self.record.id,
|
||||
method=self.METHOD,
|
||||
action_id=False)
|
||||
approval_request = self.env['studio.approval.request'].search(
|
||||
[('rule_id', '=', self.rule.id), ('res_id', '=', self.record.id)])
|
||||
activity = approval_request.mail_activity_id
|
||||
# grant approval as the manager
|
||||
# both the mail activity and the approval requested should be deleted
|
||||
self.rule.with_user(self.manager).set_approval(res_id=self.record.id, approved=True)
|
||||
self.assertFalse(activity.exists(),
|
||||
"The activity should have been deleted if approval was granted through another channel")
|
||||
self.assertFalse(approval_request.exists(),
|
||||
"The approval request should have been deleted when the approval was granted")
|
||||
|
||||
def test_14_approval_activity_dismissal_refused(self):
|
||||
"""Test that granting approval unlinks the activity that was created for that purpose"""
|
||||
self.rule.active = True
|
||||
self.rule.responsible_id = self.manager
|
||||
# generate a next activity for the rule's responsible by asking for approval
|
||||
self.env['studio.approval.rule'].with_user(self.user).check_approval(
|
||||
model=self.MODEL,
|
||||
res_id=self.record.id,
|
||||
method=self.METHOD,
|
||||
action_id=False)
|
||||
approval_request = self.env['studio.approval.request'].search(
|
||||
[('rule_id', '=', self.rule.id), ('res_id', '=', self.record.id)])
|
||||
activity = approval_request.mail_activity_id
|
||||
# refuse the approval as the manager
|
||||
# both the mail activity and the approval requested should be deleted
|
||||
self.rule.with_user(self.manager).set_approval(res_id=self.record.id, approved=False)
|
||||
self.assertFalse(activity.exists(),
|
||||
"The activity should have been deleted if approval was refused through another channel")
|
||||
self.assertFalse(approval_request.exists(),
|
||||
"The approval request should have been deleted when the approval was refused")
|
||||
436
web_studio/tests/test_ir_model.py
Normal file
436
web_studio/tests/test_ir_model.py
Normal file
@@ -0,0 +1,436 @@
|
||||
|
||||
from unittest.mock import patch
|
||||
|
||||
import odoo
|
||||
|
||||
from odoo.tests.common import TransactionCase
|
||||
from odoo.addons.web_studio.models.ir_model import OPTIONS_WL
|
||||
from odoo.exceptions import ValidationError
|
||||
from odoo import Command
|
||||
|
||||
class TestStudioIrModel(TransactionCase):
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super().setUpClass()
|
||||
# The test mode is necessary in this case. After each test, we call
|
||||
# registry.reset_changes(), which opens a new cursor to retrieve custom
|
||||
# models and fields. A regular cursor would correspond to the state of
|
||||
# the database before setUpClass(), which is not correct. Instead, a
|
||||
# test cursor will correspond to the state of the database of cls.cr at
|
||||
# that point, i.e., before the call to setUp().
|
||||
cls.registry.enter_test_mode(cls.cr)
|
||||
cls.addClassCleanup(cls.registry.leave_test_mode)
|
||||
cls.partner_elon = cls.env['res.partner'].create({
|
||||
'name': 'Elon Tusk', # 🐗
|
||||
'email': 'elon@spacex.com',
|
||||
})
|
||||
# custom m2m field between two models which don't have one yet
|
||||
cls.source_model = cls.env["ir.model"].search([("model", "=", "res.currency")])
|
||||
cls.destination_model = cls.env["ir.model"].search(
|
||||
[("model", "=", "res.country.state")]
|
||||
)
|
||||
cls.m2m = cls.env["ir.model.fields"].create(
|
||||
{
|
||||
"ttype": "many2many",
|
||||
"model_id": cls.source_model.id,
|
||||
"relation": cls.destination_model.model,
|
||||
"name": "x_state_ids",
|
||||
}
|
||||
)
|
||||
|
||||
def test_00_model_creation(self):
|
||||
"""Test that a model gets created with the selected options."""
|
||||
model_options = ['use_partner', 'use_stages', 'use_image',
|
||||
'use_responsible', 'lines']
|
||||
(model, extra_models) = self.env['ir.model'].studio_model_create('Rockets', options=model_options)
|
||||
self.assertEqual(extra_models.mapped('name'), ['Rockets Stages'], 'Only stages should be returned')
|
||||
|
||||
line_model = self.env['ir.model'].search([('model', 'like', model.model + '_line')])
|
||||
self.assertEqual(len(line_model), 1, 'one extra model should have been created for lines')
|
||||
|
||||
created_fields = self.env[model.model]._fields.keys()
|
||||
expected_fields = ['x_studio_partner_id', 'x_studio_stage_id', 'x_studio_image',
|
||||
'x_studio_user_id', model.model + '_line_ids']
|
||||
|
||||
self.assertTrue(all(list(filter(lambda x: item in x, created_fields)) for item in expected_fields),
|
||||
'some expected fields have not been created automatically')
|
||||
|
||||
def test_01_mail_inheritance(self):
|
||||
"""Test that the mail inheritance behaves as expected on custom models."""
|
||||
model_options = ['use_partner', 'use_mail']
|
||||
(model, extra_models) = self.env['ir.model'].studio_model_create('Rockets', options=model_options)
|
||||
self.assertEqual(len(extra_models), 0, 'no extra model should have been created')
|
||||
self.assertTrue(model.is_mail_thread,
|
||||
'model should inherit from mail.thread')
|
||||
# create a record
|
||||
bfr = self.env[model.model].create({
|
||||
'x_name': 'Big Fucking Rocket',
|
||||
'x_studio_partner_id': self.partner_elon.id,
|
||||
})
|
||||
# ensure the partner is suggested in email and sms communication
|
||||
mail_suggested_recipients = bfr._message_get_suggested_recipients()
|
||||
self.assertIn((self.partner_elon.id, '"Elon Tusk" <elon@spacex.com>', None, 'Contact'),
|
||||
mail_suggested_recipients.get(bfr.id),
|
||||
'custom partner field should be suggested in mail communications')
|
||||
sms_suggested_recipients = bfr._sms_get_partner_fields()
|
||||
self.assertIn('x_studio_partner_id', sms_suggested_recipients,
|
||||
'custom partner field should be included in sms communications')
|
||||
|
||||
def test_02_model_option_active(self):
|
||||
"""Test that the `active` behaviour is set up correctly."""
|
||||
model_options = ['use_active', 'use_mail']
|
||||
(model, extra_models) = self.env['ir.model'].studio_model_create('Rockets', options=model_options)
|
||||
self.assertEqual(len(extra_models), 0, 'no extra model should have been created')
|
||||
fields = self.env[model.model]._fields
|
||||
self.assertIn('x_active', fields, 'a custom active field should be set up')
|
||||
default = self.env['ir.default'].get(model.model, 'x_active')
|
||||
self.assertTrue(default, 'the default value for the x_active field should be True')
|
||||
active_field = self.env['ir.model.fields'].search([('name', '=', 'x_active'), ('model_id', '=', model.id)])
|
||||
self.assertTrue(active_field.tracking, 'the x_active field should be tracked')
|
||||
|
||||
def test_03_model_option_sequence(self):
|
||||
"""Test that the `sequence` behaviour is set up correctly."""
|
||||
model_options = ['use_sequence', 'use_mail']
|
||||
(model, extra_models) = self.env['ir.model'].studio_model_create('Rockets', options=model_options)
|
||||
self.assertEqual(len(extra_models), 0, 'no extra model should have been created')
|
||||
fields = self.env[model.model]._fields
|
||||
self.assertIn('x_studio_sequence', fields, 'a custom sequence field should be set up')
|
||||
default = self.env['ir.default'].get(model.model, 'x_studio_sequence')
|
||||
self.assertEqual(default, 10, 'the default value for the x_studio_sequence field should be 10')
|
||||
|
||||
def test_04_model_option_responsible(self):
|
||||
"""Test that the `responsible` behaviour is set up correctly."""
|
||||
model_options = ['use_responsible', 'use_mail']
|
||||
(model, extra_models) = self.env['ir.model'].studio_model_create('Rockets', options=model_options)
|
||||
self.assertEqual(len(extra_models), 0, 'no extra model should have been created')
|
||||
fields = self.env[model.model]._fields
|
||||
self.assertIn('x_studio_user_id', fields, 'a custom responsible (res.users) field should be set up')
|
||||
resp_field = self.env['ir.model.fields'].search([('name', '=', 'x_studio_user_id'), ('model_id', '=', model.id)])
|
||||
self.assertTrue(resp_field.tracking, 'the x_studio_user_id field should be tracked')
|
||||
|
||||
def test_05_model_option_partner(self):
|
||||
"""Test that the `partner` behaviour is set up correctly."""
|
||||
model_options = ['use_partner', 'use_mail']
|
||||
(model, extra_models) = self.env['ir.model'].studio_model_create('Rockets', options=model_options)
|
||||
self.assertEqual(len(extra_models), 0, 'no extra model should have been created')
|
||||
fields = self.env[model.model]._fields
|
||||
self.assertIn('x_studio_partner_id', fields, 'a custom partner field should be set up')
|
||||
self.assertIn('x_studio_partner_phone', fields, 'a related field x_studio_partner_phone should be set up')
|
||||
self.assertIn('x_studio_partner_email', fields, 'a related field x_studio_partner_email should be set up')
|
||||
partner_field = self.env['ir.model.fields'].search([('name', '=', 'x_studio_partner_id'), ('model_id', '=', model.id)])
|
||||
self.assertTrue(partner_field.tracking, 'the x_studio_partner_id field should be tracked')
|
||||
|
||||
def test_06_model_option_company(self):
|
||||
"""Test that the `company` behaviour is set up correctly."""
|
||||
model_options = ['use_company', 'use_mail']
|
||||
(model, extra_models) = self.env['ir.model'].studio_model_create('Rockets', options=model_options)
|
||||
self.assertEqual(len(extra_models), 0, 'no extra model should have been created')
|
||||
fields = self.env[model.model]._fields
|
||||
self.assertIn('x_studio_company_id', fields, 'a custom company field should be set up')
|
||||
mc_rule = self.env['ir.rule'].search([
|
||||
('model_id', '=', model.id),
|
||||
('domain_force', 'like', 'x_studio_company_id')
|
||||
])
|
||||
self.assertEqual(len(mc_rule), 1, 'there should be a multi-company rule for the model')
|
||||
comp_field = self.env['ir.model.fields'].search([('name', '=', 'x_studio_company_id'), ('model_id', '=', model.id)])
|
||||
self.assertTrue(comp_field.tracking, 'the x_studio_company_id field should be tracked')
|
||||
main_company = self.env.ref('base.main_company')
|
||||
default = self.env['ir.default'].get(model.model, 'x_studio_company_id', company_id=main_company.id)
|
||||
self.assertEqual(default, main_company.id, 'the default value for the x_studio_company_id should be set')
|
||||
new_company = self.env['res.company'].create({'name': 'SpaceY'})
|
||||
new_default = self.env['ir.default'].get(model.model, 'x_studio_company_id', company_id=new_company.id)
|
||||
self.assertEqual(new_default, new_company.id, 'default values for new companies should be created with the company')
|
||||
|
||||
def test_07_model_option_notes(self):
|
||||
"""Test that the `notes` behaviour is set up correctly."""
|
||||
model_options = ['use_notes', 'use_mail']
|
||||
(model, extra_models) = self.env['ir.model'].studio_model_create('Rockets', options=model_options)
|
||||
self.assertEqual(len(extra_models), 0, 'no extra model should have been created')
|
||||
fields = self.env[model.model]._fields
|
||||
self.assertIn('x_studio_notes', fields, 'a custom notes field should be set up')
|
||||
|
||||
def test_08_model_option_date(self):
|
||||
"""Test that the `date` behaviour is set up correctly."""
|
||||
model_options = ['use_date', 'use_mail']
|
||||
(model, extra_models) = self.env['ir.model'].studio_model_create('Rockets', options=model_options)
|
||||
self.assertEqual(len(extra_models), 0, 'no extra model should have been created')
|
||||
fields = self.env[model.model]._fields
|
||||
self.assertIn('x_studio_date', fields, 'a custom date field should be set up')
|
||||
date_field = self.env['ir.model.fields'].search([('name', '=', 'x_studio_date'), ('model_id', '=', model.id)])
|
||||
self.assertFalse(date_field.tracking, 'the x_studio_date field should not be tracked')
|
||||
|
||||
def test_09_model_option_double_dates(self):
|
||||
"""Test that the `double date` behaviour is set up correctly."""
|
||||
model_options = ['use_double_dates', 'use_mail']
|
||||
(model, extra_models) = self.env['ir.model'].studio_model_create('Rockets', options=model_options)
|
||||
self.assertEqual(len(extra_models), 0, 'no extra model should have been created')
|
||||
fields = self.env[model.model]._fields
|
||||
self.assertIn('x_studio_date_start', fields, 'a custom start date field should be set up')
|
||||
self.assertIn('x_studio_date_stop', fields, 'a custom stop date field should be set up')
|
||||
date_fields = self.env['ir.model.fields'].search([('name', 'like', 'x_studio_date'), ('model_id', '=', model.id)])
|
||||
for date_field in date_fields:
|
||||
self.assertFalse(date_field.tracking, 'start/stop date fields should not be tracked')
|
||||
|
||||
def test_10_model_option_value(self):
|
||||
"""Test that the `value` behaviour is set up correctly."""
|
||||
model_options = ['use_value', 'use_mail']
|
||||
(model, extra_models) = self.env['ir.model'].studio_model_create('Rockets', options=model_options)
|
||||
self.assertEqual(len(extra_models), 0, 'no extra model should have been created')
|
||||
fields = self.env[model.model]._fields
|
||||
self.assertIn('x_studio_currency_id', fields, 'a custom currency field should be set up')
|
||||
self.assertIn('x_studio_currency_id', fields, 'a custom value field should be set up')
|
||||
value_field = self.env['ir.model.fields'].search([('name', '=', 'x_studio_value'), ('model_id', '=', model.id)])
|
||||
self.assertTrue(value_field.tracking, 'the x_studio_value field should be tracked')
|
||||
main_company = self.env.ref('base.main_company')
|
||||
default = self.env['ir.default'].get(model.model, 'x_studio_currency_id', company_id=main_company.id)
|
||||
self.assertEqual(default, main_company.currency_id.id, 'the default value for the x_studio_currency_id should be set')
|
||||
new_company = self.env['res.company'].create({'name': 'SpaceY', 'currency_id': self.env.ref('base.INR').id})
|
||||
new_default = self.env['ir.default'].get(model.model, 'x_studio_currency_id', company_id=new_company.id)
|
||||
self.assertEqual(new_default, new_company.currency_id.id, 'default currency for new companies should be create with the company')
|
||||
|
||||
def test_11_model_option_image(self):
|
||||
"""Test that the `image` behaviour is set up correctly."""
|
||||
model_options = ['use_image', 'use_mail']
|
||||
(model, extra_models) = self.env['ir.model'].studio_model_create('Rockets', options=model_options)
|
||||
self.assertEqual(len(extra_models), 0, 'no extra model should have been created')
|
||||
fields = self.env[model.model]._fields
|
||||
self.assertIn('x_studio_image', fields, 'a custom image field should be set up')
|
||||
|
||||
def test_12_model_option_stages(self):
|
||||
"""Test that the `stage` behaviour is set up correctly."""
|
||||
model_options = ['use_stages', 'use_mail']
|
||||
(model, extra_model) = self.env['ir.model'].studio_model_create('Rockets', options=model_options)
|
||||
self.assertEqual(len(extra_model), 1, 'an extra model should have been created for stages')
|
||||
stage_fields = self.env[extra_model.model]._fields
|
||||
self.assertIn('x_studio_sequence', stage_fields, 'stages should have a sequence')
|
||||
fields = self.env[model.model]._fields
|
||||
self.assertIn('x_studio_stage_id', fields, 'a custom stage field should be set up')
|
||||
self.assertIn('x_studio_priority', fields, 'a custom priority field should be set up')
|
||||
self.assertIn('x_color', fields, 'a custom color field should be set up')
|
||||
self.assertIn('x_studio_kanban_state', fields, 'a custom kanban state field should be set up')
|
||||
auto_stage = self.env[extra_model.model].search([])
|
||||
default = self.env['ir.default'].get(model.model, 'x_studio_stage_id')
|
||||
self.assertEqual(default, auto_stage.ids[0], 'the default stage should be set')
|
||||
stage_field = self.env['ir.model.fields'].search([('name', '=', 'x_studio_stage_id'), ('model_id', '=', model.id)])
|
||||
self.assertTrue(stage_field.tracking, 'the x_studio_stage_id field should be tracked')
|
||||
|
||||
def test_13_model_option_tags(self):
|
||||
"""Test that the `tags` behaviour is set up correctly."""
|
||||
model_options = ['use_tags']
|
||||
(model, extra_model) = self.env['ir.model'].studio_model_create('Rockets', options=model_options)
|
||||
self.assertEqual(len(extra_model), 1, 'an extra model should have been created for tags')
|
||||
stage_fields = self.env[extra_model.model]._fields
|
||||
self.assertIn('x_color', stage_fields, 'tags should have a color')
|
||||
fields = self.env[model.model]._fields
|
||||
self.assertIn('x_studio_tag_ids', fields, 'a custom tags field should be set up')
|
||||
|
||||
def test_14_all_options(self):
|
||||
"""Test auto-view generation for custom models with all options enabled."""
|
||||
# Enable ALL THE OPTIONS
|
||||
(model, extra_model) = self.env['ir.model'].studio_model_create('Rockets', options=OPTIONS_WL)
|
||||
# I'm just checking it doesn't crash for now 👐
|
||||
|
||||
def test_15_custom_model_security(self):
|
||||
"""Test that ACLs are created for a custom model."""
|
||||
model_options = []
|
||||
(model, _) = self.env['ir.model'].studio_model_create('Rockets', options=model_options)
|
||||
acl_admin = self.env['ir.model.access'].search([
|
||||
('model_id', '=', model.id),
|
||||
('group_id', '=', self.env.ref('base.group_system').id)
|
||||
])
|
||||
self.assertTrue(acl_admin.perm_read, 'admin should have read access on custom models')
|
||||
self.assertTrue(acl_admin.perm_write, 'admin should have write access on custom models')
|
||||
self.assertTrue(acl_admin.perm_create, 'admin should have create access on custom models')
|
||||
self.assertTrue(acl_admin.perm_unlink, 'admin should have unlink access on custom models')
|
||||
acl_user = self.env['ir.model.access'].search([
|
||||
('model_id', '=', model.id),
|
||||
('group_id', '=', self.env.ref('base.group_user').id)
|
||||
])
|
||||
self.assertTrue(acl_user.perm_read, 'user should have read access on custom models')
|
||||
self.assertTrue(acl_user.perm_write, 'user should have write access on custom models')
|
||||
self.assertTrue(acl_user.perm_create, 'user should have create access on custom models')
|
||||
self.assertFalse(acl_user.perm_unlink, 'user should not have unlink access on custom models')
|
||||
|
||||
def test_16_next_relation(self):
|
||||
"""Check that creating the same m2m will result in a new relation table."""
|
||||
IrModelFields = self.env["ir.model.fields"].with_context(studio=True)
|
||||
current_table = IrModelFields._custom_many2many_names(
|
||||
"res.currency", "res.country.state"
|
||||
)[0]
|
||||
new_m2m = IrModelFields.create(
|
||||
{
|
||||
"ttype": "many2many",
|
||||
"model_id": self.source_model.id,
|
||||
"relation": self.destination_model.model,
|
||||
"name": "x_state_ids_2",
|
||||
"relation_table": IrModelFields._get_next_relation(
|
||||
self.source_model.model, self.destination_model.model
|
||||
),
|
||||
}
|
||||
)
|
||||
self.assertNotEqual(
|
||||
new_m2m.relation_table,
|
||||
current_table,
|
||||
"the second m2m should have its own relation table",
|
||||
)
|
||||
|
||||
def test_17_reverse_relation(self):
|
||||
IrModelFields = self.env["ir.model.fields"].with_context(studio=True)
|
||||
reverse_m2m = IrModelFields.create(
|
||||
{
|
||||
"ttype": "many2many",
|
||||
"model_id": self.destination_model.id,
|
||||
"relation": self.source_model.model,
|
||||
"name": "x_currency_ids",
|
||||
"relation_table": IrModelFields._get_next_relation(
|
||||
self.destination_model.model, self.source_model.model
|
||||
),
|
||||
}
|
||||
)
|
||||
self.assertEqual(
|
||||
self.m2m.relation_table,
|
||||
reverse_m2m.relation_table,
|
||||
"the second m2m should have the same relation table as the first m2m of the source model",
|
||||
)
|
||||
new_m2m = IrModelFields.create(
|
||||
{
|
||||
"ttype": "many2many",
|
||||
"model_id": self.source_model.id,
|
||||
"relation": self.destination_model.model,
|
||||
"name": "x_state_ids_2",
|
||||
"relation_table": IrModelFields._get_next_relation(
|
||||
self.source_model.model, self.destination_model.model
|
||||
),
|
||||
}
|
||||
)
|
||||
reverse_new_m2m = IrModelFields.create(
|
||||
{
|
||||
"ttype": "many2many",
|
||||
"model_id": self.destination_model.id,
|
||||
"relation": self.source_model.model,
|
||||
"name": "x_currency_ids_2",
|
||||
"relation_table": IrModelFields._get_next_relation(
|
||||
self.destination_model.model, self.source_model.model
|
||||
),
|
||||
}
|
||||
)
|
||||
self.assertEqual(
|
||||
new_m2m.relation_table,
|
||||
reverse_new_m2m.relation_table,
|
||||
"the second reverse m2m should have the same relation table as the second m2m of the source model",
|
||||
)
|
||||
|
||||
def test_18_lots_of_relations(self):
|
||||
IrModelFields = self.env["ir.model.fields"].with_context(studio=True)
|
||||
NUM_TEST = 10 # because some people are just that stupid
|
||||
attempt = 0
|
||||
while attempt < NUM_TEST:
|
||||
attempt += 1
|
||||
IrModelFields.create(
|
||||
{
|
||||
"ttype": "many2many",
|
||||
"model_id": self.source_model.id,
|
||||
"relation": self.destination_model.model,
|
||||
"name": "x_currency_ids_%s" % attempt,
|
||||
"relation_table": IrModelFields._get_next_relation(
|
||||
self.source_model.model, self.destination_model.model
|
||||
),
|
||||
}
|
||||
)
|
||||
latest_relation = IrModelFields.search_read(
|
||||
[
|
||||
("ttype", "=", "many2many"),
|
||||
("model_id", "=", self.source_model.id),
|
||||
("relation", "=", self.destination_model.model),
|
||||
],
|
||||
fields=["relation_table"],
|
||||
order="id desc",
|
||||
limit=1,
|
||||
)
|
||||
default = IrModelFields._custom_many2many_names(
|
||||
self.source_model.model, self.destination_model.model
|
||||
)[0]
|
||||
self.assertEqual(
|
||||
latest_relation[0]["relation_table"], "%s_%s" % (default, NUM_TEST)
|
||||
)
|
||||
|
||||
def test_19_custom_model_security(self):
|
||||
"""Test that ACLs are created for a custom model using name create."""
|
||||
|
||||
model_id, name = self.env['ir.model'].with_context(studio=True).name_create('X_Rockets')
|
||||
acl_admin = self.env['ir.model.access'].search([
|
||||
('model_id', '=', model_id),
|
||||
('group_id', '=', self.env.ref('base.group_system').id)
|
||||
])
|
||||
self.assertTrue(acl_admin.perm_read, 'admin should have read access on custom models')
|
||||
self.assertTrue(acl_admin.perm_write, 'admin should have write access on custom models')
|
||||
self.assertTrue(acl_admin.perm_create, 'admin should have create access on custom models')
|
||||
self.assertTrue(acl_admin.perm_unlink, 'admin should have unlink access on custom models')
|
||||
acl_user = self.env['ir.model.access'].search([
|
||||
('model_id', '=', model_id),
|
||||
('group_id', '=', self.env.ref('base.group_user').id)
|
||||
])
|
||||
self.assertTrue(acl_user.perm_read, 'user should have read access on custom models')
|
||||
self.assertTrue(acl_user.perm_write, 'user should have write access on custom models')
|
||||
self.assertTrue(acl_user.perm_create, 'user should have create access on custom models')
|
||||
self.assertFalse(acl_user.perm_unlink, 'user should not have unlink access on custom models')
|
||||
|
||||
def test_20_prevent_double_underscore(self):
|
||||
IrModelFields = self.env["ir.model.fields"]
|
||||
with self.assertRaises(ValidationError, msg="Custom field names cannot contain double underscores."):
|
||||
IrModelFields.create(
|
||||
{
|
||||
"ttype": "char",
|
||||
"model_id": self.source_model.id,
|
||||
"name": "x_studio_hello___hap",
|
||||
}
|
||||
)
|
||||
|
||||
def test_21_set_view_mode_new_window_action(self):
|
||||
"""Test that the `view_mode` for window action is set correctly."""
|
||||
|
||||
model = self.env['ir.model'].create({
|
||||
'name': 'Rockets',
|
||||
'model': 'x_rockets',
|
||||
'field_id': [
|
||||
Command.create({'name': 'x_name', 'ttype': 'char', 'field_description': 'Name'}),
|
||||
]
|
||||
})
|
||||
action = model._create_default_action('x_rockets')
|
||||
self.assertEqual(action.view_mode, 'tree,form', 'tree and form should be set as a default view mode on window action')
|
||||
|
||||
def test_22_rename_window_action(self):
|
||||
""" Test renaming a menu will rename the windows action."""
|
||||
|
||||
model = self.env['ir.model'].create({
|
||||
'name': 'Rockets',
|
||||
'model': 'x_rockets',
|
||||
'field_id': [
|
||||
Command.create({'name': 'x_name', 'ttype': 'char', 'field_description': 'Name'}),
|
||||
]
|
||||
})
|
||||
action = model._create_default_action('Rockets')
|
||||
action_ref = 'ir.actions.act_window,' + str(action.id)
|
||||
new_menu = self.env['ir.ui.menu'].with_context(studio=True).create({
|
||||
'name': 'Rockets',
|
||||
'action': action_ref,
|
||||
})
|
||||
self.assertEqual(action.name, new_menu.name, 'action and menu name should be same')
|
||||
# rename the menu name
|
||||
new_menu.name = 'new Rockets'
|
||||
self.assertEqual(action.name, new_menu.name, 'rename the menu name should rename the window action name')
|
||||
|
||||
def test_performance_01_fields_batch(self):
|
||||
"""Test number of call to setup_models when creating a model with multiple"""
|
||||
count_setup_models = 0
|
||||
orig_setup_models = odoo.modules.registry.Registry.setup_models
|
||||
def setup_models(registry, cr):
|
||||
nonlocal count_setup_models
|
||||
count_setup_models += 1
|
||||
orig_setup_models(registry, cr)
|
||||
with patch('odoo.modules.registry.Registry.setup_models', new=setup_models):
|
||||
# not: using a specific model (PerformanceIssues and not Rockets) is important since after the rollback of the test,
|
||||
# the model will be missing but x_rockets is still in the pool, breaking some optimizations
|
||||
self.env['ir.model'].with_context(studio=True).studio_model_create('PerformanceIssues', options=OPTIONS_WL)
|
||||
self.assertEqual(count_setup_models, 1)
|
||||
187
web_studio/tests/test_report_editor.py
Normal file
187
web_studio/tests/test_report_editor.py
Normal file
@@ -0,0 +1,187 @@
|
||||
from odoo.addons.web_studio.controllers.main import WebStudioController
|
||||
from odoo.http import _request_stack
|
||||
from odoo.tests.common import TransactionCase
|
||||
from odoo.tools import DotDict
|
||||
|
||||
|
||||
class TestReportEditor(TransactionCase):
|
||||
|
||||
def setUp(self):
|
||||
super(TestReportEditor, self).setUp()
|
||||
self.session = DotDict({'debug': ''})
|
||||
self.is_frontend = False
|
||||
_request_stack.push(self) # crappy hack to use a fake Request
|
||||
self.WebStudioController = WebStudioController()
|
||||
|
||||
def test_copy_inherit_report(self):
|
||||
report = self.env['ir.actions.report'].create({
|
||||
'name': 'test inherit report user',
|
||||
'report_name': 'web_studio.test_inherit_report_user',
|
||||
'model': 'res.users',
|
||||
})
|
||||
self.env['ir.ui.view'].create({
|
||||
'type': 'qweb',
|
||||
'name': 'web_studio.test_inherit_report_hi',
|
||||
'key': 'web_studio.test_inherit_report_hi',
|
||||
'arch': '''
|
||||
<t t-name="web_studio.test_inherit_report_hi">
|
||||
hi
|
||||
</t>
|
||||
''',
|
||||
})
|
||||
parent_view = self.env['ir.ui.view'].create({
|
||||
'type': 'qweb',
|
||||
'name': 'web_studio.test_inherit_report_user_parent',
|
||||
'key': 'web_studio.test_inherit_report_user_parent',
|
||||
'arch': '''
|
||||
<t t-name="web_studio.test_inherit_report_user_parent_view_parent">
|
||||
<t t-call="web_studio.test_inherit_report_hi"/>!
|
||||
</t>
|
||||
''',
|
||||
})
|
||||
self.env['ir.ui.view'].create({
|
||||
'type': 'qweb',
|
||||
'name': 'web_studio.test_inherit_report_user',
|
||||
'key': 'web_studio.test_inherit_report_user',
|
||||
'arch': '''
|
||||
<xpath expr="." position="inside">
|
||||
<t t-call="web_studio.test_inherit_report_hi"/>!!
|
||||
</xpath>
|
||||
''',
|
||||
'inherit_id': parent_view.id,
|
||||
|
||||
})
|
||||
|
||||
# check original report render to expected output
|
||||
report_html = report._render_template(report.report_name).decode()
|
||||
self.assertEqual(''.join(report_html.split()), 'hi!hi!!')
|
||||
|
||||
# duplicate original report
|
||||
report.copy_report_and_template()
|
||||
copy_report = self.env['ir.actions.report'].search([
|
||||
('report_name', '=', 'web_studio.test_inherit_report_user_copy_1'),
|
||||
])
|
||||
|
||||
# check duplicated report render to expected output
|
||||
copy_report_html = copy_report._render_template(copy_report.report_name).decode()
|
||||
self.assertEqual(''.join(copy_report_html.split()), 'hi!hi!!')
|
||||
|
||||
# check that duplicated view is inheritance combination of original view
|
||||
copy_view = self.env['ir.ui.view'].search([
|
||||
('key', '=', copy_report.report_name),
|
||||
])
|
||||
self.assertFalse(copy_view.inherit_id, 'copied view does not inherit another one')
|
||||
found = len(copy_view.arch_db.split('test_inherit_report_hi_copy_1')) - 1
|
||||
self.assertEqual(found, 2, 't-call is duplicated one time and used 2 times')
|
||||
|
||||
|
||||
def test_duplicate(self):
|
||||
# Inheritance during an upgrade work only with loaded views
|
||||
# The following force the inheritance to work for all views
|
||||
# so the created view is correctly inherited
|
||||
self.env = self.env(context={'load_all_views': True})
|
||||
|
||||
|
||||
# Create a report/view containing "foo"
|
||||
report = self.env['ir.actions.report'].create({
|
||||
'name': 'test duplicate',
|
||||
'report_name': 'web_studio.test_duplicate_foo',
|
||||
'model': 'res.users',})
|
||||
|
||||
self.env['ir.ui.view'].create({
|
||||
'type': 'qweb',
|
||||
'name': 'test_duplicate_foo',
|
||||
'key': 'web_studio.test_duplicate_foo',
|
||||
'arch': "<t t-name='web_studio.test_duplicate_foo'>foo</t>",})
|
||||
|
||||
duplicate_domain = [('report_name', '=like', 'web_studio.test_duplicate_foo_copy_%')]
|
||||
|
||||
# Duplicate the report and retrieve the duplicated view
|
||||
report.copy_report_and_template()
|
||||
copy1 = self.env['ir.actions.report'].search(duplicate_domain)
|
||||
copy1.ensure_one() # watchdog
|
||||
copy1_view = self.env['ir.ui.view'].search([
|
||||
('key', '=', copy1.report_name)])
|
||||
copy1_view.ensure_one() # watchdog
|
||||
|
||||
# Inherit the view to replace "foo" by "bar"
|
||||
self.env['ir.ui.view'].create({
|
||||
'inherit_id': copy1_view.id,
|
||||
'key': copy1.report_name,
|
||||
'arch': '''
|
||||
<xpath expr="." position="replace">
|
||||
<t t-name='%s'>bar</t>
|
||||
</xpath>
|
||||
''' % copy1.report_name,})
|
||||
|
||||
# Assert the duplicated view renders "bar" then unlink the report
|
||||
copy1_html = copy1._render_template(copy1.report_name).decode()
|
||||
self.assertEqual(''.join(copy1_html.split()), 'bar')
|
||||
copy1.unlink()
|
||||
|
||||
# Re-duplicate the original report, it must renders "foo"
|
||||
report.copy_report_and_template()
|
||||
copy2 = self.env['ir.actions.report'].search(duplicate_domain)
|
||||
copy2.ensure_one()
|
||||
copy2_html = copy2._render_template(copy2.report_name).decode()
|
||||
self.assertEqual(''.join(copy2_html.split()), 'foo')
|
||||
|
||||
def test_copy_custom_model_rendering(self):
|
||||
report = self.env['ir.actions.report'].search([('report_name', '=', 'base.report_irmodulereference')])
|
||||
report.copy_report_and_template()
|
||||
copy = self.env['ir.actions.report'].search([('report_name', '=', 'base.report_irmodulereference_copy_1')])
|
||||
report_model = self.env['ir.actions.report']._get_rendering_context_model(copy)
|
||||
self.assertIsNotNone(report_model)
|
||||
|
||||
def test_duplicate_keep_translations(self):
|
||||
def create_view(name, **kwargs):
|
||||
arch = '<div>{}</div>'.format(name)
|
||||
if kwargs.get('inherit_id'):
|
||||
arch = '<xpath expr="." path="inside">{}</xpath>'.format(arch)
|
||||
name = 'web_studio.test_keep_translations_{}'.format(name)
|
||||
return self.env['ir.ui.view'].create(dict({
|
||||
'type': 'qweb',
|
||||
'name': name,
|
||||
'key': name,
|
||||
'arch': arch,
|
||||
}, **kwargs))
|
||||
|
||||
report = self.env['ir.actions.report'].create({
|
||||
'name': 'test inherit report user',
|
||||
'report_name': 'web_studio.test_keep_translations_ab',
|
||||
'model': 'res.users',
|
||||
}).with_context(load_all_views=True)
|
||||
|
||||
self.env.ref('base.lang_fr').active = True
|
||||
views = report.env['ir.ui.view']
|
||||
views += create_view("a_")
|
||||
root = views[-1]
|
||||
views += create_view("b_")
|
||||
views += create_view("aa", inherit_id=root.id, mode="primary")
|
||||
views += create_view("ab", inherit_id=root.id)
|
||||
target = views[-1]
|
||||
views += create_view("aba", inherit_id=target.id)
|
||||
views[-1].arch = views[-1].arch.replace('aba', 'a_</div>aba<div>ab')
|
||||
views += create_view("abb", inherit_id=target.id, mode="primary")
|
||||
|
||||
for view in views.with_context(lang='fr_FR'):
|
||||
terms = view._fields['arch_db'].get_trans_terms(view.arch_db)
|
||||
view.update_field_translations('arch_db', {'fr_FR': {term: '%s in fr' % term for term in terms}})
|
||||
|
||||
combined_arch = '<div>a_<div>ab</div><div>a_</div>aba<div>ab</div></div>'
|
||||
self.assertEqual(target._read_template(target.id), combined_arch)
|
||||
|
||||
# duplicate original report, views will be combined into one
|
||||
report.copy_report_and_template()
|
||||
copy_view = self.env['ir.ui.view'].search([
|
||||
('key', '=', 'web_studio.test_keep_translations_ab_copy_1'),
|
||||
])
|
||||
self.assertEqual(copy_view.arch, combined_arch)
|
||||
|
||||
# translations of combined views have been copied to the new view
|
||||
new_arch = '<div>a_ in fr<div>ab in fr</div><div>a_ in fr</div>aba in fr<div>ab in fr</div></div>'
|
||||
self.assertEqual(copy_view.with_context(lang='fr_FR').arch, new_arch)
|
||||
|
||||
def tearDown(self):
|
||||
super(TestReportEditor, self).tearDown()
|
||||
_request_stack.pop()
|
||||
27
web_studio/tests/test_ui.py
Normal file
27
web_studio/tests/test_ui.py
Normal file
@@ -0,0 +1,27 @@
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
import odoo.tests
|
||||
|
||||
|
||||
@odoo.tests.tagged('post_install', '-at_install')
|
||||
class TestUi(odoo.tests.HttpCase):
|
||||
|
||||
def test_new_app_and_report(self):
|
||||
self.start_tour("/web", 'web_studio_new_app_tour', login="admin")
|
||||
|
||||
# the report tour is based on the result of the former tour
|
||||
self.start_tour("/web?debug=tests", 'web_studio_new_report_tour', login="admin")
|
||||
self.start_tour("/web?debug=tests", "web_studio_new_report_basic_layout_tour", login="admin")
|
||||
|
||||
def test_optional_fields(self):
|
||||
self.start_tour("/web?debug=tests", 'web_studio_hide_fields_tour', login="admin")
|
||||
|
||||
def test_model_option_value(self):
|
||||
self.start_tour("/web?debug=tests", 'web_studio_model_option_value_tour', login="admin")
|
||||
|
||||
def test_rename(self):
|
||||
self.start_tour("/web?debug=tests", 'web_studio_tests_tour', login="admin", timeout=200)
|
||||
|
||||
def test_approval(self):
|
||||
self.start_tour("/web?debug=tests", 'web_studio_approval_tour', login="admin")
|
||||
324
web_studio/tests/test_view_editor.py
Normal file
324
web_studio/tests/test_view_editor.py
Normal file
@@ -0,0 +1,324 @@
|
||||
import odoo
|
||||
from odoo import api
|
||||
from odoo.tools import DotDict
|
||||
from odoo.http import _request_stack
|
||||
from odoo.tests.common import TransactionCase
|
||||
from odoo.addons.web_studio.controllers.main import WebStudioController
|
||||
from copy import deepcopy
|
||||
from lxml import etree
|
||||
|
||||
class TestStudioController(TransactionCase):
|
||||
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
self.env = api.Environment(self.cr, odoo.SUPERUSER_ID, {'load_all_views': True})
|
||||
_request_stack.push(self)
|
||||
self.session = DotDict({'debug': ''})
|
||||
self.studio_controller = WebStudioController()
|
||||
|
||||
def tearDown(self):
|
||||
super().tearDown()
|
||||
_request_stack.pop()
|
||||
|
||||
def _transform_arch_for_assert(self, arch_string):
|
||||
parser = etree.XMLParser(remove_blank_text=True)
|
||||
arch_string = etree.fromstring(arch_string, parser=parser)
|
||||
return etree.tostring(arch_string, pretty_print=True, encoding='unicode')
|
||||
|
||||
def assertViewArchEqual(self, original, expected):
|
||||
if original:
|
||||
original = self._transform_arch_for_assert(original)
|
||||
if expected:
|
||||
expected = self._transform_arch_for_assert(expected)
|
||||
self.assertEqual(original, expected)
|
||||
|
||||
|
||||
class TestEditView(TestStudioController):
|
||||
|
||||
def edit_view(self, base_view, studio_arch="", operations=None, model=None):
|
||||
_ops = None
|
||||
if isinstance(operations, list):
|
||||
_ops = []
|
||||
for op in operations:
|
||||
_ops.append(deepcopy(op)) # the edit view controller may alter objects in place
|
||||
if studio_arch == "":
|
||||
studio_arch = "<data/>"
|
||||
return self.studio_controller.edit_view(base_view.id, studio_arch, _ops, model)
|
||||
|
||||
def test_edit_view_binary_and_attribute(self):
|
||||
base_view = self.env['ir.ui.view'].create({
|
||||
'name': 'TestForm',
|
||||
'type': 'form',
|
||||
'model': 'res.partner',
|
||||
'arch': """
|
||||
<form>
|
||||
<field name="display_name" />
|
||||
</form>"""
|
||||
})
|
||||
|
||||
add_binary_op = {
|
||||
'type': 'add',
|
||||
'target': {'tag': 'field',
|
||||
'attrs': {'name': 'display_name'},
|
||||
'xpath_info': [{'tag': 'form', 'indice': 1},
|
||||
{'tag': 'field', 'indice': 1}]},
|
||||
'position': 'after',
|
||||
'node': {'tag': 'field',
|
||||
'attrs': {},
|
||||
'field_description': {'type': 'binary',
|
||||
'field_description': 'New File',
|
||||
'name': 'x_studio_binary_field_WocAO',
|
||||
'model_name': 'res.partner'}}
|
||||
}
|
||||
|
||||
self.edit_view(base_view, operations=[add_binary_op])
|
||||
self.assertViewArchEqual(
|
||||
base_view.get_combined_arch(),
|
||||
"""
|
||||
<form>
|
||||
<field name="display_name"/>
|
||||
<field filename="x_studio_binary_field_WocAO_filename" name="x_studio_binary_field_WocAO"/>
|
||||
<field invisible="1" name="x_studio_binary_field_WocAO_filename"/>
|
||||
</form>
|
||||
"""
|
||||
)
|
||||
|
||||
add_widget_op = {
|
||||
'type': 'attributes',
|
||||
'target': {'tag': 'field',
|
||||
'attrs': {'name': 'x_studio_binary_field_WocAO'},
|
||||
'xpath_info': [{'tag': 'form', 'indice': 1},
|
||||
{'tag': 'field', 'indice': 2}]},
|
||||
'position': 'attributes',
|
||||
'node': {'tag': 'field',
|
||||
'attrs': {'filename': 'x_studio_binary_field_WocAO_filename',
|
||||
'name': 'x_studio_binary_field_WocAO',
|
||||
'modifiers': {},
|
||||
'id': 'x_studio_binary_field_WocAO'},
|
||||
'children': [],
|
||||
'has_label': True},
|
||||
'new_attrs': {'widget': 'pdf_viewer', 'options': ''}
|
||||
}
|
||||
|
||||
ops = [
|
||||
add_binary_op,
|
||||
add_widget_op
|
||||
]
|
||||
self.edit_view(base_view, operations=ops)
|
||||
self.assertViewArchEqual(
|
||||
base_view.get_combined_arch(),
|
||||
"""
|
||||
<form>
|
||||
<field name="display_name"/>
|
||||
<field filename="x_studio_binary_field_WocAO_filename" name="x_studio_binary_field_WocAO" widget="pdf_viewer"/>
|
||||
<field invisible="1" name="x_studio_binary_field_WocAO_filename"/>
|
||||
</form>
|
||||
"""
|
||||
)
|
||||
|
||||
def test_edit_view_binary_and_attribute_then_remove_binary(self):
|
||||
base_view = self.env['ir.ui.view'].create({
|
||||
'name': 'TestForm',
|
||||
'type': 'form',
|
||||
'model': 'res.partner',
|
||||
'arch': """
|
||||
<form>
|
||||
<field name="display_name" />
|
||||
</form>"""
|
||||
})
|
||||
|
||||
add_binary_op = {
|
||||
'type': 'add',
|
||||
'target': {'tag': 'field',
|
||||
'attrs': {'name': 'display_name'},
|
||||
'xpath_info': [{'tag': 'form', 'indice': 1},
|
||||
{'tag': 'field', 'indice': 1}]},
|
||||
'position': 'after',
|
||||
'node': {'tag': 'field',
|
||||
'attrs': {},
|
||||
'field_description': {'type': 'binary',
|
||||
'field_description': 'New File',
|
||||
'name': 'x_studio_binary_field_WocAO',
|
||||
'model_name': 'res.partner'}}
|
||||
}
|
||||
|
||||
self.edit_view(base_view, operations=[add_binary_op])
|
||||
|
||||
add_widget_op = {
|
||||
'type': 'attributes',
|
||||
'target': {'tag': 'field',
|
||||
'attrs': {'name': 'x_studio_binary_field_WocAO'},
|
||||
'xpath_info': [{'tag': 'form', 'indice': 1},
|
||||
{'tag': 'field', 'indice': 2}]},
|
||||
'position': 'attributes',
|
||||
'node': {'tag': 'field',
|
||||
'attrs': {'filename': 'x_studio_binary_field_WocAO_filename',
|
||||
'name': 'x_studio_binary_field_WocAO',
|
||||
'modifiers': {},
|
||||
'id': 'x_studio_binary_field_WocAO'},
|
||||
'children': [],
|
||||
'has_label': True},
|
||||
'new_attrs': {'widget': 'pdf_viewer', 'options': ''}
|
||||
}
|
||||
|
||||
ops = [
|
||||
add_binary_op,
|
||||
add_widget_op
|
||||
]
|
||||
self.edit_view(base_view, operations=ops)
|
||||
|
||||
remove_binary_op = {
|
||||
'type': 'remove',
|
||||
'target': {'tag': 'field',
|
||||
'attrs': {'name': 'x_studio_binary_field_WocAO'},
|
||||
'xpath_info': [{'tag': 'form', 'indice': 1},
|
||||
{'tag': 'field', 'indice': 2}]},
|
||||
}
|
||||
self.edit_view(base_view, operations=ops + [remove_binary_op])
|
||||
# The filename field is still present in the view
|
||||
# this is not intentional rather, it is way easier to leave this invisible field there
|
||||
self.assertViewArchEqual(
|
||||
base_view.get_combined_arch(),
|
||||
"""
|
||||
<form>
|
||||
<field name="display_name"/>
|
||||
<field invisible="1" name="x_studio_binary_field_WocAO_filename"/>
|
||||
</form>
|
||||
"""
|
||||
)
|
||||
|
||||
def test_edit_view_options_attribute(self):
|
||||
op = {
|
||||
'type': 'attributes',
|
||||
'target': {
|
||||
'tag': 'field',
|
||||
'attrs': {'name': 'groups_id'},
|
||||
'xpath_info': [
|
||||
{'tag': 'group', 'indice': 1},
|
||||
{'tag': 'group', 'indice': 2},
|
||||
{'tag': 'field', 'indice': 2}
|
||||
],
|
||||
'subview_xpath': "//field[@name='user_ids']/form"
|
||||
},
|
||||
'position': 'attributes',
|
||||
'node': {
|
||||
'tag': 'field',
|
||||
'attrs': {
|
||||
'name': 'groups_id',
|
||||
'widget': 'many2many_tags',
|
||||
'options': "{'color_field': 'color'}",
|
||||
},
|
||||
'children': [],
|
||||
'has_label': True
|
||||
},
|
||||
'new_attrs': {'options': '{"color_field":"color","no_create":true}'}
|
||||
}
|
||||
|
||||
base_view = self.env['ir.ui.view'].create({
|
||||
'name': 'TestForm',
|
||||
'type': 'form',
|
||||
'model': 'res.partner',
|
||||
'arch': """
|
||||
<form>
|
||||
<sheet>
|
||||
<field name="display_name"/>
|
||||
<field name="user_ids">
|
||||
<form>
|
||||
<sheet>
|
||||
<field name="groups_id" widget='many2many_tags' options="{'color_field': 'color'}"/>
|
||||
</sheet>
|
||||
</form>
|
||||
</field>
|
||||
</sheet>
|
||||
</form>"""
|
||||
})
|
||||
self.edit_view(base_view, operations=[op], model='res.users')
|
||||
|
||||
self.assertViewArchEqual(
|
||||
base_view.get_combined_arch(),
|
||||
"""
|
||||
<form>
|
||||
<sheet>
|
||||
<field name="display_name"/>
|
||||
<field name="user_ids">
|
||||
<form>
|
||||
<sheet>
|
||||
<field name="groups_id" widget="many2many_tags" options="{"color_field": "color", "no_create": true}"/>
|
||||
</sheet>
|
||||
</form>
|
||||
</field>
|
||||
</sheet>
|
||||
</form>
|
||||
"""
|
||||
)
|
||||
|
||||
def test_edit_view_add_binary_field_inside_group(self):
|
||||
arch = """<form>
|
||||
<sheet>
|
||||
<notebook>
|
||||
<page>
|
||||
<group>
|
||||
<group name="group_left" />
|
||||
<group name="group_right" />
|
||||
</group>
|
||||
</page>
|
||||
</notebook>
|
||||
</sheet>
|
||||
</form>"""
|
||||
|
||||
base_view = self.env['ir.ui.view'].create({
|
||||
'name': 'TestForm',
|
||||
'type': 'form',
|
||||
'model': 'res.partner',
|
||||
'arch': arch
|
||||
})
|
||||
|
||||
operation = {
|
||||
'type': 'add',
|
||||
'target': {
|
||||
'tag': 'group',
|
||||
'attrs': {
|
||||
'name': 'group_left'
|
||||
},
|
||||
'xpath_info': [
|
||||
{'tag': 'form', 'indice': 1},
|
||||
{'tag': 'sheet', 'indice': 1},
|
||||
{'tag': 'notebook', 'indice': 1},
|
||||
{'tag': 'page', 'indice': 1},
|
||||
{'tag': 'group', 'indice': 1},
|
||||
{'tag': 'group', 'indice': 1}
|
||||
]
|
||||
},
|
||||
'position': 'inside',
|
||||
'node': {
|
||||
'tag': 'field',
|
||||
'attrs': {},
|
||||
'field_description': {
|
||||
'type': 'binary',
|
||||
'field_description': 'New File',
|
||||
'name': 'x_studio_field_fDthx',
|
||||
'model_name': 'res.partner'
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
self.edit_view(base_view, operations=[operation])
|
||||
|
||||
expected_arch = """<form>
|
||||
<sheet>
|
||||
<notebook>
|
||||
<page>
|
||||
<group>
|
||||
<group name="group_left">
|
||||
<field filename="x_studio_field_fDthx_filename" name="x_studio_field_fDthx"/>
|
||||
<field invisible="1" name="x_studio_field_fDthx_filename"/>
|
||||
</group>
|
||||
<group name="group_right"/>
|
||||
</group>
|
||||
</page>
|
||||
</notebook>
|
||||
</sheet>
|
||||
</form>"""
|
||||
|
||||
self.assertViewArchEqual(base_view.get_combined_arch(), expected_arch)
|
||||
1387
web_studio/tests/test_view_normalization.py
Normal file
1387
web_studio/tests/test_view_normalization.py
Normal file
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user