[IMP] base_vat: replace vatnumver by stdnum library.

Python module vatnumber doesn't seem maintained anymore. Therefore, we should:
    - call directly stdnum (which is maintained and mostly used everywhere in vatnumber)

Also improve stdnum import, vat fix method and vat expected formats

task-1915371

closes odoo/odoo#36978

Signed-off-by: Quentin De Paoli (qdp) <qdp@openerp.com>
This commit is contained in:
Hardik Prajapati
2020-02-05 10:39:39 +00:00
committed by jbw
parent 825b227d5c
commit a43f857423
9 changed files with 98 additions and 92 deletions
+91 -84
View File
@@ -2,63 +2,13 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details. # Part of Odoo. See LICENSE file for full copyright and licensing details.
import datetime import datetime
import logging
import string import string
import re import re
import stdnum
_logger = logging.getLogger(__name__)
try:
import vatnumber
except ImportError:
_logger.warning("VAT validation partially unavailable because the `vatnumber` Python library cannot be found. "
"Install it to support more countries, for example with `easy_install vatnumber`.")
vatnumber = None
# Although stdnum is a dependency of vatnumber, the import of the latter is surrounded by a try/except
# if it is not installed. Therefore, we cannot be sure stdnum is installed in all cases.
try:
import stdnum
except ImportError:
stdnum = None
from odoo import api, models, tools, _ from odoo import api, models, tools, _
from odoo.tools.misc import ustr from odoo.tools.misc import ustr
from odoo.exceptions import ValidationError from odoo.exceptions import ValidationError
from stdnum.at.uid import compact as compact_at
from stdnum.be.vat import compact as compact_be
from stdnum.bg.vat import compact as compact_bg
from stdnum.ch.vat import compact as compact_ch
from stdnum.cy.vat import compact as compact_cy
from stdnum.cz.dic import compact as compact_cz
from stdnum.de.vat import compact as compact_de
from stdnum.ee.kmkr import compact as compact_ee
# el not in stdnum
from stdnum.es.nif import compact as compact_es
from stdnum.fi.alv import compact as compact_fi
from stdnum.fr.tva import compact as compact_fr
from stdnum.gb.vat import compact as compact_gb
from stdnum.gr.vat import compact as compact_gr
from stdnum.hu.anum import compact as compact_hu
from stdnum.hr.oib import compact as compact_hr
from stdnum.ie.vat import compact as compact_ie
from stdnum.it.iva import compact as compact_it
from stdnum.lt.pvm import compact as compact_lt
from stdnum.lu.tva import compact as compact_lu
from stdnum.lv.pvn import compact as compact_lv
from stdnum.mt.vat import compact as compact_mt
from stdnum.mx.rfc import compact as compact_mx
from stdnum.nl.btw import compact as compact_nl
from stdnum.no.mva import compact as compact_no
# pe is not in stdnum
from stdnum.pl.nip import compact as compact_pl
from stdnum.pt.nif import compact as compact_pt
from stdnum.ro.cf import compact as compact_ro
from stdnum.se.vat import compact as compact_se
from stdnum.si.ddv import compact as compact_si
from stdnum.sk.dph import compact as compact_sk
from stdnum.ar.cuit import compact as compact_ar
# tr compact vat is not in stdnum
_eu_country_vat = { _eu_country_vat = {
'GR': 'EL' 'GR': 'EL'
@@ -67,42 +17,51 @@ _eu_country_vat = {
_eu_country_vat_inverse = {v: k for k, v in _eu_country_vat.items()} _eu_country_vat_inverse = {v: k for k, v in _eu_country_vat.items()}
_ref_vat = { _ref_vat = {
'al': 'ALJ91402501L',
'ar': 'AR200-5536168-2 or 20055361682',
'at': 'ATU12345675', 'at': 'ATU12345675',
'be': 'BE0477472701', 'be': 'BE0477472701',
'bg': 'BG1234567892', 'bg': 'BG1234567892',
'ch': 'CHE-123.456.788 TVA or CH TVA 123456', # Swiss by Yannick Vaucher @ Camptocamp 'ch': 'CHE-123.456.788 TVA or CH TVA 123456', # Swiss by Yannick Vaucher @ Camptocamp
'cl': 'CL76086428-5', 'cl': 'CL76086428-5',
'co': 'CO213123432-1 or CO213.123.432-1', 'co': 'CO213123432-1 or CO213.123.432-1',
'cy': 'CY12345678F', 'cy': 'CY10259033P',
'cz': 'CZ12345679', 'cz': 'CZ12345679',
'de': 'DE123456788', 'de': 'DE123456788',
'dk': 'DK12345674', 'dk': 'DK12345674',
'do': 'DO1-01-85004-3 or 101850043',
'ec': 'EC1792060346-001',
'ee': 'EE123456780', 'ee': 'EE123456780',
'el': 'EL12345670', 'el': 'EL12345670',
'es': 'ESA12345674', 'es': 'ESA12345674',
'fi': 'FI12345671', 'fi': 'FI12345671',
'fr': 'FR32123456789', 'fr': 'FR23334175221',
'gb': 'GB123456782', 'gb': 'GB123456782',
'gr': 'GR12345670', 'gr': 'GR12345670',
'hu': 'HU12345676', 'hu': 'HU12345676',
'hr': 'HR01234567896', # Croatia, contributed by Milan Tribuson 'hr': 'HR01234567896', # Croatia, contributed by Milan Tribuson
'ie': 'IE1234567FA', 'ie': 'IE1234567FA',
'is': 'IS062199',
'it': 'IT12345670017', 'it': 'IT12345670017',
'lt': 'LT123456715', 'lt': 'LT123456715',
'lu': 'LU12345613', 'lu': 'LU12345613',
'lv': 'LV41234567891', 'lv': 'LV41234567891',
'mc': 'FR53000004605',
'mt': 'MT12345634', 'mt': 'MT12345634',
'mx': 'ABC123456T1B', 'mx': 'MXGODE561231GR8 or GODE561231GR8',
'nl': 'NL123456782B90', 'nl': 'NL123456782B90',
'no': 'NO123456785', 'no': 'NO123456785',
'pe': '10XXXXXXXXY or 20XXXXXXXXY or 15XXXXXXXXY or 16XXXXXXXXY or 17XXXXXXXXY', 'pe': '10XXXXXXXXY or 20XXXXXXXXY or 15XXXXXXXXY or 16XXXXXXXXY or 17XXXXXXXXY',
'pl': 'PL1234567883', 'pl': 'PL1234567883',
'pt': 'PT123456789', 'pt': 'PT123456789',
'ro': 'RO1234567897', 'ro': 'RO1234567897',
'rs': 'RS101134702',
'ru': 'RU123456789047',
'se': 'SE123456789701', 'se': 'SE123456789701',
'si': 'SI12345679', 'si': 'SI12345679',
'sk': 'SK0012345675', 'sk': 'SK2022749619',
'tr': 'TR1234567890 (VERGINO) veya TR12345678901 (TCKIMLIKNO)' # Levent Karakas @ Eska Yazilim A.S. 'sm': 'SM24165',
'tr': 'TR1234567890 (VERGINO) or TR17291716060 (TCKIMLIKNO)' # Levent Karakas @ Eska Yazilim A.S.
} }
@@ -122,7 +81,7 @@ class ResPartner(models.Model):
if not ustr(country_code).encode('utf-8').isalpha(): if not ustr(country_code).encode('utf-8').isalpha():
return False return False
check_func_name = 'check_vat_' + country_code check_func_name = 'check_vat_' + country_code
check_func = getattr(self, check_func_name, None) or getattr(vatnumber, check_func_name, None) check_func = getattr(self, check_func_name, None) or getattr(stdnum.util.get_cc_module(country_code, 'vat'), 'is_valid', None)
if not check_func: if not check_func:
# No VAT validation available, default to check that the country code exists # No VAT validation available, default to check that the country code exists
if country_code.upper() == 'EU': if country_code.upper() == 'EU':
@@ -138,7 +97,7 @@ class ResPartner(models.Model):
def _check_vies(self, vat): def _check_vies(self, vat):
# Store the VIES result in the cache. In case an exception is raised during the request # Store the VIES result in the cache. In case an exception is raised during the request
# (e.g. service unavailable), the fallback on simple_vat_check is not kept in cache. # (e.g. service unavailable), the fallback on simple_vat_check is not kept in cache.
return vatnumber.check_vies(vat) return stdnum.eu.vat.check_vies(vat)
@api.model @api.model
def vies_vat_check(self, country_code, vat_number): def vies_vat_check(self, country_code, vat_number):
@@ -252,6 +211,26 @@ class ResPartner(models.Model):
checksum = extra + sum((8-i) * int(x) for i, x in enumerate(vat[:7])) checksum = extra + sum((8-i) * int(x) for i, x in enumerate(vat[:7]))
return 'WABCDEFGHIJKLMNOPQRSTUV'[checksum % 23] return 'WABCDEFGHIJKLMNOPQRSTUV'[checksum % 23]
def check_vat_co(self, rut):
'''
Check Colombian RUT number.
Method copied from vatnumber 1.2 lib https://code.google.com/archive/p/vatnumber/
'''
if len(rut) != 10:
return False
try:
int(rut)
except ValueError:
return False
nums = [3, 7, 13, 17, 19, 23, 29, 37, 41, 43, 47, 53, 59, 67, 71]
sum = 0
for i in range(len(rut) - 2, -1, -1):
sum += int(rut[i]) * nums[len(rut) - 2 - i]
if sum % 11 > 1:
return int(rut[-1]) == 11 - (sum % 11)
else:
return int(rut[-1]) == sum % 11
def check_vat_ie(self, vat): def check_vat_ie(self, vat):
""" Temporary Ireland VAT validation to support the new format """ Temporary Ireland VAT validation to support the new format
introduced in January 2013 in Ireland, until upstream is fixed. introduced in January 2013 in Ireland, until upstream is fixed.
@@ -396,6 +375,43 @@ class ResPartner(models.Model):
dig_check = 1 dig_check = 1
return int(vat[10]) == dig_check return int(vat[10]) == dig_check
def check_vat_ru(self, vat):
'''
Check Russia VAT number.
Method copied from vatnumber 1.2 lib https://code.google.com/archive/p/vatnumber/
'''
if len(vat) != 10 and len(vat) != 12:
return False
try:
int(vat)
except ValueError:
return False
if len(vat) == 10:
check_sum = 2 * int(vat[0]) + 4 * int(vat[1]) + 10 * int(vat[2]) + \
3 * int(vat[3]) + 5 * int(vat[4]) + 9 * int(vat[5]) + \
4 * int(vat[6]) + 6 * int(vat[7]) + 8 * int(vat[8])
check = check_sum % 11
if check % 10 != int(vat[9]):
return False
else:
check_sum1 = 7 * int(vat[0]) + 2 * int(vat[1]) + 4 * int(vat[2]) + \
10 * int(vat[3]) + 3 * int(vat[4]) + 5 * int(vat[5]) + \
9 * int(vat[6]) + 4 * int(vat[7]) + 6 * int(vat[8]) + \
8 * int(vat[9])
check = check_sum1 % 11
if check != int(vat[10]):
return False
check_sum2 = 3 * int(vat[0]) + 7 * int(vat[1]) + 2 * int(vat[2]) + \
4 * int(vat[3]) + 10 * int(vat[4]) + 3 * int(vat[5]) + \
5 * int(vat[6]) + 9 * int(vat[7]) + 4 * int(vat[8]) + \
6 * int(vat[9]) + 8 * int(vat[10])
check = check_sum2 % 11
if check != int(vat[11]):
return False
return True
# VAT validation in Turkey, contributed by # Levent Karakas @ Eska Yazilim A.S. # VAT validation in Turkey, contributed by # Levent Karakas @ Eska Yazilim A.S.
def check_vat_tr(self, vat): def check_vat_tr(self, vat):
@@ -439,35 +455,27 @@ class ResPartner(models.Model):
return False return False
def check_vat_al(self, vat): def check_vat_ua(self, vat):
'''
Check Ukraine VAT number.
Method copied from vatnumber 1.2 lib https://code.google.com/archive/p/vatnumber/
'''
if len(vat) != 8:
return False
try: try:
import stdnum.al int(vat)
return stdnum.al.vat.is_valid(vat) except ValueError:
except ImportError: return False
return True return True
def check_vat_cl(self, vat):
return stdnum.util.get_cc_module('cl', 'vat').is_valid(vat) if stdnum else True
def check_vat_co(self, vat):
return stdnum.util.get_cc_module('co', 'vat').is_valid(vat) if stdnum else True
# Argentinian VAT validation, contributed by ADHOC
def check_vat_ar(self, vat):
try:
import stdnum.ar
return stdnum.ar.cuit.is_valid(vat)
except ImportError:
return True
def default_compact(self, vat):
return vat
def _fix_vat_number(self, vat): def _fix_vat_number(self, vat):
vat_country, vat_number = self._split_vat(vat) vat_country, vat_number = self._split_vat(vat)
check_func_name = 'compact_' + vat_country stdnum_vat_fix_func = getattr(stdnum.util.get_cc_module(vat_country, 'vat'), 'compact', None)
check_func = globals().get(check_func_name) or getattr(self, 'default_compact') #If any localization module need to define vat fix method for it's country then we give first priority to it.
vat_number = check_func(vat_number) format_func_name = 'format_vat_' + vat_country
format_func = getattr(self, format_func_name, None) or stdnum_vat_fix_func
if format_func:
vat_number = format_func(vat_number)
return vat_country.upper() + vat_number return vat_country.upper() + vat_number
@api.model @api.model
@@ -480,4 +488,3 @@ class ResPartner(models.Model):
if values.get('vat'): if values.get('vat'):
values['vat'] = self._fix_vat_number(values['vat']) values['vat'] = self._fix_vat_number(values['vat'])
return super(ResPartner, self).write(values) return super(ResPartner, self).write(values)
@@ -83,8 +83,8 @@ PKGS_TO_INSTALL="
python3-reportlab \ python3-reportlab \
python3-requests \ python3-requests \
python3-simplejson \ python3-simplejson \
python3-stdnum \
python3-tz \ python3-tz \
python3-vatnumber \
python3-werkzeug \ python3-werkzeug \
python3-serial \ python3-serial \
python3-pip \ python3-pip \
+1 -1
View File
@@ -39,9 +39,9 @@ Depends:
python3-qrcode, python3-qrcode,
python3-reportlab, python3-reportlab,
python3-requests, python3-requests,
python3-stdnum,
python3-suds, python3-suds,
python3-tz, python3-tz,
python3-vatnumber,
python3-vobject, python3-vobject,
python3-werkzeug, python3-werkzeug,
python3-xlsxwriter, python3-xlsxwriter,
+1 -1
View File
@@ -39,7 +39,7 @@ qrcode==6.1
reportlab==3.5.13 reportlab==3.5.13
requests==2.21.0 requests==2.21.0
zeep==3.2.0 zeep==3.2.0
vatnumber==1.2 python-stdnum==1.8
vobject==0.9.6.1 vobject==0.9.6.1
Werkzeug==0.14.1 Werkzeug==0.14.1
XlsxWriter==1.1.2 XlsxWriter==1.1.2
-1
View File
@@ -45,7 +45,6 @@ requires =
python3-six python3-six
python3-stdnum python3-stdnum
python3-suds python3-suds
python3-vatnumber
python3-vobject python3-vobject
python3-werkzeug python3-werkzeug
python3-xlwt python3-xlwt
+1 -1
View File
@@ -45,13 +45,13 @@ setup(
'pypdf2', 'pypdf2',
'pyserial', 'pyserial',
'python-dateutil', 'python-dateutil',
'python-stdnum',
'pytz', 'pytz',
'pyusb >= 1.0.0b1', 'pyusb >= 1.0.0b1',
'qrcode', 'qrcode',
'reportlab', # windows binary pypi.python.org/pypi/reportlab 'reportlab', # windows binary pypi.python.org/pypi/reportlab
'requests', 'requests',
'zeep', 'zeep',
'vatnumber',
'vobject', 'vobject',
'werkzeug', 'werkzeug',
'xlsxwriter', 'xlsxwriter',
+1 -1
View File
@@ -48,10 +48,10 @@ RUN apt-get update -qq && \
python3-reportlab \ python3-reportlab \
python3-requests \ python3-requests \
python3-serial \ python3-serial \
python3-stdnum \
python3-suds \ python3-suds \
python3-tz \ python3-tz \
python3-usb \ python3-usb \
python3-vatnumber \
python3-vobject \ python3-vobject \
python3-werkzeug \ python3-werkzeug \
python3-xlsxwriter \ python3-xlsxwriter \
+1 -1
View File
@@ -39,8 +39,8 @@ RUN dnf update -d 0 -e 0 -y && \
python3-reportlab \ python3-reportlab \
python3-requests \ python3-requests \
python3-six \ python3-six \
python3-stdnum \
python3-suds \ python3-suds \
python3-vatnumber \
python3-vobject \ python3-vobject \
python3-werkzeug \ python3-werkzeug \
python3-xlwt \ python3-xlwt \
+1 -1
View File
@@ -29,8 +29,8 @@ qrcode==5.3
reportlab>=3.3.0 reportlab>=3.3.0
requests==2.20.0 requests==2.20.0
six==1.10.0 six==1.10.0
stdnum==1.8
zeep==3.1.0 zeep==3.1.0
vatnumber==1.2
vobject==0.9.3 vobject==0.9.3
Werkzeug>=0.11.11 Werkzeug>=0.11.11
XlsxWriter==0.9.3 XlsxWriter==0.9.3