feat: Implement fixed sale price functionality for items

- Added a toggle for fixed sale price vs stock value in the item creation form.
- Introduced validation to ensure all variants have a price greater than 0 when fixed price mode is selected.
- Updated the item model to include a nullable salePrice field, which is used for sales only and does not affect GRN/FIFO/ledger.
- Enhanced the GRN page to allow off-PO items and included a refresh button to update the item list without reloading the page.
- Updated documentation to reflect changes in item pricing and GRN handling.
This commit is contained in:
2026-07-23 12:12:32 +05:30
parent 5cf9588728
commit a1b3985469
15 changed files with 245 additions and 25 deletions
+8
View File
@@ -33,6 +33,14 @@ public class Item
public StockNature StockNature { get; set; }
public TrackingMode TrackingMode { get; set; }
public string? TaxClass { get; set; }
/// <summary>
/// Optional fixed selling price used by Sales only. <c>null</c> means "use stock value"
/// (the item is sold at its FIFO stock cost at sale time); a value is the fixed sale price.
/// Never enters costing/GRN/FIFO (docs/10 Part C.1, C.9).
/// </summary>
public decimal? SalePrice { get; set; }
public EntityStatus Status { get; set; } = EntityStatus.Active;
public DateTime CreatedAt { get; set; }
+6 -2
View File
@@ -9,7 +9,7 @@ namespace ERPCore.Dtos.Items;
public sealed record ItemListItemDto(
int ItemId, string Sku, string Name, int CategoryId, int? SubCategoryId, int? BrandId,
int BaseUomId, int? DefaultVendorId, StockNature StockNature, TrackingMode TrackingMode,
string? TaxClass, EntityStatus Status);
string? TaxClass, decimal? SalePrice, EntityStatus Status);
/// <summary>A single per-warehouse reorder policy row.</summary>
public sealed record ItemReorderDto(int WarehouseId, decimal ReorderPoint, decimal ReorderQty);
@@ -27,7 +27,7 @@ public sealed record ItemDetailDto(
int ItemId, string Sku, string Name, string? Description, int CategoryId,
int? SubCategoryId, int? BrandId, int BaseUomId, int? DefaultVendorId,
StockNature StockNature, TrackingMode TrackingMode,
string? TaxClass, EntityStatus Status, IReadOnlyList<ItemReorderDto> Reorder,
string? TaxClass, decimal? SalePrice, EntityStatus Status, IReadOnlyList<ItemReorderDto> Reorder,
IReadOnlyList<UomConversionDto> Conversions,
DateTime CreatedAt, DateTime? UpdatedAt);
@@ -62,6 +62,8 @@ public sealed class CreateItemRequest
[Required, EnumDataType(typeof(StockNature))] public StockNature StockNature { get; set; }
[EnumDataType(typeof(TrackingMode))] public TrackingMode TrackingMode { get; set; } = TrackingMode.None;
[StringLength(20)] public string? TaxClass { get; set; }
/// <summary>Optional fixed sale price (Sales only). Null ⇒ sell at stock/FIFO value.</summary>
[Range(0, double.MaxValue)] public decimal? SalePrice { get; set; }
}
public sealed class UpdateItemRequest
@@ -79,6 +81,8 @@ public sealed class UpdateItemRequest
[Required, EnumDataType(typeof(StockNature))] public StockNature StockNature { get; set; }
[EnumDataType(typeof(TrackingMode))] public TrackingMode TrackingMode { get; set; } = TrackingMode.None;
[StringLength(20)] public string? TaxClass { get; set; }
/// <summary>Optional fixed sale price (Sales only). Null ⇒ sell at stock/FIFO value.</summary>
[Range(0, double.MaxValue)] public decimal? SalePrice { get; set; }
}
public sealed class UpdateItemStatusRequest
@@ -19,6 +19,9 @@ public sealed class ItemConfiguration : IEntityTypeConfiguration<Item>
builder.Property(i => i.Description).HasMaxLength(1000);
builder.Property(i => i.TaxClass).HasMaxLength(20);
// Sales-only fixed selling price; nullable (null ⇒ sell at stock/FIFO value).
builder.Property(i => i.SalePrice).HasPrecision(18, 4);
builder.Property(i => i.StockNature)
.HasConversion<string>().HasMaxLength(20).IsRequired();
builder.Property(i => i.TrackingMode)
@@ -385,6 +385,10 @@ namespace ERPCore.Infra.Persistence.Migrations
.HasColumnType("xid")
.HasColumnName("xmin");
b.Property<decimal?>("SalePrice")
.HasPrecision(18, 4)
.HasColumnType("numeric(18,4)");
b.Property<string>("Sku")
.IsRequired()
.HasMaxLength(50)
+4 -2
View File
@@ -79,7 +79,7 @@ public sealed class ItemService : IItemService
.Select(i => new ItemListItemDto(
i.ItemId, i.Sku, i.Name, i.CategoryId, i.SubCategoryId, i.BrandId,
i.BaseUomId, i.DefaultVendorId,
i.StockNature, i.TrackingMode, i.TaxClass, i.Status))
i.StockNature, i.TrackingMode, i.TaxClass, i.SalePrice, i.Status))
.ToListAsync(ct);
return PagedResponse<ItemListItemDto>.Create(rows, query.Page, query.PageSize, total);
@@ -117,6 +117,7 @@ public sealed class ItemService : IItemService
StockNature = request.StockNature,
TrackingMode = request.TrackingMode,
TaxClass = request.TaxClass,
SalePrice = request.SalePrice,
Status = EntityStatus.Active,
CreatedAt = DateTime.UtcNow
};
@@ -158,6 +159,7 @@ public sealed class ItemService : IItemService
item.StockNature = request.StockNature;
item.TrackingMode = request.TrackingMode;
item.TaxClass = request.TaxClass;
item.SalePrice = request.SalePrice;
item.UpdatedAt = DateTime.UtcNow;
await SaveGuardingConcurrencyAsync(ct);
@@ -348,7 +350,7 @@ public sealed class ItemService : IItemService
private static ItemDetailDto ToDetail(Item i) => new(
i.ItemId, i.Sku, i.Name, i.Description, i.CategoryId, i.SubCategoryId, i.BrandId,
i.BaseUomId, i.DefaultVendorId,
i.StockNature, i.TrackingMode, i.TaxClass, i.Status,
i.StockNature, i.TrackingMode, i.TaxClass, i.SalePrice, i.Status,
i.ReorderSettings
.OrderBy(r => r.WarehouseId)
.Select(r => new ItemReorderDto(r.WarehouseId, r.ReorderPoint, r.ReorderQty))