Skip to main content
GET
/
api
/
v1
/
batches
/
product
/
{product_id}
List Batches by Product
curl --request GET \
  --url https://api.example.com/api/v1/batches/product/{product_id}
{
  "[].id": "<string>",
  "[].product_id": "<string>",
  "[].initial_quantity": 123,
  "[].available_quantity": 123,
  "[].unit_cost": 123,
  "[].purchase_date": "<string>",
  "[].expiration_date": {},
  "[].supplier_id": {},
  "[].entry_transaction_ref": {}
}

Overview

Retrieve all batches for a specific product. This endpoint is useful for auditing inventory costs, monitoring expiration dates, and analyzing stock levels.

Authentication

Required Roles: admin, gestor, or consultor
# backend/Product/Adapters/batch_controller.py:51
@router.route('/product/<product_id>', methods=['GET'])
@require_role('admin', 'gestor', 'consultor')
def get_batches_by_product(product_id):
All authenticated users can view batches, including the read-only consultor role.

Path Parameters

product_id
string
required
The UUID of the product to retrieve batches for

Query Parameters

active_only
boolean
default:"true"
Filter results to only show batches with available quantity > 0.
  • true - Returns only active batches with remaining inventory
  • false - Returns all batches including fully depleted ones

Request Examples

Get Active Batches (Default)

GET /api/v1/batches/product/a8b12c3d-4e5f-6789-0abc-def123456789

Get All Batches (Including Depleted)

GET /api/v1/batches/product/a8b12c3d-4e5f-6789-0abc-def123456789?active_only=false

Response

Returns an array of batch objects sorted by FIFO priority (expiration date, then purchase date).
[].id
string
Unique batch identifier
[].product_id
string
Product UUID this batch belongs to
[].initial_quantity
integer
Original quantity received
[].available_quantity
integer
Current quantity remaining. Will be 0 for depleted batches when active_only=false.
[].unit_cost
float
Cost per unit paid for this batch
[].purchase_date
string
ISO 8601 timestamp when batch was received
[].expiration_date
string | null
ISO 8601 expiration date or null
[].supplier_id
string | null
Supplier UUID or null
[].entry_transaction_ref
string | null
Reference to the entry movement transaction

Response Example

[
  {
    "id": "batch-001",
    "product_id": "a8b12c3d-4e5f-6789-0abc-def123456789",
    "initial_quantity": 100,
    "available_quantity": 45,
    "unit_cost": 12.50,
    "purchase_date": "2026-01-15T08:00:00+00:00",
    "expiration_date": "2026-06-15T00:00:00+00:00",
    "supplier_id": "supplier-alpha",
    "entry_transaction_ref": "movement-001"
  },
  {
    "id": "batch-002",
    "product_id": "a8b12c3d-4e5f-6789-0abc-def123456789",
    "initial_quantity": 150,
    "available_quantity": 150,
    "unit_cost": 11.75,
    "purchase_date": "2026-02-20T10:30:00+00:00",
    "expiration_date": "2026-08-20T00:00:00+00:00",
    "supplier_id": "supplier-beta",
    "entry_transaction_ref": "movement-002"
  },
  {
    "id": "batch-003",
    "product_id": "a8b12c3d-4e5f-6789-0abc-def123456789",
    "initial_quantity": 200,
    "available_quantity": 200,
    "unit_cost": 13.00,
    "purchase_date": "2026-03-01T14:15:00+00:00",
    "expiration_date": null,
    "supplier_id": null,
    "entry_transaction_ref": "movement-003"
  }
]

Empty Response

When no batches exist for the product:
[]

Error Responses

401 Unauthorized

Returned when authentication token is missing or invalid.

403 Forbidden

Returned when user does not have any of the required roles (admin, gestor, consultor).

Implementation Details

# backend/Product/Adapters/batch_controller.py:51-72
@router.route('/product/<product_id>', methods=['GET'])
@require_role('admin', 'gestor', 'consultor')
def get_batches_by_product(product_id):
    db = next(get_db())
    repo = MovementRepository(db)
    active_only = request.args.get('active_only', 'true').lower() == 'true'
    
    batches = repo.get_batches_by_product(product_id, active_only)
    result = []
    for b in batches:
        result.append({
            "id": b.id,
            "product_id": b.product_id,
            "initial_quantity": b.initial_quantity,
            "available_quantity": b.available_quantity,
            "unit_cost": b.unit_cost,
            "purchase_date": b.purchase_date.isoformat(),
            "expiration_date": b.expiration_date.isoformat() if b.expiration_date else None,
            "supplier_id": b.supplier_id,
            "entry_transaction_ref": b.entry_transaction_ref
        })
    return jsonify(result), 200

Use Cases

Calculate Average Cost Basis

# Sum total cost across active batches
total_cost = sum(batch['unit_cost'] * batch['available_quantity'] for batch in batches)
total_qty = sum(batch['available_quantity'] for batch in batches)
average_cost = total_cost / total_qty if total_qty > 0 else 0

Monitor Expiring Inventory

from datetime import datetime, timedelta

today = datetime.now()
expiring_soon = [
    batch for batch in batches 
    if batch['expiration_date'] and 
       datetime.fromisoformat(batch['expiration_date']) < today + timedelta(days=30)
]

Supplier Performance Analysis

# Group batches by supplier
from collections import defaultdict

supplier_batches = defaultdict(list)
for batch in batches:
    if batch['supplier_id']:
        supplier_batches[batch['supplier_id']].append(batch)

# Calculate metrics per supplier
for supplier_id, batches in supplier_batches.items():
    total_received = sum(b['initial_quantity'] for b in batches)
    total_remaining = sum(b['available_quantity'] for b in batches)
    turnover_rate = (total_received - total_remaining) / total_received

FIFO Processing Order

Batches are returned and processed in FIFO order based on:
  1. Expiration date (earliest first) - batches with no expiration date are treated as last priority
  2. Purchase date (oldest first) - when expiration dates are equal or both null
# backend/Product/Domain/stock_service.py:68-72
# FIFO sorting logic used internally
max_date = datetime.max.replace(tzinfo=timezone.utc)
batches.sort(key=lambda b: (
    b.expiration_date.replace(tzinfo=timezone.utc) if b.expiration_date else max_date, 
    b.purchase_date.replace(tzinfo=timezone.utc) if b.purchase_date else max_date
))
The batches returned by this endpoint follow the same FIFO order used when processing exits. The first batch in the array will be depleted first during sales.

Build docs developers (and LLMs) love