operx-cli 0.6.0__tar.gz → 0.7.0__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.6.0 → operx_cli-0.7.0}/PKG-INFO +10 -4
- {operx_cli-0.6.0 → operx_cli-0.7.0}/README.md +9 -3
- {operx_cli-0.6.0 → operx_cli-0.7.0}/operx_cli/AGENTES.md +20 -12
- {operx_cli-0.6.0 → operx_cli-0.7.0}/operx_cli/__init__.py +1 -1
- {operx_cli-0.6.0 → operx_cli-0.7.0}/operx_cli/app.py +1 -0
- {operx_cli-0.6.0 → operx_cli-0.7.0}/operx_cli/commands/inventario.py +28 -0
- {operx_cli-0.6.0 → operx_cli-0.7.0}/operx_cli/commands/ordenes.py +57 -4
- {operx_cli-0.6.0 → operx_cli-0.7.0}/operx_cli/commands/personas.py +56 -1
- {operx_cli-0.6.0 → operx_cli-0.7.0}/operx_cli/commands/planta.py +33 -1
- {operx_cli-0.6.0 → operx_cli-0.7.0}/operx_cli/record_completion.py +1 -0
- {operx_cli-0.6.0 → operx_cli-0.7.0}/operx_cli/shell.py +5 -0
- {operx_cli-0.6.0 → operx_cli-0.7.0}/operx_cli.egg-info/PKG-INFO +10 -4
- {operx_cli-0.6.0 → operx_cli-0.7.0}/operx_cli.egg-info/SOURCES.txt +1 -0
- {operx_cli-0.6.0 → operx_cli-0.7.0}/pyproject.toml +1 -1
- operx_cli-0.7.0/tests/test_daily_flows.py +186 -0
- {operx_cli-0.6.0 → operx_cli-0.7.0}/tests/test_ordenes.py +2 -0
- {operx_cli-0.6.0 → operx_cli-0.7.0}/tests/test_shell.py +1 -1
- {operx_cli-0.6.0 → operx_cli-0.7.0}/operx_cli/agent_guide.py +0 -0
- {operx_cli-0.6.0 → operx_cli-0.7.0}/operx_cli/api_fields.py +0 -0
- {operx_cli-0.6.0 → operx_cli-0.7.0}/operx_cli/browser_login.py +0 -0
- {operx_cli-0.6.0 → operx_cli-0.7.0}/operx_cli/client.py +0 -0
- {operx_cli-0.6.0 → operx_cli-0.7.0}/operx_cli/commands/__init__.py +0 -0
- {operx_cli-0.6.0 → operx_cli-0.7.0}/operx_cli/commands/account.py +0 -0
- {operx_cli-0.6.0 → operx_cli-0.7.0}/operx_cli/commands/ambiental.py +0 -0
- {operx_cli-0.6.0 → operx_cli-0.7.0}/operx_cli/commands/articulos.py +0 -0
- {operx_cli-0.6.0 → operx_cli-0.7.0}/operx_cli/commands/avisos.py +0 -0
- {operx_cli-0.6.0 → operx_cli-0.7.0}/operx_cli/commands/common.py +0 -0
- {operx_cli-0.6.0 → operx_cli-0.7.0}/operx_cli/commands/completar.py +0 -0
- {operx_cli-0.6.0 → operx_cli-0.7.0}/operx_cli/commands/empresas.py +0 -0
- {operx_cli-0.6.0 → operx_cli-0.7.0}/operx_cli/commands/lecturas.py +0 -0
- {operx_cli-0.6.0 → operx_cli-0.7.0}/operx_cli/commands/preventivos.py +0 -0
- {operx_cli-0.6.0 → operx_cli-0.7.0}/operx_cli/commands/raw.py +0 -0
- {operx_cli-0.6.0 → operx_cli-0.7.0}/operx_cli/commands/registros.py +0 -0
- {operx_cli-0.6.0 → operx_cli-0.7.0}/operx_cli/commands/reportes.py +0 -0
- {operx_cli-0.6.0 → operx_cli-0.7.0}/operx_cli/commands/search.py +0 -0
- {operx_cli-0.6.0 → operx_cli-0.7.0}/operx_cli/commands/sst.py +0 -0
- {operx_cli-0.6.0 → operx_cli-0.7.0}/operx_cli/commands/update.py +0 -0
- {operx_cli-0.6.0 → operx_cli-0.7.0}/operx_cli/lookup.py +0 -0
- {operx_cli-0.6.0 → operx_cli-0.7.0}/operx_cli/output.py +0 -0
- {operx_cli-0.6.0 → operx_cli-0.7.0}/operx_cli/self_update.py +0 -0
- {operx_cli-0.6.0 → operx_cli-0.7.0}/operx_cli/settings.py +0 -0
- {operx_cli-0.6.0 → operx_cli-0.7.0}/operx_cli/spreadsheet.py +0 -0
- {operx_cli-0.6.0 → operx_cli-0.7.0}/operx_cli/updates.py +0 -0
- {operx_cli-0.6.0 → operx_cli-0.7.0}/operx_cli/values.py +0 -0
- {operx_cli-0.6.0 → operx_cli-0.7.0}/operx_cli/welcome.py +0 -0
- {operx_cli-0.6.0 → operx_cli-0.7.0}/operx_cli.egg-info/dependency_links.txt +0 -0
- {operx_cli-0.6.0 → operx_cli-0.7.0}/operx_cli.egg-info/entry_points.txt +0 -0
- {operx_cli-0.6.0 → operx_cli-0.7.0}/operx_cli.egg-info/requires.txt +0 -0
- {operx_cli-0.6.0 → operx_cli-0.7.0}/operx_cli.egg-info/top_level.txt +0 -0
- {operx_cli-0.6.0 → operx_cli-0.7.0}/setup.cfg +0 -0
- {operx_cli-0.6.0 → operx_cli-0.7.0}/tests/test_agentes.py +0 -0
- {operx_cli-0.6.0 → operx_cli-0.7.0}/tests/test_api_campos.py +0 -0
- {operx_cli-0.6.0 → operx_cli-0.7.0}/tests/test_app.py +0 -0
- {operx_cli-0.6.0 → operx_cli-0.7.0}/tests/test_articulos.py +0 -0
- {operx_cli-0.6.0 → operx_cli-0.7.0}/tests/test_cards.py +0 -0
- {operx_cli-0.6.0 → operx_cli-0.7.0}/tests/test_client.py +0 -0
- {operx_cli-0.6.0 → operx_cli-0.7.0}/tests/test_completar.py +0 -0
- {operx_cli-0.6.0 → operx_cli-0.7.0}/tests/test_inventario_lecturas.py +0 -0
- {operx_cli-0.6.0 → operx_cli-0.7.0}/tests/test_json_writes.py +0 -0
- {operx_cli-0.6.0 → operx_cli-0.7.0}/tests/test_login.py +0 -0
- {operx_cli-0.6.0 → operx_cli-0.7.0}/tests/test_logout_and_routes.py +0 -0
- {operx_cli-0.6.0 → operx_cli-0.7.0}/tests/test_lookup.py +0 -0
- {operx_cli-0.6.0 → operx_cli-0.7.0}/tests/test_more_of_the_api.py +0 -0
- {operx_cli-0.6.0 → operx_cli-0.7.0}/tests/test_output.py +0 -0
- {operx_cli-0.6.0 → operx_cli-0.7.0}/tests/test_personas.py +0 -0
- {operx_cli-0.6.0 → operx_cli-0.7.0}/tests/test_preventivos.py +0 -0
- {operx_cli-0.6.0 → operx_cli-0.7.0}/tests/test_record_completion.py +0 -0
- {operx_cli-0.6.0 → operx_cli-0.7.0}/tests/test_registros.py +0 -0
- {operx_cli-0.6.0 → operx_cli-0.7.0}/tests/test_self_update.py +0 -0
- {operx_cli-0.6.0 → operx_cli-0.7.0}/tests/test_settings.py +0 -0
- {operx_cli-0.6.0 → operx_cli-0.7.0}/tests/test_spreadsheet.py +0 -0
- {operx_cli-0.6.0 → operx_cli-0.7.0}/tests/test_sst_ambiental.py +0 -0
- {operx_cli-0.6.0 → operx_cli-0.7.0}/tests/test_updates.py +0 -0
- {operx_cli-0.6.0 → operx_cli-0.7.0}/tests/test_values.py +0 -0
- {operx_cli-0.6.0 → operx_cli-0.7.0}/tests/test_welcome.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: operx-cli
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.7.0
|
|
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
|
|
@@ -118,6 +118,8 @@ operx ordenes asignar 1234 "Pedro Pérez"
|
|
|
118
118
|
operx ordenes comentar 1234 "Llegó el repuesto"
|
|
119
119
|
operx ordenes editar 1234 --prioridad critica --vence 05/11/2026
|
|
120
120
|
operx ordenes adjuntar 1234 foto.jpg --momento despues
|
|
121
|
+
operx ordenes repuestos 1234 # lo planeado, lo usado y su costo
|
|
122
|
+
operx ordenes repuesto 1234 --articulo FIL-010 --lugar "Bodega central" --cantidad 2
|
|
121
123
|
|
|
122
124
|
operx reportes indicadores --dias 30 # MTTR, MTBF, cumplimiento de preventivos
|
|
123
125
|
operx reportes vencidas
|
|
@@ -153,10 +155,12 @@ operx inventario traslado --articulo FIL-010 --cantidad 2 --de "Bodega central"
|
|
|
153
155
|
operx inventario ajuste --articulo FIL-010 --lugar "Bodega central" --cantidad -1 --motivo Conteo
|
|
154
156
|
operx inventario movimientos --articulo FIL-010
|
|
155
157
|
operx inventario revertir 4512 # deshace un movimiento equivocado
|
|
158
|
+
operx inventario por-comprar # lo bajo el mínimo y las solicitudes aprobadas
|
|
156
159
|
|
|
157
160
|
operx equipos parar BOM-04 --motivo Falla --minutos 90
|
|
158
161
|
operx equipos reanudar 21
|
|
159
162
|
operx equipos hallazgo BOM-04 --titulo "Fuga en el sello" --severidad alta
|
|
163
|
+
operx equipos hallazgo-a-orden 8 --prioridad alta # la orden para ese hallazgo
|
|
160
164
|
operx lecturas anular 31 --motivo "Lectura de otro equipo"
|
|
161
165
|
|
|
162
166
|
operx lecturas puntos BOM-04
|
|
@@ -165,6 +169,8 @@ operx lecturas registrar --punto 12 --valor 73,4
|
|
|
165
169
|
operx personas listar --buscar pérez
|
|
166
170
|
operx personas importar empleados.xlsx # revisa el archivo, no guarda
|
|
167
171
|
operx personas importar empleados.xlsx --confirmar # guarda si ninguna fila tiene errores
|
|
172
|
+
operx personas ausencia "Ana Ruiz" --tipo vacaciones --desde 02/11/2026 --hasta 06/11/2026
|
|
173
|
+
operx personas ausencia "Ana Ruiz" --tipo permiso --desde 09/11/2026 --horas 08:00-10:00
|
|
168
174
|
|
|
169
175
|
operx sst acciones --estado abiertas
|
|
170
176
|
operx sst nueva-accion --titulo "Señalizar la salida" --para 15/11/2026 --responsable "Ana Ruiz"
|
|
@@ -221,9 +227,9 @@ tipo, si es obligatorio y los valores permitidos. También están en Acceso
|
|
|
221
227
|
por API › Documentación:
|
|
222
228
|
|
|
223
229
|
```sh
|
|
224
|
-
operx api rutas
|
|
225
|
-
operx api campos inventory/
|
|
226
|
-
operx api POST inventory/
|
|
230
|
+
operx api rutas purchase requests
|
|
231
|
+
operx api campos inventory/purchase-requests/ --metodo POST
|
|
232
|
+
operx api POST inventory/purchase-requests/ -d '{"item": 3, "quantity": "10", "reason": "Bajo el mínimo"}'
|
|
227
233
|
operx api GET pm/schedules/ -q search=compresor
|
|
228
234
|
operx api POST contacts/ -d @contacto.json
|
|
229
235
|
operx api GET work-orders/export/ -o ordenes.csv
|
|
@@ -104,6 +104,8 @@ operx ordenes asignar 1234 "Pedro Pérez"
|
|
|
104
104
|
operx ordenes comentar 1234 "Llegó el repuesto"
|
|
105
105
|
operx ordenes editar 1234 --prioridad critica --vence 05/11/2026
|
|
106
106
|
operx ordenes adjuntar 1234 foto.jpg --momento despues
|
|
107
|
+
operx ordenes repuestos 1234 # lo planeado, lo usado y su costo
|
|
108
|
+
operx ordenes repuesto 1234 --articulo FIL-010 --lugar "Bodega central" --cantidad 2
|
|
107
109
|
|
|
108
110
|
operx reportes indicadores --dias 30 # MTTR, MTBF, cumplimiento de preventivos
|
|
109
111
|
operx reportes vencidas
|
|
@@ -139,10 +141,12 @@ operx inventario traslado --articulo FIL-010 --cantidad 2 --de "Bodega central"
|
|
|
139
141
|
operx inventario ajuste --articulo FIL-010 --lugar "Bodega central" --cantidad -1 --motivo Conteo
|
|
140
142
|
operx inventario movimientos --articulo FIL-010
|
|
141
143
|
operx inventario revertir 4512 # deshace un movimiento equivocado
|
|
144
|
+
operx inventario por-comprar # lo bajo el mínimo y las solicitudes aprobadas
|
|
142
145
|
|
|
143
146
|
operx equipos parar BOM-04 --motivo Falla --minutos 90
|
|
144
147
|
operx equipos reanudar 21
|
|
145
148
|
operx equipos hallazgo BOM-04 --titulo "Fuga en el sello" --severidad alta
|
|
149
|
+
operx equipos hallazgo-a-orden 8 --prioridad alta # la orden para ese hallazgo
|
|
146
150
|
operx lecturas anular 31 --motivo "Lectura de otro equipo"
|
|
147
151
|
|
|
148
152
|
operx lecturas puntos BOM-04
|
|
@@ -151,6 +155,8 @@ operx lecturas registrar --punto 12 --valor 73,4
|
|
|
151
155
|
operx personas listar --buscar pérez
|
|
152
156
|
operx personas importar empleados.xlsx # revisa el archivo, no guarda
|
|
153
157
|
operx personas importar empleados.xlsx --confirmar # guarda si ninguna fila tiene errores
|
|
158
|
+
operx personas ausencia "Ana Ruiz" --tipo vacaciones --desde 02/11/2026 --hasta 06/11/2026
|
|
159
|
+
operx personas ausencia "Ana Ruiz" --tipo permiso --desde 09/11/2026 --horas 08:00-10:00
|
|
154
160
|
|
|
155
161
|
operx sst acciones --estado abiertas
|
|
156
162
|
operx sst nueva-accion --titulo "Señalizar la salida" --para 15/11/2026 --responsable "Ana Ruiz"
|
|
@@ -207,9 +213,9 @@ tipo, si es obligatorio y los valores permitidos. También están en Acceso
|
|
|
207
213
|
por API › Documentación:
|
|
208
214
|
|
|
209
215
|
```sh
|
|
210
|
-
operx api rutas
|
|
211
|
-
operx api campos inventory/
|
|
212
|
-
operx api POST inventory/
|
|
216
|
+
operx api rutas purchase requests
|
|
217
|
+
operx api campos inventory/purchase-requests/ --metodo POST
|
|
218
|
+
operx api POST inventory/purchase-requests/ -d '{"item": 3, "quantity": "10", "reason": "Bajo el mínimo"}'
|
|
213
219
|
operx api GET pm/schedules/ -q search=compresor
|
|
214
220
|
operx api POST contacts/ -d @contacto.json
|
|
215
221
|
operx api GET work-orders/export/ -o ordenes.csv
|
|
@@ -60,12 +60,13 @@ llamada queda en el historial del token, marcada como hecha desde la CLI.
|
|
|
60
60
|
- Todos los comandos que escriben aceptan `--json`: la salida es solo la
|
|
61
61
|
respuesta de la API (el registro, con su `id`). Úsalo para encadenar pasos.
|
|
62
62
|
- Escriben: `ordenes crear`, `ordenes editar`, `ordenes estado`,
|
|
63
|
-
`ordenes asignar`, `ordenes comentar`, `ordenes adjuntar`,
|
|
63
|
+
`ordenes asignar`, `ordenes comentar`, `ordenes adjuntar`, `ordenes repuesto`,
|
|
64
64
|
`preventivos crear`, `preventivos nota`, `equipos crear`, `equipos editar`,
|
|
65
|
-
`equipos parar`, `equipos reanudar`, `equipos hallazgo`,
|
|
65
|
+
`equipos parar`, `equipos reanudar`, `equipos hallazgo`,
|
|
66
|
+
`equipos hallazgo-a-orden`, `clientes crear`,
|
|
66
67
|
`clientes editar`, `lugares crear`, `articulos crear`, `inventario entrada`, `inventario salida`, `inventario ajuste`,
|
|
67
68
|
`inventario traslado`, `inventario revertir`, `lecturas registrar`,
|
|
68
|
-
`lecturas anular`, `personas importar --confirmar`, `sst nueva-accion`,
|
|
69
|
+
`lecturas anular`, `personas importar --confirmar`, `personas ausencia`, `sst nueva-accion`,
|
|
69
70
|
`sst accion-hecha`, `ambiental nueva-accion`, `ambiental accion-hecha`,
|
|
70
71
|
`ambiental residuo`, `ambiental lectura`, `avisos --marcar-leidos`, y
|
|
71
72
|
`operx api` con POST, PUT, PATCH o DELETE. Todos llevan `--si`.
|
|
@@ -124,7 +125,7 @@ cinco pasos:
|
|
|
124
125
|
1. **Busca la ruta.** `operx api rutas TEXTO --json` deja las que tienen
|
|
125
126
|
cada palabra del texto en la ruta, en su `producto` (SG-SST, Inventario…)
|
|
126
127
|
o en lo que hace. Las rutas están en inglés: busca también así
|
|
127
|
-
(`operx api rutas sst inspections`, `operx api rutas
|
|
128
|
+
(`operx api rutas sst inspections`, `operx api rutas purchase requests`).
|
|
128
129
|
2. **Lee lo que lleva.** `operx api campos RUTA --json` (con `--metodo POST`
|
|
129
130
|
para uno solo) da, por método:
|
|
130
131
|
- `en_la_ruta`: los valores de la dirección, como `{pk}`.
|
|
@@ -163,9 +164,9 @@ Más opciones de `operx api`:
|
|
|
163
164
|
- `-o archivo`: guarda una respuesta que es un archivo (CSV, PDF).
|
|
164
165
|
|
|
165
166
|
```sh
|
|
166
|
-
operx api rutas
|
|
167
|
-
operx api campos inventory/
|
|
168
|
-
operx api POST inventory/
|
|
167
|
+
operx api rutas purchase requests --json
|
|
168
|
+
operx api campos inventory/purchase-requests/ --metodo POST --json
|
|
169
|
+
operx api POST inventory/purchase-requests/ -d '{"item": 3, "quantity": "10", "reason": "Bajo el mínimo"}' --si
|
|
169
170
|
operx api GET work-orders/export/ -o ordenes.csv
|
|
170
171
|
```
|
|
171
172
|
|
|
@@ -193,11 +194,18 @@ operx inventario entrada --articulo CON-0001 --lugar "Bodega central" --cantidad
|
|
|
193
194
|
# Un equipo que entró a mantenimiento
|
|
194
195
|
operx equipos editar BOM-04 --estado en-mantenimiento --si
|
|
195
196
|
|
|
196
|
-
# Un repuesto usado en una orden
|
|
197
|
-
operx
|
|
198
|
-
operx
|
|
199
|
-
|
|
200
|
-
|
|
197
|
+
# Un repuesto usado en una orden, y lo que ya lleva
|
|
198
|
+
operx ordenes repuesto 1234 --articulo FIL-010 --lugar "Bodega central" --cantidad 2 --si --json
|
|
199
|
+
operx ordenes repuestos 1234 --json
|
|
200
|
+
|
|
201
|
+
# Lo que hay que comprar, y una solicitud de compra (no tiene comando propio)
|
|
202
|
+
operx inventario por-comprar --json
|
|
203
|
+
operx api campos inventory/purchase-requests/ --metodo POST --json
|
|
204
|
+
operx api POST inventory/purchase-requests/ -d '{"item": 3, "quantity": "10", "reason": "Bajo el mínimo"}' --si
|
|
205
|
+
|
|
206
|
+
# La orden para un hallazgo, y una ausencia (queda por aprobar)
|
|
207
|
+
operx equipos hallazgo-a-orden 8 --prioridad alta --si --json
|
|
208
|
+
operx personas ausencia "Ana Ruiz" --tipo permiso --desde 09/11/2026 --horas 08:00-10:00 --si --json
|
|
201
209
|
|
|
202
210
|
# Crear y seguir con el id que responde
|
|
203
211
|
operx ordenes crear --titulo "Cambio de rodamiento" --equipo BOM-04 --si --json
|
|
@@ -147,6 +147,7 @@ registros.equipos.command("parar")(planta.parar)
|
|
|
147
147
|
registros.equipos.command("reanudar")(planta.reanudar)
|
|
148
148
|
registros.equipos.command("hallazgos")(planta.hallazgos)
|
|
149
149
|
registros.equipos.command("hallazgo")(planta.hallazgo)
|
|
150
|
+
registros.equipos.command("hallazgo-a-orden")(planta.hallazgo_a_orden)
|
|
150
151
|
app.add_typer(registros.equipos, name="equipos")
|
|
151
152
|
app.add_typer(registros.clientes, name="clientes")
|
|
152
153
|
app.add_typer(registros.lugares, name="lugares")
|
|
@@ -214,3 +214,31 @@ def salida(
|
|
|
214
214
|
confirm_write(f"Salida de {cantidad} de «{item['name']}» de {place['name']} ({reason['name']}).", si)
|
|
215
215
|
moved = answer_of(client.send("POST", "inventory/actions/salida", json=move))
|
|
216
216
|
print_result(moved, as_json, f"Salida registrada: {cantidad} de {item['name']} de {place['name']}.")
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
TO_BUY_COLUMNS = [
|
|
220
|
+
("code", "Código"), ("name", "Artículo"), ("place_name", "Lugar"), ("on_hand", "Hay"), ("min_qty", "Mínimo"),
|
|
221
|
+
("quantity", "Pedir"), ("unit", "Unidad"), ("supplier_name", "Proveedor"), ("unit_price", "Último precio"),
|
|
222
|
+
("request_number", "Solicitud"),
|
|
223
|
+
]
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
@app.command("por-comprar")
|
|
227
|
+
def por_comprar(
|
|
228
|
+
ctx: typer.Context,
|
|
229
|
+
as_json: JsonOption = False,
|
|
230
|
+
as_csv: CsvOption = False,
|
|
231
|
+
):
|
|
232
|
+
"""Lo que hay que comprar: lo que está bajo el mínimo (cuánto pedir para llegar al máximo) y las solicitudes aprobadas."""
|
|
233
|
+
output = output_format(as_json, as_csv)
|
|
234
|
+
lines = client_of(ctx).get("inventory/por-comprar")
|
|
235
|
+
if output != "json":
|
|
236
|
+
lines = [
|
|
237
|
+
{**line, "code": (line.get("item") or {}).get("code"),
|
|
238
|
+
# A request may name something not in the catalog: its description.
|
|
239
|
+
"name": (line.get("item") or {}).get("name") or line.get("description"),
|
|
240
|
+
"unit": (line.get("item") or {}).get("unit"), "supplier_name": (line.get("supplier") or {}).get("name"),
|
|
241
|
+
"request_number": (line.get("request") or {}).get("number")}
|
|
242
|
+
for line in lines
|
|
243
|
+
]
|
|
244
|
+
print_rows(TO_BUY_COLUMNS, lines, output)
|
|
@@ -7,14 +7,16 @@ from typing import Annotated, Optional
|
|
|
7
7
|
import typer
|
|
8
8
|
|
|
9
9
|
from ..lookup import find_one
|
|
10
|
-
from ..output import print_card, print_notes, print_record, print_rows
|
|
10
|
+
from ..output import cell_text, json_text, print_card, print_notes, print_record, print_rows
|
|
11
11
|
from ..values import shown_moment
|
|
12
12
|
from .common import (
|
|
13
13
|
AllOption, CliError, CsvOption, JsonOption, SearchOption, YesOption, answer_of, client_of, confirm_write, day,
|
|
14
|
-
list_rows, number, output_format, print_result, without_empty,
|
|
14
|
+
list_rows, moment, number, output_format, print_result, without_empty,
|
|
15
15
|
)
|
|
16
16
|
|
|
17
|
-
app = typer.Typer(
|
|
17
|
+
app = typer.Typer(
|
|
18
|
+
help="Órdenes de trabajo: verlas, crearlas, moverlas de estado, asignarlas, comentarlas y sus repuestos.", no_args_is_help=True,
|
|
19
|
+
)
|
|
18
20
|
|
|
19
21
|
|
|
20
22
|
class Estado(str, Enum):
|
|
@@ -75,7 +77,7 @@ def _in_spanish(order):
|
|
|
75
77
|
**order,
|
|
76
78
|
"status": STATUS_LABELS.get(order.get("status"), order.get("status")),
|
|
77
79
|
"priority": PRIORITY_LABELS.get(order.get("priority"), order.get("priority")),
|
|
78
|
-
"due_date": (order.get("due_date")
|
|
80
|
+
"due_date": shown_moment(order.get("due_date")),
|
|
79
81
|
}
|
|
80
82
|
|
|
81
83
|
|
|
@@ -289,3 +291,54 @@ def comentar(
|
|
|
289
291
|
confirm_write(f"Comentar en la orden {orden}.", si)
|
|
290
292
|
comment = answer_of(client_of(ctx).send("POST", f"work-orders/{orden}/comments", json={"body": texto}))
|
|
291
293
|
print_result(comment, as_json, f"Comentario agregado a la orden {orden}.")
|
|
294
|
+
|
|
295
|
+
|
|
296
|
+
# ── Parts (Inventario: inventory/stage_views.py WorkOrderPartsView) ──────────
|
|
297
|
+
|
|
298
|
+
PART_COLUMNS = [
|
|
299
|
+
("item_code", "Código"), ("item_name", "Repuesto"), ("location_name", "De"), ("planned", "Planeado"),
|
|
300
|
+
("used", "Usado"), ("unit", "Unidad"), ("cost", "Costo"),
|
|
301
|
+
]
|
|
302
|
+
|
|
303
|
+
|
|
304
|
+
@app.command()
|
|
305
|
+
def repuestos(
|
|
306
|
+
ctx: typer.Context,
|
|
307
|
+
orden: Annotated[int, typer.Argument(help="El número de la orden.")],
|
|
308
|
+
as_json: JsonOption = False,
|
|
309
|
+
as_csv: CsvOption = False,
|
|
310
|
+
):
|
|
311
|
+
"""Los repuestos de una orden: lo planeado, lo usado y su costo."""
|
|
312
|
+
output = output_format(as_json, as_csv)
|
|
313
|
+
answer = client_of(ctx).get(f"inventory/work-orders/{orden}/parts")
|
|
314
|
+
if output == "json":
|
|
315
|
+
typer.echo(json_text(answer))
|
|
316
|
+
return
|
|
317
|
+
print_rows(PART_COLUMNS, answer.get("parts", []), output)
|
|
318
|
+
if output == "tabla":
|
|
319
|
+
typer.echo(f"Costo de repuestos: {cell_text(answer.get('cost'))}")
|
|
320
|
+
|
|
321
|
+
|
|
322
|
+
@app.command()
|
|
323
|
+
def repuesto(
|
|
324
|
+
ctx: typer.Context,
|
|
325
|
+
orden: Annotated[int, typer.Argument(help="El número de la orden.")],
|
|
326
|
+
articulo: Annotated[str, typer.Option("--articulo", help="El código o nombre del repuesto.")],
|
|
327
|
+
lugar: Annotated[str, typer.Option("--lugar", help="De qué bodega o vehículo sale.")],
|
|
328
|
+
cantidad: Annotated[str, typer.Option("--cantidad", help="Cuánto: 2 o 2,5. Negativa devuelve al stock lo que no se usó.")],
|
|
329
|
+
planeado: Annotated[bool, typer.Option("--planeado", help="Solo lo reserva para la orden; no sale del stock.")] = False,
|
|
330
|
+
fecha: Annotated[Optional[str], typer.Option("--fecha", help="Cuándo se usó, si no fue ahora: 22/10/2026 08:00.")] = None,
|
|
331
|
+
si: YesOption = False,
|
|
332
|
+
as_json: JsonOption = False,
|
|
333
|
+
):
|
|
334
|
+
"""Registra un repuesto usado en una orden: sale del stock y suma a su costo. Con --planeado solo lo reserva."""
|
|
335
|
+
client = client_of(ctx)
|
|
336
|
+
item = find_one(client, "inventory/items", articulo, ["code", "name"], "artículo")
|
|
337
|
+
place = find_one(client, "locations", lugar, ["name"], "lugar")
|
|
338
|
+
part = {"action": "planear" if planeado else "usar", "item": item["id"], "quantity": number(cantidad), "location": place["id"]}
|
|
339
|
+
if fecha and not planeado:
|
|
340
|
+
part["occurred_at"] = moment(fecha)
|
|
341
|
+
what = "Reservar" if planeado else "Usar"
|
|
342
|
+
confirm_write(f"{what} {cantidad} de «{item['name']}» de {place['name']} en la orden {orden}.", si)
|
|
343
|
+
answer = client.send("POST", f"inventory/work-orders/{orden}/parts", json=part).json()
|
|
344
|
+
print_result(answer, as_json, f"Listo. Costo de repuestos de la orden {orden}: {cell_text(answer.get('cost'))}.")
|
|
@@ -6,6 +6,8 @@ Every run checks the whole file first and shows each row's errors; with
|
|
|
6
6
|
gets access to the app here: invitations are sent from Usuarios y acceso.
|
|
7
7
|
"""
|
|
8
8
|
|
|
9
|
+
import re
|
|
10
|
+
from enum import Enum
|
|
9
11
|
from pathlib import Path
|
|
10
12
|
from typing import Annotated, Optional
|
|
11
13
|
|
|
@@ -16,7 +18,7 @@ from ..output import json_text, note, print_rows
|
|
|
16
18
|
from ..spreadsheet import DATE_FIELDS, people_rows
|
|
17
19
|
from ..values import shown_moment
|
|
18
20
|
from .common import (
|
|
19
|
-
AllOption, CliError, CsvOption, JsonOption, SearchOption, YesOption, client_of, confirm_write, list_rows,
|
|
21
|
+
AllOption, CliError, CsvOption, JsonOption, SearchOption, YesOption, client_of, confirm_write, day, list_rows,
|
|
20
22
|
output_format, print_result,
|
|
21
23
|
)
|
|
22
24
|
|
|
@@ -137,3 +139,56 @@ def importar(
|
|
|
137
139
|
print_result(
|
|
138
140
|
saved, as_json, f"Se guardaron {saved['saved']} personas. Nadie recibió acceso a la app: invítalos en Usuarios y acceso.",
|
|
139
141
|
)
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
class TipoAusencia(str, Enum):
|
|
145
|
+
vacaciones = "vacaciones"
|
|
146
|
+
permiso = "permiso"
|
|
147
|
+
licencia = "licencia"
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
# people/models.py Absence.KIND_CHOICES. An incapacidad goes with its
|
|
151
|
+
# certificate, in the app.
|
|
152
|
+
ABSENCE_KEYS = {"vacaciones": "vacation", "permiso": "permit", "licencia": "leave"}
|
|
153
|
+
HOURS = re.compile(r"^(\d{1,2}):(\d{2})\s*-\s*(\d{1,2}):(\d{2})$")
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def _hours(text):
|
|
157
|
+
"""'8:00-12:00' → ('08:00', '12:00')."""
|
|
158
|
+
found = HOURS.match(text.strip())
|
|
159
|
+
if not found:
|
|
160
|
+
raise CliError(f"«{text}» no son horas: escríbelas como 08:00-12:00.")
|
|
161
|
+
start_hour, start_minute, end_hour, end_minute = found.groups()
|
|
162
|
+
return f"{int(start_hour):02d}:{start_minute}", f"{int(end_hour):02d}:{end_minute}"
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
@app.command()
|
|
166
|
+
def ausencia(
|
|
167
|
+
ctx: typer.Context,
|
|
168
|
+
persona: Annotated[str, typer.Argument(help="El nombre o código de la persona.")],
|
|
169
|
+
tipo: Annotated[TipoAusencia, typer.Option("--tipo")],
|
|
170
|
+
desde: Annotated[str, typer.Option("--desde", help="El primer día: 02/11/2026.")],
|
|
171
|
+
hasta: Annotated[Optional[str], typer.Option("--hasta", help="El último día; sin él, el mismo.")] = None,
|
|
172
|
+
horas: Annotated[Optional[str], typer.Option("--horas", help="Un permiso de unas horas: 08:00-12:00.")] = None,
|
|
173
|
+
motivo: Annotated[Optional[str], typer.Option("--motivo")] = None,
|
|
174
|
+
si: YesOption = False,
|
|
175
|
+
as_json: JsonOption = False,
|
|
176
|
+
):
|
|
177
|
+
"""Registra una ausencia: vacaciones, un permiso o una licencia. Queda por aprobar. Una incapacidad, en la app."""
|
|
178
|
+
client = client_of(ctx)
|
|
179
|
+
person = find_one(client, "employees", persona, ["user_name", "employee_code"], "persona")
|
|
180
|
+
start = day(desde)
|
|
181
|
+
absence = {"employee_id": person["id"], "kind": ABSENCE_KEYS[tipo.value], "start_date": start, "end_date": day(hasta) or start}
|
|
182
|
+
if horas:
|
|
183
|
+
absence["hours_from"], absence["hours_to"] = _hours(horas)
|
|
184
|
+
if motivo:
|
|
185
|
+
absence["reason"] = motivo
|
|
186
|
+
when = shown_moment(start) if absence["end_date"] == start else f"del {shown_moment(start)} al {shown_moment(absence['end_date'])}"
|
|
187
|
+
if horas:
|
|
188
|
+
when += f", de {absence['hours_from']} a {absence['hours_to']}"
|
|
189
|
+
confirm_write(f"{tipo.value.capitalize()} de {person['user_name']}: {when}.", si)
|
|
190
|
+
created = client.send("POST", "people/absences", json=absence).json()
|
|
191
|
+
print_result(created, as_json, (
|
|
192
|
+
f"Ausencia registrada: {created.get('kind_label', tipo.value.capitalize())} de {person['user_name']}, {when}. "
|
|
193
|
+
f"{created.get('status_label', 'Por aprobar')}."
|
|
194
|
+
))
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"""Equipos' day at the plant, under `operx equipos` (app.py): a machine's
|
|
2
2
|
stops (equipment/stops.py) and the findings someone marked on it
|
|
3
|
-
(equipment/findings.py)
|
|
3
|
+
(equipment/findings.py), and the work order for one. Watching or closing a
|
|
4
|
+
finding stays in the app."""
|
|
4
5
|
|
|
5
6
|
from datetime import date, timedelta
|
|
6
7
|
from enum import Enum
|
|
@@ -12,6 +13,7 @@ from ..lookup import find_in, find_one
|
|
|
12
13
|
from ..output import print_rows
|
|
13
14
|
from ..values import now_iso, shown_moment
|
|
14
15
|
from .common import (
|
|
16
|
+
CliError,
|
|
15
17
|
CsvOption,
|
|
16
18
|
JsonOption,
|
|
17
19
|
YesOption,
|
|
@@ -24,6 +26,7 @@ from .common import (
|
|
|
24
26
|
print_result,
|
|
25
27
|
without_empty,
|
|
26
28
|
)
|
|
29
|
+
from .ordenes import PRIORITY_KEYS, Prioridad
|
|
27
30
|
|
|
28
31
|
MachineArgument = Annotated[str, typer.Argument(help="El código, nombre o id del equipo.")]
|
|
29
32
|
STOPS_DAYS = 7
|
|
@@ -170,3 +173,32 @@ def hallazgo(
|
|
|
170
173
|
confirm_write(f"Marcar «{titulo}» en «{machine['name']}».", si)
|
|
171
174
|
created = client.send("POST", "equipment/findings", json=finding).json()
|
|
172
175
|
print_result(created, as_json, f"Hallazgo {created['id']} marcado en {machine['name']}.")
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
def hallazgo_a_orden(
|
|
179
|
+
ctx: typer.Context,
|
|
180
|
+
hallazgo: Annotated[int, typer.Argument(help="El número del hallazgo (operx equipos hallazgos).")],
|
|
181
|
+
titulo: Annotated[Optional[str], typer.Option("--titulo", help="El título de la orden; sin él, el del hallazgo.")] = None,
|
|
182
|
+
descripcion: Annotated[Optional[str], typer.Option("--descripcion")] = None,
|
|
183
|
+
prioridad: Annotated[Optional[Prioridad], typer.Option("--prioridad", help="Sin ella, media.")] = None,
|
|
184
|
+
si: YesOption = False,
|
|
185
|
+
as_json: JsonOption = False,
|
|
186
|
+
):
|
|
187
|
+
"""Crea la orden de trabajo para un hallazgo: queda ligada a él y a su equipo."""
|
|
188
|
+
client = client_of(ctx)
|
|
189
|
+
if not titulo:
|
|
190
|
+
# The findings list has it unless it's closed. One that already has its
|
|
191
|
+
# order is refused by the API (equipment/findings.py), never doubled.
|
|
192
|
+
found = [finding for finding in client.get("equipment/findings") if finding.get("id") == hallazgo]
|
|
193
|
+
if not found:
|
|
194
|
+
raise CliError(f"No encontré el hallazgo {hallazgo} entre los abiertos. Si es otro, dale un título: --titulo.")
|
|
195
|
+
titulo = found[0]["title"]
|
|
196
|
+
order = without_empty({
|
|
197
|
+
"title": titulo,
|
|
198
|
+
"description": descripcion,
|
|
199
|
+
"priority": PRIORITY_KEYS[prioridad.value] if prioridad else None,
|
|
200
|
+
})
|
|
201
|
+
confirm_write(f"Crear la orden «{titulo}» para el hallazgo {hallazgo}.", si)
|
|
202
|
+
finding = client.send("POST", f"equipment/findings/{hallazgo}/work-order", json=order).json()
|
|
203
|
+
created = finding.get("work_order") or {}
|
|
204
|
+
print_result(finding, as_json, f"Orden {created.get('id')} creada desde el hallazgo {hallazgo}: {created.get('title', titulo)}.")
|
|
@@ -44,11 +44,16 @@ COMMAND_PRODUCTS = {
|
|
|
44
44
|
"equipos reanudar": ("equipment",),
|
|
45
45
|
"equipos hallazgos": ("equipment",),
|
|
46
46
|
"equipos hallazgo": ("equipment",),
|
|
47
|
+
"equipos hallazgo-a-orden": ("equipment",),
|
|
48
|
+
# Parts on an order are Inventario's.
|
|
49
|
+
"ordenes repuestos": ("inventory",),
|
|
50
|
+
"ordenes repuesto": ("inventory",),
|
|
47
51
|
"clientes": ("maintenance", "inventory", "sst", "environment"),
|
|
48
52
|
"inventario": ("inventory",),
|
|
49
53
|
"articulos": ("inventory",),
|
|
50
54
|
"lecturas": ("equipment",),
|
|
51
55
|
"personas importar": ("people",),
|
|
56
|
+
"personas ausencia": ("people",),
|
|
52
57
|
"sst": ("sst",),
|
|
53
58
|
"ambiental": ("environment",),
|
|
54
59
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: operx-cli
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.7.0
|
|
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
|
|
@@ -118,6 +118,8 @@ operx ordenes asignar 1234 "Pedro Pérez"
|
|
|
118
118
|
operx ordenes comentar 1234 "Llegó el repuesto"
|
|
119
119
|
operx ordenes editar 1234 --prioridad critica --vence 05/11/2026
|
|
120
120
|
operx ordenes adjuntar 1234 foto.jpg --momento despues
|
|
121
|
+
operx ordenes repuestos 1234 # lo planeado, lo usado y su costo
|
|
122
|
+
operx ordenes repuesto 1234 --articulo FIL-010 --lugar "Bodega central" --cantidad 2
|
|
121
123
|
|
|
122
124
|
operx reportes indicadores --dias 30 # MTTR, MTBF, cumplimiento de preventivos
|
|
123
125
|
operx reportes vencidas
|
|
@@ -153,10 +155,12 @@ operx inventario traslado --articulo FIL-010 --cantidad 2 --de "Bodega central"
|
|
|
153
155
|
operx inventario ajuste --articulo FIL-010 --lugar "Bodega central" --cantidad -1 --motivo Conteo
|
|
154
156
|
operx inventario movimientos --articulo FIL-010
|
|
155
157
|
operx inventario revertir 4512 # deshace un movimiento equivocado
|
|
158
|
+
operx inventario por-comprar # lo bajo el mínimo y las solicitudes aprobadas
|
|
156
159
|
|
|
157
160
|
operx equipos parar BOM-04 --motivo Falla --minutos 90
|
|
158
161
|
operx equipos reanudar 21
|
|
159
162
|
operx equipos hallazgo BOM-04 --titulo "Fuga en el sello" --severidad alta
|
|
163
|
+
operx equipos hallazgo-a-orden 8 --prioridad alta # la orden para ese hallazgo
|
|
160
164
|
operx lecturas anular 31 --motivo "Lectura de otro equipo"
|
|
161
165
|
|
|
162
166
|
operx lecturas puntos BOM-04
|
|
@@ -165,6 +169,8 @@ operx lecturas registrar --punto 12 --valor 73,4
|
|
|
165
169
|
operx personas listar --buscar pérez
|
|
166
170
|
operx personas importar empleados.xlsx # revisa el archivo, no guarda
|
|
167
171
|
operx personas importar empleados.xlsx --confirmar # guarda si ninguna fila tiene errores
|
|
172
|
+
operx personas ausencia "Ana Ruiz" --tipo vacaciones --desde 02/11/2026 --hasta 06/11/2026
|
|
173
|
+
operx personas ausencia "Ana Ruiz" --tipo permiso --desde 09/11/2026 --horas 08:00-10:00
|
|
168
174
|
|
|
169
175
|
operx sst acciones --estado abiertas
|
|
170
176
|
operx sst nueva-accion --titulo "Señalizar la salida" --para 15/11/2026 --responsable "Ana Ruiz"
|
|
@@ -221,9 +227,9 @@ tipo, si es obligatorio y los valores permitidos. También están en Acceso
|
|
|
221
227
|
por API › Documentación:
|
|
222
228
|
|
|
223
229
|
```sh
|
|
224
|
-
operx api rutas
|
|
225
|
-
operx api campos inventory/
|
|
226
|
-
operx api POST inventory/
|
|
230
|
+
operx api rutas purchase requests
|
|
231
|
+
operx api campos inventory/purchase-requests/ --metodo POST
|
|
232
|
+
operx api POST inventory/purchase-requests/ -d '{"item": 3, "quantity": "10", "reason": "Bajo el mínimo"}'
|
|
227
233
|
operx api GET pm/schedules/ -q search=compresor
|
|
228
234
|
operx api POST contacts/ -d @contacto.json
|
|
229
235
|
operx api GET work-orders/export/ -o ordenes.csv
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "operx-cli"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.7.0"
|
|
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,186 @@
|
|
|
1
|
+
"""The daily flows that had only `operx api`: parts used on an order, what to
|
|
2
|
+
buy, an order from a finding, an absence. Fixtures: real answers from a
|
|
3
|
+
local backend with a personal token."""
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
|
|
7
|
+
import pytest
|
|
8
|
+
|
|
9
|
+
from operx_cli.shell import offered_to
|
|
10
|
+
|
|
11
|
+
from .fake_api import FakeApi, page, run
|
|
12
|
+
|
|
13
|
+
# GET /v1/inventory/work-orders/6/parts/ (inventory/stage_views.py WorkOrderPartsView)
|
|
14
|
+
PARTS = {
|
|
15
|
+
"work_order_id": 6,
|
|
16
|
+
"parts": [{
|
|
17
|
+
"id": 1, "item_id": 2, "item_code": "CON-0001", "item_name": "Filtro e2e", "unit": "und", "item_photo": None,
|
|
18
|
+
"location_id": 5, "location_name": "Bodega e2e", "planned": "1.000", "used": "2.500", "cost": "62.50",
|
|
19
|
+
"billable": True, "billed_invoice_id": None, "note": "", "sale_price": None, "average_cost": "25.0000",
|
|
20
|
+
}],
|
|
21
|
+
"cost": "62.50",
|
|
22
|
+
}
|
|
23
|
+
FILTER = {"id": 2, "code": "CON-0001", "name": "Filtro e2e"}
|
|
24
|
+
STORE = {"id": 5, "name": "Bodega e2e"}
|
|
25
|
+
# GET /v1/inventory/por-comprar/ (inventory/purchasing_views.py ToBuyView)
|
|
26
|
+
TO_BUY = [{
|
|
27
|
+
"key": "low-3", "request": None, "item": {"id": 2, "code": "CON-0001", "name": "Filtro e2e", "unit": "und", "photo": None},
|
|
28
|
+
"description": "", "quantity": "12.500", "unit_price": "25.00", "supplier": None, "place_id": 5,
|
|
29
|
+
"place_name": "Bodega e2e", "on_hand": "7.500", "min_qty": "10.000", "max_qty": "20.000", "target": "20.000",
|
|
30
|
+
"asked": "0", "coming": "0", "orders": [], "is_critical": False,
|
|
31
|
+
}]
|
|
32
|
+
REQUESTED = {
|
|
33
|
+
"key": "request-4", "request": {"id": 4, "number": "SC-0004", "requested_by": "Ana Ruiz", "reason": "", "work_order_id": None},
|
|
34
|
+
"item": None, "description": "Guantes de nitrilo talla M", "quantity": "20.000", "unit_price": None,
|
|
35
|
+
"supplier": {"id": 9, "name": "Filtros Ltda", "email": ""}, "place_id": None, "place_name": "", "on_hand": None,
|
|
36
|
+
"min_qty": None, "max_qty": None, "target": None, "asked": None, "coming": None, "orders": [], "is_critical": False,
|
|
37
|
+
}
|
|
38
|
+
# GET /v1/equipment/findings/ and POST …/findings/1/work-order/ (equipment/findings.py)
|
|
39
|
+
MACHINE = {"id": 1, "code": "BOM-04", "name": "Bomba 4", "place_id": 2, "place_path": "Planta Norte", "parent": None}
|
|
40
|
+
FINDING = {
|
|
41
|
+
"id": 1, "title": "Fuga en el sello", "note": "", "machine": MACHINE, "status": "new", "severity": "high",
|
|
42
|
+
"photos": [], "found_at": "2026-10-23T13:39:04.100703-05:00", "found_by_name": "Admin Smoke",
|
|
43
|
+
}
|
|
44
|
+
WITH_ORDER = {**FINDING, "status": "work_order", "work_order": {"id": 7, "number": "OT #7", "title": "Cambiar el sello"}}
|
|
45
|
+
# GET /v1/employees/ and POST /v1/people/absences/ (people/absence_views.py)
|
|
46
|
+
PEOPLE = page({"id": 1, "user_id": 1, "user_name": "Admin Smoke", "avatar": None, "employee_code": "", "is_active": True})
|
|
47
|
+
ABSENCE = {
|
|
48
|
+
"id": 1, "employee": {"id": 1, "name": "Admin Smoke", "avatar": None}, "kind": "permit", "kind_label": "Permiso",
|
|
49
|
+
"status": "pending", "status_label": "Por aprobar", "start_date": "2026-11-03", "end_date": "2026-11-03",
|
|
50
|
+
"hours_from": "08:00", "hours_to": "12:00", "reason": "Cita médica", "business_days": 0, "has_certificate": False,
|
|
51
|
+
"requested_by": "Admin Smoke", "approver": "Un administrador",
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
@pytest.fixture(autouse=True)
|
|
56
|
+
def signed_in(monkeypatch):
|
|
57
|
+
monkeypatch.setenv("OPERX_TOKEN", "opx_test")
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
# ── Parts on an order ──────────────────────────────────────────────────────
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def test_repuestos_lists_what_an_order_planned_and_used_and_its_cost():
|
|
64
|
+
result = run(FakeApi({("GET", "inventory/work-orders/6/parts/"): PARTS}), "ordenes", "repuestos", "6")
|
|
65
|
+
assert result.exit_code == 0, result.output
|
|
66
|
+
for text in ("CON-0001", "Filtro e2e", "Bodega e2e", "2,5", "Costo de repuestos: 62,5"):
|
|
67
|
+
assert text in result.output
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def test_repuestos_json_is_the_apis_answer():
|
|
71
|
+
result = run(FakeApi({("GET", "inventory/work-orders/6/parts/"): PARTS}), "ordenes", "repuestos", "6", "--json")
|
|
72
|
+
assert json.loads(result.stdout) == PARTS
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def parts_api():
|
|
76
|
+
return FakeApi({
|
|
77
|
+
("GET", "inventory/items/"): page(FILTER),
|
|
78
|
+
("GET", "locations/"): page(STORE),
|
|
79
|
+
("POST", "inventory/work-orders/6/parts/"): PARTS,
|
|
80
|
+
})
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def test_repuesto_records_a_part_used_from_a_place():
|
|
84
|
+
fake = parts_api()
|
|
85
|
+
result = run(fake, "ordenes", "repuesto", "6", "--articulo", "CON-0001", "--lugar", "Bodega e2e", "--cantidad", "2,5", "--si")
|
|
86
|
+
assert result.exit_code == 0, result.output
|
|
87
|
+
assert fake.sent("POST", "inventory/work-orders/6/parts/") == {"action": "usar", "item": 2, "quantity": "2.5", "location": 5}
|
|
88
|
+
assert "Costo de repuestos de la orden 6: 62,5" in result.output
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def test_repuesto_planeado_only_reserves_it():
|
|
92
|
+
fake = parts_api()
|
|
93
|
+
run(fake, "ordenes", "repuesto", "6", "--articulo", "CON-0001", "--lugar", "Bodega e2e", "--cantidad", "1", "--planeado", "--si")
|
|
94
|
+
assert fake.sent("POST", "inventory/work-orders/6/parts/")["action"] == "planear"
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def test_repuesto_used_earlier_keeps_its_moment():
|
|
98
|
+
fake = parts_api()
|
|
99
|
+
run(fake, "ordenes", "repuesto", "6", "--articulo", "CON-0001", "--lugar", "Bodega e2e", "--cantidad", "1",
|
|
100
|
+
"--fecha", "22/10/2026 08:00", "--si")
|
|
101
|
+
assert fake.sent("POST", "inventory/work-orders/6/parts/")["occurred_at"].startswith("2026-10-22T08:00")
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def test_parts_need_inventario_in_the_prompt():
|
|
105
|
+
assert not offered_to("ordenes repuestos", ["maintenance"])
|
|
106
|
+
assert offered_to("ordenes repuesto", ["maintenance", "inventory"])
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
# ── What to buy ────────────────────────────────────────────────────────────
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def test_por_comprar_lists_what_is_short_and_what_was_asked_for():
|
|
113
|
+
fake = FakeApi({("GET", "inventory/por-comprar/"): TO_BUY + [REQUESTED]})
|
|
114
|
+
result = run(fake, "inventario", "por-comprar")
|
|
115
|
+
assert result.exit_code == 0, result.output
|
|
116
|
+
for text in ("CON-0001", "Bodega e2e", "12,5", "7,5", "Guantes de nitrilo talla M", "Filtros Ltda", "SC-0004"):
|
|
117
|
+
assert text in result.output
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def test_por_comprar_json_is_the_apis_answer():
|
|
121
|
+
result = run(FakeApi({("GET", "inventory/por-comprar/"): TO_BUY}), "inventario", "por-comprar", "--json")
|
|
122
|
+
assert json.loads(result.stdout) == TO_BUY
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
# ── An order from a finding ────────────────────────────────────────────────
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def test_hallazgo_a_orden_titles_it_like_the_finding_unless_told():
|
|
129
|
+
fake = FakeApi({("GET", "equipment/findings/"): [FINDING], ("POST", "equipment/findings/1/work-order/"): (201, WITH_ORDER)})
|
|
130
|
+
result = run(fake, "equipos", "hallazgo-a-orden", "1", "--si")
|
|
131
|
+
assert result.exit_code == 0, result.output
|
|
132
|
+
assert fake.sent("POST", "equipment/findings/1/work-order/") == {"title": "Fuga en el sello"}
|
|
133
|
+
assert "Orden 7 creada desde el hallazgo 1: Cambiar el sello." in result.output
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def test_hallazgo_a_orden_with_its_own_title_and_priority():
|
|
137
|
+
fake = FakeApi({("POST", "equipment/findings/1/work-order/"): (201, WITH_ORDER)})
|
|
138
|
+
run(fake, "equipos", "hallazgo-a-orden", "1", "--titulo", "Cambiar el sello", "--prioridad", "alta", "--si")
|
|
139
|
+
assert fake.sent("POST", "equipment/findings/1/work-order/") == {"title": "Cambiar el sello", "priority": "high"}
|
|
140
|
+
assert not fake.called("GET", "equipment/findings/")
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def test_hallazgo_a_orden_of_a_finding_not_open_needs_a_title():
|
|
144
|
+
result = run(FakeApi({("GET", "equipment/findings/"): []}), "equipos", "hallazgo-a-orden", "1", "--si")
|
|
145
|
+
assert result.exit_code == 1
|
|
146
|
+
assert "--titulo" in result.output
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
# ── An absence ─────────────────────────────────────────────────────────────
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def absence_api():
|
|
153
|
+
return FakeApi({("GET", "employees/"): PEOPLE, ("POST", "people/absences/"): (201, ABSENCE)})
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def test_ausencia_by_hours_is_a_permit_of_one_day():
|
|
157
|
+
fake = absence_api()
|
|
158
|
+
result = run(
|
|
159
|
+
fake, "personas", "ausencia", "Admin Smoke", "--tipo", "permiso", "--desde", "03/11/2026", "--horas", "8:00-12:00",
|
|
160
|
+
"--motivo", "Cita médica", "--si",
|
|
161
|
+
)
|
|
162
|
+
assert result.exit_code == 0, result.output
|
|
163
|
+
assert fake.sent("POST", "people/absences/") == {
|
|
164
|
+
"employee_id": 1, "kind": "permit", "start_date": "2026-11-03", "end_date": "2026-11-03",
|
|
165
|
+
"hours_from": "08:00", "hours_to": "12:00", "reason": "Cita médica",
|
|
166
|
+
}
|
|
167
|
+
assert "Ausencia registrada: Permiso de Admin Smoke, 03/11/2026, de 08:00 a 12:00. Por aprobar." in result.output
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def test_ausencia_of_several_days():
|
|
171
|
+
fake = absence_api()
|
|
172
|
+
run(fake, "personas", "ausencia", "Admin Smoke", "--tipo", "vacaciones", "--desde", "02/11/2026", "--hasta", "06/11/2026", "--si")
|
|
173
|
+
sent = fake.sent("POST", "people/absences/")
|
|
174
|
+
assert sent["kind"] == "vacation" and sent["start_date"] == "2026-11-02" and sent["end_date"] == "2026-11-06"
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
def test_ausencia_hours_must_read_as_a_range():
|
|
178
|
+
result = run(absence_api(), "personas", "ausencia", "Admin Smoke", "--tipo", "permiso", "--desde", "03/11/2026",
|
|
179
|
+
"--horas", "por la mañana", "--si")
|
|
180
|
+
assert result.exit_code == 1
|
|
181
|
+
assert "08:00-12:00" in result.output
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
def test_ausencia_json_is_the_apis_answer():
|
|
185
|
+
result = run(absence_api(), "personas", "ausencia", "Admin Smoke", "--tipo", "permiso", "--desde", "03/11/2026", "--si", "--json")
|
|
186
|
+
assert json.loads(result.stdout) == ABSENCE
|
|
@@ -20,6 +20,8 @@ def test_listar_shows_status_and_priority_in_spanish():
|
|
|
20
20
|
result = run(FakeApi({("GET", "work-orders/"): page(ORDER)}), "ordenes", "listar")
|
|
21
21
|
assert result.exit_code == 0, result.output
|
|
22
22
|
assert "En progreso" in result.output and "Alta" in result.output and "Cambio de rodamiento" in result.output
|
|
23
|
+
# The due date the way people read it, like every other list.
|
|
24
|
+
assert "30/10/2026" in result.output and "2026-10-30" not in result.output
|
|
23
25
|
|
|
24
26
|
|
|
25
27
|
def test_listar_json_keeps_the_apis_values():
|
|
@@ -272,7 +272,7 @@ def test_equipos_shows_its_stops_and_findings_only_with_equipos():
|
|
|
272
272
|
|
|
273
273
|
def test_tab_hides_a_subcommand_of_a_product_the_company_lacks():
|
|
274
274
|
assert names(suggestions(ROOT, "personas ", products=["maintenance"])) == ["listar"]
|
|
275
|
-
assert set(names(suggestions(ROOT, "personas ", products=["people"]))) == {"listar", "importar"}
|
|
275
|
+
assert set(names(suggestions(ROOT, "personas ", products=["people"]))) == {"listar", "importar", "ausencia"}
|
|
276
276
|
|
|
277
277
|
|
|
278
278
|
def test_ayuda_lists_every_word_the_prompt_understands(capsys):
|
|
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
|