Scan to open on your phone
Point a camera at the QR code - it opens this exact paste, no app needed.
https://codepastes.com/zzgs8wq5e6mqek04untitledPlain Text
package com.negocio.desinfectantes.data
import androidx.room.Dao
import androidx.room.Insert
import androidx.room.OnConflictStrategy
import androidx.room.Query
import kotlinx.coroutines.flow.Flow
import androidx.room.Transaction
import java.util.UUID
@Dao
interface ProductoDao {
// PRODUCTOS
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun guardarProducto(producto: ProductoEntity)
@Query("""
UPDATE productos
SET nombre = :nombre,
tipo = :tipo
WHERE id = :productoId
""")
suspend fun actualizarProducto(
productoId: String,
nombre: String,
tipo: String
)
@Query("SELECT * FROM productos WHERE activo = 1 ORDER BY nombre ASC")
fun obtenerProductos(): Flow<List<ProductoEntity>>
@Query("SELECT * FROM productos ORDER BY nombre ASC")
fun obtenerTodosProductos(): Flow<List<ProductoEntity>>
@Query("SELECT * FROM productos WHERE activo = 0 ORDER BY nombre ASC")
fun obtenerProductosInactivos(): Flow<List<ProductoEntity>>
@Query("SELECT * FROM productos WHERE id = :id LIMIT 1")
suspend fun buscarProducto(id: String): ProductoEntity?
@Query("UPDATE productos SET activo = 0 WHERE id = :productoId")
suspend fun desactivarProducto(productoId: String)
@Query("UPDATE productos SET activo = 1 WHERE id = :productoId")
suspend fun reactivarProducto(productoId: String)
// AROMAS
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun guardarAroma(aroma: AromaEntity)
@Query("SELECT * FROM aromas WHERE productoId = :productoId AND activo = 1 ORDER BY nombre ASC")
fun obtenerAromas(productoId: String): Flow<List<AromaEntity>>
@Query("SELECT * FROM aromas ORDER BY nombre ASC")
fun obtenerTodosAromas(): Flow<List<AromaEntity>>
@Query("SELECT * FROM aromas WHERE productoId = :productoId AND activo = 0 ORDER BY nombre ASC")
fun obtenerAromasInactivos(productoId: String): Flow<List<AromaEntity>>
@Query("UPDATE aromas SET activo = 1 WHERE id = :aromaId")
suspend fun reactivarAroma(aromaId: String)
@Query("SELECT * FROM aromas WHERE id = :aromaId LIMIT 1")
suspend fun buscarAroma(aromaId: String): AromaEntity?
@Query("SELECT * FROM aromas WHERE activo = 0 ORDER BY nombre ASC")
fun obtenerTodosAromasInactivos(): Flow<List<AromaEntity>>
@Query("SELECT COUNT(*) FROM presentaciones WHERE aromaId = :aromaId")
suspend fun contarPresentacionesDeAroma(aromaId: String): Int
@Query("UPDATE aromas SET activo = 0 WHERE id = :aromaId")
suspend fun eliminarAroma(aromaId: String)
@Query("""
UPDATE aromas
SET nombre = :nuevoNombre
WHERE id = :aromaId
""")
suspend fun actualizarAroma(
aromaId: String,
nuevoNombre: String
)
// PRESENTACIONES
@Query("SELECT * FROM presentaciones WHERE activo = 1 ORDER BY nombre ASC")
fun obtenerTodasPresentacionesActivas(): Flow<List<PresentacionEntity>>
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun guardarPresentacion(presentacion: PresentacionEntity)
@Query("SELECT * FROM presentaciones WHERE aromaId = :aromaId AND activo = 1")
fun obtenerPresentaciones(aromaId: String): Flow<List<PresentacionEntity>>
@Query("SELECT * FROM presentaciones WHERE aromaId = :aromaId AND activo = 0")
fun obtenerPresentacionesInactivas(aromaId: String): Flow<List<PresentacionEntity>>
@Query("UPDATE presentaciones SET activo = 1 WHERE id = :presentacionId")
suspend fun reactivarPresentacion(presentacionId: String)
@Query("SELECT * FROM presentaciones WHERE activo = 0 ORDER BY nombre ASC")
fun obtenerTodasPresentacionesInactivas(): Flow<List<PresentacionEntity>>
@Query("UPDATE presentaciones SET activo = 0 WHERE id = :presentacionId")
suspend fun eliminarPresentacion(presentacionId: String)
// INVENTARIO
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun guardarInventario(inventario: InventarioEntity)
@Query("SELECT COUNT(*) FROM aromas WHERE productoId = :productoId AND activo = 1")
suspend fun contarAromasDeProducto(productoId: String): Int
@Query("""
SELECT COALESCE(SUM(cantidadDisponible + cantidadReservada), 0)
FROM inventario
WHERE presentacionId = :presentacionId
""")
suspend fun contarExistenciasPresentacion(presentacionId: String): Int
@Query("SELECT * FROM inventario WHERE presentacionId = :presentacionId")
fun obtenerInventario(presentacionId: String): Flow<List<InventarioEntity>>
@Query("SELECT * FROM inventario ORDER BY sede ASC")
fun obtenerTodoInventario(): Flow<List<InventarioEntity>>
@Query("""
SELECT * FROM inventario
WHERE presentacionId = :presentacionId AND sede = :sede
LIMIT 1
""")
suspend fun buscarInventario(
presentacionId: String,
sede: String
): InventarioEntity?
@Query("""
UPDATE inventario
SET cantidadDisponible = cantidadDisponible + :cantidad,
fechaActualizacion = :fecha
WHERE id = :inventarioId
""")
suspend fun aumentarInventarioExistente(
inventarioId: String,
cantidad: Int,
fecha: Long
): Int
// PRODUCCIONES
@Insert
suspend fun guardarProduccion(produccion: ProduccionEntity)
@Query("SELECT * FROM producciones ORDER BY fechaCreacion DESC")
fun obtenerProducciones(): Flow<List<ProduccionEntity>>
@Query("SELECT * FROM producciones WHERE id = :produccionId LIMIT 1")
suspend fun buscarProduccion(produccionId: String): ProduccionEntity?
// DISTRIBUCIONES DE PRODUCCION
@Insert
suspend fun guardarDistribucion(
distribucion: DistribucionProduccionEntity
)
@Query("""
SELECT * FROM distribuciones_produccion
WHERE produccionId = :produccionId
ORDER BY sede ASC
""")
fun obtenerDistribuciones(
produccionId: String
): Flow<List<DistribucionProduccionEntity>>
// MOVIMIENTOS DE INVENTARIO
@Insert
suspend fun guardarMovimiento(
movimiento: MovimientoInventarioEntity
)
@Query("""
SELECT * FROM movimientos_inventario
ORDER BY fechaCreacion DESC
""")
fun obtenerTodosMovimientos(): Flow<List<MovimientoInventarioEntity>>
@Query("""
SELECT * FROM movimientos_inventario
WHERE presentacionId = :presentacionId
ORDER BY fechaCreacion DESC
""")
fun obtenerMovimientosPresentacion(
presentacionId: String
): Flow<List<MovimientoInventarioEntity>>
@Query("""
SELECT * FROM movimientos_inventario
WHERE sede = :sede
ORDER BY fechaCreacion DESC
""")
fun obtenerMovimientosSede(
sede: String
): Flow<List<MovimientoInventarioEntity>>
// GUARDADO COMPLETO DE PRODUCCION
@Insert
suspend fun guardarDistribuciones(
distribuciones: List<DistribucionProduccionEntity>
)
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun guardarInventarios(
inventarios: List<InventarioEntity>
)
@Insert
suspend fun guardarMovimientos(
movimientos: List<MovimientoInventarioEntity>
)
@Transaction
suspend fun registrarProduccionCompleta(
produccion: ProduccionEntity,
distribuciones: List<DistribucionProduccionEntity>,
inventarios: List<InventarioEntity>,
movimientos: List<MovimientoInventarioEntity>
) {
guardarProduccion(produccion)
guardarDistribuciones(distribuciones)
guardarInventarios(inventarios)
guardarMovimientos(movimientos)
}
// OPERACIONES DE LOTES DE PRODUCCION
@Query("SELECT ultimoNumero FROM control_numeracion_lotes WHERE id = 1")
suspend fun obtenerUltimoNumeroLote(): Int?
@Query("UPDATE control_numeracion_lotes SET ultimoNumero = :numero WHERE id = 1")
suspend fun actualizarUltimoNumeroLote(numero: Int): Int
@Insert
suspend fun guardarLote(lote: LoteProduccionEntity)
@Insert
suspend fun guardarDetallesLote(
detalles: List<LoteDetalleEntity>
)
@Insert
suspend fun guardarGastosLote(
gastos: List<LoteGastoEntity>
)
@Query("""
UPDATE lotes_gastos
SET concepto = :concepto,
monto = :monto,
observacion = :observacion
WHERE id = :gastoId
AND loteId = :loteId
""")
suspend fun actualizarGastoLote(
gastoId: String,
loteId: String,
concepto: String,
monto: Double,
observacion: String?
): Int
@Query("""
DELETE FROM lotes_gastos
WHERE id = :gastoId
AND loteId = :loteId
""")
suspend fun eliminarGastoLote(
gastoId: String,
loteId: String
): Int
@Insert
suspend fun guardarInventariosLote(
inventarios: List<LoteInventarioEntity>
)
// REGISTRAR UN LOTE COMPLETO
@Transaction
suspend fun registrarLoteCompleto(
lote: LoteProduccionEntity,
detalles: List<LoteDetalleEntity>,
gastos: List<LoteGastoEntity>,
inventariosLote: List<LoteInventarioEntity>
) {
check(detalles.isNotEmpty()) {
"El lote debe tener al menos una presentación."
}
check(inventariosLote.isNotEmpty()) {
"Debes distribuir las unidades entre las ciudades."
}
check(detalles.all {
it.loteId == lote.id && it.cantidadFabricada > 0
}) {
"Hay cantidades de producción incorrectas."
}
check(detalles.map { it.presentacionId }.distinct().size == detalles.size) {
"No puedes repetir una presentación dentro del mismo lote."
}
check(gastos.all {
it.loteId == lote.id && it.monto >= 0
}) {
"Hay gastos incorrectos o asociados a otro lote."
}
check(inventariosLote.all {
it.loteId == lote.id &&
it.cantidadDisponible > 0 &&
it.cantidadReservada == 0 &&
it.sede.isNotBlank()
}) {
"Hay una distribución incorrecta."
}
val clavesDistribucion = inventariosLote.map {
"${it.presentacionId}|${it.sede}"
}
check(clavesDistribucion.distinct().size == clavesDistribucion.size) {
"Hay distribuciones duplicadas para una misma ciudad y presentación."
}
val presentacionesFabricadas =
detalles.map { it.presentacionId }.toSet()
val presentacionesDistribuidas =
inventariosLote.map { it.presentacionId }.toSet()
check(presentacionesFabricadas == presentacionesDistribuidas) {
"Las presentaciones fabricadas no coinciden con las distribuidas."
}
for (detalle in detalles) {
val cantidadDistribuida = inventariosLote
.filter { it.presentacionId == detalle.presentacionId }
.sumOf { it.cantidadDisponible }
check(cantidadDistribuida == detalle.cantidadFabricada) {
"La distribución no coincide con la cantidad fabricada de una presentación."
}
}
val fecha = lote.fechaCreacion
// Generar el siguiente número de lote
val ultimoNumero = obtenerUltimoNumeroLote() ?: 0
val siguienteNumero = ultimoNumero + 1
val contadorActualizado = actualizarUltimoNumeroLote(siguienteNumero)
check(contadorActualizado == 1) {
"No se pudo generar el número del lote."
}
val loteConNumero = lote.copy(
numeroLote = "LOTE-${siguienteNumero.toString().padStart(4, '0')}"
)
// Guardar información del lote
guardarLote(loteConNumero)
guardarDetallesLote(detalles)
if (gastos.isNotEmpty()) {
guardarGastosLote(gastos)
}
// Guardar existencias identificadas por lote
guardarInventariosLote(inventariosLote)
val movimientos = mutableListOf<MovimientoInventarioEntity>()
// Actualizar también el inventario general
for (entrada in inventariosLote) {
val inventarioActual = buscarInventario(
presentacionId = entrada.presentacionId,
sede = entrada.sede
)
if (inventarioActual == null) {
guardarInventario(
InventarioEntity(
id = UUID.randomUUID().toString(),
presentacionId = entrada.presentacionId,
sede = entrada.sede,
cantidadDisponible = entrada.cantidadDisponible,
cantidadReservada = 0,
fechaActualizacion = fecha
)
)
} else {
val actualizado = aumentarInventarioExistente(
inventarioId = inventarioActual.id,
cantidad = entrada.cantidadDisponible,
fecha = fecha
)
check(actualizado == 1) {
"No se pudo actualizar el inventario general."
}
}
movimientos.add(
MovimientoInventarioEntity(
id = UUID.randomUUID().toString(),
presentacionId = entrada.presentacionId,
sede = entrada.sede,
tipoMovimiento = "PRODUCCION",
cantidad = entrada.cantidadDisponible,
referenciaId = lote.id,
observacion = "Entrada por lote de producción"
)
)
}
guardarMovimientos(movimientos)
}
@Insert
suspend fun guardarAsignacionesLoteVenta(
asignaciones: List<LoteVentaEntity>
)
@Query("""
SELECT * FROM lotes_produccion
ORDER BY fechaCreacion DESC
""")
fun obtenerLotes(): Flow<List<LoteProduccionEntity>>
@Query("""
SELECT COUNT(*)
FROM lotes_ventas AS lv
INNER JOIN lotes_inventario AS li
ON li.id = lv.loteInventarioId
WHERE li.loteId = :loteId
""")
suspend fun contarAsignacionesVentaDeLote(loteId: String): Int
@Query("""
SELECT COUNT(*)
FROM movimientos_inventario
WHERE referenciaId = :loteId
AND tipoMovimiento != 'PRODUCCION'
""")
suspend fun contarMovimientosPosterioresDeLote(loteId: String): Int
@Transaction
suspend fun puedeEditarProduccionLote(loteId: String): Boolean {
val asignaciones = contarAsignacionesVentaDeLote(loteId)
val movimientos = contarMovimientosPosterioresDeLote(loteId)
return asignaciones == 0 && movimientos == 0
}
@Query("""
SELECT * FROM lotes_detalle
ORDER BY rowid ASC
""")
fun obtenerTodosDetallesLote(): Flow<List<LoteDetalleEntity>>
@Query("""
SELECT * FROM lotes_gastos
ORDER BY fechaCreacion ASC
""")
fun obtenerTodosGastosLote(): Flow<List<LoteGastoEntity>>
@Query("""
SELECT * FROM lotes_inventario
ORDER BY rowid ASC
""")
fun obtenerTodosInventariosLote(): Flow<List<LoteInventarioEntity>>
@Query("""
SELECT
li.loteId AS loteId,
v.sede AS sede,
COALESCE(SUM(lv.cantidad * dv.precioUnitario), 0) AS ingresos
FROM lotes_ventas AS lv
INNER JOIN lotes_inventario AS li
ON li.id = lv.loteInventarioId
INNER JOIN detalles_venta AS dv
ON dv.id = lv.detalleVentaId
INNER JOIN ventas AS v
ON v.id = lv.ventaId
WHERE lv.estado = 'VENDIDA'
AND v.estadoEntrega = 'ENTREGADA'
GROUP BY li.loteId, v.sede
ORDER BY li.loteId, v.sede
""")
fun obtenerIngresosPorLoteYSede(): Flow<List<IngresoLoteSedeResultado>>
@Query("""
SELECT * FROM lotes_detalle
WHERE loteId = :loteId
""")
suspend fun obtenerDetallesLote(
loteId: String
): List<LoteDetalleEntity>
@Query("""
SELECT * FROM lotes_gastos
WHERE loteId = :loteId
ORDER BY fechaCreacion ASC
""")
fun obtenerGastosLote(
loteId: String
): Flow<List<LoteGastoEntity>>
@Query("""
SELECT * FROM lotes_inventario
WHERE presentacionId = :presentacionId
AND sede = :sede
AND cantidadDisponible > 0
ORDER BY
(
SELECT fechaCreacion
FROM lotes_produccion
WHERE id = loteId
) ASC,
(
SELECT CAST(SUBSTR(numeroLote, 6) AS INTEGER)
FROM lotes_produccion
WHERE id = loteId
) ASC
""")
suspend fun obtenerLotesDisponiblesFIFO(
presentacionId: String,
sede: String
): List<LoteInventarioEntity>
@Query("""
UPDATE lotes_inventario
SET cantidadDisponible = cantidadDisponible - :cantidad
WHERE id = :loteInventarioId
AND cantidadDisponible >= :cantidad
AND :cantidad > 0
""")
suspend fun descontarCantidadLote(
loteInventarioId: String,
cantidad: Int
): Int
@Query("""
UPDATE lotes_inventario
SET cantidadDisponible = cantidadDisponible - :cantidad,
cantidadReservada = cantidadReservada + :cantidad
WHERE id = :loteInventarioId
AND cantidadDisponible >= :cantidad
AND :cantidad > 0
""")
suspend fun reservarCantidadLote(
loteInventarioId: String,
cantidad: Int
): Int
@Query("""
UPDATE lotes_inventario
SET cantidadReservada = cantidadReservada - :cantidad
WHERE id = :loteInventarioId
AND cantidadReservada >= :cantidad
AND :cantidad > 0
""")
suspend fun consumirReservaLote(
loteInventarioId: String,
cantidad: Int
): Int
@Query("""
UPDATE lotes_inventario
SET cantidadDisponible = cantidadDisponible + :cantidad,
cantidadReservada = cantidadReservada - :cantidad
WHERE id = :loteInventarioId
AND cantidadReservada >= :cantidad
AND :cantidad > 0
""")
suspend fun liberarReservaLote(
loteInventarioId: String,
cantidad: Int
): Int
@Query("""
SELECT * FROM lotes_ventas
WHERE ventaId = :ventaId
AND estado = 'RESERVADA'
""")
suspend fun obtenerAsignacionesReservadas(
ventaId: String
): List<LoteVentaEntity>
@Query("""
UPDATE lotes_ventas
SET estado = :nuevoEstado
WHERE ventaId = :ventaId
AND estado = :estadoActual
""")
suspend fun actualizarEstadoAsignacionesLote(
ventaId: String,
estadoActual: String,
nuevoEstado: String
): Int
// CLIENTES
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun guardarCliente(cliente: ClienteEntity)
@Query("SELECT * FROM clientes WHERE activo = 1 ORDER BY nombre ASC")
fun obtenerClientes(): Flow<List<ClienteEntity>>
@Query("SELECT * FROM clientes ORDER BY nombre ASC")
fun obtenerTodosClientes(): Flow<List<ClienteEntity>>
@Query("SELECT * FROM clientes WHERE id = :clienteId LIMIT 1")
suspend fun buscarCliente(clienteId: String): ClienteEntity?
@Query("UPDATE clientes SET activo = 0 WHERE id = :clienteId")
suspend fun desactivarCliente(clienteId: String)
@Query("UPDATE clientes SET activo = 1 WHERE id = :clienteId")
suspend fun reactivarCliente(clienteId: String)
// VENTAS
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun guardarVenta(venta: VentaEntity)
@Query("SELECT * FROM ventas ORDER BY fechaCreacion DESC")
fun obtenerVentas(): Flow<List<VentaEntity>>
@Query("SELECT * FROM ventas WHERE id = :ventaId LIMIT 1")
suspend fun buscarVenta(ventaId: String): VentaEntity?
// DETALLES DE VENTA
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun guardarDetalleVenta(detalle: DetalleVentaEntity)
@Query("SELECT * FROM detalles_venta WHERE ventaId = :ventaId")
fun obtenerDetallesVenta(ventaId: String): Flow<List<DetalleVentaEntity>>
// PAGOS Y ABONOS
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun guardarPagoVenta(pago: PagoVentaEntity)
@Query("SELECT * FROM pagos_venta WHERE ventaId = :ventaId ORDER BY fechaCreacion ASC")
fun obtenerPagosVenta(ventaId: String): Flow<List<PagoVentaEntity>>
@Query("SELECT * FROM pagos_venta ORDER BY fechaCreacion DESC")
fun obtenerTodosLosPagos(): Flow<List<PagoVentaEntity>>
// REGISTRO COMPLETO DE VENTAS
@Insert
suspend fun guardarDetallesVenta(
detalles: List<DetalleVentaEntity>
)
@Insert
suspend fun guardarPagosVenta(
pagos: List<PagoVentaEntity>
)
@Insert
suspend fun guardarMovimientosVenta(
movimientos: List<MovimientoInventarioEntity>
)
// DESCONTAR EXISTENCIAS DE UNA SEDE
@Query("""
UPDATE inventario
SET cantidadDisponible = cantidadDisponible - :cantidad,
fechaActualizacion = :fecha
WHERE presentacionId = :presentacionId
AND sede = :sede
AND cantidadDisponible >= :cantidad
""")
suspend fun descontarExistencias(
presentacionId: String,
sede: String,
cantidad: Int,
fecha: Long
): Int
// GUARDAR VENTA Y SUS MOVIMIENTOS EN UNA SOLA TRANSACCION
@Transaction
suspend fun registrarVentaCompleta(
venta: VentaEntity,
detalles: List<DetalleVentaEntity>,
pagos: List<PagoVentaEntity>,
movimientos: List<MovimientoInventarioEntity>,
descontarInventario: Boolean
) {
check(venta.estadoEntrega != "PENDIENTE_ENTREGA") {
"Un pedido debe pasar por el proceso de reserva antes de quedar pendiente de entrega."
}
check(detalles.isNotEmpty()) {
"La venta debe tener al menos un producto."
}
check(detalles.all {
it.ventaId == venta.id && it.cantidad > 0
}) {
"Hay productos con cantidades incorrectas."
}
check(descontarInventario == (venta.estadoEntrega == "ENTREGADA")) {
"El estado de entrega no coincide con la operación de inventario."
}
if (descontarInventario) {
val asignaciones = mutableListOf<LoteVentaEntity>()
for (detalle in detalles) {
// Descontar del inventario general
val resultado = descontarExistencias(
presentacionId = detalle.presentacionId,
sede = venta.sede,
cantidad = detalle.cantidad,
fecha = System.currentTimeMillis()
)
check(resultado == 1) {
"No hay existencias suficientes para completar la venta."
}
// Buscar los lotes más antiguos de esa presentación y sede
val lotes = obtenerLotesDisponiblesFIFO(
presentacionId = detalle.presentacionId,
sede = venta.sede
)
var pendiente = detalle.cantidad
for (lote in lotes) {
if (pendiente <= 0) break
val cantidadTomada = minOf(
pendiente,
lote.cantidadDisponible
)
val actualizado = descontarCantidadLote(
loteInventarioId = lote.id,
cantidad = cantidadTomada
)
check(actualizado == 1) {
"No se pudo descontar una cantidad del lote."
}
asignaciones.add(
LoteVentaEntity(
id = UUID.randomUUID().toString(),
ventaId = venta.id,
detalleVentaId = detalle.id,
loteInventarioId = lote.id,
cantidad = cantidadTomada,
estado = "VENDIDA"
)
)
pendiente -= cantidadTomada
}
check(pendiente == 0) {
"No hay existencias suficientes en los lotes de esta sede."
}
}
if (asignaciones.isNotEmpty()) {
guardarAsignacionesLoteVenta(asignaciones)
}
}
guardarVenta(venta)
guardarDetallesVenta(detalles)
if (pagos.isNotEmpty()) {
guardarPagosVenta(pagos)
}
if (descontarInventario && movimientos.isNotEmpty()) {
guardarMovimientosVenta(movimientos)
}
}
// CONSULTAR DETALLES DE UN PEDIDO
@Query("""
SELECT * FROM detalles_venta
WHERE ventaId = :ventaId
""")
suspend fun buscarDetallesVenta(
ventaId: String
): List<DetalleVentaEntity>
// CAMBIAR ESTADO DE UN PEDIDO
@Query("""
UPDATE ventas
SET estadoEntrega = :nuevoEstado
WHERE id = :ventaId
AND estadoEntrega = :estadoActual
""")
suspend fun actualizarEstadoEntrega(
ventaId: String,
estadoActual: String,
nuevoEstado: String
): Int
// RESERVAR EXISTENCIAS
@Query("""
UPDATE inventario
SET cantidadDisponible = cantidadDisponible - :cantidad,
cantidadReservada = cantidadReservada + :cantidad,
fechaActualizacion = :fecha
WHERE presentacionId = :presentacionId
AND sede = :sede
AND cantidadDisponible >= :cantidad
""")
suspend fun reservarExistencias(
presentacionId: String,
sede: String,
cantidad: Int,
fecha: Long
): Int
// CONSUMIR EXISTENCIAS RESERVADAS AL ENTREGAR
@Query("""
UPDATE inventario
SET cantidadReservada = cantidadReservada - :cantidad,
fechaActualizacion = :fecha
WHERE presentacionId = :presentacionId
AND sede = :sede
AND cantidadReservada >= :cantidad
""")
suspend fun consumirExistenciasReservadas(
presentacionId: String,
sede: String,
cantidad: Int,
fecha: Long
): Int
// PASAR PEDIDO A PENDIENTE DE ENTREGA
@Transaction
suspend fun reservarPedidoParaEntrega(
ventaId: String
) {
val venta = buscarVenta(ventaId)
?: error("No se encontró el pedido.")
check(venta.estadoEntrega == "PENDIENTE_FABRICACION") {
"El pedido no está pendiente de fabricación."
}
val detalles = buscarDetallesVenta(ventaId)
check(detalles.isNotEmpty()) {
"El pedido no tiene productos registrados."
}
check(detalles.all {
it.ventaId == ventaId && it.cantidad > 0
}) {
"Hay productos con cantidades incorrectas en el pedido."
}
val fecha = System.currentTimeMillis()
val movimientos = mutableListOf<MovimientoInventarioEntity>()
val asignaciones = mutableListOf<LoteVentaEntity>()
// Reservar primero en el inventario general
val detallesAgrupados = detalles.groupBy { it.presentacionId }
for ((presentacionId, lineas) in detallesAgrupados) {
val cantidad = lineas.sumOf { it.cantidad }
val resultado = reservarExistencias(
presentacionId = presentacionId,
sede = venta.sede,
cantidad = cantidad,
fecha = fecha
)
check(resultado == 1) {
"No hay existencias suficientes para reservar el pedido."
}
movimientos.add(
MovimientoInventarioEntity(
id = UUID.randomUUID().toString(),
presentacionId = presentacionId,
sede = venta.sede,
tipoMovimiento = "RESERVA_PEDIDO",
cantidad = cantidad,
referenciaId = ventaId,
observacion = "Reserva de unidades para pedido"
)
)
}
// Reservar también en los lotes, siguiendo FIFO
for (detalle in detalles) {
var pendiente = detalle.cantidad
val lotes = obtenerLotesDisponiblesFIFO(
presentacionId = detalle.presentacionId,
sede = venta.sede
)
for (lote in lotes) {
if (pendiente <= 0) break
val cantidadTomada = minOf(
pendiente,
lote.cantidadDisponible
)
val actualizado = reservarCantidadLote(
loteInventarioId = lote.id,
cantidad = cantidadTomada
)
check(actualizado == 1) {
"No se pudo reservar una cantidad del lote."
}
asignaciones.add(
LoteVentaEntity(
id = UUID.randomUUID().toString(),
ventaId = ventaId,
detalleVentaId = detalle.id,
loteInventarioId = lote.id,
cantidad = cantidadTomada,
estado = "RESERVADA"
)
)
pendiente -= cantidadTomada
}
check(pendiente == 0) {
"No hay existencias suficientes en los lotes para reservar el pedido."
}
}
// Guardar las relaciones entre pedido y lotes
if (asignaciones.isNotEmpty()) {
guardarAsignacionesLoteVenta(asignaciones)
}
guardarMovimientosVenta(movimientos)
val actualizado = actualizarEstadoEntrega(
ventaId = ventaId,
estadoActual = "PENDIENTE_FABRICACION",
nuevoEstado = "PENDIENTE_ENTREGA"
)
check(actualizado == 1) {
"No se pudo actualizar el estado del pedido."
}
}
// CONFIRMAR ENTREGA DE UN PEDIDO RESERVADO
@Transaction
suspend fun entregarPedidoReservado(
ventaId: String
) {
val venta = buscarVenta(ventaId)
?: error("No se encontró el pedido.")
check(venta.estadoEntrega == "PENDIENTE_ENTREGA") {
"El pedido no está pendiente de entrega."
}
val detalles = buscarDetallesVenta(ventaId)
check(detalles.isNotEmpty()) {
"El pedido no tiene productos registrados."
}
val fecha = System.currentTimeMillis()
val movimientos = mutableListOf<MovimientoInventarioEntity>()
// Recuperar las asignaciones de lotes reservadas
val asignaciones = obtenerAsignacionesReservadas(ventaId)
check(asignaciones.isNotEmpty()) {
"El pedido no tiene unidades reservadas en los lotes."
}
// Comprobar que las cantidades reservadas coincidan
for (detalle in detalles) {
val cantidadAsignada = asignaciones
.filter { it.detalleVentaId == detalle.id }
.sumOf { it.cantidad }
check(cantidadAsignada == detalle.cantidad) {
"Las unidades reservadas no coinciden con el detalle del pedido."
}
}
// Consumir las reservas de cada lote
for (asignacion in asignaciones) {
val resultado = consumirReservaLote(
loteInventarioId = asignacion.loteInventarioId,
cantidad = asignacion.cantidad
)
check(resultado == 1) {
"No se pudo consumir una reserva de lote."
}
}
// Cambiar las asignaciones de RESERVADA a VENDIDA
val asignacionesActualizadas = actualizarEstadoAsignacionesLote(
ventaId = ventaId,
estadoActual = "RESERVADA",
nuevoEstado = "VENDIDA"
)
check(asignacionesActualizadas == asignaciones.size) {
"No se pudieron actualizar todas las asignaciones de lotes."
}
// Consumir también las reservas del inventario general
val detallesAgrupados = detalles.groupBy { it.presentacionId }
for ((presentacionId, lineas) in detallesAgrupados) {
val cantidad = lineas.sumOf { it.cantidad }
val resultado = consumirExistenciasReservadas(
presentacionId = presentacionId,
sede = venta.sede,
cantidad = cantidad,
fecha = fecha
)
check(resultado == 1) {
"No se encontraron todas las unidades reservadas."
}
movimientos.add(
MovimientoInventarioEntity(
id = UUID.randomUUID().toString(),
presentacionId = presentacionId,
sede = venta.sede,
tipoMovimiento = "VENTA",
cantidad = cantidad,
referenciaId = ventaId,
observacion = "Salida por entrega de pedido"
)
)
}
guardarMovimientosVenta(movimientos)
val actualizado = actualizarEstadoEntrega(
ventaId = ventaId,
estadoActual = "PENDIENTE_ENTREGA",
nuevoEstado = "ENTREGADA"
)
check(actualizado == 1) {
"No se pudo confirmar la entrega del pedido."
}
}
@Query("""
UPDATE inventario
SET cantidadDisponible = cantidadDisponible + :cantidad,
cantidadReservada = cantidadReservada - :cantidad,
fechaActualizacion = :fecha
WHERE presentacionId = :presentacionId
AND sede = :sede
AND cantidadReservada >= :cantidad
""")
suspend fun liberarExistenciasReservadas(
presentacionId: String,
sede: String,
cantidad: Int,
fecha: Long
): Int
@Transaction
suspend fun cancelarPedido(ventaId: String) {
val venta = buscarVenta(ventaId)
?: error("No se encontró el pedido.")
check(
venta.estadoEntrega == "PENDIENTE_FABRICACION" ||
venta.estadoEntrega == "PENDIENTE_ENTREGA"
) {
"Este pedido no se puede cancelar."
}
val fecha = System.currentTimeMillis()
val movimientos = mutableListOf<MovimientoInventarioEntity>()
// Si ya estaba reservado, devolver las unidades a sus lotes originales.
if (venta.estadoEntrega == "PENDIENTE_ENTREGA") {
val detalles = buscarDetallesVenta(ventaId)
check(detalles.isNotEmpty()) {
"El pedido no tiene productos registrados."
}
val asignaciones = obtenerAsignacionesReservadas(ventaId)
check(asignaciones.isNotEmpty()) {
"No se encontraron las reservas de los lotes."
}
// Comprobar que las reservas coincidan con los detalles.
for (detalle in detalles) {
val cantidadAsignada = asignaciones
.filter { it.detalleVentaId == detalle.id }
.sumOf { it.cantidad }
check(cantidadAsignada == detalle.cantidad) {
"Las unidades reservadas no coinciden con el pedido."
}
}
// Liberar las reservas de cada lote.
for (asignacion in asignaciones) {
val resultado = liberarReservaLote(
loteInventarioId = asignacion.loteInventarioId,
cantidad = asignacion.cantidad
)
check(resultado == 1) {
"No se pudo devolver una reserva a su lote original."
}
}
// Cambiar las asignaciones de RESERVADA a CANCELADA.
val asignacionesActualizadas = actualizarEstadoAsignacionesLote(
ventaId = ventaId,
estadoActual = "RESERVADA",
nuevoEstado = "CANCELADA"
)
check(asignacionesActualizadas == asignaciones.size) {
"No se pudieron actualizar todas las asignaciones de lotes."
}
// Liberar también las reservas del inventario general.
val detallesAgrupados = detalles.groupBy { it.presentacionId }
for ((presentacionId, lineas) in detallesAgrupados) {
val cantidad = lineas.sumOf { it.cantidad }
val resultado = liberarExistenciasReservadas(
presentacionId,
venta.sede,
cantidad,
fecha
)
check(resultado == 1) {
"No se pudieron liberar todas las unidades reservadas."
}
movimientos.add(
MovimientoInventarioEntity(
id = UUID.randomUUID().toString(),
presentacionId = presentacionId,
sede = venta.sede,
tipoMovimiento = "LIBERACION_RESERVA",
cantidad = cantidad,
referenciaId = ventaId,
observacion = "Liberación por cancelación del pedido"
)
)
}
if (movimientos.isNotEmpty()) {
guardarMovimientosVenta(movimientos)
}
}
val actualizado = actualizarEstadoEntrega(
ventaId,
venta.estadoEntrega,
"CANCELADA"
)
check(actualizado == 1) {
"No se pudo cancelar el pedido."
}
}
// LIMPIEZA COMPLETA DE DATOS DE PRUEBA
@Query("DELETE FROM lotes_ventas")
suspend fun borrarAsignacionesLotesVenta()
@Query("DELETE FROM pagos_venta")
suspend fun borrarPagosVenta()
@Query("DELETE FROM detalles_venta")
suspend fun borrarDetallesVenta()
@Query("DELETE FROM ventas")
suspend fun borrarVentas()
@Query("DELETE FROM movimientos_inventario")
suspend fun borrarMovimientosInventario()
@Query("DELETE FROM inventario")
suspend fun borrarInventario()
@Query("DELETE FROM distribuciones_produccion")
suspend fun borrarDistribucionesProduccion()
@Query("DELETE FROM producciones")
suspend fun borrarProducciones()
@Query("DELETE FROM lotes_gastos")
suspend fun borrarGastosLotes()
@Query("DELETE FROM lotes_detalle")
suspend fun borrarDetallesLotes()
@Query("DELETE FROM lotes_inventario")
suspend fun borrarInventariosLotes()
@Query("DELETE FROM lotes_produccion")
suspend fun borrarLotesProduccion()
@Query("DELETE FROM clientes")
suspend fun borrarClientes()
@Query("DELETE FROM presentaciones")
suspend fun borrarPresentaciones()
@Query("DELETE FROM aromas")
suspend fun borrarAromas()
@Query("DELETE FROM productos")
suspend fun borrarProductos()
@Query("""
UPDATE control_numeracion_lotes
SET ultimoNumero = 0
WHERE id = 1
""")
suspend fun reiniciarContadorLotes(): Int
@Transaction
suspend fun limpiarDatosDePrueba() {
borrarAsignacionesLotesVenta()
borrarPagosVenta()
borrarDetallesVenta()
borrarVentas()
borrarMovimientosInventario()
borrarInventario()
borrarDistribucionesProduccion()
borrarProducciones()
borrarGastosLotes()
borrarDetallesLotes()
borrarInventariosLotes()
borrarLotesProduccion()
borrarClientes()
borrarPresentaciones()
borrarAromas()
borrarProductos()
val contadorReiniciado = reiniciarContadorLotes()
check(contadorReiniciado == 1) {
"No se pudo reiniciar la numeración de los lotes."
}
}
// ESTADISTICAS DEL INICIO
// Total vendido durante el día
@Query("""
SELECT COALESCE(SUM(total), 0)
FROM ventas
WHERE fechaCreacion >= :inicioDia
AND fechaCreacion < :finDia
AND estadoEntrega != 'CANCELADA'
""")
fun obtenerVentasDelDia(
inicioDia: Long,
finDia: Long
): Flow<Double>
// Dinero recibido durante el día
@Query("""
SELECT COALESCE(SUM(monto), 0)
FROM pagos_venta
WHERE fechaCreacion >= :inicioDia
AND fechaCreacion < :finDia
AND ventaId IN (
SELECT id FROM ventas
WHERE estadoEntrega != 'CANCELADA'
)
""")
fun obtenerDineroRecibidoDelDia(
inicioDia: Long,
finDia: Long
): Flow<Double>
// Saldo pendiente de cobro de todas las ventas no canceladas
@Query("""
SELECT COALESCE(
SUM(MAX(
v.total - COALESCE(
(SELECT SUM(p.monto)
FROM pagos_venta p
WHERE p.ventaId = v.id),
0
),
0
)),
0
)
FROM ventas v
WHERE v.estadoEntrega != 'CANCELADA'
""")
fun obtenerTotalPorCobrar(): Flow<Double>
// Unidades disponibles en todas las sedes
@Query("""
SELECT COALESCE(SUM(cantidadDisponible), 0)
FROM inventario
""")
fun obtenerInventarioDisponible(): Flow<Int>
}
41,968 chars · codepastes.com