feat(web): código de cuenta con separadores de segmento, ramas vacías sin flecha y Cuenta de mayor con formato

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
GeorgeandClaude Sonnet 5.5 committed 2026-10-19 09:56:48 -05:00
1 parent 456b0f4ac0
commit a10de1ff29
11 files changed
+108 -6

No files matched your search

@@ -1,3 +1,4 @@
import { formatAccountCode } from '../../core/sl/account-segments';
import type { VhLookup } from '../../shared/value-help/value-help.types';
/** Dimensión de costos (centro de costo, `CostingCode` 1 a 5): reglas de distribución activas de esa dimensión. */
@@ -45,6 +46,6 @@ export const ACCOUNT_LOOKUP: VhLookup = {
{ key: 'Code', title: 'Código', width: '160px', mono: true },
{ key: 'Name', title: 'Nombre' },
// en un plan de cuentas segmentado, `Code` es interno (`_SYS…`): se muestra también el código con formato
{ key: 'FormatCode', title: 'Cuenta', width: '140px', mono: true },
{ key: 'FormatCode', title: 'Cuenta', width: '140px', mono: true, format: (r) => formatAccountCode(String(r['FormatCode'] ?? '')) },
],
};
@@ -40,6 +40,10 @@ describe('árbol del plan de cuentas', () => {
expect(flattenBranch(root, new Map(), new Set(['1']))[0]).toMatchObject({ expandable: true, expanded: false });
});
it('una cuenta de título sin hijas no se puede desplegar', () => {
expect(flattenBranch(root, new Map([['1', []]]), new Set(['1']))[0]).toMatchObject({ expandable: false, expanded: false });
});
it('el nivel máximo oculta las cuentas más profundas', () => {
expect(flattenBranch(root, children, new Set(['1', '1-1']), 2).map((r) => r.account.Code)).toEqual(['1', '1-1', '1-2']);
});
@@ -1,3 +1,5 @@
import { formatAccountCode } from '../../../core/sl/account-segments';
/** Campos de cada cuenta que pide el árbol. */
export const TREE_FIELDS = 'Code,FormatCode,Name,AccountLevel,FatherAccountKey,ActiveAccount,AccountType,Balance,PrimaryAccount';
@@ -19,7 +21,7 @@ export interface TreeAccount {
* Código que ve el usuario: el `FormatCode` y, si la cuenta no lo tiene (las de título de un plan segmentado,
* o cualquiera en uno sin segmentar), su `Code`. La clave que se envía a SAP es siempre `Code`.
*/
export const displayCode = (a: { Code: string; FormatCode?: string | null }): string => (a.FormatCode && a.FormatCode.trim() !== '' ? a.FormatCode : a.Code);
export const displayCode = (a: { Code: string; FormatCode?: string | null }): string => (a.FormatCode && a.FormatCode.trim() !== '' ? formatAccountCode(a.FormatCode) : a.Code);
export interface TreeRow {
account: TreeAccount;
@@ -37,7 +39,8 @@ export interface TreeRow {
export function flattenBranch(root: TreeAccount, children: ReadonlyMap<string, readonly TreeAccount[]>, expanded: ReadonlySet<string>, maxLevel = Infinity): TreeRow[] {
const out: TreeRow[] = [];
const walk = (a: TreeAccount, depth: number): void => {
const expandable = a.ActiveAccount !== 'tYES';
// una cuenta de título cuyas hijas ya se pidieron y no hay ninguna se muestra como hoja (sin flecha)
const expandable = a.ActiveAccount !== 'tYES' && children.get(a.Code)?.length !== 0;
const open = expandable && expanded.has(a.Code) && children.has(a.Code);
out.push({ account: a, depth, expandable, expanded: open });
if (!open) return;
@@ -52,6 +52,9 @@
} @empty {
<div class="none">{{ error() || (loading() ? 'Cargando el plan de cuentas…' : 'Sin cuentas.') }}</div>
}
@if (rows().length === 1 && !rows()[0].expandable && !loading()) {
<div class="none">Este grupo no tiene cuentas.</div>
}
</div>
}
</div>
@@ -81,7 +81,9 @@ export class ChartTreePanel {
private async loadGroups(): Promise<void> {
try {
this.groups.set(await this.list('AccountLevel eq 1'));
const groups = await this.list('AccountLevel eq 1');
this.groups.set(groups);
if (groups[0] && !this.selected()) void this.openRoot(groups[0].Code);
} catch (e) {
this.error.set(apiErrorMessage(e));
} finally {
@@ -136,6 +138,18 @@ export class ChartTreePanel {
protected pickGroup(code: string): void {
this.picked.set(code);
void this.openRoot(code);
}
/** Al ver un grupo se muestran sus cuentas de primer nivel sin que el usuario tenga que desplegarlo. */
private async openRoot(code: string): Promise<void> {
if (this.expanded().has(code)) return;
try {
await this.load(code);
this.expanded.update((s) => new Set(s).add(code));
} catch (e) {
this.error.set(apiErrorMessage(e));
}
}
protected setLevel(value: string): void {
this.level.set(Number(value) || 10);
@@ -9,6 +9,7 @@ import { RowsTable } from '../../shared/grid/rows-table';
import { Icon, SboWindow, Tab, Tabs, Toolbar, type StatusMessage, type WindowBadge } from '../../shared/window';
import { updatePayload, type DocRecord } from '../documents/document-model';
import type { HeaderField } from '../documents/document-layouts';
import { displayCode } from './finanzas/chart-tree.model';
import { ChartTreePanel } from './finanzas/chart-tree.panel';
import { MasterField } from './master-field';
import {
@@ -131,7 +132,7 @@ export class MasterFormPage {
});
protected readonly windowTitle = computed(() => {
const d = this.doc();
const code = (this.screen.displayField && text(d?.[this.screen.displayField]).trim()) || d?.[this.screen.keyField];
const code = this.screen.displayField && d ? displayCode({ Code: text(d[this.screen.keyField]), FormatCode: text(d[this.screen.displayField]) }) : d?.[this.screen.keyField];
return code === undefined ? this.screen.singular : `${this.screen.singular} · ${text(code)}`;
});
@@ -178,6 +179,11 @@ export class MasterFormPage {
}
protected value(key: string): unknown {
// el código de una cuenta ya creada se ve con su formato (el `_SYS…` interno no es lo que el usuario conoce)
if (this.screen.displayField && key === this.screen.keyField && !this.isNew()) {
const d = this.doc();
return displayCode({ Code: text(d?.[this.screen.keyField]), FormatCode: text(d?.[this.screen.displayField]) });
}
return this.header()[key];
}