diff --git a/addons/account_edi/models/account_edi_format.py b/addons/account_edi/models/account_edi_format.py
index 3d8ed29566a..dced5f43abc 100644
--- a/addons/account_edi/models/account_edi_format.py
+++ b/addons/account_edi/models/account_edi_format.py
@@ -420,7 +420,7 @@ class AccountEdiFormat(models.Model):
element = xml_element.xpath(xpath, namespaces=namespaces)
return element[0].text if element else None
- def _retrieve_partner(self, name=None, phone=None, mail=None, vat=None):
+ def _retrieve_partner(self, name=None, phone=None, mail=None, vat=None, domain=None):
'''Search all partners and find one that matches one of the parameters.
:param name: The name of the partner.
@@ -430,14 +430,17 @@ class AccountEdiFormat(models.Model):
:returns: A partner or an empty recordset if not found.
'''
domains = []
- for value, domain in (
+ for value, dom in (
(name, [('name', 'ilike', name)]),
(phone, expression.OR([[('phone', '=', phone)], [('mobile', '=', phone)]])),
(mail, [('email', '=', mail)]),
(vat, [('vat', 'like', vat)]),
):
if value is not None:
- domains.append(domain)
+ domains.append(dom)
+
+ if domain:
+ domains.append(domain)
domain = expression.OR(domains)
return self.env['res.partner'].search(domain, limit=1)
diff --git a/addons/account_edi/models/account_move.py b/addons/account_edi/models/account_move.py
index 9cfdd13eaec..58f6a935b76 100644
--- a/addons/account_edi/models/account_move.py
+++ b/addons/account_edi/models/account_move.py
@@ -433,6 +433,7 @@ class AccountMoveLine(models.Model):
taxes_added_to_base.add(tax.id)
vals['tax_amount_currency'] += tax_vals['amount']
+ vals['tax_amount_currency_closing'] += tax_vals['amount'] if tax_rep.use_in_tax_closing else 0
for tag_id in tax_rep.tag_ids:
vals['tag_ids'].add(tag_id)
diff --git a/addons/account_edi_ubl/models/account_edi_format.py b/addons/account_edi_ubl/models/account_edi_format.py
index 10aff8ac29a..ab371e43810 100644
--- a/addons/account_edi_ubl/models/account_edi_format.py
+++ b/addons/account_edi_ubl/models/account_edi_format.py
@@ -234,6 +234,14 @@ class AccountEdiFormat(models.Model):
return super()._is_compatible_with_journal(journal)
return journal.type == 'sale'
+ def _is_enabled_by_default_on_journal(self, journal):
+ # OVERRIDE
+ # UBL is disabled by default to prevent conflict with other implementations of UBL.
+ self.ensure_one()
+ if self.code != 'ubl_2_1':
+ return super()._is_enabled_by_default_on_journal(journal)
+ return False
+
def _post_invoice_edi(self, invoices, test_mode=False):
# OVERRIDE
self.ensure_one()
diff --git a/addons/account_edi_ubl_bis3/__init__.py b/addons/account_edi_ubl_bis3/__init__.py
new file mode 100644
index 00000000000..0650744f6bc
--- /dev/null
+++ b/addons/account_edi_ubl_bis3/__init__.py
@@ -0,0 +1 @@
+from . import models
diff --git a/addons/account_edi_ubl_bis3/__manifest__.py b/addons/account_edi_ubl_bis3/__manifest__.py
new file mode 100644
index 00000000000..8b866bf547f
--- /dev/null
+++ b/addons/account_edi_ubl_bis3/__manifest__.py
@@ -0,0 +1,15 @@
+# -*- coding: utf-8 -*-
+{
+ 'name': 'Import/Export invoices with UBL (BIS3)',
+ 'description': '''
+ Support for Export/Import in UBL format (BIS3).
+ ''',
+ 'version': '1.0',
+ 'category': 'Accounting/Accounting',
+ 'depends': ['account_edi_ubl'],
+ 'data': [
+ 'data/bis3_templates.xml',
+ ],
+ 'installable': True,
+ 'application': False,
+}
diff --git a/addons/account_edi_ubl_bis3/data/bis3_templates.xml b/addons/account_edi_ubl_bis3/data/bis3_templates.xml
new file mode 100644
index 00000000000..dab2de16064
--- /dev/null
+++ b/addons/account_edi_ubl_bis3/data/bis3_templates.xml
@@ -0,0 +1,187 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ VAT
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Discount ( %)
+
+
+
+
+
+
+
+
+
+
+
+
+ VAT
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ VAT
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/addons/account_edi_ubl_bis3/models/__init__.py b/addons/account_edi_ubl_bis3/models/__init__.py
new file mode 100644
index 00000000000..9a32558d2d4
--- /dev/null
+++ b/addons/account_edi_ubl_bis3/models/__init__.py
@@ -0,0 +1,3 @@
+# -*- coding: utf-8 -*-
+
+from . import account_edi_format
diff --git a/addons/account_edi_ubl_bis3/models/account_edi_format.py b/addons/account_edi_ubl_bis3/models/account_edi_format.py
new file mode 100644
index 00000000000..92fbb48578c
--- /dev/null
+++ b/addons/account_edi_ubl_bis3/models/account_edi_format.py
@@ -0,0 +1,219 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+
+from odoo import models
+from odoo.exceptions import UserError
+from odoo.tests.common import Form
+
+
+COUNTRY_EAS = {
+ 'HU': 9910,
+
+ 'AD': 9922,
+ 'AL': 9923,
+ 'BA': 9924,
+ 'BE': 9925,
+ 'BG': 9926,
+ 'CH': 9927,
+ 'CY': 9928,
+ 'CZ': 9929,
+ 'DE': 9930,
+ 'EE': 9931,
+ 'UK': 9932,
+ 'GR': 9933,
+ 'HR': 9934,
+ 'IE': 9935,
+ 'LI': 9936,
+ 'LT': 9937,
+ 'LU': 9938,
+ 'LV': 9939,
+ 'MC': 9940,
+ 'ME': 9941,
+ 'MK': 9942,
+ 'MT': 9943,
+ 'NL': 9944,
+ 'PL': 9945,
+ 'PT': 9946,
+ 'RO': 9947,
+ 'RS': 9948,
+ 'SI': 9949,
+ 'SK': 9950,
+ 'SM': 9951,
+ 'TR': 9952,
+ 'VA': 9953,
+
+ 'SE': 9955,
+
+ 'FR': 9957
+}
+
+
+class AccountEdiFormat(models.Model):
+ ''' This edi_format is "abstract" meaning that it provides an additional layer for similar edi_format (formats
+ deriving from EN16931) that share some functionalities but needs to be extended to be used.
+ '''
+ _inherit = 'account.edi.format'
+
+ ####################################################
+ # Export
+ ####################################################
+
+ def _get_bis3_values(self, invoice):
+ values = super()._get_ubl_values(invoice)
+ values.update({
+ 'customization_id': 'urn:cen.eu:en16931:2017#compliant#urn:fdc:peppol.eu:2017:poacc:billing:3.0',
+ 'profile_id': 'urn:fdc:peppol.eu:2017:poacc:billing:01:1.0',
+ })
+
+ all_tax_detail_per_tax = {}
+ for line_vals in values['invoice_line_vals_list']:
+ line_vals['price_subtotal_with_no_tax_closing'] = line_vals['line'].price_subtotal
+ tax_detail_per_tax = {}
+ for tax_detail_vals in line_vals['tax_detail_vals_list']:
+ tax = tax_detail_vals['tax']
+
+ tax_percent = tax.amount
+ tax_category = 'S' if tax_percent else 'Z'
+ key = (tax_category, tax_percent)
+ tax_detail_per_tax.setdefault(key, {
+ 'base': tax_detail_vals['tax_base_amount'],
+ 'base_currency': tax_detail_vals['tax_base_amount_currency'],
+ 'amount': 0.0,
+ 'amount_currency': 0.0,
+ 'tax_percent': tax_percent,
+ 'tax_category': tax_category,
+ })
+ vals = tax_detail_per_tax[key]
+
+ vals['amount'] += tax_detail_vals['tax_amount_closing']
+ vals['amount_currency'] += tax_detail_vals['tax_amount_currency_closing']
+ delta_tax = tax_detail_vals['tax_amount_currency'] - tax_detail_vals['tax_amount_currency_closing']
+ line_vals['price_subtotal_with_no_tax_closing'] += delta_tax
+
+ if len(tax_detail_per_tax) > 1:
+ raise UserError("Multiple vat percentage not supported on the same invoice line")
+
+ line_vals['tax_detail_vals'] = list(tax_detail_per_tax.values())[0]
+
+ for key, tax_vals in tax_detail_per_tax.items():
+ all_tax_detail_per_tax.setdefault(key, {
+ **tax_vals,
+ 'base': 0.0,
+ 'base_currency': 0.0,
+ 'amount': 0.0,
+ 'amount_currency': 0.0,
+ })
+ vals = all_tax_detail_per_tax[key]
+ vals['base'] += tax_vals['base']
+ vals['base_currency'] += tax_vals['base_currency']
+ vals['amount'] += tax_vals['amount']
+ vals['amount_currency'] += tax_vals['amount_currency']
+
+ values['tax_detail_vals_list'] = list(all_tax_detail_per_tax.values())
+ values['total_untaxed_amount'] = sum(x['price_subtotal_with_no_tax_closing'] for x in values['invoice_line_vals_list'])
+ values['total_tax_amount'] = sum(x['amount'] for x in values['tax_detail_vals_list'])
+ values['total_tax_amount_currency'] = sum(x['amount_currency'] for x in values['tax_detail_vals_list'])
+
+ for partner_vals in (values['customer_vals'], values['supplier_vals']):
+ partner = partner_vals['partner']
+ if partner.country_id.code in COUNTRY_EAS:
+ partner_vals['bis3_endpoint'] = partner.vat
+ partner_vals['bis3_endpoint_scheme'] = COUNTRY_EAS[partner.country_id.code]
+
+ return values
+
+ ####################################################
+ # Import
+ ####################################################
+
+ 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
+
+ :returns: a list of domains
+ """
+ return []
+
+ def _decode_bis3(self, tree, invoice):
+ """ Decodes an EN16931 invoice into an invoice.
+ :param tree: the UBL (EN16931) tree to decode.
+ :param invoice: the invoice to update or an empty recordset.
+ :returns: the invoice where the UBL (EN16931) data was imported.
+ """
+ def _find_value(path, root=tree):
+ element = root.find(path)
+ return element.text if element is not None else None
+
+ element = tree.find('./{*}InvoiceTypeCode')
+ if element is not None:
+ type_code = element.text
+ move_type = 'in_refund' if type_code == '381' else 'in_invoice'
+ else:
+ move_type = 'in_invoice'
+
+ default_journal = invoice.with_context(default_move_type=move_type)._get_default_journal()
+
+ with Form(invoice.with_context(default_move_type=move_type, default_journal_id=default_journal.id)) as invoice_form:
+ # Reference
+ element = tree.find('./{*}ID')
+ if element is not None:
+ invoice_form.ref = element.text
+
+ # Dates
+ element = tree.find('./{*}IssueDate')
+ if element is not None:
+ invoice_form.invoice_date = element.text
+ element = tree.find('./{*}DueDate')
+ if element is not None:
+ invoice_form.invoice_date_due = element.text
+
+ # Currency
+ currency = self._retrieve_currency(_find_value('./{*}DocumentCurrencyCode'))
+ if currency:
+ invoice_form.currency_id = currency
+
+ # Partner
+ specific_domain = self._bis3_get_extra_partner_domains(tree)
+ invoice_form.partner_id = self._retrieve_partner(
+ name=_find_value('./{*}AccountingSupplierParty/{*}Party/*/{*}Name'),
+ phone=_find_value('./{*}AccountingSupplierParty/{*}Party/*/{*}Telephone'),
+ mail=_find_value('./{*}AccountingSupplierParty/{*}Party/*/{*}ElectronicMail'),
+ vat=_find_value('./{*}AccountingSupplierParty/{*}Party/{*}PartyTaxScheme/{*}CompanyID'),
+ domain=specific_domain,
+ )
+
+ # Lines
+ for eline in tree.findall('.//{*}InvoiceLine'):
+ with invoice_form.invoice_line_ids.new() as invoice_line_form:
+ # Product
+ invoice_line_form.product_id = self._retrieve_product(
+ default_code=_find_value('./{*}Item/{*}SellersItemIdentification/{*}ID', eline),
+ name=_find_value('./{*}Item/{*}Name', eline),
+ barcode=_find_value('./{*}Item/{*}StandardItemIdentification/{*}ID[@schemeID=\'0160\']', eline)
+ )
+
+ # Quantity
+ element = eline.find('./{*}InvoicedQuantity')
+ quantity = element is not None and float(element.text) or 1.0
+ invoice_line_form.quantity = quantity
+
+ # Price Unit
+ element = eline.find('./{*}Price/{*}PriceAmount')
+ price_unit = element is not None and float(element.text) or 0.0
+ line_extension_amount = element is not None and float(element.text) or 0.0
+ invoice_line_form.price_unit = price_unit or line_extension_amount / invoice_line_form.quantity or 0.0
+
+ # Name
+ element = eline.find('./{*}Item/{*}Description')
+ invoice_line_form.name = element is not None and element.text or ''
+
+ # Taxes
+ tax_elements = eline.findall('./{*}Item/{*}ClassifiedTaxCategory')
+ invoice_line_form.tax_ids.clear()
+ for tax_element in tax_elements:
+ invoice_line_form.tax_ids.add(self._retrieve_tax(
+ amount=_find_value('./{*}Percent', tax_element),
+ type_tax_use=invoice_form.journal_id.type
+ ))
+
+ return invoice_form.save()
diff --git a/addons/l10n_id_efaktur/models/res_partner.py b/addons/l10n_id_efaktur/models/res_partner.py
index 96f7c9c1c69..bff5353198d 100644
--- a/addons/l10n_id_efaktur/models/res_partner.py
+++ b/addons/l10n_id_efaktur/models/res_partner.py
@@ -8,7 +8,6 @@ class ResPartner(models.Model):
"""Inherit res.partner object to add NPWP field and Kode Transaksi"""
_inherit = "res.partner"
- country_code = fields.Char(related='country_id.code', string='Country Code')
l10n_id_pkp = fields.Boolean(string="ID PKP", compute='_compute_l10n_id_pkp', store=True, readonly=False)
l10n_id_nik = fields.Char(string='NIK')
l10n_id_tax_address = fields.Char('Tax Address')
diff --git a/addons/l10n_id_efaktur/views/res_partner_views.xml b/addons/l10n_id_efaktur/views/res_partner_views.xml
index 9c6f25c61e9..0a7d692b759 100644
--- a/addons/l10n_id_efaktur/views/res_partner_views.xml
+++ b/addons/l10n_id_efaktur/views/res_partner_views.xml
@@ -7,7 +7,6 @@
-
diff --git a/addons/l10n_nl/demo/demo_company.xml b/addons/l10n_nl/demo/demo_company.xml
index 817faf37aa3..788341b2263 100644
--- a/addons/l10n_nl/demo/demo_company.xml
+++ b/addons/l10n_nl/demo/demo_company.xml
@@ -3,14 +3,15 @@
NL Company
NL43603B11
-
+ Bloemstraat 42
Groningen
-
+ 9700
+31 6 12345678
info@company.nlexample.com
www.nlexample.com
+ 12345678
diff --git a/addons/l10n_nl/models/__init__.py b/addons/l10n_nl/models/__init__.py
index a3c0ab069f1..c8614475e2f 100644
--- a/addons/l10n_nl/models/__init__.py
+++ b/addons/l10n_nl/models/__init__.py
@@ -3,3 +3,4 @@
from . import account_journal
from . import account_chart_template
+from . import res_partner
diff --git a/addons/l10n_nl/models/res_partner.py b/addons/l10n_nl/models/res_partner.py
new file mode 100644
index 00000000000..79c5d8c05db
--- /dev/null
+++ b/addons/l10n_nl/models/res_partner.py
@@ -0,0 +1,9 @@
+# coding: utf-8
+from odoo import fields, models
+
+
+class ResPartner(models.Model):
+ _inherit = 'res.partner'
+
+ l10n_nl_kvk = fields.Char(string='KVK-nummer')
+ l10n_nl_oin = fields.Char(string='Organisatie Indentificatie Nummer')
diff --git a/addons/l10n_nl/views/res_partner_views.xml b/addons/l10n_nl/views/res_partner_views.xml
new file mode 100644
index 00000000000..edaa47965e3
--- /dev/null
+++ b/addons/l10n_nl/views/res_partner_views.xml
@@ -0,0 +1,14 @@
+
+
+
+ res.partner.form.inherit.l10n_nl
+ res.partner
+
+
+
+
+
+
+
+
+
diff --git a/addons/l10n_nl_edi/__init__.py b/addons/l10n_nl_edi/__init__.py
new file mode 100644
index 00000000000..dc5e6b693d1
--- /dev/null
+++ b/addons/l10n_nl_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_nl_edi/__manifest__.py b/addons/l10n_nl_edi/__manifest__.py
new file mode 100644
index 00000000000..6f8112e369d
--- /dev/null
+++ b/addons/l10n_nl_edi/__manifest__.py
@@ -0,0 +1,17 @@
+# -*- coding: utf-8 -*-
+{
+ 'name': 'Netherlands - E-Invoicing (NLCIUS 1.0.3)',
+ 'version': '0.1',
+ 'category': 'Accounting/Localizations/EDI',
+ 'summary': 'E-Invoicing, Universal Business Language (NLCIUS 1.0.3), nlc',
+ 'description': """
+NLCIUS is the Dutch implementation of EN 16931 norm. Both for UBL and UN / CEFACT XML CII.
+ """,
+ 'depends': ['l10n_nl', 'account_edi_ubl_bis3'],
+ 'data': [
+ 'data/account_edi_data.xml',
+ 'data/nlcius_template.xml',
+ ],
+ 'installable': True,
+ 'auto_install': True,
+}
diff --git a/addons/l10n_nl_edi/data/account_edi_data.xml b/addons/l10n_nl_edi/data/account_edi_data.xml
new file mode 100644
index 00000000000..95418d9de35
--- /dev/null
+++ b/addons/l10n_nl_edi/data/account_edi_data.xml
@@ -0,0 +1,11 @@
+
+
+
+
+
+ NLCIUS (Netherlands)
+ nlcius_1
+
+
+
+
diff --git a/addons/l10n_nl_edi/data/nlcius_template.xml b/addons/l10n_nl_edi/data/nlcius_template.xml
new file mode 100644
index 00000000000..9ae1a1a82c6
--- /dev/null
+++ b/addons/l10n_nl_edi/data/nlcius_template.xml
@@ -0,0 +1,31 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/addons/l10n_nl_edi/models/__init__.py b/addons/l10n_nl_edi/models/__init__.py
new file mode 100644
index 00000000000..80dd2914f7b
--- /dev/null
+++ b/addons/l10n_nl_edi/models/__init__.py
@@ -0,0 +1,3 @@
+# -*- encoding: utf-8 -*-
+
+from . import account_edi_format
diff --git a/addons/l10n_nl_edi/models/account_edi_format.py b/addons/l10n_nl_edi/models/account_edi_format.py
new file mode 100644
index 00000000000..b6210e4132f
--- /dev/null
+++ b/addons/l10n_nl_edi/models/account_edi_format.py
@@ -0,0 +1,148 @@
+# -*- coding: utf-8 -*-
+
+from odoo import models, _
+
+import base64
+
+
+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_nlcius(filename, tree)
+
+ def _is_nlcius(self, filename, tree):
+ profile_id = tree.find('./{*}ProfileID')
+ customization_id = tree.find('./{*}CustomizationID')
+ return tree.tag == '{urn:oasis:names:specification:ubl:schema:xsd:Invoice-2}Invoice' and \
+ profile_id is not None and 'peppol' in profile_id.text and \
+ customization_id is not None and 'nlcius' in customization_id.text
+
+ def _bis3_get_extra_partner_domains(self, tree):
+ if self.code == 'nlcius_1':
+ endpoint = tree.find('./{*}AccountingSupplierParty/{*}Party/{*}EndpointID')
+ if endpoint is not None:
+ scheme = endpoint.attrib['schemeID']
+ if scheme == '0106' and endpoint.text:
+ return [('l10n_nl_kvk', '=', endpoint.text)]
+ elif scheme == '0190' and endpoint.text:
+ return [('l10n_nl_oin', '=', endpoint.text)]
+ return []
+
+ ####################################################
+ # Export
+ ####################################################
+
+ def _get_nlcius_values(self, invoice):
+ values = super()._get_bis3_values(invoice)
+ values.update({
+ 'customization_id': 'urn:cen.eu:en16931:2017#compliant#urn:fdc:nen.nl:nlcius:v1.0',
+ 'payment_means_code': 30,
+ })
+
+ for partner_vals in (values['customer_vals'], values['supplier_vals']):
+ partner = partner_vals['partner']
+ endpoint = partner.l10n_nl_oin or partner.l10n_nl_kvk
+ if partner.country_code == 'NL' and endpoint:
+ scheme = '0190' if partner.l10n_nl_oin else '0106'
+ partner_vals.update({
+ 'bis3_endpoint': endpoint,
+ 'bis3_endpoint_scheme': scheme,
+ 'legal_entity': endpoint,
+ 'legal_entity_scheme': scheme,
+ 'partner_identification': endpoint,
+ })
+
+ return values
+
+ def _export_nlcius(self, invoice):
+ self.ensure_one()
+ # Create file content.
+ xml_content = b""
+ xml_content += self.env.ref('l10n_nl_edi.export_nlcius_invoice')._render(self._get_nlcius_values(invoice))
+ vat = invoice.company_id.partner_id.commercial_partner_id.vat
+ xml_name = 'nlcius-%s%s%s.xml' % (vat or '', '-' if vat else '', invoice.name.replace('/', '_'))
+ return self.env['ir.attachment'].create({
+ 'name': xml_name,
+ 'datas': base64.encodebytes(xml_content),
+ 'res_model': 'account.move',
+ 'res_id': invoice.id,
+ 'mimetype': 'application/xml'
+ })
+
+ ####################################################
+ # Account.edi.format override
+ ####################################################
+
+ def _check_move_configuration(self, invoice):
+ res = super()._check_move_configuration(invoice)
+ if self.code != 'nlcius_1':
+ return res
+
+ errors = []
+
+ supplier = invoice.company_id.partner_id.commercial_partner_id
+ if not supplier.street or not supplier.zip or not supplier.city:
+ errors.append(_("The supplier's address must include street, zip and city (%s).", supplier.display_name))
+ if supplier.country_code == 'NL' and not supplier.l10n_nl_kvk and not supplier.l10n_nl_oin:
+ errors.append(_("The supplier %s must have a KvK-nummer or OIN.", supplier.display_name))
+ if not supplier.vat:
+ errors.append(_("Please define a VAT number for '%s'.", supplier.display_name))
+
+ customer = invoice.commercial_partner_id
+ if customer.country_code == 'NL' and (not customer.street or not customer.zip or not customer.city):
+ errors.append(_("Customer's address must include street, zip and city (%s).", customer.display_name))
+ if customer.country_code == 'NL' and not customer.l10n_nl_kvk and not customer.l10n_nl_oin:
+ errors.append(_("The customer %s must have a KvK-nummer or OIN.", customer.display_name))
+
+ if not invoice.partner_bank_id:
+ errors.append(_("The supplier %s must have a bank account.", supplier.display_name))
+
+ if invoice.invoice_line_ids.filtered(lambda l: not (l.product_id.name or l.name)):
+ errors.append(_('Each invoice line must have a product or a label.'))
+
+ if invoice.invoice_line_ids.tax_ids.invoice_repartition_line_ids.filtered(lambda r: r.use_in_tax_closing) and \
+ not supplier.vat:
+ errors.append(_("When vat is present, the supplier must have a vat number."))
+
+ return errors
+
+ def _is_compatible_with_journal(self, journal):
+ self.ensure_one()
+ if self.code != 'nlcius_1':
+ return super()._is_compatible_with_journal(journal)
+ return journal.type == 'sale' and journal.country_code == 'NL'
+
+ def _post_invoice_edi(self, invoices, test_mode=False):
+ self.ensure_one()
+ if self.code != 'nlcius_1':
+ return super()._post_invoice_edi(invoices, test_mode=test_mode)
+
+ invoice = invoices # no batch ensure that there is only one invoice
+ attachment = self._export_nlcius(invoice)
+ return {invoice: {'attachment': attachment}}
+
+ def _is_embedding_to_invoice_pdf_needed(self):
+ self.ensure_one()
+ if self.code != 'nlcius_1':
+ return super()._is_embedding_to_invoice_pdf_needed()
+ return False
+
+ def _create_invoice_from_xml_tree(self, filename, tree):
+ self.ensure_one()
+ if self.code == 'nlcius_1' and self._is_nlcius(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 == 'nlcius_1' and self._is_nlcius(filename, tree):
+ return self._decode_bis3(tree, invoice)
+ return super()._update_invoice_from_xml_tree(filename, tree, invoice)
diff --git a/addons/l10n_nl_edi/test_xml_file/nlcius_test.xml b/addons/l10n_nl_edi/test_xml_file/nlcius_test.xml
new file mode 100644
index 00000000000..ffc4ab19b0d
--- /dev/null
+++ b/addons/l10n_nl_edi/test_xml_file/nlcius_test.xml
@@ -0,0 +1,138 @@
+
+
+ urn:cen.eu:en16931:2017#compliant#urn:fdc:nen.nl:nlcius:v1.0
+ urn:fdc:peppol.eu:2017:poacc:billing:01:1.0
+ INV/2020/11/0001
+ 2020-11-02
+ 2020-11-02
+ 380
+
+ EUR
+
+ Azure Interior
+
+
+
+ 77777677
+
+ 77777677
+
+
+ De Silva St 4557
+
+ Fremont
+ 94538
+
+ NL
+
+
+
+ NL000021504B09
+
+ VAT
+
+
+
+ partner_a
+ 77777677
+
+
+
+
+
+ 12345678
+
+ 12345678
+
+
+ NL Company
+
+
+ Bloemstraat 42
+
+ Groningen
+ 6700
+
+ NL
+
+
+
+ NL000043603B11
+
+ VAT
+
+
+
+ NL Company
+ 12345678
+
+
+ NL Company
+ +31 6 12345678
+ info@company.nlexample.com
+
+
+
+
+ 30
+
+ BE68 5390 0754 7034
+
+
+
+
+ 67.20
+
+
+ 320.00
+ 67.20
+
+ S
+ 21.0
+
+ VAT
+
+
+
+
+
+
+
+ 320.00
+ 320.00
+ 387.20
+ 0.00
+ 387.20
+
+
+
+
+ 1
+
+
+ 1.0
+ 320.00
+
+ [E-COM07] Large Cabinet
+ Large Cabinet
+
+ E-COM07
+
+
+
+ S
+ 21.0
+
+ VAT
+
+
+
+
+
+
+ 320.00
+ 1.0
+
+
+
+
diff --git a/addons/l10n_nl_edi/tests/__init__.py b/addons/l10n_nl_edi/tests/__init__.py
new file mode 100644
index 00000000000..7063d8432b3
--- /dev/null
+++ b/addons/l10n_nl_edi/tests/__init__.py
@@ -0,0 +1,3 @@
+# -*- coding: utf-8 -*-
+
+from . import test_nlcius
diff --git a/addons/l10n_nl_edi/tests/test_nlcius.py b/addons/l10n_nl_edi/tests/test_nlcius.py
new file mode 100644
index 00000000000..229b06d9b10
--- /dev/null
+++ b/addons/l10n_nl_edi/tests/test_nlcius.py
@@ -0,0 +1,218 @@
+# -*- coding: utf-8 -*-
+from odoo.addons.account_edi.tests.common import AccountEdiTestCommon
+from freezegun import freeze_time
+
+
+class TestUBL(AccountEdiTestCommon):
+
+ @classmethod
+ def setUpClass(cls, chart_template_ref='l10n_nl.l10nnl_chart_template', edi_format_ref='l10n_nl_edi.edi_nlcius_1'):
+ 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.nl').id,
+ 'l10n_nl_kvk': '82777822',
+ 'vat': 'NL000099998B57',
+ })
+
+ cls.partner_a.write({
+ 'l10n_nl_kvk': '77777677',
+ 'country_id': cls.env.ref('base.be').id,
+ 'vat': 'BE0477472701',
+ })
+
+ bank_account = cls.env['res.partner.bank'].create({
+ 'acc_number': 'BE93999574162167',
+ '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:nen.nl:nlcius:v1.0
+ urn:fdc:peppol.eu:2017:poacc:billing:01:1.0
+ INV/2020/12/0001
+ 2020-12-16
+ 2020-12-16
+ 380
+ EUR
+ partner_a
+
+
+ 82777822
+
+ 82777822
+
+
+ company_1_data
+
+
+ Archefstraat 42
+ Amsterdam
+ 1000
+
+ NL
+
+
+
+ NL000099998B57
+
+ VAT
+
+
+
+ company_1_data
+ 82777822
+
+
+ company_1_data
+
+
+
+
+
+ BE0477472701
+
+ partner_a
+
+
+
+ BE
+
+
+
+ BE0477472701
+
+ VAT
+
+
+
+ partner_a
+
+
+ partner_a
+
+
+
+
+ 30
+
+ BE93 9995 7416 2167
+
+
+
+ 7267.50
+
+ 33750.00
+ 7087.50
+
+ S
+ 21.0
+
+ VAT
+
+
+
+
+ 1200.00
+ 180.00
+
+ S
+ 15.0
+
+ VAT
+
+
+
+
+
+ 34950.00
+ 34950.00
+ 42217.50
+ 0.00
+ 42217.50
+
+
+ 1
+ Discount (10.0 %)
+ 150.0
+ 33750.00
+
+ product_a
+ product_a
+
+ S
+ 21.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_nlcius_import(self):
+ invoice = self.env['account.move'].with_context(default_move_type='in_invoice').create({})
+ invoice_count = len(self.env['account.move'].search([]))
+ self.update_invoice_from_file('l10n_nl_edi', 'test_xml_file', 'nlcius_test.xml', invoice)
+ self.assertEqual(len(self.env['account.move'].search([])), invoice_count)
+ self.assertEqual(invoice.amount_total, 387.2)
+ self.assertEqual(invoice.amount_tax, 67.2)
+ self.assertEqual(invoice.partner_id, self.partner_a)
+
+ @freeze_time('2020-12-16')
+ def test_nlcius_export(self):
+ self.assert_generated_file_equal(self.invoice, self.expected_invoice_values)
diff --git a/odoo/addons/base/models/res_partner.py b/odoo/addons/base/models/res_partner.py
index d7481b3969f..1672b417e89 100644
--- a/odoo/addons/base/models/res_partner.py
+++ b/odoo/addons/base/models/res_partner.py
@@ -198,6 +198,7 @@ class Partner(models.Model):
city = fields.Char()
state_id = fields.Many2one("res.country.state", string='State', ondelete='restrict', domain="[('country_id', '=?', country_id)]")
country_id = fields.Many2one('res.country', string='Country', ondelete='restrict')
+ country_code = fields.Char(related='country_id.code', string="Country Code")
partner_latitude = fields.Float(string='Geo Latitude', digits=(10, 7))
partner_longitude = fields.Float(string='Geo Longitude', digits=(10, 7))
email = fields.Char()
diff --git a/odoo/addons/base/views/res_partner_views.xml b/odoo/addons/base/views/res_partner_views.xml
index 834c899c22c..add35cc2c92 100644
--- a/odoo/addons/base/views/res_partner_views.xml
+++ b/odoo/addons/base/views/res_partner_views.xml
@@ -156,6 +156,7 @@
+