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:
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,
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in new issue
Block a user