# Edara API v3: Sales Orders

Read the [guide](../guide.md) first: authentication, paging, errors, rate limits and the shared `PagedResult`, `BatchResult` and `*Reference` shapes.

- `GET /v3/sales-orders`: Lists sales orders.
- `GET /v3/sales-orders/{id}`: Gets a sales order by id.
- `POST /v3/sales-orders`: Creates a sales order.
- `POST /v3/sales-orders/issued`: Creates an issued sales order.
- `PUT /v3/sales-orders/header`: Updates a sales order header.
- `PUT /v3/sales-orders/code/{code}`: Updates a sales order by code.
- `PATCH /v3/sales-orders/code/{code}/tags`: Updates sales order tags by code.
- `DELETE /v3/sales-orders/{id}`: Deletes a sales order by id.
- `PATCH /v3/sales-orders/{id}/cancel`: Cancels a sales order by id.
- `PATCH /v3/sales-orders/code/{code}/cancel`: Cancels a sales order by code.
- `PATCH /v3/sales-orders/code/{code}/status`: Updates sales order status by code.
- `PATCH /v3/sales-orders/code/{code}/unissue`: Unissues a sales order by code.
- `GET /v3/sales-orders/print-templates`: Lists print templates.
- `GET /v3/sales-orders/print-templates/{templateId}`: Gets a print template by id.
- `POST /v3/sales-orders/cash-in`: Records a sales order cash-in.

## Endpoints

### `GET /v3/sales-orders`: Lists sales orders.

Operation `GetSalesOrders` · permission `read:sales-order`

> - `offset` is zero-based; `limit` defaults to 100.
> - `orderDateFrom` and `orderDateTo` filter on or after / on or before the document date (inclusive bounds).

**Query string**: `GetSalesOrdersQuery`

| field | type | default | validation | description |
|---|---|---|---|---|
| `offset` | `int?` | 0 | >= 0 | Number of items to skip before returning results. Defaults to 0. |
| `limit` | `int?` | 100 | 1..1000 (out of range → 400) | Maximum number of items to return. Defaults to 100, maximum 1000. |
| `documentCode` | `string` |  |  | Document code filter. |
| `paperNumber` | `string` |  |  | Paper/reference number filter. |
| `warehouseId` | `int?` |  |  | Warehouse id filter. |
| `salesPersonId` | `int?` |  |  | Sales person id filter. |
| `tags` | `List<string>` |  |  | Tags filter. |
| `orderDateFrom` | `DateTime?` |  |  | Inclusive lower bound of the document date range. |
| `orderDateTo` | `DateTime?` |  |  | Inclusive upper bound of the document date range. |
| `orderStatus` | `SalesOrderStatus?` |  |  | Order status filter. |
| `createdBy` | `string` |  |  | Creator filter. |
| `warehouseOnly` | `bool` | true |  | Whether to limit to warehouse orders only. |
| `onlyMyOrders` | `bool` | true |  | Whether to limit results to orders created by the current user. Defaults to true. |
| `customerId` | `int?` |  |  | Customer id filter. |
| `paymentStatus` | `SalesOrderPaymentStatus?` |  |  | Payment status filter. |

**Responses**: OK `PagedResult<SalesOrderHeaderListResponse>`: Paged list of sales orders.

**Notes**

- `onlyMyOrders` defaults to true, so by default you get only the orders the calling user created. Set it to false to get orders from all users.
- `onlyMyOrders` goes by the user who created the order, not by the order's sales person.
- `warehouseOnly` defaults to true, which leaves out sales store (point of sale) orders. Set it to false to include them.
- You only get orders in warehouses or sales stores that the calling user has data permission for. A user with no such permission gets an empty list, not `403`.
- `documentCode`, `salesPersonId`, `warehouseId`, `customerId` and `orderStatus` are exact matches. `paperNumber` is a partial match.
- `orderDateFrom` and `orderDateTo` compare the full date and time. An `orderDateTo` without a time means the start of that day, so send the end of the day to include it.
- `createdBy` is an exact match on the email of the user who created the order.
- `tags` takes a comma-separated list and returns orders that have at least one of those tags. Spaces around each tag and blank tags are ignored.
- `paymentStatus` `Unpaid` also matches orders that have no payment status.
- An empty `documentCode`, `paperNumber` or `createdBy` is treated as no filter.
- The list can include sales returns that match your filters. Check `documentType` in the response to tell them apart.
- Results are ordered by `id`, highest first.
- `totalCount` counts all orders that match your filters, but it is 0 when the page you asked for is empty, for example past the last page.
- An order whose customer record no longer exists is not returned.
- Each order in the list includes its full details and installments.

---

### `GET /v3/sales-orders/{id}`: Gets a sales order by id.

Operation `GetSalesOrderById` · permission `read:sales-order`

**Parameters**

| name | in | type | default | description |
|---|---|---|---|---|
| `id` | route | `int` |  | The sales order id. |

**Responses**: OK `SalesOrderResponse`: The sales order.

**Notes**

- An id that belongs to a sales return is returned too. In that case the response carries `relatedSalesOrderCodes` instead of `relatedSalesReturnCodes`.
- `isApproved` is always true for an order that requires an automatic issue order. For other orders it shows whether the order has passed all approvals.
- For a sales order, the response also includes the codes of linked sales returns and the paid amount for each related document code.
- The response always includes the order lines and installments. The customer, sales person, warehouse and sales store are returned as reference objects.
- `paperNumber` falls back to the document number when the order has no paper number.

---

### `POST /v3/sales-orders`: Creates a sales order.

Operation `CreateSalesOrder` · permission `create:sales-order`

**Body**: `SalesOrderRequest`

| field | type | default | validation | description |
|---|---|---|---|---|
| `id` | `int?` |  |  | The sales order id (set on update only). |
| `orderStatus` | `SalesOrderStatus?` |  |  | The initial order status. |
| `paperNumber` | `string` |  |  | The paper number printed on the source document. |
| `runSheetId` | `string` |  |  | The run-sheet identifier when grouped for delivery. |
| `customerId` | `int?` |  | NotNull() .GreaterThan(0) | The id of the customer the order is issued to. Required. |
| `salesPersonId` | `int?` |  |  | The id of the sales person credited with the sale. |
| `warehouseId` | `int?` |  |  | The id of the warehouse the items are issued from. |
| `salesStoreId` | `int?` |  |  | The id of the sales store originating the order. |
| `shippmentCost` | `decimal?` |  |  | The shipment cost charged on the order. |
| `documentDate` | `DateTime?` |  | NotNull() | The document date. Required. |
| `shippingDate` | `DateTime?` |  |  | The planned shipping date. |
| `grossTotal` | `decimal?` |  |  | The gross total before discounts and tax. |
| `subTotal` | `decimal?` |  |  | The subtotal after item-level discounts, before order-level discount and tax. |
| `netTotal` | `decimal?` |  |  | The final payable amount. |
| `totalItemsDiscounts` | `decimal?` |  |  | The sum of all item-level discounts. |
| `discount` | `decimal?` |  |  | The order-level discount amount. |
| `discountRate` | `decimal?` |  |  | The order-level discount rate as a percentage. |
| `taxable` | `bool?` |  |  | Whether tax should be applied to the order. |
| `applyTaxAfterDiscount` | `bool?` |  |  | Whether tax is computed after applying the order-level discount. |
| `tax` | `decimal?` |  |  | The total tax amount. |
| `cashAmount` | `decimal?` |  |  | The amount paid in cash at order creation. |
| `onAccountAmount` | `decimal?` |  |  | The amount placed on the customer account (credit). |
| `cashPaid` | `decimal?` |  |  | The cash already collected against the order. |
| `currencyId` | `int?` |  |  | The id of the document currency. |
| `exchangeRate` | `decimal?` |  |  | The exchange rate against the system currency at the document date. |
| `channel` | `string` |  |  | The sales channel originating the order (e.g. web, POS). |
| `notes` | `string` |  |  | Free-form notes captured on the order. |
| `externalId` | `string` |  |  | An external identifier supplied by the caller for reconciliation. |
| `relatedWorkOrderCode` | `string` |  |  | The related work-order code, when one was already generated. |
| `relatedWorkOrderValue` | `decimal?` |  |  | The value of the related work order. |
| `tags` | `List<string>` |  |  | The tags to assign to the order. |
| `isApproved` | `bool?` |  |  | Whether the order is approved for fulfillment. |
| `costCenterId` | `int?` |  |  | The id of the cost center to associate with the order. |
| `requireAutoWorkorder` | `bool?` |  |  | Whether a work order should be auto-generated on issue. |
| `salesOrderDetails` | `List<SalesOrderDetailRequest>` | new List<SalesOrderDetailRequest>() | NotNull() .NotEmpty(); each: SetValidator(new SalesOrderDetailRequestValidator()) .When(x => x.SalesOrderDetails != null) | The line items on the order. Required, at least one. |
| `salesOrderInstallments` | `List<SalesOrderInstallmentRequest>` | new List<SalesOrderInstallmentRequest>() | each: SetValidator(new SalesOrderInstallmentRequestValidator()) .When(x => x.SalesOrderInstallments != null) | The payment installments scheduled against the order. |

**Responses**: Created `SalesOrderResponse`: Sales order created.

**Business errors** (HTTP 409, match on `errorCode`):

- `BundleNotFound`: A referenced bundle was not found.
- `CannotAssignInactiveTax`: An inactive tax cannot be assigned.
- `CannotSetCreditPaymentForCustomersOfTypeCashPayment`: Credit installments cannot be used for cash-payment customers.
- `CostCenterNotFound`: The specified cost center was not found.
- `CostCenterNotPermitted`: The user is not permitted to use the specified cost center.
- `CostCenterRequiredOnSalesDocument`: A cost center is required on the document.
- `CustomerNotFound`: The referenced customer was not found.
- `DuplicatedRefenceDocuments`: Another document already references the same source document.
- `DupplicatedCode`: Another sales order already uses the same document code.
- `DupplicatedDate`: Another sales order already exists with the same document date.
- `EdaraBusinessError`: A legacy business-rule violation occurred (see message for details).
- `EInvoice_Egypt_MissingCustomersAddress`: Egypt e-invoicing requires the customer address.
- `EInvoice_Egypt_MissingCustomersTaxRegistrationID`: Egypt e-invoicing requires the customer tax registration id.
- `EInvoice_KSA_AllDetailsMustBeTaxable`: KSA e-invoicing requires every detail line to be taxable.
- `EInvoice_KSA_BundleDetailsMustHaveSameTaxRate`: KSA e-invoicing requires all bundle items to share the same tax rate.
- `EInvoice_KSA_MissingShippingDate`: KSA e-invoicing requires a shipping date.
- `EInvoice_KSA_SalesOrderMustBeTaxable`: KSA e-invoicing requires the order to be taxable.
- `ExchangeRateDecimalsExceedLimit`: The exchange rate exceeds the allowed decimal precision.
- `InvalidTaxScopeForItem`: The tax scope is not valid for this item.
- `InvalidTaxTypeForItem`: The tax type is not valid for this item.
- `MustSelectValidTaxType`: A valid tax type must be selected.
- `MustSelectValidWHTaxType`: A valid withholding-tax type must be selected.
- `NoSystemCurrency`: A system currency must be configured before saving the document.
- `PreventSaveDocumentsInFutureDate`: The document date cannot be in the future.
- `PrventStockItemsExceedStoreQuotaLimit`: One or more items exceed the sales-store quota limit.
- `SerialAlreadyReserved`: One or more serials are already reserved on another document.
- `SO_PO_TaxHasBeenDeleted`: A tax referenced by the order has been deleted.
- `TaxNotFound`: The specified tax was not found.

**Notes**

- Send exactly one of `warehouseId` or `salesStoreId`. Sending both or neither returns `400`.
- A `customerId` that does not exist returns `400`, not `404`.
- Each line needs one of `stockItemId`, `serviceItemId` or `bundleId`.
- Line `comments` cannot contain a comma or a vertical bar.
- A line `taxId` must point to an existing, active tax. Otherwise the request fails with `TaxNotFound` or `CannotAssignInactiveTax`.
- The line price is never looked up from the item. If you omit `price`, `taxRate` or `itemDiscount`, 0 is stored.
- A line with a zero price is rejected unless an organization setting allows zero prices. An order with a zero total is always rejected.
- Depending on organization settings, a price below the item's sales price, minimum price or cost is rejected with `EdaraBusinessError`. This differs between organizations, so handle the error.
- `orderStatus` defaults to `Open` and `isApproved` defaults to false. A new order always has `isIssued` false and `documentType` `SO`.
- When the organization does not use multiple currencies, the currency you send is ignored and the exchange rate is stored as 1.
- An unknown stock item, service item or unit of measure id is rejected with `EdaraBusinessError`. So are duplicated stock item lines.
- A duplicate `paperNumber` is rejected only when an organization setting that prevents duplicate reference documents is on. The error is `DuplicatedRefenceDocuments` and it includes the code of the existing order.
- `externalId` must be unique. A request that reuses one returns `400` with a generic message and no `errorCode`, and no second order is created.
- A retry without `externalId` creates a second order.
- Line quantities must fit the non-reserved stock balance, otherwise the order is rejected. A created order reserves stock.
- Creating an order does not create an issue order or a cash-in.
- The document number is generated automatically unless you send one.
- The order is saved all or nothing: if any step fails, nothing is stored.
- A document code that already exists is rejected with `DupplicatedCode`.
- The response is the created order, including its lines and installments.

---

### `POST /v3/sales-orders/issued`: Creates an issued sales order.

Operation `CreateIssuedSalesOrder` · permission `create:sales-order`

> - Preserves the legacy issued-create flow; the order is marked issued immediately on insert.
> - The request validation matches the standard create endpoint, except that detail quantities are not required to be positive.

**Body**: `SalesOrderRequest`

