diff --git a/account_edi_ubl_cii_purchase_match_product_packaging/README.rst b/account_edi_ubl_cii_purchase_match_product_packaging/README.rst new file mode 100644 index 0000000000..3b3d36d96f --- /dev/null +++ b/account_edi_ubl_cii_purchase_match_product_packaging/README.rst @@ -0,0 +1,95 @@ +.. image:: https://odoo-community.org/readme-banner-image + :target: https://odoo-community.org/get-involved?utm_source=readme + :alt: Odoo Community Association + +==================================================== +Account Edi Ubl Cii Purchase Match Product Packaging +==================================================== + +.. + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! This file is generated by oca-gen-addon-readme !! + !! changes will be overwritten. !! + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! source digest: sha256:aa1bb8c238590904cd7495d3d7e66d15c29020fd61c29dad0c06f3f06ec467cd + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + +.. |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/16.0/account_edi_ubl_cii_purchase_match_product_packaging + :alt: OCA/edi +.. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png + :target: https://translation.odoo-community.org/projects/edi-16-0/edi-16-0-account_edi_ubl_cii_purchase_match_product_packaging + :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=16.0 + :alt: Try me on Runboat + +|badge1| |badge2| |badge3| |badge4| |badge5| + +This module provides a fallback mechanism to recover the Unit of Measure +(UoM) and product packaging during purchase line reconciliation of +vendor bills imported from UBL/CII files. + +It is a glue module between: + +- Account Edi Ubl Cii Purchase Match +- Account EDI UBL move line uom and packaging By UNECE + +**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 +------- + +* ACSONE SA/NV + +Contributors +------------ + +- Souheil Bejaoui - ACSONE SA/NV souheil.bejaoui@acsone.eu + +Maintainers +----------- + +This module is maintained by the OCA. + +.. image:: https://odoo-community.org/logo.png + :alt: Odoo Community Association + :target: https://odoo-community.org + +OCA, or the Odoo Community Association, is a nonprofit organization whose +mission is to support the collaborative development of Odoo features and +promote its widespread use. + +.. |maintainer-sbejaoui| image:: https://github.com/sbejaoui.png?size=40px + :target: https://github.com/sbejaoui + :alt: sbejaoui + +Current `maintainer `__: + +|maintainer-sbejaoui| + +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/account_edi_ubl_cii_purchase_match_product_packaging/__init__.py b/account_edi_ubl_cii_purchase_match_product_packaging/__init__.py new file mode 100644 index 0000000000..0650744f6b --- /dev/null +++ b/account_edi_ubl_cii_purchase_match_product_packaging/__init__.py @@ -0,0 +1 @@ +from . import models diff --git a/account_edi_ubl_cii_purchase_match_product_packaging/__manifest__.py b/account_edi_ubl_cii_purchase_match_product_packaging/__manifest__.py new file mode 100644 index 0000000000..b63949b2da --- /dev/null +++ b/account_edi_ubl_cii_purchase_match_product_packaging/__manifest__.py @@ -0,0 +1,19 @@ +# Copyright 2026 ACSONE SA/NV +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +{ + "name": "Account Edi Ubl Cii Purchase Match Product Packaging", + "summary": """Allows retrieving the correct UoM and packaging from UNECE codes + when matching invoice lines with purchase orders""", + "version": "16.0.1.0.0", + "license": "AGPL-3", + "author": "ACSONE SA/NV,Odoo Community Association (OCA)", + "website": "https://github.com/OCA/edi", + "maintainers": ["sbejaoui"], + "depends": [ + "account_edi_ubl_cii_purchase_match", + "account_edi_ubl_move_line_uom_and_packaging_unece", + ], + "data": [], + "demo": [], +} diff --git a/account_edi_ubl_cii_purchase_match_product_packaging/models/__init__.py b/account_edi_ubl_cii_purchase_match_product_packaging/models/__init__.py new file mode 100644 index 0000000000..5226bd6496 --- /dev/null +++ b/account_edi_ubl_cii_purchase_match_product_packaging/models/__init__.py @@ -0,0 +1,2 @@ +from . import account_move_line +from . import account_edi_xml_ubl_20 diff --git a/account_edi_ubl_cii_purchase_match_product_packaging/models/account_edi_xml_ubl_20.py b/account_edi_ubl_cii_purchase_match_product_packaging/models/account_edi_xml_ubl_20.py new file mode 100644 index 0000000000..4084d493fc --- /dev/null +++ b/account_edi_ubl_cii_purchase_match_product_packaging/models/account_edi_xml_ubl_20.py @@ -0,0 +1,18 @@ +# Copyright 2026 ACSONE SA/NV +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from odoo import models + + +class AccountEdiXmlUbl_20(models.AbstractModel): + + _inherit = "account.edi.xml.ubl_20" + + def _import_fill_invoice_line_packaging(self, invoice_line): + res = super()._import_fill_invoice_line_packaging(invoice_line) + # force recomputation of the quantity + # this is needed because after a second reconciliation with a purchase line, + # the packaging may remain the same while the underlying quantity changes + # without this explicit call, the computed quantity may become inconsistent + invoice_line._compute_quantity() + return res diff --git a/account_edi_ubl_cii_purchase_match_product_packaging/models/account_move_line.py b/account_edi_ubl_cii_purchase_match_product_packaging/models/account_move_line.py new file mode 100644 index 0000000000..8eec6c2d2a --- /dev/null +++ b/account_edi_ubl_cii_purchase_match_product_packaging/models/account_move_line.py @@ -0,0 +1,20 @@ +# Copyright 2026 ACSONE SA/NV +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from odoo import models + + +class AccountMoveLine(models.Model): + + _inherit = "account.move.line" + + def _set_product(self, product): + self.ensure_one() + res = super()._set_product(product) + + if self.ubl_unece_unit_code: + if not self.env[ + "account.edi.xml.ubl_20" + ]._import_fill_invoice_line_packaging(self): + self.env["account.edi.xml.ubl_20"]._import_fill_invoice_line_uom(self) + return res diff --git a/account_edi_ubl_cii_purchase_match_product_packaging/readme/CONTRIBUTORS.md b/account_edi_ubl_cii_purchase_match_product_packaging/readme/CONTRIBUTORS.md new file mode 100644 index 0000000000..18f298014b --- /dev/null +++ b/account_edi_ubl_cii_purchase_match_product_packaging/readme/CONTRIBUTORS.md @@ -0,0 +1 @@ +- Souheil Bejaoui - ACSONE SA/NV \ No newline at end of file diff --git a/account_edi_ubl_cii_purchase_match_product_packaging/readme/DESCRIPTION.md b/account_edi_ubl_cii_purchase_match_product_packaging/readme/DESCRIPTION.md new file mode 100644 index 0000000000..fb16853871 --- /dev/null +++ b/account_edi_ubl_cii_purchase_match_product_packaging/readme/DESCRIPTION.md @@ -0,0 +1,7 @@ +This module provides a fallback mechanism to recover the Unit of Measure (UoM) +and product packaging during purchase line reconciliation of vendor bills +imported from UBL/CII files. + +It is a glue module between: +- Account Edi Ubl Cii Purchase Match +- Account EDI UBL move line uom and packaging By UNECE diff --git a/account_edi_ubl_cii_purchase_match_product_packaging/static/description/icon.png b/account_edi_ubl_cii_purchase_match_product_packaging/static/description/icon.png new file mode 100644 index 0000000000..3a0328b516 Binary files /dev/null and b/account_edi_ubl_cii_purchase_match_product_packaging/static/description/icon.png differ diff --git a/account_edi_ubl_cii_purchase_match_product_packaging/static/description/index.html b/account_edi_ubl_cii_purchase_match_product_packaging/static/description/index.html new file mode 100644 index 0000000000..8ad9f5bc40 --- /dev/null +++ b/account_edi_ubl_cii_purchase_match_product_packaging/static/description/index.html @@ -0,0 +1,438 @@ + + + + + +README.rst + + + +
+ + + +Odoo Community Association + +
+

