Repository: alvarofpp/validate-docbr

Issue:
Novo formato de CNPJ a partir de 2026
O CNPJ Alfanumérico será atribuído, a partir de Julho de 2026, exclusivamente a novas inscrições.

Os números de CNPJ já existentes não sofrerão nenhuma alteração, ou seja, quem já está inscrito no CNPJ permanecerá com o seu número válido.

https://www.gov.br/receitafederal/pt-br/acesso-a-informacao/acoes-e-programas/programas-e-atividades/cnpj-alfanumerico

Hints from issue comments:
@arthrod: Fala pessoal,
Obrigado pelo repo! Então quer dizer que até essa data o "mil ao contrário" continue forte e operante, certo?
Já que estou aqui, alguma base de dados pública com os CEPs? (só os números) Os APIs eu conheço, mas estou tentando gerar um gerador de samples mais realistas.
Abs.,
Arthur@alvarofpp: @leogregianin Obrigado por avisar, não estava ciente disso.@alvarofpp: @arthrod 

> Então quer dizer que até essa data o "mil ao contrário" continue forte e operante, certo?

Sim!

> Já que estou aqui, alguma base de dados pública com os CEPs?

Eu encontrei esse aqui: https://basedosdados.org/dataset/33b49786-fb5f-496f-bb7c-9811c985af8e?table=566ed7f2-db5f-4ac3-bfce-b579137c993c . Você só precisará fazer um tratamento dos dados baixados para deixar apenas os números.

Solution patch:
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 5fadba8..a754c3c 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -3,7 +3,7 @@
 Para contribuir com o pacote com a inserção de um novo documento:
 
 - Crie uma _issue_ dizendo sobre o documento que deseja inserir ao pacote;
-  - Preferencialmente coloque links que ajudem a entender o algoritmo de geração e válidação do documento.
+  - Preferencialmente coloque links que ajudem a entender o algoritmo de geração e validação do documento.
 - Realize os procedimentos padrões, sendo que na hora de criar a sua _branch_, referencie a sua _issue_;
 - Realize o _pull request_ para a branch _master_.
 
diff --git a/validate_docbr/BaseDoc.py b/validate_docbr/BaseDoc.py
index 3f5f47b..1f1f98d 100644
--- a/validate_docbr/BaseDoc.py
+++ b/validate_docbr/BaseDoc.py
@@ -6,7 +6,7 @@ class BaseDoc(ABC):
     """Classe base para todas as classes referentes a documentos."""
 
     @abstractmethod
-    def validate(self, doc: str = '') -> bool:
+    def validate(self, doc: str = "") -> bool:
         """Método para validar o documento desejado."""
         pass
 
@@ -20,10 +20,7 @@ class BaseDoc(ABC):
         pass
 
     def generate_list(
-        self,
-        n: int = 1,
-        mask: bool = False,
-        repeat: bool = False
+        self, n: int = 1, mask: bool = False, repeat: bool = False
     ) -> list:
         """Gerar uma lista do mesmo documento."""
         doc_list = []
@@ -40,9 +37,7 @@ class BaseDoc(ABC):
 
             if unique_values < n:
                 doc_list = list(doc_set) + self.generate_list(
-                    (n - unique_values),
-                    mask,
-                    repeat
+                    (n - unique_values), mask, repeat
                 )
             else:
                 repeat = True
@@ -50,23 +45,35 @@ class BaseDoc(ABC):
         return doc_list
 
     @abstractmethod
-    def mask(self, doc: str = '') -> str:
+    def mask(self, doc: str = "") -> str:
         """Mascara o documento enviado"""
         pass
 
-    def _only_digits(self, doc: str = '') -> str:
+    def _only_digits(self, doc: str = "") -> str:
         """Remove os outros caracteres que não sejam dígitos."""
-        return ''.join([x for x in doc if x.isdigit()])
+        return "".join([x for x in doc if x.isdigit()])
 
-    def _validate_input(self, input: str, valid_characters: List = None) -> bool:
+    def _only_digits_and_letters(self, doc: str = "") -> str:
+        """Remove os outros caracteres que não sejam dígitos ou letras."""
+        return "".join([x for x in doc if x.isdigit() or x.isalpha()])
+
+    def _validate_input(
+        self, input: str, valid_characters: List = None, allow_letters: bool = False
+    ) -> bool:
         """Validar input.
-        Caso ele possua apenas dígitos e caracteres válidos, retorna True.
+        Caso ele possua apenas dígitos (e, opcionalmente, letras) e caracteres
+        válidos, retorna True.
         Caso possua algum caractere que não seja dígito ou caractere válido,
         retorna False."""
         if valid_characters is None:
