This commit is contained in:
2026-02-01 14:28:17 -06:00
parent 700af7ea60
commit 1784131456
109 changed files with 19894 additions and 0 deletions

View File

@@ -0,0 +1,276 @@
using Microsoft.AspNetCore.Http;
using Microsoft.EntityFrameworkCore;
using Rs_system.Data;
using Rs_system.Models;
using Rs_system.Models.ViewModels;
namespace Rs_system.Services;
public class ArticuloService : IArticuloService
{
private readonly ApplicationDbContext _context;
private readonly IFileStorageService _fileStorageService;
public ArticuloService(ApplicationDbContext context, IFileStorageService fileStorageService)
{
_context = context;
_fileStorageService = fileStorageService;
}
public async Task<IEnumerable<ArticuloViewModel>> GetAllAsync(string? search = null, int? categoriaId = null, int? ubicacionId = null, int? estadoId = null)
{
var query = _context.Articulos
.Include(a => a.Categoria)
.Include(a => a.Estado)
.Include(a => a.Ubicacion)
.Where(a => !a.Eliminado)
.AsQueryable();
if (!string.IsNullOrWhiteSpace(search))
{
var term = search.ToLower();
query = query.Where(a =>
a.Nombre.ToLower().Contains(term) ||
a.Codigo.ToLower().Contains(term) ||
a.Modelo.ToLower().Contains(term) ||
a.Marca.ToLower().Contains(term) ||
a.NumeroSerie.ToLower().Contains(term));
}
if (categoriaId.HasValue)
query = query.Where(a => a.CategoriaId == categoriaId);
if (ubicacionId.HasValue)
query = query.Where(a => a.UbicacionId == ubicacionId);
if (estadoId.HasValue)
query = query.Where(a => a.EstadoId == estadoId);
return await query
.OrderByDescending(a => a.CreadoEn)
.Select(a => new ArticuloViewModel
{
Id = a.Id,
Codigo = a.Codigo,
Nombre = a.Nombre,
Descripcion = a.Descripcion,
Marca = a.Marca,
Modelo = a.Modelo,
NumeroSerie = a.NumeroSerie,
Precio = a.Precio,
FechaAdquisicion = a.FechaAdquisicion,
ImagenUrl = a.ImagenUrl,
CategoriaId = a.CategoriaId,
CategoriaNombre = a.Categoria.Nombre,
EstadoId = a.EstadoId,
EstadoNombre = a.Estado.Nombre,
EstadoColor = a.Estado.Color,
UbicacionId = a.UbicacionId,
UbicacionNombre = a.Ubicacion.Nombre,
Activo = a.Activo
})
.ToListAsync();
}
public async Task<ArticuloViewModel?> GetByIdAsync(int id)
{
var a = await _context.Articulos
.Include(a => a.Categoria)
.Include(a => a.Estado)
.Include(a => a.Ubicacion)
.FirstOrDefaultAsync(x => x.Id == id && !x.Eliminado);
if (a == null) return null;
return new ArticuloViewModel
{
Id = a.Id,
Codigo = a.Codigo,
Nombre = a.Nombre,
Descripcion = a.Descripcion,
Marca = a.Marca,
Modelo = a.Modelo,
NumeroSerie = a.NumeroSerie,
Precio = a.Precio,
FechaAdquisicion = a.FechaAdquisicion,
ImagenUrl = a.ImagenUrl,
CategoriaId = a.CategoriaId,
CategoriaNombre = a.Categoria.Nombre,
EstadoId = a.EstadoId,
EstadoNombre = a.Estado.Nombre,
EstadoColor = a.Estado.Color,
UbicacionId = a.UbicacionId,
UbicacionNombre = a.Ubicacion.Nombre,
Activo = a.Activo,
CantidadGlobal = a.CantidadGlobal,
// New Fields
TipoControl = a.TipoControl,
CantidadInicial = a.CantidadGlobal // Map Global Qty to CantidadInicial for Display
};
}
public async Task<bool> CreateAsync(ArticuloViewModel viewModel, string createdBy)
{
var strategy = _context.Database.CreateExecutionStrategy();
try
{
await strategy.ExecuteAsync(async () =>
{
using var transaction = await _context.Database.BeginTransactionAsync();
string? imagenUrl = null;
if (viewModel.ImagenFile != null)
{
imagenUrl = await _fileStorageService.SaveFileAsync(viewModel.ImagenFile, "articulos");
}
var articulo = new Articulo
{
Codigo = viewModel.Codigo,
Nombre = viewModel.Nombre,
Descripcion = viewModel.Descripcion,
Marca = viewModel.Marca,
Modelo = viewModel.Modelo,
NumeroSerie = viewModel.NumeroSerie,
Precio = viewModel.Precio,
FechaAdquisicion = viewModel.FechaAdquisicion,
ImagenUrl = imagenUrl,
CategoriaId = viewModel.CategoriaId,
EstadoId = viewModel.EstadoId,
UbicacionId = viewModel.UbicacionId,
Activo = viewModel.Activo,
Eliminado = false,
CreadoPor = createdBy,
CreadoEn = DateTime.UtcNow,
ActualizadoEn = DateTime.UtcNow,
// New Fields
TipoControl = viewModel.TipoControl ?? nameof(Articulo.TipoControlInventario.UNITARIO),
CantidadGlobal = (viewModel.TipoControl == nameof(Articulo.TipoControlInventario.LOTE)) ? viewModel.CantidadInicial : 1
};
_context.Articulos.Add(articulo);
await _context.SaveChangesAsync();
// If LOTE, initialize Existencia
if (articulo.TipoControl == nameof(Articulo.TipoControlInventario.LOTE))
{
var existencia = new Existencia
{
ArticuloId = articulo.Id,
UbicacionId = articulo.UbicacionId,
Cantidad = articulo.CantidadGlobal,
ActualizadoEn = DateTime.UtcNow
};
_context.Existencias.Add(existencia);
await _context.SaveChangesAsync();
}
await transaction.CommitAsync();
});
return true;
}
catch
{
return false;
}
}
public async Task<bool> UpdateAsync(ArticuloViewModel viewModel)
{
try
{
var articulo = await _context.Articulos.FindAsync(viewModel.Id);
if (articulo == null || articulo.Eliminado) return false;
if (viewModel.ImagenFile != null)
{
if (!string.IsNullOrEmpty(articulo.ImagenUrl))
{
await _fileStorageService.DeleteFileAsync(articulo.ImagenUrl);
}
articulo.ImagenUrl = await _fileStorageService.SaveFileAsync(viewModel.ImagenFile, "articulos");
}
articulo.Codigo = viewModel.Codigo;
articulo.Nombre = viewModel.Nombre;
articulo.Descripcion = viewModel.Descripcion;
articulo.Marca = viewModel.Marca;
articulo.Modelo = viewModel.Modelo;
articulo.NumeroSerie = viewModel.NumeroSerie;
articulo.Precio = viewModel.Precio;
articulo.FechaAdquisicion = viewModel.FechaAdquisicion;
articulo.CategoriaId = viewModel.CategoriaId;
articulo.EstadoId = viewModel.EstadoId;
articulo.UbicacionId = viewModel.UbicacionId;
articulo.Activo = viewModel.Activo;
articulo.ActualizadoEn = DateTime.UtcNow;
_context.Articulos.Update(articulo);
await _context.SaveChangesAsync();
return true;
}
catch
{
return false;
}
}
public async Task<bool> DeleteAsync(int id)
{
try
{
var articulo = await _context.Articulos.FindAsync(id);
if (articulo == null || articulo.Eliminado) return false;
articulo.Eliminado = true;
articulo.ActualizadoEn = DateTime.UtcNow;
_context.Articulos.Update(articulo);
await _context.SaveChangesAsync();
return true;
}
catch
{
return false;
}
}
public async Task<bool> ExistsCodigoAsync(string codigo, int? excludeId = null)
{
var query = _context.Articulos.AsQueryable();
if (excludeId.HasValue)
{
query = query.Where(a => a.Id != excludeId.Value);
}
return await query.AnyAsync(a => a.Codigo.ToLower() == codigo.ToLower() && !a.Eliminado);
}
public async Task<IEnumerable<(int Id, string Nombre)>> GetCategoriasAsync()
{
return await _context.Categorias
.Where(x => x.Activo && !x.Eliminado)
.OrderBy(x => x.Nombre)
.Select(x => new ValueTuple<int, string>(x.Id, x.Nombre))
.ToListAsync();
}
public async Task<IEnumerable<(int Id, string Nombre, string Color)>> GetEstadosAsync()
{
return await _context.EstadosArticulos
.Where(x => x.Activo && !x.Eliminado)
.OrderBy(x => x.Nombre)
.Select(x => new ValueTuple<int, string, string>(x.Id, x.Nombre, x.Color ?? "secondary"))
.ToListAsync();
}
public async Task<IEnumerable<(int Id, string Nombre)>> GetUbicacionesAsync()
{
return await _context.Ubicaciones
.Where(x => x.Activo && !x.Eliminado)
.OrderBy(x => x.Nombre)
.Select(x => new ValueTuple<int, string>(x.Id, x.Nombre))
.ToListAsync();
}
}

View File

