Compare commits
1 Commits
main
...
feat/cierr
| Author | SHA1 | Date | |
|---|---|---|---|
| 24eaf18fd9 |
2
.gitignore
vendored
2
.gitignore
vendored
@@ -161,3 +161,5 @@ junit.xml
|
|||||||
# ===================================================================
|
# ===================================================================
|
||||||
# Fin del archivo .gitignore
|
# Fin del archivo .gitignore
|
||||||
# ===================================================================
|
# ===================================================================
|
||||||
|
Backend/SQL
|
||||||
|
.atl
|
||||||
|
|||||||
@@ -1,27 +0,0 @@
|
|||||||
-- Script para agregar borrado lógico a Distribuidores
|
|
||||||
|
|
||||||
-- 1. Agregar columnas a la tabla principal
|
|
||||||
ALTER TABLE dbo.dist_dtDistribuidores
|
|
||||||
ADD Baja bit NOT NULL DEFAULT 0;
|
|
||||||
|
|
||||||
ALTER TABLE dbo.dist_dtDistribuidores
|
|
||||||
ADD FechaBaja datetime2(0) NULL;
|
|
||||||
|
|
||||||
-- 2. Agregar columnas a la tabla histórica
|
|
||||||
ALTER TABLE dbo.dist_dtDistribuidores_H
|
|
||||||
ADD Baja bit NULL;
|
|
||||||
|
|
||||||
ALTER TABLE dbo.dist_dtDistribuidores_H
|
|
||||||
ADD FechaBaja datetime2(0) NULL;
|
|
||||||
|
|
||||||
-- 3. ATENCION: Actualizar Stored Procedures de Reportes
|
|
||||||
-- Los siguientes Stored Procedures deben ser modificados para incluir la condicion "AND Baja = 0"
|
|
||||||
-- en las consultas a "dist_dtDistribuidores":
|
|
||||||
-- - SP_BalanceCuentaDistEntradaSalidaPorEmpresa
|
|
||||||
-- - SP_BalanceCuentDistDebCredEmpresa
|
|
||||||
-- - SP_BalanceCuentDistPagosEmpresa
|
|
||||||
-- - SP_BalanceCuentSaldosEmpresas
|
|
||||||
-- - SP_CantidadEntradaSalida
|
|
||||||
-- - SP_CantidadEntradaSalidaCPromAgDia
|
|
||||||
|
|
||||||
PRINT 'Se agregaron correctamente las columnas Baja y FechaBaja a dist_dtDistribuidores y dist_dtDistribuidores_H';
|
|
||||||
@@ -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.");
|
||||||
|
|||||||
@@ -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);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -894,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." });
|
||||||
}
|
}
|
||||||
@@ -911,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
|
||||||
};
|
};
|
||||||
@@ -932,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." });
|
||||||
}
|
}
|
||||||
@@ -950,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"),
|
||||||
|
|||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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; }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -23,6 +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;
|
||||||
|
|
||||||
var builder = WebApplication.CreateBuilder(args);
|
var builder = WebApplication.CreateBuilder(args);
|
||||||
|
|
||||||
@@ -96,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
|
||||||
@@ -269,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
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
@@ -106,6 +109,11 @@ namespace GestionIntegral.Api.Services.Contables
|
|||||||
return (null, mensajeError);
|
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
|
||||||
{
|
{
|
||||||
IdDistribuidor = createDto.IdDistribuidor,
|
IdDistribuidor = createDto.IdDistribuidor,
|
||||||
@@ -182,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();
|
||||||
@@ -219,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."); }
|
||||||
@@ -253,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);
|
||||||
@@ -266,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."); }
|
||||||
|
|||||||
@@ -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."); }
|
||||||
|
|||||||
@@ -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,6 +171,11 @@ 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))
|
||||||
{
|
{
|
||||||
@@ -262,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(
|
||||||
@@ -307,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 { }
|
||||||
@@ -330,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,
|
||||||
@@ -364,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 { }
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -420,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()
|
||||||
@@ -455,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
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -466,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."
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
@@ -95,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 } = {};
|
||||||
|
|||||||
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 } = {};
|
||||||
|
|||||||
@@ -41,7 +41,8 @@ const getModuloConceptualDelPermiso = (permisoModulo: string): string => {
|
|||||||
}
|
}
|
||||||
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") ||
|
||||||
|
|||||||
@@ -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';
|
||||||
|
}
|
||||||
@@ -1,14 +1,14 @@
|
|||||||
import type { BalanceCuentaDebCredDto } from "./BalanceCuentaDebCredDto";
|
import type { BalanceCuentaDebCredDto } from "./BalanceCuentaDebCredDto";
|
||||||
import type { BalanceCuentaDistDto } from "./BalanceCuentaDistDto";
|
import type { BalanceCuentaDistDto } from "./BalanceCuentaDistDto";
|
||||||
import type { BalanceCuentaPagosDto } from "./BalanceCuentaPagosDto";
|
import type { BalanceCuentaPagosDto } from "./BalanceCuentaPagosDto";
|
||||||
import type { SaldoDto } from "./SaldoDto";
|
|
||||||
|
|
||||||
export interface ReporteCuentasDistribuidorResponseDto {
|
export interface ReporteCuentasDistribuidorResponseDto {
|
||||||
entradasSalidas: BalanceCuentaDistDto[];
|
entradasSalidas: BalanceCuentaDistDto[];
|
||||||
debitosCreditos: BalanceCuentaDebCredDto[];
|
debitosCreditos: BalanceCuentaDebCredDto[];
|
||||||
pagos: BalanceCuentaPagosDto[];
|
pagos: BalanceCuentaPagosDto[];
|
||||||
saldos: SaldoDto[]; // Aunque SP_BalanceCuentSaldos devuelve una lista, para un distribuidor/empresa debería ser 1 solo.
|
// Saldo inicial del período: snapshot del último cierre + movimientos netos hasta fechaDesde-1.
|
||||||
// Se podría ajustar el DTO o el servicio para devolver solo el primer saldo.
|
// 0 si no hay cierre previo.
|
||||||
nombreDistribuidor?: string; // Para el título del reporte
|
saldoInicial: number;
|
||||||
nombreEmpresa?: string; // Para el título del reporte
|
nombreDistribuidor?: string;
|
||||||
|
nombreEmpresa?: string;
|
||||||
}
|
}
|
||||||
@@ -1,13 +1,12 @@
|
|||||||
import type { BalanceCuentaDebCredDto } from "./BalanceCuentaDebCredDto";
|
import type { BalanceCuentaDebCredDto } from "./BalanceCuentaDebCredDto";
|
||||||
import type { BalanceCuentaDistDto } from "./BalanceCuentaDistDto";
|
import type { BalanceCuentaDistDto } from "./BalanceCuentaDistDto";
|
||||||
import type { BalanceCuentaPagosDto } from "./BalanceCuentaPagosDto";
|
import type { BalanceCuentaPagosDto } from "./BalanceCuentaPagosDto";
|
||||||
import type { SaldoDto } from "./SaldoDto";
|
|
||||||
|
|
||||||
export interface ReporteCuentasDistribuidorResponseDto {
|
export interface ReporteCuentasDistribuidorResponseDto {
|
||||||
entradasSalidas: BalanceCuentaDistDto[];
|
entradasSalidas: BalanceCuentaDistDto[];
|
||||||
debitosCreditos: BalanceCuentaDebCredDto[];
|
debitosCreditos: BalanceCuentaDebCredDto[];
|
||||||
pagos: BalanceCuentaPagosDto[];
|
pagos: BalanceCuentaPagosDto[];
|
||||||
saldos: SaldoDto[];
|
saldoInicial: number;
|
||||||
nombreDistribuidor?: string;
|
nombreDistribuidor?: string;
|
||||||
nombreEmpresa?: string;
|
nombreEmpresa?: string;
|
||||||
}
|
}
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
export interface SaldoDto {
|
|
||||||
monto: number;
|
|
||||||
}
|
|
||||||
@@ -62,6 +62,7 @@ const TIPOS_ENTIDAD_AUDITABLES = [
|
|||||||
{ value: "PermisoMaestro", label: "Permisos (Definición) (gral_Permisos_H)" },
|
{ value: "PermisoMaestro", label: "Permisos (Definición) (gral_Permisos_H)" },
|
||||||
{ value: "PermisosPerfiles", label: "Asignación de Permisos a Perfiles (gral_PermisosPerfiles_H)" },
|
{ value: "PermisosPerfiles", label: "Asignación de Permisos a Perfiles (gral_PermisosPerfiles_H)" },
|
||||||
{ value: "CambioParada", label: "Cambios de Parada (dist_CambiosParadasCanillas_H)" },
|
{ value: "CambioParada", label: "Cambios de Parada (dist_CambiosParadasCanillas_H)" },
|
||||||
|
{ value: "CierreCC", label: "Cierres de Cuenta Corriente (cue_CierresCuentaCorriente_H)" },
|
||||||
].sort((a, b) => a.label.localeCompare(b.label));
|
].sort((a, b) => a.label.localeCompare(b.label));
|
||||||
|
|
||||||
const TIPOS_MODIFICACION = [
|
const TIPOS_MODIFICACION = [
|
||||||
@@ -653,6 +654,29 @@ const AuditoriaGeneralPage: React.FC = () => {
|
|||||||
{ field: 'vigenciaH', headerName: 'Vig. Hasta', width: 120, type: 'date', valueFormatter: (value) => formatDate(value as string) },
|
{ field: 'vigenciaH', headerName: 'Vig. Hasta', width: 120, type: 'date', valueFormatter: (value) => formatDate(value as string) },
|
||||||
];
|
];
|
||||||
break;
|
break;
|
||||||
|
case "CierreCC":
|
||||||
|
const cierreCcHist = await auditoriaService.getHistorialCierresCC({
|
||||||
|
...commonParams,
|
||||||
|
// "ID Entidad Afectada" filtra por Id_Cierre puntual.
|
||||||
|
idCierreAfectado: filtroIdEntidad ? Number(filtroIdEntidad) : undefined
|
||||||
|
});
|
||||||
|
rawData = cierreCcHist;
|
||||||
|
cols = [
|
||||||
|
...getCommonAuditColumns(),
|
||||||
|
{ field: 'id_Cierre', headerName: 'ID Cierre', width: 100, align: 'center', headerAlign: 'center' },
|
||||||
|
{ field: 'id_Distribuidor', headerName: 'ID Dist.', width: 100, align: 'center', headerAlign: 'center' },
|
||||||
|
{ field: 'id_Empresa', headerName: 'ID Emp.', width: 100, align: 'center', headerAlign: 'center' },
|
||||||
|
{ field: 'fechaCorte', headerName: 'Fecha Corte', width: 120, type: 'date', valueFormatter: (value) => formatDate(value as string) },
|
||||||
|
{ field: 'fechaCierre', headerName: 'Fecha Cierre', width: 170, valueFormatter: (value) => formatDate(value as string) },
|
||||||
|
{ field: 'saldoCierre', headerName: 'Saldo Cierre', width: 140, type: 'number', valueFormatter: (v) => currencyFormatter(v as number) },
|
||||||
|
{ field: 'estado', headerName: 'Estado', width: 100 },
|
||||||
|
{ field: 'justificacion', headerName: 'Justif. Cierre', width: 200, renderCell: (params) => (<Tooltip title={params.value || ''}><Typography noWrap variant="body2">{params.value || '-'}</Typography></Tooltip>) },
|
||||||
|
{ field: 'id_Usuario_Cierre', headerName: 'Usr. Cierre', width: 100, align: 'center', headerAlign: 'center' },
|
||||||
|
{ field: 'id_Usuario_Anula', headerName: 'Usr. Anula', width: 100, align: 'center', headerAlign: 'center', renderCell: (p) => p.value ?? '-' },
|
||||||
|
{ field: 'fechaAnulacion', headerName: 'Fecha Anul.', width: 170, valueFormatter: (v) => v ? formatDate(v as string) : '-' },
|
||||||
|
{ field: 'justificacion_Anulacion', headerName: 'Justif. Anul.', flex: 1, minWidth: 200, renderCell: (params) => (<Tooltip title={params.value || ''}><Typography noWrap variant="body2">{params.value || '-'}</Typography></Tooltip>) },
|
||||||
|
];
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
setError(`La vista de auditoría para '${filtroTipoEntidad}' aún no está implementada.`);
|
setError(`La vista de auditoría para '${filtroTipoEntidad}' aún no está implementada.`);
|
||||||
setDatosAuditoria([]);
|
setDatosAuditoria([]);
|
||||||
|
|||||||
448
Frontend/src/pages/Contables/CierresCuentaCorrientePage.tsx
Normal file
448
Frontend/src/pages/Contables/CierresCuentaCorrientePage.tsx
Normal file
@@ -0,0 +1,448 @@
|
|||||||
|
import React, { useState, useEffect, useCallback } from 'react';
|
||||||
|
import {
|
||||||
|
Box, Typography, Button, Paper, IconButton, Chip, Alert, CircularProgress,
|
||||||
|
Table, TableBody, TableCell, TableContainer, TableHead, TableRow, TablePagination,
|
||||||
|
FormControl, InputLabel, Select, MenuItem, Tabs, Tab, Tooltip
|
||||||
|
} from '@mui/material';
|
||||||
|
import AddIcon from '@mui/icons-material/Add';
|
||||||
|
import LockOpenIcon from '@mui/icons-material/LockOpen';
|
||||||
|
import HistoryIcon from '@mui/icons-material/History';
|
||||||
|
import FilterListIcon from '@mui/icons-material/FilterList';
|
||||||
|
|
||||||
|
import cierresCcService from '../../services/Contables/cierresCcService';
|
||||||
|
import distribuidorService from '../../services/Distribucion/distribuidorService';
|
||||||
|
import empresaService from '../../services/Distribucion/empresaService';
|
||||||
|
|
||||||
|
import type { CierreCuentaCorrienteDto } from '../../models/dtos/Contables/CierreCuentaCorrienteDto';
|
||||||
|
import type { CrearCierreDto } from '../../models/dtos/Contables/CrearCierreDto';
|
||||||
|
import type { ReabrirCierreDto } from '../../models/dtos/Contables/ReabrirCierreDto';
|
||||||
|
import type { CierreCuentaCorrienteHistorialDto } from '../../models/dtos/Auditoria/CierreCuentaCorrienteHistorialDto';
|
||||||
|
import type { DistribuidorDropdownDto } from '../../models/dtos/Distribucion/DistribuidorDropdownDto';
|
||||||
|
import type { EmpresaDropdownDto } from '../../models/dtos/Distribucion/EmpresaDropdownDto';
|
||||||
|
|
||||||
|
import NuevoCierreModal from '../../components/Modals/Contables/NuevoCierreModal';
|
||||||
|
import ReabrirCierreModal from '../../components/Modals/Contables/ReabrirCierreModal';
|
||||||
|
import { usePermissions } from '../../hooks/usePermissions';
|
||||||
|
import axios from 'axios';
|
||||||
|
|
||||||
|
const formatDate = (dateString?: string | null): string => {
|
||||||
|
if (!dateString) return '-';
|
||||||
|
const datePart = dateString.split('T')[0];
|
||||||
|
const parts = datePart.split('-');
|
||||||
|
if (parts.length === 3) return `${parts[2]}/${parts[1]}/${parts[0]}`;
|
||||||
|
return datePart;
|
||||||
|
};
|
||||||
|
|
||||||
|
const formatDateTime = (dateString?: string | null): string => {
|
||||||
|
if (!dateString) return '-';
|
||||||
|
const d = new Date(dateString);
|
||||||
|
if (isNaN(d.getTime())) return dateString;
|
||||||
|
return d.toLocaleString('es-AR', {
|
||||||
|
day: '2-digit',
|
||||||
|
month: '2-digit',
|
||||||
|
year: 'numeric',
|
||||||
|
hour: '2-digit',
|
||||||
|
minute: '2-digit',
|
||||||
|
hour12: false
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// Extrae el codigo de error del backend si viene en el response (PERIODO_CERRADO_BLOQUEO_OPERACION,
|
||||||
|
// CIERRE_FECHA_FUTURA, etc.) y arma un mensaje legible. Si no hay codigo, usa message generico.
|
||||||
|
const parseApiError = (err: unknown, fallback: string): string => {
|
||||||
|
if (axios.isAxiosError(err)) {
|
||||||
|
const data = err.response?.data;
|
||||||
|
if (data && typeof data === 'object') {
|
||||||
|
const codigo = (data as { codigo?: string }).codigo;
|
||||||
|
const mensaje = (data as { mensaje?: string; message?: string }).mensaje
|
||||||
|
|| (data as { mensaje?: string; message?: string }).message;
|
||||||
|
if (mensaje) return codigo ? `[${codigo}] ${mensaje}` : mensaje;
|
||||||
|
if (codigo) return `Error: ${codigo}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return fallback;
|
||||||
|
};
|
||||||
|
|
||||||
|
const CierresCuentaCorrientePage: React.FC = () => {
|
||||||
|
const { tienePermiso, isSuperAdmin } = usePermissions();
|
||||||
|
const puedeVer = isSuperAdmin || tienePermiso('CC003');
|
||||||
|
const puedeCrear = isSuperAdmin || tienePermiso('CC001');
|
||||||
|
// Reapertura: exclusivo SuperAdmin (CC002 no se valida — no es asignable a perfiles).
|
||||||
|
const puedeReabrir = isSuperAdmin;
|
||||||
|
|
||||||
|
const [filtroIdDistribuidor, setFiltroIdDistribuidor] = useState<number | string>('');
|
||||||
|
const [filtroIdEmpresa, setFiltroIdEmpresa] = useState<number | string>('');
|
||||||
|
|
||||||
|
const [distribuidores, setDistribuidores] = useState<DistribuidorDropdownDto[]>([]);
|
||||||
|
const [empresas, setEmpresas] = useState<EmpresaDropdownDto[]>([]);
|
||||||
|
const [loadingDropdowns, setLoadingDropdowns] = useState(false);
|
||||||
|
|
||||||
|
const [cierres, setCierres] = useState<CierreCuentaCorrienteDto[]>([]);
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
const [pageApiErrorMessage, setPageApiErrorMessage] = useState<string | null>(null);
|
||||||
|
const [modalApiErrorMessage, setModalApiErrorMessage] = useState<string | null>(null);
|
||||||
|
|
||||||
|
const [page, setPage] = useState(0);
|
||||||
|
const [rowsPerPage, setRowsPerPage] = useState(25);
|
||||||
|
|
||||||
|
const [tabIndex, setTabIndex] = useState(0);
|
||||||
|
const [cierreSeleccionadoHistorial, setCierreSeleccionadoHistorial] = useState<CierreCuentaCorrienteDto | null>(null);
|
||||||
|
const [historial, setHistorial] = useState<CierreCuentaCorrienteHistorialDto[]>([]);
|
||||||
|
const [loadingHistorial, setLoadingHistorial] = useState(false);
|
||||||
|
|
||||||
|
const [nuevoModalOpen, setNuevoModalOpen] = useState(false);
|
||||||
|
const [reabrirModalOpen, setReabrirModalOpen] = useState(false);
|
||||||
|
const [cierreParaReabrir, setCierreParaReabrir] = useState<CierreCuentaCorrienteDto | null>(null);
|
||||||
|
|
||||||
|
// Carga dropdowns una sola vez al montar
|
||||||
|
useEffect(() => {
|
||||||
|
const fetchDropdowns = async () => {
|
||||||
|
setLoadingDropdowns(true);
|
||||||
|
try {
|
||||||
|
const [distData, empData] = await Promise.all([
|
||||||
|
distribuidorService.getAllDistribuidoresDropdown(),
|
||||||
|
empresaService.getEmpresasDropdown()
|
||||||
|
]);
|
||||||
|
setDistribuidores(distData);
|
||||||
|
setEmpresas(empData);
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
setError('Error al cargar opciones de filtro.');
|
||||||
|
} finally {
|
||||||
|
setLoadingDropdowns(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
fetchDropdowns();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const cargarCierres = useCallback(async () => {
|
||||||
|
if (!puedeVer) {
|
||||||
|
setError('No tiene permiso.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!filtroIdDistribuidor || !filtroIdEmpresa) {
|
||||||
|
setCierres([]);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setLoading(true);
|
||||||
|
setError(null);
|
||||||
|
setPageApiErrorMessage(null);
|
||||||
|
try {
|
||||||
|
const data = await cierresCcService.getAllCierres(
|
||||||
|
Number(filtroIdDistribuidor),
|
||||||
|
Number(filtroIdEmpresa)
|
||||||
|
);
|
||||||
|
setCierres(data);
|
||||||
|
setPage(0);
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
setPageApiErrorMessage(parseApiError(err, 'Error al cargar los cierres.'));
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
}, [puedeVer, filtroIdDistribuidor, filtroIdEmpresa]);
|
||||||
|
|
||||||
|
useEffect(() => { cargarCierres(); }, [cargarCierres]);
|
||||||
|
|
||||||
|
const cargarHistorial = useCallback(async (idCierre: number) => {
|
||||||
|
setLoadingHistorial(true);
|
||||||
|
try {
|
||||||
|
const data = await cierresCcService.getHistorialCierre(idCierre);
|
||||||
|
setHistorial(data);
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
setPageApiErrorMessage(parseApiError(err, 'Error al cargar el historial.'));
|
||||||
|
} finally {
|
||||||
|
setLoadingHistorial(false);
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleSelectCierreHistorial = (cierre: CierreCuentaCorrienteDto) => {
|
||||||
|
setCierreSeleccionadoHistorial(cierre);
|
||||||
|
setTabIndex(1);
|
||||||
|
cargarHistorial(cierre.idCierre);
|
||||||
|
};
|
||||||
|
|
||||||
|
const clearModalApiErrorMessage = useCallback(() => setModalApiErrorMessage(null), []);
|
||||||
|
|
||||||
|
const handleSubmitNuevoCierre = async (data: CrearCierreDto) => {
|
||||||
|
setModalApiErrorMessage(null);
|
||||||
|
try {
|
||||||
|
const creado = await cierresCcService.crearCierre(data);
|
||||||
|
cargarCierres();
|
||||||
|
return creado;
|
||||||
|
} catch (err) {
|
||||||
|
const msg = parseApiError(err, 'Error al crear el cierre.');
|
||||||
|
setModalApiErrorMessage(msg);
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleOpenReabrir = (cierre: CierreCuentaCorrienteDto) => {
|
||||||
|
setCierreParaReabrir(cierre);
|
||||||
|
setReabrirModalOpen(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSubmitReabrir = async (idCierre: number, data: ReabrirCierreDto) => {
|
||||||
|
setModalApiErrorMessage(null);
|
||||||
|
try {
|
||||||
|
const result = await cierresCcService.reabrirCierre(idCierre, data);
|
||||||
|
cargarCierres();
|
||||||
|
if (cierreSeleccionadoHistorial?.idCierre === idCierre) {
|
||||||
|
cargarHistorial(idCierre);
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
} catch (err) {
|
||||||
|
const msg = parseApiError(err, 'Error al reabrir el cierre.');
|
||||||
|
setModalApiErrorMessage(msg);
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleChangePage = (_e: unknown, newPage: number) => setPage(newPage);
|
||||||
|
const handleChangeRowsPerPage = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
setRowsPerPage(parseInt(event.target.value, 10));
|
||||||
|
setPage(0);
|
||||||
|
};
|
||||||
|
|
||||||
|
const displayData = cierres.slice(page * rowsPerPage, page * rowsPerPage + rowsPerPage);
|
||||||
|
|
||||||
|
if (!puedeVer) {
|
||||||
|
return (
|
||||||
|
<Box sx={{ p: 2 }}>
|
||||||
|
<Alert severity="error">No tiene permiso para acceder a esta sección.</Alert>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box sx={{ p: 1 }}>
|
||||||
|
<Typography variant="h5" gutterBottom>Cierres de Cuenta Corriente</Typography>
|
||||||
|
|
||||||
|
<Paper sx={{ p: 2, mb: 2 }}>
|
||||||
|
<Typography variant="h6" gutterBottom>Filtros <FilterListIcon fontSize="small" /></Typography>
|
||||||
|
<Box sx={{ display: 'flex', flexWrap: 'wrap', gap: 2, alignItems: 'center', mb: 2 }}>
|
||||||
|
<FormControl size="small" sx={{ minWidth: 220, flexGrow: 1 }} disabled={loadingDropdowns}>
|
||||||
|
<InputLabel>Distribuidor</InputLabel>
|
||||||
|
<Select
|
||||||
|
value={filtroIdDistribuidor}
|
||||||
|
label="Distribuidor"
|
||||||
|
onChange={(e) => setFiltroIdDistribuidor(e.target.value as number | string)}
|
||||||
|
>
|
||||||
|
<MenuItem value=""><em>Seleccione</em></MenuItem>
|
||||||
|
{distribuidores.map(d => <MenuItem key={d.idDistribuidor} value={d.idDistribuidor}>{d.nombre}</MenuItem>)}
|
||||||
|
</Select>
|
||||||
|
</FormControl>
|
||||||
|
<FormControl size="small" sx={{ minWidth: 200, flexGrow: 1 }} disabled={loadingDropdowns}>
|
||||||
|
<InputLabel>Empresa</InputLabel>
|
||||||
|
<Select
|
||||||
|
value={filtroIdEmpresa}
|
||||||
|
label="Empresa"
|
||||||
|
onChange={(e) => setFiltroIdEmpresa(e.target.value as number | string)}
|
||||||
|
>
|
||||||
|
<MenuItem value=""><em>Seleccione</em></MenuItem>
|
||||||
|
{empresas.map(e => <MenuItem key={e.idEmpresa} value={e.idEmpresa}>{e.nombre}</MenuItem>)}
|
||||||
|
</Select>
|
||||||
|
</FormControl>
|
||||||
|
</Box>
|
||||||
|
{puedeCrear && (
|
||||||
|
<Button
|
||||||
|
variant="contained"
|
||||||
|
startIcon={<AddIcon />}
|
||||||
|
onClick={() => setNuevoModalOpen(true)}
|
||||||
|
disabled={loadingDropdowns}
|
||||||
|
>
|
||||||
|
Nuevo Cierre
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</Paper>
|
||||||
|
|
||||||
|
<Paper sx={{ mb: 2 }}>
|
||||||
|
<Tabs value={tabIndex} onChange={(_e, v) => setTabIndex(v)}>
|
||||||
|
<Tab label="Cierres" />
|
||||||
|
<Tab
|
||||||
|
label={cierreSeleccionadoHistorial
|
||||||
|
? `Historial #${cierreSeleccionadoHistorial.idCierre}`
|
||||||
|
: 'Historial'}
|
||||||
|
disabled={!cierreSeleccionadoHistorial}
|
||||||
|
icon={<HistoryIcon fontSize="small" />}
|
||||||
|
iconPosition="start"
|
||||||
|
/>
|
||||||
|
</Tabs>
|
||||||
|
</Paper>
|
||||||
|
|
||||||
|
{error && <Alert severity="error" sx={{ my: 2 }}>{error}</Alert>}
|
||||||
|
{pageApiErrorMessage && <Alert severity="error" sx={{ my: 2 }}>{pageApiErrorMessage}</Alert>}
|
||||||
|
|
||||||
|
{tabIndex === 0 && (
|
||||||
|
<>
|
||||||
|
{loading && (
|
||||||
|
<Box sx={{ display: 'flex', justifyContent: 'center', my: 2 }}>
|
||||||
|
<CircularProgress />
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
{!loading && (!filtroIdDistribuidor || !filtroIdEmpresa) && (
|
||||||
|
<Alert severity="info">Seleccioná un distribuidor y una empresa para ver los cierres.</Alert>
|
||||||
|
)}
|
||||||
|
{!loading && filtroIdDistribuidor && filtroIdEmpresa && (
|
||||||
|
<TableContainer component={Paper}>
|
||||||
|
<Table size="small">
|
||||||
|
<TableHead>
|
||||||
|
<TableRow>
|
||||||
|
<TableCell>Fecha de Corte</TableCell>
|
||||||
|
<TableCell>Fecha de Cierre</TableCell>
|
||||||
|
<TableCell align="right">Saldo del Cierre</TableCell>
|
||||||
|
<TableCell align="center">Estado</TableCell>
|
||||||
|
<TableCell>Justificación</TableCell>
|
||||||
|
<TableCell>Usuario</TableCell>
|
||||||
|
<TableCell align="right">Acciones</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
</TableHead>
|
||||||
|
<TableBody>
|
||||||
|
{displayData.length === 0 ? (
|
||||||
|
<TableRow><TableCell colSpan={7} align="center">No hay cierres registrados.</TableCell></TableRow>
|
||||||
|
) : (
|
||||||
|
displayData.map((c) => (
|
||||||
|
<TableRow key={c.idCierre} hover>
|
||||||
|
<TableCell>{formatDate(c.fechaCorte)}</TableCell>
|
||||||
|
<TableCell>{formatDateTime(c.fechaCierre)}</TableCell>
|
||||||
|
<TableCell align="right">
|
||||||
|
{c.saldoCierre.toLocaleString('es-AR', { style: 'currency', currency: 'ARS' })}
|
||||||
|
</TableCell>
|
||||||
|
<TableCell align="center">
|
||||||
|
<Chip
|
||||||
|
label={c.estado === 'Activo' && c.esUltimoVigente ? 'Activo (último)' : c.estado}
|
||||||
|
color={c.estado === 'Activo' ? 'success' : 'default'}
|
||||||
|
size="small"
|
||||||
|
/>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
<Tooltip title={c.justificacion || ''}>
|
||||||
|
<Box sx={{ maxWidth: 220, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>
|
||||||
|
{c.justificacion || '-'}
|
||||||
|
</Box>
|
||||||
|
</Tooltip>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell>{c.nombreUsuarioCierre}</TableCell>
|
||||||
|
<TableCell align="right">
|
||||||
|
<Tooltip title="Ver historial">
|
||||||
|
<IconButton size="small" onClick={() => handleSelectCierreHistorial(c)}>
|
||||||
|
<HistoryIcon fontSize="small" />
|
||||||
|
</IconButton>
|
||||||
|
</Tooltip>
|
||||||
|
{puedeReabrir && c.estado === 'Activo' && c.esUltimoVigente && (
|
||||||
|
<Tooltip title="Reabrir cierre">
|
||||||
|
<IconButton
|
||||||
|
size="small"
|
||||||
|
color="warning"
|
||||||
|
onClick={() => handleOpenReabrir(c)}
|
||||||
|
>
|
||||||
|
<LockOpenIcon fontSize="small" />
|
||||||
|
</IconButton>
|
||||||
|
</Tooltip>
|
||||||
|
)}
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
))
|
||||||
|
)}
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
<TablePagination
|
||||||
|
rowsPerPageOptions={[25, 50, 100]}
|
||||||
|
component="div"
|
||||||
|
count={cierres.length}
|
||||||
|
rowsPerPage={rowsPerPage}
|
||||||
|
page={page}
|
||||||
|
onPageChange={handleChangePage}
|
||||||
|
onRowsPerPageChange={handleChangeRowsPerPage}
|
||||||
|
labelRowsPerPage="Filas por página:"
|
||||||
|
/>
|
||||||
|
</TableContainer>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{tabIndex === 1 && cierreSeleccionadoHistorial && (
|
||||||
|
<>
|
||||||
|
<Paper sx={{ p: 2, mb: 2 }}>
|
||||||
|
<Typography variant="body2">
|
||||||
|
Historial del cierre <strong>#{cierreSeleccionadoHistorial.idCierre}</strong>
|
||||||
|
{' — '}{cierreSeleccionadoHistorial.nombreDistribuidor} / {cierreSeleccionadoHistorial.nombreEmpresa}
|
||||||
|
{' — '}corte: {formatDate(cierreSeleccionadoHistorial.fechaCorte)}
|
||||||
|
</Typography>
|
||||||
|
</Paper>
|
||||||
|
{loadingHistorial && (
|
||||||
|
<Box sx={{ display: 'flex', justifyContent: 'center', my: 2 }}>
|
||||||
|
<CircularProgress />
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
{!loadingHistorial && (
|
||||||
|
<TableContainer component={Paper}>
|
||||||
|
<Table size="small">
|
||||||
|
<TableHead>
|
||||||
|
<TableRow>
|
||||||
|
<TableCell>Tipo Modificación</TableCell>
|
||||||
|
<TableCell>Fecha Modificación</TableCell>
|
||||||
|
<TableCell>Usuario</TableCell>
|
||||||
|
<TableCell align="right">Saldo</TableCell>
|
||||||
|
<TableCell align="center">Estado</TableCell>
|
||||||
|
<TableCell>Justificación</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
</TableHead>
|
||||||
|
<TableBody>
|
||||||
|
{historial.length === 0 ? (
|
||||||
|
<TableRow><TableCell colSpan={6} align="center">No hay registros de auditoría.</TableCell></TableRow>
|
||||||
|
) : (
|
||||||
|
historial.map((h) => (
|
||||||
|
<TableRow key={h.id_Historial}>
|
||||||
|
<TableCell>{h.tipoMod}</TableCell>
|
||||||
|
<TableCell>{formatDateTime(h.fechaMod)}</TableCell>
|
||||||
|
<TableCell>{h.nombreUsuarioModifico}</TableCell>
|
||||||
|
<TableCell align="right">
|
||||||
|
{h.saldoCierre.toLocaleString('es-AR', { style: 'currency', currency: 'ARS' })}
|
||||||
|
</TableCell>
|
||||||
|
<TableCell align="center">
|
||||||
|
<Chip label={h.estado} color={h.estado === 'Activo' ? 'success' : 'default'} size="small" />
|
||||||
|
</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
<Tooltip title={h.justificacion_Anulacion || h.justificacion || ''}>
|
||||||
|
<Box sx={{ maxWidth: 220, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>
|
||||||
|
{h.justificacion_Anulacion || h.justificacion || '-'}
|
||||||
|
</Box>
|
||||||
|
</Tooltip>
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
))
|
||||||
|
)}
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
</TableContainer>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<NuevoCierreModal
|
||||||
|
open={nuevoModalOpen}
|
||||||
|
onClose={() => setNuevoModalOpen(false)}
|
||||||
|
onSubmit={handleSubmitNuevoCierre}
|
||||||
|
initialIdDistribuidor={filtroIdDistribuidor ? Number(filtroIdDistribuidor) : null}
|
||||||
|
initialIdEmpresa={filtroIdEmpresa ? Number(filtroIdEmpresa) : null}
|
||||||
|
errorMessage={modalApiErrorMessage}
|
||||||
|
clearErrorMessage={clearModalApiErrorMessage}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<ReabrirCierreModal
|
||||||
|
open={reabrirModalOpen}
|
||||||
|
onClose={() => { setReabrirModalOpen(false); setCierreParaReabrir(null); }}
|
||||||
|
onSubmit={handleSubmitReabrir}
|
||||||
|
cierre={cierreParaReabrir}
|
||||||
|
errorMessage={modalApiErrorMessage}
|
||||||
|
clearErrorMessage={clearModalApiErrorMessage}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default CierresCuentaCorrientePage;
|
||||||
@@ -9,6 +9,7 @@ const contablesSubModules = [
|
|||||||
{ label: 'Notas Crédito/Débito', path: 'notas-cd' },
|
{ label: 'Notas Crédito/Débito', path: 'notas-cd' },
|
||||||
{ label: 'Gestión de Saldos', path: 'gestion-saldos' },
|
{ label: 'Gestión de Saldos', path: 'gestion-saldos' },
|
||||||
{ label: 'Tipos de Pago', path: 'tipos-pago' },
|
{ label: 'Tipos de Pago', path: 'tipos-pago' },
|
||||||
|
{ label: 'Cierres CC', path: 'cierres-cc' },
|
||||||
];
|
];
|
||||||
|
|
||||||
const ContablesIndexPage: React.FC = () => {
|
const ContablesIndexPage: React.FC = () => {
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ import type { EmpresaDto } from '../../models/dtos/Distribucion/EmpresaDto';
|
|||||||
|
|
||||||
import NotaCreditoDebitoFormModal from '../../components/Modals/Contables/NotaCreditoDebitoFormModal';
|
import NotaCreditoDebitoFormModal from '../../components/Modals/Contables/NotaCreditoDebitoFormModal';
|
||||||
import { usePermissions } from '../../hooks/usePermissions';
|
import { usePermissions } from '../../hooks/usePermissions';
|
||||||
import axios from 'axios';
|
import { parseApiError } from '../../utils/apiErrorParser';
|
||||||
|
|
||||||
type DestinoFiltroType = 'Distribuidores' | 'Canillas' | '';
|
type DestinoFiltroType = 'Distribuidores' | 'Canillas' | '';
|
||||||
type TipoNotaFiltroType = 'Credito' | 'Debito' | '';
|
type TipoNotaFiltroType = 'Credito' | 'Debito' | '';
|
||||||
@@ -124,7 +124,7 @@ const GestionarNotasCDPage: React.FC = () => {
|
|||||||
}
|
}
|
||||||
cargarNotas();
|
cargarNotas();
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
const message = axios.isAxiosError(err) && err.response?.data?.message ? err.response.data.message : 'Error al guardar la nota.';
|
const { message } = parseApiError(err, 'Error al guardar la nota.');
|
||||||
setApiErrorMessage(message); throw err;
|
setApiErrorMessage(message); throw err;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -133,7 +133,7 @@ const GestionarNotasCDPage: React.FC = () => {
|
|||||||
if (window.confirm(`¿Seguro de eliminar esta nota (ID: ${idNota})? Esta acción revertirá el impacto en el saldo.`)) {
|
if (window.confirm(`¿Seguro de eliminar esta nota (ID: ${idNota})? Esta acción revertirá el impacto en el saldo.`)) {
|
||||||
setApiErrorMessage(null);
|
setApiErrorMessage(null);
|
||||||
try { await notaCreditoDebitoService.deleteNota(idNota); cargarNotas(); }
|
try { await notaCreditoDebitoService.deleteNota(idNota); cargarNotas(); }
|
||||||
catch (err: any) { const msg = axios.isAxiosError(err) && err.response?.data?.message ? err.response.data.message : 'Error al eliminar.'; setApiErrorMessage(msg); }
|
catch (err: any) { const { message } = parseApiError(err, 'Error al eliminar.'); setApiErrorMessage(message); }
|
||||||
}
|
}
|
||||||
handleMenuClose();
|
handleMenuClose();
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ import type { EmpresaDto } from '../../models/dtos/Distribucion/EmpresaDto';
|
|||||||
|
|
||||||
import PagoDistribuidorFormModal from '../../components/Modals/Contables/PagoDistribuidorFormModal';
|
import PagoDistribuidorFormModal from '../../components/Modals/Contables/PagoDistribuidorFormModal';
|
||||||
import { usePermissions } from '../../hooks/usePermissions';
|
import { usePermissions } from '../../hooks/usePermissions';
|
||||||
import axios from 'axios';
|
import { parseApiError } from '../../utils/apiErrorParser';
|
||||||
|
|
||||||
const GestionarPagosDistribuidorPage: React.FC = () => {
|
const GestionarPagosDistribuidorPage: React.FC = () => {
|
||||||
const [pagos, setPagos] = useState<PagoDistribuidorDto[]>([]);
|
const [pagos, setPagos] = useState<PagoDistribuidorDto[]>([]);
|
||||||
@@ -110,7 +110,7 @@ const GestionarPagosDistribuidorPage: React.FC = () => {
|
|||||||
}
|
}
|
||||||
cargarPagos();
|
cargarPagos();
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
const message = axios.isAxiosError(err) && err.response?.data?.message ? err.response.data.message : 'Error al guardar el pago.';
|
const { message } = parseApiError(err, 'Error al guardar el pago.');
|
||||||
setModalApiErrorMessage(message);
|
setModalApiErrorMessage(message);
|
||||||
throw err;
|
throw err;
|
||||||
}
|
}
|
||||||
@@ -121,8 +121,8 @@ const GestionarPagosDistribuidorPage: React.FC = () => {
|
|||||||
setPageApiErrorMessage(null);
|
setPageApiErrorMessage(null);
|
||||||
try { await pagoDistribuidorService.deletePagoDistribuidor(idPago); cargarPagos(); }
|
try { await pagoDistribuidorService.deletePagoDistribuidor(idPago); cargarPagos(); }
|
||||||
catch (err: any) {
|
catch (err: any) {
|
||||||
const msg = axios.isAxiosError(err) && err.response?.data?.message ? err.response.data.message : 'Error al eliminar.';
|
const { message } = parseApiError(err, 'Error al eliminar.');
|
||||||
setPageApiErrorMessage(msg);
|
setPageApiErrorMessage(message);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
handleMenuClose();
|
handleMenuClose();
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ import type { CanillaDto } from '../../models/dtos/Distribucion/CanillaDto';
|
|||||||
|
|
||||||
import AjusteSaldoModal from '../../components/Modals/Contables/AjusteSaldoModal';
|
import AjusteSaldoModal from '../../components/Modals/Contables/AjusteSaldoModal';
|
||||||
import { usePermissions } from '../../hooks/usePermissions';
|
import { usePermissions } from '../../hooks/usePermissions';
|
||||||
import axios from 'axios';
|
import { parseApiError } from '../../utils/apiErrorParser';
|
||||||
|
|
||||||
type TipoDestinoFiltro = 'Distribuidores' | 'Canillas' | '';
|
type TipoDestinoFiltro = 'Distribuidores' | 'Canillas' | '';
|
||||||
|
|
||||||
@@ -48,7 +48,7 @@ const GestionarSaldosPage: React.FC = () => {
|
|||||||
|
|
||||||
const { tienePermiso, isSuperAdmin } = usePermissions();
|
const { tienePermiso, isSuperAdmin } = usePermissions();
|
||||||
const puedeVerSaldos = isSuperAdmin || tienePermiso("CS001"); // Permiso para ver
|
const puedeVerSaldos = isSuperAdmin || tienePermiso("CS001"); // Permiso para ver
|
||||||
const puedeAjustarSaldos = isSuperAdmin || tienePermiso("CS002"); // Permiso para ajustar
|
const puedeAjustarSaldos = isSuperAdmin; // Ajuste manual: exclusivo SuperAdmin (no se valida CS002)
|
||||||
|
|
||||||
|
|
||||||
const fetchDropdownData = useCallback(async () => {
|
const fetchDropdownData = useCallback(async () => {
|
||||||
@@ -128,9 +128,7 @@ const GestionarSaldosPage: React.FC = () => {
|
|||||||
await saldoService.ajustarSaldo(data);
|
await saldoService.ajustarSaldo(data);
|
||||||
cargarSaldos(); // Recargar lista para ver el saldo actualizado
|
cargarSaldos(); // Recargar lista para ver el saldo actualizado
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
const message = axios.isAxiosError(err) && err.response?.data?.message
|
const { message } = parseApiError(err, 'Error al aplicar el ajuste de saldo.');
|
||||||
? err.response.data.message
|
|
||||||
: 'Error al aplicar el ajuste de saldo.';
|
|
||||||
setApiErrorMessage(message);
|
setApiErrorMessage(message);
|
||||||
throw err; // Para que el modal sepa que hubo error
|
throw err; // Para que el modal sepa que hubo error
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ import type { DistribuidorDropdownDto } from '../../models/dtos/Distribucion/Dis
|
|||||||
|
|
||||||
import EntradaSalidaDistFormModal from '../../components/Modals/Distribucion/EntradaSalidaDistFormModal';
|
import EntradaSalidaDistFormModal from '../../components/Modals/Distribucion/EntradaSalidaDistFormModal';
|
||||||
import { usePermissions } from '../../hooks/usePermissions';
|
import { usePermissions } from '../../hooks/usePermissions';
|
||||||
import axios from 'axios';
|
import { parseApiError } from '../../utils/apiErrorParser';
|
||||||
|
|
||||||
const GestionarEntradasSalidasDistPage: React.FC = () => {
|
const GestionarEntradasSalidasDistPage: React.FC = () => {
|
||||||
const [movimientos, setMovimientos] = useState<EntradaSalidaDistDto[]>([]);
|
const [movimientos, setMovimientos] = useState<EntradaSalidaDistDto[]>([]);
|
||||||
@@ -115,7 +115,7 @@ const GestionarEntradasSalidasDistPage: React.FC = () => {
|
|||||||
}
|
}
|
||||||
cargarMovimientos();
|
cargarMovimientos();
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
const message = axios.isAxiosError(err) && err.response?.data?.message ? err.response.data.message : 'Error al guardar.';
|
const { message } = parseApiError(err, 'Error al guardar.');
|
||||||
setApiErrorMessage(message); throw err;
|
setApiErrorMessage(message); throw err;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -124,7 +124,7 @@ const GestionarEntradasSalidasDistPage: React.FC = () => {
|
|||||||
if (window.confirm(`¿Seguro (ID: ${idParte})? Esto revertirá el saldo.`)) {
|
if (window.confirm(`¿Seguro (ID: ${idParte})? Esto revertirá el saldo.`)) {
|
||||||
setApiErrorMessage(null);
|
setApiErrorMessage(null);
|
||||||
try { await entradaSalidaDistService.deleteEntradaSalidaDist(idParte); cargarMovimientos(); }
|
try { await entradaSalidaDistService.deleteEntradaSalidaDist(idParte); cargarMovimientos(); }
|
||||||
catch (err: any) { const msg = axios.isAxiosError(err) && err.response?.data?.message ? err.response.data.message : 'Error al eliminar.'; setApiErrorMessage(msg); }
|
catch (err: any) { const { message } = parseApiError(err, 'Error al eliminar.'); setApiErrorMessage(message); }
|
||||||
}
|
}
|
||||||
handleMenuClose();
|
handleMenuClose();
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -63,8 +63,9 @@ const ReporteCuentasDistribuidoresPage: React.FC = () => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const movs = procesarLista(data.entradasSalidas, 'mov', 0);
|
const baseInicial = data.saldoInicial ?? 0;
|
||||||
const ultimoMov = movs.length ? movs[movs.length - 1].saldoAcumulado : 0;
|
const movs = procesarLista(data.entradasSalidas, 'mov', baseInicial);
|
||||||
|
const ultimoMov = movs.length ? movs[movs.length - 1].saldoAcumulado : baseInicial;
|
||||||
const notas = procesarLista(data.debitosCreditos, 'nota', ultimoMov);
|
const notas = procesarLista(data.debitosCreditos, 'nota', ultimoMov);
|
||||||
const ultimoNota = notas.length ? notas[notas.length - 1].saldoAcumulado : ultimoMov;
|
const ultimoNota = notas.length ? notas[notas.length - 1].saldoAcumulado : ultimoMov;
|
||||||
const pagos = procesarLista(data.pagos, 'pago', ultimoNota);
|
const pagos = procesarLista(data.pagos, 'pago', ultimoNota);
|
||||||
@@ -210,7 +211,7 @@ const ReporteCuentasDistribuidoresPage: React.FC = () => {
|
|||||||
<GridFooter sx={{ borderTop: 'none' }} />
|
<GridFooter sx={{ borderTop: 'none' }} />
|
||||||
<Box sx={{ p: 1, fontWeight: 'bold' }}>
|
<Box sx={{ p: 1, fontWeight: 'bold' }}>
|
||||||
<Typography variant="subtitle2" component="span" sx={{ mr: 2 }}>
|
<Typography variant="subtitle2" component="span" sx={{ mr: 2 }}>
|
||||||
TOTA DEBE: <strong>{totalDebe.toLocaleString('es-AR', { style: 'currency', currency: 'ARS' })}</strong>
|
TOTAL DEBE: <strong>{totalDebe.toLocaleString('es-AR', { style: 'currency', currency: 'ARS' })}</strong>
|
||||||
</Typography>
|
</Typography>
|
||||||
<Typography variant="subtitle2" component="span">
|
<Typography variant="subtitle2" component="span">
|
||||||
TOTAL HABER: <strong>{totalHaber.toLocaleString('es-AR', { style: 'currency', currency: 'ARS' })}</strong>
|
TOTAL HABER: <strong>{totalHaber.toLocaleString('es-AR', { style: 'currency', currency: 'ARS' })}</strong>
|
||||||
@@ -284,39 +285,44 @@ const ReporteCuentasDistribuidoresPage: React.FC = () => {
|
|||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const handleExportToExcel = useCallback(() => {
|
const handleExportToExcel = useCallback(() => {
|
||||||
if (
|
if (!originalReportData) {
|
||||||
!originalReportData ||
|
alert('No hay datos para exportar.');
|
||||||
(movimientosConSaldo.length === 0 &&
|
return;
|
||||||
notasConSaldo.length === 0 &&
|
|
||||||
pagosConSaldo.length === 0)
|
|
||||||
) {
|
|
||||||
alert("No hay datos para exportar."); // O un mensaje más amigable
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const wb = XLSX.utils.book_new();// Se crea un nuevo libro
|
const sumDebe = (rows: Array<{ debe?: number }>) => rows.reduce((s, r) => s + (r.debe || 0), 0);
|
||||||
|
const sumHaber = (rows: Array<{ haber?: number }>) => rows.reduce((s, r) => s + (r.haber || 0), 0);
|
||||||
|
|
||||||
// Movimientos
|
const totalDebeAll = sumDebe(movimientosConSaldo) + sumDebe(notasConSaldo) + sumDebe(pagosConSaldo);
|
||||||
if (movimientosConSaldo.length) { // <--- CHEQUEO 1
|
const totalHaberAll = sumHaber(movimientosConSaldo) + sumHaber(notasConSaldo) + sumHaber(pagosConSaldo);
|
||||||
// Si movimientosConSaldo está vacío, esta hoja no se añade
|
const saldoInicialExp = originalReportData.saldoInicial ?? 0;
|
||||||
|
const saldoFinalExp = saldoInicialExp + totalDebeAll - totalHaberAll;
|
||||||
|
|
||||||
|
const wb = XLSX.utils.book_new();
|
||||||
|
|
||||||
|
// Hoja Resumen — siempre presente, con saldo inicial y final aunque no haya movimientos
|
||||||
|
const resumenRows = [
|
||||||
|
{ Concepto: 'Saldo Inicial', Monto: saldoInicialExp },
|
||||||
|
{ Concepto: 'Total Debe', Monto: totalDebeAll },
|
||||||
|
{ Concepto: 'Total Haber', Monto: totalHaberAll },
|
||||||
|
{ Concepto: 'Saldo Final', Monto: saldoFinalExp },
|
||||||
|
];
|
||||||
|
const wsResumen = XLSX.utils.json_to_sheet(resumenRows);
|
||||||
|
XLSX.utils.book_append_sheet(wb, wsResumen, 'Resumen');
|
||||||
|
|
||||||
|
if (movimientosConSaldo.length) {
|
||||||
const ws = XLSX.utils.json_to_sheet(movimientosConSaldo.map(({ id, ...rest }) => rest));
|
const ws = XLSX.utils.json_to_sheet(movimientosConSaldo.map(({ id, ...rest }) => rest));
|
||||||
XLSX.utils.book_append_sheet(wb, ws, 'Movimientos');
|
XLSX.utils.book_append_sheet(wb, ws, 'Movimientos');
|
||||||
}
|
}
|
||||||
// Notas
|
if (notasConSaldo.length) {
|
||||||
if (notasConSaldo.length) { // <--- CHEQUEO 2
|
|
||||||
// Si notasConSaldo está vacío, esta hoja no se añade
|
|
||||||
const ws = XLSX.utils.json_to_sheet(notasConSaldo.map(({ id, ...rest }) => rest));
|
const ws = XLSX.utils.json_to_sheet(notasConSaldo.map(({ id, ...rest }) => rest));
|
||||||
XLSX.utils.book_append_sheet(wb, ws, 'Notas');
|
XLSX.utils.book_append_sheet(wb, ws, 'Notas');
|
||||||
}
|
}
|
||||||
// Pagos
|
if (pagosConSaldo.length) {
|
||||||
if (pagosConSaldo.length) { // <--- CHEQUEO 3
|
|
||||||
// Si pagosConSaldo está vacío, esta hoja no se añade
|
|
||||||
const ws = XLSX.utils.json_to_sheet(pagosConSaldo.map(({ id, ...rest }) => rest));
|
const ws = XLSX.utils.json_to_sheet(pagosConSaldo.map(({ id, ...rest }) => rest));
|
||||||
XLSX.utils.book_append_sheet(wb, ws, 'Pagos');
|
XLSX.utils.book_append_sheet(wb, ws, 'Pagos');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Si ninguno de los arrays tiene datos, el libro 'wb' quedará vacío.
|
|
||||||
// Y la siguiente línea dará el error:
|
|
||||||
XLSX.writeFile(wb, `Reporte_${Date.now()}.xlsx`);
|
XLSX.writeFile(wb, `Reporte_${Date.now()}.xlsx`);
|
||||||
}, [originalReportData, movimientosConSaldo, notasConSaldo, pagosConSaldo]);
|
}, [originalReportData, movimientosConSaldo, notasConSaldo, pagosConSaldo]);
|
||||||
|
|
||||||
@@ -355,7 +361,8 @@ const ReporteCuentasDistribuidoresPage: React.FC = () => {
|
|||||||
const totalMov = movimientosConSaldo.reduce((sum, r) => sum + ((r.debe || 0) - (r.haber || 0)), 0);
|
const totalMov = movimientosConSaldo.reduce((sum, r) => sum + ((r.debe || 0) - (r.haber || 0)), 0);
|
||||||
const totalNot = notasConSaldo.reduce((sum, r) => sum + ((r.debe || 0) - (r.haber || 0)), 0);
|
const totalNot = notasConSaldo.reduce((sum, r) => sum + ((r.debe || 0) - (r.haber || 0)), 0);
|
||||||
const totalPag = pagosConSaldo.reduce((sum, r) => sum + ((r.debe || 0) - (r.haber || 0)), 0);
|
const totalPag = pagosConSaldo.reduce((sum, r) => sum + ((r.debe || 0) - (r.haber || 0)), 0);
|
||||||
const saldoInicial = originalReportData?.saldos?.[0]?.monto || 0;
|
const saldoInicial = originalReportData?.saldoInicial ?? 0;
|
||||||
|
const saldoFinal = saldoInicial + totalMov + totalNot + totalPag;
|
||||||
|
|
||||||
const cols = generarColumns();
|
const cols = generarColumns();
|
||||||
|
|
||||||
@@ -385,7 +392,7 @@ const ReporteCuentasDistribuidoresPage: React.FC = () => {
|
|||||||
<Typography>Distribuidor: <strong>{currentParams?.nombreDistribuidor}</strong></Typography>
|
<Typography>Distribuidor: <strong>{currentParams?.nombreDistribuidor}</strong></Typography>
|
||||||
<Typography>Empresa: <strong>{currentParams?.nombreEmpresa}</strong></Typography>
|
<Typography>Empresa: <strong>{currentParams?.nombreEmpresa}</strong></Typography>
|
||||||
<Typography>Período: <strong>{currentParams?.fechaDesde}</strong> al <strong>{currentParams?.fechaHasta}</strong></Typography>
|
<Typography>Período: <strong>{currentParams?.fechaDesde}</strong> al <strong>{currentParams?.fechaHasta}</strong></Typography>
|
||||||
<Typography>Saldo a la Fecha {new Date().toLocaleDateString('es-AR')}: <strong>{saldoInicial.toLocaleString('es-AR', { style: 'currency', currency: 'ARS' })}</strong></Typography>
|
<Typography>Saldo Inicial del Período: <strong>{saldoInicial.toLocaleString('es-AR', { style: 'currency', currency: 'ARS' })}</strong></Typography>
|
||||||
</Paper>
|
</Paper>
|
||||||
|
|
||||||
<Typography variant="h6" sx={{ mt: 2 }}>Movimientos de Entrada / Salida</Typography>
|
<Typography variant="h6" sx={{ mt: 2 }}>Movimientos de Entrada / Salida</Typography>
|
||||||
@@ -399,11 +406,12 @@ const ReporteCuentasDistribuidoresPage: React.FC = () => {
|
|||||||
|
|
||||||
<Paper sx={{ p: 2, mt: 3 }}>
|
<Paper sx={{ p: 2, mt: 3 }}>
|
||||||
<Typography variant="h6">Resumen Final</Typography>
|
<Typography variant="h6">Resumen Final</Typography>
|
||||||
|
<Typography>Saldo Inicial: <strong>{saldoInicial.toLocaleString('es-AR', { style: 'currency', currency: 'ARS' })}</strong></Typography>
|
||||||
<Typography>Movimientos (Debe - Haber): {totalMov.toLocaleString('es-AR', { style: 'currency', currency: 'ARS' })}</Typography>
|
<Typography>Movimientos (Debe - Haber): {totalMov.toLocaleString('es-AR', { style: 'currency', currency: 'ARS' })}</Typography>
|
||||||
<Typography>Notas C/D (Debe - Haber): {totalNot.toLocaleString('es-AR', { style: 'currency', currency: 'ARS' })}</Typography>
|
<Typography>Notas C/D (Debe - Haber): {totalNot.toLocaleString('es-AR', { style: 'currency', currency: 'ARS' })}</Typography>
|
||||||
<Typography>Pagos (Debe - Haber): {totalPag.toLocaleString('es-AR', { style: 'currency', currency: 'ARS' })}</Typography>
|
<Typography>Pagos (Debe - Haber): {totalPag.toLocaleString('es-AR', { style: 'currency', currency: 'ARS' })}</Typography>
|
||||||
<Typography variant="subtitle1" sx={{ fontWeight: 'bold', mt: 1 }}>
|
<Typography variant="subtitle1" sx={{ fontWeight: 'bold', mt: 1 }}>
|
||||||
Saldo Final del Período: {(totalMov + totalNot + totalPag).toLocaleString('es-AR', { style: 'currency', currency: 'ARS' })}
|
Saldo Final: {saldoFinal.toLocaleString('es-AR', { style: 'currency', currency: 'ARS' })}
|
||||||
</Typography>
|
</Typography>
|
||||||
</Paper>
|
</Paper>
|
||||||
</Box>
|
</Box>
|
||||||
|
|||||||
@@ -1,12 +1,14 @@
|
|||||||
import React, { useState, useEffect } from 'react';
|
import React, { useState, useEffect } from 'react';
|
||||||
import {
|
import {
|
||||||
Box, Typography, TextField, Button, CircularProgress, Alert,
|
Box, Typography, TextField, Button, CircularProgress, Alert,
|
||||||
FormControl, InputLabel, Select, MenuItem
|
FormControl, InputLabel, Select, MenuItem, Tooltip
|
||||||
} from '@mui/material';
|
} from '@mui/material';
|
||||||
|
import EventAvailableIcon from '@mui/icons-material/EventAvailable';
|
||||||
import type { DistribuidorDropdownDto } from '../../models/dtos/Distribucion/DistribuidorDropdownDto';
|
import type { DistribuidorDropdownDto } from '../../models/dtos/Distribucion/DistribuidorDropdownDto';
|
||||||
import distribuidorService from '../../services/Distribucion/distribuidorService';
|
import distribuidorService from '../../services/Distribucion/distribuidorService';
|
||||||
import type { EmpresaDropdownDto } from '../../models/dtos/Distribucion/EmpresaDropdownDto';
|
import type { EmpresaDropdownDto } from '../../models/dtos/Distribucion/EmpresaDropdownDto';
|
||||||
import empresaService from '../../services/Distribucion/empresaService';
|
import empresaService from '../../services/Distribucion/empresaService';
|
||||||
|
import cierresCcService from '../../services/Contables/cierresCcService';
|
||||||
|
|
||||||
interface SeleccionaReporteCuentasDistribuidoresProps {
|
interface SeleccionaReporteCuentasDistribuidoresProps {
|
||||||
onGenerarReporte: (params: {
|
onGenerarReporte: (params: {
|
||||||
@@ -20,6 +22,18 @@ interface SeleccionaReporteCuentasDistribuidoresProps {
|
|||||||
apiErrorMessage?: string | null;
|
apiErrorMessage?: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Suma 1 día a una fecha en formato yyyy-MM-dd y devuelve la fecha resultante
|
||||||
|
// también en yyyy-MM-dd. Usado por el atajo "Desde último cierre" para arrancar
|
||||||
|
// el reporte el día siguiente al cierre.
|
||||||
|
const addOneDay = (yyyyMmDd: string): string => {
|
||||||
|
const d = new Date(yyyyMmDd + 'T00:00:00');
|
||||||
|
d.setDate(d.getDate() + 1);
|
||||||
|
const year = d.getFullYear();
|
||||||
|
const month = String(d.getMonth() + 1).padStart(2, '0');
|
||||||
|
const day = String(d.getDate()).padStart(2, '0');
|
||||||
|
return `${year}-${month}-${day}`;
|
||||||
|
};
|
||||||
|
|
||||||
const SeleccionaReporteCuentasDistribuidores: React.FC<SeleccionaReporteCuentasDistribuidoresProps> = ({
|
const SeleccionaReporteCuentasDistribuidores: React.FC<SeleccionaReporteCuentasDistribuidoresProps> = ({
|
||||||
onGenerarReporte,
|
onGenerarReporte,
|
||||||
isLoading,
|
isLoading,
|
||||||
@@ -35,15 +49,19 @@ const SeleccionaReporteCuentasDistribuidores: React.FC<SeleccionaReporteCuentasD
|
|||||||
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 }>({});
|
||||||
|
|
||||||
|
const [loadingUltimoCierre, setLoadingUltimoCierre] = useState(false);
|
||||||
|
const [hayCierrePrevio, setHayCierrePrevio] = useState<boolean | null>(null);
|
||||||
|
const [infoCierre, setInfoCierre] = useState<string | null>(null);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const fetchData = async () => {
|
const fetchData = async () => {
|
||||||
setLoadingDropdowns(true);
|
setLoadingDropdowns(true);
|
||||||
try {
|
try {
|
||||||
const [distData, empData] = await Promise.all([
|
const [distData, empData] = await Promise.all([
|
||||||
distribuidorService.getAllDistribuidoresDropdown(), // Asume que este servicio existe
|
distribuidorService.getAllDistribuidoresDropdown(),
|
||||||
empresaService.getEmpresasDropdown() // Asume que este servicio existe
|
empresaService.getEmpresasDropdown()
|
||||||
]);
|
]);
|
||||||
setDistribuidores(distData.map(d => d)); // El servicio devuelve tupla
|
setDistribuidores(distData.map(d => d));
|
||||||
setEmpresas(empData);
|
setEmpresas(empData);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error al cargar datos:", error);
|
console.error("Error al cargar datos:", error);
|
||||||
@@ -55,6 +73,13 @@ const SeleccionaReporteCuentasDistribuidores: React.FC<SeleccionaReporteCuentasD
|
|||||||
fetchData();
|
fetchData();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
// Reset del estado del atajo cuando cambian distribuidor/empresa: el "ultimo cierre"
|
||||||
|
// depende del par (distribuidor, empresa), si alguno cambia hay que volver a chequear.
|
||||||
|
useEffect(() => {
|
||||||
|
setHayCierrePrevio(null);
|
||||||
|
setInfoCierre(null);
|
||||||
|
}, [idDistribuidor, idEmpresa]);
|
||||||
|
|
||||||
const validate = (): boolean => {
|
const validate = (): boolean => {
|
||||||
const errors: { [key: string]: string | null } = {};
|
const errors: { [key: string]: string | null } = {};
|
||||||
if (!idDistribuidor) errors.idDistribuidor = 'Debe seleccionar un distribuidor.';
|
if (!idDistribuidor) errors.idDistribuidor = 'Debe seleccionar un distribuidor.';
|
||||||
@@ -78,6 +103,37 @@ const SeleccionaReporteCuentasDistribuidores: React.FC<SeleccionaReporteCuentasD
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleDesdeUltimoCierre = async () => {
|
||||||
|
if (!idDistribuidor || !idEmpresa) return;
|
||||||
|
setLoadingUltimoCierre(true);
|
||||||
|
setInfoCierre(null);
|
||||||
|
try {
|
||||||
|
const ultimo = await cierresCcService.getUltimoCierre(Number(idDistribuidor), Number(idEmpresa));
|
||||||
|
if (ultimo === null) {
|
||||||
|
setHayCierrePrevio(false);
|
||||||
|
setInfoCierre('Sin cierres previos para este distribuidor y empresa.');
|
||||||
|
} else {
|
||||||
|
const nuevaFechaDesde = addOneDay(ultimo.fechaCorte);
|
||||||
|
setFechaDesde(nuevaFechaDesde);
|
||||||
|
setLocalErrors(p => ({ ...p, fechaDesde: null, fechaHasta: null }));
|
||||||
|
setHayCierrePrevio(true);
|
||||||
|
setInfoCierre(`Último cierre: ${ultimo.fechaCorte}. Fecha Desde ajustada al día siguiente.`);
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Error al obtener último cierre:', err);
|
||||||
|
setInfoCierre('Error al consultar el último cierre.');
|
||||||
|
} finally {
|
||||||
|
setLoadingUltimoCierre(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const atajoDisabled = !idDistribuidor || !idEmpresa || loadingUltimoCierre || isLoading || hayCierrePrevio === false;
|
||||||
|
const atajoTooltip = !idDistribuidor || !idEmpresa
|
||||||
|
? 'Seleccioná distribuidor y empresa primero.'
|
||||||
|
: hayCierrePrevio === false
|
||||||
|
? 'Sin cierres previos.'
|
||||||
|
: 'Autocompleta Fecha Desde con el día siguiente al último cierre.';
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box sx={{ p: 2, border: '1px solid #ccc', borderRadius: '4px', minWidth: 380 }}>
|
<Box sx={{ p: 2, border: '1px solid #ccc', borderRadius: '4px', minWidth: 380 }}>
|
||||||
<Typography variant="h6" gutterBottom>
|
<Typography variant="h6" gutterBottom>
|
||||||
@@ -115,19 +171,42 @@ const SeleccionaReporteCuentasDistribuidores: React.FC<SeleccionaReporteCuentasD
|
|||||||
{localErrors.idEmpresa && <Typography color="error" variant="caption" sx={{ml:1.5}}>{localErrors.idEmpresa}</Typography>}
|
{localErrors.idEmpresa && <Typography color="error" variant="caption" sx={{ml:1.5}}>{localErrors.idEmpresa}</Typography>}
|
||||||
</FormControl>
|
</FormControl>
|
||||||
|
|
||||||
<TextField
|
<Box sx={{ display: 'flex', gap: 1, alignItems: 'flex-start' }}>
|
||||||
label="Fecha Desde"
|
<TextField
|
||||||
type="date"
|
label="Fecha Desde"
|
||||||
value={fechaDesde}
|
type="date"
|
||||||
onChange={(e) => { setFechaDesde(e.target.value); setLocalErrors(p => ({ ...p, fechaDesde: null, fechaHasta: null })); }}
|
value={fechaDesde}
|
||||||
margin="normal"
|
onChange={(e) => { setFechaDesde(e.target.value); setLocalErrors(p => ({ ...p, fechaDesde: null, fechaHasta: null })); }}
|
||||||
fullWidth
|
margin="normal"
|
||||||
required
|
fullWidth
|
||||||
error={!!localErrors.fechaDesde}
|
required
|
||||||
helperText={localErrors.fechaDesde}
|
error={!!localErrors.fechaDesde}
|
||||||
disabled={isLoading}
|
helperText={localErrors.fechaDesde}
|
||||||
InputLabelProps={{ shrink: true }}
|
disabled={isLoading}
|
||||||
/>
|
InputLabelProps={{ shrink: true }}
|
||||||
|
/>
|
||||||
|
<Tooltip title={atajoTooltip}>
|
||||||
|
<span>
|
||||||
|
<Button
|
||||||
|
variant="outlined"
|
||||||
|
size="small"
|
||||||
|
startIcon={loadingUltimoCierre ? <CircularProgress size={16} /> : <EventAvailableIcon />}
|
||||||
|
onClick={handleDesdeUltimoCierre}
|
||||||
|
disabled={atajoDisabled}
|
||||||
|
sx={{ mt: 2, whiteSpace: 'nowrap' }}
|
||||||
|
>
|
||||||
|
Desde último cierre
|
||||||
|
</Button>
|
||||||
|
</span>
|
||||||
|
</Tooltip>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
{infoCierre && (
|
||||||
|
<Alert severity={hayCierrePrevio ? 'success' : 'info'} sx={{ mt: 1 }}>
|
||||||
|
{infoCierre}
|
||||||
|
</Alert>
|
||||||
|
)}
|
||||||
|
|
||||||
<TextField
|
<TextField
|
||||||
label="Fecha Hasta"
|
label="Fecha Hasta"
|
||||||
type="date"
|
type="date"
|
||||||
|
|||||||
@@ -14,6 +14,10 @@ import PermisosChecklist from '../../components/Modals/Usuarios/PermisosChecklis
|
|||||||
|
|
||||||
const SECCION_PERMISSIONS_PREFIX = "SS";
|
const SECCION_PERMISSIONS_PREFIX = "SS";
|
||||||
|
|
||||||
|
// Permisos exclusivos de SuperAdmin: no se asignan a perfiles ni se muestran en la UI de asignación.
|
||||||
|
// CS002 = Ajuste manual de saldo. CC002 = Reapertura de cierres de cuenta corriente.
|
||||||
|
const PERMISOS_SOLO_SUPERADMIN: ReadonlySet<string> = new Set(["CS002", "CC002"]);
|
||||||
|
|
||||||
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 === "SS007") return "Suscripciones";
|
||||||
@@ -44,7 +48,8 @@ const getModuloConceptualDelPermiso = (permisoModulo: string): string => {
|
|||||||
}
|
}
|
||||||
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") ||
|
||||||
@@ -104,9 +109,12 @@ const AsignarPermisosAPerfilPage: React.FC = () => {
|
|||||||
perfilService.getPerfilById(idPerfilNum),
|
perfilService.getPerfilById(idPerfilNum),
|
||||||
perfilService.getPermisosPorPerfil(idPerfilNum) // Esto devuelve todos los permisos con su estado 'asignado'
|
perfilService.getPermisosPorPerfil(idPerfilNum) // Esto devuelve todos los permisos con su estado 'asignado'
|
||||||
]);
|
]);
|
||||||
|
// Filtrar permisos exclusivos de SuperAdmin (CS002, CC002) para que no aparezcan asignables.
|
||||||
|
// No se altera la asignación existente en la DB — solo se ocultan de esta UI.
|
||||||
|
const permisosVisibles = permisosData.filter(p => !PERMISOS_SOLO_SUPERADMIN.has(p.codAcc));
|
||||||
setPerfil(perfilData);
|
setPerfil(perfilData);
|
||||||
setPermisosDisponibles(permisosData);
|
setPermisosDisponibles(permisosVisibles);
|
||||||
setPermisosSeleccionados(new Set(permisosData.filter(p => p.asignado).map(p => p.id)));
|
setPermisosSeleccionados(new Set(permisosVisibles.filter(p => p.asignado).map(p => p.id)));
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(err);
|
console.error(err);
|
||||||
setError('Error al cargar datos del perfil o permisos.');
|
setError('Error al cargar datos del perfil o permisos.');
|
||||||
|
|||||||
@@ -39,6 +39,7 @@ import GestionarTiposPagoPage from '../pages/Contables/GestionarTiposPagoPage';
|
|||||||
import GestionarPagosDistribuidorPage from '../pages/Contables/GestionarPagosDistribuidorPage';
|
import GestionarPagosDistribuidorPage from '../pages/Contables/GestionarPagosDistribuidorPage';
|
||||||
import GestionarNotasCDPage from '../pages/Contables/GestionarNotasCDPage';
|
import GestionarNotasCDPage from '../pages/Contables/GestionarNotasCDPage';
|
||||||
import GestionarSaldosPage from '../pages/Contables/GestionarSaldosPage';
|
import GestionarSaldosPage from '../pages/Contables/GestionarSaldosPage';
|
||||||
|
import CierresCuentaCorrientePage from '../pages/Contables/CierresCuentaCorrientePage';
|
||||||
|
|
||||||
// Usuarios
|
// Usuarios
|
||||||
import UsuariosIndexPage from '../pages/Usuarios/UsuariosIndexPage'; // Crear este componente
|
import UsuariosIndexPage from '../pages/Usuarios/UsuariosIndexPage'; // Crear este componente
|
||||||
@@ -241,6 +242,14 @@ const AppRoutes = () => {
|
|||||||
<Route path="pagos-distribuidores" element={<GestionarPagosDistribuidorPage />} />
|
<Route path="pagos-distribuidores" element={<GestionarPagosDistribuidorPage />} />
|
||||||
<Route path="notas-cd" element={<GestionarNotasCDPage />} />
|
<Route path="notas-cd" element={<GestionarNotasCDPage />} />
|
||||||
<Route path="gestion-saldos" element={<GestionarSaldosPage />} />
|
<Route path="gestion-saldos" element={<GestionarSaldosPage />} />
|
||||||
|
<Route
|
||||||
|
path="cierres-cc"
|
||||||
|
element={
|
||||||
|
<SectionProtectedRoute requiredPermission="CC003" sectionName="Cierres de Cuenta Corriente">
|
||||||
|
<CierresCuentaCorrientePage />
|
||||||
|
</SectionProtectedRoute>
|
||||||
|
}
|
||||||
|
/>
|
||||||
</Route>
|
</Route>
|
||||||
|
|
||||||
{/* Módulo de Impresión (anidado) */}
|
{/* Módulo de Impresión (anidado) */}
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ import type { PerfilHistorialDto } from '../../models/dtos/Auditoria/PerfilHisto
|
|||||||
import type { PermisoHistorialDto } from '../../models/dtos/Auditoria/PermisoHistorialDto';
|
import type { PermisoHistorialDto } from '../../models/dtos/Auditoria/PermisoHistorialDto';
|
||||||
import type { PermisosPerfilesHistorialDto } from '../../models/dtos/Auditoria/PermisosPerfilesHistorialDto';
|
import type { PermisosPerfilesHistorialDto } from '../../models/dtos/Auditoria/PermisosPerfilesHistorialDto';
|
||||||
import type { CambioParadaHistorialDto } from '../../models/dtos/Auditoria/CambioParadaHistorialDto';
|
import type { CambioParadaHistorialDto } from '../../models/dtos/Auditoria/CambioParadaHistorialDto';
|
||||||
|
import type { CierreCuentaCorrienteHistorialDto } from '../../models/dtos/Auditoria/CierreCuentaCorrienteHistorialDto';
|
||||||
|
|
||||||
interface HistorialParamsComunes {
|
interface HistorialParamsComunes {
|
||||||
fechaDesde?: string; // "yyyy-MM-dd"
|
fechaDesde?: string; // "yyyy-MM-dd"
|
||||||
@@ -41,6 +42,10 @@ interface HistorialCambiosParadaParams extends HistorialParamsComunes {
|
|||||||
idCanillaAfectado?: number;
|
idCanillaAfectado?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface HistorialCierresCCParams extends HistorialParamsComunes {
|
||||||
|
idCierreAfectado?: number;
|
||||||
|
}
|
||||||
|
|
||||||
interface HistorialPermisosPerfilesParams extends HistorialParamsComunes {
|
interface HistorialPermisosPerfilesParams extends HistorialParamsComunes {
|
||||||
idPerfilAfectado?: number;
|
idPerfilAfectado?: number;
|
||||||
idPermisoAfectado?: number;
|
idPermisoAfectado?: number;
|
||||||
@@ -467,6 +472,15 @@ const getHistorialCambiosParada = async (params: HistorialCambiosParadaParams):
|
|||||||
return response.data;
|
return response.data;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const getHistorialCierresCC = async (params: HistorialCierresCCParams): Promise<CierreCuentaCorrienteHistorialDto[]> => {
|
||||||
|
const queryParams: any = { ...params };
|
||||||
|
if (params.idUsuarioModificador) queryParams.idUsuarioModifico = params.idUsuarioModificador;
|
||||||
|
delete queryParams.idUsuarioModificador;
|
||||||
|
|
||||||
|
const response = await apiClient.get<CierreCuentaCorrienteHistorialDto[]>('/auditoria/cierres-cuenta-corriente', { params: queryParams });
|
||||||
|
return response.data;
|
||||||
|
};
|
||||||
|
|
||||||
const auditoriaService = {
|
const auditoriaService = {
|
||||||
getHistorialUsuarios,
|
getHistorialUsuarios,
|
||||||
getHistorialPagosDistribuidor,
|
getHistorialPagosDistribuidor,
|
||||||
@@ -498,6 +512,7 @@ const auditoriaService = {
|
|||||||
getHistorialPermisosMaestro,
|
getHistorialPermisosMaestro,
|
||||||
getHistorialPermisosPerfiles,
|
getHistorialPermisosPerfiles,
|
||||||
getHistorialCambiosParada,
|
getHistorialCambiosParada,
|
||||||
|
getHistorialCierresCC,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default auditoriaService;
|
export default auditoriaService;
|
||||||
75
Frontend/src/services/Contables/cierresCcService.ts
Normal file
75
Frontend/src/services/Contables/cierresCcService.ts
Normal file
@@ -0,0 +1,75 @@
|
|||||||
|
import apiClient from '../apiClient';
|
||||||
|
import type { CierreCuentaCorrienteDto } from '../../models/dtos/Contables/CierreCuentaCorrienteDto';
|
||||||
|
import type { CrearCierreDto } from '../../models/dtos/Contables/CrearCierreDto';
|
||||||
|
import type { ReabrirCierreDto } from '../../models/dtos/Contables/ReabrirCierreDto';
|
||||||
|
import type { UltimoCierreDto } from '../../models/dtos/Contables/UltimoCierreDto';
|
||||||
|
import type { CierreCuentaCorrienteHistorialDto } from '../../models/dtos/Auditoria/CierreCuentaCorrienteHistorialDto';
|
||||||
|
|
||||||
|
const getAllCierres = async (
|
||||||
|
idDistribuidor: number,
|
||||||
|
idEmpresa: number,
|
||||||
|
): Promise<CierreCuentaCorrienteDto[]> => {
|
||||||
|
const response = await apiClient.get<CierreCuentaCorrienteDto[]>('/cierres-cc', {
|
||||||
|
params: { idDistribuidor, idEmpresa },
|
||||||
|
});
|
||||||
|
return response.data;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Devuelve null si no hay cierre vigente (backend responde 404 — lo capturamos acá
|
||||||
|
// para que las páginas no tengan que manejar el error).
|
||||||
|
const getUltimoCierre = async (
|
||||||
|
idDistribuidor: number,
|
||||||
|
idEmpresa: number,
|
||||||
|
): Promise<UltimoCierreDto | null> => {
|
||||||
|
try {
|
||||||
|
const response = await apiClient.get<UltimoCierreDto>('/cierres-cc/ultimo', {
|
||||||
|
params: { idDistribuidor, idEmpresa },
|
||||||
|
});
|
||||||
|
return response.data;
|
||||||
|
} catch (error: unknown) {
|
||||||
|
if (
|
||||||
|
typeof error === 'object' &&
|
||||||
|
error !== null &&
|
||||||
|
'response' in error &&
|
||||||
|
(error as { response?: { status?: number } }).response?.status === 404
|
||||||
|
) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const crearCierre = async (data: CrearCierreDto): Promise<CierreCuentaCorrienteDto> => {
|
||||||
|
const response = await apiClient.post<CierreCuentaCorrienteDto>('/cierres-cc', data);
|
||||||
|
return response.data;
|
||||||
|
};
|
||||||
|
|
||||||
|
const reabrirCierre = async (
|
||||||
|
idCierre: number,
|
||||||
|
data: ReabrirCierreDto,
|
||||||
|
): Promise<CierreCuentaCorrienteDto> => {
|
||||||
|
const response = await apiClient.post<CierreCuentaCorrienteDto>(
|
||||||
|
`/cierres-cc/${idCierre}/reabrir`,
|
||||||
|
data,
|
||||||
|
);
|
||||||
|
return response.data;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getHistorialCierre = async (
|
||||||
|
idCierre: number,
|
||||||
|
): Promise<CierreCuentaCorrienteHistorialDto[]> => {
|
||||||
|
const response = await apiClient.get<CierreCuentaCorrienteHistorialDto[]>(
|
||||||
|
`/cierres-cc/${idCierre}/historial`
|
||||||
|
);
|
||||||
|
return response.data;
|
||||||
|
};
|
||||||
|
|
||||||
|
const cierresCcService = {
|
||||||
|
getAllCierres,
|
||||||
|
getUltimoCierre,
|
||||||
|
crearCierre,
|
||||||
|
reabrirCierre,
|
||||||
|
getHistorialCierre,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default cierresCcService;
|
||||||
49
Frontend/src/utils/apiErrorParser.ts
Normal file
49
Frontend/src/utils/apiErrorParser.ts
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
import axios from 'axios';
|
||||||
|
|
||||||
|
export interface ParsedApiError {
|
||||||
|
/** Mensaje listo para mostrar al usuario. */
|
||||||
|
message: string;
|
||||||
|
/** True si el error es un bloqueo por período cerrado (codigo PERIODO_CERRADO_BLOQUEO_OPERACION). */
|
||||||
|
isPeriodoCerrado: boolean;
|
||||||
|
/** Código semántico del backend, si vino. */
|
||||||
|
codigo?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parsea el body de un error de Axios siguiendo los shapes que devuelve la API:
|
||||||
|
* - ExceptionHandlerMiddleware: { codigo, mensaje, idCierre?, fechaCorte? } (period-cerrado)
|
||||||
|
* - Service tuple wrapped: { message } (legacy, inglés)
|
||||||
|
* - Service tuple nuevo: { mensaje } (español)
|
||||||
|
* Si nada matchea, devuelve el fallback recibido.
|
||||||
|
*/
|
||||||
|
export function parseApiError(err: unknown, fallback = 'Ocurrió un error inesperado.'): ParsedApiError {
|
||||||
|
if (!axios.isAxiosError(err) || !err.response?.data) {
|
||||||
|
return { message: fallback, isPeriodoCerrado: false };
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = err.response.data as Record<string, unknown>;
|
||||||
|
const codigo = typeof data.codigo === 'string' ? data.codigo : undefined;
|
||||||
|
|
||||||
|
if (codigo === 'PERIODO_CERRADO_BLOQUEO_OPERACION') {
|
||||||
|
const mensajeBackend = typeof data.mensaje === 'string' ? data.mensaje : '';
|
||||||
|
const fechaCorteRaw = typeof data.fechaCorte === 'string' ? data.fechaCorte : '';
|
||||||
|
const fechaFormateada = fechaCorteRaw
|
||||||
|
? new Date(fechaCorteRaw).toLocaleDateString('es-AR')
|
||||||
|
: '';
|
||||||
|
const fallbackPeriodo = fechaFormateada
|
||||||
|
? `El período está cerrado al ${fechaFormateada}. No se permiten modificaciones sobre fechas anteriores o iguales a la fecha de corte.`
|
||||||
|
: 'El período está cerrado. No se permiten modificaciones sobre la fecha indicada.';
|
||||||
|
return {
|
||||||
|
message: mensajeBackend || fallbackPeriodo,
|
||||||
|
isPeriodoCerrado: true,
|
||||||
|
codigo,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const mensaje =
|
||||||
|
(typeof data.mensaje === 'string' && data.mensaje) ||
|
||||||
|
(typeof data.message === 'string' && data.message) ||
|
||||||
|
fallback;
|
||||||
|
|
||||||
|
return { message: mensaje, isPeriodoCerrado: false, codigo };
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user