diff --git a/docx_report/README.md b/docx_report/README.md new file mode 100755 index 0000000..93ff078 --- /dev/null +++ b/docx_report/README.md @@ -0,0 +1,48 @@ +# DOCX report +Functionality + +Allows you to add docx files to the report model as a source template. +You can get reports based on such a template in docx or pdf format. +Currently, the simultaneous creation of multiple reports is not supported. + +To convert docx -> pdf, an available gutenberg service is required on localhost:8808. +An example of launching a service in docker-compose next to Odoo: + +```yaml +gotenberg: + image: thecodingmachine/gotenberg:6 + restart: unless-stopped + environment: + LOG_LEVEL: INFO + DEFAULT_LISTEN_PORT: 8808 + DISABLE_GOOGLE_CHROME: 1 + DEFAULT_WAIT_TIMEOUT: 30 + MAXIMUM_WAIT_TIMEOUT: 60 +``` + +Creating a report + +The report creates in the same way as the standard Odoo procedure: +1. In Settings -> Technical -> Reports, you need to create a new record. In the record of the report + choose one of the new types: "DOCX" or "DOCX(PDF)". + You do not need to fill in the "Template name" field, but instead download the docx file of the report. + All other fields are filled in the same as in standard Odoo reports. +2. If custom fields are applied in the report template, then you need to create them on the tab + "Custom fields". +3. In the entry of the specified model, an additional item with the name of the created report will appear in the print menu. + Clicking on it will display a wizard in which you can check the values of custom fields before generating the report file. +4. When generating a report from the portal, the file is generated without displaying the wizard. + + +Templates creating + +1. Templates can be created in any text editor that supports the docx format. +2. All formatting of the template is saved in the generated report. +3. Double curly braces are used to insert variables. +4. Access to the Odoo record for which the report generation is called is performed through the "docs" variable, + accessing attributes and methods as in Odoo: {{docs.attribute_name }} +5. It is possible to call the methods available for the entry in "docs", or passed to the context of the report. +6. By default, the report context contains methods of the "report_monetary_helper" module, which can be called directly by name. +7. Custom fields may also be present in the context of the report. + Such fields must be created in the report record. + In the template, custom fields are available by the name specified in the "tech_name" field of the custom field entry. diff --git a/docx_report/__init__.py b/docx_report/__init__.py new file mode 100755 index 0000000..91c5580 --- /dev/null +++ b/docx_report/__init__.py @@ -0,0 +1,2 @@ +from . import controllers +from . import models diff --git a/docx_report/__manifest__.py b/docx_report/__manifest__.py new file mode 100755 index 0000000..a1dec89 --- /dev/null +++ b/docx_report/__manifest__.py @@ -0,0 +1,30 @@ +{ + "name": "DOCX report", + "summary": """Printing reports in docx format from docx templates.""", + "description": """ + Adds generation reports from .docx templates like standard Odoo reports + with qweb templates. Standard Odoo reports also available. + For generating .pdf from .docx external service the "Gotenberg" is used, + and it required module for integration with this service: "gotenberg". + If integration module "gotenberg" is absent, or service itself unreachable + there will be only reports in docx format. + + This is the beta version, bugs may be present. + """, + "author": "RYDLAB", + "website": "https://rydlab.ru", + "category": "Technical", + "version": "16.0.1.0.0", + "license": "LGPL-3", + "depends": ["base", "web", "custom_report_field", "report_monetary_helpers"], + "external_dependencies": {"python": ["docxcompose", "docxtpl", "bs4"]}, + "data": [ + "views/ir_actions_report_views.xml", + ], + "assets": { + "web.assets_backend": [ + "docx_report/static/src/css/mimetypes.css", + "docx_report/static/src/js/action_manager_report.js", + ], + }, +} diff --git a/docx_report/controllers/__init__.py b/docx_report/controllers/__init__.py new file mode 100644 index 0000000..12a7e52 --- /dev/null +++ b/docx_report/controllers/__init__.py @@ -0,0 +1 @@ +from . import main diff --git a/docx_report/controllers/main.py b/docx_report/controllers/main.py new file mode 100644 index 0000000..40ff84d --- /dev/null +++ b/docx_report/controllers/main.py @@ -0,0 +1,123 @@ +from json import dumps as json_dumps, loads as json_loads +from werkzeug.urls import url_decode + +from odoo.http import ( + content_disposition, + request, + route, + serialize_exception as _serialize_exception, +) +from odoo.tools import html_escape +from odoo.tools.safe_eval import safe_eval, time + +from odoo.addons.web.controllers.report import ReportController + + +class DocxReportController(ReportController): + @route() + def report_routes(self, reportname, docids=None, converter=None, **data): + """ + Запускает генерацию файла отчета и возвращает его + """ + report = request.env["ir.actions.report"]._get_report_from_name(reportname) + context = dict(request.env.context) + _data = dict() + _docids = [int(i) for i in docids.split(",")] if docids else [] + if data.get("options"): + _data.update(json_loads(data.pop("options"))) + if data.get("context"): + # Ignore 'lang' here, because the context in data is the one from the webclient *but* if + # the user explicitely wants to change the lang, this mechanism overwrites it. + _data["context"] = json_loads(data["context"]) + if _data["context"].get("lang") and not _data.get("force_context_lang"): + del _data["context"]["lang"] + context.update(_data["context"]) + if converter == "docx": + docx = report.with_context(context)._render_docx_docx(_docids, data=_data) + docxhttpheaders = [ + ( + "Content-Type", + "application/vnd.openxmlformats-officedocument.wordprocessingml.document", + ), + ] + return request.make_response(docx, headers=docxhttpheaders) + elif converter == "pdf" and "docx" in report.report_type: + pdf = report.with_context(context)._render_docx_pdf(_docids, data=_data) + pdfhttpheaders = [ + ( + "Content-Type", + "application/pdf", + ), + ("Content-Length", len(pdf[0])), + ] + return request.make_response(pdf, headers=pdfhttpheaders) + else: + return super(DocxReportController, self).report_routes( + reportname, docids, converter, **data + ) + + @route() + def report_download(self, data, token, context=None): + """ + Обрабатывает запрос на скачивание файла отчета + """ + requestcontent = json_loads(data) + url, type = requestcontent[0], requestcontent[1] + try: + if type in ["docx-docx", "docx-pdf"]: + converter = "docx" if type == "docx-docx" else "pdf" + extension = "docx" if type == "docx-docx" else "pdf" + + pattern = "/report/%s/" % ("docx" if type == "docx-docx" else "pdf") + reportname = url.split(pattern)[1].split("?")[0] + + docids = None + if "/" in reportname: + reportname, docids = reportname.split("/") + + if docids: + # Generic report: + response = self.report_routes( + reportname, docids=docids, converter=converter, context=context + ) + else: + # Particular report: + data = dict( + url_decode(url.split("?")[1]).items() + ) # decoding the args represented in JSON + if "context" in data: + context, data_context = json_loads(context or "{}"), json_loads( + data.pop("context") + ) + context = json_dumps({**context, **data_context}) + response = self.report_routes( + reportname, converter=converter, context=context, **data + ) + + report = request.env["ir.actions.report"]._get_report_from_name( + reportname + ) + filename = "%s.%s" % (report.name, extension) + + if docids: + ids = [int(x) for x in docids.split(",")] + obj = request.env[report.model].browse(ids) + if report.print_report_name and not len(obj) > 1: + report_name = safe_eval( + report.print_report_name, {"object": obj, "time": time} + ) + filename = "%s.%s" % (report_name, extension) + response.headers.add( + "Content-Disposition", content_disposition(filename) + ) + # token is dummy in 15 version + # response.set_cookie("fileToken", token) + return response + else: + return super(DocxReportController, self).report_download( + data, context=context + ) + except Exception as e: + se = _serialize_exception(e) + error = {"code": 200, "message": "Odoo Server Error", "data": se} + return request.make_response(html_escape(json_dumps(error))) diff --git a/docx_report/i18n/docx_report.pot b/docx_report/i18n/docx_report.pot new file mode 100644 index 0000000..2912961 --- /dev/null +++ b/docx_report/i18n/docx_report.pot @@ -0,0 +1,70 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * docx_report +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 16.0\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2022-12-29 08:24+0000\n" +"PO-Revision-Date: 2022-12-29 08:24+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: docx_report +#: model:ir.model.fields.selection,name:docx_report.selection__ir_actions_report__report_type__docx-docx +msgid "DOCX" +msgstr "" + +#. module: docx_report +#: model:ir.model.fields.selection,name:docx_report.selection__ir_actions_report__report_type__docx-pdf +msgid "DOCX(PDF)" +msgstr "" + +#. module: docx_report +#. odoo-python +#: code:addons/docx_report/models/ir_actions_report.py:0 +#, python-format +msgid "" +"Gotenberg converting service not available. The PDF can not be created." +msgstr "" + +#. module: docx_report +#. odoo-python +#: code:addons/docx_report/models/ir_actions_report.py:0 +#, python-format +msgid "One of the documents you try to merge caused failure." +msgstr "" + +#. module: docx_report +#: model:ir.model,name:docx_report.model_ir_actions_report +msgid "Report Action" +msgstr "" + +#. module: docx_report +#: model:ir.model.fields,field_description:docx_report.field_ir_actions_report__report_type +msgid "Report Type" +msgstr "" + +#. module: docx_report +#: model:ir.model.fields,field_description:docx_report.field_ir_actions_report__report_docx_template +msgid "Report docx template" +msgstr "" + +#. module: docx_report +#: model:ir.model.fields,field_description:docx_report.field_ir_actions_report__report_name +msgid "Template Name" +msgstr "" + +#. module: docx_report +#: model:ir.model.fields,help:docx_report.field_ir_actions_report__report_type +msgid "" +"The type of the report that will be rendered, each one having its own " +"rendering method. HTML means the report will be opened directly in your " +"browser PDF means the report will be rendered using Wkhtmltopdf and " +"downloaded by the user." +msgstr "" diff --git a/docx_report/i18n/ru.po b/docx_report/i18n/ru.po new file mode 100644 index 0000000..3007c1f --- /dev/null +++ b/docx_report/i18n/ru.po @@ -0,0 +1,75 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * docx_report +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 16.0\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2022-12-29 08:24+0000\n" +"PO-Revision-Date: 2022-12-29 08:24+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: docx_report +#: model:ir.model.fields.selection,name:docx_report.selection__ir_actions_report__report_type__docx-docx +msgid "DOCX" +msgstr "DOCX" + +#. module: docx_report +#: model:ir.model.fields.selection,name:docx_report.selection__ir_actions_report__report_type__docx-pdf +msgid "DOCX(PDF)" +msgstr "DOCX(PDF)" + +#. module: docx_report +#. odoo-python +#: code:addons/docx_report/models/ir_actions_report.py:0 +#, python-format +msgid "" +"Gotenberg converting service not available. The PDF can not be created." +msgstr "" +"Файл PDF не может быть создан, так как сервис конвертации Gotenberg не доступен." + +#. module: docx_report +#. odoo-python +#: code:addons/docx_report/models/ir_actions_report.py:0 +#, python-format +msgid "One of the documents you try to merge caused failure." +msgstr "Один из документов, которые вы пытаетесь соединить, вызывает ошибку." + +#. module: docx_report +#: model:ir.model,name:docx_report.model_ir_actions_report +msgid "Report Action" +msgstr "Действие для отчета." + +#. module: docx_report +#: model:ir.model.fields,field_description:docx_report.field_ir_actions_report__report_type +msgid "Report Type" +msgstr "Тип отчета" + +#. module: docx_report +#: model:ir.model.fields,field_description:docx_report.field_ir_actions_report__report_docx_template +msgid "Report docx template" +msgstr "Шаблон для отчета docx" + +#. module: docx_report +#: model:ir.model.fields,field_description:docx_report.field_ir_actions_report__report_name +msgid "Template Name" +msgstr "Имя шаблона" + +#. module: docx_report +#: model:ir.model.fields,help:docx_report.field_ir_actions_report__report_type +msgid "" +"The type of the report that will be rendered, each one having its own " +"rendering method. HTML means the report will be opened directly in your " +"browser PDF means the report will be rendered using Wkhtmltopdf and " +"downloaded by the user." +msgstr "" +"Тип генерируемого отчета. Каждый тип имеет свой собственный" +" метод генерации. HTML означает, что отчет будет открыт непосредственно в" +" вашем браузере, PDF означает, что отчет будет сгенерирован с помощью " +"Wkhtmltopdf и загружен пользователем." diff --git a/docx_report/models/__init__.py b/docx_report/models/__init__.py new file mode 100755 index 0000000..a248cf2 --- /dev/null +++ b/docx_report/models/__init__.py @@ -0,0 +1 @@ +from . import ir_actions_report diff --git a/docx_report/models/ir_actions_report.py b/docx_report/models/ir_actions_report.py new file mode 100644 index 0000000..0e9d591 --- /dev/null +++ b/docx_report/models/ir_actions_report.py @@ -0,0 +1,466 @@ +from base64 import b64decode +from bs4 import BeautifulSoup +from collections import OrderedDict +from io import BytesIO +from logging import getLogger + +from docx import Document +from docxcompose.composer import Composer +from docxtpl import DocxTemplate +from requests import codes as codes_request, post as post_request +from requests.exceptions import RequestException + +from odoo import _, api, fields, models +from odoo.exceptions import AccessError, UserError +from odoo.http import request +from odoo.tools.safe_eval import safe_eval, time + +try: + from odoo.addons.gotenberg.service.utils import ( + get_auth, # noqa + convert_pdf_from_office_url, # noqa + check_gotenberg_installed, # noqa + ) + + gotenberg_imported = True +except ImportError: + gotenberg_imported = False + +_logger = getLogger(__name__) + + +class IrActionsReport(models.Model): + _inherit = "ir.actions.report" + + report_name = fields.Char( + compute="_compute_report_name", + inverse="_inverse_report_name", + store=True, + required=False, + ) + report_type = fields.Selection( + selection_add=[("docx-docx", "DOCX"), ("docx-pdf", "DOCX(PDF)")], + ondelete={"docx-docx": "cascade", "docx-pdf": "cascade"}, + ) + report_docx_template = fields.Binary( + string="Report docx template", + ) + + @api.depends("report_type", "model") + def _compute_report_name(self): + for record in self: + if ( + record.report_type in ["docx-docx", "docx-pdf"] + and record.model + and record.id + ): + record.report_name = "%s-docx_report+%s" % (record.model, record.id) + else: + record.report_name = False + + def _inverse_report_name(self): + """TODO: write this method""" + pass + + def retrieve_attachment(self, record): + """ + Searc for existing report file in record's attachments by fields: + 1. name + 2. res_model + 3. res_id + """ + result = super().retrieve_attachment(record) + if result: + if self.report_type == "docx-docx": + result = ( + result.filtered( + lambda r: r.mimetype + == "application/vnd.openxmlformats-officedocument.wordprocessingml.document" + ) + or None + ) + elif self.report_type == "docx-pdf": + result = ( + result.filtered(lambda r: r.mimetype == "application/pdf") or None + ) + return result + + @api.model + def _render_docx_pdf(self, res_ids=None, data=None): + """ + Prepares the data for report file rendering, calls for the render method + and handle rendering result. + """ + if not data: + data = {} + data.setdefault("report_type", "pdf") + + # access the report details with sudo() but evaluation context as current user + self_sudo = self.sudo() + + save_in_attachment = OrderedDict() + # Maps the streams in `save_in_attachment` back to the records they came from + # stream_record = dict() + if res_ids: + Model = self.env[self_sudo.model] + record_ids = Model.browse(res_ids) + docx_record_ids = Model + if self_sudo.attachment: + for record_id in record_ids: + attachment = self_sudo.retrieve_attachment(record_id) + if attachment and self_sudo.attachment_use: + # stream = self_sudo._retrieve_stream_from_attachment(attachment) + stream = BytesIO(attachment.raw) + save_in_attachment[record_id.id] = stream + # stream_record[stream] = record_id + if not self_sudo.attachment_use or not attachment: + docx_record_ids += record_id + else: + docx_record_ids = record_ids + res_ids = docx_record_ids.ids + + if save_in_attachment: # and not res_ids: + _logger.info("The PDF report has been generated from attachment.") + # self._raise_on_unreadable_pdfs(save_in_attachment.values(), stream_record) + return self_sudo._post_pdf(save_in_attachment), "pdf" + + docx_content = self._render_docx(res_ids, data=data) + + pdf_content = ( + self._get_pdf_from_office(docx_content) + if gotenberg_imported and check_gotenberg_installed() + else None + ) + + if not pdf_content: + raise UserError( + _( + "Gotenberg converting service not available. The PDF can not be created." + ) + ) + + if res_ids: + # self._raise_on_unreadable_pdfs(save_in_attachment.values(), stream_record) + # saving pdf in attachment. + return ( + self_sudo._post_pdf( + save_in_attachment, pdf_content=pdf_content, res_ids=res_ids + ), + "pdf", + ) + + return pdf_content, "pdf" + + @api.model + def _render_docx_docx(self, res_ids=None, data=None): + """ + Prepares the data for report file rendering, calls for the render method + and handle rendering result. + """ + if not data: + data = {} + data.setdefault("report_type", "docx") + + # access the report details with sudo() but evaluation context as current user + self_sudo = self.sudo() + + save_in_attachment = OrderedDict() + # Maps the streams in `save_in_attachment` back to the records they came from + stream_record = dict() + if res_ids: + Model = self.env[self_sudo.model] + record_ids = Model.browse(res_ids) + docx_record_ids = Model + if self_sudo.attachment: + for record_id in record_ids: + attachment = self_sudo.retrieve_attachment(record_id) + if attachment: + # stream = self_sudo._retrieve_stream_from_attachment(attachment) + stream = BytesIO(attachment.raw) + save_in_attachment[record_id.id] = stream + stream_record[stream] = record_id + if not self_sudo.attachment_use or not attachment: + docx_record_ids += record_id + else: + docx_record_ids = record_ids + res_ids = docx_record_ids.ids + + if save_in_attachment and not res_ids: + _logger.info("The DOCS report has been generated from attachment.") + return self_sudo._post_docx(save_in_attachment), "docx" + + docx_content = self._render_docx(res_ids, data=data) + + if res_ids: + _logger.info( + "The DOCS report has been generated for model: %s, records %s." + % (self_sudo.model, str(res_ids)) + ) + return ( + self_sudo._post_docx( + save_in_attachment, docx_content=docx_content, res_ids=res_ids + ), + "docx", + ) + return docx_content, "docx" + + def _post_pdf(self, save_in_attachment, pdf_content=None, res_ids=None): + """ + Adds pdf file in record's attachments. + TODO: For now bunch generation is not supported. + 2 execution ways: + - save_in_attachment and not res_ids - when get reports from attachments + - res_ids and not save_in_attachment - when generate report. + """ + self_sudo = self.sudo() + attachment_vals_list = [] + if save_in_attachment: + # here get streams from save_in_attachment, make pdf file and return it + # bunch generation here is already realized. + reports_data = list(save_in_attachment.values()) + if len(reports_data) == 1: + # If only one report, no need to merge files. Returns as is. + return reports_data[0].getvalue() + else: + return self._merge_pdfs(reports_data) + for res_id in res_ids: + record = self.env[self_sudo.model].browse(res_id) + attachment_name = safe_eval( + self_sudo.attachment, {"object": record, "time": time} + ) + # Unable to compute a name for the attachment. + if not attachment_name: + continue + attachment_vals_list.append( + { + "name": attachment_name, + "raw": pdf_content, # stream_data['stream'].getvalue(), + "res_model": self_sudo.model, + "res_id": record.id, + "type": "binary", + } + ) + if attachment_vals_list: + attachment_names = ", ".join(x["name"] for x in attachment_vals_list) + try: + self.env["ir.attachment"].create(attachment_vals_list) + except AccessError: + _logger.info( + "Cannot save PDF report %r attachments for user %r", + attachment_names, + self.env.user.display_name, + ) + else: + _logger.info( + "The PDF documents %r are now saved in the database", + attachment_names, + ) + return pdf_content + + def _post_docx(self, save_in_attachment, docx_content=None, res_ids=None): + """ + Adds generated file in attachments. + """ + + def close_streams(streams): + for stream in streams: + try: + stream.close() + except Exception: + pass + + if len(save_in_attachment) == 1 and not docx_content: + return list(save_in_attachment.values())[0].getvalue() + streams = [] + if docx_content: + # Build a record_map mapping id -> record + record_map = { + r.id: r + for r in self.env[self.model].browse( + [res_id for res_id in res_ids if res_id] + ) + } + # If no value in attachment or no record specified, only append the whole docx. + if not record_map or not self.attachment: + streams.append(docx_content) + else: + if len(res_ids) == 1: + # Only one record, so postprocess directly and append the whole docx. + if ( + res_ids[0] in record_map + and not res_ids[0] in save_in_attachment + ): + new_stream = self._postprocess_docx_report( + record_map[res_ids[0]], docx_content + ) + # If the buffer has been modified, mark the old buffer to be closed as well. + if new_stream and new_stream != docx_content: + close_streams([docx_content]) + docx_content = new_stream + streams.append(docx_content) + else: + streams.append(docx_content) + if self.attachment_use: + for stream in save_in_attachment.values(): + streams.append(stream) + if len(streams) == 1: + result = streams[0].getvalue() + else: + try: + result = self._merge_docx(streams) + except Exception as e: + _logger.exception(e) + raise UserError( + _("One of the documents you try to merge caused failure.") + ) + + close_streams(streams) + return result + + def _postprocess_docx_report(self, record, buffer): + """ + Creates the record in the "ir.attachment" model. + """ + attachment_name = safe_eval(self.attachment, {"object": record, "time": time}) + if not attachment_name: + return None + attachment_vals = { + "name": attachment_name, + "raw": buffer.getvalue(), + "res_model": self.model, + "res_id": record.id, + "type": "binary", + } + try: + self.env["ir.attachment"].create(attachment_vals) + except AccessError: + _logger.info( + "Cannot save DOCX report %r as attachment", attachment_vals["name"] + ) + else: + _logger.info( + "The DOCX document %s is now saved in the database", + attachment_vals["name"], + ) + return buffer + + @staticmethod + def _merge_docx(streams): + """ + Joins several docx files into one. + """ + if streams: + writer = Document(streams[0]) + composer = Composer(writer) + for stream in streams[1:]: + reader = Document(stream) + composer.append(reader) + return composer.getvalue() + else: + return streams + + def _render_docx(self, docids: list, data: dict = None): + """ + Receive the data for rendering and calls for it. + + docids: list of record's ids for which report is generated. + data: dict, conains "context", "report_type". + """ + if not data: + data = {} + data.setdefault("report_type", "docx") + data = self._get_rendering_context( + self, docids, data + ) # self contains current record of ir.actions.report model. + return self._render_docx_template(self.report_docx_template, values=data) + + def _render_docx_template(self, template: bytes, values: dict = None): + """ + docx file rendering itself. + """ + if values is None: + values = {} + context = dict(self.env.context, inherit_branding=False) + # Browse the user instead of using the sudo self.env.user + user = self.env["res.users"].browse(self.env.uid) + website = None + if request and hasattr(request, "website"): + if request.website is not None: + website = request.website + context = dict( + context, + translatable=context.get("lang") + != request.env["ir.http"]._get_default_lang().code, + ) + values.update( + record=values["docs"], + time=time, + context_timestamp=lambda t: fields.Datetime.context_timestamp( + self.with_context(tz=user.tz), t + ), + user=user, + res_company=user.company_id, + website=website, + web_base_url=self.env["ir.config_parameter"] + .sudo() + .get_param("web.base.url", default=""), + ) + + record_to_render = values["docs"] + docs = { + key: record_to_render[key] + for key in record_to_render._fields.keys() + if not isinstance(record_to_render[key], fields.Markup) + } + docs.update( + { + key: self._parse_markup(record_to_render[key]) + for key in record_to_render._fields.keys() + if isinstance(record_to_render[key], fields.Markup) + } + ) + values["docs"] = docs + + docx_content = BytesIO() + with BytesIO(b64decode(template)) as template_file: + doc = DocxTemplate(template_file) + doc.render(values) + doc.save(docx_content) + docx_content.seek(0) + return docx_content + + @staticmethod + def _parse_markup(markup_data: fields.Markup): + """ + Extracts data from field of Html type and returns them in text format, + without html tags. + """ + soup = BeautifulSoup(markup_data.__str__()) + data_arr = list(soup.strings) + return "\n".join(data_arr) + + @staticmethod + def _get_pdf_from_office(content_stream): + """ + Converting docx into pdf with Gotenberg service. + """ + result = None + url = convert_pdf_from_office_url() + auth = get_auth() + try: + response = post_request( + url, + files={"file": ("converted_file.docx", content_stream.read())}, + auth=auth, + ) + if response.status_code == codes_request.ok: + result = response.content + else: + _logger.warning( + "Gotenberg response: %s - %s" + % (response.status_code, response.content) + ) + except RequestException as e: + _logger.exception(e) + finally: + return result diff --git a/docx_report/static/description/company_logo.jpg b/docx_report/static/description/company_logo.jpg new file mode 100644 index 0000000..4e826eb Binary files /dev/null and b/docx_report/static/description/company_logo.jpg differ diff --git a/docx_report/static/description/divider.png b/docx_report/static/description/divider.png new file mode 100644 index 0000000..b5269ea Binary files /dev/null and b/docx_report/static/description/divider.png differ diff --git a/docx_report/static/description/index.html b/docx_report/static/description/index.html new file mode 100644 index 0000000..1a494c7 --- /dev/null +++ b/docx_report/static/description/index.html @@ -0,0 +1,753 @@ + + + + + + + + + +
+ +
+

