Add migration to include AccruedExpensesCode in gl_account_config table

- Introduced a new column 'AccruedExpensesCode' of type varchar(20) to the 'gl_account_config' table.
- Added SQL command to update existing records with a default value of '2300' for the 'AccruedExpensesCode' where it is currently null.
This commit is contained in:
Dhananjaya99
2026-10-18 16:21:38 +05:30
parent dcdb628647
commit a0793b35d3
31 changed files with 23185 additions and 94 deletions
+95 -6
View File
@@ -2,6 +2,7 @@ using ERPCore.Domain;
using ERPCore.Domain.Entities;
using ERPCore.Domain.Enums;
using ERPCore.Dtos.Approvals;
using ERPCore.Dtos.Common;
using ERPCore.Dtos.Notifications;
using ERPCore.Dtos.Products;
using ERPCore.Dtos.Stock;
@@ -28,7 +29,8 @@ public class DualApprovalTests
// Requester, two approvers, and a clerk who can approve but isn't a manager.
private const int Requester = 10, ApproverA = 11, ApproverB = 12, Clerk = 13;
private const int ApproverRoleId = 2, ClerkRoleId = 3;
private const int ItemId = 1, WarehouseId = 1, ReasonId = 1;
// ReasonId is an ordinary adjustment reason (VAR); WastageReasonId is a wastage one (DMG), which has its own approval switch.
private const int ItemId = 1, WarehouseId = 1, ReasonId = 1, WastageReasonId = 2;
// --- Stock adjustments ------------------------------------------------------------------
@@ -226,11 +228,95 @@ public class DualApprovalTests
public void RequiredLevels_FollowTheSettings(int configured, double threshold, double measure, int expected)
=> Assert.Equal(expected, ApprovalService.Levels(configured, (decimal)threshold, (decimal)measure));
// --- Wastage: its own approval switch ------------------------------------------------------
[Fact]
public async Task Wastage_WithTheSwitchOff_ReducesStockAtOnce_EvenWhenAdjustmentsNeedTwoApprovals()
{
var h = await Harness.CreateAsync(stockLevels: 2, wastageLevels: 0);
var dto = await h.Stock.CreateAsync(Request(-3, WastageReasonId));
Assert.Equal(AdjustmentStatus.Posted, dto.Status);
Assert.Equal(0, dto.RequiredApprovals);
Assert.Single(h.Mutator.Calls);
}
[Fact]
public async Task Wastage_WithTheSwitchOn_IsARequest_AndMovesNothingUntilApproved()
{
var h = await Harness.CreateAsync(stockLevels: 0, wastageLevels: 2);
var dto = await h.Stock.CreateAsync(Request(-3, WastageReasonId));
Assert.Equal(AdjustmentStatus.PendingApproval, dto.Status);
Assert.Equal(2, dto.RequiredApprovals);
Assert.Empty(h.Mutator.Calls);
h.User.Be(ApproverA);
await h.Stock.ApproveAsync(dto.AdjustmentId, null);
Assert.Empty(h.Mutator.Calls); // one of two: still nothing
h.User.Be(ApproverB);
var posted = await h.Stock.ApproveAsync(dto.AdjustmentId, null);
Assert.Equal(AdjustmentStatus.Posted, posted.Status);
Assert.Single(h.Mutator.Calls);
}
[Fact]
public async Task TheWastageSwitch_DoesNotLoosenOrdinaryAdjustments_AndTheirLevelsDoNotTouchWastage()
{
var h = await Harness.CreateAsync(stockLevels: 2, wastageLevels: 0);
// An ordinary adjustment (not a wastage reason) still needs two approvals.
var ordinary = await h.Stock.CreateAsync(Request(-3));
Assert.Equal(AdjustmentStatus.PendingApproval, ordinary.Status);
// A stock GAIN with a wastage reason is not wastage, so it follows the adjustment levels too.
var gain = await h.Stock.CreateAsync(Request(+3, WastageReasonId));
Assert.Equal(AdjustmentStatus.PendingApproval, gain.Status);
}
[Fact]
public async Task WastageList_ReturnsOnlyAdjustmentsWithAWastageReason()
{
var h = await Harness.CreateAsync(stockLevels: 0, wastageLevels: 0);
await h.Stock.CreateAsync(Request(-1, WastageReasonId));
await h.Stock.CreateAsync(Request(-2));
var all = await h.Stock.ListAsync(new PageQuery { Page = 1, PageSize = 50 }, null, null, null);
var wastage = await h.Stock.ListAsync(new PageQuery { Page = 1, PageSize = 50 }, null, null, null, null, CancellationToken.None, wastageOnly: true);
Assert.Equal(2, all.Items.Count);
var only = Assert.Single(wastage.Items);
Assert.Equal(WastageReasonId, only.ReasonCodeId);
}
[Fact]
public async Task WastageList_CanBeNarrowedToOneItem()
{
var h = await Harness.CreateAsync(stockLevels: 0, wastageLevels: 0);
await h.Stock.CreateAsync(Request(-1, WastageReasonId));
var forItem = await h.Stock.ListAsync(new PageQuery { Page = 1, PageSize = 50 }, null, null, null, null, CancellationToken.None, wastageOnly: true, itemId: ItemId);
var otherItem = await h.Stock.ListAsync(new PageQuery { Page = 1, PageSize = 50 }, null, null, null, null, CancellationToken.None, wastageOnly: true, itemId: 999);
Assert.Single(forItem.Items);
Assert.Empty(otherItem.Items);
}
[Fact]
public void WastageApproval_IsOffByDefault_AndTheSettingIsReadBack()
{
Assert.Equal(0, new ApprovalConfig().WastageLevels);
Assert.True(ERPCore.Domain.WastageReasonCodes.IsWastage("dmg"));
Assert.False(ERPCore.Domain.WastageReasonCodes.IsWastage("VAR"));
}
// --- helpers ------------------------------------------------------------------------------
private static CreateAdjustmentRequest Request(decimal qty) => new()
private static CreateAdjustmentRequest Request(decimal qty, int reasonId = ReasonId) => new()
{
WarehouseId = WarehouseId, BranchId = 1, ReasonCodeId = ReasonId,
WarehouseId = WarehouseId, BranchId = 1, ReasonCodeId = reasonId,
Lines = [new CreateAdjustmentLineInput { ItemId = ItemId, QtyDelta = qty }]
};
@@ -250,7 +336,7 @@ public class DualApprovalTests
public required AdjustmentService Stock { get; init; }
public required PriceAdjustmentService Prices { get; init; }
public static async Task<Harness> CreateAsync(int stockLevels = 2, decimal stockThreshold = 0, string? level2RoleIds = null)
public static async Task<Harness> CreateAsync(int stockLevels = 2, decimal stockThreshold = 0, string? level2RoleIds = null, int wastageLevels = 0)
{
var db = new ErpDbContext(
new DbContextOptionsBuilder<ErpDbContext>().UseInMemoryDatabase(Guid.NewGuid().ToString("N"), Root).Options,
@@ -279,11 +365,14 @@ public class DualApprovalTests
db.Uoms.Add(new Uom { UomId = 1, Name = "pcs" });
db.Items.Add(new Item { ItemId = ItemId, Sku = "SKU-1", Name = "Cola", BaseUomId = 1, CategoryId = 1,
SellingMinPrice = 90m, SellingMaxPrice = 150m, SellingFlatPrice = 100m });
db.ReasonCodes.Add(new ReasonCode { ReasonCodeId = ReasonId, Code = "DMG", Description = "Damage", Context = ReasonContext.Adjustment });
db.ReasonCodes.AddRange(
new ReasonCode { ReasonCodeId = ReasonId, Code = "VAR", Description = "Variance", Context = ReasonContext.Adjustment },
new ReasonCode { ReasonCodeId = WastageReasonId, Code = "DMG", Description = "Damage", Context = ReasonContext.Adjustment });
db.ApprovalConfig.Add(new ApprovalConfig
{
ApprovalConfigId = ApprovalConfig.SingletonId, StockAdjustmentLevels = stockLevels,
StockAdjustmentLevel2ThresholdValue = stockThreshold, PriceAdjustmentLevels = 2, Level2RoleIds = level2RoleIds
StockAdjustmentLevel2ThresholdValue = stockThreshold, PriceAdjustmentLevels = 2, Level2RoleIds = level2RoleIds,
WastageLevels = wastageLevels
});
await db.SaveChangesAsync();