Sales Invoices (فواتير المبيعات)
The Sales Invoices API manages the full invoice lifecycle from draft creation through approval, posting (with automatic journal entries and COGS), payment tracking, and cancellation.
Entity Attributes
SalesInvoice (Header)
| Field | Type | Required | Description |
|---|---|---|---|
id | integer | auto | Primary key |
invoice_number | string | auto | Auto-generated via SequenceService (e.g., INV-000001) |
date | date | yes | Invoice date |
due_date | date | no | Payment due date |
customer_id | FK | yes | Business partner (must be customer) |
customer_name | string | no | Optional override for customer name |
salesperson_id | FK | no | Assigned salesperson (user) |
warehouse_id | FK | no | Default warehouse for items |
sales_order_id | FK | no | Source sales order (if created from order) |
status | enum | auto | draft, pending_approval, approved, posted, partially_paid, paid, overdue, cancelled |
payment_status | string | auto | pending, partial, paid |
reference | string | no | External reference number |
subject | string | no | Invoice subject line |
notes / notes_ar | text | no | Notes in English / Arabic |
terms / terms_ar | text | no | Terms & conditions |
currency_code | string | no | Currency code (default: KWD) |
exchange_rate | decimal(12,6) | no | Exchange rate (default: 1) |
payment_terms_days | integer | no | Payment terms in days |
subtotal | decimal(15,3) | auto | Sum of line totals |
discount_amount | decimal(15,3) | auto | Sum of line discounts |
tax_amount | decimal(15,3) | auto | Sum of line taxes |
total | decimal(15,3) | auto | subtotal + tax_amount |
amount_paid | decimal(15,3) | auto | Total payments received |
balance_due | decimal(15,3) | auto | total - amount_paid |
journal_entry_id | FK | auto | Revenue journal entry (set on post) |
cogs_journal_entry_id | FK | auto | COGS journal entry (set on post) |
approved_by | FK | auto | User who approved |
approved_at | datetime | auto | Approval timestamp |
posted_by | FK | auto | User who posted |
posted_at | datetime | auto | Posting timestamp |
cancelled_by | FK | auto | User who cancelled |
cancelled_at | datetime | auto | Cancellation timestamp |
cancellation_reason | text | no | Reason for cancellation |
SalesInvoiceItem (Line Item)
| Field | Type | Required | Description |
|---|---|---|---|
id | integer | auto | Primary key |
sales_order_item_id | FK | no | Source order item (if from order) |
product_id | FK | yes | Product reference |
product_variant_id | FK | no | Optional product variant |
unit_id | FK | yes | Unit of measure |
warehouse_id | FK | no | Source warehouse for stock deduction |
description / description_ar | string | no | Custom line description |
quantity | decimal(15,3) | yes | Invoiced quantity |
unit_price | decimal(15,3) | yes | Price per unit |
discount_percent | decimal(8,3) | no | Line discount percentage |
discount_amount | decimal(15,3) | auto | Calculated discount amount |
tax_rate_id | FK | no | Tax rate reference |
tax_amount | decimal(15,3) | auto | Calculated tax amount |
line_total | decimal(15,3) | auto | (qty x price) - discount |
unit_cost | decimal(15,3) | auto | Product cost at posting time (for COGS) |
total_cost | decimal(15,3) | auto | quantity x unit_cost |
sort_order | integer | no | Display order |
ER Diagram
Invoice Lifecycle
Posting Flow (Critical)
When an invoice is posted (POST /api/sales/invoices/{id}/post), the following happens atomically:
Step 1: Revenue Journal Entry
| Account | Debit | Credit |
|---|---|---|
Accounts Receivable (sales.receivable_account_id) | total | |
Revenue (sales.revenue_account_id) | subtotal + discount | |
Sales Discount (sales.discount_account_id) | discount | |
Tax Payable (sales.tax_payable_account_id) | tax_amount |
Step 2: COGS Journal Entry (for tracked inventory products)
| Account | Debit | Credit |
|---|---|---|
COGS (sales.cogs_account_id) | total_cogs | |
Inventory (warehouse.account_id) | total_cogs |
Step 3: Stock Deduction (if sales.stock_deduction_point = 'invoice')
- For each line with
product.track_inventory = true - Calculates cost via
StockService::getIssueCost()(WAC or FIFO) - Decreases stock via
StockService::decreaseStock()
Step 4: Update Order
- Increments
invoiced_quantityon linked order items - Recalculates order
invoice_status
API Endpoints
| Method | Endpoint | Description | Permission |
|---|---|---|---|
GET | /api/sales/invoices | List invoices (paginated) | sales.invoices.view |
POST | /api/sales/invoices | Create a new invoice | sales.invoices.create |
GET | /api/sales/invoices/{id} | Get invoice details | sales.invoices.view |
PUT | /api/sales/invoices/{id} | Update a draft invoice | sales.invoices.update |
DELETE | /api/sales/invoices/{id} | Delete a draft invoice | sales.invoices.delete |
POST | /api/sales/invoices/{id}/submit-approval | Submit invoice for approval | sales.invoices.approve |
POST | /api/sales/invoices/{id}/approve | Approve an invoice | sales.invoices.approve |
POST | /api/sales/invoices/{id}/reject | Reject invoice (back to draft) | sales.invoices.approve |
POST | /api/sales/invoices/{id}/post | Post invoice (JE + COGS + stock) | sales.invoices.post |
POST | /api/sales/invoices/{id}/cancel | Cancel an invoice | sales.invoices.cancel |
POST | /api/sales/invoices/{id}/duplicate | Duplicate an invoice | sales.invoices.duplicate |
POST | /api/sales/orders/{id}/create-invoice | Create invoice from order | sales.invoices.create |
Query Parameters (List)
| Parameter | Type | Description |
|---|---|---|
status | string | Filter by invoice status |
payment_status | string | Filter by payment status (pending/partial/paid) |
customer_id | integer | Filter by customer |
salesperson_id | integer | Filter by salesperson |
sales_order_id | integer | Filter by source order |
date_from | date | Invoices from this date |
date_to | date | Invoices up to this date |
due_date_from | date | Due from this date |
due_date_to | date | Due up to this date |
overdue | boolean | Only overdue invoices |
search | string | Search in invoice_number, reference, subject |
Request / Response Examples
Create Invoice
bash
curl -X POST /api/sales/invoices \
-H "Authorization: Bearer {token}" \
-H "Content-Type: application/json" \
-d '{
"date": "2026-02-24",
"due_date": "2026-03-26",
"customer_id": 1,
"subject": "Monthly Services",
"payment_terms_days": 30,
"items": [
{
"product_id": 1,
"unit_id": 1,
"quantity": 10,
"unit_price": 25.000,
"discount_percent": 5,
"warehouse_id": 1
}
]
}'dart
final response = await dio.post('/api/sales/invoices', data: {
'date': '2026-02-24',
'due_date': '2026-03-26',
'customer_id': 1,
'subject': 'Monthly Services',
'payment_terms_days': 30,
'items': [
{
'product_id': 1,
'unit_id': 1,
'quantity': 10,
'unit_price': 25.000,
'discount_percent': 5,
'warehouse_id': 1,
},
],
});Post Invoice
bash
curl -X POST /api/sales/invoices/1/post \
-H "Authorization: Bearer {token}"dart
final response = await dio.post('/api/sales/invoices/1/post');Create Invoice from Order
bash
curl -X POST /api/sales/orders/1/create-invoice \
-H "Authorization: Bearer {token}" \
-H "Content-Type: application/json" \
-d '{"due_date": "2026-03-26"}'dart
final response = await dio.post('/api/sales/orders/1/create-invoice', data: {
'due_date': '2026-03-26',
});Business Rules
- Invoices are created in Draft status.
- Only Draft invoices can be edited or deleted.
- Approving an invoice requires at least one line item.
- Only Approved invoices can be posted.
- Posting creates a Revenue JE (DR Receivable, CR Revenue, DR Discount, CR Tax) and optionally a COGS JE (DR COGS, CR Inventory) for tracked products.
- Stock is deducted at posting time if
sales.stock_deduction_point = 'invoice'. - Cancelling a posted invoice reverses both journal entries and restores stock.
- Cancellation is blocked if the invoice has recorded payments (
amount_paid > 0). - Creating from an order copies only remaining uninvoiced quantities.
- Posted invoices track
amount_paidandbalance_duefor payment reconciliation. - Duplicating an invoice creates a new Draft with fresh number, resets all payment/JE fields.
- Required accounting settings:
sales.receivable_account_idandsales.revenue_account_id.