ExecutableDailyPlanService.cs 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472
  1. namespace Admin.NET.Plugin.AiDOP.Production;
  2. /// <summary>
  3. /// 可执行生产日计划(PeriodSequenceDet + 排产/工单等)
  4. /// 路由前缀:/api/Production/daily-plan/...
  5. /// </summary>
  6. [ApiDescriptionSettings(Order = 266, Description = "可执行生产日计划")]
  7. [Route("api/Production")]
  8. [AllowAnonymous]
  9. [NonUnify]
  10. public class ExecutableDailyPlanService : IDynamicApiController, ITransient
  11. {
  12. /// <summary>
  13. /// S8 Stage-2「产品设计」的 End 事件码。
  14. ///
  15. /// <para>语义:某张真实销售订单<b>首次</b>有至少一条日计划由「未下达」成功变为
  16. /// <c>Status='Y'</c>,即该订单首次具备可执行生产日计划。</para>
  17. ///
  18. /// <para>Stage-2 End 取本事件 <b>FIRST SUCCESS 的 end_time</b>(不是 start_time)——
  19. /// end_time 写在业务状态提交成功之后,与 Stage-1 的 <c>S1_DELIVERY_CONFIRM</c> 同口径。</para>
  20. ///
  21. /// <para>⚠️ 动作码前缀 <c>S2_</c> 只是本系统的动作命名,<b>不代表 S8 五阶段归属</b>;
  22. /// 本事件服务于 S8 的 PRODUCT_DESIGN 阶段。</para>
  23. /// </summary>
  24. public const string ActionDailyPlanRelease = "S2_DAILY_PLAN_RELEASE";
  25. private readonly ISqlSugarClient _db;
  26. private readonly UserManager _userManager;
  27. private readonly AidopActionRunLogWriter _runLog;
  28. public ExecutableDailyPlanService(
  29. ISqlSugarClient db,
  30. UserManager userManager,
  31. AidopActionRunLogWriter runLog)
  32. {
  33. _db = db;
  34. _userManager = userManager;
  35. _runLog = runLog;
  36. }
  37. /// <summary>分页列表 GET /api/Production/daily-plan/list</summary>
  38. [DisplayName("可执行生产日计划列表")]
  39. [HttpGet("daily-plan/list")]
  40. public async Task<object> GetList([FromQuery] ExecutableDailyPlanListInput input)
  41. {
  42. var tenantId = AidopTenantHelper.Resolve(App.HttpContext);
  43. var pars = new List<SugarParameter> { new("@TenantId", tenantId) };
  44. var innerWhere = new List<string>
  45. {
  46. "p.OrdQty > 0",
  47. "p.IsActive = 1",
  48. "LOWER(IFNULL(w.Status,'')) NOT IN ('', 'c', 'p')",
  49. "p.tenant_id = @TenantId"
  50. };
  51. if (!string.IsNullOrWhiteSpace(input.PlanDateMin))
  52. {
  53. var pdm = input.PlanDateMin.Trim();
  54. if (pdm.Length > 10) pdm = pdm[..10];
  55. innerWhere.Add("DATE(p.PlanDate) >= @PlanDateMin");
  56. pars.Add(new SugarParameter("@PlanDateMin", pdm));
  57. }
  58. else
  59. {
  60. innerWhere.Add("DATE(p.PlanDate) >= CURDATE()");
  61. }
  62. if (!string.IsNullOrWhiteSpace(input.PlanDateMax))
  63. {
  64. var pdx = input.PlanDateMax.Trim();
  65. if (pdx.Length > 10) pdx = pdx[..10];
  66. innerWhere.Add("DATE(p.PlanDate) <= @PlanDateMax");
  67. pars.Add(new SugarParameter("@PlanDateMax", pdx));
  68. }
  69. if (!string.IsNullOrWhiteSpace(input.Domain))
  70. {
  71. innerWhere.Add("p.`Domain` = @Domain");
  72. pars.Add(new SugarParameter("@Domain", input.Domain.Trim()));
  73. }
  74. if (!string.IsNullOrWhiteSpace(input.WorkOrds))
  75. {
  76. innerWhere.Add("(p.WorkOrds) LIKE @WorkOrds");
  77. pars.Add(new SugarParameter("@WorkOrds", $"%{input.WorkOrds.Trim()}%"));
  78. }
  79. if (!string.IsNullOrWhiteSpace(input.ItemNum))
  80. {
  81. innerWhere.Add("(p.ItemNum) LIKE @ItemNum");
  82. pars.Add(new SugarParameter("@ItemNum", $"%{input.ItemNum.Trim()}%"));
  83. }
  84. if (!string.IsNullOrWhiteSpace(input.Batch))
  85. {
  86. innerWhere.Add("(w.Batch) LIKE @Batch");
  87. pars.Add(new SugarParameter("@Batch", $"%{input.Batch.Trim()}%"));
  88. }
  89. if (!string.IsNullOrWhiteSpace(input.WoStatus))
  90. {
  91. innerWhere.Add("LOWER(IFNULL(w.Status,'')) = @WoStatus");
  92. pars.Add(new SugarParameter("@WoStatus", input.WoStatus.Trim().ToLowerInvariant()));
  93. }
  94. if (!string.IsNullOrWhiteSpace(input.IsLabor))
  95. {
  96. if (string.Equals(input.IsLabor, "yes", StringComparison.OrdinalIgnoreCase))
  97. innerWhere.Add("IFNULL(p.Status,'') <> ''");
  98. else if (string.Equals(input.IsLabor, "no", StringComparison.OrdinalIgnoreCase))
  99. innerWhere.Add("IFNULL(p.Status,'') = ''");
  100. }
  101. if (!string.IsNullOrWhiteSpace(input.WorkCtr))
  102. {
  103. innerWhere.Add("(sm.WorkCtr) = @WorkCtr");
  104. pars.Add(new SugarParameter("@WorkCtr", input.WorkCtr.Trim()));
  105. }
  106. if (!string.IsNullOrWhiteSpace(input.OccupyEquipmentType))
  107. {
  108. innerWhere.Add("(IFNULL(sm.OccupyInternalEquipmentCode,'') LIKE @Occ OR IFNULL(sm.MoldTypeCode,'') LIKE @Occ)");
  109. var occ = $"%{input.OccupyEquipmentType.Trim()}%";
  110. pars.Add(new SugarParameter("@Occ", occ));
  111. }
  112. if (!string.IsNullOrWhiteSpace(input.OpStdOp))
  113. {
  114. innerWhere.Add("(CONCAT(CAST(sm.Op AS CHAR),' ',IFNULL(sm.OpDescr,'')) LIKE @OpStdOp");
  115. pars.Add(new SugarParameter("@OpStdOp", $"%{input.OpStdOp.Trim()}%"));
  116. }
  117. if (!string.IsNullOrWhiteSpace(input.WorkStartFrom))
  118. {
  119. innerWhere.Add("sm.WorkStartTime >= @WorkStartFrom");
  120. pars.Add(new SugarParameter("@WorkStartFrom", input.WorkStartFrom.Trim()));
  121. }
  122. var baseSql = BuildListBaseSql(string.Join(" AND ", innerWhere));
  123. var orderClause = ResolveListOrder(input.OrderBy, input.Sort);
  124. var offset = (input.Page - 1) * input.PageSize;
  125. var total = await _db.Ado.GetIntAsync(
  126. $"SELECT COUNT(*) FROM ({baseSql}) AS t", pars);
  127. var list = await _db.Ado.SqlQueryAsync<ExecutableDailyPlanListRow>(
  128. $"SELECT * FROM ({baseSql}) AS t ORDER BY {orderClause} LIMIT {input.PageSize} OFFSET {offset}", pars);
  129. return new { total, page = input.Page, pageSize = input.PageSize, list };
  130. }
  131. /// <summary>下达 POST /api/Production/daily-plan/release</summary>
  132. [DisplayName("日计划下达")]
  133. [HttpPost("daily-plan/release")]
  134. public async Task<object> Release([FromBody] ExecutableDailyPlanReleaseInput input)
  135. {
  136. var domain = input.Domain.Trim();
  137. var ids = input.Ids.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)
  138. .Select(s => long.TryParse(s, out var id) ? id : (long?)null)
  139. .Where(x => x.HasValue)
  140. .Select(x => x!.Value)
  141. .ToArray();
  142. if (ids.Length == 0)
  143. throw Oops.Oh("请选择有效的行主键");
  144. var account = _userManager.Account ?? "system";
  145. var now = DateTime.Now;
  146. var tenantId = AidopTenantHelper.Resolve(App.HttpContext);
  147. if (tenantId <= 0)
  148. throw Oops.Oh("无法确定当前租户,日计划下达已拒绝"); // fail closed,绝不按默认租户写业务状态
  149. const string releasedFlag = "Y";
  150. var idList = string.Join(",", ids);
  151. // ── 1. 只挑【本次真正会发生状态跃迁】的计划行,并当场解析其销售订单身份 ──
  152. // 为什么在业务写路径里直读源表:事件要记录的就是"源系统刚刚发生了什么",
  153. // 让 release 事务反向依赖 mdp_std_so 才能完成会把方向搞反(Source → MDP → S8,
  154. // 不是 Source → MDP → 再决定能不能 release)。S8 的「只读中台」约束管的是消费侧。
  155. //
  156. // ID 桥(全程 tenant 内,禁止 SalesJob 文本归因):
  157. // PeriodSequenceDet.WorkOrds → WorkOrdMaster.WorkOrd
  158. // → WorkOrdMaster.BusinessID (= crm_seorderentry.Id)
  159. // → crm_seorderentry.seorder_id (= crm_seorder.Id)
  160. var transitions = await _db.Ado.SqlQueryAsync<PlanReleaseTransitionRow>(
  161. $"""
  162. SELECT p.RecID AS PlanRecId,
  163. p.WorkOrds AS WorkOrd,
  164. o.Id AS SalesOrderId,
  165. o.bill_no AS SalesOrderNo
  166. FROM PeriodSequenceDet p
  167. JOIN WorkOrdMaster w ON w.tenant_id = p.tenant_id AND w.WorkOrd = p.WorkOrds
  168. JOIN crm_seorderentry e ON e.tenant_id = p.tenant_id AND e.Id = w.BusinessID
  169. JOIN crm_seorder o ON o.tenant_id = p.tenant_id AND o.Id = e.seorder_id
  170. WHERE p.tenant_id = @TenantId
  171. AND p.`Domain` = @Domain
  172. AND p.RecID IN ({idList})
  173. AND IFNULL(p.`Status`, '') <> @Flag
  174. """,
  175. new List<SugarParameter>
  176. {
  177. new("@TenantId", tenantId),
  178. new("@Domain", domain),
  179. new("@Flag", releasedFlag)
  180. });
  181. // ── 2. 按销售订单去重:一次 release 内,同一订单最多一条里程碑事件 ──
  182. // 不是一条计划行一条事件 —— Stage KPI 的 Grain 是订单,不是工序行。
  183. var byOrder = (transitions ?? [])
  184. .GroupBy(t => t.SalesOrderId)
  185. .Select(g => new
  186. {
  187. SalesOrderId = g.Key,
  188. SalesOrderNo = g.First().SalesOrderNo,
  189. PlanRecIds = g.Select(x => x.PlanRecId).Distinct().ToList(),
  190. WorkOrders = g.Select(x => x.WorkOrd).Where(x => !string.IsNullOrEmpty(x)).Distinct().ToList()
  191. })
  192. .ToList();
  193. // ── 3. 无真实跃迁 → 不写任何里程碑 ──
  194. // 重复点击下达(目标行已是 'Y')在这里被拦住:普通重复 UPDATE 不是业务里程碑。
  195. if (byOrder.Count == 0)
  196. {
  197. var repeated = await _db.Ado.ExecuteCommandAsync(
  198. $"""
  199. UPDATE PeriodSequenceDet
  200. SET UpdateUser = @UpdateUser, UpdateTime = @UpdateTime
  201. WHERE RecID IN ({idList}) AND `Domain` = @Domain AND tenant_id = @TenantId
  202. AND IFNULL(`Status`, '') = @Flag
  203. """,
  204. new List<SugarParameter>
  205. {
  206. new("@UpdateUser", account),
  207. new("@UpdateTime", now),
  208. new("@Domain", domain),
  209. new("@TenantId", tenantId),
  210. new("@Flag", releasedFlag)
  211. });
  212. return new { message = repeated > 0 ? "所选计划已处于下达状态,未产生新的下达里程碑" : "没有可下达的计划行",
  213. affected = 0, milestoneCount = 0 };
  214. }
  215. // ── 4. 先开事件(RUNNING),再做业务写;成败与事务结果严格一致 ──
  216. var runLogIds = new Dictionary<long, long>();
  217. foreach (var ord in byOrder)
  218. runLogIds[ord.SalesOrderId] = await _runLog.StartAsync(
  219. ActionDailyPlanRelease, tenantId, "crm_seorder", ord.SalesOrderId, ord.SalesOrderNo);
  220. int n;
  221. try
  222. {
  223. // 只更新"本次确实要跃迁"的那些行,不把已下达行卷进来
  224. var transitionIds = string.Join(",", byOrder.SelectMany(o => o.PlanRecIds).Distinct());
  225. n = await _db.Ado.ExecuteCommandAsync(
  226. $"""
  227. UPDATE PeriodSequenceDet
  228. SET `Status` = @Flag,
  229. UpdateUser = @UpdateUser,
  230. UpdateTime = @UpdateTime
  231. WHERE RecID IN ({transitionIds})
  232. AND `Domain` = @Domain
  233. AND tenant_id = @TenantId
  234. AND IFNULL(`Status`, '') <> @Flag
  235. """,
  236. new List<SugarParameter>
  237. {
  238. new("@Flag", releasedFlag),
  239. new("@UpdateUser", account),
  240. new("@UpdateTime", now),
  241. new("@Domain", domain),
  242. new("@TenantId", tenantId)
  243. });
  244. }
  245. catch (Exception ex)
  246. {
  247. // 业务写失败 → 每张订单的事件都必须落 FAILED,绝不允许留下 SUCCESS 里程碑
  248. foreach (var ord in byOrder)
  249. await _runLog.FailedAsync(runLogIds[ord.SalesOrderId], ex.Message,
  250. new { salesOrderId = ord.SalesOrderId, salesOrderNo = ord.SalesOrderNo });
  251. throw;
  252. }
  253. // ── 5. 业务写成功后才 SUCCESS —— end_time 即 Stage-2 End Authority ──
  254. foreach (var ord in byOrder)
  255. await _runLog.SuccessAsync(runLogIds[ord.SalesOrderId],
  256. $"日计划下达:订单 {ord.SalesOrderNo},计划行 {ord.PlanRecIds.Count} 条,工单 {ord.WorkOrders.Count} 个",
  257. new
  258. {
  259. salesOrderId = ord.SalesOrderId,
  260. salesOrderNo = ord.SalesOrderNo,
  261. releasedPlanCount = ord.PlanRecIds.Count,
  262. planRecIds = ord.PlanRecIds,
  263. workOrderCount = ord.WorkOrders.Count
  264. });
  265. return new { message = "下达成功", affected = n, milestoneCount = byOrder.Count };
  266. }
  267. /// <summary>日计划行 → 销售订单的归因投影(仅用于 release 事件,走 ID 桥不走单号文本)。</summary>
  268. private sealed class PlanReleaseTransitionRow
  269. {
  270. public long PlanRecId { get; set; }
  271. public string? WorkOrd { get; set; }
  272. public long SalesOrderId { get; set; }
  273. public string? SalesOrderNo { get; set; }
  274. }
  275. /// <summary>工单状态 PATCH POST /api/Production/daily-plan/patch-wo-status</summary>
  276. [DisplayName("可执行日计划-工单状态")]
  277. [HttpPost("daily-plan/patch-wo-status")]
  278. public async Task<object> PatchWoStatus([FromBody] ExecutableDailyPlanWoStatusInput input)
  279. {
  280. var st = input.Status.Trim().ToLowerInvariant();
  281. if (st is not ("w" or "r" or "c" or "p"))
  282. throw Oops.Oh("状态不合法");
  283. var tenantId = AidopTenantHelper.Resolve(App.HttpContext);
  284. var n = await _db.Ado.ExecuteCommandAsync(
  285. $"""
  286. UPDATE WorkOrdMaster
  287. SET `Status` = @St,
  288. UpdateTime = @Now,
  289. UpdateUser = @User
  290. WHERE WorkOrd = @WorkOrd AND `Domain` = @Domain AND tenant_id = @TenantId
  291. """,
  292. new List<SugarParameter>
  293. {
  294. new("@St", st),
  295. new("@Now", DateTime.Now),
  296. new("@User", _userManager.Account ?? "system"),
  297. new("@WorkOrd", input.WorkOrd.Trim()),
  298. new("@Domain", input.Domain.Trim()),
  299. new("@TenantId", tenantId)
  300. });
  301. if (n == 0)
  302. throw Oops.Oh("工单不存在");
  303. return new { message = "状态已更新" };
  304. }
  305. private static string BuildListBaseSql(string innerWhere) => $"""
  306. SELECT
  307. GREATEST(IFNULL(p.CompQty, 0), IFNULL(act.comp_qty, 0)) AS CompQty,
  308. GREATEST(IFNULL(p.RejectQty, 0), IFNULL(act.reject_qty, 0)) AS RejectQty,
  309. sm.WorkStartTime AS WorkStartTime,
  310. sm.DeviceAllocationCount AS DeviceAllocationCount,
  311. p.RecID AS Id,
  312. sm.WorkEndTime AS WorkEndTime,
  313. pd.InternalEquipmentCode AS Line,
  314. line.line_describe AS LineDescribe,
  315. p.WorkOrds AS WorkOrds,
  316. p.ItemNum AS ItemNum,
  317. i.Descr AS Descr,
  318. i.PkgCode AS Languages,
  319. w.LotSerial AS LotSerial,
  320. CASE WHEN IFNULL(p.Status,'') = '' THEN '否' ELSE '是' END AS IsLabor,
  321. pd.Rate AS Rate,
  322. p.Op AS Op,
  323. p.PlanDate AS PlanDate,
  324. p.OrdQty AS OrdQty,
  325. pd.RunCrew AS RunCrew,
  326. LOWER(w.Status) AS Status,
  327. w.Batch AS Batch,
  328. p.Status AS PStatus,
  329. m.MaterialSituation AS MaterialSituation,
  330. CASE WHEN DATE_FORMAT(p.PlanDate,'%Y-%m-%d') = DATE_FORMAT(CURDATE(),'%Y-%m-%d') THEN 1 ELSE 0 END AS IsUpdateQty,
  331. CASE WHEN DATE_FORMAT(p.PlanDate,'%Y-%m-%d') > DATE_FORMAT(CURDATE(),'%Y-%m-%d') THEN 1 ELSE 0 END AS IsUpdateTime,
  332. sm.SetupTime AS SetupTime,
  333. sm.SetupTime * 60 AS SetupTimeInMinutes,
  334. pd.Site AS Site,
  335. CONCAT(IFNULL(sm.WorkCtr,''), ' ', IFNULL(wm.Descr,'')) AS Descr0,
  336. w.DueDate AS DueDate,
  337. pd.OpType AS OpType,
  338. CASE WHEN pd.OpType = 'M'
  339. THEN TIMESTAMPDIFF(MINUTE, sm.WorkStartTime, sm.WorkEndTime) / 60.0 * IFNULL(sm.DeviceAllocationCount, 0)
  340. ELSE 0 END AS DeviceWorkHours,
  341. CASE WHEN IFNULL(pd.OpType,'') <> 'M'
  342. THEN TIMESTAMPDIFF(MINUTE, sm.WorkStartTime, sm.WorkEndTime) / 60.0 * IFNULL(sm.DeviceAllocationCount, 0)
  343. ELSE TIMESTAMPDIFF(MINUTE, sm.WorkStartTime, sm.WorkEndTime) / 60.0 * IFNULL(sm.DeviceAllocationCount, 0) * IFNULL(pd.StandardStaffCount, 0) END AS ManWorkHours,
  344. CASE WHEN sm.WorkEndTime > w.DueDate THEN 'WorkOrds:#FF7F50'
  345. WHEN m.MaterialSituation = '仓库不齐套' AND LOWER(IFNULL(w.Status,'')) = 'r' THEN 'WorkOrds:#FCDD18'
  346. ELSE 'WorkOrds:#FFFFFF;' END AS Background,
  347. CONCAT(CAST(sm.Op AS CHAR), ' ', IFNULL(sm.OpDescr,'')) AS OpStdOp,
  348. IF(IFNULL(r.RunCrew, 0) > 0, r.RunCrew, pd.StandardStaffCount) AS StandardStaffCount,
  349. i.Drawing AS Drawing,
  350. w.QtyOrded AS QtyOrded,
  351. w.ReleaseDate AS ReleaseDate,
  352. (
  353. SELECT GROUP_CONCAT(DISTINCT el.MachineName ORDER BY el.MachineName SEPARATOR '/')
  354. FROM EquipmentList el
  355. WHERE pd.InternalEquipmentCode IS NOT NULL
  356. AND (
  357. el.InternalEquipmentCode = pd.InternalEquipmentCode
  358. OR FIND_IN_SET(el.InternalEquipmentCode, REPLACE(REPLACE(IFNULL(pd.InternalEquipmentCode,''), '+', ','), '/', ',')) > 0
  359. )
  360. ) AS MachineName,
  361. IF(w.Rev IS NULL OR w.Rev = '', i.Rev, w.Rev) AS Rev,
  362. w.Eff AS Eff,
  363. p.`Domain` AS Domain
  364. FROM PeriodSequenceDet p
  365. LEFT JOIN LineMaster line ON p.Line = line.Line AND (line.tenant_id = p.tenant_id OR p.tenant_id IS NULL)
  366. LEFT JOIN ItemMaster i ON p.ItemNum = i.ItemNum AND (p.tenant_id = i.tenant_id OR p.tenant_id IS NULL)
  367. LEFT JOIN WorkOrdMaster w ON p.WorkOrds = w.WorkOrd AND (w.tenant_id = p.tenant_id OR p.tenant_id IS NULL)
  368. LEFT JOIN WorkOrdRouting r ON p.Op = r.OP AND p.ItemNum = r.ItemNum AND p.WorkOrds = r.WorkOrd AND (r.tenant_id = p.tenant_id OR p.tenant_id IS NULL)
  369. LEFT JOIN mes_morder m ON w.WorkOrd = m.morder_no AND CAST(w.`Domain` AS CHAR(64)) = CAST(m.factory_id AS CHAR(64)) AND m.tenant_id = w.tenant_id
  370. LEFT JOIN ProdLineDetail pd ON p.ItemNum = pd.Part AND p.Line = pd.Line AND p.Op = pd.Op AND (pd.tenant_id = p.tenant_id OR p.tenant_id IS NULL)
  371. LEFT JOIN ScheduleResultOpMaster sm ON sm.PeriodDetRecId = p.RecID
  372. LEFT JOIN WorkCtrMaster wm ON wm.WorkCtr = sm.WorkCtr AND (wm.tenant_id = sm.tenant_id OR sm.tenant_id IS NULL)
  373. LEFT JOIN ado_psd_op_actual act
  374. ON act.tenant_id = IFNULL(p.tenant_id, 0)
  375. AND act.domain = IFNULL(p.`Domain`, '')
  376. AND act.work_ord = p.WorkOrds
  377. AND act.op = p.Op
  378. WHERE {innerWhere}
  379. """;
  380. private static string ResolveListOrder(string? orderBy, string? sort)
  381. {
  382. var desc = string.Equals(sort?.Trim(), "desc", StringComparison.OrdinalIgnoreCase);
  383. var dir = desc ? "DESC" : "ASC";
  384. var key = orderBy?.Trim().ToLowerInvariant();
  385. var col = key switch
  386. {
  387. "workords" => "t.WorkOrds",
  388. "itemnum" => "t.ItemNum",
  389. "plandate" => "t.PlanDate",
  390. "status" => "t.Status",
  391. "workstarttime" => "t.WorkStartTime",
  392. "lotserial" => "t.LotSerial",
  393. _ => "t.PlanDate"
  394. };
  395. // 默认(无排序参数时)按工单号、工序、计划日期升序
  396. if (string.IsNullOrWhiteSpace(key))
  397. return "t.WorkOrds ASC, t.Op ASC, t.PlanDate ASC";
  398. return $"{col} {dir}, t.Id DESC";
  399. }
  400. private sealed class ExecutableDailyPlanListRow
  401. {
  402. public decimal? CompQty { get; set; }
  403. public decimal? RejectQty { get; set; }
  404. public DateTime? WorkStartTime { get; set; }
  405. public int? DeviceAllocationCount { get; set; }
  406. public long Id { get; set; }
  407. public DateTime? WorkEndTime { get; set; }
  408. public string? Line { get; set; }
  409. public string? LineDescribe { get; set; }
  410. public string? WorkOrds { get; set; }
  411. public string? ItemNum { get; set; }
  412. public string? Descr { get; set; }
  413. public string? Languages { get; set; }
  414. public string? LotSerial { get; set; }
  415. public string? IsLabor { get; set; }
  416. public decimal? Rate { get; set; }
  417. public int? Op { get; set; }
  418. public DateTime? PlanDate { get; set; }
  419. public decimal? OrdQty { get; set; }
  420. /// <summary>ProdLineDetail.RunCrew 为班组/编码类字符串,勿用 decimal。</summary>
  421. public string? RunCrew { get; set; }
  422. public string? Status { get; set; }
  423. public string? Batch { get; set; }
  424. public string? PStatus { get; set; }
  425. public string? MaterialSituation { get; set; }
  426. public int IsUpdateQty { get; set; }
  427. public int IsUpdateTime { get; set; }
  428. public decimal? SetupTime { get; set; }
  429. public decimal? SetupTimeInMinutes { get; set; }
  430. public string? Site { get; set; }
  431. public string? Descr0 { get; set; }
  432. public DateTime? DueDate { get; set; }
  433. public string? OpType { get; set; }
  434. public decimal? DeviceWorkHours { get; set; }
  435. public decimal? ManWorkHours { get; set; }
  436. public string? Background { get; set; }
  437. public string? OpStdOp { get; set; }
  438. public decimal? StandardStaffCount { get; set; }
  439. public string? Drawing { get; set; }
  440. public decimal? QtyOrded { get; set; }
  441. public DateTime? ReleaseDate { get; set; }
  442. public string? MachineName { get; set; }
  443. public string? Rev { get; set; }
  444. public DateTime? Eff { get; set; }
  445. public string? Domain { get; set; }
  446. }
  447. }