operx-cli 0.9.0__tar.gz → 0.10.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.
Files changed (83) hide show
  1. {operx_cli-0.9.0 → operx_cli-0.10.0}/PKG-INFO +5 -1
  2. {operx_cli-0.9.0 → operx_cli-0.10.0}/README.md +4 -0
  3. {operx_cli-0.9.0 → operx_cli-0.10.0}/operx_cli/AGENTES.md +6 -0
  4. {operx_cli-0.9.0 → operx_cli-0.10.0}/operx_cli/__init__.py +1 -1
  5. {operx_cli-0.9.0 → operx_cli-0.10.0}/operx_cli/commands/campos.py +81 -32
  6. {operx_cli-0.9.0 → operx_cli-0.10.0}/operx_cli.egg-info/PKG-INFO +5 -1
  7. {operx_cli-0.9.0 → operx_cli-0.10.0}/pyproject.toml +1 -1
  8. {operx_cli-0.9.0 → operx_cli-0.10.0}/tests/test_campos.py +67 -0
  9. {operx_cli-0.9.0 → operx_cli-0.10.0}/operx_cli/agent_guide.py +0 -0
  10. {operx_cli-0.9.0 → operx_cli-0.10.0}/operx_cli/api_fields.py +0 -0
  11. {operx_cli-0.9.0 → operx_cli-0.10.0}/operx_cli/app.py +0 -0
  12. {operx_cli-0.9.0 → operx_cli-0.10.0}/operx_cli/browser_login.py +0 -0
  13. {operx_cli-0.9.0 → operx_cli-0.10.0}/operx_cli/client.py +0 -0
  14. {operx_cli-0.9.0 → operx_cli-0.10.0}/operx_cli/commands/__init__.py +0 -0
  15. {operx_cli-0.9.0 → operx_cli-0.10.0}/operx_cli/commands/account.py +0 -0
  16. {operx_cli-0.9.0 → operx_cli-0.10.0}/operx_cli/commands/ambiental.py +0 -0
  17. {operx_cli-0.9.0 → operx_cli-0.10.0}/operx_cli/commands/articulos.py +0 -0
  18. {operx_cli-0.9.0 → operx_cli-0.10.0}/operx_cli/commands/avisos.py +0 -0
  19. {operx_cli-0.9.0 → operx_cli-0.10.0}/operx_cli/commands/chat.py +0 -0
  20. {operx_cli-0.9.0 → operx_cli-0.10.0}/operx_cli/commands/common.py +0 -0
  21. {operx_cli-0.9.0 → operx_cli-0.10.0}/operx_cli/commands/completar.py +0 -0
  22. {operx_cli-0.9.0 → operx_cli-0.10.0}/operx_cli/commands/empresas.py +0 -0
  23. {operx_cli-0.9.0 → operx_cli-0.10.0}/operx_cli/commands/inventario.py +0 -0
  24. {operx_cli-0.9.0 → operx_cli-0.10.0}/operx_cli/commands/lecturas.py +0 -0
  25. {operx_cli-0.9.0 → operx_cli-0.10.0}/operx_cli/commands/muestreo.py +0 -0
  26. {operx_cli-0.9.0 → operx_cli-0.10.0}/operx_cli/commands/ordenes.py +0 -0
  27. {operx_cli-0.9.0 → operx_cli-0.10.0}/operx_cli/commands/personas.py +0 -0
  28. {operx_cli-0.9.0 → operx_cli-0.10.0}/operx_cli/commands/planta.py +0 -0
  29. {operx_cli-0.9.0 → operx_cli-0.10.0}/operx_cli/commands/preventivos.py +0 -0
  30. {operx_cli-0.9.0 → operx_cli-0.10.0}/operx_cli/commands/raw.py +0 -0
  31. {operx_cli-0.9.0 → operx_cli-0.10.0}/operx_cli/commands/registros.py +0 -0
  32. {operx_cli-0.9.0 → operx_cli-0.10.0}/operx_cli/commands/reportes.py +0 -0
  33. {operx_cli-0.9.0 → operx_cli-0.10.0}/operx_cli/commands/rutinas.py +0 -0
  34. {operx_cli-0.9.0 → operx_cli-0.10.0}/operx_cli/commands/search.py +0 -0
  35. {operx_cli-0.9.0 → operx_cli-0.10.0}/operx_cli/commands/sst.py +0 -0
  36. {operx_cli-0.9.0 → operx_cli-0.10.0}/operx_cli/commands/update.py +0 -0
  37. {operx_cli-0.9.0 → operx_cli-0.10.0}/operx_cli/lookup.py +0 -0
  38. {operx_cli-0.9.0 → operx_cli-0.10.0}/operx_cli/output.py +0 -0
  39. {operx_cli-0.9.0 → operx_cli-0.10.0}/operx_cli/record_completion.py +0 -0
  40. {operx_cli-0.9.0 → operx_cli-0.10.0}/operx_cli/self_update.py +0 -0
  41. {operx_cli-0.9.0 → operx_cli-0.10.0}/operx_cli/settings.py +0 -0
  42. {operx_cli-0.9.0 → operx_cli-0.10.0}/operx_cli/shell.py +0 -0
  43. {operx_cli-0.9.0 → operx_cli-0.10.0}/operx_cli/spreadsheet.py +0 -0
  44. {operx_cli-0.9.0 → operx_cli-0.10.0}/operx_cli/updates.py +0 -0
  45. {operx_cli-0.9.0 → operx_cli-0.10.0}/operx_cli/values.py +0 -0
  46. {operx_cli-0.9.0 → operx_cli-0.10.0}/operx_cli/welcome.py +0 -0
  47. {operx_cli-0.9.0 → operx_cli-0.10.0}/operx_cli.egg-info/SOURCES.txt +0 -0
  48. {operx_cli-0.9.0 → operx_cli-0.10.0}/operx_cli.egg-info/dependency_links.txt +0 -0
  49. {operx_cli-0.9.0 → operx_cli-0.10.0}/operx_cli.egg-info/entry_points.txt +0 -0
  50. {operx_cli-0.9.0 → operx_cli-0.10.0}/operx_cli.egg-info/requires.txt +0 -0
  51. {operx_cli-0.9.0 → operx_cli-0.10.0}/operx_cli.egg-info/top_level.txt +0 -0
  52. {operx_cli-0.9.0 → operx_cli-0.10.0}/setup.cfg +0 -0
  53. {operx_cli-0.9.0 → operx_cli-0.10.0}/tests/test_agentes.py +0 -0
  54. {operx_cli-0.9.0 → operx_cli-0.10.0}/tests/test_api_campos.py +0 -0
  55. {operx_cli-0.9.0 → operx_cli-0.10.0}/tests/test_app.py +0 -0
  56. {operx_cli-0.9.0 → operx_cli-0.10.0}/tests/test_articulos.py +0 -0
  57. {operx_cli-0.9.0 → operx_cli-0.10.0}/tests/test_cards.py +0 -0
  58. {operx_cli-0.9.0 → operx_cli-0.10.0}/tests/test_chat.py +0 -0
  59. {operx_cli-0.9.0 → operx_cli-0.10.0}/tests/test_client.py +0 -0
  60. {operx_cli-0.9.0 → operx_cli-0.10.0}/tests/test_completar.py +0 -0
  61. {operx_cli-0.9.0 → operx_cli-0.10.0}/tests/test_daily_flows.py +0 -0
  62. {operx_cli-0.9.0 → operx_cli-0.10.0}/tests/test_inventario_lecturas.py +0 -0
  63. {operx_cli-0.9.0 → operx_cli-0.10.0}/tests/test_json_writes.py +0 -0
  64. {operx_cli-0.9.0 → operx_cli-0.10.0}/tests/test_login.py +0 -0
  65. {operx_cli-0.9.0 → operx_cli-0.10.0}/tests/test_logout_and_routes.py +0 -0
  66. {operx_cli-0.9.0 → operx_cli-0.10.0}/tests/test_lookup.py +0 -0
  67. {operx_cli-0.9.0 → operx_cli-0.10.0}/tests/test_more_of_the_api.py +0 -0
  68. {operx_cli-0.9.0 → operx_cli-0.10.0}/tests/test_muestreo.py +0 -0
  69. {operx_cli-0.9.0 → operx_cli-0.10.0}/tests/test_ordenes.py +0 -0
  70. {operx_cli-0.9.0 → operx_cli-0.10.0}/tests/test_output.py +0 -0
  71. {operx_cli-0.9.0 → operx_cli-0.10.0}/tests/test_personas.py +0 -0
  72. {operx_cli-0.9.0 → operx_cli-0.10.0}/tests/test_preventivos.py +0 -0
  73. {operx_cli-0.9.0 → operx_cli-0.10.0}/tests/test_record_completion.py +0 -0
  74. {operx_cli-0.9.0 → operx_cli-0.10.0}/tests/test_registros.py +0 -0
  75. {operx_cli-0.9.0 → operx_cli-0.10.0}/tests/test_rutinas.py +0 -0
  76. {operx_cli-0.9.0 → operx_cli-0.10.0}/tests/test_self_update.py +0 -0
  77. {operx_cli-0.9.0 → operx_cli-0.10.0}/tests/test_settings.py +0 -0
  78. {operx_cli-0.9.0 → operx_cli-0.10.0}/tests/test_shell.py +0 -0
  79. {operx_cli-0.9.0 → operx_cli-0.10.0}/tests/test_spreadsheet.py +0 -0
  80. {operx_cli-0.9.0 → operx_cli-0.10.0}/tests/test_sst_ambiental.py +0 -0
  81. {operx_cli-0.9.0 → operx_cli-0.10.0}/tests/test_updates.py +0 -0
  82. {operx_cli-0.9.0 → operx_cli-0.10.0}/tests/test_values.py +0 -0
  83. {operx_cli-0.9.0 → operx_cli-0.10.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.9.0
3
+ Version: 0.10.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
@@ -184,6 +184,10 @@ operx campos listar # la información ex
184
184
  operx campos crear "Hijos" --tipo numero
185
185
  operx campos guardar "Ana Ruiz" --valor "Hijos=2" --valor "Fecha de nacimiento=17/05/1990"
186
186
  operx campos ver "Ana Ruiz"
187
+ operx campos listar --de equipos # la información extra de los equipos
188
+ operx campos crear "Voltaje" --tipo numero --de equipos
189
+ operx campos guardar BOM-01 --valor "Voltaje=220" --de equipos
190
+ operx campos ver BOM-01 --de equipos
187
191
 
188
192
  operx sst acciones --estado abiertas
189
193
  operx sst nueva-accion --titulo "Señalizar la salida" --para 15/11/2026 --responsable "Ana Ruiz"
@@ -170,6 +170,10 @@ operx campos listar # la información ex
170
170
  operx campos crear "Hijos" --tipo numero
171
171
  operx campos guardar "Ana Ruiz" --valor "Hijos=2" --valor "Fecha de nacimiento=17/05/1990"
172
172
  operx campos ver "Ana Ruiz"
173
+ operx campos listar --de equipos # la información extra de los equipos
174
+ operx campos crear "Voltaje" --tipo numero --de equipos
175
+ operx campos guardar BOM-01 --valor "Voltaje=220" --de equipos
176
+ operx campos ver BOM-01 --de equipos
173
177
 
174
178
  operx sst acciones --estado abiertas
175
179
  operx sst nueva-accion --titulo "Señalizar la salida" --para 15/11/2026 --responsable "Ana Ruiz"
@@ -110,6 +110,12 @@ llamada queda en el historial del token, marcada como hecha desde la CLI.
110
110
  los campos que digas; `--vaciar Hijos` deja uno vacío) y `campos ver`. Antes
