Merge remote-tracking branch 'odoo/7.0' into 7.0
[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 mako.template import Template
43 from mako.lookup import TemplateLookup
44 from mako import exceptions
45
46 from openerp import netsvc
47 from openerp import pooler
48 from report_helper import WebKitHelper
49 from openerp.report.report_sxw import *
50 from openerp import addons
51 from openerp import tools
52 from openerp.tools.translate import _
53 from openerp.osv.osv import except_osv
54
55 _logger = logging.getLogger(__name__)
56
57 def mako_template(text):
58     """Build a Mako template.
59
60     This template uses UTF-8 encoding
61     """
62     tmp_lookup  = TemplateLookup() #we need it in order to allow inclusion and inheritance
63     return Template(text, input_encoding='utf-8', output_encoding='utf-8', lookup=tmp_lookup)
64
65
66 class WebKitParser(report_sxw):
67     """Custom class that use webkit to render HTML reports
68        Code partially taken from report openoffice. Thanks guys :)
69     """
70     def __init__(self, name, table, rml=False, parser=False,
71         header=True, store=False):
72         self.localcontext = {}
73         report_sxw.__init__(self, name, table, rml, parser,
74             header, store)
75
76     def get_lib(self, cursor, uid):
77         """Return the lib wkhtml path"""
78         proxy = self.pool.get('ir.config_parameter')
79         webkit_path = proxy.get_param(cursor, uid, 'webkit_path')
80
81         if not webkit_path:
82             try:
83                 defpath = os.environ.get('PATH', os.defpath).split(os.pathsep)
84                 if hasattr(sys, 'frozen'):
85                     defpath.append(os.getcwd())
86                     if tools.config['root_path']:
87                         defpath.append(os.path.dirname(tools.config['root_path']))
88                 webkit_path = tools.which('wkhtmltopdf', path=os.pathsep.join(defpath))
89             except IOError:
90                 webkit_path = None
91
92         if webkit_path:
93             return webkit_path
94
95         raise except_osv(
96                          _('Wkhtmltopdf library path is not set'),
97                          _('Please install executable on your system' \
98                          ' (sudo apt-get install wkhtmltopdf) or download it from here:' \
99                          ' http://code.google.com/p/wkhtmltopdf/downloads/list and set the' \
100                          ' path in the ir.config_parameter with the webkit_path key.' \
101                          'Minimal version is 0.9.9')
102                         )
103
104     def generate_pdf(self, comm_path, report_xml, header, footer, html_list, webkit_header=False):
105         """Call webkit in order to generate pdf"""
106         if not webkit_header:
107             webkit_header = report_xml.webkit_header
108         fd, out_filename = tempfile.mkstemp(suffix=".pdf",
109                                             prefix="webkit.tmp.")
110         file_to_del = [out_filename]
111         if comm_path:
112             command = [comm_path]
113         else:
114             command = ['wkhtmltopdf']
115
116         command.append('--quiet')
117         # default to UTF-8 encoding.  Use <meta charset="latin-1"> to override.
118         command.extend(['--encoding', 'utf-8'])
119         if header :
120             with tempfile.NamedTemporaryFile(suffix=".head.html",
121                                              delete=False) as head_file:
122                 head_file.write(self._sanitize_html(header))
123             file_to_del.append(head_file.name)
124             command.extend(['--header-html', head_file.name])
125         if footer :
126             with tempfile.NamedTemporaryFile(suffix=".foot.html",
127                                              delete=False) as foot_file:
128                 foot_file.write(self._sanitize_html(footer))
129             file_to_del.append(foot_file.name)
130             command.extend(['--footer-html', foot_file.name])
131
132         if webkit_header.margin_top :
133             command.extend(['--margin-top', str(webkit_header.margin_top).replace(',', '.')])
134         if webkit_header.margin_bottom :
135             command.extend(['--margin-bottom', str(webkit_header.margin_bottom).replace(',', '.')])
136         if webkit_header.margin_left :
137             command.extend(['--margin-left', str(webkit_header.margin_left).replace(',', '.')])
138         if webkit_header.margin_right :
139             command.extend(['--margin-right', str(webkit_header.margin_right).replace(',', '.')])
140         if webkit_header.orientation :
141             command.extend(['--orientation', str(webkit_header.orientation).replace(',', '.')])
142         if webkit_header.format :
143             command.extend(['--page-size', str(webkit_header.format).replace(',', '.')])
144         count = 0
145         for html in html_list :
146             with tempfile.NamedTemporaryFile(suffix="%d.body.html" %count,
147                                              delete=False) as html_file:
148                 count += 1
149                 html_file.write(self._sanitize_html(html))
150             file_to_del.append(html_file.name)
151             command.append(html_file.name)
152         command.append(out_filename)
153         stderr_fd, stderr_path = tempfile.mkstemp(text=True)
154         file_to_del.append(stderr_path)
155         try:
156             status = subprocess.call(command, stderr=stderr_fd)
157             os.close(stderr_fd) # ensure flush before reading
158             stderr_fd = None # avoid closing again in finally block
159             fobj = open(stderr_path, 'r')
160             error_message = fobj.read()
161             fobj.close()
162             if not error_message:
163                 error_message = _('No diagnosis message was provided')
164             else:
165                 error_message = _('The following diagnosis message was provided:\n') + error_message
166             if status :
167                 raise except_osv(_('Webkit error' ),
168                                  _("The command 'wkhtmltopdf' failed with error code = %s. Message: %s") % (status, error_message))
169             with open(out_filename, 'rb') as pdf_file:
170                 pdf = pdf_file.read()
171             os.close(fd)
172         finally:
173             if stderr_fd is not None:
174                 os.close(stderr_fd)
175             for f_to_del in file_to_del:
176                 try:
177                     os.unlink(f_to_del)
178                 except (OSError, IOError), exc:
179                     _logger.error('cannot remove file %s: %s', f_to_del, exc)
180         return pdf
181
182     def translate_call(self, parser_instance, src):
183         """Translate String."""
184         ir_translation = self.pool['ir.translation']
185         name = self.tmpl and 'addons/' + self.tmpl or None
186         res = ir_translation._get_source(parser_instance.cr, parser_instance.uid,
187                                          name, 'report', parser_instance.localcontext.get('lang', 'en_US'), src)
188         if res == src:
189             # no translation defined, fallback on None (backward compatibility)
190             res = ir_translation._get_source(parser_instance.cr, parser_instance.uid,
191                                              None, 'report', parser_instance.localcontext.get('lang', 'en_US'), src)
192         if not res :
193             return src
194         return res
195
196     # override needed to keep the attachments storing procedure
197     def create_single_pdf(self, cursor, uid, ids, data, report_xml, context=None):
198         """generate the PDF"""
199
200         if context is None:
201             context={}
202         htmls = []
203         if report_xml.report_type != 'webkit':
204             return super(WebKitParser,self).create_single_pdf(cursor, uid, ids, data, report_xml, context=context)
205
206         parser_instance = self.parser(cursor,
207                                       uid,
208                                       self.name2,
209                                       context=context)
210
211         self.pool = pooler.get_pool(cursor.dbname)
212         objs = self.getObjects(cursor, uid, ids, context)
213         parser_instance.set_context(objs, data, ids, report_xml.report_type)
214
215         template =  False
216
217         if report_xml.report_file :
218             # backward-compatible if path in Windows format
219             report_path = report_xml.report_file.replace("\\", "/")
220             path = addons.get_module_resource(*report_path.split('/'))
221             if path and os.path.exists(path) :
222                 template = file(path).read()
223         if not template and report_xml.report_webkit_data :
224             template =  report_xml.report_webkit_data
225         if not template :
226             raise except_osv(_('Error!'), _('Webkit report template not found!'))
227         header = report_xml.webkit_header.html
228         footer = report_xml.webkit_header.footer_html
229         if not header and report_xml.header:
230             raise except_osv(
231                   _('No header defined for this Webkit report!'),
232                   _('Please set a header in company settings.')
233               )
234         if not report_xml.header :
235             header = ''
236             default_head = addons.get_module_resource('report_webkit', 'default_header.html')
237             with open(default_head,'r') as f:
238                 header = f.read()
239         css = report_xml.webkit_header.css
240         if not css :
241             css = ''
242
243         translate_call = partial(self.translate_call, parser_instance)
244         #default_filters=['unicode', 'entity'] can be used to set global filter
245         body_mako_tpl = mako_template(template)
246         helper = WebKitHelper(cursor, uid, report_xml.id, context)
247         if report_xml.precise_mode:
248             objs = parser_instance.localcontext['objects']
249             for obj in objs:
250                 parser_instance.localcontext['objects'] = [obj]
251                 try :
252                     html = body_mako_tpl.render(helper=helper,
253                                                 css=css,
254                                                 _=translate_call,
255                                                 **parser_instance.localcontext)
256                     htmls.append(html)
257                 except Exception:
258                     msg = exceptions.text_error_template().render()
259                     _logger.error(msg)
260                     raise except_osv(_('Webkit render!'), msg)
261         else:
262             try :
263                 html = body_mako_tpl.render(helper=helper,
264                                             css=css,
265                                             _=translate_call,
266                                             **parser_instance.localcontext)
267                 htmls.append(html)
268             except Exception:
269                 msg = exceptions.text_error_template().render()
270                 _logger.error(msg)
271                 raise except_osv(_('Webkit render!'), msg)
272         head_mako_tpl = mako_template(header)
273         try :
274             head = head_mako_tpl.render(helper=helper,
275                                         css=css,
276                                         _=translate_call,
277                                         _debug=False,
278                                         **parser_instance.localcontext)
279         except Exception:
280             raise except_osv(_('Webkit render!'),
281                 exceptions.text_error_template().render())
282         foot = False
283         if footer :
284             foot_mako_tpl = mako_template(footer)
285             try :
286                 foot = foot_mako_tpl.render(helper=helper,
287                                             css=css,
288                                             _=translate_call,
289                                             **parser_instance.localcontext)
290             except:
291                 msg = exceptions.text_error_template().render()
292                 _logger.error(msg)
293                 raise except_osv(_('Webkit render!'), msg)
294         if report_xml.webkit_debug :
295             try :
296                 deb = head_mako_tpl.render(helper=helper,
297                                            css=css,
298                                            _debug=tools.ustr("\n".join(htmls)),
299                                            _=translate_call,
300                                            **parser_instance.localcontext)
301             except Exception:
302                 msg = exceptions.text_error_template().render()
303                 _logger.error(msg)
304                 raise except_osv(_('Webkit render!'), msg)
305             return (deb, 'html')
306         bin = self.get_lib(cursor, uid)
307         pdf = self.generate_pdf(bin, report_xml, head, foot, htmls)
308         return (pdf, 'pdf')
309
310     def create(self, cursor, uid, ids, data, context=None):
311         """We override the create function in order to handle generator
312            Code taken from report openoffice. Thanks guys :) """
313         pool = pooler.get_pool(cursor.dbname)
314         ir_obj = pool.get('ir.actions.report.xml')
315         report_xml_ids = ir_obj.search(cursor, uid,
316                 [('report_name', '=', self.name[7:])], context=context)
317         if report_xml_ids:
318
319             report_xml = ir_obj.browse(cursor,
320                                        uid,
321                                        report_xml_ids[0],
322                                        context=context)
323             report_xml.report_rml = None
324             report_xml.report_rml_content = None
325             report_xml.report_sxw_content_data = None
326             report_xml.report_sxw_content = None
327             report_xml.report_sxw = None
328         else:
329             return super(WebKitParser, self).create(cursor, uid, ids, data, context)
330         if report_xml.report_type != 'webkit':
331             return super(WebKitParser, self).create(cursor, uid, ids, data, context)
332         result = self.create_source_pdf(cursor, uid, ids, data, report_xml, context)
333         if not result:
334             return (False,False)
335         return result
336
337     def _sanitize_html(self, html):
338         """wkhtmltopdf expects the html page to declare a doctype.
339         """
340         if html and html[:9].upper() != "<!DOCTYPE":
341             html = "<!DOCTYPE html>\n" + html
342         return html
343
344 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: