Skip to content

Commit e6dc8fc

Browse files
committed
Update validator to allow new Brazilian CNPJ alphanumeric format
In July-2026, alphanumeric CNPJ will be allowed: https://www.gov.br/receitafederal/pt-br/acesso-a-informacao/acoes-e-programas/programas-e-atividades/cnpj-alfanumerico Given that the new format is 100% compatible with the current format, we can update the validators now, making the transition smoother for users of the library.
1 parent 6a5bada commit e6dc8fc

5 files changed

Lines changed: 59 additions & 73 deletions

File tree

docs/authors.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,7 @@ Authors
115115
* Rael Max
116116
* Ramiro Morales
117117
* Raphael Michel
118+
* Renne Rocha
118119
* Rolf Erik Lekang
119120
* Russell Keith-Magee
120121
* Santosh Bhattarai

docs/changelog.rst

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@ Modifications to existing flavors:
1919
(`gh-529 <https://github.com/django/django-localflavor/pull/529>`_).
2020
- Update SI postal codes
2121
(`gh-531 <https://github.com/django/django-localflavor/pull/531>`_).
22+
- Update BR CNPJ validator to accept new alphanumeric format
23+
(`gh-533 <https://github.com/django/django-localflavor/pull/533>`_)
2224

2325
Other changes:
2426

localflavor/br/forms.py

Lines changed: 4 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -88,15 +88,7 @@ class BRCNPJField(CharField):
8888
A form field that validates input as `Brazilian CNPJ`_.
8989
9090
Input can either be of the format XX.XXX.XXX/XXXX-XX or be a group of 14
91-
digits.
92-
93-
If you want to use the long format only, you can specify:
94-
brcnpj_field = BRCNPJField(min_length=16)
95-
96-
If you want to use the short format, you can specify:
97-
brcnpj_field = BRCNPJField(max_length=14)
98-
99-
Otherwise both formats will be valid.
91+
digits or upper case letters.
10092
10193
.. _Brazilian CNPJ: http://en.wikipedia.org/wiki/National_identification_number#Brazil
10294
.. versionchanged:: 1.4
@@ -106,12 +98,11 @@ class BRCNPJField(CharField):
10698
"""
10799

108100
default_error_messages = {
109-
'invalid': _("Invalid CNPJ number."),
110-
'max_digits': _("This field requires at least 14 digits"),
101+
"invalid": _("Invalid CNPJ number."),
111102
}
112103

113-
def __init__(self, min_length=14, max_length=18, **kwargs):
114-
super().__init__(max_length=max_length, min_length=min_length, **kwargs)
104+
def __init__(self, *args, **kwargs):
105+
super().__init__(*args, **kwargs)
115106
self.validators.append(BRCNPJValidator())
116107

117108

localflavor/br/validators.py

Lines changed: 31 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
1+
import itertools
12
import re
23

34
from django.core.exceptions import ValidationError
45
from django.core.validators import RegexValidator
56
from django.utils.translation import gettext_lazy as _
67

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

1111

@@ -34,35 +34,46 @@ class BRCNPJValidator(RegexValidator):
3434
.. versionadded:: 2.2
3535
"""
3636

37+
CNPJ_RE = re.compile(r'^([A-Z0-9]{2}).?([A-Z0-9]{3}).?([A-Z0-9]{3})\/?([A-Z0-9]{4})-?(\d{2})$')
38+
3739
def __init__(self, *args, **kwargs):
3840
super().__init__(
3941
*args,
40-
regex=cnpj_digits_re,
42+
regex=self.CNPJ_RE,
4143
message=_("Invalid CNPJ number."),
4244
**kwargs
4345
)
4446

45-
def __call__(self, value):
46-
orig_dv = value[-2:]
47+
def _get_check_digit(self, cnpj):
48+
'''
49+
Based on official documentation at:
50+
https://www.gov.br/receitafederal/pt-br/centrais-de-conteudo/publicacoes/documentos-tecnicos/cnpj
51+
'''
52+
def _get_digit(value):
53+
values = [ord(c) - 48 for c in value][::-1]
54+
remainder = (
55+
sum(
56+
[x * y for x, y in list(zip(values, itertools.cycle(range(2, 10))))]
57+
)
58+
% 11
59+
)
60+
check_digit = 0 if remainder in (0, 1) else 11 - remainder
61+
return str(check_digit)
62+
63+
first_check_digit = _get_digit(cnpj)
64+
second_check_digit = _get_digit(cnpj + first_check_digit)
65+
return f"{first_check_digit}{second_check_digit}"
4766

48-
if not value.isdigit():
49-
cnpj = cnpj_digits_re.search(value)
50-
if cnpj:
51-
value = ''.join(cnpj.groups())
52-
else:
53-
raise ValidationError(self.message, code='invalid')
67+
def __call__(self, value):
68+
super().__call__(value)
5469

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

