Files
enterprise-addons/account_iso20022/models/account_journal.py
T

598 lines
29 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 uuid import uuid4
from odoo import _, api, fields, models
from odoo.exceptions import UserError, ValidationError
from odoo.tools import float_repr, float_round
from odoo.addons.base_iban.models.res_partner_bank import get_iban_part
import odoo.addons.account.tools.structured_reference as sr
from odoo.addons.account_batch_payment.models.sepa_mapping import sanitize_communication
from odoo.addons.account_iso20022.models.account_payment import ISO20022_CHARGE_BEARER_SELECTION, ISO20022_PRIORITY_SELECTION, ISO20022_PRIORITY_HELP
class AccountJournal(models.Model):
_inherit = "account.journal"
sepa_pain_version = fields.Selection(
[
('pain.001.001.03.austrian.004', 'Austrian'),
('pain.001.001.03.de', 'German'),
('pain.001.001.09', 'pain.001.001.09'),
('pain.001.001.03', 'pain.001.001.03'),
],
string="SEPA XML Format",
compute='_compute_sepa_pain_version',
store=True,
readonly=False,
help="SEPA version to use to generate Credit Transfer XML files from this journal",
)
has_sepa_ct_payment_method = fields.Boolean(compute='_compute_has_sepa_ct_payment_method')
iso20022_default_priority = fields.Selection(
selection=ISO20022_PRIORITY_SELECTION,
string='Default Priority',
default='NORM',
help=ISO20022_PRIORITY_HELP,
required=True,
)
iso20022_charge_bearer = fields.Selection(
string="ISO 20022 Charge Bearer",
selection=ISO20022_CHARGE_BEARER_SELECTION,
default='SHAR',
help="Specifies which party/parties will bear the charges associated with the processing of ISO 20022 payment transactions from this journal."
)
has_iso20022_payment_method = fields.Boolean(compute='_compute_has_iso20022_payment_method')
# -------------------------------------------------------------------------
# COMPUTE METHODS
# -------------------------------------------------------------------------
@api.depends('bank_acc_number', 'company_id.account_fiscal_country_id', 'company_id.country_id')
def _compute_sepa_pain_version(self):
""" Set default value for the field sepa_pain_version"""
pains_by_country = {
'DE': 'pain.001.001.03.de',
'AT': 'pain.001.001.03.austrian.004',
}
for rec in self:
if rec.bank_account_id and rec.bank_account_id.acc_type == 'iban':
country_code = rec.bank_acc_number[:2].upper()
# Then try from the company's fiscal country, and finally from the company's country
else:
country_code = rec.company_id.account_fiscal_country_id.code or rec.company_id.country_code
if country_code in pains_by_country:
rec.sepa_pain_version = pains_by_country.get(country_code)
else:
# Having a sepa_pain_version set at pain.001.001.03 means that the user changed it manually,
# since the default is 09. In this case, we keep the user's change.
rec.sepa_pain_version = 'pain.001.001.09' \
if rec.sepa_pain_version != 'pain.001.001.03' \
else rec.sepa_pain_version
@api.depends('outbound_payment_method_line_ids.payment_method_id.code')
def _compute_has_sepa_ct_payment_method(self):
for rec in self:
payment_method_codes = rec.mapped('outbound_payment_method_line_ids.payment_method_id.code')
rec.has_sepa_ct_payment_method = (
'sepa_ct' in payment_method_codes
or 'iso20022_ch' in payment_method_codes and self.env['ir.config_parameter'].sudo().get_param('iso20022_ch_force_sepa')
)
@api.depends('outbound_payment_method_line_ids.payment_method_id.code')
def _compute_has_iso20022_payment_method(self):
for journal in self:
journal.has_iso20022_payment_method = any(
journal.mapped('outbound_payment_method_line_ids.payment_method_id.is_iso20022')
)
# -------------------------------------------------------------------------
# GENERIC OVERRIDES
# -------------------------------------------------------------------------
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_iso20022.account_payment_method_sepa_ct')
elif self._is_payment_method_available('iso20022_se'):
res |= self.env.ref('account_iso20022.account_payment_method_iso20022_se')
elif self._is_payment_method_available('iso20022_ch'):
res |= self.env.ref('account_iso20022.account_payment_method_iso20022_ch')
elif self._is_payment_method_available('iso20022_us'):
res |= self.env.ref('account_iso20022.account_payment_method_iso20022_us')
elif self._is_payment_method_available('iso20022'):
res |= self.env.ref('account_iso20022.account_payment_method_iso20022')
return res
# -------------------------------------------------------------------------
# DOCUMENT CREATION
# -------------------------------------------------------------------------
def create_iso20022_credit_transfer(self, payments, payment_method_code, batch_booking=False):
"""Returns the content of the XML file."""
Document = self.create_iso20022_credit_transfer_content(payments, payment_method_code, batch_booking=batch_booking)
return etree.tostring(Document, pretty_print=True, xml_declaration=True, encoding='utf-8')
def create_iso20022_credit_transfer_content(self, payments, payment_method_code, batch_booking=False):
"""
Creates the body of the XML file for the ISO20022 document.
"""
if self.sepa_pain_version not in self._fields['sepa_pain_version'].get_values(self.env):
# Might happen because of a migration issue (fixed in https://github.com/odoo/upgrade/pull/9771).
self.sepa_pain_version = 'pain.001.001.09'
Document = etree.Element("Document", nsmap={
None: self.get_document_namespace(payment_method_code),
'xsi': "http://www.w3.org/2001/XMLSchema-instance"
})
if schema_location := self._get_schema_location(payment_method_code):
Document.set(schema_location[0], schema_location[1])
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(payment_method_code))
# Create one PmtInf XML block per execution date, per currency
eur_currency = self.env.ref('base.EUR')
chf_currency = self.env.ref('base.CHF')
payments_date_instr_wise = defaultdict(list)
today = fields.Date.today()
iso20022_ch_force_sepa = self.env['ir.config_parameter'].sudo().get_param('iso20022_ch_force_sepa')
for payment in payments:
required_payment_date = max(payment['payment_date'], today)
currency_id = payment['currency_id'] or self.company_id.currency_id.id
priority = payment['iso20022_priority']
payments_date_instr_wise[required_payment_date, currency_id, priority].append(payment)
for count, ((payment_date, currency_id, priority), payments_list) in enumerate(payments_date_instr_wise.items()):
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)
group_payment_method_code = payment_method_code
if iso20022_ch_force_sepa and payment_method_code == 'iso20022_ch':
# The Swiss ISO20022 implementation considers SEPA as a subset of what it allows (payment type S),
# as well as more generic ISO20022 payments (payment type X). To handle that, we change the payment_method_code
# dynamically when adding the grouped payments to the XML file.
if currency_id == eur_currency.id:
group_payment_method_code = 'sepa_ct'
elif currency_id != chf_currency.id:
group_payment_method_code = 'iso20022'
PmtTpInf = self._get_PmtTpInf(group_payment_method_code, priority)
if len(PmtTpInf) != 0: # Boolean conversion from etree element triggers a deprecation warning ; this is the proper way
PmtInf.append(PmtTpInf)
ReqdExctnDt = self._get_ReqdExctnDt_content(payment_date, group_payment_method_code)
PmtInf.append(ReqdExctnDt)
PmtInf.append(self._get_Dbtr(group_payment_method_code))
PmtInf.append(self._get_DbtrAcct(group_payment_method_code))
DbtrAgt = etree.SubElement(PmtInf, "DbtrAgt")
DbtrAgt.append(self._get_FinInstnId(self.bank_account_id, group_payment_method_code, mode='DbtrAgt'))
unique_chrgbr_values = {payment.get('iso20022_charge_bearer') for payment in payments_list}
unique_chrgbr = unique_chrgbr_values.pop() if len(unique_chrgbr_values) == 1 else None
if unique_chrgbr:
PmtInf.append(self._get_ChrgBr(group_payment_method_code, unique_chrgbr))
# One CdtTrfTxInf per transaction
for payment in payments_list:
PmtInf.append(self._get_CdtTrfTxInf(
PmtInfId, payment, group_payment_method_code, include_charge_bearer=not unique_chrgbr
))
return Document
# -------------------------------------------------------------------------
# NODES CREATION
# -------------------------------------------------------------------------
def _get_CtrlSum(self, payments):
# To override per pain version
return float_repr(float_round(sum(payment['amount'] for payment in payments), 2), 2)
def _get_InitgPty(self, payment_method_code):
InitgPty = etree.Element("InitgPty")
InitgPty.extend(self._get_company_PartyIdentification32(postal_address=False, issr=True, payment_method_code=payment_method_code))
return InitgPty
def _get_PmtTpInf(self, payment_method_code, priority):
PmtTpInf = etree.Element("PmtTpInf")
if priority:
InstrPrty = etree.SubElement(PmtTpInf, "InstrPrty")
InstrPrty.text = priority
SvcLvlTxt = self._get_SvcLvlText(payment_method_code)
if SvcLvlTxt:
SvcLvl = etree.SubElement(PmtTpInf, "SvcLvl")
Cd = etree.SubElement(SvcLvl, "Cd")
Cd.text = SvcLvlTxt
CtgyPurp = self._get_CtgyPurp(payment_method_code)
if CtgyPurp is not None: # avoid FutureWarning
PmtTpInf.append(CtgyPurp)
LclInstrm = self._get_LclInstrm(payment_method_code)
if LclInstrm is not None:
PmtTpInf.append(LclInstrm)
return PmtTpInf
def _get_LclInstrm(self, payment_method_code):
if payment_method_code == 'iso20022' and (local_instrument_code := self.env['ir.config_parameter'].sudo().get_param('account_iso20022.local_instrument_code')):
LclInstrm = etree.Element("LclInstrm")
Cd = etree.SubElement(LclInstrm, 'Cd')
Cd.text = local_instrument_code
return LclInstrm
def _get_CtgyPurp(self, payment_method_code):
if self.env.context.get('sepa_payroll_sala'):
# 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.Element("CtgyPurp")
Cd = etree.SubElement(CtgyPurp, "Cd")
Cd.text = 'SALA'
return CtgyPurp
def _get_ReqdExctnDt_content(self, payment_date, payment_method_code):
ReqdExctnDt = etree.Element("ReqdExctnDt")
ReqdExctnDt.text = fields.Date.to_string(payment_date)
return ReqdExctnDt
def _get_Dbtr(self, payment_method_code):
Dbtr = etree.Element("Dbtr")
Dbtr.extend(self._get_company_PartyIdentification32(postal_address=True, payment_method_code=payment_method_code))
return Dbtr
# TODO: remove payments arg in master
def _get_DbtrAcct(self, payment_method_code=None, payments=None):
if not self.bank_account_id.sanitized_acc_number:
raise UserError(_("This journal does not have a bank account defined."))
DbtrAcct = etree.Element("DbtrAcct")
Id = etree.SubElement(DbtrAcct, "Id")
if self.bank_account_id.acc_type != 'iban' or self._is_bank_account_qr_iban(self.bank_account_id):
Id.append(self._get_DbtrAcctOthr(payment_method_code))
else:
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
# TODO: remove partner_acc_type arg in master
def _get_DbtrAcctOthr(self, payment_method_code=None, partner_acc_type=None):
Othr = etree.Element("Othr")
OthrId = etree.SubElement(Othr, "Id")
OthrId.text = self.bank_account_id.sanitized_acc_number
return Othr
def _get_ChrgBr(self, payment_method_code, forced_value):
ChrgBr = etree.Element("ChrgBr")
ChrgBr.text = forced_value or "SHAR"
return ChrgBr
def _get_CdtTrfTxInf(self, PmtInfId, payment, payment_method_code, include_charge_bearer=True):
CdtTrfTxInf = etree.Element("CdtTrfTxInf")
PmtId = etree.SubElement(CdtTrfTxInf, "PmtId")
if payment['name']:
InstrId = etree.SubElement(PmtId, "InstrId")
InstrId.text = sanitize_communication(payment['name'].replace('&', '+'), 35)
EndToEndId = etree.SubElement(PmtId, "EndToEndId")
EndToEndId.text = payment.get('end_to_end_uuid') or uuid4().hex
Amt = etree.SubElement(CdtTrfTxInf, "Amt")
journal_id = self.env['account.journal'].search([('id', '=', payment['journal_id'])], limit=1)
currency_id = self.env['res.currency'].search([('id', '=', payment['currency_id'])], limit=1) or journal_id.company_id.currency_id
val_Ccy = currency_id.name
val_InstdAmt = float_repr(currency_id.round(payment['amount']), currency_id.decimal_places)
InstdAmt = etree.SubElement(Amt, "InstdAmt", Ccy=val_Ccy)
InstdAmt.text = val_InstdAmt
if include_charge_bearer:
CdtTrfTxInf.append(self._get_ChrgBr(payment_method_code, payment['iso20022_charge_bearer']))
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, payment_method_code):
CdtTrfTxInf.append(self._get_CdtrAgt(partner_bank, payment_method_code))
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, payment_method_code)
if PstlAdr is not None:
Cdtr.append(PstlAdr)
CdtTrfTxInf.append(self._get_CdtrAcct(partner_bank, payment_method_code))
val_RmtInf = self._get_RmtInf(payment_method_code, payment)
if val_RmtInf is not False:
CdtTrfTxInf.append(val_RmtInf)
return CdtTrfTxInf
def _get_CdtrAgt(self, bank_account, payment_method_code):
CdtrAgt = etree.Element("CdtrAgt")
CdtrAgt.append(self._get_FinInstnId(bank_account, payment_method_code, mode='CdtrAgt'))
return CdtrAgt
def _get_FinInstnId(self, bank_account, payment_method_code, mode=None):
FinInstnId = etree.Element("FinInstnId")
bic_code = self._get_cleaned_bic_code(bank_account, payment_method_code)
if bic_code:
BIC = etree.SubElement(FinInstnId, self._get_bic_tag(payment_method_code))
BIC.text = bic_code
else:
Othr = etree.SubElement(FinInstnId, "Othr")
Id = etree.SubElement(Othr, "Id")
Id.text = "NOTPROVIDED"
ClrSysMmbId = self._get_ClrSysMmbId(bank_account, payment_method_code)
if ClrSysMmbId is not None:
FinInstnId.append(ClrSysMmbId)
return FinInstnId
def _get_ClrSysMmbId(self, bank_account, payment_method_code):
if bank_account.clearing_number:
ClrSysMmbId = etree.Element("ClrSysMmbId")
MmbId = etree.SubElement(ClrSysMmbId, "MmbId")
MmbId.text = bank_account.clearing_number
return ClrSysMmbId
def _get_CdtrAcct(self, bank_account, payment_method_code=None):
CdtrAcct = etree.Element("CdtrAcct")
Id = etree.SubElement(CdtrAcct, "Id")
if bank_account.acc_type == 'iban':
IBAN = etree.SubElement(Id, "IBAN")
IBAN.text = bank_account.sanitized_acc_number
else:
Id.append(self._get_CdtrAcctIdOthr(bank_account, payment_method_code))
return CdtrAcct
def _get_CdtrAcctIdOthr(self, bank_account, payment_method_code=None):
Othr = etree.Element("Othr")
Id = etree.SubElement(Othr, "Id")
acc_number = bank_account.sanitized_acc_number
# CH case 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
return Othr
def _get_RmtInf(self, payment_method_code, 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 == 'DK' and sr.is_valid_structured_reference_dk(reference):
return 'dk'
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
if not payment['memo']:
return False
RmtInf = etree.Element('RmtInf')
ref = sr.sanitize_structured_reference(payment['memo'])
partner_country_code = payment.get('partner_country_code')
reference_type = detect_reference_type(ref, partner_country_code)
if reference_type:
RmtInf.append(self._get_RmtInf_content(ref, reference_type))
# Check whether we have a structured communication
else:
Ustrd = etree.SubElement(RmtInf, "Ustrd")
Ustrd.text = sanitize_communication(payment['memo'].replace('&', '+'))
return RmtInf
def _get_organization_id_node_text(self, payment_method_code, postal_address):
return self.company_id.iso20022_orgid_id
def _get_company_PartyIdentification32(self, payment_method_code, postal_address=True, nm=True, issr=True, schme_nm=False):
""" Returns a PartyIdentification32 element identifying the current journal's company
"""
ret = []
Nm = etree.Element("Nm")
Nm.text = self.iso20022_get_company_name()
ret.append(Nm)
if postal_address:
PstlAdr = self._get_PstlAdr(self.company_id.partner_id, payment_method_code)
if PstlAdr is not None:
ret.append(PstlAdr)
if self.company_id.iso20022_orgid_id:
Id = etree.Element("Id")
OrgId = etree.SubElement(Id, "OrgId")
company = self.company_id
if self.sepa_pain_version == "pain.001.001.09" and company.iso20022_lei:
LEI = etree.Element("LEI")
LEI.text = self.company_id.iso20022_lei
OrgId.insert(0, LEI)
Othr = etree.SubElement(OrgId, "Othr")
_Id = etree.SubElement(Othr, "Id")
_Id.text = sanitize_communication(self._get_organization_id_node_text(payment_method_code, postal_address))
if issr and company.iso20022_orgid_issr:
Issr = etree.SubElement(Othr, "Issr")
Issr.text = sanitize_communication(company.iso20022_orgid_issr)
if schme_nm:
SchmeNm = etree.SubElement(Othr, "SchmeNm")
Cd = etree.SubElement(SchmeNm, "Cd")
Cd.text = schme_nm
ret.append(Id)
return ret
# -------------------------------------------------------------------------
# HELPERS/HOOKS
# -------------------------------------------------------------------------
def get_document_namespace(self, payment_method_code):
namespace = 'pain.001.001.03'
if self.sepa_pain_version == 'pain.001.001.09':
namespace = 'pain.001.001.09'
return "urn:iso:std:iso:20022:tech:xsd:%s" % namespace
def _get_schema_location(self, payment_method_code):
# To override
"""
Retrieve the XML Schema Location attributes for the payment file for validators.
:return: A tuple containing the schemaInstance attribute and the location string
or None if we don't need a schema location.
"""
return None
def _get_bic_tag(self, payment_method_code):
# To override per pain version
return 'BIC'
def iso20022_get_company_name(self):
company = self.company_id
name_length = 35 if company.iso20022_initiating_party_name else 70
name = company.iso20022_initiating_party_name or company.name
return sanitize_communication(name[:name_length])
def _get_SvcLvlText(self, payment_method_code):
if payment_method_code == 'iso20022':
return 'NURG'
return None
def _get_PstlAdr(self, partner_id, payment_method_code):
partner_address = self.get_postal_address(partner_id, payment_method_code)
if partner_address:
PstlAdr = etree.Element("PstlAdr")
if partner_address.get('state'):
CtrySubDvsn = etree.SubElement(PstlAdr, "CtrySubDvsn")
CtrySubDvsn.text = self._sepa_sanitize_communication(partner_address['state'][:35])
Ctry = etree.SubElement(PstlAdr, "Ctry")
Ctry.text = partner_address['country']
# Some banks seem allergic to having the zip in a separate tag, so we do as before
street = ", ".join(part for part in (partner_address.get('street'), partner_address.get('street2')) if part)
if street:
AdrLine = etree.SubElement(PstlAdr, "AdrLine")
AdrLine.text = sanitize_communication(street[:70])
if partner_address.get('zip') and partner_address.get('city'):
AdrLine = etree.SubElement(PstlAdr, "AdrLine")
AdrLine.text = sanitize_communication((partner_address['zip'] + " " + partner_address['city'])[:70])
return PstlAdr
return None
def get_postal_address(self, partner_id, payment_method_code):
pstl_addr_list = [address for address in partner_id._get_all_addr() if address['country']]
if not pstl_addr_list:
if partner_id.is_company:
raise ValidationError(_('Partner %s has no country code defined.', partner_id.name))
else:
raise ValidationError(_('Employee %s has no country in their address.', partner_id.name))
pstl_addr_list = [addr for addr in pstl_addr_list if addr['city']]
if not pstl_addr_list:
return None
for addr_dict in pstl_addr_list:
if addr_dict['contact_type'] == 'employee':
return addr_dict
return pstl_addr_list[0]
def _skip_CdtrAgt(self, partner_bank, payment_method_code):
return self.env.context.get('skip_bic', False) or not partner_bank.bank_id.bic
def _get_RmtInf_content(self, ref, reference_type=''):
return self.get_strd_tree(ref, cd='SCOR', issr='ISO')
def _get_cleaned_bic_code(self, bank_account, payment_method_code):
"""
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 = self._get_regex_for_bic_code(payment_method_code)
if not re.match(regex, bank_account.bank_bic):
raise UserError(_("The BIC code '%(bic_code)s' associated to the bank '%(bank)s' of bank account '%(account)s' "
"of partner '%(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",
bic_code=bank_account.bank_bic, bank=bank_account.bank_id.name,
account=bank_account.sanitized_acc_number, partner=bank_account.partner_id.name))
return bank_account.bank_bic.replace(' ', '').upper()
def _get_regex_for_bic_code(self, payment_method_code):
return '[A-Z]{6,6}[A-Z2-9][A-NP-Z0-9]([A-Z0-9]{3,3}){0,1}'
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
if partner_bank.company_id.id not in (False, company.id):
return False
return self._is_bank_account_qr_iban(partner_bank)
def _is_bank_account_qr_iban(self, bank_account):
iban = bank_account.sanitized_acc_number
if iban[:2] not in ('CH', 'LI') or len(iban) < 9:
return False
iid = get_iban_part(iban, 'bank')
return re.match(r'\d+', iid) \
and 30000 <= int(iid) <= 31999 # Those values for iid are reserved for QR-IBANs only
def get_strd_tree(self, ref, cd=None, prtry=None, issr=None):
strd = etree.Element("Strd")
CdtrRefInf = etree.SubElement(strd, "CdtrRefInf")
Tp = etree.SubElement(CdtrRefInf, "Tp")
CdOrPrtry = etree.SubElement(Tp, "CdOrPrtry")
if cd:
Cd = etree.SubElement(CdOrPrtry, "Cd")
Cd.text = cd
if prtry:
Prtry = etree.SubElement(CdOrPrtry, "Prtry")
Prtry.text = prtry
if issr:
Issr = etree.SubElement(Tp, "Issr")
Issr.text = issr
Ref = etree.SubElement(CdtrRefInf, "Ref")
Ref.text = ref
return strd