Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 3be425284c |
@@ -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;
|
||||
|
||||
@@ -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(
|
||||
|
||||
Reference in New Issue
Block a user