Mejoras de sesion: estabilizacion, IA, Nexus Yonke y marketplace B2B
- F0 blindaje: Redis/Celery, pg_hba sin trust LAN, secretos a env 600, backups, webhook auth, jobs muertos - F1 caja: bugs POS, reembolsos al corte, apartados, CFDI, topes de descuento en cotizacion/apartado - F2/F7 IA de catalogo: autocategorizacion, dedup de SKUs con embeddings, OCR de facturas, descripciones, claves SAT, limpieza de fitment - F3 IA en el flujo: vision Yonke, preguntas MeLi, venta cruzada, anomalias, sinonimos, busqueda semantica (rerank/embeddings) - F4/F8 escala y cartera: /metrics+Prometheus+Grafana, pool, score de credito, churn, metricas multi-tenant - Nexus Yonke (M1-M6): modelo de datos 1-de-1, alta por donador, vitrina publica, MeLi usado, consignacion, garantia por dias, reportes, etiquetas - Marketplace B2B entre yonkes (catalogo + pedidos cross-tenant) - Seguridad: auth en /api/admin/* (35 endpoints), alertas por tenant - Migraciones v4.27/v4.28; timers expire-demos y rollup-metrics
This commit is contained in:
338
pos/services/sat_keys.py
Normal file
338
pos/services/sat_keys.py
Normal file
@@ -0,0 +1,338 @@
|
||||
# /home/Autopartes/pos/services/sat_keys.py
|
||||
"""SAT (CFDI 4.0) product/unit key resolution for auto parts.
|
||||
|
||||
Assigns ``clave_prod_serv`` (``c_ClaveProdServ``) and ``clave_unidad``
|
||||
(``c_ClaveUnidad``) per item from the part name/brand/category using, in order:
|
||||
|
||||
1. ``RULES`` — longest keyword hit over the normalized Spanish name.
|
||||
2. ``CANONICAL_BY_CATEGORY`` — a canonical category (from
|
||||
``product_categorizer``) mapped to a code.
|
||||
3. an optional LLM fallback (``ai_chat.classify_part`` → category → rule).
|
||||
4. ``DEFAULT_*`` (``25174800`` / ``H87``) — the current hard-coded default.
|
||||
|
||||
Codes come from the public SAT group ``2517`` (autopartes) suggestions; see
|
||||
scripts/assign_sat_keys.py for provenance. **Never raises**: any missing
|
||||
template/LLM failure degrades to ``DEFAULT_*`` so the CFDI pipeline cannot
|
||||
break because of this module.
|
||||
|
||||
Public API:
|
||||
resolve_key(name=None, brand=None, category=None, canonical=None,
|
||||
part_number=None, use_llm=False) -> (clave_prod_serv, clave_unidad, source)
|
||||
"""
|
||||
|
||||
import re
|
||||
import unicodedata
|
||||
|
||||
DEFAULT_KEY = "25174800" # Componentes y sistemas de vehículos / autopartes
|
||||
DEFAULT_UNIT = "H87" # Pieza
|
||||
|
||||
# Canonical Spanish categories (product_categorizer.CANONICAL_*) -> SAT key.
|
||||
CANONICAL_BY_CATEGORY = {
|
||||
"Frenos": "25171700",
|
||||
"Baleros y Mazas": "25171900",
|
||||
"Suspensión": "25172000",
|
||||
"Dirección": "25172000",
|
||||
"Motor": "26101700",
|
||||
"Enfriamiento": "25174000",
|
||||
"Encendido": "25173900",
|
||||
"Filtros": "40161500",
|
||||
"Filtros y Encendido": "40161500",
|
||||
"Eléctrico": "25173900",
|
||||
"Combustible y Emisiones": "25172400",
|
||||
"Aire Acondicionado": "40101800",
|
||||
"Transmisión": "26111800",
|
||||
"Clutch": "26111900",
|
||||
"Tren Motriz": "25173800",
|
||||
"Escape": "25173700",
|
||||
"Luces": "25172900",
|
||||
"Limpiaparabrisas": "25171500",
|
||||
"Luces y Limpiaparabrisas": "25172900",
|
||||
"Carrocería": "25172600",
|
||||
"Lubricantes y Químicos": "15121500",
|
||||
"Llantas y Accesorios": "25172500",
|
||||
"Otros": DEFAULT_KEY,
|
||||
}
|
||||
|
||||
# Keyword rules over the normalized name. ORDER MATTERS (first hit wins), so
|
||||
# more specific terms go first. Keys are already normalized (uppercase,
|
||||
# accent-stripped, single-spaced).
|
||||
RULES = [
|
||||
# ── Exhaust / emissions ────────────────────────────────────────────────
|
||||
("catalizador", "12161600"),
|
||||
("convertidor catalitico", "12161600"),
|
||||
("mofle", "25173700"),
|
||||
("silenciador", "25173700"),
|
||||
("sistema de escape", "25173700"),
|
||||
("tubo de escape", "25173700"),
|
||||
("linea de escape", "25173700"),
|
||||
("multiple de escape", "25173700"),
|
||||
("sensor de oxigeno", "25173700"),
|
||||
# ── Clutch ─────────────────────────────────────────────────────────────
|
||||
("kit de clutch", "26111900"),
|
||||
("embrague", "26111900"),
|
||||
("clutch", "26111900"),
|
||||
("plato de clutch", "26112000"),
|
||||
("disco de clutch", "26112000"),
|
||||
# ── Batteries ──────────────────────────────────────────────────────────
|
||||
("bateria", "26111700"),
|
||||
("acumulador", "26111700"),
|
||||
# ── Tires / wheels ─────────────────────────────────────────────────────
|
||||
("llanta", "25172500"),
|
||||
("neumatico", "25172500"),
|
||||
("rin", "25172500"),
|
||||
# ── Wipers ─────────────────────────────────────────────────────────────
|
||||
("limpiaparabrisas", "25171500"),
|
||||
("pluma limpia", "25171500"),
|
||||
("wiper", "25171500"),
|
||||
("brazo limpiaparabrisas", "25171500"),
|
||||
# ── Suspension / steering ──────────────────────────────────────────────
|
||||
("amortiguador", "25172000"),
|
||||
("horquilla", "25172000"),
|
||||
("rotula", "25172000"),
|
||||
("bieleta", "25172000"),
|
||||
("espiga", "25172000"),
|
||||
("base amortiguador", "25172000"),
|
||||
("resorte", "25172000"),
|
||||
("suspension", "25172000"),
|
||||
("cremallera", "25172000"),
|
||||
("terminal de direccion", "25172000"),
|
||||
("barra de direccion", "25172000"),
|
||||
("direccion", "25172000"),
|
||||
# ── Brakes ─────────────────────────────────────────────────────────────
|
||||
("balata", "25171700"),
|
||||
("pastilla", "25171700"),
|
||||
("disco de freno", "25171700"),
|
||||
("tambor", "25171700"),
|
||||
("cilindro de freno", "25171700"),
|
||||
("freno", "25171700"),
|
||||
("birlo", "25171700"),
|
||||
# ── Wheel bearings / hubs ──────────────────────────────────────────────
|
||||
("maza", "25171900"),
|
||||
("balero", "25171900"),
|
||||
("husillo", "25171900"),
|
||||
# ── Cooling ────────────────────────────────────────────────────────────
|
||||
("radiador", "25174000"),
|
||||
("termostato", "25174000"),
|
||||
("bomba de agua", "25174000"),
|
||||
("manguera", "25174000"),
|
||||
("banda", "25174000"),
|
||||
("polea", "25174000"),
|
||||
("motoventilador", "25174000"),
|
||||
("ventilador", "25174000"),
|
||||
("refrigerante", "25174000"),
|
||||
("anticongelante", "25174000"),
|
||||
# ── Filters ────────────────────────────────────────────────────────────
|
||||
("filtro", "40161500"),
|
||||
# ── Electrical / ignition ──────────────────────────────────────────────
|
||||
("bujia", "25173900"),
|
||||
("cable bujia", "25173900"),
|
||||
("bobina", "25173900"),
|
||||
("alternador", "25173900"),
|
||||
("arrancador", "25173900"),
|
||||
("marcha", "25173900"),
|
||||
("modulo", "25173900"),
|
||||
("regulador", "25173900"),
|
||||
("relevador", "25173900"),
|
||||
("fusible", "25173900"),
|
||||
("sensor", "25173900"),
|
||||
("switch", "25173900"),
|
||||
("bomba de gasolina", "25172400"),
|
||||
("inyector", "25172400"),
|
||||
("cuerpo de aceleracion", "25172400"),
|
||||
("carburador", "25172400"),
|
||||
# ── Engine internals / gaskets ─────────────────────────────────────────
|
||||
("empaque", "26101700"),
|
||||
("junta", "26101700"),
|
||||
("sello", "26101700"),
|
||||
("reten", "26101700"),
|
||||
("piston", "26101700"),
|
||||
("valvula", "26101700"),
|
||||
("bomba de aceite", "26101700"),
|
||||
("arbol de levas", "26101700"),
|
||||
("cigue", "26101700"),
|
||||
# ── Lighting ───────────────────────────────────────────────────────────
|
||||
("faro", "25172900"),
|
||||
("calavera", "25172900"),
|
||||
("direccional", "25172900"),
|
||||
("tulipa", "25172900"),
|
||||
("foco", "25172900"),
|
||||
("luces", "25172900"),
|
||||
("luz", "25172900"),
|
||||
# ── Body / glass ───────────────────────────────────────────────────────
|
||||
("espejo", "25172600"),
|
||||
("defensa", "25172600"),
|
||||
("salpicadera", "25172600"),
|
||||
("cofre", "25172600"),
|
||||
("puerta", "25172200"),
|
||||
("parabrisas", "25172300"),
|
||||
("cristal", "25172300"),
|
||||
# ── Drivetrain ─────────────────────────────────────────────────────────
|
||||
("homocinetica", "25173800"),
|
||||
("tripode", "25173800"),
|
||||
("flecha", "25173800"),
|
||||
("transmision", "26111800"),
|
||||
("diferencial", "25173800"),
|
||||
("cople", "25173800"),
|
||||
# ── Consumables ────────────────────────────────────────────────────────
|
||||
("aceite", "15121500"),
|
||||
("grasa", "15121500"),
|
||||
("aditivo", "15121500"),
|
||||
("lubricante", "15121500"),
|
||||
("silicon", "15121500"),
|
||||
("sellador", "15121500"),
|
||||
("spray", "15121500"),
|
||||
("limpiador", "15121500"),
|
||||
# ── Tools / accessories ────────────────────────────────────────────────
|
||||
("herramienta", "25191700"),
|
||||
("desarmador", "25191700"),
|
||||
("dado", "25191700"),
|
||||
("gato hidraul", "25191700"),
|
||||
# ── English aliases (supplier catalogs ship English names) ─────────────
|
||||
("air filter", "40161500"),
|
||||
("oil filter", "40161500"),
|
||||
("fuel filter", "40161500"),
|
||||
("cabin filter", "40161500"),
|
||||
("filter", "40161500"),
|
||||
("water pump", "25174000"),
|
||||
("fuel pump", "25172400"),
|
||||
("power steering pump", "25172000"),
|
||||
("oil pump", "26101700"),
|
||||
("spark plug", "25173900"),
|
||||
("ignition", "25173900"),
|
||||
("muffler", "25173700"),
|
||||
("exhaust", "25173700"),
|
||||
("catalytic", "12161600"),
|
||||
("brake", "25171700"),
|
||||
("caliper", "25171700"),
|
||||
("rotor", "25171700"),
|
||||
("shoe", "25171700"),
|
||||
("drum", "25171700"),
|
||||
("pad", "25171700"),
|
||||
("alternator", "25173900"),
|
||||
("starter", "25173900"),
|
||||
("relay", "25173900"),
|
||||
("fuse", "25173900"),
|
||||
("cable", "25173900"),
|
||||
("injector", "25172400"),
|
||||
("battery", "26111700"),
|
||||
("radiator", "25174000"),
|
||||
("thermostat", "25174000"),
|
||||
("hose", "25174000"),
|
||||
("belt", "25174000"),
|
||||
("shock", "25172000"),
|
||||
("strut", "25172000"),
|
||||
("control arm", "25172000"),
|
||||
("ball joint", "25172000"),
|
||||
("tie rod", "25172000"),
|
||||
("bearing", "25171900"),
|
||||
("hub", "25171900"),
|
||||
("cv joint", "25173800"),
|
||||
("axle", "25173800"),
|
||||
("driveshaft", "25173800"),
|
||||
("transmission", "26111800"),
|
||||
("gasket", "26101700"),
|
||||
("valve", "26101700"),
|
||||
("piston", "26101700"),
|
||||
("engine mount", "26101700"),
|
||||
("motor mount", "26101700"),
|
||||
("mirror", "25172600"),
|
||||
("bumper", "25172600"),
|
||||
("fender", "25172600"),
|
||||
("door", "25172200"),
|
||||
("windshield", "25172300"),
|
||||
("glass", "25172300"),
|
||||
("lamp", "25172900"),
|
||||
("light", "25172900"),
|
||||
("wiper", "25171500"),
|
||||
("tire", "25172500"),
|
||||
("wheel", "25172500"),
|
||||
("rim", "25172500"),
|
||||
# ── Generic fallbacks (kept last) ──────────────────────────────────────
|
||||
("motor", "26101700"),
|
||||
("bomba", "26101700"),
|
||||
]
|
||||
|
||||
|
||||
def _norm(s):
|
||||
s = unicodedata.normalize("NFD", str(s or ""))
|
||||
s = s.encode("ascii", "ignore").decode("ascii")
|
||||
s = re.sub(r"[^A-Za-z0-9]+", " ", s).upper().strip()
|
||||
return " ".join(s.split())
|
||||
|
||||
|
||||
# RULES keys normalized once (uppercase) so they compare against _norm(name).
|
||||
_RULES_NORM = [(_norm(k), code) for k, code in RULES]
|
||||
|
||||
|
||||
def resolve_by_rules(name):
|
||||
"""Return a SAT key from the keyword rules, or ``None``.
|
||||
|
||||
Single-token keys match on token prefix (so ``freno`` also catches
|
||||
``frenos``); multi-token keys match as a contiguous substring.
|
||||
"""
|
||||
n = _norm(name)
|
||||
if not n:
|
||||
return None
|
||||
padded = " " + n + " "
|
||||
tokens = n.split()
|
||||
for key, code in _RULES_NORM:
|
||||
if " " in key:
|
||||
if " " + key + " " in padded:
|
||||
return code
|
||||
elif any(
|
||||
t == key or t.startswith(key)
|
||||
or (len(t) >= 5 and key.startswith(t))
|
||||
for t in tokens
|
||||
):
|
||||
return code
|
||||
return None
|
||||
|
||||
|
||||
def resolve_key(name=None, brand=None, canonical=None, category=None,
|
||||
part_number=None, use_llm=False, timeout=None):
|
||||
"""Resolve ``(clave_prod_serv, clave_unidad, source)`` for one item.
|
||||
|
||||
``source`` ∈ {'rules', 'category', 'llm', 'default'}. Never raises.
|
||||
"""
|
||||
# 1. keyword rules over name (fall back to part_number if no name)
|
||||
code = resolve_by_rules(name) or resolve_by_rules(part_number)
|
||||
if code:
|
||||
return code, DEFAULT_UNIT, "rules"
|
||||
|
||||
# 2. canonical category (either passed directly or as free text)
|
||||
for cat in (canonical, category):
|
||||
if not cat:
|
||||
continue
|
||||
hit = CANONICAL_BY_CATEGORY.get(str(cat).strip())
|
||||
if hit:
|
||||
return hit, DEFAULT_UNIT, "category"
|
||||
|
||||
# 3. optional LLM fallback: classify_part -> category -> rule/map
|
||||
if use_llm:
|
||||
llm_code = _llm_key(part_number or name, brand, timeout=timeout)
|
||||
if llm_code:
|
||||
return llm_code, DEFAULT_UNIT, "llm"
|
||||
|
||||
return DEFAULT_KEY, DEFAULT_UNIT, "default"
|
||||
|
||||
|
||||
def _llm_key(part_number, brand=None, timeout=None):
|
||||
"""Ask ai_chat.classify_part for the category and map it. Never raises."""
|
||||
if not part_number:
|
||||
return None
|
||||
try:
|
||||
from services.ai_chat import classify_part
|
||||
res = classify_part(str(part_number))
|
||||
except Exception:
|
||||
return None
|
||||
if not res:
|
||||
return None
|
||||
raw = (res.get("category") or "").strip()
|
||||
if not raw:
|
||||
return None
|
||||
hit = CANONICAL_BY_CATEGORY.get(raw)
|
||||
if hit:
|
||||
return hit
|
||||
# The LLM may answer in free text ("Sistema de Frenos"): try the name rules
|
||||
# and then the canonical map over its own words.
|
||||
return resolve_by_rules(raw)
|
||||
Reference in New Issue
Block a user