Commera

Commera

Open in ChatGPT
Ask ChatGPT about this page
Open in Claude
Ask Claude about this page

Step 8: Add an order card and actions

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. Later tidied in refactor: use frappe utilities across the app. 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:

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.

Add has_qikink_lines to orders.py:

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:

@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)

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.

<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.

<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).

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)
	)

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.
The Qikink card on a sent order with the order number, status and drop-ship purchase order The More actions menu on a submitted COD order with Send to Qikink

Next steps

Last updated 3 days ago
Was this helpful?
Thanks!