Fix/ui bug fixers rebase repair #108

Merged
ImanThiyanga merged 4 commits from fix/ui-bug-fixers-rebase-repair into Dev 2026-10-14 11:15:31 +05:30
7 changed files with 657 additions and 49 deletions
Showing only changes of commit 3be425284c - Show all commits
+5 -1
View File
@@ -9,6 +9,7 @@ using ERPCore.Infra.Auth;
using ERPCore.Infra.Persistence;
using ERPCore.Repositories;
using ERPCore.Services;
using ERPCore.Services.Gl;
using ERPCore.Services.Interfaces;
using ERPCore.Services.Stock;
using ERPCore.System.Errors;
@@ -300,7 +301,10 @@ public class DualApprovalTests
new Repository<StockAdjustment>(db), new Repository<Warehouse>(db), new Repository<Item>(db),
new Repository<ReasonCode>(db), new Repository<StockLedger>(db), mutator, new FixedValuation(),
numbers, user, new FakeBranchConfig(), notifications, approvals, uow, db, gl,
new ConfigurationBuilder().Build(),
new StubGlAccountConfigService(
(GlAccountRole.Inventory, "1200"),
(GlAccountRole.InventoryGain, "4900"),
(GlAccountRole.InventoryLoss, "5900")),
new SerialTrackingService(new Repository<Serial>(db), new Repository<SerialMovement>(db), new Repository<StockLayer>(db),
new Repository<ProductConfig>(db), user));
var prices = new PriceAdjustmentService(
@@ -45,6 +45,10 @@ internal sealed class FakeGeneralLedger : IGeneralLedgerService
public Task<GlPeriod> GetPeriodByDateAsync(DateOnly date, CancellationToken ct) => Task.FromResult(new GlPeriod(1, 1));
public Task<IReadOnlyList<GlBankAccount>> ListBankAccountsAsync(CancellationToken ct) => Task.FromResult(Accounts);
/// <summary>Not implemented on purpose — nothing here picks a GL account by code; see
/// <see cref="StubGeneralLedgerService"/> for the fake that does.</summary>
public Task<IReadOnlyList<GlAccount>> ListAccountsAsync(CancellationToken ct) => throw new NotSupportedException();
}
/// <summary>The in-memory provider has no transactions: run the work, then save — what the real one commits.</summary>
@@ -1,4 +1,4 @@
// <auto-generated />
// <auto-generated />
using System;
using ERPCore.Infra.Persistence;
using Microsoft.EntityFrameworkCore;
@@ -1651,6 +1651,564 @@ namespace ERPCore.Migrations
b.ToTable("hr_employment_types", (string)null);
});
modelBuilder.Entity("ERPCore.Domain.Entities.Expense", b =>
{
b.Property<int>("ExpenseId")
.ValueGeneratedOnAdd()
.HasColumnType("int");
MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property<int>("ExpenseId"));
b.Property<decimal>("Amount")
.HasPrecision(18, 4)
.HasColumnType("decimal(18,4)");
b.Property<decimal>("BalanceAmount")
.HasPrecision(18, 4)
.HasColumnType("decimal(18,4)");
b.Property<string>("BillReference")
.HasMaxLength(50)
.HasColumnType("varchar(50)");
b.Property<int>("BranchId")
.HasColumnType("int");
b.Property<string>("CancelReason")
.HasMaxLength(250)
.HasColumnType("varchar(250)");
b.Property<DateTime>("CreatedAt")
.HasColumnType("datetime(6)");
b.Property<int>("CreatedBy")
.HasColumnType("int");
b.Property<string>("Description")
.HasMaxLength(500)
.HasColumnType("varchar(500)");
b.Property<string>("DocNo")
.IsRequired()
.HasMaxLength(30)
.HasColumnType("varchar(30)");
b.Property<DateOnly?>("DueDate")
.HasColumnType("date");
b.Property<int>("ExpenseCategoryId")
.HasColumnType("int");
b.Property<DateOnly>("ExpenseDate")
.HasColumnType("date");
b.Property<int>("ExpenseTitleId")
.HasColumnType("int");
b.Property<string>("GlExpenseAccountCode")
.HasMaxLength(20)
.HasColumnType("varchar(20)");
b.Property<string>("GlJournalNo")
.HasMaxLength(30)
.HasColumnType("varchar(30)");
b.Property<DateTime?>("GlPostedAt")
.HasColumnType("datetime(6)");
b.Property<string>("GlReversalJournalNo")
.HasMaxLength(30)
.HasColumnType("varchar(30)");
b.Property<decimal>("PaidAmount")
.HasPrecision(18, 4)
.HasColumnType("decimal(18,4)");
b.Property<string>("Payee")
.HasMaxLength(150)
.HasColumnType("varchar(150)");
b.Property<string>("PaymentStatus")
.IsRequired()
.ValueGeneratedOnAdd()
.HasMaxLength(20)
.HasColumnType("varchar(20)")
.HasDefaultValue("Unpaid");
b.Property<string>("PeriodMonth")
.HasMaxLength(7)
.HasColumnType("varchar(7)");
b.Property<uint>("RowVersion")
.IsConcurrencyToken()
.HasColumnType("int unsigned");
b.Property<string>("Status")
.IsRequired()
.ValueGeneratedOnAdd()
.HasMaxLength(20)
.HasColumnType("varchar(20)")
.HasDefaultValue("Draft");
b.Property<DateTime?>("UpdatedAt")
.HasColumnType("datetime(6)");
b.Property<int?>("VendorId")
.HasColumnType("int");
b.HasKey("ExpenseId");
b.HasIndex("BranchId");
b.HasIndex("CreatedBy");
b.HasIndex("DocNo")
.IsUnique();
b.HasIndex("ExpenseCategoryId");
b.HasIndex("ExpenseDate");
b.HasIndex("ExpenseTitleId");
b.HasIndex("PaymentStatus");
b.HasIndex("Status");
b.HasIndex("VendorId");
b.ToTable("expenses", null, t =>
{
t.HasCheckConstraint("ck_expenses_amount", "`Amount` > 0");
});
});
modelBuilder.Entity("ERPCore.Domain.Entities.ExpenseCategory", b =>
{
b.Property<int>("ExpenseCategoryId")
.ValueGeneratedOnAdd()
.HasColumnType("int");
MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property<int>("ExpenseCategoryId"));
b.Property<string>("Code")
.IsRequired()
.HasMaxLength(30)
.HasColumnType("varchar(30)");
b.Property<DateTime>("CreatedAt")
.HasColumnType("datetime(6)");
b.Property<bool>("IsSystem")
.HasColumnType("tinyint(1)");
b.Property<string>("Name")
.IsRequired()
.HasMaxLength(100)
.HasColumnType("varchar(100)");
b.Property<uint>("RowVersion")
.IsConcurrencyToken()
.HasColumnType("int unsigned");
b.Property<int>("SortOrder")
.HasColumnType("int");
b.Property<string>("Status")
.IsRequired()
.ValueGeneratedOnAdd()
.HasMaxLength(20)
.HasColumnType("varchar(20)")
.HasDefaultValue("Active");
b.Property<DateTime?>("UpdatedAt")
.HasColumnType("datetime(6)");
b.HasKey("ExpenseCategoryId");
b.HasIndex("Code")
.IsUnique();
b.HasIndex("Name")
.IsUnique();
b.ToTable("expense_categories", (string)null);
b.HasData(
new
{
ExpenseCategoryId = 1,
Code = "UTILITY",
CreatedAt = new DateTime(2026, 10, 7, 0, 0, 0, 0, DateTimeKind.Utc),
IsSystem = true,
Name = "Utility",
RowVersion = 1u,
SortOrder = 1,
Status = "Active"
},
new
{
ExpenseCategoryId = 2,
Code = "RENTAL",
CreatedAt = new DateTime(2026, 10, 7, 0, 0, 0, 0, DateTimeKind.Utc),
IsSystem = true,
Name = "Rental",
RowVersion = 1u,
SortOrder = 2,
Status = "Active"
},
new
{
ExpenseCategoryId = 3,
Code = "SALARIES",
CreatedAt = new DateTime(2026, 10, 7, 0, 0, 0, 0, DateTimeKind.Utc),
IsSystem = true,
Name = "Salaries",
RowVersion = 1u,
SortOrder = 3,
Status = "Active"
},
new
{
ExpenseCategoryId = 4,
Code = "OTHER",
CreatedAt = new DateTime(2026, 10, 7, 0, 0, 0, 0, DateTimeKind.Utc),
IsSystem = true,
Name = "Other",
RowVersion = 1u,
SortOrder = 4,
Status = "Active"
});
});
modelBuilder.Entity("ERPCore.Domain.Entities.ExpensePayment", b =>
{
b.Property<int>("ExpensePaymentId")
.ValueGeneratedOnAdd()
.HasColumnType("int");
MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property<int>("ExpensePaymentId"));
b.Property<decimal>("Amount")
.HasPrecision(18, 4)
.HasColumnType("decimal(18,4)");
b.Property<string>("BankAccountName")
.IsRequired()
.HasMaxLength(200)
.HasColumnType("varchar(200)");
b.Property<string>("ChequeNo")
.HasMaxLength(30)
.HasColumnType("varchar(30)");
b.Property<DateTime>("CreatedAt")
.HasColumnType("datetime(6)");
b.Property<int>("CreatedBy")
.HasColumnType("int");
b.Property<int>("ExpenseId")
.HasColumnType("int");
b.Property<long>("GlBankAccountId")
.HasColumnType("bigint");
b.Property<string>("GlJournalNo")
.HasMaxLength(30)
.HasColumnType("varchar(30)");
b.Property<DateTime>("PaymentDate")
.HasColumnType("datetime(6)");
b.Property<string>("PaymentMethod")
.IsRequired()
.HasMaxLength(20)
.HasColumnType("varchar(20)");
b.Property<string>("Reference")
.HasMaxLength(100)
.HasColumnType("varchar(100)");
b.HasKey("ExpensePaymentId");
b.HasIndex("CreatedBy");
b.HasIndex("ExpenseId");
b.ToTable("expense_payments", (string)null);
});
modelBuilder.Entity("ERPCore.Domain.Entities.ExpenseTitle", b =>
{
b.Property<int>("ExpenseTitleId")
.ValueGeneratedOnAdd()
.HasColumnType("int");
MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property<int>("ExpenseTitleId"));
b.Property<DateTime>("CreatedAt")
.HasColumnType("datetime(6)");
b.Property<int>("ExpenseCategoryId")
.HasColumnType("int");
b.Property<string>("GlExpenseAccountCode")
.IsRequired()
.HasMaxLength(20)
.HasColumnType("varchar(20)");
b.Property<bool>("IsSystem")
.HasColumnType("tinyint(1)");
b.Property<string>("Name")
.IsRequired()
.HasMaxLength(100)
.HasColumnType("varchar(100)");
b.Property<uint>("RowVersion")
.IsConcurrencyToken()
.HasColumnType("int unsigned");
b.Property<int>("SortOrder")
.HasColumnType("int");
b.Property<string>("Status")
.IsRequired()
.ValueGeneratedOnAdd()
.HasMaxLength(20)
.HasColumnType("varchar(20)")
.HasDefaultValue("Active");
b.Property<DateTime?>("UpdatedAt")
.HasColumnType("datetime(6)");
b.HasKey("ExpenseTitleId");
b.HasIndex("ExpenseCategoryId", "Name")
.IsUnique();
b.ToTable("expense_titles", (string)null);
b.HasData(
new
{
ExpenseTitleId = 1,
CreatedAt = new DateTime(2026, 10, 7, 0, 0, 0, 0, DateTimeKind.Utc),
ExpenseCategoryId = 1,
GlExpenseAccountCode = "5200-01",
IsSystem = true,
Name = "Electricity",
RowVersion = 1u,
SortOrder = 1,
Status = "Active"
},
new
{
ExpenseTitleId = 2,
CreatedAt = new DateTime(2026, 10, 7, 0, 0, 0, 0, DateTimeKind.Utc),
ExpenseCategoryId = 1,
GlExpenseAccountCode = "5200-01",
IsSystem = true,
Name = "Water",
RowVersion = 1u,
SortOrder = 2,
Status = "Active"
},
new
{
ExpenseTitleId = 3,
CreatedAt = new DateTime(2026, 10, 7, 0, 0, 0, 0, DateTimeKind.Utc),
ExpenseCategoryId = 1,
GlExpenseAccountCode = "5200-01",
IsSystem = true,
Name = "Telephone",
RowVersion = 1u,
SortOrder = 3,
Status = "Active"
},
new
{
ExpenseTitleId = 4,
CreatedAt = new DateTime(2026, 10, 7, 0, 0, 0, 0, DateTimeKind.Utc),
ExpenseCategoryId = 1,
GlExpenseAccountCode = "5200-01",
IsSystem = true,
Name = "Internet",
RowVersion = 1u,
SortOrder = 4,
Status = "Active"
},
new
{
ExpenseTitleId = 5,
CreatedAt = new DateTime(2026, 10, 7, 0, 0, 0, 0, DateTimeKind.Utc),
ExpenseCategoryId = 1,
GlExpenseAccountCode = "5200-01",
IsSystem = true,
Name = "Gas",
RowVersion = 1u,
SortOrder = 5,
Status = "Active"
},
new
{
ExpenseTitleId = 6,
CreatedAt = new DateTime(2026, 10, 7, 0, 0, 0, 0, DateTimeKind.Utc),
ExpenseCategoryId = 2,
GlExpenseAccountCode = "5200-02",
IsSystem = true,
Name = "Building Rent",
RowVersion = 1u,
SortOrder = 1,
Status = "Active"
},
new
{
ExpenseTitleId = 7,
CreatedAt = new DateTime(2026, 10, 7, 0, 0, 0, 0, DateTimeKind.Utc),
ExpenseCategoryId = 2,
GlExpenseAccountCode = "5200-02",
IsSystem = true,
Name = "Vehicle Rent",
RowVersion = 1u,
SortOrder = 2,
Status = "Active"
},
new
{
ExpenseTitleId = 8,
CreatedAt = new DateTime(2026, 10, 7, 0, 0, 0, 0, DateTimeKind.Utc),
ExpenseCategoryId = 2,
GlExpenseAccountCode = "5200-02",
IsSystem = true,
Name = "Equipment Rent",
RowVersion = 1u,
SortOrder = 3,
Status = "Active"
},
new
{
ExpenseTitleId = 9,
CreatedAt = new DateTime(2026, 10, 7, 0, 0, 0, 0, DateTimeKind.Utc),
ExpenseCategoryId = 3,
GlExpenseAccountCode = "5200-03",
IsSystem = true,
Name = "Staff Salaries",
RowVersion = 1u,
SortOrder = 1,
Status = "Active"
},
new
{
ExpenseTitleId = 10,
CreatedAt = new DateTime(2026, 10, 7, 0, 0, 0, 0, DateTimeKind.Utc),
ExpenseCategoryId = 3,
GlExpenseAccountCode = "5200-03",
IsSystem = true,
Name = "Wages",
RowVersion = 1u,
SortOrder = 2,
Status = "Active"
},
new
{
ExpenseTitleId = 11,
CreatedAt = new DateTime(2026, 10, 7, 0, 0, 0, 0, DateTimeKind.Utc),
ExpenseCategoryId = 3,
GlExpenseAccountCode = "5200-03",
IsSystem = true,
Name = "Overtime",
RowVersion = 1u,
SortOrder = 3,
Status = "Active"
},
new
{
ExpenseTitleId = 12,
CreatedAt = new DateTime(2026, 10, 7, 0, 0, 0, 0, DateTimeKind.Utc),
ExpenseCategoryId = 3,
GlExpenseAccountCode = "5200-03",
IsSystem = true,
Name = "Bonus",
RowVersion = 1u,
SortOrder = 4,
Status = "Active"
},
new
{
ExpenseTitleId = 13,
CreatedAt = new DateTime(2026, 10, 7, 0, 0, 0, 0, DateTimeKind.Utc),
ExpenseCategoryId = 3,
GlExpenseAccountCode = "5200-03",
IsSystem = true,
Name = "EPF / ETF Contribution",
RowVersion = 1u,
SortOrder = 5,
Status = "Active"
},
new
{
ExpenseTitleId = 14,
CreatedAt = new DateTime(2026, 10, 7, 0, 0, 0, 0, DateTimeKind.Utc),
ExpenseCategoryId = 4,
GlExpenseAccountCode = "5200-04",
IsSystem = true,
Name = "Stationery",
RowVersion = 1u,
SortOrder = 1,
Status = "Active"
},
new
{
ExpenseTitleId = 15,
CreatedAt = new DateTime(2026, 10, 7, 0, 0, 0, 0, DateTimeKind.Utc),
ExpenseCategoryId = 4,
GlExpenseAccountCode = "5200-04",
IsSystem = true,
Name = "Transport",
RowVersion = 1u,
SortOrder = 2,
Status = "Active"
},
new
{
ExpenseTitleId = 16,
CreatedAt = new DateTime(2026, 10, 7, 0, 0, 0, 0, DateTimeKind.Utc),
ExpenseCategoryId = 4,
GlExpenseAccountCode = "5200-04",
IsSystem = true,
Name = "Repairs & Maintenance",
RowVersion = 1u,
SortOrder = 3,
Status = "Active"
},
new
{
ExpenseTitleId = 17,
CreatedAt = new DateTime(2026, 10, 7, 0, 0, 0, 0, DateTimeKind.Utc),
ExpenseCategoryId = 4,
GlExpenseAccountCode = "5200-04",
IsSystem = true,
Name = "Cleaning",
RowVersion = 1u,
SortOrder = 4,
Status = "Active"
},
new
{
ExpenseTitleId = 18,
CreatedAt = new DateTime(2026, 10, 7, 0, 0, 0, 0, DateTimeKind.Utc),
ExpenseCategoryId = 4,
GlExpenseAccountCode = "5200-04",
IsSystem = true,
Name = "Miscellaneous",
RowVersion = 1u,
SortOrder = 5,
Status = "Active"
});
});
modelBuilder.Entity("ERPCore.Domain.Entities.GlAccountConfig", b =>
{
b.Property<int>("ConfigId")
@@ -8139,6 +8697,78 @@ namespace ERPCore.Migrations
b.Navigation("SalaryComponent");
});
modelBuilder.Entity("ERPCore.Domain.Entities.Expense", b =>
{
b.HasOne("ERPCore.Domain.Entities.Branch", "Branch")
.WithMany()
.HasForeignKey("BranchId")
.OnDelete(DeleteBehavior.Restrict)
.IsRequired();
b.HasOne("ERPCore.Domain.Entities.User", "Creator")
.WithMany()
.HasForeignKey("CreatedBy")
.OnDelete(DeleteBehavior.Restrict)
.IsRequired();
b.HasOne("ERPCore.Domain.Entities.ExpenseCategory", "Category")
.WithMany()
.HasForeignKey("ExpenseCategoryId")
.OnDelete(DeleteBehavior.Restrict)
.IsRequired();
b.HasOne("ERPCore.Domain.Entities.ExpenseTitle", "Title")
.WithMany()
.HasForeignKey("ExpenseTitleId")
.OnDelete(DeleteBehavior.Restrict)
.IsRequired();
b.HasOne("ERPCore.Domain.Entities.Vendor", "Vendor")
.WithMany()
.HasForeignKey("VendorId")
.OnDelete(DeleteBehavior.Restrict);
b.Navigation("Branch");
b.Navigation("Category");
b.Navigation("Creator");
b.Navigation("Title");
b.Navigation("Vendor");
});
modelBuilder.Entity("ERPCore.Domain.Entities.ExpensePayment", b =>
{
b.HasOne("ERPCore.Domain.Entities.User", "Creator")
.WithMany()
.HasForeignKey("CreatedBy")
.OnDelete(DeleteBehavior.Restrict)
.IsRequired();
b.HasOne("ERPCore.Domain.Entities.Expense", "Expense")
.WithMany("Payments")
.HasForeignKey("ExpenseId")
.OnDelete(DeleteBehavior.Restrict)
.IsRequired();
b.Navigation("Creator");
b.Navigation("Expense");
});
modelBuilder.Entity("ERPCore.Domain.Entities.ExpenseTitle", b =>
{
b.HasOne("ERPCore.Domain.Entities.ExpenseCategory", "Category")
.WithMany("Titles")
.HasForeignKey("ExpenseCategoryId")
.OnDelete(DeleteBehavior.Restrict)
.IsRequired();
b.Navigation("Category");
});
modelBuilder.Entity("ERPCore.Domain.Entities.GlAccountConfig", b =>
{
b.HasOne("ERPCore.Domain.Entities.User", "UpdatedByUser")
@@ -53,9 +53,10 @@ public sealed class AdjustmentService : IAdjustmentService
public AdjustmentService(
IRepository<StockAdjustment> adjustments, IRepository<Warehouse> warehouses, IRepository<Item> items,
IRepository<ReasonCode> reasonCodes, IRepository<StockLedger> ledger, IStockMutator mutator,
INumberSequenceService numbers, ICurrentUser currentUser, IBranchConfigService branchConfig, INotificationService notifications, IUnitOfWork uow,
IGeneralLedgerService gl, IGlAccountConfigService glAccounts)
IRepository<ReasonCode> reasonCodes, IRepository<StockLedger> ledger, IStockMutator mutator, IFifoCostingService fifo,
INumberSequenceService numbers, ICurrentUser currentUser, IBranchConfigService branchConfig, INotificationService notifications,
IApprovalService approvals, IUnitOfWork uow, ErpDbContext db,
IGeneralLedgerService gl, IGlAccountConfigService glAccounts, ISerialTrackingService serialTracking)
{
_serialTracking = serialTracking;
_adjustments = adjustments;
+5 -39
View File
@@ -38,14 +38,15 @@ public sealed class CountService : ICountService
private readonly IRepository<StockLayer> _stockLayers;
private readonly IRepository<Batch> _batches;
private readonly IRepository<Serial> _serials;
private readonly IGlAccountConfigService _glAccounts;
private readonly ISerialTrackingService _serialTracking;
private readonly AdjustmentService _adjustments;
public CountService(
IRepository<StockCount> counts, IRepository<Warehouse> warehouses,
IRepository<Item> items, IRepository<ReasonCode> reasonCodes, IFifoCostingService fifo,
INumberSequenceService numbers, ICurrentUser currentUser, IBranchConfigService branchConfig, IUnitOfWork uow,
IGeneralLedgerService gl, IRepository<StockLayer> stockLayers, IRepository<Batch> batches,
IRepository<Serial> serials, IGlAccountConfigService glAccounts)
IRepository<StockLayer> stockLayers, IRepository<Batch> batches,
IRepository<Serial> serials, ISerialTrackingService serialTracking, AdjustmentService adjustments)
{
_serialTracking = serialTracking;
_counts = counts;
@@ -60,42 +61,7 @@ public sealed class CountService : ICountService
_stockLayers = stockLayers;
_batches = batches;
_serials = serials;
_glAccounts = glAccounts;
}
/// <summary>Same logic as <c>AdjustmentService</c>'s private method of the same name — see there for why
/// gains/losses post as separate lines instead of a net.</summary>
private async Task<string?> PostAdjustmentJournalAsync(
string docNo, DateTime now, IReadOnlyList<StockLedger> refs, CancellationToken ct)
{
var gain = refs.Where(r => r.Direction == Direction.In).Sum(r => r.Value);
var loss = refs.Where(r => r.Direction == Direction.Out).Sum(r => r.Value);
if (gain <= 0 && loss <= 0) return null;
var glCodes = await _glAccounts.GetCodesAsync(ct);
var period = await _gl.GetPeriodByDateAsync(BusinessClock.ToBusinessDate(now), ct);
var lines = new List<GlJournalEntryLineRequest>();
if (gain > 0)
{
lines.Add(new GlJournalEntryLineRequest(glCodes[GlAccountRole.Inventory], gain, 0m, $"Adjustment {docNo} — stock increase"));
lines.Add(new GlJournalEntryLineRequest(glCodes[GlAccountRole.InventoryGain], 0m, gain, $"Adjustment {docNo} — inventory gain"));
}
if (loss > 0)
{
lines.Add(new GlJournalEntryLineRequest(glCodes[GlAccountRole.InventoryLoss], loss, 0m, $"Adjustment {docNo} — inventory loss"));
lines.Add(new GlJournalEntryLineRequest(glCodes[GlAccountRole.Inventory], 0m, loss, $"Adjustment {docNo} — stock decrease"));
}
var result = await _gl.PostJournalEntryAsync(new GlJournalEntryRequest
{
PeriodId = period.PeriodId,
EntryDate = BusinessClock.ToBusinessDate(now),
SourceModule = "ADJUSTMENT",
Reference = docNo,
Narration = $"Stock adjustment {docNo}",
Lines = lines
}, ct);
return result.JournalNo;
_adjustments = adjustments;
}
public async Task<PagedResponse<CountSummaryDto>> ListAsync(
@@ -64,11 +64,11 @@ public sealed class PurchaseInvoicePaymentService : IPurchaseInvoicePaymentServi
var payment = await _uow.ExecuteInTransactionAsync(async token =>
{
var glCodes = await _glAccounts.GetCodesAsync(token);
var period = await _gl.GetPeriodByDateAsync(BusinessClock.ToBusinessDate(now), token);
var period = await _gl.GetPeriodByDateAsync(paymentDay, token);
var posted = await _gl.PostJournalEntryAsync(new GlJournalEntryRequest
{
PeriodId = period.PeriodId,
EntryDate = BusinessClock.ToBusinessDate(now),
EntryDate = paymentDay,
SourceModule = "PURCHASE_INVOICE_PAYMENT",
Reference = invoice.DocNo,
Narration = $"Payment against invoice {invoice.DocNo}",
@@ -104,13 +104,14 @@ public sealed class PurchaseInvoicePaymentService : IPurchaseInvoicePaymentServi
/// <summary>
/// The payment's calendar day (also the GL entry date) and the timestamp stored on it — same
/// rule as <see cref="SalesInvoicePaymentService"/>. Omitted ⇒ today. One day of slack on
/// rule as <see cref="SalesInvoicePaymentService"/>. Omitted ⇒ today — the <em>business</em> day
/// (<see cref="BusinessClock"/>), not the UTC one. One day of slack on
/// "future": the client sends its local date, which runs ahead of UTC for users east of it.
/// Today keeps the actual time of entry; a back-dated payment is stamped at that day's start.
/// </summary>
private static (DateOnly Day, DateTime Stamp) ResolvePaymentDate(DateOnly? requested, DateTime now)
{
var today = DateOnly.FromDateTime(now);
var today = BusinessClock.ToBusinessDate(now);
var day = requested ?? today;
if (day > today.AddDays(1))
throw new DomainException(ErrorCodes.Validation, "Payment date cannot be in the future.", 400);
@@ -27,12 +27,13 @@ public sealed class FifoCostingService : IFifoCostingService
private readonly ErpDbContext _db;
private readonly IGeneralLedgerService _gl;
private readonly IGlAccountConfigService _glAccounts;
private readonly LowStockTracker? _lowStock;
/// <param name="lowStock">Records each issue for the after-request low-stock check; optional so callers outside a request (tests) can omit it.</param>
public FifoCostingService(
IRepository<StockLayer> layers, IRepository<StockLedger> ledger,
IRepository<JournalEntryStub> journal, ErpDbContext db,
IGeneralLedgerService gl, IGlAccountConfigService glAccounts)
IGeneralLedgerService gl, IGlAccountConfigService glAccounts, LowStockTracker? lowStock = null)
{
_layers = layers;
_ledger = ledger;
@@ -40,6 +41,7 @@ public sealed class FifoCostingService : IFifoCostingService
_db = db;
_gl = gl;
_glAccounts = glAccounts;
_lowStock = lowStock;
}
public async Task<StockLayer> CreateInboundLayerAsync(