| field | type | default | validation | description |
|---|---|---|---|---|
| `id` | `int?` |  |  | The sales order id (set on update only). |
| `orderStatus` | `SalesOrderStatus?` |  |  | The initial order status. |
| `paperNumber` | `string` |  |  | The paper number printed on the source document. |
| `runSheetId` | `string` |  |  | The run-sheet identifier when grouped for delivery. |
| `customerId` | `int?` |  | NotNull() .GreaterThan(0) | The id of the customer the order is issued to. Required. |
| `salesPersonId` | `int?` |  |  | The id of the sales person credited with the sale. |
| `warehouseId` | `int?` |  |  | The id of the warehouse the items are issued from. |
| `salesStoreId` | `int?` |  |  | The id of the sales store originating the order. |
| `shippmentCost` | `decimal?` |  |  | The shipment cost charged on the order. |
| `documentDate` | `DateTime?` |  | NotNull() | The document date. Required. |
| `shippingDate` | `DateTime?` |  |  | The planned shipping date. |
| `grossTotal` | `decimal?` |  |  | The gross total before discounts and tax. |
| `subTotal` | `decimal?` |  |  | The subtotal after item-level discounts, before order-level discount and tax. |
| `netTotal` | `decimal?` |  |  | The final payable amount. |
| `totalItemsDiscounts` | `decimal?` |  |  | The sum of all item-level discounts. |
| `discount` | `decimal?` |  |  | The order-level discount amount. |
| `discountRate` | `decimal?` |  |  | The order-level discount rate as a percentage. |
| `taxable` | `bool?` |  |  | Whether tax should be applied to the order. |
| `applyTaxAfterDiscount` | `bool?` |  |  | Whether tax is computed after applying the order-level discount. |
| `tax` | `decimal?` |  |  | The total tax amount. |
| `cashAmount` | `decimal?` |  |  | The amount paid in cash at order creation. |
| `onAccountAmount` | `decimal?` |  |  | The amount placed on the customer account (credit). |
| `cashPaid` | `decimal?` |  |  | The cash already collected against the order. |
| `currencyId` | `int?` |  |  | The id of the document currency. |
| `exchangeRate` | `decimal?` |  |  | The exchange rate against the system currency at the document date. |
| `channel` | `string` |  |  | The sales channel originating the order (e.g. web, POS). |
| `notes` | `string` |  |  | Free-form notes captured on the order. |
| `externalId` | `string` |  |  | An external identifier supplied by the caller for reconciliation. |
| `relatedWorkOrderCode` | `string` |  |  | The related work-order code, when one was already generated. |
| `relatedWorkOrderValue` | `decimal?` |  |  | The value of the related work order. |
| `tags` | `List<string>` |  |  | The tags to assign to the order. |
| `isApproved` | `bool?` |  |  | Whether the order is approved for fulfillment. |
| `costCenterId` | `int?` |  |  | The id of the cost center to associate with the order. |
| `requireAutoWorkorder` | `bool?` |  |  | Whether a work order should be auto-generated on issue. |
| `salesOrderDetails` | `List<SalesOrderDetailRequest>` | new List<SalesOrderDetailRequest>() | NotNull() .NotEmpty(); each: SetValidator(new SalesOrderDetailRequestValidator()) .When(x => x.SalesOrderDetails != null) | The line items on the order. Required, at least one. |
| `salesOrderInstallments` | `List<SalesOrderInstallmentRequest>` | new List<SalesOrderInstallmentRequest>() | each: SetValidator(new SalesOrderInstallmentRequestValidator()) .When(x => x.SalesOrderInstallments != null) | The payment installments scheduled against the order. |

**Responses**: Created `SalesOrderResponse`: Sales order created.

**Business errors** (HTTP 409, match on `errorCode`):

- `BundleNotFound`: A referenced bundle was not found.
- `CannotAssignInactiveTax`: An inactive tax cannot be assigned.
- `CannotSetCreditPaymentForCustomersOfTypeCashPayment`: Credit installments cannot be used for cash-payment customers.
- `CostCenterNotFound`: The specified cost center was not found.
- `CostCenterNotPermitted`: The user is not permitted to use the specified cost center.
- `CostCenterRequiredOnSalesDocument`: A cost center is required on the document.
- `CustomerNotFound`: The referenced customer was not found.
- `DuplicatedRefenceDocuments`: Another document already references the same source document.
- `DupplicatedCode`: Another sales order already uses the same document code.
- `DupplicatedDate`: Another sales order already exists with the same document date.
- `EdaraBusinessError`: A legacy business-rule violation occurred (see message for details).
- `EInvoice_Egypt_MissingCustomersAddress`: Egypt e-invoicing requires the customer address.
- `EInvoice_Egypt_MissingCustomersTaxRegistrationID`: Egypt e-invoicing requires the customer tax registration id.
- `EInvoice_KSA_AllDetailsMustBeTaxable`: KSA e-invoicing requires every detail line to be taxable.
- `EInvoice_KSA_BundleDetailsMustHaveSameTaxRate`: KSA e-invoicing requires all bundle items to share the same tax rate.
- `EInvoice_KSA_MissingShippingDate`: KSA e-invoicing requires a shipping date.
- `EInvoice_KSA_SalesOrderMustBeTaxable`: KSA e-invoicing requires the order to be taxable.
- `ExchangeRateDecimalsExceedLimit`: The exchange rate exceeds the allowed decimal precision.
- `InvalidTaxScopeForItem`: The tax scope is not valid for this item.
- `InvalidTaxTypeForItem`: The tax type is not valid for this item.
- `MustSelectValidTaxType`: A valid tax type must be selected.
- `MustSelectValidWHTaxType`: A valid withholding-tax type must be selected.
- `NoSystemCurrency`: A system currency must be configured before saving the document.
- `PreventSaveDocumentsInFutureDate`: The document date cannot be in the future.
- `PrventStockItemsExceedStoreQuotaLimit`: One or more items exceed the sales-store quota limit.
- `SerialAlreadyReserved`: One or more serials are already reserved on another document.
- `SO_PO_TaxHasBeenDeleted`: A tax referenced by the order has been deleted.
- `TaxNotFound`: The specified tax was not found.

**Notes**

- Validation, defaults and duplicate rules are the same as for creating a sales order. A line quantity of zero or less is rejected.
- An issue order is always created from the stock item lines, whatever the organization's automatic issue setting is. It is saved together with the sales order: if either fails, nothing is stored.
- After the issue order is created, `orderStatus` is `Shipped` and `issueDate` is the current time.
- `isIssued` is true only when everything ordered on the stock item lines was issued. Check `isIssued` in the response.
- When an organization setting for selling over stock on automatically issued orders is on, lines are first adjusted to the available stock, so the order can be partially issued.
- An order with only service items gets no issue order. A services invoice is posted instead, and the order is marked as issued with `orderStatus` `Shipped`.
- When `isWithholdingTax` is true and `withholdingTax` is greater than 0, a withholding tax journal entry is also created.
- No cash-in is created.

---

### `PUT /v3/sales-orders/header`: Updates a sales order header.

Operation `UpdateSalesOrderHeader` · permission `update:sales-order`

**Body**: `SalesOrderRequest`

