---
title: "Step 7: Create drop-ship orders"
space: "Commera"
url: "https://docs.bwh.tech/commera/build-a-plugin/create-drop-ship-orders"
updated: "2026-10-09"
---

In this step, your plugin records what the store buys from Qikink. After Qikink accepts an order, your plugin makes and submits a drop-ship Purchase Order for the Qikink lines. Your books then show the cost of each printed item.

Code for this step: [feat: buy qikink lines with a drop-ship purchase order](https://github.com/Rl0007/commera_qikink/commit/32f07a4). Later tidied in [refactor: use frappe utilities across the app](https://github.com/Rl0007/commera_qikink/commit/a8fa754). The code on this page is the tidied version.

:::note[Using ERPNext drop ship]

An Item with **Delivered by Supplier** ticked is shipped by its supplier, not from your warehouse. A Sales Order line for that Item carries a **Supplier**. ERPNext makes a Purchase Order to that supplier with the shopper's address, and the supplier ships to the shopper. No Delivery Note is made. The Purchase Order records the delivery.

:::

## What you'll build

- A `before_validate` hook that marks Qikink lines as drop-ship on each new Sales Order.
- A `make_drop_ship_order` function that buys the Qikink lines from Qikink.
- A change to `send_order` so that a second call retries a Purchase Order that failed.
- A clear error for a Qikink reply that is not JSON.

## 1. Mark the Qikink lines as drop-ship

Commera copies cart lines to the Sales Order with **Delivered by Supplier** unticked. The field cannot change after submit, so set it before validation. This hook went in with [feat: send paid orders to qikink once](https://github.com/Rl0007/commera_qikink/commit/2944b7f).

```python
def set_drop_ship_lines(doc, method=None):
	if doc.docstatus != 0:
		return
	qikink_items = get_qikink_items([row.item_code for row in doc.items])
	supplier = frappe.db.get_single_value("Qikink Settings", "supplier")
	for row in doc.items:
		if row.item_code in qikink_items:
			row.delivered_by_supplier = 1
			row.supplier = supplier
```

Register it in `hooks.py`:

```python
doc_events = {"Sales Order": {"before_validate": "commera_qikink.orders.set_drop_ship_lines"}}
```

## 2. Make the Purchase Order

Create `commera_qikink/purchase_orders.py`. ERPNext moved `make_purchase_order` in a recent version, so import it from either place.

```python
import frappe
from commera.sdk import as_plugin_user
from frappe import _
from frappe.utils import today

try:
	from erpnext.selling.doctype.sales_order.mapper import make_purchase_order
except ImportError:
	from erpnext.selling.doctype.sales_order.sales_order import make_purchase_order


def make_drop_ship_order(order) -> str | None:
	frappe.db.get_value("Sales Order", order.name, "name", for_update=True)
	settings = frappe.get_cached_doc("Qikink Settings")
	lines = get_unordered_lines(order, settings.supplier)
	if not lines:
		return None

	rates = get_buying_rates([row.item_code for row in lines], settings.buying_price_list)
	if missing := sorted({row.item_code for row in lines if row.item_code not in rates}):
		frappe.log_error(
			title=_("Qikink purchase order not made for {0}").format(order.name),
			message=_("No buying price on price list {0} for {1}. Add it, then send the order again.").format(
				settings.buying_price_list or _("(not set in Qikink Settings)"), ", ".join(missing)
			),
			reference_doctype="Sales Order",
			reference_name=order.name,
		)
		return None

	with as_plugin_user("commera_qikink"):
		selected_items = [{"item_code": row.item_code, "supplier": settings.supplier} for row in lines]
		target = frappe.new_doc("Purchase Order", schedule_date=order.delivery_date or today())
		purchase_order = make_purchase_order(order.name, selected_items, target)[0]
		purchase_order.buying_price_list = settings.buying_price_list
		for item in purchase_order.items:
			item.price_list_rate = item.rate = rates[item.item_code]
		purchase_order.save()
		purchase_order.submit()
	return purchase_order.name
```

`as_plugin_user` runs the block as Commera's plugin user. The staff member who clicks **Send to Qikink** can sell, but may not have permission to buy.

:::caution[Pass a delivery date]

`make_purchase_order` copies the Sales Order's delivery date into each line's **Required By**. An order without that date makes the insert throw "Please enter the Required By". Pass a target Purchase Order with `schedule_date` set, as above.

:::

:::caution[A missing buying price leaves the Purchase Order unmade]

The rate comes from the **Buying Price List** in **Qikink Settings**. If an item has no price there, your plugin writes an Error Log and makes no Purchase Order. Add the **Item Price**, then click **Send to Qikink** again. The Qikink order stays as it is.

:::

## 3. Skip lines that are already bought

`get_unordered_lines` returns the drop-ship lines for Qikink that no Purchase Order holds yet. This makes `make_drop_ship_order` safe to call again.

```python
def get_unordered_lines(order, supplier: str) -> list:
	lines = [
		row for row in order.items if supplier and row.delivered_by_supplier and row.supplier == supplier
	]
	if not lines:
		return []
	ordered = set(
		frappe.get_all(
			"Purchase Order Item",
			filters={"sales_order_item": ["in", [row.name for row in lines]], "docstatus": ["<", 2]},
			pluck="sales_order_item",
		)
	)
	return [row for row in lines if row.name not in ordered]


def get_buying_rates(item_codes: list[str], price_list: str | None) -> dict:
	if not price_list:
		return {}
	prices = frappe.get_all(
		"Item Price",
		filters={"price_list": price_list, "item_code": ["in", item_codes]},
		fields=["item_code", "price_list_rate"],
	)
	return {price.item_code: price.price_list_rate for price in prices}
```

## 4. Call it from send_order

Make the Purchase Order after Qikink accepts the order. If the Purchase Order fails, log it and keep the Qikink order. In `orders.py`:

```python
from commera_qikink.purchase_orders import make_drop_ship_order


def send_order(sales_order: str) -> str | None:
	order = frappe.get_doc("Sales Order", sales_order, for_update=True)
	if order.docstatus != 1:
		frappe.throw(_("Submit order {0} before sending it to Qikink.").format(sales_order))
	if not order.commera_qikink_order_number:
		lines = get_qikink_lines(order)
		if not lines:
			return None
		submit_to_qikink(order, lines)

	save_drop_ship_order(order)
	return order.commera_qikink_order_number


PURCHASE_ORDER_SAVEPOINT = "commera_qikink_purchase_order"


def save_drop_ship_order(order):
	frappe.db.savepoint(PURCHASE_ORDER_SAVEPOINT)
	try:
		make_drop_ship_order(order)
	except Exception:
		# Qikink already holds the order, so a failed purchase order is logged, never allowed to undo it.
		frappe.db.rollback(save_point=PURCHASE_ORDER_SAVEPOINT)
		frappe.log_error(
			title=_("Qikink purchase order failed for {0}").format(order.name),
			reference_doctype="Sales Order",
			reference_name=order.name,
		)
```

The savepoint rolls back only the Purchase Order work. A bare `frappe.db.rollback()` would also undo anything that the caller wrote in the same transaction.

`submit_to_qikink` commits after Qikink accepts the order, which releases the row lock. That is why `make_drop_ship_order` locks the Sales Order again.

:::tip

An order that is already sent still runs `save_drop_ship_order`. A second call to `send_order` is the retry for a Purchase Order that failed.

:::

## 5. Name the unreadable reply

In [Send paid orders](/commera/build-a-plugin/send-paid-orders), you saw that Qikink can accept an order and reply with a body that is not JSON. Catch that case in `get_reply` in `client.py`, before the `requests.RequestException` branch. Otherwise the Error Log reads "Could not reach Qikink" for an order that Qikink may have.

```python
	except requests.JSONDecodeError as error:
		frappe.throw(_("Qikink sent a reply that could not be read: {0}").format(error), QikinkError)
```

## Check it works

1. In **Qikink Settings**, make sure that **Supplier** and **Buying Price List** are set.
2. Add an **Item Price** on that price list for each mapped T-shirt variant.
3. Buy a mapped T-shirt on the storefront with a test card.
4. Open the Sales Order in Desk. Click **Connections**. A submitted Purchase Order to Qikink is listed.
5. Open the Purchase Order. It holds only the Qikink lines, at the buying price. The **Drop Ship** tab shows the shopper as **Customer**, and the **Address & Contact** tab shows the shopper's address as **Shipping Address**.

<img src="/files/commera-qk-po-drop-ship-abf67951.png" alt="The drop-ship Purchase Order to Qikink with one T-shirt line and the shopper&#x27;s address" width="974">

## Next steps

- [Step 8: Add an order card and actions](/commera/build-a-plugin/order-card-and-actions)
- [SDK](/commera/plugin-reference/sdk)
- [Gotchas](/commera/plugin-reference/gotchas)