DOCX REPORT

+
+

+ The DOCX REPORT module is a tool for creating templates using the Jinja + template engine. The module allows you to add docx files to the report model as a source template. + Thanks to this, you can create automatically filled-out documents in docx and pdf formats. +

+
+
+ +
+
+
+
+ + Rydlab company logo + +
+
+ + +   Have questions or need support? + +
+
+ +
+
+
+
+
+

Features

+
+
+
Easy creation of auto-filled documents
+

+ There is no need to create a complex HTML template that is difficult to edit + and customize. It is enough to take a template in Docx format and insert the necessary values in + the right places. +

+
+
+
Access to all attributes of the model
+

+ During the creation of the report, the model to which this report belongs is + specified. When creating a report template, we can refer to any attribute of the specified + model. +

+
+
+
Easy template maintenance
+

+ There is no need to change the template in the Odoo code; it is enough to + upload a new template through the user interface. +

+
+
+
The amount-to-words methods are available
+

+ Thanks to these methods, we can insert numbers and sums with currencies in + words and round the numbers to the desired accuracy. +

+
+
+
+ +
+
+
+

How to use

+
+
Step 1: Install the module
+
+ +
+
+

+ Open the Apps menu in your Odoo and install the module "DOCX report". +

+
+
+ +
+
+
+
Step 2: Activate the Developer Mode
+
+ +
+
+

