feat: Implementar ingreso de bobinas por lote
All checks were successful
Optimized Build and Deploy / remote-build-and-deploy (push) Successful in 2m13s
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.
This commit is contained in:
@@ -2,6 +2,7 @@ using GestionIntegral.Api.Dtos.Impresion;
|
||||
using GestionIntegral.Api.Services.Impresion;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Mvc.ModelBinding;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
@@ -127,7 +128,7 @@ namespace GestionIntegral.Api.Controllers.Impresion
|
||||
if (!ModelState.IsValid) return BadRequest(ModelState); // Validaciones de DTO (Required, Range, etc.)
|
||||
var userId = GetCurrentUserId();
|
||||
if (userId == null) return Unauthorized();
|
||||
|
||||
|
||||
// La validación de que IdPublicacion/IdSeccion son requeridos para estado "En Uso"
|
||||
// ahora está más robusta en el servicio. Se puede quitar del controlador
|
||||
// si se prefiere que el servicio sea la única fuente de verdad para esa lógica.
|
||||
@@ -172,5 +173,72 @@ namespace GestionIntegral.Api.Controllers.Impresion
|
||||
}
|
||||
return NoContent();
|
||||
}
|
||||
|
||||
// GET: api/stockbobinas/verificar-remito
|
||||
[HttpGet("verificar-remito")]
|
||||
[ProducesResponseType(typeof(IEnumerable<StockBobinaDto>), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status400BadRequest)]
|
||||
[ProducesResponseType(StatusCodes.Status403Forbidden)]
|
||||
// CAMBIO: Hacer fechaRemito opcional (nullable)
|
||||
public async Task<IActionResult> VerificarRemito([FromQuery, BindRequired] int idPlanta, [FromQuery, BindRequired] string remito, [FromQuery] DateTime? fechaRemito)
|
||||
{
|
||||
if (!TienePermiso(PermisoIngresarBobina)) return Forbid();
|
||||
|
||||
try
|
||||
{
|
||||
// Pasamos el parámetro nullable al servicio
|
||||
var bobinasExistentes = await _stockBobinaService.VerificarRemitoExistenteAsync(idPlanta, remito, fechaRemito);
|
||||
return Ok(bobinasExistentes);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error al verificar remito {Remito} para planta {IdPlanta}", remito, idPlanta);
|
||||
return StatusCode(StatusCodes.Status500InternalServerError, "Error interno al verificar el remito.");
|
||||
}
|
||||
}
|
||||
|
||||
[HttpPut("actualizar-fecha-remito")]
|
||||
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
||||
[ProducesResponseType(StatusCodes.Status400BadRequest)]
|
||||
[ProducesResponseType(StatusCodes.Status403Forbidden)]
|
||||
public async Task<IActionResult> ActualizarFechaRemitoLote([FromBody] UpdateFechaRemitoLoteDto dto)
|
||||
{
|
||||
// Reutilizamos el permiso de modificar datos, ya que es una corrección.
|
||||
if (!TienePermiso(PermisoModificarDatos)) return Forbid();
|
||||
if (!ModelState.IsValid) return BadRequest(ModelState);
|
||||
var userId = GetCurrentUserId();
|
||||
if (userId == null) return Unauthorized();
|
||||
|
||||
var (exito, error) = await _stockBobinaService.ActualizarFechaRemitoLoteAsync(dto, userId.Value);
|
||||
|
||||
if (!exito)
|
||||
{
|
||||
return BadRequest(new { message = error });
|
||||
}
|
||||
|
||||
return NoContent();
|
||||
}
|
||||
|
||||
// POST: api/stockbobinas/lote
|
||||
[HttpPost("lote")]
|
||||
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
||||
[ProducesResponseType(StatusCodes.Status400BadRequest)]
|
||||
[ProducesResponseType(StatusCodes.Status403Forbidden)]
|
||||
public async Task<IActionResult> IngresarLoteBobinas([FromBody] CreateStockBobinaLoteDto loteDto)
|
||||
{
|
||||
if (!TienePermiso(PermisoIngresarBobina)) return Forbid();
|
||||
if (!ModelState.IsValid) return BadRequest(ModelState);
|
||||
var userId = GetCurrentUserId();
|
||||
if (userId == null) return Unauthorized();
|
||||
|
||||
var (exito, error) = await _stockBobinaService.IngresarBobinaLoteAsync(loteDto, userId.Value);
|
||||
|
||||
if (!exito)
|
||||
{
|
||||
return BadRequest(new { message = error });
|
||||
}
|
||||
|
||||
return NoContent(); // 204 es una buena respuesta para un lote procesado exitosamente sin devolver contenido.
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
// Backend/GestionIntegral.Api/Models/Dtos/Impresion/BobinaLoteDetalleDto.cs
|
||||
|
||||
namespace GestionIntegral.Api.Dtos.Impresion
|
||||
{
|
||||
public class BobinaLoteDetalleDto
|
||||
{
|
||||
public int IdTipoBobina { get; set; }
|
||||
public string NroBobina { get; set; } = string.Empty;
|
||||
public int Peso { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
// Backend/GestionIntegral.Api/Models/Dtos/Impresion/CreateStockBobinaLoteDto.cs
|
||||
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
namespace GestionIntegral.Api.Dtos.Impresion
|
||||
{
|
||||
public class CreateStockBobinaLoteDto
|
||||
{
|
||||
[Required]
|
||||
public int IdPlanta { get; set; }
|
||||
|
||||
[Required]
|
||||
[StringLength(15)]
|
||||
public string Remito { get; set; } = string.Empty;
|
||||
|
||||
[Required]
|
||||
public DateTime FechaRemito { get; set; }
|
||||
|
||||
[Required]
|
||||
[MinLength(1, ErrorMessage = "Debe ingresar al menos una bobina.")]
|
||||
public List<BobinaLoteDetalleDto> Bobinas { get; set; } = new();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
// Backend/GestionIntegral.Api/Models/Dtos/Impresion/UpdateFechaRemitoLoteDto.cs
|
||||
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
namespace GestionIntegral.Api.Dtos.Impresion
|
||||
{
|
||||
public class UpdateFechaRemitoLoteDto
|
||||
{
|
||||
[Required]
|
||||
public int IdPlanta { get; set; }
|
||||
|
||||
[Required]
|
||||
public required string Remito { get; set; }
|
||||
|
||||
[Required]
|
||||
public DateTime FechaRemitoActual { get; set; } // Para seguridad, nos aseguramos de cambiar el lote correcto
|
||||
|
||||
[Required]
|
||||
public DateTime NuevaFechaRemito { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -187,7 +187,7 @@ builder.Services.AddCors(options =>
|
||||
policy =>
|
||||
{
|
||||
policy.WithOrigins(
|
||||
"http://localhost:5175", // Para desarrollo local
|
||||
"http://localhost:5173", // Para desarrollo local
|
||||
"https://gestion.eldiaservicios.com" // Para producción
|
||||
)
|
||||
.AllowAnyHeader()
|
||||
|
||||
@@ -21,5 +21,8 @@ namespace GestionIntegral.Api.Services.Impresion
|
||||
DateTime? fechaDesde, DateTime? fechaHasta,
|
||||
int? idUsuarioModifico, string? tipoModificacion,
|
||||
int? idBobinaAfectada, int? idTipoBobinaFiltro, int? idPlantaFiltro, int? idEstadoBobinaFiltro);
|
||||
Task<IEnumerable<StockBobinaDto>> VerificarRemitoExistenteAsync(int idPlanta, string remito, DateTime? fechaRemito);
|
||||
Task<(bool Exito, string? Error)> IngresarBobinaLoteAsync(CreateStockBobinaLoteDto loteDto, int idUsuario);
|
||||
Task<(bool Exito, string? Error)> ActualizarFechaRemitoLoteAsync(UpdateFechaRemitoLoteDto dto, int idUsuario);
|
||||
}
|
||||
}
|
||||
@@ -166,16 +166,16 @@ namespace GestionIntegral.Api.Services.Impresion
|
||||
}
|
||||
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.");
|
||||
//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;
|
||||
//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");
|
||||
@@ -383,5 +383,151 @@ namespace GestionIntegral.Api.Services.Impresion
|
||||
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}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user