From 715a56ad7f797277adb2a4a3014b9e2d2ab39a56 Mon Sep 17 00:00:00 2001 From: Leandro Regueiro Date: Sun, 12 Feb 2023 16:43:37 +0100 Subject: [PATCH] Add support for Senegal TIN Fixes #357 --- stdnum/sn/__init__.py | 24 +++++ stdnum/sn/ninea.py | 165 ++++++++++++++++++++++++++++++++ tests/test_sn_ninea.doctest | 184 ++++++++++++++++++++++++++++++++++++ 3 files changed, 373 insertions(+) create mode 100644 stdnum/sn/__init__.py create mode 100644 stdnum/sn/ninea.py create mode 100644 tests/test_sn_ninea.doctest diff --git a/stdnum/sn/__init__.py b/stdnum/sn/__init__.py new file mode 100644 index 00000000..64d908b3 --- /dev/null +++ b/stdnum/sn/__init__.py @@ -0,0 +1,24 @@ +# __init__.py - collection of Senegal numbers +# coding: utf-8 +# +# Copyright (C) 2023 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 Senegal numbers.""" + +# provide aliases +from stdnum.sn import ninea as vat # noqa: F401 diff --git a/stdnum/sn/ninea.py b/stdnum/sn/ninea.py new file mode 100644 index 00000000..8904cd6f --- /dev/null +++ b/stdnum/sn/ninea.py @@ -0,0 +1,165 @@ +# ninea.py - functions for handling Senegal NINEA numbers +# coding: utf-8 +# +# Copyright (C) 2023 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 + +"""NINEA (Numéro d'Identification national des Entreprises et Associations, +Senegal tax number). + +This number consists of 7 digits (in practive it is sometimes 9 digits, the +first two being zeroes). It is usually followed by a Tax Identification Code +called COFI. + +The COFI consists of 3 alphanumeric characters. The first one is a digit: + +* 0: taxpayer subject to the real scheme, not subject to VAT. +* 1: taxpayer subject to the single global contribution (TOU). +* 2: taxpayer subject to the real scheme and subject to VAT. + +The second character is a letter that indicates the tax center of attachment: + +* A: Dakar Plateau 1. +* B: Dakar Plateau 2. +* C: Grand Dakar. +* D: Pikine. +* E: Rufisque. +* F: Thiès. +* G: Big Business Center. +* H: Luga. +* J: Diourbel. +* K: Saint-Louis. +* L: Tambacounda. +* M: Kaolack. +* N: Fatick. +* P: A. +* Q: Kolda. +* R: remediated Parcels. +* S: Liberal Professions. +* T: Guédiawaye. +* U: Dakar-Medina. +* V: Dakar Freedom. +* W: Matam. +* Z: Medium Business Centre. + +The third character is a digit that indicates the legal form of the taxpayer: + +* 1: Individual-Natural person. +* 2: SARL. +* 3: SA. +* 4: Simple Limited Partnership. +* 5: Share Sponsorship Company. +* 6: GIE. +* 7: Civil Society. +* 8: Partnership. +* 9: Cooperative Association. +* 0: Other. + +More information: + +* https://www.nkac-audit.com/en/comment-lire-un-numero-d-identifiant-fiscal-unique-ninea-au-senegal/ +* https://audifiscsn.com/en/2021/12/11/savoir-bien-lire-le-ninea-peut-vous-eviter-des-redressements-fiscaux/ +* https://www.creationdentreprise.sn/rechercher-une-societe + +>>> validate('3067221') +'3067221' +>>> validate('30672212G2') +'30672212G2' +>>> validate('306 7221') +'3067221' +>>> validate('3067221 2G2') +'30672212G2' +>>> validate('12345') +Traceback (most recent call last): + ... +InvalidLength: ... +>>> validate('VV34567') +Traceback (most recent call last): + ... +InvalidFormat: ... +>>> validate('VV345670A0') +Traceback (most recent call last): + ... +InvalidFormat: ... +>>> validate('12345679A0') +Traceback (most recent call last): + ... +InvalidComponent: ... +>>> validate('12345670I0') +Traceback (most recent call last): + ... +InvalidComponent: ... +>>> validate('12345670AZ') +Traceback (most recent call last): + ... +InvalidComponent: ... +>>> format('30672212G2') +'3067221 2G2' +""" # noqa: E501 + +from stdnum.exceptions import * +from stdnum.util import clean, isdigits + + +TAX_CENTERS = ('A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'J', 'K', 'L', 'M', 'N', + 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'Z') + + +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, ' -/,').upper().strip() + + +def validate(number): + """Check if the number is a valid Senegal NINEA number. + + This checks the length and formatting. + """ + number = compact(number) + if len(number) not in (7, 9, 10, 12): + raise InvalidLength() + if len(number) in (7, 9) and not isdigits(number): + raise InvalidFormat() + if len(number) in (10, 12) and not isdigits(number[:-3]): + raise InvalidFormat() + if len(number) in (10, 12) and number[-3] not in ('0', '1', '2'): + raise InvalidComponent() + if len(number) in (10, 12) and number[-2] not in TAX_CENTERS: + raise InvalidComponent() + if len(number) in (10, 12) and not isdigits(number[-1]): + raise InvalidComponent() + return number + + +def is_valid(number): + """Check if the number is a valid Senegal NINEA number.""" + try: + return bool(validate(number)) + except ValidationError: + return False + + +def format(number): + """Reformat the number to the standard presentation format.""" + number = compact(number) + if len(number) in (7, 9): + return number + return ' '.join([number[:-3], number[-3:]]) diff --git a/tests/test_sn_ninea.doctest b/tests/test_sn_ninea.doctest new file mode 100644 index 00000000..5d5a3c20 --- /dev/null +++ b/tests/test_sn_ninea.doctest @@ -0,0 +1,184 @@ +test_sn_ninea.doctest - more detailed doctests for stdnum.sn.ninea module + +Copyright (C) 2023 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.sn.ninea module. It +tries to test more corner cases and detailed functionality that is not really +useful as module documentation. + +>>> from stdnum.sn import ninea + + +Tests for some corner cases. + +>>> ninea.validate('3067221') +'3067221' +>>> ninea.validate('30672212G2') +'30672212G2' +>>> ninea.validate('306 7221') +'3067221' +>>> ninea.validate('3067221 2G2') +'30672212G2' +>>> ninea.validate('3067221/2/G/2') +'30672212G2' +>>> ninea.validate('3067221-2G2') +'30672212G2' +>>> ninea.validate('12345') +Traceback (most recent call last): + ... +InvalidLength: ... +>>> ninea.validate('VV34567') +Traceback (most recent call last): + ... +InvalidFormat: ... +>>> ninea.validate('VV345670A0') +Traceback (most recent call last): + ... +InvalidFormat: ... +>>> ninea.validate('12345679A0') +Traceback (most recent call last): + ... +InvalidComponent: ... +>>> ninea.validate('12345670I0') +Traceback (most recent call last): + ... +InvalidComponent: ... +>>> ninea.validate('12345670AV') +Traceback (most recent call last): + ... +InvalidComponent: ... +>>> ninea.format('306 7221') +'3067221' +>>> ninea.format('30672212G2') +'3067221 2G2' + + +These have been found online and should all be valid numbers. + +>>> numbers = ''' +... +... 30672212G2 +... 008895677 +... 0288846 2G3 +... 006269436 +... 0120 212 +... 005131305 2G3 +... 005844700 +... 2,838,516 2B3 +... 0,513,475 2C1 +... 005,830,866 1V1 +... 0,059 990 2G3 +... 006,364,472 2L2 +... 0,017,766 2G3 +... 5,729,803 2V2 +... 0,027,476 2G3 +... 005754339 2V2 +... 50 63 699 2E1 +... 0014051-2G3 +... 48522250G0 +... 0020884 2 G 3 +... 006295879 +... 1928863 2B2 +... 00830 48 0 C 9 +... 4069367 2G3 +... 005241550 2C2 +... 006416681 +... 26080342R2 +... 0283 408-2C2 +... 49615470C0 +... 22486742 S 3 +... 20839132 S 3 +... 004641363 +... 0063150572G2 +... 61523762A2 +... 2079376/2/G/3 +... 008135114 +... 006208434 +... 005046174 +... 0044440722V1 +... 00153142G3 +... 005117355 +... 30092572G3 +... 2139378 2V2 +... 0045799442C2 +... 006373295/0A9 +... 002420983 2G3 +... 244982000 +... 006946034 +... 0316390 +... 008517560 +... 005023081 +... 00569042P2 +... 26581702G2 +... 25833512R2 +... 2599770 2 B 2 +... 255 44 772 S 3 +... 004343430 +... 0366 709 2S2 +... 007039292 +... 00722992 G 3 +... 007307748 1V1 +... 0051126442L1 +... 00154212G3 +... 0185844 2 R 2 +... 0332891 +... 26132492D6 +... 007057947 +... 006325741 +... 21948852B9 +... 007660740 +... 0404913 2B1 +... 21409612D1 +... 00661012S3 +... 0149642 +... 00605 33 92 +... 006900387 +... 006715314 2G3 +... 2160472-2G3 +... 25437852G3 +... 00140012G3 +... 81329702S1 +... 5435 468 0G0 +... 0046 00096 2S9 +... 002502343 +... 270 773 72 S2 +... 00722992G3 +... 0108531 2G3 +... 2929406 0G0 +... 19370542G2 +... 007266126 +... 008895586 +... 41130152C2 +... 007389100 +... 005216371 2V3 +... 008086242 1E1 +... 00284430 C0 +... 006777463 +... 007912662 +... 0053655402R2 +... 24312110V9 +... 007992482 2A3 +... 0019366 +... 004237633 2B2 +... 005371026 +... 004912269 +... +... ''' +>>> [x for x in numbers.splitlines() if x and not ninea.is_valid(x)] +[]