Skip to content

Commit dd22123

Browse files
unhoarthurdejong
authored andcommitted
Add support for Senegal TIN
Closes #395 Closes #357
1 parent d3ec3bd commit dd22123

3 files changed

Lines changed: 356 additions & 0 deletions

File tree

stdnum/sn/__init__.py

Lines changed: 24 additions & 0 deletions
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

Lines changed: 144 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,144 @@
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 Nationale des Entreprises et Associations, Senegal tax number).
22+
23+
The National Identification Number for Businesses and Associations (NINEA) is
24+
a unique tax identifier for tax purposes in Senegal.
25+
26+
This number consists of 7 digits and is usually followed by a 3 digit tax
27+
identification code called COFI (Code d’Identification Fiscale) that is used
28+
to indicate the company's tax status and legal structure.
29+
30+
More information:
31+
32+
* https://www.wikiprocedure.com/index.php/Senegal_-_Obtain_a_Tax_Identification_Number
33+
* https://nkac-audit.com/comprendre-le-ninea-votre-guide-de-lecture-simplifie/
34+
* https://www.creationdentreprise.sn/rechercher-une-societe
35+
* https://www.dci-sn.sn/index.php/obtenir-son-ninea
36+
* https://e-ninea.ansd.sn/search_annuaire
37+
38+
>>> validate('306 7221')
39+
'3067221'
40+
>>> validate('30672212G2')
41+
'30672212G2'
42+
>>> validate('3067221 2G2')
43+
'30672212G2'
44+
>>> validate('1234567 0AZ')
45+
Traceback (most recent call last):
46+
...
47+
InvalidComponent: ...
48+
>>> format('30672212G2')
49+
'3067221 2G2'
50+
""" # noqa: E501
51+
52+
from stdnum.exceptions import *
53+
from stdnum.util import clean, isdigits
54+
55+
56+
def compact(number: str) -> str:
57+
"""Convert the number to the minimal representation.
58+
59+
This strips the number of any valid separators and removes surrounding
60+
whitespace.
61+
"""
62+
return clean(number, ' -/,').upper().strip()
63+
64+
65+
def _validate_cofi(number: str) -> None:
66+
# The first digit of the COFI indicates the tax status
67+
# 0: taxpayer subject to the real scheme, not subject to VAT.
68+
# 1: taxpayer subject to the single global contribution (TOU).
69+
# 2: taxpayer subject to the real scheme and subject to VAT.
70+
if number[0] not in '012':
71+
raise InvalidComponent()
72+
# The second character is a letter that indicates the tax centre:
73+
# A: Dakar Plateau 1
74+
# B: Dakar Plateau 2
75+
# C: Grand Dakar
76+
# D: Pikine
77+
# E: Rufisque
78+
# F: Thiès
79+
# G: Centre des grandes Entreprises
80+
# H: Louga
81+
# J: Diourbel
82+
# K: Saint-Louis
83+
# L: Tambacounda
84+
# M: Kaolack
85+
# N: Fatick
86+
# P: Ziguinchor
87+
# Q: Kolda
88+
# R: Prarcelles assainies
89+
# S: Professions libérales
90+
# T: Guédiawaye
91+
# U: Dakar-Medina
92+
# V: Dakar liberté
93+
# W: Matam
94+
# Z: Centre des Moyennes Entreprises
95+
if number[1] not in 'ABCDEFGHJKLMNPQRSTUVWZ':
96+
raise InvalidComponent()
97+
# The third character is a digit that indicates the legal form:
98+
# 1: Individual-Natural person
99+
# 2: SARL
100+
# 3: SA
101+
# 4: Simple Limited Partnership
102+
# 5: Share Sponsorship Company
103+
# 6: GIE
104+
# 7: Civil Society
105+
# 8: Partnership
106+
# 9: Cooperative Association
107+
# 0: Other
108+
if number[2] not in '0123456789':
109+
raise InvalidComponent()
110+
111+
112+
def validate(number: str) -> str:
113+
"""Check if the number is a valid Senegal NINEA.
114+
115+
This checks the length and formatting.
116+
"""
117+
cofi = ''
118+
number = compact(number)
119+
if len(number) > 9:
120+
cofi = number[-3:]
121+
number = number[:-3]
122+
if len(number) not in (7, 9):
123+
raise InvalidLength()
124+
if not isdigits(number):
125+
raise InvalidFormat()
126+
if cofi:
127+
_validate_cofi(cofi)
128+
return number + cofi
129+
130+
131+
def is_valid(number: str) -> bool:
132+
"""Check if the number is a valid Senegal NINEA."""
133+
try:
134+
return bool(validate(number))
135+
except ValidationError:
136+
return False
137+
138+
139+
def format(number: str) -> str:
140+
"""Reformat the number to the standard presentation format."""
141+
number = compact(number)
142+
if len(number) in (7, 9):
143+
return number
144+
return ' '.join([number[:-3], number[-3:]])

tests/test_sn_ninea.doctest

Lines changed: 188 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,188 @@
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('306 7221')
67+
'3067221'
68+
>>> ninea.format('30672212G2')
69+
'3067221 2G2'
70+
71+
72+
These have been found online and should all be valid numbers.
73+
74+
>>> numbers = '''
75+
...
76+
... 0,017,766 2G3
77+
... 0,027,476 2G3
78+
... 0,059 990 2G3
79+
... 0,513,475 2C1
80+
... 00140012G3
81+
... 0014051-2G3
82+
... 0015041
83+
... 00153142G3
84+
... 00154212G3
85+
... 0019366
86+
... 0020884 2 G 3
87+
... 002420983 2G3
88+
... 002502343
89+
... 00284430 C0
90+
... 004237633 2B2
91+
... 004343430
92+
... 0044440722V1
93+
... 0045799442C2
94+
... 0046 00096 2S9
95+
... 004641363
96+
... 004912269
97+
... 005,830,866 1V1
98+
... 005023081
99+
... 005046174
100+
... 0051126442L1
101+
... 005117355
102+
... 005131305 2G3
103+
... 005216371 2V3
104+
... 005241550 2C2
105+
... 0053655402R2
106+
... 005371026
107+
... 005623998
108+
... 00569042P2
109+
... 005721809
110+
... 005754339 2V2
111+
... 005844700
112+
... 006,364,472 2L2
113+
... 00605 33 92
114+
... 006208434
115+
... 006269436
116+
... 006295879
117+
... 0063150572G2
118+
... 006325741
119+
... 006373295/0A9
120+
... 006416681
121+
... 00661012S3
122+
... 006715314 2G3
123+
... 006777463
124+
... 006900387
125+
... 006946034
126+
... 007039292
127+
... 007057947
128+
... 00722992 G 3
129+
... 00722992G3
130+
... 007266126
131+
... 007307748 1V1
132+
... 007389100
133+
... 007660740
134+
... 007912662
135+
... 007992482 2A3
136+
... 008086242 1E1
137+
... 008135114
138+
... 00830 48 0 C 9
139+
... 008517560
140+
... 008895586
141+
... 008895677
142+
... 0108531 2G3
143+
... 0120 212
144+
... 0149642
145+
... 0185844 2 R 2
146+
... 0283 408-2C2
147+
... 0288846 2G3
148+
... 0316093
149+
... 0316390
150+
... 0332891
151+
... 0366 709 2S2
152+
... 0404913 2B1
153+
... 1928863 2B2
154+
... 19370542G2
155+
... 2,838,516 2B3
156+
... 2079376/2/G/3
157+
... 20839132 S 3
158+
... 2139378 2V2
159+
... 21409612D1
160+
... 2160472-2G3
161+
... 21948852B9
162+
... 22486742 S 3
163+
... 24312110V9
164+
... 244982000
165+
... 25437852G3
166+
... 255 44 772 S 3
167+
... 25833512R2
168+
... 2599770 2 B 2
169+
... 26080342R2
170+
... 26132492D6
171+
... 26581702G2
172+
... 270 773 72 S2
173+
... 2929406 0G0
174+
... 30092572G3
175+
... 30672212G2
176+
... 4069367 2G3
177+
... 41130152C2
178+
... 48522250G0
179+
... 49615470C0
180+
... 5,729,803 2V2
181+
... 50 63 699 2E1
182+
... 5435 468 0G0
183+
... 61523762A2
184+
... 81329702S1
185+
...
186+
... '''
187+
>>> [x for x in numbers.splitlines() if x and not ninea.is_valid(x)]
188+
[]

0 commit comments

Comments
 (0)