diff --git a/stdnum/kz/__init__.py b/stdnum/kz/__init__.py new file mode 100644 index 00000000..aa1ca58c --- /dev/null +++ b/stdnum/kz/__init__.py @@ -0,0 +1,22 @@ +# __init__.py - collection of Kazakhstan numbers +# coding: utf-8 +# +# Copyright (C) 2022 Leandro Regueiro +# +# 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, see . + +"""Collection of Kazakhstan numbers.""" + +# provide aliases +from stdnum.kz import bin as vat # noqa: F401 diff --git a/stdnum/kz/bin.py b/stdnum/kz/bin.py new file mode 100644 index 00000000..b1c3e301 --- /dev/null +++ b/stdnum/kz/bin.py @@ -0,0 +1,120 @@ +# bin.py - functions for handling Kazakhstan BIN numbers +# coding: utf-8 +# +# Copyright (C) 2022 Leandro Regueiro +# +# 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, see . + +"""BIN or "БСН" (Business Identification Number, "бизнес-сәйкестендіру нөмірі", +Kazakhstan tax number). + +This number consists of 12 digits and consists of five parts: + +* First part: consists of 4 digits representing the year (the last two digits) + and month of registration or re-registration. +* Second part: consists of 1 digit representing the type of legal entity or + individual entrepreneur (C): + 4 - for resident legal entities; + 5 - for non-resident legal entities; + 6 - for IP (C). +* Third part: consists of 1 digit providing additional information: + 0 - head unit of a legal entity or individual entrepreneur (S); + 1 - branch of a legal entity or individual entrepreneur (S); + 2 - representation of a legal entity or individual entrepreneur (S); + 3 - a peasant (farm) farm operating on the basis of joint entrepreneurship. +* Fourth part: consists of 5 digits and is a serial number. +* Fifth part: consists of 1 check digit. + +More information: + +* https://www.oecd.org/tax/automatic-exchange/crs-implementation-and-assistance/tax-identification-numbers/Kazakhstan-TIN.pdf + +>>> validate('940140000385') +'940140000385' +>>> validate('990 140 004 654') +'990140004654' +>>> validate('12345') +Traceback (most recent call last): + ... +InvalidLength: ... +>>> validate('12345678901X') +Traceback (most recent call last): + ... +InvalidFormat: ... +>>> validate('949940000385') +Traceback (most recent call last): + ... +InvalidComponent: ... +>>> validate('940190000385') +Traceback (most recent call last): + ... +InvalidComponent: ... +>>> validate('940149000385') +Traceback (most recent call last): + ... +InvalidComponent: ... +>>> validate('940140000386') +Traceback (most recent call last): + ... +InvalidChecksum: ... +>>> format('990 140 004 654') +'990140004654' +""" # noqa: E501 + +from stdnum.exceptions import * +from stdnum.kz.iin import calc_check_digit +from stdnum.util import clean, isdigits + + +def compact(number: str) -> str: + """Convert the number to the minimal representation. + + This strips the number of any valid separators and removes surrounding + whitespace. + """ + return clean(number, ' ') + + +def validate(number: str) -> str: + """Check if the number is a valid Kazakhstan BIN number. + + This checks the length and formatting. + """ + number = compact(number) + if len(number) != 12: + raise InvalidLength() + if not isdigits(number): + raise InvalidFormat() + if int(number[2:4]) not in (1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12): + raise InvalidComponent() + if number[4] not in ('4', '5', '6'): + raise InvalidComponent() + if number[5] not in ('0', '1', '2', '3'): + raise InvalidComponent() + if calc_check_digit(number[:-1]) != number[-1]: + raise InvalidChecksum() + return number + + +def is_valid(number: str) -> bool: + """Check if the number is a valid Kazakhstan BIN number.""" + try: + return bool(validate(number)) + except ValidationError: + return False + + +def format(number: str) -> str: + """Reformat the number to the standard presentation format.""" + return compact(number) diff --git a/stdnum/kz/iin.py b/stdnum/kz/iin.py new file mode 100644 index 00000000..319f80e8 --- /dev/null +++ b/stdnum/kz/iin.py @@ -0,0 +1,136 @@ +# iin.py - functions for handling Kazakhstani individual identification numbers +# coding: utf-8 +# +# Copyright (C) 2026 Luca Sicurello +# +# 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, see . + +"""IIN (ЖСН, Жеке сәйкестендіру нөмірі, Kazakhstani Individual identification number). + +It is a 12-digit number of which the first 6 digits denote the person's +birth date in the format YYMMDD, the next digit encodes the gender and +century of birth, the next four digits represent a birth serial number +and the last digit is a check digit. + +* https://www.gov.kz/article/648?lang=kk +* https://ru.wikipedia.org/wiki/Индивидуальный_идентификационный_номер + + +>>> compact('921029 35085 1') +'921029350851' +>>> validate('921029350851') +'921029350851' +>>> get_birth_date('921029350851') +datetime.date(1992, 10, 29) +>>> validate('92102B50851') # invalid digit +Traceback (most recent call last): + ... +InvalidFormat: ... +>>> validate('921529350859') # invalid date +Traceback (most recent call last): + ... +InvalidComponent: ... +""" + +from __future__ import annotations + +import datetime + +from stdnum.exceptions import * +from stdnum.util import clean, isdigits + + +def compact(number: str) -> str: + """Convert the number to the minimal representation. + + This strips the number of any valid separators and removes surrounding + whitespace. + """ + return clean(number, ' ') + + +def calc_check_digit(number: str) -> str: + """Calculate the check digit. The number passed should not have + the check digit included.""" + s = sum(w * int(n) for w, n in zip(range(1, 12), number[:11])) % 11 + if s != 10: + return str(s) + + s = sum(w * int(n) for w, n in zip(range(3, 14), number[:11])) % 11 + if s != 10: + return str(s) + + raise InvalidComponent() + + +def get_birth_date(number: str) -> datetime.date: + """Get the birth date from the person's Individual identification number.""" + number = compact(number) + year = int(number[0:2]) + month = int(number[2:4]) + day = int(number[4:6]) + + if number[6] == '0': # no century info for foreign nationals + today = datetime.date.today() + year += (today.year // 100) * 100 + if year >= today.year: + year -= 100 + + elif number[6] in '12': + year += 1800 + elif number[6] in '34': + year += 1900 + elif number[6] in '56': + year += 2000 + else: + raise InvalidComponent() + + try: + return datetime.date(year, month, day) + except ValueError: + raise InvalidComponent() + + +def get_gender(number: str) -> str | None: + """Get the gender of the person's Individual identification number.""" + number = compact(number) + if number[6] == '0': + return None + elif int(number[6]) % 2: + return 'M' + else: + return 'F' + + +def validate(number: str) -> str: + """Check if the given Individual identification number is valid. + This checks the length and whether the check digit is correct.""" + number = compact(number) + if not isdigits(number): + raise InvalidFormat() + if len(number) != 12: + raise InvalidLength() + if get_birth_date(number) > datetime.date.today(): + raise InvalidComponent() + if calc_check_digit(number[:-1]) != number[-1]: + raise InvalidChecksum() + return number + + +def is_valid(number: str) -> bool: + """Check if the given Individual identification number is valid.""" + try: + return bool(validate(number)) + except ValidationError: + return False diff --git a/tests/test_kz_bin.doctest b/tests/test_kz_bin.doctest new file mode 100644 index 00000000..82d1dcaf --- /dev/null +++ b/tests/test_kz_bin.doctest @@ -0,0 +1,337 @@ +test_kz_bin.doctest - more detailed doctests for stdnum.kz.bin module + +Copyright (C) 2022 Leandro Regueiro + +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, see . + + +This file contains more detailed doctests for the stdnum.kz.bin module. It +tries to test more corner cases and detailed functionality that is not really +useful as module documentation. + +>>> from stdnum.kz import bin + + +Tests for some corner cases. + +>>> bin.validate('940140000385') +'940140000385' +>>> bin.validate('990 140 004 654') +'990140004654' +>>> bin.format('990 140 004 654') +'990140004654' +>>> bin.validate('12345') +Traceback (most recent call last): + ... +InvalidLength: ... +>>> bin.validate('12345678901X') +Traceback (most recent call last): + ... +InvalidFormat: ... +>>> bin.validate('949940000385') +Traceback (most recent call last): + ... +InvalidComponent: ... +>>> bin.validate('940190000385') +Traceback (most recent call last): + ... +InvalidComponent: ... +>>> bin.validate('940149000385') +Traceback (most recent call last): + ... +InvalidComponent: ... +>>> bin.validate('940140000386') +Traceback (most recent call last): + ... +InvalidChecksum: ... + + +These have been found online and should all be valid numbers. + +>>> numbers = ''' +... +... 940140000385 +... 171140900016 +... 011140004703 +... 981240000488 +... 031140003104 +... 980740000057 +... 050540004455 +... 011140001654 +... 100240019642 +... 181141900093 +... 990 140 004 654 +... 991240015834 +... 200240900095 +... 210840900029 +... 930440000156 +... 101241001053 +... 061241010270 +... 150440002169 +... 941240000341 +... 011140001666 +... 190740015218 +... 131040003151 +... 140640004517 +... 030840002763 +... 160740009199 +... 930340000261 +... 980340000290 +... 990240003283 +... 140 840 008 587 +... 220140006999 +... 110140001589 +... 960240007379 +... 090440007304 +... 961040000685 +... 910940000091 +... 910940000081 +... 990740001453 +... 960540000294 +... 900740000062 +... 960540000303 +... 990640001380 +... 930640000331 +... 960140000468 +... 081240013779 +... 141040004756 +... 141040030110 +... 981140001105 +... 981140001115 +... 020440002422 +... 180840019420 +... 141140001547 +... 910740000113 +... 910740000044 +... 910740000143 +... 910740000153 +... 910740000133 +... 910740000123 +... 040840002339 +... 091240012910 +... 150340016393 +... 180840002049 +... 051140003851 +... 090440002968 +... 090440002978 +... 090440003074 +... 150740005302 +... 141140001150 +... 940540000528 +... 000140001813 +... 940640000492 +... 050340004626 +... 991240001567 +... 950340000572 +... 961140000232 +... 021140002283 +... 900440000031 +... 910940000071 +... 900440000051 +... 021140002134 +... 961240001086 +... 961240001036 +... 961240001016 +... 980740001213 +... 980740001362 +... 980740001322 +... 090440004151 +... 141140000836 +... 980540000991 +... 970140000975 +... 980540000971 +... 980640000955 +... 970240000890 +... 980540001018 +... 971040000994 +... 970140000985 +... 980640000995 +... 961240000860 +... 980640000975 +... 980640000915 +... 940740000595 +... 141140000549 +... 020740001571 +... 920140000232 +... 020740001600 +... 900340000058 +... 020740001620 +... 900540000065 +... 950140000624 +... 020740001757 +... 940340000273 +... 990640001251 +... 000340001800 +... 020740001551 +... 020740001452 +... 080740008619 +... 990840001338 +... 991140001791 +... 020740001581 +... 090440000545 +... 180840003632 +... 141140002664 +... 950240000648 +... 940740000614 +... 920440000352 +... 961240000992 +... 970240000979 +... 910840000107 +... 931040000104 +... 090440011223 +... 141140001140 +... 990840001328 +... 991040001551 +... 991240001498 +... 991040001563 +... 990840001318 +... 990940001212 +... 991140001781 +... 000840001333 +... 980340001050 +... 991040001526 +... 991240001438 +... 990940001252 +... 990840000944 +... 990340000553 +... 991040001514 +... 991140001800 +... 991140000317 +... 991140001810 +... 990940001202 +... 141140002347 +... 141140001517 +... 940740000624 +... 991140001860 +... 950140000644 +... 931240000449 +... 940140000533 +... 940540000469 +... 950440000615 +... 940540000518 +... 941040000522 +... 950440000625 +... 941240000669 +... 941240000659 +... 090440011898 +... 141140001329 +... 980640000886 +... 950540000400 +... 950140000634 +... 950540000440 +... 941240000649 +... 950540000371 +... 950540000351 +... 981240000953 +... 980940001111 +... 950740000498 +... 900640000099 +... 970140000955 +... 961240000890 +... 900540000055 +... 900540000035 +... 900540000015 +... 900540000045 +... 970140000965 +... 031240002328 +... 141140001775 +... 980740001154 +... 941240000699 +... 961040000467 +... 940640000472 +... 961040000665 +... 961040000615 +... 961040000635 +... 090440021657 +... 141140001229 +... 960740000477 +... 960440000469 +... 960440000489 +... 970840000831 +... 970140001031 +... 960440000439 +... 960440000409 +... 021140002085 +... 960740000453 +... 970140001100 +... 980840001306 +... 021140001999 +... 960440000419 +... 960440000429 +... 961040000576 +... 970440000430 +... 970840000811 +... 941240000679 +... 961240000979 +... 961240001006 +... 900340000028 +... 990640001291 +... 990640001300 +... 990640001281 +... 990640001271 +... 120940010470 +... 990640001231 +... 070940008327 +... 040140002670 +... 141140000456 +... 021140001929 +... 980940001161 +... 980940001171 +... 980940001131 +... 980940001240 +... 980940001191 +... 980940001210 +... 980940001200 +... 980940001151 +... 980940001181 +... 980940001141 +... 980940001220 +... 980940001230 +... 141140001051 +... 980740001203 +... 980740001134 +... 980740001312 +... 980740001144 +... 980740001114 +... 980740001174 +... 980740001233 +... 980740000364 +... 120940007261 +... 980740001302 +... 980740001223 +... 980740001243 +... 980740001263 +... 980740001293 +... 141140001205 +... 021140001897 +... 021040001777 +... 021140002055 +... 021140002075 +... 990540001199 +... 021240002336 +... 021140001959 +... 021140001979 +... 021140001909 +... 021140001919 +... 021140001969 +... 021140001873 +... 020840001515 +... 021040001787 +... 180840015606 +... 180840013401 +... 210740028187 +... +... ''' +>>> [x for x in numbers.splitlines() if x and not bin.is_valid(x)] +[] diff --git a/tests/test_kz_iin.doctest b/tests/test_kz_iin.doctest new file mode 100644 index 00000000..1afac40a --- /dev/null +++ b/tests/test_kz_iin.doctest @@ -0,0 +1,119 @@ +test_kz_iin.doctest - more detailed doctests for the stdnum.kz.iin module + +Copyright (C) 2026 Luca Sicurello + +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, see . + + +This file contains more detailed doctests for the stdnum.kz.iin module. It +tries to validate a number of numbers that have been found online. + +>>> from stdnum.kz import iin +>>> from stdnum.exceptions import * + + +Tests for some corner cases. + +>>> iin.validate('921029 35085 1') +'921029350851' +>>> iin.is_valid('921029350851') +True +>>> iin.is_valid('921029350852') +False +>>> iin.validate('921029350852') +Traceback (most recent call last): + ... +InvalidChecksum: ... +>>> iin.validate('921529350859') # wrong date +Traceback (most recent call last): + ... +InvalidComponent: ... +>>> iin.validate('921029950850') # wrong gender code +Traceback (most recent call last): + ... +InvalidComponent: ... +>>> iin.validate('921029351840') # wrong serial registration number +Traceback (most recent call last): + ... +InvalidComponent: ... +>>> iin.validate('991231623804') # future date +Traceback (most recent call last): + ... +InvalidComponent: ... +>>> iin.validate('123') +Traceback (most recent call last): + ... +InvalidLength: ... +>>> iin.validate('92102935085A') +Traceback (most recent call last): + ... +InvalidFormat: ... +>>> iin.get_gender('840621350384') +'M' +>>> iin.get_gender('850830402160') +'F' +>>> print(iin.get_gender('960206001028')) +None +>>> iin.get_birth_date('960206101024') +datetime.date(1896, 2, 6) +>>> iin.get_birth_date('960206001028') +datetime.date(1996, 2, 6) +>>> iin.get_birth_date('000206001029') +datetime.date(2000, 2, 6) + + +These have been found online and should all be valid numbers. + +>>> numbers = ''' +... +... 790327250010 +... 660811300838 +... 750911301086 +... 780911400109 +... 821119301118 +... 840621350384 +... 841204301193 +... 850830402160 +... 870822400401 +... 871218302400 +... 901026350693 +... 910918301524 +... 921029350851 +... 940609350101 +... 950512300467 +... 960206301027 +... 960417300680 +... 970605350412 +... 970629300437 +... 980415451416 +... 980424301155 +... 990503301265 +... 000117551201 +... 000302501528 +... 000601500307 +... 001026550291 +... 010321501098 +... 011231601533 +... 020105500969 +... 020118551052 +... 030326651468 +... 040120651549 +... 040301501193 +... 040412500083 +... 050305501840 +... 050401650966 +... +... ''' +>>> [x for x in numbers.splitlines() if x and not iin.is_valid(x)] +[]