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:
@@ -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 =>
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user