Skip to content

Commit

Permalink
Add Australian Company Number validator (#278)
Browse files Browse the repository at this point in the history
  • Loading branch information
koterpillar authored and benkonrath committed Mar 10, 2017
1 parent d1f9d15 commit 76c1a0e
Show file tree
Hide file tree
Showing 7 changed files with 186 additions and 16 deletions.
1 change: 1 addition & 0 deletions docs/authors.rst
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ Authors
* Alex Gaynor
* Alex Hill
* Alex Zhang
* Alexey Kotlyarov
* Alix Martineau
* Alonisser
* Andreas Pelme
Expand Down
2 changes: 2 additions & 0 deletions docs/changelog.rst
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ New fields for existing flavors:

- Added NOBankAccountNumber form field.
(`gh-275 <https://github.com/django/django-localflavor/pull/275>`_)
- Added AUCompanyNumberField model and form field.
(`gh-278 <https://github.com/django/django-localflavor/pull/278>`_)

Modifications to existing flavors:

Expand Down
24 changes: 23 additions & 1 deletion localflavor/au/forms.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
from localflavor.generic.forms import DeprecatedPhoneNumberFormFieldMixin

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

PHONE_DIGITS_RE = re.compile(r'^(\d{10})$')

Expand Down Expand Up @@ -88,6 +88,28 @@ def prepare_value(self, value):
return '{} {} {} {}'.format(spaceless[:2], spaceless[2:5], spaceless[5:8], spaceless[8:])


class AUCompanyNumberField(CharField):
"""
A form field that validates input as an Australian Company Number (ACN).
.. versionadded:: 1.5
"""

default_validators = [AUCompanyNumberFieldValidator()]

def to_python(self, value):
value = super(AUCompanyNumberField, self).to_python(value)
return value.upper().replace(' ', '')

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:])


class AUTaxFileNumberField(CharField):
"""
A form field that validates input as an Australian Tax File Number (TFN).
Expand Down
32 changes: 31 additions & 1 deletion localflavor/au/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@

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


class AUStateField(CharField):
Expand Down Expand Up @@ -92,6 +92,36 @@ def to_python(self, value):
return value


class AUCompanyNumberField(CharField):
"""
A model field that checks that the value is a valid Australian Company Number (ACN).
.. versionadded:: 1.5
"""

description = _("Australian Company Number")

validators = [AUCompanyNumberFieldValidator()]

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

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

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

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

return value


class AUTaxFileNumberField(CharField):
"""
A model field that checks that the value is a valid Tax File Number (TFN).
Expand Down
43 changes: 43 additions & 0 deletions localflavor/au/validators.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,49 @@ def __call__(self, value):
raise ValidationError(self.error_message)


class AUCompanyNumberFieldValidator(RegexValidator):
"""
Validation for Australian Company Numbers.
.. versionadded:: 1.5
"""

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

def __init__(self):
nine_digits = '^\d{9}$'
super(AUCompanyNumberFieldValidator, self).__init__(
regex=nine_digits, message=self.error_message)

def _is_valid(self, value):
"""
Return whether the given value is a valid ACN.
See http://www.clearwater.com.au/code/acn for a description of the
validation algorithm.
"""
digits = [int(i) for i in list(value)]

# 1. Multiply each digit by its weighting factor.
weighting_factors = [8, 7, 6, 5, 4, 3, 2, 1]
weighted = [digit * weight for digit, weight in zip(digits, weighting_factors)]

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

# 4. Calculate the check digit
check = (10 - total % 10) % 10

# 5. Check against the last digit
return check == digits[8]

def __call__(self, value):
super(AUCompanyNumberFieldValidator, self).__call__(value)
if not self._is_valid(value):
raise ValidationError(self.error_message)