-            valid_characters = ['.', '-', '/', ' ']
-
-        set_non_digit_characters = set([x for x in input if not x.isdigit()])
+            valid_characters = [".", "-", "/", " "]
+
+        if allow_letters:
+            set_non_digit_characters = set(
+                [x for x in input if not x.isdigit() and not x.isalpha()]
+            )
+        else:
+            set_non_digit_characters = set([x for x in input if not x.isdigit()])
         set_valid_characters = set(valid_characters)
 
         return not (len(set_non_digit_characters.difference(set_valid_characters)) > 0)
diff --git a/validate_docbr/CNPJ.py b/validate_docbr/CNPJ.py
index 004679b..1b81172 100644
--- a/validate_docbr/CNPJ.py
+++ b/validate_docbr/CNPJ.py
@@ -1,3 +1,4 @@
+import string
 from random import sample
 from typing import Union
 
@@ -9,15 +10,17 @@ class CNPJ(BaseDoc):
 
     def __init__(self):
         self.digits = list(range(10))
+        self.digits_and_letters = list(string.ascii_uppercase) + list(string.digits)
         self.weights_first = list(range(5, 1, -1)) + list(range(9, 1, -1))
         self.weights_second = list(range(6, 1, -1)) + list(range(9, 1, -1))
 
     def validate(self, doc: str = '') -> bool:
         """Validar CNPJ."""
-        if not self._validate_input(doc, ['.', '/', '-']):
+        if not self._validate_input(doc, ['.', '/', '-'], allow_letters=True):
             return False
 
-        doc = self._only_digits(doc)
+        doc = doc.strip().upper()
+        doc = self._only_digits_and_letters(doc)
 
         if len(doc) != 14:
             return False
@@ -25,10 +28,13 @@ class CNPJ(BaseDoc):
         return self._generate_first_digit(doc) == doc[12] \
                and self._generate_second_digit(doc) == doc[13]
 
-    def generate(self, mask: bool = False) -> str:
+    def generate(self, mask: bool = False, digits_only: bool = True) -> str:
         """Gerar CNPJ."""
         # Os doze primeiros dígitos
-        cnpj = [str(sample(self.digits, 1)[0]) for i in range(12)]
+        if digits_only:
+            cnpj = [str(sample(self.digits, 1)[0]) for i in range(12)]
+        else:
+            cnpj = [sample(self.digits_and_letters, 1)[0] for i in range(12)]
 
         # Gerar os dígitos verificadores
         cnpj.append(self._generate_first_digit(cnpj))
@@ -47,7 +53,7 @@ class CNPJ(BaseDoc):
         sum = 0
 
         for i in range(12):
-            sum += int(doc[i]) * self.weights_first[i]
+            sum += (ord(str(doc[i])) - 48) * self.weights_first[i]
 
         sum = sum % 11
         sum = 0 if sum < 2 else 11 - sum
@@ -59,7 +65,7 @@ class CNPJ(BaseDoc):
         sum = 0
 
         for i in range(13):
-            sum += int(doc[i]) * self.weights_second[i]
+            sum += (ord(str(doc[i])) - 48) * self.weights_second[i]
 
         sum = sum % 11
         sum = 0 if sum < 2 else 11 - sum


Test patch:
diff --git a/tests/test_CNPJ.py b/tests/test_CNPJ.py
index cfcfe9e..f254f7a 100644
--- a/tests/test_CNPJ.py
+++ b/tests/test_CNPJ.py
@@ -70,3 +70,20 @@ class TestCnpj(unittest.TestCase):
 
         # THEN
         self.assertFalse(validate_return)
+
+    def test_alphanumeric(self):
+        """Testar o método validate do CNPJ em caso de caracteres alfanuméricos."""
+        # GIVEN
+        cases = [
+            ('12.aBc.345/01dE-35', True),
+            ('12ABC34501DE35', True),
+            ('12.ABC.345/01DE-34', False),
+        ]
+
+        # WHEN
+        results = []
+        for doc, is_valid in cases:
+            results.append(self.cnpj.validate(doc) == is_valid)
+
+        # THEN
+        self.assertTrue(all(results))