feat(web): copiar de / a entre documentos de ventas y compras

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
GeorgeandClaude Sonnet 5.5 committed 2026-10-14 11:59:07 -05:00
1 parent cafe4bc035
commit 29e80181fe
8 files changed
+383 -6

No files matched your search

@@ -40,9 +40,9 @@ describe('DocumentFormPage', () => {
await new Promise((r) => setTimeout(r));
fixture.detectChanges();
};
const buttonText = () => [...el().querySelectorAll('.actions .btn')].map((b) => b.textContent?.trim());
const buttonText = () => [...el().querySelectorAll('.actions .start .btn')].map((b) => b.textContent?.trim());
async function open(docEntry: string | null, doc?: Record<string, unknown>) {
async function open(docEntry: string | null, doc?: Record<string, unknown>, copyFrom?: unknown, afterCreate?: () => void) {
const params = docEntry ? { docEntry } : {};
const route = {
snapshot: { data: { screen, title: 'Órdenes de venta' }, paramMap: convertToParamMap(params) },
@@ -53,11 +53,14 @@ describe('DocumentFormPage', () => {
providers: [provideRouter([]), provideHttpClient(), provideHttpClientTesting(), { provide: ActivatedRoute, useValue: route }],
}).compileComponents();
ctrl = TestBed.inject(HttpTestingController);
navigate = vi.spyOn(TestBed.inject(Router), 'navigate').mockResolvedValue(true);
const router = TestBed.inject(Router);
navigate = vi.spyOn(router, 'navigate').mockResolvedValue(true);
if (copyFrom) vi.spyOn(router, 'currentNavigation').mockReturnValue({ extras: { state: { copyFrom } } } as never);
fixture = TestBed.createComponent(DocumentFormPage);
await tick();
ctrl.expectOne('/api/sl/SalesTaxCodes?$select=Code,Rate&$top=500').flush({ value: [{ Code: 'IVA', Rate: 19 }] });
if (docEntry) ctrl.expectOne(`/api/sl/Orders/${docEntry}`).flush(doc ?? {});
afterCreate?.();
await tick();
}
@@ -218,4 +221,97 @@ describe('DocumentFormPage', () => {
expect(navigate).not.toHaveBeenCalled();
});
});
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 y factura', 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]]);
});
it('abre el documento nuevo de destino llevando el origen', async () => {
await open('41', order());
const nav = vi.spyOn(TestBed.inject(Router), 'navigateByUrl').mockResolvedValue(true);
page().copyTo('/ventas/entrega');
expect(nav).toHaveBeenCalledWith('/ventas/entrega/nuevo', { state: { copyFrom: { path: '/ventas/ordenes', docEntry: 41 } } });
});
it('con cambios sin guardar o cerrado no se puede copiar', async () => {
await open('41', order());
page().setHeader('Comments', 'x');
await tick();
expect(page().copyToItems().every((i: { disabled: boolean }) => i.disabled)).toBe(true);
TestBed.resetTestingModule();
await open('41', order({ DocumentStatus: 'bost_Close' }));
expect(page().copyToItems().every((i: { disabled: boolean }) => i.disabled)).toBe(true);
});
});
describe('copiar de / a: documento nuevo con origen', () => {
const source = {
DocEntry: 7, DocNum: 1007, CardCode: 'C1', CardName: 'ACME', NumAtCard: 'REF', DiscountPercent: 0,
DocumentLines: [
{ LineNum: 0, ItemCode: 'A1', ItemDescription: 'Pieza', Quantity: 4, RemainingOpenQuantity: 3, WarehouseCode: '01', Price: 100, DiscountPercent: 0, TaxCode: 'IVA', LineStatus: 'bost_Open' },
{ LineNum: 1, ItemCode: 'B2', Quantity: 1, RemainingOpenQuantity: 0, LineStatus: 'bost_Close' },
],
};
it('al llegar con un origen lo carga: socio, líneas abiertas y referencia al origen', async () => {
await open(null, undefined, { path: '/ventas/ordenes', docEntry: 7 }, () => undefined);
ctrl.expectOne('/api/sl/Orders/7').flush(source);
await tick();
expect(page().header().CardCode).toBe('C1');
expect(page().partnerName()).toBe('ACME');
expect(page().lines()).toHaveLength(1);
expect(page().lines()[0]).toMatchObject({ ItemCode: 'A1', Quantity: 3, BaseType: 17, BaseEntry: 7, BaseLine: 0 });
expect(el().querySelector('.status')?.textContent).toContain('Copiado de órdenes de venta 1007');
expect(el().querySelector('.status')?.textContent).toContain('1 línea(s) cerradas');
});
it('al crear envía las referencias al origen en las líneas', async () => {
await open(null, undefined, { path: '/ventas/ordenes', docEntry: 7 });
ctrl.expectOne('/api/sl/Orders/7').flush(source);
await tick();
(el().querySelector('.actions .btn.main') as HTMLElement).click();
await tick();
const req = ctrl.expectOne((r) => r.method === 'POST');
expect(req.request.body.DocumentLines).toEqual([
{ ItemCode: 'A1', ItemDescription: 'Pieza', Quantity: 3, WarehouseCode: '01', Price: 100, DiscountPercent: 0, TaxCode: 'IVA', BaseType: 17, BaseEntry: 7, BaseLine: 0 },
]);
req.flush({ DocEntry: 99, DocNum: 1099 });
});
it('un origen sin líneas abiertas no copia nada y avisa', async () => {
await open(null, undefined, { path: '/ventas/ordenes', docEntry: 7 });
ctrl.expectOne('/api/sl/Orders/7').flush({ ...source, DocumentLines: [source.DocumentLines[1]] });
await tick();
expect(page().lines()).toHaveLength(0);
expect(el().querySelector('.status')?.textContent).toContain('no tiene líneas abiertas');
});
it('Copiar de: no mezcla socios distintos', async () => {
await open(null);
page().setHeader('CardCode', 'C9');
await tick();
page().copyListFor.set('/ventas/ordenes');
const done = page().copyPicked([{ DocEntry: 7 }]);
await Promise.resolve();
ctrl.expectOne('/api/sl/Orders/7').flush(source);
await done;
await tick();
expect(page().lines()).toHaveLength(0);
expect(el().querySelector('.status')?.textContent).toContain('es de C1');
});
it('Copiar de: ofrece solo los orígenes del flujo y lista los abiertos del socio', async () => {
await open(null);
expect(page().copyFromItems().map((i: { id: string }) => i.id)).toEqual(['/ventas/ofertas']);
expect(page().copyListFilter()).toBe("DocumentStatus eq 'bost_Open' and Cancelled eq 'tNO'");
page().setHeader('CardCode', "C'1");
expect(page().copyListFilter()).toContain("CardCode eq 'C''1'");
});
});
});