diff options
Diffstat (limited to 'stdnum')
-rw-r--r-- | stdnum/sg/__init__.py | 24 | ||||
-rw-r--r-- | stdnum/sg/uen.py | 172 |
2 files changed, 196 insertions, 0 deletions
diff --git a/stdnum/sg/__init__.py b/stdnum/sg/__init__.py new file mode 100644 index 0000000..1d3e79d --- /dev/null +++ b/stdnum/sg/__init__.py @@ -0,0 +1,24 @@ +# __init__.py - collection of Singapore numbers +# coding: utf-8 +# +# Copyright (C) 2020 Leandro Regueiro +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA +# 02110-1301 USA + +"""Collection of Singapore numbers.""" + +# provide aliases +from stdnum.sg import uen as vat # noqa: F401 diff --git a/stdnum/sg/uen.py b/stdnum/sg/uen.py new file mode 100644 index 0000000..2b49be1 --- /dev/null +++ b/stdnum/sg/uen.py @@ -0,0 +1,172 @@ +# uen.py - functions for handling Singapore UEN numbers +# coding: utf-8 +# +# Copyright (C) 2020 Leandro Regueiro +# Copyright (C) 2020 Arthur de Jong +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA +# 02110-1301 USA + +"""UEN (Singapore's Unique Entity Number). + +The Unique Entity Number (UEN) is a 9 or 10 digit identification issued by +the government of Singapore to businesses that operate with within Singapore. + + +Accounting and Corporate Regulatory Authority (ACRA) + +There are three different formats: + +* Business (ROB): It consists of 8 digits followed by a check letter. +* Local Company (ROC): It consists of 9 digits (the 4 leftmost digits + represent the year of issuance) followed by a check letter. +* Others: Consists of 10 characters, begins with either the R letter, or the + S letter or the T letter followed by 2 digits representing the last two + digits of the issuance year, followed by two letters representing the + entity type, 4 digits and finally a check letter. + +More information: + +* https://www.oecd.org/tax/automatic-exchange/crs-implementation-and-assistance/tax-identification-numbers/Singapore-TIN.pdf +* https://www.uen.gov.sg/ueninternet/faces/pages/admin/aboutUEN.jspx + +>>> validate('00192200M') +'00192200M' +>>> validate('197401143C') +'197401143C' +>>> validate('S16FC0121D') +'S16FC0121D' +>>> validate('T01FC6132D') +'T01FC6132D' +>>> validate('123456') +Traceback (most recent call last): + ... +InvalidLength: ... +""" + +# There are some references to special 10-digit (or 7-digit) numbers that +# start with an F for foreign companies but it is unclear whether this is +# still current and not even examples of these numbers could be found. + +from datetime import datetime + +from stdnum.exceptions import * +from stdnum.util import clean, isdigits + + +OTHER_UEN_ENTITY_TYPES = ( + 'CC', 'CD', 'CH', 'CL', 'CM', 'CP', 'CS', 'CX', 'DP', 'FB', 'FC', 'FM', + 'FN', 'GA', 'GB', 'GS', 'HS', 'LL', 'LP', 'MB', 'MC', 'MD', 'MH', 'MM', + 'MQ', 'NB', 'NR', 'PA', 'PB', 'PF', 'RF', 'RP', 'SM', 'SS', 'TC', 'TU', + 'VH', 'XL', +) + + +def compact(number): + """Convert the number to the minimal representation. + + This converts to uppercase and removes surrounding whitespace. It + also replaces the whitespace in UEN for foreign companies with + zeroes. + """ + return clean(number).upper().strip() + + +def calc_business_check_digit(number): + """Calculate the check digit for the Business (ROB) number.""" + number = compact(number) + weights = (10, 4, 9, 3, 8, 2, 7, 1) + return 'XMKECAWLJDB'[sum(int(n) * w for n, w in zip(number, weights)) % 11] + + +def _validate_business(number): + """Perform validation on UEN - Business (ROB) numbers.""" + if not isdigits(number[:-1]): + raise InvalidFormat() + if not number[-1].isalpha(): + raise InvalidFormat() + if number[-1] != calc_business_check_digit(number): + raise InvalidChecksum() + return number + + +def calc_local_company_check_digit(number): + """Calculate the check digit for the Local Company (ROC) number.""" + number = compact(number) + weights = (10, 8, 6, 4, 9, 7, 5, 3, 1) + return 'ZKCMDNERGWH'[sum(int(n) * w for n, w in zip(number, weights)) % 11] + + +def _validate_local_company(number): + """Perform validation on UEN - Local Company (ROC) numbers.""" + if not isdigits(number[:-1]): + raise InvalidFormat() + current_year = str(datetime.now().year) + if number[:4] > current_year: + raise InvalidComponent() + if number[-1] != calc_local_company_check_digit(number): + raise InvalidChecksum() + return number + + +def calc_other_check_digit(number): + """Calculate the check digit for the other entities number.""" + number = compact(number) + alphabet = 'ABCDEFGHJKLMNPQRSTUVWX0123456789' + weights = (4, 3, 5, 3, 10, 2, 2, 5, 7) + return alphabet[(sum(alphabet.index(n) * w for n, w in zip(number, weights)) - 5) % 11] + + +def _validate_other(number): + """Perform validation on other UEN numbers.""" + if number[0] not in ('R', 'S', 'T'): + raise InvalidComponent() + if not isdigits(number[1:3]): + raise InvalidFormat() + current_year = str(datetime.now().year) + if number[0] == 'T' and number[1:3] > current_year[2:]: + raise InvalidComponent() + if number[3:5] not in OTHER_UEN_ENTITY_TYPES: + raise InvalidComponent() + if not isdigits(number[5:-1]): + raise InvalidFormat() + if number[-1] != calc_other_check_digit(number): + raise InvalidChecksum() + return number + + +def validate(number): + """Check if the number is a valid Singapore UEN number.""" + number = compact(number) + if len(number) not in (9, 10): + raise InvalidLength() + if len(number) == 9: + return _validate_business(number) + if isdigits(number[0]): + return _validate_local_company(number) + return _validate_other(number) + + +def is_valid(number): + """Check if the number is a valid Singapore UEN number.""" + try: + return bool(validate(number)) + except ValidationError: + return False + + +def format(number): + """Reformat the number to the standard presentation format.""" + return compact(number) |