WorkOrderSchedulingService.cs 51 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143
  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. /// <summary>
  20. /// 工单信息类(用于查询结果映射)
  21. /// </summary>
  22. private class WorkOrderInfo
  23. {
  24. public string WorkOrd { get; set; } = string.Empty;
  25. public string Domain { get; set; } = string.Empty;
  26. }
  27. /// <summary>
  28. /// 主表 WorkOrdMaster 按工单号 +「公司域或租户 id」匹配:<c>domain</c> 可为原 <c>Domain</c> 列,或与 <c>tenant_id</c> 相同的字符串(列表已做 COALESCE 回传)。
  29. /// </summary>
  30. private const string SqlWorkOrdMasterMatchDomainOrTenant = """
  31. WorkOrd = @WorkOrd AND (
  32. TRIM(IFNULL(`Domain`, '')) = TRIM(@Domain)
  33. OR (
  34. TRIM(IFNULL(`Domain`, '')) = ''
  35. AND TRIM(@Domain) <> ''
  36. AND CAST(IFNULL(tenant_id, 0) AS CHAR) = TRIM(@Domain)
  37. )
  38. )
  39. """;
  40. /// <summary>带表别名的 WorkOrdMaster 匹配(参数 <c>alias</c> 为表别名,如 a、w)。</summary>
  41. private static string SqlWorkOrdMasterMatchDomainOrTenantAliased(string alias) => $"""
  42. {alias}.WorkOrd = @WorkOrd AND (
  43. TRIM(IFNULL({alias}.`Domain`, '')) = TRIM(@Domain)
  44. OR (
  45. TRIM(IFNULL({alias}.`Domain`, '')) = ''
  46. AND TRIM(@Domain) <> ''
  47. AND CAST(IFNULL({alias}.tenant_id, 0) AS CHAR) = TRIM(@Domain)
  48. )
  49. )
  50. """;
  51. /// <summary>子表(工序/明细)按工单号 + 域或主表租户匹配。</summary>
  52. private static string SqlWorkOrdChildMatchDomainOrTenant(string childAlias, string masterAlias) => $"""
  53. {childAlias}.WorkOrd = @WorkOrd AND (
  54. TRIM(IFNULL({childAlias}.`Domain`, '')) = TRIM(@Domain)
  55. OR EXISTS (
  56. SELECT 1 FROM WorkOrdMaster {masterAlias}
  57. WHERE {masterAlias}.WorkOrd = {childAlias}.WorkOrd
  58. AND (
  59. TRIM(IFNULL({masterAlias}.`Domain`, '')) = TRIM(@Domain)
  60. OR (
  61. TRIM(IFNULL({masterAlias}.`Domain`, '')) = ''
  62. AND TRIM(@Domain) <> ''
  63. AND CAST(IFNULL({masterAlias}.tenant_id, 0) AS CHAR) = TRIM(@Domain)
  64. )
  65. )
  66. )
  67. )
  68. """;
  69. /// <summary>按主键 +「公司域或租户 id」匹配(状态更新等)。</summary>
  70. private const string SqlWorkOrdMasterMatchRecIdAndDomainOrTenant = """
  71. RecID = @Id AND (
  72. TRIM(IFNULL(`Domain`, '')) = TRIM(@Domain)
  73. OR (
  74. TRIM(IFNULL(`Domain`, '')) = ''
  75. AND TRIM(@Domain) <> ''
  76. AND CAST(IFNULL(tenant_id, 0) AS CHAR) = TRIM(@Domain)
  77. )
  78. )
  79. """;
  80. // ══════════════════════════════════════════════════════════════
  81. // 列表 GET /api/Production/scheduling/list
  82. // ══════════════════════════════════════════════════════════════
  83. /// <summary>工单工序排产分页列表(MySQL 口径,与 WorkOrdMaster 等表一致)</summary>
  84. [DisplayName("工单工序排产列表")]
  85. [HttpGet("scheduling/list")]
  86. public async Task<object> GetList([FromQuery] WorkOrderSchedulingListInput input)
  87. {
  88. //const string C = "utf8mb4_general_ci";
  89. const string C = "utf8mb4_0900_ai_ci";
  90. var tenantId = AidopTenantHelper.Resolve(App.HttpContext);
  91. var pars = new List<SugarParameter> { new("@TenantId", tenantId) };
  92. var innerWhere = new List<string>
  93. {
  94. "IFNULL(a.Status,'') <> ''",
  95. "a.tenant_id = @TenantId"
  96. };
  97. if (!string.IsNullOrWhiteSpace(input.WorkOrd))
  98. {
  99. innerWhere.Add($"a.WorkOrd LIKE @WorkOrd");
  100. pars.Add(new SugarParameter("@WorkOrd", $"%{input.WorkOrd.Trim()}%"));
  101. }
  102. if (!string.IsNullOrWhiteSpace(input.LotSerial))
  103. {
  104. innerWhere.Add($"a.LotSerial LIKE @LotSerial");
  105. pars.Add(new SugarParameter("@LotSerial", $"%{input.LotSerial.Trim()}%"));
  106. }
  107. if (!string.IsNullOrWhiteSpace(input.ItemNum))
  108. {
  109. innerWhere.Add($"a.ItemNum LIKE @ItemNum");
  110. pars.Add(new SugarParameter("@ItemNum", $"%{input.ItemNum.Trim()}%"));
  111. }
  112. if (!string.IsNullOrWhiteSpace(input.StartDateFrom))
  113. {
  114. innerWhere.Add("COALESCE(DATE(s.PlanDate), DATE(a.OrdDate)) >= @StartDateFrom");
  115. pars.Add(new SugarParameter("@StartDateFrom", input.StartDateFrom.Trim()));
  116. }
  117. if (!string.IsNullOrWhiteSpace(input.Status))
  118. {
  119. innerWhere.Add("LOWER(a.Status) = @Status");
  120. pars.Add(new SugarParameter("@Status", input.Status.Trim().ToLowerInvariant()));
  121. }
  122. var baseSql = BuildListBaseSql(string.Join(" AND ", innerWhere), C);
  123. var offset = (input.Page - 1) * input.PageSize;
  124. var total = await _db.Ado.GetIntAsync(
  125. $"SELECT COUNT(*) FROM ({baseSql}) AS t", pars);
  126. var list = await _db.Ado.SqlQueryAsync<WorkOrderSchedulingListRow>(
  127. $"SELECT * FROM ({baseSql}) AS t ORDER BY t.Id DESC LIMIT {input.PageSize} OFFSET {offset}",
  128. pars);
  129. return new { total, page = input.Page, pageSize = input.PageSize, list };
  130. }
  131. private static string BuildListBaseSql(string innerWhere, string C) => $"""
  132. SELECT
  133. a.RecID AS Id,
  134. a.Priority AS Priority,
  135. a.WorkOrd AS WorkOrd,
  136. a.LotSerial AS LotSerial,
  137. a.IssueSite AS IssueSite,
  138. a.ItemNum AS ItemNum,
  139. b.Descr AS Descr,
  140. b.Descr1 AS Descr1,
  141. a.QtyOrded AS QtyOrded,
  142. (IFNULL(a.LocationStock, 0) + IFNULL(a.OpQtyCompleted, 0)) AS LocationStock,
  143. a.QtyCompleted AS QtyCompleted,
  144. s.PlanDate AS PlanDate,
  145. s.ProdDate AS ProdDate,
  146. LOWER(a.Status) AS Status,
  147. COALESCE(NULLIF(TRIM(a.`Domain`), ''), IFNULL(CAST(a.tenant_id AS CHAR), '')) AS Domain,
  148. a.Urgent AS Urgent
  149. FROM WorkOrdMaster a
  150. LEFT JOIN ItemMaster b ON a.ItemNum = b.ItemNum
  151. LEFT JOIN ReplenishmentWeekPlan r
  152. ON a.WorkOrd = r.ProductionOrder
  153. AND CAST(a.`Domain` AS CHAR(64)) = CAST(r.factory_id AS CHAR(64))
  154. LEFT JOIN crm_seorder se ON se.bill_no = a.SalesJob
  155. LEFT JOIN CustMaster cm ON cm.Cust = se.custom_no
  156. LEFT JOIN (
  157. SELECT `Domain`, WorkOrds, MIN(PlanDate) AS PlanDate, MIN(ProdDate) AS ProdDate
  158. FROM PeriodSequenceDet
  159. GROUP BY `Domain`, WorkOrds
  160. ) s ON CAST(a.`Domain` AS CHAR(64)) = CAST(s.`Domain` AS CHAR(64))
  161. AND a.WorkOrd = s.WorkOrds
  162. LEFT JOIN (
  163. SELECT morder_no, MAX(create_time) AS checktime
  164. FROM b_examine_result
  165. GROUP BY morder_no
  166. ) exm ON exm.morder_no = a.WorkOrd
  167. LEFT JOIN WorkOrdInStorage ins
  168. ON a.WorkOrd = ins.WorkOrd
  169. AND ins.Remark = '工单预留'
  170. WHERE {innerWhere}
  171. """;
  172. // ══════════════════════════════════════════════════════════════
  173. // 编辑预览 GET /api/Production/scheduling/edit-preview
  174. // ══════════════════════════════════════════════════════════════
  175. [DisplayName("工单优先级编辑数据")]
  176. [HttpGet("scheduling/edit-preview")]
  177. public async Task<object> GetEditPreview([FromQuery] string workOrd, [FromQuery] string domain)
  178. {
  179. if (string.IsNullOrWhiteSpace(workOrd) || string.IsNullOrWhiteSpace(domain))
  180. throw Oops.Oh("工单号与公司域名不能为空");
  181. string sql = $"""
  182. SELECT
  183. a.WorkOrd,
  184. a.ItemNum,
  185. a.QtyOrded,
  186. a.Priority,
  187. a.LotSerial,
  188. IFNULL(a.Urgent, 0) AS Urgent,
  189. DATE(a.OrdDate) AS OrdDate
  190. FROM WorkOrdMaster a
  191. WHERE {SqlWorkOrdMasterMatchDomainOrTenantAliased("a")}
  192. LIMIT 1
  193. """;
  194. var row = (await _db.Ado.SqlQueryAsync<WorkOrderEditPreviewRow>(sql, new { WorkOrd = workOrd.Trim(), Domain = domain.Trim() }))
  195. .FirstOrDefault() ?? throw Oops.Oh("工单不存在");
  196. return row;
  197. }
  198. // ══════════════════════════════════════════════════════════════
  199. // 查看 GET /api/Production/scheduling/view
  200. // ══════════════════════════════════════════════════════════════
  201. [DisplayName("工单查看(主表+工序+物料)")]
  202. [HttpGet("scheduling/view")]
  203. public async Task<object> GetView([FromQuery] string workOrd, [FromQuery] string domain)
  204. {
  205. if (string.IsNullOrWhiteSpace(workOrd) || string.IsNullOrWhiteSpace(domain))
  206. throw Oops.Oh("工单号与公司域名不能为空");
  207. string masterSql = $"""
  208. SELECT
  209. a.WorkOrd,
  210. a.QtyOrded,
  211. a.QtyCompleted,
  212. a.ItemNum,
  213. IFNULL(a.ItemName, b.Descr) AS ItemName,
  214. b.Descr1 AS ItemModel,
  215. DATE(a.OrdDate) AS OrdDate,
  216. DATE(a.DueDate) AS DueDate,
  217. LOWER(a.Status) AS Status,
  218. IFNULL(a.Remark, '') AS Remark
  219. FROM WorkOrdMaster a
  220. LEFT JOIN ItemMaster b ON a.ItemNum = b.ItemNum
  221. WHERE {SqlWorkOrdMasterMatchDomainOrTenantAliased("a")}
  222. LIMIT 1
  223. """;
  224. var master = (await _db.Ado.SqlQueryAsync<WorkOrderViewMasterRow>(masterSql, new { WorkOrd = workOrd.Trim(), Domain = domain.Trim() }))
  225. .FirstOrDefault() ?? throw Oops.Oh("工单不存在");
  226. string routingSql = $"""
  227. SELECT
  228. r.OP AS Op,
  229. r.Descr AS Descr,
  230. IFNULL(r.ParentOp, '') AS ParentOp,
  231. IFNULL(r.MilestoneOp, 0) AS MilestoneOp,
  232. IFNULL(r.PackingQty, 0) AS PackingQty,
  233. IFNULL(r.QtyComplete, 0) AS QtyComplete,
  234. IFNULL(r.QtyReject, 0) AS QtyReject,
  235. IFNULL(r.QtyScrap, 0) AS QtyScrap,
  236. r.Status AS Status
  237. FROM WorkOrdRouting r
  238. WHERE {SqlWorkOrdChildMatchDomainOrTenant("r", "m")}
  239. ORDER BY (r.OP + 0) ASC, r.OP ASC, r.Line ASC, r.ColumnNum ASC
  240. """;
  241. var routings = await _db.Ado.SqlQueryAsync<WorkOrderViewRoutingRow>(routingSql, new { WorkOrd = workOrd.Trim(), Domain = domain.Trim() });
  242. string detailSql = $"""
  243. SELECT d.ItemNum, d.Op, d.QtyRequired, IFNULL(d.FrozenBOMQty, 0) AS FrozenBOMQty
  244. FROM WorkOrdDetail d
  245. WHERE {SqlWorkOrdChildMatchDomainOrTenant("d", "m")}
  246. ORDER BY d.LineNum
  247. """;
  248. var details = await _db.Ado.SqlQueryAsync<WorkOrderViewDetailRow>(detailSql, new { WorkOrd = workOrd.Trim(), Domain = domain.Trim() });
  249. return new { master, routings, details };
  250. }
  251. // ══════════════════════════════════════════════════════════════
  252. // 物料 / 工序明细列表(新标签页)
  253. // ══════════════════════════════════════════════════════════════
  254. [DisplayName("工单物料明细列表")]
  255. [HttpGet("scheduling/materials")]
  256. public async Task<object> GetMaterials([FromQuery] string workOrd, [FromQuery] string domain)
  257. {
  258. if (string.IsNullOrWhiteSpace(workOrd) || string.IsNullOrWhiteSpace(domain))
  259. throw Oops.Oh("工单号与公司域名不能为空");
  260. string sql = $"""
  261. SELECT d.ItemNum, d.Op, d.QtyRequired, IFNULL(d.FrozenBOMQty, 0) AS FrozenBOMQty
  262. FROM WorkOrdDetail d
  263. WHERE {SqlWorkOrdChildMatchDomainOrTenant("d", "m")}
  264. ORDER BY d.LineNum
  265. """;
  266. var list = await _db.Ado.SqlQueryAsync<WorkOrderViewDetailRow>(sql, new { WorkOrd = workOrd.Trim(), Domain = domain.Trim() });
  267. return new { list };
  268. }
  269. [DisplayName("工单工序明细列表")]
  270. [HttpGet("scheduling/routings")]
  271. public async Task<object> GetRoutings([FromQuery] string workOrd, [FromQuery] string domain)
  272. {
  273. if (string.IsNullOrWhiteSpace(workOrd) || string.IsNullOrWhiteSpace(domain))
  274. throw Oops.Oh("工单号与公司域名不能为空");
  275. string sql = $"""
  276. SELECT
  277. r.OP AS Op,
  278. r.Descr AS Descr,
  279. IFNULL(r.ParentOp, '') AS ParentOp,
  280. IFNULL(r.MilestoneOp, 0) AS MilestoneOp,
  281. IFNULL(r.PackingQty, 0) AS PackingQty,
  282. IFNULL(r.QtyComplete, 0) AS QtyComplete,
  283. IFNULL(r.QtyReject, 0) AS QtyReject,
  284. IFNULL(r.QtyScrap, 0) AS QtyScrap,
  285. r.Status AS Status
  286. FROM WorkOrdRouting r
  287. WHERE {SqlWorkOrdChildMatchDomainOrTenant("r", "m")}
  288. ORDER BY (r.OP + 0) ASC, r.OP ASC, r.Line ASC, r.ColumnNum ASC
  289. """;
  290. var list = await _db.Ado.SqlQueryAsync<WorkOrderViewRoutingRow>(sql, new { WorkOrd = workOrd.Trim(), Domain = domain.Trim() });
  291. return new { list };
  292. }
  293. // ══════════════════════════════════════════════════════════════
  294. // 执行追踪 GET /api/Production/scheduling/trace
  295. // ══════════════════════════════════════════════════════════════
  296. [DisplayName("工单执行追踪")]
  297. [HttpGet("scheduling/trace")]
  298. public async Task<object> GetTrace([FromQuery] string workOrd, [FromQuery] string domain)
  299. {
  300. if (string.IsNullOrWhiteSpace(workOrd) || string.IsNullOrWhiteSpace(domain))
  301. throw Oops.Oh("工单号与公司域名不能为空");
  302. var w = workOrd.Trim();
  303. var d = domain.Trim();
  304. string masterSql = $"""
  305. SELECT
  306. a.WorkOrd,
  307. a.QtyOrded,
  308. a.QtyCompleted,
  309. a.ItemNum,
  310. IFNULL(b.Descr, '') AS ItemName,
  311. IFNULL(b.Descr1, '') AS ItemModel,
  312. DATE(a.OrdDate) AS OrdDate,
  313. DATE(a.DueDate) AS DueDate,
  314. LOWER(a.Status) AS Status
  315. FROM WorkOrdMaster a
  316. LEFT JOIN ItemMaster b ON a.ItemNum = b.ItemNum
  317. WHERE {SqlWorkOrdMasterMatchDomainOrTenantAliased("a")}
  318. LIMIT 1
  319. """;
  320. var master = (await _db.Ado.SqlQueryAsync<WorkOrderTraceMasterRow>(masterSql, new { WorkOrd = w, Domain = d }))
  321. .FirstOrDefault() ?? throw Oops.Oh("工单不存在");
  322. var tab1 = await QueryTraceResourceCheckAsync(w, d);
  323. var tab2 = await QueryTraceScheduleAsync(w, d);
  324. var tab3Po = await QueryTracePoAsync(w, d);
  325. var tab3Pr = await QueryTracePrAsync(w, d);
  326. var tab4 = await QueryTraceNbrAsync(w, d);
  327. var tab5 = await QueryTraceProdReportAsync(w, d);
  328. var tab6 = await QueryTraceQualityAsync(w, d);
  329. var tab7 = await QueryTraceReceiptAsync(w, d);
  330. return new
  331. {
  332. master,
  333. resourceCheck = tab1,
  334. schedule = tab2,
  335. purchaseOrders = tab3Po,
  336. purchaseReqs = tab3Pr,
  337. picking = tab4,
  338. production = tab5,
  339. quality = tab6,
  340. receipt = tab7
  341. };
  342. }
  343. private async Task<List<TraceResourceRow>> QueryTraceResourceCheckAsync(string workOrd, string domain)
  344. {
  345. const string sql = """
  346. SELECT
  347. ROW_NUMBER() OVER (ORDER BY bce.id) AS Sno,
  348. CAST(bce.num AS CHAR) AS Num,
  349. bce.item_number AS ItemNumber,
  350. bce.item_name AS ItemName,
  351. bce.model AS Model,
  352. IFNULL(bce.unit, '') AS Unit,
  353. IFNULL(bce.bom_number, '') AS BomNumber,
  354. DATE_FORMAT(bce.kitting_time, '%Y-%m-%d') AS KittingTime,
  355. CASE bce.erp_cls
  356. WHEN 0 THEN '配置类'
  357. WHEN 1 THEN '自制'
  358. WHEN 2 THEN '委外加工'
  359. WHEN 3 THEN '外购'
  360. WHEN 4 THEN '虚拟件'
  361. ELSE ''
  362. END AS ErpClsName,
  363. CASE WHEN bce.backflush = 1 THEN '是' ELSE '否' END AS Backflush,
  364. CAST(bce.qty AS CHAR) AS Qty,
  365. CAST(bce.needCount AS CHAR) AS NeedCount
  366. FROM b_examine_result ber
  367. INNER JOIN b_bom_child_examine bce ON ber.Id = bce.examine_id AND bce.is_use = 1
  368. WHERE ber.morder_no = @WorkOrd
  369. AND ber.Id = (
  370. SELECT br.Id FROM b_examine_result br
  371. WHERE br.morder_no = @WorkOrd
  372. ORDER BY br.create_time DESC
  373. LIMIT 1
  374. )
  375. ORDER BY bce.id
  376. """;
  377. return await _db.Ado.SqlQueryAsync<TraceResourceRow>(sql, new { WorkOrd = workOrd });
  378. }
  379. private async Task<List<TraceScheduleRow>> QueryTraceScheduleAsync(string workOrd, string domain)
  380. {
  381. const string sql = """
  382. SELECT
  383. ROW_NUMBER() OVER (ORDER BY sch.PlanDate) AS Sno,
  384. sch.WorkOrds AS WorkOrds,
  385. DATE_FORMAT(sch.PlanDate, '%Y-%m-%d') AS WorkDate,
  386. IFNULL(sch.Line, '') AS Line,
  387. IFNULL(sch.Op, '') AS Op,
  388. CAST(IFNULL(sch.OrdQty, 0) AS CHAR) AS WorkQty,
  389. IFNULL(sch.ItemNum, '') AS ItemNum,
  390. IFNULL(im.Descr, '') AS Descr,
  391. IFNULL(im.Descr1, '') AS Descr1,
  392. DATE_FORMAT(sch.CreateTime, '%Y-%m-%d %H:%i:%s') AS CreateTime
  393. FROM PeriodSequenceDet sch
  394. LEFT JOIN ItemMaster im ON sch.ItemNum = im.ItemNum
  395. WHERE sch.WorkOrds = @WorkOrd
  396. AND CAST(sch.`Domain` AS CHAR(64)) = CAST(@Domain AS CHAR(64))
  397. ORDER BY sch.PlanDate
  398. """;
  399. return await _db.Ado.SqlQueryAsync<TraceScheduleRow>(sql, new { WorkOrd = workOrd, Domain = domain });
  400. }
  401. private async Task<List<TracePoRow>> QueryTracePoAsync(string workOrd, string domain)
  402. {
  403. const string sql = """
  404. SELECT
  405. ROW_NUMBER() OVER (ORDER BY t.po_billno) AS Sno,
  406. t.po_billno AS PoBillno,
  407. t.supplier_no AS SupplierNo,
  408. t.supplier_name AS SupplierName,
  409. CAST(t.po_total AS CHAR) AS PoTotal,
  410. IFNULL(t.po_purchaser, '') AS PoPurchaser,
  411. t.state AS State,
  412. IFNULL(DATE_FORMAT(t.po_ssend_date, '%Y-%m-%d'), '') AS PoSsendDate,
  413. IFNULL(DATE_FORMAT(t.create_time, '%Y-%m-%d %H:%i:%s'), '') AS CreateTime
  414. FROM (
  415. SELECT DISTINCT
  416. pm.po_billno,
  417. pm.supplier_no,
  418. pm.supplier_name,
  419. pm.po_total,
  420. pm.po_purchaser,
  421. pm.create_time,
  422. pm.po_ssend_date,
  423. CASE pm.state
  424. WHEN 0 THEN '新增'
  425. WHEN 1 THEN '审核中'
  426. WHEN 2 THEN '同意'
  427. WHEN 3 THEN '关闭'
  428. ELSE CAST(pm.state AS CHAR)
  429. END AS state
  430. FROM srm_po_main pm
  431. LEFT JOIN srm_po_list pl ON pm.id = pl.po_id
  432. LEFT JOIN srm_po_occupy occ ON pl.Id = occ.polist_id
  433. AND CAST(pm.factory_id AS CHAR(64)) = CAST(occ.factory_id AS CHAR(64))
  434. LEFT JOIN mes_morder mo ON mo.id = occ.morder_id
  435. WHERE mo.morder_no = @WorkOrd
  436. AND CAST(mo.factory_id AS CHAR(64)) = CAST(@Domain AS CHAR(64))
  437. AND pm.IsDeleted = 0
  438. ) AS t
  439. ORDER BY t.po_billno
  440. """;
  441. return await _db.Ado.SqlQueryAsync<TracePoRow>(sql, new { WorkOrd = workOrd, Domain = domain });
  442. }
  443. private async Task<List<TracePrRow>> QueryTracePrAsync(string workOrd, string domain)
  444. {
  445. const string sql = """
  446. SELECT
  447. ROW_NUMBER() OVER (ORDER BY pm.pr_billno) AS Sno,
  448. pm.pr_billno AS PrBillno,
  449. IFNULL(ic.number, '') AS Number,
  450. IFNULL(pm.icitem_name, '') AS IcitemName,
  451. IFNULL(ic.model, '') AS Model,
  452. IFNULL(pm.pr_purchasenumber, '') AS PrPurchasenumber,
  453. IFNULL(pm.pr_purchasename, '') AS PrPurchasename,
  454. CAST(IFNULL(pm.pr_aqty, 0) AS CHAR) AS PrAqty,
  455. IFNULL(DATE_FORMAT(pm.pr_ssend_date, '%Y-%m-%d'), '') AS PrSsendDate,
  456. IFNULL(DATE_FORMAT(pm.pr_sarrive_date, '%Y-%m-%d'), '') AS PrSarriveDate,
  457. IFNULL(pm.pr_unit, '') AS PrUnit,
  458. IFNULL(pm.pr_purchaser, '') AS PrPurchaser
  459. FROM srm_pr_main pm
  460. LEFT JOIN srm_po_occupy occ ON pm.Id = occ.polist_id
  461. AND CAST(pm.factory_id AS CHAR(64)) = CAST(occ.factory_id AS CHAR(64))
  462. LEFT JOIN ic_item ic ON pm.icitem_id = ic.Id
  463. LEFT JOIN mes_morder mo ON mo.id = occ.morder_id
  464. WHERE mo.morder_no = @WorkOrd
  465. AND CAST(mo.factory_id AS CHAR(64)) = CAST(@Domain AS CHAR(64))
  466. AND IFNULL(pm.state, 0) <> 0
  467. AND pm.IsDeleted = 0
  468. ORDER BY pm.pr_billno
  469. """;
  470. return await _db.Ado.SqlQueryAsync<TracePrRow>(sql, new { WorkOrd = workOrd, Domain = domain });
  471. }
  472. private async Task<List<TraceNbrRow>> QueryTraceNbrAsync(string workOrd, string domain)
  473. {
  474. const string sql = """
  475. SELECT
  476. CAST(nd.Line AS CHAR) AS Line,
  477. nm.Nbr AS Nbr,
  478. nd.ItemNum AS ItemNum,
  479. IFNULL(im.Descr, '') AS Descr,
  480. IFNULL(im.Descr1, '') AS Descr1,
  481. IFNULL(nd.LocationFrom, '') AS LocationFrom,
  482. IFNULL(nd.LocationTo, '') AS LocationTo,
  483. CAST(IFNULL(nd.CurrQtyOpened, 0) AS CHAR) AS CurrQtyOpened,
  484. CAST(IFNULL(nd.QtyOrd, 0) AS CHAR) AS QtyOrd,
  485. CAST(IFNULL(nd.QtyFrom, 0) AS CHAR) AS QtyFrom,
  486. CAST(IFNULL(nd.QtyRec, 0) AS CHAR) AS QtyRec,
  487. IFNULL(nd.UM, '') AS Unit
  488. FROM NbrDetail nd
  489. LEFT JOIN NbrMaster nm ON nd.nbr = nm.nbr
  490. LEFT JOIN ItemMaster im ON nd.ItemNum = im.ItemNum
  491. WHERE nm.WorkOrd = @WorkOrd
  492. AND nm.Type = 'SM'
  493. AND CAST(nm.`Domain` AS CHAR(64)) = CAST(@Domain AS CHAR(64))
  494. ORDER BY nm.Nbr
  495. """;
  496. return await _db.Ado.SqlQueryAsync<TraceNbrRow>(sql, new { WorkOrd = workOrd, Domain = domain });
  497. }
  498. private async Task<List<TraceProdRow>> QueryTraceProdReportAsync(string workOrd, string domain)
  499. {
  500. const string sql = """
  501. SELECT
  502. ROW_NUMBER() OVER (ORDER BY ote.ProdDate DESC) AS Sno,
  503. ote.WorkOrd AS WorkOrd,
  504. IFNULL(ote.ItemNum, '') AS ItemNum,
  505. IFNULL(im.Descr, '') AS Descr,
  506. IFNULL(im.Descr1, '') AS Descr1,
  507. IFNULL(im.Um, '') AS Um,
  508. CAST(IFNULL(ote.QtyCompleted, 0) AS CHAR) AS QtyCompleted,
  509. CAST(IFNULL(ote.QtyReject, 0) AS CHAR) AS QtyReject,
  510. CAST(IFNULL(ote.QtyScrapped, 0) AS CHAR) AS QtyScrapped,
  511. IFNULL(DATE_FORMAT(ote.ProdDate, '%Y-%m-%d %H:%i:%s'), '') AS ProdDate,
  512. IFNULL(ote.Op, '') AS Op,
  513. IFNULL(ote.Name, '') AS Name
  514. FROM OpTransEmployee ote
  515. LEFT JOIN ItemMaster im ON ote.ItemNum = im.ItemNum
  516. WHERE ote.WorkOrd = @WorkOrd
  517. ORDER BY ote.ProdDate DESC
  518. """;
  519. return await _db.Ado.SqlQueryAsync<TraceProdRow>(sql, new { WorkOrd = workOrd });
  520. }
  521. private async Task<List<TraceQualityRow>> QueryTraceQualityAsync(string workOrd, string domain)
  522. {
  523. const string sql = """
  524. SELECT
  525. ROW_NUMBER() OVER (ORDER BY mo.id) AS Sno,
  526. IFNULL(im.Um, '') AS Unit,
  527. IFNULL(mo.morder_no, '') AS MorderNo,
  528. CAST('' AS CHAR) AS WorkNumber,
  529. CAST('' AS CHAR) AS MorderProductionNumber,
  530. CAST('' AS CHAR) AS InspectionNumber,
  531. CAST('' AS CHAR) AS QualifiedNumber,
  532. CAST('' AS CHAR) AS InventoryNumber,
  533. IFNULL(mo.moentry_wrkcname, '') AS MoentryWrkcname,
  534. CAST('' AS CHAR) AS PlannerStartDate,
  535. CAST('' AS CHAR) AS PlannerEndDate,
  536. IFNULL(mo.moentry_prdname, '') AS MoentryPrdname,
  537. CAST('' AS CHAR) AS MorderFstate
  538. FROM mes_morder mo
  539. LEFT JOIN ItemMaster im ON mo.product_code = im.ItemNum
  540. WHERE mo.morder_no = @WorkOrd
  541. AND CAST(mo.factory_id AS CHAR(64)) = CAST(@Domain AS CHAR(64))
  542. AND mo.IsDeleted = 0
  543. ORDER BY mo.id
  544. """;
  545. return await _db.Ado.SqlQueryAsync<TraceQualityRow>(sql, new { WorkOrd = workOrd, Domain = domain });
  546. }
  547. private async Task<List<TraceReceiptRow>> QueryTraceReceiptAsync(string workOrd, string domain)
  548. {
  549. const string sql = """
  550. SELECT
  551. ROW_NUMBER() OVER (ORDER BY c.WorkOrd) AS Sno,
  552. CASE
  553. WHEN LOWER(c.Status) = 'r' THEN '下达'
  554. WHEN LOWER(c.Status) = 'c' THEN '关闭'
  555. WHEN LOWER(c.Status) = 'w' THEN '投产'
  556. ELSE IFNULL(c.Status, '')
  557. END AS Status,
  558. c.WorkOrd AS WorkOrd,
  559. IFNULL(im.Um, '') AS UM,
  560. CAST(IFNULL(c.QtyOrded, 0) AS CHAR) AS QtyOrded,
  561. CAST(IFNULL(compp.CompQty, 0) AS CHAR) AS CompQty,
  562. CAST('' AS CHAR) AS InspectionNumber,
  563. CAST('' AS CHAR) AS QualifiedNumber,
  564. CAST(IFNULL(a.QtyChange, 0) AS CHAR) AS QtyChangeAdvance,
  565. IFNULL(DATE_FORMAT(c.OrdDate, '%Y-%m-%d'), '') AS OrdDate,
  566. IFNULL(DATE_FORMAT(c.DueDate, '%Y-%m-%d'), '') AS DueDate
  567. FROM WorkOrdMaster c
  568. LEFT JOIN InvTransHist a ON a.WorkOrd = c.WorkOrd AND a.TransType = 'rct-wo' AND IFNULL(a.QtyChange, 0) > 0
  569. LEFT JOIN ItemMaster im ON c.ItemNum = im.ItemNum AND im.`Domain` = c.`Domain`
  570. LEFT JOIN (
  571. SELECT WorkOrds, SUM(IFNULL(CompQty, 0)) AS CompQty
  572. FROM PeriodSequenceDet
  573. GROUP BY WorkOrds
  574. ) compp ON c.WorkOrd = compp.WorkOrds
  575. LEFT JOIN mes_morder mo ON mo.morder_no = c.WorkOrd AND mo.IsDeleted = 0
  576. AND CAST(mo.factory_id AS CHAR(64)) = CAST(c.`Domain` AS CHAR(64))
  577. WHERE c.WorkOrd = @WorkOrd AND c.`Domain` = @Domain
  578. """;
  579. return await _db.Ado.SqlQueryAsync<TraceReceiptRow>(sql, new { WorkOrd = workOrd, Domain = domain });
  580. }
  581. // ══════════════════════════════════════════════════════════════
  582. // 保存 POST /api/Production/scheduling/save
  583. // ══════════════════════════════════════════════════════════════
  584. [DisplayName("保存工单(数量/批次/优先级/加急)")]
  585. [HttpPost("scheduling/save")]
  586. public async Task<object> Save([FromBody] WorkOrderSchedulingSaveInput input)
  587. {
  588. var account = _userManager.Account ?? "system";
  589. var pars = new List<SugarParameter>
  590. {
  591. new("@QtyOrded", input.QtyOrded ?? (object)DBNull.Value),
  592. new("@Priority", input.Priority ?? (object)DBNull.Value),
  593. new("@LotSerial", string.IsNullOrWhiteSpace(input.LotSerial) ? DBNull.Value : input.LotSerial.Trim()),
  594. new("@Urgent", input.Urgent ?? (object)DBNull.Value),
  595. new("@UpdateUser", account),
  596. new("@UpdateTime", DateTime.Now),
  597. new("@WorkOrd", input.WorkOrd.Trim()),
  598. new("@Domain", input.Domain.Trim())
  599. };
  600. var n = await _db.Ado.ExecuteCommandAsync(
  601. $"""
  602. UPDATE WorkOrdMaster
  603. SET QtyOrded = COALESCE(@QtyOrded, QtyOrded),
  604. Priority = COALESCE(@Priority, Priority),
  605. LotSerial = COALESCE(@LotSerial, LotSerial),
  606. Urgent = COALESCE(@Urgent, Urgent),
  607. UpdateUser = @UpdateUser,
  608. UpdateTime = @UpdateTime
  609. WHERE {SqlWorkOrdMasterMatchDomainOrTenant}
  610. """,
  611. pars);
  612. if (n == 0)
  613. throw Oops.Oh("工单不存在或未更新");
  614. return new { message = "保存成功" };
  615. }
  616. // ══════════════════════════════════════════════════════════════
  617. // 状态 PATCH POST /api/Production/scheduling/status
  618. // ══════════════════════════════════════════════════════════════
  619. [DisplayName("更新工单状态")]
  620. [HttpPost("scheduling/status")]
  621. public async Task<object> PatchStatus([FromBody] WorkOrderStatusPatchInput input)
  622. {
  623. var account = _userManager.Account ?? "system";
  624. var status = input.Status.Trim().ToLowerInvariant();
  625. var pars = new List<SugarParameter>
  626. {
  627. new("@Status", status),
  628. new("@UpdateUser", account),
  629. new("@UpdateTime", DateTime.Now),
  630. new("@Id", input.Id),
  631. new("@Domain", input.Domain.Trim())
  632. };
  633. var n = await _db.Ado.ExecuteCommandAsync(
  634. $"""
  635. UPDATE WorkOrdMaster
  636. SET Status = @Status,
  637. UpdateUser = @UpdateUser,
  638. UpdateTime = @UpdateTime
  639. WHERE {SqlWorkOrdMasterMatchRecIdAndDomainOrTenant}
  640. """,
  641. pars);
  642. if (n == 0)
  643. throw Oops.Oh("工单不存在或未更新");
  644. return new { message = "状态已更新" };
  645. }
  646. // ══════════════════════════════════════════════════════════════
  647. // 工单关闭 POST /api/Production/scheduling/close
  648. // ══════════════════════════════════════════════════════════════
  649. [DisplayName("工单关闭")]
  650. [HttpPost("scheduling/close")]
  651. public async Task<object> Close([FromBody] WorkOrderCloseInput input)
  652. {
  653. var ids = input.Ids.Trim();
  654. if (string.IsNullOrWhiteSpace(ids))
  655. throw Oops.Oh("工单主键不能为空");
  656. try
  657. {
  658. // 开启事务
  659. _db.Ado.BeginTran();
  660. // 根据 RecID 获取工单号和域信息
  661. // 注意:存储过程 pr_MES_CloseWorkOrders 需要两个参数:域和工单号(竖线分隔)
  662. // 但当前调用只传递 RecID,因此需要根据 RecID 查询对应的工单和域
  663. var workOrders = await _db.Ado.SqlQueryAsync<WorkOrderInfo>(
  664. "SELECT WorkOrd AS WorkOrd, COALESCE(NULLIF(TRIM(`Domain`), ''), CAST(tenant_id AS CHAR(20))) AS Domain FROM WorkOrdMaster WHERE RecID IN (" + BuildInClause(ids) + ")",
  665. GetInParameters(ids));
  666. if (!workOrders.Any())
  667. throw Oops.Oh("未找到对应的工单");
  668. // 为每个域执行关闭操作
  669. var domains = workOrders.Select(x => x.Domain).Distinct();
  670. foreach (var domain in domains)
  671. {
  672. // 获取该域的工单号列表(竖线分隔)
  673. var domainWorkOrds = string.Join("|", workOrders.Where(x => x.Domain == domain).Select(x => x.WorkOrd));
  674. // 执行关闭逻辑(模拟存储过程)
  675. await ExecuteCloseWorkOrders(domain, domainWorkOrds);
  676. }
  677. _db.Ado.CommitTran();
  678. return new { message = "工单关闭成功" };
  679. }
  680. catch (Exception ex)
  681. {
  682. _db.Ado.RollbackTran();
  683. return new { message = $"工单关闭失败: {ex.Message}" };
  684. }
  685. }
  686. /// <summary>
  687. /// 构建 IN 子句参数
  688. /// </summary>
  689. private string BuildInClause(string ids)
  690. {
  691. var idList = ids.Split(',').Select(x => x.Trim()).Where(x => !string.IsNullOrWhiteSpace(x));
  692. return string.Join(",", idList.Select((_, i) => $"@Id{i}"));
  693. }
  694. /// <summary>
  695. /// 获取 IN 子句参数
  696. /// </summary>
  697. private SugarParameter[] GetInParameters(string ids)
  698. {
  699. var idList = ids.Split(',').Select(x => x.Trim()).Where(x => !string.IsNullOrWhiteSpace(x));
  700. return idList.Select((id, i) => new SugarParameter($"@Id{i}", id)).ToArray();
  701. }
  702. /// <summary>
  703. /// 执行关闭工单逻辑(模拟存储过程 pr_MES_CloseWorkOrders)
  704. /// </summary>
  705. private async Task ExecuteCloseWorkOrders(string domain, string workOrds)
  706. {
  707. if (string.IsNullOrWhiteSpace(workOrds))
  708. return;
  709. // 存储过程使用竖线分隔,但FIND_IN_SET需要逗号分隔
  710. var workOrdsForFindInSet = workOrds.Replace("|", ",");
  711. // 1. 更新工单状态为C
  712. await _db.Ado.ExecuteCommandAsync(
  713. "UPDATE WorkOrdMaster SET Status = 'C', UpdateUser = 'proc' WHERE tenant_id = @Domain AND FIND_IN_SET(WorkOrd, @WorkOrds) > 0",
  714. new SugarParameter("@Domain", domain),
  715. new SugarParameter("@WorkOrds", workOrdsForFindInSet));
  716. // 2. 更新领料单状态为C
  717. await _db.Ado.ExecuteCommandAsync(
  718. "UPDATE NbrMaster SET Status = 'C' WHERE Type = 'SM' AND tenant_id = @Domain AND FIND_IN_SET(WorkOrd, @WorkOrds) > 0",
  719. new SugarParameter("@Domain", domain),
  720. new SugarParameter("@WorkOrds", workOrdsForFindInSet));
  721. // 3. 更新领料单明细状态为C
  722. await _db.Ado.ExecuteCommandAsync(
  723. "UPDATE NbrDetail SET Status = 'C' WHERE tenant_id = @Domain AND Type = 'SM' AND Nbr IN (SELECT Nbr FROM NbrMaster WHERE Type = 'SM' AND Domain = @Domain AND FIND_IN_SET(WorkOrd, @WorkOrds) > 0)",
  724. new SugarParameter("@Domain", domain),
  725. new SugarParameter("@WorkOrds", workOrdsForFindInSet));
  726. // 4. 更新退料单状态为C
  727. await _db.Ado.ExecuteCommandAsync(
  728. "UPDATE NbrMaster SET Status = 'C' WHERE Type = 'WOD' AND tenant_id = @Domain AND FIND_IN_SET(WorkOrd, @WorkOrds) > 0",
  729. new SugarParameter("@Domain", domain),
  730. new SugarParameter("@WorkOrds", workOrdsForFindInSet));
  731. // 5. 更新退料单明细状态为C
  732. await _db.Ado.ExecuteCommandAsync(
  733. "UPDATE NbrDetail SET Status = 'C' WHERE tenant_id = @Domain AND Type = 'WOD' AND Nbr IN (SELECT Nbr FROM NbrMaster WHERE Type = 'WOD' AND Domain = @Domain AND FIND_IN_SET(WorkOrd, @WorkOrds) > 0)",
  734. new SugarParameter("@Domain", domain),
  735. new SugarParameter("@WorkOrds", workOrdsForFindInSet));
  736. // 6. 删除库存占用
  737. await _db.Ado.ExecuteCommandAsync(
  738. "DELETE FROM ic_item_stockoccupy WHERE tenant_id = @Domain AND FIND_IN_SET(morder_mo, @WorkOrds) > 0",
  739. new SugarParameter("@Domain", domain),
  740. new SugarParameter("@WorkOrds", workOrdsForFindInSet));
  741. // 7. 删除PO占用
  742. await _db.Ado.ExecuteCommandAsync(
  743. "DELETE FROM srm_po_occupy WHERE tenant_id = @Domain AND FIND_IN_SET(morder_mo, @WorkOrds) > 0",
  744. new SugarParameter("@Domain", domain),
  745. new SugarParameter("@WorkOrds", workOrdsForFindInSet));
  746. // 8. 删除工单占用
  747. await _db.Ado.ExecuteCommandAsync(
  748. "DELETE FROM mes_mooccupy WHERE tenant_id = @Domain AND FIND_IN_SET(moo_mo, @WorkOrds) > 0",
  749. new SugarParameter("@Domain", domain),
  750. new SugarParameter("@WorkOrds", workOrdsForFindInSet));
  751. }
  752. // ══════════════════════════════════════════════════════════════
  753. // 同步工艺路线 POST /api/Production/scheduling/sync-routing
  754. // ══════════════════════════════════════════════════════════════
  755. [DisplayName("同步工艺路线")]
  756. [HttpPost("scheduling/sync-routing")]
  757. public async Task<object> SyncRouting([FromBody] WorkOrderKeyInput input)
  758. {
  759. var workOrd = input.WorkOrd.Trim();
  760. var domain = input.Domain.Trim();
  761. var createUser = (_userManager.Account ?? "system").Trim();
  762. if (createUser.Length > 24)
  763. createUser = createUser[..24];
  764. var pars = new List<SugarParameter>
  765. {
  766. new("@WorkOrd", workOrd),
  767. new("@Domain", domain),
  768. new("@CreateUser", createUser)
  769. };
  770. await _db.Ado.BeginTranAsync();
  771. try
  772. {
  773. await _db.Ado.ExecuteCommandAsync(
  774. $"""
  775. DELETE FROM WorkOrdRouting rr
  776. WHERE {SqlWorkOrdChildMatchDomainOrTenant("rr", "m")}
  777. """,
  778. pars);
  779. // 业务口径:由 RoutingOpDetail + ProdLineDetail 重算工单工艺路线(MySQL)。
  780. // 与历史脚本一致:DELETE 仍按工单 + 域/租户匹配,避免误删其它域数据。
  781. await _db.Ado.ExecuteCommandAsync(
  782. $"""
  783. INSERT INTO WorkOrdRouting (
  784. `Descr`, `Domain`, `ChargeCode`, `Machine`, `RunCrew`, `MilestoneOp`, `OP`, `StdOp`, `ItemNum`, `WorkCtr`,
  785. `Ufld1`, `Ufld3`, `Setup`, `MachinesperOp`, `Labor`, `RunTime`, `MachBdnRate`, `WorkCode`, `StdSetupTime`, `Engineer`,
  786. `WorkOrd`, `WorkOrdMasterRecID`, `ERPfld1`, `QtyOrded`, `ProcessOut`, `ProcessOutDay`, `ProcessOutSupp`,
  787. `IsActive`, `Status`, `ProdLine`, `CreateTime`, `CreateUser`, `tenant_id`,`CommentIndex`,`WaitTime`,`QtyComplete`,`ParentOp`
  788. )
  789. SELECT
  790. a.`Descr`,
  791. LEFT(TRIM(COALESCE(NULLIF(TRIM(IFNULL(w.`Domain`, '')), ''), NULLIF(TRIM(IFNULL(a.`Domain`, '')), ''), ' ')), 8),
  792. '',
  793. b.`InternalEquipmentCode`,
  794. IFNULL(b.`StandardStaffCount`, 0),
  795. CAST(IFNULL(a.`MilestoneOp`, 0) AS UNSIGNED),
  796. IFNULL(a.`Op`, 0),
  797. a.`StdOp`,
  798. a.`RoutingCode`,
  799. LEFT(IFNULL(b.`Site`, ''), 8),
  800. '',
  801. '',
  802. IFNULL(a.`UDeci1`, 0),
  803. IFNULL(a.`UDeci2`, 0),
  804. IFNULL(a.`UDeci3`, 0),
  805. IFNULL(a.`UDeci3`, 0) / 3600.0,
  806. IFNULL(b.`Rate`, 0),
  807. b.`OpType`,
  808. IFNULL(b.`SetupTime`, 0),
  809. b.`SkillNo`,
  810. w.`WorkOrd`,
  811. w.`RecID`,
  812. w.`ERPfld1`,
  813. IFNULL(w.`QtyOrded`, 0),
  814. CAST(IFNULL(a.`UDeci5`, 0) AS SIGNED),
  815. IFNULL(a.`ProcessOutDay`, 0),
  816. a.`ProcessOutSupp`,
  817. b'1',
  818. 'r',
  819. LEFT(IFNULL(b.`Line`, ''), 8),
  820. NOW(3),
  821. @CreateUser,
  822. w.`tenant_id`,CAST(IFNULL(a.`MilestoneOp`, 0) AS UNSIGNED),0,0,0
  823. FROM WorkOrdMaster w
  824. LEFT JOIN RoutingOpDetail a ON w.`ItemNum` = a.`RoutingCode`
  825. LEFT JOIN ProdLineDetail b ON a.`RoutingCode` = b.`Part` AND a.`Op` = b.`Op`
  826. WHERE w.`WorkOrd` = @WorkOrd
  827. AND {SqlWorkOrdMasterMatchDomainOrTenantAliased("w")}
  828. AND IFNULL(a.`IsActive`, 0) = 1
  829. AND a.`MilestoneOp` IS NOT NULL
  830. """,
  831. pars);
  832. await _db.Ado.CommitTranAsync();
  833. }
  834. catch (Exception ex)
  835. {
  836. await _db.Ado.RollbackTranAsync();
  837. throw Oops.Oh($"同步工艺路线失败:{ex.Message}");
  838. }
  839. return new { message = "同步工艺路线已执行" };
  840. }
  841. // ══════════════════════════════════════════════════════════════
  842. // 加急 POST /api/Production/scheduling/urgent
  843. // ══════════════════════════════════════════════════════════════
  844. [DisplayName("设置加急")]
  845. [HttpPost("scheduling/urgent")]
  846. public async Task<object> SetUrgent([FromBody] WorkOrderUrgentInput input)
  847. {
  848. var account = _userManager.Account ?? "system";
  849. var n = await _db.Ado.ExecuteCommandAsync(
  850. $"""
  851. UPDATE WorkOrdMaster
  852. SET Urgent = @Urgent,
  853. UpdateUser = @UpdateUser,
  854. UpdateTime = @UpdateTime
  855. WHERE {SqlWorkOrdMasterMatchDomainOrTenant}
  856. """,
  857. new List<SugarParameter>
  858. {
  859. new("@Urgent", input.Urgent),
  860. new("@UpdateUser", account),
  861. new("@UpdateTime", DateTime.Now),
  862. new("@WorkOrd", input.WorkOrd.Trim()),
  863. new("@Domain", input.Domain.Trim())
  864. });
  865. if (n == 0)
  866. throw Oops.Oh("工单不存在");
  867. return new { message = input.Urgent == 2 ? "已设为特急" : "已设为加急" };
  868. }
  869. // ──────────────── 行类型 ────────────────
  870. private sealed class WorkOrderSchedulingListRow
  871. {
  872. public int Id { get; set; }
  873. public string? Priority { get; set; }
  874. public string? WorkOrd { get; set; }
  875. public string? LotSerial { get; set; }
  876. public string? IssueSite { get; set; }
  877. public string? ItemNum { get; set; }
  878. public string? Descr { get; set; }
  879. public string? Descr1 { get; set; }
  880. public decimal? QtyOrded { get; set; }
  881. public decimal? LocationStock { get; set; }
  882. public decimal? QtyCompleted { get; set; }
  883. public DateTime? PlanDate { get; set; }
  884. public DateTime? ProdDate { get; set; }
  885. public string? Status { get; set; }
  886. public string? Domain { get; set; }
  887. public int? Urgent { get; set; }
  888. }
  889. private sealed class WorkOrderEditPreviewRow
  890. {
  891. public string? WorkOrd { get; set; }
  892. public string? ItemNum { get; set; }
  893. public decimal? QtyOrded { get; set; }
  894. public string? Priority { get; set; }
  895. public string? LotSerial { get; set; }
  896. public int Urgent { get; set; }
  897. public DateTime? OrdDate { get; set; }
  898. }
  899. private sealed class WorkOrderViewMasterRow
  900. {
  901. public string? WorkOrd { get; set; }
  902. public decimal? QtyOrded { get; set; }
  903. public decimal? QtyCompleted { get; set; }
  904. public string? ItemNum { get; set; }
  905. public string? ItemName { get; set; }
  906. public string? ItemModel { get; set; }
  907. public DateTime? OrdDate { get; set; }
  908. public DateTime? DueDate { get; set; }
  909. public string? Status { get; set; }
  910. public string? Remark { get; set; }
  911. }
  912. private sealed class WorkOrderViewRoutingRow
  913. {
  914. public string? Op { get; set; }
  915. public string? Descr { get; set; }
  916. public string? ParentOp { get; set; }
  917. public int MilestoneOp { get; set; }
  918. public decimal PackingQty { get; set; }
  919. public decimal QtyComplete { get; set; }
  920. public decimal QtyReject { get; set; }
  921. public decimal QtyScrap { get; set; }
  922. public string? Status { get; set; }
  923. }
  924. private sealed class WorkOrderViewDetailRow
  925. {
  926. public string? ItemNum { get; set; }
  927. public string? Op { get; set; }
  928. public decimal? QtyRequired { get; set; }
  929. public decimal FrozenBOMQty { get; set; }
  930. }
  931. private sealed class WorkOrderTraceMasterRow
  932. {
  933. public string? WorkOrd { get; set; }
  934. public decimal? QtyOrded { get; set; }
  935. public decimal? QtyCompleted { get; set; }
  936. public string? ItemNum { get; set; }
  937. public string? ItemName { get; set; }
  938. public string? ItemModel { get; set; }
  939. public DateTime? OrdDate { get; set; }
  940. public DateTime? DueDate { get; set; }
  941. public string? Status { get; set; }
  942. }
  943. private sealed class TraceResourceRow
  944. {
  945. public long Sno { get; set; }
  946. public string? Num { get; set; }
  947. public string? ItemNumber { get; set; }
  948. public string? ItemName { get; set; }
  949. public string? Model { get; set; }
  950. public string? Unit { get; set; }
  951. public string? BomNumber { get; set; }
  952. public string? KittingTime { get; set; }
  953. public string? ErpClsName { get; set; }
  954. public string? Backflush { get; set; }
  955. public string? Qty { get; set; }
  956. public string? NeedCount { get; set; }
  957. }
  958. private sealed class TraceScheduleRow
  959. {
  960. public long Sno { get; set; }
  961. public string? WorkOrds { get; set; }
  962. public string? WorkDate { get; set; }
  963. public string? Line { get; set; }
  964. public string? Op { get; set; }
  965. public string? WorkQty { get; set; }
  966. public string? ItemNum { get; set; }
  967. public string? Descr { get; set; }
  968. public string? Descr1 { get; set; }
  969. public string? CreateTime { get; set; }
  970. }
  971. private sealed class TracePoRow
  972. {
  973. public long Sno { get; set; }
  974. public string? PoBillno { get; set; }
  975. public string? SupplierNo { get; set; }
  976. public string? SupplierName { get; set; }
  977. public string? PoTotal { get; set; }
  978. public string? PoPurchaser { get; set; }
  979. public string? State { get; set; }
  980. public string? PoSsendDate { get; set; }
  981. public string? CreateTime { get; set; }
  982. }
  983. private sealed class TracePrRow
  984. {
  985. public long Sno { get; set; }
  986. public string? PrBillno { get; set; }
  987. public string? Number { get; set; }
  988. public string? IcitemName { get; set; }
  989. public string? Model { get; set; }
  990. public string? PrPurchasenumber { get; set; }
  991. public string? PrPurchasename { get; set; }
  992. public string? PrAqty { get; set; }
  993. public string? PrSsendDate { get; set; }
  994. public string? PrSarriveDate { get; set; }
  995. public string? PrUnit { get; set; }
  996. public string? PrPurchaser { get; set; }
  997. }
  998. private sealed class TraceNbrRow
  999. {
  1000. public string? Line { get; set; }
  1001. public string? Nbr { get; set; }
  1002. public string? ItemNum { get; set; }
  1003. public string? Descr { get; set; }
  1004. public string? Descr1 { get; set; }
  1005. public string? LocationFrom { get; set; }
  1006. public string? LocationTo { get; set; }
  1007. public string? CurrQtyOpened { get; set; }
  1008. public string? QtyOrd { get; set; }
  1009. public string? QtyFrom { get; set; }
  1010. public string? QtyRec { get; set; }
  1011. public string? Unit { get; set; }
  1012. }
  1013. private sealed class TraceProdRow
  1014. {
  1015. public long Sno { get; set; }
  1016. public string? WorkOrd { get; set; }
  1017. public string? ItemNum { get; set; }
  1018. public string? Descr { get; set; }
  1019. public string? Descr1 { get; set; }
  1020. public string? Um { get; set; }
  1021. public string? QtyCompleted { get; set; }
  1022. public string? QtyReject { get; set; }
  1023. public string? QtyScrapped { get; set; }
  1024. public string? ProdDate { get; set; }
  1025. public string? Op { get; set; }
  1026. public string? Name { get; set; }
  1027. }
  1028. private sealed class TraceQualityRow
  1029. {
  1030. public long Sno { get; set; }
  1031. public string? Unit { get; set; }
  1032. public string? MorderNo { get; set; }
  1033. public string? WorkNumber { get; set; }
  1034. public string? MorderProductionNumber { get; set; }
  1035. public string? InspectionNumber { get; set; }
  1036. public string? QualifiedNumber { get; set; }
  1037. public string? InventoryNumber { get; set; }
  1038. public string? MoentryWrkcname { get; set; }
  1039. public string? PlannerStartDate { get; set; }
  1040. public string? PlannerEndDate { get; set; }
  1041. public string? MoentryPrdname { get; set; }
  1042. public string? MorderFstate { get; set; }
  1043. }
  1044. private sealed class TraceReceiptRow
  1045. {
  1046. public long Sno { get; set; }
  1047. public string? Status { get; set; }
  1048. public string? WorkOrd { get; set; }
  1049. public string? UM { get; set; }
  1050. public string? QtyOrded { get; set; }
  1051. public string? CompQty { get; set; }
  1052. public string? InspectionNumber { get; set; }
  1053. public string? QualifiedNumber { get; set; }
  1054. public string? QtyChangeAdvance { get; set; }
  1055. public string? OrdDate { get; set; }
  1056. public string? DueDate { get; set; }
  1057. }
  1058. }