| field | type | default | validation | description |
|---|---|---|---|---|
| `id` | `int?` |  |  | The sales order id (set on update only). |
| `orderStatus` | `SalesOrderStatus?` |  |  | The initial order status. |
| `paperNumber` | `string` |  |  | The paper number printed on the source document. |
| `runSheetId` | `string` |  |  | The run-sheet identifier when grouped for delivery. |
| `customerId` | `int?` |  | NotNull() .GreaterThan(0) | The id of the customer the order is issued to. Required. |
| `salesPersonId` | `int?` |  |  | The id of the sales person credited with the sale. |
| `warehouseId` | `int?` |  |  | The id of the warehouse the items are issued from. |
| `salesStoreId` | `int?` |  |  | The id of the sales store originating the order. |
| `shippmentCost` | `decimal?` |  |  | The shipment cost charged on the order. |
| `documentDate` | `DateTime?` |  | NotNull() | The document date. Required. |
| `shippingDate` | `DateTime?` |  |  | The planned shipping date. |
| `grossTotal` | `decimal?` |  |  | The gross total before discounts and tax. |
| `subTotal` | `decimal?` |  |  | The subtotal after item-level discounts, before order-level discount and tax. |
| `netTotal` | `decimal?` |  |  | The final payable amount. |
| `totalItemsDiscounts` | `decimal?` |  |  | The sum of all item-level discounts. |
| `discount` | `decimal?` |  |  | The order-level discount amount. |
| `discountRate` | `decimal?` |  |  | The order-level discount rate as a percentage. |
| `taxable` | `bool?` |  |  | Whether tax should be applied to the order. |
| `applyTaxAfterDiscount` | `bool?` |  |  | Whether tax is computed after applying the order-level discount. |
| `tax` | `decimal?` |  |  | The total tax amount. |
| `cashAmount` | `decimal?` |  |  | The amount paid in cash at order creation. |
| `onAccountAmount` | `decimal?` |  |  | The amount placed on the customer account (credit). |
| `cashPaid` | `decimal?` |  |  | The cash already collected against the order. |
| `currencyId` | `int?` |  |  | The id of the document currency. |
| `exchangeRate` | `decimal?` |  |  | The exchange rate against the system currency at the document date. |
| `channel` | `string` |  |  | The sales channel originating the order (e.g. web, POS). |
| `notes` | `string` |  |  | Free-form notes captured on the order. |
| `externalId` | `string` |  |  | An external identifier supplied by the caller for reconciliation. |
| `relatedWorkOrderCode` | `string` |  |  | The related work-order code, when one was already generated. |
| `relatedWorkOrderValue` | `decimal?` |  |  | The value of the related work order. |
| `tags` | `List<string>` |  |  | The tags to assign to the order. |
| `isApproved` | `bool?` |  |  | Whether the order is approved for fulfillment. |
| `costCenterId` | `int?` |  |  | The id of the cost center to associate with the order. |
| `requireAutoWorkorder` | `bool?` |  |  | Whether a work order should be auto-generated on issue. |
| `salesOrderDetails` | `List<SalesOrderDetailRequest>` | new List<SalesOrderDetailRequest>() | NotNull() .NotEmpty(); each: SetValidator(new SalesOrderDetailRequestValidator()) .When(x => x.SalesOrderDetails != null) | The line items on the order. Required, at least one. |
| `salesOrderInstallments` | `List<SalesOrderInstallmentRequest>` | new List<SalesOrderInstallmentRequest>() | each: SetValidator(new SalesOrderInstallmentRequestValidator()) .When(x => x.SalesOrderInstallments != null) | The payment installments scheduled against the order. |

**Responses**: OK `SalesOrderResponse`: The updated sales order. · NotFound `ProblemDetails`: Sales order not found.

**Business errors** (HTTP 409, match on `errorCode`):

- `EdaraBusinessError`: A legacy business-rule violation occurred (see message for details).
- `ExchangeRateDecimalsExceedLimit`: The exchange rate exceeds the allowed decimal precision.
- `NoSystemCurrency`: A system currency must be configured before saving the document.
- `PhantomRead`: The sales order was modified by another user; reload and retry.
- `PreventSaveDocumentsInFutureDate`: The document date cannot be in the future.

**Notes**

- The order is identified by `id` in the request body, not in the URL. An id that does not exist returns `404`.
- Despite the PUT method, this is a partial update: a field you omit or send as null keeps its stored value. A blank string also keeps the old value, so you cannot clear a field.
- Tags are the exception: any tag list you send, including an empty one, replaces the whole tag set.
- Only the header changes. Lines and installments are never touched.
- `isApproved`, `externalId`, `requireAutoWorkorder`, `shippmentCost` and the related work order code and value are accepted but not saved. The request still returns `200`, with those values unchanged.
- Concurrent updates are not detected: the last write wins. The `PhantomRead` error is not returned by this endpoint.
- An exchange rate with more than 4 decimal places is rejected with `ExchangeRateDecimalsExceedLimit`.
- When the organization does not allow saving documents with a future date, a document date up to 60 seconds ahead (the default tolerance) is changed to the current time. A later date is rejected with `PreventSaveDocumentsInFutureDate`.
- Setting `orderStatus` to `Cancelled` here gives back the sales store's consumed quota and the related customer purchase order's remaining amount, but does not release reserved stock. To cancel an order and recalculate reserved stock, use the cancel endpoints.
- If the order is issued, the update can regenerate its sales invoice number, depending on organization settings.
- The response is read back after the update, so it shows what was actually stored.

---

### `PUT /v3/sales-orders/code/{code}`: Updates a sales order by code.

Operation `UpdateSalesOrderByCode` · permission `update:sales-order`

> - When `SalesOrderDetails` is supplied, the full document (header + details + installments) is replaced.

**Parameters**

| name | in | type | default | description |
|---|---|---|---|---|
| `code` | route | `string` |  | The sales order code. |

**Body**: `SalesOrderRequest`

| field | type | default | validation | description |
|---|---|---|---|---|
| `id` | `int?` |  |  | The sales order id (set on update only). |
| `orderStatus` | `SalesOrderStatus?` |  |  | The initial order status. |
| `paperNumber` | `string` |  |  | The paper number printed on the source document. |
| `runSheetId` | `string` |  |  | The run-sheet identifier when grouped for delivery. |
| `customerId` | `int?` |  | NotNull() .GreaterThan(0) | The id of the customer the order is issued to. Required. |
| `salesPersonId` | `int?` |  |  | The id of the sales person credited with the sale. |
| `warehouseId` | `int?` |  |  | The id of the warehouse the items are issued from. |
| `salesStoreId` | `int?` |  |  | The id of the sales store originating the order. |
| `shippmentCost` | `decimal?` |  |  | The shipment cost charged on the order. |
| `documentDate` | `DateTime?` |  | NotNull() | The document date. Required. |
| `shippingDate` | `DateTime?` |  |  | The planned shipping date. |
| `grossTotal` | `decimal?` |  |  | The gross total before discounts and tax. |
| `subTotal` | `decimal?` |  |  | The subtotal after item-level discounts, before order-level discount and tax. |
| `netTotal` | `decimal?` |  |  | The final payable amount. |
| `totalItemsDiscounts` | `decimal?` |  |  | The sum of all item-level discounts. |
| `discount` | `decimal?` |  |  | The order-level discount amount. |
| `discountRate` | `decimal?` |  |  | The order-level discount rate as a percentage. |
| `taxable` | `bool?` |  |  | Whether tax should be applied to the order. |
| `applyTaxAfterDiscount` | `bool?` |  |  | Whether tax is computed after applying the order-level discount. |
| `tax` | `decimal?` |  |  | The total tax amount. |
| `cashAmount` | `decimal?` |  |  | The amount paid in cash at order creation. |
| `onAccountAmount` | `decimal?` |  |  | The amount placed on the customer account (credit). |
| `cashPaid` | `decimal?` |  |  | The cash already collected against the order. |
| `currencyId` | `int?` |  |  | The id of the document currency. |
| `exchangeRate` | `decimal?` |  |  | The exchange rate against the system currency at the document date. |
| `channel` | `string` |  |  | The sales channel originating the order (e.g. web, POS). |
| `notes` | `string` |  |  | Free-form notes captured on the order. |
| `externalId` | `string` |  |  | An external identifier supplied by the caller for reconciliation. |
| `relatedWorkOrderCode` | `string` |  |  | The related work-order code, when one was already generated. |
| `relatedWorkOrderValue` | `decimal?` |  |  | The value of the related work order. |
| `tags` | `List<string>` |  |  | The tags to assign to the order. |
| `isApproved` | `bool?` |  |  | Whether the order is approved for fulfillment. |
| `costCenterId` | `int?` |  |  | The id of the cost center to associate with the order. |
| `requireAutoWorkorder` | `bool?` |  |  | Whether a work order should be auto-generated on issue. |
| `salesOrderDetails` | `List<SalesOrderDetailRequest>` | new List<SalesOrderDetailRequest>() | NotNull() .NotEmpty(); each: SetValidator(new SalesOrderDetailRequestValidator()) .When(x => x.SalesOrderDetails != null) | The line items on the order. Required, at least one. |
| `salesOrderInstallments` | `List<SalesOrderInstallmentRequest>` | new List<SalesOrderInstallmentRequest>() | each: SetValidator(new SalesOrderInstallmentRequestValidator()) .When(x => x.SalesOrderInstallments != null) | The payment installments scheduled against the order. |

