feat: facturas y boletas de reserva (ReserveInvoice); las facturas y boletas normales excluyen las de reserva
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
1834c64f6f
commit
52dc1957b7
12 files changed
+70
-27
No files matched your search
@@ -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).
|
- **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`.
|
- **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.
|
- **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`** 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).
|
- **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: cuatro pantallas comparten el recurso `Invoices` y se separan por dos campos: `DocumentSubType` (`bod_Bill` = boleta) y `ReserveInvoice` (`tYES` = de reserva): Facturas (`tNO` y `ne bod_Bill`), Boletas (`tNO` y `eq bod_Bill`), Facturas de reserva (`tYES` y `ne bod_Bill`) y Boletas de reserva (`tYES` y `eq bod_Bill`); `baseFilter` para la lista y `fixed` al crear. La factura solo *excluye* `bod_Bill` (`ne`, no `eq bod_None`, para no perder notas de débito u otros subtipos). En SBODemoCL: 295 facturas, 54 boletas, 469 facturas de reserva, 0 boletas de reserva. `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.
|
- 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
|
## Flujo de las pantallas de documento
|
||||||
@@ -60,7 +60,7 @@ Pendiente, en orden:
|
|||||||
|
|
||||||
**Campos por cliente (estándar adicionales o `U_*`):** no se editan las pantallas base. Cada módulo tiene su archivo de extensiones, vacío en `main`: `ventas/ventas.extensions.ts`, `compras/compras.extensions.ts` e `inventario/inventario.extensions.ts` (reunidos en `client-extensions.ts`). La rama `cliente/<nombre>` declara ahí, por ruta de pantalla, qué suma: `headerLeft`/`headerRight`/`footerLeft`/`footerRight` (campos; tipos `text`, `date`, `number`, `textarea`, `select` con `options`, `warehouse`…), `lineColumns` (se agregan antes del total y se envían a SAP) y `listColumns`. Solo suma, no quita; una ruta inexistente o ajena al módulo lanza un error al arrancar. El test «el proyecto base no usa campos de usuario» revisa `BASE_SCREENS`, no las extensiones. Verificado: el proxy `/api/sl` deja pasar nombres `U_*` en `$select`/`$orderby` y en el cuerpo (el error de un campo inexistente lo da SAP). Límite: una columna de línea de usuario con lista de valores válidos aún no tiene editor desplegable (solo texto/número).
|
**Campos por cliente (estándar adicionales o `U_*`):** no se editan las pantallas base. Cada módulo tiene su archivo de extensiones, vacío en `main`: `ventas/ventas.extensions.ts`, `compras/compras.extensions.ts` e `inventario/inventario.extensions.ts` (reunidos en `client-extensions.ts`). La rama `cliente/<nombre>` declara ahí, por ruta de pantalla, qué suma: `headerLeft`/`headerRight`/`footerLeft`/`footerRight` (campos; tipos `text`, `date`, `number`, `textarea`, `select` con `options`, `warehouse`…), `lineColumns` (se agregan antes del total y se envían a SAP) y `listColumns`. Solo suma, no quita; una ruta inexistente o ajena al módulo lanza un error al arrancar. El test «el proyecto base no usa campos de usuario» revisa `BASE_SCREENS`, no las extensiones. Verificado: el proxy `/api/sl` deja pasar nombres `U_*` en `$select`/`$orderby` y en el cuerpo (el error de un campo inexistente lo da SAP). Límite: una columna de línea de usuario con lista de valores válidos aún no tiene editor desplegable (solo texto/número).
|
||||||
|
|
||||||
**Columnas de las ventanas de selección (cliente, artículo, almacén, impuesto):** son configurables por documento. En una pantalla del base (ventas/compras): `lookups: { item: [...], partner: [...], warehouse: [...], tax: [...] }` en el último parámetro de `marketing(...)` *sustituye* las columnas de esa lista (la primera columna es el código que se escribe en el campo). En la rama del cliente: `lookupColumns` en su archivo de extensiones *suma* columnas al final (por ejemplo `U_Marca` en artículos). El recurso, el filtro y el orden de cada lista no cambian; para los documentos de inventario o solicitudes se usa `withLookupColumns(layout, cols, 'replace' | 'add')`. Las columnas de almacén aplican también al almacén de la cabecera y al de origen de una transferencia. Los formularios con socio muestran `NumAtCard` y el RUT (`FederalTaxID`, solo lectura: lo completa SAP desde el socio); facturas, notas de crédito, entregas y recepciones (`FOLIO_PATHS` en `document-screens.ts`) añaden el folio tributario, que en el Service Layer es `FolioPrefixString` + `FolioNumber` (se edita como texto y se envía como número); las listas de facturas también los muestran. Flete hecho: fila «Flete» en el pie de ventas y compras (gasto adicional + importe; `freightPayload` lo guarda como `DocumentAdditionalExpenses`, reemplazando la colección; el catálogo `AdditionalExpenses` está registrado en el proxy; con varios gastos adicionales cargados en SAP solo se muestra). `freight: false` en `marketing(...)` lo quita de un documento. El toolbar del formulario ocupa todo el ancho del contenido (`<sbo-toolbar bleed>`). Falta: botón PDF.
|
**Columnas de las ventanas de selección (cliente, artículo, almacén, impuesto):** son configurables por documento. En una pantalla del base (ventas/compras): `lookups: { item: [...], partner: [...], warehouse: [...], tax: [...] }` en el último parámetro de `marketing(...)` *sustituye* las columnas de esa lista (la primera columna es el código que se escribe en el campo). En la rama del cliente: `lookupColumns` en su archivo de extensiones *suma* columnas al final (por ejemplo `U_Marca` en artículos). El recurso, el filtro y el orden de cada lista no cambian; para los documentos de inventario o solicitudes se usa `withLookupColumns(layout, cols, 'replace' | 'add')`. Las columnas de almacén aplican también al almacén de la cabecera y al de origen de una transferencia. Los formularios con socio muestran `NumAtCard` y el RUT (`FederalTaxID`, solo lectura: lo completa SAP desde el socio); facturas, notas de crédito, entregas y recepciones (`FOLIO_PATHS` en `document-screens.ts`) añaden el folio tributario, que en el Service Layer es `FolioPrefixString` + `FolioNumber` (se edita como texto y se envía como número); las listas de facturas también los muestran. Flete hecho: fila «Flete» en el pie de ventas y compras (gasto adicional + importe; `freightPayload` lo guarda como `DocumentAdditionalExpenses`, reemplazando la colección; el catálogo `AdditionalExpenses` está registrado en el proxy; con varios gastos adicionales cargados en SAP solo se muestra). `freight: false` en `marketing(...)` lo quita de un documento. El toolbar del formulario ocupa todo el ancho del contenido (`<sbo-toolbar bleed>`). Falta: botón PDF. En ventas el flete falla en SBODemoCL ("Define the freight revenue account…"): el gasto adicional «Flete» solo tiene cuenta de gasto, no de ingresos (configuración de SAP, no del código); en compras funciona (orden de compra 2885).
|
||||||
2. Pantallas de maestros (`Items`, `BusinessPartners`: Clientes/Proveedores, Artículos), hoy "no disponible".
|
2. Pantallas de maestros (`Items`, `BusinessPartners`: Clientes/Proveedores, Artículos), hoy "no disponible".
|
||||||
3. ~~Pantalla web de permisos~~ — hecha (`/admin/permisos`).
|
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.
|
4. Auditoría (`audit_log` en SQLite; hoy solo líneas `AUDIT` en el log) y `nestjs-cls` para el usuario en los logs.
|
||||||
|
|||||||
@@ -44,8 +44,8 @@ export const SL_RESOURCES: readonly SlResource[] = [
|
|||||||
document('Quotations', '/ventas/ofertas'),
|
document('Quotations', '/ventas/ofertas'),
|
||||||
document('Orders', '/ventas/ordenes'),
|
document('Orders', '/ventas/ordenes'),
|
||||||
document('DeliveryNotes', '/ventas/entrega'),
|
document('DeliveryNotes', '/ventas/entrega'),
|
||||||
// Las boletas de venta son facturas con `DocumentSubType = bod_Bill`: mismo recurso, dos pantallas.
|
// Boletas (`DocumentSubType = bod_Bill`) y facturas de reserva (`ReserveInvoice = tYES`) son facturas: mismo recurso, cuatro pantallas.
|
||||||
document('Invoices', ['/ventas/facturas', '/ventas/boletas']),
|
document('Invoices', ['/ventas/facturas', '/ventas/boletas', '/ventas/facturas-reserva', '/ventas/boletas-reserva']),
|
||||||
document('Returns', '/ventas/devoluciones'),
|
document('Returns', '/ventas/devoluciones'),
|
||||||
document('CreditNotes', '/ventas/notas-credito'),
|
document('CreditNotes', '/ventas/notas-credito'),
|
||||||
|
|
||||||
|
|||||||
@@ -44,7 +44,7 @@ describe('PermissionsPage', () => {
|
|||||||
it('al elegir un usuario marca sus pantallas (el módulo completo por /ventas/*)', async () => {
|
it('al elegir un usuario marca sus pantallas (el módulo completo por /ventas/*)', async () => {
|
||||||
await click('.users li button', 0);
|
await click('.users li button', 0);
|
||||||
const checked = [...el().querySelectorAll<HTMLInputElement>('fieldset .items input')].filter((i) => i.checked);
|
const checked = [...el().querySelectorAll<HTMLInputElement>('fieldset .items input')].filter((i) => i.checked);
|
||||||
expect(checked.length).toBe(8); // 7 de ventas + Clientes
|
expect(checked.length).toBe(10); // 9 de ventas + Clientes
|
||||||
expect(el().querySelector<HTMLButtonElement>('.actions .main')!.disabled).toBe(true); // sin cambios
|
expect(el().querySelector<HTMLButtonElement>('.actions .main')!.disabled).toBe(true); // sin cambios
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -20,9 +20,13 @@ describe('flujos de copiado', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('copiar a / copiar de son los dos lados del mismo flujo', () => {
|
it('copiar a / copiar de son los dos lados del mismo flujo', () => {
|
||||||
expect(copyTargets('/ventas/ordenes')).toEqual(['/ventas/entrega', '/ventas/facturas', '/ventas/boletas']);
|
expect(copyTargets('/ventas/ordenes')).toEqual([
|
||||||
|
'/ventas/entrega', '/ventas/facturas', '/ventas/boletas', '/ventas/facturas-reserva', '/ventas/boletas-reserva',
|
||||||
|
]);
|
||||||
expect(copySources('/ventas/facturas')).toEqual(['/ventas/ofertas', '/ventas/ordenes', '/ventas/entrega']);
|
expect(copySources('/ventas/facturas')).toEqual(['/ventas/ofertas', '/ventas/ordenes', '/ventas/entrega']);
|
||||||
expect(copySources('/ventas/boletas')).toEqual(['/ventas/ofertas', '/ventas/ordenes', '/ventas/entrega']);
|
expect(copySources('/ventas/boletas')).toEqual(['/ventas/ofertas', '/ventas/ordenes', '/ventas/entrega']);
|
||||||
|
expect(copySources('/ventas/facturas-reserva')).toEqual(['/ventas/ofertas', '/ventas/ordenes']);
|
||||||
|
expect(copySources('/ventas/boletas-reserva')).toEqual(['/ventas/ofertas', '/ventas/ordenes']);
|
||||||
expect(copyTargets('/ventas/boletas')).toEqual(['/ventas/notas-credito']);
|
expect(copyTargets('/ventas/boletas')).toEqual(['/ventas/notas-credito']);
|
||||||
expect(copyTargets('/ventas/notas-credito')).toEqual([]);
|
expect(copyTargets('/ventas/notas-credito')).toEqual([]);
|
||||||
expect(copySources('/ventas/ofertas')).toEqual([]);
|
expect(copySources('/ventas/ofertas')).toEqual([]);
|
||||||
|
|||||||
@@ -10,6 +10,8 @@ export const OBJECT_TYPES: Readonly<Record<string, number>> = {
|
|||||||
'/ventas/devoluciones': 16,
|
'/ventas/devoluciones': 16,
|
||||||
'/ventas/facturas': 13,
|
'/ventas/facturas': 13,
|
||||||
'/ventas/boletas': 13,
|
'/ventas/boletas': 13,
|
||||||
|
'/ventas/facturas-reserva': 13,
|
||||||
|
'/ventas/boletas-reserva': 13,
|
||||||
'/ventas/notas-credito': 14,
|
'/ventas/notas-credito': 14,
|
||||||
'/compras/ofertas': 540000006,
|
'/compras/ofertas': 540000006,
|
||||||
'/compras/ordenes': 22,
|
'/compras/ordenes': 22,
|
||||||
@@ -21,8 +23,8 @@ export const OBJECT_TYPES: Readonly<Record<string, number>> = {
|
|||||||
|
|
||||||
/** De qué documento se puede copiar a cuáles (el flujo estándar de SAP B1). */
|
/** De qué documento se puede copiar a cuáles (el flujo estándar de SAP B1). */
|
||||||
export const COPY_FLOWS: Readonly<Record<string, readonly string[]>> = {
|
export const COPY_FLOWS: Readonly<Record<string, readonly string[]>> = {
|
||||||
'/ventas/ofertas': ['/ventas/ordenes', '/ventas/entrega', '/ventas/facturas', '/ventas/boletas'],
|
'/ventas/ofertas': ['/ventas/ordenes', '/ventas/entrega', '/ventas/facturas', '/ventas/boletas', '/ventas/facturas-reserva', '/ventas/boletas-reserva'],
|
||||||
'/ventas/ordenes': ['/ventas/entrega', '/ventas/facturas', '/ventas/boletas'],
|
'/ventas/ordenes': ['/ventas/entrega', '/ventas/facturas', '/ventas/boletas', '/ventas/facturas-reserva', '/ventas/boletas-reserva'],
|
||||||
'/ventas/entrega': ['/ventas/facturas', '/ventas/boletas', '/ventas/devoluciones'],
|
'/ventas/entrega': ['/ventas/facturas', '/ventas/boletas', '/ventas/devoluciones'],
|
||||||
'/ventas/devoluciones': ['/ventas/notas-credito'],
|
'/ventas/devoluciones': ['/ventas/notas-credito'],
|
||||||
'/ventas/facturas': ['/ventas/notas-credito'],
|
'/ventas/facturas': ['/ventas/notas-credito'],
|
||||||
|
|||||||
@@ -67,7 +67,7 @@ describe('folio y RUT en los formularios', () => {
|
|||||||
const withFolio = DOCUMENT_SCREENS.filter((s) => hasFolio(s.layout)).map((s) => s.path);
|
const withFolio = DOCUMENT_SCREENS.filter((s) => hasFolio(s.layout)).map((s) => s.path);
|
||||||
expect(withFolio.sort()).toEqual(
|
expect(withFolio.sort()).toEqual(
|
||||||
[
|
[
|
||||||
'/ventas/facturas', '/ventas/boletas', '/ventas/notas-credito', '/ventas/entrega',
|
'/ventas/facturas', '/ventas/boletas', '/ventas/facturas-reserva', '/ventas/boletas-reserva', '/ventas/notas-credito', '/ventas/entrega',
|
||||||
'/compras/facturas', '/compras/notas-credito', '/compras/recepcion',
|
'/compras/facturas', '/compras/notas-credito', '/compras/recepcion',
|
||||||
].sort(),
|
].sort(),
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -225,11 +225,14 @@ describe('DocumentFormPage', () => {
|
|||||||
describe('copiar a', () => {
|
describe('copiar a', () => {
|
||||||
const items = () => [...el().querySelectorAll<HTMLButtonElement>('sbo-menu-button')].map((m) => m.textContent?.trim());
|
const items = () => [...el().querySelectorAll<HTMLButtonElement>('sbo-menu-button')].map((m) => m.textContent?.trim());
|
||||||
|
|
||||||
it('un documento abierto y guardado ofrece Copiar a con entrega, factura y boleta', async () => {
|
it('un documento abierto y guardado ofrece Copiar a con entrega, factura, boleta y sus reservas', async () => {
|
||||||
await open('41', order());
|
await open('41', order());
|
||||||
expect(items().join(' ')).toContain('Copiar a');
|
expect(items().join(' ')).toContain('Copiar a');
|
||||||
const ids = page().copyToItems().map((i: { id: string; disabled: boolean }) => [i.id, i.disabled]);
|
const ids = page().copyToItems().map((i: { id: string; disabled: boolean }) => [i.id, i.disabled]);
|
||||||
expect(ids).toEqual([['/ventas/entrega', false], ['/ventas/facturas', false], ['/ventas/boletas', false]]);
|
expect(ids.map((i: [string, boolean]) => i[0])).toEqual([
|
||||||
|
'/ventas/entrega', '/ventas/facturas', '/ventas/boletas', '/ventas/facturas-reserva', '/ventas/boletas-reserva',
|
||||||
|
]);
|
||||||
|
expect(ids.every((i: [string, boolean]) => i[1] === false)).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('abre el documento nuevo de destino llevando el origen', async () => {
|
it('abre el documento nuevo de destino llevando el origen', async () => {
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ describe('modelo de los formularios sin socio', () => {
|
|||||||
expect(DOCUMENT_SCREENS.every((s) => s.layout)).toBe(true);
|
expect(DOCUMENT_SCREENS.every((s) => s.layout)).toBe(true);
|
||||||
const withPartner = DOCUMENT_SCREENS.filter((s) => s.partner).map((s) => s.resource);
|
const withPartner = DOCUMENT_SCREENS.filter((s) => s.partner).map((s) => s.resource);
|
||||||
const without = 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(13);
|
expect(withPartner).toHaveLength(15);
|
||||||
expect(without.sort()).toEqual(
|
expect(without.sort()).toEqual(
|
||||||
['InventoryGenEntries', 'InventoryGenExits', 'InventoryTransferRequests', 'PurchaseRequests', 'StockTransfers'].sort(),
|
['InventoryGenEntries', 'InventoryGenExits', 'InventoryTransferRequests', 'PurchaseRequests', 'StockTransfers'].sort(),
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -10,6 +10,8 @@ export const DOCUMENT_PATHS: readonly string[] = [
|
|||||||
'/ventas/entrega',
|
'/ventas/entrega',
|
||||||
'/ventas/facturas',
|
'/ventas/facturas',
|
||||||
'/ventas/boletas',
|
'/ventas/boletas',
|
||||||
|
'/ventas/facturas-reserva',
|
||||||
|
'/ventas/boletas-reserva',
|
||||||
'/ventas/devoluciones',
|
'/ventas/devoluciones',
|
||||||
'/ventas/notas-credito',
|
'/ventas/notas-credito',
|
||||||
// Compras
|
// Compras
|
||||||
|
|||||||
@@ -121,24 +121,35 @@ describe('referencia y RUT en las listas de ofertas y órdenes', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('facturas y boletas de venta', () => {
|
describe('facturas, boletas y sus reservas', () => {
|
||||||
const by = (p: string) => DOCUMENT_SCREENS.find((s) => s.path === p)!;
|
const by = (p: string) => DOCUMENT_SCREENS.find((s) => s.path === p)!;
|
||||||
|
const INVOICES = ['/ventas/facturas', '/ventas/boletas', '/ventas/facturas-reserva', '/ventas/boletas-reserva'];
|
||||||
|
|
||||||
it('comparten el recurso Invoices y se separan por el subtipo de documento', () => {
|
it('las cuatro comparten el recurso Invoices y se separan por ReserveInvoice y DocumentSubType', () => {
|
||||||
expect(by('/ventas/facturas').resource).toBe('Invoices');
|
for (const p of INVOICES) expect(by(p).resource, p).toBe('Invoices');
|
||||||
expect(by('/ventas/boletas').resource).toBe('Invoices');
|
// la factura solo excluye las boletas (`ne`): no pierde notas de débito ni otros subtipos
|
||||||
expect(by('/ventas/boletas').baseFilter).toBe(`DocumentSubType eq 'bod_Bill'`);
|
expect(by('/ventas/facturas').baseFilter).toBe(`ReserveInvoice eq 'tNO' and DocumentSubType ne 'bod_Bill'`);
|
||||||
// la factura solo excluye las boletas: no pierde notas de débito ni otros subtipos
|
expect(by('/ventas/boletas').baseFilter).toBe(`ReserveInvoice eq 'tNO' and DocumentSubType eq 'bod_Bill'`);
|
||||||
expect(by('/ventas/facturas').baseFilter).toBe(`DocumentSubType ne 'bod_Bill'`);
|
expect(by('/ventas/facturas-reserva').baseFilter).toBe(`ReserveInvoice eq 'tYES' and DocumentSubType ne 'bod_Bill'`);
|
||||||
|
expect(by('/ventas/boletas-reserva').baseFilter).toBe(`ReserveInvoice eq 'tYES' and DocumentSubType eq 'bod_Bill'`);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('una boleta nueva se crea con su subtipo; una factura no lo envía', () => {
|
it('cada una se crea con su marca y una factura normal no envía nada especial', () => {
|
||||||
expect(by('/ventas/boletas').layout.beforeCreate!({ CardCode: 'C1' })).toEqual({ CardCode: 'C1', DocumentSubType: 'bod_Bill' });
|
const created = (p: string) => by(p).layout.beforeCreate?.({ CardCode: 'C1' });
|
||||||
expect(by('/ventas/facturas').layout.beforeCreate).toBeUndefined();
|
expect(created('/ventas/facturas')).toBeUndefined();
|
||||||
|
expect(created('/ventas/boletas')).toEqual({ CardCode: 'C1', DocumentSubType: 'bod_Bill' });
|
||||||
|
expect(created('/ventas/facturas-reserva')).toEqual({ CardCode: 'C1', ReserveInvoice: 'tYES' });
|
||||||
|
expect(created('/ventas/boletas-reserva')).toEqual({ CardCode: 'C1', ReserveInvoice: 'tYES', DocumentSubType: 'bod_Bill' });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('las cuatro son documentos tributarios: llevan folio, referencia y RUT', () => {
|
||||||
|
for (const p of INVOICES) {
|
||||||
|
expect(by(p).columns.map((c) => c.key), p).toEqual(expect.arrayContaining(['FolioNumber', 'NumAtCard', 'FederalTaxID']));
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
it('el resto de las pantallas no tiene filtro fijo', () => {
|
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))) {
|
for (const s of DOCUMENT_SCREENS.filter((s) => !INVOICES.includes(s.path))) {
|
||||||
expect(s.baseFilter, s.path).toBeUndefined();
|
expect(s.baseFilter, s.path).toBeUndefined();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,5 +1,11 @@
|
|||||||
import { TAX_DOCUMENT, type DocumentScreen, invoiceColumns, marketing, referenceColumns } from '../screen-kit';
|
import { TAX_DOCUMENT, type DocumentScreen, invoiceColumns, marketing, referenceColumns } from '../screen-kit';
|
||||||
|
|
||||||
|
/** Filtros que separan las cuatro pantallas de `Invoices`. */
|
||||||
|
const RESERVE = `ReserveInvoice eq 'tYES'`;
|
||||||
|
const NOT_RESERVE = `ReserveInvoice eq 'tNO'`;
|
||||||
|
const BILL = `DocumentSubType eq 'bod_Bill'`;
|
||||||
|
const NOT_BILL = `DocumentSubType ne 'bod_Bill'`;
|
||||||
|
|
||||||
/** Ventas: ofertas, órdenes, entregas, facturas, devoluciones y notas de crédito de cliente. */
|
/** Ventas: ofertas, órdenes, entregas, facturas, devoluciones y notas de crédito de cliente. */
|
||||||
export const VENTAS_SCREENS: readonly DocumentScreen[] = [
|
export const VENTAS_SCREENS: readonly DocumentScreen[] = [
|
||||||
] = [
|
] = [
|
||||||
@@ -7,19 +13,32 @@ export const VENTAS_SCREENS: readonly DocumentScreen[] = [
|
|||||||
marketing('/ventas/ofertas', 'Quotations', 'Nueva oferta', 'Cliente', { columns: referenceColumns }),
|
marketing('/ventas/ofertas', 'Quotations', 'Nueva oferta', 'Cliente', { columns: referenceColumns }),
|
||||||
marketing('/ventas/ordenes', 'Orders', 'Nueva orden', 'Cliente', { columns: referenceColumns }),
|
marketing('/ventas/ordenes', 'Orders', 'Nueva orden', 'Cliente', { columns: referenceColumns }),
|
||||||
marketing('/ventas/entrega', 'DeliveryNotes', 'Nueva entrega', 'Cliente', TAX_DOCUMENT),
|
marketing('/ventas/entrega', 'DeliveryNotes', 'Nueva entrega', 'Cliente', TAX_DOCUMENT),
|
||||||
// Facturas y boletas son el mismo recurso (`Invoices`): la boleta es la de subtipo `bod_Bill`. La factura
|
// Facturas, boletas y sus reservas son el mismo recurso (`Invoices`); se separan por dos campos:
|
||||||
// excluye solo las boletas (no filtra por `bod_None`, para no perder notas de débito u otros subtipos).
|
// `DocumentSubType` (`bod_Bill` = boleta) y `ReserveInvoice` (`tYES` = de reserva). La factura excluye
|
||||||
|
// solo las boletas (`ne`, no `eq bod_None`, para no perder notas de débito u otros subtipos).
|
||||||
marketing('/ventas/facturas', 'Invoices', 'Nueva factura', 'Cliente', {
|
marketing('/ventas/facturas', 'Invoices', 'Nueva factura', 'Cliente', {
|
||||||
...TAX_DOCUMENT,
|
...TAX_DOCUMENT,
|
||||||
columns: invoiceColumns,
|
columns: invoiceColumns,
|
||||||
baseFilter: `DocumentSubType ne 'bod_Bill'`,
|
baseFilter: `${NOT_RESERVE} and ${NOT_BILL}`,
|
||||||
}),
|
}),
|
||||||
marketing('/ventas/boletas', 'Invoices', 'Nueva boleta', 'Cliente', {
|
marketing('/ventas/boletas', 'Invoices', 'Nueva boleta', 'Cliente', {
|
||||||
...TAX_DOCUMENT,
|
...TAX_DOCUMENT,
|
||||||
columns: invoiceColumns,
|
columns: invoiceColumns,
|
||||||
baseFilter: `DocumentSubType eq 'bod_Bill'`,
|
baseFilter: `${NOT_RESERVE} and ${BILL}`,
|
||||||
fixed: { DocumentSubType: 'bod_Bill' },
|
fixed: { DocumentSubType: 'bod_Bill' },
|
||||||
}),
|
}),
|
||||||
|
marketing('/ventas/facturas-reserva', 'Invoices', 'Nueva factura de reserva', 'Cliente', {
|
||||||
|
...TAX_DOCUMENT,
|
||||||
|
columns: invoiceColumns,
|
||||||
|
baseFilter: `${RESERVE} and ${NOT_BILL}`,
|
||||||
|
fixed: { ReserveInvoice: 'tYES' },
|
||||||
|
}),
|
||||||
|
marketing('/ventas/boletas-reserva', 'Invoices', 'Nueva boleta de reserva', 'Cliente', {
|
||||||
|
...TAX_DOCUMENT,
|
||||||
|
columns: invoiceColumns,
|
||||||
|
baseFilter: `${RESERVE} and ${BILL}`,
|
||||||
|
fixed: { ReserveInvoice: 'tYES', DocumentSubType: 'bod_Bill' },
|
||||||
|
}),
|
||||||
marketing('/ventas/devoluciones', 'Returns', 'Nueva devolución', 'Cliente'),
|
marketing('/ventas/devoluciones', 'Returns', 'Nueva devolución', 'Cliente'),
|
||||||
marketing('/ventas/notas-credito', 'CreditNotes', 'Nueva nota de crédito', 'Cliente', TAX_DOCUMENT),
|
marketing('/ventas/notas-credito', 'CreditNotes', 'Nueva nota de crédito', 'Cliente', TAX_DOCUMENT),
|
||||||
];
|
];
|
||||||
@@ -26,6 +26,8 @@ export const MENU: readonly MenuModule[] = [
|
|||||||
{ path: '/ventas/entrega', label: 'Entregas' },
|
{ path: '/ventas/entrega', label: 'Entregas' },
|
||||||
{ path: '/ventas/facturas', label: 'Facturas de venta' },
|
{ path: '/ventas/facturas', label: 'Facturas de venta' },
|
||||||
{ path: '/ventas/boletas', label: 'Boletas de venta' },
|
{ path: '/ventas/boletas', label: 'Boletas de venta' },
|
||||||
|
{ path: '/ventas/facturas-reserva', label: 'Facturas de reserva' },
|
||||||
|
{ path: '/ventas/boletas-reserva', label: 'Boletas de reserva' },
|
||||||
{ path: '/ventas/devoluciones', label: 'Devoluciones' },
|
{ path: '/ventas/devoluciones', label: 'Devoluciones' },
|
||||||
{ path: '/ventas/notas-credito', label: 'Notas de crédito' },
|
{ path: '/ventas/notas-credito', label: 'Notas de crédito' },
|
||||||
],
|
],
|
||||||
|
|||||||
Reference in new issue
Block a user