diff --git a/docs/index.rst b/docs/index.rst index 193efb12..a0dcf773 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -153,6 +153,7 @@ Available formats cu.ni cusip cy.vat + cz.bankaccount cz.dic cz.rc de.handelsregisternummer diff --git a/docs/stdnum.cz.bankaccount.rst b/docs/stdnum.cz.bankaccount.rst new file mode 100644 index 00000000..87c50963 --- /dev/null +++ b/docs/stdnum.cz.bankaccount.rst @@ -0,0 +1,5 @@ +stdnum.cz.bankaccount +===================== + +.. automodule:: stdnum.cz.bankaccount + :members: diff --git a/stdnum/cz/bankaccount.py b/stdnum/cz/bankaccount.py new file mode 100644 index 00000000..ccb1c268 --- /dev/null +++ b/stdnum/cz/bankaccount.py @@ -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], + )) diff --git a/stdnum/cz/banks.dat b/stdnum/cz/banks.dat new file mode 100644 index 00000000..df1a2723 --- /dev/null +++ b/stdnum/cz/banks.dat @@ -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" diff --git a/tests/test_cz_bankaccount.doctest b/tests/test_cz_bankaccount.doctest new file mode 100644 index 00000000..926bd942 --- /dev/null +++ b/tests/test_cz_bankaccount.doctest @@ -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)] +[] diff --git a/update/cz_banks.py b/update/cz_banks.py new file mode 100755 index 00000000..041f3327 --- /dev/null +++ b/update/cz_banks.py @@ -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)