diff --git a/docs/stdnum.isl.kennitala b/docs/stdnum.isl.kennitala new file mode 100644 index 00000000..b14c1f92 --- /dev/null +++ b/docs/stdnum.isl.kennitala @@ -0,0 +1,5 @@ +stdnum.isl.kennitala +==================== + +.. automodule:: stdnum.isl.kennitala + :members: diff --git a/docs/stdnum.isl.vsk b/docs/stdnum.isl.vsk new file mode 100644 index 00000000..0a215e7b --- /dev/null +++ b/docs/stdnum.isl.vsk @@ -0,0 +1,5 @@ +stdnum.isl.vsk +==================== + +.. automodule:: stdnum.isl.vsk + :members: diff --git a/docs/stdnum.no.mva b/docs/stdnum.no.mva new file mode 100644 index 00000000..fd1d27dd --- /dev/null +++ b/docs/stdnum.no.mva @@ -0,0 +1,5 @@ +stdnum.no.mva +============= + +.. automodule:: stdnum.no.mva + :members: diff --git a/docs/stdnum.no.orgnr b/docs/stdnum.no.orgnr new file mode 100644 index 00000000..76148bbd --- /dev/null +++ b/docs/stdnum.no.orgnr @@ -0,0 +1,5 @@ +stdnum.no.orgnr +=============== + +.. automodule:: stdnum.no.orgnr + :members: diff --git a/stdnum/__init__.py b/stdnum/__init__.py index 523b0df2..9ea903f1 100644 --- a/stdnum/__init__.py +++ b/stdnum/__init__.py @@ -99,7 +99,8 @@ * isan: ISAN (International Standard Audiovisual Number) * isbn: ISBN (International Standard Book Number) * isil: ISIL (International Standard Identifier for Libraries) -* isin: ISIN (International Securities Identification Number) +* isl.vsk: VSK (Virðisaukaskattsnúmer, Icelandic VAT number) +* isl.kennitala: Kennitala (Icelandic personal and organisation identity number) * ismn: ISMN (International Standard Music Number) * iso6346: ISO 6346 (International standard for container identification) * iso9362: ISO 9362 (Business identifier codes) @@ -121,7 +122,7 @@ * nl.onderwijsnummer: Onderwijsnummer (Dutch student school number) * nl.postcode: Postcode (Dutch postal code) * no.mva: MVA (Merverdiavgift, Norwegian VAT number) -* no.orgnr: Orgnr (Organisasjonsnummer, Norwegian organisation number) +* no.orgnr: Orgnr (Organisasjonsnummer, Norwegian organization number) * pl.nip: NIP (Numer Identyfikacji Podatkowej, Polish VAT number) * pl.pesel: PESEL (Polish national identification number) * pl.regon: REGON (Rejestr Gospodarki Narodowej, Polish register of economic units) @@ -172,4 +173,4 @@ # the version number of the library -__version__ = '1.6' +__version__ = '1.6.holvi.1' diff --git a/stdnum/at/businessid.py b/stdnum/at/businessid.py index 8ce5ab76..234239c7 100644 --- a/stdnum/at/businessid.py +++ b/stdnum/at/businessid.py @@ -1,4 +1,5 @@ # businessid.py - functions for handling Austrian company register numbers +# coding: utf-8 # # Copyright (C) 2015 Holvi Payment Services Oy # Copyright (C) 2012, 2013 Arthur de Jong @@ -18,11 +19,11 @@ # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA # 02110-1301 USA -"""Austrian Company Register Numbers. +"""Austrian Company Register Numbers -The Austrian company register number consist of digits followed by a single -letter, e.g. "122119m". Sometimes it is presented with preceding "FN", e.g. -"FN 122119m". +The Austrian company register number consist of digits followed by +a single letter, e.g. "122119m". Sometimes it is presented with preceding +"FN", e.g. "FN 122119m". >>> validate('FN 122119m') '122119m' @@ -38,10 +39,32 @@ InvalidFormat: ... """ +from stdnum import luhn from stdnum.exceptions import * from stdnum.util import clean +# https://www.wko.at/service/wirtschaftsrecht-gewerberecht/Das_Firmenbuch.html +AUSTRIAN_COURTS = [ + u'Wien', + u'Wiener Neustadt', + u'St. Pölten', + u'Krems an der Donau', + u'Korneuburg', + u'Linz', + u'Ried im Innkreis', + u'Steyr', + u'Wels', + u'Salzburg', + u'Eisenstadt', + u'Graz', + u'Leoben', + u'Klagenfurt', + u'Innsbruck', + u'Feldkirch', +] + + def compact(number): """Convert the number to the minimal representation. This strips the number of any valid separators and removes surrounding whitespace. @@ -53,8 +76,8 @@ def compact(number): def validate(number): - """Checks to see if the number provided is a valid company register - number. This only checks the formatting.""" + """Checks to see if the number provided is a valid company register number. + This checks only the formatting.""" number = compact(number) if not number[-1:].isalpha() or not number[:-1].isdigit(): raise InvalidFormat() @@ -62,8 +85,8 @@ def validate(number): def is_valid(number): - """Checks to see if the number provided is a valid company register - number. This only checks the formatting.""" + """Checks to see if the number provided is a valid company register number. + This checks only the formatting.""" try: return bool(validate(number)) except ValidationError: diff --git a/stdnum/at/stnr.py b/stdnum/at/stnr.py new file mode 100644 index 00000000..bb1f6f19 --- /dev/null +++ b/stdnum/at/stnr.py @@ -0,0 +1,244 @@ +# -*- coding: utf-8 -*- +# stnr.py - functions for handling Austrian tax number +# coding: utf-8 +# +# Copyright (C) 2017 Holvi Payment Services Oy +# Copyright (C) 2017 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 +""" steuernummer (Austrian tax number) +A number issued by a tax office in Austria to tax businesses, the number is +9 numerical digits long. The first two digits represent the tax office which +issued the number. The latter 7 digits represent the tax id. +https://dienstgeber.ooegkk.at/portal27/dgooegkkportal/content/contentWindow?viewmode=content&contentid=10007.681672 +The tax offices specified in a list on the tax authority website here: +https://service.bmf.gv.at/Service/Anwend/Behoerden/show_mast.asp?Typ=SM&DisTyp=FA +>>> validate('38/1234567') +'381234567' +>>> validate(number='38/1234567', office='Bruck Eisenstadt Oberwart') +'381234567' +>>> is_valid('38/1234567') +True +>>> is_valid(number='38/1234567', office='Bruck Eisenstadt Oberwart') +True +""" +from stdnum.exceptions import * +from stdnum.util import clean + +tax_offices_codes = { + 'Burgenland': { + '38': { + 'tax_office': 'Bruck Eisenstadt Oberwart', + }, + }, + 'Kärnten': { + '57': { + 'tax_office': 'Klagenfurt', + }, + '61': { + 'tax_office': 'Spittal Villach', + }, + '59': { + 'tax_office': 'St. Veit Wolfsberg', + }, + }, + 'Niederösterreich': { + '15': { + 'tax_office': 'Amstetten Melk Scheibbs', + }, + '16': { + 'tax_office': 'Baden Mödling', + }, + '38': { + 'tax_office': 'Bruck Eisenstadt Oberwart', + }, + '18': { + 'tax_office': 'Gänserndorf Mistelbach', + }, + '22': { + 'tax_office': 'Hollabrunn Korneuburg Tulln', + }, + '29': { + 'tax_office': 'Lilienfeld St. Pölten', + }, + '33': { + 'tax_office': 'Neunkirchen Wr. Neustad', + }, + '23': { + 'tax_office': 'Waldviertel', + }, + }, + + 'Oberösterreich': { + '41': { + 'tax_office': 'Braunau Ried Schärding', + }, + '52': { + 'tax_office': 'Freistadt Rohrbach Urfahr', + }, + '53': { + 'tax_office': 'Gmunden Vöcklabruck', + }, + '54': { + 'tax_office': 'Grieskirchen Wels', + }, + '51': { + 'tax_office': 'Kirchdorf Perg Steyr', + }, + '46': { + 'tax_office': 'Linz', + }, + }, + 'Salzburg': { + '93': { + 'tax_office': 'Salzburg-Land', + }, + '91': { + 'tax_office': 'Salzburg-Stadt', + }, + '90': { + 'tax_office': 'St. Johann Tamsweg Zell am See', + }, + }, + 'Steiermark': { + '65': { + 'tax_office': 'Bruck Leoben Mürzzuschlag', + }, + '72': { + 'tax_office': 'Deutschlandsberg Leibnitz Voitsberg', + }, + '68': { + 'tax_office': 'Graz-Stadt', + }, + '69': { + 'tax_office': 'Graz-Umgebung', + }, + '71': { + 'tax_office': 'Judenburg Liezen', + }, + '67': { + 'tax_office': 'Oststeiermark', + }, + }, + 'Tirol': { + '81': { + 'tax_office': 'Innsbruck', + }, + '82': { + 'tax_office': 'Kitzbühel Lienz', + }, + '83': { + 'tax_office': 'Kufstein Schwaz', + }, + '84': { + 'tax_office': 'Landeck Reutte', + }, + }, + 'Vorarlberg': { + '97': { + 'tax_office': 'Bregenz', + }, + '98': { + 'tax_office': 'Feldkirch', + }, + }, + 'Wien': { + '08': { + 'tax_office': 'Wien 12/13/14 Purkersdorf', + }, + '09': { + 'tax_office': 'Wien 1/23', + }, + '12': { + 'tax_office': 'Wien 2/20/21/22', + }, + '03': { + 'tax_office': 'Wien 3/6/7/11/15 Schwechat Gerasdorf', + }, + '04': { + 'tax_office': 'Wien 4/5/10', + }, + '06': { + 'tax_office': 'Wien 8/16/17', + }, + '07': { + 'tax_office': 'Wien 9/18/19 Klosterneuburg', + }, + }, + +} + + +def get_all_codes_dict(): + """Return a dict with opening chars and their tax offices""" + offices = {} + for _, state_dict in tax_offices_codes.items(): + offices.update(state_dict) + return offices + + +def validate_acceptable_opening_chars(number): + """Validate whether the opening chars are acceptable in + any tax office""" + opening_chars = number[:2] + if opening_chars not in get_all_codes_dict(): + raise InvalidFormat( + 'The opening characters do not belong to any tax office' + ) + return number + + +def validate_opening_chars_to_office(number, office): + """Check if the opening chars are acceptable in that office""" + opening_chars = number[:2] + office_from_chars = get_all_codes_dict()[opening_chars]['tax_office'] + if not office_from_chars == office: + raise InvalidFormat( + 'The opening characters do not belong to this tax office' + ) + return number + + +def compact(number): + """Convert the number to the minimal representation. This strips the + number of any valid separators and removes surrounding whitespace. Also + cheks that there are no junk letters in the number. + """ + return clean(number, ' -./,').strip() + + +def validate(number, office=None): + """Checks to see if the number provided is a valid tax number. This checks + the length and formatting.""" + clean_number = compact(number) + if len(clean_number) != 9: + raise InvalidLength() + if not clean_number.isdigit(): + raise InvalidFormat('The number contains non numerical digits') + if office: + validate_opening_chars_to_office(clean_number, office) + else: + validate_acceptable_opening_chars(clean_number) + return clean_number + + +def is_valid(number, office=None): + """Checks to see if the number provided is a valid tax identification + number. This checks the length, formatting and check digit.""" + try: + return bool(validate(number, office)) + except ValidationError: + return False diff --git a/stdnum/at/zvr_zahl.py b/stdnum/at/zvr_zahl.py new file mode 100644 index 00000000..1554395d --- /dev/null +++ b/stdnum/at/zvr_zahl.py @@ -0,0 +1,72 @@ +# zvr_zahl.py - functions for handling Austrian association register numbers +# coding: utf-8 +# +# Copyright (C) 2017 Holvi Payment Services Oy +# Copyright (C) 2017 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 +""" ZVR-Zahl (Zentrales Vereinsregister Zahl or ZVR-Zahl or +association registery number) +The number is givin to associations by the Association register to identify +with. The number is 9 character long and is givin on a running bases. +No known checksum, this module will just check if it is clean and all digits. +>>> validate('123456789') +'123456789' +>>> validate('0123456789') +Traceback (most recent call last): + ... +InvalidLength: ... +validate('A12345678') +Traceback (most recent call last): + ... +InvalidFormat: ... +>>> is_valid('123456789') +True +>>> is_valid('1234567890') +False +""" +from stdnum.exceptions import ( + InvalidLength, + InvalidFormat, + ValidationError +) +from stdnum.util import clean + + +def compact(number): + """Convert the number to the minimal representation. This removes + surrounding whitespace and raise an error on junk letters.""" + return clean(number, ' ').strip() + + +def validate(number): + """Checks to see if the number provided is a valid association register + number. + This checks only the formatting.""" + number = compact(number) + if len(number) > 9: + raise InvalidLength() + if not number.isdigit(): + raise InvalidFormat() + return number + + +def is_valid(number): + """Return boolean value of the association registery number validity""" + try: + return bool(validate(number)) + except ValidationError: + return False diff --git a/stdnum/de/__init__.py b/stdnum/de/__init__.py index 15a57349..9eea27dc 100644 --- a/stdnum/de/__init__.py +++ b/stdnum/de/__init__.py @@ -19,3 +19,6 @@ # 02110-1301 USA """Collection of German numbers.""" + +# provide businessid as an alias +from stdnum.de import handelsregisternummer as businessid diff --git a/stdnum/de/handelsregisternummer.py b/stdnum/de/handelsregisternummer.py new file mode 100644 index 00000000..7a78f42a --- /dev/null +++ b/stdnum/de/handelsregisternummer.py @@ -0,0 +1,379 @@ +# handelsregisternummer.py - functions for handling German company registry id +# coding: utf-8 +# +# Copyright (C) 2015 Holvi Payment Services Oy +# +# 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 + +""" Handelsregisternummer (German company register number). + +The number consists of the court where the company has registered, the type +of register and the individual number + +The type of the register is either HRA or HRB where the letter "B" stands +for HR section B, where limited liability companies and corporations are +entered (GmbH's and AG's). There is also a section HRA for business +partnerships (OHG's, KG's etc.). In other words: businesses in section +HRB are limited liability companies, while businesses in HRA have personally +liable partners. + +>>> validate('Aachen HRA 11223') +u'Aachen HRA 11223' + +>>> validate('Frankfurt/Oder GnR 11223') +u'Frankfurt/Oder GnR 11223' + +>>> validate('Bad Homburg v.d.H. PR 11223') +u'Bad Homburg v.d.H. PR 11223' + +>>> validate('Ludwigshafen a.Rhein (Ludwigshafen) VR 11223') +u'Ludwigshafen a.Rhein (Ludwigshafen) VR 11223' + +>>> validate('Aachen HRA 11223', company_form='KG') +u'Aachen HRA 11223' + +>>> validate('Frankfurt/Oder GnR 11223', company_form='e.G.') +u'Frankfurt/Oder GnR 11223' + +>>> validate('Bad Homburg v.d.H. PR 11223', company_form='PartG') +u'Bad Homburg v.d.H. PR 11223' + +>>> validate('Ludwigshafen a.Rhein (Ludwigshafen) VR 11223', company_form='e.V.') +u'Ludwigshafen a.Rhein (Ludwigshafen) VR 11223' + +>>> validate('Berlin (Charlottenburg) HRA 11223 B') +u'Berlin (Charlottenburg) HRA 11223 B' + +>>> validate('Berlin (Charlottenburg) HRB 11223B') +u'Berlin (Charlottenburg) HRB 11223 B' + +>>> validate('Berlin (Charlottenburg) HRA 11223 B', return_parts=True) +(u'Berlin (Charlottenburg)', 'HRA', '11223 B') + +>>> validate('Berlin (Charlottenburg) HRB 11223B', return_parts=True) +(u'Berlin (Charlottenburg)', 'HRB', '11223 B') + +>>> validate('Berlin (Charlottenburg) HRA 11223BB') +Traceback (most recent call last): + ... +stdnum.exceptions.InvalidFormat: The number has an invalid format. + +>>> validate('Berlin (Charlottenburg) HRA 11223 BB') +Traceback (most recent call last): + ... +stdnum.exceptions.InvalidFormat: The number has an invalid format. + +>>> validate('0 HRB 44123') +u'Aachen HRB 44123' + +>>> validate('160 HRB 44123') +Traceback (most recent call last): + ... +stdnum.exceptions.InvalidFormat: The number has an invalid format. + +>>> validate('Aachen HRC 44123') +Traceback (most recent call last): + ... +stdnum.exceptions.InvalidFormat: The number has an invalid format. + +>>> validate('Aachen HRA 44123', company_form='GmbH') +Traceback (most recent call last): + ... +stdnum.exceptions.InvalidComponent: One of the parts of the number are invalid or unknown. + +""" # NOQA + +import re +from stdnum.exceptions import * +from stdnum.util import clean + +GERMAN_COURTS = [ + u"Aachen", + u"Altenburg", + u"Amberg", + u"Ansbach", + u"Apolda", + u"Arnsberg", + u"Arnstadt", + u"Aschaffenburg", + u"Augsburg", + u"Aurich", + u"Bad Hersfeld", + u"Bad Homburg v.d.H.", + u"Bad Kreuznach", + u"Bad Langensalza", + u"Bad Lobenstein", + u"Bad Oeynhausen", + u"Bad Salzungen", + u"Bamberg", + u"Bayreuth", + u"Berlin (Charlottenburg)", + u"Bielefeld", + u"Bochum", + u"Bonn", + u"Braunschweig", + u"Bremen", + u"Bückeburg", + u"Chemnitz", + u"Coburg", + u"Coesfeld", + u"Cottbus", + u"Darmstadt", + u"Deggendorf", + u"Delmenhorst", + u"Dortmund", + u"Dresden", + u"Duisburg", + u"Düren", + u"Düsseldorf", + u"Eisenach", + u"Erfurt", + u"Eschwege", + u"Essen", + u"Flensburg", + u"Frankfurt am Main", + u"Frankfurt/Oder", + u"Freiburg", + u"Friedberg", + u"Fritzlar", + u"Fulda", + u"Fürth", + u"Gelsenkirchen", + u"Gera", + u"Gießen", + u"Gotha", + u"Göttingen", + u"Greiz", + u"Gütersloh", + u"Hagen", + u"Hamburg", + u"Hamm", + u"Hanau", + u"Hannover", + u"Heilbad Heiligenstadt", + u"Hildburghausen", + u"Hildesheim", + u"Hof", + u"Homburg", + u"Ilmenau", + u"Ingolstadt", + u"Iserlohn", + u"Jena", + u"Kaiserslautern", + u"Kassel", + u"Kempten (Allgäu)", + u"Kiel", + u"Kleve", + u"Koblenz", + u"Köln", + u"Königstein", + u"Korbach", + u"Krefeld", + u"Landau", + u"Landshut", + u"Langenfeld", + u"Lebach", + u"Leipzig", + u"Lemgo", + u"Limburg", + u"Lübeck", + u"Ludwigshafen a.Rhein (Ludwigshafen)", + u"Lüneburg", + u"Mainz", + u"Mannheim", + u"Marburg", + u"Meiningen", + u"Memmingen", + u"Merzig", + u"Mönchengladbach", + u"Montabaur", + u"Mühlhausen", + u"München", + u"Münster", + u"Neubrandenburg", + u"Neunkirchen", + u"Neuruppin", + u"Neuss", + u"Nienburg (Weser)", + u"Nordhausen", + u"Nürnberg", + u"Offenbach am Main", + u"Oldenburg (Oldenburg)", + u"Osnabrück", + u"Osterholz-Scharmbeck", + u"Ottweiler", + u"Paderborn", + u"Passau", + u"Pinneberg", + u"Pößneck", + u"Potsdam", + u"Recklinghausen", + u"Regensburg", + u"Rinteln", + u"Rostock", + u"Rotenburg (Wümme) (Rotenburg/Wümme)", + u"Rudolstadt", + u"Saalfeld", + u"Saarbrücken", + u"Saarlouis", + u"Schweinfurt", + u"Schwerin", + u"Siegburg", + u"Siegen", + u"Sömmerda", + u"Sondershausen", + u"Sonneberg", + u"Stadthagen", + u"Stadtroda", + u"Steinfurt", + u"Stendal", + u"St. Ingbert (St Ingbert)", + u"Stralsund", + u"Straubing", + u"Stuttgart", + u"St. Wendel (St Wendel)", + u"Suhl", + u"Tostedt", + u"Traunstein", + u"Ulm", + u"Vechta", + u"Verden (Aller)", + u"Völklingen", + u"Walsrode", + u"Weiden i. d. OPf.", + u"Weimar", + u"Wetzlar", + u"Wiesbaden", + u"Wittlich", + u"Wuppertal", + u"Würzburg", + u"Zweibrücken" +] + +REGISTRY_TYPES = [ + 'HRA', + 'HRB', + 'PR', + 'GnR', + 'VR', +] + +COMPANY_FORM_REGISTRY_TYPES = { + 'e.K.': 'HRA', + 'e.V.': 'VR', + 'Verein': 'VR', + 'OHG': 'HRA', + 'KG': 'HRA', + 'KGaA': 'HRB', + 'Vor-GmbH': 'HRB', + 'GmbH': 'HRB', + 'UG': 'HRB', + 'UG i.G.': 'HRB', + 'AG': 'HRB', + 'e.G.': 'GnR', + 'PartG': 'PR', +} + + +def validate(number, return_parts=False, company_form=None): + """ + Validate the format of a German company registry number. + + Parse number backwards. Expect the last part to be the number, + the next to be the registry, and the rest to be the court. + + Returns a string with the parts, but optionally return a tuple + with them. + + If a company_form (eg. GmbH or PartG) is given, the number is + validated to have the correct registry. + """ + + # Return empty if something unsplittable was sent in + try: + parts = number.split() + except AttributeError: + return '' + + if not parts: + raise InvalidFormat() + + # At least Berlin can have a B after the digit part + if parts[-1].isalpha(): + qualifier = parts.pop(-1) + if len(qualifier) != 1: + raise InvalidFormat() + else: + qualifier = None + + if not parts: + raise InvalidFormat() + + number = parts.pop(-1) + if not parts: + raise InvalidFormat() + + # The case where there was no space between the digit and the character + if number[-1].isalpha() and qualifier is None: + number, qualifier = number[:-1], number[-1] + + number = clean(number, ' -./,') + if not number.isdigit(): + raise InvalidFormat() + elif qualifier is not None: + number = "%s %s" % (number, qualifier) + + registry = parts.pop(-1) + if not parts: + raise InvalidFormat() + + if registry not in REGISTRY_TYPES: + raise InvalidFormat(registry) + + if company_form is not None: + if company_form not in COMPANY_FORM_REGISTRY_TYPES: + raise InvalidComponent() + elif COMPANY_FORM_REGISTRY_TYPES[company_form] != registry: + raise InvalidComponent() + + court = ' '.join(parts) + court = clean(court, ':').strip() + if court.lower() not in (name.lower() for name in GERMAN_COURTS): + try: + ordinal = int(court) + if ordinal < len(GERMAN_COURTS) and ordinal >= 0: + court = GERMAN_COURTS[ordinal] + else: + raise InvalidFormat() + except ValueError: + raise InvalidFormat() + else: + index = [name.lower() for name in GERMAN_COURTS].index(court.lower()) + court = GERMAN_COURTS[index] + + if return_parts: + return (court, registry, number) + else: + return "%s %s %s" % (court, registry, number) + + +def is_valid(number): + """Checks to see if the number provided is a valid company registry number. + This checks that the court exists and the format is correct.""" + try: + return bool(validate(number)) + except ValidationError: + return False diff --git a/stdnum/de/steueridentifikationsnummer.py b/stdnum/de/steueridentifikationsnummer.py new file mode 100644 index 00000000..c640780b --- /dev/null +++ b/stdnum/de/steueridentifikationsnummer.py @@ -0,0 +1,112 @@ +# steueridentifikationsnummer.py - functions for handling German tax id +# coding: utf-8 +# +# Copyright (C) 2015 Holvi Payment Services Oy +# Copyright (C) 2017 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 +"""steueridentifikationsnummer (steueridentifikationsnummer, +tax registeration number). +https://de.wikipedia.org/wiki/Steuerliche_Identifikationsnummer +The number is 11 digits long and uses the ISO 7064 Mod 11, 2 check digit +algorithm. The number has as well the following features: + 1.) one digit appears exactly twice or thrice. + 2.) one or two digits appear zero times. + 3.) and all other digits appear exactly once. + +>>> compact('423 446 779 08') +'42344677908' +>>> validate('36574261809') +'36574261809' +>>> validate('116574261809') +Traceback (most recent call last): + ... +InvalidLength: ... +>>> validate('A6574261809') +Traceback (most recent call last): + ... +InvalidFormat: ... +# Both 5 and 6 are repeated (not allowed). +>>> validate('36554266809') +Traceback (most recent call last): + ... +InvalidFormat: ... +>>> validate('36574261890') +Traceback (most recent call last): + ... +InvalidChecksum: ... +>>> is_valid('36574261890') +False +>>> is_valid('36574261809') +True +""" +from collections import Counter +from stdnum.exceptions import ( + InvalidLength, + InvalidFormat, + InvalidChecksum +) +from stdnum.iso7064 import mod_11_10 +from stdnum.util import clean + + +def compact(number): + """ + Convert the number to the minimal representation. This strips the + number of any valid separators ' -./,' and removes surrounding whitespace. + """ + number = clean(number, ' -./,').upper().strip() + return number + + +def validate(number): + """Checks to see if the number provided is a valid tax identifiication + number. This checks the length, formatting and check digit.""" + # Number should be of length 11 + number = compact(str(number)) + if len(number) != 11: + raise InvalidLength() + number_list = list(number) + + # Contain only digits + if not number.isdigit(): + raise InvalidFormat() + + # First digit in the number should not be zero. + if number_list[0] == '0': + raise InvalidFormat() + # within the first ten digits one number has to appear exactly twice or + # thrice. + counter = Counter(number_list) + repeated_nums = [i for i, v in counter.items() if v > 1] + # Since we know there are 11 numbers, at least one has to be repeated. + # One or two digits appear zero times. + # All other digits appear only once. + if len(repeated_nums) > 1: + raise InvalidFormat() + if mod_11_10.is_valid(number): + return number + raise InvalidChecksum() + + +def is_valid(number): + """Checks to see if the number provided is a valid tax identification + number. This checks the length, formatting and check digit.""" + try: + return bool(validate(number)) + except InvalidFormat(): + return False + except InvalidLength(): + return False diff --git a/stdnum/de/stnr.py b/stdnum/de/stnr.py new file mode 100644 index 00000000..bf340099 --- /dev/null +++ b/stdnum/de/stnr.py @@ -0,0 +1,199 @@ +# -*- coding: utf-8 -*- +# steuernummer.py - functions for handling German tax numbers +# Copyright (C) 2017 Holvi Payment Services +# Copyright (C) 2012, 2013 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 +"""steuernummer (German tax number). +https://de.wikipedia.org/wiki/Steuernummer +The number is 10 or 11 digits long for the long schema and 13 digits for the +new schema. The number might have one or two opening characters based on the +city of registeration in the new schema. +The number should have a checkup letter based on Mod 11 - 10, but in reality +a lot of actual registered numbers did not abide to the checks in tests. +>>> compact(' 181/815/0815 5') +'18181508155' +>>> validate('181/815/08155') +'181/815/08155' +>>> validate('201/123/12340', 'Sachsen') +'201/123/12340' +>>> validate('4151081508156', 'Thüringen') +'4151081508156' +>>> validate('4151181508156', 'Thüringen') +Traceback (most recent call last): + ... +InvalidFormat: ... +>>> validate('136695978') +Traceback (most recent call last): + ... +InvalidLength: ... +""" +from stdnum.exceptions import ( + InvalidLength, + InvalidFormat +) +from stdnum.util import clean + + +def compact(number): + """ + Convert the number to the minimal representation. This strips the + number of any valid separators ' -./,' and removes surrounding whitespace. + """ + number = clean(number, ' -./,').upper().strip() + return number + + +state_opening_character = { + 'Baden-Württemberg': { + 'standard': '', + 'bund': '28' + }, + 'Bayern': { + 'standard': '', + 'bund': '9' + }, + 'Berlin': { + 'standard': '', + 'bund': '11' + }, + 'Brandenburg': { + 'standard': '0', + 'bund': '30' + }, + 'Bremen': { + 'standard': '', + 'bund': '24' + }, + 'Hamburg': { + 'standard': '', + 'bund': '22' + }, + 'Hessen': { + 'standard': '0', + 'bund': '26' + }, + 'Mecklenburg-Vorpommern': { + 'standard': '0', + 'bund': '40' + }, + 'Niedersachsen': { + 'standard': '', + 'bund': '23' + }, + 'Nordrhein-Westfalen': { + 'standard': '', + 'bund': '5' + }, + 'Rheinland-Pfalz': { + 'standard': '', + 'bund': '27' + }, + 'Saarland': { + 'standard': '0', + 'bund': '10' + }, + 'Sachsen': { + 'standard': '2', + 'bund': '32' + }, + 'Sachsen-Anhalt': { + 'standard': '1', + 'bund': '31' + }, + 'Schleswig-Holstein': { + 'standard': '', + 'bund': '21' + }, + 'Thüringen': { + 'standard': '1', + 'bund': '41' + } +} + + +def get_state_opening_characters(state, schema): + """ + Return a string of the expected openeing chracters of that state. + The return value will depend on the specification for that state and the + schema type, schema type should be: + standard: For old standard schema (Standardschema der Länder) or + (Standardschema) + bund: For the new federal schema (Vereinheitlichtes Bundesschema) or + (Bundesschema) + Use state name as the refernce in the wikipedia article. + """ + return state_opening_character[state][schema] + + +def check_number_acceptable_to_state(number, state, schema): + """ + Check if the number is acceptable to the state by checking the opening + charicters for the state and the schema against the opening charicters of + the provided numbers. + """ + opening_chars = get_state_opening_characters(state, schema) + if number.startswith(opening_chars): + return number + raise InvalidFormat('This identifier is not acceptable for %s.' % state) + + +def validate_bund_schema(number, state): + """Return the number if valid, raises an exception otherwise""" + if state: + check_number_acceptable_to_state(number, state, 'bund') + # In Bundesschema, the fourth char is always a zero. + if not number[4] == '0': + raise InvalidFormat() + return number + + +def validate_standard_schema(number, state): + """Return the number if valid, raises an exception otherwise""" + if state: + check_number_acceptable_to_state(number, state, 'standard') + return number + + +def validate(number, state=None): + """ + Checks to see if the number provided is a valid tax number. + This checks the length and formatting. + """ + clean_number = compact(number) + if not clean_number.isdigit(): + raise InvalidFormat('The number contains letter.') + if len(clean_number) == 13: + return validate_bund_schema(number, state) + + elif len(clean_number) in [10, 11]: + return validate_standard_schema(number, state) + + raise InvalidLength() + + +def is_valid(number, state=None): + """ + Return Bool if the identifier is valid or not. + optional (state), if specified, the opening characters of the number will + be tested against the state known opening characters. + """ + try: + return bool(validate(number=number, state=state)) + except InvalidLength: + return False + except InvalidFormat: + return False diff --git a/stdnum/ee/__init__.py b/stdnum/ee/__init__.py index f35e6535..5c4ddc22 100644 --- a/stdnum/ee/__init__.py +++ b/stdnum/ee/__init__.py @@ -22,3 +22,4 @@ # provide vat as an alias from stdnum.ee import kmkr as vat +from stdnum.ee import registrikoodi as registery_code diff --git a/stdnum/fi/__init__.py b/stdnum/fi/__init__.py index eab8d0d1..54f89b95 100644 --- a/stdnum/fi/__init__.py +++ b/stdnum/fi/__init__.py @@ -23,4 +23,3 @@ # provide vat as an alias from stdnum.fi import alv as vat from stdnum.fi import ytunnus as businessid -from stdnum.fi import hetu as personalid diff --git a/stdnum/fi/ytunnus.py b/stdnum/fi/ytunnus.py index 10114eb3..13acd29b 100644 --- a/stdnum/fi/ytunnus.py +++ b/stdnum/fi/ytunnus.py @@ -2,7 +2,7 @@ # coding: utf-8 # # Copyright (C) 2015 Holvi Payment Services Oy -# Copyright (C) 2015 Arthur de Jong +# Copyright (C) 2012, 2013 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 @@ -19,46 +19,42 @@ # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA # 02110-1301 USA -"""Y-tunnus (Finnish business identifier). +"""Y-tunnus (Finnish business identifier) The number is an 8-digit code with a weighted checksum. >>> validate('2077474-0') -'20774740' +'2077474-0' >>> validate('2077474-1') # invalid check digit Traceback (most recent call last): ... InvalidChecksum: ... ->>> format('2077474-0') -'2077474-0' """ from stdnum.exceptions import * +from stdnum.util import clean from stdnum.fi import alv def compact(number): """Convert the number to the minimal representation. This strips the number of any valid separators and removes surrounding whitespace.""" - return alv.compact(number) + number = clean(number, ' -').upper().strip() + return number def validate(number): - """Checks to see if the number provided is a valid business identifier. - This checks the length, formatting and check digit.""" - return alv.validate(number) + """Checks to see if the number provided is a valid business identifier. This + checks the length, formatting and check digit.""" + number = compact(number) + number = alv.validate(number) + return "%s-%s" % (number[:7], number[7:]) def is_valid(number): - """Checks to see if the number provided is a valid business identifier. - This checks the length, formatting and check digit.""" + """Checks to see if the number provided is a valid business identifier. This + checks the length, formatting and check digit.""" try: return bool(validate(number)) except ValidationError: return False - - -def format(number): - """Reformat the passed number to the standard format.""" - number = compact(number) - return number[:7] + '-' + number[7:] diff --git a/stdnum/isl/__init__.py b/stdnum/isl/__init__.py new file mode 100644 index 00000000..456fbb97 --- /dev/null +++ b/stdnum/isl/__init__.py @@ -0,0 +1,24 @@ +# __init__.py - collection of Icelandic numbers +# coding: utf-8 +# +# Copyright (C) 2012 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 + +"""Collection of Icelandic numbers.""" + +# provide vat as an alias +from stdnum.isl import vsk as vat diff --git a/stdnum/isl/kennitala.py b/stdnum/isl/kennitala.py new file mode 100644 index 00000000..181bf791 --- /dev/null +++ b/stdnum/isl/kennitala.py @@ -0,0 +1,119 @@ +# kennitala.py - functions for handling Icelandic identity codes +# coding: utf-8 +# +# Copyright (C) 2011 Jussi Judin +# Copyright (C) 2012, 2013 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 + +"""Kennitala (Icelandic personal and organisation identity code). + +Module for handling Icelandic personal and organisation identity codes +(kennitala). + +>>> compact('530575-0209') # organisation +'5305750209' +>>> compact('120174-3399') # individual +'1201743399' +>>> validate('530575-0209') # organisation +'5305750209' +>>> validate('120174-3399') # individual +'1201743399' +>>> validate('530575-0299') +Traceback (most recent call last): + ... +InvalidChecksum: ... +>>> validate('320174-3399') +Traceback (most recent call last): + ... +InvalidComponent: ... +>>> compact('530575-0209') +'5305750209' +""" + +import re +import datetime + +from stdnum.exceptions import * +from stdnum.util import clean + + +# Icelandic personal and organisation identity codes are composed of +# date part, a dash, two random digits, a checksum, and a century +# indicator where '9' for 1900-1999 and '0' for 2000 and beyond. For +# organisations instead of birth date, the registration date is used, +# and number 4 is added to the first digit. +_kennitala_re = re.compile(r'^(?P[01234567]\d)(?P[01]\d)(?P\d\d)' + r'(?P\d\d)(?P\d)' + r'(?P[09])$') + + +def compact(number): + """Convert the kennitala to the minimal representation. This + strips surrounding whitespace and separation dash, and converts it + to upper case.""" + return clean(number, '-').upper().strip() + + +def checksum(number): + """Calculate the checksum.""" + weights = (3, 2, 7, 6, 5, 4, 3, 2) + return (11 - sum(weights[i] * int(n) for i, n in enumerate(number))) % 11 + + +def validate(number): + """Checks to see if the number provided is a valid kennitala. It + checks the format, whether a valid date is given and whether the + check digit is correct.""" + number = compact(number) + match = _kennitala_re.search(number) + if not match: + raise InvalidFormat() + day = int(match.group('day')) + month = int(match.group('month')) + year = int(match.group('year')) + century = int(match.group('century')) + if century == 9: + century = 1900 + else: + century = 2000 + # check if birth date or registration data is valid + try: + if day >= 40: # organisation + datetime.date(century + year, month, day-40) + else: # individual + datetime.date(century + year, month, day) + except ValueError: + raise InvalidComponent() + # validate the checksum + if checksum(number[:8]) != int(number[8]): + raise InvalidChecksum() + return number + + +def is_valid(number): + """Checks to see if the number provided is a valid HETU. It checks the + format, whether a valid date is given and whether the check digit is + correct.""" + try: + return bool(validate(number)) + except ValidationError: + return False + + +# This is here just for completeness as there are no different length forms +# of Finnish personal identity codes: +format = compact diff --git a/stdnum/isl/vsk.py b/stdnum/isl/vsk.py new file mode 100644 index 00000000..c0475557 --- /dev/null +++ b/stdnum/isl/vsk.py @@ -0,0 +1,71 @@ +# vsk.py - functions for handling Icelandic VAT numbers +# coding: utf-8 +# +# Copyright (C) 2012, 2013 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 + +"""VSK number (Virðisaukaskattsnúmer, Icelandic VAT number). + +The Icelandic VAT number is five or six digits. + +>>> validate('IS 00621') +'00621' +>>> validate('IS 0062199') # invalid length +Traceback (most recent call last): + ... +InvalidLength: ... +>>> validate('IS land') # invalid format +Traceback (most recent call last): + ... +InvalidFormat: ... +>>> validate('Island') # invalid format +Traceback (most recent call last): + ... +InvalidFormat: ... +""" + +from stdnum.exceptions import * +from stdnum.util import clean + + +def compact(number): + """Convert the number to the minimal representation. This strips the + number of any valid separators and removes surrounding whitespace.""" + number = clean(number, ' ').upper().strip() + if number.startswith('IS'): + number = number[2:] + return number + + +def validate(number): + """Checks to see if the number provided is a valid VAT number. This + checks the length, formatting and check digit.""" + number = compact(number) + if not number.isdigit(): + raise InvalidFormat() + if len(number) != 5 and len(number) != 6: + raise InvalidLength() + return number + + +def is_valid(number): + """Checks to see if the number provided is a valid VAT number. This + checks the length, formatting and check digit.""" + try: + return bool(validate(number)) + except ValidationError: + return False diff --git a/stdnum/no/mva.py b/stdnum/no/mva.py index f3be4e76..19db2ac0 100644 --- a/stdnum/no/mva.py +++ b/stdnum/no/mva.py @@ -26,7 +26,7 @@ >>> validate('NO 995 525 828 MVA') '995525828MVA' ->>> validate('NO 995 525 829 MVA') +>>> validate('NO 995 525 829 MVA') # invalid check digit Traceback (most recent call last): ... InvalidChecksum: ... @@ -70,4 +70,7 @@ def is_valid(number): def format(number): """Reformat the passed number to the standard format.""" number = compact(number) + if not number.endswith('MVA'): + raise InvalidFormat() + orgnr.validate(number[:-3]) return 'NO ' + orgnr.format(number[:9]) + ' ' + number[9:] diff --git a/tests/test_at_strnr.py b/tests/test_at_strnr.py new file mode 100644 index 00000000..5705f7e2 --- /dev/null +++ b/tests/test_at_strnr.py @@ -0,0 +1,65 @@ +from unittest import TestCase +from stdnum.at.stnr import ( + validate, + is_valid, + get_all_codes_dict, + validate_acceptable_opening_chars, + validate_opening_chars_to_office +) +from stdnum.exceptions import ( + InvalidFormat, + InvalidLength +) +acceptable_number_office = ('571234567', 'Klagenfurt') +unacceptable_number_office = ('571234567', 'Spittal Villach') +unacceaptable_length = '12345678' +unacceaptable_char = 'X123455678' +acceptable_number = '571234567' + + +class TestATStnr(TestCase): + """Tests for the module methods""" + def test_get_all_codes_dict(self): + """All codes dict should return a dictionary of all the valid codes""" + all_codes = get_all_codes_dict() + self.assertEqual(len(all_codes), 39) + + def test_validate_acceptable_opening_chars(self): + """The first two chars should be validated against the list of opening + chars that we have for office""" + unacceptable_number = '00XXXXXXX' + self.assertEqual( + validate_acceptable_opening_chars(acceptable_number), + acceptable_number + ) + with self.assertRaises(InvalidFormat) as _: + validate_acceptable_opening_chars(unacceptable_number) + + def test_validate_opening_chars_to_office(self): + """The first two chars should match the ofice if the office is provided + """ + + self.assertTrue( + validate_opening_chars_to_office(*acceptable_number_office) + ) + with self.assertRaises(InvalidFormat) as _: + validate_opening_chars_to_office(*unacceptable_number_office) + + def test_validate(self): + """Test the validate functionality""" + with self.assertRaises(InvalidFormat) as _: + validate(unacceaptable_char) + with self.assertRaises(InvalidLength) as _: + validate(unacceaptable_length) + with self.assertRaises(InvalidFormat) as _: + validate(*unacceptable_number_office) + self.assertEqual(validate(acceptable_number), acceptable_number) + self.assertEqual( + validate(*acceptable_number_office), acceptable_number_office[0] + ) + + def test_is_valid(self): + """Test the boolean interface""" + self.assertTrue(is_valid(acceptable_number)) + self.assertTrue(is_valid(*acceptable_number_office)) + self.assertFalse(is_valid(*unacceptable_number_office)) diff --git a/tests/test_de_stnr.doctest b/tests/test_de_stnr.doctest new file mode 100644 index 00000000..7fb2b081 --- /dev/null +++ b/tests/test_de_stnr.doctest @@ -0,0 +1,64 @@ +test_de_stnr.doctest - more detailed doctests for the stdnum.de.stnr module + +Copyright (C) 2017 Holvi Payment Services +Copyright (C) 2017 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 + + +This file contains more detailed doctests for the stdnum.de.stnr module. It +tries to validate a number of numbers that have been found online. + +>>> from stdnum.de import stnr + +These have been found online and should all be valid numbers. +>>> numbers = ''' +... +... 93815/08152 +... 2893081508152 +... 181/815/08155 +... 9181081508155 +... 21/815/08150 +... 1121081508150 +... 048/815/08155 +... 3048081508155 +... 75 815 08152 +... 2475081508152 +... 02/815/08156 +... 2202081508156 +... 013 815 08153 +... 2613081508153 +... 079/815/08151 +... 4079081508151 +... 24/815/08151 +... 2324081508151 +... 133/8150/8159 +... 5133081508159 +... 22/815/08154 +... 2722081508154 +... 010/815/08182 +... 1010081508182 +... 201/123/12340 +... 3201012312340 +... 101/815/08154 +... 3101081508154 +... 29/815/08158 +... 2129081508158 +... 151/815/08156 +... 4151081508156 +... ''' +>>> [x for x in numbers.splitlines() if x and not stnr.is_valid(x)] +[] diff --git a/tests/test_de_stnr.py b/tests/test_de_stnr.py new file mode 100644 index 00000000..c14b88a9 --- /dev/null +++ b/tests/test_de_stnr.py @@ -0,0 +1,139 @@ +""" +Provide tests for German Steuernummer (Tax number) +""" +from unittest import TestCase +from mock import Mock, patch +from stdnum.exceptions import ( + InvalidFormat, + InvalidLength +) +from stdnum.de.StNr import ( + get_state_opening_characters, + check_number_acceptable_to_state, + validate, + compact, + validate_bund_schema, + validate_standard_schema +) + + +class TestDeStNr(TestCase): + """Tests for the module methods""" + def test_get_opening_chars(self): + """ + Test that the function returns the opening chars for available state + and schema. + """ + expected_chars = '11' + opening_chars = get_state_opening_characters( + state='Berlin', + schema='bund' + ) + self.assertEqual(expected_chars, opening_chars) + + def test_get_opening_chars_invalid_input(self): + """ + Test that the method raises and exception when state or schema provided + is not valid. + """ + with self.assertRaises(KeyError) as _: + get_state_opening_characters( + state='Berln', + schema='bund' + ) + + with self.assertRaises(KeyError) as _: + get_state_opening_characters( + state='Berlin', + schema='bond' + ) + + def test_check_number_acceptable_to_state(self): + """Test the main functionality of the method""" + acceptable_number = '1123456789' + self.assertTrue( + check_number_acceptable_to_state( + number=acceptable_number, + state='Berlin', + schema='bund' + ) + ) + unacceptable_number = '1223456789' + with self.assertRaises(InvalidFormat) as _: + check_number_acceptable_to_state( + number=unacceptable_number, + state='Berlin', + schema='bund' + ) + + def test_compact(self): + """Test the compact functionality""" + original_number = '122-1234-1234' + expected_compact = '12212341234' + self.assertEqual(compact(original_number), expected_compact) + + original_number_diff_format = '122/1234/1234' + self.assertEqual( + compact(original_number_diff_format), expected_compact + ) + + def test_validate_bund_schema(self): + """Test the Bundesschema validator""" + validated = validate_bund_schema('112304', 'Berlin') + self.assertEqual(validated, '112304') + + def test_validate_invalid_bund_schema(self): + """Test the Bundesschema validator for invalid number""" + with self.assertRaises(InvalidFormat) as _: + validate_bund_schema('11234', 'Berlin') + + def test_validate_standard_schema(self): + """Test the Standardschema validator""" + validated = validate_standard_schema('011234', 'Brandenburg') + self.assertEqual(validated, '011234') + + def test_validate_invalid_standard_schema(self): + """Test the Standardschema validator""" + with self.assertRaises(InvalidFormat) as _: + validate_standard_schema('211234', 'Brandenburg') + + def test_validate_without_state(self): + """ + validate method should be able to validate without the state if + called. The checks should be based on length of the number + """ + first_valid_number = '1234567890' # 10 digits. + second_valid_number = '12345678901' # 11 digits. + third_valid_number = '1234056789012' # 13 digits + self.assertTrue(validate(first_valid_number), first_valid_number) + self.assertTrue(validate(second_valid_number), second_valid_number) + self.assertTrue(validate(third_valid_number), third_valid_number) + + def test_validate_invalid_length(self): + """Validating invalid length should raise an exception""" + with self.assertRaises(InvalidLength) as _: + short_number = '123456789' + validate(short_number) + + @patch('stdnum.de.StNr.validate_bund_schema') + def test_validate_with_state_bund_schema(self, mock_validate_bund_schema): + """if state is provided, state validation logic should be invoked""" + number = '1034056789012' + state = 'Saarland' + validate(number=number, state=state) + self.assertTrue(mock_validate_bund_schema.called) + call_num, call_state = mock_validate_bund_schema.call_args[0] + self.assertEqual(call_num, number) + self.assertEqual(state, call_state) + + @patch('stdnum.de.StNr.validate_standard_schema') + def test_validate_with_state_standard_schema(self, + mock_validate_stand_schema): + """if state is provided, state validation logic should be invoked""" + number = '1234567890' + state = 'Saarland' + validate(number=number, state=state) + self.assertTrue(mock_validate_stand_schema.called) + call_num, call_state = mock_validate_stand_schema.call_args[0] + self.assertEqual(call_num, number) + self.assertEqual(state, call_state)