Document Line Service
Overview
Document lines represent individual items within a document (sales order, credit note, quote, etc.). Each line captures product, quantity, pricing, and fulfillment details.
All document line operations are tenant-aware and scoped to the current tenant.
Endpoints
GET
/api/v1/DocumentLines/document/{documentId}
Returns all lines for a specific document.
Route parameters:
documentId(long, required) - parent document identifier
Behavior:
- Returns
404if no lines exist
Response:
List<DocumentLineModel>
Authorization:
- Requires Bearer Token
- Permission:
FullRead
GET
/api/v1/DocumentLines/{id}
Returns a single document line by its numeric identifier.
Route parameters:
id(long, required) - line identifier
Behavior:
- Returns
404if not found
Response:
DocumentLineModel
Authorization:
- Requires Bearer Token
- Permission:
FullRead
POST
/api/v1/DocumentLines
Creates a new document line.
Request body (DocumentLineModel):
DocumentId(long, required)LineNumber(int, required)ProductId(long, required)Quantity(decimal, required — must be > 0)UnitPrice(decimal, required — must be >= 0)LineTotal(decimal, required — must be >= 0)- Other fields optional — see DocumentLineModel
Behavior:
- Validation is handled in the service layer
- Successful creation writes an audit log entry
Response:
201 Created—DocumentLineModel
Authorization:
- Requires Bearer Token
- Permission:
FullManage
POST
/api/v1/DocumentLines/{id}/pick?warehouseId=123
Picks a Sales Order document line from the given warehouse and marks it picked. Gated by the
PICK_LINE document action (DocumentActionEngine) — allowed only while the parent Sales Order is
AWAITING_PICK. The caller must say which warehouse the pick is coming from — this is not inferred
from the line (its own WarehouseId is only a price-time estimate, not a reliable source of truth).
Once every (non-deleted) line on the parent document is picked, the document auto-advances via the
system-triggered MARK_PICKED action — the same action-resolution pipeline as every other document
action — to AWAITING_CUSTOMISATION/AWAITING_MANPACKING/AWAITING_DISPATCH depending on tenant
feature flags. There is no user-invokable document-level "mark picked" action.
Route parameters:
id(long, required) — line identifier
Query parameters:
warehouseId(long, required) — the warehouse being picked from; must be > 0
Behavior:
- Returns
422(DocumentLineInvalidModel) ifwarehouseIdis missing or not positive - Returns
409(DocumentActionNotAllowed) ifPICK_LINEisn't currently allowed on the parent document (e.g. it isn'tAWAITING_PICK) - Does not check stock on hand — picking never fails due to low or stale stock; a stock-required
line records a negative stock transaction against
warehouseId(reasonPick), and the line's ownWarehouseIdis updated to match. Stock is allowed to go negative; discrepancies are reconciled separately via the stock-count mechanism - Idempotent — picking an already-picked line is a no-op
Response:
200 OK—DocumentLineModel
Authorization:
- Requires Bearer Token
- Permission:
DocumentsWrite
POST
/api/v1/DocumentLines/{id}/receive?warehouseId=123
Receives a Purchase Order document line into the given warehouse and marks it received (reuses
the same IsPicked field — there is no separate "received" state). Gated by the RECEIVE_LINE
document action — allowed only while the parent Purchase Order is AWAITING_STOCK.
Once every line on the Purchase Order is received, it auto-advances via the system-triggered
MARK_RECEIVED action to RECEIVED — after that, RECEIVE_LINE is no longer allowed, so a line
can't be received twice. Stock-required lines record a positive stock transaction (reason
Receive) — the opposite of picking. After adding stock, any Sales Order sitting in
AWAITING_STOCK with a line for the same product variant is automatically rechecked (via its
STOCK_RECEIVED action) and advanced to AWAITING_PICK if it now has enough stock.
Route parameters:
id(long, required) — line identifier
Query parameters:
warehouseId(long, required) — the warehouse the stock is being received into; must be > 0
Behavior:
- Returns
422(DocumentLineInvalidModel) ifwarehouseIdis missing or not positive - Returns
409(DocumentActionNotAllowed) ifRECEIVE_LINEisn't currently allowed on the parent document - Idempotent — receiving an already-received line is a no-op
Response:
200 OK—DocumentLineModel
Authorization:
- Requires Bearer Token
- Permission:
DocumentsWrite
POST
/api/v1/DocumentLines/pick/bulk
Bulk variant of pick — picks several Sales Order lines (optionally spanning multiple documents)
into a single warehouse in one request. Best-effort: one bad line ID doesn't block the rest — each
requested line gets its own success/failure result.
Request body (BulkDocumentLineActionRequest):
LineIds(long[], required)WarehouseId(long, required) — same warehouse applies to every line in the batch
Behavior: identical per-line rules as the single pick endpoint, applied to each line.
Response:
200 OK—BulkDocumentLineActionResult:{ Results: [{ LineId, Success, Line?, Error? }] }
Authorization:
- Requires Bearer Token
- Permission:
DocumentsWrite
POST
/api/v1/DocumentLines/receive/bulk
Bulk variant of receive — same shape and behavior as pick/bulk, for Purchase Order lines.
Request body (BulkDocumentLineActionRequest):
LineIds(long[], required)WarehouseId(long, required)
Response:
200 OK—BulkDocumentLineActionResult
Authorization:
- Requires Bearer Token
- Permission:
DocumentsWrite
PUT
/api/v1/DocumentLines/{id}
Updates an existing document line.
Route parameters:
id(long, required) - line identifier
Request body (DocumentUpdate):
Id(long, required, must be > 0)Quantity(decimal?, optional)UnitPrice(decimal?, optional)DefaultSalePrice(decimal?, optional)DefaultCustomisationPrice(decimal?, optional)DiscountAmount(decimal?, optional)TaxAmount(decimal?, optional)TaxRate(decimal?, optional)IsCustomised(bool?, optional) — mark line as requiring customisationIsPacked(bool?, optional) — mark line as packed for dispatchLineNumber(int?, optional)Notes(string?, optional)ReturnedDate(DateTime?, optional)EntityUserId(long?, optional) — entity user the line is assigned toWarehouseId(long?, optional) — fulfillment warehouseDocumentInvoiceId(long?, optional)
Behavior:
- Only provided (non-null) fields are updated — a JSON
null/omitted field leaves the existing value untouched EntityUserId,WarehouseId,DocumentInvoiceIdare nullable FK-style fields with a clear-to-null sentinel: omit/null= leave unchanged, a positive value = set it,0(or negative) = clear it tonull. Sending JSONnulldoes not unassign these — you must send0- Line total is recalculated from quantity, unit price, discount and tax
- Document subtotal, tax total and grand total are recalculated after each line update
- Successful update writes an audit log entry
Response:
200 OK—DocumentLineModel
Authorization:
- Requires Bearer Token
- Permission:
DocumentsWrite
DELETE
/api/v1/DocumentLines/{id}
Deletes a document line by its identifier.
Route parameters:
id(long, required) - line identifier
Behavior:
- Performs a hard delete
- Successful deletion writes an audit log entry
Response:
204 No Content
Authorization:
- Requires Bearer Token
- Permission:
FullManage
DocumentLineModel Fields
Returned by GET and POST endpoints:
Id (type: long)— internal identifierDocumentId (type: long?)— parent documentLineNumber (type: int?)— position within the documentProductId (type: long?)— product FKProductVariantId (type: long?)— product variant FKSku (type: string?)— SKU at time of orderTitle (type: string?)— product title at time of orderDescription (type: string?)— description at time of orderQuantity (type: decimal?)— ordered quantityUnitPrice (type: decimal?)— unit priceDefaultSalePrice (type: decimal?)— RRP/sale priceDiscountAmount (type: decimal?)— discount appliedTaxRate (type: decimal?)— tax rate percentageTaxAmount (type: decimal?)— calculated taxLineTotal (type: decimal?)— calculated line totalWarehouseId (type: long?)— fulfillment warehouse; set from price calculation when the line is added, overwritten with the actual warehouse once the line is picked (seePOST {id}/pick)EntityUserId (type: long?)— entity user who placed the lineNotes (type: string?)— free-text notesQuantityFulfilled (type: decimal?)— quantity dispatchedQuantityReturned (type: decimal?)— quantity returnedReturnedDate (type: DateTime?)— date of returnIsPicked (type: bool)— whether the item has been picked from the warehouseIsPacked (type: bool)— whether the item has been packed for dispatchIsCustomised (type: bool)— whether the item requires customisation
Notes
- Validation is enforced in the service layer, not via model attributes
- All operations are tenant-aware
- All state-changing operations are audited
- Internal errors are logged but not exposed to clients