Purchase Returns / Debit Notes (مردودات المشتريات)
Purchase returns (debit notes) allow returning goods to suppliers and reversing the associated accounting entries. Returns can be created against a posted purchase bill (bill-linked) or independently without a bill (standalone mode).
Entity Attributes
PurchaseReturn
| Field | Type | Required | Description |
|---|---|---|---|
id | integer | auto | Primary key |
company_id | FK | auto | Company |
branch_id | FK | auto/conditional | Inherited from bill; required if standalone |
return_number | string | auto | Auto-generated PDN-YYYY-NNNNN |
date | date | yes | Return date |
supplier_id | FK | auto/conditional | Inherited from bill; required if standalone |
supplier_name | string | auto | Cached supplier name (auto-resolved from BusinessPartner when standalone) |
bill_id | FK | no | Source purchase bill (must be posted); null for standalone returns |
cost_center_id | FK | no | Optional cost center |
currency_code | string | auto/conditional | Inherited from bill; required if standalone (default: KWD) |
exchange_rate | decimal(12,6) | auto | Inherited from bill (1.0 for standalone) |
reason | text | no | Return reason (EN) |
reason_ar | text | no | Return reason (AR) |
notes | text | no | Notes (EN) |
notes_ar | text | no | Notes (AR) |
subtotal | decimal(15,3) | auto | Sum of line_total |
discount_amount | decimal(15,3) | auto | Sum of item discounts |
tax_amount | decimal(15,3) | auto | Sum of item taxes |
total | decimal(15,3) | auto | subtotal + tax_amount |
status | enum | auto | Current workflow status |
journal_entry_id | FK | auto | Linked JE (after posting) |
PurchaseReturnItem
| Field | Type | Required | Description |
|---|---|---|---|
id | integer | auto | Primary key |
purchase_return_id | FK | auto | Parent return |
bill_item_id | FK | conditional | Source bill item; null for standalone items |
product_id | FK | auto/conditional | From bill item; required if standalone item |
product_variant_id | FK | no | From bill item; optional for standalone |
unit_id | FK | auto/conditional | From bill item; required if standalone item |
warehouse_id | FK | no | Return warehouse (defaults to bill item) |
quantity | decimal(15,3) | yes | Quantity to return |
unit_cost | decimal(15,3) | auto/conditional | From bill item; required if standalone item |
discount_amount | decimal(15,3) | auto/no | Proportional from bill item; optional for standalone |
tax_rate_id | FK | auto/no | From bill item; optional for standalone |
tax_amount | decimal(15,3) | auto | Calculated from line_total |
line_total | decimal(15,3) | auto | (qty * unit_cost) - discount |
total_cost | decimal(15,3) | auto | qty * unit_cost |
ER Diagram
Workflow / Status Diagram
API Endpoints
| Method | Endpoint | Permission | Description |
|---|---|---|---|
| GET | /api/purchases/returns | purchases.returns.view | List returns |
| POST | /api/purchases/returns | purchases.returns.create | Create return |
| GET | /api/purchases/returns/{id} | purchases.returns.view | Show return |
| PUT | /api/purchases/returns/{id} | purchases.returns.update | Update draft |
| DELETE | /api/purchases/returns/{id} | purchases.returns.delete | Delete draft |
| POST | /api/purchases/returns/{id}/submit-approval | purchases.returns.approve | Submit for approval |
| POST | /api/purchases/returns/{id}/approve | purchases.returns.approve | Approve return |
| POST | /api/purchases/returns/{id}/reject | purchases.returns.approve | Reject (back to draft) |
| POST | /api/purchases/returns/{id}/post | purchases.returns.post | Post return |
| POST | /api/purchases/returns/{id}/cancel | purchases.returns.cancel | Cancel return |
Query Parameters (Index)
| Parameter | Type | Description |
|---|---|---|
status | string | Filter by status: draft, pending_approval, approved, posted, cancelled |
supplier_id | integer | Filter by supplier |
bill_id | integer | Filter by source bill |
branch_id | integer | Filter by branch |
date_from | date | Start date filter |
date_to | date | End date filter |
search | string | Search return_number, reason, supplier_name |
standalone | boolean | 1 = only standalone (no bill), 0 = only bill-linked |
Create Request (Bill-Linked)
bash
curl -X POST /api/purchases/returns \
-H "Authorization: Bearer {token}" \
-H "Content-Type: application/json" \
-d '{
"bill_id": 1,
"date": "2026-02-25",
"reason": "Defective goods received",
"items": [
{
"bill_item_id": 1,
"quantity": 3,
"warehouse_id": 1,
"notes": "Damaged packaging"
}
]
}'dart
final response = await dio.post(
'/api/purchases/returns',
data: {
'bill_id': 1,
'date': '2026-02-25',
'reason': 'Defective goods received',
'items': [
{
'bill_item_id': 1,
'quantity': 3,
'warehouse_id': 1,
'notes': 'Damaged packaging',
},
],
},
);Create Request (Standalone — No Bill)
bash
curl -X POST /api/purchases/returns \
-H "Authorization: Bearer {token}" \
-H "Content-Type: application/json" \
-d '{
"supplier_id": 5,
"branch_id": 1,
"currency_code": "KWD",
"date": "2026-02-28",
"reason": "Defective goods returned directly",
"items": [
{
"product_id": 12,
"unit_id": 1,
"unit_cost": 25.500,
"quantity": 2,
"discount_amount": 0,
"tax_rate": 5,
"warehouse_id": 1,
"notes": "Returned without original bill"
}
]
}'dart
final response = await dio.post(
'/api/purchases/returns',
data: {
'supplier_id': 5,
'branch_id': 1,
'currency_code': 'KWD',
'date': '2026-02-28',
'reason': 'Defective goods returned directly',
'items': [
{
'product_id': 12,
'unit_id': 1,
'unit_cost': 25.500,
'quantity': 2,
'discount_amount': 0,
'tax_rate': 5,
'warehouse_id': 1,
'notes': 'Returned without original bill',
},
],
},
);Business Rules
Bill-Linked Mode (with bill_id)
- Source Bill: Returns can only be created against posted bills (
status = posted) - Returnable Quantity: Each bill item's return quantity is validated against
original_qty - already_returned_qty(excluding cancelled returns) - Pricing: Uses original bill item pricing (unit_cost, discount, tax rate) — prices are not manually set on returns
- Auto-Derived Fields:
supplier_id,supplier_name,branch_id,currency_code,exchange_rateare inherited from the bill
Standalone Mode (without bill_id)
- Required Fields:
supplier_id,branch_id,currency_codeare required when nobill_idis provided - Supplier Name: Auto-resolved from the BusinessPartner record
- Item Fields: Each item requires
product_id,unit_id,unit_cost(instead ofbill_item_id) - Pricing:
unit_cost,discount_amount,tax_rateare set directly per item - Mixed Items: A single return can contain both bill-linked items (with
bill_item_id) and standalone items (withproduct_id/unit_id/unit_cost)
Common Rules
- Posting creates reverse AP JE:
- DR: Accounts Payable (reduces AP balance)
- CR: Inventory Account (for tracked products)
- CR: Expense Account (for services/non-inventory)
- CR: Tax Receivable (reverses input tax)
- DR: Discount Account (reverses purchase discount)
- Stock Decrease: On posting, tracked inventory products have stock decreased via
StockService::decreaseStock()withmovement_type=issueandreference_type=purchase_return - Cancellation: Reverses the JE (via
ReverseJournalEntry) and restores stock (viaStockService::increaseStock()) if return was posted
Sequence
Returns use auto-generated numbers with prefix PDN (Purchase Debit Note):
- Format:
PDN-{YYYY}-{NNNNN}(e.g.,PDN-2026-00001) - Reset frequency: Yearly