Files
enterprise-addons/l10n_se_bban/models/res_partner_bank.py
T

93 lines
4.5 KiB
Python

# Part of Odoo. See LICENSE file for full copyright and licensing details.
import re
from stdnum import luhn
from odoo import api, models, _
PLUSGIRO_ACCOUNT_NUMBER_RE = re.compile(r'^\d{1,7}-\d$')
BANKGIRO_ACCOUNT_NUMBER_RE = re.compile(r'^\d{3,5}-\d{2,5}$')
NOT_DIGIT_RE = re.compile(r'\D')
class ResPartnerBank(models.Model):
_inherit = "res.partner.bank"
@api.model
def _get_supported_account_types(self):
rslt = super()._get_supported_account_types()
rslt.extend([('plusgiro', _('Plusgiro')), ('bankgiro', _('Bankgiro')), ('bban_se', _('Swedish BBAN'))])
return rslt
def retrieve_acc_type(self, acc_number):
if acc_number:
if self._se_validate_plusgiro(acc_number):
return 'plusgiro'
elif self._se_validate_bankgiro(acc_number):
return 'bankgiro'
elif self._se_validate_bban(acc_number):
return 'bban_se'
return super().retrieve_acc_type(acc_number)
def _se_validate_plusgiro(self, acc_number):
"""Validate PlusGiro number: format XXXXXXX-C (2-8 digits total), dash at end, Luhn checksum."""
return PLUSGIRO_ACCOUNT_NUMBER_RE.match(acc_number.replace(' ', '')) and luhn.is_valid(NOT_DIGIT_RE.sub('', acc_number))
def _se_validate_bankgiro(self, acc_number):
"""Validate BankGiro number: format XXXX-XXXC (7-8 digits total), dash in the middle, Luhn checksum last."""
if not BANKGIRO_ACCOUNT_NUMBER_RE.match(acc_number.replace(' ', '')):
return False
only_digits = NOT_DIGIT_RE.sub('', acc_number)
return 7 <= len(only_digits) <= 8 and luhn.is_valid(only_digits)
def _se_validate_bban(self, acc_number):
bank_code, account_number, checksum = self._se_get_acc_number_data(acc_number)
if not all([bank_code, account_number, checksum]):
return False
return self._se_validate_domestic_account_format(checksum, bank_code, account_number)
def _se_get_acc_number_data(self, acc_number):
cleaned_acc_number = acc_number.replace(' ', '').replace(',', '').replace('-', '')
if not cleaned_acc_number.isdigit():
return False, False, False
clearing_range = self.env['se.bban.clear.range'].search([
('min_num', '<=', int(cleaned_acc_number[:4])),
('max_num', '>=', int(cleaned_acc_number[:4])),
], limit=1)
if not clearing_range:
return False, False, False
bank_code_length = 5 if clearing_range.checksum == 'mod10_max_10_digits_5' else 4
bank_code = cleaned_acc_number[:bank_code_length]
account_number = cleaned_acc_number[bank_code_length:]
return bank_code, account_number, clearing_range.checksum
def _se_validate_domestic_account_format(self, checksum, bank_code, account_number):
def mod11_is_valid(number):
""" Validate number with Mod11 with weights 1-10. It's valid when sum is a multiple of 11."""
return not sum(int(n) * ((i % 10) + 1) for i, n in enumerate(number[::-1])) % 11
# Type 1 accounts
if checksum == 'mod11_10_digits': # Validate account number with 10 digits using Mod11 checksum
return len(account_number) == 7 and mod11_is_valid(bank_code[-3:] + account_number)
elif checksum == 'mod11_11_digits': # Validate account number with 11 digits using Mod11 checksum
return len(account_number) == 7 and mod11_is_valid(bank_code + account_number)
# Type 2 accounts
elif checksum in {'mod10_max_10_digits', 'mod10_max_10_digits_5'}: # Validate account number with up to 10 digits using Mod10 checksum
return len(account_number) <= 10 and luhn.is_valid(account_number)
elif checksum == 'mod10_10_digits': # Validate account number with 10 digits using Mod10 checksum
return len(account_number) == 10 and luhn.is_valid(account_number)
elif checksum == 'mod11_9_digits': # Validate account number with 9 digits using Mod11 checksum
return len(account_number) == 9 and mod11_is_valid(account_number)
elif checksum == 'mod11_8_9_digits': # Validate account number with 8 or 9 digits using Mod11 checksum
return len(account_number) in {8, 9} and mod11_is_valid(account_number)
return False
def _se_get_bban_from_iban(self):
cleaned_acc_number = self.sanitized_acc_number[4:]
clearing_number = 5 if cleaned_acc_number[0] == '8' else 4
return cleaned_acc_number[clearing_number:].lstrip('0'), cleaned_acc_number[:clearing_number]