# 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