---
name: inventory-accounts
description: >
  Default GL-account settings used by inventory / stock-movement journal posting in Moon ERP
  (inventory asset, COGS, purchases inventory/expense/payable/input-tax/discount). Maps every
  setting key (AR+EN label, purpose, definition file:line, where it is consumed when posting a
  journal entry) and whether it is already on the first-run /setup "Default Accounts" page.
  Reference for extending /setup to cover inventory defaults.
updated: 2026-06-18
---

# Inventory Default Accounts

## TL;DR

- The **Inventory module posts NO journal entries** of its own. `Modules/Inventory/app` contains
  **zero** `CreateJournalEntry`/`JournalEntry` references — receipts, issues, transfers, adjustments
  and counts move stock **only** (verified). The GL impact of inventory movement is posted by the
  **consuming module** at the document level: Purchases (bill / return / landed cost), Sales (invoice
  COGS / return), and Production (material issue → WIP reclass).
- The inventory-asset GL account is therefore resolved by those modules through a shared
  **fallback chain**: `sales.inventory_account_id` → `purchases.inventory_account_id` →
  `inventory.stock_account` (account **code**, not id) → lazily-created child under Current Assets `11`.
- Two different value formats coexist (a real gotcha): the `*_account_id` keys store a **numeric GL
  account id**; `inventory.stock_account` / `inventory.cogs_account` store an **account code string**
  (e.g. `'1301'`, `'4101'`).
