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:
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));
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user