operx-cli 0.7.2__tar.gz → 0.7.3__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {operx_cli-0.7.2 → operx_cli-0.7.3}/PKG-INFO +7 -1
- {operx_cli-0.7.2 → operx_cli-0.7.3}/README.md +6 -0
- {operx_cli-0.7.2 → operx_cli-0.7.3}/operx_cli/AGENTES.md +8 -2
- {operx_cli-0.7.2 → operx_cli-0.7.3}/operx_cli/__init__.py +1 -1
- {operx_cli-0.7.2 → operx_cli-0.7.3}/operx_cli/app.py +2 -1
- operx_cli-0.7.3/operx_cli/commands/rutinas.py +169 -0
- {operx_cli-0.7.2 → operx_cli-0.7.3}/operx_cli/shell.py +1 -0
- {operx_cli-0.7.2 → operx_cli-0.7.3}/operx_cli.egg-info/PKG-INFO +7 -1
- {operx_cli-0.7.2 → operx_cli-0.7.3}/operx_cli.egg-info/SOURCES.txt +2 -0
- {operx_cli-0.7.2 → operx_cli-0.7.3}/pyproject.toml +1 -1
- operx_cli-0.7.3/tests/test_rutinas.py +128 -0
- {operx_cli-0.7.2 → operx_cli-0.7.3}/operx_cli/agent_guide.py +0 -0
- {operx_cli-0.7.2 → operx_cli-0.7.3}/operx_cli/api_fields.py +0 -0
- {operx_cli-0.7.2 → operx_cli-0.7.3}/operx_cli/browser_login.py +0 -0
- {operx_cli-0.7.2 → operx_cli-0.7.3}/operx_cli/client.py +0 -0
- {operx_cli-0.7.2 → operx_cli-0.7.3}/operx_cli/commands/__init__.py +0 -0
- {operx_cli-0.7.2 → operx_cli-0.7.3}/operx_cli/commands/account.py +0 -0
- {operx_cli-0.7.2 → operx_cli-0.7.3}/operx_cli/commands/ambiental.py +0 -0
- {operx_cli-0.7.2 → operx_cli-0.7.3}/operx_cli/commands/articulos.py +0 -0
- {operx_cli-0.7.2 → operx_cli-0.7.3}/operx_cli/commands/avisos.py +0 -0
- {operx_cli-0.7.2 → operx_cli-0.7.3}/operx_cli/commands/common.py +0 -0
- {operx_cli-0.7.2 → operx_cli-0.7.3}/operx_cli/commands/completar.py +0 -0
- {operx_cli-0.7.2 → operx_cli-0.7.3}/operx_cli/commands/empresas.py +0 -0
- {operx_cli-0.7.2 → operx_cli-0.7.3}/operx_cli/commands/inventario.py +0 -0
- {operx_cli-0.7.2 → operx_cli-0.7.3}/operx_cli/commands/lecturas.py +0 -0
- {operx_cli-0.7.2 → operx_cli-0.7.3}/operx_cli/commands/ordenes.py +0 -0
- {operx_cli-0.7.2 → operx_cli-0.7.3}/operx_cli/commands/personas.py +0 -0
- {operx_cli-0.7.2 → operx_cli-0.7.3}/operx_cli/commands/planta.py +0 -0
- {operx_cli-0.7.2 → operx_cli-0.7.3}/operx_cli/commands/preventivos.py +0 -0
- {operx_cli-0.7.2 → operx_cli-0.7.3}/operx_cli/commands/raw.py +0 -0
- {operx_cli-0.7.2 → operx_cli-0.7.3}/operx_cli/commands/registros.py +0 -0
- {operx_cli-0.7.2 → operx_cli-0.7.3}/operx_cli/commands/reportes.py +0 -0
- {operx_cli-0.7.2 → operx_cli-0.7.3}/operx_cli/commands/search.py +0 -0
- {operx_cli-0.7.2 → operx_cli-0.7.3}/operx_cli/commands/sst.py +0 -0
- {operx_cli-0.7.2 → operx_cli-0.7.3}/operx_cli/commands/update.py +0 -0
- {operx_cli-0.7.2 → operx_cli-0.7.3}/operx_cli/lookup.py +0 -0
- {operx_cli-0.7.2 → operx_cli-0.7.3}/operx_cli/output.py +0 -0
- {operx_cli-0.7.2 → operx_cli-0.7.3}/operx_cli/record_completion.py +0 -0
- {operx_cli-0.7.2 → operx_cli-0.7.3}/operx_cli/self_update.py +0 -0
- {operx_cli-0.7.2 → operx_cli-0.7.3}/operx_cli/settings.py +0 -0
- {operx_cli-0.7.2 → operx_cli-0.7.3}/operx_cli/spreadsheet.py +0 -0
- {operx_cli-0.7.2 → operx_cli-0.7.3}/operx_cli/updates.py +0 -0
- {operx_cli-0.7.2 → operx_cli-0.7.3}/operx_cli/values.py +0 -0
- {operx_cli-0.7.2 → operx_cli-0.7.3}/operx_cli/welcome.py +0 -0
- {operx_cli-0.7.2 → operx_cli-0.7.3}/operx_cli.egg-info/dependency_links.txt +0 -0
- {operx_cli-0.7.2 → operx_cli-0.7.3}/operx_cli.egg-info/entry_points.txt +0 -0
- {operx_cli-0.7.2 → operx_cli-0.7.3}/operx_cli.egg-info/requires.txt +0 -0
- {operx_cli-0.7.2 → operx_cli-0.7.3}/operx_cli.egg-info/top_level.txt +0 -0
- {operx_cli-0.7.2 → operx_cli-0.7.3}/setup.cfg +0 -0
- {operx_cli-0.7.2 → operx_cli-0.7.3}/tests/test_agentes.py +0 -0
- {operx_cli-0.7.2 → operx_cli-0.7.3}/tests/test_api_campos.py +0 -0
- {operx_cli-0.7.2 → operx_cli-0.7.3}/tests/test_app.py +0 -0
- {operx_cli-0.7.2 → operx_cli-0.7.3}/tests/test_articulos.py +0 -0
- {operx_cli-0.7.2 → operx_cli-0.7.3}/tests/test_cards.py +0 -0
- {operx_cli-0.7.2 → operx_cli-0.7.3}/tests/test_client.py +0 -0
- {operx_cli-0.7.2 → operx_cli-0.7.3}/tests/test_completar.py +0 -0
- {operx_cli-0.7.2 → operx_cli-0.7.3}/tests/test_daily_flows.py +0 -0
- {operx_cli-0.7.2 → operx_cli-0.7.3}/tests/test_inventario_lecturas.py +0 -0
- {operx_cli-0.7.2 → operx_cli-0.7.3}/tests/test_json_writes.py +0 -0
- {operx_cli-0.7.2 → operx_cli-0.7.3}/tests/test_login.py +0 -0
- {operx_cli-0.7.2 → operx_cli-0.7.3}/tests/test_logout_and_routes.py +0 -0
- {operx_cli-0.7.2 → operx_cli-0.7.3}/tests/test_lookup.py +0 -0
- {operx_cli-0.7.2 → operx_cli-0.7.3}/tests/test_more_of_the_api.py +0 -0
- {operx_cli-0.7.2 → operx_cli-0.7.3}/tests/test_ordenes.py +0 -0
- {operx_cli-0.7.2 → operx_cli-0.7.3}/tests/test_output.py +0 -0
- {operx_cli-0.7.2 → operx_cli-0.7.3}/tests/test_personas.py +0 -0
- {operx_cli-0.7.2 → operx_cli-0.7.3}/tests/test_preventivos.py +0 -0
- {operx_cli-0.7.2 → operx_cli-0.7.3}/tests/test_record_completion.py +0 -0
- {operx_cli-0.7.2 → operx_cli-0.7.3}/tests/test_registros.py +0 -0
- {operx_cli-0.7.2 → operx_cli-0.7.3}/tests/test_self_update.py +0 -0
- {operx_cli-0.7.2 → operx_cli-0.7.3}/tests/test_settings.py +0 -0
- {operx_cli-0.7.2 → operx_cli-0.7.3}/tests/test_shell.py +0 -0
- {operx_cli-0.7.2 → operx_cli-0.7.3}/tests/test_spreadsheet.py +0 -0
- {operx_cli-0.7.2 → operx_cli-0.7.3}/tests/test_sst_ambiental.py +0 -0
- {operx_cli-0.7.2 → operx_cli-0.7.3}/tests/test_updates.py +0 -0
- {operx_cli-0.7.2 → operx_cli-0.7.3}/tests/test_values.py +0 -0
- {operx_cli-0.7.2 → operx_cli-0.7.3}/tests/test_welcome.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: operx-cli
|
|
3
|
-
Version: 0.7.
|
|
3
|
+
Version: 0.7.3
|
|
4
4
|
Summary: La línea de comandos de OperX: tu empresa desde la terminal, con tu token de API.
|
|
5
5
|
Requires-Python: >=3.10
|
|
6
6
|
Description-Content-Type: text/markdown
|
|
@@ -172,6 +172,12 @@ operx personas importar empleados.xlsx --confirmar # guarda si ninguna fila ti
|
|
|
172
172
|
operx personas ausencia "Ana Ruiz" --tipo vacaciones --desde 02/11/2026 --hasta 06/11/2026
|
|
173
173
|
operx personas ausencia "Ana Ruiz" --tipo permiso --desde 09/11/2026 --horas 08:00-10:00
|
|
174
174
|
|
|
175
|
+
operx rutinas listar
|
|
176
|
+
operx rutinas crear "Contar la bodega" --rol Bodeguero # sin --rol ni --persona, es para todos
|
|
177
|
+
operx rutinas cambiar "Contar la bodega" --persona "Ana Ruiz"
|
|
178
|
+
operx rutinas quitar "Contar la bodega" # sale de los planes desde ahora
|
|
179
|
+
operx rutinas cumplimiento --mes 10/2026 # quién hizo sus rutinas (administradores)
|
|
180
|
+
|
|
175
181
|
operx sst acciones --estado abiertas
|
|
176
182
|
operx sst nueva-accion --titulo "Señalizar la salida" --para 15/11/2026 --responsable "Ana Ruiz"
|
|
177
183
|
operx sst accion-hecha 7 --evidencia "Se instalaron dos señales"
|
|
@@ -158,6 +158,12 @@ operx personas importar empleados.xlsx --confirmar # guarda si ninguna fila ti
|
|
|
158
158
|
operx personas ausencia "Ana Ruiz" --tipo vacaciones --desde 02/11/2026 --hasta 06/11/2026
|
|
159
159
|
operx personas ausencia "Ana Ruiz" --tipo permiso --desde 09/11/2026 --horas 08:00-10:00
|
|
160
160
|
|
|
161
|
+
operx rutinas listar
|
|
162
|
+
operx rutinas crear "Contar la bodega" --rol Bodeguero # sin --rol ni --persona, es para todos
|
|
163
|
+
operx rutinas cambiar "Contar la bodega" --persona "Ana Ruiz"
|
|
164
|
+
operx rutinas quitar "Contar la bodega" # sale de los planes desde ahora
|
|
165
|
+
operx rutinas cumplimiento --mes 10/2026 # quién hizo sus rutinas (administradores)
|
|
166
|
+
|
|
161
167
|
operx sst acciones --estado abiertas
|
|
162
168
|
operx sst nueva-accion --titulo "Señalizar la salida" --para 15/11/2026 --responsable "Ana Ruiz"
|
|
163
169
|
operx sst accion-hecha 7 --evidencia "Se instalaron dos señales"
|
|
@@ -68,12 +68,18 @@ llamada queda en el historial del token, marcada como hecha desde la CLI.
|
|
|
68
68
|
`traslado` y `revertir`.
|
|
69
69
|
- Equipos: `equipos parar`, `reanudar`, `hallazgo` y `hallazgo-a-orden`;
|
|
70
70
|
`lecturas registrar` y `anular`.
|
|
71
|
-
- Personas: `personas importar --confirmar` y `personas ausencia
|
|
71
|
+
- Personas: `personas importar --confirmar` y `personas ausencia`;
|
|
72
|
+
`rutinas crear`, `cambiar` y `quitar`.
|
|
72
73
|
- SG-SST y Ambiental: `sst nueva-accion` y `accion-hecha`;
|
|
73
74
|
`ambiental nueva-accion`, `accion-hecha`, `residuo` y `lectura`.
|
|
74
75
|
- `avisos --marcar-leidos`, y `operx api` con POST, PUT, PATCH o DELETE.
|
|
75
76
|
- Un error se corrige con su contrario, nunca borrando: `inventario revertir`
|
|
76
|
-
deshace un movimiento, `lecturas anular` una lectura.
|
|
77
|
+
deshace un movimiento, `lecturas anular` una lectura. La excepción es
|
|
78
|
+
`rutinas quitar`, que saca una rutina de los planes desde ahora; los días
|
|
79
|
+
que ya la tenían la siguen contando.
|
|
80
|
+
- Una rutina es para todos, para algunos roles (`--rol`, por su nombre) o
|
|
81
|
+
para algunas personas (`--persona`); `rutinas cambiar --rol` reemplaza la
|
|
82
|
+
lista entera de roles.
|
|
77
83
|
- SG-SST y Ambiental van por empresa: sin `--empresa`, la de la cuenta. Una
|
|
78
84
|
cuenta que lleva el SG-SST o lo ambiental de sus clientes ve las demás con
|
|
79
85
|
`operx sst empresas` o `operx ambiental empresas`.
|
|
@@ -15,7 +15,7 @@ from .agent_guide import guide_text
|
|
|
15
15
|
from .client import ApiError
|
|
16
16
|
from .commands import (
|
|
17
17
|
account, ambiental, articulos, avisos, completar, inventario, lecturas, ordenes, personas, planta, preventivos, raw,
|
|
18
|
-
registros, reportes, search, sst, update,
|
|
18
|
+
registros, reportes, rutinas, search, sst, update,
|
|
19
19
|
)
|
|
20
20
|
from .commands.common import CliError, Session
|
|
21
21
|
from .lookup import NoSingleMatch
|
|
@@ -155,6 +155,7 @@ app.add_typer(inventario.app, name="inventario")
|
|
|
155
155
|
app.add_typer(articulos.app, name="articulos")
|
|
156
156
|
app.add_typer(lecturas.app, name="lecturas")
|
|
157
157
|
app.add_typer(personas.app, name="personas")
|
|
158
|
+
app.add_typer(rutinas.app, name="rutinas")
|
|
158
159
|
app.add_typer(reportes.app, name="reportes")
|
|
159
160
|
app.add_typer(sst.app, name="sst")
|
|
160
161
|
app.add_typer(ambiental.app, name="ambiental")
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
"""`operx rutinas`: Personas › Rutinas, what goes on the day's plan of
|
|
2
|
+
everyone, of some roles or of some people, and whether each person did it
|
|
3
|
+
(backend/people/routine_views.py). A routine with no roles and no people is
|
|
4
|
+
for everyone."""
|
|
5
|
+
|
|
6
|
+
from datetime import date
|
|
7
|
+
from typing import Annotated, Optional
|
|
8
|
+
|
|
9
|
+
import typer
|
|
10
|
+
|
|
11
|
+
from ..lookup import NoSingleMatch, find_in, find_one
|
|
12
|
+
from ..output import note, print_record, print_rows
|
|
13
|
+
from ..values import shown_moment
|
|
14
|
+
from .common import (
|
|
15
|
+
CliError, CsvOption, JsonOption, YesOption, client_of, confirm_write, month, output_format, print_result,
|
|
16
|
+
without_empty,
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
app = typer.Typer(
|
|
20
|
+
help="Rutinas (Personas): lo que va en el plan del día de todos, de algunos roles o de algunas personas.",
|
|
21
|
+
no_args_is_help=True,
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
ROUTINE_COLUMNS = [
|
|
25
|
+
("id", "Id"), ("text", "Rutina"), ("roles", "Roles"), ("employees", "Personas"), ("people_count", "Les llega a"),
|
|
26
|
+
]
|
|
27
|
+
COMPLIANCE_COLUMNS = [
|
|
28
|
+
("name", "Persona"), ("position", "Cargo"), ("done", "Hechas"), ("due", "Debidas"), ("days_without_plan", "Días sin plan"),
|
|
29
|
+
]
|
|
30
|
+
|
|
31
|
+
RolOption = Annotated[
|
|
32
|
+
Optional[list[str]], typer.Option("--rol", help="El nombre de un rol que la recibe; repítelo para varios.")
|
|
33
|
+
]
|
|
34
|
+
PersonaOption = Annotated[
|
|
35
|
+
Optional[list[str]], typer.Option("--persona", help="El nombre o código de una persona que la recibe; repítelo para varias.")
|
|
36
|
+
]
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def _role_ids(client, names):
|
|
40
|
+
if not names:
|
|
41
|
+
return []
|
|
42
|
+
roles = client.get("people/routines/roles")
|
|
43
|
+
ids = []
|
|
44
|
+
for name in names:
|
|
45
|
+
try:
|
|
46
|
+
ids.append(find_in(roles, name, ["name"], "rol")["id"])
|
|
47
|
+
except NoSingleMatch as error:
|
|
48
|
+
# A short list: saying which there are saves a trip to the settings.
|
|
49
|
+
raise CliError(f"{error} Hay: {', '.join(role['name'] for role in roles)}.") from None
|
|
50
|
+
return ids
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def _employee_ids(client, names):
|
|
54
|
+
return [find_one(client, "employees", name, ["user_name", "employee_code"], "persona")["id"] for name in names or []]
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def _routine(client, text):
|
|
58
|
+
"""A routine by its id or its exact text."""
|
|
59
|
+
return find_in(client.get("people/routines"), text, ["id", "text"], "rutina")
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def _who(roles, people):
|
|
63
|
+
if not roles and not people:
|
|
64
|
+
return "para todos"
|
|
65
|
+
parts = []
|
|
66
|
+
if roles:
|
|
67
|
+
parts.append(f"los roles {', '.join(roles)}")
|
|
68
|
+
if people:
|
|
69
|
+
parts.append(f"las personas {', '.join(people)}")
|
|
70
|
+
return "para " + " y ".join(parts)
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
@app.command()
|
|
74
|
+
def listar(
|
|
75
|
+
ctx: typer.Context,
|
|
76
|
+
as_json: JsonOption = False,
|
|
77
|
+
as_csv: CsvOption = False,
|
|
78
|
+
):
|
|
79
|
+
"""Las rutinas de la empresa y a cuántas personas les llega cada una."""
|
|
80
|
+
output = output_format(as_json, as_csv)
|
|
81
|
+
print_rows(ROUTINE_COLUMNS, client_of(ctx).get("people/routines"), output)
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
@app.command()
|
|
85
|
+
def crear(
|
|
86
|
+
ctx: typer.Context,
|
|
87
|
+
texto: Annotated[str, typer.Argument(help="Lo que va en el plan: «Llenar la bitácora».")],
|
|
88
|
+
rol: RolOption = None,
|
|
89
|
+
persona: PersonaOption = None,
|
|
90
|
+
si: YesOption = False,
|
|
91
|
+
as_json: JsonOption = False,
|
|
92
|
+
):
|
|
93
|
+
"""Crea una rutina. Sin --rol ni --persona, es para todos."""
|
|
94
|
+
client = client_of(ctx)
|
|
95
|
+
body = {"text": texto, "role_ids": _role_ids(client, rol), "employee_ids": _employee_ids(client, persona)}
|
|
96
|
+
confirm_write(f"Crear la rutina «{texto}» {_who(rol, persona)}.", si)
|
|
97
|
+
created = client.send("POST", "people/routines", json=body).json()
|
|
98
|
+
print_result(
|
|
99
|
+
created, as_json,
|
|
100
|
+
f"Rutina {created['id']} creada: {created['text']}. Les llega a {created['people_count']} personas.",
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
@app.command()
|
|
105
|
+
def cambiar(
|
|
106
|
+
ctx: typer.Context,
|
|
107
|
+
rutina: Annotated[str, typer.Argument(help="El id o el texto exacto de la rutina.")],
|
|
108
|
+
texto: Annotated[Optional[str], typer.Option("--texto", help="El texto nuevo.")] = None,
|
|
109
|
+
rol: RolOption = None,
|
|
110
|
+
persona: PersonaOption = None,
|
|
111
|
+
para_todos: Annotated[bool, typer.Option("--para-todos", help="La deja para todos, sin roles ni personas.")] = False,
|
|
112
|
+
si: YesOption = False,
|
|
113
|
+
as_json: JsonOption = False,
|
|
114
|
+
):
|
|
115
|
+
"""Cambia una rutina. --rol y --persona reemplazan la lista entera; lo que no digas queda como estaba."""
|
|
116
|
+
if para_todos and (rol or persona):
|
|
117
|
+
raise CliError("--para-todos no va con --rol ni --persona.")
|
|
118
|
+
if not (texto or rol or persona or para_todos):
|
|
119
|
+
raise CliError("Di qué cambiar: --texto, --rol, --persona o --para-todos.")
|
|
120
|
+
client = client_of(ctx)
|
|
121
|
+
found = _routine(client, rutina)
|
|
122
|
+
body = without_empty({"text": texto})
|
|
123
|
+
if para_todos:
|
|
124
|
+
body["role_ids"] = []
|
|
125
|
+
body["employee_ids"] = []
|
|
126
|
+
if rol:
|
|
127
|
+
body["role_ids"] = _role_ids(client, rol)
|
|
128
|
+
if persona:
|
|
129
|
+
body["employee_ids"] = _employee_ids(client, persona)
|
|
130
|
+
confirm_write(f"Cambiar la rutina «{found['text']}».", si)
|
|
131
|
+
changed = client.send("PATCH", f"people/routines/{found['id']}", json=body).json()
|
|
132
|
+
print_result(
|
|
133
|
+
changed, as_json,
|
|
134
|
+
f"Rutina {changed['id']} cambiada: {changed['text']}. Les llega a {changed['people_count']} personas.",
|
|
135
|
+
)
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
@app.command()
|
|
139
|
+
def quitar(
|
|
140
|
+
ctx: typer.Context,
|
|
141
|
+
rutina: Annotated[str, typer.Argument(help="El id o el texto exacto de la rutina.")],
|
|
142
|
+
si: YesOption = False,
|
|
143
|
+
as_json: JsonOption = False,
|
|
144
|
+
):
|
|
145
|
+
"""Quita una rutina de los planes desde ahora; los días que ya la tenían la siguen contando."""
|
|
146
|
+
client = client_of(ctx)
|
|
147
|
+
found = _routine(client, rutina)
|
|
148
|
+
confirm_write(f"Quitar la rutina «{found['text']}» de los planes.", si)
|
|
149
|
+
client.send("DELETE", f"people/routines/{found['id']}")
|
|
150
|
+
print_result({"id": found["id"], "removed": True}, as_json, f"Rutina {found['id']} quitada: {found['text']}.")
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
@app.command()
|
|
154
|
+
def cumplimiento(
|
|
155
|
+
ctx: typer.Context,
|
|
156
|
+
mes: Annotated[Optional[str], typer.Option("--mes", help="El mes: 11/2026; sin él, el actual.")] = None,
|
|
157
|
+
as_json: JsonOption = False,
|
|
158
|
+
as_csv: CsvOption = False,
|
|
159
|
+
):
|
|
160
|
+
"""Quién hizo sus rutinas en un mes: por persona, las que traía su plan y las que marcó. Solo administradores."""
|
|
161
|
+
output = output_format(as_json, as_csv)
|
|
162
|
+
month_text = month(mes) if mes else date.today().strftime("%Y-%m")
|
|
163
|
+
answer = client_of(ctx).get("people/routines/compliance", {"month": month_text})
|
|
164
|
+
if output == "json":
|
|
165
|
+
print_record(answer, "json")
|
|
166
|
+
return
|
|
167
|
+
if answer.get("through"):
|
|
168
|
+
note(f"Cuenta hasta el {shown_moment(answer['through'])}: hoy no cuenta.")
|
|
169
|
+
print_rows(COMPLIANCE_COLUMNS, answer.get("people", []), output)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: operx-cli
|
|
3
|
-
Version: 0.7.
|
|
3
|
+
Version: 0.7.3
|
|
4
4
|
Summary: La línea de comandos de OperX: tu empresa desde la terminal, con tu token de API.
|
|
5
5
|
Requires-Python: >=3.10
|
|
6
6
|
Description-Content-Type: text/markdown
|
|
@@ -172,6 +172,12 @@ operx personas importar empleados.xlsx --confirmar # guarda si ninguna fila ti
|
|
|
172
172
|
operx personas ausencia "Ana Ruiz" --tipo vacaciones --desde 02/11/2026 --hasta 06/11/2026
|
|
173
173
|
operx personas ausencia "Ana Ruiz" --tipo permiso --desde 09/11/2026 --horas 08:00-10:00
|
|
174
174
|
|
|
175
|
+
operx rutinas listar
|
|
176
|
+
operx rutinas crear "Contar la bodega" --rol Bodeguero # sin --rol ni --persona, es para todos
|
|
177
|
+
operx rutinas cambiar "Contar la bodega" --persona "Ana Ruiz"
|
|
178
|
+
operx rutinas quitar "Contar la bodega" # sale de los planes desde ahora
|
|
179
|
+
operx rutinas cumplimiento --mes 10/2026 # quién hizo sus rutinas (administradores)
|
|
180
|
+
|
|
175
181
|
operx sst acciones --estado abiertas
|
|
176
182
|
operx sst nueva-accion --titulo "Señalizar la salida" --para 15/11/2026 --responsable "Ana Ruiz"
|
|
177
183
|
operx sst accion-hecha 7 --evidencia "Se instalaron dos señales"
|
|
@@ -40,6 +40,7 @@ operx_cli/commands/preventivos.py
|
|
|
40
40
|
operx_cli/commands/raw.py
|
|
41
41
|
operx_cli/commands/registros.py
|
|
42
42
|
operx_cli/commands/reportes.py
|
|
43
|
+
operx_cli/commands/rutinas.py
|
|
43
44
|
operx_cli/commands/search.py
|
|
44
45
|
operx_cli/commands/sst.py
|
|
45
46
|
operx_cli/commands/update.py
|
|
@@ -63,6 +64,7 @@ tests/test_personas.py
|
|
|
63
64
|
tests/test_preventivos.py
|
|
64
65
|
tests/test_record_completion.py
|
|
65
66
|
tests/test_registros.py
|
|
67
|
+
tests/test_rutinas.py
|
|
66
68
|
tests/test_self_update.py
|
|
67
69
|
tests/test_settings.py
|
|
68
70
|
tests/test_shell.py
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "operx-cli"
|
|
7
|
-
version = "0.7.
|
|
7
|
+
version = "0.7.3"
|
|
8
8
|
description = "La línea de comandos de OperX: tu empresa desde la terminal, con tu token de API."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.10"
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import json
|
|
2
|
+
|
|
3
|
+
import pytest
|
|
4
|
+
|
|
5
|
+
from .fake_api import FakeApi, page, run
|
|
6
|
+
|
|
7
|
+
# people/routine_views.py routines_payload
|
|
8
|
+
BITACORA = {"id": 3, "text": "Llenar la bitácora", "roles": [], "employees": [], "people_count": 12}
|
|
9
|
+
HERRAMIENTAS = {
|
|
10
|
+
"id": 4, "text": "Recoger herramientas", "roles": [{"id": 8, "name": "Bodeguero"}],
|
|
11
|
+
"employees": [{"id": 7, "name": "María Torres"}], "people_count": 2,
|
|
12
|
+
}
|
|
13
|
+
# people/routine_views.py RoutineRolesView
|
|
14
|
+
ROLES = [{"id": 8, "name": "Bodeguero"}, {"id": 9, "name": "Técnico"}]
|
|
15
|
+
# employees/serializers.py: the staff list
|
|
16
|
+
MARIA = {"id": 7, "user_name": "María Torres", "employee_code": "E-007"}
|
|
17
|
+
# people/routine_views.py RoutineComplianceView
|
|
18
|
+
COMPLIANCE = {
|
|
19
|
+
"month": "2026-10", "previous_month": "2026-09", "next_month": None, "is_current_month": True,
|
|
20
|
+
"through": "2026-10-25", "days": ["2026-10-24", "2026-10-25"],
|
|
21
|
+
"people": [
|
|
22
|
+
{"employee_id": 7, "name": "María Torres", "position": "Técnico", "done": 3, "due": 4, "days_without_plan": 1,
|
|
23
|
+
"days": [{"date": "2026-10-24", "plan": True, "done": 3, "due": 4}]},
|
|
24
|
+
],
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@pytest.fixture(autouse=True)
|
|
29
|
+
def signed_in(monkeypatch):
|
|
30
|
+
monkeypatch.setenv("OPERX_TOKEN", "opx_test")
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def test_listar_shows_who_gets_each_routine():
|
|
34
|
+
fake = FakeApi({("GET", "people/routines/"): [BITACORA, HERRAMIENTAS]})
|
|
35
|
+
result = run(fake, "rutinas", "listar")
|
|
36
|
+
assert result.exit_code == 0, result.output
|
|
37
|
+
assert "Llenar la bitácora" in result.output and "Recoger herramientas" in result.output
|
|
38
|
+
assert "Bodeguero" in result.output and "María Torres" in result.output
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def test_crear_names_the_roles_and_people_and_sends_their_ids():
|
|
42
|
+
created = {**HERRAMIENTAS, "id": 5, "text": "Contar la bodega"}
|
|
43
|
+
fake = FakeApi({
|
|
44
|
+
("GET", "people/routines/roles/"): ROLES,
|
|
45
|
+
("GET", "employees/"): page(MARIA),
|
|
46
|
+
("POST", "people/routines/"): (201, created),
|
|
47
|
+
})
|
|
48
|
+
result = run(fake, "rutinas", "crear", "Contar la bodega", "--rol", "bodeguero", "--persona", "María Torres", "--si")
|
|
49
|
+
assert result.exit_code == 0, result.output
|
|
50
|
+
assert fake.sent("POST", "people/routines/") == {"text": "Contar la bodega", "role_ids": [8], "employee_ids": [7]}
|
|
51
|
+
assert "Rutina 5 creada" in result.output
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def test_crear_without_roles_or_people_is_for_everyone():
|
|
55
|
+
fake = FakeApi({("POST", "people/routines/"): (201, BITACORA)})
|
|
56
|
+
result = run(fake, "rutinas", "crear", "Llenar la bitácora", "--si")
|
|
57
|
+
assert result.exit_code == 0, result.output
|
|
58
|
+
assert fake.sent("POST", "people/routines/") == {"text": "Llenar la bitácora", "role_ids": [], "employee_ids": []}
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def test_crear_with_a_role_nobody_has_says_which_there_are():
|
|
62
|
+
fake = FakeApi({("GET", "people/routines/roles/"): ROLES})
|
|
63
|
+
result = run(fake, "rutinas", "crear", "Contar la bodega", "--rol", "Gerente", "--si")
|
|
64
|
+
assert result.exit_code == 1
|
|
65
|
+
assert "Gerente" in result.output and "Bodeguero, Técnico" in result.output
|
|
66
|
+
assert not fake.called("POST", "people/routines/")
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def test_crear_json_gives_the_record_with_its_id():
|
|
70
|
+
fake = FakeApi({("POST", "people/routines/"): (201, BITACORA)})
|
|
71
|
+
result = run(fake, "rutinas", "crear", "Llenar la bitácora", "--si", "--json")
|
|
72
|
+
assert json.loads(result.output)["id"] == 3
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def test_cambiar_finds_the_routine_by_its_text_and_replaces_the_roles():
|
|
76
|
+
changed = {**HERRAMIENTAS, "roles": [{"id": 9, "name": "Técnico"}]}
|
|
77
|
+
fake = FakeApi({
|
|
78
|
+
("GET", "people/routines/"): [BITACORA, HERRAMIENTAS],
|
|
79
|
+
("GET", "people/routines/roles/"): ROLES,
|
|
80
|
+
("PATCH", "people/routines/4/"): changed,
|
|
81
|
+
})
|
|
82
|
+
result = run(fake, "rutinas", "cambiar", "recoger herramientas", "--rol", "Técnico", "--si")
|
|
83
|
+
assert result.exit_code == 0, result.output
|
|
84
|
+
assert fake.sent("PATCH", "people/routines/4/") == {"role_ids": [9]}
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def test_cambiar_para_todos_empties_both_lists():
|
|
88
|
+
fake = FakeApi({("GET", "people/routines/"): [HERRAMIENTAS], ("PATCH", "people/routines/4/"): HERRAMIENTAS})
|
|
89
|
+
result = run(fake, "rutinas", "cambiar", "4", "--para-todos", "--si")
|
|
90
|
+
assert result.exit_code == 0, result.output
|
|
91
|
+
assert fake.sent("PATCH", "people/routines/4/") == {"role_ids": [], "employee_ids": []}
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def test_cambiar_needs_something_to_change():
|
|
95
|
+
fake = FakeApi({})
|
|
96
|
+
result = run(fake, "rutinas", "cambiar", "4", "--si")
|
|
97
|
+
assert result.exit_code == 1
|
|
98
|
+
assert "Di qué cambiar" in result.output
|
|
99
|
+
assert fake.requests == []
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def test_quitar_takes_the_routine_off_the_plans():
|
|
103
|
+
fake = FakeApi({("GET", "people/routines/"): [BITACORA], ("DELETE", "people/routines/3/"): (204, None)})
|
|
104
|
+
result = run(fake, "rutinas", "quitar", "Llenar la bitácora", "--si")
|
|
105
|
+
assert result.exit_code == 0, result.output
|
|
106
|
+
assert fake.called("DELETE", "people/routines/3/")
|
|
107
|
+
assert "quitada" in result.output
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def test_quitar_with_an_unknown_routine_changes_nothing():
|
|
111
|
+
fake = FakeApi({("GET", "people/routines/"): [BITACORA]})
|
|
112
|
+
result = run(fake, "rutinas", "quitar", "Otra", "--si")
|
|
113
|
+
assert result.exit_code == 1
|
|
114
|
+
assert not fake.called("DELETE", "people/routines/3/")
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def test_cumplimiento_asks_for_the_month_and_shows_each_person():
|
|
118
|
+
fake = FakeApi({("GET", "people/routines/compliance/"): COMPLIANCE})
|
|
119
|
+
result = run(fake, "rutinas", "cumplimiento", "--mes", "10/2026")
|
|
120
|
+
assert result.exit_code == 0, result.output
|
|
121
|
+
assert fake.requests[0].url.params["month"] == "2026-10"
|
|
122
|
+
assert "María Torres" in result.output and "Técnico" in result.output
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def test_cumplimiento_json_is_the_whole_answer():
|
|
126
|
+
fake = FakeApi({("GET", "people/routines/compliance/"): COMPLIANCE})
|
|
127
|
+
result = run(fake, "rutinas", "cumplimiento", "--mes", "2026-10", "--json")
|
|
128
|
+
assert json.loads(result.output) == COMPLIANCE
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|