Skip to content

Plataforma técnica · Sevastopol

Libro Banco Island

Islands Sevastopol Finanzas

LibroBancoIsland es la vista contable de la cuenta bancaria y el centro de resolución de diferencias de conciliación. Reúne cinco pestañas:

  1. Libro — movimientos del banco con sus categorías y cuentas contables.
  2. Categorías — CRUD del catálogo de categorías de movimiento.
  3. Diferencias — conciliaciones del período con estado DIFERENCIA o PARCIAL, agrupadas por documento.
  4. Compensaciones — workspace para cruzar una venta con diferencia negativa contra facturas de proveedor del mismo emisor (resolución cruzada cliente↔proveedor).
  5. Resoluciones — historial de resoluciones aplicadas en el período.

Es la island de destino del botón “Abrir Libro de banco” en el paso incobrables de CicloContableIsland.

  • Directorysevastopol/src/components/islands/financieros/
    • LibroBancoIsland.tsx — componente único, internamente LibroBancoPanel
    • FinancierosWorkspace.tsx — tokens compartidos
  • Directorysevastopol/src/lib/hooks/
    • useActiveTenant.ts
    • useActivePeriod.ts
const { tenantId } = useActiveTenant((_id) => {
void loadBancos();
setRows([]);
setCategorias([]);
setDiferencias([]);
setIncobrableEditRow(null);
resetCompensacionWorkspace();
});
const { period } = useActivePeriod();

Cambiar tenant vacía todo y recarga catálogos. Cambiar período no re-fetcha automáticamente — el operador debe pulsar Cargar período (la operación toca 1-3 endpoints según la pestaña).

useActiveTenant carga 3 catálogos en paralelo: bancos, categorías-movimiento y plan de cuentas imputables (/api/admin/chart-of-accounts). El plan se filtra a es_imputable === true para los selectores.

const [activeTab, setActiveTab] = createSignal<
"libro" | "categorias" | "diferencias" | "compensaciones" | "resoluciones"
>("libro");
function switchTab(tab) {
setActiveTab(tab);
if ((tab === "diferencias" || tab === "compensaciones") && filtBanco()) {
void loadDiferencias();
}
if (tab === "resoluciones") {
void loadResoluciones();
}
}

Pestañas que disparan fetch al activarse:

PestañaCarga al activarEndpoint
libroNo (botón explícito).
categoriasNo (catálogo ya cargado).
diferenciasSí (si hay banco).GET /conciliacion/resumen?bancoId&anio&mes&acumulado=true
compensacionesSí (si hay banco).igual a diferencias
resolucionesSí (si hay año+mes).GET /resoluciones-conciliacion?anio&mes

Tabla del libro de banco — un movimiento por fila con su categoría asignada, código contable, cargo/abono y conteo de documentos vinculados.

const params = new URLSearchParams({ bancoId: filtBanco(), anio, mes });
const res = await authenticatedFetch(`${API}/libro-banco${buildQuery(params)}`);

Columnas:

ColumnaOrigenNotas
FechafmtDate(fecha_movimiento)
Tipotipo_movimientoCARGO o ABONO.
Descripción / Glosaconcat.
Categoríacategoria_codigo · categoria_nombre si null.
Cta. Contablecuenta_contable_codigo
Cargo / Abonomonto_cargo / monto_abonoColumnas separadas.
EstadoestadoPENDIENTE | CONCILIADO | CONCILIADO_PARCIAL.
Vínculosdocumentos_vinculadosConteo de docs en la conciliación.

Sin acciones inline en esta tabla — es read-only. La edición de movimientos ocurre en ConciliacionBancariaIsland.

CRUD de CategoriaMovimiento con tres modales (nueva, editar, eliminar). Cada categoría puede mapear a una cuenta contable del plan.

CampoObligatorioNotas
codigoSólo en altaInmutable post-creación.
nombre
aplica_aCARGO, ABONO, AMBOS.
descripcionNo
cuenta_contable_codigoNoSelector contra cuentas (imputables, ordenadas numéricamente).
ordenNoDefault 99. Define orden en selectores del libro.

El selector de cuenta usa un memo derivado del plan:

const cuentasOptions = createMemo(() =>
[...cuentas()]
.sort((a, b) => a.codigo.localeCompare(b.codigo, undefined, { numeric: true }))
.map((c) => ({ id: c.codigo, nombre: `${c.codigo} · ${c.nombre}` })),
);

Tabla de conciliaciones con estado IN ('DIFERENCIA', 'PARCIAL') y diferencia negativa significativa (la venta cobró menos de lo facturado). Se cargan con acumulado=true — incluye diferencias arrastradas de períodos anteriores no resueltas.

Cada fila expone dos acciones de resolución según el tipo de documento:

Para ventas/boletas con diferencia (incobrables). Pide dos cuentas (gasto + contraactivo) con defaults 3502003 y 1104002:

function openIncobrableCastigo(row) {
setIncobrableEditRow(row);
setIncobrableCuentaGasto(row.cuenta_gasto_codigo ?? "3502003");
setIncobrableCuentaContraactivo(row.cuenta_contraactivo_codigo ?? "1104002");
}

El POST contabiliza el castigo:

POST /api/financieros/conciliacion/:id/castigo-estimacion
{ anio, mes, cuenta_gasto_codigo, cuenta_contraactivo_codigo }

Reversa disponible vía PUT /castigo-estimacion/reversar (con confirmación explícita).

Estados visibles en la fila:

