IqcWmsResultPushService.cs 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538
  1. using System.Text.Json;
  2. using Admin.NET.Plugin.AiDOP.DataPlatform.Executors;
  3. using Admin.NET.Plugin.AiDOP.Entity.DataPlatform;
  4. using Microsoft.Extensions.Logging;
  5. using SqlSugar;
  6. namespace Admin.NET.Plugin.AiDOP.MaterialWarehouse;
  7. /// <summary>
  8. /// S5 IQC P3+P5:检验员提交结果后,按字段级回写 165(MissedPrint / TransHist / 报检分录 / 检验单),
  9. /// 并对解禁箱码入队 <c>op=PROC</c> 调用 <c>pr_WMS_BPM_SaveInvUpShelf @TransCode=QcCheck</c> 完成质检库存→合格库过账。
  10. /// 不写 PurOrdRctDetail;库存过账仅经白名单存储过程。
  11. /// </summary>
  12. public sealed class IqcWmsResultPushService : ITransient
  13. {
  14. public const string TargetSource = "DOPDEMORQ_SQLSERVER";
  15. public const string ActionMp = "IQC_WMS_MP";
  16. public const string ActionMpHist = "IQC_WMS_MP_HIST";
  17. public const string ActionEntry = "IQC_WMS_ENTRY";
  18. public const string ActionBill = "IQC_WMS_BILL";
  19. public const string ActionUpShelf = "IQC_WMS_UPSHELF";
  20. public const string QmsApiAction = "S5_IQC_RESULT_PUSH";
  21. private readonly ISqlSugarClient _db;
  22. private readonly MdpOutboxEnqueueService _enqueue;
  23. private readonly MdpOutboxWakeSignal _wake;
  24. private readonly ILogger<IqcWmsResultPushService> _logger;
  25. public IqcWmsResultPushService(
  26. ISqlSugarClient db,
  27. MdpOutboxEnqueueService enqueue,
  28. MdpOutboxWakeSignal wake,
  29. ILogger<IqcWmsResultPushService> logger)
  30. {
  31. _db = db;
  32. _enqueue = enqueue;
  33. _wake = wake;
  34. _logger = logger;
  35. }
  36. /// <summary>
  37. /// 提交结果后入队;失败不抛(不阻断主流程)。同时保留 QMS_API 占位分支。
  38. /// </summary>
  39. public async Task TryEnqueueAfterSubmitAsync(long billId, long tenantId, CancellationToken ct = default)
  40. {
  41. try
  42. {
  43. await EnqueueCoreAsync(billId, tenantId, ct);
  44. }
  45. catch (Exception ex)
  46. {
  47. _logger.LogWarning(ex, "[IqcWmsPush] 入队失败 billId={BillId}", billId);
  48. }
  49. }
  50. private async Task EnqueueCoreAsync(long billId, long tenantId, CancellationToken ct)
  51. {
  52. var bill = await _db.Ado.SqlQuerySingleAsync<BillRow>(
  53. """
  54. SELECT
  55. id AS Id, FBILLNO AS BillNo, lydjbh AS ApplyBillNo, hid AS Hid,
  56. pd AS Pd, dhsl AS Dhsl, bhgsl AS Bhgsl, clfs AS Clfs,
  57. FMATERIALCFG AS MaterialCode, jyr AS Inspector
  58. FROM qms_qcp_inspbill
  59. WHERE id = @Id AND tenant_id = @TenantId
  60. LIMIT 1
  61. """,
  62. new SugarParameter("@Id", billId),
  63. new SugarParameter("@TenantId", tenantId));
  64. if (bill == null) return;
  65. // 保留 QMS_API 占位(真实地址落地后可双投)
  66. await TryEnqueueQmsApiAsync(bill, tenantId, ct);
  67. var entry = await _db.Ado.SqlQuerySingleAsync<EntryRow>(
  68. """
  69. SELECT
  70. id AS Id, FSRCORDERNUM AS Receiver, FMATERIALCFG AS ItemNum,
  71. FAPPLYQTY AS ApplyQty
  72. FROM qms_qcp_insappnentry
  73. WHERE id = @Hid AND tenant_id = @TenantId
  74. LIMIT 1
  75. """,
  76. new SugarParameter("@Hid", bill.Hid ?? 0),
  77. new SugarParameter("@TenantId", tenantId));
  78. if (entry == null || string.IsNullOrWhiteSpace(entry.Receiver) || string.IsNullOrWhiteSpace(entry.ItemNum))
  79. {
  80. _logger.LogWarning(
  81. "[IqcWmsPush] 报检分录缺失 Receiver/ItemNum,跳过 165 推送 bill={BillNo}", bill.BillNo);
  82. return;
  83. }
  84. var domain = "8010";
  85. var qcNbr = string.IsNullOrWhiteSpace(bill.ApplyBillNo) ? bill.BillNo : bill.ApplyBillNo!;
  86. var userNo = string.IsNullOrWhiteSpace(bill.Inspector) ? "AIDOP" : bill.Inspector!;
  87. var now = DateTime.Now;
  88. // Ai-DOP clfs(0让步/1挑选/2报废) → 旧 DOP MRB 矩阵
  89. var (status, qtyAccFactor, qtyRejectFactor, resultDescr) = MapResult(bill.Pd, bill.Clfs);
  90. // 入队前预检:本库箱码(已与 165 同步)
  91. var barcodes = await _db.Ado.SqlQueryAsync<BarcodeRow>(
  92. """
  93. SELECT BarCode, Qty, Status, IFNULL(Domain,'8010') AS Domain
  94. FROM MissedPrint
  95. WHERE IFNULL(Domain,'') IN ('', @Domain)
  96. AND RctNbr = @Receiver AND ItemNum = @ItemNum
  97. AND IFNULL(Status,'') <> 'C'
  98. AND IFNULL(Type,'') <> 'Card'
  99. """,
  100. new SugarParameter("@Domain", domain),
  101. new SugarParameter("@Receiver", entry.Receiver),
  102. new SugarParameter("@ItemNum", entry.ItemNum));
  103. if (barcodes == null || barcodes.Count == 0)
  104. {
  105. _logger.LogWarning(
  106. "[IqcWmsPush] 无箱码可推 Receiver={Receiver} Item={Item}", entry.Receiver, entry.ItemNum);
  107. return;
  108. }
  109. var sumQty = barcodes.Sum(x => x.Qty);
  110. var qtyReceived = await _db.Ado.SqlQuerySingleAsync<decimal?>(
  111. """
  112. SELECT SUM(IFNULL(QtyReceived,0))
  113. FROM PurOrdRctDetail
  114. WHERE IFNULL(Domain,'') IN ('', @Domain)
  115. AND Receiver = @Receiver AND ItemNum = @ItemNum
  116. """,
  117. new SugarParameter("@Domain", domain),
  118. new SugarParameter("@Receiver", entry.Receiver),
  119. new SugarParameter("@ItemNum", entry.ItemNum)) ?? 0m;
  120. // 数量平衡:用检验数量口径(合格分支 dhsl 应等于收货数)
  121. var checkQty = (bill.Dhsl ?? 0m) + (bill.Bhgsl ?? 0m);
  122. if (checkQty <= 0) checkQty = entry.ApplyQty ?? 0m;
  123. if (qtyReceived > 0 && checkQty > 0 && checkQty != qtyReceived)
  124. {
  125. _logger.LogWarning(
  126. "[IqcWmsPush] 数量不平衡 check={Check} received={Recv},仍尝试推送(箱码侧)",
  127. checkQty, qtyReceived);
  128. }
  129. if (sumQty > 0 && qtyReceived > 0 && sumQty != qtyReceived)
  130. {
  131. _logger.LogWarning(
  132. "[IqcWmsPush] 箱码总数与收货数不一致 sum={Sum} received={Recv}", sumQty, qtyReceived);
  133. }
  134. // 待上架箱码(N=合格库 / R=不良品库);KEEP 不解禁也不上架
  135. var upShelfTargets = new List<(BarcodeRow Bc, string NewStatus)>();
  136. foreach (var bc in barcodes)
  137. {
  138. if (string.IsNullOrWhiteSpace(bc.BarCode)) continue;
  139. var qtyAccN = qtyAccFactor ? bc.Qty : 0m;
  140. var qtyRejectN = qtyRejectFactor ? bc.Qty : 0m;
  141. var newStatus = status;
  142. // 挑选且无箱码清单:旧系统不解禁任何箱码 → 保持原状态
  143. if (status == "KEEP") newStatus = string.IsNullOrWhiteSpace(bc.Status) ? "I" : bc.Status!;
  144. var mpPayload = JsonSerializer.Serialize(new
  145. {
  146. op = "UPDATE",
  147. table = "MissedPrint",
  148. keys = new Dictionary<string, object?>
  149. {
  150. ["Domain"] = string.IsNullOrWhiteSpace(bc.Domain) ? domain : bc.Domain,
  151. ["BarCode"] = bc.BarCode
  152. },
  153. update = new Dictionary<string, object?>
  154. {
  155. ["Status"] = newStatus,
  156. ["QtyAccN"] = qtyAccN,
  157. ["QtyRejectN"] = qtyRejectN,
  158. ["UpdateUser"] = userNo,
  159. ["UpdateTime"] = now.ToString("yyyy-MM-dd HH:mm:ss")
  160. }
  161. });
  162. await _enqueue.TryEnqueueAsync(new MdpOutbox
  163. {
  164. TenantId = tenantId,
  165. TargetSourceCode = TargetSource,
  166. ActionCode = ActionMp,
  167. IdemKey = $"iqc|mp|{qcNbr}|{bc.BarCode}",
  168. PayloadJson = mpPayload
  169. }, ct, pulse: false);
  170. var histPayload = JsonSerializer.Serialize(new
  171. {
  172. op = "INSERT",
  173. table = "MissedPrintTransHist",
  174. keys = new Dictionary<string, object?>
  175. {
  176. ["Domain"] = string.IsNullOrWhiteSpace(bc.Domain) ? domain : bc.Domain,
  177. ["BarCode"] = bc.BarCode,
  178. ["TransType"] = "来料检验-通过",
  179. ["CreateTime"] = now.ToString("yyyy-MM-dd HH:mm:ss.fff")
  180. },
  181. insert = new Dictionary<string, object?>
  182. {
  183. ["Domain"] = string.IsNullOrWhiteSpace(bc.Domain) ? domain : bc.Domain,
  184. ["ItemNum"] = entry.ItemNum,
  185. ["BarCode"] = bc.BarCode,
  186. ["Qty"] = bc.Qty,
  187. ["OrdNbr"] = null,
  188. ["Remark"] = "来料检验单:" + qcNbr,
  189. ["RctNbr"] = entry.Receiver,
  190. ["Status"] = newStatus,
  191. ["TransType"] = bill.Pd == 0 ? "来料检验-通过" : $"来料检验-{resultDescr}",
  192. ["CreateTime"] = now.ToString("yyyy-MM-dd HH:mm:ss"),
  193. ["CreateUser"] = userNo,
  194. ["UpdateTime"] = now.ToString("yyyy-MM-dd HH:mm:ss"),
  195. ["UpdateUser"] = userNo
  196. }
  197. });
  198. await _enqueue.TryEnqueueAsync(new MdpOutbox
  199. {
  200. TenantId = tenantId,
  201. TargetSourceCode = TargetSource,
  202. ActionCode = ActionMpHist,
  203. IdemKey = $"iqc|mph|{qcNbr}|{bc.BarCode}|{now:yyyyMMddHHmmss}",
  204. PayloadJson = histPayload
  205. }, ct, pulse: false);
  206. if (newStatus is "N" or "R")
  207. upShelfTargets.Add((bc, newStatus));
  208. }
  209. // 报检分录状态
  210. var entryPayload = JsonSerializer.Serialize(new
  211. {
  212. op = "UPDATE",
  213. table = "qms_qcp_insappnentry",
  214. keys = new Dictionary<string, object?> { ["id"] = entry.Id },
  215. update = new Dictionary<string, object?>
  216. {
  217. ["FINSPECTSTATUS"] = "检验完成",
  218. ["jywcsj"] = now.ToString("yyyy-MM-dd HH:mm:ss")
  219. }
  220. });
  221. await _enqueue.TryEnqueueAsync(new MdpOutbox
  222. {
  223. TenantId = tenantId,
  224. TargetSourceCode = TargetSource,
  225. ActionCode = ActionEntry,
  226. IdemKey = $"iqc|entry|{entry.Id}|{qcNbr}",
  227. PayloadJson = entryPayload
  228. }, ct, pulse: false);
  229. // 检验单推 165(含本库 id;用 FBILLNO 做自然键 UPSERT)
  230. var billPayload = JsonSerializer.Serialize(new
  231. {
  232. op = "UPSERT",
  233. table = "qms_qcp_inspbill",
  234. keys = new Dictionary<string, object?> { ["FBILLNO"] = bill.BillNo },
  235. insert = new Dictionary<string, object?>
  236. {
  237. ["id"] = bill.Id,
  238. ["FBILLNO"] = bill.BillNo,
  239. ["lydjbh"] = bill.ApplyBillNo,
  240. ["hid"] = entry.Id,
  241. ["FMATERIALCFG"] = bill.MaterialCode,
  242. ["pd"] = bill.Pd,
  243. ["dhsl"] = bill.Dhsl,
  244. ["bhgsl"] = bill.Bhgsl,
  245. ["clfs"] = bill.Clfs,
  246. ["jyr"] = bill.Inspector,
  247. ["FRINSQTY"] = entry.ApplyQty
  248. },
  249. update = new Dictionary<string, object?>
  250. {
  251. ["pd"] = bill.Pd,
  252. ["dhsl"] = bill.Dhsl,
  253. ["bhgsl"] = bill.Bhgsl,
  254. ["clfs"] = bill.Clfs,
  255. ["jyr"] = bill.Inspector
  256. }
  257. });
  258. // 无上架候选时由 bill 唤醒;有候选时由最后一条 UPSHELF 唤醒
  259. await _enqueue.TryEnqueueAsync(new MdpOutbox
  260. {
  261. TenantId = tenantId,
  262. TargetSourceCode = TargetSource,
  263. ActionCode = ActionBill,
  264. IdemKey = $"iqc|bill|{bill.BillNo}",
  265. PayloadJson = billPayload
  266. }, ct, pulse: upShelfTargets.Count == 0);
  267. // 本库同步置检验完成(写回过程在旧系统自动写;Ai-DOP 权威侧先落本地)
  268. await _db.Ado.ExecuteCommandAsync(
  269. """
  270. UPDATE qms_qcp_insappnentry
  271. SET FINSPECTSTATUS = '检验完成', jywcsj = @Now
  272. WHERE id = @Id AND tenant_id = @TenantId
  273. """,
  274. new SugarParameter("@Now", now.ToString("yyyy-MM-dd HH:mm:ss")),
  275. new SugarParameter("@Id", entry.Id),
  276. new SugarParameter("@TenantId", tenantId));
  277. var upShelfEnqueued = await EnqueueUpShelfAsync(
  278. upShelfTargets, entry, domain, qcNbr!, userNo, tenantId, ct);
  279. // 有候选但全部跳过(暂收 / 无默认库位)时 bill 未 pulse,补唤醒
  280. if (upShelfTargets.Count > 0 && upShelfEnqueued == 0)
  281. _wake.Pulse();
  282. }
  283. /// <summary>
  284. /// 合格/让步 → ItemMaster.Location;退货 → PurOrdControl.RejectInspectionLocation(165 resolve)。
  285. /// 暂收路径(ReceiptQty&gt;0)不入队,避免与 165 IQC 自带上架重复(任务书 U-F)。
  286. /// </summary>
  287. /// <returns>实际入队条数。</returns>
  288. private async Task<int> EnqueueUpShelfAsync(
  289. List<(BarcodeRow Bc, string NewStatus)> targets,
  290. EntryRow entry,
  291. string domain,
  292. string qcNbr,
  293. string userNo,
  294. long tenantId,
  295. CancellationToken ct)
  296. {
  297. if (targets.Count == 0) return 0;
  298. var receiptQty = await _db.Ado.SqlQuerySingleAsync<decimal?>(
  299. """
  300. SELECT SUM(IFNULL(d.ReceiptQty, 0))
  301. FROM PurOrdRctDetail r
  302. INNER JOIN PurOrdDetail d
  303. ON IFNULL(d.Domain,'') IN ('', @Domain)
  304. AND d.PurOrd = r.OrdNbr AND d.Line = r.OrdLine
  305. WHERE IFNULL(r.Domain,'') IN ('', @Domain)
  306. AND r.Receiver = @Receiver AND r.ItemNum = @ItemNum
  307. """,
  308. new SugarParameter("@Domain", domain),
  309. new SugarParameter("@Receiver", entry.Receiver!),
  310. new SugarParameter("@ItemNum", entry.ItemNum!)) ?? 0m;
  311. if (receiptQty > 0)
  312. {
  313. _logger.LogWarning(
  314. "[IqcWmsPush] ReceiptQty={Qty}>0(暂收路径),跳过 QcCheck 上架入队 Receiver={Receiver}",
  315. receiptQty, entry.Receiver);
  316. return 0;
  317. }
  318. string? passShelf = null;
  319. if (targets.Any(t => t.NewStatus == "N"))
  320. {
  321. passShelf = await _db.Ado.SqlQuerySingleAsync<string>(
  322. """
  323. SELECT Location
  324. FROM ItemMaster
  325. WHERE ItemNum = @ItemNum
  326. AND IFNULL(Domain,'') IN ('', @Domain)
  327. LIMIT 1
  328. """,
  329. new SugarParameter("@ItemNum", entry.ItemNum!),
  330. new SugarParameter("@Domain", domain));
  331. if (string.IsNullOrWhiteSpace(passShelf))
  332. {
  333. _logger.LogWarning(
  334. "[IqcWmsPush] ItemMaster.Location 为空,跳过合格品上架 Item={Item}", entry.ItemNum);
  335. }
  336. }
  337. var enqueued = 0;
  338. var pendingPulse = false;
  339. for (var i = 0; i < targets.Count; i++)
  340. {
  341. var (bc, newStatus) = targets[i];
  342. var bcDomain = string.IsNullOrWhiteSpace(bc.Domain) ? domain : bc.Domain!;
  343. Dictionary<string, object?> args;
  344. Dictionary<string, object>? resolve;
  345. if (newStatus == "N")
  346. {
  347. if (string.IsNullOrWhiteSpace(passShelf)) continue;
  348. args = new Dictionary<string, object?>
  349. {
  350. ["Domain"] = bcDomain,
  351. ["Shelf"] = passShelf,
  352. ["TransCode"] = "QcCheck",
  353. ["UserNo"] = userNo,
  354. ["IsProcCall"] = 1
  355. };
  356. resolve = new Dictionary<string, object>
  357. {
  358. ["Details"] = new Dictionary<string, object>
  359. {
  360. ["table"] = "MissedPrint",
  361. ["column"] = "RecID",
  362. ["match"] = new Dictionary<string, object?>
  363. {
  364. ["Domain"] = bcDomain,
  365. ["BarCode"] = bc.BarCode
  366. }
  367. }
  368. };
  369. }
  370. else // R:不良品库位由 165 PurOrdControl 现查
  371. {
  372. args = new Dictionary<string, object?>
  373. {
  374. ["Domain"] = bcDomain,
  375. ["TransCode"] = "QcCheck",
  376. ["UserNo"] = userNo,
  377. ["IsProcCall"] = 1
  378. };
  379. resolve = new Dictionary<string, object>
  380. {
  381. ["Details"] = new Dictionary<string, object>
  382. {
  383. ["table"] = "MissedPrint",
  384. ["column"] = "RecID",
  385. ["match"] = new Dictionary<string, object?>
  386. {
  387. ["Domain"] = bcDomain,
  388. ["BarCode"] = bc.BarCode
  389. }
  390. },
  391. ["Shelf"] = new Dictionary<string, object>
  392. {
  393. ["table"] = "PurOrdControl",
  394. ["column"] = "RejectInspectionLocation",
  395. ["match"] = new Dictionary<string, object?> { ["Domain"] = bcDomain }
  396. }
  397. };
  398. }
  399. var payload = JsonSerializer.Serialize(new
  400. {
  401. op = "PROC",
  402. proc = "pr_WMS_BPM_SaveInvUpShelf",
  403. args,
  404. resolve,
  405. output = "ReturnMsg",
  406. success = "成功",
  407. idempotent = new[] { "已是此货架", "已是此位置" }
  408. });
  409. // 先不 pulse,循环结束后对最后成功入队的一条补唤醒(避免中间 continue 导致未 pulse)
  410. var ok = await _enqueue.TryEnqueueAsync(new MdpOutbox
  411. {
  412. TenantId = tenantId,
  413. TargetSourceCode = TargetSource,
  414. ActionCode = ActionUpShelf,
  415. IdemKey = $"iqc|upshelf|{qcNbr}|{bc.BarCode}",
  416. PayloadJson = payload
  417. }, ct, pulse: false);
  418. if (ok)
  419. {
  420. enqueued++;
  421. pendingPulse = true;
  422. }
  423. }
  424. if (pendingPulse) _wake.Pulse();
  425. return enqueued;
  426. }
  427. private async Task TryEnqueueQmsApiAsync(BillRow bill, long tenantId, CancellationToken ct)
  428. {
  429. var billNo = string.IsNullOrWhiteSpace(bill.BillNo) ? bill.Id.ToString() : bill.BillNo!;
  430. var payload = JsonSerializer.Serialize(new
  431. {
  432. path = "/iqc/result",
  433. method = "POST",
  434. body = new
  435. {
  436. billId = bill.Id,
  437. billNo,
  438. pd = bill.Pd,
  439. dhsl = bill.Dhsl,
  440. bhgsl = bill.Bhgsl,
  441. action = QmsApiAction
  442. }
  443. });
  444. await _enqueue.TryEnqueueAsync(new MdpOutbox
  445. {
  446. TenantId = tenantId,
  447. TargetSourceCode = "QMS_API",
  448. ActionCode = QmsApiAction,
  449. IdemKey = billNo,
  450. PayloadJson = payload
  451. }, ct, pulse: false);
  452. }
  453. /// <summary>
  454. /// 返回 (箱码Status, 是否QtyAccN=Qty, 是否QtyRejectN=Qty, 描述)。
  455. /// Status=KEEP 表示挑选且无箱码清单时保持原状态。
  456. /// </summary>
  457. private static (string Status, bool QtyAcc, bool QtyReject, string Descr) MapResult(int? pd, long? clfs)
  458. {
  459. if (pd == 0)
  460. return ("N", true, false, "通过");
  461. // Ai-DOP:0让步 / 1挑选 / 2报废
  462. return clfs switch
  463. {
  464. 0 => ("N", true, false, "MRB让步接收"),
  465. 1 => ("KEEP", false, false, "MRB挑选使用"),
  466. 2 => ("R", false, true, "MRB退货"),
  467. _ => ("R", false, true, "退货")
  468. };
  469. }
  470. private sealed class BillRow
  471. {
  472. public long Id { get; set; }
  473. public string? BillNo { get; set; }
  474. public string? ApplyBillNo { get; set; }
  475. public long? Hid { get; set; }
  476. public int? Pd { get; set; }
  477. public decimal? Dhsl { get; set; }
  478. public decimal? Bhgsl { get; set; }
  479. public long? Clfs { get; set; }
  480. public string? MaterialCode { get; set; }
  481. public string? Inspector { get; set; }
  482. }
  483. private sealed class EntryRow
  484. {
  485. public long Id { get; set; }
  486. public string? Receiver { get; set; }
  487. public string? ItemNum { get; set; }
  488. public decimal? ApplyQty { get; set; }
  489. }
  490. private sealed class BarcodeRow
  491. {
  492. public string? BarCode { get; set; }
  493. public decimal Qty { get; set; }
  494. public string? Status { get; set; }
  495. public string? Domain { get; set; }
  496. }
  497. }