diff --git a/CLAUDE.md b/CLAUDE.md index ed5d8ce..bfec927 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -36,7 +36,7 @@ Es un ERP, no una página de publicidad: **ligera y rápida**. Sin librerías de - **TLS:** `SAPB1_SL_CA_FILE` opcional (certificado autofirmado de SAP o CA de firewall); `VERIFY_SSL=true` por defecto y no se puede apagar en producción. `fortinet.cer` no sirve para SAP10: presenta su propio certificado autofirmado (`apps/api/certs/sap10.pem`, ignorado por git). - **Proxy `/api/sl/:recurso`**: el frontend arma `$select/$filter/...`; el backend valida y ejecuta solo recursos registrados (`apps/api/src/sl/sl-resources.ts`), con permiso de menú y cabeceras `B1S-*` de lista blanca. Nunca registrar `EmployeesInfo` (tiene `U_WEB_PASS`). `InventoryRequests` no existe: es `InventoryTransferRequests`. - **Permisos de menú** en SQLite (`SQLITE_PATH`), patrones `*`, `/ventas/*`, `/ventas/ordenes`; `ADMIN_WEB_USERS` garantiza el acceso total; no se puede quitar el último admin. -- **Listas de documentos:** más recientes primero (`DocEntry desc`), 50 por página, **Estado = Abierto por defecto** (como el mini-erp; cambiarlo es un `defaultValue`), filtro de fecha **siempre rango [desde – hasta]**, multi-selección con **chips**, búsqueda numérica por igualdad. **Sin filtro base `DocumentSubType`** (excluiría notas de débito, etc.). El PDF y los reportes se migran después vía API Gateway (otro servicio, con su propia cola y sesión). +- **Listas de documentos:** más recientes primero (`DocEntry desc`), 50 por página, **Estado = Abierto por defecto** (como el mini-erp; cambiarlo es un `defaultValue`), filtro de fecha **siempre rango [desde – hasta]**, multi-selección con **chips**, búsqueda numérica por igualdad. **Sin filtro base `DocumentSubType`** salvo las boletas: la boleta de venta es una `Invoices` con `DocumentSubType = bod_Bill` (pantalla `/ventas/boletas`, `baseFilter` + `fixed` al crear); la lista de facturas solo *excluye* `bod_Bill` (`ne`, no `eq bod_None`, para no perder notas de débito u otros subtipos). En SBODemoCL hay `bod_None` y `bod_Bill`. `baseFilter` de la pantalla se aplica también a la navegación primero/anterior/siguiente/último. El PDF y los reportes se migran después vía API Gateway (otro servicio, con su propia cola y sesión). - Reportes/PDF **no** se generan en la base; el botón PDF queda pendiente hasta migrar el API Gateway. ## Flujo de las pantallas de documento diff --git a/apps/api/src/sl/sl-resources.ts b/apps/api/src/sl/sl-resources.ts index 00f41d9..83ab714 100644 --- a/apps/api/src/sl/sl-resources.ts +++ b/apps/api/src/sl/sl-resources.ts @@ -17,7 +17,7 @@ export interface SlResource { expand?: string[]; } -const document = (name: string, path: string): SlResource => ({ +const document = (name: string, path: RequiredPath): SlResource => ({ name, key: 'number', methods: ['GET', 'POST', 'PATCH'], @@ -44,7 +44,8 @@ export const SL_RESOURCES: readonly SlResource[] = [ document('Quotations', '/ventas/ofertas'), document('Orders', '/ventas/ordenes'), document('DeliveryNotes', '/ventas/entrega'), - document('Invoices', '/ventas/facturas'), + // Las boletas de venta son facturas con `DocumentSubType = bod_Bill`: mismo recurso, dos pantallas. + document('Invoices', ['/ventas/facturas', '/ventas/boletas']), document('Returns', '/ventas/devoluciones'), document('CreditNotes', '/ventas/notas-credito'), diff --git a/apps/web/src/app/core/sl/sl-document.spec.ts b/apps/web/src/app/core/sl/sl-document.spec.ts new file mode 100644 index 0000000..4347d67 --- /dev/null +++ b/apps/web/src/app/core/sl/sl-document.spec.ts @@ -0,0 +1,41 @@ +import { provideHttpClient } from '@angular/common/http'; +import { HttpTestingController, provideHttpClientTesting } from '@angular/common/http/testing'; +import { TestBed } from '@angular/core/testing'; +import { beforeEach, describe, expect, it } from 'vitest'; +import { SlDocumentApi } from './sl-document'; + +describe('SlDocumentApi.neighbour', () => { + let api: SlDocumentApi; + let ctrl: HttpTestingController; + + beforeEach(() => { + TestBed.configureTestingModule({ providers: [provideHttpClient(), provideHttpClientTesting()] }); + api = TestBed.inject(SlDocumentApi); + ctrl = TestBed.inject(HttpTestingController); + }); + + it('pide el documento anterior por DocEntry', async () => { + const p = api.neighbour('Orders', 'prev', 41); + const req = ctrl.expectOne((r) => r.url === '/api/sl/Orders'); + expect(req.request.params.get('$filter')).toBe('DocEntry lt 41'); + expect(req.request.params.get('$orderby')).toBe('DocEntry desc'); + req.flush({ value: [{ DocEntry: 40 }] }); + expect(await p).toBe(40); + }); + + it('la navegación respeta el filtro fijo de la pantalla (boletas dentro de las facturas)', async () => { + const p = api.neighbour('Invoices', 'next', 7, `DocumentSubType eq 'bod_Bill'`); + const req = ctrl.expectOne((r) => r.url === '/api/sl/Invoices'); + expect(req.request.params.get('$filter')).toBe(`(DocumentSubType eq 'bod_Bill') and (DocEntry gt 7)`); + req.flush({ value: [] }); + expect(await p).toBeNull(); + }); + + it('con filtro fijo y sin documento actual solo aplica el fijo', async () => { + const p = api.neighbour('Invoices', 'first', null, `DocumentSubType ne 'bod_Bill'`); + const req = ctrl.expectOne((r) => r.url === '/api/sl/Invoices'); + expect(req.request.params.get('$filter')).toBe(`DocumentSubType ne 'bod_Bill'`); + req.flush({ value: [{ DocEntry: 1 }] }); + expect(await p).toBe(1); + }); +}); diff --git a/apps/web/src/app/core/sl/sl-document.ts b/apps/web/src/app/core/sl/sl-document.ts index 9cf4732..7468c0d 100644 --- a/apps/web/src/app/core/sl/sl-document.ts +++ b/apps/web/src/app/core/sl/sl-document.ts @@ -3,6 +3,7 @@ import { Injectable, inject } from '@angular/core'; import { firstValueFrom } from 'rxjs'; import type { SlListResponse } from '@basic-erp/sap-types'; import { API_URL } from '../api'; +import { combineFilters } from './odata'; import { type DocRecord, type NavTarget, navigationQuery } from '../../features/documents/document-model'; /** Lectura y escritura de un documento del Service Layer a través del proxy `/api/sl`. */ @@ -20,10 +21,11 @@ export class SlDocumentApi { } /** DocEntry del documento vecino (primero / anterior / siguiente / último), o `null` si no hay. */ - async neighbour(resource: string, target: NavTarget, from: number | null): Promise { + async neighbour(resource: string, target: NavTarget, from: number | null, baseFilter?: string): Promise { const q = navigationQuery(target, from); let params = new HttpParams().set('$select', 'DocEntry').set('$top', 1).set('$orderby', q.$orderby); - if (q.$filter) params = params.set('$filter', q.$filter); + const filter = combineFilters(baseFilter, q.$filter); + if (filter) params = params.set('$filter', filter); const res = await firstValueFrom(this.http.get>(this.url(resource), { params })); const entry = res.value[0]?.['DocEntry']; return typeof entry === 'number' ? entry : null; diff --git a/apps/web/src/app/features/admin/permissions.page.spec.ts b/apps/web/src/app/features/admin/permissions.page.spec.ts index eec681b..d781e99 100644 --- a/apps/web/src/app/features/admin/permissions.page.spec.ts +++ b/apps/web/src/app/features/admin/permissions.page.spec.ts @@ -44,7 +44,7 @@ describe('PermissionsPage', () => { it('al elegir un usuario marca sus pantallas (el módulo completo por /ventas/*)', async () => { await click('.users li button', 0); const checked = [...el().querySelectorAll('fieldset .items input')].filter((i) => i.checked); - expect(checked.length).toBe(7); // 6 de ventas + Clientes + expect(checked.length).toBe(8); // 7 de ventas + Clientes expect(el().querySelector('.actions .main')!.disabled).toBe(true); // sin cambios }); diff --git a/apps/web/src/app/features/documents/document-copy.spec.ts b/apps/web/src/app/features/documents/document-copy.spec.ts index e1f9848..917c0df 100644 --- a/apps/web/src/app/features/documents/document-copy.spec.ts +++ b/apps/web/src/app/features/documents/document-copy.spec.ts @@ -20,8 +20,10 @@ describe('flujos de copiado', () => { }); it('copiar a / copiar de son los dos lados del mismo flujo', () => { - expect(copyTargets('/ventas/ordenes')).toEqual(['/ventas/entrega', '/ventas/facturas']); + expect(copyTargets('/ventas/ordenes')).toEqual(['/ventas/entrega', '/ventas/facturas', '/ventas/boletas']); expect(copySources('/ventas/facturas')).toEqual(['/ventas/ofertas', '/ventas/ordenes', '/ventas/entrega']); + expect(copySources('/ventas/boletas')).toEqual(['/ventas/ofertas', '/ventas/ordenes', '/ventas/entrega']); + expect(copyTargets('/ventas/boletas')).toEqual(['/ventas/notas-credito']); expect(copyTargets('/ventas/notas-credito')).toEqual([]); expect(copySources('/ventas/ofertas')).toEqual([]); }); diff --git a/apps/web/src/app/features/documents/document-copy.ts b/apps/web/src/app/features/documents/document-copy.ts index 7293f33..f3e841c 100644 --- a/apps/web/src/app/features/documents/document-copy.ts +++ b/apps/web/src/app/features/documents/document-copy.ts @@ -9,6 +9,7 @@ export const OBJECT_TYPES: Readonly> = { '/ventas/entrega': 15, '/ventas/devoluciones': 16, '/ventas/facturas': 13, + '/ventas/boletas': 13, '/ventas/notas-credito': 14, '/compras/ofertas': 540000006, '/compras/ordenes': 22, @@ -20,11 +21,12 @@ export const OBJECT_TYPES: Readonly> = { /** De qué documento se puede copiar a cuáles (el flujo estándar de SAP B1). */ export const COPY_FLOWS: Readonly> = { - '/ventas/ofertas': ['/ventas/ordenes', '/ventas/entrega', '/ventas/facturas'], - '/ventas/ordenes': ['/ventas/entrega', '/ventas/facturas'], - '/ventas/entrega': ['/ventas/facturas', '/ventas/devoluciones'], + '/ventas/ofertas': ['/ventas/ordenes', '/ventas/entrega', '/ventas/facturas', '/ventas/boletas'], + '/ventas/ordenes': ['/ventas/entrega', '/ventas/facturas', '/ventas/boletas'], + '/ventas/entrega': ['/ventas/facturas', '/ventas/boletas', '/ventas/devoluciones'], '/ventas/devoluciones': ['/ventas/notas-credito'], '/ventas/facturas': ['/ventas/notas-credito'], + '/ventas/boletas': ['/ventas/notas-credito'], '/compras/ofertas': ['/compras/ordenes'], '/compras/ordenes': ['/compras/recepcion', '/compras/facturas'], '/compras/recepcion': ['/compras/facturas', '/compras/devoluciones'], diff --git a/apps/web/src/app/features/documents/document-folio.spec.ts b/apps/web/src/app/features/documents/document-folio.spec.ts index c497812..7089f9e 100644 --- a/apps/web/src/app/features/documents/document-folio.spec.ts +++ b/apps/web/src/app/features/documents/document-folio.spec.ts @@ -63,10 +63,13 @@ describe('folio y RUT en los formularios', () => { beforeEach(() => TestBed.resetTestingModule()); - it('lo llevan facturas, notas de crédito, entregas y recepciones (ventas y compras)', () => { - const withFolio = DOCUMENT_SCREENS.filter((s) => hasFolio(s.layout)).map((s) => s.resource); + it('lo llevan facturas, boletas, notas de crédito, entregas y recepciones (ventas y compras)', () => { + const withFolio = DOCUMENT_SCREENS.filter((s) => hasFolio(s.layout)).map((s) => s.path); expect(withFolio.sort()).toEqual( - ['CreditNotes', 'DeliveryNotes', 'Invoices', 'PurchaseCreditNotes', 'PurchaseDeliveryNotes', 'PurchaseInvoices'].sort(), + [ + '/ventas/facturas', '/ventas/boletas', '/ventas/notas-credito', '/ventas/entrega', + '/compras/facturas', '/compras/notas-credito', '/compras/recepcion', + ].sort(), ); }); 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 ad185ae..be3ae99 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 @@ -225,11 +225,11 @@ describe('DocumentFormPage', () => { describe('copiar a', () => { const items = () => [...el().querySelectorAll('sbo-menu-button')].map((m) => m.textContent?.trim()); - it('un documento abierto y guardado ofrece Copiar a con entrega y factura', async () => { + it('un documento abierto y guardado ofrece Copiar a con entrega, factura y boleta', async () => { await open('41', order()); expect(items().join(' ')).toContain('Copiar a'); const ids = page().copyToItems().map((i: { id: string; disabled: boolean }) => [i.id, i.disabled]); - expect(ids).toEqual([['/ventas/entrega', false], ['/ventas/facturas', false]]); + expect(ids).toEqual([['/ventas/entrega', false], ['/ventas/facturas', false], ['/ventas/boletas', false]]); }); it('abre el documento nuevo de destino llevando el origen', async () => { 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 014f91d..fe0a29c 100644 --- a/apps/web/src/app/features/documents/document-form.page.ts +++ b/apps/web/src/app/features/documents/document-form.page.ts @@ -352,7 +352,7 @@ export class DocumentFormPage { protected async go(target: NavTarget): Promise { try { - const entry = await this.api.neighbour(this.screen.resource, target, this.docEntry()); + const entry = await this.api.neighbour(this.screen.resource, target, this.docEntry(), this.screen.baseFilter); if (entry === null) { this.message.set({ kind: 'warn', text: 'No hay más documentos en esa dirección.' }); return; diff --git a/apps/web/src/app/features/documents/document-layouts.spec.ts b/apps/web/src/app/features/documents/document-layouts.spec.ts index e017ae7..2ff6a57 100644 --- a/apps/web/src/app/features/documents/document-layouts.spec.ts +++ b/apps/web/src/app/features/documents/document-layouts.spec.ts @@ -19,7 +19,7 @@ describe('modelo de los formularios sin socio', () => { expect(DOCUMENT_SCREENS.every((s) => s.layout)).toBe(true); const withPartner = DOCUMENT_SCREENS.filter((s) => s.partner).map((s) => s.resource); const without = DOCUMENT_SCREENS.filter((s) => !s.partner).map((s) => s.resource); - expect(withPartner).toHaveLength(12); + expect(withPartner).toHaveLength(13); expect(without.sort()).toEqual( ['InventoryGenEntries', 'InventoryGenExits', 'InventoryTransferRequests', 'PurchaseRequests', 'StockTransfers'].sort(), ); diff --git a/apps/web/src/app/features/documents/document-layouts.ts b/apps/web/src/app/features/documents/document-layouts.ts index f3adf40..f99960b 100644 --- a/apps/web/src/app/features/documents/document-layouts.ts +++ b/apps/web/src/app/features/documents/document-layouts.ts @@ -174,6 +174,8 @@ export function marketingLayout(opts: { uom: 'sales' | 'purchase'; /** Fila del flete en el pie (por defecto sí). */ freight?: boolean; + /** Valores que se envían siempre al crear (ej. `DocumentSubType: 'bod_Bill'` en las boletas). */ + fixed?: DocRecord; }): DocumentLayout { const columns = lineColumnsFor(opts.taxResource, opts.lineColumns).map((c) => c.key === 'ItemCode' && c.lookup ? itemColumnWithUom(c, opts.uom) : c, @@ -204,6 +206,7 @@ export function marketingLayout(opts: { newHeader: () => ({ DocDate: today(), DocDueDate: today(), DiscountPercent: 0 }), newLine: newDocumentLine, recalc: recalcLine, + ...(opts.fixed ? { beforeCreate: (h: DocRecord) => ({ ...h, ...opts.fixed }) } : {}), requiredLine: [], }; return opts.lookups ? withLookupColumns(layout, opts.lookups, 'replace') : layout; diff --git a/apps/web/src/app/features/documents/document-list.page.ts b/apps/web/src/app/features/documents/document-list.page.ts index 56963cf..e6d1b5e 100644 --- a/apps/web/src/app/features/documents/document-list.page.ts +++ b/apps/web/src/app/features/documents/document-list.page.ts @@ -17,6 +17,7 @@ import type { DocumentScreen } from './document-screens'; [extraFields]="screen.extraFields ?? []" [filters]="screen.filters" [orderby]="screen.orderby" + [baseFilter]="screen.baseFilter" rowKey="DocEntry" [createLabel]="screen.createLabel" (open)="open($event)" diff --git a/apps/web/src/app/features/documents/document-paths.ts b/apps/web/src/app/features/documents/document-paths.ts index 2f00eb2..915b74b 100644 --- a/apps/web/src/app/features/documents/document-paths.ts +++ b/apps/web/src/app/features/documents/document-paths.ts @@ -9,6 +9,7 @@ export const DOCUMENT_PATHS: readonly string[] = [ '/ventas/ordenes', '/ventas/entrega', '/ventas/facturas', + '/ventas/boletas', '/ventas/devoluciones', '/ventas/notas-credito', // Compras diff --git a/apps/web/src/app/features/documents/document-screens.spec.ts b/apps/web/src/app/features/documents/document-screens.spec.ts index eb3047f..c0b1815 100644 --- a/apps/web/src/app/features/documents/document-screens.spec.ts +++ b/apps/web/src/app/features/documents/document-screens.spec.ts @@ -10,7 +10,9 @@ describe('DOCUMENT_SCREENS', () => { it('cada pantalla cuelga de una ruta del menú, sin repetir rutas ni recursos', () => { for (const s of DOCUMENT_SCREENS) expect(menuPaths.has(s.path), s.path).toBe(true); expect(new Set(DOCUMENT_SCREENS.map((s) => s.path)).size).toBe(DOCUMENT_SCREENS.length); - expect(new Set(DOCUMENT_SCREENS.map((s) => s.resource)).size).toBe(DOCUMENT_SCREENS.length); + // facturas y boletas comparten el recurso `Invoices` (las separa el filtro `DocumentSubType`); los demás no se repiten + const byResource = DOCUMENT_SCREENS.filter((s) => s.resource !== 'Invoices'); + expect(new Set(byResource.map((s) => s.resource)).size).toBe(byResource.length); }); it('todas ordenan por los más recientes y tienen una columna buscable por defecto', () => { @@ -118,3 +120,26 @@ describe('referencia y RUT en las listas de ofertas y órdenes', () => { expect(keys('DeliveryNotes')).not.toContain('NumAtCard'); }); }); + +describe('facturas y boletas de venta', () => { + const by = (p: string) => DOCUMENT_SCREENS.find((s) => s.path === p)!; + + it('comparten el recurso Invoices y se separan por el subtipo de documento', () => { + expect(by('/ventas/facturas').resource).toBe('Invoices'); + expect(by('/ventas/boletas').resource).toBe('Invoices'); + expect(by('/ventas/boletas').baseFilter).toBe(`DocumentSubType eq 'bod_Bill'`); + // la factura solo excluye las boletas: no pierde notas de débito ni otros subtipos + expect(by('/ventas/facturas').baseFilter).toBe(`DocumentSubType ne 'bod_Bill'`); + }); + + it('una boleta nueva se crea con su subtipo; una factura no lo envía', () => { + expect(by('/ventas/boletas').layout.beforeCreate!({ CardCode: 'C1' })).toEqual({ CardCode: 'C1', DocumentSubType: 'bod_Bill' }); + expect(by('/ventas/facturas').layout.beforeCreate).toBeUndefined(); + }); + + it('el resto de las pantallas no tiene filtro fijo', () => { + for (const s of DOCUMENT_SCREENS.filter((s) => !['/ventas/facturas', '/ventas/boletas'].includes(s.path))) { + expect(s.baseFilter, s.path).toBeUndefined(); + } + }); +}); diff --git a/apps/web/src/app/features/documents/screen-kit.ts b/apps/web/src/app/features/documents/screen-kit.ts index 43abfa7..fa95a4a 100644 --- a/apps/web/src/app/features/documents/screen-kit.ts +++ b/apps/web/src/app/features/documents/screen-kit.ts @@ -3,6 +3,7 @@ import { formatNumber } from '../../core/format'; import type { ListFilter } from '../../core/sl/list-filters'; import type { ListColumn, ListRow, Tone } from '../../shared/list/list.types'; import type { GridColumn } from '../../shared/grid/grid.types'; +import type { DocRecord } from './document-model'; import { type DocumentLayout, FIELDS, type FieldColumns, type HeaderField, type LookupColumns, marketingLayout } from './document-layouts'; /** Pantalla de un documento: lista de los últimos ingresados + formulario (nuevo / ver-editar). */ @@ -18,6 +19,8 @@ export interface DocumentScreen { filters: readonly ListFilter[]; /** Los más recientes primero. */ orderby: string; + /** Filtro fijo de la lista y de la navegación (ej. las boletas son las `Invoices` con `DocumentSubType = bod_Bill`). */ + baseFilter?: string; /** Formulario de documentos con socio de negocio, líneas y totales (ventas y compras). */ partner?: { label: string; @@ -121,6 +124,10 @@ export interface MarketingExtras { headerRight?: readonly HeaderField[]; /** Cabecera completa (reemplaza a la base). */ header?: FieldColumns; + /** Filtro fijo de la lista y de la navegación entre documentos. */ + baseFilter?: string; + /** Valores que se envían siempre al crear (ej. `DocumentSubType`). */ + fixed?: DocRecord; /** Columnas de línea de este documento (de `LINE_COLUMNS`); sin ellas, todas. */ lineColumns?: readonly GridColumn[]; /** Columnas de las listas de valores de este documento (cliente, artículo, almacén, impuesto). */ @@ -147,11 +154,13 @@ export const marketing = ( header: extras.header, lineColumns: extras.lineColumns, lookups: extras.lookups, + fixed: extras.fixed, uom: path.startsWith('/ventas') ? 'sales' : 'purchase', }), resource, createLabel, orderby: 'DocEntry desc', + baseFilter: extras.baseFilter, partner: path.startsWith('/ventas') ? { label: partner, type: 'cCustomer', taxResource: 'SalesTaxCodes' } : { label: partner, type: 'cSupplier', taxResource: 'SalesTaxCodes' }, diff --git a/apps/web/src/app/features/documents/ventas/ventas.screens.ts b/apps/web/src/app/features/documents/ventas/ventas.screens.ts index 7788e02..361a8d9 100644 --- a/apps/web/src/app/features/documents/ventas/ventas.screens.ts +++ b/apps/web/src/app/features/documents/ventas/ventas.screens.ts @@ -7,7 +7,19 @@ export const VENTAS_SCREENS: readonly DocumentScreen[] = [ marketing('/ventas/ofertas', 'Quotations', 'Nueva oferta', 'Cliente', { columns: referenceColumns }), marketing('/ventas/ordenes', 'Orders', 'Nueva orden', 'Cliente', { columns: referenceColumns }), marketing('/ventas/entrega', 'DeliveryNotes', 'Nueva entrega', 'Cliente', TAX_DOCUMENT), - marketing('/ventas/facturas', 'Invoices', 'Nueva factura', 'Cliente', { ...TAX_DOCUMENT, columns: invoiceColumns }), + // Facturas y boletas son el mismo recurso (`Invoices`): la boleta es la de subtipo `bod_Bill`. La factura + // excluye solo las boletas (no filtra por `bod_None`, para no perder notas de débito u otros subtipos). + marketing('/ventas/facturas', 'Invoices', 'Nueva factura', 'Cliente', { + ...TAX_DOCUMENT, + columns: invoiceColumns, + baseFilter: `DocumentSubType ne 'bod_Bill'`, + }), + marketing('/ventas/boletas', 'Invoices', 'Nueva boleta', 'Cliente', { + ...TAX_DOCUMENT, + columns: invoiceColumns, + baseFilter: `DocumentSubType eq 'bod_Bill'`, + fixed: { DocumentSubType: 'bod_Bill' }, + }), marketing('/ventas/devoluciones', 'Returns', 'Nueva devolución', 'Cliente'), marketing('/ventas/notas-credito', 'CreditNotes', 'Nueva nota de crédito', 'Cliente', TAX_DOCUMENT), ]; diff --git a/libs/sap-types/src/menu.ts b/libs/sap-types/src/menu.ts index 7352a60..a62f264 100644 --- a/libs/sap-types/src/menu.ts +++ b/libs/sap-types/src/menu.ts @@ -25,6 +25,7 @@ export const MENU: readonly MenuModule[] = [ { path: '/ventas/ordenes', label: 'Órdenes de venta' }, { path: '/ventas/entrega', label: 'Entregas' }, { path: '/ventas/facturas', label: 'Facturas de venta' }, + { path: '/ventas/boletas', label: 'Boletas de venta' }, { path: '/ventas/devoluciones', label: 'Devoluciones' }, { path: '/ventas/notas-credito', label: 'Notas de crédito' }, ],