Skip to content

Plataforma técnica · Orchestrator

Compras Inventario Service

Orchestrator Inventarios Compras

ComprasInventarioService es el puente entre operaciones SII y el inventario: toma compras ya sincronizadas (vía OperacionesService) y las convierte en movimientos de inventario. Implementa el flujo operacional que el contador usa desde la “bandeja de compras pendientes” en Sevastopol.

Tiene dos modos de procesamiento (procesarCompraExistencia por producto específico vs procesarCompraIngresoCategoria agregado), distingue facturas de notas de crédito por el signo del neto, y emite compra_inv:contabilizada por cada movimiento generado.

MétodoPara
getComprasExistencias(ctx, filtros?)Bandeja: compras candidatas + resumen período + resumen mensual.
procesarCompraExistencia(ctx, compraId, data)Procesar UNA compra como ingreso de UN producto. Emite hook.
procesarCompraIngresoCategoria(ctx, compraId, data)Procesar UNA compra como ingreso agregado a una categoría. Emite hook.
reversarCompraIngreso(ctx, compraId)Revertir un ingreso previamente procesado.
getComprasDetallePendientes(ctx, filtros?)Lista de líneas de detalle pendientes (multi-existencia).
eliminarCompraDetalle(ctx, id)Borra una línea de detalle pendiente.
eliminarComprasDetalleByProveedor(ctx, rut, año, mes?)Bulk: borra todas las pendientes de un proveedor.
generarExistenciasPorProveedor(ctx, rut, año, mes?)Bulk: contabiliza todas las pendientes de un proveedor.
getComprasContabilizadas(ctx, filtros?)Bandeja de las ya procesadas (vista agregada por proveedor + categoría).
getComprasContabilizadasDetalle(ctx, rut, catId, filtros?)Drill-down a las líneas individuales.

Los métodos procesarCompraExistencia y procesarCompraIngresoCategoria validan siempre lo mismo antes de procesar:

CheckSi falla
findCompraExistenciaProcessInfo(compraId) retorna algoValidationError("Compra no encontrada")
proveedor_activo && proveedor_genera_existenciasValidationError("La compra no pertenece a un proveedor configurado para existencias")
procesado_inventario === falseValidationError("La compra ya fue procesada en inventario")
Number(monto_neto_signed) !== 0ValidationError("El monto neto + otros impuestos del documento es cero y no puede procesarse en inventario")

El proveedor se “configura para existencias” en ProveedoresClasificacionService (sección menores en index).

Modo 1: procesarCompraExistencia — por producto

Section titled “Modo 1: procesarCompraExistencia — por producto”

Cuando el operador conoce exactamente qué producto entra al inventario por esta compra. Valida adicional:

  • data.existencia_id requerido + existencia debe existir (lookup en TX).
  • data.cantidad > 0 estricto.

Detección automática: factura vs nota de crédito

Section titled “Detección automática: factura vs nota de crédito”
flowchart TB
  IN["procesarCompraExistencia(compraId, data)"]
  V["validaciones (4 base + 2 producto)"]
  TX["withTransaction"]
  INFO["findCompraExistenciaProcessInfo"]
  EXIS["ExistenciasRepository.findExistenciaById"]
  META["build fechaDocumento + numeroDocumento + safeUserId"]
  CHK{"monto_neto_signed < 0?"}
  NC["procesarDevolucionCompraExistencia<br/>(monto positivo, glosa 'NC SII ... devolución')"]
  FA["procesarCompraExistencia<br/>(monto tal cual, glosa 'Compra SII ... procesada')"]
  PAY["findCompraInvContabilizadaEventPayload(movimiento_id)"]
  EMIT["outbox.queue(compra_inv:contabilizada, payload)"]
  RET["return result"]

  IN --> V --> TX --> INFO --> EXIS --> META --> CHK
  CHK -- sí --> NC --> PAY
  CHK -- no --> FA --> PAY
  PAY --> EMIT --> RET

Por qué la rama NC: una compra con monto_neto_signed < 0 es una nota de crédito (devolución). El service la procesa como devolución — usa el método de repository dedicado y reescribe la glosa para reflejar el flujo inverso. El monto se pasa positivo (Math.abs) al repository porque el repository encapsula el signo del movimiento.

Formato fijo: ${año}-${mes_zero_padded}-${nro}. Ejemplos:

  • Compra de mayo 2026 número 42 → "2026-05-42".
  • Compra de enero 2026 número 7 → "2026-01-7".

Este número se usa en la glosa del movimiento y como referencia humanamente legible.

Modo 2: procesarCompraIngresoCategoria — por categoría agregada

Section titled “Modo 2: procesarCompraIngresoCategoria — por categoría agregada”

