---
title: "Step 8: Add an order card and actions"
space: "Commera"
url: "https://docs.bwh.tech/commera/build-a-plugin/order-card-and-actions"
updated: "2026-10-09"
---

In this step, you show Qikink on the order page in `/commera`. A **Qikink** card shows the Qikink order number, the status, the drop-ship Purchase Order and the shipments. A **Send to Qikink** action sends a cash on delivery (COD) order.

Code for this step: [feat: show qikink on the order page and send cod orders from it](https://github.com/Rl0007/commera_qikink/commit/8303be9). 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.

## What you'll build

- `commera/order/cards/qikink/index.vue`, a card on the order page.
- `commera/order/actions/send-to-qikink/index.vue`, a row in the **More actions** menu.
- Two whitelisted methods and a condition.

## 1. Read the card data

Create `commera_qikink/order_card.py`:

```python
import frappe

from commera_qikink.orders import has_qikink_lines


def get_order_card(sales_order: str) -> dict:
	order_number, status = frappe.db.get_value(
		"Sales Order", sales_order, ["commera_qikink_order_number", "commera_qikink_status"]
	)
	return {
		"has_qikink_lines": has_qikink_lines(sales_order),
		"order_number": order_number,
		"status": status if order_number else None,
		"purchase_orders": get_purchase_orders(sales_order),
		"shipments": get_shipments(sales_order),
	}


def get_purchase_orders(sales_order: str) -> list[str]:
	return frappe.get_all(
		"Purchase Order Item",
		filters={"sales_order": sales_order, "docstatus": 1},
		pluck="parent",
		distinct=True,
		order_by="parent",
	)


def get_shipments(sales_order: str) -> list[dict]:
	if "bwh_shipping" not in frappe.get_installed_apps():
		return []
	return frappe.get_all(
		"Shipping Request",
		filters={"ref_doctype": "Sales Order", "ref_docname": sales_order, "docstatus": ["<", 2]},
		fields=["name", "awb", "carrier", "status", "tracking_url"],
		order_by="creation",
	)
```

The AWB and tracking link live on the order's **Shipping Request**, not on the Sales Order. You write them in [Keep status in sync](/commera/build-a-plugin/keep-status-in-sync).

Add `has_qikink_lines` to `orders.py`:

```python
def has_qikink_lines(sales_order: str) -> bool:
	item_codes = frappe.get_all("Sales Order Item", filters={"parent": sales_order}, pluck="item_code")
	return bool(get_qikink_items(item_codes))
```

## 2. Add the methods

In `commera_qikink/api.py`:

```python
@frappe.whitelist(methods=["GET"])
def get_order_card(sales_order: str) -> dict:
	frappe.has_permission("Sales Order", "read", doc=sales_order, throw=True)
	return order_card.get_order_card(sales_order)


@frappe.whitelist(methods=["POST"])
def send_order(name: str) -> str:
	frappe.has_permission("Sales Order", "write", doc=name, throw=True)
	order_number = orders.send_order(name)
	if not order_number:
		frappe.throw(_("Order {0} has no Qikink items to send.").format(name))
	return _("Sent to Qikink as order {0}").format(order_number)
```

:::caution[An action's method gets name=]

Commera calls an action's method as `method(name=<record name>)`. Name the argument `name`, not `sales_order`. A returned string shows in the success toast.

:::

## 3. Build the card

In `apps/commera_qikink`, run `bench commera add order-card qikink`. Replace the `commera/order/cards/qikink/index.vue` it writes with this. The card hides itself on orders without Qikink lines.

```vue
<script>
export const plugin = { label: 'Qikink' };
</script>

<script setup>
import { computed, watch } from 'vue';
import { Skeleton } from 'frappe-ui';
import { useCard, usePlugin, useMethodRead } from '@commera/admin';

const { record } = usePlugin();
const card = useCard();

const cardRequest = useMethodRead('commera_qikink.api.get_order_card', {
	params: () => ({ sales_order: record.value.name }),
});

const qikink = computed(() => cardRequest.data);
watch(qikink, (data) => card.setHidden(!!data && !data.has_qikink_lines), { immediate: true });
</script>

<template>
	<Skeleton v-if="cardRequest.loading && !qikink" class="h-16 w-full rounded-4" />
	<div v-else-if="qikink" class="divide-y divide-outline-gray-1">
		<div class="flex items-start justify-between gap-3 pb-2">
			<div class="min-w-0">
				<p class="text-sm text-ink-gray-5">Qikink order</p>
				<p class="truncate text-base text-ink-gray-8 tabular-nums">{{ qikink.order_number || 'Not sent yet' }}</p>
			</div>
			<div v-if="qikink.order_number" class="min-w-0 text-right">
				<p class="text-sm text-ink-gray-5">Qikink status</p>
				<p class="truncate text-base text-ink-gray-8">{{ qikink.status }}</p>
			</div>
		</div>
		<div v-if="qikink.purchase_orders.length" class="py-2">
			<p class="text-sm text-ink-gray-5">Drop-ship purchase order</p>
			<a
				v-for="purchaseOrder in qikink.purchase_orders"
				:key="purchaseOrder"
				:href="`/desk/purchase-order/${purchaseOrder}`"
				target="_blank"
				class="block truncate text-base font-medium text-ink-gray-8 hover:underline"
			>
				{{ purchaseOrder }}
			</a>
		</div>
		<div v-for="shipment in qikink.shipments" :key="shipment.name" class="flex items-start justify-between gap-3 py-2 last:pb-0">
			<div class="min-w-0">
				<p class="text-sm text-ink-gray-5">{{ shipment.carrier || 'Courier not named' }} · {{ shipment.status }}</p>
				<p class="truncate text-base text-ink-gray-8 tabular-nums">AWB {{ shipment.awb }}</p>
			</div>
			<a
				v-if="shipment.tracking_url"
				:href="shipment.tracking_url"
				target="_blank"
				rel="noopener"
				class="shrink-0 text-base font-medium text-ink-gray-8 hover:underline"
			>
				Track
			</a>
		</div>
	</div>
</template>
```

## 4. Add the Send to Qikink action

An action with a `method` needs no template. In `apps/commera_qikink`, run `bench commera add order-action send-to-qikink`. Replace the `commera/order/actions/send-to-qikink/index.vue` it writes with this, and delete the `send_to_qikink_order` stub it adds to `api.py`: this action calls `send_order`.

```vue
<script>
export const plugin = {
	label: 'Send to Qikink',
	icon: 'printer',
	condition: 'commera_qikink.conditions.can_send_order',
	method: 'commera_qikink.api.send_order',
	confirm: 'Qikink will print and ship this order, and a Purchase Order to Qikink is made. Qikink cannot cancel an order once it has it.',
};
</script>
```

Add the condition to `commera_qikink/conditions.py`. A card or action condition gets `(doctype, name)`.

```python
from commera_qikink.orders import has_qikink_lines


def can_send_order(doctype: str, name: str) -> bool:
	order = frappe.db.get_value(
		"Sales Order", name, ["docstatus", "commera_qikink_order_number"], as_dict=True
	)
	return bool(
		order
		and order.docstatus == 1
		and not order.commera_qikink_order_number
		and frappe.has_permission("Sales Order", "write", name)
		and has_qikink_lines(name)
	)
```

:::caution[COD orders stay draft until staff submit them]

Commera saves a COD order as a draft Sales Order. `/commera` has no action to submit it. Your staff open the order in Desk and click **Submit**. Until then, **Send to Qikink** stays hidden, because `send_order` needs a submitted order. Tell your staff about this step.

:::

:::tip

The condition also shows the action on a prepaid order that failed to send. Your staff can then send it by hand.

:::

## Check it works

1. Keep `yarn dev` running in `apps/commera/dashboard`, or run `bench build --app commera_qikink`.
2. Open a prepaid order that your plugin sent, for example `SAL-ORD-2026-00077`. The **Qikink** card shows the Qikink order number, **Sent** and the Purchase Order.
3. Place a COD order on the storefront. Open it in `/commera`. The card shows **Not sent yet**.
4. Open the order in Desk and click **Submit**.
5. In `/commera`, open **More actions** and click **Send to Qikink**. Confirm. The toast shows "Sent to Qikink as order …".
6. In `bench --site <site> console`, run `Qikink().find_order("<number>")`. The order has `payment_type` **COD**.

<img src="/files/commera-qk-order-card-sent-57a52a1a.png" alt="The Qikink card on a sent order with the order number, status and drop-ship purchase order" width="1216">

<img src="/files/commera-qk-send-to-qikink-action-5b533d6b.png" alt="The More actions menu on a submitted COD order with Send to Qikink" width="363">

## Next steps

- [Step 9: Keep status in sync](/commera/build-a-plugin/keep-status-in-sync)
- [Dashboard entries](/commera/plugin-reference/dashboard-entries)
- [Gotchas](/commera/plugin-reference/gotchas)
