---
name: Sales Default Accounts
description: Every Sales (Modules/Sales) default GL-account setting — keys, AR/EN labels, purpose, definition file:line, where each is consumed when posting journal entries, and whether it is already on the FE /setup Default-Accounts page. Reference for extending /setup to cover Sales.
updated: 2026-06-18
---

# Sales Default Accounts

Scope: the **Sales** module's default-account settings — settings whose stored value is a
GL `accounts.id` that the system uses automatically when posting journal entries for sales
invoices, sales payments, and sales returns.

All six account settings (+ the `use_customer_ar_account` toggle that selects *which* AR
account is used) are defined in the Core seeder and consumed by three posting Actions in
`Modules/Sales`.

> **Source of truth for the keys/labels:** `Modules/Core/database/seeders/SettingDefinitionSeeder.php`
> (lines 662-773). The Sales module also has its own `lang/{en,ar}/sales.php` labels and an
> `UpdateSalesSettingsRequest` validator, but those use the **short** field names
> (`revenue_account_id`) while the *stored* setting keys are namespaced (`sales.revenue_account_id`).

---

## Settings table

| Setting key | EN label | AR label | value_type | Purpose | Definition (file:line) |
|---|---|---|---|---|---|
| `sales.revenue_account_id` | Revenue GL Account | حساب الإيرادات | integer | CR Revenue when posting a sales invoice (and DR on a return). **Mandatory** — posting throws if missing. | `SettingDefinitionSeeder.php:663` |
| `sales.cogs_account_id` | COGS GL Account | حساب تكلفة البضاعة المباعة | integer | DR Cost of Goods Sold in the 2nd (COGS) JE for inventory-tracked items on an invoice; CR on a return. | `SettingDefinitionSeeder.php:679` |
| `sales.receivable_account_id` | Accounts Receivable GL Account | حساب المدينين / الذمم المدينة | integer | DR Accounts Receivable on an invoice; CR on payment; reversed on a return. The global AR account used unless `use_customer_ar_account` is on. **Mandatory.** | `SettingDefinitionSeeder.php:695` |
| `sales.inventory_account_id` | Inventory GL Account | حساب المخزون | integer | CR Inventory (fallback) in the COGS JE — the contra account that COGS is credited against. Falls back to `purchases.inventory_account_id` then `inventory.stock_account` (a code, not id). | `SettingDefinitionSeeder.php:711` |
| `sales.tax_payable_account_id` | Tax Payable GL Account | حساب الضريبة المستحقة | integer | CR Tax Payable on an invoice (only when `tax_amount > 0`); reversed on a return. | `SettingDefinitionSeeder.php:743` |
| `sales.discount_account_id` | Sales Discount GL Account | حساب خصم المبيعات | integer | DR Sales Discount on an invoice when a discount exists AND this account is set (otherwise revenue is booked net of discount); reversed on a return. **Optional** (nullable). | `SettingDefinitionSeeder.php:759` |

### Related toggle (not an account itself — selects which AR account is used)

| Setting key | EN label | AR label | value_type | Purpose | Definition (file:line) |
|---|---|---|---|---|---|
| `sales.use_customer_ar_account` | Use Customer AR Sub-Account | استخدام حساب المدينين الفرعي للعميل | boolean | When true, invoice/payment/return JEs use the customer's own AR sub-account (`customer.accExt.ar_account_id`) instead of `sales.receivable_account_id`; falls back to the global account when the customer has none. | `SettingDefinitionSeeder.php:727` |

All six account defs ship with `default_value => null`, `scope => company`, `display_group => sales`,
`is_visible => true`, display_order 15-21.

---

## Where each is consumed (journal-entry posting)

Three Actions read these settings via `$this->settingsService->get('sales.<key>', $companyId)`.

### 1. `PostSalesInvoice` — `Modules/Sales/app/Actions/PostSalesInvoice.php`

Posts **two** journal entries when an invoice is posted.

**Revenue JE** — `createRevenueJournalEntry()` (line 88):
- `sales.receivable_account_id` resolved via `resolveReceivableAccount()` (line 90 → 410-424) — DR AR for `total` (subtotal + tax). Honors `use_customer_ar_account` (line 412).
- `sales.revenue_account_id` (line 91) — CR Revenue. If a discount exists and `discount_account_id` is set, CR revenue gross (subtotal + discount); otherwise CR net subtotal.
- `sales.discount_account_id` (line 93) — DR Sales Discount when a discount exists (lines 119-135).
- `sales.tax_payable_account_id` (line 92) — CR Tax Payable when `tax_amount > 0` (lines 148-156).
- Guard: throws `missing_accounting_settings` if AR or Revenue is unset (lines 95-97).

