All checks were successful
Optimized Build and Deploy / remote-build-and-deploy (push) Successful in 2m13s
Se introduce una nueva funcionalidad para el ingreso masivo de bobinas a partir de un único remito. Esto agiliza significativamente la carga de datos y reduce errores al evitar la repetición de la planta, número y fecha de remito. La implementación incluye: - Un modal maestro-detalle de dos pasos que primero verifica el remito y luego permite la carga de las bobinas. - Lógica de autocompletado de fecha y feedback al usuario si el remito ya existe. - Un nuevo endpoint en el backend para procesar el lote de forma transaccional.
533 lines
27 KiB
C#
533 lines
27 KiB
C#
using GestionIntegral.Api.Data;
|
|
using GestionIntegral.Api.Data.Repositories.Impresion;
|
|
using GestionIntegral.Api.Dtos.Impresion;
|
|
using GestionIntegral.Api.Models.Impresion;
|
|
using GestionIntegral.Api.Models.Distribucion; // Para Publicacion, PubliSeccion
|
|
using GestionIntegral.Api.Data.Repositories.Distribucion;
|
|
using Microsoft.Extensions.Logging;
|
|
using System;
|
|
using System.Collections.Generic;
|
|
using System.Data;
|
|
using System.Linq;
|
|
using System.Threading.Tasks;
|
|
using GestionIntegral.Api.Dtos.Auditoria;
|
|
|
|
namespace GestionIntegral.Api.Services.Impresion
|
|
{
|
|
public class StockBobinaService : IStockBobinaService
|
|
{
|
|
private readonly IStockBobinaRepository _stockBobinaRepository;
|
|
private readonly ITipoBobinaRepository _tipoBobinaRepository;
|
|
private readonly IPlantaRepository _plantaRepository;
|
|
private readonly IEstadoBobinaRepository _estadoBobinaRepository;
|
|
private readonly IPublicacionRepository _publicacionRepository; // Para validar IdPublicacion
|
|
private readonly IPubliSeccionRepository _publiSeccionRepository; // Para validar IdSeccion
|
|
private readonly DbConnectionFactory _connectionFactory;
|
|
private readonly ILogger<StockBobinaService> _logger;
|
|
|
|
public StockBobinaService(
|
|
IStockBobinaRepository stockBobinaRepository,
|
|
ITipoBobinaRepository tipoBobinaRepository,
|
|
IPlantaRepository plantaRepository,
|
|
IEstadoBobinaRepository estadoBobinaRepository,
|
|
IPublicacionRepository publicacionRepository,
|
|
IPubliSeccionRepository publiSeccionRepository,
|
|
DbConnectionFactory connectionFactory,
|
|
ILogger<StockBobinaService> logger)
|
|
{
|
|
_stockBobinaRepository = stockBobinaRepository;
|
|
_tipoBobinaRepository = tipoBobinaRepository;
|
|
_plantaRepository = plantaRepository;
|
|
_estadoBobinaRepository = estadoBobinaRepository;
|
|
_publicacionRepository = publicacionRepository;
|
|
_publiSeccionRepository = publiSeccionRepository;
|
|
_connectionFactory = connectionFactory;
|
|
_logger = logger;
|
|
}
|
|
|
|
// Mapeo complejo porque necesitamos nombres de entidades relacionadas
|
|
private async Task<StockBobinaDto> MapToDto(StockBobina bobina)
|
|
{
|
|
if (bobina == null) return null!; // Debería ser manejado por el llamador
|
|
|
|
var tipoBobina = await _tipoBobinaRepository.GetByIdAsync(bobina.IdTipoBobina);
|
|
var planta = await _plantaRepository.GetByIdAsync(bobina.IdPlanta);
|
|
var estado = await _estadoBobinaRepository.GetByIdAsync(bobina.IdEstadoBobina);
|
|
Publicacion? publicacion = null;
|
|
PubliSeccion? seccion = null;
|
|
|
|
if (bobina.IdPublicacion.HasValue)
|
|
publicacion = await _publicacionRepository.GetByIdSimpleAsync(bobina.IdPublicacion.Value);
|
|
if (bobina.IdSeccion.HasValue)
|
|
seccion = await _publiSeccionRepository.GetByIdAsync(bobina.IdSeccion.Value); // Asume que GetByIdAsync existe
|
|
|
|
return new StockBobinaDto
|
|
{
|
|
IdBobina = bobina.IdBobina,
|
|
IdTipoBobina = bobina.IdTipoBobina,
|
|
NombreTipoBobina = tipoBobina?.Denominacion ?? "N/A",
|
|
NroBobina = bobina.NroBobina,
|
|
Peso = bobina.Peso,
|
|
IdPlanta = bobina.IdPlanta,
|
|
NombrePlanta = planta?.Nombre ?? "N/A",
|
|
IdEstadoBobina = bobina.IdEstadoBobina,
|
|
NombreEstadoBobina = estado?.Denominacion ?? "N/A",
|
|
Remito = bobina.Remito,
|
|
FechaRemito = bobina.FechaRemito.ToString("yyyy-MM-dd"),
|
|
FechaEstado = bobina.FechaEstado?.ToString("yyyy-MM-dd"),
|
|
IdPublicacion = bobina.IdPublicacion,
|
|
NombrePublicacion = publicacion?.Nombre,
|
|
IdSeccion = bobina.IdSeccion,
|
|
NombreSeccion = seccion?.Nombre,
|
|
Obs = bobina.Obs
|
|
};
|
|
}
|
|
|
|
public async Task<IEnumerable<StockBobinaDto>> ObtenerTodosAsync(
|
|
int? idTipoBobina, string? nroBobinaFilter, int? idPlanta,
|
|
int? idEstadoBobina, string? remitoFilter, DateTime? fechaDesde, DateTime? fechaHasta)
|
|
{
|
|
var bobinas = await _stockBobinaRepository.GetAllAsync(idTipoBobina, nroBobinaFilter, idPlanta, idEstadoBobina, remitoFilter, fechaDesde, fechaHasta);
|
|
var dtos = new List<StockBobinaDto>();
|
|
foreach (var bobina in bobinas)
|
|
{
|
|
dtos.Add(await MapToDto(bobina));
|
|
}
|
|
return dtos;
|
|
}
|
|
|
|
public async Task<StockBobinaDto?> ObtenerPorIdAsync(int idBobina)
|
|
{
|
|
var bobina = await _stockBobinaRepository.GetByIdAsync(idBobina);
|
|
return bobina == null ? null : await MapToDto(bobina);
|
|
}
|
|
|
|
public async Task<(StockBobinaDto? Bobina, string? Error)> IngresarBobinaAsync(CreateStockBobinaDto createDto, int idUsuario)
|
|
{
|
|
if (await _tipoBobinaRepository.GetByIdAsync(createDto.IdTipoBobina) == null)
|
|
return (null, "Tipo de bobina inválido.");
|
|
if (await _plantaRepository.GetByIdAsync(createDto.IdPlanta) == null)
|
|
return (null, "Planta inválida.");
|
|
if (await _stockBobinaRepository.GetByNroBobinaAsync(createDto.NroBobina) != null)
|
|
return (null, $"El número de bobina '{createDto.NroBobina}' ya existe.");
|
|
|
|
var nuevaBobina = new StockBobina
|
|
{
|
|
IdTipoBobina = createDto.IdTipoBobina,
|
|
NroBobina = createDto.NroBobina,
|
|
Peso = createDto.Peso,
|
|
IdPlanta = createDto.IdPlanta,
|
|
IdEstadoBobina = 1, // 1 = Disponible (según contexto previo)
|
|
Remito = createDto.Remito,
|
|
FechaRemito = createDto.FechaRemito.Date,
|
|
FechaEstado = createDto.FechaRemito.Date, // Estado inicial en fecha de remito
|
|
IdPublicacion = null,
|
|
IdSeccion = null,
|
|
Obs = null
|
|
};
|
|
|
|
using var connection = _connectionFactory.CreateConnection();
|
|
if (connection is System.Data.Common.DbConnection dbConn) await dbConn.OpenAsync(); else connection.Open();
|
|
using var transaction = connection.BeginTransaction();
|
|
try
|
|
{
|
|
var bobinaCreada = await _stockBobinaRepository.CreateAsync(nuevaBobina, idUsuario, transaction);
|
|
if (bobinaCreada == null) throw new DataException("Error al ingresar la bobina.");
|
|
transaction.Commit();
|
|
_logger.LogInformation("Bobina ID {Id} ingresada por Usuario ID {UserId}.", bobinaCreada.IdBobina, idUsuario);
|
|
return (await MapToDto(bobinaCreada), null);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
try { transaction.Rollback(); } catch { }
|
|
_logger.LogError(ex, "Error IngresarBobinaAsync: {NroBobina}", createDto.NroBobina);
|
|
return (null, $"Error interno: {ex.Message}");
|
|
}
|
|
}
|
|
|
|
public async Task<(bool Exito, string? Error)> ActualizarDatosBobinaDisponibleAsync(int idBobina, UpdateStockBobinaDto updateDto, int idUsuario)
|
|
{
|
|
using var connection = _connectionFactory.CreateConnection();
|
|
if (connection is System.Data.Common.DbConnection dbConn) await dbConn.OpenAsync(); else connection.Open();
|
|
using var transaction = connection.BeginTransaction();
|
|
try
|
|
{
|
|
var bobinaExistente = await _stockBobinaRepository.GetByIdAsync(idBobina); // Obtener dentro de TX
|
|
if (bobinaExistente == null) return (false, "Bobina no encontrada.");
|
|
if (bobinaExistente.IdEstadoBobina != 1) // Solo se pueden editar datos si está "Disponible"
|
|
return (false, "Solo se pueden modificar los datos de una bobina en estado 'Disponible'.");
|
|
|
|
// Validar unicidad de NroBobina si cambió
|
|
if (bobinaExistente.NroBobina != updateDto.NroBobina &&
|
|
await _stockBobinaRepository.GetByNroBobinaAsync(updateDto.NroBobina) != null) // Validar fuera de TX o pasar TX al repo
|
|
{
|
|
try { transaction.Rollback(); } catch { } // Rollback antes de retornar por validación
|
|
return (false, $"El nuevo número de bobina '{updateDto.NroBobina}' ya existe.");
|
|
}
|
|
if (await _tipoBobinaRepository.GetByIdAsync(updateDto.IdTipoBobina) == null)
|
|
return (false, "Tipo de bobina inválido.");
|
|
//if (await _plantaRepository.GetByIdAsync(updateDto.IdPlanta) == null)
|
|
// return (false, "Planta inválida.");
|
|
|
|
|
|
bobinaExistente.IdTipoBobina = updateDto.IdTipoBobina;
|
|
bobinaExistente.NroBobina = updateDto.NroBobina;
|
|
bobinaExistente.Peso = updateDto.Peso;
|
|
//bobinaExistente.IdPlanta = updateDto.IdPlanta;
|
|
//bobinaExistente.Remito = updateDto.Remito;
|
|
//bobinaExistente.FechaRemito = updateDto.FechaRemito.Date;
|
|
// FechaEstado se mantiene ya que el estado no cambia aquí
|
|
|
|
var actualizado = await _stockBobinaRepository.UpdateAsync(bobinaExistente, idUsuario, transaction, "Datos Actualizados");
|
|
if (!actualizado) throw new DataException("Error al actualizar la bobina.");
|
|
transaction.Commit();
|
|
return (true, null);
|
|
}
|
|
catch (KeyNotFoundException) { try { transaction.Rollback(); } catch { } return (false, "Bobina no encontrada."); }
|
|
catch (Exception ex)
|
|
{
|
|
try { transaction.Rollback(); } catch { }
|
|
_logger.LogError(ex, "Error ActualizarDatosBobinaDisponibleAsync ID: {IdBobina}", idBobina);
|
|
return (false, $"Error interno: {ex.Message}");
|
|
}
|
|
}
|
|
|
|
public async Task<(bool Exito, string? Error)> CambiarEstadoBobinaAsync(int idBobina, CambiarEstadoBobinaDto cambiarEstadoDto, int idUsuario)
|
|
{
|
|
using var connection = _connectionFactory.CreateConnection();
|
|
if (connection is System.Data.Common.DbConnection dbConn) await dbConn.OpenAsync(); else connection.Open();
|
|
using var transaction = connection.BeginTransaction();
|
|
try
|
|
{
|
|
var bobina = await _stockBobinaRepository.GetByIdAsync(idBobina); // Obtener dentro de la transacción
|
|
if (bobina == null)
|
|
{
|
|
try { transaction.Rollback(); } catch { }
|
|
return (false, "Bobina no encontrada.");
|
|
}
|
|
|
|
var nuevoEstado = await _estadoBobinaRepository.GetByIdAsync(cambiarEstadoDto.NuevoEstadoId);
|
|
if (nuevoEstado == null)
|
|
{
|
|
try { transaction.Rollback(); } catch { }
|
|
return (false, "El nuevo estado especificado no es válido.");
|
|
}
|
|
|
|
// --- INICIO DE VALIDACIONES DE FLUJO DE ESTADOS ---
|
|
if (bobina.IdEstadoBobina == cambiarEstadoDto.NuevoEstadoId)
|
|
{
|
|
try { transaction.Rollback(); } catch { }
|
|
return (false, "La bobina ya se encuentra en ese estado.");
|
|
}
|
|
|
|
// Reglas específicas para salir de "Dañada" (ID 3)
|
|
if (bobina.IdEstadoBobina == 3) // 3 = Dañada
|
|
{
|
|
if (cambiarEstadoDto.NuevoEstadoId != 1) // 1 = Disponible
|
|
{
|
|
try { transaction.Rollback(); } catch { }
|
|
return (false, "Una bobina dañada solo puede ser revertida a 'Disponible' (si fue un error).");
|
|
}
|
|
// Si se cambia de Dañada a Disponible, se limpiarán Publicacion y Seccion más abajo.
|
|
}
|
|
// Regla para salir de "Utilizada" (ID 2)
|
|
else if (bobina.IdEstadoBobina == 2) // 2 = En Uso
|
|
{
|
|
if (cambiarEstadoDto.NuevoEstadoId == 1) // No se puede volver a Disponible directamente
|
|
{
|
|
try { transaction.Rollback(); } catch { }
|
|
return (false, "Una bobina 'En Uso' no puede volver a 'Disponible' directamente mediante esta acción. Primero debe marcarse como Dañada si es necesario.");
|
|
}
|
|
// Si se cambia de Utilizada a cualquier otro estado que no sea Dañada (aunque aquí solo se permite Dañada),
|
|
// y si el nuevo estado NO es En Uso, también se deberían limpiar Publicacion y Seccion.
|
|
// La lógica actual ya lo hace si nuevoEstadoId no es 2.
|
|
}
|
|
// Regla para salir de "Disponible" (ID 1)
|
|
else if (bobina.IdEstadoBobina == 1) // 1 = Disponible
|
|
{
|
|
// Desde Disponible puede pasar a En Uso (2) o Dañada (3).
|
|
// El frontend ya debería filtrar esto, pero una validación aquí es buena.
|
|
if (cambiarEstadoDto.NuevoEstadoId != 2 && cambiarEstadoDto.NuevoEstadoId != 3)
|
|
{
|
|
try { transaction.Rollback(); } catch { }
|
|
return (false, "Desde 'Disponible', solo se puede cambiar a 'En Uso' o 'Dañada'.");
|
|
}
|
|
}
|
|
// --- FIN VALIDACIONES DE FLUJO DE ESTADOS ---
|
|
|
|
|
|
bobina.IdEstadoBobina = cambiarEstadoDto.NuevoEstadoId;
|
|
bobina.FechaEstado = cambiarEstadoDto.FechaCambioEstado.Date;
|
|
bobina.Obs = cambiarEstadoDto.Obs ?? bobina.Obs;
|
|
|
|
if (cambiarEstadoDto.NuevoEstadoId == 2) // 2 = "En Uso"
|
|
{
|
|
if (!cambiarEstadoDto.IdPublicacion.HasValue || cambiarEstadoDto.IdPublicacion.Value <= 0)
|
|
{
|
|
try { transaction.Rollback(); } catch { }
|
|
return (false, "Para el estado 'En Uso', se requiere Publicación.");
|
|
}
|
|
if (!cambiarEstadoDto.IdSeccion.HasValue || cambiarEstadoDto.IdSeccion.Value <= 0)
|
|
{
|
|
try { transaction.Rollback(); } catch { }
|
|
return (false, "Para el estado 'En Uso', se requiere Sección.");
|
|
}
|
|
|
|
// Validar existencia de Publicación y Sección
|
|
var publicacion = await _publicacionRepository.GetByIdSimpleAsync(cambiarEstadoDto.IdPublicacion.Value);
|
|
if (publicacion == null)
|
|
{
|
|
try { transaction.Rollback(); } catch { }
|
|
return (false, "Publicación inválida.");
|
|
}
|
|
|
|
// Asumiendo que GetByIdAsync en IPubliSeccionRepository valida si la sección pertenece a la publicación también
|
|
var seccion = await _publiSeccionRepository.GetByIdAsync(cambiarEstadoDto.IdSeccion.Value);
|
|
if (seccion == null || seccion.IdPublicacion != cambiarEstadoDto.IdPublicacion.Value)
|
|
{
|
|
try { transaction.Rollback(); } catch { }
|
|
return (false, "Sección inválida o no pertenece a la publicación seleccionada.");
|
|
}
|
|
|
|
bobina.IdPublicacion = cambiarEstadoDto.IdPublicacion.Value;
|
|
bobina.IdSeccion = cambiarEstadoDto.IdSeccion.Value;
|
|
}
|
|
else // Si el nuevo estado NO es "En Uso" (ej. Disponible, Dañada)
|
|
{
|
|
bobina.IdPublicacion = null;
|
|
bobina.IdSeccion = null;
|
|
// La observación se mantiene o se actualiza según venga en el DTO.
|
|
// Si se pasa de "Dañada" a "Disponible", el DTO de cambio de estado debe incluir la nueva obs.
|
|
}
|
|
|
|
var actualizado = await _stockBobinaRepository.UpdateAsync(bobina, idUsuario, transaction, $"Estado: {nuevoEstado.Denominacion}");
|
|
if (!actualizado) throw new DataException("Error al cambiar estado de la bobina.");
|
|
|
|
transaction.Commit();
|
|
return (true, null);
|
|
}
|
|
catch (KeyNotFoundException) { try { transaction.Rollback(); } catch { } return (false, "Bobina no encontrada."); }
|
|
catch (Exception ex)
|
|
{
|
|
try { transaction.Rollback(); } catch (Exception rbEx) { _logger.LogError(rbEx, "Error en Rollback CambiarEstadoBobinaAsync."); }
|
|
_logger.LogError(ex, "Error CambiarEstadoBobinaAsync ID: {IdBobina}", idBobina);
|
|
return (false, $"Error interno: {ex.Message}");
|
|
}
|
|
}
|
|
|
|
public async Task<(bool Exito, string? Error)> EliminarIngresoErroneoAsync(int idBobina, int idUsuario)
|
|
{
|
|
using var connection = _connectionFactory.CreateConnection();
|
|
if (connection is System.Data.Common.DbConnection dbConn) await dbConn.OpenAsync(); else connection.Open();
|
|
using var transaction = connection.BeginTransaction();
|
|
try
|
|
{
|
|
var bobina = await _stockBobinaRepository.GetByIdAsync(idBobina); // Obtener dentro de la transacción
|
|
if (bobina == null)
|
|
{
|
|
try { transaction.Rollback(); } catch { }
|
|
return (false, "Bobina no encontrada.");
|
|
}
|
|
|
|
// --- Permitir eliminar si está Disponible (1) o Dañada (3) ---
|
|
if (bobina.IdEstadoBobina != 1 && bobina.IdEstadoBobina != 3)
|
|
{
|
|
try { transaction.Rollback(); } catch { }
|
|
return (false, "Solo se pueden eliminar ingresos de bobinas que estén en estado 'Disponible' o 'Dañada'.");
|
|
}
|
|
|
|
var eliminado = await _stockBobinaRepository.DeleteAsync(idBobina, idUsuario, transaction);
|
|
if (!eliminado) throw new DataException("Error al eliminar la bobina.");
|
|
|
|
transaction.Commit();
|
|
return (true, null);
|
|
}
|
|
catch (KeyNotFoundException) { try { transaction.Rollback(); } catch { } return (false, "Bobina no encontrada."); }
|
|
catch (Exception ex)
|
|
{
|
|
try { transaction.Rollback(); } catch (Exception rbEx) { _logger.LogError(rbEx, "Error en Rollback EliminarIngresoErroneoAsync."); }
|
|
_logger.LogError(ex, "Error EliminarIngresoErroneoAsync ID: {IdBobina}", idBobina);
|
|
return (false, $"Error interno: {ex.Message}");
|
|
}
|
|
}
|
|
|
|
public async Task<IEnumerable<StockBobinaHistorialDto>> ObtenerHistorialAsync(
|
|
DateTime? fechaDesde, DateTime? fechaHasta,
|
|
int? idUsuarioModifico, string? tipoModificacion,
|
|
int? idBobinaAfectada, int? idTipoBobinaFiltro, int? idPlantaFiltro, int? idEstadoBobinaFiltro)
|
|
{
|
|
var historialData = await _stockBobinaRepository.GetHistorialDetalladoAsync(fechaDesde, fechaHasta, idUsuarioModifico, tipoModificacion, idBobinaAfectada, idTipoBobinaFiltro, idPlantaFiltro, idEstadoBobinaFiltro);
|
|
|
|
return historialData.Select(h => new StockBobinaHistorialDto
|
|
{
|
|
Id_Bobina = h.Historial.Id_Bobina,
|
|
Id_TipoBobina = h.Historial.Id_TipoBobina,
|
|
NombreTipoBobina = h.NombreTipoBobina ?? "N/A",
|
|
NroBobina = h.Historial.NroBobina,
|
|
Peso = h.Historial.Peso,
|
|
Id_Planta = h.Historial.Id_Planta,
|
|
NombrePlanta = h.NombrePlanta ?? "N/A",
|
|
Id_EstadoBobina = h.Historial.Id_EstadoBobina,
|
|
NombreEstadoBobina = h.NombreEstadoBobina ?? "N/A",
|
|
Remito = h.Historial.Remito,
|
|
FechaRemito = h.Historial.FechaRemito,
|
|
FechaEstado = h.Historial.FechaEstado,
|
|
Id_Publicacion = h.Historial.Id_Publicacion,
|
|
NombrePublicacion = h.NombrePublicacion,
|
|
Id_Seccion = h.Historial.Id_Seccion,
|
|
NombreSeccion = h.NombreSeccion,
|
|
Obs = h.Historial.Obs,
|
|
Id_Usuario = h.Historial.Id_Usuario,
|
|
NombreUsuarioModifico = h.NombreUsuarioModifico,
|
|
FechaMod = h.Historial.FechaMod,
|
|
TipoMod = h.Historial.TipoMod
|
|
}).ToList();
|
|
}
|
|
|
|
public async Task<IEnumerable<StockBobinaDto>> VerificarRemitoExistenteAsync(int idPlanta, string remito, DateTime? fechaRemito)
|
|
{
|
|
// Si la fecha tiene valor, filtramos por ese día exacto. Si no, busca en cualquier fecha.
|
|
DateTime? fechaDesde = fechaRemito?.Date;
|
|
DateTime? fechaHasta = fechaRemito?.Date;
|
|
|
|
var bobinas = await _stockBobinaRepository.GetAllAsync(null, null, idPlanta, null, remito, fechaDesde, fechaHasta);
|
|
|
|
var dtos = new List<StockBobinaDto>();
|
|
foreach (var bobina in bobinas)
|
|
{
|
|
dtos.Add(await MapToDto(bobina));
|
|
}
|
|
return dtos;
|
|
}
|
|
|
|
public async Task<(bool Exito, string? Error)> ActualizarFechaRemitoLoteAsync(UpdateFechaRemitoLoteDto dto, int idUsuario)
|
|
{
|
|
// 1. Buscar todas las bobinas que coinciden con el lote a modificar.
|
|
var bobinasAActualizar = await _stockBobinaRepository.GetAllAsync(
|
|
idTipoBobina: null,
|
|
nroBobinaFilter: null,
|
|
idPlanta: dto.IdPlanta,
|
|
idEstadoBobina: null,
|
|
remitoFilter: dto.Remito,
|
|
fechaDesde: dto.FechaRemitoActual.Date,
|
|
fechaHasta: dto.FechaRemitoActual.Date
|
|
);
|
|
|
|
if (!bobinasAActualizar.Any())
|
|
{
|
|
return (false, "No se encontraron bobinas para el remito, planta y fecha especificados. Es posible que ya hayan sido modificados.");
|
|
}
|
|
|
|
// 2. Iniciar una transacción para asegurar que todas las actualizaciones se completen o ninguna.
|
|
using var connection = _connectionFactory.CreateConnection();
|
|
if (connection is System.Data.Common.DbConnection dbConn) await dbConn.OpenAsync(); else connection.Open();
|
|
using var transaction = connection.BeginTransaction();
|
|
|
|
try
|
|
{
|
|
// 3. Iterar sobre cada bobina y actualizarla.
|
|
foreach (var bobina in bobinasAActualizar)
|
|
{
|
|
// Modificamos solo la fecha del remito.
|
|
bobina.FechaRemito = dto.NuevaFechaRemito.Date;
|
|
|
|
// Reutilizamos el método UpdateAsync que ya maneja la lógica de historial.
|
|
// Le pasamos un mensaje específico para el historial.
|
|
await _stockBobinaRepository.UpdateAsync(bobina, idUsuario, transaction, "Fecha Remito Corregida");
|
|
}
|
|
|
|
// 4. Si todo salió bien, confirmar la transacción.
|
|
transaction.Commit();
|
|
_logger.LogInformation(
|
|
"{Count} bobinas del remito {Remito} (Planta ID {IdPlanta}) actualizadas a nueva fecha {NuevaFecha} por Usuario ID {IdUsuario}.",
|
|
bobinasAActualizar.Count(), dto.Remito, dto.IdPlanta, dto.NuevaFechaRemito.Date, idUsuario
|
|
);
|
|
return (true, null);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
// 5. Si algo falla, revertir todos los cambios.
|
|
try { transaction.Rollback(); } catch { }
|
|
_logger.LogError(ex, "Error transaccional al actualizar fecha de remito {Remito}.", dto.Remito);
|
|
return (false, $"Error interno al actualizar el lote: {ex.Message}");
|
|
}
|
|
}
|
|
|
|
public async Task<(bool Exito, string? Error)> IngresarBobinaLoteAsync(CreateStockBobinaLoteDto loteDto, int idUsuario)
|
|
{
|
|
// --- FASE 1: VALIDACIÓN PREVIA (FUERA DE LA TRANSACCIÓN) ---
|
|
|
|
// Validación de la cabecera
|
|
if (await _plantaRepository.GetByIdAsync(loteDto.IdPlanta) == null)
|
|
return (false, "La planta especificada no es válida.");
|
|
|
|
// Validación de cada bobina del lote
|
|
foreach (var bobinaDetalle in loteDto.Bobinas)
|
|
{
|
|
if (await _tipoBobinaRepository.GetByIdAsync(bobinaDetalle.IdTipoBobina) == null)
|
|
{
|
|
return (false, $"El tipo de bobina con ID {bobinaDetalle.IdTipoBobina} no es válido.");
|
|
}
|
|
|
|
// Esta es la lectura que causaba el bloqueo. Ahora se hace ANTES de la transacción.
|
|
if (await _stockBobinaRepository.GetByNroBobinaAsync(bobinaDetalle.NroBobina) != null)
|
|
{
|
|
return (false, $"El número de bobina '{bobinaDetalle.NroBobina}' ya existe en el sistema.");
|
|
}
|
|
}
|
|
|
|
// Validación de números de bobina duplicados dentro del mismo lote
|
|
var nrosBobinaEnLote = loteDto.Bobinas.Select(b => b.NroBobina.Trim()).ToList();
|
|
if (nrosBobinaEnLote.Count != nrosBobinaEnLote.Distinct().Count())
|
|
{
|
|
var duplicado = nrosBobinaEnLote.GroupBy(n => n).Where(g => g.Count() > 1).First().Key;
|
|
return (false, $"El número de bobina '{duplicado}' está duplicado en el lote que intenta ingresar.");
|
|
}
|
|
|
|
|
|
// --- FASE 2: ESCRITURA TRANSACCIONAL ---
|
|
|
|
using var connection = _connectionFactory.CreateConnection();
|
|
if (connection is System.Data.Common.DbConnection dbConn) await dbConn.OpenAsync(); else connection.Open();
|
|
using var transaction = connection.BeginTransaction();
|
|
|
|
try
|
|
{
|
|
// Ahora este bucle solo contiene operaciones de escritura. No habrá bloqueos.
|
|
foreach (var bobinaDetalle in loteDto.Bobinas)
|
|
{
|
|
var nuevaBobina = new StockBobina
|
|
{
|
|
IdTipoBobina = bobinaDetalle.IdTipoBobina,
|
|
NroBobina = bobinaDetalle.NroBobina,
|
|
Peso = bobinaDetalle.Peso,
|
|
IdPlanta = loteDto.IdPlanta,
|
|
Remito = loteDto.Remito,
|
|
FechaRemito = loteDto.FechaRemito.Date,
|
|
IdEstadoBobina = 1, // 1 = Disponible
|
|
FechaEstado = loteDto.FechaRemito.Date,
|
|
IdPublicacion = null,
|
|
IdSeccion = null,
|
|
Obs = null
|
|
};
|
|
|
|
var bobinaCreada = await _stockBobinaRepository.CreateAsync(nuevaBobina, idUsuario, transaction);
|
|
if (bobinaCreada == null)
|
|
{
|
|
throw new DataException($"No se pudo crear el registro para la bobina '{nuevaBobina.NroBobina}'.");
|
|
}
|
|
}
|
|
|
|
transaction.Commit();
|
|
_logger.LogInformation("Lote de {Count} bobinas para remito {Remito} ingresado por Usuario ID {UserId}.", loteDto.Bobinas.Count, loteDto.Remito, idUsuario);
|
|
return (true, null);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
try { transaction.Rollback(); } catch { }
|
|
_logger.LogError(ex, "Error al ingresar lote de bobinas para remito {Remito}", loteDto.Remito);
|
|
return (false, $"Error interno al procesar el lote: {ex.Message}");
|
|
}
|
|
}
|
|
}
|
|
} |