Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
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 @@ -115,6 +115,7 @@ Authors
* Rael Max
* Ramiro Morales
* Raphael Michel
* Renne Rocha
* Rolf Erik Lekang
* Russell Keith-Magee
* Santosh Bhattarai
Expand Down
2 changes: 2 additions & 0 deletions docs/changelog.rst
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ Modifications to existing flavors:
(`gh-529 <https://github.com/django/django-localflavor/pull/529>`_).
- Update SI postal codes
(`gh-531 <https://github.com/django/django-localflavor/pull/531>`_).
- Update BR CNPJ validator to accept new alphanumeric format
(`gh-533 <https://github.com/django/django-localflavor/pull/533>`_)

Other changes:

Expand Down
17 changes: 4 additions & 13 deletions localflavor/br/forms.py
Original file line number Diff line number Diff line change
Expand Up @@ -88,15 +88,7 @@ class BRCNPJField(CharField):
A form field that validates input as `Brazilian CNPJ`_.

Input can either be of the format XX.XXX.XXX/XXXX-XX or be a group of 14
digits.

If you want to use the long format only, you can specify:
brcnpj_field = BRCNPJField(min_length=16)

If you want to use the short format, you can specify:
brcnpj_field = BRCNPJField(max_length=14)

Otherwise both formats will be valid.
digits or upper case letters.

.. _Brazilian CNPJ: http://en.wikipedia.org/wiki/National_identification_number#Brazil
.. versionchanged:: 1.4
Expand All @@ -106,12 +98,11 @@ class BRCNPJField(CharField):
"""

default_error_messages = {
'invalid': _("Invalid CNPJ number."),
'max_digits': _("This field requires at least 14 digits"),
"invalid": _("Invalid CNPJ number."),
}

def __init__(self, min_length=14, max_length=18, **kwargs):
super().__init__(max_length=max_length, min_length=min_length, **kwargs)
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.validators.append(BRCNPJValidator())


Expand Down
51 changes: 31 additions & 20 deletions localflavor/br/validators.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import itertools
import re

from django.core.exceptions import ValidationError
from django.core.validators import RegexValidator
from django.utils.translation import gettext_lazy as _

postal_code_re = re.compile(r'^\d{5}-\d{3}$')
cnpj_digits_re = re.compile(r'^(\d{2})[.-]?(\d{3})[.-]?(\d{3})/(\d{4})-(\d{2})$')
cpf_digits_re = re.compile(r'^(\d{3})\.(\d{3})\.(\d{3})-(\d{2})$')


Expand Down Expand Up @@ -34,35 +34,46 @@ class BRCNPJValidator(RegexValidator):
.. versionadded:: 2.2
"""

CNPJ_RE = re.compile(r'^([A-Z0-9]{2}).?([A-Z0-9]{3}).?([A-Z0-9]{3})\/?([A-Z0-9]{4})-?(\d{2})$')

def __init__(self, *args, **kwargs):
super().__init__(
*args,
regex=cnpj_digits_re,
regex=self.CNPJ_RE,
message=_("Invalid CNPJ number."),
**kwargs
)

def __call__(self, value):
orig_dv = value[-2:]
def _get_check_digit(self, cnpj):
'''
Based on official documentation at:
https://www.gov.br/receitafederal/pt-br/centrais-de-conteudo/publicacoes/documentos-tecnicos/cnpj
'''
def _get_digit(value):
values = [ord(c) - 48 for c in value][::-1]
remainder = (
sum(
[x * y for x, y in list(zip(values, itertools.cycle(range(2, 10))))]
)
% 11
)
check_digit = 0 if remainder in (0, 1) else 11 - remainder
return str(check_digit)

first_check_digit = _get_digit(cnpj)
second_check_digit = _get_digit(cnpj + first_check_digit)
return f"{first_check_digit}{second_check_digit}"

if not value.isdigit():
cnpj = cnpj_digits_re.search(value)
if cnpj:
value = ''.join(cnpj.groups())
else:
raise ValidationError(self.message, code='invalid')
def __call__(self, value):
super().__call__(value)

if len(value) != 14:
raise ValidationError(self.message, code='max_digits')
# After this point, only digits and uppercase letters are important
cleaned_value = re.sub(r"[^A-Z0-9]", "", value)

