untitled

Pasted Nov 9, 2026, 4:11 AM1 file1,345 lines

Scan to open on your phone

Point a camera at the QR code - it opens this exact paste, no app needed.

https://codepastes.com/zzgs9b0vakxzhctx
untitled

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>
    )

    @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 * 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_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>

}

40,068 chars · codepastes.com