Skip to content

Inventory Reports

Inventory reports provide analytical views of stock data: movement summaries over date ranges, slow-moving/dead stock identification, per-warehouse utilization breakdowns, and batch/serial expiry alerts. All report endpoints are read-only and require the inventory.reports.view permission.

Purpose

  • Track per-product stock movements (receipts, issues, transfers, adjustments) over any date range
  • Identify slow-moving and dead stock that has not moved for a configurable number of days
  • Summarize warehouse utilization with product counts, quantities, and values
  • Alert on upcoming batch/serial expiry dates before products become unusable
  • Support filtering by warehouse, product, and category across all reports

Report Types Overview

ReportEndpointDescription
Movement SummaryGET /reports/movement-summaryOpening/closing quantities with all movement types for a date range
Slow-Moving StockGET /reports/slow-movingProducts with no movement for N days
Warehouse SummaryGET /reports/warehouse-summaryPer-warehouse utilization stats
Expiry ReportGET /reports/expiryBatches/serials expiring within N days

API Endpoints

MethodPathDescriptionPermission
GET/api/inventory/reports/movement-summaryPer-product movement summary for a date rangeinventory.reports.view
GET/api/inventory/reports/slow-movingProducts with no movement for N daysinventory.reports.view
GET/api/inventory/reports/warehouse-summaryPer-warehouse utilization summaryinventory.reports.view
GET/api/inventory/reports/expiryItems expiring within N daysinventory.reports.view

Movement Summary

Returns per-product movement totals for a date range, including opening quantity (calculated from all movements before the start date), each movement type during the period, and the resulting closing quantity.

Query Parameters

ParameterTypeRequiredDescription
date_fromdateYesStart date (Y-m-d)
date_todateYesEnd date (Y-m-d), must be >= date_from
warehouse_idintegerNoFilter by warehouse
product_idintegerNoFilter by single product
category_idintegerNoFilter by product category

Request/Response Example

Request GET /api/inventory/reports/movement-summary?date_from=2026-01-01&date_to=2026-01-31&warehouse_id=1

bash
curl -X GET "https://moon-erp.test/api/inventory/reports/movement-summary?date_from=2026-01-01&date_to=2026-01-31&warehouse_id=1" \
  -H "Authorization: Bearer {token}" \
  -H "Accept: application/json"
dart
final response = await http.get(
  Uri.parse('https://moon-erp.test/api/inventory/reports/movement-summary?date_from=2026-01-01&date_to=2026-01-31&warehouse_id=1'),
  headers: {
    'Authorization': 'Bearer $token',
    'Accept': 'application/json',
  },
);

Response 200 OK

json
{
  "data": {
    "date_from": "2026-01-01",
    "date_to": "2026-01-31",
    "items": [
      {
        "product_id": 1,
        "product_name": "Widget A",
        "product_code": "PROD-001",
        "opening_qty": 500.000,
        "receipts": 200.000,
        "issues": 150.000,
        "transfers_in": 50.000,
        "transfers_out": 30.000,
        "adjustments_in": 10.000,
        "adjustments_out": 5.000,
        "closing_qty": 575.000
      },
      {
        "product_id": 2,
        "product_name": "Widget B",
        "product_code": "PROD-002",
        "opening_qty": 100.000,
        "receipts": 0.000,
        "issues": 25.000,
        "transfers_in": 0.000,
        "transfers_out": 0.000,
        "adjustments_in": 0.000,
        "adjustments_out": 0.000,
        "closing_qty": 75.000
      }
    ],
    "summary": {
      "total_products": 2,
      "total_receipts": 200.000,
      "total_issues": 175.000
    }
  }
}

Movement Types Tracked

Movement TypeIn/OutSource
receiptInStock receipts (GRN)
issueOutStock issues (GDN)
transfer_inInIncoming transfers
transfer_outOutOutgoing transfers
adjustmentIn or OutStock adjustments (can be positive or negative)
openingInOpening balance entries

Closing Quantity Formula

closing_qty = opening_qty + receipts + transfers_in + adjustments_in + opening_in
              - issues - transfers_out - adjustments_out

Slow-Moving Stock

Identifies products that have stock on hand but no movement for a configurable number of days. Useful for identifying dead stock and reducing carrying costs.

Query Parameters

ParameterTypeDefaultDescription
days_thresholdinteger90Days without movement to consider slow-moving (minimum: 1)
warehouse_idintegerFilter by warehouse
category_idintegerFilter by product category

Request/Response Example

Request GET /api/inventory/reports/slow-moving?days_threshold=60&warehouse_id=1

bash
curl -X GET "https://moon-erp.test/api/inventory/reports/slow-moving?days_threshold=60&warehouse_id=1" \
  -H "Authorization: Bearer {token}" \
  -H "Accept: application/json"
dart
final response = await http.get(
  Uri.parse('https://moon-erp.test/api/inventory/reports/slow-moving?days_threshold=60&warehouse_id=1'),
  headers: {
    'Authorization': 'Bearer $token',
    'Accept': 'application/json',
  },
);

Response 200 OK

json
{
  "data": {
    "days_threshold": 60,
    "items": [
      {
        "product_id": 8,
        "product_name": "Legacy Connector",
        "product_code": "PROD-008",
        "warehouse_id": 1,
        "warehouse_name": "Main Warehouse",
        "quantity": 45.000,
        "total_value": 562.500,
        "last_movement_date": "2025-10-15",
        "days_since_movement": 131
      },
      {
        "product_id": 15,
        "product_name": "Discontinued Part X",
        "product_code": "PROD-015",
        "warehouse_id": 1,
        "warehouse_name": "Main Warehouse",
        "quantity": 12.000,
        "total_value": 180.000,
        "last_movement_date": null,
        "days_since_movement": null
      }
    ],
    "summary": {
      "total_items": 2,
      "total_quantity": 57.000,
      "total_value": 742.500
    }
  }
}

