Merge branch 'Dev' into sales-return

This commit is contained in:
2026-08-12 06:26:49 +00:00
116 changed files with 1762 additions and 1067 deletions
@@ -18,7 +18,6 @@ public sealed class BundleSaleLineConfiguration : IEntityTypeConfiguration<Bundl
builder.Property(x => x.IsComponent).HasDefaultValue(true);
builder.HasOne(x => x.Item).WithMany().HasForeignKey(x => x.ItemId).OnDelete(DeleteBehavior.Restrict);
builder.HasOne(x => x.Uom).WithMany().HasForeignKey(x => x.UomId).OnDelete(DeleteBehavior.Restrict);
builder.HasOne(x => x.Warehouse).WithMany().HasForeignKey(x => x.WarehouseId).OnDelete(DeleteBehavior.Restrict);
}
}
@@ -15,7 +15,6 @@ public sealed class BundleSaleTemplateLineConfiguration : IEntityTypeConfigurati
builder.Property(x => x.SortOrder).HasDefaultValue(0);
builder.HasOne(x => x.Item).WithMany().HasForeignKey(x => x.ItemId).OnDelete(DeleteBehavior.Restrict);
builder.HasOne(x => x.Uom).WithMany().HasForeignKey(x => x.UomId).OnDelete(DeleteBehavior.Restrict);
builder.HasOne(x => x.Warehouse).WithMany().HasForeignKey(x => x.WarehouseId).OnDelete(DeleteBehavior.Restrict);
}
}
@@ -49,7 +49,6 @@ public sealed class GrnLineConfiguration : IEntityTypeConfiguration<GrnLine>
builder.HasOne(l => l.Grn).WithMany(g => g.Lines).HasForeignKey(l => l.GrnId).OnDelete(DeleteBehavior.Cascade);
builder.HasOne(l => l.PoLine).WithMany().HasForeignKey(l => l.PoLineId).OnDelete(DeleteBehavior.Restrict);
builder.HasOne(l => l.Item).WithMany().HasForeignKey(l => l.ItemId).OnDelete(DeleteBehavior.Restrict);
builder.HasOne(l => l.Uom).WithMany().HasForeignKey(l => l.UomId).OnDelete(DeleteBehavior.Restrict);
builder.HasOne(l => l.Bin).WithMany().HasForeignKey(l => l.BinId).OnDelete(DeleteBehavior.Restrict);
builder.HasOne(l => l.Batch).WithMany().HasForeignKey(l => l.BatchId).OnDelete(DeleteBehavior.Restrict);
}
@@ -22,6 +22,16 @@ public sealed class ItemConfiguration : IEntityTypeConfiguration<Item>
// Sales-only fixed selling price; nullable (null ⇒ sell at stock/FIFO value).
builder.Property(i => i.SalePrice).HasPrecision(18, 4);
// Optional content size (how much one stocked pack holds). All four are nullable
// together: null ⇒ the item has no measurable content. The base pair is derived
// server-side by ItemContent.Normalize and is only ever Ml or G.
builder.Property(i => i.ContentQty).HasPrecision(18, 4);
builder.Property(i => i.ContentBaseQty).HasPrecision(18, 4);
builder.Property(i => i.ContentUnit)
.HasConversion<string>().HasMaxLength(20);
builder.Property(i => i.ContentBaseUnit)
.HasConversion<string>().HasMaxLength(20);
builder.Property(i => i.StockNature)
.HasConversion<string>().HasMaxLength(20).IsRequired();
builder.Property(i => i.TrackingMode)
@@ -19,6 +19,11 @@ public sealed class ItemTypeConfiguration : IEntityTypeConfiguration<ItemType>
builder.Property(t => t.Name).IsRequired().HasMaxLength(200);
builder.HasIndex(t => t.Name).IsUnique();
// false is the only safe default here: EF uses the CLR default as its "unset" sentinel,
// so if the store default were true, inserting an explicit false would be mistaken for
// "not set" and silently written as true. Sentinel and store default must agree.
builder.Property(t => t.IsMeasurable).IsRequired().HasDefaultValue(false);
builder.Property(t => t.Status)
.HasConversion<string>().HasMaxLength(20).IsRequired()
.HasDefaultValue(EntityStatus.Active);
@@ -91,13 +91,13 @@ public sealed class StageInputConfiguration : IEntityTypeConfiguration<StageInpu
builder.HasKey(i => i.InputId);
builder.Property(i => i.Source).HasConversion<string>().HasMaxLength(20).IsRequired();
builder.Property(i => i.QtyUnit).HasConversion<string>().HasMaxLength(20).IsRequired();
builder.Property(i => i.QtyPerBatch).HasPrecision(18, 4);
builder.HasOne(i => i.Stage).WithMany(s => s.Inputs)
.HasForeignKey(i => i.StageId).OnDelete(DeleteBehavior.Cascade);
builder.HasOne(i => i.Item).WithMany().HasForeignKey(i => i.ItemId).OnDelete(DeleteBehavior.Restrict);
builder.HasOne(i => i.Uom).WithMany().HasForeignKey(i => i.UomId).OnDelete(DeleteBehavior.Restrict);
builder.HasOne(i => i.FromOutput).WithMany()
.HasForeignKey(i => i.FromOutputId).OnDelete(DeleteBehavior.Restrict);
}
@@ -206,6 +206,7 @@ public sealed class RunStageInputConfiguration : IEntityTypeConfiguration<RunSta
builder.HasKey(i => i.RunInputId);
builder.Property(i => i.Source).HasConversion<string>().HasMaxLength(20).IsRequired();
builder.Property(i => i.QtyUnit).HasConversion<string>().HasMaxLength(20).IsRequired();
builder.Property(i => i.PlannedQty).HasPrecision(18, 4);
builder.Property(i => i.ConsumedQty).HasPrecision(18, 4);
builder.Property(i => i.ConsumedValue).HasPrecision(18, 4);
@@ -217,7 +218,6 @@ public sealed class RunStageInputConfiguration : IEntityTypeConfiguration<RunSta
.HasForeignKey(i => i.RunStageId).OnDelete(DeleteBehavior.Cascade);
builder.HasOne(i => i.Item).WithMany().HasForeignKey(i => i.ItemId).OnDelete(DeleteBehavior.Restrict);
builder.HasOne(i => i.Uom).WithMany().HasForeignKey(i => i.UomId).OnDelete(DeleteBehavior.Restrict);
builder.HasOne(i => i.FromRunOutput).WithMany()
.HasForeignKey(i => i.FromRunOutputId).OnDelete(DeleteBehavior.Restrict);
@@ -63,11 +63,6 @@ public sealed class PoLineConfiguration : IEntityTypeConfiguration<PoLine>
.HasForeignKey(l => l.ItemId)
.OnDelete(DeleteBehavior.Restrict);
builder.HasOne(l => l.Uom)
.WithMany()
.HasForeignKey(l => l.UomId)
.OnDelete(DeleteBehavior.Restrict);
builder.HasOne(l => l.Warehouse)
.WithMany()
.HasForeignKey(l => l.WarehouseId)
@@ -79,11 +79,6 @@ public sealed class SalesInvoiceLineConfiguration : IEntityTypeConfiguration<Sal
builder.Property(x => x.TaxPct).HasPrecision(9, 4);
builder.Property(x => x.TaxAmount).HasPrecision(18, 4);
builder.HasOne(x => x.Uom)
.WithMany()
.HasForeignKey(x => x.UomId)
.OnDelete(DeleteBehavior.Restrict);
builder.HasOne(x => x.Warehouse)
.WithMany()
.HasForeignKey(x => x.WarehouseId)
@@ -79,11 +79,6 @@ public sealed class SalesSlipLineConfiguration : IEntityTypeConfiguration<SalesS
builder.Property(x => x.TaxPct).HasPrecision(9, 4);
builder.Property(x => x.TaxAmount).HasPrecision(18, 4);
builder.HasOne(x => x.Uom)
.WithMany()
.HasForeignKey(x => x.UomId)
.OnDelete(DeleteBehavior.Restrict);
builder.HasOne(x => x.Warehouse)
.WithMany()
.HasForeignKey(x => x.WarehouseId)
@@ -1,34 +0,0 @@
using ERPCore.Domain.Entities;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
namespace ERPCore.Infra.Persistence.Configurations;
public sealed class UomConversionConfiguration : IEntityTypeConfiguration<UomConversion>
{
public void Configure(EntityTypeBuilder<UomConversion> builder)
{
builder.ToTable("uom_conversions");
builder.HasKey(c => c.ConversionId);
builder.Property(c => c.Factor).HasPrecision(18, 6);
builder.HasOne(c => c.Item)
.WithMany(i => i.UomConversions)
.HasForeignKey(c => c.ItemId)
.OnDelete(DeleteBehavior.Cascade);
builder.HasOne(c => c.FromUom)
.WithMany()
.HasForeignKey(c => c.FromUomId)
.OnDelete(DeleteBehavior.Restrict);
builder.HasOne(c => c.ToUom)
.WithMany()
.HasForeignKey(c => c.ToUomId)
.OnDelete(DeleteBehavior.Restrict);
// One conversion per (item, from, to) triple.
builder.HasIndex(c => new { c.ItemId, c.FromUomId, c.ToUomId }).IsUnique();
}
}
@@ -285,13 +285,18 @@ public static class DataSeeder
new Item
{
Sku = "SKU-DEMO-002",
Name = "Demo Item 2",
Description = "Secondary seeded sample item for sales documents",
Name = "Demo Item 2 (500 ml)",
Description = "Secondary seeded sample item; carries a content size so the "
+ "production content-unit path has a fixture",
CategoryId = category.CategoryId,
BaseUomId = uom.UomId,
StockNature = StockNature.Stocked,
TrackingMode = TrackingMode.None,
SalePrice = 50m,
ContentQty = 500m,
ContentUnit = MeasureUnit.Ml,
ContentBaseQty = 500m,
ContentBaseUnit = MeasureUnit.Ml,
Status = EntityStatus.Active,
CreatedAt = now
}
@@ -438,7 +443,6 @@ public static class DataSeeder
new BundleSaleTemplateLine
{
ItemId = items[0].ItemId,
UomId = uom.UomId,
WarehouseId = warehouse.WarehouseId,
Qty = 1m,
UnitPrice = items[0].SalePrice.GetValueOrDefault(),
@@ -448,7 +452,6 @@ public static class DataSeeder
new BundleSaleTemplateLine
{
ItemId = items[1].ItemId,
UomId = uom.UomId,
WarehouseId = warehouse.WarehouseId,
Qty = 1m,
UnitPrice = items[1].SalePrice.GetValueOrDefault(),
@@ -489,7 +492,6 @@ public static class DataSeeder
ItemId = items[0].ItemId,
Description = items[0].Name,
Qty = 1m,
UomId = uom.UomId,
WarehouseId = warehouse.WarehouseId,
UnitPrice = items[0].SalePrice.GetValueOrDefault(),
LineTotal = items[0].SalePrice.GetValueOrDefault(),
@@ -501,7 +503,6 @@ public static class DataSeeder
ItemId = items[1].ItemId,
Description = items[1].Name,
Qty = 1m,
UomId = uom.UomId,
WarehouseId = warehouse.WarehouseId,
UnitPrice = items[1].SalePrice.GetValueOrDefault(),
LineTotal = items[1].SalePrice.GetValueOrDefault(),
@@ -537,7 +538,6 @@ public static class DataSeeder
ItemId = items[0].ItemId,
Description = items[0].Name,
Qty = 1m,
UomId = uom.UomId,
WarehouseId = warehouse.WarehouseId,
UnitPrice = items[0].SalePrice.GetValueOrDefault(),
LineTotal = items[0].SalePrice.GetValueOrDefault(),
@@ -549,7 +549,6 @@ public static class DataSeeder
ItemId = items[1].ItemId,
Description = items[1].Name,
Qty = 1m,
UomId = uom.UomId,
WarehouseId = warehouse.WarehouseId,
UnitPrice = items[1].SalePrice.GetValueOrDefault(),
LineTotal = items[1].SalePrice.GetValueOrDefault(),
@@ -585,7 +584,6 @@ public static class DataSeeder
ItemId = items[0].ItemId,
Description = items[0].Name,
Qty = 1m,
UomId = uom.UomId,
WarehouseId = secondaryWarehouse.WarehouseId,
UnitPrice = items[0].SalePrice.GetValueOrDefault(),
LineTotal = items[0].SalePrice.GetValueOrDefault(),
@@ -662,7 +660,6 @@ public static class DataSeeder
Description = postableItem.Name,
Qty = 2m,
FreeQty = 0m,
UomId = uom.UomId,
WarehouseId = warehouse.WarehouseId,
UnitPrice = 100m,
BaseCost = 0m,
@@ -706,7 +703,6 @@ public static class DataSeeder
Description = shortageItem.Name,
Qty = 6m,
FreeQty = 0m,
UomId = uom.UomId,
WarehouseId = secondaryWarehouse.WarehouseId,
UnitPrice = 50m,
BaseCost = 0m,
@@ -751,7 +747,6 @@ public static class DataSeeder
Description = postableItem.Name,
Qty = 1m,
FreeQty = 0m,
UomId = uom.UomId,
WarehouseId = warehouse.WarehouseId,
UnitPrice = 100m,
BaseCost = 0m,
@@ -794,7 +789,6 @@ public static class DataSeeder
Description = postableItem.Name,
Qty = 1m,
FreeQty = 0m,
UomId = uom.UomId,
WarehouseId = warehouse.WarehouseId,
UnitPrice = 50m,
BaseCost = 0m,
@@ -834,7 +828,6 @@ public static class DataSeeder
Description = shortageItem.Name,
Qty = 3m,
FreeQty = 0m,
UomId = uom.UomId,
WarehouseId = secondaryWarehouse.WarehouseId,
UnitPrice = 50m,
BaseCost = 0m,
@@ -30,7 +30,6 @@ public class ErpDbContext : DbContext
/// <summary>Color/Size/Material dimension names. Unlinked to Item by design (docs/10 C.9).</summary>
public DbSet<ItemType> ItemTypes => Set<ItemType>();
public DbSet<Uom> Uoms => Set<Uom>();
public DbSet<UomConversion> UomConversions => Set<UomConversion>();
public DbSet<Item> Items => Set<Item>();
public DbSet<ItemReorder> ItemReorders => Set<ItemReorder>();
public DbSet<Vendor> Vendors => Set<Vendor>();