Skip to content

Plataforma técnica · Orchestrator

Gastos

Orchestrator Gastos Categorias Gasto

El dominio Gastos registra hechos económicos de gasto que no pasan por el flujo estándar de compras → inventario. Cubre tres escenarios reales:

  1. Gastos manuales — el contador registra un gasto sin documento SII (provisión, ajuste, gasto sin factura).
  2. Gastos vinculados a compra SII — una compra existente en operaciones_sii.compras_ventas_detalle se clasifica como gasto en lugar de ingreso a inventario.
  3. Importación masiva por rango — impuestos específicos (códigos 20-27 del SII) o IVA no recuperable se importan en bulk desde compras SII pendientes, según gastos.fn_importar_impuestos y gastos.fn_importar_iva_no_recuperable.

A diferencia del dominio inventario, gastos no genera stock ni costo de ventas. La contabilización emite un asiento simple (cargo a gasto + crédito a proveedor / IVA crédito si aplica) y opcionalmente emite compra:contabilizada cuando hay compra_detalle_id vinculado.

orchestrator/src/domain/gastos/
├── GastosService.ts # 14 métodos públicos
├── GastosRepository.ts # queries + delegaciones a fn_* SQL
├── types.ts # CategoriaGasto, Gasto, DTOs, resultados
└── __tests__/
EntidadTablaRol
CategoriaGastogastos.categorias_gastoPlantilla de cuenta de gasto + IVA crédito + concepto de impuesto vinculado. Reusable entre gastos del mismo tipo (e.g., “Arriendo oficina”, “Servicios básicos”).
Gastogastos.gastosRegistro de gasto puntual con categoría, proveedor, monto, fecha. Opcionalmente vinculado a una compra SII (compra_detalle_id).
GastoDetallegastos.gastos_detalleLíneas contables generadas al contabilizar (cargo/crédito por línea).
ConceptoOperacionImpuestooperaciones_sii.conceptos_operaciones (vista)Filtra conceptos válidos para gastos: tipo_concepto = 'OTRO' o id=30 (IVA no recuperable) o codigo=‘GASTO-IVA’.
flowchart TB
  IN["createGasto(dto)"]
  COMP{"vincular_sii && !compra_detalle_id?"}
  MAS["createGastoMasivoDesdeSii<br/>(busca compras del proveedor en el período)"]
  CDI{"compra_detalle_id?"}
  CHK["Verifica que no esté ya<br/>asociada a otro gasto"]
  CMP["findCompraDisponibleByDetalleId<br/>+ hydratePayloadFromCompra"]
  HYD["hydratePeriodo (deriva año/mes<br/>desde fecha_documento)"]
  VAL["validateMontos (neto > 0, iva >= 0)"]
  INS["createGasto en gastos.gastos"]
  OUT["Gasto"]
  OUTM["CreateGastoMasivoResult"]

  IN --> COMP
  COMP -- sí --> MAS --> OUTM
  COMP -- no --> CDI
  CDI -- sí --> CHK --> CMP --> HYD
  CDI -- no --> HYD
  HYD --> VAL --> INS --> OUT
createGasto({
categoria_id: 5,
rut_proveedor: "76123456-7",
fecha_documento: "2026-03-15",
monto_neto: 250000,
monto_iva: 47500,
glosa: "Servicio mantención impresora",
});

Sin compra_detalle_id ni vincular_sii. El service hidrata año/mes desde fecha_documento y valida montos.

createGasto({
categoria_id: 5,
rut_proveedor: "76123456-7",
compra_detalle_id: "550e8400-e29b-41d4-a716-446655440000",
});

El service:

  • Verifica que la compra no esté ya asociada a otro gasto (idempotencia).
  • Llama findCompraDisponibleByDetalleId que reagrega NETO+IVA+OTRO por documento_id y valida estado = 'PENDIENTE'.
  • hydratePayloadFromCompra reemplaza rut_proveedor/tipo_dte/folio/fecha_documento/monto_neto/monto_iva/glosa con los datos del SII.

3. Masivo desde SII por proveedor (vincular_sii = true)

Section titled “3. Masivo desde SII por proveedor (vincular_sii = true)”
createGasto({
categoria_id: 5,
rut_proveedor: "76123456-7",
año: 2026,
mes: 3,
vincular_sii: true,
});

createGastoMasivoDesdeSii busca todas las compras pendientes del proveedor en el período (mesExacto: true) y crea un gasto por cada una. Falla con ValidationError si no hay compras disponibles.

Retorna CreateGastoMasivoResult { vinculacion: "SII_MASIVO", creados, gastos[] }.

4. Importación por rango (sin proveedor específico)

Section titled “4. Importación por rango (sin proveedor específico)”

Dos endpoints separados que cubren el caso “barré por fecha y traigo todo lo que aplique”:

