Restaurar funcionalidades desde stash + setup de despliegue Coolify
- Recupera el trabajo revertido (doble propuesta, retainer, niveles, API Python) desde el stash de GitHub Desktop - Dockerfile multi-stage para Next.js (migraciones automáticas al arrancar, seed opcional via RUN_SEED) - docker-compose.coolify.yml: postgres + web + api con healthchecks y SERVICE_FQDN_* - Endpoint público /api/health (verifica BD) para healthchecks - seed.ts parametrizado: conexión DB_* y credenciales SEED_* por entorno (sin passwords hardcodeadas) - .env.example, .dockerignore, uvicorn con --proxy-headers - Limpieza: .xlsx y __pycache__ fuera del repo Co-Authored-By: Claude Fable 5 <[email protected]>
This commit is contained in:
@@ -5,6 +5,19 @@ from datetime import date, datetime
|
||||
|
||||
IVA_RATE = 0.16
|
||||
|
||||
# Tarifa por hora sugerida para partidas personalizadas (Hora Centinela).
|
||||
TARIFA_HORA_DEFAULT = 700
|
||||
|
||||
# Modelos de cobro de una partida (espejo del frontend TS).
|
||||
MODELOS_COBRO: dict[str, str] = {
|
||||
"fijo": "Precio fijo",
|
||||
"horas": "Por horas",
|
||||
"retainer": "Retainer (minimo + adicionales)",
|
||||
}
|
||||
|
||||
# Doble propuesta: cada partida va en la Opcion 1, la 2 o en ambas (espejo del TS).
|
||||
OPCIONES = ("1", "2", "ambas")
|
||||
|
||||
FASES: dict[int, str] = {
|
||||
0: "FASE 0 - Auditoria / Acompanamiento",
|
||||
1: "FASE 1 - Setup e Infraestructura",
|
||||
@@ -45,6 +58,53 @@ BONOS = [
|
||||
]
|
||||
|
||||
|
||||
def calcular_precio_horas(horas: float, tarifa_hora: float) -> float:
|
||||
return round((horas or 0) * (tarifa_hora or 0), 2)
|
||||
|
||||
|
||||
def describir_retainer(monto_minimo: float, horas_incluidas: float, tarifa_hora: float) -> str:
|
||||
"""Texto descriptivo de un retainer (espejo de describirRetainer en TS)."""
|
||||
partes = [f"{format_currency(monto_minimo or 0)}/mes"]
|
||||
if horas_incluidas:
|
||||
partes.append(f"incluye {horas_incluidas:g} hr")
|
||||
if tarifa_hora:
|
||||
partes.append(f"adicional {format_currency(tarifa_hora)}/hr (se factura aparte)")
|
||||
return " · ".join(partes)
|
||||
|
||||
|
||||
def detalle_modelo(serv: dict) -> str:
|
||||
"""Sub-linea de desglose por modelo de cobro para PDF/Excel."""
|
||||
modelo = serv.get("modeloCobro")
|
||||
if modelo == "retainer":
|
||||
return describir_retainer(
|
||||
serv.get("montoMinimo") or 0,
|
||||
serv.get("horasIncluidas") or 0,
|
||||
serv.get("tarifaHora") or 0,
|
||||
)
|
||||
horas = serv.get("horas")
|
||||
tarifa = serv.get("tarifaHora")
|
||||
if (modelo == "horas" or serv.get("esPersonalizado")) and horas and tarifa:
|
||||
return f"{horas:g} h x {format_currency(tarifa)}/hr"
|
||||
return ""
|
||||
|
||||
|
||||
def calcular_totales_opcion(servicios: list[dict], opcion: str) -> dict:
|
||||
"""Totales de una opcion: suma sus partidas + las marcadas 'ambas'.
|
||||
|
||||
Espejo de calcularTotalesOpcion en TS. Respeta la regla de retainer: el total
|
||||
usa `precio`, no `horas x tarifa`. Las horas son informativas.
|
||||
"""
|
||||
rel = [s for s in servicios if s.get("opcion") == "ambas" or s.get("opcion") == opcion]
|
||||
total_unico = sum((s.get("precio") or 0) for s in rel if s.get("tipoPago") == "unico")
|
||||
total_mensual = sum((s.get("precio") or 0) for s in rel if s.get("tipoPago") == "mensual")
|
||||
horas = sum((s.get("horas") or 0) for s in rel)
|
||||
return {
|
||||
"totalUnico": round(total_unico, 2),
|
||||
"totalMensual": round(total_mensual, 2),
|
||||
"horas": round(horas, 2),
|
||||
}
|
||||
|
||||
|
||||
def bucefalo_precio(nivel: str) -> float:
|
||||
for p in PLANES_BUCEFALO:
|
||||
if p["nivel"] == nivel:
|
||||
|
||||
Reference in New Issue
Block a user