namespace Admin.NET.Plugin.AiDOP.Order;
///
/// 产能交期计算器:根据工艺路线、产线产能、工作日历和节假日,
/// 从物料齐套日次日开始逐日累加产能,计算生产结束日期。
///
public class CapacityDateCalculator : ITransient
{
private readonly ISqlSugarClient _db;
public CapacityDateCalculator(ISqlSugarClient db)
{
_db = db;
}
// ── 内部 DTO ──────────────────────────────────────────────
private sealed class RoutingOpRow
{
public int Op { get; set; }
public int ParentOp { get; set; }
public string RoutingCode { get; set; } = string.Empty;
}
private sealed class ProdLineRow
{
public string Part { get; set; } = string.Empty;
public int Op { get; set; }
public string Line { get; set; } = string.Empty;
public decimal Rate { get; set; }
}
private sealed class CalendarRow
{
public string ProdLine { get; set; } = string.Empty;
public int WeekDay { get; set; }
public decimal ShiftsHours1 { get; set; }
}
private sealed class QualityRow
{
public string ProdLine { get; set; } = string.Empty;
public decimal RestTime { get; set; }
}
private sealed class HolidayRow
{
public DateTime Dated { get; set; }
public string Ufld1 { get; set; } = string.Empty;
}
private sealed class IcItemLeadTimes
{
public int SelfInspectionDate { get; set; }
public int WarehousingDate { get; set; }
public int ShippingDate { get; set; }
public int RoutingDesignTime { get; set; }
}
// ── 公开方法 ──────────────────────────────────────────────
///
/// 计算产能交期。
///
/// 成品物料编码
/// 缺口数量(需生产的数量)
/// 物料齐套日(KittingTime),生产从其次日开始
/// 计划交货日期,用于兜底比较
/// 租户ID
/// 计算后的预计交货日期
public async Task CalculateAsync(
string itemNum,
decimal lackQty,
DateTime planStart,
DateTime planDate,
long tenantId)
{
if (lackQty <= 0)
return planDate;
// 加载参考数据
var routingOps = await LoadRoutingOpsAsync(tenantId);
var prodLines = await LoadProdLinesAsync(itemNum, tenantId);
var holidays = await LoadHolidaysAsync(tenantId);
var planStartDate = planStart.Date.AddDays(1);
// 产能计算
var prodEnd = ProductiveExamine(itemNum, lackQty, planStartDate,
routingOps, prodLines, holidays, tenantId);
// 查 ic_item 提前期
var leadTimes = await LoadIcItemLeadTimesAsync(itemNum, tenantId);
if (leadTimes != null)
{
var addDays = leadTimes.SelfInspectionDate
+ leadTimes.WarehousingDate
+ leadTimes.ShippingDate
+ leadTimes.RoutingDesignTime;
prodEnd = prodEnd.AddDays(addDays);
}
// 与计划交期取较大值
return prodEnd < planDate ? planDate : prodEnd;
}
// ── 数据加载 ──────────────────────────────────────────────
private async Task> LoadRoutingOpsAsync(long tenantId)
{
return await _db.Ado.SqlQueryAsync(
"""
SELECT Op, IFNULL(ParentOp, 0) AS ParentOp, RoutingCode
FROM RoutingOpDetail
WHERE IFNULL(IsActive, 1) = 1 AND tenant_id = @TenantId
""",
new SugarParameter("@TenantId", tenantId));
}
private async Task> LoadProdLinesAsync(string itemNum, long tenantId)
{
return await _db.Ado.SqlQueryAsync(
"""
SELECT Part, Op, IFNULL(Line, '') AS Line, IFNULL(Rate, 0) AS Rate
FROM ProdLineDetail
WHERE Part = @Part AND tenant_id = @TenantId
""",
new SugarParameter("@Part", itemNum),
new SugarParameter("@TenantId", tenantId));
}
private async Task> LoadHolidaysAsync(long tenantId)
{
return await _db.Ado.SqlQueryAsync(
"""
SELECT Dated, IFNULL(Ufld1, '') AS Ufld1
FROM HolidayMaster
WHERE IFNULL(IsActive, 1) = 1 AND tenant_id = @TenantId
""",
new SugarParameter("@TenantId", tenantId));
}
private async Task> LoadCalendarsAsync(string prodLine, long tenantId)
{
return await _db.Ado.SqlQueryAsync(
"""
SELECT ProdLine, WeekDay, IFNULL(ShiftsHours1, 0) AS ShiftsHours1
FROM ShopCalendarWorkCtr
WHERE ProdLine = @ProdLine AND IFNULL(IsActive, 1) = 1 AND tenant_id = @TenantId
""",
new SugarParameter("@ProdLine", prodLine),
new SugarParameter("@TenantId", tenantId));
}
private async Task> LoadQualityLinesAsync(string prodLine, long tenantId)
{
return await _db.Ado.SqlQueryAsync(
"""
SELECT ProdLine, IFNULL(RestTime, 0) AS RestTime
FROM QualityLineWorkDetail
WHERE ProdLine = @ProdLine AND IFNULL(IsActive, 1) = 1 AND tenant_id = @TenantId
""",
new SugarParameter("@ProdLine", prodLine),
new SugarParameter("@TenantId", tenantId));
}
private async Task LoadIcItemLeadTimesAsync(string itemNum, long tenantId)
{
var rows = await _db.Ado.SqlQueryAsync(
"""
SELECT
CAST(IFNULL(self_inspection_date, 0) AS SIGNED) AS SelfInspectionDate,
CAST(IFNULL(Warehousing_date, 0) AS SIGNED) AS WarehousingDate,
CAST(IFNULL(Shipping_date, 0) AS SIGNED) AS ShippingDate,
CAST(IFNULL(RoutingDesignTime, 0) AS SIGNED) AS RoutingDesignTime
FROM ic_item
WHERE number = @ItemNum AND tenant_id = @TenantId
LIMIT 1
""",
new SugarParameter("@ItemNum", itemNum),
new SugarParameter("@TenantId", tenantId));
return rows.Count > 0 ? rows[0] : null;
}
// ── 产能计算核心 ──────────────────────────────────────────
private DateTime ProductiveExamine(
string itemNum,
decimal qtyOrd,
DateTime planStart,
List routingOps,
List prodLines,
List holidays,
long tenantId)
{
DateTime planEnd = planStart.Date;
if (qtyOrd <= 0)
return planEnd;
// 获取当前产品的工艺路线:主产线,最后一道工序(ParentOp=0)
var curRoutingOps = routingOps
.Where(p => p.RoutingCode.Equals(itemNum, StringComparison.OrdinalIgnoreCase) && p.ParentOp == 0)
.OrderByDescending(p => p.Op)
.ToList();
if (curRoutingOps.Count == 0)
return planEnd;
var lastOp = curRoutingOps.First();
// 获取产线
var curProdLine = prodLines
.FirstOrDefault(p => p.Part.Equals(itemNum, StringComparison.OrdinalIgnoreCase) && p.Op == lastOp.Op);
if (curProdLine == null || string.IsNullOrWhiteSpace(curProdLine.Line))
return planEnd;
// 加载产线工作日历(延迟加载,仅在此产线需要时查询)
var curCalendars = LoadCalendarsAsync(curProdLine.Line, tenantId).GetAwaiter().GetResult();
if (curCalendars.Count == 0 || curCalendars.Count != 7)
return planEnd;
// 加载产线休息时间
var curQualityLines = LoadQualityLinesAsync(curProdLine.Line, tenantId).GetAwaiter().GetResult();
// 逐日累加产能
decimal sumAmount = 0m;
int maxDays = 365 * 3; // 防止死循环保护
int dayCount = 0;
do
{
// 获取工作日
planEnd = GetNextWorkDay(planEnd, curCalendars, holidays);
// 计算工作日的产能
sumAmount += CalcCapacity(planEnd, curProdLine, curCalendars, curQualityLines);
planEnd = planEnd.AddDays(1);
dayCount++;
} while (sumAmount < qtyOrd && dayCount < maxDays);
return planEnd.AddDays(-1);
}
/// 获取下一个工作日,跳过周末和节假日,处理调班。
private static DateTime GetNextWorkDay(
DateTime startTime,
List curCalendars,
List holidays)
{
int weekDay = (int)startTime.DayOfWeek;
// 周六、周日
if (weekDay == 0 || weekDay == 6)
{
// 检查是否有调班(周末上班)
var isShiftWork = holidays.Any(p =>
p.Dated.Date == startTime.Date &&
p.Ufld1.Equals("调班", StringComparison.OrdinalIgnoreCase));
if (!isShiftWork)
{
// 递归找下一个工作日
return GetNextWorkDay(startTime.AddDays(1), curCalendars, holidays);
}
return startTime;
}
// 工作日,检查是否是节假日(休假)
var isHoliday = holidays.Any(p =>
p.Dated.Date == startTime.Date &&
p.Ufld1.Equals("休假", StringComparison.OrdinalIgnoreCase));
if (isHoliday)
{
return GetNextWorkDay(startTime.AddDays(1), curCalendars, holidays);
}
return startTime;
}
/// 计算当天的产能。
private static decimal CalcCapacity(
DateTime startTime,
ProdLineRow prodLine,
List curCalendars,
List curQualityLines)
{
// 获取休息时长(分钟)
decimal sumRest = curQualityLines.Sum(p => p.RestTime);
// 获取当天的工作时长(分钟)
var calendar = curCalendars.FirstOrDefault(p => p.WeekDay == (int)startTime.DayOfWeek);
if (calendar == null) return 0;
decimal workTime = calendar.ShiftsHours1 * 60;
// 计算产能
if (workTime <= sumRest) return 0;
return Math.Ceiling((workTime - sumRest) / 60m * prodLine.Rate);
}
}