# -*- coding: utf-8 -*-
from __future__ import unicode_literals
import re
import string
from django.core.exceptions import ImproperlyConfigured, ValidationError
from django.utils.deconstruct import deconstructible
from django.utils.translation import ugettext_lazy as _
from . import checksums
from .countries.iso_3166 import ISO_3166_1_ALPHA2_COUNTRY_CODES
# Dictionary of ISO country code to IBAN length.
#
# The official IBAN Registry document is the best source for up-to-date information about IBAN formats and which
# countries are in IBAN.
#
# https://www.swift.com/standards/data-standards/iban
#
# The IBAN_COUNTRY_CODE_LENGTH dictionary has been updated version 64 of the IBAN Registry document which was published
# in March 2016.
#
# Other Resources:
#
# https://en.wikipedia.org/wiki/International_Bank_Account_Number#IBAN_formats_by_country
# http://www.ecbs.org/iban/france-bank-account-number.html
# https://www.nordea.com/V%C3%A5ra+tj%C3%A4nster/Internationella+produkter+och+tj%C3%A4nster/Cash+Management/IBAN+countries/908472.html
IBAN_COUNTRY_CODE_LENGTH = {'AL': 28, # Albania
'AD': 24, # Andorra
'AE': 23, # United Arab Emirates
'AT': 20, # Austria
'AZ': 28, # Azerbaijan
'BA': 20, # Bosnia and Herzegovina
'BE': 16, # Belgium
'BG': 22, # Bulgaria
'BH': 22, # Bahrain
'BR': 29, # Brazil
'CH': 21, # Switzerland
'CR': 21, # Costa Rica
'CY': 28, # Cyprus
'CZ': 24, # Czech Republic
'DE': 22, # Germany
'DK': 18, # Denmark
'DO': 28, # Dominican Republic
'EE': 20, # Estonia
'ES': 24, # Spain
'FI': 18, # Finland
'FO': 18, # Faroe Islands
'FR': 27, # France + Central African Republic, French Guiana, French Polynesia, Guadeloupe,
# Martinique, RĂ©union, Saint-Pierre and Miquelon, New Caledonia,
# Wallis and Futuna
'GB': 22, # United Kingdom + Guernsey, Isle of Man, Jersey
'GE': 22, # Georgia
'GI': 23, # Gibraltar
'GL': 18, # Greenland
'GR': 27, # Greece
'GT': 28, # Guatemala
'HR': 21, # Croatia
'HU': 28, # Hungary
'IE': 22, # Ireland
'IL': 23, # Israel
'IS': 26, # Iceland
'IT': 27, # Italy
'JO': 30, # Jordan
'KZ': 20, # Kazakhstan
'KW': 30, # Kuwait
'LB': 28, # Lebanon
'LC': 32, # Saint Lucia
'LI': 21, # Liechtenstein
'LT': 20, # Lithuania
'LU': 20, # Luxembourg
'LV': 21, # Latvia
'MC': 27, # Monaco
'MD': 24, # Moldova
'ME': 22, # Montenegro
'MK': 19, # Macedonia
'MT': 31, # Malta
'MR': 27, # Mauritania
'MU': 30, # Mauritius
'NL': 18, # Netherlands
'NO': 15, # Norway
'PS': 29, # Palestine
'PK': 24, # Pakistan
'PL': 28, # Poland
'PT': 25, # Portugal + Sao Tome and Principe
'QA': 29, # Qatar
'RO': 24, # Romania
'RS': 22, # Serbia
'SA': 24, # Saudi Arabia
'SC': 31, # Seychelles
'SE': 24, # Sweden
'SI': 19, # Slovenia
'SK': 24, # Slovakia
'SM': 27, # San Marino
'ST': 25, # Sao Tome And Principe
'TL': 23, # Timor-Leste
'TN': 24, # Tunisia
'TR': 26, # Turkey
'UA': 29, # Ukraine
'VG': 24, # British Virgin Islands
'XK': 20} # Republic of Kosovo (user-assigned country code)
# Nordea has catalogued IBANs for some additional countries but they are not part of the office IBAN network yet.
#
# Reference:
# https://www.nordea.com/V%C3%A5ra+tj%C3%A4nster/Internationella+produkter+och+tj%C3%A4nster/Cash+Management/IBAN+countries/908472.html
NORDEA_COUNTRY_CODE_LENGTH = {'AO': 25, # Angola
'BJ': 28, # Benin
'BF': 27, # Burkina Faso
'BI': 16, # Burundi
'CI': 28, # Ivory Coast
'CG': 27, # Congo
'CM': 27, # Cameroon
'CV': 25, # Cape Verde
'DZ': 24, # Algeria
'EG': 27, # Egypt
'GA': 27, # Gabon
'IR': 26, # Iran
'MG': 27, # Madagascar
'ML': 28, # Mali
'MZ': 25, # Mozambique
'SN': 28} # Senegal
@deconstructible
[docs]class IBANValidator(object):
""" A validator for International Bank Account Numbers (IBAN - ISO 13616-1:2007). """
def __init__(self, use_nordea_extensions=False, include_countries=None):
self.use_nordea_extensions = use_nordea_extensions
self.include_countries = include_countries
self.validation_countries = IBAN_COUNTRY_CODE_LENGTH.copy()
if self.use_nordea_extensions:
self.validation_countries.update(NORDEA_COUNTRY_CODE_LENGTH)
if self.include_countries:
for country_code in self.include_countries:
if country_code not in self.validation_countries:
msg = 'Explicitly requested country code %s is not part of the configured IBAN validation set.' % country_code
raise ImproperlyConfigured(msg)
def __eq__(self, other):
return (self.use_nordea_extensions == other.use_nordea_extensions and
self.include_countries == other.include_countries)
@staticmethod
[docs] def iban_checksum(value):
""" Returns check digits for an input IBAN number. Original checksum in input value is ignored. """
# 1. Move the two initial characters to the end of the string, replacing checksum for '00'
value = value[4:] + value[:2] + '00'
# 2. Replace each letter in the string with two digits, thereby expanding the string, where
# A = 10, B = 11, ..., Z = 35.
value_digits = ''
for x in value:
if '0' <= x <= '9':
value_digits += x
elif 'A' <= x <= 'Z':
value_digits += str(ord(x) - 55)
else:
raise ValidationError(_('%s is not a valid character for IBAN.') % x)
# 3. The remainder of the number above when divided by 97 is then subtracted from 98.
return '%02d' % (98 - int(value_digits) % 97)
def __call__(self, value):
"""
Validates the IBAN value using the official IBAN validation algorithm.
https://en.wikipedia.org/wiki/International_Bank_Account_Number#Validating_the_IBAN
"""
if value is None:
return value
value = value.upper().replace(' ', '').replace('-', '')
# Check that the total IBAN length is correct as per the country. If not, the IBAN is invalid.
country_code = value[:2]
if country_code in self.validation_countries:
if self.validation_countries[country_code] != len(value):
msg_params = {'country_code': country_code, 'number': self.validation_countries[country_code]}
raise ValidationError(_('%(country_code)s IBANs must contain %(number)s characters.') % msg_params)
else:
raise ValidationError(_('%s is not a valid country code for IBAN.') % country_code)
if self.include_countries and country_code not in self.include_countries:
raise ValidationError(_('%s IBANs are not allowed in this field.') % country_code)
if self.iban_checksum(value) != value[2:4]:
raise ValidationError(_('Not a valid IBAN.'))
@deconstructible
[docs]class BICValidator(object):
"""
A validator for SWIFT Business Identifier Codes (ISO 9362:2009). Validation is based on the BIC structure found on
wikipedia.
https://en.wikipedia.org/wiki/ISO_9362#Structure
"""
def __eq__(self, other):
# The is no outside modification of properties so this should always be true by default.
return True
def __call__(self, value):
if value is None:
return value
value = value.upper()
# Length is 8 or 11.
bic_length = len(value)
if bic_length != 8 and bic_length != 11:
raise ValidationError(_('BIC codes have either 8 or 11 characters.'))
# First 4 letters are A - Z.
institution_code = value[:4]
for x in institution_code:
if x not in string.ascii_uppercase:
raise ValidationError(_('%s is not a valid institution code.') % institution_code)
# Letters 5 and 6 consist of an ISO 3166-1 alpha-2 country code.
country_code = value[4:6]
if country_code not in ISO_3166_1_ALPHA2_COUNTRY_CODES:
raise ValidationError(_('%s is not a valid country code.') % country_code)
@deconstructible
[docs]class EANValidator(object):
"""
A generic validator for EAN like codes with the last digit being the checksum.
http://en.wikipedia.org/wiki/International_Article_Number_(EAN)
"""
message = _('Not a valid EAN code.')
def __init__(self, strip_nondigits=False, message=None):
if message is not None:
self.message = message
self.strip_nondigits = strip_nondigits
def __eq__(self, other):
return ((not hasattr(self, 'message') or self.message == other.message) and
self.strip_nondigits == other.strip_nondigits)
def __call__(self, value):
if value is None:
return value
if self.strip_nondigits:
value = re.compile(r'[^\d]+').sub('', value)
if not checksums.ean(value):
raise ValidationError(self.message, code='invalid')