Ya perdí el hilo de los cambios pero ahi van.
This commit is contained in:
@@ -0,0 +1,171 @@
|
||||
using GestionIntegral.Api.Data;
|
||||
using GestionIntegral.Api.Data.Repositories.Distribucion;
|
||||
using GestionIntegral.Api.Dtos.Distribucion;
|
||||
using GestionIntegral.Api.Models.Distribucion;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Data;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace GestionIntegral.Api.Services.Distribucion
|
||||
{
|
||||
public class ControlDevolucionesService : IControlDevolucionesService
|
||||
{
|
||||
private readonly IControlDevolucionesRepository _controlDevRepo;
|
||||
private readonly IEmpresaRepository _empresaRepository; // Para validar IdEmpresa y obtener nombre
|
||||
private readonly DbConnectionFactory _connectionFactory;
|
||||
private readonly ILogger<ControlDevolucionesService> _logger;
|
||||
|
||||
public ControlDevolucionesService(
|
||||
IControlDevolucionesRepository controlDevRepo,
|
||||
IEmpresaRepository empresaRepository,
|
||||
DbConnectionFactory connectionFactory,
|
||||
ILogger<ControlDevolucionesService> logger)
|
||||
{
|
||||
_controlDevRepo = controlDevRepo;
|
||||
_empresaRepository = empresaRepository;
|
||||
_connectionFactory = connectionFactory;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
private async Task<ControlDevolucionesDto> MapToDto(ControlDevoluciones control)
|
||||
{
|
||||
if (control == null) return null!;
|
||||
var empresa = await _empresaRepository.GetByIdAsync(control.IdEmpresa);
|
||||
return new ControlDevolucionesDto
|
||||
{
|
||||
IdControl = control.IdControl,
|
||||
IdEmpresa = control.IdEmpresa,
|
||||
NombreEmpresa = empresa?.Nombre ?? "Empresa Desconocida",
|
||||
Fecha = control.Fecha.ToString("yyyy-MM-dd"),
|
||||
Entrada = control.Entrada,
|
||||
Sobrantes = control.Sobrantes,
|
||||
Detalle = control.Detalle,
|
||||
SinCargo = control.SinCargo
|
||||
};
|
||||
}
|
||||
|
||||
public async Task<IEnumerable<ControlDevolucionesDto>> ObtenerTodosAsync(DateTime? fechaDesde, DateTime? fechaHasta, int? idEmpresa)
|
||||
{
|
||||
var controles = await _controlDevRepo.GetAllAsync(fechaDesde, fechaHasta, idEmpresa);
|
||||
var dtos = new List<ControlDevolucionesDto>();
|
||||
foreach (var control in controles)
|
||||
{
|
||||
dtos.Add(await MapToDto(control));
|
||||
}
|
||||
return dtos;
|
||||
}
|
||||
|
||||
public async Task<ControlDevolucionesDto?> ObtenerPorIdAsync(int idControl)
|
||||
{
|
||||
var control = await _controlDevRepo.GetByIdAsync(idControl);
|
||||
return control == null ? null : await MapToDto(control);
|
||||
}
|
||||
|
||||
public async Task<(ControlDevolucionesDto? Control, string? Error)> CrearAsync(CreateControlDevolucionesDto createDto, int idUsuario)
|
||||
{
|
||||
var empresa = await _empresaRepository.GetByIdAsync(createDto.IdEmpresa);
|
||||
if (empresa == null)
|
||||
return (null, "La empresa especificada no existe.");
|
||||
|
||||
// Validar que no exista ya un control para esa empresa y fecha
|
||||
if (await _controlDevRepo.GetByEmpresaAndFechaAsync(createDto.IdEmpresa, createDto.Fecha.Date) != null)
|
||||
{
|
||||
return (null, $"Ya existe un control de devoluciones para la empresa '{empresa.Nombre}' en la fecha {createDto.Fecha:dd/MM/yyyy}.");
|
||||
}
|
||||
|
||||
var nuevoControl = new ControlDevoluciones
|
||||
{
|
||||
IdEmpresa = createDto.IdEmpresa,
|
||||
Fecha = createDto.Fecha.Date,
|
||||
Entrada = createDto.Entrada,
|
||||
Sobrantes = createDto.Sobrantes,
|
||||
Detalle = createDto.Detalle,
|
||||
SinCargo = createDto.SinCargo
|
||||
};
|
||||
|
||||
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 controlCreado = await _controlDevRepo.CreateAsync(nuevoControl, idUsuario, transaction);
|
||||
if (controlCreado == null) throw new DataException("Error al registrar el control de devoluciones.");
|
||||
|
||||
transaction.Commit();
|
||||
_logger.LogInformation("ControlDevoluciones ID {Id} creado por Usuario ID {UserId}.", controlCreado.IdControl, idUsuario);
|
||||
return (await MapToDto(controlCreado), null);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
try { transaction.Rollback(); } catch { }
|
||||
_logger.LogError(ex, "Error CrearAsync ControlDevoluciones para Empresa ID {IdEmpresa}, Fecha {Fecha}", createDto.IdEmpresa, createDto.Fecha);
|
||||
return (null, $"Error interno: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<(bool Exito, string? Error)> ActualizarAsync(int idControl, UpdateControlDevolucionesDto 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 controlExistente = await _controlDevRepo.GetByIdAsync(idControl); // Obtener dentro de TX
|
||||
if (controlExistente == null) return (false, "Control de devoluciones no encontrado.");
|
||||
|
||||
// IdEmpresa y Fecha no se modifican
|
||||
controlExistente.Entrada = updateDto.Entrada;
|
||||
controlExistente.Sobrantes = updateDto.Sobrantes;
|
||||
controlExistente.Detalle = updateDto.Detalle;
|
||||
controlExistente.SinCargo = updateDto.SinCargo;
|
||||
|
||||
var actualizado = await _controlDevRepo.UpdateAsync(controlExistente, idUsuario, transaction);
|
||||
if (!actualizado) throw new DataException("Error al actualizar el control de devoluciones.");
|
||||
|
||||
transaction.Commit();
|
||||
_logger.LogInformation("ControlDevoluciones ID {Id} actualizado por Usuario ID {UserId}.", idControl, idUsuario);
|
||||
return (true, null);
|
||||
}
|
||||
catch (KeyNotFoundException) { try { transaction.Rollback(); } catch { } return (false, "Registro no encontrado."); }
|
||||
catch (Exception ex)
|
||||
{
|
||||
try { transaction.Rollback(); } catch { }
|
||||
_logger.LogError(ex, "Error ActualizarAsync ControlDevoluciones ID: {Id}", idControl);
|
||||
return (false, $"Error interno: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<(bool Exito, string? Error)> EliminarAsync(int idControl, 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 controlExistente = await _controlDevRepo.GetByIdAsync(idControl); // Obtener dentro de TX
|
||||
if (controlExistente == null) return (false, "Control de devoluciones no encontrado.");
|
||||
|
||||
// Aquí no hay dependencias directas que afecten saldos, es un registro informativo.
|
||||
// Se podría verificar si está "en uso" si alguna lógica de reporte o cierre depende de él,
|
||||
// pero por ahora, la eliminación es directa.
|
||||
|
||||
var eliminado = await _controlDevRepo.DeleteAsync(idControl, idUsuario, transaction);
|
||||
if (!eliminado) throw new DataException("Error al eliminar el control de devoluciones.");
|
||||
|
||||
transaction.Commit();
|
||||
_logger.LogInformation("ControlDevoluciones ID {Id} eliminado por Usuario ID {UserId}.", idControl, idUsuario);
|
||||
return (true, null);
|
||||
}
|
||||
catch (KeyNotFoundException) { try { transaction.Rollback(); } catch { } return (false, "Registro no encontrado."); }
|
||||
catch (Exception ex)
|
||||
{
|
||||
try { transaction.Rollback(); } catch { }
|
||||
_logger.LogError(ex, "Error EliminarAsync ControlDevoluciones ID: {Id}", idControl);
|
||||
return (false, $"Error interno: {ex.Message}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,481 @@
|
||||
using GestionIntegral.Api.Data;
|
||||
using GestionIntegral.Api.Data.Repositories.Distribucion;
|
||||
using GestionIntegral.Api.Data.Repositories.Usuarios;
|
||||
using GestionIntegral.Api.Dtos.Distribucion;
|
||||
using GestionIntegral.Api.Models.Distribucion;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Data;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using System.Security.Claims;
|
||||
|
||||
namespace GestionIntegral.Api.Services.Distribucion
|
||||
{
|
||||
public class EntradaSalidaCanillaService : IEntradaSalidaCanillaService
|
||||
{
|
||||
private readonly IEntradaSalidaCanillaRepository _esCanillaRepository;
|
||||
private readonly IPublicacionRepository _publicacionRepository;
|
||||
private readonly ICanillaRepository _canillaRepository;
|
||||
private readonly IPrecioRepository _precioRepository;
|
||||
private readonly IRecargoZonaRepository _recargoZonaRepository;
|
||||
private readonly IPorcMonCanillaRepository _porcMonCanillaRepository;
|
||||
private readonly IUsuarioRepository _usuarioRepository;
|
||||
private readonly DbConnectionFactory _connectionFactory;
|
||||
private readonly ILogger<EntradaSalidaCanillaService> _logger;
|
||||
|
||||
public EntradaSalidaCanillaService(
|
||||
IEntradaSalidaCanillaRepository esCanillaRepository,
|
||||
IPublicacionRepository publicacionRepository,
|
||||
ICanillaRepository canillaRepository,
|
||||
IPrecioRepository precioRepository,
|
||||
IRecargoZonaRepository recargoZonaRepository,
|
||||
IPorcMonCanillaRepository porcMonCanillaRepository,
|
||||
IUsuarioRepository usuarioRepository,
|
||||
DbConnectionFactory connectionFactory,
|
||||
ILogger<EntradaSalidaCanillaService> logger)
|
||||
{
|
||||
_esCanillaRepository = esCanillaRepository;
|
||||
_publicacionRepository = publicacionRepository;
|
||||
_canillaRepository = canillaRepository;
|
||||
_precioRepository = precioRepository;
|
||||
_recargoZonaRepository = recargoZonaRepository;
|
||||
_porcMonCanillaRepository = porcMonCanillaRepository;
|
||||
_usuarioRepository = usuarioRepository;
|
||||
_connectionFactory = connectionFactory;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
private async Task<EntradaSalidaCanillaDto?> MapToDto(EntradaSalidaCanilla? es)
|
||||
{
|
||||
if (es == null) return null;
|
||||
|
||||
var publicacionDataResult = await _publicacionRepository.GetByIdAsync(es.IdPublicacion);
|
||||
var canillaDataResult = await _canillaRepository.GetByIdAsync(es.IdCanilla); // Devuelve tupla
|
||||
|
||||
Publicacion? publicacionEntity = publicacionDataResult.Publicacion; // Componente nullable de la tupla
|
||||
Canilla? canillaEntity = canillaDataResult.Canilla; // Componente nullable de la tupla
|
||||
|
||||
var usuarioLiq = es.UserLiq.HasValue ? await _usuarioRepository.GetByIdAsync(es.UserLiq.Value) : null;
|
||||
|
||||
decimal montoARendir = await CalcularMontoARendir(es, canillaEntity, publicacionEntity);
|
||||
|
||||
return new EntradaSalidaCanillaDto
|
||||
{
|
||||
IdParte = es.IdParte,
|
||||
IdPublicacion = es.IdPublicacion,
|
||||
NombrePublicacion = publicacionEntity?.Nombre ?? "Pub. Desconocida",
|
||||
IdCanilla = es.IdCanilla,
|
||||
NomApeCanilla = canillaEntity?.NomApe ?? "Can. Desconocido",
|
||||
CanillaEsAccionista = canillaEntity?.Accionista ?? false,
|
||||
Fecha = es.Fecha.ToString("yyyy-MM-dd"),
|
||||
CantSalida = es.CantSalida,
|
||||
CantEntrada = es.CantEntrada,
|
||||
Vendidos = es.CantSalida - es.CantEntrada,
|
||||
Observacion = es.Observacion,
|
||||
Liquidado = es.Liquidado,
|
||||
FechaLiquidado = es.FechaLiquidado?.ToString("yyyy-MM-dd"),
|
||||
UserLiq = es.UserLiq,
|
||||
NombreUserLiq = usuarioLiq != null ? $"{usuarioLiq.Nombre} {usuarioLiq.Apellido}" : null,
|
||||
MontoARendir = montoARendir,
|
||||
PrecioUnitarioAplicado = (await _precioRepository.GetByIdAsync(es.IdPrecio))?.Lunes ?? 0,
|
||||
RecargoAplicado = es.IdRecargo > 0 ? (await _recargoZonaRepository.GetByIdAsync(es.IdRecargo))?.Valor ?? 0 : 0,
|
||||
PorcentajeOMontoCanillaAplicado = es.IdPorcMon > 0 ? (await _porcMonCanillaRepository.GetByIdAsync(es.IdPorcMon))?.PorcMon ?? 0 : 0,
|
||||
EsPorcentajeCanilla = es.IdPorcMon > 0 ? (await _porcMonCanillaRepository.GetByIdAsync(es.IdPorcMon))?.EsPorcentaje ?? false : false
|
||||
};
|
||||
}
|
||||
|
||||
private async Task<decimal> CalcularMontoARendir(EntradaSalidaCanilla es, Canilla? canilla, Publicacion? publicacion) // Acepta nullable Canilla y Publicacion
|
||||
{
|
||||
if (es.CantSalida - es.CantEntrada <= 0) return 0;
|
||||
|
||||
var precioConfig = await _precioRepository.GetByIdAsync(es.IdPrecio);
|
||||
if (precioConfig == null)
|
||||
{
|
||||
_logger.LogError("Configuración de precio ID {IdPrecio} no encontrada para movimiento ID {IdParte}.", es.IdPrecio, es.IdParte);
|
||||
throw new InvalidOperationException($"Configuración de precio ID {es.IdPrecio} no encontrada para el movimiento.");
|
||||
}
|
||||
|
||||
decimal precioDia = 0;
|
||||
DayOfWeek diaSemana = es.Fecha.DayOfWeek;
|
||||
switch (diaSemana)
|
||||
{
|
||||
case DayOfWeek.Monday: precioDia = precioConfig.Lunes ?? 0; break;
|
||||
case DayOfWeek.Tuesday: precioDia = precioConfig.Martes ?? 0; break;
|
||||
case DayOfWeek.Wednesday: precioDia = precioConfig.Miercoles ?? 0; break;
|
||||
case DayOfWeek.Thursday: precioDia = precioConfig.Jueves ?? 0; break;
|
||||
case DayOfWeek.Friday: precioDia = precioConfig.Viernes ?? 0; break;
|
||||
case DayOfWeek.Saturday: precioDia = precioConfig.Sabado ?? 0; break;
|
||||
case DayOfWeek.Sunday: precioDia = precioConfig.Domingo ?? 0; break;
|
||||
}
|
||||
|
||||
decimal valorRecargo = 0;
|
||||
if (es.IdRecargo > 0)
|
||||
{
|
||||
var recargoConfig = await _recargoZonaRepository.GetByIdAsync(es.IdRecargo);
|
||||
if (recargoConfig != null) valorRecargo = recargoConfig.Valor;
|
||||
}
|
||||
|
||||
decimal precioFinalUnitario = precioDia + valorRecargo;
|
||||
int cantidadVendida = es.CantSalida - es.CantEntrada;
|
||||
|
||||
if (canilla != null && canilla.Accionista && es.IdPorcMon > 0) // Check null para canilla
|
||||
{
|
||||
var porcMonConfig = await _porcMonCanillaRepository.GetByIdAsync(es.IdPorcMon);
|
||||
if (porcMonConfig != null)
|
||||
{
|
||||
if (porcMonConfig.EsPorcentaje)
|
||||
{
|
||||
return Math.Round((precioFinalUnitario * cantidadVendida * porcMonConfig.PorcMon) / 100, 2);
|
||||
}
|
||||
else
|
||||
{
|
||||
return Math.Round(cantidadVendida * porcMonConfig.PorcMon, 2);
|
||||
}
|
||||
}
|
||||
}
|
||||
return Math.Round(precioFinalUnitario * cantidadVendida, 2);
|
||||
}
|
||||
|
||||
public async Task<IEnumerable<EntradaSalidaCanillaDto>> ObtenerTodosAsync(
|
||||
DateTime? fechaDesde, DateTime? fechaHasta,
|
||||
int? idPublicacion, int? idCanilla, bool? liquidados, bool? incluirNoLiquidados)
|
||||
{
|
||||
bool? filtroLiquidadoFinal = null;
|
||||
if (liquidados.HasValue)
|
||||
{
|
||||
filtroLiquidadoFinal = liquidados.Value;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (incluirNoLiquidados.HasValue && !incluirNoLiquidados.Value)
|
||||
{
|
||||
filtroLiquidadoFinal = true;
|
||||
}
|
||||
}
|
||||
|
||||
var movimientos = await _esCanillaRepository.GetAllAsync(fechaDesde, fechaHasta, idPublicacion, idCanilla, filtroLiquidadoFinal);
|
||||
var dtos = new List<EntradaSalidaCanillaDto>();
|
||||
foreach (var mov in movimientos)
|
||||
{
|
||||
var dto = await MapToDto(mov);
|
||||
if (dto != null) dtos.Add(dto);
|
||||
}
|
||||
return dtos;
|
||||
}
|
||||
|
||||
public async Task<EntradaSalidaCanillaDto?> ObtenerPorIdAsync(int idParte)
|
||||
{
|
||||
var movimiento = await _esCanillaRepository.GetByIdAsync(idParte);
|
||||
return await MapToDto(movimiento);
|
||||
}
|
||||
|
||||
public async Task<(bool Exito, string? Error)> ActualizarMovimientoAsync(int idParte, UpdateEntradaSalidaCanillaDto updateDto, int idUsuario)
|
||||
{
|
||||
using var connection = _connectionFactory.CreateConnection();
|
||||
if (connection is System.Data.Common.DbConnection dbConnOpen && connection.State == ConnectionState.Closed) await dbConnOpen.OpenAsync(); else if (connection.State == ConnectionState.Closed) connection.Open();
|
||||
using var transaction = connection.BeginTransaction();
|
||||
try
|
||||
{
|
||||
var esExistente = await _esCanillaRepository.GetByIdAsync(idParte);
|
||||
if (esExistente == null) return (false, "Movimiento no encontrado.");
|
||||
if (esExistente.Liquidado) return (false, "No se puede modificar un movimiento ya liquidado.");
|
||||
|
||||
esExistente.CantSalida = updateDto.CantSalida;
|
||||
esExistente.CantEntrada = updateDto.CantEntrada;
|
||||
esExistente.Observacion = updateDto.Observacion;
|
||||
|
||||
var actualizado = await _esCanillaRepository.UpdateAsync(esExistente, idUsuario, transaction);
|
||||
if (!actualizado) throw new DataException("Error al actualizar el movimiento.");
|
||||
|
||||
transaction.Commit();
|
||||
_logger.LogInformation("Movimiento Canillita ID {Id} actualizado por Usuario ID {UserId}.", idParte, idUsuario);
|
||||
return (true, null);
|
||||
}
|
||||
catch (KeyNotFoundException) { if (transaction.Connection != null) try { transaction.Rollback(); } catch { } return (false, "Movimiento no encontrado."); }
|
||||
catch (Exception ex)
|
||||
{
|
||||
if (transaction.Connection != null) try { transaction.Rollback(); } catch { }
|
||||
_logger.LogError(ex, "Error ActualizarMovimientoAsync Canillita ID: {Id}", idParte);
|
||||
return (false, $"Error interno: {ex.Message}");
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (connection?.State == ConnectionState.Open)
|
||||
{
|
||||
if (connection is System.Data.Common.DbConnection dbConnClose) await dbConnClose.CloseAsync(); else connection.Close();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<(bool Exito, string? Error)> EliminarMovimientoAsync(int idParte, int idUsuario, ClaimsPrincipal userPrincipal)
|
||||
{
|
||||
// Helper interno para verificar permisos desde el ClaimsPrincipal proporcionado
|
||||
bool TienePermisoEspecifico(string codAcc) => userPrincipal.IsInRole("SuperAdmin") || userPrincipal.HasClaim(c => c.Type == "permission" && c.Value == codAcc);
|
||||
|
||||
using var connection = _connectionFactory.CreateConnection();
|
||||
IDbTransaction? transaction = null;
|
||||
|
||||
try
|
||||
{
|
||||
if (connection is System.Data.Common.DbConnection dbConnOpen && connection.State == ConnectionState.Closed) await dbConnOpen.OpenAsync();
|
||||
else if (connection.State == ConnectionState.Closed) connection.Open();
|
||||
|
||||
transaction = connection.BeginTransaction();
|
||||
|
||||
var esExistente = await _esCanillaRepository.GetByIdAsync(idParte);
|
||||
if (esExistente == null)
|
||||
{
|
||||
if (transaction?.Connection != null) transaction.Rollback();
|
||||
return (false, "Movimiento no encontrado.");
|
||||
}
|
||||
|
||||
if (esExistente.Liquidado)
|
||||
{
|
||||
// Permiso MC006 es para "Eliminar Movimientos de Canillita Liquidados"
|
||||
if (!TienePermisoEspecifico("MC006"))
|
||||
{
|
||||
if (transaction?.Connection != null) transaction.Rollback();
|
||||
return (false, "No tiene permiso para eliminar movimientos ya liquidados. Se requiere permiso especial (MC006) o ser SuperAdmin.");
|
||||
}
|
||||
_logger.LogWarning("Usuario ID {IdUsuario} está eliminando un movimiento LIQUIDADO (IDParte: {IdParte}). Permiso MC006 verificado.", idUsuario, idParte);
|
||||
}
|
||||
// Si no está liquidado, el permiso MC004 ya fue verificado en el controlador.
|
||||
|
||||
var eliminado = await _esCanillaRepository.DeleteAsync(idParte, idUsuario, transaction);
|
||||
if (!eliminado)
|
||||
{
|
||||
// No es necesario hacer rollback aquí si DeleteAsync lanza una excepción,
|
||||
// ya que el bloque catch lo manejará. Si DeleteAsync devuelve false sin lanzar,
|
||||
// entonces sí sería necesario un rollback.
|
||||
if (transaction?.Connection != null) transaction.Rollback();
|
||||
throw new DataException("Error al eliminar el movimiento desde el repositorio.");
|
||||
}
|
||||
|
||||
if (transaction?.Connection != null) transaction.Commit();
|
||||
_logger.LogInformation("Movimiento Canillita ID {IdParte} eliminado por Usuario ID {IdUsuario}.", idParte, idUsuario);
|
||||
return (true, null);
|
||||
}
|
||||
catch (KeyNotFoundException)
|
||||
{
|
||||
if (transaction?.Connection != null) try { transaction.Rollback(); } catch (Exception exR) { _logger.LogError(exR, "Rollback fallido KeyNotFoundException."); }
|
||||
return (false, "Movimiento no encontrado.");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
if (transaction?.Connection != null) { try { transaction.Rollback(); } catch (Exception exRollback) { _logger.LogError(exRollback, "Error durante rollback de transacción."); } }
|
||||
_logger.LogError(ex, "Error EliminarMovimientoAsync Canillita ID: {IdParte}", idParte);
|
||||
return (false, $"Error interno: {ex.Message}");
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (transaction != null) transaction.Dispose();
|
||||
if (connection?.State == ConnectionState.Open)
|
||||
{
|
||||
if (connection is System.Data.Common.DbConnection dbConnClose) await dbConnClose.CloseAsync(); else connection.Close();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<(bool Exito, string? Error)> LiquidarMovimientosAsync(LiquidarMovimientosCanillaRequestDto liquidarDto, int idUsuarioLiquidador)
|
||||
{
|
||||
if (liquidarDto.IdsPartesALiquidar == null || !liquidarDto.IdsPartesALiquidar.Any())
|
||||
return (false, "No se especificaron movimientos para liquidar.");
|
||||
|
||||
using var connection = _connectionFactory.CreateConnection();
|
||||
if (connection is System.Data.Common.DbConnection dbConnOpen && connection.State == ConnectionState.Closed) await dbConnOpen.OpenAsync(); else if (connection.State == ConnectionState.Closed) connection.Open();
|
||||
using var transaction = connection.BeginTransaction();
|
||||
try
|
||||
{
|
||||
bool liquidacionExitosa = await _esCanillaRepository.LiquidarAsync(liquidarDto.IdsPartesALiquidar, liquidarDto.FechaLiquidacion.Date, idUsuarioLiquidador, transaction);
|
||||
|
||||
if (!liquidacionExitosa)
|
||||
{
|
||||
_logger.LogWarning("Liquidación de movimientos de canillita pudo no haber afectado a todos los IDs solicitados. IDs: {Ids}", string.Join(",", liquidarDto.IdsPartesALiquidar));
|
||||
}
|
||||
|
||||
if (transaction.Connection != null) transaction.Commit();
|
||||
_logger.LogInformation("Movimientos de Canillita liquidados. Ids: {Ids} por Usuario ID {UserId}.", string.Join(",", liquidarDto.IdsPartesALiquidar), idUsuarioLiquidador);
|
||||
return (true, null);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
if (transaction.Connection != null) { try { transaction.Rollback(); } catch (Exception exRollback) { _logger.LogError(exRollback, "Error durante rollback de transacción."); } }
|
||||
_logger.LogError(ex, "Error al liquidar movimientos de canillita.");
|
||||
return (false, $"Error interno al liquidar movimientos: {ex.Message}");
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (connection?.State == ConnectionState.Open)
|
||||
{
|
||||
if (connection is System.Data.Common.DbConnection dbConnClose) await dbConnClose.CloseAsync(); else connection.Close();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<(IEnumerable<EntradaSalidaCanillaDto>? MovimientosCreados, string? Error)> CrearMovimientosEnLoteAsync(CreateBulkEntradaSalidaCanillaDto createBulkDto, int idUsuario)
|
||||
{
|
||||
var canillaDataResult = await _canillaRepository.GetByIdAsync(createBulkDto.IdCanilla);
|
||||
Canilla? canillaActual = canillaDataResult.Canilla;
|
||||
if (canillaActual == null) return (null, "Canillita no válido.");
|
||||
if (canillaActual.Baja) return (null, "El canillita está dado de baja.");
|
||||
|
||||
List<EntradaSalidaCanilla> movimientosCreadosEntidades = new List<EntradaSalidaCanilla>();
|
||||
List<string> erroresItems = new List<string>();
|
||||
|
||||
using var connection = _connectionFactory.CreateConnection();
|
||||
if (connection is System.Data.Common.DbConnection dbConnOpen && connection.State == ConnectionState.Closed) await dbConnOpen.OpenAsync(); else if (connection.State == ConnectionState.Closed) connection.Open();
|
||||
using var transaction = connection.BeginTransaction();
|
||||
|
||||
try
|
||||
{
|
||||
foreach (var itemDto in createBulkDto.Items.Where(i => i.IdPublicacion > 0))
|
||||
{
|
||||
if (await _esCanillaRepository.ExistsByPublicacionCanillaFechaAsync(itemDto.IdPublicacion, createBulkDto.IdCanilla, createBulkDto.Fecha.Date, transaction: transaction))
|
||||
{
|
||||
var pubInfo = await _publicacionRepository.GetByIdSimpleAsync(itemDto.IdPublicacion);
|
||||
erroresItems.Add($"Ya existe un registro para la publicación '{pubInfo?.Nombre ?? itemDto.IdPublicacion.ToString()}' para {canillaActual.NomApe} en la fecha {createBulkDto.Fecha:dd/MM/yyyy}.");
|
||||
}
|
||||
}
|
||||
if (erroresItems.Any())
|
||||
{
|
||||
if (transaction.Connection != null) transaction.Rollback();
|
||||
return (null, string.Join(" ", erroresItems));
|
||||
}
|
||||
|
||||
foreach (var itemDto in createBulkDto.Items)
|
||||
{
|
||||
if (itemDto.IdPublicacion == 0 && itemDto.CantSalida == 0 && itemDto.CantEntrada == 0 && string.IsNullOrWhiteSpace(itemDto.Observacion)) continue;
|
||||
if (itemDto.IdPublicacion == 0)
|
||||
{
|
||||
if (itemDto.CantSalida > 0 || itemDto.CantEntrada > 0 || !string.IsNullOrWhiteSpace(itemDto.Observacion))
|
||||
{
|
||||
erroresItems.Add($"Falta seleccionar la publicación para una de las líneas con cantidades/observación.");
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
var publicacionItem = await _publicacionRepository.GetByIdSimpleAsync(itemDto.IdPublicacion);
|
||||
bool noEsValidaONoHabilitada = false;
|
||||
if (publicacionItem == null)
|
||||
{
|
||||
noEsValidaONoHabilitada = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Si Habilitada es bool? y NULL significa que toma el DEFAULT 1 (true) de la BD
|
||||
// entonces solo consideramos error si es explícitamente false.
|
||||
if (publicacionItem.Habilitada.HasValue && publicacionItem.Habilitada.Value == false)
|
||||
{
|
||||
noEsValidaONoHabilitada = true;
|
||||
}
|
||||
// Si publicacionItem.Habilitada es null o true, noEsValidaONoHabilitada permanece false.
|
||||
}
|
||||
|
||||
if (noEsValidaONoHabilitada)
|
||||
{
|
||||
erroresItems.Add($"Publicación ID {itemDto.IdPublicacion} no es válida o no está habilitada.");
|
||||
continue;
|
||||
}
|
||||
|
||||
var precioActivo = await _precioRepository.GetActiveByPublicacionAndDateAsync(itemDto.IdPublicacion, createBulkDto.Fecha.Date, transaction);
|
||||
if (precioActivo == null)
|
||||
{
|
||||
string nombrePubParaError = publicacionItem?.Nombre ?? $"ID {itemDto.IdPublicacion}";
|
||||
erroresItems.Add($"No hay precio definido para '{nombrePubParaError}' en {createBulkDto.Fecha:dd/MM/yyyy}.");
|
||||
continue;
|
||||
}
|
||||
|
||||
RecargoZona? recargoActivo = null;
|
||||
// Aquí usamos canillaActual! porque ya verificamos que no es null al inicio del método.
|
||||
if (canillaActual!.IdZona > 0)
|
||||
{
|
||||
recargoActivo = await _recargoZonaRepository.GetActiveByPublicacionZonaAndDateAsync(itemDto.IdPublicacion, canillaActual.IdZona, createBulkDto.Fecha.Date, transaction);
|
||||
}
|
||||
|
||||
PorcMonCanilla? porcMonActivo = null;
|
||||
// Aquí usamos canillaActual! porque ya verificamos que no es null al inicio del método.
|
||||
if (canillaActual!.Accionista)
|
||||
{
|
||||
porcMonActivo = await _porcMonCanillaRepository.GetActiveByPublicacionCanillaAndDateAsync(itemDto.IdPublicacion, createBulkDto.IdCanilla, createBulkDto.Fecha.Date, transaction);
|
||||
if (porcMonActivo == null)
|
||||
{
|
||||
// Dentro de este bloque, canillaActual.NomApe es seguro porque Accionista era true.
|
||||
string nombreCanParaError = canillaActual.NomApe;
|
||||
string nombrePubParaError = publicacionItem?.Nombre ?? $"Publicación ID {itemDto.IdPublicacion}";
|
||||
erroresItems.Add($"'{nombreCanParaError}' es accionista pero no tiene %/monto para '{nombrePubParaError}' en {createBulkDto.Fecha:dd/MM/yyyy}.");
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
var nuevoES = new EntradaSalidaCanilla
|
||||
{
|
||||
IdPublicacion = itemDto.IdPublicacion,
|
||||
IdCanilla = createBulkDto.IdCanilla,
|
||||
Fecha = createBulkDto.Fecha.Date,
|
||||
CantSalida = itemDto.CantSalida,
|
||||
CantEntrada = itemDto.CantEntrada,
|
||||
Observacion = itemDto.Observacion,
|
||||
IdPrecio = precioActivo.IdPrecio,
|
||||
IdRecargo = recargoActivo?.IdRecargo ?? 0,
|
||||
IdPorcMon = porcMonActivo?.IdPorcMon ?? 0,
|
||||
Liquidado = false,
|
||||
FechaLiquidado = null,
|
||||
UserLiq = null
|
||||
};
|
||||
|
||||
var esCreada = await _esCanillaRepository.CreateAsync(nuevoES, idUsuario, transaction);
|
||||
if (esCreada == null) throw new DataException($"Error al registrar movimiento para Publicación ID {itemDto.IdPublicacion}.");
|
||||
movimientosCreadosEntidades.Add(esCreada);
|
||||
}
|
||||
|
||||
if (erroresItems.Any())
|
||||
{
|
||||
if (transaction.Connection != null) transaction.Rollback();
|
||||
return (null, string.Join(" ", erroresItems));
|
||||
}
|
||||
|
||||
// CORRECCIÓN PARA CS0019 (línea 394 original):
|
||||
bool tieneItemsSignificativos = false;
|
||||
if (createBulkDto.Items != null) // Checkear si Items es null antes de llamar a Any()
|
||||
{
|
||||
tieneItemsSignificativos = createBulkDto.Items.Any(i => i.IdPublicacion > 0 &&
|
||||
(i.CantSalida > 0 || i.CantEntrada > 0 || !string.IsNullOrWhiteSpace(i.Observacion)));
|
||||
}
|
||||
|
||||
if (!movimientosCreadosEntidades.Any() && tieneItemsSignificativos)
|
||||
{
|
||||
if (transaction.Connection != null) transaction.Rollback();
|
||||
return (null, "No se pudo procesar ningún ítem válido con datos significativos.");
|
||||
}
|
||||
|
||||
if (transaction.Connection != null) transaction.Commit();
|
||||
_logger.LogInformation("Lote de {Count} movimientos Canillita para Canilla ID {IdCanilla} en Fecha {Fecha} creados por Usuario ID {UserId}.",
|
||||
movimientosCreadosEntidades.Count, createBulkDto.IdCanilla, createBulkDto.Fecha.Date, idUsuario);
|
||||
|
||||
var dtosCreados = new List<EntradaSalidaCanillaDto>();
|
||||
foreach (var entidad in movimientosCreadosEntidades)
|
||||
{
|
||||
var dto = await MapToDto(entidad);
|
||||
if (dto != null) dtosCreados.Add(dto);
|
||||
}
|
||||
return (dtosCreados, null);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
if (transaction.Connection != null) { try { transaction.Rollback(); } catch (Exception exRollback) { _logger.LogError(exRollback, "Error durante rollback de transacción."); } }
|
||||
_logger.LogError(ex, "Error CrearMovimientosEnLoteAsync para Canilla ID {IdCanilla}, Fecha {Fecha}", createBulkDto.IdCanilla, createBulkDto.Fecha);
|
||||
return (null, $"Error interno al procesar el lote: {ex.Message}");
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (connection?.State == ConnectionState.Open)
|
||||
{
|
||||
if (connection is System.Data.Common.DbConnection dbConnClose) await dbConnClose.CloseAsync(); else connection.Close();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -57,9 +57,29 @@ namespace GestionIntegral.Api.Services.Distribucion
|
||||
var publicacionData = await _publicacionRepository.GetByIdAsync(es.IdPublicacion);
|
||||
var distribuidorData = await _distribuidorRepository.GetByIdAsync(es.IdDistribuidor);
|
||||
|
||||
decimal montoCalculado = await CalcularMontoMovimiento(es.IdPublicacion, es.IdDistribuidor, es.Fecha, es.Cantidad, es.TipoMovimiento,
|
||||
es.IdPrecio, es.IdRecargo, es.IdPorcentaje, distribuidorData.Distribuidor?.IdZona);
|
||||
// Obtener el valor bruto del movimiento
|
||||
decimal valorBrutoMovimiento = await CalcularMontoMovimiento(
|
||||
es.IdPublicacion, es.IdDistribuidor, es.Fecha, es.Cantidad,
|
||||
es.TipoMovimiento, // Pasamos el tipo de movimiento original aquí
|
||||
es.IdPrecio, es.IdRecargo, es.IdPorcentaje, distribuidorData.Distribuidor?.IdZona
|
||||
);
|
||||
|
||||
// Ajustar para el DTO: si es "Entrada", el monto calculado es un crédito (negativo o positivo según convención)
|
||||
// Para consistencia con el ajuste de saldo, si es Entrada, el MontoCalculado para el DTO puede ser el valor
|
||||
// que se le "acredita" al distribuidor (o sea, el valor de la mercadería devuelta).
|
||||
// La lógica de +/- para el saldo ya está en Crear/Actualizar/Eliminar.
|
||||
// Aquí solo mostramos el valor del movimiento. Si es entrada, es el valor de lo devuelto.
|
||||
// Si es salida, es el valor de lo que se le factura.
|
||||
// El método CalcularMonto ya devuelve el monto que el distribuidor DEBE pagar por una SALIDA.
|
||||
// Para una ENTRADA (devolución), el valor de esa mercadería es el mismo, pero opera en sentido contrario al saldo.
|
||||
|
||||
decimal montoCalculadoParaDto = valorBrutoMovimiento;
|
||||
// Si queremos que el DTO muestre las entradas como un valor que "reduce la deuda",
|
||||
// podría ser positivo. Si queremos que refleje el impacto directo en la factura (salidas suman, entradas restan),
|
||||
// podríamos hacerlo negativo.
|
||||
// Por ahora, dejaremos que CalcularMontoMovimiento devuelva el valor de una "Salida",
|
||||
// y si es "Entrada", este mismo valor es el que se acredita.
|
||||
// La columna `MontoCalculado` en el DTO representará el valor de la transacción.
|
||||
|
||||
return new EntradaSalidaDistDto
|
||||
{
|
||||
@@ -75,17 +95,29 @@ namespace GestionIntegral.Api.Services.Distribucion
|
||||
Cantidad = es.Cantidad,
|
||||
Remito = es.Remito,
|
||||
Observacion = es.Observacion,
|
||||
MontoCalculado = montoCalculado
|
||||
MontoCalculado = montoCalculadoParaDto // Representa el valor de los N ejemplares
|
||||
};
|
||||
}
|
||||
|
||||
private async Task<decimal> CalcularMontoMovimiento(int idPublicacion, int idDistribuidor, DateTime fecha, int cantidad, string tipoMovimiento,
|
||||
int idPrecio, int idRecargo, int idPorcentaje, int? idZonaDistribuidor)
|
||||
{
|
||||
if (tipoMovimiento == "Entrada") return 0; // Las entradas (devoluciones) no generan "debe" inmediato, ajustan el saldo.
|
||||
// YA NO SE DEVUELVE 0 PARA ENTRADA AQUÍ
|
||||
// if (tipoMovimiento == "Entrada") return 0;
|
||||
|
||||
var precioConfig = await _precioRepository.GetByIdAsync(idPrecio);
|
||||
if (precioConfig == null) throw new InvalidOperationException("Configuración de precio no encontrada para el movimiento.");
|
||||
// Es crucial que idPrecio sea válido y se haya determinado correctamente antes de llamar aquí.
|
||||
// Si precioConfig es null, se lanzará una excepción abajo, lo cual está bien si es un estado inesperado.
|
||||
if (precioConfig == null)
|
||||
{
|
||||
_logger.LogError("Configuración de precio ID {IdPrecio} no encontrada al calcular monto para Pub {IdPublicacion}, Dist {IdDistribuidor}, Fecha {Fecha}", idPrecio, idPublicacion, idDistribuidor, fecha);
|
||||
// Dependiendo de la regla de negocio, podrías devolver 0 o lanzar una excepción.
|
||||
// Si un precio es OBLIGATORIO para cualquier movimiento, lanzar excepción es más apropiado.
|
||||
// Si puede haber movimientos sin precio (ej. cortesía que no se factura), entonces 0.
|
||||
// En este contexto, un precio es fundamental para el cálculo.
|
||||
throw new InvalidOperationException($"Configuración de precio ID {idPrecio} no encontrada. No se puede calcular el monto.");
|
||||
}
|
||||
|
||||
|
||||
decimal precioDia = 0;
|
||||
DayOfWeek diaSemana = fecha.DayOfWeek;
|
||||
@@ -101,9 +133,8 @@ namespace GestionIntegral.Api.Services.Distribucion
|
||||
}
|
||||
|
||||
decimal valorRecargo = 0;
|
||||
if (idRecargo > 0 && idZonaDistribuidor.HasValue) // El recargo se aplica por la zona del distribuidor
|
||||
if (idRecargo > 0 && idZonaDistribuidor.HasValue)
|
||||
{
|
||||
// Necesitamos encontrar el recargo activo para la publicación, la zona del distribuidor y la fecha
|
||||
var recargoConfig = await _recargoZonaRepository.GetActiveByPublicacionZonaAndDateAsync(idPublicacion, idZonaDistribuidor.Value, fecha);
|
||||
if (recargoConfig != null)
|
||||
{
|
||||
@@ -119,10 +150,13 @@ namespace GestionIntegral.Api.Services.Distribucion
|
||||
var porcConfig = await _porcPagoRepository.GetByIdAsync(idPorcentaje);
|
||||
if (porcConfig != null)
|
||||
{
|
||||
return (montoBase / 100) * porcConfig.Porcentaje;
|
||||
// El porcentaje de pago del distribuidor es lo que ÉL PAGA a la editorial.
|
||||
// Entonces, el monto es (precio_tapa_con_recargo * cantidad) * (porcentaje_pago_dist / 100)
|
||||
return (montoBase * porcConfig.Porcentaje) / 100;
|
||||
}
|
||||
}
|
||||
return montoBase; // Si no hay porcentaje, se factura el 100% del precio con recargo
|
||||
// Si no hay porcentaje de pago específico, se asume que el distribuidor paga el 100% del monto base.
|
||||
return montoBase;
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
using GestionIntegral.Api.Dtos.Distribucion;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace GestionIntegral.Api.Services.Distribucion
|
||||
{
|
||||
public interface IControlDevolucionesService
|
||||
{
|
||||
Task<IEnumerable<ControlDevolucionesDto>> ObtenerTodosAsync(DateTime? fechaDesde, DateTime? fechaHasta, int? idEmpresa);
|
||||
Task<ControlDevolucionesDto?> ObtenerPorIdAsync(int idControl);
|
||||
Task<(ControlDevolucionesDto? Control, string? Error)> CrearAsync(CreateControlDevolucionesDto createDto, int idUsuario);
|
||||
Task<(bool Exito, string? Error)> ActualizarAsync(int idControl, UpdateControlDevolucionesDto updateDto, int idUsuario);
|
||||
Task<(bool Exito, string? Error)> EliminarAsync(int idControl, int idUsuario);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
using GestionIntegral.Api.Dtos.Distribucion;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Security.Claims;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace GestionIntegral.Api.Services.Distribucion
|
||||
{
|
||||
public interface IEntradaSalidaCanillaService
|
||||
{
|
||||
Task<IEnumerable<EntradaSalidaCanillaDto>> ObtenerTodosAsync(
|
||||
DateTime? fechaDesde, DateTime? fechaHasta,
|
||||
int? idPublicacion, int? idCanilla, bool? liquidados, bool? incluirNoLiquidados);
|
||||
|
||||
Task<EntradaSalidaCanillaDto?> ObtenerPorIdAsync(int idParte);
|
||||
Task<(IEnumerable<EntradaSalidaCanillaDto>? MovimientosCreados, string? Error)> CrearMovimientosEnLoteAsync(CreateBulkEntradaSalidaCanillaDto createBulkDto, int idUsuario);
|
||||
Task<(bool Exito, string? Error)> ActualizarMovimientoAsync(int idParte, UpdateEntradaSalidaCanillaDto updateDto, int idUsuario);
|
||||
|
||||
Task<(bool Exito, string? Error)> EliminarMovimientoAsync(int idParte, int idUsuario, ClaimsPrincipal userPrincipal);
|
||||
|
||||
Task<(bool Exito, string? Error)> LiquidarMovimientosAsync(LiquidarMovimientosCanillaRequestDto liquidarDto, int idUsuarioLiquidador);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user