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
+17 -2
View File
@@ -112,9 +112,19 @@ public sealed class AdjustmentService : IAdjustmentService
}
public async Task<PagedResponse<AdjustmentSummaryDto>> ListAsync(
PageQuery query, int? warehouseId, int? branchId, int? reasonCodeId, AdjustmentStatus? status = null, CancellationToken ct = default)
PageQuery query, int? warehouseId, int? branchId, int? reasonCodeId, AdjustmentStatus? status = null,
CancellationToken ct = default, bool wastageOnly = false, int? itemId = null)
{
var q = _adjustments.Query().AsNoTracking();
// Adjustments that touch this item (the product page's activity).
if (itemId is not null) q = q.Where(a => a.Lines.Any(l => l.ItemId == itemId));
if (wastageOnly)
{
// Wastage = an adjustment with a damage / theft / expiry reason code.
var wastageCodes = WastageReasonCodes.Codes.Select(c => c.ToUpper()).ToList();
var wastageReasonIds = _reasonCodes.Query().Where(r => wastageCodes.Contains(r.Code.ToUpper())).Select(r => r.ReasonCodeId);
q = q.Where(a => wastageReasonIds.Contains(a.ReasonCodeId));
}
if (!string.IsNullOrWhiteSpace(query.Q))
{
@@ -189,7 +199,12 @@ public sealed class AdjustmentService : IAdjustmentService
// Validated only: posting resolves them again and creates any new ones exactly once.
DiscardUnsavedSerials();
var required = await _approvals.RequiredForStockAdjustmentAsync(value, ct);
// Wastage (a write-off with a damage / theft / expiry reason) has its own switch; every other adjustment and a
// stock-count variance keeps the stock-adjustment levels and threshold.
var isWastage = WastageReasonCodes.IsWastage(reason.Code) && request.Lines.All(l => l.QtyDelta < 0);
var required = isWastage
? await _approvals.RequiredForWastageAsync(ct)
: await _approvals.RequiredForStockAdjustmentAsync(value, ct);
var now = DateTime.UtcNow;
var (adjustment, refs) = await _uow.ExecuteInTransactionAsync(async token =>
+5 -1
View File
@@ -55,6 +55,7 @@ public sealed class ApprovalService : IApprovalService
config.StockAdjustmentLevels = Math.Clamp(request.StockAdjustmentLevels, 0, 2);
config.StockAdjustmentLevel2ThresholdValue = Math.Max(0m, request.StockAdjustmentLevel2ThresholdValue);
if (request.WastageLevels is int wastageLevels) config.WastageLevels = Math.Clamp(wastageLevels, 0, 2);
config.PriceAdjustmentLevels = Math.Clamp(request.PriceAdjustmentLevels, 0, 2);
config.PriceAdjustmentLevel2ThresholdPercent = Math.Max(0m, request.PriceAdjustmentLevel2ThresholdPercent);
var roleIds = request.Level2RoleIds.Where(id => id > 0).Distinct().ToList();
@@ -79,6 +80,9 @@ public sealed class ApprovalService : IApprovalService
return Levels(c.StockAdjustmentLevels, c.StockAdjustmentLevel2ThresholdValue, absoluteValue);
}
public async Task<int> RequiredForWastageAsync(CancellationToken ct = default)
=> Math.Clamp((await LoadConfigAsync(ct)).WastageLevels, 0, 2);
public async Task<int> RequiredForPriceAdjustmentAsync(decimal largestChangePercent, CancellationToken ct = default)
{
var c = await LoadConfigAsync(ct);
@@ -188,5 +192,5 @@ public sealed class ApprovalService : IApprovalService
private static ApprovalConfigDto Map(ApprovalConfig c) => new(
c.StockAdjustmentLevels, c.StockAdjustmentLevel2ThresholdValue,
c.PriceAdjustmentLevels, c.PriceAdjustmentLevel2ThresholdPercent,
Level2RoleIds(c).Order().ToList(), c.UpdatedAt);
Level2RoleIds(c).Order().ToList(), c.UpdatedAt, c.WastageLevels);
}
@@ -27,11 +27,11 @@ public sealed class ExpensePaymentService : IExpensePaymentService
private readonly IGeneralLedgerService _gl;
private readonly ICurrentUser _currentUser;
private readonly IUnitOfWork _uow;
private readonly string _glAccruedExpensesCode;
private readonly IGlAccountConfigService _glAccounts;
public ExpensePaymentService(
IRepository<Expense> expenses, IRepository<ExpensePayment> payments, ExpenseService expenseService,
IGeneralLedgerService gl, ICurrentUser currentUser, IUnitOfWork uow, IConfiguration configuration)
IGeneralLedgerService gl, ICurrentUser currentUser, IUnitOfWork uow, IGlAccountConfigService glAccounts)
{
_expenses = expenses;
_payments = payments;
@@ -39,7 +39,7 @@ public sealed class ExpensePaymentService : IExpensePaymentService
_gl = gl;
_currentUser = currentUser;
_uow = uow;
_glAccruedExpensesCode = configuration["Expense:GlAccruedExpensesCode"] ?? string.Empty;
_glAccounts = glAccounts;
}
public async Task<IReadOnlyList<ExpensePaymentDto>> PayAsync(int expenseId, CreateExpensePaymentRequest request, CancellationToken ct = default)
@@ -110,7 +110,8 @@ public sealed class ExpensePaymentService : IExpensePaymentService
{
var total = request.Lines.Sum(l => l.Amount);
var narration = $"Payment of expense {expense.DocNo}";
var glLines = new List<GlJournalEntryLineRequest> { new(_glAccruedExpensesCode, total, 0m, narration) };
var accruedExpensesCode = (await _glAccounts.GetCodesAsync(ct))[GlAccountRole.AccruedExpenses];
var glLines = new List<GlJournalEntryLineRequest> { new(accruedExpensesCode, total, 0m, narration) };
glLines.AddRange(request.Lines.Select(l => new GlJournalEntryLineRequest(accounts[l.GlBankAccountId].GlAccountCode, 0m, l.Amount, narration)));
var period = await _gl.GetPeriodByDateAsync(paymentDay, ct);
+7 -5
View File
@@ -32,12 +32,12 @@ public sealed class ExpenseService : IExpenseService
private readonly IBranchConfigService _branchConfig;
private readonly IUnitOfWork _uow;
private readonly IGeneralLedgerService _gl;
private readonly string _glAccruedExpensesCode;
private readonly IGlAccountConfigService _glAccounts;
public ExpenseService(
IRepository<Expense> expenses, IRepository<ExpenseTitle> titles, IRepository<Vendor> vendors,
ICurrentUser currentUser, INumberSequenceService numbers, IBranchConfigService branchConfig,
IUnitOfWork uow, IGeneralLedgerService gl, IConfiguration configuration)
IUnitOfWork uow, IGeneralLedgerService gl, IGlAccountConfigService glAccounts)
{
_expenses = expenses;
_titles = titles;
@@ -47,7 +47,7 @@ public sealed class ExpenseService : IExpenseService
_branchConfig = branchConfig;
_uow = uow;
_gl = gl;
_glAccruedExpensesCode = configuration["Expense:GlAccruedExpensesCode"] ?? string.Empty;
_glAccounts = glAccounts;
}
public async Task<PagedResponse<ExpenseSummaryDto>> ListAsync(PageQuery query, ExpenseListFilter filter, CancellationToken ct = default)
@@ -174,6 +174,7 @@ public sealed class ExpenseService : IExpenseService
var now = DateTime.UtcNow;
var narration = $"{expense.Category?.Name} - {title.Name} ({expense.DocNo})";
var accruedExpensesCode = (await _glAccounts.GetCodesAsync(ct))[GlAccountRole.AccruedExpenses];
var period = await _gl.GetPeriodByDateAsync(expense.ExpenseDate, ct);
var posted = await _gl.PostJournalEntryAsync(new GlJournalEntryRequest
{
@@ -185,7 +186,7 @@ public sealed class ExpenseService : IExpenseService
Lines =
[
new GlJournalEntryLineRequest(title.GlExpenseAccountCode, expense.Amount, 0m, narration),
new GlJournalEntryLineRequest(_glAccruedExpensesCode, 0m, expense.Amount, narration)
new GlJournalEntryLineRequest(accruedExpensesCode, 0m, expense.Amount, narration)
]
}, ct);
@@ -215,6 +216,7 @@ public sealed class ExpenseService : IExpenseService
{
var today = DateOnly.FromDateTime(now);
var narration = $"Cancellation of expense {expense.DocNo}";
var accruedExpensesCode = (await _glAccounts.GetCodesAsync(token))[GlAccountRole.AccruedExpenses];
var period = await _gl.GetPeriodByDateAsync(today, token);
var reversal = await _gl.PostJournalEntryAsync(new GlJournalEntryRequest
{
@@ -225,7 +227,7 @@ public sealed class ExpenseService : IExpenseService
Narration = narration,
Lines =
[
new GlJournalEntryLineRequest(_glAccruedExpensesCode, expense.Amount, 0m, narration),
new GlJournalEntryLineRequest(accruedExpensesCode, expense.Amount, 0m, narration),
new GlJournalEntryLineRequest(expense.GlExpenseAccountCode ?? string.Empty, 0m, expense.Amount, narration)
]
}, token);
+5 -1
View File
@@ -54,7 +54,11 @@ public enum GlAccountRole
InventoryLoss,
/// <summary>Cash takings at day-end close.</summary>
Cash
Cash,
/// <summary>Expenses booked but not yet paid (a liability): credited when an expense is posted, debited when it is paid.
/// Used to live only in appsettings (<c>Expense:GlAccruedExpensesCode</c>), where nobody could see or check it.</summary>
AccruedExpenses
}
/// <summary>
@@ -105,6 +105,8 @@ public sealed class GlAccountConfigService : IGlAccountConfigService
config.CogsAccountCode = submitted[GlAccountRole.Cogs];
config.InventoryLossAccountCode = submitted[GlAccountRole.InventoryLoss];
config.CashAccountCode = submitted[GlAccountRole.Cash];
// An older client that does not send this role keeps what is stored (it must never blank a mapping it can't see).
if (request.AccruedExpensesCode is not null) config.AccruedExpensesCode = submitted[GlAccountRole.AccruedExpenses];
config.UpdatedAt = DateTime.UtcNow;
config.UpdatedBy = _currentUser.AuditUserId;
@@ -146,7 +148,8 @@ public sealed class GlAccountConfigService : IGlAccountConfigService
[GlAccountRole.InventoryGain] = Trim(c.InventoryGainAccountCode),
[GlAccountRole.Cogs] = Trim(c.CogsAccountCode),
[GlAccountRole.InventoryLoss] = Trim(c.InventoryLossAccountCode),
[GlAccountRole.Cash] = Trim(c.CashAccountCode)
[GlAccountRole.Cash] = Trim(c.CashAccountCode),
[GlAccountRole.AccruedExpenses] = Trim(c.AccruedExpensesCode)
};
private static Dictionary<GlAccountRole, string?> Submitted(UpdateGlAccountConfigRequest r) => new()
@@ -162,7 +165,8 @@ public sealed class GlAccountConfigService : IGlAccountConfigService
[GlAccountRole.InventoryGain] = Trim(r.InventoryGainAccountCode),
[GlAccountRole.Cogs] = Trim(r.CogsAccountCode),
[GlAccountRole.InventoryLoss] = Trim(r.InventoryLossAccountCode),
[GlAccountRole.Cash] = Trim(r.CashAccountCode)
[GlAccountRole.Cash] = Trim(r.CashAccountCode),
[GlAccountRole.AccruedExpenses] = Trim(r.AccruedExpensesCode)
};
private static string? Trim(string? code) => string.IsNullOrWhiteSpace(code) ? null : code.Trim();
@@ -8,7 +8,8 @@ namespace ERPCore.Services.Interfaces;
public interface IAdjustmentService
{
Task<PagedResponse<AdjustmentSummaryDto>> ListAsync(
PageQuery query, int? warehouseId, int? branchId, int? reasonCodeId, AdjustmentStatus? status = null, CancellationToken ct = default);
PageQuery query, int? warehouseId, int? branchId, int? reasonCodeId, AdjustmentStatus? status = null,
CancellationToken ct = default, bool wastageOnly = false, int? itemId = null);
Task<AdjustmentDto?> GetAsync(int adjustmentId, CancellationToken ct = default);
@@ -29,6 +29,9 @@ public interface IApprovalService
/// <summary>Approvals a stock adjustment worth <paramref name="absoluteValue"/> needs (0, 1 or 2).</summary>
Task<int> RequiredForStockAdjustmentAsync(decimal absoluteValue, CancellationToken ct = default);
/// <summary>Approvals a wastage write-off needs (Settings -> Approvals "Wastage needs dual approval"): 0 or 2.</summary>
Task<int> RequiredForWastageAsync(CancellationToken ct = default);
/// <summary>Approvals a price change of <paramref name="largestChangePercent"/> needs (0, 1 or 2).</summary>
Task<int> RequiredForPriceAdjustmentAsync(decimal largestChangePercent, CancellationToken ct = default);