- _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 <noreply@anthropic.com>
306 lines
12 KiB
Python
306 lines
12 KiB
Python
import json as json_lib
|
|
import httpx
|
|
from datetime import datetime
|
|
from fastapi import APIRouter, Request, Form, Depends
|
|
from fastapi.responses import JSONResponse
|
|
from app.database import get_connection
|
|
from app.auth import get_current_user
|
|
from app.services.firebird_service import get_firebird_from_config
|
|
from app.services.json_generator import generar_factura_venta
|
|
from app.services.api_client import get_tns_token, TNS_BASE
|
|
from app.utils.activity import log_activity, get_ip
|
|
from app.routes.contratos import load_excluded_ventas_set
|
|
|
|
router = APIRouter(prefix="/ventas", tags=["ventas"])
|
|
|
|
|
|
def _cfg():
|
|
conn = get_connection()
|
|
cfg = {r["key"]: r["value"] for r in conn.execute("SELECT * FROM config").fetchall()}
|
|
conn.close()
|
|
return cfg
|
|
|
|
|
|
def _query_rows(cfg, query_text, factura, fecha_inicio, fecha_fin):
|
|
import re as _re
|
|
fb, ok, msg = get_firebird_from_config(cfg)
|
|
if not ok:
|
|
return None, msg
|
|
nums = _re.findall(r'\d+', factura or "")
|
|
num_val = int(nums[-1]) if nums else 0
|
|
prefix = _re.sub(r'[\d\s]', '', factura or "").strip().upper()
|
|
|
|
params = {}
|
|
if ":fecha_ini" in query_text:
|
|
params["fecha_ini"] = f"{fecha_inicio} 00:00:00"
|
|
if ":fecha_fin" in query_text:
|
|
params["fecha_fin"] = f"{fecha_fin} 23:59:59"
|
|
if ":num_factura" in query_text:
|
|
params["num_factura"] = num_val
|
|
|
|
ok2, err, rows = fb.execute_query(query_text, params)
|
|
fb.disconnect()
|
|
if not ok2:
|
|
return None, err
|
|
|
|
if prefix and rows and "PREFIJO" in rows[0]:
|
|
rows = [r for r in rows if str(r.get("PREFIJO") or "").strip().upper() == prefix]
|
|
|
|
return rows, None
|
|
|
|
|
|
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:
|
|
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)
|
|
|
|
|
|
def _build_venta(grupo_rows, cfg):
|
|
prefijo_def = cfg.get("prefijo_tns_default", "00")
|
|
num_fac = str(grupo_rows[0].get("NUM_FACTURA") or "").strip()
|
|
return generar_factura_venta(grupo_rows, default_vendedor="00",
|
|
default_prefijo=prefijo_def, numero_override=num_fac)
|
|
|
|
|
|
def _is_sent(factura_key):
|
|
conn = get_connection()
|
|
row = conn.execute(
|
|
"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:
|
|
return {"status": row["status"], "mensaje": row["mensaje_tns"], "at": row["created_at"]}
|
|
return None
|
|
|
|
|
|
def _guardar_envio(user_id, factura, idrecepcion, contrato, json_data,
|
|
respuesta, ok, fecha_inicio, fecha_fin, servicios):
|
|
try:
|
|
conn = get_connection()
|
|
conn.execute("""
|
|
INSERT INTO envios (user_id, tipo, factura, idrecepcion, contrato,
|
|
fecha_inicio, fecha_fin, pacientes_count, servicios_count,
|
|
status, json_enviado, respuesta_api, mensaje_tns, created_at)
|
|
VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?)
|
|
""", (
|
|
user_id, "ventas", factura, idrecepcion, contrato,
|
|
fecha_inicio, fecha_fin, 1, servicios,
|
|
"success" if ok else "error",
|
|
json_lib.dumps(json_data, ensure_ascii=False)[:10000],
|
|
respuesta[:2000] if respuesta else "",
|
|
respuesta[:300] if respuesta else "",
|
|
datetime.now().isoformat(),
|
|
))
|
|
conn.commit()
|
|
conn.close()
|
|
except Exception:
|
|
pass
|
|
|
|
|
|
def _parse_tns(r):
|
|
try:
|
|
data = r.json()
|
|
ok = bool(data.get("status") or (data.get("data") or {}).get("success", False))
|
|
msg = ((data.get("data") or {}).get("response") or data.get("message") or r.text[:300])
|
|
except Exception:
|
|
ok = r.status_code < 300
|
|
msg = r.text[:300]
|
|
return ok, msg
|
|
|
|
|
|
@router.get("")
|
|
async def ventas_page(request: Request, user: dict = Depends(get_current_user)):
|
|
conn = get_connection()
|
|
queries = conn.execute(
|
|
"SELECT * FROM queries WHERE query_type = 'ventas' ORDER BY name"
|
|
).fetchall()
|
|
conn.close()
|
|
return request.app.state.templates.TemplateResponse("ventas.html", {
|
|
"request": request, "user": user, "queries": queries,
|
|
})
|
|
|
|
|
|
@router.post("/preview")
|
|
async def preview_ventas(
|
|
request: Request, user: dict = Depends(get_current_user),
|
|
query_id: int = Form(...), factura: str = Form(""),
|
|
fecha_inicio: str = Form(...), fecha_fin: str = Form(...),
|
|
):
|
|
conn = get_connection()
|
|
q = conn.execute("SELECT * FROM queries WHERE id = ?", (query_id,)).fetchone()
|
|
cfg = {r["key"]: r["value"] for r in conn.execute("SELECT * FROM config").fetchall()}
|
|
conn.close()
|
|
if not q:
|
|
return JSONResponse({"success": False, "message": "Consulta no encontrada"})
|
|
|
|
rows, err = _query_rows(cfg, q["query_text"], factura, fecha_inicio, fecha_fin)
|
|
if rows is None:
|
|
return JSONResponse({"success": False, "message": err})
|
|
if not rows:
|
|
return JSONResponse({"success": False, "message": "Sin datos"})
|
|
|
|
excluded_ventas = load_excluded_ventas_set()
|
|
grupos_all = _agrupar(rows)
|
|
grupos = {k: v for k, v in grupos_all.items()
|
|
if str(v[0].get("CODCONTRATO") or "").strip() not in excluded_ventas}
|
|
|
|
items = []
|
|
for factura_key, grupo_rows in grupos.items():
|
|
enviado = _is_sent(factura_key)
|
|
venta_json = _build_venta(grupo_rows, cfg)
|
|
items.append({
|
|
"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],
|
|
"valor": float(grupo_rows[0].get("VALORTOTAL") or 0),
|
|
"enviado": enviado,
|
|
"json": venta_json,
|
|
})
|
|
|
|
pendientes = sum(1 for i in items if not i["enviado"])
|
|
enviados_ok = sum(1 for i in items if i["enviado"] and i["enviado"]["status"] == "success")
|
|
enviados_err = sum(1 for i in items if i["enviado"] and i["enviado"]["status"] == "error")
|
|
|
|
return JSONResponse({
|
|
"success": True,
|
|
"total": len(items),
|
|
"pendientes": pendientes,
|
|
"enviados_ok": enviados_ok,
|
|
"enviados_err": enviados_err,
|
|
"items": items,
|
|
})
|
|
|
|
|
|
@router.post("/send-one")
|
|
async def send_one(
|
|
request: Request, user: dict = Depends(get_current_user),
|
|
factura_key: str = Form(...), query_id: int = Form(...),
|
|
factura: str = Form(""), fecha_inicio: str = Form(...), fecha_fin: str = Form(...),
|
|
):
|
|
conn = get_connection()
|
|
q = conn.execute("SELECT * FROM queries WHERE id = ?", (query_id,)).fetchone()
|
|
cfg = {r["key"]: r["value"] for r in conn.execute("SELECT * FROM config").fetchall()}
|
|
conn.close()
|
|
if not q:
|
|
return JSONResponse({"success": False, "message": "Consulta no encontrada"})
|
|
|
|
rows, err = _query_rows(cfg, q["query_text"], factura, fecha_inicio, fecha_fin)
|
|
if rows is None:
|
|
return JSONResponse({"success": False, "message": err})
|
|
|
|
grupos = _agrupar(rows)
|
|
grupo_rows = grupos.get(factura_key)
|
|
if not grupo_rows:
|
|
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", "")
|
|
)
|
|
if not token:
|
|
return JSONResponse({"success": False, "message": f"Error login TNS: {token_err}"})
|
|
|
|
headers = {"Content-Type": "application/json", "Authorization": f"Bearer {token}"}
|
|
endpoint = f"{TNS_BASE}/v2/facturacion/Ventas/Crear"
|
|
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
|
|
msg_tns = ""
|
|
try:
|
|
async with httpx.AsyncClient(timeout=int(cfg.get("api_timeout", 30))) as client:
|
|
r = await client.post(endpoint, json=venta_json, headers=headers)
|
|
raw_resp = r.text
|
|
ok_v, msg_tns = _parse_tns(r)
|
|
except Exception as ex:
|
|
msg_tns = str(ex)
|
|
|
|
_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, "factura_key": factura_key})
|
|
|
|
|
|
@router.post("/send")
|
|
async def send_ventas(
|
|
request: Request, user: dict = Depends(get_current_user),
|
|
query_id: int = Form(...), factura: str = Form(""),
|
|
fecha_inicio: str = Form(...), fecha_fin: str = Form(...),
|
|
solo_pendientes: str = Form("0"),
|
|
):
|
|
conn = get_connection()
|
|
q = conn.execute("SELECT * FROM queries WHERE id = ?", (query_id,)).fetchone()
|
|
cfg = {r["key"]: r["value"] for r in conn.execute("SELECT * FROM config").fetchall()}
|
|
conn.close()
|
|
if not q:
|
|
return JSONResponse({"success": False, "message": "Consulta no encontrada"})
|
|
|
|
rows, err = _query_rows(cfg, q["query_text"], factura, fecha_inicio, fecha_fin)
|
|
if rows is None:
|
|
return JSONResponse({"success": False, "message": err})
|
|
if not rows:
|
|
return JSONResponse({"success": False, "message": "Sin datos"})
|
|
|
|
excluded_ventas = load_excluded_ventas_set()
|
|
grupos_all = _agrupar(rows)
|
|
grupos = {k: v for k, v in grupos_all.items()
|
|
if str(v[0].get("CODCONTRATO") or "").strip() not in excluded_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", "")
|
|
)
|
|
if not token:
|
|
return JSONResponse({"success": False, "message": f"Error login TNS: {token_err}"})
|
|
|
|
headers = {"Content-Type": "application/json", "Authorization": f"Bearer {token}"}
|
|
endpoint = f"{TNS_BASE}/v2/facturacion/Ventas/Crear"
|
|
|
|
resultados = []
|
|
async with httpx.AsyncClient(timeout=int(cfg.get("api_timeout", 30))) as client:
|
|
for factura_key, grupo_rows in grupos.items():
|
|
venta_json = _build_venta(grupo_rows, cfg)
|
|
contrato = str(grupo_rows[0].get("CODCONTRATO") or "").strip()
|
|
raw_resp = ""
|
|
ok_v = False
|
|
msg_tns = ""
|
|
try:
|
|
r = await client.post(endpoint, json=venta_json, headers=headers)
|
|
raw_resp = r.text
|
|
ok_v, msg_tns = _parse_tns(r)
|
|
except Exception as ex:
|
|
msg_tns = str(ex)
|
|
|
|
_guardar_envio(user["user_id"], factura_key, None, contrato,
|
|
venta_json, raw_resp, ok_v, fecha_inicio, fecha_fin, len(grupo_rows))
|
|
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
|
|
log_activity(user["user_id"], user["username"], "ventas_masivo",
|
|
f"Enviados: {ok_count} OK, {err_count} errores | {fecha_inicio} → {fecha_fin}",
|
|
get_ip(request))
|
|
return JSONResponse({
|
|
"success": err_count == 0,
|
|
"total_enviados": ok_count,
|
|
"total_errores": err_count,
|
|
"resultados": resultados,
|
|
})
|