Inter-Warehouse Transfers
Inter-warehouse transfers move stock between warehouses through a 3-step workflow: draft, ship (deducts from source), and receive (adds to destination). Transfers can be cancelled at any stage before receiving — if already shipped, the stock reversal is automatic.
Purpose
- Move stock between warehouses with full audit trail
- Validate sufficient stock before shipping
- Support partial receives (received quantity can differ from shipped)
- Capture unit cost from weighted-average cost at ship time
- Record
transfer_outandtransfer_instock movements - Reverse stock changes automatically on cancellation of shipped transfers
- Generate transfer numbers automatically via the Sequence system (TRF-000001)
Entity Attributes
Inventory Transfer (Header)
| Field | Type | Description |
|---|---|---|
id | bigint | Primary key |
company_id | bigint | FK to companies |
transfer_number | string(30) | Auto-generated unique number (e.g., TRF-000001) |
date | date | Transfer date |
expected_date | date? | Expected delivery date |
from_warehouse_id | bigint | FK to warehouses (source) |
to_warehouse_id | bigint | FK to warehouses (destination, must differ from source) |
status | enum | draft, shipped, received, cancelled |
total_quantity | decimal(15,3) | Sum of requested quantities |
notes | text? | Additional notes |
notes_ar | text? | Arabic notes |
requested_by | bigint? | FK to users |
shipped_by | bigint? | FK to users |
shipped_at | timestamp? | Ship timestamp |
received_by | bigint? | FK to users |
received_at | timestamp? | Receive timestamp |
cancelled_by | bigint? | FK to users |
cancelled_at | timestamp? | Cancellation timestamp |
created_by | bigint? | FK to users |
created_at | timestamp | Creation timestamp |
updated_at | timestamp | Last update timestamp |
deleted_at | timestamp? | Soft-delete timestamp |
Indexes:
UNIQUE (company_id, transfer_number)INDEX (company_id, status)INDEX (company_id, date)
Inventory Transfer Item
| Field | Type | Description |
|---|---|---|
id | bigint | Primary key |
inventory_transfer_id | bigint | FK to inventory_transfers (cascade delete) |
product_id | bigint | FK to products |
product_variant_id | bigint? | FK to product_variants |
unit_id | bigint | FK to units |
requested_quantity | decimal(15,3) | Quantity requested for transfer |
shipped_quantity | decimal(15,3)? | Quantity actually shipped (set on ship) |
received_quantity | decimal(15,3)? | Quantity actually received (set on receive) |
unit_cost | decimal(15,3) | Cost per unit (set from weighted avg on ship) |
batch_number | string(50)? | Batch/lot tracking number |
notes | text? | Item-level notes |
Relationships
API Endpoints
| Method | Endpoint | Description | Permission |
|---|---|---|---|
GET | /api/inventory/transfers | List transfers (paginated) | inventory.transfers.view |
POST | /api/inventory/transfers | Create a draft transfer | inventory.transfers.create |
GET | /api/inventory/transfers/{id} | Get transfer details | inventory.transfers.view |
PUT | /api/inventory/transfers/{id} | Update a draft transfer | inventory.transfers.create |
DELETE | /api/inventory/transfers/{id} | Delete a draft transfer | inventory.transfers.create |
POST | /api/inventory/transfers/{id}/ship | Ship transfer (deduct from source) | inventory.transfers.ship |
POST | /api/inventory/transfers/{id}/receive | Receive transfer (add to destination) | inventory.transfers.receive |
POST | /api/inventory/transfers/{id}/cancel | Cancel transfer | inventory.transfers.cancel |
Query Parameters (Index)
| Parameter | Type | Description |
|---|---|---|
page | integer | Page number |
status | string | Filter by status: draft, shipped, received, cancelled |
from_warehouse_id | integer | Filter by source warehouse |
to_warehouse_id | integer | Filter by destination warehouse |
date_from | string | Filter from date (YYYY-MM-DD) |
date_to | string | Filter to date (YYYY-MM-DD) |
search | string | Search by transfer number |
Examples
Create a Transfer
bash
curl -X POST https://moon-erp.test/api/inventory/transfers \
-H "Authorization: Bearer {token}" \
-H "Content-Type: application/json" \
-d '{
"date": "2026-02-22",
"from_warehouse_id": 1,
"to_warehouse_id": 2,
"notes": "Monthly stock rebalancing",
"items": [
{
"product_id": 1,
"unit_id": 1,
"requested_quantity": 50
},
{
"product_id": 2,
"unit_id": 1,
"requested_quantity": 25
}
]
}'dart
final response = await dio.post('/api/inventory/transfers', data: {
'date': '2026-02-22',
'from_warehouse_id': 1,
'to_warehouse_id': 2,
'notes': 'Monthly stock rebalancing',
'items': [
{'product_id': 1, 'unit_id': 1, 'requested_quantity': 50},
{'product_id': 2, 'unit_id': 1, 'requested_quantity': 25},
],
});Ship a Transfer
bash
curl -X POST https://moon-erp.test/api/inventory/transfers/1/ship \
-H "Authorization: Bearer {token}"dart
final response = await dio.post('/api/inventory/transfers/1/ship');Receive with Partial Quantities
bash
curl -X POST https://moon-erp.test/api/inventory/transfers/1/receive \
-H "Authorization: Bearer {token}" \
-H "Content-Type: application/json" \
-d '{
"received_quantities": {
"1": 48.000,
"2": 25.000
}
}'dart
final response = await dio.post('/api/inventory/transfers/1/receive', data: {
'received_quantities': {'1': 48.0, '2': 25.0},
});Cancel a Shipped Transfer
bash
curl -X POST https://moon-erp.test/api/inventory/transfers/1/cancel \
-H "Authorization: Bearer {token}"dart
final response = await dio.post('/api/inventory/transfers/1/cancel');Business Rules
- Source and destination warehouses must be different — validated on create
- Only draft transfers can be edited or deleted — shipped/received/cancelled are immutable
- Ship validates sufficient stock — unless the source warehouse has
allow_negative_stock = true - Unit cost is captured at ship time from the weighted-average cost in the source warehouse
- Shipped quantity equals requested quantity — always set to the full requested amount on ship
- Partial receives are supported — pass
received_quantitiesmap with item_id => quantity; defaults to shipped_quantity if not specified - Ship creates
transfer_outmovements — stock is deducted from source warehouse - Receive creates
transfer_inmovements — stock is added to destination warehouse - Cancelling a shipped transfer reverses stock — increases source warehouse balance by shipped amounts
- Cancelling a draft transfer has no stock effect — simply marks as cancelled
- Received transfers cannot be cancelled — once stock is received, it cannot be reversed via cancel
- Already cancelled transfers cannot be cancelled again — returns 422