Este commit introduce una refactorización significativa en el módulo de
suscripciones para alinear el sistema con reglas de negocio clave:
facturación consolidada por empresa, cobro a mes adelantado con
imputación de ajustes diferida, y una interfaz de usuario más clara.
Backend:
- **Facturación por Empresa:** Se modifica `FacturacionService` para
  agrupar las suscripciones por cliente y empresa, generando una
  factura consolidada para cada combinación. Esto asegura la correcta
  separación fiscal.
- **Imputación de Ajustes:** Se ajusta la lógica para que la facturación
  de un período (ej. Septiembre) aplique únicamente los ajustes
  pendientes cuya fecha corresponde al período anterior (Agosto).
- **Cierre Secuencial:** Se implementa una validación en
  `GenerarFacturacionMensual` que impide generar la facturación de un
  período si el anterior no ha sido cerrado, garantizando el orden
  cronológico.
- **Emails Consolidados:** El proceso de notificación automática al
  generar el cierre ahora envía un único email consolidado por
  suscriptor, detallando los cargos de todas sus facturas/empresas.
- **Envío de PDF Individual:** Se refactoriza el endpoint de envío manual
  para que opere sobre una `idFactura` individual y adjunte el PDF
  correspondiente si existe.
- **Repositorios Mejorados:** Se optimizan y añaden métodos en
  `FacturaRepository` y `AjusteRepository` para soportar los nuevos
  requisitos de filtrado y consulta de datos consolidados.
Frontend:
- **Separación de Vistas:** La página de "Facturación" se divide en dos:
  - `ProcesosPage`: Para acciones masivas (generar cierre, archivo de
    débito, procesar respuesta).
  - `ConsultaFacturasPage`: Una nueva página dedicada a buscar,
    filtrar y gestionar facturas individuales con una interfaz de doble
    acordeón (Suscriptor -> Empresa).
- **Filtros Avanzados:** La página `ConsultaFacturasPage` ahora incluye
  filtros por nombre de suscriptor, estado de pago y estado de
  facturación.
- **Filtros de Fecha por Defecto:** La página de "Cuenta Corriente"
  ahora filtra por el mes actual por defecto para mejorar el rendimiento
  y la usabilidad.
- **Validación de Fechas:** Se añade lógica en los filtros de fecha para
  impedir la selección de rangos inválidos.
- **Validación de Monto de Pago:** El modal de pago manual ahora impide
  registrar un monto superior al saldo pendiente de la factura.
		
	
		
			
				
	
	
		
			240 lines
		
	
	
		
			16 KiB
		
	
	
	
		
			C#
		
	
	
	
	
	
			
		
		
	
	
			240 lines
		
	
	
		
			16 KiB
		
	
	
	
		
			C#
		
	
	
	
	
	
| using GestionIntegral.Api.Dtos.Reportes;
 | |
| using GestionIntegral.Api.Dtos.Reportes.ViewModels;
 | |
| using QuestPDF.Fluent;
 | |
| using QuestPDF.Helpers;
 | |
| using QuestPDF.Infrastructure;
 | |
| using System.Globalization;
 | |
| 
 | |
| namespace GestionIntegral.Api.Controllers.Reportes.PdfTemplates
 | |
