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)