diff --git a/base_ubl_parse/README.rst b/base_ubl_parse/README.rst new file mode 100644 index 0000000000..0384a92902 --- /dev/null +++ b/base_ubl_parse/README.rst @@ -0,0 +1,98 @@ +.. image:: https://odoo-community.org/readme-banner-image + :target: https://odoo-community.org/get-involved?utm_source=readme + :alt: Odoo Community Association + +============== +Base UBL Parse +============== + +.. + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! This file is generated by oca-gen-addon-readme !! + !! changes will be overwritten. !! + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! source digest: sha256:b3e1572cfa48ad7f6a25037935b7de6bb22b6fb2e56cd0bf26eaed7525b6e701 + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + +.. |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_parse + :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_parse + :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 parse UBL files. This module doesn't do +anything useful by itself, but it can be used by other modules to +process UBL data. Examples: + +- *sale_order_import_ubl* that imports UBL sale orders. +- *account_invoice_import_ubl* that imports UBL invoices, + +**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 + +Other credits +------------- + +The development of this module has been financially supported by: + +- Camptocamp + +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_parse/__init__.py b/base_ubl_parse/__init__.py new file mode 100644 index 0000000000..31660d6a96 --- /dev/null +++ b/base_ubl_parse/__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_parse/__manifest__.py b/base_ubl_parse/__manifest__.py new file mode 100644 index 0000000000..f2703056f7 --- /dev/null +++ b/base_ubl_parse/__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 Parse", + "version": "19.0.1.0.0", + "category": "Hidden", + "license": "AGPL-3", + "summary": "Base module to parse 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_parse/i18n/base_ubl_parse.pot b/base_ubl_parse/i18n/base_ubl_parse.pot new file mode 100644 index 0000000000..68cf4e0870 --- /dev/null +++ b/base_ubl_parse/i18n/base_ubl_parse.pot @@ -0,0 +1,27 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * base_ubl_parse +# +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_parse +#: model:ir.model,name:base_ubl_parse.model_base_ubl +msgid "Common methods to generate and parse UBL XML files" +msgstr "" + +#. module: base_ubl_parse +#. odoo-python +#: code:addons/base_ubl_parse/models/ubl.py:0 +msgid "" +"The UBL XML file does not contain the version for validating the content " +"according to the schema." +msgstr "" diff --git a/base_ubl_parse/i18n/it.po b/base_ubl_parse/i18n/it.po new file mode 100644 index 0000000000..5549ab0ac3 --- /dev/null +++ b/base_ubl_parse/i18n/it.po @@ -0,0 +1,32 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * base_ubl_parse +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 18.0\n" +"Report-Msgid-Bugs-To: \n" +"PO-Revision-Date: 2025-06-17 06:27+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 5.10.4\n" + +#. module: base_ubl_parse +#: model:ir.model,name:base_ubl_parse.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_parse +#. odoo-python +#: code:addons/base_ubl_parse/models/ubl.py:0 +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_parse/models/__init__.py b/base_ubl_parse/models/__init__.py new file mode 100644 index 0000000000..7ec1c77b3e --- /dev/null +++ b/base_ubl_parse/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_parse/models/ubl.py b/base_ubl_parse/models/ubl.py new file mode 100644 index 0000000000..e8fc3f0f43 --- /dev/null +++ b/base_ubl_parse/models/ubl.py @@ -0,0 +1,167 @@ +# © 2016-2017 Akretion (Alexis de Lattre ) +# Copyright 2019 Onestein () +# Copyright 2020 Jacques-Etienne Baudoux (BCIM) +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +import logging + +from odoo import api, models +from odoo.exceptions import UserError + +logger = logging.getLogger(__name__) + + +class BaseUbl(models.AbstractModel): + _inherit = "base.ubl" + + # ==================== METHODS TO PARSE UBL files + + @api.model + def _ubl_get_version(self, xml_root, root_name, ns): + version_xpath = xml_root.xpath(f"/{root_name}/cbc:UBLVersionID", namespaces=ns) + if not version_xpath: + raise UserError( + self.env._( + "The UBL XML file does not contain the version " + "for validating the content according to the schema." + ) + ) + return version_xpath[0].text.strip() + + @api.model + def ubl_parse_customer_party(self, party_node, ns): + ref_xpath = party_node.xpath("cbc:SupplierAssignedAccountID", namespaces=ns) + party_node = party_node.xpath("cac:Party", namespaces=ns)[0] + partner_dict = self.ubl_parse_party(party_node, ns) + partner_dict["ref"] = ref_xpath and ref_xpath[0].text or False + return partner_dict + + @api.model + def ubl_parse_supplier_party(self, party_node, ns): + ref_xpath = party_node.xpath("cbc:CustomerAssignedAccountID", namespaces=ns) + party_node = party_node.xpath("cac:Party", namespaces=ns)[0] + partner_dict = self.ubl_parse_party(party_node, ns) + partner_dict["ref"] = ref_xpath and ref_xpath[0].text or False + return partner_dict + + @api.model + def ubl_parse_party(self, party_node, ns): + partner_name_xpath = party_node.xpath("cac:PartyName/cbc:Name", namespaces=ns) + vat_xpath = party_node.xpath("cac:PartyTaxScheme/cbc:CompanyID", namespaces=ns) + website_xpath = party_node.xpath("cbc:WebsiteURI", namespaces=ns) + contact_name_xpath = party_node.xpath("cac:Contact/cbc:Name", namespaces=ns) + contact_email_xpath = party_node.xpath( + "cac:Contact/cbc:ElectronicMail", namespaces=ns + ) + contact_phone_xpath = party_node.xpath( + "cac:Contact/cbc:Telephone", namespaces=ns + ) + partner_dict = { + "vat": vat_xpath and vat_xpath[0].text or False, + "name": partner_name_xpath and partner_name_xpath[0].text or False, + "website": website_xpath and website_xpath[0].text or False, + "contact": contact_name_xpath and contact_name_xpath[0].text or False, + "email": contact_email_xpath and contact_email_xpath[0].text or False, + "phone": contact_phone_xpath and contact_phone_xpath[0].text or False, + } + id_nodes = party_node.xpath("cac:PartyIdentification/cbc:ID", namespaces=ns) + id_numbers = [] + for id_node in id_nodes: + id_numbers.append( + {"value": id_node.text, "schemeID": id_node.attrib.get("schemeID")} + ) + partner_dict["id_number"] = id_numbers + address_xpath = party_node.xpath("cac:PostalAddress", namespaces=ns) + if address_xpath: + address_dict = self.ubl_parse_address(address_xpath[0], ns) + partner_dict.update(address_dict) + return partner_dict + + @api.model + def ubl_parse_address(self, address_node, ns): + country_code_xpath = address_node.xpath( + "cac:Country/cbc:IdentificationCode", namespaces=ns + ) + country_code = country_code_xpath and country_code_xpath[0].text or False + state_code_xpath = address_node.xpath("cbc:CountrySubentityCode", namespaces=ns) + state_code = state_code_xpath and state_code_xpath[0].text or False + street_xpath = address_node.xpath("cbc:StreetName", namespaces=ns) + street2_xpath = address_node.xpath("cbc:AdditionalStreetName", namespaces=ns) + street_number_xpath = address_node.xpath("cbc:BuildingNumber", namespaces=ns) + city_xpath = address_node.xpath("cbc:CityName", namespaces=ns) + zip_xpath = address_node.xpath("cbc:PostalZone", namespaces=ns) + zip_code = ( + zip_xpath + and zip_xpath[0].text + and zip_xpath[0].text.replace(" ", "") + or False + ) + address_dict = { + "street": street_xpath and street_xpath[0].text or False, + "street_number": street_number_xpath + and street_number_xpath[0].text + or False, + "street2": street2_xpath and street2_xpath[0].text or False, + "city": city_xpath and city_xpath[0].text or False, + "zip": zip_code, + "state_code": state_code, + "country_code": country_code, + } + return address_dict + + @api.model + def ubl_parse_delivery(self, delivery_node, ns): + party_xpath = delivery_node.xpath("cac:DeliveryParty", namespaces=ns) + if party_xpath: + partner_dict = self.ubl_parse_party(party_xpath[0], ns) + else: + partner_dict = {} + postal_xpath = delivery_node.xpath( + "cac:DeliveryParty/cac:PostalAddress", namespaces=ns + ) + if not postal_xpath: + delivery_address_xpath = delivery_node.xpath( + "cac:DeliveryLocation/cac:Address", namespaces=ns + ) + if not delivery_address_xpath: + delivery_address_xpath = delivery_node.xpath( + "cac:DeliveryAddress", namespaces=ns + ) + if delivery_address_xpath: + partner_dict.update( + self.ubl_parse_address(delivery_address_xpath[0], ns) + ) + return partner_dict + + @api.model + def ubl_parse_delivery_details(self, delivery_node, ns): + delivery_dict = {} + latest_date = delivery_node.xpath("cbc:LatestDeliveryDate", namespaces=ns) + latest_time = delivery_node.xpath("cbc:LatestDeliveryTime", namespaces=ns) + if latest_date: + latest_delivery = latest_date[0].text + if latest_time: + latest_delivery += " " + latest_time[0].text[:-3] + delivery_dict["commitment_date"] = latest_delivery + return delivery_dict + + def ubl_parse_incoterm(self, delivery_term_node, ns): + incoterm_xpath = delivery_term_node.xpath("cbc:ID", namespaces=ns) + if incoterm_xpath: + incoterm_dict = {"code": incoterm_xpath[0].text} + return incoterm_dict + return {} + + def ubl_parse_product(self, line_node, ns): + barcode_xpath = line_node.xpath( + "cac:Item/cac:StandardItemIdentification/cbc:ID[@schemeID='GTIN']", + namespaces=ns, + ) + code_xpath = line_node.xpath( + "cac:Item/cac:SellersItemIdentification/cbc:ID", namespaces=ns + ) + product_dict = { + "barcode": barcode_xpath and barcode_xpath[0].text or False, + "code": code_xpath and code_xpath[0].text or False, + } + return product_dict diff --git a/base_ubl_parse/pyproject.toml b/base_ubl_parse/pyproject.toml new file mode 100644 index 0000000000..4231d0cccb --- /dev/null +++ b/base_ubl_parse/pyproject.toml @@ -0,0 +1,3 @@ +[build-system] +requires = ["whool"] +build-backend = "whool.buildapi" diff --git a/base_ubl_parse/readme/CONTRIBUTORS.md b/base_ubl_parse/readme/CONTRIBUTORS.md new file mode 100644 index 0000000000..c43aab59cf --- /dev/null +++ b/base_ubl_parse/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_parse/readme/CREDITS.md b/base_ubl_parse/readme/CREDITS.md new file mode 100644 index 0000000000..705d3b30ca --- /dev/null +++ b/base_ubl_parse/readme/CREDITS.md @@ -0,0 +1,3 @@ +The development of this module has been financially supported by: + +- Camptocamp diff --git a/base_ubl_parse/readme/DESCRIPTION.md b/base_ubl_parse/readme/DESCRIPTION.md new file mode 100644 index 0000000000..4dec66015a --- /dev/null +++ b/base_ubl_parse/readme/DESCRIPTION.md @@ -0,0 +1,7 @@ +This module contains methods to parse UBL files. This +module doesn't do anything useful by itself, +but it can be used by other modules to process UBL data. +Examples: + +- *sale_order_import_ubl* that imports UBL sale orders. +- *account_invoice_import_ubl* that imports UBL invoices, diff --git a/base_ubl_parse/static/description/icon.png b/base_ubl_parse/static/description/icon.png new file mode 100644 index 0000000000..1dcc49c24f Binary files /dev/null and b/base_ubl_parse/static/description/icon.png differ diff --git a/base_ubl_parse/static/description/index.html b/base_ubl_parse/static/description/index.html new file mode 100644 index 0000000000..f4ebc9e4d8 --- /dev/null +++ b/base_ubl_parse/static/description/index.html @@ -0,0 +1,449 @@ + + + + + +README.rst + + + +
+ + + +Odoo Community Association + +
+

