Skip to content

Commit 38e3be3

Browse files
committed
Add support for Benin TIN
Fixes arthurdejong#376
1 parent 6d366e3 commit 38e3be3

File tree

3 files changed

+323
-0
lines changed

3 files changed

+323
-0
lines changed

stdnum/bj/__init__.py

+24
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
# __init__.py - collection of Benin 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 Benin numbers."""
22+
23+
# provide aliases
24+
from stdnum.bj import ifu as vat # noqa: F401

stdnum/bj/ifu.py

+103
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
# ifu.py - functions for handling Benin IFU 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+
"""IFU (Identifiant Fiscal Unique, Benin tax number).
22+
23+
This number consists of 13 digits.
24+
25+
The first digit indicates the type of entity:
26+
* 1: Individual male
27+
* 2: Individual female
28+
* 3: Legal entity / company
29+
* 4: Legal person / state structure
30+
* 5: Legal person / international organization and mission diplomatic
31+
* 6: Legal person / non-governmental organization
32+
33+
The following four digits give the year. The next six digits are a unique
34+
identifier within that year.
35+
36+
The next digit indicates either:
37+
38+
* 1: a parent company
39+
* 2-9: subsidiary or agencies
40+
* 0: other types of person or taxpayer
41+
42+
The final digit is a check digit, which is used to verify the number was
43+
correctly typed.
44+
45+
More information:
46+
47+
* http://www.finances.bj/sousSites/dgi/wp-content/uploads/2016/12/IFU.pdf
48+
49+
>>> validate('3201910583176')
50+
'3201910583176'
51+
>>> validate('3201 30109 9116')
52+
'3201301099116'
53+
>>> validate('12345')
54+
Traceback (most recent call last):
55+
...
56+
InvalidLength: ...
57+
>>> format('3201 30109 9116')
58+
'3201301099116'
59+
""" # noqa: E501
60+
61+
from datetime import date
62+
63+
from stdnum.exceptions import *
64+
from stdnum.util import clean, isdigits
65+
66+
67+
def compact(number):
68+
"""Convert the number to the minimal representation.
69+
70+
This strips the number of any valid separators and removes surrounding
71+
whitespace.
72+
"""
73+
return clean(number, ' -').strip()
74+
75+
76+
def validate(number):
77+
"""Check if the number is a valid Benin IFU number.
78+
79+
This checks the length and formatting.
80+
"""
81+
number = compact(number)
82+
if len(number) != 13:
83+
raise InvalidLength()
84+
if not isdigits(number):
85+
raise InvalidFormat()
86+
if number[0] not in ('1', '2', '3', '4', '5', '6'):
87+
raise InvalidComponent()
88+
if number[1:5] > date.today().strftime('%Y'):
89+
raise InvalidComponent()
90+
return number
91+
92+
93+
def is_valid(number):
94+
"""Check if the number is a valid Benin IFU number."""
95+
try:
96+
return bool(validate(number))
97+
except ValidationError:
98+
return False
99+
100+
101+
def format(number):
102+
"""Reformat the number to the standard presentation format."""
103+
return compact(number)

tests/test_bj_ifu.doctest