@@ -0,0 +1,103 @@
using Microsoft.EntityFrameworkCore;
using Rs_system.Data;
using Rs_system.Models;
namespace Rs_system.Services;
public class CategoriaService : ICategoriaService
{
private readonly ApplicationDbContext _context;
public CategoriaService(ApplicationDbContext context)
{
_context = context;
}
public async Task<IEnumerable<Categoria>> GetAllAsync()
{
return await _context.Categorias
.Where(c => !c.Eliminado)
.OrderBy(c => c.Nombre)
.ToListAsync();
}
public async Task<Categoria?> GetByIdAsync(int id)
{
return await _context.Categorias
.FirstOrDefaultAsync(c => c.Id == id && !c.Eliminado);
}
public async Task<bool> CreateAsync(Categoria categoria)
{
try
{
categoria.CreadoEn = DateTime.UtcNow;
categoria.ActualizadoEn = DateTime.UtcNow;
// Eliminado and Activo defaults are set in the model/DB, ensuring here just in case
categoria.Eliminado = false;
_context.Categorias.Add(categoria);
await _context.SaveChangesAsync();
return true;
}
catch
{
return false;
}
}
public async Task<bool> UpdateAsync(Categoria categoria)
{
try
{
var existing = await _context.Categorias.FindAsync(categoria.Id);
if (existing == null || existing.Eliminado) return false;
existing.Nombre = categoria.Nombre;
existing.Descripcion = categoria.Descripcion;
existing.Activo = categoria.Activo;
existing.ActualizadoEn = DateTime.UtcNow;
// CreadoPor and CreadoEn should not change
_context.Categorias.Update(existing);
await _context.SaveChangesAsync();
return true;
}
catch
{
return false;
}
}
public async Task<bool> DeleteAsync(int id)
{
try
{
var categoria = await _context.Categorias.FindAsync(id);
if (categoria == null || categoria.Eliminado) return false;
categoria.Eliminado = true;
categoria.ActualizadoEn = DateTime.UtcNow;
_context.Categorias.Update(categoria);
await _context.SaveChangesAsync();
return true;
}
catch
{
return false;
}
}
public async Task<bool> ExistsAsync(string nombre, int? excludeId = null)
{
var query = _context.Categorias.AsQueryable();
if (excludeId.HasValue)
{
query = query.Where(c => c.Id != excludeId.Value);
}
return await query.AnyAsync(c => c.Nombre.ToLower() == nombre.ToLower() && !c.Eliminado);
}
}

View File

@@ -0,0 +1,373 @@
using Microsoft.EntityFrameworkCore;
using Rs_system.Data;
using Rs_system.Models;
using Rs_system.Models.ViewModels;
using System.Globalization;
namespace Rs_system.Services;
public class ColaboracionService : IColaboracionService
{
private readonly ApplicationDbContext _context;
public ColaboracionService(ApplicationDbContext context)
{
_context = context;
}
public async Task<List<TipoColaboracion>> GetTiposActivosAsync()
{
return await _context.TiposColaboracion
.Where(t => t.Activo)
.OrderBy(t => t.Orden)
.AsNoTracking()
.ToListAsync();
}
public async Task<TipoColaboracion?> GetTipoByIdAsync(long id)
{
return await _context.TiposColaboracion
.AsNoTracking()
.FirstOrDefaultAsync(t => t.Id == id);
}
public async Task<Colaboracion> RegistrarColaboracionAsync(
RegistrarColaboracionViewModel model,
string registradoPor)
{
// Validar que el rango de fechas sea válido
var fechaInicial = new DateTime(model.AnioInicial, model.MesInicial, 1);
var fechaFinal = new DateTime(model.AnioFinal, model.MesFinal, 1);
if (fechaFinal < fechaInicial)
{
throw new ArgumentException("La fecha final no puede ser anterior a la fecha inicial");
}
// Obtener información de los tipos seleccionados
var tiposColaboracion = await _context.TiposColaboracion
.Where(t => model.TiposSeleccionados.Contains(t.Id))
.ToListAsync();
// Generar todos los meses en el rango
var mesesAPagar = GenerarRangoMeses(
model.AnioInicial, model.MesInicial,
model.AnioFinal, model.MesFinal);
// Crear colaboración principal
var colaboracion = new Colaboracion
{
MiembroId = model.MiembroId,
FechaRegistro = DateTime.UtcNow,
MontoTotal = model.MontoTotal,
Observaciones = model.Observaciones,
RegistradoPor = registradoPor,
CreadoEn = DateTime.UtcNow,
ActualizadoEn = DateTime.UtcNow
};
// Distribuir el monto total entre los meses y tipos
var detalles = DistribuirMonto(
model.MontoTotal,
tiposColaboracion,
mesesAPagar,
model.TipoPrioritario);
foreach (var detalle in detalles)
{
colaboracion.Detalles.Add(detalle);
}
_context.Colaboraciones.Add(colaboracion);
await _context.SaveChangesAsync();
return colaboracion;
}
private List<DetalleColaboracion> DistribuirMonto(
decimal montoTotal,
List<TipoColaboracion> tipos,
List<(int anio, int mes)> meses,
long? tipoPrioritario)
{
var detalles = new List<DetalleColaboracion>();
var montoRestante = montoTotal;
// Estrategia: Mes a Mes
// Para cada mes, intentamos cubrir los tipos (Prioritario primero)
foreach (var (anio, mes) in meses)
{
if (montoRestante <= 0) break;
// Ordenar tipos para este mes: Prioritario al inicio
var tiposOrdenados = new List<TipoColaboracion>();
if (tipoPrioritario.HasValue)
{
var prio = tipos.FirstOrDefault(t => t.Id == tipoPrioritario.Value);
if (prio != null)
{
tiposOrdenados.Add(prio);
tiposOrdenados.AddRange(tipos.Where(t => t.Id != tipoPrioritario.Value));
}
else
{
tiposOrdenados.AddRange(tipos);
}
}
else
{
tiposOrdenados.AddRange(tipos);
}
foreach (var tipo in tiposOrdenados)
{
if (montoRestante <= 0) break;
// Determinar cuánto asignar
// Intentamos cubrir el monto sugerido completo
var montoAAsignar = Math.Min(tipo.MontoSugerido, montoRestante);
// Si es un monto muy pequeño (ej: residuo), igual lo asignamos para no perderlo,
// salvo que queramos reglas estrictas de "solo completos".
// Por ahora asignamos lo que haya.
if (montoAAsignar > 0)
{
detalles.Add(new DetalleColaboracion
{
TipoColaboracionId = tipo.Id,
Mes = mes,
Anio = anio,
Monto = montoAAsignar,
CreadoEn = DateTime.UtcNow
});
montoRestante -= montoAAsignar;
}
}
}
return detalles;
}
public async Task<List<UltimoPagoViewModel>> GetUltimosPagosPorMiembroAsync(long miembroId)
{
// Obtener todos los detalles agrupados por tipo para encontrar la fecha máxima
var detalles = await _context.DetalleColaboraciones
.Include(d => d.Colaboracion)
.Include(d => d.TipoColaboracion)
.Where(d => d.Colaboracion.MiembroId == miembroId)
.ToListAsync();
var resultado = detalles
.GroupBy(d => d.TipoColaboracion)
.Select(g =>
{
// Encontrar el registro con el mes/año más reciente
var ultimo = g.OrderByDescending(d => d.Anio).ThenByDescending(d => d.Mes).FirstOrDefault();
if (ultimo == null) return null;
return new UltimoPagoViewModel
{
TipoId = g.Key.Id,
NombreTipo = g.Key.Nombre,
UltimoMes = ultimo.Mes,
UltimoAnio = ultimo.Anio,
FechaUltimoPago = ultimo.Colaboracion.FechaRegistro
};
})
.Where(x => x != null)
.ToList();
// Asegurar que retornamos todos los tipos activos, incluso si no tienen pagos
var tiposActivos = await GetTiposActivosAsync();
var listaFinal = new List<UltimoPagoViewModel>();
foreach (var tipo in tiposActivos)
{
var pago = resultado.FirstOrDefault(r => r.TipoId == tipo.Id);
if (pago != null)
{
listaFinal.Add(pago);
}
else
{
listaFinal.Add(new UltimoPagoViewModel
{
TipoId = tipo.Id,
NombreTipo = tipo.Nombre,
UltimoMes = 0, // No hay pagos
UltimoAnio = 0
});
}
}
return listaFinal;
}
private List<(int anio, int mes)> GenerarRangoMeses(
int anioInicial, int mesInicial,
int anioFinal, int mesFinal)
{
var meses = new List<(int, int)>();
var fecha = new DateTime(anioInicial, mesInicial, 1);
var fechaFin = new DateTime(anioFinal, mesFinal, 1);
while (fecha <= fechaFin)
{
meses.Add((fecha.Year, fecha.Month));
fecha = fecha.AddMonths(1);
}
return meses;
}
public async Task<List<Colaboracion>> GetColaboracionesRecientesAsync(int cantidad = 50)
{
return await _context.Colaboraciones
.Include(c => c.Miembro)
.ThenInclude(m => m.Persona)
.Include(c => c.Detalles)
.ThenInclude(d => d.TipoColaboracion)
.OrderByDescending(c => c.FechaRegistro)
.Take(cantidad)
.AsNoTracking()
.ToListAsync();
}
public async Task<Colaboracion?> GetColaboracionByIdAsync(long id)
{
return await _context.Colaboraciones
.Include(c => c.Miembro)
.ThenInclude(m => m.Persona)
.Include(c => c.Detalles)
.ThenInclude(d => d.TipoColaboracion)
.AsNoTracking()
.FirstOrDefaultAsync(c => c.Id == id);
}
public async Task<ReporteColaboracionesViewModel> GenerarReportePorFechasAsync(
DateTime fechaInicio,
DateTime fechaFin)
{
var colaboraciones = await _context.Colaboraciones
.Include(c => c.Miembro)
.ThenInclude(m => m.Persona)
.Include(c => c.Detalles)
.ThenInclude(d => d.TipoColaboracion)
.Where(c => c.FechaRegistro >= fechaInicio && c.FechaRegistro <= fechaFin)
.OrderByDescending(c => c.FechaRegistro)
.AsNoTracking()
.ToListAsync();
var reporte = new ReporteColaboracionesViewModel
{
FechaInicio = fechaInicio,
FechaFin = fechaFin,
TotalRecaudado = colaboraciones.Sum(c => c.MontoTotal)
};
// Desglose por tipo
var desglosePorTipo = colaboraciones
.SelectMany(c => c.Detalles)
.GroupBy(d => d.TipoColaboracion.Nombre)
.Select(g => new DesglosePorTipo
{
TipoNombre = g.Key,
CantidadMeses = g.Count(),
TotalRecaudado = g.Sum(d => d.Monto)
})
.OrderBy(d => d.TipoNombre)
.ToList();
reporte.DesglosePorTipos = desglosePorTipo;
// Detalle de movimientos
var movimientos = colaboraciones.Select(c => new DetalleMovimiento
{
ColaboracionId = c.Id,
Fecha = c.FechaRegistro,
NombreMiembro = $"{c.Miembro.Persona.Nombres} {c.Miembro.Persona.Apellidos}",
TiposColaboracion = string.Join(", ", c.Detalles.Select(d => d.TipoColaboracion.Nombre).Distinct()),
PeriodoCubierto = ObtenerPeriodoCubierto(c.Detalles.ToList()),
Monto = c.MontoTotal
}).ToList();
reporte.Movimientos = movimientos;
return reporte;
}
private string ObtenerPeriodoCubierto(List<DetalleColaboracion> detalles)
{
if (!detalles.Any()) return "";
var ordenados = detalles.OrderBy(d => d.Anio).ThenBy(d => d.Mes).ToList();
var primero = ordenados.First();
var ultimo = ordenados.Last();
var cultura = new CultureInfo("es-ES");
if (primero.Anio == ultimo.Anio && primero.Mes == ultimo.Mes)
{
return new DateTime(primero.Anio, primero.Mes, 1).ToString("MMMM yyyy", cultura);
}
return $"{new DateTime(primero.Anio, primero.Mes, 1).ToString("MMM yyyy", cultura)} - " +
$"{new DateTime(ultimo.Anio, ultimo.Mes, 1).ToString("MMM yyyy", cultura)}";
}
public async Task<EstadoCuentaViewModel> GenerarEstadoCuentaAsync(long miembroId)
{
var miembro = await _context.Miembros
.Include(m => m.Persona)
.AsNoTracking()
.FirstOrDefaultAsync(m => m.Id == miembroId);
if (miembro == null)
throw new Exception("Miembro no encontrado");
var colaboraciones = await _context.Colaboraciones
.Include(c => c.Detalles)
.ThenInclude(d => d.TipoColaboracion)
.Where(c => c.MiembroId == miembroId)
.AsNoTracking()
.ToListAsync();
var estado = new EstadoCuentaViewModel
{
MiembroId = miembroId,
NombreMiembro = $"{miembro.Persona.Nombres} {miembro.Persona.Apellidos}",
FechaConsulta = DateTime.Now,
TotalAportado = colaboraciones.Sum(c => c.MontoTotal)
};
// Agrupar por tipo
var historialPorTipo = colaboraciones
.SelectMany(c => c.Detalles.Select(d => new { Detalle = d, FechaRegistro = c.FechaRegistro }))
.GroupBy(x => x.Detalle.TipoColaboracion.Nombre)
.Select(g => new HistorialPorTipo
{
TipoNombre = g.Key,
TotalTipo = g.Sum(x => x.Detalle.Monto),
Registros = g.Select(x => new RegistroMensual
{
Mes = x.Detalle.Mes,
Anio = x.Detalle.Anio,
Monto = x.Detalle.Monto,
FechaRegistro = x.FechaRegistro
})
.OrderBy(r => r.Anio)
.ThenBy(r => r.Mes)
.ToList()
})
.OrderBy(h => h.TipoNombre)
.ToList();
estado.HistorialPorTipos = historialPorTipo;
return estado;
}
}

