Purchase Orders — Technical Reference¶
Useful links: User guide
Overview¶
Purchase Orders are managed in the transport Django app (backend/transport/). The main submodule is transport/purchase_orders/.
Key files:
| File | Purpose |
|---|---|
transport/models.py |
PurchaseOrder, TransportLineItem, PurchaseOrderDocument, PurchaseOrderNote models |
transport/constants.py |
PurchaseOrderStatus, PurchaseOrderActivityType enums |
transport/purchase_orders/views.py |
PurchaseOrderViewSet, PurchaseOrderLineItemsViewSet, PurchaseOrderDocumentViewSet, PurchaseOrderNoteViewSet |
transport/purchase_orders/serializers.py |
All PO serializers |
transport/purchase_orders/utils.py |
get_purchase_orders_by_user, get_purchase_order_activity |
transport/utils.py |
update_purchase_order_status |
transport/filters.py |
PurchaseOrderFilter, LineItemsFilter |
Data Models¶
PurchaseOrder¶
DB table: purchase_orders
| Field | Type | Notes |
|---|---|---|
id |
Auto PK | |
order_number |
CharField (unique) | Generated as PO-{unix_timestamp} on create |
status |
CharField | See Statuses |
client_reference |
CharField | External reference, optional |
buyer |
FK → support.Company |
Required to save |
vendor |
FK → support.Supplier |
Required to save |
final_destination |
FK → support.Company |
Optional |
origin_agent |
FK → agents.Agent |
Auto-assigned by routing rules |
incoterm |
FK → support.Incoterm |
PO-level transport detail |
port_of_origin |
FK → support.Port |
PO-level transport detail |
port_of_destination |
FK → support.Port |
PO-level transport detail |
transport_mode |
FK → support.TransportMode |
PO-level transport detail |
packaging_type |
FK → support.PackagingType |
PO-level transport detail |
payment_term |
FK → support.PaymentTerm |
PO-level transport detail |
same_transport_details |
BooleanField | If True, all line items inherit PO transport fields |
order_tags |
ArrayField(CharField) | Optional tags |
remarks |
ArrayField(CharField) | Optional remarks |
season |
CharField | Optional |
type_of_goods |
CharField | Optional |
requested_time_departure |
DateField | Optional |
requested_cargo_ready_date |
DateField | Optional |
requested_final_destination_arrival_date |
DateField | Optional |
requested_arrival_pod |
DateField | Optional |
is_deleted |
BooleanField | Soft delete flag |
created_at |
DateTimeField | Auto |
updated_at |
DateTimeField | Auto |
created_by |
FK → authentication.User |
Set on create |
Audit trail: tracked by pghistory (excludes updated_at).
TransportLineItem¶
Each line item belongs to one PurchaseOrder via purchase_order FK.
| Field | Type | Notes |
|---|---|---|
id |
Auto PK | |
purchase_order |
FK → PurchaseOrder |
|
item_number |
CharField | Buyer's internal item ID |
description |
CharField | Product name |
quantity |
IntegerField | Ordered quantity |
group |
CharField | Product group |
tags |
ArrayField | Optional item tags |
incoterm |
FK → support.Incoterm |
Item-level if same_transport_details=False |
port_of_origin |
FK → support.Port |
|
port_of_destination |
FK → support.Port |
|
transport_mode |
FK → support.TransportMode |
|
packaging_type |
FK → support.PackagingType |
|
payment_term |
FK → support.PaymentTerm |
|
origin_agent |
FK → agents.Agent |
Auto-assigned by routing |
requested_time_departure |
DateField | |
requested_cargo_ready_date |
DateField | |
requested_final_destination_arrival_date |
DateField | |
requested_arrival_pod |
DateField |
Computed annotations (added in get_queryset):
| Annotation | Description |
|---|---|
booking_quantity |
Sum of quantities reserved in bookings |
open_quantity |
quantity - booking_quantity |
shipment_quantity |
Sum of quantities in shipments |
PurchaseOrderDocument¶
DB table: purchase_order_documents
| Field | Type | Notes |
|---|---|---|
purchase_order |
FK → PurchaseOrder |
|
file |
FileField | Uploaded file path |
name |
CharField | Display name |
document_type |
FK → support.DocumentType |
|
created_at |
DateTimeField | |
created_by |
FK → authentication.User |
PurchaseOrderNote¶
DB table: purchase_order_notes
| Field | Type | Notes |
|---|---|---|
purchase_order |
FK → PurchaseOrder |
|
text |
TextField | |
created_at |
DateTimeField |
On AFTER_CREATE and AFTER_DELETE hooks: updates purchase_order.updated_at.
Statuses¶
Defined in transport/constants.py → PurchaseOrderStatus.
| Value | Label | Visible in UI |
|---|---|---|
DRAFT |
Draft | No |
OPEN |
Open | Yes |
PARTIAL |
Partial | Yes |
BOOKED |
Booked | Yes |
ARCHIVED |
Archived | No (excluded in frontend) |
Status is updated automatically by update_purchase_order_status() in transport/utils.py whenever a line item is saved, deleted, or its booking changes.
Transition logic:
- → BOOKED: all line items have open_quantity == 0 (fully covered by bookings)
- → PARTIAL: some but not all line items are in active bookings
- → OPEN: no line items are in active bookings, or open_quantity > 0 on all
API Endpoints¶
Base path: /api/purchase-orders/
Tag: purchase-orders
Purchase Orders¶
| Method | Path | Action | Description |
|---|---|---|---|
GET |
/api/purchase-orders/ |
list |
Paginated list, filtered by user role |
POST |
/api/purchase-orders/ |
create |
Create new PO (Buyer/Admin/Staff only) |
GET |
/api/purchase-orders/{id}/ |
retrieve |
Get single PO |
PUT |
/api/purchase-orders/{id}/ |
update |
Full update |
PATCH |
/api/purchase-orders/{id}/ |
partial_update |
Partial update |
POST |
/api/purchase-orders/{id}/delete/ |
delete |
Soft delete (sets is_deleted=True) |
GET |
/api/purchase-orders/{id}/activity/ |
activity |
Activity log |
POST |
/api/purchase-orders/{id}/toggle-favorite/ |
toggle_favorite |
Pin / unpin |
GET |
/api/purchase-orders/buyers/ |
buyers |
List unique buyers |
POST |
/api/purchase-orders/autocomplete_remarks/ |
autocomplete_remarks |
Suggest remarks |
POST |
/api/purchase-orders/autocomplete_order_tags/ |
autocomplete_order_tags |
Suggest tags |
Line Items¶
Base path: /api/purchase-orders/{purchase_orders_pk}/items/
Tag: purchase-orders-items
| Method | Path | Action | Description |
|---|---|---|---|
GET |
.../items/ |
list |
List line items for a PO |
POST |
.../items/ |
create |
Add a line item |
GET |
.../items/{id}/ |
retrieve |
Get single line item |
PUT |
.../items/{id}/ |
update |
Update line item |
PATCH |
.../items/{id}/ |
partial_update |
Partial update |
DELETE |
.../items/{id}/ |
destroy |
Delete (see Delete Logic) |
POST |
.../items/{id}/copy_item/ |
copy_item |
Duplicate item |
POST |
.../items/{id}/split_item/ |
split_item |
Split into two |
Documents¶
Base path: /api/purchase-orders/{purchase_orders_pk}/documents/
Tag: purchase-orders-documents
| Method | Path | Description |
|---|---|---|
GET |
.../documents/ |
List documents |
POST |
.../documents/ |
Upload document |
GET |
.../documents/{id}/ |
Get document |
DELETE |
.../documents/{id}/ |
Delete document |
Notes¶
Base path: /api/purchase-orders/{purchase_orders_pk}/notes/
| Method | Path | Description |
|---|---|---|
POST |
.../notes/ |
Create note |
Access Control¶
Access filtering is applied in get_purchase_orders_by_user() (transport/purchase_orders/utils.py).
| Role | Filter applied | is_readonly |
|---|---|---|
ADMIN, STAFF |
All POs | False |
BUYER, FULL_CLIENT_USER |
POs where buyer is their company or linked company |
False for own, True for linked |
CLIENT |
POs where buyer is linked to their client company |
True |
CONSIGNEE, FINAL_DESTINATION |
POs with line items in bookings where consignee matches their company |
True |
ORIGIN_AGENT |
POs where origin_agent matches (when same_transport_details=True) or line items origin_agent matches |
False |
ORIGIN_AGENT_OPERATOR |
Same as ORIGIN_AGENT |
True |
| Other roles | Empty queryset | — |
is_readonly is returned in the list/retrieve serializer and used by the frontend to hide edit controls.
Origin Agents are blocked from creating, updating, or deleting POs at the view level (HTTP 403).
Business Logic¶
Order Number Generation¶
Generated in CreatePurchaseOrderSerializer.create():
python
order_number = f"PO-{int(time.time())}"
Unix timestamp in seconds — unique in practice, not guaranteed under concurrent load.
same_transport_details Flag¶
When same_transport_details=True:
- On line item create: transport fields are copied from the PO to the new item.
- On PO update (transport fields changed): all existing line items are updated to match, unless status is
PARTIALorBOOKED.
Constraints:
- Cannot switch to True when status is PARTIAL or when any items are booked.
- Cannot change same_transport_details at all when status is BOOKED.
Delete Line Item Logic¶
Implemented in PurchaseOrderLineItemsViewSet.destroy():
- If the item has no bookings → hard delete.
- If the item has bookings and
available_quantity > 0→ reducequantitytoquantity - available_quantity(Open Qty becomes 0). Item stays. - If the item has bookings and
available_quantity == 0→ HTTP 400. Cannot delete a fully booked item.
After deletion, update_purchase_order_status() is called to recalculate PO status.
Split Line Item Logic¶
Implemented in PurchaseOrderLineItemsViewSet.split_item():
- Validates that
current_quantity >= booked_quantity(cannot reduce below already booked amount). - Validates that the item is not fully booked (
booked_quantity < item.quantity). - Sets
item.quantity = current_quantity. - Creates a new
TransportLineItemwithquantity = new_quantity, copyingitem_number,group,tags,description. - If
same_transport_details=True, the new item also inherits PO transport fields.
Routing Auto-Assignment¶
After a line item is created or its transport_mode / port_of_origin changes, RoutingService(instance).apply_rules() is called. This automatically assigns an origin_agent based on routing rules configured in the routing app.
If the item already has bookings, the origin agent cannot be changed (raises ValidationError), except by Admin/Staff overriding manually.
External Integration (Frends)¶
On every line item save, FrendsAPIService().post_po_item(instance) is called (integration/services/webhook_service.py). This sends item data to the external Frends integration.
Activity Log¶
Tracked via pghistory across four event models:
| Event model | Tracks |
|---|---|
transport.PurchaseOrderEvent |
PO field changes |
transport.TransportLineItemEvent |
Line item changes |
transport.PurchaseOrderNoteEvent |
Note add/remove |
transport.PurchaseOrderDocumentEvent |
Document add/remove |
Activity types (PurchaseOrderActivityType):
| Type | Trigger |
|---|---|
PURCHASE_ORDER_CREATED |
PO insert event |
PURCHASE_ORDER_UPDATED |
PO update diff |
PURCHASE_ORDER_LINE_ITEM_CREATED |
Line item add |
PURCHASE_ORDER_LINE_ITEM_UPDATED |
Line item update diff |
PURCHASE_ORDER_LINE_ITEM_REMOVED |
Line item remove |
PURCHASE_ORDER_NOTICE_CREATED |
Note added |
PURCHASE_ORDER_DOCUMENT_CREATED |
Document uploaded |
PURCHASE_ORDER_DOCUMENT_REMOVED |
Document deleted |
Returned by GET /api/purchase-orders/{id}/activity/, ordered newest first.