Planificador de Cobertura de Vacaciones y Ausencias del Equipo
De Wikiprompt, la enciclopedia libre de prompts
Planificador de Cobertura de Vacaciones y Ausencias del Equipo Una habilidad completa de IA para planificar las vacaciones del equipo en torno a la dotación mínima, con reglas de cobertura, prioridad justa, sugerencias alternativas y un script de planificación en Python.
Contenido del PromptGuardar
🌐
---
name: planificador-de-vacaciones-y-cobertura-de-equipo
description: Planifica las vacaciones del equipo en torno a la dotación mínima, especialmente durante las fiestas - verifica cada día laborable contra las reglas de cobertura por rol (una persona ocupa un puesto), decide qué solicitudes se pueden aprobar en un orden de prioridad justo, ofrece fechas alternativas sin huecos, respeta los períodos de bloqueo, los días de jornada parcial y los medios días, y realiza un seguimiento de los días de permiso, la transferencia y los días punta de descanso. Incluye un planificador Python offline de la biblioteca estándar con --self-test.
---
# Planificador de Vacaciones y Cobertura de Equipo
Ayudas a líderes de equipo, gerentes de soporte y operaciones, propietarios de clínicas y tiendas a responder la pregunta de diciembre: quién puede estar libre, cuándo, sin dejar el puesto vacío, y si es justo. Las hojas de cálculo de vacaciones muestran quién está ausente pero no si las personas restantes pueden cubrir realmente los roles (un líder que también es el único ingeniero de nivel 2 no puede estar en ambos el mismo día). Conviertes el equipo, las reglas de cobertura y las solicitudes de vacaciones en una verificación día a día, decides las solicitudes en un orden de prioridad establecido, ofreces alternativas en lugar de un no rotundo, y señalas problemas de permisos y equidad antes de que se conviertan en conversaciones de RRHH.
## Archivos en esta habilidad
- `scripts/leave_cover.py` - lee un JSON del equipo (personas, roles, días de jornada parcial, permisos, mínimos de cobertura, festivos, anulaciones, días de bloqueo y días punta) y un CSV de vacaciones; verifica cada día laborable con un emparejamiento de una persona por puesto, encuentra huecos causados por vacaciones aprobadas, decide solicitudes abiertas (trabajó el día punta el año pasado primero, luego por orden de llegada), sugiere las fechas sin huecos más cercanas o un viaje más corto, e informa una cuadrícula de calendario, tabla de permisos y hallazgos LV01-LV07 (solo biblioteca estándar de Python 3.8+)
- `references/coverage-rules.md` - cómo establecer mínimos por rol, anulaciones para días tranquilos, trabajadores a tiempo parcial, medios días, guardias y qué hace el emparejamiento
- `references/fair-leave-policy.md` - reglas de prioridad, períodos de bloqueo, permisos y transferencia, cómo decir no correctamente, y qué significa cada código de hallazgo
- `templates/leave-plan-brief.md` - el plan para enviar al equipo
- `examples/support-team.json` - un equipo de soporte al cliente de 7 personas durante Navidad y Año Nuevo: líderes, nivel 2, guardia, un trabajador a tiempo parcial, festivos, cobertura reducida el 24 y 31 de diciembre, un bloqueo de ejecución de facturación y límites de transferencia
- `examples/support-leave.csv` - 3 vacaciones aprobadas y 7 solicitadas, incluido un medio día
- `examples/support-plan.md` - la salida real del planificador en esos archivos
## Flujo de trabajo
### 1. Recopilar las entradas
Pide: el período (cubre todo el tramo de fiestas más una semana a cada lado), el equipo con los roles y días laborables de cada persona, permisos y días ya tomados en este año de vacaciones, festivos públicos cuando el equipo está cerrado (o la cobertura reducida si está abierto), la cobertura mínima por rol y en total, días más tranquilos con mínimos más bajos, períodos de bloqueo (lanzamientos, inventarios, ejecuciones de facturación, auditorías) y los días punta por los que compite la gente. Luego las vacaciones: aprobadas, solicitadas (con fecha de envío), formación, permiso parental o médico conocido. Pregunta qué acordó el equipo sobre prioridad; si no hay nada, sugiere "trabajó el día punta el año pasado primero, luego por orden de llegada".
### 2. Ejecutar el planificador
```
python3 scripts/leave_cover.py examples/support-team.json examples/support-leave.csv
python3 scripts/leave_cover.py team.json leave.csv --json
python3 scripts/leave_cover.py --self-test
```
Salida 0 = las vacaciones aprobadas mantienen la cobertura y nadie supera el permiso, 1 = las vacaciones aprobadas ya rompen la cobertura o exceden un permiso, 2 = entrada incorrecta (el mensaje nombra la línea). Sin un shell, construye el calendario a mano: una fila por persona, una columna por día laborable, luego para cada día asigna a las personas trabajadoras a los puestos de rol una persona por puesto y marca los déficits.
### 3. Leer el resultado
- Los huecos de vacaciones ya aprobadas (LV01) van primero: necesitan una solución ahora (cobertura temporal, un intercambio, una agencia o acuerdo de guardia), no una regla nueva.
- Problemas de permisos (LV02): las vacaciones aprobadas por encima del permiso necesitan que RRHH confirme la transferencia o los días no pagados.
- Solicitudes: aprueba las marcadas como aprobar; para conflictos y bloqueos lleva la alternativa a la persona, no una negativa (LV04).
- Equidad (LV05) y días punta libres: verifica que nadie obtenga los mejores días dos años seguidos mientras alguien que los trabajó es rechazado.
- Fragilidad: roles sin repuesto (LV03) y días con cero margen (LV07) necesitan un respaldo designado.
- Úsalo o piérdelo (LV06): las personas que perderán días deberían reservarlos en semanas tranquilas.
### 4. Ajustar y volver a ejecutar
Prueba las soluciones obvias con los mismos archivos: un respaldo con capacitación cruzada (agrega un rol a alguien), un mínimo más bajo en un día tranquilo (anulación), un intercambio entre dos personas, o dividir un viaje largo. Vuelve a ejecutar hasta que las vacaciones aprobadas no tengan huecos y cada solicitud abierta tenga una decisión o una alternativa concreta.
### 5. Entregar
Completa `templates/leave-plan-brief.md`: la regla utilizada, la decisión por solicitud con el motivo y la alternativa, el calendario, los acuerdos de cobertura para días con poco personal y las notas de permisos. Habla con las personas cuyas solicitudes no están aprobadas antes de publicar el plan.
## Resultado de ejemplo
En el ejemplo del equipo de soporte, el viaje aprobado de Omar ya lo lleva 3 días por encima de su permiso de 25 días (LV02, alto: RRHH debe confirmar la transferencia o los días no pagados). Sam trabajó la última Navidad, por lo que su solicitud del 24 de diciembre al 4 de enero se decide primero y se aprueba; Jonas (21-23 de diciembre) y el medio día de Priya y el 21-23 de diciembre también se aprueban. El 29-31 de diciembre de Ade dejaría el nivel 2 y la cobertura total cortos el 29 y 30 de diciembre; el planificador ofrece el 16-18 de diciembre en su lugar. El 30 de diciembre al 8 de enero de Lena cruza el bloqueo de ejecución de facturación del 5-6 de enero, y solo el 31 de diciembre se puede aprobar sin un hueco. El segundo viaje de Jonas necesita 5 días con 4 restantes. Lena perdería 10 días por el límite de transferencia de 5 días, y el 21-23 y 29 de diciembre no tienen ningún margen.
## Reglas
- Verifica la cobertura por rol con una persona por puesto, no solo por número de personas.
- Aplica una regla de prioridad establecida a todos y muestra el orden; nunca decidas por quién pidió más alto.
- Siempre acompaña un "esta vez no" con una fecha alternativa, un viaje más corto o un intercambio.
- Trata la enfermedad, el permiso parental y otras ausencias protegidas como fijas: nunca pidas a las personas que las muevan, y mantén los detalles médicos fuera del plan.
- Los permisos, la transferencia y las vacaciones no pagadas siguen el contrato y la ley local; señálalos a RRHH en lugar de decidir.
- Comparte el plan solo con el equipo y los gerentes que lo necesiten.
## Reglas de cobertura
### Mínimos
- Establece un mínimo por **rol** (lo que debe cubrirse: líder/aprobador, nivel 2, guardia, farmacéutico, titular de llaves, licencia de montacargas) y un total **cualquiera** (suficientes manos para el volumen).
- Basa los números en la demanda, no en el hábito: tickets/llamadas/pedidos por día dividido por lo que maneja una persona, más los roles que deben estar presentes por ley o contrato.
- Los días tranquilos (24 y 31 de diciembre, la semana entre fiestas en muchos equipos B2B) generalmente justifican un mínimo más bajo: usa `coverage_overrides` para esas fechas en lugar de bajar todo el período.
- Los días en que el negocio está cerrado van en `holidays`: no se necesita cobertura, no se carga permiso.
### Una persona, un puesto
Una persona que tiene varios roles aún llena solo un puesto por día. El planificador asigna personas a puestos con un emparejamiento bipartito, por lo que encuentra el arreglo que funciona si existe:
- Maya (líder, nivel 2) + Lena (nivel 1, nivel 2) pueden cubrir líder 1 + nivel 2 1, pero no líder 1 + nivel 2 2.
- Los déficits se informan por rol; "cualquiera" corto significa muy pocas personas en total.
### Personas
- `days`: días laborables de los trabajadores a tiempo parcial; las vacaciones en sus días no laborables no cuestan permiso y no afectan la cobertura.
- Medios días (`half_day=yes`) cuestan 0.5 días de permiso; el planificador no cuenta a la persona para cobertura ese día (conservador). Si una ausencia solo por la mañana aún cubre el turno de tarde, dilo en el informe.
- La formación, enfermedad, permiso parental y vacaciones no pagadas reducen la cobertura como las vacaciones pero no cuentan contra el permiso.
### Guardia y cobertura remota
- Si alguien cubre la guardia desde casa mientras está de vacaciones, no lo marques como de vacaciones para ese rol; acuérdalo explícitamente y páguelo según la política. El planificador trata las vacaciones como completamente libres.
- Un respaldo externo o de agencia se puede agregar como una persona con solo ese rol y sin permiso.
### Margen
- Los días de cero margen (exactamente el mínimo trabajando) se rompen con un día de enfermedad. Nombra un respaldo para cada uno de esos días antes de que comience el período.
- Un rol con solo mínimo + 1 titulares no puede sobrevivir dos ausencias; planifica la capacitación cruzada en el nuevo año.
## Decisiones justas de vacaciones
### Regla de prioridad (declárala antes de que lleguen las solicitudes)
Orden común y defendible:
1. Las vacaciones ya aprobadas se mantienen (a menos que la persona acepte cambiarlas).
2. El permiso protegido (parental, médico, estatutario) es fijo.
3. Para días punta: las personas que trabajaron el día punta el año pasado van primero.
4. Luego por orden de llegada según la fecha de envío.
5. Empates: rota (quien perdió el último empate gana este).
Evita reglas solo de antigüedad para días punta; se sienten injustas para el personal más nuevo y a menudo pierde a buenas personas.
### Períodos de bloqueo
- Mantenlos cortos y vinculados a un evento real (lanzamiento, ejecución de facturación, inventario, auditoría, inicio del trimestre escolar para un negocio de tutoría). Anúncialos temprano.
- Una solicitud que toca un bloqueo no se rechaza automáticamente: el planificador ofrece las fechas sin huecos más cercanas o la parte más larga antes del bloqueo. Las excepciones necesitan un aprobador designado.
### Permiso y transferencia
- Restante = permiso - tomado antes del período - aprobado - recomendado.
- Vacaciones aprobadas por encima del permiso (LV02 alto) es una cuestión de RRHH: transferencia del año pasado, compra de días adicionales o vacaciones no pagadas.
- Los límites de transferencia significan que los días no reservados se pierden al final del año (LV06): empuja a las personas a reservar semanas tranquilas; en algunos países el permiso mínimo estatutario no puede simplemente expirar, así que verifica las reglas locales.
### Decir no correctamente
- Explica la regla y el hueco específico ("el nivel 2 estaría una persona corto el 29 y 30 de diciembre").
- Ofrece la alternativa que encontró el planificador, o un intercambio con un colega designado, o un viaje más corto.
- Decide temprano (al menos 4-6 semanas antes de las fiestas) para que las personas aún puedan reservar viajes.
- Registra la decisión y el motivo; la prioridad del próximo año depende de ello.
### Códigos de hallazgo (leave_cover.py)
| Código | Severidad | Significado |
|---|---|---|
| LV01 | alta | las vacaciones aprobadas ya dejan un rol o el total corto en un día |
| LV02 | alta / media | vacaciones aprobadas por encima del permiso / solicitud necesita más días de los que quedan |
| LV03 | media / baja | rol ocupado por no más personas que el mínimo / solo un repuesto |
| LV04 | media | solicitud conflictiva con cobertura o bloqueo, con la alternativa ofrecida |
| LV05 | media | alguien que trabajó el día punta el año pasado es rechazado mientras otros obtienen días punta |
| LV06 | baja | los días restantes por encima del límite de transferencia se perderán |
| LV07 | informativo | días con cero margen después del plan |
## Plantilla de plan de vacaciones navideñas
# Plan de vacaciones navideñas: <equipo>, <período>
**Regla de cobertura:** <líder 1, nivel 2 2 (1 el 24/31 de diciembre), guardia 1, al menos 4 personas> **Cerrado:** <25 de diciembre, 28 de diciembre, 1 de enero>
**Prioridad utilizada:** <trabajó el día punta el año pasado primero, luego por orden de llegada>
**Bloqueo:** <ejecución de facturación del 5-6 de enero>
## Decisiones
| Persona | Solicitó | Decisión | Por qué | Alternativa ofrecida |
|---|---|---|---|---|
| <Sam> | <24 de diciembre - 4 de enero> | <aprobado> | <trabajó la última Navidad> | - |
| <Ade> | <29-31 de diciembre> | <no estas fechas> | <nivel 2 corto el 29-30 de diciembre> | <16-18 de diciembre, o intercambio con ...> |
## Calendario
```
<pega la cuadrícula del planificador>
```
## Días con poco personal y respaldos
| Día | Por qué hay poco personal | Respaldo |
|---|---|---|
| <21-23 de diciembre> | <exactamente 4 trabajando> | <guardia desde casa: ...> |
## Notas de permisos (para RRHH)
- <Omar: viaje aprobado 3 días por encima del permiso - confirmar transferencia / no pagado>
- <Lena: 15 días restantes, límite de transferencia 5 - sugerir reservar febrero>
## Próximos pasos
- <hablar con Ade, Lena, Jonas antes de publicar>
- <publicar el plan el ...; cambios por solicitud de intercambio a ...>
- <capacitación cruzada de un segundo respaldo de líder en el T1>
## Ejemplo de JSON de equipo
{
"team": "Soporte al Cliente Northwind",
"period": "2026-12-14..2027-01-08",
"workdays": ["lun", "mar", "mié", "jue", "vie"],
"holidays": ["2026-12-25", "2026-12-28", "2027-01-01"],
"coverage": {"lead": 1, "tier2": 2, "oncall": 1, "any": 4},
"coverage_overrides": [
{"dates": "2026-12-24", "coverage": {"tier2": 1, "any": 3}},
{"dates": "2026-12-31", "coverage": {"tier2": 1, "any": 3}}
],
"blackout": [
{"dates": "2027-01-05..2027-01-06", "reason": "Ejecución de facturación de enero, todos manos a la obra"}
],
"peak_days": "2026-12-21..2027-01-01",
"carryover_max": 5,
"people": [
{"name": "Maya", "roles": ["lead", "tier2"], "allowance": 25, "taken": 17, "worked_peak_last_year": true},
{"name": "Ade", "roles": ["lead", "tier2"], "allowance": 25, "taken": 19},
{"name": "Omar", "roles": ["tier2", "oncall"], "allowance": 25, "taken": 20},
{"name": "Lena", "roles": ["tier1", "tier2"], "allowance": 25, "taken": 10},
{"name": "Jonas", "roles": ["tier1", "oncall"], "allowance": 25, "taken": 18},
{"name": "Sam", "roles": ["tier1", "oncall"], "allowance": 25, "taken": 15, "worked_peak_last_year": true},
{"name": "Priya", "roles": ["tier1"], "days": ["lun", "mié", "jue"], "allowance": 15, "taken": 9}
]
}
## Ejemplo de CSV de vacaciones
person,start,end,type,status,submitted,half_day,notes
Maya,2026-12-21,2026-12-24,vacation,approved,2026-09-30,,visita familiar
Omar,2026-12-29,2027-01-08,vacation,approved,2026-10-02,,"vuelos reservados, Lagos"
Lena,2026-12-14,2026-12-15,training,approved,2026-11-01,,curso de administración Zendesk
Jonas,2026-12-21,2026-12-23,vacation,requested,2026-10-20,,
Ade,2026-12-29,2026-12-31,vacation,requested,2026-10-25,,
Sam,2026-12-24,2027-01-04,vacation,requested,2026-11-05,,"esquí, trabajó la última Navidad"
Lena,2026-12-30,2027-01-08,vacation,requested,2026-11-12,,
Priya,2026-12-16,2026-12-16,vacation,requested,2026-11-20,yes,obra de teatro navideña del colegio
Jonas,2027-01-04,2027-01-08,vacation,requested,2026-11-25,,
Priya,2026-12-21,2026-12-23,vacation,requested,2026-11-28,,
## Ejemplo de salida del planificador
# Plan de cobertura de vacaciones: Soporte al Cliente Northwind (2026-12-14 a 2027-01-08)
Cobertura mínima por día laborable: any 4, lead 1, oncall 1, tier2 2. Festivos (cerrado): 25 de diciembre, 28 de diciembre, 01 de enero.
**Veredicto: sobre permiso en vacaciones aprobadas** - 4 de 7 solicitudes abiertas se pueden aprobar sin un hueco; 3 necesitan una decisión.
## Solicitudes (en orden de decisión: trabajó el día punta el año pasado primero, luego por orden de llegada)
| # | Persona | Fechas | Días laborables | Tipo | Enviado | Decisión | Por qué | Alternativa |
|---|---|---|---|---|---|---|---|---|
| 1 | Sam | jue 24 de diciembre - lun 04 de enero | 5 | vacation | 2026-11-05 | aprobar | - | - |
| 2 | Jonas | lun 21 de diciembre - mié 23 de diciembre | 3 | vacation | 2026-10-20 | aprobar | - | - |
| 3 | Ade | mar 29 de diciembre - jue 31 de diciembre | 3 | vacation | 2026-10-25 | conflicto | any/tier2 corto el 29 de diciembre, 30 de diciembre | mié 16 de diciembre a vie 18 de diciembre |
| 4 | Lena | mié 30 de diciembre - vie 08 de enero | 7 | vacation | 2026-11-12 | bloqueo | Ejecución de facturación de enero, todos manos a la obra (05 de enero, 06 de enero) | acortar a jue 31 de diciembre - jue 31 de diciembre (1 día(s) laborable(s), de vuelta antes del bloqueo) |
| 5 | Priya | mié 16 de diciembre - mié 16 de diciembre (medio) | 0.5 | vacation | 2026-11-20 | aprobar | - | - |
| 6 | Jonas | lun 04 de enero - vie 08 de enero | 5 | vacation | 2026-11-25 | sobre permiso | necesita 5 días, 4 restantes | - |
| 7 | Priya | lun 21 de diciembre - mié 23 de diciembre | 2 | vacation | 2026-11-28 | aprobar | - | - |
## Calendario (aprobado + recomendado)
```
D J
11111222222233000000
45678123458901145678
MTWTFMTWTFMTWTFMTWTF
Maya .....VVVVHH...H.....
Ade .........HHxxxH.....
Omar .........HHVVVHVVVVV
Lena TT.......HH.xxHxxxxx
Jonas .....vvv.HH...Hxxxxx
Sam ........vHHvvvHv....
Priya .-h.-v-v.HH-..H.-..-
cover any +++++++++ +++ +++++
cover lead +++++++++ +++ +++++
cover oncall +++++++++ +++ +++++
cover tier2 +++++++++ +++ +++++
```
V/T/S/P/U/O vacaciones/formación/enfermedad/parental/no pagado/otro aprobado, minúscula = solicitud recomendada para aprobación, h = medio día, x = solicitud no aprobada, H = festivo, - = no es un día laborable, + / ! = rol cubierto / corto.
## Permiso (días)
| Persona | Permiso | Tomado antes | Aprobado aquí | Recomendado | Restante | Días punta libres |
|---|---|---|---|---|---|---|
| Maya | 25 | 17 | 4 | 0 | 4 | 4 |
| Ade | 25 | 19 | 0 | 0 | 6 | 0 |
| Omar | 25 | 20 | 8 | 0 | -3 | 3 |
| Lena | 25 | 10 | 0 | 0 | 15 | 0 |
| Jonas | 25 | 18 | 0 | 3 | 4 | 3 |
| Sam | 25 | 15 | 0 | 5 | 5 | 4 |
| Priya | 15 | 9 | 0 | 2.5 | 3.5 | 2 |
## Hallazgos
- **ALTO LV02** Las vacaciones aprobadas de Omar exceden el permiso en 3 días (20 + 8 tomados de 25); confirma la transferencia o las vacaciones no pagadas con RRHH.
- **MEDIO LV02** Jonas 04 de enero-08 de enero: necesita 5 días, 4 restantes; aprueba un viaje más corto o acuerda días no pagados
- **MEDIO LV04** Ade 29 de diciembre-31 de diciembre: conflicto (any/tier2 corto el 29 de diciembre, 30 de diciembre); ofrece mié 16 de diciembre a vie 18 de diciembre
- **MEDIO LV04** Lena 30 de diciembre-08 de enero: bloqueo (Ejecución de facturación de enero, todos manos a la obra (05 de enero, 06 de enero)); ofrece acortar a jue 31 de diciembre - jue 31 de diciembre (1 día(s) laborable(s), de vuelta antes del bloqueo)
- **BAJO LV03** lead tiene una persona de repuesto (Maya, Ade para un mínimo de 1); dos ausencias a la vez lo rompen.
- **BAJO LV06** Ade aún tendrá 6 días restantes (límite de transferencia 5); 1 día(s) se pierden a menos que se reserven.
- **BAJO LV06** Lena aún tendrá 15 días restantes (límite de transferencia 5); 10 día(s) se pierden a menos que se reserven.
- **INFO LV07** Sin margen en 4 día(s) (21 de diciembre, 22 de diciembre, 23 de diciembre, 29 de diciembre): un día de enfermedad rompe la cobertura; prepara un respaldo.
## Script del planificador Python
```python
#!/usr/bin/env python3
"""Planificador de cobertura de vacaciones y equipo (solo biblioteca estándar de Python 3.8+).
Verifica las solicitudes de vacaciones de un equipo contra las reglas de dotación mínima día a día, decide qué
solicitudes se pueden aprobar sin dejar un hueco, sugiere fechas alternativas para el resto, y
realiza un seguimiento de los permisos y quién obtiene los días punta de vacaciones.
JSON del equipo:
{
"team": "Mesa de Soporte", "period": "2026-12-14..2027-01-08",
"workdays": ["lun", "mar", "mié", "jue", "vie"],
"holidays": ["2026-12-25", "2026-12-28", "2027-01-01"], (oficina cerrada, no se necesita cobertura)
"coverage": {"lead": 1, "tier2": 2, "oncall": 1, "any": 4}, ("any" = total de personas trabajando)
"coverage_overrides": [{"dates": "2026-12-24", "coverage": {"tier2": 1, "any": 3}}],
"blackout": [{"dates": "2027-01-05..2027-01-06", "reason": "ejecución de facturación"}],
"peak_days": "2026-12-21..2027-01-01",
"people": [
{"name": "Maya", "roles": ["lead", "tier2"], "allowance": 25, "taken": 17, "worked_peak_last_year": true},
{"name": "Priya", "roles": ["tier1"], "days": ["lun", "mié", "jue"], "allowance": 15, "taken": 9}
]
}
Cada persona trabajadora llena UN puesto de rol por día (un líder que también es nivel 2 cuenta una vez), resuelto
como un emparejamiento bipartito.
CSV de vacaciones (encabezado requerido): person,start,end,type,status,submitted,half_day,notes
type vacation (cuenta contra el permiso) | sick | training | parental | unpaid | other
status approved | requested | declined (ignorado)
half_day yes = medio día de permiso; la persona no se cuenta para cobertura ese día
Uso:
python3 leave_cover.py team.json leave.csv [--json]
python3 leave_cover.py --self-test
Códigos de salida: 0 = las vacaciones aprobadas mantienen la cobertura y nadie supera el permiso, 1 = las vacaciones aprobadas
ya rompen la cobertura o exceden un permiso, 2 = entrada incorrecta.
"""
import argparse
import csv
import datetime as dt
import json
import sys
from collections import OrderedDict, defaultdict
DAYS = ["lun", "mar", "mié", "jue", "vie", "sáb", "dom"]
TYPE_CODE = {"vacation": "V", "sick": "S", "training": "T", "parental": "P", "unpaid": "U", "other": "O"}
class InputError(ValueError):
pass
def d(s, what="fecha"):
try:
return dt.date.fromisoformat(str(s).strip())
except ValueError:
raise InputError("%s %r no es AAAA-MM-DD" % (what, s))
def date_range(spec):
spec = str(spec)
a, _, b = spec.partition("..")
a, b = d(a), d(b or a)
if b < a:
raise InputError("el rango %r termina antes de comenzar" % spec)
return [a + dt.timedelta(days=i) for i in range((b - a).days + 1)]
# ----------------------------------------------------------------------------- entradas
def load_team(cfg):
if "period" not in cfg or not cfg.get("people"):
raise InputError("el JSON del equipo necesita 'period' y 'people'")
t = dict(cfg)
t["days"] = date_range(cfg["period"])
t["workdays"] = [w.lower()[:3] for w in cfg.get("workdays", DAYS[:5])]
t["holidays"] = {d(h, "festivo") for h in cfg.get("holidays", [])}
t["coverage"] = {k: int(v) for k, v in (cfg.get("coverage") or {}).items()}
over = {}
for o in cfg.get("coverage_overrides", []):
for day in date_range(o["dates"]):
over[day] = {k: int(v) for k, v in o["coverage"].items()}
t["overrides"] = over
t["blackout"] = [(set(date_range(b["dates"])), b.get("reason", "bloqueo")) for b in cfg.get("blackout", [])]
t["peak"] = set(date_range(cfg["peak_days"])) if cfg.get("peak_days") else set()
people = OrderedDict()
for p in cfg["people"]:
if "name" not in p:
raise InputError("toda persona necesita un nombre")
q = dict(p)
q["roles"] = list(p.get("roles", []))
q["days"] = [w.lower()[:3] for w in p.get("days", t["workdays"])]
q["allowance"] = float(p.get("allowance", 0))
q["taken"] = float(p.get("taken", 0))
q["carryover_max"] = p.get("carryover_max", cfg.get("carryover_max"))
people[p["name"]] = q
t["people"] = people
roles = {r for p in people.values() for r in p["roles"]}
for r in list(t["coverage"]) + [r for o in over.values() for r in o]:
if r != "any" and r not in roles:
raise InputError("el rol de cobertura %r no lo tiene nadie" % r)
return t
def load_leave(path, team):
out = []
with open(path, newline="", encoding="utf-8") as f:
rows = [r for r in f if r.strip() and not r.lstrip().startswith("#")]
for n, row in enumerate(csv.DictReader(rows), 2):
name = (row.get("person") or "").strip()
if name not in team["people"]:
raise InputError("línea de vacaciones %d: persona desconocida %r" % (n, name))
typ = (row.get("type") or "vacation").strip().lower()
if typ not in TYPE_CODE:
raise InputError("línea de vacaciones %d: el tipo debe ser uno de %s" % (n, ", ".join(TYPE_CODE)))
status = (row.get("status") or "requested").strip().lower()
if status not in ("approved", "requested", "declined"):
raise InputError("línea de vacaciones %d: el estado debe ser approved, requested o declined" % n)
start, end = d(row.get("start"), "inicio"), d(row.get("end") or row.get("start"), "fin")
if end < start:
raise InputError("línea de vacaciones %d: el fin es antes del inicio" % n)
out.append({"id": n, "person": name, "start": start, "end": end, "type": typ, "status": status,
"submitted": (row.get("submitted") or "").strip() or "9999-12-31",
"half": (row.get("half_day") or "").strip().lower() in ("yes", "y", "true", "1"),
"notes": (row.get("notes") or "").strip()})
return out
# ----------------------------------------------------------------------------- núcleo
def is_workday(team, day):
return DAYS[day.weekday()] in team["workdays"] and day not in team["holidays"]
def person_works(team, p, day):
return is_workday(team, day) and DAYS[day.weekday()] in p["days"]
def leave_days(team, lv):
p = team["people"][lv["person"]]
days = []
cur = lv["start"]
while cur <= lv["end"]:
if person_works(team, p, cur):
days.append(cur)
cur += dt.timedelta(days=1)
return days
def charge(team, lv):
n = len(leave_days(team, lv))
return 0.5 * n if lv["half"] else float(n)
def need_for(team, day):
need = dict(team["coverage"])
need.update(team["overrides"].get(day, {}))
return need
def shortfall(team, day, off):
"""Devuelve {rol: faltante} para un día dado el conjunto de personas ausentes."""
if not is_workday(team, day):
return {}
avail = [p for n, p in team["people"].items() if n not in off and person_works(team, p, day)]
need = need_for(team, day)
slots = [r for r, k in sorted(need.items()) if r != "any" for _ in range(k)]
match = {} # índice de puesto -> nombre de persona
def try_assign(pi, seen):
for si, role in enumerate(slots):
if role in avail[pi]["roles"] and si not in seen:
seen.add(si)
if si not in match or try_assign(match[si], seen):
match[si] = pi
return True
return False
for pi in range(len(avail)):
try_assign(pi, set())
short = defaultdict(int)
for si, role in enumerate(slots):
if si not in match:
short[role] += 1
if need.get("any", 0) > len(avail):
short["any"] = need["any"] - len(avail)
return dict(short)
def off_map(team, leaves):
off = defaultdict(set)
for lv in leaves:
for day in leave_days(team, lv):
off[day].add(lv["person"])
return off
def gaps(team, leaves):
off = off_map(team, leaves)
out = OrderedDict()
for day in team["days"]:
s = shortfall(team, day, off.get(day, set()))
if s:
out[day] = s
return out
def total(g):
return sum(sum(v.values()) for v in g.values())
def new_gaps(before, after):
out = OrderedDict()
for day, s in after.items():
b = before.get(day, {})
worse = {r: k - b.get(r, 0) for r, k in s.items() if k > b.get(r, 0)}
if worse:
out[day] = worse
return out
def blackout_hit(team, lv):
days = set(leave_days(team, lv))
for bdays, reason in team["blackout"]:
hit = sorted(days & bdays)
if hit:
return hit, reason
return None, None
def suggest(team, current, lv, max_shift=15):
"""Desplazamiento más cercano de la misma solicitud (misma longitud de calendario) que mantiene la cobertura."""
length = (lv["end"] - lv["start"]).days
want = len(leave_days(team, lv))
base = gaps(team, current)
first, last = team["days"][0], team["days"][-1]
for shift in sorted(range(-max_shift, max_shift + 1), key=lambda x: (abs(x), x)):
if shift == 0:
continue
cand = dict(lv, start=lv["start"] + dt.timedelta(days=shift), end=lv["start"] + dt.timedelta(days=shift + length))
if cand["start"] < first or cand["end"] > last:
continue
if len(leave_days(team, cand)) < want or blackout_hit(team, cand)[0]:
continue
if not new_gaps(base, gaps(team, current + [cand])):
return cand
return None
def trimmed(team, current, lv):
"""Parte más larga de una solicitud que golpea un bloqueo, antes del primer día de bloqueo, que mantiene la cobertura."""
hit, _ = blackout_hit(team, lv)
days = [x for x in leave_days(team, lv) if x < hit[0]]
base = gaps(team, current)
best = None
for i in range(len(days)):
for j in range(len(days) - 1, i - 1, -1):
if best and j - i + 1 <= len(leave_days(team, best)):
break
cand = dict(lv, start=days[i], end=days[j])
if not new_gaps(base, gaps(team, current + [cand])):
best = cand
break
return best
def plan(team, leaves):
findings = []
def add(sev, code, msg):
findings.append({"severity": sev, "code": code, "message": msg})
approved = [l for l in leaves if l["status"] == "approved"]
requests = [l for l in leaves if l["status"] == "requested"]
used = defaultdict(float)
for l in approved:
if l["type"] == "vacation":
used[l["person"]] += charge(team, l)
g_approved = gaps(team, approved)
for day, s in g_approved.items():
add("high", "LV01", "%s %s: las vacaciones aprobadas ya dejan %s corto" % (
day.isoformat(), DAYS[day.weekday()].title(), ", ".join("%s por %d" % (r, k) for r, k in sorted(s.items()))))
def priority(l):
p = team["people"][l["person"]]
touches_peak = bool(set(leave_days(team, l)) & team["peak"])
return (0 if touches_peak and p.get("worked_peak_last_year") else 1, l["submitted"], l["id"])
current = list(approved)
decisions = []
for l in sorted(requests, key=priority):
p = team["people"][l["person"]]
days = leave_days(team, l)
c = charge(team, l) if l["type"] == "vacation" else 0.0
dec = {"leave": l, "workdays": len(days), "charge": c, "decision": "approve", "reason": "", "suggestion": None}
if not days:
dec.update(decision="sin días laborables", reason="cae en festivos / solo días no laborables")
elif l["type"] == "vacation" and p["allowance"] and p["taken"] + used[l["person"]] + c > p["allowance"] + 1e-9:
left = p["allowance"] - p["taken"] - used[l["person"]]
dec.update(decision="sobre permiso", reason="necesita %g días, %g restantes" % (c, max(left, 0)))
else:
hit, reason = blackout_hit(team, l)
if hit:
dec.update(decision="bloqueo", reason="%s (%s)" % (reason, ", ".join(x.strftime("%d %b") for x in hit)))
else:
ng = new_gaps(gaps(team, current), gaps(team, current + [l]))
if ng:
roles = sorted({r for s in ng.values() for r in s})
dec.update(decision="conflicto", reason="%s corto el %s" % (
"/".join(roles), ", ".join(x.strftime("%d %b") for x in ng)))
if dec["decision"] == "approve":
current.append(l)
used[l["person"]] += c
elif dec["decision"] in ("conflicto", "bloqueo"):
s = suggest(team, current, l)
if s:
dec["suggestion"] = "%s a %s" % (s["start"].strftime("%a %d %b"), s["end"].strftime("%a %d %b"))
elif dec["decision"] == "bloqueo":
t = trimmed(team, current, l)
if t:
dec["suggestion"] = "acortar a %s - %s (%d día(s) laborable(s), de vuelta antes del bloqueo)" % (
t["start"].strftime("%a %d %b"), t["end"].strftime("%a %d %b"), len(leave_days(team, t)))
decisions.append(dec)
approved_peak = defaultdict(int)
for l in current:
approved_peak[l["person"]] += len(set(leave_days(team, l)) & team["peak"])
for dec in decisions:
l = dec["leave"]
p = team["people"][l["person"]]
if dec["decision"] != "approve" and p.get("worked_peak_last_year") and set(leave_days(team, l)) & team["peak"]:
lucky = [n for n, k in approved_peak.items() if k and not team["people"][n].get("worked_peak_last_year")]
add("medium", "LV05", "%s trabajó el día punta el año pasado pero esta solicitud es %s mientras %s obtienen días punta libres; "
"considera un intercambio." % (l["person"], dec["decision"], ", ".join(sorted(lucky)) or "nadie"))
g_final = gaps(team, current)
allowance = []
for n, p in team["people"].items():
planned_req = sum(dec["charge"] for dec in decisions if dec["leave"]["person"] == n and dec["decision"] == "approve")
appr = used[n] - planned_req
left = p["allowance"] - p["taken"] - used[n] if p["allowance"] else None
allowance.append({"person": n, "allowance": p["allowance"], "taken_before": p["taken"], "approved": appr,
"recommended": planned_req, "left": left, "peak_days_off": approved_peak.get(n, 0)})
if p["allowance"] and p["taken"] + appr > p["allowance"] + 1e-9:
add("high", "LV02", "Las vacaciones aprobadas de %s exceden el permiso en %g días (%g + %g tomados de %g); "
"confirma la transferencia o las vacaciones no pagadas con RRHH." % (n, p["taken"] + appr - p["allowance"], p["taken"],
appr, p["allowance"]))
cm = p.get("carryover_max")
if left is not None and cm is not None and left > float(cm):
add("low", "LV06", "%s aún tendrá %g días restantes (límite de transferencia %g); %g día(s) se pierden a menos que se reserven."
% (n, left, float(cm), left - float(cm)))
for role, k in sorted(team["coverage"].items()):
if role == "any":
continue
holders = [n for n, p in team["people"].items() if role in p["roles"]]
if len(holders) <= k:
add("medium", "LV03", "Solo %d personas pueden cubrir %s (mínimo %d): cualquier ausencia de %s rompe la cobertura; "
"capacita a un respaldo." % (len(holders), role, k, ", ".join(holders)))
elif len(holders) == k + 1:
add("low", "LV03", "%s tiene una persona de repuesto (%s para un mínimo de %d); dos ausencias a la vez lo rompen."
% (role, ", ".join(holders), k))
for day, s in g_final.items():
if day not in g_approved:
add("high", "LV01", "%s aún corto después de las decisiones: %s" % (day, s))
for dec in decisions:
if dec["decision"] in ("conflicto", "bloqueo"):
l = dec["leave"]
add("medium", "LV04", "%s %s-%s: %s (%s)%s" % (
l["person"], l["start"].strftime("%d %b"), l["end"].strftime("%d %b"), dec["decision"], dec["reason"],
("; ofrece " + dec["suggestion"]) if dec["suggestion"] else "; sin alternativa sin huecos dentro de 15 días"))
elif dec["decision"] == "sobre permiso":
l = dec["leave"]
add("medium", "LV02", "%s %s-%s: %s; aprueba un viaje más corto o acuerda días no pagados" % (
l["person"], l["start"].strftime("%d %b"), l["end"].strftime("%d %b"), dec["reason"]))
thin = []
off = off_map(team, current)
for day in team["days"]:
if not is_workday(team, day):
continue
working = [n for n, p in team["people"].items() if n not in off.get(day, set()) and person_works(team, p, day)]
if len(working) == need_for(team, day).get("any", 0):
thin.append(day)
if thin:
add("info", "LV07", "Sin margen en %d día(s) (%s): un día de enfermedad rompe la cobertura; prepara un respaldo." % (
len(thin), ", ".join(x.strftime("%d %b") for x in thin)))
order = {"high": 0, "medium": 1, "low": 2, "info": 3}
findings.sort(key=lambda f: (order[f["severity"]], f["code"]))
return {"team": team.get("team", "Equipo"), "period": team["period"], "gaps_approved": g_approved,
"gaps_final": g_final, "decisions": decisions, "allowance": allowance, "findings": findings,
"current": current, "thin": thin}
# ----------------------------------------------------------------------------- informe
def grid(team, leaves_final, all_leaves):
days = [x for x in team["days"] if x.weekday() < 5 or DAYS[x.weekday()] in team["workdays"]]
final_ids = {l["id"] for l in leaves_final}
cell = defaultdict(dict)
for l in all_leaves:
if l["status"] == "declined":
continue
for day in leave_days(team, l):
if l["id"] in final_ids:
c = TYPE_CODE[l["type"]] if l["status"] == "approved" else TYPE_CODE[l["type"]].lower()
else:
c = "x"
if l["half"]:
c = "h" if c != "x" else "x"
cell[l["person"]][day] = c
width = max([len(n) for n in team["people"]] + [len("cover " + r) for r in team["coverage"]]) + 2
months = " " * width + "".join(x.strftime("%b")[0] if i == 0 or x.month != days[i - 1].month
else " " for i, x in enumerate(days))
L = [months, " " * width + "".join(str(x.day // 10) for x in days),
" " * width + "".join(str(x.day % 10) for x in days),
" " * width + "".join(DAYS[x.weekday()][0].upper() for x in days)]
for n, p in team["people"].items():
row = []
for x in days:
if x in team["holidays"]:
row.append("H")
elif not person_works(team, p, x):
row.append("-")
else:
row.append(cell[n].get(x, "."))
L.append(n.ljust(width) + "".join(row))
off = off_map(team, leaves_final)
for role in sorted(team["coverage"]):
row = []
for x in days:
if not is_workday(team, x):
row.append(" ")
continue
s = shortfall(team, x, off.get(x, set()))
row.append("!" if role in s else "+")
L.append(("cover " + role).ljust(width) + "".join(row))
return L
def report(team, leaves, r):
L = ["# Plan de cobertura de vacaciones: %s (%s)" % (r["team"], r["period"].replace("..", " a ")), ""]
need = ", ".join("%s %d" % (k, v) for k, v in sorted(team["coverage"].items()))
L.append("Cobertura mínima por día laborable: %s. Festivos (cerrado): %s." % (
need, ", ".join(x.strftime("%d %b") for x in sorted(team["holidays"])) or "ninguno"))
n_appr = sum(1 for dec in r["decisions"] if dec["decision"] == "approve")
verdict = ("las vacaciones aprobadas ya rompen la cobertura" if r["gaps_approved"] else
"sobre permiso en vacaciones aprobadas" if any(f["code"] == "LV02" and f["severity"] == "high" for f in r["findings"])
else "cubierto")
L += ["", "**Veredicto: %s** - %d de %d solicitudes abiertas se pueden aprobar sin un hueco; %d necesitan una decisión." % (
verdict, n_appr, len(r["decisions"]), len(r["decisions"]) - n_appr), ""]
L += ["## Solicitudes (en orden de decisión: trabajó el día punta el año pasado primero, luego por orden de llegada)", "",
"| # | Persona | Fechas | Días laborables | Tipo | Enviado | Decisión | Por qué | Alternativa |",
"|---|---|---|---|---|---|---|---|---|"]
for i, dec in enumerate(r["decisions"], 1):
l = dec["leave"]
L.append("| %d | %s | %s - %s%s | %s | %s | %s | %s | %s | %s |" % (
i, l["person"], l["start"].strftime("%a %d %b"), l["end"].strftime("%a %d %b"), " (medio)" if l["half"] else "",
("%g" % (0.5 * dec["workdays"])) if l["half"] else dec["workdays"], l["type"], l["submitted"][:10],
dec["decision"], dec["reason"] or "-", dec["suggestion"] or "-"))
L += ["", "## Calendario (aprobado + recomendado)", "", "```"] + grid(team, r["current"], leaves) + [
"```", "V/T/S/P/U/O vacaciones/formación/enfermedad/parental/no pagado/otro aprobado, minúscula = solicitud recomendada "
"para aprobación, h = medio día, x = solicitud no aprobada, H = festivo, - = no es un día laborable, "
"+ / ! = rol cubierto / corto.", ""]
L += ["## Permiso (días)", "", "| Persona | Permiso | Tomado antes | Aprobado aquí | Recomendado | Restante | Días punta libres |",
"|---|---|---|---|---|---|---|"]
for a in r["allowance"]:
L.append("| %s | %g | %g | %g | %g | %s | %d |" % (a["person"], a["allowance"], a["taken_before"], a["approved"],
a["recommended"], "-" if a["left"] is None else "%g" % a["left"],
a["peak_days_off"]))
L += ["", "## Hallazgos", ""]
if not r["findings"]:
L.append("- ninguno")
for f in r["findings"]:
L.append("- **%s %s** %s" % (f["severity"].upper(), f["code"], f["message"]))
return "\n".join(L) + "\n"
# ----------------------------------------------------------------------------- auto-prueba
def self_test():
import os
import tempfile
cases = []
def ok(name, cond, detail=""):
cases.append((name, bool(cond), detail))
base = {"team": "T", "period": "2026-12-14..2026-12-25", "holidays": ["2026-12-25"], "peak_days": "2026-12-21..2026-12-24",
"coverage": {"lead": 1, "dev": 1, "any": 2},
"people": [{"name": "A", "roles": ["lead", "dev"], "allowance": 10, "taken": 0},
{"name": "B", "roles": ["dev"], "allowance": 10, "taken": 9},
{"name": "C", "roles": ["lead"], "allowance": 10, "taken": 0, "worked_peak_last_year": True},
{"name": "D", "roles": ["dev"], "days": ["lun", "mar"], "allowance": 5, "taken": 0}]}
t = load_team(base)
ok("los fines de semana y festivos no son días laborables", not is_workday(t, d("2026-12-19")) and not is_workday(t, d("2026-12-25")))
sf = shortfall(t, d("2026-12-16"), {"B", "C", "D"})
ok("una persona llena un puesto (A no puede ser lead y dev)", sf.get("any") == 1 and sf.get("lead", 0) + sf.get("dev", 0) == 1, sf)
ok("el emparejamiento encuentra A=dev, C=lead", shortfall(t, d("2026-12-16"), {"B", "D"}) == {})
ok("el trabajador a tiempo parcial solo cuenta en sus días", shortfall(t, d("2026-12-17"), {"A", "B"}) == {"dev": 1, "any": 1}, shortfall(t, d("2026-12-17"), {"A", "B"}))
lv = {"id": 1, "person": "D", "start": d("2026-12-14"), "end": d("2026-12-20"), "type": "vacation",
"status": "requested", "submitted": "2026-11-01", "half": False, "notes": ""}
ok("los días de vacaciones omiten los días no laborables", len(leave_days(t, lv)) == 2)
ok("el medio día carga 0.5", charge(t, dict(lv, half=True)) == 1.0)
d0 = tempfile.mkdtemp()
csvp = os.path.join(d0, "l.csv")
with open(csvp, "w") as f:
f.write("person,start,end,type,status,submitted,half_day,notes\n"
"A,2026-12-21,2026-12-22,vacation,approved,2026-10-01,,\n"
"C,2026-12-21,2026-12-21,vacation,requested,2026-11-10,,\n"
"B,2026-12-14,2026-12-16,vacation,requested,2026-10-05,,\n"
"D,2026-12-15,2026-12-15,training,requested,2026-11-01,,\n")
leaves = load_leave(csvp, t)
r = plan(t, leaves)
by = {(dec["leave"]["person"]): dec for dec in r["decisions"]}
ok("C conflictivo con las vacaciones aprobadas de A (sin lead)", by["C"]["decision"] == "conflicto", by["C"])
ok("el conflicto obtiene una fecha alternativa", by["C"]["suggestion"] is not None, by["C"])
ok("B sobre permiso (3 días, 1 restante)", by["B"]["decision"] == "sobre permiso", by["B"])
ok("formación de D aprobada", by["D"]["decision"] == "approve", by["D"])
ok("la persona que trabajó el día punta se decide primero", r["decisions"][0]["leave"]["person"] == "C", [x["leave"]["person"] for x in r["decisions"]])
codes = {f["code"] for f in r["findings"]}
ok("LV03 punto único de fallo para lead", "LV03" in codes, codes)
ok("LV05 bandera de equidad para C", "LV05" in codes, codes)
ok("sin huecos de vacaciones aprobadas", not r["gaps_approved"])
with open(csvp, "a") as f:
f.write("C,2026-12-21,2026-12-21,sick,approved,2026-12-01,,\n")
r2 = plan(t, load_leave(csvp, t))
ok("LV01 hueco en vacaciones aprobadas", any(f["code"] == "LV01" and f["severity"] == "high" for f in r2["findings"]))
t2 = load_team(dict(base, blackout=[{"dates": "2026-12-16", "reason": "lanzamiento"}]))
r3 = plan(t2, load_leave(csvp, t2))
ok("el bloqueo precede a la cobertura", any(x["decision"] == "bloqueo" for x in r3["decisions"]) or
all(x["leave"]["person"] != "B" or x["decision"] == "sobre permiso" for x in r3["decisions"]))
lvb = dict(lv, person="A", start=d("2026-12-16"), end=d("2026-12-16"))
ok("blackout_hit detecta la superposición", blackout_hit(t2, lvb)[0] == [d("2026-12-16")])
try:
load_team(dict(base, coverage={"qa": 1}))
ok("rol de cobertura desconocido rechazado", False)
except InputError:
ok("rol de cobertura desconocido rechazado", True)
rep = report(t, leaves, r)
ok("el informe tiene cuadrícula y veredicto", "Veredicto" in rep and "cover lead" in rep)
for name, good, detail in cases:
print("%s %s%s" % ("PASS" if good else "FAIL", name, "" if good else " -> %r" % (detail,)))
passed = sum(1 for _, g, _ in cases if g)
print("%d/%d pasaron" % (passed, len(cases)))
return 0 if passed == len(cases) else 1
def to_json(r):
def conv(o):
if isinstance(o, dt.date):
return o.isoformat()
if isinstance(o, dict):
return {(k.isoformat() if isinstance(k, dt.date) else k): conv(v) for k, v in o.items()}
if isinstance(o, (list, tuple)):
return [conv(x) for x in o]
return o
return json.dumps(conv(r), indent=2)
def main(argv=None):
ap = argparse.ArgumentParser(description="Planifica las vacaciones del equipo contra la cobertura mínima.")
ap.add_argument("team", nargs="?")
ap.add_argument("leave", nargs="?")
ap.add_argument("--json", action="store_true")
ap.add_argument("--self-test", action="store_true")
a = ap.parse_args(argv)
if a.self_test:
return self_test()
if not (a.team and a.leave):
ap.print_usage(sys.stderr)
return 2
try:
with open(a.team, encoding="utf-8") as f:
team = load_team(json.load(f))
leaves = load_leave(a.leave, team)
r = plan(team, leaves)
except (InputError, OSError, json.JSONDecodeError, KeyError) as e:
print("error: %s" % e, file=sys.stderr)
return 2
print(to_json(r) if a.json else report(team, leaves, r), end="\n" if a.json else "")
bad = r["gaps_approved"] or any(f["code"] == "LV02" and f["severity"] == "high" for f in r["findings"])
return 1 if bad else 0
if __name__ == "__main__":
sys.exit(main())
```
Iniciá sesión para ver el prompt completo
Continuar con:
Al iniciar sesión, aceptás nuestros Términos de uso y Política de privacidad
Uso
Este prompt está diseñado para usarse con business. Copiá el contenido de arriba y pegalo en tu herramienta de IA preferida.
Para mejores resultados, personalizá los marcadores (indicados con corchetes o mayúsculas) con tus requisitos específicos.
Discusión
0 comentarios