[MERGE] forward port of branch 7.0 up to 3a0af6a
[odoo/odoo.git] / addons / report_webkit / webkit_report.py
1 # -*- coding: utf-8 -*-
2 ##############################################################################
3 #
4 # Copyright (c) 2010 Camptocamp SA (http://www.camptocamp.com)
5 # All Right Reserved
6 #
7 # Author : Nicolas Bessi (Camptocamp)
8 # Contributor(s) : Florent Xicluna (Wingo SA)
9 #
10 # WARNING: This program as such is intended to be used by professional
11 # programmers who take the whole responsability of assessing all potential
12 # consequences resulting from its eventual inadequacies and bugs
13 # End users who are looking for a ready-to-use solution with commercial
14 # garantees and support are strongly adviced to contract a Free Software
15 # Service Company
16 #
17 # This program is Free Software; you can redistribute it and/or
18 # modify it under the terms of the GNU General Public License
19 # as published by the Free Software Foundation; either version 2
20 # of the License, or (at your option) any later version.
21 #
22 # This program is distributed in the hope that it will be useful,
23 # but WITHOUT ANY WARRANTY; without even the implied warranty of
24 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
25 # GNU General Public License for more details.
26 #
27 # You should have received a copy of the GNU General Public License
28 # along with this program; if not, write to the Free Software
29 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
30 #
31 ##############################################################################
32
33 import subprocess
34 import os
35 import sys
36 from openerp import report
37 import tempfile
38 import time
39 import logging
40 from functools import partial
41
42 from report_helper import WebKitHelper
43 import openerp
44 from openerp.modules.module import get_module_resource
45 from openerp.report.report_sxw import *
46 from openerp import tools
47 from openerp.tools.translate import _
48 from openerp.osv.osv import except_osv
49 from urllib import urlencode, quote as quote
50
51 _logger = logging.getLogger(__name__)
52
53 try:
54     # We use a jinja2 sandboxed environment to render mako templates.
55     # Note that the rendering does not cover all the mako syntax, in particular
56     # arbitrary Python statements are not accepted, and not all expressions are
57     # allowed: only "public" attributes (not starting with '_') of objects may
58     # be accessed.
59     # This is done on purpose: it prevents incidental or malicious execution of
60     # Python code that may break the security of the server.
61     from jinja2.sandbox import SandboxedEnvironment
62     mako_template_env = SandboxedEnvironment(
63         block_start_string="<%",
64         block_end_string="%>",
65         variable_start_string="${",
66         variable_end_string="}",
67         comment_start_string="<%doc>",
68         comment_end_string="</%doc>",
69         line_statement_prefix="%",
70         line_comment_prefix="##",
71         trim_blocks=True,               # do not output newline after blocks
72         autoescape=True,                # XML/HTML automatic escaping
73     )
74     mako_template_env.globals.update({
75         'str': str,
76         'quote': quote,
77         'urlencode': urlencode,
78     })
79 except ImportError:
80     _logger.warning("jinja2 not available, templating features will not work!")
81
82 def mako_template(text):
83     """Build a Mako template.
84
85     This template uses UTF-8 encoding
86     """
87
88     return mako_template_env.from_string(text)
89
90 _extender_functions = {}
91
92 def webkit_report_extender(report_name):
93     """
94     A decorator to define functions to extend the context used in a template rendering.
95     report_name must be the xml id of the desired report (it is mandatory to indicate the
96     module in that xml id).
97
98     The given function will be called at the creation of the report. The following arguments
99     will be passed to it (in this order):
100     - pool The model pool.
101     - cr The cursor.
102     - uid The user id.
103     - localcontext The context given to the template engine to render the templates for the
104         current report. This is the context that should be modified.
105     - context The OpenERP context.
106     """
107     def fct1(fct):
108         lst = _extender_functions.get(report_name)
109         if not lst:
110             lst = []
111             _extender_functions[report_name] = lst
112         lst.append(fct)
113         return fct
114     return fct1
115
116
117 class WebKitParser(report_sxw):
118     """Custom class that use webkit to render HTML reports
119        Code partially taken from report openoffice. Thanks guys :)
120     """
121     def __init__(self, name, table, rml=False, parser=rml_parse,
122         header=True, store=False, register=True):
123         self.localcontext = {}
124         report_sxw.__init__(self, name, table, rml, parser,
125             header, store, register=register)
126
127     def get_lib(self, cursor, uid):
128         """Return the lib wkhtml path"""
129         proxy = self.pool['ir.config_parameter']
130         webkit_path = proxy.get_param(cursor, uid, 'webkit_path')
131
132         if not webkit_path:
133             try:
134                 defpath = os.environ.get('PATH', os.defpath).split(os.pathsep)
135                 if hasattr(sys, 'frozen'):
136                     defpath.append(os.getcwd())
137                     if tools.config['root_path']:
138                         defpath.append(os.path.dirname(tools.config['root_path']))
139                 webkit_path = tools.which('wkhtmltopdf', path=os.pathsep.join(defpath))
140             except IOError:
141                 webkit_path = None
142
143         if webkit_path:
144             return webkit_path
145
146         raise except_osv(
147                          _('Wkhtmltopdf library path is not set'),
148                          _('Please install executable on your system' \
149                          ' (sudo apt-get install wkhtmltopdf) or download it from here:' \
150                          ' http://code.google.com/p/wkhtmltopdf/downloads/list and set the' \
151                          ' path in the ir.config_parameter with the webkit_path key.' \
152                          'Minimal version is 0.9.9')
153                         )
154
155     def generate_pdf(self, comm_path, report_xml, header, footer, html_list, webkit_header=False):
156         """Call webkit in order to generate pdf"""
157         if not webkit_header:
158             webkit_header = report_xml.webkit_header
159         fd, out_filename = tempfile.mkstemp(suffix=".pdf",
160                                             prefix="webkit.tmp.")
161         file_to_del = [out_filename]
162         if comm_path:
163             command = [comm_path]
164         else:
165             command = ['wkhtmltopdf']
166
167         command.append('--quiet')
168         # default to UTF-8 encoding.  Use <meta charset="latin-1"> to override.
169         command.extend(['--encoding', 'utf-8'])
170         if header :
171             with tempfile.NamedTemporaryFile(suffix=".head.html",
172                                              delete=False) as head_file:
173                 head_file.write(self._sanitize_html(header.encode('utf-8')))
174             file_to_del.append(head_file.name)
175             command.extend(['--header-html', head_file.name])
176         if footer :
177             with tempfile.NamedTemporaryFile(suffix=".foot.html",
178                                              delete=False) as foot_file:
179                 foot_file.write(self._sanitize_html(footer.encode('utf-8')))
180             file_to_del.append(foot_file.name)
181             command.extend(['--footer-html', foot_file.name])
182
183         if webkit_header.margin_top :
184             command.extend(['--margin-top', str(webkit_header.margin_top).replace(',', '.')])
185         if webkit_header.margin_bottom :
186             command.extend(['--margin-bottom', str(webkit_header.margin_bottom).replace(',', '.')])
187         if webkit_header.margin_left :
188             command.extend(['--margin-left', str(webkit_header.margin_left).replace(',', '.')])
189         if webkit_header.margin_right :
190             command.extend(['--margin-right', str(webkit_header.margin_right).replace(',', '.')])
191         if webkit_header.orientation :
192             command.extend(['--orientation', str(webkit_header.orientation).replace(',', '.')])
193         if webkit_header.format :
194             command.extend(['--page-size', str(webkit_header.format).replace(',', '.')])
195         count = 0
196         for html in html_list :
197             with tempfile.NamedTemporaryFile(suffix="%d.body.html" %count,
198                                              delete=False) as html_file:
199                 count += 1
200                 html_file.write(self._sanitize_html(html.encode('utf-8')))
201             file_to_del.append(html_file.name)
202             command.append(html_file.name)
203         command.append(out_filename)
204         stderr_fd, stderr_path = tempfile.mkstemp(text=True)
205         file_to_del.append(stderr_path)
206         try:
207             status = subprocess.call(command, stderr=stderr_fd)
208             os.close(stderr_fd) # ensure flush before reading
209             stderr_fd = None # avoid closing again in finally block
210             fobj = open(stderr_path, 'r')
211             error_message = fobj.read()
212             fobj.close()
213             if not error_message:
214                 error_message = _('No diagnosis message was provided')
215             else:
216                 error_message = _('The following diagnosis message was provided:\n') + error_message
217             if status :
218                 raise except_osv(_('Webkit error' ),
219                                  _("The command 'wkhtmltopdf' failed with error code = %s. Message: %s") % (status, error_message))
220             with open(out_filename, 'rb') as pdf_file:
221                 pdf = pdf_file.read()
222             os.close(fd)
223         finally:
224             if stderr_fd is not None:
225                 os.close(stderr_fd)
226             for f_to_del in file_to_del:
227                 try:
228                     os.unlink(f_to_del)
229                 except (OSError, IOError), exc:
230                     _logger.error('cannot remove file %s: %s', f_to_del, exc)
231         return pdf
232
233     def translate_call(self, parser_instance, src):
234         """Translate String."""
235         ir_translation = self.pool['ir.translation']
236         name = self.tmpl and 'addons/' + self.tmpl or None
237         res = ir_translation._get_source(parser_instance.cr, parser_instance.uid,
238                                          name, 'report', parser_instance.localcontext.get('lang', 'en_US'), src)
239         if res == src:
240             # no translation defined, fallback on None (backward compatibility)
241             res = ir_translation._get_source(parser_instance.cr, parser_instance.uid,
242                                              None, 'report', parser_instance.localcontext.get('lang', 'en_US'), src)
243         if not res :
244             return src
245         return res
246
247     # override needed to keep the attachments storing procedure
248     def create_single_pdf(self, cursor, uid, ids, data, report_xml, context=None):
249         """generate the PDF"""
250
251         # just try to find an xml id for the report
252         cr = cursor
253         pool = openerp.registry(cr.dbname)
254         found_xml_ids = pool["ir.model.data"].search(cr, uid, [["model", "=", "ir.actions.report.xml"], \
255             ["res_id", "=", report_xml.id]], context=context)
256         xml_id = None
257         if found_xml_ids:
258             xml_id = pool["ir.model.data"].read(cr, uid, found_xml_ids[0], ["module", "name"])
259             xml_id = "%s.%s" % (xml_id["module"], xml_id["name"])
260
261         if context is None:
262             context={}
263         htmls = []
264         if report_xml.report_type != 'webkit':
265             return super(WebKitParser,self).create_single_pdf(cursor, uid, ids, data, report_xml, context=context)
266
267         parser_instance = self.parser(cursor,
268                                       uid,
269                                       self.name2,
270                                       context=context)
271
272         self.pool = pool
273         objs = self.getObjects(cursor, uid, ids, context)
274         parser_instance.set_context(objs, data, ids, report_xml.report_type)
275
276         template =  False
277
278         if report_xml.report_file :
279             path = get_module_resource(*report_xml.report_file.split('/'))
280             if path and os.path.exists(path) :
281                 template = file(path).read()
282         if not template and report_xml.report_webkit_data :
283             template =  report_xml.report_webkit_data
284         if not template :
285             raise except_osv(_('Error!'), _('Webkit report template not found!'))
286         header = report_xml.webkit_header.html
287         footer = report_xml.webkit_header.footer_html
288         if not header and report_xml.header:
289             raise except_osv(
290                   _('No header defined for this Webkit report!'),
291                   _('Please set a header in company settings.')
292               )
293         if not report_xml.header :
294             header = ''
295             default_head = get_module_resource('report_webkit', 'default_header.html')
296             with open(default_head,'r') as f:
297                 header = f.read()
298         css = report_xml.webkit_header.css
299         if not css :
300             css = ''
301
302         translate_call = partial(self.translate_call, parser_instance)
303         body_mako_tpl = mako_template(template)
304         helper = WebKitHelper(cursor, uid, report_xml.id, context)
305         parser_instance.localcontext['helper'] = helper
306         parser_instance.localcontext['css'] = css
307         parser_instance.localcontext['_'] = translate_call
308
309         # apply extender functions
310         additional = {}
311         if xml_id in _extender_functions:
312             for fct in _extender_functions[xml_id]:
313                 fct(pool, cr, uid, parser_instance.localcontext, context)
314
315         if report_xml.precise_mode:
316             ctx = dict(parser_instance.localcontext)
317             for obj in parser_instance.localcontext['objects']:
318                 ctx['objects'] = [obj]
319                 try :
320                     html = body_mako_tpl.render(dict(ctx))
321                     htmls.append(html)
322                 except Exception, e:
323                     msg = u"%s" % e
324                     _logger.error(msg)
325                     raise except_osv(_('Webkit render!'), msg)
326         else:
327             try :
328                 html = body_mako_tpl.render(dict(parser_instance.localcontext))
329                 htmls.append(html)
330             except Exception, e:
331                 msg = u"%s" % e
332                 _logger.error(msg)
333                 raise except_osv(_('Webkit render!'), msg)
334         head_mako_tpl = mako_template(header)
335         try :
336             head = head_mako_tpl.render(dict(parser_instance.localcontext, _debug=False))
337         except Exception, e:
338             raise except_osv(_('Webkit render!'), u"%s" % e)
339         foot = False
340         if footer :
341             foot_mako_tpl = mako_template(footer)
342             try :
343                 foot = foot_mako_tpl.render(dict(parser_instance.localcontext))
344             except Exception, e:
345                 msg = u"%s" % e
346                 _logger.error(msg)
347                 raise except_osv(_('Webkit render!'), msg)
348         if report_xml.webkit_debug :
349             try :
350                 deb = head_mako_tpl.render(dict(parser_instance.localcontext, _debug=tools.ustr("\n".join(htmls))))
351             except Exception, e:
352                 msg = u"%s" % e
353                 _logger.error(msg)
354                 raise except_osv(_('Webkit render!'), msg)
355             return (deb, 'html')
356         bin = self.get_lib(cursor, uid)
357         pdf = self.generate_pdf(bin, report_xml, head, foot, htmls)
358         return (pdf, 'pdf')
359
360     def create(self, cursor, uid, ids, data, context=None):
361         """We override the create function in order to handle generator
362            Code taken from report openoffice. Thanks guys :) """
363         pool = openerp.registry(cursor.dbname)
364         ir_obj = pool['ir.actions.report.xml']
365         report_xml_ids = ir_obj.search(cursor, uid,
366                 [('report_name', '=', self.name[7:])], context=context)
367         if report_xml_ids:
368
369             report_xml = ir_obj.browse(cursor,
370                                        uid,
371                                        report_xml_ids[0],
372                                        context=context)
373             report_xml.report_rml = None
374             report_xml.report_rml_content = None
375             report_xml.report_sxw_content_data = None
376             report_xml.report_sxw_content = None
377             report_xml.report_sxw = None
378         else:
379             return super(WebKitParser, self).create(cursor, uid, ids, data, context)
380         if report_xml.report_type != 'webkit':
381             return super(WebKitParser, self).create(cursor, uid, ids, data, context)
382         result = self.create_source_pdf(cursor, uid, ids, data, report_xml, context)
383         if not result:
384             return (False,False)
385         return result
386
387     def _sanitize_html(self, html):
388         """wkhtmltopdf expects the html page to declare a doctype.
389         """
390         if html and html[:9].upper() != "<!DOCTYPE":
391             html = "<!DOCTYPE html>\n" + html
392         return html
393
394 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: