Usar PDN1.OpenInvQty en la consulta FIFO y ajustar el despliegue

- DP_EntradasAbiertas (v3) lee OpenInvQty en lugar de OpenQty.
- publicar: la release no contiene enlaces simbólicos (copia real de @dp/shared).
- DEPLOY.md y CLAUDE.md actualizados.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
georgeandClaude Sonnet 5.5 committed 2026-10-13 14:30:06 -05:00
1 parent 699dd983d0
commit 2873fd3bc6
6 files changed
+40 -10

No files matched your search

+6 -3
View File
@@ -52,6 +52,8 @@ Pasos de la UI (decididos):
crea/actualiza `DP_Stock` y `DP_EntradasAbiertas` sola (variante según `DB_TYPE`). El analizador crea/actualiza `DP_Stock` y `DP_EntradasAbiertas` sola (variante según `DB_TYPE`). El analizador
SQL del Service Layer **no admite `FORMAT`** y SAP reescribe el `SqlText`: al cambiar el SQL hay SQL del Service Layer **no admite `FORMAT`** y SAP reescribe el `SqlText`: al cambiar el SQL hay
que subir `version` en `sql-queries.definitions.ts`. Los parámetros van entre comillas simples. que subir `version` en `sql-queries.definitions.ts`. Los parámetros van entre comillas simples.
`DP_EntradasAbiertas` (v3) lee la cantidad abierta de `PDN1.OpenInvQty` (no `OpenQty`); el campo
de usuario `U_OpenQty` del UDO conserva su nombre.
- **Listado** de planificaciones **paginado en SAP** (`$top/$skip/$inlinecount`, OData v3 del SL v1), - **Listado** de planificaciones **paginado en SAP** (`$top/$skip/$inlinecount`, OData v3 del SL v1),
más nueva primero, con filtros por estado, almacén y rango de fechas. Por defecto 20 por página, más nueva primero, con filtros por estado, almacén y rango de fechas. Por defecto 20 por página,
máximo 100. En la pantalla: botones **Atrás** / **Siguiente**; filtros y página quedan en la URL. máximo 100. En la pantalla: botones **Atrás** / **Siguiente**; filtros y página quedan en la URL.
@@ -69,7 +71,8 @@ Pasos de la UI (decididos):
`HTTPS_KEY_PATH`+`HTTPS_CERT_PATH`) y redirección opcional de HTTP (`HTTP_REDIRECT_PORT`). El servicio `HTTPS_KEY_PATH`+`HTTPS_CERT_PATH`) y redirección opcional de HTTP (`HTTP_REDIRECT_PORT`). El servicio
lo administra **NSSM** (decisión del usuario; se evaluó WinSW y no se lo consideró lo bastante estable); lo administra **NSSM** (decisión del usuario; se evaluó WinSW y no se lo consideró lo bastante estable);
nginx no es necesario. NSSM 2.24 estable puede no arrancar en Windows recientes: usar la 2.24-101. `npm run publicar` arma `release/devolucion-proveedores/` (API + web + solo dependencias de nginx no es necesario. NSSM 2.24 estable puede no arrancar en Windows recientes: usar la 2.24-101. `npm run publicar` arma `release/devolucion-proveedores/` (API + web + solo dependencias de
producción, ~55 MB). Guía completa: `deploy/windows/DEPLOY.md`. producción, ~55 MB, **sin enlaces simbólicos**: `@dp/shared` va como copia real, porque la API lo
importa al ejecutarse y un enlace no sobrevive a copiar de Mac a Windows). Guía completa: `deploy/windows/DEPLOY.md`.
- **Logs**: la aplicación **no escribe logs a archivo**; usa el `Logger` de Nest por consola. En - **Logs**: la aplicación **no escribe logs a archivo**; usa el `Logger` de Nest por consola. En
producción NSSM captura la salida y la rota a los 10 MB (no limita cuántos archivos conserva: hay que producción NSSM captura la salida y la rota a los 10 MB (no limita cuántos archivos conserva: hay que
limpiar los viejos). Con `NO_COLOR=1` el log sale sin códigos de limpiar los viejos). Con `NO_COLOR=1` el log sale sin códigos de
@@ -248,8 +251,8 @@ Reglas:
- **HANA**: el addon .NET ya funcionaba en HANA, y las consultas son un port de las suyas, así que - **HANA**: el addon .NET ya funcionaba en HANA, y las consultas son un port de las suyas, así que
su lógica está probada. Lo que no se ha probado es **cómo las recibe el Service Layer de HANA** su lógica está probada. Lo que no se ha probado es **cómo las recibe el Service Layer de HANA**
(`SQLQueries`: su analizador es más estricto que el `DoQuery` del addon; en MSSQL rechazó `FORMAT`) y los (`SQLQueries`: su analizador es más estricto que el `DoQuery` del addon; en MSSQL rechazó `FORMAT`) y los
dos cambios respecto al addon: el filtro por almacén (`PDN1.WhsCode`) y los parámetros con nombre tres cambios respecto al addon: el filtro por almacén (`PDN1.WhsCode`), la columna `PDN1.OpenInvQty`
(`:item`, `:whs`). Tampoco el paginado (`$inlinecount`/`odata.count` es de `/b1s/v1`; en `/b1s/v2` sería y los parámetros con nombre (`:item`, `:whs`). Tampoco el paginado (`$inlinecount`/`odata.count` es de `/b1s/v1`; en `/b1s/v2` sería
`$count`) ni los tipos que HANA informa en los metadatos. `$count`) ni los tipos que HANA informa en los metadatos.
- **Reprocesar** solo con tests (no contra SAP). - **Reprocesar** solo con tests (no contra SAP).
- Fallo parcial al crear Devoluciones (solo se probó el caso feliz). El corte en lotes de - Fallo parcial al crear Devoluciones (solo se probó el caso feliz). El corte en lotes de
@@ -42,7 +42,7 @@ function crear(anterior: ReturnType<typeof udo>, opciones: { stock?: unknown[];
const sql = { const sql = {
consultar: vi.fn(async (_s: unknown, consulta: { codigo: string }) => { consultar: vi.fn(async (_s: unknown, consulta: { codigo: string }) => {
if (consulta.codigo === 'DP_Stock') return opciones.stock ?? [{ ItemCode: 'A1', ItemName: 'Libro', OnHand: 100 }]; if (consulta.codigo === 'DP_Stock') return opciones.stock ?? [{ ItemCode: 'A1', ItemName: 'Libro', OnHand: 100 }];
return [{ DocNum: 101, CardCode: 'P1', CardName: 'Prov', DocDate: '2026-01-31T00:00:00Z', DocEntry: 15, LineNum: 0, OpenQty: 10 }]; return [{ DocNum: 101, CardCode: 'P1', CardName: 'Prov', DocDate: '2026-01-31T00:00:00Z', DocEntry: 15, LineNum: 0, OpenInvQty: 10 }];
}), }),
}; };
const catalogos = { series: vi.fn(async () => [{ series: 15, name: 'Primario', nextNumber: 1, esPorDefecto: true }]) }; const catalogos = { series: vi.fn(async () => [{ series: 15, name: 'Primario', nextNumber: 1, esPorDefecto: true }]) };
@@ -48,7 +48,7 @@ interface FilaEntrada {
DocDate: string; DocDate: string;
DocEntry: number; DocEntry: number;
LineNum: number; LineNum: number;
OpenQty: number | string; OpenInvQty: number | string;
} }
interface DocumentoCreado { interface DocumentoCreado {
@@ -242,7 +242,7 @@ export class PlanificacionesService {
docDate: fechaIso(String(f.DocDate)), docDate: fechaIso(String(f.DocDate)),
cardCode: f.CardCode, cardCode: f.CardCode,
cardName: f.CardName, cardName: f.CardName,
openQty: Math.floor(Number(f.OpenQty)), openQty: Math.floor(Number(f.OpenInvQty)),
})), })),
); );
}, },
@@ -30,17 +30,17 @@ export const CONSULTA_STOCK: ConsultaGuardada = {
export const CONSULTA_ENTRADAS_ABIERTAS: ConsultaGuardada = { export const CONSULTA_ENTRADAS_ABIERTAS: ConsultaGuardada = {
codigo: 'DP_EntradasAbiertas', codigo: 'DP_EntradasAbiertas',
version: 2, version: 3,
mssql: mssql:
'SELECT T0.[DocNum], T0.[CardCode], T0.[CardName], T0.[DocDate], ' + 'SELECT T0.[DocNum], T0.[CardCode], T0.[CardName], T0.[DocDate], ' +
'T1.[DocEntry], T1.[LineNum], T1.[OpenQty] ' + 'T1.[DocEntry], T1.[LineNum], T1.[OpenInvQty] ' +
'FROM OPDN T0 INNER JOIN PDN1 T1 ON T1.[DocEntry] = T0.[DocEntry] ' + 'FROM OPDN T0 INNER JOIN PDN1 T1 ON T1.[DocEntry] = T0.[DocEntry] ' +
"WHERE T0.[U_DOT_TipoRecepcion] = 'C' AND T1.[LineStatus] = 'O' " + "WHERE T0.[U_DOT_TipoRecepcion] = 'C' AND T1.[LineStatus] = 'O' " +
'AND T1.[ItemCode] = :item AND T1.[WhsCode] = :whs ' + 'AND T1.[ItemCode] = :item AND T1.[WhsCode] = :whs ' +
'ORDER BY T0.[DocDate], T1.[DocEntry], T1.[LineNum]', 'ORDER BY T0.[DocDate], T1.[DocEntry], T1.[LineNum]',
hana: hana:
'SELECT T0."DocNum", T0."CardCode", T0."CardName", T0."DocDate", ' + 'SELECT T0."DocNum", T0."CardCode", T0."CardName", T0."DocDate", ' +
'T1."DocEntry", T1."LineNum", T1."OpenQty" ' + 'T1."DocEntry", T1."LineNum", T1."OpenInvQty" ' +
'FROM OPDN T0 INNER JOIN PDN1 T1 ON T1."DocEntry" = T0."DocEntry" ' + 'FROM OPDN T0 INNER JOIN PDN1 T1 ON T1."DocEntry" = T0."DocEntry" ' +
"WHERE T0.\"U_DOT_TipoRecepcion\" = 'C' AND T1.\"LineStatus\" = 'O' " + "WHERE T0.\"U_DOT_TipoRecepcion\" = 'C' AND T1.\"LineStatus\" = 'O' " +
'AND T1."ItemCode" = :item AND T1."WhsCode" = :whs ' + 'AND T1."ItemCode" = :item AND T1."WhsCode" = :whs ' +
+5 -1
View File
@@ -52,7 +52,11 @@ ejecutar en Windows.
## 3. Copiar al servidor ## 3. Copiar al servidor
Copie toda la carpeta `devolucion-proveedores` a, por ejemplo, `C:\apps\devolucion-proveedores`: Copie **solo** la carpeta `release\devolucion-proveedores` (no el resto del proyecto) a, por ejemplo,
`C:\apps\devolucion-proveedores`. No contiene enlaces simbólicos, así que se puede copiar por red, USB o
comprimida en un `.zip` (más rápido: son miles de archivos pequeños; en el servidor, `Expand-Archive`).
`nssm.exe` **no** viene en la carpeta: se descarga aparte y se indica con `-Nssm`.
``` ```
C:\apps\devolucion-proveedores\ C:\apps\devolucion-proveedores\
+23
View File
@@ -64,6 +64,29 @@ copiar('apps/web/dist/web/browser');
ejecutar(npm, ['ci', '--omit=dev', '--workspace=@dp/shared', '--workspace=api', '--no-audit', '--no-fund'], app); ejecutar(npm, ['ci', '--omit=dev', '--workspace=@dp/shared', '--workspace=api', '--no-audit', '--no-fund'], app);
// npm deja enlaces simbólicos (el de cada workspace y los atajos de .bin) que no sobreviven al copiar la
// carpeta de un Mac/Linux a Windows. La API importa @dp/shared al ejecutarse, así que ese enlace se
// reemplaza por una copia real; los demás no se usan y se quitan.
const nodeModules = join(app, 'node_modules');
rmSync(join(nodeModules, 'api'), { recursive: true, force: true });
const compartido = join(nodeModules, '@dp', 'shared');
rmSync(compartido, { recursive: true, force: true }); // quita solo el enlace, no su destino
mkdirSync(compartido, { recursive: true });
cpSync(join(app, 'libs', 'shared', 'package.json'), join(compartido, 'package.json'));
cpSync(join(app, 'libs', 'shared', 'dist'), join(compartido, 'dist'), { recursive: true });
rmSync(join(app, 'libs'), { recursive: true, force: true });
for (const e of readdirSync(nodeModules, { withFileTypes: true, recursive: true })) {
if (e.name === '.bin') rmSync(join(e.parentPath, e.name), { recursive: true, force: true });
}
const enlaces = readdirSync(app, { withFileTypes: true, recursive: true })
.filter((e) => e.isSymbolicLink())
.map((e) => join(e.parentPath, e.name));
if (enlaces.length > 0) {
console.error(`\nQuedaron enlaces simbólicos en el paquete (no sobreviven en Windows):\n ${enlaces.join('\n ')}`);
process.exit(1);
}
// Archivos de despliegue para Windows junto a la carpeta app/ // Archivos de despliegue para Windows junto a la carpeta app/
for (const archivo of readdirSync(join(raiz, 'deploy', 'windows'))) { for (const archivo of readdirSync(join(raiz, 'deploy', 'windows'))) {
cpSync(join(raiz, 'deploy', 'windows', archivo), join(destino, archivo)); cpSync(join(raiz, 'deploy', 'windows', archivo), join(destino, archivo));