Skip to content

Commit c95eaef

Browse files
committed
feat(cnpj): add alphanumeric CNPJ support per RFB Nota Técnica 49/2024
1 parent 5dc9585 commit c95eaef

1 file changed

Lines changed: 124 additions & 140 deletions

File tree

brutils/cnpj.py

Lines changed: 124 additions & 140 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,17 @@
11
from itertools import chain
2-
from random import randint
2+
from random import randint, choice
3+
import string
4+
5+
# Mapeamento alfanumérico: A=10, B=11, ..., Z=35
6+
_CHAR_VALUES = {c: i + 10 for i, c in enumerate(string.ascii_uppercase)}
37

48
# FORMATTING
59
############
610

711

812
def sieve(dirty: str) -> str:
913
"""
10-
Removes specific symbols from a CNPJ (Brazilian Company Registration
11-
Number) string.
12-
13-
This function takes a CNPJ string as input and removes all occurrences of
14-
the '.', '/' and '-' characters from it.
14+
Removes specific symbols from a CNPJ string.
1515
1616
Args:
1717
cnpj (str): The CNPJ string containing symbols to be removed.
@@ -22,67 +22,23 @@ def sieve(dirty: str) -> str:
2222
Example:
2323
>>> sieve("12.345/6789-01")
2424
"12345678901"
25-
>>> sieve("98/76.543-2101")
26-
"98765432101"
27-
28-
.. note::
29-
This method should not be used in new code and is only provided for
30-
backward compatibility.
3125
"""
32-
3326
return "".join(filter(lambda char: char not in "./-", dirty))
3427

3528

3629
def remove_symbols(dirty: str) -> str:
37-
"""
38-
This function is an alias for the `sieve` function, offering a more
39-
descriptive name.
40-
41-
Args:
42-
dirty (str): The dirty string containing symbols to be removed.
43-
44-
Returns:
45-
str: A new string with the specified symbols removed.
46-
47-
Example:
48-
>>> remove_symbols("12.345/6789-01")
49-
"12345678901"
50-
>>> remove_symbols("98/76.543-2101")
51-
"98765432101"
52-
"""
53-
30+
"""Alias for sieve()."""
5431
return sieve(dirty)
5532

5633

