InventoryBalanceCalculatorTests.cs 5.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123
  1. using Admin.NET.Plugin.AiDOP.MaterialWarehouse.InventoryPosting.Inventory;
  2. using Xunit;
  3. namespace Admin.NET.Plugin.AiDOP.Tests.S5.MaterialWarehouse;
  4. /// <summary>
  5. /// C5 库存量变化契约测试(Phase 5A;纯计算,无 DB)。
  6. /// 精确固化 pr_SFM_InventoryTransactionProcessing2 各 TransType 的 ΔQtyOnHand/ΔAvail/ΔAssay/ΔFreeze/ΔQtyOnOrd。
  7. /// </summary>
  8. public class InventoryBalanceCalculatorTests
  9. {
  10. private static InventoryTransactionCommand Cmd(string transType, decimal changeQty, decimal umc = 1m)
  11. => new InventoryTransactionCommand
  12. {
  13. TransType = transType, ChangeQty = changeQty, UmConversion = umc,
  14. ItemNum = "IT-1", Location = "L1",
  15. };
  16. // TransType | ChangeQty | umc | conv | ΔQtyOnHand | ΔAvail | ΔAssay | ΔFreeze | ΔQtyOnOrd
  17. [Theory]
  18. // 采购收货入质检库存(IQC 合格入库核心):在手+、质检+、可用不变
  19. [InlineData("rct-po-ins", 100, 1, 100, 100, 0, 100, 0, 0)]
  20. // 质检转出(ChangeQty<0 走 Assay 掩码):在手-、质检-
  21. [InlineData("iss-tr-ins", -30, 1, -30, -30, 0, -30, 0, 0)]
  22. // 质检相关入(ChangeQty>0 不走 Assay 掩码):在手+、可用+
  23. [InlineData("iss-tr-ins", 30, 1, 30, 30, 30, 0, 0, 0)]
  24. // 冻结:可用-、冻结+(ΔFreeze=-conv)
  25. [InlineData("inv-frz", -50, 1, -50, 0, -50, 0, 50, 0)]
  26. // 解冻:按 LIVE 同式(DB_PENDING 符号)
  27. [InlineData("inv-unfrz", -50, 1, -50, 0, -50, 0, 50, 0)]
  28. // 销售出库:在手-、可用-、在订-
  29. [InlineData("iss-so", -20, 1, -20, -20, -20, 0, 0, -20)]
  30. // 一般出入库(其它 TransType):在手/可用同步
  31. [InlineData("rct-x-ins", 40, 1, 40, 40, 40, 0, 0, 0)]
  32. // UMConversion≠1:conv=ChangeQty×umc
  33. [InlineData("rct-po-ins", 10, 2, 20, 20, 0, 20, 0, 0)]
  34. public void ComputeRowDeltas_PerTransType_MatchLiveFormula(
  35. string tt, double changeQty, double umc, double conv,
  36. double dQoh, double dAvail, double dAssay, double dFreeze, double dOrd)
  37. {
  38. var d = InventoryBalanceCalculator.ComputeRowDeltas(Cmd(tt, (decimal)changeQty, (decimal)umc));
  39. Assert.Equal((decimal)conv, d.Conv);
  40. Assert.Equal((decimal)dQoh, d.DeltaQtyOnHand);
  41. Assert.Equal((decimal)dAvail, d.DeltaAvailStatusQty);
  42. Assert.Equal((decimal)dAssay, d.DeltaAssay);
  43. Assert.Equal((decimal)dFreeze, d.DeltaFreezeQty);
  44. Assert.Equal((decimal)dOrd, d.DeltaQtyOnOrd);
  45. }
  46. [Fact]
  47. public void ComputeRowDeltas_UmConversionZero_NormalizedToOne()
  48. {
  49. var d = InventoryBalanceCalculator.ComputeRowDeltas(Cmd("rct-po-ins", 7m, 0m));
  50. Assert.Equal(7m, d.Conv); // umc 0 → 1
  51. Assert.Equal(7m, d.DeltaAssay);
  52. }
  53. [Fact]
  54. public void Project_BeforeToAfter_RctPoIns_IncreasesOnHandAndAssayOnly()
  55. {
  56. var before = new InventoryBalance { QtyOnHand = 100m, AvailStatusQty = 80m, Assay = 20m, FreezeQty = 0m, QtyOnOrd = 0m };
  57. var (d, after) = InventoryBalanceCalculator.Project(before, Cmd("rct-po-ins", 50m));
  58. Assert.Equal(50m, d.Conv);
  59. Assert.Equal(150m, after.QtyOnHand); // 在手 +50
  60. Assert.Equal(80m, after.AvailStatusQty); // 可用不变(进质检)
  61. Assert.Equal(70m, after.Assay); // 质检 +50
  62. Assert.Equal(0m, after.FreezeQty);
  63. // 不 mutate 入参
  64. Assert.Equal(100m, before.QtyOnHand);
  65. }
  66. [Fact]
  67. public void Project_Freeze_MovesAvailToFreeze()
  68. {
  69. var before = new InventoryBalance { QtyOnHand = 100m, AvailStatusQty = 100m, Assay = 0m, FreezeQty = 0m };
  70. var (_, after) = InventoryBalanceCalculator.Project(before, Cmd("inv-frz", -40m));
  71. Assert.Equal(100m, after.QtyOnHand); // 在手不变
  72. Assert.Equal(60m, after.AvailStatusQty); // 可用 -40
  73. Assert.Equal(40m, after.FreezeQty); // 冻结 +40
  74. }
  75. [Fact]
  76. public void BuildTransHist_RctPoIns_FieldsMatchLive()
  77. {
  78. var cmd = Cmd("rct-po-ins", 100m);
  79. cmd.Price = 5m;
  80. var d = InventoryBalanceCalculator.ComputeRowDeltas(cmd);
  81. var h = InventoryBalanceCalculator.BuildTransHist(cmd, d, beginBalance: 100m);
  82. Assert.Equal(100m, h.BeginBalance);
  83. Assert.Equal(100m, h.QtyChange); // 非 frz → conv
  84. Assert.Equal(0m, h.FreezeQty);
  85. Assert.Equal(100m, h.Assay);
  86. Assert.Equal(500m, h.Amt); // conv × Price
  87. Assert.Equal("rct-po-ins", h.TransType);
  88. }
  89. [Fact]
  90. public void BuildTransHist_FreezeTrans_QtyChangeZero()
  91. {
  92. var cmd = Cmd("inv-frz", -50m);
  93. var d = InventoryBalanceCalculator.ComputeRowDeltas(cmd);
  94. var h = InventoryBalanceCalculator.BuildTransHist(cmd, d, 0m);
  95. Assert.Equal(0m, h.QtyChange); // frz/unfrz 流水 QtyChange=0
  96. Assert.Equal(50m, h.FreezeQty);
  97. }
  98. [Fact]
  99. public void BuildTransHist_QcCheckRemark_UsesReasonAsTransType()
  100. {
  101. var cmd = Cmd("rct-po-ins", 10m);
  102. cmd.Remark = "QcCheck";
  103. cmd.Reason = "iqc-pass";
  104. var d = InventoryBalanceCalculator.ComputeRowDeltas(cmd);
  105. var h = InventoryBalanceCalculator.BuildTransHist(cmd, d, 0m);
  106. Assert.Equal("iqc-pass", h.TransType); // Remark=='QcCheck' → 取 Reason
  107. }
  108. [Fact]
  109. public void ComputeRowDeltas_NullCommand_Throws()
  110. {
  111. Assert.Throws<ArgumentNullException>(() => InventoryBalanceCalculator.ComputeRowDeltas(null));
  112. }
  113. }