1
0
forked from Mapan/odoo17e
odoo17e-kedaikipas58/addons/account_sepa/models/account_journal.py
2024-12-10 09:04:09 +07:00

589 lines
28 KiB
Python

# Part of Odoo. See LICENSE file for full copyright and licensing details.
import re
import time
from collections import defaultdict
from lxml import etree
from odoo import _, api, fields, models
from odoo.exceptions import UserError, ValidationError
from odoo.tools import float_repr, float_round
import odoo.addons.account.tools.structured_reference as sr
from odoo.addons.account_batch_payment.models import sepa_mapping
def sanitize_communication(communication, size=140):
# DEPRECATED - to be removed in master
return sepa_mapping.sanitize_communication(communication, size)
class AccountJournal(models.Model):
_inherit = "account.journal"
sepa_pain_version = fields.Selection(
[
('pain.001.001.03', 'Generic'),
('pain.001.001.03.austrian.004', 'Austrian'),
('pain.001.001.03.de', 'German'),
('pain.001.001.03.se', 'Swedish'),
('pain.001.001.03.ch.02', 'Swiss'),
('pain.001.001.09', 'New generic version (09)'),
('iso_20022', 'ISO 20022'),
],
string='SEPA Pain Version',
readonly=False,
store=True,
compute='_compute_sepa_pain_version',
help='SEPA may be a generic format, some countries differ from the '
'SEPA recommendations made by the EPC (European Payment Council) '
'and thus the XML created need some tweaking.'
)
has_sepa_ct_payment_method = fields.Boolean(compute='_compute_has_sepa_ct_payment_method')
@api.depends('bank_acc_number', 'country_code', 'company_id.country_code')
def _compute_sepa_pain_version(self):
""" Set default value for the field sepa_pain_version"""
pains_by_country = {
'DE': 'pain.001.001.03.de',
'CH': 'pain.001.001.03.ch.02',
'SE': 'pain.001.001.03.se',
'AT': 'pain.001.001.03.austrian.004',
}
for rec in self:
# First try to retrieve the country_code from the IBAN
if rec.bank_acc_number and re.match('^[A-Z]{2}[0-9]{2}.*', rec.bank_acc_number):
country_code = rec.bank_acc_number[:2]
# Then try from the company's fiscal country, and finally from the company's country
else:
country_code = rec.country_code or rec.company_id.country_code or ""
rec.sepa_pain_version = pains_by_country.get(country_code, 'pain.001.001.03')
@api.depends('outbound_payment_method_line_ids.payment_method_id.code')
def _compute_has_sepa_ct_payment_method(self):
for rec in self:
rec.has_sepa_ct_payment_method = any(
payment_method.payment_method_id.code == 'sepa_ct'
for payment_method in rec.outbound_payment_method_line_ids
)
def _default_outbound_payment_methods(self):
res = super()._default_outbound_payment_methods()
if self._is_payment_method_available('sepa_ct'):
res |= self.env.ref('account_sepa.account_payment_method_sepa_ct')
return res
def create_iso20022_credit_transfer(self, payments, batch_booking=False, sct_generic=False):
"""
This method creates the body of the XML file for the SEPA document.
It returns the content of the XML file.
"""
pain_version = self.sepa_pain_version
if payments and pain_version == 'pain.001.001.09' and any(not payment['sepa_uetr'] for payment in payments):
raise UserError(_("Some payments are missing a value for 'UETR', required for the SEPA Pain.001.001.09 format."))
Document = self._get_document(pain_version)
CstmrCdtTrfInitn = etree.SubElement(Document, "CstmrCdtTrfInitn")
# Create the GrpHdr XML block
GrpHdr = etree.SubElement(CstmrCdtTrfInitn, "GrpHdr")
MsgId = etree.SubElement(GrpHdr, "MsgId")
val_MsgId = str(time.time())
MsgId.text = val_MsgId
CreDtTm = etree.SubElement(GrpHdr, "CreDtTm")
CreDtTm.text = time.strftime("%Y-%m-%dT%H:%M:%S")
NbOfTxs = etree.SubElement(GrpHdr, "NbOfTxs")
val_NbOfTxs = str(len(payments))
if len(val_NbOfTxs) > 15:
raise ValidationError(_("Too many transactions for a single file."))
NbOfTxs.text = val_NbOfTxs
CtrlSum = etree.SubElement(GrpHdr, "CtrlSum")
CtrlSum.text = self._get_CtrlSum(payments)
GrpHdr.append(self._get_InitgPty(pain_version, sct_generic))
# Create one PmtInf XML block per execution date, per currency
payments_date_instr_wise = defaultdict(lambda: [])
today = fields.Date.today()
for payment in payments:
required_payment_date = payment['payment_date'] if payment['payment_date'] > today else today
currency = payment['currency_id'] or self.company_id.currency_id.id
payments_date_instr_wise[(required_payment_date, currency)].append(payment)
count = 0
for (payment_date, currency), payments_list in payments_date_instr_wise.items():
count += 1
PmtInf = etree.SubElement(CstmrCdtTrfInitn, "PmtInf")
PmtInfId = etree.SubElement(PmtInf, "PmtInfId")
PmtInfId.text = (val_MsgId + str(self.id) + str(count))[-30:]
PmtMtd = etree.SubElement(PmtInf, "PmtMtd")
PmtMtd.text = 'TRF'
BtchBookg = etree.SubElement(PmtInf, "BtchBookg")
BtchBookg.text = batch_booking and 'true' or 'false'
NbOfTxs = etree.SubElement(PmtInf, "NbOfTxs")
NbOfTxs.text = str(len(payments_list))
CtrlSum = etree.SubElement(PmtInf, "CtrlSum")
CtrlSum.text = self._get_CtrlSum(payments_list)
PmtTpInf = self._get_PmtTpInf(sct_generic)
if len(PmtTpInf) != 0: #Boolean conversion from etree element triggers a deprecation warning ; this is the proper way
PmtInf.append(PmtTpInf)
ReqdExctnDt = etree.SubElement(PmtInf, "ReqdExctnDt")
if pain_version == "pain.001.001.09":
Dt = etree.SubElement(ReqdExctnDt, "Dt")
Dt.text = fields.Date.to_string(payment_date)
else:
ReqdExctnDt.text = fields.Date.to_string(payment_date)
PmtInf.append(self._get_Dbtr(pain_version, sct_generic))
PmtInf.append(self._get_DbtrAcct())
DbtrAgt = etree.SubElement(PmtInf, "DbtrAgt")
FinInstnId = etree.SubElement(DbtrAgt, "FinInstnId")
bank_account = self.bank_account_id
bic_code = self._get_cleaned_bic_code(bank_account)
if pain_version in ['pain.001.001.03.se', 'pain.001.001.03.ch.02'] and not bic_code:
raise UserError(_("Bank account %s 's bank does not have any BIC number associated. Please define one.", bank_account.sanitized_acc_number))
bic_tag = pain_version == "pain.001.001.09" and "BICFI" or "BIC"
if bic_code:
BIC = etree.SubElement(FinInstnId, bic_tag)
BIC.text = bic_code
else:
Othr = etree.SubElement(FinInstnId, "Othr")
Id = etree.SubElement(Othr, "Id")
Id.text = "NOTPROVIDED"
PmtInf.append(self._get_ChrgBr(sct_generic))
# One CdtTrfTxInf per transaction
for payment in payments_list:
PmtInf.append(self._get_CdtTrfTxInf(PmtInfId, payment, sct_generic, pain_version))
return etree.tostring(Document, pretty_print=True, xml_declaration=True, encoding='utf-8')
def _get_document(self, pain_version):
if pain_version == 'pain.001.001.03.ch.02':
Document = self._create_pain_001_001_03_ch_document()
elif pain_version == 'pain.001.001.09':
Document = self._create_iso20022_document('pain.001.001.09')
else: #The German version will also use the create_pain_001_001_03_document since the version 001.003.03 is deprecated
Document = self._create_pain_001_001_03_document()
return Document
def _create_pain_001_001_03_document(self):
""" Create a sepa credit transfer file that follows the European Payment Councile generic guidelines (pain.001.001.03)
:param doc_payments: recordset of account.payment to be exported in the XML document returned
"""
Document = self._create_iso20022_document('pain.001.001.03')
return Document
def _create_pain_001_001_03_ch_document(self):
""" Create a sepa credit transfer file that follows the swiss specific guidelines, as established
by SIX Interbank Clearing (pain.001.001.03.ch.02)
:param doc_payments: recordset of account.payment to be exported in the XML document returned
"""
Document = etree.Element("Document", nsmap={
None: "http://www.six-interbank-clearing.com/de/pain.001.001.03.ch.02.xsd",
'xsi': "http://www.w3.org/2001/XMLSchema-instance"})
return Document
def _create_iso20022_document(self, pain_version):
return etree.Element("Document", nsmap={
None: "urn:iso:std:iso:20022:tech:xsd:%s" % (pain_version,),
'xsi': "http://www.w3.org/2001/XMLSchema-instance"})
def _get_CtrlSum(self, payments):
return float_repr(float_round(sum(payment['amount'] for payment in payments), 2), 2)
def _get_InitgPty(self, pain_version, sct_generic=False):
InitgPty = etree.Element("InitgPty")
if pain_version == 'pain.001.001.03.se':
InitgPty.extend(self._get_company_PartyIdentification32(sct_generic, org_id=True, postal_address=False, issr=False, nm=False, schme_nm='BANK'))
elif pain_version == 'pain.001.001.03.austrian.004':
InitgPty.extend(self._get_company_PartyIdentification32(sct_generic, org_id=True, postal_address=False, issr=False))
else:
InitgPty.extend(self._get_company_PartyIdentification32(sct_generic, org_id=True, postal_address=False, issr=True))
return InitgPty
def _get_company_PartyIdentification32(self, sct_generic=False, org_id=True, postal_address=True, nm=True, issr=True, schme_nm=False):
""" Returns a PartyIdentification32 element identifying the current journal's company
"""
ret = []
company = self.company_id
name_length = sct_generic and 35 or 70
if nm:
Nm = etree.Element("Nm")
if company.sepa_initiating_party_name:
company_name = company.sepa_initiating_party_name[:name_length]
else:
company_name = company.name[:name_length]
Nm.text = sanitize_communication(company_name)
ret.append(Nm)
if postal_address:
PstlAdr = self._get_PstlAdr(company.partner_id)
if PstlAdr is not None:
ret.append(PstlAdr)
if org_id and company.sepa_orgid_id:
Id = etree.Element("Id")
OrgId = etree.SubElement(Id, "OrgId")
if self.sepa_pain_version == "pain.001.001.09" and self.company_id.account_sepa_lei:
LEI = etree.SubElement(OrgId, "LEI")
LEI.text = self.company_id.account_sepa_lei
Othr = etree.SubElement(OrgId, "Othr")
_Id = etree.SubElement(Othr, "Id")
_Id.text = sanitize_communication(company.sepa_orgid_id)
if issr and company.sepa_orgid_issr:
Issr = etree.SubElement(Othr, "Issr")
Issr.text = sanitize_communication(company.sepa_orgid_issr)
if schme_nm:
SchmeNm = etree.SubElement(Othr, "SchmeNm")
Cd = etree.SubElement(SchmeNm, "Cd")
Cd.text = schme_nm
ret.append(Id)
return ret
def _get_PmtTpInf(self, sct_generic=False):
PmtTpInf = etree.Element("PmtTpInf")
is_salary = self.env.context.get('sepa_payroll_sala')
if is_salary:
# The "High" priority level is also an attribute of the payment
# that we should specify as well for salary payments
# See https://www.febelfin.be/sites/default/files/2019-04/standard-credit_transfer-xml-v32-en_0.pdf section 2.6
InstrPrty = etree.SubElement(PmtTpInf, "InstrPrty")
InstrPrty.text = 'HIGH'
if self.sepa_pain_version != 'pain.001.001.03.ch.02':
SvcLvl = etree.SubElement(PmtTpInf, "SvcLvl")
Cd = etree.SubElement(SvcLvl, "Cd")
Cd.text = 'NURG' if sct_generic else 'SEPA'
if is_salary:
# The SALA purpose code is standard for all SEPA, and guarantees a series
# of things in instant payment: https://www.sepaforcorporates.com/sepa-payments/sala-sepa-salary-payments.
CtgyPurp = etree.SubElement(PmtTpInf, "CtgyPurp")
Cd = etree.SubElement(CtgyPurp, "Cd")
Cd.text = 'SALA'
return PmtTpInf
def _get_Dbtr(self, pain_version, sct_generic=False):
Dbtr = etree.Element("Dbtr")
if pain_version == "pain.001.001.03.se":
Dbtr.extend(self._get_company_PartyIdentification32(sct_generic, org_id=True, postal_address=True, issr=False, schme_nm="CUST"))
else:
Dbtr.extend(self._get_company_PartyIdentification32(sct_generic, org_id=not sct_generic, postal_address=True))
return Dbtr
def _get_DbtrAcct(self):
DbtrAcct = etree.Element("DbtrAcct")
Id = etree.SubElement(DbtrAcct, "Id")
IBAN = etree.SubElement(Id, "IBAN")
IBAN.text = self.bank_account_id.sanitized_acc_number
Ccy = etree.SubElement(DbtrAcct, "Ccy")
Ccy.text = self.currency_id and self.currency_id.name or self.company_id.currency_id.name
return DbtrAcct
def _get_PstlAdr(self, partner_id):
pstl_addr_list = partner_id._get_all_addr()
pstl_addr_list = [addr for addr in pstl_addr_list if addr['country']]
if not partner_id.is_company:
if not pstl_addr_list:
return None
pstl_addr_list = [
addr for addr in pstl_addr_list if (
addr['city'] or
# SE only needs country
self.sepa_pain_version == 'pain.001.001.03.se'
)
]
if not pstl_addr_list:
return None
pstl_addr = None
if len(pstl_addr_list) > 1:
for addr_dict in pstl_addr_list:
if addr_dict['contact_type'] == 'employee':
pstl_addr = addr_dict
pstl_addr = pstl_addr or pstl_addr_list[0]
else:
if not pstl_addr_list:
raise ValidationError(_('Partner %s has no country code defined.', partner_id.name))
pstl_addr = pstl_addr_list[0]
PstlAdr = etree.Element("PstlAdr")
if self.sepa_pain_version == 'pain.001.001.09':
for node_name, attr, size in [('StrtNm', 'street', 70), ('PstCd', 'zip', 140), ('TwnNm', 'city', 140)]:
if pstl_addr[attr]:
address_element = etree.SubElement(PstlAdr, node_name)
address_element.text = sanitize_communication(pstl_addr[attr], size)
Ctry = etree.SubElement(PstlAdr, 'Ctry')
Ctry.text = pstl_addr['country']
if self.sepa_pain_version != 'pain.001.001.09':
# Some banks seem allergic to having the zip in a separate tag, so we do as before
if pstl_addr['street']:
AdrLine = etree.SubElement(PstlAdr, 'AdrLine')
AdrLine.text = sanitize_communication(pstl_addr['street'], 70)
if pstl_addr['zip'] and pstl_addr['city']:
AdrLine = etree.SubElement(PstlAdr, 'AdrLine')
AdrLine.text = sanitize_communication(pstl_addr['zip'] + ' ' + pstl_addr['city'], 70)
return PstlAdr
def _skip_CdtrAgt(self, partner_bank, pain_version):
return (
not partner_bank.bank_id.bic
or (
# Creditor Agent can be omitted with IBAN and QR-IBAN accounts
pain_version == 'pain.001.001.03.ch.02'
and self._is_qr_iban({'partner_bank_id' : partner_bank.id, 'journal_id' : self.id})
)
)
def _get_CdtTrfTxInf(self, PmtInfId, payment, sct_generic, pain_version):
CdtTrfTxInf = etree.Element("CdtTrfTxInf")
PmtId = etree.SubElement(CdtTrfTxInf, "PmtId")
if payment['name']:
InstrId = etree.SubElement(PmtId, "InstrId")
InstrId.text = sanitize_communication(payment['name'], 35)
EndToEndId = etree.SubElement(PmtId, "EndToEndId")
EndToEndId.text = (PmtInfId.text + str(payment['id']))[-30:].strip()
Amt = etree.SubElement(CdtTrfTxInf, "Amt")
currency_id = self.env['res.currency'].search([('id', '=', payment['currency_id'])], limit=1)
journal_id = self.env['account.journal'].search([('id', '=', payment['journal_id'])], limit=1)
val_Ccy = currency_id and currency_id.name or journal_id.company_id.currency_id.name
val_InstdAmt = float_repr(float_round(payment['amount'], 2), 2)
max_digits = val_Ccy == 'EUR' and 11 or 15
if len(re.sub(r'\.', '', val_InstdAmt)) > max_digits:
raise ValidationError(_(
"The amount of the payment '%(payment)s' is too high. The maximum permitted is %(limit)s.",
payment=payment['name'],
limit=str(9) * (max_digits - 2) + ".99",
))
InstdAmt = etree.SubElement(Amt, "InstdAmt", Ccy=val_Ccy)
InstdAmt.text = val_InstdAmt
partner = self.env['res.partner'].sudo().browse(payment['partner_id'])
partner_bank_id = payment.get('partner_bank_id')
if not partner_bank_id:
raise UserError(_('Partner %s has not bank account defined.', partner.name))
partner_bank = self.env['res.partner.bank'].sudo().browse(partner_bank_id)
if not self._skip_CdtrAgt(partner_bank, pain_version):
CdtTrfTxInf.append(self._get_CdtrAgt(partner_bank, sct_generic, pain_version))
Cdtr = etree.SubElement(CdtTrfTxInf, "Cdtr")
Nm = etree.SubElement(Cdtr, "Nm")
Nm.text = sanitize_communication((
partner_bank.acc_holder_name or partner.name or partner.commercial_partner_id.name or '/'
)[:70]).strip() or '/'
PstlAdr = self._get_PstlAdr(partner)
if PstlAdr is not None:
Cdtr.append(PstlAdr)
CdtTrfTxInf.append(self._get_CdtrAcct(partner_bank, sct_generic))
val_RmtInf = self._get_RmtInf(payment)
if val_RmtInf is not False:
CdtTrfTxInf.append(val_RmtInf)
if self.sepa_pain_version == "pain.001.001.09":
UETR = etree.SubElement(PmtId, "UETR")
UETR.text = payment["sepa_uetr"]
return CdtTrfTxInf
def _get_ChrgBr(self, sct_generic):
ChrgBr = etree.Element("ChrgBr")
ChrgBr.text = sct_generic and "SHAR" or "SLEV"
return ChrgBr
def _get_CdtrAgt(self, bank_account, sct_generic, pain_version):
CdtrAgt = etree.Element("CdtrAgt")
FinInstnId = etree.SubElement(CdtrAgt, "FinInstnId")
bic_code = self._get_cleaned_bic_code(bank_account)
if bic_code:
BIC = etree.SubElement(FinInstnId, "BIC")
BIC.text = bic_code
if self.sepa_pain_version == "pain.001.001.09":
BIC.tag = "BICFI"
partner_lei = bank_account.partner_id.account_sepa_lei
if self.sepa_pain_version == "pain.001.001.09" and partner_lei:
# LEI needs to be inserted after BIC
LEI = etree.SubElement(FinInstnId, "LEI")
LEI.text = partner_lei
if not bic_code:
if pain_version in ['pain.001.001.03.austrian.004', 'pain.001.001.03.ch.02']:
# Othr and NOTPROVIDED are not supported in CdtrAgt by those flavours
raise UserError(_("The bank defined on account %s (from partner %s) has no BIC. Please first set one.", bank_account.acc_number, bank_account.partner_id.name))
Othr = etree.SubElement(FinInstnId, "Othr")
Id = etree.SubElement(Othr, "Id")
Id.text = "NOTPROVIDED"
return CdtrAgt
def _get_CdtrAcct(self, bank_account, sct_generic):
if not sct_generic and (not bank_account.acc_type or not bank_account.acc_type == 'iban'):
raise UserError(_("The account %s, linked to partner '%s', is not of type IBAN.\nA valid IBAN account is required to use SEPA features.", bank_account.acc_number, bank_account.partner_id.name))
CdtrAcct = etree.Element("CdtrAcct")
Id = etree.SubElement(CdtrAcct, "Id")
if sct_generic and bank_account.acc_type != 'iban':
Othr = etree.SubElement(Id, "Othr")
_Id = etree.SubElement(Othr, "Id")
acc_number = bank_account.acc_number
# CH case when when we have non-unique account numbers
if " " in bank_account.sanitized_acc_number and " " in bank_account.acc_number:
acc_number = bank_account.acc_number.split(" ")[0]
_Id.text = acc_number
else:
IBAN = etree.SubElement(Id, "IBAN")
IBAN.text = bank_account.sanitized_acc_number
return CdtrAcct
def _get_RmtInf(self, payment):
def detect_reference_type(reference, partner_country_code):
if partner_country_code == 'BE' and sr.is_valid_structured_reference_be(reference):
return 'be'
elif self._is_qr_iban(payment):
return 'ch'
elif partner_country_code == 'FI' and sr.is_valid_structured_reference_fi(reference):
return 'fi'
elif partner_country_code == 'NO' and sr.is_valid_structured_reference_no_se(reference):
return 'no'
elif partner_country_code == 'SE' and sr.is_valid_structured_reference_no_se(reference):
return 'se'
elif sr.is_valid_structured_reference_iso(reference):
return 'iso'
else:
return None
def get_strd_tree(ref, cd=None, prtry=None, issr=None):
strd_string = f"""
<Strd>
<CdtrRefInf>
<Tp>
<CdOrPrtry>
<Cd>{cd}</Cd>
<Prtry>{prtry}</Prtry>
</CdOrPrtry>
<Issr>{issr}</Issr>
</Tp>
<Ref>{ref}</Ref>
</CdtrRefInf>
</Strd>
"""
strd_tree = etree.fromstring(strd_string)
if not cd:
cd_tree = strd_tree.find('.//Cd')
cd_tree.getparent().remove(cd_tree)
if not prtry:
prtry_tree = strd_tree.find('.//Prtry')
prtry_tree.getparent().remove(prtry_tree)
if not issr:
issr_tree = strd_tree.find('.//Issr')
issr_tree.getparent().remove(issr_tree)
return strd_tree
if not payment['ref']:
return False
RmtInf = etree.Element('RmtInf')
ref = sr.sanitize_structured_reference(payment['ref'])
partner_country_code = payment.get('partner_country_code')
reference_type = detect_reference_type(ref, partner_country_code)
# Check whether we have a structured communication
if reference_type == 'iso':
RmtInf.append(get_strd_tree(ref, cd='SCOR', issr='ISO'))
elif reference_type == 'be':
RmtInf.append(get_strd_tree(ref, cd='SCOR', issr='BBA'))
elif reference_type == 'ch':
ref = ref.rjust(27, '0')
RmtInf.append(get_strd_tree(ref, prtry='QRR'))
elif reference_type in ('fi', 'no', 'se'):
RmtInf.append(get_strd_tree(ref, cd='SCOR'))
else:
Ustrd = etree.SubElement(RmtInf, "Ustrd")
Ustrd.text = sanitize_communication(payment['ref'])
# sanitize_communication() automatically removes leading slash
# characters in payment references, due to the requirements of
# European Payment Council, available here:
# https://www.europeanpaymentscouncil.eu/document-library/implementation-guidelines/sepa-credit-transfer-customer-psp-implementation
# (cfr Section 1.4 Character Set)
# However, the /A/ four-character prefix is a requirement of belgian law.
# The existence of such legal prefixes may be a reason why the leading slash
# is forbidden in normal SEPA payment references, to avoid conflicts.
# Legal references for Belgian salaries:
# https://www.ejustice.just.fgov.be/eli/loi/1967/10/10/1967101056/justel#Art.1411bis
# Article 1411bis of Belgian Judicial Code mandating the use of special codes
# for identifying payments of protected amounts, and the related penalties
# for payment originators, in case of misuse.
# https://www.ejustice.just.fgov.be/eli/arrete/2006/07/04/2006009525/moniteur
# Royal Decree defining "/A/ " as the code for salaries, in the context of
# Article 1411bis
if self.env.context.get('l10n_be_hr_payroll_sepa_salary_payment'):
Ustrd.text = f"/A/ {Ustrd.text}"
return RmtInf
def _is_qr_iban(self, payment_dict):
""" Tells if the bank account linked to the payment has a QR-IBAN account number.
QR-IBANs are specific identifiers used in Switzerland as references in
QR-codes. They are formed like regular IBANs, but are actually something
different.
"""
partner_bank = self.env['res.partner.bank'].browse(payment_dict['partner_bank_id'])
company = self.env['account.journal'].browse(payment_dict['journal_id']).company_id
iban = partner_bank.sanitized_acc_number
if (
partner_bank.acc_type != 'iban'
or (partner_bank.sanitized_acc_number or '')[:2] not in ('CH', 'LI')
or partner_bank.company_id.id not in (False, company.id)
or len(iban) < 9
):
return False
iid_start_index = 4
iid_end_index = 8
iid = iban[iid_start_index : iid_end_index+1]
return re.match(r'\d+', iid) \
and 30000 <= int(iid) <= 31999 # Those values for iid are reserved for QR-IBANs only
def _get_cleaned_bic_code(self, bank_account):
""" Checks if the BIC code is matching the pattern from the XSD to avoid
having files generated here that are refused by banks after.
It also returns a cleaned version of the BIC as a convenient use.
"""
if not bank_account.bank_bic:
return
regex = '[A-Z]{6,6}[A-Z2-9][A-NP-Z0-9]([A-Z0-9]{3,3}){0,1}'
if self.sepa_pain_version == 'pain.001.001.09':
regex = '[A-Z0-9]{4,4}[A-Z]{2,2}[A-Z0-9]{2,2}([A-Z0-9]{3,3}){0,1}'
if not re.match(regex, bank_account.bank_bic):
raise UserError(_("The BIC code '%s' associated to the bank '%s' of bank account '%s' "
"of partner '%s' does not respect the required convention.\n"
"It must contain 8 or 11 characters and match the following structure:\n"
"- 4 letters: institution code or bank code\n"
"- 2 letters: country code\n"
"- 2 letters or digits: location code\n"
"- 3 letters or digits: branch code, optional\n",
bank_account.bank_bic, bank_account.bank_id.name,
bank_account.sanitized_acc_number, bank_account.partner_id.name))
return bank_account.bank_bic.replace(' ', '').upper()