feat(web): formularios de inventario y solicitudes de compra (entradas, salidas, transferencias, solicitudes de traslado)
Configuración declarativa en document-layouts.ts; las líneas de las transferencias van en StockTransferLines y las de compra/inventario llevan UoMEntry si el artículo tiene grupo de unidades. Probado contra SBODemoCL (solicitud de compra 2, solicitud de traslado 472, entrada 155). Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
45c0ca536e
commit
155d18fce4
13 files changed
+550
-46
No files matched your search
@@ -68,6 +68,16 @@ export function computeTotals(
|
||||
/** Campos de línea que se envían a SAP; el total de línea y lo calculado lo resuelve SAP. */
|
||||
const LINE_FIELDS = ['ItemCode', 'ItemDescription', 'Quantity', 'WarehouseCode', 'Price', 'DiscountPercent', 'TaxCode'] as const;
|
||||
|
||||
/** Colección de líneas de un documento y los campos de cada línea que se envían a SAP. */
|
||||
export interface LinesSpec {
|
||||
/** `DocumentLines`, o `StockTransferLines` en las transferencias de stock. */
|
||||
key: string;
|
||||
fields: readonly string[];
|
||||
/** Los documentos de ventas y compras referencian el origen de una línea copiada (`BaseType`…). */
|
||||
base: boolean;
|
||||
}
|
||||
export const MARKETING_LINES: LinesSpec = { key: 'DocumentLines', fields: LINE_FIELDS, base: true };
|
||||
|
||||
/** Referencia al documento origen de una línea copiada; SAP devuelve `BaseType: -1` y `BaseEntry: null` en las que no tienen. */
|
||||
const BASE_FIELDS = ['BaseType', 'BaseEntry', 'BaseLine'] as const;
|
||||
const hasBase = (l: GridLine): boolean => num(l['BaseType']) > 0 && !isEmpty(l['BaseEntry']);
|
||||
@@ -76,13 +86,13 @@ const hasBase = (l: GridLine): boolean => num(l['BaseType']) > 0 && !isEmpty(l['
|
||||
export const isBlankLine = (line: GridLine): boolean => isEmpty(line['ItemCode']);
|
||||
|
||||
/** Líneas listas para SAP: sin filas en blanco ni campos vacíos. */
|
||||
export function linesPayload(lines: readonly GridLine[]): DocRecord[] {
|
||||
export function linesPayload(lines: readonly GridLine[], spec: LinesSpec = MARKETING_LINES): DocRecord[] {
|
||||
return lines
|
||||
.filter((l) => !isBlankLine(l))
|
||||
.map((l) => {
|
||||
const out: DocRecord = {};
|
||||
for (const f of LINE_FIELDS) if (!isEmpty(l[f])) out[f] = l[f];
|
||||
if (hasBase(l)) for (const f of BASE_FIELDS) out[f] = l[f];
|
||||
for (const f of spec.fields) if (!isEmpty(l[f])) out[f] = l[f];
|
||||
if (spec.base && hasBase(l)) for (const f of BASE_FIELDS) out[f] = l[f];
|
||||
return out;
|
||||
});
|
||||
}
|
||||
@@ -100,9 +110,33 @@ export function validateDocument(header: DocRecord, lines: readonly GridLine[],
|
||||
return errors;
|
||||
}
|
||||
|
||||
/** Un campo obligatorio y su nombre para el aviso. */
|
||||
export interface RequiredField {
|
||||
key: string;
|
||||
label: string;
|
||||
}
|
||||
|
||||
/** Validación de los documentos sin socio de negocio (inventario, solicitudes de compra). */
|
||||
export function validateLayout(
|
||||
header: DocRecord,
|
||||
lines: readonly GridLine[],
|
||||
requiredHeader: readonly RequiredField[],
|
||||
requiredLine: readonly RequiredField[],
|
||||
): string[] {
|
||||
const errors: string[] = [];
|
||||
for (const f of requiredHeader) if (isEmpty(header[f.key])) errors.push(`Indique ${f.label.toLowerCase()}.`);
|
||||
const real = lines.filter((l) => !isBlankLine(l));
|
||||
if (!real.length) errors.push('Agregue al menos una línea con artículo.');
|
||||
real.forEach((l, i) => {
|
||||
if (num(l['Quantity']) <= 0) errors.push(`Línea ${i + 1}: la cantidad debe ser mayor que cero.`);
|
||||
for (const f of requiredLine) if (isEmpty(l[f.key])) errors.push(`Línea ${i + 1}: indique ${f.label.toLowerCase()}.`);
|
||||
});
|
||||
return errors;
|
||||
}
|
||||
|
||||
/** Cuerpo para crear: cabecera sin campos vacíos + líneas. */
|
||||
export function createPayload(header: DocRecord, lines: readonly GridLine[]): DocRecord {
|
||||
return { ...compact(header), DocumentLines: linesPayload(lines) };
|
||||
export function createPayload(header: DocRecord, lines: readonly GridLine[], spec: LinesSpec = MARKETING_LINES): DocRecord {
|
||||
return { ...compact(header), [spec.key]: linesPayload(lines, spec) };
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -115,6 +149,7 @@ export function updatePayload(
|
||||
header: DocRecord,
|
||||
originalLines: readonly GridLine[],
|
||||
lines: readonly GridLine[],
|
||||
spec: LinesSpec = MARKETING_LINES,
|
||||
): { body: DocRecord; replaceLines: boolean } {
|
||||
const body: DocRecord = {};
|
||||
for (const [key, value] of Object.entries(header)) {
|
||||
@@ -122,9 +157,9 @@ export function updatePayload(
|
||||
if (isEmpty(value) && isEmpty(before)) continue;
|
||||
if (value !== before) body[key] = isEmpty(value) ? null : value;
|
||||
}
|
||||
const next = linesPayload(lines);
|
||||
const replaceLines = JSON.stringify(next) !== JSON.stringify(linesPayload(originalLines));
|
||||
if (replaceLines) body['DocumentLines'] = next;
|
||||
const next = linesPayload(lines, spec);
|
||||
const replaceLines = JSON.stringify(next) !== JSON.stringify(linesPayload(originalLines, spec));
|
||||
if (replaceLines) body[spec.key] = next;
|
||||
return { body, replaceLines };
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user