📦

FillUpOMatic

Stock replenishment report generator

.NET WPF MVVM MaterialDesign REST API

Overview

A WPF application that generates stock replenishment reports by analyzing sales data and comparing it against current stock levels across multiple warehouse locations.

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