From 8ed8f0999ff42802f7e49dea8e35a737c5ade2ff Mon Sep 17 00:00:00 2001 From: Lizandro Guarnizo <77708265+lizandrogd@users.noreply.github.com> Date: Tue, 28 Jul 2026 17:08:20 -0500 Subject: [PATCH] fix: refactor ventas page to group by factura instead of idrecepcion - _agrupar now keys by PREFIJO-NUM_FACTURA so all services of a factura are grouped together - _is_sent checks by factura key instead of idrecepcion - send-one and send endpoints accept/return factura_key instead of idrecepcion - ventas.html JS updated: factura_key used throughout (filaHtml, marcarFila, enviarUno, enviarTodosPendientes, verDetalle) - DOM element IDs sanitized with _safeId() to handle dashes in keys like CMXC-309 - onclick handlers now pass string keys with proper quoting Co-Authored-By: Claude Sonnet 4.6 --- app/routes/ventas.py | 42 +++++++++++++++--------------- app/templates/ventas.html | 55 +++++++++++++++++++++++---------------- requirements.txt | 1 + 3 files changed, 54 insertions(+), 44 deletions(-) diff --git a/app/routes/ventas.py b/app/routes/ventas.py index 9905444..ed3d097 100644 --- a/app/routes/ventas.py +++ b/app/routes/ventas.py @@ -50,10 +50,14 @@ def _query_rows(cfg, query_text, factura, fecha_inicio, fecha_fin): def _agrupar(rows): + """Agrupa por PREFIJO+NUM_FACTURA para incluir todos los servicios de una factura.""" from collections import defaultdict grupos = defaultdict(list) for row in rows: - grupos[row.get("IDRECEPCION")].append(dict(row)) + prefijo = str(row.get("PREFIJO") or "").strip() + num = str(row.get("NUM_FACTURA") or "").strip() + key = f"{prefijo}-{num}" + grupos[key].append(dict(row)) return dict(grupos) @@ -64,11 +68,11 @@ def _build_venta(grupo_rows, cfg): default_prefijo=prefijo_def, numero_override=num_fac) -def _is_sent(idrecepcion): +def _is_sent(factura_key): conn = get_connection() row = conn.execute( - "SELECT status, mensaje_tns, created_at FROM envios WHERE idrecepcion=? AND tipo='ventas' ORDER BY id DESC LIMIT 1", - (idrecepcion,) + "SELECT status, mensaje_tns, created_at FROM envios WHERE factura=? AND tipo='ventas' ORDER BY id DESC LIMIT 1", + (factura_key,) ).fetchone() conn.close() if row: @@ -148,14 +152,12 @@ async def preview_ventas( if str(v[0].get("CODCONTRATO") or "").strip() not in excluded_ventas} items = [] - for key, grupo_rows in grupos.items(): - enviado = _is_sent(key) + for factura_key, grupo_rows in grupos.items(): + enviado = _is_sent(factura_key) venta_json = _build_venta(grupo_rows, cfg) - num_fac = str(grupo_rows[0].get("NUM_FACTURA") or "") - prefijo = str(grupo_rows[0].get("PREFIJO") or "").strip() items.append({ - "idrecepcion": key, - "factura": f"{prefijo}-{num_fac}", + "factura_key": factura_key, + "factura": factura_key, "paciente": grupo_rows[0].get("COD_PACIENTE", ""), "contrato": str(grupo_rows[0].get("CODCONTRATO") or "").strip(), "examenes": [r.get("COD_EXAMEN", "") for r in grupo_rows], @@ -181,7 +183,7 @@ async def preview_ventas( @router.post("/send-one") async def send_one( request: Request, user: dict = Depends(get_current_user), - idrecepcion: int = Form(...), query_id: int = Form(...), + factura_key: str = Form(...), query_id: int = Form(...), factura: str = Form(""), fecha_inicio: str = Form(...), fecha_fin: str = Form(...), ): conn = get_connection() @@ -196,9 +198,9 @@ async def send_one( return JSONResponse({"success": False, "message": err}) grupos = _agrupar(rows) - grupo_rows = grupos.get(idrecepcion) + grupo_rows = grupos.get(factura_key) if not grupo_rows: - return JSONResponse({"success": False, "message": f"Registro {idrecepcion} no encontrado"}) + return JSONResponse({"success": False, "message": f"Factura {factura_key} no encontrada"}) token, token_err = await get_tns_token( cfg.get("tns_empresa", ""), cfg.get("tns_usuario", ""), cfg.get("tns_password", "") @@ -225,13 +227,13 @@ async def send_one( except Exception as ex: msg_tns = str(ex) - _guardar_envio(user["user_id"], factura_display, idrecepcion, contrato, + _guardar_envio(user["user_id"], factura_display, None, contrato, venta_json, raw_resp, ok_v, fecha_inicio, fecha_fin, len(grupo_rows)) log_activity(user["user_id"], user["username"], "venta_enviada", f"Factura {factura_display} | {'OK' if ok_v else 'ERROR: '+msg_tns[:80]}", get_ip(request)) - return JSONResponse({"success": ok_v, "message": msg_tns, "raw_tns": raw_resp, "idrecepcion": idrecepcion}) + return JSONResponse({"success": ok_v, "message": msg_tns, "raw_tns": raw_resp, "factura_key": factura_key}) @router.post("/send") @@ -261,6 +263,7 @@ async def send_ventas( if solo_pendientes == "1": grupos = {k: v for k, v in grupos.items() if not _is_sent(k)} + token, token_err = await get_tns_token( cfg.get("tns_empresa", ""), cfg.get("tns_usuario", ""), cfg.get("tns_password", "") ) @@ -272,11 +275,8 @@ async def send_ventas( resultados = [] async with httpx.AsyncClient(timeout=int(cfg.get("api_timeout", 30))) as client: - for key, grupo_rows in grupos.items(): + for factura_key, grupo_rows in grupos.items(): venta_json = _build_venta(grupo_rows, cfg) - num_fac = str(grupo_rows[0].get("NUM_FACTURA") or "").strip() - prefijo = str(grupo_rows[0].get("PREFIJO") or "").strip() - factura_display = f"{prefijo}-{num_fac}" contrato = str(grupo_rows[0].get("CODCONTRATO") or "").strip() raw_resp = "" ok_v = False @@ -288,9 +288,9 @@ async def send_ventas( except Exception as ex: msg_tns = str(ex) - _guardar_envio(user["user_id"], factura_display, key, contrato, + _guardar_envio(user["user_id"], factura_key, None, contrato, venta_json, raw_resp, ok_v, fecha_inicio, fecha_fin, len(grupo_rows)) - resultados.append({"idrecepcion": key, "success": ok_v, "msg": msg_tns}) + resultados.append({"factura": factura_key, "success": ok_v, "msg": msg_tns}) ok_count = sum(1 for r in resultados if r["success"]) err_count = len(resultados) - ok_count diff --git a/app/templates/ventas.html b/app/templates/ventas.html index 645e70c..cff4287 100644 --- a/app/templates/ventas.html +++ b/app/templates/ventas.html @@ -193,37 +193,41 @@ function renderTabla() { } function filaHtml(item) { - const id = item.idrecepcion; + const id = item.factura_key; + const sid = escAttr(id); const env = item.enviado; const examsStr = Array.isArray(item.examenes) ? item.examenes.join(', ') : (item.examenes || ''); + const rowId = 'row-' + id.replace(/[^a-zA-Z0-9]/g, '_'); + const estadoId = 'estado-' + id.replace(/[^a-zA-Z0-9]/g, '_'); + const btnId = 'btnenv-' + id.replace(/[^a-zA-Z0-9]/g, '_'); let badgeHtml, rowCls, accionHtml; if (!env) { badgeHtml = `Pendiente`; rowCls = ''; - accionHtml = ``; + accionHtml = ``; } else if (env.status === 'success') { const at = (env.at || '').slice(0, 16).replace('T', ' '); badgeHtml = `✓ Enviado`; rowCls = 'bg-green-50/50 opacity-80'; - accionHtml = ``; + accionHtml = ``; } else { const tip = escAttr(env.mensaje || 'Error'); badgeHtml = `✗ Error`; rowCls = 'bg-red-50/50'; - accionHtml = ``; + accionHtml = ``; } - return ` - ${id} + return ` + ${escHtml(id)} ${escHtml(String(item.factura || '-'))} ${escHtml(String(item.paciente || '-'))} ${escHtml(String(item.contrato || '-'))} ${escHtml(examsStr)} - ${badgeHtml} + ${badgeHtml} - - ${accionHtml} + + ${accionHtml} `; } @@ -240,34 +244,39 @@ function actualizarResumen() { document.getElementById('res-err-wrap').classList.toggle('hidden', err === 0); } +function _safeId(id) { return id.replace(/[^a-zA-Z0-9]/g, '_'); } + function marcarFila(id, ok, mensaje, rawTns) { - const idx = allItems.findIndex(i => i.idrecepcion === id); + const idx = allItems.findIndex(i => i.factura_key === id); if (idx !== -1) { allItems[idx].enviado = { status: ok ? 'success' : 'error', mensaje, at: new Date().toISOString() }; if (rawTns !== undefined) allItems[idx]._rawTns = rawTns; } - const row = document.getElementById(`row-${id}`); - const estadoCell = document.getElementById(`estado-${id}`); - const btnWrap = document.getElementById(`btnenv-${id}`); + const sid = escAttr(id); + const safe = _safeId(id); + const row = document.getElementById(`row-${safe}`); + const estadoCell = document.getElementById(`estado-${safe}`); + const btnWrap = document.getElementById(`btnenv-${safe}`); if (!row || !estadoCell) return; if (ok) { row.className = row.className.replace(/bg-red-50\/50|bg-green-50\/50/g, '') + ' bg-green-50/50 opacity-80'; estadoCell.innerHTML = `✓ Enviado`; - if (btnWrap) btnWrap.innerHTML = ``; + if (btnWrap) btnWrap.innerHTML = ``; } else { row.className = row.className.replace(/bg-red-50\/50|bg-green-50\/50/g, '') + ' bg-red-50/50'; estadoCell.innerHTML = `✗ Error`; - if (btnWrap) btnWrap.innerHTML = ``; + if (btnWrap) btnWrap.innerHTML = ``; } actualizarResumen(); } async function enviarUno(id) { - const btnWrap = document.getElementById(`btnenv-${id}`); + const safe = _safeId(id); + const btnWrap = document.getElementById(`btnenv-${safe}`); if (btnWrap) btnWrap.innerHTML = ''; - const fd = new URLSearchParams({ ...formParams, idrecepcion: id }); + const fd = new URLSearchParams({ ...formParams, factura_key: id }); let result; try { const resp = await fetch('/ventas/send-one', { method: 'POST', credentials: 'include', body: fd }); @@ -278,9 +287,9 @@ async function enviarUno(id) { marcarFila(id, result.success, result.message, result.raw_tns); if (result.success) { - showToast(`IDRECEPCION ${id}: enviado`, 'success'); + showToast(`Factura ${escHtml(id)}: enviada`, 'success'); } else { - showToast(`IDRECEPCION ${id}: ${result.message}`, 'error'); + showToast(`Factura ${escHtml(id)}: ${escHtml(result.message || 'Error')}`, 'error'); } } @@ -295,8 +304,8 @@ async function enviarTodosPendientes() { let ok = 0, err = 0; for (const item of pendientes) { - await enviarUno(item.idrecepcion); - if (allItems.find(i => i.idrecepcion === item.idrecepcion)?.enviado?.status === 'success') ok++; else err++; + await enviarUno(item.factura_key); + if (allItems.find(i => i.factura_key === item.factura_key)?.enviado?.status === 'success') ok++; else err++; } btn.disabled = false; @@ -305,9 +314,9 @@ async function enviarTodosPendientes() { } function verDetalle(id) { - const item = allItems.find(i => i.idrecepcion === id); + const item = allItems.find(i => i.factura_key === id); if (!item) return; - document.getElementById('modal-titulo').innerHTML = `IDRECEPCION ${id} — ${item.paciente || ''} — Factura ${item.factura || '-'}`; + document.getElementById('modal-titulo').innerHTML = `${escHtml(id)} — ${escHtml(item.paciente || '')} — Factura ${escHtml(item.factura || '-')}`; document.getElementById('tab-json').innerHTML = syntaxHighlight(item.json || {}); const tnsPre = document.getElementById('tab-tns'); if (item.enviado) { diff --git a/requirements.txt b/requirements.txt index 018f7c3..ad50e55 100644 --- a/requirements.txt +++ b/requirements.txt @@ -7,3 +7,4 @@ bcrypt==4.2.1 python-jose[cryptography]==3.3.0 httpx==0.28.1 fdb>=2.0.0 +apscheduler>=3.10.0