Scan to open on your phone
Point a camera at the QR code - it opens this exact paste, no app needed.
https://codepastes.com/zzgs8gpveggheaz1untitledPlain 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
@Query("""
UPDATE inventario
SET cantidadDisponible = cantidadDisponible + :diferencia,
fechaActualizacion = :fecha
WHERE presentacionId = :presentacionId
AND sede = :sede
AND cantidadDisponible + :diferencia >= 0
""")
suspend fun ajustarInventarioDisponible(
presentacionId: String,
sede: String,
diferencia: 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>
)
@Query("""
UPDATE lotes_detalle
SET cantidadFabricada = :cantidad
WHERE id = :detalleId
AND loteId = :loteId
""")
suspend fun actualizarCantidadDetalleLote(
detalleId: String,
loteId: String,
cantidad: Int
): Int
@Query("""
DELETE FROM lotes_detalle
WHERE id = :detalleId
AND loteId = :loteId
""")
suspend fun eliminarDetalleLote(
detalleId: String,
loteId: String
): Int
@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)
}
@Transaction
suspend fun editarLoteCompleto(
loteId: String,
detallesNuevos: List<LoteDetalleEntity>,
inventariosLoteNuevos: List<LoteInventarioEntity>
) {
check(puedeEditarProduccionLote(loteId)) {
"No se puede editar este lote porque tiene movimientos, ventas o reservas asociados."
}
val loteOriginal = obtenerLotePorId(loteId)
?: error("No se encontró el lote de producción.")
for (detalle in detallesNuevos) {
val presentacion = obtenerPresentacionPorId(
detalle.presentacionId
) ?: error("No se encontró una de las presentaciones.")
check(presentacion.aromaId == loteOriginal.aromaId) {
"No puedes agregar presentaciones de otro aroma."
}
val aroma = obtenerAromaPorId(presentacion.aromaId)
?: error("No se encontró el aroma de la presentación.")
check(aroma.productoId == loteOriginal.productoId) {
"No puedes agregar presentaciones de otro producto."
}
}
val detallesActuales = obtenerDetallesLote(loteId)
val inventariosActuales = obtenerInventariosDeLote(loteId)
check(detallesActuales.isNotEmpty()) {
"No se encontraron detalles de producción para este lote."
}
check(detallesNuevos.isNotEmpty()) {
"El lote debe conservar al menos una presentación."
}
check(detallesNuevos.all {
it.loteId == loteId && it.cantidadFabricada > 0
}) {
"Hay cantidades de producción incorrectas."
}
check(detallesNuevos.map { it.presentacionId }.distinct().size ==
detallesNuevos.size) {
"No puedes repetir una presentación."
}
check(inventariosLoteNuevos.all {
it.loteId == loteId &&
it.cantidadDisponible > 0 &&
it.cantidadReservada == 0 &&
it.sede.isNotBlank()
}) {
"Hay una distribución incorrecta."
}
val clavesNuevas = inventariosLoteNuevos.map {
"${it.presentacionId}|${it.sede}"
}
check(clavesNuevas.distinct().size == clavesNuevas.size) {
"Hay distribuciones duplicadas."
}
val clavesActuales = inventariosActuales.map {
"${it.presentacionId}|${it.sede}"
}
check(clavesActuales.distinct().size == clavesActuales.size) {
"El inventario actual del lote contiene distribuciones duplicadas."
}
check(inventariosActuales.all {
it.cantidadDisponible >= 0 && it.cantidadReservada == 0
}) {
"El inventario actual contiene cantidades reservadas o incorrectas."
}
val presentacionesNuevas =
detallesNuevos.map { it.presentacionId }.toSet()
val presentacionesDistribuidas =
inventariosLoteNuevos.map { it.presentacionId }.toSet()
check(presentacionesNuevas == presentacionesDistribuidas) {
"Las presentaciones fabricadas no coinciden con las distribuidas."
}
for (detalle in detallesNuevos) {
val cantidadDistribuida = inventariosLoteNuevos
.filter { it.presentacionId == detalle.presentacionId }
.sumOf { it.cantidadDisponible }
check(cantidadDistribuida == detalle.cantidadFabricada) {
"La distribución no coincide con la cantidad fabricada."
}
}
val presentacionesActuales =
detallesActuales.map { it.presentacionId }.toSet()
check(inventariosActuales.all {
it.presentacionId in presentacionesActuales
}) {
"El inventario actual contiene presentaciones que no pertenecen al lote."
}
for (detalle in detallesActuales) {
val cantidadActual = inventariosActuales
.filter { it.presentacionId == detalle.presentacionId }
.sumOf { it.cantidadDisponible + it.cantidadReservada }
check(cantidadActual == detalle.cantidadFabricada) {
"El inventario actual no coincide con la producción registrada."
}
}
val actualesPorClave = inventariosActuales.associateBy {
"${it.presentacionId}|${it.sede}"
}
val nuevasPorClave = inventariosLoteNuevos.associateBy {
"${it.presentacionId}|${it.sede}"
}
val todasLasClaves =
(actualesPorClave.keys + nuevasPorClave.keys).toSet()
val fecha = System.currentTimeMillis()
val movimientos = mutableListOf<MovimientoInventarioEntity>()
// Ajustar inventario general e inventario FIFO.
for (clave in todasLasClaves) {
val inventarioActual = actualesPorClave[clave]
val inventarioNuevo = nuevasPorClave[clave]
val presentacionId =
inventarioNuevo?.presentacionId
?: inventarioActual!!.presentacionId
val sede =
inventarioNuevo?.sede
?: inventarioActual!!.sede
val cantidadAnterior =
inventarioActual?.cantidadDisponible ?: 0
val cantidadNueva =
inventarioNuevo?.cantidadDisponible ?: 0
val diferencia = cantidadNueva - cantidadAnterior
if (diferencia == 0) continue
// Actualizar inventario general.
val inventarioGeneral = buscarInventario(
presentacionId = presentacionId,
sede = sede
)
if (inventarioGeneral == null) {
check(diferencia > 0) {
"No existe inventario general para retirar unidades."
}
guardarInventario(
InventarioEntity(
id = UUID.randomUUID().toString(),
presentacionId = presentacionId,
sede = sede,
cantidadDisponible = diferencia,
cantidadReservada = 0,
fechaActualizacion = fecha
)
)
} else {
val actualizado = ajustarInventarioDisponible(
presentacionId = presentacionId,
sede = sede,
diferencia = diferencia,
fecha = fecha
)
check(actualizado == 1) {
"No se pudo ajustar el inventario general. Puede que no haya existencias suficientes."
}
}
// Actualizar inventario individual del lote.
if (inventarioActual != null) {
val actualizado = ajustarInventarioLote(
inventarioLoteId = inventarioActual.id,
diferencia = diferencia
)
check(actualizado == 1) {
"No se pudo ajustar el inventario FIFO del lote."
}
} else if (cantidadNueva > 0) {
guardarInventariosLote(
listOf(
inventarioNuevo!!.copy(
id = UUID.randomUUID().toString(),
loteId = loteId,
cantidadReservada = 0
)
)
)
}
// Registrar el ajuste sin borrar el historial anterior.
movimientos.add(
MovimientoInventarioEntity(
id = UUID.randomUUID().toString(),
presentacionId = presentacionId,
sede = sede,
tipoMovimiento = if (diferencia > 0)
"AJUSTE_PRODUCCION_ENTRADA"
else
"AJUSTE_PRODUCCION_SALIDA",
cantidad = kotlin.math.abs(diferencia),
referenciaId = loteId,
observacion = "Ajuste de producción del lote"
)
)
}
// Actualizar cantidades de presentaciones existentes.
val detallesActualesPorPresentacion =
detallesActuales.associateBy { it.presentacionId }
for (detalleNuevo in detallesNuevos) {
val detalleActual =
detallesActualesPorPresentacion[detalleNuevo.presentacionId]
if (detalleActual != null) {
val actualizado = actualizarCantidadDetalleLote(
detalleId = detalleActual.id,
loteId = loteId,
cantidad = detalleNuevo.cantidadFabricada
)
check(actualizado == 1) {
"No se pudo actualizar una presentación del lote."
}
} else {
// Agregar una presentación nueva.
guardarDetallesLote(
listOf(
detalleNuevo.copy(
id = UUID.randomUUID().toString(),
loteId = loteId
)
)
)
}
}
// Retirar presentaciones que ya no pertenecen al lote.
for (detalleActual in detallesActuales) {
if (detalleActual.presentacionId !in presentacionesNuevas) {
val eliminado = eliminarDetalleLote(
detalleId = detalleActual.id,
loteId = loteId
)
check(eliminado == 1) {
"No se pudo retirar una presentación del lote."
}
}
}
if (movimientos.isNotEmpty()) {
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 * FROM lotes_produccion
WHERE id = :loteId
LIMIT 1
""")
suspend fun obtenerLotePorId(loteId: String): LoteProduccionEntity?
@Query("""
SELECT * FROM presentaciones
WHERE id = :presentacionId
LIMIT 1
""")
suspend fun obtenerPresentacionPorId(
presentacionId: String
): PresentacionEntity?
@Query("""
SELECT * FROM aromas
WHERE id = :aromaId
LIMIT 1
""")
suspend fun obtenerAromaPorId(
aromaId: String
): AromaEntity?
@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 AS m
WHERE m.tipoMovimiento NOT IN (
'PRODUCCION',
'AJUSTE_PRODUCCION_ENTRADA',
'AJUSTE_PRODUCCION_SALIDA'
)
AND m.fechaCreacion >= (
SELECT fechaCreacion
FROM lotes_produccion
WHERE id = :loteId
)
AND EXISTS (
SELECT 1
FROM lotes_detalle AS d
INNER JOIN lotes_inventario AS li
ON li.loteId = d.loteId
AND li.presentacionId = d.presentacionId
WHERE d.loteId = :loteId
AND d.presentacionId = m.presentacionId
AND li.sede = m.sede
)
""")
suspend fun contarMovimientosPosterioresDeLote(
loteId: String
): Int
@Query("""
SELECT COUNT(*)
FROM lotes_inventario
WHERE loteId = :loteId
AND cantidadReservada > 0
""")
suspend fun contarReservasDeLote(
loteId: String
): Int
@Transaction
suspend fun puedeEditarProduccionLote(loteId: String): Boolean {
val asignaciones = contarAsignacionesVentaDeLote(loteId)
val movimientos = contarMovimientosPosterioresDeLote(loteId)
val reservas = contarReservasDeLote(loteId)
return asignaciones == 0 &&
movimientos == 0 &&
reservas == 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 * FROM lotes_inventario
WHERE loteId = :loteId
""")
suspend fun obtenerInventariosDeLote(
loteId: String
): 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 + :diferencia
WHERE id = :inventarioLoteId
AND cantidadDisponible + :diferencia >= 0
""")
suspend fun ajustarInventarioLote(
inventarioLoteId: String,
diferencia: 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>
}
package com.negocio.desinfectantes.ui.produccion
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.*
import androidx.compose.runtime.Composable
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import com.negocio.desinfectantes.data.*
import java.text.NumberFormat
import java.text.SimpleDateFormat
import java.util.Date
import java.util.Locale
private val LoteNavy = Color(0xFF101D35)
private val LoteCardBlue = Color(0xFF1B2D4A)
private val LoteAccentBlue = Color(0xFF2585F5)
private val LoteSoftBlue = Color(0xFFA9BEDB)
private val LoteWhite = Color(0xFFFFFFFF)
private data class FilaEdicionLote(
val presentacionId: String,
val cantidadFabricada: String,
val barrancas: String,
val valledupar: String
)
@Composable
fun HistorialLotesScreen(
modifier: Modifier = Modifier,
lotes: List<LoteProduccionEntity>,
detalles: List<LoteDetalleEntity>,
gastos: List<LoteGastoEntity>,
inventariosLote: List<LoteInventarioEntity>,
ingresosPorLoteYSede: List<IngresoLoteSedeResultado> = emptyList(),
productos: List<ProductoEntity>,
aromas: List<AromaEntity>,
presentaciones: List<PresentacionEntity>,
onEditarGasto: (
gastoId: String,
loteId: String,
concepto: String,
monto: Double,
observacion: String?
) -> Unit,
onAgregarGasto: (LoteGastoEntity) -> Unit,
onEliminarGasto: (
gastoId: String,
loteId: String
) -> Unit,
onEditarProduccion: (
loteId: String,
detalles: List<LoteDetalleEntity>,
inventarios: List<LoteInventarioEntity>,
onResultado: (String?) -> Unit
) -> Unit,
onVolver: () -> Unit
) {
var loteEnEdicion by remember {
mutableStateOf<LoteProduccionEntity?>(null)
}
val filasEdicionLote = remember {
mutableStateListOf<FilaEdicionLote>()
}
var presentacionParaAgregar by remember {
mutableStateOf("")
}
var mostrarSelectorPresentacion by remember {
mutableStateOf(false)
}
var errorEdicionLote by remember {
mutableStateOf<String?>(null)
}
var gastoEnEdicion by remember {
mutableStateOf<LoteGastoEntity?>(null)
}
var gastoAEliminar by remember {
mutableStateOf<LoteGastoEntity?>(null)
}
var conceptoEditado by remember {
mutableStateOf("")
}
var montoEditado by remember {
mutableStateOf("")
}
var observacionEditada by remember {
mutableStateOf("")
}
var loteParaNuevoGasto by remember {
mutableStateOf<LoteProduccionEntity?>(null)
}
var nuevoConcepto by remember {
mutableStateOf("")
}
var nuevoMonto by remember {
mutableStateOf("")
}
var nuevaObservacion by remember {
mutableStateOf("")
}
fun abrirEditorProduccion(lote: LoteProduccionEntity) {
filasEdicionLote.clear()
val detallesDelLote = detalles.filter {
it.loteId == lote.id
}
for (detalle in detallesDelLote) {
val inventariosDePresentacion = inventariosLote.filter {
it.loteId == lote.id &&
it.presentacionId == detalle.presentacionId
}
val cantidadBarrancas = inventariosDePresentacion
.filter { it.sede == "Barrancas" }
.sumOf { it.cantidadDisponible }
val cantidadValledupar = inventariosDePresentacion
.filter { it.sede == "Valledupar" }
.sumOf { it.cantidadDisponible }
filasEdicionLote.add(
FilaEdicionLote(
presentacionId = detalle.presentacionId,
cantidadFabricada = detalle.cantidadFabricada.toString(),
barrancas = cantidadBarrancas.toString(),
valledupar = cantidadValledupar.toString()
)
)
}
presentacionParaAgregar = ""
loteEnEdicion = lote
}
val formatoFecha = SimpleDateFormat(
"dd/MM/yyyy • hh:mm a",
Locale("es", "CO")
)
val formatoPesos = NumberFormat.getCurrencyInstance(
Locale("es", "CO")
).apply {
maximumFractionDigits = 0
minimumFractionDigits = 0
}
Column(
modifier = modifier
.fillMaxSize()
.background(LoteNavy)
.verticalScroll(rememberScrollState())
.padding(20.dp),
verticalArrangement = Arrangement.spacedBy(16.dp)
) {
TextButton(onClick = onVolver) {
Text("← Volver", color = LoteSoftBlue)
}
Text(
"Historial de lotes",
color = LoteWhite,
style = MaterialTheme.typography.headlineMedium,
fontWeight = FontWeight.Bold
)
Text(
"Consulta las producciones y sus gastos registrados.",
color = LoteSoftBlue
)
if (lotes.isEmpty()) {
Card(
modifier = Modifier.fillMaxWidth(),
colors = CardDefaults.cardColors(
containerColor = LoteCardBlue
),
shape = RoundedCornerShape(16.dp)
) {
Column(
modifier = Modifier.padding(20.dp),
verticalArrangement = Arrangement.spacedBy(8.dp)
) {
Text(
"Todavía no hay lotes registrados",
color = LoteWhite,
fontWeight = FontWeight.Bold
)
Text(
"Cuando registres una producción, aparecerá aquí.",
color = LoteSoftBlue
)
}
}
}
lotes.forEach { lote ->
val producto = productos.firstOrNull {
it.id == lote.productoId
}
val aroma = aromas.firstOrNull {
it.id == lote.aromaId
}
val detallesLote = detalles.filter {
it.loteId == lote.id
}
val gastosLote = gastos.filter {
it.loteId == lote.id
}
val inventarios = inventariosLote.filter {
it.loteId == lote.id
}
val totalUnidades = detallesLote.sumOf {
it.cantidadFabricada
}
val totalGastos = gastosLote.sumOf {
it.monto
}
val ingresosLote = ingresosPorLoteYSede.filter {
it.loteId == lote.id
}
val ingresosBarrancas = ingresosLote
.filter { it.sede.equals("Barrancas", ignoreCase = true) }
.sumOf { it.ingresos }
val ingresosValledupar = ingresosLote
.filter { it.sede.equals("Valledupar", ignoreCase = true) }
.sumOf { it.ingresos }
val ingresosTotales = ingresosBarrancas + ingresosValledupar
val gananciaLote = ingresosTotales - totalGastos
Card(
modifier = Modifier.fillMaxWidth(),
colors = CardDefaults.cardColors(
containerColor = LoteCardBlue
),
shape = RoundedCornerShape(18.dp)
) {
Column(
modifier = Modifier.padding(16.dp),
verticalArrangement = Arrangement.spacedBy(12.dp)
) {
Text(
lote.numeroLote,
color = LoteAccentBlue,
style = MaterialTheme.typography.titleMedium,
fontWeight = FontWeight.Bold
)
Text(
producto?.nombre ?: "Producto no encontrado",
color = LoteWhite,
style = MaterialTheme.typography.titleLarge,
fontWeight = FontWeight.Bold
)
Text(
"Aroma: ${aroma?.nombre ?: "No encontrado"}",
color = LoteSoftBlue
)
Text(
"Fecha: ${formatoFecha.format(Date(lote.fechaCreacion))}",
color = LoteSoftBlue
)
HorizontalDivider(color = Color(0xFF40536F))
Text(
"Producción",
color = LoteAccentBlue,
fontWeight = FontWeight.Bold
)
detallesLote.forEach { detalle ->
val presentacion = presentaciones.firstOrNull {
it.id == detalle.presentacionId
}
val distribuciones = inventarios.filter {
it.presentacionId == detalle.presentacionId
}
Column(
verticalArrangement = Arrangement.spacedBy(5.dp)
) {
Text(
presentacion?.nombre
?: "Presentación no encontrada",
color = LoteWhite,
fontWeight = FontWeight.SemiBold
)
Text(
"${detalle.cantidadFabricada} unidades fabricadas",
color = LoteSoftBlue
)
distribuciones.forEach { distribucion ->
Text(
"• ${distribucion.sede}: ${distribucion.cantidadDisponible} unidades disponibles",
color = LoteSoftBlue
)
if (distribucion.cantidadReservada > 0) {
Text(
" ${distribucion.cantidadReservada} unidades reservadas",
color = LoteAccentBlue
)
}
}
}
}
Text(
"Gastos de producción",
color = LoteAccentBlue,
fontWeight = FontWeight.Bold
)
if (gastosLote.isEmpty()) {
Text(
"No se registraron gastos.",
color = LoteSoftBlue
)
} else {
gastosLote.forEach { gasto ->
Column(
verticalArrangement = Arrangement.spacedBy(6.dp)
) {
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween
) {
Text(
gasto.concepto,
color = LoteWhite,
modifier = Modifier.weight(1f)
)
Text(
formatoPesos.format(gasto.monto),
color = LoteWhite,
fontWeight = FontWeight.SemiBold
)
}
if (!gasto.observacion.isNullOrBlank()) {
Text(
gasto.observacion,
color = LoteSoftBlue,
style = MaterialTheme.typography.bodySmall
)
}
Row(
horizontalArrangement = Arrangement.spacedBy(12.dp)
) {
TextButton(
onClick = {
gastoEnEdicion = gasto
conceptoEditado = gasto.concepto
montoEditado = gasto.monto.toString()
observacionEditada = gasto.observacion ?: ""
}
) {
Text("Editar", color = LoteAccentBlue)
}
TextButton(
onClick = {
gastoAEliminar = gasto
}
) {
Text("Eliminar", color = Color(0xFFFF6B6B))
}
}
}
}
}
TextButton(
onClick = {
loteParaNuevoGasto = lote
nuevoConcepto = ""
nuevoMonto = ""
nuevaObservacion = ""
}
) {
Text(
"+ Añadir gasto",
color = LoteAccentBlue,
fontWeight = FontWeight.Bold
)
}
HorizontalDivider(color = Color(0xFF40536F))
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween
) {
Text(
"Total de gastos",
color = LoteSoftBlue
)
Text(
formatoPesos.format(totalGastos),
color = LoteWhite,
fontWeight = FontWeight.Bold
)
}
HorizontalDivider(color = Color(0xFF40536F))
Text(
"Rentabilidad del lote",
color = LoteAccentBlue,
fontWeight = FontWeight.Bold
)
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween
) {
Text("Ingresos Barrancas", color = LoteSoftBlue)
Text(
formatoPesos.format(ingresosBarrancas),
color = LoteWhite
)
}
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween
) {
Text("Ingresos Valledupar", color = LoteSoftBlue)
Text(
formatoPesos.format(ingresosValledupar),
color = LoteWhite
)
}
HorizontalDivider(color = Color(0xFF40536F))
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween
) {
Text(
"Ingresos totales",
color = LoteWhite,
fontWeight = FontWeight.Bold
)
Text(
formatoPesos.format(ingresosTotales),
color = LoteWhite,
fontWeight = FontWeight.Bold
)
}
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween
) {
Text(
if (gananciaLote >= 0) "Ganancia del lote" else "Pérdida del lote",
color = LoteWhite,
fontWeight = FontWeight.Bold
)
Text(
formatoPesos.format(gananciaLote),
color = if (gananciaLote >= 0) Color(0xFF4ADE80)
else Color(0xFFFF6B6B),
fontWeight = FontWeight.Bold
)
}
HorizontalDivider(color = Color(0xFF40536F))
TextButton(
onClick = {
abrirEditorProduccion(lote)
},
modifier = Modifier.fillMaxWidth()
) {
Text(
"✏️ Editar producción",
color = LoteAccentBlue,
fontWeight = FontWeight.Bold
)
}
}
}
}
Spacer(modifier = Modifier.height(20.dp))
}
gastoEnEdicion?.let { gasto ->
AlertDialog(
onDismissRequest = {
gastoEnEdicion = null
},
title = {
Text("Editar gasto")
},
text = {
Column(
verticalArrangement = Arrangement.spacedBy(12.dp)
) {
OutlinedTextField(
value = conceptoEditado,
onValueChange = {
conceptoEditado = it
},
label = { Text("Concepto") },
singleLine = true
)
OutlinedTextField(
value = montoEditado,
onValueChange = {
montoEditado = it
},
label = { Text("Monto") },
singleLine = true
)
OutlinedTextField(
value = observacionEditada,
onValueChange = {
observacionEditada = it
},
label = { Text("Observación (opcional)") }
)
}
},
confirmButton = {
TextButton(
onClick = {
val monto = montoEditado
.replace(",", ".")
.toDoubleOrNull()
if (
conceptoEditado.isNotBlank() &&
monto != null &&
monto >= 0
) {
onEditarGasto(
gasto.id,
gasto.loteId,
conceptoEditado.trim(),
monto,
observacionEditada.trim().ifBlank { null }
)
gastoEnEdicion = null
}
}
) {
Text("Guardar")
}
},
dismissButton = {
TextButton(
onClick = {
gastoEnEdicion = null
}
) {
Text("Cancelar")
}
},
containerColor = LoteCardBlue,
titleContentColor = LoteWhite,
textContentColor = LoteWhite
)
}
gastoAEliminar?.let { gasto ->
AlertDialog(
onDismissRequest = {
gastoAEliminar = null
},
title = {
Text("Eliminar gasto")
},
text = {
Text(
"¿Estás seguro de que deseas eliminar el gasto \"${gasto.concepto}\" por ${formatoPesos.format(gasto.monto)}? Esta acción no se puede deshacer."
)
},
confirmButton = {
TextButton(
onClick = {
onEliminarGasto(
gasto.id,
gasto.loteId
)
gastoAEliminar = null
}
) {
Text(
"Eliminar",
color = Color(0xFFFF6B6B)
)
}
},
dismissButton = {
TextButton(
onClick = {
gastoAEliminar = null
}
) {
Text("Cancelar")
}
},
containerColor = LoteCardBlue,
titleContentColor = LoteWhite,
textContentColor = LoteWhite
)
}
loteParaNuevoGasto?.let { lote ->
AlertDialog(
onDismissRequest = {
loteParaNuevoGasto = null
},
title = {
Text("Añadir gasto a ${lote.numeroLote}")
},
text = {
Column(
verticalArrangement = Arrangement.spacedBy(12.dp)
) {
OutlinedTextField(
value = nuevoConcepto,
onValueChange = {
nuevoConcepto = it
},
label = { Text("Concepto") },
singleLine = true
)
OutlinedTextField(
value = nuevoMonto,
onValueChange = {
nuevoMonto = it
},
label = { Text("Monto") },
singleLine = true
)
OutlinedTextField(
value = nuevaObservacion,
onValueChange = {
nuevaObservacion = it
},
label = { Text("Observación (opcional)") }
)
}
},
confirmButton = {
TextButton(
onClick = {
val monto = nuevoMonto
.replace(",", ".")
.toDoubleOrNull()
if (
nuevoConcepto.isNotBlank() &&
monto != null &&
monto >= 0
) {
onAgregarGasto(
LoteGastoEntity(
id = java.util.UUID.randomUUID().toString(),
loteId = lote.id,
concepto = nuevoConcepto.trim(),
monto = monto,
observacion = nuevaObservacion
.trim()
.ifBlank { null }
)
)
loteParaNuevoGasto = null
}
}
) {
Text("Guardar")
}
},
dismissButton = {
TextButton(
onClick = {
loteParaNuevoGasto = null
}
) {
Text("Cancelar")
}
},
containerColor = LoteCardBlue,
titleContentColor = LoteWhite,
textContentColor = LoteWhite
)
}
loteEnEdicion?.let { lote ->
AlertDialog(
onDismissRequest = {
loteEnEdicion = null
filasEdicionLote.clear()
},
title = {
Text(
"Editar producción ${lote.numeroLote}",
color = LoteWhite,
fontWeight = FontWeight.Bold
)
},
text = {
Column(
modifier = Modifier
.heightIn(max = 400.dp)
.verticalScroll(rememberScrollState()),
verticalArrangement = Arrangement.spacedBy(10.dp)
) {
Text(
"Modifica las cantidades fabricadas y su distribución.",
color = LoteWhite
)
errorEdicionLote?.let { mensaje ->
Text(
text = mensaje,
color = Color(0xFFFF6B6B),
fontWeight = FontWeight.Bold
)
}
filasEdicionLote.forEachIndexed { index, fila ->
val nombrePresentacion = presentaciones
.firstOrNull {
it.id == fila.presentacionId
}?.nombre ?: "Presentación"
HorizontalDivider(
color = Color(0xFF40536F)
)
Text(
nombrePresentacion,
color = LoteAccentBlue,
fontWeight = FontWeight.Bold
)
OutlinedTextField(
value = fila.cantidadFabricada,
onValueChange = { nuevoValor ->
if (nuevoValor.all { it.isDigit() }) {
filasEdicionLote[index] =
fila.copy(cantidadFabricada = nuevoValor)
}
},
label = { Text("Cantidad fabricada") },
modifier = Modifier.fillMaxWidth(),
singleLine = true
)
OutlinedTextField(
value = fila.barrancas,
onValueChange = { nuevoValor ->
if (nuevoValor.all { it.isDigit() }) {
filasEdicionLote[index] =
fila.copy(barrancas = nuevoValor)
}
},
label = { Text("Barrancas") },
modifier = Modifier.fillMaxWidth(),
singleLine = true
)
OutlinedTextField(
value = fila.valledupar,
onValueChange = { nuevoValor ->
if (nuevoValor.all { it.isDigit() }) {
filasEdicionLote[index] =
fila.copy(valledupar = nuevoValor)
}
},
label = { Text("Valledupar") },
modifier = Modifier.fillMaxWidth(),
singleLine = true
)
TextButton(
onClick = {
filasEdicionLote.removeAt(index)
}
) {
Text(
"Quitar presentación",
color = Color(0xFFFF6B6B)
)
}
}
Box {
TextButton(
onClick = {
mostrarSelectorPresentacion = true
}
) {
Text(
"+ Agregar presentación",
color = LoteAccentBlue
)
}
DropdownMenu(
expanded = mostrarSelectorPresentacion,
onDismissRequest = {
mostrarSelectorPresentacion = false
}
) {
presentaciones
.filter {
it.aromaId == lote.aromaId &&
it.activo &&
filasEdicionLote.none { fila ->
fila.presentacionId == it.id
}
}
.forEach { presentacion ->
DropdownMenuItem(
text = {
Text(presentacion.nombre)
},
onClick = {
filasEdicionLote.add(
FilaEdicionLote(
presentacionId = presentacion.id,
cantidadFabricada = "0",
barrancas = "0",
valledupar = "0"
)
)
presentacionParaAgregar = presentacion.id
mostrarSelectorPresentacion = false
}
)
}
}
}
}
},
confirmButton = {
TextButton(
onClick = {
errorEdicionLote = null
if (filasEdicionLote.isEmpty()) {
errorEdicionLote = "Debes agregar al menos una presentación."
return@TextButton
}
val detallesNuevos = mutableListOf<LoteDetalleEntity>()
val inventariosNuevos = mutableListOf<LoteInventarioEntity>()
for (fila in filasEdicionLote) {
val fabricada = fila.cantidadFabricada.toIntOrNull()
val barrancas = fila.barrancas.toIntOrNull()
val valledupar = fila.valledupar.toIntOrNull()
if (fabricada == null || barrancas == null || valledupar == null) {
errorEdicionLote = "Completa todas las cantidades."
return@TextButton
}
if (fabricada <= 0) {
errorEdicionLote = "La cantidad fabricada debe ser mayor que cero."
return@TextButton
}
if (barrancas + valledupar != fabricada) {
errorEdicionLote =
"La distribución debe coincidir con la cantidad fabricada."
return@TextButton
}
val detalleExistente = detalles.firstOrNull {
it.loteId == lote.id &&
it.presentacionId == fila.presentacionId
}
detallesNuevos.add(
LoteDetalleEntity(
id = detalleExistente?.id
?: java.util.UUID.randomUUID().toString(),
loteId = lote.id,
presentacionId = fila.presentacionId,
cantidadFabricada = fabricada
)
)
if (barrancas > 0) {
val inventarioExistente = inventariosLote.firstOrNull {
it.loteId == lote.id &&
it.presentacionId == fila.presentacionId &&
it.sede == "Barrancas"
}
inventariosNuevos.add(
LoteInventarioEntity(
id = inventarioExistente?.id
?: java.util.UUID.randomUUID().toString(),
loteId = lote.id,
presentacionId = fila.presentacionId,
sede = "Barrancas",
cantidadDisponible = barrancas,
cantidadReservada = 0
)
)
}
if (valledupar > 0) {
val inventarioExistente = inventariosLote.firstOrNull {
it.loteId == lote.id &&
it.presentacionId == fila.presentacionId &&
it.sede == "Valledupar"
}
inventariosNuevos.add(
LoteInventarioEntity(
id = inventarioExistente?.id
?: java.util.UUID.randomUUID().toString(),
loteId = lote.id,
presentacionId = fila.presentacionId,
sede = "Valledupar",
cantidadDisponible = valledupar,
cantidadReservada = 0
)
)
}
}
onEditarProduccion(
lote.id,
detallesNuevos,
inventariosNuevos
) { resultado ->
if (resultado == null) {
loteEnEdicion = null
filasEdicionLote.clear()
errorEdicionLote = null
} else {
errorEdicionLote = resultado
}
}
}
) {
Text("Guardar cambios")
}
}
)
}
}
91,843 chars · codepastes.com