fix(web): fechas de SAP con hora, enlaces como botones y un solo maestro de impuestos

Probado contra el SAP real (solo lectura): órdenes de venta y facturas de proveedor.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
GeorgeandClaude Sonnet 5.5 committed 2026-10-14 09:13:53 -05:00
1 parent 4d4df17d07
commit 6978fed6fb
8 files changed
+34 -10

No files matched your search

-1
View File
@@ -69,7 +69,6 @@ export const SL_RESOURCES: readonly SlResource[] = [
catalog('Warehouses', 'string'),
catalog('Currencies', 'string'),
catalog('SalesTaxCodes', 'string'),
catalog('PurchaseTaxCodes', 'string'),
catalog('VatGroups', 'string'),
catalog('PaymentTermsTypes', 'number'),
catalog('SalesPersons', 'number'),
+1 -1
View File
@@ -128,7 +128,7 @@ Las tres rutas las gobierna el permiso de menú de la ruta padre. Se generan sol
Lo usan las pantallas con `partner` en su `DocumentScreen` (ventas y compras); el resto de documentos muestra aún un aviso.
- **Cabecera**: socio de negocio (lista de valores filtrada por `CardType`; no se cambia en un documento ya creado), Nº de referencia, fechas y descuento %; **líneas** con `sbo-lines-grid`; **totales** informativos (SAP recalcula al guardar) con las tasas de `SalesTaxCodes` (ventas) o `PurchaseTaxCodes` (compras).
- **Cabecera**: socio de negocio (lista de valores filtrada por `CardType`; no se cambia en un documento ya creado), Nº de referencia, fechas y descuento %; **líneas** con `sbo-lines-grid`; **totales** informativos (SAP recalcula al guardar) con las tasas de `SalesTaxCodes` (también en compras: el Service Layer no tiene un maestro propio; `partner.taxResource` permite cambiarlo en una rama de cliente).
- **Crear**: valida sin SAP (socio, fecha, al menos una línea con artículo, cantidad > 0), quita campos vacíos y filas en blanco, y abre el documento creado.
- **Actualizar**: envía solo la cabecera que cambió y, si cambiaron las líneas, todas con `B1S-ReplaceCollectionsOnPatch: true`. Lleva `If-Match` con el `odata.etag` leído: si otro usuario cambió el documento, SAP responde **412**, no se guarda nada y se ofrece "Recargar".
- **Cerrado o cancelado**: solo lectura (sin botón de guardar).
@@ -16,8 +16,8 @@ const order = (over: Record<string, unknown> = {}) => ({
CardCode: 'C1',
CardName: 'ACME',
NumAtCard: '',
DocDate: '2026-09-11',
DocDueDate: '2026-09-20',
DocDate: '2026-09-11T00:00:00Z',
DocDueDate: '2026-09-20T00:00:00Z',
DiscountPercent: 0,
Comments: 'hola',
DocumentStatus: 'bost_Open',
@@ -77,6 +77,11 @@ describe('DocumentFormPage', () => {
expect(buttonText()).toContain('Actualizar');
});
it('las fechas de SAP (con hora) se muestran y se comparan como AAAA-MM-DD', () => {
expect(page().header().DocDate).toBe('2026-09-11');
expect(page().dirty()).toBe(false);
});
it('sin cambios, Actualizar no envía nada', async () => {
(el().querySelector('.actions .btn.main') as HTMLElement).click();
await tick();
@@ -30,9 +30,16 @@ import { ITEMS_MULTI_ADD, lineColumnsFor, newDocumentLine, recalcLine } from './
const HEADER_KEYS = ['CardCode', 'NumAtCard', 'DocDate', 'DocDueDate', 'DiscountPercent', 'Comments'] as const;
const today = (): string => new Date().toISOString().slice(0, 10);
/** SAP devuelve las fechas como `2026-01-05T00:00:00Z`; el campo de fecha y el guardado usan `2026-01-05`. */
const dateOnly = (v: unknown): string => (typeof v === 'string' ? v.slice(0, 10) : '');
const pickHeader = (doc: DocRecord | undefined): DocRecord => {
if (!doc) return { DocDate: today(), DocDueDate: today(), DiscountPercent: 0 };
return Object.fromEntries(HEADER_KEYS.map((k) => [k, doc[k] ?? (k === 'DiscountPercent' ? 0 : '')]));
return Object.fromEntries(
HEADER_KEYS.map((k) => [
k,
k === 'DocDate' || k === 'DocDueDate' ? dateOnly(doc[k]) : (doc[k] ?? (k === 'DiscountPercent' ? 0 : '')),
]),
);
};
const text = (v: unknown): string => (v === null || v === undefined ? '' : String(v));
@@ -17,7 +17,12 @@ export interface DocumentScreen {
/** Los más recientes primero. */
orderby: string;
/** Formulario de documentos con socio de negocio, líneas y totales (ventas y compras). */
partner?: { label: string; type: 'cCustomer' | 'cSupplier'; taxResource: 'SalesTaxCodes' | 'PurchaseTaxCodes' };
partner?: {
label: string;
type: 'cCustomer' | 'cSupplier';
/** Maestro de impuestos (el Service Layer no tiene uno propio de compras: en Chile ambos usan `SalesTaxCodes`). */
taxResource: string;
};
}
export const menuLabel = (path: string): string =>
@@ -91,7 +96,7 @@ const marketing = (path: string, resource: string, createLabel: string, partner:
orderby: 'DocEntry desc',
partner: path.startsWith('/ventas')
? { label: partner, type: 'cCustomer', taxResource: 'SalesTaxCodes' }
: { label: partner, type: 'cSupplier', taxResource: 'PurchaseTaxCodes' },
: { label: partner, type: 'cSupplier', taxResource: 'SalesTaxCodes' },
extraFields: ['Cancelled', 'DocTotalFc', 'DocCurrency'],
columns: [
docNum,
@@ -93,7 +93,7 @@ export const DOCUMENT_LINE_COLUMNS: readonly GridColumn[] = [
{ key: 'LineTotal', title: 'Total', type: 'number', decimals: 2, width: '110px', readonly: true },
];
/** Columnas de línea con el maestro de impuestos que corresponde (ventas: `SalesTaxCodes`; compras: `PurchaseTaxCodes`). */
/** Columnas de línea con el maestro de impuestos que corresponde (por defecto `SalesTaxCodes`). */
export function lineColumnsFor(taxResource: string): readonly GridColumn[] {
if (taxResource === 'SalesTaxCodes') return DOCUMENT_LINE_COLUMNS;
return DOCUMENT_LINE_COLUMNS.map((c) =>
+9 -1
View File
@@ -85,7 +85,15 @@
}
}
/* Botones de acción */
/* Botones de acción (también como enlaces: `<a class="btn">`, `<a class="tb">`) */
a.btn,
a.tb {
display: inline-flex;
align-items: center;
justify-content: center;
text-decoration: none;
color: var(--ink);
}
.btn {
height: 26px;
min-width: 84px;