fix(web): la grilla de líneas no aprieta las columnas (ancho mínimo = suma de anchos), el total va tras el impuesto y el proyecto de la cabecera abre su ventana

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
GeorgeandClaude Sonnet 5.5 committed 2026-10-16 11:09:47 -05:00
1 parent f66ef108cf
commit f077a61d08
11 files changed
+90 -13

No files matched your search

@@ -210,3 +210,38 @@ describe('copiar conserva dimensiones y proyecto', () => {
expect(objectLabel(-1)).toBe('');
});
});
describe('el proyecto de la cabecera se elige de su ventana modal', () => {
it('el campo tiene su botón «…» y abre la lista de proyectos', async () => {
TestBed.resetTestingModule();
const screen = DOCUMENT_SCREENS.find((s) => s.path === '/ventas/ordenes')!;
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 }],
}).compileComponents();
const ctrl = TestBed.inject(HttpTestingController);
const fixture = TestBed.createComponent(DocumentFormPage);
fixture.detectChanges();
await new Promise((r) => setTimeout(r));
fixture.detectChanges();
const el = fixture.nativeElement as HTMLElement;
const row = [...el.querySelectorAll('sbo-lookup-field')].find((f) => f.querySelector('label')?.textContent?.trim() === 'Proyecto')!;
expect(row, 'campo Proyecto con lista').toBeDefined();
(row.querySelector('button.sbo-btn') as HTMLElement).click();
fixture.detectChanges();
await new Promise((r) => setTimeout(r));
const req = ctrl.expectOne((r) => r.url === '/api/sl/Projects');
expect(req.request.params.get('$filter')).toContain(`Active eq 'tYES'`);
req.flush({ value: [{ Code: 'PE001', Name: 'Proyecto uno' }], 'odata.count': '1' });
fixture.detectChanges();
await new Promise((r) => setTimeout(r));
fixture.detectChanges();
(el.querySelector('.dlg tbody tr') as HTMLElement).click();
fixture.detectChanges();
expect(fixture.componentInstance['header']()['Project']).toBe('PE001');
});
});
@@ -91,6 +91,17 @@
(picked)="onPartner($event)"
/>
}
@case ('lookup') {
<sbo-lookup-field
[label]="f.label"
[required]="!!f.required"
[readonly]="readOnly()"
[value]="str(header()[f.key])"
(valueChange)="setHeader(f.key, $event)"
[lookup]="f.lookup"
resolveOnLeave
/>
}
@case ('warehouse') {
<sbo-lookup-field
[label]="f.label"
@@ -206,9 +206,9 @@ describe('columnas de línea por documento', () => {
it('sin elegir, el documento lleva todas las columnas y envía los mismos campos de siempre', () => {
const l = marketingLayout(base);
expect(l.columns.map((c) => c.key)).toEqual([
'ItemCode', 'ItemDescription', 'Quantity', 'WarehouseCode', 'Price', 'PriceAfterVAT', 'DiscountPercent', 'TaxCode',
'ItemCode', 'ItemDescription', 'Quantity', 'WarehouseCode', 'Price', 'PriceAfterVAT', 'DiscountPercent', 'TaxCode', 'LineTotal',
'CostingCode', 'CostingCode2', 'CostingCode3', 'CostingCode4', 'CostingCode5', 'ProjectCode', 'AccountCode',
'BaseType', 'BaseEntry', 'BaseLine', 'LineTotal',
'BaseType', 'BaseEntry', 'BaseLine',
]);
// lo que se envía por línea: las columnas editables (sin el total ni las de solo lectura) y la unidad de medida
expect(l.lines.fields).toEqual([
@@ -25,9 +25,9 @@ describe('columnas estándar de línea', () => {
it('son las de una línea de documento de SAP (con TaxCode)', () => {
expect(keys).toEqual([
'ItemCode', 'ItemDescription', 'Quantity', 'WarehouseCode', 'Price', 'PriceAfterVAT', 'DiscountPercent', 'TaxCode',
'ItemCode', 'ItemDescription', 'Quantity', 'WarehouseCode', 'Price', 'PriceAfterVAT', 'DiscountPercent', 'TaxCode', 'LineTotal',
'CostingCode', 'CostingCode2', 'CostingCode3', 'CostingCode4', 'CostingCode5', 'ProjectCode', 'AccountCode',
'BaseType', 'BaseEntry', 'BaseLine', 'LineTotal',
'BaseType', 'BaseEntry', 'BaseLine',
]);
});
@@ -152,6 +152,7 @@ export const DOCUMENT_LINE_COLUMNS: readonly GridColumn[] = [
apply: (tax, line) => ({ ...line, TaxCode: tax['Code'] }),
},
},
{ key: 'LineTotal', title: 'Total', type: 'number', decimals: 2, width: '110px', readonly: true },
dimension(1),
dimension(2),
dimension(3),
@@ -175,7 +176,6 @@ export const DOCUMENT_LINE_COLUMNS: readonly GridColumn[] = [
{ key: 'BaseType', title: 'Doc. base', width: '150px', readonly: true, format: (l) => (hasBase(l) ? objectLabel(l['BaseType']) : '') },
{ key: 'BaseEntry', title: 'Nº base', width: '80px', mono: true, readonly: true, format: (l) => (hasBase(l) ? String(l['BaseEntry']) : '') },
{ key: 'BaseLine', title: 'Línea base', width: '90px', mono: true, readonly: true, format: (l) => (hasBase(l) ? String(l['BaseLine'] ?? '') : '') },
{ key: 'LineTotal', title: 'Total', type: 'number', decimals: 2, width: '110px', readonly: true },
];
const columnOf = (key: string): GridColumn => DOCUMENT_LINE_COLUMNS.find((c) => c.key === key)!;
@@ -35,10 +35,11 @@ describe('extensiones por cliente', () => {
expect(headerKeysOf(s.layout)).toEqual(expect.arrayContaining(['U_Obra', 'U_Nota']));
});
it('agregan columnas de línea antes del total y las envían; y columnas de lista al final', () => {
it('agregan columnas de línea al final (después de las base) y las envían; y columnas de lista al final', () => {
const s = by(applyExtensions(BASE_SCREENS, EXT), '/ventas/facturas');
const keys = s.layout.columns.map((c) => c.key);
expect(keys.indexOf('U_Lote')).toBe(keys.indexOf('LineTotal') - 1);
expect(keys.at(-1)).toBe('U_Lote');
expect(keys.indexOf('LineTotal')).toBeLessThan(keys.indexOf('U_Lote'));
expect(s.layout.lines.fields).toContain('U_Lote');
expect(s.columns.at(-1)).toMatchObject({ key: 'U_Obra' });
});
@@ -14,7 +14,7 @@ export interface ScreenExtension {
headerRight?: readonly HeaderField[];
footerLeft?: readonly HeaderField[];
footerRight?: readonly HeaderField[];
/** Columnas de línea nuevas (antes del total, si el documento lo tiene); se envían a SAP. */
/** Columnas de línea nuevas, al final; se envían a SAP. */
lineColumns?: readonly GridColumn[];
/** Columnas nuevas de la lista, al final. */
listColumns?: readonly ListColumn[];
@@ -32,9 +32,7 @@ const withFields = (base: FieldColumns, left?: readonly HeaderField[], right?: r
function extendLayout(layout: DocumentLayout, ext: ScreenExtension): DocumentLayout {
const extra = ext.lineColumns ?? [];
const at = layout.columns.findIndex((c) => c.key === 'LineTotal');
const columns =
at < 0 ? [...layout.columns, ...extra] : [...layout.columns.slice(0, at), ...extra, ...layout.columns.slice(at)];
const columns = [...layout.columns, ...extra];
const fields = [...new Set([...layout.lines.fields, ...lineFieldsOf(extra)])];
const extended: DocumentLayout = {
...layout,
+1 -1
View File
@@ -10,7 +10,7 @@
</div>
<div class="gridwrap">
<table class="grid">
<table class="grid" [style.min-width.px]="tableMinWidth()">
<thead>
<tr>
<th class="rn">#</th>
@@ -422,3 +422,22 @@ describe('LinesGrid', () => {
});
});
});
describe('LinesGrid: ancho de la tabla', () => {
it('el ancho mínimo es la suma de los anchos de sus columnas, para no apretarlas', async () => {
await TestBed.configureTestingModule({
imports: [Host],
providers: [provideHttpClient(), provideHttpClientTesting()],
}).compileComponents();
const fixture = TestBed.createComponent(Host);
fixture.componentInstance.columns = [
{ key: 'A', title: 'A', width: '100px' },
{ key: 'B', title: 'B', width: '60px' },
{ key: 'C', title: 'C' }, // sin ancho: reserva 220
];
fixture.detectChanges();
await fixture.whenStable();
const table = (fixture.nativeElement as HTMLElement).querySelector<HTMLElement>('table.grid')!;
expect(parseFloat(table.style.minWidth)).toBe(100 + 60 + 220 + 34 + 30);
});
});
@@ -55,6 +55,19 @@ export class LinesGrid {
protected readonly lookupTarget = signal<LookupTarget | null>(null);
protected readonly multiOpen = signal(false);
protected readonly lookup = computed(() => this.lookupTarget()?.column.lookup?.config);
/**
* Ancho mínimo de la tabla: la suma de los anchos de sus columnas (las que no declaran uno, como la
* descripción, reservan 220 px), más el número de fila y el botón de quitar. Si no cabe en pantalla, la
* grilla se desplaza en horizontal en lugar de apretar las columnas hasta cortar su contenido.
*/
protected readonly tableMinWidth = computed(() => {
const px = (c: GridColumn): number => {
const w = parseFloat(c.width ?? '');
return Number.isFinite(w) ? w : 220;
};
return this.columns().reduce((sum, c) => sum + px(c), 34 + (this.readonly() ? 0 : 30));
});
protected readonly lookupMultiple = computed(() => !!this.lookupTarget()?.column.lookup?.multiple);
protected cellText(line: GridLine, column: GridColumn): string {