operx-cli 0.5.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.5.0 → operx_cli-0.7.0}/PKG-INFO +19 -7
- {operx_cli-0.5.0 → operx_cli-0.7.0}/README.md +18 -6
- {operx_cli-0.5.0 → operx_cli-0.7.0}/operx_cli/AGENTES.md +71 -16
- {operx_cli-0.5.0 → operx_cli-0.7.0}/operx_cli/__init__.py +1 -1
- operx_cli-0.7.0/operx_cli/api_fields.py +232 -0
- {operx_cli-0.5.0 → operx_cli-0.7.0}/operx_cli/app.py +1 -0
- {operx_cli-0.5.0 → operx_cli-0.7.0}/operx_cli/commands/ambiental.py +12 -6
- {operx_cli-0.5.0 → operx_cli-0.7.0}/operx_cli/commands/articulos.py +5 -2
- {operx_cli-0.5.0 → operx_cli-0.7.0}/operx_cli/commands/avisos.py +4 -2
- {operx_cli-0.5.0 → operx_cli-0.7.0}/operx_cli/commands/common.py +22 -2
- {operx_cli-0.5.0 → operx_cli-0.7.0}/operx_cli/commands/empresas.py +6 -6
- {operx_cli-0.5.0 → operx_cli-0.7.0}/operx_cli/commands/inventario.py +47 -12
- {operx_cli-0.5.0 → operx_cli-0.7.0}/operx_cli/commands/lecturas.py +9 -5
- {operx_cli-0.5.0 → operx_cli-0.7.0}/operx_cli/commands/ordenes.py +76 -15
- {operx_cli-0.5.0 → operx_cli-0.7.0}/operx_cli/commands/personas.py +69 -5
- {operx_cli-0.5.0 → operx_cli-0.7.0}/operx_cli/commands/planta.py +47 -5
- {operx_cli-0.5.0 → operx_cli-0.7.0}/operx_cli/commands/preventivos.py +7 -3
- operx_cli-0.7.0/operx_cli/commands/raw.py +288 -0
- {operx_cli-0.5.0 → operx_cli-0.7.0}/operx_cli/commands/registros.py +14 -9
- {operx_cli-0.5.0 → operx_cli-0.7.0}/operx_cli/commands/sst.py +4 -2
- {operx_cli-0.5.0 → operx_cli-0.7.0}/operx_cli/record_completion.py +1 -0
- {operx_cli-0.5.0 → operx_cli-0.7.0}/operx_cli/shell.py +5 -0
- {operx_cli-0.5.0 → operx_cli-0.7.0}/operx_cli.egg-info/PKG-INFO +19 -7
- {operx_cli-0.5.0 → operx_cli-0.7.0}/operx_cli.egg-info/SOURCES.txt +4 -0
- {operx_cli-0.5.0 → operx_cli-0.7.0}/pyproject.toml +1 -1
- operx_cli-0.7.0/tests/test_api_campos.py +166 -0
- operx_cli-0.7.0/tests/test_daily_flows.py +186 -0
- operx_cli-0.7.0/tests/test_json_writes.py +184 -0
- {operx_cli-0.5.0 → operx_cli-0.7.0}/tests/test_logout_and_routes.py +25 -2
- {operx_cli-0.5.0 → operx_cli-0.7.0}/tests/test_ordenes.py +2 -0
- {operx_cli-0.5.0 → operx_cli-0.7.0}/tests/test_shell.py +1 -1
- operx_cli-0.5.0/operx_cli/commands/raw.py +0 -151
- {operx_cli-0.5.0 → operx_cli-0.7.0}/operx_cli/agent_guide.py +0 -0
- {operx_cli-0.5.0 → operx_cli-0.7.0}/operx_cli/browser_login.py +0 -0
- {operx_cli-0.5.0 → operx_cli-0.7.0}/operx_cli/client.py +0 -0
- {operx_cli-0.5.0 → operx_cli-0.7.0}/operx_cli/commands/__init__.py +0 -0
- {operx_cli-0.5.0 → operx_cli-0.7.0}/operx_cli/commands/account.py +0 -0
- {operx_cli-0.5.0 → operx_cli-0.7.0}/operx_cli/commands/completar.py +0 -0
- {operx_cli-0.5.0 → operx_cli-0.7.0}/operx_cli/commands/reportes.py +0 -0
- {operx_cli-0.5.0 → operx_cli-0.7.0}/operx_cli/commands/search.py +0 -0
- {operx_cli-0.5.0 → operx_cli-0.7.0}/operx_cli/commands/update.py +0 -0
- {operx_cli-0.5.0 → operx_cli-0.7.0}/operx_cli/lookup.py +0 -0
- {operx_cli-0.5.0 → operx_cli-0.7.0}/operx_cli/output.py +0 -0
- {operx_cli-0.5.0 → operx_cli-0.7.0}/operx_cli/self_update.py +0 -0
- {operx_cli-0.5.0 → operx_cli-0.7.0}/operx_cli/settings.py +0 -0
- {operx_cli-0.5.0 → operx_cli-0.7.0}/operx_cli/spreadsheet.py +0 -0
- {operx_cli-0.5.0 → operx_cli-0.7.0}/operx_cli/updates.py +0 -0
- {operx_cli-0.5.0 → operx_cli-0.7.0}/operx_cli/values.py +0 -0
- {operx_cli-0.5.0 → operx_cli-0.7.0}/operx_cli/welcome.py +0 -0
- {operx_cli-0.5.0 → operx_cli-0.7.0}/operx_cli.egg-info/dependency_links.txt +0 -0
- {operx_cli-0.5.0 → operx_cli-0.7.0}/operx_cli.egg-info/entry_points.txt +0 -0
- {operx_cli-0.5.0 → operx_cli-0.7.0}/operx_cli.egg-info/requires.txt +0 -0
- {operx_cli-0.5.0 → operx_cli-0.7.0}/operx_cli.egg-info/top_level.txt +0 -0
- {operx_cli-0.5.0 → operx_cli-0.7.0}/setup.cfg +0 -0
- {operx_cli-0.5.0 → operx_cli-0.7.0}/tests/test_agentes.py +0 -0
- {operx_cli-0.5.0 → operx_cli-0.7.0}/tests/test_app.py +0 -0
- {operx_cli-0.5.0 → operx_cli-0.7.0}/tests/test_articulos.py +0 -0
- {operx_cli-0.5.0 → operx_cli-0.7.0}/tests/test_cards.py +0 -0
- {operx_cli-0.5.0 → operx_cli-0.7.0}/tests/test_client.py +0 -0
- {operx_cli-0.5.0 → operx_cli-0.7.0}/tests/test_completar.py +0 -0
- {operx_cli-0.5.0 → operx_cli-0.7.0}/tests/test_inventario_lecturas.py +0 -0
- {operx_cli-0.5.0 → operx_cli-0.7.0}/tests/test_login.py +0 -0
- {operx_cli-0.5.0 → operx_cli-0.7.0}/tests/test_lookup.py +0 -0
- {operx_cli-0.5.0 → operx_cli-0.7.0}/tests/test_more_of_the_api.py +0 -0
- {operx_cli-0.5.0 → operx_cli-0.7.0}/tests/test_output.py +0 -0
- {operx_cli-0.5.0 → operx_cli-0.7.0}/tests/test_personas.py +0 -0
- {operx_cli-0.5.0 → operx_cli-0.7.0}/tests/test_preventivos.py +0 -0
- {operx_cli-0.5.0 → operx_cli-0.7.0}/tests/test_record_completion.py +0 -0
- {operx_cli-0.5.0 → operx_cli-0.7.0}/tests/test_registros.py +0 -0
- {operx_cli-0.5.0 → operx_cli-0.7.0}/tests/test_self_update.py +0 -0
- {operx_cli-0.5.0 → operx_cli-0.7.0}/tests/test_settings.py +0 -0
- {operx_cli-0.5.0 → operx_cli-0.7.0}/tests/test_spreadsheet.py +0 -0
- {operx_cli-0.5.0 → operx_cli-0.7.0}/tests/test_sst_ambiental.py +0 -0
- {operx_cli-0.5.0 → operx_cli-0.7.0}/tests/test_updates.py +0 -0
- {operx_cli-0.5.0 → operx_cli-0.7.0}/tests/test_values.py +0 -0
- {operx_cli-0.5.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"
|
|
@@ -196,7 +202,9 @@ llevas el SG-SST o lo ambiental de tus clientes, `operx sst empresas` y
|
|
|
196
202
|
### Antes de escribir
|
|
197
203
|
|
|
198
204
|
Todo lo que crea o cambia algo pregunta antes. En un script o una tarea
|
|
199
|
-
programada no hay a quién preguntar: agrega `--si`.
|
|
205
|
+
programada no hay a quién preguntar: agrega `--si`. Con `--json`, lo que
|
|
206
|
+
escribe responde el registro creado o cambiado (con su `id`) en vez de una
|
|
207
|
+
frase. Cada escritura queda en
|
|
200
208
|
el historial del token (Acceso por API › el token › Actividad) marcada como
|
|
201
209
|
hecha desde la CLI.
|
|
202
210
|
|
|
@@ -211,13 +219,17 @@ Usuarios y acceso.
|
|
|
211
219
|
|
|
212
220
|
### Cualquier otra llamada
|
|
213
221
|
|
|
214
|
-
Lo que no tiene comando propio se hace con `operx api
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
222
|
+
Lo que no tiene comando propio se hace con `operx api`: todo lo que tu
|
|
223
|
+
token puede hacer, más de 400 llamadas. `operx api rutas` las lista
|
|
224
|
+
(`operx api rutas sst inspections` deja las que tienen esas palabras), y
|
|
225
|
+
`operx api campos RUTA` dice qué lleva una: sus filtros, cada campo con su
|
|
226
|
+
tipo, si es obligatorio y los valores permitidos. También están en Acceso
|
|
227
|
+
por API › Documentación:
|
|
218
228
|
|
|
219
229
|
```sh
|
|
220
|
-
operx api rutas
|
|
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"}'
|
|
221
233
|
operx api GET pm/schedules/ -q search=compresor
|
|
222
234
|
operx api POST contacts/ -d @contacto.json
|
|
223
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"
|
|
@@ -182,7 +188,9 @@ llevas el SG-SST o lo ambiental de tus clientes, `operx sst empresas` y
|
|
|
182
188
|
### Antes de escribir
|
|
183
189
|
|
|
184
190
|
Todo lo que crea o cambia algo pregunta antes. En un script o una tarea
|
|
185
|
-
programada no hay a quién preguntar: agrega `--si`.
|
|
191
|
+
programada no hay a quién preguntar: agrega `--si`. Con `--json`, lo que
|
|
192
|
+
escribe responde el registro creado o cambiado (con su `id`) en vez de una
|
|
193
|
+
frase. Cada escritura queda en
|
|
186
194
|
el historial del token (Acceso por API › el token › Actividad) marcada como
|
|
187
195
|
hecha desde la CLI.
|
|
188
196
|
|
|
@@ -197,13 +205,17 @@ Usuarios y acceso.
|
|
|
197
205
|
|
|
198
206
|
### Cualquier otra llamada
|
|
199
207
|
|
|
200
|
-
Lo que no tiene comando propio se hace con `operx api
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
208
|
+
Lo que no tiene comando propio se hace con `operx api`: todo lo que tu
|
|
209
|
+
token puede hacer, más de 400 llamadas. `operx api rutas` las lista
|
|
210
|
+
(`operx api rutas sst inspections` deja las que tienen esas palabras), y
|
|
211
|
+
`operx api campos RUTA` dice qué lleva una: sus filtros, cada campo con su
|
|
212
|
+
tipo, si es obligatorio y los valores permitidos. También están en Acceso
|
|
213
|
+
por API › Documentación:
|
|
204
214
|
|
|
205
215
|
```sh
|
|
206
|
-
operx api rutas
|
|
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"}'
|
|
207
219
|
operx api GET pm/schedules/ -q search=compresor
|
|
208
220
|
operx api POST contacts/ -d @contacto.json
|
|
209
221
|
operx api GET work-orders/export/ -o ordenes.csv
|
|
@@ -9,8 +9,9 @@ llamada queda en el historial del token, marcada como hecha desde la CLI.
|
|
|
9
9
|
|
|
10
10
|
1. Escribe siempre el comando completo (`operx ordenes listar …`). No abras el
|
|
11
11
|
modo interactivo: `operx` solo, fuera de una terminal, solo muestra la ayuda.
|
|
12
|
-
2.
|
|
13
|
-
|
|
12
|
+
2. Usa `--json` siempre: al leer es lo que responde la API, completo; al
|
|
13
|
+
escribir es el registro creado o cambiado, con su `id`. Las tablas y las
|
|
14
|
+
frases son para personas.
|
|
14
15
|
3. Antes de crear o cambiar algo, dile a la persona qué vas a hacer y espera
|
|
15
16
|
su sí. Con su sí, agrega `--si`: sin él, toda escritura falla fuera de una
|
|
16
17
|
terminal.
|
|
@@ -20,7 +21,9 @@ llamada queda en el historial del token, marcada como hecha desde la CLI.
|
|
|
20
21
|
consulta primero si quedó hecha.
|
|
21
22
|
6. Si un nombre coincide con varios registros, `operx` lo dice con sus ids:
|
|
22
23
|
usa el id, no adivines.
|
|
23
|
-
7. Usa un comando propio cuando exista
|
|
24
|
+
7. Usa un comando propio cuando exista: acepta nombres y fechas como los
|
|
25
|
+
escribe la persona. Para todo lo demás está `operx api` (abajo): con él
|
|
26
|
+
puedes hacer todo lo que el token permite.
|
|
24
27
|
8. `operx update` y `operx completar` cambian el computador de la persona, no
|
|
25
28
|
los datos de su empresa: son para ella, no los ejecutes.
|
|
26
29
|
|
|
@@ -54,13 +57,16 @@ llamada queda en el historial del token, marcada como hecha desde la CLI.
|
|
|
54
57
|
|
|
55
58
|
## Escribir
|
|
56
59
|
|
|
60
|
+
- Todos los comandos que escriben aceptan `--json`: la salida es solo la
|
|
61
|
+
respuesta de la API (el registro, con su `id`). Úsalo para encadenar pasos.
|
|
57
62
|
- Escriben: `ordenes crear`, `ordenes editar`, `ordenes estado`,
|
|
58
|
-
`ordenes asignar`, `ordenes comentar`, `ordenes adjuntar`,
|
|
63
|
+
`ordenes asignar`, `ordenes comentar`, `ordenes adjuntar`, `ordenes repuesto`,
|
|
59
64
|
`preventivos crear`, `preventivos nota`, `equipos crear`, `equipos editar`,
|
|
60
|
-
`equipos parar`, `equipos reanudar`, `equipos hallazgo`,
|
|
65
|
+
`equipos parar`, `equipos reanudar`, `equipos hallazgo`,
|
|
66
|
+
`equipos hallazgo-a-orden`, `clientes crear`,
|
|
61
67
|
`clientes editar`, `lugares crear`, `articulos crear`, `inventario entrada`, `inventario salida`, `inventario ajuste`,
|
|
62
68
|
`inventario traslado`, `inventario revertir`, `lecturas registrar`,
|
|
63
|
-
`lecturas anular`, `personas importar --confirmar`, `sst nueva-accion`,
|
|
69
|
+
`lecturas anular`, `personas importar --confirmar`, `personas ausencia`, `sst nueva-accion`,
|
|
64
70
|
`sst accion-hecha`, `ambiental nueva-accion`, `ambiental accion-hecha`,
|
|
65
71
|
`ambiental residuo`, `ambiental lectura`, `avisos --marcar-leidos`, y
|
|
66
72
|
`operx api` con POST, PUT, PATCH o DELETE. Todos llevan `--si`.
|
|
@@ -110,13 +116,46 @@ Lo que responde la API:
|
|
|
110
116
|
- **429**: demasiadas llamadas (120 por minuto por token, 600 por empresa).
|
|
111
117
|
`operx` espera y reintenta solo, hasta 3 veces.
|
|
112
118
|
|
|
113
|
-
##
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
119
|
+
## Todo lo demás: `operx api`
|
|
120
|
+
|
|
121
|
+
El token puede hacer más de 400 llamadas; los comandos propios cubren las
|
|
122
|
+
del día a día. Todas las demás se hacen con `operx api MÉTODO RUTA`, en
|
|
123
|
+
cinco pasos:
|
|
124
|
+
|
|
125
|
+
1. **Busca la ruta.** `operx api rutas TEXTO --json` deja las que tienen
|
|
126
|
+
cada palabra del texto en la ruta, en su `producto` (SG-SST, Inventario…)
|
|
127
|
+
o en lo que hace. Las rutas están en inglés: busca también así
|
|
128
|
+
(`operx api rutas sst inspections`, `operx api rutas purchase requests`).
|
|
129
|
+
2. **Lee lo que lleva.** `operx api campos RUTA --json` (con `--metodo POST`
|
|
130
|
+
para uno solo) da, por método:
|
|
131
|
+
- `en_la_ruta`: los valores de la dirección, como `{pk}`.
|
|
132
|
+
- `filtros`: lo que va en `-q nombre=valor`.
|
|
133
|
+
- `datos`: cada campo del cuerpo, con su `tipo`, su `formato`, si es
|
|
134
|
+
`obligatorio`, y en `valores` los permitidos con su etiqueta en español
|
|
135
|
+
(para entender lo que dice la persona). `lines[].item` es un campo de
|
|
136
|
+
cada elemento de la lista `lines`.
|
|
137
|
+
- `envio`: `json` (todo en `-d`) o `archivos` (el archivo con
|
|
138
|
+
`-a campo=ruta`, lo demás en `-d`).
|
|
139
|
+
- `responde`: los campos de la respuesta.
|
|
140
|
+
3. **Busca los ids.** La API toma ids, no nombres: búscalos con un comando
|
|
141
|
+
(`operx equipos listar --buscar bomba --json`) o con
|
|
142
|
+
`operx api GET RUTA -q search=TEXTO`.
|
|
143
|
+
4. **Escribe**, después del sí de la persona: `-d '{"campo": valor}'` y `--si`.
|
|
144
|
+
5. **Lee la respuesta**: es el registro creado o cambiado.
|
|
145
|
+
|
|
146
|
+
En `-d` los datos van como los toma la API, no como en los comandos propios:
|
|
147
|
+
|
|
148
|
+
- Decimales como texto con punto: `"2.5"`, nunca `2,5`.
|
|
149
|
+
- Fechas `"2026-10-30"`; fecha y hora con su zona:
|
|
150
|
+
`"2026-10-30T08:00:00-05:00"`.
|
|
151
|
+
- Ids, no nombres: `"item": 3`, no `"item": "FIL-010"`.
|
|
152
|
+
- Los valores de una lista, en inglés, como salen en `valores`:
|
|
153
|
+
`"priority": "high"`.
|
|
154
|
+
|
|
155
|
+
Si la API responde 400, el mensaje dice qué campo falló y por qué; corrige
|
|
156
|
+
eso, no lo demás.
|
|
157
|
+
|
|
158
|
+
Más opciones de `operx api`:
|
|
120
159
|
|
|
121
160
|
- `-q nombre=valor`: un filtro. Se repite.
|
|
122
161
|
- `-d '{"campo": "valor"}'` o `-d @archivo.json`: el cuerpo de la llamada.
|
|
@@ -125,9 +164,9 @@ sin barra inicial.
|
|
|
125
164
|
- `-o archivo`: guarda una respuesta que es un archivo (CSV, PDF).
|
|
126
165
|
|
|
127
166
|
```sh
|
|
128
|
-
operx api rutas
|
|
129
|
-
operx api
|
|
130
|
-
operx api POST
|
|
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
|
|
131
170
|
operx api GET work-orders/export/ -o ordenes.csv
|
|
132
171
|
```
|
|
133
172
|
|
|
@@ -155,6 +194,22 @@ operx inventario entrada --articulo CON-0001 --lugar "Bodega central" --cantidad
|
|
|
155
194
|
# Un equipo que entró a mantenimiento
|
|
156
195
|
operx equipos editar BOM-04 --estado en-mantenimiento --si
|
|
157
196
|
|
|
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
|
|
209
|
+
|
|
210
|
+
# Crear y seguir con el id que responde
|
|
211
|
+
operx ordenes crear --titulo "Cambio de rodamiento" --equipo BOM-04 --si --json
|
|
212
|
+
|
|
158
213
|
# Los equipos en un CSV para Excel
|
|
159
214
|
operx equipos listar --todas --csv > equipos.csv
|
|
160
215
|
|
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
"""What a route takes, read from the OpenAPI schema the API serves a token
|
|
2
|
+
(auth/api-tokens/schema/, drf-spectacular): the values in its address, its
|
|
3
|
+
filters, the fields of its body and what it answers. `operx api campos`
|
|
4
|
+
prints it, so an AI agent can call any route with `operx api` without
|
|
5
|
+
guessing; no network here, so the MCP server can use it as it is.
|
|
6
|
+
|
|
7
|
+
The schema's shapes this follows: a field's type behind a $ref; an allOf
|
|
8
|
+
of one $ref (a nested record); a oneOf of an enum with BlankEnum and
|
|
9
|
+
NullEnum (a choice that may be "" or null); each enum's labels in its
|
|
10
|
+
description, one "* `value` - Label" a line.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
import re
|
|
14
|
+
|
|
15
|
+
METHODS = ("get", "post", "put", "patch", "delete")
|
|
16
|
+
# A field inside a list inside a list… is shown this many levels deep.
|
|
17
|
+
MAX_DEPTH = 3
|
|
18
|
+
LABEL_LINE = re.compile(r"^\* `([^`]*)` - (.+)$", re.MULTILINE)
|
|
19
|
+
DECIMALS = re.compile(r"\\\.\\d\{0,(\d+)\}")
|
|
20
|
+
# Django's integer field bounds: every integer has them, they say nothing.
|
|
21
|
+
INT32 = (-2147483648, 2147483647)
|
|
22
|
+
PAGE_FIELDS = {"count", "next", "previous", "results"}
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def find_route(schema, route):
|
|
26
|
+
"""The schema's path for a route as a person or an agent types it: with
|
|
27
|
+
real ids (work-orders/1234/) or the placeholders `operx api rutas` prints
|
|
28
|
+
({pk}). A fixed word beats a placeholder, so items/siguiente-codigo/
|
|
29
|
+
isn't items/{pk}/. LookupError when no route fits."""
|
|
30
|
+
wanted = _segments(route)
|
|
31
|
+
best = None
|
|
32
|
+
for path in schema.get("paths", {}):
|
|
33
|
+
segments = _segments(path)
|
|
34
|
+
if len(segments) != len(wanted):
|
|
35
|
+
continue
|
|
36
|
+
fixed = 0
|
|
37
|
+
for template, given in zip(segments, wanted):
|
|
38
|
+
if template.startswith("{"):
|
|
39
|
+
continue
|
|
40
|
+
if template != given:
|
|
41
|
+
break
|
|
42
|
+
fixed += 1
|
|
43
|
+
else:
|
|
44
|
+
if best is None or fixed > best[0]:
|
|
45
|
+
best = (fixed, path)
|
|
46
|
+
if best is None:
|
|
47
|
+
raise LookupError(route)
|
|
48
|
+
return best[1]
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def _segments(route):
|
|
52
|
+
route = route.strip().split("?")[0].strip("/")
|
|
53
|
+
route = route.removeprefix("api/v1/")
|
|
54
|
+
return route.split("/")
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def describe(schema, path, method=None):
|
|
58
|
+
"""Each operation of the route (or only `method`): a dict an agent reads
|
|
59
|
+
as JSON and `operx api campos` prints as tables."""
|
|
60
|
+
components = schema.get("components", {}).get("schemas", {})
|
|
61
|
+
operations = schema["paths"][path]
|
|
62
|
+
described = []
|
|
63
|
+
for name in METHODS:
|
|
64
|
+
if name not in operations or (method and method.lower() != name):
|
|
65
|
+
continue
|
|
66
|
+
operation = operations[name]
|
|
67
|
+
parameters = operation.get("parameters", [])
|
|
68
|
+
body_schema, sending = _body(operation)
|
|
69
|
+
described.append({
|
|
70
|
+
"metodo": name.upper(),
|
|
71
|
+
"ruta": path.removeprefix("/api/v1/"),
|
|
72
|
+
"descripcion": (operation.get("description") or "").strip(),
|
|
73
|
+
"en_la_ruta": [_parameter(components, parameter) for parameter in parameters if parameter.get("in") == "path"],
|
|
74
|
+
"filtros": [_parameter(components, parameter) for parameter in parameters if parameter.get("in") == "query"],
|
|
75
|
+
"datos": _fields(components, body_schema) if body_schema else [],
|
|
76
|
+
"envio": sending if body_schema else None,
|
|
77
|
+
"responde": _answer(components, operation),
|
|
78
|
+
})
|
|
79
|
+
return described
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def methods_of(schema, path):
|
|
83
|
+
return [name.upper() for name in METHODS if name in schema["paths"][path]]
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def _body(operation):
|
|
87
|
+
"""The body's schema and how it goes: JSON (-d), or a form with a file
|
|
88
|
+
(-a for the file, -d for the rest)."""
|
|
89
|
+
content = operation.get("requestBody", {}).get("content", {})
|
|
90
|
+
if "application/json" in content:
|
|
91
|
+
return content["application/json"].get("schema"), "json"
|
|
92
|
+
for kind in ("multipart/form-data", "application/x-www-form-urlencoded"):
|
|
93
|
+
if kind in content:
|
|
94
|
+
return content[kind].get("schema"), "archivos"
|
|
95
|
+
return None, None
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def _resolve(components, node):
|
|
99
|
+
"""A field's schema with its $ref, its one-ref allOf and its
|
|
100
|
+
enum-or-blank-or-null oneOf followed. What the wrapper says (nullable,
|
|
101
|
+
readOnly, its own description) stays; an enum's description becomes
|
|
102
|
+
its labels."""
|
|
103
|
+
node = dict(node or {})
|
|
104
|
+
if "$ref" in node:
|
|
105
|
+
name = node.pop("$ref").split("/")[-1]
|
|
106
|
+
target = _labelled(components.get(name, {}))
|
|
107
|
+
node = {**target, **node, "_name": name}
|
|
108
|
+
if len(node.get("allOf", [])) == 1:
|
|
109
|
+
inner = _resolve(components, node.pop("allOf")[0])
|
|
110
|
+
node = {**inner, **node}
|
|
111
|
+
if "oneOf" in node:
|
|
112
|
+
choices = [_resolve(components, choice) for choice in node.pop("oneOf")]
|
|
113
|
+
kept = []
|
|
114
|
+
for choice in choices:
|
|
115
|
+
if choice.get("enum") == [""] or choice.get("maxLength") == 0:
|
|
116
|
+
node["_blank"] = True
|
|
117
|
+
elif choice.get("enum") == [None]:
|
|
118
|
+
node["nullable"] = True
|
|
119
|
+
else:
|
|
120
|
+
kept.append(choice)
|
|
121
|
+
if kept:
|
|
122
|
+
node = {**kept[0], **node}
|
|
123
|
+
if node.get("type") == "array" and "items" in node:
|
|
124
|
+
# One level only: a record's own fields are followed by _fields.
|
|
125
|
+
node["items"] = _resolve(components, node["items"]) if "$ref" in node["items"] else node["items"]
|
|
126
|
+
return node
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def _labelled(node):
|
|
130
|
+
"""An enum's "* `low` - Baja" description as its labels."""
|
|
131
|
+
if "enum" not in node:
|
|
132
|
+
return node
|
|
133
|
+
node = dict(node)
|
|
134
|
+
labels = dict(LABEL_LINE.findall(node.pop("description", "") or ""))
|
|
135
|
+
node["_labels"] = labels
|
|
136
|
+
return node
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def _fields(components, body_schema, prefix="", depth=0, seen=frozenset()):
|
|
140
|
+
record = _resolve(components, body_schema)
|
|
141
|
+
required = set(record.get("required", []))
|
|
142
|
+
rows = []
|
|
143
|
+
for name, value in record.get("properties", {}).items():
|
|
144
|
+
field = _resolve(components, value)
|
|
145
|
+
if field.get("readOnly"):
|
|
146
|
+
continue
|
|
147
|
+
path = prefix + name
|
|
148
|
+
rows.append(_row(path, field, name in required))
|
|
149
|
+
if depth >= MAX_DEPTH:
|
|
150
|
+
continue
|
|
151
|
+
if field.get("type") == "array":
|
|
152
|
+
items = _resolve(components, field.get("items"))
|
|
153
|
+
if "properties" in items and items.get("_name") not in seen:
|
|
154
|
+
rows.extend(_fields(components, items, f"{path}[].", depth + 1, seen | {items.get("_name")}))
|
|
155
|
+
elif "properties" in field and field.get("_name") not in seen:
|
|
156
|
+
rows.extend(_fields(components, field, f"{path}.", depth + 1, seen | {field.get("_name")}))
|
|
157
|
+
return rows
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
def _row(name, field, required):
|
|
161
|
+
"""One field: its JSON type and format, and only what applies to it."""
|
|
162
|
+
row = {"campo": name, "tipo": field.get("type") or ("string" if "enum" in field else None), "obligatorio": required}
|
|
163
|
+
if field.get("format"):
|
|
164
|
+
row["formato"] = field["format"]
|
|
165
|
+
if field.get("type") == "array":
|
|
166
|
+
items = field.get("items") or {}
|
|
167
|
+
row["de"] = "object" if "properties" in items else items.get("type") or ("string" if "enum" in items else None)
|
|
168
|
+
values = _values(field)
|
|
169
|
+
if field.get("type") == "array" and not values:
|
|
170
|
+
values = _values(field.get("items") or {})
|
|
171
|
+
if values:
|
|
172
|
+
row["valores"] = values
|
|
173
|
+
if field.get("nullable"):
|
|
174
|
+
row["nulo"] = True
|
|
175
|
+
if field.get("_blank"):
|
|
176
|
+
row["vacio"] = True
|
|
177
|
+
if "default" in field:
|
|
178
|
+
row["por_defecto"] = field["default"]
|
|
179
|
+
if field.get("maxLength"):
|
|
180
|
+
row["maximo_largo"] = field["maxLength"]
|
|
181
|
+
for key, label in (("minimum", "minimo"), ("maximum", "maximo")):
|
|
182
|
+
if key in field and field[key] not in INT32:
|
|
183
|
+
row[label] = field[key]
|
|
184
|
+
decimals = DECIMALS.search(field.get("pattern", "")) if field.get("format") == "decimal" else None
|
|
185
|
+
if decimals:
|
|
186
|
+
row["decimales"] = int(decimals.group(1))
|
|
187
|
+
if field.get("description"):
|
|
188
|
+
row["nota"] = field["description"].strip()
|
|
189
|
+
return row
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
def _values(field):
|
|
193
|
+
labels = field.get("_labels") or {}
|
|
194
|
+
values = [value for value in field.get("enum", []) if value not in (None, "")]
|
|
195
|
+
return [{"valor": value, "etiqueta": labels.get(value, value)} for value in values]
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
def _parameter(components, parameter):
|
|
199
|
+
"""A filter or a value in the address. A filter's labels come in its own
|
|
200
|
+
description, not in its schema's."""
|
|
201
|
+
field = _resolve(components, parameter.get("schema"))
|
|
202
|
+
description = (parameter.get("description") or "").strip()
|
|
203
|
+
labels = dict(LABEL_LINE.findall(description))
|
|
204
|
+
if "enum" in field and labels:
|
|
205
|
+
field["_labels"] = labels
|
|
206
|
+
description = ""
|
|
207
|
+
if description:
|
|
208
|
+
field["description"] = description
|
|
209
|
+
row = _row(parameter["name"], field, bool(parameter.get("required")))
|
|
210
|
+
if parameter.get("in") == "path":
|
|
211
|
+
# Only the name and type: it's written in the address itself.
|
|
212
|
+
return {key: row[key] for key in ("campo", "tipo", "obligatorio")}
|
|
213
|
+
return row
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
def _answer(components, operation):
|
|
217
|
+
"""What a call answers: its status, whether it comes by pages, and the
|
|
218
|
+
names of a record's fields."""
|
|
219
|
+
for status, response in operation.get("responses", {}).items():
|
|
220
|
+
if not status.startswith("2"):
|
|
221
|
+
continue
|
|
222
|
+
schema = (response.get("content", {}).get("application/json") or {}).get("schema")
|
|
223
|
+
if schema is None:
|
|
224
|
+
return {"estado": int(status), "pagina": False, "campos": []}
|
|
225
|
+
record = _resolve(components, schema)
|
|
226
|
+
paged = PAGE_FIELDS <= set(record.get("properties", {}))
|
|
227
|
+
if paged:
|
|
228
|
+
record = _resolve(components, record["properties"]["results"])
|
|
229
|
+
if record.get("type") == "array":
|
|
230
|
+
record = _resolve(components, record.get("items"))
|
|
231
|
+
return {"estado": int(status), "pagina": paged, "campos": list(record.get("properties", {}))}
|
|
232
|
+
return {"estado": None, "pagina": False, "campos": []}
|
|
@@ -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")
|
|
@@ -15,12 +15,14 @@ from .common import (
|
|
|
15
15
|
CsvOption,
|
|
16
16
|
JsonOption,
|
|
17
17
|
YesOption,
|
|
18
|
+
answer_of,
|
|
18
19
|
client_of,
|
|
19
20
|
confirm_write,
|
|
20
21
|
day,
|
|
21
22
|
month,
|
|
22
23
|
number,
|
|
23
24
|
output_format,
|
|
25
|
+
print_result,
|
|
24
26
|
)
|
|
25
27
|
from .empresas import (
|
|
26
28
|
ENVIRONMENT,
|
|
@@ -71,9 +73,10 @@ def nueva_accion(
|
|
|
71
73
|
tipo: Annotated[TipoAccion, typer.Option("--tipo")] = TipoAccion.correctiva,
|
|
72
74
|
empresa: EmpresaOption = None,
|
|
73
75
|
si: YesOption = False,
|
|
76
|
+
as_json: JsonOption = False,
|
|
74
77
|
):
|
|
75
78
|
"""Agrega una acción al plan de acción ambiental."""
|
|
76
|
-
create_action(ctx, ENVIRONMENT, titulo, para, responsable, tipo, empresa, si)
|
|
79
|
+
create_action(ctx, ENVIRONMENT, titulo, para, responsable, tipo, empresa, si, as_json)
|
|
77
80
|
|
|
78
81
|
|
|
79
82
|
@app.command("accion-hecha")
|
|
@@ -83,9 +86,10 @@ def accion_hecha(
|
|
|
83
86
|
evidencia: Annotated[str, typer.Option("--evidencia", help="Qué se hizo.")],
|
|
84
87
|
empresa: EmpresaOption = None,
|
|
85
88
|
si: YesOption = False,
|
|
89
|
+
as_json: JsonOption = False,
|
|
86
90
|
):
|
|
87
91
|
"""Marca hecha una acción, con lo que se hizo. Verificarla se hace en la app."""
|
|
88
|
-
mark_done(ctx, ENVIRONMENT, accion, evidencia, empresa, si)
|
|
92
|
+
mark_done(ctx, ENVIRONMENT, accion, evidencia, empresa, si, as_json)
|
|
89
93
|
|
|
90
94
|
|
|
91
95
|
@app.command()
|
|
@@ -126,6 +130,7 @@ def residuo(
|
|
|
126
130
|
sede: Annotated[Optional[str], typer.Option("--sede", help="La sede, si dos tienen una corriente con ese nombre.")] = None,
|
|
127
131
|
empresa: EmpresaOption = None,
|
|
128
132
|
si: YesOption = False,
|
|
133
|
+
as_json: JsonOption = False,
|
|
129
134
|
):
|
|
130
135
|
"""Registra los kilos de residuos generados (pesados)."""
|
|
131
136
|
client = client_of(ctx)
|
|
@@ -133,8 +138,8 @@ def residuo(
|
|
|
133
138
|
stream = _stream(client, path, corriente, sede)
|
|
134
139
|
entry = {"stream": stream["id"], "date": day(fecha) or date.today().isoformat(), "kg": number(kg)}
|
|
135
140
|
confirm_write(f"Registrar {kg} kg de «{stream['name']}» el {shown_moment(entry['date'])}.", si)
|
|
136
|
-
client.send("POST", f"{path}/waste/entries", json=entry)
|
|
137
|
-
|
|
141
|
+
recorded = answer_of(client.send("POST", f"{path}/waste/entries", json=entry))
|
|
142
|
+
print_result(recorded, as_json, f"Registrados {kg} kg de {stream['name']}.")
|
|
138
143
|
|
|
139
144
|
|
|
140
145
|
def _stream(client, path, corriente, sede):
|
|
@@ -163,6 +168,7 @@ def lectura(
|
|
|
163
168
|
fecha: FechaOption = None,
|
|
164
169
|
empresa: EmpresaOption = None,
|
|
165
170
|
si: YesOption = False,
|
|
171
|
+
as_json: JsonOption = False,
|
|
166
172
|
):
|
|
167
173
|
"""Registra la lectura de un medidor (agua, energía, gas)."""
|
|
168
174
|
client = client_of(ctx)
|
|
@@ -170,5 +176,5 @@ def lectura(
|
|
|
170
176
|
meter = find_in(client.get(f"{path}/consumption/meters"), medidor, ["name", "meter_name"], "medidor")
|
|
171
177
|
reading = {"source": meter["id"], "read_on": day(fecha) or date.today().isoformat(), "value": number(valor)}
|
|
172
178
|
confirm_write(f"Lectura {valor} en «{meter['name']}» el {shown_moment(reading['read_on'])}.", si)
|
|
173
|
-
client.send("POST", f"{path}/consumption/readings", json=reading)
|
|
174
|
-
|
|
179
|
+
recorded = answer_of(client.send("POST", f"{path}/consumption/readings", json=reading))
|
|
180
|
+
print_result(recorded, as_json, f"Lectura registrada: {valor} en {meter['name']}.")
|
|
@@ -10,7 +10,7 @@ from ..lookup import NoSingleMatch, find_in, find_one
|
|
|
10
10
|
from ..output import cell_text, print_card, print_record, print_rows
|
|
11
11
|
from .common import (
|
|
12
12
|
AllOption, CliError, CsvOption, JsonOption, SearchOption, YesOption, client_of, confirm_write, list_rows,
|
|
13
|
-
output_format, without_empty,
|
|
13
|
+
output_format, print_result, without_empty,
|
|
14
14
|
)
|
|
15
15
|
|
|
16
16
|
app = typer.Typer(help="Artículos (Inventario): los repuestos, consumibles y herramientas que manejas.", no_args_is_help=True)
|
|
@@ -98,6 +98,7 @@ def crear(
|
|
|
98
98
|
fabricante: Annotated[Optional[str], typer.Option("--fabricante")] = None,
|
|
99
99
|
referencia: Annotated[Optional[str], typer.Option("--referencia", help="La referencia del fabricante.")] = None,
|
|
100
100
|
si: YesOption = False,
|
|
101
|
+
as_json: JsonOption = False,
|
|
101
102
|
):
|
|
102
103
|
"""Crea un artículo en el catálogo. Lo que hay se carga después con `inventario entrada`."""
|
|
103
104
|
client = client_of(ctx)
|
|
@@ -119,7 +120,9 @@ def crear(
|
|
|
119
120
|
})
|
|
120
121
|
confirm_write(f"Crear el artículo «{nombre}».", si)
|
|
121
122
|
created = client.send("POST", "inventory/items", json=item).json()
|
|
122
|
-
|
|
123
|
+
print_result(created, as_json, f"Artículo {created['code']} creado: {created['name']}.")
|
|
124
|
+
if as_json:
|
|
125
|
+
return
|
|
123
126
|
typer.echo(
|
|
124
127
|
f"Para cargar lo que hay: operx inventario entrada --articulo {created['code']} --lugar … --cantidad … --costo-unitario …"
|
|
125
128
|
)
|
|
@@ -14,11 +14,13 @@ from .common import (
|
|
|
14
14
|
CsvOption,
|
|
15
15
|
JsonOption,
|
|
16
16
|
YesOption,
|
|
17
|
+
answer_of,
|
|
17
18
|
client_of,
|
|
18
19
|
confirm_write,
|
|
19
20
|
day,
|
|
20
21
|
list_rows,
|
|
21
22
|
output_format,
|
|
23
|
+
print_result,
|
|
22
24
|
)
|
|
23
25
|
|
|
24
26
|
NOTICE_COLUMNS = [("when", "Cuándo"), ("title", "Aviso"), ("body", "Detalle"), ("read", "Leído")]
|
|
@@ -40,8 +42,8 @@ def avisos(
|
|
|
40
42
|
client = client_of(ctx)
|
|
41
43
|
if marcar_leidos:
|
|
42
44
|
confirm_write("Marcar todos tus avisos como leídos.", si)
|
|
43
|
-
client.send("POST", "notifications/mark-read")
|
|
44
|
-
|
|
45
|
+
marked = answer_of(client.send("POST", "notifications/mark-read"))
|
|
46
|
+
print_result(marked, as_json, "Listo: no te quedan avisos sin leer.")
|
|
45
47
|
return
|
|
46
48
|
output = output_format(as_json, as_csv)
|
|
47
49
|
rows = list_rows(client, "notifications", {"is_read": None if todos else "false"}, todas)
|