Update repository

This commit is contained in:
Владислав Костров 2024-03-29 11:54:24 +05:00
parent c90c242b38
commit 297453dfb0
25 changed files with 1645 additions and 0 deletions

48
docx_report/README.md Executable file
View File

@ -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.

2
docx_report/__init__.py Executable file
View File

@ -0,0 +1,2 @@
from . import controllers
from . import models

30
docx_report/__manifest__.py Executable file
View File

@ -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",
],
},
}

View File

@ -0,0 +1 @@
from . import main

View File

@ -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)))

View File

@ -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 ""

75
docx_report/i18n/ru.po Normal file
View File

@ -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 и загружен пользователем."

1
docx_report/models/__init__.py Executable file
View File

@ -0,0 +1 @@
from . import ir_actions_report

View File

@ -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

Binary file not shown.

After

Width:  |  Height:  |  Size: 64 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB

View File

@ -0,0 +1,753 @@
<!DOCTYPE html>
<html lang="en-US" data-website-id="1" data-oe-company-name="Rydlab">
<head>
<meta charset="UTF-8">
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/css/bootstrap.min.css" rel="stylesheet"
integrity="sha384-EVSTQN3/azprG1Anm3QDgpJLIm9Nao0Yz1ztcQTwFspd3yD65VohhpuuCOmLASjC" crossorigin="anonymous">
</head>
<body>
<header>
<!-- Module name and short description of the module. -->
<section>
<h1 class="display-3" style="
text-align: center;
font-family: FontAwesome;
margin-bottom: 10px;
font-weight: 700;
color: #eb8900;
">DOCX REPORT</h1>
<div style="
font-family: FontAwesome;
font-size: 26px;
margin: 0 auto;
width: 70%;
text-align: center;
margin-top: 5px;
margin-bottom: 5px;
">
<p style="text-align: center">
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.
</p>
</div>
<div style="
text-align: center;
margin-top: 10px;
margin-bottom: 10px;
height: 30px;
">
<img style="height: 10px; width: 40%" src="divider.png">
</div>
</section>
<section>
<div style="width: 15%; margin: 0 auto">
<a href="https://rydlab.ru/" target="_blank" style="color: black; text-decoration: none">
<img style="display: block; max-width: 100%; margin: 0 auto" src="company_logo.jpg"
alt="Rydlab company logo">
</a>
</div>
<div style="
width: 30%;
margin: 0 auto;
text-align: center;
margin-top: 20px;
">
<a href="mailto:company@rydlab.ru?subject=Odoo Support / Development / Module: Contract&amp;body="
style="
border-radius: 42px;
display: inline-block;
cursor: pointer;
color: #ffffff !important;
font-family: 'Roboto', sans-serif;
font-size: 23px;
padding: 10px 33px;
background-color: black;
text-decoration: none;
border-radius: 45px;
">
<i class="fa fa-envelope"></i>
&nbsp; Have questions or need support?
</a>
</div>
<div style="
text-align: center;
margin-top: 10px;
margin-bottom: 10px;
height: 30px;
">
<img style="height: 10px; width: 70%" src="divider.png" />
</div>
</section>
</header>
<main>
<section>
<h3 class="display-5" style="
text-align: center;
font-family: FontAwesome;
margin-bottom: 10px;
font-weight: 700;
">Features</h3>
<div class="d-flex flex-wrap" style="display: flex; flex-wrap: wrap; width: 70%; margin: 0 auto">
<div style="
box-sizing: border-box;
padding: 10px 10px;
border: 2px solid #dfdee8;
text-align: center;
width: 48%;
word-wrap: break-word;
font-family: FontAwesome;
font-size: 20px;
margin: 0 auto;
margin-top: 10px;
margin-bottom: 10px;
">
<h5 style="
text-align: center;
font-family: FontAwesome;
font-weight: 700;
font-size: 22px;
margin-bottom: 1px;
">Easy creation of auto-filled documents</h5>
<p style="margin-top: 1px; margin-bottom: 2px">
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.
</p>
</div>
<div style="
box-sizing: border-box;
padding: 10px 10px;
border: 2px solid #dfdee8;
text-align: center;
width: 48%;
word-wrap: break-word;
font-family: FontAwesome;
font-size: 20px;
margin: 0 auto;
margin-top: 10px;
margin-bottom: 10px;
">
<h5 style="
text-align: center;
font-family: FontAwesome;
font-weight: 700;
font-size: 22px;
margin-bottom: 1px;
">Access to all attributes of the model</h5>
<p style="margin-top: 1px; margin-bottom: 2px">
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.
</p>
</div>
<div style="
box-sizing: border-box;
padding: 10px 10px;
border: 2px solid #dfdee8;
text-align: center;
width: 48%;
word-wrap: break-word;
font-family: FontAwesome;
font-size: 20px;
margin: 0 auto;
margin-top: 10px;
margin-bottom: 10px;
">
<h5 style="
text-align: center;
font-family: FontAwesome;
font-weight: 700;
font-size: 22px;
margin-bottom: 1px;
">Easy template maintenance</h5>
<p style="margin-top: 1px; margin-bottom: 2px">
There is no need to change the template in the Odoo code; it is enough to
upload a new template through the user interface.
</p>
</div>
<div style="
box-sizing: border-box;
padding: 10px 10px;
border: 2px solid #dfdee8;
text-align: center;
width: 48%;
word-wrap: break-word;
font-family: FontAwesome;
font-size: 20px;
margin: 0 auto;
margin-top: 10px;
margin-bottom: 10px;
">
<h5 style="
text-align: center;
font-family: FontAwesome;
font-weight: 700;
font-size: 22px;
margin-bottom: 1px;
">The amount-to-words methods are available</h5>
<p style="margin-top: 1px; margin-bottom: 2px">
Thanks to these methods, we can insert numbers and sums with currencies in
words and round the numbers to the desired accuracy.
</p>
</div>
</div>
<div style="
text-align: center;
margin-top: 10px;
margin-bottom: 10px;
height: 30px;
">
<img style="height: 10px; width: 70%" src="divider.png" />
</div>
</section>
<section>
<h3 class="display-5" style="
text-align: center;
font-family: FontAwesome;
margin-bottom: 10px;
font-weight: 700;
">How to use</h3>
<div>
<h5 class="display-6" style="
text-align: center;
font-family: FontAwesome;
margin-bottom: 10px;
font-weight: 700;
">Step 1: Install the module</h5>
<div>
<img style="
display: block;
border: none;
width: 70%;
position: relative;
margin: 0 auto;
margin-top: 20px;
" src="screenshots/1_intall_app.jpg">
</div>
<div>
<p style="
font-family: FontAwesome;
font-size: 26px;
margin: 0 auto;
width: 70%;
text-align: center;
margin-top: 5px;
margin-bottom: 5px;
">
Open the Apps menu in your Odoo and install the module "DOCX report".
</p>
</div>
<div style="
text-align: center;
margin-top: 10px;
margin-bottom: 10px;
height: 30px;
">
<img style="height: 10px; width: 70%" src="divider.png" />
</div>
</div>
<div>
<h5 class="display-6" style="
text-align: center;
font-family: FontAwesome;
margin-bottom: 10px;
font-weight: 700;
">Step 2: Activate the Developer Mode</h5>
<div>
<img style="
display: block;
border: none;
width: 70%;
position: relative;
margin: 0 auto;
margin-top: 20px;
" src="screenshots/2_activate_developer_mode.jpg">
</div>
<div>
<p style="
font-family: FontAwesome;
font-size: 26px;
margin: 0 auto;
width: 70%;
text-align: center;
margin-top: 5px;
margin-bottom: 5px;
">
To create a new report in the user interface, we need to activate the
developer mode.
</p>
</div>
<div style="
text-align: center;
margin-top: 10px;
margin-bottom: 10px;
height: 30px;
">
<img style="height: 10px; width: 70%" src="divider.png" />
</div>
</div>
<div>
<h5 class="display-6" style="
text-align: center;
font-family: FontAwesome;
margin-bottom: 10px;
font-weight: 700;
">Step 3: Open Reports</h5>
<div>
<img style="
display: block;
border: none;
width: 70%;
position: relative;
margin: 0 auto;
margin-top: 20px;
" src="screenshots/3_open_reports.jpg">
</div>
<div>
<p style="
font-family: FontAwesome;
font-size: 26px;
margin: 0 auto;
width: 70%;
text-align: center;
margin-top: 5px;
margin-bottom: 5px;
">
Now you should go back to the settings. Click "Technical", scroll down the
list, and click "Reports".
</p>
</div>
<div style="
text-align: center;
margin-top: 10px;
margin-bottom: 10px;
height: 30px;
">
<img style="height: 10px; width: 70%" src="divider.png" />
</div>
</div>
<div>
<h5 class="display-6" style="
text-align: center;
font-family: FontAwesome;
margin-bottom: 10px;
font-weight: 700;
">Step 4: Create Docx template</h5>
<div>
<img style="
display: block;
border: none;
width: 70%;
position: relative;
margin: 0 auto;
margin-top: 20px;
" src="screenshots/4_docx_template.jpg">
</div>
<div>
<p style="
font-family: FontAwesome;
font-size: 26px;
margin: 0 auto;
width: 70%;
text-align: center;
margin-top: 5px;
margin-bottom: 5px;
">
1. To get model attributes like model field values, use the word "docs" +
(dot) + model field name.
</p>
<p style="
font-family: FontAwesome;
font-size: 26px;
margin: 0 auto;
width: 70%;
text-align: center;
margin-top: 5px;
margin-bottom: 5px;
">
2. To call a model method that returns a value, use the word "record" + .
(dot) + model method name + () to call it.
</p>
<p style="
font-family: FontAwesome;
font-size: 26px;
margin: 0 auto;
width: 70%;
text-align: center;
margin-top: 5px;
margin-bottom: 5px;
">3. Use double curly braces "{{ }}" to call methods and attributes.</p>
<p style="
font-family: FontAwesome;
font-size: 26px;
margin: 0 auto;
width: 70%;
text-align: center;
margin-top: 5px;
margin-bottom: 5px;
">
4. Use curly braces with the percentage sign "{% %}" to create local
variables and use loops and if conditions.
</p>
</div>
<div style="
text-align: center;
margin-top: 10px;
margin-bottom: 10px;
height: 30px;
">
<img style="height: 10px; width: 70%" src="divider.png" />
</div>
</div>
<div>
<h5 class="display-6" style="
text-align: center;
font-family: FontAwesome;
margin-bottom: 10px;
font-weight: 700;
">Step 5: Reports list view</h5>
<div>
<img style="
display: block;
border: none;
width: 70%;
position: relative;
margin: 0 auto;
margin-top: 20px;
" src="screenshots/5_reporst_list.jpg">
</div>
<div>
<p style="
font-family: FontAwesome;
font-size: 26px;
margin: 0 auto;
width: 70%;
text-align: center;
margin-top: 5px;
margin-bottom: 5px;
">
When the Reports list view will be opened, click the "New" button to create a
new report.
</p>
</div>
<div style="
text-align: center;
margin-top: 10px;
margin-bottom: 10px;
height: 30px;
">
<img style="height: 10px; width: 70%" src="divider.png" />
</div>
</div>
<div>
<h5 class="display-6" style="
text-align: center;
font-family: FontAwesome;
margin-bottom: 10px;
font-weight: 700;
">Step 6: Create a Report</h5>
<div>
<img style="
display: block;
border: none;
width: 70%;
position: relative;
margin: 0 auto;
margin-top: 20px;
" src="screenshots/6_report_form.jpg">
</div>
<div>
<p style="
font-family: FontAwesome;
font-size: 26px;
margin: 0 auto;
width: 70%;
text-align: center;
margin-top: 5px;
margin-bottom: 5px;
">1. To create a new report, you should fill out the form.</p>
<p style="
font-family: FontAwesome;
font-size: 26px;
margin: 0 auto;
width: 70%;
text-align: center;
margin-top: 5px;
margin-bottom: 5px;
">
2. "Action name" is the name that will be shown in the Print menu of the
model.
</p>
<p style="
font-family: FontAwesome;
font-size: 26px;
margin: 0 auto;
width: 70%;
text-align: center;
margin-top: 5px;
margin-bottom: 5px;
">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.</p>
<p style="
font-family: FontAwesome;
font-size: 26px;
margin: 0 auto;
width: 70%;
text-align: center;
margin-top: 5px;
margin-bottom: 5px;
">
4. "Model name" is the name of the model. Fields and methods will be derived
from this model.
</p>
<p style="
font-family: FontAwesome;
font-size: 26px;
margin: 0 auto;
width: 70%;
text-align: center;
margin-top: 5px;
margin-bottom: 5px;
">
5. "Report docx template" is the template file that was created at step 4.
</p>
<p style="
font-family: FontAwesome;
font-size: 26px;
margin: 0 auto;
width: 70%;
text-align: center;
margin-top: 5px;
margin-bottom: 5px;
">
6. "Printed report name" is the name of the file after generation.
</p>
<p style="
font-family: FontAwesome;
font-size: 26px;
margin: 0 auto;
width: 70%;
text-align: center;
margin-top: 5px;
margin-bottom: 5px;
">
7. To add this report to the Print menu, you should click on the button "Add
in the Print".
</p>
</div>
<div style="
text-align: center;
margin-top: 10px;
margin-bottom: 10px;
height: 30px;
">
<img style="height: 10px; width: 70%" src="divider.png" />
</div>
</div>
<div>
<h5 class="display-6" style="
text-align: center;
font-family: FontAwesome;
margin-bottom: 10px;
font-weight: 700;
">Step 7: Create a custom field</h5>
<div>
<img style="
display: block;
border: none;
width: 70%;
position: relative;
margin: 0 auto;
margin-top: 20px;
" src="screenshots/7_cutom_fields.jpg">
</div>
<div>
<p style="
font-family: FontAwesome;
font-size: 26px;
margin: 0 auto;
width: 70%;
text-align: center;
margin-top: 5px;
margin-bottom: 5px;
">
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.
<p style="
font-family: FontAwesome;
font-size: 26px;
margin: 0 auto;
width: 70%;
text-align: center;
margin-top: 5px;
margin-bottom: 5px;
">
2. To create a custom field you should click "Custom Fields" menu on the form
and then click "Add a line"
</p>
<p style="
font-family: FontAwesome;
font-size: 26px;
margin: 0 auto;
width: 70%;
text-align: center;
margin-top: 5px;
margin-bottom: 5px;
">3. After that, write your Python code for the new variable.</p>
<p style="
font-family: FontAwesome;
font-size: 26px;
margin: 0 auto;
width: 70%;
text-align: center;
margin-top: 5px;
margin-bottom: 5px;
">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 }}.</p>
</div>
<div style="
text-align: center;
margin-top: 10px;
margin-bottom: 10px;
height: 30px;
">
<img style="height: 10px; width: 70%" src="divider.png" />
</div>
</div>
<div>
<h5 class="display-6" style="
text-align: center;
font-family: FontAwesome;
margin-bottom: 10px;
font-weight: 700;
">Step 8: Print the Report</h5>
<div>
<img style="
display: block;
border: none;
width: 70%;
position: relative;
margin: 0 auto;
margin-top: 20px;
" src="screenshots/8_result.jpg">
</div>
<div>
<p style="
font-family: FontAwesome;
font-size: 26px;
margin: 0 auto;
width: 70%;
text-align: center;
margin-top: 5px;
margin-bottom: 5px;
">
After printing the report the file will be saved. The information from the
model will complete the template.
</p>
</div>
<div style="
text-align: center;
margin-top: 10px;
margin-bottom: 10px;
height: 30px;
">
<img style="height: 10px; width: 70%" src="divider.png" />
</div>
</div>
</div>
<div>
<h5 class="display-6" style="
text-align: center;
font-family: FontAwesome;
margin-bottom: 10px;
font-weight: 700;
">Step 9: Make a report from the Python code</h5>
<div>
<img style="
display: block;
border: none;
width: 70%;
position: relative;
margin: 0 auto;
margin-top: 20px;
" src="screenshots/9_report_code.jpg">
</div>
<div>
<p style="
font-family: FontAwesome;
font-size: 26px;
margin: 0 auto;
width: 70%;
text-align: center;
margin-top: 5px;
margin-bottom: 5px;
">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.</p>
</div>
<div style="
text-align: center;
margin-top: 10px;
margin-bottom: 10px;
height: 30px;
">
<img style="height: 10px; width: 70%" src="divider.png" />
</div>
</div>
</section>
</main>
<footer style="margin-bottom: 70px">
<div style="width: 15%; margin: 0 auto">
<a href="https://rydlab.ru/" target="_blank" style="color: black; text-decoration: none">
<img style="display: block; max-width: 100%; margin: 0 auto" src="company_logo.jpg"
alt="Rydlab company logo">
</a>
</div>
<div style="margin: 0 auto; margin-top: 10px; margin-bottom: 5px; padding: 0">
<p style="
font-family: FontAwesome;
font-size: 26px;
margin: 0 auto;
width: 70%;
text-align: center;
margin-top: 5px;
margin-bottom: 5px;
">Need any help for this module?</p>
<p style="
font-family: FontAwesome;
font-size: 26px;
margin: 0 auto;
width: 70%;
text-align: center;
margin-top: 5px;
margin-bottom: 5px;
">Contact us for your queries</p>
<p style="
font-family: FontAwesome;
font-size: 26px;
margin: 0 auto;
width: 70%;
text-align: center;
margin-top: 5px;
margin-bottom: 5px;
font-weight: 700;
"> Email: company@rydlab.ru</p>
</div>
<div style="
text-align: center;
margin-top: 10px;
margin-bottom: 10px;
height: 30px;
">
<img style="height: 10px; width: 70%" src="divider.png">
</div>
</footer>
</body>
</html>

Binary file not shown.

After

Width:  |  Height:  |  Size: 133 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 251 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 276 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 852 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 437 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 239 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 222 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 669 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 183 KiB

View File

@ -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');
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

View File

@ -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);

View File

@ -0,0 +1,18 @@
<?xml version='1.0' encoding='utf-8'?>
<odoo>
<record id="act_report_xml_inherit_view_form" model="ir.ui.view">
<field name="name">ir.actions.report.inherit.view.form</field>
<field name="model">ir.actions.report</field>
<field name="inherit_id" ref="base.act_report_xml_view"/>
<field name="arch" type="xml">
<xpath expr="//field[@name='report_name']" position="attributes">
<attribute name="attrs">{'readonly': [('report_type', 'in', ['docx-docx', 'docx-pdf'])], 'required': [('report_type', 'not in', ['docx-docx', 'docx-pdf'])]}</attribute>
</xpath>
<xpath expr="//field[@name='report_name']" position="after">
<field name="report_docx_template" attrs="{'required': [('report_type', 'in', ['docx-docx', 'docx-pdf'])]}"/>
</xpath>
</field>
</record>
</odoo>