| | | 1 | | using System.Globalization; |
| | | 2 | | using System.IO.Compression; |
| | | 3 | | using System.Reflection; |
| | | 4 | | using FAU.DataAccess.Repositories; |
| | | 5 | | using FAU.Entidades; |
| | | 6 | | using FAU.Logica.DTOs; |
| | | 7 | | using QuestPDF.Fluent; |
| | | 8 | | using QuestPDF.Helpers; |
| | | 9 | | using QuestPDF.Infrastructure; |
| | | 10 | | |
| | | 11 | | namespace FAU.Logica.Services; |
| | | 12 | | |
| | | 13 | | public class ReciboService : IReciboService |
| | | 14 | | { |
| | | 15 | | private readonly IReciboRepository _repo; |
| | | 16 | | |
| | 24 | 17 | | public ReciboService(IReciboRepository repo) |
| | | 18 | | { |
| | 24 | 19 | | _repo = repo; |
| | 24 | 20 | | } |
| | | 21 | | |
| | | 22 | | public async Task<PagedResult<ReciboResumenDto>> ObtenerResumenesAsync(long periodoId, ReciboFiltroRequest filtro) |
| | | 23 | | { |
| | 4 | 24 | | if (filtro.Page.HasValue && filtro.Page < 1) filtro.Page = 1; |
| | 5 | 25 | | if (filtro.PageSize.HasValue) filtro.PageSize = Math.Clamp(filtro.PageSize.Value, 1, 100); |
| | | 26 | | |
| | 3 | 27 | | var version = await _repo.ObtenerVersionMaximaAsync(periodoId); |
| | 3 | 28 | | var (items, totalCount) = await _repo.ObtenerResumenesAsync(periodoId, version, filtro); |
| | | 29 | | |
| | 3 | 30 | | return new PagedResult<ReciboResumenDto> |
| | 3 | 31 | | { |
| | 3 | 32 | | Items = items, |
| | 3 | 33 | | Page = filtro.Page ?? 1, |
| | 3 | 34 | | PageSize = filtro.PageSize ?? totalCount, |
| | 3 | 35 | | TotalCount = totalCount |
| | 3 | 36 | | }; |
| | 3 | 37 | | } |
| | | 38 | | |
| | | 39 | | public async Task<ReciboDto?> ObtenerReciboAsync(long periodoId, long relacionId) |
| | | 40 | | { |
| | 4 | 41 | | var version = await _repo.ObtenerVersionMaximaAsync(periodoId); |
| | 4 | 42 | | return await _repo.ObtenerDatosReciboAsync(periodoId, version, relacionId); |
| | 4 | 43 | | } |
| | | 44 | | |
| | | 45 | | public async Task<byte[]> GenerarPdfAsync(long periodoId, long relacionId) |
| | | 46 | | { |
| | 14 | 47 | | var version = await _repo.ObtenerVersionMaximaAsync(periodoId); |
| | 14 | 48 | | var recibo = await _repo.ObtenerDatosReciboAsync(periodoId, version, relacionId) |
| | 14 | 49 | | ?? throw new KeyNotFoundException($"No se encontró liquidación para la relación {relacionId} en el período { |
| | | 50 | | |
| | 13 | 51 | | return GenerarPdfDesdeRecibo(recibo); |
| | 13 | 52 | | } |
| | | 53 | | |
| | | 54 | | public async Task<Stream> GenerarLoteZipAsync(long periodoId, ReciboFiltroRequest filtro) |
| | | 55 | | { |
| | 3 | 56 | | var version = await _repo.ObtenerVersionMaximaAsync(periodoId); |
| | 3 | 57 | | var (resumenes, _) = await _repo.ObtenerResumenesAsync(periodoId, version, filtro); |
| | | 58 | | |
| | 3 | 59 | | var memoryStream = new MemoryStream(); |
| | 3 | 60 | | using (var zip = new ZipArchive(memoryStream, ZipArchiveMode.Create, leaveOpen: true)) |
| | | 61 | | { |
| | 10 | 62 | | foreach (var resumen in resumenes) |
| | | 63 | | { |
| | 2 | 64 | | var recibo = await _repo.ObtenerDatosReciboAsync(periodoId, version, resumen.RelacionLaboralId); |
| | 2 | 65 | | if (recibo == null) continue; |
| | | 66 | | |
| | 1 | 67 | | var pdfBytes = GenerarPdfDesdeRecibo(recibo); |
| | 1 | 68 | | var entry = zip.CreateEntry($"recibo_{resumen.Cedula}.pdf", CompressionLevel.Fastest); |
| | 1 | 69 | | using var entryStream = entry.Open(); |
| | 1 | 70 | | await entryStream.WriteAsync(pdfBytes); |
| | 1 | 71 | | } |
| | 3 | 72 | | } |
| | | 73 | | |
| | 3 | 74 | | memoryStream.Position = 0; |
| | 3 | 75 | | return memoryStream; |
| | 3 | 76 | | } |
| | | 77 | | |
| | 1 | 78 | | private static readonly byte[]? _logoBytes = CargarLogo(); |
| | | 79 | | |
| | | 80 | | private static byte[]? CargarLogo() |
| | | 81 | | { |
| | 1 | 82 | | var asm = Assembly.GetExecutingAssembly(); |
| | 1 | 83 | | var nombre = asm.GetManifestResourceNames() |
| | 2 | 84 | | .FirstOrDefault(n => n.EndsWith("fau-logo.png", StringComparison.OrdinalIgnoreCase)); |
| | 1 | 85 | | if (nombre is null) return null; |
| | 1 | 86 | | using var stream = asm.GetManifestResourceStream(nombre)!; |
| | 1 | 87 | | using var ms = new MemoryStream(); |
| | 1 | 88 | | stream.CopyTo(ms); |
| | 1 | 89 | | return ms.ToArray(); |
| | 1 | 90 | | } |
| | | 91 | | |
| | | 92 | | private static byte[] GenerarPdfDesdeRecibo(ReciboDto recibo) |
| | | 93 | | { |
| | 14 | 94 | | var ci = CultureInfo.GetCultureInfo("es-UY"); |
| | 14 | 95 | | var nombreMes = ci.TextInfo.ToTitleCase(ci.DateTimeFormat.GetMonthName(recibo.Mes)); |
| | | 96 | | |
| | 14 | 97 | | var apellidos = string.IsNullOrWhiteSpace(recibo.SegundoApellido) |
| | 14 | 98 | | ? recibo.PrimerApellido |
| | 14 | 99 | | : $"{recibo.PrimerApellido} {recibo.SegundoApellido}"; |
| | 14 | 100 | | var nombres = string.IsNullOrWhiteSpace(recibo.SegundoNombre) |
| | 14 | 101 | | ? recibo.PrimerNombre |
| | 14 | 102 | | : $"{recibo.PrimerNombre} {recibo.SegundoNombre}"; |
| | 14 | 103 | | var apellidoNombre = $"{apellidos.ToUpper()}, {nombres.ToUpper()}"; |
| | | 104 | | |
| | 14 | 105 | | var (fecha1Label, fecha1Value, fecha2Label, fecha2Value) = ObtenerFechas(recibo); |
| | 14 | 106 | | var descuentosFooter = recibo.Nominal - recibo.ACobrar; |
| | | 107 | | |
| | 14 | 108 | | return Document.Create(container => |
| | 14 | 109 | | { |
| | 14 | 110 | | container.Page(page => |
| | 14 | 111 | | { |
| | 14 | 112 | | page.Size(PageSizes.A4); |
| | 14 | 113 | | page.MarginHorizontal(20); |
| | 14 | 114 | | page.MarginTop(15); |
| | 14 | 115 | | page.MarginBottom(15); |
| | 28 | 116 | | page.DefaultTextStyle(x => x.FontSize(7.5f).FontFamily("Courier New")); |
| | 14 | 117 | | |
| | 14 | 118 | | page.Content().Column(col => |
| | 14 | 119 | | { |
| | 14 | 120 | | // ── ENCABEZADO ──────────────────────────────────────── |
| | 14 | 121 | | col.Item().PaddingBottom(4).Row(row => |
| | 14 | 122 | | { |
| | 14 | 123 | | row.RelativeItem().Row(r => |
| | 14 | 124 | | { |
| | 14 | 125 | | if (_logoBytes is not null) |
| | 14 | 126 | | r.ConstantItem(90).Image(_logoBytes).FitArea(); |
| | 14 | 127 | | r.RelativeItem().PaddingLeft(_logoBytes is not null ? 6 : 0) |
| | 14 | 128 | | .Column(c => |
| | 14 | 129 | | { |
| | 14 | 130 | | c.Item().Text("FUERZA AÉREA").Bold().FontSize(13).FontFamily("Arial"); |
| | 14 | 131 | | c.Item().Text("URUGUAYA").Bold().FontSize(13).FontFamily("Arial"); |
| | 28 | 132 | | }); |
| | 28 | 133 | | }); |
| | 14 | 134 | | row.ConstantItem(165).Column(c => |
| | 14 | 135 | | { |
| | 14 | 136 | | c.Item().Border(1).Padding(6).Column(inner => |
| | 14 | 137 | | { |
| | 14 | 138 | | inner.Item().Text($" MES DE : {nombreMes} {recibo.Anio}"); |
| | 14 | 139 | | inner.Item().Text(" "); |
| | 28 | 140 | | }); |
| | 28 | 141 | | }); |
| | 28 | 142 | | }); |
| | 14 | 143 | | |
| | 14 | 144 | | // ── FILA DE ENCABEZADOS DEL FUNCIONARIO ─────────────── |
| | 14 | 145 | | col.Item().Border(1).Row(row => |
| | 14 | 146 | | { |
| | 14 | 147 | | row.ConstantItem(85).BorderRight(1).PaddingHorizontal(5).PaddingVertical(3) |
| | 14 | 148 | | .Text("CEDULA").Bold(); |
| | 14 | 149 | | row.ConstantItem(80).BorderRight(1).PaddingHorizontal(5).PaddingVertical(3) |
| | 14 | 150 | | .AlignCenter().Text("GDO").Bold(); |
| | 14 | 151 | | row.ConstantItem(32).BorderRight(1).PaddingHorizontal(5).PaddingVertical(3) |
| | 14 | 152 | | .AlignCenter().Text("ESC").Bold(); |
| | 14 | 153 | | row.RelativeItem().BorderRight(1).PaddingHorizontal(5).PaddingVertical(3) |
| | 14 | 154 | | .Text("APELLIDO Y NOMBRE").Bold(); |
| | 14 | 155 | | row.ConstantItem(22).BorderRight(1).PaddingHorizontal(4).PaddingVertical(3) |
| | 14 | 156 | | .AlignCenter().Text("UN").Bold(); |
| | 14 | 157 | | row.ConstantItem(22).BorderRight(1).PaddingHorizontal(4).PaddingVertical(3) |
| | 14 | 158 | | .AlignCenter().Text("CIA").Bold(); |
| | 14 | 159 | | row.ConstantItem(138).PaddingHorizontal(5).PaddingVertical(3) |
| | 14 | 160 | | .AlignCenter().Text("FECHAS").Bold(); |
| | 28 | 161 | | }); |
| | 14 | 162 | | |
| | 14 | 163 | | // ── FILA DE DATOS DEL FUNCIONARIO ───────────────────── |
| | 14 | 164 | | col.Item().BorderLeft(1).BorderRight(1).BorderBottom(1).Row(row => |
| | 14 | 165 | | { |
| | 14 | 166 | | row.ConstantItem(85).BorderRight(1).PaddingHorizontal(5).PaddingVertical(3) |
| | 14 | 167 | | .Text(recibo.Cedula); |
| | 14 | 168 | | row.ConstantItem(80).BorderRight(1).PaddingHorizontal(5).PaddingVertical(3) |
| | 14 | 169 | | .Text(recibo.Grado); |
| | 14 | 170 | | row.ConstantItem(32).BorderRight(1).PaddingHorizontal(5).PaddingVertical(3) |
| | 14 | 171 | | .Text(recibo.Escalafon); |
| | 14 | 172 | | row.RelativeItem().BorderRight(1).PaddingHorizontal(5).PaddingVertical(3) |
| | 14 | 173 | | .Text(apellidoNombre); |
| | 14 | 174 | | row.ConstantItem(22).BorderRight(1).PaddingHorizontal(4).PaddingVertical(3) |
| | 14 | 175 | | .AlignCenter().Text(recibo.UnidadCodigo); |
| | 14 | 176 | | row.ConstantItem(22).BorderRight(1).PaddingHorizontal(4).PaddingVertical(3) |
| | 14 | 177 | | .AlignCenter().Text(recibo.Compania); |
| | 14 | 178 | | row.ConstantItem(138).BorderLeft(1).PaddingHorizontal(5).PaddingVertical(3) |
| | 14 | 179 | | .Column(c => |
| | 14 | 180 | | { |
| | 14 | 181 | | if (fecha1Label != null) |
| | 7 | 182 | | c.Item().Text(t => |
| | 7 | 183 | | { |
| | 7 | 184 | | t.Span(fecha1Label).Bold(); |
| | 7 | 185 | | t.Span(fecha1Value ?? ""); |
| | 14 | 186 | | }); |
| | 14 | 187 | | if (fecha2Label != null) |
| | 2 | 188 | | c.Item().Text(t => |
| | 2 | 189 | | { |
| | 2 | 190 | | t.Span(fecha2Label).Bold(); |
| | 2 | 191 | | t.Span(fecha2Value ?? ""); |
| | 4 | 192 | | }); |
| | 14 | 193 | | if (fecha1Label == null) |
| | 7 | 194 | | c.Item().Text(" "); // blank row for alignment |
| | 28 | 195 | | }); |
| | 28 | 196 | | }); |
| | 14 | 197 | | |
| | 14 | 198 | | // ── CUERPO DE 3 COLUMNAS ────────────────────────────── |
| | 14 | 199 | | col.Item().Extend() |
| | 14 | 200 | | .BorderLeft(1).BorderRight(1).BorderTop(1) |
| | 14 | 201 | | .Row(row => |
| | 14 | 202 | | { |
| | 14 | 203 | | // Columna 1: Haberes + Beneficios |
| | 14 | 204 | | row.RelativeItem(40).BorderRight(1).Column(c => |
| | 14 | 205 | | { |
| | 84 | 206 | | foreach (var item in recibo.Haberes) |
| | 28 | 207 | | FilaItem(c, item.Descripcion, item.Monto); |
| | 14 | 208 | | |
| | 14 | 209 | | FilaSeparador(c, "----------------"); |
| | 14 | 210 | | FilaTotalBold(c, "TOTAL DE HABERES", recibo.TotalHaberes); |
| | 14 | 211 | | |
| | 14 | 212 | | if (recibo.Beneficios.Any()) |
| | 14 | 213 | | { |
| | 1 | 214 | | c.Item().Text(" "); |
| | 4 | 215 | | foreach (var item in recibo.Beneficios) |
| | 1 | 216 | | FilaItem(c, item.Descripcion, item.Monto); |
| | 1 | 217 | | FilaSeparador(c, "----------------"); |
| | 1 | 218 | | FilaTotalBold(c, "TOTAL DE BENEFIC", recibo.TotalBeneficios); |
| | 14 | 219 | | } |
| | 28 | 220 | | }); |
| | 14 | 221 | | |
| | 14 | 222 | | // Columna 2: Descuentos Legales + ítems de Descuentos Personales |
| | 14 | 223 | | row.RelativeItem(35).BorderRight(1).Column(c => |
| | 14 | 224 | | { |
| | 84 | 225 | | foreach (var item in recibo.DescuentosLegales) |
| | 28 | 226 | | FilaItem(c, item.Descripcion, Math.Abs(item.Monto)); |
| | 14 | 227 | | |
| | 14 | 228 | | FilaSeparador(c, "------------"); |
| | 14 | 229 | | FilaTotalBold(c, "TOTAL DESC.", recibo.TotalDescuentosLegales); |
| | 14 | 230 | | |
| | 14 | 231 | | if (recibo.DescuentosPersonales.Any()) |
| | 14 | 232 | | { |
| | 12 | 233 | | c.Item().Text(" "); |
| | 48 | 234 | | foreach (var item in recibo.DescuentosPersonales) |
| | 12 | 235 | | FilaItem(c, item.Descripcion, Math.Abs(item.Monto)); |
| | 14 | 236 | | } |
| | 28 | 237 | | }); |
| | 14 | 238 | | |
| | 14 | 239 | | // Columna 3: Total de Descuentos Personales + Ajuste |
| | 14 | 240 | | row.RelativeItem(25).Column(c => |
| | 14 | 241 | | { |
| | 14 | 242 | | if (recibo.TotalDescuentosPersonales > 0) |
| | 14 | 243 | | { |
| | 13 | 244 | | FilaSeparador(c, "----------------"); |
| | 13 | 245 | | FilaTotalBold(c, "TOTAL DESC. PERS.", recibo.TotalDescuentosPersonales); |
| | 14 | 246 | | } |
| | 14 | 247 | | |
| | 14 | 248 | | if (recibo.Ajuste != 0) |
| | 14 | 249 | | { |
| | 2 | 250 | | if (recibo.TotalDescuentosPersonales > 0) |
| | 1 | 251 | | c.Item().Text(" "); |
| | 2 | 252 | | c.Item().Row(r => |
| | 2 | 253 | | { |
| | 2 | 254 | | r.RelativeItem().PaddingHorizontal(5).Text("AJUSTE"); |
| | 2 | 255 | | r.ConstantItem(60).AlignRight().PaddingHorizontal(5) |
| | 2 | 256 | | .Text(Monto(recibo.Ajuste)); |
| | 4 | 257 | | }); |
| | 14 | 258 | | } |
| | 28 | 259 | | }); |
| | 28 | 260 | | }); |
| | 28 | 261 | | }); |
| | 14 | 262 | | |
| | 14 | 263 | | // ── PIE: NOMINAL | DESCUENTOS | A COBRAR ───────────────── |
| | 14 | 264 | | page.Footer().Border(1).Row(row => |
| | 14 | 265 | | { |
| | 14 | 266 | | row.RelativeItem().BorderRight(1).Padding(5).Row(r => |
| | 14 | 267 | | { |
| | 28 | 268 | | r.RelativeItem().Text(t => t.Span("NOMINAL....: ").Bold()); |
| | 14 | 269 | | r.ConstantItem(75).AlignRight().Text(Monto(recibo.Nominal)); |
| | 28 | 270 | | }); |
| | 14 | 271 | | row.RelativeItem().BorderRight(1).Padding(5).Row(r => |
| | 14 | 272 | | { |
| | 28 | 273 | | r.RelativeItem().Text(t => t.Span("DESCUENTOS.: ").Bold()); |
| | 14 | 274 | | r.ConstantItem(75).AlignRight().Text(Monto(descuentosFooter)); |
| | 28 | 275 | | }); |
| | 14 | 276 | | row.RelativeItem().Padding(5).Row(r => |
| | 14 | 277 | | { |
| | 28 | 278 | | r.RelativeItem().Text(t => t.Span("A COBRAR...: ").Bold()); |
| | 14 | 279 | | r.ConstantItem(75).AlignRight().Text(Monto(recibo.ACobrar)); |
| | 28 | 280 | | }); |
| | 28 | 281 | | }); |
| | 28 | 282 | | }); |
| | 28 | 283 | | }).GeneratePdf(); |
| | | 284 | | } |
| | | 285 | | |
| | | 286 | | private static string Monto(decimal valor) => |
| | 155 | 287 | | valor.ToString("N2", CultureInfo.InvariantCulture); |
| | | 288 | | |
| | | 289 | | private static void FilaItem(ColumnDescriptor col, string descripcion, decimal monto) |
| | | 290 | | { |
| | 69 | 291 | | col.Item().Row(r => |
| | 69 | 292 | | { |
| | 69 | 293 | | r.RelativeItem().PaddingHorizontal(5).Text(descripcion); |
| | 69 | 294 | | r.ConstantItem(62).AlignRight().PaddingHorizontal(5).Text(Monto(monto)); |
| | 138 | 295 | | }); |
| | 69 | 296 | | } |
| | | 297 | | |
| | | 298 | | private static void FilaSeparador(ColumnDescriptor col, string _) |
| | | 299 | | { |
| | 42 | 300 | | col.Item().PaddingHorizontal(5).PaddingVertical(2).LineHorizontal(0.5f); |
| | 42 | 301 | | } |
| | | 302 | | |
| | | 303 | | private static void FilaTotalBold(ColumnDescriptor col, string label, decimal total) |
| | | 304 | | { |
| | 42 | 305 | | col.Item().Row(r => |
| | 42 | 306 | | { |
| | 42 | 307 | | r.RelativeItem().PaddingHorizontal(5).Text(label).Bold(); |
| | 42 | 308 | | r.ConstantItem(62).AlignRight().PaddingHorizontal(5).Text(Monto(total)).Bold(); |
| | 84 | 309 | | }); |
| | 42 | 310 | | } |
| | | 311 | | |
| | | 312 | | private static (string? l1, string? v1, string? l2, string? v2) ObtenerFechas(ReciboDto recibo) |
| | | 313 | | { |
| | | 314 | | const string fmt = "dd/MM/yyyy"; |
| | | 315 | | |
| | 14 | 316 | | if (recibo.EsCivil) |
| | 2 | 317 | | return ("Fecha Ing.(FIAP): ", recibo.FechaInicio != default |
| | 2 | 318 | | ? recibo.FechaInicio.ToString(fmt) : string.Empty, null, null); |
| | | 319 | | |
| | 12 | 320 | | if (recibo.EsOficial) |
| | | 321 | | { |
| | 2 | 322 | | var v1 = recibo.FechaInicio != default ? recibo.FechaInicio.ToString(fmt) : string.Empty; |
| | 2 | 323 | | if (recibo.FechaUltimoAscenso.HasValue) |
| | 1 | 324 | | return ("Asc. a Oficial: ", v1, |
| | 1 | 325 | | "Ult. Ascenso .: ", recibo.FechaUltimoAscenso.Value.ToString(fmt)); |
| | 1 | 326 | | return ("Asc. a Oficial: ", v1, null, null); |
| | | 327 | | } |
| | | 328 | | |
| | 10 | 329 | | if (recibo.EsSubalterno) |
| | | 330 | | { |
| | 2 | 331 | | var v1 = recibo.FechaInicio != default ? recibo.FechaInicio.ToString(fmt) : string.Empty; |
| | 2 | 332 | | if (recibo.FechaUltimoAscenso.HasValue) |
| | 1 | 333 | | return ("Fecha de Ingreso: ", v1, |
| | 1 | 334 | | "Fecha Ult. Asc. : ", recibo.FechaUltimoAscenso.Value.ToString(fmt)); |
| | 1 | 335 | | return ("Fecha de Ingreso: ", v1, null, null); |
| | | 336 | | } |
| | | 337 | | |
| | 8 | 338 | | if (recibo.FechaInicio != default) |
| | 1 | 339 | | return ("Fecha Ingreso: ", recibo.FechaInicio.ToString(fmt), null, null); |
| | | 340 | | |
| | 7 | 341 | | return (null, null, null, null); |
| | | 342 | | } |
| | | 343 | | } |