111
111
  de crear un campo, mira que no exista ya. `campos quitar` lo oculta de todas
112
112
  las personas y deja los valores guardados.
113
+ - Lo que la ficha de un equipo no tiene (voltaje, capacidad, última
114
+ inspección…) va igual, con `--de equipos` en cada comando: `campos listar
115
+ --de equipos`, `campos crear "Voltaje" --tipo numero --de equipos`, `campos
116
+ guardar BOM-01 --valor "Voltaje=220" --de equipos`. Personas y equipos tienen
117
+ campos aparte. Los clientes ven estos valores en su portal: no guardes ahí lo
118
+ que no deban leer.
113
119
  - SG-SST y Ambiental van por empresa: sin `--empresa`, la de la cuenta. Una
114
120
  cuenta que lleva el SG-SST o lo ambiental de sus clientes ve las demás con
115
121
  `operx sst empresas` o `operx ambiental empresas`.
@@ -2,4 +2,4 @@
2
2
  API (backend/users/api_allowlist.py) from a terminal, with a personal API
3
3
  token."""
4
4
 
5
- __version__ = "0.9.0"
5
+ __version__ = "0.10.0"
@@ -1,9 +1,12 @@
1
1
  """`operx campos`: Información extra, the fields a company adds to the record
2
- of its people (a birthday, the children, the marital status...) for what the
3
- record doesn't have, and their values on each person
4
- (backend/extra_fields, backend/people/extra_info.py). The fields are
5
- company-wide; each person has a value or none for every one."""
6
-
2
+ of its people (a birthday, the children, the marital status...) or of its
3
+ machines (the voltage, the last inspection...) for what the record doesn't
4
+ have, and their values on each one (backend/extra_fields,
5
+ backend/people/extra_info.py, backend/assets/extra_info.py). The fields are
6
+ company-wide; each record has a value or none for every one. `--de` says which
7
+ kind of record: people unless it says otherwise."""
8
+
9
+ from dataclasses import dataclass
7
10
  from decimal import Decimal
