Logo
Afbeelding

Inventory Aging Analysis (FIFO)

Written by The Audit Analytics | 7 minutes

For many companies, inventory is a material item in the annual audit. Several analyses can be devised for this item. In this article, we focus on how old the current inventory is. This can be relevant for companies working with products with a limited shelf life or those that are highly trend-sensitive.

Inventory aging analysis

Difference Between Turnover and Age

In this article, we discuss the age of the inventory, but in another article, we also covered the inventory turnover analysis. In short, with turnover, you look at how often inventory is converted, whereas with an aging analysis, you look at how old the current inventory is.

Inventory turnover is useful if you want to know how efficiently inventory has been used throughout the year. You look at throughput and turnover speed. This is especially useful for companies with many items and continuous sales.

An aging analysis is useful if you want to know how old the current inventory is on the balance sheet date. You are looking more at the current risks of obsolescence or write-down. This is particularly important for items with a limited shelf life, that are seasonal, or where the industry is trend-sensitive.

In practice, both analyses complement each other: turnover shows the dynamics over time, while age provides a snapshot of the moment. It might well be that the turnover was quite good at the beginning of the year, but the remaining stock on the balance sheet date is already quite old.

What Do We Need?

Alright, let's start with the analysis itself. For this analysis, we will use Python with pandas and the dummy dataset with stock movements:

The dataset contains all movements per product (purchases, sales, corrections, and opening/closing balances).

import pandas as pd
from datetime import datetime

# We load the data directly from the website.
# This makes the code immediately executable.
url = "https://www.theauditanalytics.com/data/movement/dummy_stock_movements.csv"
df = pd.read_csv(url, sep="|")

# Ensure the 'Date' column is of type datetime
df['Date'] = pd.to_datetime(df['Date'])

FIFO Logic

Next, we implement the FIFO logic. After all, we want to know the age of the current inventory. If a product was purchased six months ago but has since been sold, it should not count as old stock. It's only about the inventory layers that are still present on the balance sheet date. To process this correctly, we use a simplified FIFO (First-In, First-Out) approach:

  • Every purchase creates an inventory layer with the receipt date.
  • Every sale first reduces the oldest available layer.
  • On the balance sheet date, only the remaining layers are left. Only these count in the aging analysis.

Step 1: Build the FIFO Layers

We simulate purchases and sales in chronological order and track which quantities are left on the balance sheet date.

# Sort transactions by date to apply FIFO logic correctly
df = df.sort_values(by='Date')

# We use a deque for efficient 'pop' operations from the left (oldest layer)
from collections import deque

# Dictionary to track stock layers per product
stock_layers = {}

# Loop through all transactions
for _, row in df.iterrows():
    item = row['ItemID']
    qty = row['Quantity']
    date = row['Date']
    price = row.get('UnitPrice', 0) # Use .get() in case the price is missing

    # If it's a purchase, add a new layer
    if row['MutationType'] == 'Purchase':
        stock_layers.setdefault(item, deque()).append([qty, date, price])
    
    # If it's a sale, update the oldest layers
    elif row['MutationType'] == 'Sales':
        to_sell = qty
        # Continue as long as there is quantity to sell and stock exists
        while to_sell > 0 and stock_layers.get(item):
            # Get the oldest layer
            layer_qty, layer_date, layer_price = stock_layers[item][0]
            
            # If the layer is completely sold
            if layer_qty <= to_sell:
                to_sell -= layer_qty
                stock_layers[item].popleft() # Remove the oldest layer
            # If the layer is partially sold
            else:
                stock_layers[item][0][0] -= to_sell # Decrease the quantity in the layer
                to_sell = 0

# Collect all remaining layers after processing all transactions
fifo_layers = []
for item, layers in stock_layers.items():
    for layer_qty, layer_date, layer_price in layers:
        fifo_layers.append({
            'ItemID': item,
            'Date': layer_date,
            'Quantity': layer_qty,
            'UnitPrice': layer_price
        })

# Create a DataFrame from the remaining FIFO layers
fifo_df = pd.DataFrame(fifo_layers)

Step 2: Calculate the Age of the Remaining Stock

We only analyze the layers that still exist on the balance sheet date.

# Set the reference date for the age calculation (e.g., balance sheet date)
reference_date = datetime(2024, 12, 31)

# Calculate the age in days for each stock layer
fifo_df['AgeDays'] = (reference_date - fifo_df['Date']).dt.days

# Calculate the value of each stock layer
fifo_df['StockValue'] = fifo_df['Quantity'] * fifo_df['UnitPrice']

Step 3: Classify into Age Categories

# Define the age categories (buckets) in days
bins = [0, 30, 90, 180, 9999]
labels = ["0-30 days", "31-90 days", "91-180 days", ">180 days"]

# Assign each stock layer to an age category
fifo_df['AgeBucket'] = pd.cut(fifo_df['AgeDays'], bins=bins, labels=labels, right=True)

# Group by category and sum the stock value
age_summary = fifo_df.groupby('AgeBucket')['StockValue'].sum().reset_index()

# Display the summary
print(age_summary)

Example result (dummy data):

Age CategoryTotal Value
0-30 days€ 48,200
31-90 days€ 21,400
91-180 days€ 5,100
>180 days€ 12,600

Step 4: Visualization

import matplotlib.pyplot as plt

# Create a bar chart to visualize the results
plt.figure(figsize=(8,5)) # Set the figure size
plt.bar(age_summary['AgeBucket'], age_summary['StockValue'])

# Add titles and labels for clarity
plt.title('Inventory Aging Analysis (FIFO)')
plt.xlabel('Age Category')
plt.ylabel('Total Stock Value (€)')

# Show the plot
plt.show()

Conclusion

With this aging analysis, you can quickly see which products are staying in stock for a long time. Large stocks in the >180 days category may indicate obsolete inventory and a risk of write-down. Hopefully, this helps with your audit process, good luck!

Inventory Aging Analysis (FIFO) - The Audit Analytics