1# -*- coding: utf-8 -*-
2# Part of Odoo. See LICENSE file for full copyright and licensing details.
3
4from odoo import models
5from odoo.addons.base.tests.common import SavepointCaseWithUserDemo
6from odoo.tools import mute_logger
7from odoo.exceptions import AccessError
8
9
10class TestAPI(SavepointCaseWithUserDemo):
11    """ test the new API of the ORM """
12
13    @classmethod
14    def setUpClass(cls):
15        super(TestAPI, cls).setUpClass()
16        cls._load_partners_set()
17
18    def assertIsRecordset(self, value, model):
19        self.assertIsInstance(value, models.BaseModel)
20        self.assertEqual(value._name, model)
21
22    def assertIsRecord(self, value, model):
23        self.assertIsRecordset(value, model)
24        self.assertTrue(len(value) <= 1)
25
26    def assertIsNull(self, value, model):
27        self.assertIsRecordset(value, model)
28        self.assertFalse(value)
29
30    @mute_logger('odoo.models')
31    def test_00_query(self):
32        """ Build a recordset, and check its contents. """
33        domain = [('name', 'ilike', 'j')]
34        partners = self.env['res.partner'].search(domain)
35
36        # partners is a collection of browse records
37        self.assertTrue(partners)
38
39        # partners and its contents are instance of the model
40        self.assertIsRecordset(partners, 'res.partner')
41        for p in partners:
42            self.assertIsRecord(p, 'res.partner')
43
44    @mute_logger('odoo.models')
45    def test_01_query_offset(self):
46        """ Build a recordset with offset, and check equivalence. """
47        partners1 = self.env['res.partner'].search([], offset=10)
48        partners2 = self.env['res.partner'].search([])[10:]
49        self.assertIsRecordset(partners1, 'res.partner')
50        self.assertIsRecordset(partners2, 'res.partner')
51        self.assertEqual(list(partners1), list(partners2))
52
53    @mute_logger('odoo.models')
54    def test_02_query_limit(self):
55        """ Build a recordset with offset, and check equivalence. """
56        partners1 = self.env['res.partner'].search([], order='id asc', limit=10)
57        partners2 = self.env['res.partner'].search([], order='id asc')[:10]
58        self.assertIsRecordset(partners1, 'res.partner')
59        self.assertIsRecordset(partners2, 'res.partner')
60        self.assertEqual(list(partners1), list(partners2))
61
62    @mute_logger('odoo.models')
63    def test_03_query_offset_limit(self):
64        """ Build a recordset with offset and limit, and check equivalence. """
65        partners1 = self.env['res.partner'].search([], order='id asc', offset=3, limit=7)
66        partners2 = self.env['res.partner'].search([], order='id asc')[3:10]
67        self.assertIsRecordset(partners1, 'res.partner')
68        self.assertIsRecordset(partners2, 'res.partner')
69        self.assertEqual(list(partners1), list(partners2))
70
71    @mute_logger('odoo.models')
72    def test_04_query_count(self):
73        """ Test the search method with count=True. """
74        self.cr.execute("SELECT COUNT(*) FROM res_partner WHERE active")
75        count1 = self.cr.fetchone()[0]
76        count2 = self.env['res.partner'].search([], count=True)
77        self.assertIsInstance(count1, int)
78        self.assertIsInstance(count2, int)
79        self.assertEqual(count1, count2)
80
81    @mute_logger('odoo.models')
82    def test_05_immutable(self):
83        """ Check that a recordset remains the same, even after updates. """
84        domain = [('name', 'ilike', 'g')]
85        partners = self.env['res.partner'].search(domain)
86        self.assertTrue(partners)
87        ids = partners.ids
88
89        # modify those partners, and check that partners has not changed
90        partners.write({'active': False})
91        self.assertEqual(ids, partners.ids)
92
93        # redo the search, and check that the result is now empty
94        partners2 = self.env['res.partner'].search(domain)
95        self.assertFalse(partners2)
96
97    @mute_logger('odoo.models')
98    def test_06_fields(self):
99        """ Check that relation fields return records, recordsets or nulls. """
100        user = self.env.user
101        self.assertIsRecord(user, 'res.users')
102        self.assertIsRecord(user.partner_id, 'res.partner')
103        self.assertIsRecordset(user.groups_id, 'res.groups')
104
105        partners = self.env['res.partner'].search([])
106        for name, field in partners._fields.items():
107            if field.type == 'many2one':
108                for p in partners:
109                    self.assertIsRecord(p[name], field.comodel_name)
110            elif field.type == 'reference':
111                for p in partners:
112                    if p[name]:
113                        self.assertIsRecord(p[name], field.comodel_name)
114            elif field.type in ('one2many', 'many2many'):
115                for p in partners:
116                    self.assertIsRecordset(p[name], field.comodel_name)
117
118    @mute_logger('odoo.models')
119    def test_07_null(self):
120        """ Check behavior of null instances. """
121        # select a partner without a parent
122        partner = self.env['res.partner'].search([('parent_id', '=', False)])[0]
123
124        # check partner and related null instances
125        self.assertTrue(partner)
126        self.assertIsRecord(partner, 'res.partner')
127
128        self.assertFalse(partner.parent_id)
129        self.assertIsNull(partner.parent_id, 'res.partner')
130
131        self.assertIs(partner.parent_id.id, False)
132
133        self.assertFalse(partner.parent_id.user_id)
134        self.assertIsNull(partner.parent_id.user_id, 'res.users')
135
136        self.assertIs(partner.parent_id.user_id.name, False)
137
138        self.assertFalse(partner.parent_id.user_id.groups_id)
139        self.assertIsRecordset(partner.parent_id.user_id.groups_id, 'res.groups')
140
141    @mute_logger('odoo.models')
142    def test_40_new_new(self):
143        """ Call new-style methods in the new API style. """
144        partners = self.env['res.partner'].search([('name', 'ilike', 'g')])
145        self.assertTrue(partners)
146
147        # call method write on partners itself, and check its effect
148        partners.write({'active': False})
149        for p in partners:
150            self.assertFalse(p.active)
151
152    @mute_logger('odoo.models')
153    def test_45_new_new(self):
154        """ Call new-style methods on records (new API style). """
155        partners = self.env['res.partner'].search([('name', 'ilike', 'g')])
156        self.assertTrue(partners)
157
158        # call method write on partner records, and check its effects
159        for p in partners:
160            p.write({'active': False})
161        for p in partners:
162            self.assertFalse(p.active)
163
164    @mute_logger('odoo.models')
165    @mute_logger('odoo.addons.base.models.ir_model')
166    def test_50_environment(self):
167        """ Test environment on records. """
168        # partners and reachable records are attached to self.env
169        partners = self.env['res.partner'].search([('name', 'ilike', 'j')])
170        self.assertEqual(partners.env, self.env)
171        for x in (partners, partners[0], partners[0].company_id):
172            self.assertEqual(x.env, self.env)
173        for p in partners:
174            self.assertEqual(p.env, self.env)
175
176        # check that the current user can read and modify company data
177        partners[0].company_id.name
178        partners[0].company_id.write({'name': 'Fools'})
179
180        # create an environment with the demo user
181        demo = self.env['res.users'].search([('login', 'ilike', 'demo')])[0]
182        demo_env = self.env(user=demo)
183        self.assertNotEqual(demo_env, self.env)
184
185        # partners and related records are still attached to self.env
186        self.assertEqual(partners.env, self.env)
187        for x in (partners, partners[0], partners[0].company_id):
188            self.assertEqual(x.env, self.env)
189        for p in partners:
190            self.assertEqual(p.env, self.env)
191
192        # create record instances attached to demo_env
193        demo_partners = partners.with_user(demo)
194        self.assertEqual(demo_partners.env, demo_env)
195        for x in (demo_partners, demo_partners[0], demo_partners[0].company_id):
196            self.assertEqual(x.env, demo_env)
197        for p in demo_partners:
198            self.assertEqual(p.env, demo_env)
199
200        # demo user can read but not modify company data
201        demo_partner = self.env['res.partner'].search([('name', '=', 'Landon Roberts')]).with_user(demo)
202        self.assertTrue(demo_partner.company_id, 'This partner is supposed to be linked to a company')
203        demo_partner.company_id.name
204        with self.assertRaises(AccessError):
205            demo_partner.company_id.write({'name': 'Pricks'})
206
207        # remove demo user from all groups
208        demo.write({'groups_id': [(5,)]})
209
210        # demo user can no longer access partner data
211        with self.assertRaises(AccessError):
212            demo_partner.company_id.name
213
214    @mute_logger('odoo.models')
215    def test_60_cache(self):
216        """ Check the record cache behavior """
217        Partners = self.env['res.partner']
218        pids = []
219        data = {
220            'partner One': ['Partner One - One', 'Partner One - Two'],
221            'Partner Two': ['Partner Two - One'],
222            'Partner Three': ['Partner Three - One'],
223        }
224        for p in data:
225            pids.append(Partners.create({
226                'name': p,
227                'child_ids': [(0, 0, {'name': c}) for c in data[p]],
228            }).id)
229
230        partners = Partners.search([('id', 'in', pids)])
231        partner1, partner2 = partners[0], partners[1]
232        children1, children2 = partner1.child_ids, partner2.child_ids
233        self.assertTrue(children1)
234        self.assertTrue(children2)
235
236        # take a child contact
237        child = children1[0]
238        self.assertEqual(child.parent_id, partner1)
239        self.assertIn(child, partner1.child_ids)
240        self.assertNotIn(child, partner2.child_ids)
241
242        # fetch data in the cache
243        for p in partners:
244            p.name, p.company_id.name, p.user_id.name, p.contact_address
245        self.env.cache.check(self.env)
246
247        # change its parent
248        child.write({'parent_id': partner2.id})
249        self.env.cache.check(self.env)
250
251        # check recordsets
252        self.assertEqual(child.parent_id, partner2)
253        self.assertNotIn(child, partner1.child_ids)
254        self.assertIn(child, partner2.child_ids)
255        self.assertEqual(set(partner1.child_ids + child), set(children1))
256        self.assertEqual(set(partner2.child_ids), set(children2 + child))
257        self.env.cache.check(self.env)
258
259        # delete it
260        child.unlink()
261        self.env.cache.check(self.env)
262
263        # check recordsets
264        self.assertEqual(set(partner1.child_ids), set(children1) - set([child]))
265        self.assertEqual(set(partner2.child_ids), set(children2))
266        self.env.cache.check(self.env)
267
268        # convert from the cache format to the write format
269        partner = partner1
270        partner.country_id, partner.child_ids
271        data = partner._convert_to_write(partner._cache)
272        self.assertEqual(data['country_id'], partner.country_id.id)
273        self.assertEqual(data['child_ids'], [(6, 0, partner.child_ids.ids)])
274
275    @mute_logger('odoo.models')
276    def test_60_prefetch(self):
277        """ Check the record cache prefetching """
278        partners = self.env['res.partner'].search([], limit=models.PREFETCH_MAX)
279        self.assertTrue(len(partners) > 1)
280
281        # all the records in partners are ready for prefetching
282        self.assertItemsEqual(partners.ids, partners._prefetch_ids)
283
284        # reading ONE partner should fetch them ALL
285        for partner in partners:
286            state = partner.state_id
287            break
288        partner_ids_with_field = [partner.id
289                                  for partner in partners
290                                  if 'state_id' in partner._cache]
291        self.assertItemsEqual(partner_ids_with_field, partners.ids)
292
293        # partners' states are ready for prefetching
294        state_ids = {
295            partner._cache['state_id']
296            for partner in partners
297            if partner._cache['state_id'] is not None
298        }
299        self.assertTrue(len(state_ids) > 1)
300        self.assertItemsEqual(state_ids, state._prefetch_ids)
301
302        # reading ONE partner country should fetch ALL partners' countries
303        for partner in partners:
304            if partner.state_id:
305                partner.state_id.name
306                break
307        state_ids_with_field = [st.id for st in partners.state_id if 'name' in st._cache]
308        self.assertItemsEqual(state_ids_with_field, state_ids)
309
310    @mute_logger('odoo.models')
311    def test_60_prefetch_model(self):
312        """ Check the prefetching model. """
313        partners = self.env['res.partner'].search([], limit=models.PREFETCH_MAX)
314        self.assertTrue(partners)
315
316        def same_prefetch(a, b):
317            self.assertEqual(set(a._prefetch_ids), set(b._prefetch_ids))
318
319        def diff_prefetch(a, b):
320            self.assertNotEqual(set(a._prefetch_ids), set(b._prefetch_ids))
321
322        # the recordset operations below use different prefetch sets
323        diff_prefetch(partners, partners.browse())
324        diff_prefetch(partners, partners[0])
325        diff_prefetch(partners, partners[:10])
326
327        # the recordset operations below share the prefetch set
328        same_prefetch(partners, partners.browse(partners.ids))
329        same_prefetch(partners, partners.with_user(self.user_demo))
330        same_prefetch(partners, partners.with_context(active_test=False))
331        same_prefetch(partners, partners[:10].with_prefetch(partners._prefetch_ids))
332
333        # iteration and relational fields should use the same prefetch set
334        self.assertEqual(type(partners).country_id.type, 'many2one')
335        self.assertEqual(type(partners).bank_ids.type, 'one2many')
336        self.assertEqual(type(partners).category_id.type, 'many2many')
337
338        vals0 = {
339            'name': 'Empty relational fields',
340            'country_id': False,
341            'bank_ids': [],
342            'category_id': [],
343        }
344        vals1 = {
345            'name': 'Non-empty relational fields',
346            'country_id': self.ref('base.be'),
347            'bank_ids': [(0, 0, {'acc_number': 'FOO42'})],
348            'category_id': [(4, self.partner_category.id)],
349        }
350        partners = partners.create(vals0) + partners.create(vals1)
351        for partner in partners:
352            same_prefetch(partner, partners)
353            same_prefetch(partner.country_id, partners.country_id)
354            same_prefetch(partner.bank_ids, partners.bank_ids)
355            same_prefetch(partner.category_id, partners.category_id)
356
357    @mute_logger('odoo.models')
358    def test_60_prefetch_read(self):
359        """ Check that reading a field computes it on self only. """
360        Partner = self.env['res.partner']
361        field = type(Partner).company_type
362        self.assertTrue(field.compute and not field.store)
363
364        partner1 = Partner.create({'name': 'Foo'})
365        partner2 = Partner.create({'name': 'Bar', 'parent_id': partner1.id})
366        self.assertEqual(partner1.child_ids, partner2)
367
368        # reading partner1 should not prefetch 'company_type' on partner2
369        self.env.clear()
370        partner1 = partner1.with_prefetch()
371        partner1.read(['company_type'])
372        self.assertIn('company_type', partner1._cache)
373        self.assertNotIn('company_type', partner2._cache)
374
375        # reading partner1 should not prefetch 'company_type' on partner2
376        self.env.clear()
377        partner1 = partner1.with_prefetch()
378        partner1.read(['child_ids', 'company_type'])
379        self.assertIn('company_type', partner1._cache)
380        self.assertNotIn('company_type', partner2._cache)
381
382    @mute_logger('odoo.models')
383    def test_70_one(self):
384        """ Check method one(). """
385        # check with many records
386        ps = self.env['res.partner'].search([('name', 'ilike', 'a')])
387        self.assertTrue(len(ps) > 1)
388        with self.assertRaises(ValueError):
389            ps.ensure_one()
390
391        p1 = ps[0]
392        self.assertEqual(len(p1), 1)
393        self.assertEqual(p1.ensure_one(), p1)
394
395        p0 = self.env['res.partner'].browse()
396        self.assertEqual(len(p0), 0)
397        with self.assertRaises(ValueError):
398            p0.ensure_one()
399
400    @mute_logger('odoo.models')
401    def test_80_contains(self):
402        """ Test membership on recordset. """
403        p1 = self.env['res.partner'].search([('name', 'ilike', 'a')], limit=1).ensure_one()
404        ps = self.env['res.partner'].search([('name', 'ilike', 'a')])
405        self.assertTrue(p1 in ps)
406
407    @mute_logger('odoo.models')
408    def test_80_set_operations(self):
409        """ Check set operations on recordsets. """
410        pa = self.env['res.partner'].search([('name', 'ilike', 'a')])
411        pb = self.env['res.partner'].search([('name', 'ilike', 'b')])
412        self.assertTrue(pa)
413        self.assertTrue(pb)
414        self.assertTrue(set(pa) & set(pb))
415
416        concat = pa + pb
417        self.assertEqual(list(concat), list(pa) + list(pb))
418        self.assertEqual(len(concat), len(pa) + len(pb))
419
420        difference = pa - pb
421        self.assertEqual(len(difference), len(set(difference)))
422        self.assertEqual(set(difference), set(pa) - set(pb))
423        self.assertLessEqual(difference, pa)
424
425        intersection = pa & pb
426        self.assertEqual(len(intersection), len(set(intersection)))
427        self.assertEqual(set(intersection), set(pa) & set(pb))
428        self.assertLessEqual(intersection, pa)
429        self.assertLessEqual(intersection, pb)
430
431        union = pa | pb
432        self.assertEqual(len(union), len(set(union)))
433        self.assertEqual(set(union), set(pa) | set(pb))
434        self.assertGreaterEqual(union, pa)
435        self.assertGreaterEqual(union, pb)
436
437        # one cannot mix different models with set operations
438        ps = pa
439        ms = self.env['ir.ui.menu'].search([])
440        self.assertNotEqual(ps._name, ms._name)
441        self.assertNotEqual(ps, ms)
442
443        with self.assertRaises(TypeError):
444            res = ps + ms
445        with self.assertRaises(TypeError):
446            res = ps - ms
447        with self.assertRaises(TypeError):
448            res = ps & ms
449        with self.assertRaises(TypeError):
450            res = ps | ms
451        with self.assertRaises(TypeError):
452            res = ps < ms
453        with self.assertRaises(TypeError):
454            res = ps <= ms
455        with self.assertRaises(TypeError):
456            res = ps > ms
457        with self.assertRaises(TypeError):
458            res = ps >= ms
459
460    @mute_logger('odoo.models')
461    def test_80_filter(self):
462        """ Check filter on recordsets. """
463        ps = self.env['res.partner'].search([])
464        customers = ps.browse([p.id for p in ps if p.employee])
465
466        # filter on a single field
467        self.assertEqual(ps.filtered(lambda p: p.employee), customers)
468        self.assertEqual(ps.filtered('employee'), customers)
469
470        # filter on a sequence of fields
471        self.assertEqual(
472            ps.filtered(lambda p: p.parent_id.employee),
473            ps.filtered('parent_id.employee')
474        )
475
476    @mute_logger('odoo.models')
477    def test_80_map(self):
478        """ Check map on recordsets. """
479        ps = self.env['res.partner'].search([])
480        parents = ps.browse()
481        for p in ps:
482            parents |= p.parent_id
483
484        # map a single field
485        self.assertEqual(ps.mapped(lambda p: p.parent_id), parents)
486        self.assertEqual(ps.mapped('parent_id'), parents)
487        self.assertEqual(ps.parent_id, parents)
488
489        # map a sequence of fields
490        self.assertEqual(
491            ps.mapped(lambda p: p.parent_id.name),
492            [p.parent_id.name for p in ps]
493        )
494        self.assertEqual(
495            ps.mapped('parent_id.name'),
496            [p.name for p in parents]
497        )
498        self.assertEqual(
499            ps.parent_id.mapped('name'),
500            [p.name for p in parents]
501        )
502
503        # map an empty sequence of fields
504        self.assertEqual(ps.mapped(''), ps)
505
506    @mute_logger('odoo.models')
507    def test_80_sorted(self):
508        """ Check sorted on recordsets. """
509        ps = self.env['res.partner'].search([])
510
511        # sort by model order
512        qs = ps[:len(ps) // 2] + ps[len(ps) // 2:]
513        self.assertEqual(qs.sorted().ids, ps.ids)
514
515        # sort by name, with a function or a field name
516        by_name_ids = [p.id for p in sorted(ps, key=lambda p: p.name)]
517        self.assertEqual(ps.sorted(lambda p: p.name).ids, by_name_ids)
518        self.assertEqual(ps.sorted('name').ids, by_name_ids)
519
520        # sort by inverse name, with a field name
521        by_name_ids = [p.id for p in sorted(ps, key=lambda p: p.name, reverse=True)]
522        self.assertEqual(ps.sorted('name', reverse=True).ids, by_name_ids)
523