WorkOrderSchedulingService.cs 108 KB

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