Step 5: Block unmapped carts
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. Later tidied in refactor: use frappe utilities across the app. In 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.pythat 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:
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.
2. Register the hook
Add this line at the end of commera_qikink/hooks.py:
commera_hooks = {"validate_cart": ["commera_qikink.cart.get_unmapped_item_refusal"]}
Then clear the cache, because Frappe caches hooks.py:
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
- In
/commera, open the product from step 4 and click More actions (the three dots next to Save) > Set Qikink SKUs. - Clear the SKU of one size, for example L, and click Save SKUs. The card shows that size as "Not mapped".
- Open your storefront at
/en/and log in as a shopper. - Open the product, choose size L and click Add to Cart.
- Go to checkout. Fill in the address, choose a payment method and accept the terms.
- 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.
- Remove size L from the cart, add size S and place the order again. Checkout continues.
Next steps
- Step 6: Send paid orders
- Hooks: every hook, and the order Commera calls them in
- Testing: test a hook with a cart built in code