5734
def display(cnpj: str) -> str | None:
5835
"""
59-
Will format an adequately formatted numbers-only CNPJ string,
60-
adding in standard formatting visual aid symbols for display.
61-
62-
Formats a CNPJ (Brazilian Company Registration Number) string for
63-
visual display.
64-
65-
This function takes a CNPJ string as input, validates its format, and
66-
formats it with standard visual aid symbols for display purposes.
67-
68-
Args:
69-
cnpj (str): The CNPJ string to be formatted for display.
70-
71-
Returns:
72-
str: The formatted CNPJ with visual aid symbols if it's valid,
73-
None if it's not valid.
74-
75-
Example:
76-
>>> display("12345678901234")
77-
"12.345.678/9012-34"
78-
>>> display("98765432100100")
79-
"98.765.432/1001-00"
36+
Formats a CNPJ string for visual display (legacy, numeric only).
8037
8138
.. note::
8239
This method should not be used in new code and is only provided for
83-
backward compatibility.
40+
backward compatibility. Use format_cnpj() instead.
8441
"""
85-
8642
if not cnpj.isdigit() or len(cnpj) != 14 or len(set(cnpj)) == 1:
8743
return None
8844
return "{}.{}.{}/{}-{}".format(
@@ -92,29 +48,25 @@ def display(cnpj: str) -> str | None:
9248

9349
def format_cnpj(cnpj: str) -> str | None:
9450
"""
95-
Formats a CNPJ (Brazilian Company Registration Number) string for visual
96-
display.
51+
Formats a CNPJ string for visual display.
9752
98-
This function takes a CNPJ string as input, validates its format, and
99-
formats it with standard visual aid symbols for display purposes.
53+
Supports both the classic numeric format and the new alphanumeric format
54+
introduced by RFB Nota Técnica 49/2024 (effective July 2026).
10055
10156
Args:
102-
cnpj (str): The CNPJ string to be formatted for display.
57+
cnpj (str): The CNPJ string to be formatted (14 characters, no symbols).
10358
10459
Returns:
105-
str: The formatted CNPJ with visual aid symbols if it's valid,
106-
None if it's not valid.
60+
str: The formatted CNPJ if valid, None otherwise.
10761
10862
Example:
10963
>>> format_cnpj("03560714000142")
11064
'03.560.714/0001-42'
111-
>>> format_cnpj("98765432100100")
112-
None
65+
>>> format_cnpj("B3S30714000142")
66+
'B3.S30.714/0001-42'
11367
"""
114-
11568
if not is_valid(cnpj):
11669
return None
117-
11870
return "{}.{}.{}/{}-{}".format(
11971
cnpj[:2], cnpj[2:5], cnpj[5:8], cnpj[8:12], cnpj[12:14]
12072
)
@@ -124,50 +76,125 @@ def format_cnpj(cnpj: str) -> str | None:
12476
############
12577

12678

79+
def _char_to_val(c: str) -> int:
80+
"""
81+
Converts a CNPJ character to its numeric value for checksum calculation.
82+
83+
Digits map to their integer value; uppercase letters map to 10-35
84+
(A=10, B=11, ..., Z=35), as defined by RFB Nota Técnica 49/2024.
85+
86+
Args:
87+
c (str): A single character (digit or uppercase letter).
88+
89+
Returns:
90+
int: The numeric value of the character.
91+
"""
92+
if c.isdigit():
93+
return int(c)
94+
return _CHAR_VALUES[c.upper()]
95+
96+
97+
def _is_valid_chars(cnpj: str) -> bool:
98+
"""
99+
Checks if all characters in a CNPJ are valid (digits or uppercase letters).
100+
101+
Args:
102+
cnpj (str): The CNPJ string to check.
103+
104+
Returns:
105+
bool: True if all characters are valid, False otherwise.
106+
"""
107+
return all(c.isdigit() or c.upper() in _CHAR_VALUES for c in cnpj)
108+
109+
110+
def _hashdigit(cnpj: str, position: int) -> int:
111+
"""
112+
Calculates the checksum digit at the given position for the provided CNPJ.
113+
114+
Supports both numeric and alphanumeric CNPJs per RFB Nota Técnica 49/2024.
115+
116+
Args:
117+
cnpj (str): The CNPJ string.
118+
position (int): The position of the checksum digit (13 or 14).
119+
120+
Returns:
121+
int: The calculated checksum digit.
122+
"""
123+
weightgen = chain(range(position - 8, 1, -1), range(9, 1, -1))
124+
val = (
125+
sum(_char_to_val(c) * w for c, w in zip(cnpj, weightgen)) % 11
126+
)
127+
return 0 if val < 2 else 11 - val
128+
129+
130+
def _checksum(basenum: str) -> str:
131+
"""
132+
Calculates the verifying checksum digits for a given CNPJ base number.
133+
134+
Supports both numeric and alphanumeric base numbers.
135+
136+
Args:
137+
basenum (str): The 12-character CNPJ base number.
138+
139+
Returns:
140+
str: The two verifying checksum digits.
141+
"""
142+
d1 = str(_hashdigit(basenum, 13))
143+
d2 = str(_hashdigit(basenum + d1, 14))
144+
return d1 + d2
145+
146+
127147
def validate(cnpj: str) -> bool:
128148
"""
129-
Validates a CNPJ (Brazilian Company Registration Number) by comparing its
130-
verifying checksum digits to its base number.
149+
Validates a CNPJ by comparing its verifying checksum digits to its base.
131150
132-
This function checks the validity of a CNPJ by comparing its verifying
133-
checksum digits to its base number. The input should be a string of digits
134-
with the appropriate length.
151+
Supports both the classic numeric format and the new alphanumeric format
152+
introduced by RFB Nota Técnica 49/2024 (effective July 2026).
135153
136154
Args:
137-
cnpj (str): The CNPJ to be validated.
155+
cnpj (str): The CNPJ to be validated (14 characters, no symbols).
138156
139157
Returns:
140-
bool: True if the checksum digits match the base number,
141-
False otherwise.
158+
bool: True if valid, False otherwise.
142159
143160
Example:
144161
>>> validate("03560714000142")
145162
True
146163
>>> validate("00111222000133")
147164
False
165+
>>> validate("B3S30714000142")
166+
True
148167
149168
.. note::
150169
This method should not be used in new code and is only provided for
151170
backward compatibility.
152171
"""
153-
154-
if not cnpj.isdigit() or len(cnpj) != 14 or len(set(cnpj)) == 1:
172+
if (
173+
not isinstance(cnpj, str)
174+
or len(cnpj) != 14
175+
or not _is_valid_chars(cnpj)
176+
or len(set(cnpj.upper())) == 1
177+
):
155178
return False
156179
return all(
157-
_hashdigit(cnpj, i + 13) == int(v) for i, v in enumerate(cnpj[12:])
180+
_hashdigit(cnpj, i + 13) == int(cnpj[12 + i])
181+
for i in range(2)
158182
)
159183

160184

161185
def is_valid(cnpj: str) -> bool:
162186
"""
163-
Returns whether or not the verifying checksum digits of the given `cnpj`
164-
match its base number.
187+
Returns whether the verifying checksum digits of the given CNPJ match
188+
its base number.
189+
190+
Supports both the classic numeric format and the new alphanumeric format
191+
introduced by RFB Nota Técnica 49/2024 (effective July 2026).
165192
166193
This function does not verify the existence of the CNPJ; it only
167194
validates the format of the string.
168195
169196
Args:
170-
cnpj (str): The CNPJ to be validated, a 14-digit string
197+
cnpj (str): The CNPJ to be validated (14 characters, no symbols).
171198
172199
Returns:
173200
bool: True if the checksum digits match the base number,
@@ -178,85 +205,42 @@ def is_valid(cnpj: str) -> bool:
178205
True
179206
>>> is_valid("00111222000133")
180207
False
208+
>>> is_valid("B3S30714000142")
209+
True
181210
"""
182-
183211
return isinstance(cnpj, str) and validate(cnpj)
184212

185213

186-
def generate(branch: int = 1) -> str:
214+
def generate(branch: int = 1, alphanumeric: bool = False) -> str:
187215
"""
188-
Generates a random valid CNPJ digit string. An optional branch number
189-
parameter can be given; it defaults to 1.
216+
Generates a random valid CNPJ string.
190217
191218
Args:
192-
branch (int): An optional branch number to be included in the CNPJ.
219+
branch (int): An optional branch number (default: 1).
220+
alphanumeric (bool): If True, generates a new alphanumeric CNPJ
221+
per RFB Nota Técnica 49/2024. Default: False.
193222
194223
Returns:
195224
str: A randomly generated valid CNPJ string.
196225
197226
Example:
198227
>>> generate()
199228
"30180536000105"
200-
>>> generate(1234)
201-
"01745284123455"
229+
>>> generate(alphanumeric=True)
230+
"B3S30714000142"
202231
"""
203-
204232
branch %= 10000
205233
branch += int(branch == 0)
206234
branch = str(branch).zfill(4)
207-
base = str(randint(0, 99999999)).zfill(8) + branch
208235

209-
return base + _checksum(base)
236+
if alphanumeric:
237+
charset = string.digits + string.ascii_uppercase
238+
base = "".join(choice(charset) for _ in range(8)) + branch
239+
# Ensure at least one letter in the base (distinguishes from numeric)
240+
if base[:8].isdigit():
241+
pos = randint(0, 7)
242+
base = base[:pos] + choice(string.ascii_uppercase) + base[pos + 1:]
243+
else:
244+
base = str(randint(0, 99999999)).zfill(8) + branch
210245

211-
212-
def _hashdigit(cnpj: str, position: int) -> int:
213-
"""
214-
Calculates the checksum digit at the given `position` for the provided
215-
`cnpj`. The input must contain all elements before `position`.
216-
217-
Args:
218-
cnpj (str): The CNPJ for which the checksum digit is calculated.
219-
position (int): The position of the checksum digit to be calculated.
220-
221-
Returns:
222-
int: The calculated checksum digit.
223-
224-
Example:
225-
>>> _hashdigit("12345678901234", 13)
226-
3
227-
>>> _hashdigit("98765432100100", 14)
228-
9
229-
"""
230-
231-
weightgen = chain(range(position - 8, 1, -1), range(9, 1, -1))
232-
val = (
233-
sum(int(digit) * weight for digit, weight in zip(cnpj, weightgen)) % 11
234-
)
235-
return 0 if val < 2 else 11 - val
236-
237-
238-
def _checksum(basenum: str) -> str:
239-
"""
240-
Calculates the verifying checksum digits for a given CNPJ base number.
241-
242-
This function computes the verifying checksum digits for a provided CNPJ
243-
base number. The `basenum` should be a digit-string of the appropriate
244-
length.
245-
246-
Args:
247-
basenum (str): The base number of the CNPJ for which verifying checksum
248-
digits are calculated.
249-
250-
Returns:
251-
str: The verifying checksum digits.
252-
253-
Example:
254-
>>> _checksum("123456789012")
255-
"30"
256-
>>> _checksum("987654321001")
257-
"41"
258-
"""
259-
260-
verifying_digits = str(_hashdigit(basenum, 13))
261-
verifying_digits += str(_hashdigit(basenum + verifying_digits, 14))
262-
return verifying_digits
246+
return base + _checksum(base)

0 commit comments

Comments
 (0)