8
11
  from enum import Enum
9
12
  from typing import Annotated, Optional
@@ -16,12 +19,42 @@ from ..values import decimal_text, iso_date
16
19
  from .common import CliError, CsvOption, JsonOption, YesOption, client_of, confirm_write, output_format, print_result
17
20
 
18
21
  app = typer.Typer(
19
- help="Información extra: los campos que tu empresa agrega a la ficha de las personas, y sus valores.",
22
+ help="Información extra: los campos que tu empresa agrega a personas y equipos, y sus valores.",
20
23
  no_args_is_help=True,
21
24
  )
22
25
 
23
- # The kinds of record that can have extra fields; for now, people.
24
- ENTITY = "employee"
26
+
27
+ class Registro(str, Enum):
28
+ personas = "personas"
29
+ equipos = "equipos"
30
+
31
+
32
+ @dataclass(frozen=True)
33
+ class RecordKind:
34
+ """How a kind of record is found and where its values are."""
35
+
36
+ # A key of backend/extra_fields/entities.py.
37
+ entity: str
38
+ everyone: str
39
+ list_path: str
40
+ lookup_fields: tuple
41
+ what: str
42
+ name_field: str
43
+ values_path: str
44
+
45
+
46
+ KINDS = {
47
+ Registro.personas: RecordKind(
48
+ entity="employee", everyone="todas las personas", list_path="employees",
49
+ lookup_fields=("user_name", "employee_code"), what="persona", name_field="user_name",
50
+ values_path="people/employees/{id}/extra-fields",
51
+ ),
52
+ Registro.equipos: RecordKind(
53
+ entity="equipment", everyone="todos los equipos", list_path="assets",
54
+ lookup_fields=("code", "name"), what="equipo", name_field="name",
55
+ values_path="assets/{id}/extra-fields",
56
+ ),
57
+ }
25
58
 