new_1dv = sum([i * int(value[idx]) for idx, i in enumerate(list(range(5, 1, -1)) + list(range(9, 1, -1)))])
new_1dv = dv_maker(new_1dv % 11)
value = value[:-2] + str(new_1dv) + value[-1]
new_2dv = sum([i * int(value[idx]) for idx, i in enumerate(list(range(6, 1, -1)) + list(range(9, 1, -1)))])
new_2dv = dv_maker(new_2dv % 11)
value = value[:-1] + str(new_2dv)
if value[-2:] != orig_dv:
raise ValidationError(self.message, code='invalid')
input_check_digit = cleaned_value[-2:]
calculated_check_digit = self._get_check_digit(cleaned_value[:-2])
if input_check_digit != calculated_check_digit:
raise ValidationError(self.message, code="invalid")


class BRCPFValidator(RegexValidator):
Expand Down
61 changes: 21 additions & 40 deletions tests/test_br/test_br.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,50 +31,31 @@ def test_BRZipCodeField(self):
self.assertEqual(form.errors['postal_code'], error)

def test_BRCNPJField(self):
error_format = {
'invalid': ['Invalid CNPJ number.'],
'only_long_version': ['Ensure this value has at least 16 characters (it has 14).'],
# The long version can be 16 or 18 characters long so actual error message is set dynamically when the
# invalid_long dict is generated.
'only_short_version': ['Ensure this value has at most 14 characters (it has %s).'],
}

long_version_valid = {
valid_inputs = {
'64.132.916/0001-88': '64.132.916/0001-88',
'64-132-916/0001-88': '64-132-916/0001-88',
'64132916/0001-88': '64132916/0001-88',
}
short_version_valid = {
'12.ABC.345/01DE-35': '12.ABC.345/01DE-35',
'AB.CCC.DEF/GHIJ-08': 'AB.CCC.DEF/GHIJ-08',
'64132916000188': '64132916000188',
'12ABC34501DE35': '12ABC34501DE35',
'ABCDEFGHIJKL80': 'ABCDEFGHIJKL80',
'MNOPQRSTUVWX50': 'MNOPQRSTUVWX50',
'YZOPQRSTUVWX76': 'YZOPQRSTUVWX76',
'03634711000106': '03634711000106',
}
valid = long_version_valid.copy()
valid.update(short_version_valid)

invalid = {
'../-12345678901234': error_format['invalid'],
'12-345-678/9012-10': error_format['invalid'],
'12.345.678/9012-10': error_format['invalid'],
'12345678/9012-10': error_format['invalid'],
'64.132.916/0001-XX': error_format['invalid'],
invalid_inputs = {
'64.132.916/0001-80': [BRCNPJField.default_error_messages['invalid'], ],
'64,132,916/0001-80': [BRCNPJField.default_error_messages['invalid'], ],
'641329160001AA': [BRCNPJField.default_error_messages['invalid'], ],
'2ABC34501DE35': [BRCNPJField.default_error_messages['invalid'], ],
'2ABC34501DE35': [BRCNPJField.default_error_messages['invalid'], ],
'3634711000106': [BRCNPJField.default_error_messages['invalid'], ],
'12.abc.345/01de-35': [BRCNPJField.default_error_messages['invalid'], ],
}
self.assertFieldOutput(BRCNPJField, valid, invalid)

# The short versions should be invalid when 'min_length=16' passed to the field.
invalid_short = dict([(k, error_format['only_long_version']) for k in short_version_valid.keys()])
self.assertFieldOutput(BRCNPJField, long_version_valid, invalid_short, field_kwargs={'min_length': 16})

# The long versions should be invalid when 'max_length=14' passed to the field.
invalid_long = dict([(k, [error_format['only_short_version'][0] % len(k)]) for k in long_version_valid.keys()])
self.assertFieldOutput(BRCNPJField, short_version_valid, invalid_long, field_kwargs={'max_length': 14})

for cnpj, invalid_msg in invalid.items():
with self.subTest(cnpj=cnpj, invalid_msg=invalid_msg):
form = BRPersonProfileForm({
'cnpj': cnpj
})

self.assertFalse(form.is_valid())
self.assertEqual(form.errors['cnpj'], invalid_msg)
self.assertFieldOutput(
BRCNPJField,
valid=valid_inputs,
invalid=invalid_inputs,
)

def test_BRCPFField(self):
error_format = ['Invalid CPF number.']
Expand Down