Skip to content

Plataforma técnica · Sevastopol

Activos Fijos Island

Activo Fijo Islands Sevastopol

ActivosFijosIsland es la island SolidJS que opera todo el ciclo de activo fijo desde Sevastopol: reconocimiento inicial, baja, mantenimiento de categorías y depreciación periódica. Es un único componente con tres pestañas; toda la persistencia y la validación ocurren en Orchestrator y la island actúa como interfaz operativa y consumidora del proxy /api/activo-fijo.

  • Directorysevastopol/src/components/islands/contabilidad/
    • ActivosFijosIsland.tsx — componente único, 3 pestañas
  • Directorysevastopol/src/pages/api/activo-fijo/
    • index.ts
    • […path].ts — proxy genérico a Orchestrator
  • Directorysevastopol/src/lib/hooks/
    • useActiveTenant.ts
    • useActivePeriod.ts
PestañaFunciónEndpoint base
activosConsulta, alta y baja de activos./api/activo-fijo/activos
categoriasAlta y edición de categorías tributarias, vida útil y cuentas./api/activo-fijo/categorias
depreciacionConsulta, generación, contabilización, reversa y eliminación de cuotas./api/activo-fijo/depreciacion

La pestaña activa vive en una signal local; las tres dependen del tenant activo y la pestaña de depreciación además del periodo activo global.

const [tab, setTab] = createSignal<'activos' | 'categorias' | 'depreciacion'>('activos');
const AF_API = '/api/activo-fijo';

La island consume dos hooks compartidos del repositorio Sevastopol. Ambos disparan recargas reactivas cuando su valor cambia.

import { useActiveTenant } from '@/lib/hooks/useActiveTenant';
import { useActivePeriod } from '@/lib/hooks/useActivePeriod';
const tenantId = useActiveTenant();
const { period } = useActivePeriod(() => {
if (tab() !== 'depreciacion') return;
const tid = tenantId();
if (!tid) return;
void loadDepreciacion(tid);
});
HookAportaReactividad
useActiveTenantUUID de la base tenant seleccionada en TenantSelectorBar.Cambia al cambiar tenant.
useActivePeriod{ year, month } global del contexto contable.Cambia al cambiar periodo; el callback dispara la recarga sólo cuando la pestaña activa es depreciacion.

El estado de fila (activos, categorias, depRows), formularios (crearActivoForm, catForm, generarForm) y banderas de loading son todas signals locales (createSignal), sin store global propio.

  1. Antes de mostrar el modal, precargar compras disponibles, tabla de vida útil y categorías.

    await loadCompras(tid);
    await loadVidaUtil();
    await loadCategorias(tid);
    resetCrearForm();
    setShowCrearActivo(true);
  2. Validar campos obligatorios antes de enviar.

    const f = crearActivoForm();
    if (!f.categoria_id || !f.nombre || !f.monto_neto || !f.fecha_compra) {
    toast.push('Completa los campos obligatorios', 'error');
    return;
    }
  3. Construir el cuerpo; la fecha de inicio de depreciación se iguala a la fecha de compra.

    const body: Record<string, any> = {
    categoria_id: Number(f.categoria_id),
    nombre: f.nombre.trim(),
    rut_proveedor: f.rut_proveedor.trim(),
    monto_neto: Number(f.monto_neto),
    fecha_compra: f.fecha_compra,
    fecha_inicio_dep: f.fecha_compra,
    };
  4. Si hay compra de origen, enriquecer el cuerpo con datos de la línea.

    if (f.compra_id) {
    const compra = compras().find((c) => c.id === f.compra_id);
    if (compra) {
    body.compra_id = compra.id;
    body.rut_proveedor = compra.rut_emisor ?? body.rut_proveedor;
    body.compra_año = compra.año;
    body.compra_mes = compra.mes;
    if (compra.folio) body.folio_factura = String(compra.folio);
    }
    }
  5. Enviar al proxy local y recargar la lista.

    const r = await authenticatedFetch(`${AF_API}/activos?tenant_id=${tid}`, {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify(body),
    });
    await handleAuthResponse(r);
    if (r.ok) {
    toast.push('Activo creado correctamente', 'success');
    setShowCrearActivo(false);
    await loadActivos(tid);
    }

Solo aplica sobre activos en estado ACTIVO. El modal exige fecha y motivo opcional.

const r = await authenticatedFetch(`${AF_API}/activos/${id}/baja?tenant_id=${tid}`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ estado: 'DADO_DE_BAJA', fecha_baja, motivo_baja }),
});
ColumnaOrigen
Activonombre
Categoríanombre_categoria
Comprafecha_compra
Monto Netomonto_neto
Valor Librovalor_libro_actual
Depreciadopct_depreciado
Estadoestado

La categoría es el contrato contable del activo. Se crea antes que cualquier activo y fija las tres cuentas contables y la vida útil normal/acelerada.

  1. Validar todos los campos, incluida la selección SII.

    if (
    !f.nombre || !f.seccion_sii || !f.numero_item_sii ||
    !f.vida_util_anos || !f.dep_acelerada_anos ||
    !f.cuenta_activo_codigo || !f.cuenta_dep_acumulada_codigo || !f.cuenta_gasto_dep_codigo
    ) {
    toast.push('Completa todos los campos obligatorios (incluye selección SII)', 'error');
    return;
    }
  2. Enviar la categoría al proxy local y recargar.

    const r = await authenticatedFetch(`${AF_API}/categorias?tenant_id=${tid}`, {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({
    nombre: f.nombre.trim(),
    seccion_sii: f.seccion_sii,
    vida_util_anos: Number(f.vida_util_anos),
    dep_acelerada_anos: Number(f.dep_acelerada_anos),
    cuenta_activo_codigo: f.cuenta_activo_codigo.trim(),
    cuenta_dep_acumulada_codigo: f.cuenta_dep_acumulada_codigo.trim(),
    cuenta_gasto_dep_codigo: f.cuenta_gasto_dep_codigo.trim(),
    }),
    });

