295 lines
12 KiB
C#
295 lines
12 KiB
C#
using CPRNIMS.Domain.Contracts.Inventory;
|
|
using CPRNIMS.Infrastructure.Database;
|
|
using CPRNIMS.Infrastructure.Dto.Inventory.Reports;
|
|
using Microsoft.EntityFrameworkCore;
|
|
|
|
namespace CPRNIMS.Domain.Services.Inventory
|
|
{
|
|
public class InventoryReports : IInventoryReports
|
|
{
|
|
private readonly NonInventoryDbContext _db;
|
|
public InventoryReports(NonInventoryDbContext db) => _db = db;
|
|
|
|
public async Task<RISReportDto> GetRISReportAsync(DateTime dateFrom, DateTime dateTo, CancellationToken ct,
|
|
int? departmentId = null, string? userName = "")
|
|
{
|
|
var endDate = dateTo.Date.AddDays(1);
|
|
|
|
var allowedAllDeptUsers = new[] { "LSKRISUR24", "LSCYNDIZ25", "LSJONTAN25", "LHRIOCAS24" };
|
|
bool seeAllDepartments = !string.IsNullOrWhiteSpace(userName)
|
|
&& allowedAllDeptUsers.Contains(userName, StringComparer.OrdinalIgnoreCase);
|
|
|
|
var dateToInclusive = dateTo.AddDays(1);
|
|
|
|
var query = _db.RIS
|
|
.Include(r => r.Discipline)
|
|
.Include(r => r.Inventory)
|
|
.Include(r => r.MaterialReturns)
|
|
.Where(r => r.CreatedDate >= dateFrom && r.CreatedDate < dateToInclusive);
|
|
|
|
if (departmentId.HasValue && !seeAllDepartments)
|
|
{
|
|
query = query.Where(m =>
|
|
m.Inventory.User != null &&
|
|
m.Inventory.User.DepartmentId == departmentId.Value);
|
|
}
|
|
|
|
var rows = await query
|
|
.OrderByDescending(r => r.CreatedDate)
|
|
.Select(r => new RISReportRow
|
|
{
|
|
RISNo = r.RISNo,
|
|
CreatedDate = r.CreatedDate,
|
|
ItemName = r.PRDetail != null ? r.PRDetail.ItemName : "—",
|
|
ItemNo = r.Inventory.ItemNo,
|
|
DisciplineName = r.Discipline.DisciplineName,
|
|
IssuedTo = r.IssuedTo,
|
|
QtyIssued = r.QtyIssued,
|
|
TotalReturned = r.MaterialReturns
|
|
.Where(m => m.Status != 2)
|
|
.Sum(m => (int?)m.QtyReturned) ?? 0,
|
|
Status = r.Status,
|
|
StatusLabel = r.Status == 0 ? "Draft"
|
|
: r.Status == 1 ? "Approved"
|
|
: "Cancelled"
|
|
})
|
|
.ToListAsync(ct);
|
|
|
|
foreach (var row in rows)
|
|
row.NetIssued = row.QtyIssued - row.TotalReturned;
|
|
|
|
var summary = new RISReportSummary
|
|
{
|
|
TotalRIS = rows.Count,
|
|
TotalApproved = rows.Count(r => r.Status == 1),
|
|
TotalPending = rows.Count(r => r.Status == 0),
|
|
TotalCancelled = rows.Count(r => r.Status == 2),
|
|
TotalQtyIssued = rows.Sum(r => r.QtyIssued),
|
|
TotalQtyReturned = rows.Sum(r => r.TotalReturned),
|
|
TotalNetIssued = rows.Sum(r => r.NetIssued),
|
|
ApprovalRatePct = rows.Count > 0
|
|
? Math.Round(rows.Count(r => r.Status == 1) * 100m / rows.Count, 1)
|
|
: 0
|
|
};
|
|
|
|
var byDiscipline = rows
|
|
.GroupBy(r => r.DisciplineName)
|
|
.Select(g => new DisciplineCount { DisciplineName = g.Key, Count = g.Count() })
|
|
.OrderByDescending(d => d.Count)
|
|
.ToList();
|
|
|
|
var topRecipients = rows
|
|
.GroupBy(r => r.IssuedTo)
|
|
.Select(g => new TopRecipient
|
|
{
|
|
IssuedTo = g.Key,
|
|
SlipCount = g.Count(),
|
|
TotalQty = g.Sum(r => r.QtyIssued)
|
|
})
|
|
.OrderByDescending(t => t.TotalQty)
|
|
.Take(5)
|
|
.ToList();
|
|
|
|
return new RISReportDto
|
|
{
|
|
ReportNo = $"RPT-RIS-{DateTime.Now:yyyyMM}-{Random.Shared.Next(1, 999):D3}",
|
|
DateFrom = dateFrom,
|
|
DateTo = dateTo,
|
|
Summary = summary,
|
|
Rows = rows,
|
|
ByDiscipline = byDiscipline,
|
|
TopRecipients = topRecipients
|
|
};
|
|
}
|
|
|
|
public async Task<MRSReportDto> GetMRSReportAsync(DateTime dateFrom, DateTime dateTo, CancellationToken ct,
|
|
int? departmentId = null, string? userName = "")
|
|
{
|
|
var endDate = dateTo.Date.AddDays(1);
|
|
|
|
var allowedAllDeptUsers = new[] { "LSKRISUR24", "LSCYNDIZ25", "LSJONTAN25", "LHRIOCAS24" };
|
|
bool seeAllDepartments = !string.IsNullOrWhiteSpace(userName)
|
|
&& allowedAllDeptUsers.Contains(userName, StringComparer.OrdinalIgnoreCase);
|
|
|
|
var query = _db.MRS
|
|
.Include(m => m.RIS)
|
|
.Include(m => m.Inventory)
|
|
.ThenInclude(i => i.User)
|
|
.Where(m => m.CreatedDate >= dateFrom &&
|
|
m.CreatedDate < endDate);
|
|
|
|
if (departmentId.HasValue && !seeAllDepartments)
|
|
{
|
|
query = query.Where(m =>
|
|
m.Inventory.User != null &&
|
|
m.Inventory.User.DepartmentId == departmentId.Value);
|
|
}
|
|
|
|
var rows = await query
|
|
.OrderByDescending(m => m.CreatedDate)
|
|
.Select(m => new MRSReportRow
|
|
{
|
|
MRSNo = m.MRSNo,
|
|
CreatedDate = m.CreatedDate,
|
|
RISNo = m.RIS.RISNo,
|
|
ItemName = m.RIS.PRDetail != null ? m.RIS.PRDetail.ItemName : "—",
|
|
ReturnedBy = m.ReturnedBy,
|
|
QtyReturned = m.QtyReturned,
|
|
Condition = m.Condition ?? "Good",
|
|
Status = m.Status,
|
|
StatusLabel = m.Status == 0 ? "Draft"
|
|
: m.Status == 1 ? "Approved"
|
|
: "Cancelled"
|
|
})
|
|
.ToListAsync(ct);
|
|
|
|
// Total RIS qty issued in the same period (for the comparison panel)
|
|
var totalRISQty = await _db.RIS
|
|
.Where(r => r.CreatedDate >= dateFrom && r.CreatedDate < endDate
|
|
&& r.Status != 2)
|
|
.SumAsync(r => (int?)r.QtyIssued, ct) ?? 0;
|
|
|
|
var totalReturned = rows.Where(r => r.Status != 2).Sum(r => r.QtyReturned);
|
|
var goodCount = rows.Count(r => r.Condition == "Good");
|
|
|
|
var summary = new MRSReportSummary
|
|
{
|
|
TotalMRS = rows.Count,
|
|
TotalQtyReturned = totalReturned,
|
|
TotalQtyIssuedRIS = totalRISQty,
|
|
NetQtyConsumed = totalRISQty - totalReturned,
|
|
ReturnRatePct = totalRISQty > 0
|
|
? Math.Round(totalReturned * 100m / totalRISQty, 1)
|
|
: 0,
|
|
GoodConditionPct = rows.Count > 0
|
|
? Math.Round(goodCount * 100m / rows.Count, 1)
|
|
: 0
|
|
};
|
|
|
|
var byCondition = rows
|
|
.Where(r => r.Status != 2)
|
|
.GroupBy(r => r.Condition)
|
|
.Select(g => new ConditionTotal { Condition = g.Key, TotalQty = g.Sum(r => r.QtyReturned) })
|
|
.OrderByDescending(c => c.TotalQty)
|
|
.ToList();
|
|
|
|
return new MRSReportDto
|
|
{
|
|
ReportNo = $"RPT-MRS-{DateTime.Now:yyyyMM}-{Random.Shared.Next(1, 999):D3}",
|
|
DateFrom = dateFrom,
|
|
DateTo = dateTo,
|
|
Summary = summary,
|
|
Rows = rows,
|
|
ByCondition = byCondition
|
|
};
|
|
}
|
|
|
|
public async Task<InventoryReportDto> GetInventoryReportAsync(DateTime dateFrom, DateTime dateTo, CancellationToken ct,
|
|
int? departmentId = null, string? userName = "")
|
|
{
|
|
var endDate = dateTo.Date.AddDays(1);
|
|
|
|
var allowedAllDeptUsers = new[] { "LSKRISUR24", "LSCYNDIZ25", "LSJONTAN25" , "LHRIOCAS24" };
|
|
bool seeAllDepartments = !string.IsNullOrWhiteSpace(userName)
|
|
&& allowedAllDeptUsers.Contains(userName, StringComparer.OrdinalIgnoreCase);
|
|
|
|
var query = _db.InventTransDetails
|
|
.Where(itd =>
|
|
itd.IsActive &&
|
|
itd.InventTrans.IsActive &&
|
|
itd.InventTrans.Inventory.IsActive &&
|
|
itd.CreatedDate >= dateFrom &&
|
|
itd.CreatedDate < endDate);
|
|
|
|
if (departmentId.HasValue && !seeAllDepartments)
|
|
{
|
|
query = query.Where(itd =>
|
|
itd.InventTrans.Inventory.User.DepartmentId == departmentId.Value);
|
|
}
|
|
|
|
var rawRows = await query
|
|
.Select(itd => new
|
|
{
|
|
itd.InventTrans.Inventory.InventoryId,
|
|
itd.InventTrans.Inventory.ItemNo,
|
|
itd.InventTrans.Inventory.QtyIn,
|
|
itd.InventTrans.Inventory.QtyOut,
|
|
itd.InventTrans.Inventory.QtyOnHand,
|
|
LotNo = itd.InventTrans.Inventory.Lot != null
|
|
? itd.InventTrans.Inventory.Lot.LotName
|
|
: null,
|
|
ItemName = itd.InventTrans.Inventory.Item.ItemCode.ItemName ?? "None",
|
|
ItemCategoryName =
|
|
itd.InventTrans.Inventory.Item.ItemCode.ItemCategory.ItemCategoryName ?? "None",
|
|
itd.CreatedDate
|
|
})
|
|
.ToListAsync(ct);
|
|
|
|
// De-duplicate: one row per Inventory (latest trans detail wins for the date shown)
|
|
var rows = rawRows
|
|
.GroupBy(r => r.InventoryId)
|
|
.Select(g =>
|
|
{
|
|
var inv = g.OrderByDescending(x => x.CreatedDate).First();
|
|
return new InventoryReportRow
|
|
{
|
|
ItemName = inv.ItemName,
|
|
ItemNo = inv.ItemNo,
|
|
ItemCategoryName = inv.ItemCategoryName,
|
|
LotNo = inv.LotNo,
|
|
QtyIn = inv.QtyIn,
|
|
QtyOut = inv.QtyOut,
|
|
QtyOnHand = inv.QtyOnHand,
|
|
StockPct = inv.QtyIn > 0
|
|
? (int)Math.Round(Math.Max(0, Math.Min(100, (inv.QtyOnHand / inv.QtyIn) * 100)))
|
|
: 0
|
|
};
|
|
})
|
|
.OrderBy(r => r.ItemName)
|
|
.ToList();
|
|
|
|
var summary = new InventoryReportSummary
|
|
{
|
|
TotalSKUs = rows.Count,
|
|
TotalOnHand = rows.Sum(r => r.QtyOnHand),
|
|
TotalQtyIn = rows.Sum(r => r.QtyIn),
|
|
TotalQtyOut = rows.Sum(r => r.QtyOut),
|
|
LowStockCount = rows.Count(r => r.StockPct < 20 && r.QtyOnHand > 0),
|
|
OutOfStockCount = rows.Count(r => r.QtyOnHand <= 0)
|
|
};
|
|
|
|
var byCategory = rows
|
|
.GroupBy(r => r.ItemCategoryName)
|
|
.Select(g => new CategoryStockLevel
|
|
{
|
|
CategoryName = g.Key,
|
|
AvgStockPct = (int)Math.Round(g.Average(r => r.StockPct))
|
|
})
|
|
.OrderByDescending(c => c.AvgStockPct)
|
|
.ToList();
|
|
|
|
var alerts = rows
|
|
.Where(r => r.StockPct < 20)
|
|
.OrderBy(r => r.StockPct)
|
|
.Take(10)
|
|
.Select(r => new InventoryAlert
|
|
{
|
|
ItemName = r.ItemName,
|
|
QtyOnHand = r.QtyOnHand,
|
|
Severity = r.QtyOnHand <= 0 ? "Critical" : "Low"
|
|
})
|
|
.ToList();
|
|
|
|
return new InventoryReportDto
|
|
{
|
|
ReportNo = $"RPT-INV-{DateTime.Now:yyyyMM}-{Random.Shared.Next(1, 999):D3}",
|
|
AsOf = dateTo.Date, // reflect the requested period end, not always today
|
|
Summary = summary,
|
|
Rows = rows,
|
|
ByCategory = byCategory,
|
|
Alerts = alerts
|
|
};
|
|
}
|
|
}
|
|
}
|