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:
@@ -69,6 +69,27 @@ def _resolve_metodo_pago(sale):
|
||||
return "PUE"
|
||||
|
||||
|
||||
def _item_sat_keys(item):
|
||||
"""Return (clave_prod_serv, clave_unidad) with SAT-rule fallback.
|
||||
|
||||
Precedence: sale-item value, then ``services.sat_keys`` rules over
|
||||
name/brand/category, then the historic hard-coded default. Never raises.
|
||||
"""
|
||||
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 _build_items(sale_items):
|
||||
items = []
|
||||
for item in sale_items or []:
|
||||
@@ -76,12 +97,13 @@ def _build_items(sale_items):
|
||||
unit_price = _to_dec(item.get("unit_price", 0))
|
||||
discount = _to_dec(item.get("discount_amount", 0))
|
||||
tax_rate = _to_dec(item.get("tax_rate", "0.16"))
|
||||
product_key, unit_key = _item_sat_keys(item)
|
||||
|
||||
# Facturapi price is unit price before taxes and discounts
|
||||
product = {
|
||||
"description": item.get("name") or "Autoparte",
|
||||
"product_key": item.get("clave_prod_serv") or "25174800",
|
||||
"unit_key": item.get("clave_unidad") or "H87",
|
||||
"product_key": product_key,
|
||||
"unit_key": unit_key,
|
||||
"unit_name": "Pieza",
|
||||
"price": _fmt2(unit_price),
|
||||
"tax_included": False,
|
||||
@@ -136,65 +158,91 @@ def build_ingreso_payload(sale, tenant_config, customer=None):
|
||||
"folio_number": sale["id"],
|
||||
}
|
||||
|
||||
# Optional exchange rate for USD
|
||||
if sale.get("currency") and sale["currency"] != "MXN" and sale.get("exchange_rate"):
|
||||
payload["exchange"] = _fmt6(sale["exchange_rate"])
|
||||
payload["currency"] = sale["currency"]
|
||||
# Optional currency/exchange rate for non-MXN sales
|
||||
currency = (sale.get("currency") or "MXN").upper()
|
||||
if currency != "MXN":
|
||||
payload["currency"] = currency
|
||||
if sale.get("exchange_rate"):
|
||||
payload["exchange"] = _fmt6(sale["exchange_rate"])
|
||||
|
||||
return payload
|
||||
|
||||
|
||||
def build_egreso_payload(sale, tenant_config, customer, original_uuid):
|
||||
def build_egreso_payload(sale, tenant_config, customer, original_uuid, folio_number=None, idempotency_key=None):
|
||||
"""Build Facturapi payload for a credit note (Comprobante tipo Egreso)."""
|
||||
payload = build_ingreso_payload(sale, tenant_config, customer)
|
||||
payload["type"] = "E"
|
||||
payload["related_documents"] = [{"relationship": "01", "documents": [original_uuid]}]
|
||||
payload["use"] = "G02" # Devoluciones, descuentos o bonificaciones
|
||||
payload["payment_method"] = "PUE"
|
||||
payload["related_documents"] = [{"relationship": "01", "documents": original_uuid}]
|
||||
if folio_number is not None:
|
||||
payload["folio_number"] = folio_number
|
||||
if idempotency_key:
|
||||
payload["idempotency_key"] = idempotency_key
|
||||
return payload
|
||||
|
||||
|
||||
def build_pago_payload(payment, tenant_config, customer, original_uuid):
|
||||
"""Build Facturapi payload for a payment complement (Comprobante tipo Pago)."""
|
||||
def _normalize_payment_date(value):
|
||||
"""Return the ISO8601 datetime string Facturapi expects for a payment."""
|
||||
if not value:
|
||||
return datetime.now().strftime("%Y-%m-%dT%H:%M:%S")
|
||||
text = str(value).replace(" ", "T")
|
||||
if len(text) == 10:
|
||||
return f"{text}T12:00:00"
|
||||
return text[:19]
|
||||
|
||||
|
||||
def build_pago_payload(payment, tenant_config, customer, original_uuid, idempotency_key=None):
|
||||
"""Build Facturapi payload for a payment complement (Comprobante tipo Pago).
|
||||
|
||||
`complements[0].data` is a list of payments (Facturapi JS SDK v5 types),
|
||||
each payment carrying its own related_documents with the installment number
|
||||
and the previous balance of the linked PPD invoice.
|
||||
"""
|
||||
tenant_cp = tenant_config.get("cp", "00000")
|
||||
customer_payload = _build_customer_payload(customer, tenant_cp)
|
||||
|
||||
amount = _to_dec(payment.get("amount", 0))
|
||||
base = (amount / Decimal("1.16")).quantize(TWO, ROUND_HALF_UP)
|
||||
|
||||
payment_date = payment.get("date") or datetime.now().strftime("%Y-%m-%dT%H:%M:%S")
|
||||
if "T" not in str(payment_date):
|
||||
payment_date = f"{payment_date}T12:00:00"
|
||||
rate = _to_dec(payment.get("tax_rate") or "0.16") or Decimal("0.16")
|
||||
base = (amount / (Decimal("1") + rate)).quantize(TWO, ROUND_HALF_UP)
|
||||
|
||||
forma_pago = FORMA_PAGO_MAP.get((payment.get("payment_method") or "").lower().strip(), "01")
|
||||
|
||||
payload = {
|
||||
"type": "P",
|
||||
"customer": customer_payload,
|
||||
"payment_form": forma_pago,
|
||||
"complements": [
|
||||
{
|
||||
"type": "pago",
|
||||
"data": {
|
||||
"payment_form": forma_pago,
|
||||
"payment_date": payment_date,
|
||||
"amount": _fmt2(amount),
|
||||
"related_documents": [
|
||||
{
|
||||
"uuid": original_uuid,
|
||||
"amount": _fmt2(amount),
|
||||
"taxes": [
|
||||
{
|
||||
"type": "IVA",
|
||||
"rate": 0.16,
|
||||
"factor": "Tasa",
|
||||
"base": _fmt2(base),
|
||||
}
|
||||
],
|
||||
}
|
||||
],
|
||||
},
|
||||
"data": [
|
||||
{
|
||||
"payment_form": forma_pago,
|
||||
"date": _normalize_payment_date(payment.get("date")),
|
||||
"related_documents": [
|
||||
{
|
||||
"uuid": original_uuid,
|
||||
"amount": _fmt2(amount),
|
||||
"installment": int(payment.get("installment") or 1),
|
||||
"last_balance": _fmt2(payment.get("last_balance") or 0),
|
||||
"taxes": [
|
||||
{
|
||||
"base": _fmt2(base),
|
||||
"type": "IVA",
|
||||
"rate": _fmt6(rate),
|
||||
"factor": "Tasa",
|
||||
"withholding": False,
|
||||
}
|
||||
],
|
||||
}
|
||||
],
|
||||
}
|
||||
],
|
||||
}
|
||||
],
|
||||
}
|
||||
if idempotency_key:
|
||||
payload["idempotency_key"] = idempotency_key
|
||||
return payload
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user