import re from lxml import etree from odoo import api, fields, models from odoo.tools import float_repr from odoo.addons.account_batch_payment.models.sepa_mapping import sanitize_communication class AccountJournal(models.Model): _inherit = "account.journal" has_iso_se_payment_method = fields.Boolean(compute='_compute_has_iso_se_payment_method') @api.depends('outbound_payment_method_line_ids.payment_method_id.code') def _compute_has_iso_se_payment_method(self): for journal in self: journal.has_iso_se_payment_method = 'iso20022_se' in journal.outbound_payment_method_line_ids.payment_method_id.mapped('code') @api.depends('bank_acc_number', 'company_id.account_fiscal_country_id', 'company_id.country_id') def _compute_sepa_pain_version(self): se_bban_journals = self.filtered(lambda j: j.bank_account_id.acc_type in ('bban_se', 'plusgiro', 'bankgiro')) # For SE BBAN, we use the pain.001.001.03 version se_bban_journals.sepa_pain_version = 'pain.001.001.03' super(AccountJournal, self - se_bban_journals)._compute_sepa_pain_version() # TODO: remove partner_acc_type arg in master def _is_se_bban(self, payment_method_code, partner_acc_type=None): """ Whenever this journal should be considered as a swedish bban, plusgiro or bankgiro in a batch payment. :param payment_method_code: The payment method used for the payment :return: True if the payment method is set to **iso20022_se** and the bank account is not IBAN, else False. """ return ( payment_method_code == 'iso20022_se' and self.bank_account_id.acc_type in {'bban_se', 'plusgiro', 'bankgiro'} ) or self.env.context.get('bban') def _get_organization_id_node_text(self, payment_method_code, postal_address): # EXTENDS account_iso_20022 if payment_method_code == 'iso20022_se' and postal_address and self.company_id.company_registry and self.bank_id.bic == 'SWEDSESS': return f"06{re.sub(r'[^0-9]', '', self.company_id.company_registry)}B001" return super()._get_organization_id_node_text(payment_method_code, postal_address) def _get_CtgyPurp(self, payment_method_code): if not self._is_se_bban(payment_method_code): return super()._get_CtgyPurp(payment_method_code) CtgyPurp = etree.Element('CtgyPurp') Cd = etree.SubElement(CtgyPurp, 'Cd') Cd.text = 'SALA' if self.env.context.get('sepa_payroll_sala') else 'SUPP' return CtgyPurp # TODO: remove partner_acc_type arg in master def _get_DbtrAcctOthr(self, payment_method_code=None, partner_acc_type=None): # EXTEND of account_iso20022 Othr = super()._get_DbtrAcctOthr(payment_method_code) if payment_method_code == 'iso20022_se': SchmeNm = etree.SubElement(Othr, "SchmeNm") # Nordea has a special rule for DbtrAcct as they only use IBAN or BBAN if self.bank_account_id.acc_type == 'bankgiro' and 'nordea' not in self.bank_account_id.bank_name.lower(): Prtry = etree.SubElement(SchmeNm, "Prtry") Prtry.text = 'BGNR' else: Cd = etree.SubElement(SchmeNm, "Cd") Cd.text = 'BBAN' return Othr def _get_CdtrAcctIdOthr(self, bank_account, payment_method_code=None): if payment_method_code != 'iso20022_se': return super()._get_CdtrAcctIdOthr(bank_account, payment_method_code) Othr = etree.Element("Othr") Id = etree.SubElement(Othr, "Id") Id.text = bank_account.sanitized_acc_number SchmeNm = etree.SubElement(Othr, "SchmeNm") if bank_account.acc_type == 'bankgiro': Prtry = etree.SubElement(SchmeNm, "Prtry") Prtry.text = 'BGNR' else: Cd = etree.SubElement(SchmeNm, "Cd") Cd.text = 'BBAN' return Othr def _get_FinInstnId(self, bank_account, payment_method_code, mode=None): if not self._is_se_bban(payment_method_code): return super()._get_FinInstnId(bank_account, payment_method_code, mode=mode) FinInstnId = etree.Element("FinInstnId") bic_code = self._get_cleaned_bic_code(bank_account, payment_method_code) if mode == 'DbtrAgt': BIC = etree.SubElement(FinInstnId, self._get_bic_tag(payment_method_code)) BIC.text = bic_code return FinInstnId ClrSysMmbId = etree.SubElement(FinInstnId, "ClrSysMmbId") ClrSysId = etree.SubElement(ClrSysMmbId, "ClrSysId") Cd = etree.SubElement(ClrSysId, "Cd") Cd.text = "SESBA" MmbId = etree.SubElement(ClrSysMmbId, "MmbId") if bank_account.acc_type == 'bankgiro': MmbId.text = '9900' elif bank_account.acc_type == 'plusgiro': bank_code, _acc_num, _checksum = bank_account._se_get_acc_number_data(bank_account.acc_number) MmbId.text = '9960' if bank_code and bank_code.startswith('996') else '9500' else: if not bank_account.acc_number.isdigit(): _bban, bank_code = bank_account._se_get_bban_from_iban() else: bank_code, _acc_num, _checksum = bank_account._se_get_acc_number_data(bank_account.acc_number) MmbId.text = bank_code return FinInstnId def _get_RmtInf(self, payment_method_code, payment): RmtInf = super()._get_RmtInf(payment_method_code, payment) if RmtInf is False or not self._is_se_bban(payment_method_code): return RmtInf strd = RmtInf.find('Strd') if strd is not None: partner_bank_id = payment.get('partner_bank_id') if partner_bank_id: partner_bank = self.env['res.partner.bank'].browse(partner_bank_id) if partner_bank and partner_bank.acc_type == 'bankgiro': # if we got structured reference and the recipient has an account of type bankgiro, we need RfdDocAmt. currency_id = payment.get('currency_id') if currency_id: ccy = self.env['res.currency'].browse(currency_id) RfrdDocAmt = etree.Element('RfrdDocAmt') if payment['payment_type'] == 'inbound': CdtNoteAmt = etree.SubElement(RfrdDocAmt, 'CdtNoteAmt', Ccy=ccy.name) CdtNoteAmt.text = float_repr(ccy.round(payment['amount']), 2) RmtdAmt = etree.SubElement(RfrdDocAmt, 'RmtdAmt', Ccy=ccy.name) RmtdAmt.text = '0.00' elif payment['payment_type'] == 'outbound': CdtNoteAmt = etree.SubElement(RfrdDocAmt, 'CdtNoteAmt', Ccy=ccy.name) CdtNoteAmt.text = '0.00' RmtdAmt = etree.SubElement(RfrdDocAmt, 'RmtdAmt', Ccy=ccy.name) RmtdAmt.text = float_repr(ccy.round(payment['amount']), 2) strd.insert(0, RfrdDocAmt) return RmtInf def _get_PstlAdr(self, partner_id, payment_method_code): # EXTEND account_iso20022 if payment_method_code == 'iso20022_se': postal_address = self.get_postal_address(partner_id, payment_method_code) if postal_address is not None: PstlAdr = etree.Element("PstlAdr") for node_name, attr, size in [('StrtNm', 'street', 70), ('PstCd', 'zip', 140), ('TwnNm', 'city', 140), ('Ctry', 'country', 2)]: if postal_address[attr]: address_element = etree.SubElement(PstlAdr, node_name) address_element.text = sanitize_communication(postal_address[attr], size) return PstlAdr return super()._get_PstlAdr(partner_id, payment_method_code) def _skip_CdtrAgt(self, partner_bank, payment_method_code): """ Determine whether to skip the Creditor Agent (CdtrAgt) element in SEPA XML. This override ensures that for Swedish Bankgiro and Plusgiro accounts, the CdtrAgt element is always included, even if the BIC is missing. For other accounts or payment methods, the standard behavior is preserved. :param partner_bank: The partner's bank account record. :type partner_bank: res.partner.bank :param payment_method_code: The payment method code, e.g., 'iso20022_se'. :type payment_method_code: str :return: False to indicate that CdtrAgt should not be skipped, or the result of the standard implementation. :rtype: bool """ if payment_method_code == 'iso20022_se': return False return super()._skip_CdtrAgt(partner_bank, payment_method_code)