feat(web): el stock por almacén totaliza en stock, comprometido y pedido en una fila bajo los títulos (columna total en las tablas de filas)

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
GeorgeandClaude Sonnet 5.5 committed 2026-10-19 19:06:21 -05:00
1 parent d05b497cb2
commit a659276d9a
3 files changed
+30 -3

No files matched your search

@@ -152,9 +152,9 @@ export const ARTICULOS_SCREENS: readonly MasterScreen[] = [
columns: [
{ key: 'WarehouseCode', title: 'Almacén', type: 'text', width: '110px' },
{ key: 'WarehouseName', title: 'Nombre', type: 'text', width: '260px', nameOf: { codeKey: 'WarehouseCode', resource: 'Warehouses', valueField: 'WarehouseCode', labelField: 'WarehouseName' } },
{ key: 'InStock', title: 'En stock', type: 'number', width: '110px' },
{ key: 'Committed', title: 'Comprometido', type: 'number', width: '110px' },
{ key: 'Ordered', title: 'Pedido', type: 'number', width: '110px' },
{ key: 'InStock', title: 'En stock', type: 'number', width: '110px', total: true },
{ key: 'Committed', title: 'Comprometido', type: 'number', width: '110px', total: true },
{ key: 'Ordered', title: 'Pedido', type: 'number', width: '110px', total: true },
{ key: 'MinimalStock', title: 'Mínimo', type: 'number', width: '100px' },
{ key: 'MaximalStock', title: 'Máximo', type: 'number', width: '100px' },
],
@@ -59,6 +59,8 @@ export interface DetailColumn {
lookup?: VhLookup;
/** Decimales fijos de una columna numérica; sin él, hasta 3. */
decimals?: number;
/** Se suma la columna en una fila «Total» al pie de la tabla (stock por almacén). */
total?: boolean;
/**
* Una celda que puede tomar su valor de varias listas (la «Cuenta / socio» de un asiento: una cuenta, un cliente o
* un proveedor). El botón «…» pregunta cuál abrir; un código escrito se busca en ellas en orden.
@@ -34,6 +34,17 @@ const dateOnly = (v: unknown): string => (typeof v === 'string' ? v.slice(0, 10)
<th class="del"></th>
}
</tr>
@if (hasTotals() && rows().length) {
<tr>
<td class="rn"></td>
@for (c of columns(); track c.key; let first = $first) {
<td class="tot" [class.num]="c.total">{{ c.total ? totalOf(c) : first ? 'Total' : '' }}</td>
}
@if (canRemove()) {
<td class="del"></td>
}
</tr>
}
</thead>
<tbody>
@for (row of rows(); track $index; let i = $index) {
@@ -132,6 +143,8 @@ const dateOnly = (v: unknown): string => (typeof v === 'string' ? v.slice(0, 10)
th.req::after { content: ' *'; color: var(--err); }
td { padding: 0 2px; height: 27px; border: 1px solid var(--line-soft); white-space: nowrap; }
td.ro { background: var(--field-ro); }
td.tot { padding: 0 8px; font-weight: 600; background: var(--grid-head); }
td.tot.num { text-align: right; }
td.rn, th.rn { width: 30px; text-align: center; color: var(--quiet); }
td.del, th.del { width: 30px; text-align: center; }
input, select { width: 100%; height: 22px; padding: 1px 4px; background: transparent; border: 1px solid transparent; border-radius: var(--radius); }
@@ -227,6 +240,18 @@ export class RowsTable {
return !this.readonly() && this.removable();
}
protected readonly hasTotals = computed(() => this.columns().some((c) => c.total));
/** La suma de una columna numérica en todas las filas. */
protected totalOf(c: DetailColumn): string {
let sum = 0;
for (const r of this.rows()) {
const v = Number(r[c.key]);
if (Number.isFinite(v)) sum += v;
}
return formatNumber(sum, c.decimals);
}
protected locked(c: DetailColumn, row?: Row): boolean {
return this.readonly() || !!c.readonly || !!c.computed || !!c.nameOf || (!!row && !!c.readonlyFor?.(row));
}