feat(contratos): gestión de códigos de pago + asignación por contrato

- Nueva tabla SQLite codigos_pago (codigo, nombre) con seed CIAC/CR/MU
- Columna cod_forma_pago en contratos (override por contrato)
- Página contratos: tarjeta para agregar/eliminar códigos, select inline por fila
- generar_factura_venta acepta forma_pago_override; si asignado lo usa, sino auto CIAC/CR
- Rutas: /codigos-pago/create, /codigos-pago/delete, /set-forma-pago

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Lizandro Guarnizo
2026-07-30 16:43:13 -05:00
co-authored by Claude Sonnet 4.6
parent 551dec9c11
commit 91dbcc8135
5 changed files with 157 additions and 14 deletions
+64 -2
View File
@@ -141,13 +141,30 @@ def load_excluded_ventas_set() -> set:
return result
def load_forma_pago_map() -> dict:
"""Devuelve {numero_contrato: cod_forma_pago} para contratos con forma de pago asignada."""
conn = get_connection()
rows = conn.execute("SELECT numero_contrato, cod_forma_pago FROM contratos WHERE cod_forma_pago != ''").fetchall()
conn.close()
result = {}
for r in rows:
nc = r["numero_contrato"].strip()
result[nc] = r["cod_forma_pago"].strip()
nc_s = nc.lstrip("0") or nc
if nc_s != nc:
result[nc_s] = r["cod_forma_pago"].strip()
return result
@router.get("")
async def contratos_page(request: Request, user: dict = Depends(get_current_user)):
conn = get_connection()
rows = conn.execute("SELECT * FROM contratos ORDER BY numero_contrato").fetchall()
codigos_pago = conn.execute("SELECT * FROM codigos_pago ORDER BY codigo").fetchall()
conn.close()
return request.app.state.templates.TemplateResponse("contratos.html", {
"request": request, "user": user, "contratos": rows,
"codigos_pago": codigos_pago,
})
@@ -190,11 +207,12 @@ async def contrato_update(
excluir: str = Form("0"),
sin_contrato: str = Form("0"),
excluir_ventas: str = Form("0"),
cod_forma_pago: str = Form(""),
):
conn = get_connection()
conn.execute(
"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),
"UPDATE contratos SET numero_contrato=?, nit_empresa=?, tipo_usuario=?, descripcion=?, excluir=?, sin_contrato=?, excluir_ventas=?, cod_forma_pago=? WHERE id=?",
(numero_contrato.strip(), nit_empresa.strip(), tipo_usuario.strip(), descripcion.strip(), int(excluir), int(sin_contrato), int(excluir_ventas), cod_forma_pago.strip(), contrato_id),
)
conn.commit()
log_activity(user["user_id"], user["username"], "contrato_editado",
@@ -252,6 +270,50 @@ async def toggle_excluir_ventas(contrato_id: int, request: Request, user: dict =
return RedirectResponse("/contratos", status_code=302)
@router.post("/set-forma-pago/{contrato_id}")
async def set_forma_pago(
contrato_id: int, request: Request, user: dict = Depends(get_current_user),
cod_forma_pago: str = Form(""),
):
conn = get_connection()
conn.execute("UPDATE contratos SET cod_forma_pago=? WHERE id=?", (cod_forma_pago.strip(), contrato_id))
conn.commit()
row = conn.execute("SELECT numero_contrato FROM contratos WHERE id=?", (contrato_id,)).fetchone()
if row:
log_activity(user["user_id"], user["username"], "contrato_forma_pago",
f"Contrato {row['numero_contrato']} forma_pago→{cod_forma_pago.strip() or '(auto)'}",
get_ip(request))
conn.close()
return RedirectResponse("/contratos", status_code=302)
@router.post("/codigos-pago/create")
async def codigo_pago_create(
request: Request, user: dict = Depends(get_current_user),
codigo: str = Form(...), nombre: str = Form(""),
):
conn = get_connection()
try:
conn.execute("INSERT INTO codigos_pago (codigo, nombre) VALUES (?,?)",
(codigo.strip().upper(), nombre.strip()))
conn.commit()
log_activity(user["user_id"], user["username"], "codigo_pago_creado",
f"{codigo.strip().upper()}{nombre.strip()}", get_ip(request))
except Exception:
pass
conn.close()
return RedirectResponse("/contratos", status_code=302)
@router.post("/codigos-pago/delete/{cp_id}")
async def codigo_pago_delete(cp_id: int, user: dict = Depends(get_current_user)):
conn = get_connection()
conn.execute("DELETE FROM codigos_pago WHERE id=?", (cp_id,))
conn.commit()
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()
+12 -6
View File
@@ -9,7 +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
from app.routes.contratos import load_excluded_ventas_set, load_forma_pago_map
router = APIRouter(prefix="/ventas", tags=["ventas"])
@@ -103,11 +103,14 @@ def _agrupar(rows):
return dict(grupos)
def _build_venta(grupo_rows, cfg):
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)
default_prefijo=prefijo_def, numero_override=num_fac,
forma_pago_override=fp_override)
def _is_sent(factura_key):
@@ -177,6 +180,7 @@ async def preview_ventas(
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}
@@ -184,7 +188,7 @@ async def preview_ventas(
items = []
for factura_key, grupo_rows in grupos.items():
enviado = _is_sent(factura_key)
venta_json = _build_venta(grupo_rows, cfg)
venta_json = _build_venta(grupo_rows, cfg, fp_map)
items.append({
"factura_key": factura_key,
"factura": factura_key,
@@ -242,7 +246,8 @@ async def send_one(
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}"
venta_json = _build_venta(grupo_rows, cfg)
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 = ""
@@ -279,6 +284,7 @@ async def send_ventas(
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}
@@ -298,7 +304,7 @@ async def send_ventas(
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)
venta_json = _build_venta(grupo_rows, cfg, fp_map)
contrato = str(grupo_rows[0].get("CODCONTRATO") or "").strip()
raw_resp = ""
ok_v = False