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)