Skip to content

Commit a6e58a4

Browse files
committed
Add support for Tunisia TIN
Fixes #309.
1 parent e40c827 commit a6e58a4

File tree

3 files changed

+432
-0
lines changed

3 files changed

+432
-0
lines changed

stdnum/tn/__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 Tunisian numbers
2+
# coding: utf-8
3+
#
4+
# Copyright (C) 2022 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 Tunisian numbers."""
22+
23+
# provide aliases
24+
from stdnum.tn import mf as vat # noqa: F401

stdnum/tn/mf.py

Lines changed: 154 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,154 @@
1+
# pin.py - functions for handling Tunisia MF numbers
2+
# coding: utf-8
3+
#
4+
# Copyright (C) 2022 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+
"""MF (Matricule Fiscal, Tunisia tax number).
22+
23+
The MF consists of 4 parts: the "identifiant fiscal", the "code TVA", the "code
24+
catégorie" and the "numéro d'etablissement secondaire".
25+
26+
The "identifiant fiscal" consists of 2 parts: the "identifiant unique" and the
27+
"clef de contrôle". The "identifiant unique" is composed of 7 digits. The "clef
28+
de contrôle" is a letter, excluding "I", "O" and "U" because of their
29+
similarity to "1", "0" and "4".
30+
31+
The "code TVA" is a letter that tells which VAT regime is being used. The valid
32+
values are "A", "P", "B", "D" and "N".
33+
34+
The "code catégorie" is a letter that tells the category the contributor
35+
belongs to. The valid values are "M", "P", "C", "N" and "E".
36+
37+
The "numéro d'etablissement secondaire" consists of 3 digits. It is usually
38+
"000", but it can be "001", "002"... depending on the branches. If it is not
39+
"000" then "code catégorie" must be "E".
40+
41+
More information:
42+
43+
* https://futurexpert.tn/2019/10/22/structure-et-utilite-du-matricule-fiscal/
44+
* https://www.registre-entreprises.tn/
45+
46+
>>> validate('1234567/M/A/E/001')
47+
'1234567MAE001'
48+
>>> validate('000 123 LAM 000')
49+
'0000123LAM000'
50+
>>> validate('1282182 / W')
51+
'1282182W'
52+
>>> validate('121J')
53+
'0000121J'
54+
>>> validate('12345')
55+
Traceback (most recent call last):
56+
...
57+
InvalidLength: ...
58+
>>> validate('000/M/A/1222334L')
59+
Traceback (most recent call last):
60+
...
61+
InvalidLength: ...
62+
>>> validate('1219773U')
63+
Traceback (most recent call last):
64+
...
65+
InvalidFormat: ...
66+
>>> validate('1234567/M/A/X/000')
67+
Traceback (most recent call last):
68+
...
69+
InvalidFormat: ...
70+
>>> format('1282182 / W')
71+
'1282182/W'
72+
>>> format('121J')
73+
'0000121/J'
74+
>>> format('1496298 T P N 000')
75+
'1496298/T/P/N/000'
76+
"""
77+
78+
from stdnum.exceptions import *
79+
from stdnum.util import clean, isdigits
80+
81+
82+
VALID_CONTROL_KEYS = ('A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'J', 'K', 'L',
83+
'M', 'N', 'P', 'Q', 'R', 'S', 'T', 'V', 'W', 'X', 'Y',
84+
'Z')
85+
VALID_TVA_CODES = ('A', 'P', 'B', 'D', 'N')
86+
VALID_CATEGORY_CODES = ('M', 'P', 'C', 'N', 'E')
87+
88+
89+
def compact(number):
90+
"""Convert the number to the minimal representation.
91+
92+
This strips the number of any valid separators, removes surrounding
93+
whitespace.
94+
"""
95+
result = clean(number, ' /.-').upper()
96+
97+
# Find position of first letter.
98+
first_letter_index = 7
99+
100+
for i, char in enumerate(result):
101+
if char not in ('0', '1', '2', '3', '4', '5', '6', '7', '8', '9'):
102+
first_letter_index = i
103+
break
104+
105+
# Number must begin with a digit, so abort otherwise.
106+
if first_letter_index == 0:
107+
return result
108+
109+
# Pad with enough zeros.
110+
if first_letter_index < 7:
111+
result = result.zfill(7 + len(result[first_letter_index:]))
112+
113+
return result
114+
115+
116+
def validate(number):
117+
"""Check if the number is a valid Tunisia MF number.
118+
119+
This checks the length and formatting.
120+
"""
121+
number = compact(number)
122+
if len(number) not in (8, 13):
123+
raise InvalidLength()
124+
if not isdigits(number[:7]):
125+
raise InvalidFormat()
126+
if number[7] not in VALID_CONTROL_KEYS:
127+
raise InvalidFormat()
128+
if len(number) == 8:
129+
return number
130+
if number[8] not in VALID_TVA_CODES:
131+
raise InvalidFormat()
132+
if number[9] not in VALID_CATEGORY_CODES:
133+
raise InvalidFormat()
134+
if not isdigits(number[10:]):
135+
raise InvalidFormat()
136+
if number[10:] != '000' and number[9] != 'E':
137+
raise InvalidFormat()
138+
return number
139+
140+
141+
def is_valid(number):
142+
"""Check if the number is a valid Tunisia MF number."""
143+
try:
144+
return bool(validate(number))
145+
except ValidationError:
146+
return False
147+
148+
149+
def format(number):
150+
"""Reformat the number to the standard presentation format."""
151+
result = compact(number)
152+
if len(result) == 8:
153+
return '/'.join([result[:7], result[7]])
154+
return '/'.join([result[:7], result[7], result[8], result[9], result[10:]])

0 commit comments

Comments
 (0)