From d5848df776da903afaa688a252a7035838249922 Mon Sep 17 00:00:00 2001 From: Olivier Dony Date: Fri, 14 Feb 2014 12:48:45 +0100 Subject: [PATCH] Add support for 2013 extension of Irish PPS Numbers References: - https://www.welfare.ie/en/Pages/PPSN.aspx - http://www.citizensinformation.ie/en/social_welfare/irish_social_welfare_system/personal_public_service_number.html --- stdnum/ie/pps.py | 37 ++++++++++++++++++++++++++++++------- stdnum/ie/vat.py | 23 +++++++++++++++-------- tests/test_eu_vat.doctest | 2 ++ 3 files changed, 47 insertions(+), 15 deletions(-) diff --git a/stdnum/ie/pps.py b/stdnum/ie/pps.py index 0447bf3f..a98f94db 100644 --- a/stdnum/ie/pps.py +++ b/stdnum/ie/pps.py @@ -19,11 +19,28 @@ """PPS No (Personal Public Service Number, Irish personal number). -The Personal Public Service number consists of 8 digits. The first seven -are numeric and the last is the check character. The number is sometimes -be followed by an extra letter that can be a 'W', 'T' or an 'X' and is -ignored for the check algorithm. +The Personal Public Service number consists of 7 digits, and one or +two letters. The first letter is a check character. +When present (which should be the case for new numbers as of 2013), +the second letter can be 'A' (for individuals) or 'H' (for +non-individuals, such as limited companies, trusts, partnerships +and unincorporated bodies). Pre-2013 values may have 'W', 'T', +or 'X' as the second letter ; it is ignored by the check. +>>> validate('6433435F') # pre-2013 +'6433435F' +>>> validate('6433435FT') # pre-2013 with special final 'T' +'6433435FT' +>>> validate('6433435FW') # pre-2013 format for married women +'6433435FW' +>>> validate('6433435OA') # 2013 format (personal) +'6433435OA' +>>> validate('6433435IH') # 2013 format (non-personal) +'6433435IH' +>>> validate('6433435VH') # 2013 format (incorrect check) +Traceback (most recent call last): + ... +InvalidChecksum: ... >>> validate('6433435F') '6433435F' >>> validate('6433435E') # incorrect check digit @@ -39,7 +56,7 @@ from stdnum.util import clean -pps_re = re.compile('^\d{7}[A-W][WTX]?$') +pps_re = re.compile('^\d{7}[A-W][AHWTX]?$') """Regular expression used to check syntax of PPS numbers.""" @@ -55,8 +72,14 @@ def validate(number): number = compact(number) if not pps_re.match(number): raise InvalidFormat() - if number[7] != vat.calc_check_digit(number[:7]): - raise InvalidChecksum() + if len(number) == 9 and number[8] in 'AH': + # new 2013 format + if number[7] != vat.calc_check_digit(number[:7] + number[8:]): + raise InvalidChecksum() + else: + # old format, last letter ignored + if number[7] != vat.calc_check_digit(number[:7]): + raise InvalidChecksum() return number diff --git a/stdnum/ie/vat.py b/stdnum/ie/vat.py index 1988093d..39b5207c 100644 --- a/stdnum/ie/vat.py +++ b/stdnum/ie/vat.py @@ -22,8 +22,10 @@ The Irish VAT number consists of 8 digits. The last digit is a check letter, the second digit may be a number, a letter, "+" or "*". ->>> validate('IE 6433435F') +>>> validate('IE 6433435F') # pre-2013 format '6433435F' +>>> validate('IE 6433435OA') # 2013 format +'6433435OA' >>> validate('6433435E') # incorrect check digit Traceback (most recent call last): ... @@ -53,9 +55,14 @@ def calc_check_digit(number): """Calculate the check digit. The number passed should not have the check digit included.""" alphabet = 'WABCDEFGHIJKLMNOPQRSTUV' - number = (7 - len(number)) * '0' + number - return alphabet[sum((8 - i) * int(n) for i, n in enumerate(number)) % 23] - + number = number.zfill(7) + if len(number) < 8 or number[7] in ' W': + # for pre-2013 compatibility, W or space are not significant + extra_number = 0 + else: + extra_number = 9 * (ord(number[7]) - 64) + return alphabet[(sum((8 - i) * int(n) for i, n in enumerate(number[:7])) + + + extra_number) % 23] def validate(number): """Checks to see if the number provided is a valid VAT number. This checks @@ -63,19 +70,19 @@ def validate(number): number = compact(number) if not number[:1].isdigit() or not number[2:7].isdigit(): raise InvalidFormat() - if len(number) != 8: + if len(number) not in (8, 9): raise InvalidLength() if number[:7].isdigit(): # new system - if number[-1] != calc_check_digit(number[:-1]): + if number[7] != calc_check_digit(number[:7] + number[8:]): raise InvalidChecksum() elif number[1] in 'ABCDEFGHIJKLMNOPQRSTUVWXYZ+*': # old system - if number[-1] != calc_check_digit(number[2:-1] + number[0]): + if number[7] != calc_check_digit(number[2:7] + number[0]): raise InvalidChecksum() else: raise InvalidFormat() - return number + return compact(number) def is_valid(number): diff --git a/tests/test_eu_vat.doctest b/tests/test_eu_vat.doctest index b1c67a06..098dfd1d 100644 --- a/tests/test_eu_vat.doctest +++ b/tests/test_eu_vat.doctest @@ -387,6 +387,8 @@ These have been found online and should all be valid numbers. ... IE6599001W ... IE8D79739I ... IE9Y71814N +... IE6433435OA +... IE6433435IH ... ... IT - 01404480202 ... IT 0 0 6 1 8 2 8 0 4 9 9