diff --git a/README.md b/README.md index d6d67fc6b9..fa7c38f6ce 100644 --- a/README.md +++ b/README.md @@ -23,6 +23,7 @@ addon | version | maintainers | summary --- | --- | --- | --- [base_comment_template](base_comment_template/) | 19.0.0.0.0 | | Add conditional mako template to any reporton models that inherits comment.template. [bi_sql_editor](bi_sql_editor/) | 19.0.1.0.1 | legalsylvain | BI Views builder, based on Materialized or Normal SQL Views +[pdf_xml_attachment](pdf_xml_attachment/) | 19.0.1.0.0 | simahawk alexis-via | Provides helpers to work w/ PDFs and XML attachments [report_context](report_context/) | 19.0.1.0.0 | | Adding context to reports [report_csv](report_csv/) | 19.0.1.0.0 | | Base module to create csv report [report_partner_address](report_partner_address/) | 19.0.1.0.0 | yostashiro aungkokolin1997 | Translatable partner address details for reports and portal diff --git a/pdf_xml_attachment/README.rst b/pdf_xml_attachment/README.rst new file mode 100644 index 0000000000..aee6ffce84 --- /dev/null +++ b/pdf_xml_attachment/README.rst @@ -0,0 +1,112 @@ +.. image:: https://odoo-community.org/readme-banner-image + :target: https://odoo-community.org/get-involved?utm_source=readme + :alt: Odoo Community Association + +================== +PDF XML attachment +================== + +.. + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! This file is generated by oca-gen-addon-readme !! + !! changes will be overwritten. !! + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! source digest: sha256:371ba278f875e21635be2293b5963acfc68e85c1ff13d55e8d7ff5049dbbe72f + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + +.. |badge1| image:: https://img.shields.io/badge/maturity-Beta-yellow.png + :target: https://odoo-community.org/page/development-status + :alt: Beta +.. |badge2| image:: https://img.shields.io/badge/license-LGPL--3-blue.png + :target: http://www.gnu.org/licenses/lgpl-3.0-standalone.html + :alt: License: LGPL-3 +.. |badge3| image:: https://img.shields.io/badge/github-OCA%2Freporting--engine-lightgray.png?logo=github + :target: https://github.com/OCA/reporting-engine/tree/19.0/pdf_xml_attachment + :alt: OCA/reporting-engine +.. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png + :target: https://translation.odoo-community.org/projects/reporting-engine-19-0/reporting-engine-19-0-pdf_xml_attachment + :alt: Translate me on Weblate +.. |badge5| image:: https://img.shields.io/badge/runboat-Try%20me-875A7B.png + :target: https://runboat.odoo-community.org/builds?repo=OCA/reporting-engine&target_branch=19.0 + :alt: Try me on Runboat + +|badge1| |badge2| |badge3| |badge4| |badge5| + +Technical module to share easily deal with XML attachments in PDF files. + +**Table of contents** + +.. contents:: + :local: + +Usage +===== + +Inside Odoo env: + +:: + + res = env["pdf.xml.tool"].pdf_get_xml_files(pdf_filecontent) + + new_pdf_filecontent = env["pdf.xml.tool"].pdf_embed_xml(pdf_filecontent, filename, xml) + +Outside Odoo env: + +:: + + from odoo.addons.pdf_xml_attachment.utils import PDFParser + [...] + res = PDFParser(pdf_filecontent).get_xml_files() + +Bug Tracker +=========== + +Bugs are tracked on `GitHub Issues `_. +In case of trouble, please check there if your issue has already been reported. +If you spotted it first, help us to smash it by providing a detailed and welcomed +`feedback `_. + +Do not contact contributors directly about support or help with technical issues. + +Credits +======= + +Authors +------- + +* Camptocamp + +Contributors +------------ + +- Simone Orsi +- Alexis de Lattre +- Jacques-Etienne Baudoux (BCIM) + +Maintainers +----------- + +This module is maintained by the OCA. + +.. image:: https://odoo-community.org/logo.png + :alt: Odoo Community Association + :target: https://odoo-community.org + +OCA, or the Odoo Community Association, is a nonprofit organization whose +mission is to support the collaborative development of Odoo features and +promote its widespread use. + +.. |maintainer-simahawk| image:: https://github.com/simahawk.png?size=40px + :target: https://github.com/simahawk + :alt: simahawk +.. |maintainer-alexis-via| image:: https://github.com/alexis-via.png?size=40px + :target: https://github.com/alexis-via + :alt: alexis-via + +Current `maintainers `__: + +|maintainer-simahawk| |maintainer-alexis-via| + +This module is part of the `OCA/reporting-engine `_ project on GitHub. + +You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute. diff --git a/pdf_xml_attachment/__init__.py b/pdf_xml_attachment/__init__.py new file mode 100644 index 0000000000..0650744f6b --- /dev/null +++ b/pdf_xml_attachment/__init__.py @@ -0,0 +1 @@ +from . import models diff --git a/pdf_xml_attachment/__manifest__.py b/pdf_xml_attachment/__manifest__.py new file mode 100644 index 0000000000..5e2d616684 --- /dev/null +++ b/pdf_xml_attachment/__manifest__.py @@ -0,0 +1,17 @@ +# Copyright 2022 Camptocamp SA +# @author: Simone Orsi +# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). + +{ + "name": "PDF XML attachment", + "version": "19.0.1.0.0", + "category": "Tools", + "license": "LGPL-3", + "summary": "Provides helpers to work w/ PDFs and XML attachments", + "author": "Camptocamp, Odoo Community Association (OCA)", + "maintainers": ["simahawk", "alexis-via"], + "website": "https://github.com/OCA/reporting-engine", + "depends": [ + "base", + ], +} diff --git a/pdf_xml_attachment/i18n/es.po b/pdf_xml_attachment/i18n/es.po new file mode 100644 index 0000000000..59116fb6b4 --- /dev/null +++ b/pdf_xml_attachment/i18n/es.po @@ -0,0 +1,22 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * pdf_xml_attachment +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 16.0\n" +"Report-Msgid-Bugs-To: \n" +"PO-Revision-Date: 2023-08-07 11:09+0000\n" +"Last-Translator: Ivorra78 \n" +"Language-Team: none\n" +"Language: es\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Plural-Forms: nplurals=2; plural=n != 1;\n" +"X-Generator: Weblate 4.17\n" + +#. module: pdf_xml_attachment +#: model:ir.model,name:pdf_xml_attachment.model_pdf_xml_tool +msgid "PDF XML attachment" +msgstr "Ayudante de PDF" diff --git a/pdf_xml_attachment/i18n/hr.po b/pdf_xml_attachment/i18n/hr.po new file mode 100644 index 0000000000..cf62c59f29 --- /dev/null +++ b/pdf_xml_attachment/i18n/hr.po @@ -0,0 +1,23 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * pdf_xml_attachment +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 16.0\n" +"Report-Msgid-Bugs-To: \n" +"PO-Revision-Date: 2023-06-02 12:09+0000\n" +"Last-Translator: Bole \n" +"Language-Team: none\n" +"Language: hr\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Plural-Forms: nplurals=3; plural=n%10==1 && n%100!=11 ? 0 : n%10>=2 && " +"n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2;\n" +"X-Generator: Weblate 4.17\n" + +#. module: pdf_xml_attachment +#: model:ir.model,name:pdf_xml_attachment.model_pdf_xml_tool +msgid "PDF XML attachment" +msgstr "PDF Pomoćnik" diff --git a/pdf_xml_attachment/i18n/it.po b/pdf_xml_attachment/i18n/it.po new file mode 100644 index 0000000000..0025bce5f5 --- /dev/null +++ b/pdf_xml_attachment/i18n/it.po @@ -0,0 +1,22 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * pdf_xml_attachment +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 17.0\n" +"Report-Msgid-Bugs-To: \n" +"PO-Revision-Date: 2024-02-04 23:45+0000\n" +"Last-Translator: mymage \n" +"Language-Team: none\n" +"Language: it\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Plural-Forms: nplurals=2; plural=n != 1;\n" +"X-Generator: Weblate 4.17\n" + +#. module: pdf_xml_attachment +#: model:ir.model,name:pdf_xml_attachment.model_pdf_xml_tool +msgid "PDF XML attachment" +msgstr "Aiuto PDF" diff --git a/pdf_xml_attachment/i18n/pdf_xml_attachment.pot b/pdf_xml_attachment/i18n/pdf_xml_attachment.pot new file mode 100644 index 0000000000..c81b6da23d --- /dev/null +++ b/pdf_xml_attachment/i18n/pdf_xml_attachment.pot @@ -0,0 +1,29 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * pdf_xml_attachment +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 19.0\n" +"Report-Msgid-Bugs-To: \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: pdf_xml_attachment +#: model:ir.model.fields,field_description:pdf_xml_attachment.field_pdf_xml_tool__display_name +msgid "Display Name" +msgstr "" + +#. module: pdf_xml_attachment +#: model:ir.model.fields,field_description:pdf_xml_attachment.field_pdf_xml_tool__id +msgid "ID" +msgstr "" + +#. module: pdf_xml_attachment +#: model:ir.model,name:pdf_xml_attachment.model_pdf_xml_tool +msgid "PDF XML attachment" +msgstr "" diff --git a/pdf_xml_attachment/models/__init__.py b/pdf_xml_attachment/models/__init__.py new file mode 100644 index 0000000000..d533863bba --- /dev/null +++ b/pdf_xml_attachment/models/__init__.py @@ -0,0 +1 @@ +from . import helper diff --git a/pdf_xml_attachment/models/helper.py b/pdf_xml_attachment/models/helper.py new file mode 100644 index 0000000000..b95f09173f --- /dev/null +++ b/pdf_xml_attachment/models/helper.py @@ -0,0 +1,50 @@ +# Copyright 2022 Camptocamp SA +# @author: Simone Orsi +# Copyright 2023 Jacques-Etienne Baudoux (BCIM) +# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). + +import io +import logging + +from odoo import api, models +from odoo.tools.pdf import NameObject, OdooPdfFileReader, OdooPdfFileWriter + +from ..utils import PDFParser + +_logger = logging.getLogger(__name__) + + +class PDFHelper(models.AbstractModel): + _name = "pdf.xml.tool" + _description = "PDF XML attachment" + + _PDF_PARSER_KLASS = PDFParser + + @api.model + def pdf_get_xml_files(self, pdf_file): + """Extract XML attachments from pdf + + :param pdf_file: binary PDF file content + :returns: a dict like {$filename: $parsed_xml_file_obj}. + """ + parser = self._PDF_PARSER_KLASS(pdf_file) + try: + return parser.get_xml_files() + except parser.get_xml_files_swallable_exceptions() as err: + _logger.error("PDF file parsing failed: %s", str(err)) + return {} + + @api.model + def pdf_embed_xml(self, pdf_content, xml_filename, xml_string): + """Add an XML attachment in a pdf""" + with io.BytesIO(pdf_content) as reader_buffer, io.BytesIO() as new_pdf_stream: + reader = OdooPdfFileReader(reader_buffer, strict=False) + writer = OdooPdfFileWriter() + writer.cloneReaderDocumentRoot(reader) + writer.addAttachment(xml_filename, xml_string, subtype="text/xml") + # show attachments when opening PDF + writer._root_object.update( + {NameObject("/PageMode"): NameObject("/UseAttachments")} + ) + writer.write(new_pdf_stream) + return new_pdf_stream.getvalue() diff --git a/pdf_xml_attachment/pyproject.toml b/pdf_xml_attachment/pyproject.toml new file mode 100644 index 0000000000..4231d0cccb --- /dev/null +++ b/pdf_xml_attachment/pyproject.toml @@ -0,0 +1,3 @@ +[build-system] +requires = ["whool"] +build-backend = "whool.buildapi" diff --git a/pdf_xml_attachment/readme/CONTRIBUTORS.md b/pdf_xml_attachment/readme/CONTRIBUTORS.md new file mode 100644 index 0000000000..532896fdcc --- /dev/null +++ b/pdf_xml_attachment/readme/CONTRIBUTORS.md @@ -0,0 +1,3 @@ +- Simone Orsi \<\> +- Alexis de Lattre \<\> +- Jacques-Etienne Baudoux (BCIM) \<\> diff --git a/pdf_xml_attachment/readme/DESCRIPTION.md b/pdf_xml_attachment/readme/DESCRIPTION.md new file mode 100644 index 0000000000..4a43d9a8e1 --- /dev/null +++ b/pdf_xml_attachment/readme/DESCRIPTION.md @@ -0,0 +1 @@ +Technical module to share easily deal with XML attachments in PDF files. diff --git a/pdf_xml_attachment/readme/USAGE.md b/pdf_xml_attachment/readme/USAGE.md new file mode 100644 index 0000000000..fef72cd80d --- /dev/null +++ b/pdf_xml_attachment/readme/USAGE.md @@ -0,0 +1,11 @@ +Inside Odoo env: + + res = env["pdf.xml.tool"].pdf_get_xml_files(pdf_filecontent) + + new_pdf_filecontent = env["pdf.xml.tool"].pdf_embed_xml(pdf_filecontent, filename, xml) + +Outside Odoo env: + + from odoo.addons.pdf_xml_attachment.utils import PDFParser + [...] + res = PDFParser(pdf_filecontent).get_xml_files() diff --git a/pdf_xml_attachment/static/description/icon.png b/pdf_xml_attachment/static/description/icon.png new file mode 100644 index 0000000000..3a0328b516 Binary files /dev/null and b/pdf_xml_attachment/static/description/icon.png differ diff --git a/pdf_xml_attachment/static/description/index.html b/pdf_xml_attachment/static/description/index.html new file mode 100644 index 0000000000..9990ec7d33 --- /dev/null +++ b/pdf_xml_attachment/static/description/index.html @@ -0,0 +1,449 @@ + + + + + +README.rst + + + +
+ + + +Odoo Community Association + +
+

