Skip to content

Commit

Permalink
Added AUTaxFileNumberField
Browse files Browse the repository at this point in the history
  • Loading branch information
FrancoisConstant authored and benkonrath committed Jul 8, 2016
1 parent 81ea00a commit fd9fcf1
Show file tree
Hide file tree
Showing 8 changed files with 199 additions and 6 deletions.
1 change: 1 addition & 0 deletions docs/authors.rst
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ Authors
* Erik Romijn
* Flavio Curella
* Florian Apolloner
* François Constant
* Gary Wilson Jr
* Gerardo Orozco
* Ghassen Telmoudi
Expand Down
3 changes: 3 additions & 0 deletions docs/changelog.rst
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,9 @@ New fields for existing flavors:
- Added MXCLABEField model and form fields.
(`gh-227 <https://github.com/django/django-localflavor/pull/227>`_).

- Added AUTaxFileNumberField model and form fields.
(`gh-238 <https://github.com/django/django-localflavor/pull/238>`_)

Modifications to existing flavors:

- Fixed century bug with Kuwait Civil ID verification localflavor.kw.forms
Expand Down
22 changes: 21 additions & 1 deletion localflavor/au/forms.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
from django.utils.translation import ugettext_lazy as _

from .au_states import STATE_CHOICES
from .validators import AUBusinessNumberFieldValidator
from .validators import AUBusinessNumberFieldValidator, AUTaxFileNumberFieldValidator


PHONE_DIGITS_RE = re.compile(r'^(\d{10})$')
Expand Down Expand Up @@ -85,3 +85,23 @@ def prepare_value(self, value):

spaceless = ''.join(value.split())
return '{} {} {} {}'.format(spaceless[:2], spaceless[2:5], spaceless[5:8], spaceless[8:])


class AUTaxFileNumberField(CharField):
"""
A form field that validates input as an Australian Tax File Number (TFN)
.. versionadded:: 1.4
"""

default_validators = [AUTaxFileNumberFieldValidator()]

def prepare_value(self, value):
"""
Format the value for display.
"""
if value is None:
return value

spaceless = ''.join(value.split())
return '{} {} {}'.format(spaceless[:3], spaceless[3:6], spaceless[6:])
43 changes: 42 additions & 1 deletion localflavor/au/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@

from . import forms
from .au_states import STATE_CHOICES
from .validators import AUBusinessNumberFieldValidator
from .validators import AUBusinessNumberFieldValidator, AUTaxFileNumberFieldValidator


class AUStateField(CharField):
Expand Down Expand Up @@ -107,3 +107,44 @@ def to_python(self, value):
return ''.join(value.split())

return value


class AUTaxFileNumberField(CharField):
"""
A model field that checks that the value is a valid Tax File Number (TFN).
A TFN is a number issued to a person by the Commissioner of Taxation and
is used to verify client identity and establish their income levels.
It is a eight or nine digit number without any embedded meaning.
.. versionadded:: 1.4
"""

description = _("Australian Tax File Number")

validators = [AUTaxFileNumberFieldValidator()]

def __init__(self, *args, **kwargs):
kwargs['max_length'] = 11
super(AUTaxFileNumberField, self).__init__(*args, **kwargs)

def deconstruct(self):
name, path, args, kwargs = super(AUTaxFileNumberField, self).deconstruct()
del kwargs['max_length']
return name, path, args, kwargs

def formfield(self, **kwargs):
defaults = {'form_class': forms.AUTaxFileNumberField}
defaults.update(kwargs)
return super(AUTaxFileNumberField, self).formfield(**defaults)

def to_python(self, value):
"""
Ensure the TFN is stored without spaces.
"""
value = super(AUTaxFileNumberField, self).to_python(value)

if value is not None:
return ''.join(value.split())

return value
43 changes: 43 additions & 0 deletions localflavor/au/validators.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,3 +49,46 @@ def __call__(self, value):
super(AUBusinessNumberFieldValidator, self).__call__(value)
if not self._is_valid(value):
raise ValidationError(self.error_message)


