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); } }