@nubiia/mcp-freematica 0.8.0 → 0.9.0

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 (40) hide show
  1. package/README.md +12 -7
  2. package/dist/clients/fiql-builder.d.ts +32 -8
  3. package/dist/clients/fiql-builder.js +42 -14
  4. package/dist/clients/fiql-builder.js.map +1 -1
  5. package/dist/clients/freematica-client.d.ts +92 -35
  6. package/dist/clients/freematica-client.js +251 -98
  7. package/dist/clients/freematica-client.js.map +1 -1
  8. package/dist/schemas/cartera.d.ts +7 -5
  9. package/dist/schemas/cartera.js +13 -11
  10. package/dist/schemas/cartera.js.map +1 -1
  11. package/dist/schemas/facturas-ventas.d.ts +20 -16
  12. package/dist/schemas/facturas-ventas.js +36 -32
  13. package/dist/schemas/facturas-ventas.js.map +1 -1
  14. package/dist/schemas/pedidos-compras.d.ts +8 -6
  15. package/dist/schemas/pedidos-compras.js +8 -6
  16. package/dist/schemas/pedidos-compras.js.map +1 -1
  17. package/dist/server-instructions.js +19 -0
  18. package/dist/server-instructions.js.map +1 -1
  19. package/dist/server.js +2 -0
  20. package/dist/server.js.map +1 -1
  21. package/dist/tools/articulos.d.ts +14 -0
  22. package/dist/tools/articulos.js +183 -0
  23. package/dist/tools/articulos.js.map +1 -0
  24. package/dist/tools/cartera.js +2 -2
  25. package/dist/tools/cartera.js.map +1 -1
  26. package/dist/tools/contabilidad.js +7 -7
  27. package/dist/tools/contabilidad.js.map +1 -1
  28. package/dist/tools/facturas-ventas.js +12 -12
  29. package/dist/tools/facturas-ventas.js.map +1 -1
  30. package/dist/tools/localizaciones.d.ts +1 -1
  31. package/dist/tools/localizaciones.js +3 -12
  32. package/dist/tools/localizaciones.js.map +1 -1
  33. package/dist/tools/personal.js +13 -17
  34. package/dist/tools/personal.js.map +1 -1
  35. package/dist/tools/prl.js +15 -49
  36. package/dist/tools/prl.js.map +1 -1
  37. package/dist/tools/proveedores.d.ts +2 -2
  38. package/dist/tools/proveedores.js +8 -6
  39. package/dist/tools/proveedores.js.map +1 -1
  40. package/package.json +1 -1
@@ -6,6 +6,38 @@ import { buildEstadoPedidoFiql, } from '../schemas/pedidos-compras.js';
6
6
  import { LOC_FIELD_MAP } from '../schemas/localizaciones.js';
7
7
  import { logger } from '../logger.js';
8
8
  import { loadMaxResponseSizeMb } from '../utils/size-guardrail.js';
