Skip to content

Plataforma técnica · Sevastopol

Conciliacion Bancaria Island

Islands Sevastopol Finanzas

ConciliacionBancariaIsland opera el matching movimientos bancarios ↔ movimientos del sistema del período activo: pinta dos columnas de pendientes, distribuye montos por documento con rebalance automático, ofrece dos modos de cierre (vincular contra documentos y conciliar manual sin documentos) y separa una pestaña de conciliados con agrupación por movimiento bancario y diferencias visibles.

  • Directorysevastopol/src/components/islands/financieros/
    • ConciliacionBancariaIsland.tsx — componente único, internamente ConciliacionPanel
    • FinancierosWorkspace.tsx — tokens compartidos
  • Directorysevastopol/src/lib/hooks/
    • useActiveTenant.ts
    • useActivePeriod.ts

Dos pestañas controladas por activeTab. Ambas requieren un banco filtrado:

PestañaFunciónAcciones
pendientesTres listas: movimientos bancarios PENDIENTE (izq), documentos sistema (der), totales/pills (header).Vincular, Conciliar manual, Crear movimiento manual, Editar, Eliminar.
conciliadosConciliados del período agrupados por movimiento bancario.Buscar, Desconciliar (revierte el grupo).
const { tenantId } = useActiveTenant((_id) => {
void loadBancos();
setMovimientos([]);
setMovimientosSistema([]);
setConciliados([]);
clearSelection();
});
const { period } = useActivePeriod();
HookAportaReactividad
useActiveTenantUUID.Recarga bancos/categorías; vacía las tres listas y limpia la selección.
useActivePeriod{ year, month }.No re-fetcha automático; los valores entran al loadPeriodo() cuando el operador pulsa Cargar período.

Signals principales (hay >25 en total):

SignalTipoUso
activeTab"pendientes" | "conciliados"Pestaña visible.
bancos / categoriascatálogosSelector de banco activo + categorías de movimiento.
filtBancostringBanco filtrado (requerido para loadPeriodo).
movimientosMovimientoBancario[]Pendientes del banco/período.
movimientosSistemaMovimientoSistema[]Documentos del sistema candidatos del período.
conciliadosConciliacionResumen[]Filas conciliadas del período.
selectedMovIdstring | nullMovimiento bancario activo para matching.
checkedDocsRecord<string, boolean>Documentos sistema chequeados.
docMontosRecord<string, string>Monto asignado por documento (string para el <input>).
vinculando / manualClosing / desconciliandoId / confirmDesconciliarbanderasBloqueos de UI por acción en curso.
showModal / showEditModal / form / editFormmodal CRUDAlta/edición de movimientos manuales.

loadPeriodo() se ejecuta a demanda (botón Cargar período), requiere un banco filtrado y dispara 3 fetch en paralelo:

const paramsBanco = new URLSearchParams({
bancoId: filtBanco(), anio, mes,
estado: "PENDIENTE",
includeAnticiposProveedor: "true",
});
const paramsSistema = new URLSearchParams({ anio, mes });
const paramsResumen = new URLSearchParams({ bancoId: filtBanco(), anio, mes });
const [resBanco, resSistema, resResumen] = await Promise.all([
authenticatedFetch(`${API}/movimientos${buildQuery(paramsBanco)}`),
authenticatedFetch(`${API}/movimientos-sistema${buildQuery(paramsSistema)}`),
authenticatedFetch(`${API}/conciliacion/resumen${buildQuery(paramsResumen)}`),
]);

includeAnticiposProveedor=true agrega los movimientos marcados como anticipos al lado bancario para que el operador pueda cubrir compras con anticipo (ver más abajo).

Click en un movimiento bancario lo marca como activo (selectedMovId). Click otra vez lo deselecciona y limpia checks.

Click en un documento sistema lo toggle-a. Al chequear un documento positivo, la UI:

  1. Calcula getMontoSugeridoDoc(doc) = min(doc.monto, getMontoMaximoDoc(doc.id))
  2. Asigna ese monto
  3. Llama rebalancePositiveDocs() que rellena el monto disponible restante entre los demás documentos chequeados positivos