View File

@@ -0,0 +1,337 @@
using Microsoft.EntityFrameworkCore;
using Rs_system.Data;
using Rs_system.Models;
namespace Rs_system.Services;
public class ContabilidadGeneralService : IContabilidadGeneralService
{
private readonly ApplicationDbContext _context;
public ContabilidadGeneralService(ApplicationDbContext context)
{
_context = context;
}
// ==================== Categorías de Ingreso ====================
public async Task<List<CategoriaIngreso>> ObtenerCategoriasIngresoAsync()
{
return await _context.CategoriasIngreso
.Where(c => c.Activa)
.OrderBy(c => c.Nombre)
.AsNoTracking()
.ToListAsync();
}
public async Task<CategoriaIngreso?> ObtenerCategoriaIngresoPorIdAsync(long id)
{
return await _context.CategoriasIngreso.FindAsync(id);
}
public async Task<CategoriaIngreso> CrearCategoriaIngresoAsync(CategoriaIngreso categoria)
{
categoria.FechaCreacion = DateTime.UtcNow;
_context.CategoriasIngreso.Add(categoria);
await _context.SaveChangesAsync();
return categoria;
}
public async Task<bool> ActualizarCategoriaIngresoAsync(CategoriaIngreso categoria)
{
try
{
_context.CategoriasIngreso.Update(categoria);
await _context.SaveChangesAsync();
return true;
}
catch
{
return false;
}
}
public async Task<bool> EliminarCategoriaIngresoAsync(long id)
{
try
{
var categoria = await _context.CategoriasIngreso.FindAsync(id);
if (categoria == null) return false;
// Soft delete - marcar como inactiva en lugar de eliminar
categoria.Activa = false;
await _context.SaveChangesAsync();
return true;
}
catch
{
return false;
}
}
// ==================== Categorías de Egreso ====================
public async Task<List<CategoriaEgreso>> ObtenerCategoriasEgresoAsync()
{
return await _context.CategoriasEgreso
.Where(c => c.Activa)
.OrderBy(c => c.Nombre)
.AsNoTracking()
.ToListAsync();
}
public async Task<CategoriaEgreso?> ObtenerCategoriaEgresoPorIdAsync(long id)
{
return await _context.CategoriasEgreso.FindAsync(id);
}
public async Task<CategoriaEgreso> CrearCategoriaEgresoAsync(CategoriaEgreso categoria)
{
categoria.FechaCreacion = DateTime.UtcNow;
_context.CategoriasEgreso.Add(categoria);
await _context.SaveChangesAsync();
return categoria;
}
public async Task<bool> ActualizarCategoriaEgresoAsync(CategoriaEgreso categoria)
{
try
{
_context.CategoriasEgreso.Update(categoria);
await _context.SaveChangesAsync();
return true;
}
catch
{
return false;
}
}
public async Task<bool> EliminarCategoriaEgresoAsync(long id)
{
try
{
var categoria = await _context.CategoriasEgreso.FindAsync(id);
if (categoria == null) return false;
// Soft delete - marcar como inactiva en lugar de eliminar
categoria.Activa = false;
await _context.SaveChangesAsync();
return true;
}
catch
{
return false;
}
}
// ==================== Reportes Mensuales ====================
public async Task<ReporteMensualGeneral?> ObtenerReporteMensualAsync(int mes, int anio)
{
return await _context.ReportesMensualesGenerales
.Include(r => r.Movimientos)
.ThenInclude(m => m.CategoriaIngreso)
.Include(r => r.Movimientos)
.ThenInclude(m => m.CategoriaEgreso)
.FirstOrDefaultAsync(r => r.Mes == mes && r.Anio == anio);
}
public async Task<ReporteMensualGeneral> ObtenerOCrearReporteMensualAsync(int mes, int anio)
{
var reporteExistente = await ObtenerReporteMensualAsync(mes, anio);
if (reporteExistente != null)
return reporteExistente;
// Obtener el saldo final del mes anterior
var mesAnterior = mes == 1 ? 12 : mes - 1;
var anioAnterior = mes == 1 ? anio - 1 : anio;
var reporteAnterior = await ObtenerReporteMensualAsync(mesAnterior, anioAnterior);
var saldoInicial = reporteAnterior != null
? await CalcularSaldoActualAsync(reporteAnterior.Id)
: 0;
var nuevoReporte = new ReporteMensualGeneral
{
Mes = mes,
Anio = anio,
SaldoInicial = saldoInicial,
FechaCreacion = DateTime.UtcNow,
Cerrado = false
};
_context.ReportesMensualesGenerales.Add(nuevoReporte);
await _context.SaveChangesAsync();
return nuevoReporte;
}
public async Task<List<ReporteMensualGeneral>> ListarReportesAsync(int? anio = null)
{
var query = _context.ReportesMensualesGenerales.AsQueryable();
if (anio.HasValue)
query = query.Where(r => r.Anio == anio.Value);
return await query
.OrderByDescending(r => r.Anio)
.ThenByDescending(r => r.Mes)
.AsNoTracking()
.ToListAsync();
}
public async Task<bool> CerrarReporteAsync(long reporteId)
{
try
{
var reporte = await _context.ReportesMensualesGenerales.FindAsync(reporteId);
if (reporte == null) return false;
_context.Entry(reporte).Property(x => x.Cerrado).CurrentValue = true;
_context.Entry(reporte).Property(x => x.Cerrado).IsModified = true;
await _context.SaveChangesAsync();
return true;
}
catch(Exception ex)
{
return false;
}
}
// ==================== Movimientos ====================
public async Task<bool> GuardarMovimientosBulkAsync(long reporteId, List<MovimientoGeneral> movimientos)
{
try
{
var reporte = await _context.ReportesMensualesGenerales.FindAsync(reporteId);
if (reporte == null || reporte.Cerrado)
return false;
foreach (var movimiento in movimientos)
{
movimiento.ReporteMensualGeneralId = reporteId;
movimiento.Fecha = DateTime.SpecifyKind(movimiento.Fecha, DateTimeKind.Utc);
if (movimiento.Id > 0)
{
// Update existing
var existente = await _context.MovimientosGenerales.FindAsync(movimiento.Id);
if (existente != null)
{
existente.Tipo = movimiento.Tipo;
existente.CategoriaIngresoId = movimiento.CategoriaIngresoId;
existente.CategoriaEgresoId = movimiento.CategoriaEgresoId;
existente.Monto = movimiento.Monto;
existente.Fecha = movimiento.Fecha;
existente.Descripcion = movimiento.Descripcion;
existente.NumeroComprobante = movimiento.NumeroComprobante;
}
}
else
{
// Insert new
_context.MovimientosGenerales.Add(movimiento);
}
}
await _context.SaveChangesAsync();
return true;
}
catch(Exception ex)
{
return false;
}
}
public async Task<decimal> CalcularSaldoActualAsync(long reporteId)
{
var reporte = await _context.ReportesMensualesGenerales
.Include(r => r.Movimientos)
.FirstOrDefaultAsync(r => r.Id == reporteId);
if (reporte == null) return 0;
var totalIngresos = reporte.Movimientos
.Where(m => m.Tipo == (int) TipoMovimientoGeneral.Ingreso)
.Sum(m => m.Monto);
var totalEgresos = reporte.Movimientos
.Where(m => m.Tipo == (int)TipoMovimientoGeneral.Egreso)
.Sum(m => m.Monto);
return reporte.SaldoInicial + totalIngresos - totalEgresos;
}
// ==================== Consolidados ====================
public async Task<Dictionary<string, decimal>> ObtenerConsolidadoIngresosAsync(long reporteId)
{
var movimientos = await _context.MovimientosGenerales
.Include(m => m.CategoriaIngreso)
.Where(m => m.ReporteMensualGeneralId == reporteId
&& m.Tipo == (int)TipoMovimientoGeneral.Ingreso)
.AsNoTracking()
.ToListAsync();
return movimientos
.GroupBy(m => m.CategoriaIngreso?.Nombre ?? "Sin Categoría")
.ToDictionary(g => g.Key, g => g.Sum(m => m.Monto));
}
public async Task<Dictionary<string, decimal>> ObtenerConsolidadoEgresosAsync(long reporteId)
{
var movimientos = await _context.MovimientosGenerales
.Include(m => m.CategoriaEgreso)
.Where(m => m.ReporteMensualGeneralId == reporteId
&& m.Tipo == (int)TipoMovimientoGeneral.Egreso)
.AsNoTracking()
.ToListAsync();
return movimientos
.GroupBy(m => m.CategoriaEgreso?.Nombre ?? "Sin Categoría")
.ToDictionary(g => g.Key, g => g.Sum(m => m.Monto));
}
// ==================== Adjuntos ====================
public async Task<List<MovimientoGeneralAdjunto>> ObtenerAdjuntosMovimientoAsync(long movimientoId)
{
return await _context.MovimientosGeneralesAdjuntos
.Where(a => a.MovimientoGeneralId == movimientoId)
.OrderByDescending(a => a.FechaSubida)
.AsNoTracking()
.ToListAsync();
}
public async Task<MovimientoGeneralAdjunto?> CrearAdjuntoAsync(long movimientoId, string nombreArchivo, string rutaArchivo, string tipoContenido)
{
var movimiento = await _context.MovimientosGenerales.FindAsync(movimientoId);
if (movimiento == null) return null;
var adjunto = new MovimientoGeneralAdjunto
{
MovimientoGeneralId = movimientoId,
NombreArchivo = nombreArchivo,
RutaArchivo = rutaArchivo,
TipoContenido = tipoContenido,
FechaSubida = DateTime.UtcNow
};
_context.MovimientosGeneralesAdjuntos.Add(adjunto);
await _context.SaveChangesAsync();
return adjunto;
}
public async Task<bool> EliminarAdjuntoAsync(long adjuntoId)
{
var adjunto = await _context.MovimientosGeneralesAdjuntos.FindAsync(adjuntoId);
if (adjunto == null) return false;
_context.MovimientosGeneralesAdjuntos.Remove(adjunto);
await _context.SaveChangesAsync();
return true;
}
}

