|
| 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 | +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] != '000' and number[1:4] != ' ': |
| 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 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) < 9 or len(number) > 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