@ -1,24 +1,29 @@
# -*- coding: utf-8 -*-
# -*- coding: utf-8 -*-
# Copyright 2013 XCG Consulting (http://odoo.consulting)
# Copyright 2013 XCG Consulting (http://odoo.consulting)
# Copyright 2016 ACSONE SA/NV
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl)
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl)
import base64
from base64 import b64decode
from cStringIO import StringIO
from cStringIO import StringIO
import json
import json
import pkg_resources
import logging
import os
import os
import sys
from base64 import b64decode
import pkg_resources
import requests
import requests
import sys
from tempfile import NamedTemporaryFile
from tempfile import NamedTemporaryFile
from odoo import api , _
from odoo import exceptions
from odoo import exceptions
from odoo.report.report_sxw import report_sxw
from odoo.report.report_sxw import report_sxw
import logging
import logging
from zipfile import ZipFile , ZIP_DEFLATED
from openerp import api , fields , models , _
logger = logging . getLogger ( __name__ )
logger = logging . getLogger ( __name__ )
try :
try :
from py3o.template.helpers import Py3oConvertor
from py3o.template.helpers import Py3oConvertor
from py3o.template import Template
from py3o.template import Template
from py3o import formats
except ImportError :
except ImportError :
logger . debug ( ' Cannot import py3o.template ' )
logger . debug ( ' Cannot import py3o.template ' )
try :
try :
@ -64,11 +69,18 @@ def defautl_extend(report_xml, localcontext):
localcontext [ ' report_xml ' ] = report_xml
localcontext [ ' report_xml ' ] = report_xml
class Py3oParser ( report_sxw ) :
""" Custom class that use Py3o to render libroffice reports.
Code partially taken from CampToCamp ' s webkit_report. " " "
class Py3oReport ( models . TransientModel ) :
_name = " py3o.report "
_inherit = ' report '
_description = " Report Py30 "
ir_actions_report_xml_id = fields . Many2one (
comodel_name = " ir.actions.report.xml " ,
required = True
)
def get_template ( self , report_obj ) :
@api.multi
def get_template ( self ) :
""" private helper to fetch the template data either from the database
""" private helper to fetch the template data either from the database
or from the default template file provided by the implementer.
or from the default template file provided by the implementer.
@ -76,30 +88,27 @@ class Py3oParser(report_sxw):
to try and fetch the report template from database. If not found it
to try and fetch the report template from database. If not found it
will fallback to the template file referenced in the report definition .
will fallback to the template file referenced in the report definition .
@param report_obj : a recordset representing the report defintion
@type report_obj : odoo . model . recordset instance
@returns : string or buffer containing the template data
@returns : string or buffer containing the template data
@raises : TemplateNotFound which is a subclass of
@raises : TemplateNotFound which is a subclass of
odoo . exceptions . DeferredException
odoo . exceptions . DeferredException
"""
"""
self . ensure_one ( )
tmpl_data = None
tmpl_data = None
if report_obj . py3o_template_id and report_obj . py3o_template_id . id :
report_xml = self . ir_actions_report_xml_id
if report_xml . py3o_template_id and report_xml . py3o_template_id . id :
# if a user gave a report template
# if a user gave a report template
tmpl_data = b64decode (
tmpl_data = b64decode (
report_obj . py3o_template_id . py3o_template_data
report_xml . py3o_template_id . py3o_template_data
)
)
elif report_obj . py3o_template_fallback :
tmpl_name = report_obj . py3o_template_fallback
elif report_xml . py3o_template_fallback :
tmpl_name = report_xml . py3o_template_fallback
flbk_filename = None
flbk_filename = None
if report_obj . module :
if report_xml . module :
# if the default is defined
# if the default is defined
flbk_filename = pkg_resources . resource_filename (
flbk_filename = pkg_resources . resource_filename (
" odoo.addons. %s " % report_obj . module ,
" odoo.addons. %s " % report_xml . module ,
tmpl_name ,
tmpl_name ,
)
)
elif os . path . isabs ( tmpl_name ) :
elif os . path . isabs ( tmpl_name ) :
@ -119,37 +128,54 @@ class Py3oParser(report_sxw):
return tmpl_data
return tmpl_data
def _extend_parser_context ( self , parser_instance , report_xml ) :
@api.multi
def _extend_parser_context ( self , context_instance , report_xml ) :
# add default extenders
# add default extenders
for fct in _extender_functions . get ( None , [ ] ) :
for fct in _extender_functions . get ( None , [ ] ) :
fct ( report_xml , parser _instance. localcontext )
fct ( report_xml , context _instance. localcontext )
# add extenders for registered on the template
# add extenders for registered on the template
xml_id = report_xml . get_external_id ( ) . get ( report_xml . id )
xml_id = report_xml . get_external_id ( ) . get ( report_xml . id )
if xml_id in _extender_functions :
if xml_id in _extender_functions :
for fct in _extender_functions [ xml_id ] :
for fct in _extender_functions [ xml_id ] :
fct ( report_xml , parser_instance . localcontext )
fct ( report_xml , context_instance . localcontext )
@api.multi
def _get_parser_context ( self , model_instance , data ) :
report_xml = self . ir_actions_report_xml_id
context_instance = rml_parse ( self . env . cr , self . env . uid ,
report_xml . name ,
context = self . env . context )
context_instance . set_context ( model_instance , data , model_instance . ids ,
report_xml . report_type )
self . _extend_parser_context ( context_instance , report_xml )
return context_instance . localcontext
@api.multi
def _postprocess_report ( self , content , res_id , save_in_attachment ) :
if save_in_attachment . get ( res_id ) :
attachment = {
' name ' : save_in_attachment . get ( res_id ) ,
' datas ' : base64 . encodestring ( content ) ,
' datas_fname ' : save_in_attachment . get ( res_id ) ,
' res_model ' : save_in_attachment . get ( ' model ' ) ,
' res_id ' : res_id ,
}
return self . env [ ' ir.attachment ' ] . create ( attachment )
return False
def create_single_pdf ( self , cr , uid , ids , data , report_xml , context = None ) :
""" Overide this function to generate our py3o report
@api.multi
def _create_single_report ( self , model_instance , data , save_in_attachment ) :
""" This function to generate our py3o report
"""
"""
if report_xml . report_type != ' py3o ' :
return super ( Py3oParser , self ) . create_single_pdf (
cr , uid , ids , data , report_xml , context = context
)
parser_instance = self . parser ( cr , uid , self . name2 , context = context )
parser_instance . set_context (
self . getObjects ( cr , uid , ids , context ) ,
data , ids , report_xml . report_type
)
self . _extend_parser_context ( parser_instance , report_xml )
self . ensure_one ( )
report_xml = self . ir_actions_report_xml_id
tmpl_data = self . get_template ( report_xml )
tmpl_data = self . get_template ( )
in_stream = StringIO ( tmpl_data )
in_stream = StringIO ( tmpl_data )
out_stream = StringIO ( )
out_stream = StringIO ( )
template = Template ( in_stream , out_stream , escape_false = True )
template = Template ( in_stream , out_stream , escape_false = True )
localcontext = parser_instance . localcontext
localcontext = self . _get_parser_context ( model_instance , data )
if report_xml . py3o_is_local_fusion :
if report_xml . py3o_is_local_fusion :
template . render ( localcontext )
template . render ( localcontext )
in_stream = out_stream
in_stream = out_stream
@ -181,7 +207,7 @@ class Py3oParser(report_sxw):
report_xml . py3o_server_id . url , data = fields , files = files )
report_xml . py3o_server_id . url , data = fields , files = files )
if r . status_code != 200 :
if r . status_code != 200 :
# server says we have an issue... let's tell that to enduser
# server says we have an issue... let's tell that to enduser
raise exceptions . Warning (
raise UserError (
_ ( ' Fusion server error %s ' ) % r . text ,
_ ( ' Fusion server error %s ' ) % r . text ,
)
)
@ -189,32 +215,79 @@ class Py3oParser(report_sxw):
# we do nice chunked reading from the network...
# we do nice chunked reading from the network...
chunk_size = 1024
chunk_size = 1024
with NamedTemporaryFile (
with NamedTemporaryFile (
suffix = filetype ,
prefix = ' py3o-template- '
suffix = filetype ,
prefix = ' py3o-template- '
) as fd :
) as fd :
for chunk in r . iter_content ( chunk_size ) :
for chunk in r . iter_content ( chunk_size ) :
fd . write ( chunk )
fd . write ( chunk )
fd . seek ( 0 )
fd . seek ( 0 )
# ... but odoo wants the whole data in memory anyways :)
# ... but odoo wants the whole data in memory anyways :)
res = fd . read ( )
res = fd . read ( )
self . _postprocess_report (
res , model_instance . id , save_in_attachment )
return res , " . " + self . ir_actions_report_xml_id . py3o_filetype
@api.multi
def _get_or_create_single_report ( self , model_instance , data ,
save_in_attachment ) :
self . ensure_one ( )
if save_in_attachment and save_in_attachment [
' loaded_documents ' ] . get ( model_instance . id ) :
d = save_in_attachment [
' loaded_documents ' ] . get ( model_instance . id )
return d , self . ir_actions_report_xml_id . py3o_filetype
return self . _create_single_report (
model_instance , data , save_in_attachment )
@api.multi
def _zip_results ( self , results ) :
self . ensure_one ( )
zfname_prefix = self . ir_actions_report_xml_id . name
with NamedTemporaryFile ( suffix = " zip " , prefix = ' py3o-zip-result ' ) as fd :
with ZipFile ( fd , ' w ' , ZIP_DEFLATED ) as zf :
cpt = 0
for r , ext in results :
fname = " %s _ %d . %s " % ( zfname_prefix , cpt , ext )
zf . writestr ( fname , r )
cpt + = 1
fd . seek ( 0 )
return fd . read ( ) , ' zip '
@api.multi
def _merge_pdfs ( self , results ) :
from pyPdf import PdfFileWriter , PdfFileReader
output = PdfFileWriter ( )
for r in results :
reader = PdfFileReader ( StringIO ( r [ 0 ] ) )
for page in range ( reader . getNumPages ( ) ) :
output . addPage ( reader . getPage ( page ) )
s = StringIO ( )
output . write ( s )
return s . getvalue ( ) , formats . FORMAT_PDF
@api.multi
def _merge_results ( self , results ) :
self . ensure_one ( )
if not results :
return False , False
if len ( results ) == 1 :
return results [ 0 ]
filetype = self . ir_actions_report_xml_id . py3o_filetype
if filetype == formats . FORMAT_PDF :
return self . _merge_pdfs ( results )
else :
return self . _zip_results ( results )
return res , filetype
def create ( self , cr , uid , ids , data , context = None ) :
@api.multi
def create_report ( self , res_ids , data ) :
""" Override this function to handle our py3o report
""" Override this function to handle our py3o report
"""
"""
env = api . Environment ( cr , uid , context )
report_xmls = env [ ' ir.actions.report.xml ' ] . search (
[ ( ' report_name ' , ' = ' , self . name [ 7 : ] ) ] )
if not report_xmls :
return super ( Py3oParser , self ) . create (
cr , uid , ids , data , context = context
)
result = self . create_source_pdf (
cr , uid , ids , data , report_xmls [ 0 ] , context
)
if not result :
return False , False
return result
model_instances = self . env [ self . ir_actions_report_xml_id . model ] . browse (
res_ids )
save_in_attachment = self . _check_attachment_use (
model_instances , self . ir_actions_report_xml_id ) or { }
results = [ ]
for model_instance in model_instances :
results . append ( self . _get_or_create_single_report (
model_instance , data , save_in_attachment ) )
return self . _merge_results ( results )