66 lines
2.6 KiB
Python
66 lines
2.6 KiB
Python
"""Validador local determinístico de GTIN / EAN (GS1 Standard).
|
|
|
|
Sem chamadas externas, sem IA. Implementa:
|
|
1. Validação de formato (GTIN-8, GTIN-12, GTIN-13, GTIN-14);
|
|
2. Rejeição de placeholders (zeros, sequências óbvias, códigos internos de balança restrita);
|
|
3. Verificação do dígito verificador oficial GS1 (módulo 10 com pesos alternados 3 e 1);
|
|
4. Validação de faixas de prefixo GS1 reconhecidas.
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
|
|
def _digits(value: str | None) -> str:
|
|
return "".join(c for c in str(value or "") if c.isdigit())
|
|
|
|
|
|
# Prefixos de circulação restrita / uso interno de loja (não são GTINs globais válidos para KB nacional)
|
|
# Faixas 020-029 e 200-299 são reservadas pelo GS1 para distribuição restrita (itens pesáveis, balança de loja)
|
|
INTERNAL_STORE_PREFIXES = tuple(str(p) for p in range(20, 30)) + tuple(str(p) for p in range(200, 300))
|
|
|
|
|
|
def calculate_gs1_check_digit(digits_without_dv: str) -> int:
|
|
"""Calcula o dígito verificador padrão GS1 pelo algoritmo oficial módulo 10."""
|
|
total = 0
|
|
# Da direita para a esquerda, os fatores alternam entre 3 e 1
|
|
# Para o penúltimo dígito (imediatamente antes do DV), o peso é sempre 3
|
|
weight = 3
|
|
for char in reversed(digits_without_dv):
|
|
total += int(char) * weight
|
|
weight = 1 if weight == 3 else 3
|
|
remainder = total % 10
|
|
return 0 if remainder == 0 else (10 - remainder)
|
|
|
|
|
|
def validate_gtin(value: str | None) -> tuple[bool, str]:
|
|
"""Valida um código GTIN.
|
|
|
|
Retorna (is_valid, reason).
|
|
"""
|
|
if not value:
|
|
return False, "GTIN_EMPTY"
|
|
|
|
cleaned = _digits(value)
|
|
|
|
if len(cleaned) not in (8, 12, 13, 14):
|
|
return False, f"GTIN_INVALID_LENGTH_{len(cleaned)}"
|
|
|
|
# Rejeita dígitos todos iguais (ex: 00000000, 1111111111111, 9999999999999)
|
|
if len(set(cleaned)) == 1:
|
|
return False, "GTIN_PLACEHOLDER_REPEATED_DIGITS"
|
|
|
|
# Rejeita prefixos de uso interno / balança
|
|
# Normalizado para 13 dígitos para verificar o prefixo
|
|
padded_13 = cleaned.zfill(13) if len(cleaned) < 14 else cleaned[1:]
|
|
prefix_3 = padded_13[:3]
|
|
prefix_2 = padded_13[:2]
|
|
if prefix_3 in INTERNAL_STORE_PREFIXES or prefix_2 in INTERNAL_STORE_PREFIXES:
|
|
return False, "GTIN_RESTRICTED_INTERNAL_STORE_PREFIX"
|
|
|
|
# Validação do Dígito Verificador
|
|
expected_dv = calculate_gs1_check_digit(cleaned[:-1])
|
|
actual_dv = int(cleaned[-1])
|
|
if actual_dv != expected_dv:
|
|
return False, f"GTIN_INVALID_CHECK_DIGIT_EXPECTED_{expected_dv}_GOT_{actual_dv}"
|
|
|
|
return True, "GTIN_VALID"
|