WorkOrderSchedulingService.cs 41 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925
  1. namespace Admin.NET.Plugin.AiDOP.Production;
  2. /// <summary>
  3. /// 工单工序排产服务 🏭
  4. /// 路由前缀:/api/Production/scheduling/...
  5. /// </summary>
  6. [ApiDescriptionSettings(Order = 265, Description = "工单工序排产")]
  7. [Route("api/Production")]
  8. [AllowAnonymous]
  9. [NonUnify]
  10. public class WorkOrderSchedulingService : IDynamicApiController, ITransient
  11. {
  12. private readonly ISqlSugarClient _db;
  13. private readonly UserManager _userManager;
  14. public WorkOrderSchedulingService(ISqlSugarClient db, UserManager userManager)
  15. {
  16. _db = db;
  17. _userManager = userManager;
  18. }
  19. // ══════════════════════════════════════════════════════════════
  20. // 列表 GET /api/Production/scheduling/list
  21. // ══════════════════════════════════════════════════════════════
  22. /// <summary>工单工序排产分页列表(MySQL 口径,与 WorkOrdMaster 等表一致)</summary>
  23. [DisplayName("工单工序排产列表")]
  24. [HttpGet("scheduling/list")]
  25. public async Task<object> GetList([FromQuery] WorkOrderSchedulingListInput input)
  26. {
  27. const string C = "utf8mb4_general_ci";
  28. var pars = new List<SugarParameter>();
  29. var innerWhere = new List<string>
  30. {
  31. "IFNULL(a.Status,'') <> ''"
  32. };
  33. if (!string.IsNullOrWhiteSpace(input.WorkOrd))
  34. {
  35. innerWhere.Add($"(a.WorkOrd COLLATE {C}) LIKE @WorkOrd");
  36. pars.Add(new SugarParameter("@WorkOrd", $"%{input.WorkOrd.Trim()}%"));
  37. }
  38. if (!string.IsNullOrWhiteSpace(input.LotSerial))
  39. {
  40. innerWhere.Add($"(a.LotSerial COLLATE {C}) LIKE @LotSerial");
  41. pars.Add(new SugarParameter("@LotSerial", $"%{input.LotSerial.Trim()}%"));
  42. }
  43. if (!string.IsNullOrWhiteSpace(input.ItemNum))
  44. {
  45. innerWhere.Add($"(a.ItemNum COLLATE {C}) LIKE @ItemNum");
  46. pars.Add(new SugarParameter("@ItemNum", $"%{input.ItemNum.Trim()}%"));
  47. }
  48. if (!string.IsNullOrWhiteSpace(input.StartDateFrom))
  49. {
  50. innerWhere.Add("COALESCE(DATE(s.PlanDate), DATE(a.OrdDate)) >= @StartDateFrom");
  51. pars.Add(new SugarParameter("@StartDateFrom", input.StartDateFrom.Trim()));
  52. }
  53. if (!string.IsNullOrWhiteSpace(input.Status))
  54. {
  55. innerWhere.Add("LOWER(a.Status) = @Status");
  56. pars.Add(new SugarParameter("@Status", input.Status.Trim().ToLowerInvariant()));
  57. }
  58. var baseSql = BuildListBaseSql(string.Join(" AND ", innerWhere), C);
  59. var offset = (input.Page - 1) * input.PageSize;
  60. var total = await _db.Ado.GetIntAsync(
  61. $"SELECT COUNT(*) FROM ({baseSql}) AS t", pars);
  62. var list = await _db.Ado.SqlQueryAsync<WorkOrderSchedulingListRow>(
  63. $"SELECT * FROM ({baseSql}) AS t ORDER BY t.Id DESC LIMIT {input.PageSize} OFFSET {offset}",
  64. pars);
  65. return new { total, page = input.Page, pageSize = input.PageSize, list };
  66. }
  67. private static string BuildListBaseSql(string innerWhere, string C) => $"""
  68. SELECT
  69. a.RecID AS Id,
  70. a.Priority AS Priority,
  71. a.WorkOrd AS WorkOrd,
  72. a.LotSerial AS LotSerial,
  73. a.IssueSite AS IssueSite,
  74. a.ItemNum AS ItemNum,
  75. b.Descr AS Descr,
  76. b.Descr1 AS Descr1,
  77. a.QtyOrded AS QtyOrded,
  78. (IFNULL(a.LocationStock, 0) + IFNULL(a.OpQtyCompleted, 0)) AS LocationStock,
  79. a.QtyCompleted AS QtyCompleted,
  80. s.PlanDate AS PlanDate,
  81. s.ProdDate AS ProdDate,
  82. LOWER(a.Status) AS Status,
  83. a.`Domain` AS Domain,
  84. a.Urgent AS Urgent
  85. FROM WorkOrdMaster a
  86. LEFT JOIN ItemMaster b ON a.ItemNum COLLATE {C} = b.ItemNum COLLATE {C}
  87. LEFT JOIN ReplenishmentWeekPlan r
  88. ON a.WorkOrd COLLATE {C} = r.ProductionOrder COLLATE {C}
  89. AND CAST(a.`Domain` AS CHAR(64)) COLLATE {C} = CAST(r.factory_id AS CHAR(64)) COLLATE {C}
  90. LEFT JOIN crm_seorder se ON se.bill_no COLLATE {C} = a.SalesJob COLLATE {C}
  91. LEFT JOIN CustMaster cm ON cm.Cust COLLATE {C} = se.custom_no COLLATE {C}
  92. LEFT JOIN (
  93. SELECT `Domain`, WorkOrds, MIN(PlanDate) AS PlanDate, MIN(ProdDate) AS ProdDate
  94. FROM PeriodSequenceDet
  95. GROUP BY `Domain`, WorkOrds
  96. ) s ON CAST(a.`Domain` AS CHAR(64)) COLLATE {C} = CAST(s.`Domain` AS CHAR(64)) COLLATE {C}
  97. AND a.WorkOrd COLLATE {C} = s.WorkOrds COLLATE {C}
  98. LEFT JOIN (
  99. SELECT morder_no, MAX(create_time) AS checktime
  100. FROM b_examine_result
  101. GROUP BY morder_no
  102. ) exm ON exm.morder_no COLLATE {C} = a.WorkOrd COLLATE {C}
  103. LEFT JOIN WorkOrdInStorage ins
  104. ON a.WorkOrd COLLATE {C} = ins.WorkOrd COLLATE {C}
  105. AND ins.Remark COLLATE {C} = '工单预留' COLLATE {C}
  106. WHERE {innerWhere}
  107. """;
  108. // ══════════════════════════════════════════════════════════════
  109. // 编辑预览 GET /api/Production/scheduling/edit-preview
  110. // ══════════════════════════════════════════════════════════════
  111. [DisplayName("工单优先级编辑数据")]
  112. [HttpGet("scheduling/edit-preview")]
  113. public async Task<object> GetEditPreview([FromQuery] string workOrd, [FromQuery] string domain)
  114. {
  115. if (string.IsNullOrWhiteSpace(workOrd) || string.IsNullOrWhiteSpace(domain))
  116. throw Oops.Oh("工单号与公司域名不能为空");
  117. const string sql = """
  118. SELECT WorkOrd, ItemNum, QtyOrded, Priority, LotSerial, IFNULL(Urgent, 0) AS Urgent
  119. FROM WorkOrdMaster
  120. WHERE WorkOrd = @WorkOrd AND `Domain` = @Domain
  121. LIMIT 1
  122. """;
  123. var row = (await _db.Ado.SqlQueryAsync<WorkOrderEditPreviewRow>(sql, new { WorkOrd = workOrd.Trim(), Domain = domain.Trim() }))
  124. .FirstOrDefault() ?? throw Oops.Oh("工单不存在");
  125. return row;
  126. }
  127. // ══════════════════════════════════════════════════════════════
  128. // 查看 GET /api/Production/scheduling/view
  129. // ══════════════════════════════════════════════════════════════
  130. [DisplayName("工单查看(主表+工序+物料)")]
  131. [HttpGet("scheduling/view")]
  132. public async Task<object> GetView([FromQuery] string workOrd, [FromQuery] string domain)
  133. {
  134. if (string.IsNullOrWhiteSpace(workOrd) || string.IsNullOrWhiteSpace(domain))
  135. throw Oops.Oh("工单号与公司域名不能为空");
  136. const string masterSql = """
  137. SELECT
  138. a.WorkOrd,
  139. a.QtyOrded,
  140. a.QtyCompleted,
  141. a.ItemNum,
  142. IFNULL(a.ItemName, b.Descr) AS ItemName,
  143. b.Descr1 AS ItemModel,
  144. DATE(a.OrdDate) AS OrdDate,
  145. DATE(a.DueDate) AS DueDate,
  146. LOWER(a.Status) AS Status,
  147. IFNULL(a.Remark, '') AS Remark
  148. FROM WorkOrdMaster a
  149. LEFT JOIN ItemMaster b ON a.ItemNum = b.ItemNum
  150. WHERE a.WorkOrd = @WorkOrd AND a.`Domain` = @Domain
  151. LIMIT 1
  152. """;
  153. var master = (await _db.Ado.SqlQueryAsync<WorkOrderViewMasterRow>(masterSql, new { WorkOrd = workOrd.Trim(), Domain = domain.Trim() }))
  154. .FirstOrDefault() ?? throw Oops.Oh("工单不存在");
  155. const string routingSql = """
  156. SELECT
  157. r.OP AS Op,
  158. r.Descr AS Descr,
  159. IFNULL(r.ParentOp, '') AS ParentOp,
  160. IFNULL(r.MilestoneOp, 0) AS MilestoneOp,
  161. IFNULL(r.PackingQty, 0) AS PackingQty,
  162. IFNULL(r.QtyComplete, 0) AS QtyComplete,
  163. IFNULL(r.QtyReject, 0) AS QtyReject,
  164. IFNULL(r.QtyScrap, 0) AS QtyScrap,
  165. r.Status AS Status
  166. FROM WorkOrdRouting r
  167. WHERE r.WorkOrd = @WorkOrd AND r.`Domain` = @Domain
  168. ORDER BY r.Line, r.ColumnNum
  169. """;
  170. var routings = await _db.Ado.SqlQueryAsync<WorkOrderViewRoutingRow>(routingSql, new { WorkOrd = workOrd.Trim(), Domain = domain.Trim() });
  171. const string detailSql = """
  172. SELECT ItemNum, Op, QtyRequired, IFNULL(FrozenBOMQty, 0) AS FrozenBOMQty
  173. FROM WorkOrdDetail
  174. WHERE WorkOrd = @WorkOrd AND `Domain` = @Domain
  175. ORDER BY LineNum
  176. """;
  177. var details = await _db.Ado.SqlQueryAsync<WorkOrderViewDetailRow>(detailSql, new { WorkOrd = workOrd.Trim(), Domain = domain.Trim() });
  178. return new { master, routings, details };
  179. }
  180. // ══════════════════════════════════════════════════════════════
  181. // 物料 / 工序明细列表(新标签页)
  182. // ══════════════════════════════════════════════════════════════
  183. [DisplayName("工单物料明细列表")]
  184. [HttpGet("scheduling/materials")]
  185. public async Task<object> GetMaterials([FromQuery] string workOrd, [FromQuery] string domain)
  186. {
  187. if (string.IsNullOrWhiteSpace(workOrd) || string.IsNullOrWhiteSpace(domain))
  188. throw Oops.Oh("工单号与公司域名不能为空");
  189. const string sql = """
  190. SELECT ItemNum, Op, QtyRequired, IFNULL(FrozenBOMQty, 0) AS FrozenBOMQty
  191. FROM WorkOrdDetail
  192. WHERE WorkOrd = @WorkOrd AND `Domain` = @Domain
  193. ORDER BY LineNum
  194. """;
  195. var list = await _db.Ado.SqlQueryAsync<WorkOrderViewDetailRow>(sql, new { WorkOrd = workOrd.Trim(), Domain = domain.Trim() });
  196. return new { list };
  197. }
  198. [DisplayName("工单工序明细列表")]
  199. [HttpGet("scheduling/routings")]
  200. public async Task<object> GetRoutings([FromQuery] string workOrd, [FromQuery] string domain)
  201. {
  202. if (string.IsNullOrWhiteSpace(workOrd) || string.IsNullOrWhiteSpace(domain))
  203. throw Oops.Oh("工单号与公司域名不能为空");
  204. const string sql = """
  205. SELECT
  206. r.OP AS Op,
  207. r.Descr AS Descr,
  208. IFNULL(r.ParentOp, '') AS ParentOp,
  209. IFNULL(r.MilestoneOp, 0) AS MilestoneOp,
  210. IFNULL(r.PackingQty, 0) AS PackingQty,
  211. IFNULL(r.QtyComplete, 0) AS QtyComplete,
  212. IFNULL(r.QtyReject, 0) AS QtyReject,
  213. IFNULL(r.QtyScrap, 0) AS QtyScrap,
  214. r.Status AS Status
  215. FROM WorkOrdRouting r
  216. WHERE r.WorkOrd = @WorkOrd AND r.`Domain` = @Domain
  217. ORDER BY r.Line, r.ColumnNum
  218. """;
  219. var list = await _db.Ado.SqlQueryAsync<WorkOrderViewRoutingRow>(sql, new { WorkOrd = workOrd.Trim(), Domain = domain.Trim() });
  220. return new { list };
  221. }
  222. // ══════════════════════════════════════════════════════════════
  223. // 执行追踪 GET /api/Production/scheduling/trace
  224. // ══════════════════════════════════════════════════════════════
  225. [DisplayName("工单执行追踪")]
  226. [HttpGet("scheduling/trace")]
  227. public async Task<object> GetTrace([FromQuery] string workOrd, [FromQuery] string domain)
  228. {
  229. if (string.IsNullOrWhiteSpace(workOrd) || string.IsNullOrWhiteSpace(domain))
  230. throw Oops.Oh("工单号与公司域名不能为空");
  231. var w = workOrd.Trim();
  232. var d = domain.Trim();
  233. const string masterSql = """
  234. SELECT
  235. a.WorkOrd,
  236. a.QtyOrded,
  237. a.QtyCompleted,
  238. a.ItemNum,
  239. IFNULL(b.Descr, '') AS ItemName,
  240. IFNULL(b.Descr1, '') AS ItemModel,
  241. DATE(a.OrdDate) AS OrdDate,
  242. DATE(a.DueDate) AS DueDate,
  243. LOWER(a.Status) AS Status
  244. FROM WorkOrdMaster a
  245. LEFT JOIN ItemMaster b ON a.ItemNum = b.ItemNum
  246. WHERE a.WorkOrd = @WorkOrd AND a.`Domain` = @Domain
  247. LIMIT 1
  248. """;
  249. var master = (await _db.Ado.SqlQueryAsync<WorkOrderTraceMasterRow>(masterSql, new { WorkOrd = w, Domain = d }))
  250. .FirstOrDefault() ?? throw Oops.Oh("工单不存在");
  251. var tab1 = await QueryTraceResourceCheckAsync(w, d);
  252. var tab2 = await QueryTraceScheduleAsync(w, d);
  253. var tab3Po = await QueryTracePoAsync(w, d);
  254. var tab3Pr = await QueryTracePrAsync(w, d);
  255. var tab4 = await QueryTraceNbrAsync(w, d);
  256. var tab5 = await QueryTraceProdReportAsync(w, d);
  257. var tab6 = await QueryTraceQualityAsync(w, d);
  258. var tab7 = await QueryTraceReceiptAsync(w, d);
  259. return new
  260. {
  261. master,
  262. resourceCheck = tab1,
  263. schedule = tab2,
  264. purchaseOrders = tab3Po,
  265. purchaseReqs = tab3Pr,
  266. picking = tab4,
  267. production = tab5,
  268. quality = tab6,
  269. receipt = tab7
  270. };
  271. }
  272. private async Task<List<TraceResourceRow>> QueryTraceResourceCheckAsync(string workOrd, string domain)
  273. {
  274. const string sql = """
  275. SELECT
  276. ROW_NUMBER() OVER (ORDER BY bce.id) AS Sno,
  277. CAST(bce.num AS CHAR) AS Num,
  278. bce.item_number AS ItemNumber,
  279. bce.item_name AS ItemName,
  280. bce.model AS Model,
  281. IFNULL(bce.unit, '') AS Unit,
  282. IFNULL(bce.bom_number, '') AS BomNumber,
  283. DATE_FORMAT(bce.kitting_time, '%Y-%m-%d') AS KittingTime,
  284. CASE bce.erp_cls
  285. WHEN 0 THEN '配置类'
  286. WHEN 1 THEN '自制'
  287. WHEN 2 THEN '委外加工'
  288. WHEN 3 THEN '外购'
  289. WHEN 4 THEN '虚拟件'
  290. ELSE ''
  291. END AS ErpClsName,
  292. CASE WHEN bce.backflush = 1 THEN '是' ELSE '否' END AS Backflush,
  293. CAST(bce.qty AS CHAR) AS Qty,
  294. CAST(bce.needCount AS CHAR) AS NeedCount
  295. FROM b_examine_result ber
  296. INNER JOIN b_bom_child_examine bce ON ber.Id = bce.examine_id AND bce.is_use = 1
  297. WHERE ber.morder_no = @WorkOrd
  298. AND ber.Id = (
  299. SELECT br.Id FROM b_examine_result br
  300. WHERE br.morder_no = @WorkOrd
  301. ORDER BY br.create_time DESC
  302. LIMIT 1
  303. )
  304. ORDER BY bce.id
  305. """;
  306. return await _db.Ado.SqlQueryAsync<TraceResourceRow>(sql, new { WorkOrd = workOrd });
  307. }
  308. private async Task<List<TraceScheduleRow>> QueryTraceScheduleAsync(string workOrd, string domain)
  309. {
  310. const string sql = """
  311. SELECT
  312. ROW_NUMBER() OVER (ORDER BY sch.PlanDate) AS Sno,
  313. sch.WorkOrds AS WorkOrds,
  314. DATE_FORMAT(sch.PlanDate, '%Y-%m-%d') AS WorkDate,
  315. IFNULL(sch.Line, '') AS Line,
  316. IFNULL(sch.Op, '') AS Op,
  317. CAST(IFNULL(sch.OrdQty, 0) AS CHAR) AS WorkQty,
  318. IFNULL(sch.ItemNum, '') AS ItemNum,
  319. IFNULL(im.Descr, '') AS Descr,
  320. IFNULL(im.Descr1, '') AS Descr1,
  321. DATE_FORMAT(sch.CreateTime, '%Y-%m-%d %H:%i:%s') AS CreateTime
  322. FROM PeriodSequenceDet sch
  323. LEFT JOIN ItemMaster im ON sch.ItemNum = im.ItemNum
  324. WHERE sch.WorkOrds = @WorkOrd
  325. AND CAST(sch.`Domain` AS CHAR(64)) = CAST(@Domain AS CHAR(64))
  326. ORDER BY sch.PlanDate
  327. """;
  328. return await _db.Ado.SqlQueryAsync<TraceScheduleRow>(sql, new { WorkOrd = workOrd, Domain = domain });
  329. }
  330. private async Task<List<TracePoRow>> QueryTracePoAsync(string workOrd, string domain)
  331. {
  332. const string sql = """
  333. SELECT
  334. ROW_NUMBER() OVER (ORDER BY t.po_billno) AS Sno,
  335. t.po_billno AS PoBillno,
  336. t.supplier_no AS SupplierNo,
  337. t.supplier_name AS SupplierName,
  338. CAST(t.po_total AS CHAR) AS PoTotal,
  339. IFNULL(t.po_purchaser, '') AS PoPurchaser,
  340. t.state AS State,
  341. IFNULL(DATE_FORMAT(t.po_ssend_date, '%Y-%m-%d'), '') AS PoSsendDate,
  342. IFNULL(DATE_FORMAT(t.create_time, '%Y-%m-%d %H:%i:%s'), '') AS CreateTime
  343. FROM (
  344. SELECT DISTINCT
  345. pm.po_billno,
  346. pm.supplier_no,
  347. pm.supplier_name,
  348. pm.po_total,
  349. pm.po_purchaser,
  350. pm.create_time,
  351. pm.po_ssend_date,
  352. CASE pm.state
  353. WHEN 0 THEN '新增'
  354. WHEN 1 THEN '审核中'
  355. WHEN 2 THEN '同意'
  356. WHEN 3 THEN '关闭'
  357. ELSE CAST(pm.state AS CHAR)
  358. END AS state
  359. FROM srm_po_main pm
  360. LEFT JOIN srm_po_list pl ON pm.id = pl.po_id
  361. LEFT JOIN srm_po_occupy occ ON pl.Id = occ.polist_id
  362. AND CAST(pm.factory_id AS CHAR(64)) = CAST(occ.factory_id AS CHAR(64))
  363. LEFT JOIN mes_morder mo ON mo.id = occ.morder_id
  364. WHERE mo.morder_no = @WorkOrd
  365. AND CAST(mo.factory_id AS CHAR(64)) = CAST(@Domain AS CHAR(64))
  366. AND pm.IsDeleted = 0
  367. ) AS t
  368. ORDER BY t.po_billno
  369. """;
  370. return await _db.Ado.SqlQueryAsync<TracePoRow>(sql, new { WorkOrd = workOrd, Domain = domain });
  371. }
  372. private async Task<List<TracePrRow>> QueryTracePrAsync(string workOrd, string domain)
  373. {
  374. const string sql = """
  375. SELECT
  376. ROW_NUMBER() OVER (ORDER BY pm.pr_billno) AS Sno,
  377. pm.pr_billno AS PrBillno,
  378. IFNULL(ic.number, '') AS Number,
  379. IFNULL(pm.icitem_name, '') AS IcitemName,
  380. IFNULL(ic.model, '') AS Model,
  381. IFNULL(pm.pr_purchasenumber, '') AS PrPurchasenumber,
  382. IFNULL(pm.pr_purchasename, '') AS PrPurchasename,
  383. CAST(IFNULL(pm.pr_aqty, 0) AS CHAR) AS PrAqty,
  384. IFNULL(DATE_FORMAT(pm.pr_ssend_date, '%Y-%m-%d'), '') AS PrSsendDate,
  385. IFNULL(DATE_FORMAT(pm.pr_sarrive_date, '%Y-%m-%d'), '') AS PrSarriveDate,
  386. IFNULL(pm.pr_unit, '') AS PrUnit,
  387. IFNULL(pm.pr_purchaser, '') AS PrPurchaser
  388. FROM srm_pr_main pm
  389. LEFT JOIN srm_po_occupy occ ON pm.Id = occ.polist_id
  390. AND CAST(pm.factory_id AS CHAR(64)) = CAST(occ.factory_id AS CHAR(64))
  391. LEFT JOIN ic_item ic ON pm.icitem_id = ic.Id
  392. LEFT JOIN mes_morder mo ON mo.id = occ.morder_id
  393. WHERE mo.morder_no = @WorkOrd
  394. AND CAST(mo.factory_id AS CHAR(64)) = CAST(@Domain AS CHAR(64))
  395. AND IFNULL(pm.state, 0) <> 0
  396. AND pm.IsDeleted = 0
  397. ORDER BY pm.pr_billno
  398. """;
  399. return await _db.Ado.SqlQueryAsync<TracePrRow>(sql, new { WorkOrd = workOrd, Domain = domain });
  400. }
  401. private async Task<List<TraceNbrRow>> QueryTraceNbrAsync(string workOrd, string domain)
  402. {
  403. const string sql = """
  404. SELECT
  405. CAST(nd.Line AS CHAR) AS Line,
  406. nm.Nbr AS Nbr,
  407. nd.ItemNum AS ItemNum,
  408. IFNULL(im.Descr, '') AS Descr,
  409. IFNULL(im.Descr1, '') AS Descr1,
  410. IFNULL(nd.LocationFrom, '') AS LocationFrom,
  411. IFNULL(nd.LocationTo, '') AS LocationTo,
  412. CAST(IFNULL(nd.CurrQtyOpened, 0) AS CHAR) AS CurrQtyOpened,
  413. CAST(IFNULL(nd.QtyOrd, 0) AS CHAR) AS QtyOrd,
  414. CAST(IFNULL(nd.QtyFrom, 0) AS CHAR) AS QtyFrom,
  415. CAST(IFNULL(nd.QtyRec, 0) AS CHAR) AS QtyRec,
  416. IFNULL(nd.UM, '') AS Unit
  417. FROM NbrDetail nd
  418. LEFT JOIN NbrMaster nm ON nd.nbr = nm.nbr
  419. LEFT JOIN ItemMaster im ON nd.ItemNum = im.ItemNum
  420. WHERE nm.WorkOrd = @WorkOrd
  421. AND nm.Type = 'SM'
  422. AND CAST(nm.`Domain` AS CHAR(64)) = CAST(@Domain AS CHAR(64))
  423. ORDER BY nm.Nbr
  424. """;
  425. return await _db.Ado.SqlQueryAsync<TraceNbrRow>(sql, new { WorkOrd = workOrd, Domain = domain });
  426. }
  427. private async Task<List<TraceProdRow>> QueryTraceProdReportAsync(string workOrd, string domain)
  428. {
  429. const string sql = """
  430. SELECT
  431. ROW_NUMBER() OVER (ORDER BY ote.ProdDate DESC) AS Sno,
  432. ote.WorkOrd AS WorkOrd,
  433. IFNULL(ote.ItemNum, '') AS ItemNum,
  434. IFNULL(im.Descr, '') AS Descr,
  435. IFNULL(im.Descr1, '') AS Descr1,
  436. IFNULL(im.Um, '') AS Um,
  437. CAST(IFNULL(ote.QtyCompleted, 0) AS CHAR) AS QtyCompleted,
  438. CAST(IFNULL(ote.QtyReject, 0) AS CHAR) AS QtyReject,
  439. CAST(IFNULL(ote.QtyScrapped, 0) AS CHAR) AS QtyScrapped,
  440. IFNULL(DATE_FORMAT(ote.ProdDate, '%Y-%m-%d %H:%i:%s'), '') AS ProdDate,
  441. IFNULL(ote.Op, '') AS Op,
  442. IFNULL(ote.Name, '') AS Name
  443. FROM OpTransEmployee ote
  444. LEFT JOIN ItemMaster im ON ote.ItemNum = im.ItemNum
  445. WHERE ote.WorkOrd = @WorkOrd
  446. ORDER BY ote.ProdDate DESC
  447. """;
  448. return await _db.Ado.SqlQueryAsync<TraceProdRow>(sql, new { WorkOrd = workOrd });
  449. }
  450. private async Task<List<TraceQualityRow>> QueryTraceQualityAsync(string workOrd, string domain)
  451. {
  452. const string sql = """
  453. SELECT
  454. ROW_NUMBER() OVER (ORDER BY mo.id) AS Sno,
  455. IFNULL(im.Um, '') AS Unit,
  456. IFNULL(mo.morder_no, '') AS MorderNo,
  457. CAST('' AS CHAR) AS WorkNumber,
  458. CAST('' AS CHAR) AS MorderProductionNumber,
  459. CAST('' AS CHAR) AS InspectionNumber,
  460. CAST('' AS CHAR) AS QualifiedNumber,
  461. CAST('' AS CHAR) AS InventoryNumber,
  462. IFNULL(mo.moentry_wrkcname, '') AS MoentryWrkcname,
  463. CAST('' AS CHAR) AS PlannerStartDate,
  464. CAST('' AS CHAR) AS PlannerEndDate,
  465. IFNULL(mo.moentry_prdname, '') AS MoentryPrdname,
  466. CAST('' AS CHAR) AS MorderFstate
  467. FROM mes_morder mo
  468. LEFT JOIN ItemMaster im ON mo.product_code = im.ItemNum
  469. WHERE mo.morder_no = @WorkOrd
  470. AND CAST(mo.factory_id AS CHAR(64)) = CAST(@Domain AS CHAR(64))
  471. AND mo.IsDeleted = 0
  472. ORDER BY mo.id
  473. """;
  474. return await _db.Ado.SqlQueryAsync<TraceQualityRow>(sql, new { WorkOrd = workOrd, Domain = domain });
  475. }
  476. private async Task<List<TraceReceiptRow>> QueryTraceReceiptAsync(string workOrd, string domain)
  477. {
  478. const string sql = """
  479. SELECT
  480. ROW_NUMBER() OVER (ORDER BY c.WorkOrd) AS Sno,
  481. CASE
  482. WHEN LOWER(c.Status) = 'r' THEN '下达'
  483. WHEN LOWER(c.Status) = 'c' THEN '关闭'
  484. WHEN LOWER(c.Status) = 'w' THEN '投产'
  485. ELSE IFNULL(c.Status, '')
  486. END AS Status,
  487. c.WorkOrd AS WorkOrd,
  488. IFNULL(im.Um, '') AS UM,
  489. CAST(IFNULL(c.QtyOrded, 0) AS CHAR) AS QtyOrded,
  490. CAST(IFNULL(compp.CompQty, 0) AS CHAR) AS CompQty,
  491. CAST('' AS CHAR) AS InspectionNumber,
  492. CAST('' AS CHAR) AS QualifiedNumber,
  493. CAST(IFNULL(a.QtyChange, 0) AS CHAR) AS QtyChangeAdvance,
  494. IFNULL(DATE_FORMAT(c.OrdDate, '%Y-%m-%d'), '') AS OrdDate,
  495. IFNULL(DATE_FORMAT(c.DueDate, '%Y-%m-%d'), '') AS DueDate
  496. FROM WorkOrdMaster c
  497. LEFT JOIN InvTransHist a ON a.WorkOrd = c.WorkOrd AND a.TransType = 'rct-wo' AND IFNULL(a.QtyChange, 0) > 0
  498. LEFT JOIN ItemMaster im ON c.ItemNum = im.ItemNum AND im.`Domain` = c.`Domain`
  499. LEFT JOIN (
  500. SELECT WorkOrds, SUM(IFNULL(CompQty, 0)) AS CompQty
  501. FROM PeriodSequenceDet
  502. GROUP BY WorkOrds
  503. ) compp ON c.WorkOrd = compp.WorkOrds
  504. LEFT JOIN mes_morder mo ON mo.morder_no = c.WorkOrd AND mo.IsDeleted = 0
  505. AND CAST(mo.factory_id AS CHAR(64)) = CAST(c.`Domain` AS CHAR(64))
  506. WHERE c.WorkOrd = @WorkOrd AND c.`Domain` = @Domain
  507. """;
  508. return await _db.Ado.SqlQueryAsync<TraceReceiptRow>(sql, new { WorkOrd = workOrd, Domain = domain });
  509. }
  510. // ══════════════════════════════════════════════════════════════
  511. // 保存 POST /api/Production/scheduling/save
  512. // ══════════════════════════════════════════════════════════════
  513. [DisplayName("保存工单(数量/批次/优先级/加急)")]
  514. [HttpPost("scheduling/save")]
  515. public async Task<object> Save([FromBody] WorkOrderSchedulingSaveInput input)
  516. {
  517. var account = _userManager.Account ?? "system";
  518. var pars = new List<SugarParameter>
  519. {
  520. new("@QtyOrded", input.QtyOrded ?? (object)DBNull.Value),
  521. new("@Priority", input.Priority ?? (object)DBNull.Value),
  522. new("@LotSerial", string.IsNullOrWhiteSpace(input.LotSerial) ? DBNull.Value : input.LotSerial.Trim()),
  523. new("@Urgent", input.Urgent ?? (object)DBNull.Value),
  524. new("@UpdateUser", account),
  525. new("@UpdateTime", DateTime.Now),
  526. new("@WorkOrd", input.WorkOrd.Trim()),
  527. new("@Domain", input.Domain.Trim())
  528. };
  529. var n = await _db.Ado.ExecuteCommandAsync(
  530. """
  531. UPDATE WorkOrdMaster
  532. SET QtyOrded = COALESCE(@QtyOrded, QtyOrded),
  533. Priority = COALESCE(@Priority, Priority),
  534. LotSerial = COALESCE(@LotSerial, LotSerial),
  535. Urgent = COALESCE(@Urgent, Urgent),
  536. UpdateUser = @UpdateUser,
  537. UpdateTime = @UpdateTime
  538. WHERE WorkOrd = @WorkOrd AND `Domain` = @Domain
  539. """,
  540. pars);
  541. if (n == 0)
  542. throw Oops.Oh("工单不存在或未更新");
  543. return new { message = "保存成功" };
  544. }
  545. // ══════════════════════════════════════════════════════════════
  546. // 状态 PATCH POST /api/Production/scheduling/status
  547. // ══════════════════════════════════════════════════════════════
  548. [DisplayName("更新工单状态")]
  549. [HttpPost("scheduling/status")]
  550. public async Task<object> PatchStatus([FromBody] WorkOrderStatusPatchInput input)
  551. {
  552. var account = _userManager.Account ?? "system";
  553. var status = input.Status.Trim().ToLowerInvariant();
  554. var pars = new List<SugarParameter>
  555. {
  556. new("@Status", status),
  557. new("@UpdateUser", account),
  558. new("@UpdateTime", DateTime.Now),
  559. new("@Id", input.Id),
  560. new("@Domain", input.Domain.Trim())
  561. };
  562. var n = await _db.Ado.ExecuteCommandAsync(
  563. """
  564. UPDATE WorkOrdMaster
  565. SET Status = @Status,
  566. UpdateUser = @UpdateUser,
  567. UpdateTime = @UpdateTime
  568. WHERE RecID = @Id AND `Domain` = @Domain
  569. """,
  570. pars);
  571. if (n == 0)
  572. throw Oops.Oh("工单不存在或未更新");
  573. return new { message = "状态已更新" };
  574. }
  575. // ══════════════════════════════════════════════════════════════
  576. // 工单关闭 POST /api/Production/scheduling/close
  577. // ══════════════════════════════════════════════════════════════
  578. [DisplayName("工单关闭(存储过程)")]
  579. [HttpPost("scheduling/close")]
  580. public async Task<object> Close([FromBody] WorkOrderCloseInput input)
  581. {
  582. await _db.Ado.ExecuteCommandAsync(
  583. "CALL pr_MES_CloseWorkOrders(@Ids)",
  584. new SugarParameter("@Ids", input.Ids.Trim()));
  585. return new { message = "已提交关闭" };
  586. }
  587. // ══════════════════════════════════════════════════════════════
  588. // 同步工艺路线 POST /api/Production/scheduling/sync-routing
  589. // ══════════════════════════════════════════════════════════════
  590. [DisplayName("同步工艺路线")]
  591. [HttpPost("scheduling/sync-routing")]
  592. public async Task<object> SyncRouting([FromBody] WorkOrderKeyInput input)
  593. {
  594. var workOrd = input.WorkOrd.Trim();
  595. var domain = input.Domain.Trim();
  596. var pars = new List<SugarParameter>
  597. {
  598. new("@WorkOrd", workOrd),
  599. new("@Domain", domain)
  600. };
  601. await _db.Ado.BeginTranAsync();
  602. try
  603. {
  604. await _db.Ado.ExecuteCommandAsync(
  605. """
  606. DELETE FROM WorkOrdRouting
  607. WHERE WorkOrd = @WorkOrd AND CAST(`Domain` AS CHAR(64)) COLLATE utf8mb4_general_ci = CAST(@Domain AS CHAR(64)) COLLATE utf8mb4_general_ci
  608. """,
  609. pars);
  610. await _db.Ado.ExecuteCommandAsync(
  611. """
  612. INSERT INTO WorkOrdRouting (
  613. `Domain`, Descr, MilestoneOp, WorkOrd, OP, ParentOp, RunTime, ItemNum, QtyOrded, OverlapUnits, Status, IsActive, CommentIndex, CreateTime, StdOp, PackingQty, WorkOrdMasterRecID
  614. )
  615. SELECT
  616. w.`Domain`,
  617. r.Descr,
  618. r.MilestoneOp,
  619. w.WorkOrd,
  620. r.OP,
  621. r.ParentOp,
  622. r.RunTime,
  623. w.ItemNum,
  624. w.QtyOrded,
  625. r.OverlapUnits,
  626. w.Status,
  627. 1,
  628. r.CommentIndex,
  629. NOW(),
  630. r.StdOp,
  631. r.PackingQty,
  632. w.RecID
  633. FROM WorkOrdMaster w
  634. LEFT JOIN RoutingOpDetail r ON w.ItemNum COLLATE utf8mb4_general_ci = r.RoutingCode COLLATE utf8mb4_general_ci
  635. WHERE w.WorkOrd = @WorkOrd
  636. AND CAST(w.`Domain` AS CHAR(64)) COLLATE utf8mb4_general_ci = CAST(@Domain AS CHAR(64)) COLLATE utf8mb4_general_ci
  637. AND r.MilestoneOp IS NOT NULL
  638. """,
  639. pars);
  640. await _db.Ado.CommitTranAsync();
  641. }
  642. catch (Exception ex)
  643. {
  644. await _db.Ado.RollbackTranAsync();
  645. throw Oops.Oh($"同步工艺路线失败:{ex.Message}");
  646. }
  647. return new { message = "同步工艺路线已执行" };
  648. }
  649. // ══════════════════════════════════════════════════════════════
  650. // 加急 POST /api/Production/scheduling/urgent
  651. // ══════════════════════════════════════════════════════════════
  652. [DisplayName("设置加急")]
  653. [HttpPost("scheduling/urgent")]
  654. public async Task<object> SetUrgent([FromBody] WorkOrderUrgentInput input)
  655. {
  656. var account = _userManager.Account ?? "system";
  657. var n = await _db.Ado.ExecuteCommandAsync(
  658. """
  659. UPDATE WorkOrdMaster
  660. SET Urgent = @Urgent,
  661. UpdateUser = @UpdateUser,
  662. UpdateTime = @UpdateTime
  663. WHERE WorkOrd = @WorkOrd AND `Domain` = @Domain
  664. """,
  665. new List<SugarParameter>
  666. {
  667. new("@Urgent", input.Urgent),
  668. new("@UpdateUser", account),
  669. new("@UpdateTime", DateTime.Now),
  670. new("@WorkOrd", input.WorkOrd.Trim()),
  671. new("@Domain", input.Domain.Trim())
  672. });
  673. if (n == 0)
  674. throw Oops.Oh("工单不存在");
  675. return new { message = input.Urgent == 2 ? "已设为特急" : "已设为加急" };
  676. }
  677. // ──────────────── 行类型 ────────────────
  678. private sealed class WorkOrderSchedulingListRow
  679. {
  680. public int Id { get; set; }
  681. public string? Priority { get; set; }
  682. public string? WorkOrd { get; set; }
  683. public string? LotSerial { get; set; }
  684. public string? IssueSite { get; set; }
  685. public string? ItemNum { get; set; }
  686. public string? Descr { get; set; }
  687. public string? Descr1 { get; set; }
  688. public decimal? QtyOrded { get; set; }
  689. public decimal? LocationStock { get; set; }
  690. public decimal? QtyCompleted { get; set; }
  691. public DateTime? PlanDate { get; set; }
  692. public DateTime? ProdDate { get; set; }
  693. public string? Status { get; set; }
  694. public string? Domain { get; set; }
  695. public int? Urgent { get; set; }
  696. }
  697. private sealed class WorkOrderEditPreviewRow
  698. {
  699. public string? WorkOrd { get; set; }
  700. public string? ItemNum { get; set; }
  701. public decimal? QtyOrded { get; set; }
  702. public string? Priority { get; set; }
  703. public string? LotSerial { get; set; }
  704. public int Urgent { get; set; }
  705. }
  706. private sealed class WorkOrderViewMasterRow
  707. {
  708. public string? WorkOrd { get; set; }
  709. public decimal? QtyOrded { get; set; }
  710. public decimal? QtyCompleted { get; set; }
  711. public string? ItemNum { get; set; }
  712. public string? ItemName { get; set; }
  713. public string? ItemModel { get; set; }
  714. public DateTime? OrdDate { get; set; }
  715. public DateTime? DueDate { get; set; }
  716. public string? Status { get; set; }
  717. public string? Remark { get; set; }
  718. }
  719. private sealed class WorkOrderViewRoutingRow
  720. {
  721. public string? Op { get; set; }
  722. public string? Descr { get; set; }
  723. public string? ParentOp { get; set; }
  724. public int MilestoneOp { get; set; }
  725. public decimal PackingQty { get; set; }
  726. public decimal QtyComplete { get; set; }
  727. public decimal QtyReject { get; set; }
  728. public decimal QtyScrap { get; set; }
  729. public string? Status { get; set; }
  730. }
  731. private sealed class WorkOrderViewDetailRow
  732. {
  733. public string? ItemNum { get; set; }
  734. public string? Op { get; set; }
  735. public decimal? QtyRequired { get; set; }
  736. public decimal FrozenBOMQty { get; set; }
  737. }
  738. private sealed class WorkOrderTraceMasterRow
  739. {
  740. public string? WorkOrd { get; set; }
  741. public decimal? QtyOrded { get; set; }
  742. public decimal? QtyCompleted { get; set; }
  743. public string? ItemNum { get; set; }
  744. public string? ItemName { get; set; }
  745. public string? ItemModel { get; set; }
  746. public DateTime? OrdDate { get; set; }
  747. public DateTime? DueDate { get; set; }
  748. public string? Status { get; set; }
  749. }
  750. private sealed class TraceResourceRow
  751. {
  752. public long Sno { get; set; }
  753. public string? Num { get; set; }
  754. public string? ItemNumber { get; set; }
  755. public string? ItemName { get; set; }
  756. public string? Model { get; set; }
  757. public string? Unit { get; set; }
  758. public string? BomNumber { get; set; }
  759. public string? KittingTime { get; set; }
  760. public string? ErpClsName { get; set; }
  761. public string? Backflush { get; set; }
  762. public string? Qty { get; set; }
  763. public string? NeedCount { get; set; }
  764. }
  765. private sealed class TraceScheduleRow
  766. {
  767. public long Sno { get; set; }
  768. public string? WorkOrds { get; set; }
  769. public string? WorkDate { get; set; }
  770. public string? Line { get; set; }
  771. public string? Op { get; set; }
  772. public string? WorkQty { get; set; }
  773. public string? ItemNum { get; set; }
  774. public string? Descr { get; set; }
  775. public string? Descr1 { get; set; }
  776. public string? CreateTime { get; set; }
  777. }
  778. private sealed class TracePoRow
  779. {
  780. public long Sno { get; set; }
  781. public string? PoBillno { get; set; }
  782. public string? SupplierNo { get; set; }
  783. public string? SupplierName { get; set; }
  784. public string? PoTotal { get; set; }
  785. public string? PoPurchaser { get; set; }
  786. public string? State { get; set; }
  787. public string? PoSsendDate { get; set; }
  788. public string? CreateTime { get; set; }
  789. }
  790. private sealed class TracePrRow
  791. {
  792. public long Sno { get; set; }
  793. public string? PrBillno { get; set; }
  794. public string? Number { get; set; }
  795. public string? IcitemName { get; set; }
  796. public string? Model { get; set; }
  797. public string? PrPurchasenumber { get; set; }
  798. public string? PrPurchasename { get; set; }
  799. public string? PrAqty { get; set; }
  800. public string? PrSsendDate { get; set; }
  801. public string? PrSarriveDate { get; set; }
  802. public string? PrUnit { get; set; }
  803. public string? PrPurchaser { get; set; }
  804. }
  805. private sealed class TraceNbrRow
  806. {
  807. public string? Line { get; set; }
  808. public string? Nbr { get; set; }
  809. public string? ItemNum { get; set; }
  810. public string? Descr { get; set; }
  811. public string? Descr1 { get; set; }
  812. public string? LocationFrom { get; set; }
  813. public string? LocationTo { get; set; }
  814. public string? CurrQtyOpened { get; set; }
  815. public string? QtyOrd { get; set; }
  816. public string? QtyFrom { get; set; }
  817. public string? QtyRec { get; set; }
  818. public string? Unit { get; set; }
  819. }
  820. private sealed class TraceProdRow
  821. {
  822. public long Sno { get; set; }
  823. public string? WorkOrd { get; set; }
  824. public string? ItemNum { get; set; }
  825. public string? Descr { get; set; }
  826. public string? Descr1 { get; set; }
  827. public string? Um { get; set; }
  828. public string? QtyCompleted { get; set; }
  829. public string? QtyReject { get; set; }
  830. public string? QtyScrapped { get; set; }
  831. public string? ProdDate { get; set; }
  832. public string? Op { get; set; }
  833. public string? Name { get; set; }
  834. }
  835. private sealed class TraceQualityRow
  836. {
  837. public long Sno { get; set; }
  838. public string? Unit { get; set; }
  839. public string? MorderNo { get; set; }
  840. public string? WorkNumber { get; set; }
  841. public string? MorderProductionNumber { get; set; }
  842. public string? InspectionNumber { get; set; }
  843. public string? QualifiedNumber { get; set; }
  844. public string? InventoryNumber { get; set; }
  845. public string? MoentryWrkcname { get; set; }
  846. public string? PlannerStartDate { get; set; }
  847. public string? PlannerEndDate { get; set; }
  848. public string? MoentryPrdname { get; set; }
  849. public string? MorderFstate { get; set; }
  850. }
  851. private sealed class TraceReceiptRow
  852. {
  853. public long Sno { get; set; }
  854. public string? Status { get; set; }
  855. public string? WorkOrd { get; set; }
  856. public string? UM { get; set; }
  857. public string? QtyOrded { get; set; }
  858. public string? CompQty { get; set; }
  859. public string? InspectionNumber { get; set; }
  860. public string? QualifiedNumber { get; set; }
  861. public string? QtyChangeAdvance { get; set; }
  862. public string? OrdDate { get; set; }
  863. public string? DueDate { get; set; }
  864. }
  865. }