diff --git a/base_report_assembler/__init__.py b/base_report_assembler/__init__.py new file mode 100644 index 00000000..f91d090e --- /dev/null +++ b/base_report_assembler/__init__.py @@ -0,0 +1,24 @@ +# -*- coding: utf-8 -*- +############################################################################## +# +# Author: Yannick Vaucher +# Copyright 2013 Camptocamp SA +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +############################################################################## +from . import report_assembler +from . import assembled_report +from . import ir_report + diff --git a/base_report_assembler/__openerp__.py b/base_report_assembler/__openerp__.py new file mode 100644 index 00000000..45dbc41a --- /dev/null +++ b/base_report_assembler/__openerp__.py @@ -0,0 +1,34 @@ +# -*- coding: utf-8 -*- +############################################################################## +# +# Author: Yannick Vaucher +# Copyright 2013 Camptocamp SA +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +############################################################################## +{'name' : 'Base Report Assembler', + 'version' : '1.0', + 'category': 'Report', + 'description': """Defines a new type of report which is an assemblage of multiple other reports""", + 'author' : 'Camptocamp', + 'maintainer': 'Camptocamp', + 'website': 'http://www.camptocamp.com/', + 'depends' : ['base'], + 'data': [], + 'test': [], + 'installable': True, + 'auto_install': False, + 'application': False, + } diff --git a/base_report_assembler/assembled_report.py b/base_report_assembler/assembled_report.py new file mode 100644 index 00000000..f2fe0ca4 --- /dev/null +++ b/base_report_assembler/assembled_report.py @@ -0,0 +1,40 @@ +# -*- coding: utf-8 -*- +############################################################################## +# +# Author: Yannick Vaucher +# Copyright 2013 Camptocamp SA +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +############################################################################## +from openerp.osv import orm, fields + +class AssembledReport(orm.Model): + _name = 'assembled.report' + + _order = 'sequence' + + _columns = { + 'report_id': fields.many2one('ir.actions.report.xml', 'Report', + domain="[('model', '=', model)," + "('report_type', '!=', 'assemblage')]", required=True), + 'model': fields.char('Object model'), + 'sequence': fields.integer('Sequence', required=True), + 'company_id': fields.many2one('res.company', 'Company', required=True), + } + + _defaults = { + 'sequence': 1, + 'company_id': lambda s,cr,uid,c: s.pool.get('res.company')._company_default_get(cr, uid, 'assembled.report', context=c) + } diff --git a/base_report_assembler/i18n/base_report_assembler.pot b/base_report_assembler/i18n/base_report_assembler.pot new file mode 100644 index 00000000..5b2331ef --- /dev/null +++ b/base_report_assembler/i18n/base_report_assembler.pot @@ -0,0 +1,36 @@ +# Translation of OpenERP Server. +# This file contains the translation of the following modules: +# * base_report_assembler +# +msgid "" +msgstr "" +"Project-Id-Version: OpenERP Server 7.0\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2013-11-05 14:55+0000\n" +"PO-Revision-Date: 2013-11-05 14:55+0000\n" +"Last-Translator: <>\n" +"Language-Team: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Plural-Forms: \n" + +#. module: base_report_assembler +#: field:assembled.report,model:0 +msgid "Object model" +msgstr "" + +#. module: base_report_assembler +#: field:assembled.report,sequence:0 +msgid "Sequence" +msgstr "" + +#. module: base_report_assembler +#: field:assembled.report,company_id:0 +msgid "Company" +msgstr "" + +#. module: base_report_assembler +#: field:assembled.report,report_id:0 +msgid "Report" +msgstr "" diff --git a/base_report_assembler/i18n/fr.po b/base_report_assembler/i18n/fr.po new file mode 100644 index 00000000..b95b27a2 --- /dev/null +++ b/base_report_assembler/i18n/fr.po @@ -0,0 +1,37 @@ +# Translation of OpenERP Server. +# This file contains the translation of the following modules: +# * base_report_assembler +# +msgid "" +msgstr "" +"Project-Id-Version: OpenERP Server 7.0\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2013-11-05 14:55+0000\n" +"PO-Revision-Date: 2013-11-05 14:55+0000\n" +"Last-Translator: <>\n" +"Language-Team: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Plural-Forms: \n" + +#. module: base_report_assembler +#: field:assembled.report,model:0 +msgid "Object model" +msgstr "Modèle d'object" + +#. module: base_report_assembler +#: field:assembled.report,sequence:0 +msgid "Sequence" +msgstr "Séquence" + +#. module: base_report_assembler +#: field:assembled.report,company_id:0 +msgid "Company" +msgstr "Companie" + +#. module: base_report_assembler +#: field:assembled.report,report_id:0 +msgid "Report" +msgstr "Rapport" + diff --git a/base_report_assembler/ir_report.py b/base_report_assembler/ir_report.py new file mode 100644 index 00000000..ab2e7c96 --- /dev/null +++ b/base_report_assembler/ir_report.py @@ -0,0 +1,112 @@ +# -*- coding: utf-8 -*- +############################################################################## +# +# Author: Yannick Vaucher +# Copyright 2013 Camptocamp SA +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +############################################################################## + +from openerp.osv import orm +from openerp import netsvc +from openerp.report.report_sxw import rml_parse +from report_assembler import PDFReportAssembler + +def register_report(name, model, parser=rml_parse): + """Register the report into the services""" + name = 'report.%s' % name + if netsvc.Service._services.get(name, False): + service = netsvc.Service._services[name] + if isinstance(service, PDFReportAssembler): + #already instantiated properly, skip it + return + if hasattr(service, 'parser'): + parser = service.parser + del netsvc.Service._services[name] + PDFReportAssembler(name, model, parser=parser) + + +class ReportAssembleXML(orm.Model): + + _name = 'ir.actions.report.xml' + _inherit = 'ir.actions.report.xml' + + def __init__(self, pool, cr): + super(ReportAssembleXML, self).__init__(pool, cr) + + def register_all(self,cursor): + value = super(ReportAssembleXML, self).register_all(cursor) + cursor.execute("SELECT * FROM ir_act_report_xml WHERE report_type = 'assemblage'") + records = cursor.dictfetchall() + for record in records: + register_report(record['report_name'], record['model']) + return value + + def unlink(self, cursor, user, ids, context=None): + """Delete report and unregister it""" + trans_obj = self.pool.get('ir.translation') + trans_ids = trans_obj.search( + cursor, + user, + [('type', '=', 'report'), ('res_id', 'in', ids)] + ) + trans_obj.unlink(cursor, user, trans_ids) + + # Warning: we cannot unregister the services at the moment + # because they are shared across databases. Calling a deleted + # report will fail so it's ok. + + res = super(ReportAssembleXML, self).unlink( + cursor, + user, + ids, + context + ) + return res + + def create(self, cursor, user, vals, context=None): + "Create report and register it" + res = super(ReportAssembleXML, self).create(cursor, user, vals, context) + if vals.get('report_type','') == 'assemblage': + # I really look forward to virtual functions :S + register_report( + vals['report_name'], + vals['model'], + ) + return res + + def write(self, cr, uid, ids, vals, context=None): + "Edit report and manage its registration" + if isinstance(ids, (int, long)): + ids = [ids,] + for rep in self.browse(cr, uid, ids, context=context): + if rep.report_type != 'assemblage': + continue + if vals.get('report_name', False) and \ + vals['report_name'] != rep.report_name: + report_name = vals['report_name'] + else: + report_name = rep.report_name + + register_report( + report_name, + vals.get('model', rep.model), + False + ) + res = super(ReportAssembleXML, self).write(cr, uid, ids, vals, context) + return res + + +# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: diff --git a/base_report_assembler/report_assembler.py b/base_report_assembler/report_assembler.py new file mode 100644 index 00000000..8753f105 --- /dev/null +++ b/base_report_assembler/report_assembler.py @@ -0,0 +1,138 @@ +# -*- coding: utf-8 -*- +############################################################################## +# +# Author: Yannick Vaucher +# Copyright 2013 Camptocamp SA +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +############################################################################## +import time +import base64 +from PyPDF2 import PdfFileReader, PdfFileWriter +from StringIO import StringIO + +from openerp.service.web_services import report_spool +from openerp.netsvc import Service +from openerp.netsvc import ExportService +from openerp.report import report_sxw +from openerp import pooler, sql_db + +_POLLING_DELAY = 0.25 + +def assemble_pdf(pdf_list): + """ + Assemble a list of pdf + """ + # Even though we are using PyPDF2 we can't use PdfFileMerger + # as this issue still exists in mostly used wktohtml reports version + # http://code.google.com/p/wkhtmltopdf/issues/detail?id=635 + #merger = PdfFileMerger() + #merger.append(fileobj=StringIO(invoice_pdf)) + #merger.append(fileobj=StringIO(bvr_pdf)) + + #with tempfile.TemporaryFile() as merged_pdf: + #merger.write(merged_pdf) + #return merged_pdf.read(), 'pdf' + + output = PdfFileWriter() + for pdf in pdf_list: + reader = PdfFileReader(StringIO(pdf)) + for page in range(reader.getNumPages()): + output.addPage(reader.getPage(page)) + s = StringIO() + output.write(s) + return s.getvalue() + +class PDFReportAssembler(report_sxw.report_sxw): + """ PDFReportAssembler allows to put 2 invoice reports in one single pdf""" + + def _generate_all_pdf(self, cr, uid, ids, data, report_ids, context=None): + """ + Return a list of pdf encoded in base64 + """ + pool = pooler.get_pool(cr.dbname) + db = sql_db.db_connect(cr.dbname) + report_obj = pool.get('ir.actions.report.xml') + + spool = ExportService._services['report'] + + pdf_reports = [] + report_list = report_obj.browse(cr, uid, report_ids, context=context) + for report in report_list: + + report_key = spool.exp_report(cr.dbname, uid, report.report_name, ids, datas=data, context=context) + while 1: + res = spool.exp_report_get(cr.dbname, uid, report_key) + if res.get('state'): + break + time.sleep(_POLLING_DELAY) + pdf = base64.b64decode(res.get('result')) + pdf_reports.append(pdf) + return pdf_reports + + #pdf_reports = {} + #report_list = report_obj.browse(cr, uid, report_ids, context=context) + #report_obj = pool.get('ir.actions.report.xml') + #pool = pooler.get_pool(cr.dbname) + #for report in report_list: + + #report_parser = Service._services['report.%s' %report.report_name] + #pdf_reports[report.id] = report_parser.create_single_pdf(cr, uid, ids, data, report, context=context)[0] + #return pdf_reports + + + + def _get_report_ids(self, cr, uid, ids, context=None): + """ + Hook to define the list of report to print + """ + return [] + + def create_single_pdf(self, cr, uid, ids, data, report_xml, context=None): + """Call both report to assemble both pdf""" + + report_ids = self._get_report_ids(cr, uid, ids, context=context) + + pdf_reports = self._generate_all_pdf(cr, uid, ids, data, report_ids, context=context) + + pdf_assemblage = assemble_pdf(pdf_reports) + return pdf_assemblage, 'pdf' + + def create(self, cr, uid, ids, data, context=None): + """We override the create function in order to handle generator + Code taken from report openoffice. Thanks guys :) """ + pool = pooler.get_pool(cr.dbname) + ir_obj = pool.get('ir.actions.report.xml') + report_xml_ids = ir_obj.search(cr, uid, + [('report_name', '=', self.name[7:])], context=context) + if report_xml_ids: + + report_xml = ir_obj.browse(cr, + uid, + report_xml_ids[0], + context=context) + report_xml.report_rml = None + report_xml.report_rml_content = None + report_xml.report_sxw_content_data = None + report_xml.report_sxw_content = None + report_xml.report_sxw = None + else: + return super(PDFReportAssembler, self).create(cr, uid, ids, data, context) + if report_xml.report_type != 'assemblage' : + return super(PDFReportAssembler, self).create(cr, uid, ids, data, context) + result = self.create_source_pdf(cr, uid, ids, data, report_xml, context) + if not result: + return (False, False) + return result