feat(web): la cuenta asociada de los documentos de ventas ofrece solo las de activo y la de compras solo las de pasivo (listas locales en la ventana de selección)

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
GeorgeandClaude Sonnet 5.5 committed 2026-10-19 15:04:25 -05:00
1 parent 896fa25f2f
commit 57424432f9
12 files changed
+264 -8

No files matched your search

@@ -154,7 +154,7 @@
[readonly]="readOnly() || locked(f)"
[value]="str(header()[f.key])"
(valueChange)="setHeader(f.key, typedLookup(f, $event))"
[lookup]="f.lookup"
[lookup]="lookupOf(f)"
resolveOnLeave
(picked)="pickedLookup(f, $event)"
/>
@@ -8,11 +8,12 @@ import { map } from 'rxjs';
import { API_URL, apiErrorMessage } from '../../core/api';
import { formatNumber, todayIso } from '../../core/format';
import { CompanySettingsService } from '../../core/company/company-settings';
import { ControlAccountsService } from '../../core/sl/control-accounts';
import { SlDocumentApi } from '../../core/sl/sl-document';
import { CheckboxField, CheckboxInput, DateField, LookupField, NumberField, NumberInput, SboField, SelectField, SelectInput, TextField, TextInput, TextareaField, TextareaInput } from '../../shared/form';
import type { GridLine, GridMultiAdd } from '../../shared/grid/grid.types';
import { LinesGrid } from '../../shared/grid/lines-grid';
import type { VhLookup } from '../../shared/value-help/value-help.types';
import type { VhLookup, VhRow } from '../../shared/value-help/value-help.types';
import { Icon, MenuButton, type MenuItem, SboWindow, Tab, Tabs, Toolbar, type StatusMessage, type WindowBadge } from '../../shared/window';
import { ValueHelp } from '../../shared/value-help/value-help';
import type { ListRow } from '../../shared/list/list.types';
@@ -477,6 +478,7 @@ export class DocumentFormPage {
constructor() {
if (this.copyOrigin) void this.loadCopy(this.copyOrigin);
if (this.headerKeys.includes('ControlAccount')) void this.loadControlRows();
// al cargarse los datos del socio de un documento nuevo, sus direcciones por defecto
effect(() => {
const data = this.partnerData();
@@ -515,6 +517,30 @@ export class DocumentFormPage {
/** Tipo al que se quiere cambiar y que espera confirmación porque hay líneas escritas que se descartarían. */
protected readonly pendingType = signal<string | null>(null);
// --- cuenta asociada: de activo en ventas y de pasivo en compras ------------------------------------
/**
* Cuentas asociadas del grupo que corresponde al documento: las de activo en ventas (cuentas por cobrar) y las de
* pasivo en compras (por pagar). `null` mientras se calculan; `undefined` si no se pudieron calcular (se ofrecen todas).
*/
protected readonly controlRows = signal<VhRow[] | null | undefined>(null);
private readonly controlAccounts = inject(ControlAccountsService);
private async loadControlRows(): Promise<void> {
try {
this.controlRows.set(await this.controlAccounts.rows(this.partner?.type === 'cSupplier' ? 'liabilities' : 'assets'));
} catch {
this.controlRows.set(undefined);
}
}
/** La lista de un campo de cabecera; la de la cuenta asociada lleva solo las cuentas de su grupo. */
protected lookupOf(f: { key: string; lookup: VhLookup }): VhLookup {
if (f.key !== 'ControlAccount') return f.lookup;
const rows = this.controlRows();
return rows === undefined ? f.lookup : { ...f.lookup, rows: rows ?? [] };
}
/** Al elegir de una lista de la cabecera, los campos que esa elección completa (el solicitante trae su nombre, correo y departamento). */
protected pickedLookup(f: { onPick?: (row: Record<string, unknown>) => Record<string, unknown> }, row: Record<string, unknown>): void {
if (f.onPick) this.header.update((h) => ({ ...h, ...f.onPick!(row) }));
@@ -4,6 +4,7 @@ import { type ComponentFixture, TestBed } from '@angular/core/testing';
import { ActivatedRoute, Router, convertToParamMap, provideRouter } from '@angular/router';
import { of } from 'rxjs';
import { beforeEach, describe, expect, it, vi } from 'vitest';
import { ControlAccountsService } from '../../core/sl/control-accounts';
import { DocumentFormPage } from './document-form.page';
import { DOCUMENT_SCREENS } from './document-screens';
@@ -22,6 +23,11 @@ const PARTNER = {
],
};
const CONTROL_ROWS = {
assets: [{ Code: '_A1', FormatCode: '1212111010', Name: 'Por cobrar' }],
liabilities: [{ Code: '_P1', FormatCode: '4212111010', Name: 'Por pagar' }, { Code: '_P2', FormatCode: '4222111010', Name: 'Anticipos recibidos' }],
};
describe('pestañas Contenido, Logística y Finanzas', () => {
let fixture: ComponentFixture<DocumentFormPage>;
let ctrl: HttpTestingController;
@@ -39,7 +45,13 @@ describe('pestañas Contenido, Logística y Finanzas', () => {
const route = { snapshot: { data: { screen, title: 'Documento' }, paramMap: convertToParamMap({}) }, paramMap: of(convertToParamMap({})) };
await TestBed.configureTestingModule({
imports: [DocumentFormPage],
providers: [provideRouter([]), provideHttpClient(), provideHttpClientTesting(), { provide: ActivatedRoute, useValue: route }],
providers: [
provideRouter([]),
provideHttpClient(),
provideHttpClientTesting(),
{ provide: ActivatedRoute, useValue: route },
{ provide: ControlAccountsService, useValue: { rows: async (g: 'assets' | 'liabilities') => CONTROL_ROWS[g] } },
],
}).compileComponents();
ctrl = TestBed.inject(HttpTestingController);
vi.spyOn(TestBed.inject(Router), 'navigate').mockResolvedValue(true);
@@ -74,6 +86,20 @@ describe('pestañas Contenido, Logística y Finanzas', () => {
expect(sales.lookup.baseFilter).toContain('boptIncoming');
});
it('la cuenta asociada de ventas ofrece solo las de activo y la de compras solo las de pasivo', async () => {
await open('Orders');
await tick();
const field = () => page().layout.tabs.finance.left.find((f: { key?: string }) => f.key === 'ControlAccount');
expect(page().lookupOf(field()).rows.map((r: { Code: string }) => r.Code)).toEqual(['_A1']);
TestBed.resetTestingModule();
await open('PurchaseOrders');
await tick();
expect(page().lookupOf(field()).rows.map((r: { Code: string }) => r.Code)).toEqual(['_P1', '_P2']);
// los demás campos de lista no cambian
const terms = page().layout.tabs.finance.left.find((f: { key?: string }) => f.key === 'PaymentGroupCode');
expect(page().lookupOf(terms).rows).toBeUndefined();
});
it('el proyecto pasó de la cabecera a Finanzas y la persona de contacto está bajo el nombre del socio', async () => {
await open('Orders');
const left = page().layout.header.left as { type: string; key?: string }[];