[ADD] account_edi_ubl_bis3,l10n_nl_edi : added bis3 and nlcius formats

closes odoo/odoo#68514

Signed-off-by: Laurent Smet <smetl@users.noreply.github.com>
This commit is contained in:
Laurent Smet
2021-04-01 14:58:20 +00:00
committed by Benjamin Frantzen (bfr)
parent b58810a77b
commit 6db346767e
25 changed files with 1042 additions and 7 deletions
@@ -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)
@@ -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)
@@ -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()
+1
View File
@@ -0,0 +1 @@
from . import models
@@ -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,
}
@@ -0,0 +1,187 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<data>
<!--
res.partner
-->
<template id="export_bis3_invoice_partner">
<cac:Party
xmlns="urn:oasis:names:specification:ubl:schema:xsd:Invoice-2"
xmlns:cac="urn:oasis:names:specification:ubl:schema:xsd:CommonAggregateComponents-2"
xmlns:cbc="urn:oasis:names:specification:ubl:schema:xsd:CommonBasicComponents-2">
<t t-set="partner" t-value="partner_vals['partner']"/>
<cbc:EndpointID
t-att-schemeID="partner_vals['bis3_endpoint_scheme']"
t-esc="partner_vals['bis3_endpoint']"/>
<cac:PartyIdentification t-if="partner_vals.get('partner_identification')">
<cbc:ID t-esc="partner_vals['partner_identification']"/>
</cac:PartyIdentification>
<cac:PartyName>
<cbc:Name t-esc="partner.name"/>
</cac:PartyName>
<cac:PostalAddress>
<cbc:StreetName
t-if="partner.street"
t-esc="partner.street"/>
<cbc:AdditionalStreetName
t-if="partner.street2"
t-esc="partner.street2"/>
<cbc:CityName
t-if="partner.city"
t-esc="partner.city"/>
<cbc:PostalZone
t-if="partner.zip"
t-esc="partner.zip"/>
<cbc:CountrySubentity
t-if="partner.state_id"
t-esc="partner.state_id.name"/>
<cac:Country
t-if="partner.country_id">
<cbc:IdentificationCode t-esc="partner.country_id.code"/>
</cac:Country>
</cac:PostalAddress>
<cac:PartyTaxScheme t-if="partner.vat">
<cbc:CompanyID t-esc="partner.vat"/>
<cac:TaxScheme>
<cbc:ID>VAT</cbc:ID>
</cac:TaxScheme>
</cac:PartyTaxScheme>
<cac:PartyLegalEntity>
<cbc:RegistrationName t-esc="partner.name"/>
<cbc:CompanyID
t-if="partner_vals.get('legal_entity')"
t-att-schemeID="partner_vals['legal_entity_scheme']"
t-esc="partner_vals['legal_entity']"/>
</cac:PartyLegalEntity>
<cac:Contact>
<cbc:Name t-esc="partner.name"/>
<cbc:Telephone t-if="partner.phone" t-esc="partner.phone"/>
<cbc:ElectronicMail t-if="partner.email" t-esc="partner.email"/>
</cac:Contact>
</cac:Party>
</template>
<!--
account.move.line
-->
<template id="export_bis3_invoice_line">
<cac:InvoiceLine
xmlns="urn:oasis:names:specification:ubl:schema:xsd:Invoice-2"
xmlns:cac="urn:oasis:names:specification:ubl:schema:xsd:CommonAggregateComponents-2"
xmlns:cbc="urn:oasis:names:specification:ubl:schema:xsd:CommonBasicComponents-2">
<t t-set="line" t-value="line_vals['line']"/>
<cbc:ID t-esc="line_vals['index']"/>
<cbc:Note t-if="line.discount">Discount (<t t-esc='line.discount'/> %)</cbc:Note>
<cbc:InvoicedQuantity unitCode="ZZ" t-esc="line.quantity"/>
<cbc:LineExtensionAmount
t-att-currencyID="record.currency_id.name"
t-esc="format_monetary(line_vals['price_subtotal_with_no_tax_closing'])"/>
<cac:Item>
<cbc:Description t-esc="line.name.replace('\n', ', ')"/>
<cbc:Name t-esc="(line.name or '').replace('\n', ', ')"/>
<cac:SellersItemIdentification t-if="line.product_id.default_code">
<cbc:ID t-esc="line.product_id.default_code"/>
</cac:SellersItemIdentification>
<cac:ClassifiedTaxCategory>
<cbc:ID t-esc="line_vals['tax_detail_vals']['tax_category']"/>
<cbc:Percent t-esc="line_vals['tax_detail_vals']['tax_percent']"/>
<cac:TaxScheme>
<cbc:ID>VAT</cbc:ID>
</cac:TaxScheme>
</cac:ClassifiedTaxCategory>
</cac:Item>
<cac:Price>
<cbc:PriceAmount
t-att-currencyID="record.currency_id.name"
t-esc="format_monetary(line_vals['price_subtotal_with_no_tax_closing'] / line.quantity)"/>
<cbc:BaseQuantity t-esc="line.quantity"/>
</cac:Price>
</cac:InvoiceLine>
</template>
<!--
invoice
-->
<template id="export_bis3_invoice">
<Invoice
xmlns="urn:oasis:names:specification:ubl:schema:xsd:Invoice-2"
xmlns:cac="urn:oasis:names:specification:ubl:schema:xsd:CommonAggregateComponents-2"
xmlns:cbc="urn:oasis:names:specification:ubl:schema:xsd:CommonBasicComponents-2">
<cbc:CustomizationID t-esc="customization_id"/>
<cbc:ProfileID t-esc="profile_id"/>
<cbc:ID t-esc="record.name"/>
<cbc:IssueDate t-esc="record.invoice_date"/>
<cbc:DueDate t-esc="record.invoice_date_due"/>
<cbc:InvoiceTypeCode t-esc="type_code"/>
<cbc:Note t-esc="record.narration"/>
<cbc:DocumentCurrencyCode t-esc="record.currency_id.name"/>
<cbc:TaxCurrencyCode t-if="record.currency_id != record.company_currency_id"
t-esc="record.company_currency_id.name"/>
<cbc:BuyerReference t-esc="record.commercial_partner_id.name"/>
<cac:OrderReference t-if="record.invoice_origin">
<cbc:ID t-esc="record.invoice_origin"/>
</cac:OrderReference>
<cac:AccountingSupplierParty t-call="account_edi_ubl_bis3.export_bis3_invoice_partner">
<t t-set="partner_vals" t-value="supplier_vals"/>
</cac:AccountingSupplierParty>
<cac:AccountingCustomerParty t-call="account_edi_ubl_bis3.export_bis3_invoice_partner">
<t t-set="partner_vals" t-value="customer_vals"/>
</cac:AccountingCustomerParty>
<cac:PaymentMeans>
<cbc:PaymentMeansCode t-esc="payment_means_code"/>
<cac:PayeeFinancialAccount t-if="bank_account">
<cbc:ID t-esc="bank_account.acc_number"/>
</cac:PayeeFinancialAccount>
</cac:PaymentMeans>
<cac:PaymentTerms t-if="record.invoice_payment_term_id">
<cbc:Note t-esc="record.invoice_payment_term_id.name"/>
</cac:PaymentTerms>
<cac:TaxTotal>
<cbc:TaxAmount
t-att-currencyID="record.currency_id.name"
t-esc="format_monetary(total_tax_amount_currency)"/>
<cac:TaxSubtotal t-foreach="tax_detail_vals_list" t-as="tax_detail_vals">
<cbc:TaxableAmount
t-att-currencyID="record.currency_id.name"
t-esc="format_monetary(tax_detail_vals['base_currency'])"/>
<cbc:TaxAmount
t-att-currencyID="record.currency_id.name"
t-esc="format_monetary(tax_detail_vals['amount_currency'])"/>
<cac:TaxCategory>
<cbc:ID t-esc="tax_detail_vals['tax_category']"/>
<cbc:Percent t-esc="tax_detail_vals['tax_percent']"/>
<cac:TaxScheme>
<cbc:ID>VAT</cbc:ID>
</cac:TaxScheme>
</cac:TaxCategory>
</cac:TaxSubtotal>
</cac:TaxTotal>
<cac:TaxTotal t-if="record.currency_id != record.company_currency_id">
<cbc:TaxAmount
t-att-currencyID="record.company_currency_id.name"
t-esc="format_monetary(total_tax_amount)"/>
</cac:TaxTotal>
<cac:LegalMonetaryTotal>
<cbc:LineExtensionAmount
t-att-currencyID="record.currency_id.name"
t-esc="format_monetary(total_untaxed_amount)"/>
<cbc:TaxExclusiveAmount
t-att-currencyID="record.currency_id.name"
t-esc="format_monetary(total_untaxed_amount)"/>
<cbc:TaxInclusiveAmount
t-att-currencyID="record.currency_id.name"
t-esc="format_monetary(record.amount_total)"/>
<cbc:PrepaidAmount
t-att-currencyID="record.currency_id.name"
t-esc="format_monetary(record.amount_total - record.amount_residual)"/>
<cbc:PayableAmount
t-att-currencyID="record.currency_id.name"
t-esc="format_monetary(record.amount_residual)"/>
</cac:LegalMonetaryTotal>
<t t-foreach="invoice_line_vals_list" t-as="line_vals">
<t t-call="account_edi_ubl_bis3.export_bis3_invoice_line"/>
</t>
</Invoice>
</template>
</data>
</odoo>
@@ -0,0 +1,3 @@
# -*- coding: utf-8 -*-
from . import account_edi_format
@@ -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()
@@ -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')
@@ -7,7 +7,6 @@
<field name="inherit_id" ref="base.view_partner_form"/>
<field name="arch" type="xml">
<field name="vat" position="after">
<field name="country_code" invisible="1"/>
<field name="l10n_id_pkp" attrs="{'invisible': [('country_id', '!=', False), ('country_code', '!=', 'ID')]}"/>
</field>
<page name="accounting" position="inside">
+3 -2
View File
@@ -3,14 +3,15 @@
<record id="partner_demo_company_nl" model="res.partner">
<field name="name">NL Company</field>
<field name="vat">NL43603B11</field>
<field name="street"></field>
<field name="street">Bloemstraat 42</field>
<field name="city">Groningen</field>
<field name="country_id" ref="base.nl"/>
<field name="state_id" ref="base.state_nl_bq3"/>
<field name="zip"></field>
<field name="zip">9700</field>
<field name="phone">+31 6 12345678</field>
<field name="email">info@company.nlexample.com</field>
<field name="website">www.nlexample.com</field>
<field name="l10n_nl_kvk">12345678</field>
</record>
<record id="demo_company_nl" model="res.company">
+1
View File
@@ -3,3 +3,4 @@
from . import account_journal
from . import account_chart_template
from . import res_partner
+9
View File
@@ -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')
@@ -0,0 +1,14 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record id="view_partner_form_inherit_l10n_nl" model="ir.ui.view">
<field name="name">res.partner.form.inherit.l10n_nl</field>
<field name="model">res.partner</field>
<field name="inherit_id" ref="base.view_partner_form"/>
<field name="arch" type="xml">
<xpath expr="//field[@name='vat']" position="after">
<field name="l10n_nl_kvk" attrs="{'invisible': ['|', ('country_code', '!=', 'NL'), ('is_company', '=', False)]}"/>
<field name="l10n_nl_oin" attrs="{'invisible': ['|', ('country_code', '!=', 'NL'), ('is_company', '=', False)]}"/>
</xpath>
</field>
</record>
</odoo>
+4
View File
@@ -0,0 +1,4 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from . import models
+17
View File
@@ -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,
}
@@ -0,0 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<data>
<record id="edi_nlcius_1" model="account.edi.format">
<field name="name">NLCIUS (Netherlands)</field>
<field name="code">nlcius_1</field>
</record>
</data>
</odoo>
@@ -0,0 +1,31 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<data>
<template id="export_nlcius_invoice_partner"
inherit_id="account_edi_ubl_bis3.export_bis3_invoice_partner"
primary="True">
<xpath expr="//*[local-name()='CountrySubentity']" position="replace"/>
</template>
<template id="export_nlcius_invoice"
inherit_id="account_edi_ubl_bis3.export_bis3_invoice"
primary="True">
<xpath expr="//*[local-name()='AccountingSupplierParty']" position="replace">
<cac:AccountingSupplierParty t-call="l10n_nl_edi.export_nlcius_invoice_partner"
xmlns="urn:oasis:names:specification:ubl:schema:xsd:Invoice-2"
xmlns:cac="urn:oasis:names:specification:ubl:schema:xsd:CommonAggregateComponents-2">
<t t-set="partner_vals" t-value="supplier_vals"/>
</cac:AccountingSupplierParty>
</xpath>
<xpath expr="//*[local-name()='AccountingCustomerParty']" position="replace">
<cac:AccountingCustomerParty t-call="l10n_nl_edi.export_nlcius_invoice_partner"
xmlns="urn:oasis:names:specification:ubl:schema:xsd:Invoice-2"
xmlns:cac="urn:oasis:names:specification:ubl:schema:xsd:CommonAggregateComponents-2">
<t t-set="partner_vals" t-value="customer_vals"/>
</cac:AccountingCustomerParty>
</xpath>
</template>
</data>
</odoo>
+3
View File
@@ -0,0 +1,3 @@
# -*- encoding: utf-8 -*-
from . import account_edi_format
@@ -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 version='1.0' encoding='UTF-8'?>"
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)
@@ -0,0 +1,138 @@
<?xml version='1.0' encoding='UTF-8'?>
<Invoice xmlns:cbc="urn:oasis:names:specification:ubl:schema:xsd:CommonBasicComponents-2" xmlns:cac="urn:oasis:names:specification:ubl:schema:xsd:CommonAggregateComponents-2" xmlns="urn:oasis:names:specification:ubl:schema:xsd:Invoice-2">
<cbc:CustomizationID>urn:cen.eu:en16931:2017#compliant#urn:fdc:nen.nl:nlcius:v1.0</cbc:CustomizationID>
<cbc:ProfileID>urn:fdc:peppol.eu:2017:poacc:billing:01:1.0</cbc:ProfileID>
<cbc:ID>INV/2020/11/0001</cbc:ID>
<cbc:IssueDate>2020-11-02</cbc:IssueDate>
<cbc:DueDate>2020-11-02</cbc:DueDate>
<cbc:InvoiceTypeCode>380</cbc:InvoiceTypeCode>
<cbc:DocumentCurrencyCode>EUR</cbc:DocumentCurrencyCode>
<cbc:BuyerReference>Azure Interior</cbc:BuyerReference>
<cac:AccountingSupplierParty>
<cac:Party>
<cbc:EndpointID schemeID="0106">77777677</cbc:EndpointID>
<cac:PartyIdentification>
<cbc:ID>77777677</cbc:ID>
</cac:PartyIdentification>
<cac:PostalAddress>
<cbc:StreetName>De Silva St 4557</cbc:StreetName>
<cbc:CityName>Fremont</cbc:CityName>
<cbc:PostalZone>94538</cbc:PostalZone>
<cac:Country>
<cbc:IdentificationCode>NL</cbc:IdentificationCode>
</cac:Country>
</cac:PostalAddress>
<cac:PartyTaxScheme>
<cbc:CompanyID>NL000021504B09</cbc:CompanyID>
<cac:TaxScheme>
<cbc:ID>VAT</cbc:ID>
</cac:TaxScheme>
</cac:PartyTaxScheme>
<cac:PartyLegalEntity>
<cbc:RegistrationName>partner_a</cbc:RegistrationName>
<cbc:CompanyID schemeID="0106">77777677</cbc:CompanyID>
</cac:PartyLegalEntity>
</cac:Party>
</cac:AccountingSupplierParty>
<cac:AccountingCustomerParty>
<cac:Party>
<cbc:EndpointID schemeID="0106">12345678</cbc:EndpointID>
<cac:PartyIdentification>
<cbc:ID>12345678</cbc:ID>
</cac:PartyIdentification>
<cac:PartyName>
<cbc:Name>NL Company</cbc:Name>
</cac:PartyName>
<cac:PostalAddress>
<cbc:StreetName>Bloemstraat 42</cbc:StreetName>
<cbc:CityName>Groningen</cbc:CityName>
<cbc:PostalZone>6700</cbc:PostalZone>
<cac:Country>
<cbc:IdentificationCode>NL</cbc:IdentificationCode>
</cac:Country>
</cac:PostalAddress>
<cac:PartyTaxScheme>
<cbc:CompanyID>NL000043603B11</cbc:CompanyID>
<cac:TaxScheme>
<cbc:ID>VAT</cbc:ID>
</cac:TaxScheme>
</cac:PartyTaxScheme>
<cac:PartyLegalEntity>
<cbc:RegistrationName>NL Company</cbc:RegistrationName>
<cbc:CompanyID schemeID="0106">12345678</cbc:CompanyID>
</cac:PartyLegalEntity>
<cac:Contact>
<cbc:Name>NL Company</cbc:Name>
<cbc:Telephone>+31 6 12345678</cbc:Telephone>
<cbc:ElectronicMail>info@company.nlexample.com</cbc:ElectronicMail>
</cac:Contact>
</cac:Party>
</cac:AccountingCustomerParty>
<cac:PaymentMeans>
<cbc:PaymentMeansCode>30</cbc:PaymentMeansCode>
<cac:PayeeFinancialAccount>
<cbc:ID>BE68 5390 0754 7034</cbc:ID>
</cac:PayeeFinancialAccount>
</cac:PaymentMeans>
<cac:TaxTotal>
<cbc:TaxAmount currencyID="EUR">67.20</cbc:TaxAmount>
<cac:TaxSubtotal>
<cbc:TaxableAmount currencyID="EUR">320.00</cbc:TaxableAmount>
<cbc:TaxAmount currencyID="EUR">67.20</cbc:TaxAmount>
<cac:TaxCategory>
<cbc:ID>S</cbc:ID>
<cbc:Percent>21.0</cbc:Percent>
<cac:TaxScheme>
<cbc:ID>VAT</cbc:ID>
</cac:TaxScheme>
</cac:TaxCategory>
</cac:TaxSubtotal>
</cac:TaxTotal>
<cac:LegalMonetaryTotal>
<cbc:LineExtensionAmount currencyID="EUR">320.00</cbc:LineExtensionAmount>
<cbc:TaxExclusiveAmount currencyID="EUR">320.00</cbc:TaxExclusiveAmount>
<cbc:TaxInclusiveAmount currencyID="EUR">387.20</cbc:TaxInclusiveAmount>
<cbc:PrepaidAmount currencyID="EUR">0.00</cbc:PrepaidAmount>
<cbc:PayableAmount currencyID="EUR">387.20</cbc:PayableAmount>
</cac:LegalMonetaryTotal>
<cac:InvoiceLine>
<cbc:ID>1</cbc:ID>
<cbc:InvoicedQuantity unitCode="ZZ">1.0</cbc:InvoicedQuantity>
<cbc:LineExtensionAmount currencyID="EUR">320.00</cbc:LineExtensionAmount>
<cac:Item>
<cbc:Description>[E-COM07] Large Cabinet</cbc:Description>
<cbc:Name>Large Cabinet</cbc:Name>
<cac:SellersItemIdentification>
<cbc:ID>E-COM07</cbc:ID>
</cac:SellersItemIdentification>
<cac:ClassifiedTaxCategory>
<cbc:ID>S</cbc:ID>
<cbc:Percent>21.0</cbc:Percent>
<cac:TaxScheme>
<cbc:ID>VAT</cbc:ID>
</cac:TaxScheme>
</cac:ClassifiedTaxCategory>
</cac:Item>
<cac:Price>
<cbc:PriceAmount currencyID="EUR">320.00</cbc:PriceAmount>
<cbc:BaseQuantity>1.0</cbc:BaseQuantity>
</cac:Price>
</cac:InvoiceLine>
</Invoice>
+3
View File
@@ -0,0 +1,3 @@
# -*- coding: utf-8 -*-
from . import test_nlcius
+218
View File
@@ -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 = '''
<Invoice xmlns:cac="urn:oasis:names:specification:ubl:schema:xsd:CommonAggregateComponents-2" xmlns:cbc="urn:oasis:names:specification:ubl:schema:xsd:CommonBasicComponents-2" xmlns="urn:oasis:names:specification:ubl:schema:xsd:Invoice-2">
<cbc:CustomizationID>urn:cen.eu:en16931:2017#compliant#urn:fdc:nen.nl:nlcius:v1.0</cbc:CustomizationID>
<cbc:ProfileID>urn:fdc:peppol.eu:2017:poacc:billing:01:1.0</cbc:ProfileID>
<cbc:ID>INV/2020/12/0001</cbc:ID>
<cbc:IssueDate>2020-12-16</cbc:IssueDate>
<cbc:DueDate>2020-12-16</cbc:DueDate>
<cbc:InvoiceTypeCode>380</cbc:InvoiceTypeCode>
<cbc:DocumentCurrencyCode>EUR</cbc:DocumentCurrencyCode>
<cbc:BuyerReference>partner_a</cbc:BuyerReference>
<cac:AccountingSupplierParty>
<cac:Party>
<cbc:EndpointID schemeID="0106">82777822</cbc:EndpointID>
<cac:PartyIdentification>
<cbc:ID>82777822</cbc:ID>
</cac:PartyIdentification>
<cac:PartyName>
<cbc:Name>company_1_data</cbc:Name>
</cac:PartyName>
<cac:PostalAddress>
<cbc:StreetName>Archefstraat 42</cbc:StreetName>
<cbc:CityName>Amsterdam</cbc:CityName>
<cbc:PostalZone>1000</cbc:PostalZone>
<cac:Country>
<cbc:IdentificationCode>NL</cbc:IdentificationCode>
</cac:Country>
</cac:PostalAddress>
<cac:PartyTaxScheme>
<cbc:CompanyID>NL000099998B57</cbc:CompanyID>
<cac:TaxScheme>
<cbc:ID>VAT</cbc:ID>
</cac:TaxScheme>
</cac:PartyTaxScheme>
<cac:PartyLegalEntity>
<cbc:RegistrationName>company_1_data</cbc:RegistrationName>
<cbc:CompanyID schemeID="0106">82777822</cbc:CompanyID>
</cac:PartyLegalEntity>
<cac:Contact>
<cbc:Name>company_1_data</cbc:Name>
</cac:Contact>
</cac:Party>
</cac:AccountingSupplierParty>
<cac:AccountingCustomerParty>
<cac:Party>
<cbc:EndpointID schemeID="9925">BE0477472701</cbc:EndpointID>
<cac:PartyName>
<cbc:Name>partner_a</cbc:Name>
</cac:PartyName>
<cac:PostalAddress>
<cac:Country>
<cbc:IdentificationCode>BE</cbc:IdentificationCode>
</cac:Country>
</cac:PostalAddress>
<cac:PartyTaxScheme>
<cbc:CompanyID>BE0477472701</cbc:CompanyID>
<cac:TaxScheme>
<cbc:ID>VAT</cbc:ID>
</cac:TaxScheme>
</cac:PartyTaxScheme>
<cac:PartyLegalEntity>
<cbc:RegistrationName>partner_a</cbc:RegistrationName>
</cac:PartyLegalEntity>
<cac:Contact>
<cbc:Name>partner_a</cbc:Name>
</cac:Contact>
</cac:Party>
</cac:AccountingCustomerParty>
<cac:PaymentMeans>
<cbc:PaymentMeansCode>30</cbc:PaymentMeansCode>
<cac:PayeeFinancialAccount>
<cbc:ID>BE93 9995 7416 2167</cbc:ID>
</cac:PayeeFinancialAccount>
</cac:PaymentMeans>
<cac:TaxTotal>
<cbc:TaxAmount currencyID="EUR">7267.50</cbc:TaxAmount>
<cac:TaxSubtotal>
<cbc:TaxableAmount currencyID="EUR">33750.00</cbc:TaxableAmount>
<cbc:TaxAmount currencyID="EUR">7087.50</cbc:TaxAmount>
<cac:TaxCategory>
<cbc:ID>S</cbc:ID>
<cbc:Percent>21.0</cbc:Percent>
<cac:TaxScheme>
<cbc:ID>VAT</cbc:ID>
</cac:TaxScheme>
</cac:TaxCategory>
</cac:TaxSubtotal>
<cac:TaxSubtotal>
<cbc:TaxableAmount currencyID="EUR">1200.00</cbc:TaxableAmount>
<cbc:TaxAmount currencyID="EUR">180.00</cbc:TaxAmount>
<cac:TaxCategory>
<cbc:ID>S</cbc:ID>
<cbc:Percent>15.0</cbc:Percent>
<cac:TaxScheme>
<cbc:ID>VAT</cbc:ID>
</cac:TaxScheme>
</cac:TaxCategory>
</cac:TaxSubtotal>
</cac:TaxTotal>
<cac:LegalMonetaryTotal>
<cbc:LineExtensionAmount currencyID="EUR">34950.00</cbc:LineExtensionAmount>
<cbc:TaxExclusiveAmount currencyID="EUR">34950.00</cbc:TaxExclusiveAmount>
<cbc:TaxInclusiveAmount currencyID="EUR">42217.50</cbc:TaxInclusiveAmount>
<cbc:PrepaidAmount currencyID="EUR">0.00</cbc:PrepaidAmount>
<cbc:PayableAmount currencyID="EUR">42217.50</cbc:PayableAmount>
</cac:LegalMonetaryTotal>
<cac:InvoiceLine>
<cbc:ID>1</cbc:ID>
<cbc:Note>Discount (10.0 %)</cbc:Note>
<cbc:InvoicedQuantity unitCode="ZZ">150.0</cbc:InvoicedQuantity>
<cbc:LineExtensionAmount currencyID="EUR">33750.00</cbc:LineExtensionAmount>
<cac:Item>
<cbc:Description>product_a</cbc:Description>
<cbc:Name>product_a</cbc:Name>
<cac:ClassifiedTaxCategory>
<cbc:ID>S</cbc:ID>
<cbc:Percent>21.0</cbc:Percent>
<cac:TaxScheme>
<cbc:ID>VAT</cbc:ID>
</cac:TaxScheme>
</cac:ClassifiedTaxCategory>
</cac:Item>
<cac:Price>
<cbc:PriceAmount currencyID="EUR">225.00</cbc:PriceAmount>
<cbc:BaseQuantity>150.0</cbc:BaseQuantity>
</cac:Price>
</cac:InvoiceLine>
<cac:InvoiceLine>
<cbc:ID>2</cbc:ID>
<cbc:InvoicedQuantity unitCode="ZZ">12.0</cbc:InvoicedQuantity>
<cbc:LineExtensionAmount currencyID="EUR">1200.00</cbc:LineExtensionAmount>
<cac:Item>
<cbc:Description>product_b</cbc:Description>
<cbc:Name>product_b</cbc:Name>
<cac:ClassifiedTaxCategory>
<cbc:ID>S</cbc:ID>
<cbc:Percent>15.0</cbc:Percent>
<cac:TaxScheme>
<cbc:ID>VAT</cbc:ID>
</cac:TaxScheme>
</cac:ClassifiedTaxCategory>
</cac:Item>
<cac:Price>
<cbc:PriceAmount currencyID="EUR">100.00</cbc:PriceAmount>
<cbc:BaseQuantity>12.0</cbc:BaseQuantity>
</cac:Price>
</cac:InvoiceLine>
</Invoice>
'''
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)
+1
View File
@@ -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()
@@ -156,6 +156,7 @@
<field name="is_company" invisible="1"/>
<field name="commercial_partner_id" invisible="1"/>
<field name="active" invisible="1"/>
<field name="country_code" invisible="1"/>
<field name="company_type" widget="radio" options="{'horizontal': true}"/>
<h1>
<field class="o_text_overflow" name="name" default_focus="1" placeholder="e.g. Lumber Inc" attrs="{'required' : [('type', '=', 'contact')], 'invisible': [('is_company','=', False)]}"/>