PDF XML attachment

+ +

Beta License: LGPL-3 OCA/reporting-engine Translate me on Weblate Try me on Runboat

+

Technical module to share easily deal with XML attachments in PDF files.

+

Table of contents

+ +
+

Usage

+

Inside Odoo env:

+
+res = env["pdf.xml.tool"].pdf_get_xml_files(pdf_filecontent)
+
+new_pdf_filecontent = env["pdf.xml.tool"].pdf_embed_xml(pdf_filecontent, filename, xml)
+
+

Outside Odoo env:

+
+from odoo.addons.pdf_xml_attachment.utils import PDFParser
+[...]
+res = PDFParser(pdf_filecontent).get_xml_files()
+
+
+
+

Bug Tracker

+

Bugs are tracked on GitHub Issues. +In case of trouble, please check there if your issue has already been reported. +If you spotted it first, help us to smash it by providing a detailed and welcomed +feedback.

+

Do not contact contributors directly about support or help with technical issues.

+
+
+

Credits

+
+

Authors

+
    +
  • Camptocamp
  • +
+
+
+

Contributors

+ +
+
+

Maintainers

+

This module is maintained by the OCA.

+ +Odoo Community Association + +

OCA, or the Odoo Community Association, is a nonprofit organization whose +mission is to support the collaborative development of Odoo features and +promote its widespread use.

