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.

227 lines
7.9 KiB

  1. # -*- encoding: utf-8 -*-
  2. import pkg_resources
  3. import os
  4. import sys
  5. from base64 import b64decode
  6. import requests
  7. from tempfile import NamedTemporaryFile
  8. from openerp import _
  9. from openerp.report.report_sxw import report_sxw, rml_parse
  10. from openerp import registry
  11. from openerp.exceptions import ValidationError
  12. from py3o.template import Template
  13. _extender_functions = {}
  14. class TemplateNotFound(Exception):
  15. pass
  16. def py3o_report_extender(report_name):
  17. """
  18. A decorator to define function to extend the context sent to a template.
  19. This will be called at the creation of the report.
  20. The following arguments will be passed to it:
  21. - pool: the model pool
  22. - cr: the database cursor
  23. - uid: the id of the user that call the renderer
  24. - localcontext: The context that will be passed to the report engine
  25. - context: the Odoo context
  26. Method copied from CampToCamp report_webkit module.
  27. :param report_name: xml id of the report
  28. :return: a decorated class
  29. """
  30. def fct1(fct):
  31. lst = _extender_functions.get(report_name)
  32. if not lst:
  33. lst = []
  34. _extender_functions[report_name] = lst
  35. lst.append(fct)
  36. return fct
  37. return fct1
  38. class Py3oParser(report_sxw):
  39. """Custom class that use Py3o to render libroffice reports.
  40. Code partially taken from CampToCamp's webkit_report."""
  41. def __init__(self, name, table, rml=False, parser=rml_parse,
  42. header=False, store=False, register=True):
  43. self.localcontext = {}
  44. super(Py3oParser, self).__init__(
  45. name, table, rml=rml, parser=parser,
  46. header=header, store=store, register=register
  47. )
  48. def get_template(self, report_obj):
  49. """private helper to fetch the template data either from the database
  50. or from the default template file provided by the implementer.
  51. ATM this method takes a report definition recordset
  52. to try and fetch the report template from database. If not found it will
  53. fallback to the template file referenced in the report definition.
  54. @param report_obj: a recordset representing the report defintion
  55. @type report_obj: openerp.model.recordset instance
  56. @returns: string or buffer containing the template data
  57. @raises: TemplateNotFound which is a subclass of
  58. openerp.exceptions.DeferredException
  59. """
  60. tmpl_data = None
  61. if report_obj.py3o_template_id and report_obj.py3o_template_id.id:
  62. # if a user gave a report template
  63. tmpl_data = b64decode(
  64. report_obj.py3o_template_id.py3o_template_data
  65. )
  66. elif report_obj.py3o_template_fallback and report_obj.module:
  67. # if the default is defined
  68. flbk_filename = pkg_resources.resource_filename(
  69. "openerp.addons.%s" % report_obj.module,
  70. report_obj.py3o_template_fallback,
  71. )
  72. if os.path.exists(flbk_filename):
  73. # and it exists on the fileystem
  74. with open(flbk_filename, 'r') as tmpl:
  75. tmpl_data = tmpl.read()
  76. if tmpl_data is None:
  77. # if for any reason the template is not found
  78. raise TemplateNotFound(
  79. _(u'No template found. Aborting.'),
  80. sys.exc_info(),
  81. )
  82. return tmpl_data
  83. def create_single_pdf(self, cr, uid, ids, data, report_xml, context=None):
  84. """ Overide this function to generate our py3o report
  85. """
  86. if report_xml.report_type != 'py3o':
  87. return super(Py3oParser, self).create_single_pdf(
  88. cr, uid, ids, data, report_xml, context=context
  89. )
  90. pool = registry(cr.dbname)
  91. model_data_ids = pool['ir.model.data'].search(
  92. cr, uid, [
  93. ('model', '=', 'ir.actions.report.xml'),
  94. ('res_id', '=', report_xml.id),
  95. ]
  96. )
  97. xml_id = None
  98. if model_data_ids:
  99. model_data = pool['ir.model.data'].browse(
  100. cr, uid, model_data_ids[0], context=context
  101. )
  102. xml_id = '%s.%s' % (model_data.module, model_data.name)
  103. parser_instance = self.parser(cr, uid, self.name2, context=context)
  104. parser_instance.set_context(
  105. self.getObjects(cr, uid, ids, context),
  106. data, ids, report_xml.report_type
  107. )
  108. if xml_id in _extender_functions:
  109. for fct in _extender_functions[xml_id]:
  110. fct(pool, cr, uid, parser_instance.localcontext, context)
  111. template = report_xml.py3o_template_id
  112. filetype = report_xml.py3o_fusion_filetype
  113. tmpl_data = self.get_template(report_xml)
  114. # py3o.template operates on filenames so create temporary files.
  115. with NamedTemporaryFile(
  116. suffix='.odt',
  117. prefix='py3o-template-') as in_temp, NamedTemporaryFile(
  118. suffix='.odt',
  119. prefix='py3o-report-') as out_temp:
  120. in_temp.write(tmpl_data)
  121. in_temp.flush()
  122. template = Template(in_temp.name, out_temp.name)
  123. template.render(parser_instance.localcontext)
  124. out_temp.seek(0)
  125. # TODO: use py3o.formats to know native formats instead
  126. # of hardcoding this value
  127. # TODO: why use the human readable form when you're a machine?
  128. # this is non-sense AND dangerous... please use technical name
  129. if filetype.human_ext != 'odt':
  130. # Now we ask fusion server to convert our template
  131. fusion_server_obj = pool['py3o.server']
  132. fusion_server_id = fusion_server_obj.search(
  133. cr, uid, [], context=context
  134. )[0]
  135. fusion_server = fusion_server_obj.browse(
  136. cr, uid, fusion_server_id, context=context
  137. )
  138. files = {
  139. 'tmpl_file': out_temp,
  140. }
  141. fields = {
  142. "targetformat": filetype.fusion_ext,
  143. "datadict": "{}",
  144. "image_mapping": "{}",
  145. "skipfusion": True,
  146. }
  147. # Here is a little joke about Odoo
  148. # we do nice chunked reading from the network...
  149. r = requests.post(fusion_server.url, data=fields, files=files)
  150. if r.status_code == 400:
  151. # server says we have an issue... let's to that
  152. raise ValidationError(
  153. r.json(),
  154. )
  155. else:
  156. chunk_size = 1024
  157. with NamedTemporaryFile(
  158. suffix=filetype.human_ext,
  159. prefix='py3o-template-'
  160. ) as fd:
  161. for chunk in r.iter_content(chunk_size):
  162. fd.write(chunk)
  163. fd.seek(0)
  164. # ... but odoo wants the whole data in memory anyways :)
  165. return fd.read(), filetype.human_ext
  166. return out_temp.read(), 'odt'
  167. def create(self, cr, uid, ids, data, context=None):
  168. """ Override this function to handle our py3o report
  169. """
  170. pool = registry(cr.dbname)
  171. ir_action_report_obj = pool['ir.actions.report.xml']
  172. report_xml_ids = ir_action_report_obj.search(
  173. cr, uid, [('report_name', '=', self.name[7:])], context=context
  174. )
  175. if not report_xml_ids:
  176. return super(Py3oParser, self).create(
  177. cr, uid, ids, data, context=context
  178. )
  179. report_xml = ir_action_report_obj.browse(
  180. cr, uid, report_xml_ids[0], context=context
  181. )
  182. result = self.create_source_pdf(
  183. cr, uid, ids, data, report_xml, context
  184. )
  185. if not result:
  186. return False, False
  187. return result