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
@@ -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<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
|
||||
});
|
||||
|
||||
|
||||
@@ -20,9 +20,13 @@ 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', '/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/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/notas-credito')).toEqual([]);
|
||||
expect(copySources('/ventas/ofertas')).toEqual([]);
|
||||
|
||||
@@ -10,6 +10,8 @@ export const OBJECT_TYPES: Readonly<Record<string, number>> = {
|
||||
'/ventas/devoluciones': 16,
|
||||
'/ventas/facturas': 13,
|
||||
'/ventas/boletas': 13,
|
||||
'/ventas/facturas-reserva': 13,
|
||||
'/ventas/boletas-reserva': 13,
|
||||
'/ventas/notas-credito': 14,
|
||||
'/compras/ofertas': 540000006,
|
||||
'/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). */
|
||||
export const COPY_FLOWS: Readonly<Record<string, readonly string[]>> = {
|
||||
'/ventas/ofertas': ['/ventas/ordenes', '/ventas/entrega', '/ventas/facturas', '/ventas/boletas'],
|
||||
'/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/facturas-reserva', '/ventas/boletas-reserva'],
|
||||
'/ventas/entrega': ['/ventas/facturas', '/ventas/boletas', '/ventas/devoluciones'],
|
||||
'/ventas/devoluciones': ['/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);
|
||||
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',
|
||||
].sort(),
|
||||
);
|
||||
|
||||
@@ -225,11 +225,14 @@ describe('DocumentFormPage', () => {
|
||||
describe('copiar a', () => {
|
||||
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());
|
||||
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], ['/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 () => {
|
||||
|
||||
@@ -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(13);
|
||||
expect(withPartner).toHaveLength(15);
|
||||
expect(without.sort()).toEqual(
|
||||
['InventoryGenEntries', 'InventoryGenExits', 'InventoryTransferRequests', 'PurchaseRequests', 'StockTransfers'].sort(),
|
||||
);
|
||||
|
||||
@@ -10,6 +10,8 @@ export const DOCUMENT_PATHS: readonly string[] = [
|
||||
'/ventas/entrega',
|
||||
'/ventas/facturas',
|
||||
'/ventas/boletas',
|
||||
'/ventas/facturas-reserva',
|
||||
'/ventas/boletas-reserva',
|
||||
'/ventas/devoluciones',
|
||||
'/ventas/notas-credito',
|
||||
// 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 INVOICES = ['/ventas/facturas', '/ventas/boletas', '/ventas/facturas-reserva', '/ventas/boletas-reserva'];
|
||||
|
||||
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('las cuatro comparten el recurso Invoices y se separan por ReserveInvoice y DocumentSubType', () => {
|
||||
for (const p of INVOICES) expect(by(p).resource, p).toBe('Invoices');
|
||||
// la factura solo excluye las boletas (`ne`): no pierde notas de débito ni otros subtipos
|
||||
expect(by('/ventas/facturas').baseFilter).toBe(`ReserveInvoice eq 'tNO' and DocumentSubType ne 'bod_Bill'`);
|
||||
expect(by('/ventas/boletas').baseFilter).toBe(`ReserveInvoice eq 'tNO' and DocumentSubType eq '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', () => {
|
||||
expect(by('/ventas/boletas').layout.beforeCreate!({ CardCode: 'C1' })).toEqual({ CardCode: 'C1', DocumentSubType: 'bod_Bill' });
|
||||
expect(by('/ventas/facturas').layout.beforeCreate).toBeUndefined();
|
||||
it('cada una se crea con su marca y una factura normal no envía nada especial', () => {
|
||||
const created = (p: string) => by(p).layout.beforeCreate?.({ CardCode: 'C1' });
|
||||
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', () => {
|
||||
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();
|
||||
}
|
||||
});
|
||||
|
||||
@@ -1,5 +1,11 @@
|
||||
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. */
|
||||
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/ordenes', 'Orders', 'Nueva orden', 'Cliente', { columns: referenceColumns }),
|
||||
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
|
||||
// excluye solo las boletas (no filtra por `bod_None`, para no perder notas de débito u otros subtipos).
|
||||
// Facturas, boletas y sus reservas son el mismo recurso (`Invoices`); se separan por dos campos:
|
||||
// `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', {
|
||||
...TAX_DOCUMENT,
|
||||
columns: invoiceColumns,
|
||||
baseFilter: `DocumentSubType ne 'bod_Bill'`,
|
||||
baseFilter: `${NOT_RESERVE} and ${NOT_BILL}`,
|
||||
}),
|
||||
marketing('/ventas/boletas', 'Invoices', 'Nueva boleta', 'Cliente', {
|
||||
...TAX_DOCUMENT,
|
||||
columns: invoiceColumns,
|
||||
baseFilter: `DocumentSubType eq 'bod_Bill'`,
|
||||
baseFilter: `${NOT_RESERVE} and ${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/notas-credito', 'CreditNotes', 'Nueva nota de crédito', 'Cliente', TAX_DOCUMENT),
|
||||
];
|
||||
Reference in new issue
Block a user