32 lines
1.8 KiB
Python
32 lines
1.8 KiB
Python
"""Deterministic GTIN normalization and validation (GTIN-8, UPC/GTIN-12, EAN/GTIN-13, GTIN-14)."""
|
|
import re
|
|
|
|
VALID, INVALID, MISSING, RESTRICTED = "VALID_GTIN", "INVALID_GTIN", "MISSING_GTIN", "RESTRICTED_GTIN"
|
|
|
|
|
|
def check_digit_ok(digits: str) -> bool:
|
|
body, check = digits[:-1], int(digits[-1])
|
|
total = sum(int(d) * (3 if i % 2 == 0 else 1) for i, d in enumerate(reversed(body)))
|
|
return (10 - total % 10) % 10 == check
|
|
|
|
|
|
def normalize_gtin(raw) -> dict:
|
|
"""Returns {status, gtin (digits as typed), key (zero-padded to 14: the KB identity), reason}. Never calls anything external.
|
|
RESTRICTED: EAN prefixes 02, 04 and 20-29 are store-internal / restricted circulation (not a global product identity), so no provider is asked."""
|
|
text = "" if raw is None else str(raw).strip()
|
|
if not text or text.upper() in ("SEM GTIN", "SEM EAN", "0", "NULL"):
|
|
return {"status": MISSING, "gtin": None, "key": None, "reason": "EMPTY"}
|
|
digits = re.sub(r"\D", "", text)
|
|
if not digits or len(digits) not in (8, 12, 13, 14):
|
|
return {"status": INVALID, "gtin": digits or None, "key": None, "reason": "BAD_LENGTH"}
|
|
if len(set(digits)) == 1:
|
|
return {"status": INVALID, "gtin": digits, "key": None, "reason": "REPEATED_DIGITS"}
|
|
if not check_digit_ok(digits):
|
|
return {"status": INVALID, "gtin": digits, "key": None, "reason": "BAD_CHECK_DIGIT"}
|
|
key = digits.zfill(14)
|
|
prefix = key[1:3] if len(digits) == 14 else key[1:3] # key = 0 + EAN-13 layout for 12/13; GTIN-14 keeps its indicator digit in key[0]
|
|
ean13 = key[1:]
|
|
if ean13[:2] in ("02", "04") or "20" <= ean13[:2] <= "29":
|
|
return {"status": RESTRICTED, "gtin": digits, "key": key, "reason": "STORE_INTERNAL_PREFIX"}
|
|
return {"status": VALID, "gtin": digits, "key": key, "reason": None}
|