Purchase Orders (أوامر الشراء)
Formal purchase orders sent to suppliers. Can be created standalone or converted from an approved Purchase Request.
Status Workflow
| Status | Editable | Can Submit | Can Approve | Can Confirm | Can Send | Can Cancel | Can Close |
|---|---|---|---|---|---|---|---|
draft | Yes | Yes | No | No | No | Yes | No |
pending_approval | No | No | Yes | No | No | Yes | No |
approved | No | No | No | Yes | No | Yes | No |
confirmed | No | No | No | No | Yes | Yes* | Yes |
sent | No | No | No | No | Yes | Yes* | Yes |
partially_received | No | No | No | No | No | No | Yes |
fully_received | No | No | No | No | No | No | Yes |
partially_billed | No | No | No | No | No | No | Yes |
fully_billed | No | No | No | No | No | No | Yes |
closed | No | No | No | No | No | No | No |
cancelled | No | No | No | No | No | No | No |
*Cancel is blocked if any items have received or billed quantities > 0.
Entity Attributes
PurchaseOrder
| Field | Type | Description |
|---|---|---|
id | integer | Primary key |
company_id | integer | Tenant company |
branch_id | integer | Branch (nullable) |
order_number | string | Auto-generated (e.g., PO-2026-00001) |
date | date | Order date |
expected_delivery_date | date | Expected delivery date |
supplier_id | integer | Supplier business partner ID |
supplier_name | string | Cached supplier name |
warehouse_id | integer | Receiving warehouse |
purchase_request_id | integer | Source PR (nullable) |
status | enum | See workflow above (11 states) |
receive_status | enum | pending, partial, complete |
bill_status | enum | pending, partial, complete |
reference | string | External reference |
subject | string | Order subject |
notes / notes_ar | text | Additional notes |
terms / terms_ar | text | Payment/delivery terms |
currency_code | string | Currency (default KWD) |
exchange_rate | decimal(12,6) | Exchange rate |
payment_terms_days | integer | Payment terms in days |
subtotal | decimal(15,3) | Sum of line totals |
discount_amount | decimal(15,3) | Sum of line discounts |
tax_amount | decimal(15,3) | Sum of line taxes |
total | decimal(15,3) | subtotal + tax_amount |
created_by | integer | Creator user ID |
approved_by | integer | Approver user ID |
approved_at | datetime | Approval timestamp |
confirmed_by | integer | Confirmer user ID |
confirmed_at | datetime | Confirmation timestamp |
cancelled_by | integer | Canceller user ID |
cancelled_at | datetime | Cancellation timestamp |
cancellation_reason | text | Cancellation reason |
rejection_reason | text | Rejection reason |
PurchaseOrderItem
| Field | Type | Description |
|---|---|---|
id | integer | Primary key |
purchase_order_id | integer | FK to purchase order |
product_id | integer | Product |
product_variant_id | integer | Optional variant |
unit_id | integer | Unit of measure |
description / description_ar | string | Item description |
quantity | decimal(15,3) | Ordered quantity |
unit_cost | decimal(15,3) | Unit cost |
discount_percent | decimal(8,3) | Discount percentage |
discount_amount | decimal(15,3) | Calculated discount amount |
tax_rate_id | integer | Optional tax rate |
tax_amount | decimal(15,3) | Calculated tax amount |
line_total | decimal(15,3) | Line total after discount |
received_quantity | decimal(15,3) | Quantity received via GRN |
billed_quantity | decimal(15,3) | Quantity billed |
notes | text | Item-level notes |
sort_order | integer | Display order |
API Endpoints
| Method | Endpoint | Permission | Description |
|---|---|---|---|
GET | /api/purchases/orders | purchases.orders.view | List purchase orders |
POST | /api/purchases/orders | purchases.orders.create | Create purchase order |
GET | /api/purchases/orders/{id} | purchases.orders.view | Show purchase order |
PUT | /api/purchases/orders/{id} | purchases.orders.update | Update purchase order |
DELETE | /api/purchases/orders/{id} | purchases.orders.delete | Delete draft order |
POST | /api/purchases/orders/{id}/submit-approval | purchases.orders.approve | Submit for approval |
POST | /api/purchases/orders/{id}/approve | purchases.orders.approve | Approve order |
POST | /api/purchases/orders/{id}/reject | purchases.orders.approve | Reject order |
POST | /api/purchases/orders/{id}/confirm | purchases.orders.confirm | Confirm order |
POST | /api/purchases/orders/{id}/send | purchases.orders.manage | Mark sent to supplier |
POST | /api/purchases/orders/{id}/cancel | purchases.orders.manage | Cancel order |
POST | /api/purchases/orders/{id}/close | purchases.orders.manage | Close order |
POST | /api/purchases/requests/{id}/convert-to-order | purchases.orders.create | Convert PR to PO |
Query Parameters (List)
| Parameter | Type | Description |
|---|---|---|
status | string | Filter by status |
receive_status | string | Filter by receive status |
bill_status | string | Filter by bill status |
supplier_id | integer | Filter by supplier |
warehouse_id | integer | Filter by warehouse |
purchase_request_id | integer | Filter by source PR |
date_from | date | Start date filter |
date_to | date | End date filter |
search | string | Search in order_number, reference, subject |
Examples
Create Purchase Order
bash
curl -X POST /api/purchases/orders \
-H "Authorization: Bearer {token}" \
-H "Content-Type: application/json" \
-d '{
"date": "2026-02-25",
"supplier_id": 1,
"expected_delivery_date": "2026-03-15",
"payment_terms_days": 30,
"items": [
{
"product_id": 1,
"unit_id": 1,
"quantity": 100,
"unit_cost": 25.000
}
]
}'dart
final response = await dio.post('/api/purchases/orders', data: {
'date': '2026-02-25',
'supplier_id': 1,
'expected_delivery_date': '2026-03-15',
'payment_terms_days': 30,
'items': [
{
'product_id': 1,
'unit_id': 1,
'quantity': 100,
'unit_cost': 25.000,
}
],
});Convert Purchase Request to Order
bash
curl -X POST /api/purchases/requests/1/convert-to-order \
-H "Authorization: Bearer {token}" \
-d '{"supplier_id": 1}'dart
await dio.post('/api/purchases/requests/1/convert-to-order', data: {
'supplier_id': 1,
});Reject Order
bash
curl -X POST /api/purchases/orders/1/reject \
-H "Authorization: Bearer {token}" \
-d '{"reason": "Budget exceeded"}'dart
await dio.post('/api/purchases/orders/1/reject', data: {
'reason': 'Budget exceeded',
});Business Rules
- Purchase orders are auto-numbered using the
POsequence (e.g.,PO-2026-00001) - Only draft orders can be edited or deleted
- Submitting for approval requires at least one item
- Rejection requires a reason and returns the order to draft status for editing
- Cancel is blocked if any items have
received_quantity > 0orbilled_quantity > 0 - Creating from a Purchase Request marks the PR as converted and copies all items
- The
subtotal,discount_amount,tax_amount, andtotalare automatically recalculated when items change receive_statusandbill_statusare updated automatically when GRNs/Bills are processed