Skip to content

Commit 1a254d9

Browse files
cedkarthurdejong
authored andcommitted
Add European Excise Number
Closes #424
1 parent d534b3c commit 1a254d9

File tree

2 files changed

+135
-0
lines changed

2 files changed

+135
-0
lines changed

stdnum/eu/excise.py

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
# excise.py - functions for handling EU Excise numbers
2+
# coding: utf-8
3+
#
4+
# Copyright (C) 2023 Cédric Krier
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+
"""European Excise Number
22+
23+
The excise duty identification number assigned to businesses authorised to
24+
operate with excise goods (e.g. alcohol, tobacco, energy products, etc.).
25+
26+
The number is issued by the national customs or tax authority of the Member
27+
State where the business is established. The number consists of a two-letter
28+
country code followed by up to 13 alphanumeric characters.
29+
30+
More information:
31+
32+
* https://ec.europa.eu/taxation_customs/dds2/seed/
33+
34+
>>> validate('LU 987ABC')
35+
'LU00000987ABC'
36+
"""
37+
38+
from __future__ import annotations
39+
40+
from stdnum.eu.vat import MEMBER_STATES
41+
from stdnum.exceptions import *
42+
from stdnum.util import NumberValidationModule, clean, get_cc_module
43+
44+
45+
_country_modules = dict()
46+
47+
48+
def _get_cc_module(cc: str) -> NumberValidationModule | None:
49+
"""Get the Excise number module based on the country code."""
50+
cc = cc.lower()
51+
if cc not in MEMBER_STATES:
52+
raise InvalidComponent()
53+
if cc not in _country_modules:
54+
_country_modules[cc] = get_cc_module(cc, 'excise')
55+
return _country_modules[cc]
56+
57+
58+
def compact(number: str) -> str:
59+
"""Convert the number to the minimal representation. This strips the number
60+
of any valid separators and removes surrounding whitespace."""
61+
number = clean(number, ' ').upper().strip()
62+
if len(number) < 13:
63+
number = number[:2] + number[2:].zfill(11)
64+
return number
65+
66+
67+
def validate(number: str) -> str:
68+
"""Check if the number is a valid Excise number."""
69+
number = compact(number)
70+
if len(number) != 13:
71+
raise InvalidLength()
72+
module = _get_cc_module(number[:2])
73+
if module: # pragma: no cover (no implementation yet)
74+
module.validate(number)
75+
return number
76+
77+
78+
def is_valid(number: str) -> bool:
79+
"""Check if the number is a valid excise number."""
80+
try:
81+
return bool(validate(number))
82+
except ValidationError:
83+
return False

tests/test_eu_excise.doctest

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
test_eu_excise.doctest - more detailed doctests for the stdnum.eu.excise module
2+
3+
Copyright (C) 2023 Cédric Krier
4+
5+
This library is free software; you can redistribute it and/or
6+
modify it under the terms of the GNU Lesser General Public
7+
License as published by the Free Software Foundation; either
8+
version 2.1 of the License, or (at your option) any later version.
9+
10+
This library is distributed in the hope that it will be useful,
11+
but WITHOUT ANY WARRANTY; without even the implied warranty of
12+
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
13+
Lesser General Public License for more details.
14+
15+
You should have received a copy of the GNU Lesser General Public
16+
License along with this library; if not, write to the Free Software
17+
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
18+
02110-1301 USA
19+
20+
This file contains more detailed doctests for the stdnum.eu.excise module. It
21+
tries to validate a number of Excise numbers that have been found online.
22+
23+
>>> from stdnum.eu import excise
24+
>>> from stdnum.exceptions import *
25+
26+
These numbers should be mostly valid except that they have the wrong length.
27+
28+
>>> excise.validate('LU987ABC')
29+
'LU00000987ABC'
30+
>>> excise.validate('LU000000987ABC')
31+
Traceback (most recent call last):
32+
...
33+
InvalidLength: ...
34+
35+
36+
These numbers should be mostly valid except that they have the wrong prefix.
37+
38+
>>> excise.validate('XX00000987ABC')
39+
Traceback (most recent call last):
40+
...
41+
InvalidComponent: ...
42+
43+
44+
These have been found online and should all be valid numbers.
45+
46+
>>> numbers = '''
47+
...
48+
... LU 00000987ABC
49+
... FR012907E0820
50+
... '''
51+
>>> [x for x in numbers.splitlines() if x and not excise.is_valid(x)]
52+
[]

0 commit comments

Comments
 (0)