operx-cli 0.6.0__tar.gz → 0.7.1__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.1}/PKG-INFO +10 -4
- {operx_cli-0.6.0 → operx_cli-0.7.1}/README.md +9 -3
- {operx_cli-0.6.0 → operx_cli-0.7.1}/operx_cli/AGENTES.md +36 -26
- {operx_cli-0.6.0 → operx_cli-0.7.1}/operx_cli/__init__.py +1 -1
- {operx_cli-0.6.0 → operx_cli-0.7.1}/operx_cli/app.py +1 -0
- {operx_cli-0.6.0 → operx_cli-0.7.1}/operx_cli/commands/inventario.py +28 -0
- {operx_cli-0.6.0 → operx_cli-0.7.1}/operx_cli/commands/ordenes.py +57 -4
- {operx_cli-0.6.0 → operx_cli-0.7.1}/operx_cli/commands/personas.py +56 -1
- {operx_cli-0.6.0 → operx_cli-0.7.1}/operx_cli/commands/planta.py +33 -1
- {operx_cli-0.6.0 → operx_cli-0.7.1}/operx_cli/record_completion.py +1 -0
- {operx_cli-0.6.0 → operx_cli-0.7.1}/operx_cli/shell.py +5 -0
- {operx_cli-0.6.0 → operx_cli-0.7.1}/operx_cli.egg-info/PKG-INFO +10 -4
- {operx_cli-0.6.0 → operx_cli-0.7.1}/operx_cli.egg-info/SOURCES.txt +1 -0
- {operx_cli-0.6.0 → operx_cli-0.7.1}/pyproject.toml +1 -1
- operx_cli-0.7.1/tests/test_daily_flows.py +186 -0
- {operx_cli-0.6.0 → operx_cli-0.7.1}/tests/test_ordenes.py +2 -0
- {operx_cli-0.6.0 → operx_cli-0.7.1}/tests/test_shell.py +1 -1
- {operx_cli-0.6.0 → operx_cli-0.7.1}/operx_cli/agent_guide.py +0 -0
- {operx_cli-0.6.0 → operx_cli-0.7.1}/operx_cli/api_fields.py +0 -0
- {operx_cli-0.6.0 → operx_cli-0.7.1}/operx_cli/browser_login.py +0 -0
- {operx_cli-0.6.0 → operx_cli-0.7.1}/operx_cli/client.py +0 -0
- {operx_cli-0.6.0 → operx_cli-0.7.1}/operx_cli/commands/__init__.py +0 -0
- {operx_cli-0.6.0 → operx_cli-0.7.1}/operx_cli/commands/account.py +0 -0
- {operx_cli-0.6.0 → operx_cli-0.7.1}/operx_cli/commands/ambiental.py +0 -0
- {operx_cli-0.6.0 → operx_cli-0.7.1}/operx_cli/commands/articulos.py +0 -0
- {operx_cli-0.6.0 → operx_cli-0.7.1}/operx_cli/commands/avisos.py +0 -0
- {operx_cli-0.6.0 → operx_cli-0.7.1}/operx_cli/commands/common.py +0 -0
- {operx_cli-0.6.0 → operx_cli-0.7.1}/operx_cli/commands/completar.py +0 -0
- {operx_cli-0.6.0 → operx_cli-0.7.1}/operx_cli/commands/empresas.py +0 -0
- {operx_cli-0.6.0 → operx_cli-0.7.1}/operx_cli/commands/lecturas.py +0 -0
- {operx_cli-0.6.0 → operx_cli-0.7.1}/operx_cli/commands/preventivos.py +0 -0
- {operx_cli-0.6.0 → operx_cli-0.7.1}/operx_cli/commands/raw.py +0 -0
- {operx_cli-0.6.0 → operx_cli-0.7.1}/operx_cli/commands/registros.py +0 -0
- {operx_cli-0.6.0 → operx_cli-0.7.1}/operx_cli/commands/reportes.py +0 -0
- {operx_cli-0.6.0 → operx_cli-0.7.1}/operx_cli/commands/search.py +0 -0
- {operx_cli-0.6.0 → operx_cli-0.7.1}/operx_cli/commands/sst.py +0 -0
- {operx_cli-0.6.0 → operx_cli-0.7.1}/operx_cli/commands/update.py +0 -0
- {operx_cli-0.6.0 → operx_cli-0.7.1}/operx_cli/lookup.py +0 -0
- {operx_cli-0.6.0 → operx_cli-0.7.1}/operx_cli/output.py +0 -0
- {operx_cli-0.6.0 → operx_cli-0.7.1}/operx_cli/self_update.py +0 -0
- {operx_cli-0.6.0 → operx_cli-0.7.1}/operx_cli/settings.py +0 -0
- {operx_cli-0.6.0 → operx_cli-0.7.1}/operx_cli/spreadsheet.py +0 -0
- {operx_cli-0.6.0 → operx_cli-0.7.1}/operx_cli/updates.py +0 -0
- {operx_cli-0.6.0 → operx_cli-0.7.1}/operx_cli/values.py +0 -0
- {operx_cli-0.6.0 → operx_cli-0.7.1}/operx_cli/welcome.py +0 -0
- {operx_cli-0.6.0 → operx_cli-0.7.1}/operx_cli.egg-info/dependency_links.txt +0 -0
- {operx_cli-0.6.0 → operx_cli-0.7.1}/operx_cli.egg-info/entry_points.txt +0 -0
- {operx_cli-0.6.0 → operx_cli-0.7.1}/operx_cli.egg-info/requires.txt +0 -0
- {operx_cli-0.6.0 → operx_cli-0.7.1}/operx_cli.egg-info/top_level.txt +0 -0
- {operx_cli-0.6.0 → operx_cli-0.7.1}/setup.cfg +0 -0
- {operx_cli-0.6.0 → operx_cli-0.7.1}/tests/test_agentes.py +0 -0
- {operx_cli-0.6.0 → operx_cli-0.7.1}/tests/test_api_campos.py +0 -0
- {operx_cli-0.6.0 → operx_cli-0.7.1}/tests/test_app.py +0 -0
- {operx_cli-0.6.0 → operx_cli-0.7.1}/tests/test_articulos.py +0 -0
- {operx_cli-0.6.0 → operx_cli-0.7.1}/tests/test_cards.py +0 -0
- {operx_cli-0.6.0 → operx_cli-0.7.1}/tests/test_client.py +0 -0
- {operx_cli-0.6.0 → operx_cli-0.7.1}/tests/test_completar.py +0 -0
- {operx_cli-0.6.0 → operx_cli-0.7.1}/tests/test_inventario_lecturas.py +0 -0
- {operx_cli-0.6.0 → operx_cli-0.7.1}/tests/test_json_writes.py +0 -0
- {operx_cli-0.6.0 → operx_cli-0.7.1}/tests/test_login.py +0 -0
- {operx_cli-0.6.0 → operx_cli-0.7.1}/tests/test_logout_and_routes.py +0 -0
- {operx_cli-0.6.0 → operx_cli-0.7.1}/tests/test_lookup.py +0 -0
- {operx_cli-0.6.0 → operx_cli-0.7.1}/tests/test_more_of_the_api.py +0 -0
- {operx_cli-0.6.0 → operx_cli-0.7.1}/tests/test_output.py +0 -0
- {operx_cli-0.6.0 → operx_cli-0.7.1}/tests/test_personas.py +0 -0
- {operx_cli-0.6.0 → operx_cli-0.7.1}/tests/test_preventivos.py +0 -0
- {operx_cli-0.6.0 → operx_cli-0.7.1}/tests/test_record_completion.py +0 -0
- {operx_cli-0.6.0 → operx_cli-0.7.1}/tests/test_registros.py +0 -0
- {operx_cli-0.6.0 → operx_cli-0.7.1}/tests/test_self_update.py +0 -0
- {operx_cli-0.6.0 → operx_cli-0.7.1}/tests/test_settings.py +0 -0
- {operx_cli-0.6.0 → operx_cli-0.7.1}/tests/test_spreadsheet.py +0 -0
- {operx_cli-0.6.0 → operx_cli-0.7.1}/tests/test_sst_ambiental.py +0 -0
- {operx_cli-0.6.0 → operx_cli-0.7.1}/tests/test_updates.py +0 -0
- {operx_cli-0.6.0 → operx_cli-0.7.1}/tests/test_values.py +0 -0
- {operx_cli-0.6.0 → operx_cli-0.7.1}/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.1
|
|
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
|
|
@@ -59,16 +59,19 @@ llamada queda en el historial del token, marcada como hecha desde la CLI.
|
|
|
59
59
|
|
|
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
|
-
- Escriben
|
|
63
|
-
`ordenes
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
`
|
|
68
|
-
|
|
69
|
-
`
|
|
70
|
-
|
|
71
|
-
|
|
62
|
+
- Escriben, y todos llevan `--si`:
|
|
63
|
+
- Trabajos: `ordenes crear`, `editar`, `estado`, `asignar`, `comentar`,
|
|
64
|
+
`adjuntar` y `repuesto`; `preventivos crear` y `nota`.
|
|
65
|
+
- Registros: `equipos crear` y `editar`, `clientes crear` y `editar`,
|
|
66
|
+
`lugares crear`.
|
|
67
|
+
- Inventario: `articulos crear`; `inventario entrada`, `salida`, `ajuste`,
|
|
68
|
+
`traslado` y `revertir`.
|
|
69
|
+
- Equipos: `equipos parar`, `reanudar`, `hallazgo` y `hallazgo-a-orden`;
|
|
70
|
+
`lecturas registrar` y `anular`.
|
|
71
|
+
- Personas: `personas importar --confirmar` y `personas ausencia`.
|
|
72
|
+
- SG-SST y Ambiental: `sst nueva-accion` y `accion-hecha`;
|
|
73
|
+
`ambiental nueva-accion`, `accion-hecha`, `residuo` y `lectura`.
|
|
74
|
+
- `avisos --marcar-leidos`, y `operx api` con POST, PUT, PATCH o DELETE.
|
|
72
75
|
- Un error se corrige con su contrario, nunca borrando: `inventario revertir`
|
|
73
76
|
deshace un movimiento, `lecturas anular` una lectura.
|
|
74
77
|
- SG-SST y Ambiental van por empresa: sin `--empresa`, la de la cuenta. Una
|
|
@@ -124,7 +127,7 @@ cinco pasos:
|
|
|
124
127
|
1. **Busca la ruta.** `operx api rutas TEXTO --json` deja las que tienen
|
|
125
128
|
cada palabra del texto en la ruta, en su `producto` (SG-SST, Inventario…)
|
|
126
129
|
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
|
|
130
|
+
(`operx api rutas sst inspections`, `operx api rutas purchase requests`).
|
|
128
131
|
2. **Lee lo que lleva.** `operx api campos RUTA --json` (con `--metodo POST`
|
|
129
132
|
para uno solo) da, por método:
|
|
130
133
|
- `en_la_ruta`: los valores de la dirección, como `{pk}`.
|
|
@@ -163,9 +166,9 @@ Más opciones de `operx api`:
|
|
|
163
166
|
- `-o archivo`: guarda una respuesta que es un archivo (CSV, PDF).
|
|
164
167
|
|
|
165
168
|
```sh
|
|
166
|
-
operx api rutas
|
|
167
|
-
operx api campos inventory/
|
|
168
|
-
operx api POST inventory/
|
|
169
|
+
operx api rutas purchase requests --json
|
|
170
|
+
operx api campos inventory/purchase-requests/ --metodo POST --json
|
|
171
|
+
operx api POST inventory/purchase-requests/ -d '{"item": 3, "quantity": "10", "reason": "Bajo el mínimo"}' --si
|
|
169
172
|
operx api GET work-orders/export/ -o ordenes.csv
|
|
170
173
|
```
|
|
171
174
|
|
|
@@ -176,28 +179,35 @@ operx api GET work-orders/export/ -o ordenes.csv
|
|
|
176
179
|
operx ordenes listar --vencidas --asignada-a yo --todas --json
|
|
177
180
|
|
|
178
181
|
# Pasar una orden a completada, después de su sí
|
|
179
|
-
operx ordenes estado 1234 completada --si
|
|
182
|
+
operx ordenes estado 1234 completada --si --json
|
|
180
183
|
|
|
181
184
|
# Registrar una lectura: primero el número del punto
|
|
182
185
|
operx lecturas puntos BOM-04 --json
|
|
183
|
-
operx lecturas registrar --punto 12 --valor 73,4 --si
|
|
186
|
+
operx lecturas registrar --punto 12 --valor 73,4 --si --json
|
|
184
187
|
|
|
185
188
|
# Sacar stock: primero el código y el lugar exactos
|
|
186
189
|
operx inventario existencias --buscar filtro --json
|
|
187
|
-
operx inventario salida --articulo FIL-010 --lugar "Bodega central" --cantidad 2 --motivo Consumo --si
|
|
190
|
+
operx inventario salida --articulo FIL-010 --lugar "Bodega central" --cantidad 2 --motivo Consumo --si --json
|
|
188
191
|
|
|
189
192
|
# Un artículo nuevo y lo que llegó de él (crear da el código)
|
|
190
|
-
operx articulos crear --nombre "Filtro de aceite" --unidad und --tipo consumible --si
|
|
191
|
-
operx inventario entrada --articulo CON-0001 --lugar "Bodega central" --cantidad 10 --costo-unitario 25.000 --si
|
|
193
|
+
operx articulos crear --nombre "Filtro de aceite" --unidad und --tipo consumible --si --json
|
|
194
|
+
operx inventario entrada --articulo CON-0001 --lugar "Bodega central" --cantidad 10 --costo-unitario 25.000 --si --json
|
|
192
195
|
|
|
193
196
|
# Un equipo que entró a mantenimiento
|
|
194
|
-
operx equipos editar BOM-04 --estado en-mantenimiento --si
|
|
197
|
+
operx equipos editar BOM-04 --estado en-mantenimiento --si --json
|
|
195
198
|
|
|
196
|
-
# Un repuesto usado en una orden
|
|
197
|
-
operx
|
|
198
|
-
operx
|
|
199
|
-
|
|
200
|
-
|
|
199
|
+
# Un repuesto usado en una orden, y lo que ya lleva
|
|
200
|
+
operx ordenes repuesto 1234 --articulo FIL-010 --lugar "Bodega central" --cantidad 2 --si --json
|
|
201
|
+
operx ordenes repuestos 1234 --json
|
|
202
|
+
|
|
203
|
+
# Lo que hay que comprar, y una solicitud de compra (no tiene comando propio)
|
|
204
|
+
operx inventario por-comprar --json
|
|
205
|
+
operx api campos inventory/purchase-requests/ --metodo POST --json
|
|
206
|
+
operx api POST inventory/purchase-requests/ -d '{"item": 3, "quantity": "10", "reason": "Bajo el mínimo"}' --si
|
|
207
|
+
|
|
208
|
+
# La orden para un hallazgo, y una ausencia (queda por aprobar)
|
|
209
|
+
operx equipos hallazgo-a-orden 8 --prioridad alta --si --json
|
|
210
|
+
operx personas ausencia "Ana Ruiz" --tipo permiso --desde 09/11/2026 --horas 08:00-10:00 --si --json
|
|
201
211
|
|
|
202
212
|
# Crear y seguir con el id que responde
|
|
203
213
|
operx ordenes crear --titulo "Cambio de rodamiento" --equipo BOM-04 --si --json
|
|
@@ -221,5 +231,5 @@ operx sst acciones --estado abiertas --json
|
|
|
221
231
|
|
|
222
232
|
# Lo que vence este mes en lo ambiental, y registrar residuos
|
|
223
233
|
operx ambiental obligaciones --json
|
|
224
|
-
operx ambiental residuo --corriente "Aceite usado" --kg 12,5 --si
|
|
234
|
+
operx ambiental residuo --corriente "Aceite usado" --kg 12,5 --si --json
|
|
225
235
|
```
|
|
@@ -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.1
|
|
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.1"
|
|
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
|