1# -*- coding: utf-8 -*-
2# Part of Odoo. See LICENSE file for full copyright and licensing details.
3
4from datetime import date
5from psycopg2 import IntegrityError, ProgrammingError
6
7import odoo
8from odoo.exceptions import UserError, ValidationError, AccessError
9from odoo.tools import mute_logger
10from odoo.tests import common
11
12
13class TestServerActionsBase(common.TransactionCase):
14
15    def setUp(self):
16        super(TestServerActionsBase, self).setUp()
17
18        # Data on which we will run the server action
19        self.test_country = self.env['res.country'].create({
20            'name': 'TestingCountry',
21            'code': 'TY',
22            'address_format': 'SuperFormat',
23        })
24        self.test_partner = self.env['res.partner'].create({
25            'name': 'TestingPartner',
26            'city': 'OrigCity',
27            'country_id': self.test_country.id,
28        })
29        self.context = {
30            'active_model': 'res.partner',
31            'active_id': self.test_partner.id,
32        }
33
34        # Model data
35        Model = self.env['ir.model']
36        Fields = self.env['ir.model.fields']
37        self.res_partner_model = Model.search([('model', '=', 'res.partner')])
38        self.res_partner_name_field = Fields.search([('model', '=', 'res.partner'), ('name', '=', 'name')])
39        self.res_partner_city_field = Fields.search([('model', '=', 'res.partner'), ('name', '=', 'city')])
40        self.res_partner_country_field = Fields.search([('model', '=', 'res.partner'), ('name', '=', 'country_id')])
41        self.res_partner_parent_field = Fields.search([('model', '=', 'res.partner'), ('name', '=', 'parent_id')])
42        self.res_partner_children_field = Fields.search([('model', '=', 'res.partner'), ('name', '=', 'child_ids')])
43        self.res_partner_category_field = Fields.search([('model', '=', 'res.partner'), ('name', '=', 'category_id')])
44        self.res_country_model = Model.search([('model', '=', 'res.country')])
45        self.res_country_name_field = Fields.search([('model', '=', 'res.country'), ('name', '=', 'name')])
46        self.res_country_code_field = Fields.search([('model', '=', 'res.country'), ('name', '=', 'code')])
47        self.res_partner_category_model = Model.search([('model', '=', 'res.partner.category')])
48        self.res_partner_category_name_field = Fields.search([('model', '=', 'res.partner.category'), ('name', '=', 'name')])
49
50        # create server action to
51        self.action = self.env['ir.actions.server'].create({
52            'name': 'TestAction',
53            'model_id': self.res_partner_model.id,
54            'model_name': 'res.partner',
55            'state': 'code',
56            'code': 'record.write({"comment": "MyComment"})',
57        })
58
59
60class TestServerActions(TestServerActionsBase):
61
62    def test_00_action(self):
63        self.action.with_context(self.context).run()
64        self.assertEqual(self.test_partner.comment, 'MyComment', 'ir_actions_server: invalid condition check')
65        self.test_partner.write({'comment': False})
66
67        # Do: create contextual action
68        self.action.create_action()
69        self.assertEqual(self.action.binding_model_id.model, 'res.partner')
70
71        # Do: remove contextual action
72        self.action.unlink_action()
73        self.assertFalse(self.action.binding_model_id)
74
75    def test_10_code(self):
76        self.action.write({
77            'state': 'code',
78            'code': ("partner_name = record.name + '_code'\n"
79                     "record.env['res.partner'].create({'name': partner_name})"),
80        })
81        run_res = self.action.with_context(self.context).run()
82        self.assertFalse(run_res, 'ir_actions_server: code server action correctly finished should return False')
83
84        partners = self.test_partner.search([('name', 'ilike', 'TestingPartner_code')])
85        self.assertEqual(len(partners), 1, 'ir_actions_server: 1 new partner should have been created')
86
87    def test_20_crud_create(self):
88        # Do: create a new record in another model
89        self.action.write({
90            'state': 'object_create',
91            'crud_model_id': self.res_country_model.id,
92            'link_field_id': False,
93            'fields_lines': [(5,),
94                             (0, 0, {'col1': self.res_country_name_field.id, 'value': 'record.name', 'evaluation_type': 'equation'}),
95                             (0, 0, {'col1': self.res_country_code_field.id, 'value': 'record.name[0:2]', 'evaluation_type': 'equation'})],
96        })
97        run_res = self.action.with_context(self.context).run()
98        self.assertFalse(run_res, 'ir_actions_server: create record action correctly finished should return False')
99        # Test: new country created
100        country = self.test_country.search([('name', 'ilike', 'TestingPartner')])
101        self.assertEqual(len(country), 1, 'ir_actions_server: TODO')
102        self.assertEqual(country.code, 'TE', 'ir_actions_server: TODO')
103
104    def test_20_crud_create_link_many2one(self):
105        _city = 'TestCity'
106        _name = 'TestNew'
107
108        # Do: create a new record in the same model and link it with a many2one
109        self.action.write({
110            'state': 'object_create',
111            'crud_model_id': self.action.model_id.id,
112            'link_field_id': self.res_partner_parent_field.id,
113            'fields_lines': [(0, 0, {'col1': self.res_partner_name_field.id, 'value': _name}),
114                             (0, 0, {'col1': self.res_partner_city_field.id, 'value': _city})],
115        })
116        run_res = self.action.with_context(self.context).run()
117        self.assertFalse(run_res, 'ir_actions_server: create record action correctly finished should return False')
118        # Test: new partner created
119        partner = self.test_partner.search([('name', 'ilike', _name)])
120        self.assertEqual(len(partner), 1, 'ir_actions_server: TODO')
121        self.assertEqual(partner.city, _city, 'ir_actions_server: TODO')
122        # Test: new partner linked
123        self.assertEqual(self.test_partner.parent_id, partner, 'ir_actions_server: TODO')
124
125    def test_20_crud_create_link_one2many(self):
126        _name = 'TestNew'
127
128        # Do: create a new record in the same model and link it with a one2many
129        self.action.write({
130            'state': 'object_create',
131            'crud_model_id': self.action.model_id.id,
132            'link_field_id': self.res_partner_children_field.id,
133            'fields_lines': [(0, 0, {'col1': self.res_partner_name_field.id, 'value': _name})],
134        })
135        run_res = self.action.with_context(self.context).run()
136        self.assertFalse(run_res, 'ir_actions_server: create record action correctly finished should return False')
137        # Test: new partner created
138        partner = self.test_partner.search([('name', 'ilike', _name)])
139        self.assertEqual(len(partner), 1, 'ir_actions_server: TODO')
140        self.assertEqual(partner.name, _name, 'ir_actions_server: TODO')
141        # Test: new partner linked
142        self.assertIn(partner, self.test_partner.child_ids, 'ir_actions_server: TODO')
143
144    def test_20_crud_create_link_many2many(self):
145        # Do: create a new record in another model
146        self.action.write({
147            'state': 'object_create',
148            'crud_model_id': self.res_partner_category_model.id,
149            'link_field_id': self.res_partner_category_field.id,
150            'fields_lines': [(0, 0, {'col1': self.res_partner_category_name_field.id, 'value': 'record.name', 'evaluation_type': 'equation'})],
151        })
152        run_res = self.action.with_context(self.context).run()
153        self.assertFalse(run_res, 'ir_actions_server: create record action correctly finished should return False')
154        # Test: new category created
155        category = self.env['res.partner.category'].search([('name', 'ilike', 'TestingPartner')])
156        self.assertEqual(len(category), 1, 'ir_actions_server: TODO')
157        self.assertIn(category, self.test_partner.category_id)
158
159    def test_30_crud_write(self):
160        _name = 'TestNew'
161
162        # Do: update partner name
163        self.action.write({
164            'state': 'object_write',
165            'fields_lines': [(0, 0, {'col1': self.res_partner_name_field.id, 'value': _name})],
166        })
167        run_res = self.action.with_context(self.context).run()
168        self.assertFalse(run_res, 'ir_actions_server: create record action correctly finished should return False')
169        # Test: partner updated
170        partner = self.test_partner.search([('name', 'ilike', _name)])
171        self.assertEqual(len(partner), 1, 'ir_actions_server: TODO')
172        self.assertEqual(partner.city, 'OrigCity', 'ir_actions_server: TODO')
173
174    @mute_logger('odoo.addons.base.models.ir_model', 'odoo.models')
175    def test_40_multi(self):
176        # Data: 2 server actions that will be nested
177        action1 = self.action.create({
178            'name': 'Subaction1',
179            'sequence': 1,
180            'model_id': self.res_partner_model.id,
181            'state': 'code',
182            'code': 'action = {"type": "ir.actions.act_window"}',
183        })
184        action2 = self.action.create({
185            'name': 'Subaction2',
186            'sequence': 2,
187            'model_id': self.res_partner_model.id,
188            'crud_model_id': self.res_partner_model.id,
189            'state': 'object_create',
190            'fields_lines': [(0, 0, {'col1': self.res_partner_name_field.id, 'value': 'RaoulettePoiluchette'}),
191                             (0, 0, {'col1': self.res_partner_city_field.id, 'value': 'TestingCity'})],
192        })
193        action3 = self.action.create({
194            'name': 'Subaction3',
195            'sequence': 3,
196            'model_id': self.res_partner_model.id,
197            'state': 'code',
198            'code': 'action = {"type": "ir.actions.act_url"}',
199        })
200        self.action.write({
201            'state': 'multi',
202            'child_ids': [(6, 0, [action1.id, action2.id, action3.id])],
203        })
204
205        # Do: run the action
206        res = self.action.with_context(self.context).run()
207
208        # Test: new partner created
209        # currently res_partner overrides default['name'] whatever its value
210        partner = self.test_partner.search([('name', 'ilike', 'RaoulettePoiluchette')])
211        self.assertEqual(len(partner), 1)
212        # Test: action returned
213        self.assertEqual(res.get('type'), 'ir.actions.act_url')
214
215        # Test loops
216        with self.assertRaises(ValidationError):
217            self.action.write({
218                'child_ids': [(6, 0, [self.action.id])]
219            })
220
221    def test_50_groups(self):
222        """ check the action is returned only for groups dedicated to user """
223        Actions = self.env['ir.actions.actions']
224
225        group0 = self.env['res.groups'].create({'name': 'country group'})
226
227        self.context = {
228            'active_model': 'res.country',
229            'active_id': self.test_country.id,
230        }
231
232        # Do: update model and group
233        self.action.write({
234            'model_id': self.res_country_model.id,
235            'binding_model_id': self.res_country_model.id,
236            'groups_id': [(4, group0.id, 0)],
237            'code': 'record.write({"vat_label": "VatFromTest"})',
238        })
239
240        # Test: action is not returned
241        bindings = Actions.get_bindings('res.country')
242        self.assertFalse(bindings)
243
244        with self.assertRaises(AccessError):
245            self.action.with_context(self.context).run()
246        self.assertFalse(self.test_country.vat_label)
247
248        # add group to the user, and test again
249        self.env.user.write({'groups_id': [(4, group0.id)]})
250
251        bindings = Actions.get_bindings('res.country')
252        self.assertItemsEqual(bindings.get('action'), self.action.read())
253
254        self.action.with_context(self.context).run()
255        self.assertEqual(self.test_country.vat_label, 'VatFromTest', 'vat label should be changed to VatFromTest')
256
257    def test_60_sort(self):
258        """ check the actions sorted by sequence """
259        Actions = self.env['ir.actions.actions']
260
261        # Do: update model
262        self.action.write({
263            'model_id': self.res_country_model.id,
264            'binding_model_id': self.res_country_model.id,
265        })
266        self.action2 = self.action.copy({'name': 'TestAction2', 'sequence': 1})
267
268        # Test: action returned by sequence
269        bindings = Actions.get_bindings('res.country')
270        self.assertEqual([vals.get('name') for vals in bindings['action']], ['TestAction2', 'TestAction'])
271        self.assertEqual([vals.get('sequence') for vals in bindings['action']], [1, 5])
272
273    def test_70_copy_action(self):
274        # first check that the base case (reset state) works normally
275        r = self.env['ir.actions.todo'].create({
276            'action_id': self.action.id,
277            'state': 'done',
278        })
279        self.assertEqual(r.state, 'done')
280        self.assertEqual(
281            r.copy().state, 'open',
282            "by default state should be reset by copy"
283        )
284
285        # then check that on server action we've changed that
286        self.assertEqual(
287            self.action.copy().state, 'code',
288            "copying a server action should not reset the state"
289        )
290
291    def test_80_permission(self):
292        self.action.write({
293            'state': 'code',
294            'code': """record.write({'date': datetime.date.today()})""",
295        })
296
297        user_demo = self.env.ref("base.user_demo")
298        self_demo = self.action.with_user(user_demo.id)
299
300        # can write on contact partner
301        self.test_partner.type = "contact"
302        self.test_partner.with_user(user_demo.id).check_access_rule("write")
303
304        self_demo.with_context(self.context).run()
305        self.assertEqual(self.test_partner.date, date.today())
306
307        # but can not write on private address
308        self.test_partner.type = "private"
309        with self.assertRaises(AccessError):
310            self.test_partner.with_user(user_demo.id).check_access_rule("write")
311        # nor execute a server action on it
312        with self.assertRaises(AccessError), mute_logger('odoo.addons.base.models.ir_actions'):
313            self_demo.with_context(self.context).run()
314
315
316class TestCustomFields(common.TransactionCase):
317    MODEL = 'res.partner'
318    COMODEL = 'res.users'
319
320    def setUp(self):
321        # check that the registry is properly reset
322        registry = odoo.registry()
323        fnames = set(registry[self.MODEL]._fields)
324        @self.addCleanup
325        def check_registry():
326            assert set(registry[self.MODEL]._fields) == fnames
327
328        super(TestCustomFields, self).setUp()
329
330        # use a test cursor instead of a real cursor
331        self.registry.enter_test_mode(self.cr)
332        self.addCleanup(self.registry.leave_test_mode)
333
334    def create_field(self, name, *, field_type='char'):
335        """ create a custom field and return it """
336        model = self.env['ir.model'].search([('model', '=', self.MODEL)])
337        field = self.env['ir.model.fields'].create({
338            'model_id': model.id,
339            'name': name,
340            'field_description': name,
341            'ttype': field_type,
342        })
343        self.assertIn(name, self.env[self.MODEL]._fields)
344        return field
345
346    def create_view(self, name):
347        """ create a view with the given field name """
348        return self.env['ir.ui.view'].create({
349            'name': 'yet another view',
350            'model': self.MODEL,
351            'arch': '<tree string="X"><field name="%s"/></tree>' % name,
352        })
353
354    def test_create_custom(self):
355        """ custom field names must be start with 'x_' """
356        with self.assertRaises(ValidationError):
357            self.create_field('foo')
358
359    def test_rename_custom(self):
360        """ custom field names must be start with 'x_' """
361        field = self.create_field('x_foo')
362        with self.assertRaises(ValidationError):
363            field.name = 'foo'
364
365    def test_create_valid(self):
366        """ field names must be valid pg identifiers """
367        with self.assertRaises(ValidationError):
368            self.create_field('x_foo bar')
369
370    def test_rename_valid(self):
371        """ field names must be valid pg identifiers """
372        field = self.create_field('x_foo')
373        with self.assertRaises(ValidationError):
374            field.name = 'x_foo bar'
375
376    def test_create_unique(self):
377        """ one cannot create two fields with the same name on a given model """
378        self.create_field('x_foo')
379        with self.assertRaises(IntegrityError), mute_logger('odoo.sql_db'):
380            self.create_field('x_foo')
381
382    def test_rename_unique(self):
383        """ one cannot create two fields with the same name on a given model """
384        field1 = self.create_field('x_foo')
385        field2 = self.create_field('x_bar')
386        with self.assertRaises(IntegrityError), mute_logger('odoo.sql_db'):
387            field2.name = field1.name
388
389    def test_remove_without_view(self):
390        """ try removing a custom field that does not occur in views """
391        field = self.create_field('x_foo')
392        field.unlink()
393
394    def test_rename_without_view(self):
395        """ try renaming a custom field that does not occur in views """
396        field = self.create_field('x_foo')
397        field.name = 'x_bar'
398
399    @mute_logger('odoo.addons.base.models.ir_ui_view')
400    def test_remove_with_view(self):
401        """ try removing a custom field that occurs in a view """
402        field = self.create_field('x_foo')
403        self.create_view('x_foo')
404
405        # try to delete the field, this should fail but not modify the registry
406        with self.assertRaises(UserError):
407            field.unlink()
408        self.assertIn('x_foo', self.env[self.MODEL]._fields)
409
410    @mute_logger('odoo.addons.base.models.ir_ui_view')
411    def test_rename_with_view(self):
412        """ try renaming a custom field that occurs in a view """
413        field = self.create_field('x_foo')
414        self.create_view('x_foo')
415
416        # try to delete the field, this should fail but not modify the registry
417        with self.assertRaises(UserError):
418            field.name = 'x_bar'
419        self.assertIn('x_foo', self.env[self.MODEL]._fields)
420
421    def test_unlink_with_inverse(self):
422        """ create a custom o2m and then delete its m2o inverse """
423        model = self.env['ir.model']._get(self.MODEL)
424        comodel = self.env['ir.model']._get(self.COMODEL)
425
426        m2o_field = self.env['ir.model.fields'].create({
427            'model_id': comodel.id,
428            'name': 'x_my_m2o',
429            'field_description': 'my_m2o',
430            'ttype': 'many2one',
431            'relation': self.MODEL,
432        })
433
434        o2m_field = self.env['ir.model.fields'].create({
435            'model_id': model.id,
436            'name': 'x_my_o2m',
437            'field_description': 'my_o2m',
438            'ttype': 'one2many',
439            'relation': self.COMODEL,
440            'relation_field': m2o_field.name,
441        })
442
443        # normal mode: you cannot break dependencies
444        with self.assertRaises(UserError):
445            m2o_field.unlink()
446
447        # uninstall mode: unlink dependant fields
448        m2o_field.with_context(_force_unlink=True).unlink()
449        self.assertFalse(o2m_field.exists())
450
451    def test_unlink_with_dependant(self):
452        """ create a computed field, then delete its dependency """
453        # Also applies to compute fields
454        comodel = self.env['ir.model'].search([('model', '=', self.COMODEL)])
455
456        field = self.create_field('x_my_char')
457
458        dependant = self.env['ir.model.fields'].create({
459            'model_id': comodel.id,
460            'name': 'x_oh_boy',
461            'field_description': 'x_oh_boy',
462            'ttype': 'char',
463            'related': 'partner_id.x_my_char',
464        })
465
466        # normal mode: you cannot break dependencies
467        with self.assertRaises(UserError):
468            field.unlink()
469
470        # uninstall mode: unlink dependant fields
471        field.with_context(_force_unlink=True).unlink()
472        self.assertFalse(dependant.exists())
473
474    def test_create_binary(self):
475        """ binary custom fields should be created as attachment=True to avoid
476        bloating the DB when creating e.g. image fields via studio
477        """
478        self.create_field('x_image', field_type='binary')
479        custom_binary = self.env[self.MODEL]._fields['x_image']
480
481        self.assertTrue(custom_binary.attachment)
482
483    def test_related_field(self):
484        """ create a custom related field, and check filled values """
485        #
486        # Add a custom field equivalent to the following definition:
487        #
488        # class Partner(models.Model)
489        #     _inherit = 'res.partner'
490        #     x_oh_boy = fields.Char(related="country_id.code", store=True)
491        #
492
493        # pick N=100 records in comodel
494        countries = self.env['res.country'].search([('code', '!=', False)], limit=100)
495        self.assertEqual(len(countries), 100, "Not enough records in comodel 'res.country'")
496
497        # create records in model, with N distinct values for the related field
498        partners = self.env['res.partner'].create([
499            {'name': country.code, 'country_id': country.id} for country in countries
500        ])
501        partners.flush()
502
503        # determine how many queries it takes to create a non-computed field
504        query_count = self.cr.sql_log_count
505        self.env['ir.model.fields'].create({
506            'model_id': self.env['ir.model']._get_id('res.partner'),
507            'name': 'x_oh_box',
508            'field_description': 'x_oh_box',
509            'ttype': 'char',
510        })
511        query_count = self.cr.sql_log_count - query_count
512
513        # create the related field, and assert it only takes 1 extra queries
514        with self.assertQueryCount(query_count + 1):
515            self.env['ir.model.fields'].create({
516                'model_id': self.env['ir.model']._get_id('res.partner'),
517                'name': 'x_oh_boy',
518                'field_description': 'x_oh_boy',
519                'ttype': 'char',
520                'related': 'country_id.code',
521                'store': True,
522            })
523
524        # check the computed values
525        for partner in partners:
526            self.assertEqual(partner.x_oh_boy, partner.country_id.code)
527
528    def test_selection(self):
529        """ custom selection field """
530        Model = self.env[self.MODEL]
531        model = self.env['ir.model'].search([('model', '=', self.MODEL)])
532        field = self.env['ir.model.fields'].create({
533            'model_id': model.id,
534            'name': 'x_sel',
535            'field_description': "Custom Selection",
536            'ttype': 'selection',
537            'selection_ids': [
538                (0, 0, {'value': 'foo', 'name': 'Foo', 'sequence': 0}),
539                (0, 0, {'value': 'bar', 'name': 'Bar', 'sequence': 1}),
540            ],
541        })
542
543        x_sel = Model._fields['x_sel']
544        self.assertEqual(x_sel.type, 'selection')
545        self.assertEqual(x_sel.selection, [('foo', 'Foo'), ('bar', 'Bar')])
546
547        # add selection value 'baz'
548        field.selection_ids.create({
549            'field_id': field.id, 'value': 'baz', 'name': 'Baz', 'sequence': 2,
550        })
551        x_sel = Model._fields['x_sel']
552        self.assertEqual(x_sel.type, 'selection')
553        self.assertEqual(x_sel.selection, [('foo', 'Foo'), ('bar', 'Bar'), ('baz', 'Baz')])
554
555        # assign values to records
556        rec1 = Model.create({'name': 'Rec1', 'x_sel': 'foo'})
557        rec2 = Model.create({'name': 'Rec2', 'x_sel': 'bar'})
558        rec3 = Model.create({'name': 'Rec3', 'x_sel': 'baz'})
559        self.assertEqual(rec1.x_sel, 'foo')
560        self.assertEqual(rec2.x_sel, 'bar')
561        self.assertEqual(rec3.x_sel, 'baz')
562
563        # remove selection value 'foo'
564        field.selection_ids[0].unlink()
565        x_sel = Model._fields['x_sel']
566        self.assertEqual(x_sel.type, 'selection')
567        self.assertEqual(x_sel.selection, [('bar', 'Bar'), ('baz', 'Baz')])
568
569        self.assertEqual(rec1.x_sel, False)
570        self.assertEqual(rec2.x_sel, 'bar')
571        self.assertEqual(rec3.x_sel, 'baz')
572
573        # update selection value 'bar'
574        field.selection_ids[0].value = 'quux'
575        x_sel = Model._fields['x_sel']
576        self.assertEqual(x_sel.type, 'selection')
577        self.assertEqual(x_sel.selection, [('quux', 'Bar'), ('baz', 'Baz')])
578
579        self.assertEqual(rec1.x_sel, False)
580        self.assertEqual(rec2.x_sel, 'quux')
581        self.assertEqual(rec3.x_sel, 'baz')
582