class AUTaxFileNumberFieldValidator(RegexValidator):
"""
Validation for Australian Tax File Numbers.
.. versionadded:: 1.4
"""

error_message = _('Enter a valid TFN.')

def __init__(self):
""" Regex for 8 to 9 digits """
super(AUTaxFileNumberFieldValidator, self).__init__(
regex='^\d{8,9}$', message=self.error_message)

def _is_valid(self, value):
"""
Return whether the given value is a valid TFN.
See http://www.mathgen.ch/codes/tfn.html for a description of the
validation algorithm.
"""
# 1. Multiply each digit by its weighting factor.
digits = [int(i) for i in list(value)]
WEIGHTING_FACTORS = [1, 4, 3, 7, 5, 8, 6, 9, 10]
weighted = [digit * weight for digit, weight in zip(digits, WEIGHTING_FACTORS)]

# 2. Sum the resulting values.
total = sum(weighted)

# 3. Divide the total by 11, noting the remainder.
remainder = total % 11

# 4. If the remainder is zero, then it's a valid TFN.
return remainder == 0

def __call__(self, value):
value = value.replace(' ', '')
super(AUTaxFileNumberFieldValidator, self).__call__(value)
if not self._is_valid(value):
raise ValidationError(self.error_message)
2 changes: 1 addition & 1 deletion tests/test_au/forms.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,4 +9,4 @@ class Meta:
model = AustralianPlace
fields = ('state', 'state_required', 'state_default', 'postcode',
'postcode_required', 'postcode_default',
'phone', 'name', 'abn')
'phone', 'name', 'abn', 'tfn')
4 changes: 3 additions & 1 deletion tests/test_au/models.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
from django.db import models
from localflavor.au.models import AUBusinessNumberField, AUPhoneNumberField, AUPostCodeField, AUStateField
from localflavor.au.models import (AUBusinessNumberField, AUPhoneNumberField, AUPostCodeField, AUStateField,
AUTaxFileNumberField)


class AustralianPlace(models.Model):
Expand All @@ -12,3 +13,4 @@ class AustralianPlace(models.Model):
phone = AUPhoneNumberField(blank=True)
name = models.CharField(max_length=20)
abn = AUBusinessNumberField()
tfn = AUTaxFileNumberField()
87 changes: 85 additions & 2 deletions tests/test_au/tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
from django.test import TestCase

from localflavor.au import forms, models
from localflavor.au.validators import AUBusinessNumberFieldValidator
from localflavor.au.validators import AUBusinessNumberFieldValidator, AUTaxFileNumberFieldValidator

from .forms import AustralianPlaceForm
from .models import AustralianPlace
Expand All @@ -26,6 +26,7 @@ def setUp(self):
'postcode': '1234',
'postcode_required': '4321',
'abn': '74457506140',
'tfn': '123456782'
})

def test_get_display_methods(self):
Expand All @@ -49,13 +50,15 @@ def test_required(self):
self.assertEqual(set(form.errors.keys()),
set(('state_required',
'postcode_required',
'abn')))
'abn', 'tfn')))
self.assertEqual(
form.errors['state_required'], ['This field is required.'])
self.assertEqual(
form.errors['postcode_required'], ['This field is required.'])
self.assertEqual(
form.errors['abn'], ['This field is required.'])
self.assertEqual(
form.errors['tfn'], ['This field is required.'])

def test_field_blank_option(self):
""" Test that the empty option is there. """
Expand Down Expand Up @@ -128,6 +131,18 @@ def test_abn(self):
}
self.assertFieldOutput(forms.AUBusinessNumberField, valid, invalid)

def test_tfn(self):
error_format = ['Enter a valid TFN.']
valid = {
'123456782': '123456782',
'123 456 782': '123 456 782'
}
invalid = {
'123456789': error_format, # wrong number
'12345678B': error_format, # letter at the end
}
self.assertFieldOutput(forms.AUTaxFileNumberField, valid, invalid)