class AUTaxFileNumberFieldValidator(RegexValidator):
"""
Validation for Australian Tax File Numbers.
Expand Down
5 changes: 3 additions & 2 deletions tests/test_au/models.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
from django.db import models

from localflavor.au.models import (AUBusinessNumberField, AUPhoneNumberField, AUPostCodeField, AUStateField,
AUTaxFileNumberField)
from localflavor.au.models import (AUBusinessNumberField, AUCompanyNumberField, AUPhoneNumberField, AUPostCodeField,
AUStateField, AUTaxFileNumberField)


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

from localflavor.au import forms, models
from localflavor.au.validators import AUBusinessNumberFieldValidator, AUTaxFileNumberFieldValidator
from localflavor.au.validators import (AUBusinessNumberFieldValidator, AUCompanyNumberFieldValidator,
AUTaxFileNumberFieldValidator)

from .forms import AustralianPlaceForm
from .models import AustralianPlace
Expand Down Expand Up @@ -132,6 +133,18 @@ def test_abn(self):
}
self.assertFieldOutput(forms.AUBusinessNumberField, valid, invalid)

def test_acn(self):
error_format = ['Enter a valid ACN.']
valid = {
'604327504': '604327504',
'604 327 504': '604327504',
}
invalid = {
'604327505': error_format,
'60A327504': error_format,
}
self.assertFieldOutput(forms.AUCompanyNumberField, valid, invalid)

def test_tfn(self):
error_format = ['Enter a valid TFN.']
valid = {
Expand All @@ -149,52 +162,94 @@ class AULocalFlavorAUBusinessNumberFieldValidatorTests(TestCase):

def test_no_error_for_a_valid_abn(self):
"""Test a valid ABN does not cause an error."""
valid_abn = '53004085616'

valid_abn = '53004085616'
validator = AUBusinessNumberFieldValidator()

validator(valid_abn)

def test_raises_error_for_abn_containing_a_letter(self):
"""Test an ABN containing a letter is invalid."""
invalid_abn = '5300408561A'

invalid_abn = '5300408561A'
validator = AUBusinessNumberFieldValidator()

self.assertRaises(ValidationError, lambda: validator(invalid_abn))

def test_raises_error_for_too_short_abn(self):
"""Test an ABN with fewer than eleven digits is invalid."""
invalid_abn = '5300408561'

invalid_abn = '5300408561'
validator = AUBusinessNumberFieldValidator()

self.assertRaises(ValidationError, lambda: validator(invalid_abn))

def test_raises_error_for_too_long_abn(self):
"""Test an ABN with more than eleven digits is invalid."""

invalid_abn = '530040856160'
validator = AUBusinessNumberFieldValidator()
self.assertRaises(ValidationError, lambda: validator(invalid_abn))

def test_raises_error_for_whitespace(self):
"""Test an ABN can be valid when it contains whitespace."""
# NB: Form field should strip the whitespace before regex valdation is run.
invalid_abn = '5300 4085 616'

# NB: Form field should strip the whitespace before regex validation is run.
invalid_abn = '5300 4085 616'
validator = AUBusinessNumberFieldValidator()

self.assertRaises(ValidationError, lambda: validator(invalid_abn))

def test_raises_error_for_invalid_abn(self):
"""Test that an ABN must pass the ATO's validation algorithm."""
invalid_abn = '53004085617'

invalid_abn = '53004085617'
validator = AUBusinessNumberFieldValidator()

self.assertRaises(ValidationError, lambda: validator(invalid_abn))


class AULocalFlavorAUCompanyNumberFieldValidatorTests(TestCase):

def test_no_error_for_a_valid_acn(self):
"""Test a valid ACN does not cause an error."""

valid_acn = '604327504'
validator = AUCompanyNumberFieldValidator()
validator(valid_acn)

def test_raises_error_for_acn_containing_a_letter(self):
"""Test an ACN containing a letter is invalid."""

invalid_acn = '60432750A'
validator = AUCompanyNumberFieldValidator()
self.assertRaises(ValidationError, lambda: validator(invalid_acn))

def test_raises_error_for_too_short_acn(self):
"""Test an ACN with fewer than nine digits is invalid."""

invalid_acn = '60432750'
validator = AUCompanyNumberFieldValidator()
self.assertRaises(ValidationError, lambda: validator(invalid_acn))

def test_raises_error_for_too_long_acn(self):
"""Test an ACN with more than nine digits is invalid."""

invalid_acn = '6043275040'
validator = AUCompanyNumberFieldValidator()
self.assertRaises(ValidationError, lambda: validator(invalid_acn))

def test_raises_error_for_whitespace(self):
"""Test an ACN can be valid when it contains whitespace."""

# NB: Form field should strip the whitespace before regex validation is run.
invalid_acn = '604 327 504'
validator = AUCompanyNumberFieldValidator()
self.assertRaises(ValidationError, lambda: validator(invalid_acn))

def test_raises_error_for_invalid_acn(self):
"""Test that an ACN must pass the ATO's validation algorithm."""

invalid_acn = '604327509'
validator = AUCompanyNumberFieldValidator()
self.assertRaises(ValidationError, lambda: validator(invalid_acn))


class AULocalFlavorAUTaxFileNumberFieldValidatorTests(TestCase):

def test_no_error_for_a_valid_tfn(self):
Expand Down Expand Up @@ -267,6 +322,22 @@ def test_spaces_are_reconfigured(self):
self.assertEqual('53 004 085 616', field.prepare_value('53 0 04 08561 6'))


class AULocalFlavourAUCompanyNumberFormFieldTests(TestCase):

def test_abn_with_spaces_remains_unchanged(self):
"""Test that an ACN with the formatting we expect is unchanged."""
field = forms.AUCompanyNumberField()

self.assertEqual('604 327 504', field.prepare_value('604 327 504'))

def test_spaces_are_reconfigured(self):
"""Test that an ACN with formatting we don't expect is transformed."""
field = forms.AUCompanyNumberField()

self.assertEqual('604 327 504', field.prepare_value('604327504'))
self.assertEqual('604 327 504', field.prepare_value('60 4 32750 4'))


class AULocalFlavourAUTaxFileNumberFormFieldTests(TestCase):

def test_tfn_with_spaces_remains_unchanged(self):
Expand Down

0 comments on commit 76c1a0e

Please sign in to comment.