Skip to content

Added Czech bank account numbers #296

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions docs/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,7 @@ Available formats
cu.ni
cusip
cy.vat
cz.bankaccount
cz.dic
cz.rc
de.handelsregisternummer
Expand Down
5 changes: 5 additions & 0 deletions docs/stdnum.cz.bankaccount.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
stdnum.cz.bankaccount
=====================

.. automodule:: stdnum.cz.bankaccount
:members:
161 changes: 161 additions & 0 deletions stdnum/cz/bankaccount.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,161 @@
# bankaccount.py - functions for handling Czech bank account numbers
# coding: utf-8
#
# Copyright (C) 2022 Arthur de Jong
#
# This library is free software; you can redistribute it and/or
# modify it under the terms of the GNU Lesser General Public
# License as published by the Free Software Foundation; either
# version 2.1 of the License, or (at your option) any later version.
#
# This library is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
# Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public
# License along with this library; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
# 02110-1301 USA

"""Czech bank account number

The Czech bank account numbers consist of up to 20 digits:
UUUUUK-MMMMMMMMKM/XXXX

The first part is prefix that is up to 6 digits. The following part is from 2 to 10 digits.
Both parts could be filled with zeros from left if missing.
The final 4 digits represent the bank code.

More information:

* https://www.penize.cz/osobni-ucty/424173-tajemstvi-cisla-uctu-klicem-pro-banky-je-11
* http://www.zlatakoruna.info/zpravy/ucty/cislo-uctu-v-cr

>>> validate('34278-0727558021/0100')
'034278-0727558021/0100'
>>> validate('4278-727558021/0100') # invalid check digits (prefix)
Traceback (most recent call last):
...
InvalidChecksum: ...
>>> validate('34278-727558021/0000') # invalid bank
Traceback (most recent call last):
...
InvalidComponent: ...
>>> format('34278-727558021/0100')
'034278-0727558021/0100'
"""

import re

from stdnum.exceptions import (
InvalidChecksum, InvalidComponent, InvalidFormat, ValidationError)
from stdnum.util import clean


_prefix_regex = r'[0-9]{0,6}'
_root_regex = r'[0-9]{2,10}'
_bank_regex = r'[0-9]{4}'
_regex = r'((%s)-)?(%s)\/(%s)' % (_prefix_regex, _root_regex, _bank_regex)

_root_weights = [6, 3, 7, 9, 10, 5, 8, 4, 2, 1]
_prefix_weights = _root_weights[4:] # prefix weights are same as root, but we are interested in last 6 weights only


def _parse(number):
number = clean(number).strip()
match = re.match(_regex, number)
if match:
prefix = match.group(2)
root = match.group(3)
bank = match.group(4)
return prefix, root, bank


def compact(number):
"""Convert the number to the minimal representation. This strips the
number of any valid separators and removes surrounding whitespace."""
parsed = _parse(number)
if parsed:
prefix, root, bank = parsed
return ''.join((
prefix + '-' if prefix else '', root, '/', bank,
))


def _info(bank_code):
from stdnum import numdb
info = {}
for nr, found in numdb.get('cz/banks').info(bank_code):
info.update(found)
return info


def info(number):
"""Return a dictionary of data about the supplied number. This typically
returns the name of the bank and branch and a BIC if it is valid."""
parsed = _parse(number)
if parsed:
return _info(parsed[2])


def _get_checksum(part):
if len(part) > 6:
weights = _root_weights
part.zfill(10)
else:
weights = _prefix_weights
part.zfill(6)

_sum = 0
for i, n in enumerate(part):
_sum += weights[i] * int(n)

return _sum % 11


def is_checksum_valid(part):
"""Check if prefix or root of bank account number is valid."""
return _get_checksum(part) == 0


def is_bank_valid(bank_code):
"""Check if bank code is valid."""
return 'bank' in _info(bank_code)


def validate(number):
"""Check if the number provided is a valid bank account number."""
number = format(number) # fill missing zeros
if not number:
raise InvalidFormat()

prefix, root, bank = _parse(number)

if not is_checksum_valid(prefix):
raise InvalidChecksum()

if not is_checksum_valid(root):
raise InvalidChecksum()

if not is_bank_valid(bank):
raise InvalidComponent()

return number


def is_valid(number):
"""Check if the number provided is a valid bank account number."""
try:
return bool(validate(number))
except ValidationError:
return False


