WorkOrdMesPushService.cs 24 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593
  1. using System.Text.Json;
  2. using Admin.NET.Plugin.AiDOP.DataPlatform.Executors;
  3. using Admin.NET.Plugin.AiDOP.Entity.DataPlatform;
  4. using Admin.NET.Plugin.AiDOP.WorkOrder;
  5. using Microsoft.AspNetCore.Http;
  6. using Microsoft.Extensions.Logging;
  7. using SqlSugar;
  8. namespace Admin.NET.Plugin.AiDOP.DataPlatform.Wms;
  9. /// <summary>
  10. /// WP4 · 计划侧工单入 165:本库 WorkOrdMaster/Detail/Routing 快照 → Outbox UPSERT。
  11. /// 挂接点:订单评审 / 计划刷新 / 交期确认;下达侧只推进状态(CreatePickBill)。
  12. /// </summary>
  13. [ApiDescriptionSettings(Order = 328, Description = "工单推送 MES")]
  14. [Route("api/aidop/wms-workord")]
  15. [AllowAnonymous]
  16. [NonUnify]
  17. public class WorkOrdMesPushService : IDynamicApiController, ITransient
  18. {
  19. public const string TargetSource = "DOPDEMORQ_SQLSERVER";
  20. public const string ActionWom = "WO_MES_WOM";
  21. public const string ActionWod = "WO_MES_WOD";
  22. public const string ActionWor = "WO_MES_WOR";
  23. private readonly ISqlSugarClient _db;
  24. private readonly MdpSourceScopeFactory _scopeFactory;
  25. private readonly MdpOutboxEnqueueService _enqueue;
  26. private readonly MdpOutboxWakeSignal _wake;
  27. private readonly UserManager _userManager;
  28. private readonly IHttpContextAccessor _httpContextAccessor;
  29. private readonly ILogger<WorkOrdMesPushService> _logger;
  30. public WorkOrdMesPushService(
  31. ISqlSugarClient db,
  32. MdpSourceScopeFactory scopeFactory,
  33. MdpOutboxEnqueueService enqueue,
  34. MdpOutboxWakeSignal wake,
  35. UserManager userManager,
  36. IHttpContextAccessor httpContextAccessor,
  37. ILogger<WorkOrdMesPushService> logger)
  38. {
  39. _db = db;
  40. _scopeFactory = scopeFactory;
  41. _enqueue = enqueue;
  42. _wake = wake;
  43. _userManager = userManager;
  44. _httpContextAccessor = httpContextAccessor;
  45. _logger = logger;
  46. }
  47. public sealed class PushInput
  48. {
  49. public string WorkOrd { get; set; } = "";
  50. public long TenantId { get; set; }
  51. public string? Domain { get; set; }
  52. }
  53. public sealed class PushResult
  54. {
  55. public bool Ok { get; set; }
  56. public string WorkOrd { get; set; } = "";
  57. public string Domain { get; set; } = "";
  58. public int Enqueued { get; set; }
  59. public int DetailCount { get; set; }
  60. public int RoutingCount { get; set; }
  61. public string? Message { get; set; }
  62. }
  63. /// <summary>补偿 / 联调:按本库快照入队推送 165。</summary>
  64. [DisplayName("推送工单到 MES")]
  65. [HttpPost("push")]
  66. public async Task<PushResult> Push([FromBody] PushInput input, CancellationToken ct = default)
  67. {
  68. if (input == null || string.IsNullOrWhiteSpace(input.WorkOrd))
  69. throw Oops.Oh("工单号不能为空");
  70. if (input.TenantId <= 0)
  71. throw Oops.Oh("租户号无效,请指定有效的 TenantId");
  72. var jwtTenantId = _userManager.TenantId;
  73. if (jwtTenantId > 0 && input.TenantId != jwtTenantId)
  74. throw Oops.Oh("请求租户与当前登录租户不一致");
  75. if (jwtTenantId <= 0)
  76. {
  77. var clientIp = _httpContextAccessor.HttpContext?.Connection?.RemoteIpAddress?.ToString() ?? "unknown";
  78. _logger.LogInformation(
  79. "[WorkOrdMesPush] 无登录态推送 audit ip={ClientIp} workOrd={WorkOrd} tenantId={TenantId}",
  80. clientIp, input.WorkOrd.Trim(), input.TenantId);
  81. }
  82. var tenantId = input.TenantId;
  83. var workOrd = input.WorkOrd.Trim();
  84. var domain = string.IsNullOrWhiteSpace(input.Domain)
  85. ? await ResolveDomainAsync(tenantId, workOrd)
  86. : input.Domain.Trim();
  87. return await EnqueueSnapshotAsync(tenantId, domain, workOrd, ct);
  88. }
  89. /// <summary>评审/刷新/确认后调用:失败只记日志,不抛(避免拖垮本库评审事务)。</summary>
  90. public async Task TryEnqueueSnapshotSafeAsync(long tenantId, string workOrd, List<string>? warnings = null)
  91. {
  92. try
  93. {
  94. if (string.IsNullOrWhiteSpace(workOrd)) return;
  95. var domain = await ResolveDomainAsync(tenantId, workOrd);
  96. var r = await EnqueueSnapshotAsync(tenantId, domain, workOrd);
  97. warnings?.Add($"工单 {workOrd} 已入队推送 MES({r.Enqueued} 条 Outbox,明细 {r.DetailCount}/工序 {r.RoutingCount})");
  98. }
  99. catch (Exception ex)
  100. {
  101. _logger.LogWarning(ex, "[WorkOrdMesPush] enqueue failed tenant={Tenant} wo={WorkOrd}", tenantId, workOrd);
  102. warnings?.Add($"工单 {workOrd} 推送 MES 入队失败:{ex.Message}(可稍后调用 /api/aidop/wms-workord/push 补偿)");
  103. }
  104. }
  105. /// <summary>从本库读三人组并入队(主表 → 明细 → 工序);末尾 Pulse 一次。</summary>
  106. public async Task<PushResult> EnqueueSnapshotAsync(
  107. long tenantId, string domain, string workOrd, CancellationToken ct = default)
  108. {
  109. domain = string.IsNullOrWhiteSpace(domain) ? "8010" : domain.Trim();
  110. workOrd = workOrd.Trim();
  111. var masters = await _db.Queryable<WorkOrdMaster>()
  112. .Where(m => m.TenantId == tenantId && m.WorkOrd == workOrd)
  113. .Take(1)
  114. .ToListAsync(ct);
  115. if (masters.Count == 0)
  116. throw Oops.Oh($"本库未找到工单 {workOrd}");
  117. var master = masters[0];
  118. var details = await _db.Ado.SqlQueryAsync<DetailRow>(
  119. """
  120. SELECT
  121. TRIM(IFNULL(Domain, '')) AS Domain,
  122. WorkOrd,
  123. IFNULL(LineNum, 0) AS LineNum,
  124. TRIM(IFNULL(ItemNum, '')) AS ItemNum,
  125. IFNULL(Op, 0) AS Op,
  126. TRIM(IFNULL(Location, '')) AS Location,
  127. TRIM(IFNULL(Site, '')) AS Site,
  128. IFNULL(QtyRequired, 0) AS QtyRequired,
  129. IFNULL(FrozenBOMQty, 0) AS FrozenBOMQty,
  130. TRIM(IFNULL(LotSerial, '')) AS LotSerial,
  131. TRIM(IFNULL(Status, '')) AS Status,
  132. TRIM(IFNULL(Typed, '')) AS Typed,
  133. TRIM(IFNULL(UM, '')) AS UM,
  134. IFNULL(IsActive, 0) AS IsActive,
  135. IFNULL(BusinessID, 0) AS BusinessID,
  136. CreateUser, CreateTime, UpdateUser, UpdateTime
  137. FROM WorkOrdDetail
  138. WHERE tenant_id = @TenantId AND WorkOrd = @WorkOrd
  139. AND IFNULL(IsActive, 0) = 1
  140. ORDER BY LineNum, Op, ItemNum
  141. """,
  142. new SugarParameter("@TenantId", tenantId),
  143. new SugarParameter("@WorkOrd", workOrd));
  144. var routings = await _db.Ado.SqlQueryAsync<RoutingRow>(
  145. """
  146. SELECT
  147. TRIM(IFNULL(Domain, '')) AS Domain,
  148. WorkOrd,
  149. OP,
  150. TRIM(IFNULL(ItemNum, '')) AS ItemNum,
  151. TRIM(IFNULL(Descr, '')) AS Descr,
  152. IFNULL(MilestoneOp, 0) AS MilestoneOp,
  153. IFNULL(ParentOp, 0) AS ParentOp,
  154. IFNULL(QtyOrded, 0) AS QtyOrded,
  155. TRIM(IFNULL(Status, '')) AS Status,
  156. TRIM(IFNULL(ProdLine, '')) AS ProdLine,
  157. TRIM(IFNULL(WorkCtr, '')) AS WorkCtr,
  158. TRIM(IFNULL(Machine, '')) AS Machine,
  159. IFNULL(RunCrew, 0) AS RunCrew,
  160. IFNULL(StdSetupTime, 0) AS StdSetupTime,
  161. IFNULL(ProcessOut, 0) AS ProcessOut,
  162. IFNULL(IsActive, 0) AS IsActive,
  163. CreateUser, CreateTime, UpdateUser, UpdateTime
  164. FROM WorkOrdRouting
  165. WHERE tenant_id = @TenantId AND WorkOrd = @WorkOrd
  166. AND IFNULL(IsActive, 0) = 1
  167. ORDER BY OP
  168. """,
  169. new SugarParameter("@TenantId", tenantId),
  170. new SugarParameter("@WorkOrd", workOrd));
  171. if (details.Count == 0)
  172. throw Oops.Oh($"工单 {workOrd} 无活跃物料明细,无法推送 MES");
  173. if (routings.Count == 0)
  174. throw Oops.Oh($"工单 {workOrd} 无活跃工序,无法推送 MES");
  175. var masterStatus = StatusForMasterPush(master.Status);
  176. var now = DateTime.Now;
  177. var enqueued = 0;
  178. var womInsert = BuildMasterInsert(domain, master, masterStatus, now);
  179. var womUpdate = BuildMasterUpdate(master, masterStatus, now);
  180. // 165 唯一索引 IX_WorkOrdMaster = (Domain, Typed, WorkOrd)
  181. var typed = string.IsNullOrWhiteSpace(master.Typed) ? "" : master.Typed.Trim();
  182. if (await EnqueueRowAsync(tenantId, $"wo|{domain}|{workOrd}|wom", ActionWom, "WorkOrdMaster",
  183. new Dictionary<string, object?>
  184. {
  185. ["Domain"] = domain,
  186. ["Typed"] = typed,
  187. ["WorkOrd"] = workOrd
  188. },
  189. womInsert, womUpdate, ct))
  190. enqueued++;
  191. foreach (var d in details)
  192. {
  193. var line = d.LineNum;
  194. var item = d.ItemNum ?? "";
  195. var op = d.Op;
  196. // 165 唯一索引 IX_WorkOrdDetail = (Domain, ID, WorkOrd, Op, Line, ItemNum, Dimension1, Dimension2)
  197. var keys = new Dictionary<string, object?>
  198. {
  199. ["Domain"] = domain,
  200. ["ID"] = "",
  201. ["WorkOrd"] = workOrd,
  202. ["Op"] = op,
  203. ["Line"] = line,
  204. ["ItemNum"] = item,
  205. ["Dimension1"] = "",
  206. ["Dimension2"] = ""
  207. };
  208. var insert = BuildDetailInsert(domain, workOrd, d, now);
  209. var update = BuildDetailUpdate(d, now);
  210. var idem = $"wo|{domain}|{workOrd}|wod|{line}|{op}|{item}";
  211. if (await EnqueueRowAsync(tenantId, idem, ActionWod, "WorkOrdDetail", keys, insert, update, ct))
  212. enqueued++;
  213. }
  214. foreach (var r in routings)
  215. {
  216. var routingStatus = StatusForRoutingPush(masterStatus, r.Status);
  217. // 165 唯一索引 IX_WorkOrdRouting_1 = (Domain, ID, OP, WorkOrd)
  218. var keys = new Dictionary<string, object?>
  219. {
  220. ["Domain"] = domain,
  221. ["ID"] = "",
  222. ["OP"] = r.OP,
  223. ["WorkOrd"] = workOrd
  224. };
  225. var insert = BuildRoutingInsert(domain, workOrd, r, routingStatus, now);
  226. var update = BuildRoutingUpdate(r, routingStatus, now);
  227. var idem = $"wo|{domain}|{workOrd}|wor|{r.OP}";
  228. if (await EnqueueRowAsync(tenantId, idem, ActionWor, "WorkOrdRouting", keys, insert, update, ct))
  229. enqueued++;
  230. }
  231. _wake.Pulse();
  232. return new PushResult
  233. {
  234. Ok = true,
  235. WorkOrd = workOrd,
  236. Domain = domain,
  237. Enqueued = enqueued,
  238. DetailCount = details.Count,
  239. RoutingCount = routings.Count,
  240. Message = $"已入队 {enqueued} 条(明细 {details.Count} / 工序 {routings.Count})"
  241. };
  242. }
  243. /// <summary>探测 165 是否已有完整工单三人组。</summary>
  244. public async Task<(bool Ready, int MasterCnt, int DetailCnt, int RoutingCnt)> ProbeReadyOnMesAsync(
  245. string domain, string workOrd, CancellationToken ct = default)
  246. {
  247. domain = string.IsNullOrWhiteSpace(domain) ? "8010" : domain.Trim();
  248. workOrd = workOrd.Trim();
  249. var ss = await _scopeFactory.GetScopeAsync(TargetSource, ct);
  250. var masterCnt = await ss.Ado.GetIntAsync(
  251. "SELECT COUNT(1) FROM WorkOrdMaster WHERE Domain=@d AND WorkOrd=@w",
  252. new SugarParameter("@d", domain),
  253. new SugarParameter("@w", workOrd));
  254. var detailCnt = await ss.Ado.GetIntAsync(
  255. "SELECT COUNT(1) FROM WorkOrdDetail WHERE Domain=@d AND WorkOrd=@w AND ISNULL(IsActive,0)=1",
  256. new SugarParameter("@d", domain),
  257. new SugarParameter("@w", workOrd));
  258. var routingCnt = await ss.Ado.GetIntAsync(
  259. "SELECT COUNT(1) FROM WorkOrdRouting WHERE Domain=@d AND WorkOrd=@w AND ISNULL(IsActive,0)=1",
  260. new SugarParameter("@d", domain),
  261. new SugarParameter("@w", workOrd));
  262. return (masterCnt > 0 && detailCnt > 0 && routingCnt > 0, masterCnt, detailCnt, routingCnt);
  263. }
  264. /// <summary>Mes 下达前置:165 上须已有完整工单三人组。</summary>
  265. public async Task AssertReadyOnMesAsync(string domain, string workOrd, CancellationToken ct = default)
  266. {
  267. try
  268. {
  269. var probe = await ProbeReadyOnMesAsync(domain, workOrd, ct);
  270. if (!probe.Ready)
  271. {
  272. throw Oops.Oh(PickBillUserMessages.ForReleaseFailure("WO_MES_NOT_READY"));
  273. }
  274. }
  275. catch (Exception ex) when (ex is not AppFriendlyException)
  276. {
  277. throw Oops.Oh(PickBillUserMessages.ForReleaseFailure("MES_UNREACHABLE"));
  278. }
  279. }
  280. private async Task<string> ResolveDomainAsync(long tenantId, string workOrd)
  281. {
  282. var domain = await _db.Ado.GetStringAsync(
  283. """
  284. SELECT TRIM(IFNULL(`Domain`, '')) FROM WorkOrdMaster
  285. WHERE tenant_id = @TenantId AND WorkOrd = @WorkOrd LIMIT 1
  286. """,
  287. new SugarParameter("@TenantId", tenantId),
  288. new SugarParameter("@WorkOrd", workOrd));
  289. return string.IsNullOrWhiteSpace(domain) ? "8010" : domain;
  290. }
  291. private async Task<bool> EnqueueRowAsync(
  292. long tenantId,
  293. string idem,
  294. string action,
  295. string table,
  296. Dictionary<string, object?> keys,
  297. Dictionary<string, object?> insert,
  298. Dictionary<string, object?> update,
  299. CancellationToken ct)
  300. {
  301. var payload = new Dictionary<string, object?>
  302. {
  303. ["op"] = "UPSERT",
  304. ["table"] = table,
  305. ["keys"] = keys,
  306. ["insert"] = insert,
  307. ["update"] = update,
  308. ["expect"] = new Dictionary<string, object?>()
  309. };
  310. var item = new MdpOutbox
  311. {
  312. TenantId = tenantId,
  313. TargetSourceCode = TargetSource,
  314. ActionCode = action,
  315. IdemKey = idem.Length > 200 ? idem[..200] : idem,
  316. PayloadJson = JsonSerializer.Serialize(payload)
  317. };
  318. return await _enqueue.TryEnqueueOrRefreshAsync(item, ct, pulse: false);
  319. }
  320. private static string StatusForMasterPush(string? local)
  321. {
  322. var t = (local ?? "").Trim().ToLowerInvariant();
  323. return t is "r" or "w" or "s" or "c" or "p" ? t : "p";
  324. }
  325. private static string StatusForRoutingPush(string masterStatus, string? localRouting)
  326. {
  327. var m = (masterStatus ?? "").Trim().ToLowerInvariant();
  328. if (m is "r" or "w" or "s")
  329. {
  330. var r = (localRouting ?? "").Trim().ToLowerInvariant();
  331. return string.IsNullOrEmpty(r) ? "r" : r;
  332. }
  333. // 计划态:不写执行态;空串表示未下达
  334. return "";
  335. }
  336. private static Dictionary<string, object?> BuildMasterInsert(
  337. string domain, WorkOrdMaster m, string status, DateTime now)
  338. {
  339. var insert = new Dictionary<string, object?>(StringComparer.OrdinalIgnoreCase)
  340. {
  341. ["Domain"] = domain,
  342. ["WorkOrd"] = m.WorkOrd,
  343. ["ID"] = m.WorkOrd,
  344. ["Status"] = status,
  345. ["Batch"] = m.Batch ?? "",
  346. ["ItemNum"] = m.ItemNum ?? "",
  347. ["Site"] = m.Site ?? "",
  348. ["Typed"] = string.IsNullOrWhiteSpace(m.Typed) ? "" : m.Typed.Trim(),
  349. ["QtyOrded"] = m.QtyOrded,
  350. ["ProdLine"] = m.ProdLine ?? "",
  351. ["Location"] = m.Location ?? "",
  352. ["Priority"] = m.Priority,
  353. ["Urgent"] = m.Urgent,
  354. ["ItemName"] = Trunc(m.ItemName, 256),
  355. ["Department"] = Trunc(m.Department, 64),
  356. ["RoutingCode"] = m.RoutingCode ?? "",
  357. ["BOMFormula"] = m.BOMFormula ?? "",
  358. ["Project"] = m.Project ?? "",
  359. ["Drawing"] = Trunc(m.Drawing, 64),
  360. ["Rev"] = m.Rev ?? "",
  361. ["CustNo"] = Trunc(m.CustNo, 64),
  362. ["BusinessID"] = m.BusinessID,
  363. ["IsActive"] = m.IsActive,
  364. ["IsConfirm"] = m.IsConfirm,
  365. ["CreateUser"] = Trunc(m.CreateUser ?? "aidop", 24),
  366. ["UpdateUser"] = Trunc(m.UpdateUser ?? m.CreateUser ?? "aidop", 24),
  367. ["CreateTime"] = Fmt(m.CreateTime ?? now),
  368. ["UpdateTime"] = Fmt(m.UpdateTime ?? now)
  369. };
  370. if (m.OrdDate != null) insert["OrdDate"] = Fmt(m.OrdDate);
  371. if (m.DueDate != null) insert["DueDate"] = Fmt(m.DueDate);
  372. if (m.ReleaseDate != null) insert["ReleaseDate"] = Fmt(m.ReleaseDate);
  373. return insert;
  374. }
  375. private static Dictionary<string, object?> BuildMasterUpdate(WorkOrdMaster m, string status, DateTime now)
  376. {
  377. var update = new Dictionary<string, object?>(StringComparer.OrdinalIgnoreCase)
  378. {
  379. ["Status"] = status,
  380. ["Batch"] = m.Batch ?? "",
  381. ["ItemNum"] = m.ItemNum ?? "",
  382. ["Site"] = m.Site ?? "",
  383. ["Typed"] = m.Typed ?? "",
  384. ["QtyOrded"] = m.QtyOrded,
  385. ["ProdLine"] = m.ProdLine ?? "",
  386. ["Location"] = m.Location ?? "",
  387. ["Priority"] = m.Priority,
  388. ["Urgent"] = m.Urgent,
  389. ["ItemName"] = Trunc(m.ItemName, 256),
  390. ["Department"] = Trunc(m.Department, 64),
  391. ["RoutingCode"] = m.RoutingCode ?? "",
  392. ["BOMFormula"] = m.BOMFormula ?? "",
  393. ["Project"] = m.Project ?? "",
  394. ["Drawing"] = Trunc(m.Drawing, 64),
  395. ["Rev"] = m.Rev ?? "",
  396. ["CustNo"] = Trunc(m.CustNo, 64),
  397. ["IsActive"] = m.IsActive,
  398. ["UpdateUser"] = Trunc(m.UpdateUser ?? "aidop", 24),
  399. ["UpdateTime"] = Fmt(now)
  400. };
  401. if (m.OrdDate != null) update["OrdDate"] = Fmt(m.OrdDate);
  402. if (m.DueDate != null) update["DueDate"] = Fmt(m.DueDate);
  403. if (m.ReleaseDate != null) update["ReleaseDate"] = Fmt(m.ReleaseDate);
  404. return update;
  405. }
  406. private static Dictionary<string, object?> BuildDetailInsert(string domain, string workOrd, DetailRow d, DateTime now)
  407. {
  408. return new Dictionary<string, object?>(StringComparer.OrdinalIgnoreCase)
  409. {
  410. ["Domain"] = domain,
  411. ["ID"] = "",
  412. ["WorkOrd"] = workOrd,
  413. ["LineNum"] = d.LineNum,
  414. ["Line"] = d.LineNum,
  415. ["ItemNum"] = d.ItemNum ?? "",
  416. ["Op"] = d.Op,
  417. ["Dimension1"] = "",
  418. ["Dimension2"] = "",
  419. ["Location"] = d.Location ?? "",
  420. ["Site"] = d.Site ?? "",
  421. ["QtyRequired"] = d.QtyRequired,
  422. ["FrozenBOMQty"] = d.FrozenBOMQty,
  423. ["LotSerial"] = Trunc(d.LotSerial, 25),
  424. ["Status"] = "",
  425. ["Typed"] = d.Typed ?? "",
  426. ["UM"] = d.UM ?? "",
  427. ["IsActive"] = d.IsActive != 0,
  428. ["BusinessID"] = d.BusinessID,
  429. ["QtyReturned"] = 0m,
  430. ["CreateUser"] = Trunc(d.CreateUser ?? "aidop", 24),
  431. ["UpdateUser"] = Trunc(d.UpdateUser ?? d.CreateUser ?? "aidop", 24),
  432. ["CreateTime"] = Fmt(d.CreateTime ?? now),
  433. ["UpdateTime"] = Fmt(d.UpdateTime ?? now)
  434. };
  435. }
  436. private static Dictionary<string, object?> BuildDetailUpdate(DetailRow d, DateTime now)
  437. {
  438. return new Dictionary<string, object?>(StringComparer.OrdinalIgnoreCase)
  439. {
  440. ["Location"] = d.Location ?? "",
  441. ["Site"] = d.Site ?? "",
  442. ["QtyRequired"] = d.QtyRequired,
  443. ["FrozenBOMQty"] = d.FrozenBOMQty,
  444. ["LotSerial"] = Trunc(d.LotSerial, 25),
  445. ["Typed"] = d.Typed ?? "",
  446. ["UM"] = d.UM ?? "",
  447. ["IsActive"] = d.IsActive != 0,
  448. ["UpdateUser"] = Trunc(d.UpdateUser ?? "aidop", 24),
  449. ["UpdateTime"] = Fmt(now)
  450. };
  451. }
  452. private static Dictionary<string, object?> BuildRoutingInsert(
  453. string domain, string workOrd, RoutingRow r, string status, DateTime now)
  454. {
  455. return new Dictionary<string, object?>(StringComparer.OrdinalIgnoreCase)
  456. {
  457. ["Domain"] = domain,
  458. ["ID"] = "",
  459. ["WorkOrd"] = workOrd,
  460. ["OP"] = r.OP,
  461. ["ItemNum"] = r.ItemNum ?? "",
  462. ["Descr"] = Trunc(r.Descr, 256),
  463. ["MilestoneOp"] = r.MilestoneOp != 0 ? 1 : 0,
  464. ["ParentOp"] = r.ParentOp,
  465. ["QtyOrded"] = r.QtyOrded,
  466. ["Status"] = status,
  467. ["ProdLine"] = Trunc(r.ProdLine, 8),
  468. ["WorkCtr"] = Trunc(r.WorkCtr, 8),
  469. ["Machine"] = Trunc(r.Machine, 800),
  470. ["RunCrew"] = r.RunCrew,
  471. ["StdSetupTime"] = r.StdSetupTime,
  472. ["ProcessOut"] = r.ProcessOut,
  473. ["IsActive"] = r.IsActive != 0,
  474. ["CreateUser"] = Trunc(r.CreateUser ?? "aidop", 24),
  475. ["UpdateUser"] = Trunc(r.UpdateUser ?? r.CreateUser ?? "aidop", 24),
  476. ["CreateTime"] = Fmt(r.CreateTime ?? now),
  477. ["UpdateTime"] = Fmt(r.UpdateTime ?? now)
  478. };
  479. }
  480. private static Dictionary<string, object?> BuildRoutingUpdate(RoutingRow r, string status, DateTime now)
  481. {
  482. return new Dictionary<string, object?>(StringComparer.OrdinalIgnoreCase)
  483. {
  484. ["ItemNum"] = r.ItemNum ?? "",
  485. ["Descr"] = Trunc(r.Descr, 256),
  486. ["MilestoneOp"] = r.MilestoneOp != 0 ? 1 : 0,
  487. ["ParentOp"] = r.ParentOp,
  488. ["QtyOrded"] = r.QtyOrded,
  489. ["Status"] = status,
  490. ["ProdLine"] = Trunc(r.ProdLine, 8),
  491. ["WorkCtr"] = Trunc(r.WorkCtr, 8),
  492. ["Machine"] = Trunc(r.Machine, 800),
  493. ["RunCrew"] = r.RunCrew,
  494. ["StdSetupTime"] = r.StdSetupTime,
  495. ["ProcessOut"] = r.ProcessOut,
  496. ["IsActive"] = r.IsActive != 0,
  497. ["UpdateUser"] = Trunc(r.UpdateUser ?? "aidop", 24),
  498. ["UpdateTime"] = Fmt(now)
  499. };
  500. }
  501. private static string Fmt(DateTime? dt) =>
  502. (dt ?? DateTime.Now).ToString("yyyy-MM-dd HH:mm:ss");
  503. private static string? Trunc(string? s, int max)
  504. {
  505. if (string.IsNullOrEmpty(s)) return s ?? "";
  506. return s.Length <= max ? s : s[..max];
  507. }
  508. private sealed class DetailRow
  509. {
  510. public string? Domain { get; set; }
  511. public string? WorkOrd { get; set; }
  512. public int LineNum { get; set; }
  513. public string? ItemNum { get; set; }
  514. public int Op { get; set; }
  515. public string? Location { get; set; }
  516. public string? Site { get; set; }
  517. public decimal QtyRequired { get; set; }
  518. public decimal FrozenBOMQty { get; set; }
  519. public string? LotSerial { get; set; }
  520. public string? Status { get; set; }
  521. public string? Typed { get; set; }
  522. public string? UM { get; set; }
  523. public int IsActive { get; set; }
  524. public long BusinessID { get; set; }
  525. public string? CreateUser { get; set; }
  526. public DateTime? CreateTime { get; set; }
  527. public string? UpdateUser { get; set; }
  528. public DateTime? UpdateTime { get; set; }
  529. }
  530. private sealed class RoutingRow
  531. {
  532. public string? Domain { get; set; }
  533. public string? WorkOrd { get; set; }
  534. public int OP { get; set; }
  535. public string? ItemNum { get; set; }
  536. public string? Descr { get; set; }
  537. public int MilestoneOp { get; set; }
  538. public int ParentOp { get; set; }
  539. public decimal QtyOrded { get; set; }
  540. public string? Status { get; set; }
  541. public string? ProdLine { get; set; }
  542. public string? WorkCtr { get; set; }
  543. public string? Machine { get; set; }
  544. public decimal RunCrew { get; set; }
  545. public decimal StdSetupTime { get; set; }
  546. public int ProcessOut { get; set; }
  547. public int IsActive { get; set; }
  548. public string? CreateUser { get; set; }
  549. public DateTime? CreateTime { get; set; }
  550. public string? UpdateUser { get; set; }
  551. public DateTime? UpdateTime { get; set; }
  552. }
  553. }