feat: excluir_ventas independiente por contrato

- Nueva columna excluir_ventas en tabla contratos (migración automática)
- load_excluded_ventas_set() para filtrar en /ventas
- Toggle independiente en /contratos para RDA y Ventas por separado
- Modal edición con checkbox excluir_ventas
- 040 marcado excluir_rda=1, excluir_ventas=0 en seed

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Lizandro Guarnizo
2026-07-11 11:56:28 -05:00
co-authored by Claude Sonnet 4.6
parent af71968c7f
commit f103d536c7
4 changed files with 125 additions and 63 deletions
+2
View File
@@ -53,6 +53,8 @@ def _migrate(conn):
conn.execute("ALTER TABLE contratos ADD COLUMN excluir INTEGER NOT NULL DEFAULT 0")
if "sin_contrato" not in contrato_cols:
conn.execute("ALTER TABLE contratos ADD COLUMN sin_contrato INTEGER NOT NULL DEFAULT 0")
if "excluir_ventas" not in contrato_cols:
conn.execute("ALTER TABLE contratos ADD COLUMN excluir_ventas INTEGER NOT NULL DEFAULT 0")
# Ampliar CHECK constraint de queries para incluir 'ventas'
q_sql = conn.execute("SELECT sql FROM sqlite_master WHERE type='table' AND name='queries'").fetchone()
if q_sql and "'ventas'" not in q_sql[0]:
+88 -55
View File
@@ -6,71 +6,71 @@ from app.utils.activity import log_activity, get_ip
router = APIRouter(prefix="/contratos", tags=["contratos"])
# (numero_contrato, nit_empresa, tipo_usuario, descripcion, excluir, sin_contrato)
# (numero_contrato, nit_empresa, tipo_usuario, descripcion, excluir_rda, sin_contrato, excluir_ventas)
_SEED = [
("001", "860078828", "11", "EPS SANITAS", 0, 0),
("002", "830054904", "11", "EPS COMPENSAR", 0, 0),
("003", "900278729", "12", "PARTICULAR", 0, 0),
("004", "800106339", "11", "EPS NUEVA EPS", 0, 0),
("005", "800153424", "11", "EPS SURA", 0, 0),
("006", "805009741", "11", "EPS COOMEVA", 0, 0),
("007", "860002183", "11", "EPS FAMISANAR", 0, 0),
("008", "860002503", "11", "EPS CRUZ BLANCA", 0, 0),
("009", "860027404", "11", "EPS COLSANITAS", 0, 0),
("010", "860039988", "11", "EPS SALUD TOTAL", 0, 0),
("011", "890903790", "11", "EPS SAVIA SALUD", 0, 0),
("012", "900178724", "11", "EPS MUTUAL SER", 0, 0),
("034", "860078828", "11", "EPS SANITAS (alt)", 0, 0),
("035", "860078828", "11", "EPS SANITAS (alt)", 0, 0),
("036", "860078828", "11", "EPS SANITAS (alt)", 0, 0),
("20062026", "800182856", "01", "SUBSIDIADO", 0, 0),
("CW225489", "899999068", "07", "POLIZA / SEGURO", 0, 0),
("001", "860078828", "11", "EPS SANITAS", 0, 0, 0),
("002", "830054904", "11", "EPS COMPENSAR", 0, 0, 0),
("003", "900278729", "12", "PARTICULAR", 0, 0, 0),
("004", "800106339", "11", "EPS NUEVA EPS", 0, 0, 0),
("005", "800153424", "11", "EPS SURA", 0, 0, 0),
("006", "805009741", "11", "EPS COOMEVA", 0, 0, 0),
("007", "860002183", "11", "EPS FAMISANAR", 0, 0, 0),
("008", "860002503", "11", "EPS CRUZ BLANCA", 0, 0, 0),
("009", "860027404", "11", "EPS COLSANITAS", 0, 0, 0),
("010", "860039988", "11", "EPS SALUD TOTAL", 0, 0, 0),
("011", "890903790", "11", "EPS SAVIA SALUD", 0, 0, 0),
("012", "900178724", "11", "EPS MUTUAL SER", 0, 0, 0),
("034", "860078828", "11", "EPS SANITAS (alt)", 0, 0, 0),
("035", "860078828", "11", "EPS SANITAS (alt)", 0, 0, 0),
("036", "860078828", "11", "EPS SANITAS (alt)", 0, 0, 0),
("20062026", "800182856", "01", "SUBSIDIADO", 0, 0, 0),
("CW225489", "899999068", "07", "POLIZA / SEGURO", 0, 0, 0),
# Contratos excluidos del envío TNS
("013", "", "11", "MEDILAVORO S.A.S", 0, 0),
("014", "", "11", "LABORATORIO UROCLINICO",0, 0),
("015", "", "11", "ANDRES AFANADOR VILLAMIZAR", 1, 0),
("016", "", "11", "OMAR FERNANDO RIBERO GOMEZ", 1, 0),
("017", "", "11", "CLAUDIA BELEN JULIO SEPULVEDA", 1, 0),
("018", "", "11", "LABORATORIO MICROBIOLOGICO - MARGIE OJEDA", 1, 0),
("019", "", "11", "LABORATORIO TOXICOLOGICO - MARTHA MORALES", 1, 0),
("020", "", "11", "MARTHA LUCIA GALLARDO", 1, 0),
("021", "", "11", "LABORATORIO VILMA OROZCO AYALA", 1, 0),
("022", "", "11", "CLINICA SAN JOSE DE CUCUTA", 1, 0),
("023", "", "11", "URONORTE S.A", 1, 0),
("024", "", "11", "LABORATORIO CLINICO BIOLAB S.A.S", 1, 0),
("025", "", "11", "JOEL LEONARDO CARRILLO CORREDOR", 1, 0),
("026", "", "11", "ROLANDO IVAN PENARANDA DEVIA", 1, 0),
("027", "", "11", "JOSE WILMER GARCIA CALDERON", 1, 0),
("028", "", "11", "ONCOMEDICAL IPS S.A.S", 1, 0),
("029", "", "11", "GENETIX S.A.S", 1, 0),
("030", "", "11", "NORFETUS S.A.S", 1, 0),
("031", "", "11", "TBTB GLOBAL LAB S.A.S", 1, 0),
("032", "", "11", "COLGENES S.A.S", 1, 0),
("033", "", "11", "IPS FIGURAS SPA CUCUTA S.A.S", 1, 0),
("037", "", "11", "CLINICA URGENCIAS LA MERCED", 1, 0),
("038", "", "11", "CLINICA COLSANITAS S.A.", 1, 0),
("039", "", "11", "GOMEZ GIL JOSE JESUS", 1, 0),
("040", "", "11", "MARTHA LILIANA SALGAR GALLEGO", 0, 1),
("041", "", "11", "GENCELL PHARMA S.A.S", 1, 0),
("042", "", "07", "AXA COLPATRIA HYC", 1, 0),
("013", "", "11", "MEDILAVORO S.A.S", 0, 0, 0),
("014", "", "11", "LABORATORIO UROCLINICO",0, 0, 0),
("015", "", "11", "ANDRES AFANADOR VILLAMIZAR", 1, 0, 0),
("016", "", "11", "OMAR FERNANDO RIBERO GOMEZ", 1, 0, 0),
("017", "", "11", "CLAUDIA BELEN JULIO SEPULVEDA", 1, 0, 0),
("018", "", "11", "LABORATORIO MICROBIOLOGICO - MARGIE OJEDA", 1, 0, 0),
("019", "", "11", "LABORATORIO TOXICOLOGICO - MARTHA MORALES", 1, 0, 0),
("020", "", "11", "MARTHA LUCIA GALLARDO", 1, 0, 0),
("021", "", "11", "LABORATORIO VILMA OROZCO AYALA", 1, 0, 0),
("022", "", "11", "CLINICA SAN JOSE DE CUCUTA", 1, 0, 0),
("023", "", "11", "URONORTE S.A", 1, 0, 0),
("024", "", "11", "LABORATORIO CLINICO BIOLAB S.A.S", 1, 0, 0),
("025", "", "11", "JOEL LEONARDO CARRILLO CORREDOR", 1, 0, 0),
("026", "", "11", "ROLANDO IVAN PENARANDA DEVIA", 1, 0, 0),
("027", "", "11", "JOSE WILMER GARCIA CALDERON", 1, 0, 0),
("028", "", "11", "ONCOMEDICAL IPS S.A.S", 1, 0, 0),
("029", "", "11", "GENETIX S.A.S", 1, 0, 0),
("030", "", "11", "NORFETUS S.A.S", 1, 0, 0),
("031", "", "11", "TBTB GLOBAL LAB S.A.S", 1, 0, 0),
("032", "", "11", "COLGENES S.A.S", 1, 0, 0),
("033", "", "11", "IPS FIGURAS SPA CUCUTA S.A.S", 1, 0, 0),
("037", "", "11", "CLINICA URGENCIAS LA MERCED", 1, 0, 0),
("038", "", "11", "CLINICA COLSANITAS S.A.", 1, 0, 0),
("039", "", "11", "GOMEZ GIL JOSE JESUS", 1, 0, 0),
("040", "", "11", "MARTHA LILIANA SALGAR GALLEGO", 1, 1, 0),
("041", "", "11", "GENCELL PHARMA S.A.S", 1, 0, 0),
("042", "", "07", "AXA COLPATRIA HYC", 1, 0, 0),
]
def ensure_defaults():
conn = get_connection()
for nc, nit, tu, desc, excluir, sin_contrato in _SEED:
for nc, nit, tu, desc, excluir, sin_contrato, excluir_ventas in _SEED:
exists = conn.execute(
"SELECT id FROM contratos WHERE numero_contrato = ?", (nc,)
).fetchone()
if not exists:
conn.execute(
"INSERT INTO contratos (numero_contrato, nit_empresa, tipo_usuario, descripcion, excluir, sin_contrato) VALUES (?,?,?,?,?,?)",
(nc, nit, tu, desc, excluir, sin_contrato),
"INSERT INTO contratos (numero_contrato, nit_empresa, tipo_usuario, descripcion, excluir, sin_contrato, excluir_ventas) VALUES (?,?,?,?,?,?,?)",
(nc, nit, tu, desc, excluir, sin_contrato, excluir_ventas),
)
else:
conn.execute(
"UPDATE contratos SET excluir=?, sin_contrato=? WHERE numero_contrato=?",
(excluir, sin_contrato, nc),
"UPDATE contratos SET excluir=?, sin_contrato=?, excluir_ventas=? WHERE numero_contrato=?",
(excluir, sin_contrato, excluir_ventas, nc),
)
conn.commit()
conn.close()
@@ -108,7 +108,7 @@ def load_sin_contrato_set() -> set:
def load_excluded_set() -> set:
"""Devuelve set de numero_contrato (raw + sin ceros) marcados como excluir=1."""
"""Devuelve set de numero_contrato marcados como excluir=1 (bloqueados para RDA)."""
conn = get_connection()
rows = conn.execute("SELECT numero_contrato FROM contratos WHERE excluir=1").fetchall()
conn.close()
@@ -122,6 +122,21 @@ def load_excluded_set() -> set:
return result
def load_excluded_ventas_set() -> set:
"""Devuelve set de numero_contrato marcados como excluir_ventas=1 (bloqueados para Ventas)."""
conn = get_connection()
rows = conn.execute("SELECT numero_contrato FROM contratos WHERE excluir_ventas=1").fetchall()
conn.close()
result = set()
for r in rows:
nc = r["numero_contrato"].strip()
result.add(nc)
nc_s = nc.lstrip("0") or nc
if nc_s != nc:
result.add(nc_s)
return result
@router.get("")
async def contratos_page(request: Request, user: dict = Depends(get_current_user)):
conn = get_connection()
@@ -142,12 +157,13 @@ async def contrato_create(
descripcion: str = Form(""),
excluir: str = Form("0"),
sin_contrato: str = Form("0"),
excluir_ventas: str = Form("0"),
):
conn = get_connection()
try:
conn.execute(
"INSERT INTO contratos (numero_contrato, nit_empresa, tipo_usuario, descripcion, excluir, sin_contrato) VALUES (?,?,?,?,?,?)",
(numero_contrato.strip(), nit_empresa.strip(), tipo_usuario.strip(), descripcion.strip(), int(excluir), int(sin_contrato)),
"INSERT INTO contratos (numero_contrato, nit_empresa, tipo_usuario, descripcion, excluir, sin_contrato, excluir_ventas) VALUES (?,?,?,?,?,?,?)",
(numero_contrato.strip(), nit_empresa.strip(), tipo_usuario.strip(), descripcion.strip(), int(excluir), int(sin_contrato), int(excluir_ventas)),
)
conn.commit()
log_activity(user["user_id"], user["username"], "contrato_creado",
@@ -169,11 +185,12 @@ async def contrato_update(
descripcion: str = Form(""),
excluir: str = Form("0"),
sin_contrato: str = Form("0"),
excluir_ventas: str = Form("0"),
):
conn = get_connection()
conn.execute(
"UPDATE contratos SET numero_contrato=?, nit_empresa=?, tipo_usuario=?, descripcion=?, excluir=?, sin_contrato=? WHERE id=?",
(numero_contrato.strip(), nit_empresa.strip(), tipo_usuario.strip(), descripcion.strip(), int(excluir), int(sin_contrato), contrato_id),
"UPDATE contratos SET numero_contrato=?, nit_empresa=?, tipo_usuario=?, descripcion=?, excluir=?, sin_contrato=?, excluir_ventas=? WHERE id=?",
(numero_contrato.strip(), nit_empresa.strip(), tipo_usuario.strip(), descripcion.strip(), int(excluir), int(sin_contrato), int(excluir_ventas), contrato_id),
)
conn.commit()
log_activity(user["user_id"], user["username"], "contrato_editado",
@@ -215,6 +232,22 @@ async def toggle_sin_contrato(contrato_id: int, request: Request, user: dict = D
return RedirectResponse("/contratos", status_code=302)
@router.post("/toggle-excluir-ventas/{contrato_id}")
async def toggle_excluir_ventas(contrato_id: int, request: Request, user: dict = Depends(get_current_user)):
conn = get_connection()
conn.execute(
"UPDATE contratos SET excluir_ventas = CASE WHEN excluir_ventas=1 THEN 0 ELSE 1 END WHERE id=?",
(contrato_id,)
)
conn.commit()
row = conn.execute("SELECT numero_contrato, excluir_ventas FROM contratos WHERE id=?", (contrato_id,)).fetchone()
if row:
log_activity(user["user_id"], user["username"], "contrato_toggle_ventas",
f"Contrato {row['numero_contrato']} excluir_ventas→{row['excluir_ventas']}", get_ip(request))
conn.close()
return RedirectResponse("/contratos", status_code=302)
@router.post("/delete/{contrato_id}")
async def contrato_delete(contrato_id: int, user: dict = Depends(get_current_user)):
conn = get_connection()
+10 -2
View File
@@ -9,6 +9,7 @@ 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"])
@@ -141,7 +142,11 @@ async def preview_ventas(
if not rows:
return JSONResponse({"success": False, "message": "Sin datos"})
grupos = _agrupar(rows)
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 key, grupo_rows in grupos.items():
enviado = _is_sent(key)
@@ -245,7 +250,10 @@ async def send_ventas(
if not rows:
return JSONResponse({"success": False, "message": "Sin datos"})
grupos = _agrupar(rows)
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)}
+25 -6
View File
@@ -67,7 +67,8 @@
<th class="px-4 py-3 text-left text-xs font-semibold text-gray-500 uppercase">NIT Empresa</th>
<th class="px-4 py-3 text-left text-xs font-semibold text-gray-500 uppercase">Tipo Usuario</th>
<th class="px-4 py-3 text-left text-xs font-semibold text-gray-500 uppercase">Descripción</th>
<th class="px-4 py-3 text-center text-xs font-semibold text-gray-500 uppercase">Envío TNS</th>
<th class="px-4 py-3 text-center text-xs font-semibold text-gray-500 uppercase">Excluir RDA</th>
<th class="px-4 py-3 text-center text-xs font-semibold text-gray-500 uppercase">Excluir Ventas</th>
<th class="px-4 py-3 text-center text-xs font-semibold text-gray-500 uppercase">Sin Contrato</th>
<th class="px-4 py-3 text-center text-xs font-semibold text-gray-500 uppercase">Acciones</th>
</tr>
@@ -105,6 +106,19 @@
{% endif %}
</form>
</td>
<td class="px-4 py-3 text-center">
<form method="POST" action="/contratos/toggle-excluir-ventas/{{ c.id }}" class="inline">
{% if c.excluir_ventas %}
<button type="submit" class="inline-flex items-center gap-1 px-2.5 py-1 bg-red-100 text-red-700 hover:bg-red-200 rounded-full text-xs font-medium transition-colors">
<i class="fas fa-ban text-xs"></i> Excluido
</button>
{% else %}
<button type="submit" class="inline-flex items-center gap-1 px-2.5 py-1 bg-emerald-100 text-emerald-700 hover:bg-emerald-200 rounded-full text-xs font-medium transition-colors">
<i class="fas fa-check text-xs"></i> Activo
</button>
{% endif %}
</form>
</td>
<td class="px-4 py-3 text-center">
<form method="POST" action="/contratos/toggle-sin-contrato/{{ c.id }}" class="inline">
{% if c.sin_contrato %}
@@ -119,7 +133,7 @@
</form>
</td>
<td class="px-4 py-3 text-center">
<button onclick="openEdit({{ c.id }}, '{{ c.numero_contrato }}', '{{ c.nit_empresa }}', '{{ c.tipo_usuario }}', '{{ c.descripcion }}', {{ c.excluir }}, {{ c.sin_contrato }})"
<button onclick="openEdit({{ c.id }}, '{{ c.numero_contrato }}', '{{ c.nit_empresa }}', '{{ c.tipo_usuario }}', '{{ c.descripcion }}', {{ c.excluir }}, {{ c.sin_contrato }}, {{ c.excluir_ventas }})"
class="text-blue-600 hover:text-blue-800 mr-3 text-xs">
<i class="fas fa-edit"></i> Editar
</button>
@@ -175,14 +189,18 @@
class="w-full px-3 py-2 border border-gray-300 rounded-lg text-sm focus:ring-2 focus:ring-blue-500">
</div>
</div>
<div class="flex items-center gap-6">
<div class="flex flex-wrap items-center gap-4">
<label class="flex items-center gap-2 cursor-pointer">
<input type="checkbox" name="excluir" id="edit-excluir" value="1" class="rounded">
<span class="text-sm text-gray-700">Excluir del envío TNS</span>
<span class="text-sm text-gray-700">Excluir RDA <span class="text-xs text-red-500">(bloquea transacción/automation)</span></span>
</label>
<label class="flex items-center gap-2 cursor-pointer">
<input type="checkbox" name="excluir_ventas" id="edit-excluir-ventas" value="1" class="rounded">
<span class="text-sm text-gray-700">Excluir Ventas <span class="text-xs text-red-500">(bloquea /ventas)</span></span>
</label>
<label class="flex items-center gap-2 cursor-pointer">
<input type="checkbox" name="sin_contrato" id="edit-sin-contrato" value="1" class="rounded">
<span class="text-sm text-gray-700">Enviar sin contrato <span class="text-xs text-yellow-600">(numeroContrato: null)</span></span>
<span class="text-sm text-gray-700">Sin contrato <span class="text-xs text-yellow-600">(numeroContrato: null)</span></span>
</label>
</div>
<div class="flex justify-end space-x-3 pt-2">
@@ -200,7 +218,7 @@
</div>
<script>
function openEdit(id, nc, nit, tu, desc, excluir, sinContrato) {
function openEdit(id, nc, nit, tu, desc, excluir, sinContrato, excluirVentas) {
document.getElementById('edit-form').action = '/contratos/update/' + id;
document.getElementById('edit-nc').value = nc;
document.getElementById('edit-nit').value = nit;
@@ -208,6 +226,7 @@ function openEdit(id, nc, nit, tu, desc, excluir, sinContrato) {
document.getElementById('edit-desc').value = desc;
document.getElementById('edit-excluir').checked = excluir === 1;
document.getElementById('edit-sin-contrato').checked = sinContrato === 1;
document.getElementById('edit-excluir-ventas').checked = excluirVentas === 1;
document.getElementById('edit-modal').classList.remove('hidden');
}
function closeEdit() {