Odoo18-Base/addons/base_vat/models/res_partner.py
2025-01-06 10:57:38 +07:00

737 lines
31 KiB
Python

import datetime
import string
import re
import stdnum
from stdnum.eu.vat import check_vies
from stdnum.exceptions import InvalidComponent, InvalidChecksum, InvalidFormat
from stdnum.util import clean
from stdnum import luhn
import logging
from odoo import api, models, fields
from odoo.tools import _, zeep, LazyTranslate
from odoo.exceptions import ValidationError
_lt = LazyTranslate(__name__)
_logger = logging.getLogger(__name__)
_eu_country_vat = {
'GR': 'EL'
}
_eu_country_vat_inverse = {v: k for k, v in _eu_country_vat.items()}
_ref_vat = {
'al': 'ALJ91402501L',
'ar': _lt('AR200-5536168-2 or 20055361682'),
'at': 'ATU12345675',
'au': '83 914 571 673',
'be': 'BE0477472701',
'bg': 'BG1234567892',
'br': _lt('either 11 digits for CPF or 14 digits for CNPJ'),
'cr': _lt('3101012009'),
'ch': _lt('CHE-123.456.788 TVA or CHE-123.456.788 MWST or CHE-123.456.788 IVA'), # Swiss by Yannick Vaucher @ Camptocamp
'cl': 'CL76086428-5',
'co': _lt('CO213123432-1 or CO213.123.432-1'),
'cy': 'CY10259033P',
'cz': 'CZ12345679',
'de': _lt('DE123456788 or 12/345/67890'),
'dk': 'DK12345674',
'do': _lt('DO1-01-85004-3 or 101850043'),
'ec': _lt('1792060346001 or 1792060346'),
'ee': 'EE123456780',
'es': 'ESA12345674',
'fi': 'FI12345671',
'fr': 'FR23334175221',
'gb': _lt('GB123456782 or XI123456782'),
'gr': 'EL123456783',
'hu': _lt('HU12345676 or 12345678-1-11 or 8071592153'),
'hr': 'HR01234567896', # Croatia, contributed by Milan Tribuson
'ie': 'IE1234567FA',
'il': _lt('XXXXXXXXX [9 digits] and it should respect the Luhn algorithm checksum'),
'in': "12AAAAA1234AAZA",
'is': 'IS062199',
'it': 'IT12345670017',
'kr': '123-45-67890 or 1234567890',
'lt': 'LT123456715',
'lu': 'LU12345613',
'lv': 'LV41234567891',
'mc': 'FR53000004605',
'mt': 'MT12345634',
'mx': _lt('MXGODE561231GR8 or GODE561231GR8'),
'nl': 'NL123456782B90',
'no': 'NO123456785',
'nz': _lt('49-098-576 or 49098576'),
'pe': _lt('10XXXXXXXXY or 20XXXXXXXXY or 15XXXXXXXXY or 16XXXXXXXXY or 17XXXXXXXXY'),
'ph': '123-456-789-123',
'pl': 'PL1234567883',
'pt': 'PT123456789',
'ro': 'RO1234567897 or 8001011234567 or 9000123456789',
'rs': 'RS101134702',
'ru': 'RU123456789047',
'se': 'SE123456789701',
'si': 'SI12345679',
'sk': 'SK2022749619',
'sm': 'SM24165',
'tr': _lt('17291716060 (NIN) or 1729171602 (VKN)'),
'uy': _lt("Example: '219999830019' (format: 12 digits, all numbers, valid check digit)"),
've': 'V-12345678-1, V123456781, V-12.345.678-1',
'xi': 'XI123456782',
'sa': _lt('310175397400003 [Fifteen digits, first and last digits should be "3"]')
}
_region_specific_vat_codes = {
'xi',
't',
}
class ResPartner(models.Model):
_inherit = 'res.partner'
vies_valid = fields.Boolean(
string="Intra-Community Valid",
compute='_compute_vies_valid', store=True, readonly=False,
tracking=True,
help='European VAT numbers are automatically checked on the VIES database.',
)
# Field representing whether vies_valid is relevant for selecting a fiscal position on this partner
perform_vies_validation = fields.Boolean(compute='_compute_perform_vies_validation')
# Technical field used to determine the VAT to check
vies_vat_to_check = fields.Char(compute='_compute_vies_vat_to_check')
def _split_vat(self, vat):
'''
Splits the VAT Number to get the country code in a first place and the code itself in a second place.
This has to be done because some countries' code are one character long instead of two (i.e. "T" for Japan)
'''
if len(vat) > 1 and vat[1].isalpha():
vat_country, vat_number = vat[:2].lower(), vat[2:].replace(' ', '')
else:
vat_country, vat_number = vat[:1].lower(), vat[1:].replace(' ', '')
return vat_country, vat_number
@api.model
def simple_vat_check(self, country_code, vat_number):
'''
Check the VAT number depending of the country.
http://sima-pc.com/nif.php
'''
if not country_code.encode().isalpha():
return False
check_func_name = 'check_vat_' + country_code
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:
# No VAT validation available, default to check that the country code exists
country_code = _eu_country_vat_inverse.get(country_code, country_code)
return bool(self.env['res.country'].search([('code', '=ilike', country_code)]))
return check_func(vat_number)
@api.depends('vat', 'country_id')
def _compute_vies_vat_to_check(self):
""" Retrieve the VAT number, if one such exists, to be used when checking against the VIES system """
eu_country_codes = self.env.ref('base.europe').country_ids.mapped('code')
for partner in self:
# Skip checks when only one character is used. Some users like to put '/' or other as VAT to differentiate between
# a partner for which they haven't yet input VAT, and one not subject to VAT
if not partner.vat or len(partner.vat) == 1:
partner.vies_vat_to_check = ''
continue
country_code, number = partner._split_vat(partner.vat)
if not country_code.isalpha() and partner.country_id:
country_code = partner.country_id.code
number = partner.vat
partner.vies_vat_to_check = (
country_code.upper() in eu_country_codes or
country_code.lower() in _region_specific_vat_codes
) and self._fix_vat_number(country_code + number, partner.country_id.id) or ''
@api.depends_context('company')
@api.depends('vies_vat_to_check')
def _compute_perform_vies_validation(self):
""" Determine whether to show VIES validity on the current VAT number """
for partner in self:
to_check = partner.vies_vat_to_check
company_code = self.env.company.account_fiscal_country_id.code
partner.perform_vies_validation = (
to_check
and not to_check[:2].upper() == company_code
and self.env.company.vat_check_vies
)
@api.model
def fix_eu_vat_number(self, country_id, vat):
europe = self.env.ref('base.europe')
country = self.env["res.country"].browse(country_id)
if not europe:
europe = self.env["res.country.group"].search([('name', '=', 'Europe')], limit=1)
if europe and country and country.id in europe.country_ids.ids:
vat = re.sub('[^A-Za-z0-9]', '', vat).upper()
country_code = _eu_country_vat.get(country.code, country.code).upper()
if vat[:2] != country_code:
vat = country_code + vat
return vat
@api.constrains('vat', 'country_id')
def check_vat(self):
# The context key 'no_vat_validation' allows you to store/set a VAT number without doing validations.
# This is for API pushes from external platforms where you have no control over VAT numbers.
if self.env.context.get('no_vat_validation'):
return
for partner in self:
# Skip checks when only one character is used. Some users like to put '/' or other as VAT to differentiate between
# A partner for which they didn't input VAT, and the one not subject to VAT
if not partner.vat or len(partner.vat) == 1:
continue
country = partner.commercial_partner_id.country_id
if self._run_vat_test(partner.vat, country, partner.is_company) is False:
partner_label = _("partner [%s]", partner.name)
msg = partner._build_vat_error_message(country and country.code.lower() or None, partner.vat, partner_label)
raise ValidationError(msg)
@api.depends('vies_vat_to_check')
def _compute_vies_valid(self):
""" Check the VAT number with VIES, if enabled."""
if not self.env['res.company'].sudo().search_count([('vat_check_vies', '=', True)]):
self.vies_valid = False
return
for partner in self:
if not partner.vies_vat_to_check:
partner.vies_valid = False
continue
if partner.parent_id and partner.parent_id.vies_vat_to_check == partner.vies_vat_to_check:
partner.vies_valid = partner.parent_id.vies_valid
continue
try:
_logger.info('Calling VIES service to check VAT for validation: %s', partner.vies_vat_to_check)
vies_valid = check_vies(partner.vies_vat_to_check, timeout=10)
partner.vies_valid = vies_valid['valid']
except (OSError, InvalidComponent, zeep.exceptions.Fault) as e:
if partner._origin.id:
msg = ""
if isinstance(e, OSError):
msg = _("Connection with the VIES server failed. The VAT number %s could not be validated.", partner.vies_vat_to_check)
elif isinstance(e, InvalidComponent):
msg = _("The VAT number %s could not be interpreted by the VIES server.", partner.vies_vat_to_check)
elif isinstance(e, zeep.exceptions.Fault):
msg = _('The request for VAT validation was not processed. VIES service has responded with the following error: %s', e.message)
partner._origin.message_post(body=msg)
_logger.warning("The VAT number %s failed VIES check.", partner.vies_vat_to_check)
partner.vies_valid = False
@api.model
def _run_vat_test(self, vat_number, default_country, partner_is_company=True):
# OVERRIDE account
check_result = None
# First check with country code as prefix of the TIN
vat_country_code, vat_number_split = self._split_vat(vat_number)
if vat_country_code == 'eu' and default_country not in self.env.ref('base.europe').country_ids:
# Foreign companies that trade with non-enterprises in the EU
# may have a VATIN starting with "EU" instead of a country code.
return True
vat_has_legit_country_code = self.env['res.country'].search([('code', '=', vat_country_code.upper())], limit=1)
if not vat_has_legit_country_code:
vat_has_legit_country_code = vat_country_code.lower() in _region_specific_vat_codes
if vat_has_legit_country_code:
check_result = self.simple_vat_check(vat_country_code, vat_number_split)
if check_result:
return vat_country_code
# If it fails, check with default_country (if it exists)
if default_country:
check_result = self.simple_vat_check(default_country.code.lower(), vat_number)
if check_result:
return default_country.code.lower()
# We allow any number if it doesn't start with a country code and the partner has no country.
# This is necessary to support an ORM limitation: setting vat and country_id together on a company
# triggers two distinct write on res.partner, one for each field, both triggering this constraint.
# If vat is set before country_id, the constraint must not break.
return check_result
@api.model
def _build_vat_error_message(self, country_code, wrong_vat, record_label):
# OVERRIDE account
if self.env.context.get('company_id'):
company = self.env['res.company'].browse(self.env.context['company_id'])
else:
company = self.env.company
vat_label = _("VAT")
if country_code and company.country_id and country_code == company.country_id.code.lower() and company.country_id.vat_label:
vat_label = company.country_id.vat_label
expected_format = _ref_vat.get(country_code, "'CC##' (CC=Country Code, ##=VAT Number)")
# Catch use case where the record label is about the public user (name: False)
if 'False' not in record_label:
return '\n' + _(
'The %(vat_label)s number [%(wrong_vat)s] for %(record_label)s does not seem to be valid. \nNote: the expected format is %(expected_format)s',
vat_label=vat_label,
wrong_vat=wrong_vat,
record_label=record_label,
expected_format=expected_format,
)
else:
return '\n' + _(
'The %(vat_label)s number [%(wrong_vat)s] does not seem to be valid. \nNote: the expected format is %(expected_format)s',
vat_label=vat_label,
wrong_vat=wrong_vat,
expected_format=expected_format,
)
__check_vat_al_re = re.compile(r'^[JKLM][0-9]{8}[A-Z]$')
def check_vat_al(self, vat):
"""Check Albania VAT number"""
number = stdnum.util.get_cc_module('al', 'vat').compact(vat)
if len(number) == 10 and self.__check_vat_al_re.match(number):
return True
return False
__check_tin1_ro_natural_persons = re.compile(r'[1-9]\d{2}(0[1-9]|1[0-2])(0[1-9]|[12]\d|3[01])\d{6}')
__check_tin2_ro_natural_persons = re.compile(r'9000\d{9}')
def check_vat_ro(self, vat):
"""
Check Romanian VAT number that can be for example 'RO1234567897 or 'xyyzzaabbxxxx' or '9000xxxxxxxx'.
- For xyyzzaabbxxxx, 'x' can be any number, 'y' is the two last digit of a year (in the range 00…99),
'a' is a month, b is a day of the month, the number 8 and 9 are Country or district code
(For those twos digits, we decided to let some flexibility to avoid complexifying the regex and also
for maintainability)
- 9000xxxxxxxx, start with 9000 and then is filled by number In the range 0...9
Also stdum also checks the CUI or CIF (Romanian company identifier). So a number like '123456897' will pass.
"""
tin1 = self.__check_tin1_ro_natural_persons.match(vat)
if tin1:
return True
tin2 = self.__check_tin1_ro_natural_persons.match(vat)
if tin2:
return True
# Check the vat number
return stdnum.util.get_cc_module('ro', 'vat').is_valid(vat)
__check_tin_hu_individual_re = re.compile(r'^8\d{9}$')
__check_tin_hu_companies_re = re.compile(r'^\d{8}-?[1-5]-?\d{2}$')
__check_tin_hu_european_re = re.compile(r'^\d{8}$')
def check_vat_hu(self, vat):
"""
Check Hungary VAT number that can be for example 'HU12345676 or 'xxxxxxxx-y-zz' or '8xxxxxxxxy'
- For xxxxxxxx-y-zz, 'x' can be any number, 'y' is a number between 1 and 5 depending on the person and the 'zz'
is used for region code.
- 8xxxxxxxxy, Tin number for individual, it has to start with an 8 and finish with the check digit
- In case of EU format it will be the first 8 digits of the full VAT
"""
companies = self.__check_tin_hu_companies_re.match(vat)
if companies:
return True
individual = self.__check_tin_hu_individual_re.match(vat)
if individual:
return True
european = self.__check_tin_hu_european_re.match(vat)
if european:
return True
# Check the vat number
return stdnum.util.get_cc_module('hu', 'vat').is_valid(vat)
__check_vat_ch_re = re.compile(r'E([0-9]{9}|-[0-9]{3}\.[0-9]{3}\.[0-9]{3})(MWST|TVA|IVA)$')
def check_vat_ch(self, vat):
'''
Check Switzerland VAT number.
'''
# A new VAT number format in Switzerland has been introduced between 2011 and 2013
# https://www.estv.admin.ch/estv/fr/home/mehrwertsteuer/fachinformationen/steuerpflicht/unternehmens-identifikationsnummer--uid-.html
# The old format "TVA 123456" is not valid since 2014
# Accepted format are: (spaces are ignored)
# CHE#########MWST
# CHE#########TVA
# CHE#########IVA
# CHE-###.###.### MWST
# CHE-###.###.### TVA
# CHE-###.###.### IVA
#
# /!\ The english abbreviation VAT is not valid /!\
match = self.__check_vat_ch_re.match(vat)
if match:
# For new TVA numbers, the last digit is a MOD11 checksum digit build with weighting pattern: 5,4,3,2,7,6,5,4
num = [s for s in match.group(1) if s.isdigit()] # get the digits only
factor = (5, 4, 3, 2, 7, 6, 5, 4)
csum = sum([int(num[i]) * factor[i] for i in range(8)])
check = (11 - (csum % 11)) % 11
return check == int(num[8])
return False
def is_valid_ruc_ec(self, vat):
if len(vat) in (10, 13) and vat.isdecimal():
return True
return False
def check_vat_ec(self, vat):
vat = clean(vat, ' -.').upper().strip()
return self.is_valid_ruc_ec(vat)
def _ie_check_char(self, vat):
vat = vat.zfill(8)
extra = 0
if vat[7] not in ' W':
if vat[7].isalpha():
extra = 9 * (ord(vat[7]) - 64)
else:
# invalid
return -1
checksum = extra + sum((8-i) * int(x) for i, x in enumerate(vat[:7]))
return 'WABCDEFGHIJKLMNOPQRSTUV'[checksum % 23]
# TODO: remove in master
def check_vat_ie(self, vat):
return stdnum.util.get_cc_module('ie', 'vat').is_valid(vat)
# Mexican VAT verification, contributed by Vauxoo
# and Panos Christeas <p_christ@hol.gr>
__check_vat_mx_re = re.compile(r"(?P<primeras>[A-Za-z\xd1\xf1&]{3,4})"
r"[ \-_]?"
r"(?P<ano>[0-9]{2})(?P<mes>[01][0-9])(?P<dia>[0-3][0-9])"
r"[ \-_]?"
r"(?P<code>[A-Za-z0-9&\xd1\xf1]{3})")
def check_vat_mx(self, vat):
''' Mexican VAT verification
Verificar RFC México
'''
m = self.__check_vat_mx_re.fullmatch(vat)
if not m:
#No valid format
return False
ano = int(m['ano'])
if ano > 30:
ano = 1900 + ano
else:
ano = 2000 + ano
try:
datetime.date(ano, int(m['mes']), int(m['dia']))
except ValueError:
return False
# Valid format and valid date
return True
# Norway VAT validation, contributed by Rolv Råen (adEgo) <rora@adego.no>
# Support for MVA suffix contributed by Bringsvor Consulting AS (bringsvor@bringsvor.com)
def check_vat_no(self, vat):
"""
Check Norway VAT number.See http://www.brreg.no/english/coordination/number.html
"""
if len(vat) == 12 and vat.upper().endswith('MVA'):
vat = vat[:-3] # Strictly speaking we should enforce the suffix MVA but...
if len(vat) != 9:
return False
try:
int(vat)
except ValueError:
return False
sum = (3 * int(vat[0])) + (2 * int(vat[1])) + \
(7 * int(vat[2])) + (6 * int(vat[3])) + \
(5 * int(vat[4])) + (4 * int(vat[5])) + \
(3 * int(vat[6])) + (2 * int(vat[7]))
check = 11 - (sum % 11)
if check == 11:
check = 0
if check == 10:
# 10 is not a valid check digit for an organization number
return False
return check == int(vat[8])
# Peruvian VAT validation, contributed by Vauxoo
def check_vat_pe(self, vat):
if len(vat) != 11 or not vat.isdigit():
return False
dig_check = 11 - (sum([int('5432765432'[f]) * int(vat[f]) for f in range(0, 10)]) % 11)
if dig_check == 10:
dig_check = 0
elif dig_check == 11:
dig_check = 1
return int(vat[10]) == dig_check
# Philippines TIN (+ branch code) validation
__check_vat_ph_re = re.compile(r"\d{3}-\d{3}-\d{3}(-\d{3,5})?$")
def check_vat_ph(self, vat):
return len(vat) >= 11 and len(vat) <= 17 and self.__check_vat_ph_re.match(vat)
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
def check_vat_tr(self, vat):
return stdnum.util.get_cc_module('tr', 'tckimlik').is_valid(vat) or stdnum.util.get_cc_module('tr', 'vkn').is_valid(vat)
__check_vat_sa_re = re.compile(r"^3[0-9]{13}3$")
# Saudi Arabia TIN validation
def check_vat_sa(self, vat):
"""
Check company VAT TIN according to ZATCA specifications: The VAT number should start and begin with a '3'
and be 15 digits long
"""
return self.__check_vat_sa_re.match(vat) or False
def check_vat_ua(self, vat):
res = []
for partner in self:
if partner.commercial_partner_id.country_id.code == 'MX':
if len(vat) == 10:
res.append(True)
else:
res.append(False)
elif partner.commercial_partner_id.is_company:
if len(vat) == 12:
res.append(True)
else:
res.append(False)
else:
if len(vat) == 10 or len(vat) == 9:
res.append(True)
else:
res.append(False)
return all(res)
def check_vat_uy(self, vat):
""" Taken from python-stdnum's master branch, as the release doesn't handle RUT numbers starting with 22.
origin https://github.com/arthurdejong/python-stdnum/blob/master/stdnum/uy/rut.py
FIXME Can be removed when python-stdnum does a new release. """
def compact(number):
"""Convert the number to its minimal representation."""
number = clean(number, ' -').upper().strip()
if number.startswith('UY'):
return number[2:]
return number
def calc_check_digit(number):
"""Calculate the check digit."""
weights = (4, 3, 2, 9, 8, 7, 6, 5, 4, 3, 2)
total = sum(int(n) * w for w, n in zip(weights, number))
return str(-total % 11)
vat = compact(vat)
return (
vat.isdigit() # InvalidFormat
and len(vat) == 12 # InvalidLength
and '01' <= vat[:2] <= '22' # InvalidComponent
and vat[2:8] != '000000'
and vat[8:11] == '001'
and vat[-1] == calc_check_digit(vat) # Invalid Check Digit
)
def check_vat_ve(self, vat):
# https://tin-check.com/en/venezuela/
# https://techdocs.broadcom.com/us/en/symantec-security-software/information-security/data-loss-prevention/15-7/About-content-packs/What-s-included-in-Content-Pack-2021-02/Updated-data-identifiers-in-Content-Pack-2021-02/venezuela-national-identification-number-v115451096-d327e108002-CP2021-02.html
# Sources last visited on 2022-12-09
# VAT format: (kind - 1 letter)(identifier number - 8-digit number)(check digit - 1 digit)
vat_regex = re.compile(r"""
([vecjpg]) # group 1 - kind
(
(?P<optional_1>-)? # optional '-' (1)
[0-9]{2}
(?(optional_1)(?P<optional_2>[.])?) # optional '.' (2) only if (1)
[0-9]{3}
(?(optional_2)[.]) # mandatory '.' if (2)
[0-9]{3}
(?(optional_1)-) # mandatory '-' if (1)
) # group 2 - identifier number
([0-9]{1}) # group X - check digit
""", re.VERBOSE | re.IGNORECASE)
matches = re.fullmatch(vat_regex, vat)
if not matches:
return False
kind, identifier_number, *_, check_digit = matches.groups()
kind = kind.lower()
identifier_number = identifier_number.replace("-", "").replace(".", "")
check_digit = int(check_digit)
if kind == 'v': # Venezuela citizenship
kind_digit = 1
elif kind == 'e': # Foreigner
kind_digit = 2
elif kind == 'c' or kind == 'j': # Township/Communal Council or Legal entity
kind_digit = 3
elif kind == 'p': # Passport
kind_digit = 4
else: # Government ('g')
kind_digit = 5
# === Checksum validation ===
multipliers = [3, 2, 7, 6, 5, 4, 3, 2]
checksum = kind_digit * 4
checksum += sum(map(lambda n, m: int(n) * m, identifier_number, multipliers))
checksum_digit = 11 - checksum % 11
if checksum_digit > 9:
checksum_digit = 0
return check_digit == checksum_digit
def check_vat_in(self, vat):
#reference from https://www.gstzen.in/a/format-of-a-gst-number-gstin.html
if vat and len(vat) == 15:
all_gstin_re = [
r'[0-9]{2}[a-zA-Z]{5}[0-9]{4}[a-zA-Z]{1}[1-9A-Za-z]{1}[Zz1-9A-Ja-j]{1}[0-9a-zA-Z]{1}', # Normal, Composite, Casual GSTIN
r'[0-9]{4}[A-Z]{3}[0-9]{5}[UO]{1}[N][A-Z0-9]{1}', #UN/ON Body GSTIN
r'[0-9]{4}[a-zA-Z]{3}[0-9]{5}[N][R][0-9a-zA-Z]{1}', #NRI GSTIN
r'[0-9]{2}[a-zA-Z]{4}[a-zA-Z0-9]{1}[0-9]{4}[a-zA-Z]{1}[1-9A-Za-z]{1}[DK]{1}[0-9a-zA-Z]{1}', #TDS GSTIN
r'[0-9]{2}[a-zA-Z]{5}[0-9]{4}[a-zA-Z]{1}[1-9A-Za-z]{1}[C]{1}[0-9a-zA-Z]{1}' #TCS GSTIN
]
return any(re.compile(rx).match(vat) for rx in all_gstin_re)
return False
def check_vat_t(self, vat):
if self.country_id.code == 'JP':
return self.simple_vat_check('jp', vat)
def check_vat_br(self, vat):
is_cpf_valid = stdnum.get_cc_module('br', 'cpf').is_valid
is_cnpj_valid = stdnum.get_cc_module('br', 'cnpj').is_valid
return is_cpf_valid(vat) or is_cnpj_valid(vat)
__check_vat_cr_re = re.compile(r'^(?:[1-9]\d{8}|\d{10}|[1-9]\d{10,11})$')
def check_vat_cr(self, vat):
# CÉDULA FÍSICA: 9 digits
# CÉDULA JURÍDICA: 10 digits
# CÉDULA DIMEX: 11 or 12 digits
# CÉDULA NITE: 10 digits
return self.__check_vat_cr_re.match(vat) or False
def format_vat_eu(self, vat):
# Foreign companies that trade with non-enterprises in the EU
# may have a VATIN starting with "EU" instead of a country code.
return vat
def format_vat_ch(self, vat):
stdnum_vat_format = getattr(stdnum.util.get_cc_module('ch', 'vat'), 'format', None)
return stdnum_vat_format('CH' + vat)[2:] if stdnum_vat_format else vat
def check_vat_id(self, vat):
""" Temporary Indonesian VAT validation to support the new format
introduced in January 2024."""
vat = clean(vat, ' -.').strip()
if len(vat) not in (15, 16) or not vat[0:15].isdecimal() or not vat[-1].isdecimal():
return False
# VAT is only digits and of the right length, check the Luhn checksum.
try:
luhn.validate(vat[0:9] if len(vat) == 15 else vat[1:10])
except (InvalidFormat, InvalidChecksum):
return False
return True
def check_vat_de(self, vat):
is_valid_vat = stdnum.util.get_cc_module("de", "vat").is_valid
is_valid_stnr = stdnum.util.get_cc_module("de", "stnr").is_valid
return is_valid_vat(vat) or is_valid_stnr(vat)
def check_vat_il(self, vat):
check_func = stdnum.util.get_cc_module('il', 'idnr').is_valid
return check_func(vat)
def format_vat_sm(self, vat):
stdnum_vat_format = stdnum.util.get_cc_module('sm', 'vat').compact
return stdnum_vat_format('SM' + vat)[2:]
def _fix_vat_number(self, vat, country_id):
code = self.env['res.country'].browse(country_id).code if country_id else False
vat_country, vat_number = self._split_vat(vat)
if code and code.lower() != vat_country:
return vat
stdnum_vat_fix_func = getattr(stdnum.util.get_cc_module(vat_country, 'vat'), 'compact', None)
#If any localization module need to define vat fix method for it's country then we give first priority to it.
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
@api.model
def _convert_hu_local_to_eu_vat(self, local_vat):
if self.__check_tin_hu_companies_re.match(local_vat):
return f'HU{local_vat[:8]}'
return False
@api.model_create_multi
def create(self, vals_list):
for values in vals_list:
if values.get('vat'):
country_id = values.get('country_id')
values['vat'] = self._fix_vat_number(values['vat'], country_id)
res = super().create(vals_list)
if self.env.context.get('import_file'):
res.env.remove_to_compute(self._fields['vies_valid'], res)
return res
def write(self, values):
if values.get('vat') and len(self.mapped('country_id')) == 1:
country_id = values.get('country_id', self.country_id.id)
values['vat'] = self._fix_vat_number(values['vat'], country_id)
res = super().write(values)
if self.env.context.get('import_file'):
self.env.remove_to_compute(self._fields['vies_valid'], self)
return res