Base UBL Parse

+ +

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

+

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

+
    +
  • sale_order_import_ubl that imports UBL sale orders.
  • +
  • account_invoice_import_ubl that imports UBL invoices,
  • +
+

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

+ +
+
+

Other credits

+

The development of this module has been financially supported by:

+
    +
  • Camptocamp
  • +
+
+
+

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_parse/tests/__init__.py b/base_ubl_parse/tests/__init__.py new file mode 100644 index 0000000000..535c957e9b --- /dev/null +++ b/base_ubl_parse/tests/__init__.py @@ -0,0 +1 @@ +from . import test_ubl_parse diff --git a/base_ubl_parse/tests/samples/UBL-Invoice-2.1-Example.xml b/base_ubl_parse/tests/samples/UBL-Invoice-2.1-Example.xml new file mode 100644 index 0000000000..5921e766d6 --- /dev/null +++ b/base_ubl_parse/tests/samples/UBL-Invoice-2.1-Example.xml @@ -0,0 +1,470 @@ + + + 2.1 + TOSL108 + 2009-12-15 + 380 + Ordered in our booth at the convention. + 2009-11-30 + EUR + Project cost code 123 + + 2009-11-01 + 2009-11-30 + + + 123 + + + Contract321 + Framework agreement + + + Doc1 + Timesheet + + + http://www.suppliersite.eu/sheet001.html + + + + + Doc2 + Drawing + + UjBsR09EbGhjZ0dTQUxNQUFBUUNBRU1tQ1p0dU1GUXhEUzhi + + + + + 1234567890123 + + Supp123 + + + Salescompany ltd. + + + 1231412341324 + 5467 + Main street + Suite 123 + 1 + Revenue department + Big city + 54321 + RegionA + + DK + + + + DK12345 + + VAT + + + + The Sellercompany Incorporated + 5402697509 + + Big city + RegionA + + DK + + + + + 4621230 + 4621231 + antonio@salescompany.dk + + + Antonio + M + Salemacher + Sales manager + + + + + + 1234567987654 + + 345KS5324 + + + Buyercompany ltd + + + 1238764941386 + 123 + Anystreet + Back door + 8 + Accounting department + Anytown + 101 + RegionB + + BE + + + + BE54321 + + VAT + + + + The buyercompany inc. + 5645342123 + + Mainplace + RegionB + + BE + + + + + 5121230 + 5121231 + john@buyercompany.eu + + + John + X + Doe + Purchasing manager + + + + + + 098740918237 + + + Ebeneser Scrooge Inc. + + + 6411982340 + + + + 2009-12-15 + + 6754238987648 + + Deliverystreet + Side door + 12 + DeliveryCity + 523427 + RegionC + + BE + + + + + + 31 + 2009-12-31 + IBAN + Payref1 + + DK1212341234123412 + + + DKDKABCD + + + + + + Penalty percentage 10% from due date + + + true + Packing cost + 100 + + + false + Promotion discount + 100 + + + 292.20 + + 1460.5 + 292.1 + + S + 20 + + VAT + + + + + 1 + 0.1 + + AA + 10 + + VAT + + + + + -25 + 0 + + E + 0 + AAM + Exempt New Means of Transport + + VAT + + + + + + 1436.5 + 1436.5 + 1729 + 100 + 100 + 1000 + 0.30 + 729 + + + 1 + Scratch on box + 1 + 1273 + BookingCode001 + + 1 + + + false + Damage + 12 + + + true + Testing + 10 + + + 254.6 + + + Processor: Intel Core 2 Duo SU9400 LV (1.4GHz). RAM: + 3MB. Screen 1440x900 + Labtop computer + + JB007 + + + 1234567890124 + + + 12344321 + + + 65434568 + + + S + 20 + + VAT + + + + Color + black + + + + 1273 + 1 + + false + Contract + 0.15 + 225 + 1500 + + + + + 2 + Cover is slightly damaged. + -1 + -3.96 + + 5 + + + -0.396 + + + Returned "Advanced computing" book + + JB008 + + + 1234567890125 + + + 32344324 + + + 65434567 + + + AA + 10 + + VAT + + + + + 3.96 + 1 + + + + 3 + 2 + 4.96 + + 3 + + + 0.496 + + + "Computing for dummies" book + + JB009 + + + 1234567890126 + + + 32344324 + + + 65434566 + + + AA + 10 + + VAT + + + + + 2.48 + 1 + + false + Contract + 0.1 + 0.275 + 2.75 + + + + + 4 + -1 + -25 + + 2 + + + 0 + + + Returned IBM 5150 desktop + + JB010 + + + 1234567890127 + + + 12344322 + + + 65434565 + + + E + 0 + + VAT + + + + + 25 + 1 + + + + 5 + 250 + 187.5 + BookingCode002 + + 4 + + + 37.5 + + + Network cable + + JB011 + + + 1234567890128 + + + 12344325 + + + 65434564 + + + S + 20 + + VAT + + + + Type + Cat5 + + + + 0.75 + 1 + + + \ No newline at end of file diff --git a/base_ubl_parse/tests/samples/UBL-Order-2.1-Example.xml b/base_ubl_parse/tests/samples/UBL-Order-2.1-Example.xml new file mode 100644 index 0000000000..6d03bffe38 --- /dev/null +++ b/base_ubl_parse/tests/samples/UBL-Order-2.1-Example.xml @@ -0,0 +1,339 @@ + + + 2.1 + urn:www.cenbii.eu:transaction:biicoretrdm001:ver1.0 + urn:www.cenbii.eu:profile:BII01:ver1.0 + 34 + 2010-01-20 + 12:30:00 + Information text for the whole order + SEK + Project123 + + 2010-01-31 + + + QuoteID123 + + + RjectedOrderID123 + + + MAFO + + + Doc1 + Timesheet + + + http://www.suppliersite.eu/sheet001.html + + + + + Doc2 + Drawing + + UjBsR09EbGhjZ0dTQUxNQUFBUUNBRU1tQ1p0dU1GUXhEUzhi + + + + 34322 + FrameworkAgreementID123 + + + + 7300072311115 + + 7300070011115 + + + PartyID123 + + + Johnssons byggvaror + + + 1234567890123 + PoBox123 + Rådhusgatan + 2nd floor + 5 + Purchasing department + Stockholm + 11000 + RegionX + + SE + + + + Herra Johnssons byggvaror AS + SE1234567801 + + Stockholm + + SE + + + + VAT + + + + Johnssons Byggvaror AB + 5532331183 + + Stockholm + RegionX + + SE + + + + + 123456 + 123456 + pelle@johnsson.se + + + Pelle + Svensson + X + Boss + + + + Eva Johnsson + 1234356 + 123455 + eva@johnsson.se + + + + + 7302347231111 + + SellerPartyID123 + + + Moderna Produkter AB + + + 0987654321123 + 321 + Kungsgatan + suite12 + 22 + Sales department + Stockholm + 11000 + RegionX + + SE + + + + Moderna Produkter AB + 5532332283 + + Stockholm + RegionX + + SE + + + + + 34557 + 3456767 + lars@moderna.se + + + Lars + Petersen + M + Sales manager + + + + + + + 0987678321123 + + + Moderna Produkter AB + + + 346788 + 8567443 + sven@moderna.se + + + Sven + Pereson + N + Stuffuser + + + + + + + 1234567890123 + 123 + Rådhusgatan + 2nd floor + 5 + Purchasing department + Stockholm + 11000 + RegionX + + SE + + + + 2010-02-10 + 11:30:00 + + 2010-02-10 + 2010-02-25 + + + + 67654328394567 + + + Swedish trucking + + + Per + 987098709 + 34673435 + bill@svetruck.se + + + + + FOT + CAD + + STO + + + + true + Transport documents + 100 + + + false + Total order value discount + 100 + + + 100 + + + 6225 + 100 + 100 + 6225 + + + Freetext note on line 1 + + 1 + 120 + 6000 + 10 + false + ProjectID123 + + + 2010-02-10 + 2010-02-25 + + + + + EmployeeXXX + + + Josef K. + + + + 50 + 1 + + + Red paint + Falu Rödfärg + + SItemNo001 + + + 1234567890123 + + + Paint type + Acrylic + + + Solvant + Water + + + + + + Freetext note on line 2 + + 2 + 15 + 225 + 10 + false + ProjectID123 + + + 2010-02-10 + 2010-02-25 + + + + + EmployeeXXX + + + Josef K. + + + + 15 + 1 + + + Very good pencils for red paint. + Pensel 20 mm + + SItemNo011 + + + 123452340123 + + + Hair color + Black + + + Width + 20mm + + + + + diff --git a/base_ubl_parse/tests/test_ubl_parse.py b/base_ubl_parse/tests/test_ubl_parse.py new file mode 100644 index 0000000000..dd6e4b3d14 --- /dev/null +++ b/base_ubl_parse/tests/test_ubl_parse.py @@ -0,0 +1,170 @@ +# 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 lxml import etree + +from odoo.tests.common import TransactionCase +from odoo.tools.misc import file_open, file_path + + +class TestBaseUblParse(TransactionCase): + @classmethod + def setUpClass(cls): + super().setUpClass() + cls.base_ubl = cls.env["base.ubl"] + cls.inv_filepath = file_path( + "base_ubl_parse/tests/samples/UBL-Invoice-2.1-Example.xml" + ) + cls.ord_filepath = file_path( + "base_ubl_parse/tests/samples/UBL-Order-2.1-Example.xml" + ) + cls.ns = { + "cbc": "urn:oasis:names:specification:ubl:schema:xsd:CommonBasicComponents-2", # noqa + "cac": "urn:oasis:names:specification:ubl:schema:xsd:CommonAggregateComponents-2", # noqa + } + + def _get_xml_root(self, file_path): + with file_open(file_path) as f: + return etree.parse(f).getroot() + + def test_ubl_get_version(self): + xml = etree.fromstring( + b'' # noqa + b"2.1" + ) + version = self.base_ubl._ubl_get_version( + xml, + "Invoice", + { + "cbc": "urn:oasis:names:specification:ubl:schema:xsd:CommonBasicComponents-2" # noqa + }, + ) + self.assertEqual(version, "2.1") + + def test_ubl_parse_customer_party(self): + root = self._get_xml_root(self.inv_filepath) + party_node = root.find(".//cac:AccountingCustomerParty", namespaces=self.ns) + parsed = parsed = self.base_ubl.ubl_parse_customer_party(party_node, self.ns) + expected = { + "vat": "BE54321", + "name": "Buyercompany ltd", + "website": False, + "contact": False, + "email": "john@buyercompany.eu", + "phone": "5121230", + "id_number": [{"value": "345KS5324", "schemeID": "ZZZ"}], + "street": "Anystreet", + "street_number": "8", + "street2": "Back door", + "city": "Anytown", + "zip": "101", + "state_code": False, + "country_code": "BE", + "ref": False, + } + self.assertEqual(parsed, expected) + + def test_ubl_parse_supplier_party(self): + root = self._get_xml_root(self.inv_filepath) + party_node = root.find(".//cac:AccountingSupplierParty", namespaces=self.ns) + parsed = self.base_ubl.ubl_parse_supplier_party(party_node, self.ns) + expected = { + "city": "Big city", + "contact": False, + "country_code": "DK", + "email": "antonio@salescompany.dk", + "id_number": [{"schemeID": "ZZZ", "value": "Supp123"}], + "name": "Salescompany ltd.", + "phone": "4621230", + "ref": False, + "state_code": "RegionA", + "street": "Main street", + "street2": "Suite 123", + "street_number": "1", + "vat": "DK12345", + "website": False, + "zip": "54321", + } + self.assertEqual(parsed, expected) + + def test_ubl_parse_party(self): + root = self._get_xml_root(self.inv_filepath) + party_node = root.find(".//cac:Party", namespaces=self.ns) + parsed = self.base_ubl.ubl_parse_party(party_node, self.ns) + expected = { + "city": "Big city", + "contact": False, + "country_code": "DK", + "email": "antonio@salescompany.dk", + "id_number": [{"schemeID": "ZZZ", "value": "Supp123"}], + "name": "Salescompany ltd.", + "phone": "4621230", + "state_code": "RegionA", + "street": "Main street", + "street2": "Suite 123", + "street_number": "1", + "vat": "DK12345", + "website": False, + "zip": "54321", + } + self.assertEqual(parsed, expected) + + def test_ubl_parse_address(self): + root = self._get_xml_root(self.inv_filepath) + address_node = root.find(".//cac:PostalAddress", namespaces=self.ns) + parsed = self.base_ubl.ubl_parse_address(address_node, self.ns) + expected = { + "street": "Main street", + "street_number": "1", + "street2": "Suite 123", + "city": "Big city", + "zip": "54321", + "state_code": "RegionA", + "country_code": "DK", + } + self.assertEqual(parsed, expected) + + def test_ubl_parse_delivery(self): + root = self._get_xml_root(self.ord_filepath) + delivery_node = root.find(".//cac:Delivery", namespaces=self.ns) + parsed = self.base_ubl.ubl_parse_delivery(delivery_node, self.ns) + expected = { + "city": "Stockholm", + "contact": "Per", + "country_code": "SE", + "email": "bill@svetruck.se", + "id_number": [{"schemeID": "GLN", "value": "67654328394567"}], + "name": "Swedish trucking", + "phone": "987098709", + "state_code": False, + "street": "Rådhusgatan", + "street2": "2nd floor", + "street_number": "5", + "vat": False, + "website": False, + "zip": "11000", + } + self.assertEqual(parsed, expected) + + def test_ubl_parse_delivery_details(self): + root = self._get_xml_root(self.ord_filepath) + delivery_node = root.find(".//cac:Delivery", namespaces=self.ns) + parsed = self.base_ubl.ubl_parse_delivery_details(delivery_node, self.ns) + expected = {"commitment_date": "2010-02-10 11:30"} + self.assertEqual(parsed, expected) + + def test_ubl_parse_incoterm(self): + root = self._get_xml_root(self.ord_filepath) + delivery_term_node = root.find(".//cac:DeliveryTerms", namespaces=self.ns) + parsed = self.base_ubl.ubl_parse_incoterm(delivery_term_node, self.ns) + expected = {"code": "FOT"} + self.assertEqual(parsed, expected) + + def test_ubl_parse_product(self): + root = self._get_xml_root(self.inv_filepath) + line_node = root.find(".//cac:InvoiceLine", namespaces=self.ns) + parsed = self.base_ubl.ubl_parse_product(line_node, self.ns) + expected = {"barcode": "1234567890124", "code": "JB007"} + self.assertEqual(parsed, expected)