Plataforma técnica · Sevastopol
Conciliacion Bancaria Island
Islands Sevastopol Finanzas
Propósito
Section titled “Propósito”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.
Ubicación
Section titled “Ubicación”Directorysevastopol/src/components/islands/financieros/
- ConciliacionBancariaIsland.tsx — componente único, internamente
ConciliacionPanel - FinancierosWorkspace.tsx — tokens compartidos
- ConciliacionBancariaIsland.tsx — componente único, internamente
Directorysevastopol/src/lib/hooks/
- useActiveTenant.ts
- useActivePeriod.ts
Composición
Section titled “Composición”Dos pestañas controladas por activeTab. Ambas requieren un banco filtrado:
| Pestaña | Función | Acciones |
|---|---|---|
pendientes | Tres listas: movimientos bancarios PENDIENTE (izq), documentos sistema (der), totales/pills (header). | Vincular, Conciliar manual, Crear movimiento manual, Editar, Eliminar. |
conciliados | Conciliados del período agrupados por movimiento bancario. | Buscar, Desconciliar (revierte el grupo). |
Hooks y estado global
Section titled “Hooks y estado global”const { tenantId } = useActiveTenant((_id) => { void loadBancos(); setMovimientos([]); setMovimientosSistema([]); setConciliados([]); clearSelection();});const { period } = useActivePeriod();| Hook | Aporta | Reactividad |
|---|---|---|
useActiveTenant | UUID. | 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):
| Signal | Tipo | Uso |
|---|---|---|
activeTab | "pendientes" | "conciliados" | Pestaña visible. |
bancos / categorias | catálogos | Selector de banco activo + categorías de movimiento. |
filtBanco | string | Banco filtrado (requerido para loadPeriodo). |
movimientos | MovimientoBancario[] | Pendientes del banco/período. |
movimientosSistema | MovimientoSistema[] | Documentos del sistema candidatos del período. |
conciliados | ConciliacionResumen[] | Filas conciliadas del período. |
selectedMovId | string | null | Movimiento bancario activo para matching. |
checkedDocs | Record<string, boolean> | Documentos sistema chequeados. |
docMontos | Record<string, string> | Monto asignado por documento (string para el <input>). |
vinculando / manualClosing / desconciliandoId / confirmDesconciliar | banderas | Bloqueos de UI por acción en curso. |
showModal / showEditModal / form / editForm | modal CRUD | Alta/edición de movimientos manuales. |
Flujo de carga
Section titled “Flujo de carga”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).
Selección y rebalance
Section titled “Selección y rebalance”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:
- Calcula
getMontoSugeridoDoc(doc) = min(doc.monto, getMontoMaximoDoc(doc.id)) - Asigna ese monto
- 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 250, se reparten 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.
Validaciones de vinculación
Section titled “Validaciones de vinculación”canVincular requiere todo lo siguiente:
selectedMovId() !== null&& Object.values(checkedDocs()).some(Boolean)&& totalAsignado() > 0&& !hasOverassignment() // montoDisponible() >= -0.01canConciliarManual 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ón | Cuando | Endpoint |
|---|---|---|
| Vincular | Hay documentos chequeados con suma positiva ≤ disponible. | POST /conciliacion/vincular |
| Conciliar manual | Movimiento 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.
Anticipos a proveedor
Section titled “Anticipos a proveedor”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:
| Regla | Efecto |
|---|---|
filteredSistema se filtra a sólo compras positivas | El anticipo cubre compras, no ventas ni notas. |
canConciliarManual es false siempre | Un anticipo debe cubrir documentos, no cerrarse sin contrapartida. |
El selector de origen incluye "ANTICIPO" como pseudo-origen | El operador puede filtrar pendientes por anticipos. |
| El toast post-vincular dice “cubierto(s) con anticipo” en vez de “vinculado(s)“ | Lenguaje contable distinto. |
Movimientos manuales
Section titled “Movimientos manuales”Modal CRUD para movimientos bancarios manuales (caja chica, transferencias internas, intereses sin cartola). Campos:
| Campo | Obligatorio | Notas |
|---|---|---|
banco_id | Sí | Pre-cargado con filtBanco() al abrir. |
fecha_movimiento | Sí | Default hoy. |
tipo_movimiento | Sí | CARGO o ABONO. |
monto | Sí | parseFloat, > 0. |
descripcion | No | — |
glosa | No | — |
numero_operacion | No | — |
categoria_movimiento_id | Sí en alta | Sin 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.
Pestaña conciliados
Section titled “Pestaña conciliados”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
Endpoints consumidos
Section titled “Endpoints consumidos”| Método | Ruta | Operació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_id | Pendientes del banco. |
GET | /api/financieros/movimientos-sistema?anio&mes&tenant_id | Documentos sistema candidatos. |
GET | /api/financieros/conciliacion/resumen?bancoId&anio&mes&tenant_id | Conciliados del período. |
POST | /api/financieros/conciliacion/vincular?tenant_id | Asocia movimiento con documentos (con montos). |
POST | /api/financieros/conciliacion/manual?tenant_id | Cierra movimiento sin documentos. |
POST | /api/financieros/movimientos?tenant_id | Alta de movimiento manual. |
PUT | /api/financieros/movimientos/:id?tenant_id | Edición. |
DELETE | /api/financieros/movimientos/:id?tenant_id | Eliminación. |
DELETE | /api/financieros/conciliacion/movimiento/:id?tenant_id | Desconcilia el grupo completo. |
Reglas de UI
Section titled “Reglas de UI”| Regla | Motivo |
|---|---|
loadPeriodo exige banco filtrado | El backend devolvería el universo completo; sin un banco la UI no es navegable. |
| Las 3 listas se vacían al cambiar tenant | El matching anterior no es válido en otro tenant. |
| Rebalance automático al chequear documento positivo | Asignar manualmente cada monto es repetitivo; la UI sugiere lo razonable y deja editar. |
| Documentos negativos sin rebalance | Notas de crédito y similares deben asignarse manualmente para evitar netteos artificiales. |
| Vincular bloqueado si excede disponible | Permitirlo causaría descuadres en el grupo conciliado. |
| Conciliar manual sólo sin documentos chequeados | Combinar ambos modos produce conciliaciones híbridas no soportadas por el backend. |
| Conciliar manual exige categoría | Sin categoría el asiento generado no tiene contrapartida contable. |
| Anticipos sólo cubren compras positivas | El anticipo es un pre-pago de proveedor; aplicarlo a ventas o NC es semánticamente inválido. |
| Desconciliar revierte el grupo completo | No tiene sentido desconciliar 1 de 3 documentos de un mismo movimiento — el monto del banco no cuadraría. |
| Confirmación explícita para desconciliar | Acción destructiva con efecto contable. |
Errores con fallback err.error ?? "Error" | El backend usa {error}; la UI no asume {message}. |