Skip to content

Plataforma técnica · Sevastopol

Declaraciones View Island

Islands Sevastopol Declaraciones

DeclaracionesViewIsland es la island que gestiona el Formulario 29 mensual desde Sevastopol: lista los F29 del tenant filtrados por el período activo, abre un detalle con líneas tributarias, expone el workflow BORRADOR → VALIDADO → DECLARADO → ANULADO y soporta dos acciones contables sensibles — baja administrativa de remanente SII y regeneración anual de asientos.

  • Directorysevastopol/src/components/islands/declaraciones/
    • DeclaracionesViewIsland.tsx — componente único, internamente DeclaracionesPanel
    • DeclaracionesWorkspace.tsx — tokens visuales compartidos por las tres islands
  • Directorysevastopol/src/services/declaraciones/
    • DeclaracionesService.ts — cliente HTTP (list, getById, generate, updateStatus, delete, ajuste baja, regenerar asientos)
  • Directorysevastopol/src/pages/api/declaraciones/f29/
    • index.ts — proxy raíz createProxy("/api/declaraciones/f29")
    • […all].ts — proxy catch-all
  • Directorysevastopol/src/lib/hooks/
    • useActiveTenant.ts
    • useActivePeriod.ts

La island es una página con cuatro superficies que comparten el mismo tenant y período:

SuperficieFunciónVisible cuando
Tabla de F29Lista filtrada por período + búsqueda libre.Siempre.
Modal GenerarCrea un borrador para un periodo YYYY-MM.Click en “Nueva declaración”.
Modal DetalleCabecera + tabla de líneas con sintetizados.Click en el icono de ver fila.
Modal Baja remanenteFormulario controlado con validaciones de monto/motivo.Click en “Baja remanente SII” desde el detalle, si aplica.
Modal Editar estadoCambia el estado del workflow.Click en el icono de editar fila.
const { tenantId } = useActiveTenant((id) => void loadData());
const { period } = useActivePeriod();
HookAportaReactividad
useActiveTenantUUID del tenant.El callback re-fetcha la lista al cambiar tenant.
useActivePeriod{ year, month } global.No re-fetcha — el filtro se aplica en cliente sobre la lista ya cargada.

El filtro de período es derivado en cliente porque la lista trae hasta 1000 F29 del tenant en una sola llamada:

const filteredItems = createMemo(() => {
let result = items();
const activeYear = period().year ?? 0;
const activeMonth = period().month ?? 0;
if (activeYear > 0) {
if (activeMonth > 0) {
const p = `${activeYear}-${pad2(activeMonth)}`;
result = result.filter((i) => i.periodo === p);
} else {
const pStart = `${activeYear}-`;
result = result.filter((i) => i.periodo.startsWith(pStart));
}
}
// … búsqueda libre por periodo, RUT o monto
return result;
});

Signals principales:

SignalTipoUso
itemsF29Header[]Lista cruda del tenant (limit=1000, offset=0).
selectedDetailF29Full | nullDetalle abierto del F29 actual.
qstringBúsqueda libre.
isModalOpen / isDetailOpen / isAjusteOpen / isEditOpenbooleanEstado de los 4 modales.
genPeriodstring (YYYY-MM)Período del modal de generación.
editStatusstringSelección del modal de cambio de estado.
ajusteActual / ajusteMonto / ajusteMotivo / ajusteReferenciaSii / ajusteMongoNoteIdvariasFormulario controlado del modal de baja remanente.
isGenerating / isLoadingAjuste / isSavingAjuste / isDeletingAjuste / isRegeneratingbooleanBloqueos de UI por acción en curso.

El selector del modal de edición lista los cuatro estados del workflow. Cada estado habilita acciones distintas:

EstadoEditableBaja remanenteCuenta para cierre
BORRADORSí (si hay remanente positivo)No
VALIDADONo
DECLARADONoNoSí — habilita la regeneración de asientos
ANULADONoNoNo
const statusOptions = [
{ id: "BORRADOR", nombre: "Borrador" },
{ id: "VALIDADO", nombre: "Validada" },
{ id: "DECLARADO", nombre: "Declarada" },
{ id: "ANULADO", nombre: "Anulada" },
];