- **None** of these inventory-related account settings are on the `/setup` Default-Accounts page
  today. That page only writes 7 `accounting.default_*` keys (cash, bank, client, supplier, expense,
  revenue, check) — see [§ /setup page coverage](#setup-page-coverage).

All definitions live in
`/home/moonui/moon-erp-be/Modules/Core/database/seeders/SettingDefinitionSeeder.php`.

---

## 1. The default-account settings (definitions)

### 1a. Dedicated `inventory.*` accounts (store an account CODE string)

| Key | EN label | AR label | Type / default | Purpose | Def file:line |
|-----|----------|----------|----------------|---------|---------------|
| `inventory.stock_account` | Stock Account Code | رمز حساب المخزون | string / `'1301'` | Default inventory/stock GL **account code** — last code-based fallback for the inventory-asset account when no `*_account_id` is set. | `SettingDefinitionSeeder.php:809` |
| `inventory.cogs_account` | COGS Account Code | رمز حساب تكلفة البضاعة المباعة | string / `'4101'` | Default cost-of-goods-sold GL **account code**. (Defined as an inventory default; COGS posting in practice flows through `sales.cogs_account_id` — see note below.) | `SettingDefinitionSeeder.php:825` |

> The other `inventory.*` settings in that block are **not** GL accounts: `inventory.valuation_method`
> (777), `inventory.allow_negative` (793), `inventory.auto_approve_receipts` (842),
> `inventory.auto_approve_issues` (858), `inventory.require_batch_tracking` (874),
> `inventory.default_warehouse_id` (890). Out of scope for default-accounts, listed here so a future
> editor does not mistake them.

### 1b. Purchases-side accounts that drive inventory-asset & related postings (store an account ID)

These live under `module/display_group = purchases` but they are the keys that actually carry the
**inventory asset** account used when stock is received (purchase bill) — they are the primary
inventory default accounts in code.

| Key | EN label | AR label | Type / default | Purpose | Def file:line |
|-----|----------|----------|----------------|---------|---------------|
| `purchases.inventory_account_id` | Inventory GL Account | حساب المخزون | integer / `null` | Inventory **asset** GL account debited when inventory products are received on a purchase bill; also the shared inventory-asset account credited by Sales COGS and Production material-issue. | `SettingDefinitionSeeder.php:1084` |
| `purchases.expense_account_id` | Expense GL Account | حساب المصروفات | integer / `null` | Default expense GL account debited for non-inventory / service lines on a purchase bill (fallback target when `inventory_account_id` is unset). | `SettingDefinitionSeeder.php:1100` |
| `purchases.payable_account_id` | Accounts Payable GL Account | حساب الدائنين | integer / `null` | AP (creditors) GL account credited by purchase bills (unless supplier AP sub-account is used). | `SettingDefinitionSeeder.php:1116` |
| `purchases.tax_receivable_account_id` | Input Tax GL Account | حساب ضريبة المدخلات | integer / `null` | Input VAT (tax receivable) GL account debited on purchase bills. | `SettingDefinitionSeeder.php:1132` |
| `purchases.discount_account_id` | Purchase Discount GL Account | حساب خصم المشتريات | integer / `null` | Purchase-discount GL account used in purchase bill / return journal entries. | `SettingDefinitionSeeder.php:1148` |

> Toggle `purchases.use_supplier_ap_account` (`SettingDefinitionSeeder.php:1164`, boolean) changes
> whether bill/payment entries credit the supplier's AP **sub-account** instead of
> `purchases.payable_account_id`. Not an account-id setting, but affects which payable account is used.

### 1c. Sales-side inventory account (store an account ID) — first link in the fallback chain

| Key | EN label | AR label | Type / default | Purpose | Def file:line |
|-----|----------|----------|----------------|---------|---------------|
| `sales.inventory_account_id` | (Sales) Inventory GL Account | حساب المخزون | integer / `null` | Inventory-asset GL account **credited** when Sales posts COGS / auto stock-issue; first key in the inventory-asset fallback chain. | `SettingDefinitionSeeder.php:711` |

---

## 2. Auto-seed defaults (code → id resolution at install)

`SettingDefinitionSeeder::seedDefaultAccountSettings(int $companyId)` pre-fills some of these by
resolving a hardcoded **account code → id** map after the chart of accounts is seeded
(`SettingDefinitionSeeder.php:2111`-2123):

| Setting key auto-seeded | Resolves to account code |
|-------------------------|--------------------------|
| `sales.inventory_account_id` | `1105` |
| `purchases.inventory_account_id` | `1105` |
| `sales.revenue_account_id` | `4101` |
| `sales.cogs_account_id` | `5101` |
| `sales.receivable_account_id` | `1103` |
| `sales.discount_account_id` | `4105` |
| `purchases.payable_account_id` | `2101` |

Note the **drift**: the auto-seed resolves the inventory account to code `1105`, while the
`inventory.stock_account` definition default is the code string `'1301'`. On a freshly seeded company
the `*_account_id` keys win (fallback chain checks them first), so `1105` is what posts; `1301` only
matters if the id keys are blank. Worth surfacing when designing the /setup UI.

---

## 3. Where these accounts are CONSUMED when posting

The Inventory module never posts GL. Consumption happens in the modules below.

### 3a. Shared inventory-asset fallback chain

The canonical resolution order (documented in
`Modules/Production/app/Listeners/PostMaterialIssueJournal.php:27`-31 and implemented identically in
Sales) is:

```
sales.inventory_account_id (id)
  → purchases.inventory_account_id (id)
    → inventory.stock_account (CODE → looked up to id via Account::where('code', …))
      → lazily-created child detail account under Current Assets (parent code '11')
```

| Consumer (file:line) | What it posts | Accounts used |
|----------------------|---------------|---------------|
| `Modules/Production/app/Listeners/PostMaterialIssueJournal.php:117`-154 (`resolveInventoryAccount`) | DR WIP / **CR inventory asset** on an approved material issue. Full fallback chain; on miss, auto-creates under `11` and **persists** the result back into `purchases.inventory_account_id` (`:151`). | `sales.inventory_account_id` (`:35,119`), `purchases.inventory_account_id` (`:37,120,151`), `inventory.stock_account` (`:39,126`) |
| `Modules/Sales/app/Actions/PostSalesInvoice.php:300`-325 (`resolveInventoryAccount`) | Inventory-asset account **credited** for COGS / auto stock-issue on a sales invoice. | `sales.inventory_account_id` (`:300`), `purchases.inventory_account_id` (`:307`), `inventory.stock_account` code (`:314`) |
| `Modules/Sales/app/Actions/PostSalesReturn.php:313`-314 | Same fallback for sales returns. | `inventory.stock_account` (and the two id keys above) |

### 3b. Purchases postings (the keys' primary use)

| Consumer (file:line) | What it posts | Accounts used |
|----------------------|---------------|---------------|
| `Modules/Purchases/app/Actions/PostPurchaseBill.php:63`-130 (`createJournalEntry`) | Purchase bill JE: **DR inventory** (track_inventory lines), DR expense (service lines, falls back to inventory acct), DR input tax, CR/DR discount, CR payable. Throws `missing_accounting_settings` if neither inventory nor expense is set, or payable is missing. | `purchases.inventory_account_id` (`:65,89,97`), `purchases.expense_account_id` (`:66,108`), `purchases.payable_account_id` (`:182` via `resolvePayableAccount`), `purchases.tax_receivable_account_id` (`:68,121`), `purchases.discount_account_id` (`:69`) |
| `Modules/Purchases/app/Actions/PostPurchaseReturn.php:57`-60 | Purchase-return JE (reverse of bill). | same five purchases keys + payable (`:173`) |
| `Modules/Purchases/app/Actions/PostPurchasePayment.php:103` | Supplier payment JE — debit payable. | `purchases.payable_account_id` |
| `Modules/Purchases/app/Actions/PostLandedCostVoucher.php:50,52` | Capitalises landed cost into inventory / expense. | `purchases.inventory_account_id` (SETTING_INVENTORY), `purchases.expense_account_id` (SETTING_EXPENSE) |
| `Modules/Production/app/Actions/SettleBorrow.php:127,275`-276 | Consignment borrow settlement JE. | `purchases.inventory_account_id`, `purchases.expense_account_id` |

### 3c. Inventory module — stock-only (no GL)

| File:line | Behaviour |
|-----------|-----------|
| `Modules/Inventory/app/Services/StockService.php:240` | Reads only `inventory.valuation_method` (not a GL account) for costing. |
| `Modules/Inventory/app/Services/InventoryCostService.php:204`-206 | Reads EOQ params (`inventory.carrying_cost_percent`, `ordering_cost`, `stockout_cost`) — not GL accounts. |
| `Modules/Inventory/app/Http/Controllers/CostingController.php:213,224` | Gets/sets `inventory.valuation_method`. |
| `Modules/Inventory/app/Actions/ApproveAdjustment.php`, `CancelAdjustment.php`, `FinalizeCount.php` | Move stock only — **no journal entry, no account setting** (verified: no `CreateJournalEntry`/account reads). |

> Implication for "all default accounts": stock adjustments and counts currently have **no**
> dedicated gain/loss GL account setting at all — if the owner wants adjustments to post GL later,
> that account does not yet exist as a setting.

---

## 4. /setup page coverage {#setup-page-coverage}

FE: `/home/moonui/public_html/moon-erp/src/app/features/setup/setup-wizard.component.ts`
(Step 5 = "Default Accounts", steps array `:86`; save logic `saveDefaults()` `:674`-739).

The page writes exactly **7** keys, all in the `accounting.*` namespace (`:678`-712):

| Signal | Setting key written |
|--------|---------------------|
| `defaultCash` | `accounting.default_cash_account` |
| `defaultBank` | `accounting.default_bank_account` |
| `defaultReceivable` (المدينون) | `accounting.default_client_account` |
| `defaultPayable` (الدائنون) | `accounting.default_supplier_account` |
| `defaultExpense` | `accounting.default_expense_account` |
| `defaultRevenue` | `accounting.default_revenue_account` |
| `defaultCheckAccount` | `accounting.default_check_account` |

**None of the inventory-area keys** (`inventory.stock_account`, `inventory.cogs_account`,
`purchases.inventory_account_id`, `purchases.expense_account_id`, `purchases.payable_account_id`,
`purchases.tax_receivable_account_id`, `purchases.discount_account_id`, `sales.inventory_account_id`)
are on the page. So `on_setup_page = false` for every inventory account below.

> Note: the page's "Payable الدائنون" writes `accounting.default_supplier_account`, which is a
> **different key** from `purchases.payable_account_id` consumed by purchase postings — they are not
> linked. Extending /setup for inventory should write the `purchases.*` / `inventory.*` keys above,
> not the `accounting.default_*` ones.

---

## 5. Gotchas for extending /setup

1. **Two value formats.** `inventory.stock_account` / `inventory.cogs_account` store an account
   **code string**; everything else stores an account **id**. A unified picker must write the right
   format per key (or prefer the `*_account_id` keys and drop the code-based ones from the UI).
2. **Fallback already auto-creates accounts.** If left blank, Production material-issue will
   auto-create an inventory child under `11` and back-fill `purchases.inventory_account_id`. The UI
   can pre-populate from the auto-seed (`1105`) rather than forcing a choice.
3. **`accounting.default_supplier_account` ≠ `purchases.payable_account_id`.** Don't assume the
   existing /setup payable choice already covers purchase AP postings — it does not.
4. **No adjustment/count GL account exists.** If the owner expects inventory adjustments to post a
   gain/loss to GL, a new setting + posting code would be net-new work.