function rebalancePositiveDocs(nextChecked, nextMontos) {
let remainingCapacity = getMovimientoDisponible(mov)
- Σ(montos chequeados);
for (const doc of filteredSistema()) {
if (!nextChecked[doc.id] || doc.monto <= 0) continue;
const missing = Math.max(doc.monto - current, 0);
const increment = Math.min(missing, remainingCapacity);
updated[doc.id] = String(current + increment);
remainingCapacity -= increment;
if (remainingCapacity <= 0.01) break;
}
}

Esto significa: si chequeo 3 documentos de 100contraunmovimientode100 contra un movimiento de 250, se reparten 100/100/100/$50 automáticamente. Documentos con monto negativo (notas de crédito) no entran al rebalance — su monto se asigna directo.

Botón “Toggle all filtrados” (toggleAllFiltered): si todos los documentos visibles están chequeados, los des-chequea todos; si no, chequea los que falten respetando capacidad disponible.

canVincular requiere todo lo siguiente:

selectedMovId() !== null
&& Object.values(checkedDocs()).some(Boolean)
&& totalAsignado() > 0
&& !hasOverassignment() // montoDisponible() >= -0.01

canConciliarManual requiere lo opuesto (sin documentos chequeados) más categoría:

mov !== null
&& !selectedMovIsAnticipo() // los anticipos no pueden conciliarse manuales
&& !Object.values(checkedDocs()).some(Boolean)
&& mov.categoria_movimiento_id != null
&& Number(mov.monto_conciliado ?? 0) < 0.01
AcciónCuandoEndpoint
VincularHay documentos chequeados con suma positiva ≤ disponible.POST /conciliacion/vincular
Conciliar manualMovimiento sin documentos pero con categoria_movimiento_id.POST /conciliacion/manual

canConciliarManual exige monto_conciliado < 0.01 — si el movimiento ya fue parcialmente vinculado a documentos, conciliar manual deja de tener sentido.

Un movimiento bancario marcado es_anticipo_proveedor = true tiene tratamiento especial:

function isAnticipoProveedorMovimiento(mov) {
return Boolean(mov?.es_anticipo_proveedor);
}
function isDocumentoAplicableAnticipoProveedor(doc) {
return doc.tipo_contrapartida === "COMPRA_VENTA"
&& doc.tipo_documento === "COMPRA"
&& Number(doc.monto ?? 0) > 0.01;
}

Reglas que aplican sólo al anticipo:

ReglaEfecto
filteredSistema se filtra a sólo compras positivasEl anticipo cubre compras, no ventas ni notas.
canConciliarManual es false siempreUn anticipo debe cubrir documentos, no cerrarse sin contrapartida.
El selector de origen incluye "ANTICIPO" como pseudo-origenEl operador puede filtrar pendientes por anticipos.
El toast post-vincular dice “cubierto(s) con anticipo” en vez de “vinculado(s)“Lenguaje contable distinto.

Modal CRUD para movimientos bancarios manuales (caja chica, transferencias internas, intereses sin cartola). Campos:

CampoObligatorioNotas
banco_idPre-cargado con filtBanco() al abrir.
fecha_movimientoDefault hoy.
tipo_movimientoCARGO o ABONO.
montoparseFloat, > 0.
descripcionNo
glosaNo
numero_operacionNo
categoria_movimiento_idSí en altaSin categoría no se puede conciliar manual después.

El backend rechaza eliminar movimientos ya conciliados. La UI no lo previene — confía en el error del backend.

Agrupación por movimiento_id con totales por grupo:

const conciliadosAgrupados = createMemo<ConciliacionAgrupada[]>(() => {
const map = new Map<string, ConciliacionAgrupada>();
for (const c of conciliados()) {
if (!map.has(c.movimiento_id)) map.set(c.movimiento_id, {
mov: c, docs: [], totalMontoBanco: 0, totalMontoSistema: 0, totalDiferencia: 0,
});
const group = map.get(c.movimiento_id)!;
group.docs.push(c);
group.totalMontoBanco += Number(c.monto_banco ?? 0);
group.totalMontoSistema += Number(c.monto_sistema ?? 0);
group.totalDiferencia += Number(c.diferencia ?? 0);
}
return [...map.values()];
});

