| | | 1 | | using ClosedXML.Excel; |
| | | 2 | | using FAU.DataAccess; |
| | | 3 | | using FAU.Entidades; |
| | | 4 | | using Microsoft.EntityFrameworkCore; |
| | | 5 | | |
| | | 6 | | namespace FAU.Logica.Services; |
| | | 7 | | |
| | | 8 | | public class AguinaldoPadronService : IAguinaldoPadronService |
| | | 9 | | { |
| | | 10 | | private readonly ApplicationDbContext _context; |
| | | 11 | | |
| | 10 | 12 | | public AguinaldoPadronService(ApplicationDbContext context) |
| | | 13 | | { |
| | 10 | 14 | | _context = context; |
| | 10 | 15 | | } |
| | | 16 | | |
| | | 17 | | public async Task<(byte[] Bytes, string FileName)> GenerarPadronExcelAsync(int anio, int mes, bool esLeyVieja) |
| | | 18 | | { |
| | 10 | 19 | | if (mes != 6 && mes != 12) |
| | 1 | 20 | | throw new ArgumentException("El mes debe ser 6 o 12.", nameof(mes)); |
| | | 21 | | |
| | 9 | 22 | | var semestre = (short)(mes == 6 ? 1 : 2); |
| | 9 | 23 | | var periodoDate = new DateTime(anio, mes, 1); |
| | | 24 | | |
| | | 25 | | // Cargar tramos de sanidad vigentes al período (mismo patrón que InsumosPeriodoLoader) |
| | 9 | 26 | | var tramosRaw = await _context.TramosSanidadMilitar |
| | 9 | 27 | | .AsNoTracking() |
| | 9 | 28 | | .Where(t => t.VigenteDesde <= periodoDate && (t.VigenteHasta == null || t.VigenteHasta >= periodoDate)) |
| | 9 | 29 | | .ToListAsync(); |
| | 9 | 30 | | var tramos = tramosRaw |
| | 2 | 31 | | .GroupBy(t => t.CodigoRetencion) |
| | 4 | 32 | | .Select(g => g.OrderByDescending(t => t.VigenteDesde).First()) |
| | 2 | 33 | | .OrderBy(t => t.Desde) |
| | 9 | 34 | | .ToList(); |
| | | 35 | | |
| | 9 | 36 | | var rawData = await ( |
| | 9 | 37 | | from a in _context.Aguinaldos |
| | 9 | 38 | | where a.Anio == (short)anio && a.Semestre == semestre |
| | 9 | 39 | | join rl in _context.RelacionesLaborales on a.PersonaId equals rl.PersonaId |
| | 9 | 40 | | where rl.Estado == EstadoRelacion.Activo && rl.Regimen.EsLeyVieja == esLeyVieja |
| | 9 | 41 | | select new |
| | 9 | 42 | | { |
| | 9 | 43 | | rl.Persona.Cedula, |
| | 9 | 44 | | rl.Persona.PrimerNombre, |
| | 9 | 45 | | rl.Persona.SegundoNombre, |
| | 9 | 46 | | rl.Persona.PrimerApellido, |
| | 9 | 47 | | rl.Persona.SegundoApellido, |
| | 9 | 48 | | GradoDenominacion = rl.Grado.Denominacion, |
| | 9 | 49 | | GradoOrden = rl.Grado.Orden, |
| | 9 | 50 | | EsOficial = rl.Grado.EsOficial, |
| | 9 | 51 | | EsSubalterno = rl.Grado.EsSubalterno, |
| | 9 | 52 | | SituacionCodigo = rl.Situacion.Codigo, |
| | 9 | 53 | | SituacionDenominacion = rl.Situacion.Denominacion, |
| | 9 | 54 | | ProgramaCodigo = rl.Programa.Codigo, |
| | 9 | 55 | | ProgramaDenominacion = rl.Programa.Denominacion, |
| | 9 | 56 | | Nominal = a.Nominal ?? 0m, |
| | 9 | 57 | | Montepio = a.Montepio ?? 0m, |
| | 9 | 58 | | Sanidad = a.Sanidad ?? 0m, |
| | 9 | 59 | | } |
| | 9 | 60 | | ).ToListAsync(); |
| | | 61 | | |
| | | 62 | | const decimal TasaSueldoRetiro = 0.01m; |
| | | 63 | | |
| | 9 | 64 | | var filas = rawData.Select(d => |
| | 9 | 65 | | { |
| | 6 | 66 | | var nominal = d.Nominal; |
| | 6 | 67 | | var sueldoRetiro = Math.Round(nominal * TasaSueldoRetiro, 2); |
| | 6 | 68 | | var codigoSanidad = ObtenerCodigoSanidad(nominal, tramos); |
| | 6 | 69 | | var liquido = Math.Round(nominal - sueldoRetiro - d.Montepio - d.Sanidad, 2); |
| | 6 | 70 | | return new FilaPadron( |
| | 6 | 71 | | d.Cedula, |
| | 6 | 72 | | FormatApellidosNombres(d.PrimerApellido, d.SegundoApellido, d.PrimerNombre, d.SegundoNombre), |
| | 6 | 73 | | d.GradoDenominacion, |
| | 6 | 74 | | d.GradoOrden, |
| | 6 | 75 | | d.EsOficial, |
| | 6 | 76 | | d.EsSubalterno, |
| | 6 | 77 | | d.SituacionCodigo, |
| | 6 | 78 | | d.SituacionDenominacion, |
| | 6 | 79 | | d.ProgramaCodigo, |
| | 6 | 80 | | d.ProgramaDenominacion, |
| | 6 | 81 | | nominal, |
| | 6 | 82 | | sueldoRetiro, |
| | 6 | 83 | | d.Montepio, |
| | 6 | 84 | | d.Sanidad, |
| | 6 | 85 | | codigoSanidad, |
| | 6 | 86 | | liquido); |
| | 9 | 87 | | }).ToList(); |
| | | 88 | | |
| | 9 | 89 | | var leyLabel = esLeyVieja ? "ley_vieja" : "ley_nueva"; |
| | 9 | 90 | | var bytes = GenerarExcel(filas, anio, mes, esLeyVieja); |
| | 9 | 91 | | return (bytes, $"padron_aguinaldo_{anio}_{mes:D2}_{leyLabel}.xlsx"); |
| | 9 | 92 | | } |
| | | 93 | | |
| | | 94 | | // Devuelve el CodigoRetencion del tramo que contiene el nominal (5010 / 5011 / 5012) |
| | | 95 | | private static int ObtenerCodigoSanidad(decimal nominal, IReadOnlyList<TramoSanidadMilitar> tramos) |
| | | 96 | | { |
| | 13 | 97 | | foreach (var t in tramos) |
| | 1 | 98 | | if (nominal >= t.Desde && (t.Hasta == null || nominal <= t.Hasta)) |
| | 1 | 99 | | return t.CodigoRetencion; |
| | 5 | 100 | | return tramos.Count > 0 ? tramos[^1].CodigoRetencion : 5012; |
| | 1 | 101 | | } |
| | | 102 | | |
| | | 103 | | private static byte[] GenerarExcel(List<FilaPadron> filas, int anio, int mes, bool esLeyVieja) |
| | | 104 | | { |
| | 9 | 105 | | using var wb = new XLWorkbook(); |
| | 9 | 106 | | var ws = wb.Worksheets.Add("Padrón"); |
| | | 107 | | |
| | 9 | 108 | | var semNombre = mes == 6 ? "1er Semestre" : "2do Semestre"; |
| | 9 | 109 | | var leyNombre = esLeyVieja ? "Ley Vieja (14.157)" : "Ley Nueva (19.695)"; |
| | | 110 | | const int TotalCols = 13; |
| | | 111 | | const int RightCol = 11; |
| | 9 | 112 | | int row = 1; |
| | | 113 | | |
| | | 114 | | // ── Cabezal ────────────────────────────────────────────────────────── |
| | 9 | 115 | | ws.Cell(row, 1).Value = "F.A.U. Dirección de Economía y Finanzas"; |
| | 9 | 116 | | ws.Range(row, 1, row, RightCol - 1).Merge(); |
| | 9 | 117 | | ws.Cell(row, RightCol).Value = $"Fecha: {DateTime.Now:dd/MM/yyyy}"; |
| | 9 | 118 | | ws.Range(row, RightCol, row, TotalCols).Merge(); |
| | 9 | 119 | | ws.Cell(row, RightCol).Style.Alignment.Horizontal = XLAlignmentHorizontalValues.Right; |
| | 9 | 120 | | row++; |
| | | 121 | | |
| | 9 | 122 | | ws.Cell(row, 1).Value = "Departamento de Administración Presupuestal"; |
| | 9 | 123 | | ws.Range(row, 1, row, RightCol - 1).Merge(); |
| | 9 | 124 | | ws.Cell(row, RightCol).Value = $"Hora : {DateTime.Now:HH:mm:ss}"; |
| | 9 | 125 | | ws.Range(row, RightCol, row, TotalCols).Merge(); |
| | 9 | 126 | | ws.Cell(row, RightCol).Style.Alignment.Horizontal = XLAlignmentHorizontalValues.Right; |
| | 9 | 127 | | row++; |
| | | 128 | | |
| | 9 | 129 | | ws.Cell(row, 1).Value = $"PADRÓN DE AGUINALDO | {leyNombre} | {semNombre} {anio}"; |
| | 9 | 130 | | ws.Range(row, 1, row, TotalCols).Merge(); |
| | 9 | 131 | | ws.Cell(row, 1).Style.Font.Bold = true; |
| | 9 | 132 | | ws.Cell(row, 1).Style.Alignment.Horizontal = XLAlignmentHorizontalValues.Center; |
| | 9 | 133 | | row += 2; |
| | | 134 | | |
| | | 135 | | // ── Cabecera de columnas (dos filas: grupos + individuales) ─────────── |
| | 9 | 136 | | int groupRow = row; |
| | 9 | 137 | | int colRow = row + 1; |
| | 9 | 138 | | int dataStartRow = row + 2; |
| | | 139 | | |
| | 9 | 140 | | var headerBg = XLColor.FromHtml("#4472C4"); |
| | | 141 | | |
| | 9 | 142 | | ws.Range(groupRow, 1, colRow, 1).Merge(); |
| | 9 | 143 | | ws.Cell(groupRow, 1).Value = "Cédula"; |
| | | 144 | | |
| | 9 | 145 | | ws.Range(groupRow, 2, colRow, 2).Merge(); |
| | 9 | 146 | | ws.Cell(groupRow, 2).Value = "Apellido y Nombre"; |
| | | 147 | | |
| | 9 | 148 | | ws.Cell(groupRow, 3).Value = "DISPON./"; |
| | 9 | 149 | | ws.Range(groupRow, 3, groupRow, 6).Merge(); |
| | | 150 | | |
| | 9 | 151 | | ws.Cell(groupRow, 7).Value = "Montepíos"; |
| | 9 | 152 | | ws.Range(groupRow, 7, groupRow, 9).Merge(); |
| | | 153 | | |
| | 9 | 154 | | ws.Cell(groupRow, 10).Value = "Sanidad"; |
| | 9 | 155 | | ws.Range(groupRow, 10, groupRow, 12).Merge(); |
| | | 156 | | |
| | 9 | 157 | | ws.Range(groupRow, 13, colRow, 13).Merge(); |
| | 9 | 158 | | ws.Cell(groupRow, 13).Value = "Líquido"; |
| | | 159 | | |
| | 9 | 160 | | ws.Cell(colRow, 3).Value = "Duodécimo"; |
| | 9 | 161 | | ws.Cell(colRow, 4).Value = "SUB.TRAN."; |
| | 9 | 162 | | ws.Cell(colRow, 5).Value = "Suel. Ret."; |
| | 9 | 163 | | ws.Cell(colRow, 6).Value = "S.Fun"; |
| | 9 | 164 | | ws.Cell(colRow, 7).Value = "Oficial"; |
| | 9 | 165 | | ws.Cell(colRow, 8).Value = "Tropa"; |
| | 9 | 166 | | ws.Cell(colRow, 9).Value = "Civil"; |
| | 9 | 167 | | ws.Cell(colRow, 10).Value = "5.010"; |
| | 9 | 168 | | ws.Cell(colRow, 11).Value = "5.011"; |
| | 9 | 169 | | ws.Cell(colRow, 12).Value = "5.012"; |
| | | 170 | | |
| | 54 | 171 | | foreach (var r in new[] { groupRow, colRow }) |
| | | 172 | | { |
| | 18 | 173 | | var rng = ws.Range(r, 1, r, TotalCols); |
| | 18 | 174 | | rng.Style.Font.Bold = true; |
| | 18 | 175 | | rng.Style.Fill.BackgroundColor = headerBg; |
| | 18 | 176 | | rng.Style.Font.FontColor = XLColor.White; |
| | 18 | 177 | | rng.Style.Alignment.Horizontal = XLAlignmentHorizontalValues.Center; |
| | 18 | 178 | | rng.Style.Alignment.Vertical = XLAlignmentVerticalValues.Center; |
| | 18 | 179 | | rng.Style.Alignment.WrapText = true; |
| | | 180 | | } |
| | | 181 | | |
| | 9 | 182 | | row = dataStartRow; |
| | | 183 | | |
| | 9 | 184 | | var totalGeneral = new TotalesPadron(); |
| | | 185 | | |
| | 9 | 186 | | var porPrograma = filas |
| | 6 | 187 | | .GroupBy(f => (f.ProgramaCodigo, f.ProgramaDenominacion)) |
| | 15 | 188 | | .OrderBy(g => g.Key.ProgramaCodigo); |
| | | 189 | | |
| | 30 | 190 | | foreach (var programaGroup in porPrograma) |
| | | 191 | | { |
| | 6 | 192 | | ws.Cell(row, 1).Value = $"Programa: {programaGroup.Key.ProgramaCodigo} - {programaGroup.Key.ProgramaDenomina |
| | 6 | 193 | | ws.Range(row, 1, row, TotalCols).Merge(); |
| | 6 | 194 | | ws.Cell(row, 1).Style.Font.Bold = true; |
| | 6 | 195 | | ws.Cell(row, 1).Style.Fill.BackgroundColor = XLColor.FromHtml("#D9E1F2"); |
| | 6 | 196 | | row++; |
| | | 197 | | |
| | 6 | 198 | | var totalPrograma = new TotalesPadron(); |
| | | 199 | | |
| | 6 | 200 | | var porSituacion = programaGroup |
| | 6 | 201 | | .GroupBy(f => (f.SituacionCodigo, f.SituacionDenominacion)) |
| | 12 | 202 | | .OrderBy(g => g.Key.SituacionCodigo); |
| | | 203 | | |
| | 24 | 204 | | foreach (var situacionGroup in porSituacion) |
| | | 205 | | { |
| | 6 | 206 | | ws.Cell(row, 1).Value = $" Situación: {situacionGroup.Key.SituacionCodigo} - {situacionGroup.Key.Situac |
| | 6 | 207 | | ws.Range(row, 1, row, TotalCols).Merge(); |
| | 6 | 208 | | ws.Cell(row, 1).Style.Font.Italic = true; |
| | 6 | 209 | | ws.Cell(row, 1).Style.Fill.BackgroundColor = XLColor.FromHtml("#EEF2F9"); |
| | 6 | 210 | | row++; |
| | | 211 | | |
| | 6 | 212 | | var totalSituacion = new TotalesPadron(); |
| | | 213 | | |
| | 36 | 214 | | foreach (var fila in situacionGroup.OrderBy(f => f.GradoOrden).ThenBy(f => f.NombreCompleto)) |
| | | 215 | | { |
| | 6 | 216 | | EscribirFilaPadron(ws, row, fila); |
| | 6 | 217 | | totalSituacion.Acumular(fila); |
| | 6 | 218 | | row++; |
| | | 219 | | } |
| | | 220 | | |
| | 6 | 221 | | EscribirSubtotalPadron(ws, row, TotalCols, $"Subtotal {situacionGroup.Key.SituacionDenominacion}", total |
| | 6 | 222 | | totalPrograma.Acumular(totalSituacion); |
| | 6 | 223 | | row++; |
| | | 224 | | } |
| | | 225 | | |
| | 6 | 226 | | EscribirSubtotalPadron(ws, row, TotalCols, $"Subtotal Programa {programaGroup.Key.ProgramaCodigo}", totalPro |
| | 6 | 227 | | totalGeneral.Acumular(totalPrograma); |
| | 6 | 228 | | row++; |
| | 6 | 229 | | row++; |
| | | 230 | | } |
| | | 231 | | |
| | 9 | 232 | | EscribirSubtotalPadron(ws, row, TotalCols, "TOTAL GENERAL", totalGeneral, XLColor.FromHtml("#4472C4"), XLColor.W |
| | | 233 | | |
| | 9 | 234 | | ws.Range(dataStartRow, 3, row, TotalCols).Style.NumberFormat.Format = "#,##0.00"; |
| | 9 | 235 | | ws.Columns().AdjustToContents(); |
| | | 236 | | |
| | 9 | 237 | | using var ms = new MemoryStream(); |
| | 9 | 238 | | wb.SaveAs(ms); |
| | 9 | 239 | | return ms.ToArray(); |
| | 9 | 240 | | } |
| | | 241 | | |
| | | 242 | | private static void EscribirFilaPadron(IXLWorksheet ws, int row, FilaPadron f) |
| | | 243 | | { |
| | 6 | 244 | | ws.Cell(row, 1).Value = f.Cedula; |
| | 6 | 245 | | ws.Cell(row, 2).Value = f.NombreCompleto; |
| | 6 | 246 | | ws.Cell(row, 3).Value = f.Nominal; |
| | | 247 | | // col 4: DISPON./SUB.TRAN — reservada (vacía por ahora) |
| | 6 | 248 | | ws.Cell(row, 5).Value = f.SueldoRetiro; |
| | | 249 | | |
| | | 250 | | // Montepío: solo una columna aplica por persona |
| | 6 | 251 | | if (f.EsOficial) |
| | 1 | 252 | | ws.Cell(row, 6).Value = f.Montepio; // S.Fun |
| | 5 | 253 | | else if (f.EsSubalterno) |
| | 4 | 254 | | ws.Cell(row, 7).Value = f.Montepio; // Oficial |
| | | 255 | | else |
| | 1 | 256 | | ws.Cell(row, 9).Value = f.Montepio; // Civil (col 8 = Tropa, vacía) |
| | | 257 | | |
| | | 258 | | // Sanidad: columna determinada por tramo (CodigoRetencion) |
| | 7 | 259 | | if (f.CodigoSanidad == 5010) ws.Cell(row, 10).Value = f.Sanidad; |
| | 5 | 260 | | else if (f.CodigoSanidad == 5011) ws.Cell(row, 11).Value = f.Sanidad; |
| | 5 | 261 | | else ws.Cell(row, 12).Value = f.Sanidad; |
| | | 262 | | |
| | 6 | 263 | | ws.Cell(row, 13).Value = f.Liquido; |
| | 6 | 264 | | } |
| | | 265 | | |
| | | 266 | | private static void EscribirSubtotalPadron( |
| | | 267 | | IXLWorksheet ws, int row, int totalCols, string etiqueta, |
| | | 268 | | TotalesPadron t, XLColor bgColor, |
| | | 269 | | XLColor? fontColor = null, int? fontSize = null) |
| | | 270 | | { |
| | 21 | 271 | | ws.Cell(row, 1).Value = etiqueta; |
| | 21 | 272 | | ws.Cell(row, 3).Value = t.Nominal; |
| | 21 | 273 | | ws.Cell(row, 5).Value = t.SueldoRetiro; |
| | 21 | 274 | | ws.Cell(row, 6).Value = t.MontepiSFun; |
| | 21 | 275 | | ws.Cell(row, 7).Value = t.MontepiOficial; |
| | 21 | 276 | | ws.Cell(row, 9).Value = t.MontepiCivil; |
| | 21 | 277 | | ws.Cell(row, 10).Value = t.San5010; |
| | 21 | 278 | | ws.Cell(row, 11).Value = t.San5011; |
| | 21 | 279 | | ws.Cell(row, 12).Value = t.San5012; |
| | 21 | 280 | | ws.Cell(row, 13).Value = t.Liquido; |
| | | 281 | | |
| | 21 | 282 | | var rango = ws.Range(row, 1, row, totalCols); |
| | 21 | 283 | | rango.Style.Font.Bold = true; |
| | 21 | 284 | | rango.Style.Fill.BackgroundColor = bgColor; |
| | 21 | 285 | | if (fontColor != null) |
| | 9 | 286 | | rango.Style.Font.FontColor = fontColor; |
| | 21 | 287 | | if (fontSize.HasValue) |
| | 9 | 288 | | rango.Style.Font.FontSize = fontSize.Value; |
| | 21 | 289 | | } |
| | | 290 | | |
| | | 291 | | private static string FormatApellidosNombres( |
| | | 292 | | string primerApellido, string? segundoApellido, |
| | | 293 | | string primerNombre, string? segundoNombre) |
| | | 294 | | { |
| | 6 | 295 | | var apellidos = primerApellido + (string.IsNullOrWhiteSpace(segundoApellido) ? "" : " " + segundoApellido); |
| | 6 | 296 | | var nombres = primerNombre + (string.IsNullOrWhiteSpace(segundoNombre) ? "" : " " + segundoNombre); |
| | 6 | 297 | | return $"{apellidos}, {nombres}"; |
| | | 298 | | } |
| | | 299 | | |
| | 6 | 300 | | private record FilaPadron( |
| | 6 | 301 | | string Cedula, |
| | 12 | 302 | | string NombreCompleto, |
| | 0 | 303 | | string GradoDenominacion, |
| | 6 | 304 | | short GradoOrden, |
| | 12 | 305 | | bool EsOficial, |
| | 10 | 306 | | bool EsSubalterno, |
| | 6 | 307 | | string SituacionCodigo, |
| | 6 | 308 | | string SituacionDenominacion, |
| | 6 | 309 | | string ProgramaCodigo, |
| | 6 | 310 | | string ProgramaDenominacion, |
| | 12 | 311 | | decimal Nominal, |
| | 12 | 312 | | decimal SueldoRetiro, |
| | 12 | 313 | | decimal Montepio, |
| | 12 | 314 | | decimal Sanidad, |
| | 22 | 315 | | int CodigoSanidad, // 5010 / 5011 / 5012 según tramo |
| | 18 | 316 | | decimal Liquido); |
| | | 317 | | |
| | | 318 | | private class TotalesPadron |
| | | 319 | | { |
| | 69 | 320 | | public decimal Nominal { get; private set; } |
| | 69 | 321 | | public decimal SueldoRetiro { get; private set; } |
| | 59 | 322 | | public decimal MontepiSFun { get; private set; } // EsOficial → col S.Fun |
| | 65 | 323 | | public decimal MontepiOficial { get; private set; } // EsSubalterno → col Oficial |
| | 59 | 324 | | public decimal MontepiCivil { get; private set; } // ni oficial ni subalterno → col Civil |
| | 59 | 325 | | public decimal San5010 { get; private set; } |
| | 57 | 326 | | public decimal San5011 { get; private set; } |
| | 67 | 327 | | public decimal San5012 { get; private set; } |
| | 69 | 328 | | public decimal Liquido { get; private set; } |
| | | 329 | | |
| | | 330 | | public void Acumular(FilaPadron f) |
| | | 331 | | { |
| | 6 | 332 | | Nominal += f.Nominal; |
| | 6 | 333 | | SueldoRetiro += f.SueldoRetiro; |
| | 7 | 334 | | if (f.EsOficial) MontepiSFun += f.Montepio; |
| | 9 | 335 | | else if (f.EsSubalterno) MontepiOficial += f.Montepio; |
| | 1 | 336 | | else MontepiCivil += f.Montepio; |
| | 7 | 337 | | if (f.CodigoSanidad == 5010) San5010 += f.Sanidad; |
| | 5 | 338 | | else if (f.CodigoSanidad == 5011) San5011 += f.Sanidad; |
| | 5 | 339 | | else San5012 += f.Sanidad; |
| | 6 | 340 | | Liquido += f.Liquido; |
| | 6 | 341 | | } |
| | | 342 | | |
| | | 343 | | public void Acumular(TotalesPadron t) |
| | | 344 | | { |
| | 12 | 345 | | Nominal += t.Nominal; |
| | 12 | 346 | | SueldoRetiro += t.SueldoRetiro; |
| | 12 | 347 | | MontepiSFun += t.MontepiSFun; |
| | 12 | 348 | | MontepiOficial += t.MontepiOficial; |
| | 12 | 349 | | MontepiCivil += t.MontepiCivil; |
| | 12 | 350 | | San5010 += t.San5010; |
| | 12 | 351 | | San5011 += t.San5011; |
| | 12 | 352 | | San5012 += t.San5012; |
| | 12 | 353 | | Liquido += t.Liquido; |
| | 12 | 354 | | } |
| | | 355 | | } |
| | | 356 | | } |