Account Edi Ubl Cii Purchase Match Product Packaging

+ +

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

+

This module provides a fallback mechanism to recover the Unit of Measure +(UoM) and product packaging during purchase line reconciliation of +vendor bills imported from UBL/CII files.

+

It is a glue module between:

+
    +
  • Account Edi Ubl Cii Purchase Match
  • +
  • Account EDI UBL move line uom and packaging By UNECE
  • +
+

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

+
    +
  • ACSONE SA/NV
  • +
+
+
+

Contributors

+ +
+
+

Maintainers

+

This module is maintained by the OCA.

+ +Odoo Community Association + +

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

+

Current maintainer:

+

sbejaoui

+

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/account_edi_ubl_cii_purchase_match_product_packaging/tests/__init__.py b/account_edi_ubl_cii_purchase_match_product_packaging/tests/__init__.py new file mode 100644 index 0000000000..3dfdaec930 --- /dev/null +++ b/account_edi_ubl_cii_purchase_match_product_packaging/tests/__init__.py @@ -0,0 +1 @@ +from . import test_account_edi_ubl_cii_purchase_match_product_packaging diff --git a/account_edi_ubl_cii_purchase_match_product_packaging/tests/test_account_edi_ubl_cii_purchase_match_product_packaging.py b/account_edi_ubl_cii_purchase_match_product_packaging/tests/test_account_edi_ubl_cii_purchase_match_product_packaging.py new file mode 100644 index 0000000000..7206bdbf63 --- /dev/null +++ b/account_edi_ubl_cii_purchase_match_product_packaging/tests/test_account_edi_ubl_cii_purchase_match_product_packaging.py @@ -0,0 +1,197 @@ +# Copyright 2026 ACSONE SA/NV, BCIM +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from odoo import Command +from odoo.tests import tagged +from odoo.tools import file_open + +from odoo.addons.account.tests.common import AccountTestInvoicingCommon + + +@tagged("post_install", "-at_install") +class TestAccountEdiUblCiiPurchaseMatchProductPackaging(AccountTestInvoicingCommon): + @classmethod + def setUpClass(cls, chart_template_ref=None): + super().setUpClass() + cls.env.user.groups_id += cls.env.ref("product.group_stock_packaging") + cls.env["ir.config_parameter"].set_param( + "account_edi.product_name_match", "True" + ) + cls.env["ir.sequence"].search( + [("code", "=", "purchase.order")], limit=1 + ).prefix = "PO" + cls.partner = cls.env["res.partner"].create( + {"name": "ALD Automotive LU", "vat": "LU25587702"} + ) + cls.uom_unit = cls.env.ref("uom.product_uom_unit") + cls.uom_dozen = cls.env.ref("uom.product_uom_dozen") + cls.unece_packaging_type_x8a = cls.env["unece.code.list"].create( + { + "type": "packaging_type", + "code": "X8A", + "name": "Pallet, wooden", + "description": "Wooden pallet.", + } + ) + cls.packaging_level = cls.env["product.packaging.level"].create( + { + "name": "Pallet", + "code": "PAL", + "sequence": 2, + "unece_type_ids": [Command.set(cls.unece_packaging_type_x8a.ids)], + } + ) + cls.product = cls.env["product.product"].create( + { + "name": "Locations and leasing", + "type": "consu", + "uom_id": cls.uom_unit.id, + } + ) + cls.purchase_order = cls.env["purchase.order"].create( + { + "partner_id": cls.partner.id, + "name": "PO00101", + "order_line": [ + Command.create( + { + "name": cls.product.name, + "product_id": cls.product.id, + "product_qty": 1, + "price_unit": 1000, + } + ), + ], + } + ) + cls.purchase_order.button_confirm() + cls.po_line = cls.purchase_order.order_line + + def _import_invoice(self, journal, file_path): + with file_open(file_path, "rb") as file: + xml_attachment = self.env["ir.attachment"].create( + { + "mimetype": "application/xml", + "name": "test_invoice.xml", + "raw": file.read(), + } + ) + move = ( + self.env["account.journal"] + .with_context(default_journal_id=journal.id) + ._create_document_from_attachment(xml_attachment.id) + ) + return move + + def test_0(self): + """ + the UNECE unit code from the UBL file is stored on the invoice inv_line during import + """ + file_path = ( + "account_edi_ubl_cii_purchase_match_product_packaging/tests/test_files/" + "bis3_bill_example_uom_dozen.xml" + ) + bill = self._import_invoice( + self.company_data["default_journal_purchase"], file_path + ) + inv_line = bill.invoice_line_ids + self.assertEqual(inv_line.ubl_unece_unit_code, "DPC") + self.assertAlmostEqual(inv_line.ubl_price_unit, 657.0) + self.assertAlmostEqual(inv_line.price_unit, 657.0) + + def test_1(self): + """ + If the UNECE code can't be matched to a uom at import time, + it is still stored and later used during reconciliation to recover and apply + the correct uom once available + """ + self.uom_dozen.unece_code = False + file_path = ( + "account_edi_ubl_cii_purchase_match_product_packaging/tests/test_files/" + "bis3_bill_example_uom_dozen.xml" + ) + bill = self._import_invoice( + self.company_data["default_journal_purchase"], file_path + ) + inv_line = bill.invoice_line_ids + self.assertEqual(inv_line.ubl_unece_unit_code, "DPC") + self.assertEqual(inv_line.product_uom_id, self.uom_unit) + action = inv_line.action_select_purchase_line() + wizard = ( + self.env[action.get("res_model")] + .with_context(**action.get("context")) + .create({}) + ) + self.assertEqual(wizard.move_line_id, inv_line) + self.assertEqual(wizard.purchase_order_ids, self.purchase_order) + wizard.product_id = self.product + wizard.select_purchase_line() + self.assertEqual(inv_line.purchase_line_id, self.po_line) + self.uom_dozen.unece_code = "DPC" + wizard.select_purchase_line() + self.assertEqual(inv_line.product_uom_id, self.uom_dozen) + self.assertAlmostEqual(inv_line.ubl_price_unit, 657.0) + self.assertAlmostEqual(inv_line.price_unit, 657.0) + + def test_2(self): + """ + check that packaging is correctly applied during reconciliation when available + """ + file_path = ( + "account_edi_ubl_cii_purchase_match_product_packaging/tests/test_files/" + "bis3_bill_example_packaging_pallet.xml" + ) + bill = self._import_invoice( + self.company_data["default_journal_purchase"], file_path + ) + inv_line = bill.invoice_line_ids + self.assertEqual(inv_line.ubl_unece_unit_code, "X8A") + self.assertEqual(inv_line.quantity, 1) + self.assertEqual(inv_line.product_uom_id, self.uom_unit) + self.assertFalse(inv_line.product_packaging_qty) + self.assertFalse(inv_line.product_packaging_id) + action = inv_line.action_select_purchase_line() + wizard = ( + self.env[action.get("res_model")] + .with_context(**action.get("context")) + .create({}) + ) + self.assertEqual(wizard.move_line_id, inv_line) + self.assertEqual(wizard.purchase_order_ids, self.purchase_order) + wizard.product_id = self.product + wizard.select_purchase_line() + self.assertEqual(inv_line.purchase_line_id, self.po_line) + packaging = self.env["product.packaging"].create( + { + "name": "pallet 72", + "product_id": self.product.id, + "qty": 72.0, + "packaging_level_id": self.packaging_level.id, + } + ) + wizard.select_purchase_line() + self.assertEqual(inv_line.quantity, 72) + self.assertEqual(inv_line.product_uom_id, self.uom_unit) + self.assertEqual(inv_line.product_packaging_qty, 1) + self.assertEqual(inv_line.product_packaging_id, packaging) + self.assertAlmostEqual(inv_line.ubl_price_unit, 657.0) + self.assertAlmostEqual(inv_line.price_unit, 9.13) + return inv_line + + def test_3(self): + inv_line = self.test_2() + packaging = inv_line.product_packaging_id + packaging.qty = 100 + action = inv_line.action_select_purchase_line() + wizard = ( + self.env[action.get("res_model")] + .with_context(**action.get("context")) + .create({}) + ) + wizard.select_purchase_line() + self.assertEqual(inv_line.quantity, 100) + self.assertEqual(inv_line.product_uom_id, self.uom_unit) + self.assertEqual(inv_line.product_packaging_qty, 1) + self.assertEqual(inv_line.product_packaging_id, packaging) + self.assertAlmostEqual(inv_line.ubl_price_unit, 657.0) + self.assertAlmostEqual(inv_line.price_unit, 6.57) diff --git a/account_edi_ubl_cii_purchase_match_product_packaging/tests/test_files/bis3_bill_example_packaging_pallet.xml b/account_edi_ubl_cii_purchase_match_product_packaging/tests/test_files/bis3_bill_example_packaging_pallet.xml new file mode 100644 index 0000000000..fbb6da05d8 --- /dev/null +++ b/account_edi_ubl_cii_purchase_match_product_packaging/tests/test_files/bis3_bill_example_packaging_pallet.xml @@ -0,0 +1,144 @@ + + + urn:cen.eu:en16931:2017#compliant#urn:fdc:peppol.eu:2017:poacc:billing:3.0 + urn:fdc:peppol.eu:2017:poacc:billing:01:1.0 + PO00101 + 2023-08-04 + 2023-09-04 + 380 + EUR + + PO00101 + S00012 + + + FAC_2023_00052.pdf + + + + + + LU25587702 + + ALD Automotive LU + + + 270 rte d'Arlon + Strassen + 8010 + + LU + + + + LU12977109 + + VAT + + + + ALD Automotive LU + LU12977109 + + + ALD Automotive LU + adl@test.com + + + + + + LU25587702 + + Odoo Lu + + + Rue de l'industrie 13 + Windhof + + LU + + + + LU25587702 + + VAT + + + + Odoo Lu + LU25587702 + + + Odoo Lu + odoo@test.com + + + + + + + Rue de l'industrie 13 + Windhof + + LU + + + + + + 30 + PO00101 + + LU071241358706500000 + + + + 105.12 + + 657.00 + 105.12 + + S + 16.0 + + VAT + + + + + + 657.00 + 657.00 + 762.12 + 0.00 + 762.12 + + + 1 + 1.0 + 657.00 + + Locations and leasing + Locations and leasing + + leasing001 + + + S + 16.0 + + VAT + + + + + 657.00 + + + diff --git a/account_edi_ubl_cii_purchase_match_product_packaging/tests/test_files/bis3_bill_example_uom_dozen.xml b/account_edi_ubl_cii_purchase_match_product_packaging/tests/test_files/bis3_bill_example_uom_dozen.xml new file mode 100644 index 0000000000..6f50857660 --- /dev/null +++ b/account_edi_ubl_cii_purchase_match_product_packaging/tests/test_files/bis3_bill_example_uom_dozen.xml @@ -0,0 +1,144 @@ + + + urn:cen.eu:en16931:2017#compliant#urn:fdc:peppol.eu:2017:poacc:billing:3.0 + urn:fdc:peppol.eu:2017:poacc:billing:01:1.0 + PO00101 + 2023-08-04 + 2023-09-04 + 380 + EUR + + PO00101 + S00012 + + + FAC_2023_00052.pdf + + + + + + LU25587702 + + ALD Automotive LU + + + 270 rte d'Arlon + Strassen + 8010 + + LU + + + + LU12977109 + + VAT + + + + ALD Automotive LU + LU12977109 + + + ALD Automotive LU + adl@test.com + + + + + + LU25587702 + + Odoo Lu + + + Rue de l'industrie 13 + Windhof + + LU + + + + LU25587702 + + VAT + + + + Odoo Lu + LU25587702 + + + Odoo Lu + odoo@test.com + + + + + + + Rue de l'industrie 13 + Windhof + + LU + + + + + + 30 + PO00101 + + LU071241358706500000 + + + + 105.12 + + 657.00 + 105.12 + + S + 16.0 + + VAT + + + + + + 657.00 + 657.00 + 762.12 + 0.00 + 762.12 + + + 1 + 1.0 + 657.00 + + Locations and leasing + Locations and leasing + + leasing001 + + + S + 16.0 + + VAT + + + + + 657.00 + + + diff --git a/setup/account_edi_ubl_cii_purchase_match_product_packaging/odoo/addons/account_edi_ubl_cii_purchase_match_product_packaging b/setup/account_edi_ubl_cii_purchase_match_product_packaging/odoo/addons/account_edi_ubl_cii_purchase_match_product_packaging new file mode 120000 index 0000000000..f3e4e2b93c --- /dev/null +++ b/setup/account_edi_ubl_cii_purchase_match_product_packaging/odoo/addons/account_edi_ubl_cii_purchase_match_product_packaging @@ -0,0 +1 @@ +../../../../account_edi_ubl_cii_purchase_match_product_packaging \ No newline at end of file diff --git a/setup/account_edi_ubl_cii_purchase_match_product_packaging/setup.py b/setup/account_edi_ubl_cii_purchase_match_product_packaging/setup.py new file mode 100644 index 0000000000..28c57bb640 --- /dev/null +++ b/setup/account_edi_ubl_cii_purchase_match_product_packaging/setup.py @@ -0,0 +1,6 @@ +import setuptools + +setuptools.setup( + setup_requires=['setuptools-odoo'], + odoo_addon=True, +) diff --git a/test-requirements.txt b/test-requirements.txt index 0ffc43b83b..d88aba4ae0 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -1 +1,6 @@ freezegun==1.2.2 +odoo-addon-account-edi-ubl-cii-purchase-match @ git+https://github.com/OCA/edi.git@refs/pull/1267/head#subdirectory=setup/account_edi_ubl_cii_purchase_match +odoo-addon-account_edi_ubl_move_line_uom_and_packaging_unece@ git+https://github.com/OCA/edi.git@refs/pull/1311/head#subdirectory=setup/account_edi_ubl_move_line_uom_and_packaging_unece +odoo-addon-account_move_line_packaging @ git+https://github.com/OCA/account-invoicing.git@refs/pull/2297/head#subdirectory=setup/account_move_line_packaging +odoo-addon-uom_unece @ git+https://github.com/oca/community-data-files@refs/pull/270/head#subdirectory=setup/uom_unece +odoo-addon-product_packaging_unece @ git+https://github.com/oca/community-data-files@refs/pull/271/head#subdirectory=setup/product_packaging_unece