Skip to content

Commit 582b3fe

Browse files
committed
Add support for Senegal TIN
Fixes arthurdejong#357
1 parent 6d366e3 commit 582b3fe

File tree

3 files changed

+353
-0
lines changed

3 files changed

+353
-0
lines changed

stdnum/sn/__init__.py

+24
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
# __init__.py - collection of Senegal numbers
2+
# coding: utf-8
3+
#
4+
# Copyright (C) 2023 Leandro Regueiro
5+
#
6+
# This library is free software; you can redistribute it and/or
7+
# modify it under the terms of the GNU Lesser General Public
8+
# License as published by the Free Software Foundation; either
9+
# version 2.1 of the License, or (at your option) any later version.
10+
#
11+
# This library is distributed in the hope that it will be useful,
12+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
13+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
14+
# Lesser General Public License for more details.
15+
#
16+
# You should have received a copy of the GNU Lesser General Public
17+
# License along with this library; if not, write to the Free Software
18+
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
19+
# 02110-1301 USA
20+
21+
"""Collection of Senegal numbers."""
22+
23+
# provide aliases
24+
from stdnum.sn import ninea as vat # noqa: F401

stdnum/sn/ninea.py

+147
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
1+
# ninea.py - functions for handling Senegal NINEA numbers
2+
# coding: utf-8
3+
#
4+
# Copyright (C) 2023 Leandro Regueiro
5+
#
6+
# This library is free software; you can redistribute it and/or
7+
# modify it under the terms of the GNU Lesser General Public
8+
# License as published by the Free Software Foundation; either
9+
# version 2.1 of the License, or (at your option) any later version.
10+
#
11+
# This library is distributed in the hope that it will be useful,
12+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
13+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
14+
# Lesser General Public License for more details.
15+
#
16+
# You should have received a copy of the GNU Lesser General Public
17+
# License along with this library; if not, write to the Free Software
18+
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
19+
# 02110-1301 USA
20+
21+
"""NINEA (Numéro d'Identification national des Entreprises et Associations,
22+
Senegal tax number).
23+
24+
This number consists of 7 digits (in practive it is sometimes 9 digits, the
25+
first two being zeroes). It is usually followed by a Tax Identification Code
26+
called COFI.
27+
28+
The COFI consists of 3 alphanumeric characters. The first one is a digit:
29+
30+
* 0: taxpayer subject to the real scheme, not subject to VAT.
31+
* 1: taxpayer subject to the single global contribution (TOU).
32+
* 2: taxpayer subject to the real scheme and subject to VAT.
33+
34+
The second character is a letter that indicates the tax center of attachment:
35+
36+
* A: Dakar Plateau 1.
37+
* B: Dakar Plateau 2.
38+
* C: Grand Dakar.
39+
* D: Pikine.
40+
* E: Rufisque.
41+
* F: Thiès.
42+
* G: Big Business Center.
43+
* H: Luga.
44+
* J: Diourbel.
45+
* K: Saint-Louis.
46+
* L: Tambacounda.
47+
* M: Kaolack.
48+
* N: Fatick.
49+
* P: A.
50+
* Q: Kolda.
51+
* R: remediated Parcels.
52+
* S: Liberal Professions.
53+
* T: Guédiawaye.
54+
* U: Dakar-Medina.
55+
* V: Dakar Freedom.
56+
* W: Matam.
57+
* Z: Medium Business Centre.
58+
59+
The third character is a digit that indicates the legal form of the taxpayer:
60+
61+
* 1: Individual-Natural person.
62+
* 2: SARL.
63+
* 3: SA.
64+
* 4: Simple Limited Partnership.
65+
* 5: Share Sponsorship Company.
66+
* 6: GIE.
67+
* 7: Civil Society.
68+
* 8: Partnership.
69+
* 9: Cooperative Association.
70+
* 0: Other.
71+
72+
More information:
73+
74+
* https://www.nkac-audit.com/en/comment-lire-un-numero-d-identifiant-fiscal-unique-ninea-au-senegal/
75+
* https://audifiscsn.com/en/2021/12/11/savoir-bien-lire-le-ninea-peut-vous-eviter-des-redressements-fiscaux/
76+
* https://www.creationdentreprise.sn/rechercher-une-societe
77+
78+
>>> validate('3067221')
79+
'3067221'
80+
>>> validate('30672212G2')
81+
'30672212G2'
82+
>>> validate('306 7221')
83+
'306 7221'
84+
>>> validate('3067221 2G2')
85+
'3067221 2G2'
86+
>>> validate('12345')
87+
Traceback (most recent call last):
88+
...
89+
InvalidLength: ...
90+
>>> format('30672212G2')
91+
'3067221 2G2'
92+
""" # noqa: E501
93+
94+
from stdnum.exceptions import *
95+
from stdnum.util import clean, isdigits
96+
97+
98+
TAX_CENTERS = ('A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'J', 'K', 'L', 'M', 'N',
99+
'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'Z')
100+
101+
102+
def compact(number):
103+
"""Convert the number to the minimal representation.
104+
105+
This strips the number of any valid separators and removes surrounding
106+
whitespace.
107+
"""
108+
return clean(number, ' -/,').upper().strip()
109+
110+
111+
def validate(number):
112+
"""Check if the number is a valid Senegal NINEA number.
113+
114+
This checks the length and formatting.
115+
"""
116+
number = compact(number)
117+
if len(number) not in (7, 9, 10, 12):
118+
raise InvalidLength()
119+
if len(number) in (7, 9):
120+
if not isdigits(number):
121+
raise InvalidFormat()
122+
return number
123+
if not isdigits(number[:-3]):
124+
raise InvalidFormat()
125+
if number[-3] not in ('0', '1', '2'):
126+
raise InvalidComponent()
127+
if number[-2] not in TAX_CENTERS:
128+
raise InvalidComponent()
129+
if not isdigits(number[-1]):
130+
raise InvalidComponent()
131+
return number
132+
133+
134+
def is_valid(number):
135+
"""Check if the number is a valid Senegal NINEA number."""
136+
try:
137+
return bool(validate(number))
138+
except ValidationError:
139+
return False
140+
141+
142+
def format(number):
143+
"""Reformat the number to the standard presentation format."""
144+
number = compact(number)
145+
if len(number) in (7, 9):
146+
return number
147+
return ' '.join([number[:-3], number[-3:]])

tests/test_sn_ninea.doctest

+182
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,182 @@
1+
test_sn_ninea.doctest - more detailed doctests for stdnum.sn.ninea module
2+
3+
Copyright (C) 2023 Leandro Regueiro
4+
5+
This library is free software; you can redistribute it and/or
6+
modify it under the terms of the GNU Lesser General Public
7+
License as published by the Free Software Foundation; either
8+
version 2.1 of the License, or (at your option) any later version.
9+
10+
This library is distributed in the hope that it will be useful,
11+
but WITHOUT ANY WARRANTY; without even the implied warranty of
12+
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
13+
Lesser General Public License for more details.
14+
15+
You should have received a copy of the GNU Lesser General Public
16+
License along with this library; if not, write to the Free Software
17+
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
18+
02110-1301 USA
19+
20+
21+
This file contains more detailed doctests for the stdnum.sn.ninea module. It
22+
tries to test more corner cases and detailed functionality that is not really
23+
useful as module documentation.
24+
25+
>>> from stdnum.sn import ninea
26+
27+
28+
Tests for some corner cases.
29+
30+
>>> ninea.validate('3067221')
31+
'3067221'
32+
>>> ninea.validate('30672212G2')
33+
'30672212G2'
34+
>>> ninea.validate('306 7221')
35+
'3067221'
36+
>>> ninea.validate('3067221 2G2')
37+
'30672212G2'
38+
>>> ninea.validate('3067221/2/G/2')
39+
'30672212G2'
40+
>>> ninea.validate('3067221-2G2')
41+
'30672212G2'
42+
>>> ninea.validate('12345')
43+
Traceback (most recent call last):
44+
...
45+
InvalidLength: ...
46+
>>> ninea.validate('VV34567')
47+
Traceback (most recent call last):
48+
...
49+
InvalidFormat: ...
50+
>>> ninea.validate('VV345670A0')
51+
Traceback (most recent call last):
52+
...
53+
InvalidFormat: ...
54+
>>> ninea.validate('12345679A0')
55+
Traceback (most recent call last):
56+
...
57+
InvalidComponent: ...
58+
>>> ninea.validate('12345670I0')
59+
Traceback (most recent call last):
60+
...
61+
InvalidComponent: ...
62+
>>> ninea.validate('12345670AV')
63+
Traceback (most recent call last):
64+
...
65+
InvalidComponent: ...
66+
>>> ninea.format('30672212G2')
67+
'3067221 2G2'
68+
69+
70+
These have been found online and should all be valid numbers.
71+
72+
>>> numbers = '''
73+
...
74+
... 30672212G2
75+
... 008895677
76+
... 0288846 2G3
77+
... 006269436
78+
... 0120 212
79+
... 005131305 2G3
80+
... 005844700
81+
... 2,838,516 2B3
82+
... 0,513,475 2C1
83+
... 005,830,866 1V1
84+
... 0,059 990 2G3
85+
... 006,364,472 2L2
86+
... 0,017,766 2G3
87+
... 5,729,803 2V2
88+
... 0,027,476 2G3
89+
... 005754339 2V2
90+
... 50 63 699 2E1
91+
... 0014051-2G3
92+
... 48522250G0
93+
... 0020884 2 G 3
94+
... 006295879
95+
... 1928863 2B2
96+
... 00830 48 0 C 9
97+
... 4069367 2G3
98+
... 005241550 2C2
99+
... 006416681
100+
... 26080342R2
101+
... 0283 408-2C2
102+
... 49615470C0
103+
... 22486742 S 3
104+
... 20839132 S 3
105+
... 004641363
106+
... 0063150572G2
107+
... 61523762A2
108+
... 2079376/2/G/3
109+
... 008135114
110+
... 006208434
111+
... 005046174
112+
... 0044440722V1
113+
... 00153142G3
114+
... 005117355
115+
... 30092572G3
116+
... 2139378 2V2
117+
... 0045799442C2
118+
... 006373295/0A9
119+
... 002420983 2G3
120+
... 244982000
121+
... 006946034
122+
... 0316390
123+
... 008517560
124+
... 005023081
125+
... 00569042P2
126+
... 26581702G2
127+
... 25833512R2
128+
... 2599770 2 B 2
129+
... 255 44 772 S 3
130+
... 004343430
131+
... 0366 709 2S2
132+
... 007039292
133+
... 00722992 G 3
134+
... 007307748 1V1
135+
... 0051126442L1
136+
... 00154212G3
137+
... 0185844 2 R 2
138+
... 0332891
139+
... 26132492D6
140+
... 007057947
141+
... 006325741
142+
... 21948852B9
143+
... 007660740
144+
... 0404913 2B1
145+
... 21409612D1
146+
... 00661012S3
147+
... 0149642
148+
... 00605 33 92
149+
... 006900387
150+
... 006715314 2G3
151+
... 2160472-2G3
152+
... 25437852G3
153+
... 00140012G3
154+
... 81329702S1
155+
... 5435 468 0G0
156+
... 0046 00096 2S9
157+
... 002502343
158+
... 270 773 72 S2
159+
... 00722992G3
160+
... 0108531 2G3
161+
... 2929406 0G0
162+
... 19370542G2
163+
... 007266126
164+
... 008895586
165+
... 41130152C2
166+
... 007389100
167+
... 005216371 2V3
168+
... 008086242 1E1
169+
... 00284430 C0
170+
... 006777463
171+
... 007912662
172+
... 0053655402R2
173+
... 24312110V9
174+
... 007992482 2A3
175+
... 0019366
176+
... 004237633 2B2
177+
... 005371026
178+
... 004912269
179+
...
180+
... '''
181+
>>> [x for x in numbers.splitlines() if x and not ninea.is_valid(x)]
182+
[]

0 commit comments

Comments
 (0)