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:
GeorgeandClaude Sonnet 5.5 committed 2026-10-15 17:32:32 -05:00
1 parent 1834c64f6f
commit 52dc1957b7
12 files changed
+70 -27

No files matched your search

@@ -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),
];