26
59
  FIELD_COLUMNS = [("id", "Id"), ("name", "Campo"), ("type", "Tipo"), ("options", "Opciones")]
27
60
  VALUE_COLUMNS = [("name", "Campo"), ("value", "Valor")]
@@ -50,10 +83,14 @@ OpcionOption = Annotated[
50
83
  Optional[list[str]],
51
84
  typer.Option("--opcion", help="Una opción de la lista; repítela para varias. Solo para el tipo lista."),
52
85
  ]
86
+ DeOption = Annotated[Registro, typer.Option("--de", help="De qué registro son los campos: personas o equipos.")]
87
+ RegistroArgument = Annotated[
88
+ str, typer.Argument(help="El nombre exacto, el código o el id de la persona o del equipo."),
89
+ ]
53
90
 
54
91
 
55
- def _fields(client):
56
- return client.get("extra-fields", {"entity": ENTITY})
92
+ def _fields(client, kind):
93
+ return client.get("extra-fields", {"entity": kind.entity})
57
94
 
58
95
 
59
96
  def _find_field(fields, text):
@@ -64,8 +101,8 @@ def _find_field(fields, text):
64
101
  raise CliError(str(error)) from None
65
102
 
66
103
 
67
- def _person(client, text):
68
- return find_one(client, "employees", text, ["user_name", "employee_code"], "persona")
104
+ def _record(client, kind, text):
105
+ return find_one(client, kind.list_path, text, list(kind.lookup_fields), kind.what)
69
106
 