**Responses**: OK `SalesOrderResponse`: The updated sales order. · NotFound `ProblemDetails`: Sales order not found.

**Business errors** (HTTP 409, match on `errorCode`):

- `CannotAssignInactiveTax`: An inactive tax cannot be assigned.
- `CannotDeleteOrderAsItHasRelatedManualDocuments`: The order has related manual documents that prevent the update.
- `DuplicatedRefenceDocuments`: Another document already references the same source document.
- `EdaraBusinessError`: A legacy business-rule violation occurred (see message for details).
- `ExchangeRateDecimalsExceedLimit`: The exchange rate exceeds the allowed decimal precision.
- `InvalidTaxScopeForItem`: The tax scope is not valid for this item.
- `InvalidTaxTypeForItem`: The tax type is not valid for this item.
- `MustSelectValidTaxType`: A valid tax type must be selected.
- `MustSelectValidWHTaxType`: A valid withholding-tax type must be selected.
- `NoSystemCurrency`: A system currency must be configured before saving the document.
- `PhantomRead`: The sales order was modified by another user; reload and retry.
- `PreventSaveDocumentsInFutureDate`: The document date cannot be in the future.
- `PrventStockItemsExceedStoreQuotaLimit`: One or more items exceed the sales-store quota limit.
- `SerialAlreadyReserved`: One or more serials are already reserved on another document.
- `StockItemAlreadyReturned`: A detail line targets an item that has already been returned.
- `TaxNotFound`: The specified tax was not found.

**Notes**

- `code` is trimmed and then matched exactly. An unknown code returns `404`.
- Header fields are a partial update, as in the header endpoint: a field you omit or send as null keeps its stored value.
- When `salesOrderDetails` is omitted or empty, only the header is updated. Lines and installments stay as they are, and reserved stock is not recalculated.
- When `salesOrderDetails` has lines, it replaces the stored lines: a stored line whose `id` is not in the request is deleted, and a line without `id` is added as new. Send every line you want to keep.
- In a full update, a business rule failure returns its specific `errorCode` when there is one, and `EdaraBusinessError` otherwise.
- The response is the order read back after the update.

---

### `PATCH /v3/sales-orders/code/{code}/tags`: Updates sales order tags by code.

Operation `UpdateSalesOrderTags` · permission `update:sales-order`

> - Supplying an empty array clears all tags.

**Parameters**

| name | in | type | default | description |
|---|---|---|---|---|
| `code` | route | `string` |  | The sales order code. |

**Body**: `SalesOrderTagsUpdateRequest`

| field | type | default | validation | description |
|---|---|---|---|---|
| `tags` | `List<string>` |  |  | The full set of tags to assign to the order (replaces any existing tags). |

**Responses**: OK `SalesOrderResponse`: The updated sales order. · NotFound `ProblemDetails`: Sales order not found.

**Business errors** (HTTP 409, match on `errorCode`):

- `PhantomRead`: The sales order was modified by another user; reload and retry.

**Notes**

- `code` is trimmed and matched exactly. An unknown code returns `404`, and a request without a body returns `400`.
- The tags you send replace the whole tag set. Nothing is merged with the existing tags, and an empty list clears them.
- Tags can be changed whatever the order status is, including on cancelled and issued orders.

---

### `DELETE /v3/sales-orders/{id}`: Deletes a sales order by id.

Operation `DeleteSalesOrder` · permission `delete:sales-order`

**Parameters**

| name | in | type | default | description |
|---|---|---|---|---|
| `id` | route | `int` |  | The sales order id. |

**Responses**: NoContent: (not declared; read from the method body)

**Business errors** (HTTP 409, match on `errorCode`):

- `CannotDeleteOrderAsItHasRelatedManualDocuments`: The order has related manual accounting documents (CI/NR/SI/JE) that prevent deletion.

**Notes**

- A successful delete returns `204` with no body. An id that does not exist returns `404`.
- The delete is rejected with `CannotDeleteOrderAsItHasRelatedManualDocuments` when the order has a posted related accounting document, such as a cash-in, sales invoice or journal entry, or related documents that were created manually. The error names the blocking document code.
- The delete is permanent. It also removes the order's lines and installments and the related sales invoice, issue order and cash-in, together with their journal postings.
- Stock reserved by the order is released.
- If the order is still referenced elsewhere, the delete fails with `ItemCannotDeleteItInUse`.
- The order status is not checked: issued and cancelled orders can be deleted.

---

### `PATCH /v3/sales-orders/{id}/cancel`: Cancels a sales order by id.

Operation `CancelSalesOrderById` · permission `update:sales-order`

**Parameters**

| name | in | type | default | description |
|---|---|---|---|---|
| `id` | route | `int` |  | The sales order id. |

**Responses**: OK `SalesOrderActionResponse`: The cancelled sales order.

**Business errors** (HTTP 409, match on `errorCode`):

- `CannotDeleteOrderAsItHasRelatedManualDocuments`: The order has related manual accounting documents that prevent cancellation.

**Notes**

- An order whose `orderStatus` is `Processing` or `Shipped` cannot be cancelled: the request returns `409` with `SalesOrderIsInProcessingOrShipped`.
- Every other status is accepted, including `OutForDelivery`, `Returned` and an order that is already `Cancelled`. Only the status is checked, not whether the order is issued.
- An order with manually created related documents returns `409` with `CannotDeleteOrderAsItHasRelatedManualDocuments` and the code of the blocking document.
- Cancelling only sets `orderStatus` to `Cancelled`. The order is not deleted, and `isIssued`, the lines and any related sales invoice or issue order stay unchanged.
- Reserved stock is recalculated. When the status really changes to `Cancelled`, the sales store's consumed quota and the related customer purchase order's remaining amount are given back.
- The response is a short confirmation with the order code, the status `Cancelled` and the change time in UTC. It is not the full order.

---

### `PATCH /v3/sales-orders/code/{code}/cancel`: Cancels a sales order by code.

Operation `CancelSalesOrderByCode` · permission `update:sales-order`

**Parameters**

| name | in | type | default | description |
|---|---|---|---|---|
| `code` | route | `string` |  | The sales order code. |

**Responses**: OK `SalesOrderActionResponse`: The cancelled sales order. · NotFound `ProblemDetails`: Sales order not found or not in a cancellable state.

**Business errors** (HTTP 409, match on `errorCode`):

- `CannotDeleteOrderAsItHasRelatedManualDocuments`: The order has related manual accounting documents that prevent cancellation.

**Notes**

- This works like cancelling by id, except that the order is found by `code`, which is trimmed and matched exactly.
- The same rules apply: an order whose `orderStatus` is `Processing` or `Shipped` returns `409` with `SalesOrderIsInProcessingOrShipped`, and a successful cancel has the same effects.

---

### `PATCH /v3/sales-orders/code/{code}/status`: Updates sales order status by code.

Operation `UpdateSalesOrderStatus` · permission `update:sales-order`

> - Accepted values: `Cancelled`, `Confirmed`, `OutForDelivery`.

**Parameters**

| name | in | type | default | description |
|---|---|---|---|---|
| `code` | route | `string` |  | The sales order code. |

**Body**: `UpdateSalesOrderStatusRequest`

| field | type | default | validation | description |
|---|---|---|---|---|
| `status` | `UpdateableSalesOrderStatus?` |  | NotNull() | Target status. |

