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