price adjustment and stock adjustments
This commit is contained in:
@@ -0,0 +1,34 @@
|
||||
using ERPCore.Services.Stock;
|
||||
using Microsoft.AspNetCore.Mvc.Filters;
|
||||
|
||||
namespace ERPCore.Infra.Filters;
|
||||
|
||||
/// <summary>
|
||||
/// After every API action that succeeded, checks whether its stock issues pushed anything to or
|
||||
/// below its reorder point (<see cref="LowStockAlertService"/>). By then the action's UoW
|
||||
/// transaction has committed, so only stock that really left is alerted on. The check is
|
||||
/// best-effort: a failure is logged and never fails the request that moved the stock.
|
||||
/// </summary>
|
||||
public sealed class LowStockAlertFilter(LowStockAlertService alerts, LowStockTracker tracker, ILogger<LowStockAlertFilter> logger) : IAsyncActionFilter
|
||||
{
|
||||
public async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next)
|
||||
{
|
||||
var executed = await next();
|
||||
|
||||
// A failed action rolled its transaction back: nothing it "issued" actually left stock.
|
||||
if (executed.Exception is not null && !executed.ExceptionHandled)
|
||||
{
|
||||
tracker.Drain();
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
await alerts.CheckAsync(context.HttpContext.RequestAborted);
|
||||
}
|
||||
catch (Exception ex) when (ex is not OperationCanceledException)
|
||||
{
|
||||
logger.LogWarning(ex, "Low-stock check failed after {Path}", context.HttpContext.Request.Path);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
using ERPCore.Domain.Entities;
|
||||
using ERPCore.Domain.Enums;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Metadata.Builders;
|
||||
|
||||
namespace ERPCore.Infra.Persistence.Configurations;
|
||||
|
||||
/// <summary>Dual approval tables (docs/DUAL_APPROVAL_PLAN.md §4, §6, §7).</summary>
|
||||
public sealed class DocumentApprovalConfiguration : IEntityTypeConfiguration<DocumentApproval>
|
||||
{
|
||||
public void Configure(EntityTypeBuilder<DocumentApproval> builder)
|
||||
{
|
||||
builder.ToTable("document_approvals");
|
||||
builder.HasKey(a => a.DocumentApprovalId);
|
||||
|
||||
builder.Property(a => a.DocType).IsRequired().HasMaxLength(10);
|
||||
builder.Property(a => a.Decision).HasConversion<string>().HasMaxLength(20).IsRequired();
|
||||
builder.Property(a => a.Comment).HasMaxLength(250);
|
||||
builder.Property(a => a.DecidedAt).IsRequired();
|
||||
|
||||
builder.HasOne(a => a.Decider).WithMany().HasForeignKey(a => a.DecidedBy).OnDelete(DeleteBehavior.Restrict);
|
||||
builder.HasIndex(a => new { a.DocType, a.DocId });
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class ApprovalConfigConfiguration : IEntityTypeConfiguration<ApprovalConfig>
|
||||
{
|
||||
public void Configure(EntityTypeBuilder<ApprovalConfig> builder)
|
||||
{
|
||||
builder.ToTable("approval_config", t =>
|
||||
t.HasCheckConstraint("ck_approval_config_singleton", $"`ApprovalConfigId` = {ApprovalConfig.SingletonId}"));
|
||||
builder.HasKey(c => c.ApprovalConfigId);
|
||||
builder.Property(c => c.ApprovalConfigId).ValueGeneratedNever();
|
||||
|
||||
builder.Property(c => c.StockAdjustmentLevel2ThresholdValue).HasPrecision(18, 4);
|
||||
builder.Property(c => c.PriceAdjustmentLevel2ThresholdPercent).HasPrecision(9, 4);
|
||||
builder.Property(c => c.Level2RoleCodes).HasMaxLength(500);
|
||||
|
||||
// Dual approval on by default for both documents (plan §8 recommended defaults).
|
||||
builder.HasData(new ApprovalConfig
|
||||
{
|
||||
ApprovalConfigId = ApprovalConfig.SingletonId,
|
||||
StockAdjustmentLevels = 2,
|
||||
PriceAdjustmentLevels = 2,
|
||||
RowVersion = 1
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class PriceAdjustmentConfiguration : IEntityTypeConfiguration<PriceAdjustment>
|
||||
{
|
||||
public void Configure(EntityTypeBuilder<PriceAdjustment> builder)
|
||||
{
|
||||
builder.ToTable("price_adjustments");
|
||||
builder.HasKey(p => p.PriceAdjustmentId);
|
||||
|
||||
builder.Property(p => p.DocNo).IsRequired().HasMaxLength(30);
|
||||
builder.HasIndex(p => p.DocNo).IsUnique();
|
||||
builder.Property(p => p.Reason).IsRequired().HasMaxLength(250);
|
||||
builder.Property(p => p.Status).HasConversion<string>().HasMaxLength(30).IsRequired()
|
||||
.HasDefaultValue(PriceAdjustmentStatus.PendingApproval);
|
||||
builder.Property(p => p.RejectReason).HasMaxLength(250);
|
||||
builder.Property(p => p.CreatedAt).IsRequired();
|
||||
|
||||
builder.HasOne(p => p.Creator).WithMany().HasForeignKey(p => p.CreatedBy).OnDelete(DeleteBehavior.Restrict);
|
||||
builder.HasMany(p => p.Lines).WithOne(l => l.PriceAdjustment).HasForeignKey(l => l.PriceAdjustmentId).OnDelete(DeleteBehavior.Cascade);
|
||||
builder.HasIndex(p => p.Status);
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class PriceAdjustmentLineConfiguration : IEntityTypeConfiguration<PriceAdjustmentLine>
|
||||
{
|
||||
public void Configure(EntityTypeBuilder<PriceAdjustmentLine> builder)
|
||||
{
|
||||
builder.ToTable("price_adjustment_lines");
|
||||
builder.HasKey(l => l.PriceAdjustmentLineId);
|
||||
foreach (var p in new[] { nameof(PriceAdjustmentLine.OldMinPrice), nameof(PriceAdjustmentLine.OldMaxPrice), nameof(PriceAdjustmentLine.OldFlatPrice),
|
||||
nameof(PriceAdjustmentLine.NewMinPrice), nameof(PriceAdjustmentLine.NewMaxPrice), nameof(PriceAdjustmentLine.NewFlatPrice) })
|
||||
builder.Property<decimal?>(p).HasPrecision(18, 4);
|
||||
builder.HasOne(l => l.Item).WithMany().HasForeignKey(l => l.ItemId).OnDelete(DeleteBehavior.Restrict);
|
||||
builder.HasIndex(l => l.ItemId);
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class ItemPriceHistoryConfiguration : IEntityTypeConfiguration<ItemPriceHistory>
|
||||
{
|
||||
public void Configure(EntityTypeBuilder<ItemPriceHistory> builder)
|
||||
{
|
||||
builder.ToTable("item_price_history");
|
||||
builder.HasKey(h => h.ItemPriceHistoryId);
|
||||
foreach (var p in new[] { nameof(ItemPriceHistory.OldMinPrice), nameof(ItemPriceHistory.OldMaxPrice), nameof(ItemPriceHistory.OldFlatPrice),
|
||||
nameof(ItemPriceHistory.NewMinPrice), nameof(ItemPriceHistory.NewMaxPrice), nameof(ItemPriceHistory.NewFlatPrice) })
|
||||
builder.Property<decimal?>(p).HasPrecision(18, 4);
|
||||
builder.Property(h => h.AppliedAt).IsRequired();
|
||||
builder.HasOne(h => h.Item).WithMany().HasForeignKey(h => h.ItemId).OnDelete(DeleteBehavior.Restrict);
|
||||
builder.HasOne(h => h.PriceAdjustment).WithMany().HasForeignKey(h => h.PriceAdjustmentId).OnDelete(DeleteBehavior.Restrict);
|
||||
builder.HasIndex(h => new { h.ItemId, h.AppliedAt });
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
using ERPCore.Domain.Entities;
|
||||
using ERPCore.Domain.Enums;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Metadata.Builders;
|
||||
|
||||
namespace ERPCore.Infra.Persistence.Configurations;
|
||||
|
||||
/// <summary>Bank deposits of till cash (docs/BANK_DEPOSIT_MODULE_PLAN.md §4).</summary>
|
||||
public sealed class BankDepositConfiguration : IEntityTypeConfiguration<BankDeposit>
|
||||
{
|
||||
public void Configure(EntityTypeBuilder<BankDeposit> builder)
|
||||
{
|
||||
builder.ToTable("bank_deposits", t =>
|
||||
t.HasCheckConstraint("ck_bank_deposits_amount", "`Amount` > 0"));
|
||||
builder.HasKey(x => x.BankDepositId);
|
||||
|
||||
builder.Property(x => x.DocNo).IsRequired().HasMaxLength(30);
|
||||
builder.HasIndex(x => x.DocNo).IsUnique();
|
||||
|
||||
builder.Property(x => x.BusinessDate).IsRequired();
|
||||
builder.Property(x => x.DepositDate).IsRequired();
|
||||
builder.Property(x => x.Amount).HasPrecision(18, 4);
|
||||
|
||||
builder.Property(x => x.FromCashAccountCode).IsRequired().HasMaxLength(20);
|
||||
builder.Property(x => x.FromCashAccountName).IsRequired().HasMaxLength(200);
|
||||
builder.Property(x => x.ToBankAccountCode).IsRequired().HasMaxLength(20);
|
||||
builder.Property(x => x.ToBankAccountName).IsRequired().HasMaxLength(200);
|
||||
builder.Property(x => x.ToBankName).HasMaxLength(150);
|
||||
builder.Property(x => x.ToAccountNumber).HasMaxLength(50);
|
||||
|
||||
builder.Property(x => x.DepositSlipNo).HasMaxLength(50);
|
||||
builder.Property(x => x.DepositedBy).HasMaxLength(150);
|
||||
builder.Property(x => x.Remarks).HasMaxLength(500);
|
||||
|
||||
builder.Property(x => x.Status)
|
||||
.HasConversion<string>().HasMaxLength(20).IsRequired()
|
||||
.HasDefaultValue(BankDepositStatus.Posted);
|
||||
|
||||
builder.Property(x => x.GlJournalNo).HasMaxLength(30);
|
||||
builder.Property(x => x.GlReversalJournalNo).HasMaxLength(30);
|
||||
builder.Property(x => x.CancelReason).HasMaxLength(250);
|
||||
builder.Property(x => x.CreatedAt).IsRequired();
|
||||
|
||||
builder.HasOne(x => x.Branch).WithMany().HasForeignKey(x => x.BranchId).OnDelete(DeleteBehavior.Restrict);
|
||||
builder.HasOne(x => x.Creator).WithMany().HasForeignKey(x => x.CreatedBy).OnDelete(DeleteBehavior.Restrict);
|
||||
|
||||
builder.HasIndex(x => new { x.BranchId, x.BusinessDate, x.Status });
|
||||
builder.HasIndex(x => x.ToGlBankAccountId);
|
||||
}
|
||||
}
|
||||
@@ -26,6 +26,8 @@ public sealed class BundleSaleConfiguration : IEntityTypeConfiguration<BundleSal
|
||||
builder.Property(x => x.DiscountTotal).HasPrecision(18, 4);
|
||||
builder.Property(x => x.TaxTotal).HasPrecision(18, 4);
|
||||
builder.Property(x => x.GrandTotal).HasPrecision(18, 4);
|
||||
builder.Property(x => x.CashAmount).HasPrecision(18, 4);
|
||||
builder.Property(x => x.CardAmount).HasPrecision(18, 4);
|
||||
builder.Property(x => x.CreatedAt).IsRequired();
|
||||
|
||||
builder.HasOne(x => x.Customer).WithMany().HasForeignKey(x => x.CustomerId).OnDelete(DeleteBehavior.Restrict);
|
||||
|
||||
@@ -0,0 +1,170 @@
|
||||
using ERPCore.Domain.Entities;
|
||||
using ERPCore.Domain.Enums;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Metadata.Builders;
|
||||
|
||||
namespace ERPCore.Infra.Persistence.Configurations;
|
||||
|
||||
/// <summary>
|
||||
/// Expense module tables (docs/EXPENSE_MODULE_PLAN.md §3). The four categories and their
|
||||
/// titles are seeded as <c>IsSystem</c> rows. The GL codes are the children of the GL
|
||||
/// "Operating Expenses" (5200) control account created for this module on 2026-10-08 — GL
|
||||
/// generates codes itself, so a GL instance with a different chart needs these corrected per
|
||||
/// title on the Expense Setup screen.
|
||||
/// </summary>
|
||||
public sealed class ExpenseCategoryConfiguration : IEntityTypeConfiguration<ExpenseCategory>
|
||||
{
|
||||
public const string UtilityGlCode = "5200-01";
|
||||
public const string RentalGlCode = "5200-02";
|
||||
public const string SalariesGlCode = "5200-03";
|
||||
public const string OtherGlCode = "5200-04";
|
||||
|
||||
public void Configure(EntityTypeBuilder<ExpenseCategory> builder)
|
||||
{
|
||||
builder.ToTable("expense_categories");
|
||||
builder.HasKey(c => c.ExpenseCategoryId);
|
||||
|
||||
builder.Property(c => c.Code).IsRequired().HasMaxLength(30);
|
||||
builder.HasIndex(c => c.Code).IsUnique();
|
||||
builder.Property(c => c.Name).IsRequired().HasMaxLength(100);
|
||||
builder.HasIndex(c => c.Name).IsUnique();
|
||||
|
||||
builder.Property(c => c.Status)
|
||||
.HasConversion<string>().HasMaxLength(20).IsRequired()
|
||||
.HasDefaultValue(EntityStatus.Active);
|
||||
builder.Property(c => c.CreatedAt).IsRequired();
|
||||
|
||||
builder.HasMany(c => c.Titles)
|
||||
.WithOne(t => t.Category)
|
||||
.HasForeignKey(t => t.ExpenseCategoryId)
|
||||
.OnDelete(DeleteBehavior.Restrict);
|
||||
|
||||
var seededAt = new DateTime(2026, 10, 7, 0, 0, 0, DateTimeKind.Utc);
|
||||
builder.HasData(
|
||||
new ExpenseCategory { ExpenseCategoryId = 1, Code = "UTILITY", Name = "Utility", IsSystem = true, SortOrder = 1, CreatedAt = seededAt, RowVersion = 1 },
|
||||
new ExpenseCategory { ExpenseCategoryId = 2, Code = "RENTAL", Name = "Rental", IsSystem = true, SortOrder = 2, CreatedAt = seededAt, RowVersion = 1 },
|
||||
new ExpenseCategory { ExpenseCategoryId = 3, Code = "SALARIES", Name = "Salaries", IsSystem = true, SortOrder = 3, CreatedAt = seededAt, RowVersion = 1 },
|
||||
new ExpenseCategory { ExpenseCategoryId = 4, Code = "OTHER", Name = "Other", IsSystem = true, SortOrder = 4, CreatedAt = seededAt, RowVersion = 1 }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class ExpenseTitleConfiguration : IEntityTypeConfiguration<ExpenseTitle>
|
||||
{
|
||||
public void Configure(EntityTypeBuilder<ExpenseTitle> builder)
|
||||
{
|
||||
builder.ToTable("expense_titles");
|
||||
builder.HasKey(t => t.ExpenseTitleId);
|
||||
|
||||
builder.Property(t => t.Name).IsRequired().HasMaxLength(100);
|
||||
builder.HasIndex(t => new { t.ExpenseCategoryId, t.Name }).IsUnique();
|
||||
builder.Property(t => t.GlExpenseAccountCode).IsRequired().HasMaxLength(20);
|
||||
|
||||
builder.Property(t => t.Status)
|
||||
.HasConversion<string>().HasMaxLength(20).IsRequired()
|
||||
.HasDefaultValue(EntityStatus.Active);
|
||||
builder.Property(t => t.CreatedAt).IsRequired();
|
||||
|
||||
var seededAt = new DateTime(2026, 10, 7, 0, 0, 0, DateTimeKind.Utc);
|
||||
var id = 0;
|
||||
ExpenseTitle Seed(int categoryId, string name, string glCode, int sortOrder) => new()
|
||||
{
|
||||
ExpenseTitleId = ++id, ExpenseCategoryId = categoryId, Name = name, GlExpenseAccountCode = glCode,
|
||||
IsSystem = true, SortOrder = sortOrder, CreatedAt = seededAt, RowVersion = 1
|
||||
};
|
||||
|
||||
// Append only — the ids are assigned in this order, so reordering would renumber seeded rows.
|
||||
builder.HasData(
|
||||
Seed(1, "Electricity", ExpenseCategoryConfiguration.UtilityGlCode, 1),
|
||||
Seed(1, "Water", ExpenseCategoryConfiguration.UtilityGlCode, 2),
|
||||
Seed(1, "Telephone", ExpenseCategoryConfiguration.UtilityGlCode, 3),
|
||||
Seed(1, "Internet", ExpenseCategoryConfiguration.UtilityGlCode, 4),
|
||||
Seed(1, "Gas", ExpenseCategoryConfiguration.UtilityGlCode, 5),
|
||||
Seed(2, "Building Rent", ExpenseCategoryConfiguration.RentalGlCode, 1),
|
||||
Seed(2, "Vehicle Rent", ExpenseCategoryConfiguration.RentalGlCode, 2),
|
||||
Seed(2, "Equipment Rent", ExpenseCategoryConfiguration.RentalGlCode, 3),
|
||||
Seed(3, "Staff Salaries", ExpenseCategoryConfiguration.SalariesGlCode, 1),
|
||||
Seed(3, "Wages", ExpenseCategoryConfiguration.SalariesGlCode, 2),
|
||||
Seed(3, "Overtime", ExpenseCategoryConfiguration.SalariesGlCode, 3),
|
||||
Seed(3, "Bonus", ExpenseCategoryConfiguration.SalariesGlCode, 4),
|
||||
Seed(3, "EPF / ETF Contribution", ExpenseCategoryConfiguration.SalariesGlCode, 5),
|
||||
Seed(4, "Stationery", ExpenseCategoryConfiguration.OtherGlCode, 1),
|
||||
Seed(4, "Transport", ExpenseCategoryConfiguration.OtherGlCode, 2),
|
||||
Seed(4, "Repairs & Maintenance", ExpenseCategoryConfiguration.OtherGlCode, 3),
|
||||
Seed(4, "Cleaning", ExpenseCategoryConfiguration.OtherGlCode, 4),
|
||||
Seed(4, "Miscellaneous", ExpenseCategoryConfiguration.OtherGlCode, 5)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class ExpenseConfiguration : IEntityTypeConfiguration<Expense>
|
||||
{
|
||||
public void Configure(EntityTypeBuilder<Expense> builder)
|
||||
{
|
||||
builder.ToTable("expenses", t =>
|
||||
t.HasCheckConstraint("ck_expenses_amount", "`Amount` > 0"));
|
||||
builder.HasKey(x => x.ExpenseId);
|
||||
|
||||
builder.Property(x => x.DocNo).IsRequired().HasMaxLength(30);
|
||||
builder.HasIndex(x => x.DocNo).IsUnique();
|
||||
|
||||
builder.Property(x => x.ExpenseDate).IsRequired();
|
||||
builder.Property(x => x.Payee).HasMaxLength(150);
|
||||
builder.Property(x => x.BillReference).HasMaxLength(50);
|
||||
builder.Property(x => x.Description).HasMaxLength(500);
|
||||
builder.Property(x => x.PeriodMonth).HasMaxLength(7);
|
||||
|
||||
foreach (var p in new[] { nameof(Expense.Amount), nameof(Expense.PaidAmount), nameof(Expense.BalanceAmount) })
|
||||
builder.Property<decimal>(p).HasPrecision(18, 4);
|
||||
|
||||
builder.Property(x => x.Status)
|
||||
.HasConversion<string>().HasMaxLength(20).IsRequired()
|
||||
.HasDefaultValue(ExpenseStatus.Draft);
|
||||
builder.Property(x => x.PaymentStatus)
|
||||
.HasConversion<string>().HasMaxLength(20).IsRequired()
|
||||
.HasDefaultValue(ExpensePaymentStatus.Unpaid);
|
||||
|
||||
builder.Property(x => x.GlExpenseAccountCode).HasMaxLength(20);
|
||||
builder.Property(x => x.GlJournalNo).HasMaxLength(30);
|
||||
builder.Property(x => x.GlReversalJournalNo).HasMaxLength(30);
|
||||
builder.Property(x => x.CancelReason).HasMaxLength(250);
|
||||
builder.Property(x => x.CreatedAt).IsRequired();
|
||||
|
||||
builder.HasOne(x => x.Category).WithMany().HasForeignKey(x => x.ExpenseCategoryId).OnDelete(DeleteBehavior.Restrict);
|
||||
builder.HasOne(x => x.Title).WithMany().HasForeignKey(x => x.ExpenseTitleId).OnDelete(DeleteBehavior.Restrict);
|
||||
builder.HasOne(x => x.Vendor).WithMany().HasForeignKey(x => x.VendorId).OnDelete(DeleteBehavior.Restrict);
|
||||
builder.HasOne(x => x.Branch).WithMany().HasForeignKey(x => x.BranchId).OnDelete(DeleteBehavior.Restrict);
|
||||
builder.HasOne(x => x.Creator).WithMany().HasForeignKey(x => x.CreatedBy).OnDelete(DeleteBehavior.Restrict);
|
||||
|
||||
builder.HasMany(x => x.Payments)
|
||||
.WithOne(p => p.Expense)
|
||||
.HasForeignKey(p => p.ExpenseId)
|
||||
.OnDelete(DeleteBehavior.Restrict);
|
||||
|
||||
builder.HasIndex(x => x.Status);
|
||||
builder.HasIndex(x => x.PaymentStatus);
|
||||
builder.HasIndex(x => x.ExpenseDate);
|
||||
builder.HasIndex(x => x.BranchId);
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class ExpensePaymentConfiguration : IEntityTypeConfiguration<ExpensePayment>
|
||||
{
|
||||
public void Configure(EntityTypeBuilder<ExpensePayment> builder)
|
||||
{
|
||||
builder.ToTable("expense_payments");
|
||||
builder.HasKey(p => p.ExpensePaymentId);
|
||||
|
||||
builder.Property(p => p.Amount).HasPrecision(18, 4);
|
||||
builder.Property(p => p.PaymentDate).IsRequired();
|
||||
builder.Property(p => p.PaymentMethod).HasConversion<string>().HasMaxLength(20).IsRequired();
|
||||
builder.Property(p => p.BankAccountName).IsRequired().HasMaxLength(200);
|
||||
builder.Property(p => p.ChequeNo).HasMaxLength(30);
|
||||
builder.Property(p => p.Reference).HasMaxLength(100);
|
||||
builder.Property(p => p.GlJournalNo).HasMaxLength(30);
|
||||
builder.Property(p => p.CreatedAt).IsRequired();
|
||||
|
||||
builder.HasOne(p => p.Creator).WithMany().HasForeignKey(p => p.CreatedBy).OnDelete(DeleteBehavior.Restrict);
|
||||
builder.HasIndex(p => p.ExpenseId);
|
||||
}
|
||||
}
|
||||
@@ -84,7 +84,13 @@ public sealed class PermissionConfiguration : IEntityTypeConfiguration<Permissio
|
||||
new Permission { PermissionId = 50, Code = "NAV:sales.invoices", SubNavItemId = 40 },
|
||||
new Permission { PermissionId = 51, Code = "NAV:sales.free-issues", SubNavItemId = 41 },
|
||||
new Permission { PermissionId = 52, Code = "NAV:sales.day-end", SubNavItemId = 42 },
|
||||
new Permission { PermissionId = 53, Code = "NAV:products.tax-rates", SubNavItemId = 43 }
|
||||
new Permission { PermissionId = 53, Code = "NAV:products.tax-rates", SubNavItemId = 43 },
|
||||
new Permission { PermissionId = 54, Code = "NAV:accounts.expenses", SubNavItemId = 44 },
|
||||
new Permission { PermissionId = 55, Code = "NAV:accounts.expense-setup", SubNavItemId = 45 },
|
||||
new Permission { PermissionId = 56, Code = "NAV:sales.day-end-report", SubNavItemId = 46 },
|
||||
new Permission { PermissionId = 57, Code = "NAV:accounts.bank-deposits", SubNavItemId = 47 },
|
||||
new Permission { PermissionId = 58, Code = "NAV:products.price-adjustments", SubNavItemId = 48 },
|
||||
new Permission { PermissionId = 59, Code = "NAV:settings.approvals", SubNavItemId = 49 }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -70,6 +70,9 @@ public sealed class SalesInvoicePaymentConfiguration : IEntityTypeConfiguration<
|
||||
builder.Property(p => p.Amount).HasPrecision(18, 4);
|
||||
builder.Property(p => p.PaymentDate).IsRequired();
|
||||
builder.Property(p => p.BankAccountName).IsRequired().HasMaxLength(200);
|
||||
builder.Property(p => p.PaymentMethod)
|
||||
.HasConversion<string>().HasMaxLength(20).IsRequired()
|
||||
.HasDefaultValue(SalesPaymentMethod.Cash);
|
||||
builder.Property(p => p.Reference).HasMaxLength(100);
|
||||
builder.Property(p => p.GlJournalNo).HasMaxLength(30);
|
||||
builder.Property(p => p.CreatedAt).IsRequired();
|
||||
|
||||
@@ -38,7 +38,7 @@ public sealed class SalesSlipConfiguration : IEntityTypeConfiguration<SalesSlip>
|
||||
.HasConversion<string>().HasMaxLength(20).IsRequired()
|
||||
.HasDefaultValue(SalesSlipStatus.Draft);
|
||||
|
||||
foreach (var p in new[] { nameof(SalesSlip.Subtotal), nameof(SalesSlip.DiscountTotal), nameof(SalesSlip.TaxTotal), nameof(SalesSlip.GrandTotal), nameof(SalesSlip.PaidAmount), nameof(SalesSlip.BalanceAmount) })
|
||||
foreach (var p in new[] { nameof(SalesSlip.Subtotal), nameof(SalesSlip.DiscountTotal), nameof(SalesSlip.TaxTotal), nameof(SalesSlip.GrandTotal), nameof(SalesSlip.PaidAmount), nameof(SalesSlip.BalanceAmount), nameof(SalesSlip.CashAmount), nameof(SalesSlip.CardAmount) })
|
||||
builder.Property<decimal>(p).HasPrecision(18, 4);
|
||||
|
||||
builder.Property(x => x.CreatedAt).IsRequired();
|
||||
|
||||
@@ -14,9 +14,12 @@ public sealed class StockAdjustmentConfiguration : IEntityTypeConfiguration<Stoc
|
||||
builder.Property(a => a.DocNo).IsRequired().HasMaxLength(30);
|
||||
builder.HasIndex(a => a.DocNo).IsUnique();
|
||||
|
||||
builder.Property(a => a.Status).HasConversion<string>().HasMaxLength(20).IsRequired();
|
||||
// 30, not 20: "PendingSecondApproval" is 21 characters.
|
||||
builder.Property(a => a.Status).HasConversion<string>().HasMaxLength(30).IsRequired();
|
||||
builder.Property(a => a.CreatedAt).IsRequired();
|
||||
builder.Property(a => a.GlJournalNo).HasMaxLength(30);
|
||||
builder.Property(a => a.RejectReason).HasMaxLength(250);
|
||||
builder.HasIndex(a => a.Status);
|
||||
|
||||
builder.HasOne(a => a.Warehouse).WithMany().HasForeignKey(a => a.WarehouseId).OnDelete(DeleteBehavior.Restrict);
|
||||
builder.HasOne(a => a.ReasonCode).WithMany().HasForeignKey(a => a.ReasonCodeId).OnDelete(DeleteBehavior.Restrict);
|
||||
@@ -35,6 +38,8 @@ public sealed class StockAdjustmentLineConfiguration : IEntityTypeConfiguration<
|
||||
builder.HasKey(l => l.AdjLineId);
|
||||
|
||||
builder.Property(l => l.QtyDelta).HasPrecision(18, 4);
|
||||
builder.Property(l => l.RequestedSerialIds).HasMaxLength(4000);
|
||||
builder.Property(l => l.RequestedSerialNos).HasMaxLength(4000);
|
||||
|
||||
builder.HasOne(l => l.Adjustment).WithMany(a => a.Lines).HasForeignKey(l => l.AdjustmentId).OnDelete(DeleteBehavior.Cascade);
|
||||
builder.HasOne(l => l.Item).WithMany().HasForeignKey(l => l.ItemId).OnDelete(DeleteBehavior.Restrict);
|
||||
|
||||
@@ -32,17 +32,20 @@ public sealed class SubNavItemConfiguration : IEntityTypeConfiguration<SubNavIte
|
||||
new SubNavItem { SubNavItemId = 5, NavItemId = 2, Code = "products.uom", Label = "UOM", Href = "/dashboard/products/uoms", SortOrder = 5 },
|
||||
new SubNavItem { SubNavItemId = 6, NavItemId = 2, Code = "products.configuration", Label = "Configuration", Href = "/dashboard/products/settings", SortOrder = 6 },
|
||||
new SubNavItem { SubNavItemId = 43, NavItemId = 2, Code = "products.tax-rates", Label = "Tax Rates", Href = "/dashboard/products/tax-rates", SortOrder = 7 },
|
||||
new SubNavItem { SubNavItemId = 48, NavItemId = 2, Code = "products.price-adjustments", Label = "Price Adjustments", Href = "/dashboard/products/price-adjustments", SortOrder = 8 },
|
||||
new SubNavItem { SubNavItemId = 29, NavItemId = 9, Code = "settings.branches", Label = "Branches", Href = "/dashboard/settings/branches", SortOrder = 0 },
|
||||
new SubNavItem { SubNavItemId = 7, NavItemId = 9, Code = "settings.roles", Label = "Roles", Href = "/dashboard/settings/roles", SortOrder = 1 },
|
||||
new SubNavItem { SubNavItemId = 8, NavItemId = 9, Code = "settings.users", Label = "Users", Href = "/dashboard/settings/users", SortOrder = 2 },
|
||||
new SubNavItem { SubNavItemId = 24, NavItemId = 9, Code = "settings.notification-config", Label = "Notification Config", Href = "/dashboard/settings/notification-config", SortOrder = 3 },
|
||||
new SubNavItem { SubNavItemId = 49, NavItemId = 9, Code = "settings.approvals", Label = "Approvals", Href = "/dashboard/settings/approvals", SortOrder = 4 },
|
||||
// Sales (NavItemId 13) children — mirror the sidebar order (AppSidebar.tsx). SubNavItemId
|
||||
// 23 is renamed in place (was "sales.bundle-sales" / Bundle Sales) rather than
|
||||
// delete+recreate, so any role already granted this permission keeps it; the rest are new.
|
||||
new SubNavItem { SubNavItemId = 40, NavItemId = 13, Code = "sales.invoices", Label = "Sales", Href = "/dashboard/sales/invoices", SortOrder = 1 },
|
||||
new SubNavItem { SubNavItemId = 23, NavItemId = 13, Code = "sales.bundle-templates", Label = "Bundle Templates", Href = "/dashboard/sales/bundles/templates", SortOrder = 2 },
|
||||
new SubNavItem { SubNavItemId = 41, NavItemId = 13, Code = "sales.free-issues", Label = "Free Issues", Href = "/dashboard/sales/free-issues", SortOrder = 3 },
|
||||
new SubNavItem { SubNavItemId = 42, NavItemId = 13, Code = "sales.day-end", Label = "Day End", Href = "/dashboard/sales/day-end", SortOrder = 4 },
|
||||
new SubNavItem { SubNavItemId = 42, NavItemId = 13, Code = "sales.day-end", Label = "Approved Invoices", Href = "/dashboard/sales/day-end", SortOrder = 4 },
|
||||
new SubNavItem { SubNavItemId = 46, NavItemId = 13, Code = "sales.day-end-report", Label = "Day-End Report", Href = "/dashboard/sales/day-end-report", SortOrder = 5 },
|
||||
// Procurement (NavItemId 4) children — mirror the hub page order.
|
||||
// IDs 17-20 (not 9-12): 9-12 were already claimed by the Ledgers sub-items below;
|
||||
// these procurement rows were never actually migrated into the database before now.
|
||||
@@ -67,6 +70,9 @@ public sealed class SubNavItemConfiguration : IEntityTypeConfiguration<SubNavIte
|
||||
new SubNavItem { SubNavItemId = 15, NavItemId = 12, Code = "accounts.bank-accounts", Label = "Cash / Bank Accounts", Href = "/dashboard/accounts/bank-accounts", SortOrder = 1 },
|
||||
new SubNavItem { SubNavItemId = 21, NavItemId = 12, Code = "accounts.cheque-books", Label = "Cheque Books", Href = "/dashboard/accounts/cheque-books", SortOrder = 2 },
|
||||
new SubNavItem { SubNavItemId = 22, NavItemId = 12, Code = "accounts.received-cheques", Label = "Received Cheques", Href = "/dashboard/accounts/received-cheques", SortOrder = 3 },
|
||||
new SubNavItem { SubNavItemId = 44, NavItemId = 12, Code = "accounts.expenses", Label = "Expenses", Href = "/dashboard/accounts/expenses", SortOrder = 4 },
|
||||
new SubNavItem { SubNavItemId = 45, NavItemId = 12, Code = "accounts.expense-setup", Label = "Expense Setup", Href = "/dashboard/accounts/expense-setup", SortOrder = 5 },
|
||||
new SubNavItem { SubNavItemId = 47, NavItemId = 12, Code = "accounts.bank-deposits", Label = "Bank Deposits", Href = "/dashboard/accounts/bank-deposits", SortOrder = 6 },
|
||||
// Production (NavItemId 14) children — mirror the sidebar order (AppSidebar.tsx).
|
||||
// ID 27 (not 24): 24 collides with settings.notification-config above — that ID was
|
||||
// reused by mistake in migration 20260820100345, which upserts row 24 to become the
|
||||
|
||||
@@ -90,6 +90,22 @@ public class ErpDbContext : DbContext
|
||||
public DbSet<PurchaseInvoiceLine> PurchaseInvoiceLines => Set<PurchaseInvoiceLine>();
|
||||
public DbSet<PurchaseInvoicePayment> PurchaseInvoicePayments => Set<PurchaseInvoicePayment>();
|
||||
|
||||
// --- Accounts: expenses (docs/EXPENSE_MODULE_PLAN.md) ---
|
||||
public DbSet<ExpenseCategory> ExpenseCategories => Set<ExpenseCategory>();
|
||||
public DbSet<ExpenseTitle> ExpenseTitles => Set<ExpenseTitle>();
|
||||
public DbSet<Expense> Expenses => Set<Expense>();
|
||||
public DbSet<ExpensePayment> ExpensePayments => Set<ExpensePayment>();
|
||||
|
||||
// --- Accounts: bank deposits of till cash (docs/BANK_DEPOSIT_MODULE_PLAN.md) ---
|
||||
public DbSet<BankDeposit> BankDeposits => Set<BankDeposit>();
|
||||
|
||||
// --- Dual approval: stock & price adjustments (docs/DUAL_APPROVAL_PLAN.md) ---
|
||||
public DbSet<DocumentApproval> DocumentApprovals => Set<DocumentApproval>();
|
||||
public DbSet<ApprovalConfig> ApprovalConfig => Set<ApprovalConfig>();
|
||||
public DbSet<PriceAdjustment> PriceAdjustments => Set<PriceAdjustment>();
|
||||
public DbSet<PriceAdjustmentLine> PriceAdjustmentLines => Set<PriceAdjustmentLine>();
|
||||
public DbSet<ItemPriceHistory> ItemPriceHistory => Set<ItemPriceHistory>();
|
||||
|
||||
// --- Batch / Serial (docs/10 Part C.4) ---
|
||||
public DbSet<Batch> Batches => Set<Batch>();
|
||||
public DbSet<Serial> Serials => Set<Serial>();
|
||||
@@ -221,6 +237,8 @@ public class ErpDbContext : DbContext
|
||||
modelBuilder.Entity<Rfq>().HasQueryFilter(e => !_currentBranch.IsActive || e.BranchId == _currentBranch.BranchId);
|
||||
modelBuilder.Entity<PurchaseOrder>().HasQueryFilter(e => !_currentBranch.IsActive || e.BranchId == _currentBranch.BranchId);
|
||||
modelBuilder.Entity<PurchaseInvoice>().HasQueryFilter(e => !_currentBranch.IsActive || e.BranchId == _currentBranch.BranchId);
|
||||
modelBuilder.Entity<Expense>().HasQueryFilter(e => !_currentBranch.IsActive || e.BranchId == _currentBranch.BranchId);
|
||||
modelBuilder.Entity<BankDeposit>().HasQueryFilter(e => !_currentBranch.IsActive || e.BranchId == _currentBranch.BranchId);
|
||||
|
||||
// Optimistic concurrency token. On PostgreSQL this was the virtual `xmin` system
|
||||
// column via .IsRowVersion(), which the engine maintained for free. MariaDB has no
|
||||
|
||||
Reference in New Issue
Block a user