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.
+
+ 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'])]}
+
+
+
+
+
+
+
+