FillUpOMatic
Stock replenishment report generator
Overview
A WPF application that generates stock replenishment reports by analyzing sales data and comparing it against current stock levels across multiple warehouse locations.
- Fetches sales data from the previous week via REST API
- Checks stock levels across Main, Central, RemoteA, and RemoteB locations
- Identifies products that need replenishing based on configurable thresholds
- Prints formatted replenishment lists for warehouse staff
- Modern WPF using MVVM pattern with CommunityToolkit.Mvvm
- MaterialDesign UI components for a polished look
Key Features
Batched API Requests
Large datasets are processed in batches to avoid API timeouts and improve performance. Uses a generic extension method that chunks items and processes them in parallel batches.
public static async Task<List<TOut>> FetchInBatches<TIn, TOut>(
this IEnumerable<TIn> items,
int batchSize,
Func<List<TIn>, Task<List<TOut>>> fetchFunc)
{
var results = new List<TOut>();
foreach (var batch in items.Chunk(batchSize))
results.AddRange(await fetchFunc(batch.ToList()));
return results;
}
// Usage with API calls
var stockInfos = await primaries.FetchInBatches(BatchSize, async batch =>
{
var request = new GetBranchStockRequest { Sku = string.Join(",", batch) };
var response = await client.GetBranchStockQtysAsync(request);
return response
.Where(r => r.BranchStock != null)
.Select(r => StockInformation.FromBranchStocks(...))
.ToList();
});
Multi-Branch Stock Analysis
Checks stock levels across four different locations (Main, Central, RemoteA, RemoteB) and calculates whether replenishment is needed based on aggregate stock levels.
public class StockInformation
{
private const int MainId = 2;
private const int CentralId = 1;
private const int RemoteAId = 4;
private const int RemoteBId = 8;
public bool NeedsReplenishment => MainStock == 0 && TotalStock > 0;
public bool IsAtRemoteA => RemoteAStock > 0;
public bool IsInRemoteB => RemoteBStock > 0;
public bool IsNegative => MainStock < 0 || CentralStock < 0;
public int MainStock => GetStockByBranch(BranchStocks, MainId);
public int CentralStock => GetStockByBranch(BranchStocks, CentralId);
public int RemoteAStock => GetStockByBranch(BranchStocks, RemoteAId);
public int RemoteBStock => GetStockByBranch(BranchStocks, RemoteBId);
public int TotalStock => MainStock + CentralStock + RemoteAStock + RemoteBStock;
public static StockInformation FromBranchStocks(string sku, List<BranchStock> stocks, ...)
{
// Map API response to stock information per branch
}
}
Dynamic Print Report Generation
Generates professional landscape A4 printouts with formatted tables showing product details, locations, and special notes for warehouse staff.
public void Print()
{
var printDialog = new PrintDialog();
printDialog.PrintTicket.PageOrientation = PageOrientation.Landscape;
printDialog.PrintTicket.PageMediaSize = new PageMediaSize(PageMediaSizeName.ISOA4);
var flowDocument = new FlowDocument
{
PageWidth = printDialog.PrintableAreaWidth,
PageHeight = printDialog.PrintableAreaHeight
};
var table = new Table { CellSpacing = 0 };
var propertyMap = new[]
{
("Brand", "Brand"),
("Type", "Product Type"),
("Name", "Description"),
("Colour", "Stored With"),
("Colourname", "Colour On Box"),
("Size", "Size"),
("Model", "Model"),
("Notes", "Notes")
};
foreach (var product in ProductsToReplenish)
{
var row = new TableRow();
foreach (var (propName, _) in propertyMap)
{
var value = typeof(ProductsToReplenish)
.GetProperty(propName)?.GetValue(product)?.ToString() ?? "";
row.Cells.Add(new TableCell(new Paragraph(new Run(value))));
}
table.RowGroups[1].Rows.Add(row);
}
printDialog.PrintDocument(flowDocument.DocumentPaginator, "Products to Replenish");
}
Paginated Sales Data Fetching
Handles large sales datasets by using cursor-based pagination with LastUid tracking, fetching all sales from the previous week in manageable chunks.
private async Task<List<string>> GetAllSalesFormattedPrimariesAsync()
{
var allSales = new List<SalesOrder>();
int? lastUid = null;
var (fromDate, toDate) = StringHelpers.GetPreviousWeekRange();
while (true)
{
var request = new GetSalesRequest { LastUid = lastUid, FromDate = fromDate, ToDate = toDate };
var salesOrders = await client.GetSalesOrdersAsync(request);
if (salesOrders.Count == 0) break;
allSales.AddRange(salesOrders);
lastUid = salesOrders.Last().SalesLines!.LastOrDefault()?.Uid;
}
return allSales
.Where(s => s.SaleBranch == StoreId)
.SelectMany(s => s.SalesLines!)
.Where(sl => sl.FormattedPrimary != "Unknown Format")
.Select(sl => sl.FormattedPrimary)
.ToList();
}
Challenges & Solutions
- Large dataset handling: Implemented batch processing to handle thousands of sales records without overwhelming the API or causing timeouts
- Caching product details: Fetches product details in batches to avoid redundant API calls for the same SKU
- Duplicate elimination: Groups products by Uid and Size to avoid listing the same product multiple times when variants exist
- Print formatting: Used WPF FlowDocument with Table for professional-looking landscape A4 printouts with proper column proportions