No Movement Date

When last_movement_date is null and days_since_movement is null, it means the product has stock but no movement records at all — typically from a data migration or manual database entry.


Warehouse Summary

Returns per-warehouse utilization statistics: total distinct products, total quantity, total value, and last activity date. Only warehouses with active stock (quantity > 0) are included.

Query Parameters

This endpoint takes no parameters. It always returns all warehouses with active stock for the authenticated user's company.

Request/Response Example

Request GET /api/inventory/reports/warehouse-summary

bash
curl -X GET "https://moon-erp.test/api/inventory/reports/warehouse-summary" \
  -H "Authorization: Bearer {token}" \
  -H "Accept: application/json"
dart
final response = await http.get(
  Uri.parse('https://moon-erp.test/api/inventory/reports/warehouse-summary'),
  headers: {
    'Authorization': 'Bearer $token',
    'Accept': 'application/json',
  },
);

Response 200 OK

json
{
  "data": {
    "warehouses": [
      {
        "warehouse_id": 1,
        "warehouse_name": "Main Warehouse",
        "warehouse_code": "WH-001",
        "total_products": 85,
        "total_quantity": 12540.000,
        "total_value": 458750.500,
        "last_activity": "2026-02-22"
      },
      {
        "warehouse_id": 2,
        "warehouse_name": "Branch Warehouse",
        "warehouse_code": "WH-002",
        "total_products": 42,
        "total_quantity": 3200.000,
        "total_value": 96400.000,
        "last_activity": "2026-02-20"
      }
    ],
    "summary": {
      "total_warehouses": 2,
      "grand_total_quantity": 15740.000,
      "grand_total_value": 555150.500
    }
  }
}

Last Activity

The last_activity date is derived from the most recent last_receipt_date or last_issue_date on the stock balance records for that warehouse.


Expiry Report

Returns batches and serial numbers that are expiring within a configurable number of days. Uses the product_serials table and filters by status = available to exclude already consumed serials.

Query Parameters

ParameterTypeDefaultDescription
days_aheadinteger30Days ahead to check for expiring items (minimum: 1)
warehouse_idintegerFilter by warehouse
include_expiredbooleanfalseInclude items that are already expired

Request/Response Example

Request GET /api/inventory/reports/expiry?days_ahead=60&include_expired=true

bash
curl -X GET "https://moon-erp.test/api/inventory/reports/expiry?days_ahead=60&include_expired=true" \
  -H "Authorization: Bearer {token}" \
  -H "Accept: application/json"
dart
final response = await http.get(
  Uri.parse('https://moon-erp.test/api/inventory/reports/expiry?days_ahead=60&include_expired=true'),
  headers: {
    'Authorization': 'Bearer $token',
    'Accept': 'application/json',
  },
);

Response 200 OK

json
{
  "data": {
    "days_ahead": 60,
    "items": [
      {
        "id": 42,
        "product_id": 3,
        "product_name": "Antiseptic Solution",
        "product_code": "PROD-003",
        "serial_number": "SN-2025-042",
        "batch_number": "BATCH-2025-A",
        "warehouse_id": 1,
        "expiry_date": "2026-01-15",
        "days_until_expiry": -39,
        "is_expired": true,
        "cost": 8.500
      },
      {
        "id": 78,
        "product_id": 7,
        "product_name": "Thermal Paste",
        "product_code": "PROD-007",
        "serial_number": "SN-2026-078",
        "batch_number": "BATCH-2026-B",
        "warehouse_id": 1,
        "expiry_date": "2026-04-10",
        "days_until_expiry": 46,
        "is_expired": false,
        "cost": 3.250
      }
    ],
    "summary": {
      "total_items": 2,
      "already_expired": 1,
      "expiring_soon": 1,
      "total_value_at_risk": 11.750
    }
  }
}

Negative Days

When days_until_expiry is negative, the item is already expired. These are only included when include_expired=true.


Business Rules

  1. Permission — All four report endpoints require the inventory.reports.view permission.
  2. Company scoping — All queries are scoped to the authenticated user's company_id.
  3. Movement summary validationdate_from and date_to are required. date_to must be on or after date_from.
  4. Opening quantity — Calculated by summing all movements (quantity_in - quantity_out) before the date_from date.
  5. Closing quantityopening_qty + all inflows - all outflows during the period.
  6. Slow-moving threshold — Minimum 1 day. Products with stock but no movement records at all are included (shown with null dates).
  7. Slow-moving scope — Results are per product-warehouse combination, not aggregated across warehouses.
  8. Warehouse summary — Only includes warehouses with positive stock quantities (quantity > 0).
  9. Expiry source — Uses the product_serials table, not receipt items. Only serials with status = available and a non-null expiry_date are checked.
  10. Expiry window — By default, only items expiring in the future (within days_ahead) are shown. Set include_expired = true to also see already-expired items.
  11. Value at risk — The expiry report sums the cost field from product serials to calculate total_value_at_risk.
  12. No pagination — The movement summary, slow-moving, warehouse summary, and expiry reports return all matching results without pagination. For large datasets, use filters to narrow results.

Report Selection Guide

Movement Summary Flow

Expiry Report Flow

Moon ERP API Documentation