From 227f36c8460f82b1103124fb465e1cc9dabc7c2e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Maillard=20-=20Elico=20Corp?= Date: Tue, 21 Nov 2017 12:07:52 +0800 Subject: [PATCH 01/55] Do not test OCA/OCB anymore --- .travis.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 921ce565..87883d56 100644 --- a/.travis.yml +++ b/.travis.yml @@ -26,7 +26,6 @@ env: matrix: - LINT_CHECK="1" - EXCLUDE="excel_report_for_stock_valuation,pos_membership" TESTS="1" ODOO_REPO="odoo/odoo" - - EXCLUDE="excel_report_for_stock_valuation,pos_membership" TESTS="1" ODOO_REPO="OCA/OCB" global: - VERSION="10.0" TESTS="0" LINT_CHECK="0" TRANSIFEX="0" From fbc13670918ef3707a746da6385f103ed222fb6e Mon Sep 17 00:00:00 2001 From: Kevin4577 Date: Fri, 20 Apr 2018 14:54:15 +0800 Subject: [PATCH 02/55] [ADD]add the updated v10 report_docx --- report_docx/README.rst | 85 ++++++++ report_docx/__init__.py | 5 + report_docx/__manifest__.py | 15 ++ report_docx/i18n/zh_CN.po | 52 +++++ report_docx/models/__init__.py | 4 + report_docx/models/ir_actions.py | 76 +++++++ report_docx/report/__init__.py | 5 + report_docx/report/ir_report.py | 17 ++ report_docx/report/report_docx.py | 316 ++++++++++++++++++++++++++++++ report_docx/views/ir_actions.xml | 28 +++ 10 files changed, 603 insertions(+) create mode 100644 report_docx/README.rst create mode 100644 report_docx/__init__.py create mode 100644 report_docx/__manifest__.py create mode 100644 report_docx/i18n/zh_CN.po create mode 100644 report_docx/models/__init__.py create mode 100644 report_docx/models/ir_actions.py create mode 100644 report_docx/report/__init__.py create mode 100644 report_docx/report/ir_report.py create mode 100644 report_docx/report/report_docx.py create mode 100644 report_docx/views/ir_actions.xml diff --git a/report_docx/README.rst b/report_docx/README.rst new file mode 100644 index 00000000..f35c77ed --- /dev/null +++ b/report_docx/README.rst @@ -0,0 +1,85 @@ +.. image:: https://img.shields.io/badge/licence-AGPL--3-blue.svg + :target: http://www.gnu.org/licenses/agpl-3.0-standalone.html + :alt: License: AGPL-3 + +==================== +Docx template report +==================== + +This module adds a new reporting engine to the current standard QWEB allowing the user to directly upload docx files containing scripts (in jinja2) into Odoo. + +The reporting engine will interpret the script and create a pdf/docx based on the docx template. + + +Installation +============ +To install this module, you need to: + +* install the python lib: docxtpl (http://docxtpl.readthedocs.org/en/latest/) + +* install the libreoffice (https://wiki.ubuntu.com/LibreOffice) + +Configuration +============= + +To configure this module, you need to: + +* Inherit the function generate_docx_data in the report/report_docx.py + +* Pass the list of the dictionary to the engine. (etc [{data1}, {data2}, .....]) + +* Each dictionary will generate a report base on the docx template. + + +Usage +===== + +To use this module, you need to: + +1. Go to System/Technical/Reports/Report +2. Create a new report +3. Change the report type to docx +4. upload the template file. + + +Bug Tracker +=========== + +Bugs are tracked on `GitHub Issues `. + + +Known issues / Roadmap +====================== + +* To be implemented: export from docx to html. +* load the watermark from html (qweb) + + +Credits +======= + +Images +------ + +* Odoo Community Association: `Icon `_. + + +Contributors +------------ + +* Siyuan Gu: gu.siyuan@elico-corp.com +* Alex Duan: alex.duan@elico-corp.com +* Xie Xiao Peng + +Maintainer +---------- + +.. image:: https://www.elico-corp.com/logo.png + :alt: Elico Corp + :target: https://www.elico-corp.com + +This module is maintained by Elico Corporation. + +Elico Corporation offers consulting services to implement open source management software in SMEs, with a strong involvement in quality of service. + +Our headquarters are located in Shanghai with branches in ShenZhen and Singapore servicing customers from China, Hong Kong, Japan, Australia, Europe, Middle East, etc... diff --git a/report_docx/__init__.py b/report_docx/__init__.py new file mode 100644 index 00000000..b112b636 --- /dev/null +++ b/report_docx/__init__.py @@ -0,0 +1,5 @@ +# -*- coding: utf-8 -*- +# © 2018 Elico Corp (https://www.elico-corp.com). +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). +from . import models +from . import report diff --git a/report_docx/__manifest__.py b/report_docx/__manifest__.py new file mode 100644 index 00000000..ad7a2818 --- /dev/null +++ b/report_docx/__manifest__.py @@ -0,0 +1,15 @@ +# -*- coding: utf-8 -*- +# © <2018> +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). +{ + 'name': 'Report Docx', + 'version': '10.0.1.0.0', + 'category': 'report', + 'depends': ['base'], + 'author': 'Elico Corp', + 'license': 'AGPL-3', + 'website': 'https://www.elico-corp.com', + 'data': ['views/ir_actions.xml'], + 'installable': True, + 'application': False +} diff --git a/report_docx/i18n/zh_CN.po b/report_docx/i18n/zh_CN.po new file mode 100644 index 00000000..6eb8ae88 --- /dev/null +++ b/report_docx/i18n/zh_CN.po @@ -0,0 +1,52 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * report_docx +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 10.0e\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2018-04-20 03:17+0000\n" +"PO-Revision-Date: 2018-04-20 03:17+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: report_docx +#: selection:ir.actions.report.xml,output_type:0 +msgid "Docx" +msgstr "Docx" + +#. module: report_docx +#: model:ir.model.fields,field_description:report_docx.field_ir_act_report_xml_output_type +msgid "Output Type" +msgstr "导出文件类型" + +#. module: report_docx +#: selection:ir.actions.report.xml,output_type:0 +msgid "PDF" +msgstr "PDF" + +#. module: report_docx +#: model:ir.model.fields,field_description:report_docx.field_ir_act_report_xml_template_file +msgid "Template File" +msgstr "模板" + +#. module: report_docx +#: model:ir.model.fields,field_description:report_docx.field_ir_act_report_xml_watermark_string +msgid "Wartermark String" +msgstr "水印文字" + +#. module: report_docx +#: model:ir.model.fields,field_description:report_docx.field_ir_act_report_xml_watermark_template +msgid "Watermark Template" +msgstr "水印模板" + +#. module: report_docx +#: model:ir.model,name:report_docx.model_ir_actions_report_xml +msgid "ir.actions.report.xml" +msgstr "ir.actions.report.xml" + diff --git a/report_docx/models/__init__.py b/report_docx/models/__init__.py new file mode 100644 index 00000000..de999c27 --- /dev/null +++ b/report_docx/models/__init__.py @@ -0,0 +1,4 @@ +# -*- coding: utf-8 -*- +# © 2018 Elico Corp (https://www.elico-corp.com). +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). +from . import ir_actions diff --git a/report_docx/models/ir_actions.py b/report_docx/models/ir_actions.py new file mode 100644 index 00000000..d736c736 --- /dev/null +++ b/report_docx/models/ir_actions.py @@ -0,0 +1,76 @@ +# -*- coding: utf-8 -*- +# © 2018 Elico Corp (https://www.elico-corp.com). +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). +from odoo import api, fields, models, _ + + +class IrActionsReportXml(models.Model): + _inherit = 'ir.actions.report.xml' + + ir_values_id = fields.Many2one( + comodel_name='ir.values', + string='More Menu entry', + readonly=True, + help='More menu entry.', + copy=False + ) + + report_type = fields.Selection( + selection_add=[(('docx', 'Docx'))] + ) + + template_file = fields.Many2one( + comodel_name='ir.attachment', + string='Template File' + ) + + watermark_string = fields.Char( + string='Wartermark String' + ) + + watermark_template = fields.Many2one( + comodel_name='ir.attachment', + string='Watermark Template' + ) + + output_type = fields.Selection( + [ + ('pdf', 'PDF'), + ('docx', 'Docx'), + ], + 'Output Type', + required=True, + default='pdf' + ) + + @api.multi + def create_action(self): + """ Create a contextual action for each of the report.""" + + for ir_actions_report_xml in self: + ir_values_id = self.env['ir.values'].sudo().create( { + 'name': ir_actions_report_xml.name, + 'model': ir_actions_report_xml.model, + 'key2': 'client_print_multi', + 'value': "ir.actions.report.xml,%s" % + ir_actions_report_xml.id, + }) + ir_actions_report_xml.write({ + 'ir_values_id': ir_values_id.id, + }) + return True + + @api.multi + def unlink_action(self): + """ Remove the contextual actions created for the reports.""" + self.check_access_rights('write', raise_exception=True) + for ir_actions_report_xml in self: + if ir_actions_report_xml.ir_values_id.id: + try: + self.env['ir.values'].sudo().browse( + ir_actions_report_xml.ir_values_id.id).unlink() + except Exception: + raise Exception( + _('Deletion of the action record failed.') + ) + return True diff --git a/report_docx/report/__init__.py b/report_docx/report/__init__.py new file mode 100644 index 00000000..d064479c --- /dev/null +++ b/report_docx/report/__init__.py @@ -0,0 +1,5 @@ +# -*- coding: utf-8 -*- +# © 2018 Elico Corp (https://www.elico-corp.com). +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). +from . import report_docx +from . import ir_report diff --git a/report_docx/report/ir_report.py b/report_docx/report/ir_report.py new file mode 100644 index 00000000..1becbf74 --- /dev/null +++ b/report_docx/report/ir_report.py @@ -0,0 +1,17 @@ +# -*- coding: utf-8 -*- +# © 2018 Elico Corp (https://www.elico-corp.com). +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). + +from odoo import api, models + + +class IrActionReportDocx(models.Model): + _inherit = 'ir.actions.report.xml' + + @api.model + def _check_selection_field_value(self, field, value): + if field == 'report_type' and value == 'docx': + return + + return super(IrActionReportDocx, self)._check_selection_field_value( + field, value) diff --git a/report_docx/report/report_docx.py b/report_docx/report/report_docx.py new file mode 100644 index 00000000..5571a183 --- /dev/null +++ b/report_docx/report/report_docx.py @@ -0,0 +1,316 @@ +# -*- coding: utf-8 -*- +# © 2018 Elico Corp (https://www.elico-corp.com). +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). + +from odoo.report import report_sxw +from odoo import api +import logging +from pyPdf import PdfFileWriter, PdfFileReader +from reportlab.pdfgen import canvas +import os +import base64 +from lxml import etree +import time +import random + +try: + from docxtpl import DocxTemplate +except ImportError: + pass +try: + from docx import Document +except ImportError: + pass + +_logger = logging.getLogger(__name__) + + +class ReportDocx(report_sxw.rml_parse): + + @api.model + def create(self, data): + report_id_list = self.env['ir.actions.report.xml'].search([ + ('report_name', '=', self.name[7:])]) + if report_id_list: + report_xml = report_id_list[0] + self.title = report_xml.name + if report_xml.report_type == 'docx': + return self.create_source_docx(data) + return super(ReportDocx, self).create(data) + + @api.multi + def create_source_docx(self, dict): + data = self.generate_docx_data(dict) + + tmp_folder_name = '/tmp/docx_to_pdf/' +\ + str(int(time.time())) +\ + str(int(1000 + random.random() * 1000)) + '/' + + output_type = self._get_output_type(dict) + output_report = { + 'pdf': 'report.pdf', + 'docx': 'report.docx' + } + + self._delete_temp_folder(tmp_folder_name) + self._create_temp_folder(tmp_folder_name) + + self._generate_reports(tmp_folder_name, data, output_type, output_report, dict) + + report = self._get_convert_file(tmp_folder_name, output_report[output_type]) + + self._delete_temp_folder(tmp_folder_name) + + return (report, output_type) + + def generate_docx_data(self, data): + """ + Override this method to pass your own data to the engine. + The return value of this module should be a list with + report data. + """ + return [{}] + + def _generate_reports(self, tmp_folder_name, datas, + output_type, output_report, dict): + if "pdf" == output_type: + self._generate_pdf_reports( + tmp_folder_name, datas, + output_type, output_report, dict) + return + + self._generate_doc_reports(tmp_folder_name, datas, output_type, output_report, dict) + + def _generate_pdf_reports(self, tmp_folder_name, datas, output_type, output_report, dict): + count = 0 + for data in datas: + self._convert_single_report(tmp_folder_name, count, data, output_type, dict) + count = count + 1 + + self._combine_pdf_files( + tmp_folder_name, output_report[output_type]) + + def _generate_doc_reports( + self, tmp_folder_name, + datas, output_type, output_report, dict + ): + temp_docxs = [] + count = 0 + for data in datas: + report = self._convert_single_report( + tmp_folder_name, + count, data, output_type, dict) + temp_docxs.append(report) + count = count + 1 + + self._combine_docx_files( + tmp_folder_name, output_report[output_type], temp_docxs) + + def _combine_pdf_files(self, tmp_folder_name, output_report): + output_path = tmp_folder_name + output_report + output_temp_path = tmp_folder_name + 'temp.pdf' + + cmd = "gs -q -dNOPAUSE -sDEVICE=pdfwrite -sOUTPUTFILE=%s \ + -dBATCH %s*water*.pdf" % (output_temp_path, tmp_folder_name) + os.system(cmd) + + # remove the last empty page + input_stream = PdfFileReader(file(output_temp_path, 'rb')) + output_stream = PdfFileWriter() + + pagenum = input_stream.getNumPages() + for i in range(pagenum - 1): + page = input_stream.getPage(i) + output_stream.addPage(page) + + out_stream = file(output_path, 'wb') + try: + output_stream.write(out_stream) + finally: + out_stream.close() + + def _combine_docx_files(self, tmp_folder_name, output_report, temp_docxs): + output_path = tmp_folder_name + output_report + first_document = True + xml_header = "" + xml_body = "" + xml_footer = "" + + # merge all the reports into first report + report = Document(tmp_folder_name + temp_docxs[0]) + + for file in temp_docxs: + docx = Document(tmp_folder_name + file) + xml = etree.tostring( + docx._element.body, encoding='unicode', pretty_print=False) + + # get the header from first document since + # all the report have the same format. + if first_document: + xml_header = xml.split('>')[0] + '>' + first_document = False + + for body in etree.fromstring(xml): + xml_body = xml_body + etree.tostring(body) + + report._element.replace( + report._element.body, etree.fromstring( + xml_header + xml_body + xml_footer) + ) + + report.save(output_path) + + def _convert_single_report( + self, tmp_folder_name, + count, data, output_type, dict + ): + docx_template_name = 'template_%s.docx' % count + convert_docx_file_name = 'convert_%s.docx' % count + convert_pdf_file_name = 'convert_%s.pdf' % count + pdf_file_with_watermark = 'convert_watermark_%s.pdf' % count + watermark_file = 'watermark.pdf' + + self._convert_docx_from_template(data, tmp_folder_name, + docx_template_name, convert_docx_file_name, dict) + + if output_type == 'pdf': + self._convert_docx_to_pdf(tmp_folder_name,convert_docx_file_name) + + self._create_watermark_pdf(tmp_folder_name, watermark_file, dict) + + self._add_watermark_to_pdf(tmp_folder_name, watermark_file, + convert_pdf_file_name, pdf_file_with_watermark) + + return pdf_file_with_watermark + + return convert_docx_file_name + + def _convert_docx_from_template(self, tmp_folder_name, data, + docx_template_name, convert_docx_file_name, dict): + action_id = dict['template_id'] + action = self.env['ir.actions.report.xml'].browse(action_id) + + template_path = tmp_folder_name + docx_template_name + convert_path = tmp_folder_name + convert_docx_file_name + + self._save_file(template_path, base64.b64decode(action.template_file.datas)) + + doc = DocxTemplate(template_path) + doc.render(data) + doc.save(convert_path) + + def _convert_docx_to_pdf( + self, tmp_folder_name, + convert_docx_file_name + ): + docx_path = tmp_folder_name + \ + convert_docx_file_name + output_path = tmp_folder_name + + cmd = "soffice --headless --convert-to pdf --outdir " + output_path \ + + " " + docx_path + os.system(cmd) + + def _create_watermark_pdf(self, tmp_folder_name, watermark_file, dict): + watermark_path = tmp_folder_name + watermark_file + watermark_string = self._get_watermark_string(dict) + watermark_template = self._get_watermark_template(dict) + + if watermark_template: + self._save_file(watermark_path, base64.b64decode(watermark_template)) + return + + self._save_watermark_pdf(watermark_path, watermark_string) + + def _save_watermark_pdf(self, tmp_folder_name, watermark_string): + wartermark = canvas.Canvas(tmp_folder_name) + wartermark.setFont("Courier", 60) + + wartermark.setFillGray(0.5, 0.5) + + wartermark.saveState() + wartermark.translate(500, 100) + wartermark.rotate(45) + wartermark.drawCentredString(0, 0, watermark_string) + wartermark.drawCentredString(0, 300, watermark_string) + wartermark.drawCentredString(0, 600, watermark_string) + wartermark.restoreState() + wartermark.save() + + def _add_watermark_to_pdf( + self, tmp_folder_name, watermark_file, + convert_pdf_file, pdf_file_with_watermark + ): + watermark_path = tmp_folder_name + \ + watermark_file + pdf_path = tmp_folder_name + \ + convert_pdf_file + output_path = tmp_folder_name + \ + pdf_file_with_watermark + + output = PdfFileWriter() + input_pdf = PdfFileReader(file(pdf_path, 'rb')) + water = PdfFileReader(file(watermark_path, 'rb')) + + pagenum = input_pdf.getNumPages() + + for i in range(pagenum): + page = input_pdf.getPage(i) + page.mergePage(water.getPage(0)) + output.addPage(page) + + out_stream = file(output_path, 'wb') + try: + output.write(out_stream) + finally: + out_stream.close() + + def _get_convert_file( + self, tmp_folder_name, convert_file_name + ): + path = tmp_folder_name + \ + convert_file_name + + input_stream = open(path, 'r') + try: + report = input_stream.read() + finally: + input_stream.close() + + return report + + def _get_watermark_string(self, dict): + action_id = dict['template_id'] + action = self.env['ir.actions.report.xml'].browse(action_id) + + if action.watermark_string: + return action.watermark_string + + return "" + + def _get_watermark_template(self, dict): + action_id = dict['template_id'] + action = self.env['ir.actions.report.xml'].browse(action_id) + + return action.watermark_template.datas + + def _get_output_type(self, dict): + action_id = dict['template_id'] + action = self.env['ir.actions.report.xml'].browse(action_id) + + return action.output_type + + def _create_temp_folder(self, tmp_folder_name): + cmd = 'mkdir ' + tmp_folder_name + os.system(cmd) + + def _delete_temp_folder(self, tmp_folder_name): + cmd = 'rm -rf ' + tmp_folder_name + os.system(cmd) + + def _save_file(self, folder_name, file): + out_stream = open(folder_name, 'wb') + try: + out_stream.writelines(file) + finally: + out_stream.close() diff --git a/report_docx/views/ir_actions.xml b/report_docx/views/ir_actions.xml new file mode 100644 index 00000000..87333601 --- /dev/null +++ b/report_docx/views/ir_actions.xml @@ -0,0 +1,28 @@ + + + + + ir.actions.report.xml.template + ir.actions.report.xml + + + + +