OCA reporting engine fork for dev and update.
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

260 lines
11 KiB

  1. # Copyright 2016 ACSONE SA/NV
  2. # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).).
  3. import base64
  4. from base64 import b64decode
  5. import mock
  6. import os
  7. import pkg_resources
  8. import shutil
  9. import tempfile
  10. from contextlib import contextmanager
  11. from odoo import tools
  12. from odoo.tests.common import TransactionCase
  13. from odoo.exceptions import ValidationError
  14. from odoo.addons.base.tests.test_mimetypes import PNG
  15. from ..models.ir_actions_report import PY3O_CONVERSION_COMMAND_PARAMETER
  16. from ..models.py3o_report import TemplateNotFound
  17. from ..models._py3o_parser_context import format_multiline_value
  18. from base64 import b64encode
  19. from PyPDF2 import PdfFileWriter
  20. from PyPDF2.pdf import PageObject
  21. import logging
  22. logger = logging.getLogger(__name__)
  23. try:
  24. from genshi.core import Markup
  25. except ImportError:
  26. logger.debug('Cannot import genshi.core')
  27. @contextmanager
  28. def temporary_copy(path):
  29. filname, ext = os.path.splitext(path)
  30. tmp_filename = tempfile.mktemp(suffix='.' + ext)
  31. try:
  32. shutil.copy2(path, tmp_filename)
  33. yield tmp_filename
  34. finally:
  35. os.unlink(tmp_filename)
  36. class TestReportPy3o(TransactionCase):
  37. def setUp(self):
  38. super(TestReportPy3o, self).setUp()
  39. self.env.user.image = PNG
  40. self.report = self.env.ref("report_py3o.res_users_report_py3o")
  41. self.py3o_report = self.env['py3o.report'].create({
  42. 'ir_actions_report_id': self.report.id})
  43. def test_required_py3_filetype(self):
  44. self.assertEqual(self.report.report_type, "py3o")
  45. with self.assertRaises(ValidationError) as e:
  46. self.report.py3o_filetype = False
  47. self.assertEqual(
  48. e.exception.name,
  49. "Field 'Output Format' is required for Py3O report")
  50. def _render_patched(self, result_text='test result', call_count=1):
  51. py3o_report = self.env['py3o.report']
  52. py3o_report_obj = py3o_report.create({
  53. "ir_actions_report_id": self.report.id
  54. })
  55. with mock.patch.object(
  56. py3o_report.__class__, '_create_single_report') as patched_pdf:
  57. result = tempfile.mktemp('.txt')
  58. with open(result, 'w') as fp:
  59. fp.write(result_text)
  60. patched_pdf.side_effect = lambda record, data:\
  61. py3o_report_obj._postprocess_report(
  62. record, result
  63. ) or result
  64. # test the call the the create method inside our custom parser
  65. self.report.render(self.env.user.ids)
  66. self.assertEqual(call_count, patched_pdf.call_count)
  67. # generated files no more exists
  68. self.assertFalse(os.path.exists(result))
  69. def test_reports(self):
  70. res = self.report.render(self.env.user.ids)
  71. self.assertTrue(res)
  72. def test_reports_merge_zip(self):
  73. self.report.py3o_filetype = "odt"
  74. users = self.env['res.users'].search([])
  75. self.assertTrue(len(users) > 0)
  76. py3o_report = self.env['py3o.report']
  77. _zip_results = self.py3o_report._zip_results
  78. with mock.patch.object(
  79. py3o_report.__class__, '_zip_results') as patched_zip_results:
  80. patched_zip_results.side_effect = _zip_results
  81. content, filetype = self.report.render(users.ids)
  82. self.assertEqual(1, patched_zip_results.call_count)
  83. self.assertEqual(filetype, 'zip')
  84. def test_reports_merge_pdf(self):
  85. reports_path = []
  86. for i in range(0, 3):
  87. result = tempfile.mktemp('.txt')
  88. writer = PdfFileWriter()
  89. writer.addPage(PageObject.createBlankPage(width=100, height=100))
  90. with open(result, 'wb') as fp:
  91. writer.write(fp)
  92. reports_path.append(result)
  93. res = self.py3o_report._merge_pdf(reports_path)
  94. self.assertTrue(res)
  95. def test_report_load_from_attachment(self):
  96. self.report.write({"attachment_use": True,
  97. "attachment": "'my_saved_report'"})
  98. attachments = self.env['ir.attachment'].search([])
  99. self._render_patched()
  100. new_attachments = self.env['ir.attachment'].search([])
  101. created_attachement = new_attachments - attachments
  102. self.assertEqual(1, len(created_attachement))
  103. content = b64decode(created_attachement.datas)
  104. self.assertEqual(b"test result", content)
  105. # put a new content into tha attachement and check that the next
  106. # time we ask the report we received the saved attachment not a newly
  107. # generated document
  108. created_attachement.datas = base64.encodestring(b"new content")
  109. res = self.report.render(self.env.user.ids)
  110. self.assertEqual((b'new content', self.report.py3o_filetype), res)
  111. def test_report_post_process(self):
  112. """
  113. By default the post_process method is in charge to save the
  114. generated report into an ir.attachment if requested.
  115. """
  116. self.report.attachment = "object.name + '.txt'"
  117. ir_attachment = self.env['ir.attachment']
  118. attachements = ir_attachment.search([(1, '=', 1)])
  119. self._render_patched()
  120. attachements = ir_attachment.search([(1, '=', 1)]) - attachements
  121. self.assertEqual(1, len(attachements.ids))
  122. self.assertEqual(self.env.user.name + '.txt', attachements.name)
  123. self.assertEqual(self.env.user._name, attachements.res_model)
  124. self.assertEqual(self.env.user.id, attachements.res_id)
  125. self.assertEqual(b'test result', b64decode(attachements.datas))
  126. @tools.misc.mute_logger('odoo.addons.report_py3o.models.py3o_report')
  127. def test_report_template_configs(self):
  128. # the demo template is specified with a relative path in in the module
  129. # path
  130. tmpl_name = self.report.py3o_template_fallback
  131. flbk_filename = pkg_resources.resource_filename(
  132. "odoo.addons.%s" % self.report.module,
  133. tmpl_name)
  134. self.assertTrue(os.path.exists(flbk_filename))
  135. res = self.report.render(self.env.user.ids)
  136. self.assertTrue(res)
  137. # The generation fails if the template is not found
  138. self.report.module = False
  139. with self.assertRaises(TemplateNotFound), self.env.cr.savepoint():
  140. self.report.render(self.env.user.ids)
  141. # the template can also be provided as an abspath if it's root path
  142. # is trusted
  143. self.report.py3o_template_fallback = flbk_filename
  144. with self.assertRaises(TemplateNotFound):
  145. self.report.render(self.env.user.ids)
  146. with temporary_copy(flbk_filename) as tmp_filename:
  147. self.report.py3o_template_fallback = tmp_filename
  148. tools.config.misc['report_py3o'] = {
  149. 'root_tmpl_path': os.path.dirname(tmp_filename)}
  150. res = self.report.render(self.env.user.ids)
  151. self.assertTrue(res)
  152. # the tempalte can also be provided as a binary field
  153. self.report.py3o_template_fallback = False
  154. with open(flbk_filename, 'rb') as tmpl_file:
  155. tmpl_data = b64encode(tmpl_file.read())
  156. py3o_template = self.env['py3o.template'].create({
  157. 'name': 'test_template',
  158. 'py3o_template_data': tmpl_data,
  159. 'filetype': 'odt'})
  160. self.report.py3o_template_id = py3o_template
  161. self.report.py3o_template_fallback = flbk_filename
  162. res = self.report.render(self.env.user.ids)
  163. self.assertTrue(res)
  164. @tools.misc.mute_logger('odoo.addons.report_py3o.models.py3o_report')
  165. def test_report_template_fallback_validity(self):
  166. tmpl_name = self.report.py3o_template_fallback
  167. flbk_filename = pkg_resources.resource_filename(
  168. "odoo.addons.%s" % self.report.module,
  169. tmpl_name)
  170. # an exising file in a native format is a valid template if it's
  171. self.assertTrue(self.py3o_report._get_template_from_path(
  172. tmpl_name))
  173. self.report.module = None
  174. # a directory is not a valid template..
  175. self.assertFalse(self.py3o_report._get_template_from_path('/etc/'))
  176. self.assertFalse(self.py3o_report._get_template_from_path('.'))
  177. # an vaild template outside the root_tmpl_path is not a valid template
  178. # path
  179. # located in trusted directory
  180. self.report.py3o_template_fallback = flbk_filename
  181. self.assertFalse(self.py3o_report._get_template_from_path(
  182. flbk_filename))
  183. with temporary_copy(flbk_filename) as tmp_filename:
  184. self.assertTrue(self.py3o_report._get_template_from_path(
  185. tmp_filename))
  186. # check security
  187. self.assertFalse(self.py3o_report._get_template_from_path(
  188. 'rm -rf . & %s' % flbk_filename))
  189. # a file in a non native LibreOffice format is not a valid template
  190. with tempfile.NamedTemporaryFile(suffix='.toto')as f:
  191. self.assertFalse(self.py3o_report._get_template_from_path(
  192. f.name))
  193. # non exising files are not valid template
  194. self.assertFalse(self.py3o_report._get_template_from_path(
  195. '/etc/test.odt'))
  196. def test_escape_html_characters_format_multiline_value(self):
  197. self.assertEqual(Markup('&lt;&gt;<text:line-break/>&amp;test;'),
  198. format_multiline_value('<>\n&test;'))
  199. def test_py3o_report_availability(self):
  200. # This test could fails if libreoffice is not available on the server
  201. self.report.py3o_filetype = "odt"
  202. self.assertTrue(self.report.lo_bin_path)
  203. self.assertTrue(self.report.is_py3o_native_format)
  204. self.assertFalse(self.report.is_py3o_report_not_available)
  205. self.assertFalse(self.report.msg_py3o_report_not_available)
  206. # specify a wrong lo bin path
  207. self.env['ir.config_parameter'].set_param(
  208. PY3O_CONVERSION_COMMAND_PARAMETER, "/wrong_path")
  209. self.report.refresh()
  210. # no bin path available but the report is still available since
  211. # the output is into native format
  212. self.assertFalse(self.report.lo_bin_path)
  213. self.assertFalse(self.report.is_py3o_report_not_available)
  214. self.assertFalse(self.report.msg_py3o_report_not_available)
  215. res = self.report.render(self.env.user.ids)
  216. self.assertTrue(res)
  217. # The report should become unavailable for an non native output format
  218. self.report.py3o_filetype = "pdf"
  219. self.assertFalse(self.report.is_py3o_native_format)
  220. self.assertTrue(self.report.is_py3o_report_not_available)
  221. self.assertTrue(self.report.msg_py3o_report_not_available)
  222. with self.assertRaises(RuntimeError):
  223. self.report.render(self.env.user.ids)
  224. # if we reset the wrong path, everything should work
  225. self.env['ir.config_parameter'].set_param(
  226. PY3O_CONVERSION_COMMAND_PARAMETER, "libreoffice")
  227. self.report.refresh()
  228. self.assertTrue(self.report.lo_bin_path)
  229. self.assertFalse(self.report.is_py3o_native_format)
  230. self.assertFalse(self.report.is_py3o_report_not_available)
  231. self.assertFalse(self.report.msg_py3o_report_not_available)
  232. res = self.report.render(self.env.user.ids)
  233. self.assertTrue(res)