---
title: "Step 5: Block unmapped carts"
space: "Commera"
url: "https://docs.bwh.tech/commera/build-a-plugin/block-unmapped-carts"
updated: "2026-10-09"
---

In this step, you add a hook. It refuses a cart that holds a Qikink product without a Qikink SKU, because the plugin could not send that line to Qikink.

Commit: [feat: refuse checkout for qikink items without a sku](https://github.com/Rl0007/commera_qikink/commit/213f818). Later tidied in [refactor: use frappe utilities across the app](https://github.com/Rl0007/commera_qikink/commit/a8fa754). In [Send paid orders](/commera/build-a-plugin/send-paid-orders), the query moves to a shared `get_qikink_items`.

## What you'll build

- `commera_qikink/cart.py`, a hook that refuses a Qikink item with no SKU.
- One line in `hooks.py` that registers it.

## Which lines to refuse

A Qikink item is an item that Qikink ships for the store. Step 4 marks it in ERPNext's own way:

- **Delivered by Supplier (Drop Ship)** is ticked on the Item.
- The Item's default supplier is the **Supplier** from Qikink Settings.

The hook refuses only those items that have no Qikink SKU. It does not refuse every item without a SKU. That would block every product in the store that Qikink does not make. An item without the two marks is a normal stock item, and Commera's own stock check handles it.

## 1. Write the hook

Create `commera_qikink/cart.py`:

```python
import frappe
from frappe import _
from frappe.query_builder import DocType
from frappe.query_builder.functions import Coalesce


def get_unmapped_item_refusal(quotation) -> str | None:
	supplier = frappe.db.get_single_value("Qikink Settings", "supplier")
	item_names = {row.item_code: row.item_name for row in quotation.items}
	if not supplier or not item_names:
		return None

	unmapped = get_unmapped_qikink_items(list(item_names), supplier)
	if not unmapped:
		return None
	return _("{0} can't be ordered right now. Please remove it from your cart to check out.").format(
		", ".join(sorted(item_names[item_code] for item_code in unmapped))
	)


def get_unmapped_qikink_items(item_codes: list[str], supplier: str) -> list[str]:
	# A Qikink item is one Qikink drop-ships: delivered by supplier, with the Qikink supplier as default.
	item = DocType("Item")
	item_default = DocType("Item Default")
	return (
		frappe.qb.from_(item)
		.join(item_default)
		.on((item_default.parent == item.name) & (item_default.parenttype == "Item"))
		.select(item.name)
		.distinct()
		.where(
			item.name.isin(item_codes)
			& (item.delivered_by_supplier == 1)
			& (item_default.default_supplier == supplier)
			& (Coalesce(item.commera_qikink_sku, "") == "")
		)
		.run(pluck=True)
	)
```

Commera passes the cart, a `Quotation`, to the hook. Return `None` to let the cart check out. Return a string to refuse checkout. Commera shows the string to the shopper.

When Qikink Settings has no **Supplier**, the hook returns `None` and refuses nothing. The hook runs one query, and it makes no call to Qikink.

:::danger[Return a message, do not throw]

Do not call `frappe.throw` in this hook. The shopper sees "Something went wrong", not your message. While the hook raises, no shopper can check out, even with a cart that holds no Qikink product.

:::

## 2. Register the hook

Add this line at the end of `commera_qikink/hooks.py`:

```python
commera_hooks = {"validate_cart": ["commera_qikink.cart.get_unmapped_item_refusal"]}
```

Then clear the cache, because Frappe caches `hooks.py`:

```bash
bench --site your.site clear-cache
```

Then restart the bench. On a development bench, stop `bench start` and run it again. On a bench that runs under supervisor, run `bench restart`.

## Check it works

1. In `/commera`, open the product from step 4 and click **More actions** (the three dots next to **Save**) **> Set Qikink SKUs**.
2. Clear the SKU of one size, for example L, and click **Save SKUs**. The card shows that size as "Not mapped".
3. Open your storefront at `/en/` and log in as a shopper.
4. Open the product, choose size L and click **Add to Cart**.
5. Go to checkout. Fill in the address, choose a payment method and accept the terms.
6. Click **Place Order**. A toast shows "Ccs Tee L can't be ordered right now. Please remove it from your cart to check out." Commera makes no order and opens no payment.

<img src="/files/commera-qikink-checkout-refusal-6970273d.png" alt="The checkout refusal toast in the storefront" width="489">

7. Remove size L from the cart, add size S and place the order again. Checkout continues.

:::caution[The shopper sees the refusal only at Place Order]

Commera calls `validate_cart` when the shopper starts checkout and when a cash on delivery order is confirmed. It does not call it when the shopper adds to cart or edits the cart. The cart page, the address step and **Continue** all pass, and the shopper sees your message as a toast after the last click. Write a message that tells the shopper what to remove.

:::

:::note[The message uses the item name]

The refusal shows the Item's **Item Name**, such as "Ccs Tee L". This can differ from the title of the product in the storefront. Give your variant Items names that a shopper understands.

:::

## Next steps

- [Step 6: Send paid orders](/commera/build-a-plugin/send-paid-orders)
- [Hooks](/commera/plugin-reference/hooks): every hook, and the order Commera calls them in
- [Testing](/commera/plugin-reference/testing): test a hook with a cart built in code
