add shared pricing resolution with fixed yem price,GRN weighted and FIFO valuation cost fallback
This commit is contained in:
@@ -32,6 +32,7 @@ public sealed class CreateSalesInvoiceLineRequest
|
|||||||
[Range(0.0001, double.MaxValue)] public decimal Qty { get; set; }
|
[Range(0.0001, double.MaxValue)] public decimal Qty { get; set; }
|
||||||
[Range(0, double.MaxValue)] public decimal FreeQty { get; set; }
|
[Range(0, double.MaxValue)] public decimal FreeQty { get; set; }
|
||||||
[Range(0, double.MaxValue)] public decimal? UnitPrice { get; set; }
|
[Range(0, double.MaxValue)] public decimal? UnitPrice { get; set; }
|
||||||
|
public bool AllowManualPriceOverride { get; set; }
|
||||||
[Range(0, 100)] public decimal DiscountPct { get; set; }
|
[Range(0, 100)] public decimal DiscountPct { get; set; }
|
||||||
[Range(0, double.MaxValue)] public decimal DiscountAmount { get; set; }
|
[Range(0, double.MaxValue)] public decimal DiscountAmount { get; set; }
|
||||||
[Range(0, 100)] public decimal TaxPct { get; set; }
|
[Range(0, 100)] public decimal TaxPct { get; set; }
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ public sealed class CreateSalesSlipLineRequest
|
|||||||
[Range(0.0001, double.MaxValue)] public decimal Qty { get; set; }
|
[Range(0.0001, double.MaxValue)] public decimal Qty { get; set; }
|
||||||
[Range(0, double.MaxValue)] public decimal FreeQty { get; set; }
|
[Range(0, double.MaxValue)] public decimal FreeQty { get; set; }
|
||||||
[Range(0, double.MaxValue)] public decimal? UnitPrice { get; set; }
|
[Range(0, double.MaxValue)] public decimal? UnitPrice { get; set; }
|
||||||
|
public bool AllowManualPriceOverride { get; set; }
|
||||||
[Range(0, 100)] public decimal DiscountPct { get; set; }
|
[Range(0, 100)] public decimal DiscountPct { get; set; }
|
||||||
[Range(0, double.MaxValue)] public decimal DiscountAmount { get; set; }
|
[Range(0, double.MaxValue)] public decimal DiscountAmount { get; set; }
|
||||||
[Range(0, 100)] public decimal TaxPct { get; set; }
|
[Range(0, 100)] public decimal TaxPct { get; set; }
|
||||||
|
|||||||
@@ -90,6 +90,7 @@ builder.Services.AddScoped<IStockService, StockService>();
|
|||||||
builder.Services.AddScoped<IGrnService, GrnService>();
|
builder.Services.AddScoped<IGrnService, GrnService>();
|
||||||
|
|
||||||
// Sales (Phase 1)
|
// Sales (Phase 1)
|
||||||
|
builder.Services.AddScoped<ISalesPricingService, SalesPricingService>();
|
||||||
builder.Services.AddScoped<ISalesInvoiceService, SalesInvoiceService>();
|
builder.Services.AddScoped<ISalesInvoiceService, SalesInvoiceService>();
|
||||||
builder.Services.AddScoped<ISalesSlipService, SalesSlipService>();
|
builder.Services.AddScoped<ISalesSlipService, SalesSlipService>();
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,16 @@
|
|||||||
|
namespace ERPCore.Services.Interfaces;
|
||||||
|
|
||||||
|
public interface ISalesPricingService
|
||||||
|
{
|
||||||
|
Task<SalesPriceResolution> ResolveAsync(
|
||||||
|
int itemId,
|
||||||
|
int warehouseId,
|
||||||
|
decimal? requestedUnitPrice,
|
||||||
|
bool allowManualOverride,
|
||||||
|
CancellationToken ct = default);
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed record SalesPriceResolution(
|
||||||
|
decimal UnitPrice,
|
||||||
|
string PriceSource,
|
||||||
|
decimal BaseCost);
|
||||||
@@ -21,6 +21,7 @@ public sealed class SalesInvoiceService : ISalesInvoiceService
|
|||||||
private readonly IRepository<Item> _items;
|
private readonly IRepository<Item> _items;
|
||||||
private readonly IRepository<Uom> _uoms;
|
private readonly IRepository<Uom> _uoms;
|
||||||
private readonly IRepository<Warehouse> _warehouses;
|
private readonly IRepository<Warehouse> _warehouses;
|
||||||
|
private readonly ISalesPricingService _pricing;
|
||||||
private readonly IFifoCostingService _fifo;
|
private readonly IFifoCostingService _fifo;
|
||||||
private readonly ICurrentUser _currentUser;
|
private readonly ICurrentUser _currentUser;
|
||||||
private readonly INumberSequenceService _numbers;
|
private readonly INumberSequenceService _numbers;
|
||||||
@@ -28,7 +29,7 @@ public sealed class SalesInvoiceService : ISalesInvoiceService
|
|||||||
|
|
||||||
public SalesInvoiceService(
|
public SalesInvoiceService(
|
||||||
IRepository<SalesInvoice> invoices, IRepository<Customer> customers, IRepository<Item> items,
|
IRepository<SalesInvoice> invoices, IRepository<Customer> customers, IRepository<Item> items,
|
||||||
IRepository<Uom> uoms, IRepository<Warehouse> warehouses, IFifoCostingService fifo,
|
IRepository<Uom> uoms, IRepository<Warehouse> warehouses, ISalesPricingService pricing, IFifoCostingService fifo,
|
||||||
ICurrentUser currentUser, INumberSequenceService numbers, IUnitOfWork uow)
|
ICurrentUser currentUser, INumberSequenceService numbers, IUnitOfWork uow)
|
||||||
{
|
{
|
||||||
_invoices = invoices;
|
_invoices = invoices;
|
||||||
@@ -36,6 +37,7 @@ public sealed class SalesInvoiceService : ISalesInvoiceService
|
|||||||
_items = items;
|
_items = items;
|
||||||
_uoms = uoms;
|
_uoms = uoms;
|
||||||
_warehouses = warehouses;
|
_warehouses = warehouses;
|
||||||
|
_pricing = pricing;
|
||||||
_fifo = fifo;
|
_fifo = fifo;
|
||||||
_currentUser = currentUser;
|
_currentUser = currentUser;
|
||||||
_numbers = numbers;
|
_numbers = numbers;
|
||||||
@@ -175,23 +177,9 @@ public sealed class SalesInvoiceService : ISalesInvoiceService
|
|||||||
foreach (var r in requests)
|
foreach (var r in requests)
|
||||||
{
|
{
|
||||||
var item = await _items.Query().AsNoTracking().FirstAsync(x => x.ItemId == r.ItemId, ct);
|
var item = await _items.Query().AsNoTracking().FirstAsync(x => x.ItemId == r.ItemId, ct);
|
||||||
var priceSource = "SALE_PRICE";
|
var resolved = await _pricing.ResolveAsync(r.ItemId, r.WarehouseId, r.UnitPrice, r.AllowManualPriceOverride, ct);
|
||||||
decimal unitPrice;
|
var unitPrice = resolved.UnitPrice;
|
||||||
if (r.UnitPrice is not null)
|
var priceSource = resolved.PriceSource;
|
||||||
{
|
|
||||||
unitPrice = r.UnitPrice.Value;
|
|
||||||
priceSource = "MANUAL";
|
|
||||||
}
|
|
||||||
else if (item.SalePrice.HasValue)
|
|
||||||
{
|
|
||||||
unitPrice = item.SalePrice.Value;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
var valuation = await _fifo.GetValuationAsync(r.ItemId, r.WarehouseId, ct);
|
|
||||||
unitPrice = valuation.TotalQty > 0 ? valuation.TotalValue / valuation.TotalQty : 0m;
|
|
||||||
priceSource = "FIFO_AVG";
|
|
||||||
}
|
|
||||||
|
|
||||||
var gross = r.Qty * unitPrice;
|
var gross = r.Qty * unitPrice;
|
||||||
var discountAfterPct = gross * (r.DiscountPct / 100m);
|
var discountAfterPct = gross * (r.DiscountPct / 100m);
|
||||||
|
|||||||
@@ -0,0 +1,79 @@
|
|||||||
|
using ERPCore.Domain;
|
||||||
|
using ERPCore.Domain.Entities;
|
||||||
|
using ERPCore.Domain.Enums;
|
||||||
|
using ERPCore.Repositories.Interfaces;
|
||||||
|
using ERPCore.Services.Interfaces;
|
||||||
|
using ERPCore.Services.Stock;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
|
||||||
|
namespace ERPCore.Services;
|
||||||
|
|
||||||
|
public sealed class SalesPricingService : ISalesPricingService
|
||||||
|
{
|
||||||
|
private readonly IRepository<Item> _items;
|
||||||
|
private readonly IRepository<GrnLine> _grnLines;
|
||||||
|
private readonly IFifoCostingService _fifo;
|
||||||
|
|
||||||
|
public SalesPricingService(IRepository<Item> items, IRepository<GrnLine> grnLines, IFifoCostingService fifo)
|
||||||
|
{
|
||||||
|
_items = items;
|
||||||
|
_grnLines = grnLines;
|
||||||
|
_fifo = fifo;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<SalesPriceResolution> ResolveAsync(
|
||||||
|
int itemId, int warehouseId, decimal? requestedUnitPrice, bool allowManualOverride, CancellationToken ct = default)
|
||||||
|
{
|
||||||
|
var item = await _items.Query().AsNoTracking().FirstOrDefaultAsync(x => x.ItemId == itemId, ct)
|
||||||
|
?? throw new InvalidOperationException($"Item {itemId} was not found.");
|
||||||
|
|
||||||
|
if (requestedUnitPrice is not null)
|
||||||
|
{
|
||||||
|
if (!allowManualOverride)
|
||||||
|
{
|
||||||
|
if (item.SalePrice.HasValue)
|
||||||
|
return new SalesPriceResolution(item.SalePrice.Value, "SALE_PRICE", item.SalePrice.Value);
|
||||||
|
|
||||||
|
var grnPrice = await GetWeightedGrnPriceAsync(itemId, warehouseId, ct);
|
||||||
|
if (grnPrice is not null)
|
||||||
|
return new SalesPriceResolution(grnPrice.Value, "GRN_WEIGHTED_AVG", grnPrice.Value);
|
||||||
|
|
||||||
|
return new SalesPriceResolution(await GetFifoFallbackPriceAsync(itemId, warehouseId, ct), "FIFO_AVG", 0m);
|
||||||
|
}
|
||||||
|
|
||||||
|
return new SalesPriceResolution(requestedUnitPrice.Value, "MANUAL", requestedUnitPrice.Value);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (item.SalePrice.HasValue)
|
||||||
|
return new SalesPriceResolution(item.SalePrice.Value, "SALE_PRICE", item.SalePrice.Value);
|
||||||
|
|
||||||
|
var weighted = await GetWeightedGrnPriceAsync(itemId, warehouseId, ct);
|
||||||
|
if (weighted is not null)
|
||||||
|
return new SalesPriceResolution(weighted.Value, "GRN_WEIGHTED_AVG", weighted.Value);
|
||||||
|
|
||||||
|
return new SalesPriceResolution(await GetFifoFallbackPriceAsync(itemId, warehouseId, ct), "FIFO_AVG", 0m);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<decimal?> GetWeightedGrnPriceAsync(int itemId, int warehouseId, CancellationToken ct)
|
||||||
|
{
|
||||||
|
var rows = await _grnLines.Query().AsNoTracking()
|
||||||
|
.Where(l => l.ItemId == itemId
|
||||||
|
&& l.Grn != null
|
||||||
|
&& l.Grn.WarehouseId == warehouseId
|
||||||
|
&& l.Grn.Status == GrnStatus.Confirmed)
|
||||||
|
.Select(l => new { l.Qty, l.ReceivedValue })
|
||||||
|
.ToListAsync(ct);
|
||||||
|
|
||||||
|
var totalQty = rows.Sum(x => x.Qty);
|
||||||
|
if (totalQty <= 0) return null;
|
||||||
|
|
||||||
|
var totalValue = rows.Sum(x => x.ReceivedValue);
|
||||||
|
return totalValue / totalQty;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<decimal> GetFifoFallbackPriceAsync(int itemId, int warehouseId, CancellationToken ct)
|
||||||
|
{
|
||||||
|
var valuation = await _fifo.GetValuationAsync(itemId, warehouseId, ct);
|
||||||
|
return valuation.TotalQty > 0 ? valuation.TotalValue / valuation.TotalQty : 0m;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -22,6 +22,7 @@ public sealed class SalesSlipService : ISalesSlipService
|
|||||||
private readonly IRepository<Uom> _uoms;
|
private readonly IRepository<Uom> _uoms;
|
||||||
private readonly IRepository<Warehouse> _warehouses;
|
private readonly IRepository<Warehouse> _warehouses;
|
||||||
private readonly IRepository<User> _users;
|
private readonly IRepository<User> _users;
|
||||||
|
private readonly ISalesPricingService _pricing;
|
||||||
private readonly IFifoCostingService _fifo;
|
private readonly IFifoCostingService _fifo;
|
||||||
private readonly ICurrentUser _currentUser;
|
private readonly ICurrentUser _currentUser;
|
||||||
private readonly INumberSequenceService _numbers;
|
private readonly INumberSequenceService _numbers;
|
||||||
@@ -29,7 +30,7 @@ public sealed class SalesSlipService : ISalesSlipService
|
|||||||
|
|
||||||
public SalesSlipService(
|
public SalesSlipService(
|
||||||
IRepository<SalesSlip> slips, IRepository<Customer> customers, IRepository<Item> items,
|
IRepository<SalesSlip> slips, IRepository<Customer> customers, IRepository<Item> items,
|
||||||
IRepository<Uom> uoms, IRepository<Warehouse> warehouses, IRepository<User> users, IFifoCostingService fifo,
|
IRepository<Uom> uoms, IRepository<Warehouse> warehouses, IRepository<User> users, ISalesPricingService pricing, IFifoCostingService fifo,
|
||||||
ICurrentUser currentUser, INumberSequenceService numbers, IUnitOfWork uow)
|
ICurrentUser currentUser, INumberSequenceService numbers, IUnitOfWork uow)
|
||||||
{
|
{
|
||||||
_slips = slips;
|
_slips = slips;
|
||||||
@@ -38,6 +39,7 @@ public sealed class SalesSlipService : ISalesSlipService
|
|||||||
_uoms = uoms;
|
_uoms = uoms;
|
||||||
_warehouses = warehouses;
|
_warehouses = warehouses;
|
||||||
_users = users;
|
_users = users;
|
||||||
|
_pricing = pricing;
|
||||||
_fifo = fifo;
|
_fifo = fifo;
|
||||||
_currentUser = currentUser;
|
_currentUser = currentUser;
|
||||||
_numbers = numbers;
|
_numbers = numbers;
|
||||||
@@ -157,8 +159,6 @@ public sealed class SalesSlipService : ISalesSlipService
|
|||||||
throw new NotFoundException($"Customer {customerId} was not found.");
|
throw new NotFoundException($"Customer {customerId} was not found.");
|
||||||
if (!await _warehouses.Query().AnyAsync(x => x.WarehouseId == warehouseId, ct))
|
if (!await _warehouses.Query().AnyAsync(x => x.WarehouseId == warehouseId, ct))
|
||||||
throw new NotFoundException($"Warehouse {warehouseId} was not found.");
|
throw new NotFoundException($"Warehouse {warehouseId} was not found.");
|
||||||
if (!await _customers.Query().AnyAsync(x => x.CustomerId == customerId, ct))
|
|
||||||
throw new NotFoundException($"Customer {customerId} was not found.");
|
|
||||||
if (!await _users.Query().AnyAsync(x => x.UserId == cashierUserId, ct))
|
if (!await _users.Query().AnyAsync(x => x.UserId == cashierUserId, ct))
|
||||||
throw new NotFoundException($"User {cashierUserId} was not found.");
|
throw new NotFoundException($"User {cashierUserId} was not found.");
|
||||||
foreach (var line in lines)
|
foreach (var line in lines)
|
||||||
@@ -178,23 +178,9 @@ public sealed class SalesSlipService : ISalesSlipService
|
|||||||
foreach (var r in requests)
|
foreach (var r in requests)
|
||||||
{
|
{
|
||||||
var item = await _items.Query().AsNoTracking().FirstAsync(x => x.ItemId == r.ItemId, ct);
|
var item = await _items.Query().AsNoTracking().FirstAsync(x => x.ItemId == r.ItemId, ct);
|
||||||
var priceSource = "SALE_PRICE";
|
var resolved = await _pricing.ResolveAsync(r.ItemId, r.WarehouseId, r.UnitPrice, r.AllowManualPriceOverride, ct);
|
||||||
decimal unitPrice;
|
var unitPrice = resolved.UnitPrice;
|
||||||
if (r.UnitPrice is not null)
|
var priceSource = resolved.PriceSource;
|
||||||
{
|
|
||||||
unitPrice = r.UnitPrice.Value;
|
|
||||||
priceSource = "MANUAL";
|
|
||||||
}
|
|
||||||
else if (item.SalePrice.HasValue)
|
|
||||||
{
|
|
||||||
unitPrice = item.SalePrice.Value;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
var valuation = await _fifo.GetValuationAsync(r.ItemId, r.WarehouseId, ct);
|
|
||||||
unitPrice = valuation.TotalQty > 0 ? valuation.TotalValue / valuation.TotalQty : 0m;
|
|
||||||
priceSource = "FIFO_AVG";
|
|
||||||
}
|
|
||||||
|
|
||||||
var gross = r.Qty * unitPrice;
|
var gross = r.Qty * unitPrice;
|
||||||
var discountAfterPct = gross * (r.DiscountPct / 100m);
|
var discountAfterPct = gross * (r.DiscountPct / 100m);
|
||||||
|
|||||||
Reference in New Issue
Block a user