[FIX] website_sale: add the parameter _mail_post_access on product.template
[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
41 from report_helper import WebKitHelper
42 import openerp
43 from openerp.modules.module import get_module_resource
44 from openerp.report.report_sxw import *
45 from openerp import tools
46 from openerp.tools.translate import _
47 from openerp.osv.osv import except_osv
48 from urllib import urlencode, quote as quote
49
50 _logger = logging.getLogger(__name__)
51
52 try:
53     # We use a jinja2 sandboxed environment to render mako templates.
54     # Note that the rendering does not cover all the mako syntax, in particular
55     # arbitrary Python statements are not accepted, and not all expressions are
56     # allowed: only "public" attributes (not starting with '_') of objects may
57     # be accessed.
58     # This is done on purpose: it prevents incidental or malicious execution of
59     # Python code that may break the security of the server.
60     from jinja2.sandbox import SandboxedEnvironment
61     mako_template_env = SandboxedEnvironment(
62         block_start_string="<%",
63         block_end_string="%>",
64         variable_start_string="${",
65         variable_end_string="}",
66         comment_start_string="<%doc>",
67         comment_end_string="</%doc>",
68         line_statement_prefix="%",
69         line_comment_prefix="##",
70         trim_blocks=True,               # do not output newline after blocks
71         autoescape=True,                # XML/HTML automatic escaping
72     )
73     mako_template_env.globals.update({
74         'str': str,
75         'quote': quote,
76         'urlencode': urlencode,
77     })
78 except ImportError:
79     _logger.warning("jinja2 not available, templating features will not work!")
80
81 def mako_template(text):
82     """Build a Mako template.
83
84     This template uses UTF-8 encoding
85     """
86
87     return mako_template_env.from_string(text)
88
89 _extender_functions = {}
90
91 def webkit_report_extender(report_name):
92     """
93     A decorator to define functions to extend the context used in a template rendering.
94     report_name must be the xml id of the desired report (it is mandatory to indicate the
95     module in that xml id).
96
97     The given function will be called at the creation of the report. The following arguments
98     will be passed to it (in this order):
99     - pool The model pool.
100     - cr The cursor.
101     - uid The user id.
102     - localcontext The context given to the template engine to render the templates for the
103         current report. This is the context that should be modified.
104     - context The OpenERP context.
105     """
106     def fct1(fct):
107         lst = _extender_functions.get(report_name)
108         if not lst:
109             lst = []
110             _extender_functions[report_name] = lst
111         lst.append(fct)
112         return fct
113     return fct1
114
115
116 class WebKitParser(report_sxw):
117     """Custom class that use webkit to render HTML reports
118        Code partially taken from report openoffice. Thanks guys :)
119     """
120     def __init__(self, name, table, rml=False, parser=rml_parse,
121         header=True, store=False, register=True):
122         self.parser_instance = False
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         tmp_dir = tempfile.gettempdir()
160         out_filename = tempfile.mktemp(suffix=".pdf", 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             head_file = file( os.path.join(
172                                   tmp_dir,
173                                   str(time.time()) + '.head.html'
174                                  ),
175                                 'w'
176                             )
177             head_file.write(self._sanitize_html(header.encode('utf-8')))
178             head_file.close()
179             file_to_del.append(head_file.name)
180             command.extend(['--header-html', head_file.name])
181         if footer :
182             foot_file = file(  os.path.join(
183                                   tmp_dir,
184                                   str(time.time()) + '.foot.html'
185                                  ),
186                                 'w'
187                             )
188             foot_file.write(self._sanitize_html(footer.encode('utf-8')))
189             foot_file.close()
190             file_to_del.append(foot_file.name)
191             command.extend(['--footer-html', foot_file.name])
192
193         if webkit_header.margin_top :
194             command.extend(['--margin-top', str(webkit_header.margin_top).replace(',', '.')])
195         if webkit_header.margin_bottom :
196             command.extend(['--margin-bottom', str(webkit_header.margin_bottom).replace(',', '.')])
197         if webkit_header.margin_left :
198             command.extend(['--margin-left', str(webkit_header.margin_left).replace(',', '.')])
199         if webkit_header.margin_right :
200             command.extend(['--margin-right', str(webkit_header.margin_right).replace(',', '.')])
201         if webkit_header.orientation :
202             command.extend(['--orientation', str(webkit_header.orientation).replace(',', '.')])
203         if webkit_header.format :
204             command.extend(['--page-size', str(webkit_header.format).replace(',', '.')])
205         count = 0
206         for html in html_list :
207             html_file = file(os.path.join(tmp_dir, str(time.time()) + str(count) +'.body.html'), 'w')
208             count += 1
209             html_file.write(self._sanitize_html(html.encode('utf-8')))
210             html_file.close()
211             file_to_del.append(html_file.name)
212             command.append(html_file.name)
213         command.append(out_filename)
214         stderr_fd, stderr_path = tempfile.mkstemp(text=True)
215         file_to_del.append(stderr_path)
216         try:
217             status = subprocess.call(command, stderr=stderr_fd)
218             os.close(stderr_fd) # ensure flush before reading
219             stderr_fd = None # avoid closing again in finally block
220             fobj = open(stderr_path, 'r')
221             error_message = fobj.read()
222             fobj.close()
223             if not error_message:
224                 error_message = _('No diagnosis message was provided')
225             else:
226                 error_message = _('The following diagnosis message was provided:\n') + error_message
227             if status :
228                 raise except_osv(_('Webkit error' ),
229                                  _("The command 'wkhtmltopdf' failed with error code = %s. Message: %s") % (status, error_message))
230             pdf_file = open(out_filename, 'rb')
231             pdf = pdf_file.read()
232             pdf_file.close()
233         finally:
234             if stderr_fd is not None:
235                 os.close(stderr_fd)
236             for f_to_del in file_to_del:
237                 try:
238                     os.unlink(f_to_del)
239                 except (OSError, IOError), exc:
240                     _logger.error('cannot remove file %s: %s', f_to_del, exc)
241         return pdf
242
243     def translate_call(self, src):
244         """Translate String."""
245         ir_translation = self.pool['ir.translation']
246         name = self.tmpl and 'addons/' + self.tmpl or None
247         res = ir_translation._get_source(self.parser_instance.cr, self.parser_instance.uid,
248                                          name, 'report', self.parser_instance.localcontext.get('lang', 'en_US'), src)
249         if res == src:
250             # no translation defined, fallback on None (backward compatibility)
251             res = ir_translation._get_source(self.parser_instance.cr, self.parser_instance.uid,
252                                              None, 'report', self.parser_instance.localcontext.get('lang', 'en_US'), src)
253         if not res :
254             return src
255         return res
256
257     # override needed to keep the attachments storing procedure
258     def create_single_pdf(self, cursor, uid, ids, data, report_xml, context=None):
259         """generate the PDF"""
260
261         # just try to find an xml id for the report
262         cr = cursor
263         pool = openerp.registry(cr.dbname)
264         found_xml_ids = pool["ir.model.data"].search(cr, uid, [["model", "=", "ir.actions.report.xml"], \
265             ["res_id", "=", report_xml.id]], context=context)
266         xml_id = None
267         if found_xml_ids:
268             xml_id = pool["ir.model.data"].read(cr, uid, found_xml_ids[0], ["module", "name"])
269             xml_id = "%s.%s" % (xml_id["module"], xml_id["name"])
270
271         if context is None:
272             context={}
273         htmls = []
274         if report_xml.report_type != 'webkit':
275             return super(WebKitParser,self).create_single_pdf(cursor, uid, ids, data, report_xml, context=context)
276
277         self.parser_instance = self.parser(cursor,
278                                            uid,
279                                            self.name2,
280                                            context=context)
281
282         self.pool = pool
283         objs = self.getObjects(cursor, uid, ids, context)
284         self.parser_instance.set_context(objs, data, ids, report_xml.report_type)
285
286         template =  False
287
288         if report_xml.report_file :
289             path = get_module_resource(*report_xml.report_file.split('/'))
290             if path and os.path.exists(path) :
291                 template = file(path).read()
292         if not template and report_xml.report_webkit_data :
293             template =  report_xml.report_webkit_data
294         if not template :
295             raise except_osv(_('Error!'), _('Webkit report template not found!'))
296         header = report_xml.webkit_header.html
297         footer = report_xml.webkit_header.footer_html
298         if not header and report_xml.header:
299             raise except_osv(
300                   _('No header defined for this Webkit report!'),
301                   _('Please set a header in company settings.')
302               )
303         if not report_xml.header :
304             header = ''
305             default_head = get_module_resource('report_webkit', 'default_header.html')
306             with open(default_head,'r') as f:
307                 header = f.read()
308         css = report_xml.webkit_header.css
309         if not css :
310             css = ''
311
312         body_mako_tpl = mako_template(template)
313         helper = WebKitHelper(cursor, uid, report_xml.id, context)
314         self.parser_instance.localcontext['helper'] = helper
315         self.parser_instance.localcontext['css'] = css
316         self.parser_instance.localcontext['_'] = self.translate_call
317
318         # apply extender functions
319         additional = {}
320         if xml_id in _extender_functions:
321             for fct in _extender_functions[xml_id]:
322                 fct(pool, cr, uid, self.parser_instance.localcontext, context)
323
324         if report_xml.precise_mode:
325             ctx = dict(self.parser_instance.localcontext)
326             for obj in self.parser_instance.localcontext['objects']:
327                 ctx['objects'] = [obj]
328                 try :
329                     html = body_mako_tpl.render(dict(ctx))
330                     htmls.append(html)
331                 except Exception, e:
332                     msg = u"%s" % e
333                     _logger.error(msg)
334                     raise except_osv(_('Webkit render!'), msg)
335         else:
336             try :
337                 html = body_mako_tpl.render(dict(self.parser_instance.localcontext))
338                 htmls.append(html)
339             except Exception, e:
340                 msg = u"%s" % e
341                 _logger.error(msg)
342                 raise except_osv(_('Webkit render!'), msg)
343         head_mako_tpl = mako_template(header)
344         try :
345             head = head_mako_tpl.render(dict(self.parser_instance.localcontext, _debug=False))
346         except Exception, e:
347             raise except_osv(_('Webkit render!'), u"%s" % e)
348         foot = False
349         if footer :
350             foot_mako_tpl = mako_template(footer)
351             try :
352                 foot = foot_mako_tpl.render(dict({},
353                                             **self.parser_instance.localcontext))
354             except Exception, e:
355                 msg = u"%s" % e
356                 _logger.error(msg)
357                 raise except_osv(_('Webkit render!'), msg)
358         if report_xml.webkit_debug :
359             try :
360                 deb = head_mako_tpl.render(dict(self.parser_instance.localcontext, _debug=tools.ustr("\n".join(htmls))))
361             except Exception, e:
362                 msg = u"%s" % e
363                 _logger.error(msg)
364                 raise except_osv(_('Webkit render!'), msg)
365             return (deb, 'html')
366         bin = self.get_lib(cursor, uid)
367         pdf = self.generate_pdf(bin, report_xml, head, foot, htmls)
368         return (pdf, 'pdf')
369
370     def create(self, cursor, uid, ids, data, context=None):
371         """We override the create function in order to handle generator
372            Code taken from report openoffice. Thanks guys :) """
373         pool = openerp.registry(cursor.dbname)
374         ir_obj = pool['ir.actions.report.xml']
375         report_xml_ids = ir_obj.search(cursor, uid,
376                 [('report_name', '=', self.name[7:])], context=context)
377         if report_xml_ids:
378
379             report_xml = ir_obj.browse(cursor,
380                                        uid,
381                                        report_xml_ids[0],
382                                        context=context)
383             report_xml.report_rml = None
384             report_xml.report_rml_content = None
385             report_xml.report_sxw_content_data = None
386             report_xml.report_sxw_content = None
387             report_xml.report_sxw = None
388         else:
389             return super(WebKitParser, self).create(cursor, uid, ids, data, context)
390         if report_xml.report_type != 'webkit':
391             return super(WebKitParser, self).create(cursor, uid, ids, data, context)
392         result = self.create_source_pdf(cursor, uid, ids, data, report_xml, context)
393         if not result:
394             return (False,False)
395         return result
396
397     def _sanitize_html(self, html):
398         """wkhtmltopdf expects the html page to declare a doctype.
399         """
400         if html and html[:9].upper() != "<!DOCTYPE":
401             html = "<!DOCTYPE html>\n" + html
402         return html
403
404 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: