IqcWmsResultPushService.cs 22 KB

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