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:
1 parent
699dd983d0
commit
b6e34276ce
35 files changed
+915
-50
No files matched your search
@@ -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,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -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 ' +
|
||||
|
||||
@@ -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+/);
|
||||
});
|
||||
});
|
||||
@@ -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();
|
||||
@@ -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', () => {
|
||||
|
||||
Reference in new issue
Block a user