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