def format(number):
"""Reformat the number to the standard presentation format."""
parsed = _parse(number)
if parsed:
return ''.join((
(parsed[0] or '').zfill(6), '-', parsed[1].zfill(10), '/', parsed[2],
))
59 changes: 59 additions & 0 deletions stdnum/cz/banks.dat
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
# generated from kody_bank_CR.csv downloaded from
# https://www.cnb.cz/cs/platebni-styk/.galleries/ucty_kody_bank/download/kody_bank_CR.csv
0100 bic="KOMBCZPP" bank="Komerční banka, a.s." certis="True"
0300 bic="CEKOCZPP" bank="Československá obchodní banka, a. s." certis="True"
0600 bic="AGBACZPP" bank="MONETA Money Bank, a.s." certis="True"
0710 bic="CNBACZPP" bank="ČESKÁ NÁRODNÍ BANKA" certis="True"
0800 bic="GIBACZPX" bank="Česká spořitelna, a.s." certis="True"
2010 bic="FIOBCZPP" bank="Fio banka, a.s." certis="True"
2020 bic="BOTKCZPP" bank="MUFG Bank (Europe) N.V. Prague Branch" certis="True"
2060 bic="CITFCZPP" bank="Citfin, spořitelní družstvo" certis="True"
2070 bic="MPUBCZPP" bank="TRINITY BANK a.s." certis="True"
2100 bank="Hypoteční banka, a.s." certis="True"
2200 bank="Peněžní dům, spořitelní družstvo" certis="True"
2220 bic="ARTTCZPP" bank="Artesa, spořitelní družstvo" certis="True"
2250 bic="CTASCZ22" bank="Banka CREDITAS a.s." certis="True"
2260 bank="NEY spořitelní družstvo" certis="True"
2275 bank="Podnikatelská družstevní záložna"
2600 bic="CITICZPX" bank="Citibank Europe plc, organizační složka"
2700 bic="BACXCZPP" bank="UniCredit Bank Czech Republic and Slovakia, a.s." certis="True"
3030 bic="AIRACZPP" bank="Air Bank a.s." certis="True"
3050 bic="BPPFCZP1" bank="BNP Paribas Personal Finance SA, odštěpný závod" certis="True"
3060 bic="BPKOCZPP" bank="PKO BP S.A., Czech Branch" certis="True"
3500 bic="INGBCZPP" bank="ING Bank N.V." certis="True"
4000 bic="EXPNCZPP" bank="Expobank CZ a.s." certis="True"
4300 bic="NROZCZPP" bank="Národní rozvojová banka, a.s." certis="True"
5500 bic="RZBCCZPP" bank="Raiffeisenbank a.s." certis="True"
5800 bic="JTBPCZPP" bank="J&T BANKA, a.s."
6000 bic="PMBPCZPP" bank="PPF banka a.s." certis="True"
6100 bic="EQBKCZPP" bank="Raiffeisenbank a.s. (do 31. 12. 2021 Equa bank a.s.)" certis="True"
6200 bic="COBACZPX" bank="COMMERZBANK Aktiengesellschaft, pobočka Praha" certis="True"
6210 bic="BREXCZPP" bank="mBank S.A., organizační složka" certis="True"
6300 bic="GEBACZPP" bank="BNP Paribas S.A., pobočka Česká republika" certis="True"
6700 bic="SUBACZPP" bank="Všeobecná úverová banka a.s., pobočka Praha" certis="True"
6800 bic="VBOECZ2X" bank="Sberbank CZ, a.s. v likvidaci" certis="True"
7910 bic="DEUTCZPX" bank="Deutsche Bank Aktiengesellschaft Filiale Prag, organizační složka" certis="True"
7950 bank="Raiffeisen stavební spořitelna a.s." certis="True"
7960 bank="ČSOB Stavební spořitelna, a.s." certis="True"
7970 bank="MONETA Stavební Spořitelna, a.s." certis="True"
7990 bank="Modrá pyramida stavební spořitelna, a.s." certis="True"
8030 bic="GENOCZ21" bank="Volksbank Raiffeisenbank Nordoberpfalz eG pobočka Cheb" certis="True"
8040 bic="OBKLCZ2X" bank="Oberbank AG pobočka Česká republika" certis="True"
8060 bank="Stavební spořitelna České spořitelny, a.s." certis="True"
8090 bic="CZEECZPP" bank="Česká exportní banka, a.s." certis="True"
8150 bic="MIDLCZPP" bank="HSBC Continental Europe, Czech Republic" certis="True"
8190 bank="Sparkasse Oberlausitz-Niederschlesien" certis="True"
8198 bic="FFCSCZP1" bank="FAS finance company s.r.o."
8199 bic="MOUSCZP2" bank="MoneyPolo Europe s.r.o."
8200 bank="PRIVAT BANK der Raiffeisenlandesbank Oberösterreich Aktiengesellschaft, pobočka Česká republika"
8220 bic="PAERCZP1" bank="Payment execution s.r.o."
8230 bank="ABAPAY s.r.o."
8240 bank="Družstevní záložna Kredit, v likvidaci"
8250 bic="BKCHCZPP" bank="Bank of China (CEE) Ltd. Prague Branch" certis="True"
8255 bic="COMMCZPP" bank="Bank of Communications Co., Ltd., Prague Branch odštěpný závod" certis="True"
8265 bic="ICBKCZPP" bank="Industrial and Commercial Bank of China Limited, Prague Branch, odštěpný závod" certis="True"
8270 bic="FAPOCZP1" bank="Fairplay Pay s.r.o."
8280 bic="BEFKCZP1" bank="B-Efekt a.s."
8293 bic="MRPSCZPP" bank="Mercurius partners s.r.o."
8299 bic="BEORCZP2" bank="BESTPAY s.r.o."
8500 bank="Ferratum Bank plc"
65 changes: 65 additions & 0 deletions tests/test_cz_bankaccount.doctest
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
test_cz_bankaccount.doctest - more detailed doctests for stdnum.cz.bankaccount

