Files
gti d21a9d1140 Add 'fiscal/' from commit '102ba84500dbd9655a0471288c3e278652593ca9'
git-subtree-dir: fiscal
git-subtree-mainline: d95165fc8f
git-subtree-split: 102ba84500
2026-09-29 14:43:23 -03:00

76 lines
2.6 KiB
Python

"""Product identity: stable fingerprints and GTIN validation.
Global identity uses ONLY stable attributes (normalized description, brand, unit,
package quantity, GTIN). Fiscal/company context (CFOP, CSOSN, CST, rate, ST, tenant)
never participates: those vary per context and must not merge distinct knowledge.
"""
import hashlib
import re
import unicodedata
from ..classification import ENGINE_VERSION # noqa: F401 (re-exported for the AI layer)
def normalize_description(value) -> str:
"""NFKD -> ascii -> A-Z0-9 tokens, space-joined (same canonical form as classification)."""
text = unicodedata.normalize("NFKD", str(value or "")).encode("ascii", "ignore").decode().upper()
return " ".join(re.sub(r"[^A-Z0-9]+", " ", text).split())
def _normalize_token(value) -> str:
return normalize_description(value)
def _normalize_quantity(value) -> str:
digits = re.sub(r"[^0-9.,]", "", str(value or ""))
if not digits:
return ""
try:
number = float(digits.replace(",", "."))
except ValueError:
return digits
return f"{number:g}"
def product_fingerprint(description, brand=None, unit=None, package_quantity=None) -> str:
parts = [_normalize_token(description), _normalize_token(brand), _normalize_token(unit), _normalize_quantity(package_quantity)]
return hashlib.sha256("|".join(parts).encode("utf-8")).hexdigest()
def _stem(token: str) -> str:
"""Minimal Portuguese plural stemming so AGUAS matches AGUA / MINERAIS matches MINERAL."""
if len(token) > 4 and token.endswith("AIS"):
return token[:-3] + "AL"
if len(token) > 4 and token.endswith("ES"):
return token[:-2]
if len(token) > 3 and token.endswith("S"):
return token[:-1]
return token
def token_stems(value) -> set:
return {_stem(token) for token in normalize_description(value).split()}
def overlap_score(query, official) -> float:
"""Fraction of query description stems present in the official description stems."""
stems = token_stems(query)
if not stems:
return 0.0
return len(stems & token_stems(official)) / len(stems)
def validate_gtin(value) -> str | None:
"""Returns the canonical GTIN when the checksum is valid, else None.
Accepts GTIN-8/12/13/14 (EAN-8/UPC-12/EAN-13/GTIN-14)."""
digits = re.sub(r"\D", "", str(value or ""))
if len(digits) not in (8, 12, 13, 14):
return None
total = 0
for index, char in enumerate(reversed(digits)):
digit = int(char)
total += digit * (1 if index % 2 == 0 else 3)
if total % 10 != 0:
return None
return digits