|
|
@@ -0,0 +1,293 @@
|
|
|
+namespace Admin.NET.Plugin.AiDOP.Order;
|
|
|
+
|
|
|
+/// <summary>
|
|
|
+/// 产能交期计算器:根据工艺路线、产线产能、工作日历和节假日,
|
|
|
+/// 从物料齐套日次日开始逐日累加产能,计算生产结束日期。
|
|
|
+/// </summary>
|
|
|
+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; }
|
|
|
+ }
|
|
|
+
|
|
|
+ // ── 公开方法 ──────────────────────────────────────────────
|
|
|
+
|
|
|
+ /// <summary>
|
|
|
+ /// 计算产能交期。
|
|
|
+ /// </summary>
|
|
|
+ /// <param name="itemNum">成品物料编码</param>
|
|
|
+ /// <param name="lackQty">缺口数量(需生产的数量)</param>
|
|
|
+ /// <param name="planStart">物料齐套日(KittingTime),生产从其次日开始</param>
|
|
|
+ /// <param name="planDate">计划交货日期,用于兜底比较</param>
|
|
|
+ /// <param name="tenantId">租户ID</param>
|
|
|
+ /// <returns>计算后的预计交货日期</returns>
|
|
|
+ public async Task<DateTime> 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<List<RoutingOpRow>> LoadRoutingOpsAsync(long tenantId)
|
|
|
+ {
|
|
|
+ return await _db.Ado.SqlQueryAsync<RoutingOpRow>(
|
|
|
+ """
|
|
|
+ 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<List<ProdLineRow>> LoadProdLinesAsync(string itemNum, long tenantId)
|
|
|
+ {
|
|
|
+ return await _db.Ado.SqlQueryAsync<ProdLineRow>(
|
|
|
+ """
|
|
|
+ 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<List<HolidayRow>> LoadHolidaysAsync(long tenantId)
|
|
|
+ {
|
|
|
+ return await _db.Ado.SqlQueryAsync<HolidayRow>(
|
|
|
+ """
|
|
|
+ SELECT Dated, IFNULL(Ufld1, '') AS Ufld1
|
|
|
+ FROM HolidayMaster
|
|
|
+ WHERE IFNULL(IsActive, 1) = 1 AND tenant_id = @TenantId
|
|
|
+ """,
|
|
|
+ new SugarParameter("@TenantId", tenantId));
|
|
|
+ }
|
|
|
+
|
|
|
+ private async Task<List<CalendarRow>> LoadCalendarsAsync(string prodLine, long tenantId)
|
|
|
+ {
|
|
|
+ return await _db.Ado.SqlQueryAsync<CalendarRow>(
|
|
|
+ """
|
|
|
+ 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<List<QualityRow>> LoadQualityLinesAsync(string prodLine, long tenantId)
|
|
|
+ {
|
|
|
+ return await _db.Ado.SqlQueryAsync<QualityRow>(
|
|
|
+ """
|
|
|
+ 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<IcItemLeadTimes?> LoadIcItemLeadTimesAsync(string itemNum, long tenantId)
|
|
|
+ {
|
|
|
+ var rows = await _db.Ado.SqlQueryAsync<IcItemLeadTimes>(
|
|
|
+ """
|
|
|
+ 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<RoutingOpRow> routingOps,
|
|
|
+ List<ProdLineRow> prodLines,
|
|
|
+ List<HolidayRow> 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);
|
|
|
+ }
|
|
|
+
|
|
|
+ /// <summary>获取下一个工作日,跳过周末和节假日,处理调班。</summary>
|
|
|
+ private static DateTime GetNextWorkDay(
|
|
|
+ DateTime startTime,
|
|
|
+ List<CalendarRow> curCalendars,
|
|
|
+ List<HolidayRow> 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;
|
|
|
+ }
|
|
|
+
|
|
|
+ /// <summary>计算当天的产能。</summary>
|
|
|
+ private static decimal CalcCapacity(
|
|
|
+ DateTime startTime,
|
|
|
+ ProdLineRow prodLine,
|
|
|
+ List<CalendarRow> curCalendars,
|
|
|
+ List<QualityRow> 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);
|
|
|
+ }
|
|
|
+}
|