Filtros locales: texto libre (sobre campos del banco + documentos), tipo de movimiento, estado de la conciliación, tipo de documento. Las opciones de estado y tipo de documento se derivan de los datos cargados.

Desconciliar revierte la conciliación del grupo completo (todos los documentos del movimiento bancario vuelven a PENDIENTE):

const res = await authenticatedFetch(
`${API}/conciliacion/movimiento/${movimientoId}${tq()}`,
{ method: "DELETE" },
);

La acción exige confirmación previa (confirmDesconciliar mantiene el ID hasta que se confirma).

flowchart TB
  CHGT["useActiveTenant onChange"]
  MOUNT["onMount"]
  LOADB["loadBancos<br/>+ categorias"]
  SELB["filtBanco set"]
  LP["loadPeriodo<br/>Promise.all(3)"]
  M["movimientos: PENDIENTE"]
  D["movimientosSistema"]
  C["conciliados (resumen)"]
  SEL["selectMovimiento(id)"]
  TG["toggleDoc / toggleAllFiltered"]
  REB["rebalancePositiveDocs"]
  VINC["vincular()<br/>POST /conciliacion/vincular"]
  MAN["conciliarManual()<br/>POST /conciliacion/manual"]
  DESC["desconciliar()<br/>DELETE /conciliacion/movimiento/:id"]

  MOUNT --> LOADB
  CHGT --> LOADB
  LOADB --> SELB --> LP
  LP --> M
  LP --> D
  LP --> C
  M --> SEL
  D --> TG
  TG --> REB
  SEL --> VINC --> LP
  SEL --> MAN --> LP
  C --> DESC --> LP
MétodoRutaOperación
GET/api/financieros/bancos?tenant_id=...Catálogo de bancos.
GET/api/financieros/categorias-movimiento?tenant_id=...Categorías para conciliación manual.
GET/api/financieros/movimientos?bancoId&anio&mes&estado=PENDIENTE&includeAnticiposProveedor=true&tenant_idPendientes del banco.
GET/api/financieros/movimientos-sistema?anio&mes&tenant_idDocumentos sistema candidatos.
GET/api/financieros/conciliacion/resumen?bancoId&anio&mes&tenant_idConciliados del período.
POST/api/financieros/conciliacion/vincular?tenant_idAsocia movimiento con documentos (con montos).
POST/api/financieros/conciliacion/manual?tenant_idCierra movimiento sin documentos.
POST/api/financieros/movimientos?tenant_idAlta de movimiento manual.
PUT/api/financieros/movimientos/:id?tenant_idEdición.
DELETE/api/financieros/movimientos/:id?tenant_idEliminación.
DELETE/api/financieros/conciliacion/movimiento/:id?tenant_idDesconcilia el grupo completo.
ReglaMotivo
loadPeriodo exige banco filtradoEl backend devolvería el universo completo; sin un banco la UI no es navegable.
Las 3 listas se vacían al cambiar tenantEl matching anterior no es válido en otro tenant.
Rebalance automático al chequear documento positivoAsignar manualmente cada monto es repetitivo; la UI sugiere lo razonable y deja editar.
Documentos negativos sin rebalanceNotas de crédito y similares deben asignarse manualmente para evitar netteos artificiales.
Vincular bloqueado si excede disponiblePermitirlo causaría descuadres en el grupo conciliado.
Conciliar manual sólo sin documentos chequeadosCombinar ambos modos produce conciliaciones híbridas no soportadas por el backend.
Conciliar manual exige categoríaSin categoría el asiento generado no tiene contrapartida contable.
Anticipos sólo cubren compras positivasEl anticipo es un pre-pago de proveedor; aplicarlo a ventas o NC es semánticamente inválido.
Desconciliar revierte el grupo completoNo tiene sentido desconciliar 1 de 3 documentos de un mismo movimiento — el monto del banco no cuadraría.
Confirmación explícita para desconciliarAcción destructiva con efecto contable.
Errores con fallback err.error ?? "Error"El backend usa {error}; la UI no asume {message}.