Compare commits
31 Commits
8c194b8441
...
feat/cierr
| Author | SHA1 | Date | |
|---|---|---|---|
| 24eaf18fd9 | |||
| 7e274ef114 | |||
| 5212e31a03 | |||
| 9201d7222b | |||
| fc27b4b43e | |||
| 35e8d803b9 | |||
| 8e1b8d2326 | |||
| bc19e184aa | |||
| 29109cff13 | |||
| 7f1fadfc84 | |||
| 74f07df960 | |||
| 6ceb1477ae | |||
| c049c1e544 | |||
| 8c7278ceae | |||
| e8215f8586 | |||
| bf7d7c22ef | |||
| 2c584e9383 | |||
| e123dae182 | |||
| c27dc2a0ba | |||
| 24b1c07342 | |||
| cb64bbc1f5 | |||
| 057310ca47 | |||
| e95c851e5b | |||
| 038faefd35 | |||
| da50c052f1 | |||
| 5781713b13 | |||
| 9f8d577265 | |||
| b594a48fde | |||
| 2e7d1e36be | |||
| dd2277fce2 | |||
| 9412556fa8 |
@@ -26,12 +26,6 @@ jobs:
|
|||||||
set -e
|
set -e
|
||||||
echo "--- INICIO DEL DESPLIEGUE OPTIMIZADO ---"
|
echo "--- INICIO DEL DESPLIEGUE OPTIMIZADO ---"
|
||||||
|
|
||||||
# --- Asegurar que el Stack de la Base de Datos esté corriendo ---
|
|
||||||
echo "Asegurando que el stack de la base de datos esté activo..."
|
|
||||||
cd /opt/shared-services/database
|
|
||||||
# El comando 'up -d' es idempotente. Si ya está corriendo, no hace nada.
|
|
||||||
docker compose up -d
|
|
||||||
|
|
||||||
# 1. Preparar entorno
|
# 1. Preparar entorno
|
||||||
TEMP_DIR=$(mktemp -d)
|
TEMP_DIR=$(mktemp -d)
|
||||||
REPO_OWNER="dmolinari"
|
REPO_OWNER="dmolinari"
|
||||||
|
|||||||
2
.gitignore
vendored
2
.gitignore
vendored
@@ -161,3 +161,5 @@ junit.xml
|
|||||||
# ===================================================================
|
# ===================================================================
|
||||||
# Fin del archivo .gitignore
|
# Fin del archivo .gitignore
|
||||||
# ===================================================================
|
# ===================================================================
|
||||||
|
Backend/SQL
|
||||||
|
.atl
|
||||||
|
|||||||
@@ -1,12 +0,0 @@
|
|||||||
# ================================================
|
|
||||||
# VARIABLES DE ENTORNO PARA LA CONFIGURACIÓN DE CORREO
|
|
||||||
# ================================================
|
|
||||||
# El separador de doble guion bajo (__) se usa para mapear la jerarquía del JSON.
|
|
||||||
# MailSettings:SmtpHost se convierte en MailSettings__SmtpHost
|
|
||||||
|
|
||||||
MailSettings__SmtpHost="mail.eldia.com"
|
|
||||||
MailSettings__SmtpPort=587
|
|
||||||
MailSettings__SenderName="Club - Diario El Día"
|
|
||||||
MailSettings__SenderEmail="alertas@eldia.com"
|
|
||||||
MailSettings__SmtpUser="alertas@eldia.com"
|
|
||||||
MailSettings__SmtpPass="@Alertas713550@"
|
|
||||||
@@ -51,6 +51,7 @@ namespace GestionIntegral.Api.Controllers
|
|||||||
private readonly IPerfilService _perfilService;
|
private readonly IPerfilService _perfilService;
|
||||||
private readonly IPermisoService _permisoService;
|
private readonly IPermisoService _permisoService;
|
||||||
private readonly ICambioParadaService _cambioParadaService;
|
private readonly ICambioParadaService _cambioParadaService;
|
||||||
|
private readonly ICierreCuentaCorrienteService _cierreCcService;
|
||||||
private readonly ILogger<AuditoriaController> _logger;
|
private readonly ILogger<AuditoriaController> _logger;
|
||||||
|
|
||||||
// Permiso general para ver cualquier auditoría.
|
// Permiso general para ver cualquier auditoría.
|
||||||
@@ -86,6 +87,7 @@ namespace GestionIntegral.Api.Controllers
|
|||||||
IPerfilService perfilService,
|
IPerfilService perfilService,
|
||||||
IPermisoService permisoService,
|
IPermisoService permisoService,
|
||||||
ICambioParadaService cambioParadaService,
|
ICambioParadaService cambioParadaService,
|
||||||
|
ICierreCuentaCorrienteService cierreCcService,
|
||||||
ILogger<AuditoriaController> logger)
|
ILogger<AuditoriaController> logger)
|
||||||
{
|
{
|
||||||
_usuarioService = usuarioService;
|
_usuarioService = usuarioService;
|
||||||
@@ -116,6 +118,7 @@ namespace GestionIntegral.Api.Controllers
|
|||||||
_perfilService = perfilService;
|
_perfilService = perfilService;
|
||||||
_cambioParadaService = cambioParadaService;
|
_cambioParadaService = cambioParadaService;
|
||||||
_permisoService = permisoService;
|
_permisoService = permisoService;
|
||||||
|
_cierreCcService = cierreCcService;
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -692,6 +695,26 @@ namespace GestionIntegral.Api.Controllers
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[HttpGet("cierres-cuenta-corriente")]
|
||||||
|
[ProducesResponseType(typeof(IEnumerable<CierreCuentaCorrienteHistorialDto>), StatusCodes.Status200OK)]
|
||||||
|
public async Task<IActionResult> GetHistorialCierresCuentaCorriente(
|
||||||
|
[FromQuery] DateTime? fechaDesde, [FromQuery] DateTime? fechaHasta,
|
||||||
|
[FromQuery] int? idUsuarioModifico, [FromQuery] string? tipoModificacion,
|
||||||
|
[FromQuery] int? idCierreAfectado)
|
||||||
|
{
|
||||||
|
if (!TienePermiso(PermisoVerAuditoria)) return Forbid();
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var historial = await _cierreCcService.ObtenerHistorialAsync(fechaDesde, fechaHasta, idUsuarioModifico, tipoModificacion, idCierreAfectado);
|
||||||
|
return Ok(historial ?? Enumerable.Empty<CierreCuentaCorrienteHistorialDto>());
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "Error obteniendo historial de Cierres de Cuenta Corriente.");
|
||||||
|
return StatusCode(500, "Error interno al obtener historial de Cierres de Cuenta Corriente.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
[HttpGet("cambios-parada-canilla")]
|
[HttpGet("cambios-parada-canilla")]
|
||||||
[ProducesResponseType(typeof(IEnumerable<CambioParadaHistorialDto>), StatusCodes.Status200OK)]
|
[ProducesResponseType(typeof(IEnumerable<CambioParadaHistorialDto>), StatusCodes.Status200OK)]
|
||||||
public async Task<IActionResult> GetHistorialCambiosParada(
|
public async Task<IActionResult> GetHistorialCambiosParada(
|
||||||
|
|||||||
@@ -0,0 +1,168 @@
|
|||||||
|
using GestionIntegral.Api.Dtos.Auditoria;
|
||||||
|
using GestionIntegral.Api.Dtos.Contables;
|
||||||
|
using GestionIntegral.Api.Services.Contables;
|
||||||
|
using Microsoft.AspNetCore.Authorization;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Security.Claims;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace GestionIntegral.Api.Controllers.Contables
|
||||||
|
{
|
||||||
|
[Route("api/cierres-cc")]
|
||||||
|
[ApiController]
|
||||||
|
[Authorize]
|
||||||
|
public class CierresCuentaCorrienteController : ControllerBase
|
||||||
|
{
|
||||||
|
private readonly ICierreCuentaCorrienteService _cierreService;
|
||||||
|
private readonly ILogger<CierresCuentaCorrienteController> _logger;
|
||||||
|
|
||||||
|
// Permisos asignables a perfiles. La reapertura (CC002) NO se valida acá: es exclusiva de SuperAdmin.
|
||||||
|
private const string PermisoCrear = "CC001";
|
||||||
|
private const string PermisoVer = "CC003";
|
||||||
|
|
||||||
|
public CierresCuentaCorrienteController(
|
||||||
|
ICierreCuentaCorrienteService cierreService,
|
||||||
|
ILogger<CierresCuentaCorrienteController> logger)
|
||||||
|
{
|
||||||
|
_cierreService = cierreService;
|
||||||
|
_logger = logger;
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool TienePermiso(string codAcc) =>
|
||||||
|
User.IsInRole("SuperAdmin") || User.HasClaim(c => c.Type == "permission" && c.Value == codAcc);
|
||||||
|
|
||||||
|
private bool EsSuperAdmin() => User.IsInRole("SuperAdmin");
|
||||||
|
|
||||||
|
private int? GetCurrentUserId()
|
||||||
|
{
|
||||||
|
if (int.TryParse(User.FindFirstValue(ClaimTypes.NameIdentifier) ?? User.FindFirstValue("sub"), out int userId))
|
||||||
|
return userId;
|
||||||
|
_logger.LogWarning("No se pudo obtener el UserId del token JWT en CierresCuentaCorrienteController.");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// POST: api/cierres-cc
|
||||||
|
[HttpPost]
|
||||||
|
[ProducesResponseType(typeof(CierreCuentaCorrienteDto), StatusCodes.Status201Created)]
|
||||||
|
[ProducesResponseType(StatusCodes.Status400BadRequest)]
|
||||||
|
[ProducesResponseType(StatusCodes.Status403Forbidden)]
|
||||||
|
[ProducesResponseType(StatusCodes.Status409Conflict)]
|
||||||
|
public async Task<IActionResult> Crear([FromBody] CrearCierreDto dto)
|
||||||
|
{
|
||||||
|
if (!TienePermiso(PermisoCrear)) return Forbid();
|
||||||
|
if (!ModelState.IsValid) return BadRequest(ModelState);
|
||||||
|
var userId = GetCurrentUserId();
|
||||||
|
if (userId == null) return Unauthorized();
|
||||||
|
|
||||||
|
var (cierre, errorCode, errorMessage) = await _cierreService.CrearCierreAsync(dto, userId.Value);
|
||||||
|
|
||||||
|
if (errorCode != null)
|
||||||
|
{
|
||||||
|
int status = errorCode switch
|
||||||
|
{
|
||||||
|
"CIERRE_FECHA_ANTERIOR_A_ULTIMO" => StatusCodes.Status409Conflict,
|
||||||
|
"CIERRE_ERROR_INTERNO" => StatusCodes.Status500InternalServerError,
|
||||||
|
_ => StatusCodes.Status400BadRequest
|
||||||
|
};
|
||||||
|
return StatusCode(status, new { codigo = errorCode, mensaje = errorMessage });
|
||||||
|
}
|
||||||
|
|
||||||
|
return StatusCode(StatusCodes.Status201Created, cierre);
|
||||||
|
}
|
||||||
|
|
||||||
|
// POST: api/cierres-cc/{idCierre}/reabrir
|
||||||
|
[HttpPost("{idCierre:int}/reabrir")]
|
||||||
|
[ProducesResponseType(typeof(CierreCuentaCorrienteDto), StatusCodes.Status200OK)]
|
||||||
|
[ProducesResponseType(StatusCodes.Status400BadRequest)]
|
||||||
|
[ProducesResponseType(StatusCodes.Status403Forbidden)]
|
||||||
|
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||||
|
[ProducesResponseType(StatusCodes.Status409Conflict)]
|
||||||
|
public async Task<IActionResult> Reabrir(int idCierre, [FromBody] ReabrirCierreDto dto)
|
||||||
|
{
|
||||||
|
if (!EsSuperAdmin())
|
||||||
|
return Forbid();
|
||||||
|
if (!ModelState.IsValid) return BadRequest(ModelState);
|
||||||
|
var userId = GetCurrentUserId();
|
||||||
|
if (userId == null) return Unauthorized();
|
||||||
|
|
||||||
|
var (cierre, errorCode, errorMessage) = await _cierreService.ReabrirCierreAsync(idCierre, dto, userId.Value, esSuperAdmin: true);
|
||||||
|
|
||||||
|
if (errorCode != null)
|
||||||
|
{
|
||||||
|
int status = errorCode switch
|
||||||
|
{
|
||||||
|
"CIERRE_NO_ENCONTRADO" => StatusCodes.Status404NotFound,
|
||||||
|
"CIERRE_PERMISO_DENEGADO" => StatusCodes.Status403Forbidden,
|
||||||
|
"CIERRE_HAY_POSTERIORES_VIGENTES" => StatusCodes.Status409Conflict,
|
||||||
|
"CIERRE_YA_ANULADO" => StatusCodes.Status409Conflict,
|
||||||
|
"CIERRE_ERROR_INTERNO" => StatusCodes.Status500InternalServerError,
|
||||||
|
_ => StatusCodes.Status400BadRequest
|
||||||
|
};
|
||||||
|
return StatusCode(status, new { codigo = errorCode, mensaje = errorMessage });
|
||||||
|
}
|
||||||
|
|
||||||
|
return Ok(cierre);
|
||||||
|
}
|
||||||
|
|
||||||
|
// GET: api/cierres-cc?idDistribuidor=&idEmpresa=&estado=&fechaCorteDesde=&fechaCorteHasta=
|
||||||
|
[HttpGet]
|
||||||
|
[ProducesResponseType(typeof(IEnumerable<CierreCuentaCorrienteDto>), StatusCodes.Status200OK)]
|
||||||
|
[ProducesResponseType(StatusCodes.Status403Forbidden)]
|
||||||
|
public async Task<IActionResult> GetAll(
|
||||||
|
[FromQuery] int? idDistribuidor,
|
||||||
|
[FromQuery] int? idEmpresa,
|
||||||
|
[FromQuery] string? estado,
|
||||||
|
[FromQuery] DateTime? fechaCorteDesde,
|
||||||
|
[FromQuery] DateTime? fechaCorteHasta)
|
||||||
|
{
|
||||||
|
if (!TienePermiso(PermisoVer)) return Forbid();
|
||||||
|
var cierres = await _cierreService.GetAllAsync(idDistribuidor, idEmpresa, estado, fechaCorteDesde, fechaCorteHasta);
|
||||||
|
return Ok(cierres);
|
||||||
|
}
|
||||||
|
|
||||||
|
// GET: api/cierres-cc/{idCierre}
|
||||||
|
[HttpGet("{idCierre:int}")]
|
||||||
|
[ProducesResponseType(typeof(CierreCuentaCorrienteDto), StatusCodes.Status200OK)]
|
||||||
|
[ProducesResponseType(StatusCodes.Status403Forbidden)]
|
||||||
|
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||||
|
public async Task<IActionResult> GetById(int idCierre)
|
||||||
|
{
|
||||||
|
if (!TienePermiso(PermisoVer)) return Forbid();
|
||||||
|
var cierre = await _cierreService.GetByIdAsync(idCierre);
|
||||||
|
if (cierre == null) return NotFound(new { message = $"Cierre #{idCierre} no encontrado." });
|
||||||
|
return Ok(cierre);
|
||||||
|
}
|
||||||
|
|
||||||
|
// GET: api/cierres-cc/ultimo?idDistribuidor=&idEmpresa=
|
||||||
|
// Atajo del frontend para autorrellenar "Desde último cierre" en filtros del reporte.
|
||||||
|
// Acepta CC003 (gestión de cierres) o RR001 (acceso al reporte que CONTIENE este atajo):
|
||||||
|
// operadores con solo el permiso del reporte deben poder usar el atajo desde la pantalla del reporte.
|
||||||
|
[HttpGet("ultimo")]
|
||||||
|
[ProducesResponseType(typeof(UltimoCierreDto), StatusCodes.Status200OK)]
|
||||||
|
[ProducesResponseType(StatusCodes.Status403Forbidden)]
|
||||||
|
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||||
|
public async Task<IActionResult> GetUltimoVigente(
|
||||||
|
[FromQuery] int idDistribuidor,
|
||||||
|
[FromQuery] int idEmpresa)
|
||||||
|
{
|
||||||
|
if (!TienePermiso(PermisoVer) && !TienePermiso("RR001")) return Forbid();
|
||||||
|
var ultimo = await _cierreService.GetUltimoVigenteAsync(idDistribuidor, idEmpresa);
|
||||||
|
if (ultimo == null) return NotFound(new { message = "No hay cierres vigentes para el distribuidor en la empresa indicada." });
|
||||||
|
return Ok(ultimo);
|
||||||
|
}
|
||||||
|
|
||||||
|
// GET: api/cierres-cc/{idCierre}/historial
|
||||||
|
[HttpGet("{idCierre:int}/historial")]
|
||||||
|
[ProducesResponseType(typeof(IEnumerable<CierreCuentaCorrienteHistorialDto>), StatusCodes.Status200OK)]
|
||||||
|
[ProducesResponseType(StatusCodes.Status403Forbidden)]
|
||||||
|
public async Task<IActionResult> GetHistorial(int idCierre)
|
||||||
|
{
|
||||||
|
if (!TienePermiso(PermisoVer)) return Forbid();
|
||||||
|
var historial = await _cierreService.GetHistorialAsync(idCierre);
|
||||||
|
return Ok(historial);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -18,9 +18,8 @@ namespace GestionIntegral.Api.Controllers.Contables
|
|||||||
private readonly ISaldoService _saldoService;
|
private readonly ISaldoService _saldoService;
|
||||||
private readonly ILogger<SaldosController> _logger;
|
private readonly ILogger<SaldosController> _logger;
|
||||||
|
|
||||||
// Define un permiso específico para ver saldos, y otro para ajustarlos (SuperAdmin implícito)
|
// Permiso para ver saldos. El ajuste manual es exclusivo de SuperAdmin (no se valida un permiso asignable).
|
||||||
private const string PermisoVerSaldos = "CS001"; // Ejemplo: Cuentas Saldos Ver
|
private const string PermisoVerSaldos = "CS001";
|
||||||
private const string PermisoAjustarSaldos = "CS002"; // Ejemplo: Cuentas Saldos Ajustar (o solo SuperAdmin)
|
|
||||||
|
|
||||||
|
|
||||||
public SaldosController(ISaldoService saldoService, ILogger<SaldosController> logger)
|
public SaldosController(ISaldoService saldoService, ILogger<SaldosController> logger)
|
||||||
@@ -76,11 +75,11 @@ namespace GestionIntegral.Api.Controllers.Contables
|
|||||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||||
public async Task<IActionResult> AjustarSaldoManualmente([FromBody] AjusteSaldoRequestDto ajusteDto)
|
public async Task<IActionResult> AjustarSaldoManualmente([FromBody] AjusteSaldoRequestDto ajusteDto)
|
||||||
{
|
{
|
||||||
// Esta operación debería ser MUY restringida. Solo SuperAdmin o un permiso muy específico.
|
// El ajuste manual de saldo es operación crítica: solo SuperAdmin. No se admite vía permiso asignable.
|
||||||
if (!User.IsInRole("SuperAdmin") && !TienePermiso(PermisoAjustarSaldos))
|
if (!User.IsInRole("SuperAdmin"))
|
||||||
{
|
{
|
||||||
_logger.LogWarning("Intento no autorizado de ajustar saldo por Usuario ID {userId}", GetCurrentUserId() ?? 0);
|
_logger.LogWarning("Intento no autorizado de ajustar saldo por Usuario ID {userId}", GetCurrentUserId() ?? 0);
|
||||||
return Forbid("No tiene permisos para realizar ajustes manuales de saldo.");
|
return Forbid("Solo SuperAdmin puede ajustar saldos manualmente.");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!ModelState.IsValid) return BadRequest(ModelState);
|
if (!ModelState.IsValid) return BadRequest(ModelState);
|
||||||
@@ -99,6 +98,10 @@ namespace GestionIntegral.Api.Controllers.Contables
|
|||||||
}
|
}
|
||||||
return Ok(saldoActualizado);
|
return Ok(saldoActualizado);
|
||||||
}
|
}
|
||||||
|
catch (BloqueoPorPeriodoCerradoException)
|
||||||
|
{
|
||||||
|
throw;
|
||||||
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
_logger.LogError(ex, "Error crítico al ajustar saldo manualmente.");
|
_logger.LogError(ex, "Error crítico al ajustar saldo manualmente.");
|
||||||
|
|||||||
@@ -40,19 +40,19 @@ namespace GestionIntegral.Api.Controllers.Distribucion
|
|||||||
[HttpGet]
|
[HttpGet]
|
||||||
[ProducesResponseType(typeof(IEnumerable<DistribuidorDto>), StatusCodes.Status200OK)]
|
[ProducesResponseType(typeof(IEnumerable<DistribuidorDto>), StatusCodes.Status200OK)]
|
||||||
[ProducesResponseType(StatusCodes.Status403Forbidden)]
|
[ProducesResponseType(StatusCodes.Status403Forbidden)]
|
||||||
public async Task<IActionResult> GetAllDistribuidores([FromQuery] string? nombre, [FromQuery] string? nroDoc)
|
public async Task<IActionResult> GetAllDistribuidores([FromQuery] string? nombre, [FromQuery] string? nroDoc, [FromQuery] bool? soloActivos = true)
|
||||||
{
|
{
|
||||||
if (!TienePermiso(PermisoVer)) return Forbid();
|
if (!TienePermiso(PermisoVer)) return Forbid();
|
||||||
var distribuidores = await _distribuidorService.ObtenerTodosAsync(nombre, nroDoc);
|
var distribuidores = await _distribuidorService.ObtenerTodosAsync(nombre, nroDoc, soloActivos);
|
||||||
return Ok(distribuidores);
|
return Ok(distribuidores);
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpGet("dropdown")]
|
[HttpGet("dropdown")]
|
||||||
[ProducesResponseType(typeof(IEnumerable<DistribuidorDropdownDto>), StatusCodes.Status200OK)]
|
[ProducesResponseType(typeof(IEnumerable<DistribuidorDropdownDto>), StatusCodes.Status200OK)]
|
||||||
[ProducesResponseType(StatusCodes.Status403Forbidden)]
|
[ProducesResponseType(StatusCodes.Status403Forbidden)]
|
||||||
public async Task<IActionResult> GetAllDropdownDistribuidores()
|
public async Task<IActionResult> GetAllDropdownDistribuidores([FromQuery] bool? soloActivos = true)
|
||||||
{
|
{
|
||||||
var distribuidores = await _distribuidorService.GetAllDropdownAsync();
|
var distribuidores = await _distribuidorService.GetAllDropdownAsync(soloActivos);
|
||||||
return Ok(distribuidores);
|
return Ok(distribuidores);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -117,6 +117,27 @@ namespace GestionIntegral.Api.Controllers.Distribucion
|
|||||||
return NoContent();
|
return NoContent();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[HttpPut("{id:int}/toggle-baja")]
|
||||||
|
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
||||||
|
[ProducesResponseType(StatusCodes.Status400BadRequest)]
|
||||||
|
[ProducesResponseType(StatusCodes.Status403Forbidden)]
|
||||||
|
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||||
|
public async Task<IActionResult> ToggleBajaDistribuidor(int id, [FromBody] ToggleBajaDistribuidorDto dto)
|
||||||
|
{
|
||||||
|
if (!TienePermiso(PermisoModificar)) return Forbid();
|
||||||
|
if (!ModelState.IsValid) return BadRequest(ModelState);
|
||||||
|
var userId = GetCurrentUserId();
|
||||||
|
if (userId == null) return Unauthorized();
|
||||||
|
|
||||||
|
var (exito, error) = await _distribuidorService.ToggleBajaAsync(id, dto.DarDeBaja, dto.FechaBaja, userId.Value);
|
||||||
|
if (!exito)
|
||||||
|
{
|
||||||
|
if (error == "Distribuidor no encontrado.") return NotFound(new { message = error });
|
||||||
|
return BadRequest(new { message = error });
|
||||||
|
}
|
||||||
|
return NoContent();
|
||||||
|
}
|
||||||
|
|
||||||
[HttpDelete("{id:int}")]
|
[HttpDelete("{id:int}")]
|
||||||
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
||||||
[ProducesResponseType(StatusCodes.Status400BadRequest)]
|
[ProducesResponseType(StatusCodes.Status400BadRequest)]
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ using GestionIntegral.Api.Dtos.Impresion;
|
|||||||
using GestionIntegral.Api.Services.Impresion;
|
using GestionIntegral.Api.Services.Impresion;
|
||||||
using Microsoft.AspNetCore.Authorization;
|
using Microsoft.AspNetCore.Authorization;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using Microsoft.AspNetCore.Mvc.ModelBinding;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
@@ -40,6 +41,7 @@ namespace GestionIntegral.Api.Controllers.Impresion
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GET: api/stockbobinas
|
||||||
// GET: api/stockbobinas
|
// GET: api/stockbobinas
|
||||||
[HttpGet]
|
[HttpGet]
|
||||||
[ProducesResponseType(typeof(IEnumerable<StockBobinaDto>), StatusCodes.Status200OK)]
|
[ProducesResponseType(typeof(IEnumerable<StockBobinaDto>), StatusCodes.Status200OK)]
|
||||||
@@ -47,12 +49,23 @@ namespace GestionIntegral.Api.Controllers.Impresion
|
|||||||
public async Task<IActionResult> GetAllStockBobinas(
|
public async Task<IActionResult> GetAllStockBobinas(
|
||||||
[FromQuery] int? idTipoBobina, [FromQuery] string? nroBobina, [FromQuery] int? idPlanta,
|
[FromQuery] int? idTipoBobina, [FromQuery] string? nroBobina, [FromQuery] int? idPlanta,
|
||||||
[FromQuery] int? idEstadoBobina, [FromQuery] string? remito,
|
[FromQuery] int? idEstadoBobina, [FromQuery] string? remito,
|
||||||
[FromQuery] DateTime? fechaDesde, [FromQuery] DateTime? fechaHasta)
|
[FromQuery] DateTime? fechaDesde, [FromQuery] DateTime? fechaHasta,
|
||||||
|
[FromQuery] DateTime? fechaEstadoDesde, [FromQuery] DateTime? fechaEstadoHasta) // <--- Nuevos parámetros agregados
|
||||||
{
|
{
|
||||||
if (!TienePermiso(PermisoVerStock)) return Forbid();
|
if (!TienePermiso(PermisoVerStock)) return Forbid();
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var bobinas = await _stockBobinaService.ObtenerTodosAsync(idTipoBobina, nroBobina, idPlanta, idEstadoBobina, remito, fechaDesde, fechaHasta);
|
var bobinas = await _stockBobinaService.ObtenerTodosAsync(
|
||||||
|
idTipoBobina,
|
||||||
|
nroBobina,
|
||||||
|
idPlanta,
|
||||||
|
idEstadoBobina,
|
||||||
|
remito,
|
||||||
|
fechaDesde,
|
||||||
|
fechaHasta,
|
||||||
|
fechaEstadoDesde,
|
||||||
|
fechaEstadoHasta
|
||||||
|
);
|
||||||
return Ok(bobinas);
|
return Ok(bobinas);
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
@@ -172,5 +185,72 @@ namespace GestionIntegral.Api.Controllers.Impresion
|
|||||||
}
|
}
|
||||||
return NoContent();
|
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.
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -41,9 +41,11 @@ namespace GestionIntegral.Api.Controllers.Reportes.PdfTemplates
|
|||||||
|
|
||||||
void ComposeContent(IContainer container)
|
void ComposeContent(IContainer container)
|
||||||
{
|
{
|
||||||
container.PaddingTop(1, Unit.Centimetre).Column(column =>
|
// << CAMBIO: Reducido el padding superior de 1cm a 5mm >>
|
||||||
|
container.PaddingTop(5, Unit.Millimetre).Column(column =>
|
||||||
{
|
{
|
||||||
column.Spacing(15);
|
// << CAMBIO: Reducido el espaciado principal entre elementos de 15 a 10 puntos >>
|
||||||
|
column.Spacing(10);
|
||||||
|
|
||||||
column.Item().Row(row =>
|
column.Item().Row(row =>
|
||||||
{
|
{
|
||||||
@@ -59,23 +61,24 @@ namespace GestionIntegral.Api.Controllers.Reportes.PdfTemplates
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
column.Item().PaddingTop(5).Border(1).Background(Colors.Grey.Lighten3).AlignCenter().Padding(2).Text(Model.NombreEmpresa).SemiBold();
|
column.Item().PaddingTop(3).Border(1).Background(Colors.Grey.Lighten3).AlignCenter().Padding(2).Text(Model.NombreEmpresa).SemiBold();
|
||||||
|
|
||||||
column.Item().Border(1).Padding(10).Column(innerCol =>
|
column.Item().Border(1).Padding(8).Column(innerCol => // << CAMBIO: Padding reducido de 10 a 8 >>
|
||||||
{
|
{
|
||||||
innerCol.Spacing(5);
|
// << CAMBIO: Reducido el espaciado interno de 5 a 4 >>
|
||||||
|
innerCol.Spacing(4);
|
||||||
|
|
||||||
// Fila de "Ingresados por Remito" con borde inferior sólido.
|
|
||||||
innerCol.Item().BorderBottom(1, Unit.Point).BorderColor(Colors.Grey.Medium).Row(row =>
|
innerCol.Item().BorderBottom(1, Unit.Point).BorderColor(Colors.Grey.Medium).Row(row =>
|
||||||
{
|
{
|
||||||
row.RelativeItem().Text("Ingresados por Remito:").SemiBold();
|
row.RelativeItem().Text("Ingresados por Remito:").SemiBold();
|
||||||
row.RelativeItem().AlignRight().Text(Model.TotalIngresadosPorRemito.ToString("N0"));
|
row.RelativeItem().AlignRight().Text(Model.TotalIngresadosPorRemito.ToString("N0"));
|
||||||
}); // <-- SOLUCIÓN: Borde sólido simple.
|
});
|
||||||
|
|
||||||
foreach (var item in Model.Detalles)
|
foreach (var item in Model.Detalles)
|
||||||
{
|
{
|
||||||
var totalSeccion = item.Devueltos - item.Llevados;
|
var totalSeccion = item.Devueltos - item.Llevados;
|
||||||
innerCol.Item().PaddingTop(5).Row(row =>
|
// << CAMBIO: Reducido el padding superior de 5 a 3 >>
|
||||||
|
innerCol.Item().PaddingTop(3).Row(row =>
|
||||||
{
|
{
|
||||||
row.ConstantItem(100).Text(item.Tipo).SemiBold();
|
row.ConstantItem(100).Text(item.Tipo).SemiBold();
|
||||||
|
|
||||||
@@ -90,7 +93,8 @@ namespace GestionIntegral.Api.Controllers.Reportes.PdfTemplates
|
|||||||
r.RelativeItem().Text("Devueltos");
|
r.RelativeItem().Text("Devueltos");
|
||||||
r.RelativeItem().AlignRight().Text($"{item.Devueltos:N0}");
|
r.RelativeItem().AlignRight().Text($"{item.Devueltos:N0}");
|
||||||
});
|
});
|
||||||
sub.Item().BorderTop(1.5f).BorderColor(Colors.Black).PaddingTop(2).Row(r => {
|
// << CAMBIO: Reducido el padding superior de 2 a 1 >>
|
||||||
|
sub.Item().BorderTop(1.5f).BorderColor(Colors.Black).PaddingTop(1).Row(r => {
|
||||||
r.RelativeItem().Text(t => t.Span("Total").SemiBold());
|
r.RelativeItem().Text(t => t.Span("Total").SemiBold());
|
||||||
r.RelativeItem().AlignRight().Text(t => t.Span(totalSeccion.ToString("N0")).SemiBold());
|
r.RelativeItem().AlignRight().Text(t => t.Span(totalSeccion.ToString("N0")).SemiBold());
|
||||||
});
|
});
|
||||||
@@ -99,7 +103,8 @@ namespace GestionIntegral.Api.Controllers.Reportes.PdfTemplates
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
column.Item().PaddingTop(10).Column(finalCol =>
|
// << CAMBIO: Reducido el padding superior de 10 a 8 >>
|
||||||
|
column.Item().PaddingTop(8).Column(finalCol =>
|
||||||
{
|
{
|
||||||
finalCol.Spacing(2);
|
finalCol.Spacing(2);
|
||||||
|
|
||||||
@@ -112,13 +117,15 @@ namespace GestionIntegral.Api.Controllers.Reportes.PdfTemplates
|
|||||||
if (isBold) valueText.SemiBold();
|
if (isBold) valueText.SemiBold();
|
||||||
};
|
};
|
||||||
|
|
||||||
// Usamos bordes superiores para separar las líneas de total
|
// << CAMBIO: Reducido el padding superior de 2 a 1 en las siguientes líneas >>
|
||||||
finalCol.Item().BorderTop(2f).BorderColor(Colors.Black).PaddingTop(2).Row(row => AddTotalRow(row, "Total Devolución a la Fecha", Model.TotalDevolucionALaFecha.ToString("N0"), false));
|
finalCol.Item().BorderTop(2f).BorderColor(Colors.Black).PaddingTop(1).Row(row => AddTotalRow(row, "Total Devolución a la Fecha", Model.TotalDevolucionALaFecha.ToString("N0"), false));
|
||||||
finalCol.Item().BorderTop(1).BorderColor(Colors.Grey.Lighten2).PaddingTop(2).Row(row => AddTotalRow(row, "Total Devolución Días Anteriores", Model.TotalDevolucionDiasAnteriores.ToString("N0"), false));
|
finalCol.Item().BorderTop(1).BorderColor(Colors.Grey.Lighten2).PaddingTop(1).Row(row => AddTotalRow(row, "Total Devolución Días Anteriores", Model.TotalDevolucionDiasAnteriores.ToString("N0"), false));
|
||||||
finalCol.Item().BorderTop(1).BorderColor(Colors.Grey.Lighten2).PaddingTop(2).Row(row => AddTotalRow(row, "Total Devolución", Model.TotalDevolucionGeneral.ToString("N0"), false));
|
finalCol.Item().BorderTop(1).BorderColor(Colors.Grey.Lighten2).PaddingTop(1).Row(row => AddTotalRow(row, "Total Devolución", Model.TotalDevolucionGeneral.ToString("N0"), false));
|
||||||
finalCol.Item().BorderTop(2f).BorderColor(Colors.Black).PaddingTop(5).Row(row => AddTotalRow(row, "Sin Cargo", Model.TotalSinCargo.ToString("N0"), false));
|
// << CAMBIO: Reducido el padding superior de 5 a 3 >>
|
||||||
finalCol.Item().BorderTop(1).BorderColor(Colors.Grey.Lighten2).PaddingTop(2).Row(row => AddTotalRow(row, "Sobrantes", $"-{Model.TotalSobrantes.ToString("N0")}", false));
|
finalCol.Item().BorderTop(2f).BorderColor(Colors.Black).PaddingTop(3).Row(row => AddTotalRow(row, "Sin Cargo", Model.TotalSinCargo.ToString("N0"), false));
|
||||||
finalCol.Item().BorderTop(1).BorderColor(Colors.Grey.Lighten2).BorderBottom(1).BorderColor(Colors.Grey.Lighten2).PaddingTop(5).Row(row => AddTotalRow(row, "Diferencia", Model.DiferenciaFinal.ToString("N0"), true));
|
finalCol.Item().BorderTop(1).BorderColor(Colors.Grey.Lighten2).PaddingTop(1).Row(row => AddTotalRow(row, "Sobrantes", $"-{Model.TotalSobrantes.ToString("N0")}", false));
|
||||||
|
// << CAMBIO: Reducido el padding superior de 5 a 3 >>
|
||||||
|
finalCol.Item().BorderTop(1).BorderColor(Colors.Grey.Lighten2).BorderBottom(1).BorderColor(Colors.Grey.Lighten2).PaddingTop(3).Row(row => AddTotalRow(row, "Diferencia", Model.DiferenciaFinal.ToString("N0"), true));
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -74,7 +74,6 @@ namespace GestionIntegral.Api.Controllers.Reportes.PdfTemplates
|
|||||||
if (Model.DebitosCreditos.Any()) column.Item().Element(ComposeDebCredTable);
|
if (Model.DebitosCreditos.Any()) column.Item().Element(ComposeDebCredTable);
|
||||||
|
|
||||||
column.Item().Element(ComposeResumenPeriodo);
|
column.Item().Element(ComposeResumenPeriodo);
|
||||||
column.Item().Element(ComposeSaldoFinal);
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -107,7 +106,11 @@ namespace GestionIntegral.Api.Controllers.Reportes.PdfTemplates
|
|||||||
header.Cell().Border(1).Background(Colors.Grey.Lighten3).Padding(2).AlignRight().Text("Saldo");
|
header.Cell().Border(1).Background(Colors.Grey.Lighten3).Padding(2).AlignRight().Text("Saldo");
|
||||||
});
|
});
|
||||||
|
|
||||||
decimal saldoAcumulado = 0; // Inicia en CERO
|
// Fila Saldo Inicial al inicio de la primera tabla — ancla del acumulado
|
||||||
|
table.Cell().ColumnSpan(6).Border(1).Padding(2).Text(t => t.Span("Saldo Inicial").SemiBold());
|
||||||
|
table.Cell().Border(1).Padding(2).AlignRight().Text(t => t.Span(Model.SaldoInicial.ToString("C", CultureAr)).SemiBold());
|
||||||
|
|
||||||
|
decimal saldoAcumulado = Model.SaldoInicial;
|
||||||
foreach (var item in Model.Movimientos.OrderBy(m => m.Fecha))
|
foreach (var item in Model.Movimientos.OrderBy(m => m.Fecha))
|
||||||
{
|
{
|
||||||
saldoAcumulado += (item.Debe - item.Haber);
|
saldoAcumulado += (item.Debe - item.Haber);
|
||||||
@@ -157,7 +160,7 @@ namespace GestionIntegral.Api.Controllers.Reportes.PdfTemplates
|
|||||||
header.Cell().Border(1).Background(Colors.Grey.Lighten3).Padding(2).Text("Detalle");
|
header.Cell().Border(1).Background(Colors.Grey.Lighten3).Padding(2).Text("Detalle");
|
||||||
});
|
});
|
||||||
|
|
||||||
decimal saldoAcumulado = Model.TotalMovimientos;
|
decimal saldoAcumulado = Model.SaldoInicial + Model.TotalMovimientos;
|
||||||
foreach (var item in Model.Pagos.OrderBy(p => p.Fecha).ThenBy(p => p.Recibo))
|
foreach (var item in Model.Pagos.OrderBy(p => p.Fecha).ThenBy(p => p.Recibo))
|
||||||
{
|
{
|
||||||
saldoAcumulado += (item.Debe - item.Haber);
|
saldoAcumulado += (item.Debe - item.Haber);
|
||||||
@@ -204,7 +207,7 @@ namespace GestionIntegral.Api.Controllers.Reportes.PdfTemplates
|
|||||||
header.Cell().Border(1).Background(Colors.Grey.Lighten3).Padding(2).AlignRight().Text("Saldo");
|
header.Cell().Border(1).Background(Colors.Grey.Lighten3).Padding(2).AlignRight().Text("Saldo");
|
||||||
});
|
});
|
||||||
|
|
||||||
decimal saldoAcumulado = Model.TotalMovimientos + Model.TotalPagos;
|
decimal saldoAcumulado = Model.SaldoInicial + Model.TotalMovimientos + Model.TotalPagos;
|
||||||
foreach (var item in Model.DebitosCreditos.OrderBy(dc => dc.Fecha))
|
foreach (var item in Model.DebitosCreditos.OrderBy(dc => dc.Fecha))
|
||||||
{
|
{
|
||||||
saldoAcumulado += (item.Debe - item.Haber);
|
saldoAcumulado += (item.Debe - item.Haber);
|
||||||
@@ -236,25 +239,17 @@ namespace GestionIntegral.Api.Controllers.Reportes.PdfTemplates
|
|||||||
row.RelativeItem().Text(label);
|
row.RelativeItem().Text(label);
|
||||||
row.ConstantItem(120).AlignRight().Text(value.ToString("C", CultureAr));
|
row.ConstantItem(120).AlignRight().Text(value.ToString("C", CultureAr));
|
||||||
};
|
};
|
||||||
|
col.Item().Row(row => AddResumenRow(row, "Saldo Inicial", Model.SaldoInicial));
|
||||||
col.Item().Row(row => AddResumenRow(row, "Movimientos", Model.TotalMovimientos));
|
col.Item().Row(row => AddResumenRow(row, "Movimientos", Model.TotalMovimientos));
|
||||||
col.Item().Row(row => AddResumenRow(row, "Débitos/Créditos", Model.TotalDebitosCreditos));
|
col.Item().Row(row => AddResumenRow(row, "Débitos/Créditos", Model.TotalDebitosCreditos));
|
||||||
col.Item().Row(row => AddResumenRow(row, "Pagos", Model.TotalPagos));
|
col.Item().Row(row => AddResumenRow(row, "Pagos", Model.TotalPagos));
|
||||||
col.Item().BorderTop(1.5f).BorderColor(Colors.Black).PaddingTop(5).Row(row =>
|
col.Item().BorderTop(1.5f).BorderColor(Colors.Black).PaddingTop(5).Row(row =>
|
||||||
{
|
{
|
||||||
row.RelativeItem().Text("Total").SemiBold();
|
row.RelativeItem().Text("Saldo Final").SemiBold();
|
||||||
row.ConstantItem(120).AlignRight().Text(t => t.Span(Model.TotalPeriodo.ToString("C", CultureAr)).SemiBold());
|
row.ConstantItem(120).AlignRight().Text(t => t.Span(Model.SaldoFinal.ToString("C", CultureAr)).SemiBold());
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
void ComposeSaldoFinal(IContainer container)
|
|
||||||
{
|
|
||||||
container.PaddingTop(5, Unit.Millimetre).AlignLeft().Text(text =>
|
|
||||||
{
|
|
||||||
text.Span($"Saldo Total del Distribuidor al {Model.FechaReporte} ").SemiBold().FontSize(12);
|
|
||||||
text.Span(Model.SaldoDeCuenta.ToString("C", CultureAr)).SemiBold().FontSize(12);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -19,22 +19,31 @@ namespace GestionIntegral.Api.Controllers.Reportes.PdfTemplates
|
|||||||
|
|
||||||
public DocumentMetadata GetMetadata() => DocumentMetadata.Default;
|
public DocumentMetadata GetMetadata() => DocumentMetadata.Default;
|
||||||
|
|
||||||
// CORRECCIÓN: El método GetSettings ya no es necesario para este diseño.
|
|
||||||
// La configuración por defecto es suficiente.
|
|
||||||
// public DocumentSettings GetSettings() => DocumentSettings.Default;
|
|
||||||
|
|
||||||
public void Compose(IDocumentContainer container)
|
public void Compose(IDocumentContainer container)
|
||||||
{
|
{
|
||||||
container.Page(page =>
|
container.Page(page =>
|
||||||
{
|
{
|
||||||
page.Size(PageSizes.A5);
|
page.Size(PageSizes.A4);
|
||||||
page.Margin(1, Unit.Centimetre);
|
page.Margin(5, Unit.Millimetre);
|
||||||
page.DefaultTextStyle(x => x.FontFamily("Arial").FontSize(9));
|
page.DefaultTextStyle(x => x.FontFamily("Arial").FontSize(9));
|
||||||
|
|
||||||
page.Header().Element(ComposeHeader);
|
page.Content().Column(mainColumn =>
|
||||||
page.Content().Element(ComposeContent);
|
{
|
||||||
});
|
mainColumn.Item()
|
||||||
}
|
.AlignCenter()
|
||||||
|
.Width(PageSizes.A6.Width)
|
||||||
|
.Height(PageSizes.A6.Height)
|
||||||
|
.Column(a6ContentColumn =>
|
||||||
|
{
|
||||||
|
a6ContentColumn.Item().PaddingRight(10, Unit.Millimetre).PaddingLeft(10, Unit.Millimetre).Column(content =>
|
||||||
|
{
|
||||||
|
content.Item().Element(ComposeHeader);
|
||||||
|
content.Item().Element(ComposeContent);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
void ComposeHeader(IContainer container)
|
void ComposeHeader(IContainer container)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -148,7 +148,7 @@ namespace GestionIntegral.Api.Controllers.Reportes.PdfTemplates
|
|||||||
|
|
||||||
foreach (var item in Model.PromediosPorDia.OrderBy(d => dayOrder.GetValueOrDefault(d.Dia, 99)))
|
foreach (var item in Model.PromediosPorDia.OrderBy(d => dayOrder.GetValueOrDefault(d.Dia, 99)))
|
||||||
{
|
{
|
||||||
var porcDevolucion = item.Llevados > 0 ? (decimal)item.Devueltos * 100 / item.Llevados : 0;
|
var porcDevolucion = item.Promedio_Llevados > 0 ? (decimal)item.Promedio_Devueltos * 100 / item.Promedio_Llevados : 0;
|
||||||
|
|
||||||
table.Cell().Border(1).Padding(3).Text(item.Dia);
|
table.Cell().Border(1).Padding(3).Text(item.Dia);
|
||||||
table.Cell().Border(1).Padding(3).AlignRight().Text(item.Cant.ToString("N0"));
|
table.Cell().Border(1).Padding(3).AlignRight().Text(item.Cant.ToString("N0"));
|
||||||
@@ -162,7 +162,6 @@ namespace GestionIntegral.Api.Controllers.Reportes.PdfTemplates
|
|||||||
var general = Model.PromedioGeneral;
|
var general = Model.PromedioGeneral;
|
||||||
if (general != null)
|
if (general != null)
|
||||||
{
|
{
|
||||||
var porcDevolucionGeneral = general.Promedio_Llevados > 0 ? (decimal)general.Promedio_Devueltos * 100 / general.Promedio_Llevados : 0;
|
|
||||||
var boldStyle = TextStyle.Default.SemiBold();
|
var boldStyle = TextStyle.Default.SemiBold();
|
||||||
|
|
||||||
table.Cell().Border(1).Padding(3).Text(text => text.Span(general.Dia).Style(boldStyle));
|
table.Cell().Border(1).Padding(3).Text(text => text.Span(general.Dia).Style(boldStyle));
|
||||||
@@ -170,7 +169,7 @@ namespace GestionIntegral.Api.Controllers.Reportes.PdfTemplates
|
|||||||
table.Cell().Border(1).Padding(3).AlignRight().Text(text => text.Span(general.Promedio_Llevados.ToString("N0")).Style(boldStyle));
|
table.Cell().Border(1).Padding(3).AlignRight().Text(text => text.Span(general.Promedio_Llevados.ToString("N0")).Style(boldStyle));
|
||||||
table.Cell().Border(1).Padding(3).AlignRight().Text(text => text.Span(general.Promedio_Devueltos.ToString("N0")).Style(boldStyle));
|
table.Cell().Border(1).Padding(3).AlignRight().Text(text => text.Span(general.Promedio_Devueltos.ToString("N0")).Style(boldStyle));
|
||||||
table.Cell().Border(1).Padding(3).AlignRight().Text(text => text.Span(general.Promedio_Ventas.ToString("N0")).Style(boldStyle));
|
table.Cell().Border(1).Padding(3).AlignRight().Text(text => text.Span(general.Promedio_Ventas.ToString("N0")).Style(boldStyle));
|
||||||
table.Cell().Border(1).Padding(3).AlignRight().Text(text => text.Span(porcDevolucionGeneral.ToString("F2") + "%").Style(boldStyle));
|
table.Cell().Border(1).Padding(3).AlignRight().Text(text => text.Span(Model.PorcentajeDevolucionGeneral.ToString("F2") + "%").Style(boldStyle));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -90,7 +90,7 @@ namespace GestionIntegral.Api.Controllers.Reportes.PdfTemplates
|
|||||||
var llevados = item.Llevados ?? 0;
|
var llevados = item.Llevados ?? 0;
|
||||||
var devueltos = item.Devueltos ?? 0;
|
var devueltos = item.Devueltos ?? 0;
|
||||||
var ventaNetaDia = llevados - devueltos;
|
var ventaNetaDia = llevados - devueltos;
|
||||||
if(llevados > 0)
|
if (llevados > 0)
|
||||||
{
|
{
|
||||||
ventaNetaAcumulada += ventaNetaDia;
|
ventaNetaAcumulada += ventaNetaDia;
|
||||||
conteoDias++;
|
conteoDias++;
|
||||||
@@ -123,7 +123,7 @@ namespace GestionIntegral.Api.Controllers.Reportes.PdfTemplates
|
|||||||
|
|
||||||
void ComposePromediosTable(IContainer container)
|
void ComposePromediosTable(IContainer container)
|
||||||
{
|
{
|
||||||
var dayOrder = new Dictionary<string, int> { { "Lunes", 1 }, { "Martes", 2 }, { "Miércoles", 3 }, { "Jueves", 4 }, { "Viernes", 5 }, { "Sábado", 6 }, { "Domingo", 7 }};
|
var dayOrder = new Dictionary<string, int> { { "Lunes", 1 }, { "Martes", 2 }, { "Miércoles", 3 }, { "Jueves", 4 }, { "Viernes", 5 }, { "Sábado", 6 }, { "Domingo", 7 } };
|
||||||
|
|
||||||
container.Table(table =>
|
container.Table(table =>
|
||||||
{
|
{
|
||||||
@@ -161,16 +161,16 @@ namespace GestionIntegral.Api.Controllers.Reportes.PdfTemplates
|
|||||||
if (general != null)
|
if (general != null)
|
||||||
{
|
{
|
||||||
var boldStyle = TextStyle.Default.SemiBold();
|
var boldStyle = TextStyle.Default.SemiBold();
|
||||||
var llevadosGeneral = general.Llevados ?? 0; // Usamos el total para el %
|
var avgPercentage = Model.PromediosPorDia
|
||||||
var devueltosGeneral = general.Devueltos ?? 0; // Usamos el total para el %
|
.Where(p => p.Dia != "General" && (p.Promedio_Llevados ?? 0) > 0)
|
||||||
var porcDevolucionGeneral = llevadosGeneral > 0 ? (decimal)devueltosGeneral * 100 / llevadosGeneral : 0;
|
.Average(p => (decimal)(p.Promedio_Devueltos ?? 0) * 100 / (p.Promedio_Llevados ?? 1));
|
||||||
|
|
||||||
table.Cell().Border(1).Padding(3).Text(t => t.Span(general.Dia).Style(boldStyle));
|
table.Cell().Border(1).Padding(3).Text(t => t.Span(general.Dia).Style(boldStyle));
|
||||||
table.Cell().Border(1).Padding(3).AlignRight().Text(t => t.Span(general.Cant?.ToString("N0")).Style(boldStyle));
|
table.Cell().Border(1).Padding(3).AlignRight().Text(t => t.Span(general.Cant?.ToString("N0")).Style(boldStyle));
|
||||||
table.Cell().Border(1).Padding(3).AlignRight().Text(t => t.Span(general.Promedio_Llevados?.ToString("N0")).Style(boldStyle));
|
table.Cell().Border(1).Padding(3).AlignRight().Text(t => t.Span(general.Promedio_Llevados?.ToString("N0")).Style(boldStyle));
|
||||||
table.Cell().Border(1).Padding(3).AlignRight().Text(t => t.Span(general.Promedio_Devueltos?.ToString("N0")).Style(boldStyle));
|
table.Cell().Border(1).Padding(3).AlignRight().Text(t => t.Span(general.Promedio_Devueltos?.ToString("N0")).Style(boldStyle));
|
||||||
table.Cell().Border(1).Padding(3).AlignRight().Text(t => t.Span(general.Promedio_Ventas?.ToString("N0")).Style(boldStyle));
|
table.Cell().Border(1).Padding(3).AlignRight().Text(t => t.Span(general.Promedio_Ventas?.ToString("N0")).Style(boldStyle));
|
||||||
table.Cell().Border(1).Padding(3).AlignRight().Text(t => t.Span(porcDevolucionGeneral.ToString("F2") + "%").Style(boldStyle));
|
table.Cell().Border(1).Padding(3).AlignRight().Text(t => t.Span(avgPercentage.ToString("F2") + "%").Style(boldStyle));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -40,6 +40,7 @@ namespace GestionIntegral.Api.Controllers
|
|||||||
private const string PermisoVerReporteListadoDistMensual = "RR009";
|
private const string PermisoVerReporteListadoDistMensual = "RR009";
|
||||||
private const string PermisoVerReporteFacturasPublicidad = "RR010";
|
private const string PermisoVerReporteFacturasPublicidad = "RR010";
|
||||||
private const string PermisoVerReporteDistSuscripciones = "RR011";
|
private const string PermisoVerReporteDistSuscripciones = "RR011";
|
||||||
|
private const string PermisoVerReportesSecretaria = "RR012";
|
||||||
|
|
||||||
public ReportesController(
|
public ReportesController(
|
||||||
IReportesService reportesService,
|
IReportesService reportesService,
|
||||||
@@ -526,7 +527,7 @@ namespace GestionIntegral.Api.Controllers
|
|||||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||||
public async Task<IActionResult> GetVentaMensualSecretariaElDia([FromQuery] DateTime fechaDesde, [FromQuery] DateTime fechaHasta)
|
public async Task<IActionResult> GetVentaMensualSecretariaElDia([FromQuery] DateTime fechaDesde, [FromQuery] DateTime fechaHasta)
|
||||||
{
|
{
|
||||||
if (!TienePermiso(PermisoVerListadoDistribucion)) return Forbid(); // Asumiendo RR002 para todos estos
|
if (!TienePermiso(PermisoVerReportesSecretaria)) return Forbid(); // Asumiendo RR002 para todos estos
|
||||||
var (data, error) = await _reportesService.ObtenerVentaMensualSecretariaElDiaAsync(fechaDesde, fechaHasta);
|
var (data, error) = await _reportesService.ObtenerVentaMensualSecretariaElDiaAsync(fechaDesde, fechaHasta);
|
||||||
if (error != null) return BadRequest(new { message = error });
|
if (error != null) return BadRequest(new { message = error });
|
||||||
if (data == null || !data.Any()) return NotFound(new { message = "No hay datos para el reporte de ventas 'El Día'." });
|
if (data == null || !data.Any()) return NotFound(new { message = "No hay datos para el reporte de ventas 'El Día'." });
|
||||||
@@ -540,7 +541,7 @@ namespace GestionIntegral.Api.Controllers
|
|||||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||||
public async Task<IActionResult> GetVentaMensualSecretariaElDiaPdf([FromQuery] DateTime fechaDesde, [FromQuery] DateTime fechaHasta)
|
public async Task<IActionResult> GetVentaMensualSecretariaElDiaPdf([FromQuery] DateTime fechaDesde, [FromQuery] DateTime fechaHasta)
|
||||||
{
|
{
|
||||||
if (!TienePermiso(PermisoVerListadoDistribucion)) return Forbid();
|
if (!TienePermiso(PermisoVerReportesSecretaria)) return Forbid();
|
||||||
|
|
||||||
var (data, error) = await _reportesService.ObtenerVentaMensualSecretariaElDiaAsync(fechaDesde, fechaHasta);
|
var (data, error) = await _reportesService.ObtenerVentaMensualSecretariaElDiaAsync(fechaDesde, fechaHasta);
|
||||||
|
|
||||||
@@ -577,7 +578,7 @@ namespace GestionIntegral.Api.Controllers
|
|||||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||||
public async Task<IActionResult> GetVentaMensualSecretariaElPlata([FromQuery] DateTime fechaDesde, [FromQuery] DateTime fechaHasta)
|
public async Task<IActionResult> GetVentaMensualSecretariaElPlata([FromQuery] DateTime fechaDesde, [FromQuery] DateTime fechaHasta)
|
||||||
{
|
{
|
||||||
if (!TienePermiso(PermisoVerListadoDistribucion)) return Forbid(); // Asumiendo RR002
|
if (!TienePermiso(PermisoVerReportesSecretaria)) return Forbid(); // Asumiendo RR002
|
||||||
var (data, error) = await _reportesService.ObtenerVentaMensualSecretariaElPlataAsync(fechaDesde, fechaHasta);
|
var (data, error) = await _reportesService.ObtenerVentaMensualSecretariaElPlataAsync(fechaDesde, fechaHasta);
|
||||||
if (error != null) return BadRequest(new { message = error });
|
if (error != null) return BadRequest(new { message = error });
|
||||||
if (data == null || !data.Any()) return NotFound(new { message = "No hay datos para el reporte de ventas 'El Plata'." });
|
if (data == null || !data.Any()) return NotFound(new { message = "No hay datos para el reporte de ventas 'El Plata'." });
|
||||||
@@ -591,7 +592,7 @@ namespace GestionIntegral.Api.Controllers
|
|||||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||||
public async Task<IActionResult> GetVentaMensualSecretariaElPlataPdf([FromQuery] DateTime fechaDesde, [FromQuery] DateTime fechaHasta)
|
public async Task<IActionResult> GetVentaMensualSecretariaElPlataPdf([FromQuery] DateTime fechaDesde, [FromQuery] DateTime fechaHasta)
|
||||||
{
|
{
|
||||||
if (!TienePermiso(PermisoVerListadoDistribucion)) return Forbid();
|
if (!TienePermiso(PermisoVerReportesSecretaria)) return Forbid();
|
||||||
|
|
||||||
var (data, error) = await _reportesService.ObtenerVentaMensualSecretariaElPlataAsync(fechaDesde, fechaHasta);
|
var (data, error) = await _reportesService.ObtenerVentaMensualSecretariaElPlataAsync(fechaDesde, fechaHasta);
|
||||||
|
|
||||||
@@ -628,7 +629,7 @@ namespace GestionIntegral.Api.Controllers
|
|||||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||||
public async Task<IActionResult> GetVentaMensualSecretariaTirDevo([FromQuery] DateTime fechaDesde, [FromQuery] DateTime fechaHasta)
|
public async Task<IActionResult> GetVentaMensualSecretariaTirDevo([FromQuery] DateTime fechaDesde, [FromQuery] DateTime fechaHasta)
|
||||||
{
|
{
|
||||||
if (!TienePermiso(PermisoVerListadoDistribucion)) return Forbid(); // Asumiendo RR002
|
if (!TienePermiso(PermisoVerReportesSecretaria)) return Forbid(); // Asumiendo RR002
|
||||||
var (data, error) = await _reportesService.ObtenerVentaMensualSecretariaTirDevoAsync(fechaDesde, fechaHasta);
|
var (data, error) = await _reportesService.ObtenerVentaMensualSecretariaTirDevoAsync(fechaDesde, fechaHasta);
|
||||||
if (error != null) return BadRequest(new { message = error });
|
if (error != null) return BadRequest(new { message = error });
|
||||||
if (data == null || !data.Any()) return NotFound(new { message = "No hay datos para el reporte de tirada/devolución." });
|
if (data == null || !data.Any()) return NotFound(new { message = "No hay datos para el reporte de tirada/devolución." });
|
||||||
@@ -642,7 +643,7 @@ namespace GestionIntegral.Api.Controllers
|
|||||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||||
public async Task<IActionResult> GetVentaMensualSecretariaTirDevoPdf([FromQuery] DateTime fechaDesde, [FromQuery] DateTime fechaHasta)
|
public async Task<IActionResult> GetVentaMensualSecretariaTirDevoPdf([FromQuery] DateTime fechaDesde, [FromQuery] DateTime fechaHasta)
|
||||||
{
|
{
|
||||||
if (!TienePermiso(PermisoVerListadoDistribucion)) return Forbid();
|
if (!TienePermiso(PermisoVerReportesSecretaria)) return Forbid();
|
||||||
|
|
||||||
var (data, error) = await _reportesService.ObtenerVentaMensualSecretariaTirDevoAsync(fechaDesde, fechaHasta);
|
var (data, error) = await _reportesService.ObtenerVentaMensualSecretariaTirDevoAsync(fechaDesde, fechaHasta);
|
||||||
|
|
||||||
@@ -677,13 +678,18 @@ namespace GestionIntegral.Api.Controllers
|
|||||||
[ProducesResponseType(StatusCodes.Status400BadRequest)]
|
[ProducesResponseType(StatusCodes.Status400BadRequest)]
|
||||||
[ProducesResponseType(StatusCodes.Status403Forbidden)]
|
[ProducesResponseType(StatusCodes.Status403Forbidden)]
|
||||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||||
public async Task<IActionResult> GetReporteDistribucionCanillasData([FromQuery] DateTime fecha, [FromQuery] int idEmpresa)
|
public async Task<IActionResult> GetReporteDistribucionCanillasData(
|
||||||
|
[FromQuery] DateTime fecha,
|
||||||
|
[FromQuery] int idEmpresa,
|
||||||
|
[FromQuery] bool? esAccionista
|
||||||
|
)
|
||||||
{
|
{
|
||||||
if (!TienePermiso(PermisoVerComprobanteLiquidacionCanilla)) return Forbid();
|
if (!TienePermiso(PermisoVerComprobanteLiquidacionCanilla)) return Forbid();
|
||||||
|
|
||||||
|
// Pasar el nuevo parámetro al servicio
|
||||||
var (canillas, canillasAcc, canillasAll, canillasFechaLiq, canillasAccFechaLiq,
|
var (canillas, canillasAcc, canillasAll, canillasFechaLiq, canillasAccFechaLiq,
|
||||||
ctrlDevolucionesRemitos, ctrlDevolucionesParaDistCan, ctrlDevolucionesOtrosDias, error) =
|
ctrlDevolucionesRemitos, ctrlDevolucionesParaDistCan, ctrlDevolucionesOtrosDias, error) =
|
||||||
await _reportesService.ObtenerReporteDistribucionCanillasAsync(fecha, idEmpresa);
|
await _reportesService.ObtenerReporteDistribucionCanillasAsync(fecha, idEmpresa, esAccionista);
|
||||||
|
|
||||||
if (error != null) return BadRequest(new { message = error });
|
if (error != null) return BadRequest(new { message = error });
|
||||||
|
|
||||||
@@ -718,14 +724,20 @@ namespace GestionIntegral.Api.Controllers
|
|||||||
[ProducesResponseType(StatusCodes.Status400BadRequest)]
|
[ProducesResponseType(StatusCodes.Status400BadRequest)]
|
||||||
[ProducesResponseType(StatusCodes.Status403Forbidden)]
|
[ProducesResponseType(StatusCodes.Status403Forbidden)]
|
||||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||||
public async Task<IActionResult> GetReporteDistribucionCanillasPdf([FromQuery] DateTime fecha, [FromQuery] int idEmpresa, [FromQuery] bool soloTotales = false)
|
public async Task<IActionResult> GetReporteDistribucionCanillasPdf(
|
||||||
|
[FromQuery] DateTime fecha,
|
||||||
|
[FromQuery] int idEmpresa,
|
||||||
|
[FromQuery] bool? esAccionista,
|
||||||
|
[FromQuery] bool soloTotales = false
|
||||||
|
)
|
||||||
{
|
{
|
||||||
if (!TienePermiso(PermisoVerComprobanteLiquidacionCanilla)) return Forbid();
|
if (!TienePermiso(PermisoVerComprobanteLiquidacionCanilla)) return Forbid();
|
||||||
|
|
||||||
|
// Pasar el nuevo parámetro al servicio
|
||||||
var (
|
var (
|
||||||
canillas, canillasAcc, canillasAll, canillasFechaLiq, canillasAccFechaLiq,
|
canillas, canillasAcc, canillasAll, canillasFechaLiq, canillasAccFechaLiq,
|
||||||
remitos, ctrlDevoluciones, _, error
|
remitos, ctrlDevoluciones, _, error
|
||||||
) = await _reportesService.ObtenerReporteDistribucionCanillasAsync(fecha, idEmpresa);
|
) = await _reportesService.ObtenerReporteDistribucionCanillasAsync(fecha, idEmpresa, esAccionista);
|
||||||
|
|
||||||
if (error != null) return BadRequest(new { message = error });
|
if (error != null) return BadRequest(new { message = error });
|
||||||
|
|
||||||
@@ -794,11 +806,11 @@ namespace GestionIntegral.Api.Controllers
|
|||||||
_, // canillasAll
|
_, // canillasAll
|
||||||
_, // canillasFechaLiq
|
_, // canillasFechaLiq
|
||||||
_, // canillasAccFechaLiq
|
_, // canillasAccFechaLiq
|
||||||
ctrlDevolucionesRemitosData, // Para SP_ObtenerCtrlDevoluciones -> DataSet "DSObtenerCtrlDevoluciones"
|
ctrlDevolucionesRemitosData,
|
||||||
ctrlDevolucionesParaDistCanData, // Para SP_DistCanillasCantidadEntradaSalida -> DataSet "DSCtrlDevoluciones"
|
ctrlDevolucionesParaDistCanData,
|
||||||
ctrlDevolucionesOtrosDiasData, // Para SP_DistCanillasCantidadEntradaSalidaOtrosDias -> DataSet "DSCtrlDevolucionesOtrosDias"
|
ctrlDevolucionesOtrosDiasData,
|
||||||
error
|
error
|
||||||
) = await _reportesService.ObtenerReporteDistribucionCanillasAsync(fecha, idEmpresa); // Reutilizamos este método
|
) = await _reportesService.ObtenerReporteDistribucionCanillasAsync(fecha, idEmpresa, null);
|
||||||
|
|
||||||
if (error != null) return BadRequest(new { message = error });
|
if (error != null) return BadRequest(new { message = error });
|
||||||
|
|
||||||
@@ -832,7 +844,7 @@ namespace GestionIntegral.Api.Controllers
|
|||||||
var (
|
var (
|
||||||
_, _, _, _, _, // Datos no utilizados
|
_, _, _, _, _, // Datos no utilizados
|
||||||
remitos, detalles, otrosDias, error
|
remitos, detalles, otrosDias, error
|
||||||
) = await _reportesService.ObtenerReporteDistribucionCanillasAsync(fecha, idEmpresa);
|
) = await _reportesService.ObtenerReporteDistribucionCanillasAsync(fecha, idEmpresa, null);
|
||||||
|
|
||||||
if (error != null) return BadRequest(new { message = error });
|
if (error != null) return BadRequest(new { message = error });
|
||||||
|
|
||||||
@@ -882,11 +894,11 @@ namespace GestionIntegral.Api.Controllers
|
|||||||
{
|
{
|
||||||
if (!TienePermiso(PermisoVerBalanceCuentas)) return Forbid();
|
if (!TienePermiso(PermisoVerBalanceCuentas)) return Forbid();
|
||||||
|
|
||||||
var (entradasSalidas, debitosCreditos, pagos, saldos, error) =
|
var (entradasSalidas, debitosCreditos, pagos, saldoInicial, error) =
|
||||||
await _reportesService.ObtenerReporteCuentasDistribuidorAsync(idDistribuidor, idEmpresa, fechaDesde, fechaHasta);
|
await _reportesService.ObtenerReporteCuentasDistribuidorAsync(idDistribuidor, idEmpresa, fechaDesde, fechaHasta);
|
||||||
|
|
||||||
if (error != null) return BadRequest(new { message = error });
|
if (error != null) return BadRequest(new { message = error });
|
||||||
if (!entradasSalidas.Any() && !debitosCreditos.Any() && !pagos.Any() && !saldos.Any())
|
if (!entradasSalidas.Any() && !debitosCreditos.Any() && !pagos.Any() && saldoInicial == 0m)
|
||||||
{
|
{
|
||||||
return NotFound(new { message = "No hay datos para generar el reporte de cuenta del distribuidor." });
|
return NotFound(new { message = "No hay datos para generar el reporte de cuenta del distribuidor." });
|
||||||
}
|
}
|
||||||
@@ -899,7 +911,7 @@ namespace GestionIntegral.Api.Controllers
|
|||||||
EntradasSalidas = entradasSalidas ?? Enumerable.Empty<BalanceCuentaDistDto>(),
|
EntradasSalidas = entradasSalidas ?? Enumerable.Empty<BalanceCuentaDistDto>(),
|
||||||
DebitosCreditos = debitosCreditos ?? Enumerable.Empty<BalanceCuentaDebCredDto>(),
|
DebitosCreditos = debitosCreditos ?? Enumerable.Empty<BalanceCuentaDebCredDto>(),
|
||||||
Pagos = pagos ?? Enumerable.Empty<BalanceCuentaPagosDto>(),
|
Pagos = pagos ?? Enumerable.Empty<BalanceCuentaPagosDto>(),
|
||||||
Saldos = saldos ?? Enumerable.Empty<SaldoDto>(),
|
SaldoInicial = saldoInicial,
|
||||||
NombreDistribuidor = distribuidor.Distribuidor?.Nombre,
|
NombreDistribuidor = distribuidor.Distribuidor?.Nombre,
|
||||||
NombreEmpresa = empresa?.Nombre
|
NombreEmpresa = empresa?.Nombre
|
||||||
};
|
};
|
||||||
@@ -920,11 +932,11 @@ namespace GestionIntegral.Api.Controllers
|
|||||||
{
|
{
|
||||||
if (!TienePermiso(PermisoVerBalanceCuentas)) return Forbid();
|
if (!TienePermiso(PermisoVerBalanceCuentas)) return Forbid();
|
||||||
|
|
||||||
var (entradasSalidas, debitosCreditos, pagos, saldos, error) =
|
var (entradasSalidas, debitosCreditos, pagos, saldoInicial, error) =
|
||||||
await _reportesService.ObtenerReporteCuentasDistribuidorAsync(idDistribuidor, idEmpresa, fechaDesde, fechaHasta);
|
await _reportesService.ObtenerReporteCuentasDistribuidorAsync(idDistribuidor, idEmpresa, fechaDesde, fechaHasta);
|
||||||
|
|
||||||
if (error != null) return BadRequest(new { message = error });
|
if (error != null) return BadRequest(new { message = error });
|
||||||
if (!entradasSalidas.Any() && !debitosCreditos.Any() && !pagos.Any())
|
if (!entradasSalidas.Any() && !debitosCreditos.Any() && !pagos.Any() && saldoInicial == 0m)
|
||||||
{
|
{
|
||||||
return NotFound(new { message = "No hay datos para generar el reporte de cuenta del distribuidor." });
|
return NotFound(new { message = "No hay datos para generar el reporte de cuenta del distribuidor." });
|
||||||
}
|
}
|
||||||
@@ -938,7 +950,7 @@ namespace GestionIntegral.Api.Controllers
|
|||||||
Movimientos = entradasSalidas,
|
Movimientos = entradasSalidas,
|
||||||
Pagos = pagos,
|
Pagos = pagos,
|
||||||
DebitosCreditos = debitosCreditos,
|
DebitosCreditos = debitosCreditos,
|
||||||
SaldoDeCuenta = saldos.FirstOrDefault()?.Monto ?? 0, // <-- Se asigna a SaldoDeCuenta
|
SaldoInicial = saldoInicial,
|
||||||
NombreDistribuidor = distribuidor.Distribuidor?.Nombre ?? $"Distribuidor ID {idDistribuidor}",
|
NombreDistribuidor = distribuidor.Distribuidor?.Nombre ?? $"Distribuidor ID {idDistribuidor}",
|
||||||
FechaDesde = fechaDesde.ToString("dd/MM/yyyy"),
|
FechaDesde = fechaDesde.ToString("dd/MM/yyyy"),
|
||||||
FechaHasta = fechaHasta.ToString("dd/MM/yyyy"),
|
FechaHasta = fechaHasta.ToString("dd/MM/yyyy"),
|
||||||
|
|||||||
@@ -72,15 +72,16 @@ namespace GestionIntegral.Api.Controllers.Suscripciones
|
|||||||
|
|
||||||
[HttpGet("{anio:int}/{mes:int}")]
|
[HttpGet("{anio:int}/{mes:int}")]
|
||||||
public async Task<IActionResult> GetFacturas(
|
public async Task<IActionResult> GetFacturas(
|
||||||
int anio, int mes,
|
int anio, int mes,
|
||||||
[FromQuery] string? nombreSuscriptor,
|
[FromQuery] string? nombreSuscriptor,
|
||||||
[FromQuery] string? estadoPago,
|
[FromQuery] string? estadoPago,
|
||||||
[FromQuery] string? estadoFacturacion)
|
[FromQuery] string? estadoFacturacion,
|
||||||
|
[FromQuery] string? tipoFactura)
|
||||||
{
|
{
|
||||||
if (!TienePermiso(PermisoGestionarFacturacion)) return Forbid();
|
if (!TienePermiso(PermisoGestionarFacturacion)) return Forbid();
|
||||||
if (anio < 2020 || mes < 1 || mes > 12) return BadRequest(new { message = "El período no es válido." });
|
if (anio < 2020 || mes < 1 || mes > 12) return BadRequest(new { message = "El período no es válido." });
|
||||||
|
|
||||||
var resumenes = await _facturacionService.ObtenerResumenesDeCuentaPorPeriodo(anio, mes, nombreSuscriptor, estadoPago, estadoFacturacion);
|
var resumenes = await _facturacionService.ObtenerResumenesDeCuentaPorPeriodo(anio, mes, nombreSuscriptor, estadoPago, estadoFacturacion, tipoFactura);
|
||||||
return Ok(resumenes);
|
return Ok(resumenes);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,435 @@
|
|||||||
|
using Dapper;
|
||||||
|
using GestionIntegral.Api.Dtos.Auditoria;
|
||||||
|
using GestionIntegral.Api.Dtos.Contables;
|
||||||
|
using GestionIntegral.Api.Models.Contables;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Data;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace GestionIntegral.Api.Data.Repositories.Contables
|
||||||
|
{
|
||||||
|
public class CierreCuentaCorrienteRepository : ICierreCuentaCorrienteRepository
|
||||||
|
{
|
||||||
|
private readonly DbConnectionFactory _cf;
|
||||||
|
private readonly ILogger<CierreCuentaCorrienteRepository> _log;
|
||||||
|
|
||||||
|
public CierreCuentaCorrienteRepository(DbConnectionFactory cf, ILogger<CierreCuentaCorrienteRepository> log)
|
||||||
|
{
|
||||||
|
_cf = cf;
|
||||||
|
_log = log;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Aliases SELECT — mapean Id_X (SQL) → IdX (modelo C#).
|
||||||
|
private const string SelectModelBase = @"
|
||||||
|
SELECT
|
||||||
|
Id_Cierre AS IdCierre,
|
||||||
|
Id_Distribuidor AS IdDistribuidor,
|
||||||
|
Id_Empresa AS IdEmpresa,
|
||||||
|
FechaCorte,
|
||||||
|
FechaCierre,
|
||||||
|
SaldoCierre,
|
||||||
|
Estado,
|
||||||
|
Justificacion,
|
||||||
|
Id_Usuario_Cierre AS IdUsuarioCierre,
|
||||||
|
Id_Usuario_Anula AS IdUsuarioAnula,
|
||||||
|
FechaAnulacion,
|
||||||
|
Justificacion_Anulacion AS JustificacionAnulacion
|
||||||
|
FROM dbo.cue_CierresCuentaCorriente";
|
||||||
|
|
||||||
|
public async Task<CierreCuentaCorriente?> GetByIdAsync(int idCierre)
|
||||||
|
{
|
||||||
|
var sql = SelectModelBase + " WHERE Id_Cierre = @IdCierreParam;";
|
||||||
|
try
|
||||||
|
{
|
||||||
|
using var connection = _cf.CreateConnection();
|
||||||
|
return await connection.QuerySingleOrDefaultAsync<CierreCuentaCorriente>(sql, new { IdCierreParam = idCierre });
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_log.LogError(ex, "Error al obtener Cierre por ID: {IdCierre}", idCierre);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<CierreCuentaCorriente?> GetUltimoCierreVigenteAsync(int idDistribuidor, int idEmpresa, IDbTransaction? transaction = null)
|
||||||
|
{
|
||||||
|
var sql = @"
|
||||||
|
SELECT TOP 1
|
||||||
|
Id_Cierre AS IdCierre,
|
||||||
|
Id_Distribuidor AS IdDistribuidor,
|
||||||
|
Id_Empresa AS IdEmpresa,
|
||||||
|
FechaCorte,
|
||||||
|
FechaCierre,
|
||||||
|
SaldoCierre,
|
||||||
|
Estado,
|
||||||
|
Justificacion,
|
||||||
|
Id_Usuario_Cierre AS IdUsuarioCierre,
|
||||||
|
Id_Usuario_Anula AS IdUsuarioAnula,
|
||||||
|
FechaAnulacion,
|
||||||
|
Justificacion_Anulacion AS JustificacionAnulacion
|
||||||
|
FROM dbo.cue_CierresCuentaCorriente
|
||||||
|
WHERE Id_Distribuidor = @IdDist
|
||||||
|
AND Id_Empresa = @IdEmp
|
||||||
|
AND Estado = 'Activo'
|
||||||
|
ORDER BY FechaCorte DESC, Id_Cierre DESC;";
|
||||||
|
|
||||||
|
var parameters = new { IdDist = idDistribuidor, IdEmp = idEmpresa };
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (transaction != null)
|
||||||
|
{
|
||||||
|
return await transaction.Connection!.QuerySingleOrDefaultAsync<CierreCuentaCorriente>(sql, parameters, transaction);
|
||||||
|
}
|
||||||
|
|
||||||
|
using var connection = _cf.CreateConnection();
|
||||||
|
return await connection.QuerySingleOrDefaultAsync<CierreCuentaCorriente>(sql, parameters);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_log.LogError(ex, "Error al obtener último cierre vigente Dist={IdDist} Emp={IdEmp}", idDistribuidor, idEmpresa);
|
||||||
|
if (transaction != null) throw;
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<CierreCuentaCorriente?> GetCierreVigenteParaFechaAsync(int idDistribuidor, int idEmpresa, DateTime fechaOperacion)
|
||||||
|
{
|
||||||
|
// Cae en período cerrado si existe un cierre Activo cuya FechaCorte sea >= fechaOperacion (la fecha está dentro del período cerrado).
|
||||||
|
// Se devuelve el más reciente (TOP 1 ORDER BY FechaCorte DESC) — el más restrictivo desde la perspectiva de la fecha consultada.
|
||||||
|
const string sql = @"
|
||||||
|
SELECT TOP 1
|
||||||
|
Id_Cierre AS IdCierre,
|
||||||
|
Id_Distribuidor AS IdDistribuidor,
|
||||||
|
Id_Empresa AS IdEmpresa,
|
||||||
|
FechaCorte,
|
||||||
|
FechaCierre,
|
||||||
|
SaldoCierre,
|
||||||
|
Estado,
|
||||||
|
Justificacion,
|
||||||
|
Id_Usuario_Cierre AS IdUsuarioCierre,
|
||||||
|
Id_Usuario_Anula AS IdUsuarioAnula,
|
||||||
|
FechaAnulacion,
|
||||||
|
Justificacion_Anulacion AS JustificacionAnulacion
|
||||||
|
FROM dbo.cue_CierresCuentaCorriente
|
||||||
|
WHERE Id_Distribuidor = @IdDist
|
||||||
|
AND Id_Empresa = @IdEmp
|
||||||
|
AND Estado = 'Activo'
|
||||||
|
AND FechaCorte >= @FechaOp
|
||||||
|
ORDER BY FechaCorte DESC, Id_Cierre DESC;";
|
||||||
|
try
|
||||||
|
{
|
||||||
|
using var connection = _cf.CreateConnection();
|
||||||
|
return await connection.QuerySingleOrDefaultAsync<CierreCuentaCorriente>(sql, new
|
||||||
|
{
|
||||||
|
IdDist = idDistribuidor,
|
||||||
|
IdEmp = idEmpresa,
|
||||||
|
FechaOp = fechaOperacion.Date
|
||||||
|
});
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_log.LogError(ex, "Error en GetCierreVigenteParaFechaAsync Dist={IdDist} Emp={IdEmp} Fecha={Fecha}", idDistribuidor, idEmpresa, fechaOperacion);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<bool> ExisteCierrePosteriorVigenteAsync(int idDistribuidor, int idEmpresa, DateTime fechaCorte, int? excluirIdCierre = null, IDbTransaction? transaction = null)
|
||||||
|
{
|
||||||
|
const string sql = @"
|
||||||
|
SELECT CASE WHEN EXISTS (
|
||||||
|
SELECT 1 FROM dbo.cue_CierresCuentaCorriente
|
||||||
|
WHERE Id_Distribuidor = @IdDist
|
||||||
|
AND Id_Empresa = @IdEmp
|
||||||
|
AND Estado = 'Activo'
|
||||||
|
AND FechaCorte > @FechaCorte
|
||||||
|
AND (@Excluir IS NULL OR Id_Cierre <> @Excluir)
|
||||||
|
) THEN 1 ELSE 0 END;";
|
||||||
|
|
||||||
|
var parameters = new
|
||||||
|
{
|
||||||
|
IdDist = idDistribuidor,
|
||||||
|
IdEmp = idEmpresa,
|
||||||
|
FechaCorte = fechaCorte.Date,
|
||||||
|
Excluir = excluirIdCierre
|
||||||
|
};
|
||||||
|
|
||||||
|
if (transaction != null)
|
||||||
|
{
|
||||||
|
return await transaction.Connection!.ExecuteScalarAsync<bool>(sql, parameters, transaction);
|
||||||
|
}
|
||||||
|
|
||||||
|
using var connection = _cf.CreateConnection();
|
||||||
|
return await connection.ExecuteScalarAsync<bool>(sql, parameters);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<int> CreateAsync(CierreCuentaCorriente cierre, int idUsuarioMod, IDbTransaction transaction)
|
||||||
|
{
|
||||||
|
const string sqlInsertMaster = @"
|
||||||
|
INSERT INTO dbo.cue_CierresCuentaCorriente
|
||||||
|
(Id_Distribuidor, Id_Empresa, FechaCorte, FechaCierre, SaldoCierre, Estado, Justificacion, Id_Usuario_Cierre)
|
||||||
|
VALUES
|
||||||
|
(@IdDistribuidor, @IdEmpresa, @FechaCorte, @FechaCierre, @SaldoCierre, @Estado, @Justificacion, @IdUsuarioCierre);
|
||||||
|
SELECT CAST(SCOPE_IDENTITY() AS INT);";
|
||||||
|
|
||||||
|
var idCierre = await transaction.Connection!.ExecuteScalarAsync<int>(sqlInsertMaster, new
|
||||||
|
{
|
||||||
|
cierre.IdDistribuidor,
|
||||||
|
cierre.IdEmpresa,
|
||||||
|
FechaCorte = cierre.FechaCorte.Date,
|
||||||
|
cierre.FechaCierre,
|
||||||
|
cierre.SaldoCierre,
|
||||||
|
cierre.Estado,
|
||||||
|
cierre.Justificacion,
|
||||||
|
cierre.IdUsuarioCierre
|
||||||
|
}, transaction);
|
||||||
|
|
||||||
|
if (idCierre <= 0) throw new DataException("No se pudo crear el cierre — SCOPE_IDENTITY no devolvió valor.");
|
||||||
|
|
||||||
|
const string sqlInsertHist = @"
|
||||||
|
INSERT INTO dbo.cue_CierresCuentaCorriente_H
|
||||||
|
(Id_Cierre, Id_Distribuidor, Id_Empresa, FechaCorte, FechaCierre, SaldoCierre,
|
||||||
|
Estado, Justificacion, Id_Usuario_Cierre,
|
||||||
|
Id_Usuario_Anula, FechaAnulacion, Justificacion_Anulacion,
|
||||||
|
TipoMod, Id_Usuario_Mod, FechaMod)
|
||||||
|
VALUES
|
||||||
|
(@IdCierre, @IdDistribuidor, @IdEmpresa, @FechaCorte, @FechaCierre, @SaldoCierre,
|
||||||
|
@Estado, @Justificacion, @IdUsuarioCierre,
|
||||||
|
NULL, NULL, NULL,
|
||||||
|
@TipoMod, @IdUsuarioMod, @FechaMod);";
|
||||||
|
|
||||||
|
await transaction.Connection!.ExecuteAsync(sqlInsertHist, new
|
||||||
|
{
|
||||||
|
IdCierre = idCierre,
|
||||||
|
cierre.IdDistribuidor,
|
||||||
|
cierre.IdEmpresa,
|
||||||
|
FechaCorte = cierre.FechaCorte.Date,
|
||||||
|
cierre.FechaCierre,
|
||||||
|
cierre.SaldoCierre,
|
||||||
|
cierre.Estado,
|
||||||
|
cierre.Justificacion,
|
||||||
|
cierre.IdUsuarioCierre,
|
||||||
|
TipoMod = "Creacion",
|
||||||
|
IdUsuarioMod = idUsuarioMod,
|
||||||
|
FechaMod = DateTime.Now
|
||||||
|
}, transaction);
|
||||||
|
|
||||||
|
return idCierre;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<bool> AnularAsync(int idCierre, int idUsuarioAnula, string justificacionAnulacion, int idUsuarioMod, IDbTransaction transaction)
|
||||||
|
{
|
||||||
|
// UPDATE atómico: solo cambia Estado si está actualmente Activo. Evita doble anulación concurrente.
|
||||||
|
const string sqlUpdate = @"
|
||||||
|
UPDATE dbo.cue_CierresCuentaCorriente
|
||||||
|
SET Estado = 'Anulado',
|
||||||
|
Id_Usuario_Anula = @IdUsuarioAnula,
|
||||||
|
FechaAnulacion = @FechaAnulacion,
|
||||||
|
Justificacion_Anulacion = @JustificacionAnulacion
|
||||||
|
WHERE Id_Cierre = @IdCierre
|
||||||
|
AND Estado = 'Activo';";
|
||||||
|
|
||||||
|
var fechaAnulacion = DateTime.Now;
|
||||||
|
|
||||||
|
int affected = await transaction.Connection!.ExecuteAsync(sqlUpdate, new
|
||||||
|
{
|
||||||
|
IdCierre = idCierre,
|
||||||
|
IdUsuarioAnula = idUsuarioAnula,
|
||||||
|
FechaAnulacion = fechaAnulacion,
|
||||||
|
JustificacionAnulacion = justificacionAnulacion
|
||||||
|
}, transaction);
|
||||||
|
|
||||||
|
if (affected != 1) return false;
|
||||||
|
|
||||||
|
// Snapshot post-update para el _H. Trae los valores ya actualizados.
|
||||||
|
const string sqlSnapshot = SelectModelBase + " WHERE Id_Cierre = @IdCierre;";
|
||||||
|
var actualizado = await transaction.Connection!.QuerySingleAsync<CierreCuentaCorriente>(sqlSnapshot, new { IdCierre = idCierre }, transaction);
|
||||||
|
|
||||||
|
const string sqlInsertHist = @"
|
||||||
|
INSERT INTO dbo.cue_CierresCuentaCorriente_H
|
||||||
|
(Id_Cierre, Id_Distribuidor, Id_Empresa, FechaCorte, FechaCierre, SaldoCierre,
|
||||||
|
Estado, Justificacion, Id_Usuario_Cierre,
|
||||||
|
Id_Usuario_Anula, FechaAnulacion, Justificacion_Anulacion,
|
||||||
|
TipoMod, Id_Usuario_Mod, FechaMod)
|
||||||
|
VALUES
|
||||||
|
(@IdCierre, @IdDistribuidor, @IdEmpresa, @FechaCorte, @FechaCierre, @SaldoCierre,
|
||||||
|
@Estado, @Justificacion, @IdUsuarioCierre,
|
||||||
|
@IdUsuarioAnula, @FechaAnulacion, @JustificacionAnulacion,
|
||||||
|
@TipoMod, @IdUsuarioMod, @FechaMod);";
|
||||||
|
|
||||||
|
await transaction.Connection!.ExecuteAsync(sqlInsertHist, new
|
||||||
|
{
|
||||||
|
IdCierre = actualizado.IdCierre,
|
||||||
|
actualizado.IdDistribuidor,
|
||||||
|
actualizado.IdEmpresa,
|
||||||
|
FechaCorte = actualizado.FechaCorte.Date,
|
||||||
|
actualizado.FechaCierre,
|
||||||
|
actualizado.SaldoCierre,
|
||||||
|
actualizado.Estado,
|
||||||
|
actualizado.Justificacion,
|
||||||
|
actualizado.IdUsuarioCierre,
|
||||||
|
actualizado.IdUsuarioAnula,
|
||||||
|
actualizado.FechaAnulacion,
|
||||||
|
JustificacionAnulacion = actualizado.JustificacionAnulacion,
|
||||||
|
TipoMod = "Reapertura",
|
||||||
|
IdUsuarioMod = idUsuarioMod,
|
||||||
|
FechaMod = DateTime.Now
|
||||||
|
}, transaction);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<IEnumerable<CierreCuentaCorrienteDto>> GetAllAsync(
|
||||||
|
int? idDistribuidor, int? idEmpresa, string? estado,
|
||||||
|
DateTime? fechaCorteDesde, DateTime? fechaCorteHasta)
|
||||||
|
{
|
||||||
|
var sqlBuilder = new StringBuilder(@"
|
||||||
|
SELECT
|
||||||
|
c.Id_Cierre AS IdCierre,
|
||||||
|
c.Id_Distribuidor AS IdDistribuidor,
|
||||||
|
d.Nombre AS NombreDistribuidor,
|
||||||
|
c.Id_Empresa AS IdEmpresa,
|
||||||
|
e.Nombre AS NombreEmpresa,
|
||||||
|
CONVERT(varchar(10), c.FechaCorte, 23) AS FechaCorte,
|
||||||
|
c.FechaCierre,
|
||||||
|
c.SaldoCierre,
|
||||||
|
c.Estado,
|
||||||
|
c.Justificacion,
|
||||||
|
c.Id_Usuario_Cierre AS IdUsuarioCierre,
|
||||||
|
(uc.Nombre + ' ' + uc.Apellido) AS NombreUsuarioCierre,
|
||||||
|
c.Id_Usuario_Anula AS IdUsuarioAnula,
|
||||||
|
CASE WHEN ua.Id IS NULL THEN NULL ELSE (ua.Nombre + ' ' + ua.Apellido) END AS NombreUsuarioAnula,
|
||||||
|
c.FechaAnulacion,
|
||||||
|
c.Justificacion_Anulacion AS JustificacionAnulacion,
|
||||||
|
CAST(CASE WHEN c.Estado = 'Activo'
|
||||||
|
AND c.Id_Cierre = (
|
||||||
|
SELECT TOP 1 c2.Id_Cierre FROM dbo.cue_CierresCuentaCorriente c2
|
||||||
|
WHERE c2.Id_Distribuidor = c.Id_Distribuidor
|
||||||
|
AND c2.Id_Empresa = c.Id_Empresa
|
||||||
|
AND c2.Estado = 'Activo'
|
||||||
|
ORDER BY c2.FechaCorte DESC, c2.Id_Cierre DESC)
|
||||||
|
THEN 1 ELSE 0 END AS bit) AS EsUltimoVigente
|
||||||
|
FROM dbo.cue_CierresCuentaCorriente c
|
||||||
|
JOIN dbo.dist_dtDistribuidores d ON d.Id_Distribuidor = c.Id_Distribuidor
|
||||||
|
JOIN dbo.dist_dtEmpresas e ON e.Id_Empresa = c.Id_Empresa
|
||||||
|
JOIN dbo.gral_Usuarios uc ON uc.Id = c.Id_Usuario_Cierre
|
||||||
|
LEFT JOIN dbo.gral_Usuarios ua ON ua.Id = c.Id_Usuario_Anula
|
||||||
|
WHERE 1=1");
|
||||||
|
|
||||||
|
var parameters = new DynamicParameters();
|
||||||
|
if (idDistribuidor.HasValue) { sqlBuilder.Append(" AND c.Id_Distribuidor = @IdDist"); parameters.Add("IdDist", idDistribuidor.Value); }
|
||||||
|
if (idEmpresa.HasValue) { sqlBuilder.Append(" AND c.Id_Empresa = @IdEmp"); parameters.Add("IdEmp", idEmpresa.Value); }
|
||||||
|
if (!string.IsNullOrWhiteSpace(estado)) { sqlBuilder.Append(" AND c.Estado = @Estado"); parameters.Add("Estado", estado); }
|
||||||
|
if (fechaCorteDesde.HasValue) { sqlBuilder.Append(" AND c.FechaCorte >= @FechaDesde"); parameters.Add("FechaDesde", fechaCorteDesde.Value.Date); }
|
||||||
|
if (fechaCorteHasta.HasValue) { sqlBuilder.Append(" AND c.FechaCorte <= @FechaHasta"); parameters.Add("FechaHasta", fechaCorteHasta.Value.Date); }
|
||||||
|
sqlBuilder.Append(" ORDER BY c.FechaCorte DESC, c.Id_Cierre DESC;");
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
using var connection = _cf.CreateConnection();
|
||||||
|
return await connection.QueryAsync<CierreCuentaCorrienteDto>(sqlBuilder.ToString(), parameters);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_log.LogError(ex, "Error en GetAllAsync de CierresCuentaCorriente.");
|
||||||
|
return Enumerable.Empty<CierreCuentaCorrienteDto>();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<IEnumerable<CierreCuentaCorrienteHistorialDto>> GetHistorialAsync(int idCierre)
|
||||||
|
{
|
||||||
|
const string sql = @"
|
||||||
|
SELECT
|
||||||
|
h.Id_Historial,
|
||||||
|
h.Id_Cierre,
|
||||||
|
h.Id_Distribuidor,
|
||||||
|
h.Id_Empresa,
|
||||||
|
h.FechaCorte,
|
||||||
|
h.FechaCierre,
|
||||||
|
h.SaldoCierre,
|
||||||
|
h.Estado,
|
||||||
|
h.Justificacion,
|
||||||
|
h.Id_Usuario_Cierre,
|
||||||
|
h.Id_Usuario_Anula,
|
||||||
|
h.FechaAnulacion,
|
||||||
|
h.Justificacion_Anulacion,
|
||||||
|
h.TipoMod,
|
||||||
|
h.Id_Usuario_Mod,
|
||||||
|
(u.Nombre + ' ' + u.Apellido) AS NombreUsuarioModifico,
|
||||||
|
h.FechaMod
|
||||||
|
FROM dbo.cue_CierresCuentaCorriente_H h
|
||||||
|
JOIN dbo.gral_Usuarios u ON u.Id = h.Id_Usuario_Mod
|
||||||
|
WHERE h.Id_Cierre = @IdCierre
|
||||||
|
ORDER BY h.FechaMod ASC, h.Id_Historial ASC;";
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
using var connection = _cf.CreateConnection();
|
||||||
|
return await connection.QueryAsync<CierreCuentaCorrienteHistorialDto>(sql, new { IdCierre = idCierre });
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_log.LogError(ex, "Error en GetHistorialAsync para Cierre ID {IdCierre}", idCierre);
|
||||||
|
return Enumerable.Empty<CierreCuentaCorrienteHistorialDto>();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<IEnumerable<CierreCuentaCorrienteHistorialDto>> ObtenerHistorialAsync(
|
||||||
|
DateTime? fechaDesde, DateTime? fechaHasta,
|
||||||
|
int? idUsuarioModifico, string? tipoModificacion,
|
||||||
|
int? idCierreAfectado)
|
||||||
|
{
|
||||||
|
// FechaMod cubre el rango inclusive: [fechaDesde 00:00, fechaHasta+1día). Patrón consistente con otros ObtenerHistorial del proyecto.
|
||||||
|
var sql = @"
|
||||||
|
SELECT
|
||||||
|
h.Id_Historial,
|
||||||
|
h.Id_Cierre,
|
||||||
|
h.Id_Distribuidor,
|
||||||
|
h.Id_Empresa,
|
||||||
|
h.FechaCorte,
|
||||||
|
h.FechaCierre,
|
||||||
|
h.SaldoCierre,
|
||||||
|
h.Estado,
|
||||||
|
h.Justificacion,
|
||||||
|
h.Id_Usuario_Cierre,
|
||||||
|
h.Id_Usuario_Anula,
|
||||||
|
h.FechaAnulacion,
|
||||||
|
h.Justificacion_Anulacion,
|
||||||
|
h.TipoMod,
|
||||||
|
h.Id_Usuario_Mod,
|
||||||
|
(u.Nombre + ' ' + u.Apellido) AS NombreUsuarioModifico,
|
||||||
|
h.FechaMod
|
||||||
|
FROM dbo.cue_CierresCuentaCorriente_H h
|
||||||
|
JOIN dbo.gral_Usuarios u ON u.Id = h.Id_Usuario_Mod
|
||||||
|
WHERE 1 = 1
|
||||||
|
AND (@FechaDesde IS NULL OR h.FechaMod >= @FechaDesde)
|
||||||
|
AND (@FechaHasta IS NULL OR h.FechaMod < DATEADD(DAY, 1, @FechaHasta))
|
||||||
|
AND (@IdUsuarioMod IS NULL OR h.Id_Usuario_Mod = @IdUsuarioMod)
|
||||||
|
AND (@TipoMod IS NULL OR h.TipoMod = @TipoMod)
|
||||||
|
AND (@IdCierre IS NULL OR h.Id_Cierre = @IdCierre)
|
||||||
|
ORDER BY h.FechaMod DESC, h.Id_Historial DESC;";
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
using var connection = _cf.CreateConnection();
|
||||||
|
return await connection.QueryAsync<CierreCuentaCorrienteHistorialDto>(sql, new
|
||||||
|
{
|
||||||
|
FechaDesde = fechaDesde?.Date,
|
||||||
|
FechaHasta = fechaHasta?.Date,
|
||||||
|
IdUsuarioMod = idUsuarioModifico,
|
||||||
|
TipoMod = tipoModificacion,
|
||||||
|
IdCierre = idCierreAfectado
|
||||||
|
});
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_log.LogError(ex, "Error en ObtenerHistorialAsync (auditoría) de Cierres CC.");
|
||||||
|
return Enumerable.Empty<CierreCuentaCorrienteHistorialDto>();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
using GestionIntegral.Api.Dtos.Auditoria;
|
||||||
|
using GestionIntegral.Api.Dtos.Contables;
|
||||||
|
using GestionIntegral.Api.Models.Contables;
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Data;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace GestionIntegral.Api.Data.Repositories.Contables
|
||||||
|
{
|
||||||
|
public interface ICierreCuentaCorrienteRepository
|
||||||
|
{
|
||||||
|
Task<CierreCuentaCorriente?> GetByIdAsync(int idCierre);
|
||||||
|
|
||||||
|
// Devuelve el último cierre con Estado = 'Activo' para el par (Distribuidor + Empresa).
|
||||||
|
// Acepta una transacción opcional para usarse dentro del flujo Crear/Reabrir.
|
||||||
|
Task<CierreCuentaCorriente?> GetUltimoCierreVigenteAsync(int idDistribuidor, int idEmpresa, IDbTransaction? transaction = null);
|
||||||
|
|
||||||
|
// Verifica si la fecha cae dentro de un período cerrado: existe un cierre Activo con FechaCorte >= fechaOperacion.
|
||||||
|
Task<CierreCuentaCorriente?> GetCierreVigenteParaFechaAsync(int idDistribuidor, int idEmpresa, DateTime fechaOperacion);
|
||||||
|
|
||||||
|
// True si existe otro cierre Activo con FechaCorte > fechaCorte (excluyendo opcionalmente un cierre puntual).
|
||||||
|
// Se usa al reabrir para forzar la cascada manual.
|
||||||
|
Task<bool> ExisteCierrePosteriorVigenteAsync(int idDistribuidor, int idEmpresa, DateTime fechaCorte, int? excluirIdCierre = null, IDbTransaction? transaction = null);
|
||||||
|
|
||||||
|
// Crea la fila maestra y registra la entrada inicial en _H con TipoMod='Creacion'. Devuelve el Id_Cierre generado.
|
||||||
|
Task<int> CreateAsync(CierreCuentaCorriente cierre, int idUsuarioMod, IDbTransaction transaction);
|
||||||
|
|
||||||
|
// Marca un cierre como Anulado (UPDATE atómico solo si Estado='Activo') y registra entrada en _H con TipoMod='Reapertura'.
|
||||||
|
// Devuelve true si se anuló (Estado pasó de Activo a Anulado), false si ya estaba anulado o no existía.
|
||||||
|
Task<bool> AnularAsync(int idCierre, int idUsuarioAnula, string justificacionAnulacion, int idUsuarioMod, IDbTransaction transaction);
|
||||||
|
|
||||||
|
Task<IEnumerable<CierreCuentaCorrienteDto>> GetAllAsync(
|
||||||
|
int? idDistribuidor, int? idEmpresa, string? estado,
|
||||||
|
DateTime? fechaCorteDesde, DateTime? fechaCorteHasta);
|
||||||
|
|
||||||
|
Task<IEnumerable<CierreCuentaCorrienteHistorialDto>> GetHistorialAsync(int idCierre);
|
||||||
|
|
||||||
|
// Auditoría general: filtra el historial cruzado de todos los cierres por rango de FechaMod, usuario, tipo, y opcional Id_Cierre.
|
||||||
|
Task<IEnumerable<CierreCuentaCorrienteHistorialDto>> ObtenerHistorialAsync(
|
||||||
|
DateTime? fechaDesde, DateTime? fechaHasta,
|
||||||
|
int? idUsuarioModifico, string? tipoModificacion,
|
||||||
|
int? idCierreAfectado);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -16,7 +16,7 @@ namespace GestionIntegral.Api.Data.Repositories.Contables
|
|||||||
Task<PagoDistribuidor?> CreateAsync(PagoDistribuidor nuevoPago, int idUsuario, IDbTransaction transaction);
|
Task<PagoDistribuidor?> CreateAsync(PagoDistribuidor nuevoPago, int idUsuario, IDbTransaction transaction);
|
||||||
Task<bool> UpdateAsync(PagoDistribuidor pagoAActualizar, int idUsuario, IDbTransaction transaction);
|
Task<bool> UpdateAsync(PagoDistribuidor pagoAActualizar, int idUsuario, IDbTransaction transaction);
|
||||||
Task<bool> DeleteAsync(int idPago, int idUsuario, IDbTransaction transaction);
|
Task<bool> DeleteAsync(int idPago, int idUsuario, IDbTransaction transaction);
|
||||||
Task<bool> ExistsByReciboAndTipoMovimientoAsync(int recibo, string tipoMovimiento, int? excludeIdPago = null);
|
Task<PagoDistribuidor?> GetByReciboAndTipoMovimientoAsync(int recibo, string tipoMovimiento, int? excludeIdPago = null);
|
||||||
Task<IEnumerable<(PagoDistribuidorHistorico Historial, string NombreUsuarioModifico)>> GetHistorialAsync(
|
Task<IEnumerable<(PagoDistribuidorHistorico Historial, string NombreUsuarioModifico)>> GetHistorialAsync(
|
||||||
DateTime? fechaDesde, DateTime? fechaHasta,
|
DateTime? fechaDesde, DateTime? fechaHasta,
|
||||||
int? idUsuarioModifico, string? tipoModificacion,
|
int? idUsuarioModifico, string? tipoModificacion,
|
||||||
|
|||||||
@@ -70,9 +70,10 @@ namespace GestionIntegral.Api.Data.Repositories.Contables
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<bool> ExistsByReciboAndTipoMovimientoAsync(int recibo, string tipoMovimiento, int? excludeIdPago = null)
|
public async Task<PagoDistribuidor?> GetByReciboAndTipoMovimientoAsync(int recibo, string tipoMovimiento, int? excludeIdPago = null)
|
||||||
{
|
{
|
||||||
var sqlBuilder = new StringBuilder("SELECT COUNT(1) FROM dbo.cue_PagosDistribuidor WHERE Recibo = @ReciboParam AND TipoMovimiento = @TipoMovParam");
|
var sqlBuilder = new StringBuilder(SelectQueryBase()); // Reutiliza la consulta base
|
||||||
|
sqlBuilder.Append(" WHERE Recibo = @ReciboParam AND TipoMovimiento = @TipoMovParam");
|
||||||
var parameters = new DynamicParameters();
|
var parameters = new DynamicParameters();
|
||||||
parameters.Add("ReciboParam", recibo);
|
parameters.Add("ReciboParam", recibo);
|
||||||
parameters.Add("TipoMovParam", tipoMovimiento);
|
parameters.Add("TipoMovParam", tipoMovimiento);
|
||||||
@@ -85,12 +86,12 @@ namespace GestionIntegral.Api.Data.Repositories.Contables
|
|||||||
try
|
try
|
||||||
{
|
{
|
||||||
using var connection = _cf.CreateConnection();
|
using var connection = _cf.CreateConnection();
|
||||||
return await connection.ExecuteScalarAsync<bool>(sqlBuilder.ToString(), parameters);
|
return await connection.QuerySingleOrDefaultAsync<PagoDistribuidor>(sqlBuilder.ToString(), parameters);
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
_log.LogError(ex, "Error en ExistsByReciboAndTipoMovimientoAsync. Recibo: {Recibo}, Tipo: {Tipo}", recibo, tipoMovimiento);
|
_log.LogError(ex, "Error en GetByReciboAndTipoMovimientoAsync. Recibo: {Recibo}, Tipo: {Tipo}", recibo, tipoMovimiento);
|
||||||
return true; // Asumir que existe en caso de error para prevenir duplicados
|
throw; // Relanzar para que el servicio lo maneje
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ namespace GestionIntegral.Api.Data.Repositories.Contables
|
|||||||
|
|
||||||
public async Task<IEnumerable<int>> GetAllDistribuidorIdsAsync()
|
public async Task<IEnumerable<int>> GetAllDistribuidorIdsAsync()
|
||||||
{
|
{
|
||||||
var sql = "SELECT Id_Distribuidor FROM dbo.dist_dtDistribuidores";
|
var sql = "SELECT Id_Distribuidor FROM dbo.dist_dtDistribuidores WHERE Baja = 0";
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
using (var connection = _connectionFactory.CreateConnection())
|
using (var connection = _connectionFactory.CreateConnection())
|
||||||
@@ -138,25 +138,45 @@ namespace GestionIntegral.Api.Data.Repositories.Contables
|
|||||||
|
|
||||||
public async Task<IEnumerable<Saldo>> GetSaldosParaGestionAsync(string? destinoFilter, int? idDestinoFilter, int? idEmpresaFilter)
|
public async Task<IEnumerable<Saldo>> GetSaldosParaGestionAsync(string? destinoFilter, int? idDestinoFilter, int? idEmpresaFilter)
|
||||||
{
|
{
|
||||||
var sqlBuilder = new StringBuilder("SELECT Id_Saldo AS IdSaldo, Destino, Id_Destino AS IdDestino, Monto, Id_Empresa AS IdEmpresa, FechaUltimaModificacion FROM dbo.cue_Saldos WHERE 1=1");
|
var sqlBuilder = new StringBuilder(@"
|
||||||
|
SELECT Id_Saldo AS IdSaldo, Destino, Id_Destino AS IdDestino, Monto, Id_Empresa AS IdEmpresa, FechaUltimaModificacion
|
||||||
|
FROM dbo.cue_Saldos s
|
||||||
|
WHERE 1=1");
|
||||||
|
|
||||||
var parameters = new DynamicParameters();
|
var parameters = new DynamicParameters();
|
||||||
|
|
||||||
if (!string.IsNullOrWhiteSpace(destinoFilter))
|
if (!string.IsNullOrWhiteSpace(destinoFilter))
|
||||||
{
|
{
|
||||||
sqlBuilder.Append(" AND Destino = @Destino");
|
sqlBuilder.Append(" AND s.Destino = @Destino");
|
||||||
parameters.Add("Destino", destinoFilter);
|
parameters.Add("Destino", destinoFilter);
|
||||||
|
|
||||||
|
// Filtro para excluir distribuidores de baja si el tipo es Distribuidores
|
||||||
|
// No se aplica a Canillas por requerimiento explícito del usuario
|
||||||
|
if (destinoFilter == "Distribuidores")
|
||||||
|
{
|
||||||
|
sqlBuilder.Append(" AND EXISTS (SELECT 1 FROM dbo.dist_dtDistribuidores d WHERE d.Id_Distribuidor = s.Id_Destino AND d.Baja = 0)");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// Si no hay filtro de destino, aplicamos el filtro de baja solo para Distribuidores
|
||||||
|
sqlBuilder.Append(@" AND (
|
||||||
|
(s.Destino = 'Distribuidores' AND EXISTS (SELECT 1 FROM dbo.dist_dtDistribuidores d WHERE d.Id_Distribuidor = s.Id_Destino AND d.Baja = 0))
|
||||||
|
OR (s.Destino != 'Distribuidores')
|
||||||
|
)");
|
||||||
|
}
|
||||||
|
|
||||||
if (idDestinoFilter.HasValue)
|
if (idDestinoFilter.HasValue)
|
||||||
{
|
{
|
||||||
sqlBuilder.Append(" AND Id_Destino = @IdDestino");
|
sqlBuilder.Append(" AND s.Id_Destino = @IdDestino");
|
||||||
parameters.Add("IdDestino", idDestinoFilter.Value);
|
parameters.Add("IdDestino", idDestinoFilter.Value);
|
||||||
}
|
}
|
||||||
if (idEmpresaFilter.HasValue)
|
if (idEmpresaFilter.HasValue)
|
||||||
{
|
{
|
||||||
sqlBuilder.Append(" AND Id_Empresa = @IdEmpresa");
|
sqlBuilder.Append(" AND s.Id_Empresa = @IdEmpresa");
|
||||||
parameters.Add("IdEmpresa", idEmpresaFilter.Value);
|
parameters.Add("IdEmpresa", idEmpresaFilter.Value);
|
||||||
}
|
}
|
||||||
sqlBuilder.Append(" ORDER BY Destino, Id_Empresa, Id_Destino;");
|
sqlBuilder.Append(" ORDER BY s.Destino, s.Id_Empresa, s.Id_Destino;");
|
||||||
|
|
||||||
using var connection = _connectionFactory.CreateConnection();
|
using var connection = _connectionFactory.CreateConnection();
|
||||||
return await connection.QueryAsync<Saldo>(sqlBuilder.ToString(), parameters);
|
return await connection.QueryAsync<Saldo>(sqlBuilder.ToString(), parameters);
|
||||||
|
|||||||
@@ -22,12 +22,12 @@ namespace GestionIntegral.Api.Data.Repositories.Distribucion
|
|||||||
_logger = logger;
|
_logger = logger;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<IEnumerable<(Distribuidor Distribuidor, string? NombreZona)>> GetAllAsync(string? nombreFilter, string? nroDocFilter)
|
public async Task<IEnumerable<(Distribuidor Distribuidor, string? NombreZona)>> GetAllAsync(string? nombreFilter, string? nroDocFilter, bool? soloActivos = true)
|
||||||
{
|
{
|
||||||
var sqlBuilder = new StringBuilder(@"
|
var sqlBuilder = new StringBuilder(@"
|
||||||
SELECT
|
SELECT
|
||||||
d.Id_Distribuidor AS IdDistribuidor, d.Nombre, d.Contacto, d.NroDoc, d.Id_Zona AS IdZona,
|
d.Id_Distribuidor AS IdDistribuidor, d.Nombre, d.Contacto, d.NroDoc, d.Id_Zona AS IdZona,
|
||||||
d.Calle, d.Numero, d.Piso, d.Depto, d.Telefono, d.Email, d.Localidad,
|
d.Calle, d.Numero, d.Piso, d.Depto, d.Telefono, d.Email, d.Localidad, d.Baja, d.FechaBaja,
|
||||||
z.Nombre AS NombreZona
|
z.Nombre AS NombreZona
|
||||||
FROM dbo.dist_dtDistribuidores d
|
FROM dbo.dist_dtDistribuidores d
|
||||||
LEFT JOIN dbo.dist_dtZonas z ON d.Id_Zona = z.Id_Zona
|
LEFT JOIN dbo.dist_dtZonas z ON d.Id_Zona = z.Id_Zona
|
||||||
@@ -44,6 +44,11 @@ namespace GestionIntegral.Api.Data.Repositories.Distribucion
|
|||||||
sqlBuilder.Append(" AND d.NroDoc LIKE @NroDocParam");
|
sqlBuilder.Append(" AND d.NroDoc LIKE @NroDocParam");
|
||||||
parameters.Add("NroDocParam", $"%{nroDocFilter}%");
|
parameters.Add("NroDocParam", $"%{nroDocFilter}%");
|
||||||
}
|
}
|
||||||
|
if (soloActivos.HasValue)
|
||||||
|
{
|
||||||
|
sqlBuilder.Append(" AND d.Baja = @BajaStatus ");
|
||||||
|
parameters.Add("BajaStatus", !soloActivos.Value);
|
||||||
|
}
|
||||||
sqlBuilder.Append(" ORDER BY d.Nombre;");
|
sqlBuilder.Append(" ORDER BY d.Nombre;");
|
||||||
|
|
||||||
try
|
try
|
||||||
@@ -63,7 +68,7 @@ namespace GestionIntegral.Api.Data.Repositories.Distribucion
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<IEnumerable<DistribuidorDropdownDto?>> GetAllDropdownAsync()
|
public async Task<IEnumerable<DistribuidorDropdownDto?>> GetAllDropdownAsync(bool? soloActivos = true)
|
||||||
{
|
{
|
||||||
var sqlBuilder = new StringBuilder(@"
|
var sqlBuilder = new StringBuilder(@"
|
||||||
SELECT
|
SELECT
|
||||||
@@ -71,6 +76,13 @@ namespace GestionIntegral.Api.Data.Repositories.Distribucion
|
|||||||
FROM dbo.dist_dtDistribuidores
|
FROM dbo.dist_dtDistribuidores
|
||||||
WHERE 1=1");
|
WHERE 1=1");
|
||||||
var parameters = new DynamicParameters();
|
var parameters = new DynamicParameters();
|
||||||
|
|
||||||
|
if (soloActivos.HasValue)
|
||||||
|
{
|
||||||
|
sqlBuilder.Append(" AND Baja = @BajaStatus ");
|
||||||
|
parameters.Add("BajaStatus", !soloActivos.Value);
|
||||||
|
}
|
||||||
|
|
||||||
sqlBuilder.Append(" ORDER BY Nombre;");
|
sqlBuilder.Append(" ORDER BY Nombre;");
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
@@ -92,7 +104,7 @@ namespace GestionIntegral.Api.Data.Repositories.Distribucion
|
|||||||
const string sql = @"
|
const string sql = @"
|
||||||
SELECT
|
SELECT
|
||||||
d.Id_Distribuidor AS IdDistribuidor, d.Nombre, d.Contacto, d.NroDoc, d.Id_Zona AS IdZona,
|
d.Id_Distribuidor AS IdDistribuidor, d.Nombre, d.Contacto, d.NroDoc, d.Id_Zona AS IdZona,
|
||||||
d.Calle, d.Numero, d.Piso, d.Depto, d.Telefono, d.Email, d.Localidad,
|
d.Calle, d.Numero, d.Piso, d.Depto, d.Telefono, d.Email, d.Localidad, d.Baja, d.FechaBaja,
|
||||||
z.Nombre AS NombreZona
|
z.Nombre AS NombreZona
|
||||||
FROM dbo.dist_dtDistribuidores d
|
FROM dbo.dist_dtDistribuidores d
|
||||||
LEFT JOIN dbo.dist_dtZonas z ON d.Id_Zona = z.Id_Zona
|
LEFT JOIN dbo.dist_dtZonas z ON d.Id_Zona = z.Id_Zona
|
||||||
@@ -139,7 +151,7 @@ namespace GestionIntegral.Api.Data.Repositories.Distribucion
|
|||||||
const string sql = @"
|
const string sql = @"
|
||||||
SELECT
|
SELECT
|
||||||
Id_Distribuidor AS IdDistribuidor, Nombre, Contacto, NroDoc, Id_Zona AS IdZona,
|
Id_Distribuidor AS IdDistribuidor, Nombre, Contacto, NroDoc, Id_Zona AS IdZona,
|
||||||
Calle, Numero, Piso, Depto, Telefono, Email, Localidad
|
Calle, Numero, Piso, Depto, Telefono, Email, Localidad, Baja, FechaBaja
|
||||||
FROM dbo.dist_dtDistribuidores
|
FROM dbo.dist_dtDistribuidores
|
||||||
WHERE Id_Distribuidor = @IdParam";
|
WHERE Id_Distribuidor = @IdParam";
|
||||||
try
|
try
|
||||||
@@ -223,10 +235,10 @@ namespace GestionIntegral.Api.Data.Repositories.Distribucion
|
|||||||
public async Task<Distribuidor?> CreateAsync(Distribuidor nuevoDistribuidor, int idUsuario, IDbTransaction transaction)
|
public async Task<Distribuidor?> CreateAsync(Distribuidor nuevoDistribuidor, int idUsuario, IDbTransaction transaction)
|
||||||
{
|
{
|
||||||
const string sqlInsert = @"
|
const string sqlInsert = @"
|
||||||
INSERT INTO dbo.dist_dtDistribuidores (Nombre, Contacto, NroDoc, Id_Zona, Calle, Numero, Piso, Depto, Telefono, Email, Localidad)
|
INSERT INTO dbo.dist_dtDistribuidores (Nombre, Contacto, NroDoc, Id_Zona, Calle, Numero, Piso, Depto, Telefono, Email, Localidad, Baja, FechaBaja)
|
||||||
OUTPUT INSERTED.Id_Distribuidor AS IdDistribuidor, INSERTED.Nombre, INSERTED.Contacto, INSERTED.NroDoc, INSERTED.Id_Zona AS IdZona,
|
OUTPUT INSERTED.Id_Distribuidor AS IdDistribuidor, INSERTED.Nombre, INSERTED.Contacto, INSERTED.NroDoc, INSERTED.Id_Zona AS IdZona,
|
||||||
INSERTED.Calle, INSERTED.Numero, INSERTED.Piso, INSERTED.Depto, INSERTED.Telefono, INSERTED.Email, INSERTED.Localidad
|
INSERTED.Calle, INSERTED.Numero, INSERTED.Piso, INSERTED.Depto, INSERTED.Telefono, INSERTED.Email, INSERTED.Localidad, INSERTED.Baja, INSERTED.FechaBaja
|
||||||
VALUES (@Nombre, @Contacto, @NroDoc, @IdZona, @Calle, @Numero, @Piso, @Depto, @Telefono, @Email, @Localidad);";
|
VALUES (@Nombre, @Contacto, @NroDoc, @IdZona, @Calle, @Numero, @Piso, @Depto, @Telefono, @Email, @Localidad, 0, NULL);";
|
||||||
|
|
||||||
var connection = transaction.Connection!;
|
var connection = transaction.Connection!;
|
||||||
var inserted = await connection.QuerySingleAsync<Distribuidor>(sqlInsert, nuevoDistribuidor, transaction);
|
var inserted = await connection.QuerySingleAsync<Distribuidor>(sqlInsert, nuevoDistribuidor, transaction);
|
||||||
@@ -234,8 +246,8 @@ namespace GestionIntegral.Api.Data.Repositories.Distribucion
|
|||||||
|
|
||||||
const string sqlInsertHistorico = @"
|
const string sqlInsertHistorico = @"
|
||||||
INSERT INTO dbo.dist_dtDistribuidores_H
|
INSERT INTO dbo.dist_dtDistribuidores_H
|
||||||
(Id_Distribuidor, Nombre, Contacto, NroDoc, Id_Zona, Calle, Numero, Piso, Depto, Telefono, Email, Localidad, Id_Usuario, FechaMod, TipoMod)
|
(Id_Distribuidor, Nombre, Contacto, NroDoc, Id_Zona, Calle, Numero, Piso, Depto, Telefono, Email, Localidad, Baja, FechaBaja, Id_Usuario, FechaMod, TipoMod)
|
||||||
VALUES (@IdDistribuidorParam, @NombreParam, @ContactoParam, @NroDocParam, @IdZonaParam, @CalleParam, @NumeroParam, @PisoParam, @DeptoParam, @TelefonoParam, @EmailParam, @LocalidadParam, @IdUsuarioParam, @FechaModParam, @TipoModParam);";
|
VALUES (@IdDistribuidorParam, @NombreParam, @ContactoParam, @NroDocParam, @IdZonaParam, @CalleParam, @NumeroParam, @PisoParam, @DeptoParam, @TelefonoParam, @EmailParam, @LocalidadParam, @BajaParam, @FechaBajaParam, @IdUsuarioParam, @FechaModParam, @TipoModParam);";
|
||||||
|
|
||||||
await connection.ExecuteAsync(sqlInsertHistorico, new
|
await connection.ExecuteAsync(sqlInsertHistorico, new
|
||||||
{
|
{
|
||||||
@@ -251,6 +263,8 @@ namespace GestionIntegral.Api.Data.Repositories.Distribucion
|
|||||||
TelefonoParam = inserted.Telefono,
|
TelefonoParam = inserted.Telefono,
|
||||||
EmailParam = inserted.Email,
|
EmailParam = inserted.Email,
|
||||||
LocalidadParam = inserted.Localidad,
|
LocalidadParam = inserted.Localidad,
|
||||||
|
BajaParam = inserted.Baja,
|
||||||
|
FechaBajaParam = inserted.FechaBaja,
|
||||||
IdUsuarioParam = idUsuario,
|
IdUsuarioParam = idUsuario,
|
||||||
FechaModParam = DateTime.Now,
|
FechaModParam = DateTime.Now,
|
||||||
TipoModParam = "Creado"
|
TipoModParam = "Creado"
|
||||||
@@ -263,7 +277,7 @@ namespace GestionIntegral.Api.Data.Repositories.Distribucion
|
|||||||
var connection = transaction.Connection!;
|
var connection = transaction.Connection!;
|
||||||
var actual = await connection.QuerySingleOrDefaultAsync<Distribuidor>(
|
var actual = await connection.QuerySingleOrDefaultAsync<Distribuidor>(
|
||||||
@"SELECT Id_Distribuidor AS IdDistribuidor, Nombre, Contacto, NroDoc, Id_Zona AS IdZona,
|
@"SELECT Id_Distribuidor AS IdDistribuidor, Nombre, Contacto, NroDoc, Id_Zona AS IdZona,
|
||||||
Calle, Numero, Piso, Depto, Telefono, Email, Localidad
|
Calle, Numero, Piso, Depto, Telefono, Email, Localidad, Baja, FechaBaja
|
||||||
FROM dbo.dist_dtDistribuidores WHERE Id_Distribuidor = @IdDistribuidorParam",
|
FROM dbo.dist_dtDistribuidores WHERE Id_Distribuidor = @IdDistribuidorParam",
|
||||||
new { IdDistribuidorParam = distribuidorAActualizar.IdDistribuidor }, transaction);
|
new { IdDistribuidorParam = distribuidorAActualizar.IdDistribuidor }, transaction);
|
||||||
if (actual == null) throw new KeyNotFoundException("Distribuidor no encontrado.");
|
if (actual == null) throw new KeyNotFoundException("Distribuidor no encontrado.");
|
||||||
@@ -275,8 +289,8 @@ namespace GestionIntegral.Api.Data.Repositories.Distribucion
|
|||||||
WHERE Id_Distribuidor = @IdDistribuidor;";
|
WHERE Id_Distribuidor = @IdDistribuidor;";
|
||||||
const string sqlInsertHistorico = @"
|
const string sqlInsertHistorico = @"
|
||||||
INSERT INTO dbo.dist_dtDistribuidores_H
|
INSERT INTO dbo.dist_dtDistribuidores_H
|
||||||
(Id_Distribuidor, Nombre, Contacto, NroDoc, Id_Zona, Calle, Numero, Piso, Depto, Telefono, Email, Localidad, Id_Usuario, FechaMod, TipoMod)
|
(Id_Distribuidor, Nombre, Contacto, NroDoc, Id_Zona, Calle, Numero, Piso, Depto, Telefono, Email, Localidad, Baja, FechaBaja, Id_Usuario, FechaMod, TipoMod)
|
||||||
VALUES (@IdDistribuidorParam, @NombreParam, @ContactoParam, @NroDocParam, @IdZonaParam, @CalleParam, @NumeroParam, @PisoParam, @DeptoParam, @TelefonoParam, @EmailParam, @LocalidadParam, @IdUsuarioParam, @FechaModParam, @TipoModParam);";
|
VALUES (@IdDistribuidorParam, @NombreParam, @ContactoParam, @NroDocParam, @IdZonaParam, @CalleParam, @NumeroParam, @PisoParam, @DeptoParam, @TelefonoParam, @EmailParam, @LocalidadParam, @BajaParam, @FechaBajaParam, @IdUsuarioParam, @FechaModParam, @TipoModParam);";
|
||||||
|
|
||||||
await connection.ExecuteAsync(sqlInsertHistorico, new
|
await connection.ExecuteAsync(sqlInsertHistorico, new
|
||||||
{
|
{
|
||||||
@@ -292,6 +306,8 @@ namespace GestionIntegral.Api.Data.Repositories.Distribucion
|
|||||||
TelefonoParam = actual.Telefono,
|
TelefonoParam = actual.Telefono,
|
||||||
EmailParam = actual.Email,
|
EmailParam = actual.Email,
|
||||||
LocalidadParam = actual.Localidad,
|
LocalidadParam = actual.Localidad,
|
||||||
|
BajaParam = actual.Baja,
|
||||||
|
FechaBajaParam = actual.FechaBaja,
|
||||||
IdUsuarioParam = idUsuario,
|
IdUsuarioParam = idUsuario,
|
||||||
FechaModParam = DateTime.Now,
|
FechaModParam = DateTime.Now,
|
||||||
TipoModParam = "Actualizado"
|
TipoModParam = "Actualizado"
|
||||||
@@ -306,7 +322,7 @@ namespace GestionIntegral.Api.Data.Repositories.Distribucion
|
|||||||
var connection = transaction.Connection!;
|
var connection = transaction.Connection!;
|
||||||
var actual = await connection.QuerySingleOrDefaultAsync<Distribuidor>(
|
var actual = await connection.QuerySingleOrDefaultAsync<Distribuidor>(
|
||||||
@"SELECT Id_Distribuidor AS IdDistribuidor, Nombre, Contacto, NroDoc, Id_Zona AS IdZona,
|
@"SELECT Id_Distribuidor AS IdDistribuidor, Nombre, Contacto, NroDoc, Id_Zona AS IdZona,
|
||||||
Calle, Numero, Piso, Depto, Telefono, Email, Localidad
|
Calle, Numero, Piso, Depto, Telefono, Email, Localidad, Baja, FechaBaja
|
||||||
FROM dbo.dist_dtDistribuidores WHERE Id_Distribuidor = @IdParam", new { IdParam = id }, transaction);
|
FROM dbo.dist_dtDistribuidores WHERE Id_Distribuidor = @IdParam", new { IdParam = id }, transaction);
|
||||||
if (actual == null) throw new KeyNotFoundException("Distribuidor no encontrado.");
|
if (actual == null) throw new KeyNotFoundException("Distribuidor no encontrado.");
|
||||||
|
|
||||||
@@ -314,8 +330,8 @@ namespace GestionIntegral.Api.Data.Repositories.Distribucion
|
|||||||
const string sqlDelete = "DELETE FROM dbo.dist_dtDistribuidores WHERE Id_Distribuidor = @IdParam";
|
const string sqlDelete = "DELETE FROM dbo.dist_dtDistribuidores WHERE Id_Distribuidor = @IdParam";
|
||||||
const string sqlInsertHistorico = @"
|
const string sqlInsertHistorico = @"
|
||||||
INSERT INTO dbo.dist_dtDistribuidores_H
|
INSERT INTO dbo.dist_dtDistribuidores_H
|
||||||
(Id_Distribuidor, Nombre, Contacto, NroDoc, Id_Zona, Calle, Numero, Piso, Depto, Telefono, Email, Localidad, Id_Usuario, FechaMod, TipoMod)
|
(Id_Distribuidor, Nombre, Contacto, NroDoc, Id_Zona, Calle, Numero, Piso, Depto, Telefono, Email, Localidad, Baja, FechaBaja, Id_Usuario, FechaMod, TipoMod)
|
||||||
VALUES (@IdDistribuidorParam, @NombreParam, @ContactoParam, @NroDocParam, @IdZonaParam, @CalleParam, @NumeroParam, @PisoParam, @DeptoParam, @TelefonoParam, @EmailParam, @LocalidadParam, @IdUsuarioParam, @FechaModParam, @TipoModParam);";
|
VALUES (@IdDistribuidorParam, @NombreParam, @ContactoParam, @NroDocParam, @IdZonaParam, @CalleParam, @NumeroParam, @PisoParam, @DeptoParam, @TelefonoParam, @EmailParam, @LocalidadParam, @BajaParam, @FechaBajaParam, @IdUsuarioParam, @FechaModParam, @TipoModParam);";
|
||||||
|
|
||||||
await connection.ExecuteAsync(sqlInsertHistorico, new
|
await connection.ExecuteAsync(sqlInsertHistorico, new
|
||||||
{
|
{
|
||||||
@@ -331,6 +347,8 @@ namespace GestionIntegral.Api.Data.Repositories.Distribucion
|
|||||||
TelefonoParam = actual.Telefono,
|
TelefonoParam = actual.Telefono,
|
||||||
EmailParam = actual.Email,
|
EmailParam = actual.Email,
|
||||||
LocalidadParam = actual.Localidad,
|
LocalidadParam = actual.Localidad,
|
||||||
|
BajaParam = actual.Baja,
|
||||||
|
FechaBajaParam = actual.FechaBaja,
|
||||||
IdUsuarioParam = idUsuario,
|
IdUsuarioParam = idUsuario,
|
||||||
FechaModParam = DateTime.Now,
|
FechaModParam = DateTime.Now,
|
||||||
TipoModParam = "Eliminado"
|
TipoModParam = "Eliminado"
|
||||||
@@ -340,6 +358,47 @@ namespace GestionIntegral.Api.Data.Repositories.Distribucion
|
|||||||
return rowsAffected == 1;
|
return rowsAffected == 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async Task<bool> ToggleBajaAsync(int id, bool darDeBaja, DateTime? fechaBaja, int idUsuario, IDbTransaction transaction)
|
||||||
|
{
|
||||||
|
var connection = transaction.Connection!;
|
||||||
|
var actual = await connection.QuerySingleOrDefaultAsync<Distribuidor>(
|
||||||
|
@"SELECT Id_Distribuidor AS IdDistribuidor, Nombre, Contacto, NroDoc, Id_Zona AS IdZona,
|
||||||
|
Calle, Numero, Piso, Depto, Telefono, Email, Localidad, Baja, FechaBaja
|
||||||
|
FROM dbo.dist_dtDistribuidores WHERE Id_Distribuidor = @IdDistribuidorParam",
|
||||||
|
new { IdDistribuidorParam = id }, transaction);
|
||||||
|
if (actual == null) throw new KeyNotFoundException("Distribuidor no encontrado para dar de baja/alta.");
|
||||||
|
|
||||||
|
const string sqlUpdate = "UPDATE dbo.dist_dtDistribuidores SET Baja = @BajaParam, FechaBaja = @FechaBajaParam WHERE Id_Distribuidor = @IdDistribuidorParam;";
|
||||||
|
const string sqlInsertHistorico = @"
|
||||||
|
INSERT INTO dbo.dist_dtDistribuidores_H
|
||||||
|
(Id_Distribuidor, Nombre, Contacto, NroDoc, Id_Zona, Calle, Numero, Piso, Depto, Telefono, Email, Localidad, Baja, FechaBaja, Id_Usuario, FechaMod, TipoMod)
|
||||||
|
VALUES (@IdDistribuidorParam, @NombreParam, @ContactoParam, @NroDocParam, @IdZonaParam, @CalleParam, @NumeroParam, @PisoParam, @DeptoParam, @TelefonoParam, @EmailParam, @LocalidadParam, @BajaNuevaParam, @FechaBajaNuevaParam, @IdUsuarioParam, @FechaModParam, @TipoModHistParam);";
|
||||||
|
|
||||||
|
await connection.ExecuteAsync(sqlInsertHistorico, new
|
||||||
|
{
|
||||||
|
IdDistribuidorParam = actual.IdDistribuidor,
|
||||||
|
NombreParam = actual.Nombre,
|
||||||
|
ContactoParam = actual.Contacto,
|
||||||
|
NroDocParam = actual.NroDoc,
|
||||||
|
IdZonaParam = actual.IdZona,
|
||||||
|
CalleParam = actual.Calle,
|
||||||
|
NumeroParam = actual.Numero,
|
||||||
|
PisoParam = actual.Piso,
|
||||||
|
DeptoParam = actual.Depto,
|
||||||
|
TelefonoParam = actual.Telefono,
|
||||||
|
EmailParam = actual.Email,
|
||||||
|
LocalidadParam = actual.Localidad,
|
||||||
|
BajaNuevaParam = darDeBaja,
|
||||||
|
FechaBajaNuevaParam = (darDeBaja ? fechaBaja : null),
|
||||||
|
IdUsuarioParam = idUsuario,
|
||||||
|
FechaModParam = DateTime.Now,
|
||||||
|
TipoModHistParam = (darDeBaja ? "Baja" : "Alta")
|
||||||
|
}, transaction);
|
||||||
|
|
||||||
|
var rowsAffected = await connection.ExecuteAsync(sqlUpdate, new { BajaParam = darDeBaja, FechaBajaParam = (darDeBaja ? fechaBaja : null), IdDistribuidorParam = id }, transaction);
|
||||||
|
return rowsAffected == 1;
|
||||||
|
}
|
||||||
|
|
||||||
public async Task<IEnumerable<(DistribuidorHistorico Historial, string NombreUsuarioModifico)>> GetHistorialAsync(
|
public async Task<IEnumerable<(DistribuidorHistorico Historial, string NombreUsuarioModifico)>> GetHistorialAsync(
|
||||||
DateTime? fechaDesde, DateTime? fechaHasta,
|
DateTime? fechaDesde, DateTime? fechaHasta,
|
||||||
int? idUsuarioModifico, string? tipoModificacion,
|
int? idUsuarioModifico, string? tipoModificacion,
|
||||||
|
|||||||
@@ -72,6 +72,13 @@ namespace GestionIntegral.Api.Data.Repositories.Distribucion
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// -------------------------------------------------------------------------------
|
||||||
|
// Inhabilitada la comprobacion de existencia previa por remito y tipo de movimiento
|
||||||
|
// Pedido por Claudia Acosta el 18/11/2025
|
||||||
|
// Motivo: El ex canillita Sergio Mazza opera como distribuidor y no utiliza remitos.
|
||||||
|
// En el campo de remito se le asigna un numero aleatorio para cumplir con el requisito del sistema.
|
||||||
|
// -------------------------------------------------------------------------------
|
||||||
|
/*
|
||||||
public async Task<bool> ExistsByRemitoAndTipoForPublicacionAsync(int remito, string tipoMovimiento, int idPublicacion, int? excludeIdParte = null)
|
public async Task<bool> ExistsByRemitoAndTipoForPublicacionAsync(int remito, string tipoMovimiento, int idPublicacion, int? excludeIdParte = null)
|
||||||
{
|
{
|
||||||
var sqlBuilder = new StringBuilder("SELECT COUNT(1) FROM dbo.dist_EntradasSalidas WHERE Remito = @RemitoParam AND TipoMovimiento = @TipoMovimientoParam AND Id_Publicacion = @IdPublicacionParam");
|
var sqlBuilder = new StringBuilder("SELECT COUNT(1) FROM dbo.dist_EntradasSalidas WHERE Remito = @RemitoParam AND TipoMovimiento = @TipoMovimientoParam AND Id_Publicacion = @IdPublicacionParam");
|
||||||
@@ -96,7 +103,7 @@ namespace GestionIntegral.Api.Data.Repositories.Distribucion
|
|||||||
return true; // Asumir que existe en caso de error para prevenir duplicados
|
return true; // Asumir que existe en caso de error para prevenir duplicados
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
public async Task<EntradaSalidaDist?> CreateAsync(EntradaSalidaDist nuevaES, int idUsuario, IDbTransaction transaction)
|
public async Task<EntradaSalidaDist?> CreateAsync(EntradaSalidaDist nuevaES, int idUsuario, IDbTransaction transaction)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -8,16 +8,17 @@ namespace GestionIntegral.Api.Data.Repositories.Distribucion
|
|||||||
{
|
{
|
||||||
public interface IDistribuidorRepository
|
public interface IDistribuidorRepository
|
||||||
{
|
{
|
||||||
Task<IEnumerable<(Distribuidor Distribuidor, string? NombreZona)>> GetAllAsync(string? nombreFilter, string? nroDocFilter);
|
Task<IEnumerable<(Distribuidor Distribuidor, string? NombreZona)>> GetAllAsync(string? nombreFilter, string? nroDocFilter, bool? soloActivos = true);
|
||||||
Task<(Distribuidor? Distribuidor, string? NombreZona)> GetByIdAsync(int id);
|
Task<(Distribuidor? Distribuidor, string? NombreZona)> GetByIdAsync(int id);
|
||||||
Task<Distribuidor?> GetByIdSimpleAsync(int id); // Para uso interno en el servicio
|
Task<Distribuidor?> GetByIdSimpleAsync(int id); // Para uso interno en el servicio
|
||||||
Task<Distribuidor?> CreateAsync(Distribuidor nuevoDistribuidor, int idUsuario, IDbTransaction transaction);
|
Task<Distribuidor?> CreateAsync(Distribuidor nuevoDistribuidor, int idUsuario, IDbTransaction transaction);
|
||||||
Task<bool> UpdateAsync(Distribuidor distribuidorAActualizar, int idUsuario, IDbTransaction transaction);
|
Task<bool> UpdateAsync(Distribuidor distribuidorAActualizar, int idUsuario, IDbTransaction transaction);
|
||||||
Task<bool> DeleteAsync(int id, int idUsuario, IDbTransaction transaction);
|
Task<bool> DeleteAsync(int id, int idUsuario, IDbTransaction transaction);
|
||||||
|
Task<bool> ToggleBajaAsync(int id, bool darDeBaja, DateTime? fechaBaja, int idUsuario, IDbTransaction transaction);
|
||||||
Task<bool> ExistsByNroDocAsync(string nroDoc, int? excludeIdDistribuidor = null);
|
Task<bool> ExistsByNroDocAsync(string nroDoc, int? excludeIdDistribuidor = null);
|
||||||
Task<bool> ExistsByNameAsync(string nombre, int? excludeIdDistribuidor = null);
|
Task<bool> ExistsByNameAsync(string nombre, int? excludeIdDistribuidor = null);
|
||||||
Task<bool> IsInUseAsync(int id); // Verificar en dist_EntradasSalidas, cue_PagosDistribuidor, dist_PorcPago
|
Task<bool> IsInUseAsync(int id); // Verificar en dist_EntradasSalidas, cue_PagosDistribuidor, dist_PorcPago
|
||||||
Task<IEnumerable<DistribuidorDropdownDto?>> GetAllDropdownAsync();
|
Task<IEnumerable<DistribuidorDropdownDto?>> GetAllDropdownAsync(bool? soloActivos = true);
|
||||||
Task<DistribuidorLookupDto?> ObtenerLookupPorIdAsync(int id);
|
Task<DistribuidorLookupDto?> ObtenerLookupPorIdAsync(int id);
|
||||||
Task<IEnumerable<(DistribuidorHistorico Historial, string NombreUsuarioModifico)>> GetHistorialAsync(
|
Task<IEnumerable<(DistribuidorHistorico Historial, string NombreUsuarioModifico)>> GetHistorialAsync(
|
||||||
DateTime? fechaDesde, DateTime? fechaHasta,
|
DateTime? fechaDesde, DateTime? fechaHasta,
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ namespace GestionIntegral.Api.Data.Repositories.Distribucion
|
|||||||
Task<EntradaSalidaDist?> CreateAsync(EntradaSalidaDist nuevaES, int idUsuario, IDbTransaction transaction);
|
Task<EntradaSalidaDist?> CreateAsync(EntradaSalidaDist nuevaES, int idUsuario, IDbTransaction transaction);
|
||||||
Task<bool> UpdateAsync(EntradaSalidaDist esAActualizar, int idUsuario, IDbTransaction transaction);
|
Task<bool> UpdateAsync(EntradaSalidaDist esAActualizar, int idUsuario, IDbTransaction transaction);
|
||||||
Task<bool> DeleteAsync(int idParte, int idUsuario, IDbTransaction transaction);
|
Task<bool> DeleteAsync(int idParte, int idUsuario, IDbTransaction transaction);
|
||||||
Task<bool> ExistsByRemitoAndTipoForPublicacionAsync(int remito, string tipoMovimiento, int idPublicacion, int? excludeIdParte = null);
|
//Task<bool> ExistsByRemitoAndTipoForPublicacionAsync(int remito, string tipoMovimiento, int idPublicacion, int? excludeIdParte = null);
|
||||||
Task<IEnumerable<(EntradaSalidaDistHistorico Historial, string NombreUsuarioModifico)>> GetHistorialAsync(
|
Task<IEnumerable<(EntradaSalidaDistHistorico Historial, string NombreUsuarioModifico)>> GetHistorialAsync(
|
||||||
DateTime? fechaDesde, DateTime? fechaHasta,
|
DateTime? fechaDesde, DateTime? fechaHasta,
|
||||||
int? idUsuarioModifico, string? tipoModificacion,
|
int? idUsuarioModifico, string? tipoModificacion,
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ namespace GestionIntegral.Api.Data.Repositories.Distribucion
|
|||||||
d.Nombre AS NombreDistribuidor
|
d.Nombre AS NombreDistribuidor
|
||||||
FROM dbo.dist_PorcPago pp
|
FROM dbo.dist_PorcPago pp
|
||||||
INNER JOIN dbo.dist_dtDistribuidores d ON pp.Id_Distribuidor = d.Id_Distribuidor
|
INNER JOIN dbo.dist_dtDistribuidores d ON pp.Id_Distribuidor = d.Id_Distribuidor
|
||||||
WHERE pp.Id_Publicacion = @IdPublicacionParam
|
WHERE pp.Id_Publicacion = @IdPublicacionParam AND d.Baja = 0
|
||||||
ORDER BY d.Nombre, pp.VigenciaD DESC";
|
ORDER BY d.Nombre, pp.VigenciaD DESC";
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -15,7 +15,9 @@ namespace GestionIntegral.Api.Data.Repositories.Distribucion
|
|||||||
int? idEstadoBobina,
|
int? idEstadoBobina,
|
||||||
string? remitoFilter,
|
string? remitoFilter,
|
||||||
DateTime? fechaDesde,
|
DateTime? fechaDesde,
|
||||||
DateTime? fechaHasta);
|
DateTime? fechaHasta,
|
||||||
|
DateTime? fechaEstadoDesde,
|
||||||
|
DateTime? fechaEstadoHasta);
|
||||||
|
|
||||||
Task<StockBobina?> GetByIdAsync(int idBobina);
|
Task<StockBobina?> GetByIdAsync(int idBobina);
|
||||||
Task<StockBobina?> GetByNroBobinaAsync(string nroBobina); // Para validar unicidad de NroBobina
|
Task<StockBobina?> GetByNroBobinaAsync(string nroBobina); // Para validar unicidad de NroBobina
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ namespace GestionIntegral.Api.Data.Repositories.Distribucion
|
|||||||
|
|
||||||
public async Task<IEnumerable<StockBobina>> GetAllAsync(
|
public async Task<IEnumerable<StockBobina>> GetAllAsync(
|
||||||
int? idTipoBobina, string? nroBobinaFilter, int? idPlanta,
|
int? idTipoBobina, string? nroBobinaFilter, int? idPlanta,
|
||||||
int? idEstadoBobina, string? remitoFilter, DateTime? fechaDesde, DateTime? fechaHasta)
|
int? idEstadoBobina, string? remitoFilter, DateTime? fechaDesde, DateTime? fechaHasta, DateTime? fechaEstadoDesde, DateTime? fechaEstadoHasta)
|
||||||
{
|
{
|
||||||
var sqlBuilder = new StringBuilder(@"
|
var sqlBuilder = new StringBuilder(@"
|
||||||
SELECT
|
SELECT
|
||||||
@@ -69,6 +69,16 @@ namespace GestionIntegral.Api.Data.Repositories.Distribucion
|
|||||||
sqlBuilder.Append(" AND sb.FechaRemito <= @FechaHastaParam");
|
sqlBuilder.Append(" AND sb.FechaRemito <= @FechaHastaParam");
|
||||||
parameters.Add("FechaHastaParam", fechaHasta.Value.Date);
|
parameters.Add("FechaHastaParam", fechaHasta.Value.Date);
|
||||||
}
|
}
|
||||||
|
if (fechaEstadoDesde.HasValue)
|
||||||
|
{
|
||||||
|
sqlBuilder.Append(" AND sb.FechaEstado >= @FechaEstadoDesdeParam");
|
||||||
|
parameters.Add("FechaEstadoDesdeParam", fechaEstadoDesde.Value.Date);
|
||||||
|
}
|
||||||
|
if (fechaEstadoHasta.HasValue)
|
||||||
|
{
|
||||||
|
sqlBuilder.Append(" AND sb.FechaEstado <= @FechaEstadoHastaParam");
|
||||||
|
parameters.Add("FechaEstadoHastaParam", fechaEstadoHasta.Value.Date);
|
||||||
|
}
|
||||||
|
|
||||||
sqlBuilder.Append(" ORDER BY sb.FechaRemito DESC, sb.NroBobina;");
|
sqlBuilder.Append(" ORDER BY sb.FechaRemito DESC, sb.NroBobina;");
|
||||||
|
|
||||||
|
|||||||
@@ -48,5 +48,7 @@ namespace GestionIntegral.Api.Data.Repositories.Reportes
|
|||||||
Task<IEnumerable<FacturasParaReporteDto>> GetDatosReportePublicidadAsync(string periodo);
|
Task<IEnumerable<FacturasParaReporteDto>> GetDatosReportePublicidadAsync(string periodo);
|
||||||
Task<IEnumerable<DistribucionSuscripcionDto>> GetDistribucionSuscripcionesActivasAsync(DateTime fechaDesde, DateTime fechaHasta);
|
Task<IEnumerable<DistribucionSuscripcionDto>> GetDistribucionSuscripcionesActivasAsync(DateTime fechaDesde, DateTime fechaHasta);
|
||||||
Task<IEnumerable<DistribucionSuscripcionDto>> GetDistribucionSuscripcionesBajasAsync(DateTime fechaDesde, DateTime fechaHasta);
|
Task<IEnumerable<DistribucionSuscripcionDto>> GetDistribucionSuscripcionesBajasAsync(DateTime fechaDesde, DateTime fechaHasta);
|
||||||
|
Task<IEnumerable<DetalleDistribucionCanillaDto>> GetDetalleDistribucionCanillasPubli_AllEmpresasAsync(DateTime fecha);
|
||||||
|
Task<IEnumerable<DetalleDistribucionCanillaDto>> GetDetalleDistribucionCanillasAccPubli_AllEmpresasAsync(DateTime fecha);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -653,5 +653,39 @@ namespace GestionIntegral.Api.Data.Repositories.Reportes
|
|||||||
return Enumerable.Empty<DistribucionSuscripcionDto>();
|
return Enumerable.Empty<DistribucionSuscripcionDto>();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async Task<IEnumerable<DetalleDistribucionCanillaDto>> GetDetalleDistribucionCanillasPubli_AllEmpresasAsync(DateTime fecha)
|
||||||
|
{
|
||||||
|
const string spName = "dbo.SP_DistCanillasEntradaSalidaPubli_AllEmpresas";
|
||||||
|
var parameters = new DynamicParameters();
|
||||||
|
parameters.Add("@fecha", fecha, DbType.DateTime);
|
||||||
|
try
|
||||||
|
{
|
||||||
|
using var connection = _dbConnectionFactory.CreateConnection();
|
||||||
|
return await connection.QueryAsync<DetalleDistribucionCanillaDto>(spName, parameters, commandType: CommandType.StoredProcedure, commandTimeout: 120);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "Error SP {SPName}", spName);
|
||||||
|
return Enumerable.Empty<DetalleDistribucionCanillaDto>();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<IEnumerable<DetalleDistribucionCanillaDto>> GetDetalleDistribucionCanillasAccPubli_AllEmpresasAsync(DateTime fecha)
|
||||||
|
{
|
||||||
|
const string spName = "dbo.SP_DistCanillasAccEntradaSalidaPubli_AllEmpresas";
|
||||||
|
var parameters = new DynamicParameters();
|
||||||
|
parameters.Add("@fecha", fecha, DbType.DateTime);
|
||||||
|
try
|
||||||
|
{
|
||||||
|
using var connection = _dbConnectionFactory.CreateConnection();
|
||||||
|
return await connection.QueryAsync<DetalleDistribucionCanillaDto>(spName, parameters, commandType: CommandType.StoredProcedure, commandTimeout: 120);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "Error SP {SPName}", spName);
|
||||||
|
return Enumerable.Empty<DetalleDistribucionCanillaDto>();
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -59,10 +59,15 @@ namespace GestionIntegral.Api.Data.Repositories.Suscripciones
|
|||||||
{
|
{
|
||||||
throw new ArgumentNullException(nameof(transaction), "La transacción o su conexión no pueden ser nulas.");
|
throw new ArgumentNullException(nameof(transaction), "La transacción o su conexión no pueden ser nulas.");
|
||||||
}
|
}
|
||||||
|
|
||||||
const string sqlInsert = @"
|
const string sqlInsert = @"
|
||||||
INSERT INTO dbo.susc_Facturas (IdSuscriptor, Periodo, FechaEmision, FechaVencimiento, ImporteBruto, DescuentoAplicado, ImporteFinal, EstadoPago, EstadoFacturacion)
|
INSERT INTO dbo.susc_Facturas
|
||||||
|
(IdSuscriptor, Periodo, FechaEmision, FechaVencimiento, ImporteBruto,
|
||||||
|
DescuentoAplicado, ImporteFinal, EstadoPago, EstadoFacturacion, TipoFactura)
|
||||||
OUTPUT INSERTED.*
|
OUTPUT INSERTED.*
|
||||||
VALUES (@IdSuscriptor, @Periodo, @FechaEmision, @FechaVencimiento, @ImporteBruto, @DescuentoAplicado, @ImporteFinal, @EstadoPago, @EstadoFacturacion);";
|
VALUES
|
||||||
|
(@IdSuscriptor, @Periodo, @FechaEmision, @FechaVencimiento, @ImporteBruto,
|
||||||
|
@DescuentoAplicado, @ImporteFinal, @EstadoPago, @EstadoFacturacion, @TipoFactura);";
|
||||||
|
|
||||||
return await transaction.Connection.QuerySingleAsync<Factura>(sqlInsert, nuevaFactura, transaction);
|
return await transaction.Connection.QuerySingleAsync<Factura>(sqlInsert, nuevaFactura, transaction);
|
||||||
}
|
}
|
||||||
@@ -104,7 +109,8 @@ namespace GestionIntegral.Api.Data.Repositories.Suscripciones
|
|||||||
return rowsAffected == idsFacturas.Count();
|
return rowsAffected == idsFacturas.Count();
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<IEnumerable<(Factura Factura, string NombreSuscriptor, int IdEmpresa, decimal TotalPagado)>> GetByPeriodoEnrichedAsync(string periodo, string? nombreSuscriptor, string? estadoPago, string? estadoFacturacion)
|
public async Task<IEnumerable<(Factura Factura, string NombreSuscriptor, int IdEmpresa, decimal TotalPagado)>> GetByPeriodoEnrichedAsync(
|
||||||
|
string periodo, string? nombreSuscriptor, string? estadoPago, string? estadoFacturacion, string? tipoFactura)
|
||||||
{
|
{
|
||||||
var sqlBuilder = new StringBuilder(@"
|
var sqlBuilder = new StringBuilder(@"
|
||||||
WITH FacturaConEmpresa AS (
|
WITH FacturaConEmpresa AS (
|
||||||
@@ -149,6 +155,12 @@ namespace GestionIntegral.Api.Data.Repositories.Suscripciones
|
|||||||
parameters.Add("EstadoFacturacion", estadoFacturacion);
|
parameters.Add("EstadoFacturacion", estadoFacturacion);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!string.IsNullOrWhiteSpace(tipoFactura))
|
||||||
|
{
|
||||||
|
sqlBuilder.Append(" AND f.TipoFactura = @TipoFactura");
|
||||||
|
parameters.Add("TipoFactura", tipoFactura);
|
||||||
|
}
|
||||||
|
|
||||||
sqlBuilder.Append(" ORDER BY s.NombreCompleto, f.IdFactura;");
|
sqlBuilder.Append(" ORDER BY s.NombreCompleto, f.IdFactura;");
|
||||||
|
|
||||||
try
|
try
|
||||||
|
|||||||
@@ -15,7 +15,8 @@ namespace GestionIntegral.Api.Data.Repositories.Suscripciones
|
|||||||
Task<bool> UpdateEstadoPagoAsync(int idFactura, string nuevoEstadoPago, IDbTransaction transaction);
|
Task<bool> UpdateEstadoPagoAsync(int idFactura, string nuevoEstadoPago, IDbTransaction transaction);
|
||||||
Task<bool> UpdateNumeroFacturaAsync(int idFactura, string numeroFactura, IDbTransaction transaction);
|
Task<bool> UpdateNumeroFacturaAsync(int idFactura, string numeroFactura, IDbTransaction transaction);
|
||||||
Task<bool> UpdateLoteDebitoAsync(IEnumerable<int> idsFacturas, int idLoteDebito, IDbTransaction transaction);
|
Task<bool> UpdateLoteDebitoAsync(IEnumerable<int> idsFacturas, int idLoteDebito, IDbTransaction transaction);
|
||||||
Task<IEnumerable<(Factura Factura, string NombreSuscriptor, int IdEmpresa, decimal TotalPagado)>> GetByPeriodoEnrichedAsync(string periodo, string? nombreSuscriptor, string? estadoPago, string? estadoFacturacion);
|
Task<IEnumerable<(Factura Factura, string NombreSuscriptor, int IdEmpresa, decimal TotalPagado)>> GetByPeriodoEnrichedAsync(
|
||||||
|
string periodo, string? nombreSuscriptor, string? estadoPago, string? estadoFacturacion, string? tipoFactura);
|
||||||
Task<bool> UpdateEstadoYMotivoAsync(int idFactura, string nuevoEstadoPago, string? motivoRechazo, IDbTransaction transaction);
|
Task<bool> UpdateEstadoYMotivoAsync(int idFactura, string nuevoEstadoPago, string? motivoRechazo, IDbTransaction transaction);
|
||||||
Task<string?> GetUltimoPeriodoFacturadoAsync();
|
Task<string?> GetUltimoPeriodoFacturadoAsync();
|
||||||
Task<IEnumerable<Factura>> GetFacturasPagadasPendientesDeFacturar(string periodo);
|
Task<IEnumerable<Factura>> GetFacturasPagadasPendientesDeFacturar(string periodo);
|
||||||
|
|||||||
@@ -1,15 +1,15 @@
|
|||||||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<TargetFramework>net9.0</TargetFramework>
|
<TargetFramework>net9.0</TargetFramework>
|
||||||
<Nullable>enable</Nullable>
|
<Nullable>enable</Nullable>
|
||||||
<ImplicitUsings>enable</ImplicitUsings>
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
<UserSecretsId>4923d7ee-0944-456c-abcd-d6ce13ba8485</UserSecretsId>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="AspNetCore.HealthChecks.SqlServer" Version="9.0.0" />
|
<PackageReference Include="AspNetCore.HealthChecks.SqlServer" Version="9.0.0" />
|
||||||
<PackageReference Include="Dapper" Version="2.1.66" />
|
<PackageReference Include="Dapper" Version="2.1.66" />
|
||||||
<PackageReference Include="DotNetEnv" Version="3.1.1" />
|
|
||||||
<PackageReference Include="MailKit" Version="4.13.0" />
|
<PackageReference Include="MailKit" Version="4.13.0" />
|
||||||
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="9.0.4" />
|
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="9.0.4" />
|
||||||
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="9.0.3" />
|
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="9.0.3" />
|
||||||
|
|||||||
@@ -0,0 +1,73 @@
|
|||||||
|
using GestionIntegral.Api.Services.Contables;
|
||||||
|
using Microsoft.AspNetCore.Http;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
using System;
|
||||||
|
using System.Text.Json;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace GestionIntegral.Api.Middleware
|
||||||
|
{
|
||||||
|
// Centraliza el mapeo de excepciones semánticas a HTTP responses con cuerpo JSON estandarizado.
|
||||||
|
// Va PRIMERO en el pipeline para catchear cualquier excepción que escape de los controllers/services.
|
||||||
|
public class ExceptionHandlerMiddleware
|
||||||
|
{
|
||||||
|
private readonly RequestDelegate _next;
|
||||||
|
private readonly ILogger<ExceptionHandlerMiddleware> _logger;
|
||||||
|
|
||||||
|
private static readonly JsonSerializerOptions JsonOptions = new()
|
||||||
|
{
|
||||||
|
PropertyNamingPolicy = JsonNamingPolicy.CamelCase
|
||||||
|
};
|
||||||
|
|
||||||
|
public ExceptionHandlerMiddleware(RequestDelegate next, ILogger<ExceptionHandlerMiddleware> logger)
|
||||||
|
{
|
||||||
|
_next = next;
|
||||||
|
_logger = logger;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task InvokeAsync(HttpContext context)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await _next(context);
|
||||||
|
}
|
||||||
|
catch (BloqueoPorPeriodoCerradoException ex)
|
||||||
|
{
|
||||||
|
_logger.LogWarning(
|
||||||
|
"Bloqueo por período cerrado: cierre #{IdCierre} FechaCorte={FechaCorte:yyyy-MM-dd}. Path={Path}",
|
||||||
|
ex.IdCierre, ex.FechaCorte, context.Request.Path);
|
||||||
|
|
||||||
|
await WriteJsonAsync(context, StatusCodes.Status409Conflict, new
|
||||||
|
{
|
||||||
|
codigo = "PERIODO_CERRADO_BLOQUEO_OPERACION",
|
||||||
|
mensaje = ex.Message,
|
||||||
|
idCierre = ex.IdCierre,
|
||||||
|
fechaCorte = ex.FechaCorte.ToString("yyyy-MM-dd")
|
||||||
|
});
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "Excepción no manejada. Path={Path}", context.Request.Path);
|
||||||
|
|
||||||
|
await WriteJsonAsync(context, StatusCodes.Status500InternalServerError, new
|
||||||
|
{
|
||||||
|
codigo = "ERROR_INTERNO",
|
||||||
|
mensaje = "Ocurrió un error inesperado al procesar la solicitud."
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Task WriteJsonAsync(HttpContext context, int statusCode, object body)
|
||||||
|
{
|
||||||
|
if (context.Response.HasStarted)
|
||||||
|
{
|
||||||
|
// Si los headers ya se enviaron no podemos re-escribir el response. Solo loguear y salir.
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
context.Response.Clear();
|
||||||
|
context.Response.StatusCode = statusCode;
|
||||||
|
context.Response.ContentType = "application/json; charset=utf-8";
|
||||||
|
return context.Response.WriteAsync(JsonSerializer.Serialize(body, JsonOptions));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
using System;
|
||||||
|
namespace GestionIntegral.Api.Models.Contables
|
||||||
|
{
|
||||||
|
public class CierreCuentaCorriente // Corresponde a cue_CierresCuentaCorriente
|
||||||
|
{
|
||||||
|
public int IdCierre { get; set; }
|
||||||
|
public int IdDistribuidor { get; set; }
|
||||||
|
public int IdEmpresa { get; set; }
|
||||||
|
public DateTime FechaCorte { get; set; }
|
||||||
|
public DateTime FechaCierre { get; set; }
|
||||||
|
public decimal SaldoCierre { get; set; } // money en SQL, decimal en C#
|
||||||
|
public string Estado { get; set; } = "Activo"; // 'Activo' | 'Anulado'
|
||||||
|
public string? Justificacion { get; set; }
|
||||||
|
public int IdUsuarioCierre { get; set; }
|
||||||
|
public int? IdUsuarioAnula { get; set; }
|
||||||
|
public DateTime? FechaAnulacion { get; set; }
|
||||||
|
public string? JustificacionAnulacion { get; set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
using System;
|
||||||
|
namespace GestionIntegral.Api.Models.Contables
|
||||||
|
{
|
||||||
|
public class CierreCuentaCorrienteHistorico // Corresponde a cue_CierresCuentaCorriente_H
|
||||||
|
{
|
||||||
|
public int Id_Historial { get; set; }
|
||||||
|
public int Id_Cierre { get; set; }
|
||||||
|
public int Id_Distribuidor { get; set; }
|
||||||
|
public int Id_Empresa { get; set; }
|
||||||
|
public DateTime FechaCorte { get; set; }
|
||||||
|
public DateTime FechaCierre { get; set; }
|
||||||
|
public decimal SaldoCierre { get; set; }
|
||||||
|
public string Estado { get; set; } = string.Empty;
|
||||||
|
public string? Justificacion { get; set; }
|
||||||
|
public int Id_Usuario_Cierre { get; set; }
|
||||||
|
public int? Id_Usuario_Anula { get; set; }
|
||||||
|
public DateTime? FechaAnulacion { get; set; }
|
||||||
|
public string? Justificacion_Anulacion { get; set; }
|
||||||
|
public string TipoMod { get; set; } = string.Empty; // 'Creacion' | 'Reapertura' | 'Modificacion'
|
||||||
|
public int Id_Usuario_Mod { get; set; }
|
||||||
|
public DateTime FechaMod { get; set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -14,5 +14,7 @@ namespace GestionIntegral.Api.Models.Distribucion
|
|||||||
public string? Telefono { get; set; }
|
public string? Telefono { get; set; }
|
||||||
public string? Email { get; set; }
|
public string? Email { get; set; }
|
||||||
public string? Localidad { get; set; }
|
public string? Localidad { get; set; }
|
||||||
|
public bool Baja { get; set; } // Baja (bit, NOT NULL, DEFAULT 0)
|
||||||
|
public DateTime? FechaBaja { get; set; } // FechaBaja (datetime2(0), NULL)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -16,6 +16,8 @@ namespace GestionIntegral.Api.Models.Distribucion
|
|||||||
public string? Telefono { get; set; }
|
public string? Telefono { get; set; }
|
||||||
public string? Email { get; set; }
|
public string? Email { get; set; }
|
||||||
public string? Localidad { get; set; }
|
public string? Localidad { get; set; }
|
||||||
|
public bool? Baja { get; set; }
|
||||||
|
public DateTime? FechaBaja { get; set; }
|
||||||
public int Id_Usuario { get; set; }
|
public int Id_Usuario { get; set; }
|
||||||
public DateTime FechaMod { get; set; }
|
public DateTime FechaMod { get; set; }
|
||||||
public string TipoMod { get; set; } = string.Empty;
|
public string TipoMod { get; set; } = string.Empty;
|
||||||
|
|||||||
@@ -0,0 +1,26 @@
|
|||||||
|
using System;
|
||||||
|
|
||||||
|
namespace GestionIntegral.Api.Dtos.Auditoria
|
||||||
|
{
|
||||||
|
public class CierreCuentaCorrienteHistorialDto
|
||||||
|
{
|
||||||
|
public int Id_Historial { get; set; }
|
||||||
|
public int Id_Cierre { get; set; }
|
||||||
|
public int Id_Distribuidor { get; set; }
|
||||||
|
public int Id_Empresa { get; set; }
|
||||||
|
public DateTime FechaCorte { get; set; }
|
||||||
|
public DateTime FechaCierre { get; set; }
|
||||||
|
public decimal SaldoCierre { get; set; }
|
||||||
|
public string Estado { get; set; } = string.Empty;
|
||||||
|
public string? Justificacion { get; set; }
|
||||||
|
public int Id_Usuario_Cierre { get; set; }
|
||||||
|
public int? Id_Usuario_Anula { get; set; }
|
||||||
|
public DateTime? FechaAnulacion { get; set; }
|
||||||
|
public string? Justificacion_Anulacion { get; set; }
|
||||||
|
|
||||||
|
public string TipoMod { get; set; } = string.Empty; // 'Creacion' | 'Reapertura' | 'Modificacion'
|
||||||
|
public int Id_Usuario_Mod { get; set; }
|
||||||
|
public string NombreUsuarioModifico { get; set; } = string.Empty;
|
||||||
|
public DateTime FechaMod { get; set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
|
using System;
|
||||||
using System.ComponentModel.DataAnnotations;
|
using System.ComponentModel.DataAnnotations;
|
||||||
|
|
||||||
namespace GestionIntegral.Api.Dtos.Contables
|
namespace GestionIntegral.Api.Dtos.Contables
|
||||||
@@ -24,5 +25,11 @@ namespace GestionIntegral.Api.Dtos.Contables
|
|||||||
[Required(ErrorMessage = "La justificación del ajuste es obligatoria.")]
|
[Required(ErrorMessage = "La justificación del ajuste es obligatoria.")]
|
||||||
[StringLength(250, MinimumLength = 5, ErrorMessage = "La justificación debe tener entre 5 y 250 caracteres.")]
|
[StringLength(250, MinimumLength = 5, ErrorMessage = "La justificación debe tener entre 5 y 250 caracteres.")]
|
||||||
public string Justificacion { get; set; } = string.Empty;
|
public string Justificacion { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
// Fecha lógica de la operación. Se valida contra el último cierre vigente
|
||||||
|
// del par (Distribuidor + Empresa) para bloquear ajustes en períodos cerrados.
|
||||||
|
// Distinta de FechaAjuste, que es el momento de ejecución del ajuste en el sistema.
|
||||||
|
[Required(ErrorMessage = "La fecha de operación es obligatoria.")]
|
||||||
|
public DateTime FechaOperacion { get; set; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
using System;
|
||||||
|
|
||||||
|
namespace GestionIntegral.Api.Dtos.Contables
|
||||||
|
{
|
||||||
|
public class CierreCuentaCorrienteDto
|
||||||
|
{
|
||||||
|
public int IdCierre { get; set; }
|
||||||
|
public int IdDistribuidor { get; set; }
|
||||||
|
public string NombreDistribuidor { get; set; } = string.Empty;
|
||||||
|
public int IdEmpresa { get; set; }
|
||||||
|
public string NombreEmpresa { get; set; } = string.Empty;
|
||||||
|
public string FechaCorte { get; set; } = string.Empty; // yyyy-MM-dd
|
||||||
|
public DateTime FechaCierre { get; set; }
|
||||||
|
public decimal SaldoCierre { get; set; }
|
||||||
|
public string Estado { get; set; } = string.Empty;
|
||||||
|
public string? Justificacion { get; set; }
|
||||||
|
public int IdUsuarioCierre { get; set; }
|
||||||
|
public string NombreUsuarioCierre { get; set; } = string.Empty;
|
||||||
|
public int? IdUsuarioAnula { get; set; }
|
||||||
|
public string? NombreUsuarioAnula { get; set; }
|
||||||
|
public DateTime? FechaAnulacion { get; set; }
|
||||||
|
public string? JustificacionAnulacion { get; set; }
|
||||||
|
public bool EsUltimoVigente { get; set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
using System;
|
||||||
|
using System.ComponentModel.DataAnnotations;
|
||||||
|
|
||||||
|
namespace GestionIntegral.Api.Dtos.Contables
|
||||||
|
{
|
||||||
|
public class CrearCierreDto
|
||||||
|
{
|
||||||
|
[Required(ErrorMessage = "El distribuidor es obligatorio.")]
|
||||||
|
[Range(1, int.MaxValue, ErrorMessage = "ID de Distribuidor inválido.")]
|
||||||
|
public int IdDistribuidor { get; set; }
|
||||||
|
|
||||||
|
[Required(ErrorMessage = "La empresa es obligatoria.")]
|
||||||
|
[Range(1, int.MaxValue, ErrorMessage = "ID de Empresa inválido.")]
|
||||||
|
public int IdEmpresa { get; set; }
|
||||||
|
|
||||||
|
[Required(ErrorMessage = "La fecha de corte es obligatoria.")]
|
||||||
|
public DateTime FechaCorte { get; set; }
|
||||||
|
|
||||||
|
[StringLength(500, ErrorMessage = "La justificación no puede superar los 500 caracteres.")]
|
||||||
|
public string? Justificacion { get; set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
using System.ComponentModel.DataAnnotations;
|
||||||
|
|
||||||
|
namespace GestionIntegral.Api.Dtos.Contables
|
||||||
|
{
|
||||||
|
public class ReabrirCierreDto
|
||||||
|
{
|
||||||
|
[Required(ErrorMessage = "La justificación es obligatoria al reabrir un cierre.")]
|
||||||
|
[StringLength(500, MinimumLength = 10, ErrorMessage = "La justificación debe tener entre 10 y 500 caracteres.")]
|
||||||
|
public string Justificacion { get; set; } = string.Empty;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
using System;
|
||||||
|
|
||||||
|
namespace GestionIntegral.Api.Dtos.Contables
|
||||||
|
{
|
||||||
|
public class UltimoCierreDto
|
||||||
|
{
|
||||||
|
public int IdCierre { get; set; }
|
||||||
|
public string FechaCorte { get; set; } = string.Empty; // yyyy-MM-dd
|
||||||
|
public decimal SaldoCierre { get; set; }
|
||||||
|
public string Estado { get; set; } = string.Empty;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -15,5 +15,7 @@ namespace GestionIntegral.Api.Dtos.Distribucion
|
|||||||
public string? Telefono { get; set; }
|
public string? Telefono { get; set; }
|
||||||
public string? Email { get; set; }
|
public string? Email { get; set; }
|
||||||
public string? Localidad { get; set; }
|
public string? Localidad { get; set; }
|
||||||
|
public bool Baja { get; set; }
|
||||||
|
public DateTime? FechaBaja { get; set; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
using System;
|
||||||
|
|
||||||
|
namespace GestionIntegral.Api.Dtos.Distribucion
|
||||||
|
{
|
||||||
|
public class ToggleBajaDistribuidorDto
|
||||||
|
{
|
||||||
|
public bool DarDeBaja { get; set; }
|
||||||
|
public DateTime? FechaBaja { get; set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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; }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -7,8 +7,12 @@ namespace GestionIntegral.Api.Dtos.Reportes
|
|||||||
public IEnumerable<BalanceCuentaDistDto> EntradasSalidas { get; set; } = new List<BalanceCuentaDistDto>();
|
public IEnumerable<BalanceCuentaDistDto> EntradasSalidas { get; set; } = new List<BalanceCuentaDistDto>();
|
||||||
public IEnumerable<BalanceCuentaDebCredDto> DebitosCreditos { get; set; } = new List<BalanceCuentaDebCredDto>();
|
public IEnumerable<BalanceCuentaDebCredDto> DebitosCreditos { get; set; } = new List<BalanceCuentaDebCredDto>();
|
||||||
public IEnumerable<BalanceCuentaPagosDto> Pagos { get; set; } = new List<BalanceCuentaPagosDto>();
|
public IEnumerable<BalanceCuentaPagosDto> Pagos { get; set; } = new List<BalanceCuentaPagosDto>();
|
||||||
public IEnumerable<SaldoDto> Saldos { get; set; } = new List<SaldoDto>(); // O podría ser SaldoDto SaldoActual si siempre es uno
|
public string? NombreDistribuidor { get; set; }
|
||||||
public string? NombreDistribuidor { get; set; } // Para el título del reporte
|
public string? NombreEmpresa { get; set; }
|
||||||
public string? NombreEmpresa { get; set; } // Para el título del reporte
|
|
||||||
|
// Saldo a la fecha desde elegida en el filtro:
|
||||||
|
// - Si existe cierre con FechaCorte < FechaDesde: SaldoCierre + movimientos netos entre fechaCierre+1 y fechaDesde-1.
|
||||||
|
// - Sin cierres previos: 0.
|
||||||
|
public decimal SaldoInicial { get; set; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -11,8 +11,9 @@ namespace GestionIntegral.Api.Dtos.Reportes.ViewModels
|
|||||||
public IEnumerable<BalanceCuentaPagosDto> Pagos { get; set; } = new List<BalanceCuentaPagosDto>();
|
public IEnumerable<BalanceCuentaPagosDto> Pagos { get; set; } = new List<BalanceCuentaPagosDto>();
|
||||||
public IEnumerable<BalanceCuentaDebCredDto> DebitosCreditos { get; set; } = new List<BalanceCuentaDebCredDto>();
|
public IEnumerable<BalanceCuentaDebCredDto> DebitosCreditos { get; set; } = new List<BalanceCuentaDebCredDto>();
|
||||||
|
|
||||||
// Saldo real de la cuenta, se muestra al final sin usarse en cálculos intermedios.
|
// Saldo inicial del período: snapshot del último cierre + movimientos netos hasta fechaDesde.
|
||||||
public decimal SaldoDeCuenta { get; set; }
|
// 0 si no hay cierre previo.
|
||||||
|
public decimal SaldoInicial { get; set; }
|
||||||
|
|
||||||
// --- Parámetros del reporte ---
|
// --- Parámetros del reporte ---
|
||||||
public string NombreDistribuidor { get; set; } = string.Empty;
|
public string NombreDistribuidor { get; set; } = string.Empty;
|
||||||
@@ -24,6 +25,8 @@ namespace GestionIntegral.Api.Dtos.Reportes.ViewModels
|
|||||||
public decimal TotalMovimientos => Movimientos.Sum(m => m.Debe - m.Haber);
|
public decimal TotalMovimientos => Movimientos.Sum(m => m.Debe - m.Haber);
|
||||||
public decimal TotalPagos => Pagos.Sum(p => p.Debe - p.Haber);
|
public decimal TotalPagos => Pagos.Sum(p => p.Debe - p.Haber);
|
||||||
public decimal TotalDebitosCreditos => DebitosCreditos.Sum(d => d.Debe - d.Haber);
|
public decimal TotalDebitosCreditos => DebitosCreditos.Sum(d => d.Debe - d.Haber);
|
||||||
public decimal TotalPeriodo => TotalMovimientos + TotalPagos + TotalDebitosCreditos;
|
|
||||||
|
// Saldo Final = Saldo Inicial + suma neta del período (Debe - Haber por sección).
|
||||||
|
public decimal SaldoFinal => SaldoInicial + TotalMovimientos + TotalPagos + TotalDebitosCreditos;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -30,6 +30,22 @@ namespace GestionIntegral.Api.Dtos.Reportes.ViewModels
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public decimal PorcentajeDevolucionGeneral
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
if (PromediosPorDia == null || !PromediosPorDia.Any()) return 0;
|
||||||
|
|
||||||
|
var totalPonderadoLlevados = PromediosPorDia.Sum(p => p.Promedio_Llevados * p.Cant);
|
||||||
|
var totalPonderadoDevueltos = PromediosPorDia.Sum(p => p.Promedio_Devueltos * p.Cant);
|
||||||
|
|
||||||
|
if (totalPonderadoLlevados == 0) return 0;
|
||||||
|
|
||||||
|
// Calculamos el porcentaje usando los totales ponderados para máxima precisión como lo hace el frontend.
|
||||||
|
return (decimal)totalPonderadoDevueltos * 100 / totalPonderadoLlevados;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// --- PROPIEDAD PARA LA FILA "GENERAL" ---
|
// --- PROPIEDAD PARA LA FILA "GENERAL" ---
|
||||||
public ListadoDistribucionCanillasPromedioDiaDto? PromedioGeneral
|
public ListadoDistribucionCanillasPromedioDiaDto? PromedioGeneral
|
||||||
{
|
{
|
||||||
@@ -37,20 +53,27 @@ namespace GestionIntegral.Api.Dtos.Reportes.ViewModels
|
|||||||
{
|
{
|
||||||
if (PromediosPorDia == null || !PromediosPorDia.Any()) return null;
|
if (PromediosPorDia == null || !PromediosPorDia.Any()) return null;
|
||||||
|
|
||||||
// Sumamos los totales, no promediamos los promedios
|
// Sumamos los totales ponderados para cada columna
|
||||||
var totalLlevados = PromediosPorDia.Sum(p => p.Llevados);
|
var totalPonderadoLlevados = PromediosPorDia.Sum(p => p.Promedio_Llevados * p.Cant);
|
||||||
var totalDevueltos = PromediosPorDia.Sum(p => p.Devueltos);
|
var totalPonderadoDevueltos = PromediosPorDia.Sum(p => p.Promedio_Devueltos * p.Cant);
|
||||||
|
var totalPonderadoVentas = PromediosPorDia.Sum(p => p.Promedio_Ventas * p.Cant);
|
||||||
var totalDias = PromediosPorDia.Sum(p => p.Cant);
|
var totalDias = PromediosPorDia.Sum(p => p.Cant);
|
||||||
|
|
||||||
if (totalDias == 0) return null;
|
if (totalDias == 0) return null;
|
||||||
|
|
||||||
|
// Usamos Math.Round para un redondeo matemático estándar antes de la conversión.
|
||||||
|
// MidpointRounding.AwayFromZero asegura que .5 se redondee hacia arriba, igual que en JavaScript.
|
||||||
|
var promGeneralLlevados = (int)Math.Round((decimal)totalPonderadoLlevados / totalDias, MidpointRounding.AwayFromZero);
|
||||||
|
var promGeneralDevueltos = (int)Math.Round((decimal)totalPonderadoDevueltos / totalDias, MidpointRounding.AwayFromZero);
|
||||||
|
var promGeneralVentas = (int)Math.Round((decimal)totalPonderadoVentas / totalDias, MidpointRounding.AwayFromZero);
|
||||||
|
|
||||||
return new ListadoDistribucionCanillasPromedioDiaDto
|
return new ListadoDistribucionCanillasPromedioDiaDto
|
||||||
{
|
{
|
||||||
Dia = "General",
|
Dia = "General",
|
||||||
Cant = totalDias,
|
Cant = totalDias,
|
||||||
Promedio_Llevados = totalLlevados / totalDias,
|
Promedio_Llevados = promGeneralLlevados,
|
||||||
Promedio_Devueltos = totalDevueltos / totalDias,
|
Promedio_Devueltos = promGeneralDevueltos,
|
||||||
Promedio_Ventas = (totalLlevados - totalDevueltos) / totalDias
|
Promedio_Ventas = promGeneralVentas
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,26 +20,26 @@ namespace GestionIntegral.Api.Dtos.Reportes.ViewModels
|
|||||||
{
|
{
|
||||||
get
|
get
|
||||||
{
|
{
|
||||||
if (DetalleDiario == null || !DetalleDiario.Any())
|
if (PromediosPorDia == null || !PromediosPorDia.Any())
|
||||||
{
|
{
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
var promediosValidos = PromediosPorDia.Where(p => p.Dia != "General").ToList();
|
||||||
var diasConDatos = DetalleDiario.Count(d => (d.Llevados ?? 0) > 0);
|
if (!promediosValidos.Any()) return null;
|
||||||
if (diasConDatos == 0) return null;
|
var countPromedios = promediosValidos.Count;
|
||||||
|
var sumPromLlevados = promediosValidos.Sum(p => p.Promedio_Llevados ?? 0);
|
||||||
var totalLlevados = DetalleDiario.Sum(d => d.Llevados ?? 0);
|
var sumPromDevueltos = promediosValidos.Sum(p => p.Promedio_Devueltos ?? 0);
|
||||||
var totalDevueltos = DetalleDiario.Sum(d => d.Devueltos ?? 0);
|
var sumPromVentas = promediosValidos.Sum(p => p.Promedio_Ventas ?? 0);
|
||||||
|
|
||||||
return new ListadoDistribucionDistPromedioDiaDto
|
return new ListadoDistribucionDistPromedioDiaDto
|
||||||
{
|
{
|
||||||
Dia = "General",
|
Dia = "General",
|
||||||
Cant = diasConDatos,
|
Cant = promediosValidos.Sum(p => p.Cant ?? 0),
|
||||||
Promedio_Llevados = totalLlevados / diasConDatos,
|
Promedio_Llevados = (int)Math.Round((decimal)sumPromLlevados / countPromedios, MidpointRounding.AwayFromZero),
|
||||||
Promedio_Devueltos = totalDevueltos / diasConDatos,
|
Promedio_Devueltos = (int)Math.Round((decimal)sumPromDevueltos / countPromedios, MidpointRounding.AwayFromZero),
|
||||||
Promedio_Ventas = (totalLlevados - totalDevueltos) / diasConDatos,
|
Promedio_Ventas = (int)Math.Round((decimal)sumPromVentas / countPromedios, MidpointRounding.AwayFromZero),
|
||||||
Llevados = totalLlevados, // Guardamos el total para el cálculo del %
|
Llevados = (int)Math.Round((decimal)sumPromLlevados / countPromedios, MidpointRounding.AwayFromZero),
|
||||||
Devueltos = totalDevueltos // Guardamos el total para el cálculo del %
|
Devueltos = (int)Math.Round((decimal)sumPromDevueltos / countPromedios, MidpointRounding.AwayFromZero)
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,6 +8,9 @@ namespace GestionIntegral.Api.Dtos.Suscripciones
|
|||||||
public string EstadoPago { get; set; } = string.Empty;
|
public string EstadoPago { get; set; } = string.Empty;
|
||||||
public string EstadoFacturacion { get; set; } = string.Empty;
|
public string EstadoFacturacion { get; set; } = string.Empty;
|
||||||
public string? NumeroFactura { get; set; }
|
public string? NumeroFactura { get; set; }
|
||||||
|
public decimal TotalPagado { get; set; }
|
||||||
|
public string TipoFactura { get; set; } = string.Empty;
|
||||||
|
public int IdSuscriptor { get; set; }
|
||||||
public List<FacturaDetalleDto> Detalles { get; set; } = new List<FacturaDetalleDto>();
|
public List<FacturaDetalleDto> Detalles { get; set; } = new List<FacturaDetalleDto>();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -15,5 +15,6 @@ namespace GestionIntegral.Api.Models.Suscripciones
|
|||||||
public string? NumeroFactura { get; set; }
|
public string? NumeroFactura { get; set; }
|
||||||
public int? IdLoteDebito { get; set; }
|
public int? IdLoteDebito { get; set; }
|
||||||
public string? MotivoRechazo { get; set; }
|
public string? MotivoRechazo { get; set; }
|
||||||
|
public string TipoFactura { get; set; } = string.Empty;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -23,10 +23,7 @@ using GestionIntegral.Api.Services.Suscripciones;
|
|||||||
using GestionIntegral.Api.Models.Comunicaciones;
|
using GestionIntegral.Api.Models.Comunicaciones;
|
||||||
using GestionIntegral.Api.Services.Comunicaciones;
|
using GestionIntegral.Api.Services.Comunicaciones;
|
||||||
using GestionIntegral.Api.Data.Repositories.Comunicaciones;
|
using GestionIntegral.Api.Data.Repositories.Comunicaciones;
|
||||||
|
using GestionIntegral.Api.Middleware;
|
||||||
// Carga las variables de entorno desde el archivo .env al inicio de la aplicación.
|
|
||||||
// Debe ser la primera línea para que la configuración esté disponible para el 'builder'.
|
|
||||||
DotNetEnv.Env.Load();
|
|
||||||
|
|
||||||
var builder = WebApplication.CreateBuilder(args);
|
var builder = WebApplication.CreateBuilder(args);
|
||||||
|
|
||||||
@@ -100,6 +97,12 @@ builder.Services.AddScoped<ICambioParadaRepository, CambioParadaRepository>();
|
|||||||
builder.Services.AddScoped<ICambioParadaService, CambioParadaService>();
|
builder.Services.AddScoped<ICambioParadaService, CambioParadaService>();
|
||||||
// Servicio de Saldos
|
// Servicio de Saldos
|
||||||
builder.Services.AddScoped<ISaldoService, SaldoService>();
|
builder.Services.AddScoped<ISaldoService, SaldoService>();
|
||||||
|
// Cierre de Cuenta Corriente de Distribuidor
|
||||||
|
builder.Services.AddMemoryCache();
|
||||||
|
builder.Services.AddScoped<ICierreCuentaCorrienteRepository, CierreCuentaCorrienteRepository>();
|
||||||
|
builder.Services.AddScoped<ICierreCuentaCorrienteService, CierreCuentaCorrienteService>();
|
||||||
|
// Validador de período cerrado: SINGLETON porque mantiene cache en memoria de IMemoryCache que debe ser compartido entre requests.
|
||||||
|
builder.Services.AddSingleton<IPeriodoCerradoValidator, PeriodoCerradoValidator>();
|
||||||
// Repositorios de Reportes
|
// Repositorios de Reportes
|
||||||
builder.Services.AddScoped<IReportesRepository, ReportesRepository>();
|
builder.Services.AddScoped<IReportesRepository, ReportesRepository>();
|
||||||
// Servicios de Reportes
|
// Servicios de Reportes
|
||||||
@@ -273,6 +276,10 @@ if (app.Environment.IsDevelopment())
|
|||||||
// Comenta o elimina la siguiente línea si SÓLO usas http://localhost:5183
|
// Comenta o elimina la siguiente línea si SÓLO usas http://localhost:5183
|
||||||
//app.UseHttpsRedirection();
|
//app.UseHttpsRedirection();
|
||||||
|
|
||||||
|
// Middleware global de excepciones — debe ir TEMPRANO en el pipeline para catchear cualquier excepción
|
||||||
|
// que escape de los controllers/services. Mapea BloqueoPorPeriodoCerradoException → 409 con cuerpo JSON estandarizado.
|
||||||
|
app.UseMiddleware<ExceptionHandlerMiddleware>();
|
||||||
|
|
||||||
app.UseCors(MyAllowSpecificOrigins);
|
app.UseCors(MyAllowSpecificOrigins);
|
||||||
|
|
||||||
app.UseAuthentication(); // Debe ir ANTES de UseAuthorization
|
app.UseAuthentication(); // Debe ir ANTES de UseAuthorization
|
||||||
|
|||||||
@@ -22,13 +22,16 @@ namespace GestionIntegral.Api.Services.Anomalia
|
|||||||
public async Task<IEnumerable<AlertaGenericaDto>> ObtenerAlertasNoLeidasAsync()
|
public async Task<IEnumerable<AlertaGenericaDto>> ObtenerAlertasNoLeidasAsync()
|
||||||
{
|
{
|
||||||
// Apunta a la nueva tabla genérica 'Sistema_Alertas'
|
// Apunta a la nueva tabla genérica 'Sistema_Alertas'
|
||||||
var query = "SELECT * FROM Sistema_Alertas WHERE Leida = 0 ORDER BY FechaDeteccion DESC";
|
//var query = "SELECT * FROM Sistema_Alertas WHERE Leida = 0 ORDER BY FechaDeteccion DESC";
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
using (var connection = _dbConnectionFactory.CreateConnection())
|
using (var connection = _dbConnectionFactory.CreateConnection())
|
||||||
{
|
{
|
||||||
|
/*
|
||||||
var alertas = await connection.QueryAsync<AlertaGenericaDto>(query);
|
var alertas = await connection.QueryAsync<AlertaGenericaDto>(query);
|
||||||
return alertas ?? Enumerable.Empty<AlertaGenericaDto>();
|
return alertas ?? Enumerable.Empty<AlertaGenericaDto>();
|
||||||
|
*/
|
||||||
|
return Enumerable.Empty<AlertaGenericaDto>();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
catch (System.Exception ex)
|
catch (System.Exception ex)
|
||||||
@@ -40,17 +43,20 @@ namespace GestionIntegral.Api.Services.Anomalia
|
|||||||
|
|
||||||
public async Task<(bool Exito, string? Error)> MarcarComoLeidaAsync(int idAlerta)
|
public async Task<(bool Exito, string? Error)> MarcarComoLeidaAsync(int idAlerta)
|
||||||
{
|
{
|
||||||
var query = "UPDATE Sistema_Alertas SET Leida = 1 WHERE IdAlerta = @IdAlerta";
|
//var query = "UPDATE Sistema_Alertas SET Leida = 1 WHERE IdAlerta = @IdAlerta";
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
using (var connection = _dbConnectionFactory.CreateConnection())
|
using (var connection = _dbConnectionFactory.CreateConnection())
|
||||||
{
|
{
|
||||||
|
/*
|
||||||
var result = await connection.ExecuteAsync(query, new { IdAlerta = idAlerta });
|
var result = await connection.ExecuteAsync(query, new { IdAlerta = idAlerta });
|
||||||
if (result > 0)
|
if (result > 0)
|
||||||
{
|
{
|
||||||
return (true, null);
|
return (true, null);
|
||||||
}
|
}
|
||||||
return (false, "La alerta no fue encontrada o ya estaba marcada.");
|
return (false, "La alerta no fue encontrada o ya estaba marcada.");
|
||||||
|
*/
|
||||||
|
return (true, null); // Retornar éxito silencioso por ahora
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
catch (System.Exception ex)
|
catch (System.Exception ex)
|
||||||
@@ -62,15 +68,18 @@ namespace GestionIntegral.Api.Services.Anomalia
|
|||||||
|
|
||||||
public async Task<(bool Exito, string? Error)> MarcarGrupoComoLeidoAsync(string tipoAlerta, int idEntidad)
|
public async Task<(bool Exito, string? Error)> MarcarGrupoComoLeidoAsync(string tipoAlerta, int idEntidad)
|
||||||
{
|
{
|
||||||
var query = "UPDATE Sistema_Alertas SET Leida = 1 WHERE TipoAlerta = @TipoAlerta AND IdEntidad = @IdEntidad AND Leida = 0";
|
//var query = "UPDATE Sistema_Alertas SET Leida = 1 WHERE TipoAlerta = @TipoAlerta AND IdEntidad = @IdEntidad AND Leida = 0";
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
using (var connection = _dbConnectionFactory.CreateConnection())
|
using (var connection = _dbConnectionFactory.CreateConnection())
|
||||||
{
|
{
|
||||||
|
/*
|
||||||
var result = await connection.ExecuteAsync(query, new { TipoAlerta = tipoAlerta, IdEntidad = idEntidad });
|
var result = await connection.ExecuteAsync(query, new { TipoAlerta = tipoAlerta, IdEntidad = idEntidad });
|
||||||
// No es un error si no se actualizan filas (puede que no hubiera ninguna para ese grupo)
|
// No es un error si no se actualizan filas (puede que no hubiera ninguna para ese grupo)
|
||||||
_logger.LogInformation("Marcadas como leídas {Count} alertas para Tipo: {Tipo}, EntidadID: {IdEntidad}", result, tipoAlerta, idEntidad);
|
_logger.LogInformation("Marcadas como leídas {Count} alertas para Tipo: {Tipo}, EntidadID: {IdEntidad}", result, tipoAlerta, idEntidad);
|
||||||
return (true, null);
|
return (true, null);
|
||||||
|
*/
|
||||||
|
return (true, null);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
catch (System.Exception ex)
|
catch (System.Exception ex)
|
||||||
|
|||||||
@@ -4,6 +4,8 @@ using MailKit.Net.Smtp;
|
|||||||
using MailKit.Security;
|
using MailKit.Security;
|
||||||
using Microsoft.Extensions.Options;
|
using Microsoft.Extensions.Options;
|
||||||
using MimeKit;
|
using MimeKit;
|
||||||
|
using System.Net.Security;
|
||||||
|
using System.Security.Cryptography.X509Certificates;
|
||||||
|
|
||||||
namespace GestionIntegral.Api.Services.Comunicaciones
|
namespace GestionIntegral.Api.Services.Comunicaciones
|
||||||
{
|
{
|
||||||
@@ -88,6 +90,30 @@ namespace GestionIntegral.Api.Services.Comunicaciones
|
|||||||
using var smtp = new SmtpClient();
|
using var smtp = new SmtpClient();
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
|
// Se añade una política de validación de certificado personalizada.
|
||||||
|
// Esto es necesario para entornos de desarrollo o redes internas donde
|
||||||
|
// el nombre del host al que nos conectamos (ej. una IP) no coincide
|
||||||
|
// con el nombre en el certificado SSL (ej. mail.eldia.com).
|
||||||
|
smtp.ServerCertificateValidationCallback = (sender, certificate, chain, sslPolicyErrors) =>
|
||||||
|
{
|
||||||
|
// Si no hay errores, el certificado es válido.
|
||||||
|
if (sslPolicyErrors == SslPolicyErrors.None)
|
||||||
|
return true;
|
||||||
|
|
||||||
|
// Si el único error es que el nombre no coincide (RemoteCertificateNameMismatch)
|
||||||
|
// Y el certificado es el que esperamos (emitido para "mail.eldia.com"),
|
||||||
|
// entonces lo aceptamos como válido.
|
||||||
|
if (sslPolicyErrors.HasFlag(SslPolicyErrors.RemoteCertificateNameMismatch) && certificate != null && certificate.Subject.Contains("CN=mail.eldia.com"))
|
||||||
|
{
|
||||||
|
_logger.LogWarning("Se aceptó un certificado SSL con 'Name Mismatch' para el host de confianza 'mail.eldia.com'.");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Para cualquier otro error, rechazamos el certificado.
|
||||||
|
_logger.LogError("Error de validación de certificado SSL: {Errors}", sslPolicyErrors);
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
await smtp.ConnectAsync(_mailSettings.SmtpHost, _mailSettings.SmtpPort, SecureSocketOptions.StartTls);
|
await smtp.ConnectAsync(_mailSettings.SmtpHost, _mailSettings.SmtpPort, SecureSocketOptions.StartTls);
|
||||||
await smtp.AuthenticateAsync(_mailSettings.SmtpUser, _mailSettings.SmtpPass);
|
await smtp.AuthenticateAsync(_mailSettings.SmtpUser, _mailSettings.SmtpPass);
|
||||||
await smtp.SendAsync(emailMessage);
|
await smtp.SendAsync(emailMessage);
|
||||||
@@ -95,20 +121,6 @@ namespace GestionIntegral.Api.Services.Comunicaciones
|
|||||||
log.Estado = "Enviado";
|
log.Estado = "Enviado";
|
||||||
_logger.LogInformation("Email enviado exitosamente a {Destinatario}. Asunto: {Asunto}", destinatario, emailMessage.Subject);
|
_logger.LogInformation("Email enviado exitosamente a {Destinatario}. Asunto: {Asunto}", destinatario, emailMessage.Subject);
|
||||||
}
|
}
|
||||||
catch (SmtpCommandException scEx)
|
|
||||||
{
|
|
||||||
_logger.LogError(scEx, "Error de comando SMTP al enviar a {Destinatario}. StatusCode: {StatusCode}", destinatario, scEx.StatusCode);
|
|
||||||
log.Estado = "Fallido";
|
|
||||||
log.Error = $"Error del servidor: ({scEx.StatusCode}) {scEx.Message}";
|
|
||||||
throw;
|
|
||||||
}
|
|
||||||
catch (AuthenticationException authEx)
|
|
||||||
{
|
|
||||||
_logger.LogError(authEx, "Error de autenticación con el servidor SMTP.");
|
|
||||||
log.Estado = "Fallido";
|
|
||||||
log.Error = "Error de autenticación. Revise las credenciales de correo.";
|
|
||||||
throw;
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
_logger.LogError(ex, "Error general al enviar email a {Destinatario}. Asunto: {Asunto}", destinatario, emailMessage.Subject);
|
_logger.LogError(ex, "Error general al enviar email a {Destinatario}. Asunto: {Asunto}", destinatario, emailMessage.Subject);
|
||||||
|
|||||||
@@ -0,0 +1,20 @@
|
|||||||
|
using System;
|
||||||
|
|
||||||
|
namespace GestionIntegral.Api.Services.Contables
|
||||||
|
{
|
||||||
|
// Se lanza cuando un C/U/D intenta afectar la cuenta corriente de un distribuidor en una fecha
|
||||||
|
// que cae dentro de un período cerrado (cue_CierresCuentaCorriente con Estado='Activo').
|
||||||
|
// Mapea al código de error PERIODO_CERRADO_BLOQUEO_OPERACION (HTTP 409).
|
||||||
|
public class BloqueoPorPeriodoCerradoException : Exception
|
||||||
|
{
|
||||||
|
public int IdCierre { get; }
|
||||||
|
public DateTime FechaCorte { get; }
|
||||||
|
|
||||||
|
public BloqueoPorPeriodoCerradoException(int idCierre, DateTime fechaCorte)
|
||||||
|
: base($"El período está cerrado al {fechaCorte:dd/MM/yyyy} (cierre #{idCierre}). No se permiten modificaciones sobre fechas anteriores o iguales a la fecha de corte.")
|
||||||
|
{
|
||||||
|
IdCierre = idCierre;
|
||||||
|
FechaCorte = fechaCorte;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,351 @@
|
|||||||
|
using Dapper;
|
||||||
|
using GestionIntegral.Api.Data;
|
||||||
|
using GestionIntegral.Api.Data.Repositories.Contables;
|
||||||
|
using GestionIntegral.Api.Data.Repositories.Distribucion;
|
||||||
|
using GestionIntegral.Api.Data.Repositories.Reportes;
|
||||||
|
using GestionIntegral.Api.Dtos.Auditoria;
|
||||||
|
using GestionIntegral.Api.Dtos.Contables;
|
||||||
|
using GestionIntegral.Api.Models.Contables;
|
||||||
|
using Microsoft.Data.SqlClient;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Data;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace GestionIntegral.Api.Services.Contables
|
||||||
|
{
|
||||||
|
public class CierreCuentaCorrienteService : ICierreCuentaCorrienteService
|
||||||
|
{
|
||||||
|
private readonly ICierreCuentaCorrienteRepository _cierreRepo;
|
||||||
|
private readonly IDistribuidorRepository _distRepo;
|
||||||
|
private readonly IEmpresaRepository _empresaRepo;
|
||||||
|
private readonly IPeriodoCerradoValidator _validator;
|
||||||
|
private readonly IReportesRepository _reportesRepo;
|
||||||
|
private readonly DbConnectionFactory _cf;
|
||||||
|
private readonly ILogger<CierreCuentaCorrienteService> _logger;
|
||||||
|
|
||||||
|
// Retry de deadlocks (SqlException.Number == 1205) para la transacción Serializable de Crear cierre.
|
||||||
|
private static readonly int[] DeadlockBackoffMs = { 50, 150, 400 };
|
||||||
|
|
||||||
|
// Sentinela para "sin límite inferior" en el cálculo histórico — anterior a cualquier dato real.
|
||||||
|
private static readonly DateTime InicioHistoria = new DateTime(1900, 1, 1);
|
||||||
|
|
||||||
|
public CierreCuentaCorrienteService(
|
||||||
|
ICierreCuentaCorrienteRepository cierreRepo,
|
||||||
|
IDistribuidorRepository distRepo,
|
||||||
|
IEmpresaRepository empresaRepo,
|
||||||
|
IPeriodoCerradoValidator validator,
|
||||||
|
IReportesRepository reportesRepo,
|
||||||
|
DbConnectionFactory cf,
|
||||||
|
ILogger<CierreCuentaCorrienteService> logger)
|
||||||
|
{
|
||||||
|
_cierreRepo = cierreRepo;
|
||||||
|
_distRepo = distRepo;
|
||||||
|
_empresaRepo = empresaRepo;
|
||||||
|
_validator = validator;
|
||||||
|
_reportesRepo = reportesRepo;
|
||||||
|
_cf = cf;
|
||||||
|
_logger = logger;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<(CierreCuentaCorrienteDto? Cierre, string? ErrorCode, string? ErrorMessage)> CrearCierreAsync(CrearCierreDto dto, int idUsuario)
|
||||||
|
{
|
||||||
|
// Validaciones pre-transacción (no requieren lock).
|
||||||
|
if (dto.FechaCorte.Date > DateTime.Today)
|
||||||
|
return (null, "CIERRE_FECHA_FUTURA", "La fecha de corte no puede ser futura.");
|
||||||
|
|
||||||
|
var distribuidor = await _distRepo.GetByIdSimpleAsync(dto.IdDistribuidor);
|
||||||
|
if (distribuidor == null)
|
||||||
|
return (null, "CIERRE_DISTRIBUIDOR_BAJA", "El distribuidor especificado no existe.");
|
||||||
|
if (distribuidor.Baja)
|
||||||
|
return (null, "CIERRE_DISTRIBUIDOR_BAJA", "El distribuidor está dado de baja.");
|
||||||
|
|
||||||
|
if (await _empresaRepo.GetByIdAsync(dto.IdEmpresa) == null)
|
||||||
|
return (null, "CIERRE_EMPRESA_INEXISTENTE", "La empresa especificada no existe.");
|
||||||
|
|
||||||
|
// Retry loop ante deadlock 1205. Los range locks de Serializable sobre las queries de movimientos
|
||||||
|
// pueden chocar con transacciones concurrentes que insertan pagos/notas/ajustes/movimientos.
|
||||||
|
for (int attempt = 0; ; attempt++)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
return await CrearCierreInternalAsync(dto, idUsuario);
|
||||||
|
}
|
||||||
|
catch (SqlException ex) when (ex.Number == 1205 && attempt < DeadlockBackoffMs.Length)
|
||||||
|
{
|
||||||
|
_logger.LogWarning("Deadlock 1205 al crear cierre Dist={IdDist} Emp={IdEmp}, intento {Attempt}. Reintentando.", dto.IdDistribuidor, dto.IdEmpresa, attempt + 1);
|
||||||
|
await Task.Delay(DeadlockBackoffMs[attempt]);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "Error inesperado al crear cierre Dist={IdDist} Emp={IdEmp}.", dto.IdDistribuidor, dto.IdEmpresa);
|
||||||
|
return (null, "CIERRE_ERROR_INTERNO", $"Error interno: {ex.Message}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<(CierreCuentaCorrienteDto? Cierre, string? ErrorCode, string? ErrorMessage)> CrearCierreInternalAsync(CrearCierreDto dto, int idUsuario)
|
||||||
|
{
|
||||||
|
using var connection = _cf.CreateConnection();
|
||||||
|
if (connection.State != ConnectionState.Open)
|
||||||
|
{
|
||||||
|
if (connection is System.Data.Common.DbConnection dbConn) await dbConn.OpenAsync(); else connection.Open();
|
||||||
|
}
|
||||||
|
|
||||||
|
using var transaction = connection.BeginTransaction(IsolationLevel.Serializable);
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// 1. Lookup último cierre dentro de la transacción Serializable.
|
||||||
|
var ultimo = await _cierreRepo.GetUltimoCierreVigenteAsync(dto.IdDistribuidor, dto.IdEmpresa, transaction);
|
||||||
|
if (ultimo != null && dto.FechaCorte.Date <= ultimo.FechaCorte.Date)
|
||||||
|
{
|
||||||
|
transaction.Rollback();
|
||||||
|
return (null, "CIERRE_FECHA_ANTERIOR_A_ULTIMO",
|
||||||
|
$"Existe un cierre vigente al {ultimo.FechaCorte:dd/MM/yyyy}. La fecha de corte debe ser posterior.");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Calcular SaldoCierre por suma de movimientos (la tabla cue_Saldos NO se consulta — queda paralela
|
||||||
|
// para conciliación y su lógica original es independiente).
|
||||||
|
// Si hay cierre anterior: desde (ultimo.FechaCorte + 1 día) hasta dto.FechaCorte INCLUSIVE.
|
||||||
|
// Si no hay cierre anterior: desde InicioHistoria hasta dto.FechaCorte INCLUSIVE (histórico completo).
|
||||||
|
DateTime desde = ultimo != null ? ultimo.FechaCorte.Date.AddDays(1) : InicioHistoria;
|
||||||
|
DateTime hasta = dto.FechaCorte.Date;
|
||||||
|
|
||||||
|
decimal sumaMovimientos = await CalcularSaldoEntreFechasAsync(
|
||||||
|
dto.IdDistribuidor, dto.IdEmpresa, desde, hasta, connection, transaction);
|
||||||
|
|
||||||
|
decimal saldoBase = ultimo?.SaldoCierre ?? 0m;
|
||||||
|
decimal saldoCierre = saldoBase + sumaMovimientos;
|
||||||
|
|
||||||
|
// 3. Crear el cierre + entry _H ('Creacion') dentro de la misma transacción.
|
||||||
|
var nuevo = new CierreCuentaCorriente
|
||||||
|
{
|
||||||
|
IdDistribuidor = dto.IdDistribuidor,
|
||||||
|
IdEmpresa = dto.IdEmpresa,
|
||||||
|
FechaCorte = dto.FechaCorte.Date,
|
||||||
|
FechaCierre = DateTime.Now,
|
||||||
|
SaldoCierre = saldoCierre,
|
||||||
|
Estado = "Activo",
|
||||||
|
Justificacion = dto.Justificacion,
|
||||||
|
IdUsuarioCierre = idUsuario
|
||||||
|
};
|
||||||
|
|
||||||
|
int idCierre = await _cierreRepo.CreateAsync(nuevo, idUsuario, transaction);
|
||||||
|
|
||||||
|
transaction.Commit();
|
||||||
|
_logger.LogInformation("Cierre #{Id} creado Dist={IdDist} Emp={IdEmp} FechaCorte={FechaCorte} Saldo={Saldo} (base={Base}, suma={Suma})",
|
||||||
|
idCierre, dto.IdDistribuidor, dto.IdEmpresa, dto.FechaCorte, saldoCierre, saldoBase, sumaMovimientos);
|
||||||
|
|
||||||
|
// 4. Invalidar cache del validador post-commit.
|
||||||
|
_validator.InvalidarCache(dto.IdDistribuidor, dto.IdEmpresa);
|
||||||
|
|
||||||
|
// 5. Mapear DTO completo (con nombres) para la respuesta.
|
||||||
|
var dtoCreado = (await _cierreRepo.GetAllAsync(dto.IdDistribuidor, dto.IdEmpresa, null, null, null))
|
||||||
|
.FirstOrDefault(c => c.IdCierre == idCierre);
|
||||||
|
|
||||||
|
return (dtoCreado, null, null);
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
try { transaction.Rollback(); } catch (Exception rbEx) { _logger.LogError(rbEx, "Error en Rollback de CrearCierreInternalAsync."); }
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
if (connection.State == ConnectionState.Open)
|
||||||
|
{
|
||||||
|
if (connection is System.Data.Common.DbConnection dbConn) await dbConn.CloseAsync(); else connection.Close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<(CierreCuentaCorrienteDto? Cierre, string? ErrorCode, string? ErrorMessage)> ReabrirCierreAsync(int idCierre, ReabrirCierreDto dto, int idUsuario, bool esSuperAdmin)
|
||||||
|
{
|
||||||
|
if (!esSuperAdmin)
|
||||||
|
return (null, "CIERRE_PERMISO_DENEGADO", "Sólo SuperAdmin puede reabrir cierres.");
|
||||||
|
|
||||||
|
// Re-validar justificación a nivel service (defensa en profundidad — el DTO ya tiene DataAnnotations).
|
||||||
|
if (string.IsNullOrWhiteSpace(dto.Justificacion) || dto.Justificacion.Trim().Length < 10)
|
||||||
|
return (null, "CIERRE_JUSTIFICACION_OBLIGATORIA", "La justificación es obligatoria y debe tener al menos 10 caracteres.");
|
||||||
|
|
||||||
|
using var connection = _cf.CreateConnection();
|
||||||
|
if (connection.State != ConnectionState.Open)
|
||||||
|
{
|
||||||
|
if (connection is System.Data.Common.DbConnection dbConn) await dbConn.OpenAsync(); else connection.Open();
|
||||||
|
}
|
||||||
|
|
||||||
|
using var transaction = connection.BeginTransaction(IsolationLevel.Serializable);
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var cierre = await _cierreRepo.GetByIdAsync(idCierre);
|
||||||
|
if (cierre == null)
|
||||||
|
{
|
||||||
|
transaction.Rollback();
|
||||||
|
return (null, "CIERRE_NO_ENCONTRADO", "El cierre solicitado no existe.");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (cierre.Estado != "Activo")
|
||||||
|
{
|
||||||
|
transaction.Rollback();
|
||||||
|
return (null, "CIERRE_YA_ANULADO", "El cierre ya está anulado.");
|
||||||
|
}
|
||||||
|
|
||||||
|
bool hayPosterior = await _cierreRepo.ExisteCierrePosteriorVigenteAsync(
|
||||||
|
cierre.IdDistribuidor, cierre.IdEmpresa, cierre.FechaCorte, idCierre, transaction);
|
||||||
|
if (hayPosterior)
|
||||||
|
{
|
||||||
|
transaction.Rollback();
|
||||||
|
return (null, "CIERRE_HAY_POSTERIORES_VIGENTES",
|
||||||
|
"Existe un cierre posterior vigente. Reabrir primero los cierres posteriores en cascada manual.");
|
||||||
|
}
|
||||||
|
|
||||||
|
bool anulado = await _cierreRepo.AnularAsync(idCierre, idUsuario, dto.Justificacion.Trim(), idUsuario, transaction);
|
||||||
|
if (!anulado)
|
||||||
|
{
|
||||||
|
transaction.Rollback();
|
||||||
|
// Carrera concurrente: alguien lo anuló entre el GetByIdAsync y el UPDATE atómico.
|
||||||
|
return (null, "CIERRE_YA_ANULADO", "El cierre fue anulado concurrentemente por otra operación.");
|
||||||
|
}
|
||||||
|
|
||||||
|
transaction.Commit();
|
||||||
|
_logger.LogInformation("Cierre #{Id} reabierto (anulado) por Usuario {IdUsuario}.", idCierre, idUsuario);
|
||||||
|
|
||||||
|
_validator.InvalidarCache(cierre.IdDistribuidor, cierre.IdEmpresa);
|
||||||
|
|
||||||
|
var dtoActualizado = (await _cierreRepo.GetAllAsync(cierre.IdDistribuidor, cierre.IdEmpresa, null, null, null))
|
||||||
|
.FirstOrDefault(c => c.IdCierre == idCierre);
|
||||||
|
return (dtoActualizado, null, null);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
try { transaction.Rollback(); } catch (Exception rbEx) { _logger.LogError(rbEx, "Error en Rollback de ReabrirCierreAsync."); }
|
||||||
|
_logger.LogError(ex, "Error inesperado al reabrir cierre #{Id}.", idCierre);
|
||||||
|
return (null, "CIERRE_ERROR_INTERNO", $"Error interno: {ex.Message}");
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
if (connection.State == ConnectionState.Open)
|
||||||
|
{
|
||||||
|
if (connection is System.Data.Common.DbConnection dbConn) await dbConn.CloseAsync(); else connection.Close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<decimal> CalcularSaldoInicialReporteAsync(int idDistribuidor, int idEmpresa, DateTime fechaDesde)
|
||||||
|
{
|
||||||
|
// 1. Buscar último cierre vigente con FechaCorte ESTRICTAMENTE menor a fechaDesde.
|
||||||
|
// Si no hay cierre previo, saldoInicial = 0 (comportamiento "como antes" del reporte).
|
||||||
|
const string sqlUltimoCierre = @"
|
||||||
|
SELECT TOP 1 FechaCorte, SaldoCierre
|
||||||
|
FROM dbo.cue_CierresCuentaCorriente
|
||||||
|
WHERE Id_Distribuidor = @IdDist
|
||||||
|
AND Id_Empresa = @IdEmp
|
||||||
|
AND Estado = 'Activo'
|
||||||
|
AND FechaCorte < @FechaDesde
|
||||||
|
ORDER BY FechaCorte DESC, Id_Cierre DESC;";
|
||||||
|
|
||||||
|
using var connection = _cf.CreateConnection();
|
||||||
|
var ultimo = await connection.QuerySingleOrDefaultAsync<(DateTime FechaCorte, decimal SaldoCierre)?>(
|
||||||
|
sqlUltimoCierre,
|
||||||
|
new { IdDist = idDistribuidor, IdEmp = idEmpresa, FechaDesde = fechaDesde.Date });
|
||||||
|
|
||||||
|
if (ultimo == null) return 0m;
|
||||||
|
|
||||||
|
// 2. Sumar movimientos netos entre (fechaCorte + 1 día) y (fechaDesde - 1 día) — INCLUSIVE en ambos extremos.
|
||||||
|
// El SaldoCierre ya incluye los movimientos hasta fechaCorte, así que arrancamos al día siguiente.
|
||||||
|
DateTime desdeMov = ultimo.Value.FechaCorte.Date.AddDays(1);
|
||||||
|
DateTime hastaMov = fechaDesde.Date.AddDays(-1);
|
||||||
|
|
||||||
|
decimal sumaMovs = await CalcularSaldoEntreFechasAsync(
|
||||||
|
idDistribuidor, idEmpresa, desdeMov, hastaMov, connection, null);
|
||||||
|
|
||||||
|
return ultimo.Value.SaldoCierre + sumaMovs;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper unificado de suma neta de movimientos en rango [desde, hasta] (ambos INCLUSIVE).
|
||||||
|
// Si se pasa una transacción activa, las queries corren bajo ese contexto y los range locks
|
||||||
|
// de Serializable cubren la consistencia con respecto a inserts concurrentes en las tablas de movimientos.
|
||||||
|
private async Task<decimal> CalcularSaldoEntreFechasAsync(
|
||||||
|
int idDistribuidor, int idEmpresa,
|
||||||
|
DateTime desde, DateTime hasta,
|
||||||
|
IDbConnection connection,
|
||||||
|
IDbTransaction? transaction)
|
||||||
|
{
|
||||||
|
if (desde > hasta) return 0m;
|
||||||
|
|
||||||
|
// Pagos + Notas C/D destino Distribuidores + Ajustes manuales destino Distribuidores en una sola query.
|
||||||
|
const string sqlSumaTransaccionales = @"
|
||||||
|
SELECT ISNULL(SUM(MontoNeto), 0) AS Total
|
||||||
|
FROM (
|
||||||
|
SELECT CASE WHEN TipoMovimiento = 'Recibido' THEN -CAST(Monto AS DECIMAL(18,4)) ELSE CAST(Monto AS DECIMAL(18,4)) END AS MontoNeto
|
||||||
|
FROM dbo.cue_PagosDistribuidor
|
||||||
|
WHERE Id_Distribuidor = @IdDist AND Id_Empresa = @IdEmp
|
||||||
|
AND Fecha >= @Desde AND Fecha <= @Hasta
|
||||||
|
|
||||||
|
UNION ALL
|
||||||
|
|
||||||
|
SELECT CASE WHEN Tipo = 'Credito' THEN -CAST(Monto AS DECIMAL(18,4)) ELSE CAST(Monto AS DECIMAL(18,4)) END
|
||||||
|
FROM dbo.cue_CreditosDebitos
|
||||||
|
WHERE Destino = 'Distribuidores' AND Id_Destino = @IdDist AND Id_Empresa = @IdEmp
|
||||||
|
AND Fecha >= @Desde AND Fecha <= @Hasta
|
||||||
|
|
||||||
|
UNION ALL
|
||||||
|
|
||||||
|
SELECT CAST(MontoAjuste AS DECIMAL(18,4))
|
||||||
|
FROM dbo.cue_SaldoAjustesHistorial
|
||||||
|
WHERE Destino = 'Distribuidores' AND Id_Destino = @IdDist AND Id_Empresa = @IdEmp
|
||||||
|
AND FechaOperacion >= @Desde AND FechaOperacion <= @Hasta
|
||||||
|
) m;";
|
||||||
|
|
||||||
|
decimal sumaTransaccional = await connection.ExecuteScalarAsync<decimal>(
|
||||||
|
sqlSumaTransaccionales,
|
||||||
|
new { IdDist = idDistribuidor, IdEmp = idEmpresa, Desde = desde, Hasta = hasta },
|
||||||
|
transaction);
|
||||||
|
|
||||||
|
// EntradasSalidas: SP existente (single source of truth de la fórmula CalcularMontoMovimiento que es private en EntradaSalidaDistService).
|
||||||
|
var movimientosES = await _reportesRepo.GetBalanceCuentaDistEntradaSalidaPorEmpresaAsync(
|
||||||
|
idDistribuidor, idEmpresa, desde, hasta);
|
||||||
|
|
||||||
|
decimal sumaEntradasSalidas = movimientosES?.Sum(m => m.Debe - m.Haber) ?? 0m;
|
||||||
|
|
||||||
|
return sumaTransaccional + sumaEntradasSalidas;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<CierreCuentaCorrienteDto?> GetByIdAsync(int idCierre)
|
||||||
|
{
|
||||||
|
var cierre = await _cierreRepo.GetByIdAsync(idCierre);
|
||||||
|
if (cierre == null) return null;
|
||||||
|
return (await _cierreRepo.GetAllAsync(cierre.IdDistribuidor, cierre.IdEmpresa, null, null, null))
|
||||||
|
.FirstOrDefault(c => c.IdCierre == idCierre);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<UltimoCierreDto?> GetUltimoVigenteAsync(int idDistribuidor, int idEmpresa)
|
||||||
|
{
|
||||||
|
var ultimo = await _cierreRepo.GetUltimoCierreVigenteAsync(idDistribuidor, idEmpresa);
|
||||||
|
if (ultimo == null) return null;
|
||||||
|
return new UltimoCierreDto
|
||||||
|
{
|
||||||
|
IdCierre = ultimo.IdCierre,
|
||||||
|
FechaCorte = ultimo.FechaCorte.ToString("yyyy-MM-dd"),
|
||||||
|
SaldoCierre = ultimo.SaldoCierre,
|
||||||
|
Estado = ultimo.Estado
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task<IEnumerable<CierreCuentaCorrienteDto>> GetAllAsync(
|
||||||
|
int? idDistribuidor, int? idEmpresa, string? estado,
|
||||||
|
DateTime? fechaCorteDesde, DateTime? fechaCorteHasta)
|
||||||
|
=> _cierreRepo.GetAllAsync(idDistribuidor, idEmpresa, estado, fechaCorteDesde, fechaCorteHasta);
|
||||||
|
|
||||||
|
public Task<IEnumerable<CierreCuentaCorrienteHistorialDto>> GetHistorialAsync(int idCierre)
|
||||||
|
=> _cierreRepo.GetHistorialAsync(idCierre);
|
||||||
|
|
||||||
|
public Task<IEnumerable<CierreCuentaCorrienteHistorialDto>> ObtenerHistorialAsync(
|
||||||
|
DateTime? fechaDesde, DateTime? fechaHasta,
|
||||||
|
int? idUsuarioModifico, string? tipoModificacion,
|
||||||
|
int? idCierreAfectado)
|
||||||
|
=> _cierreRepo.ObtenerHistorialAsync(fechaDesde, fechaHasta, idUsuarioModifico, tipoModificacion, idCierreAfectado);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
using GestionIntegral.Api.Dtos.Auditoria;
|
||||||
|
using GestionIntegral.Api.Dtos.Contables;
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace GestionIntegral.Api.Services.Contables
|
||||||
|
{
|
||||||
|
public interface ICierreCuentaCorrienteService
|
||||||
|
{
|
||||||
|
// Devuelve (Cierre, ErrorCode, ErrorMessage).
|
||||||
|
// ErrorCode = código semántico (CIERRE_FECHA_FUTURA, CIERRE_FECHA_ANTERIOR_A_ULTIMO, CIERRE_DISTRIBUIDOR_BAJA, CIERRE_EMPRESA_INEXISTENTE, CIERRE_ERROR_INTERNO).
|
||||||
|
Task<(CierreCuentaCorrienteDto? Cierre, string? ErrorCode, string? ErrorMessage)> CrearCierreAsync(CrearCierreDto dto, int idUsuario);
|
||||||
|
|
||||||
|
// ErrorCode = CIERRE_NO_ENCONTRADO, CIERRE_YA_ANULADO, CIERRE_HAY_POSTERIORES_VIGENTES, CIERRE_PERMISO_DENEGADO, CIERRE_ERROR_INTERNO.
|
||||||
|
Task<(CierreCuentaCorrienteDto? Cierre, string? ErrorCode, string? ErrorMessage)> ReabrirCierreAsync(int idCierre, ReabrirCierreDto dto, int idUsuario, bool esSuperAdmin);
|
||||||
|
|
||||||
|
// Saldo inicial del reporte de cuenta corriente para el filtro fechaDesde:
|
||||||
|
// - Sin cierre previo: 0.
|
||||||
|
// - Con cierre con FechaCorte < fechaDesde: SaldoCierre + sumaNeta(fechaCierre+1 .. fechaDesde-1) sobre 4 fuentes.
|
||||||
|
Task<decimal> CalcularSaldoInicialReporteAsync(int idDistribuidor, int idEmpresa, DateTime fechaDesde);
|
||||||
|
|
||||||
|
Task<CierreCuentaCorrienteDto?> GetByIdAsync(int idCierre);
|
||||||
|
Task<UltimoCierreDto?> GetUltimoVigenteAsync(int idDistribuidor, int idEmpresa);
|
||||||
|
Task<IEnumerable<CierreCuentaCorrienteDto>> GetAllAsync(
|
||||||
|
int? idDistribuidor, int? idEmpresa, string? estado,
|
||||||
|
DateTime? fechaCorteDesde, DateTime? fechaCorteHasta);
|
||||||
|
Task<IEnumerable<CierreCuentaCorrienteHistorialDto>> GetHistorialAsync(int idCierre);
|
||||||
|
|
||||||
|
// Auditoría general — filtros cruzados sobre todos los cierres.
|
||||||
|
Task<IEnumerable<CierreCuentaCorrienteHistorialDto>> ObtenerHistorialAsync(
|
||||||
|
DateTime? fechaDesde, DateTime? fechaHasta,
|
||||||
|
int? idUsuarioModifico, string? tipoModificacion,
|
||||||
|
int? idCierreAfectado);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
using System;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace GestionIntegral.Api.Services.Contables
|
||||||
|
{
|
||||||
|
// Resultado del chequeo de período cerrado.
|
||||||
|
// Si EstaCerrado=true, IdCierre y FechaCorte tienen el cierre que bloquea la operación.
|
||||||
|
public record PeriodoCerradoResult(bool EstaCerrado, int? IdCierre, DateTime? FechaCorte);
|
||||||
|
|
||||||
|
public interface IPeriodoCerradoValidator
|
||||||
|
{
|
||||||
|
Task<PeriodoCerradoResult> EstaCerradoAsync(string destino, int idDestino, int idEmpresa, DateTime fechaOperacion);
|
||||||
|
|
||||||
|
// Invalida la entrada de cache para el par (Distribuidor + Empresa). Llamar después de Crear o Anular cierre.
|
||||||
|
void InvalidarCache(int idDistribuidor, int idEmpresa);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -20,6 +20,7 @@ namespace GestionIntegral.Api.Services.Contables
|
|||||||
private readonly ICanillaRepository _canillaRepo;
|
private readonly ICanillaRepository _canillaRepo;
|
||||||
private readonly IEmpresaRepository _empresaRepo;
|
private readonly IEmpresaRepository _empresaRepo;
|
||||||
private readonly ISaldoRepository _saldoRepo;
|
private readonly ISaldoRepository _saldoRepo;
|
||||||
|
private readonly IPeriodoCerradoValidator _periodoCerrado;
|
||||||
private readonly DbConnectionFactory _connectionFactory;
|
private readonly DbConnectionFactory _connectionFactory;
|
||||||
private readonly ILogger<NotaCreditoDebitoService> _logger;
|
private readonly ILogger<NotaCreditoDebitoService> _logger;
|
||||||
|
|
||||||
@@ -29,6 +30,7 @@ namespace GestionIntegral.Api.Services.Contables
|
|||||||
ICanillaRepository canillaRepo,
|
ICanillaRepository canillaRepo,
|
||||||
IEmpresaRepository empresaRepo,
|
IEmpresaRepository empresaRepo,
|
||||||
ISaldoRepository saldoRepo,
|
ISaldoRepository saldoRepo,
|
||||||
|
IPeriodoCerradoValidator periodoCerrado,
|
||||||
DbConnectionFactory connectionFactory,
|
DbConnectionFactory connectionFactory,
|
||||||
ILogger<NotaCreditoDebitoService> logger)
|
ILogger<NotaCreditoDebitoService> logger)
|
||||||
{
|
{
|
||||||
@@ -37,6 +39,7 @@ namespace GestionIntegral.Api.Services.Contables
|
|||||||
_canillaRepo = canillaRepo;
|
_canillaRepo = canillaRepo;
|
||||||
_empresaRepo = empresaRepo;
|
_empresaRepo = empresaRepo;
|
||||||
_saldoRepo = saldoRepo;
|
_saldoRepo = saldoRepo;
|
||||||
|
_periodoCerrado = periodoCerrado;
|
||||||
_connectionFactory = connectionFactory;
|
_connectionFactory = connectionFactory;
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
}
|
}
|
||||||
@@ -111,6 +114,11 @@ namespace GestionIntegral.Api.Services.Contables
|
|||||||
if (await _empresaRepo.GetByIdAsync(createDto.IdEmpresa) == null)
|
if (await _empresaRepo.GetByIdAsync(createDto.IdEmpresa) == null)
|
||||||
return (null, "La empresa especificada no existe.");
|
return (null, "La empresa especificada no existe.");
|
||||||
|
|
||||||
|
// Bloqueo por período cerrado (sólo aplica si Destino="Distribuidores"; el validator filtra internamente).
|
||||||
|
var bloqueoCrear = await _periodoCerrado.EstaCerradoAsync(createDto.Destino, createDto.IdDestino, createDto.IdEmpresa, createDto.Fecha);
|
||||||
|
if (bloqueoCrear.EstaCerrado)
|
||||||
|
throw new BloqueoPorPeriodoCerradoException(bloqueoCrear.IdCierre!.Value, bloqueoCrear.FechaCorte!.Value);
|
||||||
|
|
||||||
var nuevaNota = new NotaCreditoDebito
|
var nuevaNota = new NotaCreditoDebito
|
||||||
{
|
{
|
||||||
Destino = createDto.Destino,
|
Destino = createDto.Destino,
|
||||||
@@ -187,6 +195,14 @@ namespace GestionIntegral.Api.Services.Contables
|
|||||||
return (false, "Nota no encontrada.");
|
return (false, "Nota no encontrada.");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Bloqueo por período cerrado sobre la fecha original (el DTO de update no permite cambiar Fecha).
|
||||||
|
var bloqueoUpd = await _periodoCerrado.EstaCerradoAsync(notaExistente.Destino, notaExistente.IdDestino, notaExistente.IdEmpresa, notaExistente.Fecha);
|
||||||
|
if (bloqueoUpd.EstaCerrado)
|
||||||
|
{
|
||||||
|
transaction.Rollback();
|
||||||
|
throw new BloqueoPorPeriodoCerradoException(bloqueoUpd.IdCierre!.Value, bloqueoUpd.FechaCorte!.Value);
|
||||||
|
}
|
||||||
|
|
||||||
decimal impactoOriginalSaldo = notaExistente.Tipo == "Credito" ? -notaExistente.Monto : notaExistente.Monto;
|
decimal impactoOriginalSaldo = notaExistente.Tipo == "Credito" ? -notaExistente.Monto : notaExistente.Monto;
|
||||||
decimal impactoNuevoSaldo = notaExistente.Tipo == "Credito" ? -updateDto.Monto : updateDto.Monto;
|
decimal impactoNuevoSaldo = notaExistente.Tipo == "Credito" ? -updateDto.Monto : updateDto.Monto;
|
||||||
decimal diferenciaAjusteSaldo = impactoNuevoSaldo - impactoOriginalSaldo;
|
decimal diferenciaAjusteSaldo = impactoNuevoSaldo - impactoOriginalSaldo;
|
||||||
@@ -252,6 +268,14 @@ namespace GestionIntegral.Api.Services.Contables
|
|||||||
return (false, "Nota no encontrada.");
|
return (false, "Nota no encontrada.");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Bloqueo por período cerrado: no se puede eliminar una nota cuya fecha cae en un cierre vigente.
|
||||||
|
var bloqueoDel = await _periodoCerrado.EstaCerradoAsync(notaExistente.Destino, notaExistente.IdDestino, notaExistente.IdEmpresa, notaExistente.Fecha);
|
||||||
|
if (bloqueoDel.EstaCerrado)
|
||||||
|
{
|
||||||
|
transaction.Rollback();
|
||||||
|
throw new BloqueoPorPeriodoCerradoException(bloqueoDel.IdCierre!.Value, bloqueoDel.FechaCorte!.Value);
|
||||||
|
}
|
||||||
|
|
||||||
decimal montoReversion = notaExistente.Tipo == "Credito" ? notaExistente.Monto : -notaExistente.Monto;
|
decimal montoReversion = notaExistente.Tipo == "Credito" ? notaExistente.Monto : -notaExistente.Monto;
|
||||||
|
|
||||||
var eliminado = await _notaRepo.DeleteAsync(idNota, idUsuario, transaction);
|
var eliminado = await _notaRepo.DeleteAsync(idNota, idUsuario, transaction);
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ namespace GestionIntegral.Api.Services.Contables
|
|||||||
private readonly ITipoPagoRepository _tipoPagoRepo;
|
private readonly ITipoPagoRepository _tipoPagoRepo;
|
||||||
private readonly IEmpresaRepository _empresaRepo;
|
private readonly IEmpresaRepository _empresaRepo;
|
||||||
private readonly ISaldoRepository _saldoRepo;
|
private readonly ISaldoRepository _saldoRepo;
|
||||||
|
private readonly IPeriodoCerradoValidator _periodoCerrado;
|
||||||
private readonly DbConnectionFactory _connectionFactory;
|
private readonly DbConnectionFactory _connectionFactory;
|
||||||
private readonly ILogger<PagoDistribuidorService> _logger;
|
private readonly ILogger<PagoDistribuidorService> _logger;
|
||||||
|
|
||||||
@@ -29,6 +30,7 @@ namespace GestionIntegral.Api.Services.Contables
|
|||||||
ITipoPagoRepository tipoPagoRepo,
|
ITipoPagoRepository tipoPagoRepo,
|
||||||
IEmpresaRepository empresaRepo,
|
IEmpresaRepository empresaRepo,
|
||||||
ISaldoRepository saldoRepo,
|
ISaldoRepository saldoRepo,
|
||||||
|
IPeriodoCerradoValidator periodoCerrado,
|
||||||
DbConnectionFactory connectionFactory,
|
DbConnectionFactory connectionFactory,
|
||||||
ILogger<PagoDistribuidorService> logger)
|
ILogger<PagoDistribuidorService> logger)
|
||||||
{
|
{
|
||||||
@@ -37,6 +39,7 @@ namespace GestionIntegral.Api.Services.Contables
|
|||||||
_tipoPagoRepo = tipoPagoRepo;
|
_tipoPagoRepo = tipoPagoRepo;
|
||||||
_empresaRepo = empresaRepo;
|
_empresaRepo = empresaRepo;
|
||||||
_saldoRepo = saldoRepo;
|
_saldoRepo = saldoRepo;
|
||||||
|
_periodoCerrado = periodoCerrado;
|
||||||
_connectionFactory = connectionFactory;
|
_connectionFactory = connectionFactory;
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
}
|
}
|
||||||
@@ -93,8 +96,23 @@ namespace GestionIntegral.Api.Services.Contables
|
|||||||
return (null, "Tipo de pago no válido.");
|
return (null, "Tipo de pago no válido.");
|
||||||
if (await _empresaRepo.GetByIdAsync(createDto.IdEmpresa) == null)
|
if (await _empresaRepo.GetByIdAsync(createDto.IdEmpresa) == null)
|
||||||
return (null, "Empresa no válida.");
|
return (null, "Empresa no válida.");
|
||||||
if (await _pagoRepo.ExistsByReciboAndTipoMovimientoAsync(createDto.Recibo, createDto.TipoMovimiento))
|
var pagoExistente = await _pagoRepo.GetByReciboAndTipoMovimientoAsync(createDto.Recibo, createDto.TipoMovimiento);
|
||||||
return (null, $"Ya existe un pago '{createDto.TipoMovimiento}' con el número de recibo '{createDto.Recibo}'.");
|
if (pagoExistente != null)
|
||||||
|
{
|
||||||
|
// Si encontramos un duplicado, obtenemos los detalles para el mensaje de error
|
||||||
|
var distribuidor = await _distribuidorRepo.GetByIdSimpleAsync(pagoExistente.IdDistribuidor);
|
||||||
|
var empresa = await _empresaRepo.GetByIdAsync(pagoExistente.IdEmpresa);
|
||||||
|
|
||||||
|
string mensajeError = $"El recibo N° {createDto.Recibo} ya fue registrado como '{pagoExistente.TipoMovimiento}' el {pagoExistente.Fecha:dd/MM/yyyy} " +
|
||||||
|
$"para el distribuidor '{distribuidor?.Nombre ?? "Desconocido"}' en la empresa '{empresa?.Nombre ?? "Desconocida"}'.";
|
||||||
|
|
||||||
|
return (null, mensajeError);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Bloqueo por período cerrado: la fecha de operación no puede caer dentro de un cierre vigente.
|
||||||
|
var bloqueoCrear = await _periodoCerrado.EstaCerradoAsync("Distribuidores", createDto.IdDistribuidor, createDto.IdEmpresa, createDto.Fecha);
|
||||||
|
if (bloqueoCrear.EstaCerrado)
|
||||||
|
throw new BloqueoPorPeriodoCerradoException(bloqueoCrear.IdCierre!.Value, bloqueoCrear.FechaCorte!.Value);
|
||||||
|
|
||||||
var nuevoPago = new PagoDistribuidor
|
var nuevoPago = new PagoDistribuidor
|
||||||
{
|
{
|
||||||
@@ -172,6 +190,14 @@ namespace GestionIntegral.Api.Services.Contables
|
|||||||
return (false, "Pago no encontrado.");
|
return (false, "Pago no encontrado.");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Bloqueo por período cerrado sobre la fecha original del pago (el DTO de update no permite cambiar Fecha).
|
||||||
|
var bloqueoUpd = await _periodoCerrado.EstaCerradoAsync("Distribuidores", pagoExistente.IdDistribuidor, pagoExistente.IdEmpresa, pagoExistente.Fecha);
|
||||||
|
if (bloqueoUpd.EstaCerrado)
|
||||||
|
{
|
||||||
|
transaction.Rollback();
|
||||||
|
throw new BloqueoPorPeriodoCerradoException(bloqueoUpd.IdCierre!.Value, bloqueoUpd.FechaCorte!.Value);
|
||||||
|
}
|
||||||
|
|
||||||
if (await _tipoPagoRepo.GetByIdAsync(updateDto.IdTipoPago) == null)
|
if (await _tipoPagoRepo.GetByIdAsync(updateDto.IdTipoPago) == null)
|
||||||
{
|
{
|
||||||
transaction.Rollback();
|
transaction.Rollback();
|
||||||
@@ -209,6 +235,11 @@ namespace GestionIntegral.Api.Services.Contables
|
|||||||
return (true, null);
|
return (true, null);
|
||||||
}
|
}
|
||||||
catch (KeyNotFoundException) { try { transaction?.Rollback(); } catch (Exception rbEx) { _logger.LogError(rbEx, "Error en Rollback de ActualizarAsync PagoDistribuidor (KeyNotFound)."); } return (false, "Pago no encontrado."); }
|
catch (KeyNotFoundException) { try { transaction?.Rollback(); } catch (Exception rbEx) { _logger.LogError(rbEx, "Error en Rollback de ActualizarAsync PagoDistribuidor (KeyNotFound)."); } return (false, "Pago no encontrado."); }
|
||||||
|
catch (BloqueoPorPeriodoCerradoException)
|
||||||
|
{
|
||||||
|
try { transaction?.Rollback(); } catch (Exception rbEx) { _logger.LogError(rbEx, "Error en Rollback de ActualizarAsync PagoDistribuidor (Bloqueo)."); }
|
||||||
|
throw;
|
||||||
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
try { transaction?.Rollback(); } catch (Exception rbEx) { _logger.LogError(rbEx, "Error en Rollback de ActualizarAsync PagoDistribuidor."); }
|
try { transaction?.Rollback(); } catch (Exception rbEx) { _logger.LogError(rbEx, "Error en Rollback de ActualizarAsync PagoDistribuidor."); }
|
||||||
@@ -243,6 +274,14 @@ namespace GestionIntegral.Api.Services.Contables
|
|||||||
return (false, "Pago no encontrado.");
|
return (false, "Pago no encontrado.");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Bloqueo por período cerrado: no se puede eliminar un pago cuya fecha cae en un cierre vigente.
|
||||||
|
var bloqueoDel = await _periodoCerrado.EstaCerradoAsync("Distribuidores", pagoExistente.IdDistribuidor, pagoExistente.IdEmpresa, pagoExistente.Fecha);
|
||||||
|
if (bloqueoDel.EstaCerrado)
|
||||||
|
{
|
||||||
|
transaction.Rollback();
|
||||||
|
throw new BloqueoPorPeriodoCerradoException(bloqueoDel.IdCierre!.Value, bloqueoDel.FechaCorte!.Value);
|
||||||
|
}
|
||||||
|
|
||||||
decimal montoReversion = pagoExistente.TipoMovimiento == "Recibido" ? pagoExistente.Monto : -pagoExistente.Monto;
|
decimal montoReversion = pagoExistente.TipoMovimiento == "Recibido" ? pagoExistente.Monto : -pagoExistente.Monto;
|
||||||
|
|
||||||
var eliminado = await _pagoRepo.DeleteAsync(idPago, idUsuario, transaction);
|
var eliminado = await _pagoRepo.DeleteAsync(idPago, idUsuario, transaction);
|
||||||
@@ -256,6 +295,11 @@ namespace GestionIntegral.Api.Services.Contables
|
|||||||
return (true, null);
|
return (true, null);
|
||||||
}
|
}
|
||||||
catch (KeyNotFoundException) { try { transaction?.Rollback(); } catch (Exception rbEx) { _logger.LogError(rbEx, "Error en Rollback de EliminarAsync PagoDistribuidor (KeyNotFound)."); } return (false, "Pago no encontrado."); }
|
catch (KeyNotFoundException) { try { transaction?.Rollback(); } catch (Exception rbEx) { _logger.LogError(rbEx, "Error en Rollback de EliminarAsync PagoDistribuidor (KeyNotFound)."); } return (false, "Pago no encontrado."); }
|
||||||
|
catch (BloqueoPorPeriodoCerradoException)
|
||||||
|
{
|
||||||
|
try { transaction?.Rollback(); } catch (Exception rbEx) { _logger.LogError(rbEx, "Error en Rollback de EliminarAsync PagoDistribuidor (Bloqueo)."); }
|
||||||
|
throw;
|
||||||
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
try { transaction?.Rollback(); } catch (Exception rbEx) { _logger.LogError(rbEx, "Error en Rollback de EliminarAsync PagoDistribuidor."); }
|
try { transaction?.Rollback(); } catch (Exception rbEx) { _logger.LogError(rbEx, "Error en Rollback de EliminarAsync PagoDistribuidor."); }
|
||||||
@@ -275,25 +319,25 @@ namespace GestionIntegral.Api.Services.Contables
|
|||||||
DateTime? fechaDesde, DateTime? fechaHasta,
|
DateTime? fechaDesde, DateTime? fechaHasta,
|
||||||
int? idUsuarioModifico, string? tipoModificacion,
|
int? idUsuarioModifico, string? tipoModificacion,
|
||||||
int? idPagoAfectado)
|
int? idPagoAfectado)
|
||||||
{
|
|
||||||
var historialData = await _pagoRepo.GetHistorialAsync(fechaDesde, fechaHasta, idUsuarioModifico, tipoModificacion, idPagoAfectado);
|
|
||||||
|
|
||||||
return historialData.Select(h => new PagoDistribuidorHistorialDto
|
|
||||||
{
|
{
|
||||||
Id_Pago = h.Historial.Id_Pago,
|
var historialData = await _pagoRepo.GetHistorialAsync(fechaDesde, fechaHasta, idUsuarioModifico, tipoModificacion, idPagoAfectado);
|
||||||
Id_Distribuidor = h.Historial.Id_Distribuidor,
|
|
||||||
Fecha = h.Historial.Fecha,
|
return historialData.Select(h => new PagoDistribuidorHistorialDto
|
||||||
TipoMovimiento = h.Historial.TipoMovimiento,
|
{
|
||||||
Recibo = h.Historial.Recibo,
|
Id_Pago = h.Historial.Id_Pago,
|
||||||
Monto = h.Historial.Monto,
|
Id_Distribuidor = h.Historial.Id_Distribuidor,
|
||||||
Id_TipoPago = h.Historial.Id_TipoPago,
|
Fecha = h.Historial.Fecha,
|
||||||
Detalle = h.Historial.Detalle,
|
TipoMovimiento = h.Historial.TipoMovimiento,
|
||||||
Id_Empresa = h.Historial.Id_Empresa,
|
Recibo = h.Historial.Recibo,
|
||||||
Id_Usuario = h.Historial.Id_Usuario,
|
Monto = h.Historial.Monto,
|
||||||
NombreUsuarioModifico = h.NombreUsuarioModifico,
|
Id_TipoPago = h.Historial.Id_TipoPago,
|
||||||
FechaMod = h.Historial.FechaMod,
|
Detalle = h.Historial.Detalle,
|
||||||
TipoMod = h.Historial.TipoMod
|
Id_Empresa = h.Historial.Id_Empresa,
|
||||||
}).ToList();
|
Id_Usuario = h.Historial.Id_Usuario,
|
||||||
}
|
NombreUsuarioModifico = h.NombreUsuarioModifico,
|
||||||
|
FechaMod = h.Historial.FechaMod,
|
||||||
|
TipoMod = h.Historial.TipoMod
|
||||||
|
}).ToList();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,69 @@
|
|||||||
|
using GestionIntegral.Api.Data.Repositories.Contables;
|
||||||
|
using Microsoft.Extensions.Caching.Memory;
|
||||||
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
using System;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace GestionIntegral.Api.Services.Contables
|
||||||
|
{
|
||||||
|
public class PeriodoCerradoValidator : IPeriodoCerradoValidator
|
||||||
|
{
|
||||||
|
private readonly IServiceScopeFactory _scopeFactory;
|
||||||
|
private readonly IMemoryCache _cache;
|
||||||
|
private readonly ILogger<PeriodoCerradoValidator> _logger;
|
||||||
|
|
||||||
|
// TTL del cache: ventana stale aceptada por contrato (los cierres son operación rara).
|
||||||
|
private static readonly TimeSpan CacheTtl = TimeSpan.FromMinutes(10);
|
||||||
|
|
||||||
|
// Singleton consumiendo Scoped (ICierreCuentaCorrienteRepository) → resolver vía IServiceScopeFactory cada lookup.
|
||||||
|
public PeriodoCerradoValidator(
|
||||||
|
IServiceScopeFactory scopeFactory,
|
||||||
|
IMemoryCache cache,
|
||||||
|
ILogger<PeriodoCerradoValidator> logger)
|
||||||
|
{
|
||||||
|
_scopeFactory = scopeFactory;
|
||||||
|
_cache = cache;
|
||||||
|
_logger = logger;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<PeriodoCerradoResult> EstaCerradoAsync(string destino, int idDestino, int idEmpresa, DateTime fechaOperacion)
|
||||||
|
{
|
||||||
|
// Sólo aplica a Distribuidores. Canillas no tiene cierre por (Canilla + Empresa).
|
||||||
|
if (!string.Equals(destino, "Distribuidores", StringComparison.OrdinalIgnoreCase))
|
||||||
|
return new PeriodoCerradoResult(false, null, null);
|
||||||
|
|
||||||
|
var key = CacheKey(idDestino, idEmpresa);
|
||||||
|
|
||||||
|
var ultimo = await _cache.GetOrCreateAsync(key, async entry =>
|
||||||
|
{
|
||||||
|
entry.AbsoluteExpirationRelativeToNow = CacheTtl;
|
||||||
|
|
||||||
|
using var scope = _scopeFactory.CreateScope();
|
||||||
|
var repo = scope.ServiceProvider.GetRequiredService<ICierreCuentaCorrienteRepository>();
|
||||||
|
var cierre = await repo.GetUltimoCierreVigenteAsync(idDestino, idEmpresa);
|
||||||
|
|
||||||
|
// Cacheamos sólo lo necesario para que la decisión sea rápida.
|
||||||
|
return cierre == null ? null : new CachedUltimoCierre(cierre.IdCierre, cierre.FechaCorte);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (ultimo == null)
|
||||||
|
return new PeriodoCerradoResult(false, null, null);
|
||||||
|
|
||||||
|
// FechaCorte es DATE; comparar en .Date para ignorar componente hora.
|
||||||
|
if (ultimo.FechaCorte.Date >= fechaOperacion.Date)
|
||||||
|
return new PeriodoCerradoResult(true, ultimo.IdCierre, ultimo.FechaCorte);
|
||||||
|
|
||||||
|
return new PeriodoCerradoResult(false, null, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void InvalidarCache(int idDistribuidor, int idEmpresa)
|
||||||
|
{
|
||||||
|
_cache.Remove(CacheKey(idDistribuidor, idEmpresa));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string CacheKey(int idDestino, int idEmpresa) => $"cierre-ultimo:{idDestino}:{idEmpresa}";
|
||||||
|
|
||||||
|
private sealed record CachedUltimoCierre(int IdCierre, DateTime FechaCorte);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -19,6 +19,7 @@ namespace GestionIntegral.Api.Services.Contables
|
|||||||
private readonly IDistribuidorRepository _distribuidorRepo; // Para nombres
|
private readonly IDistribuidorRepository _distribuidorRepo; // Para nombres
|
||||||
private readonly ICanillaRepository _canillaRepo; // Para nombres
|
private readonly ICanillaRepository _canillaRepo; // Para nombres
|
||||||
private readonly IEmpresaRepository _empresaRepo; // Para nombres
|
private readonly IEmpresaRepository _empresaRepo; // Para nombres
|
||||||
|
private readonly IPeriodoCerradoValidator _periodoCerrado;
|
||||||
private readonly DbConnectionFactory _connectionFactory;
|
private readonly DbConnectionFactory _connectionFactory;
|
||||||
private readonly ILogger<SaldoService> _logger;
|
private readonly ILogger<SaldoService> _logger;
|
||||||
|
|
||||||
@@ -27,6 +28,7 @@ namespace GestionIntegral.Api.Services.Contables
|
|||||||
IDistribuidorRepository distribuidorRepo,
|
IDistribuidorRepository distribuidorRepo,
|
||||||
ICanillaRepository canillaRepo,
|
ICanillaRepository canillaRepo,
|
||||||
IEmpresaRepository empresaRepo,
|
IEmpresaRepository empresaRepo,
|
||||||
|
IPeriodoCerradoValidator periodoCerrado,
|
||||||
DbConnectionFactory connectionFactory,
|
DbConnectionFactory connectionFactory,
|
||||||
ILogger<SaldoService> logger)
|
ILogger<SaldoService> logger)
|
||||||
{
|
{
|
||||||
@@ -34,6 +36,7 @@ namespace GestionIntegral.Api.Services.Contables
|
|||||||
_distribuidorRepo = distribuidorRepo;
|
_distribuidorRepo = distribuidorRepo;
|
||||||
_canillaRepo = canillaRepo;
|
_canillaRepo = canillaRepo;
|
||||||
_empresaRepo = empresaRepo;
|
_empresaRepo = empresaRepo;
|
||||||
|
_periodoCerrado = periodoCerrado;
|
||||||
_connectionFactory = connectionFactory;
|
_connectionFactory = connectionFactory;
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
}
|
}
|
||||||
@@ -103,6 +106,11 @@ namespace GestionIntegral.Api.Services.Contables
|
|||||||
if (await _empresaRepo.GetByIdAsync(ajusteDto.IdEmpresa) == null)
|
if (await _empresaRepo.GetByIdAsync(ajusteDto.IdEmpresa) == null)
|
||||||
return (false, "La empresa especificada no existe.", null);
|
return (false, "La empresa especificada no existe.", null);
|
||||||
|
|
||||||
|
// Bloqueo por período cerrado: la FechaOperacion del ajuste no puede caer dentro de un cierre vigente.
|
||||||
|
// El validator filtra internamente Destino="Distribuidores"; ajustes a Canillas no se bloquean.
|
||||||
|
var bloqueo = await _periodoCerrado.EstaCerradoAsync(ajusteDto.Destino, ajusteDto.IdDestino, ajusteDto.IdEmpresa, ajusteDto.FechaOperacion);
|
||||||
|
if (bloqueo.EstaCerrado)
|
||||||
|
throw new BloqueoPorPeriodoCerradoException(bloqueo.IdCierre!.Value, bloqueo.FechaCorte!.Value);
|
||||||
|
|
||||||
using var connection = _connectionFactory.CreateConnection();
|
using var connection = _connectionFactory.CreateConnection();
|
||||||
if (connection.State != ConnectionState.Open) { if (connection is System.Data.Common.DbConnection dbConn) await dbConn.OpenAsync(); else connection.Open(); }
|
if (connection.State != ConnectionState.Open) { if (connection is System.Data.Common.DbConnection dbConn) await dbConn.OpenAsync(); else connection.Open(); }
|
||||||
@@ -152,6 +160,11 @@ namespace GestionIntegral.Api.Services.Contables
|
|||||||
var saldoDtoActualizado = await MapToGestionDto(saldoDespuesDeModificacion);
|
var saldoDtoActualizado = await MapToGestionDto(saldoDespuesDeModificacion);
|
||||||
return (true, null, saldoDtoActualizado);
|
return (true, null, saldoDtoActualizado);
|
||||||
}
|
}
|
||||||
|
catch (BloqueoPorPeriodoCerradoException)
|
||||||
|
{
|
||||||
|
try { transaction.Rollback(); } catch (Exception rbEx) { _logger.LogError(rbEx, "Error en Rollback de RealizarAjusteManualSaldoAsync (Bloqueo)."); }
|
||||||
|
throw;
|
||||||
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
try { transaction.Rollback(); } catch (Exception rbEx) { _logger.LogError(rbEx, "Error en Rollback de RealizarAjusteManualSaldoAsync."); }
|
try { transaction.Rollback(); } catch (Exception rbEx) { _logger.LogError(rbEx, "Error en Rollback de RealizarAjusteManualSaldoAsync."); }
|
||||||
|
|||||||
@@ -56,20 +56,22 @@ namespace GestionIntegral.Api.Services.Distribucion
|
|||||||
Depto = data.Distribuidor.Depto,
|
Depto = data.Distribuidor.Depto,
|
||||||
Telefono = data.Distribuidor.Telefono,
|
Telefono = data.Distribuidor.Telefono,
|
||||||
Email = data.Distribuidor.Email,
|
Email = data.Distribuidor.Email,
|
||||||
Localidad = data.Distribuidor.Localidad
|
Localidad = data.Distribuidor.Localidad,
|
||||||
|
Baja = data.Distribuidor.Baja,
|
||||||
|
FechaBaja = data.Distribuidor.FechaBaja
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<IEnumerable<DistribuidorDto>> ObtenerTodosAsync(string? nombreFilter, string? nroDocFilter)
|
public async Task<IEnumerable<DistribuidorDto>> ObtenerTodosAsync(string? nombreFilter, string? nroDocFilter, bool? soloActivos = true)
|
||||||
{
|
{
|
||||||
var data = await _distribuidorRepository.GetAllAsync(nombreFilter, nroDocFilter);
|
var data = await _distribuidorRepository.GetAllAsync(nombreFilter, nroDocFilter, soloActivos);
|
||||||
// Filtrar nulos y asegurar al compilador que no hay nulos en la lista final
|
// Filtrar nulos y asegurar al compilador que no hay nulos en la lista final
|
||||||
return data.Select(MapToDto).Where(dto => dto != null).Select(dto => dto!);
|
return data.Select(MapToDto).Where(dto => dto != null).Select(dto => dto!);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<IEnumerable<DistribuidorDropdownDto>> GetAllDropdownAsync()
|
public async Task<IEnumerable<DistribuidorDropdownDto>> GetAllDropdownAsync(bool? soloActivos = true)
|
||||||
{
|
{
|
||||||
var data = await _distribuidorRepository.GetAllDropdownAsync();
|
var data = await _distribuidorRepository.GetAllDropdownAsync(soloActivos);
|
||||||
// Asegurar que el resultado no sea nulo y no contiene elementos nulos
|
// Asegurar que el resultado no sea nulo y no contiene elementos nulos
|
||||||
if (data == null)
|
if (data == null)
|
||||||
{
|
{
|
||||||
@@ -223,6 +225,31 @@ namespace GestionIntegral.Api.Services.Distribucion
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async Task<(bool Exito, string? Error)> ToggleBajaAsync(int id, bool darDeBaja, DateTime? fechaBaja, int idUsuario)
|
||||||
|
{
|
||||||
|
var distribuidorExistente = await _distribuidorRepository.GetByIdSimpleAsync(id);
|
||||||
|
if (distribuidorExistente == null) return (false, "Distribuidor no encontrado.");
|
||||||
|
|
||||||
|
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 toggled = await _distribuidorRepository.ToggleBajaAsync(id, darDeBaja, fechaBaja, idUsuario, transaction);
|
||||||
|
if (!toggled) throw new DataException("Error al cambiar estado de baja.");
|
||||||
|
transaction.Commit();
|
||||||
|
_logger.LogInformation("Distribuidor ID {IdDistribuidor} dado de {Estado} por Usuario ID {IdUsuario}.", id, darDeBaja ? "baja" : "alta", idUsuario);
|
||||||
|
return (true, null);
|
||||||
|
}
|
||||||
|
catch (KeyNotFoundException) { try { transaction.Rollback(); } catch { } return (false, "Distribuidor no encontrado."); }
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
try { transaction.Rollback(); } catch { }
|
||||||
|
_logger.LogError(ex, "Error ToggleBajaAsync Distribuidor ID: {IdDistribuidor}", id);
|
||||||
|
return (false, $"Error interno: {ex.Message}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public async Task<IEnumerable<DistribuidorHistorialDto>> ObtenerHistorialAsync(
|
public async Task<IEnumerable<DistribuidorHistorialDto>> ObtenerHistorialAsync(
|
||||||
DateTime? fechaDesde, DateTime? fechaHasta,
|
DateTime? fechaDesde, DateTime? fechaHasta,
|
||||||
int? idUsuarioModifico, string? tipoModificacion,
|
int? idUsuarioModifico, string? tipoModificacion,
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ using GestionIntegral.Api.Data.Repositories.Distribucion;
|
|||||||
using GestionIntegral.Api.Dtos.Auditoria;
|
using GestionIntegral.Api.Dtos.Auditoria;
|
||||||
using GestionIntegral.Api.Dtos.Distribucion;
|
using GestionIntegral.Api.Dtos.Distribucion;
|
||||||
using GestionIntegral.Api.Models.Distribucion;
|
using GestionIntegral.Api.Models.Distribucion;
|
||||||
|
using GestionIntegral.Api.Services.Contables;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
@@ -24,6 +25,7 @@ namespace GestionIntegral.Api.Services.Distribucion
|
|||||||
private readonly IPorcPagoRepository _porcPagoRepository;
|
private readonly IPorcPagoRepository _porcPagoRepository;
|
||||||
private readonly ISaldoRepository _saldoRepository;
|
private readonly ISaldoRepository _saldoRepository;
|
||||||
private readonly IEmpresaRepository _empresaRepository; // Para obtener IdEmpresa de la publicación
|
private readonly IEmpresaRepository _empresaRepository; // Para obtener IdEmpresa de la publicación
|
||||||
|
private readonly IPeriodoCerradoValidator _periodoCerrado;
|
||||||
private readonly DbConnectionFactory _connectionFactory;
|
private readonly DbConnectionFactory _connectionFactory;
|
||||||
private readonly ILogger<EntradaSalidaDistService> _logger;
|
private readonly ILogger<EntradaSalidaDistService> _logger;
|
||||||
|
|
||||||
@@ -36,6 +38,7 @@ namespace GestionIntegral.Api.Services.Distribucion
|
|||||||
IPorcPagoRepository porcPagoRepository,
|
IPorcPagoRepository porcPagoRepository,
|
||||||
ISaldoRepository saldoRepository,
|
ISaldoRepository saldoRepository,
|
||||||
IEmpresaRepository empresaRepository,
|
IEmpresaRepository empresaRepository,
|
||||||
|
IPeriodoCerradoValidator periodoCerrado,
|
||||||
DbConnectionFactory connectionFactory,
|
DbConnectionFactory connectionFactory,
|
||||||
ILogger<EntradaSalidaDistService> logger)
|
ILogger<EntradaSalidaDistService> logger)
|
||||||
{
|
{
|
||||||
@@ -47,6 +50,7 @@ namespace GestionIntegral.Api.Services.Distribucion
|
|||||||
_porcPagoRepository = porcPagoRepository;
|
_porcPagoRepository = porcPagoRepository;
|
||||||
_saldoRepository = saldoRepository;
|
_saldoRepository = saldoRepository;
|
||||||
_empresaRepository = empresaRepository;
|
_empresaRepository = empresaRepository;
|
||||||
|
_periodoCerrado = periodoCerrado;
|
||||||
_connectionFactory = connectionFactory;
|
_connectionFactory = connectionFactory;
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
}
|
}
|
||||||
@@ -167,10 +171,16 @@ namespace GestionIntegral.Api.Services.Distribucion
|
|||||||
var distribuidor = await _distribuidorRepository.GetByIdSimpleAsync(createDto.IdDistribuidor);
|
var distribuidor = await _distribuidorRepository.GetByIdSimpleAsync(createDto.IdDistribuidor);
|
||||||
if (distribuidor == null) return (null, "Distribuidor no válido.");
|
if (distribuidor == null) return (null, "Distribuidor no válido.");
|
||||||
|
|
||||||
|
// Bloqueo por período cerrado: la fecha del movimiento no puede caer dentro de un cierre vigente del distribuidor en la empresa de la publicación.
|
||||||
|
var bloqueoCrear = await _periodoCerrado.EstaCerradoAsync("Distribuidores", createDto.IdDistribuidor, publicacion.IdEmpresa, createDto.Fecha);
|
||||||
|
if (bloqueoCrear.EstaCerrado)
|
||||||
|
throw new BloqueoPorPeriodoCerradoException(bloqueoCrear.IdCierre!.Value, bloqueoCrear.FechaCorte!.Value);
|
||||||
|
|
||||||
|
/*
|
||||||
if (await _esRepository.ExistsByRemitoAndTipoForPublicacionAsync(createDto.Remito, createDto.TipoMovimiento, createDto.IdPublicacion))
|
if (await _esRepository.ExistsByRemitoAndTipoForPublicacionAsync(createDto.Remito, createDto.TipoMovimiento, createDto.IdPublicacion))
|
||||||
{
|
{
|
||||||
return (null, $"Ya existe un movimiento de '{createDto.TipoMovimiento}' con el remito N°{createDto.Remito} para esta publicación.");
|
return (null, $"Ya existe un movimiento de '{createDto.TipoMovimiento}' con el remito N°{createDto.Remito} para esta publicación.");
|
||||||
}
|
}*/
|
||||||
|
|
||||||
// Determinar IDs de Precio, Recargo y Porcentaje activos en la fecha del movimiento
|
// Determinar IDs de Precio, Recargo y Porcentaje activos en la fecha del movimiento
|
||||||
var precioActivo = await _precioRepository.GetActiveByPublicacionAndDateAsync(createDto.IdPublicacion, createDto.Fecha.Date);
|
var precioActivo = await _precioRepository.GetActiveByPublicacionAndDateAsync(createDto.IdPublicacion, createDto.Fecha.Date);
|
||||||
@@ -261,6 +271,14 @@ namespace GestionIntegral.Api.Services.Distribucion
|
|||||||
var distribuidor = await _distribuidorRepository.GetByIdSimpleAsync(esExistente.IdDistribuidor);
|
var distribuidor = await _distribuidorRepository.GetByIdSimpleAsync(esExistente.IdDistribuidor);
|
||||||
if (distribuidor == null) return (false, "Distribuidor asociado no encontrado.");
|
if (distribuidor == null) return (false, "Distribuidor asociado no encontrado.");
|
||||||
|
|
||||||
|
// Bloqueo por período cerrado sobre la fecha original del movimiento (el DTO de update no permite cambiar Fecha).
|
||||||
|
var bloqueoUpd = await _periodoCerrado.EstaCerradoAsync("Distribuidores", esExistente.IdDistribuidor, publicacion.IdEmpresa, esExistente.Fecha);
|
||||||
|
if (bloqueoUpd.EstaCerrado)
|
||||||
|
{
|
||||||
|
transaction.Rollback();
|
||||||
|
throw new BloqueoPorPeriodoCerradoException(bloqueoUpd.IdCierre!.Value, bloqueoUpd.FechaCorte!.Value);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
// 1. Calcular monto del movimiento original (antes de la actualización)
|
// 1. Calcular monto del movimiento original (antes de la actualización)
|
||||||
decimal montoOriginal = await CalcularMontoMovimiento(
|
decimal montoOriginal = await CalcularMontoMovimiento(
|
||||||
@@ -306,6 +324,11 @@ namespace GestionIntegral.Api.Services.Distribucion
|
|||||||
return (true, null);
|
return (true, null);
|
||||||
}
|
}
|
||||||
catch (KeyNotFoundException) { try { transaction.Rollback(); } catch { } return (false, "Movimiento no encontrado."); }
|
catch (KeyNotFoundException) { try { transaction.Rollback(); } catch { } return (false, "Movimiento no encontrado."); }
|
||||||
|
catch (BloqueoPorPeriodoCerradoException)
|
||||||
|
{
|
||||||
|
try { transaction.Rollback(); } catch { }
|
||||||
|
throw;
|
||||||
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
try { transaction.Rollback(); } catch { }
|
try { transaction.Rollback(); } catch { }
|
||||||
@@ -329,6 +352,14 @@ namespace GestionIntegral.Api.Services.Distribucion
|
|||||||
var distribuidor = await _distribuidorRepository.GetByIdSimpleAsync(esExistente.IdDistribuidor);
|
var distribuidor = await _distribuidorRepository.GetByIdSimpleAsync(esExistente.IdDistribuidor);
|
||||||
if (distribuidor == null) return (false, "Distribuidor asociado no encontrado.");
|
if (distribuidor == null) return (false, "Distribuidor asociado no encontrado.");
|
||||||
|
|
||||||
|
// Bloqueo por período cerrado: no se puede eliminar un movimiento cuya fecha cae en un cierre vigente.
|
||||||
|
var bloqueoDel = await _periodoCerrado.EstaCerradoAsync("Distribuidores", esExistente.IdDistribuidor, publicacion.IdEmpresa, esExistente.Fecha);
|
||||||
|
if (bloqueoDel.EstaCerrado)
|
||||||
|
{
|
||||||
|
transaction.Rollback();
|
||||||
|
throw new BloqueoPorPeriodoCerradoException(bloqueoDel.IdCierre!.Value, bloqueoDel.FechaCorte!.Value);
|
||||||
|
}
|
||||||
|
|
||||||
// 1. Calcular el monto del movimiento a eliminar para revertir el saldo
|
// 1. Calcular el monto del movimiento a eliminar para revertir el saldo
|
||||||
decimal montoReversion = await CalcularMontoMovimiento(
|
decimal montoReversion = await CalcularMontoMovimiento(
|
||||||
esExistente.IdPublicacion, esExistente.IdDistribuidor, esExistente.Fecha, esExistente.Cantidad, esExistente.TipoMovimiento,
|
esExistente.IdPublicacion, esExistente.IdDistribuidor, esExistente.Fecha, esExistente.Cantidad, esExistente.TipoMovimiento,
|
||||||
@@ -363,6 +394,11 @@ namespace GestionIntegral.Api.Services.Distribucion
|
|||||||
return (true, null);
|
return (true, null);
|
||||||
}
|
}
|
||||||
catch (KeyNotFoundException) { try { transaction.Rollback(); } catch { } return (false, "Movimiento no encontrado."); }
|
catch (KeyNotFoundException) { try { transaction.Rollback(); } catch { } return (false, "Movimiento no encontrado."); }
|
||||||
|
catch (BloqueoPorPeriodoCerradoException)
|
||||||
|
{
|
||||||
|
try { transaction.Rollback(); } catch { }
|
||||||
|
throw;
|
||||||
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
try { transaction.Rollback(); } catch { }
|
try { transaction.Rollback(); } catch { }
|
||||||
|
|||||||
@@ -7,12 +7,13 @@ namespace GestionIntegral.Api.Services.Distribucion
|
|||||||
{
|
{
|
||||||
public interface IDistribuidorService
|
public interface IDistribuidorService
|
||||||
{
|
{
|
||||||
Task<IEnumerable<DistribuidorDto>> ObtenerTodosAsync(string? nombreFilter, string? nroDocFilter);
|
Task<IEnumerable<DistribuidorDto>> ObtenerTodosAsync(string? nombreFilter, string? nroDocFilter, bool? soloActivos = true);
|
||||||
Task<DistribuidorDto?> ObtenerPorIdAsync(int id);
|
Task<DistribuidorDto?> ObtenerPorIdAsync(int id);
|
||||||
Task<(DistribuidorDto? Distribuidor, string? Error)> CrearAsync(CreateDistribuidorDto createDto, int idUsuario);
|
Task<(DistribuidorDto? Distribuidor, string? Error)> CrearAsync(CreateDistribuidorDto createDto, int idUsuario);
|
||||||
Task<(bool Exito, string? Error)> ActualizarAsync(int id, UpdateDistribuidorDto updateDto, int idUsuario);
|
Task<(bool Exito, string? Error)> ActualizarAsync(int id, UpdateDistribuidorDto updateDto, int idUsuario);
|
||||||
Task<(bool Exito, string? Error)> EliminarAsync(int id, int idUsuario);
|
Task<(bool Exito, string? Error)> EliminarAsync(int id, int idUsuario);
|
||||||
Task<IEnumerable<DistribuidorDropdownDto>> GetAllDropdownAsync();
|
Task<(bool Exito, string? Error)> ToggleBajaAsync(int id, bool darDeBaja, DateTime? fechaBaja, int idUsuario);
|
||||||
|
Task<IEnumerable<DistribuidorDropdownDto>> GetAllDropdownAsync(bool? soloActivos = true);
|
||||||
Task<DistribuidorLookupDto?> ObtenerLookupPorIdAsync(int id);
|
Task<DistribuidorLookupDto?> ObtenerLookupPorIdAsync(int id);
|
||||||
Task<IEnumerable<DistribuidorHistorialDto>> ObtenerHistorialAsync(
|
Task<IEnumerable<DistribuidorHistorialDto>> ObtenerHistorialAsync(
|
||||||
DateTime? fechaDesde, DateTime? fechaHasta,
|
DateTime? fechaDesde, DateTime? fechaHasta,
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ namespace GestionIntegral.Api.Services.Impresion
|
|||||||
{
|
{
|
||||||
Task<IEnumerable<StockBobinaDto>> ObtenerTodosAsync(
|
Task<IEnumerable<StockBobinaDto>> ObtenerTodosAsync(
|
||||||
int? idTipoBobina, string? nroBobinaFilter, int? idPlanta,
|
int? idTipoBobina, string? nroBobinaFilter, int? idPlanta,
|
||||||
int? idEstadoBobina, string? remitoFilter, DateTime? fechaDesde, DateTime? fechaHasta);
|
int? idEstadoBobina, string? remitoFilter, DateTime? fechaDesde, DateTime? fechaHasta, DateTime? fechaEstadoDesde, DateTime? fechaEstadoHasta);
|
||||||
|
|
||||||
Task<StockBobinaDto?> ObtenerPorIdAsync(int idBobina);
|
Task<StockBobinaDto?> ObtenerPorIdAsync(int idBobina);
|
||||||
Task<(StockBobinaDto? Bobina, string? Error)> IngresarBobinaAsync(CreateStockBobinaDto createDto, int idUsuario);
|
Task<(StockBobinaDto? Bobina, string? Error)> IngresarBobinaAsync(CreateStockBobinaDto createDto, int idUsuario);
|
||||||
@@ -21,5 +21,8 @@ namespace GestionIntegral.Api.Services.Impresion
|
|||||||
DateTime? fechaDesde, DateTime? fechaHasta,
|
DateTime? fechaDesde, DateTime? fechaHasta,
|
||||||
int? idUsuarioModifico, string? tipoModificacion,
|
int? idUsuarioModifico, string? tipoModificacion,
|
||||||
int? idBobinaAfectada, int? idTipoBobinaFiltro, int? idPlantaFiltro, int? idEstadoBobinaFiltro);
|
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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -85,9 +85,9 @@ namespace GestionIntegral.Api.Services.Impresion
|
|||||||
|
|
||||||
public async Task<IEnumerable<StockBobinaDto>> ObtenerTodosAsync(
|
public async Task<IEnumerable<StockBobinaDto>> ObtenerTodosAsync(
|
||||||
int? idTipoBobina, string? nroBobinaFilter, int? idPlanta,
|
int? idTipoBobina, string? nroBobinaFilter, int? idPlanta,
|
||||||
int? idEstadoBobina, string? remitoFilter, DateTime? fechaDesde, DateTime? fechaHasta)
|
int? idEstadoBobina, string? remitoFilter, DateTime? fechaDesde, DateTime? fechaHasta, DateTime? fechaEstadoDesde, DateTime? fechaEstadoHasta)
|
||||||
{
|
{
|
||||||
var bobinas = await _stockBobinaRepository.GetAllAsync(idTipoBobina, nroBobinaFilter, idPlanta, idEstadoBobina, remitoFilter, fechaDesde, fechaHasta);
|
var bobinas = await _stockBobinaRepository.GetAllAsync(idTipoBobina, nroBobinaFilter, idPlanta, idEstadoBobina, remitoFilter, fechaDesde, fechaHasta, fechaEstadoDesde, fechaEstadoHasta);
|
||||||
var dtos = new List<StockBobinaDto>();
|
var dtos = new List<StockBobinaDto>();
|
||||||
foreach (var bobina in bobinas)
|
foreach (var bobina in bobinas)
|
||||||
{
|
{
|
||||||
@@ -166,16 +166,16 @@ namespace GestionIntegral.Api.Services.Impresion
|
|||||||
}
|
}
|
||||||
if (await _tipoBobinaRepository.GetByIdAsync(updateDto.IdTipoBobina) == null)
|
if (await _tipoBobinaRepository.GetByIdAsync(updateDto.IdTipoBobina) == null)
|
||||||
return (false, "Tipo de bobina inválido.");
|
return (false, "Tipo de bobina inválido.");
|
||||||
if (await _plantaRepository.GetByIdAsync(updateDto.IdPlanta) == null)
|
//if (await _plantaRepository.GetByIdAsync(updateDto.IdPlanta) == null)
|
||||||
return (false, "Planta inválida.");
|
// return (false, "Planta inválida.");
|
||||||
|
|
||||||
|
|
||||||
bobinaExistente.IdTipoBobina = updateDto.IdTipoBobina;
|
bobinaExistente.IdTipoBobina = updateDto.IdTipoBobina;
|
||||||
bobinaExistente.NroBobina = updateDto.NroBobina;
|
bobinaExistente.NroBobina = updateDto.NroBobina;
|
||||||
bobinaExistente.Peso = updateDto.Peso;
|
bobinaExistente.Peso = updateDto.Peso;
|
||||||
bobinaExistente.IdPlanta = updateDto.IdPlanta;
|
//bobinaExistente.IdPlanta = updateDto.IdPlanta;
|
||||||
bobinaExistente.Remito = updateDto.Remito;
|
//bobinaExistente.Remito = updateDto.Remito;
|
||||||
bobinaExistente.FechaRemito = updateDto.FechaRemito.Date;
|
//bobinaExistente.FechaRemito = updateDto.FechaRemito.Date;
|
||||||
// FechaEstado se mantiene ya que el estado no cambia aquí
|
// FechaEstado se mantiene ya que el estado no cambia aquí
|
||||||
|
|
||||||
var actualizado = await _stockBobinaRepository.UpdateAsync(bobinaExistente, idUsuario, transaction, "Datos Actualizados");
|
var actualizado = await _stockBobinaRepository.UpdateAsync(bobinaExistente, idUsuario, transaction, "Datos Actualizados");
|
||||||
@@ -199,13 +199,21 @@ namespace GestionIntegral.Api.Services.Impresion
|
|||||||
using var transaction = connection.BeginTransaction();
|
using var transaction = connection.BeginTransaction();
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var bobina = await _stockBobinaRepository.GetByIdAsync(idBobina); // Obtener dentro de la transacción
|
var bobina = await _stockBobinaRepository.GetByIdAsync(idBobina);
|
||||||
if (bobina == null)
|
if (bobina == null)
|
||||||
{
|
{
|
||||||
try { transaction.Rollback(); } catch { }
|
try { transaction.Rollback(); } catch { }
|
||||||
return (false, "Bobina no encontrada.");
|
return (false, "Bobina no encontrada.");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Comparamos solo las fechas (sin hora) para evitar problemas de precisión.
|
||||||
|
if (cambiarEstadoDto.FechaCambioEstado.Date < bobina.FechaRemito.Date)
|
||||||
|
{
|
||||||
|
try { transaction.Rollback(); } catch { }
|
||||||
|
return (false, $"Error de integridad: La fecha del nuevo estado ({cambiarEstadoDto.FechaCambioEstado:dd/MM/yyyy}) " +
|
||||||
|
$"no puede ser anterior a la fecha de ingreso por remito ({bobina.FechaRemito:dd/MM/yyyy}).");
|
||||||
|
}
|
||||||
|
|
||||||
var nuevoEstado = await _estadoBobinaRepository.GetByIdAsync(cambiarEstadoDto.NuevoEstadoId);
|
var nuevoEstado = await _estadoBobinaRepository.GetByIdAsync(cambiarEstadoDto.NuevoEstadoId);
|
||||||
if (nuevoEstado == null)
|
if (nuevoEstado == null)
|
||||||
{
|
{
|
||||||
@@ -383,5 +391,153 @@ namespace GestionIntegral.Api.Services.Impresion
|
|||||||
TipoMod = h.Historial.TipoMod
|
TipoMod = h.Historial.TipoMod
|
||||||
}).ToList();
|
}).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, null, null);
|
||||||
|
|
||||||
|
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,
|
||||||
|
fechaEstadoDesde: null,
|
||||||
|
fechaEstadoHasta: null
|
||||||
|
);
|
||||||
|
|
||||||
|
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}");
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -27,16 +27,16 @@ namespace GestionIntegral.Api.Services.Reportes
|
|||||||
|
|
||||||
// Reporte Distribucion Canillas (MC005) - Este es un reporte más complejo
|
// Reporte Distribucion Canillas (MC005) - Este es un reporte más complejo
|
||||||
Task<(
|
Task<(
|
||||||
IEnumerable<DetalleDistribucionCanillaDto> Canillas,
|
IEnumerable<DetalleDistribucionCanillaDto> Canillas,
|
||||||
IEnumerable<DetalleDistribucionCanillaDto> CanillasAcc,
|
IEnumerable<DetalleDistribucionCanillaDto> CanillasAcc,
|
||||||
IEnumerable<DetalleDistribucionCanillaAllDto> CanillasAll,
|
IEnumerable<DetalleDistribucionCanillaAllDto> CanillasAll,
|
||||||
IEnumerable<DetalleDistribucionCanillaDto> CanillasFechaLiq,
|
IEnumerable<DetalleDistribucionCanillaDto> CanillasFechaLiq,
|
||||||
IEnumerable<DetalleDistribucionCanillaDto> CanillasAccFechaLiq,
|
IEnumerable<DetalleDistribucionCanillaDto> CanillasAccFechaLiq,
|
||||||
IEnumerable<ObtenerCtrlDevolucionesDto> CtrlDevolucionesRemitos, // Para SP_ObtenerCtrlDevoluciones
|
IEnumerable<ObtenerCtrlDevolucionesDto> CtrlDevolucionesRemitos,
|
||||||
IEnumerable<ControlDevolucionesReporteDto> CtrlDevolucionesParaDistCan, // Para SP_DistCanillasCantidadEntradaSalida
|
IEnumerable<ControlDevolucionesReporteDto> CtrlDevolucionesParaDistCan,
|
||||||
IEnumerable<DevueltosOtrosDiasDto> CtrlDevolucionesOtrosDias, // <--- NUEVO para SP_DistCanillasCantidadEntradaSalidaOtrosDias
|
IEnumerable<DevueltosOtrosDiasDto> CtrlDevolucionesOtrosDias,
|
||||||
string? Error
|
string? Error
|
||||||
)> ObtenerReporteDistribucionCanillasAsync(DateTime fecha, int idEmpresa);
|
)> ObtenerReporteDistribucionCanillasAsync(DateTime fecha, int idEmpresa, bool? esAccionista);
|
||||||
|
|
||||||
// Reporte Tiradas por Publicación y Secciones (RR008)
|
// Reporte Tiradas por Publicación y Secciones (RR008)
|
||||||
Task<(IEnumerable<TiradasPublicacionesSeccionesDto> Data, string? Error)> ObtenerTiradasPublicacionesSeccionesAsync(int idPublicacion, DateTime fechaDesde, DateTime fechaHasta, int idPlanta);
|
Task<(IEnumerable<TiradasPublicacionesSeccionesDto> Data, string? Error)> ObtenerTiradasPublicacionesSeccionesAsync(int idPublicacion, DateTime fechaDesde, DateTime fechaHasta, int idPlanta);
|
||||||
@@ -53,12 +53,14 @@ namespace GestionIntegral.Api.Services.Reportes
|
|||||||
Task<(IEnumerable<ComparativaConsumoBobinasDto> Data, string? Error)> ObtenerComparativaConsumoBobinasAsync(DateTime fechaInicioMesA, DateTime fechaFinMesA, DateTime fechaInicioMesB, DateTime fechaFinMesB, int idPlanta);
|
Task<(IEnumerable<ComparativaConsumoBobinasDto> Data, string? Error)> ObtenerComparativaConsumoBobinasAsync(DateTime fechaInicioMesA, DateTime fechaFinMesA, DateTime fechaInicioMesB, DateTime fechaFinMesB, int idPlanta);
|
||||||
Task<(IEnumerable<ComparativaConsumoBobinasDto> Data, string? Error)> ObtenerComparativaConsumoBobinasConsolidadoAsync(DateTime fechaInicioMesA, DateTime fechaFinMesA, DateTime fechaInicioMesB, DateTime fechaFinMesB);
|
Task<(IEnumerable<ComparativaConsumoBobinasDto> Data, string? Error)> ObtenerComparativaConsumoBobinasConsolidadoAsync(DateTime fechaInicioMesA, DateTime fechaFinMesA, DateTime fechaInicioMesB, DateTime fechaFinMesB);
|
||||||
|
|
||||||
// DTOs para ReporteCuentasDistribuidores
|
// DTOs para ReporteCuentasDistribuidores.
|
||||||
|
// El antiguo IEnumerable<SaldoDto> Saldos (saldo actual del distribuidor, sin sentido temporal en un reporte por período)
|
||||||
|
// se reemplazó por decimal SaldoInicial calculado en base al último cierre vigente.
|
||||||
Task<(
|
Task<(
|
||||||
IEnumerable<BalanceCuentaDistDto> EntradasSalidas,
|
IEnumerable<BalanceCuentaDistDto> EntradasSalidas,
|
||||||
IEnumerable<BalanceCuentaDebCredDto> DebitosCreditos,
|
IEnumerable<BalanceCuentaDebCredDto> DebitosCreditos,
|
||||||
IEnumerable<BalanceCuentaPagosDto> Pagos,
|
IEnumerable<BalanceCuentaPagosDto> Pagos,
|
||||||
IEnumerable<SaldoDto> Saldos,
|
decimal SaldoInicial,
|
||||||
string? Error
|
string? Error
|
||||||
)> ObtenerReporteCuentasDistribuidorAsync(int idDistribuidor, int idEmpresa, DateTime fechaDesde, DateTime fechaHasta);
|
)> ObtenerReporteCuentasDistribuidorAsync(int idDistribuidor, int idEmpresa, DateTime fechaDesde, DateTime fechaHasta);
|
||||||
Task<(IEnumerable<ListadoDistribucionDistSimpleDto> Simple, IEnumerable<ListadoDistribucionDistPromedioDiaDto> Promedios, string? Error)> ObtenerListadoDistribucionDistribuidoresAsync(int idDistribuidor, int idPublicacion, DateTime fechaDesde, DateTime fechaHasta);
|
Task<(IEnumerable<ListadoDistribucionDistSimpleDto> Simple, IEnumerable<ListadoDistribucionDistPromedioDiaDto> Promedios, string? Error)> ObtenerListadoDistribucionDistribuidoresAsync(int idDistribuidor, int idPublicacion, DateTime fechaDesde, DateTime fechaHasta);
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ using GestionIntegral.Api.Data.Repositories.Distribucion;
|
|||||||
using GestionIntegral.Api.Data.Repositories.Reportes;
|
using GestionIntegral.Api.Data.Repositories.Reportes;
|
||||||
using GestionIntegral.Api.Data.Repositories.Suscripciones;
|
using GestionIntegral.Api.Data.Repositories.Suscripciones;
|
||||||
using GestionIntegral.Api.Dtos.Reportes;
|
using GestionIntegral.Api.Dtos.Reportes;
|
||||||
|
using GestionIntegral.Api.Services.Contables;
|
||||||
|
|
||||||
namespace GestionIntegral.Api.Services.Reportes
|
namespace GestionIntegral.Api.Services.Reportes
|
||||||
{
|
{
|
||||||
@@ -14,10 +15,11 @@ namespace GestionIntegral.Api.Services.Reportes
|
|||||||
private readonly IEmpresaRepository _empresaRepository;
|
private readonly IEmpresaRepository _empresaRepository;
|
||||||
private readonly ISuscriptorRepository _suscriptorRepository;
|
private readonly ISuscriptorRepository _suscriptorRepository;
|
||||||
private readonly ISuscripcionRepository _suscripcionRepository;
|
private readonly ISuscripcionRepository _suscripcionRepository;
|
||||||
|
private readonly ICierreCuentaCorrienteService _cierreService;
|
||||||
private readonly ILogger<ReportesService> _logger;
|
private readonly ILogger<ReportesService> _logger;
|
||||||
|
|
||||||
public ReportesService(IReportesRepository reportesRepository, IFacturaRepository facturaRepository, IFacturaDetalleRepository facturaDetalleRepository, IPublicacionRepository publicacionRepository, IEmpresaRepository empresaRepository
|
public ReportesService(IReportesRepository reportesRepository, IFacturaRepository facturaRepository, IFacturaDetalleRepository facturaDetalleRepository, IPublicacionRepository publicacionRepository, IEmpresaRepository empresaRepository
|
||||||
, ISuscriptorRepository suscriptorRepository, ISuscripcionRepository suscripcionRepository, ILogger<ReportesService> logger)
|
, ISuscriptorRepository suscriptorRepository, ISuscripcionRepository suscripcionRepository, ICierreCuentaCorrienteService cierreService, ILogger<ReportesService> logger)
|
||||||
{
|
{
|
||||||
_reportesRepository = reportesRepository;
|
_reportesRepository = reportesRepository;
|
||||||
_facturaRepository = facturaRepository;
|
_facturaRepository = facturaRepository;
|
||||||
@@ -26,6 +28,7 @@ namespace GestionIntegral.Api.Services.Reportes
|
|||||||
_empresaRepository = empresaRepository;
|
_empresaRepository = empresaRepository;
|
||||||
_suscriptorRepository = suscriptorRepository;
|
_suscriptorRepository = suscriptorRepository;
|
||||||
_suscripcionRepository = suscripcionRepository;
|
_suscripcionRepository = suscripcionRepository;
|
||||||
|
_cierreService = cierreService;
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -218,30 +221,66 @@ namespace GestionIntegral.Api.Services.Reportes
|
|||||||
}
|
}
|
||||||
|
|
||||||
public async Task<(
|
public async Task<(
|
||||||
IEnumerable<DetalleDistribucionCanillaDto> Canillas,
|
IEnumerable<DetalleDistribucionCanillaDto> Canillas,
|
||||||
IEnumerable<DetalleDistribucionCanillaDto> CanillasAcc,
|
IEnumerable<DetalleDistribucionCanillaDto> CanillasAcc,
|
||||||
IEnumerable<DetalleDistribucionCanillaAllDto> CanillasAll,
|
IEnumerable<DetalleDistribucionCanillaAllDto> CanillasAll,
|
||||||
IEnumerable<DetalleDistribucionCanillaDto> CanillasFechaLiq,
|
IEnumerable<DetalleDistribucionCanillaDto> CanillasFechaLiq,
|
||||||
IEnumerable<DetalleDistribucionCanillaDto> CanillasAccFechaLiq,
|
IEnumerable<DetalleDistribucionCanillaDto> CanillasAccFechaLiq,
|
||||||
IEnumerable<ObtenerCtrlDevolucionesDto> CtrlDevolucionesRemitos,
|
IEnumerable<ObtenerCtrlDevolucionesDto> CtrlDevolucionesRemitos,
|
||||||
IEnumerable<ControlDevolucionesReporteDto> CtrlDevolucionesParaDistCan,
|
IEnumerable<ControlDevolucionesReporteDto> CtrlDevolucionesParaDistCan,
|
||||||
IEnumerable<DevueltosOtrosDiasDto> CtrlDevolucionesOtrosDias,
|
IEnumerable<DevueltosOtrosDiasDto> CtrlDevolucionesOtrosDias,
|
||||||
string? Error
|
string? Error
|
||||||
)> ObtenerReporteDistribucionCanillasAsync(DateTime fecha, int idEmpresa)
|
)> ObtenerReporteDistribucionCanillasAsync(DateTime fecha, int idEmpresa, bool? esAccionista)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var canillasTask = _reportesRepository.GetDetalleDistribucionCanillasPubliAsync(fecha, idEmpresa);
|
// Función helper para convertir fechas a UTC
|
||||||
var canillasAccTask = _reportesRepository.GetDetalleDistribucionCanillasAccPubliAsync(fecha, idEmpresa);
|
Func<IEnumerable<DetalleDistribucionCanillaDto>, IEnumerable<DetalleDistribucionCanillaDto>> toUtc =
|
||||||
|
items => items?.Select(c => { if (c.Fecha.HasValue) c.Fecha = DateTime.SpecifyKind(c.Fecha.Value.Date, DateTimeKind.Utc); return c; }).ToList()
|
||||||
|
?? Enumerable.Empty<DetalleDistribucionCanillaDto>();
|
||||||
|
|
||||||
|
// --- NUEVA LÓGICA PARA "TODAS LAS EMPRESAS" ---
|
||||||
|
if (idEmpresa == 0)
|
||||||
|
{
|
||||||
|
Task<IEnumerable<DetalleDistribucionCanillaDto>> canillasTask = Task.FromResult(Enumerable.Empty<DetalleDistribucionCanillaDto>());
|
||||||
|
Task<IEnumerable<DetalleDistribucionCanillaDto>> canillasAccTask = Task.FromResult(Enumerable.Empty<DetalleDistribucionCanillaDto>());
|
||||||
|
|
||||||
|
if (esAccionista == true) // Solo accionistas
|
||||||
|
{
|
||||||
|
canillasAccTask = _reportesRepository.GetDetalleDistribucionCanillasAccPubli_AllEmpresasAsync(fecha);
|
||||||
|
}
|
||||||
|
else // Solo canillitas (o si es null, por defecto canillitas)
|
||||||
|
{
|
||||||
|
canillasTask = _reportesRepository.GetDetalleDistribucionCanillasPubli_AllEmpresasAsync(fecha);
|
||||||
|
}
|
||||||
|
|
||||||
|
await Task.WhenAll(canillasTask, canillasAccTask);
|
||||||
|
|
||||||
|
return (
|
||||||
|
toUtc(await canillasTask),
|
||||||
|
toUtc(await canillasAccTask),
|
||||||
|
Enumerable.Empty<DetalleDistribucionCanillaAllDto>(), // El resumen no aplica
|
||||||
|
Enumerable.Empty<DetalleDistribucionCanillaDto>(), // Liquidaciones de otras fechas no aplican en esta vista simplificada
|
||||||
|
Enumerable.Empty<DetalleDistribucionCanillaDto>(),
|
||||||
|
Enumerable.Empty<ObtenerCtrlDevolucionesDto>(), // Control de devoluciones no aplica
|
||||||
|
Enumerable.Empty<ControlDevolucionesReporteDto>(),
|
||||||
|
Enumerable.Empty<DevueltosOtrosDiasDto>(),
|
||||||
|
null
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- LÓGICA ORIGINAL PARA UNA EMPRESA ESPECÍFICA ---
|
||||||
|
var canillasTaskOriginal = _reportesRepository.GetDetalleDistribucionCanillasPubliAsync(fecha, idEmpresa);
|
||||||
|
var canillasAccTaskOriginal = _reportesRepository.GetDetalleDistribucionCanillasAccPubliAsync(fecha, idEmpresa);
|
||||||
var canillasAllTask = _reportesRepository.GetDetalleDistribucionCanillasAllPubliAsync(fecha, idEmpresa);
|
var canillasAllTask = _reportesRepository.GetDetalleDistribucionCanillasAllPubliAsync(fecha, idEmpresa);
|
||||||
var canillasFechaLiqTask = _reportesRepository.GetDetalleDistribucionCanillasPubliFechaLiqAsync(fecha, idEmpresa);
|
var canillasFechaLiqTask = _reportesRepository.GetDetalleDistribucionCanillasPubliFechaLiqAsync(fecha, idEmpresa);
|
||||||
var canillasAccFechaLiqTask = _reportesRepository.GetDetalleDistribucionCanillasAccPubliFechaLiqAsync(fecha, idEmpresa);
|
var canillasAccFechaLiqTask = _reportesRepository.GetDetalleDistribucionCanillasAccPubliFechaLiqAsync(fecha, idEmpresa);
|
||||||
var ctrlDevolucionesRemitosTask = _reportesRepository.GetReporteObtenerCtrlDevolucionesAsync(fecha, idEmpresa); // SP_ObtenerCtrlDevoluciones
|
var ctrlDevolucionesRemitosTask = _reportesRepository.GetReporteObtenerCtrlDevolucionesAsync(fecha, idEmpresa);
|
||||||
var ctrlDevolucionesParaDistCanTask = _reportesRepository.GetReporteCtrlDevolucionesParaDistCanAsync(fecha, idEmpresa); // SP_DistCanillasCantidadEntradaSalida
|
var ctrlDevolucionesParaDistCanTask = _reportesRepository.GetReporteCtrlDevolucionesParaDistCanAsync(fecha, idEmpresa);
|
||||||
var ctrlDevolucionesOtrosDiasTask = _reportesRepository.GetEntradaSalidaOtrosDiasAsync(fecha, idEmpresa); // SP_DistCanillasCantidadEntradaSalidaOtrosDias
|
var ctrlDevolucionesOtrosDiasTask = _reportesRepository.GetEntradaSalidaOtrosDiasAsync(fecha, idEmpresa);
|
||||||
|
|
||||||
await Task.WhenAll(
|
await Task.WhenAll(
|
||||||
canillasTask, canillasAccTask, canillasAllTask,
|
canillasTaskOriginal, canillasAccTaskOriginal, canillasAllTask,
|
||||||
canillasFechaLiqTask, canillasAccFechaLiqTask,
|
canillasFechaLiqTask, canillasAccFechaLiqTask,
|
||||||
ctrlDevolucionesRemitosTask, ctrlDevolucionesParaDistCanTask,
|
ctrlDevolucionesRemitosTask, ctrlDevolucionesParaDistCanTask,
|
||||||
ctrlDevolucionesOtrosDiasTask
|
ctrlDevolucionesOtrosDiasTask
|
||||||
@@ -250,13 +289,9 @@ namespace GestionIntegral.Api.Services.Reportes
|
|||||||
var detallesOriginales = await ctrlDevolucionesParaDistCanTask ?? Enumerable.Empty<ControlDevolucionesReporteDto>();
|
var detallesOriginales = await ctrlDevolucionesParaDistCanTask ?? Enumerable.Empty<ControlDevolucionesReporteDto>();
|
||||||
var detallesOrdenados = detallesOriginales.OrderBy(d => d.Tipo).ToList();
|
var detallesOrdenados = detallesOriginales.OrderBy(d => d.Tipo).ToList();
|
||||||
|
|
||||||
Func<IEnumerable<DetalleDistribucionCanillaDto>, IEnumerable<DetalleDistribucionCanillaDto>> toUtc =
|
|
||||||
items => items?.Select(c => { if (c.Fecha.HasValue) c.Fecha = DateTime.SpecifyKind(c.Fecha.Value.Date, DateTimeKind.Utc); return c; }).ToList()
|
|
||||||
?? Enumerable.Empty<DetalleDistribucionCanillaDto>();
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
toUtc(await canillasTask),
|
toUtc(await canillasTaskOriginal),
|
||||||
toUtc(await canillasAccTask),
|
toUtc(await canillasAccTaskOriginal),
|
||||||
await canillasAllTask ?? Enumerable.Empty<DetalleDistribucionCanillaAllDto>(),
|
await canillasAllTask ?? Enumerable.Empty<DetalleDistribucionCanillaAllDto>(),
|
||||||
toUtc(await canillasFechaLiqTask),
|
toUtc(await canillasFechaLiqTask),
|
||||||
toUtc(await canillasAccFechaLiqTask),
|
toUtc(await canillasAccFechaLiqTask),
|
||||||
@@ -388,26 +423,28 @@ namespace GestionIntegral.Api.Services.Reportes
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Implementación para ReporteCuentasDistribuidores
|
// Implementación para ReporteCuentasDistribuidores.
|
||||||
|
// Reemplaza el campo legacy "Saldos" (saldo actual sin sentido temporal) por "SaldoInicial":
|
||||||
|
// saldo del último cierre + movimientos netos hasta fechaDesde-1 (0 si no hay cierre previo).
|
||||||
public async Task<(
|
public async Task<(
|
||||||
IEnumerable<BalanceCuentaDistDto> EntradasSalidas,
|
IEnumerable<BalanceCuentaDistDto> EntradasSalidas,
|
||||||
IEnumerable<BalanceCuentaDebCredDto> DebitosCreditos,
|
IEnumerable<BalanceCuentaDebCredDto> DebitosCreditos,
|
||||||
IEnumerable<BalanceCuentaPagosDto> Pagos,
|
IEnumerable<BalanceCuentaPagosDto> Pagos,
|
||||||
IEnumerable<SaldoDto> Saldos,
|
decimal SaldoInicial,
|
||||||
string? Error
|
string? Error
|
||||||
)> ObtenerReporteCuentasDistribuidorAsync(int idDistribuidor, int idEmpresa, DateTime fechaDesde, DateTime fechaHasta)
|
)> ObtenerReporteCuentasDistribuidorAsync(int idDistribuidor, int idEmpresa, DateTime fechaDesde, DateTime fechaHasta)
|
||||||
{
|
{
|
||||||
if (fechaDesde > fechaHasta)
|
if (fechaDesde > fechaHasta)
|
||||||
return (Enumerable.Empty<BalanceCuentaDistDto>(), Enumerable.Empty<BalanceCuentaDebCredDto>(), Enumerable.Empty<BalanceCuentaPagosDto>(), Enumerable.Empty<SaldoDto>(), "Fecha 'Desde' no puede ser mayor que 'Hasta'.");
|
return (Enumerable.Empty<BalanceCuentaDistDto>(), Enumerable.Empty<BalanceCuentaDebCredDto>(), Enumerable.Empty<BalanceCuentaPagosDto>(), 0m, "Fecha 'Desde' no puede ser mayor que 'Hasta'.");
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var esTask = _reportesRepository.GetBalanceCuentaDistEntradaSalidaPorEmpresaAsync(idDistribuidor, idEmpresa, fechaDesde, fechaHasta);
|
var esTask = _reportesRepository.GetBalanceCuentaDistEntradaSalidaPorEmpresaAsync(idDistribuidor, idEmpresa, fechaDesde, fechaHasta);
|
||||||
var dcTask = _reportesRepository.GetBalanceCuentDistDebCredEmpresaAsync(idDistribuidor, idEmpresa, fechaDesde, fechaHasta);
|
var dcTask = _reportesRepository.GetBalanceCuentDistDebCredEmpresaAsync(idDistribuidor, idEmpresa, fechaDesde, fechaHasta);
|
||||||
var paTask = _reportesRepository.GetBalanceCuentDistPagosEmpresaAsync(idDistribuidor, idEmpresa, fechaDesde, fechaHasta);
|
var paTask = _reportesRepository.GetBalanceCuentDistPagosEmpresaAsync(idDistribuidor, idEmpresa, fechaDesde, fechaHasta);
|
||||||
var saTask = _reportesRepository.GetBalanceCuentSaldosEmpresasAsync("Distribuidores", idDistribuidor, idEmpresa);
|
var siTask = _cierreService.CalcularSaldoInicialReporteAsync(idDistribuidor, idEmpresa, fechaDesde);
|
||||||
|
|
||||||
await Task.WhenAll(esTask, dcTask, paTask, saTask);
|
await Task.WhenAll(esTask, dcTask, paTask, siTask);
|
||||||
|
|
||||||
Func<IEnumerable<BalanceCuentaDistDto>, IEnumerable<BalanceCuentaDistDto>> esToUtc =
|
Func<IEnumerable<BalanceCuentaDistDto>, IEnumerable<BalanceCuentaDistDto>> esToUtc =
|
||||||
items => items?.Select(i => { i.Fecha = DateTime.SpecifyKind(i.Fecha.Date, DateTimeKind.Utc); return i; }).ToList()
|
items => items?.Select(i => { i.Fecha = DateTime.SpecifyKind(i.Fecha.Date, DateTimeKind.Utc); return i; }).ToList()
|
||||||
@@ -423,7 +460,7 @@ namespace GestionIntegral.Api.Services.Reportes
|
|||||||
esToUtc(await esTask),
|
esToUtc(await esTask),
|
||||||
dcToUtc(await dcTask),
|
dcToUtc(await dcTask),
|
||||||
paToUtc(await paTask),
|
paToUtc(await paTask),
|
||||||
await saTask ?? Enumerable.Empty<SaldoDto>(),
|
await siTask,
|
||||||
null
|
null
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -434,7 +471,7 @@ namespace GestionIntegral.Api.Services.Reportes
|
|||||||
Enumerable.Empty<BalanceCuentaDistDto>(),
|
Enumerable.Empty<BalanceCuentaDistDto>(),
|
||||||
Enumerable.Empty<BalanceCuentaDebCredDto>(),
|
Enumerable.Empty<BalanceCuentaDebCredDto>(),
|
||||||
Enumerable.Empty<BalanceCuentaPagosDto>(),
|
Enumerable.Empty<BalanceCuentaPagosDto>(),
|
||||||
Enumerable.Empty<SaldoDto>(),
|
0m,
|
||||||
"Error interno al generar el reporte."
|
"Error interno al generar el reporte."
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,8 +17,8 @@ namespace GestionIntegral.Api.Services.Suscripciones
|
|||||||
private readonly DbConnectionFactory _connectionFactory;
|
private readonly DbConnectionFactory _connectionFactory;
|
||||||
private readonly ILogger<DebitoAutomaticoService> _logger;
|
private readonly ILogger<DebitoAutomaticoService> _logger;
|
||||||
|
|
||||||
private const string NRO_PRESTACION = "123456";
|
private const string NRO_PRESTACION = "26435"; // Reemplazar por el número real
|
||||||
private const string ORIGEN_EMPRESA = "ELDIA";
|
private const string ORIGEN_EMPRESA = "EMPRESA";
|
||||||
|
|
||||||
public DebitoAutomaticoService(
|
public DebitoAutomaticoService(
|
||||||
IFacturaRepository facturaRepository,
|
IFacturaRepository facturaRepository,
|
||||||
@@ -40,9 +40,7 @@ namespace GestionIntegral.Api.Services.Suscripciones
|
|||||||
|
|
||||||
public async Task<(string? ContenidoArchivo, string? NombreArchivo, string? Error)> GenerarArchivoPagoDirecto(int anio, int mes, int idUsuario)
|
public async Task<(string? ContenidoArchivo, string? NombreArchivo, string? Error)> GenerarArchivoPagoDirecto(int anio, int mes, int idUsuario)
|
||||||
{
|
{
|
||||||
// Se define la identificación del archivo.
|
// Este número debe ser gestionado para no repetirse. Por ahora, lo mantenemos como 1.
|
||||||
// Este número debe ser gestionado para no repetirse en archivos generados
|
|
||||||
// para la misma prestación y fecha.
|
|
||||||
const int identificacionArchivo = 1;
|
const int identificacionArchivo = 1;
|
||||||
|
|
||||||
var periodo = $"{anio}-{mes:D2}";
|
var periodo = $"{anio}-{mes:D2}";
|
||||||
@@ -62,8 +60,6 @@ namespace GestionIntegral.Api.Services.Suscripciones
|
|||||||
|
|
||||||
var importeTotal = facturasParaDebito.Sum(f => f.Factura.ImporteFinal);
|
var importeTotal = facturasParaDebito.Sum(f => f.Factura.ImporteFinal);
|
||||||
var cantidadRegistros = facturasParaDebito.Count();
|
var cantidadRegistros = facturasParaDebito.Count();
|
||||||
|
|
||||||
// Se utiliza la variable 'identificacionArchivo' para nombrar el archivo.
|
|
||||||
var nombreArchivo = $"{NRO_PRESTACION}{fechaGeneracion:yyyyMMdd}{identificacionArchivo}.txt";
|
var nombreArchivo = $"{NRO_PRESTACION}{fechaGeneracion:yyyyMMdd}{identificacionArchivo}.txt";
|
||||||
|
|
||||||
var nuevoLote = new LoteDebito
|
var nuevoLote = new LoteDebito
|
||||||
@@ -78,13 +74,11 @@ namespace GestionIntegral.Api.Services.Suscripciones
|
|||||||
if (loteCreado == null) throw new DataException("No se pudo crear el registro del lote de débito.");
|
if (loteCreado == null) throw new DataException("No se pudo crear el registro del lote de débito.");
|
||||||
|
|
||||||
var sb = new StringBuilder();
|
var sb = new StringBuilder();
|
||||||
// Se pasa la 'identificacionArchivo' al método que crea el Header.
|
|
||||||
sb.Append(CrearRegistroHeader(fechaGeneracion, importeTotal, cantidadRegistros, identificacionArchivo));
|
sb.Append(CrearRegistroHeader(fechaGeneracion, importeTotal, cantidadRegistros, identificacionArchivo));
|
||||||
foreach (var item in facturasParaDebito)
|
foreach (var item in facturasParaDebito)
|
||||||
{
|
{
|
||||||
sb.Append(CrearRegistroDetalle(item.Factura, item.Suscriptor));
|
sb.Append(CrearRegistroDetalle(item.Factura, item.Suscriptor));
|
||||||
}
|
}
|
||||||
// Se pasa la 'identificacionArchivo' al método que crea el Trailer.
|
|
||||||
sb.Append(CrearRegistroTrailer(fechaGeneracion, importeTotal, cantidadRegistros, identificacionArchivo));
|
sb.Append(CrearRegistroTrailer(fechaGeneracion, importeTotal, cantidadRegistros, identificacionArchivo));
|
||||||
|
|
||||||
var idsFacturas = facturasParaDebito.Select(f => f.Factura.IdFactura);
|
var idsFacturas = facturasParaDebito.Select(f => f.Factura.IdFactura);
|
||||||
@@ -108,17 +102,18 @@ namespace GestionIntegral.Api.Services.Suscripciones
|
|||||||
var facturas = await _facturaRepository.GetByPeriodoAsync(periodo);
|
var facturas = await _facturaRepository.GetByPeriodoAsync(periodo);
|
||||||
var resultado = new List<(Factura, Suscriptor)>();
|
var resultado = new List<(Factura, Suscriptor)>();
|
||||||
|
|
||||||
foreach (var f in facturas.Where(fa => fa.EstadoPago == "Pendiente"))
|
// Filtramos por estado Y POR TIPO DE FACTURA
|
||||||
|
foreach (var f in facturas.Where(fa =>
|
||||||
|
(fa.EstadoPago == "Pendiente" || fa.EstadoPago == "Pagada Parcialmente" || fa.EstadoPago == "Rechazada") &&
|
||||||
|
fa.TipoFactura == "Mensual"
|
||||||
|
))
|
||||||
{
|
{
|
||||||
var suscriptor = await _suscriptorRepository.GetByIdAsync(f.IdSuscriptor);
|
var suscriptor = await _suscriptorRepository.GetByIdAsync(f.IdSuscriptor);
|
||||||
|
|
||||||
// Se valida que el CBU de Banelco (22 caracteres) exista antes de intentar la conversión.
|
|
||||||
if (suscriptor == null || string.IsNullOrWhiteSpace(suscriptor.CBU) || suscriptor.CBU.Length != 22)
|
if (suscriptor == null || string.IsNullOrWhiteSpace(suscriptor.CBU) || suscriptor.CBU.Length != 22)
|
||||||
{
|
{
|
||||||
_logger.LogWarning("Suscriptor ID {IdSuscriptor} omitido del lote de débito por CBU inválido o ausente (se esperan 22 dígitos).", suscriptor?.IdSuscriptor);
|
_logger.LogWarning("Suscriptor ID {IdSuscriptor} omitido del lote de débito por CBU inválido o ausente (se esperan 22 dígitos).", f.IdSuscriptor);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
var formaPago = await _formaPagoRepository.GetByIdAsync(suscriptor.IdFormaPagoPreferida);
|
var formaPago = await _formaPagoRepository.GetByIdAsync(suscriptor.IdFormaPagoPreferida);
|
||||||
if (formaPago != null && formaPago.RequiereCBU)
|
if (formaPago != null && formaPago.RequiereCBU)
|
||||||
{
|
{
|
||||||
@@ -128,26 +123,13 @@ namespace GestionIntegral.Api.Services.Suscripciones
|
|||||||
return resultado;
|
return resultado;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Lógica de conversión de CBU.
|
|
||||||
private string ConvertirCbuBanelcoASnp(string cbu22)
|
private string ConvertirCbuBanelcoASnp(string cbu22)
|
||||||
{
|
{
|
||||||
if (string.IsNullOrEmpty(cbu22) || cbu22.Length != 22)
|
if (string.IsNullOrEmpty(cbu22) || cbu22.Length != 22) return "".PadRight(26);
|
||||||
{
|
|
||||||
_logger.LogError("Se intentó convertir un CBU inválido de {Length} caracteres. Se devolverá un campo vacío.", cbu22?.Length ?? 0);
|
|
||||||
// Devolver un string de 26 espacios/ceros según la preferencia del banco para campos erróneos.
|
|
||||||
return "".PadRight(26);
|
|
||||||
}
|
|
||||||
|
|
||||||
// El formato SNP de 26 se obtiene insertando un "0" al inicio y "000" después del 8vo caracter del CBU de 22.
|
|
||||||
// Formato Banelco (22): [BBBSSSSX] [T....Y]
|
|
||||||
// Posiciones: (0-7) (8-21)
|
|
||||||
// Formato SNP (26): 0[BBBSSSSX]000[T....Y]
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
string bloque1 = cbu22.Substring(0, 8); // Contiene código de banco, sucursal y DV del bloque 1.
|
string bloque1 = cbu22.Substring(0, 8);
|
||||||
string bloque2 = cbu22.Substring(8); // Contiene el resto de la cadena.
|
string bloque2 = cbu22.Substring(8);
|
||||||
|
|
||||||
// Reconstruir en formato SNP de 26 dígitos según el instructivo.
|
|
||||||
return $"0{bloque1}000{bloque2}";
|
return $"0{bloque1}000{bloque2}";
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
@@ -157,9 +139,10 @@ namespace GestionIntegral.Api.Services.Suscripciones
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- Métodos de Formateo y Mapeo ---
|
// --- Helpers de Formateo ---
|
||||||
private string FormatString(string? value, int length) => (value ?? "").PadRight(length);
|
private string FormatString(string? value, int length) => (value ?? "").PadRight(length);
|
||||||
private string FormatNumeric(long value, int length) => value.ToString().PadLeft(length, '0');
|
private string FormatNumeric(long value, int length) => value.ToString().PadLeft(length, '0');
|
||||||
|
private string FormatNumericString(string? value, int length) => (value ?? "").PadLeft(length, '0');
|
||||||
private string MapTipoDocumento(string tipo) => tipo.ToUpper() switch
|
private string MapTipoDocumento(string tipo) => tipo.ToUpper() switch
|
||||||
{
|
{
|
||||||
"DNI" => "0096",
|
"DNI" => "0096",
|
||||||
@@ -167,17 +150,17 @@ namespace GestionIntegral.Api.Services.Suscripciones
|
|||||||
"CUIL" => "0086",
|
"CUIL" => "0086",
|
||||||
"LE" => "0089",
|
"LE" => "0089",
|
||||||
"LC" => "0090",
|
"LC" => "0090",
|
||||||
_ => "0000" // Tipo no especificado o C.I. Policía Federal según anexo.
|
_ => "0000"
|
||||||
};
|
};
|
||||||
|
|
||||||
private string CrearRegistroHeader(DateTime fechaGeneracion, decimal importeTotal, int cantidadRegistros, int identificacionArchivo)
|
private string CrearRegistroHeader(DateTime fechaGeneracion, decimal importeTotal, int cantidadRegistros, int identificacionArchivo)
|
||||||
{
|
{
|
||||||
var sb = new StringBuilder();
|
var sb = new StringBuilder();
|
||||||
sb.Append("00"); // Tipo de Registro Header
|
sb.Append("00");
|
||||||
sb.Append(FormatString(NRO_PRESTACION, 6));
|
sb.Append(FormatNumericString(NRO_PRESTACION, 6));
|
||||||
sb.Append("C"); // Servicio: Sistema Nacional de Pagos
|
sb.Append("C");
|
||||||
sb.Append(fechaGeneracion.ToString("yyyyMMdd"));
|
sb.Append(fechaGeneracion.ToString("yyyyMMdd"));
|
||||||
sb.Append(FormatString(identificacionArchivo.ToString(), 1)); // Identificación de Archivo
|
sb.Append(FormatString(identificacionArchivo.ToString(), 1));
|
||||||
sb.Append(FormatString(ORIGEN_EMPRESA, 7));
|
sb.Append(FormatString(ORIGEN_EMPRESA, 7));
|
||||||
sb.Append(FormatNumeric((long)(importeTotal * 100), 14));
|
sb.Append(FormatNumeric((long)(importeTotal * 100), 14));
|
||||||
sb.Append(FormatNumeric(cantidadRegistros, 7));
|
sb.Append(FormatNumeric(cantidadRegistros, 7));
|
||||||
@@ -188,35 +171,33 @@ namespace GestionIntegral.Api.Services.Suscripciones
|
|||||||
|
|
||||||
private string CrearRegistroDetalle(Factura factura, Suscriptor suscriptor)
|
private string CrearRegistroDetalle(Factura factura, Suscriptor suscriptor)
|
||||||
{
|
{
|
||||||
// Convertimos el CBU de 22 (Banelco) a 26 (SNP) antes de usarlo.
|
|
||||||
string cbu26 = ConvertirCbuBanelcoASnp(suscriptor.CBU!);
|
string cbu26 = ConvertirCbuBanelcoASnp(suscriptor.CBU!);
|
||||||
|
|
||||||
var sb = new StringBuilder();
|
var sb = new StringBuilder();
|
||||||
sb.Append("0370"); // Tipo de Registro Detalle (Orden de Débito)
|
sb.Append("0370");
|
||||||
sb.Append(FormatString(suscriptor.IdSuscriptor.ToString(), 22)); // Identificación de Cliente
|
sb.Append(FormatString(suscriptor.IdSuscriptor.ToString(), 22));
|
||||||
sb.Append(FormatString(cbu26, 26)); // CBU en formato SNP de 26 caracteres.
|
sb.Append(cbu26);
|
||||||
sb.Append(FormatString($"SUSC-{factura.IdFactura}", 15)); // Referencia Unívoca de la factura.
|
sb.Append(FormatString($"SUSC-{factura.IdFactura}", 15));
|
||||||
sb.Append(factura.FechaVencimiento.ToString("yyyyMMdd"));
|
sb.Append(factura.FechaVencimiento.ToString("yyyyMMdd"));
|
||||||
sb.Append(FormatNumeric((long)(factura.ImporteFinal * 100), 14));
|
sb.Append(FormatNumeric((long)(factura.ImporteFinal * 100), 14));
|
||||||
sb.Append(FormatNumeric(0, 8)); // Fecha 2do Vencimiento
|
sb.Append(FormatNumeric(0, 8)); // Fecha 2do Vencimiento
|
||||||
sb.Append(FormatNumeric(0, 14)); // Importe 2do Vencimiento
|
sb.Append(FormatNumeric(0, 14)); // Importe 2do Vencimiento
|
||||||
sb.Append(FormatNumeric(0, 8)); // Fecha 3er Vencimiento
|
sb.Append(FormatNumeric(0, 8)); // Fecha 3er Vencimiento
|
||||||
sb.Append(FormatNumeric(0, 14)); // Importe 3er Vencimiento
|
sb.Append(FormatNumeric(0, 14)); // Importe 3er Vencimiento
|
||||||
sb.Append("0"); // Moneda (0 = Pesos)
|
sb.Append("0");
|
||||||
sb.Append(FormatString("", 3)); // Motivo Rechazo (vacío en el envío)
|
sb.Append(FormatString("", 3));
|
||||||
sb.Append(FormatString(MapTipoDocumento(suscriptor.TipoDocumento), 4));
|
sb.Append(FormatString(MapTipoDocumento(suscriptor.TipoDocumento), 4));
|
||||||
sb.Append(FormatString(suscriptor.NroDocumento, 11));
|
sb.Append(FormatNumericString(suscriptor.NroDocumento, 11));
|
||||||
sb.Append(FormatString("", 22)); // Nueva ID Cliente
|
sb.Append(FormatString("", 22));
|
||||||
sb.Append(FormatString("", 26)); // Nueva CBU
|
sb.Append(FormatString("", 26));
|
||||||
sb.Append(FormatNumeric(0, 14)); // Importe Mínimo
|
sb.Append(FormatNumeric(0, 14));
|
||||||
sb.Append(FormatNumeric(0, 8)); // Fecha Próximo Vencimiento
|
sb.Append(FormatNumeric(0, 8));
|
||||||
sb.Append(FormatString("", 22)); // Identificación Cuenta Anterior
|
sb.Append(FormatString("", 22));
|
||||||
sb.Append(FormatString("", 40)); // Mensaje ATM
|
sb.Append(FormatString("", 40));
|
||||||
sb.Append(FormatString($"Susc.{factura.Periodo}", 10)); // Concepto Factura
|
sb.Append(FormatString($"Susc.{factura.Periodo}", 10));
|
||||||
sb.Append(FormatNumeric(0, 8)); // Fecha de Cobro
|
sb.Append(FormatNumeric(0, 8));
|
||||||
sb.Append(FormatNumeric(0, 14)); // Importe Cobrado
|
sb.Append(FormatNumeric(0, 14));
|
||||||
sb.Append(FormatNumeric(0, 8)); // Fecha de Acreditamiento
|
sb.Append(FormatNumeric(0, 8));
|
||||||
sb.Append(FormatString("", 26)); // Libre
|
sb.Append(FormatString("", 26));
|
||||||
sb.Append("\r\n");
|
sb.Append("\r\n");
|
||||||
return sb.ToString();
|
return sb.ToString();
|
||||||
}
|
}
|
||||||
@@ -224,16 +205,16 @@ namespace GestionIntegral.Api.Services.Suscripciones
|
|||||||
private string CrearRegistroTrailer(DateTime fechaGeneracion, decimal importeTotal, int cantidadRegistros, int identificacionArchivo)
|
private string CrearRegistroTrailer(DateTime fechaGeneracion, decimal importeTotal, int cantidadRegistros, int identificacionArchivo)
|
||||||
{
|
{
|
||||||
var sb = new StringBuilder();
|
var sb = new StringBuilder();
|
||||||
sb.Append("99"); // Tipo de Registro Trailer
|
sb.Append("99");
|
||||||
sb.Append(FormatString(NRO_PRESTACION, 6));
|
sb.Append(FormatNumericString(NRO_PRESTACION, 6));
|
||||||
sb.Append("C"); // Servicio: Sistema Nacional de Pagos
|
sb.Append("C");
|
||||||
sb.Append(fechaGeneracion.ToString("yyyyMMdd"));
|
sb.Append(fechaGeneracion.ToString("yyyyMMdd"));
|
||||||
sb.Append(FormatString(identificacionArchivo.ToString(), 1)); // Identificación de Archivo
|
sb.Append(FormatString(identificacionArchivo.ToString(), 1));
|
||||||
sb.Append(FormatString(ORIGEN_EMPRESA, 7));
|
sb.Append(FormatString(ORIGEN_EMPRESA, 7));
|
||||||
sb.Append(FormatNumeric((long)(importeTotal * 100), 14));
|
sb.Append(FormatNumeric((long)(importeTotal * 100), 14));
|
||||||
sb.Append(FormatNumeric(cantidadRegistros, 7));
|
sb.Append(FormatNumeric(cantidadRegistros, 7));
|
||||||
sb.Append(FormatString("", 304));
|
sb.Append(FormatString("", 304));
|
||||||
// La última línea del archivo no lleva salto de línea (\r\n).
|
sb.Append("\r\n");
|
||||||
return sb.ToString();
|
return sb.ToString();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -171,10 +171,13 @@ namespace GestionIntegral.Api.Services.Suscripciones
|
|||||||
DescuentoAplicado = descuentoPromocionesTotal,
|
DescuentoAplicado = descuentoPromocionesTotal,
|
||||||
ImporteFinal = importeFinal,
|
ImporteFinal = importeFinal,
|
||||||
EstadoPago = "Pendiente",
|
EstadoPago = "Pendiente",
|
||||||
EstadoFacturacion = "Pendiente de Facturar"
|
EstadoFacturacion = "Pendiente de Facturar",
|
||||||
|
TipoFactura = "Mensual"
|
||||||
};
|
};
|
||||||
|
|
||||||
var facturaCreada = await _facturaRepository.CreateAsync(nuevaFactura, transaction);
|
var facturaCreada = await _facturaRepository.CreateAsync(nuevaFactura, transaction);
|
||||||
if (facturaCreada == null) throw new DataException($"No se pudo crear la factura para suscriptor ID {idSuscriptor} y empresa ID {idEmpresa}");
|
if (facturaCreada == null) throw new DataException($"No se pudo crear la factura para suscriptor ID {idSuscriptor} y empresa ID {idEmpresa}");
|
||||||
|
|
||||||
facturasCreadas.Add(facturaCreada);
|
facturasCreadas.Add(facturaCreada);
|
||||||
foreach (var detalle in detallesParaFactura)
|
foreach (var detalle in detallesParaFactura)
|
||||||
{
|
{
|
||||||
@@ -278,11 +281,12 @@ namespace GestionIntegral.Api.Services.Suscripciones
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<IEnumerable<ResumenCuentaSuscriptorDto>> ObtenerResumenesDeCuentaPorPeriodo(int anio, int mes, string? nombreSuscriptor, string? estadoPago, string? estadoFacturacion)
|
public async Task<IEnumerable<ResumenCuentaSuscriptorDto>> ObtenerResumenesDeCuentaPorPeriodo(
|
||||||
|
int anio, int mes, string? nombreSuscriptor, string? estadoPago, string? estadoFacturacion, string? tipoFactura)
|
||||||
{
|
{
|
||||||
var periodo = $"{anio}-{mes:D2}";
|
var periodo = $"{anio}-{mes:D2}";
|
||||||
var facturasData = await _facturaRepository.GetByPeriodoEnrichedAsync(periodo, nombreSuscriptor, estadoPago, estadoFacturacion);
|
var facturasData = await _facturaRepository.GetByPeriodoEnrichedAsync(periodo, nombreSuscriptor, estadoPago, estadoFacturacion, tipoFactura);
|
||||||
var detallesData = await _facturaDetalleRepository.GetDetallesPorPeriodoAsync(periodo); // Necesitaremos este nuevo método en el repo
|
var detallesData = await _facturaDetalleRepository.GetDetallesPorPeriodoAsync(periodo);
|
||||||
var empresas = await _empresaRepository.GetAllAsync(null, null);
|
var empresas = await _empresaRepository.GetAllAsync(null, null);
|
||||||
|
|
||||||
var resumenes = facturasData
|
var resumenes = facturasData
|
||||||
@@ -301,10 +305,18 @@ namespace GestionIntegral.Api.Services.Suscripciones
|
|||||||
EstadoPago = itemFactura.Factura.EstadoPago,
|
EstadoPago = itemFactura.Factura.EstadoPago,
|
||||||
EstadoFacturacion = itemFactura.Factura.EstadoFacturacion,
|
EstadoFacturacion = itemFactura.Factura.EstadoFacturacion,
|
||||||
NumeroFactura = itemFactura.Factura.NumeroFactura,
|
NumeroFactura = itemFactura.Factura.NumeroFactura,
|
||||||
|
TotalPagado = itemFactura.TotalPagado,
|
||||||
|
|
||||||
|
// Faltaba esta línea para pasar el tipo de factura al frontend.
|
||||||
|
TipoFactura = itemFactura.Factura.TipoFactura,
|
||||||
|
|
||||||
Detalles = detallesData
|
Detalles = detallesData
|
||||||
.Where(d => d.IdFactura == itemFactura.Factura.IdFactura)
|
.Where(d => d.IdFactura == itemFactura.Factura.IdFactura)
|
||||||
.Select(d => new FacturaDetalleDto { Descripcion = d.Descripcion, ImporteNeto = d.ImporteNeto })
|
.Select(d => new FacturaDetalleDto { Descripcion = d.Descripcion, ImporteNeto = d.ImporteNeto })
|
||||||
.ToList()
|
.ToList(),
|
||||||
|
|
||||||
|
// Pasamos el id del suscriptor para facilitar las cosas en el frontend
|
||||||
|
IdSuscriptor = itemFactura.Factura.IdSuscriptor
|
||||||
};
|
};
|
||||||
}).ToList();
|
}).ToList();
|
||||||
|
|
||||||
@@ -314,7 +326,7 @@ namespace GestionIntegral.Api.Services.Suscripciones
|
|||||||
NombreSuscriptor = primerItem.NombreSuscriptor,
|
NombreSuscriptor = primerItem.NombreSuscriptor,
|
||||||
Facturas = facturasConsolidadas,
|
Facturas = facturasConsolidadas,
|
||||||
ImporteTotal = facturasConsolidadas.Sum(f => f.ImporteFinal),
|
ImporteTotal = facturasConsolidadas.Sum(f => f.ImporteFinal),
|
||||||
SaldoPendienteTotal = facturasConsolidadas.Sum(f => f.EstadoPago == "Pagada" ? 0 : f.ImporteFinal)
|
SaldoPendienteTotal = facturasConsolidadas.Sum(f => f.ImporteFinal - f.TotalPagado)
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -578,7 +590,7 @@ namespace GestionIntegral.Api.Services.Suscripciones
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task<decimal> CalcularImporteParaSuscripcion(Suscripcion suscripcion, int anio, int mes, IDbTransaction transaction)
|
public async Task<decimal> CalcularImporteParaSuscripcion(Suscripcion suscripcion, int anio, int mes, IDbTransaction transaction)
|
||||||
{
|
{
|
||||||
decimal importeTotal = 0;
|
decimal importeTotal = 0;
|
||||||
var diasDeEntrega = suscripcion.DiasEntrega.Split(',').ToHashSet();
|
var diasDeEntrega = suscripcion.DiasEntrega.Split(',').ToHashSet();
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
|
using System.Data;
|
||||||
using GestionIntegral.Api.Dtos.Comunicaciones;
|
using GestionIntegral.Api.Dtos.Comunicaciones;
|
||||||
using GestionIntegral.Api.Dtos.Suscripciones;
|
using GestionIntegral.Api.Dtos.Suscripciones;
|
||||||
|
using GestionIntegral.Api.Models.Suscripciones;
|
||||||
|
|
||||||
namespace GestionIntegral.Api.Services.Suscripciones
|
namespace GestionIntegral.Api.Services.Suscripciones
|
||||||
{
|
{
|
||||||
@@ -7,8 +9,10 @@ namespace GestionIntegral.Api.Services.Suscripciones
|
|||||||
{
|
{
|
||||||
Task<(bool Exito, string? Mensaje, LoteDeEnvioResumenDto? ResultadoEnvio)> GenerarFacturacionMensual(int anio, int mes, int idUsuario);
|
Task<(bool Exito, string? Mensaje, LoteDeEnvioResumenDto? ResultadoEnvio)> GenerarFacturacionMensual(int anio, int mes, int idUsuario);
|
||||||
Task<IEnumerable<LoteDeEnvioHistorialDto>> ObtenerHistorialLotesEnvio(int? anio, int? mes);
|
Task<IEnumerable<LoteDeEnvioHistorialDto>> ObtenerHistorialLotesEnvio(int? anio, int? mes);
|
||||||
Task<IEnumerable<ResumenCuentaSuscriptorDto>> ObtenerResumenesDeCuentaPorPeriodo(int anio, int mes, string? nombreSuscriptor, string? estadoPago, string? estadoFacturacion);
|
Task<IEnumerable<ResumenCuentaSuscriptorDto>> ObtenerResumenesDeCuentaPorPeriodo(
|
||||||
|
int anio, int mes, string? nombreSuscriptor, string? estadoPago, string? estadoFacturacion, string? tipoFactura);
|
||||||
Task<(bool Exito, string? Error, string? EmailDestino)> EnviarFacturaPdfPorEmail(int idFactura, int idUsuario);
|
Task<(bool Exito, string? Error, string? EmailDestino)> EnviarFacturaPdfPorEmail(int idFactura, int idUsuario);
|
||||||
Task<(bool Exito, string? Error)> ActualizarNumeroFactura(int idFactura, string numeroFactura, int idUsuario);
|
Task<(bool Exito, string? Error)> ActualizarNumeroFactura(int idFactura, string numeroFactura, int idUsuario);
|
||||||
|
Task<decimal> CalcularImporteParaSuscripcion(Suscripcion suscripcion, int anio, int mes, IDbTransaction transaction);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -70,14 +70,11 @@ namespace GestionIntegral.Api.Services.Suscripciones
|
|||||||
{
|
{
|
||||||
var factura = await _facturaRepository.GetByIdAsync(createDto.IdFactura);
|
var factura = await _facturaRepository.GetByIdAsync(createDto.IdFactura);
|
||||||
if (factura == null) return (null, "La factura especificada no existe.");
|
if (factura == null) return (null, "La factura especificada no existe.");
|
||||||
|
|
||||||
// Usar EstadoPago para la validación
|
|
||||||
if (factura.EstadoPago == "Anulada") return (null, "No se puede registrar un pago sobre una factura anulada.");
|
if (factura.EstadoPago == "Anulada") return (null, "No se puede registrar un pago sobre una factura anulada.");
|
||||||
|
|
||||||
var formaPago = await _formaPagoRepository.GetByIdAsync(createDto.IdFormaPago);
|
var formaPago = await _formaPagoRepository.GetByIdAsync(createDto.IdFormaPago);
|
||||||
if (formaPago == null || !formaPago.Activo) return (null, "La forma de pago no es válida.");
|
if (formaPago == null || !formaPago.Activo) return (null, "La forma de pago no es válida.");
|
||||||
|
|
||||||
// Obtenemos la suma de pagos ANTERIORES
|
|
||||||
var totalPagadoAnteriormente = await _pagoRepository.GetTotalPagadoAprobadoAsync(createDto.IdFactura, transaction);
|
var totalPagadoAnteriormente = await _pagoRepository.GetTotalPagadoAprobadoAsync(createDto.IdFactura, transaction);
|
||||||
|
|
||||||
var nuevoPago = new Pago
|
var nuevoPago = new Pago
|
||||||
@@ -96,37 +93,31 @@ namespace GestionIntegral.Api.Services.Suscripciones
|
|||||||
var pagoCreado = await _pagoRepository.CreateAsync(nuevoPago, transaction);
|
var pagoCreado = await _pagoRepository.CreateAsync(nuevoPago, transaction);
|
||||||
if (pagoCreado == null) throw new DataException("No se pudo registrar el pago.");
|
if (pagoCreado == null) throw new DataException("No se pudo registrar el pago.");
|
||||||
|
|
||||||
// Calculamos el nuevo total EN MEMORIA
|
|
||||||
var nuevoTotalPagado = totalPagadoAnteriormente + pagoCreado.Monto;
|
var nuevoTotalPagado = totalPagadoAnteriormente + pagoCreado.Monto;
|
||||||
|
|
||||||
// Comparamos y actualizamos el estado si es necesario
|
// Nueva lógica para manejar todos los estados de pago
|
||||||
// CORRECCIÓN: Usar EstadoPago y el método correcto del repositorio
|
string nuevoEstadoPago = factura.EstadoPago;
|
||||||
if (factura.EstadoPago != "Pagada" && nuevoTotalPagado >= factura.ImporteFinal)
|
if (nuevoTotalPagado >= factura.ImporteFinal)
|
||||||
{
|
{
|
||||||
bool actualizado = await _facturaRepository.UpdateEstadoPagoAsync(factura.IdFactura, "Pagada", transaction);
|
nuevoEstadoPago = "Pagada";
|
||||||
if (!actualizado) throw new DataException("No se pudo actualizar el estado de la factura a 'Pagada'.");
|
}
|
||||||
|
else if (nuevoTotalPagado > 0)
|
||||||
|
{
|
||||||
|
nuevoEstadoPago = "Pagada Parcialmente";
|
||||||
|
}
|
||||||
|
// Si nuevoTotalPagado es 0, el estado no cambia.
|
||||||
|
|
||||||
|
// Solo actualizamos si el estado calculado es diferente al actual.
|
||||||
|
if (nuevoEstadoPago != factura.EstadoPago)
|
||||||
|
{
|
||||||
|
bool actualizado = await _facturaRepository.UpdateEstadoPagoAsync(factura.IdFactura, nuevoEstadoPago, transaction);
|
||||||
|
if (!actualizado) throw new DataException($"No se pudo actualizar el estado de la factura a '{nuevoEstadoPago}'.");
|
||||||
}
|
}
|
||||||
|
|
||||||
transaction.Commit();
|
transaction.Commit();
|
||||||
_logger.LogInformation("Pago manual ID {IdPago} registrado para Factura ID {IdFactura} por Usuario ID {IdUsuario}", pagoCreado.IdPago, pagoCreado.IdFactura, idUsuario);
|
_logger.LogInformation("Pago manual ID {IdPago} registrado para Factura ID {IdFactura} por Usuario ID {IdUsuario}", pagoCreado.IdPago, pagoCreado.IdFactura, idUsuario);
|
||||||
|
|
||||||
// Construimos el DTO de respuesta SIN volver a consultar la base de datos
|
var dto = await MapToDto(pagoCreado); // MapToDto ahora es más simple
|
||||||
var usuario = await _usuarioRepository.GetByIdAsync(idUsuario);
|
|
||||||
var dto = new PagoDto
|
|
||||||
{
|
|
||||||
IdPago = pagoCreado.IdPago,
|
|
||||||
IdFactura = pagoCreado.IdFactura,
|
|
||||||
FechaPago = pagoCreado.FechaPago.ToString("yyyy-MM-dd"),
|
|
||||||
IdFormaPago = pagoCreado.IdFormaPago,
|
|
||||||
NombreFormaPago = formaPago.Nombre,
|
|
||||||
Monto = pagoCreado.Monto,
|
|
||||||
Estado = pagoCreado.Estado,
|
|
||||||
Referencia = pagoCreado.Referencia,
|
|
||||||
Observaciones = pagoCreado.Observaciones,
|
|
||||||
IdUsuarioRegistro = pagoCreado.IdUsuarioRegistro,
|
|
||||||
NombreUsuarioRegistro = usuario != null ? $"{usuario.Nombre} {usuario.Apellido}" : "N/A"
|
|
||||||
};
|
|
||||||
|
|
||||||
return (dto, null);
|
return (dto, null);
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
|
|||||||
@@ -3,12 +3,8 @@ using GestionIntegral.Api.Data.Repositories.Distribucion;
|
|||||||
using GestionIntegral.Api.Data.Repositories.Suscripciones;
|
using GestionIntegral.Api.Data.Repositories.Suscripciones;
|
||||||
using GestionIntegral.Api.Dtos.Suscripciones;
|
using GestionIntegral.Api.Dtos.Suscripciones;
|
||||||
using GestionIntegral.Api.Models.Suscripciones;
|
using GestionIntegral.Api.Models.Suscripciones;
|
||||||
using Microsoft.Extensions.Logging;
|
|
||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Data;
|
using System.Data;
|
||||||
using System.Linq;
|
using System.Globalization;
|
||||||
using System.Threading.Tasks;
|
|
||||||
|
|
||||||
namespace GestionIntegral.Api.Services.Suscripciones
|
namespace GestionIntegral.Api.Services.Suscripciones
|
||||||
{
|
{
|
||||||
@@ -18,23 +14,32 @@ namespace GestionIntegral.Api.Services.Suscripciones
|
|||||||
private readonly ISuscriptorRepository _suscriptorRepository;
|
private readonly ISuscriptorRepository _suscriptorRepository;
|
||||||
private readonly IPublicacionRepository _publicacionRepository;
|
private readonly IPublicacionRepository _publicacionRepository;
|
||||||
private readonly IPromocionRepository _promocionRepository;
|
private readonly IPromocionRepository _promocionRepository;
|
||||||
private readonly DbConnectionFactory _connectionFactory;
|
private readonly IFacturaRepository _facturaRepository;
|
||||||
|
private readonly IFacturaDetalleRepository _facturaDetalleRepository;
|
||||||
|
private readonly IFacturacionService _facturacionService;
|
||||||
private readonly ILogger<SuscripcionService> _logger;
|
private readonly ILogger<SuscripcionService> _logger;
|
||||||
|
private readonly DbConnectionFactory _connectionFactory;
|
||||||
|
|
||||||
public SuscripcionService(
|
public SuscripcionService(
|
||||||
ISuscripcionRepository suscripcionRepository,
|
ISuscripcionRepository suscripcionRepository,
|
||||||
ISuscriptorRepository suscriptorRepository,
|
ISuscriptorRepository suscriptorRepository,
|
||||||
IPublicacionRepository publicacionRepository,
|
IPublicacionRepository publicacionRepository,
|
||||||
IPromocionRepository promocionRepository,
|
IPromocionRepository promocionRepository,
|
||||||
DbConnectionFactory connectionFactory,
|
IFacturaRepository facturaRepository,
|
||||||
ILogger<SuscripcionService> logger)
|
IFacturaDetalleRepository facturaDetalleRepository,
|
||||||
|
IFacturacionService facturacionService,
|
||||||
|
ILogger<SuscripcionService> logger,
|
||||||
|
DbConnectionFactory connectionFactory)
|
||||||
{
|
{
|
||||||
_suscripcionRepository = suscripcionRepository;
|
_suscripcionRepository = suscripcionRepository;
|
||||||
_suscriptorRepository = suscriptorRepository;
|
_suscriptorRepository = suscriptorRepository;
|
||||||
_publicacionRepository = publicacionRepository;
|
_publicacionRepository = publicacionRepository;
|
||||||
_promocionRepository = promocionRepository;
|
_promocionRepository = promocionRepository;
|
||||||
_connectionFactory = connectionFactory;
|
_facturaRepository = facturaRepository;
|
||||||
|
_facturaDetalleRepository = facturaDetalleRepository;
|
||||||
|
_facturacionService = facturacionService;
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
|
_connectionFactory = connectionFactory;
|
||||||
}
|
}
|
||||||
|
|
||||||
private PromocionDto MapPromocionToDto(Promocion promo) => new PromocionDto
|
private PromocionDto MapPromocionToDto(Promocion promo) => new PromocionDto
|
||||||
@@ -122,6 +127,53 @@ namespace GestionIntegral.Api.Services.Suscripciones
|
|||||||
var creada = await _suscripcionRepository.CreateAsync(nuevaSuscripcion, transaction);
|
var creada = await _suscripcionRepository.CreateAsync(nuevaSuscripcion, transaction);
|
||||||
if (creada == null) throw new DataException("Error al crear la suscripción.");
|
if (creada == null) throw new DataException("Error al crear la suscripción.");
|
||||||
|
|
||||||
|
var ultimoPeriodoFacturadoStr = await _facturaRepository.GetUltimoPeriodoFacturadoAsync();
|
||||||
|
if (ultimoPeriodoFacturadoStr != null)
|
||||||
|
{
|
||||||
|
var ultimoPeriodo = DateTime.ParseExact(ultimoPeriodoFacturadoStr, "yyyy-MM", CultureInfo.InvariantCulture);
|
||||||
|
var periodoSuscripcion = new DateTime(creada.FechaInicio.Year, creada.FechaInicio.Month, 1);
|
||||||
|
|
||||||
|
if (periodoSuscripcion <= ultimoPeriodo)
|
||||||
|
{
|
||||||
|
_logger.LogInformation("Suscripción en período ya cerrado detectada. Generando factura de alta pro-rata.");
|
||||||
|
|
||||||
|
decimal importeProporcional = await _facturacionService.CalcularImporteParaSuscripcion(creada, creada.FechaInicio.Year, creada.FechaInicio.Month, transaction);
|
||||||
|
|
||||||
|
if (importeProporcional > 0)
|
||||||
|
{
|
||||||
|
var facturaDeAlta = new Factura
|
||||||
|
{
|
||||||
|
IdSuscriptor = creada.IdSuscriptor,
|
||||||
|
Periodo = creada.FechaInicio.ToString("yyyy-MM"),
|
||||||
|
FechaEmision = DateTime.Now.Date,
|
||||||
|
FechaVencimiento = DateTime.Now.AddDays(10).Date,
|
||||||
|
ImporteBruto = importeProporcional,
|
||||||
|
ImporteFinal = importeProporcional,
|
||||||
|
EstadoPago = "Pendiente",
|
||||||
|
EstadoFacturacion = "Pendiente de Facturar",
|
||||||
|
TipoFactura = "Alta"
|
||||||
|
};
|
||||||
|
|
||||||
|
var facturaCreada = await _facturaRepository.CreateAsync(facturaDeAlta, transaction);
|
||||||
|
if (facturaCreada == null) throw new DataException("No se pudo crear la factura de alta.");
|
||||||
|
|
||||||
|
var publicacion = await _publicacionRepository.GetByIdSimpleAsync(creada.IdPublicacion);
|
||||||
|
var finDeMes = new DateTime(creada.FechaInicio.Year, creada.FechaInicio.Month, 1).AddMonths(1).AddDays(-1);
|
||||||
|
|
||||||
|
await _facturaDetalleRepository.CreateAsync(new FacturaDetalle
|
||||||
|
{
|
||||||
|
IdFactura = facturaCreada.IdFactura,
|
||||||
|
IdSuscripcion = creada.IdSuscripcion,
|
||||||
|
Descripcion = $"Suscripción proporcional {publicacion?.Nombre} ({creada.FechaInicio:dd/MM} al {finDeMes:dd/MM})",
|
||||||
|
ImporteBruto = importeProporcional,
|
||||||
|
ImporteNeto = importeProporcional,
|
||||||
|
DescuentoAplicado = 0
|
||||||
|
}, transaction);
|
||||||
|
|
||||||
|
_logger.LogInformation("Factura de alta #{IdFactura} por ${Importe} generada para la nueva suscripción #{IdSuscripcion}.", facturaCreada.IdFactura, importeProporcional, creada.IdSuscripcion);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
transaction.Commit();
|
transaction.Commit();
|
||||||
_logger.LogInformation("Suscripción ID {Id} creada por Usuario ID {UserId}.", creada.IdSuscripcion, idUsuario);
|
_logger.LogInformation("Suscripción ID {Id} creada por Usuario ID {UserId}.", creada.IdSuscripcion, idUsuario);
|
||||||
return (await MapToDto(creada), null);
|
return (await MapToDto(creada), null);
|
||||||
|
|||||||
@@ -16,11 +16,11 @@
|
|||||||
},
|
},
|
||||||
"AllowedHosts": "*",
|
"AllowedHosts": "*",
|
||||||
"MailSettings": {
|
"MailSettings": {
|
||||||
"SmtpHost": "",
|
"SmtpHost": "192.168.5.201",
|
||||||
"SmtpPort": 0,
|
"SmtpPort": 587,
|
||||||
"SenderName": "",
|
"SenderName": "Club - Diario El Día",
|
||||||
"SenderEmail": "",
|
"SenderEmail": "alertas@eldia.com",
|
||||||
"SmtpUser": "",
|
"SmtpUser": "alertas@eldia.com",
|
||||||
"SmtpPass": ""
|
"SmtpPass": "@Alertas713550@"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -36,6 +36,7 @@ const AjusteSaldoModal: React.FC<AjusteSaldoModalProps> = ({
|
|||||||
}) => {
|
}) => {
|
||||||
const [montoAjuste, setMontoAjuste] = useState<string>('');
|
const [montoAjuste, setMontoAjuste] = useState<string>('');
|
||||||
const [justificacion, setJustificacion] = useState('');
|
const [justificacion, setJustificacion] = useState('');
|
||||||
|
const [fechaOperacion, setFechaOperacion] = useState<string>(new Date().toISOString().split('T')[0]);
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [localErrors, setLocalErrors] = useState<{ [key: string]: string | null }>({});
|
const [localErrors, setLocalErrors] = useState<{ [key: string]: string | null }>({});
|
||||||
|
|
||||||
@@ -43,10 +44,10 @@ const AjusteSaldoModal: React.FC<AjusteSaldoModalProps> = ({
|
|||||||
if (open) {
|
if (open) {
|
||||||
setMontoAjuste('');
|
setMontoAjuste('');
|
||||||
setJustificacion('');
|
setJustificacion('');
|
||||||
|
setFechaOperacion(new Date().toISOString().split('T')[0]);
|
||||||
setLocalErrors({});
|
setLocalErrors({});
|
||||||
clearErrorMessage();
|
|
||||||
}
|
}
|
||||||
}, [open, clearErrorMessage]);
|
}, [open]);
|
||||||
|
|
||||||
const validate = (): boolean => {
|
const validate = (): boolean => {
|
||||||
const errors: { [key: string]: string | null } = {};
|
const errors: { [key: string]: string | null } = {};
|
||||||
@@ -65,11 +66,16 @@ const AjusteSaldoModal: React.FC<AjusteSaldoModalProps> = ({
|
|||||||
} else if (justificacion.trim().length < 5 || justificacion.trim().length > 250) {
|
} else if (justificacion.trim().length < 5 || justificacion.trim().length > 250) {
|
||||||
errors.justificacion = 'La justificación debe tener entre 5 y 250 caracteres.';
|
errors.justificacion = 'La justificación debe tener entre 5 y 250 caracteres.';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!fechaOperacion) {
|
||||||
|
errors.fechaOperacion = 'La fecha de operación es obligatoria.';
|
||||||
|
}
|
||||||
|
|
||||||
setLocalErrors(errors);
|
setLocalErrors(errors);
|
||||||
return Object.keys(errors).length === 0;
|
return Object.keys(errors).length === 0;
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleInputChange = (fieldName: 'montoAjuste' | 'justificacion') => {
|
const handleInputChange = (fieldName: 'montoAjuste' | 'justificacion' | 'fechaOperacion') => {
|
||||||
if (localErrors[fieldName]) setLocalErrors(prev => ({ ...prev, [fieldName]: null }));
|
if (localErrors[fieldName]) setLocalErrors(prev => ({ ...prev, [fieldName]: null }));
|
||||||
if (errorMessage) clearErrorMessage();
|
if (errorMessage) clearErrorMessage();
|
||||||
};
|
};
|
||||||
@@ -87,6 +93,7 @@ const AjusteSaldoModal: React.FC<AjusteSaldoModalProps> = ({
|
|||||||
idEmpresa: saldoParaAjustar.idEmpresa,
|
idEmpresa: saldoParaAjustar.idEmpresa,
|
||||||
montoAjuste: parseFloat(montoAjuste),
|
montoAjuste: parseFloat(montoAjuste),
|
||||||
justificacion,
|
justificacion,
|
||||||
|
fechaOperacion,
|
||||||
};
|
};
|
||||||
await onSubmit(dataToSubmit);
|
await onSubmit(dataToSubmit);
|
||||||
onClose(); // Cerrar en éxito (el padre recargará)
|
onClose(); // Cerrar en éxito (el padre recargará)
|
||||||
@@ -117,6 +124,19 @@ const AjusteSaldoModal: React.FC<AjusteSaldoModalProps> = ({
|
|||||||
</Typography>
|
</Typography>
|
||||||
|
|
||||||
<Box component="form" onSubmit={handleSubmit} sx={{ mt: 2 }}>
|
<Box component="form" onSubmit={handleSubmit} sx={{ mt: 2 }}>
|
||||||
|
<TextField
|
||||||
|
label="Fecha de Operación"
|
||||||
|
type="date"
|
||||||
|
fullWidth
|
||||||
|
required
|
||||||
|
value={fechaOperacion}
|
||||||
|
onChange={(e) => { setFechaOperacion(e.target.value); handleInputChange('fechaOperacion'); }}
|
||||||
|
margin="normal"
|
||||||
|
error={!!localErrors.fechaOperacion}
|
||||||
|
helperText={localErrors.fechaOperacion || ''}
|
||||||
|
disabled={loading}
|
||||||
|
InputLabelProps={{ shrink: true }}
|
||||||
|
/>
|
||||||
<TextField
|
<TextField
|
||||||
label="Monto de Ajuste (+/-)"
|
label="Monto de Ajuste (+/-)"
|
||||||
type="number"
|
type="number"
|
||||||
|
|||||||
@@ -114,9 +114,8 @@ const NotaCreditoDebitoFormModal: React.FC<NotaCreditoDebitoFormModalProps> = ({
|
|||||||
setObservaciones(initialData?.observaciones || '');
|
setObservaciones(initialData?.observaciones || '');
|
||||||
setIdEmpresa(initialData?.idEmpresa || '');
|
setIdEmpresa(initialData?.idEmpresa || '');
|
||||||
setLocalErrors({});
|
setLocalErrors({});
|
||||||
clearErrorMessage();
|
|
||||||
}
|
}
|
||||||
}, [open, initialData, clearErrorMessage, fetchEmpresas, fetchDestinatarios]);
|
}, [open, initialData, fetchEmpresas, fetchDestinatarios]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if(open && !isEditing) { // Solo cambiar destinatarios si es creación y cambia el tipo de Destino
|
if(open && !isEditing) { // Solo cambiar destinatarios si es creación y cambia el tipo de Destino
|
||||||
|
|||||||
235
Frontend/src/components/Modals/Contables/NuevoCierreModal.tsx
Normal file
235
Frontend/src/components/Modals/Contables/NuevoCierreModal.tsx
Normal file
@@ -0,0 +1,235 @@
|
|||||||
|
import React, { useState, useEffect } from 'react';
|
||||||
|
import {
|
||||||
|
Modal, Box, Typography, TextField, Button, CircularProgress, Alert,
|
||||||
|
FormControl, InputLabel, Select, MenuItem
|
||||||
|
} from '@mui/material';
|
||||||
|
import type { CrearCierreDto } from '../../../models/dtos/Contables/CrearCierreDto';
|
||||||
|
import type { CierreCuentaCorrienteDto } from '../../../models/dtos/Contables/CierreCuentaCorrienteDto';
|
||||||
|
import type { DistribuidorDropdownDto } from '../../../models/dtos/Distribucion/DistribuidorDropdownDto';
|
||||||
|
import type { EmpresaDropdownDto } from '../../../models/dtos/Distribucion/EmpresaDropdownDto';
|
||||||
|
import distribuidorService from '../../../services/Distribucion/distribuidorService';
|
||||||
|
import empresaService from '../../../services/Distribucion/empresaService';
|
||||||
|
|
||||||
|
const modalStyle = {
|
||||||
|
position: 'absolute' as const,
|
||||||
|
top: '50%',
|
||||||
|
left: '50%',
|
||||||
|
transform: 'translate(-50%, -50%)',
|
||||||
|
width: { xs: '90%', sm: 600 },
|
||||||
|
bgcolor: 'background.paper',
|
||||||
|
border: '2px solid #000',
|
||||||
|
boxShadow: 24,
|
||||||
|
p: 4,
|
||||||
|
maxHeight: '90vh',
|
||||||
|
overflowY: 'auto'
|
||||||
|
};
|
||||||
|
|
||||||
|
interface NuevoCierreModalProps {
|
||||||
|
open: boolean;
|
||||||
|
onClose: () => void;
|
||||||
|
onSubmit: (data: CrearCierreDto) => Promise<CierreCuentaCorrienteDto>;
|
||||||
|
initialIdDistribuidor?: number | null;
|
||||||
|
initialIdEmpresa?: number | null;
|
||||||
|
errorMessage?: string | null;
|
||||||
|
clearErrorMessage: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const todayIso = () => new Date().toISOString().split('T')[0];
|
||||||
|
|
||||||
|
const NuevoCierreModal: React.FC<NuevoCierreModalProps> = ({
|
||||||
|
open,
|
||||||
|
onClose,
|
||||||
|
onSubmit,
|
||||||
|
initialIdDistribuidor,
|
||||||
|
initialIdEmpresa,
|
||||||
|
errorMessage,
|
||||||
|
clearErrorMessage
|
||||||
|
}) => {
|
||||||
|
const [idDistribuidor, setIdDistribuidor] = useState<number | string>('');
|
||||||
|
const [idEmpresa, setIdEmpresa] = useState<number | string>('');
|
||||||
|
const [fechaCorte, setFechaCorte] = useState<string>(todayIso());
|
||||||
|
const [justificacion, setJustificacion] = useState<string>('');
|
||||||
|
|
||||||
|
const [distribuidores, setDistribuidores] = useState<DistribuidorDropdownDto[]>([]);
|
||||||
|
const [empresas, setEmpresas] = useState<EmpresaDropdownDto[]>([]);
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [loadingDropdowns, setLoadingDropdowns] = useState(false);
|
||||||
|
const [localErrors, setLocalErrors] = useState<{ [key: string]: string | null }>({});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const fetchDropdownData = async () => {
|
||||||
|
setLoadingDropdowns(true);
|
||||||
|
try {
|
||||||
|
const [distData, empData] = await Promise.all([
|
||||||
|
distribuidorService.getAllDistribuidoresDropdown(),
|
||||||
|
empresaService.getEmpresasDropdown()
|
||||||
|
]);
|
||||||
|
setDistribuidores(distData);
|
||||||
|
setEmpresas(empData);
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Error al cargar dropdowns', err);
|
||||||
|
setLocalErrors(prev => ({ ...prev, dropdowns: 'Error al cargar distribuidores o empresas.' }));
|
||||||
|
} finally {
|
||||||
|
setLoadingDropdowns(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (open) {
|
||||||
|
fetchDropdownData();
|
||||||
|
setIdDistribuidor(initialIdDistribuidor ?? '');
|
||||||
|
setIdEmpresa(initialIdEmpresa ?? '');
|
||||||
|
setFechaCorte(todayIso());
|
||||||
|
setJustificacion('');
|
||||||
|
setLocalErrors({});
|
||||||
|
clearErrorMessage();
|
||||||
|
}
|
||||||
|
}, [open, initialIdDistribuidor, initialIdEmpresa, clearErrorMessage]);
|
||||||
|
|
||||||
|
const validate = (): boolean => {
|
||||||
|
const errors: { [key: string]: string | null } = {};
|
||||||
|
if (!idDistribuidor) errors.idDistribuidor = 'Seleccione un distribuidor.';
|
||||||
|
if (!idEmpresa) errors.idEmpresa = 'Seleccione una empresa.';
|
||||||
|
if (!fechaCorte) {
|
||||||
|
errors.fechaCorte = 'La fecha de corte es obligatoria.';
|
||||||
|
} else if (new Date(fechaCorte) > new Date(todayIso())) {
|
||||||
|
errors.fechaCorte = 'La fecha de corte no puede ser futura.';
|
||||||
|
}
|
||||||
|
if (justificacion.length > 500) {
|
||||||
|
errors.justificacion = 'La justificación no puede superar los 500 caracteres.';
|
||||||
|
}
|
||||||
|
|
||||||
|
setLocalErrors(errors);
|
||||||
|
return Object.keys(errors).length === 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleInputChange = (fieldName: string) => {
|
||||||
|
if (localErrors[fieldName]) setLocalErrors(prev => ({ ...prev, [fieldName]: null }));
|
||||||
|
if (errorMessage) clearErrorMessage();
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSubmit = async (event: React.FormEvent<HTMLFormElement>) => {
|
||||||
|
event.preventDefault();
|
||||||
|
clearErrorMessage();
|
||||||
|
if (!validate()) return;
|
||||||
|
|
||||||
|
const distSeleccionado = distribuidores.find(d => d.idDistribuidor === Number(idDistribuidor));
|
||||||
|
const empSeleccionada = empresas.find(e => e.idEmpresa === Number(idEmpresa));
|
||||||
|
const confirmMsg = `Estás por cerrar el período hasta ${fechaCorte} para "${distSeleccionado?.nombre ?? ''}" en "${empSeleccionada?.nombre ?? ''}".\n\n` +
|
||||||
|
`Después de cerrar no se podrán registrar movimientos, pagos, notas ni ajustes con fecha menor o igual a esta. ¿Continuar?`;
|
||||||
|
if (!window.confirm(confirmMsg)) return;
|
||||||
|
|
||||||
|
setLoading(true);
|
||||||
|
try {
|
||||||
|
const dto: CrearCierreDto = {
|
||||||
|
idDistribuidor: Number(idDistribuidor),
|
||||||
|
idEmpresa: Number(idEmpresa),
|
||||||
|
fechaCorte,
|
||||||
|
justificacion: justificacion.trim() || null
|
||||||
|
};
|
||||||
|
await onSubmit(dto);
|
||||||
|
onClose();
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Error al crear cierre:', err);
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal open={open} onClose={onClose}>
|
||||||
|
<Box sx={modalStyle}>
|
||||||
|
<Typography variant="h6" component="h2" gutterBottom>
|
||||||
|
Nuevo Cierre de Cuenta Corriente
|
||||||
|
</Typography>
|
||||||
|
<Box component="form" onSubmit={handleSubmit} sx={{ mt: 1 }}>
|
||||||
|
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 1.5 }}>
|
||||||
|
<FormControl fullWidth margin="dense" error={!!localErrors.idDistribuidor} required>
|
||||||
|
<InputLabel id="distribuidor-cierre-label">Distribuidor</InputLabel>
|
||||||
|
<Select
|
||||||
|
labelId="distribuidor-cierre-label"
|
||||||
|
label="Distribuidor"
|
||||||
|
value={idDistribuidor}
|
||||||
|
onChange={(e) => { setIdDistribuidor(e.target.value as number); handleInputChange('idDistribuidor'); }}
|
||||||
|
disabled={loading || loadingDropdowns}
|
||||||
|
>
|
||||||
|
<MenuItem value="" disabled><em>Seleccione</em></MenuItem>
|
||||||
|
{distribuidores.map((d) => (
|
||||||
|
<MenuItem key={d.idDistribuidor} value={d.idDistribuidor}>{d.nombre}</MenuItem>
|
||||||
|
))}
|
||||||
|
</Select>
|
||||||
|
{localErrors.idDistribuidor && <Typography color="error" variant="caption">{localErrors.idDistribuidor}</Typography>}
|
||||||
|
</FormControl>
|
||||||
|
|
||||||
|
<FormControl fullWidth margin="dense" error={!!localErrors.idEmpresa} required>
|
||||||
|
<InputLabel id="empresa-cierre-label">Empresa</InputLabel>
|
||||||
|
<Select
|
||||||
|
labelId="empresa-cierre-label"
|
||||||
|
label="Empresa"
|
||||||
|
value={idEmpresa}
|
||||||
|
onChange={(e) => { setIdEmpresa(e.target.value as number); handleInputChange('idEmpresa'); }}
|
||||||
|
disabled={loading || loadingDropdowns}
|
||||||
|
>
|
||||||
|
<MenuItem value="" disabled><em>Seleccione</em></MenuItem>
|
||||||
|
{empresas.map((e) => (
|
||||||
|
<MenuItem key={e.idEmpresa} value={e.idEmpresa}>{e.nombre}</MenuItem>
|
||||||
|
))}
|
||||||
|
</Select>
|
||||||
|
{localErrors.idEmpresa && <Typography color="error" variant="caption">{localErrors.idEmpresa}</Typography>}
|
||||||
|
</FormControl>
|
||||||
|
|
||||||
|
<TextField
|
||||||
|
label="Fecha de Corte"
|
||||||
|
type="date"
|
||||||
|
value={fechaCorte}
|
||||||
|
required
|
||||||
|
onChange={(e) => { setFechaCorte(e.target.value); handleInputChange('fechaCorte'); }}
|
||||||
|
margin="dense"
|
||||||
|
fullWidth
|
||||||
|
error={!!localErrors.fechaCorte}
|
||||||
|
helperText={localErrors.fechaCorte || 'No puede ser una fecha futura.'}
|
||||||
|
disabled={loading}
|
||||||
|
InputLabelProps={{ shrink: true }}
|
||||||
|
inputProps={{ max: todayIso() }}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Alert severity="info" sx={{ mt: 0.5 }}>
|
||||||
|
La fecha de corte es inclusive: los movimientos con fecha igual a la seleccionada se
|
||||||
|
incluyen en el cálculo del saldo de cierre.
|
||||||
|
</Alert>
|
||||||
|
|
||||||
|
<TextField
|
||||||
|
label="Justificación (opcional)"
|
||||||
|
value={justificacion}
|
||||||
|
onChange={(e) => { setJustificacion(e.target.value); handleInputChange('justificacion'); }}
|
||||||
|
margin="dense"
|
||||||
|
fullWidth
|
||||||
|
multiline
|
||||||
|
rows={3}
|
||||||
|
disabled={loading}
|
||||||
|
error={!!localErrors.justificacion}
|
||||||
|
helperText={localErrors.justificacion || `${justificacion.length}/500 caracteres`}
|
||||||
|
inputProps={{ maxLength: 500 }}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<Alert severity="warning" sx={{ mt: 2 }}>
|
||||||
|
Una vez creado el cierre, no podrán registrarse, modificarse ni eliminarse pagos, notas, ajustes
|
||||||
|
o movimientos cuya fecha de operación sea menor o igual a la fecha de corte.
|
||||||
|
</Alert>
|
||||||
|
|
||||||
|
{errorMessage && <Alert severity="error" sx={{ mt: 2, width: '100%' }}>{errorMessage}</Alert>}
|
||||||
|
{localErrors.dropdowns && <Alert severity="warning" sx={{ mt: 1 }}>{localErrors.dropdowns}</Alert>}
|
||||||
|
|
||||||
|
<Box sx={{ mt: 3, display: 'flex', justifyContent: 'flex-end', gap: 1 }}>
|
||||||
|
<Button onClick={onClose} color="secondary" disabled={loading}>Cancelar</Button>
|
||||||
|
<Button type="submit" variant="contained" disabled={loading || loadingDropdowns}>
|
||||||
|
{loading ? <CircularProgress size={24} /> : 'Crear Cierre'}
|
||||||
|
</Button>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default NuevoCierreModal;
|
||||||
@@ -64,6 +64,7 @@ const PagoDistribuidorFormModal: React.FC<PagoDistribuidorFormModalProps> = ({
|
|||||||
const isEditing = Boolean(initialData);
|
const isEditing = Boolean(initialData);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
// Esta función se encarga de cargar los datos de los dropdowns.
|
||||||
const fetchDropdownData = async () => {
|
const fetchDropdownData = async () => {
|
||||||
setLoadingDropdowns(true);
|
setLoadingDropdowns(true);
|
||||||
try {
|
try {
|
||||||
@@ -94,9 +95,8 @@ const PagoDistribuidorFormModal: React.FC<PagoDistribuidorFormModalProps> = ({
|
|||||||
setDetalle(initialData?.detalle || '');
|
setDetalle(initialData?.detalle || '');
|
||||||
setIdEmpresa(initialData?.idEmpresa || '');
|
setIdEmpresa(initialData?.idEmpresa || '');
|
||||||
setLocalErrors({});
|
setLocalErrors({});
|
||||||
clearErrorMessage();
|
|
||||||
}
|
}
|
||||||
}, [open, initialData, clearErrorMessage]);
|
}, [open, initialData]);
|
||||||
|
|
||||||
const validate = (): boolean => {
|
const validate = (): boolean => {
|
||||||
const errors: { [key: string]: string | null } = {};
|
const errors: { [key: string]: string | null } = {};
|
||||||
@@ -133,7 +133,9 @@ const PagoDistribuidorFormModal: React.FC<PagoDistribuidorFormModalProps> = ({
|
|||||||
idTipoPago: Number(idTipoPago),
|
idTipoPago: Number(idTipoPago),
|
||||||
detalle: detalle || undefined,
|
detalle: detalle || undefined,
|
||||||
};
|
};
|
||||||
|
// << INICIO DE LA CORRECCIÓN >>
|
||||||
await onSubmit(dataToSubmit, initialData.idPago);
|
await onSubmit(dataToSubmit, initialData.idPago);
|
||||||
|
// << FIN DE LA CORRECCIÓN >>
|
||||||
} else {
|
} else {
|
||||||
const dataToSubmit: CreatePagoDistribuidorDto = {
|
const dataToSubmit: CreatePagoDistribuidorDto = {
|
||||||
idDistribuidor: Number(idDistribuidor),
|
idDistribuidor: Number(idDistribuidor),
|
||||||
@@ -147,7 +149,9 @@ const PagoDistribuidorFormModal: React.FC<PagoDistribuidorFormModalProps> = ({
|
|||||||
};
|
};
|
||||||
await onSubmit(dataToSubmit);
|
await onSubmit(dataToSubmit);
|
||||||
}
|
}
|
||||||
|
|
||||||
onClose();
|
onClose();
|
||||||
|
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
console.error("Error en submit de PagoDistribuidorFormModal:", error);
|
console.error("Error en submit de PagoDistribuidorFormModal:", error);
|
||||||
} finally {
|
} finally {
|
||||||
|
|||||||
140
Frontend/src/components/Modals/Contables/ReabrirCierreModal.tsx
Normal file
140
Frontend/src/components/Modals/Contables/ReabrirCierreModal.tsx
Normal file
@@ -0,0 +1,140 @@
|
|||||||
|
import React, { useState, useEffect } from 'react';
|
||||||
|
import {
|
||||||
|
Modal, Box, Typography, TextField, Button, CircularProgress, Alert
|
||||||
|
} from '@mui/material';
|
||||||
|
import WarningAmberIcon from '@mui/icons-material/WarningAmber';
|
||||||
|
import type { ReabrirCierreDto } from '../../../models/dtos/Contables/ReabrirCierreDto';
|
||||||
|
import type { CierreCuentaCorrienteDto } from '../../../models/dtos/Contables/CierreCuentaCorrienteDto';
|
||||||
|
|
||||||
|
const modalStyle = {
|
||||||
|
position: 'absolute' as const,
|
||||||
|
top: '50%',
|
||||||
|
left: '50%',
|
||||||
|
transform: 'translate(-50%, -50%)',
|
||||||
|
width: { xs: '90%', sm: 560 },
|
||||||
|
bgcolor: 'background.paper',
|
||||||
|
border: '2px solid #000',
|
||||||
|
boxShadow: 24,
|
||||||
|
p: 4,
|
||||||
|
maxHeight: '90vh',
|
||||||
|
overflowY: 'auto'
|
||||||
|
};
|
||||||
|
|
||||||
|
const MIN_LEN = 10;
|
||||||
|
const MAX_LEN = 500;
|
||||||
|
|
||||||
|
interface ReabrirCierreModalProps {
|
||||||
|
open: boolean;
|
||||||
|
onClose: () => void;
|
||||||
|
onSubmit: (idCierre: number, data: ReabrirCierreDto) => Promise<CierreCuentaCorrienteDto>;
|
||||||
|
cierre: CierreCuentaCorrienteDto | null;
|
||||||
|
errorMessage?: string | null;
|
||||||
|
clearErrorMessage: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ReabrirCierreModal: React.FC<ReabrirCierreModalProps> = ({
|
||||||
|
open,
|
||||||
|
onClose,
|
||||||
|
onSubmit,
|
||||||
|
cierre,
|
||||||
|
errorMessage,
|
||||||
|
clearErrorMessage
|
||||||
|
}) => {
|
||||||
|
const [justificacion, setJustificacion] = useState<string>('');
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [localError, setLocalError] = useState<string | null>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (open) {
|
||||||
|
setJustificacion('');
|
||||||
|
setLocalError(null);
|
||||||
|
clearErrorMessage();
|
||||||
|
}
|
||||||
|
}, [open, clearErrorMessage]);
|
||||||
|
|
||||||
|
const justifTrimLen = justificacion.trim().length;
|
||||||
|
const submitDisabled = loading || justifTrimLen < MIN_LEN || justifTrimLen > MAX_LEN;
|
||||||
|
|
||||||
|
const handleSubmit = async (event: React.FormEvent<HTMLFormElement>) => {
|
||||||
|
event.preventDefault();
|
||||||
|
if (!cierre) return;
|
||||||
|
if (justifTrimLen < MIN_LEN) {
|
||||||
|
setLocalError(`La justificación debe tener al menos ${MIN_LEN} caracteres.`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (justifTrimLen > MAX_LEN) {
|
||||||
|
setLocalError(`La justificación no puede superar ${MAX_LEN} caracteres.`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setLocalError(null);
|
||||||
|
clearErrorMessage();
|
||||||
|
setLoading(true);
|
||||||
|
try {
|
||||||
|
await onSubmit(cierre.idCierre, { justificacion: justificacion.trim() });
|
||||||
|
onClose();
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Error al reabrir cierre:', err);
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal open={open} onClose={onClose}>
|
||||||
|
<Box sx={modalStyle}>
|
||||||
|
<Typography variant="h6" component="h2" gutterBottom>
|
||||||
|
Reabrir Cierre de Cuenta Corriente
|
||||||
|
</Typography>
|
||||||
|
|
||||||
|
<Alert severity="warning" icon={<WarningAmberIcon />} sx={{ mb: 2 }}>
|
||||||
|
<strong>ATENCIÓN:</strong> estás por reabrir un cierre. Esta acción quedará registrada en auditoría.
|
||||||
|
Solo se puede reabrir el último cierre vigente. Si hay cierres posteriores, primero hay que reabrirlos a ellos.
|
||||||
|
</Alert>
|
||||||
|
|
||||||
|
{cierre && (
|
||||||
|
<Box sx={{ mb: 2, p: 1.5, bgcolor: 'grey.100', borderRadius: 1 }}>
|
||||||
|
<Typography variant="body2"><strong>Distribuidor:</strong> {cierre.nombreDistribuidor}</Typography>
|
||||||
|
<Typography variant="body2"><strong>Empresa:</strong> {cierre.nombreEmpresa}</Typography>
|
||||||
|
<Typography variant="body2"><strong>Fecha de Corte:</strong> {cierre.fechaCorte}</Typography>
|
||||||
|
<Typography variant="body2">
|
||||||
|
<strong>Saldo del Cierre:</strong>{' '}
|
||||||
|
{cierre.saldoCierre.toLocaleString('es-AR', { style: 'currency', currency: 'ARS' })}
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<Box component="form" onSubmit={handleSubmit}>
|
||||||
|
<TextField
|
||||||
|
label="Justificación de la reapertura"
|
||||||
|
value={justificacion}
|
||||||
|
onChange={(e) => { setJustificacion(e.target.value); if (localError) setLocalError(null); }}
|
||||||
|
required
|
||||||
|
multiline
|
||||||
|
rows={4}
|
||||||
|
fullWidth
|
||||||
|
margin="dense"
|
||||||
|
error={!!localError}
|
||||||
|
helperText={
|
||||||
|
localError ||
|
||||||
|
`Mínimo ${MIN_LEN} caracteres — ${justifTrimLen}/${MAX_LEN}`
|
||||||
|
}
|
||||||
|
inputProps={{ maxLength: MAX_LEN }}
|
||||||
|
disabled={loading}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{errorMessage && <Alert severity="error" sx={{ mt: 2 }}>{errorMessage}</Alert>}
|
||||||
|
|
||||||
|
<Box sx={{ mt: 3, display: 'flex', justifyContent: 'flex-end', gap: 1 }}>
|
||||||
|
<Button onClick={onClose} color="secondary" disabled={loading}>Cancelar</Button>
|
||||||
|
<Button type="submit" variant="contained" color="warning" disabled={submitDisabled}>
|
||||||
|
{loading ? <CircularProgress size={24} /> : 'Confirmar Reapertura'}
|
||||||
|
</Button>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ReabrirCierreModal;
|
||||||
@@ -86,9 +86,8 @@ const EntradaSalidaDistFormModal: React.FC<EntradaSalidaDistFormModalProps> = ({
|
|||||||
setRemito(initialData?.remito?.toString() || '');
|
setRemito(initialData?.remito?.toString() || '');
|
||||||
setObservacion(initialData?.observacion || '');
|
setObservacion(initialData?.observacion || '');
|
||||||
setLocalErrors({});
|
setLocalErrors({});
|
||||||
clearErrorMessage();
|
|
||||||
}
|
}
|
||||||
}, [open, initialData, clearErrorMessage]);
|
}, [open, initialData]);
|
||||||
|
|
||||||
const validate = (): boolean => {
|
const validate = (): boolean => {
|
||||||
const errors: { [key: string]: string | null } = {};
|
const errors: { [key: string]: string | null } = {};
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ import {
|
|||||||
import type { StockBobinaDto } from '../../../models/dtos/Impresion/StockBobinaDto';
|
import type { StockBobinaDto } from '../../../models/dtos/Impresion/StockBobinaDto';
|
||||||
import type { CambiarEstadoBobinaDto } from '../../../models/dtos/Impresion/CambiarEstadoBobinaDto';
|
import type { CambiarEstadoBobinaDto } from '../../../models/dtos/Impresion/CambiarEstadoBobinaDto';
|
||||||
import type { EstadoBobinaDto } from '../../../models/dtos/Impresion/EstadoBobinaDto';
|
import type { EstadoBobinaDto } from '../../../models/dtos/Impresion/EstadoBobinaDto';
|
||||||
// --- CAMBIO: Importar PublicacionDropdownDto ---
|
|
||||||
import type { PublicacionDropdownDto } from '../../../models/dtos/Distribucion/PublicacionDropdownDto';
|
import type { PublicacionDropdownDto } from '../../../models/dtos/Distribucion/PublicacionDropdownDto';
|
||||||
import type { PubliSeccionDto } from '../../../models/dtos/Distribucion/PubliSeccionDto';
|
import type { PubliSeccionDto } from '../../../models/dtos/Distribucion/PubliSeccionDto';
|
||||||
import estadoBobinaService from '../../../services/Impresion/estadoBobinaService';
|
import estadoBobinaService from '../../../services/Impresion/estadoBobinaService';
|
||||||
@@ -33,272 +32,288 @@ const ID_ESTADO_EN_USO = 2; // Usaremos este consistentemente
|
|||||||
const ID_ESTADO_DANADA = 3;
|
const ID_ESTADO_DANADA = 3;
|
||||||
|
|
||||||
interface StockBobinaCambioEstadoModalProps {
|
interface StockBobinaCambioEstadoModalProps {
|
||||||
open: boolean;
|
open: boolean;
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
onSubmit: (idBobina: number, data: CambiarEstadoBobinaDto) => Promise<void>;
|
onSubmit: (idBobina: number, data: CambiarEstadoBobinaDto) => Promise<void>;
|
||||||
bobinaActual: StockBobinaDto | null;
|
bobinaActual: StockBobinaDto | null;
|
||||||
errorMessage?: string | null;
|
errorMessage?: string | null;
|
||||||
clearErrorMessage: () => void;
|
clearErrorMessage: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const StockBobinaCambioEstadoModal: React.FC<StockBobinaCambioEstadoModalProps> = ({
|
const StockBobinaCambioEstadoModal: React.FC<StockBobinaCambioEstadoModalProps> = ({
|
||||||
open,
|
open,
|
||||||
onClose,
|
onClose,
|
||||||
onSubmit,
|
onSubmit,
|
||||||
bobinaActual,
|
bobinaActual,
|
||||||
errorMessage,
|
errorMessage,
|
||||||
clearErrorMessage
|
clearErrorMessage
|
||||||
}) => {
|
}) => {
|
||||||
const [nuevoEstadoId, setNuevoEstadoId] = useState<number | string>('');
|
const [nuevoEstadoId, setNuevoEstadoId] = useState<number | string>('');
|
||||||
const [idPublicacion, setIdPublicacion] = useState<number | string>('');
|
const [idPublicacion, setIdPublicacion] = useState<number | string>('');
|
||||||
const [idSeccion, setIdSeccion] = useState<number | string>('');
|
const [idSeccion, setIdSeccion] = useState<number | string>('');
|
||||||
const [obs, setObs] = useState('');
|
const [obs, setObs] = useState('');
|
||||||
const [fechaCambioEstado, setFechaCambioEstado] = useState('');
|
const [fechaCambioEstado, setFechaCambioEstado] = useState('');
|
||||||
|
|
||||||
const [estadosDisponibles, setEstadosDisponibles] = useState<EstadoBobinaDto[]>([]);
|
const [estadosDisponibles, setEstadosDisponibles] = useState<EstadoBobinaDto[]>([]);
|
||||||
// --- CAMBIO: Usar PublicacionDropdownDto para el estado ---
|
const [publicacionesDisponibles, setPublicacionesDisponibles] = useState<PublicacionDropdownDto[]>([]);
|
||||||
const [publicacionesDisponibles, setPublicacionesDisponibles] = useState<PublicacionDropdownDto[]>([]);
|
const [seccionesDisponibles, setSeccionesDisponibles] = useState<PubliSeccionDto[]>([]);
|
||||||
const [seccionesDisponibles, setSeccionesDisponibles] = useState<PubliSeccionDto[]>([]);
|
|
||||||
|
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [loadingDropdowns, setLoadingDropdowns] = useState(false);
|
const [loadingDropdowns, setLoadingDropdowns] = useState(false);
|
||||||
const [localErrors, setLocalErrors] = useState<{ [key: string]: string | null }>({});
|
const [localErrors, setLocalErrors] = useState<{ [key: string]: string | null }>({});
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const fetchDropdownData = async () => {
|
const fetchDropdownData = async () => {
|
||||||
if (!bobinaActual) return;
|
if (!bobinaActual) return;
|
||||||
setLoadingDropdowns(true);
|
|
||||||
try {
|
|
||||||
const todosLosEstados = await estadoBobinaService.getAllEstadosBobina();
|
|
||||||
let estadosFiltrados: EstadoBobinaDto[];
|
|
||||||
|
|
||||||
if (bobinaActual.idEstadoBobina === ID_ESTADO_DANADA) {
|
|
||||||
estadosFiltrados = todosLosEstados.filter(e => e.idEstadoBobina === ID_ESTADO_DISPONIBLE);
|
|
||||||
} else if (bobinaActual.idEstadoBobina === ID_ESTADO_EN_USO) {
|
|
||||||
estadosFiltrados = todosLosEstados.filter(e => e.idEstadoBobina === ID_ESTADO_DANADA);
|
|
||||||
} else if (bobinaActual.idEstadoBobina === ID_ESTADO_DISPONIBLE) {
|
|
||||||
// --- CAMBIO: Usar ID_ESTADO_EN_USO ---
|
|
||||||
estadosFiltrados = todosLosEstados.filter(
|
|
||||||
e => e.idEstadoBobina === ID_ESTADO_EN_USO || e.idEstadoBobina === ID_ESTADO_DANADA
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
estadosFiltrados = todosLosEstados.filter(e => e.idEstadoBobina !== bobinaActual.idEstadoBobina);
|
|
||||||
}
|
|
||||||
|
|
||||||
setEstadosDisponibles(estadosFiltrados);
|
|
||||||
|
|
||||||
const sePuedePonerEnUso = estadosFiltrados.some(e => e.idEstadoBobina === ID_ESTADO_EN_USO);
|
|
||||||
|
|
||||||
if (sePuedePonerEnUso) {
|
|
||||||
// --- CAMBIO: La data es PublicacionDropdownDto[] ---
|
|
||||||
const publicacionesData: PublicacionDropdownDto[] = await publicacionService.getPublicacionesForDropdown(true);
|
|
||||||
setPublicacionesDisponibles(publicacionesData);
|
|
||||||
} else {
|
|
||||||
setPublicacionesDisponibles([]);
|
|
||||||
setIdPublicacion('');
|
|
||||||
setIdSeccion('');
|
|
||||||
}
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Error al cargar datos para dropdowns (Cambio Estado Bobina)", error);
|
|
||||||
setLocalErrors(prev => ({...prev, dropdowns: 'Error al cargar datos necesarios.'}));
|
|
||||||
} finally {
|
|
||||||
setLoadingDropdowns(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
if (open && bobinaActual) {
|
|
||||||
fetchDropdownData();
|
|
||||||
setNuevoEstadoId('');
|
|
||||||
// Pre-cargar basado en si la bobina actual está "En Uso"
|
|
||||||
if (bobinaActual.idEstadoBobina === ID_ESTADO_EN_USO) {
|
|
||||||
setIdPublicacion(bobinaActual.idPublicacion?.toString() || '');
|
|
||||||
// Solo pre-cargar sección si la publicación también estaba pre-cargada
|
|
||||||
if (bobinaActual.idPublicacion) {
|
|
||||||
setIdSeccion(bobinaActual.idSeccion?.toString() || '');
|
|
||||||
} else {
|
|
||||||
setIdSeccion('');
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
setIdPublicacion('');
|
|
||||||
setIdSeccion('');
|
|
||||||
}
|
|
||||||
setObs(bobinaActual.obs || '');
|
|
||||||
setFechaCambioEstado(new Date().toISOString().split('T')[0]);
|
|
||||||
setLocalErrors({});
|
|
||||||
clearErrorMessage();
|
|
||||||
}
|
|
||||||
}, [open, bobinaActual, clearErrorMessage]);
|
|
||||||
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const fetchSecciones = async () => {
|
|
||||||
if (Number(nuevoEstadoId) === ID_ESTADO_EN_USO && idPublicacion) {
|
|
||||||
setLoadingDropdowns(true);
|
setLoadingDropdowns(true);
|
||||||
try {
|
try {
|
||||||
const data = await publiSeccionService.getSeccionesPorPublicacion(Number(idPublicacion), true);
|
const todosLosEstados = await estadoBobinaService.getAllEstadosBobina();
|
||||||
setSeccionesDisponibles(data);
|
let estadosFiltrados: EstadoBobinaDto[];
|
||||||
|
|
||||||
|
if (bobinaActual.idEstadoBobina === ID_ESTADO_DANADA) {
|
||||||
|
estadosFiltrados = todosLosEstados.filter(e => e.idEstadoBobina === ID_ESTADO_DISPONIBLE);
|
||||||
|
} else if (bobinaActual.idEstadoBobina === ID_ESTADO_EN_USO) {
|
||||||
|
estadosFiltrados = todosLosEstados.filter(e => e.idEstadoBobina === ID_ESTADO_DANADA);
|
||||||
|
} else if (bobinaActual.idEstadoBobina === ID_ESTADO_DISPONIBLE) {
|
||||||
|
estadosFiltrados = todosLosEstados.filter(
|
||||||
|
e => e.idEstadoBobina === ID_ESTADO_EN_USO || e.idEstadoBobina === ID_ESTADO_DANADA
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
estadosFiltrados = todosLosEstados.filter(e => e.idEstadoBobina !== bobinaActual.idEstadoBobina);
|
||||||
|
}
|
||||||
|
|
||||||
|
setEstadosDisponibles(estadosFiltrados);
|
||||||
|
|
||||||
|
const sePuedePonerEnUso = estadosFiltrados.some(e => e.idEstadoBobina === ID_ESTADO_EN_USO);
|
||||||
|
|
||||||
|
if (sePuedePonerEnUso) {
|
||||||
|
const publicacionesData: PublicacionDropdownDto[] = await publicacionService.getPublicacionesForDropdown(true);
|
||||||
|
setPublicacionesDisponibles(publicacionesData);
|
||||||
|
} else {
|
||||||
|
setPublicacionesDisponibles([]);
|
||||||
|
setIdPublicacion('');
|
||||||
|
setIdSeccion('');
|
||||||
|
}
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error al cargar secciones:", error);
|
console.error("Error al cargar datos para dropdowns (Cambio Estado Bobina)", error);
|
||||||
setLocalErrors(prev => ({ ...prev, secciones: 'Error al cargar secciones.'}));
|
setLocalErrors(prev => ({ ...prev, dropdowns: 'Error al cargar datos necesarios.' }));
|
||||||
setSeccionesDisponibles([]);
|
|
||||||
} finally {
|
} finally {
|
||||||
setLoadingDropdowns(false);
|
setLoadingDropdowns(false);
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (open && bobinaActual) {
|
||||||
|
fetchDropdownData();
|
||||||
|
setNuevoEstadoId('');
|
||||||
|
// Pre-cargar basado en si la bobina actual está "En Uso"
|
||||||
|
if (bobinaActual.idEstadoBobina === ID_ESTADO_EN_USO) {
|
||||||
|
setIdPublicacion(bobinaActual.idPublicacion?.toString() || '');
|
||||||
|
// Solo pre-cargar sección si la publicación también estaba pre-cargada
|
||||||
|
if (bobinaActual.idPublicacion) {
|
||||||
|
setIdSeccion(bobinaActual.idSeccion?.toString() || '');
|
||||||
|
} else {
|
||||||
|
setIdSeccion('');
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
setIdPublicacion('');
|
||||||
|
setIdSeccion('');
|
||||||
|
}
|
||||||
|
setObs(bobinaActual.obs || '');
|
||||||
|
setFechaCambioEstado(new Date().toISOString().split('T')[0]);
|
||||||
|
setLocalErrors({});
|
||||||
|
clearErrorMessage();
|
||||||
|
}
|
||||||
|
}, [open, bobinaActual, clearErrorMessage]);
|
||||||
|
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const fetchSecciones = async () => {
|
||||||
|
if (Number(nuevoEstadoId) === ID_ESTADO_EN_USO && idPublicacion) {
|
||||||
|
setLoadingDropdowns(true);
|
||||||
|
try {
|
||||||
|
const data = await publiSeccionService.getSeccionesPorPublicacion(Number(idPublicacion), true);
|
||||||
|
setSeccionesDisponibles(data);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error al cargar secciones:", error);
|
||||||
|
setLocalErrors(prev => ({ ...prev, secciones: 'Error al cargar secciones.' }));
|
||||||
|
setSeccionesDisponibles([]);
|
||||||
|
} finally {
|
||||||
|
setLoadingDropdowns(false);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
setSeccionesDisponibles([]);
|
||||||
|
// No es necesario setIdSeccion('') aquí si el useEffect de nuevoEstadoId ya lo hace.
|
||||||
|
}
|
||||||
|
};
|
||||||
|
if (idPublicacion && Number(nuevoEstadoId) === ID_ESTADO_EN_USO) { // Solo fetchear si hay idPublicacion
|
||||||
|
fetchSecciones();
|
||||||
} else {
|
} else {
|
||||||
setSeccionesDisponibles([]);
|
setSeccionesDisponibles([]); // Limpiar si no se cumplen condiciones
|
||||||
// No es necesario setIdSeccion('') aquí si el useEffect de nuevoEstadoId ya lo hace.
|
}
|
||||||
|
}, [nuevoEstadoId, idPublicacion]);
|
||||||
|
|
||||||
|
|
||||||
|
// Efecto para limpiar publicacion/seccion si el nuevo estado no es "En Uso"
|
||||||
|
useEffect(() => {
|
||||||
|
if (Number(nuevoEstadoId) !== ID_ESTADO_EN_USO) {
|
||||||
|
setIdPublicacion('');
|
||||||
|
setIdSeccion('');
|
||||||
|
}
|
||||||
|
}, [nuevoEstadoId]);
|
||||||
|
|
||||||
|
|
||||||
|
const validate = (): boolean => {
|
||||||
|
const errors: { [key: string]: string | null } = {};
|
||||||
|
if (!nuevoEstadoId) errors.nuevoEstadoId = 'Seleccione un nuevo estado.';
|
||||||
|
if (!fechaCambioEstado.trim()) {
|
||||||
|
errors.fechaCambioEstado = 'La fecha es obligatoria.';
|
||||||
|
} else if (!/^\d{4}-\d{2}-\d{2}$/.test(fechaCambioEstado)) {
|
||||||
|
errors.fechaCambioEstado = 'Formato de fecha inválido.';
|
||||||
|
} else if (bobinaActual) {
|
||||||
|
const fechaRemitoSimple = bobinaActual.fechaRemito.split('T')[0];
|
||||||
|
if (fechaCambioEstado < fechaRemitoSimple) {
|
||||||
|
errors.fechaCambioEstado = `La fecha no puede ser anterior al ingreso (${fechaRemitoSimple}).`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Number(nuevoEstadoId) === ID_ESTADO_EN_USO) {
|
||||||
|
if (!idPublicacion) errors.idPublicacion = 'Seleccione una publicación.';
|
||||||
|
if (!idSeccion) errors.idSeccion = 'Seleccione una sección.';
|
||||||
|
}
|
||||||
|
setLocalErrors(errors);
|
||||||
|
return Object.keys(errors).length === 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleInputChange = (fieldName: string) => {
|
||||||
|
if (localErrors[fieldName]) setLocalErrors(prev => ({ ...prev, [fieldName]: null }));
|
||||||
|
if (errorMessage) clearErrorMessage();
|
||||||
|
// La lógica de limpieza de pub/secc se movió a un useEffect dedicado a nuevoEstadoId
|
||||||
|
// y el de sección a un useEffect de idPublicacion
|
||||||
|
if (fieldName === 'idPublicacion') { // Si cambia la publicación, resetear seccion
|
||||||
|
setIdSeccion('');
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
if (idPublicacion && Number(nuevoEstadoId) === ID_ESTADO_EN_USO) { // Solo fetchear si hay idPublicacion
|
|
||||||
fetchSecciones();
|
|
||||||
} else {
|
|
||||||
setSeccionesDisponibles([]); // Limpiar si no se cumplen condiciones
|
|
||||||
}
|
|
||||||
}, [nuevoEstadoId, idPublicacion]);
|
|
||||||
|
|
||||||
|
const handleSubmit = async (event: React.FormEvent<HTMLFormElement>) => {
|
||||||
|
event.preventDefault();
|
||||||
|
clearErrorMessage();
|
||||||
|
if (!validate() || !bobinaActual) return;
|
||||||
|
|
||||||
// Efecto para limpiar publicacion/seccion si el nuevo estado no es "En Uso"
|
setLoading(true);
|
||||||
useEffect(() => {
|
try {
|
||||||
if (Number(nuevoEstadoId) !== ID_ESTADO_EN_USO) {
|
const esEnUso = Number(nuevoEstadoId) === ID_ESTADO_EN_USO;
|
||||||
setIdPublicacion('');
|
const dataToSubmit: CambiarEstadoBobinaDto = {
|
||||||
setIdSeccion('');
|
nuevoEstadoId: Number(nuevoEstadoId),
|
||||||
}
|
idPublicacion: esEnUso && idPublicacion ? Number(idPublicacion) : null,
|
||||||
}, [nuevoEstadoId]);
|
idSeccion: esEnUso && idPublicacion && idSeccion ? Number(idSeccion) : null,
|
||||||
|
obs: obs.trim() || null,
|
||||||
|
fechaCambioEstado,
|
||||||
|
};
|
||||||
|
await onSubmit(bobinaActual.idBobina, dataToSubmit);
|
||||||
|
onClose();
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error("Error en submit de StockBobinaCambioEstadoModal:", error);
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!bobinaActual) return null;
|
||||||
|
|
||||||
const validate = (): boolean => {
|
return (
|
||||||
const errors: { [key: string]: string | null } = {};
|
<Modal open={open} onClose={onClose}>
|
||||||
if (!nuevoEstadoId) errors.nuevoEstadoId = 'Seleccione un nuevo estado.';
|
<Box sx={modalStyle}>
|
||||||
if (!fechaCambioEstado.trim()) errors.fechaCambioEstado = 'La fecha es obligatoria.';
|
<Typography variant="h6" component="h2" gutterBottom>
|
||||||
else if (!/^\d{4}-\d{2}-\d{2}$/.test(fechaCambioEstado)) errors.fechaCambioEstado = 'Formato de fecha inválido.';
|
Cambiar Estado de Bobina: {bobinaActual.nroBobina}
|
||||||
|
</Typography>
|
||||||
if (Number(nuevoEstadoId) === ID_ESTADO_EN_USO) {
|
<Typography variant="body2" gutterBottom>
|
||||||
if (!idPublicacion) errors.idPublicacion = 'Seleccione una publicación.';
|
Estado Actual: <strong>{bobinaActual.nombreEstadoBobina}</strong>
|
||||||
if (!idSeccion) errors.idSeccion = 'Seleccione una sección.';
|
</Typography>
|
||||||
}
|
<Box component="form" onSubmit={handleSubmit} sx={{ mt: 1 }}>
|
||||||
setLocalErrors(errors);
|
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 1.5 }}>
|
||||||
return Object.keys(errors).length === 0;
|
<FormControl fullWidth margin="dense" error={!!localErrors.nuevoEstadoId} required>
|
||||||
};
|
<InputLabel id="nuevo-estado-select-label">Nuevo Estado</InputLabel>
|
||||||
|
<Select
|
||||||
const handleInputChange = (fieldName: string) => {
|
labelId="nuevo-estado-select-label"
|
||||||
if (localErrors[fieldName]) setLocalErrors(prev => ({ ...prev, [fieldName]: null }));
|
label="Nuevo Estado"
|
||||||
if (errorMessage) clearErrorMessage();
|
value={nuevoEstadoId}
|
||||||
// La lógica de limpieza de pub/secc se movió a un useEffect dedicado a nuevoEstadoId
|
onChange={(e) => {
|
||||||
// y el de sección a un useEffect de idPublicacion
|
setNuevoEstadoId(e.target.value as number | string);
|
||||||
if (fieldName === 'idPublicacion') { // Si cambia la publicación, resetear seccion
|
handleInputChange('nuevoEstadoId');
|
||||||
setIdSeccion('');
|
}}
|
||||||
}
|
disabled={loading || loadingDropdowns || estadosDisponibles.length === 0}
|
||||||
};
|
|
||||||
|
|
||||||
const handleSubmit = async (event: React.FormEvent<HTMLFormElement>) => {
|
|
||||||
event.preventDefault();
|
|
||||||
clearErrorMessage();
|
|
||||||
if (!validate() || !bobinaActual) return;
|
|
||||||
|
|
||||||
setLoading(true);
|
|
||||||
try {
|
|
||||||
const esEnUso = Number(nuevoEstadoId) === ID_ESTADO_EN_USO;
|
|
||||||
const dataToSubmit: CambiarEstadoBobinaDto = {
|
|
||||||
nuevoEstadoId: Number(nuevoEstadoId),
|
|
||||||
idPublicacion: esEnUso && idPublicacion ? Number(idPublicacion) : null,
|
|
||||||
idSeccion: esEnUso && idPublicacion && idSeccion ? Number(idSeccion) : null,
|
|
||||||
obs: obs.trim() || null,
|
|
||||||
fechaCambioEstado,
|
|
||||||
};
|
|
||||||
await onSubmit(bobinaActual.idBobina, dataToSubmit);
|
|
||||||
onClose();
|
|
||||||
} catch (error: any) {
|
|
||||||
console.error("Error en submit de StockBobinaCambioEstadoModal:", error);
|
|
||||||
} finally {
|
|
||||||
setLoading(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
if (!bobinaActual) return null;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Modal open={open} onClose={onClose}>
|
|
||||||
<Box sx={modalStyle}>
|
|
||||||
<Typography variant="h6" component="h2" gutterBottom>
|
|
||||||
Cambiar Estado de Bobina: {bobinaActual.nroBobina}
|
|
||||||
</Typography>
|
|
||||||
<Typography variant="body2" gutterBottom>
|
|
||||||
Estado Actual: <strong>{bobinaActual.nombreEstadoBobina}</strong>
|
|
||||||
</Typography>
|
|
||||||
<Box component="form" onSubmit={handleSubmit} sx={{ mt: 1 }}>
|
|
||||||
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 1.5 }}>
|
|
||||||
<FormControl fullWidth margin="dense" error={!!localErrors.nuevoEstadoId} required>
|
|
||||||
<InputLabel id="nuevo-estado-select-label">Nuevo Estado</InputLabel>
|
|
||||||
<Select
|
|
||||||
labelId="nuevo-estado-select-label"
|
|
||||||
label="Nuevo Estado"
|
|
||||||
value={nuevoEstadoId}
|
|
||||||
onChange={(e) => {
|
|
||||||
setNuevoEstadoId(e.target.value as number | string);
|
|
||||||
handleInputChange('nuevoEstadoId');
|
|
||||||
}}
|
|
||||||
disabled={loading || loadingDropdowns || estadosDisponibles.length === 0}
|
|
||||||
>
|
|
||||||
<MenuItem value="" disabled><em>Seleccione un estado</em></MenuItem>
|
|
||||||
{estadosDisponibles.map((e) => (<MenuItem key={e.idEstadoBobina} value={e.idEstadoBobina}>{e.denominacion}</MenuItem>))}
|
|
||||||
</Select>
|
|
||||||
{localErrors.nuevoEstadoId && <Typography color="error" variant="caption">{localErrors.nuevoEstadoId}</Typography>}
|
|
||||||
</FormControl>
|
|
||||||
|
|
||||||
{Number(nuevoEstadoId) === ID_ESTADO_EN_USO && (
|
|
||||||
<>
|
|
||||||
<FormControl fullWidth margin="dense" error={!!localErrors.idPublicacion} required>
|
|
||||||
<InputLabel id="publicacion-estado-select-label">Publicación</InputLabel>
|
|
||||||
<Select labelId="publicacion-estado-select-label" label="Publicación" value={idPublicacion}
|
|
||||||
onChange={(e) => {setIdPublicacion(e.target.value as number); handleInputChange('idPublicacion');}}
|
|
||||||
disabled={loading || loadingDropdowns || publicacionesDisponibles.length === 0}
|
|
||||||
>
|
>
|
||||||
<MenuItem value="" disabled><em>Seleccione publicación</em></MenuItem>
|
<MenuItem value="" disabled><em>Seleccione un estado</em></MenuItem>
|
||||||
{publicacionesDisponibles.map((p) => (<MenuItem key={p.idPublicacion} value={p.idPublicacion}>{p.nombre}</MenuItem>))}
|
{estadosDisponibles.map((e) => (<MenuItem key={e.idEstadoBobina} value={e.idEstadoBobina}>{e.denominacion}</MenuItem>))}
|
||||||
</Select>
|
</Select>
|
||||||
{localErrors.idPublicacion && <Typography color="error" variant="caption">{localErrors.idPublicacion}</Typography>}
|
{localErrors.nuevoEstadoId && <Typography color="error" variant="caption">{localErrors.nuevoEstadoId}</Typography>}
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<FormControl fullWidth margin="dense" error={!!localErrors.idSeccion} required>
|
|
||||||
<InputLabel id="seccion-estado-select-label">Sección</InputLabel>
|
|
||||||
<Select labelId="seccion-estado-select-label" label="Sección" value={idSeccion}
|
|
||||||
onChange={(e) => {setIdSeccion(e.target.value as number); handleInputChange('idSeccion');}}
|
|
||||||
disabled={loading || loadingDropdowns || !idPublicacion || seccionesDisponibles.length === 0}
|
|
||||||
>
|
|
||||||
<MenuItem value="" disabled><em>{idPublicacion ? (seccionesDisponibles.length > 0 ? 'Seleccione sección' : 'No hay secciones para esta pub.') : 'Seleccione publicación primero'}</em></MenuItem>
|
|
||||||
{seccionesDisponibles.map((s) => (<MenuItem key={s.idSeccion} value={s.idSeccion}>{s.nombre}</MenuItem>))}
|
|
||||||
</Select>
|
|
||||||
{localErrors.idSeccion && <Typography color="error" variant="caption">{localErrors.idSeccion}</Typography>}
|
|
||||||
{localErrors.secciones && <Alert severity="warning" sx={{mt:0.5}}>{localErrors.secciones}</Alert>}
|
|
||||||
</FormControl>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<TextField label="Fecha Cambio de Estado" type="date" value={fechaCambioEstado} required
|
{Number(nuevoEstadoId) === ID_ESTADO_EN_USO && (
|
||||||
onChange={(e) => {setFechaCambioEstado(e.target.value); handleInputChange('fechaCambioEstado');}}
|
<>
|
||||||
margin="dense" fullWidth error={!!localErrors.fechaCambioEstado} helperText={localErrors.fechaCambioEstado || ''}
|
<FormControl fullWidth margin="dense" error={!!localErrors.idPublicacion} required>
|
||||||
disabled={loading} InputLabelProps={{ shrink: true }}
|
<InputLabel id="publicacion-estado-select-label">Publicación</InputLabel>
|
||||||
/>
|
<Select labelId="publicacion-estado-select-label" label="Publicación" value={idPublicacion}
|
||||||
<TextField label="Observaciones (Opcional)" value={obs}
|
onChange={(e) => { setIdPublicacion(e.target.value as number); handleInputChange('idPublicacion'); }}
|
||||||
onChange={(e) => setObs(e.target.value)}
|
disabled={loading || loadingDropdowns || publicacionesDisponibles.length === 0}
|
||||||
margin="dense" fullWidth multiline rows={3} disabled={loading}
|
>
|
||||||
/>
|
<MenuItem value="" disabled><em>Seleccione publicación</em></MenuItem>
|
||||||
|
{publicacionesDisponibles.map((p) => (<MenuItem key={p.idPublicacion} value={p.idPublicacion}>{p.nombre}</MenuItem>))}
|
||||||
|
</Select>
|
||||||
|
{localErrors.idPublicacion && <Typography color="error" variant="caption">{localErrors.idPublicacion}</Typography>}
|
||||||
|
</FormControl>
|
||||||
|
<FormControl fullWidth margin="dense" error={!!localErrors.idSeccion} required>
|
||||||
|
<InputLabel id="seccion-estado-select-label">Sección</InputLabel>
|
||||||
|
<Select labelId="seccion-estado-select-label" label="Sección" value={idSeccion}
|
||||||
|
onChange={(e) => { setIdSeccion(e.target.value as number); handleInputChange('idSeccion'); }}
|
||||||
|
disabled={loading || loadingDropdowns || !idPublicacion || seccionesDisponibles.length === 0}
|
||||||
|
>
|
||||||
|
<MenuItem value="" disabled><em>{idPublicacion ? (seccionesDisponibles.length > 0 ? 'Seleccione sección' : 'No hay secciones para esta pub.') : 'Seleccione publicación primero'}</em></MenuItem>
|
||||||
|
{seccionesDisponibles.map((s) => (<MenuItem key={s.idSeccion} value={s.idSeccion}>{s.nombre}</MenuItem>))}
|
||||||
|
</Select>
|
||||||
|
{localErrors.idSeccion && <Typography color="error" variant="caption">{localErrors.idSeccion}</Typography>}
|
||||||
|
{localErrors.secciones && <Alert severity="warning" sx={{ mt: 0.5 }}>{localErrors.secciones}</Alert>}
|
||||||
|
</FormControl>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<TextField
|
||||||
|
label="Fecha Cambio de Estado"
|
||||||
|
type="date"
|
||||||
|
value={fechaCambioEstado}
|
||||||
|
required
|
||||||
|
onChange={(e) => { setFechaCambioEstado(e.target.value); handleInputChange('fechaCambioEstado'); }}
|
||||||
|
margin="dense"
|
||||||
|
fullWidth
|
||||||
|
error={!!localErrors.fechaCambioEstado}
|
||||||
|
helperText={localErrors.fechaCambioEstado || ''}
|
||||||
|
disabled={loading}
|
||||||
|
InputLabelProps={{ shrink: true }}
|
||||||
|
inputProps={{
|
||||||
|
min: bobinaActual?.fechaRemito.split('T')[0]
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<TextField label="Observaciones (Opcional)" value={obs}
|
||||||
|
onChange={(e) => setObs(e.target.value)}
|
||||||
|
margin="dense" fullWidth multiline rows={3} disabled={loading}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
{errorMessage && <Alert severity="error" sx={{ mt: 2, width: '100%' }}>{errorMessage}</Alert>}
|
||||||
|
{localErrors.dropdowns && <Alert severity="warning" sx={{ mt: 1 }}>{localErrors.dropdowns}</Alert>}
|
||||||
|
|
||||||
|
<Box sx={{ mt: 3, display: 'flex', justifyContent: 'flex-end', gap: 1 }}>
|
||||||
|
<Button onClick={onClose} color="secondary" disabled={loading}>Cancelar</Button>
|
||||||
|
<Button type="submit" variant="contained"
|
||||||
|
disabled={loading || loadingDropdowns || (estadosDisponibles.length === 0 && bobinaActual.idEstadoBobina === ID_ESTADO_EN_USO)}>
|
||||||
|
{loading ? <CircularProgress size={24} /> : 'Guardar Cambio de Estado'}
|
||||||
|
</Button>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
|
</Modal>
|
||||||
{errorMessage && <Alert severity="error" sx={{ mt: 2, width: '100%' }}>{errorMessage}</Alert>}
|
);
|
||||||
{localErrors.dropdowns && <Alert severity="warning" sx={{ mt: 1 }}>{localErrors.dropdowns}</Alert>}
|
|
||||||
|
|
||||||
<Box sx={{ mt: 3, display: 'flex', justifyContent: 'flex-end', gap: 1 }}>
|
|
||||||
<Button onClick={onClose} color="secondary" disabled={loading}>Cancelar</Button>
|
|
||||||
<Button type="submit" variant="contained"
|
|
||||||
disabled={loading || loadingDropdowns || (estadosDisponibles.length === 0 && bobinaActual.idEstadoBobina === ID_ESTADO_EN_USO) }>
|
|
||||||
{loading ? <CircularProgress size={24} /> : 'Guardar Cambio de Estado'}
|
|
||||||
</Button>
|
|
||||||
</Box>
|
|
||||||
</Box>
|
|
||||||
</Box>
|
|
||||||
</Modal>
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export default StockBobinaCambioEstadoModal;
|
export default StockBobinaCambioEstadoModal;
|
||||||
@@ -0,0 +1,89 @@
|
|||||||
|
import React, { useState, useEffect } from 'react';
|
||||||
|
import { Modal, Box, Typography, TextField, Button, CircularProgress, Alert } from '@mui/material';
|
||||||
|
import type { StockBobinaDto } from '../../../models/dtos/Impresion/StockBobinaDto';
|
||||||
|
import type { UpdateFechaRemitoLoteDto } from '../../../models/dtos/Impresion/UpdateFechaRemitoLoteDto';
|
||||||
|
|
||||||
|
const modalStyle = {
|
||||||
|
position: 'absolute' as 'absolute',
|
||||||
|
top: '50%', left: '50%',
|
||||||
|
transform: 'translate(-50%, -50%)',
|
||||||
|
width: { xs: '90%', sm: 450 },
|
||||||
|
bgcolor: 'background.paper',
|
||||||
|
border: '2px solid #000', boxShadow: 24, p: 4
|
||||||
|
};
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
open: boolean;
|
||||||
|
onClose: () => void;
|
||||||
|
onSubmit: (data: UpdateFechaRemitoLoteDto) => Promise<void>;
|
||||||
|
bobinaContexto: StockBobinaDto | null;
|
||||||
|
errorMessage: string | null;
|
||||||
|
clearErrorMessage: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const StockBobinaFechaRemitoModal: React.FC<Props> = ({ open, onClose, onSubmit, bobinaContexto, errorMessage, clearErrorMessage }) => {
|
||||||
|
const [nuevaFecha, setNuevaFecha] = useState('');
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [localError, setLocalError] = useState<string | null>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (open && bobinaContexto) {
|
||||||
|
setNuevaFecha(bobinaContexto.fechaRemito.split('T')[0]); // Iniciar con la fecha actual
|
||||||
|
setLocalError(null);
|
||||||
|
clearErrorMessage();
|
||||||
|
}
|
||||||
|
}, [open, bobinaContexto, clearErrorMessage]);
|
||||||
|
|
||||||
|
if (!bobinaContexto) return null;
|
||||||
|
|
||||||
|
const handleSubmit = async (event: React.FormEvent<HTMLFormElement>) => {
|
||||||
|
event.preventDefault();
|
||||||
|
if (!nuevaFecha) {
|
||||||
|
setLocalError('Debe seleccionar una nueva fecha.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setLoading(true);
|
||||||
|
try {
|
||||||
|
const data: UpdateFechaRemitoLoteDto = {
|
||||||
|
idPlanta: bobinaContexto.idPlanta,
|
||||||
|
remito: bobinaContexto.remito,
|
||||||
|
fechaRemitoActual: bobinaContexto.fechaRemito.split('T')[0],
|
||||||
|
nuevaFechaRemito: nuevaFecha
|
||||||
|
};
|
||||||
|
await onSubmit(data);
|
||||||
|
onClose();
|
||||||
|
} catch (err) {
|
||||||
|
// El error de la API es manejado por el prop `errorMessage`
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal open={open} onClose={onClose}>
|
||||||
|
<Box sx={modalStyle}>
|
||||||
|
<Typography variant="h6" component="h2">Corregir Fecha de Remito</Typography>
|
||||||
|
<Typography variant="body2" sx={{ mt: 1 }}>
|
||||||
|
Esto cambiará la fecha para <strong>todas</strong> las bobinas del remito <strong>{bobinaContexto.remito}</strong> en la planta <strong>{bobinaContexto.nombrePlanta}</strong>.
|
||||||
|
</Typography>
|
||||||
|
<Box component="form" onSubmit={handleSubmit} sx={{ mt: 2 }}>
|
||||||
|
<TextField label="Fecha Actual" value={new Date(bobinaContexto.fechaRemito).toLocaleDateString('es-AR', { timeZone: 'UTC' })} disabled fullWidth margin="normal" />
|
||||||
|
<TextField label="Nueva Fecha de Remito" type="date" value={nuevaFecha}
|
||||||
|
onChange={e => { setNuevaFecha(e.target.value); setLocalError(null); }}
|
||||||
|
required fullWidth margin="normal" InputLabelProps={{ shrink: true }}
|
||||||
|
error={!!localError} helperText={localError} autoFocus
|
||||||
|
/>
|
||||||
|
{errorMessage && <Alert severity="error" sx={{ mt: 2 }}>{errorMessage}</Alert>}
|
||||||
|
<Box sx={{ mt: 3, display: 'flex', justifyContent: 'flex-end', gap: 1 }}>
|
||||||
|
<Button onClick={onClose} color="secondary" disabled={loading}>Cancelar</Button>
|
||||||
|
<Button type="submit" variant="contained" disabled={loading}>
|
||||||
|
{loading ? <CircularProgress size={24} /> : 'Guardar Nueva Fecha'}
|
||||||
|
</Button>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default StockBobinaFechaRemitoModal;
|
||||||
@@ -0,0 +1,374 @@
|
|||||||
|
import React, { useState, useEffect, useRef, useCallback } from 'react';
|
||||||
|
import {
|
||||||
|
Modal, Box, Typography, TextField, Button, CircularProgress, Alert,
|
||||||
|
FormControl, InputLabel, Select, MenuItem, Stepper, Step, StepLabel,
|
||||||
|
Table, TableBody, TableCell, TableContainer, TableHead, TableRow, Paper, IconButton, Divider,
|
||||||
|
InputAdornment, Tooltip
|
||||||
|
} from '@mui/material';
|
||||||
|
import AddCircleOutlineIcon from '@mui/icons-material/AddCircleOutline';
|
||||||
|
import DeleteOutlineIcon from '@mui/icons-material/DeleteOutline';
|
||||||
|
import EditIcon from '@mui/icons-material/Edit';
|
||||||
|
import CloseIcon from '@mui/icons-material/Close';
|
||||||
|
|
||||||
|
import type { StockBobinaDto } from '../../../models/dtos/Impresion/StockBobinaDto';
|
||||||
|
import type { CreateStockBobinaLoteDto } from '../../../models/dtos/Impresion/CreateStockBobinaLoteDto';
|
||||||
|
import type { BobinaLoteDetalleDto } from '../../../models/dtos/Impresion/BobinaLoteDetalleDto';
|
||||||
|
import type { PlantaDto } from '../../../models/dtos/Impresion/PlantaDto';
|
||||||
|
import type { TipoBobinaDto } from '../../../models/dtos/Impresion/TipoBobinaDto';
|
||||||
|
import stockBobinaService from '../../../services/Impresion/stockBobinaService';
|
||||||
|
import plantaService from '../../../services/Impresion/plantaService';
|
||||||
|
import tipoBobinaService from '../../../services/Impresion/tipoBobinaService';
|
||||||
|
|
||||||
|
|
||||||
|
const modalStyle = {
|
||||||
|
position: 'absolute' as 'absolute',
|
||||||
|
top: '50%', left: '50%',
|
||||||
|
transform: 'translate(-50%, -50%)',
|
||||||
|
width: { xs: '95%', sm: '80%', md: '900px' },
|
||||||
|
bgcolor: 'background.paper',
|
||||||
|
border: '2px solid #000', boxShadow: 24, p: 3,
|
||||||
|
maxHeight: '90vh', overflowY: 'auto', display: 'flex', flexDirection: 'column'
|
||||||
|
};
|
||||||
|
|
||||||
|
interface NuevaBobinaState extends BobinaLoteDetalleDto {
|
||||||
|
idTemporal: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface StockBobinaLoteFormModalProps {
|
||||||
|
open: boolean;
|
||||||
|
onClose: (refrescar: boolean) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const steps = ['Datos del Remito', 'Ingreso de Bobinas'];
|
||||||
|
|
||||||
|
const StockBobinaLoteFormModal: React.FC<StockBobinaLoteFormModalProps> = ({ open, onClose }) => {
|
||||||
|
const [activeStep, setActiveStep] = useState(0);
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [apiError, setApiError] = useState<string | null>(null);
|
||||||
|
|
||||||
|
// Step 1 State
|
||||||
|
const [idPlanta, setIdPlanta] = useState<number | ''>('');
|
||||||
|
const [remito, setRemito] = useState('');
|
||||||
|
const [fechaRemito, setFechaRemito] = useState(new Date().toISOString().split('T')[0]);
|
||||||
|
const [headerErrors, setHeaderErrors] = useState<{ [key: string]: string }>({});
|
||||||
|
const [isVerifying, setIsVerifying] = useState(false);
|
||||||
|
const [remitoStatusMessage, setRemitoStatusMessage] = useState<string | null>(null);
|
||||||
|
const [remitoStatusSeverity, setRemitoStatusSeverity] = useState<'success' | 'info'>('info');
|
||||||
|
const [isDateAutocompleted, setIsDateAutocompleted] = useState(false);
|
||||||
|
|
||||||
|
// Step 2 State
|
||||||
|
const [bobinasExistentes, setBobinasExistentes] = useState<StockBobinaDto[]>([]);
|
||||||
|
const [nuevasBobinas, setNuevasBobinas] = useState<NuevaBobinaState[]>([]);
|
||||||
|
const [detalleErrors, setDetalleErrors] = useState<{ [key: string]: string }>({});
|
||||||
|
|
||||||
|
// Dropdowns data
|
||||||
|
const [plantas, setPlantas] = useState<PlantaDto[]>([]);
|
||||||
|
const [tiposBobina, setTiposBobina] = useState<TipoBobinaDto[]>([]);
|
||||||
|
const [loadingDropdowns, setLoadingDropdowns] = useState(true);
|
||||||
|
const tableContainerRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
|
const resetState = useCallback(() => {
|
||||||
|
setActiveStep(0); setLoading(false); setApiError(null);
|
||||||
|
setIdPlanta(''); setRemito(''); setFechaRemito(new Date().toISOString().split('T')[0]);
|
||||||
|
setHeaderErrors({}); setBobinasExistentes([]); setNuevasBobinas([]); setDetalleErrors({});
|
||||||
|
setRemitoStatusMessage(null);
|
||||||
|
setIsDateAutocompleted(false);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const fetchDropdowns = async () => {
|
||||||
|
setLoadingDropdowns(true);
|
||||||
|
try {
|
||||||
|
const [plantasData, tiposData] = await Promise.all([
|
||||||
|
plantaService.getAllPlantas(),
|
||||||
|
tipoBobinaService.getAllTiposBobina()
|
||||||
|
]);
|
||||||
|
setPlantas(plantasData);
|
||||||
|
setTiposBobina(tiposData);
|
||||||
|
} catch (error) {
|
||||||
|
setApiError("Error al cargar datos necesarios (plantas, tipos).");
|
||||||
|
} finally {
|
||||||
|
setLoadingDropdowns(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
if (open) {
|
||||||
|
fetchDropdowns();
|
||||||
|
} else {
|
||||||
|
resetState();
|
||||||
|
}
|
||||||
|
}, [open, resetState]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const verificarRemitoParaAutocompletar = async () => {
|
||||||
|
setRemitoStatusMessage(null);
|
||||||
|
if (remito.trim() && idPlanta) {
|
||||||
|
setIsVerifying(true);
|
||||||
|
try {
|
||||||
|
const existentes = await stockBobinaService.verificarRemitoExistente(Number(idPlanta), remito.trim());
|
||||||
|
if (existentes.length > 0) {
|
||||||
|
setFechaRemito(existentes[0].fechaRemito.split('T')[0]);
|
||||||
|
setRemitoStatusMessage("Remito existente. Se autocompletó la fecha.");
|
||||||
|
setRemitoStatusSeverity('info');
|
||||||
|
setIsDateAutocompleted(true);
|
||||||
|
} else {
|
||||||
|
setRemitoStatusMessage("Este es un remito nuevo.");
|
||||||
|
setRemitoStatusSeverity('success');
|
||||||
|
setIsDateAutocompleted(false);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Fallo la verificación automática de remito: ", error);
|
||||||
|
setRemitoStatusMessage("No se pudo verificar el remito.");
|
||||||
|
setRemitoStatusSeverity('info');
|
||||||
|
} finally {
|
||||||
|
setIsVerifying(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const handler = setTimeout(() => {
|
||||||
|
verificarRemitoParaAutocompletar();
|
||||||
|
}, 500);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
clearTimeout(handler);
|
||||||
|
};
|
||||||
|
}, [idPlanta, remito]);
|
||||||
|
|
||||||
|
const handleClose = () => onClose(false);
|
||||||
|
|
||||||
|
const handleNext = async () => {
|
||||||
|
const errors: { [key: string]: string } = {};
|
||||||
|
if (!remito.trim()) errors.remito = "El número de remito es obligatorio.";
|
||||||
|
if (!idPlanta) errors.idPlanta = "Seleccione una planta.";
|
||||||
|
if (!fechaRemito) errors.fechaRemito = "La fecha es obligatoria.";
|
||||||
|
|
||||||
|
if (Object.keys(errors).length > 0) {
|
||||||
|
setHeaderErrors(errors);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setLoading(true); setApiError(null);
|
||||||
|
try {
|
||||||
|
const existentes = await stockBobinaService.verificarRemitoExistente(Number(idPlanta), remito, fechaRemito);
|
||||||
|
setBobinasExistentes(existentes);
|
||||||
|
setActiveStep(1);
|
||||||
|
} catch (error: any) {
|
||||||
|
const message = error.response?.data?.message || "Error al verificar el remito.";
|
||||||
|
setApiError(message);
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleBack = () => setActiveStep(0);
|
||||||
|
|
||||||
|
const handleAddBobina = () => {
|
||||||
|
setNuevasBobinas(prev => [...prev, {
|
||||||
|
idTemporal: crypto.randomUUID(), idTipoBobina: 0, nroBobina: '', peso: 0
|
||||||
|
}]);
|
||||||
|
setTimeout(() => {
|
||||||
|
tableContainerRef.current?.scrollTo({ top: tableContainerRef.current.scrollHeight, behavior: 'smooth' });
|
||||||
|
}, 100);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleRemoveBobina = (idTemporal: string) => {
|
||||||
|
setNuevasBobinas(prev => prev.filter(b => b.idTemporal !== idTemporal));
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleBobinaChange = (idTemporal: string, field: keyof NuevaBobinaState, value: any) => {
|
||||||
|
setNuevasBobinas(prev => prev.map(b => b.idTemporal === idTemporal ? { ...b, [field]: value } : b));
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSubmit = async () => {
|
||||||
|
const errors: { [key: string]: string } = {};
|
||||||
|
if (nuevasBobinas.length === 0) {
|
||||||
|
setApiError("Debe agregar al menos una nueva bobina para guardar.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const todosNrosBobina = new Set(bobinasExistentes.map(b => b.nroBobina));
|
||||||
|
nuevasBobinas.forEach(b => {
|
||||||
|
if (!b.idTipoBobina) errors[b.idTemporal + '_tipo'] = "Requerido";
|
||||||
|
if (!b.nroBobina.trim()) errors[b.idTemporal + '_nro'] = "Requerido";
|
||||||
|
if ((b.peso || 0) <= 0) errors[b.idTemporal + '_peso'] = "Inválido";
|
||||||
|
if (todosNrosBobina.has(b.nroBobina.trim())) errors[b.idTemporal + '_nro'] = "Duplicado";
|
||||||
|
todosNrosBobina.add(b.nroBobina.trim());
|
||||||
|
});
|
||||||
|
|
||||||
|
if (Object.keys(errors).length > 0) {
|
||||||
|
setDetalleErrors(errors);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setLoading(true); setApiError(null);
|
||||||
|
try {
|
||||||
|
const lote: CreateStockBobinaLoteDto = {
|
||||||
|
idPlanta: Number(idPlanta),
|
||||||
|
remito: remito.trim(),
|
||||||
|
fechaRemito,
|
||||||
|
bobinas: nuevasBobinas.map(({ idTipoBobina, nroBobina, peso }) => ({
|
||||||
|
idTipoBobina: Number(idTipoBobina), nroBobina: nroBobina.trim(), peso: Number(peso)
|
||||||
|
}))
|
||||||
|
};
|
||||||
|
await stockBobinaService.ingresarLoteBobinas(lote);
|
||||||
|
onClose(true);
|
||||||
|
} catch (error: any) {
|
||||||
|
const message = error.response?.data?.message || "Error al guardar el lote de bobinas.";
|
||||||
|
setApiError(message);
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
const renderStepContent = (step: number) => {
|
||||||
|
switch (step) {
|
||||||
|
case 0:
|
||||||
|
return (
|
||||||
|
<Box sx={{ p: 2, display: 'flex', flexDirection: 'column', gap: 2 }}>
|
||||||
|
<Typography variant="h6">Datos de Cabecera</Typography>
|
||||||
|
<TextField label="Número de Remito" value={remito}
|
||||||
|
onChange={e => {
|
||||||
|
setRemito(e.target.value);
|
||||||
|
setIsDateAutocompleted(false);
|
||||||
|
}}
|
||||||
|
required error={!!headerErrors.remito} helperText={headerErrors.remito} disabled={loading} autoFocus
|
||||||
|
/>
|
||||||
|
<FormControl fullWidth error={!!headerErrors.idPlanta}>
|
||||||
|
<InputLabel id="planta-label" required>Planta de Destino</InputLabel>
|
||||||
|
<Select labelId="planta-label" value={idPlanta} label="Planta de Destino"
|
||||||
|
onChange={e => {
|
||||||
|
setIdPlanta(e.target.value as number);
|
||||||
|
setIsDateAutocompleted(false);
|
||||||
|
}}
|
||||||
|
disabled={loading || loadingDropdowns}
|
||||||
|
endAdornment={isVerifying && (<InputAdornment position="end" sx={{ mr: 2 }}><CircularProgress size={20} /></InputAdornment>)}
|
||||||
|
>
|
||||||
|
{plantas.map(p => <MenuItem key={p.idPlanta} value={p.idPlanta}>{p.nombre}</MenuItem>)}
|
||||||
|
</Select>
|
||||||
|
{headerErrors.idPlanta && <Typography color="error" variant="caption">{headerErrors.idPlanta}</Typography>}
|
||||||
|
</FormControl>
|
||||||
|
<TextField
|
||||||
|
label="Fecha de Remito"
|
||||||
|
type="date"
|
||||||
|
value={fechaRemito}
|
||||||
|
onChange={e => setFechaRemito(e.target.value)}
|
||||||
|
InputLabelProps={{ shrink: true }}
|
||||||
|
required
|
||||||
|
error={!!headerErrors.fechaRemito}
|
||||||
|
helperText={headerErrors.fechaRemito}
|
||||||
|
disabled={loading || isDateAutocompleted}
|
||||||
|
InputProps={{
|
||||||
|
endAdornment: (
|
||||||
|
isDateAutocompleted && (
|
||||||
|
<InputAdornment position="end">
|
||||||
|
<Tooltip title="Editar fecha">
|
||||||
|
<IconButton onClick={() => setIsDateAutocompleted(false)} edge="end">
|
||||||
|
<EditIcon />
|
||||||
|
</IconButton>
|
||||||
|
</Tooltip>
|
||||||
|
</InputAdornment>
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Box sx={{ minHeight: 48, mt: 1 }}>
|
||||||
|
{remitoStatusMessage && !isVerifying && (
|
||||||
|
<Alert severity={remitoStatusSeverity} icon={false} variant="outlined">
|
||||||
|
{remitoStatusMessage}
|
||||||
|
</Alert>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
case 1:
|
||||||
|
return (
|
||||||
|
<Box sx={{ p: 2, flexGrow: 1, display: 'flex', flexDirection: 'column', overflow: 'hidden' }}>
|
||||||
|
{bobinasExistentes.length > 0 && (
|
||||||
|
<>
|
||||||
|
<Typography variant="subtitle1" gutterBottom>Bobinas ya ingresadas para este remito:</Typography>
|
||||||
|
<TableContainer component={Paper} variant="outlined" sx={{ maxHeight: '150px', mb: 2 }}>
|
||||||
|
<Table size="small" stickyHeader>
|
||||||
|
<TableHead><TableRow><TableCell>Nro. Bobina</TableCell><TableCell>Tipo</TableCell><TableCell align="right">Peso (Kg)</TableCell></TableRow></TableHead>
|
||||||
|
<TableBody>
|
||||||
|
{bobinasExistentes.map(b => (
|
||||||
|
<TableRow key={b.idBobina}><TableCell>{b.nroBobina}</TableCell><TableCell>{b.nombreTipoBobina}</TableCell><TableCell align="right">{b.peso}</TableCell></TableRow>
|
||||||
|
))}
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
</TableContainer>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
<Typography variant="h6">Nuevas Bobinas a Ingresar</Typography>
|
||||||
|
<TableContainer component={Paper} variant="outlined" sx={{ flexGrow: 1, my: 1, minHeight: '150px' }} ref={tableContainerRef}>
|
||||||
|
<Table size="small" stickyHeader>
|
||||||
|
<TableHead><TableRow><TableCell sx={{ minWidth: 200 }}>Tipo Bobina</TableCell><TableCell>Nro. Bobina</TableCell><TableCell>Peso (Kg)</TableCell><TableCell></TableCell></TableRow></TableHead>
|
||||||
|
<TableBody>
|
||||||
|
{nuevasBobinas.map(bobina => (
|
||||||
|
<TableRow key={bobina.idTemporal}>
|
||||||
|
<TableCell><FormControl fullWidth size="small" error={!!detalleErrors[bobina.idTemporal + '_tipo']}><Select value={bobina.idTipoBobina} onChange={e => handleBobinaChange(bobina.idTemporal, 'idTipoBobina', e.target.value)} disabled={loadingDropdowns}><MenuItem value={0} disabled>Seleccione</MenuItem>{tiposBobina.map(t => <MenuItem key={t.idTipoBobina} value={t.idTipoBobina}>{t.denominacion}</MenuItem>)}</Select></FormControl></TableCell>
|
||||||
|
<TableCell><TextField fullWidth size="small" value={bobina.nroBobina} onChange={e => handleBobinaChange(bobina.idTemporal, 'nroBobina', e.target.value)} error={!!detalleErrors[bobina.idTemporal + '_nro']} helperText={detalleErrors[bobina.idTemporal + '_nro']} /></TableCell>
|
||||||
|
<TableCell><TextField fullWidth size="small" type="number" value={bobina.peso || ''} onChange={e => handleBobinaChange(bobina.idTemporal, 'peso', e.target.value)} error={!!detalleErrors[bobina.idTemporal + '_peso']} helperText={detalleErrors[bobina.idTemporal + '_peso']} /></TableCell>
|
||||||
|
<TableCell><IconButton size="small" color="error" onClick={() => handleRemoveBobina(bobina.idTemporal)}><DeleteOutlineIcon /></IconButton></TableCell>
|
||||||
|
</TableRow>
|
||||||
|
))}
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
</TableContainer>
|
||||||
|
<Button startIcon={<AddCircleOutlineIcon />} onClick={handleAddBobina} sx={{ mt: 1 }}>Agregar Bobina</Button>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
default:
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal open={open} onClose={handleClose}>
|
||||||
|
<Box sx={modalStyle}>
|
||||||
|
<IconButton
|
||||||
|
aria-label="close"
|
||||||
|
onClick={handleClose}
|
||||||
|
sx={{
|
||||||
|
position: 'absolute',
|
||||||
|
right: 8,
|
||||||
|
top: 8,
|
||||||
|
color: (theme) => theme.palette.grey[500],
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<CloseIcon />
|
||||||
|
</IconButton>
|
||||||
|
|
||||||
|
<Typography variant="h5" component="h2" gutterBottom>Ingreso de Bobinas por Lote</Typography>
|
||||||
|
|
||||||
|
<Stepper activeStep={activeStep} sx={{ mb: 2 }}>
|
||||||
|
{steps.map((label) => (
|
||||||
|
<Step key={label}>
|
||||||
|
<StepLabel>{label}</StepLabel>
|
||||||
|
</Step>
|
||||||
|
))}
|
||||||
|
</Stepper>
|
||||||
|
|
||||||
|
<Box sx={{ flexGrow: 1, overflowY: 'auto' }}>
|
||||||
|
{loadingDropdowns && activeStep === 0 ? <Box sx={{ display: 'flex', justifyContent: 'center', my: 5 }}><CircularProgress /></Box> : renderStepContent(activeStep)}
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
{apiError && <Alert severity="error" sx={{ mt: 2, flexShrink: 0 }}>{apiError}</Alert>}
|
||||||
|
|
||||||
|
<Divider sx={{ my: 2, flexShrink: 0 }} />
|
||||||
|
|
||||||
|
<Box sx={{ display: 'flex', justifyContent: 'space-between', pt: 1, flexShrink: 0 }}>
|
||||||
|
<Button color="inherit" disabled={activeStep === 0 || loading} onClick={handleBack}>
|
||||||
|
Atrás
|
||||||
|
</Button>
|
||||||
|
<Box>
|
||||||
|
{activeStep === 0 && <Button onClick={handleNext} variant="contained" disabled={loading || loadingDropdowns}>{loading ? <CircularProgress size={24} /> : 'Verificar y Continuar'}</Button>}
|
||||||
|
{activeStep === 1 && <Button onClick={handleSubmit} variant="contained" color="success" disabled={loading}>{loading ? <CircularProgress size={24} /> : 'Guardar Lote'}</Button>}
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default StockBobinaLoteFormModal;
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
import React, { useState, useEffect } from 'react';
|
import React, { useState, useEffect } from 'react';
|
||||||
import { Modal, Box, Typography, TextField, Button, CircularProgress, Alert, FormControl, InputLabel, Select, MenuItem, type SelectChangeEvent, InputAdornment } from '@mui/material';
|
import { Modal, Box, Typography, TextField, Button, CircularProgress, Alert, FormControl, InputLabel, Select, MenuItem, type SelectChangeEvent, InputAdornment } from '@mui/material';
|
||||||
import type { FacturaDto } from '../../../models/dtos/Suscripciones/FacturaDto';
|
import type { FacturaConsolidadaDto } from '../../../models/dtos/Suscripciones/ResumenCuentaSuscriptorDto';
|
||||||
import type { CreatePagoDto } from '../../../models/dtos/Suscripciones/CreatePagoDto';
|
import type { CreatePagoDto } from '../../../models/dtos/Suscripciones/CreatePagoDto';
|
||||||
import type { FormaPagoDto } from '../../../models/dtos/Suscripciones/FormaPagoDto';
|
import type { FormaPagoDto } from '../../../models/dtos/Suscripciones/FormaPagoDto';
|
||||||
import formaPagoService from '../../../services/Suscripciones/formaPagoService';
|
import formaPagoService from '../../../services/Suscripciones/formaPagoService';
|
||||||
@@ -23,17 +23,19 @@ interface PagoManualModalProps {
|
|||||||
open: boolean;
|
open: boolean;
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
onSubmit: (data: CreatePagoDto) => Promise<void>;
|
onSubmit: (data: CreatePagoDto) => Promise<void>;
|
||||||
factura: FacturaDto | null;
|
factura: FacturaConsolidadaDto | null;
|
||||||
|
nombreSuscriptor: string; // Se pasa el nombre del suscriptor como prop-
|
||||||
errorMessage?: string | null;
|
errorMessage?: string | null;
|
||||||
clearErrorMessage: () => void;
|
clearErrorMessage: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const PagoManualModal: React.FC<PagoManualModalProps> = ({ open, onClose, onSubmit, factura, errorMessage, clearErrorMessage }) => {
|
const PagoManualModal: React.FC<PagoManualModalProps> = ({ open, onClose, onSubmit, factura, nombreSuscriptor, errorMessage, clearErrorMessage }) => {
|
||||||
const [formData, setFormData] = useState<Partial<CreatePagoDto>>({});
|
const [formData, setFormData] = useState<Partial<CreatePagoDto>>({});
|
||||||
const [formasDePago, setFormasDePago] = useState<FormaPagoDto[]>([]);
|
const [formasDePago, setFormasDePago] = useState<FormaPagoDto[]>([]);
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [loadingFormasPago, setLoadingFormasPago] = useState(false);
|
const [loadingFormasPago, setLoadingFormasPago] = useState(false);
|
||||||
const [localErrors, setLocalErrors] = useState<{ [key: string]: string | null }>({});
|
const [localErrors, setLocalErrors] = useState<{ [key: string]: string | null }>({});
|
||||||
|
const saldoPendiente = factura ? factura.importeFinal - factura.totalPagado : 0;
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const fetchFormasDePago = async () => {
|
const fetchFormasDePago = async () => {
|
||||||
@@ -52,12 +54,12 @@ const PagoManualModal: React.FC<PagoManualModalProps> = ({ open, onClose, onSubm
|
|||||||
fetchFormasDePago();
|
fetchFormasDePago();
|
||||||
setFormData({
|
setFormData({
|
||||||
idFactura: factura.idFactura,
|
idFactura: factura.idFactura,
|
||||||
monto: factura.saldoPendiente, // Usar el saldo pendiente como valor por defecto
|
monto: saldoPendiente,
|
||||||
fechaPago: new Date().toISOString().split('T')[0]
|
fechaPago: new Date().toISOString().split('T')[0]
|
||||||
});
|
});
|
||||||
setLocalErrors({});
|
setLocalErrors({});
|
||||||
}
|
}
|
||||||
}, [open, factura]);
|
}, [open, factura, saldoPendiente]);
|
||||||
|
|
||||||
const validate = (): boolean => {
|
const validate = (): boolean => {
|
||||||
const errors: { [key: string]: string | null } = {};
|
const errors: { [key: string]: string | null } = {};
|
||||||
@@ -65,13 +67,11 @@ const PagoManualModal: React.FC<PagoManualModalProps> = ({ open, onClose, onSubm
|
|||||||
if (!formData.fechaPago) errors.fechaPago = "La fecha de pago es obligatoria.";
|
if (!formData.fechaPago) errors.fechaPago = "La fecha de pago es obligatoria.";
|
||||||
|
|
||||||
const monto = formData.monto ?? 0;
|
const monto = formData.monto ?? 0;
|
||||||
const saldo = factura?.saldoPendiente ?? 0;
|
|
||||||
|
|
||||||
if (monto <= 0) {
|
if (monto <= 0) {
|
||||||
errors.monto = "El monto debe ser mayor a cero.";
|
errors.monto = "El monto debe ser mayor a cero.";
|
||||||
} else if (monto > saldo) {
|
} else if (monto > saldoPendiente) {
|
||||||
// Usamos toFixed(2) para mostrar el formato de moneda correcto en el mensaje
|
errors.monto = `El monto no puede superar el saldo pendiente de $${saldoPendiente.toFixed(2)}.`;
|
||||||
errors.monto = `El monto no puede superar el saldo pendiente de $${saldo.toFixed(2)}.`;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
setLocalErrors(errors);
|
setLocalErrors(errors);
|
||||||
@@ -117,29 +117,32 @@ const PagoManualModal: React.FC<PagoManualModalProps> = ({ open, onClose, onSubm
|
|||||||
<Modal open={open} onClose={onClose}>
|
<Modal open={open} onClose={onClose}>
|
||||||
<Box sx={modalStyle}>
|
<Box sx={modalStyle}>
|
||||||
<Typography variant="h6">Registrar Pago Manual</Typography>
|
<Typography variant="h6">Registrar Pago Manual</Typography>
|
||||||
<Typography variant="subtitle1" gutterBottom sx={{fontWeight: 'bold'}}>
|
<Typography variant="body1" color="text.secondary" gutterBottom>
|
||||||
Saldo Pendiente: ${factura.saldoPendiente.toFixed(2)}
|
Para: {nombreSuscriptor}
|
||||||
|
</Typography>
|
||||||
|
<Typography variant="subtitle1" gutterBottom sx={{ fontWeight: 'bold' }}>
|
||||||
|
Saldo Pendiente: ${saldoPendiente.toFixed(2)}
|
||||||
</Typography>
|
</Typography>
|
||||||
<Box component="form" onSubmit={handleSubmit} sx={{ mt: 2 }}>
|
<Box component="form" onSubmit={handleSubmit} sx={{ mt: 2 }}>
|
||||||
<TextField name="fechaPago" label="Fecha de Pago" type="date" value={formData.fechaPago || ''} onChange={handleInputChange} required fullWidth margin="dense" InputLabelProps={{ shrink: true }} error={!!localErrors.fechaPago} helperText={localErrors.fechaPago} />
|
<TextField name="fechaPago" label="Fecha de Pago" type="date" value={formData.fechaPago || ''} onChange={handleInputChange} required fullWidth margin="dense" InputLabelProps={{ shrink: true }} error={!!localErrors.fechaPago} helperText={localErrors.fechaPago} />
|
||||||
<FormControl fullWidth margin="dense" error={!!localErrors.idFormaPago}>
|
<FormControl fullWidth margin="dense" error={!!localErrors.idFormaPago}>
|
||||||
<InputLabel id="forma-pago-label" required>Forma de Pago</InputLabel>
|
<InputLabel id="forma-pago-label" required>Forma de Pago</InputLabel>
|
||||||
<Select name="idFormaPago" labelId="forma-pago-label" value={formData.idFormaPago || ''} onChange={handleSelectChange} label="Forma de Pago" disabled={loadingFormasPago}>
|
<Select name="idFormaPago" labelId="forma-pago-label" value={formData.idFormaPago || ''} onChange={handleSelectChange} label="Forma de Pago" disabled={loadingFormasPago}>
|
||||||
{formasDePago.map(fp => <MenuItem key={fp.idFormaPago} value={fp.idFormaPago}>{fp.nombre}</MenuItem>)}
|
{formasDePago.map(fp => <MenuItem key={fp.idFormaPago} value={fp.idFormaPago}>{fp.nombre}</MenuItem>)}
|
||||||
</Select>
|
</Select>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<TextField name="monto" label="Monto Pagado" type="number" value={formData.monto || ''} onChange={handleInputChange} required fullWidth margin="dense" error={!!localErrors.monto} helperText={localErrors.monto} InputProps={{ startAdornment: <InputAdornment position="start">$</InputAdornment> }} inputProps={{ step: "0.01" }} />
|
<TextField name="monto" label="Monto Pagado" type="number" value={formData.monto || ''} onChange={handleInputChange} required fullWidth margin="dense" error={!!localErrors.monto} helperText={localErrors.monto} InputProps={{ startAdornment: <InputAdornment position="start">$</InputAdornment> }} inputProps={{ step: "0.01" }} />
|
||||||
<TextField name="referencia" label="Referencia (Opcional)" value={formData.referencia || ''} onChange={handleInputChange} fullWidth margin="dense" />
|
<TextField name="referencia" label="Referencia (Opcional)" value={formData.referencia || ''} onChange={handleInputChange} fullWidth margin="dense" />
|
||||||
<TextField name="observaciones" label="Observaciones (Opcional)" value={formData.observaciones || ''} onChange={handleInputChange} fullWidth margin="dense" multiline rows={2} />
|
<TextField name="observaciones" label="Observaciones (Opcional)" value={formData.observaciones || ''} onChange={handleInputChange} fullWidth margin="dense" multiline rows={2} />
|
||||||
|
|
||||||
{errorMessage && <Alert severity="error" sx={{ mt: 2 }}>{errorMessage}</Alert>}
|
{errorMessage && <Alert severity="error" sx={{ mt: 2 }}>{errorMessage}</Alert>}
|
||||||
|
|
||||||
<Box sx={{ mt: 3, display: 'flex', justifyContent: 'flex-end', gap: 1 }}>
|
<Box sx={{ mt: 3, display: 'flex', justifyContent: 'flex-end', gap: 1 }}>
|
||||||
<Button onClick={onClose} color="secondary" disabled={loading}>Cancelar</Button>
|
<Button onClick={onClose} color="secondary" disabled={loading}>Cancelar</Button>
|
||||||
<Button type="submit" variant="contained" disabled={loading || loadingFormasPago}>
|
<Button type="submit" variant="contained" disabled={loading || loadingFormasPago}>
|
||||||
{loading ? <CircularProgress size={24} /> : 'Registrar Pago'}
|
{loading ? <CircularProgress size={24} /> : 'Registrar Pago'}
|
||||||
</Button>
|
</Button>
|
||||||
</Box>
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
</Modal>
|
</Modal>
|
||||||
|
|||||||
@@ -14,18 +14,18 @@ const SECCION_PERMISSIONS_PREFIX = "SS";
|
|||||||
// Mapeo de codAcc de sección a su módulo conceptual
|
// Mapeo de codAcc de sección a su módulo conceptual
|
||||||
const getModuloFromSeccionCodAcc = (codAcc: string): string | null => {
|
const getModuloFromSeccionCodAcc = (codAcc: string): string | null => {
|
||||||
if (codAcc === "SS001") return "Distribución";
|
if (codAcc === "SS001") return "Distribución";
|
||||||
|
if (codAcc === "SS007") return "Suscripciones";
|
||||||
if (codAcc === "SS002") return "Contables";
|
if (codAcc === "SS002") return "Contables";
|
||||||
if (codAcc === "SS003") return "Impresión";
|
if (codAcc === "SS003") return "Impresión";
|
||||||
if (codAcc === "SS004") return "Reportes";
|
if (codAcc === "SS004") return "Reportes";
|
||||||
if (codAcc === "SS005") return "Radios";
|
|
||||||
if (codAcc === "SS006") return "Usuarios";
|
if (codAcc === "SS006") return "Usuarios";
|
||||||
|
if (codAcc === "SS005") return "Radios";
|
||||||
return null;
|
return null;
|
||||||
};
|
};
|
||||||
|
|
||||||
// Función para determinar el módulo conceptual de un permiso individual
|
// Función para determinar el módulo conceptual de un permiso individual
|
||||||
const getModuloConceptualDelPermiso = (permisoModulo: string): string => {
|
const getModuloConceptualDelPermiso = (permisoModulo: string): string => {
|
||||||
const moduloLower = permisoModulo.toLowerCase();
|
const moduloLower = permisoModulo.toLowerCase();
|
||||||
|
|
||||||
if (moduloLower.includes("distribuidores") ||
|
if (moduloLower.includes("distribuidores") ||
|
||||||
moduloLower.includes("canillas") || // Cubre "Canillas" y "Movimientos Canillas"
|
moduloLower.includes("canillas") || // Cubre "Canillas" y "Movimientos Canillas"
|
||||||
moduloLower.includes("publicaciones distribución") ||
|
moduloLower.includes("publicaciones distribución") ||
|
||||||
@@ -36,9 +36,13 @@ const getModuloConceptualDelPermiso = (permisoModulo: string): string => {
|
|||||||
moduloLower.includes("ctrl. devoluciones")) {
|
moduloLower.includes("ctrl. devoluciones")) {
|
||||||
return "Distribución";
|
return "Distribución";
|
||||||
}
|
}
|
||||||
|
if (moduloLower.includes("suscripciones")) {
|
||||||
|
return "Suscripciones";
|
||||||
|
}
|
||||||
if (moduloLower.includes("cuentas pagos") ||
|
if (moduloLower.includes("cuentas pagos") ||
|
||||||
moduloLower.includes("cuentas notas") ||
|
moduloLower.includes("cuentas notas") ||
|
||||||
moduloLower.includes("cuentas tipos pagos")) {
|
moduloLower.includes("cuentas tipos pagos") ||
|
||||||
|
moduloLower.includes("cuentas cierres")) {
|
||||||
return "Contables";
|
return "Contables";
|
||||||
}
|
}
|
||||||
if (moduloLower.includes("impresión tiradas") ||
|
if (moduloLower.includes("impresión tiradas") ||
|
||||||
@@ -89,7 +93,7 @@ const PermisosChecklist: React.FC<PermisosChecklistProps> = ({
|
|||||||
return acc;
|
return acc;
|
||||||
}, {} as Record<string, PermisoAsignadoDto[]>);
|
}, {} as Record<string, PermisoAsignadoDto[]>);
|
||||||
|
|
||||||
const ordenModulosPrincipales = ["Distribución", "Contables", "Impresión", "Radios", "Usuarios", "Reportes", "Permisos (Definición)"];
|
const ordenModulosPrincipales = ["Distribución", "Suscripciones", "Contables", "Impresión", "Usuarios", "Reportes", "Radios","Permisos (Definición)"];
|
||||||
// Añadir módulos que solo tienen permiso de sección (como Radios) pero no hijos (aún)
|
// Añadir módulos que solo tienen permiso de sección (como Radios) pero no hijos (aún)
|
||||||
permisosDeSeccion.forEach(ps => {
|
permisosDeSeccion.forEach(ps => {
|
||||||
const moduloConceptual = getModuloFromSeccionCodAcc(ps.codAcc);
|
const moduloConceptual = getModuloFromSeccionCodAcc(ps.codAcc);
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import React, { useState, useEffect } from 'react';
|
import React, { useState, useEffect } from 'react';
|
||||||
import {
|
import { Modal, Box, Typography, TextField, Button, CircularProgress, Alert,
|
||||||
Modal, Box, Typography, TextField, Button, CircularProgress, Alert,
|
|
||||||
FormControl, InputLabel, Select, MenuItem, FormControlLabel, Checkbox
|
FormControl, InputLabel, Select, MenuItem, FormControlLabel, Checkbox
|
||||||
} from '@mui/material';
|
} from '@mui/material';
|
||||||
import type { UsuarioDto } from '../../../models/dtos/Usuarios/UsuarioDto';
|
import type { UsuarioDto } from '../../../models/dtos/Usuarios/UsuarioDto';
|
||||||
|
|||||||
@@ -1,22 +1,22 @@
|
|||||||
// src/hooks/usePermissions.ts
|
|
||||||
import { useAuth } from '../contexts/AuthContext';
|
import { useAuth } from '../contexts/AuthContext';
|
||||||
|
import { useCallback } from 'react';
|
||||||
|
|
||||||
export const usePermissions = () => {
|
export const usePermissions = () => {
|
||||||
const { user } = useAuth(); // user aquí es de tipo UserContextData | null
|
const { user } = useAuth();
|
||||||
|
|
||||||
const tienePermiso = (codigoPermisoRequerido: string): boolean => {
|
// Envolvemos la función en useCallback.
|
||||||
if (!user) { // Si no hay usuario logueado
|
// Su dependencia es [user], por lo que la función solo se
|
||||||
|
// volverá a crear si el objeto 'user' cambia (ej. al iniciar/cerrar sesión).
|
||||||
|
const tienePermiso = useCallback((codigoPermisoRequerido: string): boolean => {
|
||||||
|
if (!user) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if (user.esSuperAdmin) { // SuperAdmin tiene todos los permisos
|
if (user.esSuperAdmin) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
// Verificar si la lista de permisos del usuario incluye el código requerido
|
|
||||||
return user.permissions?.includes(codigoPermisoRequerido) ?? false;
|
return user.permissions?.includes(codigoPermisoRequerido) ?? false;
|
||||||
};
|
}, [user]);
|
||||||
|
|
||||||
// También puede exportar el objeto user completo si se necesita en otros lugares
|
|
||||||
// o propiedades específicas como idPerfil, esSuperAdmin.
|
|
||||||
return {
|
return {
|
||||||
tienePermiso,
|
tienePermiso,
|
||||||
isSuperAdmin: user?.esSuperAdmin ?? false,
|
isSuperAdmin: user?.esSuperAdmin ?? false,
|
||||||
|
|||||||
@@ -0,0 +1,24 @@
|
|||||||
|
// Refleja DTO C# Dtos.Auditoria.CierreCuentaCorrienteHistorialDto.
|
||||||
|
// La tabla _H y este DTO usan PascalCase con underscores; el JSON serializer
|
||||||
|
// default de ASP.NET Core sólo aplica camelCase a la primera letra,
|
||||||
|
// preservando los underscores subsiguientes.
|
||||||
|
export interface CierreCuentaCorrienteHistorialDto {
|
||||||
|
id_Historial: number;
|
||||||
|
id_Cierre: number;
|
||||||
|
id_Distribuidor: number;
|
||||||
|
id_Empresa: number;
|
||||||
|
fechaCorte: string; // ISO datetime
|
||||||
|
fechaCierre: string;
|
||||||
|
saldoCierre: number;
|
||||||
|
estado: 'Activo' | 'Anulado';
|
||||||
|
justificacion?: string | null;
|
||||||
|
id_Usuario_Cierre: number;
|
||||||
|
id_Usuario_Anula?: number | null;
|
||||||
|
fechaAnulacion?: string | null;
|
||||||
|
justificacion_Anulacion?: string | null;
|
||||||
|
|
||||||
|
tipoMod: 'Creacion' | 'Reapertura' | 'Modificacion';
|
||||||
|
id_Usuario_Mod: number;
|
||||||
|
nombreUsuarioModifico: string;
|
||||||
|
fechaMod: string; // ISO datetime
|
||||||
|
}
|
||||||
@@ -4,4 +4,7 @@ export interface AjusteSaldoRequestDto {
|
|||||||
idEmpresa: number;
|
idEmpresa: number;
|
||||||
montoAjuste: number;
|
montoAjuste: number;
|
||||||
justificacion: string;
|
justificacion: string;
|
||||||
|
// Fecha lógica de la operación contable (no la del ajuste físico).
|
||||||
|
// Se valida contra períodos cerrados del distribuidor+empresa.
|
||||||
|
fechaOperacion: string; // "yyyy-MM-dd"
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
export interface CierreCuentaCorrienteDto {
|
||||||
|
idCierre: number;
|
||||||
|
idDistribuidor: number;
|
||||||
|
nombreDistribuidor: string;
|
||||||
|
idEmpresa: number;
|
||||||
|
nombreEmpresa: string;
|
||||||
|
fechaCorte: string; // "yyyy-MM-dd"
|
||||||
|
fechaCierre: string; // ISO datetime
|
||||||
|
saldoCierre: number;
|
||||||
|
estado: 'Activo' | 'Anulado';
|
||||||
|
justificacion?: string | null;
|
||||||
|
idUsuarioCierre: number;
|
||||||
|
nombreUsuarioCierre: string;
|
||||||
|
idUsuarioAnula?: number | null;
|
||||||
|
nombreUsuarioAnula?: string | null;
|
||||||
|
fechaAnulacion?: string | null; // ISO datetime
|
||||||
|
justificacionAnulacion?: string | null;
|
||||||
|
esUltimoVigente: boolean;
|
||||||
|
}
|
||||||
6
Frontend/src/models/dtos/Contables/CrearCierreDto.ts
Normal file
6
Frontend/src/models/dtos/Contables/CrearCierreDto.ts
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
export interface CrearCierreDto {
|
||||||
|
idDistribuidor: number;
|
||||||
|
idEmpresa: number;
|
||||||
|
fechaCorte: string; // "yyyy-MM-dd"
|
||||||
|
justificacion?: string | null;
|
||||||
|
}
|
||||||
4
Frontend/src/models/dtos/Contables/ReabrirCierreDto.ts
Normal file
4
Frontend/src/models/dtos/Contables/ReabrirCierreDto.ts
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
export interface ReabrirCierreDto {
|
||||||
|
// Min 10, max 500 caracteres (validado en backend con DataAnnotations).
|
||||||
|
justificacion: string;
|
||||||
|
}
|
||||||
6
Frontend/src/models/dtos/Contables/UltimoCierreDto.ts
Normal file
6
Frontend/src/models/dtos/Contables/UltimoCierreDto.ts
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
export interface UltimoCierreDto {
|
||||||
|
idCierre: number;
|
||||||
|
fechaCorte: string; // "yyyy-MM-dd"
|
||||||
|
saldoCierre: number;
|
||||||
|
estado: 'Activo' | 'Anulado';
|
||||||
|
}
|
||||||
@@ -12,4 +12,6 @@ export interface DistribuidorDto {
|
|||||||
telefono?: string | null;
|
telefono?: string | null;
|
||||||
email?: string | null;
|
email?: string | null;
|
||||||
localidad?: string | null;
|
localidad?: string | null;
|
||||||
|
baja?: boolean;
|
||||||
|
fechaBaja?: string | null;
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
export interface BobinaLoteDetalleDto {
|
||||||
|
idTipoBobina: number;
|
||||||
|
nroBobina: string;
|
||||||
|
peso: number;
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
import type { BobinaLoteDetalleDto } from './BobinaLoteDetalleDto';
|
||||||
|
|
||||||
|
export interface CreateStockBobinaLoteDto {
|
||||||
|
idPlanta: number;
|
||||||
|
remito: string;
|
||||||
|
fechaRemito: string; // "yyyy-MM-dd"
|
||||||
|
bobinas: BobinaLoteDetalleDto[];
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user