Skip to content
Open
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 @@ -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