9
+ /**
10
+ * Día siguiente a una fecha ISO (YYYY-MM-DD), en formato ISO.
11
+ *
12
+ * Se usa para emular "hasta X inclusive" como `=lt='X+1'` en los endpoints
13
+ * donde el operador `=le=` responde 400/500 (facturas-cabecera, cartera,
14
+ * export-asientos — verificado contra el API real).
15
+ */
16
+ function nextDayIso(isoDate) {
17
+ const fmt = (d) => {
18
+ // Formateo manual: toISOString() cambia de formato con años de 5+ dígitos
19
+ // (p.ej. 9999-12-31 + 1 día → '+010000-01-01…').
20
+ const y = String(d.getUTCFullYear()).padStart(4, '0');
21
+ const m = String(d.getUTCMonth() + 1).padStart(2, '0');
22
+ const day = String(d.getUTCDate()).padStart(2, '0');
23
+ return `${y}-${m}-${day}`;
24
+ };
25
+ const d = new Date(`${isoDate}T00:00:00Z`);
26
+ // Round-trip: V8 "arrastra" días fuera de rango (2026-02-30 → 2026-03-02)
27
+ // en vez de fallar; solo el round-trip detecta ambos casos (NaN y arrastre).
28
+ if (Number.isNaN(d.getTime()) || fmt(d) !== isoDate) {
29
+ throw new Error(`Fecha inválida: ${isoDate}. Usa una fecha de calendario real en formato YYYY-MM-DD.`);
30
+ }
31
+ d.setUTCDate(d.getUTCDate() + 1);
32
+ return fmt(d);
33
+ }
34
+ /**
35
+ * Mensaje de error para los endpoints donde el API de Freemática devuelve
36
+ * 0 filas si se combinan dos condiciones de rango sobre el MISMO campo
37
+ * (verificado con ge+lt, ge+le, orden inverso y paréntesis).
38
+ */
39
+ const RANGO_NO_SOPORTADO = 'El API de Freemática no soporta combinar fecha-desde y fecha-hasta a la vez ' +
40
+ 'en este endpoint (devuelve 0 resultados). Usa solo uno de los dos límites por consulta.';
9
41
  /**
10
42
  * Typed client for the Freemática REST API.
11
43
  *
@@ -32,6 +64,96 @@ export class FreematicaClient extends BaseClient {
32
64
  const data = await this.get('/pvss/v2/contratos-servicios-material');
33
65
  return { items: data.items, total: Number(data.total) };
34
66
  }
67
+ // ---------------------------------------------------------------------------
68
+ // Artículos — catálogo de inventario (v0.9.0)
69
+ // ---------------------------------------------------------------------------
70
+ /**
71
+ * Lista paginada de artículos del catálogo de inventario.
72
+ *
73
+ * Endpoint: GET /part/v1/articulos
74
+ *
75
+ * Todos los filtros son de coincidencia EXACTA (el endpoint no soporta
76
+ * búsqueda parcial: =lk= responde 400 y los wildcards % devuelven 0
77
+ * resultados — verificado contra el API real). OJO: COD_ARTICULO puede
78
+ * llevar espacios iniciales en Freemática (ej. " QQ10615") que forman
79
+ * parte del código.
80
+ *
81
+ * El filtro `activo` mapea a MOTIVO_BAJA (columna de texto — las columnas
82
+ * de fecha no permiten comparar con vacío):
83
+ * - `true` → MOTIVO_BAJA=='' (sin motivo de baja).
84
+ * - `false` → MOTIVO_BAJA!='' (con motivo de baja).
85
+ * Los artículos con MOTIVO_BAJA nulo (≈36 de 6304 en producción) no
86
+ * matchean ninguno de los dos valores.
87
+ *
88
+ * @param opts - Paginación y filtros opcionales.
89
+ * @returns Lista paginada de artículos (cada item incluye `idReg`).
90
+ */
91
+ async listArticulos(opts) {
92
+ const url = new URL('placeholder://x/part/v1/articulos');
93
+ if (opts.items !== undefined)
94
+ url.searchParams.set('items', String(opts.items));
95
+ if (opts.page !== undefined)
96
+ url.searchParams.set('page', String(opts.page));
97
+ const fiqlGroup = {};
98
+ if (opts.codArticulo !== undefined)
99
+ fiqlGroup['COD_ARTICULO'] = opts.codArticulo;
100
+ if (opts.tipoCodigo !== undefined)
101
+ fiqlGroup['TIPO_CODIGO'] = opts.tipoCodigo;
102
+ if (opts.codProveedor !== undefined)
103
+ fiqlGroup['COD_PROVEEDOR'] = opts.codProveedor;
104
+ if (opts.linea !== undefined)
105
+ fiqlGroup['COD_LIN_ART'] = opts.linea;
106
+ if (opts.familia !== undefined)
107
+ fiqlGroup['COD_FAMILIA'] = opts.familia;
108
+ if (opts.subfamilia !== undefined)
109
+ fiqlGroup['COD_SUBFAM'] = opts.subfamilia;
110
+ if (opts.descripcion !== undefined)
111
+ fiqlGroup['DESC_ART'] = opts.descripcion;
112
+ if (opts.codigoBarras !== undefined)
113
+ fiqlGroup['CODIGO_BARRAS'] = opts.codigoBarras;
114
+ if (opts.activo === true)
115
+ fiqlGroup['MOTIVO_BAJA'] = '';
116
+ if (opts.activo === false)
117
+ fiqlGroup['MOTIVO_BAJA'] = { op: 'ne', value: '' };
118
+ appendRquery(url, buildFiql(fiqlGroup));
119
+ const qs = url.searchParams.toString();
120
+ const path = qs ? `/part/v1/articulos?${qs}` : '/part/v1/articulos';
121
+ const data = await this.get(path);
122
+ return { items: data.items, total: Number(data.total) };
123
+ }
124
+ /**
125
+ * Detalle de un artículo por `idReg` opaco.
126
+ *
127
+ * Endpoint: GET /part/v1/articulos/{idreg}
128
+ *
129
+ * El API devuelve un envelope de LISTA con un único item (verificado contra
130
+ * el API real), no un objeto detalle; este método lo desenvuelve.
131
+ *
132
+ * @param idReg - Identificador opaco (base64) del artículo.
133
+ * @returns Objeto con todos los campos del artículo.
134
+ */
135
+ async getArticulo(idReg) {
136
+ const data = await this.get(`/part/v1/articulos/${encodeURIComponent(idReg)}`);
137
+ const item = data.items?.[0];
138
+ if (item === undefined) {
139
+ throw new FreematicaError('not_found', `Artículo no encontrado: ${idReg}`);
140
+ }
141
+ return item;
142
+ }
143
+ /**
144
+ * Precios de venta de un artículo por `idReg` opaco.
145
+ *
146
+ * Endpoint: GET /pgrl/v1/precio-articulo/{idreg}
147
+ *
148
+ * Devuelve un objeto con PRECIO_VENTA, DESCUENTO y FACTURABLE.
149
+ *
150
+ * @param idReg - Identificador opaco (base64) del artículo (el mismo que
151
+ * devuelve freematica_list_articulos).
152
+ * @returns Objeto de precios del artículo.
153
+ */
154
+ async getPrecioArticulo(idReg) {
155
+ return this.get(`/pgrl/v1/precio-articulo/${encodeURIComponent(idReg)}`);
156
+ }
35
157
  /**
36
158
  * Obtener un catálogo de datos maestros.
37
159
  */
