Eine Bestellung mit Confy ist keine einzelne Position, sondern ein Baum: die Konfiguration, darunter die Schritte, darunter die gewählten Möglichkeiten oder die Einträge einer Feldsammlung, darunter die einzelnen Felder. Shopwares Standardvorlage läuft über order.nestedLineItems zwar bereits rekursiv, kennt aber nur Produkt-Positionen: Schritte erscheinen als Zeile mit Menge 1 und Preis 0,00 €, Einträge tragen ihre rohe Beschriftung (0, 1, 2…) und Felder kommen ohne den eingegebenen Wert — der steht im Payload und braucht einen eigenen Filter.
Der Block auf dieser Seite ersetzt die Positions-Tabelle der Vorlage und gibt die Konfiguration so aus, wie der Kunde sie im Warenkorb gesehen hat.
Die Zeilentypen
Confy legt vier eigene Positions-Typen in der Bestellung ab. Der Block unterscheidet sie an lineItem.type:
Typ
Was es ist
label
In der Tabelle
configuration-item
Das konfigurierte Produkt, die Wurzel des Baums
Produktname
Normale Produktzeile mit Artikelnummer, Bild, Menge und Preis
Keine Menge; der Wert kommt aus dem Payload über confyFieldValue
Zwei Werte im Payload eines Schrittes steuern die Ausgabe:
payload.type
Der Schritt-Typ. Steht dort definition, handelt es sich um einen Felder-Schritt: die Tabelle beschriftet ihre dritte Spalte dann mit Wert statt mit Menge und lässt die Spalten für Artikelnummer und Bild weg.
payload.display
Ist der Schritt im Backend versteckt, steht hier false — der Block überspringt ihn samt Kindern. Ein sichtbarer Schritt ganz ohne Kinder wurde abgewählt und wird als solcher ausgewiesen.
Positionsnummern nur bei duplizierbaren Feldsammlungen
Eine Feldsammlung kann der Kunde im Shop mehrfach ausfüllen — pro Ausfüllung entsteht ein eigener Eintrag. Genau dafür gibt es die Zeile Position 1, Position 2, … Ist im Schritt dagegen Feldsammlung nicht duplizierbar gesetzt, existiert genau ein Eintrag, und eine Nummerierung führt in die Irre: sie verspricht eine zweite Position, die es nie geben kann.
Die Bestellung selbst führt dieses Kennzeichen nicht mit — im Payload der Schritt-Position stehen nur type, display, parentId und configurationId. Der Block erkennt den Fall deshalb an der Zahl der Einträge, nach derselben Regel, nach der auch der Storefront seine Überschrift setzt:
{# beim Schritt, bevor die Kinder-Tabelle gerendert wird #}
{% set entryCount = nestedItem.children|length %}
{# beim Eintrag selbst #}
{% set isFieldEntry = nestedItem.type is same as 'configuration-field-entry' %}
{% set showEntryPosition = isFieldEntry and entryCount|default(0) > 1 %}
{% set showEntryRow = not isFieldEntry or showEntryPosition or nestedItem.unitPrice > 0 %}
Der Block
Der Block setzt currencyIsoCode voraus; die Standardvorlage legt die Variable in ihrer ersten Zeile an ({% set currencyIsoCode = order.currency.isoCode %}). Sonst steht er für sich und lässt sich unverändert übernehmen.
{% set notQuantity = ['configuration-field-entry', 'configuration-field-item'] %}
{% set nestedLineItems = order.nestedLineItems %}
{% set nestedItem = nestedLineItems.first %}
{% if nestedLineItems|length < 1 %}
{% set nestedLineItems = order.lineItems %}
{% endif %}
{% for item in nestedLineItems %}
{% if nestedItem is not defined %}
{% set nestedItem = item %}
{% endif %}
{% endfor %}
{% block lineItemTable %}
<table cellspacing='0' style='width: 100%'>
{% if nestedItem.type is not same as 'configuration-field-entry' %}
<tr bgcolor="#F7F7F2"
style="border-top:solid 1px #cccccc; border-bottom:solid 1px #cccccc;">
{% if nestedItem.payload.type is not defined or nestedItem.payload.type is not same as "definition" %}
<th width='60' bgcolor="#F7F7F2" style="border:solid 1px #cccccc; padding: 5px; font-weight: bold">
Produkt-Nr.
</th>
<th width='1' bgcolor="#F7F7F2" style="border:solid 1px #cccccc; padding: 5px; font-weight: bold">
Produktbild
</th>
{% else %}
<th width='10' bgcolor="#F7F7F2"
style="border:solid 1px #cccccc; padding: 5px; font-weight: bold"></th>
{% endif %}
<th width='200' bgcolor="#F7F7F2" style="border:solid 1px #cccccc; padding: 5px; font-weight: bold">
Bezeichnung
</th>
<th width='60' bgcolor="#F7F7F2" style="border:solid 1px #cccccc; padding: 5px; font-weight: bold">
{% if nestedItem.payload.type is defined and nestedItem.payload.type is same as "definition" %}
Wert
{% else %}
Menge
{% endif %}
</th>
<th width='60' bgcolor="#F7F7F2" style="border:solid 1px #cccccc; padding: 5px; font-weight: bold">
Preis
</th>
</tr>
{% endif %}
{% for lineItem in nestedLineItems %}
{% set nestingLevel = 0 %}
{% set nestedItem = lineItem %}
{% block lineItem %}
{% set isFieldEntry = nestedItem.type is same as 'configuration-field-entry' %}
{# "Position n" nur, wenn die Feldsammlung duplizierbar war - erkennbar an mehr als einem Eintrag #}
{% set showEntryPosition = isFieldEntry and entryCount|default(0) > 1 %}
{% set showEntryRow = not isFieldEntry or showEntryPosition or nestedItem.unitPrice > 0 %}
{% if nestedItem.type is not same as 'configuration-step' %}
{% if showEntryRow %}
<tr style="border-top:solid 1px #cccccc;border-bottom:solid 1px #cccccc;">
{% if nestedItem.type is not same as 'configuration-field-item' and nestedItem.type is not same as 'configuration-field-entry' %}
<td style="border:solid 1px #cccccc; padding: 5px;">
{% if nestedItem.payload.productNumber is defined and nestedItem.payload.productNumber is defined %}
{{ nestedItem.payload.productNumber|u.wordwrap(80) }}
{% endif %}
</td>
<td style="border:solid 1px #cccccc; padding: 5px;">
{% if nestedItem.cover is defined and nestedItem.cover is not null %}
<img src="{{ nestedItem.cover.url }}" width="75" height="auto"/>
{% endif %}
</td>
{% endif %}
{% if nestedItem.type is same as 'configuration-field-item' %}
<td style="border:solid 1px #cccccc; padding: 5px; min-width: 2rem"></td>
{% endif %}
<td style="border:solid 1px #cccccc; padding: 5px;">
{% if isFieldEntry %}{% if showEntryPosition %}{{ 'Confy.configuration.step.position'|trans }} {{ nestedItem.label + 1 }}{% endif %}{% else %}{{ nestedItem.label|u.wordwrap(80) }}{% endif %}
{% if nestedItem.payload.options is defined and nestedItem.payload.options is defined and nestedItem.payload.options|length >= 1 %}
<div>
{% for option in nestedItem.payload.options %}
{{ option.group }}: {{ option.option }}
{% if nestedItem.payload.options|last != option %}
{{ " | " }}
{% endif %}
{% endfor %}
</div>
{% endif %}
{% if nestedItem.payload.features is defined and nestedItem.payload.features is defined and nestedItem.payload.features|length >= 1 %}
{% set referencePriceFeatures = nestedItem.payload.features|filter(feature => feature.type == 'referencePrice') %}
{% if referencePriceFeatures|length >= 1 %}
{% set referencePriceFeature = referencePriceFeatures|first %}
<div>
{{ referencePriceFeature.value.purchaseUnit }} {{ referencePriceFeature.value.unitName }}
({{ referencePriceFeature.value.price|currency(currencyIsoCode) }}*
/ {{ referencePriceFeature.value.referenceUnit }} {{ referencePriceFeature.value.unitName }}
)
</div>
{% endif %}
{% endif %}
</td>
<td style="text-align: center; border:solid 1px #cccccc; padding: 5px;">
{% if nestedItem.type not in notQuantity %}
{{ nestedItem.quantity }}
{% endif %}
{% if nestedItem.type == 'configuration-field-item' %}
{% if nestedItem.payload.fieldType == "file" %}
{% for file in nestedItem.payload.files %}
{{ file.name }}<br/>
{% endfor %}
{% else %}
{{ nestedItem.payload|confyFieldValue }}
{% endif %}
{% endif %}
</td>
<td style="border:solid 1px #cccccc; padding: 5px;">
{% if nestedItem.unitPrice > 0 %}
{% if nestedItem.price.listPrice is defined and nestedItem.price.listPrice is not null and nestedItem.price.listPrice.percentage > 0 %}
<s>{{ nestedItem.price.listPrice.price|currency(currencyIsoCode) }}</s>
<span style='color: red; font-weight: bold'>
-{{ nestedItem.price.listPrice.percentage }}%
</span>
<br/>
{{ nestedItem.unitPrice }}
{% else %}
{{ nestedItem.unitPrice|currency(currencyIsoCode) }}
{% endif %}
<br/>
{{ nestedItem.totalPrice|currency(currencyIsoCode) }}
{% endif %}
</td>
{% if isFieldEntry %}
<td style="border:solid 1px #cccccc; padding: 5px;"></td>
{% endif %}
</tr>
{% endif %}
{% if nestedItem.children|length > 0 %}
{% set nestingLevel = nestingLevel + 1 %}
{% for lineItem in nestedItem.children %}
{% set nestedItem = lineItem %}
{{ block('lineItem') }}
{% endfor %}
{% endif %}
{% elseif nestedItem.payload.display is defined and nestedItem.payload.display %}
<tr style="border-top:solid 1px #cccccc;border-bottom:solid 1px #cccccc;">
<td style="border:solid 1px #cccccc; padding: 5px;"></td>
<td style="border:solid 1px #cccccc; font-weight: bold; padding: 11px 5px;"
colspan='4'>{{ nestedItem.label|u.wordwrap(80) }}</td>
</tr>
<tr style="border-top:solid 1px #cccccc;border-bottom:solid 1px #cccccc;">
<td style="border:solid 1px #cccccc; padding: 5px;"></td>
{% set nestedLineItems = nestedItem.children %}
{% set entryCount = nestedItem.children|length %}
{% if nestedLineItems|length > 0 %}
<td colspan='4'>{{ block('lineItemTable') }}</td>
{% else %}
<td colspan='4' style="border:solid 1px #cccccc; padding: 5px; font-weight: bold; text-align: center">
Option wurde abgewählt
</td>
{% endif %}
</tr>
{% endif %}
{% endblock %}
{% endfor %}
</table>
{% endblock %}
Einbau
Block in eine Vorlage einsetzen
Im Backend Einstellungen → E-Mail-Vorlagen öffnen und die Vorlage wählen, die Positionen auflistet — in der Regel Bestellbestätigung. Dieselbe Anpassung lohnt für jede weitere Vorlage, die eine Positionsliste enthält (etwa Stornierung oder Statuswechsel).
Im Reiter HTML die vorhandene Positions-Tabelle ersetzen: alles von {% set notQuantity = … %} beziehungsweise von der Schleife über order.nestedLineItems bis zum Ende des Blocks lineItemTable. Der Summenblock darunter (Versandkosten, Netto, Steuer, Gesamt) bleibt unberührt.
Speichern und über Test-E-Mail senden eine Bestellung mit Konfiguration prüfen. Ohne echte Bestellung greift Shopware auf Beispieldaten zurück — die enthalten keine Confy-Positionen.
Snippets und Filter
Baustein
Herkunft
Zweck
Confy.configuration.step.position
Storefront-Snippets von Confy
Das Wort vor der laufenden Nummer — „Position"
Confy.fields.error.valueMissing
Storefront-Snippets von Confy
Rückfall, wenn ein Feld keinen lesbaren Wert trägt
confyFieldValue
Twig-Filter von Confy
Macht aus dem Feld-Payload den lesbaren Wert: Optionsnamen statt IDs, Bereiche als „von - bis", Datum und Uhrzeit in der Sprache des Verkaufskanals
Die Beschriftungen ändern Sie nicht in der Vorlage, sondern unter Einstellungen → Snippets — dann stimmen E-Mail und Shop überein.
Grenzen
Dateifelder
Die Vorlage listet die Dateinamen aus payload.files. Die hochgeladenen Dateien selbst hängen nicht an der E-Mail.
Feste Texte
Die Spaltenköpfe (Produkt-Nr., Produktbild, Bezeichnung, Menge beziehungsweise Wert, Preis) und der Hinweis Option wurde abgewählt bei einem abgewählten Schritt stehen fest im Block. Wer mehrsprachig versendet, ersetzt sie durch eigene Snippets oder pflegt je Sprache eine eigene Vorlage — so hält es Shopware auch bei der Standardvorlage.
Belege
Rechnung und Lieferschein entstehen aus Dokument-Vorlagen, nicht aus E-Mail-Vorlagen. Dieser Block gilt für den E-Mail-Versand.