account_edi_ubl_cii/models/account_edi_xml_ubl_bis3.py

432 lines
21 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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