diff --git a/app/database.py b/app/database.py index 9e1de7e..228847e 100644 --- a/app/database.py +++ b/app/database.py @@ -61,6 +61,8 @@ def _migrate(conn): 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") + if "cod_forma_pago" not in contrato_cols: + conn.execute("ALTER TABLE contratos ADD COLUMN cod_forma_pago TEXT NOT NULL DEFAULT ''") # 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]: @@ -110,6 +112,17 @@ def _migrate(conn): conn.execute("PRAGMA foreign_keys = ON") conn.commit() + if "codigos_pago" not in tables: + conn.execute(""" + CREATE TABLE codigos_pago ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + codigo TEXT UNIQUE NOT NULL, + nombre TEXT NOT NULL DEFAULT '' + ) + """) + for codigo, nombre in [("CIAC", "Contado inmediato"), ("CR", "Crédito"), ("MU", "Mixto")]: + conn.execute("INSERT OR IGNORE INTO codigos_pago (codigo, nombre) VALUES (?,?)", (codigo, nombre)) + conn.commit() diff --git a/app/routes/contratos.py b/app/routes/contratos.py index e9448f7..a66ed20 100644 --- a/app/routes/contratos.py +++ b/app/routes/contratos.py @@ -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() diff --git a/app/routes/ventas.py b/app/routes/ventas.py index b35da76..4975692 100644 --- a/app/routes/ventas.py +++ b/app/routes/ventas.py @@ -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 diff --git a/app/services/json_generator.py b/app/services/json_generator.py index 3f5993b..47a66d3 100644 --- a/app/services/json_generator.py +++ b/app/services/json_generator.py @@ -271,7 +271,8 @@ def _fecha_add_dias(fecha_val, dias: int) -> str: def generar_factura_venta(rows: list, default_vendedor: str = "00", - default_prefijo: str = "00", numero_override: str = "") -> dict: + default_prefijo: str = "00", numero_override: str = "", + forma_pago_override: str = "") -> dict: if not rows: return {} h = rows[0] @@ -312,12 +313,11 @@ def generar_factura_venta(rows: list, default_vendedor: str = "00", prefijo_real = str(h.get("PREFIJO") or "").strip() or default_prefijo or "00" diasvenc = int(h.get("DIASVENC") or 30) - if total == 0: - cod_forma_pago = "CIAC" + cod_forma_pago = forma_pago_override.strip() if forma_pago_override else ("CIAC" if total == 0 else "CR") + if cod_forma_pago == "CIAC": plazo_dias = 0 fecha_vence = fecha else: - cod_forma_pago = "CR" plazo_dias = diasvenc fecha_vence = _fecha_add_dias(fecha_raw, diasvenc) diff --git a/app/templates/contratos.html b/app/templates/contratos.html index e1b88f0..3abc6f6 100644 --- a/app/templates/contratos.html +++ b/app/templates/contratos.html @@ -50,6 +50,45 @@ + +
+
+

Códigos de Pago

+
+
+
+ {% for cp in codigos_pago %} +
+ {{ cp.codigo }} + {{ cp.nombre }} +
+ +
+
+ {% endfor %} +
+
+
+ + +
+
+ + +
+ +
+
+
+
@@ -70,6 +109,7 @@ Excluir RDA Excluir Ventas Sin Contrato + Forma Pago Acciones @@ -133,7 +173,18 @@ - @@ -189,6 +240,16 @@ class="w-full px-3 py-2 border border-gray-300 rounded-lg text-sm focus:ring-2 focus:ring-blue-500">
+
+ + +