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.

232 lines
7.9 KiB

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