Copyright (C) 2022 Arthur de Jong

This library is free software; you can redistribute it and/or
modify it under the terms of the GNU Lesser General Public
License as published by the Free Software Foundation; either
version 2.1 of the License, or (at your option) any later version.

This library is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
Lesser General Public License for more details.

You should have received a copy of the GNU Lesser General Public
License along with this library; if not, write to the Free Software
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
02110-1301 USA


This file contains more detailed doctests for the stdnum.cz.bankaccount
module.

>>> from stdnum.cz import bankaccount

>>> bankaccount.validate('34278-0727558021/0100')
'034278-0727558021/0100'

>>> bankaccount.is_valid('4278-0727558021/0100')
False

>>> bankaccount.compact('34278-0727558021/0100')
'34278-0727558021/0100'

>>> bankaccount.compact('1/0100')

>>> str(bankaccount.info('34278-0727558021/0100')['bic'])
'KOMBCZPP'

>>> bankaccount.info('1/0000')

>>> bankaccount.validate('1/0100')
Traceback (most recent call last):
...
bankaccount.InvalidFormat: ...

>>> bankaccount.validate('8021/0100')
Traceback (most recent call last):
...
bankaccount.InvalidChecksum: ...


These have been found online and should all be valid numbers.

>>> numbers = '''
...
... 19-2000145399/0800
... 178124-4159/0710
... 19-34222621/0710
... 280154417/0300
... 0500021502/0800
...
... '''
>>> [x for x in numbers.splitlines() if x and not bankaccount.is_valid(x)]
[]
63 changes: 63 additions & 0 deletions update/cz_banks.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
#!/usr/bin/env python3
# coding: utf-8

# update/cz_banks.py - script to download Bank list from Czech National Bank
#
# Copyright (C) 2022 Arthur de Jong
#
# This library is free software; you can redistribute it and/or
# modify it under the terms of the GNU Lesser General Public
# License as published by the Free Software Foundation; either
# version 2.1 of the License, or (at your option) any later version.
#
# This library is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
# Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public
# License along with this library; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
# 02110-1301 USA

"""This script downloads the list of banks with bank codes as used in the
IBAN and BIC codes as published by the Czech National Bank."""
import csv
import os.path
from io import StringIO

import requests


# The location of the CSV version of the bank identification codes. Also see
# https://www.cnb.cz/cs/platebni-styk/ucty-kody-bank/
download_url = 'https://www.cnb.cz/cs/platebni-styk/.galleries/ucty_kody_bank/download/kody_bank_CR.csv'


def get_values(csv_reader):
"""Return values (bank_number, bic, bank_name, certis) from the CSV."""
# skip first row (header)
try:
next(csv_reader)
except StopIteration:
pass # ignore empty CSV

for row in csv_reader:
yield row[0], row[2], row[1], row[3] == 'A'


if __name__ == '__main__':
response = requests.get(download_url)
response.raise_for_status()
csv_reader = csv.reader(StringIO(response.content.decode('utf-8')), delimiter=';')
print('# generated from %s downloaded from' % os.path.basename(download_url))
print('# %s' % download_url)
for bank_number, bic, bank, certis in get_values(csv_reader):
info = '%s' % bank_number
if bic:
info += ' bic="%s"' % bic
if bank:
info += ' bank="%s"' % bank
if certis:
info += ' certis="%s"' % certis
print(info)