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
The UUID of the product to retrieve batches for
Query Parameters
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).
Product UUID this batch belongs to
Original quantity received
Current quantity remaining. Will be 0 for depleted batches when active_only=false.
Cost per unit paid for this batch
ISO 8601 timestamp when batch was received
ISO 8601 expiration date or 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)
]
# 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:
- Expiration date (earliest first) - batches with no expiration date are treated as last priority
- 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.