Mejoras de sesion: estabilizacion, IA, Nexus Yonke y marketplace B2B
Some checks failed
CI / lint-and-test (3.11) (push) Has been cancelled
CI / lint-and-test (3.13) (push) Has been cancelled

- 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:
2026-10-12 00:57:31 +00:00
parent f2d4558b80
commit a386ca5b4a
165 changed files with 31313 additions and 2203 deletions

View File

@@ -63,6 +63,28 @@ def _format_rate(val):
return str(_to_dec(val).quantize(SIX, ROUND_HALF_UP))
def _item_sat_keys(item):
"""Return (clave_prod_serv, clave_unidad), falling back to SAT rules.
Precedence: the value carried by the sale item, then the rule engine over
the item name/brand/category (``services.sat_keys``), then the historic
hard-coded default. Never raises, so a CFDI can always be built.
"""
cps = item.get('clave_prod_serv')
cu = item.get('clave_unidad')
if cps and cu:
return cps, cu
try:
from services import sat_keys
k, u, _src = sat_keys.resolve_key(
name=item.get('name'), brand=item.get('brand'),
category=item.get('category'), part_number=item.get('part_number'),
)
except Exception:
k, u = None, None
return (cps or k or '25174800'), (cu or u or 'H87')
def _make_element(parent, tag, attribs=None, ns=CFDI_NS):
"""Create a subelement with the given namespace."""
elem = etree.SubElement(parent, f'{{{ns}}}{tag}')
@@ -83,6 +105,9 @@ def build_ingreso_xml(sale, tenant_config, customer=None):
items: [{part_number, name, quantity, unit_price, discount_amount,
tax_rate, tax_amount, subtotal,
clave_prod_serv, clave_unidad}]
(clave_prod_serv / clave_unidad are optional: when absent they are
resolved by services.sat_keys rules over name/brand/category,
falling back to 25174800 / H87)
tenant_config: dict with keys:
rfc, razon_social, regimen_fiscal, cp (codigo postal),
serie (optional), nombre_comercial (optional)
@@ -175,10 +200,10 @@ def build_ingreso_xml(sale, tenant_config, customer=None):
base = (importe - discount_amount).quantize(TWO, ROUND_HALF_UP)
concepto = _make_element(conceptos, 'Concepto')
concepto.set('ClaveProdServ', item.get('clave_prod_serv') or '25174800')
concepto.set('ClaveProdServ', _item_sat_keys(item)[0])
concepto.set('NoIdentificacion', item.get('part_number') or '')
concepto.set('Cantidad', str(qty))
concepto.set('ClaveUnidad', item.get('clave_unidad') or 'H87')
concepto.set('ClaveUnidad', _item_sat_keys(item)[1])
concepto.set('Unidad', 'PZA')
concepto.set('Descripcion', item.get('name') or 'Autoparte')
concepto.set('ValorUnitario', _format_amount(unit_price))
@@ -301,10 +326,10 @@ def build_egreso_xml(sale, tenant_config, customer, original_uuid):
base = (importe - discount_amount).quantize(TWO, ROUND_HALF_UP)
concepto = _make_element(conceptos, 'Concepto')
concepto.set('ClaveProdServ', item.get('clave_prod_serv') or '25174800')
concepto.set('ClaveProdServ', _item_sat_keys(item)[0])
concepto.set('NoIdentificacion', item.get('part_number') or '')
concepto.set('Cantidad', str(qty))
concepto.set('ClaveUnidad', item.get('clave_unidad') or 'H87')
concepto.set('ClaveUnidad', _item_sat_keys(item)[1])
concepto.set('Unidad', 'PZA')
concepto.set('Descripcion', item.get('name') or 'Autoparte')
concepto.set('ValorUnitario', _format_amount(unit_price))
@@ -556,10 +581,10 @@ def build_global_invoice_xml(sales, tenant_config, year, month):
base = (importe - discount_amount).quantize(TWO, ROUND_HALF_UP)
concepto = _make_element(conceptos, 'Concepto')
concepto.set('ClaveProdServ', item.get('clave_prod_serv') or '25174800')
concepto.set('ClaveProdServ', _item_sat_keys(item)[0])
concepto.set('NoIdentificacion', item.get('part_number') or str(sale['id']))
concepto.set('Cantidad', str(qty))
concepto.set('ClaveUnidad', item.get('clave_unidad') or 'H87')
concepto.set('ClaveUnidad', _item_sat_keys(item)[1])
concepto.set('Unidad', 'PZA')
concepto.set('Descripcion', item.get('name') or 'Autoparte')
concepto.set('ValorUnitario', _format_amount(unit_price))