fix(web): ventas y compras envían la unidad de medida del artículo (UoMEntry)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
0d5a3074c8
commit
d3ab97cd2e
7 files changed
+71
-20
No files matched your search
@@ -201,21 +201,21 @@ describe('DocumentFormPage de inventario', () => {
|
||||
});
|
||||
|
||||
describe('columnas de línea por documento', () => {
|
||||
const base = { partnerLabel: 'Cliente', taxResource: 'SalesTaxCodes' };
|
||||
const base = { partnerLabel: 'Cliente', taxResource: 'SalesTaxCodes', uom: 'sales' } as const;
|
||||
|
||||
it('sin elegir, el documento lleva todas las columnas y envía los mismos campos de siempre', () => {
|
||||
const l = marketingLayout(base);
|
||||
expect(l.columns.map((c) => c.key)).toEqual([
|
||||
'ItemCode', 'ItemDescription', 'Quantity', 'WarehouseCode', 'Price', 'DiscountPercent', 'TaxCode', 'LineTotal',
|
||||
]);
|
||||
expect(l.lines.fields).toEqual(MARKETING_LINES.fields);
|
||||
expect(l.lines.fields).toEqual([...MARKETING_LINES.fields, 'UoMEntry']);
|
||||
});
|
||||
|
||||
it('un documento elige sus columnas y el orden; los campos enviados salen de ellas', () => {
|
||||
const { item, description, quantity, warehouse } = LINE_COLUMNS;
|
||||
const l = marketingLayout({ ...base, lineColumns: [item, description, quantity, warehouse] });
|
||||
expect(l.columns.map((c) => c.key)).toEqual(['ItemCode', 'ItemDescription', 'Quantity', 'WarehouseCode']);
|
||||
expect(l.lines.fields).toEqual(['ItemCode', 'ItemDescription', 'Quantity', 'WarehouseCode']);
|
||||
expect(l.lines.fields).toEqual(['ItemCode', 'ItemDescription', 'Quantity', 'WarehouseCode', 'UoMEntry']);
|
||||
const body = createPayload({ CardCode: 'C1' }, [{ ItemCode: 'A', Quantity: 1, WarehouseCode: '01', Price: 9, TaxCode: 'IVA' }], l.lines);
|
||||
expect(body['DocumentLines']).toEqual([{ ItemCode: 'A', Quantity: 1, WarehouseCode: '01' }]);
|
||||
});
|
||||
@@ -236,3 +236,36 @@ describe('columnas de línea por documento', () => {
|
||||
expect(other.columns).toHaveLength(8);
|
||||
});
|
||||
});
|
||||
|
||||
describe('unidad de medida en ventas y compras', () => {
|
||||
const item = { ItemCode: 'SU01', ItemName: 'Sutura', UoMGroupEntry: 1, InventoryUoMEntry: 1, DefaultPurchasingUoMEntry: null, DefaultSalesUoMEntry: 2 };
|
||||
const pick = (uom: 'sales' | 'purchase', picked: Record<string, unknown>, line: Record<string, unknown> = {}) =>
|
||||
marketingLayout({ partnerLabel: 'Cliente', taxResource: 'SalesTaxCodes', uom })
|
||||
.columns.find((c) => c.key === 'ItemCode')!
|
||||
.lookup!.apply(picked, line);
|
||||
|
||||
it('al elegir un artículo la línea toma su unidad: la de venta en ventas, la de compra en compras', () => {
|
||||
expect(pick('sales', item)).toMatchObject({ ItemCode: 'SU01', UoMEntry: 2 });
|
||||
expect(pick('purchase', item)).toMatchObject({ ItemCode: 'SU01', UoMEntry: 1 }); // sin unidad de compra: la de inventario
|
||||
});
|
||||
|
||||
it('un artículo de unidad manual no lleva unidad, y al cambiar de artículo no arrastra la anterior', () => {
|
||||
const manual = { ItemCode: 'M1', UoMGroupEntry: -1, InventoryUoMEntry: -1 };
|
||||
expect(pick('purchase', manual, { ItemCode: 'SU01', UoMEntry: 1 })['UoMEntry']).toBeUndefined();
|
||||
});
|
||||
|
||||
it('la unidad se envía a SAP solo si es válida (la manual, -1, no)', () => {
|
||||
const l = marketingLayout({ partnerLabel: 'Cliente', taxResource: 'SalesTaxCodes', uom: 'purchase' });
|
||||
const body = createPayload({}, [{ ItemCode: 'A', Quantity: 1, UoMEntry: 3 }, { ItemCode: 'B', Quantity: 1, UoMEntry: -1 }], l.lines);
|
||||
expect(body['DocumentLines']).toEqual([
|
||||
{ ItemCode: 'A', Quantity: 1, UoMEntry: 3 },
|
||||
{ ItemCode: 'B', Quantity: 1 },
|
||||
]);
|
||||
});
|
||||
|
||||
it('cada pantalla toma la unidad de su lado', () => {
|
||||
const ap = (path: string) => DOCUMENT_SCREENS.find((s) => s.path === path)!.layout.columns.find((c) => c.key === 'ItemCode')!.lookup!.apply(item, {});
|
||||
expect(ap('/ventas/ordenes')['UoMEntry']).toBe(2);
|
||||
expect(ap('/compras/ordenes')['UoMEntry']).toBe(1);
|
||||
});
|
||||
});
|
||||
@@ -88,14 +88,22 @@ export const commentsOnly: FieldColumns = { left: [FIELDS.comments], right: [] }
|
||||
const [baseItemColumn, descriptionColumn, quantityColumn, warehouseColumn, priceColumn] = DOCUMENT_LINE_COLUMNS;
|
||||
export { descriptionColumn, priceColumn, quantityColumn, warehouseColumn };
|
||||
|
||||
/** Artículo que además completa la unidad de medida de la línea (`UoMEntry`). */
|
||||
export const itemColumn: GridColumn = {
|
||||
...baseItemColumn,
|
||||
lookup: {
|
||||
...baseItemColumn.lookup!,
|
||||
apply: (item, line) => ({ ...baseItemColumn.lookup!.apply(item, line), UoMEntry: uomEntryOf(item) ?? line['UoMEntry'] }),
|
||||
},
|
||||
};
|
||||
/**
|
||||
* Columna de artículo que además completa la unidad de medida de la línea (`UoMEntry`): sin ella SAP
|
||||
* rechaza el documento si el artículo no tiene unidad por defecto. Al cambiar de artículo se reemplaza
|
||||
* (un artículo de unidad manual no lleva).
|
||||
*/
|
||||
export function itemColumnWithUom(item: GridColumn, side: 'purchase' | 'sales'): GridColumn {
|
||||
return {
|
||||
...item,
|
||||
lookup: {
|
||||
...item.lookup!,
|
||||
apply: (picked, line) => ({ ...item.lookup!.apply(picked, line), UoMEntry: uomEntryOf(picked, side) }),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export const itemColumn: GridColumn = itemColumnWithUom(baseItemColumn, 'purchase');
|
||||
|
||||
/** Lista de almacenes de la cabecera (la misma de las líneas). */
|
||||
export const WAREHOUSE_LOOKUP: VhLookup = warehouseColumn.lookup!.config;
|
||||
@@ -160,10 +168,14 @@ export function marketingLayout(opts: {
|
||||
lineColumns?: readonly GridColumn[];
|
||||
/** Columnas de las listas de valores (cliente, artículo, almacén, impuesto) de este documento. */
|
||||
lookups?: LookupColumns;
|
||||
/** Unidad de medida que se toma del artículo: la de venta o la de compra. */
|
||||
uom: 'sales' | 'purchase';
|
||||
}): DocumentLayout {
|
||||
const columns = lineColumnsFor(opts.taxResource, opts.lineColumns);
|
||||
const columns = lineColumnsFor(opts.taxResource, opts.lineColumns).map((c) =>
|
||||
c.key === 'ItemCode' && c.lookup ? itemColumnWithUom(c, opts.uom) : c,
|
||||
);
|
||||
const layout: DocumentLayout = {
|
||||
lines: { key: 'DocumentLines', fields: lineFieldsOf(columns), base: true },
|
||||
lines: { key: 'DocumentLines', fields: [...lineFieldsOf(columns), 'UoMEntry'], base: true },
|
||||
header: opts.header ?? {
|
||||
left: [
|
||||
{ type: 'partner', key: 'CardCode', label: opts.partnerLabel, required: true, missing: `el ${opts.partnerLabel.toLowerCase()}` },
|
||||
|
||||
@@ -91,7 +91,11 @@ export function linesPayload(lines: readonly GridLine[], spec: LinesSpec = MARKE
|
||||
.filter((l) => !isBlankLine(l))
|
||||
.map((l) => {
|
||||
const out: DocRecord = {};
|
||||
for (const f of spec.fields) if (!isEmpty(l[f])) out[f] = l[f];
|
||||
for (const f of spec.fields) {
|
||||
if (isEmpty(l[f])) continue;
|
||||
if (f === 'UoMEntry' && !(Number(l[f]) > 0)) continue; // -1 = unidad manual: SAP no admite enviarla
|
||||
out[f] = l[f];
|
||||
}
|
||||
if (spec.base && hasBase(l)) for (const f of BASE_FIELDS) out[f] = l[f];
|
||||
return out;
|
||||
});
|
||||
|
||||
@@ -17,13 +17,13 @@ export function recalcLine(line: GridLine): GridLine {
|
||||
* usuario no lo escribe no se envía y SAP aplica la lista de precios del socio de negocio.
|
||||
*/
|
||||
/**
|
||||
* Unidad de medida de un artículo con grupo de unidades, para las líneas de compra e inventario: la de
|
||||
* compra por defecto o, si no tiene, la de inventario. Sin ella SAP rechaza el documento ("specify a
|
||||
* Unidad de medida de un artículo con grupo de unidades, para las líneas de un documento: la de
|
||||
* compra (o de venta) por defecto o, si no tiene, la de inventario. Sin ella SAP rechaza el documento ("specify a
|
||||
* UoM code"); y a un artículo de unidad manual (sin grupo) SAP no admite ponérsela.
|
||||
*/
|
||||
export function uomEntryOf(item: Record<string, unknown>): number | undefined {
|
||||
export function uomEntryOf(item: Record<string, unknown>, side: 'purchase' | 'sales' = 'purchase'): number | undefined {
|
||||
if (!(Number(item['UoMGroupEntry']) > 0)) return undefined;
|
||||
for (const key of ['DefaultPurchasingUoMEntry', 'InventoryUoMEntry']) {
|
||||
for (const key of [side === 'sales' ? 'DefaultSalesUoMEntry' : 'DefaultPurchasingUoMEntry', 'InventoryUoMEntry']) {
|
||||
const entry = Number(item[key]);
|
||||
if (entry > 0) return entry;
|
||||
}
|
||||
@@ -58,7 +58,7 @@ export const DOCUMENT_LINE_COLUMNS: readonly GridColumn[] = [
|
||||
title: 'Artículos',
|
||||
resource: 'Items',
|
||||
orderby: 'ItemCode',
|
||||
extraFields: ['UoMGroupEntry', 'DefaultPurchasingUoMEntry', 'InventoryUoMEntry'],
|
||||
extraFields: ['UoMGroupEntry', 'DefaultPurchasingUoMEntry', 'DefaultSalesUoMEntry', 'InventoryUoMEntry'],
|
||||
columns: [
|
||||
{ key: 'ItemCode', title: 'Código', width: '150px', mono: true },
|
||||
{ key: 'ItemName', title: 'Descripción' },
|
||||
|
||||
@@ -184,6 +184,7 @@ describe('columnas de las listas de valores por documento', () => {
|
||||
const layout = marketingLayout({
|
||||
partnerLabel: 'Cliente',
|
||||
taxResource: 'SalesTaxCodes',
|
||||
uom: 'sales',
|
||||
lookups: {
|
||||
item: [{ key: 'ItemCode', title: 'Código' }, { key: 'ItemName', title: 'Descripción' }, { key: 'QuantityOnStock', title: 'Stock', type: 'number' }],
|
||||
partner: [{ key: 'CardCode', title: 'Código' }, { key: 'CardName', title: 'Nombre' }, { key: 'Phone1', title: 'Teléfono' }],
|
||||
|
||||
@@ -142,6 +142,7 @@ export const marketing = (
|
||||
header: extras.header,
|
||||
lineColumns: extras.lineColumns,
|
||||
lookups: extras.lookups,
|
||||
uom: path.startsWith('/ventas') ? 'sales' : 'purchase',
|
||||
}),
|
||||
resource,
|
||||
createLabel,
|
||||
|
||||
Reference in new issue
Block a user