File: pvn.py

package info (click to toggle)
python-stdnum 2.2-1
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid
  • size: 6,960 kB
  • sloc: python: 10,600; javascript: 6,995; sh: 13; makefile: 10
file content (133 lines) | stat: -rw-r--r-- 4,535 bytes parent folder | download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
# pvn.py - functions for handling Latvian PVN (VAT) numbers
# coding: utf-8
#
# Copyright (C) 2012-2026 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

"""PVN (Pievienotās vērtības nodokļa, Latvian VAT number).

The PVN is a 11-digit number that can either be a reference to a legal
entity (in which case the first digit > 3) or a natural person (in which
case it should be the same as the personal code (personas kods)). Personal
codes start "32". Older personal codes start with 6 digits to denote the birth
date in the form ddmmyy.

More information:

* https://en.wikipedia.org/wiki/National_identification_number#Latvia

>>> validate('LV 4000 3521 600')
'40003521600'
>>> validate('40003521601')  # invalid check digit
Traceback (most recent call last):
    ...
InvalidChecksum: ...
>>> validate('161175-19997')  # personal code, old format
'16117519997'
>>> validate('161375-19997')  # invalid date
Traceback (most recent call last):
    ...
InvalidComponent: ...
>>> validate('328673-00679')  # personal code, new format
'32867300679'
>>> validate('328673-00677')  # personal code, new format
Traceback (most recent call last):
    ...
InvalidChecksum: ...
"""

from __future__ import annotations

import datetime

from stdnum.exceptions import *
from stdnum.util import clean, isdigits


# validation functions are available on-line but it is not allowed
# to perform automated queries:
# https://www6.vid.gov.lv/VID_PDB?aspxerrorpath=/vid_pdb/pvn.asp


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 number.startswith('LV'):
        number = number[2:]
    return number


def checksum(number: str) -> int:
    """Calculate the checksum for legal entities."""
    weights = (9, 1, 4, 8, 3, 10, 2, 5, 7, 6, 1)
    return sum(w * int(n) for w, n in zip(weights, number)) % 11


def calc_check_digit_pers(number: str) -> str:
    """Calculate the check digit for personal codes. The number passed
    should not have the check digit included."""
    # note that this algorithm has not been confirmed by an independent source
    weights = (10, 5, 8, 4, 2, 1, 6, 3, 7, 9)
    check = 1 + sum(w * int(n) for w, n in zip(weights, number))
    return str(check % 11 % 10)


def get_birth_date(number: str) -> datetime.date:
    """Split the date parts from the number and return the birth date."""
    number = compact(number)
    day = int(number[0:2])
    month = int(number[2:4])
    year = int(number[4:6])
    year += 1800 + int(number[6]) * 100
    try:
        return datetime.date(year, month, day)
    except ValueError:
        raise InvalidComponent()


def validate(number: str) -> str:
    """Check if the number is a valid VAT number. This checks the length,
    formatting and check digit."""
    number = compact(number)
    if not isdigits(number):
        raise InvalidFormat()
    if len(number) != 11:
        raise InvalidLength()
    if number[0] > '3':
        # legal entity
        if checksum(number) != 3:
            raise InvalidChecksum()
    elif number.startswith('32'):
        # personal code without a date of birth (issued from July 2017 onwards)
        if calc_check_digit_pers(number[:-1]) != number[-1]:
            raise InvalidChecksum()
    else:
        # natural resident, check if birth date is valid
        get_birth_date(number)
        # TODO: check that the birth date is not in the future
        if calc_check_digit_pers(number[:-1]) != number[-1]:
            raise InvalidChecksum()
    return number


def is_valid(number: str) -> bool:
    """Check if the number is a valid VAT number."""
    try:
        return bool(validate(number))
    except ValidationError:
        return False