Checkout Process
Checkout Flow Architecture
The checkout is a multi-step process controlled by the sekce (section) variable. The main template kosik.tpl acts as a router, including the appropriate step partial based on the current section:
{# kosik.tpl — checkout router #}
{% if kos %}
{# nesouhlas = terms and conditions not accepted #}
{% if nesouhlas %}
<div class="stav_err">{% trans 'checkout.terms-and-conditions.error.message' %}</div>
{% endif %}
{% if not sekce or sekce == 'kosik' %}
{% include "kosik_obsah.tpl" %} {# cart content step #}
{% elseif sekce == 'udaje' %}
{% include "kosik_udaje.tpl" %} {# address entry step #}
{% elseif sekce == 'dodani' %}
{% include "kosik_dodani.tpl" %} {# delivery selection step #}
{% endif %}
{% else %}
{% include 'kosik_prazdny.tpl' %} {# empty cart #}
{% endif %}Step Templates
| Template | Purpose |
|---|---|
kosik.tpl | Router — includes step partials based on sekce |
kosik_obsah.tpl | Cart content — product list, quantities, discount codes |
kosik_dodani.tpl | Delivery — transport and payment selection (see Delivery) |
kosik_udaje.tpl | Address — customer details entry |
kosik_souhrn.tpl | Order summary sidebar — included in every step |
kosik_prazdny.tpl | Empty cart fallback — shown when kos is falsy |
kosik_kroky.tpl | Step indicator breadcrumb — included within each step |
kosik_darky.tpl | Gift selection — included when this.slaveConfig.enableGifts is true |
The step flow is configured in config.json via the cartSteps setting (see Configuration), which maps the progression: content -> delivery -> address. Each step form contains a hidden toSekce field that tells the server which step to advance to on submit.
Cross-sell Products
After the checkout steps, kosik.tpl can display a cross-sell product carousel using a content grid slot:
{# Show cross-sell products below checkout when on the cart content step #}
{% if kos and not sekce and this.contentGrid('checkout-select-products').section.product is not empty %}
{% set section = this.contentGrid('checkout-select-products').section %}
<i:list-container type="product" hide-empty collection="{{ section.product }}">
<h3>{{ section.headline }}</h3>
<div data-role="list" class="listing"></div>
</i:list-container>
{% endif %}Cart Items
The cart content step (kosik_obsah.tpl) displays products using the cart.catalogItems loop. The entire product list is wrapped in a form that posts to this.urlSave. The cart content area uses i:dynamic="cart, objednavka" so it reloads automatically when cart state changes (e.g., quantity updates). See Syntax - Dynamic Content Reload for more on i:dynamic.
Info Banner
An optional informational banner can be displayed above the cart items using a content grid slot:
{% if this.contentGrid('checkout-content-info') %}
{% set block = this.contentGrid('checkout-content-info').checkouttext %}
<div style="color: {{ block.color }}; background: {{ block.bgcolor }};
border: {{ block.bordercolor ? '1px solid ' ~ block.bordercolor }}">
{{ block.text }}
</div>
{% endif %}Product List
<form action="{{ this.urlSave }}" method="post">
<h1>{% trans 'checkout.headline' %} ({{ cart.catalogItemTotalAmount }} {% trans 'template.qty' %})</h1>
{% for orderItem in cart.catalogItems %}
<div class="cartie {{ orderItem.gift ? 'cartie--gift' }}">
{# Product image — use variant image if available #}
<a href="{{ orderItem.product.url }}">
{% if orderItem.variant.image %}
<i:img src="orderItem.variant.image" format="130x92" fill />
{% else %}
<i:img src="orderItem.image" format="130x92" fill />
{% endif %}
</a>
{# Product name and child items (e.g., bundle contents) #}
<a href="{{ orderItem.product.url }}">
<strong>{{ orderItem.item.name }}</strong>
</a>
{% if orderItem.children is not empty %}
{% for item in orderItem.children %}
{{ item.name }} {{ item.variantName }}
{% if item.priceTotal.current.value != 0 %}
({{ item.priceTotal.current.formatted }})
{% endif %}
{% endfor %}
{% endif %}
{# Variant name #}
{% if orderItem.variantName %}
{% trans 'template.variant' %}: <strong>{{ orderItem.variantName }}</strong>
{% endif %}
{# Unit price with optional discount display #}
{% if orderItem.item.price.discount %}
<span class="u-decoration-through">{{ orderItem.item.price.beforeDiscount.formatted }}</span>
{% endif %}
<strong>{{ orderItem.item.price.current.formatted }}</strong>
{# Quantity input — name uses ks[orderItem.id] pattern #}
{# For gift items, show "1 qty" text instead of input #}
{% if orderItem.gift %}
1 {% trans 'template.qty' %}
{% else %}
<input type="tel" name="ks[{{ orderItem.id }}]"
value="{{ orderItem.amount }}" size="1" min="0" />
{% endif %}
{# Total price per item #}
{% if orderItem.gift %}
<strong>{% trans 'global.free' %}</strong>
{% else %}
<strong>{{ orderItem.priceTotal.current.formatted }}</strong>
{% endif %}
{# Remove link — uses ?del= query parameter #}
{% if not orderItem.gift %}
<a href="?del={{ orderItem.id }}">{% trans 'cart-popup.item.delete' %}</a>
{% endif %}
</div>
{% endfor %}
{# Hidden field to advance to delivery step on submit #}
<input type="hidden" name="toSekce" value="dodani">
</form>orderItem Properties
| Property | Description |
|---|---|
orderItem.product.url | URL to the product detail page |
orderItem.image | Product image |
orderItem.variant.image | Variant-specific image (if available) |
orderItem.item.name | Product name |
orderItem.variantName | Selected variant name |
orderItem.item.price.current.formatted | Formatted unit price |
orderItem.item.price.beforeDiscount.formatted | Price before discount |
orderItem.item.price.discountPercent | Discount percentage |
orderItem.priceTotal.current.formatted | Total price for the line |
orderItem.amount | Quantity in cart |
orderItem.id | Order item ID (used for quantity input names and removal) |
orderItem.gift | Whether the item is a gift (read-only quantity, free) |
orderItem.children | Child items for bundles/sets |
orderItem.flagNew | Whether the product is flagged as new |
orderItem.labels | Custom product labels |
orderItem.freeDelivery | Whether the item qualifies for free delivery |
orderItem.item.availability.text | Availability text |
orderItem.item.availability.hours | Availability in hours |
orderItem.availableUpsells | Per-item upsell products |
Per-Item Upsells
Each cart item can offer upsell products (e.g., extended warranty, gift wrapping). These are rendered as checkboxes within the item row:
{% if orderItem.availableUpsells is not empty and not orderItem.gift %}
<strong>{% trans 'cart-dialog.upsells.headline' %}</strong>
{% for upsell in orderItem.availableUpsells %}
<input type="checkbox" name="upsell[{{ orderItem.id }}]"
value="{{ orderItem.id }}-{{ upsell.id }}"
if:checked="upsell.inCart">
<label>
{% if upsell.image %}
<i:img src="upsell.image" format="40x40" fill />
{% endif %}
{{ upsell.name }}
<span>{{ upsell.price.current.formatted }}</span>
</label>
{% endfor %}
{% endif %}Loyalty Program Integration
When the loyalty program is enabled, customers can apply loyalty points to their order:
{% if customer and this.slaveConfig.enableLoyaltyProgram and cart.useLoyaltyPointsManually %}
<strong>{% trans 'checkout.points.text' %} {{ customer.userPointsAvailable }}
{% trans %}user.credits-count{% plural customer.userPointsAvailable %}user.credits-count-plural{% endtrans %}:
</strong>
{# Hidden input ensures value submits even when checkbox is unchecked #}
<input type="hidden" name="uplatnitbody" value="{{ cart.useLoyaltyPoints ? 1 : 0 }}" />
<input type="checkbox" name="uplatnitBody"
value="1"
if:checked="cart.useLoyaltyPoints">
<label>{% trans 'checkout.points.use' %}</label>
{% endif %}The cart page also shows how many points the order will earn, and prompts unregistered users to sign up:
{% if this.slaveConfig.enableLoyaltyProgram %}
{% trans 'checkout.loyalty.you-get' %} {{ cart.loyaltyPoints }}
{% trans %}user.credits-count{% plural cart.loyaltyPoints %}user.credits-count-plural{% endtrans %}
{% trans 'checkout.loyalty.discount-to-next-purchase' %}.
{% if not user %}
{% trans 'checkout.loyalty.dont-forget' %}
<a href="{{ iUrl('User/Registrace') }}">{% trans 'checkout.loyalty.register' %}</a>.
{% endif %}
{% endif %}Discount Codes
Discount items applied to the cart are displayed using the cart.discountItems loop. They appear after the product list in kosik_obsah.tpl:
{% for orderDiscountCart in cart.discountItems %}
<div class="cartie">
{# Discount icon from SVG sprite #}
<svg class="icon"><use xlink:href="{{ images('symbol-defs.svg#bi-Discount') }}"></use></svg>
{# Discount name #}
{{ orderDiscountCart.name }}
{# Discount value — either percentage or fixed amount #}
{% if orderDiscountCart.percent > 0 %}
<strong>-{{ orderDiscountCart.percent }}%</strong>
{% elseif orderDiscountCart.price.current.value > 0 %}
<strong>{{ orderDiscountCart.price.current.formatted }}</strong>
{% endif %}
{# Remove discount link — uses ?delSleva= query parameter #}
<a href="?delSleva={{ orderDiscountCart.id }}">
{% trans 'cart-popup.item.delete' %}
</a>
</div>
{% endfor %}The voucher code input is typically placed in the order summary sidebar (kosik_souhrn.tpl), sending the code via the slev_kod form field.
Gift Selection
When gift selection is enabled in the shop configuration, the cart content step includes a gift picker partial:
{% if this.slaveConfig.enableGifts %}
{% include 'kosik_darky.tpl' %}
{% endif %}Order Summary Sidebar
Each checkout step includes the order summary sidebar by passing a checkout context variable that controls what the sidebar displays (totals, buttons, voucher input):
{# In kosik_obsah.tpl (cart content step) #}
{% include 'kosik_souhrn.tpl' with {'checkout': 'products'} %}
{# In kosik_dodani.tpl (delivery step) #}
{% include 'kosik_souhrn.tpl' with {'checkout': 'delivery'} %}The sidebar adapts its content based on the checkout variable — showing different submit buttons and information depending on the current step.
Variant Selection
Enabling variant selection in the cart detail requires three steps:
- In config.json, list the supported selection modes (dropdown, icons, ...)
- In the shop admin panel, define the steps (which attribute set uses which selection mode, labels, ...)
- In the template, implement
i:variant-choice
i:variant-choice
The i:variant-choice tag is used inside the add-to-cart form. It handles rendering the required number of steps, progressive loading of steps, pre-selection based on URL or customer preferences, URL updates on selection, etc.
Important elements:
- else branch - if multiple selection modes are supported, one (ideally the simplest, such as a dropdown) should be in the else branch. If the template only accounts for certain modes and the shop expects different ones, purchasing would be impossible -- the final else branch ensures the purchase can still proceed.
data-role="step"- wrapper defining individual steps; when loading choices based on the current selection, only the subsequent steps are replacedreload-targets- classes (or CSS selector) of elements that should be updated on each selection change -- typically images, price, availability, etc. These elements must first be marked with i:dynamic in the template.step.inputName,choice.inputValue- name/value for the form input. Whether a select or radio is used is up to the template.choice.image- product or variant image (typically a product photo), not available everywherechoice.icon- image uploaded for the attribute set (typically an icon)choice.color- HEX color code set on the attribute value
<i:variant-choice product="product" reload-targets=".js_var_change_example">
{%for step in product.variantChoiceSteps%}
<div data-role="step">
{%if step.mode == 'icons'%}
<h5>{{step.headline}}</h5>
<ul>
{%for choice in step.choices%}
<li>
<label for="color-{{choice.inputValue}}" title="{{choice.name}}" class="custom-tooltip" i:href="{{choice}}">
<input name="{{step.inputName}}" type="radio" value="{{choice.inputValue}}" required data-msg-required="{{step.messageEmpty}}" if:checked="choice.selected">
{%if choice.image%}
<i:img src="choice.image" format="50x50" fill lazyload/>
{%elseif choice.icon%}
<i:img src="choice.icon" format="50x50" fill vector lazyload/>
{%elseif choice.color%}
<span style="background-color: {{choice.color}}"/>
{%endif%}
{{choice.name}}
</label>
</li>
{%endfor%}
</ul>
{%else%}
<h5>{%if step.headline%}{{step.headline}}{%else%}{%trans%}Vyberte variantu{%endtrans%}{%endif%}</h5>
<select name="{{step.inputName}}" required>
<option value="">{{step.placeholder}}</option>
{%for choice in step.choices%}
<option value="{{choice.inputValue}}" if:selected="choice.selected">
{{choice.name}}
</option>
{%endfor%}
</select>
{%endif%}
</div>
{%endfor%}
</i:variant-choice>