feat(web): dirección del documento desde AddressExtension (texto y corrección con «…») y campos de lista que muestran el código con formato (cuenta asociada)

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
GeorgeandClaude Sonnet 5.5 committed 2026-10-19 14:58:30 -05:00
1 parent c1f2cea1e1
commit 896fa25f2f
9 files changed
+342 -14

No files matched your search

@@ -0,0 +1,66 @@
import { provideHttpClient } from '@angular/common/http';
import { HttpTestingController, provideHttpClientTesting } from '@angular/common/http/testing';
import { Component, signal } from '@angular/core';
import { type ComponentFixture, TestBed } from '@angular/core/testing';
import { beforeEach, describe, expect, it } from 'vitest';
import { ACCOUNT_CODES, AccountCodesService } from '../../core/sl/account-codes';
import { ACCOUNT_LOOKUP } from '../../features/documents/dimension-lookups';
import { LookupField } from './lookup-field';
@Component({
imports: [LookupField],
template: `<sbo-lookup-field label="Cuenta" resolveOnLeave [lookup]="lookup" [(value)]="value" />`,
})
class Host {
lookup = ACCOUNT_LOOKUP;
value = signal('');
}
describe('LookupField: lista con texto propio para el código (la cuenta)', () => {
let fixture: ComponentFixture<Host>;
let ctrl: HttpTestingController;
const input = () => (fixture.nativeElement as HTMLElement).querySelector('input') as HTMLInputElement;
const tick = async () => {
fixture.detectChanges();
await new Promise((r) => setTimeout(r));
fixture.detectChanges();
};
beforeEach(async () => {
await TestBed.configureTestingModule({ imports: [Host], providers: [provideHttpClient(), provideHttpClientTesting()] }).compileComponents();
ctrl = TestBed.inject(HttpTestingController);
TestBed.inject(AccountCodesService);
fixture = TestBed.createComponent(Host);
await tick();
});
it('muestra el código con formato en lugar del interno que está guardado', async () => {
ACCOUNT_CODES.remember({ Code: '_SYS77', FormatCode: '1212111010', Name: 'Cuentas por cobrar' });
fixture.componentInstance.value.set('_SYS77');
await tick();
expect(input().value).toBe('1212111010');
expect(fixture.componentInstance.value()).toBe('_SYS77'); // lo guardado sigue siendo el interno
});
it('pide el código con formato de un valor que llega sin conocerse', async () => {
fixture.componentInstance.value.set('_SYS88');
await tick();
const req = ctrl.expectOne((r) => r.url === '/api/sl/ChartOfAccounts' && r.params.get('$filter') === "Code eq '_SYS88'");
req.flush({ value: [{ Code: '_SYS88', FormatCode: '4012111010', Name: 'Proveedores' }] });
await tick();
expect(input().value).toBe('4012111010');
});
it('escribir el código con formato guarda el interno al salir del campo', async () => {
input().value = '1071111010';
input().dispatchEvent(new Event('input'));
await tick();
input().dispatchEvent(new Event('focusout', { bubbles: true }));
await tick();
const req = ctrl.expectOne((r) => r.url === '/api/sl/ChartOfAccounts' && (r.params.get('$filter') ?? '').includes("FormatCode eq '1071111010'"));
req.flush({ value: [{ Code: '_SYS99', FormatCode: '1071111010', Name: 'Banco' }] });
await tick();
expect(fixture.componentInstance.value()).toBe('_SYS99');
expect(input().value).toBe('1071111010');
});
});
+47 -3
View File
@@ -4,6 +4,7 @@ import {
booleanAttribute,
computed,
Injector,
effect,
inject,
input,
model,
@@ -37,7 +38,8 @@ import { SboField } from './sbo-field';
>
<sbo-input
#box
[(value)]="value"
[value]="shown()"
(valueChange)="typing($event)"
[readonly]="readOnly()"
[disabled]="disabled()"
[required]="required()"
@@ -93,6 +95,36 @@ export class LookupField extends FieldBase {
private resolved = '';
private readonly box = viewChild.required<TextInput>('box');
/** El usuario está escribiendo: se muestra lo que escribe y no el texto con que la lista presenta el código guardado. */
private readonly editing = signal(false);
/**
* Lo que se ve en el campo: el código guardado o, en una lista que tiene texto propio para él (la cuenta con su
* código con formato en vez del interno `_SYS…`), ese texto.
*/
protected readonly shown = computed(() => {
const v = this.value();
const codes = this.lookup().codes;
return !this.editing() && codes && v !== '' ? codes.display(v) : v;
});
constructor() {
super();
effect(() => {
const v = this.value();
const codes = this.lookup().codes;
// un valor que llega de fuera (documento cargado, valor por defecto) ya está resuelto y se pide su texto; lo que se escribe, no
if (!this.editing()) {
if (codes && v !== '') codes.ensure([v]);
this.resolved = v.trim();
}
});
}
protected typing(text: string): void {
this.editing.set(true);
this.value.set(text);
}
protected openList(): void {
if (!this.readOnly() && !this.disabled()) this.listOpen.set(true);
}
@@ -111,6 +143,8 @@ export class LookupField extends FieldBase {
private apply(row: VhRow): void {
const key = this.lookup().valueKey ?? this.lookup().columns[0]?.key ?? '';
this.missing.set(null);
this.lookup().codes?.remember(row);
this.editing.set(false);
this.value.set(String(row[key] ?? ''));
this.resolved = this.value();
this.picked.emit(row);
@@ -118,13 +152,21 @@ export class LookupField extends FieldBase {
protected async resolveTyped(): Promise<void> {
const code = this.value().trim();
if (!this.resolveOnLeave() || this.readOnly() || this.disabled() || this.listOpen()) return;
if (!this.editing()) return; // no se escribió nada: lo guardado ya está resuelto
if (!this.resolveOnLeave() || this.readOnly() || this.disabled() || this.listOpen()) {
this.editing.set(false);
return;
}
if (!code) {
this.missing.set(null);
this.resolved = '';
this.editing.set(false);
return;
}
if (code === this.resolved) {
this.editing.set(false);
return;
}
if (code === this.resolved) return;
try {
const row = await this.injector.get(SlLookupApi).find(this.lookup(), code);
if (this.value().trim() !== code) return; // el usuario siguió escribiendo
@@ -132,6 +174,8 @@ export class LookupField extends FieldBase {
else this.missing.set({ code, text: `No existe «${code}» en ${this.lookup().title.toLowerCase()}.` });
} catch {
this.missing.set({ code, text: 'No se pudo comprobar el código.' });
} finally {
if (this.value().trim() === code) this.editing.set(false);
}
}
}