feat: módulo Finanzas (plan de cuentas y asientos contables con cancelación)

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
GeorgeandClaude Sonnet 5.5 committed 2026-10-16 14:36:24 -05:00
1 parent c9d1a714c9
commit 8bbd05d07b
23 files changed
+708 -29

No files matched your search

@@ -0,0 +1,8 @@
import type { MasterExtensions } from '../master-extensions';
/**
* Campos que este cliente agrega al plan de cuentas. En `main` va vacío: el proyecto base solo lleva campos
* estándar de SAP. La rama `cliente/<nombre>` lo completa, por ruta de pantalla (`/finanzas/plan-de-cuentas`),
* igual que las extensiones de artículos y de socios de negocio.
*/
export const FINANZAS_EXTENSIONS: MasterExtensions = {};
@@ -0,0 +1,87 @@
import type { SelectFilter } from '../../../core/sl/list-filters';
import type { HeaderField } from '../../documents/document-layouts';
import { ACCOUNT_LOOKUP } from '../../documents/dimension-lookups';
import { MASTER_LOOKUPS } from '../master-lookups';
import type { MasterScreen } from '../master-types';
const field = (f: HeaderField): HeaderField => f;
/** Tipos de cuenta de SAP (`BoAccountTypes`). */
const ACCOUNT_TYPES = [
{ value: 'at_Other', label: 'Otras' },
{ value: 'at_Revenues', label: 'Ingresos' },
{ value: 'at_Expenses', label: 'Gastos' },
];
/** Una cuenta de título agrupa a otras y no recibe asientos; la de movimiento (`ActiveAccount`) sí. */
const postableFilter: SelectFilter = {
type: 'select',
key: 'ActiveAccount',
label: 'Clase',
options: [
{ value: 'tYES', label: 'De movimiento' },
{ value: 'tNO', label: 'De título' },
],
};
/** Cuentas de título: las únicas que pueden ser la cuenta padre de otra. */
const TITLE_ACCOUNTS = { ...ACCOUNT_LOOKUP, title: 'Cuentas de título', baseFilter: `ActiveAccount eq 'tNO'` };
/** Plan de cuentas (`ChartOfAccounts`): solo campos estándar de SAP. */
export const FINANZAS_SCREENS: readonly MasterScreen[] = [
{
path: '/finanzas/plan-de-cuentas',
resource: 'ChartOfAccounts',
keyField: 'Code',
singular: 'Cuenta',
createLabel: 'Nueva cuenta',
orderby: 'Code',
extraFields: ['ActiveAccount'],
columns: [
{ key: 'Code', title: 'Código', width: '170px', mono: true, searchable: true },
{ key: 'Name', title: 'Nombre', searchable: true },
{ key: 'AccountLevel', title: 'Nivel', type: 'number', align: 'left', width: '70px' },
{
key: 'AccountType',
title: 'Tipo',
width: '100px',
format: (r) => ACCOUNT_TYPES.find((t) => t.value === r['AccountType'])?.label ?? String(r['AccountType'] ?? ''),
},
{ key: 'Balance', title: 'Saldo', type: 'number', decimals: 2, width: '140px' },
{
key: 'ActiveAccount',
title: 'Clase',
width: '120px',
format: (r) => (r['ActiveAccount'] === 'tYES' ? 'De movimiento' : 'De título'),
tone: (r) => (r['ActiveAccount'] === 'tYES' ? 'ok' : 'muted'),
},
],
filters: [postableFilter, { type: 'select', key: 'AccountType', label: 'Tipo', options: ACCOUNT_TYPES }],
layout: {
header: {
left: [
field({ type: 'text', key: 'Code', label: 'Código', immutable: true, required: true, missing: 'el código' }),
field({ type: 'text', key: 'Name', label: 'Nombre', required: true, missing: 'el nombre' }),
],
right: [field({ type: 'checkbox', key: 'ActiveAccount', label: 'Cuenta de movimiento', immutable: true })],
},
tabs: [
{
id: 'general',
label: 'General',
left: [
field({ type: 'lookup', key: 'FatherAccountKey', label: 'Cuenta padre', lookup: TITLE_ACCOUNTS, immutable: true }),
field({ type: 'select', key: 'AccountType', label: 'Tipo de cuenta', options: ACCOUNT_TYPES }),
field({ type: 'lookup', key: 'AcctCurrency', label: 'Moneda', lookup: MASTER_LOOKUPS.currency }),
],
right: [
field({ type: 'checkbox', key: 'CashAccount', label: 'Cuenta de caja' }),
field({ type: 'number', key: 'AccountLevel', label: 'Nivel', readonly: true }),
field({ type: 'number', key: 'Balance', label: 'Saldo', decimals: 2, readonly: true }),
],
},
],
newHeader: () => ({ ActiveAccount: 'tYES', AccountType: 'at_Other', CashAccount: 'tNO' }),
},
},
];
@@ -108,8 +108,8 @@ export class MasterField {
});
protected readonly ro = computed(() => {
const f = this.field();
if (f.type === 'text') return f.readonly || (f.immutable && this.saved());
return 'readonly' in f ? f.readonly : false;
if ('immutable' in f && f.immutable && this.saved()) return true;
return 'readonly' in f ? !!f.readonly : false;
});
protected readonly lookup = computed(() => {
const f = this.field();
@@ -20,7 +20,7 @@ export const masterRequiredOf = (layout: MasterLayout): { key: string; label: st
/** Campos que no se pueden cambiar una vez creado el registro (el código). */
export const masterImmutableOf = (layout: MasterLayout): ReadonlySet<string> =>
new Set(masterFieldsOf(layout).flatMap((f) => (f.type === 'text' && f.immutable ? [f.key] : [])));
new Set(masterFieldsOf(layout).flatMap((f) => ('immutable' in f && f.immutable ? [f.key] : [])));
/** Valores editables de un registro de SAP (los vacíos quedan como texto vacío). */
export function pickMaster(doc: DocRecord | undefined, keys: readonly string[], blank: DocRecord): DocRecord {
@@ -3,4 +3,4 @@
* configuración completa (`master-screens.ts`) se carga en diferido al abrir una. Un test comprueba que
* coincide con `MASTER_SCREENS`.
*/
export const MASTER_PATHS: readonly string[] = ['/socios/clientes', '/socios/proveedores', '/inventario/articulos'];
export const MASTER_PATHS: readonly string[] = ['/socios/clientes', '/socios/proveedores', '/inventario/articulos', '/finanzas/plan-de-cuentas'];
@@ -1,3 +1,5 @@
import { FINANZAS_EXTENSIONS } from './finanzas/finanzas.extensions';
import { FINANZAS_SCREENS } from './finanzas/finanzas.screens';
import { ARTICULOS_SCREENS } from './articulos/articulos.screens';
import { ARTICULOS_EXTENSIONS } from './articulos/articulos.extensions';
import { type MasterExtensions, applyMasterExtensions } from './master-extensions';
@@ -6,7 +8,7 @@ import { SOCIOS_SCREENS } from './socios-negocio/socios.screens';
import type { MasterScreen } from './master-types';
/** Las pantallas de maestros del proyecto base, agrupadas por módulo (una carpeta cada uno). */
export const BASE_MASTER_SCREENS: readonly MasterScreen[] = [...SOCIOS_SCREENS, ...ARTICULOS_SCREENS];
export const BASE_MASTER_SCREENS: readonly MasterScreen[] = [...SOCIOS_SCREENS, ...ARTICULOS_SCREENS, ...FINANZAS_SCREENS];
/** Cada módulo solo puede extender sus propias pantallas: una ruta ajena es un error, no un descuido silencioso. */
function ownedBy(screens: readonly MasterScreen[], extensions: MasterExtensions): MasterExtensions {
@@ -20,6 +22,7 @@ function ownedBy(screens: readonly MasterScreen[], extensions: MasterExtensions)
export const MASTER_EXTENSIONS: MasterExtensions = {
...ownedBy(SOCIOS_SCREENS, SOCIOS_EXTENSIONS),
...ownedBy(ARTICULOS_SCREENS, ARTICULOS_EXTENSIONS),
...ownedBy(FINANZAS_SCREENS, FINANZAS_EXTENSIONS),
};
/** Las pantallas con lo que cada cliente agrega (en `main`, igual al base). */