test_chart_template.py 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447
  1. import logging
  2. from odoo import Command
  3. from odoo.addons.account.models.chart_template import update_taxes_from_templates
  4. from odoo.exceptions import ValidationError
  5. from odoo.tests import tagged
  6. from odoo.tests.common import TransactionCase
  7. @tagged('post_install', '-at_install')
  8. class TestChartTemplate(TransactionCase):
  9. @classmethod
  10. def setUpClass(cls):
  11. """ Set up a company with the generic chart template, containing two taxes and a fiscal position.
  12. We need to add xml_ids to the templates because they are loaded from their xml_ids
  13. """
  14. super().setUpClass()
  15. us_country_id = cls.env.ref('base.us').id
  16. cls.company = cls.env['res.company'].create({
  17. 'name': 'TestCompany1',
  18. 'country_id': us_country_id,
  19. 'account_fiscal_country_id': us_country_id,
  20. })
  21. cls.chart_template_xmlid = 'l10n_test.test_chart_template_xmlid'
  22. cls.chart_template = cls.env['account.chart.template']._load_records([{
  23. 'xml_id': cls.chart_template_xmlid,
  24. 'values': {
  25. 'name': 'Test Chart Template US',
  26. 'currency_id': cls.env.ref('base.USD').id,
  27. 'bank_account_code_prefix': 1000,
  28. 'cash_account_code_prefix': 2000,
  29. 'transfer_account_code_prefix': 3000,
  30. 'country_id': us_country_id,
  31. }
  32. }])
  33. account_templates = cls.env['account.account.template']._load_records([{
  34. 'xml_id': 'account.test_account_income_template',
  35. 'values':
  36. {
  37. 'name': 'property_income_account',
  38. 'code': '222221',
  39. 'account_type': 'income',
  40. 'chart_template_id': cls.chart_template.id,
  41. }
  42. }, {
  43. 'xml_id': 'account.test_account_expense_template',
  44. 'values':
  45. {
  46. 'name': 'property_expense_account',
  47. 'code': '222222',
  48. 'account_type': 'expense',
  49. 'chart_template_id': cls.chart_template.id,
  50. }
  51. }])
  52. cls.chart_template.property_account_income_categ_id = account_templates[0].id
  53. cls.chart_template.property_account_expense_categ_id = account_templates[1].id
  54. cls.fiscal_position_template = cls._create_fiscal_position_template('account.test_fiscal_position_template',
  55. 'US fiscal position test', us_country_id)
  56. cls.tax_template_1 = cls._create_tax_template('account.test_tax_template_1', 'Tax name 1', 1, tag_name='tag_name_1')
  57. cls.tax_template_2 = cls._create_tax_template('account.test_tax_template_2', 'Tax name 2', 2, tag_name='tag_name_2')
  58. cls.fiscal_position_tax_template_1 = cls._create_fiscal_position_tax_template(
  59. cls.fiscal_position_template, 'account.test_fp_tax_template_1', cls.tax_template_1, cls.tax_template_2
  60. )
  61. cls.chart_template.try_loading(company=cls.company, install_demo=False)
  62. cls.fiscal_position = cls.env['account.fiscal.position'].search([
  63. ('company_id', '=', cls.company.id),
  64. ('name', '=', cls.fiscal_position_template.name),
  65. ])
  66. @classmethod
  67. def create_tax_template(cls, name, template_name, amount):
  68. # TODO to remove in master
  69. logging.warning("Deprecated method, please use _create_tax_template() instead")
  70. return cls._create_tax_template(template_name, name, amount, tag_name=None)
  71. @classmethod
  72. def _create_group_tax_template(cls, tax_template_xmlid, name, chart_template_id=None, active=True):
  73. children_1 = cls._create_tax_template(f'{tax_template_xmlid}_children1', f'{name}_children_1', 10, active=active)
  74. children_2 = cls._create_tax_template(f'{tax_template_xmlid}_children2', f'{name}_children_2', 15, active=active)
  75. return cls.env['account.tax.template']._load_records([{
  76. 'xml_id': tax_template_xmlid,
  77. 'values': {
  78. 'name': name,
  79. 'amount_type': 'group',
  80. 'type_tax_use': 'none',
  81. 'active': active,
  82. 'chart_template_id': chart_template_id if chart_template_id else cls.chart_template.id,
  83. 'children_tax_ids': [Command.set((children_1 + children_2).ids)],
  84. },
  85. }])
  86. @classmethod
  87. def _create_tax_template(cls, tax_template_xmlid, name, amount, tag_name=None, chart_template_id=None, account_data=None, active=True):
  88. if tag_name:
  89. tag = cls.env['account.account.tag'].create({
  90. 'name': tag_name,
  91. 'applicability': 'taxes',
  92. 'country_id': cls.company.account_fiscal_country_id.id,
  93. })
  94. if account_data:
  95. account_vals = {
  96. 'name': account_data['name'],
  97. 'code': account_data['code'],
  98. 'account_type': 'liability_current',
  99. }
  100. # We have to instantiate both the template and the record since we suppose accounts are already created.
  101. account_template = cls.env['account.account.template'].create(account_vals)
  102. account_vals.update({'company_id': cls.company.id})
  103. cls.env['account.account'].create(account_vals)
  104. return cls.env['account.tax.template']._load_records([{
  105. 'xml_id': tax_template_xmlid,
  106. 'values': {
  107. 'name': name,
  108. 'amount': amount,
  109. 'type_tax_use': 'none',
  110. 'active': active,
  111. 'chart_template_id': chart_template_id if chart_template_id else cls.chart_template.id,
  112. 'invoice_repartition_line_ids': [
  113. Command.create({
  114. 'factor_percent': 100,
  115. 'repartition_type': 'base',
  116. 'tag_ids': [(6, 0, tag.ids)] if tag_name else None,
  117. }),
  118. Command.create({
  119. 'factor_percent': 100,
  120. 'account_id': account_template.id if account_data else None,
  121. 'repartition_type': 'tax',
  122. }),
  123. ],
  124. 'refund_repartition_line_ids': [
  125. Command.create({
  126. 'factor_percent': 100,
  127. 'repartition_type': 'base',
  128. 'tag_ids': [(6, 0, tag.ids)] if tag_name else None,
  129. }),
  130. Command.create({
  131. 'factor_percent': 100,
  132. 'account_id': account_template.id if account_data else None,
  133. 'repartition_type': 'tax',
  134. }),
  135. ],
  136. },
  137. }])
  138. @classmethod
  139. def _create_fiscal_position_template(cls, fp_template_xmlid, fp_template_name, country_id):
  140. return cls.env['account.fiscal.position.template']._load_records([{
  141. 'xml_id': fp_template_xmlid,
  142. 'values': {
  143. 'name': fp_template_name,
  144. 'chart_template_id': cls.chart_template.id,
  145. 'country_id': country_id,
  146. 'auto_apply': True,
  147. },
  148. }])
  149. @classmethod
  150. def _create_fiscal_position_tax_template(cls, fiscal_position_template, fiscal_position_tax_template_xmlid, tax_template_src, tax_template_dest):
  151. return cls.env['account.fiscal.position.tax.template']._load_records([{
  152. 'xml_id': fiscal_position_tax_template_xmlid,
  153. 'values': {
  154. 'tax_src_id': tax_template_src.id,
  155. 'tax_dest_id': tax_template_dest.id,
  156. 'position_id': fiscal_position_template.id,
  157. },
  158. }])
  159. def test_update_taxes_new_template(self):
  160. """ Tests that adding a new tax template and a fiscal position tax template
  161. creates this new tax and fiscal position line when updating
  162. """
  163. tax_template_3 = self._create_tax_template('account.test_tax_3_template', 'Tax name 3', 3, tag_name='tag_name_3')
  164. tax_template_4 = self._create_tax_template('account.test_tax_4_template', 'Tax name 4', 4, account_data={'name': 'account_name_4', 'code': 'TACT'})
  165. self._create_fiscal_position_tax_template(self.fiscal_position_template, 'account.test_fiscal_position_tax_template', tax_template_3, tax_template_4)
  166. update_taxes_from_templates(self.env.cr, self.chart_template_xmlid)
  167. taxes = self.env['account.tax'].search([
  168. ('company_id', '=', self.company.id),
  169. ('name', 'in', [tax_template_3.name, tax_template_4.name]),
  170. ])
  171. self.assertRecordValues(taxes, [
  172. {'name': 'Tax name 3', 'amount': 3},
  173. {'name': 'Tax name 4', 'amount': 4},
  174. ])
  175. self.assertEqual(taxes.invoice_repartition_line_ids.tag_ids.name, 'tag_name_3')
  176. self.assertEqual(taxes.invoice_repartition_line_ids.account_id.name, 'account_name_4')
  177. self.assertRecordValues(self.fiscal_position.tax_ids.tax_src_id, [
  178. {'name': 'Tax name 1'},
  179. {'name': 'Tax name 3'},
  180. ])
  181. self.assertRecordValues(self.fiscal_position.tax_ids.tax_dest_id, [
  182. {'name': 'Tax name 2'},
  183. {'name': 'Tax name 4'},
  184. ])
  185. def test_update_taxes_existing_template_update(self):
  186. """ When a template is close enough from the corresponding existing tax we want to update
  187. that tax with the template values.
  188. """
  189. self.tax_template_1.invoice_repartition_line_ids.tag_ids.name += " [DUP]"
  190. update_taxes_from_templates(self.env.cr, self.chart_template_xmlid)
  191. tax = self.env['account.tax'].search([
  192. ('company_id', '=', self.company.id),
  193. ('name', '=', self.tax_template_1.name),
  194. ])
  195. # Check that tax was not recreated
  196. self.assertEqual(len(tax), 1)
  197. # Check that tags have been updated
  198. self.assertEqual(tax.invoice_repartition_line_ids.tag_ids.name, self.tax_template_1.invoice_repartition_line_ids.tag_ids.name)
  199. def test_update_taxes_existing_template_recreation(self):
  200. """ When a template is too different from the corresponding existing tax we want to recreate
  201. a new taxes from template.
  202. """
  203. # We increment the amount so the template gets slightly different from the
  204. # corresponding tax and triggers recreation
  205. old_tax_name = self.tax_template_1.name
  206. old_tax_amount = self.tax_template_1.amount
  207. self.tax_template_1.name = "Tax name 1 modified"
  208. self.tax_template_1.amount += 1
  209. update_taxes_from_templates(self.env.cr, self.chart_template_xmlid)
  210. # Check that old tax has not been changed
  211. old_tax = self.env['account.tax'].search([
  212. ('company_id', '=', self.company.id),
  213. ('name', '=', old_tax_name),
  214. ], limit=1)
  215. self.assertEqual(old_tax[0].amount, old_tax_amount)
  216. # Check that new tax has been recreated
  217. tax = self.env['account.tax'].search([
  218. ('company_id', '=', self.company.id),
  219. ('name', '=', self.tax_template_1.name),
  220. ], limit=1)
  221. self.assertEqual(tax[0].amount, self.tax_template_1.amount)
  222. def test_update_taxes_remove_fiscal_position_from_tax(self):
  223. """ Tests that when we remove the tax from the fiscal position mapping it is not
  224. recreated after update of taxes.
  225. """
  226. self.fiscal_position.tax_ids.unlink()
  227. update_taxes_from_templates(self.env.cr, self.chart_template_xmlid)
  228. self.assertEqual(len(self.fiscal_position.tax_ids), 0)
  229. def test_update_taxes_conflict_name(self):
  230. """ When recreating a tax during update a conflict name can occur since
  231. we need to respect unique constraint on (name, company_id, type_tax_use, tax_scope).
  232. To do so, the old tax needs to be prefixed with '[old] '.
  233. """
  234. # We increment the amount so the template gets slightly different from the
  235. # corresponding tax and triggers recreation
  236. old_amount = self.tax_template_1.amount
  237. self.tax_template_1.amount += 1
  238. update_taxes_from_templates(self.env.cr, self.chart_template_xmlid)
  239. taxes_from_template_1 = self.env['account.tax'].search([
  240. ('company_id', '=', self.company.id),
  241. ('name', 'like', f"%{self.tax_template_1.name}"),
  242. ])
  243. self.assertRecordValues(taxes_from_template_1, [
  244. {'name': f"[old] {self.tax_template_1.name}", 'amount': old_amount},
  245. {'name': f"{self.tax_template_1.name}", 'amount': self.tax_template_1.amount},
  246. ])
  247. def test_update_taxes_multi_company(self):
  248. """ In a multi-company environment all companies should be correctly updated."""
  249. company_2 = self.env['res.company'].create({
  250. 'name': 'TestCompany2',
  251. 'country_id': self.env.ref('base.us').id,
  252. 'account_fiscal_country_id': self.env.ref('base.us').id,
  253. })
  254. self.chart_template.try_loading(company=company_2, install_demo=False)
  255. # triggers recreation of taxes related to template 1
  256. self.tax_template_1.amount += 1
  257. update_taxes_from_templates(self.env.cr, self.chart_template_xmlid)
  258. taxes_from_template_1 = self.env['account.tax'].search([
  259. ('name', 'like', f"%{self.tax_template_1.name}"),
  260. ('company_id', 'in', [self.company.id, company_2.id]),
  261. ])
  262. # we should have 4 records: 2 companies * (1 original tax + 1 recreated tax)
  263. self.assertEqual(len(taxes_from_template_1), 4)
  264. def test_message_to_accountants(self):
  265. """ When we duplicate a tax because it was too different from the existing one we send
  266. a message to accountant advisors. This message should only be sent to advisors
  267. and not to regular users.
  268. """
  269. # create 1 normal user, 2 accountants managers
  270. accountant_manager_group = self.env.ref('account.group_account_manager')
  271. advisor_users = self.env['res.users'].create([{
  272. 'name': 'AccountAdvisorTest1',
  273. 'login': 'aat1',
  274. 'password': 'aat1aat1',
  275. 'groups_id': [(4, accountant_manager_group.id)],
  276. }, {
  277. 'name': 'AccountAdvisorTest2',
  278. 'login': 'aat2',
  279. 'password': 'aat2aat2',
  280. 'groups_id': [(4, accountant_manager_group.id)],
  281. }])
  282. normal_user = self.env['res.users'].create([{
  283. 'name': 'AccountUserTest1',
  284. 'login': 'aut1',
  285. 'password': 'aut1aut1',
  286. 'groups_id': [(4, self.env.ref('account.group_account_user').id)],
  287. }])
  288. # create situation where we need to recreate the tax during update to get notification(s) sent
  289. self.tax_template_1.amount += 1
  290. update_taxes_from_templates(self.env.cr, self.chart_template_xmlid)
  291. # accountants received the message
  292. self.assertEqual(self.env['mail.message'].search_count([
  293. ('partner_ids', 'in', advisor_users.partner_id.ids),
  294. ('body', 'like', f"%{self.tax_template_1.name}%"), # we look for taxes' name that have been sent in the message's body
  295. ]), 1)
  296. # normal user didn't
  297. self.assertEqual(self.env['mail.message'].search_count([
  298. ('partner_ids', 'in', normal_user.partner_id.ids),
  299. ('body', 'like', f"%{self.tax_template_1.name}%"), # we look for taxes' name that have been sent in the message's body
  300. ]), 0)
  301. def test_update_taxes_foreign_taxes(self):
  302. """ When taxes are instantiated through the fiscal position system (in multivat),
  303. its taxes should also be updated.
  304. """
  305. country_test = self.env['res.country'].create({
  306. 'name': 'Country Test',
  307. 'code': 'ZZ',
  308. })
  309. chart_template_xmlid_test = 'l10n_test2.test_chart_template_xmlid_2'
  310. chart_template_test = self.env['account.chart.template']._load_records([{
  311. 'xml_id': chart_template_xmlid_test,
  312. 'values': {
  313. 'name': 'Test Chart Template ZZ',
  314. 'currency_id': self.env.ref('base.EUR').id,
  315. 'bank_account_code_prefix': 1000,
  316. 'cash_account_code_prefix': 2000,
  317. 'transfer_account_code_prefix': 3000,
  318. 'country_id': country_test.id,
  319. }
  320. }])
  321. self._create_tax_template('account.test_tax_test_template', 'Tax name 1 TEST', 10, chart_template_id=chart_template_test.id)
  322. self.env['account.tax.template']._try_instantiating_foreign_taxes(country_test, self.company)
  323. self._create_tax_template('account.test_tax_test_template2', 'Tax name 2 TEST', 15, chart_template_id=chart_template_test.id)
  324. update_taxes_from_templates(self.env.cr, chart_template_xmlid_test)
  325. tax_test_model_data = self.env['ir.model.data'].search([
  326. ('name', '=', f'{self.company.id}_test_tax_test_template2'),
  327. ('model', '=', 'account.tax'),
  328. ])
  329. self.assertEqual(len(tax_test_model_data), 1, "Taxes should have been created even if the chart_template is installed through fiscal position system.")
  330. def test_update_taxes_chart_template_country_check(self):
  331. """ We can't update taxes that don't match the chart_template's country. """
  332. self.company.chart_template_id.country_id = self.env.ref('base.lu')
  333. # Generic chart_template is now (16.0+) in US so we also need to set fiscal country elsewhere for this test to fail as expected
  334. self.company.account_fiscal_country_id = self.env.ref('base.lu')
  335. # We provoke one recreation and one update
  336. self.tax_template_1.amount += 1
  337. self.tax_template_2.invoice_repartition_line_ids.tag_ids.name = 'tag_name_2_modified'
  338. with self.assertRaises(ValidationError):
  339. update_taxes_from_templates(self.env.cr, self.chart_template_xmlid)
  340. def test_update_taxes_fiscal_country_check(self):
  341. """ If there is no country set on chart_template, the taxes can only be updated if
  342. their country matches the fiscal country. """
  343. self.chart_template.country_id = None
  344. country_lu = self.env.ref('base.lu')
  345. self.company.account_fiscal_country_id = country_lu
  346. self.tax_template_1.amount += 1
  347. self.tax_template_2.invoice_repartition_line_ids.tag_ids.name = 'tag_name_2_modified'
  348. with self.assertRaises(ValidationError):
  349. update_taxes_from_templates(self.env.cr, self.chart_template_xmlid)
  350. def test_update_taxes_children_tax_ids(self):
  351. """ Ensures children_tax_ids are correctly generated when updating taxes with
  352. amount_type='group'.
  353. """
  354. # Both parent and its two children should be created.
  355. group_tax_name = 'Group Tax name 1 TEST'
  356. self._create_group_tax_template('account.test_group_tax_test_template', group_tax_name, chart_template_id=self.chart_template.id)
  357. update_taxes_from_templates(self.env.cr, self.chart_template_xmlid)
  358. parent_tax = self.env['account.tax'].search([
  359. ('company_id', '=', self.company.id),
  360. ('name', '=', group_tax_name),
  361. ])
  362. children_taxes = self.env['account.tax'].search([
  363. ('company_id', '=', self.company.id),
  364. ('name', 'like', f'{group_tax_name}_%'),
  365. ])
  366. self.assertEqual(len(parent_tax), 1, "The parent tax should have been created.")
  367. self.assertEqual(len(children_taxes), 2, "Two children should have been created.")
  368. self.assertEqual(parent_tax.children_tax_ids.ids, children_taxes.ids, "The parent and its children taxes should be linked together.")
  369. # Parent exists - only the two children should be created.
  370. children_taxes.unlink()
  371. update_taxes_from_templates(self.env.cr, self.chart_template_xmlid)
  372. children_taxes = self.env['account.tax'].search([
  373. ('company_id', '=', self.company.id),
  374. ('name', 'like', f'{group_tax_name}_%'),
  375. ])
  376. self.assertEqual(len(children_taxes), 2, "Two children should be re-created.")
  377. self.assertEqual(parent_tax.children_tax_ids.ids, children_taxes.ids,
  378. "The parent and its children taxes should be linked together.")
  379. # Children exist - only the parent should be created.
  380. parent_tax.unlink()
  381. update_taxes_from_templates(self.env.cr, self.chart_template_xmlid)
  382. parent_tax = self.env['account.tax'].search([
  383. ('company_id', '=', self.company.id),
  384. ('name', '=', group_tax_name),
  385. ])
  386. self.assertEqual(len(parent_tax), 1, "The parent tax should have been re-created.")
  387. self.assertEqual(parent_tax.children_tax_ids.ids, children_taxes.ids,
  388. "The parent and its children taxes should be linked together.")
  389. def test_update_taxes_children_tax_ids_inactive(self):
  390. """ Ensure tax templates are correctly generated when updating taxes with children taxes,
  391. even if templates are inactive.
  392. """
  393. group_tax_name = 'Group Tax name 1 inactive TEST'
  394. self._create_group_tax_template('account.test_group_tax_test_template_inactive', group_tax_name, chart_template_id=self.chart_template.id, active=False)
  395. update_taxes_from_templates(self.env.cr, self.chart_template_xmlid)
  396. parent_tax = self.env['account.tax'].with_context(active_test=False).search([
  397. ('company_id', '=', self.company.id),
  398. ('name', '=', group_tax_name),
  399. ])
  400. children_taxes = self.env['account.tax'].with_context(active_test=False).search([
  401. ('company_id', '=', self.company.id),
  402. ('name', 'like', f'{group_tax_name}_%'),
  403. ])
  404. self.assertEqual(len(parent_tax), 1, "The parent tax should have been created, even if it is inactive.")
  405. self.assertFalse(parent_tax.active, "The parent tax should be inactive.")
  406. self.assertEqual(len(children_taxes), 2, "Two children should have been created, even if they are inactive.")
  407. self.assertEqual(children_taxes.mapped('active'), [False] * 2, "Children taxes should be inactive.")