**Responses**: OK `SalesOrderActionResponse`: The updated sales order. · NotFound `ProblemDetails`: Sales order not found.

**Business errors** (HTTP 409, match on `errorCode`):

- `ExchangeRateDecimalsExceedLimit`: The exchange rate exceeds the allowed decimal precision.

**Notes**

- Any status value is accepted: `Cancelled`, `Confirmed`, `Pending`, `Processing`, `OutForDelivery`, `Shipped`, `Returned` or `Open`.
- Transitions are not validated, so a change such as `Shipped` to `Confirmed` or `Cancelled` to `Open` is stored as sent. Validate transitions in your own code.
- Only `orderStatus` changes. `isIssued` stays as it is, and reserved stock is recalculated.
- Setting `Cancelled` from another status also gives back the sales store's consumed quota and the related customer purchase order's remaining amount.
- The response is a short confirmation with the order code, the status you sent and the change time in UTC. It is not read back from the stored order.

---

### `PATCH /v3/sales-orders/code/{code}/unissue`: Unissues a sales order by code.

Operation `UnIssueSalesOrderByCode` · permission `update:sales-order`

**Parameters**

| name | in | type | default | description |
|---|---|---|---|---|
| `code` | route | `string` |  | The sales order code. |

**Responses**: OK `SalesOrderActionResponse`: The unissued sales order. · NotFound `ProblemDetails`: Sales order not found.

**Business errors** (HTTP 409, match on `errorCode`):

- `PhantomRead`: The sales order was modified by another user; reload and retry.

**Notes**

- `code` is trimmed and matched exactly. A blank code returns `400` and an unknown code returns `404`.
- If the related sales invoice is posted, the request returns `400` with no `errorCode`. Unpost the invoice first.
- An unposted related sales invoice is deleted. This happens as a separate step before the order is updated, so the invoice stays deleted even if the order update then fails.
- The endpoint does not check that the order is issued or what status it has. It always sets `isIssued` to false and `orderStatus` to `Confirmed`, so a cancelled or shipped order also becomes `Confirmed`.
- Reserved stock is recalculated.
- The response reports the status as `Unissued`, but the stored `orderStatus` is `Confirmed`.

---

### `GET /v3/sales-orders/print-templates`: Lists print templates.

Operation `GetSalesOrderPrintTemplates` · permission `print:sales-order`

**Query string**: `GetSalesOrderPrintTemplatesQuery`

| field | type | default | validation | description |
|---|---|---|---|---|
| `offset` | `int?` | 0 | **no validator**: negative silently becomes 0 | Number of items to skip before returning results. Defaults to 0. |
| `limit` | `int?` | 100 | **no validator**: out of 1..1000 silently becomes 100 | Maximum number of items to return. Defaults to 100, maximum 1000. |
| `updateDate` | `DateTime?` |  |  | Inclusive lower bound of the print-template update date. |

**Responses**: OK `PagedResult<SalesOrderPrintTemplateResponse>`: Paged list of print templates.

**Notes**

- The list always returns sales order templates for the small paper size. You cannot choose another module, page or paper size: the only parameters are `offset`, `limit` and `updateDate`.
- Only templates the calling user has permission for are returned. The others are left out without an error.
- Pre-printed templates are never returned.
- `updateDate` returns templates created or updated strictly after the value, not on it. A date without a time means the start of that day.
- `limit` defaults to 100. A value below 1 or above 1000 does not return `400`: it is replaced by 100.
- Results are ordered by id.
- An `offset` past the end returns an empty list and a total count of 0, not the real total.
- Each item includes `htmlContent` and `printTemplateDetails`, a list of key and value pairs.
- `direction` is always `ltr` in this list. It does not reflect the template's real text direction.

---

### `GET /v3/sales-orders/print-templates/{templateId}`: Gets a print template by id.

Operation `GetSalesOrderPrintTemplateById` · permission `print:sales-order`

**Parameters**

| name | in | type | default | description |
|---|---|---|---|---|
| `templateId` | route | `int` |  | The print template id. |

**Responses**: OK `SalesOrderPrintTemplateResponse`: The print template. · NotFound `ProblemDetails`: Print template not found.

**Notes**

- A `templateId` that does not exist or is not an integer returns `404`. Zero or a negative value returns `400`.
- `htmlContent` is always null on this endpoint. Use the list endpoint to get a template's HTML.

---

### `POST /v3/sales-orders/cash-in`: Records a sales order cash-in.

Operation `CreateCashInForSalesOrder` · permission `create:cash-transaction`

**Body**: `CashInForSalesOrderRequest`

| field | type | default | validation | description |
|---|---|---|---|---|
| `documentCode` | `string` |  |  | Generated accounting document code. |
| `paperNumber` | `string` |  |  | Paper or external reference number. |
| `salesPersonId` | `int?` |  |  | Sales person id. |
| `documentDate` | `DateTime?` |  |  | Accounting document date. |
| `paidAmount` | `decimal?` |  | NotNull() .GreaterThan(0) | Amount paid. Required. |
| `relatedSalesOrderCode` | `string` |  | NotEmpty() | Sales-order code the receipt is posted against. Required. |
| `cashAccountId` | `int?` |  | NotNull() .GreaterThan(0) | Cash account id receiving the payment. Required. |
| `notes` | `string` |  |  | Additional notes. |

**Responses**: Created `CashTransactionResponse`: Cash-in document created. · NotFound `ProblemDetails`: Related sales order or cash account not found.

**Business errors** (HTTP 409, match on `errorCode`):

- `CannotInsertDocumentBeforeClosingDate`: The document date is before the closing date.
- `CK_NoCustomerOrSupplierSelectedforAccount`: The selected account requires a linked customer or supplier, but none was provided.
- `CK_NocustomerSelectedforAR`: The selected receivable account requires a linked customer, but none was provided.
- `CK_NoSupplierSelectedforAP`: The selected payable account requires a linked supplier, but none was provided.
- `CustomerHaveNoRelatedAccount`: The selected customer has no related GL account; raised when the journal entry for the cash-in is created (AccDocumentLogic).
- `DocumentmainAccountHasNoAlias`: The selected main account does not have the cash-account alias required by this document type.
- `DupplicatedCode`: Another document already uses the same document code.
- `DupplicatedDate`: Another document already exists with the same document date.
- `ExchangeRateDecimalsExceedLimit`: The exchange rate has too many decimal places.
- `PreventSaveDocumentsInFutureDate`: The document date is in the future.
- `RelatedSalesOrderCancelled`: Occurs when the linked sales order is already canceled, so the payment journal entry cannot be created from it.

**Notes**

- An unknown `cashAccountId` or `relatedSalesOrderCode` returns `404`.
- `documentDate` defaults to the current time. The customer is always taken from the sales order, not from the request.
- The response is `201` with the `documentCode` of the new cash-in.
- A `documentDate` in the future is always rejected with `PreventSaveDocumentsInFutureDate`, whatever the organization's future-date setting is.
- The cash-in creates a journal entry for `paidAmount` that debits the cash account and credits the customer's related account.
- The customer must have a related accounts receivable account. Otherwise the request fails with `CustomerHaveNoRelatedAccount`.
- Whether the journal entry is posted immediately or in a batch depends on an organization setting.
- A cancelled sales order is rejected with `RelatedSalesOrderCancelled`.
- The cash-in is saved all or nothing: if any step fails, nothing is recorded.

## Types

### `AccountReference`

Lightweight account reference (id + code + name).

| field | type | description |
|---|---|---|
| `id` | `int` | The account id. |
| `code` | `string` | The account code. |
| `name` | `string` | The account name. |

### `CashTransactionResponse`

Response describing a created cash transaction.