@@ -164,13 +286,15 @@ export class FreematicaClient extends BaseClient {
164
286
  *
165
287
  * Endpoint: GET /pprl/v1/vigilancia-salud
166
288
  *
167
- * `idRegPersona` se envía como query param nativo. El resto de filtros se
168
- * codifican en FIQL y se envían como `rquery`.
169
- *
170
- * @param opts - Opciones de paginación y filtrado.
289
+ * @param opts - Paginación y filtro opcional idRegPersona.
171
290
  * @returns Lista paginada de registros VS.
172
291
  */
173
292
  async listVigilanciaSalud(opts) {
293
+ // El endpoint IGNORA el parámetro `rquery` por completo: cualquier FIQL
294
+ // (incluso con campos inexistentes) responde 200 con el dataset completo
295
+ // sin filtrar (verificado contra el API real). El único filtro operativo
296
+ // es el query param nativo `idRegPersona` (el idReg de la persona en
297
+ // freematica_list_personal), que sí filtra correctamente.
174
298
  const url = new URL('placeholder://x/pprl/v1/vigilancia-salud');
175
299
  if (opts.items !== undefined)
176
300
  url.searchParams.set('items', String(opts.items));
@@ -178,28 +302,6 @@ export class FreematicaClient extends BaseClient {
178
302
  url.searchParams.set('page', String(opts.page));
179
303
  if (opts.idRegPersona !== undefined)
180
304
  url.searchParams.set('idRegPersona', opts.idRegPersona);
181
- // Filtros FIQL
182
- const fiqlParts = {};
183
- if (opts.empresa !== undefined)
184
- fiqlParts['PERVS_EMP'] = opts.empresa;
185
- if (opts.delegacion !== undefined)
186
- fiqlParts['PERVS_DELEG'] = opts.delegacion;
187
- if (opts.codPersona !== undefined)
188
- fiqlParts['PERVS_PERSO'] = opts.codPersona;
189
- if (opts.tipoRevision !== undefined)
190
- fiqlParts['PERVS_TIPO_REVISION'] = opts.tipoRevision;
191
- if (opts.resultado !== undefined)
192
- fiqlParts['PERVS_RESULTADO'] = opts.resultado;
193
- if (opts.fechaCitaDesde !== undefined)
194
- fiqlParts['PERVS_FCH_CITA'] = { op: 'ge', value: opts.fechaCitaDesde };
195
- // If both desde and hasta, we need to combine with AND
196
- const fiqlParts2 = {};
197
- if (opts.fechaCitaHasta !== undefined)
198
- fiqlParts2['PERVS_FCH_CITA'] = { op: 'le', value: opts.fechaCitaHasta };
199
- const fiql1 = buildFiql(fiqlParts);
200
- const fiql2 = buildFiql(fiqlParts2);
201
- const combinedFiql = [fiql1, fiql2].filter(Boolean).join(';');
202
- appendRquery(url, combinedFiql);
203
305
  const qs = url.searchParams.toString();
204
306
  const path = qs ? `/pprl/v1/vigilancia-salud?${qs}` : '/pprl/v1/vigilancia-salud';
205
307
  const data = await this.get(path);
@@ -264,18 +366,23 @@ export class FreematicaClient extends BaseClient {
264
366
  /**
265
367
  * Lista paginada de personas (empleados) con filtros FIQL opcionales.
266
368
  *
267
- * Endpoint: GET /pers/v2/personal
369
+ * Endpoint: GET /pers/v1/personal
268
370
  *
269
- * El filtro `activo` (boolean) se traduce a `VSSPER_ACTIVO==S` (true) o
270
- * `VSSPER_ACTIVO==N` (false). Las búsquedas por `nombre` y `apellido` usan
271
- * el operador de igualdad estándar FIQL (el servidor aplica LIKE internamente
272
- * para estos campos).
371
+ * Todos los filtros son de coincidencia EXACTA: el endpoint no soporta
372
+ * búsqueda parcial (=lk= responde 400 y los wildcards % / * devuelven 0
373
+ * resultados verificado contra el API real).
273
374
  *
274
375
  * @param opts - Opciones de paginación y filtrado.
275
376
  * @returns Lista paginada de personas.
276
377
  */
277
378
  async listPersonal(opts) {
278
- const url = new URL('placeholder://x/pers/v2/personal');
379
+ // v1, no v2: /pers/v2/personal es un endpoint de sincronización incremental
380
+ // que exige el parámetro nativo `fchmodificacion` (sin él → 400 "Parámetro
381
+ // [fchmodificacion] obligatorio") y devuelve un subconjunto del dataset.
382
+ // /pers/v1/personal devuelve el dataset completo con idReg. Verificado
383
+ // contra el API real (v1: 5482 personas; v2 con fchmodificacion=1900-01-01:
384
+ // 3984). El campo VSSPER_ACTIVO no existe en v1 (filtrarlo → 400).
385
+ const url = new URL('placeholder://x/pers/v1/personal');
279
386
  if (opts.items !== undefined)
280
387
  url.searchParams.set('items', String(opts.items));
281
388
  if (opts.page !== undefined)
@@ -299,24 +406,32 @@ export class FreematicaClient extends BaseClient {
299
406
  fiqlGroup['VSSPER_DPTO'] = opts.departamento;
300
407
  if (opts.seccion !== undefined)
301
408
  fiqlGroup['VSSPER_SECCION'] = opts.seccion;
302
- if (opts.activo !== undefined)
303
- fiqlGroup['VSSPER_ACTIVO'] = opts.activo ? 'S' : 'N';
304
409
  appendRquery(url, buildFiql(fiqlGroup));
305
410
  const qs = url.searchParams.toString();
306
- const path = qs ? `/pers/v2/personal?${qs}` : '/pers/v2/personal';
411
+ const path = qs ? `/pers/v1/personal?${qs}` : '/pers/v1/personal';
307
412
  const data = await this.get(path);
308
413
  return { items: data.items, total: Number(data.total) };
309
414
  }
310
415
  /**
311
- * Detalle de una persona por `idReg` opaco.
416
+ * Detalle de una persona por su `idReg` opaco.
417
+ *
418
+ * Endpoint: GET /pers/v1/personal/{idreg}
312
419
  *
313
- * Endpoint: GET /pers/v2/personal/{idreg}
420
+ * v1, igual que el listado: garantiza que todo idReg devuelto por
421
+ * freematica_list_personal funciona aquí (la vista v2 es un subconjunto
422
+ * de sincronización incremental). El API devuelve un envelope de LISTA
423
+ * con un único item (verificado contra el API real); se desenvuelve.
314
424
  *
315
- * @param idReg - Identificador opaco de la persona.
425
+ * @param idReg - Identificador opaco (base64) de la persona.
316
426
  * @returns Objeto con todos los campos de la persona.
317
427
  */
318
428
  async getPersona(idReg) {
319
- return this.get(`/pers/v2/personal/${encodeURIComponent(idReg)}`);
429
+ const data = await this.get(`/pers/v1/personal/${encodeURIComponent(idReg)}`);
430
+ const item = data.items?.[0];
431
+ if (item === undefined) {
432
+ throw new FreematicaError('not_found', `Persona no encontrada: ${idReg}`);
433
+ }
434
+ return item;
320
435
  }
321
436
  // ---------------------------------------------------------------------------
322
437
  // Calendarios laborales (v0.5.0)
@@ -372,13 +487,19 @@ export class FreematicaClient extends BaseClient {
372
487
  url.searchParams.set('cal', opts.cal);
373
488
  if (opts.periodo !== undefined)
374
489
  url.searchParams.set('periodo', opts.periodo);
375
- // Construir FIQL para filtros adicionales
490
+ // Construir FIQL para filtros adicionales.
491
+ // ASI_FCHASI: `=ge=` funciona; `=le=` responde 400 (se emula con `=lt=`
492
+ // del día siguiente); combinar ambos límites devuelve 0 filas (bug del
493
+ // API verificado en producción) → se rechaza.
494
+ if (opts.fechaDesde !== undefined && opts.fechaHasta !== undefined) {
495
+ throw new Error(RANGO_NO_SOPORTADO);
496
+ }
376
497
  const andGroups = [];
377
498
  if (opts.fechaDesde !== undefined) {
378
499
  andGroups.push({ ASI_FCHASI: { op: 'ge', value: opts.fechaDesde } });
379
500
  }
380
501
  if (opts.fechaHasta !== undefined) {
381
- andGroups.push({ ASI_FCHASI: { op: 'le', value: opts.fechaHasta } });
502
+ andGroups.push({ ASI_FCHASI: { op: 'lt', value: nextDayIso(opts.fechaHasta) } });
382
503
  }
383
504
  if (opts.diario !== undefined) {
384
505
  andGroups.push({ ASI_DIARIO: opts.diario });
@@ -496,12 +617,11 @@ export class FreematicaClient extends BaseClient {
496
617
  * Construye la FIQL a partir de los filtros tipados y la añade como `rquery`
497
618
  * usando `appendRquery()` (patrón canónico del foundation TD-117).
498
619
  *
499
- * El filtro `soloImpagados=true` genera `CARCL_FECIMPAG!=null`, donde el
500
- * valor `'null'` es la convención centinela de Freemática para indicar "sin
501
- * valor" en campos de fecha opcionales (assumption empírica: observado en
502
- * datos reales de la API; documentado para futuras implementaciones).
503
- * El operador `ne` se pasa vía `buildFiql` para garantizar escaping
504
- * consistente con el resto de filtros.
620
+ * El filtro `soloImpagados=true` genera `CARCL_FECIMPAG=ge='1900-01-01'`
621
+ * (cualquier fecha de impago con valor). El FIQL de Freemática no tiene
622
+ * IS NOT NULL: `!=null` devuelve 0 resultados silenciosamente y `!='null'`
623
+ * responde 500 (verificado contra el API real; con este rango devuelve
624
+ * 382 impagados de 72054 documentos).
505
625
  *
506
626
  * El filtro `estado` mapea a los valores numéricos de `CARCL_SITCAR`
507
627
  * (1=pendiente, 2=cancelado, 3=derivado).
@@ -540,20 +660,27 @@ export class FreematicaClient extends BaseClient {
540
660
  const eqFiql = buildFiql(eqGroup);
541
661
  if (eqFiql)
542
662
  parts.push(eqFiql);
543
- // Range filters: each gets its own buildFiql call to preserve both endpoints
663
+ // Range filters. CARCL_FECDOC: `=ge=` funciona; `=le=` responde 500
664
+ // (hasta se emula con `=lt=` del día siguiente); combinar desde+hasta
665
+ // devuelve 0 filas (bug del API verificado en producción) → se rechaza.
666
+ // CARCL_FECVCTO: solo `=ge=` funciona (le/lt se ignoran silenciosamente
667
+ // devolviendo el dataset completo), así que fechaVencimientoHasta no
668
+ // existe como filtro.
669
+ if (opts.fechaDocDesde !== undefined && opts.fechaDocHasta !== undefined) {
670
+ throw new Error(RANGO_NO_SOPORTADO);
671
+ }
544
672
  if (opts.fechaDocDesde !== undefined)
545
673
  parts.push(buildFiql({ CARCL_FECDOC: { op: 'ge', value: opts.fechaDocDesde } }));
546
674
  if (opts.fechaDocHasta !== undefined)
547
- parts.push(buildFiql({ CARCL_FECDOC: { op: 'le', value: opts.fechaDocHasta } }));
675
+ parts.push(buildFiql({ CARCL_FECDOC: { op: 'lt', value: nextDayIso(opts.fechaDocHasta) } }));
548
676
  if (opts.fechaVencimientoDesde !== undefined)
549
677
  parts.push(buildFiql({ CARCL_FECVCTO: { op: 'ge', value: opts.fechaVencimientoDesde } }));
550
- if (opts.fechaVencimientoHasta !== undefined)
551
- parts.push(buildFiql({ CARCL_FECVCTO: { op: 'le', value: opts.fechaVencimientoHasta } }));
552
- // soloImpagados: 'null' es el centinela de Freemática para fecha sin valor.
553
- // Se pasa por buildFiql (op 'ne') igual que el resto de filtros para
554
- // garantizar escaping uniforme y legibilidad del código.
678
+ // soloImpagados: el FIQL de Freemática no soporta IS NOT NULL (`!=null`
679
+ // devuelve 0 resultados y `!='null'` responde 500 verificado contra el
680
+ // API real). El rango de fecha equivale: cualquier CARCL_FECIMPAG con
681
+ // valor es >= 1900-01-01.
555
682
  if (opts.soloImpagados === true) {
556
- parts.push(buildFiql({ CARCL_FECIMPAG: { op: 'ne', value: 'null' } }));
683
+ parts.push(buildFiql({ CARCL_FECIMPAG: { op: 'ge', value: '1900-01-01' } }));
557
684
  }
558
685
  appendRquery(url, parts.join(';'));
559
686
  const path = url.pathname + (url.search ? url.search : '');
@@ -692,13 +819,16 @@ export class FreematicaClient extends BaseClient {
692
819
  /**
693
820
  * Lista paginada de proveedores con filtros FIQL.
694
821
  *
695
- * El filtro `activo` mapea a FECHA_BAJA:
696
- * - `true` → FECHA_BAJA es nula (proveedor activo). Se usa el operador `=is-null=true` si el
697
- * API lo soporta; como fallback se omite el filtro y se filtra post-process.
698
- * En Freemática el convenio estándar es: activo=true → `FECHA_BAJA==null` (FIQL `==null`).
699
- * - `false` FECHA_BAJA tiene valor (proveedor de baja).
822
+ * El filtro `activo` mapea a FECHA_BAJA. El API no soporta IS NULL en FIQL
823
+ * (`==null` devuelve 0 resultados y `=='null'` responde 500 verificado
824
+ * contra el API real), así que:
825
+ * - `false` → `FECHA_BAJA=ge='1900-01-01'` (cualquier fecha de baja).
826
+ * - `true` sin filtro server-side; las bajas se descartan post-process
827
+ * sobre la página recibida (FECHA_BAJA con valor). El `total`
828
+ * devuelto es el del dataset sin filtrar.
700
829
  *
701
- * El filtro `nombre` usa operador FIQL `=lk=` para búsqueda parcial (LIKE).
830
+ * El filtro `nombre` es coincidencia EXACTA: el operador `=lk=` responde
831
+ * 400 en este endpoint y los wildcards % devuelven 0 resultados.
702
832
  *
703
833
  * Endpoint: GET /pgrl/v2/proveedores
704
834
  */
@@ -718,21 +848,25 @@ export class FreematicaClient extends BaseClient {
718
848
  COD_PAIS: opts.codPais,
719
849
  };
720
850
  if (opts.nombre !== undefined) {
721
- filters['NOMBRE_PRO'] = { op: 'lk', value: opts.nombre };
722
- }
723
- if (opts.activo === true) {
724
- // Activos: FECHA_BAJA nula. FIQL: FECHA_BAJA==null
725
- filters['FECHA_BAJA'] = 'null';
851
+ filters['NOMBRE_PRO'] = opts.nombre;
726
852
  }
727
- else if (opts.activo === false) {
728
- // Dados de baja: FECHA_BAJA tiene valor (not null). FIQL: FECHA_BAJA!=null
729
- filters['FECHA_BAJA'] = { op: 'ne', value: 'null' };
853
+ if (opts.activo === false) {
854
+ // Dados de baja: cualquier FECHA_BAJA con valor. IS NOT NULL no existe
855
+ // en el FIQL de Freemática; el rango de fecha equivale.
856
+ filters['FECHA_BAJA'] = { op: 'ge', value: '1900-01-01' };
730
857
  }
731
858
  const fiql = buildFiql(filters);
732
859
  appendRquery(url, fiql);
733
860
  const path = url.pathname + (url.search ? url.search : '');
734
861
  const data = await this.get(path);
735
- return { items: data.items, total: Number(data.total) };
862
+ // activo=true: descartar bajas de la página (no expresable en FIQL).
863
+ const items = opts.activo === true
864
+ ? data.items.filter((p) => {
865
+ const fb = p['FECHA_BAJA'];
866
+ return fb === undefined || fb === null || String(fb).trim() === '';
867
+ })
868
+ : data.items;
869
+ return { items, total: Number(data.total) };
736
870
  }
737
871
  // ---------------------------------------------------------------------------
738
872
  // Facturas de ventas (v0.5.0)
@@ -752,33 +886,43 @@ export class FreematicaClient extends BaseClient {
752
886
  if (opts.page !== undefined)
753
887
  url.searchParams.set('page', String(opts.page));
754
888
  const fiqlParts = [];
755
- // Standard equality filters
889
+ // Standard equality filters. Nombres de columna verificados contra el API
890
+ // real: la vista de facturas-cabecera expone FVC_CODEMP, FVC_CODCLI,
891
+ // FVC_CODREPRES, FVC_SERIEFRA, FVC_NUMFRA, FVC_FPAGO, FVC_FCHFAC y
892
+ // FVC_TRASP_CONTAB ('1'/'0'). Los nombres anteriores (FVC_EMP, FVC_CODAUX,
893
+ // FVC_CODREP, FVC_SERFAC, FVC_NUMFAC, FVC_CODFPAG, FVC_FECFAC,
894
+ // FVC_TRSCONT) no existen y provocaban 500 en cada consulta con filtro.
756
895
  const eqGroup = {};
757
896
  if (opts.empresa !== undefined)
758
- eqGroup['FVC_EMP'] = opts.empresa;
897
+ eqGroup['FVC_CODEMP'] = opts.empresa;
759
898
  if (opts.codCliente !== undefined)
760
- eqGroup['FVC_CODAUX'] = opts.codCliente;
899
+ eqGroup['FVC_CODCLI'] = opts.codCliente;
761
900
  if (opts.representante !== undefined)
762
- eqGroup['FVC_CODREP'] = opts.representante;
901
+ eqGroup['FVC_CODREPRES'] = opts.representante;
763
902
  if (opts.serie !== undefined)
764
- eqGroup['FVC_SERFAC'] = opts.serie;
903
+ eqGroup['FVC_SERIEFRA'] = opts.serie;
765
904
  if (opts.numFactura !== undefined)
766
- eqGroup['FVC_NUMFAC'] = opts.numFactura;
905
+ eqGroup['FVC_NUMFRA'] = opts.numFactura;
767
906
  if (opts.formaPago !== undefined)
768
- eqGroup['FVC_CODFPAG'] = opts.formaPago;
907
+ eqGroup['FVC_FPAGO'] = opts.formaPago;
769
908
  if (opts.delegacion !== undefined)
770
909
  eqGroup['FVC_DELEG'] = opts.delegacion;
771
910
  const eqFiql = buildFiql(eqGroup);
772
911
  if (eqFiql)
773
912
  fiqlParts.push(eqFiql);
774
- // Date range filters
913
+ // Date range filters. FVC_FCHFAC: `=ge=` funciona; `=le=`/`=gt=` responden
914
+ // 500 (hasta se emula con `=lt=` del día siguiente); combinar desde+hasta
915
+ // devuelve 0 filas (bug del API verificado en producción) → se rechaza.
916
+ if (opts.fechaFacturaDesde !== undefined && opts.fechaFacturaHasta !== undefined) {
917
+ throw new Error(RANGO_NO_SOPORTADO);
918
+ }
775
919
  if (opts.fechaFacturaDesde !== undefined)
776
- fiqlParts.push(buildFiql({ FVC_FECFAC: { op: 'ge', value: opts.fechaFacturaDesde } }));
920
+ fiqlParts.push(buildFiql({ FVC_FCHFAC: { op: 'ge', value: opts.fechaFacturaDesde } }));
777
921
  if (opts.fechaFacturaHasta !== undefined)
778
- fiqlParts.push(buildFiql({ FVC_FECFAC: { op: 'le', value: opts.fechaFacturaHasta } }));
779
- // Boolean: traspasadoContabilidad
922
+ fiqlParts.push(buildFiql({ FVC_FCHFAC: { op: 'lt', value: nextDayIso(opts.fechaFacturaHasta) } }));
923
+ // Boolean: traspasadoContabilidad — la columna real usa '1'/'0'
780
924
  if (opts.traspasadoContabilidad !== undefined) {
781
- fiqlParts.push(buildFiql({ FVC_TRSCONT: opts.traspasadoContabilidad ? 'S' : 'N' }));
925
+ fiqlParts.push(buildFiql({ FVC_TRASP_CONTAB: opts.traspasadoContabilidad ? '1' : '0' }));
782
926
  }
783
927
  appendRquery(url, fiqlParts.join(';'));
784
928
  const path = url.pathname + (url.search ? url.search : '');
@@ -828,7 +972,11 @@ export class FreematicaClient extends BaseClient {
828
972
  * Lista paginada de localizaciones de servicio de clientes.
829
973
  *
830
974
  * Filtros soportados: codCliente (COD_CLI), grupoCliente (GRUPO_CLI), codPais (COD_PAIS),
831
- * codProvincia (COD_PROVINCIA), representante (COD_REPRES), activo (FECHA_BAJA nulo/no nulo).
975
+ * codProvincia (COD_PROVINCIA), representante (COD_REPRES).
976
+ *
977
+ * La vista NO tiene columna FECHA_BAJA (filtrar por ella responde 400
978
+ * "Error en rquery" — verificado contra el API real), así que no existe
979
+ * filtro activo/baja en este recurso.
832
980
  *
833
981
  * Endpoint: GET /pgrl/v2/localizaciones-servicio-clientes
834
982
  */
@@ -840,12 +988,6 @@ export class FreematicaClient extends BaseClient {
840
988
  COD_PROVINCIA: opts.codProvincia,
841
989
  COD_REPRES: opts.representante,
842
990
  };
843
- if (opts.activo === true) {
844
- fiqlFilters['FECHA_BAJA'] = 'null';
845
- }
846
- else if (opts.activo === false) {
847
- fiqlFilters['FECHA_BAJA'] = { op: 'ne', value: 'null' };
848
- }
849
991
  return this.listResourceWithFiql('/pgrl/v2/localizaciones-servicio-clientes', opts, fiqlFilters);
850
992
  }
851
993
  /**
@@ -874,13 +1016,15 @@ export class FreematicaClient extends BaseClient {
874
1016
  url.searchParams.set('items', String(opts.items));
875
1017
  if (opts.page !== undefined)
876
1018
  url.searchParams.set('page', String(opts.page));
1019
+ // Nombres de columna verificados contra el API real (las líneas exponen
1020
+ // FVL_CODARTIC, FVL_COD_FAMILIA, FVL_COD_SUBFAM y FVL_DELEG).
877
1021
  const fiqlGroup = {};
878
1022
  if (opts.codArticulo !== undefined)
879
- fiqlGroup['FVL_CODART'] = opts.codArticulo;
1023
+ fiqlGroup['FVL_CODARTIC'] = opts.codArticulo;
880
1024
  if (opts.codFamilia !== undefined)
881
- fiqlGroup['FVL_CODFAM'] = opts.codFamilia;
1025
+ fiqlGroup['FVL_COD_FAMILIA'] = opts.codFamilia;
882
1026
  if (opts.codSubfamilia !== undefined)
883
- fiqlGroup['FVL_CODSFAM'] = opts.codSubfamilia;
1027
+ fiqlGroup['FVL_COD_SUBFAM'] = opts.codSubfamilia;
884
1028
  if (opts.delegacion !== undefined)
885
1029
  fiqlGroup['FVL_DELEG'] = opts.delegacion;
886
1030
  appendRquery(url, buildFiql(fiqlGroup));
@@ -903,9 +1047,10 @@ export class FreematicaClient extends BaseClient {
903
1047
  url.searchParams.set('items', String(opts.items));
904
1048
  if (opts.page !== undefined)
905
1049
  url.searchParams.set('page', String(opts.page));
1050
+ // Columna real: FVI_TIPO_IVA (verificado contra el API; FVI_TIPIVA no existe).
906
1051
  const fiqlGroup = {};
907
1052
  if (opts.tipoIva !== undefined)
908
- fiqlGroup['FVI_TIPIVA'] = opts.tipoIva;
1053
+ fiqlGroup['FVI_TIPO_IVA'] = opts.tipoIva;
909
1054
  appendRquery(url, buildFiql(fiqlGroup));
910
1055
  const path = url.pathname + (url.search ? url.search : '');
911
1056
  const data = await this.get(path);
@@ -926,13 +1071,18 @@ export class FreematicaClient extends BaseClient {
926
1071
  url.searchParams.set('items', String(opts.items));
927
1072
  if (opts.page !== undefined)
928
1073
  url.searchParams.set('page', String(opts.page));
1074
+ // Columnas reales: FVV_MODOPAGO y FVV_FCH_VTO (verificado contra el API;
1075
+ // FVV_CODMPAG y FVV_FECVCTO no existen). A diferencia de FVC_FCHFAC en la
1076
+ // cabecera, en este sub-recurso `=le=` responde 200 y ge+le combinados
1077
+ // devuelven el registro esperado (verificado en producción con una factura
1078
+ // de 1 vencimiento), así que se mantienen el =le= y el rango.
929
1079
  const fiqlParts = [];
930
1080
  if (opts.modoPago !== undefined)
931
- fiqlParts.push(buildFiql({ FVV_CODMPAG: opts.modoPago }));
1081
+ fiqlParts.push(buildFiql({ FVV_MODOPAGO: opts.modoPago }));
932
1082
  if (opts.fechaVencimientoDesde !== undefined)
933
- fiqlParts.push(buildFiql({ FVV_FECVCTO: { op: 'ge', value: opts.fechaVencimientoDesde } }));
1083
+ fiqlParts.push(buildFiql({ FVV_FCH_VTO: { op: 'ge', value: opts.fechaVencimientoDesde } }));
934
1084
  if (opts.fechaVencimientoHasta !== undefined)
935
- fiqlParts.push(buildFiql({ FVV_FECVCTO: { op: 'le', value: opts.fechaVencimientoHasta } }));
1085
+ fiqlParts.push(buildFiql({ FVV_FCH_VTO: { op: 'le', value: opts.fechaVencimientoHasta } }));
936
1086
  appendRquery(url, fiqlParts.join(';'));
937
1087
  const path = url.pathname + (url.search ? url.search : '');
938
1088
  const data = await this.get(path);
@@ -1596,10 +1746,13 @@ export class FreematicaClient extends BaseClient {
1596
1746
  /**
1597
1747
  * Lista paginada de localizaciones de factura de clientes.
1598
1748
  *
1599
- * Endpoint: GET /pgrl/v2/localizaciones-factura-clientes
1749
+ * Endpoint: GET /pgrl/v1/localizaciones-factura-clientes
1750
+ * (la lista solo existe en v1; el v2 de este recurso es solo POST/PUT).
1600
1751
  */
1601
1752
  async listLocalizacionesFacturaClientes(opts = {}) {
1602
- return this.listResourceWithFiql('/pgrl/v2/localizaciones-factura-clientes', opts, {
1753
+ // v1, no v2: la LISTA solo existe en /pgrl/v1 (v2 responde 404 — solo
1754
+ // tiene POST/PUT). Verificado contra el API real y el Postman.
1755
+ return this.listResourceWithFiql('/pgrl/v1/localizaciones-factura-clientes', opts, {
1603
1756
  COD_CLI: opts.codCliente,
1604
1757
  COD_GRUPO_CLI: opts.grupoCliente,
1605
1758
  });