Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 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
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

### Added

- Utilitário `generate_cnpj` com suporte a CNPJ alfanumérico [#741](https://github.com/brazilian-utils/python/pull/741)

## [2.4.0] - 2026-04-20

### Added
Expand Down
17 changes: 13 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -213,7 +213,8 @@ Exemplo:

Verifica se os dígitos de verificação do CNPJ (Cadastro Nacional da Pessoa
Jurídica) fornecido correspondem ao seu número base. A entrada deve ser uma
string de dígitos com o comprimento apropriado. Esta função não verifica a
string de 14 caracteres, permitindo dígitos e letras maiúsculas nas 12
primeiras posições e dígitos nas 2 últimas. Esta função não verifica a
existência do CNPJ; ela só valida o formato da string.

Argumentos:
Expand Down Expand Up @@ -285,12 +286,16 @@ Exemplo:

### generate_cnpj

Gera uma string de dígitos CNPJ válida aleatória. Um número de filial
opcional pode ser fornecido; o padrão é 1.
Gera uma string de CNPJ válida aleatória. Um número de filial opcional pode ser
fornecido; o padrão é 1. Use `alphanumeric=True` para gerar um CNPJ cujas 12
primeiras posições podem conter dígitos e letras maiúsculas.

Argumentos:

- branch (int): Um número de filial opcional a ser incluído no CNPJ.
- branch (int | str): Um número de filial opcional a ser incluído no CNPJ.
Valores de filial alfanuméricos são aceitos apenas com
`alphanumeric=True`.
- alphanumeric (bool): Define se o CNPJ gerado deve ser alfanumérico.

Retorna:

Expand All @@ -304,6 +309,10 @@ Exemplo:
'34665388000161'
>>> generate_cnpj(1234)
"01745284123455"
>>> generate_cnpj(alphanumeric=True)
"9359QAG9000184"
>>> generate_cnpj(branch="AB12", alphanumeric=True)
"NX9K79E2AB1200"
Comment thread
niltonpimentel02 marked this conversation as resolved.
Outdated
```

## CEP
Expand Down
21 changes: 15 additions & 6 deletions README_EN.md
Original file line number Diff line number Diff line change
Expand Up @@ -213,9 +213,10 @@ Example:

Returns whether or not the verifying checksum digits of the given CNPJ
(Brazilian Company Registration Number) match its base number.
Input should be a digit string of proper length.
This function does not verify the existence of the CNPJ; it only
validates the format of the string.
Input should be a 14-character string, allowing digits and uppercase letters
in the first 12 positions and digits in the last 2 positions. This function
does not verify the existence of the CNPJ; it only validates the format of the
string.

Args:

Expand Down Expand Up @@ -287,12 +288,16 @@ Example:

### generate_cnpj

Generates a random valid CNPJ (Brazilian Company Registration Number) digit
string. An optional branch number parameter can be given; it defaults to 1.
Generates a random valid CNPJ (Brazilian Company Registration Number) string.
An optional branch number parameter can be given; it defaults to 1. Use
`alphanumeric=True` to generate a CNPJ whose first 12 positions may contain
digits and uppercase letters.

Args:

- branch (int): An optional branch number to be included in the CNPJ.
- branch (int | str): An optional branch number to be included in the CNPJ.
Alphanumeric branch values are accepted only with `alphanumeric=True`.
- alphanumeric (bool): Whether the generated CNPJ should be alphanumeric.

Returns:

Expand All @@ -306,6 +311,10 @@ Example:
'34665388000161'
>>> generate_cnpj(1234)
"01745284123455"
>>> generate_cnpj(alphanumeric=True)
"9359QAG9000184"
>>> generate_cnpj(branch="AB12", alphanumeric=True)
"NX9K79E2AB1200"
```

## CEP
Expand Down
71 changes: 63 additions & 8 deletions brutils/cnpj.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
from itertools import chain
from random import randint
from random import choices, randint
from string import ascii_uppercase, digits

# FORMATTING
############
Expand Down Expand Up @@ -83,7 +84,12 @@ def display(cnpj: str) -> str | None:
backward compatibility.
"""

if not cnpj.isdigit() or len(cnpj) != 14 or len(set(cnpj)) == 1:
if (
len(cnpj) != 14
or not _is_alphanumeric(cnpj[:12])
or not cnpj[12:].isdigit()
or len(set(cnpj)) == 1
):
return None
return "{}.{}.{}/{}-{}".format(
cnpj[:2], cnpj[2:5], cnpj[5:8], cnpj[8:12], cnpj[12:]
Expand Down Expand Up @@ -124,6 +130,27 @@ def format_cnpj(cnpj: str) -> str | None:
############


def _is_alphanumeric(cnpj: str) -> bool:
"""
Checks whether all characters are digits or uppercase letters.

Args:
cnpj (str): The CNPJ string to be validated.

Returns:
bool: True if all characters are either digits or uppercase letters,
False otherwise.

Example:
>>> _is_alphanumeric("035ABC1400Z142")
True
>>> _is_alphanumeric("0011-22200013!")
False
"""

return all(char in (digits + ascii_uppercase) for char in cnpj)


def validate(cnpj: str) -> bool:
"""
Validates a CNPJ (Brazilian Company Registration Number) by comparing its
Expand Down Expand Up @@ -151,7 +178,12 @@ def validate(cnpj: str) -> bool:
backward compatibility.
"""

if not cnpj.isdigit() or len(cnpj) != 14 or len(set(cnpj)) == 1:
if (
len(cnpj) != 14
or not _is_alphanumeric(cnpj[:12])
or not cnpj[12:].isdigit()
or len(set(cnpj)) == 1
):
return False
return all(
_hashdigit(cnpj, i + 13) == int(v) for i, v in enumerate(cnpj[12:])
Expand Down Expand Up @@ -183,13 +215,18 @@ def is_valid(cnpj: str) -> bool:
return isinstance(cnpj, str) and validate(cnpj)


def generate(branch: int = 1) -> str:
def generate(branch: int | str = 1, alphanumeric: bool = False) -> str:
"""
Generates a random valid CNPJ digit string. An optional branch number
parameter can be given; it defaults to 1.
Generates a random valid CNPJ string. An optional branch number parameter
can be given; it defaults to 1. Use alphanumeric=True to generate a CNPJ
whose first 12 characters may contain digits and uppercase letters.

Args:
branch (int): An optional branch number to be included in the CNPJ.
branch (int | str): An optional branch number to be included in the
CNPJ. Alphanumeric branch values are accepted only with
alphanumeric=True.
alphanumeric (bool): Whether the generated CNPJ should be
alphanumeric.

Returns:
str: A randomly generated valid CNPJ string.
Expand All @@ -199,8 +236,23 @@ def generate(branch: int = 1) -> str:
"30180536000105"
>>> generate(1234)
"01745284123455"
>>> generate(branch="AB12", alphanumeric=True)
"NX9K79E2AB1200"
"""

if alphanumeric:
branch = str(branch)
branch = branch[:4] if len(branch) >= 4 else branch.zfill(4)
branch = (
"0001"
if branch == "0000" or not _is_alphanumeric(branch)
else branch
)
base = "".join(choices(digits * 3 + ascii_uppercase, k=8)) + branch

return base + _checksum(base)

branch = int(branch)
branch %= 10000
branch += int(branch == 0)
branch = str(branch).zfill(4)
Expand Down Expand Up @@ -230,7 +282,10 @@ def _hashdigit(cnpj: str, position: int) -> int:

weightgen = chain(range(position - 8, 1, -1), range(9, 1, -1))
val = (
sum(int(digit) * weight for digit, weight in zip(cnpj, weightgen)) % 11
sum(
(ord(digit) - 48) * weight for digit, weight in zip(cnpj, weightgen)
)
% 11
)
return 0 if val < 2 else 11 - val

Expand Down
33 changes: 33 additions & 0 deletions tests/test_cnpj.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
from brutils.cnpj import (
_checksum,
_hashdigit,
_is_alphanumeric,
display,
format_cnpj,
generate,
Expand All @@ -27,12 +28,23 @@ def test_sieve(self):

def test_display(self):
self.assertEqual(display("00000000000109"), "00.000.000/0001-09")
self.assertEqual(display("12ABC34501DE35"), "12.ABC.345/01DE-35")
self.assertIsNone(display("12ABC34501DEAA"))
self.assertIsNone(display("00000000000000"))
self.assertIsNone(display("0000000000000"))
self.assertIsNone(display("0000000000000a"))

def test__is_alphanumeric(self):
self.assertIs(_is_alphanumeric("12ABC34501DE35"), True)
self.assertIs(_is_alphanumeric("12345678910111"), True)
self.assertIs(_is_alphanumeric("123456a78b10C1"), False)
self.assertIs(_is_alphanumeric("12.ABC.345/01DE-35"), False)

def test_validate(self):
self.assertIs(validate("34665388000161"), True)
self.assertIs(validate("12ABC34501DE35"), True)
self.assertIs(validate("Z46ABC88000164"), True)
self.assertIs(validate("12ABC34501DEAA"), False)
self.assertIs(validate("52599927000100"), False)
self.assertIs(validate("00000000000"), False)

Expand Down Expand Up @@ -71,6 +83,19 @@ def test_generate(self):
for _ in range(10_000):
self.assertIs(validate(generate()), True)
self.assertIsNotNone(display(generate()))
self.assertIs(validate(generate(branch=1234)), True)

def test_generate_alphanumeric(self):
for _ in range(10_000):
generated = generate(alphanumeric=True)
self.assertIs(validate(generated), True)
self.assertIsNotNone(display(generated))
self.assertIs(
validate(generate(branch="1234", alphanumeric=True)), True
)
self.assertIs(
validate(generate(branch="AB12", alphanumeric=True)), True
)

def test__hashdigit(self):
self.assertEqual(_hashdigit("00000000000000", 13), 0)
Expand Down Expand Up @@ -109,5 +134,13 @@ def test_when_cnpj_is_not_valid_returns_none(self, mock_is_valid):
self.assertIsNone(format_cnpj("01838723000127"))


class TestFormatCnpj(TestCase):
def test_when_cnpj_is_alphanumeric_valid_returns_formatted_cnpj(self):
self.assertEqual(format_cnpj("12ABC34501DE35"), "12.ABC.345/01DE-35")

def test_when_cnpj_has_alphanumeric_check_digits_returns_none(self):
self.assertIsNone(format_cnpj("12ABC34501DEAA"))


if __name__ == "__main__":
main()
Loading