[IMP] Added the route converting all reports to pdf thanks to wkhtmltopdf. The html rendered report is downloaded via werkzeug.test.client and then parsed into an lxml.etree in order to extract only the useful data : local css, header, content and footer. We then generate a minimalist html page that is passed to wkhtmltopdf. Save in attachment feature is handled. A method transform a paperformat object into a list of parameters for wkhtmltopdf. Multiple IDS reports are generated in different pdf merged at the end.
bzr revid: openerp-sle@openerp-sle.home-20140212180934-dupp8x2ivo52uzib
This commit is contained in:
parent
5fa1be1303
commit
8dbd892d8b
|
@ -19,10 +19,22 @@
|
||||||
#
|
#
|
||||||
##############################################################################
|
##############################################################################
|
||||||
|
|
||||||
|
from openerp.osv.osv import except_osv
|
||||||
from openerp.addons.web import http
|
from openerp.addons.web import http
|
||||||
|
from openerp.tools.translate import _
|
||||||
from openerp.addons.web.http import request
|
from openerp.addons.web.http import request
|
||||||
|
|
||||||
|
import time
|
||||||
|
import base64
|
||||||
import logging
|
import logging
|
||||||
|
import tempfile
|
||||||
|
import lxml.html
|
||||||
|
import subprocess
|
||||||
|
|
||||||
|
from pyPdf import PdfFileWriter, PdfFileReader
|
||||||
|
from werkzeug.test import Client
|
||||||
|
from werkzeug.wrappers import BaseResponse
|
||||||
|
from werkzeug.datastructures import Headers
|
||||||
|
|
||||||
|
|
||||||
_logger = logging.getLogger(__name__)
|
_logger = logging.getLogger(__name__)
|
||||||
|
@ -67,6 +79,310 @@ class Report(http.Controller):
|
||||||
return request.registry['report'].render(request.cr, request.uid, [], report.report_file,
|
return request.registry['report'].render(request.cr, request.uid, [], report.report_file,
|
||||||
docargs, context=request.context)
|
docargs, context=request.context)
|
||||||
|
|
||||||
|
@http.route(['/report/pdf/<path:path>'], type='http', auth="user", website=True)
|
||||||
|
def report_pdf(self, path=None, landscape=False, **post):
|
||||||
|
cr, uid, context = request.cr, request.uid, request.context
|
||||||
|
|
||||||
|
# Get the report we are working on.
|
||||||
|
# Pattern is /report/module.reportname(?a=1)
|
||||||
|
reportname_in_path = path.split('/')[1].split('?')[0]
|
||||||
|
report = self._get_report_from_name(reportname_in_path)
|
||||||
|
|
||||||
|
# Check attachment_use field. If set to true and an existing pdf is already saved, return
|
||||||
|
# this one now. If not, mark save it.
|
||||||
|
save_in_attachment = {}
|
||||||
|
|
||||||
|
if report.attachment_use is True:
|
||||||
|
# Get the record ids we are working on.
|
||||||
|
path_ids = [int(i) for i in path.split('/')[2].split('?')[0].split(',')]
|
||||||
|
|
||||||
|
save_in_attachment['model'] = report.model
|
||||||
|
save_in_attachment['loaded_documents'] = {}
|
||||||
|
|
||||||
|
for path_id in path_ids:
|
||||||
|
obj = request.registry[report.model].browse(cr, uid, path_id)
|
||||||
|
filename = eval(report.attachment, {'object': obj, 'time': time})
|
||||||
|
|
||||||
|
if filename is False: # May be false if, for instance, the record is in draft state
|
||||||
|
continue
|
||||||
|
else:
|
||||||
|
alreadyindb = [('datas_fname', '=', filename),
|
||||||
|
('res_model', '=', report.model),
|
||||||
|
('res_id', '=', path_id)]
|
||||||
|
|
||||||
|
attach_ids = request.registry['ir.attachment'].search(cr, uid, alreadyindb)
|
||||||
|
if attach_ids:
|
||||||
|
# Add the loaded pdf in the loaded_documents list
|
||||||
|
pdf = request.registry['ir.attachment'].browse(cr, uid, attach_ids[0]).datas
|
||||||
|
pdf = base64.decodestring(pdf)
|
||||||
|
save_in_attachment['loaded_documents'][path_id] = pdf
|
||||||
|
_logger.info('The PDF document %s was loaded from the database' % filename)
|
||||||
|
else:
|
||||||
|
# Mark current document to be saved
|
||||||
|
save_in_attachment[path_id] = filename
|
||||||
|
|
||||||
|
# Get the paperformat associated to the report, if there is.
|
||||||
|
if not report.paperformat_id:
|
||||||
|
user = request.registry['res.users'].browse(cr, uid, uid, context=context)
|
||||||
|
paperformat = user.company_id.paperformat_id
|
||||||
|
else:
|
||||||
|
paperformat = report.paperformat_id
|
||||||
|
|
||||||
|
# Get the html report.
|
||||||
|
html = self._get_url_content('/' + path, post)
|
||||||
|
|
||||||
|
# Get some css and script in order to build a minimal html page for the report.
|
||||||
|
# This page will later be sent to wkhtmltopdf.
|
||||||
|
css = self._get_url_content('/report/static/src/css/reset.min.css')
|
||||||
|
css += self._get_url_content('/website/static/src/css/website.css')
|
||||||
|
subst = self._get_url_content('/report/static/src/js/subst.js')
|
||||||
|
|
||||||
|
headerhtml = []
|
||||||
|
contenthtml = []
|
||||||
|
footerhtml = []
|
||||||
|
minimalhtml = """
|
||||||
|
<html style="height: 0mm;">
|
||||||
|
<head>
|
||||||
|
<style type='text/css'>{0}</style>
|
||||||
|
<link rel="stylesheet" href="http://netdna.bootstrapcdn.com/bootstrap/3.1.0/css/bootstrap.min.css">
|
||||||
|
<script type='text/javascript'>{1}</script>
|
||||||
|
</head>
|
||||||
|
<body class="container" onload='subst()'>
|
||||||
|
{2}
|
||||||
|
</body>
|
||||||
|
</html>"""
|
||||||
|
|
||||||
|
# The retrieved html report must be simplified. We convert it into a xml tree
|
||||||
|
# via lxml in order to extract header, footer and all reportcontent.
|
||||||
|
try:
|
||||||
|
root = lxml.html.fromstring(html)
|
||||||
|
|
||||||
|
for node in root.xpath("//html/head/style"):
|
||||||
|
css += node.text
|
||||||
|
|
||||||
|
for node in root.xpath("//div[@class='header']"):
|
||||||
|
body = lxml.html.tostring(node)
|
||||||
|
header = minimalhtml.format(css, subst, body)
|
||||||
|
headerhtml.append(header)
|
||||||
|
|
||||||
|
for node in root.xpath("//div[@class='footer']"):
|
||||||
|
body = lxml.html.tostring(node)
|
||||||
|
footer = minimalhtml.format(css, subst, body)
|
||||||
|
footerhtml.append(footer)
|
||||||
|
|
||||||
|
for node in root.xpath("//div[@class='page']"):
|
||||||
|
# Previously, we marked some reports to be saved in attachment via their ids, so we
|
||||||
|
# must set a relation between report ids and reportcontent. We use the QWeb
|
||||||
|
# branding in order to do so: searching after a node having a data-oe-model
|
||||||
|
# attribute with the value of the current report model and read its oe-id attribute
|
||||||
|
oemodelnode = node.find(".//*[@data-oe-model='" + report.model + "']")
|
||||||
|
if oemodelnode is not None:
|
||||||
|
reportid = oemodelnode.get('data-oe-id', False)
|
||||||
|
if reportid is not False:
|
||||||
|
reportid = int(reportid)
|
||||||
|
else:
|
||||||
|
reportid = False
|
||||||
|
|
||||||
|
body = lxml.html.tostring(node)
|
||||||
|
reportcontent = minimalhtml.format(css, '', body)
|
||||||
|
contenthtml.append(tuple([reportid, reportcontent]))
|
||||||
|
|
||||||
|
except lxml.etree.XMLSyntaxError:
|
||||||
|
contenthtml = []
|
||||||
|
contenthtml.append(html)
|
||||||
|
save_in_attachment = {} # Don't save this potentially malformed document
|
||||||
|
|
||||||
|
# Get paperformat arguments set in the root html tag. They are prioritized over
|
||||||
|
# paperformat-record arguments.
|
||||||
|
specific_paperformat_args = {}
|
||||||
|
for attribute in root.items():
|
||||||
|
if attribute[0].startswith('data-report-'):
|
||||||
|
specific_paperformat_args[attribute[0]] = attribute[1]
|
||||||
|
|
||||||
|
# Execute wkhtmltopdf process.
|
||||||
|
pdf = self._generate_wkhtml_pdf(headerhtml, footerhtml, contenthtml, landscape,
|
||||||
|
paperformat, specific_paperformat_args, save_in_attachment)
|
||||||
|
|
||||||
|
return self._make_pdf_response(pdf)
|
||||||
|
|
||||||
|
def _get_url_content(self, url, post=None):
|
||||||
|
"""Resolve an internal webpage url and return its content with the help of
|
||||||
|
werkzeug.test.client.
|
||||||
|
|
||||||
|
:param url: string representinf the url to resolve
|
||||||
|
:param post: a dict representing the query string
|
||||||
|
:returns: a tuple str(html), int(statuscode)
|
||||||
|
"""
|
||||||
|
# Rebuilding the query string.
|
||||||
|
if post:
|
||||||
|
url += '?'
|
||||||
|
url += '&'.join('%s=%s' % (k, v) for (k, v) in post.iteritems())
|
||||||
|
|
||||||
|
# We have to pass the current headers in order to see the report.
|
||||||
|
reqheaders = Headers(request.httprequest.headers)
|
||||||
|
response = Client(request.httprequest.app, BaseResponse).get(url, headers=reqheaders,
|
||||||
|
follow_redirects=True)
|
||||||
|
content = response.data
|
||||||
|
|
||||||
|
try:
|
||||||
|
content = content.decode('utf-8')
|
||||||
|
except UnicodeDecodeError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
return content
|
||||||
|
|
||||||
|
def _generate_wkhtml_pdf(self, headers, footers, bodies, landscape,
|
||||||
|
paperformat, spec_paperformat_args=None, save_in_attachment=None):
|
||||||
|
"""Execute wkhtmltopdf as a subprocess in order to convert html given in input into a pdf
|
||||||
|
document.
|
||||||
|
|
||||||
|
:param header: list of string containing the headers
|
||||||
|
:param footer: list of string containing the footers
|
||||||
|
:param bodies: list of string containing the reports
|
||||||
|
:param landscape: boolean to force the pdf to be rendered under a landscape format
|
||||||
|
:param paperformat: ir.actions.report.paperformat to generate the wkhtmltopf arguments
|
||||||
|
:param specific_paperformat_args: dict of prioritized paperformat arguments
|
||||||
|
:param save_in_attachment: dict of reports to save/load in/from the db
|
||||||
|
:returns: Content of the pdf as a string
|
||||||
|
"""
|
||||||
|
command = ['wkhtmltopdf-0.12']
|
||||||
|
tmp_dir = tempfile.gettempdir()
|
||||||
|
|
||||||
|
# Display arguments
|
||||||
|
command_args = []
|
||||||
|
|
||||||
|
if paperformat:
|
||||||
|
command_args.extend(self._build_wkhtmltopdf_args(paperformat, spec_paperformat_args))
|
||||||
|
|
||||||
|
if landscape and '--orientation' in command_args:
|
||||||
|
command_args_copy = list(command_args)
|
||||||
|
for index, elem in enumerate(command_args_copy):
|
||||||
|
if elem == '--orientation':
|
||||||
|
del command_args[index]
|
||||||
|
del command_args[index]
|
||||||
|
command_args.extend(['--orientation', 'landscape'])
|
||||||
|
elif landscape and not '--orientation' in command_args:
|
||||||
|
command_args.extend(['--orientation', 'landscape'])
|
||||||
|
|
||||||
|
pdfdocuments = []
|
||||||
|
# HTML to PDF thanks to WKhtmltopdf
|
||||||
|
for index, reporthtml in enumerate(bodies):
|
||||||
|
command_arg_local = []
|
||||||
|
pdfreport = tempfile.NamedTemporaryFile(suffix='.pdf', prefix='report.tmp.',
|
||||||
|
mode='w+b')
|
||||||
|
# Directly load the document if we have it
|
||||||
|
if save_in_attachment and save_in_attachment['loaded_documents'].get(reporthtml[0]):
|
||||||
|
pdfreport.write(save_in_attachment['loaded_documents'].get(reporthtml[0]))
|
||||||
|
pdfreport.seek(0)
|
||||||
|
pdfdocuments.append(pdfreport)
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Header stuff
|
||||||
|
if headers:
|
||||||
|
head_file = tempfile.NamedTemporaryFile(suffix='.html', prefix='report.header.tmp.',
|
||||||
|
dir=tmp_dir, mode='w+')
|
||||||
|
head_file.write(headers[index])
|
||||||
|
head_file.seek(0)
|
||||||
|
command_arg_local.extend(['--header-html', head_file.name])
|
||||||
|
|
||||||
|
# Footer stuff
|
||||||
|
if footers:
|
||||||
|
foot_file = tempfile.NamedTemporaryFile(suffix='.html', prefix='report.footer.tmp.',
|
||||||
|
dir=tmp_dir, mode='w+')
|
||||||
|
foot_file.write(footers[index])
|
||||||
|
foot_file.seek(0)
|
||||||
|
command_arg_local.extend(['--footer-html', foot_file.name])
|
||||||
|
|
||||||
|
# Body stuff
|
||||||
|
content_file = tempfile.NamedTemporaryFile(suffix='.html', prefix='report.body.tmp.',
|
||||||
|
dir=tmp_dir, mode='w+')
|
||||||
|
content_file.write(reporthtml[1])
|
||||||
|
content_file.seek(0)
|
||||||
|
|
||||||
|
try:
|
||||||
|
wkhtmltopdf = command + command_args + command_arg_local
|
||||||
|
wkhtmltopdf += [content_file.name] + [pdfreport.name]
|
||||||
|
|
||||||
|
process = subprocess.Popen(wkhtmltopdf, stdout=subprocess.PIPE,
|
||||||
|
stderr=subprocess.PIPE)
|
||||||
|
out, err = process.communicate()
|
||||||
|
|
||||||
|
if process.returncode != 0:
|
||||||
|
raise except_osv(_('Report (PDF)'),
|
||||||
|
_('wkhtmltopdf-patched failed with error code = %s. '
|
||||||
|
'Message: %s') % (str(process.returncode), err))
|
||||||
|
|
||||||
|
# Save the pdf in attachment if marked
|
||||||
|
if reporthtml[0] is not False and save_in_attachment.get(reporthtml[0]):
|
||||||
|
attachment = {
|
||||||
|
'name': save_in_attachment.get(reporthtml[0]),
|
||||||
|
'datas': base64.encodestring(pdfreport.read()),
|
||||||
|
'datas_fname': save_in_attachment.get(reporthtml[0]),
|
||||||
|
'res_model': save_in_attachment.get('model'),
|
||||||
|
'res_id': reporthtml[0],
|
||||||
|
}
|
||||||
|
request.registry['ir.attachment'].create(request.cr, request.uid, attachment)
|
||||||
|
_logger.info('The PDF document %s is now saved in the '
|
||||||
|
'database' % attachment['name'])
|
||||||
|
|
||||||
|
pdfreport.seek(0)
|
||||||
|
pdfdocuments.append(pdfreport)
|
||||||
|
|
||||||
|
if headers:
|
||||||
|
head_file.close()
|
||||||
|
if footers:
|
||||||
|
foot_file.close()
|
||||||
|
except:
|
||||||
|
raise
|
||||||
|
|
||||||
|
# Get and return the full pdf
|
||||||
|
if len(pdfdocuments) == 1:
|
||||||
|
content = pdfdocuments[0].read()
|
||||||
|
pdfdocuments[0].close()
|
||||||
|
else:
|
||||||
|
content = self._merge_pdf(pdfdocuments)
|
||||||
|
|
||||||
|
return content
|
||||||
|
|
||||||
|
def _build_wkhtmltopdf_args(self, paperformat, specific_paperformat_args=None):
|
||||||
|
"""Build arguments understandable by wkhtmltopdf from an ir.actions.report.paperformat
|
||||||
|
record.
|
||||||
|
|
||||||
|
:paperformat: ir.actions.report.paperformat record associated to a document
|
||||||
|
:specific_paperformat_args: a dict containing prioritized wkhtmltopdf arguments
|
||||||
|
:returns: list of string containing the wkhtmltopdf arguments
|
||||||
|
"""
|
||||||
|
command_args = []
|
||||||
|
if paperformat.format and paperformat.format != 'custom':
|
||||||
|
command_args.extend(['--page-size', paperformat.format])
|
||||||
|
|
||||||
|
if paperformat.page_height and paperformat.page_width and paperformat.format == 'custom':
|
||||||
|
command_args.extend(['--page-width', str(paperformat.page_width) + 'in'])
|
||||||
|
command_args.extend(['--page-height', str(paperformat.page_height) + 'in'])
|
||||||
|
|
||||||
|
if specific_paperformat_args and specific_paperformat_args['data-report-margin-top']:
|
||||||
|
command_args.extend(['--margin-top',
|
||||||
|
str(specific_paperformat_args['data-report-margin-top'])])
|
||||||
|
else:
|
||||||
|
command_args.extend(['--margin-top', str(paperformat.margin_top)])
|
||||||
|
|
||||||
|
if paperformat.margin_left:
|
||||||
|
command_args.extend(['--margin-left', str(paperformat.margin_left)])
|
||||||
|
if paperformat.margin_bottom:
|
||||||
|
command_args.extend(['--margin-bottom', str(paperformat.margin_bottom)])
|
||||||
|
if paperformat.margin_right:
|
||||||
|
command_args.extend(['--margin-right', str(paperformat.margin_right)])
|
||||||
|
if paperformat.orientation:
|
||||||
|
command_args.extend(['--orientation', str(paperformat.orientation)])
|
||||||
|
if paperformat.header_spacing:
|
||||||
|
command_args.extend(['--header-spacing', str(paperformat.header_spacing)])
|
||||||
|
if paperformat.header_line:
|
||||||
|
command_args.extend(['--header-line'])
|
||||||
|
if paperformat.dpi:
|
||||||
|
command_args.extend(['--dpi', str(paperformat.dpi)])
|
||||||
|
|
||||||
|
return command_args
|
||||||
|
|
||||||
def _get_report_from_name(self, report_name):
|
def _get_report_from_name(self, report_name):
|
||||||
"""Get the first record of ir.actions.report.xml having the argument as value for
|
"""Get the first record of ir.actions.report.xml having the argument as value for
|
||||||
the field report_name.
|
the field report_name.
|
||||||
|
@ -81,3 +397,33 @@ class Report(http.Controller):
|
||||||
report = report_obj.browse(request.cr, request.uid, idreport[0],
|
report = report_obj.browse(request.cr, request.uid, idreport[0],
|
||||||
context=request.context)
|
context=request.context)
|
||||||
return report
|
return report
|
||||||
|
|
||||||
|
def _make_pdf_response(self, pdf):
|
||||||
|
"""Make a request response for a PDF file with correct http headers.
|
||||||
|
|
||||||
|
:param pdf: content of a pdf in a string
|
||||||
|
:returns: request response for a pdf document
|
||||||
|
"""
|
||||||
|
pdfhttpheaders = [('Content-Type', 'application/pdf'),
|
||||||
|
('Content-Length', len(pdf))]
|
||||||
|
return request.make_response(pdf, headers=pdfhttpheaders)
|
||||||
|
|
||||||
|
def _merge_pdf(self, documents):
|
||||||
|
"""Merge PDF files into one.
|
||||||
|
|
||||||
|
:param documents: list of pdf files
|
||||||
|
:returns: string containing the merged pdf
|
||||||
|
"""
|
||||||
|
writer = PdfFileWriter()
|
||||||
|
for document in documents:
|
||||||
|
reader = PdfFileReader(file(document.name, "rb"))
|
||||||
|
for page in range(0, reader.getNumPages()):
|
||||||
|
writer.addPage(reader.getPage(page))
|
||||||
|
document.close()
|
||||||
|
|
||||||
|
merged = tempfile.NamedTemporaryFile(suffix='.pdf', prefix='report.tmp.', mode='w+b')
|
||||||
|
writer.write(merged)
|
||||||
|
merged.seek(0)
|
||||||
|
content = merged.read()
|
||||||
|
merged.close()
|
||||||
|
return content
|
||||||
|
|
|
@ -0,0 +1,2 @@
|
||||||
|
/* reset5 2011 opensource.736cs.com MIT - https://code.google.com/p/reset5 */
|
||||||
|
html,body,div,span,applet,object,iframe,h1,h2,h3,h4,h5,h6,p,blockquote,pre,a,abbr,acronym,address,big,cite,code,del,dfn,em,font,img,ins,kbd,q,s,samp,small,strike,strong,sub,sup,tt,var,b,i,center,dl,dt,dd,ol,ul,li,fieldset,form,label,legend,table,caption,tbody,tfoot,thead,tr,th,td,article,aside,audio,canvas,details,figcaption,figure,footer,header,hgroup,mark,menu,meter,nav,output,progress,section,summary,time,video{border:0;outline:0;font-size:100%;vertical-align:baseline;background:transparent;margin:0;padding:0;}body{line-height:1;}article,aside,dialog,figure,footer,header,hgroup,nav,section,blockquote{display:block;}nav ul{list-style:none;}ol{list-style:decimal;}ul{list-style:disc;}ul ul{list-style:circle;}blockquote,q{quotes:none;}blockquote:before,blockquote:after,q:before,q:after{content:none;}ins{text-decoration:underline;}del{text-decoration:line-through;}mark{background:none;}abbr[title],dfn[title]{border-bottom:1px dotted #000;cursor:help;}table{border-collapse:collapse;border-spacing:0;}hr{display:block;height:1px;border:0;border-top:1px solid #ccc;margin:1em 0;padding:0;}input[type=submit],input[type=button],button{margin:0!important;padding:0!important;}input,select,a img{vertical-align:middle;}
|
|
@ -0,0 +1,14 @@
|
||||||
|
function subst() {
|
||||||
|
var vars = {};
|
||||||
|
var x = document.location.search.substring(1).split('&');
|
||||||
|
for (var i in x) {
|
||||||
|
var z = x[i].split('=', 2);
|
||||||
|
vars[z[0]] = unescape(z[1]);
|
||||||
|
}
|
||||||
|
var x=['frompage', 'topage', 'page', 'webpage', 'section', 'subsection', 'subsubsection'];
|
||||||
|
for (var i in x) {
|
||||||
|
var y = document.getElementsByClassName(x[i]);
|
||||||
|
for (var j=0; j<y.length; ++j)
|
||||||
|
y[j].textContent = vars[x[i]];
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue