How to Build a Custom Inventory Forecasting Module for Magento 2

Introduction

In this post I’ll walk you through building a custom Inventory Forecasting module for Magento 2. I’ll keep the tone relaxed — think of this as a chat with a teammate who’s comfortable with PHP and Magento basics but new to forecasting. We’ll cover architecture, data models, integration with Magento Inventory (MSI), prediction approaches (simple statistics and ML), a custom admin dashboard, and real-world use cases (how forecasts reduce stockouts and optimize supplier orders).

Why build a custom module?

Magento 2 doesn’t ship with advanced forecasting. You can find paid extensions, but building your own gives you full control: tailor the model to your catalog, integrate with your procurement workflows, and host everything alongside your store (or call an ML microservice). For stores with specific needs — custom bundles, multi-source inventory, or special reorder rules — a bespoke module beats one-size-fits-all solutions.

High-level design and goals

  • Collect sales history per SKU and source (MSI).
  • Compute forecasts on a schedule (cron) or on-demand.
  • Store forecasts and show them in an admin dashboard with charts.
  • Offer simple statistical methods for small shops and a path to ML service for larger catalogs.
  • Expose APIs so procurement automation or third-party tools can consume forecasts.

Architecture technical: module structure and optimized data models

Let’s map the architecture first. Keep it simple and Magento-friendly:

  • Magento module code that integrates with MSI and sales data.
  • Custom DB tables to store aggregated historical sales and computed forecasts.
  • Cron jobs to aggregate history and compute forecasts.
  • Admin UI (menu, page, API endpoints) for visualization and manual triggers.
  • Optional: an external ML microservice for advanced models.

Module folder layout

app/code/MageFine/InventoryForecast/
├── etc/
│   ├── module.xml
│   ├── di.xml
│   ├── adminhtml/routes.xml
│   ├── crontab.xml
│   └── db_schema.xml
├── registration.php
├── composer.json
├── Cron/
│   └── ForecastRunner.php
├── Model/
│   ├── Aggregator.php
│   ├── ForecastCalculator.php
│   └── ResourceModel/
│       ├── Historical.php
│       └── Forecast.php
├── Api/
│   └── ForecastRepositoryInterface.php
├── Setup/
│   └── InstallData.php (if needed)
├── Controller/Adminhtml/Index/Index.php
├── view/adminhtml/layout/...
└── view/adminhtml/templates/...

Database design (optimized)

You’ll want two small, efficient tables:

  • magefine_inventory_historical: aggregated daily sales per sku and source.
  • magefine_inventory_forecast: forecast records (sku, source, period_start, period_end, predicted_qty, method, confidence).
-- Simplified db_schema.xml snippet (declarative schema)
<table name="magefine_inventory_historical" resource="default" engine="innodb" comment="MageFine inventory historical sales">
  <column xsi:type="int" name="id" nullable="false" unsigned="true" identity="true"/>
  <column xsi:type="varchar" name="sku" nullable="false" length="64"/>
  <column xsi:type="varchar" name="source_code" nullable="true" length="32"/>
  <column xsi:type="date" name="sales_date" nullable="false"/>
  <column xsi:type="int" name="qty_sold" nullable="false" default="0"/>
  <constraint referenceId="PRIMARY"/>
</table>

<table name="magefine_inventory_forecast" resource="default" engine="innodb" comment="MageFine inventory forecasts">
  <column xsi:type="int" name="id" nullable="false" unsigned="true" identity="true"/>
  <column xsi:type="varchar" name="sku" nullable="false" length="64"/>
  <column xsi:type="varchar" name="source_code" nullable="true" length="32"/>
  <column xsi:type="date" name="period_start" nullable="false"/>
  <column xsi:type="date" name="period_end" nullable="false"/>
  <column xsi:type="decimal" name="predicted_qty" nullable="false" scale="2" precision="12"/>
  <column xsi:type="varchar" name="method" nullable="true" length="32"/>
  <column xsi:type="decimal" name="confidence" nullable="true" scale="4" precision="8"/>
</table>

Why aggregated daily rows? Two reasons: the raw orders table can be huge; pre-aggregating simplifies forecasting and reduces memory usage. Aggregating per SKU per source per day gives you the timeseries you need without high-cost JOINs at forecast time.

Data aggregation: pulling historical sales

You’ll want a class that runs daily (cron) that reads sales_order_item (or your analytics table) and fills magefine_inventory_historical. Keep this incremental: only import rows for days not yet aggregated.

// Model/Aggregator.php (pseudo-code)
class Aggregator
{
    protected $connection; // Magento DB

    public function aggregateDay(
        \DateTimeInterface $date
    ) {
        $start = $date->format('Y-m-d 00:00:00');
        $end = $date->format('Y-m-d 23:59:59');

        $sql = "SELECT sku, source_code, SUM(qty_ordered) as sold
                FROM sales_order_item as soi
                JOIN sales_order as so ON so.entity_id = soi.order_id
                WHERE so.created_at BETWEEN :start AND :end
                  AND so.state NOT IN ('canceled')
                GROUP BY sku, source_code";

        $rows = $this->connection->fetchAll($sql, ['start'=>$start, 'end'=>$end]);
        foreach ($rows as $r) {
            // insert or update magefine_inventory_historical
        }
    }
}

Important notes:

  • Use the source_code from MSI (if you use sources). If your store only uses a stock without sources, set source_code = 'default'.
  • Exclude canceled/returned orders or handle returns separately.
  • Consider timezone consistency. Store sales_date in UTC or your store time consistently.

Integration with Magento Inventory (MSI) for real-time context

When showing forecasts and recommendations, combine predicted demand with real-time salable quantities and on-hand stock. For Magento 2 (MSI), use these common APIs:

  • Magento\InventorySalesApi\Api\GetProductSalableQtyInterface — to check salable qty for SKU and stock.
  • Magento\InventoryApi\Api\SourceRepositoryInterface — to get source details.
  • Magento\InventoryApi\Api\GetSourceItemsBySkuInterface — to get per-source on-hand quantities.
// Example usage in a Magento service class (PHP)
use Magento\InventorySalesApi\Api\GetProductSalableQtyInterface;

class InventoryContext
{
    private $getSalableQty;

    public function __construct(GetProductSalableQtyInterface $getSalableQty)
    {
        $this->getSalableQty = $getSalableQty;
    }

    public function getSalableQty(string $sku, int $stockId): float
    {
        return $this->getSalableQty->execute($sku, $stockId);
    }
}

Use stockId mapping if you have multiple websites/stocks. Cross-reference stockId and sources when offering per-source forecasts.

Forecast algorithms: simple statistics vs machine learning

This is where the fun begins. I’ll present two tiers:

  1. Statistical methods (easy to implement, fast): moving average, weighted moving average, exponential smoothing.
  2. Machine learning or time-series models (better accuracy for big catalogs): random forests, XGBoost, Prophet, or deep-learning LSTM. These usually run outside Magento in a service.

1) Simple moving average (SMA)

SMA over N days: predicted demand for next day = average of last N days. Use it as baseline. It’s robust and cheap.

// ForecastCalculator::movingAverage (PHP)
public function movingAverage(array $history, int $window = 7): float
{
    // $history = [ 'YYYY-mm-dd' => qty, ... ] ordered by date ascending
    $last = array_slice($history, -$window);
    $sum = array_sum($last);
    return $sum / max(1, count($last));
}

Pros: easy to explain. Cons: slow to react to trends.

2) Exponential smoothing (single)

Exponential smoothing is just as cheap but responds faster to changes. You choose alpha (0..1).

// single exponential smoothing
public function exponentialSmoothing(array $history, float $alpha = 0.3): float
{
    // assume $history sorted ascending
    $s = null;
    foreach ($history as $v) {
        if ($s === null) { $s = $v; continue; }
        $s = $alpha * $v + (1 - $alpha) * $s;
    }
    return $s ?? 0.0;
}

Tip: choose alpha by evaluating historical error, or keep a default like 0.3.

3) Seasonality and windowing

If you have weekly seasonality, use a 7-day moving average or decompose the series. For monthly seasonality, adjust the window. A simple approach is to compute day-of-week averages.

// Example: day-of-week average
function dowAverage(array $history, string $targetDow) {
    // $history keyed by date
    $sum = 0; $count = 0;
    foreach ($history as $date => $qty) {
        if ((new \DateTime($date))->format('w') === $targetDow) {
            $sum += $qty; $count++;
        }
    }
    return $count ? $sum/$count : 0;
}

4) When to use ML models

If you manage hundreds of SKUs with complex patterns, ML can help. Typical stack:

  • Feature engineering: recent sales (lags), moving averages, promotions flag, price, day-of-week, holiday flag.
  • Model: RandomForest/GradientBoostedTrees for cross-sectional data, Prophet or ARIMA for time series, or LSTM for long sequences.
  • Train offline and serve predictions via HTTP API.

Why externalize ML? Magento PHP layer isn’t ideal for heavy model training. Create a service (Python/Flask/FastAPI) that exposes /predict and /train endpoints. Magento sends training data, requests predictions, and stores results.

# Example Python Flask predict endpoint (very simplified)
from flask import Flask, request, jsonify
import joblib

app = Flask(__name__)
model = joblib.load('rf_model.joblib')

@app.route('/predict', methods=['POST'])
def predict():
    payload = request.json
    features = payload['features']  # matrix
    preds = model.predict(features)
    return jsonify({'preds': preds.tolist()})

if __name__ == '__main__':
    app.run(host='0.0.0.0', port=5000)

From Magento, call this endpoint with cURL/Guzzle, pass precomputed features, and store returned predictions in magefine_inventory_forecast.

Putting it together: Cron to compute forecasts

Design a cron (runs daily, or multiple times a day) that:

  1. Aggregates yesterday’s sales into magefine_inventory_historical (incremental).
  2. For each SKU (and source), fetch last N days and compute forecast(s).
  3. Save forecasts into magefine_inventory_forecast.
  4. Optionally call ML service for heavy predictions.
// Cron/ForecastRunner.php (pseudo)
public function execute()
{
    // 1) aggregate
    $yesterday = (new \DateTime('yesterday'))->format('Y-m-d');
    $this->aggregator->aggregateDay(new \DateTime($yesterday));

    // 2) list SKUs to forecast (you can limit based on activity)
    $skus = $this->skuRepository->getAllSkusForForecast();
    foreach ($skus as $sku) {
        $history = $this->historicalRepo->getLastNDays($sku, 60);
        $predSma = $this->calculator->movingAverage($history, 14);
        $predEs = $this->calculator->exponentialSmoothing($history, 0.25);

        // store predictions for next 7 days
        $this->forecastRepo->saveForecastRange($sku, $predSma, $predEs, 'sma_es', $confidence=0.7);

        // Optionally call ML
        if ($this->useMl) {
            $features = $this->featureBuilder->build($sku, $history);
            $mlPreds = $this->mlClient->predict($features);
            $this->forecastRepo->saveMlPredictions($sku, $mlPreds, 'rf-ml', $confidence=0.9);
        }
    }
}

Custom admin dashboard: visualize forecasts intuitively

Users need a clear view: current stock, salable qty, forecast for next 7/30 days, reorder suggestion, and confidence. Let’s outline UI components:

  • Admin menu: Catalog > Inventory Forecasts
  • Grid: list SKUs, current on-hand (MSI), salable qty, next-7-day predicted demand, reorder suggestion.
  • Detail page: interactive chart (Chart.js) with historical sales and forecast lines, ability to switch method (SMA, ES, ML), and a manual recalculation button.

Admin route and controller

<route id="magefine_inventoryforecast" frontName="inventoryforecast">
  <module name="MageFine_InventoryForecast" />
</route>

// Controller/Adminhtml/Index/Index.php
class Index extends \Magento\Backend\App\Action
{
    public function execute()
    {
        $resultPage = $this->resultFactory->create(\Magento\Framework\Controller\ResultFactory::TYPE_PAGE);
        $resultPage->getConfig()->getTitle()->prepend(__('Inventory Forecasts'));
        return $resultPage;
    }
}

AJAX endpoint for chart data

// Controller/Adminhtml/Ajax/History.php
public function execute()
{
    $sku = $this->getRequest()->getParam('sku');
    $history = $this->historicalRepo->getLastNDays($sku, 90);
    $forecasts = $this->forecastRepo->getForecastsForSku($sku, 30);
    $this->getResponse()->representJson(json_encode(['history'=>$history,'forecasts'=>$forecasts]));
}

Front-end (admin) template snippet using Chart.js

<div id="forecastChart" style="height:400px"></div>
<script>
require(['jquery','Chart'], function($, Chart) {
  $.getJSON('/admin/inventoryforecast/ajax/history', { sku: 'ABC-123' }, function(data){
    const ctx = document.createElement('canvas');
    $('#forecastChart').append(ctx);
    const labels = Object.keys(data.history);
    const hist = Object.values(data.history);
    const x = data.forecasts.map(f => f.predicted_qty);

    new Chart(ctx, {
      type: 'line',
      data: {
        labels: labels.concat(data.forecasts.map(f => f.period_start)),
        datasets: [{label: 'Historical', data: hist, borderColor: 'blue'},
                   {label: 'Forecast', data: new Array(hist.length-1).fill(null).concat(x), borderColor: 'red'}]
      }
    });
  });
});
</script>

Reorder suggestions: simple logic

From forecasts, compute reorder points and suggested order quantities. A simple formula:

  • Reorder Point = Lead Time Demand + Safety Stock
  • Lead Time Demand = forecasted daily demand * lead_time_days
  • Safety Stock = z * stddev(lead time demand) or simply a multiple of average daily demand
// Example: computeReorderPoint
function computeOrderSuggestion($sku, $leadTimeDays, $desiredCoverDays = 14) {
    $dailyForecast = $this->forecastRepo->getAverageDailyForecast($sku, 14); // average of next 14 days
    $leadDemand = $dailyForecast * $leadTimeDays;
    $safety = $dailyForecast * ($desiredCoverDays * 0.1); // 10% of cover days as simple safety
    $reorderPoint = $leadDemand + $safety;

    $onHand = $this->inventoryContext->getOnHand($sku);
    $orderQty = max(0, ceil($reorderPoint - $onHand));
    return ['reorder_point'=>$reorderPoint, 'suggested_order'=>$orderQty];
}

This is intentionally simple. For production, compute safety stock statistically using demand variance and service levels (z-score).

Concrete use cases: how forecasts reduce stockouts and optimize supplier orders

Let me spell out two clear scenarios:

Case 1 — Reducing stockouts

Problem: A SKU frequently runs out because the team only reorders when stock dips below a fixed threshold. With forecasts you:

  • Predict increased demand for the next 14 days (e.g. seasonal spike).
  • Raise reorder point and order more in advance, covering lead time.
  • Proactively alert procurement and optionally create a purchase order suggestion.

Result: fewer missed sales and happier customers. You can measure the improvement by tracking stockout events per month before/after forecasts.

Case 2 — Optimizing supplier orders

Problem: Over-ordering ties up cash and warehouse space.

With forecasting, group SKUs and plan orders with supplier constraints. For example:

  • Forecast aggregated demand over supplier lead time + reorder cycle.
  • Round suggested orders to supplier pack sizes or MOQ.
  • Create grouped purchase suggestions to reduce freight costs.

Result: reduced inventory carrying costs and better supplier negotiation leverage.

Testing and validating your forecasts

Don’t deploy blindly. Validate performance using backtesting:

  • Hold out the last N days as test set.
  • Compute forecasts using only data prior to the test set.
  • Measure error: MAE, RMSE, MAPE.
// simple MAE computation (PHP)
function mae(array $actual, array $predicted) {
    $n = count($actual);
    $sum = 0;
    for ($i=0;$i<$n;$i++) { $sum += abs($actual[$i]-$predicted[$i]); }
    return $sum / max(1,$n);
}

Track errors per SKU and flag SKUs with high error for manual review or to trigger an ML model.

