Sales Payments (مدفوعات المبيعات)
The Sales Payments API records customer payments against posted invoices. Supports partial payments, multiple payment methods, installment schedules, and automatic journal entry creation.
Entity Attributes
SalesPayment
| Field | Type | Required | Description |
|---|---|---|---|
id | integer | auto | Primary key |
payment_number | string | auto | Auto-generated via SequenceService (e.g., SPAY-00001) |
date | date | yes | Payment date |
invoice_id | FK | yes | Source posted invoice |
partner_id | FK | auto | Copied from the source invoice |
amount | decimal(15,3) | yes | Payment amount (max = invoice balance_due) |
currency_id | FK | no | Payment currency |
exchange_rate | decimal(15,6) | no | Exchange rate (default 1.000000) |
payment_method | enum | yes | cash, bank_transfer, check, credit_card |
bank_account_id | FK | no | Bank account (for bank_transfer/check) |
check_number | string(50) | no | Check number |
check_date | date | no | Check date |
check_bank | string | no | Check issuing bank |
reference | string | no | External reference number |
notes / notes_ar | text | no | Notes in English / Arabic |
receiving_account_id | FK | yes | GL account receiving payment (Cash/Bank) |
journal_entry_id | FK | auto | Created on post |
status | enum | auto | draft, posted, cancelled |
posted_at | datetime | auto | When posted |
posted_by | FK | auto | User who posted |
cancelled_at | datetime | auto | When cancelled |
cancelled_by | FK | auto | User who cancelled |
cancellation_reason | text | no | Reason for cancellation |
SalesPaymentSchedule (Installment Tracking)
| Field | Type | Required | Description |
|---|---|---|---|
id | integer | auto | Primary key |
invoice_id | FK | yes | Source invoice |
installment_number | integer | yes | Installment sequence number |
due_date | date | yes | Installment due date |
amount | decimal(15,3) | yes | Installment amount |
amount_paid | decimal(15,3) | auto | Amount paid so far (default 0) |
status | enum | auto | pending, partially_paid, paid, overdue |
ER Diagram
Payment Lifecycle
Posting Flow (Critical)
When a payment is posted (POST /api/sales/payments/{id}/post), the following happens atomically:
Step 1: Create Journal Entry
DR Receiving Account (Cash/Bank) ← payment amount
CR Accounts Receivable (AR) ← payment amount (with partner_id)The AR account is read from sales.receivable_account_id setting.
Step 2: Update Invoice
- Increments
invoice.amount_paidby the payment amount - Recalculates
invoice.balance_due = total - amount_paid - Updates
invoice.payment_status:paid— if balance_due = 0partial— if amount_paid > 0
- Updates
invoice.status:paid— if fully paidpartially_paid— if partially paid
Step 3: Allocate to Installments
If the invoice has payment schedule installments:
- Allocates to the oldest unpaid installment first
- Updates each installment's
amount_paidandstatus - Moves to the next installment if the current one is fully paid
Cancellation Flow
When a posted payment is cancelled (POST /api/sales/payments/{id}/cancel):
- Reverse JE — creates a reversal journal entry via
ReverseJournalEntry - Update invoice — decrements
amount_paid, recalculatesbalance_dueandpayment_status - Reverse installment allocation — deallocates from the newest paid installment first, reverting status
API Endpoints
| Method | Endpoint | Description | Permission |
|---|---|---|---|
GET | /api/sales/payments | List payments (paginated) | sales.payments.view |
POST | /api/sales/payments | Create a payment | sales.payments.create |
GET | /api/sales/payments/{id} | Get payment details | sales.payments.view |
PUT | /api/sales/payments/{id} | Update a draft payment | sales.payments.update |
DELETE | /api/sales/payments/{id} | Delete a draft payment | sales.payments.delete |
POST | /api/sales/payments/{id}/post | Post payment (creates JE) | sales.payments.post |
POST | /api/sales/payments/{id}/cancel | Cancel payment (reverses JE) | sales.payments.cancel |
GET | /api/sales/invoices/{id}/payments | List payments for invoice | sales.payments.view |
GET | /api/sales/invoices/{id}/payment-schedule | View installment schedule | sales.payments.view |
Query Parameters (List)
| Parameter | Type | Description |
|---|---|---|
status | string | Filter by payment status |
partner_id | integer | Filter by customer |
invoice_id | integer | Filter by source invoice |
payment_method | string | Filter by payment method |
branch_id | integer | Filter by branch |
date_from | date | Payments from this date |
date_to | date | Payments up to this date |
search | string | Search in payment_number, reference, check_number |
Request / Response Examples
Create Payment
bash
curl -X POST /api/sales/payments \
-H "Authorization: Bearer {token}" \
-H "Content-Type: application/json" \
-d '{
"invoice_id": 1,
"date": "2026-02-24",
"amount": 250.000,
"payment_method": "cash",
"receiving_account_id": 5,
"reference": "REC-001"
}'dart
final response = await dio.post('/api/sales/payments', data: {
'invoice_id': 1,
'date': '2026-02-24',
'amount': 250.000,
'payment_method': 'cash',
'receiving_account_id': 5,
'reference': 'REC-001',
});Create Check Payment
bash
curl -X POST /api/sales/payments \
-H "Authorization: Bearer {token}" \
-H "Content-Type: application/json" \
-d '{
"invoice_id": 1,
"date": "2026-02-24",
"amount": 500.000,
"payment_method": "check",
"receiving_account_id": 5,
"check_number": "CHK-12345",
"check_date": "2026-03-01",
"check_bank": "National Bank of Kuwait"
}'dart
final response = await dio.post('/api/sales/payments', data: {
'invoice_id': 1,
'date': '2026-02-24',
'amount': 500.000,
'payment_method': 'check',
'receiving_account_id': 5,
'check_number': 'CHK-12345',
'check_date': '2026-03-01',
'check_bank': 'National Bank of Kuwait',
});Post Payment
bash
curl -X POST /api/sales/payments/1/post \
-H "Authorization: Bearer {token}"dart
final response = await dio.post('/api/sales/payments/1/post');Cancel Payment
bash
curl -X POST /api/sales/payments/1/cancel \
-H "Authorization: Bearer {token}" \
-H "Content-Type: application/json" \
-d '{"cancellation_reason": "Customer requested refund"}'dart
final response = await dio.post('/api/sales/payments/1/cancel', data: {
'cancellation_reason': 'Customer requested refund',
});View Invoice Payments
bash
curl -X GET /api/sales/invoices/1/payments \
-H "Authorization: Bearer {token}"dart
final response = await dio.get('/api/sales/invoices/1/payments');View Payment Schedule
bash
curl -X GET /api/sales/invoices/1/payment-schedule \
-H "Authorization: Bearer {token}"dart
final response = await dio.get('/api/sales/invoices/1/payment-schedule');Business Rules
- Payments can only be created against posted invoices — draft, approved, or cancelled invoices are rejected
- Payment amount cannot exceed the invoice balance due — prevents overpayment
- Partner and branch are inherited from the source invoice — cannot be manually set
- Only draft payments can be edited or deleted
- Posting creates a journal entry — DR receiving account (Cash/Bank), CR AR
- Cancellation fully reverses — JE is reversed, invoice amounts are updated, installment allocations are reversed
- Installment allocation is automatic — oldest unpaid installment is allocated first on post, newest paid is reversed first on cancel
- Invoice status auto-updates — posted → partially_paid → paid based on amount_paid vs total