+ To create a new report in the user interface, we need to activate the + developer mode. +

+
+
+ +
+
+
+
Step 3: Open Reports
+
+ +
+
+

+ Now you should go back to the settings. Click "Technical", scroll down the + list, and click "Reports". +

+
+
+ +
+
+
+
Step 4: Create Docx template
+
+ +
+
+

+ 1. To get model attributes like model field values, use the word "docs" + + (dot) + model field name. +

+

+ 2. To call a model method that returns a value, use the word "record" + . + (dot) + model method name + () to call it. +

+

3. Use double curly braces "{{ }}" to call methods and attributes.

+

+ 4. Use curly braces with the percentage sign "{% %}" to create local + variables and use loops and if conditions. +

+
+
+ +
+
+
+
Step 5: Reports list view
+
+ +
+
+

+ When the Reports list view will be opened, click the "New" button to create a + new report. +

+
+
+ +
+
+
+
Step 6: Create a Report
+
+ +
+
+

1. To create a new report, you should fill out the form.

+

+ 2. "Action name" is the name that will be shown in the Print menu of the + model. +

+

3. "Report Type" should be DOCS or DOCX (PDF). The gotenberg service is used + to create pdf files. To use it, you need to install the Odoo module to communicate with the + service and enter the access details.

+

+ 4. "Model name" is the name of the model. Fields and methods will be derived + from this model. +

