Feat(suscripciones): Implementa facturación pro-rata para altas y excluye del débito automático
All checks were successful
Optimized Build and Deploy / remote-build-and-deploy (push) Successful in 3m45s
All checks were successful
Optimized Build and Deploy / remote-build-and-deploy (push) Successful in 3m45s
Se introduce una refactorización mayor del ciclo de facturación para manejar correctamente las suscripciones que inician en un período ya cerrado. Esto soluciona el problema de cobrar un mes completo a un nuevo suscriptor, mejorando la transparencia y la experiencia del cliente. ### ✨ Nuevas Características y Lógica de Negocio - **Facturación Pro-rata Automática (Factura de Alta):** - Al crear una nueva suscripción cuya fecha de inicio corresponde a un período de facturación ya cerrado, el sistema ahora calcula automáticamente el costo proporcional por los días restantes de ese mes. - Se genera de forma inmediata una nueva factura de tipo "Alta" por este monto parcial, separándola del ciclo de facturación mensual regular. - **Exclusión del Débito Automático para Facturas de Alta:** - Se implementa una regla de negocio clave: las facturas de tipo "Alta" son **excluidas** del proceso de generación del archivo de débito automático para el banco. - Esto fuerza a que el primer cobro (el proporcional) se gestione a través de un medio de pago manual (efectivo, transferencia, etc.), evitando cargos inesperados en la cuenta bancaria del cliente. - El débito automático comenzará a operar normalmente a partir del primer ciclo de facturación completo. ### 🔄 Cambios en el Backend - **Base de Datos:** - Se ha añadido la columna `TipoFactura` (`varchar(20)`) a la tabla `susc_Facturas`. - Se ha implementado una `CHECK constraint` para permitir únicamente los valores 'Mensual' y 'Alta'. - **Servicios:** - **`SuscripcionService`:** Ahora contiene la lógica para detectar una alta retroactiva, invocar al `FacturacionService` para el cálculo pro-rata y crear la "Factura de Alta" y su detalle correspondiente dentro de la misma transacción. - **`FacturacionService`:** Expone públicamente el método `CalcularImporteParaSuscripcion` y se ha actualizado `ObtenerResumenesDeCuentaPorPeriodo` para que envíe la propiedad `TipoFactura` al frontend. - **`DebitoAutomaticoService`:** El método `GetFacturasParaDebito` ahora filtra y excluye explícitamente las facturas donde `TipoFactura = 'Alta'`. ### 🎨 Mejoras en la Interfaz de Usuario (Frontend) - **`ConsultaFacturasPage.tsx`:** - **Nueva Columna:** Se ha añadido una columna "Tipo Factura" en la tabla de detalle, que muestra un `Chip` distintivo para identificar fácilmente las facturas de "Alta". - **Nuevo Filtro:** Se ha agregado un nuevo menú desplegable para filtrar la vista por "Tipo de Factura" (`Todas`, `Mensual`, `Alta`), permitiendo a los administradores auditar rápidamente los nuevos ingresos.
This commit is contained in:
@@ -72,15 +72,16 @@ namespace GestionIntegral.Api.Controllers.Suscripciones
|
|||||||
|
|
||||||
[HttpGet("{anio:int}/{mes:int}")]
|
[HttpGet("{anio:int}/{mes:int}")]
|
||||||
public async Task<IActionResult> GetFacturas(
|
public async Task<IActionResult> GetFacturas(
|
||||||
int anio, int mes,
|
int anio, int mes,
|
||||||
[FromQuery] string? nombreSuscriptor,
|
[FromQuery] string? nombreSuscriptor,
|
||||||
[FromQuery] string? estadoPago,
|
[FromQuery] string? estadoPago,
|
||||||
[FromQuery] string? estadoFacturacion)
|
[FromQuery] string? estadoFacturacion,
|
||||||
|
[FromQuery] string? tipoFactura)
|
||||||
{
|
{
|
||||||
if (!TienePermiso(PermisoGestionarFacturacion)) return Forbid();
|
if (!TienePermiso(PermisoGestionarFacturacion)) return Forbid();
|
||||||
if (anio < 2020 || mes < 1 || mes > 12) return BadRequest(new { message = "El período no es válido." });
|
if (anio < 2020 || mes < 1 || mes > 12) return BadRequest(new { message = "El período no es válido." });
|
||||||
|
|
||||||
var resumenes = await _facturacionService.ObtenerResumenesDeCuentaPorPeriodo(anio, mes, nombreSuscriptor, estadoPago, estadoFacturacion);
|
var resumenes = await _facturacionService.ObtenerResumenesDeCuentaPorPeriodo(anio, mes, nombreSuscriptor, estadoPago, estadoFacturacion, tipoFactura);
|
||||||
return Ok(resumenes);
|
return Ok(resumenes);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -104,7 +104,8 @@ namespace GestionIntegral.Api.Data.Repositories.Suscripciones
|
|||||||
return rowsAffected == idsFacturas.Count();
|
return rowsAffected == idsFacturas.Count();
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<IEnumerable<(Factura Factura, string NombreSuscriptor, int IdEmpresa, decimal TotalPagado)>> GetByPeriodoEnrichedAsync(string periodo, string? nombreSuscriptor, string? estadoPago, string? estadoFacturacion)
|
public async Task<IEnumerable<(Factura Factura, string NombreSuscriptor, int IdEmpresa, decimal TotalPagado)>> GetByPeriodoEnrichedAsync(
|
||||||
|
string periodo, string? nombreSuscriptor, string? estadoPago, string? estadoFacturacion, string? tipoFactura)
|
||||||
{
|
{
|
||||||
var sqlBuilder = new StringBuilder(@"
|
var sqlBuilder = new StringBuilder(@"
|
||||||
WITH FacturaConEmpresa AS (
|
WITH FacturaConEmpresa AS (
|
||||||
@@ -149,6 +150,12 @@ namespace GestionIntegral.Api.Data.Repositories.Suscripciones
|
|||||||
parameters.Add("EstadoFacturacion", estadoFacturacion);
|
parameters.Add("EstadoFacturacion", estadoFacturacion);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!string.IsNullOrWhiteSpace(tipoFactura))
|
||||||
|
{
|
||||||
|
sqlBuilder.Append(" AND f.TipoFactura = @TipoFactura");
|
||||||
|
parameters.Add("TipoFactura", tipoFactura);
|
||||||
|
}
|
||||||
|
|
||||||
sqlBuilder.Append(" ORDER BY s.NombreCompleto, f.IdFactura;");
|
sqlBuilder.Append(" ORDER BY s.NombreCompleto, f.IdFactura;");
|
||||||
|
|
||||||
try
|
try
|
||||||
|
|||||||
@@ -15,7 +15,8 @@ namespace GestionIntegral.Api.Data.Repositories.Suscripciones
|
|||||||
Task<bool> UpdateEstadoPagoAsync(int idFactura, string nuevoEstadoPago, IDbTransaction transaction);
|
Task<bool> UpdateEstadoPagoAsync(int idFactura, string nuevoEstadoPago, IDbTransaction transaction);
|
||||||
Task<bool> UpdateNumeroFacturaAsync(int idFactura, string numeroFactura, IDbTransaction transaction);
|
Task<bool> UpdateNumeroFacturaAsync(int idFactura, string numeroFactura, IDbTransaction transaction);
|
||||||
Task<bool> UpdateLoteDebitoAsync(IEnumerable<int> idsFacturas, int idLoteDebito, IDbTransaction transaction);
|
Task<bool> UpdateLoteDebitoAsync(IEnumerable<int> idsFacturas, int idLoteDebito, IDbTransaction transaction);
|
||||||
Task<IEnumerable<(Factura Factura, string NombreSuscriptor, int IdEmpresa, decimal TotalPagado)>> GetByPeriodoEnrichedAsync(string periodo, string? nombreSuscriptor, string? estadoPago, string? estadoFacturacion);
|
Task<IEnumerable<(Factura Factura, string NombreSuscriptor, int IdEmpresa, decimal TotalPagado)>> GetByPeriodoEnrichedAsync(
|
||||||
|
string periodo, string? nombreSuscriptor, string? estadoPago, string? estadoFacturacion, string? tipoFactura);
|
||||||
Task<bool> UpdateEstadoYMotivoAsync(int idFactura, string nuevoEstadoPago, string? motivoRechazo, IDbTransaction transaction);
|
Task<bool> UpdateEstadoYMotivoAsync(int idFactura, string nuevoEstadoPago, string? motivoRechazo, IDbTransaction transaction);
|
||||||
Task<string?> GetUltimoPeriodoFacturadoAsync();
|
Task<string?> GetUltimoPeriodoFacturadoAsync();
|
||||||
Task<IEnumerable<Factura>> GetFacturasPagadasPendientesDeFacturar(string periodo);
|
Task<IEnumerable<Factura>> GetFacturasPagadasPendientesDeFacturar(string periodo);
|
||||||
|
|||||||
@@ -9,6 +9,8 @@ namespace GestionIntegral.Api.Dtos.Suscripciones
|
|||||||
public string EstadoFacturacion { get; set; } = string.Empty;
|
public string EstadoFacturacion { get; set; } = string.Empty;
|
||||||
public string? NumeroFactura { get; set; }
|
public string? NumeroFactura { get; set; }
|
||||||
public decimal TotalPagado { get; set; }
|
public decimal TotalPagado { get; set; }
|
||||||
|
public string TipoFactura { get; set; } = string.Empty;
|
||||||
|
public int IdSuscriptor { get; set; }
|
||||||
public List<FacturaDetalleDto> Detalles { get; set; } = new List<FacturaDetalleDto>();
|
public List<FacturaDetalleDto> Detalles { get; set; } = new List<FacturaDetalleDto>();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -15,5 +15,6 @@ namespace GestionIntegral.Api.Models.Suscripciones
|
|||||||
public string? NumeroFactura { get; set; }
|
public string? NumeroFactura { get; set; }
|
||||||
public int? IdLoteDebito { get; set; }
|
public int? IdLoteDebito { get; set; }
|
||||||
public string? MotivoRechazo { get; set; }
|
public string? MotivoRechazo { get; set; }
|
||||||
|
public string TipoFactura { get; set; } = string.Empty;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -42,7 +42,7 @@ namespace GestionIntegral.Api.Services.Suscripciones
|
|||||||
{
|
{
|
||||||
// Este número debe ser gestionado para no repetirse. Por ahora, lo mantenemos como 1.
|
// Este número debe ser gestionado para no repetirse. Por ahora, lo mantenemos como 1.
|
||||||
const int identificacionArchivo = 1;
|
const int identificacionArchivo = 1;
|
||||||
|
|
||||||
var periodo = $"{anio}-{mes:D2}";
|
var periodo = $"{anio}-{mes:D2}";
|
||||||
var fechaGeneracion = DateTime.Now;
|
var fechaGeneracion = DateTime.Now;
|
||||||
|
|
||||||
@@ -102,7 +102,11 @@ namespace GestionIntegral.Api.Services.Suscripciones
|
|||||||
var facturas = await _facturaRepository.GetByPeriodoAsync(periodo);
|
var facturas = await _facturaRepository.GetByPeriodoAsync(periodo);
|
||||||
var resultado = new List<(Factura, Suscriptor)>();
|
var resultado = new List<(Factura, Suscriptor)>();
|
||||||
|
|
||||||
foreach (var f in facturas.Where(fa => fa.EstadoPago == "Pendiente" || fa.EstadoPago == "Pagada Parcialmente" || fa.EstadoPago == "Rechazada"))
|
// Filtramos por estado Y POR TIPO DE FACTURA
|
||||||
|
foreach (var f in facturas.Where(fa =>
|
||||||
|
(fa.EstadoPago == "Pendiente" || fa.EstadoPago == "Pagada Parcialmente" || fa.EstadoPago == "Rechazada") &&
|
||||||
|
fa.TipoFactura == "Mensual"
|
||||||
|
))
|
||||||
{
|
{
|
||||||
var suscriptor = await _suscriptorRepository.GetByIdAsync(f.IdSuscriptor);
|
var suscriptor = await _suscriptorRepository.GetByIdAsync(f.IdSuscriptor);
|
||||||
if (suscriptor == null || string.IsNullOrWhiteSpace(suscriptor.CBU) || suscriptor.CBU.Length != 22)
|
if (suscriptor == null || string.IsNullOrWhiteSpace(suscriptor.CBU) || suscriptor.CBU.Length != 22)
|
||||||
@@ -141,7 +145,12 @@ namespace GestionIntegral.Api.Services.Suscripciones
|
|||||||
private string FormatNumericString(string? value, int length) => (value ?? "").PadLeft(length, '0');
|
private string FormatNumericString(string? value, int length) => (value ?? "").PadLeft(length, '0');
|
||||||
private string MapTipoDocumento(string tipo) => tipo.ToUpper() switch
|
private string MapTipoDocumento(string tipo) => tipo.ToUpper() switch
|
||||||
{
|
{
|
||||||
"DNI" => "0096", "CUIT" => "0080", "CUIL" => "0086", "LE" => "0089", "LC" => "0090", _ => "0000"
|
"DNI" => "0096",
|
||||||
|
"CUIT" => "0080",
|
||||||
|
"CUIL" => "0086",
|
||||||
|
"LE" => "0089",
|
||||||
|
"LC" => "0090",
|
||||||
|
_ => "0000"
|
||||||
};
|
};
|
||||||
|
|
||||||
private string CrearRegistroHeader(DateTime fechaGeneracion, decimal importeTotal, int cantidadRegistros, int identificacionArchivo)
|
private string CrearRegistroHeader(DateTime fechaGeneracion, decimal importeTotal, int cantidadRegistros, int identificacionArchivo)
|
||||||
@@ -212,7 +221,7 @@ namespace GestionIntegral.Api.Services.Suscripciones
|
|||||||
public async Task<ProcesamientoLoteResponseDto> ProcesarArchivoRespuesta(IFormFile archivo, int idUsuario)
|
public async Task<ProcesamientoLoteResponseDto> ProcesarArchivoRespuesta(IFormFile archivo, int idUsuario)
|
||||||
{
|
{
|
||||||
// Se mantiene la lógica original para procesar el archivo de respuesta del banco.
|
// Se mantiene la lógica original para procesar el archivo de respuesta del banco.
|
||||||
|
|
||||||
var respuesta = new ProcesamientoLoteResponseDto();
|
var respuesta = new ProcesamientoLoteResponseDto();
|
||||||
if (archivo == null || archivo.Length == 0)
|
if (archivo == null || archivo.Length == 0)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -278,10 +278,11 @@ namespace GestionIntegral.Api.Services.Suscripciones
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<IEnumerable<ResumenCuentaSuscriptorDto>> ObtenerResumenesDeCuentaPorPeriodo(int anio, int mes, string? nombreSuscriptor, string? estadoPago, string? estadoFacturacion)
|
public async Task<IEnumerable<ResumenCuentaSuscriptorDto>> ObtenerResumenesDeCuentaPorPeriodo(
|
||||||
|
int anio, int mes, string? nombreSuscriptor, string? estadoPago, string? estadoFacturacion, string? tipoFactura)
|
||||||
{
|
{
|
||||||
var periodo = $"{anio}-{mes:D2}";
|
var periodo = $"{anio}-{mes:D2}";
|
||||||
var facturasData = await _facturaRepository.GetByPeriodoEnrichedAsync(periodo, nombreSuscriptor, estadoPago, estadoFacturacion);
|
var facturasData = await _facturaRepository.GetByPeriodoEnrichedAsync(periodo, nombreSuscriptor, estadoPago, estadoFacturacion, tipoFactura);
|
||||||
var detallesData = await _facturaDetalleRepository.GetDetallesPorPeriodoAsync(periodo);
|
var detallesData = await _facturaDetalleRepository.GetDetallesPorPeriodoAsync(periodo);
|
||||||
var empresas = await _empresaRepository.GetAllAsync(null, null);
|
var empresas = await _empresaRepository.GetAllAsync(null, null);
|
||||||
|
|
||||||
@@ -302,10 +303,17 @@ namespace GestionIntegral.Api.Services.Suscripciones
|
|||||||
EstadoFacturacion = itemFactura.Factura.EstadoFacturacion,
|
EstadoFacturacion = itemFactura.Factura.EstadoFacturacion,
|
||||||
NumeroFactura = itemFactura.Factura.NumeroFactura,
|
NumeroFactura = itemFactura.Factura.NumeroFactura,
|
||||||
TotalPagado = itemFactura.TotalPagado,
|
TotalPagado = itemFactura.TotalPagado,
|
||||||
|
|
||||||
|
// Faltaba esta línea para pasar el tipo de factura al frontend.
|
||||||
|
TipoFactura = itemFactura.Factura.TipoFactura,
|
||||||
|
|
||||||
Detalles = detallesData
|
Detalles = detallesData
|
||||||
.Where(d => d.IdFactura == itemFactura.Factura.IdFactura)
|
.Where(d => d.IdFactura == itemFactura.Factura.IdFactura)
|
||||||
.Select(d => new FacturaDetalleDto { Descripcion = d.Descripcion, ImporteNeto = d.ImporteNeto })
|
.Select(d => new FacturaDetalleDto { Descripcion = d.Descripcion, ImporteNeto = d.ImporteNeto })
|
||||||
.ToList()
|
.ToList(),
|
||||||
|
|
||||||
|
// Pasamos el id del suscriptor para facilitar las cosas en el frontend
|
||||||
|
IdSuscriptor = itemFactura.Factura.IdSuscriptor
|
||||||
};
|
};
|
||||||
}).ToList();
|
}).ToList();
|
||||||
|
|
||||||
@@ -579,7 +587,7 @@ namespace GestionIntegral.Api.Services.Suscripciones
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task<decimal> CalcularImporteParaSuscripcion(Suscripcion suscripcion, int anio, int mes, IDbTransaction transaction)
|
public async Task<decimal> CalcularImporteParaSuscripcion(Suscripcion suscripcion, int anio, int mes, IDbTransaction transaction)
|
||||||
{
|
{
|
||||||
decimal importeTotal = 0;
|
decimal importeTotal = 0;
|
||||||
var diasDeEntrega = suscripcion.DiasEntrega.Split(',').ToHashSet();
|
var diasDeEntrega = suscripcion.DiasEntrega.Split(',').ToHashSet();
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
|
using System.Data;
|
||||||
using GestionIntegral.Api.Dtos.Comunicaciones;
|
using GestionIntegral.Api.Dtos.Comunicaciones;
|
||||||
using GestionIntegral.Api.Dtos.Suscripciones;
|
using GestionIntegral.Api.Dtos.Suscripciones;
|
||||||
|
using GestionIntegral.Api.Models.Suscripciones;
|
||||||
|
|
||||||
namespace GestionIntegral.Api.Services.Suscripciones
|
namespace GestionIntegral.Api.Services.Suscripciones
|
||||||
{
|
{
|
||||||
@@ -7,8 +9,10 @@ namespace GestionIntegral.Api.Services.Suscripciones
|
|||||||
{
|
{
|
||||||
Task<(bool Exito, string? Mensaje, LoteDeEnvioResumenDto? ResultadoEnvio)> GenerarFacturacionMensual(int anio, int mes, int idUsuario);
|
Task<(bool Exito, string? Mensaje, LoteDeEnvioResumenDto? ResultadoEnvio)> GenerarFacturacionMensual(int anio, int mes, int idUsuario);
|
||||||
Task<IEnumerable<LoteDeEnvioHistorialDto>> ObtenerHistorialLotesEnvio(int? anio, int? mes);
|
Task<IEnumerable<LoteDeEnvioHistorialDto>> ObtenerHistorialLotesEnvio(int? anio, int? mes);
|
||||||
Task<IEnumerable<ResumenCuentaSuscriptorDto>> ObtenerResumenesDeCuentaPorPeriodo(int anio, int mes, string? nombreSuscriptor, string? estadoPago, string? estadoFacturacion);
|
Task<IEnumerable<ResumenCuentaSuscriptorDto>> ObtenerResumenesDeCuentaPorPeriodo(
|
||||||
|
int anio, int mes, string? nombreSuscriptor, string? estadoPago, string? estadoFacturacion, string? tipoFactura);
|
||||||
Task<(bool Exito, string? Error, string? EmailDestino)> EnviarFacturaPdfPorEmail(int idFactura, int idUsuario);
|
Task<(bool Exito, string? Error, string? EmailDestino)> EnviarFacturaPdfPorEmail(int idFactura, int idUsuario);
|
||||||
Task<(bool Exito, string? Error)> ActualizarNumeroFactura(int idFactura, string numeroFactura, int idUsuario);
|
Task<(bool Exito, string? Error)> ActualizarNumeroFactura(int idFactura, string numeroFactura, int idUsuario);
|
||||||
|
Task<decimal> CalcularImporteParaSuscripcion(Suscripcion suscripcion, int anio, int mes, IDbTransaction transaction);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -3,12 +3,8 @@ using GestionIntegral.Api.Data.Repositories.Distribucion;
|
|||||||
using GestionIntegral.Api.Data.Repositories.Suscripciones;
|
using GestionIntegral.Api.Data.Repositories.Suscripciones;
|
||||||
using GestionIntegral.Api.Dtos.Suscripciones;
|
using GestionIntegral.Api.Dtos.Suscripciones;
|
||||||
using GestionIntegral.Api.Models.Suscripciones;
|
using GestionIntegral.Api.Models.Suscripciones;
|
||||||
using Microsoft.Extensions.Logging;
|
|
||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Data;
|
using System.Data;
|
||||||
using System.Linq;
|
using System.Globalization;
|
||||||
using System.Threading.Tasks;
|
|
||||||
|
|
||||||
namespace GestionIntegral.Api.Services.Suscripciones
|
namespace GestionIntegral.Api.Services.Suscripciones
|
||||||
{
|
{
|
||||||
@@ -18,23 +14,32 @@ namespace GestionIntegral.Api.Services.Suscripciones
|
|||||||
private readonly ISuscriptorRepository _suscriptorRepository;
|
private readonly ISuscriptorRepository _suscriptorRepository;
|
||||||
private readonly IPublicacionRepository _publicacionRepository;
|
private readonly IPublicacionRepository _publicacionRepository;
|
||||||
private readonly IPromocionRepository _promocionRepository;
|
private readonly IPromocionRepository _promocionRepository;
|
||||||
private readonly DbConnectionFactory _connectionFactory;
|
private readonly IFacturaRepository _facturaRepository;
|
||||||
|
private readonly IFacturaDetalleRepository _facturaDetalleRepository;
|
||||||
|
private readonly IFacturacionService _facturacionService;
|
||||||
private readonly ILogger<SuscripcionService> _logger;
|
private readonly ILogger<SuscripcionService> _logger;
|
||||||
|
private readonly DbConnectionFactory _connectionFactory;
|
||||||
|
|
||||||
public SuscripcionService(
|
public SuscripcionService(
|
||||||
ISuscripcionRepository suscripcionRepository,
|
ISuscripcionRepository suscripcionRepository,
|
||||||
ISuscriptorRepository suscriptorRepository,
|
ISuscriptorRepository suscriptorRepository,
|
||||||
IPublicacionRepository publicacionRepository,
|
IPublicacionRepository publicacionRepository,
|
||||||
IPromocionRepository promocionRepository,
|
IPromocionRepository promocionRepository,
|
||||||
DbConnectionFactory connectionFactory,
|
IFacturaRepository facturaRepository,
|
||||||
ILogger<SuscripcionService> logger)
|
IFacturaDetalleRepository facturaDetalleRepository,
|
||||||
|
IFacturacionService facturacionService,
|
||||||
|
ILogger<SuscripcionService> logger,
|
||||||
|
DbConnectionFactory connectionFactory)
|
||||||
{
|
{
|
||||||
_suscripcionRepository = suscripcionRepository;
|
_suscripcionRepository = suscripcionRepository;
|
||||||
_suscriptorRepository = suscriptorRepository;
|
_suscriptorRepository = suscriptorRepository;
|
||||||
_publicacionRepository = publicacionRepository;
|
_publicacionRepository = publicacionRepository;
|
||||||
_promocionRepository = promocionRepository;
|
_promocionRepository = promocionRepository;
|
||||||
_connectionFactory = connectionFactory;
|
_facturaRepository = facturaRepository;
|
||||||
|
_facturaDetalleRepository = facturaDetalleRepository;
|
||||||
|
_facturacionService = facturacionService;
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
|
_connectionFactory = connectionFactory;
|
||||||
}
|
}
|
||||||
|
|
||||||
private PromocionDto MapPromocionToDto(Promocion promo) => new PromocionDto
|
private PromocionDto MapPromocionToDto(Promocion promo) => new PromocionDto
|
||||||
@@ -122,6 +127,53 @@ namespace GestionIntegral.Api.Services.Suscripciones
|
|||||||
var creada = await _suscripcionRepository.CreateAsync(nuevaSuscripcion, transaction);
|
var creada = await _suscripcionRepository.CreateAsync(nuevaSuscripcion, transaction);
|
||||||
if (creada == null) throw new DataException("Error al crear la suscripción.");
|
if (creada == null) throw new DataException("Error al crear la suscripción.");
|
||||||
|
|
||||||
|
var ultimoPeriodoFacturadoStr = await _facturaRepository.GetUltimoPeriodoFacturadoAsync();
|
||||||
|
if (ultimoPeriodoFacturadoStr != null)
|
||||||
|
{
|
||||||
|
var ultimoPeriodo = DateTime.ParseExact(ultimoPeriodoFacturadoStr, "yyyy-MM", CultureInfo.InvariantCulture);
|
||||||
|
var periodoSuscripcion = new DateTime(creada.FechaInicio.Year, creada.FechaInicio.Month, 1);
|
||||||
|
|
||||||
|
if (periodoSuscripcion <= ultimoPeriodo)
|
||||||
|
{
|
||||||
|
_logger.LogInformation("Suscripción en período ya cerrado detectada para Suscriptor {IdSuscriptor}. Generando factura de alta pro-rata.", creada.IdSuscriptor);
|
||||||
|
|
||||||
|
decimal importeProporcional = await _facturacionService.CalcularImporteParaSuscripcion(creada, creada.FechaInicio.Year, creada.FechaInicio.Month, transaction);
|
||||||
|
|
||||||
|
if (importeProporcional > 0)
|
||||||
|
{
|
||||||
|
var facturaDeAlta = new Factura
|
||||||
|
{
|
||||||
|
IdSuscriptor = creada.IdSuscriptor,
|
||||||
|
Periodo = creada.FechaInicio.ToString("yyyy-MM"),
|
||||||
|
FechaEmision = DateTime.Now.Date,
|
||||||
|
FechaVencimiento = DateTime.Now.AddDays(10).Date, // Vencimiento corto
|
||||||
|
ImporteBruto = importeProporcional,
|
||||||
|
ImporteFinal = importeProporcional,
|
||||||
|
EstadoPago = "Pendiente",
|
||||||
|
EstadoFacturacion = "Pendiente de Facturar",
|
||||||
|
TipoFactura = "Alta" // Marcamos la factura como de tipo "Alta"
|
||||||
|
};
|
||||||
|
|
||||||
|
var facturaCreada = await _facturaRepository.CreateAsync(facturaDeAlta, transaction);
|
||||||
|
if (facturaCreada == null) throw new DataException("No se pudo crear la factura de alta.");
|
||||||
|
|
||||||
|
var publicacion = await _publicacionRepository.GetByIdSimpleAsync(creada.IdPublicacion);
|
||||||
|
var finDeMes = new DateTime(creada.FechaInicio.Year, creada.FechaInicio.Month, 1).AddMonths(1).AddDays(-1);
|
||||||
|
|
||||||
|
await _facturaDetalleRepository.CreateAsync(new FacturaDetalle
|
||||||
|
{
|
||||||
|
IdFactura = facturaCreada.IdFactura,
|
||||||
|
IdSuscripcion = creada.IdSuscripcion,
|
||||||
|
Descripcion = $"Suscripción proporcional {publicacion?.Nombre} ({creada.FechaInicio:dd/MM} al {finDeMes:dd/MM})",
|
||||||
|
ImporteBruto = importeProporcional,
|
||||||
|
ImporteNeto = importeProporcional,
|
||||||
|
DescuentoAplicado = 0
|
||||||
|
}, transaction);
|
||||||
|
|
||||||
|
_logger.LogInformation("Factura de alta #{IdFactura} por ${Importe} generada para la nueva suscripción #{IdSuscripcion}.", facturaCreada.IdFactura, importeProporcional, creada.IdSuscripcion);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
transaction.Commit();
|
transaction.Commit();
|
||||||
_logger.LogInformation("Suscripción ID {Id} creada por Usuario ID {UserId}.", creada.IdSuscripcion, idUsuario);
|
_logger.LogInformation("Suscripción ID {Id} creada por Usuario ID {UserId}.", creada.IdSuscripcion, idUsuario);
|
||||||
return (await MapToDto(creada), null);
|
return (await MapToDto(creada), null);
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import React, { useState, useEffect } from 'react';
|
import React, { useState, useEffect } from 'react';
|
||||||
import { Modal, Box, Typography, TextField, Button, CircularProgress, Alert, FormControl, InputLabel, Select, MenuItem, type SelectChangeEvent, InputAdornment } from '@mui/material';
|
import { Modal, Box, Typography, TextField, Button, CircularProgress, Alert, FormControl, InputLabel, Select, MenuItem, type SelectChangeEvent, InputAdornment } from '@mui/material';
|
||||||
import type { FacturaDto } from '../../../models/dtos/Suscripciones/FacturaDto';
|
import type { FacturaConsolidadaDto } from '../../../models/dtos/Suscripciones/ResumenCuentaSuscriptorDto';
|
||||||
import type { CreatePagoDto } from '../../../models/dtos/Suscripciones/CreatePagoDto';
|
import type { CreatePagoDto } from '../../../models/dtos/Suscripciones/CreatePagoDto';
|
||||||
import type { FormaPagoDto } from '../../../models/dtos/Suscripciones/FormaPagoDto';
|
import type { FormaPagoDto } from '../../../models/dtos/Suscripciones/FormaPagoDto';
|
||||||
import formaPagoService from '../../../services/Suscripciones/formaPagoService';
|
import formaPagoService from '../../../services/Suscripciones/formaPagoService';
|
||||||
@@ -23,17 +23,19 @@ interface PagoManualModalProps {
|
|||||||
open: boolean;
|
open: boolean;
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
onSubmit: (data: CreatePagoDto) => Promise<void>;
|
onSubmit: (data: CreatePagoDto) => Promise<void>;
|
||||||
factura: FacturaDto | null;
|
factura: FacturaConsolidadaDto | null;
|
||||||
|
nombreSuscriptor: string; // Se pasa el nombre del suscriptor como prop-
|
||||||
errorMessage?: string | null;
|
errorMessage?: string | null;
|
||||||
clearErrorMessage: () => void;
|
clearErrorMessage: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const PagoManualModal: React.FC<PagoManualModalProps> = ({ open, onClose, onSubmit, factura, errorMessage, clearErrorMessage }) => {
|
const PagoManualModal: React.FC<PagoManualModalProps> = ({ open, onClose, onSubmit, factura, nombreSuscriptor, errorMessage, clearErrorMessage }) => {
|
||||||
const [formData, setFormData] = useState<Partial<CreatePagoDto>>({});
|
const [formData, setFormData] = useState<Partial<CreatePagoDto>>({});
|
||||||
const [formasDePago, setFormasDePago] = useState<FormaPagoDto[]>([]);
|
const [formasDePago, setFormasDePago] = useState<FormaPagoDto[]>([]);
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [loadingFormasPago, setLoadingFormasPago] = useState(false);
|
const [loadingFormasPago, setLoadingFormasPago] = useState(false);
|
||||||
const [localErrors, setLocalErrors] = useState<{ [key: string]: string | null }>({});
|
const [localErrors, setLocalErrors] = useState<{ [key: string]: string | null }>({});
|
||||||
|
const saldoPendiente = factura ? factura.importeFinal - factura.totalPagado : 0;
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const fetchFormasDePago = async () => {
|
const fetchFormasDePago = async () => {
|
||||||
@@ -52,26 +54,24 @@ const PagoManualModal: React.FC<PagoManualModalProps> = ({ open, onClose, onSubm
|
|||||||
fetchFormasDePago();
|
fetchFormasDePago();
|
||||||
setFormData({
|
setFormData({
|
||||||
idFactura: factura.idFactura,
|
idFactura: factura.idFactura,
|
||||||
monto: factura.saldoPendiente, // Usar el saldo pendiente como valor por defecto
|
monto: saldoPendiente,
|
||||||
fechaPago: new Date().toISOString().split('T')[0]
|
fechaPago: new Date().toISOString().split('T')[0]
|
||||||
});
|
});
|
||||||
setLocalErrors({});
|
setLocalErrors({});
|
||||||
}
|
}
|
||||||
}, [open, factura]);
|
}, [open, factura, saldoPendiente]);
|
||||||
|
|
||||||
const validate = (): boolean => {
|
const validate = (): boolean => {
|
||||||
const errors: { [key: string]: string | null } = {};
|
const errors: { [key: string]: string | null } = {};
|
||||||
if (!formData.idFormaPago) errors.idFormaPago = "Seleccione una forma de pago.";
|
if (!formData.idFormaPago) errors.idFormaPago = "Seleccione una forma de pago.";
|
||||||
if (!formData.fechaPago) errors.fechaPago = "La fecha de pago es obligatoria.";
|
if (!formData.fechaPago) errors.fechaPago = "La fecha de pago es obligatoria.";
|
||||||
|
|
||||||
const monto = formData.monto ?? 0;
|
const monto = formData.monto ?? 0;
|
||||||
const saldo = factura?.saldoPendiente ?? 0;
|
|
||||||
|
|
||||||
if (monto <= 0) {
|
if (monto <= 0) {
|
||||||
errors.monto = "El monto debe ser mayor a cero.";
|
errors.monto = "El monto debe ser mayor a cero.";
|
||||||
} else if (monto > saldo) {
|
} else if (monto > saldoPendiente) {
|
||||||
// Usamos toFixed(2) para mostrar el formato de moneda correcto en el mensaje
|
errors.monto = `El monto no puede superar el saldo pendiente de $${saldoPendiente.toFixed(2)}.`;
|
||||||
errors.monto = `El monto no puede superar el saldo pendiente de $${saldo.toFixed(2)}.`;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
setLocalErrors(errors);
|
setLocalErrors(errors);
|
||||||
@@ -85,7 +85,7 @@ const PagoManualModal: React.FC<PagoManualModalProps> = ({ open, onClose, onSubm
|
|||||||
if (localErrors[name]) setLocalErrors(prev => ({ ...prev, [name]: null }));
|
if (localErrors[name]) setLocalErrors(prev => ({ ...prev, [name]: null }));
|
||||||
if (errorMessage) clearErrorMessage();
|
if (errorMessage) clearErrorMessage();
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleSelectChange = (e: SelectChangeEvent<any>) => {
|
const handleSelectChange = (e: SelectChangeEvent<any>) => {
|
||||||
const { name, value } = e.target;
|
const { name, value } = e.target;
|
||||||
setFormData(prev => ({ ...prev, [name]: value }));
|
setFormData(prev => ({ ...prev, [name]: value }));
|
||||||
@@ -117,29 +117,32 @@ const PagoManualModal: React.FC<PagoManualModalProps> = ({ open, onClose, onSubm
|
|||||||
<Modal open={open} onClose={onClose}>
|
<Modal open={open} onClose={onClose}>
|
||||||
<Box sx={modalStyle}>
|
<Box sx={modalStyle}>
|
||||||
<Typography variant="h6">Registrar Pago Manual</Typography>
|
<Typography variant="h6">Registrar Pago Manual</Typography>
|
||||||
<Typography variant="subtitle1" gutterBottom sx={{fontWeight: 'bold'}}>
|
<Typography variant="body1" color="text.secondary" gutterBottom>
|
||||||
Saldo Pendiente: ${factura.saldoPendiente.toFixed(2)}
|
Para: {nombreSuscriptor}
|
||||||
|
</Typography>
|
||||||
|
<Typography variant="subtitle1" gutterBottom sx={{ fontWeight: 'bold' }}>
|
||||||
|
Saldo Pendiente: ${saldoPendiente.toFixed(2)}
|
||||||
</Typography>
|
</Typography>
|
||||||
<Box component="form" onSubmit={handleSubmit} sx={{ mt: 2 }}>
|
<Box component="form" onSubmit={handleSubmit} sx={{ mt: 2 }}>
|
||||||
<TextField name="fechaPago" label="Fecha de Pago" type="date" value={formData.fechaPago || ''} onChange={handleInputChange} required fullWidth margin="dense" InputLabelProps={{ shrink: true }} error={!!localErrors.fechaPago} helperText={localErrors.fechaPago} />
|
<TextField name="fechaPago" label="Fecha de Pago" type="date" value={formData.fechaPago || ''} onChange={handleInputChange} required fullWidth margin="dense" InputLabelProps={{ shrink: true }} error={!!localErrors.fechaPago} helperText={localErrors.fechaPago} />
|
||||||
<FormControl fullWidth margin="dense" error={!!localErrors.idFormaPago}>
|
<FormControl fullWidth margin="dense" error={!!localErrors.idFormaPago}>
|
||||||
<InputLabel id="forma-pago-label" required>Forma de Pago</InputLabel>
|
<InputLabel id="forma-pago-label" required>Forma de Pago</InputLabel>
|
||||||
<Select name="idFormaPago" labelId="forma-pago-label" value={formData.idFormaPago || ''} onChange={handleSelectChange} label="Forma de Pago" disabled={loadingFormasPago}>
|
<Select name="idFormaPago" labelId="forma-pago-label" value={formData.idFormaPago || ''} onChange={handleSelectChange} label="Forma de Pago" disabled={loadingFormasPago}>
|
||||||
{formasDePago.map(fp => <MenuItem key={fp.idFormaPago} value={fp.idFormaPago}>{fp.nombre}</MenuItem>)}
|
{formasDePago.map(fp => <MenuItem key={fp.idFormaPago} value={fp.idFormaPago}>{fp.nombre}</MenuItem>)}
|
||||||
</Select>
|
</Select>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<TextField name="monto" label="Monto Pagado" type="number" value={formData.monto || ''} onChange={handleInputChange} required fullWidth margin="dense" error={!!localErrors.monto} helperText={localErrors.monto} InputProps={{ startAdornment: <InputAdornment position="start">$</InputAdornment> }} inputProps={{ step: "0.01" }} />
|
<TextField name="monto" label="Monto Pagado" type="number" value={formData.monto || ''} onChange={handleInputChange} required fullWidth margin="dense" error={!!localErrors.monto} helperText={localErrors.monto} InputProps={{ startAdornment: <InputAdornment position="start">$</InputAdornment> }} inputProps={{ step: "0.01" }} />
|
||||||
<TextField name="referencia" label="Referencia (Opcional)" value={formData.referencia || ''} onChange={handleInputChange} fullWidth margin="dense" />
|
<TextField name="referencia" label="Referencia (Opcional)" value={formData.referencia || ''} onChange={handleInputChange} fullWidth margin="dense" />
|
||||||
<TextField name="observaciones" label="Observaciones (Opcional)" value={formData.observaciones || ''} onChange={handleInputChange} fullWidth margin="dense" multiline rows={2} />
|
<TextField name="observaciones" label="Observaciones (Opcional)" value={formData.observaciones || ''} onChange={handleInputChange} fullWidth margin="dense" multiline rows={2} />
|
||||||
|
|
||||||
{errorMessage && <Alert severity="error" sx={{ mt: 2 }}>{errorMessage}</Alert>}
|
{errorMessage && <Alert severity="error" sx={{ mt: 2 }}>{errorMessage}</Alert>}
|
||||||
|
|
||||||
<Box sx={{ mt: 3, display: 'flex', justifyContent: 'flex-end', gap: 1 }}>
|
<Box sx={{ mt: 3, display: 'flex', justifyContent: 'flex-end', gap: 1 }}>
|
||||||
<Button onClick={onClose} color="secondary" disabled={loading}>Cancelar</Button>
|
<Button onClick={onClose} color="secondary" disabled={loading}>Cancelar</Button>
|
||||||
<Button type="submit" variant="contained" disabled={loading || loadingFormasPago}>
|
<Button type="submit" variant="contained" disabled={loading || loadingFormasPago}>
|
||||||
{loading ? <CircularProgress size={24} /> : 'Registrar Pago'}
|
{loading ? <CircularProgress size={24} /> : 'Registrar Pago'}
|
||||||
</Button>
|
</Button>
|
||||||
</Box>
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
</Modal>
|
</Modal>
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ export interface FacturaConsolidadaDto {
|
|||||||
estadoFacturacion: string;
|
estadoFacturacion: string;
|
||||||
numeroFactura?: string | null;
|
numeroFactura?: string | null;
|
||||||
totalPagado: number;
|
totalPagado: number;
|
||||||
|
tipoFactura: 'Mensual' | 'Alta';
|
||||||
detalles: FacturaDetalleDto[];
|
detalles: FacturaDetalleDto[];
|
||||||
// Añadimos el id del suscriptor para que sea fácil pasarlo a los handlers
|
// Añadimos el id del suscriptor para que sea fácil pasarlo a los handlers
|
||||||
idSuscriptor: number;
|
idSuscriptor: number;
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ const meses = [
|
|||||||
|
|
||||||
const estadosPago = ['Pendiente', 'Pagada', 'Pagada Parcialmente', 'Rechazada', 'Anulada'];
|
const estadosPago = ['Pendiente', 'Pagada', 'Pagada Parcialmente', 'Rechazada', 'Anulada'];
|
||||||
const estadosFacturacion = ['Pendiente de Facturar', 'Facturado'];
|
const estadosFacturacion = ['Pendiente de Facturar', 'Facturado'];
|
||||||
|
const tiposFactura = ['Mensual', 'Alta'];
|
||||||
|
|
||||||
const SuscriptorRow: React.FC<{
|
const SuscriptorRow: React.FC<{
|
||||||
resumen: ResumenCuentaSuscriptorDto;
|
resumen: ResumenCuentaSuscriptorDto;
|
||||||
@@ -33,8 +34,6 @@ const SuscriptorRow: React.FC<{
|
|||||||
handleOpenHistorial: (factura: FacturaConsolidadaDto) => void;
|
handleOpenHistorial: (factura: FacturaConsolidadaDto) => void;
|
||||||
}> = ({ resumen, handleMenuOpen, handleOpenHistorial }) => {
|
}> = ({ resumen, handleMenuOpen, handleOpenHistorial }) => {
|
||||||
const [open, setOpen] = useState(false);
|
const [open, setOpen] = useState(false);
|
||||||
|
|
||||||
// Función para formatear moneda
|
|
||||||
const formatCurrency = (value: number) => `$${value.toFixed(2)}`;
|
const formatCurrency = (value: number) => `$${value.toFixed(2)}`;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -43,20 +42,16 @@ const SuscriptorRow: React.FC<{
|
|||||||
<TableCell><IconButton size="small" onClick={() => setOpen(!open)}>{open ? <KeyboardArrowUpIcon /> : <KeyboardArrowDownIcon />}</IconButton></TableCell>
|
<TableCell><IconButton size="small" onClick={() => setOpen(!open)}>{open ? <KeyboardArrowUpIcon /> : <KeyboardArrowDownIcon />}</IconButton></TableCell>
|
||||||
<TableCell component="th" scope="row">{resumen.nombreSuscriptor}</TableCell>
|
<TableCell component="th" scope="row">{resumen.nombreSuscriptor}</TableCell>
|
||||||
<TableCell align="right">
|
<TableCell align="right">
|
||||||
<Typography variant="body2" sx={{ fontWeight: 'bold', color: resumen.saldoPendienteTotal > 0 ? 'error.main' : 'success.main' }}>
|
<Typography variant="body2" sx={{ fontWeight: 'bold', color: resumen.saldoPendienteTotal > 0 ? 'error.main' : 'success.main' }}>{formatCurrency(resumen.saldoPendienteTotal)}</Typography>
|
||||||
{formatCurrency(resumen.saldoPendienteTotal)}
|
|
||||||
</Typography>
|
|
||||||
<Typography variant="caption" color="text.secondary">de {formatCurrency(resumen.importeTotal)}</Typography>
|
<Typography variant="caption" color="text.secondary">de {formatCurrency(resumen.importeTotal)}</Typography>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell colSpan={7}></TableCell>
|
<TableCell colSpan={6}></TableCell>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
<TableRow>
|
<TableRow>
|
||||||
<TableCell style={{ paddingBottom: 0, paddingTop: 0 }} colSpan={10}>
|
<TableCell style={{ paddingBottom: 0, paddingTop: 0 }} colSpan={9}> {/* <-- Ajustado para la nueva columna */}
|
||||||
<Collapse in={open} timeout="auto" unmountOnExit>
|
<Collapse in={open} timeout="auto" unmountOnExit>
|
||||||
<Box sx={{ margin: 1, padding: 2, backgroundColor: 'grey.50', borderRadius: 1 }}>
|
<Box sx={{ margin: 1, padding: 2, backgroundColor: 'grey.50', borderRadius: 1 }}>
|
||||||
<Typography variant="h6" gutterBottom component="div" sx={{ fontSize: '1rem', fontWeight: 'bold' }}>
|
<Typography variant="h6" gutterBottom component="div" sx={{ fontSize: '1rem', fontWeight: 'bold' }}>Facturas del Período para {resumen.nombreSuscriptor}</Typography>
|
||||||
Facturas del Período para {resumen.nombreSuscriptor}
|
|
||||||
</Typography>
|
|
||||||
<Table size="small">
|
<Table size="small">
|
||||||
<TableHead>
|
<TableHead>
|
||||||
<TableRow>
|
<TableRow>
|
||||||
@@ -64,6 +59,7 @@ const SuscriptorRow: React.FC<{
|
|||||||
<TableCell align="right">Importe Total</TableCell>
|
<TableCell align="right">Importe Total</TableCell>
|
||||||
<TableCell align="right">Pagado</TableCell>
|
<TableCell align="right">Pagado</TableCell>
|
||||||
<TableCell align="right">Saldo</TableCell>
|
<TableCell align="right">Saldo</TableCell>
|
||||||
|
<TableCell>Tipo Factura</TableCell>
|
||||||
<TableCell>Estado Pago</TableCell>
|
<TableCell>Estado Pago</TableCell>
|
||||||
<TableCell>Estado Facturación</TableCell>
|
<TableCell>Estado Facturación</TableCell>
|
||||||
<TableCell>Nro. Factura</TableCell>
|
<TableCell>Nro. Factura</TableCell>
|
||||||
@@ -77,35 +73,17 @@ const SuscriptorRow: React.FC<{
|
|||||||
<TableRow key={factura.idFactura}>
|
<TableRow key={factura.idFactura}>
|
||||||
<TableCell sx={{ fontWeight: 'medium' }}>{factura.nombreEmpresa}</TableCell>
|
<TableCell sx={{ fontWeight: 'medium' }}>{factura.nombreEmpresa}</TableCell>
|
||||||
<TableCell align="right">{formatCurrency(factura.importeFinal)}</TableCell>
|
<TableCell align="right">{formatCurrency(factura.importeFinal)}</TableCell>
|
||||||
<TableCell align="right" sx={{ color: 'success.dark' }}>
|
<TableCell align="right" sx={{ color: 'success.dark' }}>{formatCurrency(factura.totalPagado)}</TableCell>
|
||||||
{formatCurrency(factura.totalPagado)}
|
<TableCell align="right" sx={{ fontWeight: 'bold', color: saldo > 0 ? 'error.main' : 'inherit' }}>{formatCurrency(saldo)}</TableCell>
|
||||||
</TableCell>
|
|
||||||
<TableCell align="right" sx={{ fontWeight: 'bold', color: saldo > 0 ? 'error.main' : 'inherit' }}>
|
|
||||||
{formatCurrency(saldo)}
|
|
||||||
</TableCell>
|
|
||||||
<TableCell>
|
<TableCell>
|
||||||
<Chip
|
<Chip label={factura.tipoFactura} size="small" color={factura.tipoFactura === 'Alta' ? 'secondary' : 'default'} />
|
||||||
label={factura.estadoPago}
|
|
||||||
size="small"
|
|
||||||
color={
|
|
||||||
factura.estadoPago === 'Pagada' ? 'success' :
|
|
||||||
factura.estadoPago === 'Pagada Parcialmente' ? 'primary' :
|
|
||||||
factura.estadoPago === 'Rechazada' ? 'error' :
|
|
||||||
'default'
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</TableCell>
|
</TableCell>
|
||||||
|
<TableCell><Chip label={factura.estadoPago} size="small" color={factura.estadoPago === 'Pagada' ? 'success' : (factura.estadoPago === 'Pagada Parcialmente' ? 'primary' : (factura.estadoPago === 'Rechazada' ? 'error' : 'default'))} /></TableCell>
|
||||||
<TableCell><Chip label={factura.estadoFacturacion} size="small" color={factura.estadoFacturacion === 'Facturado' ? 'info' : 'warning'} /></TableCell>
|
<TableCell><Chip label={factura.estadoFacturacion} size="small" color={factura.estadoFacturacion === 'Facturado' ? 'info' : 'warning'} /></TableCell>
|
||||||
<TableCell>{factura.numeroFactura || '-'}</TableCell>
|
<TableCell>{factura.numeroFactura || '-'}</TableCell>
|
||||||
<TableCell align="right">
|
<TableCell align="right">
|
||||||
<IconButton onClick={(e) => handleMenuOpen(e, factura)} disabled={factura.estadoPago === 'Anulada'}>
|
<IconButton onClick={(e) => handleMenuOpen(e, factura)} disabled={factura.estadoPago === 'Anulada'}><MoreVertIcon /></IconButton>
|
||||||
<MoreVertIcon />
|
<Tooltip title="Ver Historial de Envíos"><IconButton onClick={() => handleOpenHistorial(factura)}><MailOutlineIcon /></IconButton></Tooltip>
|
||||||
</IconButton>
|
|
||||||
<Tooltip title="Ver Historial de Envíos">
|
|
||||||
<IconButton onClick={() => handleOpenHistorial(factura)}>
|
|
||||||
<MailOutlineIcon />
|
|
||||||
</IconButton>
|
|
||||||
</Tooltip>
|
|
||||||
</TableCell>
|
</TableCell>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
);
|
);
|
||||||
@@ -135,6 +113,7 @@ const ConsultaFacturasPage: React.FC = () => {
|
|||||||
const [filtroNombre, setFiltroNombre] = useState('');
|
const [filtroNombre, setFiltroNombre] = useState('');
|
||||||
const [filtroEstadoPago, setFiltroEstadoPago] = useState('');
|
const [filtroEstadoPago, setFiltroEstadoPago] = useState('');
|
||||||
const [filtroEstadoFacturacion, setFiltroEstadoFacturacion] = useState('');
|
const [filtroEstadoFacturacion, setFiltroEstadoFacturacion] = useState('');
|
||||||
|
const [filtroTipoFactura, setFiltroTipoFactura] = useState('');
|
||||||
|
|
||||||
const [selectedFactura, setSelectedFactura] = useState<FacturaConsolidadaDto | null>(null);
|
const [selectedFactura, setSelectedFactura] = useState<FacturaConsolidadaDto | null>(null);
|
||||||
const [anchorEl, setAnchorEl] = useState<null | HTMLElement>(null);
|
const [anchorEl, setAnchorEl] = useState<null | HTMLElement>(null);
|
||||||
@@ -154,7 +133,8 @@ const ConsultaFacturasPage: React.FC = () => {
|
|||||||
selectedMes,
|
selectedMes,
|
||||||
filtroNombre || undefined,
|
filtroNombre || undefined,
|
||||||
filtroEstadoPago || undefined,
|
filtroEstadoPago || undefined,
|
||||||
filtroEstadoFacturacion || undefined
|
filtroEstadoFacturacion || undefined,
|
||||||
|
filtroTipoFactura || undefined
|
||||||
);
|
);
|
||||||
setResumenes(data);
|
setResumenes(data);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
@@ -163,7 +143,7 @@ const ConsultaFacturasPage: React.FC = () => {
|
|||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
}, [selectedAnio, selectedMes, puedeConsultar, filtroNombre, filtroEstadoPago, filtroEstadoFacturacion]);
|
}, [selectedAnio, selectedMes, puedeConsultar, filtroNombre, filtroEstadoPago, filtroEstadoFacturacion, filtroTipoFactura]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const timer = setTimeout(() => {
|
const timer = setTimeout(() => {
|
||||||
@@ -251,6 +231,17 @@ const ConsultaFacturasPage: React.FC = () => {
|
|||||||
<TextField label="Buscar por Suscriptor" size="small" value={filtroNombre} onChange={(e) => setFiltroNombre(e.target.value)} sx={{ flexGrow: 1, minWidth: '200px' }} />
|
<TextField label="Buscar por Suscriptor" size="small" value={filtroNombre} onChange={(e) => setFiltroNombre(e.target.value)} sx={{ flexGrow: 1, minWidth: '200px' }} />
|
||||||
<FormControl sx={{ minWidth: 200 }} size="small"><InputLabel>Estado de Pago</InputLabel><Select value={filtroEstadoPago} label="Estado de Pago" onChange={(e) => setFiltroEstadoPago(e.target.value)}><MenuItem value=""><em>Todos</em></MenuItem>{estadosPago.map(e => <MenuItem key={e} value={e}>{e}</MenuItem>)}</Select></FormControl>
|
<FormControl sx={{ minWidth: 200 }} size="small"><InputLabel>Estado de Pago</InputLabel><Select value={filtroEstadoPago} label="Estado de Pago" onChange={(e) => setFiltroEstadoPago(e.target.value)}><MenuItem value=""><em>Todos</em></MenuItem>{estadosPago.map(e => <MenuItem key={e} value={e}>{e}</MenuItem>)}</Select></FormControl>
|
||||||
<FormControl sx={{ minWidth: 200 }} size="small"><InputLabel>Estado de Facturación</InputLabel><Select value={filtroEstadoFacturacion} label="Estado de Facturación" onChange={(e) => setFiltroEstadoFacturacion(e.target.value)}><MenuItem value=""><em>Todos</em></MenuItem>{estadosFacturacion.map(e => <MenuItem key={e} value={e}>{e}</MenuItem>)}</Select></FormControl>
|
<FormControl sx={{ minWidth: 200 }} size="small"><InputLabel>Estado de Facturación</InputLabel><Select value={filtroEstadoFacturacion} label="Estado de Facturación" onChange={(e) => setFiltroEstadoFacturacion(e.target.value)}><MenuItem value=""><em>Todos</em></MenuItem>{estadosFacturacion.map(e => <MenuItem key={e} value={e}>{e}</MenuItem>)}</Select></FormControl>
|
||||||
|
<FormControl sx={{ minWidth: 200 }} size="small">
|
||||||
|
<InputLabel>Tipo de Factura</InputLabel>
|
||||||
|
<Select
|
||||||
|
value={filtroTipoFactura}
|
||||||
|
label="Tipo de Factura"
|
||||||
|
onChange={(e) => setFiltroTipoFactura(e.target.value)}
|
||||||
|
>
|
||||||
|
<MenuItem value=""><em>Todos</em></MenuItem>
|
||||||
|
{tiposFactura.map(t => <MenuItem key={t} value={t}>{t}</MenuItem>)}
|
||||||
|
</Select>
|
||||||
|
</FormControl>
|
||||||
</Box>
|
</Box>
|
||||||
</Paper>
|
</Paper>
|
||||||
|
|
||||||
@@ -259,7 +250,7 @@ const ConsultaFacturasPage: React.FC = () => {
|
|||||||
|
|
||||||
<TableContainer component={Paper}>
|
<TableContainer component={Paper}>
|
||||||
<Table aria-label="collapsible table">
|
<Table aria-label="collapsible table">
|
||||||
<TableHead><TableRow><TableCell /><TableCell>Suscriptor</TableCell><TableCell align="right">Saldo Total / Importe Total</TableCell><TableCell colSpan={5}></TableCell></TableRow></TableHead>
|
<TableHead><TableRow><TableCell /><TableCell>Suscriptor</TableCell><TableCell align="right">Saldo Total / Importe Total</TableCell><TableCell colSpan={6}></TableCell></TableRow></TableHead>
|
||||||
<TableBody>
|
<TableBody>
|
||||||
{loading ? (<TableRow><TableCell colSpan={8} align="center"><CircularProgress /></TableCell></TableRow>)
|
{loading ? (<TableRow><TableCell colSpan={8} align="center"><CircularProgress /></TableCell></TableRow>)
|
||||||
: resumenes.length === 0 ? (<TableRow><TableCell colSpan={8} align="center">No hay facturas para el período seleccionado.</TableCell></TableRow>)
|
: resumenes.length === 0 ? (<TableRow><TableCell colSpan={8} align="center">No hay facturas para el período seleccionado.</TableCell></TableRow>)
|
||||||
@@ -285,22 +276,9 @@ const ConsultaFacturasPage: React.FC = () => {
|
|||||||
open={pagoModalOpen}
|
open={pagoModalOpen}
|
||||||
onClose={handleClosePagoModal}
|
onClose={handleClosePagoModal}
|
||||||
onSubmit={handleSubmitPagoModal}
|
onSubmit={handleSubmitPagoModal}
|
||||||
factura={
|
factura={selectedFactura}
|
||||||
selectedFactura ? {
|
nombreSuscriptor={
|
||||||
idFactura: selectedFactura.idFactura,
|
resumenes.find(r => r.idSuscriptor === selectedFactura?.idSuscriptor)?.nombreSuscriptor || ''
|
||||||
nombreSuscriptor: resumenes.find(r => r.idSuscriptor === resumenes.find(res => res.facturas.some(f => f.idFactura === selectedFactura.idFactura))?.idSuscriptor)?.nombreSuscriptor || '',
|
|
||||||
importeFinal: selectedFactura.importeFinal,
|
|
||||||
saldoPendiente: selectedFactura.importeFinal - selectedFactura.totalPagado,
|
|
||||||
totalPagado: selectedFactura.totalPagado,
|
|
||||||
idSuscriptor: resumenes.find(res => res.facturas.some(f => f.idFactura === selectedFactura.idFactura))?.idSuscriptor || 0,
|
|
||||||
periodo: '',
|
|
||||||
fechaEmision: '',
|
|
||||||
fechaVencimiento: '',
|
|
||||||
estadoPago: selectedFactura.estadoPago,
|
|
||||||
estadoFacturacion: selectedFactura.estadoFacturacion,
|
|
||||||
numeroFactura: selectedFactura.numeroFactura,
|
|
||||||
detalles: selectedFactura.detalles,
|
|
||||||
} : null
|
|
||||||
}
|
}
|
||||||
errorMessage={apiError}
|
errorMessage={apiError}
|
||||||
clearErrorMessage={() => setApiError(null)}
|
clearErrorMessage={() => setApiError(null)}
|
||||||
|
|||||||
@@ -19,11 +19,16 @@ const procesarArchivoRespuesta = async (archivo: File): Promise<ProcesamientoLot
|
|||||||
return response.data;
|
return response.data;
|
||||||
};
|
};
|
||||||
|
|
||||||
const getResumenesDeCuentaPorPeriodo = async (anio: number, mes: number, nombreSuscriptor?: string, estadoPago?: string, estadoFacturacion?: string): Promise<ResumenCuentaSuscriptorDto[]> => {
|
const getResumenesDeCuentaPorPeriodo = async (
|
||||||
|
anio: number, mes: number,
|
||||||
|
nombreSuscriptor?: string, estadoPago?: string, estadoFacturacion?: string,
|
||||||
|
tipoFactura?: string
|
||||||
|
): Promise<ResumenCuentaSuscriptorDto[]> => {
|
||||||
const params = new URLSearchParams();
|
const params = new URLSearchParams();
|
||||||
if (nombreSuscriptor) params.append('nombreSuscriptor', nombreSuscriptor);
|
if (nombreSuscriptor) params.append('nombreSuscriptor', nombreSuscriptor);
|
||||||
if (estadoPago) params.append('estadoPago', estadoPago);
|
if (estadoPago) params.append('estadoPago', estadoPago);
|
||||||
if (estadoFacturacion) params.append('estadoFacturacion', estadoFacturacion);
|
if (estadoFacturacion) params.append('estadoFacturacion', estadoFacturacion);
|
||||||
|
if (tipoFactura) params.append('tipoFactura', tipoFactura);
|
||||||
|
|
||||||
const queryString = params.toString();
|
const queryString = params.toString();
|
||||||
const url = `${API_URL}/${anio}/${mes}${queryString ? `?${queryString}` : ''}`;
|
const url = `${API_URL}/${anio}/${mes}${queryString ? `?${queryString}` : ''}`;
|
||||||
@@ -81,10 +86,10 @@ const getHistorialLotesEnvio = async (anio?: number, mes?: number): Promise<Lote
|
|||||||
const params = new URLSearchParams();
|
const params = new URLSearchParams();
|
||||||
if (anio) params.append('anio', String(anio));
|
if (anio) params.append('anio', String(anio));
|
||||||
if (mes) params.append('mes', String(mes));
|
if (mes) params.append('mes', String(mes));
|
||||||
|
|
||||||
const queryString = params.toString();
|
const queryString = params.toString();
|
||||||
const url = `${API_URL}/historial-lotes-envio${queryString ? `?${queryString}` : ''}`;
|
const url = `${API_URL}/historial-lotes-envio${queryString ? `?${queryString}` : ''}`;
|
||||||
|
|
||||||
const response = await apiClient.get<LoteDeEnvioHistorialDto[]>(url);
|
const response = await apiClient.get<LoteDeEnvioHistorialDto[]>(url);
|
||||||
return response.data;
|
return response.data;
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user