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, load_forma_pago_map router = APIRouter(prefix="/ventas", tags=["ventas"]) _SQL_VENTAS = """ SELECT r.IDRECEPCION, r.PREFIJO, r.NUM_FACTURA, r.FECHA_RECEPCION, r.COD_PACIENTE, r.NIT_EMPRESA, r.VALORTOTAL, r.VALORDESC, rel.COD_EXAMEN, COALESCE(NULLIF(TRIM(ex.NUM_ISS), ''), TRIM(rel.COD_EXAMEN)) AS CUPS, rel.PRECIO, COALESCE(t.VALOR, rel.PRECIO) AS PRECIO_TARIFA, TRIM(e.CODCONTRATO) AS CODCONTRATO, CAST(fd.FECHAFACT AS VARCHAR(30)) AS FECHAFACT, fd.DIASVENC FROM RECEPCION r JOIN FACTURA_DIAN fd ON fd.PREFIJO = r.PREFIJO AND fd.NUM_FACTURA = r.NUM_FACTURA JOIN RELACION rel ON rel.IDRECEPCION = r.IDRECEPCION LEFT JOIN EXAMEN ex ON TRIM(ex.CODIGO) = TRIM(rel.COD_EXAMEN) LEFT JOIN EMPRESA e ON e.NIT = r.NIT_EMPRESA LEFT JOIN TARIFA t ON TRIM(t.COD_EXAMEN) = TRIM(rel.COD_EXAMEN) AND t.TARIFA = e.TARIFA WHERE CAST(fd.FECHAFACT AS TIMESTAMP) BETWEEN :fecha_ini AND :fecha_fin AND (fd.ANULADA IS NULL OR fd.ANULADA = 'F') AND r.PREFIJO = 'CMXC' ORDER BY r.NUM_FACTURA, r.IDRECEPCION """ _SQL_VENTAS_BY_FACTURA = """ SELECT r.IDRECEPCION, r.PREFIJO, r.NUM_FACTURA, r.FECHA_RECEPCION, r.COD_PACIENTE, r.NIT_EMPRESA, r.VALORTOTAL, r.VALORDESC, rel.COD_EXAMEN, COALESCE(NULLIF(TRIM(ex.NUM_ISS), ''), TRIM(rel.COD_EXAMEN)) AS CUPS, rel.PRECIO, COALESCE(t.VALOR, rel.PRECIO) AS PRECIO_TARIFA, TRIM(e.CODCONTRATO) AS CODCONTRATO, CAST(fd.FECHAFACT AS VARCHAR(30)) AS FECHAFACT, fd.DIASVENC FROM RECEPCION r JOIN FACTURA_DIAN fd ON fd.PREFIJO = r.PREFIJO AND fd.NUM_FACTURA = r.NUM_FACTURA JOIN RELACION rel ON rel.IDRECEPCION = r.IDRECEPCION LEFT JOIN EXAMEN ex ON TRIM(ex.CODIGO) = TRIM(rel.COD_EXAMEN) LEFT JOIN EMPRESA e ON e.NIT = r.NIT_EMPRESA LEFT JOIN TARIFA t ON TRIM(t.COD_EXAMEN) = TRIM(rel.COD_EXAMEN) AND t.TARIFA = e.TARIFA WHERE r.PREFIJO = :prefijo AND r.NUM_FACTURA = :num_factura ORDER BY r.IDRECEPCION """ 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, fecha_inicio, fecha_fin): fb, ok, msg = get_firebird_from_config(cfg) if not ok: return None, msg ok2, err, rows = fb.execute_query(_SQL_VENTAS, { "fecha_ini": f"{fecha_inicio} 00:00:00", "fecha_fin": f"{fecha_fin} 23:59:59", }) fb.disconnect() if not ok2: return None, err return rows, None def _query_rows_by_factura(cfg, prefijo, num_factura): fb, ok, msg = get_firebird_from_config(cfg) if not ok: return None, msg ok2, err, rows = fb.execute_query(_SQL_VENTAS_BY_FACTURA, { "prefijo": prefijo, "num_factura": int(num_factura), }) fb.disconnect() if not ok2: return None, err return [dict(r) for r in 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, forma_pago_map=None): prefijo_def = cfg.get("prefijo_tns_default", "00") num_fac = str(grupo_rows[0].get("NUM_FACTURA") or "").strip() contrato = str(grupo_rows[0].get("CODCONTRATO") or "").strip() fp_override = (forma_pago_map or {}).get(contrato, "") return generar_factura_venta(grupo_rows, default_vendedor="00", default_prefijo=prefijo_def, numero_override=num_fac, forma_pago_override=fp_override) 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, 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, None, 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)): return request.app.state.templates.TemplateResponse("ventas.html", { "request": request, "user": user, }) @router.post("/preview") async def preview_ventas( request: Request, user: dict = Depends(get_current_user), fecha_inicio: str = Form(...), fecha_fin: str = Form(...), ): cfg = _cfg() rows, err = _query_rows(cfg, fecha_inicio, fecha_fin) if rows is None: return JSONResponse({"success": False, "message": err}) if not rows: return JSONResponse({"success": False, "message": "Sin datos para ese rango de fechas"}) excluded_ventas = load_excluded_ventas_set() fp_map = load_forma_pago_map() 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, fp_map) 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(...), fecha_inicio: str = Form(...), fecha_fin: str = Form(...), ): import re as _re cfg = _cfg() parts = factura_key.split("-", 1) if len(parts) != 2: return JSONResponse({"success": False, "message": f"Clave de factura inválida: {factura_key}"}) prefijo, num_str = parts[0], parts[1] nums = _re.findall(r'\d+', num_str) if not nums: return JSONResponse({"success": False, "message": f"Número de factura inválido: {factura_key}"}) grupo_rows, err = _query_rows_by_factura(cfg, prefijo, nums[-1]) if grupo_rows is None: return JSONResponse({"success": False, "message": err}) 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}"} api_sucursal = cfg.get("api_sucursal", "") or "00" endpoint = f"{TNS_BASE}/v2/facturacion/Ventas/Crear?codigosucursal={api_sucursal}" fp_map = load_forma_pago_map() venta_json = _build_venta(grupo_rows, cfg, fp_map) 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_key, 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_key} | {'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), fecha_inicio: str = Form(...), fecha_fin: str = Form(...), solo_pendientes: str = Form("0"), ): cfg = _cfg() rows, err = _query_rows(cfg, 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() fp_map = load_forma_pago_map() 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}"} api_sucursal = cfg.get("api_sucursal", "") or "00" endpoint = f"{TNS_BASE}/v2/facturacion/Ventas/Crear?codigosucursal={api_sucursal}" 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, fp_map) 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, 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, })