MétodoFunción SQLPropósito
importarImpuestosPorRangogastos.fn_importar_impuestos(fecha_desde, fecha_hasta, codigo_desde, codigo_hasta, userId)Importa impuestos específicos (códigos SII 20-27 por default: tabaco, bebidas, gasolina) desde compras pendientes en el rango.
importarIvaNoRecuperablePorRangogastos.fn_importar_iva_no_recuperable(fecha_desde, fecha_hasta, concepto_id, userId)Importa IVA no recuperable (concepto_id=30 por default) desde compras pendientes en el rango.

Ambos retornan { encontrados, creados, duplicados, sin_categoria, categorias_sin_mapear[] }. La lógica de matching concepto → categoría_gasto vive en SQL (las funciones SQL hacen el JOIN). categorias_sin_mapear indica qué conceptos no encontraron una categoría definida — accionable para parametrizar.

stateDiagram-v2
  [*] --> PENDIENTE : createGasto
  PENDIENTE --> CONTABILIZADO : contabilizarGasto (vía fn_contabilizar_gasto)
  CONTABILIZADO --> PENDIENTE : reversarGasto
  PENDIENTE --> [*] : deleteGasto
  • deleteGasto solo funciona en PENDIENTE (DELETE con WHERE estado = 'PENDIENTE'; retorna { deleted: false } en otros estados sin error).
  • reversarGasto solo funciona en CONTABILIZADO (UPDATE con guard); falla con ValidationError si no aplica.
  • contabilizarGasto delega a la función SQL gastos.fn_contabilizar_gasto(id, userId, año?, mes?) que genera líneas en gastos.gastos_detalle y marca el estado. Period override permite re-asignar el período contable si difiere del original.
EventoTriggerPayloadCondición
compra:contabilizadacontabilizarGasto y contabilizarPendientesGrupo post-COMMIT{ compraId, monto, formaPago }Solo si el gasto tiene compra_detalle_id (no se emite para gastos manuales sin SII)

findCompraContabilizadaEventPayload reagrega desde compras_ventas_detalle el documento_id, monto_total del gasto y forma_pago normalizada (lower-case, default 'otro'). El evento alimenta a cicloContable y financieros para conciliación bancaria.

ConvenciónDetalle
Cuenta proveedor default2102001 (acreedores varios). Sobreescribible vía categoría.
Conceptos válidos para gastostipo_concepto = 'OTRO' OR id = 30 (IVA no recuperable) OR codigo = 'GASTO-IVA'. Excluye conceptos NETO e IVA estándar — esos van por flujo de inventario.
Default impuestos específicosCódigos 20-27 del SII (tabaco, gasolina, bebidas, etc.)
RUT normalizaciónregexp_replace(lower(rut), '[^0-9k]', '', 'g') aplicado en JOINs con proveedores_clasificacion para tolerar formatos con/sin guion/puntos.
mesExacto en búsqueda masivafindComprasDisponibles admite mesExacto: true (match exacto mes = $) o mesExacto: false (acumulado mes <= $). El flujo masivo usa exacto; el listado UI usa acumulado.

categoriasHasActivoColumn verifica con information_schema.columns si la tabla gastos.categorias_gasto tiene columna activo. Tenants viejos que aún no migraron tratan todas las categorías como activas (true::boolean AS activo). Es el mismo patrón de resilencia que usa cicloContable con 42P01 — el código tolera schemas en distintas versiones sin romperse.

FunciónRol
gastos.fn_contabilizar_gasto(id, userId, año?, mes?)Genera detalle contable y marca CONTABILIZADO. Acepta period override.
gastos.fn_importar_impuestos(desde, hasta, cod_desde, cod_hasta, userId)Bulk-insert de impuestos específicos.
gastos.fn_importar_iva_no_recuperable(desde, hasta, concepto_id, userId)Bulk-insert de IVA no recuperable.

Estas funciones encapsulan la lógica de imputación que históricamente vivía en SPs. El service las consume sin replicar la lógica en TypeScript — coherente con la decisión opuesta tomada en F29GeneratorService (rewrite de SP a TS). La diferencia: fn_contabilizar_gasto y las dos fn_importar_* son operaciones bulk con muchas inserciones por llamada, donde el cost de roundtrip TS↔SQL excede el beneficio de testabilidad. F29GeneratorService calcula valores agregados de varias fuentes — testear esa lógica en TS aportó más.

GET /api/gastos/categorias
POST /api/gastos/categorias
PATCH /api/gastos/categorias/:id
GET /api/gastos/conceptos-impuestos
GET /api/gastos # con filtros: año, mes, estado, categoria_id
POST /api/gastos # manual, vinculado o masivo según DTO
DELETE /api/gastos/:id # solo PENDIENTE
POST /api/gastos/:id/contabilizar # opcional periodo override
POST /api/gastos/:id/reversar
POST /api/gastos/contabilizar-pendientes # grupo por año/mesHasta/categoria
POST /api/gastos/importar-impuestos # rango + códigos SII
POST /api/gastos/importar-iva-no-recuperable
GET /api/gastos/resumen-anual
GET /api/gastos/compras-disponibles # del SII, pendientes de gasto
GET /api/gastos/proveedores # con compras_pendientes para gasto