lists.arthurdejong.org
RSS feed

python-stdnum branch master updated. 2.1-18-ga906a1e

[Date Prev][Date Next] [Thread Prev][Thread Next]

python-stdnum branch master updated. 2.1-18-ga906a1e



This is an automated email from the git hooks/post-receive script. It was
generated because a ref change was pushed to the repository containing
the project "python-stdnum".

The branch, master has been updated
       via  a906a1e348daa03f682542c4a3b9fbec37a6a7e8 (commit)
       via  1a254d9a979ad794b3d36eaf2e0e771468917d90 (commit)
      from  d534b3c6b8baae7e84a9b79bd06dc21070826af1 (commit)

Those revisions listed above that are new to this repository have
not appeared on any other notification email; so we list those
revisions in full, below.

- Log -----------------------------------------------------------------
https://arthurdejong.org/git/python-stdnum/commit/?id=a906a1e348daa03f682542c4a3b9fbec37a6a7e8

commit a906a1e348daa03f682542c4a3b9fbec37a6a7e8
Author: Cédric Krier <ced@b2ck.com>
Date:   Fri Dec 8 18:48:54 2023 +0100

    Add accise - the French number for excise
    
    Closes https://github.com/arthurdejong/python-stdnum/pull/424

diff --git a/stdnum/eu/excise.py b/stdnum/eu/excise.py
index c9a7121..d84377d 100644
--- a/stdnum/eu/excise.py
+++ b/stdnum/eu/excise.py
@@ -70,7 +70,7 @@ def validate(number: str) -> str:
     if len(number) != 13:
         raise InvalidLength()
     module = _get_cc_module(number[:2])
-    if module:  # pragma: no cover (no implementation yet)
+    if module:
         module.validate(number)
     return number
 
diff --git a/stdnum/fr/__init__.py b/stdnum/fr/__init__.py
index abb5e23..70293e5 100644
--- a/stdnum/fr/__init__.py
+++ b/stdnum/fr/__init__.py
@@ -22,5 +22,6 @@
 
 from __future__ import annotations
 
-# provide vat as an alias
+# provide excise and vat as an alias
+from stdnum.fr import accise as excise  # noqa: F401
 from stdnum.fr import tva as vat  # noqa: F401
diff --git a/stdnum/fr/accise.py b/stdnum/fr/accise.py
new file mode 100644
index 0000000..1e97ffe
--- /dev/null
+++ b/stdnum/fr/accise.py
@@ -0,0 +1,79 @@
+# accise.py - functions for handling French Accise 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
+
+"""n° d'accise (French number to identify taxpayers of excise taxes).
+
+The n° d'accise always start by FR0 following by the 2 ending digits of the
+year, 3 number of customs office, one letter for the type and an ordering
+number of 4 digits.
+
+>>> validate('FR023004N9448')
+'FR023004N9448'
+>>> validate('FR0XX907E0820')
+Traceback (most recent call last):
+    ...
+InvalidFormat: ...
+>>> validate('FR012907A0820')
+Traceback (most recent call last):
+    ...
+InvalidComponent: ...
+"""
+
+from __future__ import annotations
+
+from stdnum.exceptions import *
+from stdnum.util import clean, isdigits
+
+
+OPERATORS = set(['E', 'N', 'C', 'B'])
+
+
+def compact(number: str) -> str:
+    """Convert the number to the minimal representation. This strips the number
+    of any valid separators and removes surrounding whitespace."""
+    return clean(number, ' ').upper().strip()
+
+
+def validate(number: str) -> str:
+    """Check if the number is a valid accise number. This checks the length,
+    formatting."""
+    number = compact(number)
+    code = number[:3]
+    if code != 'FR0':
+        raise InvalidFormat()
+    if len(number) != 13:
+        raise InvalidLength()
+    if not isdigits(number[3:5]):
+        raise InvalidFormat()
+    if not isdigits(number[5:8]):
+        raise InvalidFormat()
+    if number[8] not in OPERATORS:
+        raise InvalidComponent()
+    if not isdigits(number[9:12]):
+        raise InvalidFormat()
+    return number
+
+
+def is_valid(number: str) -> bool:
+    """Check if the number is a valid accise number."""
+    try:
+        return bool(validate(number))
+    except ValidationError:
+        return False
diff --git a/tests/test_fr_accise.doctest b/tests/test_fr_accise.doctest
new file mode 100644
index 0000000..d4484aa
--- /dev/null
+++ b/tests/test_fr_accise.doctest
@@ -0,0 +1,53 @@
+test_fr_accise.doctest - more detailed doctests for the stdnum.fr.accise 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.fr.accise module. It
+tries to validate a number of Excise numbers that have been found online.
+
+>>> from stdnum.fr import accise
+>>> from stdnum.exceptions import *
+
+
+>>> accise.compact('FR0 23 004 N 9448')
+'FR023004N9448'
+>>> accise.validate('FR023004N9448')
+'FR023004N9448'
+>>> accise.validate('FR012907E0820')
+'FR012907E0820'
+
+>>> accise.validate('FR012345')
+Traceback (most recent call last):
+    ...
+InvalidLength: ...
+>>> accise.validate('FR0XX907E0820')
+Traceback (most recent call last):
+    ...
+InvalidFormat: ...
+>>> accise.validate('FR012XXXE0820')
+Traceback (most recent call last):
+    ...
+InvalidFormat: ...
+>>> accise.validate('FR012907A0820')
+Traceback (most recent call last):
+    ...
+InvalidComponent: ...
+>>> accise.validate('FR012907EXXXX')
+Traceback (most recent call last):
+    ...
+InvalidFormat: ...

