CapacityDateCalculator.cs 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293
  1. namespace Admin.NET.Plugin.AiDOP.Order;
  2. /// <summary>
  3. /// 产能交期计算器:根据工艺路线、产线产能、工作日历和节假日,
  4. /// 从物料齐套日次日开始逐日累加产能,计算生产结束日期。
  5. /// </summary>
  6. public class CapacityDateCalculator : ITransient
  7. {
  8. private readonly ISqlSugarClient _db;
  9. public CapacityDateCalculator(ISqlSugarClient db)
  10. {
  11. _db = db;
  12. }
  13. // ── 内部 DTO ──────────────────────────────────────────────
  14. private sealed class RoutingOpRow
  15. {
  16. public int Op { get; set; }
  17. public int ParentOp { get; set; }
  18. public string RoutingCode { get; set; } = string.Empty;
  19. }
  20. private sealed class ProdLineRow
  21. {
  22. public string Part { get; set; } = string.Empty;
  23. public int Op { get; set; }
  24. public string Line { get; set; } = string.Empty;
  25. public decimal Rate { get; set; }
  26. }
  27. private sealed class CalendarRow
  28. {
  29. public string ProdLine { get; set; } = string.Empty;
  30. public int WeekDay { get; set; }
  31. public decimal ShiftsHours1 { get; set; }
  32. }
  33. private sealed class QualityRow
  34. {
  35. public string ProdLine { get; set; } = string.Empty;
  36. public decimal RestTime { get; set; }
  37. }
  38. private sealed class HolidayRow
  39. {
  40. public DateTime Dated { get; set; }
  41. public string Ufld1 { get; set; } = string.Empty;
  42. }
  43. private sealed class IcItemLeadTimes
  44. {
  45. public int SelfInspectionDate { get; set; }
  46. public int WarehousingDate { get; set; }
  47. public int ShippingDate { get; set; }
  48. public int RoutingDesignTime { get; set; }
  49. }
  50. // ── 公开方法 ──────────────────────────────────────────────
  51. /// <summary>
  52. /// 计算产能交期。
  53. /// </summary>
  54. /// <param name="itemNum">成品物料编码</param>
  55. /// <param name="lackQty">缺口数量(需生产的数量)</param>
  56. /// <param name="planStart">物料齐套日(KittingTime),生产从其次日开始</param>
  57. /// <param name="planDate">计划交货日期,用于兜底比较</param>
  58. /// <param name="tenantId">租户ID</param>
  59. /// <returns>计算后的预计交货日期</returns>
  60. public async Task<DateTime> CalculateAsync(
  61. string itemNum,
  62. decimal lackQty,
  63. DateTime planStart,
  64. DateTime planDate,
  65. long tenantId)
  66. {
  67. if (lackQty <= 0)
  68. return planDate;
  69. // 加载参考数据
  70. var routingOps = await LoadRoutingOpsAsync(tenantId);
  71. var prodLines = await LoadProdLinesAsync(itemNum, tenantId);
  72. var holidays = await LoadHolidaysAsync(tenantId);
  73. var planStartDate = planStart.Date.AddDays(1);
  74. // 产能计算
  75. var prodEnd = ProductiveExamine(itemNum, lackQty, planStartDate,
  76. routingOps, prodLines, holidays, tenantId);
  77. // 查 ic_item 提前期
  78. var leadTimes = await LoadIcItemLeadTimesAsync(itemNum, tenantId);
  79. if (leadTimes != null)
  80. {
  81. var addDays = leadTimes.SelfInspectionDate
  82. + leadTimes.WarehousingDate
  83. + leadTimes.ShippingDate
  84. + leadTimes.RoutingDesignTime;
  85. prodEnd = prodEnd.AddDays(addDays);
  86. }
  87. // 与计划交期取较大值
  88. return prodEnd < planDate ? planDate : prodEnd;
  89. }
  90. // ── 数据加载 ──────────────────────────────────────────────
  91. private async Task<List<RoutingOpRow>> LoadRoutingOpsAsync(long tenantId)
  92. {
  93. return await _db.Ado.SqlQueryAsync<RoutingOpRow>(
  94. """
  95. SELECT Op, IFNULL(ParentOp, 0) AS ParentOp, RoutingCode
  96. FROM RoutingOpDetail
  97. WHERE IFNULL(IsActive, 1) = 1 AND tenant_id = @TenantId
  98. """,
  99. new SugarParameter("@TenantId", tenantId));
  100. }
  101. private async Task<List<ProdLineRow>> LoadProdLinesAsync(string itemNum, long tenantId)
  102. {
  103. return await _db.Ado.SqlQueryAsync<ProdLineRow>(
  104. """
  105. SELECT Part, Op, IFNULL(Line, '') AS Line, IFNULL(Rate, 0) AS Rate
  106. FROM ProdLineDetail
  107. WHERE Part = @Part AND tenant_id = @TenantId
  108. """,
  109. new SugarParameter("@Part", itemNum),
  110. new SugarParameter("@TenantId", tenantId));
  111. }
  112. private async Task<List<HolidayRow>> LoadHolidaysAsync(long tenantId)
  113. {
  114. return await _db.Ado.SqlQueryAsync<HolidayRow>(
  115. """
  116. SELECT Dated, IFNULL(Ufld1, '') AS Ufld1
  117. FROM HolidayMaster
  118. WHERE IFNULL(IsActive, 1) = 1 AND tenant_id = @TenantId
  119. """,
  120. new SugarParameter("@TenantId", tenantId));
  121. }
  122. private async Task<List<CalendarRow>> LoadCalendarsAsync(string prodLine, long tenantId)
  123. {
  124. return await _db.Ado.SqlQueryAsync<CalendarRow>(
  125. """
  126. SELECT ProdLine, WeekDay, IFNULL(ShiftsHours1, 0) AS ShiftsHours1
  127. FROM ShopCalendarWorkCtr
  128. WHERE ProdLine = @ProdLine AND IFNULL(IsActive, 1) = 1 AND tenant_id = @TenantId
  129. """,
  130. new SugarParameter("@ProdLine", prodLine),
  131. new SugarParameter("@TenantId", tenantId));
  132. }
  133. private async Task<List<QualityRow>> LoadQualityLinesAsync(string prodLine, long tenantId)
  134. {
  135. return await _db.Ado.SqlQueryAsync<QualityRow>(
  136. """
  137. SELECT ProdLine, IFNULL(RestTime, 0) AS RestTime
  138. FROM QualityLineWorkDetail
  139. WHERE ProdLine = @ProdLine AND IFNULL(IsActive, 1) = 1 AND tenant_id = @TenantId
  140. """,
  141. new SugarParameter("@ProdLine", prodLine),
  142. new SugarParameter("@TenantId", tenantId));
  143. }
  144. private async Task<IcItemLeadTimes?> LoadIcItemLeadTimesAsync(string itemNum, long tenantId)
  145. {
  146. var rows = await _db.Ado.SqlQueryAsync<IcItemLeadTimes>(
  147. """
  148. SELECT
  149. CAST(IFNULL(self_inspection_date, 0) AS SIGNED) AS SelfInspectionDate,
  150. CAST(IFNULL(Warehousing_date, 0) AS SIGNED) AS WarehousingDate,
  151. CAST(IFNULL(Shipping_date, 0) AS SIGNED) AS ShippingDate,
  152. CAST(IFNULL(RoutingDesignTime, 0) AS SIGNED) AS RoutingDesignTime
  153. FROM ic_item
  154. WHERE number = @ItemNum AND tenant_id = @TenantId
  155. LIMIT 1
  156. """,
  157. new SugarParameter("@ItemNum", itemNum),
  158. new SugarParameter("@TenantId", tenantId));
  159. return rows.Count > 0 ? rows[0] : null;
  160. }
  161. // ── 产能计算核心 ──────────────────────────────────────────
  162. private DateTime ProductiveExamine(
  163. string itemNum,
  164. decimal qtyOrd,
  165. DateTime planStart,
  166. List<RoutingOpRow> routingOps,
  167. List<ProdLineRow> prodLines,
  168. List<HolidayRow> holidays,
  169. long tenantId)
  170. {
  171. DateTime planEnd = planStart.Date;
  172. if (qtyOrd <= 0)
  173. return planEnd;
  174. // 获取当前产品的工艺路线:主产线,最后一道工序(ParentOp=0)
  175. var curRoutingOps = routingOps
  176. .Where(p => p.RoutingCode.Equals(itemNum, StringComparison.OrdinalIgnoreCase) && p.ParentOp == 0)
  177. .OrderByDescending(p => p.Op)
  178. .ToList();
  179. if (curRoutingOps.Count == 0)
  180. return planEnd;
  181. var lastOp = curRoutingOps.First();
  182. // 获取产线
  183. var curProdLine = prodLines
  184. .FirstOrDefault(p => p.Part.Equals(itemNum, StringComparison.OrdinalIgnoreCase) && p.Op == lastOp.Op);
  185. if (curProdLine == null || string.IsNullOrWhiteSpace(curProdLine.Line))
  186. return planEnd;
  187. // 加载产线工作日历(延迟加载,仅在此产线需要时查询)
  188. var curCalendars = LoadCalendarsAsync(curProdLine.Line, tenantId).GetAwaiter().GetResult();
  189. if (curCalendars.Count == 0 || curCalendars.Count != 7)
  190. return planEnd;
  191. // 加载产线休息时间
  192. var curQualityLines = LoadQualityLinesAsync(curProdLine.Line, tenantId).GetAwaiter().GetResult();
  193. // 逐日累加产能
  194. decimal sumAmount = 0m;
  195. int maxDays = 365 * 3; // 防止死循环保护
  196. int dayCount = 0;
  197. do
  198. {
  199. // 获取工作日
  200. planEnd = GetNextWorkDay(planEnd, curCalendars, holidays);
  201. // 计算工作日的产能
  202. sumAmount += CalcCapacity(planEnd, curProdLine, curCalendars, curQualityLines);
  203. planEnd = planEnd.AddDays(1);
  204. dayCount++;
  205. } while (sumAmount < qtyOrd && dayCount < maxDays);
  206. return planEnd.AddDays(-1);
  207. }
  208. /// <summary>获取下一个工作日,跳过周末和节假日,处理调班。</summary>
  209. private static DateTime GetNextWorkDay(
  210. DateTime startTime,
  211. List<CalendarRow> curCalendars,
  212. List<HolidayRow> holidays)
  213. {
  214. int weekDay = (int)startTime.DayOfWeek;
  215. // 周六、周日
  216. if (weekDay == 0 || weekDay == 6)
  217. {
  218. // 检查是否有调班(周末上班)
  219. var isShiftWork = holidays.Any(p =>
  220. p.Dated.Date == startTime.Date &&
  221. p.Ufld1.Equals("调班", StringComparison.OrdinalIgnoreCase));
  222. if (!isShiftWork)
  223. {
  224. // 递归找下一个工作日
  225. return GetNextWorkDay(startTime.AddDays(1), curCalendars, holidays);
  226. }
  227. return startTime;
  228. }
  229. // 工作日,检查是否是节假日(休假)
  230. var isHoliday = holidays.Any(p =>
  231. p.Dated.Date == startTime.Date &&
  232. p.Ufld1.Equals("休假", StringComparison.OrdinalIgnoreCase));
  233. if (isHoliday)
  234. {
  235. return GetNextWorkDay(startTime.AddDays(1), curCalendars, holidays);
  236. }
  237. return startTime;
  238. }
  239. /// <summary>计算当天的产能。</summary>
  240. private static decimal CalcCapacity(
  241. DateTime startTime,
  242. ProdLineRow prodLine,
  243. List<CalendarRow> curCalendars,
  244. List<QualityRow> curQualityLines)
  245. {
  246. // 获取休息时长(分钟)
  247. decimal sumRest = curQualityLines.Sum(p => p.RestTime);
  248. // 获取当天的工作时长(分钟)
  249. var calendar = curCalendars.FirstOrDefault(p => p.WeekDay == (int)startTime.DayOfWeek);
  250. if (calendar == null) return 0;
  251. decimal workTime = calendar.ShiftsHours1 * 60;
  252. // 计算产能
  253. if (workTime <= sumRest) return 0;
  254. return Math.Ceiling((workTime - sumRest) / 60m * prodLine.Rate);
  255. }
  256. }