Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Australian Company Number validator #278

Merged
merged 4 commits into from
Mar 10, 2017
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 @@ -4,7 +4,7 @@
from localflavor.generic.models import DeprecatedPhoneNumberField
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 @@ -91,6 +91,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()
53 changes: 52 additions & 1 deletion 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 @@ -195,6 +196,56 @@ def test_raises_error_for_invalid_abn(self):
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)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It would be nice to be consistent on the spacing. For instance, some tests have a space between each statement while one has no spaces. The space don't seem to be needed because it's pretty simple code. Feel free to fix the previous tests as well where this was copied from.


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 valdation is run.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

valdation -> validation

I see that's also in the copy & pasted code so maybe you can also fix the typo there as well.

As this comment points out, the whitespace is stripped in the form. It would be nice to have a test for the form as well - at least testing the expected functionality that isn't tested with the validation tests.

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