El badge de la tabla usa StatusBadge con type="payroll" — mismo lenguaje visual que liquidaciones, no específico de F29.

El modal de detalle pinta las líneas del F29 con dos transformaciones en cliente:

Si el F29 no trae una línea 62 y sí trae una línea 88 (PPM) con monto distinto de cero, la UI agrega una línea 62 derivada para visibilidad contable:

const ppmPorPagar: F29Detail = {
...ppmLine,
codigo_linea: "62",
descripcion_linea: "PPM - Por pagar",
};
return [...details, ppmPorPagar].sort(/* por código numérico */);

Esta línea es vista contable, no se envía al SII como dato adicional.

Cuando existe un ajuste de baja, el detalle incluye una línea con codigo_linea = "89B". La UI la reconoce con isBajaRemanenteLine(row) y pinta el badge BAJA SII al lado de la descripción.

function isBajaRemanenteLine(row: F29Detail): boolean {
return String(row.codigo_linea).toUpperCase() === "89B";
}

El botón Baja remanente SII aparece dentro del modal de detalle bajo dos condiciones combinadas (canManageAjusteBaja):

const canManageAjusteBaja = createMemo(() => {
const f29 = selectedDetail();
if (!f29) return false;
if (Number(f29.remanente_anterior) <= 0 && !hasAjusteBaja()) return false;
return ["BORRADOR", "VALIDADO"].includes(f29.estado);
});

El label del botón cambia: Editar baja remanente si ya existe, Baja remanente SII si es nueva. Cuando ya existe, aparece además un botón secundario Eliminar baja que llama a eliminarAjusteBajaRemanente(id, tenant) y restituye el remanente.

Cinco reglas duras antes de enviar:

if (monto <= 0) → "Ingrese un monto mayor que cero"
if (monto > ajusteEditableRemanente()) → "El monto excede el remanente disponible"
if (motivo.length < 5) → "El motivo debe tener al menos 5 caracteres"
if (motivo.length > 1000) → "El motivo no puede superar 1000 caracteres"
if (referenciaSii.length > 100) → "La referencia SII no puede superar 100 caracteres"
if (mongoNoteId.length > 50) → "La nota Mongo no puede superar 50 caracteres"

ajusteEditableRemanente() es el remanente disponible incluyendo el ajuste actual si se está editando — permite reasignar sin perder el saldo del ajuste previo:

function ajusteEditableRemanente(): number {
const f29 = selectedDetail();
if (!f29) return 0;
return Number(f29.remanente_anterior) + Number(ajusteActual()?.monto ?? 0);
}

El modal muestra en vivo el remanente después del ajuste computado con Math.max(0, ajusteEditableRemanente() - ajusteMontoNumber()), y un recordatorio del asiento contable que generará el backend:

Asiento contable: D 3701040 IVA no Recuperable / H 1108002 IVA Crédito Fiscal

Tras guardar, setSelectedDetail(updated) reemplaza el detalle en memoria con la respuesta del backend (que ya incluye la línea 89B sintetizada) y loadData() refresca la tabla principal.

Botón secundario en la cabecera (Regenerar asientos {año}). Sólo se monta si el período activo tiene año. Pide confirmación explícita:

if (!confirm(
`Regenerar asientos F29 del año ${anio}?\nSe eliminarán y reinsertarán a partir de los F29 en estado DECLARADO.`,
)) return;

La acción llama a DeclaracionesService.regenerarAsientosAnual(anio, tenant) y reporta ${res.anio} regenerados (${res.filas} filas). Es destructiva sobre el lado contabilidad — sólo debe usarse cuando cambia el mapeo de cuentas o tras correcciones masivas.

