diff --git a/stdnum/eu/excise.py b/stdnum/eu/excise.py new file mode 100644 index 00000000..25625d34 --- /dev/null +++ b/stdnum/eu/excise.py @@ -0,0 +1,84 @@ +# excise.py - functions for handling EU Excise numbers +# coding: utf-8 +# +# Copyright (C) 2023 Cédric Krier +# +# 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 + +"""Excise Number + +The Excise Number is the identification number issued by the competent +authority in respect of the person or premises. + +The first two letters are the ISO country code of the Member State where the +operator is located (e.g. LU); +The next 11 alphanumeric characters are the identifier of the operator. +The identifier must include 11 digits, shorter identifiers must be padded to +the left with zeroes (e.g. 00000987ABC) + +More information: + +* https://ec.europa.eu/taxation_customs/dds2/seed/help/seedhedn.jsp + +>>> compact('LU 00000987ABC') +'LU00000987ABC' +>>> validate('LU00000987ABC') +'LU00000987ABC' +""" + +from stdnum.eu.vat import MEMBER_STATES +from stdnum.exceptions import * +from stdnum.util import clean, get_soap_client + + +seed_wsdl = 'https://ec.europa.eu/taxation_customs/dds2/seed/services/excise/verification?wsdl' +"""The WSDL URL of the System for Exchange of Excise Data (SEED).""" + + +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): + """Check if the number is a valid Excise number.""" + number = clean(number, ' ').upper().strip() + cc = number[:2] + if cc.lower() not in MEMBER_STATES: + raise InvalidComponent() + if len(number) != 13: + raise InvalidLength() + return number + + +def is_valid(number): + """Check if the number is a valid Excise number.""" + try: + return bool(validate(number)) + except ValidationError: + return False + + +def check_seed(number, timeout=30): # pragma: no cover (not part of normal test suite) + """Query the online European Commission System for Exchange of Excise Data + (SEED) for validity of the provided number. Note that the service has + usage limitations (see the VIES website for details). The timeout is in + seconds. This returns a dict-like object.""" + number = compact(number) + client = get_soap_client(seed_wsdl, timeout) + return client.verifyExcise(number) diff --git a/tests/test_eu_excise.doctest b/tests/test_eu_excise.doctest new file mode 100644 index 00000000..f3122892 --- /dev/null +++ b/tests/test_eu_excise.doctest @@ -0,0 +1,64 @@ +test_eu_excise.doctests - more detailed doctests for the stdnum.eu.excise module + +Copyright (C) 2023 Cédric Krier + +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.eu.excise module. It +tries to validate a number of Excise numbers that have been found online. + +>>> from stdnum.eu import excise +>>> from stdnum.exceptions import * + +These have been found online and should all be valid numbers. + +>>> numbers = ''' +... +... LU 00000987ABC +... FR012907E0820 +... ''' +>>> [x for x in numbers.splitlines() if x and not excise.is_valid(x)] +[] + +The following numbers are wrong in one way or another. First we need a +function to be able to determine the kind of error. + +>>> def caught(number, exception): +... try: +... excise.validate(number) +... return False +... except exception: +... return True +... + + +These numbers should be mostly valid except that they have the wrong length. + +>>> numbers = ''' +... +... LU987ABC +... ''' +>>> [x for x in numbers.splitlines() if x and not caught(x, InvalidLength)] +[] + +These numbers should be mostly valid except that they have the wrong prefix + +>>> numbers = ''' +... +... XX00000987ABC +... ''' +>>> [x for x in numbers.splitlines() if x and not caught(x, InvalidComponent)] +[] diff --git a/tests/test_eu_excise.py b/tests/test_eu_excise.py new file mode 100644 index 00000000..dd36044a --- /dev/null +++ b/tests/test_eu_excise.py @@ -0,0 +1,45 @@ +# test_eu_excise.py - functions for testing the online SEED validation +# coding: utf-8 +# +# Copyright (C) 2023 Cédric Krier +# +# 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 is a separate test file because it should not be run regularly +# because it could negatively impact the SEED service. + +"""Extra tests for the stdnum.eu.excise module.""" + +import os +import unittest + +from stdnum.eu import excise + + +@unittest.skipIf( + not os.environ.get('ONLINE_TESTS'), + 'Do not overload online services') +class TestSeed(unittest.TestCase): + """Test the SEED web service provided by the European commission for + validation Excise numbers of European countries.""" + + def test_check_seed(self): + """Test stdnum.eu.excise.check_seed()""" + result = excise.check_seed('FR012907E0820') + self.assertTrue('errorDescription' not in result) + self.assertTrue(len(result['result']) > 0) + first = result['result'][0] + self.assertEqual(first['excise'], 'FR012907E0820')