Las cuentas contables se eligen desde el plan contable del tenant, filtradas por cuentas padre fijas: 1201000 para activo y 1202000 para depreciación acumulada.

El periodo se arma desde useActivePeriod y se consulta sólo cuando la pestaña está activa.

function activeDepPeriodoMes() {
const now = new Date();
const ano = String(period().year ?? now.getFullYear());
const mes = String(period().month ?? (now.getMonth() + 1)).padStart(2, '0');
return `${ano}-${mes}-01`;
}
async function loadDepreciacion(tid: string) {
const periodoMes = activeDepPeriodoMes();
const r = await authenticatedFetch(`${AF_API}/depreciacion/periodo/${periodoMes}?tenant_id=${tid}`);
await handleAuthResponse(r);
setDepRows(r.ok ? await r.json() : []);
}

La UI envía categoría, periodo y tipo de depreciación; Orchestrator procesa cada activo elegible de esa categoría y devuelve { procesados, omitidos, errores }.

const r = await authenticatedFetch(`${AF_API}/depreciacion/generar-categoria?tenant_id=${tid}`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
categoria_id: Number(f.categoria_id),
periodo_mes: f.periodo_mes,
tipo_depreciacion: f.tipo_depreciacion,
...(f.notas ? { notas: f.notas } : {}),
}),
});
const { procesados, omitidos, errores } = await r.json();
if (procesados > 0) toast.push(`${procesados} activo(s) depreciado(s)`, 'success');
else if (omitidos > 0) toast.push(`${omitidos} ya tenían cuota para este período`, 'success');
for (const err of errores as string[]) toast.push(err, 'error');

La fila de cada cuota expone acciones distintas según el estado, para evitar acciones inválidas desde la UI:

EstadoAcción visibleEndpoint
PENDIENTEContabilizarPUT /depreciacion/:id/contabilizar
PENDIENTEEliminarDELETE /depreciacion/:id
CONTABILIZADOReversarPUT /depreciacion/:id/reversar-contabilizacion
const ok = confirm(
'¿Reversar contabilización de esta depreciación? Se eliminarán sus líneas de movimiento y quedará en PENDIENTE.',
);
if (!ok) return;
ColumnaOrigen
Activonombre_activo
Periodoperiodo_mes
Tipotipo_depreciacion
Númeroperiodo_numero
Cuotacuota_depreciacion
Depreciación acumuladadep_acumulada_posterior
Valor librovalor_libro
Estadoestado
MétodoRutaOperación
GET/api/activo-fijo/activosLista activos por estado.
POST/api/activo-fijo/activosDa de alta un activo.
PUT/api/activo-fijo/activos/:id/bajaDa de baja un activo.
GET/api/activo-fijo/categoriasLista categorías (todas=true incluye inactivas).
POST/api/activo-fijo/categoriasCrea una categoría.
PUT/api/activo-fijo/categorias/:idActualiza una categoría.
GET/api/activo-fijo/comprasLista compras SII disponibles para alta.
GET/api/activo-fijo/depreciacion/periodo/:yyyy-mm-01Cuotas del periodo.
POST/api/activo-fijo/depreciacion/generar-categoriaGenera cuotas para una categoría.
PUT/api/activo-fijo/depreciacion/:id/contabilizarContabiliza una cuota pendiente.
PUT/api/activo-fijo/depreciacion/:id/reversar-contabilizacionReversa una cuota contabilizada.
DELETE/api/activo-fijo/depreciacion/:idElimina una cuota pendiente.

Sevastopol no llama directamente a Orchestrator. La ruta src/pages/api/activo-fijo/[...path].ts reenvía método, query string, cookies, Authorization y cuerpo hacia Orchestrator preservando credenciales.

let targetUrl = `${ORCHESTRATOR_BASE_URL}/api/activo-fijo/${restPath}`;
if (url.search) targetUrl += url.search;
const orchestratorRes = await fetch(targetUrl, {
method: request.method,
headers: getProxyHeaders(request),
body,
credentials: 'include',
});
ReglaMotivo
Usar tenant activoActivos, categorías y cuotas pertenecen a una base tenant.
Usar periodo activo en depreciacionLa consulta de cuotas se acota al mes contable global.
Cargar compras al abrir el altaEl activo puede enlazarse a la línea de compra que lo originó.
Igualar fecha_inicio_dep a fecha_compraEl flujo actual inicia la depreciación en la fecha de adquisición.
Crear la categoría antes que el activoEl activo exige una categoría existente que parametrice sus cuentas.
Generar depreciación por categoríaEl flujo actual procesa activos elegibles de una categoría completa, no por activo individual.
Confirmar reversa explícitamenteLa reversa elimina líneas contables y debe ser consciente.
Mostrar acciones por estadoEvita acciones inválidas desde la UI.
Recargar tras cada acciónMantiene la tabla alineada con el estado real en Orchestrator.