From 7b2b3e5ec948fddea8ba9d050bde78160923718f Mon Sep 17 00:00:00 2001 From: NATAN ALMEIDA Date: Wed, 11 Feb 2026 14:05:15 -0300 Subject: [PATCH 1/3] feat: Support for the new alphanumeric CNPJ --- README.md | 4 ++++ README_EN.md | 4 ++++ brutils/cnpj.py | 8 ++++++-- tests/test_cnpj.py | 19 +++++++++++++++++++ 4 files changed, 33 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index a24fffa3..bcf204b5 100644 --- a/README.md +++ b/README.md @@ -228,6 +228,8 @@ Exemplo: True >>> is_valid_cnpj('00111222000133') False +>>> is_valid_cnpj('12ABC34501DE35') +True ``` ### format_cnpj @@ -276,6 +278,8 @@ Exemplo: >>> from brutils import remove_symbols_cnpj >>> remove_symbols_cnpj('00.111.222/0001-00') '00111222000100' +>>> remove_symbols_cnpj('12.ABC.345/01DE-35') +'12ABC34501DE35' ``` ### generate_cnpj diff --git a/README_EN.md b/README_EN.md index aa784da5..4d1a6f7a 100644 --- a/README_EN.md +++ b/README_EN.md @@ -229,6 +229,8 @@ Example: True >>> is_valid_cnpj('00111222000133') False +>>> is_valid_cnpj('12ABC34501DE35') +True ``` ### format_cnpj @@ -278,6 +280,8 @@ Example: >>> from brutils import remove_symbols_cnpj >>> remove_symbols_cnpj('00.111.222/0001-00') '00111222000100' +>>> remove_symbols_cnpj('12.ABC.345/01DE-35') +'12ABC34501DE35' ``` ### generate_cnpj diff --git a/brutils/cnpj.py b/brutils/cnpj.py index 84dd7ae8..ddd2140a 100644 --- a/brutils/cnpj.py +++ b/brutils/cnpj.py @@ -151,7 +151,7 @@ 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 len(set(cnpj)) == 1: return False return all( _hashdigit(cnpj, i + 13) == int(v) for i, v in enumerate(cnpj[12:]) @@ -230,7 +230,11 @@ 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(char) - 48) * weight + for char, 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..927aa8e1 100644 --- a/tests/test_cnpj.py +++ b/tests/test_cnpj.py @@ -35,6 +35,10 @@ def test_validate(self): self.assertIs(validate("34665388000161"), True) self.assertIs(validate("52599927000100"), False) self.assertIs(validate("00000000000"), False) + self.assertIs(validate("12ABC34501DE35"), True) + self.assertIs(validate("12ABC34501DE00"), False) + self.assertIs(validate("AAAAAAAAAAAAAA"), False) + self.assertIs(validate("12ABC34501DE3"), False) def test_is_valid(self): # When CNPJ is not string, returns False @@ -66,6 +70,7 @@ def test_is_valid(self): # When CNPJ is valid self.assertIs(is_valid("34665388000161"), True) self.assertIs(is_valid("01838723000127"), True) + self.assertIs(is_valid("12ABC34501DE35"), True) def test_generate(self): for _ in range(10_000): @@ -77,10 +82,15 @@ def test__hashdigit(self): self.assertEqual(_hashdigit("00000000000000", 14), 0) self.assertEqual(_hashdigit("52513127000292", 13), 9) self.assertEqual(_hashdigit("52513127000292", 14), 9) + self.assertEqual(_hashdigit("12ABC34501DE", 13), 3) + self.assertEqual(_hashdigit("12ABC34501DE3", 14), 5) def test__checksum(self): self.assertEqual(_checksum("00000000000000"), "00") self.assertEqual(_checksum("52513127000299"), "99") + self.assertEqual(_checksum("12ABC34501DE35"), "35") + self.assertEqual(_checksum("1345C3A50001"), "06") + self.assertEqual(_checksum("R55231B30007"), "57") @patch("brutils.cnpj.sieve") @@ -89,6 +99,8 @@ def test_remove_symbols(self, mock_sieve): # When call remove_symbols, it calls sieve remove_symbols("12.345.678/0001-90") mock_sieve.assert_called() + remove_symbols("12.ABC.345/01DE-35") + mock_sieve.assert_called() @patch("brutils.cnpj.is_valid") @@ -102,6 +114,13 @@ def test_when_cnpj_is_valid_returns_true_to_format(self, mock_is_valid): # Checks if function is_valid_cnpj is called mock_is_valid.assert_called_once_with("01838723000127") + mock_is_valid.reset_mock() + # When cnpj is_valid, returns formatted cnpj + self.assertEqual(format_cnpj("12ABC34501DE35"), "12.ABC.345/01DE-35") + + # Checks if function is_valid_cnpj is called + mock_is_valid.assert_called_once_with("12ABC34501DE35") + def test_when_cnpj_is_not_valid_returns_none(self, mock_is_valid): mock_is_valid.return_value = False From 3452b02d2481dc8cbb51aeb7a70f68d7e5998ddf Mon Sep 17 00:00:00 2001 From: NATAN ALMEIDA Date: Wed, 11 Feb 2026 14:09:41 -0300 Subject: [PATCH 2/3] docs: update changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index a8d204d2..1074ade0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Utilitário `list_all_legal_nature` [#653](https://github.com/brazilian-utils/python/pull/653) - Utilitário `is_valid_cnh` [#651](https://github.com/brazilian-utils/brutils-python/pull/651) - Utilitário `is_valid_renavam` [#652](https://github.com/brazilian-utils/brutils-python/pull/652) +- Suporte a CNPJ alfanumérico em `is_valid_cnpj`, `format_cnpj` e `remove_symbols_cnpj` ### Fixed From b835edafb960df9260926425c44d5818d2bf8e96 Mon Sep 17 00:00:00 2001 From: NATAN ALMEIDA Date: Wed, 11 Feb 2026 14:36:00 -0300 Subject: [PATCH 3/3] fix: lint check --- brutils/cnpj.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/brutils/cnpj.py b/brutils/cnpj.py index ddd2140a..228a9ef7 100644 --- a/brutils/cnpj.py +++ b/brutils/cnpj.py @@ -230,10 +230,7 @@ def _hashdigit(cnpj: str, position: int) -> int: weightgen = chain(range(position - 8, 1, -1), range(9, 1, -1)) val = ( - sum( - (ord(char) - 48) * weight - for char, weight in zip(cnpj, weightgen) - ) + sum((ord(char) - 48) * weight for char, weight in zip(cnpj, weightgen)) % 11 ) return 0 if val < 2 else 11 - val