+196
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,196 @@
1+
test_bj_ifu.doctest - more detailed doctests for stdnum.bj.ifu 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.bj.ifu 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.bj import ifu
26+
27+
28+
Tests for some corner cases.
29+
30+
>>> ifu.validate('3201910583176')
31+
'3201910583176'
32+
>>> ifu.validate('3201 30109 9116')
33+
'3201301099116'
34+
>>> ifu.validate('320-110-2073316')
35+
'3201102073316'
36+
>>> ifu.validate('12345')
37+
Traceback (most recent call last):
38+
...
39+
InvalidLength: ...
40+
>>> ifu.validate('VV34567890123')
41+
Traceback (most recent call last):
42+
...
43+
InvalidFormat: ...
44+
>>> ifu.validate('9200067890123')
45+
Traceback (most recent call last):
46+
...
47+
InvalidComponent: ...
48+
>>> ifu.validate('3999967890123')
49+
Traceback (most recent call last):
50+
...
51+
InvalidComponent: ...
52+
>>> ifu.format('3201 30109 9116')
53+
'3201301099116'
54+
55+
56+
These have been found online and should all be valid numbers.
57+
58+
>>> numbers = '''
59+
...
60+
... 3201910650626
61+
... 3201910583176
62+
... 4201001686513
63+
... 3201 30109 9116
64+
... 3200700033415
65+
... 3200900261914
66+
... 3200800032118
67+
... 3200800032118
68+
... 3200800032118
69+
... 3200700034111
70+
... 3200800032118
71+
... 1201301488009
72+
... 3200900161518
73+
... 3201100806612
74+
... 3200901052914
75+
... 3200800777415
76+
... 3202011243136
77+
... 3201300602910
78+
... 3201910624299
79+
... 3200800555113
80+
... 3200800579315
81+
... 3200700024415
82+
... 3200700110113
83+
... 3200700092919
84+
... 3200700092919
85+
... 3201000393119
86+
... 3201810208081
87+
... 3201810468789
88+
... 1201510370202
89+
... 1201701326901
90+
... 3200801478417
91+
... 1200801282305
92+
... 1201700917809
93+
... 3200700044213
94+
... 3200800203617
95+
... 3200801478417
96+
... 3200800818719
97+
... 3200900360812
98+
... 3200801443912
99+
... 3201100075919
100+
... 3200900261914
101+
... 3202011243136
102+
... 3201910632079
103+
... 3200800667517
104+
... 3201500238418
105+
... 3201501125118
106+
... 3200800583113
107+
... 3201810302352
108+
... 1201301488009
109+
... 3200900161518
110+
... 3201100806612
111+
... 3200901052914
112+
... 3200800777415
113+
... 3202011243136
114+
... 3201300602910
115+
... 3201910624299
116+
... 3200800555113
117+
... 3200800579315
118+
... 3200700024415
119+
... 3200700110113
120+
... 3200700092919
121+
... 3201000393119
122+
... 3201810208081
123+
... 3201810468789
124+
... 1201510370202
125+
... 1201701326901
126+
... 3200801478417
127+
... 1200801282305
128+
... 1201700917809
129+
... 3200800203617
130+
... 3200800818719
131+
... 3200900261914
132+
... 3200900360812
133+
... 3200801443912
134+
... 3201100075919
135+
... 3201910632079
136+
... 3200800667517
137+
... 3201500238418
138+
... 3201501125118
139+
... 3200800583113
140+
... 3201810302352
141+
... 3201301077418
142+
... 1201201249906
143+
... 1201400877201
144+
... 3200800646619
145+
... 3201643198031
146+
... 3201007665114
147+
... 1201200548009
148+
... 1201642938909
149+
... 1201641325203
150+
... 3201100951210
151+
... 2201500672302
152+
... 3201701918418
153+
... 2201641943708
154+
... 2201300191201
155+
... 3201910336807
156+
... 3201710167035
157+
... 2201644099708
158+
... 1201201292100
159+
... 1201500706605
160+
... 1201001231403
161+
... 3201000909217
162+
... 1201642988403
163+
... 1201500703604
164+
... 2201405651202
165+
... 3200800565618
166+
... 3201910638526
167+
... 3200800074214
168+
... 3200700065718
169+
... 3201301099116
170+
... 3201810230787
171+
... 4201641583612
172+
... 3201810464026
173+
... 3 200 7 000 21213
174+
... 320-110-2073316
175+
... 1201642788000
176+
... 1201642565000
177+
... 1201641158203
178+
... 3200900819518
179+
... 3200900819113
180+
... 3200700076616
181+
... 3201200605910
182+
... 3200800583113
183+
... 3200700018516
184+
... 3200800696112
185+
... 3202113085279
186+
... 3201810397557
187+
... 3201642997218
188+
... 1201502253302
189+
... 1201200731706
190+
... 6 2020 1197 2710
191+
... 3201810336807
192+
... 3200700065718
193+
...
194+
... '''
195+
>>> [x for x in numbers.splitlines() if x and not ifu.is_valid(x)]
196+
[]

0 commit comments

Comments
 (0)