| field | type | description |
|---|---|---|
| `documentCode` | `string` | The generated accounting document code. |

### `CurrencyReference`

Lightweight currency reference (id + ISO code + name).

| field | type | description |
|---|---|---|
| `id` | `int` | The currency id. |
| `code` | `string` | The ISO 4217 currency code. |
| `name` | `string` | The currency name. |

### `CustomerReference`

Lightweight customer reference (id + code + name).

| field | type | description |
|---|---|---|
| `id` | `int` | The customer id. |
| `code` | `string` | The customer code. |
| `name` | `string` | The customer name. |

### `DocumentType`

Document type codes shared across sales, purchase, and warehouse domains.

Values (sent/returned as the name): `RS`, `RR`, `RT`, `IO`, `IR`, `IT`, `TR`, `CI`, `CO`, `JE`, `SI`, `PI`, `CN`, `DN`, `NR`, `NP`, `SO`, `SR`, `RecSO`, `PO`, `PR`, `OB`, `RMA`

### `InstallmentPaymentType`

Payment method for a sales-order installment.

Values (sent/returned as the name): `Cash`=0, `Installments`=1

### `SalesOrderActionResponse`

Response describing a sales-order state transition (issue / cancel / un-issue).

| field | type | description |
|---|---|---|
| `salesOrderCode` | `string` | The sales-order code the action was performed on. |
| `status` | `string` | The new status after the action. |
| `changedAt` | `DateTime` | The timestamp at which the action was applied. |

### `SalesOrderDetailRequest`

Request to create or update a sales-order line item.

| field | type | validation | description |
|---|---|---|---|
| `id` | `int?` |  | The line id (set on update only). |
| `quantity` | `decimal?` | GreaterThan(0) .When(x => x.Quantity.HasValue) | The quantity ordered, expressed in the line's unit of measure. Required. |
| `price` | `decimal?` | GreaterThanOrEqualTo(0) .When(x => x.Price.HasValue) | The unit price for the line. Required. |
| `taxRate` | `decimal?` |  | The tax rate applied to the line as a percentage. |
| `taxId` | `int?` |  | The id of the tax applied to the line. |
| `withholdingTaxId` | `int?` |  | The id of the withholding tax applied to the line. |
| `itemDiscount` | `decimal?` |  | The item-level discount value (interpreted per `ItemDiscountType`). |
| `itemDiscountType` | `DiscountType?` |  | Whether the item discount is a flat amount or a percentage. |
| `warehouseId` | `int?` |  | The id of the warehouse the line is issued from. |
| `bundleId` | `int?` |  | The bundle id when the line is part of a bundle. |
| `serviceItemId` | `int?` |  | The service-item id when the line is a service. |
| `stockItemId` | `int?` |  | The stock-item id when the line is a stock item. |
| `stockItemCode` | `string` |  | The stock-item code (alternative key when `StockItemId` is not supplied). |
| `stockItemDescription` | `string` |  | The stock-item description (echoed on the response; not a lookup key). |
| `unitOfMeasureId` | `int?` |  | The id of the unit of measure the quantity is expressed in. |
| `batchNumber` | `string` |  | The batch number, when the stock item is batch-tracked. |
| `expiryDate` | `DateTime?` |  | The batch expiry date, when applicable. |
| `comments` | `string` | Must(c => !c.Contains(",") && !c.Contains("\|"))  .When(x => !string.IsNullOrEmpty(x.Comments)) | Free-form notes captured on the line. |
| `bundleQuantity` | `decimal?` |  | The bundle quantity when the line is a bundle component. |
| `relatedSerials` | `List<string>` |  | The list of serial numbers when the stock item is serial-tracked. |

### `SalesOrderDetailResponse`

Response describing a sales-order line item.

| field | type | description |
|---|---|---|
| `id` | `int?` | The line item id. |
| `quantity` | `decimal?` | The quantity ordered, expressed in the line's unit of measure. |
| `issuedQuantity` | `decimal?` | The quantity that has been issued from the warehouse. |
| `invoicedQuantity` | `decimal?` | The quantity that has already been invoiced against this line. |
| `unitPrice` | `decimal?` | The unit price before discount and tax. |
| `price` | `decimal?` | The extended line price after item-level discount, before tax. |
| `taxRate` | `decimal?` | The tax rate applied to the line as a percentage. |
| `withholdingTaxRate` | `decimal?` | The withholding-tax rate applied to the line as a percentage. |
| `tax` | `TaxReference` | The tax applied to the line. |
| `itemDiscount` | `decimal?` | The item-level discount value (interpreted per `ItemDiscountType`). |
| `itemDiscountType` | `DiscountType?` | Whether the item discount is a flat amount or a percentage. |
| `warehouse` | `WarehouseReference` | The warehouse the line is issued from. |
| `bundleId` | `int?` | The bundle id when the line is part of a bundle, otherwise null. |
| `serviceItemId` | `int?` | The service-item id when the line is a service, otherwise null. |
| `stockItem` | `StockItemReference` | The stock item ordered on the line. |
| `unitOfMeasure` | `UnitOfMeasureReference` | The unit of measure the quantity is expressed in. |
| `batchNumber` | `string` | The batch number, when the stock item is batch-tracked. |
| `expiryDate` | `DateTime?` | The batch expiry date, when applicable. |
| `comments` | `string` | Free-form notes captured on the line. |
| `returnedQuantity` | `decimal?` | The quantity returned against this line. |
| `bundleQuantity` | `decimal?` | The bundle quantity when the line is a bundle component. |

### `SalesOrderHeaderListResponse`

Response row for the sales-order header listing (one row per sales order).

| field | type | description |
|---|---|---|
| `id` | `int?` | The sales-order document id. |
| `documentCode` | `string` | The sales-order document code. |
| `customerId` | `int?` | The customer id. |
| `customerName` | `string` | The customer name. |
| `salesPersonId` | `int?` | The sales-person id. |
| `salesPersonName` | `string` | The sales-person name. |
| `documentDate` | `DateTime?` | The sales-order document date. |
| `dueDate` | `DateTime?` | The next installment due date, if any. |
| `orderStatus` | `SalesOrderStatus?` | The sales-order status. |
| `salesOrderDetails` | `List<SalesOrderDetailResponse>` | The line items on the order. |
| `salesOrderInstallments` | `List<SalesOrderInstallmentResponse>` | The payment installments scheduled against the order. |
| `paymentStatus` | `SalesOrderPaymentStatus?` | The order's payment status. |
| `paidAmount` | `decimal?` | The live, running paid amount: reflects actual collections, never the frozen down payment. |
| `remainingAmount` | `decimal?` | The outstanding balance (`NetTotal - PaidAmount`). |
| `netTotal` | `decimal?` | The order's net total. |
| `currency` | `CurrencyReference` | The order's currency. |
| `salesStore` | `SalesStoreReference` | The sales store originating the order. |
| `isIssued` | `bool?` | Whether the order has been issued. |
| `priceIncludeVat` | `bool?` | Whether item prices include VAT (tax-inclusive pricing). |
| `taxable` | `bool?` | Whether tax is applied to the order. |
| `channel` | `string` | The sales channel originating the order (e.g. web, POS). |
| `externalId` | `string` | An external identifier supplied by the caller for reconciliation. |
| `tags` | `List<string>` | The tags assigned to the order. |
| `relatedWorkOrderCodes` | `List<string>` | The codes of the work orders (issue offerings) related to this order. |

### `SalesOrderInstallmentRequest`

Request to create or update a sales-order payment installment.

