account_edi_ubl_cii/models/account_edi_xml_ubl_bis3.py

432 lines
21 KiB
Python
Raw Permalink Normal View History

# -*- 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 suppliers 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 customers 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