https://arthurdejong.org/git/python-stdnum/commit/?id=1a254d9a979ad794b3d36eaf2e0e771468917d90

commit 1a254d9a979ad794b3d36eaf2e0e771468917d90
Author: Cédric Krier <ced@b2ck.com>
Date:   Fri Dec 8 16:04:51 2023 +0100

    Add European Excise Number
    
    Closes https://github.com/arthurdejong/python-stdnum/pull/424

diff --git a/stdnum/eu/excise.py b/stdnum/eu/excise.py
new file mode 100644
index 0000000..c9a7121
--- /dev/null
+++ b/stdnum/eu/excise.py
@@ -0,0 +1,83 @@
+# 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
+
+"""European Excise Number
+
+The excise duty identification number assigned to businesses authorised to
+operate with excise goods (e.g. alcohol, tobacco, energy products, etc.).
+
+The number is issued by the national customs or tax authority of the Member
+State where the business is established. The number consists of a two-letter
+country code followed by up to 13 alphanumeric characters.
+
+More information:
+
+* https://ec.europa.eu/taxation_customs/dds2/seed/
+
+>>> validate('LU 987ABC')
+'LU00000987ABC'
+"""
+
+from __future__ import annotations
+
+from stdnum.eu.vat import MEMBER_STATES
+from stdnum.exceptions import *
+from stdnum.util import NumberValidationModule, clean, get_cc_module
+
+
+_country_modules = dict()
+
+
+def _get_cc_module(cc: str) -> NumberValidationModule | None:
+    """Get the Excise number module based on the country code."""
+    cc = cc.lower()
+    if cc not in MEMBER_STATES:
+        raise InvalidComponent()
+    if cc not in _country_modules:
+        _country_modules[cc] = get_cc_module(cc, 'excise')
+    return _country_modules[cc]
+
+
+def compact(number: str) -> str:
+    """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 len(number) < 13:
+        number = number[:2] + number[2:].zfill(11)
+    return number
+
+
+def validate(number: str) -> str:
+    """Check if the number is a valid Excise number."""
+    number = compact(number)
+    if len(number) != 13:
+        raise InvalidLength()
+    module = _get_cc_module(number[:2])
+    if module:  # pragma: no cover (no implementation yet)
+        module.validate(number)
+    return number
+
+
+def is_valid(number: str) -> bool:
+    """Check if the number is a valid excise number."""
+    try:
+        return bool(validate(number))
+    except ValidationError:
+        return False
diff --git a/tests/test_eu_excise.doctest b/tests/test_eu_excise.doctest
new file mode 100644
index 0000000..99457d1
--- /dev/null
+++ b/tests/test_eu_excise.doctest
@@ -0,0 +1,52 @@
+test_eu_excise.doctest - 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 numbers should be mostly valid except that they have the wrong length.
+
+>>> excise.validate('LU987ABC')
+'LU00000987ABC'
+>>> excise.validate('LU000000987ABC')
+Traceback (most recent call last):
+    ...
+InvalidLength: ...
+
+
+These numbers should be mostly valid except that they have the wrong prefix.
+
+>>> excise.validate('XX00000987ABC')
+Traceback (most recent call last):
+    ...
+InvalidComponent: ...
+
+
+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)]
+[]

-----------------------------------------------------------------------

Summary of changes:
 stdnum/eu/excise.py                                | 83 ++++++++++++++++++++++
 stdnum/fr/__init__.py                              |  3 +-
 stdnum/{eu/banknote.py => fr/accise.py}            | 61 ++++++++--------
 ...{test_nz_ird.doctest => test_eu_excise.doctest} | 44 ++++--------
 ...{test_fr_rcs.doctest => test_fr_accise.doctest} | 41 +++++------
 5 files changed, 151 insertions(+), 81 deletions(-)
 create mode 100644 stdnum/eu/excise.py
 copy stdnum/{eu/banknote.py => fr/accise.py} (54%)
 copy tests/{test_nz_ird.doctest => test_eu_excise.doctest} (53%)
 copy tests/{test_fr_rcs.doctest => test_fr_accise.doctest} (57%)


hooks/post-receive
-- 
python-stdnum