View File

@@ -0,0 +1,169 @@
using Microsoft.EntityFrameworkCore;
using Rs_system.Data;
using Rs_system.Models;
namespace Rs_system.Services;
public class ContabilidadService : IContabilidadService
{
private readonly ApplicationDbContext _context;
public ContabilidadService(ApplicationDbContext context)
{
_context = context;
}
public async Task<ContabilidadRegistro> CrearRegistroAsync(ContabilidadRegistro registro)
{
// Ensure Group exists
var groupExists = await _context.GruposTrabajo.AnyAsync(g => g.Id == registro.GrupoTrabajoId);
if (!groupExists)
{
throw new ArgumentException($"Grupo de trabajo con ID {registro.GrupoTrabajoId} no existe.");
}
_context.ContabilidadRegistros.Add(registro);
await _context.SaveChangesAsync();
return registro;
}
public async Task<IReadOnlyList<ContabilidadRegistro>> ObtenerRegistrosAsync(long grupoId, DateTime desde, DateTime hasta)
{
return await _context.ContabilidadRegistros
.Include(c => c.GrupoTrabajo)
.Where(c => c.GrupoTrabajoId == grupoId && c.Fecha.Date >= desde.Date && c.Fecha.Date <= hasta.Date)
.OrderByDescending(c => c.Fecha)
.ToListAsync();
}
public async Task<ReporteMensualContable?> ObtenerReporteMensualAsync(long grupoId, int mes, int anio)
{
return await _context.ReportesMensualesContables
.Include(r => r.Registros)
.FirstOrDefaultAsync(r => r.GrupoTrabajoId == grupoId && r.Mes == mes && r.Anio == anio);
}
public async Task<ReporteMensualContable> ObtenerOCrearReporteMensualAsync(long grupoId, int mes, int anio)
{
var reporte = await ObtenerReporteMensualAsync(grupoId, mes, anio);
if (reporte != null) return reporte;
// Calculate Saldo Inicial based on previous month
decimal saldoInicial = 0;
var prevMes = mes == 1 ? 12 : mes - 1;
var prevAnio = mes == 1 ? anio - 1 : anio;
var reportePrevio = await ObtenerReporteMensualAsync(grupoId, prevMes, prevAnio);
if (reportePrevio != null)
{
saldoInicial = await CalcularSaldoActualAsync(reportePrevio.Id);
}
reporte = new ReporteMensualContable
{
GrupoTrabajoId = grupoId,
Mes = mes,
Anio = anio,
SaldoInicial = saldoInicial,
FechaCreacion = DateTime.UtcNow,
Cerrado = false
};
_context.ReportesMensualesContables.Add(reporte);
await _context.SaveChangesAsync();
return reporte;
}
public async Task<List<ReporteMensualContable>> ListarReportesPorGrupoAsync(long grupoId)
{
return await _context.ReportesMensualesContables
.Where(r => r.GrupoTrabajoId == grupoId)
.OrderByDescending(r => r.Anio)
.ThenByDescending(r => r.Mes)
.ToListAsync();
}
public async Task<bool> GuardarRegistrosBulkAsync(long reporteId, List<ContabilidadRegistro> registros)
{
var reporte = await _context.ReportesMensualesContables
.Include(r => r.Registros)
.FirstOrDefaultAsync(r => r.Id == reporteId);
if (reporte == null || reporte.Cerrado) return false;
try
{
// Remove existing records for this report (or handle updates carefully)
// For a simple bulk entry system, we might replace all or upsert by ID.
// Let's go with UPSERT based on ID.
var existingIds = reporte.Registros.Select(r => r.Id).ToList();
var incomingIds = registros.Where(r => r.Id > 0).Select(r => r.Id).ToList();
// Delete records that are no longer in the list
var toDelete = reporte.Registros.Where(r => !incomingIds.Contains(r.Id)).ToList();
_context.ContabilidadRegistros.RemoveRange(toDelete);
foreach (var registro in registros)
{
if (registro.Id > 0)
{
// Update
var existing = reporte.Registros.FirstOrDefault(r => r.Id == registro.Id);
if (existing != null)
{
existing.Tipo = registro.Tipo;
existing.Monto = registro.Monto;
existing.Fecha = registro.Fecha;
existing.Descripcion = registro.Descripcion;
_context.Entry(existing).State = EntityState.Modified;
}
}
else
{
// Add
registro.ReporteMensualId = reporteId;
registro.GrupoTrabajoId = reporte.GrupoTrabajoId;
_context.ContabilidadRegistros.Add(registro);
}
}
await _context.SaveChangesAsync();
return true;
}
catch
{
return false;
}
}
public async Task<decimal> CalcularSaldoActualAsync(long reporteId)
{
var reporte = await _context.ReportesMensualesContables
.Include(r => r.Registros)
.FirstOrDefaultAsync(r => r.Id == reporteId);
if (reporte == null) return 0;
decimal ingresos = reporte.Registros
.Where(r => r.Tipo == TipoMovimientoContable.Ingreso)
.Sum(r => r.Monto);
decimal egresos = reporte.Registros
.Where(r => r.Tipo == TipoMovimientoContable.Egreso)
.Sum(r => r.Monto);
return reporte.SaldoInicial + ingresos - egresos;
}
public async Task<bool> CerrarReporteAsync(long reporteId)
{
var reporte = await _context.ReportesMensualesContables.FindAsync(reporteId);
if (reporte == null || reporte.Cerrado) return false;
reporte.Cerrado = true;
_context.ReportesMensualesContables.Update(reporte);
await _context.SaveChangesAsync();
return true;
}
}

