From 09009cf7b474b2bed5e740ad2f8d45ff15a9879b Mon Sep 17 00:00:00 2001 From: Simone Orsi Date: Tue, 10 Jun 2025 08:09:44 +0200 Subject: [PATCH 1/5] base_ubl: split in base_ubl_parse + base_ubl_generate --- base_ubl_generate/README.rst | 1 + base_ubl_generate/__init__.py | 3 + base_ubl_generate/__manifest__.py | 14 + base_ubl_generate/i18n/base_ubl.pot | 49 ++ base_ubl_generate/i18n/es.po | 55 ++ base_ubl_generate/i18n/fr.po | 57 ++ base_ubl_generate/i18n/hr.po | 53 ++ base_ubl_generate/i18n/it.po | 54 ++ base_ubl_generate/i18n/nl.po | 56 ++ base_ubl_generate/models/__init__.py | 3 + base_ubl_generate/models/ubl.py | 597 +++++++++++++++++++ base_ubl_generate/pyproject.toml | 3 + base_ubl_generate/readme/CONTRIBUTORS.md | 5 + base_ubl_generate/readme/DESCRIPTION.md | 6 + base_ubl_generate/tests/__init__.py | 3 + base_ubl_generate/tests/test_ubl_generate.py | 110 ++++ 16 files changed, 1069 insertions(+) create mode 100644 base_ubl_generate/README.rst create mode 100644 base_ubl_generate/__init__.py create mode 100644 base_ubl_generate/__manifest__.py create mode 100644 base_ubl_generate/i18n/base_ubl.pot create mode 100644 base_ubl_generate/i18n/es.po create mode 100644 base_ubl_generate/i18n/fr.po create mode 100644 base_ubl_generate/i18n/hr.po create mode 100644 base_ubl_generate/i18n/it.po create mode 100644 base_ubl_generate/i18n/nl.po create mode 100644 base_ubl_generate/models/__init__.py create mode 100644 base_ubl_generate/models/ubl.py create mode 100644 base_ubl_generate/pyproject.toml create mode 100644 base_ubl_generate/readme/CONTRIBUTORS.md create mode 100644 base_ubl_generate/readme/DESCRIPTION.md create mode 100644 base_ubl_generate/tests/__init__.py create mode 100644 base_ubl_generate/tests/test_ubl_generate.py diff --git a/base_ubl_generate/README.rst b/base_ubl_generate/README.rst new file mode 100644 index 0000000000..ddfd4d0241 --- /dev/null +++ b/base_ubl_generate/README.rst @@ -0,0 +1 @@ +bot pls :) diff --git a/base_ubl_generate/__init__.py b/base_ubl_generate/__init__.py new file mode 100644 index 0000000000..31660d6a96 --- /dev/null +++ b/base_ubl_generate/__init__.py @@ -0,0 +1,3 @@ +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from . import models diff --git a/base_ubl_generate/__manifest__.py b/base_ubl_generate/__manifest__.py new file mode 100644 index 0000000000..8ecfa5ae35 --- /dev/null +++ b/base_ubl_generate/__manifest__.py @@ -0,0 +1,14 @@ +# © 2016-2017 Akretion (Alexis de Lattre ) +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +{ + "name": "Base UBL generate", + "version": "17.0.1.0.0", + "category": "Hidden", + "license": "AGPL-3", + "summary": "Base module to generate UBL files (Universal Business Language)", + "author": "Akretion,Onestein,Camptocamp,Odoo Community Association (OCA)", + "website": "https://github.com/OCA/edi", + "depends": ["base_ubl"], + "installable": True, +} diff --git a/base_ubl_generate/i18n/base_ubl.pot b/base_ubl_generate/i18n/base_ubl.pot new file mode 100644 index 0000000000..955dc7e345 --- /dev/null +++ b/base_ubl_generate/i18n/base_ubl.pot @@ -0,0 +1,49 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * base_ubl_generate +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 17.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: base_ubl_generate +#: model:ir.model,name:base_ubl_generate.model_base_ubl_generate +msgid "Common methods to generate and parse UBL XML files" +msgstr "" + +#. module: base_ubl_generate +#. odoo-python +#: code:addons/base_ubl_generate/models/ubl.py:0 +#, python-format +msgid "Missing UNECE Tax Category on tax '%(tax_name)s'" +msgstr "" + +#. module: base_ubl_generate +#. odoo-python +#: code:addons/base_ubl_generate/models/ubl.py:0 +#, python-format +msgid "Missing UNECE Tax Type on tax '%(tax_name)s'" +msgstr "" + +#. module: base_ubl_generate +#. odoo-python +#: code:addons/base_ubl_generate/models/ubl.py:0 +#, python-format +msgid "Missing UNECE code on unit of measure '%s'" +msgstr "" + +#. module: base_ubl_generate +#. odoo-python +#: code:addons/base_ubl_generate/models/ubl.py:0 +#, python-format +msgid "" +"The UBL XML file does not contain the version for validating the content " +"according to the schema." +msgstr "" diff --git a/base_ubl_generate/i18n/es.po b/base_ubl_generate/i18n/es.po new file mode 100644 index 0000000000..a2a6939ed2 --- /dev/null +++ b/base_ubl_generate/i18n/es.po @@ -0,0 +1,55 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * base_ubl_generate +# +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: base_ubl_generate +#: model:ir.model,name:base_ubl_generate.model_base_ubl_generate +msgid "Common methods to generate and parse UBL XML files" +msgstr "Métodos habituales para generar y analizar archivos XML UBL" + +#. module: base_ubl_generate +#. odoo-python +#: code:addons/base_ubl_generate/models/ubl.py:0 +#, python-format +msgid "Missing UNECE Tax Category on tax '%(tax_name)s'" +msgstr "" +"Falta la categoría fiscal de la UNECE (CEPE) en el impuesto '%(tax_name)s'" + +#. module: base_ubl_generate +#. odoo-python +#: code:addons/base_ubl_generate/models/ubl.py:0 +#, python-format +msgid "Missing UNECE Tax Type on tax '%(tax_name)s'" +msgstr "Falta el tipo de impuesto UNECE en el impuesto '%(tax_name)s'" + +#. module: base_ubl_generate +#. odoo-python +#: code:addons/base_ubl_generate/models/ubl.py:0 +#, python-format +msgid "Missing UNECE code on unit of measure '%s'" +msgstr "Falta el código UNECE en la unidad de medida '%s'" + +#. module: base_ubl_generate +#. odoo-python +#: code:addons/base_ubl_generate/models/ubl.py:0 +#, python-format +msgid "" +"The UBL XML file does not contain the version for validating the content " +"according to the schema." +msgstr "" +"El archivo UBL XML no contiene la versión para validar el contenido según el " +"esquema." diff --git a/base_ubl_generate/i18n/fr.po b/base_ubl_generate/i18n/fr.po new file mode 100644 index 0000000000..f9106ac010 --- /dev/null +++ b/base_ubl_generate/i18n/fr.po @@ -0,0 +1,57 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * base_ubl_generate +# +# Translators: +# OCA Transbot , 2016 +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 8.0\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2016-11-12 13:37+0000\n" +"PO-Revision-Date: 2024-04-22 12:35+0000\n" +"Last-Translator: c2cdidier \n" +"Language-Team: French (https://www.transifex.com/oca/teams/23907/fr/)\n" +"Language: fr\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: base_ubl_generate +#: model:ir.model,name:base_ubl_generate.model_base_ubl_generate +msgid "Common methods to generate and parse UBL XML files" +msgstr "Méthodes courantes pour générer et analyser les fichiers UBL XML" + +#. module: base_ubl_generate +#. odoo-python +#: code:addons/base_ubl_generate/models/ubl.py:0 +#, python-format +msgid "Missing UNECE Tax Category on tax '%(tax_name)s'" +msgstr "Catégorie de taxe UNECE manquante '%(tax_name)s'" + +#. module: base_ubl_generate +#. odoo-python +#: code:addons/base_ubl_generate/models/ubl.py:0 +#, python-format +msgid "Missing UNECE Tax Type on tax '%(tax_name)s'" +msgstr "Type de taxe UNECE manquante sur la taxe '%(tax_name)s'" + +#. module: base_ubl_generate +#. odoo-python +#: code:addons/base_ubl_generate/models/ubl.py:0 +#, python-format +msgid "Missing UNECE code on unit of measure '%s'" +msgstr "Code UNECE manquant pour l'unité de mesure '%s'" + +#. module: base_ubl_generate +#. odoo-python +#: code:addons/base_ubl_generate/models/ubl.py:0 +#, python-format +msgid "" +"The UBL XML file does not contain the version for validating the content " +"according to the schema." +msgstr "" +"Le fichier XML UBL ne contient pas la version permettant de valider le " +"contenu selon le schéma." diff --git a/base_ubl_generate/i18n/hr.po b/base_ubl_generate/i18n/hr.po new file mode 100644 index 0000000000..d4d8ec9f7e --- /dev/null +++ b/base_ubl_generate/i18n/hr.po @@ -0,0 +1,53 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * base_ubl_generate +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 12.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: base_ubl_generate +#: model:ir.model,name:base_ubl_generate.model_base_ubl_generate +msgid "Common methods to generate and parse UBL XML files" +msgstr "Uobičajene metode za generiranje i parsiranje UBL XML datoteka" + +#. module: base_ubl_generate +#. odoo-python +#: code:addons/base_ubl_generate/models/ubl.py:0 +#, python-format +msgid "Missing UNECE Tax Category on tax '%(tax_name)s'" +msgstr "Nedostaje UNECE kategorija poreza na '%(tax_name)s'" + +#. module: base_ubl_generate +#. odoo-python +#: code:addons/base_ubl_generate/models/ubl.py:0 +#, python-format +msgid "Missing UNECE Tax Type on tax '%(tax_name)s'" +msgstr "Nedostaje UNECE tip poreza na '%(tax_name)s'" + +#. module: base_ubl_generate +#. odoo-python +#: code:addons/base_ubl_generate/models/ubl.py:0 +#, python-format +msgid "Missing UNECE code on unit of measure '%s'" +msgstr "Nedostaje UNECE šifra jedinice mjere za '%s'" + +#. module: base_ubl_generate +#. odoo-python +#: code:addons/base_ubl_generate/models/ubl.py:0 +#, python-format +msgid "" +"The UBL XML file does not contain the version for validating the content " +"according to the schema." +msgstr "" diff --git a/base_ubl_generate/i18n/it.po b/base_ubl_generate/i18n/it.po new file mode 100644 index 0000000000..f5bf81a659 --- /dev/null +++ b/base_ubl_generate/i18n/it.po @@ -0,0 +1,54 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * base_ubl_generate +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 16.0\n" +"Report-Msgid-Bugs-To: \n" +"PO-Revision-Date: 2024-05-13 10:34+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: base_ubl_generate +#: model:ir.model,name:base_ubl_generate.model_base_ubl_generate +msgid "Common methods to generate and parse UBL XML files" +msgstr "Metodi comuni per generare ed elaborare file UBL e XML" + +#. module: base_ubl_generate +#. odoo-python +#: code:addons/base_ubl_generate/models/ubl.py:0 +#, python-format +msgid "Missing UNECE Tax Category on tax '%(tax_name)s'" +msgstr "Categoria imposta UNECE non presente nell'imposta %(tax_name)s'" + +#. module: base_ubl_generate +#. odoo-python +#: code:addons/base_ubl_generate/models/ubl.py:0 +#, python-format +msgid "Missing UNECE Tax Type on tax '%(tax_name)s'" +msgstr "Categoria imposta UNECE non presente nell'imposta '%(tax_name)s'" + +#. module: base_ubl_generate +#. odoo-python +#: code:addons/base_ubl_generate/models/ubl.py:0 +#, python-format +msgid "Missing UNECE code on unit of measure '%s'" +msgstr "Codice UNECE mancante nell'unità di misura '%s'" + +#. module: base_ubl_generate +#. odoo-python +#: code:addons/base_ubl_generate/models/ubl.py:0 +#, python-format +msgid "" +"The UBL XML file does not contain the version for validating the content " +"according to the schema." +msgstr "" +"Il file UBL XML non contiene la versione per validare il contenuto in " +"accordo allo schema." diff --git a/base_ubl_generate/i18n/nl.po b/base_ubl_generate/i18n/nl.po new file mode 100644 index 0000000000..3a20e4d67a --- /dev/null +++ b/base_ubl_generate/i18n/nl.po @@ -0,0 +1,56 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * base_ubl_generate +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 13.0\n" +"Report-Msgid-Bugs-To: \n" +"PO-Revision-Date: 2020-12-13 02:36+0000\n" +"Last-Translator: Bosd \n" +"Language-Team: none\n" +"Language: nl\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 3.10\n" + +#. module: base_ubl_generate +#: model:ir.model,name:base_ubl_generate.model_base_ubl_generate +msgid "Common methods to generate and parse UBL XML files" +msgstr "" +"Gemeenschappelijke methode voor het genereren en verwerken van UBL XML " +"bestanden" + +#. module: base_ubl_generate +#. odoo-python +#: code:addons/base_ubl_generate/models/ubl.py:0 +#, python-format +msgid "Missing UNECE Tax Category on tax '%(tax_name)s'" +msgstr "" + +#. module: base_ubl_generate +#. odoo-python +#: code:addons/base_ubl_generate/models/ubl.py:0 +#, python-format +msgid "Missing UNECE Tax Type on tax '%(tax_name)s'" +msgstr "" + +#. module: base_ubl_generate +#. odoo-python +#: code:addons/base_ubl_generate/models/ubl.py:0 +#, python-format +msgid "Missing UNECE code on unit of measure '%s'" +msgstr "Ontbrekende UNECE code op maateenheid '%s'" + +#. module: base_ubl_generate +#. odoo-python +#: code:addons/base_ubl_generate/models/ubl.py:0 +#, python-format +msgid "" +"The UBL XML file does not contain the version for validating the content " +"according to the schema." +msgstr "" +"Het UBL XML bestand bevat geen versie informatie volgens het overeenkomstig " +"schema." diff --git a/base_ubl_generate/models/__init__.py b/base_ubl_generate/models/__init__.py new file mode 100644 index 0000000000..7ec1c77b3e --- /dev/null +++ b/base_ubl_generate/models/__init__.py @@ -0,0 +1,3 @@ +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from . import ubl diff --git a/base_ubl_generate/models/ubl.py b/base_ubl_generate/models/ubl.py new file mode 100644 index 0000000000..2c05b9891e --- /dev/null +++ b/base_ubl_generate/models/ubl.py @@ -0,0 +1,597 @@ +# © 2016-2017 Akretion (Alexis de Lattre ) +# Copyright 2019 Onestein () +# Copyright 2020 Jacques-Etienne Baudoux (BCIM) +# Copyright 2025 Simome Orsi it will return the new PDF binary with the embedded XML + (used for qweb-pdf reports) + """ + logger.warning( + "`_embed_ubl_xml_in_pdf_content` deprecated: use `pdf.helper.pdf_embed_xml`" + ) + self.ensure_one() + logger.debug("Starting to embed %s in PDF", xml_filename) + pdf_content = self.env["pdf.helper"].pdf_embed_xml( + pdf_content, xml_filename, xml_string + ) + logger.info("%s file added to PDF content", xml_filename) + return pdf_content + + # TODO: move to pdf_helper + @api.model + def embed_xml_in_pdf( + self, xml_string, xml_filename, pdf_content=None, pdf_file=None + ): + """ + 2 possible uses: + a) use the pdf_content argument, which has the binary of the PDF + -> it will return the new PDF binary with the embedded XML + (used for qweb-pdf reports) + b) OR use the pdf_file argument, which has the full path to the + original PDF file + -> it will re-write this file with the new PDF + (used for py3o reports, *_ubl_py3o modules in this repo) + """ + assert pdf_content or pdf_file, "Missing pdf_file or pdf_content" + if pdf_file: + with open(pdf_file, "rb") as f: + pdf_content = f.read() + updated_pdf_content = self.env["pdf.helper"].pdf_embed_xml( + pdf_content, xml_filename, xml_string + ) + if pdf_file: + with open(pdf_file, "wb") as f: + f.write(updated_pdf_content) + return updated_pdf_content diff --git a/base_ubl_generate/pyproject.toml b/base_ubl_generate/pyproject.toml new file mode 100644 index 0000000000..4231d0cccb --- /dev/null +++ b/base_ubl_generate/pyproject.toml @@ -0,0 +1,3 @@ +[build-system] +requires = ["whool"] +build-backend = "whool.buildapi" diff --git a/base_ubl_generate/readme/CONTRIBUTORS.md b/base_ubl_generate/readme/CONTRIBUTORS.md new file mode 100644 index 0000000000..c43aab59cf --- /dev/null +++ b/base_ubl_generate/readme/CONTRIBUTORS.md @@ -0,0 +1,5 @@ +- Alexis de Lattre \<\> +- Andrea Stirpe \<\> +- Jacques-Etienne Baudoux \<\> +- Phuc (Tran Thanh) \<\> +- Simone Orsi \<\> diff --git a/base_ubl_generate/readme/DESCRIPTION.md b/base_ubl_generate/readme/DESCRIPTION.md new file mode 100644 index 0000000000..0ea1a17087 --- /dev/null +++ b/base_ubl_generate/readme/DESCRIPTION.md @@ -0,0 +1,6 @@ +This module contains methods to generate UBL files. This +module doesn't do anything useful by itself, +but it can be used by other modules to process UBL data. +Examples: + +- *purchase_order_ubl* that generate UBL purchase orders, diff --git a/base_ubl_generate/tests/__init__.py b/base_ubl_generate/tests/__init__.py new file mode 100644 index 0000000000..895163754b --- /dev/null +++ b/base_ubl_generate/tests/__init__.py @@ -0,0 +1,3 @@ +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from . import test_ubl_generate diff --git a/base_ubl_generate/tests/test_ubl_generate.py b/base_ubl_generate/tests/test_ubl_generate.py new file mode 100644 index 0000000000..4917cce19f --- /dev/null +++ b/base_ubl_generate/tests/test_ubl_generate.py @@ -0,0 +1,110 @@ +# Copyright 2019 Onestein () +# © 2017-2020 Akretion (Alexis de Lattre ) +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from lxml import etree + +from odoo.tests.common import HttpCase + + +class TestUblInvoice(HttpCase): + def test_pdf_generate(self): + invoice = self.create_test_invoice() + content, doc_type = ( + self.env.ref("account.account_invoices") + .with_context(no_embedded_ubl_xml=True, force_report_rendering=True) + ._render_qweb_pdf("account.account_invoices", invoice.ids) + ) + self.assertTrue(content) + self.assertEqual(doc_type, "pdf") + + def test_ubl_generate(self): + invoice = self.create_test_invoice() + nsmap, ns = self.env["base.ubl"]._ubl_get_nsmap_namespace("Invoice-2") + xml_root = etree.Element("Invoice", nsmap=nsmap) + + self.env["base.ubl"]._ubl_add_supplier_party( + False, invoice.company_id, "AccountingSupplierParty", xml_root, ns + ) + self.env["base.ubl"]._ubl_add_customer_party( + invoice.partner_id, False, "AccountingCustomerParty", xml_root, ns + ) + + def create_test_invoice( + self, product=False, qty=1, price=12.42, discount=0, validate=True + ): + aio = self.env["account.move"] + aao = self.env["account.account"] + ato = self.env["account.tax"] + company = self.env.ref("base.main_company") + account_revenue = aao.search( + [("code", "=", "707100"), ("company_id", "=", company.id)], limit=1 + ) + if not account_revenue: + account_revenue = aao.create( + { + "code": "707100", + "name": "Product Sales - (test)", + "company_id": company.id, + "account_type": "income", + } + ) + taxes = ato.search( + [ + ("company_id", "=", company.id), + ("type_tax_use", "=", "sale"), + ("unece_type_id", "!=", False), + ("unece_categ_id", "!=", False), + ("amount_type", "=", "percent"), + ] + ) + if taxes: + tax = taxes[0] + else: + unece_type_id = self.env.ref("account_tax_unece.tax_type_vat").id + unece_categ_id = self.env.ref("account_tax_unece.tax_categ_s").id + tax = ato.create( + { + "name": "German VAT purchase 18.0%", + "description": "DE-VAT-sale-18.0", + "company_id": company.id, + "type_tax_use": "sale", + "price_include": False, + "amount": 18, + "amount_type": "percent", + "unece_type_id": unece_type_id, + "unece_categ_id": unece_categ_id, + } + ) + # validate invoice + if not product: + product = self.env.ref("product.product_product_4") + self.env.ref("base.EUR").active = True + invoice = aio.create( + { + "partner_id": self.env.ref("base.res_partner_2").id, + "currency_id": self.env.ref("base.EUR").id, + "move_type": "out_invoice", + "company_id": company.id, + "name": "SO1242", + "invoice_line_ids": [ + ( + 0, + 0, + { + "product_id": product.id, + "product_uom_id": product.uom_id.id, + "quantity": qty, + "price_unit": price, + "discount": discount, + "name": product.name, + "account_id": account_revenue.id, + "tax_ids": [(6, 0, [tax.id])], + }, + ) + ], + } + ) + if validate: + invoice.action_post() + return invoice From ed3c5e18d9e8810c355835e9f3ad868ba704fc91 Mon Sep 17 00:00:00 2001 From: Simone Orsi Date: Tue, 10 Jun 2025 13:00:17 +0200 Subject: [PATCH 2/5] base_ubl_generate: migrate to v18 --- base_ubl_generate/__manifest__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/base_ubl_generate/__manifest__.py b/base_ubl_generate/__manifest__.py index 8ecfa5ae35..8e8d97421d 100644 --- a/base_ubl_generate/__manifest__.py +++ b/base_ubl_generate/__manifest__.py @@ -3,7 +3,7 @@ { "name": "Base UBL generate", - "version": "17.0.1.0.0", + "version": "18.0.1.0.0", "category": "Hidden", "license": "AGPL-3", "summary": "Base module to generate UBL files (Universal Business Language)", From 799299c4c467ed68d357211f6d072385176cce37 Mon Sep 17 00:00:00 2001 From: Simone Orsi Date: Tue, 10 Jun 2025 13:04:11 +0200 Subject: [PATCH 3/5] base_ubl_generate: get rid of deprecated pdf methods --- base_ubl_generate/models/ubl.py | 58 --------------------------------- 1 file changed, 58 deletions(-) diff --git a/base_ubl_generate/models/ubl.py b/base_ubl_generate/models/ubl.py index 2c05b9891e..6dac37cebe 100644 --- a/base_ubl_generate/models/ubl.py +++ b/base_ubl_generate/models/ubl.py @@ -5,7 +5,6 @@ # License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). import logging -from io import BytesIO from lxml import etree @@ -538,60 +537,3 @@ def _ubl_add_tax_scheme(self, tax_scheme_dict, parent_node, ns, version="2.1"): tax_scheme, ns["cbc"] + "TaxTypeCode" ) tax_scheme_type_code.text = tax_scheme_dict["type_code"] - - @api.model - def _ubl_add_xml_in_pdf_buffer(self, xml_string, xml_filename, buffer): - logger.warning( - "`_ubl_add_xml_in_pdf_buffer` deprecated: use `pdf.helper.pdf_embed_xml`" - ) - pdf_content = buffer.getvalue() - new_content = self.env["pdf.helper"].pdf_embed_xml( - pdf_content, xml_filename, xml_string - ) - new_buffer = BytesIO(new_content) - return new_buffer - - @api.model - def _embed_ubl_xml_in_pdf_content(self, xml_string, xml_filename, pdf_content): - """Add the attachments to the PDF content. - Use the pdf_content argument, which has the binary of the PDF - -> it will return the new PDF binary with the embedded XML - (used for qweb-pdf reports) - """ - logger.warning( - "`_embed_ubl_xml_in_pdf_content` deprecated: use `pdf.helper.pdf_embed_xml`" - ) - self.ensure_one() - logger.debug("Starting to embed %s in PDF", xml_filename) - pdf_content = self.env["pdf.helper"].pdf_embed_xml( - pdf_content, xml_filename, xml_string - ) - logger.info("%s file added to PDF content", xml_filename) - return pdf_content - - # TODO: move to pdf_helper - @api.model - def embed_xml_in_pdf( - self, xml_string, xml_filename, pdf_content=None, pdf_file=None - ): - """ - 2 possible uses: - a) use the pdf_content argument, which has the binary of the PDF - -> it will return the new PDF binary with the embedded XML - (used for qweb-pdf reports) - b) OR use the pdf_file argument, which has the full path to the - original PDF file - -> it will re-write this file with the new PDF - (used for py3o reports, *_ubl_py3o modules in this repo) - """ - assert pdf_content or pdf_file, "Missing pdf_file or pdf_content" - if pdf_file: - with open(pdf_file, "rb") as f: - pdf_content = f.read() - updated_pdf_content = self.env["pdf.helper"].pdf_embed_xml( - pdf_content, xml_filename, xml_string - ) - if pdf_file: - with open(pdf_file, "wb") as f: - f.write(updated_pdf_content) - return updated_pdf_content From e785f2dc595e1227e3c52401e3ac39fae5d085de Mon Sep 17 00:00:00 2001 From: Simone Orsi Date: Tue, 10 Jun 2025 13:04:26 +0200 Subject: [PATCH 4/5] base_ubl_generate: add test coverage --- base_ubl_generate/README.rst | 91 +++- base_ubl_generate/i18n/base_ubl_generate.pot | 37 ++ base_ubl_generate/i18n/es.po | 20 +- base_ubl_generate/i18n/fr.po | 20 +- base_ubl_generate/i18n/hr.po | 18 +- base_ubl_generate/i18n/it.po | 20 +- base_ubl_generate/i18n/nl.po | 20 +- base_ubl_generate/models/ubl.py | 8 +- base_ubl_generate/static/description/icon.png | Bin 0 -> 10254 bytes .../static/description/index.html | 440 ++++++++++++++++++ base_ubl_generate/tests/__init__.py | 3 +- base_ubl_generate/tests/common.py | 34 ++ base_ubl_generate/tests/test_ubl_generate.py | 110 ----- .../tests/test_ubl_generate_base.py | 284 +++++++++++ .../tests/test_ubl_generate_invoice.py | 89 ++++ 15 files changed, 1013 insertions(+), 181 deletions(-) create mode 100644 base_ubl_generate/i18n/base_ubl_generate.pot create mode 100644 base_ubl_generate/static/description/icon.png create mode 100644 base_ubl_generate/static/description/index.html create mode 100644 base_ubl_generate/tests/common.py delete mode 100644 base_ubl_generate/tests/test_ubl_generate.py create mode 100644 base_ubl_generate/tests/test_ubl_generate_base.py create mode 100644 base_ubl_generate/tests/test_ubl_generate_invoice.py diff --git a/base_ubl_generate/README.rst b/base_ubl_generate/README.rst index ddfd4d0241..7427a90b31 100644 --- a/base_ubl_generate/README.rst +++ b/base_ubl_generate/README.rst @@ -1 +1,90 @@ -bot pls :) +.. image:: https://odoo-community.org/readme-banner-image + :target: https://odoo-community.org/get-involved?utm_source=readme + :alt: Odoo Community Association + +================= +Base UBL generate +================= + +.. + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! This file is generated by oca-gen-addon-readme !! + !! changes will be overwritten. !! + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! source digest: sha256:cb66ce9e5b206f852540875959f009daec8d80252497731bc05519ebb274ef8e + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + +.. |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-AGPL--3-blue.png + :target: http://www.gnu.org/licenses/agpl-3.0-standalone.html + :alt: License: AGPL-3 +.. |badge3| image:: https://img.shields.io/badge/github-OCA%2Fedi-lightgray.png?logo=github + :target: https://github.com/OCA/edi/tree/18.0/base_ubl_generate + :alt: OCA/edi +.. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png + :target: https://translation.odoo-community.org/projects/edi-18-0/edi-18-0-base_ubl_generate + :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/edi&target_branch=18.0 + :alt: Try me on Runboat + +|badge1| |badge2| |badge3| |badge4| |badge5| + +This module contains methods to generate UBL files. This module doesn't +do anything useful by itself, but it can be used by other modules to +process UBL data. Examples: + +- *purchase_order_ubl* that generate UBL purchase orders, + +**Table of contents** + +.. contents:: + :local: + +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 +------- + +* Akretion +* Onestein +* Camptocamp + +Contributors +------------ + +- Alexis de Lattre +- Andrea Stirpe +- Jacques-Etienne Baudoux +- Phuc (Tran Thanh) +- Simone Orsi + +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. + +This module is part of the `OCA/edi `_ project on GitHub. + +You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute. diff --git a/base_ubl_generate/i18n/base_ubl_generate.pot b/base_ubl_generate/i18n/base_ubl_generate.pot new file mode 100644 index 0000000000..b5d6fc6b3d --- /dev/null +++ b/base_ubl_generate/i18n/base_ubl_generate.pot @@ -0,0 +1,37 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * base_ubl_generate +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 18.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: base_ubl_generate +#: model:ir.model,name:base_ubl_generate.model_base_ubl +msgid "Common methods to generate and parse UBL XML files" +msgstr "" + +#. module: base_ubl_generate +#. odoo-python +#: code:addons/base_ubl_generate/models/ubl.py:0 +msgid "Missing UNECE Tax Category on tax '%(tax_name)s'" +msgstr "" + +#. module: base_ubl_generate +#. odoo-python +#: code:addons/base_ubl_generate/models/ubl.py:0 +msgid "Missing UNECE Tax Type on tax '%(tax_name)s'" +msgstr "" + +#. module: base_ubl_generate +#. odoo-python +#: code:addons/base_ubl_generate/models/ubl.py:0 +msgid "Missing UNECE code on unit of measure '%s'" +msgstr "" diff --git a/base_ubl_generate/i18n/es.po b/base_ubl_generate/i18n/es.po index a2a6939ed2..0dd2ccd064 100644 --- a/base_ubl_generate/i18n/es.po +++ b/base_ubl_generate/i18n/es.po @@ -17,14 +17,13 @@ msgstr "" "X-Generator: Weblate 4.17\n" #. module: base_ubl_generate -#: model:ir.model,name:base_ubl_generate.model_base_ubl_generate +#: model:ir.model,name:base_ubl_generate.model_base_ubl msgid "Common methods to generate and parse UBL XML files" msgstr "Métodos habituales para generar y analizar archivos XML UBL" #. module: base_ubl_generate #. odoo-python #: code:addons/base_ubl_generate/models/ubl.py:0 -#, python-format msgid "Missing UNECE Tax Category on tax '%(tax_name)s'" msgstr "" "Falta la categoría fiscal de la UNECE (CEPE) en el impuesto '%(tax_name)s'" @@ -32,24 +31,19 @@ msgstr "" #. module: base_ubl_generate #. odoo-python #: code:addons/base_ubl_generate/models/ubl.py:0 -#, python-format msgid "Missing UNECE Tax Type on tax '%(tax_name)s'" msgstr "Falta el tipo de impuesto UNECE en el impuesto '%(tax_name)s'" #. module: base_ubl_generate #. odoo-python #: code:addons/base_ubl_generate/models/ubl.py:0 -#, python-format msgid "Missing UNECE code on unit of measure '%s'" msgstr "Falta el código UNECE en la unidad de medida '%s'" -#. module: base_ubl_generate -#. odoo-python -#: code:addons/base_ubl_generate/models/ubl.py:0 #, python-format -msgid "" -"The UBL XML file does not contain the version for validating the content " -"according to the schema." -msgstr "" -"El archivo UBL XML no contiene la versión para validar el contenido según el " -"esquema." +#~ msgid "" +#~ "The UBL XML file does not contain the version for validating the content " +#~ "according to the schema." +#~ msgstr "" +#~ "El archivo UBL XML no contiene la versión para validar el contenido según " +#~ "el esquema." diff --git a/base_ubl_generate/i18n/fr.po b/base_ubl_generate/i18n/fr.po index f9106ac010..744ffddd49 100644 --- a/base_ubl_generate/i18n/fr.po +++ b/base_ubl_generate/i18n/fr.po @@ -20,38 +20,32 @@ msgstr "" "X-Generator: Weblate 4.17\n" #. module: base_ubl_generate -#: model:ir.model,name:base_ubl_generate.model_base_ubl_generate +#: model:ir.model,name:base_ubl_generate.model_base_ubl msgid "Common methods to generate and parse UBL XML files" msgstr "Méthodes courantes pour générer et analyser les fichiers UBL XML" #. module: base_ubl_generate #. odoo-python #: code:addons/base_ubl_generate/models/ubl.py:0 -#, python-format msgid "Missing UNECE Tax Category on tax '%(tax_name)s'" msgstr "Catégorie de taxe UNECE manquante '%(tax_name)s'" #. module: base_ubl_generate #. odoo-python #: code:addons/base_ubl_generate/models/ubl.py:0 -#, python-format msgid "Missing UNECE Tax Type on tax '%(tax_name)s'" msgstr "Type de taxe UNECE manquante sur la taxe '%(tax_name)s'" #. module: base_ubl_generate #. odoo-python #: code:addons/base_ubl_generate/models/ubl.py:0 -#, python-format msgid "Missing UNECE code on unit of measure '%s'" msgstr "Code UNECE manquant pour l'unité de mesure '%s'" -#. module: base_ubl_generate -#. odoo-python -#: code:addons/base_ubl_generate/models/ubl.py:0 #, python-format -msgid "" -"The UBL XML file does not contain the version for validating the content " -"according to the schema." -msgstr "" -"Le fichier XML UBL ne contient pas la version permettant de valider le " -"contenu selon le schéma." +#~ msgid "" +#~ "The UBL XML file does not contain the version for validating the content " +#~ "according to the schema." +#~ msgstr "" +#~ "Le fichier XML UBL ne contient pas la version permettant de valider le " +#~ "contenu selon le schéma." diff --git a/base_ubl_generate/i18n/hr.po b/base_ubl_generate/i18n/hr.po index d4d8ec9f7e..695b369443 100644 --- a/base_ubl_generate/i18n/hr.po +++ b/base_ubl_generate/i18n/hr.po @@ -13,41 +13,29 @@ msgstr "" "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" +"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: base_ubl_generate -#: model:ir.model,name:base_ubl_generate.model_base_ubl_generate +#: model:ir.model,name:base_ubl_generate.model_base_ubl msgid "Common methods to generate and parse UBL XML files" msgstr "Uobičajene metode za generiranje i parsiranje UBL XML datoteka" #. module: base_ubl_generate #. odoo-python #: code:addons/base_ubl_generate/models/ubl.py:0 -#, python-format msgid "Missing UNECE Tax Category on tax '%(tax_name)s'" msgstr "Nedostaje UNECE kategorija poreza na '%(tax_name)s'" #. module: base_ubl_generate #. odoo-python #: code:addons/base_ubl_generate/models/ubl.py:0 -#, python-format msgid "Missing UNECE Tax Type on tax '%(tax_name)s'" msgstr "Nedostaje UNECE tip poreza na '%(tax_name)s'" #. module: base_ubl_generate #. odoo-python #: code:addons/base_ubl_generate/models/ubl.py:0 -#, python-format msgid "Missing UNECE code on unit of measure '%s'" msgstr "Nedostaje UNECE šifra jedinice mjere za '%s'" - -#. module: base_ubl_generate -#. odoo-python -#: code:addons/base_ubl_generate/models/ubl.py:0 -#, python-format -msgid "" -"The UBL XML file does not contain the version for validating the content " -"according to the schema." -msgstr "" diff --git a/base_ubl_generate/i18n/it.po b/base_ubl_generate/i18n/it.po index f5bf81a659..c30d3fea58 100644 --- a/base_ubl_generate/i18n/it.po +++ b/base_ubl_generate/i18n/it.po @@ -17,38 +17,32 @@ msgstr "" "X-Generator: Weblate 4.17\n" #. module: base_ubl_generate -#: model:ir.model,name:base_ubl_generate.model_base_ubl_generate +#: model:ir.model,name:base_ubl_generate.model_base_ubl msgid "Common methods to generate and parse UBL XML files" msgstr "Metodi comuni per generare ed elaborare file UBL e XML" #. module: base_ubl_generate #. odoo-python #: code:addons/base_ubl_generate/models/ubl.py:0 -#, python-format msgid "Missing UNECE Tax Category on tax '%(tax_name)s'" msgstr "Categoria imposta UNECE non presente nell'imposta %(tax_name)s'" #. module: base_ubl_generate #. odoo-python #: code:addons/base_ubl_generate/models/ubl.py:0 -#, python-format msgid "Missing UNECE Tax Type on tax '%(tax_name)s'" msgstr "Categoria imposta UNECE non presente nell'imposta '%(tax_name)s'" #. module: base_ubl_generate #. odoo-python #: code:addons/base_ubl_generate/models/ubl.py:0 -#, python-format msgid "Missing UNECE code on unit of measure '%s'" msgstr "Codice UNECE mancante nell'unità di misura '%s'" -#. module: base_ubl_generate -#. odoo-python -#: code:addons/base_ubl_generate/models/ubl.py:0 #, python-format -msgid "" -"The UBL XML file does not contain the version for validating the content " -"according to the schema." -msgstr "" -"Il file UBL XML non contiene la versione per validare il contenuto in " -"accordo allo schema." +#~ msgid "" +#~ "The UBL XML file does not contain the version for validating the content " +#~ "according to the schema." +#~ msgstr "" +#~ "Il file UBL XML non contiene la versione per validare il contenuto in " +#~ "accordo allo schema." diff --git a/base_ubl_generate/i18n/nl.po b/base_ubl_generate/i18n/nl.po index 3a20e4d67a..69aa5ec306 100644 --- a/base_ubl_generate/i18n/nl.po +++ b/base_ubl_generate/i18n/nl.po @@ -17,7 +17,7 @@ msgstr "" "X-Generator: Weblate 3.10\n" #. module: base_ubl_generate -#: model:ir.model,name:base_ubl_generate.model_base_ubl_generate +#: model:ir.model,name:base_ubl_generate.model_base_ubl msgid "Common methods to generate and parse UBL XML files" msgstr "" "Gemeenschappelijke methode voor het genereren en verwerken van UBL XML " @@ -26,31 +26,25 @@ msgstr "" #. module: base_ubl_generate #. odoo-python #: code:addons/base_ubl_generate/models/ubl.py:0 -#, python-format msgid "Missing UNECE Tax Category on tax '%(tax_name)s'" msgstr "" #. module: base_ubl_generate #. odoo-python #: code:addons/base_ubl_generate/models/ubl.py:0 -#, python-format msgid "Missing UNECE Tax Type on tax '%(tax_name)s'" msgstr "" #. module: base_ubl_generate #. odoo-python #: code:addons/base_ubl_generate/models/ubl.py:0 -#, python-format msgid "Missing UNECE code on unit of measure '%s'" msgstr "Ontbrekende UNECE code op maateenheid '%s'" -#. module: base_ubl_generate -#. odoo-python -#: code:addons/base_ubl_generate/models/ubl.py:0 #, python-format -msgid "" -"The UBL XML file does not contain the version for validating the content " -"according to the schema." -msgstr "" -"Het UBL XML bestand bevat geen versie informatie volgens het overeenkomstig " -"schema." +#~ msgid "" +#~ "The UBL XML file does not contain the version for validating the content " +#~ "according to the schema." +#~ msgstr "" +#~ "Het UBL XML bestand bevat geen versie informatie volgens het " +#~ "overeenkomstig schema." diff --git a/base_ubl_generate/models/ubl.py b/base_ubl_generate/models/ubl.py index 6dac37cebe..115bc56b3c 100644 --- a/base_ubl_generate/models/ubl.py +++ b/base_ubl_generate/models/ubl.py @@ -393,7 +393,11 @@ def _ubl_add_item( if not seller_code: seller_code = self._ubl_get_seller_code_from_product(product) if not product_name: - variant = ", ".join(product.attribute_line_ids.mapped("value_ids.name")) + variant = "" + if product.attribute_line_ids: + variant = ", ".join( + [x.name for x in product.attribute_line_ids.value_ids] + ) product_name = variant and f"{product.name} ({variant})" or product.name description = etree.SubElement(item, ns["cbc"] + "Description") description.text = name @@ -446,7 +450,7 @@ def _ubl_add_item( node_name="ClassifiedTaxCategory", version=version, ) - for attribute_value in product.attribute_line_ids.mapped("value_ids"): + for attribute_value in product.attribute_line_ids.value_ids: item_property = etree.SubElement( item, ns["cac"] + "AdditionalItemProperty" ) diff --git a/base_ubl_generate/static/description/icon.png b/base_ubl_generate/static/description/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..1dcc49c24f364e9adf0afbc6fc0bac6dbecdeb11 GIT binary patch literal 10254 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 + + + + +README.rst + + + +
+ + + +Odoo Community Association + +
+