58-
new_1dv = sum([i * int(value[idx]) for idx, i in enumerate(list(range(5, 1, -1)) + list(range(9, 1, -1)))])
59-
new_1dv = dv_maker(new_1dv % 11)
60-
value = value[:-2] + str(new_1dv) + value[-1]
61-
new_2dv = sum([i * int(value[idx]) for idx, i in enumerate(list(range(6, 1, -1)) + list(range(9, 1, -1)))])
62-
new_2dv = dv_maker(new_2dv % 11)
63-
value = value[:-1] + str(new_2dv)
64-
if value[-2:] != orig_dv:
65-
raise ValidationError(self.message, code='invalid')
73+
input_check_digit = cleaned_value[-2:]
74+
calculated_check_digit = self._get_check_digit(cleaned_value[:-2])
75+
if input_check_digit != calculated_check_digit:
76+
raise ValidationError(self.message, code="invalid")
6677

6778

6879
class BRCPFValidator(RegexValidator):

tests/test_br/test_br.py

Lines changed: 21 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -31,50 +31,31 @@ def test_BRZipCodeField(self):
3131
self.assertEqual(form.errors['postal_code'], error)
3232

3333
def test_BRCNPJField(self):
34-
error_format = {
35-
'invalid': ['Invalid CNPJ number.'],
36-
'only_long_version': ['Ensure this value has at least 16 characters (it has 14).'],
37-
# The long version can be 16 or 18 characters long so actual error message is set dynamically when the
38-
# invalid_long dict is generated.
39-
'only_short_version': ['Ensure this value has at most 14 characters (it has %s).'],
40-
}
41-
42-
long_version_valid = {
34+
valid_inputs = {
4335
'64.132.916/0001-88': '64.132.916/0001-88',
44-
'64-132-916/0001-88': '64-132-916/0001-88',
45-
'64132916/0001-88': '64132916/0001-88',
46-
}
47-
short_version_valid = {
36+
'12.ABC.345/01DE-35': '12.ABC.345/01DE-35',
37+
'AB.CCC.DEF/GHIJ-08': 'AB.CCC.DEF/GHIJ-08',
4838
'64132916000188': '64132916000188',
39+
'12ABC34501DE35': '12ABC34501DE35',
40+
'ABCDEFGHIJKL80': 'ABCDEFGHIJKL80',
41+
'MNOPQRSTUVWX50': 'MNOPQRSTUVWX50',
42+
'YZOPQRSTUVWX76': 'YZOPQRSTUVWX76',
43+
'03634711000106': '03634711000106',
4944
}
50-
valid = long_version_valid.copy()
51-
valid.update(short_version_valid)
52-
53-
invalid = {
54-
'../-12345678901234': error_format['invalid'],
55-
'12-345-678/9012-10': error_format['invalid'],
56-
'12.345.678/9012-10': error_format['invalid'],
57-
'12345678/9012-10': error_format['invalid'],
58-
'64.132.916/0001-XX': error_format['invalid'],
45+
invalid_inputs = {
46+
'64.132.916/0001-80': [BRCNPJField.default_error_messages['invalid'], ],
47+
'64,132,916/0001-80': [BRCNPJField.default_error_messages['invalid'], ],
48+
'641329160001AA': [BRCNPJField.default_error_messages['invalid'], ],
49+
'2ABC34501DE35': [BRCNPJField.default_error_messages['invalid'], ],
50+
'2ABC34501DE35': [BRCNPJField.default_error_messages['invalid'], ],
51+
'3634711000106': [BRCNPJField.default_error_messages['invalid'], ],
52+
'12.abc.345/01de-35': [BRCNPJField.default_error_messages['invalid'], ],
5953
}
60-
self.assertFieldOutput(BRCNPJField, valid, invalid)
61-
62-
# The short versions should be invalid when 'min_length=16' passed to the field.
63-
invalid_short = dict([(k, error_format['only_long_version']) for k in short_version_valid.keys()])
64-
self.assertFieldOutput(BRCNPJField, long_version_valid, invalid_short, field_kwargs={'min_length': 16})
65-
66-
# The long versions should be invalid when 'max_length=14' passed to the field.
67-
invalid_long = dict([(k, [error_format['only_short_version'][0] % len(k)]) for k in long_version_valid.keys()])
68-
self.assertFieldOutput(BRCNPJField, short_version_valid, invalid_long, field_kwargs={'max_length': 14})
69-
70-
for cnpj, invalid_msg in invalid.items():
71-
with self.subTest(cnpj=cnpj, invalid_msg=invalid_msg):
72-
form = BRPersonProfileForm({
73-
'cnpj': cnpj
74-
})
75-
76-
self.assertFalse(form.is_valid())
77-
self.assertEqual(form.errors['cnpj'], invalid_msg)
54+
self.assertFieldOutput(
55+
BRCNPJField,
56+
valid=valid_inputs,
57+
invalid=invalid_inputs,
58+
)
7859

7960
def test_BRCPFField(self):
8061
error_format = ['Invalid CPF number.']

0 commit comments

Comments
 (0)