You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

534 lines
20 KiB

  1. # -*- encoding: utf-8 -*-
  2. ##############################################################################
  3. #
  4. # Copyright (C) 2009 EduSense BV (<http://www.edusense.nl>).
  5. # All Rights Reserved
  6. #
  7. # This program is free software: you can redistribute it and/or modify
  8. # it under the terms of the GNU Affero General Public License as published by
  9. # the Free Software Foundation, either version 3 of the License, or
  10. # (at your option) any later version.
  11. #
  12. # This program is distributed in the hope that it will be useful,
  13. # but WITHOUT ANY WARRANTY; without even the implied warranty of
  14. # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  15. # GNU Affero General Public License for more details.
  16. #
  17. # You should have received a copy of the GNU Affero General Public License
  18. # along with this program. If not, see <http://www.gnu.org/licenses/>.
  19. #
  20. ##############################################################################
  21. # The information about SEPA account numbers in this module are collected
  22. # from ISO 13616-1, which can be found at SWIFT's website:
  23. # http://www.swift.com/solutions/messaging/information_products/bic_downloads_documents/pdfs/IBAN_Registry.pdf
  24. #
  25. # This module uses both SEPA and IBAN as seemingly interchangeble terms.
  26. # However, a SEPA account is a bank account in the SEPA zone, which is
  27. # represented by a IBAN number, which is build up from a ISO-693-1 two letter
  28. # country code, two check digits and a BBAN number, representing the
  29. # local/national accounting scheme.
  30. #
  31. # With the exception of Turkey, all countries use the full local adressing
  32. # scheme in the IBAN numbers, making it possible to deduce the BBAN from the
  33. # IBAN. As Turkey uses an additional code in the local scheme which is not
  34. # part of the BBAN, for accounts located in Turkeys banks it is not possible
  35. # to use the BBAN to reconstruct the local account.
  36. #
  37. # WARNING:
  38. # This module contains seemingly enough info to create IBAN's from BBAN's.
  39. # Although many BBAN/IBAN conversions seem algorithmic, there is enough
  40. # deviation to take the warning from SEPA seriously: this is the domain of the
  41. # account owning banks. Don't use it, unless you are prepared to loose your
  42. # money. It is for heuristic validation purposes only.
  43. __all__ = ['IBAN', 'BBAN']
  44. def modulo_97_base10(abuffer):
  45. '''
  46. Calculate the modulo 97 value of a string in base10
  47. '''
  48. checksum = int(abuffer[0])
  49. for digit in abuffer[1:]:
  50. checksum *= 10
  51. checksum += int(digit)
  52. checksum %= 97
  53. return checksum
  54. def base36_to_base10str(abuffer):
  55. '''
  56. Convert a base36 string value to a string of base10 digits.
  57. '''
  58. result = ''
  59. for digit in abuffer:
  60. if digit.isalpha():
  61. result += str(ord(digit) - 55)
  62. else:
  63. result += digit
  64. return result
  65. class BBANFormat(object):
  66. '''
  67. A BBANFormat is an auxilliary class for IBAN. It represents the composition
  68. of a BBAN number from the different elements in order to translate a
  69. IBAN number to a localized number. The reverse route, transforming a local
  70. account to a SEPA account, is the sole responsibility of the banks.
  71. '''
  72. def __init__(self, ibanfmt, bbanfmt='%A', nolz=False):
  73. '''
  74. Specify the structure of the SEPA account in relation to the local
  75. account. The XXZZ prefix that all SEPA accounts have is not part of
  76. the structure in BBANFormat.
  77. ibanfmt: string of identifiers from position 5 (start = 1):
  78. A = Account position
  79. N = Account digit
  80. B = Bank code digit
  81. C = Branch code digit
  82. V = Account check digit
  83. W = Bank code check digit
  84. X = Additional check digit (some countries check everything)
  85. P = Account prefix digit
  86. The combination of N and A can be used to encode minimum length
  87. leading-zero-stripped account numbers.
  88. Example: (NL) 'CCCCAAAAAAAAAA'
  89. will convert 'INGB0001234567' into
  90. bankcode 'INGB' and account '0001234567'
  91. bbanfmt: string of placeholders for the local bank account
  92. %C: bank code
  93. %B: branch code
  94. %I: IBAN number (complete)
  95. %T: account type
  96. %P: account prefix
  97. %A: account number. This will include the 'N' placeholder
  98. positions in the ibanfmt.
  99. %V, %W, %X: check digits (separate meanings)
  100. %Z: IBAN check digits (only Poland uses these)
  101. %%: %
  102. anything else: literal copy
  103. Example: (AT): '%A BLZ %C'
  104. nolz: boolean indicating stripping of leading zeroes in the account
  105. number. Defaults to False
  106. '''
  107. self._iban = ibanfmt
  108. self._bban = bbanfmt
  109. self._nolz = nolz
  110. def __extract__(self, spec, value):
  111. '''Extract the value based on the spec'''
  112. i = self._iban.find(spec)
  113. if i < 0:
  114. return ''
  115. result = ''
  116. j = len(self._iban)
  117. while i < j and self._iban[i] == spec:
  118. result += value[i+4]
  119. i += 1
  120. return self._nolz and result.lstrip('0') or result
  121. def bankcode(self, iban):
  122. '''Return the bankcode'''
  123. return self.__extract__('B', iban)
  124. def branchcode(self, iban):
  125. '''Return the branch code'''
  126. return self.__extract__('C', iban)
  127. def account(self, iban):
  128. '''Return the account number'''
  129. if self._iban.find('N') >= 0:
  130. prefix = self.__extract__('N', iban).lstrip('0')
  131. else:
  132. prefix = ''
  133. return prefix + self.__extract__('A', iban)
  134. def BBAN(self, iban):
  135. '''
  136. Format the BBAN part of the IBAN in iban following the local
  137. addressing scheme. We need the full IBAN in order to be able to use
  138. the IBAN check digits in it, as Poland needs.
  139. '''
  140. res = ''
  141. i = 0
  142. while i < len(self._bban):
  143. if self._bban[i] == '%':
  144. i += 1
  145. parm = self._bban[i]
  146. if parm == 'I':
  147. res += unicode(iban)
  148. elif parm in 'BCDPTVWX':
  149. res += self.__extract__(parm, iban)
  150. elif parm == 'A':
  151. res += self.account(iban)
  152. elif parm == 'S':
  153. res += iban
  154. elif parm == 'Z':
  155. # IBAN check digits (Poland)
  156. res += iban[2:4]
  157. elif parm == '%':
  158. res += '%'
  159. else:
  160. res += self._bban[i]
  161. i += 1
  162. return res
  163. class IBAN(str):
  164. '''
  165. A IBAN string represents a SEPA bank account number. This class provides
  166. the interpretation and some validation of such strings.
  167. Mind that, although there is sufficient reason to comment on the chosen
  168. approach, we are talking about a transition period of at max. 1 year. Good
  169. is good enough.
  170. '''
  171. BBAN_formats = {
  172. 'AL': BBANFormat('CCBBBBVAAAAAAAAAAAAAAAAAA', '%B%A'),
  173. 'AD': BBANFormat('CCCCBBBBAAAAAAAAAAAA', '%A'),
  174. 'AT': BBANFormat('BBBBBAAAAAAAAAAA', '%A BLZ %B'),
  175. 'BE': BBANFormat('CCCAAAAAAAVV', '%C-%A-%V'),
  176. 'BA': BBANFormat('BBBCCCAAAAAAAA', '%I'),
  177. 'BG': BBANFormat('BBBBCCCCAAAAAAAAAA', '%I'),
  178. 'CH': BBANFormat('CCCCCAAAAAAAAAAAAV', '%C %A', nolz=True),
  179. 'CS': BBANFormat('BBBAAAAAAAAAAAAAVV', '%B-%A-%V'),
  180. 'CY': BBANFormat('BBBCCCCCAAAAAAAAAAAAAAAA', '%B%C%A'),
  181. 'CZ': BBANFormat('BBBBPPPPPPAAAAAAAAAA', '%B-%P/%A'),
  182. 'DE': BBANFormat('BBBBBBBBAAAAAAAAAAV', '%A BLZ %B'),
  183. 'DK': BBANFormat('CCCCAAAAAAAAAV', '%C %A%V'),
  184. 'EE': BBANFormat('BBCCAAAAAAAAAAAV', '%A%V'),
  185. 'ES': BBANFormat('BBBBCCCCWVAAAAAAAAAA', '%B%C%W%V%A'),
  186. 'FI': BBANFormat('CCCCTTAAAAAAAV', '%C%T-%A%V', nolz=True),
  187. 'FR': BBANFormat('BBBBBCCCCCAAAAAAAAAAAVV', '%B %C %A %V'),
  188. 'FO': BBANFormat('BBBBAAAAAAAAAV', '%B %A%V'),
  189. # Great Brittain uses a special display for the branch code, which we
  190. # can't honor using the current system. If this appears to be a
  191. # problem, we can come up with something later.
  192. 'GB': BBANFormat('BBBBCCCCCCAAAAAAAAV', '%C %A'),
  193. 'GI': BBANFormat('BBBBAAAAAAAAAAAAAAA', '%A'),
  194. 'GL': BBANFormat('CCCCAAAAAAAAAV', '%C %A%V'),
  195. 'GR': BBANFormat('BBBCCCCAAAAAAAAAAAAAAAA', '%B-%C-%A', nolz=True),
  196. 'HR': BBANFormat('BBBBBBBAAAAAAAAAA', '%B-%A'),
  197. 'HU': BBANFormat('BBBCCCCXAAAAAAAAAAAAAAAV', '%B%C%X %A%V'),
  198. 'IE': BBANFormat('BBBBCCCCCCAAAAAAAA', '%C %A'),
  199. 'IL': BBANFormat('BBBCCCAAAAAAAAAAAAA', '%C%A'),
  200. # Iceland uses an extra identification number, split in two on
  201. # display. Coded here as %P%V.
  202. 'IS': BBANFormat('CCCCTTAAAAAAPPPPPPVVVV', '%C-%T-%A-%P-%V'),
  203. 'IT': BBANFormat('WBBBBBCCCCCAAAAAAAAAAAA', '%W/%B/%C/%A'),
  204. 'LV': BBANFormat('BBBBAAAAAAAAAAAAA', '%I'),
  205. 'LI': BBANFormat('CCCCCAAAAAAAAAAAA', '%C %A', nolz=True),
  206. 'LT': BBANFormat('BBBBBAAAAAAAAAAA', '%I'),
  207. 'LU': BBANFormat('BBBAAAAAAAAAAAAA', '%I'),
  208. 'MC': BBANFormat('BBBBBCCCCCAAAAAAAAAAAVV', '%B %C %A %V'),
  209. 'ME': BBANFormat('CCCAAAAAAAAAAAAAVV', '%C-%A-%V'),
  210. 'MK': BBANFormat('BBBAAAAAAAAAAVV', '%B-%A-%V', nolz=True),
  211. 'MT': BBANFormat('BBBBCCCCCAAAAAAAAAAAAAAAAAA', '%A', nolz=True),
  212. # Mauritius has an aditional bank identifier, a reserved part and the
  213. # currency as part of the IBAN encoding. As there is no representation
  214. # given for the local account in ISO 13616-1 we assume IBAN, which
  215. # circumvents the BBAN display problem.
  216. 'MU': BBANFormat('BBBBBBCCAAAAAAAAAAAAVVVWWW', '%I'),
  217. # Netherlands has two different local account schemes: one with and
  218. # one without check digit (9-scheme and 7-scheme). Luckily most Dutch
  219. # financial services can keep the two apart without telling, so leave
  220. # that. Also leave the leading zero issue, as most banks are already
  221. # converting their local account numbers to BBAN format.
  222. 'NL': BBANFormat('BBBBAAAAAAAAAA', '%A'),
  223. # Norway seems to split the account number in two on display. For now
  224. # we leave that. If this appears to be a problem, we can fix it later.
  225. 'NO': BBANFormat('CCCCAAAAAV', '%C.%A%V'),
  226. 'PL': BBANFormat('CCCCCCCCAAAAAAAAAAAAAAAA', '%Z%C %A'),
  227. 'PT': BBANFormat('BBBBCCCCAAAAAAAAAAAVV', '%B.%C.%A.%V'),
  228. 'RO': BBANFormat('BBBBAAAAAAAAAAAAAAAA', '%A'),
  229. 'SA': BBANFormat('BBAAAAAAAAAAAAAAAA', '%B%A'),
  230. 'SE': BBANFormat('CCCAAAAAAAAAAAAAAAAV', '%A'),
  231. 'SI': BBANFormat('CCCCCAAAAAAAAVV', '%C-%A%V', ),
  232. # Slovakia uses two different format for local display. We stick with
  233. # their formal BBAN specs
  234. 'SK': BBANFormat('BBBBPPPPPPAAAAAAAAAAAA', '%B%P%A'),
  235. # San Marino: No information for display of BBAN, so stick with IBAN
  236. 'SM': BBANFormat('WBBBBBCCCCCCAAAAAAAAAAAAV', '%I'),
  237. 'TN': BBANFormat('BBCCCAAAAAAAAAAAAAVV', '%B %C %A %V'),
  238. # Turkey has insufficient information in the IBAN number to regenerate
  239. # the BBAN: the branch code for local addressing is missing (5n).
  240. 'TR': BBANFormat('BBBBBWAAAAAAAAAAAAAAAA', '%B%C%A'),
  241. }
  242. countries = BBAN_formats.keys()
  243. unknown_BBAN_format = BBANFormat('AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA', '%I')
  244. def __new__(cls, arg, **kwargs):
  245. '''
  246. All letters should be uppercase and acceptable. As str is an
  247. in 'C' implemented class, this can't be done in __init__.
  248. '''
  249. init = ''
  250. if arg:
  251. for item in arg.upper():
  252. if item.isalnum():
  253. init += item
  254. elif item not in ' \t.-':
  255. raise ValueError('Invalid chars found in IBAN number')
  256. return str.__new__(cls, init)
  257. def __init__(self, *args, **kwargs):
  258. '''
  259. Sanity check: don't offer extensions unless the base is sound.
  260. '''
  261. super(IBAN, self).__init__()
  262. if self.countrycode not in self.countries:
  263. self.BBAN_format = self.unknown_BBAN_format
  264. else:
  265. self.BBAN_format = self.BBAN_formats[self.countrycode]
  266. @classmethod
  267. def create(cls, BIC=None, countrycode=None, BBAN=None, bankcode=None,
  268. branchcode=None, account=None):
  269. '''
  270. Create a IBAN number from a BBAN and a country code. Optionaly create
  271. a BBAN from BBAN components before generation.
  272. Incomplete: can only work with valid BBAN now.
  273. '''
  274. if BIC:
  275. if not bankcode:
  276. bankcode = BIC[:4]
  277. if not countrycode:
  278. countrycode = BIC[4:6]
  279. else:
  280. if countrycode:
  281. countrycode = countrycode.upper()
  282. else:
  283. raise ValueError('Either BIC or countrycode is required')
  284. if countrycode not in cls.countries:
  285. raise ValueError('%s is not a SEPA country' % countrycode)
  286. format = cls.BBAN_formats[countrycode]
  287. if BBAN:
  288. if len(BBAN) == len(format._iban):
  289. ibanno = cls(countrycode + '00' + BBAN)
  290. return cls(countrycode + ibanno.checksum + BBAN)
  291. raise ValueError('Insufficient data to generate IBAN')
  292. @property
  293. def valid(self):
  294. '''
  295. Check if the string + check digits deliver a valid checksum
  296. '''
  297. _buffer = self[4:] + self[:4]
  298. return (
  299. self.countrycode in self.countries
  300. and int(base36_to_base10str(_buffer)) % 97 == 1
  301. )
  302. def __repr__(self):
  303. '''
  304. Formal representation is in chops of four characters, devided by a
  305. space.
  306. '''
  307. parts = []
  308. for i in range(0, len(self), 4):
  309. parts.append(self[i:i+4])
  310. return ' '.join(parts)
  311. def __unicode__(self):
  312. '''
  313. Return unicode representation of self
  314. '''
  315. return u'%r' % self
  316. @property
  317. def checksum(self):
  318. '''
  319. Generate a new checksum for an otherwise correct layed out BBAN in a
  320. IBAN string.
  321. NOTE: This is the responsability of the banks. No guaranties whatsoever
  322. that this delivers usable IBAN accounts. Mind your money!
  323. '''
  324. _buffer = self[4:] + self[:2] + '00'
  325. _buffer = base36_to_base10str(_buffer)
  326. return '%.2d' % (98 - modulo_97_base10(_buffer))
  327. @property
  328. def checkdigits(self):
  329. '''
  330. Return the digits which form the checksum in the IBAN string
  331. '''
  332. return self[2:4]
  333. @property
  334. def countrycode(self):
  335. '''
  336. Return the ISO country code
  337. '''
  338. return self[:2]
  339. @property
  340. def bankcode(self):
  341. '''
  342. Return the bank code
  343. '''
  344. return self.BBAN_format.bankcode(self)
  345. @property
  346. def BIC_searchkey(self):
  347. '''
  348. BIC's, or Bank Identification Numbers, are composed of the bank
  349. code, followed by the country code, followed by the localization
  350. code, followed by an optional department number.
  351. The bank code seems to be world wide unique. Knowing this,
  352. one can use the country + bankcode info from BIC to narrow a
  353. search for the bank itself.
  354. Note that some countries use one single localization code for
  355. all bank transactions in that country, while others do not. This
  356. makes it impossible to use an algorithmic approach for generating
  357. the full BIC.
  358. '''
  359. return self.bankcode[:4] + self.countrycode
  360. @property
  361. def branchcode(self):
  362. '''
  363. Return the branch code
  364. '''
  365. return self.BBAN_format.branchcode(self)
  366. @property
  367. def localized_BBAN(self):
  368. '''
  369. Localized format of local or Basic Bank Account Number, aka BBAN
  370. '''
  371. if self.countrycode == 'TR':
  372. # The Turkish BBAN requires information that is not in the
  373. # IBAN number.
  374. return False
  375. return self.BBAN_format.BBAN(self)
  376. @property
  377. def BBAN(self):
  378. '''
  379. Return full encoded BBAN, which is for all countries the IBAN string
  380. after the ISO-639 code and the two check digits.
  381. '''
  382. return self[4:]
  383. class BBAN(object):
  384. '''
  385. Class to reformat a local BBAN account number to IBAN specs.
  386. Simple validation based on length of spec string elements and real data.
  387. '''
  388. @staticmethod
  389. def _get_length(fmt, element):
  390. '''
  391. Internal method to calculate the length of a parameter in a
  392. formatted string
  393. '''
  394. i = 0
  395. max_i = len(fmt._iban)
  396. while i < max_i:
  397. if fmt._iban[i] == element:
  398. next = i + 1
  399. while next < max_i and fmt._iban[next] == element:
  400. next += 1
  401. return next - i
  402. i += 1
  403. return 0
  404. def __init__(self, bban, countrycode):
  405. '''
  406. Reformat and sanity check on BBAN format.
  407. Note that this is not a fail safe check, it merely checks the format of
  408. the BBAN following the IBAN specifications.
  409. '''
  410. self._bban = None
  411. if countrycode.upper() in IBAN.countries:
  412. self._fmt = IBAN.BBAN_formats[countrycode.upper()]
  413. res = ''
  414. i = 0
  415. j = 0
  416. max_i = len(self._fmt._bban)
  417. max_j = len(bban)
  418. while i < max_i and j < max_j:
  419. while bban[j] in ' \t' and j < max_j:
  420. j += 1
  421. if self._fmt._bban[i] == '%':
  422. i += 1
  423. parm = self._fmt._bban[i]
  424. if parm == 'I':
  425. _bban = IBAN(bban)
  426. if _bban.valid:
  427. self._bban = str(_bban)
  428. else:
  429. self._bban = None
  430. # Valid, so nothing else to do
  431. return
  432. elif parm in 'ABCDPSTVWXZ':
  433. _len = self._get_length(self._fmt, parm)
  434. addon = bban[j:j+_len]
  435. if len(addon) != _len:
  436. # Note that many accounts in the IBAN standard
  437. # are allowed to have leading zeros, so zfill
  438. # to full spec length for visual validation.
  439. #
  440. # Note 2: this may look funny to some, as most
  441. # local schemes strip leading zeros. It allows
  442. # us however to present the user a visual feedback
  443. # in order to catch simple user mistakes as
  444. # missing digits.
  445. if parm == 'A':
  446. res += addon.zfill(_len)
  447. else:
  448. # Invalid, just drop the work and leave
  449. return
  450. else:
  451. res += addon
  452. j += _len
  453. elif self._fmt._bban[i] in [bban[j], ' ', '/', '-', '.']:
  454. res += self._fmt._bban[i]
  455. if self._fmt._bban[i] == bban[j]:
  456. j += 1
  457. elif self._fmt._bban[i].isalpha():
  458. res += self._fmt._bban[i]
  459. i += 1
  460. if i == max_i:
  461. self._bban = res
  462. def __str__(self):
  463. '''String representation'''
  464. return self._bban
  465. def __unicode__(self):
  466. '''Unicode representation'''
  467. return unicode(self._bban)
  468. @property
  469. def valid(self):
  470. '''Simple check if BBAN is in the right format'''
  471. return self._bban and True or False
  472. if __name__ == '__main__':
  473. import sys
  474. for arg in sys.argv[1:]:
  475. iban = IBAN(arg)
  476. print('IBAN:', iban)
  477. print('country code:', iban.countrycode)
  478. print('bank code:', iban.bankcode)
  479. print('branch code:', iban.branchcode)
  480. print('BBAN:', iban.BBAN)
  481. print('localized BBAN:', iban.localized_BBAN)
  482. print('check digits:', iban.checkdigits)
  483. print('checksum:', iban.checksum)