From d22b517838d1a75976c4ac8cfabc041b657ae54b Mon Sep 17 00:00:00 2001 From: "Benjamin Frantzen (bfr)" Date: Tue, 11 May 2021 13:34:26 +0000 Subject: [PATCH] [ADD] l10n_no_edi: added support for EHF3 EHF3 is the Norvegian standard for Peppol. This module generates xml (based on BIS3 with NO country profile) to be submitted on the Peppol network. TASK ID: 2528203 closes odoo/odoo#70679 Related: odoo/enterprise#20050 Signed-off-by: Laurent Smet --- .../models/account_edi_format.py | 6 + .../l10n_nl_edi/models/account_edi_format.py | 2 +- addons/l10n_no/__manifest__.py | 2 + addons/l10n_no/demo/demo_company.xml | 1 + addons/l10n_no/models/__init__.py | 2 + addons/l10n_no/models/res_company.py | 8 + addons/l10n_no/models/res_partner.py | 8 + addons/l10n_no/views/res_company_views.xml | 15 + addons/l10n_no/views/res_partner_views.xml | 13 + addons/l10n_no_edi/__init__.py | 4 + addons/l10n_no_edi/__manifest__.py | 19 + addons/l10n_no_edi/data/account_edi_data.xml | 11 + addons/l10n_no_edi/data/ehf_3_template.xml | 29 ++ addons/l10n_no_edi/models/__init__.py | 3 + .../l10n_no_edi/models/account_edi_format.py | 116 +++++ addons/l10n_no_edi/test_xml_file/ehf_test.xml | 457 ++++++++++++++++++ addons/l10n_no_edi/tests/__init__.py | 3 + addons/l10n_no_edi/tests/test_ehf.py | 224 +++++++++ 18 files changed, 922 insertions(+), 1 deletion(-) create mode 100644 addons/l10n_no/models/res_company.py create mode 100644 addons/l10n_no/models/res_partner.py create mode 100644 addons/l10n_no/views/res_company_views.xml create mode 100644 addons/l10n_no/views/res_partner_views.xml create mode 100644 addons/l10n_no_edi/__init__.py create mode 100644 addons/l10n_no_edi/__manifest__.py create mode 100644 addons/l10n_no_edi/data/account_edi_data.xml create mode 100644 addons/l10n_no_edi/data/ehf_3_template.xml create mode 100644 addons/l10n_no_edi/models/__init__.py create mode 100644 addons/l10n_no_edi/models/account_edi_format.py create mode 100644 addons/l10n_no_edi/test_xml_file/ehf_test.xml create mode 100644 addons/l10n_no_edi/tests/__init__.py create mode 100644 addons/l10n_no_edi/tests/test_ehf.py diff --git a/addons/account_edi_ubl_bis3/models/account_edi_format.py b/addons/account_edi_ubl_bis3/models/account_edi_format.py index 0f9bcd10289..abfbbb43b6b 100644 --- a/addons/account_edi_ubl_bis3/models/account_edi_format.py +++ b/addons/account_edi_ubl_bis3/models/account_edi_format.py @@ -106,6 +106,12 @@ class AccountEdiFormat(models.Model): # Import #################################################### + def _get_bis3_namespaces(self): + return { + 'cac': 'urn:oasis:names:specification:ubl:schema:xsd:CommonAggregateComponents-2', + 'cbc': 'urn:oasis:names:specification:ubl:schema:xsd:CommonBasicComponents-2', + } + def _bis3_get_extra_partner_domains(self, tree): """ Returns an additional domain to find the partner of the invoice based on specific implementation of BIS3. TO OVERRIDE diff --git a/addons/l10n_nl_edi/models/account_edi_format.py b/addons/l10n_nl_edi/models/account_edi_format.py index 23d11b8329b..b69be42177e 100644 --- a/addons/l10n_nl_edi/models/account_edi_format.py +++ b/addons/l10n_nl_edi/models/account_edi_format.py @@ -35,7 +35,7 @@ class AccountEdiFormat(models.Model): return [('l10n_nl_kvk', '=', endpoint.text)] elif scheme == '0190' and endpoint.text: return [('l10n_nl_oin', '=', endpoint.text)] - return [] + return super()._bis3_get_extra_partner_domains(tree) #################################################### # Export diff --git a/addons/l10n_no/__manifest__.py b/addons/l10n_no/__manifest__.py index a7fbb0d9a49..e969c919358 100644 --- a/addons/l10n_no/__manifest__.py +++ b/addons/l10n_no/__manifest__.py @@ -21,6 +21,8 @@ Updated for Odoo 9 by Bringsvor Consulting AS 'data/account.account.template.csv', 'data/account_tax_data.xml', 'data/account_chart_template_data.xml', + 'views/res_partner_views.xml', + 'views/res_company_views.xml', ], 'demo': [ 'demo/demo_company.xml', diff --git a/addons/l10n_no/demo/demo_company.xml b/addons/l10n_no/demo/demo_company.xml index a113f1810e7..7a5d5c11b47 100644 --- a/addons/l10n_no/demo/demo_company.xml +++ b/addons/l10n_no/demo/demo_company.xml @@ -11,6 +11,7 @@ +47 406 12 345 info@company.noexample.com www.noexample.com + 123456785 diff --git a/addons/l10n_no/models/__init__.py b/addons/l10n_no/models/__init__.py index d2409f022e4..c208e4ca091 100644 --- a/addons/l10n_no/models/__init__.py +++ b/addons/l10n_no/models/__init__.py @@ -3,3 +3,5 @@ from . import account_journal from . import account_move +from . import res_partner +from . import res_company diff --git a/addons/l10n_no/models/res_company.py b/addons/l10n_no/models/res_company.py new file mode 100644 index 00000000000..2a4cb99cf58 --- /dev/null +++ b/addons/l10n_no/models/res_company.py @@ -0,0 +1,8 @@ +# coding: utf-8 +from odoo import fields, models + + +class ResCompany(models.Model): + _inherit = 'res.company' + + l10n_no_bronnoysund_number = fields.Char(related='partner_id.l10n_no_bronnoysund_number', readonly=False) diff --git a/addons/l10n_no/models/res_partner.py b/addons/l10n_no/models/res_partner.py new file mode 100644 index 00000000000..c1f1b1c0553 --- /dev/null +++ b/addons/l10n_no/models/res_partner.py @@ -0,0 +1,8 @@ +# coding: utf-8 +from odoo import fields, models + + +class ResPartner(models.Model): + _inherit = 'res.partner' + + l10n_no_bronnoysund_number = fields.Char(string='Register of Legal Entities (Brønnøysund Register Center)', size=9) diff --git a/addons/l10n_no/views/res_company_views.xml b/addons/l10n_no/views/res_company_views.xml new file mode 100644 index 00000000000..47405ed6fca --- /dev/null +++ b/addons/l10n_no/views/res_company_views.xml @@ -0,0 +1,15 @@ + + + + + res.company.form.inherit.l10n.no + res.company + + + + + + + + + diff --git a/addons/l10n_no/views/res_partner_views.xml b/addons/l10n_no/views/res_partner_views.xml new file mode 100644 index 00000000000..a05e65ca759 --- /dev/null +++ b/addons/l10n_no/views/res_partner_views.xml @@ -0,0 +1,13 @@ + + + + res.partner.form.inherit.l10n_no + res.partner + + + + + + + + diff --git a/addons/l10n_no_edi/__init__.py b/addons/l10n_no_edi/__init__.py new file mode 100644 index 00000000000..dc5e6b693d1 --- /dev/null +++ b/addons/l10n_no_edi/__init__.py @@ -0,0 +1,4 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from . import models diff --git a/addons/l10n_no_edi/__manifest__.py b/addons/l10n_no_edi/__manifest__.py new file mode 100644 index 00000000000..27c753d675b --- /dev/null +++ b/addons/l10n_no_edi/__manifest__.py @@ -0,0 +1,19 @@ +# -*- coding: utf-8 -*- +{ + 'name': 'Norway - E-Invoicing (EHF 3)', + 'icon': '/l10n_no/static/description/icon.png', + 'version': '0.1', + 'category': 'Accounting/Localizations/EDI', + 'summary': 'E-Invoicing, Universal Business Language (EHF 3)', + 'description': """ +EHF 3 is the Norvegian implementation of EN 16931 norm. + """, + 'depends': ['l10n_no', 'account_edi_ubl_bis3'], + 'data': [ + 'data/account_edi_data.xml', + 'data/ehf_3_template.xml', + ], + 'installable': True, + 'auto_install': True, + 'license': 'LGPL-3', +} diff --git a/addons/l10n_no_edi/data/account_edi_data.xml b/addons/l10n_no_edi/data/account_edi_data.xml new file mode 100644 index 00000000000..c5dce88c8d2 --- /dev/null +++ b/addons/l10n_no_edi/data/account_edi_data.xml @@ -0,0 +1,11 @@ + + + + + + EHF 3 (Norway) + ehf_3 + + + + diff --git a/addons/l10n_no_edi/data/ehf_3_template.xml b/addons/l10n_no_edi/data/ehf_3_template.xml new file mode 100644 index 00000000000..21d59135bd4 --- /dev/null +++ b/addons/l10n_no_edi/data/ehf_3_template.xml @@ -0,0 +1,29 @@ + + + + + + + + + diff --git a/addons/l10n_no_edi/models/__init__.py b/addons/l10n_no_edi/models/__init__.py new file mode 100644 index 00000000000..80dd2914f7b --- /dev/null +++ b/addons/l10n_no_edi/models/__init__.py @@ -0,0 +1,3 @@ +# -*- encoding: utf-8 -*- + +from . import account_edi_format diff --git a/addons/l10n_no_edi/models/account_edi_format.py b/addons/l10n_no_edi/models/account_edi_format.py new file mode 100644 index 00000000000..62fa00a0629 --- /dev/null +++ b/addons/l10n_no_edi/models/account_edi_format.py @@ -0,0 +1,116 @@ +# -*- coding: utf-8 -*- + +from odoo import models, _ +from odoo.addons.account_edi_ubl_bis3.models.account_edi_format import COUNTRY_EAS + + +class AccountEdiFormat(models.Model): + _inherit = 'account.edi.format' + + #################################################### + # Import + #################################################### + + def _is_ubl(self, filename, tree): + """ OVERRIDE so that the generic ubl parser does not parse BIS3 any longer. + """ + is_ubl = super()._is_ubl(filename, tree) + return is_ubl and not self._is_ehf_3(filename, tree) + + def _is_ehf_3(self, filename, tree): + ns = self._get_bis3_namespaces() + return tree.tag == '{urn:oasis:names:specification:ubl:schema:xsd:Invoice-2}Invoice' \ + and 'peppol' in tree.findtext('./cbc:ProfileID', '', namespaces=ns) \ + and tree.xpath( + "./cac:AccountingSupplierParty/cac:Party/cac:PartyTaxScheme/cbc:CompanyID[text()='Foretaksregisteret']", + namespaces=ns) is not None + + def _bis3_get_extra_partner_domains(self, tree): + if self.code == 'ehf_3': + ns = self._get_bis3_namespaces() + bronnoysund = tree.xpath('./cac:AccountingSupplierParty/cac:Party/cbc:EndpointID[@schemeID="0192"]/text()', namespaces=ns) + if bronnoysund: + return [('l10n_no_bronnoysund_number', '=', bronnoysund[0])] + return super()._bis3_get_extra_partner_domains(tree) + + #################################################### + # Export + #################################################### + + def _get_ehf_3_values(self, invoice): + values = super()._get_bis3_values(invoice) + for partner_vals in (values['customer_vals'], values['supplier_vals']): + partner = partner_vals['partner'] + if partner.country_code == 'NO': + partner_vals.update( + bis3_endpoint=partner.l10n_no_bronnoysund_number, + bis3_endpoint_scheme='0192', + ) + + return values + + def _export_ehf_3(self, invoice): + self.ensure_one() + # Create file content. + xml_content = self.env.ref('l10n_no_edi.export_ehf_3_invoice')._render(self._get_ehf_3_values(invoice)) + vat = invoice.company_id.partner_id.commercial_partner_id.vat + xml_name = 'ehf-%s%s%s.xml' % (vat or '', '-' if vat else '', invoice.name.replace('/', '_')) + return self.env['ir.attachment'].create({ + 'name': xml_name, + 'raw': xml_content.encode(), + 'res_model': 'account.move', + 'res_id': invoice.id, + 'mimetype': 'application/xml' + }) + + #################################################### + # Account.edi.format override + #################################################### + + def _check_move_configuration(self, invoice): + errors = super()._check_move_configuration(invoice) + if self.code != 'ehf_3': + return errors + + supplier = invoice.company_id.partner_id.commercial_partner_id + if supplier.country_code == 'NO' and not supplier.l10n_no_bronnoysund_number: + errors.append(_("The supplier %r must have a Bronnoysund company registry.", supplier.display_name)) + + if supplier.country_code != 'NO' and supplier.country_code not in COUNTRY_EAS: + errors.append(_("The supplier %r is from a country that is not supported for EHF (Bis3)", supplier.display_name)) + + customer = invoice.commercial_partner_id + if customer.country_code == 'NO' and not customer.l10n_no_bronnoysund_number: + errors.append(_("The customer %r must have a Bronnoysund company registry.", customer.display_name)) + + if customer.country_code != 'NO' and customer.country_code not in COUNTRY_EAS: + errors.append(_("The customer %r is from a country that is not supported for EHF (Bis3)", customer.display_name)) + + return errors + + def _is_compatible_with_journal(self, journal): + self.ensure_one() + if self.code != 'ehf_3': + return super()._is_compatible_with_journal(journal) + return journal.type == 'sale' and journal.country_code == 'NO' + + def _post_invoice_edi(self, invoices): + self.ensure_one() + if self.code != 'ehf_3': + return super()._post_invoice_edi(invoices) + + invoice = invoices # no batch ensure that there is only one invoice + attachment = self._export_ehf_3(invoice) + return {invoice: {'attachment': attachment}} + + def _create_invoice_from_xml_tree(self, filename, tree): + self.ensure_one() + if self.code == 'ehf_3' and self._is_ehf_3(filename, tree): + return self._decode_bis3(tree, self.env['account.move']) + return super()._create_invoice_from_xml_tree(filename, tree) + + def _update_invoice_from_xml_tree(self, filename, tree, invoice): + self.ensure_one() + if self.code == 'ehf_3' and self._is_ehf_3(filename, tree): + return self._decode_bis3(tree, invoice) + return super()._update_invoice_from_xml_tree(filename, tree, invoice) diff --git a/addons/l10n_no_edi/test_xml_file/ehf_test.xml b/addons/l10n_no_edi/test_xml_file/ehf_test.xml new file mode 100644 index 00000000000..1ceb06102ff --- /dev/null +++ b/addons/l10n_no_edi/test_xml_file/ehf_test.xml @@ -0,0 +1,457 @@ + + + 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 + TOSL108 + 2013-06-30 + 2013-07-20 + 380 + Ordered in our booth at the convention. + 2013-06-30 + NOK + Project cost code 123 + 3150bdn + + 2013-06-01 + 2013-06-30 + + + 123 + + + Contract321 + + + Doc1 + Timesheet + + + http://www.suppliersite.eu/sheet001.html + + + + + Doc2 + EHF specification + + VGVzdCBiYXNlIDY0IGVuY29kaW5n + + + + + 864234232 + + 1238764941386 + + + partner_a + + + Main street 34 + Suite 123 + Big city + 54321 + RegionA + + NO + + + + NO864234232MVA + + VAT + + + + Foretaksregisteret + + TAX + + + + The Sellercompany ASA + 864234232 + + + Antonio Salesmacher + 46211230 + antonio@salescompany.no + + + + + + 987654325 + + 3456789012098 + + + The Buyercompany + + + Anystreet 8 + Back door + Anytown + 101 + RegionB + + NO + + + + NO987654325MVA + + VAT + + + + Buyercompany ASA + 987654325 + + + John Doe + 5121230 + john@buyercompany.no + + + + + + 2298740918237 + + + Ebeneser Scrooge AS + + + 999999999 + + + + + Tax handling company AS + + + Regent street + Front door + Newtown + 101 + RegionC + + NO + + + + NO999999999MVA + + VAT + + + + + 2013-06-15 + + 6754238987643 + + + + 31 + 0003434323213231 + + 86011117947 + + DNBANOKK + + + + + 2 % discount if paid within 2 days. Penalty percentage 10% from due date + + + true + FC + Freight + 100 + + S + 25 + + VAT + + + + + false + 95 + Promotion discount + 100 + + S + 25 + + VAT + + + + + 365.28 + + 1460.5 + 365.13 + + S + 25 + + VAT + + + + + 1 + 0.15 + + S + 15 + + VAT + + + + + -25 + 0 + + E + 0 + Exempt New Means of Transport + + VAT + + + + + + 1436.5 + 1436.5 + 1801.78 + 100 + 100 + 1000 + 0.22 + 802.00 + + + 1 + Scratch on box + 1 + 1273 + BookingCode001 + + 2013-06-01 + 2013-06-30 + + + 1 + + + false + Damage + 12 + + + true + Testing + 12 + + + Processor: Intel Core 2 Duo SU9400 LV (1.4GHz). RAM: 3MB. Screen 1440x900 + Laptop computer + + JB007 + + + 1234567890124 + + + DE + + + 12344321 + + + 65434568 + + + S + 25 + + VAT + + + + Color + Black + + + + 1273 + 1 + + false + 227 + 1500 + + + + + 2 + Cover is slightly damaged. + -1 + -3.96 + BookingCode002 + + 5 + + + Returned "Advanced computing" book + + JB008 + + + 1234567890125 + + + 32344324 + + + 65434567 + + + S + 15 + + VAT + + + + + 3.96 + 1 + + + + 3 + 2 + 4.96 + BookingCode003 + + 3 + + + "Computing for dummies" book + + JB009 + + + 1234567890126 + + + 32344324 + + + 65434566 + + + S + 15 + + VAT + + + + + 2.48 + 1 + + false + 0.27 + 2.75 + + + + + 4 + -1 + -25 + BookingCode004 + + 2 + + + Returned IBM 5150 desktop + + JB010 + + + 1234567890127 + + + 12344322 + + + 65434565 + + + E + 0 + + VAT + + + + + 25 + 1 + + + + 5 + 250 + 187.5 + BookingCode005 + + 4 + + + Network cable + + JB011 + + + 1234567890128 + + + 12344325 + + + 65434564 + + + S + 25 + + VAT + + + + Type + Cat5 + + + + 0.75 + 1 + + + \ No newline at end of file diff --git a/addons/l10n_no_edi/tests/__init__.py b/addons/l10n_no_edi/tests/__init__.py new file mode 100644 index 00000000000..c7a14a20a32 --- /dev/null +++ b/addons/l10n_no_edi/tests/__init__.py @@ -0,0 +1,3 @@ +# -*- coding: utf-8 -*- + +from . import test_ehf diff --git a/addons/l10n_no_edi/tests/test_ehf.py b/addons/l10n_no_edi/tests/test_ehf.py new file mode 100644 index 00000000000..d4a2afe6951 --- /dev/null +++ b/addons/l10n_no_edi/tests/test_ehf.py @@ -0,0 +1,224 @@ +# -*- coding: utf-8 -*- +from odoo.addons.account_edi.tests.common import AccountEdiTestCommon +from odoo.tests import tagged + +from freezegun import freeze_time + + +@tagged('post_install_l10n', 'post_install', '-at_install') +class TestUBL(AccountEdiTestCommon): + + @classmethod + def setUpClass(cls, chart_template_ref='l10n_no.no_chart_template', edi_format_ref='l10n_no_edi.edi_ehf_3'): + super().setUpClass(chart_template_ref=chart_template_ref, edi_format_ref=edi_format_ref) + + cls.company_data['company'].partner_id.write({ + 'street': 'Archefstraat 42', + 'zip': '1000', + 'city': 'Amsterdam', + 'country_id': cls.env.ref('base.no').id, + 'l10n_no_bronnoysund_number': '987654325', + 'vat': 'NO987654325MVA', + }) + + cls.partner_a.write({ + 'l10n_no_bronnoysund_number': '864234232', + 'country_id': cls.env.ref('base.no').id, + 'vat': 'NO864234232MVA', + }) + + bank_account = cls.env['res.partner.bank'].create({ + 'acc_number': '86011117947', + 'partner_id': cls.partner_a.id, + }) + + cls.tax_sale_b.write({ + 'amount': 15 + }) + + cls.invoice = cls.env['account.move'].create({ + 'partner_id': cls.partner_a.id, + 'move_type': 'out_invoice', + 'partner_bank_id': bank_account.id, + 'invoice_date_due': '2020-12-16', + 'invoice_line_ids': [ + (0, 0, { + 'product_id': cls.product_a.id, + 'quantity': 150, + 'price_unit': 250, + 'discount': 10, + 'tax_ids': [(6, 0, cls.tax_sale_a.ids)], + }), + (0, 0, { + 'product_id': cls.product_b.id, + 'quantity': 12, + 'price_unit': 100, + 'tax_ids': [(6, 0, cls.tax_sale_b.ids)], + }), + ] + }) + + cls.expected_invoice_values = ''' + + 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 + INV/2020/00001 + 2020-12-16 + 2020-12-16 + 380 + NOK + partner_a + + + 987654325 + + company_1_data + + + Archefstraat 42 + Amsterdam + 1000 + + NO + + + + NO987654325MVA + + VAT + + + + Foretaksregisteret + + TAX + + + + company_1_data + + + company_1_data + + + + + + 864234232 + + partner_a + + + + NO + + + + NO864234232MVA + + VAT + + + + partner_a + + + partner_a + + + + + 31 + + 86011117947 + + + + 8617.50 + + 33750.00 + 8437.50 + + S + 25.0 + + VAT + + + + 1200.00 + 180.00 + + S + 15.0 + + VAT + + + + + + 34950.00 + 34950.00 + 43567.50 + 0.00 + 43567.50 + + + 1 + Discount (10.0 %) + 150.0 + 33750.00 + + product_a + product_a + + S + 25.0 + + VAT + + + + + 225.00 + 150.0 + + + + 2 + 12.0 + 1200.00 + + product_b + product_b + + S + 15.0 + + VAT + + + + + 100.00 + 12.0 + + + + ''' + + def test_ehf_import(self): + invoice = self.env['account.move'].with_context(default_move_type='in_invoice').create({}) + invoice_count = self.env['account.move'].search_count([]) + self.update_invoice_from_file('l10n_no_edi', 'test_xml_file', 'ehf_test.xml', invoice) + self.assertEqual(self.env['account.move'].search_count([]), invoice_count) + self.assertRecordValues(invoice, [{ + 'partner_id': self.partner_a.id, + 'amount_total': 1801.78, + 'amount_tax': 365.28, + }]) + + @freeze_time('2020-12-16') + def test_ehf_export(self): + self.assert_generated_file_equal(self.invoice, self.expected_invoice_values)