implement fe with backend
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
using ERPCore.Domain.Entities;
|
||||
using ERPCore.Domain;
|
||||
using ERPCore.Domain.Enums;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
@@ -42,7 +43,11 @@ public static class DataSeeder
|
||||
{
|
||||
var dirty = await SeedReasonCodesAsync(db, ct);
|
||||
dirty |= await SeedItemTypesAsync(db, ct);
|
||||
dirty |= await SeedCompanyProfileAsync(db, ct);
|
||||
dirty |= await SeedProductConfigAsync(db, ct);
|
||||
dirty |= await SeedSalesMastersAsync(db, ct);
|
||||
dirty |= await SeedSalesStockAsync(db, ct);
|
||||
dirty |= await SeedSalesAsync(db, ct);
|
||||
|
||||
if (dirty) await db.SaveChangesAsync(ct);
|
||||
}
|
||||
@@ -99,4 +104,541 @@ public static class DataSeeder
|
||||
});
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Seeds a printable company profile with reasonable defaults for invoice headers.
|
||||
/// These values are intentionally editable later through the API.
|
||||
/// </summary>
|
||||
private static async Task<bool> SeedCompanyProfileAsync(ErpDbContext db, CancellationToken ct)
|
||||
{
|
||||
if (await db.CompanyProfiles.AnyAsync(c => c.CompanyProfileId == CompanyProfile.SingletonId, ct)) return false;
|
||||
|
||||
db.CompanyProfiles.Add(new CompanyProfile
|
||||
{
|
||||
CompanyProfileId = CompanyProfile.SingletonId,
|
||||
LegalName = "ERP Core Trading (Pvt) Ltd",
|
||||
TradeName = "ERP Core Trading",
|
||||
TaxRegistrationNo = "TAX-DEFAULT-001",
|
||||
VatRegistrationNo = "VAT-DEFAULT-001",
|
||||
AddressLine1 = "1 Demo Street",
|
||||
City = "Colombo",
|
||||
Country = "Sri Lanka",
|
||||
Phone = "+94 11 000 0000",
|
||||
Email = "accounts@example.com",
|
||||
BankName = "Demo Bank",
|
||||
BankBranch = "Colombo Main",
|
||||
AccountName = "ERP Core Trading (Pvt) Ltd",
|
||||
AccountNumber = "000123456789",
|
||||
SwiftCode = "DEMO1234",
|
||||
FooterNote = "Thank you for your business."
|
||||
});
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Seeds the minimum catalog data required for the sales demo rows to exist.
|
||||
/// These are safe additive rows and do not alter any existing data.
|
||||
/// </summary>
|
||||
private static async Task<bool> SeedSalesMastersAsync(ErpDbContext db, CancellationToken ct)
|
||||
{
|
||||
var dirty = false;
|
||||
|
||||
dirty |= await SeedWarehousesAsync(db, ct);
|
||||
dirty |= await SeedUomsAsync(db, ct);
|
||||
dirty |= await SeedCategoriesAsync(db, ct);
|
||||
dirty |= await SeedItemsAsync(db, ct);
|
||||
|
||||
return dirty;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Seeds a simple on-hand FIFO layer for the demo sales item so the sample
|
||||
/// invoices can be posted without immediately failing stock validation.
|
||||
/// This keeps the stock-check and posting flows testable on a fresh database.
|
||||
/// </summary>
|
||||
private static async Task<bool> SeedSalesStockAsync(ErpDbContext db, CancellationToken ct)
|
||||
{
|
||||
var warehouse = await db.Warehouses.AsNoTracking()
|
||||
.OrderBy(w => w.WarehouseId)
|
||||
.FirstOrDefaultAsync(ct);
|
||||
var item = await db.Items.AsNoTracking()
|
||||
.OrderBy(i => i.ItemId)
|
||||
.FirstOrDefaultAsync(ct);
|
||||
var secondItem = await db.Items.AsNoTracking()
|
||||
.OrderByDescending(i => i.ItemId)
|
||||
.FirstOrDefaultAsync(ct);
|
||||
|
||||
if (warehouse is null || item is null || secondItem is null)
|
||||
return false;
|
||||
|
||||
var existing = await db.StockLayers.AnyAsync(
|
||||
l => (l.ItemId == item.ItemId || l.ItemId == secondItem.ItemId) && l.WarehouseId == warehouse.WarehouseId && l.QtyRemaining > 0m,
|
||||
ct);
|
||||
if (existing) return false;
|
||||
|
||||
db.StockLayers.Add(new StockLayer
|
||||
{
|
||||
ItemId = item.ItemId,
|
||||
WarehouseId = warehouse.WarehouseId,
|
||||
QtyReceived = 100m,
|
||||
QtyRemaining = 100m,
|
||||
UnitCost = item.SalePrice.GetValueOrDefault() > 0m ? item.SalePrice.GetValueOrDefault() / 2m : 25m,
|
||||
ReceiptDate = DateTime.UtcNow.AddDays(-7)
|
||||
});
|
||||
db.StockLayers.Add(new StockLayer
|
||||
{
|
||||
ItemId = secondItem.ItemId,
|
||||
WarehouseId = warehouse.WarehouseId,
|
||||
QtyReceived = 5m,
|
||||
QtyRemaining = 5m,
|
||||
UnitCost = secondItem.SalePrice.GetValueOrDefault() > 0m ? secondItem.SalePrice.GetValueOrDefault() / 2m : 15m,
|
||||
ReceiptDate = DateTime.UtcNow.AddDays(-6)
|
||||
});
|
||||
return true;
|
||||
}
|
||||
|
||||
private static async Task<bool> SeedWarehousesAsync(ErpDbContext db, CancellationToken ct)
|
||||
{
|
||||
var existingCodes = await db.Warehouses.Select(w => w.Code).ToListAsync(ct);
|
||||
var have = existingCodes.ToHashSet(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
var seeds = new[]
|
||||
{
|
||||
new Warehouse { Code = "MAIN", Name = "Main Warehouse" },
|
||||
new Warehouse { Code = "SHOP", Name = "Sales Counter" }
|
||||
};
|
||||
|
||||
var toAdd = seeds.Where(w => !have.Contains(w.Code)).ToList();
|
||||
if (toAdd.Count == 0) return false;
|
||||
|
||||
db.Warehouses.AddRange(toAdd);
|
||||
return true;
|
||||
}
|
||||
|
||||
private static async Task<bool> SeedUomsAsync(ErpDbContext db, CancellationToken ct)
|
||||
{
|
||||
var existingNames = await db.Uoms.Select(u => u.Name).ToListAsync(ct);
|
||||
var have = existingNames.ToHashSet(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
var seeds = new[]
|
||||
{
|
||||
new Uom { Name = "PCS" },
|
||||
new Uom { Name = "BOX" }
|
||||
};
|
||||
|
||||
var toAdd = seeds.Where(u => !have.Contains(u.Name)).ToList();
|
||||
if (toAdd.Count == 0) return false;
|
||||
|
||||
db.Uoms.AddRange(toAdd);
|
||||
return true;
|
||||
}
|
||||
|
||||
private static async Task<bool> SeedCategoriesAsync(ErpDbContext db, CancellationToken ct)
|
||||
{
|
||||
var existingNames = await db.Categories.Select(c => c.Name).ToListAsync(ct);
|
||||
var have = existingNames.ToHashSet(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
var seeds = new[]
|
||||
{
|
||||
new Category { Name = "General Goods", Status = EntityStatus.Active, CreatedAt = DateTime.UtcNow },
|
||||
new Category { Name = "Accessories", Status = EntityStatus.Active, CreatedAt = DateTime.UtcNow }
|
||||
};
|
||||
|
||||
var toAdd = seeds.Where(c => !have.Contains(c.Name)).ToList();
|
||||
if (toAdd.Count == 0) return false;
|
||||
|
||||
db.Categories.AddRange(toAdd);
|
||||
return true;
|
||||
}
|
||||
|
||||
private static async Task<bool> SeedItemsAsync(ErpDbContext db, CancellationToken ct)
|
||||
{
|
||||
var existingSkus = await db.Items.Select(i => i.Sku).ToListAsync(ct);
|
||||
var have = existingSkus.ToHashSet(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
var category = await db.Categories.AsNoTracking()
|
||||
.OrderBy(c => c.CategoryId)
|
||||
.FirstOrDefaultAsync(ct);
|
||||
var uom = await db.Uoms.AsNoTracking()
|
||||
.OrderBy(u => u.UomId)
|
||||
.FirstOrDefaultAsync(ct);
|
||||
|
||||
if (category is null || uom is null)
|
||||
return false;
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
var seeds = new[]
|
||||
{
|
||||
new Item
|
||||
{
|
||||
Sku = "SKU-DEMO-001",
|
||||
Name = "Demo Item 1",
|
||||
Description = "Seeded sample item for sales documents",
|
||||
CategoryId = category.CategoryId,
|
||||
BaseUomId = uom.UomId,
|
||||
StockNature = StockNature.Stocked,
|
||||
TrackingMode = TrackingMode.None,
|
||||
SalePrice = 100m,
|
||||
Status = EntityStatus.Active,
|
||||
CreatedAt = now
|
||||
},
|
||||
new Item
|
||||
{
|
||||
Sku = "SKU-DEMO-002",
|
||||
Name = "Demo Item 2",
|
||||
Description = "Secondary seeded sample item for sales documents",
|
||||
CategoryId = category.CategoryId,
|
||||
BaseUomId = uom.UomId,
|
||||
StockNature = StockNature.Stocked,
|
||||
TrackingMode = TrackingMode.None,
|
||||
SalePrice = 50m,
|
||||
Status = EntityStatus.Active,
|
||||
CreatedAt = now
|
||||
}
|
||||
};
|
||||
|
||||
var toAdd = seeds.Where(i => !have.Contains(i.Sku)).ToList();
|
||||
if (toAdd.Count == 0) return false;
|
||||
|
||||
db.Items.AddRange(toAdd);
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Seeds the minimum sales bootstrap data needed for UI/backend development:
|
||||
/// a couple of customer rows, current-year document counters, and a few draft
|
||||
/// invoice/slip samples when the required master data already exists.
|
||||
/// This intentionally never clears or rewrites any existing rows.
|
||||
/// </summary>
|
||||
private static async Task<bool> SeedSalesAsync(ErpDbContext db, CancellationToken ct)
|
||||
{
|
||||
var dirty = false;
|
||||
|
||||
dirty |= await SeedSalesCustomersAsync(db, ct);
|
||||
dirty |= await SeedSalesSequencesAsync(db, ct);
|
||||
dirty |= await SeedSampleSalesDocsAsync(db, ct);
|
||||
|
||||
return dirty;
|
||||
}
|
||||
|
||||
private static async Task<bool> SeedSalesCustomersAsync(ErpDbContext db, CancellationToken ct)
|
||||
{
|
||||
var existingCodes = await db.Customers.Select(c => c.CustomerCode).ToListAsync(ct);
|
||||
var have = existingCodes.ToHashSet(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
var seeds = new[]
|
||||
{
|
||||
new Customer
|
||||
{
|
||||
CustomerCode = "CUST-WALKIN",
|
||||
CustomerType = CustomerType.B2C,
|
||||
Name = "Walk-in Customer",
|
||||
DisplayName = "Walk-in Customer",
|
||||
CreditLimit = 0m,
|
||||
CreditDays = 0,
|
||||
Status = EntityStatus.Active,
|
||||
CreatedAt = DateTime.UtcNow
|
||||
},
|
||||
new Customer
|
||||
{
|
||||
CustomerCode = "CUST-DEMO",
|
||||
CustomerType = CustomerType.B2B,
|
||||
Name = "Demo Retail Ltd",
|
||||
DisplayName = "Demo Retail Ltd",
|
||||
Phone = "+94 11 000 0000",
|
||||
Email = "sales@example.com",
|
||||
AddressLine1 = "1 Demo Street",
|
||||
City = "Colombo",
|
||||
Country = "Sri Lanka",
|
||||
TaxRegistrationNo = "VAT-DEMO-001",
|
||||
CreditLimit = 250000m,
|
||||
CreditDays = 30,
|
||||
Status = EntityStatus.Active,
|
||||
CreatedAt = DateTime.UtcNow
|
||||
}
|
||||
};
|
||||
|
||||
var toAdd = seeds.Where(c => !have.Contains(c.CustomerCode)).ToList();
|
||||
if (toAdd.Count == 0) return false;
|
||||
|
||||
db.Customers.AddRange(toAdd);
|
||||
return true;
|
||||
}
|
||||
|
||||
private static async Task<bool> SeedSalesSequencesAsync(ErpDbContext db, CancellationToken ct)
|
||||
{
|
||||
var year = DateTime.UtcNow.Year;
|
||||
var existing = await db.NumberSequences
|
||||
.Where(s => s.Year == year && (s.DocType == DocumentTypes.SalesInvoice || s.DocType == DocumentTypes.SalesSlip))
|
||||
.Select(s => s.DocType)
|
||||
.ToListAsync(ct);
|
||||
var have = existing.ToHashSet(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
var seeds = new[]
|
||||
{
|
||||
new NumberSequence { DocType = DocumentTypes.SalesInvoice, Year = year, LastNumber = 0 },
|
||||
new NumberSequence { DocType = DocumentTypes.SalesSlip, Year = year, LastNumber = 0 }
|
||||
};
|
||||
|
||||
var toAdd = seeds.Where(s => !have.Contains(s.DocType)).ToList();
|
||||
if (toAdd.Count == 0) return false;
|
||||
|
||||
db.NumberSequences.AddRange(toAdd);
|
||||
return true;
|
||||
}
|
||||
|
||||
private static async Task<bool> SeedSampleSalesDocsAsync(ErpDbContext db, CancellationToken ct)
|
||||
{
|
||||
if (await db.SalesInvoices.AnyAsync(ct) || await db.SalesSlips.AnyAsync(ct))
|
||||
return false;
|
||||
|
||||
var customer = await db.Customers.AsNoTracking()
|
||||
.OrderBy(c => c.CustomerId)
|
||||
.FirstOrDefaultAsync(ct);
|
||||
var warehouse = await db.Warehouses.AsNoTracking()
|
||||
.OrderBy(w => w.WarehouseId)
|
||||
.FirstOrDefaultAsync(ct);
|
||||
var secondaryWarehouse = await db.Warehouses.AsNoTracking()
|
||||
.OrderByDescending(w => w.WarehouseId)
|
||||
.FirstOrDefaultAsync(ct);
|
||||
var items = await db.Items.AsNoTracking()
|
||||
.OrderBy(i => i.ItemId)
|
||||
.Take(2)
|
||||
.ToListAsync(ct);
|
||||
var uom = await db.Uoms.AsNoTracking()
|
||||
.OrderBy(u => u.UomId)
|
||||
.FirstOrDefaultAsync(ct);
|
||||
var user = await db.Users.AsNoTracking()
|
||||
.OrderBy(u => u.UserId)
|
||||
.FirstOrDefaultAsync(ct);
|
||||
|
||||
if (customer is null || warehouse is null || secondaryWarehouse is null || items.Count < 2 || uom is null || user is null)
|
||||
return false;
|
||||
|
||||
var postableItem = items[0];
|
||||
var shortageItem = items[1];
|
||||
var today = DateTime.UtcNow.Date;
|
||||
var createdAt = DateTime.UtcNow.AddDays(-1);
|
||||
|
||||
db.SalesInvoices.AddRange(
|
||||
new SalesInvoice
|
||||
{
|
||||
InvoiceNo = $"SI-{today:yyyy}-00001",
|
||||
InvoiceDate = today.AddDays(-2),
|
||||
CustomerId = customer.CustomerId,
|
||||
CustomerSnapshotName = customer.Name,
|
||||
CustomerSnapshotTaxNo = customer.TaxRegistrationNo,
|
||||
WarehouseId = warehouse.WarehouseId,
|
||||
InvoiceType = SalesInvoiceType.B2C,
|
||||
Status = SalesInvoiceStatus.Draft,
|
||||
Subtotal = 200m,
|
||||
DiscountTotal = 0m,
|
||||
TaxTotal = 0m,
|
||||
GrandTotal = 200m,
|
||||
RoundOff = 0m,
|
||||
NetPayable = 200m,
|
||||
PaidAmount = 0m,
|
||||
BalanceAmount = 200m,
|
||||
CreatedBy = user.UserId,
|
||||
CreatedAt = createdAt,
|
||||
Lines =
|
||||
[
|
||||
new SalesInvoiceLine
|
||||
{
|
||||
ItemId = postableItem.ItemId,
|
||||
Description = postableItem.Name,
|
||||
Qty = 2m,
|
||||
FreeQty = 0m,
|
||||
UomId = uom.UomId,
|
||||
WarehouseId = warehouse.WarehouseId,
|
||||
UnitPrice = 100m,
|
||||
BaseCost = 0m,
|
||||
PriceSource = "seed",
|
||||
DiscountMode = SalesDiscountMode.Percentage,
|
||||
DiscountPct = 0m,
|
||||
DiscountAmount = 0m,
|
||||
NetUnitPrice = 100m,
|
||||
LineTotal = 200m,
|
||||
TaxPct = 0m,
|
||||
TaxAmount = 0m,
|
||||
IsFreeIssue = false
|
||||
}
|
||||
]
|
||||
},
|
||||
new SalesInvoice
|
||||
{
|
||||
InvoiceNo = $"SI-{today:yyyy}-00002",
|
||||
InvoiceDate = today.AddDays(-1),
|
||||
CustomerId = customer.CustomerId,
|
||||
CustomerSnapshotName = customer.Name,
|
||||
CustomerSnapshotTaxNo = customer.TaxRegistrationNo,
|
||||
WarehouseId = secondaryWarehouse.WarehouseId,
|
||||
InvoiceType = SalesInvoiceType.B2B,
|
||||
Status = SalesInvoiceStatus.Draft,
|
||||
Subtotal = 300m,
|
||||
DiscountTotal = 0m,
|
||||
TaxTotal = 0m,
|
||||
GrandTotal = 300m,
|
||||
RoundOff = 0m,
|
||||
NetPayable = 300m,
|
||||
PaidAmount = 0m,
|
||||
BalanceAmount = 300m,
|
||||
CreatedBy = user.UserId,
|
||||
CreatedAt = createdAt,
|
||||
Lines =
|
||||
[
|
||||
new SalesInvoiceLine
|
||||
{
|
||||
ItemId = shortageItem.ItemId,
|
||||
Description = shortageItem.Name,
|
||||
Qty = 6m,
|
||||
FreeQty = 0m,
|
||||
UomId = uom.UomId,
|
||||
WarehouseId = secondaryWarehouse.WarehouseId,
|
||||
UnitPrice = 50m,
|
||||
BaseCost = 0m,
|
||||
PriceSource = "seed",
|
||||
DiscountMode = SalesDiscountMode.Percentage,
|
||||
DiscountPct = 0m,
|
||||
DiscountAmount = 0m,
|
||||
NetUnitPrice = 50m,
|
||||
LineTotal = 300m,
|
||||
TaxPct = 0m,
|
||||
TaxAmount = 0m,
|
||||
IsFreeIssue = false
|
||||
}
|
||||
]
|
||||
},
|
||||
new SalesInvoice
|
||||
{
|
||||
InvoiceNo = $"SI-{today:yyyy}-00003",
|
||||
InvoiceDate = today,
|
||||
CustomerId = customer.CustomerId,
|
||||
CustomerSnapshotName = customer.Name,
|
||||
CustomerSnapshotTaxNo = customer.TaxRegistrationNo,
|
||||
WarehouseId = warehouse.WarehouseId,
|
||||
InvoiceType = SalesInvoiceType.B2C,
|
||||
Status = SalesInvoiceStatus.Posted,
|
||||
Subtotal = 100m,
|
||||
DiscountTotal = 0m,
|
||||
TaxTotal = 0m,
|
||||
GrandTotal = 100m,
|
||||
RoundOff = 0m,
|
||||
NetPayable = 100m,
|
||||
PaidAmount = 100m,
|
||||
BalanceAmount = 0m,
|
||||
CreatedBy = user.UserId,
|
||||
CreatedAt = createdAt,
|
||||
UpdatedAt = DateTime.UtcNow,
|
||||
Lines =
|
||||
[
|
||||
new SalesInvoiceLine
|
||||
{
|
||||
ItemId = postableItem.ItemId,
|
||||
Description = postableItem.Name,
|
||||
Qty = 1m,
|
||||
FreeQty = 0m,
|
||||
UomId = uom.UomId,
|
||||
WarehouseId = warehouse.WarehouseId,
|
||||
UnitPrice = 100m,
|
||||
BaseCost = 0m,
|
||||
PriceSource = "seed",
|
||||
DiscountMode = SalesDiscountMode.Percentage,
|
||||
DiscountPct = 0m,
|
||||
DiscountAmount = 0m,
|
||||
NetUnitPrice = 100m,
|
||||
LineTotal = 100m,
|
||||
TaxPct = 0m,
|
||||
TaxAmount = 0m,
|
||||
IsFreeIssue = false
|
||||
}
|
||||
]
|
||||
}
|
||||
);
|
||||
|
||||
db.SalesSlips.AddRange(
|
||||
new SalesSlip
|
||||
{
|
||||
SlipNo = $"SSL-{today:yyyy}-00001",
|
||||
SlipDate = today.AddDays(-2),
|
||||
CustomerId = customer.CustomerId,
|
||||
CustomerSnapshotName = customer.Name,
|
||||
WarehouseId = warehouse.WarehouseId,
|
||||
CashierUserId = user.UserId,
|
||||
Status = SalesSlipStatus.Draft,
|
||||
Subtotal = 50m,
|
||||
DiscountTotal = 0m,
|
||||
TaxTotal = 0m,
|
||||
GrandTotal = 50m,
|
||||
PaidAmount = 0m,
|
||||
BalanceAmount = 50m,
|
||||
CreatedAt = createdAt,
|
||||
Lines =
|
||||
[
|
||||
new SalesSlipLine
|
||||
{
|
||||
ItemId = postableItem.ItemId,
|
||||
Description = postableItem.Name,
|
||||
Qty = 1m,
|
||||
FreeQty = 0m,
|
||||
UomId = uom.UomId,
|
||||
WarehouseId = warehouse.WarehouseId,
|
||||
UnitPrice = 50m,
|
||||
BaseCost = 0m,
|
||||
PriceSource = "seed",
|
||||
DiscountMode = SalesDiscountMode.Percentage,
|
||||
DiscountPct = 0m,
|
||||
DiscountAmount = 0m,
|
||||
NetUnitPrice = 50m,
|
||||
LineTotal = 50m,
|
||||
TaxPct = 0m,
|
||||
TaxAmount = 0m,
|
||||
IsFreeIssue = false
|
||||
}
|
||||
]
|
||||
},
|
||||
new SalesSlip
|
||||
{
|
||||
SlipNo = $"SSL-{today:yyyy}-00002",
|
||||
SlipDate = today.AddDays(-1),
|
||||
CustomerId = customer.CustomerId,
|
||||
CustomerSnapshotName = customer.Name,
|
||||
WarehouseId = secondaryWarehouse.WarehouseId,
|
||||
CashierUserId = user.UserId,
|
||||
Status = SalesSlipStatus.Draft,
|
||||
Subtotal = 150m,
|
||||
DiscountTotal = 15m,
|
||||
TaxTotal = 0m,
|
||||
GrandTotal = 135m,
|
||||
PaidAmount = 0m,
|
||||
BalanceAmount = 135m,
|
||||
CreatedAt = createdAt,
|
||||
Lines =
|
||||
[
|
||||
new SalesSlipLine
|
||||
{
|
||||
ItemId = shortageItem.ItemId,
|
||||
Description = shortageItem.Name,
|
||||
Qty = 3m,
|
||||
FreeQty = 0m,
|
||||
UomId = uom.UomId,
|
||||
WarehouseId = secondaryWarehouse.WarehouseId,
|
||||
UnitPrice = 50m,
|
||||
BaseCost = 0m,
|
||||
PriceSource = "seed",
|
||||
DiscountMode = SalesDiscountMode.Percentage,
|
||||
DiscountPct = 10m,
|
||||
DiscountAmount = 15m,
|
||||
NetUnitPrice = 45m,
|
||||
LineTotal = 135m,
|
||||
TaxPct = 0m,
|
||||
TaxAmount = 0m,
|
||||
IsFreeIssue = false
|
||||
}
|
||||
]
|
||||
}
|
||||
);
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,6 +15,7 @@ namespace ERPCore.Infra.Persistence;
|
||||
public class ErpDbContext : DbContext
|
||||
{
|
||||
private readonly ICurrentUser _currentUser;
|
||||
private bool _writingAuditLogs;
|
||||
|
||||
public ErpDbContext(DbContextOptions<ErpDbContext> options, ICurrentUser currentUser) : base(options)
|
||||
{
|
||||
@@ -35,6 +36,8 @@ public class ErpDbContext : DbContext
|
||||
public DbSet<Vendor> Vendors => Set<Vendor>();
|
||||
public DbSet<Warehouse> Warehouses => Set<Warehouse>();
|
||||
public DbSet<Bin> Bins => Set<Bin>();
|
||||
/// <summary>Singleton business profile used by printable documents.</summary>
|
||||
public DbSet<CompanyProfile> CompanyProfiles => Set<CompanyProfile>();
|
||||
/// <summary>Singleton row (FR-MD-11).</summary>
|
||||
public DbSet<ProductConfig> ProductConfig => Set<ProductConfig>();
|
||||
|
||||
@@ -185,24 +188,44 @@ public class ErpDbContext : DbContext
|
||||
// persists the logs without re-auditing them.
|
||||
public override async Task<int> SaveChangesAsync(bool acceptAllChangesOnSuccess, CancellationToken ct = default)
|
||||
{
|
||||
var pending = AuditScribe.Capture(ChangeTracker);
|
||||
IReadOnlyList<PendingAudit> pending = _writingAuditLogs
|
||||
? Array.Empty<PendingAudit>()
|
||||
: AuditScribe.Capture(ChangeTracker);
|
||||
var result = await base.SaveChangesAsync(acceptAllChangesOnSuccess, ct);
|
||||
if (pending.Count > 0)
|
||||
{
|
||||
WriteAuditLogs(pending);
|
||||
await base.SaveChangesAsync(acceptAllChangesOnSuccess, ct);
|
||||
try
|
||||
{
|
||||
_writingAuditLogs = true;
|
||||
WriteAuditLogs(pending);
|
||||
await base.SaveChangesAsync(acceptAllChangesOnSuccess, ct);
|
||||
}
|
||||
finally
|
||||
{
|
||||
_writingAuditLogs = false;
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
public override int SaveChanges(bool acceptAllChangesOnSuccess)
|
||||
{
|
||||
var pending = AuditScribe.Capture(ChangeTracker);
|
||||
IReadOnlyList<PendingAudit> pending = _writingAuditLogs
|
||||
? Array.Empty<PendingAudit>()
|
||||
: AuditScribe.Capture(ChangeTracker);
|
||||
var result = base.SaveChanges(acceptAllChangesOnSuccess);
|
||||
if (pending.Count > 0)
|
||||
{
|
||||
WriteAuditLogs(pending);
|
||||
base.SaveChanges(acceptAllChangesOnSuccess);
|
||||
try
|
||||
{
|
||||
_writingAuditLogs = true;
|
||||
WriteAuditLogs(pending);
|
||||
base.SaveChanges(acceptAllChangesOnSuccess);
|
||||
}
|
||||
finally
|
||||
{
|
||||
_writingAuditLogs = false;
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
@@ -11,7 +11,6 @@ using ERPCore.Services;
|
||||
using ERPCore.Services.Interfaces;
|
||||
using ERPCore.Services.Auth;
|
||||
using ERPCore.Services.Hrm;
|
||||
using ERPCore.Services.Interfaces;
|
||||
using ERPCore.Services.Production;
|
||||
using ERPCore.Services.Stock;
|
||||
using ERPCore.System.Errors;
|
||||
@@ -79,6 +78,7 @@ builder.Services.AddScoped<IUomService, UomService>();
|
||||
builder.Services.AddScoped<ICategoryService, CategoryService>();
|
||||
builder.Services.AddScoped<IBrandService, BrandService>();
|
||||
builder.Services.AddScoped<IItemTypeService, ItemTypeService>();
|
||||
builder.Services.AddScoped<ICompanyProfileService, CompanyProfileService>();
|
||||
builder.Services.AddScoped<IProductConfigService, ProductConfigService>();
|
||||
builder.Services.AddScoped<IVendorService, VendorService>();
|
||||
builder.Services.AddScoped<IWarehouseService, WarehouseService>();
|
||||
@@ -102,6 +102,9 @@ builder.Services.AddScoped<IGrnService, GrnService>();
|
||||
// Sales (Phase 1)
|
||||
builder.Services.AddScoped<ISalesPricingService, SalesPricingService>();
|
||||
builder.Services.AddScoped<ISalesInvoiceService, SalesInvoiceService>();
|
||||
builder.Services.AddScoped<ISalesSlipService, SalesSlipService>();
|
||||
builder.Services.AddScoped<ISalesPromotionSuggestionService, SalesPromotionSuggestionService>();
|
||||
builder.Services.AddScoped<ISalesReportService, SalesReportService>();
|
||||
|
||||
// Stock transactions + reference data (docs/11 §5–6)
|
||||
builder.Services.AddScoped<IStockMutator, StockMutator>();
|
||||
@@ -177,7 +180,15 @@ var app = builder.Build();
|
||||
using (var scope = app.Services.CreateScope())
|
||||
{
|
||||
var db = scope.ServiceProvider.GetRequiredService<ErpDbContext>();
|
||||
await DataSeeder.SeedAsync(db);
|
||||
await db.Database.MigrateAsync();
|
||||
try
|
||||
{
|
||||
await DataSeeder.SeedAsync(db);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
throw new InvalidOperationException("Database migration succeeded, but startup seeding failed.", ex);
|
||||
}
|
||||
}
|
||||
|
||||
app.UseSerilogRequestLogging();
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
using ERPCore.Common.Http;
|
||||
using ERPCore.Dtos.Config;
|
||||
|
||||
namespace ERPCore.Services.Interfaces;
|
||||
|
||||
public interface ICompanyProfileService
|
||||
{
|
||||
Task<ETagged<CompanyProfileDto>> GetAsync(CancellationToken ct = default);
|
||||
Task<ETagged<CompanyProfileDto>> UpdateAsync(UpdateCompanyProfileRequest request, uint expectedRowVersion, CancellationToken ct = default);
|
||||
}
|
||||
@@ -4,6 +4,11 @@ Legend: `[ ]` not started · `[~]` in progress · `[x]` done
|
||||
Spec: `docs/10-BACKEND-PHASE1.md` (model + rules) · `docs/11-BACKEND-PHASE1.md` (API)
|
||||
Convention: `docs/01-DOC-GUIDE.md §6`. Update this file in the **same commit** as the code. When ticking `[x]`, append a short note + any deviation.
|
||||
|
||||
## 8. Sales
|
||||
- [x] Sales bootstrap data seeded locally for development: warehouses, UOMs, categories, items, customers, current-year `SI`/`SSL` sequences, plus sample invoice/slip headers and lines. Existing data is preserved.
|
||||
- [x] Sales report API consolidated into `GET /api/v1/reports/sales` (catalog), `GET /api/v1/reports/sales/{reportId}` (report metadata), and `POST /api/v1/reports/sales/query` (filtered data). Legacy per-report GET routes removed; invalid report/filter combinations now fail validation.
|
||||
- [x] Free-issue CRUD exposed as `api/v1/free-issues` as a thin alias over sales slips. Free issue remains a line-level `IsFreeIssue` / `FreeQty` behavior, not a separate table.
|
||||
|
||||
## 0. Bootstrap
|
||||
- [x] Solution + Web API project (`net10.0`), packages restored (00-CORE §5.4)
|
||||
- [x] Folder structure per 00-CORE §5.3
|
||||
|
||||
Reference in New Issue
Block a user