432 lines
21 KiB
Python
432 lines
21 KiB
Python
# -*- coding: utf-8 -*-
|
||
|
||
from odoo import models, _
|
||
|
||
from stdnum.no import mva
|
||
|
||
|
||
class AccountEdiXmlUBLBIS3(models.AbstractModel):
|
||
_name = "account.edi.xml.ubl_bis3"
|
||
_inherit = 'account.edi.xml.ubl_21'
|
||
_description = "UBL BIS Billing 3.0.12"
|
||
|
||
"""
|
||
* Documentation of EHF Billing 3.0: https://anskaffelser.dev/postaward/g3/
|
||
* EHF 2.0 is no longer used:
|
||
https://anskaffelser.dev/postaward/g2/announcement/2019-11-14-removal-old-invoicing-specifications/
|
||
* Official doc for EHF Billing 3.0 is the OpenPeppol BIS 3 doc +
|
||
https://anskaffelser.dev/postaward/g3/spec/current/billing-3.0/norway/
|
||
|
||
"Based on work done in PEPPOL BIS Billing 3.0, Difi has included Norwegian rules in PEPPOL BIS Billing 3.0 and
|
||
does not see a need to implement a different CIUS targeting the Norwegian market. Implementation of EHF Billing
|
||
3.0 is therefore done by implementing PEPPOL BIS Billing 3.0 without extensions or extra rules."
|
||
|
||
Thus, EHF 3 and Bis 3 are actually the same format. The specific rules for NO defined in Bis 3 are added in Bis 3.
|
||
"""
|
||
|
||
# -------------------------------------------------------------------------
|
||
# EXPORT
|
||
# -------------------------------------------------------------------------
|
||
|
||
def _export_invoice_filename(self, invoice):
|
||
return f"{invoice.name.replace('/', '_')}_ubl_bis3.xml"
|
||
|
||
def _export_invoice_ecosio_schematrons(self):
|
||
return {
|
||
'invoice': 'eu.peppol.bis3:invoice:3.13.0',
|
||
'credit_note': 'eu.peppol.bis3:creditnote:3.13.0',
|
||
}
|
||
|
||
def _get_country_vals(self, country):
|
||
# EXTENDS account.edi.xml.ubl_21
|
||
vals = super()._get_country_vals(country)
|
||
|
||
vals.pop('name', None)
|
||
|
||
return vals
|
||
|
||
def _get_partner_party_tax_scheme_vals_list(self, partner, role):
|
||
# EXTENDS account.edi.xml.ubl_21
|
||
vals_list = super()._get_partner_party_tax_scheme_vals_list(partner, role)
|
||
|
||
if not partner.vat:
|
||
return [{
|
||
'company_id': partner.peppol_endpoint,
|
||
'tax_scheme_vals': {'id': partner.peppol_eas},
|
||
}]
|
||
|
||
for vals in vals_list:
|
||
vals.pop('registration_name', None)
|
||
vals.pop('registration_address_vals', None)
|
||
|
||
# Some extra european countries use Bis 3 but do not prepend their VAT with the country code (i.e.
|
||
# Australia). Allow them to use Bis 3 without raising BR-CO-09.
|
||
if (
|
||
partner.country_id
|
||
and partner.country_id not in self.env.ref('base.europe').country_ids
|
||
and not partner.vat[:2].isalpha()
|
||
):
|
||
vals['company_id'] = partner.country_id.code + partner.vat
|
||
|
||
# sources:
|
||
# https://anskaffelser.dev/postaward/g3/spec/current/billing-3.0/norway/#_applying_foretaksregisteret
|
||
# https://docs.peppol.eu/poacc/billing/3.0/bis/#national_rules (NO-R-002 (warning))
|
||
if partner.country_id.code == "NO" and role == 'supplier':
|
||
vals_list.append({
|
||
'company_id': "Foretaksregisteret",
|
||
'tax_scheme_vals': {'id': 'TAX'},
|
||
})
|
||
|
||
return vals_list
|
||
|
||
def _get_partner_party_legal_entity_vals_list(self, partner):
|
||
# EXTENDS account.edi.xml.ubl_21
|
||
vals_list = super()._get_partner_party_legal_entity_vals_list(partner)
|
||
|
||
for vals in vals_list:
|
||
vals.pop('registration_address_vals', None)
|
||
if partner.country_code == 'NL':
|
||
vals.update({
|
||
'company_id': partner.peppol_endpoint,
|
||
'company_id_attrs': {'schemeID': partner.peppol_eas},
|
||
})
|
||
if partner.country_id.code == "LU" and 'l10n_lu_peppol_identifier' in partner._fields and partner.l10n_lu_peppol_identifier:
|
||
vals['company_id'] = partner.l10n_lu_peppol_identifier
|
||
if partner.country_id.code == 'DK':
|
||
# DK-R-014: For Danish Suppliers it is mandatory to specify schemeID as "0184" (DK CVR-number) when
|
||
# PartyLegalEntity/CompanyID is used for AccountingSupplierParty
|
||
vals['company_id_attrs'] = {'schemeID': '0184'}
|
||
if not vals['company_id']:
|
||
vals['company_id'] = partner.peppol_endpoint
|
||
|
||
return vals_list
|
||
|
||
def _get_partner_contact_vals(self, partner):
|
||
# EXTENDS account.edi.xml.ubl_21
|
||
vals = super()._get_partner_contact_vals(partner)
|
||
|
||
vals.pop('id', None)
|
||
|
||
return vals
|
||
|
||
def _get_partner_party_vals(self, partner, role):
|
||
# EXTENDS account.edi.xml.ubl_21
|
||
vals = super()._get_partner_party_vals(partner, role)
|
||
|
||
vals.update({
|
||
'endpoint_id': partner.peppol_endpoint,
|
||
'endpoint_id_attrs': {'schemeID': partner.peppol_eas},
|
||
})
|
||
|
||
return vals
|
||
|
||
def _get_partner_party_identification_vals_list(self, partner):
|
||
# EXTENDS account.edi.xml.ubl_21
|
||
vals = super()._get_partner_party_identification_vals_list(partner)
|
||
|
||
if partner.country_code == 'NL':
|
||
vals.append({
|
||
'id': partner.peppol_endpoint,
|
||
})
|
||
return vals
|
||
|
||
def _get_delivery_vals_list(self, invoice):
|
||
# EXTENDS account.edi.xml.ubl_21
|
||
supplier = invoice.company_id.partner_id.commercial_partner_id
|
||
customer = invoice.commercial_partner_id
|
||
|
||
economic_area = self.env.ref('base.europe').country_ids.mapped('code') + ['NO']
|
||
intracom_delivery = (customer.country_id.code in economic_area
|
||
and supplier.country_id.code in economic_area
|
||
and supplier.country_id != customer.country_id)
|
||
|
||
# [BR-IC-12]-In an Invoice with a VAT breakdown (BG-23) where the VAT category code (BT-118) is
|
||
# "Intra-community supply" the Deliver to country code (BT-80) shall not be blank.
|
||
|
||
# [BR-IC-11]-In an Invoice with a VAT breakdown (BG-23) where the VAT category code (BT-118) is
|
||
# "Intra-community supply" the Actual delivery date (BT-72) or the Invoicing period (BG-14)
|
||
# shall not be blank.
|
||
|
||
if intracom_delivery:
|
||
partner_shipping = invoice.partner_shipping_id or customer
|
||
|
||
return [{
|
||
'actual_delivery_date': invoice.invoice_date,
|
||
'delivery_location_vals': {
|
||
'delivery_address_vals': self._get_partner_address_vals(partner_shipping),
|
||
},
|
||
}]
|
||
|
||
return super()._get_delivery_vals_list(invoice)
|
||
|
||
def _get_partner_address_vals(self, partner):
|
||
# EXTENDS account.edi.xml.ubl_21
|
||
vals = super()._get_partner_address_vals(partner)
|
||
# schematron/openpeppol/3.13.0/xslt/CEN-EN16931-UBL.xslt
|
||
# [UBL-CR-225]-A UBL invoice should not include the AccountingCustomerParty Party PostalAddress CountrySubentityCode
|
||
vals.pop('country_subentity_code', None)
|
||
return vals
|
||
|
||
def _get_financial_institution_branch_vals(self, bank):
|
||
# EXTENDS account.edi.xml.ubl_21
|
||
vals = super()._get_financial_institution_branch_vals(bank)
|
||
# schematron/openpeppol/3.13.0/xslt/CEN-EN16931-UBL.xslt
|
||
# [UBL-CR-664]-A UBL invoice should not include the FinancialInstitutionBranch FinancialInstitution
|
||
# xpath test: not(//cac:FinancialInstitution)
|
||
vals.pop('id_attrs', None)
|
||
vals.pop('financial_institution_vals', None)
|
||
return vals
|
||
|
||
def _get_invoice_payment_means_vals_list(self, invoice):
|
||
# EXTENDS account.edi.xml.ubl_21
|
||
vals_list = super()._get_invoice_payment_means_vals_list(invoice)
|
||
|
||
for vals in vals_list:
|
||
vals.pop('payment_due_date', None)
|
||
vals.pop('instruction_id', None)
|
||
if vals.get('payment_id_vals'):
|
||
vals['payment_id_vals'] = vals['payment_id_vals'][:1]
|
||
|
||
return vals_list
|
||
|
||
def _get_tax_category_list(self, invoice, taxes):
|
||
# EXTENDS account.edi.xml.ubl_21
|
||
vals_list = super()._get_tax_category_list(invoice, taxes)
|
||
|
||
for vals in vals_list:
|
||
vals.pop('name')
|
||
# [UBL-CR-601]-A UBL invoice should not include the InvoiceLine Item ClassifiedTaxCategory TaxExemptionReason
|
||
#vals.pop('tax_exemption_reason')
|
||
|
||
return vals_list
|
||
|
||
def _get_invoice_tax_totals_vals_list(self, invoice, taxes_vals):
|
||
# EXTENDS account.edi.xml.ubl_21
|
||
vals_list = super()._get_invoice_tax_totals_vals_list(invoice, taxes_vals)
|
||
|
||
for vals in vals_list:
|
||
vals['currency_dp'] = 2
|
||
for subtotal_vals in vals.get('tax_subtotal_vals', []):
|
||
subtotal_vals.pop('percent', None)
|
||
subtotal_vals['currency_dp'] = 2
|
||
|
||
return vals_list
|
||
|
||
def _get_invoice_line_allowance_vals_list(self, line, tax_values_list=None):
|
||
# EXTENDS account.edi.xml.ubl_21
|
||
vals_list = super()._get_invoice_line_allowance_vals_list(line, tax_values_list=tax_values_list)
|
||
|
||
for vals in vals_list:
|
||
vals['currency_dp'] = 2
|
||
|
||
return vals_list
|
||
|
||
def _get_invoice_line_vals(self, line, line_id, taxes_vals):
|
||
# EXTENDS account.edi.xml.ubl_21
|
||
vals = super()._get_invoice_line_vals(line, line_id, taxes_vals)
|
||
|
||
vals.pop('tax_total_vals', None)
|
||
|
||
vals['currency_dp'] = 2
|
||
vals['price_vals']['currency_dp'] = 2
|
||
|
||
return vals
|
||
|
||
def _export_invoice_vals(self, invoice):
|
||
# EXTENDS account.edi.xml.ubl_21
|
||
vals = super()._export_invoice_vals(invoice)
|
||
|
||
vals['vals'].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',
|
||
'currency_dp': 2,
|
||
'ubl_version_id': None,
|
||
})
|
||
vals['vals']['monetary_total_vals']['currency_dp'] = 2
|
||
|
||
# [NL-R-001] For suppliers in the Netherlands, if the document is a creditnote, the document MUST
|
||
# contain an invoice reference (cac:BillingReference/cac:InvoiceDocumentReference/cbc:ID)
|
||
if vals['supplier'].country_id.code == 'NL' and 'refund' in invoice.move_type:
|
||
vals['vals'].update({
|
||
'billing_reference_vals': {
|
||
'id': invoice.ref,
|
||
'issue_date': None,
|
||
}
|
||
})
|
||
|
||
return vals
|
||
|
||
def _export_invoice_constraints(self, invoice, vals):
|
||
# EXTENDS account.edi.xml.ubl_21
|
||
constraints = super()._export_invoice_constraints(invoice, vals)
|
||
|
||
constraints.update({
|
||
'peppol_eas_is_set_supplier': self._check_required_fields(vals['supplier'], 'peppol_eas'),
|
||
'peppol_eas_is_set_customer': self._check_required_fields(vals['customer'], 'peppol_eas'),
|
||
'peppol_endpoint_is_set_supplier': self._check_required_fields(vals['supplier'], 'peppol_endpoint'),
|
||
'peppol_endpoint_is_set_customer': self._check_required_fields(vals['customer'], 'peppol_endpoint'),
|
||
})
|
||
|
||
constraints.update(
|
||
self._invoice_constraints_peppol_en16931_ubl(invoice, vals)
|
||
)
|
||
constraints.update(
|
||
self._invoice_constraints_cen_en16931_ubl(invoice, vals)
|
||
)
|
||
|
||
return constraints
|
||
|
||
def _invoice_constraints_cen_en16931_ubl(self, invoice, vals):
|
||
"""
|
||
corresponds to the errors raised by ' schematron/openpeppol/3.13.0/xslt/CEN-EN16931-UBL.xslt' for invoices.
|
||
This xslt was obtained by transforming the corresponding sch
|
||
https://docs.peppol.eu/poacc/billing/3.0/files/CEN-EN16931-UBL.sch.
|
||
"""
|
||
eu_countries = self.env.ref('base.europe').country_ids
|
||
intracom_delivery = (vals['customer'].country_id in eu_countries
|
||
and vals['supplier'].country_id in eu_countries
|
||
and vals['customer'].country_id != vals['supplier'].country_id)
|
||
|
||
constraints = {
|
||
# [BR-61]-If the Payment means type code (BT-81) means SEPA credit transfer, Local credit transfer or
|
||
# Non-SEPA international credit transfer, the Payment account identifier (BT-84) shall be present.
|
||
# note: Payment account identifier is <cac:PayeeFinancialAccount>
|
||
# note: no need to check account_number, because it's a required field for a partner_bank
|
||
'cen_en16931_payment_account_identifier': self._check_required_fields(
|
||
invoice, 'partner_bank_id'
|
||
) if vals['vals']['payment_means_vals_list'][0]['payment_means_code'] in (30, 58) else None,
|
||
# [BR-IC-12]-In an Invoice with a VAT breakdown (BG-23) where the VAT category code (BT-118) is
|
||
# "Intra-community supply" the Deliver to country code (BT-80) shall not be blank.
|
||
'cen_en16931_delivery_country_code': self._check_required_fields(
|
||
vals['vals']['delivery_vals_list'][0], 'delivery_location_vals',
|
||
_("For intracommunity supply, the delivery address should be included.")
|
||
) if intracom_delivery else None,
|
||
|
||
# [BR-IC-11]-In an Invoice with a VAT breakdown (BG-23) where the VAT category code (BT-118) is
|
||
# "Intra-community supply" the Actual delivery date (BT-72) or the Invoicing period (BG-14)
|
||
# shall not be blank.
|
||
'cen_en16931_delivery_date_invoicing_period': self._check_required_fields(
|
||
vals['vals']['delivery_vals_list'][0], 'actual_delivery_date',
|
||
_("For intracommunity supply, the actual delivery date or the invoicing period should be included.")
|
||
) and self._check_required_fields(
|
||
vals['vals']['invoice_period_vals_list'][0], ['start_date', 'end_date'],
|
||
_("For intracommunity supply, the actual delivery date or the invoicing period should be included.")
|
||
) if intracom_delivery else None,
|
||
}
|
||
|
||
for line_vals in vals['vals']['line_vals']:
|
||
if not line_vals['item_vals'].get('name'):
|
||
# [BR-25]-Each Invoice line (BG-25) shall contain the Item name (BT-153).
|
||
constraints.update({'cen_en16931_item_name': _("Each invoice line should have a product or a label.")})
|
||
break
|
||
|
||
for line in invoice.invoice_line_ids.filtered(lambda x: x.display_type not in ('line_note', 'line_section')):
|
||
if invoice.currency_id.compare_amounts(line.price_unit, 0) == -1:
|
||
# [BR-27]-The Item net price (BT-146) shall NOT be negative.
|
||
constraints.update({'cen_en16931_positive_item_net_price': _(
|
||
"The invoice contains line(s) with a negative unit price, which is not allowed."
|
||
" You might need to set a negative quantity instead.")})
|
||
if len(line.tax_ids.flatten_taxes_hierarchy().filtered(lambda t: t.amount_type != 'fixed')) != 1:
|
||
# [UBL-SR-48]-Invoice lines shall have one and only one classified tax category.
|
||
# /!\ exception: possible to have any number of ecotaxes (fixed tax) with a regular percentage tax
|
||
constraints.update({'cen_en16931_tax_line': _("Each invoice line shall have one and only one tax.")})
|
||
|
||
for role in ('supplier', 'customer'):
|
||
constraints[f'cen_en16931_{role}_country'] = self._check_required_fields(vals[role], 'country_id')
|
||
scheme_vals = vals['vals'][f'accounting_{role}_party_vals']['party_vals']['party_tax_scheme_vals'][-1:]
|
||
if (
|
||
not (scheme_vals and scheme_vals[0]['company_id'] and scheme_vals[0]['company_id'][:2].isalpha())
|
||
and (scheme_vals and scheme_vals[0]['tax_scheme_vals'].get('id') == 'VAT')
|
||
and self._name in ('account.edi.xml.ubl_bis3', 'account.edi.xml.ubl_nl', 'account.edi.xml.ubl_de')
|
||
):
|
||
# [BR-CO-09]-The Seller VAT identifier (BT-31), the Seller tax representative VAT identifier (BT-63)
|
||
# and the Buyer VAT identifier (BT-48) shall have a prefix in accordance with ISO code ISO 3166-1
|
||
# alpha-2 by which the country of issue may be identified. Nevertheless, Greece may use the prefix ‘EL’.
|
||
constraints.update({f'cen_en16931_{role}_vat_country_code': _(
|
||
"The VAT of the %s should be prefixed with its country code.", role)})
|
||
|
||
return constraints
|
||
|
||
def _invoice_constraints_peppol_en16931_ubl(self, invoice, vals):
|
||
"""
|
||
corresponds to the errors raised by 'schematron/openpeppol/3.13.0/xslt/PEPPOL-EN16931-UBL.xslt' for
|
||
invoices in ecosio. This xslt was obtained by transforming the corresponding sch
|
||
https://docs.peppol.eu/poacc/billing/3.0/files/PEPPOL-EN16931-UBL.sch.
|
||
|
||
The national rules (https://docs.peppol.eu/poacc/billing/3.0/bis/#national_rules) are included in this file.
|
||
They always refer to the supplier's country.
|
||
"""
|
||
constraints = {
|
||
# PEPPOL-EN16931-R003: A buyer reference or purchase order reference MUST be provided.
|
||
'peppol_en16931_ubl_buyer_ref_po_ref':
|
||
"A buyer reference or purchase order reference must be provided." if self._check_required_fields(
|
||
vals['vals'], 'buyer_reference'
|
||
) and self._check_required_fields(vals['vals'], 'order_reference') else None,
|
||
}
|
||
|
||
if vals['supplier'].country_id.code == 'NL':
|
||
constraints.update({
|
||
# [NL-R-001] For suppliers in the Netherlands, if the document is a creditnote, the document MUST contain
|
||
# an invoice reference (cac:BillingReference/cac:InvoiceDocumentReference/cbc:ID)
|
||
'nl_r_001': self._check_required_fields(invoice, 'ref') if 'refund' in invoice.move_type else '',
|
||
|
||
# [NL-R-002] For suppliers in the Netherlands the supplier’s address (cac:AccountingSupplierParty/cac:Party
|
||
# /cac:PostalAddress) MUST contain street name (cbc:StreetName), city (cbc:CityName) and post code (cbc:PostalZone)
|
||
'nl_r_002_street': self._check_required_fields(vals['supplier'], 'street'),
|
||
'nl_r_002_zip': self._check_required_fields(vals['supplier'], 'zip'),
|
||
'nl_r_002_city': self._check_required_fields(vals['supplier'], 'city'),
|
||
|
||
# [NL-R-003] For suppliers in the Netherlands, the legal entity identifier MUST be either a
|
||
# KVK or OIN number (schemeID 0106 or 0190)
|
||
'nl_r_003': _(
|
||
"%s should have a KVK or OIN number: the Peppol e-address (EAS) should be '0106' or '0190'.",
|
||
vals['supplier'].display_name
|
||
) if vals['supplier'].peppol_eas not in ('0106', '0190') else '',
|
||
|
||
# [NL-R-007] For suppliers in the Netherlands, the supplier MUST provide a means of payment
|
||
# (cac:PaymentMeans) if the payment is from customer to supplier
|
||
'nl_r_007': self._check_required_fields(invoice, 'partner_bank_id')
|
||
})
|
||
|
||
if vals['customer'].country_id.code == 'NL':
|
||
constraints.update({
|
||
# [NL-R-004] For suppliers in the Netherlands, if the customer is in the Netherlands, the customer
|
||
# address (cac:AccountingCustomerParty/cac:Party/cac:PostalAddress) MUST contain the street name
|
||
# (cbc:StreetName), the city (cbc:CityName) and post code (cbc:PostalZone)
|
||
'nl_r_004_street': self._check_required_fields(vals['customer'], 'street'),
|
||
'nl_r_004_city': self._check_required_fields(vals['customer'], 'city'),
|
||
'nl_r_004_zip': self._check_required_fields(vals['customer'], 'zip'),
|
||
|
||
# [NL-R-005] For suppliers in the Netherlands, if the customer is in the Netherlands,
|
||
# the customer’s legal entity identifier MUST be either a KVK or OIN number (schemeID 0106 or 0190)
|
||
'nl_r_005': _(
|
||
"%s should have a KVK or OIN number: the Peppol e-address (EAS) should be '0106' or '0190'.",
|
||
vals['customer'].display_name
|
||
) if vals['customer'].peppol_eas not in ('0106', '0190') else '',
|
||
})
|
||
|
||
if vals['supplier'].country_id.code == 'NO':
|
||
vat = vals['supplier'].vat
|
||
constraints.update({
|
||
# NO-R-001: For Norwegian suppliers, a VAT number MUST be the country code prefix NO followed by a
|
||
# valid Norwegian organization number (nine numbers) followed by the letters MVA.
|
||
# Note: mva.is_valid("179728982MVA") is True while it lacks the NO prefix
|
||
'no_r_001': _(
|
||
"The VAT number of the supplier does not seem to be valid. It should be of the form: NO179728982MVA."
|
||
) if not mva.is_valid(vat) or len(vat) != 14 or vat[:2] != 'NO' or vat[-3:] != 'MVA' else "",
|
||
|
||
'no_supplier_bronnoysund': _(
|
||
"The supplier %s must have a Bronnoysund company registry.",
|
||
vals['supplier'].display_name
|
||
) if 'l10n_no_bronnoysund_number' not in vals['supplier']._fields or not vals['supplier'].l10n_no_bronnoysund_number else "",
|
||
})
|
||
if vals['customer'].country_id.code == 'NO':
|
||
constraints.update({
|
||
'no_customer_bronnoysund': _(
|
||
"The supplier %s must have a Bronnoysund company registry.",
|
||
vals['customer'].display_name
|
||
) if 'l10n_no_bronnoysund_number' not in vals['customer']._fields or not vals['customer'].l10n_no_bronnoysund_number else "",
|
||
})
|
||
|
||
return constraints
|