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