Skip to content

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:

FieldTypeDescription
reference_typeenumAlways opening for opening balance receipts
statusenumAlways approved after creation (auto-approved)
receipt_numberstring(30)Auto-generated via Sequence system (e.g., GRN-000001)
warehouse_idbigintFK to warehouses — the target warehouse
datedateThe opening balance date
total_quantitydecimal(15,3)Sum of item quantities
total_costdecimal(15,3)Sum of item total costs

Opening Balance Item

FieldTypeDescription
product_idbigintFK to products
product_variant_idbigint?FK to product_variants
unit_idbigintFK to units
quantitydecimal(15,3)Opening quantity (must be > 0)
unit_costdecimal(15,3)Cost per unit (must be >= 0)
total_costdecimal(15,3)quantity x unit_cost (auto-calculated)
batch_numberstring?Batch/lot number
expiry_datedate?Expiry date
serial_numbersarray?Array of serial number strings
notestext?Item-level notes

Relationships

API Endpoints

MethodPathDescriptionPermission
GET/api/inventory/opening-balancesList opening balance receipts (paginated)inventory.opening.view
GET/api/inventory/opening-balances/{id}Get opening balance with itemsinventory.opening.view
POST/api/inventory/opening-balancesCreate single-warehouse opening balanceinventory.opening.create
POST/api/inventory/opening-balances/bulkBulk create across multiple warehousesinventory.opening.create
POST/api/inventory/opening-balances/{id}/cancelCancel and reverse stockinventory.opening.cancel

Query Parameters (List)

ParameterTypeDescription
pageintegerPage number (default: 1)
warehouse_idintegerFilter by warehouse
statusstringFilter by status (approved, cancelled)
date_fromdateFilter from this date
date_todateFilter up to this date
searchstringSearch by receipt number

Request/Response Examples

Create Opening Balance

Request POST /api/inventory/opening-balances

bash
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"]
      }
    ]
  }'
dart
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

json
{
  "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.

bash
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"
      }
    ]
  }'
dart
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

json
{
  "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

json
{
  "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

json
{
  "message": "Only approved receipts can be cancelled."
}

Validation Rules

Store (POST /opening-balances)

FieldRules
warehouse_idrequired, must exist in warehouses
daterequired, valid date
notesnullable, string, max 1000
notes_arnullable, string, max 1000
itemsrequired, array, min 1 item
items.*.product_idrequired, must exist in products
items.*.product_variant_idnullable, must exist in product_variants
items.*.unit_idrequired, must exist in units
items.*.quantityrequired, numeric, > 0
items.*.unit_costrequired, numeric, >= 0
items.*.batch_numbernullable, string, max 100
items.*.expiry_datenullable, valid date
items.*.serial_numbersnullable, array of strings (max 100 chars each)
items.*.notesnullable, string, max 500

Bulk (POST /opening-balances/bulk)

FieldRules
daterequired, valid date
entriesrequired, array, min 1 entry
entries.*.warehouse_idrequired, must exist in warehouses
entries.*.product_idrequired, must exist in products
entries.*.product_variant_idnullable, must exist in product_variants
entries.*.unit_idrequired, must exist in units
entries.*.quantityrequired, numeric, > 0
entries.*.unit_costrequired, numeric, >= 0
entries.*.batch_numbernullable, string, max 100
entries.*.expiry_datenullable, valid date
entries.*.serial_numbersnullable, array of strings (max 100 chars each)

Business Rules

  1. Auto-approved — Opening balance receipts are created with status = draft then immediately auto-approved within the same transaction. The API response always shows status = approved.
  2. 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.
  3. Auto-generated number — The receipt_number is assigned via the Sequence system (prefix GRN). Do not pass it in the request.
  4. Items required — At least one item is required per receipt.
  5. Cost required — Unlike regular issues, opening balances require an explicit unit_cost for each item since there is no prior stock to derive cost from.
  6. Total auto-calculatedtotal_cost per item is quantity x unit_cost. Header total_quantity and total_cost are summed from items.
  7. Bulk grouping — Bulk entries are grouped by warehouse_id. Each unique warehouse produces a separate receipt.
  8. All-or-nothing — Both single and bulk creation run inside a database transaction. If any item fails, the entire operation is rolled back.
  9. Cancellation reverses stock — Cancelling an opening balance decreases stock balances and creates reversal movement records.
  10. Only approved can be cancelled — Since opening balances are always auto-approved, cancellation is always available (unless already cancelled).
  11. Batch & serial support — Items optionally support batch_number, expiry_date, and serial_numbers for tracking.
  12. Audit trailcreated_by and approved_by record the user who created the opening balance.

Workflow / State Diagram

Step-by-Step: Opening Balance Flow

Bulk Creation Flow

Moon ERP API Documentation