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.

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