Expenses
Expenses provide a simplified entry form for recording company expenditures. Each expense is linked to an expense category, a paying account, and optionally a tax rate, cost center, and business partner. When approved, the system automatically creates a balanced journal entry. Cancellation reverses the journal entry.
Purpose
- Record expense transactions with a streamlined single-entry form
- Auto-calculate tax amounts when a tax rate is assigned
- Auto-generate document numbers via the sequence service
- Create journal entries automatically on approval
- Reverse journal entries on cancellation
- Support filtering and reporting by category, partner, branch, and date range
Entity Attributes
Expense
| Field | Type | Description |
|---|---|---|
id | bigint | Primary key |
company_id | bigint | FK to companies |
branch_id | bigint? | FK to branches |
document_number | string | Auto-generated sequence number (e.g., EXP-2026-0001) |
date | date | Expense date |
expense_category_id | bigint | FK to transaction_categories (type=expense) |
description | string | English description |
description_ar | string? | Arabic description |
amount | decimal(12,3) | Base expense amount (before tax) |
tax_rate_id | bigint? | FK to tax_rates |
tax_amount | decimal(12,3) | Auto-calculated tax amount |
total_amount | decimal(12,3) | amount + tax_amount |
payment_method | enum | cash, check, bank_transfer, card, mixed |
paying_account_id | bigint | FK to accounts (cash or bank GL account used for payment) |
bank_account_id | bigint? | FK to bank_accounts (required for check/bank_transfer) |
check_number | string? | Check number (if payment by check) |
cost_center_id | bigint? | FK to cost_centers |
partner_id | bigint? | FK to business_partners |
status | enum | draft, approved, cancelled |
journal_entry_id | bigint? | FK to journal_entries (set on approval) |
notes | text? | Additional notes |
created_by | bigint | FK to users |
approved_by | bigint? | FK to users |
approved_at | datetime? | Approval timestamp |
created_at | timestamp | Creation timestamp |
updated_at | timestamp | Last update timestamp |
deleted_at | timestamp? | Soft-delete timestamp |
Relationships
Lifecycle
API Endpoints
| Method | Path | Description |
|---|---|---|
GET | /api/accounting/expenses | List expenses (paginated) |
POST | /api/accounting/expenses | Create a new expense (draft) |
GET | /api/accounting/expenses/{id} | Get a single expense |
PUT | /api/accounting/expenses/{id} | Update an expense (draft only) |
DELETE | /api/accounting/expenses/{id} | Soft-delete an expense (draft only) |
POST | /api/accounting/expenses/{id}/approve | Approve and create journal entry |
POST | /api/accounting/expenses/{id}/cancel | Cancel and reverse journal entry |
Query Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
status | string | No | Filter by status: draft, approved, cancelled |
expense_category_id | integer | No | Filter by expense category |
partner_id | integer | No | Filter by business partner |
branch_id | integer | No | Filter by branch |
date_from | date | No | Filter expenses on or after this date |
date_to | date | No | Filter expenses on or before this date |
page | integer | No | Page number (25 per page) |
Request/Response Examples
Create Expense
Request POST /api/accounting/expenses
bash
curl -X POST https://moon-erp.test/api/accounting/expenses \
-H "Authorization: Bearer {token}" \
-H "Content-Type: application/json" \
-H "Accept: application/json" \
-d '{
"date": "2026-02-23",
"expense_category_id": 1,
"description": "Monthly office rent",
"description_ar": "إيجار المكتب الشهري",
"amount": "1500.000",
"payment_method": "bank_transfer",
"paying_account_id": 15,
"bank_account_id": 1,
"tax_rate_id": 1,
"cost_center_id": 2,
"partner_id": 5,
"branch_id": 1,
"notes": "February 2026 rent"
}'dart
final response = await http.post(
Uri.parse('https://moon-erp.test/api/accounting/expenses'),
headers: {
'Authorization': 'Bearer $token',
'Content-Type': 'application/json',
'Accept': 'application/json',
},
body: jsonEncode({
'date': '2026-02-23',
'expense_category_id': 1,
'description': 'Monthly office rent',
'description_ar': 'إيجار المكتب الشهري',
'amount': '1500.000',
'payment_method': 'bank_transfer',
'paying_account_id': 15,
'bank_account_id': 1,
'tax_rate_id': 1,
'cost_center_id': 2,
'partner_id': 5,
'branch_id': 1,
'notes': 'February 2026 rent',
}),
);Response 201 Created
json
{
"data": {
"id": 1,
"company_id": 1,
"branch_id": 1,
"document_number": "EXP-2026-0001",
"date": "2026-02-23",
"expense_category_id": 1,
"expense_category": {
"id": 1,
"code": "EXP-RENT",
"name": "Office Rent",
"name_ar": "إيجار المكتب"
},
"description": "Monthly office rent",
"description_ar": "إيجار المكتب الشهري",
"amount": "1500.000",
"tax_rate_id": 1,
"tax_rate": {
"id": 1,
"name": "VAT 5%",
"rate": "5.0000"
},
"tax_amount": "75.000",
"total_amount": "1575.000",
"payment_method": "bank_transfer",
"payment_method_label": "Bank Transfer",
"paying_account_id": 15,
"paying_account": {
"id": 15,
"code": "1201001",
"name": "NBK Main Account"
},
"bank_account_id": 1,
"check_number": null,
"cost_center_id": 2,
"partner_id": 5,
"partner": {
"id": 5,
"name": "Al Salam Real Estate",
"name_ar": "شركة السلام العقارية"
},
"status": "draft",
"status_label": "Draft",
"journal_entry_id": null,
"notes": "February 2026 rent",
"created_by": 1,
"approved_by": null,
"approved_at": null,
"created_at": "2026-02-23T10:00:00.000000Z",
"updated_at": "2026-02-23T10:00:00.000000Z"
}
}Approve Expense
Request POST /api/accounting/expenses/1/approve
Response 200 OK
json
{
"data": {
"id": 1,
"document_number": "EXP-2026-0001",
"status": "approved",
"status_label": "Approved",
"journal_entry_id": 42,
"approved_by": 1,
"approved_at": "2026-02-23T11:00:00.000000Z"
}
}List Expenses
Request GET /api/accounting/expenses?status=approved&date_from=2026-01-01&date_to=2026-12-31
Response 200 OK
json
{
"data": [
{
"id": 1,
"document_number": "EXP-2026-0001",
"date": "2026-02-23",
"expense_category": {
"id": 1,
"name": "إيجار المكتب"
},
"amount": "1500.000",
"tax_amount": "75.000",
"total_amount": "1575.000",
"payment_method": "bank_transfer",
"paying_account": {
"id": 15,
"code": "1201001",
"name": "NBK Main Account"
},
"partner": {
"id": 5,
"name": "شركة السلام العقارية"
},
"status": "approved"
}
],
"links": { "first": "...", "last": "...", "prev": null, "next": null },
"meta": { "current_page": 1, "last_page": 1, "per_page": 25, "total": 1 }
}Generated Journal Entry
When an expense is approved, the system creates a posted journal entry:
| Line | Account | Debit | Credit | Description |
|---|---|---|---|---|
| 1 | Expense category GL account (e.g., Rent Expense) | 1,500.000 | -- | Expense amount |
| 2 | Tax Payable account (from tax rate) | 75.000 | -- | Tax amount (if applicable) |
| 3 | Paying account (e.g., NBK Main Account) | -- | 1,575.000 | Total payment |
Business Rules
| Rule | Description |
|---|---|
| Draft-only edits | Expenses can only be updated or deleted while in draft status |
| Auto document number | document_number is auto-generated via the sequence service on creation |
| Tax auto-calculation | If tax_rate_id is provided, tax_amount = amount * rate / 100 and total_amount = amount + tax_amount |
| Tax recalculation | On update, tax is recalculated if amount or tax_rate_id changes |
| Approval creates JE | Approving a draft expense creates a posted journal entry and sets approved_by and approved_at |
| Cancel reverses JE | Cancelling an approved expense reverses the associated journal entry |
| Company scoped | All expenses are filtered by the authenticated user's company |
| Bank details required | bank_account_id is required when payment_method is check or bank_transfer |
Permissions
| Permission | Description |
|---|---|
accounting.expenses.view | View expenses |
accounting.expenses.create | Create new expenses |
accounting.expenses.update | Update draft expenses |
accounting.expenses.approve | Approve draft expenses |
accounting.expenses.cancel | Cancel approved expenses |