| {
 | |
|     public class DistribucionCanillasDocument : IDocument
 | |
|     {
 | |
|         public DistribucionCanillasViewModel Model { get; }
 | |
|         private static readonly CultureInfo CultureAr = new CultureInfo("es-AR");
 | |
| 
 | |
|         public DistribucionCanillasDocument(DistribucionCanillasViewModel model)
 | |
|         {
 | |
|             Model = model;
 | |
|         }
 | |
| 
 | |
|         public DocumentMetadata GetMetadata() => DocumentMetadata.Default;
 | |
| 
 | |
|         public void Compose(IDocumentContainer container)
 | |
|         {
 | |
|             container.Page(page =>
 | |
|             {
 | |
|                 page.Margin(1, Unit.Centimetre);
 | |
|                 page.DefaultTextStyle(x => x.FontFamily("Arial").FontSize(9));
 | |
|                 
 | |
|                 page.Header().Element(ComposeHeader);
 | |
|                 page.Content().Element(ComposeContent);
 | |
|                 page.Footer().AlignCenter().Text(x => { x.Span("Página "); x.CurrentPageNumber(); });
 | |
|             });
 | |
|         }
 | |
| 
 | |
|         void ComposeHeader(IContainer container)
 | |
|         {
 | |
|             container.Column(column =>
 | |
|             {
 | |
|                 column.Item().Row(row => {
 | |
|                     row.RelativeItem().Text("Listado de Distribución: Canillas / Accionistas").FontSize(12);
 | |
|                     row.RelativeItem().AlignRight().Text(text => {
 | |
|                         text.Span("Fecha Consultada ").SemiBold().FontSize(12);
 | |
|                         text.Span(Model.FechaConsultada).FontSize(12);
 | |
|                     });
 | |
|                 });
 | |
|                 column.Item().PaddingTop(5).Row(row => {
 | |
|                     row.RelativeItem().Text(text => {
 | |
|                          text.Span("Fecha de Generación del Reporte ").SemiBold().FontSize(12);
 | |
|                          text.Span(Model.FechaReporte);
 | |
|                     });
 | |
|                      row.RelativeItem().AlignRight().Text(text => {
 | |
|                          text.Span("Empresa ").SemiBold().FontSize(12);
 | |
|                          text.Span(Model.Empresa).FontSize(12);
 | |
|                     });
 | |
|                 });
 | |
|             });
 | |
|         }
 | |
|         
 | |
|         void ComposeContent(IContainer container)
 | |
|         {
 | |
|             container.PaddingTop(5, Unit.Millimetre).Column(column =>
 | |
|             {
 | |
|                 column.Spacing(15);
 | |
|                 
 | |
|                 if(Model.Canillas.Any())
 | |
|                     column.Item().Element(c => ComposeTablaDetallada(c, "Canillas", Model.Canillas));
 | |
|                 
 | |
|                 if(Model.CanillasAccionistas.Any())
 | |
|                     column.Item().Element(c => ComposeTablaDetallada(c, "Accionistas", Model.CanillasAccionistas));
 | |
| 
 | |
|                 if(Model.CanillasLiquidadasOtraFecha.Any())
 | |
|                     column.Item().Element(c => ComposeTablaLiquidacion(c, "Liquidación de movimientos de otras fechas canillas", Model.CanillasLiquidadasOtraFecha));
 | |
| 
 | |
|                 if(Model.CanillasAccionistasLiquidadasOtraFecha.Any())
 | |
|                     column.Item().Element(c => ComposeTablaLiquidacion(c, "Liquidación de movimientos de otras fechas accionistas", Model.CanillasAccionistasLiquidadasOtraFecha));
 | |
| 
 | |
|                 if(Model.CanillasTodos.Any())
 | |
|                     column.Item().Element(c => ComposeTablaTotales(c, "Recuento de Publicaciones", Model.CanillasTodos));
 | |
|                 
 | |
|                 if(Model.RemitoIngresado > 0)
 | |
|                     column.Item().Element(ComposeResumenDevoluciones);
 | |
|             });
 | |
|         }
 | |
|         
 | |
|         void ComposeTablaDetallada(IContainer container, string title, IEnumerable<DetalleDistribucionCanillaDto> data)
 | |
|         {
 | |
|             container.Column(column =>
 | |
|             {
 | |
|                 column.Item().PaddingBottom(4).Text(title).SemiBold().FontSize(11);
 | |
|                 column.Item().Table(table =>
 | |
|                 {
 | |
|                     table.ColumnsDefinition(columns =>
 | |
|                     {
 | |
|                         columns.RelativeColumn(3); columns.RelativeColumn(3);
 | |
|                         columns.RelativeColumn(1); columns.RelativeColumn(1);
 | |
|                         columns.RelativeColumn(1); columns.RelativeColumn(1.2f);
 | |
|                     });
 | |
|                     table.Header(header =>
 | |
|                     {
 | |
|                         header.Cell().Border(1).Background(Colors.Grey.Lighten3).Padding(2).Text("Vendedor");
 | |
|                         header.Cell().Border(1).Background(Colors.Grey.Lighten3).Padding(2).Text("Publicación");
 | |
|                         header.Cell().Border(1).Background(Colors.Grey.Lighten3).Padding(2).AlignRight().Text("Llevados");
 | |
|                         header.Cell().Border(1).Background(Colors.Grey.Lighten3).Padding(2).AlignRight().Text("Devueltos");
 | |
|                         header.Cell().Border(1).Background(Colors.Grey.Lighten3).Padding(2).AlignRight().Text("Vendidos");
 | |
|                         header.Cell().Border(1).Background(Colors.Grey.Lighten3).Padding(2).AlignRight().Text("A Rendir");
 | |
|                     });
 | |
|                     foreach (var grupoCanilla in data.GroupBy(c => c.Canilla))
 | |
|                     {
 | |
|                         var primeraFila = true;
 | |
|                         // El RowSpan es el número de publicaciones + la fila de total
 | |
|                         var totalFilasGrupo = grupoCanilla.Count() + 1;
 | |
|                         foreach (var item in grupoCanilla.OrderBy(p => p.Publicacion))
 | |
|                         {
 | |
|                             if (primeraFila)
 | |
|                             {
 | |
|                                 table.Cell().RowSpan((uint)totalFilasGrupo).Border(1).Padding(2).AlignTop().Text(item.Canilla);
 | |
|                                 primeraFila = false;
 | |
|                             }
 | |
|                             table.Cell().Border(1).Padding(2).Text(item.Publicacion);
 | |
|                             table.Cell().Border(1).Padding(2).AlignRight().Text(item.TotalCantSalida.ToString("N0"));
 | |
|                             table.Cell().Border(1).Padding(2).AlignRight().Text(item.TotalCantEntrada.ToString("N0"));
 | |
|                             table.Cell().Border(1).Padding(2).AlignRight().Text((item.TotalCantSalida - item.TotalCantEntrada).ToString("N0"));
 | |
|                             table.Cell().Border(1).Padding(2).AlignRight().Text(item.TotalRendir.ToString("C", CultureAr));
 | |
|                         }
 | |
|                         // Subtotal por canilla
 | |
|                         table.Cell().Border(1).Padding(2).AlignRight().Text("Totales").SemiBold();
 | |
|                         table.Cell().Border(1).Padding(2).AlignRight().Text(t => t.Span(grupoCanilla.Sum(i => i.TotalCantSalida).ToString("N0")).SemiBold());
 | |
|                         table.Cell().Border(1).Padding(2).AlignRight().Text(t => t.Span(grupoCanilla.Sum(i => i.TotalCantEntrada).ToString("N0")).SemiBold());
 | |
|                         table.Cell().Border(1).Padding(2).AlignRight().Text(t => t.Span(grupoCanilla.Sum(i => i.TotalCantSalida - i.TotalCantEntrada).ToString("N0")).SemiBold());
 | |
|                         table.Cell().Border(1).Padding(2).AlignRight().Text(t => t.Span(grupoCanilla.Sum(i => i.TotalRendir).ToString("C", CultureAr)).SemiBold());
 | |
|                     }
 | |
|                     
 | |
|                     var boldStyle = TextStyle.Default.ExtraBold();
 | |
|                     table.Cell().ColumnSpan(2).BorderTop(2).BorderColor(Colors.Black).Padding(2).AlignRight().Text(t => t.Span("Total " + title).Style(boldStyle));
 | |
|                     table.Cell().BorderTop(2).BorderColor(Colors.Black).Padding(2).AlignRight().Text(t => t.Span(data.Sum(i => i.TotalCantSalida).ToString("N0")).Style(boldStyle));
 | |
|                     table.Cell().BorderTop(2).BorderColor(Colors.Black).Padding(2).AlignRight().Text(t => t.Span(data.Sum(i => i.TotalCantEntrada).ToString("N0")).Style(boldStyle));
 | |
|                     table.Cell().BorderTop(2).BorderColor(Colors.Black).Padding(2).AlignRight().Text(t => t.Span(data.Sum(i => i.TotalCantSalida - i.TotalCantEntrada).ToString("N0")).Style(boldStyle));
 | |
|                     table.Cell().BorderTop(2).BorderColor(Colors.Black).Padding(2).AlignRight().Text(t => t.Span(data.Sum(i => i.TotalRendir).ToString("C", CultureAr)).Style(boldStyle));
 | |
|                 });
 | |
|             });
 | |
|         }
 | |
| 
 | |
|         void ComposeTablaLiquidacion(IContainer container, string title, IEnumerable<DetalleDistribucionCanillaDto> data)
 | |
|         {
 | |
|             container.Column(column =>
 | |
|             {
 | |
|                 column.Item().PaddingBottom(4).Text(title).SemiBold().FontSize(11);
 | |
|                 column.Item().Table(table =>
 | |
|                 {
 | |
|                     table.ColumnsDefinition(columns =>
 | |
|                     {
 | |
|                         columns.RelativeColumn(3); columns.RelativeColumn(1.5f);
 | |
|                         columns.RelativeColumn(3); columns.RelativeColumn(1);
 | |
|                         columns.RelativeColumn(1); columns.RelativeColumn(1);
 | |
|                         columns.RelativeColumn(1.2f);
 | |
|                     });
 | |
|                     table.Header(header =>
 | |
|                     {
 | |
|                         header.Cell().Border(1).Background(Colors.Grey.Lighten3).Padding(2).Text("Vendedor");
 | |
|                         header.Cell().Border(1).Background(Colors.Grey.Lighten3).Padding(2).Text("Fecha");
 | |
|                         header.Cell().Border(1).Background(Colors.Grey.Lighten3).Padding(2).Text("Publicación");
 | |
|                         header.Cell().Border(1).Background(Colors.Grey.Lighten3).Padding(2).AlignRight().Text("Llevados");
 | |
|                         header.Cell().Border(1).Background(Colors.Grey.Lighten3).Padding(2).AlignRight().Text("Devueltos");
 | |
|                         header.Cell().Border(1).Background(Colors.Grey.Lighten3).Padding(2).AlignRight().Text("Vendidos");
 | |
|                         header.Cell().Border(1).Background(Colors.Grey.Lighten3).Padding(2).AlignRight().Text("A Rendir");
 | |
|                     });
 | |
|                     foreach(var item in data.OrderBy(d => d.Canilla).ThenBy(d => d.Fecha))
 | |
|                     {
 | |
|                         table.Cell().Border(1).Padding(2).Text(item.Canilla);
 | |
|                         table.Cell().Border(1).Padding(2).Text(item.Fecha?.ToString("dd/MM/yyyy") ?? "");
 | |
|                         table.Cell().Border(1).Padding(2).Text(item.Publicacion);
 | |
|                         table.Cell().Border(1).Padding(2).AlignRight().Text(item.TotalCantSalida.ToString("N0"));
 | |
|                         table.Cell().Border(1).Padding(2).AlignRight().Text(item.TotalCantEntrada.ToString("N0"));
 | |
|                         table.Cell().Border(1).Padding(2).AlignRight().Text((item.TotalCantSalida - item.TotalCantEntrada).ToString("N0"));
 | |
|                         table.Cell().Border(1).Padding(2).AlignRight().Text(item.TotalRendir.ToString("C", CultureAr));
 | |
|                     }
 | |
|                     var boldStyle = TextStyle.Default.ExtraBold();
 | |
|                     table.Cell().ColumnSpan(3).BorderTop(2).BorderColor(Colors.Black).Padding(2).AlignRight().Text(t => t.Span("Total").Style(boldStyle));
 | |
|                     table.Cell().BorderTop(2).BorderColor(Colors.Black).Padding(2).AlignRight().Text(t => t.Span(data.Sum(i => i.TotalCantSalida).ToString("N0")).Style(boldStyle));
 | |
|                     table.Cell().BorderTop(2).BorderColor(Colors.Black).Padding(2).AlignRight().Text(t => t.Span(data.Sum(i => i.TotalCantEntrada).ToString("N0")).Style(boldStyle));
 | |
|                     table.Cell().BorderTop(2).BorderColor(Colors.Black).Padding(2).AlignRight().Text(t => t.Span(data.Sum(i => i.TotalCantSalida - i.TotalCantEntrada).ToString("N0")).Style(boldStyle));
 | |
|                     table.Cell().BorderTop(2).BorderColor(Colors.Black).Padding(2).AlignRight().Text(t => t.Span(data.Sum(i => i.TotalRendir).ToString("C", CultureAr)).Style(boldStyle));
 | |
|                 });
 | |
|             });
 | |
|         }
 | |
|         
 | |
|         void ComposeTablaTotales(IContainer container, string title, IEnumerable<DetalleDistribucionCanillaAllDto> data)
 | |
|         {
 | |
|              container.Column(column =>
 | |
|             {
 | |
|                 column.Item().PaddingBottom(4).Text(title).SemiBold().FontSize(11);
 | |
|                 column.Item().Table(table =>
 | |
|                 {
 | |
|                     table.ColumnsDefinition(columns =>
 | |
|                     {
 | |
|                         columns.RelativeColumn(2); columns.RelativeColumn(3);
 | |
|                         columns.RelativeColumn(1.2f); columns.RelativeColumn(1.2f);
 | |
|                         columns.RelativeColumn(1.2f); columns.RelativeColumn(1.5f);
 | |
|                     });
 | |
|                     table.Header(header =>
 | |
|                     {
 | |
|                         header.Cell().Border(1).Background(Colors.Grey.Lighten3).Padding(2).Text("Tipo Vendedor");
 | |
|                         header.Cell().Border(1).Background(Colors.Grey.Lighten3).Padding(2).Text("Publicación");
 | |
|                         header.Cell().Border(1).Background(Colors.Grey.Lighten3).Padding(2).AlignRight().Text("Llevados");
 | |
|                         header.Cell().Border(1).Background(Colors.Grey.Lighten3).Padding(2).AlignRight().Text("Devueltos");
 | |
|                         header.Cell().Border(1).Background(Colors.Grey.Lighten3).Padding(2).AlignRight().Text("Vendidos");
 | |
|                         header.Cell().Border(1).Background(Colors.Grey.Lighten3).Padding(2).AlignRight().Text("A Rendir");
 | |
|                     });
 | |
|                     foreach(var item in data.OrderBy(d => d.TipoVendedor).ThenBy(d => d.Publicacion))
 | |
|                     {
 | |
|                         table.Cell().Border(1).Padding(2).Text(item.TipoVendedor);
 | |
|                         table.Cell().Border(1).Padding(2).Text(item.Publicacion);
 | |
|                         table.Cell().Border(1).Padding(2).AlignRight().Text(item.TotalCantSalida.ToString("N0"));
 | |
|                         table.Cell().Border(1).Padding(2).AlignRight().Text(item.TotalCantEntrada.ToString("N0"));
 | |
|                         table.Cell().Border(1).Padding(2).AlignRight().Text((item.TotalCantSalida-item.TotalCantEntrada).ToString("N0"));
 | |
|                         table.Cell().Border(1).Padding(2).AlignRight().Text(item.TotalRendir.ToString("C", CultureAr));
 | |
|                     }
 | |
|                     var boldStyle = TextStyle.Default.ExtraBold();
 | |
|                     table.Cell().ColumnSpan(2).BorderTop(2).BorderColor(Colors.Black).Padding(2).AlignRight().Text(t => t.Span("Total General").Style(boldStyle));
 | |
|                     table.Cell().BorderTop(2).BorderColor(Colors.Black).Padding(2).AlignRight().Text(t => t.Span(data.Sum(i => i.TotalCantSalida).ToString("N0")).Style(boldStyle));
 | |
|                     table.Cell().BorderTop(2).BorderColor(Colors.Black).Padding(2).AlignRight().Text(t => t.Span(data.Sum(i => i.TotalCantEntrada).ToString("N0")).Style(boldStyle));
 | |
|                     table.Cell().BorderTop(2).BorderColor(Colors.Black).Padding(2).AlignRight().Text(t => t.Span(data.Sum(i => i.TotalCantSalida - i.TotalCantEntrada).ToString("N0")).Style(boldStyle));
 | |
|                     table.Cell().BorderTop(2).BorderColor(Colors.Black).Padding(2).AlignRight().Text(t => t.Span(data.Sum(i => i.TotalRendir).ToString("C", CultureAr)).Style(boldStyle));
 | |
|                 });
 | |
|             });
 | |
|         }
 | |
| 
 | |
|         void ComposeResumenDevoluciones(IContainer container)
 | |
|         {
 | |
|             container.PaddingTop(5).Column(column => {
 | |
|                 column.Item().Row(row => {
 | |
|                     row.Spacing(15);
 | |
|                     row.AutoItem().Text(text => { text.Span("Remito: ").SemiBold().FontSize(11); text.Span(Model.RemitoIngresado.ToString("N0")).FontSize(11); });
 | |
|                     row.AutoItem().Text(text => { text.Span("Devolución: ").SemiBold().FontSize(11); text.Span(Model.DevolucionTotal.ToString("N0")).FontSize(11); });
 | |
|                     row.AutoItem().Text(text => { text.Span("Venta: ").SemiBold().FontSize(11); text.Span(Model.VentaTotal.ToString("N0")).FontSize(11); });
 | |
|                 });
 | |
|             });
 | |
|         }
 | |
|     }
 | |
| } |