Skip to content

Commit 3521335

Browse files
committed
Add support for Singapore Unique Entity Number
Fixes #111.
1 parent 273dd54 commit 3521335

File tree

3 files changed

+655
-0
lines changed

3 files changed

+655
-0
lines changed

stdnum/sg/__init__.py

+22
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
# __init__.py - collection of Singapore numbers
2+
# coding: utf-8
3+
#
4+
# Copyright (C) 2020 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 Singapore numbers."""
22+
from stdnum.sg import uen as vat # noqa: F401

stdnum/sg/uen.py

+182
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,182 @@
1+
# uen.py - functions for handling Singapore UEN numbers
2+
# coding: utf-8
3+
#
4+
# Copyright (C) 2020 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+
"""UEN (Singapore's Unique Entity Number).
22+
23+
There are four different UEN numbers:
24+
25+
* UEN – Business (ROB): It has a total length of 9 characters. Consists
26+
of 8 digits followed by a check letter.
27+
* UEN – Local Company (ROC): It has a total length of 10 characters.
28+
Consists of 9 digits (the 4 leftmost digits represent the year of
29+
issuance) followed by a check letter.
30+
* UEN – Foreign Companies: It has a total length of 10 characters.
31+
Begins with the letter F, followed by either 3 zeroes or
32+
alternatively 3 whitespaces, then followed by 5 digits, and finally
33+
by a check letter.
34+
* UEN – Others: It has a total length of 10 characters. Begins with
35+
either the R letter, or the S letter or the T letter (where R
36+
represents '18', S represents '19' and T represents '20') followed by
37+
2 digits representing the last two digits of the issuance year. After
38+
that come two letters representing the entity type, followed by 4
39+
digits, and finally by a check letter.
40+
41+
For example 'T08' means year 2008, 'S99' means year 1999, and 'R00'
42+
means year 1800.
43+
44+
Entity type must be one of the following:
45+
'CC', 'CD', 'CH', 'CL', 'CM', 'CP', 'CS', 'CX', 'DP', 'FB', 'FC',
46+
'FM', 'FN', 'GA', 'GB', 'GS', 'HS', 'LL', 'LP', 'MB', 'MC', 'MD',
47+
'MH', 'MM', 'MQ', 'NB', 'NR', 'PA', 'PB', 'PF', 'RF', 'RP', 'SM',
48+
'SS', 'TC', 'TU', 'VH', 'XL'.
49+
50+
For example, the UEN for a limited liability partnership (LLP) formed
51+
on 1 January 2009 could be 'T09LL0001B'.
52+
53+
More information:
54+
55+
* https://www.oecd.org/tax/automatic-exchange/crs-implementation-and-assistance/tax-identification-numbers/Singapore-TIN.pdf
56+
* https://www.uen.gov.sg/ueninternet/faces/pages/admin/aboutUEN.jspx
57+
58+
>>> validate('00192200M')
59+
'00192200M'
60+
>>> validate('197401143C')
61+
'197401143C'
62+
>>> validate('F00056789H')
63+
'F00056789H'
64+
>>> validate('F 56789H')
65+
'F00056789H'
66+
>>> validate('S16FC0121D')
67+
'S16FC0121D'
68+
>>> validate('T01FC6132D')
69+
'T01FC6132D'
70+
>>> validate('123456')
71+
Traceback (most recent call last):
72+
...
73+
InvalidLength: ...
74+
>>> format('F 56789H')
75+
'F00056789H'
76+
"""
77+
78+
from datetime import datetime
79+
80+
from stdnum.exceptions import *
81+
from stdnum.util import clean, isdigits
82+
83+
84+
OTHER_UEN_ENTITY_TYPES = ('CC', 'CD', 'CH', 'CL', 'CM', 'CP', 'CS', 'CX',
85+
'DP', 'FB', 'FC', 'FM', 'FN', 'GA', 'GB', 'GS',
86+
'HS', 'LL', 'LP', 'MB', 'MC', 'MD', 'MH', 'MM',
87+
'MQ', 'NB', 'NR', 'PA', 'PB', 'PF', 'RF', 'RP',
88+
'SM', 'SS', 'TC', 'TU', 'VH', 'XL')
89+
90+
91+
def compact(number):
92+
"""Convert the number to the minimal representation.
93+
94+
This converts to uppercase and removes surrounding whitespace. It
95+
also replaces the whitespace in UEN for foreign companies with
96+
zeroes.
97+
"""
98+
return clean(number).upper().strip().replace(' ', '0')
99+
100+
101+
def _validate_business_uen(number):
102+
"""Perform validation on UEN – Business (ROB) numbers."""
103+
if not isdigits(number[:-1]):
104+
raise InvalidFormat()
105+
if not number[-1].isalpha():
106+
raise InvalidFormat()
107+
108+
109+
def _validate_local_company_uen(number):
110+
"""Perform validation on UEN – Local Company (ROC) numbers."""
111+
if not isdigits(number[:-1]):
112+
raise InvalidFormat()
113+
current_year = str(datetime.now().year)
114+
if number[:4] > current_year:
115+
raise InvalidComponent()
116+
if not number[-1].isalpha():
117+
raise InvalidFormat()
118+
119+
120+
def _validate_foreign_companies_uen(number):
121+
"""Perform validation on UEN – Foreign Companies numbers."""
122+
if number[1:4] not in ('000', ' '):
123+
raise InvalidComponent()
124+
if not isdigits(number[4:-1]):
125+
raise InvalidFormat()
126+
if not number[-1].isalpha():
127+
raise InvalidFormat()
128+
129+
130+
def _validate_other_uen(number):
131+
"""Perform validation on other UEN numbers."""
132+
if number[0] not in ('R', 'S', 'T'):
133+
raise InvalidComponent()
134+
if not isdigits(number[1:3]):
135+
raise InvalidFormat()
136+
current_year = str(datetime.now().year)
137+
if number[0] == 'T' and number[1:3] > current_year[2:]:
138+
raise InvalidComponent()
139+
if number[3:5] not in OTHER_UEN_ENTITY_TYPES:
140+
raise InvalidComponent()
141+
if not isdigits(number[5:-1]):
142+
raise InvalidFormat()
143+
if not number[-1].isalpha():
144+
raise InvalidFormat()
145+
146+
147+
def validate(number):
148+
"""Check if the number is a valid Singapore UEN number.
149+
150+
This checks the length and formatting.
151+
"""
152+
number = compact(number)
153+
if len(number) not in (9, 10):
154+
raise InvalidLength()
155+
# UEN – Business (ROB).
156+
if len(number) == 9:
157+
_validate_business_uen(number)
158+
return number
159+
# UEN – Local Company (ROC).
160+
if not number[0].isalpha():
161+
_validate_local_company_uen(number)
162+
return number
163+
# UEN – Foreign Companies.
164+
if number[0] == 'F':
165+
_validate_foreign_companies_uen(number)
166+
return number
167+
# UEN – Others.
168+
_validate_other_uen(number)
169+
return number
170+
171+
172+
def is_valid(number):
173+
"""Check if the number is a valid Singapore UEN number."""
174+
try:
175+
return bool(validate(number))
176+
except ValidationError:
177+
return False
178+
179+
180+
def format(number):
181+
"""Reformat the number to the standard presentation format."""
182+
return compact(number)

0 commit comments

Comments
 (0)