Aplicación web de Devolución a Proveedores para SAP Business One
Reemplaza al addon .NET Framework (SolucionLibros). Genera Devoluciones de mercancías enlazadas a sus Entradas por compra a partir de un listado pegado desde Excel. - libs/shared (@dp/shared): modelos y lógica de dominio pura (FIFO, consolidación por proveedor, parseo del portapapeles), portada del addon. - apps/api (NestJS): cliente del Service Layer con cola por sesión, login con la sesión de SAP, metadatos (UDT/UDF/UDO) y botón Reconstruir, catálogos, planificaciones (pegar, procesar, reprocesar, marcar líneas, crear Devoluciones, cerrar), listado paginado con filtros y descarga de PDF de la Guía de despacho desde BES. Sirve también la web compilada, con HTTPS opcional. - apps/web (Angular): login, Configuración → Metadatos y Planificaciones (listado, nueva, detalle). - deploy/windows y scripts/publicar.mjs: despliegue como servicio de Windows con WinSW. Verificado contra el Service Layer de SQL Server. Sin probar aún: SAP HANA y la instalación del servicio en Windows (ver CLAUDE.md, sección "Sin verificar"). Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
commit
287b187c14
126 files changed
+20033
No files matched your search
@@ -0,0 +1,82 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { EntradaAbierta, LineaPlanificacion } from '../models/planificacion.js';
|
||||
import { asignarFifo, consolidarDocumentos, extraerLineasAplicables, puedeCambiarSeleccion } from './fifo.js';
|
||||
|
||||
function entrada(docEntry: number, docDate: string, openQty: number, cardCode = 'P1'): EntradaAbierta {
|
||||
return { docEntry, lineNum: 0, docNum: docEntry + 1000, docDate, cardCode, cardName: `Prov ${cardCode}`, openQty };
|
||||
}
|
||||
|
||||
describe('asignarFifo', () => {
|
||||
it('reparte la cantidad desde la Entrada más antigua', () => {
|
||||
const entradas = new Map([['A001', [entrada(2, '2026-02-01', 10), entrada(1, '2026-01-01', 4)]]]);
|
||||
const { lineas, advertencias } = asignarFifo([{ itemCode: 'a001', itemName: 'Art', quantity: 6, onHand: 20 }], entradas);
|
||||
|
||||
expect(advertencias).toEqual([]);
|
||||
expect(lineas.map((l) => [l.tipoFila, l.baseEntry, l.allocatedQty])).toEqual([
|
||||
['F', null, null],
|
||||
['S', 1, 4],
|
||||
['S', 2, 2],
|
||||
]);
|
||||
expect(lineas[1]).toMatchObject({ itemCode: 'a001', applied: true, baseType: 20, baseRef: 1001 });
|
||||
});
|
||||
|
||||
it('omite artículos sin stock suficiente', () => {
|
||||
const { lineas, advertencias } = asignarFifo([{ itemCode: 'A001', itemName: 'Art', quantity: 6, onHand: 5 }], new Map());
|
||||
expect(lineas).toEqual([]);
|
||||
expect(advertencias[0]).toContain('sin stock');
|
||||
});
|
||||
|
||||
it('advierte cuando las Entradas no alcanzan', () => {
|
||||
const entradas = new Map([['A001', [entrada(1, '2026-01-01', 2)]]]);
|
||||
const { lineas, advertencias } = asignarFifo([{ itemCode: 'A001', itemName: 'Art', quantity: 5, onHand: 5 }], entradas);
|
||||
expect(lineas).toHaveLength(2);
|
||||
expect(advertencias[0]).toContain('faltan 3');
|
||||
});
|
||||
});
|
||||
|
||||
describe('extraerLineasAplicables / puedeCambiarSeleccion', () => {
|
||||
const base = asignarFifo(
|
||||
[{ itemCode: 'A001', itemName: 'Art', quantity: 5, onHand: 5 }],
|
||||
new Map([['A001', [entrada(1, '2026-01-01', 2), entrada(2, '2026-01-02', 2), entrada(3, '2026-01-03', 2)]]]),
|
||||
).lineas;
|
||||
|
||||
it('devuelve solo Hijos marcados sin documento destino', () => {
|
||||
const lineas: LineaPlanificacion[] = base.map((l) => ({ ...l }));
|
||||
lineas[2].applied = false;
|
||||
lineas[3].targetEntry = 99;
|
||||
expect(extraerLineasAplicables(lineas).map((l) => l.baseEntry)).toEqual([1]);
|
||||
});
|
||||
|
||||
it('hereda el ItemCode del Padre si el Hijo no lo trae', () => {
|
||||
const lineas = base.map((l) => (l.tipoFila === 'S' ? { ...l, itemCode: '' } : l));
|
||||
expect(extraerLineasAplicables(lineas)[0].itemCode).toBe('A001');
|
||||
});
|
||||
|
||||
it('bloquea la selección de líneas con documento destino', () => {
|
||||
expect(puedeCambiarSeleccion(base[1])).toBe(true);
|
||||
expect(puedeCambiarSeleccion({ ...base[1], targetEntry: 10 })).toBe(false);
|
||||
expect(puedeCambiarSeleccion(base[0])).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('consolidarDocumentos', () => {
|
||||
it('agrupa por proveedor y corta en lotes', () => {
|
||||
const entradas = new Map([
|
||||
['A001', [entrada(1, '2026-01-01', 1, 'P1'), entrada(2, '2026-01-02', 1, 'P2'), entrada(3, '2026-01-03', 1, 'P1'), entrada(4, '2026-01-04', 1, 'P1')]],
|
||||
]);
|
||||
const { lineas } = asignarFifo([{ itemCode: 'A001', itemName: 'Art', quantity: 4, onHand: 4 }], entradas);
|
||||
const docs = consolidarDocumentos(extraerLineasAplicables(lineas), 2);
|
||||
|
||||
expect(docs.map((d) => [d.numero, d.cardCode, d.lineas.map((l) => l.baseEntry)])).toEqual([
|
||||
[1, 'P1', [1, 3]],
|
||||
[2, 'P1', [4]],
|
||||
[3, 'P2', [2]],
|
||||
]);
|
||||
});
|
||||
|
||||
it('usa 11 líneas si el máximo no es válido', () => {
|
||||
const entradas = new Map([['A001', Array.from({ length: 12 }, (_, i) => entrada(i + 1, '2026-01-01', 1))]]);
|
||||
const { lineas } = asignarFifo([{ itemCode: 'A001', itemName: 'Art', quantity: 12, onHand: 12 }], entradas);
|
||||
expect(consolidarDocumentos(extraerLineasAplicables(lineas), 0).map((d) => d.lineas.length)).toEqual([11, 1]);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,176 @@
|
||||
// Asignación FIFO y consolidación de Devoluciones. Portado de
|
||||
// FormularioDevolucionProveedores.Obtener_Documentos_FIFO / ExtraerLineasAplicadas /
|
||||
// ConsolidarDocumentos (.NET).
|
||||
|
||||
import {
|
||||
ArticuloADevolver,
|
||||
DocumentoDevolucion,
|
||||
EntradaAbierta,
|
||||
EstadoStock,
|
||||
LineaPlanificacion,
|
||||
MAX_LINEAS_POR_DOCUMENTO_DEFECTO,
|
||||
OBJ_TYPE_ENTRADA_MERCANCIAS,
|
||||
} from '../models/planificacion.js';
|
||||
|
||||
export function evaluarStock(quantity: number, onHand: number): EstadoStock {
|
||||
return quantity <= onHand ? 'OK' : 'SIN_STOCK';
|
||||
}
|
||||
|
||||
function lineaVacia(): LineaPlanificacion {
|
||||
return {
|
||||
tipoFila: 'F',
|
||||
itemCode: '',
|
||||
itemDescription: '',
|
||||
quantity: null,
|
||||
onHand: null,
|
||||
status: null,
|
||||
baseType: null,
|
||||
baseEntry: null,
|
||||
baseLine: null,
|
||||
baseRef: null,
|
||||
baseDocDate: null,
|
||||
openQty: null,
|
||||
allocatedQty: null,
|
||||
applied: false,
|
||||
cardCode: '',
|
||||
cardName: '',
|
||||
targetType: null,
|
||||
targetEntry: null,
|
||||
errCode: '',
|
||||
errMsg: '',
|
||||
};
|
||||
}
|
||||
|
||||
export function crearLineaPadre(articulo: ArticuloADevolver): LineaPlanificacion {
|
||||
return {
|
||||
...lineaVacia(),
|
||||
tipoFila: 'F',
|
||||
itemCode: articulo.itemCode,
|
||||
itemDescription: articulo.itemName,
|
||||
quantity: articulo.quantity,
|
||||
onHand: articulo.onHand,
|
||||
status: evaluarStock(articulo.quantity, articulo.onHand),
|
||||
};
|
||||
}
|
||||
|
||||
function compararEntradas(a: EntradaAbierta, b: EntradaAbierta): number {
|
||||
return a.docDate.localeCompare(b.docDate) || a.docEntry - b.docEntry || a.lineNum - b.lineNum;
|
||||
}
|
||||
|
||||
export interface ResultadoFifo {
|
||||
lineas: LineaPlanificacion[];
|
||||
advertencias: string[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Por cada artículo con stock suficiente agrega su fila Padre y, debajo, una fila Hijo por
|
||||
* cada Entrada de mercancías consumida (más antigua primero) hasta cubrir la cantidad.
|
||||
* Los artículos sin stock quedan fuera. entradasPorItem debe traer solo las Entradas del
|
||||
* almacén elegido; la clave es el ItemCode en mayúsculas.
|
||||
*/
|
||||
export function asignarFifo(articulos: ArticuloADevolver[], entradasPorItem: Map<string, EntradaAbierta[]>): ResultadoFifo {
|
||||
const lineas: LineaPlanificacion[] = [];
|
||||
const advertencias: string[] = [];
|
||||
|
||||
for (const articulo of articulos) {
|
||||
const padre = crearLineaPadre(articulo);
|
||||
|
||||
if (padre.status === 'SIN_STOCK') {
|
||||
advertencias.push(`${articulo.itemCode}: sin stock suficiente (${articulo.onHand} de ${articulo.quantity}), se omite.`);
|
||||
continue;
|
||||
}
|
||||
|
||||
lineas.push(padre);
|
||||
|
||||
let pendiente = articulo.quantity;
|
||||
const entradas = [...(entradasPorItem.get(articulo.itemCode.toUpperCase()) ?? [])].sort(compararEntradas);
|
||||
|
||||
for (const entrada of entradas) {
|
||||
if (pendiente <= 0) break;
|
||||
if (entrada.openQty <= 0) continue;
|
||||
|
||||
const asignado = Math.min(pendiente, entrada.openQty);
|
||||
|
||||
lineas.push({
|
||||
...lineaVacia(),
|
||||
tipoFila: 'S',
|
||||
itemCode: articulo.itemCode,
|
||||
itemDescription: articulo.itemName,
|
||||
baseType: OBJ_TYPE_ENTRADA_MERCANCIAS,
|
||||
baseEntry: entrada.docEntry,
|
||||
baseLine: entrada.lineNum,
|
||||
baseRef: entrada.docNum,
|
||||
baseDocDate: entrada.docDate,
|
||||
openQty: entrada.openQty,
|
||||
allocatedQty: asignado,
|
||||
applied: true,
|
||||
cardCode: entrada.cardCode,
|
||||
cardName: entrada.cardName,
|
||||
});
|
||||
|
||||
pendiente -= asignado;
|
||||
}
|
||||
|
||||
if (pendiente > 0) {
|
||||
advertencias.push(`${articulo.itemCode}: faltan ${pendiente} unidad(es) sin Entrada de mercancías abierta en el almacén.`);
|
||||
}
|
||||
}
|
||||
|
||||
return { lineas, advertencias };
|
||||
}
|
||||
|
||||
/** Una línea con documento destino ya no puede desmarcarse. */
|
||||
export function puedeCambiarSeleccion(linea: LineaPlanificacion): boolean {
|
||||
return linea.tipoFila === 'S' && linea.targetEntry == null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Filas Hijo marcadas que todavía no tienen Devolución creada. Si una fila Hijo no trae
|
||||
* ItemCode lo hereda de su Padre (registros grabados por el addon .NET).
|
||||
*/
|
||||
export function extraerLineasAplicables(lineas: LineaPlanificacion[]): LineaPlanificacion[] {
|
||||
const resultado: LineaPlanificacion[] = [];
|
||||
let padre: LineaPlanificacion | null = null;
|
||||
|
||||
for (const linea of lineas) {
|
||||
if (linea.tipoFila === 'F') {
|
||||
padre = linea;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (linea.applied && linea.targetEntry == null) {
|
||||
resultado.push(
|
||||
linea.itemCode || !padre ? linea : { ...linea, itemCode: padre.itemCode, itemDescription: padre.itemDescription },
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return resultado;
|
||||
}
|
||||
|
||||
/**
|
||||
* Agrupa por proveedor (en el orden en que aparecen) y parte cada grupo en lotes de a lo
|
||||
* sumo maxLineasPorDocumento líneas: un DocumentoDevolucion por lote.
|
||||
*/
|
||||
export function consolidarDocumentos(lineas: LineaPlanificacion[], maxLineasPorDocumento: number): DocumentoDevolucion[] {
|
||||
const max = Number.isInteger(maxLineasPorDocumento) && maxLineasPorDocumento > 0 ? maxLineasPorDocumento : MAX_LINEAS_POR_DOCUMENTO_DEFECTO;
|
||||
|
||||
const grupos = new Map<string, LineaPlanificacion[]>();
|
||||
for (const linea of lineas) {
|
||||
const grupo = grupos.get(linea.cardCode) ?? [];
|
||||
grupo.push(linea);
|
||||
grupos.set(linea.cardCode, grupo);
|
||||
}
|
||||
|
||||
const documentos: DocumentoDevolucion[] = [];
|
||||
let numero = 1;
|
||||
|
||||
for (const [cardCode, grupo] of grupos) {
|
||||
for (let i = 0; i < grupo.length; i += max) {
|
||||
const lote = grupo.slice(i, i + max);
|
||||
documentos.push({ numero: numero++, cardCode, cardName: lote[0].cardName, lineas: lote });
|
||||
}
|
||||
}
|
||||
|
||||
return documentos;
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { parsearPortapapeles } from './portapapeles.js';
|
||||
|
||||
describe('parsearPortapapeles', () => {
|
||||
it('lee ItemCode y cantidad de 2 columnas', () => {
|
||||
const r = parsearPortapapeles('A001\t5\r\nA002\t3\r\n');
|
||||
expect(r.errores).toEqual([]);
|
||||
expect(r.filas).toEqual([
|
||||
{ itemCode: 'A001', quantity: 5 },
|
||||
{ itemCode: 'A002', quantity: 3 },
|
||||
]);
|
||||
});
|
||||
|
||||
it('con 3 o más columnas toma la cantidad de la columna 2', () => {
|
||||
const r = parsearPortapapeles('A001\tTornillo\t7\tx');
|
||||
expect(r.filas).toEqual([{ itemCode: 'A001', quantity: 7 }]);
|
||||
});
|
||||
|
||||
it('descarta la primera fila si es encabezado, sin error', () => {
|
||||
const r = parsearPortapapeles('Codigo\tCantidad\nA001\t2');
|
||||
expect(r.errores).toEqual([]);
|
||||
expect(r.filas).toEqual([{ itemCode: 'A001', quantity: 2 }]);
|
||||
});
|
||||
|
||||
it('informa cantidades no enteras, cero o negativas', () => {
|
||||
const r = parsearPortapapeles('A001\t1\nA002\t1,5\nA003\t0\nA004\t-2');
|
||||
expect(r.filas).toEqual([{ itemCode: 'A001', quantity: 1 }]);
|
||||
expect(r.errores).toHaveLength(3);
|
||||
expect(r.errores[0]).toContain('Fila 2');
|
||||
});
|
||||
|
||||
it('informa filas con menos de 2 columnas salvo la primera', () => {
|
||||
const r = parsearPortapapeles('titulo\nA001\t1\nsolo');
|
||||
expect(r.filas).toHaveLength(1);
|
||||
expect(r.errores).toEqual(['Fila 3: se esperaban al menos 2 columnas, se encontraron 1.']);
|
||||
});
|
||||
|
||||
it('rechaza todo si hay ItemCode duplicados (sin distinguir mayúsculas)', () => {
|
||||
const r = parsearPortapapeles('A001\t1\na001\t2\nB001\t1');
|
||||
expect(r.filas).toEqual([]);
|
||||
expect(r.errores[0]).toContain('A001');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,80 @@
|
||||
// Parseo del texto pegado desde Excel (filas separadas por salto de línea, columnas por
|
||||
// tabulador). Portado de FormularioDevolucionProveedores.ParsearPortapapeles (.NET).
|
||||
|
||||
export interface FilaPegada {
|
||||
itemCode: string;
|
||||
quantity: number;
|
||||
}
|
||||
|
||||
export interface ResultadoPegado {
|
||||
filas: FilaPegada[];
|
||||
errores: string[];
|
||||
}
|
||||
|
||||
const ENTERO = /^[+-]?\d+$/;
|
||||
|
||||
/**
|
||||
* Columna 0 = ItemCode siempre. La cantidad sale de la columna 2 si la fila trae 3 o más
|
||||
* columnas (la columna 1 se ignora, suele ser la descripción), o de la columna 1 si trae
|
||||
* exactamente 2. Si la primera fila no tiene una cantidad numérica se asume encabezado y se
|
||||
* descarta sin error. Si hay ItemCode duplicados no se acepta ninguna fila.
|
||||
*/
|
||||
export function parsearPortapapeles(texto: string): ResultadoPegado {
|
||||
const errores: string[] = [];
|
||||
const filas: FilaPegada[] = [];
|
||||
|
||||
const lineas = texto.split(/\r?\n/).filter((l) => l.length > 0);
|
||||
|
||||
lineas.forEach((linea, i) => {
|
||||
const celdas = linea.split('\t');
|
||||
const numeroFila = i + 1;
|
||||
const esPrimeraFila = i === 0;
|
||||
|
||||
if (celdas.length < 2) {
|
||||
if (!esPrimeraFila) {
|
||||
errores.push(`Fila ${numeroFila}: se esperaban al menos 2 columnas, se encontraron ${celdas.length}.`);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const itemCode = celdas[0].trim();
|
||||
const quantityTexto = celdas[celdas.length === 2 ? 1 : 2].trim();
|
||||
|
||||
if (!ENTERO.test(quantityTexto)) {
|
||||
if (!esPrimeraFila) {
|
||||
errores.push(`Fila ${numeroFila}: '${quantityTexto}' debe ser un valor entero.`);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const quantity = Number.parseInt(quantityTexto, 10);
|
||||
|
||||
if (quantity <= 0) {
|
||||
errores.push(`Fila ${numeroFila}: la cantidad debe ser mayor a cero.`);
|
||||
return;
|
||||
}
|
||||
|
||||
if (itemCode.length === 0) {
|
||||
errores.push(`Fila ${numeroFila}: código de artículo vacío.`);
|
||||
return;
|
||||
}
|
||||
|
||||
filas.push({ itemCode, quantity });
|
||||
});
|
||||
|
||||
// Clave en mayúsculas → ItemCode tal como apareció primero y cuántas veces se repite.
|
||||
const conteo = new Map<string, { itemCode: string; veces: number }>();
|
||||
for (const fila of filas) {
|
||||
const clave = fila.itemCode.toUpperCase();
|
||||
const actual = conteo.get(clave);
|
||||
conteo.set(clave, { itemCode: actual?.itemCode ?? fila.itemCode, veces: (actual?.veces ?? 0) + 1 });
|
||||
}
|
||||
const duplicados = [...conteo.values()].filter((c) => c.veces > 1).map((c) => c.itemCode);
|
||||
|
||||
if (duplicados.length > 0) {
|
||||
errores.push(`Código(s) de artículo duplicado(s): ${duplicados.join(', ')}. No se pegó ninguna fila.`);
|
||||
return { filas: [], errores };
|
||||
}
|
||||
|
||||
return { filas, errores };
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
export * from './models/auth.js';
|
||||
export * from './models/metadata.js';
|
||||
export * from './models/planificacion.js';
|
||||
export * from './domain/portapapeles.js';
|
||||
export * from './domain/fifo.js';
|
||||
export * from './models/catalogos.js';
|
||||
@@ -0,0 +1,16 @@
|
||||
export interface LoginRequest {
|
||||
userName: string;
|
||||
password: string;
|
||||
}
|
||||
|
||||
export interface UsuarioSesion {
|
||||
userName: string;
|
||||
companyDb: string;
|
||||
/** Versión del Service Layer devuelta en el Login. */
|
||||
version: string;
|
||||
}
|
||||
|
||||
export interface InfoConexion {
|
||||
companyDb: string;
|
||||
dbType: 'HANA' | 'MSSQL';
|
||||
}
|
||||
@@ -0,0 +1,141 @@
|
||||
// Catálogos y respuestas del módulo de planificaciones.
|
||||
|
||||
import type { CabeceraPlanificacion, DocumentoDevolucion, LineaPlanificacion, Planificacion } from './planificacion.js';
|
||||
|
||||
export interface Almacen {
|
||||
code: string;
|
||||
name: string;
|
||||
}
|
||||
|
||||
export interface SerieDocumento {
|
||||
series: number;
|
||||
name: string;
|
||||
nextNumber: number | null;
|
||||
esPorDefecto: boolean;
|
||||
}
|
||||
|
||||
/** Valor permitido de un UDF de la Devolución (por ejemplo BFE_TipoDespacho). */
|
||||
export interface OpcionUdf {
|
||||
value: string;
|
||||
description: string;
|
||||
}
|
||||
|
||||
export interface CatalogosDevolucion {
|
||||
almacenes: Almacen[];
|
||||
series: SerieDocumento[];
|
||||
tiposDespacho: OpcionUdf[];
|
||||
indicadoresTraslado: OpcionUdf[];
|
||||
}
|
||||
|
||||
/** Resultado de pegar el listado de Excel: filas con stock y los errores de formato/artículos. */
|
||||
export interface ResultadoPegadoApi {
|
||||
lineas: LineaPlanificacion[];
|
||||
errores: string[];
|
||||
}
|
||||
|
||||
export interface ResumenPlanificacion {
|
||||
docEntry: number;
|
||||
docNum: number;
|
||||
status: 'O' | 'C';
|
||||
whsCode: string;
|
||||
docDate: string;
|
||||
}
|
||||
|
||||
export const TAMANO_PAGINA_DEFECTO = 20;
|
||||
export const TAMANO_PAGINA_MAXIMO = 100;
|
||||
|
||||
/** Filtros del listado de planificaciones. Todos son opcionales y se combinan con "y". */
|
||||
export interface FiltrosPlanificaciones {
|
||||
status?: 'O' | 'C';
|
||||
whsCode?: string;
|
||||
/** Fecha del documento, desde (inclusive), formato yyyy-MM-dd. */
|
||||
desde?: string;
|
||||
/** Fecha del documento, hasta (inclusive), formato yyyy-MM-dd. */
|
||||
hasta?: string;
|
||||
/** Desde 1. */
|
||||
pagina?: number;
|
||||
tamano?: number;
|
||||
}
|
||||
|
||||
export interface PaginaPlanificaciones {
|
||||
items: ResumenPlanificacion[];
|
||||
/** Total de planificaciones que cumplen los filtros (no solo las de esta página). */
|
||||
total: number;
|
||||
pagina: number;
|
||||
tamano: number;
|
||||
totalPaginas: number;
|
||||
}
|
||||
|
||||
/** Respuesta de GET /planificaciones/abierta: siempre un JSON, con `abierta` en null si no hay. */
|
||||
export interface RespuestaAbierta {
|
||||
abierta: ResumenPlanificacion | null;
|
||||
}
|
||||
|
||||
export interface ResultadoProcesar {
|
||||
planificacion: Planificacion;
|
||||
advertencias: string[];
|
||||
/** Solo al reprocesar: la planificación anterior que esta reemplaza. */
|
||||
reemplazada?: { docNum: number; errorCierre?: string };
|
||||
}
|
||||
|
||||
/** Devolución creada (o fallida) a partir de un lote de líneas Hijo. */
|
||||
export interface ResultadoDevolucion {
|
||||
numero: number;
|
||||
cardCode: string;
|
||||
cardName: string;
|
||||
lineas: number;
|
||||
docEntry?: number;
|
||||
docNum?: number;
|
||||
error?: string;
|
||||
}
|
||||
|
||||
export interface ResultadoCrearDevoluciones {
|
||||
devoluciones: ResultadoDevolucion[];
|
||||
/** Falla al grabar TargetEntry/ErrMsg en la planificación (las Devoluciones sí se crearon). */
|
||||
errorGuardado?: string;
|
||||
planificacion: Planificacion;
|
||||
}
|
||||
|
||||
export type CabeceraEditable = Partial<
|
||||
Pick<
|
||||
CabeceraPlanificacion,
|
||||
| 'docDate'
|
||||
| 'series'
|
||||
| 'printingLines'
|
||||
| 'tipoDespacho'
|
||||
| 'indicadorTraslado'
|
||||
| 'rutTransporte'
|
||||
| 'rutChofer'
|
||||
| 'nombreChofer'
|
||||
| 'fechaSalida'
|
||||
| 'horaSalida'
|
||||
| 'fechaLlegada'
|
||||
| 'patente'
|
||||
>
|
||||
>;
|
||||
|
||||
export type { DocumentoDevolucion };
|
||||
|
||||
/** Resultado de pedir el PDF de la Guía de despacho de una Devolución. */
|
||||
export interface ResultadoPdfDevolucion {
|
||||
/** DocEntry de la Devolución (ORPD). */
|
||||
docEntry: number;
|
||||
/** Folio con el que se consultó el web service (FolioNum de la Devolución, o su DocNum si no tiene). */
|
||||
folio?: number;
|
||||
error?: string;
|
||||
}
|
||||
|
||||
export interface ArchivoPdf {
|
||||
nombre: string;
|
||||
/** Contenido del PDF en base64. */
|
||||
base64: string;
|
||||
/** Folios que contiene (varios si es el PDF unificado). */
|
||||
folios: number[];
|
||||
}
|
||||
|
||||
export interface ResultadoPdf {
|
||||
devoluciones: ResultadoPdfDevolucion[];
|
||||
/** Un archivo por Devolución, o uno solo (unificado) si se pidió unir y hay más de uno. */
|
||||
archivos: ArchivoPdf[];
|
||||
unificado: boolean;
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
// Definición y estado de los metadatos de usuario (tablas, campos y objeto) que el
|
||||
// proceso necesita en la base SAP. Los nombres de propiedades siguen al Service Layer
|
||||
// (UserTablesMD, UserFieldsMD, UserObjectsMD) para poder enviarlos sin transformar.
|
||||
|
||||
export type BoYesNo = 'tYES' | 'tNO';
|
||||
|
||||
export type TipoCampo = 'db_Alpha' | 'db_Numeric' | 'db_Date' | 'db_Float' | 'db_Memo';
|
||||
|
||||
export type SubTipoCampo = 'st_None' | 'st_Time' | 'st_Price' | 'st_Quantity' | 'st_Sum' | 'st_Rate';
|
||||
|
||||
export interface DefinicionTabla {
|
||||
TableName: string;
|
||||
TableDescription: string;
|
||||
TableType: 'bott_Document' | 'bott_DocumentLines' | 'bott_NoObject';
|
||||
Archivable: BoYesNo;
|
||||
}
|
||||
|
||||
export interface DefinicionCampo {
|
||||
/** Nombre de la tabla tal como lo espera el Service Layer: "@LIB_ODEV". */
|
||||
TableName: string;
|
||||
/** Nombre sin prefijo "U_". */
|
||||
Name: string;
|
||||
Description: string;
|
||||
Type: TipoCampo;
|
||||
SubType: SubTipoCampo;
|
||||
EditSize?: number;
|
||||
Mandatory: BoYesNo;
|
||||
DefaultValue?: string;
|
||||
ValidValuesMD?: { Value: string; Description: string }[];
|
||||
}
|
||||
|
||||
export interface DefinicionObjeto {
|
||||
Code: string;
|
||||
Name: string;
|
||||
ObjectType: 'boud_Document' | 'boud_MasterData';
|
||||
TableName: string;
|
||||
CanFind: BoYesNo;
|
||||
CanClose: BoYesNo;
|
||||
CanCancel: BoYesNo;
|
||||
CanDelete: BoYesNo;
|
||||
CanLog: BoYesNo;
|
||||
LogTableName?: string;
|
||||
CanCreateDefaultForm: BoYesNo;
|
||||
ManageSeries: BoYesNo;
|
||||
UserObjectMD_ChildTables: { TableName: string; ObjectName: string }[];
|
||||
/** Obligatorias para que CanFind quede en tYES: SAP lo deja en tNO si no hay ninguna. */
|
||||
UserObjectMD_FindColumns?: { ColumnAlias: string; ColumnDescription: string }[];
|
||||
}
|
||||
|
||||
/** Elemento externo que este proceso solo lee (lo crean otros addons/localización). */
|
||||
export interface DependenciaExterna {
|
||||
tipo: 'tabla' | 'campo';
|
||||
/** Tabla SAP: "OPDN", "@DTE_CONFC", ... */
|
||||
tabla: string;
|
||||
/** Campo sin "U_" (solo tipo campo). */
|
||||
campo?: string;
|
||||
descripcion: string;
|
||||
}
|
||||
|
||||
export type EstadoElemento = 'OK' | 'FALTA' | 'DIFERENTE' | 'SOBRANTE';
|
||||
|
||||
export interface EstadoMetadato {
|
||||
tipo: 'tabla' | 'campo' | 'objeto' | 'dependencia';
|
||||
/** "LIB_ODEV", "@LIB_DEV1.U_Quantity", ... */
|
||||
nombre: string;
|
||||
descripcion: string;
|
||||
estado: EstadoElemento;
|
||||
/** Detalle de la diferencia encontrada, p. ej. "Tipo db_Alpha, se esperaba db_Numeric". */
|
||||
detalle?: string;
|
||||
/** true si corregirlo implica borrar y volver a crear (se pierden datos de esa columna). */
|
||||
destructivo?: boolean;
|
||||
}
|
||||
|
||||
export interface EstadoMetadatos {
|
||||
/** true cuando todo lo propio está OK y no faltan dependencias externas. */
|
||||
completo: boolean;
|
||||
elementos: EstadoMetadato[];
|
||||
}
|
||||
|
||||
export interface OpcionesReconstruccion {
|
||||
/**
|
||||
* Borra y vuelve a crear los campos con tipo distinto, y elimina los campos sobrantes
|
||||
* de las tablas propias. Se pierden los datos de esas columnas.
|
||||
*/
|
||||
permitirDestructivos: boolean;
|
||||
}
|
||||
|
||||
export interface ResultadoPaso {
|
||||
accion: string;
|
||||
nombre: string;
|
||||
ok: boolean;
|
||||
mensaje?: string;
|
||||
}
|
||||
|
||||
export interface ResultadoReconstruccion {
|
||||
pasos: ResultadoPaso[];
|
||||
estado: EstadoMetadatos;
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
// Modelo de la Planificación de Devolución a Proveedores (UDO LIB_ODEV).
|
||||
// Cabecera = @LIB_ODEV, líneas = @LIB_DEV1 (filas Padre "F" y filas Hijo "S").
|
||||
|
||||
/** Devolución de mercancías (ORPD). Es el único documento destino del proceso. */
|
||||
export const OBJ_TYPE_DEVOLUCION = 21;
|
||||
|
||||
/** Entrada de mercancías por compra (OPDN). Documento base de cada línea Hijo. */
|
||||
export const OBJ_TYPE_ENTRADA_MERCANCIAS = 20;
|
||||
|
||||
/** Máximo de líneas por Devolución cuando la cabecera no trae U_PrintingLines válido. */
|
||||
export const MAX_LINEAS_POR_DOCUMENTO_DEFECTO = 11;
|
||||
|
||||
/** F = Padre (artículo pegado desde Excel), S = Hijo (asignación FIFO contra una Entrada). */
|
||||
export type TipoFila = 'F' | 'S';
|
||||
|
||||
/** Resultado de comparar la cantidad a devolver contra el stock del almacén. */
|
||||
export type EstadoStock = 'OK' | 'SIN_STOCK';
|
||||
|
||||
export interface CabeceraPlanificacion {
|
||||
docEntry?: number;
|
||||
docNum?: number;
|
||||
/** Estado estándar del UDO: O = abierta, C = cerrada. */
|
||||
status?: 'O' | 'C';
|
||||
whsCode: string;
|
||||
/** Fecha contable, formato ISO yyyy-MM-dd. */
|
||||
docDate: string;
|
||||
series: number;
|
||||
printingLines: number;
|
||||
tipoDespacho: string;
|
||||
indicadorTraslado: string;
|
||||
rutTransporte?: string;
|
||||
rutChofer?: string;
|
||||
nombreChofer?: string;
|
||||
fechaSalida?: string;
|
||||
/** Hora de salida, formato HH:mm. */
|
||||
horaSalida?: string;
|
||||
fechaLlegada?: string;
|
||||
patente?: string;
|
||||
}
|
||||
|
||||
export interface LineaPlanificacion {
|
||||
lineId?: number;
|
||||
tipoFila: TipoFila;
|
||||
itemCode: string;
|
||||
itemDescription: string;
|
||||
/** Solo filas Padre. */
|
||||
quantity: number | null;
|
||||
onHand: number | null;
|
||||
status: EstadoStock | null;
|
||||
/** Solo filas Hijo. */
|
||||
baseType: number | null;
|
||||
baseEntry: number | null;
|
||||
baseLine: number | null;
|
||||
baseRef: number | null;
|
||||
baseDocDate: string | null;
|
||||
openQty: number | null;
|
||||
allocatedQty: number | null;
|
||||
applied: boolean;
|
||||
cardCode: string;
|
||||
cardName: string;
|
||||
targetType: number | null;
|
||||
targetEntry: number | null;
|
||||
errCode: string;
|
||||
errMsg: string;
|
||||
}
|
||||
|
||||
export interface Planificacion {
|
||||
cabecera: CabeceraPlanificacion;
|
||||
lineas: LineaPlanificacion[];
|
||||
}
|
||||
|
||||
/** Artículo pegado desde Excel, ya completado con su descripción y stock en el almacén. */
|
||||
export interface ArticuloADevolver {
|
||||
itemCode: string;
|
||||
itemName: string;
|
||||
quantity: number;
|
||||
onHand: number;
|
||||
}
|
||||
|
||||
/** Línea abierta de una Entrada de mercancías (PDN1) candidata para el FIFO. */
|
||||
export interface EntradaAbierta {
|
||||
docEntry: number;
|
||||
lineNum: number;
|
||||
docNum: number;
|
||||
docDate: string;
|
||||
cardCode: string;
|
||||
cardName: string;
|
||||
openQty: number;
|
||||
}
|
||||
|
||||
/** Una Devolución a crear: un proveedor y como máximo N líneas Hijo. */
|
||||
export interface DocumentoDevolucion {
|
||||
numero: number;
|
||||
cardCode: string;
|
||||
cardName: string;
|
||||
lineas: LineaPlanificacion[];
|
||||
}
|
||||
Reference in new issue
Block a user