From 342c88e5ef3b8aadaa3a0bf96353ddf5211c3946 Mon Sep 17 00:00:00 2001 From: Brice bib Bartoletti Date: Fri, 15 Sep 2023 15:47:43 +0200 Subject: [PATCH] [ADD] {l10n_}account_edi_ubl_cii{_tests}: OIOUBL export MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The aim of this commit is to support OIOUBL format export in order to comply with the Danish accountancy act. Before this commit: OIOUBL isn't a supported format in Odoo After this commit: It is possible to generate oioubl document for invoice and credit-note Current limitation: - Payment Terms: will be left empty - Payment Means: will always be 'unknown' Documentation: oioubl official website: https://oioubl.info/classes/en/index.html List of documentation: https://oioubl.info/documents/en/documentoverview.html OIOUBL Schematron validator: https://rep.erst.dk/git/openebusiness/common/-/tree/master/resources/Schematrons/OIOUBL Downloadable ressources: https://rep.erst.dk/git/openebusiness/common/-/tree/master/resources Validation: https://www.oioubl.info/validator/default.aspx https://ecosio.com/en/peppol-and-xml-document-validator/ closes odoo/odoo#136072 Task-id: 3326977 Signed-off-by: Laurent Smet (las) Signed-off-by: Brice Bartoletti (bib) --- addons/l10n_dk_oioubl/__init__.py | 1 + addons/l10n_dk_oioubl/__manifest__.py | 20 ++ addons/l10n_dk_oioubl/models/__init__.py | 2 + .../models/account_edi_xml_oioubl_201.py | 263 +++++++++++++++++ addons/l10n_dk_oioubl/models/res_partner.py | 19 ++ addons/l10n_dk_oioubl/tests/__init__.py | 1 + .../oioubl_out_invoice_foreign_partner_be.xml | 272 ++++++++++++++++++ .../oioubl_out_invoice_foreign_partner_fr.xml | 271 +++++++++++++++++ .../oioubl_out_invoice_partner_dk.xml | 262 +++++++++++++++++ .../oioubl_out_refund_foreign_partner_fr.xml | 261 +++++++++++++++++ .../oioubl_out_refund_partner_dk.xml | 252 ++++++++++++++++ .../tests/test_xml_oioubl_dk.py | 168 +++++++++++ 12 files changed, 1792 insertions(+) create mode 100644 addons/l10n_dk_oioubl/__init__.py create mode 100644 addons/l10n_dk_oioubl/__manifest__.py create mode 100644 addons/l10n_dk_oioubl/models/__init__.py create mode 100644 addons/l10n_dk_oioubl/models/account_edi_xml_oioubl_201.py create mode 100644 addons/l10n_dk_oioubl/models/res_partner.py create mode 100644 addons/l10n_dk_oioubl/tests/__init__.py create mode 100644 addons/l10n_dk_oioubl/tests/test_files/from_odoo/oioubl_out_invoice_foreign_partner_be.xml create mode 100644 addons/l10n_dk_oioubl/tests/test_files/from_odoo/oioubl_out_invoice_foreign_partner_fr.xml create mode 100644 addons/l10n_dk_oioubl/tests/test_files/from_odoo/oioubl_out_invoice_partner_dk.xml create mode 100644 addons/l10n_dk_oioubl/tests/test_files/from_odoo/oioubl_out_refund_foreign_partner_fr.xml create mode 100644 addons/l10n_dk_oioubl/tests/test_files/from_odoo/oioubl_out_refund_partner_dk.xml create mode 100644 addons/l10n_dk_oioubl/tests/test_xml_oioubl_dk.py diff --git a/addons/l10n_dk_oioubl/__init__.py b/addons/l10n_dk_oioubl/__init__.py new file mode 100644 index 00000000000..0650744f6bc --- /dev/null +++ b/addons/l10n_dk_oioubl/__init__.py @@ -0,0 +1 @@ +from . import models diff --git a/addons/l10n_dk_oioubl/__manifest__.py b/addons/l10n_dk_oioubl/__manifest__.py new file mode 100644 index 00000000000..e16ed171011 --- /dev/null +++ b/addons/l10n_dk_oioubl/__manifest__.py @@ -0,0 +1,20 @@ +{ + 'author': 'Odoo', + 'name': 'Denmark - E-invoicing', + 'version': '0.1', + 'category': 'Accounting/Localizations/EDI', + 'description': """ + E-invoice implementation for the Denmark + """, + 'summary': """ + E-Invoicing, Offentlig Information Online Universal Business Language + """, + 'countries': ['dk'], + 'depends': [ + 'account_edi_ubl_cii', + 'l10n_dk', + ], + 'installable': True, + 'auto_install': True, + 'license': 'LGPL-3', +} diff --git a/addons/l10n_dk_oioubl/models/__init__.py b/addons/l10n_dk_oioubl/models/__init__.py new file mode 100644 index 00000000000..bbe8039cd64 --- /dev/null +++ b/addons/l10n_dk_oioubl/models/__init__.py @@ -0,0 +1,2 @@ +from . import account_edi_xml_oioubl_201 +from . import res_partner diff --git a/addons/l10n_dk_oioubl/models/account_edi_xml_oioubl_201.py b/addons/l10n_dk_oioubl/models/account_edi_xml_oioubl_201.py new file mode 100644 index 00000000000..9ab2f33072f --- /dev/null +++ b/addons/l10n_dk_oioubl/models/account_edi_xml_oioubl_201.py @@ -0,0 +1,263 @@ +from odoo import _, models, tools + +DANISH_NATIONAL_IT_AND_TELECOM_AGENCY_ID = '320' + +PAYMENT_MEANS_CODE = { + # https://www.oioubl.info/codelists/en/urn_oioubl_codelist_paymentmeanscode-1.1.html + 'unknown': 1, + 'cash': 10, + 'cheque': 20, + 'debit': 31, + 'bank': 42, + 'card': 48, # credit card + 'direct debit': 49, + 'compensation': 97, +} +UBL_TO_OIOUBL_TAX_CATEGORY_ID_MAPPING = { + # Simple mapping between tax type provided in UBL and what is accepted in OIOUBL + # https://docs.peppol.eu/poacc/billing/3.0/codelist/UNCL5305/ + # https://www.oioubl.info/codelists/en/urn_oioubl_id_taxcategoryid-1.1.html + 'AE': 'ReverseCharge', + 'E': 'ZeroRated', + 'S': 'StandardRated', + 'Z': 'ZeroRated', + 'G': 'ZeroRated', + 'O': 'ZeroRated', + 'K': 'ReverseCharge', + 'L': 'ZeroRated', + 'M': 'ZeroRated', +} +TAX_POSSIBLE_VALUES = set(UBL_TO_OIOUBL_TAX_CATEGORY_ID_MAPPING.values()) + + +def format_vat_number(partner): + vat = partner.vat.replace(' ', '') + if vat[:2].isnumeric(): + vat = partner.country_code.upper() + vat + return vat + + +class AccountEdiXmlOIOUBL201(models.AbstractModel): + _name = "account.edi.xml.oioubl_201" + _inherit = 'account.edi.xml.ubl_20' + _description = "OIOUBL 2.01" + + # Data validation Schematron available at the following URL: + # https://rep.erst.dk/git/openebusiness/common/-/tree/master/resources/Schematrons/OIOUBL + + # ------------------------------------------------------------------------- + # EXPORT + # ------------------------------------------------------------------------- + + def _export_invoice_filename(self, invoice): + return f"{invoice.name.replace('/', '_')}_oioubl_201.xml" + + def _export_invoice_ecosio_schematrons(self): + return { + 'invoice': 'org.oasis-open:invoice:2.0', + 'credit_note': 'org.oasis-open:creditnote:2.0', + } + + def _export_invoice_vals(self, invoice): + # EXTENDS account.edi.xml.ubl_20 + vals = super()._export_invoice_vals(invoice) + vals['vals'].update({ + 'customization_id': 'OIOUBL-2.01', + 'AddressType_template': 'l10n_dk_oioubl.oioubl_201_AddressType', + # ProfileID is the property that define which documents the company can send and receive + # 'Procurement-BilSim-1.0' is the simplest one: invoice and bill + # https://www.oioubl.info/documents/en/en/Guidelines/OIOUBL_GUIDE_PROFILES.pdf + 'profile_id': 'Procurement-BilSim-1.0', + 'profile_id_attrs': { + 'schemeID': 'urn:oioubl:id:profileid-1.6', + 'schemeAgencyID': DANISH_NATIONAL_IT_AND_TELECOM_AGENCY_ID, + } + }) + vals['vals'].setdefault('invoice_type_code_attrs', {}).update({ + 'listID': 'urn:oioubl:codelist:invoicetypecode-1.2', + 'listAgencyID': DANISH_NATIONAL_IT_AND_TELECOM_AGENCY_ID, + }) + + return vals + + def _get_partner_party_vals(self, partner, role): + # EXTENDS account.edi.xml.ubl_20 + vals = super()._get_partner_party_vals(partner, role) + endpoint_id = format_vat_number(partner) + if endpoint_id[:2] == 'DK': + scheme_id = 'DK:CVR' + elif endpoint_id[:2] == 'FR': + scheme_id = 'FR:SIRET' + # SIRET is the french company registry + endpoint_id = (partner.company_registry or "").replace(" ", "") + else: + scheme_id = f'{endpoint_id[:2]}:VAT' + + vals.update({ + # list of possible endpointID available at + # https://www.oioubl.info/documents/en/en/Guidelines/OIOUBL_GUIDE_ENDPOINT.pdf + 'endpoint_id': endpoint_id, + 'endpoint_id_attrs': {'schemeID': scheme_id}, + }) + for party_tax_scheme in vals['party_tax_scheme_vals']: + # the doc says it could be empty but the schematron says otherwise + # https://www.oioubl.info/Classes/en/TaxScheme.html + party_tax_scheme.update({ + 'tax_scheme_id': 'VAT', + 'tax_scheme_id_attrs': {'schemeID': 'urn:oioubl:id:taxschemeid-1.5'}, + 'tax_name': 'VAT', + }) + return vals + + def _get_partner_address_vals(self, partner): + # EXTENDS account.edi.xml.ubl_20 + vals = super()._get_partner_address_vals(partner) + # https://www.oioubl.info/Classes/en/Address.html + address = tools.street_split(partner.street) + street_name = address.get('street_name') + building_number = address.get('street_number') + vals.update({ + # could be 'UN/CEFACT codeliste 3477' instead of StructuredDK' for partner out of DK + # not implemented yet because `StructuredDK` seems more than enough + 'address_format_code': 'StructuredDK', + 'address_format_code_attrs': { + 'listAgencyID': DANISH_NATIONAL_IT_AND_TELECOM_AGENCY_ID, + 'listID': 'urn:oioubl:codelist:addressformatcode-1.1', + }, + 'street_name': street_name, + 'building_number': building_number, + }) + return vals + + def _get_partner_party_tax_scheme_vals_list(self, partner, role): + # EXTENDS account.edi.xml.ubl_20 + vals_list = super()._get_partner_party_tax_scheme_vals_list(partner, role) + vat = format_vat_number(partner) + schemeID = 'DK:SE' if vat[:2] == 'DK' else 'ZZZ' + for vals in vals_list: + if partner.vat: + # SE is the danish vat number + # DK:SE indicates we're using it and 'ZZZ' is for international number + # https://www.oioubl.info/Codelists/en/urn_oioubl_scheme_partytaxschemecompanyid-1.1.html + vals.update({ + 'company_id_attrs': {'schemeID': schemeID}, + 'company_id': vat, + }) + + return vals_list + + def _get_partner_party_legal_entity_vals_list(self, partner): + # EXTENDS account.edi.xml.ubl_20 + vals_list = super()._get_partner_party_legal_entity_vals_list(partner) + vat = format_vat_number(partner) + schemeID = 'DK:CVR' if vat[:2] == 'DK' else 'ZZZ' + for vals in vals_list: + vals.update({ + 'company_id': vat, + 'company_id_attrs': {'schemeID': schemeID}, + }) + return vals_list + + def _get_invoice_payment_means_vals_list(self, invoice): + # EXTENDS account.edi.xml.ubl_20 + vals_list = super()._get_invoice_payment_means_vals_list(invoice) + for vals in vals_list: + # Hardcoded 'unknown' for now + # Later on, it would be nice to create a dynamically selected template that would depends on the payment means + vals['payment_means_code'] = PAYMENT_MEANS_CODE['unknown'] + + return vals_list + + def _get_invoice_tax_totals_vals_list(self, invoice, taxes_vals): + # EXTENDS account.edi.xml.ubl_20 + vals_list = super()._get_invoice_tax_totals_vals_list(invoice, taxes_vals) + for tax_total_vals in vals_list: + for subtotal_vals in tax_total_vals.get('tax_subtotal_vals', []): + # https://www.oioubl.info/Classes/en/TaxSubtotal.html + # No 'percent' node in OIOUBL + subtotal_vals.pop('percent', None) + + # TaxCategory https://www.oioubl.info/Classes/en/TaxCategory.html + subtotal_vals['tax_category_vals']['id_attrs'] = { + 'schemeID': 'urn:oioubl:id:taxcategoryid-1.3', + 'schemeAgencyID': DANISH_NATIONAL_IT_AND_TELECOM_AGENCY_ID, + } + + # TaxCategory id list: https://www.oioubl.info/codelists/en/urn_oioubl_id_taxcategoryid-1.1.html + # The condition prevents the value to be mapped again when the methods is run several time + if subtotal_vals['tax_category_vals']['id'] not in TAX_POSSIBLE_VALUES: + subtotal_vals['tax_category_vals']['id'] = UBL_TO_OIOUBL_TAX_CATEGORY_ID_MAPPING.get(subtotal_vals['tax_category_vals']['id']) + + subtotal_vals['tax_category_vals']['tax_scheme_name'] = 'VAT' + subtotal_vals['tax_category_vals']['tax_scheme_id_attrs'] = {'schemeID': 'urn:oioubl:id:taxschemeid-1.5'} + + # /Invoice[1]/cac:TaxTotal[1]/cac:TaxSubtotal[1]/cac:TaxCategory[1] + # [W-LIB230] Name should only be used within NES profiles + # (cbc:Name != '') and not(contains(/doc:Invoice/cbc:ProfileID, 'nesubl.eu')) + if 'name' in subtotal_vals['tax_category_vals']: + del subtotal_vals['tax_category_vals']['name'] + + return vals_list + + def _get_invoice_legal_monetary_total_vals(self, invoice, taxes_vals, line_extension_amount, allowance_total_amount): + # EXTENDS account.edi.xml.ubl_20 + vals = super()._get_invoice_legal_monetary_total_vals(invoice, taxes_vals, line_extension_amount, allowance_total_amount) + # In OIOUBL context, tax_exclusive_amount means "tax only" + vals['tax_exclusive_amount'] = taxes_vals['tax_amount_currency'] + if invoice.currency_id.is_zero(vals['prepaid_amount']): + del vals['prepaid_amount'] + return vals + + def _get_invoice_payment_terms_vals_list(self, invoice): + # cleaned atm because it's not mandatory and the standard payment terms gets the document rejected for validation + # https://www.oioubl.info/Classes/en/PaymentTerms.html + return [] + + def _get_tax_category_list(self, invoice, taxes): + # EXTENDS account.edi.common + vals_list = super()._get_tax_category_list(invoice, taxes) + for vals in vals_list: + # TaxCategory https://www.oioubl.info/Classes/en/TaxCategory.html + vals['id'] = UBL_TO_OIOUBL_TAX_CATEGORY_ID_MAPPING.get(vals['id']) + vals['id_attrs'] = { + 'schemeID': 'urn:oioubl:id:taxcategoryid-1.3', + 'schemeAgencyID': DANISH_NATIONAL_IT_AND_TELECOM_AGENCY_ID, + } + vals['tax_scheme_id_attrs'] = {'schemeID': 'urn:oioubl:id:taxschemeid-1.5'} + vals['tax_scheme_name'] = 'VAT' + # OIOUBL can't contain name for category + # /Invoice[1]/cac:InvoiceLine[1]/cac:Item[1]/cac:ClassifiedTaxCategory[1] + # [W-LIB230] Name should only be used within NES profiles + # (cbc:Name != '') and not(contains(/doc:Invoice/cbc:ProfileID, 'nesubl.eu')) + if 'name' in vals: + del vals['name'] + + return vals_list + + def _get_document_type_code_vals(self, invoice, invoice_data): + # EXTENDS 'account_edi_ubl_cii + # http://www.datypic.com/sc/ubl20/e-cbc_DocumentTypeCode.html + vals = super()._get_document_type_code_vals(invoice, invoice_data) + vals['attrs']['listAgencyID'] = "6" + vals['attrs']['listID'] = "UN/ECE 1001" + return vals + + def _export_invoice_constraints(self, invoice, vals): + # EXTENDS account.edi.xml.ubl_20 + constraints = super()._export_invoice_constraints(invoice, vals) + + for partner_type in ('supplier', 'customer'): + partner = vals[partner_type] + building_number = tools.street_split(partner.street).get('street_number') + if not building_number: + constraints[f"oioubl201_{partner_type}_building_number_required"] = \ + _("The following partner's street number is missing: %s", partner.name) + if partner.country_code == "FR" and not partner.company_registry: + constraints["oioubl201_company_registry_required_for_french_partner"] = \ + _("The company registry is required for french partner: %s", partner.name) + + return constraints + + def _get_currency_decimal_places(self, currency_id): + # OIOUBL needs the data to be formated to 2 decimals + return 2 diff --git a/addons/l10n_dk_oioubl/models/res_partner.py b/addons/l10n_dk_oioubl/models/res_partner.py new file mode 100644 index 00000000000..44ff90a0b8c --- /dev/null +++ b/addons/l10n_dk_oioubl/models/res_partner.py @@ -0,0 +1,19 @@ +from odoo import api, models, fields + + +class ResPartner(models.Model): + _inherit = 'res.partner' + + ubl_cii_format = fields.Selection(selection_add=[('oioubl_201', "OIOUBL 2.01")]) + + def _get_edi_builder(self): + if self.ubl_cii_format == 'oioubl_201': + return self.env['account.edi.xml.oioubl_201'] + return super()._get_edi_builder() + + @api.depends('country_code') + def _compute_ubl_cii_format(self): + super()._compute_ubl_cii_format() + for partner in self: + if partner.country_code == 'DK': + partner.ubl_cii_format = 'oioubl_201' diff --git a/addons/l10n_dk_oioubl/tests/__init__.py b/addons/l10n_dk_oioubl/tests/__init__.py new file mode 100644 index 00000000000..18e1e64fef8 --- /dev/null +++ b/addons/l10n_dk_oioubl/tests/__init__.py @@ -0,0 +1 @@ +from . import test_xml_oioubl_dk diff --git a/addons/l10n_dk_oioubl/tests/test_files/from_odoo/oioubl_out_invoice_foreign_partner_be.xml b/addons/l10n_dk_oioubl/tests/test_files/from_odoo/oioubl_out_invoice_foreign_partner_be.xml new file mode 100644 index 00000000000..35e05af9d15 --- /dev/null +++ b/addons/l10n_dk_oioubl/tests/test_files/from_odoo/oioubl_out_invoice_foreign_partner_be.xml @@ -0,0 +1,272 @@ + + + 2.0 + OIOUBL-2.01 + Procurement-BilSim-1.0 + INV/2017/00001 + 2017-01-01 + 380 + test narration + DKK + + ref_move + + + INV_2017_00001.pdf + 380 + + ___ignore___ + + + + + DK12345674 + + company_1_data + + + + StructuredDK + Paradisæblevej, + 10 + Aalborg + 9430 + + DK + Denmark + + + + company_1_data + DK12345674 + + StructuredDK + Paradisæblevej, + 10 + Aalborg + 9430 + + DK + Denmark + + + + VAT + VAT + + + + company_1_data + DK12345674 + + StructuredDK + Paradisæblevej, + 10 + Aalborg + 9430 + + DK + Denmark + + + + + ___ignore___ + company_1_data + +45 32 12 34 56 + + + + + + BE0897223670 + + SUPER BELGIAN PARTNER + + + + StructuredDK + Rue du Paradis, + 10 + Eghezee + 6870 + + BE + Belgium + + + + SUPER BELGIAN PARTNER + BE0897223670 + + StructuredDK + Rue du Paradis, + 10 + Eghezee + 6870 + + BE + Belgium + + + + VAT + VAT + + + + SUPER BELGIAN PARTNER + BE0897223670 + + StructuredDK + Rue du Paradis, + 10 + Eghezee + 6870 + + BE + Belgium + + + + + ___ignore___ + SUPER BELGIAN PARTNER + 061928374 + partner_b@tsointsoin + + + + + + + + StructuredDK + Rue du Paradis, + 10 + Eghezee + 6870 + + BE + Belgium + + + + + + 1 + 2017-02-28 + INV/2017/00001 + INV/2017/00001 + + DK5000400440116243 + + + + 0.00 + + 1500.00 + 0.00 + + ReverseCharge + 0.0 + VATEX-EU-IC + Intra-Community supply + + VAT + VAT + + + + + + 1500.00 + 0.00 + 1500.00 + 1500.00 + + + ___ignore___ + 1.0 + 500.00 + + 0.00 + + 500.00 + 0.00 + + ReverseCharge + 0.0 + VATEX-EU-IC + Intra-Community supply + + VAT + VAT + + + + + + product_a + product_a + + ReverseCharge + 0.0 + VATEX-EU-IC + Intra-Community supply + + VAT + VAT + + + + + 500.00 + + + + ___ignore___ + 1.0 + 1000.00 + + 0.00 + + 1000.00 + 0.00 + + ReverseCharge + 0.0 + VATEX-EU-IC + Intra-Community supply + + VAT + VAT + + + + + + product_b + product_b + + ReverseCharge + 0.0 + VATEX-EU-IC + Intra-Community supply + + VAT + VAT + + + + + 1000.00 + + + diff --git a/addons/l10n_dk_oioubl/tests/test_files/from_odoo/oioubl_out_invoice_foreign_partner_fr.xml b/addons/l10n_dk_oioubl/tests/test_files/from_odoo/oioubl_out_invoice_foreign_partner_fr.xml new file mode 100644 index 00000000000..de9b88af9df --- /dev/null +++ b/addons/l10n_dk_oioubl/tests/test_files/from_odoo/oioubl_out_invoice_foreign_partner_fr.xml @@ -0,0 +1,271 @@ + + + 2.0 + OIOUBL-2.01 + Procurement-BilSim-1.0 + INV/2017/00001 + 2017-01-01 + 380 + test narration + DKK + + ref_move + + + INV_2017_00001.pdf + 380 + + ___ignore___ + + + + + DK12345674 + + company_1_data + + + + StructuredDK + Paradisæblevej, + 10 + Aalborg + 9430 + + DK + Denmark + + + + company_1_data + DK12345674 + + StructuredDK + Paradisæblevej, + 10 + Aalborg + 9430 + + DK + Denmark + + + + VAT + VAT + + + + company_1_data + DK12345674 + + StructuredDK + Paradisæblevej, + 10 + Aalborg + 9430 + + DK + Denmark + + + + + ___ignore___ + company_1_data + +45 32 12 34 56 + + + + + + 12356894100056 + + SUPER FRENCH PARTNER + + + + StructuredDK + Rue Fabricy, + 16 + Lille + 59000 + + FR + France + + + + SUPER FRENCH PARTNER + FR23334175221 + + StructuredDK + Rue Fabricy, + 16 + Lille + 59000 + + FR + France + + + + VAT + VAT + + + + SUPER FRENCH PARTNER + FR23334175221 + + StructuredDK + Rue Fabricy, + 16 + Lille + 59000 + + FR + France + + + + + ___ignore___ + SUPER FRENCH PARTNER + +33 1 23 45 67 89 + + + + + + + + StructuredDK + Rue Fabricy, + 16 + Lille + 59000 + + FR + France + + + + + + 1 + 2017-02-28 + INV/2017/00001 + INV/2017/00001 + + DK5000400440116243 + + + + 0.00 + + 1500.00 + 0.00 + + ReverseCharge + 0.0 + VATEX-EU-IC + Intra-Community supply + + VAT + VAT + + + + + + 1500.00 + 0.00 + 1500.00 + 1500.00 + + + ___ignore___ + 1.0 + 500.00 + + 0.00 + + 500.00 + 0.00 + + ReverseCharge + 0.0 + VATEX-EU-IC + Intra-Community supply + + VAT + VAT + + + + + + product_a + product_a + + ReverseCharge + 0.0 + VATEX-EU-IC + Intra-Community supply + + VAT + VAT + + + + + 500.00 + + + + ___ignore___ + 1.0 + 1000.00 + + 0.00 + + 1000.00 + 0.00 + + ReverseCharge + 0.0 + VATEX-EU-IC + Intra-Community supply + + VAT + VAT + + + + + + product_b + product_b + + ReverseCharge + 0.0 + VATEX-EU-IC + Intra-Community supply + + VAT + VAT + + + + + 1000.00 + + + diff --git a/addons/l10n_dk_oioubl/tests/test_files/from_odoo/oioubl_out_invoice_partner_dk.xml b/addons/l10n_dk_oioubl/tests/test_files/from_odoo/oioubl_out_invoice_partner_dk.xml new file mode 100644 index 00000000000..8c4c4c9a5e1 --- /dev/null +++ b/addons/l10n_dk_oioubl/tests/test_files/from_odoo/oioubl_out_invoice_partner_dk.xml @@ -0,0 +1,262 @@ + + + 2.0 + OIOUBL-2.01 + Procurement-BilSim-1.0 + INV/2017/00001 + 2017-01-01 + 380 + test narration + DKK + + ref_move + + + INV_2017_00001.pdf + 380 + + ___ignore___ + + + + + DK12345674 + + company_1_data + + + + StructuredDK + Paradisæblevej, + 10 + Aalborg + 9430 + + DK + Denmark + + + + company_1_data + DK12345674 + + StructuredDK + Paradisæblevej, + 10 + Aalborg + 9430 + + DK + Denmark + + + + VAT + VAT + + + + company_1_data + DK12345674 + + StructuredDK + Paradisæblevej, + 10 + Aalborg + 9430 + + DK + Denmark + + + + + ___ignore___ + company_1_data + +45 32 12 34 56 + + + + + + DK12345674 + + SUPER DANISH PARTNER + + + + StructuredDK + Paradisæblevej, + 11 + Aalborg + 9430 + + DK + Denmark + + + + SUPER DANISH PARTNER + DK12345674 + + StructuredDK + Paradisæblevej, + 11 + Aalborg + 9430 + + DK + Denmark + + + + VAT + VAT + + + + SUPER DANISH PARTNER + DK12345674 + + StructuredDK + Paradisæblevej, + 11 + Aalborg + 9430 + + DK + Denmark + + + + + ___ignore___ + SUPER DANISH PARTNER + +45 32 12 35 56 + partner_a@tsointsoin + + + + + + + + StructuredDK + Paradisæblevej, + 11 + Aalborg + 9430 + + DK + Denmark + + + + + + 1 + 2017-02-28 + INV/2017/00001 + INV/2017/00001 + + DK5000400440116243 + + + + 375.00 + + 1500.00 + 375.00 + + StandardRated + 25.0 + + VAT + VAT + + + + + + 1500.00 + 375.00 + 1875.00 + 1875.00 + + + ___ignore___ + 1.0 + 500.00 + + 125.00 + + 500.00 + 125.00 + + StandardRated + 25.0 + + VAT + VAT + + + + + + product_a + product_a + + StandardRated + 25.0 + + VAT + VAT + + + + + 500.00 + + + + ___ignore___ + 1.0 + 1000.00 + + 250.00 + + 1000.00 + 250.00 + + StandardRated + 25.0 + + VAT + VAT + + + + + + product_b + product_b + + StandardRated + 25.0 + + VAT + VAT + + + + + 1000.00 + + + diff --git a/addons/l10n_dk_oioubl/tests/test_files/from_odoo/oioubl_out_refund_foreign_partner_fr.xml b/addons/l10n_dk_oioubl/tests/test_files/from_odoo/oioubl_out_refund_foreign_partner_fr.xml new file mode 100644 index 00000000000..177a1acb734 --- /dev/null +++ b/addons/l10n_dk_oioubl/tests/test_files/from_odoo/oioubl_out_refund_foreign_partner_fr.xml @@ -0,0 +1,261 @@ + + + 2.0 + OIOUBL-2.01 + Procurement-BilSim-1.0 + RINV/2017/00001 + 2017-01-01 + test narration + DKK + + ref_move + + + RINV_2017_00001.pdf + 381 + + ___ignore___ + + + + + DK12345674 + + company_1_data + + + + StructuredDK + Paradisæblevej, + 10 + Aalborg + 9430 + + DK + Denmark + + + + company_1_data + DK12345674 + + StructuredDK + Paradisæblevej, + 10 + Aalborg + 9430 + + DK + Denmark + + + + VAT + VAT + + + + company_1_data + DK12345674 + + StructuredDK + Paradisæblevej, + 10 + Aalborg + 9430 + + DK + Denmark + + + + + ___ignore___ + company_1_data + +45 32 12 34 56 + + + + + + 12356894100056 + + SUPER FRENCH PARTNER + + + + StructuredDK + Rue Fabricy, + 16 + Lille + 59000 + + FR + France + + + + SUPER FRENCH PARTNER + FR23334175221 + + StructuredDK + Rue Fabricy, + 16 + Lille + 59000 + + FR + France + + + + VAT + VAT + + + + SUPER FRENCH PARTNER + FR23334175221 + + StructuredDK + Rue Fabricy, + 16 + Lille + 59000 + + FR + France + + + + + ___ignore___ + SUPER FRENCH PARTNER + +33 1 23 45 67 89 + + + + + + + + StructuredDK + Rue Fabricy, + 16 + Lille + 59000 + + FR + France + + + + + + 0.00 + + 1500.00 + 0.00 + + ReverseCharge + 0.0 + VATEX-EU-IC + Intra-Community supply + + VAT + VAT + + + + + + 1500.00 + 0.00 + 1500.00 + 1500.00 + + + ___ignore___ + 1.0 + 500.00 + + 0.00 + + 500.00 + 0.00 + + ReverseCharge + 0.0 + VATEX-EU-IC + Intra-Community supply + + VAT + VAT + + + + + + product_a + product_a + + ReverseCharge + 0.0 + VATEX-EU-IC + Intra-Community supply + + VAT + VAT + + + + + 500.00 + + + + ___ignore___ + 1.0 + 1000.00 + + 0.00 + + 1000.00 + 0.00 + + ReverseCharge + 0.0 + VATEX-EU-IC + Intra-Community supply + + VAT + VAT + + + + + + product_b + product_b + + ReverseCharge + 0.0 + VATEX-EU-IC + Intra-Community supply + + VAT + VAT + + + + + 1000.00 + + + diff --git a/addons/l10n_dk_oioubl/tests/test_files/from_odoo/oioubl_out_refund_partner_dk.xml b/addons/l10n_dk_oioubl/tests/test_files/from_odoo/oioubl_out_refund_partner_dk.xml new file mode 100644 index 00000000000..28d7e382e2f --- /dev/null +++ b/addons/l10n_dk_oioubl/tests/test_files/from_odoo/oioubl_out_refund_partner_dk.xml @@ -0,0 +1,252 @@ + + + 2.0 + OIOUBL-2.01 + Procurement-BilSim-1.0 + RINV/2017/00001 + 2017-01-01 + test narration + DKK + + ref_move + + + RINV_2017_00001.pdf + 381 + + ___ignore___ + + + + + DK12345674 + + company_1_data + + + + StructuredDK + Paradisæblevej, + 10 + Aalborg + 9430 + + DK + Denmark + + + + company_1_data + DK12345674 + + StructuredDK + Paradisæblevej, + 10 + Aalborg + 9430 + + DK + Denmark + + + + VAT + VAT + + + + company_1_data + DK12345674 + + StructuredDK + Paradisæblevej, + 10 + Aalborg + 9430 + + DK + Denmark + + + + + ___ignore___ + company_1_data + +45 32 12 34 56 + + + + + + DK12345674 + + SUPER DANISH PARTNER + + + + StructuredDK + Paradisæblevej, + 11 + Aalborg + 9430 + + DK + Denmark + + + + SUPER DANISH PARTNER + DK12345674 + + StructuredDK + Paradisæblevej, + 11 + Aalborg + 9430 + + DK + Denmark + + + + VAT + VAT + + + + SUPER DANISH PARTNER + DK12345674 + + StructuredDK + Paradisæblevej, + 11 + Aalborg + 9430 + + DK + Denmark + + + + + ___ignore___ + SUPER DANISH PARTNER + +45 32 12 35 56 + partner_a@tsointsoin + + + + + + + + StructuredDK + Paradisæblevej, + 11 + Aalborg + 9430 + + DK + Denmark + + + + + + 375.00 + + 1500.00 + 375.00 + + StandardRated + 25.0 + + VAT + VAT + + + + + + 1500.00 + 375.00 + 1875.00 + 1875.00 + + + ___ignore___ + 1.0 + 500.00 + + 125.00 + + 500.00 + 125.00 + + StandardRated + 25.0 + + VAT + VAT + + + + + + product_a + product_a + + StandardRated + 25.0 + + VAT + VAT + + + + + 500.00 + + + + ___ignore___ + 1.0 + 1000.00 + + 250.00 + + 1000.00 + 250.00 + + StandardRated + 25.0 + + VAT + VAT + + + + + + product_b + product_b + + StandardRated + 25.0 + + VAT + VAT + + + + + 1000.00 + + + diff --git a/addons/l10n_dk_oioubl/tests/test_xml_oioubl_dk.py b/addons/l10n_dk_oioubl/tests/test_xml_oioubl_dk.py new file mode 100644 index 00000000000..2503255f14b --- /dev/null +++ b/addons/l10n_dk_oioubl/tests/test_xml_oioubl_dk.py @@ -0,0 +1,168 @@ +from freezegun import freeze_time + +from odoo import Command +from odoo.addons.l10n_account_edi_ubl_cii_tests.tests.common import TestUBLCommon +from odoo.addons.account.tests.test_account_move_send import TestAccountMoveSendCommon +from odoo.exceptions import UserError +from odoo.tests import tagged + + +@tagged('post_install_l10n', 'post_install', '-at_install') +class TestUBLDK(TestUBLCommon, TestAccountMoveSendCommon): + + @classmethod + def setUpClass(cls, chart_template_ref="dk"): + super().setUpClass(chart_template_ref=chart_template_ref) + cls.company_data['company'].write({ + 'country_id': cls.env.ref('base.dk').id, + 'currency_id': cls.env.ref('base.DKK').id, + 'city': 'Aalborg', + 'zip': '9430', + 'vat': 'DK12345674', + 'phone': '+45 32 12 34 56', + 'street': 'Paradisæblevej, 10', + 'invoice_is_ubl_cii': True, + }) + cls.env['res.partner.bank'].create({ + 'acc_type': 'iban', + 'partner_id': cls.company_data['company'].partner_id.id, + 'acc_number': 'DK5000400440116243', + }) + + cls.partner_a.write({ + 'name': 'SUPER DANISH PARTNER', + 'city': 'Aalborg', + 'zip': '9430', + 'vat': 'DK12345674', + 'phone': '+45 32 12 35 56', + 'street': 'Paradisæblevej, 11', + 'country_id': cls.env.ref('base.dk').id, + 'ubl_cii_format': 'oioubl_201', + }) + cls.partner_b.write({ + 'name': 'SUPER BELGIAN PARTNER', + 'street': 'Rue du Paradis, 10', + 'zip': '6870', + 'city': 'Eghezee', + 'country_id': cls.env.ref('base.be').id, + 'phone': '061928374', + 'vat': 'BE0897223670', + 'ubl_cii_format': 'oioubl_201', + }) + cls.partner_c = cls.env["res.partner"].create({ + 'name': 'SUPER FRENCH PARTNER', + 'street': 'Rue Fabricy, 16', + 'zip': '59000', + 'city': 'Lille', + 'country_id': cls.env.ref('base.fr').id, + 'phone': '+33 1 23 45 67 89', + 'vat': 'FR23334175221', + 'company_registry': '123 568 941 00056', + 'ubl_cii_format': 'oioubl_201', + }) + cls.dk_local_tax_1 = cls.env["account.chart.template"].ref('tax120') + cls.dk_local_tax_2 = cls.env["account.chart.template"].ref('tax110') + cls.dk_foreign_tax_1 = cls.env["account.chart.template"].ref('tax210') + cls.dk_foreign_tax_2 = cls.env["account.chart.template"].ref('tax220') + + def create_post_and_send_invoice(self, partner=None, move_type='out_invoice'): + if not partner: + partner = self.partner_a + + if partner == self.partner_a: + # local dk taxes + tax_1, tax_2 = self.dk_local_tax_1, self.dk_local_tax_2 + else: + # dk taxes for foreigners + tax_1, tax_2 = self.dk_foreign_tax_1, self.dk_foreign_tax_2 + + invoice = self.env["account.move"].create({ + 'move_type': move_type, + 'partner_id': partner.id, + 'partner_bank_id': self.env.company.partner_id.bank_ids[:1].id, + 'invoice_payment_term_id': self.pay_terms_b.id, + 'invoice_date': '2017-01-01', + 'date': '2017-01-01', + 'narration': 'test narration', + 'ref': 'ref_move', + 'invoice_line_ids': [ + Command.create({ + 'product_id': self.product_a.id, + 'quantity': 1.0, + 'price_unit': 500.0, + 'tax_ids': [Command.set(tax_1.ids)], + }), + Command.create({ + 'product_id': self.product_b.id, + 'quantity': 1.0, + 'price_unit': 1000.0, + 'tax_ids': [Command.set(tax_2.ids)], + }), + ], + }) + invoice.action_post() + invoice._generate_pdf_and_send_invoice(self.move_template, from_cron=False, allow_fallback_pdf=False) + return invoice + + @freeze_time('2017-01-01') + def test_export_invoice_two_line_partner_dk(self): + invoice = self.create_post_and_send_invoice() + self.assertTrue(invoice.ubl_cii_xml_id) + self._assert_invoice_attachment(invoice.ubl_cii_xml_id, xpaths=None, expected_file_path="from_odoo/oioubl_out_invoice_partner_dk.xml") + + @freeze_time('2017-01-01') + def test_export_invoice_two_line_foreign_partner_be(self): + invoice = self.create_post_and_send_invoice(partner=self.partner_b) + self.assertTrue(invoice.ubl_cii_xml_id) + self._assert_invoice_attachment(invoice.ubl_cii_xml_id, xpaths=None, expected_file_path="from_odoo/oioubl_out_invoice_foreign_partner_be.xml") + + @freeze_time('2017-01-01') + def test_export_invoice_two_line_foreign_partner_fr(self): + invoice = self.create_post_and_send_invoice(partner=self.partner_c) + self.assertTrue(invoice.ubl_cii_xml_id) + self._assert_invoice_attachment(invoice.ubl_cii_xml_id, xpaths=None, expected_file_path="from_odoo/oioubl_out_invoice_foreign_partner_fr.xml") + + @freeze_time('2017-01-01') + def test_export_credit_note_two_line_partner_dk(self): + refund = self.create_post_and_send_invoice(move_type='out_refund') + self.assertTrue(refund.ubl_cii_xml_id) + self._assert_invoice_attachment(refund.ubl_cii_xml_id, xpaths=None, expected_file_path="from_odoo/oioubl_out_refund_partner_dk.xml") + + @freeze_time('2017-01-01') + def test_export_credit_note_two_line_partner_fr(self): + refund = self.create_post_and_send_invoice(partner=self.partner_c, move_type='out_refund') + self.assertTrue(refund.ubl_cii_xml_id) + self._assert_invoice_attachment(refund.ubl_cii_xml_id, xpaths=None, expected_file_path="from_odoo/oioubl_out_refund_foreign_partner_fr.xml") + + @freeze_time('2017-01-01') + def test_oioubl_export_should_still_be_valid_when_currency_has_more_precision_digit(self): + self.company_data['company'].currency_id.rounding = 0.001 + invoice = self.create_post_and_send_invoice() + self.assertTrue(invoice.ubl_cii_xml_id) + self._assert_invoice_attachment(invoice.ubl_cii_xml_id, xpaths=None, expected_file_path="from_odoo/oioubl_out_invoice_partner_dk.xml") + + @freeze_time('2017-01-01') + def test_oioubl_export_should_raise_an_error_when_partner_building_number_is_missing(self): + self.partner_a.street = 'Paradisæblevej' # remove the street number from the address + with self.assertRaisesRegex(UserError, "The following partner's street number is missing"): + self.create_post_and_send_invoice() + + @freeze_time('2017-01-01') + def test_oioubl_export_should_raise_an_error_when_company_building_number_is_missing(self): + self.env.company.partner_id.street = 'Paradisæblevej' + with self.assertRaisesRegex(UserError, "The following partner's street number is missing"): + self.create_post_and_send_invoice() + + @freeze_time('2017-01-01') + def test_export_invoice_company_and_partner_without_country_code_prefix_in_vat(self): + self.company_data['company'].vat = '12345674' + self.partner_a.vat = 'DK12345674' + invoice = self.create_post_and_send_invoice() + self.assertTrue(invoice.ubl_cii_xml_id) + self._assert_invoice_attachment(invoice.ubl_cii_xml_id, xpaths=None, expected_file_path="from_odoo/oioubl_out_invoice_partner_dk.xml") + + @freeze_time('2017-01-01') + def test_export_partner_fr_without_siret_should_raise_an_error(self): + self.partner_c.company_registry = False + with self.assertRaisesRegex(UserError, "The company registry is required for french partner:"): + self.create_post_and_send_invoice(partner=self.partner_c)