class AULocalFlavorAUBusinessNumberFieldValidatorTests(TestCase):

Expand Down Expand Up @@ -179,6 +194,45 @@ def test_raises_error_for_invalid_abn(self):
self.assertRaises(ValidationError, lambda: validator(invalid_abn))


class AULocalFlavorAUTaxFileNumberFieldValidatorTests(TestCase):

def test_no_error_for_a_valid_tfn(self):
"""Test a valid TFN does not cause an error."""
valid_tfn = '123456782'
validator = AUTaxFileNumberFieldValidator()
validator(valid_tfn)

def test_no_error_for_valid_tfn_with_whitespace(self):
"""Test a TFN can be valid when it contains whitespace."""
valid_tfn = '123 456 782'
validator = AUTaxFileNumberFieldValidator()
validator(valid_tfn)

def test_raises_error_for_tfn_containing_a_letter(self):
"""Test an TFN containing a letter is invalid."""
invalid_tfn = '12345678W'
validator = AUTaxFileNumberFieldValidator()
self.assertRaises(ValidationError, lambda: validator(invalid_tfn))

def test_raises_error_for_too_short_tfn(self):
"""Test a TFN with fewer than 8 digits is invalid."""
invalid_tfn = '1234567'
validator = AUTaxFileNumberFieldValidator()
self.assertRaises(ValidationError, lambda: validator(invalid_tfn))

def test_raises_error_for_too_long_tfn(self):
"""Test a TFN with more than 9 digits is invalid."""
invalid_tfn = '1234567890'
validator = AUTaxFileNumberFieldValidator()
self.assertRaises(ValidationError, lambda: validator(invalid_tfn))

def test_raises_error_for_invalid_tfn(self):
"""Test that a TFN must pass the ATO's validation algorithm."""
invalid_tfn = '123456783'
validator = AUTaxFileNumberFieldValidator()
self.assertRaises(ValidationError, lambda: validator(invalid_tfn))


class AULocalFlavorAUBusinessNumberModelTests(TestCase):

def test_AUBusinessNumberModel_invalid_abn_raises_error(self):
Expand All @@ -190,6 +244,7 @@ def test_AUBusinessNumberModel_invalid_abn_raises_error(self):
'postcode': '1234',
'postcode_required': '4321',
'abn': '5300 4085 616 INVALID',
'tfn': '123456782'
})

self.assertRaises(ValidationError, place.clean_fields)
Expand All @@ -213,6 +268,24 @@ def test_spaces_are_reconfigured(self):
self.assertEqual('53 004 085 616', field.prepare_value('53 0 04 08561 6'))


class AULocalFlavourAUTaxFileNumberFormFieldTests(TestCase):

def test_tfn_with_spaces_remains_unchanged(self):
"""Test that a TFN with the formatting we expect is unchanged."""

field = forms.AUTaxFileNumberField()

self.assertEqual('123 456 782', field.prepare_value('123 456 782'))

def test_spaces_are_reconfigured(self):
"""Test that a TFN with formatting we don't expect is transformed."""

field = forms.AUTaxFileNumberField()

self.assertEqual('123 456 782', field.prepare_value('123456782'))
self.assertEqual('123 456 782', field.prepare_value('12 345 678 2'))


class AULocalFlavourAUBusinessNumberModelFieldTests(TestCase):

def test_to_python_strips_whitespace(self):
Expand All @@ -221,3 +294,13 @@ def test_to_python_strips_whitespace(self):
field = models.AUBusinessNumberField()

self.assertEqual('53004085616', field.to_python('53 004 085 616'))


class AULocalFlavourAUTaxFileNumberModelFieldTests(TestCase):

def test_to_python_strips_whitespace(self):
"""Test the value is stored without whitespace."""

field = models.AUTaxFileNumberField()

self.assertEqual('123456782', field.to_python('123 456 782'))

0 comments on commit fd9fcf1

Please sign in to comment.