WorkOrderSchedulingService.cs 91 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710171117121713171417151716171717181719172017211722172317241725172617271728172917301731173217331734173517361737173817391740174117421743174417451746174717481749175017511752175317541755175617571758175917601761176217631764176517661767176817691770177117721773177417751776177717781779178017811782178317841785178617871788178917901791179217931794179517961797179817991800180118021803180418051806180718081809181018111812181318141815181618171818181918201821182218231824182518261827182818291830183118321833183418351836183718381839184018411842184318441845184618471848184918501851185218531854185518561857185818591860186118621863186418651866186718681869187018711872187318741875187618771878187918801881188218831884188518861887188818891890189118921893189418951896189718981899190019011902190319041905190619071908190919101911191219131914191519161917191819191920192119221923192419251926192719281929193019311932193319341935193619371938193919401941194219431944194519461947194819491950195119521953195419551956195719581959196019611962196319641965196619671968196919701971197219731974197519761977197819791980198119821983198419851986198719881989199019911992199319941995199619971998199920002001200220032004200520062007200820092010201120122013201420152016201720182019202020212022202320242025202620272028202920302031203220332034
  1. using System.Text.Json;
  2. using Admin.NET.Plugin.AiDOP.Entity.DataPlatform;
  3. namespace Admin.NET.Plugin.AiDOP.Production;
  4. /// <summary>
  5. /// 工单工序排产服务 🏭
  6. /// 路由前缀:/api/Production/scheduling/...
  7. /// </summary>
  8. [ApiDescriptionSettings(Order = 265, Description = "工单工序排产")]
  9. [Route("api/Production")]
  10. [AllowAnonymous]
  11. [NonUnify]
  12. public class WorkOrderSchedulingService : IDynamicApiController, ITransient
  13. {
  14. private readonly ISqlSugarClient _db;
  15. private readonly UserManager _userManager;
  16. public WorkOrderSchedulingService(ISqlSugarClient db, UserManager userManager)
  17. {
  18. _db = db;
  19. _userManager = userManager;
  20. }
  21. /// <summary>
  22. /// 工单信息类(用于查询结果映射)
  23. /// </summary>
  24. private class WorkOrderInfo
  25. {
  26. public string WorkOrd { get; set; } = string.Empty;
  27. public string Domain { get; set; } = string.Empty;
  28. }
  29. /// <summary>
  30. /// 主表 WorkOrdMaster 按工单号 +「公司域或租户 id」匹配:<c>domain</c> 可为原 <c>Domain</c> 列,或与 <c>tenant_id</c> 相同的字符串(列表已做 COALESCE 回传)。
  31. /// </summary>
  32. private const string SqlWorkOrdMasterMatchDomainOrTenant = """
  33. WorkOrd = @WorkOrd AND (
  34. TRIM(IFNULL(`Domain`, '')) = TRIM(@Domain)
  35. OR (
  36. TRIM(IFNULL(`Domain`, '')) = ''
  37. AND TRIM(@Domain) <> ''
  38. AND CAST(IFNULL(tenant_id, 0) AS CHAR) = TRIM(@Domain)
  39. )
  40. )
  41. """;
  42. /// <summary>带表别名的 WorkOrdMaster 匹配(参数 <c>alias</c> 为表别名,如 a、w)。</summary>
  43. private static string SqlWorkOrdMasterMatchDomainOrTenantAliased(string alias) => $"""
  44. {alias}.WorkOrd = @WorkOrd AND (
  45. TRIM(IFNULL({alias}.`Domain`, '')) = TRIM(@Domain)
  46. OR (
  47. TRIM(IFNULL({alias}.`Domain`, '')) = ''
  48. AND TRIM(@Domain) <> ''
  49. AND CAST(IFNULL({alias}.tenant_id, 0) AS CHAR) = TRIM(@Domain)
  50. )
  51. )
  52. """;
  53. /// <summary>子表(工序/明细)按工单号 + 域或主表租户匹配。</summary>
  54. private static string SqlWorkOrdChildMatchDomainOrTenant(string childAlias, string masterAlias) => $"""
  55. {childAlias}.WorkOrd = @WorkOrd AND (
  56. TRIM(IFNULL({childAlias}.`Domain`, '')) = TRIM(@Domain)
  57. OR EXISTS (
  58. SELECT 1 FROM WorkOrdMaster {masterAlias}
  59. WHERE {masterAlias}.WorkOrd = {childAlias}.WorkOrd
  60. AND (
  61. TRIM(IFNULL({masterAlias}.`Domain`, '')) = TRIM(@Domain)
  62. OR (
  63. TRIM(IFNULL({masterAlias}.`Domain`, '')) = ''
  64. AND TRIM(@Domain) <> ''
  65. AND CAST(IFNULL({masterAlias}.tenant_id, 0) AS CHAR) = TRIM(@Domain)
  66. )
  67. )
  68. )
  69. )
  70. """;
  71. /// <summary>按主键 +「公司域或租户 id」匹配(状态更新等)。</summary>
  72. private const string SqlWorkOrdMasterMatchRecIdAndDomainOrTenant = """
  73. RecID = @Id AND (
  74. TRIM(IFNULL(`Domain`, '')) = TRIM(@Domain)
  75. OR (
  76. TRIM(IFNULL(`Domain`, '')) = ''
  77. AND TRIM(@Domain) <> ''
  78. AND CAST(IFNULL(tenant_id, 0) AS CHAR) = TRIM(@Domain)
  79. )
  80. )
  81. """;
  82. // ══════════════════════════════════════════════════════════════
  83. // 列表 GET /api/Production/scheduling/list
  84. // ══════════════════════════════════════════════════════════════
  85. /// <summary>工单工序排产分页列表(MySQL 口径,与 WorkOrdMaster 等表一致)</summary>
  86. [DisplayName("工单工序排产列表")]
  87. [HttpGet("scheduling/list")]
  88. public async Task<object> GetList([FromQuery] WorkOrderSchedulingListInput input)
  89. {
  90. //const string C = "utf8mb4_general_ci";
  91. const string C = "utf8mb4_0900_ai_ci";
  92. var tenantId = AidopTenantHelper.Resolve(App.HttpContext);
  93. var pars = new List<SugarParameter> { new("@TenantId", tenantId) };
  94. var innerWhere = new List<string>
  95. {
  96. "IFNULL(a.Status,'') <> ''",
  97. "a.tenant_id = @TenantId"
  98. };
  99. if (!string.IsNullOrWhiteSpace(input.WorkOrd))
  100. {
  101. innerWhere.Add($"a.WorkOrd LIKE @WorkOrd");
  102. pars.Add(new SugarParameter("@WorkOrd", $"%{input.WorkOrd.Trim()}%"));
  103. }
  104. if (!string.IsNullOrWhiteSpace(input.LotSerial))
  105. {
  106. innerWhere.Add($"a.LotSerial LIKE @LotSerial");
  107. pars.Add(new SugarParameter("@LotSerial", $"%{input.LotSerial.Trim()}%"));
  108. }
  109. if (!string.IsNullOrWhiteSpace(input.ItemNum))
  110. {
  111. innerWhere.Add($"a.ItemNum LIKE @ItemNum");
  112. pars.Add(new SugarParameter("@ItemNum", $"%{input.ItemNum.Trim()}%"));
  113. }
  114. if (!string.IsNullOrWhiteSpace(input.StartDateFrom))
  115. {
  116. innerWhere.Add("COALESCE(DATE(s.PlanDate), DATE(a.OrdDate)) >= @StartDateFrom");
  117. pars.Add(new SugarParameter("@StartDateFrom", input.StartDateFrom.Trim()));
  118. }
  119. if (!string.IsNullOrWhiteSpace(input.Status))
  120. {
  121. innerWhere.Add("LOWER(a.Status) = @Status");
  122. pars.Add(new SugarParameter("@Status", input.Status.Trim().ToLowerInvariant()));
  123. }
  124. var baseSql = BuildListBaseSql(string.Join(" AND ", innerWhere), C);
  125. var offset = (input.Page - 1) * input.PageSize;
  126. var total = await _db.Ado.GetIntAsync(
  127. $"SELECT COUNT(*) FROM ({baseSql}) AS t", pars);
  128. var list = await _db.Ado.SqlQueryAsync<WorkOrderSchedulingListRow>(
  129. $"SELECT * FROM ({baseSql}) AS t ORDER BY t.Id DESC LIMIT {input.PageSize} OFFSET {offset}",
  130. pars);
  131. return new { total, page = input.Page, pageSize = input.PageSize, list };
  132. }
  133. private static string BuildListBaseSql(string innerWhere, string C) => $"""
  134. SELECT
  135. a.RecID AS Id,
  136. a.Priority AS Priority,
  137. a.WorkOrd AS WorkOrd,
  138. a.LotSerial AS LotSerial,
  139. a.IssueSite AS IssueSite,
  140. a.ItemNum AS ItemNum,
  141. b.Descr AS Descr,
  142. b.Descr1 AS Descr1,
  143. a.QtyOrded AS QtyOrded,
  144. (IFNULL(a.LocationStock, 0) + IFNULL(a.OpQtyCompleted, 0)) AS LocationStock,
  145. a.QtyCompleted AS QtyCompleted,
  146. s.PlanDate AS PlanDate,
  147. s.ProdDate AS ProdDate,
  148. LOWER(a.Status) AS Status,
  149. COALESCE(NULLIF(TRIM(a.`Domain`), ''), IFNULL(CAST(a.tenant_id AS CHAR), '')) AS Domain,
  150. a.Urgent AS Urgent
  151. FROM WorkOrdMaster a
  152. LEFT JOIN ItemMaster b ON a.ItemNum = b.ItemNum
  153. LEFT JOIN ReplenishmentWeekPlan r
  154. ON a.WorkOrd = r.ProductionOrder
  155. AND CAST(a.`Domain` AS CHAR(64)) = CAST(r.factory_id AS CHAR(64))
  156. LEFT JOIN crm_seorder se ON se.bill_no = a.SalesJob
  157. LEFT JOIN CustMaster cm ON cm.Cust = se.custom_no
  158. LEFT JOIN (
  159. SELECT `Domain`, WorkOrds, MIN(PlanDate) AS PlanDate, MIN(ProdDate) AS ProdDate
  160. FROM PeriodSequenceDet
  161. GROUP BY `Domain`, WorkOrds
  162. ) s ON CAST(a.`Domain` AS CHAR(64)) = CAST(s.`Domain` AS CHAR(64))
  163. AND a.WorkOrd = s.WorkOrds
  164. LEFT JOIN (
  165. SELECT morder_no, MAX(create_time) AS checktime
  166. FROM b_examine_result
  167. GROUP BY morder_no
  168. ) exm ON exm.morder_no = a.WorkOrd
  169. LEFT JOIN WorkOrdInStorage ins
  170. ON a.WorkOrd = ins.WorkOrd
  171. AND ins.Remark = '工单预留'
  172. WHERE {innerWhere}
  173. """;
  174. // ══════════════════════════════════════════════════════════════
  175. // 编辑预览 GET /api/Production/scheduling/edit-preview
  176. // ══════════════════════════════════════════════════════════════
  177. [DisplayName("工单优先级编辑数据")]
  178. [HttpGet("scheduling/edit-preview")]
  179. public async Task<object> GetEditPreview([FromQuery] string workOrd, [FromQuery] string domain)
  180. {
  181. if (string.IsNullOrWhiteSpace(workOrd) || string.IsNullOrWhiteSpace(domain))
  182. throw Oops.Oh("工单号与公司域名不能为空");
  183. string sql = $"""
  184. SELECT
  185. a.WorkOrd,
  186. a.ItemNum,
  187. a.QtyOrded,
  188. a.Priority,
  189. a.LotSerial,
  190. IFNULL(a.Urgent, 0) AS Urgent,
  191. DATE(a.OrdDate) AS OrdDate
  192. FROM WorkOrdMaster a
  193. WHERE {SqlWorkOrdMasterMatchDomainOrTenantAliased("a")}
  194. LIMIT 1
  195. """;
  196. var row = (await _db.Ado.SqlQueryAsync<WorkOrderEditPreviewRow>(sql, new { WorkOrd = workOrd.Trim(), Domain = domain.Trim() }))
  197. .FirstOrDefault() ?? throw Oops.Oh("工单不存在");
  198. return row;
  199. }
  200. // ══════════════════════════════════════════════════════════════
  201. // 查看 GET /api/Production/scheduling/view
  202. // ══════════════════════════════════════════════════════════════
  203. [DisplayName("工单查看(主表+工序+物料)")]
  204. [HttpGet("scheduling/view")]
  205. public async Task<object> GetView([FromQuery] string workOrd, [FromQuery] string domain)
  206. {
  207. if (string.IsNullOrWhiteSpace(workOrd) || string.IsNullOrWhiteSpace(domain))
  208. throw Oops.Oh("工单号与公司域名不能为空");
  209. string masterSql = $"""
  210. SELECT
  211. a.WorkOrd,
  212. a.QtyOrded,
  213. a.QtyCompleted,
  214. a.ItemNum,
  215. IFNULL(a.ItemName, b.Descr) AS ItemName,
  216. b.Descr1 AS ItemModel,
  217. DATE(a.OrdDate) AS OrdDate,
  218. DATE(a.DueDate) AS DueDate,
  219. LOWER(a.Status) AS Status,
  220. IFNULL(a.Remark, '') AS Remark
  221. FROM WorkOrdMaster a
  222. LEFT JOIN ItemMaster b ON a.ItemNum = b.ItemNum
  223. WHERE {SqlWorkOrdMasterMatchDomainOrTenantAliased("a")}
  224. LIMIT 1
  225. """;
  226. var master = (await _db.Ado.SqlQueryAsync<WorkOrderViewMasterRow>(masterSql, new { WorkOrd = workOrd.Trim(), Domain = domain.Trim() }))
  227. .FirstOrDefault() ?? throw Oops.Oh("工单不存在");
  228. string routingSql = $"""
  229. SELECT
  230. r.OP AS Op,
  231. r.Descr AS Descr,
  232. IFNULL(r.ParentOp, '') AS ParentOp,
  233. IFNULL(r.MilestoneOp, 0) AS MilestoneOp,
  234. IFNULL(r.PackingQty, 0) AS PackingQty,
  235. IFNULL(r.QtyComplete, 0) AS QtyComplete,
  236. IFNULL(r.QtyReject, 0) AS QtyReject,
  237. IFNULL(r.QtyScrap, 0) AS QtyScrap,
  238. r.Status AS Status
  239. FROM WorkOrdRouting r
  240. WHERE {SqlWorkOrdChildMatchDomainOrTenant("r", "m")}
  241. ORDER BY (r.OP + 0) ASC, r.OP ASC, r.Line ASC, r.ColumnNum ASC
  242. """;
  243. var routings = await _db.Ado.SqlQueryAsync<WorkOrderViewRoutingRow>(routingSql, new { WorkOrd = workOrd.Trim(), Domain = domain.Trim() });
  244. string detailSql = $"""
  245. SELECT d.ItemNum, d.Op, d.QtyRequired, IFNULL(d.FrozenBOMQty, 0) AS FrozenBOMQty
  246. FROM WorkOrdDetail d
  247. WHERE {SqlWorkOrdChildMatchDomainOrTenant("d", "m")}
  248. ORDER BY d.LineNum
  249. """;
  250. var details = await _db.Ado.SqlQueryAsync<WorkOrderViewDetailRow>(detailSql, new { WorkOrd = workOrd.Trim(), Domain = domain.Trim() });
  251. return new { master, routings, details };
  252. }
  253. // ══════════════════════════════════════════════════════════════
  254. // 物料 / 工序明细列表(新标签页)
  255. // ══════════════════════════════════════════════════════════════
  256. [DisplayName("工单物料明细列表")]
  257. [HttpGet("scheduling/materials")]
  258. public async Task<object> GetMaterials([FromQuery] string workOrd, [FromQuery] string domain)
  259. {
  260. if (string.IsNullOrWhiteSpace(workOrd) || string.IsNullOrWhiteSpace(domain))
  261. throw Oops.Oh("工单号与公司域名不能为空");
  262. string sql = $"""
  263. SELECT d.RecID, d.ItemNum, d.Op, d.QtyRequired, IFNULL(d.FrozenBOMQty, 0) AS FrozenBOMQty,
  264. IFNULL(d.Location, '') AS Location, IFNULL(d.LotSerial, '') AS LotSerial, d.WorkOrd
  265. FROM WorkOrdDetail d
  266. WHERE {SqlWorkOrdChildMatchDomainOrTenant("d", "m")}
  267. ORDER BY d.LineNum
  268. """;
  269. var list = await _db.Ado.SqlQueryAsync<WorkOrderViewDetailRow>(sql, new { WorkOrd = workOrd.Trim(), Domain = domain.Trim() });
  270. return new { list };
  271. }
  272. [DisplayName("工单工序明细列表")]
  273. [HttpGet("scheduling/routings")]
  274. public async Task<object> GetRoutings([FromQuery] string workOrd, [FromQuery] string domain)
  275. {
  276. if (string.IsNullOrWhiteSpace(workOrd) || string.IsNullOrWhiteSpace(domain))
  277. throw Oops.Oh("工单号与公司域名不能为空");
  278. string sql = $"""
  279. SELECT
  280. r.RecID, r.OP AS Op, r.Descr AS Descr,
  281. IFNULL(r.ParentOp, '') AS ParentOp,
  282. IFNULL(r.MilestoneOp, 0) AS MilestoneOp,
  283. IFNULL(r.PackingQty, 0) AS PackingQty,
  284. IFNULL(r.QtyComplete, 0) AS QtyComplete,
  285. IFNULL(r.QtyReject, 0) AS QtyReject,
  286. IFNULL(r.QtyScrap, 0) AS QtyScrap,
  287. r.Status AS Status, r.WorkOrd,
  288. IFNULL(r.WorkCtr, '') AS WorkCtr,
  289. IFNULL(r.ProdLine, '') AS ProdLine,
  290. IFNULL(r.StdSetupTime, 0) AS StdSetupTime,
  291. IFNULL(r.QtyOrded, 0) AS QtyOrded,
  292. IFNULL(r.ProcessOut, 0) AS ProcessOut,
  293. IFNULL(r.ProcessOutSupp, '') AS ProcessOutSupp,
  294. IFNULL(r.ProcessOutDay, 0) AS ProcessOutDay,
  295. IFNULL(r.Machine, '') AS Machine,
  296. IFNULL(r.ToolCode, '') AS ToolCode,
  297. IFNULL(r.RunCrew, 0) AS RunCrew,
  298. IFNULL(r.Engineer, '') AS Engineer,
  299. IFNULL(r.MachBdnRate, 0) AS MachBdnRate,
  300. IFNULL(r.WorkCode, '') AS WorkCode,
  301. IFNULL(r.Setup, 0) AS Setup,
  302. IFNULL(r.MachinesperOp, 0) AS MachinesperOp,
  303. IFNULL(r.Labor, 0) AS Labor,
  304. IFNULL(r.ChargeCode, '') AS ChargeCode,
  305. IFNULL(r.ItemNum, '') AS ItemNum
  306. FROM WorkOrdRouting r
  307. WHERE {SqlWorkOrdChildMatchDomainOrTenant("r", "m")}
  308. ORDER BY (r.OP + 0) ASC, r.OP ASC, r.Line ASC, r.ColumnNum ASC
  309. """;
  310. var list = await _db.Ado.SqlQueryAsync<WorkOrderViewRoutingRow>(sql, new { WorkOrd = workOrd.Trim(), Domain = domain.Trim() });
  311. return new { list };
  312. }
  313. // ══════════════════════════════════════════════════════════════
  314. // 执行追踪 GET /api/Production/scheduling/trace
  315. // ══════════════════════════════════════════════════════════════
  316. [DisplayName("工单执行追踪")]
  317. [HttpGet("scheduling/trace")]
  318. public async Task<object> GetTrace([FromQuery] string workOrd, [FromQuery] string domain)
  319. {
  320. if (string.IsNullOrWhiteSpace(workOrd) || string.IsNullOrWhiteSpace(domain))
  321. throw Oops.Oh("工单号与公司域名不能为空");
  322. var w = workOrd.Trim();
  323. var d = domain.Trim();
  324. string masterSql = $"""
  325. SELECT
  326. a.WorkOrd,
  327. a.QtyOrded,
  328. a.QtyCompleted,
  329. a.ItemNum,
  330. IFNULL(b.Descr, '') AS ItemName,
  331. IFNULL(b.Descr1, '') AS ItemModel,
  332. DATE(a.OrdDate) AS OrdDate,
  333. DATE(a.DueDate) AS DueDate,
  334. LOWER(a.Status) AS Status
  335. FROM WorkOrdMaster a
  336. LEFT JOIN ItemMaster b ON a.ItemNum = b.ItemNum
  337. WHERE {SqlWorkOrdMasterMatchDomainOrTenantAliased("a")}
  338. LIMIT 1
  339. """;
  340. var master = (await _db.Ado.SqlQueryAsync<WorkOrderTraceMasterRow>(masterSql, new { WorkOrd = w, Domain = d }))
  341. .FirstOrDefault() ?? throw Oops.Oh("工单不存在");
  342. var tab1 = await QueryTraceResourceCheckAsync(w, d);
  343. var tab2 = await QueryTraceScheduleAsync(w, d);
  344. var tab3Po = await QueryTracePoAsync(w, d);
  345. var tab3Pr = await QueryTracePrAsync(w, d);
  346. var tab4 = await QueryTraceNbrAsync(w, d);
  347. var tab5 = await QueryTraceProdReportAsync(w, d);
  348. var tab6 = await QueryTraceQualityAsync(w, d);
  349. var tab7 = await QueryTraceReceiptAsync(w, d);
  350. return new
  351. {
  352. master,
  353. resourceCheck = tab1,
  354. schedule = tab2,
  355. purchaseOrders = tab3Po,
  356. purchaseReqs = tab3Pr,
  357. picking = tab4,
  358. production = tab5,
  359. quality = tab6,
  360. receipt = tab7
  361. };
  362. }
  363. private async Task<List<TraceResourceRow>> QueryTraceResourceCheckAsync(string workOrd, string domain)
  364. {
  365. const string sql = """
  366. SELECT
  367. ROW_NUMBER() OVER (ORDER BY bce.id) AS Sno,
  368. CAST(bce.num AS CHAR) AS Num,
  369. bce.item_number AS ItemNumber,
  370. bce.item_name AS ItemName,
  371. bce.model AS Model,
  372. IFNULL(bce.unit, '') AS Unit,
  373. IFNULL(bce.bom_number, '') AS BomNumber,
  374. DATE_FORMAT(bce.kitting_time, '%Y-%m-%d') AS KittingTime,
  375. CASE bce.erp_cls
  376. WHEN 0 THEN '配置类'
  377. WHEN 1 THEN '自制'
  378. WHEN 2 THEN '委外加工'
  379. WHEN 3 THEN '外购'
  380. WHEN 4 THEN '虚拟件'
  381. ELSE ''
  382. END AS ErpClsName,
  383. CASE WHEN bce.backflush = 1 THEN '是' ELSE '否' END AS Backflush,
  384. CAST(bce.qty AS CHAR) AS Qty,
  385. CAST(bce.needCount AS CHAR) AS NeedCount
  386. FROM b_examine_result ber
  387. INNER JOIN b_bom_child_examine bce ON ber.Id = bce.examine_id AND bce.is_use = 1
  388. WHERE ber.morder_no = @WorkOrd
  389. AND ber.Id = (
  390. SELECT br.Id FROM b_examine_result br
  391. WHERE br.morder_no = @WorkOrd
  392. ORDER BY br.create_time DESC
  393. LIMIT 1
  394. )
  395. ORDER BY bce.id
  396. """;
  397. return await _db.Ado.SqlQueryAsync<TraceResourceRow>(sql, new { WorkOrd = workOrd });
  398. }
  399. private async Task<List<TraceScheduleRow>> QueryTraceScheduleAsync(string workOrd, string domain)
  400. {
  401. const string sql = """
  402. SELECT
  403. ROW_NUMBER() OVER (ORDER BY sch.PlanDate) AS Sno,
  404. sch.WorkOrds AS WorkOrds,
  405. DATE_FORMAT(sch.PlanDate, '%Y-%m-%d') AS WorkDate,
  406. IFNULL(sch.Line, '') AS Line,
  407. IFNULL(sch.Op, '') AS Op,
  408. CAST(IFNULL(sch.OrdQty, 0) AS CHAR) AS WorkQty,
  409. IFNULL(sch.ItemNum, '') AS ItemNum,
  410. IFNULL(im.Descr, '') AS Descr,
  411. IFNULL(im.Descr1, '') AS Descr1,
  412. DATE_FORMAT(sch.CreateTime, '%Y-%m-%d %H:%i:%s') AS CreateTime
  413. FROM PeriodSequenceDet sch
  414. LEFT JOIN ItemMaster im ON sch.ItemNum = im.ItemNum
  415. WHERE sch.WorkOrds = @WorkOrd
  416. AND CAST(sch.`Domain` AS CHAR(64)) = CAST(@Domain AS CHAR(64))
  417. ORDER BY sch.PlanDate
  418. """;
  419. return await _db.Ado.SqlQueryAsync<TraceScheduleRow>(sql, new { WorkOrd = workOrd, Domain = domain });
  420. }
  421. private async Task<List<TracePoRow>> QueryTracePoAsync(string workOrd, string domain)
  422. {
  423. const string sql = """
  424. SELECT
  425. ROW_NUMBER() OVER (ORDER BY t.po_billno) AS Sno,
  426. t.po_billno AS PoBillno,
  427. t.supplier_no AS SupplierNo,
  428. t.supplier_name AS SupplierName,
  429. CAST(t.po_total AS CHAR) AS PoTotal,
  430. IFNULL(t.po_purchaser, '') AS PoPurchaser,
  431. t.state AS State,
  432. IFNULL(DATE_FORMAT(t.po_ssend_date, '%Y-%m-%d'), '') AS PoSsendDate,
  433. IFNULL(DATE_FORMAT(t.create_time, '%Y-%m-%d %H:%i:%s'), '') AS CreateTime
  434. FROM (
  435. SELECT DISTINCT
  436. pm.po_billno,
  437. pm.supplier_no,
  438. pm.supplier_name,
  439. pm.po_total,
  440. pm.po_purchaser,
  441. pm.create_time,
  442. pm.po_ssend_date,
  443. CASE pm.state
  444. WHEN 0 THEN '新增'
  445. WHEN 1 THEN '审核中'
  446. WHEN 2 THEN '同意'
  447. WHEN 3 THEN '关闭'
  448. ELSE CAST(pm.state AS CHAR)
  449. END AS state
  450. FROM srm_po_main pm
  451. LEFT JOIN srm_po_list pl ON pm.id = pl.po_id
  452. LEFT JOIN srm_po_occupy occ ON pl.Id = occ.polist_id
  453. AND CAST(pm.factory_id AS CHAR(64)) = CAST(occ.factory_id AS CHAR(64))
  454. LEFT JOIN mes_morder mo ON mo.id = occ.morder_id
  455. WHERE mo.morder_no = @WorkOrd
  456. AND CAST(mo.factory_id AS CHAR(64)) = CAST(@Domain AS CHAR(64))
  457. AND pm.IsDeleted = 0
  458. ) AS t
  459. ORDER BY t.po_billno
  460. """;
  461. return await _db.Ado.SqlQueryAsync<TracePoRow>(sql, new { WorkOrd = workOrd, Domain = domain });
  462. }
  463. private async Task<List<TracePrRow>> QueryTracePrAsync(string workOrd, string domain)
  464. {
  465. const string sql = """
  466. SELECT
  467. ROW_NUMBER() OVER (ORDER BY pm.pr_billno) AS Sno,
  468. pm.pr_billno AS PrBillno,
  469. IFNULL(ic.number, '') AS Number,
  470. IFNULL(pm.icitem_name, '') AS IcitemName,
  471. IFNULL(ic.model, '') AS Model,
  472. IFNULL(pm.pr_purchasenumber, '') AS PrPurchasenumber,
  473. IFNULL(pm.pr_purchasename, '') AS PrPurchasename,
  474. CAST(IFNULL(pm.pr_aqty, 0) AS CHAR) AS PrAqty,
  475. IFNULL(DATE_FORMAT(pm.pr_ssend_date, '%Y-%m-%d'), '') AS PrSsendDate,
  476. IFNULL(DATE_FORMAT(pm.pr_sarrive_date, '%Y-%m-%d'), '') AS PrSarriveDate,
  477. IFNULL(pm.pr_unit, '') AS PrUnit,
  478. IFNULL(pm.pr_purchaser, '') AS PrPurchaser
  479. FROM srm_pr_main pm
  480. LEFT JOIN srm_po_occupy occ ON pm.Id = occ.polist_id
  481. AND CAST(pm.factory_id AS CHAR(64)) = CAST(occ.factory_id AS CHAR(64))
  482. LEFT JOIN ic_item ic ON pm.icitem_id = ic.Id
  483. LEFT JOIN mes_morder mo ON mo.id = occ.morder_id
  484. WHERE mo.morder_no = @WorkOrd
  485. AND CAST(mo.factory_id AS CHAR(64)) = CAST(@Domain AS CHAR(64))
  486. AND IFNULL(pm.state, 0) <> 0
  487. AND pm.IsDeleted = 0
  488. ORDER BY pm.pr_billno
  489. """;
  490. return await _db.Ado.SqlQueryAsync<TracePrRow>(sql, new { WorkOrd = workOrd, Domain = domain });
  491. }
  492. private async Task<List<TraceNbrRow>> QueryTraceNbrAsync(string workOrd, string domain)
  493. {
  494. const string sql = """
  495. SELECT
  496. CAST(nd.Line AS CHAR) AS Line,
  497. nm.Nbr AS Nbr,
  498. nd.ItemNum AS ItemNum,
  499. IFNULL(im.Descr, '') AS Descr,
  500. IFNULL(im.Descr1, '') AS Descr1,
  501. IFNULL(nd.LocationFrom, '') AS LocationFrom,
  502. IFNULL(nd.LocationTo, '') AS LocationTo,
  503. CAST(IFNULL(nd.CurrQtyOpened, 0) AS CHAR) AS CurrQtyOpened,
  504. CAST(IFNULL(nd.QtyOrd, 0) AS CHAR) AS QtyOrd,
  505. CAST(IFNULL(nd.QtyFrom, 0) AS CHAR) AS QtyFrom,
  506. CAST(IFNULL(nd.QtyRec, 0) AS CHAR) AS QtyRec,
  507. IFNULL(nd.UM, '') AS Unit
  508. FROM NbrDetail nd
  509. LEFT JOIN NbrMaster nm ON nd.nbr = nm.nbr
  510. LEFT JOIN ItemMaster im ON nd.ItemNum = im.ItemNum
  511. WHERE nm.WorkOrd = @WorkOrd
  512. AND nm.Type = 'SM'
  513. AND CAST(nm.`Domain` AS CHAR(64)) = CAST(@Domain AS CHAR(64))
  514. ORDER BY nm.Nbr
  515. """;
  516. return await _db.Ado.SqlQueryAsync<TraceNbrRow>(sql, new { WorkOrd = workOrd, Domain = domain });
  517. }
  518. private async Task<List<TraceProdRow>> QueryTraceProdReportAsync(string workOrd, string domain)
  519. {
  520. const string sql = """
  521. SELECT
  522. ROW_NUMBER() OVER (ORDER BY ote.ProdDate DESC) AS Sno,
  523. ote.WorkOrd AS WorkOrd,
  524. IFNULL(ote.ItemNum, '') AS ItemNum,
  525. IFNULL(im.Descr, '') AS Descr,
  526. IFNULL(im.Descr1, '') AS Descr1,
  527. IFNULL(im.Um, '') AS Um,
  528. CAST(IFNULL(ote.QtyCompleted, 0) AS CHAR) AS QtyCompleted,
  529. CAST(IFNULL(ote.QtyReject, 0) AS CHAR) AS QtyReject,
  530. CAST(IFNULL(ote.QtyScrapped, 0) AS CHAR) AS QtyScrapped,
  531. IFNULL(DATE_FORMAT(ote.ProdDate, '%Y-%m-%d %H:%i:%s'), '') AS ProdDate,
  532. IFNULL(ote.Op, '') AS Op,
  533. IFNULL(ote.Name, '') AS Name
  534. FROM OpTransEmployee ote
  535. LEFT JOIN ItemMaster im ON ote.ItemNum = im.ItemNum
  536. WHERE ote.WorkOrd = @WorkOrd
  537. ORDER BY ote.ProdDate DESC
  538. """;
  539. return await _db.Ado.SqlQueryAsync<TraceProdRow>(sql, new { WorkOrd = workOrd });
  540. }
  541. private async Task<List<TraceQualityRow>> QueryTraceQualityAsync(string workOrd, string domain)
  542. {
  543. const string sql = """
  544. SELECT
  545. ROW_NUMBER() OVER (ORDER BY mo.id) AS Sno,
  546. IFNULL(im.Um, '') AS Unit,
  547. IFNULL(mo.morder_no, '') AS MorderNo,
  548. CAST('' AS CHAR) AS WorkNumber,
  549. CAST('' AS CHAR) AS MorderProductionNumber,
  550. CAST('' AS CHAR) AS InspectionNumber,
  551. CAST('' AS CHAR) AS QualifiedNumber,
  552. CAST('' AS CHAR) AS InventoryNumber,
  553. IFNULL(mo.moentry_wrkcname, '') AS MoentryWrkcname,
  554. CAST('' AS CHAR) AS PlannerStartDate,
  555. CAST('' AS CHAR) AS PlannerEndDate,
  556. IFNULL(mo.moentry_prdname, '') AS MoentryPrdname,
  557. CAST('' AS CHAR) AS MorderFstate
  558. FROM mes_morder mo
  559. LEFT JOIN ItemMaster im ON mo.product_code = im.ItemNum
  560. WHERE mo.morder_no = @WorkOrd
  561. AND CAST(mo.factory_id AS CHAR(64)) = CAST(@Domain AS CHAR(64))
  562. AND mo.IsDeleted = 0
  563. ORDER BY mo.id
  564. """;
  565. return await _db.Ado.SqlQueryAsync<TraceQualityRow>(sql, new { WorkOrd = workOrd, Domain = domain });
  566. }
  567. private async Task<List<TraceReceiptRow>> QueryTraceReceiptAsync(string workOrd, string domain)
  568. {
  569. const string sql = """
  570. SELECT
  571. ROW_NUMBER() OVER (ORDER BY c.WorkOrd) AS Sno,
  572. CASE
  573. WHEN LOWER(c.Status) = 'r' THEN '下达'
  574. WHEN LOWER(c.Status) = 'c' THEN '关闭'
  575. WHEN LOWER(c.Status) = 'w' THEN '投产'
  576. ELSE IFNULL(c.Status, '')
  577. END AS Status,
  578. c.WorkOrd AS WorkOrd,
  579. IFNULL(im.Um, '') AS UM,
  580. CAST(IFNULL(c.QtyOrded, 0) AS CHAR) AS QtyOrded,
  581. CAST(IFNULL(compp.CompQty, 0) AS CHAR) AS CompQty,
  582. CAST('' AS CHAR) AS InspectionNumber,
  583. CAST('' AS CHAR) AS QualifiedNumber,
  584. CAST(IFNULL(a.QtyChange, 0) AS CHAR) AS QtyChangeAdvance,
  585. IFNULL(DATE_FORMAT(c.OrdDate, '%Y-%m-%d'), '') AS OrdDate,
  586. IFNULL(DATE_FORMAT(c.DueDate, '%Y-%m-%d'), '') AS DueDate
  587. FROM WorkOrdMaster c
  588. LEFT JOIN InvTransHist a ON a.WorkOrd = c.WorkOrd AND a.TransType = 'rct-wo' AND IFNULL(a.QtyChange, 0) > 0
  589. LEFT JOIN ItemMaster im ON c.ItemNum = im.ItemNum AND im.`Domain` = c.`Domain`
  590. LEFT JOIN (
  591. SELECT WorkOrds, SUM(IFNULL(CompQty, 0)) AS CompQty
  592. FROM PeriodSequenceDet
  593. GROUP BY WorkOrds
  594. ) compp ON c.WorkOrd = compp.WorkOrds
  595. LEFT JOIN mes_morder mo ON mo.morder_no = c.WorkOrd AND mo.IsDeleted = 0
  596. AND CAST(mo.factory_id AS CHAR(64)) = CAST(c.`Domain` AS CHAR(64))
  597. WHERE c.WorkOrd = @WorkOrd AND c.`Domain` = @Domain
  598. """;
  599. return await _db.Ado.SqlQueryAsync<TraceReceiptRow>(sql, new { WorkOrd = workOrd, Domain = domain });
  600. }
  601. // ══════════════════════════════════════════════════════════════
  602. // 保存 POST /api/Production/scheduling/save
  603. // ══════════════════════════════════════════════════════════════
  604. [DisplayName("保存工单(数量/批次/优先级/加急)")]
  605. [HttpPost("scheduling/save")]
  606. public async Task<object> Save([FromBody] WorkOrderSchedulingSaveInput input)
  607. {
  608. var account = _userManager.Account ?? "system";
  609. var workOrd = input.WorkOrd.Trim();
  610. var domain = input.Domain.Trim();
  611. // ── 1. 获取旧的 QtyOrded,用于判断数量是否变更 ──
  612. var oldQtyOrded = await _db.Ado.GetDecimalAsync(
  613. $"SELECT IFNULL(QtyOrded, 0) FROM WorkOrdMaster WHERE {SqlWorkOrdMasterMatchDomainOrTenant}",
  614. new SugarParameter("@WorkOrd", workOrd),
  615. new SugarParameter("@Domain", domain));
  616. var newQtyOrded = input.QtyOrded;
  617. var qtyChanged = newQtyOrded.HasValue && oldQtyOrded > 0 && newQtyOrded.Value != oldQtyOrded;
  618. // ── 2. 更新 WorkOrdMaster ──
  619. var pars = new List<SugarParameter>
  620. {
  621. new("@QtyOrded", input.QtyOrded ?? (object)DBNull.Value),
  622. new("@Priority", input.Priority ?? (object)DBNull.Value),
  623. new("@LotSerial", string.IsNullOrWhiteSpace(input.LotSerial) ? DBNull.Value : input.LotSerial.Trim()),
  624. new("@Urgent", input.Urgent ?? (object)DBNull.Value),
  625. new("@UpdateUser", account),
  626. new("@UpdateTime", DateTime.Now),
  627. new("@WorkOrd", workOrd),
  628. new("@Domain", domain)
  629. };
  630. var n = await _db.Ado.ExecuteCommandAsync(
  631. $"""
  632. UPDATE WorkOrdMaster
  633. SET QtyOrded = COALESCE(@QtyOrded, QtyOrded),
  634. Priority = COALESCE(@Priority, Priority),
  635. LotSerial = COALESCE(@LotSerial, LotSerial),
  636. Urgent = COALESCE(@Urgent, Urgent),
  637. UpdateUser = @UpdateUser,
  638. UpdateTime = @UpdateTime
  639. WHERE {SqlWorkOrdMasterMatchDomainOrTenant}
  640. """,
  641. pars);
  642. if (n == 0)
  643. throw Oops.Oh("工单不存在或未更新");
  644. // ── 3. 数量变更时同步子表 ──
  645. if (qtyChanged)
  646. {
  647. var ratio = newQtyOrded!.Value / oldQtyOrded;
  648. var now = DateTime.Now;
  649. // 3a. WorkOrdDetail: 按比例调整 QtyRequired
  650. await _db.Ado.ExecuteCommandAsync(
  651. """
  652. UPDATE WorkOrdDetail
  653. SET QtyRequired = ROUND(QtyRequired * @Ratio, 6),
  654. UpdateUser = @User, UpdateTime = @Now
  655. WHERE tenant_id = @TenantId AND WorkOrd = @WorkOrd
  656. AND IFNULL(IsActive, 0) = 1
  657. """,
  658. new SugarParameter("@Ratio", ratio),
  659. new SugarParameter("@User", account),
  660. new SugarParameter("@Now", now),
  661. new SugarParameter("@TenantId", domain),
  662. new SugarParameter("@WorkOrd", workOrd));
  663. // 3b. WorkOrdRouting: 同步 QtyOrded
  664. await _db.Ado.ExecuteCommandAsync(
  665. """
  666. UPDATE WorkOrdRouting
  667. SET QtyOrded = @QtyOrded,
  668. UpdateUser = @User, UpdateTime = @Now
  669. WHERE tenant_id = @TenantId AND WorkOrd = @WorkOrd
  670. AND IFNULL(IsActive, 0) = 1
  671. """,
  672. new SugarParameter("@QtyOrded", newQtyOrded.Value),
  673. new SugarParameter("@User", account),
  674. new SugarParameter("@Now", now),
  675. new SugarParameter("@TenantId", domain),
  676. new SugarParameter("@WorkOrd", workOrd));
  677. // 3c. NbrDetail: 按比例调整 QtyOrd 和 CurrQtyOpened(仅下达/投产/暂停状态)
  678. var woStatus = await _db.Ado.GetStringAsync(
  679. """
  680. SELECT IFNULL(LOWER(TRIM(Status)), '') FROM WorkOrdMaster
  681. WHERE tenant_id = @TenantId AND WorkOrd = @WorkOrd LIMIT 1
  682. """,
  683. new SugarParameter("@TenantId", domain),
  684. new SugarParameter("@WorkOrd", workOrd));
  685. if (woStatus is "r" or "w" or "s")
  686. {
  687. await _db.Ado.ExecuteCommandAsync(
  688. """
  689. UPDATE NbrDetail
  690. SET QtyOrd = ROUND(QtyOrd * @Ratio, 6),
  691. CurrQtyOpened = ROUND(CurrQtyOpened * @Ratio, 6),
  692. UpdateUser = @User, UpdateTime = @Now
  693. WHERE tenant_id = @TenantId AND Type = 'SM'
  694. AND Nbr IN (
  695. SELECT Nbr FROM NbrMaster
  696. WHERE tenant_id = @TenantId AND WorkOrd = @WorkOrd AND Type = 'SM'
  697. AND IFNULL(IsActive, 0) = 1
  698. )
  699. AND IFNULL(IsActive, 0) = 1
  700. AND IFNULL(Status, '') <> 'C'
  701. """,
  702. new SugarParameter("@Ratio", ratio),
  703. new SugarParameter("@User", account),
  704. new SugarParameter("@Now", now),
  705. new SugarParameter("@TenantId", domain),
  706. new SugarParameter("@WorkOrd", workOrd));
  707. }
  708. }
  709. return new { message = "保存成功" };
  710. }
  711. // ══════════════════════════════════════════════════════════════
  712. // 状态 PATCH POST /api/Production/scheduling/status
  713. // ══════════════════════════════════════════════════════════════
  714. [DisplayName("更新工单状态")]
  715. [HttpPost("scheduling/status")]
  716. public async Task<object> PatchStatus([FromBody] WorkOrderStatusPatchInput input)
  717. {
  718. var account = _userManager.Account ?? "system";
  719. var status = input.Status.Trim().ToLowerInvariant();
  720. var pars = new List<SugarParameter>
  721. {
  722. new("@Status", status),
  723. new("@UpdateUser", account),
  724. new("@UpdateTime", DateTime.Now),
  725. new("@Id", input.Id),
  726. new("@Domain", input.Domain.Trim())
  727. };
  728. var n = await _db.Ado.ExecuteCommandAsync(
  729. $"""
  730. UPDATE WorkOrdMaster
  731. SET Status = @Status,
  732. UpdateUser = @UpdateUser,
  733. UpdateTime = @UpdateTime
  734. WHERE {SqlWorkOrdMasterMatchRecIdAndDomainOrTenant}
  735. """,
  736. pars);
  737. if (n == 0)
  738. throw Oops.Oh("工单不存在或未更新");
  739. // 双模式出站:状态确认类变更写 Outbox(WMS_API/MES 占位)
  740. if (status is "released" or "confirm" or "confirmed" or "c" or "r")
  741. await TryEnqueueScheduleDispatchOutboxAsync(input.Id, input.Domain.Trim(), status);
  742. return new { message = "状态已更新" };
  743. }
  744. private async Task TryEnqueueScheduleDispatchOutboxAsync(long recId, string domain, string status)
  745. {
  746. try
  747. {
  748. var row = await _db.Ado.SqlQuerySingleAsync<WorkOrderInfo>(
  749. $"""
  750. SELECT WorkOrd, IFNULL(NULLIF(TRIM(`Domain`),''), CAST(IFNULL(tenant_id,0) AS CHAR)) AS Domain
  751. FROM WorkOrdMaster
  752. WHERE {SqlWorkOrdMasterMatchRecIdAndDomainOrTenant}
  753. LIMIT 1
  754. """,
  755. new List<SugarParameter>
  756. {
  757. new("@Id", recId),
  758. new("@Domain", domain)
  759. });
  760. if (row == null || string.IsNullOrWhiteSpace(row.WorkOrd)) return;
  761. string idem = $"{row.Domain}#{row.WorkOrd}#{status}";
  762. long tenantId = _userManager.TenantId;
  763. var payload = JsonSerializer.Serialize(new
  764. {
  765. path = "/mes/schedule/dispatch",
  766. method = "POST",
  767. body = new
  768. {
  769. domain = row.Domain,
  770. workOrd = row.WorkOrd,
  771. status,
  772. action = "S2_SCHEDULE_DISPATCH"
  773. }
  774. });
  775. var exists = await _db.Ado.GetIntAsync(
  776. "SELECT COUNT(1) FROM mdp_outbox WHERE tenant_id=@tid AND target_source_code='WMS_API' AND action_code='S2_SCHEDULE_DISPATCH' AND idem_key=@idem",
  777. new List<SugarParameter> { new("@tid", tenantId), new("@idem", idem) }) > 0;
  778. if (exists) return;
  779. await _db.Insertable(new MdpOutbox
  780. {
  781. TenantId = tenantId,
  782. TargetSourceCode = "WMS_API",
  783. ActionCode = "S2_SCHEDULE_DISPATCH",
  784. IdemKey = idem,
  785. PayloadJson = payload,
  786. Status = 0,
  787. RetryCount = 0,
  788. CreateTime = DateTime.Now,
  789. UpdateTime = DateTime.Now
  790. }).ExecuteCommandAsync();
  791. }
  792. catch { /* Outbox 失败不影响状态更新 */ }
  793. }
  794. // ══════════════════════════════════════════════════════════════
  795. // 工单关闭 POST /api/Production/scheduling/close
  796. // ══════════════════════════════════════════════════════════════
  797. [DisplayName("工单关闭")]
  798. [HttpPost("scheduling/close")]
  799. public async Task<object> Close([FromBody] WorkOrderCloseInput input)
  800. {
  801. var ids = input.Ids.Trim();
  802. if (string.IsNullOrWhiteSpace(ids))
  803. throw Oops.Oh("工单主键不能为空");
  804. try
  805. {
  806. // 开启事务
  807. _db.Ado.BeginTran();
  808. // 根据 RecID 获取工单号和域信息
  809. // 注意:存储过程 pr_MES_CloseWorkOrders 需要两个参数:域和工单号(竖线分隔)
  810. // 但当前调用只传递 RecID,因此需要根据 RecID 查询对应的工单和域
  811. var workOrders = await _db.Ado.SqlQueryAsync<WorkOrderInfo>(
  812. "SELECT WorkOrd AS WorkOrd, COALESCE(NULLIF(TRIM(`Domain`), ''), CAST(tenant_id AS CHAR(20))) AS Domain FROM WorkOrdMaster WHERE RecID IN (" + BuildInClause(ids) + ")",
  813. GetInParameters(ids));
  814. if (!workOrders.Any())
  815. throw Oops.Oh("未找到对应的工单");
  816. // 为每个域执行关闭操作
  817. var domains = workOrders.Select(x => x.Domain).Distinct();
  818. foreach (var domain in domains)
  819. {
  820. // 获取该域的工单号列表(竖线分隔)
  821. var domainWorkOrds = string.Join("|", workOrders.Where(x => x.Domain == domain).Select(x => x.WorkOrd));
  822. // 执行关闭逻辑(模拟存储过程)
  823. await ExecuteCloseWorkOrders(domain, domainWorkOrds);
  824. }
  825. _db.Ado.CommitTran();
  826. return new { message = "工单关闭成功" };
  827. }
  828. catch (Exception ex)
  829. {
  830. _db.Ado.RollbackTran();
  831. return new { message = $"工单关闭失败: {ex.Message}" };
  832. }
  833. }
  834. /// <summary>
  835. /// 构建 IN 子句参数
  836. /// </summary>
  837. private string BuildInClause(string ids)
  838. {
  839. var idList = ids.Split(',').Select(x => x.Trim()).Where(x => !string.IsNullOrWhiteSpace(x));
  840. return string.Join(",", idList.Select((_, i) => $"@Id{i}"));
  841. }
  842. /// <summary>
  843. /// 获取 IN 子句参数
  844. /// </summary>
  845. private SugarParameter[] GetInParameters(string ids)
  846. {
  847. var idList = ids.Split(',').Select(x => x.Trim()).Where(x => !string.IsNullOrWhiteSpace(x));
  848. return idList.Select((id, i) => new SugarParameter($"@Id{i}", id)).ToArray();
  849. }
  850. /// <summary>
  851. /// 执行关闭工单逻辑(模拟存储过程 pr_MES_CloseWorkOrders)
  852. /// </summary>
  853. private async Task ExecuteCloseWorkOrders(string domain, string workOrds)
  854. {
  855. if (string.IsNullOrWhiteSpace(workOrds))
  856. return;
  857. // 存储过程使用竖线分隔,但FIND_IN_SET需要逗号分隔
  858. var workOrdsForFindInSet = workOrds.Replace("|", ",");
  859. // 1. 更新工单状态为C
  860. await _db.Ado.ExecuteCommandAsync(
  861. "UPDATE WorkOrdMaster SET Status = 'C', UpdateUser = 'proc' WHERE tenant_id = @Domain AND FIND_IN_SET(WorkOrd, @WorkOrds) > 0",
  862. new SugarParameter("@Domain", domain),
  863. new SugarParameter("@WorkOrds", workOrdsForFindInSet));
  864. // 2. 更新领料单状态为C
  865. await _db.Ado.ExecuteCommandAsync(
  866. "UPDATE NbrMaster SET Status = 'C' WHERE Type = 'SM' AND tenant_id = @Domain AND FIND_IN_SET(WorkOrd, @WorkOrds) > 0",
  867. new SugarParameter("@Domain", domain),
  868. new SugarParameter("@WorkOrds", workOrdsForFindInSet));
  869. // 3. 更新领料单明细状态为C
  870. await _db.Ado.ExecuteCommandAsync(
  871. "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)",
  872. new SugarParameter("@Domain", domain),
  873. new SugarParameter("@WorkOrds", workOrdsForFindInSet));
  874. // 4. 更新退料单状态为C
  875. await _db.Ado.ExecuteCommandAsync(
  876. "UPDATE NbrMaster SET Status = 'C' WHERE Type = 'WOD' AND tenant_id = @Domain AND FIND_IN_SET(WorkOrd, @WorkOrds) > 0",
  877. new SugarParameter("@Domain", domain),
  878. new SugarParameter("@WorkOrds", workOrdsForFindInSet));
  879. // 5. 更新退料单明细状态为C
  880. await _db.Ado.ExecuteCommandAsync(
  881. "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)",
  882. new SugarParameter("@Domain", domain),
  883. new SugarParameter("@WorkOrds", workOrdsForFindInSet));
  884. // 6. 删除库存占用
  885. await _db.Ado.ExecuteCommandAsync(
  886. "DELETE FROM ic_item_stockoccupy WHERE tenant_id = @Domain AND FIND_IN_SET(morder_mo, @WorkOrds) > 0",
  887. new SugarParameter("@Domain", domain),
  888. new SugarParameter("@WorkOrds", workOrdsForFindInSet));
  889. // 7. 删除PO占用
  890. await _db.Ado.ExecuteCommandAsync(
  891. "DELETE FROM srm_po_occupy WHERE tenant_id = @Domain AND FIND_IN_SET(morder_mo, @WorkOrds) > 0",
  892. new SugarParameter("@Domain", domain),
  893. new SugarParameter("@WorkOrds", workOrdsForFindInSet));
  894. // 8. 删除工单占用
  895. await _db.Ado.ExecuteCommandAsync(
  896. "DELETE FROM mes_mooccupy WHERE tenant_id = @Domain AND FIND_IN_SET(moo_mo, @WorkOrds) > 0",
  897. new SugarParameter("@Domain", domain),
  898. new SugarParameter("@WorkOrds", workOrdsForFindInSet));
  899. }
  900. // ══════════════════════════════════════════════════════════════
  901. // 同步工艺路线 POST /api/Production/scheduling/sync-routing
  902. // ══════════════════════════════════════════════════════════════
  903. [DisplayName("同步工艺路线")]
  904. [HttpPost("scheduling/sync-routing")]
  905. public async Task<object> SyncRouting([FromBody] WorkOrderKeyInput input)
  906. {
  907. var workOrd = input.WorkOrd.Trim();
  908. var domain = input.Domain.Trim();
  909. var createUser = (_userManager.Account ?? "system").Trim();
  910. if (createUser.Length > 24)
  911. createUser = createUser[..24];
  912. var pars = new List<SugarParameter>
  913. {
  914. new("@WorkOrd", workOrd),
  915. new("@Domain", domain),
  916. new("@CreateUser", createUser)
  917. };
  918. await _db.Ado.BeginTranAsync();
  919. try
  920. {
  921. await _db.Ado.ExecuteCommandAsync(
  922. $"""
  923. DELETE FROM WorkOrdRouting rr
  924. WHERE {SqlWorkOrdChildMatchDomainOrTenant("rr", "m")}
  925. """,
  926. pars);
  927. // 业务口径:由 RoutingOpDetail + ProdLineDetail 重算工单工艺路线(MySQL)。
  928. // 与历史脚本一致:DELETE 仍按工单 + 域/租户匹配,避免误删其它域数据。
  929. await _db.Ado.ExecuteCommandAsync(
  930. $"""
  931. INSERT INTO WorkOrdRouting (
  932. `Descr`, `Domain`, `ChargeCode`, `Machine`, `RunCrew`, `MilestoneOp`, `OP`, `StdOp`, `ItemNum`, `WorkCtr`,
  933. `Ufld1`, `Ufld3`, `Setup`, `MachinesperOp`, `Labor`, `RunTime`, `MachBdnRate`, `WorkCode`, `StdSetupTime`, `Engineer`,
  934. `WorkOrd`, `WorkOrdMasterRecID`, `ERPfld1`, `QtyOrded`, `ProcessOut`, `ProcessOutDay`, `ProcessOutSupp`,
  935. `IsActive`, `Status`, `ProdLine`, `CreateTime`, `CreateUser`, `tenant_id`,`CommentIndex`,`WaitTime`,`QtyComplete`,`ParentOp`
  936. )
  937. SELECT
  938. a.`Descr`,
  939. LEFT(TRIM(COALESCE(NULLIF(TRIM(IFNULL(w.`Domain`, '')), ''), NULLIF(TRIM(IFNULL(a.`Domain`, '')), ''), ' ')), 8),
  940. '',
  941. b.`InternalEquipmentCode`,
  942. IFNULL(b.`StandardStaffCount`, 0),
  943. CAST(IFNULL(a.`MilestoneOp`, 0) AS UNSIGNED),
  944. IFNULL(a.`Op`, 0),
  945. a.`StdOp`,
  946. a.`RoutingCode`,
  947. LEFT(IFNULL(b.`Site`, ''), 8),
  948. '',
  949. '',
  950. IFNULL(a.`UDeci1`, 0),
  951. IFNULL(a.`UDeci2`, 0),
  952. IFNULL(a.`UDeci3`, 0),
  953. IFNULL(a.`UDeci3`, 0) / 3600.0,
  954. IFNULL(b.`Rate`, 0),
  955. b.`OpType`,
  956. IFNULL(b.`SetupTime`, 0),
  957. b.`SkillNo`,
  958. w.`WorkOrd`,
  959. w.`RecID`,
  960. w.`ERPfld1`,
  961. IFNULL(w.`QtyOrded`, 0),
  962. CAST(IFNULL(a.`UDeci5`, 0) AS SIGNED),
  963. IFNULL(a.`ProcessOutDay`, 0),
  964. a.`ProcessOutSupp`,
  965. b'1',
  966. 'r',
  967. LEFT(IFNULL(b.`Line`, ''), 8),
  968. NOW(3),
  969. @CreateUser,
  970. w.`tenant_id`,CAST(IFNULL(a.`MilestoneOp`, 0) AS UNSIGNED),0,0,0
  971. FROM WorkOrdMaster w
  972. LEFT JOIN RoutingOpDetail a ON w.`ItemNum` = a.`RoutingCode`
  973. LEFT JOIN ProdLineDetail b ON a.`RoutingCode` = b.`Part` AND a.`Op` = b.`Op`
  974. WHERE w.`WorkOrd` = @WorkOrd
  975. AND {SqlWorkOrdMasterMatchDomainOrTenantAliased("w")}
  976. AND IFNULL(a.`IsActive`, 0) = 1
  977. AND a.`MilestoneOp` IS NOT NULL
  978. """,
  979. pars);
  980. await _db.Ado.CommitTranAsync();
  981. }
  982. catch (Exception ex)
  983. {
  984. await _db.Ado.RollbackTranAsync();
  985. throw Oops.Oh($"同步工艺路线失败:{ex.Message}");
  986. }
  987. return new { message = "同步工艺路线已执行" };
  988. }
  989. // ══════════════════════════════════════════════════════════════
  990. // 下拉数据 GET endpoints
  991. // ══════════════════════════════════════════════════════════════
  992. /// <summary>
  993. /// 获取工单下拉选项(状态为空/P/R/W/S 且类型为空/RW/TEST/TCN)
  994. /// </summary>
  995. [DisplayName("工单下拉选项")]
  996. [HttpGet("scheduling/work-ord-options")]
  997. public async Task<object> GetWorkOrdOptions()
  998. {
  999. var tenantId = _userManager.TenantId;
  1000. var list = await _db.Ado.SqlQueryAsync<dynamic>(
  1001. """
  1002. SELECT WorkOrd FROM WorkOrdMaster
  1003. WHERE UPPER(IFNULL(Status, '')) IN ('', 'P', 'R', 'W', 'S')
  1004. AND UPPER(IFNULL(Typed, '')) IN ('', 'RW', 'TEST', 'TCN')
  1005. AND tenant_id = @TenantId
  1006. ORDER BY WorkOrd DESC
  1007. """,
  1008. new { TenantId = tenantId });
  1009. return new { list };
  1010. }
  1011. /// <summary>
  1012. /// 获取工作中心下拉选项
  1013. /// </summary>
  1014. [DisplayName("工作中心下拉选项")]
  1015. [HttpGet("scheduling/work-ctr-options")]
  1016. public async Task<object> GetWorkCtrOptions()
  1017. {
  1018. var tenantId = _userManager.TenantId;
  1019. var list = await _db.Ado.SqlQueryAsync<dynamic>(
  1020. """
  1021. SELECT WorkCtr, IFNULL(Descr, '') AS Descr
  1022. FROM WorkCtrMaster
  1023. WHERE tenant_id = @TenantId
  1024. ORDER BY WorkCtr
  1025. """,
  1026. new { TenantId = tenantId });
  1027. return new { list };
  1028. }
  1029. /// <summary>
  1030. /// 获取生产线下拉选项
  1031. /// </summary>
  1032. [DisplayName("生产线下拉选项")]
  1033. [HttpGet("scheduling/line-options")]
  1034. public async Task<object> GetLineOptions()
  1035. {
  1036. var tenantId = _userManager.TenantId;
  1037. var list = await _db.Ado.SqlQueryAsync<dynamic>(
  1038. """
  1039. SELECT Line, IFNULL(`Describe`, '') AS `Describe`
  1040. FROM LineMaster
  1041. WHERE tenant_id = @TenantId
  1042. ORDER BY Line
  1043. """,
  1044. new { TenantId = tenantId });
  1045. return new { list };
  1046. }
  1047. /// <summary>
  1048. /// 获取供应商下拉选项
  1049. /// </summary>
  1050. [DisplayName("供应商下拉选项")]
  1051. [HttpGet("scheduling/supp-options")]
  1052. public async Task<object> GetSuppOptions()
  1053. {
  1054. var tenantId = _userManager.TenantId;
  1055. var list = await _db.Ado.SqlQueryAsync<dynamic>(
  1056. """
  1057. SELECT Supp, TRIM(CONCAT(Supp, ' ', IFNULL(SortName, ''))) AS SuppName
  1058. FROM SuppMaster
  1059. WHERE tenant_id = @TenantId
  1060. ORDER BY Supp
  1061. """,
  1062. new { TenantId = tenantId });
  1063. return new { list };
  1064. }
  1065. // ══════════════════════════════════════════════════════════════
  1066. // 工单物料明细 CRUD
  1067. // ══════════════════════════════════════════════════════════════
  1068. /// <summary>
  1069. /// 保存工单物料明细(新增/编辑)
  1070. /// </summary>
  1071. [DisplayName("保存工单物料明细")]
  1072. [HttpPost("scheduling/material-save")]
  1073. public async Task<object> SaveMaterialDetail([FromBody] WorkOrdDetailSaveInput input)
  1074. {
  1075. if (string.IsNullOrWhiteSpace(input.WorkOrd))
  1076. throw Oops.Oh("工单编号不能为空");
  1077. if (string.IsNullOrWhiteSpace(input.ItemNum))
  1078. throw Oops.Oh("物料号不能为空");
  1079. var tenantId = _userManager.TenantId;
  1080. var account = _userManager.Account ?? "system";
  1081. var now = DateTime.Now;
  1082. var workOrd = input.WorkOrd.Trim();
  1083. // 获取工单主记录信息和域
  1084. var master = await _db.Ado.SqlQueryAsync<dynamic>(
  1085. $"""
  1086. SELECT RecID, UPPER(IFNULL(Status, '')) AS Status, IFNULL(`Domain`, '') AS Domain
  1087. FROM WorkOrdMaster
  1088. WHERE WorkOrd = @WorkOrd AND tenant_id = @TenantId
  1089. LIMIT 1
  1090. """,
  1091. new { WorkOrd = workOrd, TenantId = tenantId });
  1092. if (master == null || master.Count == 0)
  1093. throw Oops.Oh("工单不存在");
  1094. var masterRow = master[0];
  1095. var masterRecId = (long)masterRow.RecID;
  1096. var woStatus = (string)masterRow.Status;
  1097. var domain = (string)masterRow.Domain ?? "";
  1098. // 查找物料是否存在于ItemMaster
  1099. var itemExists = await _db.Ado.SqlQueryAsync<int>(
  1100. """
  1101. SELECT COUNT(1) FROM ItemMaster
  1102. WHERE ItemNum = @ItemNum AND tenant_id = @TenantId
  1103. """,
  1104. new { ItemNum = input.ItemNum.Trim(), TenantId = tenantId });
  1105. if (itemExists[0] == 0)
  1106. throw Oops.Oh($"物料 {input.ItemNum} 在基础数据中不存在,不能新增");
  1107. // 查找物料的Descr和Unit
  1108. var itemInfo = await _db.Ado.SqlQueryAsync<dynamic>(
  1109. """
  1110. SELECT IFNULL(Descr, '') AS ItemName, IFNULL(UM, '') AS UM
  1111. FROM ItemMaster
  1112. WHERE ItemNum = @ItemNum AND tenant_id = @TenantId
  1113. LIMIT 1
  1114. """,
  1115. new { ItemNum = input.ItemNum.Trim(), TenantId = tenantId });
  1116. var itemName = itemInfo.Count > 0 ? (string)itemInfo[0].ItemName : "";
  1117. var um = itemInfo.Count > 0 ? (string)itemInfo[0].UM : "";
  1118. var isP = woStatus == "P";
  1119. if (input.RecID == 0)
  1120. {
  1121. // ── 新增 ──
  1122. // 检查工单中是否已有此物料
  1123. var dupCheck = await _db.Ado.SqlQueryAsync<int>(
  1124. """
  1125. SELECT COUNT(1) FROM WorkOrdDetail
  1126. WHERE WorkOrd = @WorkOrd AND ItemNum = @ItemNum
  1127. AND tenant_id = @TenantId
  1128. """,
  1129. new { WorkOrd = workOrd, ItemNum = input.ItemNum.Trim(), TenantId = tenantId });
  1130. if (dupCheck[0] > 0)
  1131. throw Oops.Oh($"工单 {workOrd} 中已有物料 {input.ItemNum},不能重复新增");
  1132. // 获取下一个RecID
  1133. var nextIdRows = await _db.Ado.SqlQueryAsync<long>(
  1134. "SELECT IFNULL(MAX(RecID), 0) + 1 FROM WorkOrdDetail");
  1135. var newRecId = nextIdRows[0];
  1136. // 获取下一个LineNum
  1137. var lineNumRows = await _db.Ado.SqlQueryAsync<int>(
  1138. "SELECT IFNULL(MAX(LineNum), 0) + 1 FROM WorkOrdDetail WHERE WorkOrd = @WorkOrd",
  1139. new { WorkOrd = workOrd });
  1140. var lineNum = lineNumRows[0];
  1141. // 插入WorkOrdDetail
  1142. await _db.Ado.ExecuteCommandAsync(
  1143. """
  1144. INSERT INTO WorkOrdDetail (
  1145. RecID, WorkOrdMasterRecID, `Domain`, WorkOrd, LineNum, ItemNum, Op,
  1146. Location, QtyRequired, QtyPosted, UM, LotSerial,
  1147. IsActive, IsConfirm, CreateUser, CreateTime, tenant_id
  1148. ) VALUES (
  1149. @RecID, @MasterRecId, @Domain, @WorkOrd, @LineNum, @ItemNum, @Op,
  1150. @Location, @QtyRequired, 0, @UM, @LotSerial,
  1151. 1, 0, @User, @Now, @TenantId
  1152. )
  1153. """,
  1154. new SugarParameter("@RecID", newRecId),
  1155. new SugarParameter("@MasterRecId", masterRecId),
  1156. new SugarParameter("@Domain", domain.Length > 8 ? domain[..8] : domain),
  1157. new SugarParameter("@WorkOrd", workOrd),
  1158. new SugarParameter("@LineNum", lineNum),
  1159. new SugarParameter("@ItemNum", input.ItemNum.Trim()),
  1160. new SugarParameter("@Op", (object?)input.Op ?? DBNull.Value),
  1161. new SugarParameter("@QtyRequired", input.QtyRequired),
  1162. new SugarParameter("@Location", (object?)input.Location ?? DBNull.Value),
  1163. new SugarParameter("@LotSerial", (object?)input.LotSerial ?? DBNull.Value),
  1164. new SugarParameter("@UM", string.IsNullOrEmpty(um) ? DBNull.Value : um),
  1165. new SugarParameter("@User", account.Length > 24 ? account[..24] : account),
  1166. new SugarParameter("@Now", now),
  1167. new SugarParameter("@TenantId", tenantId));
  1168. // 非P状态需同步创建领料单NbrDetail
  1169. if (!isP)
  1170. {
  1171. await CreateOrUpdateNbrDetailForMaterial(workOrd, domain, input.ItemNum.Trim(),
  1172. input.QtyRequired, um, itemName, tenantId, account, now, isNew: true);
  1173. }
  1174. return new { message = "新增成功", recID = newRecId };
  1175. }
  1176. else
  1177. {
  1178. // ── 编辑 ──
  1179. await _db.Ado.ExecuteCommandAsync(
  1180. """
  1181. UPDATE WorkOrdDetail
  1182. SET ItemNum = @ItemNum, Op = @Op, QtyRequired = @QtyRequired,
  1183. Location = @Location, LotSerial = @LotSerial,
  1184. UM = @UM,
  1185. UpdateUser = @User, UpdateTime = @Now
  1186. WHERE RecID = @RecID AND tenant_id = @TenantId
  1187. """,
  1188. new SugarParameter("@ItemNum", input.ItemNum.Trim()),
  1189. new SugarParameter("@Op", (object?)input.Op ?? DBNull.Value),
  1190. new SugarParameter("@QtyRequired", input.QtyRequired),
  1191. new SugarParameter("@Location", (object?)input.Location ?? DBNull.Value),
  1192. new SugarParameter("@LotSerial", (object?)input.LotSerial ?? DBNull.Value),
  1193. new SugarParameter("@UM", string.IsNullOrEmpty(um) ? DBNull.Value : um),
  1194. new SugarParameter("@User", account.Length > 24 ? account[..24] : account),
  1195. new SugarParameter("@Now", now),
  1196. new SugarParameter("@RecID", input.RecID),
  1197. new SugarParameter("@TenantId", tenantId));
  1198. // 非P状态需同步修改领料单NbrDetail
  1199. if (!isP)
  1200. {
  1201. await CreateOrUpdateNbrDetailForMaterial(workOrd, domain, input.ItemNum.Trim(),
  1202. input.QtyRequired, um, itemName, tenantId, account, now, isNew: false);
  1203. }
  1204. return new { message = "保存成功" };
  1205. }
  1206. }
  1207. /// <summary>
  1208. /// 删除工单物料明细
  1209. /// </summary>
  1210. [DisplayName("删除工单物料明细")]
  1211. [HttpPost("scheduling/material-delete")]
  1212. public async Task<object> DeleteMaterialDetail([FromBody] WorkOrdDetailDeleteInput input)
  1213. {
  1214. if (input.RecID == 0)
  1215. throw Oops.Oh("主键不能为空");
  1216. var tenantId = _userManager.TenantId;
  1217. // 获取物料明细信息用于NbrDetail同步
  1218. var detailRows = await _db.Ado.SqlQueryAsync<dynamic>(
  1219. """
  1220. SELECT d.WorkOrd, d.ItemNum, IFNULL(d.`Domain`, '') AS Domain, m.Status
  1221. FROM WorkOrdDetail d
  1222. LEFT JOIN WorkOrdMaster m ON d.WorkOrd = m.WorkOrd AND d.tenant_id = m.tenant_id
  1223. WHERE d.RecID = @RecID AND d.tenant_id = @TenantId
  1224. """,
  1225. new { RecID = input.RecID, TenantId = tenantId });
  1226. if (detailRows == null || detailRows.Count == 0)
  1227. throw Oops.Oh("物料明细不存在");
  1228. var row = detailRows[0];
  1229. var workOrd = (string)row.WorkOrd;
  1230. var itemNum = (string)row.ItemNum;
  1231. var domain = (string)row.Domain ?? "";
  1232. var woStatus = (string)(row.Status ?? "");
  1233. // 删除WorkOrdDetail
  1234. await _db.Ado.ExecuteCommandAsync(
  1235. "DELETE FROM WorkOrdDetail WHERE RecID = @RecID AND tenant_id = @TenantId",
  1236. new SugarParameter("@RecID", input.RecID),
  1237. new SugarParameter("@TenantId", tenantId));
  1238. // 非P状态需同步删除NbrDetail
  1239. if (woStatus != "P")
  1240. {
  1241. await _db.Ado.ExecuteCommandAsync(
  1242. """
  1243. DELETE FROM NbrDetail
  1244. WHERE WorkOrd = @WorkOrd AND ItemNum = @ItemNum AND Type = 'SM'
  1245. AND tenant_id = @TenantId
  1246. """,
  1247. new SugarParameter("@WorkOrd", workOrd),
  1248. new SugarParameter("@ItemNum", itemNum),
  1249. new SugarParameter("@TenantId", tenantId));
  1250. }
  1251. return new { message = "删除成功" };
  1252. }
  1253. // ══════════════════════════════════════════════════════════════
  1254. // 工单工序明细 CRUD
  1255. // ══════════════════════════════════════════════════════════════
  1256. /// <summary>
  1257. /// 保存工单工序明细(新增/编辑)
  1258. /// </summary>
  1259. [DisplayName("保存工单工序明细")]
  1260. [HttpPost("scheduling/routing-save")]
  1261. public async Task<object> SaveRoutingDetail([FromBody] WorkOrdRoutingSaveInput input)
  1262. {
  1263. if (string.IsNullOrWhiteSpace(input.WorkOrd))
  1264. throw Oops.Oh("工单编号不能为空");
  1265. if (string.IsNullOrWhiteSpace(input.OP))
  1266. throw Oops.Oh("工序不能为空");
  1267. var tenantId = _userManager.TenantId;
  1268. var account = _userManager.Account ?? "system";
  1269. var now = DateTime.Now;
  1270. var workOrd = input.WorkOrd.Trim();
  1271. // 获取工单主记录信息和域
  1272. var master = await _db.Ado.SqlQueryAsync<dynamic>(
  1273. """
  1274. SELECT RecID, IFNULL(`Domain`, '') AS Domain, IFNULL(ERPfld1, '') AS ERPfld1,
  1275. IFNULL(QtyOrded, 0) AS QtyOrded
  1276. FROM WorkOrdMaster
  1277. WHERE WorkOrd = @WorkOrd AND tenant_id = @TenantId
  1278. LIMIT 1
  1279. """,
  1280. new { WorkOrd = workOrd, TenantId = tenantId });
  1281. if (master == null || master.Count == 0)
  1282. throw Oops.Oh("工单不存在");
  1283. var masterRow = master[0];
  1284. var masterRecId = (long)masterRow.RecID;
  1285. var domain = (string)masterRow.Domain ?? "";
  1286. var erpfld1 = (string)masterRow.ERPfld1 ?? "";
  1287. var qtyOrded = (decimal)masterRow.QtyOrded;
  1288. // 检查是否已有相同工序编号(新增时检查,编辑时排除自身)
  1289. var dupCheck = await _db.Ado.SqlQueryAsync<int>(
  1290. $"""
  1291. SELECT COUNT(1) FROM WorkOrdRouting
  1292. WHERE WorkOrd = @WorkOrd AND OP = @OP
  1293. AND tenant_id = @TenantId
  1294. {(input.RecID > 0 ? "AND RecID <> @RecID" : "")}
  1295. """,
  1296. new { WorkOrd = workOrd, OP = input.OP.Trim(), TenantId = tenantId, RecID = input.RecID });
  1297. if (dupCheck[0] > 0)
  1298. throw Oops.Oh($"工单 {workOrd} 已有相同工序编号 {input.OP},不允许保存");
  1299. // 检查后序是否已有完成数量(编辑时)
  1300. if (input.RecID > 0)
  1301. {
  1302. var currentOpRows = await _db.Ado.SqlQueryAsync<int>(
  1303. $"""
  1304. SELECT CAST(IFNULL(OP, 0) AS SIGNED) FROM WorkOrdRouting
  1305. WHERE RecID = @RecID AND tenant_id = @TenantId
  1306. """,
  1307. new { RecID = input.RecID, TenantId = tenantId });
  1308. if (currentOpRows.Count > 0)
  1309. {
  1310. var currentOp = currentOpRows[0];
  1311. var afterComplete = await _db.Ado.SqlQueryAsync<int>(
  1312. """
  1313. SELECT COUNT(1) FROM WorkOrdRouting
  1314. WHERE WorkOrd = @WorkOrd
  1315. AND CAST(IFNULL(OP, 0) AS SIGNED) > @CurrentOp
  1316. AND IFNULL(QtyComplete, 0) > 0
  1317. AND tenant_id = @TenantId
  1318. """,
  1319. new { WorkOrd = workOrd, CurrentOp = currentOp, TenantId = tenantId });
  1320. if (afterComplete[0] > 0)
  1321. throw Oops.Oh("后序已有完成数量,不允许保存");
  1322. }
  1323. }
  1324. if (input.RecID == 0)
  1325. {
  1326. // ── 新增 ──
  1327. var nextIdRows = await _db.Ado.SqlQueryAsync<long>(
  1328. "SELECT IFNULL(MAX(RecID), 0) + 1 FROM WorkOrdRouting");
  1329. var newRecId = nextIdRows[0];
  1330. await _db.Ado.ExecuteCommandAsync(
  1331. """
  1332. INSERT INTO WorkOrdRouting (
  1333. RecID, `Descr`, `Domain`, `ChargeCode`, `Machine`, `RunCrew`, `MilestoneOp`,
  1334. `OP`, `ItemNum`, `WorkCtr`, `Setup`, `MachinesperOp`, `Labor`,
  1335. `MachBdnRate`, `WorkCode`, `StdSetupTime`, `Engineer`,
  1336. `WorkOrd`, `WorkOrdMasterRecID`, `ERPfld1`, `QtyOrded`,
  1337. `ProcessOut`, `ProcessOutDay`, `ProcessOutSupp`,
  1338. `IsActive`, `Status`, `ProdLine`, `ToolCode`,
  1339. `CreateTime`, `CreateUser`, `tenant_id`, `CommentIndex`, `WaitTime`, `QtyComplete`, `ParentOp`
  1340. ) VALUES (
  1341. @RecID, @Descr, @Domain, @ChargeCode, @Machine, @RunCrew, @MilestoneOp,
  1342. @OP, @ItemNum, @WorkCtr, @Setup, @MachinesperOp, @Labor,
  1343. @MachBdnRate, @WorkCode, @StdSetupTime, @Engineer,
  1344. @WorkOrd, @MasterRecId, @ERPfld1, @QtyOrded,
  1345. @ProcessOut, @ProcessOutDay, @ProcessOutSupp,
  1346. b'1', 'r', @ProdLine, @ToolCode,
  1347. @Now, @User, @TenantId, 0, 0, 0, 0
  1348. )
  1349. """,
  1350. new SugarParameter("@RecID", newRecId),
  1351. new SugarParameter("@Descr", (object?)input.Descr ?? DBNull.Value),
  1352. new SugarParameter("@Domain", domain.Length > 8 ? domain[..8] : domain),
  1353. new SugarParameter("@ChargeCode", (object?)input.ChargeCode ?? DBNull.Value),
  1354. new SugarParameter("@Machine", (object?)input.Machine ?? DBNull.Value),
  1355. new SugarParameter("@RunCrew", input.RunCrew),
  1356. new SugarParameter("@MilestoneOp", input.MilestoneOp ? 1 : 0),
  1357. new SugarParameter("@OP", input.OP.Trim()),
  1358. new SugarParameter("@ItemNum", (object?)input.ItemNum ?? DBNull.Value),
  1359. new SugarParameter("@WorkCtr", (object?)input.WorkCtr ?? DBNull.Value),
  1360. new SugarParameter("@Setup", input.Setup),
  1361. new SugarParameter("@MachinesperOp", input.MachinesperOp),
  1362. new SugarParameter("@Labor", input.Labor),
  1363. new SugarParameter("@MachBdnRate", input.MachBdnRate),
  1364. new SugarParameter("@WorkCode", (object?)input.WorkCode ?? DBNull.Value),
  1365. new SugarParameter("@StdSetupTime", input.StdSetupTime),
  1366. new SugarParameter("@Engineer", (object?)input.Engineer ?? DBNull.Value),
  1367. new SugarParameter("@WorkOrd", workOrd),
  1368. new SugarParameter("@MasterRecId", masterRecId),
  1369. new SugarParameter("@ERPfld1", erpfld1),
  1370. new SugarParameter("@QtyOrded", input.QtyOrded > 0 ? input.QtyOrded : qtyOrded),
  1371. new SugarParameter("@ProcessOut", input.ProcessOut ? 1 : 0),
  1372. new SugarParameter("@ProcessOutDay", input.ProcessOutDay),
  1373. new SugarParameter("@ProcessOutSupp", (object?)input.ProcessOutSupp ?? DBNull.Value),
  1374. new SugarParameter("@ProdLine", (object?)input.ProdLine ?? DBNull.Value),
  1375. new SugarParameter("@ToolCode", (object?)input.ToolCode ?? DBNull.Value),
  1376. new SugarParameter("@Now", now),
  1377. new SugarParameter("@User", account.Length > 24 ? account[..24] : account),
  1378. new SugarParameter("@TenantId", tenantId));
  1379. return new { message = "新增成功", recID = newRecId };
  1380. }
  1381. else
  1382. {
  1383. // ── 编辑 ──
  1384. await _db.Ado.ExecuteCommandAsync(
  1385. """
  1386. UPDATE WorkOrdRouting
  1387. SET `Descr` = @Descr, `ChargeCode` = @ChargeCode, `Machine` = @Machine,
  1388. `RunCrew` = @RunCrew, `MilestoneOp` = @MilestoneOp, `OP` = @OP,
  1389. `ItemNum` = @ItemNum, `WorkCtr` = @WorkCtr,
  1390. `Setup` = @Setup, `MachinesperOp` = @MachinesperOp, `Labor` = @Labor,
  1391. `MachBdnRate` = @MachBdnRate, `WorkCode` = @WorkCode,
  1392. `StdSetupTime` = @StdSetupTime, `Engineer` = @Engineer,
  1393. `QtyOrded` = @QtyOrded, `ProcessOut` = @ProcessOut,
  1394. `ProcessOutDay` = @ProcessOutDay, `ProcessOutSupp` = @ProcessOutSupp,
  1395. `ProdLine` = @ProdLine, `ToolCode` = @ToolCode,
  1396. `UpdateUser` = @User, `UpdateTime` = @Now
  1397. WHERE RecID = @RecID AND tenant_id = @TenantId
  1398. """,
  1399. new SugarParameter("@Descr", (object?)input.Descr ?? DBNull.Value),
  1400. new SugarParameter("@ChargeCode", (object?)input.ChargeCode ?? DBNull.Value),
  1401. new SugarParameter("@Machine", (object?)input.Machine ?? DBNull.Value),
  1402. new SugarParameter("@RunCrew", input.RunCrew),
  1403. new SugarParameter("@MilestoneOp", input.MilestoneOp ? 1 : 0),
  1404. new SugarParameter("@OP", input.OP.Trim()),
  1405. new SugarParameter("@ItemNum", (object?)input.ItemNum ?? DBNull.Value),
  1406. new SugarParameter("@WorkCtr", (object?)input.WorkCtr ?? DBNull.Value),
  1407. new SugarParameter("@Setup", input.Setup),
  1408. new SugarParameter("@MachinesperOp", input.MachinesperOp),
  1409. new SugarParameter("@Labor", input.Labor),
  1410. new SugarParameter("@MachBdnRate", input.MachBdnRate),
  1411. new SugarParameter("@WorkCode", (object?)input.WorkCode ?? DBNull.Value),
  1412. new SugarParameter("@StdSetupTime", input.StdSetupTime),
  1413. new SugarParameter("@Engineer", (object?)input.Engineer ?? DBNull.Value),
  1414. new SugarParameter("@QtyOrded", input.QtyOrded),
  1415. new SugarParameter("@ProcessOut", input.ProcessOut ? 1 : 0),
  1416. new SugarParameter("@ProcessOutDay", input.ProcessOutDay),
  1417. new SugarParameter("@ProcessOutSupp", (object?)input.ProcessOutSupp ?? DBNull.Value),
  1418. new SugarParameter("@ProdLine", (object?)input.ProdLine ?? DBNull.Value),
  1419. new SugarParameter("@ToolCode", (object?)input.ToolCode ?? DBNull.Value),
  1420. new SugarParameter("@User", account.Length > 24 ? account[..24] : account),
  1421. new SugarParameter("@Now", now),
  1422. new SugarParameter("@RecID", input.RecID),
  1423. new SugarParameter("@TenantId", tenantId));
  1424. return new { message = "保存成功" };
  1425. }
  1426. }
  1427. /// <summary>
  1428. /// 删除工单工序明细
  1429. /// </summary>
  1430. [DisplayName("删除工单工序明细")]
  1431. [HttpPost("scheduling/routing-delete")]
  1432. public async Task<object> DeleteRoutingDetail([FromBody] WorkOrdRoutingDeleteInput input)
  1433. {
  1434. if (input.RecID == 0)
  1435. throw Oops.Oh("主键不能为空");
  1436. var tenantId = _userManager.TenantId;
  1437. // 检查后序是否已有完成数量
  1438. var detailRows = await _db.Ado.SqlQueryAsync<dynamic>(
  1439. """
  1440. SELECT r.WorkOrd, CAST(IFNULL(r.OP, 0) AS SIGNED) AS OP, IFNULL(r.`Domain`, '') AS Domain
  1441. FROM WorkOrdRouting r
  1442. WHERE r.RecID = @RecID AND r.tenant_id = @TenantId
  1443. """,
  1444. new { RecID = input.RecID, TenantId = tenantId });
  1445. if (detailRows == null || detailRows.Count == 0)
  1446. throw Oops.Oh("工序明细不存在");
  1447. var row = detailRows[0];
  1448. var workOrd = (string)row.WorkOrd;
  1449. var currentOp = (int)row.OP;
  1450. var domain = (string)row.Domain ?? "";
  1451. var afterComplete = await _db.Ado.SqlQueryAsync<int>(
  1452. """
  1453. SELECT COUNT(1) FROM WorkOrdRouting
  1454. WHERE WorkOrd = @WorkOrd
  1455. AND CAST(IFNULL(OP, 0) AS SIGNED) > @CurrentOp
  1456. AND IFNULL(QtyComplete, 0) > 0
  1457. AND tenant_id = @TenantId
  1458. """,
  1459. new { WorkOrd = workOrd, CurrentOp = currentOp, TenantId = tenantId });
  1460. if (afterComplete[0] > 0)
  1461. throw Oops.Oh("后序已有完成数量,不允许删除");
  1462. await _db.Ado.ExecuteCommandAsync(
  1463. "DELETE FROM WorkOrdRouting WHERE RecID = @RecID AND tenant_id = @TenantId",
  1464. new SugarParameter("@RecID", input.RecID),
  1465. new SugarParameter("@TenantId", tenantId));
  1466. return new { message = "删除成功" };
  1467. }
  1468. // ══════════════════════════════════════════════════════════════
  1469. // 辅助方法
  1470. // ══════════════════════════════════════════════════════════════
  1471. /// <summary>
  1472. /// 为物料明细创建或更新领料单NbrDetail
  1473. /// </summary>
  1474. private async Task CreateOrUpdateNbrDetailForMaterial(
  1475. string workOrd, string domain, string itemNum, decimal qtyRequired,
  1476. string um, string itemName, long tenantId, string account, DateTime now, bool isNew)
  1477. {
  1478. // 查找此工单此物料的SM类型领料单明细
  1479. var existRows = await _db.Ado.SqlQueryAsync<dynamic>(
  1480. """
  1481. SELECT nd.RecID, nd.NbrRecID
  1482. FROM NbrDetail nd
  1483. WHERE nd.WorkOrd = @WorkOrd AND nd.ItemNum = @ItemNum AND nd.Type = 'SM'
  1484. AND nd.tenant_id = @TenantId
  1485. """,
  1486. new { WorkOrd = workOrd, ItemNum = itemNum, TenantId = tenantId });
  1487. if (existRows == null || existRows.Count == 0)
  1488. {
  1489. // 没有领料单明细,创建一条
  1490. // 先查找此工单是否有SM类型的领料单主记录
  1491. var nbrMasterRows = await _db.Ado.SqlQueryAsync<dynamic>(
  1492. """
  1493. SELECT RecID, Nbr FROM NbrMaster
  1494. WHERE WorkOrd = @WorkOrd AND Type = 'SM' AND tenant_id = @TenantId
  1495. ORDER BY RecID DESC LIMIT 1
  1496. """,
  1497. new { WorkOrd = workOrd, TenantId = tenantId });
  1498. long nbrRecId = 0;
  1499. string nbrNo = "";
  1500. if (nbrMasterRows == null || nbrMasterRows.Count == 0)
  1501. {
  1502. // 创建领料单主记录
  1503. var nextMasterIdRows = await _db.Ado.SqlQueryAsync<long>(
  1504. "SELECT IFNULL(MAX(RecID), 0) + 1 FROM NbrMaster");
  1505. nbrRecId = nextMasterIdRows[0];
  1506. nbrNo = $"SM-{workOrd}-{DateTime.Now:yyyyMMddHHmmss}";
  1507. await _db.Ado.ExecuteCommandAsync(
  1508. """
  1509. INSERT INTO NbrMaster (
  1510. RecID, `Domain`, Type, Nbr, WorkOrd, NbrDate,
  1511. Status, IsActive, CreateUser, CreateTime, tenant_id
  1512. ) VALUES (
  1513. @RecID, @Domain, 'SM', @Nbr, @WorkOrd, @Now,
  1514. '', 1, @User, @Now, @TenantId
  1515. )
  1516. """,
  1517. new SugarParameter("@RecID", nbrRecId),
  1518. new SugarParameter("@Domain", domain.Length > 8 ? domain[..8] : domain),
  1519. new SugarParameter("@Nbr", nbrNo),
  1520. new SugarParameter("@WorkOrd", workOrd),
  1521. new SugarParameter("@Now", now),
  1522. new SugarParameter("@User", account.Length > 24 ? account[..24] : account),
  1523. new SugarParameter("@TenantId", tenantId));
  1524. }
  1525. else
  1526. {
  1527. nbrRecId = (long)nbrMasterRows[0].RecID;
  1528. nbrNo = (string)nbrMasterRows[0].Nbr;
  1529. }
  1530. // 创建领料单明细
  1531. var nextDetailIdRows = await _db.Ado.SqlQueryAsync<long>(
  1532. "SELECT IFNULL(MAX(RecID), 0) + 1 FROM NbrDetail");
  1533. var newDetailId = nextDetailIdRows[0];
  1534. var maxLineRows = await _db.Ado.SqlQueryAsync<short>(
  1535. "SELECT IFNULL(MAX(Line), 0) + 1 FROM NbrDetail WHERE NbrRecID = @NbrRecID AND tenant_id = @TenantId",
  1536. new { NbrRecID = nbrRecId, TenantId = tenantId });
  1537. var newLine = maxLineRows[0];
  1538. await _db.Ado.ExecuteCommandAsync(
  1539. """
  1540. INSERT INTO NbrDetail (
  1541. RecID, `Domain`, Type, Nbr, Line, ItemNum, Dimension1, Dimension2,
  1542. LocationFrom, LocationTo, QtyFrom, QtyTo, QtyOrd, QtyRec,
  1543. CurrQtyOpened, UM, WorkOrd, ItemName, Status,
  1544. IsActive, IsConfirm, IsChanged, BusinessID, NbrRecID,
  1545. CreateUser, CreateTime, UpdateUser, UpdateTime, tenant_id
  1546. ) VALUES (
  1547. @RecID, @Domain, 'SM', @Nbr, @Line, @ItemNum, '', '',
  1548. '', '', 0, 0, @QtyOrd, 0,
  1549. @QtyOrd, @UM, @WorkOrd, @ItemName, '',
  1550. 1, 0, 1, 0, @NbrRecID,
  1551. @User, @Now, @User, @Now, @TenantId
  1552. )
  1553. """,
  1554. new SugarParameter("@RecID", newDetailId),
  1555. new SugarParameter("@Domain", domain.Length > 8 ? domain[..8] : domain),
  1556. new SugarParameter("@Nbr", nbrNo),
  1557. new SugarParameter("@Line", newLine),
  1558. new SugarParameter("@ItemNum", itemNum),
  1559. new SugarParameter("@QtyOrd", qtyRequired),
  1560. new SugarParameter("@UM", string.IsNullOrEmpty(um) ? DBNull.Value : um),
  1561. new SugarParameter("@WorkOrd", workOrd),
  1562. new SugarParameter("@ItemName", string.IsNullOrEmpty(itemName) ? DBNull.Value : itemName),
  1563. new SugarParameter("@NbrRecID", nbrRecId),
  1564. new SugarParameter("@User", account.Length > 24 ? account[..24] : account),
  1565. new SugarParameter("@Now", now),
  1566. new SugarParameter("@TenantId", tenantId));
  1567. }
  1568. else
  1569. {
  1570. // 更新已有的领料单明细
  1571. var ndRecId = (long)existRows[0].RecID;
  1572. await _db.Ado.ExecuteCommandAsync(
  1573. """
  1574. UPDATE NbrDetail
  1575. SET QtyOrd = @QtyOrd, CurrQtyOpened = @QtyOrd,
  1576. UM = @UM, ItemName = @ItemName,
  1577. UpdateUser = @User, UpdateTime = @Now
  1578. WHERE RecID = @RecID AND tenant_id = @TenantId
  1579. """,
  1580. new SugarParameter("@QtyOrd", qtyRequired),
  1581. new SugarParameter("@UM", string.IsNullOrEmpty(um) ? DBNull.Value : um),
  1582. new SugarParameter("@ItemName", string.IsNullOrEmpty(itemName) ? DBNull.Value : itemName),
  1583. new SugarParameter("@User", account.Length > 24 ? account[..24] : account),
  1584. new SugarParameter("@Now", now),
  1585. new SugarParameter("@RecID", ndRecId),
  1586. new SugarParameter("@TenantId", tenantId));
  1587. }
  1588. }
  1589. // ══════════════════════════════════════════════════════════════
  1590. // 加急 POST /api/Production/scheduling/urgent
  1591. // ══════════════════════════════════════════════════════════════
  1592. [DisplayName("设置加急")]
  1593. [HttpPost("scheduling/urgent")]
  1594. public async Task<object> SetUrgent([FromBody] WorkOrderUrgentInput input)
  1595. {
  1596. var account = _userManager.Account ?? "system";
  1597. var n = await _db.Ado.ExecuteCommandAsync(
  1598. $"""
  1599. UPDATE WorkOrdMaster
  1600. SET Urgent = @Urgent,
  1601. UpdateUser = @UpdateUser,
  1602. UpdateTime = @UpdateTime
  1603. WHERE {SqlWorkOrdMasterMatchDomainOrTenant}
  1604. """,
  1605. new List<SugarParameter>
  1606. {
  1607. new("@Urgent", input.Urgent),
  1608. new("@UpdateUser", account),
  1609. new("@UpdateTime", DateTime.Now),
  1610. new("@WorkOrd", input.WorkOrd.Trim()),
  1611. new("@Domain", input.Domain.Trim())
  1612. });
  1613. if (n == 0)
  1614. throw Oops.Oh("工单不存在");
  1615. return new { message = input.Urgent == 2 ? "已设为特急" : "已设为加急" };
  1616. }
  1617. // ──────────────── 行类型 ────────────────
  1618. private sealed class WorkOrderSchedulingListRow
  1619. {
  1620. public int Id { get; set; }
  1621. public string? Priority { get; set; }
  1622. public string? WorkOrd { get; set; }
  1623. public string? LotSerial { get; set; }
  1624. public string? IssueSite { get; set; }
  1625. public string? ItemNum { get; set; }
  1626. public string? Descr { get; set; }
  1627. public string? Descr1 { get; set; }
  1628. public decimal? QtyOrded { get; set; }
  1629. public decimal? LocationStock { get; set; }
  1630. public decimal? QtyCompleted { get; set; }
  1631. public DateTime? PlanDate { get; set; }
  1632. public DateTime? ProdDate { get; set; }
  1633. public string? Status { get; set; }
  1634. public string? Domain { get; set; }
  1635. public int? Urgent { get; set; }
  1636. }
  1637. // ══════════════════════════════════════════════════════════════
  1638. // Input/Output DTOs
  1639. // ══════════════════════════════════════════════════════════════
  1640. public class WorkOrdDetailSaveInput
  1641. {
  1642. public long RecID { get; set; }
  1643. public string? WorkOrd { get; set; }
  1644. public string? ItemNum { get; set; }
  1645. public string? Op { get; set; }
  1646. public decimal QtyRequired { get; set; }
  1647. public string? Location { get; set; }
  1648. public string? LotSerial { get; set; }
  1649. }
  1650. public class WorkOrdDetailDeleteInput
  1651. {
  1652. public long RecID { get; set; }
  1653. }
  1654. public class WorkOrdRoutingSaveInput
  1655. {
  1656. public long RecID { get; set; }
  1657. public string? WorkOrd { get; set; }
  1658. public string? ItemNum { get; set; }
  1659. public string? OP { get; set; }
  1660. public string? Descr { get; set; }
  1661. public string? WorkCtr { get; set; }
  1662. public string? ProdLine { get; set; }
  1663. public decimal StdSetupTime { get; set; }
  1664. public decimal QtyOrded { get; set; }
  1665. public bool MilestoneOp { get; set; } = true;
  1666. public bool ProcessOut { get; set; }
  1667. public string? ProcessOutSupp { get; set; }
  1668. public decimal ProcessOutDay { get; set; }
  1669. public string? Machine { get; set; }
  1670. public string? ToolCode { get; set; }
  1671. public decimal RunCrew { get; set; }
  1672. public string? Engineer { get; set; }
  1673. public decimal MachBdnRate { get; set; }
  1674. public string? WorkCode { get; set; }
  1675. public decimal Setup { get; set; }
  1676. public decimal MachinesperOp { get; set; }
  1677. public decimal Labor { get; set; }
  1678. public string? ChargeCode { get; set; }
  1679. }
  1680. public class WorkOrdRoutingDeleteInput
  1681. {
  1682. public long RecID { get; set; }
  1683. }
  1684. private sealed class WorkOrderEditPreviewRow
  1685. {
  1686. public string? WorkOrd { get; set; }
  1687. public string? ItemNum { get; set; }
  1688. public decimal? QtyOrded { get; set; }
  1689. public string? Priority { get; set; }
  1690. public string? LotSerial { get; set; }
  1691. public int Urgent { get; set; }
  1692. public DateTime? OrdDate { get; set; }
  1693. }
  1694. private sealed class WorkOrderViewMasterRow
  1695. {
  1696. public string? WorkOrd { get; set; }
  1697. public decimal? QtyOrded { get; set; }
  1698. public decimal? QtyCompleted { get; set; }
  1699. public string? ItemNum { get; set; }
  1700. public string? ItemName { get; set; }
  1701. public string? ItemModel { get; set; }
  1702. public DateTime? OrdDate { get; set; }
  1703. public DateTime? DueDate { get; set; }
  1704. public string? Status { get; set; }
  1705. public string? Remark { get; set; }
  1706. }
  1707. private sealed class WorkOrderViewRoutingRow
  1708. {
  1709. public long RecID { get; set; }
  1710. public string? Op { get; set; }
  1711. public string? Descr { get; set; }
  1712. public string? ParentOp { get; set; }
  1713. public int MilestoneOp { get; set; }
  1714. public decimal PackingQty { get; set; }
  1715. public decimal QtyComplete { get; set; }
  1716. public decimal QtyReject { get; set; }
  1717. public decimal QtyScrap { get; set; }
  1718. public string? Status { get; set; }
  1719. public string? WorkOrd { get; set; }
  1720. public string? WorkCtr { get; set; }
  1721. public string? ProdLine { get; set; }
  1722. public decimal StdSetupTime { get; set; }
  1723. public decimal QtyOrded { get; set; }
  1724. public int ProcessOut { get; set; }
  1725. public string? ProcessOutSupp { get; set; }
  1726. public decimal ProcessOutDay { get; set; }
  1727. public string? Machine { get; set; }
  1728. public string? ToolCode { get; set; }
  1729. public decimal RunCrew { get; set; }
  1730. public string? Engineer { get; set; }
  1731. public decimal MachBdnRate { get; set; }
  1732. public string? WorkCode { get; set; }
  1733. public decimal Setup { get; set; }
  1734. public decimal MachinesperOp { get; set; }
  1735. public decimal Labor { get; set; }
  1736. public string? ChargeCode { get; set; }
  1737. public string? ItemNum { get; set; }
  1738. }
  1739. private sealed class WorkOrderViewDetailRow
  1740. {
  1741. public long RecID { get; set; }
  1742. public string? ItemNum { get; set; }
  1743. public string? Op { get; set; }
  1744. public decimal? QtyRequired { get; set; }
  1745. public decimal FrozenBOMQty { get; set; }
  1746. public string? Location { get; set; }
  1747. public string? LotSerial { get; set; }
  1748. public string? WorkOrd { get; set; }
  1749. }
  1750. private sealed class WorkOrderTraceMasterRow
  1751. {
  1752. public string? WorkOrd { get; set; }
  1753. public decimal? QtyOrded { get; set; }
  1754. public decimal? QtyCompleted { get; set; }
  1755. public string? ItemNum { get; set; }
  1756. public string? ItemName { get; set; }
  1757. public string? ItemModel { get; set; }
  1758. public DateTime? OrdDate { get; set; }
  1759. public DateTime? DueDate { get; set; }
  1760. public string? Status { get; set; }
  1761. }
  1762. private sealed class TraceResourceRow
  1763. {
  1764. public long Sno { get; set; }
  1765. public string? Num { get; set; }
  1766. public string? ItemNumber { get; set; }
  1767. public string? ItemName { get; set; }
  1768. public string? Model { get; set; }
  1769. public string? Unit { get; set; }
  1770. public string? BomNumber { get; set; }
  1771. public string? KittingTime { get; set; }
  1772. public string? ErpClsName { get; set; }
  1773. public string? Backflush { get; set; }
  1774. public string? Qty { get; set; }
  1775. public string? NeedCount { get; set; }
  1776. }
  1777. private sealed class TraceScheduleRow
  1778. {
  1779. public long Sno { get; set; }
  1780. public string? WorkOrds { get; set; }
  1781. public string? WorkDate { get; set; }
  1782. public string? Line { get; set; }
  1783. public string? Op { get; set; }
  1784. public string? WorkQty { get; set; }
  1785. public string? ItemNum { get; set; }
  1786. public string? Descr { get; set; }
  1787. public string? Descr1 { get; set; }
  1788. public string? CreateTime { get; set; }
  1789. }
  1790. private sealed class TracePoRow
  1791. {
  1792. public long Sno { get; set; }
  1793. public string? PoBillno { get; set; }
  1794. public string? SupplierNo { get; set; }
  1795. public string? SupplierName { get; set; }
  1796. public string? PoTotal { get; set; }
  1797. public string? PoPurchaser { get; set; }
  1798. public string? State { get; set; }
  1799. public string? PoSsendDate { get; set; }
  1800. public string? CreateTime { get; set; }
  1801. }
  1802. private sealed class TracePrRow
  1803. {
  1804. public long Sno { get; set; }
  1805. public string? PrBillno { get; set; }
  1806. public string? Number { get; set; }
  1807. public string? IcitemName { get; set; }
  1808. public string? Model { get; set; }
  1809. public string? PrPurchasenumber { get; set; }
  1810. public string? PrPurchasename { get; set; }
  1811. public string? PrAqty { get; set; }
  1812. public string? PrSsendDate { get; set; }
  1813. public string? PrSarriveDate { get; set; }
  1814. public string? PrUnit { get; set; }
  1815. public string? PrPurchaser { get; set; }
  1816. }
  1817. private sealed class TraceNbrRow
  1818. {
  1819. public string? Line { get; set; }
  1820. public string? Nbr { get; set; }
  1821. public string? ItemNum { get; set; }
  1822. public string? Descr { get; set; }
  1823. public string? Descr1 { get; set; }
  1824. public string? LocationFrom { get; set; }
  1825. public string? LocationTo { get; set; }
  1826. public string? CurrQtyOpened { get; set; }
  1827. public string? QtyOrd { get; set; }
  1828. public string? QtyFrom { get; set; }
  1829. public string? QtyRec { get; set; }
  1830. public string? Unit { get; set; }
  1831. }
  1832. private sealed class TraceProdRow
  1833. {
  1834. public long Sno { get; set; }
  1835. public string? WorkOrd { get; set; }
  1836. public string? ItemNum { get; set; }
  1837. public string? Descr { get; set; }
  1838. public string? Descr1 { get; set; }
  1839. public string? Um { get; set; }
  1840. public string? QtyCompleted { get; set; }
  1841. public string? QtyReject { get; set; }
  1842. public string? QtyScrapped { get; set; }
  1843. public string? ProdDate { get; set; }
  1844. public string? Op { get; set; }
  1845. public string? Name { get; set; }
  1846. }
  1847. private sealed class TraceQualityRow
  1848. {
  1849. public long Sno { get; set; }
  1850. public string? Unit { get; set; }
  1851. public string? MorderNo { get; set; }
  1852. public string? WorkNumber { get; set; }
  1853. public string? MorderProductionNumber { get; set; }
  1854. public string? InspectionNumber { get; set; }
  1855. public string? QualifiedNumber { get; set; }
  1856. public string? InventoryNumber { get; set; }
  1857. public string? MoentryWrkcname { get; set; }
  1858. public string? PlannerStartDate { get; set; }
  1859. public string? PlannerEndDate { get; set; }
  1860. public string? MoentryPrdname { get; set; }
  1861. public string? MorderFstate { get; set; }
  1862. }
  1863. private sealed class TraceReceiptRow
  1864. {
  1865. public long Sno { get; set; }
  1866. public string? Status { get; set; }
  1867. public string? WorkOrd { get; set; }
  1868. public string? UM { get; set; }
  1869. public string? QtyOrded { get; set; }
  1870. public string? CompQty { get; set; }
  1871. public string? InspectionNumber { get; set; }
  1872. public string? QualifiedNumber { get; set; }
  1873. public string? QtyChangeAdvance { get; set; }
  1874. public string? OrdDate { get; set; }
  1875. public string? DueDate { get; set; }
  1876. }
  1877. }