From 35e8d803b92606638cd54cb7416fcd2839655690 Mon Sep 17 00:00:00 2001 From: dmolinari Date: Thu, 4 Dec 2025 10:19:36 -0300 Subject: [PATCH] =?UTF-8?q?Fix:=20Alinea=20c=C3=A1lculos=20del=20PDF=20y?= =?UTF-8?q?=20web=20en=20distribuci=C3=B3n=20de=20canillitas?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Este commit soluciona varias inconsistencias de cálculo y visualización que existían entre el reporte de distribución para canillitas en la web y el PDF generado. Los cambios principales incluyen: - **Cálculo de Promedios Generales:** Se implementa un promedio ponderado utilizando división decimal y redondeo matemático (`MidpointRounding.AwayFromZero`) en el backend. Esto soluciona las diferencias numéricas en los totales ("Prom. Llevados", "Prom. Devueltos", etc.) que eran causadas por la división de enteros. - **Corrección de % Devolución:** Se ajusta la fórmula en todo el reporte (web y PDF) para calcular correctamente el porcentaje de devolución (`Devueltos / Llevados`) en lugar del porcentaje de venta que se mostraba erróneamente. - **Orden de Columnas en PDF:** Se corrige el orden de las columnas "Prom. Devueltos" y "Prom. Ventas" que estaban intercambiadas en la fila "General" del PDF. - **Precisión en Redondeo Final:** Se refina el cálculo del "% Devolución General" para que se base en los totales sin redondear, eliminando una diferencia de 0.01% y logrando una paridad exacta con la interfaz web. --- .../ListadoDistribucionCanillasDocument.cs | 5 +- .../ListadoDistribucionCanillasViewModel.cs | 39 ++++++++-- Backend/GestionIntegral.Api/Program.cs | 2 +- ...ReporteListadoDistribucionCanillasPage.tsx | 73 +++++++++---------- 4 files changed, 70 insertions(+), 49 deletions(-) diff --git a/Backend/GestionIntegral.Api/Controllers/Reportes/PdfTemplates/ListadoDistribucionCanillasDocument.cs b/Backend/GestionIntegral.Api/Controllers/Reportes/PdfTemplates/ListadoDistribucionCanillasDocument.cs index f414595..8000a15 100644 --- a/Backend/GestionIntegral.Api/Controllers/Reportes/PdfTemplates/ListadoDistribucionCanillasDocument.cs +++ b/Backend/GestionIntegral.Api/Controllers/Reportes/PdfTemplates/ListadoDistribucionCanillasDocument.cs @@ -148,7 +148,7 @@ namespace GestionIntegral.Api.Controllers.Reportes.PdfTemplates foreach (var item in Model.PromediosPorDia.OrderBy(d => dayOrder.GetValueOrDefault(d.Dia, 99))) { - var porcDevolucion = item.Llevados > 0 ? (decimal)item.Devueltos * 100 / item.Llevados : 0; + var porcDevolucion = item.Promedio_Llevados > 0 ? (decimal)item.Promedio_Devueltos * 100 / item.Promedio_Llevados : 0; table.Cell().Border(1).Padding(3).Text(item.Dia); table.Cell().Border(1).Padding(3).AlignRight().Text(item.Cant.ToString("N0")); @@ -162,7 +162,6 @@ namespace GestionIntegral.Api.Controllers.Reportes.PdfTemplates var general = Model.PromedioGeneral; if (general != null) { - var porcDevolucionGeneral = general.Promedio_Llevados > 0 ? (decimal)general.Promedio_Devueltos * 100 / general.Promedio_Llevados : 0; var boldStyle = TextStyle.Default.SemiBold(); table.Cell().Border(1).Padding(3).Text(text => text.Span(general.Dia).Style(boldStyle)); @@ -170,7 +169,7 @@ namespace GestionIntegral.Api.Controllers.Reportes.PdfTemplates table.Cell().Border(1).Padding(3).AlignRight().Text(text => text.Span(general.Promedio_Llevados.ToString("N0")).Style(boldStyle)); table.Cell().Border(1).Padding(3).AlignRight().Text(text => text.Span(general.Promedio_Devueltos.ToString("N0")).Style(boldStyle)); table.Cell().Border(1).Padding(3).AlignRight().Text(text => text.Span(general.Promedio_Ventas.ToString("N0")).Style(boldStyle)); - table.Cell().Border(1).Padding(3).AlignRight().Text(text => text.Span(porcDevolucionGeneral.ToString("F2") + "%").Style(boldStyle)); + table.Cell().Border(1).Padding(3).AlignRight().Text(text => text.Span(Model.PorcentajeDevolucionGeneral.ToString("F2") + "%").Style(boldStyle)); } }); }); diff --git a/Backend/GestionIntegral.Api/Models/Dtos/Reportes/ViewModels/ListadoDistribucionCanillasViewModel.cs b/Backend/GestionIntegral.Api/Models/Dtos/Reportes/ViewModels/ListadoDistribucionCanillasViewModel.cs index c662ec1..8731e83 100644 --- a/Backend/GestionIntegral.Api/Models/Dtos/Reportes/ViewModels/ListadoDistribucionCanillasViewModel.cs +++ b/Backend/GestionIntegral.Api/Models/Dtos/Reportes/ViewModels/ListadoDistribucionCanillasViewModel.cs @@ -13,7 +13,7 @@ namespace GestionIntegral.Api.Dtos.Reportes.ViewModels public string FechaDesde { get; set; } = string.Empty; public string FechaHasta { get; set; } = string.Empty; public string FechaReporte { get; set; } = DateTime.Now.ToString("dd/MM/yyyy"); - + public ListadoDistribucionCanillasSimpleDto TotalesDetalleDiario { get @@ -29,7 +29,23 @@ namespace GestionIntegral.Api.Dtos.Reportes.ViewModels }; } } - + + public decimal PorcentajeDevolucionGeneral + { + get + { + if (PromediosPorDia == null || !PromediosPorDia.Any()) return 0; + + var totalPonderadoLlevados = PromediosPorDia.Sum(p => p.Promedio_Llevados * p.Cant); + var totalPonderadoDevueltos = PromediosPorDia.Sum(p => p.Promedio_Devueltos * p.Cant); + + if (totalPonderadoLlevados == 0) return 0; + + // Calculamos el porcentaje usando los totales ponderados para máxima precisión como lo hace el frontend. + return (decimal)totalPonderadoDevueltos * 100 / totalPonderadoLlevados; + } + } + // --- PROPIEDAD PARA LA FILA "GENERAL" --- public ListadoDistribucionCanillasPromedioDiaDto? PromedioGeneral { @@ -37,20 +53,27 @@ namespace GestionIntegral.Api.Dtos.Reportes.ViewModels { if (PromediosPorDia == null || !PromediosPorDia.Any()) return null; - // Sumamos los totales, no promediamos los promedios - var totalLlevados = PromediosPorDia.Sum(p => p.Llevados); - var totalDevueltos = PromediosPorDia.Sum(p => p.Devueltos); + // Sumamos los totales ponderados para cada columna + var totalPonderadoLlevados = PromediosPorDia.Sum(p => p.Promedio_Llevados * p.Cant); + var totalPonderadoDevueltos = PromediosPorDia.Sum(p => p.Promedio_Devueltos * p.Cant); + var totalPonderadoVentas = PromediosPorDia.Sum(p => p.Promedio_Ventas * p.Cant); var totalDias = PromediosPorDia.Sum(p => p.Cant); if (totalDias == 0) return null; + // Usamos Math.Round para un redondeo matemático estándar antes de la conversión. + // MidpointRounding.AwayFromZero asegura que .5 se redondee hacia arriba, igual que en JavaScript. + var promGeneralLlevados = (int)Math.Round((decimal)totalPonderadoLlevados / totalDias, MidpointRounding.AwayFromZero); + var promGeneralDevueltos = (int)Math.Round((decimal)totalPonderadoDevueltos / totalDias, MidpointRounding.AwayFromZero); + var promGeneralVentas = (int)Math.Round((decimal)totalPonderadoVentas / totalDias, MidpointRounding.AwayFromZero); + return new ListadoDistribucionCanillasPromedioDiaDto { Dia = "General", Cant = totalDias, - Promedio_Llevados = totalLlevados / totalDias, - Promedio_Devueltos = totalDevueltos / totalDias, - Promedio_Ventas = (totalLlevados - totalDevueltos) / totalDias + Promedio_Llevados = promGeneralLlevados, + Promedio_Devueltos = promGeneralDevueltos, + Promedio_Ventas = promGeneralVentas }; } } diff --git a/Backend/GestionIntegral.Api/Program.cs b/Backend/GestionIntegral.Api/Program.cs index 0815017..064ad81 100644 --- a/Backend/GestionIntegral.Api/Program.cs +++ b/Backend/GestionIntegral.Api/Program.cs @@ -187,7 +187,7 @@ builder.Services.AddCors(options => policy => { policy.WithOrigins( - "http://localhost:5174", // Para desarrollo local + "http://localhost:5173", // Para desarrollo local "https://gestion.eldiaservicios.com" // Para producción ) .AllowAnyHeader() diff --git a/Frontend/src/pages/Reportes/ReporteListadoDistribucionCanillasPage.tsx b/Frontend/src/pages/Reportes/ReporteListadoDistribucionCanillasPage.tsx index f485fb7..f996c79 100644 --- a/Frontend/src/pages/Reportes/ReporteListadoDistribucionCanillasPage.tsx +++ b/Frontend/src/pages/Reportes/ReporteListadoDistribucionCanillasPage.tsx @@ -69,8 +69,8 @@ const ReporteListadoDistribucionCanillasPage: React.FC = () => { setReportData(null); setDetalleDiarioCalculado([]); setPromediosPorDiaCalculado([]); - setTotalesDetalle({ llevados:0, devueltos:0, ventaNeta:0, promedioGeneralVentaNeta:0, porcentajeDevolucionGeneral:0 }); - setTotalesPromedios({ cantDias:0, promLlevados:0, promDevueltos:0, promVentas:0, porcentajeDevolucionGeneral:0}); + setTotalesDetalle({ llevados: 0, devueltos: 0, ventaNeta: 0, promedioGeneralVentaNeta: 0, porcentajeDevolucionGeneral: 0 }); + setTotalesPromedios({ cantDias: 0, promLlevados: 0, promDevueltos: 0, promVentas: 0, porcentajeDevolucionGeneral: 0 }); const pubService = (await import('../../services/Distribucion/publicacionService')).default; @@ -89,7 +89,7 @@ const ReporteListadoDistribucionCanillasPage: React.FC = () => { const llevados = item.llevados || 0; const devueltos = item.devueltos || 0; const ventaNeta = llevados - devueltos; - + if (llevados > 0) { diasConActividadDetalle++; acumuladoVentaNeta += ventaNeta; @@ -101,7 +101,7 @@ const ReporteListadoDistribucionCanillasPage: React.FC = () => { ...item, id: `simple-can-${index}`, // o simple-dist-${index} ventaNeta: ventaNeta, - promedio: promedioActual, + promedio: promedioActual, porcentajeDevolucion: llevados > 0 ? (devueltos / llevados) * 100 : 0, // Esto es % Devolución real }; }); @@ -110,45 +110,44 @@ const ReporteListadoDistribucionCanillasPage: React.FC = () => { const totalLlevadosDetalle = detalleCalculadoLocal.reduce((sum, item) => sum + (item.llevados || 0), 0); const totalDevueltosDetalle = detalleCalculadoLocal.reduce((sum, item) => sum + (item.devueltos || 0), 0); const totalVentaNetaDetalle = totalLlevadosDetalle - totalDevueltosDetalle; - + setTotalesDetalle({ - llevados: totalLlevadosDetalle, - devueltos: totalDevueltosDetalle, - ventaNeta: totalVentaNetaDetalle, - promedioGeneralVentaNeta: ultimoPromedioDetalle, - porcentajeDevolucionGeneral: totalLlevadosDetalle > 0 ? (totalDevueltosDetalle / totalLlevadosDetalle) * 100 : 0 + llevados: totalLlevadosDetalle, + devueltos: totalDevueltosDetalle, + ventaNeta: totalVentaNetaDetalle, + promedioGeneralVentaNeta: ultimoPromedioDetalle, + porcentajeDevolucionGeneral: totalLlevadosDetalle > 0 ? (totalDevueltosDetalle / totalLlevadosDetalle) * 100 : 0 }); // --- Cálculos para promedios y sus totales --- const promediosCalculadoLocal = data.promediosPorDia.map((item, index) => { const promLlevados = item.promedio_Llevados || 0; + const promDevueltos = item.promedio_Devueltos || 0; const promVentas = item.promedio_Ventas || 0; return { ...item, - id: `prom-can-${index}`, // o prom-dist-${index} - // LA COLUMNA EN EL PDF SE LLAMA "% Devolución" PERO PARECE SER "% VENTA" + id: `prom-can-${index}`, porcentajeColumnaPDF: promLlevados > 0 ? (promVentas / promLlevados) * 100 : 0, - porcentajeDevolucion: promLlevados > 0 ? (promVentas / promLlevados) * 100 : 0, + porcentajeDevolucion: promLlevados > 0 ? (promDevueltos / promLlevados) * 100 : 0, }; }); setPromediosPorDiaCalculado(promediosCalculadoLocal); - + const totalDiasProm = promediosCalculadoLocal.reduce((sum, item) => sum + (item.cant || 0), 0); const totalPonderadoLlevados = promediosCalculadoLocal.reduce((sum, item) => sum + ((item.promedio_Llevados || 0) * (item.cant || 0)), 0); - // const totalPonderadoDevueltos = promediosCalculadoLocal.reduce((sum, item) => sum + ((item.promedio_Devueltos || 0) * (item.cant || 0)), 0); // No se usa para el % del PDF + const totalPonderadoDevueltos = promediosCalculadoLocal.reduce((sum, item) => sum + ((item.promedio_Devueltos || 0) * (item.cant || 0)), 0); const totalPonderadoVentas = promediosCalculadoLocal.reduce((sum, item) => sum + ((item.promedio_Ventas || 0) * (item.cant || 0)), 0); + const promGeneralLlevados = totalDiasProm > 0 ? totalPonderadoLlevados / totalDiasProm : 0; + const promGeneralDevueltos = totalDiasProm > 0 ? totalPonderadoDevueltos / totalDiasProm : 0; + setTotalesPromedios({ cantDias: totalDiasProm, - promLlevados: totalDiasProm > 0 ? totalPonderadoLlevados / totalDiasProm : 0, - promDevueltos: totalDiasProm > 0 ? promediosCalculadoLocal.reduce((sum, item) => sum + (item.promedio_Devueltos || 0), 0) / promediosCalculadoLocal.length :0, // Promedio simple para mostrar + promLlevados: promGeneralLlevados, + promDevueltos: promGeneralDevueltos, promVentas: totalDiasProm > 0 ? totalPonderadoVentas / totalDiasProm : 0, - // Para la fila "General" de promedios, el PDF usa (Total Prom. Ventas / Total Prom. Llevados) * 100 - // Usaremos los promedios generales calculados aquí - porcentajeDevolucionGeneral: (totalDiasProm > 0 && (totalPonderadoLlevados / totalDiasProm) > 0) - ? ((totalPonderadoVentas / totalDiasProm) / (totalPonderadoLlevados / totalDiasProm)) * 100 - : 0, + porcentajeDevolucionGeneral: promGeneralLlevados > 0 ? (promGeneralDevueltos / promGeneralLlevados) * 100 : 0, }); setReportData({ detalleSimple: detalleCalculadoLocal, promediosPorDia: promediosCalculadoLocal }); @@ -280,16 +279,16 @@ const ReporteListadoDistribucionCanillasPage: React.FC = () => { {/* Contenedor para tus totales */} - General: - {totalesDetalle.llevados.toLocaleString('es-AR')} - {totalesDetalle.devueltos.toLocaleString('es-AR')} - {totalesDetalle.ventaNeta.toLocaleString('es-AR')} - {totalesDetalle.promedioGeneralVentaNeta.toLocaleString('es-AR', { maximumFractionDigits: 0 })} + General: + {totalesDetalle.llevados.toLocaleString('es-AR')} + {totalesDetalle.devueltos.toLocaleString('es-AR')} + {totalesDetalle.ventaNeta.toLocaleString('es-AR')} + {totalesDetalle.promedioGeneralVentaNeta.toLocaleString('es-AR', { maximumFractionDigits: 0 })} {totalesDetalle.porcentajeDevolucionGeneral.toLocaleString('es-AR', { maximumFractionDigits: 2 })}% ); - + // --- Custom Footer para Promedios por Día --- const CustomFooterPromedios = () => ( @@ -297,11 +296,11 @@ const ReporteListadoDistribucionCanillasPage: React.FC = () => { - General: - {totalesPromedios.cantDias.toLocaleString('es-AR')} - {totalesPromedios.promLlevados.toLocaleString('es-AR', { maximumFractionDigits: 0 })} - {totalesPromedios.promDevueltos.toLocaleString('es-AR', { maximumFractionDigits: 0 })} - {totalesPromedios.promVentas.toLocaleString('es-AR', { maximumFractionDigits: 0 })} + General: + {totalesPromedios.cantDias.toLocaleString('es-AR')} + {totalesPromedios.promLlevados.toLocaleString('es-AR', { maximumFractionDigits: 0 })} + {totalesPromedios.promDevueltos.toLocaleString('es-AR', { maximumFractionDigits: 0 })} + {totalesPromedios.promVentas.toLocaleString('es-AR', { maximumFractionDigits: 0 })} {totalesPromedios.porcentajeDevolucionGeneral.toLocaleString('es-AR', { minimumFractionDigits: 2, maximumFractionDigits: 2 })}% @@ -372,10 +371,10 @@ const ReporteListadoDistribucionCanillasPage: React.FC = () => { density="compact" slots={{ footer: CustomFooterPromedios }} hideFooterSelectedRowCount - sx={{ - '& .MuiTablePagination-root': { // Oculta el paginador por defecto - display: 'none', - }, + sx={{ + '& .MuiTablePagination-root': { // Oculta el paginador por defecto + display: 'none', + }, }} />