70
107
 
71
108
  def _split_pair(pair):
@@ -123,12 +160,13 @@ def _shown(field):
123
160
  @app.command()
124
161
  def listar(
125
162
  ctx: typer.Context,
163
+ de: DeOption = Registro.personas,
126
164
  as_json: JsonOption = False,
127
165
  as_csv: CsvOption = False,
128
166
  ):
129
- """Los campos de información extra de las personas: su nombre, su tipo y las opciones de las listas."""
167
+ """Los campos de información extra de las personas o de los equipos: su nombre, su tipo y las opciones de las listas."""
130
168
  output = output_format(as_json, as_csv)
131
- fields = _fields(client_of(ctx))
169
+ fields = _fields(client_of(ctx), KINDS[de])
132
170
  if output != "json":
133
171
  fields = [
134
172
  {**field, "type": TYPE_LABELS.get(field["field_type"], field["field_type"]), "options": ", ".join(field["options"])}
@@ -143,16 +181,18 @@ def crear(
143
181
  nombre: Annotated[str, typer.Argument(help="El nombre del campo: «Fecha de nacimiento».")],
144
182
  tipo: Annotated[Tipo, typer.Option("--tipo", help="Qué guarda el campo; no se cambia después.")] = Tipo.texto,
145
183
  opcion: OpcionOption = None,
184
+ de: DeOption = Registro.personas,
146
185
  si: YesOption = False,
147
186
  as_json: JsonOption = False,
148
187
  ):
149
- """Agrega un campo a la ficha de las personas. Todas las personas lo tienen, vacío hasta que se llene."""
188
+ """Agrega un campo a la ficha de las personas o de los equipos. Todos lo tienen, vacío hasta que se llene."""
150
189
  if tipo != Tipo.lista and opcion:
151
190
  raise CliError("--opcion solo va con --tipo lista.")
152
191
  if tipo == Tipo.lista and not opcion:
153
192
  raise CliError("Una lista necesita sus opciones: --opcion «Soltero/a» --opcion «Casado/a».")
154
- body = {"entity": ENTITY, "name": nombre, "field_type": TYPE_KEYS[tipo.value], "options": opcion or []}
155
- confirm_write(f"Agregar a las personas el campo «{nombre}» ({TYPE_LABELS[body['field_type']]}).", si)
193
+ kind = KINDS[de]
194
+ body = {"entity": kind.entity, "name": nombre, "field_type": TYPE_KEYS[tipo.value], "options": opcion or []}
195
+ confirm_write(f"Agregar a {kind.everyone} el campo «{nombre}» ({TYPE_LABELS[body['field_type']]}).", si)
156
196
  created = client_of(ctx).send("POST", "extra-fields", json=body).json()
157
197
  print_result(created, as_json, f"Campo {created['id']} creado: {created['name']}.")
158
198
 
@@ -163,6 +203,7 @@ def cambiar(
163
203
  campo: Annotated[str, typer.Argument(help="El id o el nombre exacto del campo.")],
164
204
  nombre: Annotated[Optional[str], typer.Option("--nombre", help="El nombre nuevo.")] = None,
165
205
  opcion: OpcionOption = None,
206
+ de: DeOption = Registro.personas,
166
207
  si: YesOption = False,
167
208
  as_json: JsonOption = False,
168
209
  ):
@@ -170,7 +211,7 @@ def cambiar(
170
211
  if not (nombre or opcion):
171
212
  raise CliError("Di qué cambiar: --nombre o --opcion.")
172
213
  client = client_of(ctx)
173
- found = _find_field(_fields(client), campo)
214
+ found = _find_field(_fields(client, KINDS[de]), campo)
174
215
  body = {}
175
216
  if nombre:
176
217
  body["name"] = nombre
@@ -185,29 +226,35 @@ def cambiar(
185
226
  def quitar(
186
227
  ctx: typer.Context,
187
228
  campo: Annotated[str, typer.Argument(help="El id o el nombre exacto del campo.")],
229
+ de: DeOption = Registro.personas,
188
230
  si: YesOption = False,
189
231
  as_json: JsonOption = False,
190
232
  ):
191
- """Quita un campo de la ficha de todas las personas. Los valores ya guardados quedan en la base de datos."""
233
+ """Quita un campo de la ficha de todas las personas o todos los equipos. Los valores ya guardados quedan en la base de datos."""
192
234
  client = client_of(ctx)
193
- found = _find_field(_fields(client), campo)
194
- confirm_write(f"Quitar el campo «{found['name']}» de todas las personas.", si)
235
+ kind = KINDS[de]
236
+ found = _find_field(_fields(client, kind), campo)
237
+ confirm_write(f"Quitar el campo «{found['name']}» de {kind.everyone}.", si)
195
238
  client.send("DELETE", f"extra-fields/{found['id']}")
196
- print_result({"id": found["id"], "removed": True}, as_json, f"Campo {found['id']} quitado: {found['name']}.")
239
+ print_result(
240
+ {"id": found["id"], "removed": True}, as_json, f"Campo {found['id']} quitado de {kind.everyone}: {found['name']}.",
241
+ )
197
242
 
198
243
 
199
244
  @app.command()
200
245
  def ver(
201
246
  ctx: typer.Context,
202
- persona: Annotated[str, typer.Argument(help="El nombre exacto, el código o el id de la persona.")],
247
+ registro: RegistroArgument,
248
+ de: DeOption = Registro.personas,
203
249
  as_json: JsonOption = False,
204
250
  as_csv: CsvOption = False,
205
251
  ):
206
- """La información extra de una persona: cada campo con su valor."""
252
+ """La información extra de una persona o de un equipo: cada campo con su valor."""
207
253
  output = output_format(as_json, as_csv)
208
254
  client = client_of(ctx)
209
- person = _person(client, persona)
210
- answer = client.get(f"people/employees/{person['id']}/extra-fields")
255
+ kind = KINDS[de]
256
+ found = _record(client, kind, registro)
257
+ answer = client.get(kind.values_path.format(id=found["id"]))
211
258
  if output == "json":
212
259
  print_record(answer, "json")
213
260
  return
@@ -218,7 +265,7 @@ def ver(
218
265
  @app.command()
219
266
  def guardar(
220
267
  ctx: typer.Context,
221
- persona: Annotated[str, typer.Argument(help="El nombre exacto, el código o el id de la persona.")],
268
+ registro: RegistroArgument,
222
269
  valor: Annotated[
223
270
  Optional[list[str]],
224
271
  typer.Option("--valor", help="Campo=valor, por ejemplo «Hijos=2» o «Fecha de nacimiento=17/05/1990»; repítelo para varios."),
@@ -226,14 +273,16 @@ def guardar(
226
273
  vaciar: Annotated[
227
274
  Optional[list[str]], typer.Option("--vaciar", help="El campo que se deja vacío; repítelo para varios."),
228
275
  ] = None,
276
+ de: DeOption = Registro.personas,
229
277
  si: YesOption = False,
230
278
  as_json: JsonOption = False,
231
279
  ):
232
- """Guarda valores en la información extra de una persona. Solo cambian los campos que digas."""
280
+ """Guarda valores en la información extra de una persona o de un equipo. Solo cambian los campos que digas."""
233
281
  if not (valor or vaciar):
234
282
  raise CliError("Di qué guardar: --valor Campo=valor o --vaciar Campo.")
235
283
  client = client_of(ctx)
236
- fields = _fields(client)
284
+ kind = KINDS[de]
285
+ fields = _fields(client, kind)
237
286
  values = {}
238
287
  for pair in valor or []:
239
288
  name, text = _split_pair(pair)
@@ -241,9 +290,9 @@ def guardar(
241
290
  values[str(field["id"])] = _value_for(field, text)
242
291
  for name in vaciar or []:
243
292
  values[str(_find_field(fields, name)["id"])] = None
244
- person = _person(client, persona)
245
- who = person.get("user_name") or persona
293
+ found = _record(client, kind, registro)
294
+ who = found.get(kind.name_field) or registro
246
295
  changes = list(valor or []) + [f"vaciar {name}" for name in vaciar or []]
247
296
  confirm_write(f"Guardar en la información extra de {who}: {'; '.join(changes)}.", si)
248
- saved = client.send("PATCH", f"people/employees/{person['id']}/extra-fields", json={"values": values}).json()
297
+ saved = client.send("PATCH", kind.values_path.format(id=found["id"]), json={"values": values}).json()
249
298
  print_result(saved, as_json, f"Información extra de {who} guardada.")
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: operx-cli
3
- Version: 0.9.0
3
+ Version: 0.10.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
@@ -184,6 +184,10 @@ operx campos listar # la información ex
184
184
  operx campos crear "Hijos" --tipo numero
185
185
  operx campos guardar "Ana Ruiz" --valor "Hijos=2" --valor "Fecha de nacimiento=17/05/1990"
186
186
  operx campos ver "Ana Ruiz"
187
+ operx campos listar --de equipos # la información extra de los equipos
188
+ operx campos crear "Voltaje" --tipo numero --de equipos
189
+ operx campos guardar BOM-01 --valor "Voltaje=220" --de equipos
190
+ operx campos ver BOM-01 --de equipos
187
191
 
188
192
  operx sst acciones --estado abiertas
189
193
  operx sst nueva-accion --titulo "Señalizar la salida" --para 15/11/2026 --responsable "Ana Ruiz"
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "operx-cli"
7
- version = "0.9.0"
7
+ version = "0.10.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"
@@ -163,3 +163,70 @@ def test_guardar_with_nothing_to_save_says_so():
163
163
  fake = FakeApi({})
164
164
  result = run(fake, "campos", "guardar", "María Torres", "--si")
165
165
  assert result.exit_code == 1 and "Di qué guardar" in result.output
166
+
167
+
168
+ # --de equipos: the same commands on a machine's record.
169
+ # extra_fields/serializers.py ExtraFieldSerializer
170
+ VOLTAJE = {"id": 11, "entity": "equipment", "name": "Voltaje", "field_type": "number", "options": [], "position": 0}
171
+ MACHINE_FIELDS = [VOLTAJE]
172
+ # assets/serializers.py AssetListSerializer
173
+ BOMBA = {"id": 40, "name": "Bomba 1", "code": "BOM-01"}
174
+ # assets/extra_info.py extra_payload
175
+ BOMBA_VALUES = {"fields": [{"id": 11, "name": "Voltaje", "field_type": "number", "options": [], "value": 220}]}
176
+
177
+
178
+ def test_listar_de_equipos_asks_for_the_fields_of_the_machines():
179
+ fake = FakeApi({("GET", "extra-fields/"): MACHINE_FIELDS})
180
+ result = run(fake, "campos", "listar", "--de", "equipos")
181
+ assert result.exit_code == 0, result.output
182
+ assert "Voltaje" in result.output
183
+ assert fake.requests[0].url.params["entity"] == "equipment"
184
+
185
+
186
+ def test_crear_de_equipos_adds_the_field_to_the_machines():
187
+ fake = FakeApi({("POST", "extra-fields/"): (201, VOLTAJE)})
188
+ result = run(fake, "campos", "crear", "Voltaje", "--tipo", "numero", "--de", "equipos", "--si")
189
+ assert result.exit_code == 0, result.output
190
+ assert fake.sent("POST", "extra-fields/") == {"entity": "equipment", "name": "Voltaje", "field_type": "number", "options": []}
191
+
192
+
193
+ def test_cambiar_de_equipos_finds_the_field_among_those_of_the_machines():
194
+ fake = FakeApi({("GET", "extra-fields/"): MACHINE_FIELDS, ("PATCH", "extra-fields/11/"): VOLTAJE})
195
+ result = run(fake, "campos", "cambiar", "Voltaje", "--nombre", "Voltaje nominal", "--de", "equipos", "--si")
196
+ assert result.exit_code == 0, result.output
197
+ assert fake.requests[0].url.params["entity"] == "equipment"
198
+ assert fake.sent("PATCH", "extra-fields/11/") == {"name": "Voltaje nominal"}
199
+
200
+
201
+ def test_quitar_de_equipos_says_it_leaves_every_machine():
202
+ fake = FakeApi({("GET", "extra-fields/"): MACHINE_FIELDS, ("DELETE", "extra-fields/11/"): (204, None)})
203
+ result = run(fake, "campos", "quitar", "Voltaje", "--de", "equipos", "--si")
204
+ assert result.exit_code == 0, result.output
205
+ assert "todos los equipos" in result.output
206
+ assert fake.called("DELETE", "extra-fields/11/")
207
+
208
+
209
+ def test_ver_de_equipos_reads_the_machine_by_its_code():
210
+ fake = FakeApi({("GET", "assets/"): page(BOMBA), ("GET", "assets/40/extra-fields/"): BOMBA_VALUES})
211
+ result = run(fake, "campos", "ver", "BOM-01", "--de", "equipos")
212
+ assert result.exit_code == 0, result.output
213
+ assert "Voltaje" in result.output and "220" in result.output
214
+
215
+
216
+ def test_guardar_de_equipos_saves_on_the_machine():
217
+ fake = FakeApi({
218
+ ("GET", "extra-fields/"): MACHINE_FIELDS,
219
+ ("GET", "assets/"): page(BOMBA),
220
+ ("PATCH", "assets/40/extra-fields/"): BOMBA_VALUES,
221
+ })
222
+ result = run(fake, "campos", "guardar", "Bomba 1", "--valor", "Voltaje=220", "--de", "equipos", "--si")
223
+ assert result.exit_code == 0, result.output
224
+ assert "Bomba 1" in result.output
225
+ assert fake.sent("PATCH", "assets/40/extra-fields/") == {"values": {"11": 220}}
226
+
227
+
228
+ def test_de_refuses_a_kind_of_record_that_has_no_fields():
229
+ fake = FakeApi({})
230
+ result = run(fake, "campos", "listar", "--de", "ordenes")
231
+ assert result.exit_code != 0
232
+ assert fake.requests == []
File without changes
File without changes
File without changes
File without changes
File without changes