Add Stock Tracking endpoint to Inventory
Adds GET /client/inventory/stock-movements, a unified read-only ledger flattening every GRN line, sale line and return across all products into one shape for the new Stock Tracking tab's table.
This commit is contained in:
@@ -170,5 +170,12 @@ namespace Fitness_core_BE.Controllers
|
|||||||
notes = request.Notes,
|
notes = request.Notes,
|
||||||
returnedAt = request.ReturnedAt?.ToString("o")
|
returnedAt = request.ReturnedAt?.ToString("o")
|
||||||
}, HttpContext));
|
}, HttpContext));
|
||||||
|
|
||||||
|
[CustomMiddleware]
|
||||||
|
[TenantResolution]
|
||||||
|
[RequiresPermission("inventory")]
|
||||||
|
[HttpGet("stock-movements")]
|
||||||
|
public async Task<ActionResult<ApiResponse<object>>> GetStockMovements()
|
||||||
|
=> Response(await _inventoryService.GetStockMovements(new { }, HttpContext));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -85,6 +85,27 @@ namespace Fitness_core_BE.DTOs.Inventory
|
|||||||
public List<SaleItemDto> Items { get; set; } = [];
|
public List<SaleItemDto> Items { get; set; } = [];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>One row in the Stock Tracking tab's unified ledger -- every GRN line, sale line
|
||||||
|
/// and return across every product, flattened into one shape for a single filterable table.
|
||||||
|
/// Not persisted; built on read from GRNs/Sales/Returns.</summary>
|
||||||
|
public class StockMovementDto
|
||||||
|
{
|
||||||
|
/// <summary>"GRN" | "Sale" | "Return".</summary>
|
||||||
|
public string Type { get; set; } = string.Empty;
|
||||||
|
public Guid ProductId { get; set; }
|
||||||
|
public string ProductName { get; set; } = string.Empty;
|
||||||
|
/// <summary>The GRN/Sale number this line belongs to, or the return's Category label.</summary>
|
||||||
|
public string Reference { get; set; } = string.Empty;
|
||||||
|
/// <summary>Only set for Type == "Return" -- the write-off/return category.</summary>
|
||||||
|
public string? Category { get; set; }
|
||||||
|
/// <summary>Signed: positive for a GRN or a CustomerReturn (stock in), negative for a
|
||||||
|
/// Sale or any other return category (stock out).</summary>
|
||||||
|
public decimal QuantityChange { get; set; }
|
||||||
|
/// <summary>This line's monetary value -- a GRN/Sale line's LineTotal, or a return's Amount.</summary>
|
||||||
|
public decimal Amount { get; set; }
|
||||||
|
public DateTime Date { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
public class StockReturnDto
|
public class StockReturnDto
|
||||||
{
|
{
|
||||||
public Guid StockReturnId { get; set; }
|
public Guid StockReturnId { get; set; }
|
||||||
|
|||||||
@@ -22,5 +22,7 @@ namespace Fitness_core_BE.Interfaces.Services
|
|||||||
Task<object> CreateStockReturn(object payload, HttpContext httpContext);
|
Task<object> CreateStockReturn(object payload, HttpContext httpContext);
|
||||||
Task<object> GetStockReturns(object payload, HttpContext httpContext);
|
Task<object> GetStockReturns(object payload, HttpContext httpContext);
|
||||||
Task<object> GetStockReturnById(object payload, HttpContext httpContext);
|
Task<object> GetStockReturnById(object payload, HttpContext httpContext);
|
||||||
|
|
||||||
|
Task<object> GetStockMovements(object payload, HttpContext httpContext);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -576,6 +576,67 @@ namespace Fitness_core_BE.Services
|
|||||||
return MapToDto(stockReturn);
|
return MapToDto(stockReturn);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --- Stock Tracking (unified ledger, new) ---
|
||||||
|
|
||||||
|
/// <summary>Every GRN line, sale line and return across every product, flattened into one
|
||||||
|
/// shape for the Stock Tracking tab's table. Built from the same three already-tenant-
|
||||||
|
/// scoped reads the GRN/Sales/Returns tabs use -- no new repository queries needed, and
|
||||||
|
/// the dataset is small enough (a single gym's "simple inventory") that filtering happens
|
||||||
|
/// client-side rather than via query params.</summary>
|
||||||
|
public async Task<object> GetStockMovements(object payload, HttpContext httpContext)
|
||||||
|
{
|
||||||
|
var clientId = await _userRoleHelper.GetClientId(DataExtractor.GetUserIdFromContext(httpContext));
|
||||||
|
|
||||||
|
var grns = await _inventoryRepository.GetGrns(clientId);
|
||||||
|
var sales = await _inventoryRepository.GetSales(clientId);
|
||||||
|
var returns = await _inventoryRepository.GetStockReturns(clientId);
|
||||||
|
|
||||||
|
var movements = new List<StockMovementDto>();
|
||||||
|
|
||||||
|
foreach (var g in grns)
|
||||||
|
foreach (var i in g.Items)
|
||||||
|
movements.Add(new StockMovementDto
|
||||||
|
{
|
||||||
|
Type = "GRN",
|
||||||
|
ProductId = i.FKProductId,
|
||||||
|
ProductName = i.Product?.Name ?? string.Empty,
|
||||||
|
Reference = g.GrnNumber,
|
||||||
|
QuantityChange = i.Quantity,
|
||||||
|
Amount = i.LineTotal,
|
||||||
|
Date = g.ReceivedAt
|
||||||
|
});
|
||||||
|
|
||||||
|
foreach (var s in sales)
|
||||||
|
foreach (var i in s.Items)
|
||||||
|
movements.Add(new StockMovementDto
|
||||||
|
{
|
||||||
|
Type = "Sale",
|
||||||
|
ProductId = i.FKProductId,
|
||||||
|
ProductName = i.Product?.Name ?? string.Empty,
|
||||||
|
Reference = s.SaleNumber,
|
||||||
|
QuantityChange = -i.Quantity,
|
||||||
|
Amount = i.LineTotal,
|
||||||
|
Date = s.SoldAt
|
||||||
|
});
|
||||||
|
|
||||||
|
foreach (var r in returns)
|
||||||
|
movements.Add(new StockMovementDto
|
||||||
|
{
|
||||||
|
Type = "Return",
|
||||||
|
ProductId = r.FKProductId,
|
||||||
|
ProductName = r.Product?.Name ?? string.Empty,
|
||||||
|
Reference = r.Sale?.SaleNumber ?? r.Grn?.GrnNumber ?? r.Category,
|
||||||
|
Category = r.Category,
|
||||||
|
// CustomerReturn is the one category that adds back to stock -- see
|
||||||
|
// StockReturn's doc comment.
|
||||||
|
QuantityChange = r.Category == "CustomerReturn" ? r.Quantity : -r.Quantity,
|
||||||
|
Amount = r.Amount,
|
||||||
|
Date = r.ReturnedAt
|
||||||
|
});
|
||||||
|
|
||||||
|
return movements.OrderByDescending(m => m.Date).ToList();
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>Staff optionally picking an existing registered member to attach to a sale
|
/// <summary>Staff optionally picking an existing registered member to attach to a sale
|
||||||
/// (search-and-pick, not free-typed) -- purely for identification, mirrors
|
/// (search-and-pick, not free-typed) -- purely for identification, mirrors
|
||||||
/// BookingService.ResolveOptionalCustomerLinkAsync exactly.</summary>
|
/// BookingService.ResolveOptionalCustomerLinkAsync exactly.</summary>
|
||||||
|
|||||||
Reference in New Issue
Block a user