View File

@@ -0,0 +1,102 @@
using Microsoft.EntityFrameworkCore;
using Rs_system.Data;
using Rs_system.Models;
namespace Rs_system.Services;
public class EstadoArticuloService : IEstadoArticuloService
{
private readonly ApplicationDbContext _context;
public EstadoArticuloService(ApplicationDbContext context)
{
_context = context;
}
public async Task<IEnumerable<EstadoArticulo>> GetAllAsync()
{
return await _context.EstadosArticulos
.Where(e => !e.Eliminado)
.OrderBy(e => e.Nombre)
.ToListAsync();
}
public async Task<EstadoArticulo?> GetByIdAsync(int id)
{
return await _context.EstadosArticulos
.FirstOrDefaultAsync(e => e.Id == id && !e.Eliminado);
}
public async Task<bool> CreateAsync(EstadoArticulo estado)
{
try
{
estado.CreadoEn = DateTime.UtcNow;
estado.ActualizadoEn = DateTime.UtcNow;
estado.Eliminado = false;
_context.EstadosArticulos.Add(estado);
await _context.SaveChangesAsync();
return true;
}
catch
{
return false;
}
}
public async Task<bool> UpdateAsync(EstadoArticulo estado)
{
try
{
var existing = await _context.EstadosArticulos.FindAsync(estado.Id);
if (existing == null || existing.Eliminado) return false;
existing.Nombre = estado.Nombre;
existing.Descripcion = estado.Descripcion;
existing.Color = estado.Color;
existing.Activo = estado.Activo;
existing.ActualizadoEn = DateTime.UtcNow;
_context.EstadosArticulos.Update(existing);
await _context.SaveChangesAsync();
return true;
}
catch
{
return false;
}
}
public async Task<bool> DeleteAsync(int id)
{
try
{
var estado = await _context.EstadosArticulos.FindAsync(id);
if (estado == null || estado.Eliminado) return false;
estado.Eliminado = true;
estado.ActualizadoEn = DateTime.UtcNow;
_context.EstadosArticulos.Update(estado);
await _context.SaveChangesAsync();
return true;
}
catch
{
return false;
}
}
public async Task<bool> ExistsAsync(string nombre, int? excludeId = null)
{
var query = _context.EstadosArticulos.AsQueryable();
if (excludeId.HasValue)
{
query = query.Where(e => e.Id != excludeId.Value);
}
return await query.AnyAsync(e => e.Nombre.ToLower() == nombre.ToLower() && !e.Eliminado);
}
}

View File

@@ -0,0 +1,18 @@
using Rs_system.Models.ViewModels;
namespace Rs_system.Services;
public interface IArticuloService
{
Task<IEnumerable<ArticuloViewModel>> GetAllAsync(string? search = null, int? categoriaId = null, int? ubicacionId = null, int? estadoId = null);
Task<ArticuloViewModel?> GetByIdAsync(int id);
Task<bool> CreateAsync(ArticuloViewModel viewModel, string createdBy);
Task<bool> UpdateAsync(ArticuloViewModel viewModel);
Task<bool> DeleteAsync(int id);
Task<bool> ExistsCodigoAsync(string codigo, int? excludeId = null);
// Dropdown helpers
Task<IEnumerable<(int Id, string Nombre)>> GetCategoriasAsync();
Task<IEnumerable<(int Id, string Nombre, string Color)>> GetEstadosAsync();
Task<IEnumerable<(int Id, string Nombre)>> GetUbicacionesAsync();
}

View File

@@ -0,0 +1,13 @@
using Rs_system.Models;
namespace Rs_system.Services;
public interface ICategoriaService
{
Task<IEnumerable<Categoria>> GetAllAsync();
Task<Categoria?> GetByIdAsync(int id);
Task<bool> CreateAsync(Categoria categoria);
Task<bool> UpdateAsync(Categoria categoria);
Task<bool> DeleteAsync(int id);
Task<bool> ExistsAsync(string nombre, int? excludeId = null);
}

View File

@@ -0,0 +1,21 @@
using Rs_system.Models;
using Rs_system.Models.ViewModels;
namespace Rs_system.Services;
public interface IColaboracionService
{
// Tipos de colaboración
Task<List<TipoColaboracion>> GetTiposActivosAsync();
Task<TipoColaboracion?> GetTipoByIdAsync(long id);
// Colaboraciones
Task<Colaboracion> RegistrarColaboracionAsync(RegistrarColaboracionViewModel model, string registradoPor);
Task<List<Colaboracion>> GetColaboracionesRecientesAsync(int cantidad = 50);
Task<Colaboracion?> GetColaboracionByIdAsync(long id);
// Reportes
Task<ReporteColaboracionesViewModel> GenerarReportePorFechasAsync(DateTime fechaInicio, DateTime fechaFin);
Task<EstadoCuentaViewModel> GenerarEstadoCuentaAsync(long miembroId);
Task<List<UltimoPagoViewModel>> GetUltimosPagosPorMiembroAsync(long miembroId);
}

View File

@@ -0,0 +1,39 @@
using Rs_system.Models;
namespace Rs_system.Services;
public interface IContabilidadGeneralService
{
// Categorías de Ingreso
Task<List<CategoriaIngreso>> ObtenerCategoriasIngresoAsync();
Task<CategoriaIngreso?> ObtenerCategoriaIngresoPorIdAsync(long id);
Task<CategoriaIngreso> CrearCategoriaIngresoAsync(CategoriaIngreso categoria);
Task<bool> ActualizarCategoriaIngresoAsync(CategoriaIngreso categoria);
Task<bool> EliminarCategoriaIngresoAsync(long id);
// Categorías de Egreso
Task<List<CategoriaEgreso>> ObtenerCategoriasEgresoAsync();
Task<CategoriaEgreso?> ObtenerCategoriaEgresoPorIdAsync(long id);
Task<CategoriaEgreso> CrearCategoriaEgresoAsync(CategoriaEgreso categoria);
Task<bool> ActualizarCategoriaEgresoAsync(CategoriaEgreso categoria);
Task<bool> EliminarCategoriaEgresoAsync(long id);
// Reportes Mensuales
Task<ReporteMensualGeneral?> ObtenerReporteMensualAsync(int mes, int anio);
Task<ReporteMensualGeneral> ObtenerOCrearReporteMensualAsync(int mes, int anio);
Task<List<ReporteMensualGeneral>> ListarReportesAsync(int? anio = null);
Task<bool> CerrarReporteAsync(long reporteId);
// Movimientos
Task<bool> GuardarMovimientosBulkAsync(long reporteId, List<MovimientoGeneral> movimientos);
Task<decimal> CalcularSaldoActualAsync(long reporteId);
// Consolidados
Task<Dictionary<string, decimal>> ObtenerConsolidadoIngresosAsync(long reporteId);
Task<Dictionary<string, decimal>> ObtenerConsolidadoEgresosAsync(long reporteId);
// Adjuntos
Task<List<MovimientoGeneralAdjunto>> ObtenerAdjuntosMovimientoAsync(long movimientoId);
Task<MovimientoGeneralAdjunto?> CrearAdjuntoAsync(long movimientoId, string nombreArchivo, string rutaArchivo, string tipoContenido);
Task<bool> EliminarAdjuntoAsync(long adjuntoId);
}

