Versión 1.1.0: exportar a Excel, valores por defecto de la cabecera y versión visible

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
georgeandClaude Sonnet 5.5 committed 2026-10-15 16:03:00 -05:00
1 parent 699dd983d0
commit b6e34276ce
35 files changed
+915 -50

No files matched your search

+3 -1
View File
@@ -5,12 +5,13 @@ import type { Request, Response } from 'express';
import { AppConfig } from '../config/configuration.js';
import { ServiceLayerClient } from '../service-layer/service-layer.client.js';
import { SessionStore } from '../service-layer/session.store.js';
import { VERSION_APP } from '../version.js';
import type { SapSession } from '../service-layer/session.store.js';
import { LoginDto } from './dto/login.dto.js';
import { CurrentSession, Public, SESSION_COOKIE } from './session.guard.js';
function aUsuario(sesion: SapSession): UsuarioSesion {
return { userName: sesion.userName, companyDb: sesion.companyDb, version: sesion.version };
return { userName: sesion.userName, companyDb: sesion.companyDb, version: sesion.version, app: VERSION_APP };
}
@Controller('auth')
@@ -27,6 +28,7 @@ export class AuthController {
return {
companyDb: this.config.get('serviceLayer', { infer: true }).companyDb,
dbType: this.config.get('dbType', { infer: true }),
app: VERSION_APP,
};
}
+3
View File
@@ -5,6 +5,7 @@ import cookieParser from 'cookie-parser';
import { AppModule } from './app.module.js';
import configuration from './config/configuration.js';
import { HttpExceptionLogger } from './http-exception.logger.js';
import { VERSION_APP } from './version.js';
import { cargarHttps, crearRedireccionHttp } from './https.js';
import { cabecerasSeguridad, servirWeb } from './web-estatico.js';
@@ -28,6 +29,8 @@ async function bootstrap() {
await app.listen(cfg.port);
const esquema = cfg.https ? 'https' : 'http';
const v = VERSION_APP;
logger.log(`Versión ${v.version} · ${v.publicado ? `publicada ${v.publicado}${v.commit ? ` · commit ${v.commit}` : ''}` : 'desarrollo'}`);
logger.log(`Escuchando en ${esquema}://0.0.0.0:${cfg.port} · SAP ${cfg.serviceLayer.companyDb} (${cfg.dbType})`);
logger.log(cfg.webDistDir ? `Sirviendo la web desde ${cfg.webDistDir}` : 'No se sirve la web (no hay build de Angular): use ng serve.');
if (cfg.cookieSecure && !cfg.https) {
@@ -42,7 +42,7 @@ function crear(anterior: ReturnType<typeof udo>, opciones: { stock?: unknown[];
const sql = {
consultar: vi.fn(async (_s: unknown, consulta: { codigo: string }) => {
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 }]) };
@@ -48,7 +48,7 @@ interface FilaEntrada {
DocDate: string;
DocEntry: number;
LineNum: number;
OpenQty: number | string;
OpenInvQty: number | string;
}
interface DocumentoCreado {
@@ -242,7 +242,7 @@ export class PlanificacionesService {
docDate: fechaIso(String(f.DocDate)),
cardCode: f.CardCode,
cardName: f.CardName,
openQty: Math.floor(Number(f.OpenQty)),
openQty: Math.floor(Number(f.OpenInvQty)),
})),
);
},
@@ -28,19 +28,23 @@ export const CONSULTA_STOCK: ConsultaGuardada = {
'WHERE T1."WhsCode" = :whs AND T0."ItemCode" = :item',
};
/**
* OpenInvQty, no OpenQty: OpenQty solo baja con las Devoluciones, mientras que OpenInvQty también
* descuenta lo facturado, y SAP rechaza (-10) devolver contra la Entrada más que eso.
*/
export const CONSULTA_ENTRADAS_ABIERTAS: ConsultaGuardada = {
codigo: 'DP_EntradasAbiertas',
version: 2,
version: 3,
mssql:
'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] ' +
"WHERE T0.[U_DOT_TipoRecepcion] = 'C' AND T1.[LineStatus] = 'O' " +
'AND T1.[ItemCode] = :item AND T1.[WhsCode] = :whs ' +
'ORDER BY T0.[DocDate], T1.[DocEntry], T1.[LineNum]',
hana:
'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" ' +
"WHERE T0.\"U_DOT_TipoRecepcion\" = 'C' AND T1.\"LineStatus\" = 'O' " +
'AND T1."ItemCode" = :item AND T1."WhsCode" = :whs ' +
+33
View File
@@ -0,0 +1,33 @@
import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import { leerVersion } from './version.js';
describe('leerVersion', () => {
let raiz: string;
let dist: string;
beforeEach(() => {
// Igual que el release: app/package.json (raíz) y la API en app/apps/api/dist, con su propio package.json.
raiz = mkdtempSync(join(tmpdir(), 'dp-version-'));
dist = join(raiz, 'apps', 'api', 'dist');
mkdirSync(dist, { recursive: true });
writeFileSync(join(raiz, 'package.json'), JSON.stringify({ name: 'devolucion-proveedores-web', version: '1.2.3' }));
writeFileSync(join(raiz, 'apps', 'api', 'package.json'), JSON.stringify({ name: 'api', version: '0.0.1' }));
});
afterEach(() => rmSync(raiz, { recursive: true, force: true }));
it('en desarrollo: el número del package.json raíz, sin fecha de publicación', () => {
expect(leerVersion(dist)).toEqual({ version: '1.2.3', publicado: null, commit: null });
});
it('en un release: agrega la fecha y el commit de version.json', () => {
writeFileSync(join(raiz, 'version.json'), JSON.stringify({ version: '1.2.3', publicado: '2026-10-15T20:00:00.000Z', commit: 'abc1234' }));
expect(leerVersion(dist)).toEqual({ version: '1.2.3', publicado: '2026-10-15T20:00:00.000Z', commit: 'abc1234' });
});
it('el repositorio real tiene un número de versión', () => {
expect(leerVersion().version).toMatch(/^\d+\.\d+\.\d+/);
});
});
+41
View File
@@ -0,0 +1,41 @@
import { existsSync, readFileSync } from 'node:fs';
import { dirname, join } from 'node:path';
import { fileURLToPath } from 'node:url';
import type { VersionApp } from '@dp/shared';
/** Nombre del `package.json` raíz del proyecto; está igual en el repositorio y en el release (`app/`). */
const NOMBRE_RAIZ = 'devolucion-proveedores-web';
/** Lo que `npm run publicar` deja en `app/version.json`, junto al `package.json` raíz. */
const ARCHIVO_PUBLICACION = 'version.json';
function leerJson(ruta: string): Record<string, unknown> | null {
try {
return JSON.parse(readFileSync(ruta, 'utf8')) as Record<string, unknown>;
} catch {
return null;
}
}
/**
* Busca hacia arriba desde `desde` la carpeta raíz del proyecto y arma la versión: el número sale del
* `package.json` raíz; la fecha y el commit, del `version.json` del release (en desarrollo no existe).
*/
export function leerVersion(desde = dirname(fileURLToPath(import.meta.url))): VersionApp {
for (let carpeta = desde; ; carpeta = dirname(carpeta)) {
const paquete = leerJson(join(carpeta, 'package.json'));
if (paquete?.name === NOMBRE_RAIZ) {
const publicacion = existsSync(join(carpeta, ARCHIVO_PUBLICACION)) ? leerJson(join(carpeta, ARCHIVO_PUBLICACION)) : null;
const texto = (v: unknown) => (typeof v === 'string' && v ? v : null);
return {
version: texto(paquete.version) ?? '0.0.0',
publicado: texto(publicacion?.publicado),
commit: texto(publicacion?.commit),
};
}
if (dirname(carpeta) === carpeta) return { version: '0.0.0', publicado: null, commit: null };
}
}
/** Se lee una vez al arrancar: cambiar de versión implica reiniciar el servicio. */
export const VERSION_APP: VersionApp = leerVersion();
+7 -1
View File
@@ -30,7 +30,13 @@ describe('API (e2e)', () => {
});
it('/api/auth/info devuelve la sociedad configurada', () => {
return request(app.getHttpServer()).get('/api/auth/info').expect(200).expect({ companyDb: 'TEST', dbType: 'MSSQL' });
return request(app.getHttpServer())
.get('/api/auth/info')
.expect(200)
.expect((res) => {
expect(res.body).toMatchObject({ companyDb: 'TEST', dbType: 'MSSQL', app: { publicado: null } });
expect(res.body.app.version).toMatch(/^\d+\.\d+\.\d+/);
});
});
it('/api/metadata/status exige sesión', () => {