to select ↑↓ to navigate
iVend

iVend


## How It Works

ls_shop is headless. All data lives in Jinja globals (shop_* functions registered via hooks.py).
Templates call these functions directly, there is no controller passing data to templates.

Request flow:
1. Request hits /en/products/item-019-red
2. ThemePageRenderer (page_renderer hook) checks: does active theme have pages/products/details.html?
   YES → render theme template with ThemeFallbackLoader
   NO  → Frappe falls back to www/products/details.html (default)
3. Template calls shop_product_detail(product_route) to fetch data
4. Page renders with theme HTML + Tailwind

---

## Theme Folder Structure

ls_shop/themes/<theme_name>/ pages/ index.html → /en and /ar products/ list.html → /en/products details.html → /en/products/ cart/ cart.html → /en/cart checkout.html → /en/cart/checkout account/ login.html → /en/login dashboard.html → /en/account/dashboard profile.html → /en/account/profile wishlist.html → /en/account/wishlist address.html → /en/account/address orders/ index.html → /en/account/orders detail.html → /en/account/orders/detail confirmation.html → /en/account/orders/confirmation .html → /en/ (custom pages, no registration needed) components/ includes/ header.html → overrides templates/includes/header.html footer.html → overrides templates/includes/footer.html core/ language_selector.html → overrides templates/components/core/language_selector.html search_bar.html → overrides templates/components/core/search_bar.html pagination.html → overrides templates/components/core/pagination.html sort_by.html → overrides templates/components/core/sort_by.html country_selector.html → overrides templates/components/core/country_selector.html offcanvas.html → overrides templates/components/core/offcanvas.html macros/ item_card.html → overrides templates/macros/item_card.html (any macro) → overrides same path in templates/macros/


All paths are optional. Missing → falls back to default.

---

## Template Resolution Order (ThemeFallbackLoader)

When a theme page renders, every include/extends goes through ThemeFallbackLoader:

1. theme_dir/<path>             (exact match in theme root)
2. theme_dir/components/<path>  (convenience: skip "components/" prefix)
3. Frappe default loader        (templates/ directory, ls_shop defaults)

Examples:
{% include "templates/includes/header.html" %}
  → checks theme_dir/includes/header.html
  → checks theme_dir/components/includes/header.html  ← finds it here if theme has it
  → falls back to ls_shop/templates/includes/header.html

{% include "templates/components/core/language_selector.html" %}
  → checks theme_dir/components/core/language_selector.html  ← theme override
  → falls back to ls_shop/templates/components/core/language_selector.html

{% extends "templates/layout.html" %}
  → checks theme_dir/layout.html  ← use this for a custom base layout
  → falls back to ls_shop/templates/layout.html

---

## Context Variables (available in every theme page)

These are set by the renderer before the template runs:

  lang              string    "en" or "ar", current language
  is_rtl            bool      True when lang is "ar"
  csrf_token        string    CSRF token for forms
  show_breadcrumb   bool      True by default
  product_route     string    URL path segment after lang prefix
                               e.g. /en/products/item-019-red → "item-019-red"
                               Use this to call shop_product_detail(product_route)
  boot              dict      Frappe boot data (site config, user info, etc.)

Everything else comes from shop_* Jinja calls, see below.

---

## Jinja API — shop_* Methods

All available as globals in every template. Call them directly, assign to variables.

### Products

shop_products(filters=None, product_list=None, page=1, page_length=30, sort_by="default")
  Returns list of product variant dicts.
  filters: dict with keys: subcategory, colors, sizes, brands, search, category,
           has_discount, min_price, max_price
  product_list: list of item_variant names (for curated lists)
  sort_by: "new_arrival" | "price_low_high" | "price_high_low" | "default"

shop_product_count(filters=None, product_list=None)
  Returns int, total count for pagination.

shop_product_filters(selected_filters=None)
  Returns (filters_dict, price_range_dict)
  filters_dict keys: brands, colors, sizes, subcategories
  price_range_dict keys: min_price, max_price

shop_category_tree(root_category)
  Returns nested category tree dict.

shop_product_detail(route, selected_size=None)
  Returns full product detail dict:
    product_variant   , variant doc (display_name, attribute_value, route, etc.)
    product           , parent item doc (brand, item_name, custom_item_name_ar, etc.)
    images            , list of image URLs
    in_stock          , bool
    available_sizes   , list of {size, stock_detail: {in_stock, stock_qty}}
    selected_size     , currently selected size string
    selected_item     , the specific item doc for selected size
    sale_price        , float
    default_price     , float (original before discount)
    discount_percent  , float
    recommended_items , list of related products
    other_variants    , list of other color variants {image, route, attribute_value}
    size_chart        , size chart data or None
    item_qty          , available quantity

shop_recommended_products(variant_name)
  Returns list of related products.

### Homepage

shop_homepage()
  Returns dict:
    banners           , list of banner dicts
    new_arrivals      , list of {item_variant} dicts
    best_picks        , list of {item_variant} dicts
    sections          , list of section dicts

