test_flow.py 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463
  1. # # -*- coding: utf-8 -*-
  2. # # Part of Odoo. See LICENSE file for full copyright and licensing details.
  3. from unittest.mock import patch
  4. import sys
  5. from odoo.addons.base.tests.common import TransactionCaseWithUserDemo
  6. from odoo.tests import common, tagged
  7. from odoo.exceptions import AccessError
  8. @tagged('post_install', '-at_install')
  9. class BaseAutomationTest(TransactionCaseWithUserDemo):
  10. def setUp(self):
  11. super(BaseAutomationTest, self).setUp()
  12. self.user_root = self.env.ref('base.user_root')
  13. self.user_admin = self.env.ref('base.user_admin')
  14. self.test_mail_template_automation = self.env['mail.template'].create({
  15. 'name': 'Template Automation',
  16. 'model_id': self.env.ref('test_base_automation.model_base_automation_lead_test').id,
  17. 'body_html': """<div>Email automation</div>""",
  18. })
  19. self.res_partner_1 = self.env['res.partner'].create({'name': 'My Partner'})
  20. self.env['base.automation'].create([
  21. {
  22. 'name': 'Base Automation: test rule on create',
  23. 'model_id': self.env.ref('test_base_automation.model_base_automation_lead_test').id,
  24. 'state': 'code',
  25. 'code': "records.write({'user_id': %s})" % (self.user_demo.id),
  26. 'trigger': 'on_create',
  27. 'active': True,
  28. 'filter_domain': "[('state', '=', 'draft')]",
  29. }, {
  30. 'name': 'Base Automation: test rule on write',
  31. 'model_id': self.env.ref('test_base_automation.model_base_automation_lead_test').id,
  32. 'state': 'code',
  33. 'code': "records.write({'user_id': %s})" % (self.user_demo.id),
  34. 'trigger': 'on_write',
  35. 'active': True,
  36. 'filter_domain': "[('state', '=', 'done')]",
  37. 'filter_pre_domain': "[('state', '=', 'open')]",
  38. }, {
  39. 'name': 'Base Automation: test rule on recompute',
  40. 'model_id': self.env.ref('test_base_automation.model_base_automation_lead_test').id,
  41. 'state': 'code',
  42. 'code': "records.write({'user_id': %s})" % (self.user_demo.id),
  43. 'trigger': 'on_write',
  44. 'active': True,
  45. 'filter_domain': "[('employee', '=', True)]",
  46. }, {
  47. 'name': 'Base Automation: test recursive rule',
  48. 'model_id': self.env.ref('test_base_automation.model_base_automation_lead_test').id,
  49. 'state': 'code',
  50. 'code': """
  51. record = model.browse(env.context['active_id'])
  52. if 'partner_id' in env.context['old_values'][record.id]:
  53. record.write({'state': 'draft'})""",
  54. 'trigger': 'on_write',
  55. 'active': True,
  56. }, {
  57. 'name': 'Base Automation: test rule on secondary model',
  58. 'model_id': self.env.ref('test_base_automation.model_base_automation_line_test').id,
  59. 'state': 'code',
  60. 'code': "records.write({'user_id': %s})" % (self.user_demo.id),
  61. 'trigger': 'on_create',
  62. 'active': True,
  63. }, {
  64. 'name': 'Base Automation: test rule on write check context',
  65. 'model_id': self.env.ref('test_base_automation.model_base_automation_lead_test').id,
  66. 'state': 'code',
  67. 'code': """
  68. record = model.browse(env.context['active_id'])
  69. if 'user_id' in env.context['old_values'][record.id]:
  70. record.write({'is_assigned_to_admin': (record.user_id.id == 1)})""",
  71. 'trigger': 'on_write',
  72. 'active': True,
  73. }, {
  74. 'name': 'Base Automation: test rule with trigger',
  75. 'model_id': self.env.ref('test_base_automation.model_base_automation_lead_test').id,
  76. 'trigger_field_ids': [(4, self.env.ref('test_base_automation.field_base_automation_lead_test__state').id)],
  77. 'state': 'code',
  78. 'code': """
  79. record = model.browse(env.context['active_id'])
  80. record['name'] = record.name + 'X'""",
  81. 'trigger': 'on_write',
  82. 'active': True,
  83. }, {
  84. 'name': 'Base Automation: test send an email',
  85. 'mail_post_method': 'email',
  86. 'model_id': self.env.ref('test_base_automation.model_base_automation_lead_test').id,
  87. 'template_id': self.test_mail_template_automation.id,
  88. 'trigger_field_ids': [(4, self.env.ref('test_base_automation.field_base_automation_lead_test__deadline').id)],
  89. 'state': 'mail_post',
  90. 'code': """
  91. record = model.browse(env.context['active_id'])
  92. record['name'] = record.name + 'X'""",
  93. 'trigger': 'on_write',
  94. 'active': True,
  95. 'filter_domain': "[('deadline', '!=', False)]",
  96. 'filter_pre_domain': "[('deadline', '=', False)]",
  97. }
  98. ])
  99. def tearDown(self):
  100. super().tearDown()
  101. self.env['base.automation']._unregister_hook()
  102. def create_lead(self, **kwargs):
  103. vals = {
  104. 'name': "Lead Test",
  105. 'user_id': self.user_root.id,
  106. }
  107. vals.update(kwargs)
  108. return self.env['base.automation.lead.test'].create(vals)
  109. def test_00_check_to_state_open_pre(self):
  110. """
  111. Check that a new record (with state = open) doesn't change its responsible
  112. when there is a precondition filter which check that the state is open.
  113. """
  114. lead = self.create_lead(state='open')
  115. self.assertEqual(lead.state, 'open')
  116. self.assertEqual(lead.user_id, self.user_root, "Responsible should not change on creation of Lead with state 'open'.")
  117. def test_01_check_to_state_draft_post(self):
  118. """
  119. Check that a new record changes its responsible when there is a postcondition
  120. filter which check that the state is draft.
  121. """
  122. lead = self.create_lead()
  123. self.assertEqual(lead.state, 'draft', "Lead state should be 'draft'")
  124. self.assertEqual(lead.user_id, self.user_demo, "Responsible should be change on creation of Lead with state 'draft'.")
  125. def test_02_check_from_draft_to_done_with_steps(self):
  126. """
  127. A new record is created and goes from states 'open' to 'done' via the
  128. other states (open, pending and cancel). We have a rule with:
  129. - precondition: the record is in "open"
  130. - postcondition: that the record is "done".
  131. If the state goes from 'open' to 'done' the responsible is changed.
  132. If those two conditions aren't verified, the responsible remains the same.
  133. """
  134. lead = self.create_lead(state='open')
  135. self.assertEqual(lead.state, 'open', "Lead state should be 'open'")
  136. self.assertEqual(lead.user_id, self.user_root, "Responsible should not change on creation of Lead with state 'open'.")
  137. # change state to pending and check that responsible has not changed
  138. lead.write({'state': 'pending'})
  139. self.assertEqual(lead.state, 'pending', "Lead state should be 'pending'")
  140. self.assertEqual(lead.user_id, self.user_root, "Responsible should not change on creation of Lead with state from 'draft' to 'open'.")
  141. # change state to done and check that responsible has not changed
  142. lead.write({'state': 'done'})
  143. self.assertEqual(lead.state, 'done', "Lead state should be 'done'")
  144. self.assertEqual(lead.user_id, self.user_root, "Responsible should not chang on creation of Lead with state from 'pending' to 'done'.")
  145. def test_03_check_from_draft_to_done_without_steps(self):
  146. """
  147. A new record is created and goes from states 'open' to 'done' via the
  148. other states (open, pending and cancel). We have a rule with:
  149. - precondition: the record is in "open"
  150. - postcondition: that the record is "done".
  151. If the state goes from 'open' to 'done' the responsible is changed.
  152. If those two conditions aren't verified, the responsible remains the same.
  153. """
  154. lead = self.create_lead(state='open')
  155. self.assertEqual(lead.state, 'open', "Lead state should be 'open'")
  156. self.assertEqual(lead.user_id, self.user_root, "Responsible should not change on creation of Lead with state 'open'.")
  157. # change state to done and check that responsible has changed
  158. lead.write({'state': 'done'})
  159. self.assertEqual(lead.state, 'done', "Lead state should be 'done'")
  160. self.assertEqual(lead.user_id, self.user_demo, "Responsible should be change on write of Lead with state from 'open' to 'done'.")
  161. def test_10_recomputed_field(self):
  162. """
  163. Check that a rule is executed whenever a field is recomputed after a
  164. change on another model.
  165. """
  166. partner = self.res_partner_1
  167. partner.write({'employee': False})
  168. lead = self.create_lead(state='open', partner_id=partner.id)
  169. self.assertFalse(lead.employee, "Customer field should updated to False")
  170. self.assertEqual(lead.user_id, self.user_root, "Responsible should not change on creation of Lead with state from 'draft' to 'open'.")
  171. # change partner, recompute on lead should trigger the rule
  172. partner.write({'employee': True})
  173. self.env.flush_all()
  174. self.assertTrue(lead.employee, "Customer field should updated to True")
  175. self.assertEqual(lead.user_id, self.user_demo, "Responsible should be change on write of Lead when Customer becomes True.")
  176. def test_11_recomputed_field(self):
  177. """
  178. Check that a rule is executed whenever a field is recomputed and the
  179. context contains the target field
  180. """
  181. partner = self.res_partner_1
  182. lead = self.create_lead(state='draft', partner_id=partner.id)
  183. self.assertFalse(lead.deadline, 'There should not be a deadline defined')
  184. # change priority and user; this triggers deadline recomputation, and
  185. # the server action should set the boolean field to True
  186. lead.write({'priority': True, 'user_id': self.user_root.id})
  187. self.assertTrue(lead.deadline, 'Deadline should be defined')
  188. self.assertTrue(lead.is_assigned_to_admin, 'Lead should be assigned to admin')
  189. def test_11b_recomputed_field(self):
  190. mail_automation = self.env['base.automation'].search([('name', '=', 'Base Automation: test send an email')])
  191. send_mail_count = 0
  192. def _patched_get_actions(*args, **kwargs):
  193. obj = args[0]
  194. if '__action_done' not in obj._context:
  195. obj = obj.with_context(__action_done={})
  196. return mail_automation.with_env(obj.env)
  197. def _patched_send_mail(*args, **kwargs):
  198. nonlocal send_mail_count
  199. send_mail_count += 1
  200. patchers = [
  201. patch('odoo.addons.base_automation.models.base_automation.BaseAutomation._get_actions', _patched_get_actions),
  202. patch('odoo.addons.mail.models.mail_template.MailTemplate.send_mail', _patched_send_mail),
  203. ]
  204. self.startPatcher(patchers[0])
  205. lead = self.create_lead()
  206. self.assertFalse(lead.priority)
  207. self.assertFalse(lead.deadline)
  208. self.startPatcher(patchers[1])
  209. lead.write({'priority': True})
  210. self.assertTrue(lead.priority)
  211. self.assertTrue(lead.deadline)
  212. self.assertEqual(send_mail_count, 1)
  213. def test_12_recursive(self):
  214. """ Check that a rule is executed recursively by a secondary change. """
  215. lead = self.create_lead(state='open')
  216. self.assertEqual(lead.state, 'open')
  217. self.assertEqual(lead.user_id, self.user_root)
  218. # change partner; this should trigger the rule that modifies the state
  219. partner = self.res_partner_1
  220. lead.write({'partner_id': partner.id})
  221. self.assertEqual(lead.state, 'draft')
  222. def test_20_direct_line(self):
  223. """
  224. Check that a rule is executed after creating a line record.
  225. """
  226. line = self.env['base.automation.line.test'].create({'name': "Line"})
  227. self.assertEqual(line.user_id, self.user_demo)
  228. def test_20_indirect_line(self):
  229. """
  230. Check that creating a lead with a line executes rules on both records.
  231. """
  232. lead = self.create_lead(line_ids=[(0, 0, {'name': "Line"})])
  233. self.assertEqual(lead.state, 'draft', "Lead state should be 'draft'")
  234. self.assertEqual(lead.user_id, self.user_demo, "Responsible should change on creation of Lead test line.")
  235. self.assertEqual(len(lead.line_ids), 1, "New test line is not created")
  236. self.assertEqual(lead.line_ids.user_id, self.user_demo, "Responsible should be change on creation of Lead test line.")
  237. def test_21_trigger_fields(self):
  238. """
  239. Check that the rule with trigger is executed only once per pertinent update.
  240. """
  241. lead = self.create_lead(name="X")
  242. lead.priority = True
  243. partner1 = self.res_partner_1
  244. lead.partner_id = partner1.id
  245. self.assertEqual(lead.name, 'X', "No update until now.")
  246. lead.state = 'open'
  247. self.assertEqual(lead.name, 'XX', "One update should have happened.")
  248. lead.state = 'done'
  249. self.assertEqual(lead.name, 'XXX', "One update should have happened.")
  250. lead.state = 'done'
  251. self.assertEqual(lead.name, 'XXX', "No update should have happened.")
  252. lead.state = 'cancel'
  253. self.assertEqual(lead.name, 'XXXX', "One update should have happened.")
  254. # change the rule to trigger on partner_id
  255. rule = self.env['base.automation'].search([('name', '=', 'Base Automation: test rule with trigger')])
  256. rule.write({'trigger_field_ids': [(6, 0, [self.env.ref('test_base_automation.field_base_automation_lead_test__partner_id').id])]})
  257. partner2 = self.env['res.partner'].create({'name': 'A new partner'})
  258. lead.name = 'X'
  259. lead.state = 'open'
  260. self.assertEqual(lead.name, 'X', "No update should have happened.")
  261. lead.partner_id = partner2
  262. self.assertEqual(lead.name, 'XX', "One update should have happened.")
  263. lead.partner_id = partner2
  264. self.assertEqual(lead.name, 'XX', "No update should have happened.")
  265. lead.partner_id = partner1
  266. self.assertEqual(lead.name, 'XXX', "One update should have happened.")
  267. def test_30_modelwithoutaccess(self):
  268. """
  269. Ensure a domain on a M2O without user access doesn't fail.
  270. We create a base automation with a filter on a model the user haven't access to
  271. - create a group
  272. - restrict acl to this group and set only admin in it
  273. - create base.automation with a filter
  274. - create a record in the restricted model in admin
  275. - create a record in the non restricted model in demo
  276. """
  277. Model = self.env['base.automation.link.test']
  278. Comodel = self.env['base.automation.linked.test']
  279. access = self.env.ref("test_base_automation.access_base_automation_linked_test")
  280. access.group_id = self.env['res.groups'].create({
  281. 'name': "Access to base.automation.linked.test",
  282. "users": [(6, 0, [self.user_admin.id,])]
  283. })
  284. # sanity check: user demo has no access to the comodel of 'linked_id'
  285. with self.assertRaises(AccessError):
  286. Comodel.with_user(self.user_demo).check_access_rights('read')
  287. # check base automation with filter that performs Comodel.search()
  288. self.env['base.automation'].create({
  289. 'name': 'test no access',
  290. 'model_id': self.env['ir.model']._get_id("base.automation.link.test"),
  291. 'trigger': 'on_create_or_write',
  292. 'filter_pre_domain': "[('linked_id.another_field', '=', 'something')]",
  293. 'state': 'code',
  294. 'active': True,
  295. 'code': "action = [rec.name for rec in records]"
  296. })
  297. Comodel.create([
  298. {'name': 'a first record', 'another_field': 'something'},
  299. {'name': 'another record', 'another_field': 'something different'},
  300. ])
  301. rec1 = Model.create({'name': 'a record'})
  302. rec1.write({'name': 'a first record'})
  303. rec2 = Model.with_user(self.user_demo).create({'name': 'another record'})
  304. rec2.write({'name': 'another value'})
  305. # check base automation with filter that performs Comodel.name_search()
  306. self.env['base.automation'].create({
  307. 'name': 'test no name access',
  308. 'model_id': self.env['ir.model']._get_id("base.automation.link.test"),
  309. 'trigger': 'on_create_or_write',
  310. 'filter_pre_domain': "[('linked_id', '=', 'whatever')]",
  311. 'state': 'code',
  312. 'active': True,
  313. 'code': "action = [rec.name for rec in records]"
  314. })
  315. rec3 = Model.create({'name': 'a random record'})
  316. rec3.write({'name': 'a first record'})
  317. rec4 = Model.with_user(self.user_demo).create({'name': 'again another record'})
  318. rec4.write({'name': 'another value'})
  319. @common.tagged('post_install', '-at_install')
  320. class TestCompute(common.TransactionCase):
  321. def test_inversion(self):
  322. """ If a stored field B depends on A, an update to the trigger for A
  323. should trigger the recomputaton of A, then B.
  324. However if a search() is performed during the computation of A
  325. ??? and _order is affected ??? a flush will be triggered, forcing the
  326. computation of B, based on the previous A.
  327. This happens if a rule has has a non-empty filter_pre_domain, even if
  328. it's an empty list (``'[]'`` as opposed to ``False``).
  329. """
  330. company1 = self.env['res.partner'].create({
  331. 'name': "Gorofy",
  332. 'is_company': True,
  333. })
  334. company2 = self.env['res.partner'].create({
  335. 'name': "Awiclo",
  336. 'is_company': True
  337. })
  338. r = self.env['res.partner'].create({
  339. 'name': 'Bob',
  340. 'is_company': False,
  341. 'parent_id': company1.id
  342. })
  343. self.assertEqual(r.display_name, 'Gorofy, Bob')
  344. r.parent_id = company2
  345. self.assertEqual(r.display_name, 'Awiclo, Bob')
  346. self.env['base.automation'].create({
  347. 'name': "test rule",
  348. 'filter_pre_domain': False,
  349. 'trigger': 'on_create_or_write',
  350. 'state': 'code', # no-op action
  351. 'model_id': self.env.ref('base.model_res_partner').id,
  352. })
  353. r.parent_id = company1
  354. self.assertEqual(r.display_name, 'Gorofy, Bob')
  355. self.env['base.automation'].create({
  356. 'name': "test rule",
  357. 'filter_pre_domain': '[]',
  358. 'trigger': 'on_create_or_write',
  359. 'state': 'code', # no-op action
  360. 'model_id': self.env.ref('base.model_res_partner').id,
  361. })
  362. r.parent_id = company2
  363. self.assertEqual(r.display_name, 'Awiclo, Bob')
  364. def test_recursion(self):
  365. project = self.env['test_base_automation.project'].create({})
  366. # this action is executed every time a task is assigned to project
  367. self.env['base.automation'].create({
  368. 'name': 'dummy',
  369. 'model_id': self.env['ir.model']._get_id('test_base_automation.task'),
  370. 'state': 'code',
  371. 'trigger': 'on_create_or_write',
  372. 'filter_domain': repr([('project_id', '=', project.id)]),
  373. })
  374. # create one task in project with 10 subtasks; all the subtasks are
  375. # automatically assigned to project, too
  376. task = self.env['test_base_automation.task'].create({'project_id': project.id})
  377. subtasks = task.create([{'parent_id': task.id} for _ in range(10)])
  378. subtasks.flush_model()
  379. # This test checks what happens when a stored recursive computed field
  380. # is marked to compute on many records, and automated actions are
  381. # triggered depending on that field. In this case, we trigger the
  382. # recomputation of 'project_id' on 'subtasks' by deleting their parent
  383. # task.
  384. #
  385. # An issue occurs when the domain of automated actions is evaluated by
  386. # method search(), because the latter flushes the fields to search on,
  387. # which are also the ones being recomputed. Combined with the fact
  388. # that recursive fields are not computed in batch, this leads to a huge
  389. # amount of recursive calls between the automated action and flush().
  390. #
  391. # The execution of task.unlink() looks like this:
  392. # - mark 'project_id' to compute on subtasks
  393. # - delete task
  394. # - flush()
  395. # - recompute 'project_id' on subtask1
  396. # - call compute on subtask1
  397. # - in action, search([('id', 'in', subtask1.ids), ('project_id', '=', pid)])
  398. # - flush(['id', 'project_id'])
  399. # - recompute 'project_id' on subtask2
  400. # - call compute on subtask2
  401. # - in action search([('id', 'in', subtask2.ids), ('project_id', '=', pid)])
  402. # - flush(['id', 'project_id'])
  403. # - recompute 'project_id' on subtask3
  404. # - call compute on subtask3
  405. # - in action, search([('id', 'in', subtask3.ids), ('project_id', '=', pid)])
  406. # - flush(['id', 'project_id'])
  407. # - recompute 'project_id' on subtask4
  408. # ...
  409. limit = sys.getrecursionlimit()
  410. try:
  411. sys.setrecursionlimit(100)
  412. task.unlink()
  413. finally:
  414. sys.setrecursionlimit(limit)