Skip to content

Commit 1773a91

Browse files
lucasicrarthurdejong
authored andcommitted
Add support for Mozambique TIN
Closes #360 Closes #392 Closes #473
1 parent 5ef6ade commit 1773a91

File tree

3 files changed

+260
-0
lines changed

3 files changed

+260
-0
lines changed

stdnum/mz/__init__.py

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
# __init__.py - collection of Mozambique 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 Mozambique numbers."""
22+
23+
from __future__ import annotations
24+
25+
# provide aliases
26+
from stdnum.mz import nuit as vat # noqa: F401

stdnum/mz/nuit.py

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
# nuit.py - functions for handling Mozambique NUIT numbers
2+
# coding: utf-8
3+
#
4+
# Copyright (C) 2023 Leandro Regueiro
5+
# Copyright (C) 2025 Luca Sicurello
6+
#
7+
# This library is free software; you can redistribute it and/or
8+
# modify it under the terms of the GNU Lesser General Public
9+
# License as published by the Free Software Foundation; either
10+
# version 2.1 of the License, or (at your option) any later version.
11+
#
12+
# This library is distributed in the hope that it will be useful,
13+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
14+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
15+
# Lesser General Public License for more details.
16+
#
17+
# You should have received a copy of the GNU Lesser General Public
18+
# License along with this library; if not, write to the Free Software
19+
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
20+
# 02110-1301 USA
21+
22+
"""NUIT (Número Único de Identificação Tributaria, Mozambique tax number).
23+
24+
This number consists of 9 digits, sometimes separated in three groups of three
25+
digits using whitespace to make it easier to read.
26+
27+
The first digit indicates the type of entity. The next seven digits are a
28+
sequential number. The last digit is the check digit, which is used to verify
29+
the number was correctly typed.
30+
31+
More information:
32+
33+
* https://www.mobilize.org.mz/nuit-numero-unico-de-identificacao-tributaria/
34+
* https://www.at.gov.mz/por/Perguntas-Frequentes2/NUIT
35+
36+
>>> validate('400339910')
37+
'400339910'
38+
>>> validate('400 005 834')
39+
'400005834'
40+
>>> validate('12345')
41+
Traceback (most recent call last):
42+
...
43+
InvalidLength: ...
44+
>>> format('400339910')
45+
'400 339 910'
46+
"""
47+
48+
from __future__ import annotations
49+
50+
from stdnum.exceptions import *
51+
from stdnum.util import clean, isdigits
52+
53+
54+
def compact(number: str) -> str:
55+
"""Convert the number to the minimal representation."""
56+
return clean(number, ' -.').strip()
57+
58+
59+
def calc_check_digit(number: str) -> str:
60+
"""Calculate the check digit."""
61+
weights = (8, 9, 4, 5, 6, 7, 8, 9)
62+
check = sum(w * int(n) for w, n in zip(weights, number)) % 11
63+
return '01234567891'[check]
64+
65+
66+
def validate(number: str) -> str:
67+
"""Check if the number is a valid Mozambique NUIT number."""
68+
number = compact(number)
69+
if len(number) != 9:
70+
raise InvalidLength()
71+
if not isdigits(number):
72+
raise InvalidFormat()
73+
if calc_check_digit(number[:-1]) != number[-1]:
74+
raise InvalidChecksum()
75+
return number
76+
77+
78+
def is_valid(number: str) -> bool:
79+
"""Check if the number is a valid Mozambique NUIT number."""
80+
try:
81+
return bool(validate(number))
82+
except ValidationError:
83+
return False
84+
85+
86+
def format(number: str) -> str:
87+
"""Reformat the number to the standard presentation format."""
88+
number = compact(number)
89+
return ' '.join([number[:3], number[3:-3], number[-3:]])

tests/test_mz_nuit.doctest

Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
1+
test_mz_nuit.doctest - more detailed doctests for stdnum.mz.nuit 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.mz.nuit 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.mz import nuit
26+
27+
28+
Tests for some corner cases.
29+
30+
>>> nuit.validate('400339910')
31+
'400339910'
32+
>>> nuit.validate('400 005 834')
33+
'400005834'
34+
>>> nuit.validate('100.033.593')
35+
'100033593'
36+
>>> nuit.validate('12345')
37+
Traceback (most recent call last):
38+
...
39+
InvalidLength: ...
40+
>>> nuit.validate('VV3456789')
41+
Traceback (most recent call last):
42+
...
43+
InvalidFormat: ...
44+
>>> nuit.validate('101935626')
45+
Traceback (most recent call last):
46+
...
47+
InvalidChecksum: ...
48+
>>> nuit.format('400339910')
49+
'400 339 910'
50+
>>> nuit.format('100.033.593')
51+
'100 033 593'
52+
53+
54+
These have been found online and should all be valid numbers.
55+
56+
>>> numbers = '''
57+
...
58+
... 400339910
59+
... 400 005 834
60+
... 500171650
61+
... 700152855
62+
... 400 005 834
63+
... 400027145
64+
... 400001391
65+
... 400584291
66+
... 103017602
67+
... 400872120
68+
... 500001615
69+
... 400786704
70+
... 500 024 240
71+
... 400066183
72+
... 500006005
73+
... 401 191 607
74+
... 400 102 961
75+
... 105564724
76+
... 500003545
77+
... 400787451
78+
... 116773767
79+
... 111878641
80+
... 154695168
81+
... 102889010
82+
... 101908372
83+
... 149349324
84+
... 400339910
85+
... 400509182
86+
... 400 006 245
87+
... 400778922
88+
... 400015546
89+
... 401343261
90+
... 401120807
91+
... 400 108 791
92+
... 400 415 870
93+
... 108 755 423
94+
... 108 755 385
95+
... 400007225
96+
... 401508317
97+
... 400535825
98+
... 400418810
99+
... 401129006
100+
... 400058172
101+
... 400267839
102+
... 500017341
103+
... 700074854
104+
... 401215298
105+
... 400786704
106+
... 400058921
107+
... 400238685
108+
... 400005516
109+
... 500050012
110+
... 400 052 786
111+
... 400 111 200
112+
... 400824037
113+
... 400 410 151
114+
... 120883275
115+
... 100002892
116+
... 118924045
117+
... 400157715
118+
... 400370028
119+
... 129926945
120+
... 400364001
121+
... 101002561
122+
... 400551847
123+
... 400 769 052
124+
... 400 120 323
125+
... 100.033.593
126+
... 105032031
127+
... 401430989
128+
... 103709776
129+
... 500171650
130+
... 400316341
131+
... 400509182
132+
... 400021260
133+
... 400129551
134+
... 400187398
135+
... 600000063
136+
... 400102961
137+
... 400994579
138+
... 400905649
139+
... 500003839
140+
... 401041877
141+
... 400068038
142+
...
143+
... '''
144+
>>> [x for x in numbers.splitlines() if x and not nuit.is_valid(x)]
145+
[]

0 commit comments

Comments
 (0)