### Cart / Checkout

shop_delivery_config()
  Returns {shipping_amount, threshold}

shop_cod_config()
  Returns {cod_charge_applicable_below, cod_charge}

shop_checkout()
  Returns full checkout dict:
    cart_quotation, items, billing_addresses, shipping_addresses,
    store_pickup_addresses, delivery_charge, delivery_charge_applicable_below,
    cod_charge, cod_charge_applicable_below, coupon_code,
    show_telr, show_tabby, show_cod, payment_gateways, country_list

### Account

shop_user_details()
  Returns User doc for current session user.

shop_addresses(address_type="Billing")
  Returns list of address dicts. address_type: "Billing" | "Shipping"

shop_orders(page=1, page_length=6)
  Returns {total_count, orders}

shop_order_detail(order_id)
  Returns order dict or {}.

shop_return_reasons()
  Returns list of {name, display_name}

### Utilities

shop_stock(item_code, warehouse=None)
  Returns {stock_qty, in_stock}

shop_country_list()
  Returns list of country names.

shop_discount_percent(default_price, sale_price)
  Returns float discount percentage.

shop_store_pickup_addresses()
  Returns list of store pickup address dicts.

shop_payment_gateways()
  Returns list of enabled gateway names.

shop_theme_config()
  Returns active theme's config dict (from iVend Theme DocType config field).
  Use for theme settings: colors, feature toggles, etc.

shop_theme_asset_url(path)
  Returns the URL for a static asset in the active theme's public folder.
  Files go in: ls_shop/public/themes/<slug>/
  Served at:   /assets/ls_shop/themes/<slug>/
  Example: shop_theme_asset_url('css/theme.css') → /assets/ls_shop/themes/my_theme/css/theme.css
  Usage in template: <link rel="stylesheet" href="{{ shop_theme_asset_url('css/theme.css') }}">

---

## Jinja Filters

{{ price | money }}
  Formats price with currency symbol. e.g. "₺ 22.00"

{{ image_path | img_url }}
  Ensures valid image URL. Returns placeholder if empty.

{{ order | can_return }}
  Returns bool, whether this order is eligible for return.

---

## URL Params in Templates

frappe.form_dict is available in templates for query string params:

  frappe.form_dict.get("search", "")
  frappe.form_dict.get("category", "")
  frappe.form_dict.get("sort_by", "new_arrival")
  frappe.form_dict.get("page", "1") | int
  frappe.form_dict.get("order_id")
  frappe.form_dict.get("size")

product_route (from context) gives the URL path segment, use it for product detail:
  {% set detail = shop_product_detail(product_route) %}

---

## Default Templates to Reference

When building theme pages, reference these for logic and structure:

  www/index.html                          → homepage
  www/products/list.html                  → product listing
  www/products/details.html               → product detail
  www/cart/cart.html                      → cart
  www/cart/checkout.html                  → checkout
  www/account/dashboard.html             → account dashboard
  www/account/orders/index.html          → orders list
  www/account/orders/detail.html         → order detail
  www/account/wishlist.html              → wishlist
  www/account/address.html               → address management
  www/account/profile.html               → profile

Default components (all overridable):

  templates/layout.html                               → base layout (extend this)
  templates/includes/header.html                      → site header
  templates/includes/footer.html                      → site footer
  templates/includes/cart-state.html                  → Alpine cart store
  templates/includes/wishlist-state.html              → Alpine wishlist store
  templates/includes/toast-state.html                 → Alpine toast store
  templates/includes/i18-state.html                   → Alpine i18n store
  templates/components/core/language_selector.html    → language switcher
  templates/components/core/search_bar.html           → search
  templates/components/core/pagination.html           → pagination
  templates/components/core/sort_by.html              → sort dropdown
  templates/components/core/country_selector.html     → country picker
  templates/components/product_details_carousel.html  → product image carousel
  templates/components/product_filter.html            → filter sidebar
  templates/macros/item_card.html                     → product card macro
  templates/macros/icon.html                          → icon macro
  templates/macros/modal.html                         → modal macro
  templates/macros/tabby-promo.html                   → Tabby payment promo

---

## Common Patterns

### Minimal page skeleton

