308 lines
14 KiB
C#
308 lines
14 KiB
C#
using CPRNIMS.Domain.Contracts.Inventory;
|
|
using CPRNIMS.Infrastructure.Database;
|
|
using CPRNIMS.Infrastructure.Dto.Inventory.Reports;
|
|
using CPRNIMS.Infrastructure.Dto.Inventory.Request;
|
|
using Microsoft.Data.SqlClient;
|
|
using Microsoft.EntityFrameworkCore;
|
|
using System.Drawing.Printing;
|
|
using static System.Runtime.InteropServices.JavaScript.JSType;
|
|
|
|
namespace CPRNIMS.Domain.Services.Inventory
|
|
{
|
|
public class InventoryReports : IInventoryReports
|
|
{
|
|
private readonly NonInventoryDbContext _db;
|
|
public InventoryReports(NonInventoryDbContext db) => _db = db;
|
|
|
|
public async Task<RISReportDto> GetRISReportAsync(InventoryReportsRequest request, string userName, int? departmentId, CancellationToken ct)
|
|
{
|
|
var endDate = request.DateTo.Date.AddDays(1);
|
|
|
|
var allowedAllDeptUsers = new[] { "LSKRISUR24", "LSCYNDIZ25", "LSJONTAN25", "LHRIOCAS24" };
|
|
bool seeAllDepartments = !string.IsNullOrWhiteSpace(userName)
|
|
&& allowedAllDeptUsers.Contains(userName, StringComparer.OrdinalIgnoreCase);
|
|
|
|
var dateToInclusive = request.DateTo.AddDays(1);
|
|
|
|
var query = _db.RIS
|
|
.Include(r => r.Discipline)
|
|
.Include(r => r.Inventory)
|
|
.Include(r => r.MaterialReturns)
|
|
.Skip((request.Page - 1) * request.PageSize)
|
|
.Take(request.PageSize)
|
|
.Where(r => r.CreatedDate >= request.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,
|
|
ProjectName = r.ProjectCodes.ProjectName,
|
|
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.ProjectName)
|
|
.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 = request.DateFrom,
|
|
DateTo = request.DateTo,
|
|
Summary = summary,
|
|
Rows = rows,
|
|
ByDiscipline = byDiscipline,
|
|
TopRecipients = topRecipients
|
|
};
|
|
}
|
|
|
|
public async Task<MRSReportDto> GetMRSReportAsync(InventoryReportsRequest request, string userName, int? departmentId, CancellationToken ct)
|
|
{
|
|
var endDate = request.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)
|
|
.Skip((request.Page - 1) * request.PageSize)
|
|
.Take(request.PageSize)
|
|
.Where(m => m.CreatedDate >= request.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 >= request.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 = request.DateFrom,
|
|
DateTo = request.DateTo,
|
|
Summary = summary,
|
|
Rows = rows,
|
|
ByCondition = byCondition
|
|
};
|
|
}
|
|
|
|
public async Task<InventoryReportDto> GetInventoryReportAsync(InventoryReportsRequest request, string userName, int? departmentId, CancellationToken ct)
|
|
{
|
|
var allowedAllDeptUsers = new[] { "LSKRISUR24", "LSCYNDIZ25", "LSJONTAN25", "LHRIOCAS24" };
|
|
bool seeAllDepartments = !string.IsNullOrWhiteSpace(userName)
|
|
&& allowedAllDeptUsers.Contains(userName, StringComparer.OrdinalIgnoreCase);
|
|
|
|
var endDate = request.DateTo.Date.AddDays(1);
|
|
|
|
var dto = new InventoryReportDto
|
|
{
|
|
ReportNo = $"RPT-INV-{DateTime.Now:yyyyMM}-{Random.Shared.Next(1, 999):D3}",
|
|
AsOf = endDate,
|
|
Rows = new List<InventoryReportRow>(),
|
|
ByCategory = new List<CategoryStockLevel>(),
|
|
Alerts = new List<InventoryAlert>(),
|
|
Summary = new InventoryReportSummary(),
|
|
Departments = new List<string>(),
|
|
Page = request.Page,
|
|
PageSize = request.PageSize
|
|
};
|
|
|
|
var conn = _db.Database.GetDbConnection();
|
|
await using var cmd = conn.CreateCommand();
|
|
cmd.CommandText = "dbo.GetInventoryReport";
|
|
cmd.CommandType = System.Data.CommandType.StoredProcedure;
|
|
cmd.Parameters.Add(new SqlParameter("@DateFrom", (object?)request.DateFrom ?? DBNull.Value));
|
|
cmd.Parameters.Add(new SqlParameter("@DateTo", (object?)request.DateTo ?? DBNull.Value));
|
|
cmd.Parameters.Add(new SqlParameter("@Department", (object?)request.Department ?? DBNull.Value));
|
|
cmd.Parameters.Add(new SqlParameter("@DepartmentId", (object?)departmentId ?? DBNull.Value));
|
|
cmd.Parameters.Add(new SqlParameter("@SeeAllDepartments", seeAllDepartments ? 1 : 0));
|
|
cmd.Parameters.Add(new SqlParameter("@Page", request.Page));
|
|
cmd.Parameters.Add(new SqlParameter("@PageSize", request.PageSize));
|
|
cmd.Parameters.Add(new SqlParameter("@Paginate", request.Paginate ? 1 : 0));
|
|
|
|
bool openedHere = conn.State != System.Data.ConnectionState.Open;
|
|
if (openedHere) await conn.OpenAsync(ct);
|
|
|
|
try
|
|
{
|
|
await using var reader = await cmd.ExecuteReaderAsync(ct);
|
|
|
|
// 0: departments
|
|
while (await reader.ReadAsync(ct))
|
|
dto.Departments.Add(reader.GetString(reader.GetOrdinal("Department")));
|
|
|
|
// 1: detail rows
|
|
if (await reader.NextResultAsync(ct))
|
|
{
|
|
while (await reader.ReadAsync(ct))
|
|
{
|
|
dto.Rows.Add(new InventoryReportRow
|
|
{
|
|
ItemName = reader.GetString(reader.GetOrdinal("ItemName")),
|
|
ItemNo = reader.GetInt64(reader.GetOrdinal("ItemNo")),
|
|
ItemCategoryName = reader.GetString(reader.GetOrdinal("ItemCategoryName")),
|
|
LotNo = reader.IsDBNull(reader.GetOrdinal("LotNo")) ? null : reader.GetString(reader.GetOrdinal("LotNo")),
|
|
Department = reader.IsDBNull(reader.GetOrdinal("Department")) ? null : reader.GetString(reader.GetOrdinal("Department")),
|
|
CurrencyCode = reader.IsDBNull(reader.GetOrdinal("CurrencyCode")) ? null : reader.GetString(reader.GetOrdinal("CurrencyCode")),
|
|
QtyIn = reader.GetDecimal(reader.GetOrdinal("QtyIn")),
|
|
QtyOut = reader.GetDecimal(reader.GetOrdinal("QtyOut")),
|
|
QtyOnHand = reader.GetDecimal(reader.GetOrdinal("QtyOnHand")),
|
|
UnitPrice = reader.GetDecimal(reader.GetOrdinal("UnitPrice")),
|
|
StockPct = reader.GetInt32(reader.GetOrdinal("StockPct"))
|
|
});
|
|
}
|
|
}
|
|
|
|
// 2: summary
|
|
if (await reader.NextResultAsync(ct) && await reader.ReadAsync(ct))
|
|
{
|
|
dto.Summary = new InventoryReportSummary
|
|
{
|
|
TotalSKUs = reader.GetInt32(reader.GetOrdinal("TotalSKUs")),
|
|
TotalOnHand = reader.GetDecimal(reader.GetOrdinal("TotalOnHand")),
|
|
TotalQtyIn = reader.GetDecimal(reader.GetOrdinal("TotalQtyIn")),
|
|
TotalQtyOut = reader.GetDecimal(reader.GetOrdinal("TotalQtyOut")),
|
|
TotalValue = reader.GetDecimal(reader.GetOrdinal("TotalValue")),
|
|
LowStockCount = reader.GetInt32(reader.GetOrdinal("LowStockCount")),
|
|
OutOfStockCount = reader.GetInt32(reader.GetOrdinal("OutOfStockCount"))
|
|
};
|
|
}
|
|
|
|
// 3: by category
|
|
if (await reader.NextResultAsync(ct))
|
|
while (await reader.ReadAsync(ct))
|
|
dto.ByCategory.Add(new CategoryStockLevel
|
|
{
|
|
CategoryName = reader.GetString(reader.GetOrdinal("CategoryName")),
|
|
AvgStockPct = reader.GetInt32(reader.GetOrdinal("AvgStockPct"))
|
|
});
|
|
|
|
// 4: alerts
|
|
if (await reader.NextResultAsync(ct))
|
|
while (await reader.ReadAsync(ct))
|
|
dto.Alerts.Add(new InventoryAlert
|
|
{
|
|
ItemName = reader.GetString(reader.GetOrdinal("ItemName")),
|
|
QtyOnHand = reader.GetDecimal(reader.GetOrdinal("QtyOnHand")),
|
|
Severity = reader.GetString(reader.GetOrdinal("Severity"))
|
|
});
|
|
|
|
// 5: total row count
|
|
if (await reader.NextResultAsync(ct) && await reader.ReadAsync(ct))
|
|
dto.TotalRows = reader.GetInt32(reader.GetOrdinal("TotalRows"));
|
|
}
|
|
finally
|
|
{
|
|
if (conn.State == System.Data.ConnectionState.Open)
|
|
await conn.CloseAsync();
|
|
}
|
|
|
|
return dto;
|
|
}
|
|
}
|
|
}
|