Cuando el operador no quiere desglose por producto y solo necesita afectar una categoría contable. Útil para compras genéricas (materiales de oficina, insumos no inventariados a nivel de SKU).

DTO fieldValidación
categoria_idOpcional; si se pasa: entero positivo.

Mismo flujo que el modo 1 pero sin lookup de existencia — el ingreso queda asociado a la categoría, no a un producto. Glosa: "Compra SII {numero} — ingreso inventario".

Emite el mismo hook con el mismo payload shape.

EventoCuándoPayload
compra_inv:contabilizadaprocesarCompraExistencia y procesarCompraIngresoCategoria, después del COMMIT, por cada movimiento_id que findCompraInvContabilizadaEventPayload resuelva con payload no-null.{ movimientoCostoId, compraDetalleId, categoriaId, monto }

Si el repository no puede construir el payload (datos incompletos), el evento no se emite pero la operación se completa OK.

Inverso de procesar: borra el movimiento y revierte las líneas de detalle. Devuelve { movimiento_eliminado, lineas_revertidas }.

Dos métodos para operar a granel sobre todas las compras de un proveedor en un período:

eliminarComprasDetalleByProveedor(ctx, rutProveedor, año, mes?)

Section titled “eliminarComprasDetalleByProveedor(ctx, rutProveedor, año, mes?)”

Borra TODAS las líneas de detalle pendientes del proveedor en el período. Útil para “limpiar” después de un cambio de criterio de clasificación. No emite hook (no hay movimientos asociados a esas líneas pendientes).

generarExistenciasPorProveedor(ctx, rutProveedor, año, mes?)

Section titled “generarExistenciasPorProveedor(ctx, rutProveedor, año, mes?)”

Contabiliza TODAS las líneas de detalle pendientes del proveedor. Útil para procesar bandeja completa de un proveedor recurrente.

mes es opcional — si se omite, procesa todo el año.

Vista de bandeja — getComprasExistencias

Section titled “Vista de bandeja — getComprasExistencias”

Devuelve { compras, resumen_periodo, resumen_mensual }. El service hace el cálculo de resumen en JS después de la query (no en SQL):

Cálculo de resumen
const resumen_periodo = compras.reduce((acc, c) => {
acc.documentos += 1;
acc.procesadas += c.procesado_inventario ? 1 : 0;
acc.pendientes += c.procesado_inventario ? 0 : 1;
acc.monto_neto += Number(c.monto_neto_signed ?? 0);
acc.monto_otros_impuestos += Number(c.monto_otros_impuestos_signed ?? 0);
acc.monto_iva += Number(c.monto_iva_signed ?? 0);
acc.monto_total += Number(c.monto_total_signed ?? 0);
return acc;
}, { documentos: 0, procesadas: 0, pendientes: 0, monto_neto: 0, monto_otros_impuestos: 0, monto_iva: 0, monto_total: 0 });
// resumen_mensual: agrupa por `${año}-${mes}` y suma lo mismo, ordenado por (año, mes) ascendente.

Razón: el repository ya filtra y trae todas las filas; agregar en SQL requeriría 3 queries (lista + resumen + resumen mensual). Hacerlo en JS es más simple y, para volúmenes típicos (menos de 1000 compras/período), el costo es despreciable.

FiltroTipoNotas
añonumberAño del documento.
mesnumberMes del documento.
rutProveedorstringRUT exacto.
categoriaIdnumberSolo compras ya clasificadas en esa categoría.
estado'pendientes' | 'procesadas'Filtro de procesado_inventario.
MétodoRutaService call
GET/api/inventario/comprasgetComprasExistencias(ctx, filtros)
POST/api/inventario/compras/:id/procesarprocesarCompraExistencia(ctx, id, data)
POST/api/inventario/compras/:id/procesar-categoriaprocesarCompraIngresoCategoria(ctx, id, data)
POST/api/inventario/compras/:id/reversarreversarCompraIngreso(ctx, id)
GET/api/inventario/compras/pendientesgetComprasDetallePendientes(ctx, filtros)
DELETE/api/inventario/compras/pendiente/:ideliminarCompraDetalle(ctx, id)
DELETE/api/inventario/compras/proveedor/:rut/:año/:mes?eliminarComprasDetalleByProveedor(ctx, rut, año, mes)
POST/api/inventario/compras/generar-por-proveedor/:rut/:año/:mes?generarExistenciasPorProveedor(ctx, rut, año, mes)
GET/api/inventario/compras/contabilizadasgetComprasContabilizadas(ctx, filtros)
GET/api/inventario/compras/contabilizadas/:rut/:catIdgetComprasContabilizadasDetalle(ctx, rut, catId, filtros)