using Admin.NET.Plugin.AiDOP.MaterialWarehouse.InventoryPosting.Inventory; using Xunit; namespace Admin.NET.Plugin.AiDOP.Tests.S5.MaterialWarehouse; /// /// C5 库存量变化契约测试(Phase 5A;纯计算,无 DB)。 /// 精确固化 pr_SFM_InventoryTransactionProcessing2 各 TransType 的 ΔQtyOnHand/ΔAvail/ΔAssay/ΔFreeze/ΔQtyOnOrd。 /// public class InventoryBalanceCalculatorTests { private static InventoryTransactionCommand Cmd(string transType, decimal changeQty, decimal umc = 1m) => new InventoryTransactionCommand { TransType = transType, ChangeQty = changeQty, UmConversion = umc, ItemNum = "IT-1", Location = "L1", }; // TransType | ChangeQty | umc | conv | ΔQtyOnHand | ΔAvail | ΔAssay | ΔFreeze | ΔQtyOnOrd [Theory] // 采购收货入质检库存(IQC 合格入库核心):在手+、质检+、可用不变 [InlineData("rct-po-ins", 100, 1, 100, 100, 0, 100, 0, 0)] // 质检转出(ChangeQty<0 走 Assay 掩码):在手-、质检- [InlineData("iss-tr-ins", -30, 1, -30, -30, 0, -30, 0, 0)] // 质检相关入(ChangeQty>0 不走 Assay 掩码):在手+、可用+ [InlineData("iss-tr-ins", 30, 1, 30, 30, 30, 0, 0, 0)] // 冻结:可用-、冻结+(ΔFreeze=-conv) [InlineData("inv-frz", -50, 1, -50, 0, -50, 0, 50, 0)] // 解冻:按 LIVE 同式(DB_PENDING 符号) [InlineData("inv-unfrz", -50, 1, -50, 0, -50, 0, 50, 0)] // 销售出库:在手-、可用-、在订- [InlineData("iss-so", -20, 1, -20, -20, -20, 0, 0, -20)] // 一般出入库(其它 TransType):在手/可用同步 [InlineData("rct-x-ins", 40, 1, 40, 40, 40, 0, 0, 0)] // UMConversion≠1:conv=ChangeQty×umc [InlineData("rct-po-ins", 10, 2, 20, 20, 0, 20, 0, 0)] public void ComputeRowDeltas_PerTransType_MatchLiveFormula( string tt, double changeQty, double umc, double conv, double dQoh, double dAvail, double dAssay, double dFreeze, double dOrd) { var d = InventoryBalanceCalculator.ComputeRowDeltas(Cmd(tt, (decimal)changeQty, (decimal)umc)); Assert.Equal((decimal)conv, d.Conv); Assert.Equal((decimal)dQoh, d.DeltaQtyOnHand); Assert.Equal((decimal)dAvail, d.DeltaAvailStatusQty); Assert.Equal((decimal)dAssay, d.DeltaAssay); Assert.Equal((decimal)dFreeze, d.DeltaFreezeQty); Assert.Equal((decimal)dOrd, d.DeltaQtyOnOrd); } [Fact] public void ComputeRowDeltas_UmConversionZero_NormalizedToOne() { var d = InventoryBalanceCalculator.ComputeRowDeltas(Cmd("rct-po-ins", 7m, 0m)); Assert.Equal(7m, d.Conv); // umc 0 → 1 Assert.Equal(7m, d.DeltaAssay); } [Fact] public void Project_BeforeToAfter_RctPoIns_IncreasesOnHandAndAssayOnly() { var before = new InventoryBalance { QtyOnHand = 100m, AvailStatusQty = 80m, Assay = 20m, FreezeQty = 0m, QtyOnOrd = 0m }; var (d, after) = InventoryBalanceCalculator.Project(before, Cmd("rct-po-ins", 50m)); Assert.Equal(50m, d.Conv); Assert.Equal(150m, after.QtyOnHand); // 在手 +50 Assert.Equal(80m, after.AvailStatusQty); // 可用不变(进质检) Assert.Equal(70m, after.Assay); // 质检 +50 Assert.Equal(0m, after.FreezeQty); // 不 mutate 入参 Assert.Equal(100m, before.QtyOnHand); } [Fact] public void Project_Freeze_MovesAvailToFreeze() { var before = new InventoryBalance { QtyOnHand = 100m, AvailStatusQty = 100m, Assay = 0m, FreezeQty = 0m }; var (_, after) = InventoryBalanceCalculator.Project(before, Cmd("inv-frz", -40m)); Assert.Equal(100m, after.QtyOnHand); // 在手不变 Assert.Equal(60m, after.AvailStatusQty); // 可用 -40 Assert.Equal(40m, after.FreezeQty); // 冻结 +40 } [Fact] public void BuildTransHist_RctPoIns_FieldsMatchLive() { var cmd = Cmd("rct-po-ins", 100m); cmd.Price = 5m; var d = InventoryBalanceCalculator.ComputeRowDeltas(cmd); var h = InventoryBalanceCalculator.BuildTransHist(cmd, d, beginBalance: 100m); Assert.Equal(100m, h.BeginBalance); Assert.Equal(100m, h.QtyChange); // 非 frz → conv Assert.Equal(0m, h.FreezeQty); Assert.Equal(100m, h.Assay); Assert.Equal(500m, h.Amt); // conv × Price Assert.Equal("rct-po-ins", h.TransType); } [Fact] public void BuildTransHist_FreezeTrans_QtyChangeZero() { var cmd = Cmd("inv-frz", -50m); var d = InventoryBalanceCalculator.ComputeRowDeltas(cmd); var h = InventoryBalanceCalculator.BuildTransHist(cmd, d, 0m); Assert.Equal(0m, h.QtyChange); // frz/unfrz 流水 QtyChange=0 Assert.Equal(50m, h.FreezeQty); } [Fact] public void BuildTransHist_QcCheckRemark_UsesReasonAsTransType() { var cmd = Cmd("rct-po-ins", 10m); cmd.Remark = "QcCheck"; cmd.Reason = "iqc-pass"; var d = InventoryBalanceCalculator.ComputeRowDeltas(cmd); var h = InventoryBalanceCalculator.BuildTransHist(cmd, d, 0m); Assert.Equal("iqc-pass", h.TransType); // Remark=='QcCheck' → 取 Reason } [Fact] public void ComputeRowDeltas_NullCommand_Throws() { Assert.Throws(() => InventoryBalanceCalculator.ComputeRowDeltas(null)); } }