View File

@@ -0,0 +1,18 @@
using Rs_system.Models;
namespace Rs_system.Services;
public interface IContabilidadService
{
Task<ContabilidadRegistro> CrearRegistroAsync(ContabilidadRegistro registro);
Task<IReadOnlyList<ContabilidadRegistro>> ObtenerRegistrosAsync(long grupoId, DateTime desde, DateTime hasta);
// Monthly Report Methods
Task<ReporteMensualContable?> ObtenerReporteMensualAsync(long grupoId, int mes, int anio);
Task<ReporteMensualContable> ObtenerOCrearReporteMensualAsync(long grupoId, int mes, int anio);
Task<List<ReporteMensualContable>> ListarReportesPorGrupoAsync(long grupoId);
Task<bool> GuardarRegistrosBulkAsync(long reporteId, List<ContabilidadRegistro> registros);
Task<decimal> CalcularSaldoActualAsync(long reporteId);
Task<bool> CerrarReporteAsync(long reporteId);
}

View File

@@ -0,0 +1,13 @@
using Rs_system.Models;
namespace Rs_system.Services;
public interface IEstadoArticuloService
{
Task<IEnumerable<EstadoArticulo>> GetAllAsync();
Task<EstadoArticulo?> GetByIdAsync(int id);
Task<bool> CreateAsync(EstadoArticulo estado);
Task<bool> UpdateAsync(EstadoArticulo estado);
Task<bool> DeleteAsync(int id);
Task<bool> ExistsAsync(string nombre, int? excludeId = null);
}

View File

@@ -0,0 +1,21 @@
using Rs_system.Models;
namespace Rs_system.Services;
public interface IMovimientoService
{
Task<IEnumerable<MovimientoInventario>> GetHistorialGeneralAsync(int limit = 100);
Task<IEnumerable<MovimientoInventario>> GetHistorialPorArticuloAsync(int articuloId);
// Legacy wrappers (Quantity = 1)
Task<bool> RegistrarTrasladoAsync(int articuloId, int nuevaUbicacionId, string observacion, string usuario);
Task<bool> RegistrarBajaAsync(int articuloId, string motivo, string usuario);
// New Quantity-Aware Methods
Task<bool> RegistrarTrasladoCantidadAsync(int articuloId, int nuevaUbicacionId, int cantidad, string observacion, string usuario);
Task<bool> RegistrarBajaCantidadAsync(int articuloId, int cantidad, string motivo, string usuario);
Task<bool> RegistrarCambioEstadoAsync(int articuloId, int nuevoEstadoId, string observacion, string usuario);
Task<bool> RegistrarPrestamoAsync(int articuloId, int cantidad, string personaNombre, string? personaIdentificacion, DateTime? fechaDevolucionEstimada, string observacion, string usuario);
Task<bool> RegistrarEntradaCantidadAsync(int articuloId, int cantidad, string observacion, string usuario);
}

View File

@@ -0,0 +1,13 @@
using Rs_system.Models;
namespace Rs_system.Services;
public interface IPrestamoService
{
Task<IEnumerable<Prestamo>> GetHistorialPrestamosAsync(int limit = 100);
Task<IEnumerable<Prestamo>> GetPrestamosActivosAsync();
Task<Prestamo?> GetPrestamoByIdAsync(long id);
Task<bool> RegistrarPrestamoAsync(int articuloId, int cantidad, string personaNombre, string? personaIdentificacion, DateTime? fechaDevolucionEstimada, string observacion, string usuario);
Task<bool> RegistrarDevolucionAsync(long prestamoId, string observacion, string usuario);
Task<bool> RegistrarDevolucionParcialAsync(long prestamoId, List<string> codigosDevolucion, string observacion, string usuario);
}

View File

@@ -0,0 +1,13 @@
using Rs_system.Models;
namespace Rs_system.Services;
public interface IUbicacionService
{
Task<IEnumerable<Ubicacion>> GetAllAsync();
Task<Ubicacion?> GetByIdAsync(int id);
Task<bool> CreateAsync(Ubicacion ubicacion);
Task<bool> UpdateAsync(Ubicacion ubicacion);
Task<bool> DeleteAsync(int id);
Task<bool> ExistsAsync(string nombre, int? excludeId = null);
}

View File