Base UBL generate

+ +

Beta License: AGPL-3 OCA/edi Translate me on Weblate Try me on Runboat

+

This module contains methods to generate UBL files. This module doesn’t +do anything useful by itself, but it can be used by other modules to +process UBL data. Examples:

+
    +
  • purchase_order_ubl that generate UBL purchase orders,
  • +
+

Table of contents

+ +
+

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

+
    +
  • Akretion
  • +
  • Onestein
  • +
  • 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.

+

This module is part of the OCA/edi project on GitHub.

+

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

+
+
+
+
+ + diff --git a/base_ubl_generate/tests/__init__.py b/base_ubl_generate/tests/__init__.py index 895163754b..02b5846e35 100644 --- a/base_ubl_generate/tests/__init__.py +++ b/base_ubl_generate/tests/__init__.py @@ -1,3 +1,4 @@ # License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). -from . import test_ubl_generate +from . import test_ubl_generate_base +from . import test_ubl_generate_invoice diff --git a/base_ubl_generate/tests/common.py b/base_ubl_generate/tests/common.py new file mode 100644 index 0000000000..7928ad6e90 --- /dev/null +++ b/base_ubl_generate/tests/common.py @@ -0,0 +1,34 @@ +# Copyright 2025 Camptocamp SA (http://www.camptocamp.com) +# @author Simone Orsi +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). + +import random + + +class DummyRecord: + """Minimal dummy object for partner, company, product, etc. + + This module can be used without a full Odoo environment, + simulating the necessary attributes and methods. + + This way we can test handling line items w/o depending on ``product`` + or other Odoo modules. + """ + + def __init__(self, model=None, **kwargs): + self.__dict__.update(kwargs) + self.id = random.randint(1, 1000) # Simulate an ID for the dummy object + self._ids = [self.id] + + def __iter__(self): + yield from self._iter + + def __getattr__(self, item): + return None + + def __repr__(self): + if hasattr(self, "name"): + name = self.name + else: + name = f"{self.__dict__}" + return f"" diff --git a/base_ubl_generate/tests/test_ubl_generate.py b/base_ubl_generate/tests/test_ubl_generate.py deleted file mode 100644 index 4917cce19f..0000000000 --- a/base_ubl_generate/tests/test_ubl_generate.py +++ /dev/null @@ -1,110 +0,0 @@ -# Copyright 2019 Onestein () -# © 2017-2020 Akretion (Alexis de Lattre ) -# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). - -from lxml import etree - -from odoo.tests.common import HttpCase - - -class TestUblInvoice(HttpCase): - def test_pdf_generate(self): - invoice = self.create_test_invoice() - content, doc_type = ( - self.env.ref("account.account_invoices") - .with_context(no_embedded_ubl_xml=True, force_report_rendering=True) - ._render_qweb_pdf("account.account_invoices", invoice.ids) - ) - self.assertTrue(content) - self.assertEqual(doc_type, "pdf") - - def test_ubl_generate(self): - invoice = self.create_test_invoice() - nsmap, ns = self.env["base.ubl"]._ubl_get_nsmap_namespace("Invoice-2") - xml_root = etree.Element("Invoice", nsmap=nsmap) - - self.env["base.ubl"]._ubl_add_supplier_party( - False, invoice.company_id, "AccountingSupplierParty", xml_root, ns - ) - self.env["base.ubl"]._ubl_add_customer_party( - invoice.partner_id, False, "AccountingCustomerParty", xml_root, ns - ) - - def create_test_invoice( - self, product=False, qty=1, price=12.42, discount=0, validate=True - ): - aio = self.env["account.move"] - aao = self.env["account.account"] - ato = self.env["account.tax"] - company = self.env.ref("base.main_company") - account_revenue = aao.search( - [("code", "=", "707100"), ("company_id", "=", company.id)], limit=1 - ) - if not account_revenue: - account_revenue = aao.create( - { - "code": "707100", - "name": "Product Sales - (test)", - "company_id": company.id, - "account_type": "income", - } - ) - taxes = ato.search( - [ - ("company_id", "=", company.id), - ("type_tax_use", "=", "sale"), - ("unece_type_id", "!=", False), - ("unece_categ_id", "!=", False), - ("amount_type", "=", "percent"), - ] - ) - if taxes: - tax = taxes[0] - else: - unece_type_id = self.env.ref("account_tax_unece.tax_type_vat").id - unece_categ_id = self.env.ref("account_tax_unece.tax_categ_s").id - tax = ato.create( - { - "name": "German VAT purchase 18.0%", - "description": "DE-VAT-sale-18.0", - "company_id": company.id, - "type_tax_use": "sale", - "price_include": False, - "amount": 18, - "amount_type": "percent", - "unece_type_id": unece_type_id, - "unece_categ_id": unece_categ_id, - } - ) - # validate invoice - if not product: - product = self.env.ref("product.product_product_4") - self.env.ref("base.EUR").active = True - invoice = aio.create( - { - "partner_id": self.env.ref("base.res_partner_2").id, - "currency_id": self.env.ref("base.EUR").id, - "move_type": "out_invoice", - "company_id": company.id, - "name": "SO1242", - "invoice_line_ids": [ - ( - 0, - 0, - { - "product_id": product.id, - "product_uom_id": product.uom_id.id, - "quantity": qty, - "price_unit": price, - "discount": discount, - "name": product.name, - "account_id": account_revenue.id, - "tax_ids": [(6, 0, [tax.id])], - }, - ) - ], - } - ) - if validate: - invoice.action_post() - return invoice diff --git a/base_ubl_generate/tests/test_ubl_generate_base.py b/base_ubl_generate/tests/test_ubl_generate_base.py new file mode 100644 index 0000000000..3ffc947c11 --- /dev/null +++ b/base_ubl_generate/tests/test_ubl_generate_base.py @@ -0,0 +1,284 @@ +# Copyright 2025 Camptocamp SA (http://www.camptocamp.com) +# @author Simone Orsi +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). + +from io import BytesIO +from unittest import mock + +from lxml import etree + +from odoo.tests.common import TransactionCase + +from .common import DummyRecord + + +class TestBaseUblGenerate(TransactionCase): + @classmethod + def setUpClass(cls): + super().setUpClass() + cls.base_ubl = cls.env["base.ubl"] + cls.nsmap, cls.ns = cls.base_ubl._ubl_get_nsmap_namespace("Invoice") + cls.partner = DummyRecord( + name="Test Partner", + street="Street 1", + street2="Street 2", + street3="Street 3", + city="Testville", + zip="12345", + state_id=DummyRecord(name="Test State", code="TS"), + country_id=DummyRecord(code="IT", name="Italy"), + parent_id=None, + phone="123456789", + email="test@example.com", + lang="en_US", + commercial_partner_id=None, + vat="IT123456789", + website="https://test.com", + is_company=True, + ) + cls.partner.commercial_partner_id = cls.partner + cls.company = DummyRecord(partner_id=cls.partner) + cls.uom = DummyRecord(name="Unit", unece_code="EA") + cls.currency = DummyRecord(name="EUR") + cls.product = DummyRecord( + name="Test Product", + default_code="TP001", + attribute_line_ids=DummyRecord( + value_ids=DummyRecord( + _iter=[ + DummyRecord(name="Red", attribute_id=DummyRecord(name="Color")), + DummyRecord( + name="Large", attribute_id=DummyRecord(name="Color") + ), + ] + ), + attribute_id=DummyRecord(name="Color"), + ), + barcode="1234567890123", + taxes_id=[], + supplier_taxes_id=[], + ) + cls.tax = DummyRecord( + name="VAT 22%", + unece_categ_id=True, + unece_categ_code="S", + amount_type="percent", + amount=22.0, + unece_type_id=True, + unece_type_code="VAT", + ) + cls.payment_term = DummyRecord(name="30 days") + cls.incoterm = DummyRecord(code="EXW") + cls.buffer = BytesIO(b"PDFDATA") + cls.pdf_content = b"PDFDATA" + cls.xml_string = "UBL" + cls.xml_filename = "ubl.xml" + + def test_ubl_add_country(self): + root = etree.Element(self.ns["cac"] + "Root") + self.base_ubl._ubl_add_country(self.partner.country_id, root, self.ns) + country = root.find(self.ns["cac"] + "Country") + self.assertIsNotNone(country) + self.assertEqual(country.find(self.ns["cbc"] + "IdentificationCode").text, "IT") + self.assertEqual(country.find(self.ns["cbc"] + "Name").text, "Italy") + + def test_ubl_add_address(self): + root = etree.Element(self.ns["cac"] + "Root") + self.base_ubl._ubl_add_address(self.partner, "PostalAddress", root, self.ns) + address = root.find(self.ns["cac"] + "PostalAddress") + self.assertEqual(address.find(self.ns["cbc"] + "Department").text, "Street 1") + self.assertEqual(address.find(self.ns["cbc"] + "StreetName").text, "Street 2") + self.assertEqual( + address.find(self.ns["cbc"] + "AdditionalStreetName").text, "Street 3" + ) + self.assertEqual(address.find(self.ns["cbc"] + "CityName").text, "Testville") + + def test_ubl_get_contact_id(self): + self.assertFalse(self.base_ubl._ubl_get_contact_id(self.partner)) + + def test_ubl_add_contact(self): + root = etree.Element(self.ns["cac"] + "Root") + self.base_ubl._ubl_add_contact(self.partner, root, self.ns) + contact = root.find(self.ns["cac"] + "Contact") + self.assertIsNotNone(contact) + self.assertEqual(contact.find(self.ns["cbc"] + "Telephone").text, "123456789") + self.assertEqual( + contact.find(self.ns["cbc"] + "ElectronicMail").text, "test@example.com" + ) + + def test_ubl_add_language(self): + root = etree.Element(self.ns["cac"] + "Root") + self.base_ubl._ubl_add_language("en_US", root, self.ns) + language = root.find(self.ns["cac"] + "Language") + self.assertEqual(language.find(self.ns["cbc"] + "LocaleCode").text, "en_US") + self.assertEqual(language.find(self.ns["cbc"] + "Name").text, "English (US)") + + def test_ubl_get_party_identification(self): + self.assertEqual(self.base_ubl._ubl_get_party_identification(self.partner), {}) + + def test_ubl_add_party_identification(self): + root = etree.Element(self.ns["cac"] + "Root") + id_info = { + "id": "IT123456789", + "name": "VAT", + "agency_id": "UNECE", + "agency_name": "United Nations Economic Commission for Europe", + } + with mock.patch.object( + type(self.base_ubl), + "_ubl_get_party_identification", + return_value=id_info, + ): + self.base_ubl._ubl_add_party_identification(self.partner, root, self.ns) + party_id = root.find(self.ns["cac"] + "PartyIdentification") + txts = [el.text for el in party_id.findall(self.ns["cbc"] + "ID")] + self.assertEqual( + txts, + [ + "IT123456789", + "VAT", + "UNECE", + "United Nations Economic Commission for Europe", + ], + ) + + def test_ubl_get_tax_scheme_dict_from_partner(self): + d = self.base_ubl._ubl_get_tax_scheme_dict_from_partner(self.partner) + self.assertEqual(d, {"id": "VAT", "name": False, "type_code": False}) + + def test_ubl_add_party_tax_scheme(self): + root = etree.Element(self.ns["cac"] + "Root") + self.base_ubl._ubl_add_party_tax_scheme(self.partner, root, self.ns) + party_tax_scheme = root.find(self.ns["cac"] + "PartyTaxScheme") + self.assertIsNotNone(party_tax_scheme) + self.assertEqual( + party_tax_scheme.find(self.ns["cbc"] + "CompanyID").text, "IT123456789" + ) + + def test_ubl_add_party_legal_entity(self): + root = etree.Element(self.ns["cac"] + "Root") + self.base_ubl._ubl_add_party_legal_entity(self.partner, root, self.ns) + ple = root.find(self.ns["cac"] + "PartyLegalEntity") + self.assertIsNotNone(ple) + self.assertEqual( + ple.find(self.ns["cbc"] + "RegistrationName").text, "Test Partner" + ) + + def test_ubl_add_party(self): + root = etree.Element(self.ns["cac"] + "Root") + self.base_ubl._ubl_add_party(self.partner, self.company, "Party", root, self.ns) + party = root.find(self.ns["cac"] + "Party") + self.assertIsNotNone(party) + self.assertEqual( + party.find(self.ns["cac"] + "PartyName").find(self.ns["cbc"] + "Name").text, + "Test Partner", + ) + + def test_ubl_get_customer_assigned_id(self): + self.assertEqual( + self.base_ubl._ubl_get_customer_assigned_id(self.partner), None + ) + + def test_ubl_add_customer_party(self): + root = etree.Element(self.ns["cac"] + "Root") + node = self.base_ubl._ubl_add_customer_party( + self.partner, self.company, "CustomerParty", root, self.ns + ) + self.assertIsNotNone(node) + self.assertEqual(node.tag, self.ns["cac"] + "CustomerParty") + + def test_ubl_add_supplier_party(self): + root = etree.Element(self.ns["cac"] + "Root") + node = self.base_ubl._ubl_add_supplier_party( + self.partner, self.company, "SupplierParty", root, self.ns + ) + self.assertIsNotNone(node) + self.assertEqual(node.tag, self.ns["cac"] + "SupplierParty") + + def test_ubl_add_delivery(self): + root = etree.Element(self.ns["cac"] + "Root") + self.base_ubl._ubl_add_delivery(self.partner, root, self.ns) + delivery = root.find(self.ns["cac"] + "Delivery") + self.assertIsNotNone(delivery) + + def test_ubl_add_delivery_terms(self): + root = etree.Element(self.ns["cac"] + "Root") + self.base_ubl._ubl_add_delivery_terms(self.incoterm, root, self.ns) + delivery_term = root.find(self.ns["cac"] + "DeliveryTerms") + self.assertIsNotNone(delivery_term) + self.assertEqual(delivery_term.find(self.ns["cbc"] + "ID").text, "EXW") + + def test_ubl_add_payment_terms(self): + root = etree.Element(self.ns["cac"] + "Root") + self.base_ubl._ubl_add_payment_terms(self.payment_term, root, self.ns) + pay_term = root.find(self.ns["cac"] + "PaymentTerms") + self.assertIsNotNone(pay_term) + self.assertEqual(pay_term.find(self.ns["cbc"] + "Note").text, "30 days") + + def test_ubl_add_line_item(self): + root = etree.Element(self.ns["cac"] + "Root") + self.base_ubl._ubl_add_line_item( + 1, + "Test Item", + self.product, + "purchase", + 2, + self.uom, + root, + self.ns, + currency=self.currency, + price_subtotal=20.0, + ) + line_item = root.find(self.ns["cac"] + "LineItem") + self.assertIsNotNone(line_item) + self.assertEqual(line_item.find(self.ns["cbc"] + "ID").text, "1") + + def test_ubl_get_seller_code_from_product(self): + self.assertEqual( + self.base_ubl._ubl_get_seller_code_from_product(self.product), "TP001" + ) + + def test_ubl_get_customer_product_code(self): + self.assertEqual( + self.base_ubl._ubl_get_customer_product_code(self.product, self.partner), "" + ) + + def test_ubl_add_item(self): + root = etree.Element(self.ns["cac"] + "Root") + node = self.base_ubl._ubl_add_item("Test Item", self.product, root, self.ns) + self.assertIsNotNone(node) + self.assertEqual(node.tag, self.ns["cac"] + "Item") + + def test_ubl_add_tax_subtotal(self): + root = etree.Element(self.ns["cac"] + "Root") + self.base_ubl._ubl_add_tax_subtotal(100, 22, self.tax, "EUR", root, self.ns) + tax_subtotal = root.find(self.ns["cac"] + "TaxSubtotal") + taxable_amount = tax_subtotal.find(self.ns["cbc"] + "TaxableAmount") + self.assertEqual(taxable_amount.text, "100.00") + + def test_ubl_add_tax_category(self): + root = etree.Element(self.ns["cac"] + "Root") + self.base_ubl._ubl_add_tax_category(self.tax, root, self.ns) + tax_category = root.find(self.ns["cac"] + "TaxCategory") + self.assertIsNotNone(tax_category) + self.assertEqual(tax_category.find(self.ns["cbc"] + "ID").text, "S") + + def test_ubl_get_tax_scheme_dict_from_tax(self): + d = self.base_ubl._ubl_get_tax_scheme_dict_from_tax(self.tax) + self.assertEqual(d, {"id": "VAT", "name": False, "type_code": False}) + + def test_ubl_add_tax_scheme(self): + root = etree.Element(self.ns["cac"] + "Root") + self.base_ubl._ubl_add_tax_scheme( + {"id": "VAT", "name": "VAT", "type_code": "S"}, root, self.ns + ) + tax_scheme = root.find(self.ns["cac"] + "TaxScheme") + self.assertIsNotNone(tax_scheme) + self.assertEqual(tax_scheme.find(self.ns["cbc"] + "ID").text, "VAT") + + def test_ubl_get_nsmap_namespace(self): + nsmap, ns = self.base_ubl._ubl_get_nsmap_namespace("Invoice") + self.assertIn("cac", nsmap) + self.assertIn("cbc", nsmap) + self.assertIn("cac", ns) + self.assertIn("cbc", ns) diff --git a/base_ubl_generate/tests/test_ubl_generate_invoice.py b/base_ubl_generate/tests/test_ubl_generate_invoice.py new file mode 100644 index 0000000000..9a99529942 --- /dev/null +++ b/base_ubl_generate/tests/test_ubl_generate_invoice.py @@ -0,0 +1,89 @@ +# Copyright 2019 Onestein () +# © 2017-2020 Akretion (Alexis de Lattre ) +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + + +from lxml import etree + +from odoo.tests.common import TransactionCase + +from .common import DummyRecord + + +class TestBaseUblGenerate(TransactionCase): + @classmethod + def setUpClass(cls): + super().setUpClass() + cls.base_ubl = cls.env["base.ubl"] + cls.company = DummyRecord( + name="Test Company", + partner_id=DummyRecord( + name="Test Company Partner", + country_id=DummyRecord(code="DE"), + commercial_partner_id=DummyRecord( + name="Test Company Commercial", + ref="TC1234", + country_id=DummyRecord(code="DE"), + ), + ), + ) + cls.partner = DummyRecord( + name="Test Partner", + company_id=cls.company, + country_id=DummyRecord(code="CH"), + commercial_partner_id=DummyRecord( + name="Test Partner Commercial", ref="TP1234" + ), + ) + cls.product = DummyRecord( + name="Test Product", + uom_id=DummyRecord(id=1, name="Unit"), + ) + cls.account = DummyRecord( + code="707100", + name="Product Sales - (test)", + company_id=cls.company, + account_type="income", + ) + cls.tax = DummyRecord( + name="German VAT purchase 18.0%", + unece_type_id=True, + unece_categ_id=True, + amount_type="percent", + amount=18, + type_tax_use="sale", + company_id=cls.company, + ) + cls.invoice_line = DummyRecord( + product_id=cls.product, + product_uom_id=cls.product.uom_id, + quantity=1, + price_unit=12.42, + discount=0, + name=cls.product.name, + account_id=cls.account, + tax_ids=[cls.tax], + ) + cls.invoice = DummyRecord( + partner_id=cls.partner, + company_id=cls.company, + currency_id=DummyRecord(name="EUR"), + move_type="out_invoice", + name="SO1242", + invoice_line_ids=[cls.invoice_line], + ) + + def test_ubl_generate(self): + nsmap, ns = self.base_ubl._ubl_get_nsmap_namespace("Invoice-2") + xml_root = etree.Element("Invoice", nsmap=nsmap) + self.base_ubl._ubl_add_supplier_party( + None, self.invoice.company_id, "AccountingSupplierParty", xml_root, ns + ) + self.base_ubl._ubl_add_customer_party( + self.invoice.partner_id, None, "AccountingCustomerParty", xml_root, ns + ) + # Check that the tags were added + supplier = xml_root.find(ns["cac"] + "AccountingSupplierParty") + customer = xml_root.find(ns["cac"] + "AccountingCustomerParty") + self.assertIsNotNone(supplier) + self.assertIsNotNone(customer) From 0fb6131b55b603de070ec8133c386b685f66936c Mon Sep 17 00:00:00 2001 From: Maksym Yankin Date: Mon, 6 Apr 2026 17:14:19 +0300 Subject: [PATCH 5/5] [MIG] base_ubl_generate: Migration to 19.0 --- base_ubl_generate/README.rst | 10 +++---- base_ubl_generate/__manifest__.py | 2 +- base_ubl_generate/models/ubl.py | 28 +++++++++++-------- .../static/description/index.html | 6 ++-- 4 files changed, 25 insertions(+), 21 deletions(-) diff --git a/base_ubl_generate/README.rst b/base_ubl_generate/README.rst index 7427a90b31..de98bdd0e3 100644 --- a/base_ubl_generate/README.rst +++ b/base_ubl_generate/README.rst @@ -21,13 +21,13 @@ Base UBL generate :target: http://www.gnu.org/licenses/agpl-3.0-standalone.html :alt: License: AGPL-3 .. |badge3| image:: https://img.shields.io/badge/github-OCA%2Fedi-lightgray.png?logo=github - :target: https://github.com/OCA/edi/tree/18.0/base_ubl_generate + :target: https://github.com/OCA/edi/tree/19.0/base_ubl_generate :alt: OCA/edi .. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png - :target: https://translation.odoo-community.org/projects/edi-18-0/edi-18-0-base_ubl_generate + :target: https://translation.odoo-community.org/projects/edi-19-0/edi-19-0-base_ubl_generate :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/edi&target_branch=18.0 + :target: https://runboat.odoo-community.org/builds?repo=OCA/edi&target_branch=19.0 :alt: Try me on Runboat |badge1| |badge2| |badge3| |badge4| |badge5| @@ -49,7 +49,7 @@ 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 `_. +`feedback `_. Do not contact contributors directly about support or help with technical issues. @@ -85,6 +85,6 @@ 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. -This module is part of the `OCA/edi `_ project on GitHub. +This module is part of the `OCA/edi `_ project on GitHub. You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute. diff --git a/base_ubl_generate/__manifest__.py b/base_ubl_generate/__manifest__.py index 8e8d97421d..243d63f8b5 100644 --- a/base_ubl_generate/__manifest__.py +++ b/base_ubl_generate/__manifest__.py @@ -3,7 +3,7 @@ { "name": "Base UBL generate", - "version": "18.0.1.0.0", + "version": "19.0.1.0.0", "category": "Hidden", "license": "AGPL-3", "summary": "Base module to generate UBL files (Universal Business Language)", diff --git a/base_ubl_generate/models/ubl.py b/base_ubl_generate/models/ubl.py index 115bc56b3c..dd860e7c1b 100644 --- a/base_ubl_generate/models/ubl.py +++ b/base_ubl_generate/models/ubl.py @@ -8,7 +8,7 @@ from lxml import etree -from odoo import _, api, models +from odoo import api, models from odoo.exceptions import UserError from odoo.tools import float_is_zero, float_round @@ -211,9 +211,9 @@ def _ubl_add_customer_party( """Please read the docstring of the method _ubl_add_supplier_party""" if company: if partner: - assert ( - partner.commercial_partner_id == company.partner_id - ), "partner is wrong" + assert partner.commercial_partner_id == company.partner_id, ( + "partner is wrong" + ) else: partner = company.partner_id customer_party_root = etree.SubElement(parent_node, ns["cac"] + node_name) @@ -259,9 +259,9 @@ def _ubl_add_supplier_party( """ if company: if partner: - assert ( - partner.commercial_partner_id == company.partner_id - ), "partner is wrong" + assert partner.commercial_partner_id == company.partner_id, ( + "partner is wrong" + ) else: partner = company.partner_id supplier_party_root = etree.SubElement(parent_node, ns["cac"] + node_name) @@ -323,7 +323,11 @@ def _ubl_add_line_item( line_item_id = etree.SubElement(line_item, ns["cbc"] + "ID") line_item_id.text = str(line_number) if not uom.unece_code: - raise UserError(_("Missing UNECE code on unit of measure '%s'") % uom.name) + raise UserError( + self.env._( + "Missing UNECE code on unit of measure '%(uom)s'", uom=uom.name + ) + ) quantity_node = etree.SubElement( line_item, ns["cbc"] + "Quantity", unitCode=uom.unece_code ) @@ -477,11 +481,11 @@ def _ubl_add_tax_subtotal( taxable_amount_node = etree.SubElement( tax_subtotal, ns["cbc"] + "TaxableAmount", currencyID=currency_code ) - taxable_amount_node.text = "%0.*f" % (prec, taxable_amount) + taxable_amount_node.text = f"{taxable_amount:.{prec}f}" tax_amount_node = etree.SubElement( tax_subtotal, ns["cbc"] + "TaxAmount", currencyID=currency_code ) - tax_amount_node.text = "%0.*f" % (prec, tax_amount) + tax_amount_node.text = f"{tax_amount:.{prec}f}" if tax.amount_type == "percent" and not float_is_zero( tax.amount, precision_digits=prec + 3 ): @@ -496,7 +500,7 @@ def _ubl_add_tax_category( tax_category = etree.SubElement(parent_node, ns["cac"] + node_name) if not tax.unece_categ_id: raise UserError( - _( + self.env._( "Missing UNECE Tax Category on tax '%(tax_name)s'", tax_name=tax.name, ) @@ -517,7 +521,7 @@ def _ubl_add_tax_category( def _ubl_get_tax_scheme_dict_from_tax(self, tax): if not tax.unece_type_id: raise UserError( - _( + self.env._( "Missing UNECE Tax Type on tax '%(tax_name)s'", tax_name=tax.name, ) diff --git a/base_ubl_generate/static/description/index.html b/base_ubl_generate/static/description/index.html index b584a44ca5..43b9682f60 100644 --- a/base_ubl_generate/static/description/index.html +++ b/base_ubl_generate/static/description/index.html @@ -374,7 +374,7 @@

Base UBL generate

!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! !! source digest: sha256:cb66ce9e5b206f852540875959f009daec8d80252497731bc05519ebb274ef8e !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! --> -

Beta License: AGPL-3 OCA/edi Translate me on Weblate Try me on Runboat

+

Beta License: AGPL-3 OCA/edi Translate me on Weblate Try me on Runboat

This module contains methods to generate UBL files. This module doesn’t do anything useful by itself, but it can be used by other modules to process UBL data. Examples:

@@ -398,7 +398,7 @@

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.

+feedback.

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

@@ -430,7 +430,7 @@

Maintainers

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.

-

This module is part of the OCA/edi project on GitHub.

+

This module is part of the OCA/edi project on GitHub.

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