Add inventory management features including Sales, Stock Returns, and Suppliers
- Implement Sale and SaleItem models for tracking sales transactions. - Create StockReturn model to manage stock adjustments for various reasons. - Introduce Supplier model to represent suppliers of products. - Develop InventoryRepository for CRUD operations on products, suppliers, sales, and stock returns. - Implement InventoryService to handle business logic for inventory management. - Update TenantDbContext to include new models and relationships. - Enhance ProfitLossRepository to account for sales, purchases, and stock returns in profit and loss calculations. - Modify Program.cs to register new inventory-related services.
This commit is contained in:
@@ -0,0 +1,188 @@
|
||||
using Fitness_core_BE.Interfaces;
|
||||
using Fitness_core_BE.Models;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace Fitness_core_BE.Repos
|
||||
{
|
||||
public class InventoryRepository : IInventoryRepository
|
||||
{
|
||||
private readonly TenantDbContext _context;
|
||||
|
||||
public InventoryRepository(TenantDbContext context)
|
||||
{
|
||||
_context = context;
|
||||
}
|
||||
|
||||
// --- Products ---
|
||||
|
||||
public async Task<List<Product>> GetProducts(Guid clientId, bool includeInactive = false)
|
||||
{
|
||||
var query = _context.Products.Where(p => p.FKClientId == clientId);
|
||||
if (!includeInactive)
|
||||
query = query.Where(p => p.IsActive);
|
||||
|
||||
return await query.OrderBy(p => p.Name).ToListAsync();
|
||||
}
|
||||
|
||||
public Task<Product?> GetProductById(Guid productId)
|
||||
=> _context.Products.FirstOrDefaultAsync(p => p.ProductId == productId);
|
||||
|
||||
public async Task<Product> CreateProduct(Product product)
|
||||
{
|
||||
product.ProductId = Guid.NewGuid();
|
||||
product.CreatedAt = DateTime.UtcNow;
|
||||
product.IsActive = true;
|
||||
product.CostPrice = 0m;
|
||||
product.QuantityOnHand = 0m;
|
||||
|
||||
_context.Products.Add(product);
|
||||
return product;
|
||||
}
|
||||
|
||||
public async Task<Product> UpdateProduct(Product product)
|
||||
{
|
||||
var existing = await _context.Products.FindAsync(product.ProductId)
|
||||
?? throw new KeyNotFoundException("Product not found");
|
||||
|
||||
// Sku is system-generated at creation and never edited -- not copied here.
|
||||
existing.Name = product.Name;
|
||||
existing.UnitOfMeasure = product.UnitOfMeasure;
|
||||
existing.SellingPrice = product.SellingPrice;
|
||||
existing.IsActive = product.IsActive;
|
||||
|
||||
return existing;
|
||||
}
|
||||
|
||||
public async Task<int> GetNextProductSequence(Guid clientId)
|
||||
=> await _context.Products.CountAsync(p => p.FKClientId == clientId) + 1;
|
||||
|
||||
public Task<List<GoodsReceivedNoteItem>> GetGrnItemsForProduct(Guid productId)
|
||||
=> _context.GoodsReceivedNoteItems
|
||||
.Include(i => i.Grn)
|
||||
.Where(i => i.FKProductId == productId)
|
||||
.OrderByDescending(i => i.Grn!.ReceivedAt)
|
||||
.ToListAsync();
|
||||
|
||||
public Task<List<SaleItem>> GetSaleItemsForProduct(Guid productId)
|
||||
=> _context.SaleItems
|
||||
.Include(i => i.Sale)
|
||||
.Where(i => i.FKProductId == productId)
|
||||
.OrderByDescending(i => i.Sale!.SoldAt)
|
||||
.ToListAsync();
|
||||
|
||||
public Task<List<StockReturn>> GetStockReturnsForProduct(Guid productId)
|
||||
=> _context.StockReturns
|
||||
.Where(r => r.FKProductId == productId)
|
||||
.OrderByDescending(r => r.ReturnedAt)
|
||||
.ToListAsync();
|
||||
|
||||
// --- Suppliers ---
|
||||
|
||||
public async Task<List<Supplier>> GetSuppliers(Guid clientId, bool includeInactive = false)
|
||||
{
|
||||
var query = _context.Suppliers.Where(s => s.FKClientId == clientId);
|
||||
if (!includeInactive)
|
||||
query = query.Where(s => s.IsActive);
|
||||
|
||||
return await query.OrderBy(s => s.Name).ToListAsync();
|
||||
}
|
||||
|
||||
public Task<Supplier?> GetSupplierById(Guid supplierId)
|
||||
=> _context.Suppliers.FirstOrDefaultAsync(s => s.SupplierId == supplierId);
|
||||
|
||||
public async Task<Supplier> CreateSupplier(Supplier supplier)
|
||||
{
|
||||
supplier.SupplierId = Guid.NewGuid();
|
||||
supplier.CreatedAt = DateTime.UtcNow;
|
||||
supplier.IsActive = true;
|
||||
|
||||
_context.Suppliers.Add(supplier);
|
||||
return supplier;
|
||||
}
|
||||
|
||||
// --- GRN ---
|
||||
|
||||
public async Task<GoodsReceivedNote> CreateGrn(GoodsReceivedNote grn)
|
||||
{
|
||||
grn.GrnId = Guid.NewGuid();
|
||||
grn.CreatedAt = DateTime.UtcNow;
|
||||
foreach (var item in grn.Items)
|
||||
item.GrnItemId = Guid.NewGuid();
|
||||
|
||||
_context.GoodsReceivedNotes.Add(grn);
|
||||
return grn;
|
||||
}
|
||||
|
||||
public Task<List<GoodsReceivedNote>> GetGrns(Guid clientId)
|
||||
=> _context.GoodsReceivedNotes
|
||||
.Include(g => g.Supplier)
|
||||
.Include(g => g.Items).ThenInclude(i => i.Product)
|
||||
.Where(g => g.FKClientId == clientId)
|
||||
.OrderByDescending(g => g.ReceivedAt)
|
||||
.ToListAsync();
|
||||
|
||||
public Task<GoodsReceivedNote?> GetGrnById(Guid grnId)
|
||||
=> _context.GoodsReceivedNotes
|
||||
.Include(g => g.Supplier)
|
||||
.Include(g => g.Items).ThenInclude(i => i.Product)
|
||||
.FirstOrDefaultAsync(g => g.GrnId == grnId);
|
||||
|
||||
public async Task<int> GetNextGrnSequence(Guid clientId)
|
||||
=> await _context.GoodsReceivedNotes.CountAsync(g => g.FKClientId == clientId) + 1;
|
||||
|
||||
// --- Sales ---
|
||||
|
||||
public async Task<Sale> CreateSale(Sale sale)
|
||||
{
|
||||
sale.SaleId = Guid.NewGuid();
|
||||
sale.CreatedAt = DateTime.UtcNow;
|
||||
foreach (var item in sale.Items)
|
||||
item.SaleItemId = Guid.NewGuid();
|
||||
|
||||
_context.Sales.Add(sale);
|
||||
return sale;
|
||||
}
|
||||
|
||||
public Task<List<Sale>> GetSales(Guid clientId)
|
||||
=> _context.Sales
|
||||
.Include(s => s.Items).ThenInclude(i => i.Product)
|
||||
.Where(s => s.FKClientId == clientId)
|
||||
.OrderByDescending(s => s.SoldAt)
|
||||
.ToListAsync();
|
||||
|
||||
public Task<Sale?> GetSaleById(Guid saleId)
|
||||
=> _context.Sales
|
||||
.Include(s => s.Items).ThenInclude(i => i.Product)
|
||||
.FirstOrDefaultAsync(s => s.SaleId == saleId);
|
||||
|
||||
public async Task<int> GetNextSaleSequence(Guid clientId)
|
||||
=> await _context.Sales.CountAsync(s => s.FKClientId == clientId) + 1;
|
||||
|
||||
// --- Returns ---
|
||||
|
||||
public async Task<StockReturn> CreateStockReturn(StockReturn stockReturn)
|
||||
{
|
||||
stockReturn.StockReturnId = Guid.NewGuid();
|
||||
stockReturn.CreatedAt = DateTime.UtcNow;
|
||||
|
||||
_context.StockReturns.Add(stockReturn);
|
||||
return stockReturn;
|
||||
}
|
||||
|
||||
public Task<List<StockReturn>> GetStockReturns(Guid clientId)
|
||||
=> _context.StockReturns
|
||||
.Include(r => r.Product)
|
||||
.Include(r => r.Sale)
|
||||
.Include(r => r.Grn)
|
||||
.Where(r => r.FKClientId == clientId)
|
||||
.OrderByDescending(r => r.ReturnedAt)
|
||||
.ToListAsync();
|
||||
|
||||
public Task<StockReturn?> GetStockReturnById(Guid stockReturnId)
|
||||
=> _context.StockReturns
|
||||
.Include(r => r.Product)
|
||||
.Include(r => r.Sale)
|
||||
.Include(r => r.Grn)
|
||||
.FirstOrDefaultAsync(r => r.StockReturnId == stockReturnId);
|
||||
}
|
||||
}
|
||||
@@ -105,9 +105,68 @@ namespace Fitness_core_BE.Repos
|
||||
})
|
||||
.ToListAsync(ct);
|
||||
|
||||
// Inventory: a Sale's revenue, a GRN's cost, and a StockReturn's Amount (staff-
|
||||
// entered -- see StockReturn's doc comment). Every return Category still posts the
|
||||
// same way here (one Expense category, reducing profit) even though CustomerReturn
|
||||
// is the one category that adds the stock back -- the P&L side stays uniform while
|
||||
// the stock-direction side doesn't.
|
||||
var productSales = await _context.Sales
|
||||
.AsNoTracking()
|
||||
.Where(sale =>
|
||||
sale.FKClientId == clientId &&
|
||||
sale.SoldAt >= fromUtc &&
|
||||
sale.SoldAt < toUtc)
|
||||
.Select(sale => new LedgerEntryDto
|
||||
{
|
||||
EntryType = "Income",
|
||||
Category = "Product Sales",
|
||||
Description = sale.SaleNumber,
|
||||
Amount = sale.TotalAmount,
|
||||
EntryDate = sale.SoldAt,
|
||||
IsManual = false
|
||||
})
|
||||
.ToListAsync(ct);
|
||||
|
||||
var inventoryPurchases = await _context.GoodsReceivedNotes
|
||||
.AsNoTracking()
|
||||
.Where(grn =>
|
||||
grn.FKClientId == clientId &&
|
||||
grn.ReceivedAt >= fromUtc &&
|
||||
grn.ReceivedAt < toUtc)
|
||||
.Select(grn => new LedgerEntryDto
|
||||
{
|
||||
EntryType = "Expense",
|
||||
Category = "Inventory Purchases",
|
||||
Description = grn.GrnNumber,
|
||||
Amount = grn.TotalAmount,
|
||||
EntryDate = grn.ReceivedAt,
|
||||
IsManual = false
|
||||
})
|
||||
.ToListAsync(ct);
|
||||
|
||||
var inventoryWriteOffs = await _context.StockReturns
|
||||
.AsNoTracking()
|
||||
.Where(stockReturn =>
|
||||
stockReturn.FKClientId == clientId &&
|
||||
stockReturn.ReturnedAt >= fromUtc &&
|
||||
stockReturn.ReturnedAt < toUtc)
|
||||
.Select(stockReturn => new LedgerEntryDto
|
||||
{
|
||||
EntryType = "Expense",
|
||||
Category = "Inventory Write-offs",
|
||||
Description = stockReturn.Category + " — " + stockReturn.Product!.Name,
|
||||
Amount = stockReturn.Amount,
|
||||
EntryDate = stockReturn.ReturnedAt,
|
||||
IsManual = false
|
||||
})
|
||||
.ToListAsync(ct);
|
||||
|
||||
memberPayments.AddRange(platformInvoices);
|
||||
memberPayments.AddRange(staffPayments);
|
||||
memberPayments.AddRange(bookingPayments);
|
||||
memberPayments.AddRange(productSales);
|
||||
memberPayments.AddRange(inventoryPurchases);
|
||||
memberPayments.AddRange(inventoryWriteOffs);
|
||||
return memberPayments;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user