From b43603a9eaef6afe72a5d85eac3626ab65c5a848 Mon Sep 17 00:00:00 2001 From: Luca Date: Mon, 21 Sep 2026 21:06:45 +0200 Subject: [PATCH 01/14] Add Kazakhstani Individual identity number --- stdnum/stdnum/kz/iin.py | 128 +++++++++++++++++++++++++++++++ stdnum/tests/test_kz_iin.doctest | 102 ++++++++++++++++++++++++ 2 files changed, 230 insertions(+) create mode 100644 stdnum/stdnum/kz/iin.py create mode 100644 stdnum/tests/test_kz_iin.doctest diff --git a/stdnum/stdnum/kz/iin.py b/stdnum/stdnum/kz/iin.py new file mode 100644 index 00000000..943f2bd8 --- /dev/null +++ b/stdnum/stdnum/kz/iin.py @@ -0,0 +1,128 @@ +# 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, the next three digits represent a birth order number +and the last digit is a check digit. + +* https://www.gov.kz/article/648?lang=kk +* https://www.oecd.org/en/networks/global-forum-tax-transparency/resources/aeoi-implementation-portal/tax-identification-numbers.html +* 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: ... +""" + + + +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 '46': year += 2000 + else: raise InvalidComponent() + + try: + return datetime.date(year, month, day) + except ValueError: + raise InvalidComponent() + +def get_gender(number: str) -> str: + """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 'F' + else: + return 'M' + + +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/stdnum/tests/test_kz_iin.doctest b/stdnum/tests/test_kz_iin.doctest new file mode 100644 index 00000000..620992fe --- /dev/null +++ b/stdnum/tests/test_kz_iin.doctest @@ -0,0 +1,102 @@ +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('921029350851') +'921029350851' +>>> 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') +Traceback (most recent call last): + ... +InvalidComponent: ... +>>> iin.validate('921029950850') +Traceback (most recent call last): + ... +InvalidComponent: ... +>>> iin.validate('123') +Traceback (most recent call last): + ... +InvalidLength: ... +>>> iin.validate('92102935085A') +Traceback (most recent call last): + ... +InvalidFormat: ... + + +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 +... 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)] +[] \ No newline at end of file From 2fdafc2588f0966a8d997f9bfbe8b0047bb9a866 Mon Sep 17 00:00:00 2001 From: Luca Date: Mon, 21 Sep 2026 21:23:25 +0200 Subject: [PATCH 02/14] Fix and rename stdnum/stdnum/kz/iin.py to stdnum/kz/iin.py --- stdnum/{stdnum => }/kz/iin.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) rename stdnum/{stdnum => }/kz/iin.py (98%) diff --git a/stdnum/stdnum/kz/iin.py b/stdnum/kz/iin.py similarity index 98% rename from stdnum/stdnum/kz/iin.py rename to stdnum/kz/iin.py index 943f2bd8..4b89835c 100644 --- a/stdnum/stdnum/kz/iin.py +++ b/stdnum/kz/iin.py @@ -113,7 +113,7 @@ def validate(number: str) -> str: raise InvalidFormat() if len(number) != 12: raise InvalidLength() - if get_birth_date(number) > datetime.date.today() + if get_birth_date(number) > datetime.date.today(): raise InvalidComponent() if calc_check_digit(number[:-1]) != number[-1]: raise InvalidChecksum() From 3b409b774517e3c158e201813720ffd60061fadf Mon Sep 17 00:00:00 2001 From: Luca Date: Mon, 21 Sep 2026 21:24:14 +0200 Subject: [PATCH 03/14] Rename stdnum/tests/test_kz_iin.doctest to tests/test_kz_iin.doctest --- {stdnum/tests => tests}/test_kz_iin.doctest | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) rename {stdnum/tests => tests}/test_kz_iin.doctest (99%) diff --git a/stdnum/tests/test_kz_iin.doctest b/tests/test_kz_iin.doctest similarity index 99% rename from stdnum/tests/test_kz_iin.doctest rename to tests/test_kz_iin.doctest index 620992fe..960f3137 100644 --- a/stdnum/tests/test_kz_iin.doctest +++ b/tests/test_kz_iin.doctest @@ -99,4 +99,4 @@ These have been found online and should all be valid numbers. ... ... ''' >>> [x for x in numbers.splitlines() if x and not iin.is_valid(x)] -[] \ No newline at end of file +[] From 23e02d2283bd8316e504fc462571bd851d4b1778 Mon Sep 17 00:00:00 2001 From: Luca Date: Mon, 21 Sep 2026 21:27:32 +0200 Subject: [PATCH 04/14] Fix iin.py --- stdnum/kz/iin.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/stdnum/kz/iin.py b/stdnum/kz/iin.py index 4b89835c..ac249265 100644 --- a/stdnum/kz/iin.py +++ b/stdnum/kz/iin.py @@ -43,6 +43,12 @@ InvalidComponent: ... """ +from __future__ import annotations + +import datetime + +from stdnum.exceptions import * +from stdnum.util import clean, isdigits def compact(number: str) -> str: @@ -92,6 +98,7 @@ def get_birth_date(number: str) -> datetime.date: except ValueError: raise InvalidComponent() + def get_gender(number: str) -> str: """Get the gender of the person's Individual identification number.""" From e8b1a46de118db90290e045db9e1e2fa8aaf82e7 Mon Sep 17 00:00:00 2001 From: Luca Date: Mon, 21 Sep 2026 21:29:55 +0200 Subject: [PATCH 05/14] Fix test_kz_iin.doctest --- tests/test_kz_iin.doctest | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_kz_iin.doctest b/tests/test_kz_iin.doctest index 960f3137..1f95fb17 100644 --- a/tests/test_kz_iin.doctest +++ b/tests/test_kz_iin.doctest @@ -27,7 +27,7 @@ Tests for some corner cases. >>> iin.validate('921029350851') '921029350851' ->>> iin.validate('921029 35085 1) +>>> iin.validate('921029 35085 1') '921029350851' >>> iin.is_valid('921029350851') True From dfb15fa3b94aad8154b78877729b023277d5b8e8 Mon Sep 17 00:00:00 2001 From: Luca Date: Mon, 21 Sep 2026 21:46:11 +0200 Subject: [PATCH 06/14] Fix typo --- stdnum/kz/iin.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/stdnum/kz/iin.py b/stdnum/kz/iin.py index ac249265..dd097f4a 100644 --- a/stdnum/kz/iin.py +++ b/stdnum/kz/iin.py @@ -90,7 +90,7 @@ def get_birth_date(number: str) -> datetime.date: elif number[6] in '12': year += 1800 elif number[6] in '34': year += 1900 - elif number[6] in '46': year += 2000 + elif number[6] in '56': year += 2000 else: raise InvalidComponent() try: From a8d1bda61a7d3183ea25cbbf79716ae549559e40 Mon Sep 17 00:00:00 2001 From: Luca Date: Mon, 21 Sep 2026 22:23:18 +0200 Subject: [PATCH 07/14] Add tests --- tests/test_kz_iin.doctest | 23 ++++++++++++++++++----- 1 file changed, 18 insertions(+), 5 deletions(-) diff --git a/tests/test_kz_iin.doctest b/tests/test_kz_iin.doctest index 1f95fb17..e3054cd5 100644 --- a/tests/test_kz_iin.doctest +++ b/tests/test_kz_iin.doctest @@ -25,8 +25,8 @@ tries to validate a number of numbers that have been found online. Tests for some corner cases. ->>> iin.validate('921029350851') -'921029350851' +>>> iin.validate('960206001027') +'960206001027' >>> iin.validate('921029 35085 1') '921029350851' >>> iin.is_valid('921029350851') @@ -37,14 +37,22 @@ False Traceback (most recent call last): ... InvalidChecksum: ... ->>> iin.validate('921529350859') +>>> 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('921029950850') +>>> iin.validate('921029351840') # wrong serial registration number Traceback (most recent call last): ... InvalidComponent: ... +>>> iin.validate('993112623802') # future date +Traceback (most recent call last): + ... +InvalidLength: ... >>> iin.validate('123') Traceback (most recent call last): ... @@ -53,6 +61,12 @@ InvalidLength: ... Traceback (most recent call last): ... InvalidFormat: ... +>>> iin.get_gender('840621350384') +'M' +>>> iin.get_gender('850830402160') +'F' +>>> print(iin.get_gender('960206001027')) +None These have been found online and should all be valid numbers. @@ -75,7 +89,6 @@ These have been found online and should all be valid numbers. ... 940609350101 ... 950512300467 ... 960206301027 -... 960206301027 ... 960417300680 ... 970605350412 ... 970629300437 From cdf4401b18b2bef5e764ef344d89fc59dded64e8 Mon Sep 17 00:00:00 2001 From: Luca Date: Mon, 21 Sep 2026 22:24:24 +0200 Subject: [PATCH 08/14] Fix gender calculation --- stdnum/kz/iin.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/stdnum/kz/iin.py b/stdnum/kz/iin.py index dd097f4a..858848e2 100644 --- a/stdnum/kz/iin.py +++ b/stdnum/kz/iin.py @@ -99,7 +99,7 @@ def get_birth_date(number: str) -> datetime.date: raise InvalidComponent() -def get_gender(number: str) -> str: +def get_gender(number: str) -> str | None: """Get the gender of the person's Individual identification number.""" number = compact(number) @@ -107,9 +107,9 @@ def get_gender(number: str) -> str: if number[6] == '0': return None elif int(number[6]) % 2: - return 'F' - else: return 'M' + else: + return 'F' def validate(number: str) -> str: From fa04a3b1028b99d8ccc41e54e7b1db6a4ff4db62 Mon Sep 17 00:00:00 2001 From: Luca Date: Mon, 21 Sep 2026 23:22:11 +0200 Subject: [PATCH 09/14] Add tests --- tests/test_kz_iin.doctest | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/tests/test_kz_iin.doctest b/tests/test_kz_iin.doctest index e3054cd5..1afac40a 100644 --- a/tests/test_kz_iin.doctest +++ b/tests/test_kz_iin.doctest @@ -25,8 +25,6 @@ tries to validate a number of numbers that have been found online. Tests for some corner cases. ->>> iin.validate('960206001027') -'960206001027' >>> iin.validate('921029 35085 1') '921029350851' >>> iin.is_valid('921029350851') @@ -49,10 +47,10 @@ InvalidComponent: ... Traceback (most recent call last): ... InvalidComponent: ... ->>> iin.validate('993112623802') # future date +>>> iin.validate('991231623804') # future date Traceback (most recent call last): ... -InvalidLength: ... +InvalidComponent: ... >>> iin.validate('123') Traceback (most recent call last): ... @@ -65,8 +63,14 @@ InvalidFormat: ... 'M' >>> iin.get_gender('850830402160') 'F' ->>> print(iin.get_gender('960206001027')) +>>> 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. From efd62f0aaca387c8b15bb7680bf937a675fdb150 Mon Sep 17 00:00:00 2001 From: Luca Date: Mon, 21 Sep 2026 23:47:18 +0200 Subject: [PATCH 10/14] Fix style --- stdnum/kz/iin.py | 34 +++++++++++++++++----------------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/stdnum/kz/iin.py b/stdnum/kz/iin.py index 858848e2..6a083154 100644 --- a/stdnum/kz/iin.py +++ b/stdnum/kz/iin.py @@ -19,11 +19,10 @@ """IIN (ЖСН, Жеке сәйкестендіру нөмірі, Kazakhstani Individual identification number). It is a 12-digit number of which the first 6 digits denote the person's -birth date, the next three digits represent a birth order number +birth date, the next three digits represent a birth order number and the last digit is a check digit. * https://www.gov.kz/article/648?lang=kk -* https://www.oecd.org/en/networks/global-forum-tax-transparency/resources/aeoi-implementation-portal/tax-identification-numbers.html * https://ru.wikipedia.org/wiki/Индивидуальный_идентификационный_номер @@ -63,35 +62,38 @@ def compact(number: str) -> str: 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(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) + 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 + 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() + 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) @@ -101,9 +103,7 @@ def get_birth_date(number: str) -> datetime.date: 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: @@ -113,7 +113,7 @@ def get_gender(number: str) -> str | None: def validate(number: str) -> str: - """Check if the given Individual identification number is valid. + """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): From b8c03726637df640dc79ef7c4f9df06256e4429b Mon Sep 17 00:00:00 2001 From: Leandro Regueiro Date: Sat, 10 Sep 2022 15:48:54 +0200 Subject: [PATCH 11/14] Add support for Kazakhstan TIN Fixes #114. --- stdnum/kz/__init__.py | 24 +++ stdnum/kz/bin.py | 115 +++++++++++++ tests/test_kz_bin.doctest | 335 ++++++++++++++++++++++++++++++++++++++ 3 files changed, 474 insertions(+) create mode 100644 stdnum/kz/__init__.py create mode 100644 stdnum/kz/bin.py create mode 100644 tests/test_kz_bin.doctest diff --git a/stdnum/kz/__init__.py b/stdnum/kz/__init__.py new file mode 100644 index 00000000..b69a9caf --- /dev/null +++ b/stdnum/kz/__init__.py @@ -0,0 +1,24 @@ +# __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, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA +# 02110-1301 USA + +"""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..c9eba247 --- /dev/null +++ b/stdnum/kz/bin.py @@ -0,0 +1,115 @@ +# 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, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA +# 02110-1301 USA + +"""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: ... +>>> format('990 140 004 654') +'990140004654' +""" # noqa: E501 + +from stdnum.exceptions import * +from stdnum.util import clean, isdigits + + +def compact(number): + """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): + """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() + return number + + +def is_valid(number): + """Check if the number is a valid Kazakhstan BIN number.""" + try: + return bool(validate(number)) + except ValidationError: + return False + + +def format(number): + """Reformat the number to the standard presentation format.""" + return compact(number) diff --git a/tests/test_kz_bin.doctest b/tests/test_kz_bin.doctest new file mode 100644 index 00000000..8a74bf47 --- /dev/null +++ b/tests/test_kz_bin.doctest @@ -0,0 +1,335 @@ +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, 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.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: ... + + +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)] +[] From 32ba057c180561495b1ba265c5a9fc9ca5a50dc8 Mon Sep 17 00:00:00 2001 From: lucasicr <29181563+lucasicr@users.noreply.github.com> Date: Tue, 22 Sep 2026 10:32:20 +0200 Subject: [PATCH 12/14] Add Kazakhstan Business Identification Number validation --- stdnum/kz/__init__.py | 4 +--- stdnum/kz/bin.py | 19 ++++++++++++------- tests/test_kz_bin.doctest | 8 +++++--- 3 files changed, 18 insertions(+), 13 deletions(-) diff --git a/stdnum/kz/__init__.py b/stdnum/kz/__init__.py index b69a9caf..aa1ca58c 100644 --- a/stdnum/kz/__init__.py +++ b/stdnum/kz/__init__.py @@ -14,9 +14,7 @@ # 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 +# License along with this library; if not, see . """Collection of Kazakhstan numbers.""" diff --git a/stdnum/kz/bin.py b/stdnum/kz/bin.py index c9eba247..b1c3e301 100644 --- a/stdnum/kz/bin.py +++ b/stdnum/kz/bin.py @@ -14,9 +14,7 @@ # 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 +# License along with this library; if not, see . """BIN or "БСН" (Business Identification Number, "бизнес-сәйкестендіру нөмірі", Kazakhstan tax number). @@ -66,15 +64,20 @@ 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): +def compact(number: str) -> str: """Convert the number to the minimal representation. This strips the number of any valid separators and removes surrounding @@ -83,7 +86,7 @@ def compact(number): return clean(number, ' ') -def validate(number): +def validate(number: str) -> str: """Check if the number is a valid Kazakhstan BIN number. This checks the length and formatting. @@ -99,10 +102,12 @@ def validate(number): 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): +def is_valid(number: str) -> bool: """Check if the number is a valid Kazakhstan BIN number.""" try: return bool(validate(number)) @@ -110,6 +115,6 @@ def is_valid(number): return False -def format(number): +def format(number: str) -> str: """Reformat the number to the standard presentation format.""" return compact(number) diff --git a/tests/test_kz_bin.doctest b/tests/test_kz_bin.doctest index 8a74bf47..82d1dcaf 100644 --- a/tests/test_kz_bin.doctest +++ b/tests/test_kz_bin.doctest @@ -13,9 +13,7 @@ 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 +License along with this library; if not, see . This file contains more detailed doctests for the stdnum.kz.bin module. It @@ -53,6 +51,10 @@ InvalidComponent: ... 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. From 0d0a689fb4ec4c911027ebb0da7a971072662131 Mon Sep 17 00:00:00 2001 From: Luca <29181563+lucasicr@users.noreply.github.com> Date: Tue, 22 Sep 2026 11:52:03 +0200 Subject: [PATCH 13/14] Prevent future date output If the date returned is of the current year, it could be in the future, and thus invalidate the number. --- stdnum/kz/iin.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/stdnum/kz/iin.py b/stdnum/kz/iin.py index 6a083154..a3638c5c 100644 --- a/stdnum/kz/iin.py +++ b/stdnum/kz/iin.py @@ -83,7 +83,7 @@ def get_birth_date(number: str) -> datetime.date: if number[6] == '0': # no century info for foreign nationals today = datetime.date.today() year += (today.year // 100) * 100 - if year > today.year: + if year >= today.year: year -= 100 elif number[6] in '12': From 268af4334e1cc194c78ade59fa76debf75c5f986 Mon Sep 17 00:00:00 2001 From: Luca <29181563+lucasicr@users.noreply.github.com> Date: Tue, 22 Sep 2026 12:01:10 +0200 Subject: [PATCH 14/14] Update IIN description --- stdnum/kz/iin.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/stdnum/kz/iin.py b/stdnum/kz/iin.py index a3638c5c..319f80e8 100644 --- a/stdnum/kz/iin.py +++ b/stdnum/kz/iin.py @@ -19,7 +19,8 @@ """IIN (ЖСН, Жеке сәйкестендіру нөмірі, Kazakhstani Individual identification number). It is a 12-digit number of which the first 6 digits denote the person's -birth date, the next three digits represent a birth order number +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