Performance and scaling tips

  • Aggregate history nightly, not in real-time, unless you truly need minute-granular forecasts.
  • Batch predictions and parallelize (multiple cron jobs) if you have thousands of SKUs.
  • Cache forecast results and use TTLs for heavy visualization pages.
  • For ML, train on a dedicated environment and expose predictions through a REST API; don’t attempt heavy training inside Magento.

Security and infra considerations

If you use an ML microservice, secure communication (TLS + API keys). Keep sensitive business logic in trusted environments. If forecasts influence automated purchase orders, build human approval flows to prevent unintended mass orders.

Extending the module

Ideas to expand:

  • Add promotional flags: import promotion calendars to reduce false positives.
  • Incorporate price elasticity (price drops increase demand).
  • Expose GraphQL endpoints for headless dashboards.
  • Integrate with ERP/purchasing modules to auto-create POs.

Example: Full quick walkthrough — from zero to a working minimal prototype

Below is a condensed step-by-step plan you can implement in a weekend to validate the idea on a small catalog (10-50 SKUs):

  1. Create the module scaffolding: registration.php, module.xml, composer.json.
  2. Add db_schema.xml with the two tables shown earlier and run bin/magento setup:upgrade.
  3. Implement Aggregator that aggregates last 90 days into magefine_inventory_historical. - Can be run via bin/magento cron:run manual execution first.
  4. Implement ForecastCalculator with movingAverage and exponentialSmoothing methods and a ForecastRunner cron that computes a 7-day forecast per SKU and stores into magefine_inventory_forecast.
  5. Implement a basic admin page showing a grid of SKUs with predicted 7-day demand and current salable quantity (use GetProductSalableQtyInterface).
  6. Add an AJAX detail view with Chart.js to show historical vs forecasted values.
  7. Test with a few SKUs, measure MAE by withholding last 7 days from aggregator and comparing forecasts.

If the quick prototype shows promise, replace movingAverage with a call to an ML service for improved accuracy.

Code snippets recap (most important pieces)

Registration and module.xml (boilerplate but required):

// registration.php
\Magento\Framework\Component\ComponentRegistrar::register(
    \Magento\Framework\Component\ComponentRegistrar::MODULE,
    'MageFine_InventoryForecast',
    __DIR__
);

// etc/module.xml
<module name="MageFine_InventoryForecast" setup_version="1.0.0" />

DI configuration: inject MSI interfaces and your repos via di.xml. Use repositories for historical and forecast tables.

Key interfaces to use:

  • Magento\InventorySalesApi\Api\GetProductSalableQtyInterface
  • Magento\InventoryApi\Api\GetSourceItemsBySkuInterface
  • Magento\Sales\Model\ResourceModel\Order\CollectionFactory (for aggregation if you prefer collections)

Monitoring and measurement

Track these KPIs:

  • Stockout events per SKU per month
  • Forecast accuracy (MAE or MAPE)
  • Inventory turnover and carrying cost
  • Purchasing frequency and order consolidation rate

Start collecting these metrics before enabling auto-ordering so you can quantify improvements.

Common pitfalls and how to avoid them

  • Overfitting ML models: validate on holdout data and prefer simpler models if data is noisy.
  • Poor data quality: ensure orders, returns, and canceled orders are handled correctly.
  • Ignoring lead times: always factor supplier lead time into suggestions.
  • Scaling mistakes: don’t attempt to compute forecasts for every SKU every hour if you have tens of thousands — sample, prioritize, and batch.

Conclusion and next steps

Building a custom Inventory Forecasting module for Magento 2 is very doable. Start small with statistical models and clear KPIs. Once the system proves value (fewer stockouts, optimized ordering), move to an ML service for more advanced prediction. The architecture we covered balances Magento-native integration (MSI, admin UI) with the flexibility to call specialized ML services when needed.

If you need a reference implementation or a starting point, I can sketch specific files (fully fleshed PHP classes, service wiring, or a tiny Flask ML server) for your environment — tell me whether you prefer a single-server PHP-only approach or a Magento+Python microservice architecture and I’ll provide code you can drop into your repo.

Happy forecasting — and let me know which part you want to build first (aggregator, calculator, cron, or dashboard) and I’ll help you get the exact files ready.