diff --git a/base_ubl_generate/README.rst b/base_ubl_generate/README.rst new file mode 100644 index 0000000000..de98bdd0e3 --- /dev/null +++ b/base_ubl_generate/README.rst @@ -0,0 +1,90 @@ +.. 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/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-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=19.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/__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..243d63f8b5 --- /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": "19.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/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 new file mode 100644 index 0000000000..0dd2ccd064 --- /dev/null +++ b/base_ubl_generate/i18n/es.po @@ -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 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 +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 +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 +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 +msgid "Missing UNECE code on unit of measure '%s'" +msgstr "Falta el código UNECE en la unidad de medida '%s'" + +#, 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..744ffddd49 --- /dev/null +++ b/base_ubl_generate/i18n/fr.po @@ -0,0 +1,51 @@ +# 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 +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 +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 +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 +msgid "Missing UNECE code on unit of measure '%s'" +msgstr "Code UNECE manquant pour l'unité de mesure '%s'" + +#, 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..695b369443 --- /dev/null +++ b/base_ubl_generate/i18n/hr.po @@ -0,0 +1,41 @@ +# 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 +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 +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 +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 +msgid "Missing UNECE code on unit of measure '%s'" +msgstr "Nedostaje UNECE šifra jedinice mjere za '%s'" diff --git a/base_ubl_generate/i18n/it.po b/base_ubl_generate/i18n/it.po new file mode 100644 index 0000000000..c30d3fea58 --- /dev/null +++ b/base_ubl_generate/i18n/it.po @@ -0,0 +1,48 @@ +# 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 +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 +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 +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 +msgid "Missing UNECE code on unit of measure '%s'" +msgstr "Codice UNECE mancante nell'unità di misura '%s'" + +#, 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..69aa5ec306 --- /dev/null +++ b/base_ubl_generate/i18n/nl.po @@ -0,0 +1,50 @@ +# 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 +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 +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 "Ontbrekende UNECE code op maateenheid '%s'" + +#, 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..dd860e7c1b --- /dev/null +++ b/base_ubl_generate/models/ubl.py @@ -0,0 +1,547 @@ +# © 2016-2017 Akretion (Alexis de Lattre ) +# Copyright 2019 Onestein () +# Copyright 2020 Jacques-Etienne Baudoux (BCIM) +# Copyright 2025 Simome Orsi \> +- 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/static/description/icon.png b/base_ubl_generate/static/description/icon.png new file mode 100644 index 0000000000..1dcc49c24f Binary files /dev/null and b/base_ubl_generate/static/description/icon.png differ diff --git a/base_ubl_generate/static/description/index.html b/base_ubl_generate/static/description/index.html new file mode 100644 index 0000000000..43b9682f60 --- /dev/null +++ b/base_ubl_generate/static/description/index.html @@ -0,0 +1,440 @@ + + + + + +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 new file mode 100644 index 0000000000..02b5846e35 --- /dev/null +++ b/base_ubl_generate/tests/__init__.py @@ -0,0 +1,4 @@ +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +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_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)