**COGS JE** — `handleCogsAndStock()` (line 171):
- `sales.cogs_account_id` (line 173) — DR COGS (line 266). If unset, the entire COGS JE is skipped (line 249).
- `sales.inventory_account_id` (line 300, via `resolveFallbackInventoryAccount()`, lines 297-326) — CR Inventory contra. Fallback chain: `sales.inventory_account_id` → `purchases.inventory_account_id` (line 307) → `inventory.stock_account` resolved by **code** (lines 314-322). Per-warehouse inventory accounts from the stock service take precedence; this is the fallback to balance the JE (lines 254-256, 273-282).

### 2. `PostSalesPayment` — `Modules/Sales/app/Actions/PostSalesPayment.php`

Posts one JE when a customer payment is posted (line 30 onward):
- DR `$payment->receiving_account_id` (cash/bank chosen per-payment, **not** a default setting) (lines 40-46).
- CR `sales.receivable_account_id` via `resolveReceivableAccount()` (line 31 → 90-104). Honors `use_customer_ar_account` (line 92). Throws `missing_accounting_settings` if AR unresolved (lines 33-35).

### 3. `PostSalesReturn` — `Modules/Sales/app/Actions/PostSalesReturn.php`

Mirrors the invoice JE in reverse:
- `sales.revenue_account_id` (line 65), `sales.tax_payable_account_id` (line 66), `sales.discount_account_id` (line 67).
- `sales.receivable_account_id` via `resolveReceivableAccount()` (line 155), honoring `use_customer_ar_account` (line 144).
- `sales.cogs_account_id` (line 160) + inventory fallback via `resolveFallbackInventoryAccount()` (line 300, same chain as the invoice: lines 300-307).

---

## API surface (how these are written today)

- **Controller:** `Modules/Sales/app/Http/Controllers/SalesSettingController.php` (documents `revenue_account_id`, `cogs_account_id`, `receivable_account_id`, `tax_payable_account_id`, `discount_account_id` as body params, lines 73-77).
- **Validator:** `Modules/Sales/app/Http/Requests/UpdateSalesSettingsRequest.php:55-59` — each `exists:accounts,id` **and must be a detail (postable) account** via the `$detailAccount` rule (headers are rejected; see `SalesSettingApiTest.php:228-246`). `discount_account_id` is nullable; the rest are `sometimes` integers.
- **Short→namespaced:** the controller/request use short keys (`revenue_account_id`); persisted setting keys are `sales.<key>`. Any /setup extension must write the **namespaced** keys (mirroring the Sales settings page).
- **Module labels:** `Modules/Sales/lang/en/sales.php:39-43` and `Modules/Sales/lang/ar/sales.php:39-43` (note: `inventory_account_id` and `use_customer_ar_account` are not in these lang files; the canonical labels are in the seeder).

---

## Already on the /setup Default-Accounts page?

**No.** None of the `sales.*_account_id` settings are on the FE first-run page.

The /setup page (`src/app/features/setup/setup-wizard.component.ts`, save logic lines 677-712)
only writes the generic **`accounting.default_*`** keys:
`default_cash_account`, `default_bank_account`, `default_client_account`,
`default_supplier_account`, `default_expense_account`, `default_revenue_account`,
`default_check_account`. These are a *different* key namespace from the Sales posting keys —
the Sales Actions read `sales.revenue_account_id`, not `accounting.default_revenue_account`,
so the /setup "Default Revenue" value does **not** feed sales invoice posting.

**Implication for extending /setup:** add controls that write the six namespaced Sales keys
(`sales.revenue_account_id`, `sales.cogs_account_id`, `sales.receivable_account_id`,
`sales.inventory_account_id`, `sales.tax_payable_account_id`, `sales.discount_account_id`).
Enforce **detail/postable** accounts only (the BE validator already rejects headers — and a
non-detail receivable causes the same draft/unpaid trap documented for LIS). `discount_account_id`
is the only optional one. The `use_customer_ar_account` toggle is a boolean, not an account
picker, and is better left on the Sales settings page than the first-run wizard.

---

## Gotchas

- **Two namespaces, not one.** `accounting.default_revenue_account` (on /setup) ≠ `sales.revenue_account_id` (used by posting). Don't assume the /setup wizard already configures sales posting — it doesn't.
- **Detail accounts only.** Pointing any receivable/revenue/etc. setting at a *header* GL account makes the JE line un-postable → invoice silently stays draft/unpaid (same class of bug as the LIS receivable-header trap). The BE validator enforces this; the /setup UI must too.
- **Inventory account is a fallback chain**, partly resolved by **code** (`inventory.stock_account`), not id — `sales.inventory_account_id` is only the first preference.
- **Cash/bank on payments is per-payment** (`receiving_account_id`), not a default-account setting — out of scope for this page.