| field | type | validation | description |
|---|---|---|---|
| `id` | `int?` |  | The installment id (set on update only). |
| `daysLimit` | `int?` |  | The number of days from the document date until the installment is due. |
| `amount` | `decimal?` | GreaterThan(0) .When(x => x.Amount.HasValue) | The installment amount in the document currency. |
| `dueDate` | `DateTime?` |  | The installment due date. |
| `accountId` | `int?` |  | The id of the accounting account the installment is posted against. |
| `paymentType` | `InstallmentPaymentType?` |  | The payment method for the installment (e.g. cash, on-account). |

### `SalesOrderInstallmentResponse`

Response describing a sales-order payment installment.

| field | type | description |
|---|---|---|
| `id` | `int?` | The installment id. |
| `daysLimit` | `int?` | The number of days from the document date until the installment is due. |
| `amount` | `decimal?` | The installment amount in the document currency. |
| `dueDate` | `DateTime?` | The installment due date. |
| `account` | `AccountReference` | The accounting account the installment is posted against. |
| `paymentType` | `InstallmentPaymentType?` | The payment method for the installment (e.g. cash, on-account). |

### `SalesOrderPaymentStatus`

Sales-order payment states. Matches `Sales.Documents.PaymentStatus` exactly: that column, and its CHECK constraint, only ever allow "Unpaid" or "Paid"; there is no third "partially paid" state anywhere in the schema or in a live code path that sets it.

Values (sent/returned as the name): `Unpaid`=0 (The order has not been marked as paid.), `Paid`=1 (The order has been marked as paid.)

### `SalesOrderPrintTemplateDetailResponse`

A single key/value pair in a sales-order print template body.

| field | type | description |
|---|---|---|
| `id` | `int` | The detail row id. |
| `salesOrderPrintTemplateId` | `int` | The id of the parent print template. |
| `key` | `string` | The detail key. |
| `value` | `string` | The detail value. |

### `SalesOrderPrintTemplateResponse`

Response describing a sales-order print template.

| field | type | description |
|---|---|---|
| `id` | `int?` | The print-template id. |
| `moduleName` | `string` | The module the template belongs to. |
| `pageName` | `string` | The logical page the template is rendered on. |
| `paperSize` | `string` | The paper size (e.g. A4, A5). |
| `templateName` | `string` | The display name of the template. |
| `direction` | `string` | The text direction ("ltr" or "rtl"). |
| `printTemplateDetails` | `List<SalesOrderPrintTemplateDetailResponse>` | The key/value pairs that make up the template body. |
| `htmlContent` | `string` | The saved Advanced HTML template body, or null for a Simple/system template. |

### `SalesOrderResponse`

Response describing a sales order.

| field | type | description |
|---|---|---|
| `id` | `int?` | The sales order id. |
| `documentCode` | `string` | The sales order document code (unique per tenant). |
| `documentType` | `DocumentType?` | The document type. |
| `orderStatus` | `SalesOrderStatus?` | The current order status. |
| `paperNumber` | `string` | The paper number printed on the source document. |
| `runSheetId` | `string` | The run-sheet identifier when grouped for delivery. |
| `customer` | `CustomerReference` | The customer the order is issued to. |
| `salesPerson` | `SalesPersonReference` | The sales person credited with the sale. |
| `warehouse` | `WarehouseReference` | The warehouse the items are issued from. |
| `salesStore` | `SalesStoreReference` | The sales store originating the order. |
| `shippmentCost` | `decimal?` | The shipment cost charged on the order. |
| `documentDate` | `DateTime?` | The document date. |
| `shippingDate` | `DateTime?` | The planned shipping date. |
| `grossTotal` | `decimal?` | The gross total before discounts and tax. |
| `subTotal` | `decimal?` | The subtotal after item-level discounts, before order-level discount and tax. |
| `netTotal` | `decimal?` | The final payable amount. |
| `totalItemsDiscounts` | `decimal?` | The sum of all item-level discounts. |
| `discount` | `decimal?` | The order-level discount amount. |
| `discountRate` | `decimal?` | The order-level discount rate as a percentage. |
| `taxable` | `bool?` | Whether tax is applied to the order. |
| `priceIncludeVat` | `bool?` | Whether item prices include VAT (tax-inclusive pricing). |
| `isWithholdingTax` | `bool?` | Whether the document is liable for withholding tax. |
| `applyTaxAfterDiscount` | `bool?` | Whether tax is computed after applying the order-level discount. |
| `tax` | `decimal?` | The total tax amount. |
| `cashAmount` | `decimal?` | The amount paid in cash at order creation. |
| `onAccountAmount` | `decimal?` | The amount placed on the customer account (credit). |
| `cashPaid` | `decimal?` | The cash already collected against the order. |
| `currency` | `CurrencyReference` | The document currency. |
| `exchangeRate` | `decimal?` | The exchange rate against the system currency at the document date. |
| `channel` | `string` | The sales channel originating the order (e.g. web, POS). |
| `notes` | `string` | Free-form notes captured on the order. |
| `externalId` | `string` | An external identifier supplied by the caller for reconciliation. |
| `relatedWorkOrderCode` | `string` | The related work-order code, when one was generated. |
| `relatedWorkOrderValue` | `decimal?` | The value of the related work order. |
| `tags` | `List<string>` | The tags assigned to the order. |
| `isApproved` | `bool?` | Whether the order has been approved for fulfillment. |
| `isIssued` | `bool?` | Whether the order has been issued (committed to inventory). |
| `requireAutoWorkorder` | `bool?` | Whether a work order should be auto-generated on issue. |
| `salesOrderDetails` | `List<SalesOrderDetailResponse>` | The line items on the order. |
| `salesOrderInstallments` | `List<SalesOrderInstallmentResponse>` | The payment installments scheduled against the order. |

### `SalesOrderStatus`

Sales-order lifecycle states.

Values (sent/returned as the name): `Cancelled`=0, `Confirmed`=1, `Pending`=2, `Processing`=3, `OutForDelivery`=4, `Shipped`=5, `Returned`=6, `Open`=7

### `SalesPersonReference`

Lightweight sales-person reference (id + code + name).

| field | type | description |
|---|---|---|
| `id` | `int` | The sales-person id. |
| `code` | `string` | The sales-person code. |
| `name` | `string` | The sales-person name. |

### `SalesStoreReference`

Lightweight sales-store reference (id + code + name).

| field | type | description |
|---|---|---|
| `id` | `int` | The sales-store id. |
| `code` | `string` | The sales-store code. |
| `name` | `string` | The sales-store name. |

### `StockItemReference`

Lightweight stock-item reference (id + code + description).

| field | type | description |
|---|---|---|
| `id` | `int` | The stock-item id. |
| `code` | `string` | The stock-item code. |
| `description` | `string` | The stock-item description. |

### `TaxReference`

Lightweight tax reference (id + name + rate).

| field | type | description |
|---|---|---|
| `id` | `int` | The tax id. |
| `name` | `string` | The tax name. |
| `rate` | `decimal` | The tax rate as a percentage. |

### `UnitOfMeasureReference`

Lightweight unit-of-measure reference (id + name).

| field | type | description |
|---|---|---|
| `id` | `int` | The unit-of-measure id. |
| `name` | `string` | The unit-of-measure name. |

### `UpdateableSalesOrderStatus`

Sales-order statuses that may be set via the status-update endpoint.

Values (sent/returned as the name): `Cancelled`, `Confirmed`, `OutForDelivery`

### `WarehouseReference`

Lightweight warehouse reference (id + code + name).

| field | type | description |
|---|---|---|
| `id` | `int` | The warehouse id. |
| `code` | `string` | The warehouse code. |
| `name` | `string` | The warehouse name. |
