diff --git a/CLAUDE.md b/CLAUDE.md index 34bb6e2..96176ce 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -52,7 +52,7 @@ Es un ERP, no una página de publicidad: **ligera y rápida**. Sin librerías de ## Flujo de las pantallas de documento -Cada menú abre primero una **lista de los últimos documentos** (barra de filtros + búsqueda por columna); desde ahí se crea uno nuevo (`/…/nuevo`, formulario vacío) o se abre uno existente (`/…/:docEntry`, ver/editar; los cerrados/cancelados en solo lectura). Las pantallas se definen por módulo en `apps/web/src/app/features/documents/` (17 documentos): `ventas/ventas.screens.ts`, `compras/compras.screens.ts` (+ `compras.layouts.ts`) e `inventario/inventario.screens.ts` (+ `inventario.layouts.ts`); los maestros (socios de negocio, artículos) irán en su propia carpeta. `screen-kit.ts` tiene el tipo `DocumentScreen` y las columnas y filtros comunes; `document-screens.ts` las reúne en `DOCUMENT_SCREENS`. Las columnas de línea estándar están en `features/documents/line-columns.ts`. **Tipo Artículo / Servicio** (`DocType`): los documentos de ventas y compras con socio llevan en la cabecera la lista «Tipo» (primera de la columna izquierda; no se cambia en uno guardado). Con «Servicio» las líneas pasan a `layout.service` (`serviceLineColumns` en `line-columns.ts`): Cuenta contable (obligatoria), Descripción, Impuesto y Total (se escribe, sin recalcular), dimensiones, proyecto y referencias al origen; sin artículo, cantidad, almacén, precio, lotes ni «Elegir artículos». Se guardan `AccountCode`, `ItemDescription`, `TaxCode`, `LineTotal` (`LinesSpec.service`); cambiar el tipo con líneas escritas pide permiso y las descarta; «Copiar de» trae el tipo y el importe abierto (`OpenAmount`) del origen y no mezcla tipos. Las extensiones de cliente de columnas de línea aplican a las líneas de artículos; las de servicio aún no son extensibles. **Indicador y folio juntos y en ese orden** (`IDENTITY_GROUP`, un `HeaderField` tipo `group` = varias celdas en una fila: Indicador con su lista, prefijo y número del folio) en facturas, boletas, reservas y notas de crédito de ventas y compras (`BILLING_DOCUMENT`) y en el asiento. Una empresa que los sustituye por campos de usuario arma su grupo con `fieldGroup(label, cells)` en `headerRight` de su extensión y oculta el del base con `hideHeaderFields: ['Indicator']` (oculta el grupo completo). Las **tres fechas** de la cabecera de todo documento de ventas y compras van juntas y en este orden, una debajo de la otra: contabilización, vencimiento y del documento (`TaxDate`), las tres con la fecha de hoy al crear. **Pestañas Contenido, Logística y Finanzas** en ventas y compras (`layout.tabs`, `tools/logistica.png` y `tools/finanzas.png`): Logística = destino y dirección de facturación (se eligen de las direcciones del socio, con su texto debajo), forma de envío, número de seguimiento y bloquear reclamaciones; Finanzas = entrada en el diario, cuenta asociada, bloqueo de pago, condiciones y forma de pago (de cobro en ventas, de pago en compras), indicador banco central, plazos, recálculo del vencimiento y proyecto. Al elegir el socio se cargan sus contactos y direcciones (`BusinessPartners(cod)` con `BPAddresses` y `ContactEmployees`) y se prellenan las direcciones, condiciones, forma de pago y cuenta asociada que trae (solo si el campo está vacío). La persona de contacto va bajo el nombre del socio. Las transferencias de stock y las solicitudes de traslado también llevan socio, contacto, destino, lista de precios (`PriceList`) y empleado (`SalesPersonCode`). Los códigos numéricos de las listas se guardan como número (`typedLookup`). Las extensiones de cliente pueden agregar campos a las pestañas (`logisticsLeft/Right`, `financeLeft/Right`). Sin verificar: «Responsable» de Logística (no se identificó su campo del Service Layer), canal del socio, cuota/consolidación de Finanzas. **Solicitudes de compra** (`/compras/solicitudes`, `tools/solicitud de compra.png`) son documentos de marketing sin socio: `marketingLayout({ requester: true })` — cabecera con tipo, solicitante (usuario de SAP: al elegirlo se completan nombre, correo, departamento y sucursal; `ReqType` 12), «Válido hasta», fecha necesaria e indicador/proyecto; líneas con proveedor sugerido (`LineVendor`), fecha necesaria por línea (columna `date` de la grilla) y cantidad/precio/descuento/impuesto/total/almacén; pie con gastos adicionales y totales; sin moneda, redondeo ni pestañas. Para el solicitante la API registra `Users` con `selectOnly` (`UserCode`, `UserName`, `eMail`, `Department`, `Branch`: `$select` obligatorio y limitado; la entidad trae contraseña y permisos), más `Departments` y `Branches`. Los documentos sin socio indican su maestro de impuestos con `DocumentScreen.taxResource`. **La dirección del documento** (`AddressExtension`: `ShipTo…` y `BillTo…`, `address-fields.ts`): el texto bajo el destino y la dirección de facturación se arma con sus partes (calle, número, bloque, edificio, ciudad, código postal, provincia, región, país) del documento guardado o, si se eligió otra, de la dirección del socio; el botón «…» abre una ventana (`address-dialog.ts`) para corregirlas en este documento y solo lo que cambió viaja en `AddressExtension`; elegir otra dirección descarta las correcciones. Las celdas de lista con texto propio para el código (`VhLookup.codes`: la cuenta asociada con su código con formato) lo muestran también en los campos de cabecera (`sbo-lookup-field`). **Cuenta asociada por grupo:** en ventas solo se ofrecen las cuentas asociadas de activo y en compras las de pasivo. El Service Layer no expone `GroupMask` (ni como campo ni como filtro) ni deja filtrar por cuenta padre, y limita la consulta a 2048 caracteres, así que `ControlAccountsService` pide las asociadas (`LockManualTransaction = tYES`), sube por sus cuentas padre en lotes de 40 hasta la de primer nivel y las agrupa por su primer carácter (`1…` activos, `2…` pasivos); se calcula una vez por sesión y la lista se busca y pagina en el navegador (`VhLookup.rows`, `ValueHelp.localRows`). En el asiento la «Cuenta» de un socio aún ofrece todas las asociadas. +Cada menú abre primero una **lista de los últimos documentos** (barra de filtros + búsqueda por columna); desde ahí se crea uno nuevo (`/…/nuevo`, formulario vacío) o se abre uno existente (`/…/:docEntry`, ver/editar; los cerrados/cancelados en solo lectura). Las pantallas se definen por módulo en `apps/web/src/app/features/documents/` (17 documentos): `ventas/ventas.screens.ts`, `compras/compras.screens.ts` (+ `compras.layouts.ts`) e `inventario/inventario.screens.ts` (+ `inventario.layouts.ts`); los maestros (socios de negocio, artículos) irán en su propia carpeta. `screen-kit.ts` tiene el tipo `DocumentScreen` y las columnas y filtros comunes; `document-screens.ts` las reúne en `DOCUMENT_SCREENS`. Las columnas de línea estándar están en `features/documents/line-columns.ts`. **Tipo Artículo / Servicio** (`DocType`): los documentos de ventas y compras con socio llevan en la cabecera la lista «Tipo» (primera de la columna izquierda; no se cambia en uno guardado). Con «Servicio» las líneas pasan a `layout.service` (`serviceLineColumns` en `line-columns.ts`): Cuenta contable (obligatoria), Descripción, Impuesto y Total (se escribe, sin recalcular), dimensiones, proyecto y referencias al origen; sin artículo, cantidad, almacén, precio, lotes ni «Elegir artículos». Se guardan `AccountCode`, `ItemDescription`, `TaxCode`, `LineTotal` (`LinesSpec.service`); cambiar el tipo con líneas escritas pide permiso y las descarta; «Copiar de» trae el tipo y el importe abierto (`OpenAmount`) del origen y no mezcla tipos. Las extensiones de cliente de columnas de línea aplican a las líneas de artículos; las de servicio aún no son extensibles. **Indicador y folio juntos y en ese orden** (`IDENTITY_GROUP`, un `HeaderField` tipo `group` = varias celdas en una fila: Indicador con su lista, prefijo y número del folio) en facturas, boletas, reservas y notas de crédito de ventas y compras (`BILLING_DOCUMENT`) y en el asiento. Una empresa que los sustituye por campos de usuario arma su grupo con `fieldGroup(label, cells)` en `headerRight` de su extensión y oculta el del base con `hideHeaderFields: ['Indicator']` (oculta el grupo completo). Las **tres fechas** de la cabecera de todo documento de ventas y compras van juntas y en este orden, una debajo de la otra: contabilización, vencimiento y del documento (`TaxDate`), las tres con la fecha de hoy al crear. **Pestañas Contenido, Logística y Finanzas** en ventas y compras (`layout.tabs`, `tools/logistica.png` y `tools/finanzas.png`): Logística = destino y dirección de facturación (se eligen de las direcciones del socio, con su texto debajo), forma de envío, número de seguimiento y bloquear reclamaciones; Finanzas = entrada en el diario, cuenta asociada, bloqueo de pago, condiciones y forma de pago (de cobro en ventas, de pago en compras), indicador banco central, plazos, recálculo del vencimiento y proyecto. Al elegir el socio se cargan sus contactos y direcciones (`BusinessPartners(cod)` con `BPAddresses` y `ContactEmployees`) y se prellenan las direcciones, condiciones, forma de pago y cuenta asociada que trae (solo si el campo está vacío). La persona de contacto va bajo el nombre del socio. Las transferencias de stock y las solicitudes de traslado también llevan socio, contacto, destino, lista de precios (`PriceList`) y empleado (`SalesPersonCode`). Los códigos numéricos de las listas se guardan como número (`typedLookup`). Las extensiones de cliente pueden agregar campos a las pestañas (`logisticsLeft/Right`, `financeLeft/Right`). Sin verificar: «Responsable» de Logística (no se identificó su campo del Service Layer), canal del socio, cuota/consolidación de Finanzas. **Solicitudes de compra** (`/compras/solicitudes`, `tools/solicitud de compra.png`) son documentos de marketing sin socio: `marketingLayout({ requester: true })` — cabecera con tipo, solicitante (usuario de SAP: al elegirlo se completan nombre, correo, departamento y sucursal; `ReqType` 12), «Válido hasta», fecha necesaria e indicador/proyecto; líneas con proveedor sugerido (`LineVendor`), fecha necesaria por línea (columna `date` de la grilla) y cantidad/precio/descuento/impuesto/total/almacén; pie con gastos adicionales y totales; sin moneda, redondeo ni pestañas. Para el solicitante la API registra `Users` con `selectOnly` (`UserCode`, `UserName`, `eMail`, `Department`, `Branch`: `$select` obligatorio y limitado; la entidad trae contraseña y permisos), más `Departments` y `Branches`. Los documentos sin socio indican su maestro de impuestos con `DocumentScreen.taxResource`. **La dirección del documento** (`AddressExtension`: `ShipTo…` y `BillTo…`, `address-fields.ts`): el texto bajo el destino y la dirección de facturación se arma con sus partes (calle, número, bloque, edificio, ciudad, código postal, provincia, región, país) del documento guardado o, si se eligió otra, de la dirección del socio; el botón «…» abre una ventana (`address-dialog.ts`) para corregirlas en este documento y solo lo que cambió viaja en `AddressExtension`; elegir otra dirección descarta las correcciones. Las celdas de lista con texto propio para el código (`VhLookup.codes`: la cuenta asociada con su código con formato) lo muestran también en los campos de cabecera (`sbo-lookup-field`). **Cuenta asociada por grupo:** en ventas solo se ofrecen las cuentas asociadas de activo y en compras las de pasivo. El Service Layer no expone `GroupMask` (ni como campo ni como filtro) ni deja filtrar por cuenta padre, y limita la consulta a 2048 caracteres, así que `ControlAccountsService` pide las asociadas (`LockManualTransaction = tYES`), sube por sus cuentas padre en lotes de 40 hasta la de primer nivel y las agrupa por su primer carácter (`1…` activos, `2…` pasivos); se calcula una vez por sesión y la lista se busca y pagina en el navegador (`VhLookup.rows`, `ValueHelp.localRows`). En el asiento la «Cuenta» de un socio aún ofrece todas las asociadas. **Cancelados:** en SAP un documento cancelado (`Cancelled = tYES`, `CancelStatus = csYes`) queda cerrado y SAP crea otro, el documento de cancelación (`CancelStatus = csCancellation`), también cerrado. Las listas de ventas y compras lo muestran en «Estado» como «Cancelado» y «Cancelación» y tienen el filtro «Cancelación» (cancelados / documentos de cancelación). El enlace entre la pareja no es un campo: el documento de cancelación nombra al cancelado en sus comentarios («Basado en … 260013973.»), así que los botones «Ver su cancelación» / «Ver el documento cancelado» del formulario lo buscan por ese número, el socio y el orden. ## Cómo verificar cambios de interfaz diff --git a/apps/web/src/app/core/sl/sl-document.ts b/apps/web/src/app/core/sl/sl-document.ts index 8c6d2f5..e262763 100644 --- a/apps/web/src/app/core/sl/sl-document.ts +++ b/apps/web/src/app/core/sl/sl-document.ts @@ -41,6 +41,14 @@ export class SlDocumentApi { return firstValueFrom(this.http.post(`${API_URL}/sl/SBOBobService_GetCurrencyRate`, { Currency: currency, Date: date })); } + /** `DocEntry` del primer documento que cumple el filtro (en el orden dado), o `null` si no hay. */ + async findOne(resource: string, filter: string, orderby: string): Promise { + const params = new HttpParams().set('$select', 'DocEntry').set('$filter', filter).set('$orderby', orderby).set('$top', 1); + const res = await firstValueFrom(this.http.get>(this.url(resource), { params })); + const entry = res.value[0]?.['DocEntry']; + return typeof entry === 'number' ? entry : null; + } + /** Crea el documento; SAP devuelve el documento creado (con `DocEntry` y `DocNum`). */ create(resource: string, body: DocRecord): Promise { return firstValueFrom(this.http.post(this.url(resource), body)); diff --git a/apps/web/src/app/features/documents/compras/compras.screens.ts b/apps/web/src/app/features/documents/compras/compras.screens.ts index 2d01cf8..8ec03c7 100644 --- a/apps/web/src/app/features/documents/compras/compras.screens.ts +++ b/apps/web/src/app/features/documents/compras/compras.screens.ts @@ -11,7 +11,7 @@ export const COMPRAS_SCREENS: readonly DocumentScreen[] = [ resource: 'PurchaseRequests', createLabel: 'Nueva solicitud', orderby: 'DocEntry desc', - extraFields: ['Cancelled'], + extraFields: ['Cancelled', 'CancelStatus'], columns: [ docNum, statusColumn, diff --git a/apps/web/src/app/features/documents/document-cancellation.spec.ts b/apps/web/src/app/features/documents/document-cancellation.spec.ts new file mode 100644 index 0000000..f9e23e0 --- /dev/null +++ b/apps/web/src/app/features/documents/document-cancellation.spec.ts @@ -0,0 +1,101 @@ +import { provideHttpClient } from '@angular/common/http'; +import { HttpTestingController, provideHttpClientTesting } from '@angular/common/http/testing'; +import { type ComponentFixture, TestBed } from '@angular/core/testing'; +import { ActivatedRoute, Router, convertToParamMap, provideRouter } from '@angular/router'; +import { of } from 'rxjs'; +import { beforeEach, describe, expect, it, vi } from 'vitest'; +import { DocumentFormPage } from './document-form.page'; +import { DOCUMENT_SCREENS } from './document-screens'; +import { documentStatus } from './screen-kit'; + +describe('estado de los documentos cancelados', () => { + it('el cancelado y su documento de cancelación se distinguen del resto', () => { + expect(documentStatus({ Cancelled: 'tYES', DocumentStatus: 'bost_Close' })).toEqual({ label: 'Cancelado', tone: 'err' }); + expect(documentStatus({ Cancelled: 'tNO', CancelStatus: 'csCancellation', DocumentStatus: 'bost_Close' })).toEqual({ label: 'Cancelación', tone: 'warn' }); + expect(documentStatus({ Cancelled: 'tNO', CancelStatus: 'csNo', DocumentStatus: 'bost_Close' })).toEqual({ label: 'Cerrado', tone: 'muted' }); + }); + + it('las listas de ventas y compras filtran por cancelación y piden CancelStatus', () => { + for (const path of ['/ventas/facturas', '/compras/facturas', '/ventas/notas-credito']) { + const s = DOCUMENT_SCREENS.find((x) => x.path === path)!; + const f = s.filters.find((x) => x.key === 'CancelStatus'); + expect(f && f.type === 'select' && f.options.map((o) => o.value), path).toEqual(['csYes', 'csCancellation']); + expect(s.extraFields, path).toContain('CancelStatus'); + } + expect(DOCUMENT_SCREENS.find((s) => s.path === '/inventario/entradas')!.filters.some((x) => x.key === 'CancelStatus')).toBe(false); + }); +}); + +describe('del documento cancelado a su cancelación y al revés', () => { + let fixture: ComponentFixture; + let ctrl: HttpTestingController; + let navigate: ReturnType; + const el = () => fixture.nativeElement as HTMLElement; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const page = () => fixture.componentInstance as any; + const tick = async () => { + fixture.detectChanges(); + await new Promise((r) => setTimeout(r)); + fixture.detectChanges(); + }; + + async function open(doc: Record) { + const screen = DOCUMENT_SCREENS.find((s) => s.path === '/compras/facturas')!; + const route = { snapshot: { data: { screen, title: 'Documento' }, paramMap: convertToParamMap({ docEntry: '5' }) }, paramMap: of(convertToParamMap({ docEntry: '5' })) }; + await TestBed.configureTestingModule({ + imports: [DocumentFormPage], + providers: [provideRouter([]), provideHttpClient(), provideHttpClientTesting(), { provide: ActivatedRoute, useValue: route }], + }).compileComponents(); + ctrl = TestBed.inject(HttpTestingController); + navigate = vi.spyOn(TestBed.inject(Router), 'navigate').mockResolvedValue(true); + fixture = TestBed.createComponent(DocumentFormPage); + await tick(); + ctrl.expectOne((r) => r.url === '/api/sl/SalesTaxCodes').flush({ value: [] }); + ctrl.expectOne((r) => r.url === '/api/sl/AdditionalExpenses').flush({ value: [] }); + ctrl.expectOne((r) => r.url === '/api/sl/Currencies').flush({ value: [] }); + ctrl.expectOne('/api/sl/PurchaseInvoices/5').flush({ DocEntry: 5, DocNum: 260013973, CardCode: 'P1', DocDate: '2026-10-19T00:00:00Z', DocumentLines: [], DocumentStatus: 'bost_Close', ...doc }); + await tick(); + } + + const button = () => [...el().querySelectorAll('.actions button')].find((b) => /cancelaci|cancelado/i.test(b.textContent ?? '')); + beforeEach(() => TestBed.resetTestingModule()); + + it('un documento normal no ofrece ver cancelaciones', async () => { + await open({ Cancelled: 'tNO', CancelStatus: 'csNo' }); + expect(button()).toBeUndefined(); + }); + + it('el cancelado busca su documento de cancelación por su número en los comentarios del otro', async () => { + await open({ Cancelled: 'tYES', CancelStatus: 'csYes' }); + expect(button()!.textContent).toContain('Ver su cancelación'); + button()!.click(); + await tick(); + const req = ctrl.expectOne((r) => r.url === '/api/sl/PurchaseInvoices' && (r.params.get('$filter') ?? '').includes("contains(Comments,'260013973')")); + expect(req.request.params.get('$filter')).toContain("CancelStatus eq 'csCancellation' and CardCode eq 'P1'"); + req.flush({ value: [{ DocEntry: 6 }] }); + await tick(); + expect(navigate).toHaveBeenCalledWith(['..', 6], expect.anything()); + }); + + it('el documento de cancelación abre el documento que nombra en sus comentarios', async () => { + await open({ Cancelled: 'tNO', CancelStatus: 'csCancellation', Comments: 'COPAS Basado en Fact.proveedores 260013972.' }); + expect(button()!.textContent).toContain('Ver el documento cancelado'); + button()!.click(); + await tick(); + const req = ctrl.expectOne((r) => r.url === '/api/sl/PurchaseInvoices' && (r.params.get('$filter') ?? '').includes('DocNum eq 260013972')); + req.flush({ value: [{ DocEntry: 4 }] }); + await tick(); + expect(navigate).toHaveBeenCalledWith(['..', 4], expect.anything()); + }); + + it('si no encuentra la pareja lo avisa', async () => { + await open({ Cancelled: 'tYES', CancelStatus: 'csYes' }); + button()!.click(); + await tick(); + ctrl.expectOne((r) => (r.params.get('$filter') ?? '').includes('contains(Comments')).flush({ value: [] }); + await tick(); + ctrl.expectOne((r) => (r.params.get('$filter') ?? '').includes('DocEntry gt 5')).flush({ value: [] }); + await tick(); + expect(el().querySelector('.status')!.textContent).toContain('No se encontró su documento de cancelación'); + }); +}); diff --git a/apps/web/src/app/features/documents/document-form.page.html b/apps/web/src/app/features/documents/document-form.page.html index a8a2e97..d81b241 100644 --- a/apps/web/src/app/features/documents/document-form.page.html +++ b/apps/web/src/app/features/documents/document-form.page.html @@ -82,6 +82,9 @@ } {{ readOnly() ? 'Cerrar' : 'Cancelar' }} + @if (cancellation(); as c) { + + } @if (copyFromItems().length) { } 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 a73f30d..5531809 100644 --- a/apps/web/src/app/features/documents/document-form.page.ts +++ b/apps/web/src/app/features/documents/document-form.page.ts @@ -517,6 +517,46 @@ export class DocumentFormPage { /** Tipo al que se quiere cambiar y que espera confirmación porque hay líneas escritas que se descartarían. */ protected readonly pendingType = signal(null); + // --- cancelación: del documento cancelado a su documento de cancelación y al revés --------------- + + /** El documento abierto es un cancelado o el documento de cancelación que SAP creó al cancelarlo. */ + protected readonly cancellation = computed<'cancelled' | 'reversal' | null>(() => { + const d = this.doc(); + return d?.['Cancelled'] === 'tYES' ? 'cancelled' : d?.['CancelStatus'] === 'csCancellation' ? 'reversal' : null; + }); + + /** + * Abre el otro documento de la pareja. SAP no guarda el enlace en un campo: el documento de cancelación nombra al + * cancelado en sus comentarios («Basado en … 260013973.») y es posterior y del mismo socio; así se busca. + */ + protected async openCancellationPair(): Promise { + const d = this.doc(); + const mine = this.cancellation(); + if (!d || !mine) return; + const card = `CardCode eq '${text(d['CardCode']).replace(/'/g, "''")}'`; + const base = this.screen.baseFilter; + const withBase = (f: string): string => (base ? `(${base}) and ${f}` : f); + try { + let entry: number | null; + if (mine === 'cancelled') { + entry = await this.api.findOne(this.screen.resource, withBase(`CancelStatus eq 'csCancellation' and ${card} and contains(Comments,'${text(d['DocNum'])}')`), 'DocEntry asc'); + entry ??= await this.api.findOne(this.screen.resource, withBase(`CancelStatus eq 'csCancellation' and ${card} and DocEntry gt ${this.docEntry()}`), 'DocEntry asc'); + } else { + // el número del cancelado es el último que nombran los comentarios («Basado en Fact.proveedores 260013973.») + const named = [...text(d['Comments']).matchAll(/(\d{5,})/g)].at(-1)?.[1]; + entry = named ? await this.api.findOne(this.screen.resource, withBase(`Cancelled eq 'tYES' and ${card} and DocNum eq ${named}`), 'DocEntry desc') : null; + entry ??= await this.api.findOne(this.screen.resource, withBase(`Cancelled eq 'tYES' and ${card} and DocEntry lt ${this.docEntry()}`), 'DocEntry desc'); + } + if (entry === null) { + this.message.set({ kind: 'warn', text: mine === 'cancelled' ? 'No se encontró su documento de cancelación.' : 'No se encontró el documento que cancela.' }); + return; + } + await this.router.navigate(['..', entry], { relativeTo: this.route }); + } catch (e) { + this.message.set({ kind: 'err', text: apiErrorMessage(e) }); + } + } + // --- cuenta asociada: de activo en ventas y de pasivo en compras ------------------------------------ /** diff --git a/apps/web/src/app/features/documents/screen-kit.ts b/apps/web/src/app/features/documents/screen-kit.ts index d36be1a..12eca9e 100644 --- a/apps/web/src/app/features/documents/screen-kit.ts +++ b/apps/web/src/app/features/documents/screen-kit.ts @@ -39,6 +39,8 @@ export const menuLabel = (path: string): string => export function documentStatus(row: ListRow): { label: string; tone: Tone } | undefined { if (row['Cancelled'] === 'tYES') return { label: 'Cancelado', tone: 'err' }; + // el documento que SAP crea al cancelar otro (su reverso): también está cerrado, pero no es un documento más + if (row['CancelStatus'] === 'csCancellation') return { label: 'Cancelación', tone: 'warn' }; switch (row['DocumentStatus']) { case 'bost_Open': return { label: 'Abierto', tone: 'ok' }; @@ -129,6 +131,16 @@ export const docTypeFilter: ListFilter = { { value: 'dDocument_Service', label: 'Servicio' }, ], }; +/** Cancelaciones: los documentos cancelados y los documentos de cancelación que SAP crea al cancelarlos (por defecto, todos). */ +export const cancelFilter: ListFilter = { + type: 'select', + key: 'CancelStatus', + label: 'Cancelación', + options: [ + { value: 'csYes', label: 'Cancelados' }, + { value: 'csCancellation', label: 'Documentos de cancelación' }, + ], +}; export const dateFilter: ListFilter = { type: 'date', key: 'DocDate', label: 'Fecha contab.' }; export const currencyFilter: ListFilter = { type: 'multi', @@ -195,7 +207,7 @@ export const marketing = ( partner: path.startsWith('/ventas') ? { label: partner, type: 'cCustomer', taxResource: 'SalesTaxCodes' } : { label: partner, type: 'cSupplier', taxResource: 'SalesTaxCodes' }, - extraFields: ['Cancelled', 'DocTotalFc', 'DocCurrency'], + extraFields: ['Cancelled', 'CancelStatus', 'DocTotalFc', 'DocCurrency'], columns: [ docNum, statusColumn, @@ -207,7 +219,7 @@ export const marketing = ( { key: 'DocDueDate', title: 'Fecha venc.', type: 'date', align: 'center', width: '115px' }, { key: 'DocTotal', title: 'Total', align: 'right', width: '140px', format: documentTotal }, ], - filters: [statusFilter, docTypeFilter, currencyFilter, dateFilter], + filters: [statusFilter, cancelFilter, docTypeFilter, currencyFilter, dateFilter], }); export const inventory = (