castigo_estimacion_estadoSignificado
nullNo se ha contabilizado castigo.
CONTABILIZADOCastigo activo, expone botón Reversar.
REVERSADOCastigo deshecho — la diferencia vuelve a estar abierta.

Workspace específico para resolver una diferencia cruzando documentos. La idea contable: una venta cobró 80cuandofacturoˊ80 cuando facturó 100; si el mismo emisor tiene una factura de proveedor de $20 pendiente, se compensa contra esa factura en vez de castigar.

const [res, resAplicadas] = await Promise.all([
authenticatedFetch(`${API}/conciliacion/documento/:id/compensacion-proveedor?anio&mes`),
authenticatedFetch(`${API}/conciliacion/documento/:id/compensaciones-proveedor`),
]);

La primera respuesta trae { documento, documentos_proveedor[] } — el detalle del documento cliente y los proveedores aplicables. La segunda trae las compensaciones ya aplicadas.

Toggle por documento proveedor; al chequear se calcula un monto sugerido conservador:

function suggestedCompensacionMonto(detalle, doc, maxDisponible) {
const monto = Math.min(
Math.abs(Number(detalle.diferencia_pendiente ?? 0)),
Math.max(Number(maxDisponible ?? Infinity), 0),
Number(doc.monto_pendiente ?? 0),
);
return monto > 0 ? String(Number(monto.toFixed(2))) : "";
}

El monto se acota por:

  1. La diferencia pendiente del documento cliente.
  2. El máximo todavía disponible (descontando otros seleccionados).
  3. El pendiente del documento proveedor.

updateCompensacionDocumentoMonto re-normaliza al modificar manualmente — nunca permite asignar más allá del disponible o del pendiente del proveedor.

const res = await authenticatedFetch(
`${API}/conciliacion/documento/${detalle.documento_cliente_id}/compensaciones-proveedor${tq()}`,
{
method: "POST",
body: JSON.stringify({
anio, mes,
documentos: documentos.map(item => ({
documento_id: item.documento_id,
monto: Number(parseFloat(item.monto).toFixed(2)),
})),
observaciones: compensacionObservacion().trim() || null,
}),
},
);

Tras aplicar: recarga diferencias y refresca el detalle de compensación (modo silent para no toastar de nuevo).

Botón en cada compensación aplicada → DELETE /conciliacion/compensacion-proveedor/:resolucionId. Pide confirmación y recarga ambos lados del workspace.

Historial de todas las resoluciones del período (castigo + compensación). Una fila por resolución con cliente↔proveedor, monto, observaciones, autor y fecha.

const params = new URLSearchParams({ anio: String(qAnio), mes: String(qMes) });
const res = await authenticatedFetch(`${API}/resoluciones-conciliacion${buildQuery(params)}`);

Acción única: Reversar (mismo endpoint que la reversa de compensación — comparten infraestructura). Pide confirmación que advierte explícitamente sobre el recálculo:

¿Reversar esta resolución? Se recalcularán los estados de conciliación y documentos afectados.
MétodoRutaPestaña
GET/api/financieros/bancos(carga inicial)
GET/api/financieros/categorias-movimiento(carga inicial)
GET/api/admin/chart-of-accounts(carga inicial, filtra es_imputable)
GET/api/financieros/libro-banco?bancoId&anio&meslibro
POST / PUT / DELETE/api/financieros/categorias-movimiento[/:id]categorias
GET/api/financieros/conciliacion/resumen?bancoId&anio&mes&acumulado=truediferencias, compensaciones
POST/api/financieros/conciliacion/:id/castigo-estimaciondiferencias
PUT/api/financieros/conciliacion/:id/castigo-estimacion/reversardiferencias
GET/api/financieros/conciliacion/documento/:id/compensacion-proveedor?anio&mescompensaciones
GET/api/financieros/conciliacion/documento/:id/compensaciones-proveedorcompensaciones
POST/api/financieros/conciliacion/documento/:id/compensaciones-proveedorcompensaciones
DELETE/api/financieros/conciliacion/compensacion-proveedor/:idcompensaciones, resoluciones
GET/api/financieros/resoluciones-conciliacion?anio&mesresoluciones

Todas usan tenant_id como query param.

ReglaMotivo
Diferencias se cargan con acumulado=trueLas diferencias arrastradas de períodos anteriores siguen pendientes hasta resolverse; ocultarlas crearía falsa sensación de cierre.
Castigo expone cuentas por defecto 3502003 / 1104002Plan estándar; el operador puede sobreescribir para casos no-estándar.
Compensación sólo para VENTA o BOLETA con diferencia < 0Una compensación tiene sentido cuando el cliente cobró menos; cuando cobró más es un anticipo.
Monto sugerido = mínimo de 3 cotasEvita over-allocate de la diferencia pendiente, del disponible y del pendiente del proveedor.
Re-normaliza monto al editarMantener la invariante: la suma de asignaciones ≤ diferencia pendiente, y cada monto ≤ pendiente del proveedor.
Switch a compensaciones precarga el documento clickeadoEl operador viene desde diferencias; cargar otra vez sería redundante.
Reversa de compensación es el mismo endpoint que reversa de resoluciónAmbas son tipo_resolucion = COMPENSACION_PROVEEDOR en backend.
Confirmación explícita en castigo, compensación, resolucionesTres acciones contables irreversibles desde la UI.
Cargas en paralelo en carga inicialBancos, categorías y plan son independientes; serializar agrega latencia visible.
Libro sin acciones inlineLa edición es responsabilidad de ConciliacionBancariaIsland; mantener la separación de roles entre vistas.