{% extends "templates/layout.html" %}
{% block body %}
<div class="mx-auto py-8">
  {# your content here #}
</div>
{% endblock %}

### Product detail page

{% extends "templates/layout.html" %}
{% block body %}
{% set detail = shop_product_detail(product_route) %}
{% if not detail %}
  <p>Product not found.</p>
{% else %}
  <h1>{{ detail.product_variant.display_name }}</h1>
  <p>{{ detail.sale_price | money }}</p>
  {% if detail.discount_percent %}
    <span>{{ detail.default_price | money }}</span>
    <span>{{ detail.discount_percent | int }}% OFF</span>
  {% endif %}

  {# Sizes #}
  {% for size in detail.available_sizes %}
    <button>{{ size.size }}</button>
  {% endfor %}

  {# Images #}
  {% for img in detail.images %}
    <img src="{{ img | img_url }}">
  {% endfor %}
{% endif %}
{% endblock %}

### Product listing page

{% extends "templates/layout.html" %}
{% block body %}
{% set page = frappe.form_dict.get("page", "1") | int %}
{% set search = frappe.form_dict.get("search", "") %}
{% set selected_filters = {
  "search": search,
  "category": frappe.form_dict.get("category", ""),
  "has_discount": frappe.form_dict.get("has_discount", "0") == "1"
} %}
{% set products = shop_products(filters=selected_filters, page=page) %}
{% set total = shop_product_count(filters=selected_filters) %}

<div class="grid grid-cols-3 gap-4">
  {% for p in products %}
    <a href="/{{ lang }}/products/{{ p.route }}">
      <img src="{{ p.image | img_url }}">
      <p>{{ p.brand }}</p>
      <p>{{ p.sale_price | money }}</p>
    </a>
  {% endfor %}
</div>
{% endblock %}

### Homepage

{% extends "templates/layout.html" %}
{% block body %}
{% set hp = shop_homepage() %}
{% set new_arrivals_ids = hp.new_arrivals | map(attribute="item_variant") | list %}
{% set products = shop_products(product_list=new_arrivals_ids, page_length=new_arrivals_ids | length or 6) %}

{% for banner in hp.banners %}
  <img src="{{ banner.image | img_url }}">
{% endfor %}

<div class="grid grid-cols-4 gap-4">
  {% for p in products %}
    <a href="/{{ lang }}/products/{{ p.route }}">{{ p.sale_price | money }}</a>
  {% endfor %}
</div>
{% endblock %}

### Custom page (no registration needed)

Create: themes/<name>/pages/about.html
Access: /en/about

{% extends "templates/layout.html" %}
{% block body %}
<h1>About Us</h1>
{% set config = shop_theme_config() %}
{% if config.show_social_links %}
  {% include "templates/includes/social_links.html" %}
{% endif %}
{% endblock %}

### Overriding a component

Create: themes/<name>/components/core/language_selector.html
→ Automatically used everywhere templates/components/core/language_selector.html is included.
No registration. No changes to other files.

### Alpine.js stores (available globally)

$store.cart.add(product, variant, price, default_price, brand, sizes, qty)
$store.cart.items         → current cart items
$store.cart.total         → cart total

$store.wishlist.toggle(item_code)
$store.wishlist.has(item_code)

$store.i18n.t("key")      → translated string
toast("message", { type: "success" | "warning" | "error" })

### RTL support

{% if is_rtl %}
  {# use dir="rtl" attributes, flip flex directions, use ms-/me- instead of ml-/mr- #}
{% endif %}

All Tailwind directional utilities use logical properties:
  ms-* = margin-start (left in LTR, right in RTL)
  me-* = margin-end

### frappe.call (API calls from templates/JS)

Use frappe_call() (defined in frappe-call-script.html, included via layout.html):
  const data = await frappe_call("/api/v2/method/ls_shop.api.some_method", { param: value })

---

## Creating a Theme

### Via CLI (recommended for development)

bench --site <site> create-theme "My Theme"

Creates:
  ls_shop/themes/my_theme/          — template files
  ls_shop/public/themes/my_theme/   — static assets (css/, js/, images/)

Then register in Desk → iVend Theme and set is_active = 1.

### Via Desk

Go to Desk → iVend Theme → New. Same folders are scaffolded on save.

---

## Theme Static Assets

Put CSS/JS/images in: ls_shop/public/themes/<slug>/
They are served at:   /assets/ls_shop/themes/<slug>/

In templates use shop_theme_asset_url(), it resolves the path for the active theme:

{% block head %}
  <link rel="stylesheet" href="{{ shop_theme_asset_url('css/theme.css') }}">
{% endblock %}

{% block script %}
  <script src="{{ shop_theme_asset_url('js/theme.js') }}"></script>
{% endblock %}

After adding files to public/, run bench build to make them available.

---

## Theme Registration

1. Go to Desk → iVend Theme → New
2. Set theme_name (must match folder name under ls_shop/themes/)
3. Check is_active → deactivates any previously active theme
4. config field: JSON for theme settings (colors, feature flags, etc.)
   Access in templates: {% set cfg = shop_theme_config() %}

---

## Auth / Protected Routes

Routes under /en/account/ and /en/cart/checkout automatically require login.
The renderer raises PermissionError for Guest users on these paths.
No need to check auth in templates for these routes.

For any other page that needs auth:
  {% if frappe.session.user == "Guest" %}
    <p>Please <a href="/en/login">log in</a>.</p>
  {% else %}
    {# authenticated content #}
  {% endif %}

---

## What NOT to Do

- Do NOT add frappe.db calls or Python imports in templates. Use only shop_*() Jinja methods.

Last updated 3 weeks ago
Was this helpful?
Thanks!