+

+ 5. "Report docx template" is the template file that was created at step 4. +

+

+ 6. "Printed report name" is the name of the file after generation. +

+

+ 7. To add this report to the Print menu, you should click on the button "Add + in the Print". +

+
+
+ +
+
+
+
Step 7: Create a custom field
+
+ +
+
+

+ 1. Custom fields are needed to get data that is not in the fields of the model associated with + the + report. Thanks to them, you can get data from other models, for example, through the reference + fields of the current model. +

+ 2. To create a custom field you should click "Custom Fields" menu on the form + and then click "Add a line" +

+

3. After that, write your Python code for the new variable.

+

4. In the template, custom fields are available by the name specified in the + "tech_name" field of the custom field entry. For exapmle: {{ contract_date }}.

+
+
+ +
+
+
+
Step 8: Print the Report
+
+ +
+
+

+ After printing the report the file will be saved. The information from the + model will complete the template. +

+
+
+ +
+
+ +
+
Step 9: Make a report from the Python code
+
+ +
+
+

If you want to make the report from the Python code, you should make an + ir.action.report record and an ir.attachment record that is connected to the first one. Add the + path to the Docx template from step 4.

+
+
+ +
+
+
+
+ + + + \ No newline at end of file diff --git a/docx_report/static/description/screenshots/1_intall_app.jpg b/docx_report/static/description/screenshots/1_intall_app.jpg new file mode 100644 index 0000000..0c53487 Binary files /dev/null and b/docx_report/static/description/screenshots/1_intall_app.jpg differ diff --git a/docx_report/static/description/screenshots/2_activate_developer_mode.jpg b/docx_report/static/description/screenshots/2_activate_developer_mode.jpg new file mode 100644 index 0000000..48fc97e Binary files /dev/null and b/docx_report/static/description/screenshots/2_activate_developer_mode.jpg differ diff --git a/docx_report/static/description/screenshots/3_open_reports.jpg b/docx_report/static/description/screenshots/3_open_reports.jpg new file mode 100644 index 0000000..e0ee81d Binary files /dev/null and b/docx_report/static/description/screenshots/3_open_reports.jpg differ diff --git a/docx_report/static/description/screenshots/4_docx_template.jpg b/docx_report/static/description/screenshots/4_docx_template.jpg new file mode 100644 index 0000000..554a47b Binary files /dev/null and b/docx_report/static/description/screenshots/4_docx_template.jpg differ diff --git a/docx_report/static/description/screenshots/5_reporst_list.jpg b/docx_report/static/description/screenshots/5_reporst_list.jpg new file mode 100644 index 0000000..b8320c7 Binary files /dev/null and b/docx_report/static/description/screenshots/5_reporst_list.jpg differ diff --git a/docx_report/static/description/screenshots/6_report_form.jpg b/docx_report/static/description/screenshots/6_report_form.jpg new file mode 100644 index 0000000..dce6037 Binary files /dev/null and b/docx_report/static/description/screenshots/6_report_form.jpg differ diff --git a/docx_report/static/description/screenshots/7_cutom_fields.jpg b/docx_report/static/description/screenshots/7_cutom_fields.jpg new file mode 100644 index 0000000..0ff23c3 Binary files /dev/null and b/docx_report/static/description/screenshots/7_cutom_fields.jpg differ diff --git a/docx_report/static/description/screenshots/8_result.jpg b/docx_report/static/description/screenshots/8_result.jpg new file mode 100644 index 0000000..bf5c62b Binary files /dev/null and b/docx_report/static/description/screenshots/8_result.jpg differ diff --git a/docx_report/static/description/screenshots/9_report_code.jpg b/docx_report/static/description/screenshots/9_report_code.jpg new file mode 100644 index 0000000..ffe9052 Binary files /dev/null and b/docx_report/static/description/screenshots/9_report_code.jpg differ diff --git a/docx_report/static/src/css/mimetypes.css b/docx_report/static/src/css/mimetypes.css new file mode 100755 index 0000000..e7b0185 --- /dev/null +++ b/docx_report/static/src/css/mimetypes.css @@ -0,0 +1,4 @@ +.o_image[data-mimetype$='msword'], +.o_image[data-mimetype$='application/vnd.openxmlformats-officedocument.wordprocessingml.document'] { + background-image: url('/docx_report/static/src/img/msword.png'); +} diff --git a/docx_report/static/src/img/msword.png b/docx_report/static/src/img/msword.png new file mode 100755 index 0000000..5ac980f Binary files /dev/null and b/docx_report/static/src/img/msword.png differ diff --git a/docx_report/static/src/js/action_manager_report.js b/docx_report/static/src/js/action_manager_report.js new file mode 100644 index 0000000..012a629 --- /dev/null +++ b/docx_report/static/src/js/action_manager_report.js @@ -0,0 +1,54 @@ +/** @odoo-module **/ + +import { registry } from "@web/core/registry"; +import { download } from "@web/core/network/download"; + +async function docxHandler(action, options, env) { + let reportType = null; + if (action.report_type === "docx-docx") { + reportType = "docx"; + } else if (action.report_type === "docx-pdf") { + reportType = "pdf"; + } + if (reportType) { + // Make URL + let url = `/report/${reportType}/${action.report_name}`; + const actionContext = action.context || {}; + if (action.data && JSON.stringify(action.data) !== "{}") { + // build a query string with `action.data` (it's the place where reports + // using a wizard to customize the output traditionally put their options) + const options = encodeURIComponent(JSON.stringify(action.data)); + const context = encodeURIComponent(JSON.stringify(actionContext)); + url += `?options=${options}&context=${context}`; + } else { + if (actionContext.active_ids) { + url += `/${actionContext.active_ids.join(",")}`; + } + } + // Download report + env.services.ui.block(); + try { + const template_type = (action.report_type && action.report_type.split("-")[0]) || "docx"; + const type = template_type + "-" + url.split("/")[2]; + await download({ + url: "/report/download", + data: { + data: JSON.stringify([url, type]), + context: JSON.stringify(Object.assign({}, action.context, env.services.user.context)), + }, + }); + } finally { + env.services.ui.unblock(); + } + const onClose = options.onClose; + if (action.close_on_report_download) { + return env.services.action.doAction({type: "ir.actions.act_window_close"}, {onClose}); + } else if (onClose) { + onClose(); + } + return Promise.resolve(true); + } + return Promise.resolve(false); +} + +registry.category("ir.actions.report handlers").add("docx_handler", docxHandler); diff --git a/docx_report/views/ir_actions_report_views.xml b/docx_report/views/ir_actions_report_views.xml new file mode 100755 index 0000000..c0f56f9 --- /dev/null +++ b/docx_report/views/ir_actions_report_views.xml @@ -0,0 +1,18 @@ + + + + + ir.actions.report.inherit.view.form + ir.actions.report + + + + {'readonly': [('report_type', 'in', ['docx-docx', 'docx-pdf'])], 'required': [('report_type', 'not in', ['docx-docx', 'docx-pdf'])]} + + + + + + + +