flowchart TB
  MOUNT["onMount<br/>tenantId ready"]
  CHGT["useActiveTenant onChange"]
  LOAD["loadData()<br/>DeclaracionesService.list({limit:1000})"]
  ST["setItems(F29Header[])"]
  FILT["filteredItems<br/>(period + q)"]
  TABLE["DataTable F29"]

  OPEN["openDetail(row)"]
  GETBYID["DeclaracionesService.getById(id, tenant)"]
  SD["setSelectedDetail(F29Full)"]
  DTABLE["detailTableRows<br/>+ Línea 62 derivada<br/>+ marcador 89B"]

  AJ["openAjusteModal"]
  AJGET["getAjusteBajaRemanente"]
  AJSAVE["registrarAjusteBajaRemanente"]
  AJDEL["eliminarAjusteBajaRemanente"]

  GEN["openNewModal → generate"]
  EDIT["openEdit → updateStatus"]
  DEL["handleDelete → delete"]
  REGEN["handleRegenerarAsientos<br/>regenerarAsientosAnual"]

  MOUNT --> LOAD
  CHGT --> LOAD
  LOAD --> ST --> FILT --> TABLE

  TABLE --> OPEN --> GETBYID --> SD --> DTABLE
  DTABLE --> AJ --> AJGET
  AJ --> AJSAVE --> SD
  AJ --> AJDEL --> SD

  GEN --> LOAD
  EDIT --> LOAD
  DEL --> LOAD
  REGEN -.no recarga.-> TABLE

Todos van por DeclaracionesService (cliente HTTP en sevastopol/src/services/declaraciones/), que a su vez delega en /api/declaraciones/f29 (proxy).

MétodoRuta lógicaOperación
GET/api/declaraciones/f29?limit=1000&offset=0Lista de cabeceras del tenant.
GET/api/declaraciones/f29/:idF29 completo con detalle de líneas.
POST/api/declaraciones/f29/generateGenera borrador desde la base operacional.
PUT/api/declaraciones/f29/:id/statusCambia el estado del workflow.
DELETE/api/declaraciones/f29/:idElimina (sólo no-DECLARADO).
GET/api/declaraciones/f29/:id/ajuste-baja-remanenteLee el ajuste actual si existe.
POST/api/declaraciones/f29/:id/ajuste-baja-remanenteCrea o reemplaza el ajuste.
DELETE/api/declaraciones/f29/:id/ajuste-baja-remanenteElimina el ajuste y restituye remanente.
POST/api/declaraciones/f29/regenerar-asientos/:anioReconstruye los asientos del año.

tenant_id viaja como query param en cada llamada.

sevastopol/src/pages/api/declaraciones/f29/index.ts
import { createProxy } from "@/lib/proxyUtils";
export const { GET, POST, PUT, DELETE, PATCH } = createProxy(
"/api/declaraciones/f29",
);

A diferencia del proxy de ciclo-contable (read-only), aquí se exportan todos los métodos porque el dominio incluye creación, actualización y eliminación. El detalle del BFF está en BFF Proxy a Orchestrator.

ReglaMotivo
Lista completa en una llamada (limit=1000)Evita paginación; el F29 es bajo volumen por tenant/año. El filtro de período se aplica en cliente.
Confirmar antes de regenerar asientosLa operación es destructiva sobre declaraciones.asientos_f29.
Confirmar antes de eliminarSe prefiere ANULADO sobre DELETE para mantener trazabilidad.
Bloquear baja en DECLARADO/ANULADOEl ajuste contable de baja sólo procede sobre borrador o validado.
monto ≤ remanente disponibleUna baja no puede exceder el remanente acumulado más el monto del ajuste actual (si se está editando).
motivo ≥ 5 caracteresTrazabilidad mínima para el auditor — no se admite “ok” como justificación.
Reemplazar selectedDetail con la respuesta del saveEl backend devuelve el F29 ya con la línea 89B sintetizada, evita un fetch extra.
Línea 62 derivada en cliente, no SIIEl sintetizado es vista contable; el F29 enviado al SII conserva sólo la línea 88.
Hardcoded rut = "6000431-5" como fallbackTODO conocido — el RUT debería resolverse desde el tenant; ver línea 137 del archivo fuente.