From 6978fed6fbd3bf3449043c1af0eaca8d69283cc6 Mon Sep 17 00:00:00 2001 From: George Date: Wed, 14 Oct 2026 09:13:53 -0500 Subject: [PATCH] fix(web): fechas de SAP con hora, enlaces como botones y un solo maestro de impuestos MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Probado contra el SAP real (solo lectura): órdenes de venta y facturas de proveedor. Co-Authored-By: Claude Sonnet 5.5 --- CLAUDE.md | 2 +- apps/api/src/sl/sl-resources.ts | 1 - apps/web/README.md | 2 +- .../app/features/documents/document-form.page.spec.ts | 9 +++++++-- .../src/app/features/documents/document-form.page.ts | 9 ++++++++- .../web/src/app/features/documents/document-screens.ts | 9 +++++++-- apps/web/src/app/features/documents/line-columns.ts | 2 +- apps/web/src/styles/forms.css | 10 +++++++++- 8 files changed, 34 insertions(+), 10 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 3f96032..d8c24ff 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -56,7 +56,7 @@ Cada menú abre primero una **lista de los últimos documentos** (barra de filtr Hecho (commits en `main`): monorepo · API (config, capa SAP con sesión compartida y cola, login, permisos SQLite, proxy `/api/sl`) · web (temas, login, layout con menú por permisos, `sbo-value-help`, campos y filas compuestas, listas con filtros, ventana/toolbar/pestañas/menús, grilla de líneas, `/dev/componentes`). Pendiente, en orden: -1. **Paso 9 — formulario de documentos:** hecho para ventas y compras (`document-form.page`, `document-model.ts`: cargar, crear, actualizar con `If-Match`/412, navegación, totales con `SalesTaxCodes`/`PurchaseTaxCodes`, solo lectura), probado solo con SAP simulado y **sin verlo aún en el navegador**. Falta: probar escrituras reales (pedir autorización), "Copiar de/a", botón PDF, fila del flete, toolbar de ancho completo, formularios de inventario y solicitudes de compra. +1. **Paso 9 — formulario de documentos:** hecho para ventas y compras (`document-form.page`, `document-model.ts`: cargar, crear, actualizar con `If-Match`/412, navegación, totales con `SalesTaxCodes` (también en compras), solo lectura), probado solo con SAP simulado y **sin verlo aún en el navegador**. Falta: probar escrituras reales (pedir autorización), "Copiar de/a", botón PDF, fila del flete, toolbar de ancho completo, formularios de inventario y solicitudes de compra. 2. Pantallas de maestros (`Items`, `BusinessPartners`: Clientes/Proveedores, Artículos), hoy "no disponible". 3. ~~Pantalla web de permisos~~ — hecha (`/admin/permisos`). 4. Auditoría (`audit_log` en SQLite; hoy solo líneas `AUDIT` en el log) y `nestjs-cls` para el usuario en los logs. diff --git a/apps/api/src/sl/sl-resources.ts b/apps/api/src/sl/sl-resources.ts index 96cf325..b2235ed 100644 --- a/apps/api/src/sl/sl-resources.ts +++ b/apps/api/src/sl/sl-resources.ts @@ -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'), diff --git a/apps/web/README.md b/apps/web/README.md index 1e2fb11..2a62112 100644 --- a/apps/web/README.md +++ b/apps/web/README.md @@ -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). diff --git a/apps/web/src/app/features/documents/document-form.page.spec.ts b/apps/web/src/app/features/documents/document-form.page.spec.ts index ece6c1c..64076bb 100644 --- a/apps/web/src/app/features/documents/document-form.page.spec.ts +++ b/apps/web/src/app/features/documents/document-form.page.spec.ts @@ -16,8 +16,8 @@ const order = (over: Record = {}) => ({ 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(); diff --git a/apps/web/src/app/features/documents/document-form.page.ts b/apps/web/src/app/features/documents/document-form.page.ts index fdf185d..dc15a0e 100644 --- a/apps/web/src/app/features/documents/document-form.page.ts +++ b/apps/web/src/app/features/documents/document-form.page.ts @@ -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)); diff --git a/apps/web/src/app/features/documents/document-screens.ts b/apps/web/src/app/features/documents/document-screens.ts index 29abdc7..facb8b7 100644 --- a/apps/web/src/app/features/documents/document-screens.ts +++ b/apps/web/src/app/features/documents/document-screens.ts @@ -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, diff --git a/apps/web/src/app/features/documents/line-columns.ts b/apps/web/src/app/features/documents/line-columns.ts index fef5aaf..c6c13b1 100644 --- a/apps/web/src/app/features/documents/line-columns.ts +++ b/apps/web/src/app/features/documents/line-columns.ts @@ -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) => diff --git a/apps/web/src/styles/forms.css b/apps/web/src/styles/forms.css index c7b3494..d49b2f4 100644 --- a/apps/web/src/styles/forms.css +++ b/apps/web/src/styles/forms.css @@ -85,7 +85,15 @@ } } -/* Botones de acción */ +/* Botones de acción (también como enlaces: ``, ``) */ +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;