合并企业版代码(未测试,先提交到测试分支)

This commit is contained in:
qihao.gong@jikimo.com
2023-04-14 17:42:23 +08:00
parent 7a7b3d7126
commit d28525526a
1300 changed files with 513579 additions and 5426 deletions

View 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

View 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")

View 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)

View 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()

View 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")

View 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="{&quot;color_field&quot;: &quot;color&quot;, &quot;no_create&quot;: 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)

File diff suppressed because it is too large Load Diff