@@ -0,0 +1,450 @@
using Microsoft.EntityFrameworkCore;
using Rs_system.Data;
using Rs_system.Models;
namespace Rs_system.Services;
public class MovimientoService : IMovimientoService
{
private readonly ApplicationDbContext _context;
public MovimientoService(ApplicationDbContext context)
{
_context = context;
}
public async Task<IEnumerable<MovimientoInventario>> GetHistorialGeneralAsync(int limit = 100)
{
return await _context.MovimientosInventario
.Include(m => m.Articulo)
.Include(m => m.UbicacionOrigen)
.Include(m => m.UbicacionDestino)
.Include(m => m.EstadoAnterior)
.Include(m => m.EstadoNuevo)
.OrderByDescending(m => m.Fecha)
.Take(limit)
.ToListAsync();
}
public async Task<IEnumerable<MovimientoInventario>> GetHistorialPorArticuloAsync(int articuloId)
{
return await _context.MovimientosInventario
.Include(m => m.UbicacionOrigen)
.Include(m => m.UbicacionDestino)
.Include(m => m.EstadoAnterior)
.Include(m => m.EstadoNuevo)
.Where(m => m.ArticuloId == articuloId)
.OrderByDescending(m => m.Fecha)
.ToListAsync();
}
public async Task<bool> RegistrarTrasladoAsync(int articuloId, int nuevaUbicacionId, string observacion, string usuario)
{
return await RegistrarTrasladoCantidadAsync(articuloId, nuevaUbicacionId, 1, observacion, usuario);
}
public async Task<bool> RegistrarTrasladoCantidadAsync(
int articuloId,
int nuevaUbicacionId,
int cantidad,
string observacion,
string usuario)
{
var strategy = _context.Database.CreateExecutionStrategy();
return await strategy.ExecuteAsync(async () =>
{
using var transaction = await _context.Database.BeginTransactionAsync();
try
{
var articulo = await _context.Articulos.FindAsync(articuloId);
if (articulo == null) return false;
var fecha = DateTime.UtcNow;
if (articulo.TipoControl == nameof(Articulo.TipoControlInventario.LOTE))
{
// ===== LOTE =====
var origenExistencia = await _context.Existencias
.FirstOrDefaultAsync(e =>
e.ArticuloId == articuloId &&
e.UbicacionId == articulo.UbicacionId);
if (origenExistencia == null || origenExistencia.Cantidad < cantidad)
return false;
origenExistencia.Cantidad -= cantidad;
origenExistencia.ActualizadoEn = fecha;
_context.Existencias.Update(origenExistencia);
var destinoExistencia = await _context.Existencias
.FirstOrDefaultAsync(e =>
e.ArticuloId == articuloId &&
e.UbicacionId == nuevaUbicacionId);
if (destinoExistencia == null)
{
destinoExistencia = new Existencia
{
ArticuloId = articuloId,
UbicacionId = nuevaUbicacionId,
Cantidad = 0,
ActualizadoEn = fecha
};
_context.Existencias.Add(destinoExistencia);
}
destinoExistencia.Cantidad += cantidad;
destinoExistencia.ActualizadoEn = fecha;
// 📉 MOVIMIENTO SALIDA
var movSalida = new MovimientoInventario
{
ArticuloId = articuloId,
TipoMovimiento = nameof(TipoMovimiento.TRASLADO),
Fecha = fecha,
UbicacionOrigenId = articulo.UbicacionId,
Cantidad = cantidad,
TipMov = 2,
Observacion = observacion,
UsuarioId = usuario,
EstadoAnteriorId = articulo.EstadoId,
EstadoNuevoId = articulo.EstadoId
};
// 📈 MOVIMIENTO ENTRADA
var movEntrada = new MovimientoInventario
{
ArticuloId = articuloId,
TipoMovimiento = nameof(TipoMovimiento.TRASLADO),
Fecha = fecha,
UbicacionDestinoId = nuevaUbicacionId,
Cantidad = cantidad,
TipMov = 1,
Observacion = observacion,
UsuarioId = usuario,
EstadoAnteriorId = articulo.EstadoId,
EstadoNuevoId = articulo.EstadoId
};
_context.MovimientosInventario.AddRange(movSalida, movEntrada);
}
else
{
// ===== UNITARIO =====
if (articulo.UbicacionId == nuevaUbicacionId)
return false;
// 📉 SALIDA
var movSalida = new MovimientoInventario
{
ArticuloId = articuloId,
TipoMovimiento = nameof(TipoMovimiento.TRASLADO),
Fecha = fecha,
UbicacionOrigenId = articulo.UbicacionId,
Cantidad = 1,
TipMov = 2,
Observacion = observacion,
UsuarioId = usuario,
EstadoAnteriorId = articulo.EstadoId,
EstadoNuevoId = articulo.EstadoId
};
// 📈 ENTRADA
var movEntrada = new MovimientoInventario
{
ArticuloId = articuloId,
TipoMovimiento = nameof(TipoMovimiento.TRASLADO),
Fecha = fecha,
UbicacionDestinoId = nuevaUbicacionId,
Cantidad = 1,
TipMov = 1,
Observacion = observacion,
UsuarioId = usuario,
EstadoAnteriorId = articulo.EstadoId,
EstadoNuevoId = articulo.EstadoId
};
articulo.UbicacionId = nuevaUbicacionId;
articulo.ActualizadoEn = fecha;
_context.Articulos.Update(articulo);
_context.MovimientosInventario.AddRange(movSalida, movEntrada);
}
await _context.SaveChangesAsync();
await transaction.CommitAsync();
return true;
}
catch
{
await transaction.RollbackAsync();
return false;
}
});
}
public async Task<bool> RegistrarCambioEstadoAsync(int articuloId, int nuevoEstadoId, string observacion, string usuario)
{
var strategy = _context.Database.CreateExecutionStrategy();
return await strategy.ExecuteAsync(async () =>
{
using var transaction = await _context.Database.BeginTransactionAsync();
try
{
var articulo = await _context.Articulos.FindAsync(articuloId);
if (articulo == null) return false;
if (articulo.EstadoId == nuevoEstadoId) return false;
var movimiento = new MovimientoInventario
{
ArticuloId = articuloId,
TipoMovimiento = nameof(TipoMovimiento.CAMBIO_ESTADO),
Fecha = DateTime.UtcNow,
UbicacionOrigenId = articulo.UbicacionId,
UbicacionDestinoId = articulo.UbicacionId,
EstadoAnteriorId = articulo.EstadoId,
EstadoNuevoId = nuevoEstadoId,
Cantidad = (articulo.TipoControl == nameof(Articulo.TipoControlInventario.LOTE)) ? articulo.CantidadGlobal : 1,
Observacion = observacion,
UsuarioId = usuario
};
articulo.EstadoId = nuevoEstadoId;
articulo.ActualizadoEn = DateTime.UtcNow;
_context.MovimientosInventario.Add(movimiento);
_context.Articulos.Update(articulo);
await _context.SaveChangesAsync();
await transaction.CommitAsync();
return true;
}
catch
{
await transaction.RollbackAsync();
return false;
}
});
}
public async Task<bool> RegistrarPrestamoAsync(int articuloId, int cantidad, string personaNombre, string? personaIdentificacion,
DateTime? fechaDevolucionEstimada, string observacion, string usuario)
{
var strategy = _context.Database.CreateExecutionStrategy();
return await strategy.ExecuteAsync(async () =>
{
using var transaction = await _context.Database.BeginTransactionAsync();
try
{
var articulo = await _context.Articulos.FindAsync(articuloId);
if (articulo == null) return false;
// Validar stock disponible
if (articulo.TipoControl == nameof(Articulo.TipoControlInventario.LOTE))
{
var existencia = await _context.Existencias
.FirstOrDefaultAsync(e => e.ArticuloId == articuloId && e.UbicacionId == articulo.UbicacionId);
if (existencia == null || existencia.Cantidad < cantidad) return false;
// Reducir stock existente
existencia.Cantidad -= cantidad;
articulo.CantidadGlobal -= cantidad;
if (articulo.CantidadGlobal < 0) articulo.CantidadGlobal = 0;
if (existencia.Cantidad < 0) existencia.Cantidad = 0;
_context.Existencias.Update(existencia);
_context.Articulos.Update(articulo);
}
else
{
// UNITARIO - Solo se puede prestar 1
if (cantidad != 1) return false;
if (!articulo.Activo) return false;
}
// Crear movimiento de inventario
var movimiento = new MovimientoInventario
{
ArticuloId = articuloId,
TipoMovimiento = nameof(TipoMovimiento.PRESTAMO),
TipMov = 2,
Fecha = DateTime.UtcNow,
UbicacionOrigenId = articulo.UbicacionId,
UbicacionDestinoId = articulo.UbicacionId, // Mismo lugar, solo está prestado
EstadoAnteriorId = articulo.EstadoId,
EstadoNuevoId = articulo.EstadoId,
Cantidad = cantidad,
Observacion = $"Préstamo a {personaNombre}. {observacion}",
UsuarioId = usuario
};
_context.MovimientosInventario.Add(movimiento);
await _context.SaveChangesAsync();
await transaction.CommitAsync();
return true;
}
catch
{
await transaction.RollbackAsync();
return false;
}
});
}
public async Task<bool> RegistrarBajaAsync(int articuloId, string motivo, string usuario)
{
return await RegistrarBajaCantidadAsync(articuloId, 1, motivo, usuario);
}
public async Task<bool> RegistrarBajaCantidadAsync(int articuloId, int cantidad, string motivo, string usuario)
{
var strategy = _context.Database.CreateExecutionStrategy();
return await strategy.ExecuteAsync(async () =>
{
using var transaction = await _context.Database.BeginTransactionAsync();
try
{
var articulo = await _context.Articulos.FindAsync(articuloId);
if (articulo == null) return false;
if (articulo.TipoControl == nameof(Articulo.TipoControlInventario.LOTE))
{
var existencia = await _context.Existencias
.FirstOrDefaultAsync(e => e.ArticuloId == articuloId && e.UbicacionId == articulo.UbicacionId);
if (existencia == null || existencia.Cantidad < cantidad) return false;
existencia.Cantidad -= cantidad;
articulo.CantidadGlobal -= cantidad;
if (articulo.CantidadGlobal <= 0) articulo.Activo = false;
var movimiento = new MovimientoInventario
{
ArticuloId = articuloId,
TipoMovimiento = nameof(TipoMovimiento.BAJA),
TipMov = 2,
Fecha = DateTime.UtcNow,
UbicacionOrigenId = articulo.UbicacionId,
EstadoAnteriorId = articulo.EstadoId,
Cantidad = cantidad,
Observacion = motivo,
UsuarioId = usuario
};
_context.Existencias.Update(existencia);
_context.MovimientosInventario.Add(movimiento);
_context.Articulos.Update(articulo);
}
else
{
var movimiento = new MovimientoInventario
{
ArticuloId = articuloId,
TipoMovimiento = nameof(TipoMovimiento.BAJA),
TipMov = 2,
Fecha = DateTime.UtcNow,
UbicacionOrigenId = articulo.UbicacionId,
EstadoAnteriorId = articulo.EstadoId,
Cantidad = 1,
Observacion = motivo,
UsuarioId = usuario
};
articulo.Activo = false;
_context.MovimientosInventario.Add(movimiento);
_context.Articulos.Update(articulo);
}
await _context.SaveChangesAsync();
await transaction.CommitAsync();
return true;
}
catch
{
await transaction.RollbackAsync();
return false;
}
});
}
public async Task<bool> RegistrarEntradaCantidadAsync(int articuloId, int cantidad, string observacion, string usuario)
{
var strategy = _context.Database.CreateExecutionStrategy();
return await strategy.ExecuteAsync(async () =>
{
try
{
var articulo = await _context.Articulos.FindAsync(articuloId);
if (articulo == null) return false;
if (articulo.TipoControl == nameof(Articulo.TipoControlInventario.LOTE))
{
var existencia = await _context.Existencias
.FirstOrDefaultAsync(e => e.ArticuloId == articuloId && e.UbicacionId == articulo.UbicacionId);
if (existencia == null)
{
existencia = new Existencia
{
ArticuloId = articuloId,
UbicacionId = articulo.UbicacionId,
Cantidad = 0,
ActualizadoEn = DateTime.UtcNow
};
_context.Existencias.Add(existencia);
}
existencia.Cantidad += cantidad;
articulo.CantidadGlobal += cantidad;
articulo.ActualizadoEn = DateTime.UtcNow;
_context.Existencias.Update(existencia);
_context.Articulos.Update(articulo);
}
else
{
// UNITARIO - Si está inactivo por baja, lo reactivamos?
// En unitario, una entrada suele ser que el objeto "vuelve" a estar disponible.
articulo.Activo = true;
articulo.ActualizadoEn = DateTime.UtcNow;
_context.Articulos.Update(articulo);
}
var movimiento = new MovimientoInventario
{
ArticuloId = articuloId,
TipoMovimiento = nameof(TipoMovimiento.ENTRADA),
TipMov = 1,
Fecha = DateTime.UtcNow,
UbicacionDestinoId = articulo.UbicacionId,
EstadoAnteriorId = articulo.EstadoId,
EstadoNuevoId = articulo.EstadoId,
Cantidad = cantidad,
Observacion = observacion,
UsuarioId = usuario
};
_context.MovimientosInventario.Add(movimiento);
await _context.SaveChangesAsync();
return true;
}
catch
{
return false;
}
});
}
}

View File