+

Current maintainers:

+

simahawk alexis-via

+

This module is part of the OCA/reporting-engine project on GitHub.

+

You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute.

+
+
+
+
+ + diff --git a/pdf_xml_attachment/tests/__init__.py b/pdf_xml_attachment/tests/__init__.py new file mode 100644 index 0000000000..018dd86602 --- /dev/null +++ b/pdf_xml_attachment/tests/__init__.py @@ -0,0 +1 @@ +from . import test_helper diff --git a/pdf_xml_attachment/tests/fixtures/pdf_with_xml_test.pdf b/pdf_xml_attachment/tests/fixtures/pdf_with_xml_test.pdf new file mode 100644 index 0000000000..72dcaf7c7c Binary files /dev/null and b/pdf_xml_attachment/tests/fixtures/pdf_with_xml_test.pdf differ diff --git a/pdf_xml_attachment/tests/test_helper.py b/pdf_xml_attachment/tests/test_helper.py new file mode 100644 index 0000000000..e03ccf9bb8 --- /dev/null +++ b/pdf_xml_attachment/tests/test_helper.py @@ -0,0 +1,57 @@ +# Copyright 2022 Camptocamp SA +# @author: Simone Orsi +# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). +import os + +from lxml import etree + +from odoo.tests.case import TestCase +from odoo.tests.common import TransactionCase + +from odoo.addons.pdf_xml_attachment.utils import PDFParser + + +def read_test_file(filename, mode="r"): + path = os.path.join(os.path.dirname(__file__), "fixtures", filename) + with open(path, mode) as thefile: + return thefile.read() + + +class TestPDFHelperUtils(TestCase): + def test_parse_xml(self): + pdf_content = read_test_file("pdf_with_xml_test.pdf", mode="rb") + res = PDFParser(pdf_content).get_xml_files() + fname, xml_root = tuple(res.items())[0] + self.assertEqual(fname, "factur-x.xml") + self.assertTrue(isinstance(xml_root, etree._Element)) + + +class TestPDFHelper(TransactionCase): + def test_get_xml(self): + pdf_content = read_test_file("pdf_with_xml_test.pdf", mode="rb") + res = self.env["pdf.xml.tool"].pdf_get_xml_files(pdf_content) + fname, xml_root = tuple(res.items())[0] + self.assertEqual(fname, "factur-x.xml") + self.assertTrue(isinstance(xml_root, etree._Element)) + + def test_get_xml_fail(self): + with self.assertLogs( + "odoo.addons.pdf_xml_attachment.models.helper", level="ERROR" + ) as log_catcher: + self.env["pdf.xml.tool"].pdf_get_xml_files(b"") + self.assertIn( + "PDF file parsing failed: Cannot read an empty file", + log_catcher.output[0], + ) + + def test_embed_xml(self): + pdf_content = read_test_file("pdf_with_xml_test.pdf", mode="rb") + filename = "test" + xml = b"test" + newpdf_content = self.env["pdf.xml.tool"].pdf_embed_xml( + pdf_content, filename, xml + ) + attachments = self.env["pdf.xml.tool"].pdf_get_xml_files(newpdf_content) + self.assertTrue(filename in attachments) + etree_content = attachments[filename] + self.assertEqual(xml, etree.tostring(etree_content)) diff --git a/pdf_xml_attachment/utils.py b/pdf_xml_attachment/utils.py new file mode 100644 index 0000000000..5a065f4d38 --- /dev/null +++ b/pdf_xml_attachment/utils.py @@ -0,0 +1,43 @@ +# Copyright 2015-2021 Akretion France +# @author: Alexis de Lattre +# Copyright 2022 Camptocamp SA +# @author: Simone Orsi +# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). + +import logging +from io import BytesIO +from struct import error as StructError + +from lxml import etree + +from odoo.tools.pdf import OdooPdfFileReader, PdfReadError + +_logger = logging.getLogger(__name__) + + +class PDFParser: + def __init__(self, pdf_file): + self.pdf_file = pdf_file + + def get_xml_files(self): + """Parse PDF files to extract XML content. + + :param pdf_file: binary PDF file content + :returns: a dict like {$filename: $parsed_xml_file_obj}. + """ + res = {} + with BytesIO(self.pdf_file) as buffer: + pdf_reader = OdooPdfFileReader(buffer, strict=False) + + # Process embedded files. + for xml_name, content in pdf_reader.getAttachments(): + try: + res[xml_name] = etree.fromstring(content) + except Exception: + _logger.debug("Non XML file found in PDF") + if res: + _logger.debug("Valid XML files found in PDF: %s", list(res.keys())) + return res + + def get_xml_files_swallable_exceptions(self): + return (NotImplementedError, StructError, PdfReadError) diff --git a/setup/_metapackage/pyproject.toml b/setup/_metapackage/pyproject.toml index 8a22d98dac..563c75af75 100644 --- a/setup/_metapackage/pyproject.toml +++ b/setup/_metapackage/pyproject.toml @@ -1,9 +1,10 @@ [project] name = "odoo-addons-oca-reporting-engine" -version = "19.0.20260425.0" +version = "19.0.20260429.0" dependencies = [ "odoo-addon-base_comment_template==19.0.*", "odoo-addon-bi_sql_editor==19.0.*", + "odoo-addon-pdf_xml_attachment==19.0.*", "odoo-addon-report_context==19.0.*", "odoo-addon-report_csv==19.0.*", "odoo-addon-report_partner_address==19.0.*",