Opening Stock Balances
Opening stock balances allow you to set initial inventory quantities and costs during system setup or go-live. Each opening balance creates a stock receipt with reference_type=opening that is auto-approved immediately, updating stock balances and recording movements in a single step.
Purpose
- Set initial stock quantities and costs per product, variant, and warehouse during system setup
- Support batch tracking, expiry dates, and serial numbers on opening items
- Auto-approve receipts on creation — no manual approval step required
- Record stock movements for full audit trail from day one
- Support bulk creation of opening balances across multiple warehouses
- Reverse stock changes on cancellation
Entity Attributes
Opening balances reuse the Inventory Receipt entity with reference_type = opening. See Stock Receipts for the full entity schema. Key fields specific to opening balances:
| Field | Type | Description |
|---|---|---|
reference_type | enum | Always opening for opening balance receipts |
status | enum | Always approved after creation (auto-approved) |
receipt_number | string(30) | Auto-generated via Sequence system (e.g., GRN-000001) |
warehouse_id | bigint | FK to warehouses — the target warehouse |
date | date | The opening balance date |
total_quantity | decimal(15,3) | Sum of item quantities |
total_cost | decimal(15,3) | Sum of item total costs |
Opening Balance Item
| Field | Type | Description |
|---|---|---|
product_id | bigint | FK to products |
product_variant_id | bigint? | FK to product_variants |
unit_id | bigint | FK to units |
quantity | decimal(15,3) | Opening quantity (must be > 0) |
unit_cost | decimal(15,3) | Cost per unit (must be >= 0) |
total_cost | decimal(15,3) | quantity x unit_cost (auto-calculated) |
batch_number | string? | Batch/lot number |
expiry_date | date? | Expiry date |
serial_numbers | array? | Array of serial number strings |
notes | text? | Item-level notes |
Relationships
API Endpoints
| Method | Path | Description | Permission |
|---|---|---|---|
GET | /api/inventory/opening-balances | List opening balance receipts (paginated) | inventory.opening.view |
GET | /api/inventory/opening-balances/{id} | Get opening balance with items | inventory.opening.view |
POST | /api/inventory/opening-balances | Create single-warehouse opening balance | inventory.opening.create |
POST | /api/inventory/opening-balances/bulk | Bulk create across multiple warehouses | inventory.opening.create |
POST | /api/inventory/opening-balances/{id}/cancel | Cancel and reverse stock | inventory.opening.cancel |
Query Parameters (List)
| Parameter | Type | Description |
|---|---|---|
page | integer | Page number (default: 1) |
warehouse_id | integer | Filter by warehouse |
status | string | Filter by status (approved, cancelled) |
date_from | date | Filter from this date |
date_to | date | Filter up to this date |
search | string | Search by receipt number |
Request/Response Examples
Create Opening Balance
Request POST /api/inventory/opening-balances
curl -X POST https://moon-erp.test/api/inventory/opening-balances \
-H "Authorization: Bearer {token}" \
-H "Content-Type: application/json" \
-H "Accept: application/json" \
-d '{
"warehouse_id": 1,
"date": "2026-01-01",
"notes": "Opening stock for main warehouse",
"notes_ar": "رصيد افتتاحي للمخزن الرئيسي",
"items": [
{
"product_id": 1,
"unit_id": 1,
"quantity": "500.000",
"unit_cost": "12.500",
"batch_number": "BATCH-2026-001",
"expiry_date": "2027-12-31"
},
{
"product_id": 2,
"unit_id": 1,
"quantity": "200.000",
"unit_cost": "45.750",
"serial_numbers": ["SN-001", "SN-002"]
}
]
}'final response = await http.post(
Uri.parse('https://moon-erp.test/api/inventory/opening-balances'),
headers: {
'Authorization': 'Bearer $token',
'Content-Type': 'application/json',
'Accept': 'application/json',
},
body: jsonEncode({
'warehouse_id': 1,
'date': '2026-01-01',
'notes': 'Opening stock for main warehouse',
'notes_ar': 'رصيد افتتاحي للمخزن الرئيسي',
'items': [
{
'product_id': 1,
'unit_id': 1,
'quantity': '500.000',
'unit_cost': '12.500',
'batch_number': 'BATCH-2026-001',
'expiry_date': '2027-12-31',
},
{
'product_id': 2,
'unit_id': 1,
'quantity': '200.000',
'unit_cost': '45.750',
'serial_numbers': ['SN-001', 'SN-002'],
},
],
}),
);Response 201 Created
{
"data": {
"id": 1,
"company_id": 1,
"receipt_number": "GRN-000001",
"date": "2026-01-01",
"warehouse_id": 1,
"partner_id": null,
"reference_type": "opening",
"reference_type_label": "Opening",
"reference_number": null,
"status": "approved",
"status_label": "Approved",
"total_quantity": "700.000",
"total_cost": "15400.000",
"notes": "Opening stock for main warehouse",
"notes_ar": "رصيد افتتاحي للمخزن الرئيسي",
"approved_by": 1,
"approved_at": "2026-01-01T00:00:00.000000Z",
"cancelled_by": null,
"cancelled_at": null,
"created_by": 1,
"warehouse": { "id": 1, "name": "Main Warehouse" },
"items": [
{
"id": 1,
"product_id": 1,
"product_variant_id": null,
"unit_id": 1,
"quantity": "500.000",
"unit_cost": "12.500",
"total_cost": "6250.000",
"batch_number": "BATCH-2026-001",
"expiry_date": "2027-12-31",
"product": { "id": 1, "name": "Widget A" },
"unit": { "id": 1, "name": "Piece" }
},
{
"id": 2,
"product_id": 2,
"product_variant_id": null,
"unit_id": 1,
"quantity": "200.000",
"unit_cost": "45.750",
"total_cost": "9150.000",
"batch_number": null,
"expiry_date": null,
"product": { "id": 2, "name": "Widget B" },
"unit": { "id": 1, "name": "Piece" }
}
],
"created_at": "2026-01-01T00:00:00.000000Z",
"updated_at": "2026-01-01T00:00:00.000000Z"
}
}Bulk Create Opening Balances
Request POST /api/inventory/opening-balances/bulk
Grouping by Warehouse
Entries are automatically grouped by warehouse_id. Each warehouse group creates a separate receipt, all using the same date.
curl -X POST https://moon-erp.test/api/inventory/opening-balances/bulk \
-H "Authorization: Bearer {token}" \
-H "Content-Type: application/json" \
-H "Accept: application/json" \
-d '{
"date": "2026-01-01",
"entries": [
{
"warehouse_id": 1,
"product_id": 1,
"unit_id": 1,
"quantity": "100.000",
"unit_cost": "10.000"
},
{
"warehouse_id": 1,
"product_id": 2,
"unit_id": 1,
"quantity": "50.000",
"unit_cost": "25.500"
},
{
"warehouse_id": 2,
"product_id": 1,
"unit_id": 1,
"quantity": "75.000",
"unit_cost": "10.000"
}
]
}'final response = await http.post(
Uri.parse('https://moon-erp.test/api/inventory/opening-balances/bulk'),
headers: {
'Authorization': 'Bearer $token',
'Content-Type': 'application/json',
'Accept': 'application/json',
},
body: jsonEncode({
'date': '2026-01-01',
'entries': [
{'warehouse_id': 1, 'product_id': 1, 'unit_id': 1, 'quantity': '100.000', 'unit_cost': '10.000'},
{'warehouse_id': 1, 'product_id': 2, 'unit_id': 1, 'quantity': '50.000', 'unit_cost': '25.500'},
{'warehouse_id': 2, 'product_id': 1, 'unit_id': 1, 'quantity': '75.000', 'unit_cost': '10.000'},
],
}),
);Response 201 Created
{
"message": "2 opening balance receipts created successfully.",
"receipts_created": 2,
"items_processed": 3,
"data": [
{
"id": 2,
"receipt_number": "GRN-000002",
"date": "2026-01-01",
"warehouse_id": 1,
"reference_type": "opening",
"status": "approved",
"total_quantity": "150.000",
"total_cost": "2275.000",
"items": [
{ "product_id": 1, "quantity": "100.000", "unit_cost": "10.000", "total_cost": "1000.000" },
{ "product_id": 2, "quantity": "50.000", "unit_cost": "25.500", "total_cost": "1275.000" }
]
},
{
"id": 3,
"receipt_number": "GRN-000003",
"date": "2026-01-01",
"warehouse_id": 2,
"reference_type": "opening",
"status": "approved",
"total_quantity": "75.000",
"total_cost": "750.000",
"items": [
{ "product_id": 1, "quantity": "75.000", "unit_cost": "10.000", "total_cost": "750.000" }
]
}
]
}Cancel Opening Balance
Request POST /api/inventory/opening-balances/1/cancel
Stock Reversal
Cancellation reverses all stock changes: quantities are decreased and values are adjusted. A new stock movement with negative quantity is recorded for each item.
Response 200 OK
{
"data": {
"id": 1,
"receipt_number": "GRN-000001",
"status": "cancelled",
"status_label": "Cancelled",
"cancelled_by": 1,
"cancelled_at": "2026-01-15T09:30:00.000000Z"
}
}Cancel Error — Already Cancelled
Response 422 Unprocessable Entity
{
"message": "Only approved receipts can be cancelled."
}Validation Rules
Store (POST /opening-balances)
| Field | Rules |
|---|---|
warehouse_id | required, must exist in warehouses |
date | required, valid date |
notes | nullable, string, max 1000 |
notes_ar | nullable, string, max 1000 |
items | required, array, min 1 item |
items.*.product_id | required, must exist in products |
items.*.product_variant_id | nullable, must exist in product_variants |
items.*.unit_id | required, must exist in units |
items.*.quantity | required, numeric, > 0 |
items.*.unit_cost | required, numeric, >= 0 |
items.*.batch_number | nullable, string, max 100 |
items.*.expiry_date | nullable, valid date |
items.*.serial_numbers | nullable, array of strings (max 100 chars each) |
items.*.notes | nullable, string, max 500 |
Bulk (POST /opening-balances/bulk)
| Field | Rules |
|---|---|
date | required, valid date |
entries | required, array, min 1 entry |
entries.*.warehouse_id | required, must exist in warehouses |
entries.*.product_id | required, must exist in products |
entries.*.product_variant_id | nullable, must exist in product_variants |
entries.*.unit_id | required, must exist in units |
entries.*.quantity | required, numeric, > 0 |
entries.*.unit_cost | required, numeric, >= 0 |
entries.*.batch_number | nullable, string, max 100 |
entries.*.expiry_date | nullable, valid date |
entries.*.serial_numbers | nullable, array of strings (max 100 chars each) |
Business Rules
- Auto-approved — Opening balance receipts are created with
status = draftthen immediately auto-approved within the same transaction. The API response always showsstatus = approved. - Receipt with reference_type=opening — Opening balances are stored as standard inventory receipts. They appear in the regular receipts list when filtered by
reference_type=opening. - Auto-generated number — The
receipt_numberis assigned via the Sequence system (prefixGRN). Do not pass it in the request. - Items required — At least one item is required per receipt.
- Cost required — Unlike regular issues, opening balances require an explicit
unit_costfor each item since there is no prior stock to derive cost from. - Total auto-calculated —
total_costper item isquantity x unit_cost. Headertotal_quantityandtotal_costare summed from items. - Bulk grouping — Bulk entries are grouped by
warehouse_id. Each unique warehouse produces a separate receipt. - All-or-nothing — Both single and bulk creation run inside a database transaction. If any item fails, the entire operation is rolled back.
- Cancellation reverses stock — Cancelling an opening balance decreases stock balances and creates reversal movement records.
- Only approved can be cancelled — Since opening balances are always auto-approved, cancellation is always available (unless already cancelled).
- Batch & serial support — Items optionally support
batch_number,expiry_date, andserial_numbersfor tracking. - Audit trail —
created_byandapproved_byrecord the user who created the opening balance.