@@ -0,0 +1,226 @@
using Microsoft.EntityFrameworkCore;
using Rs_system.Data;
using Rs_system.Models;
namespace Rs_system.Services;
public class PrestamoService : IPrestamoService
{
private readonly ApplicationDbContext _context;
public PrestamoService(ApplicationDbContext context)
{
_context = context;
}
public async Task<IEnumerable<Prestamo>> GetHistorialPrestamosAsync(int limit = 100)
{
return await _context.Prestamos
.Include(p => p.Articulo)
.OrderByDescending(p => p.FechaPrestamo)
.Take(limit)
.ToListAsync();
}
public async Task<IEnumerable<Prestamo>> GetPrestamosActivosAsync()
{
return await _context.Prestamos
.Include(p => p.Articulo)
.Where(p => p.Estado == "ACTIVO" || p.Estado == "ATRASADO")
.OrderByDescending(p => p.FechaPrestamo)
.ToListAsync();
}
public async Task<Prestamo?> GetPrestamoByIdAsync(long id)
{
return await _context.Prestamos
.Include(p => p.Articulo)
.Include(p => p.Detalles)
.FirstOrDefaultAsync(p => p.Id == id);
}
public async Task<bool> RegistrarPrestamoAsync(int articuloId, int cantidad, string personaNombre, string? personaIdentificacion, DateTime? fechaDevolucionEstimada, string observacion, string usuario)
{
var strategy = _context.Database.CreateExecutionStrategy();
return await strategy.ExecuteAsync(async () =>
{
using var transaction = await _context.Database.BeginTransactionAsync();
try
{
var articulo = await _context.Articulos.FindAsync(articuloId);
if (articulo == null) return false;
// 1. Validar y actualizar stock
if (articulo.TipoControl == nameof(Articulo.TipoControlInventario.LOTE))
{
var existencia = await _context.Existencias
.FirstOrDefaultAsync(e => e.ArticuloId == articuloId && e.UbicacionId == articulo.UbicacionId);
if (existencia == null || existencia.Cantidad < cantidad) return false;
existencia.Cantidad -= cantidad;
articulo.CantidadGlobal -= cantidad;
_context.Existencias.Update(existencia);
_context.Articulos.Update(articulo);
}
else
{
// Unitario
if (cantidad != 1 || !articulo.Activo) return false;
// En unitario, podrías marcar como inactivo o simplemente registrar el préstamo
// Para este sistema, asumiremos que prestado sigue siendo "Activo" pero en una ubicación de préstamo (vía movimiento)
}
// 2. Crear el registro de préstamo
var prestamo = new Prestamo
{
ArticuloId = articuloId,
Cantidad = cantidad,
PersonaNombre = personaNombre,
PersonaIdentificacion = personaIdentificacion,
FechaPrestamo = DateTime.UtcNow,
FechaDevolucionEstimada = fechaDevolucionEstimada,
Estado = "ACTIVO",
Observacion = observacion,
UsuarioId = usuario
};
_context.Prestamos.Add(prestamo);
await _context.SaveChangesAsync(); // Guardamos para tener el ID del préstamo
// 3. Crear movimiento de inventario (auditoría)
var movimiento = new MovimientoInventario
{
ArticuloId = articuloId,
TipoMovimiento = nameof(TipoMovimiento.PRESTAMO),
TipMov = 2,
Fecha = DateTime.UtcNow,
UbicacionOrigenId = articulo.UbicacionId,
UbicacionDestinoId = articulo.UbicacionId,
EstadoAnteriorId = articulo.EstadoId,
EstadoNuevoId = articulo.EstadoId,
Cantidad = cantidad,
Observacion = $"Préstamo #{prestamo.Id} a {personaNombre}. {observacion}",
UsuarioId = usuario
};
_context.MovimientosInventario.Add(movimiento);
await _context.SaveChangesAsync();
await transaction.CommitAsync();
return true;
}
catch
{
await transaction.RollbackAsync();
return false;
}
});
}
public async Task<bool> RegistrarDevolucionAsync(
long prestamoId,
string observacion,
string usuario)
{
var strategy = _context.Database.CreateExecutionStrategy();
return await strategy.ExecuteAsync(async () =>
{
try
{
var prestamo = await _context.Prestamos.FindAsync(prestamoId);
if (prestamo == null)
return false;
if (prestamo.Estado == "DEVUELTO")
return false;
var articulo = await _context.Articulos.FindAsync(prestamo.ArticuloId);
if (articulo == null)
return false;
var fechaActual = DateTime.UtcNow;
if (articulo.TipoControl == nameof(Articulo.TipoControlInventario.LOTE))
{
// --- Buscar existencia ---
var existencia = await _context.Existencias
.FirstOrDefaultAsync(e =>
e.ArticuloId == articulo.Id &&
e.UbicacionId == articulo.UbicacionId);
// --- Crear existencia si no existe ---
if (existencia == null)
{
existencia = new Existencia
{
ArticuloId = articulo.Id,
UbicacionId = articulo.UbicacionId,
Cantidad = 0,
ActualizadoEn = fechaActual
};
_context.Existencias.Add(existencia);
}
// --- Actualizar cantidades ---
existencia.Cantidad += prestamo.Cantidad;
existencia.ActualizadoEn = fechaActual;
articulo.CantidadGlobal += prestamo.Cantidad;
articulo.ActualizadoEn = fechaActual;
_context.Existencias.Update(existencia);
_context.Articulos.Update(articulo);
}
else
{
articulo.Activo = true;
articulo.ActualizadoEn = fechaActual;
_context.Articulos.Update(articulo);
}
prestamo.Estado = "DEVUELTO";
prestamo.FechaDevolucionReal = fechaActual;
prestamo.Observacion =
$"{prestamo.Observacion}\nDevolución: {observacion}";
_context.Prestamos.Update(prestamo);
var movimiento = new MovimientoInventario
{
ArticuloId = articulo.Id,
TipoMovimiento = nameof(TipoMovimiento.DEVOLUCION),
TipMov = 1, // ENTRADA
Fecha = fechaActual,
UbicacionOrigenId = articulo.UbicacionId,
UbicacionDestinoId = articulo.UbicacionId,
EstadoAnteriorId = articulo.EstadoId,
EstadoNuevoId = articulo.EstadoId,
Cantidad = prestamo.Cantidad,
Observacion = $"Devolución de préstamo #{prestamo.Id}. {observacion}",
UsuarioId = usuario
};
_context.MovimientosInventario.Add(movimiento);
await _context.SaveChangesAsync();
return true;
}
catch
{
return false;
}
});
}
public async Task<bool> RegistrarDevolucionParcialAsync(long prestamoId, List<string> codigosDevolucion, string observacion, string usuario)
{
// Implementación básica para seguir la interfaz, aunque el controlador actual no la usa directamente
// Esta lógica sería más para artículos unitarios con códigos específicos (PrestamoDetalle)
return await RegistrarDevolucionAsync(prestamoId, "Devolución parcial - " + observacion, usuario);
}
}

View File

@@ -0,0 +1,102 @@
using Microsoft.EntityFrameworkCore;
using Rs_system.Data;
using Rs_system.Models;
namespace Rs_system.Services;
public class UbicacionService : IUbicacionService
{
private readonly ApplicationDbContext _context;
public UbicacionService(ApplicationDbContext context)
{
_context = context;
}
public async Task<IEnumerable<Ubicacion>> GetAllAsync()
{
return await _context.Ubicaciones
.Where(u => !u.Eliminado)
.OrderBy(u => u.Nombre)
.ToListAsync();
}
public async Task<Ubicacion?> GetByIdAsync(int id)
{
return await _context.Ubicaciones
.FirstOrDefaultAsync(u => u.Id == id && !u.Eliminado);
}
public async Task<bool> CreateAsync(Ubicacion ubicacion)
{
try
{
ubicacion.CreadoEn = DateTime.UtcNow;
ubicacion.ActualizadoEn = DateTime.UtcNow;
ubicacion.Eliminado = false;
_context.Ubicaciones.Add(ubicacion);
await _context.SaveChangesAsync();
return true;
}
catch
{
return false;
}
}
public async Task<bool> UpdateAsync(Ubicacion ubicacion)
{
try
{
var existing = await _context.Ubicaciones.FindAsync(ubicacion.Id);
if (existing == null || existing.Eliminado) return false;
existing.Nombre = ubicacion.Nombre;
existing.Descripcion = ubicacion.Descripcion;
existing.Responsable = ubicacion.Responsable;
existing.Activo = ubicacion.Activo;
existing.ActualizadoEn = DateTime.UtcNow;
_context.Ubicaciones.Update(existing);
await _context.SaveChangesAsync();
return true;
}
catch
{
return false;
}
}
public async Task<bool> DeleteAsync(int id)
{
try
{
var ubicacion = await _context.Ubicaciones.FindAsync(id);
if (ubicacion == null || ubicacion.Eliminado) return false;
ubicacion.Eliminado = true;
ubicacion.ActualizadoEn = DateTime.UtcNow;
_context.Ubicaciones.Update(ubicacion);
await _context.SaveChangesAsync();
return true;
}
catch
{
return false;
}
}
public async Task<bool> ExistsAsync(string nombre, int? excludeId = null)
{
var query = _context.Ubicaciones.AsQueryable();
if (excludeId.HasValue)
{
query = query.Where(u => u.Id != excludeId.Value);
}
return await query.AnyAsync(u => u.Nombre.ToLower() == nombre.ToLower() && !u.Eliminado);
}
}