diff --git a/CHANGELOG.md b/CHANGELOG.md index d3c74c57..fbe1955c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Added + +- Utilitário `generate_alphanumeric_cnpj` [#741](https://github.com/brazilian-utils/python/pull/741) + ## [2.4.0] - 2026-04-20 ### Added diff --git a/README.md b/README.md index 73a5ce02..d0658f73 100644 --- a/README.md +++ b/README.md @@ -52,6 +52,7 @@ False - [format\_cnpj](#format_cnpj) - [remove\_symbols\_cnpj](#remove_symbols_cnpj) - [generate\_cnpj](#generate_cnpj) + - [generate\_alphanumeric\_cnpj](#generate_alphanumeric_cnpj) - [CEP](#cep) - [is\_valid\_cep](#is_valid_cep) - [format\_cep](#format_cep) @@ -213,7 +214,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: @@ -306,6 +308,29 @@ Exemplo: "01745284123455" ``` +### generate_alphanumeric_cnpj + +Gera uma string de CNPJ alfanumérico válida aleatória. Um número de filial +opcional pode ser fornecido; o padrão é '1'. + +Argumentos: + +- branch (str): Um número de filial opcional a ser incluído no CNPJ. + +Retorna: + +- str: Um CNPJ alfanumérico válido gerado aleatoriamente. + +Exemplo: + +```python +>>> from brutils import generate_alphanumeric_cnpj +>>> generate_alphanumeric_cnpj() +"9359QAG9000184" +>>> generate_alphanumeric_cnpj('1234') +"NX9K79E2123400" +``` + ## CEP ### is_valid_cep diff --git a/README_EN.md b/README_EN.md index 5933d771..362126dc 100644 --- a/README_EN.md +++ b/README_EN.md @@ -51,6 +51,7 @@ False - [format\_cnpj](#format_cnpj) - [remove\_symbols\_cnpj](#remove_symbols_cnpj) - [generate\_cnpj](#generate_cnpj) + - [generate\_alphanumeric\_cnpj](#generate_alphanumeric_cnpj) - [CEP](#cep) - [is\_valid\_cep](#is_valid_cep) - [format\_cep](#format_cep) @@ -213,9 +214,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: @@ -308,6 +310,29 @@ Example: "01745284123455" ``` +### generate_alphanumeric_cnpj + +Generates a random valid alphanumeric CNPJ string. An optional branch number +parameter can be given; it defaults to '1'. + +Args: + +- branch (str): An optional branch number to be included in the CNPJ. + +Returns: + +- str: A randomly generated valid alphanumeric CNPJ string. + +Example: + +```python +>>> from brutils import generate_alphanumeric_cnpj +>>> generate_alphanumeric_cnpj() +"9359QAG9000184" +>>> generate_alphanumeric_cnpj('1234') +"NX9K79E2123400" +``` + ## CEP ### is_valid_cep diff --git a/brutils/__init__.py b/brutils/__init__.py index ddb264e4..aad2c5a5 100644 --- a/brutils/__init__.py +++ b/brutils/__init__.py @@ -14,6 +14,7 @@ # CNPJ Imports from brutils.cnpj import format_cnpj from brutils.cnpj import generate as generate_cnpj +from brutils.cnpj import generate_alphanumeric as generate_alphanumeric_cnpj from brutils.cnpj import is_valid as is_valid_cnpj from brutils.cnpj import remove_symbols as remove_symbols_cnpj @@ -105,6 +106,7 @@ # CNPJ "format_cnpj", "generate_cnpj", + "generate_alphanumeric_cnpj", "is_valid_cnpj", "remove_symbols_cnpj", # CPF diff --git a/brutils/cnpj.py b/brutils/cnpj.py index 84dd7ae8..b77ffeca 100644 --- a/brutils/cnpj.py +++ b/brutils/cnpj.py @@ -1,5 +1,6 @@ from itertools import chain -from random import randint +from random import choices, randint +from string import ascii_uppercase, digits # FORMATTING ############ @@ -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:] @@ -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 @@ -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:]) @@ -209,6 +241,34 @@ def generate(branch: int = 1) -> str: return base + _checksum(base) +def generate_alphanumeric(branch: str = "1") -> str: + """ + Generates a random valid alphanumeric CNPJ digit string. An optional branch + number parameter can be given; it defaults to '1'. + + Args: + branch (str): An optional branch number to be included in the CNPJ. + + Returns: + str: A randomly generated valid alphanumeric CNPJ string. + + Example: + >>> generate_alphanumeric() + "9359QAG9000184" + >>> generate_alphanumeric('1234') + "NX9K79E2123400" + """ + + 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) + + def _hashdigit(cnpj: str, position: int) -> int: """ Calculates the checksum digit at the given `position` for the provided @@ -230,7 +290,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 diff --git a/tests/test_cnpj.py b/tests/test_cnpj.py index 2ec7261f..eecb638b 100644 --- a/tests/test_cnpj.py +++ b/tests/test_cnpj.py @@ -4,9 +4,11 @@ from brutils.cnpj import ( _checksum, _hashdigit, + _is_alphanumeric, display, format_cnpj, generate, + generate_alphanumeric, is_valid, remove_symbols, sieve, @@ -27,12 +29,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) @@ -71,6 +84,14 @@ 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() + self.assertIs(validate(generated), True) + self.assertIsNotNone(display(generated)) + self.assertIs(validate(generate_alphanumeric(branch="1234")), True) def test__hashdigit(self): self.assertEqual(_hashdigit("00000000000000", 13), 0) @@ -109,5 +130,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()