Preisregeln pro Store ohne Rundungsfehler
Magento 2 Währungsumrechnung wirkt auf den ersten Blick wie ein einfacher Faktor zwischen zwei Zahlen, ist in der Praxis aber ein Zusammenspiel aus Basiswährung, Kurs-Import, Rundungsregeln und Preisregeln, das pro Store unterschiedlich konfiguriert werden muss. Wer Basiswährung und Anzeigewährung verwechselt, riskiert Preisdrift bei jedem Kursupdate und Rabattregeln, die in der falschen Währung greifen.
Inhaltsverzeichnis
- 1. Warum Währungsumrechnung mehr als ein Faktor ist
- 2. Basiswährung versus Anzeigewährung
- 3. Preisumfang: Website-Scope entscheidet über Kurse
- 4. Kurs-Import: Cron, Provider und eigene Connectoren
- 5. Rundung und Preis-Anzeige ohne Nachkommastellen-Chaos
- 6. Cart Price Rules pro Store view korrekt begrenzen
- 7. Steuerklassen und Währung im Zusammenspiel
- 8. Typische Fallstricke bei Kursänderungen
- 9. Strategien im Vergleich: fester Kurs versus Live-Kurs
- 10. Zusammenfassung
- 11. FAQ
1. Warum Währungsumrechnung mehr als ein Faktor ist
Sobald ein Magento-2-Shop mehrere Länder mit unterschiedlichen Landeswährungen bedient, wird die Magento 2 Währungsumrechnung zu einem der Themen, die auf den ersten Blick trivial wirken und in der Praxis überraschend viele Stellschrauben haben. Es genügt nicht, einfach einen Umrechnungsfaktor zu hinterlegen. Magento unterscheidet strikt zwischen der Basiswährung, in der Preise intern gespeichert und Bestellungen für die Buchhaltung verarbeitet werden, und der Anzeigewährung, die dem Kunden im jeweiligen Store view präsentiert wird.
Diese Trennung ist kein Implementierungsdetail, sondern eine bewusste Architekturentscheidung: Sie erlaubt es, in einem Store mehrere Anzeigewährungen anzubieten, ohne die zugrunde liegende Preislogik und die Rabattregeln mehrfach pflegen zu müssen. Gleichzeitig entstehen dadurch Fehlerquellen, die ohne Verständnis der zugrunde liegenden Mechanik schwer zu diagnostizieren sind, etwa wenn eine Preisregel pro Store unerwartet einen anderen Betrag rabattiert als erwartet, weil sie auf Basiswährung statt Anzeigewährung wirkt.
2. Basiswährung versus Anzeigewährung
Magento speichert jeden Produktpreis, jede Rechnung und jede Bestellposition primär in der Basiswährung der jeweiligen Website, konfiguriert unter currency/options/base. Diese Basiswährung ist bewusst auf Website-Ebene und nicht auf Store-View-Ebene skopiert, weil eine Website als wirtschaftliche Einheit genau eine Buchführungswährung besitzt. Die Anzeigewährung dagegen, konfiguriert unter currency/options/default und currency/options/allow, ist auf Store-View-Ebene gültig und bestimmt, in welcher Währung Preise dem Kunden präsentiert werden.
Für die Preisregeln pro Store bedeutet das: Eine Cart Price Rule, die einen festen Rabattbetrag definiert, etwa zehn Euro Nachlass, bezieht sich technisch auf die Basiswährung der Website und wird erst zur Anzeige in die jeweilige Store-View-Währung umgerechnet. Wer eine Website mit Basiswährung Euro betreibt, aber einen Store view mit Anzeigewährung US-Dollar anbietet, muss diesen Umrechnungsschritt bei jeder festen Rabattsumme mitdenken, weil sich der tatsächliche Dollarwert des Rabatts mit jedem Kursupdate verändert.
<?php
declare(strict_types=1);
namespace Mironsoft\CurrencyTools\ViewModel;
use Magento\Directory\Model\CurrencyFactory;
use Magento\Framework\View\Element\Block\ArgumentInterface;
use Magento\Store\Model\StoreManagerInterface;
/**
* ViewModel exposing the effective currency conversion rate for a store view.
*/
final class ConversionRate implements ArgumentInterface
{
/**
* @param StoreManagerInterface $storeManager Resolves store base currency
* @param CurrencyFactory $currencyFactory Creates currency conversion models
*/
public function __construct(
private readonly StoreManagerInterface $storeManager,
private readonly CurrencyFactory $currencyFactory
) {
}
/**
* Get the current conversion rate from the website base currency
* to the store view display currency.
*
* @return float Conversion rate, 1.0 if currencies are identical
* @throws \Magento\Framework\Exception\NoSuchEntityException
*/
public function getRate(): float
{
$store = $this->storeManager->getStore();
$baseCode = $store->getBaseCurrencyCode();
$displayCode = $store->getCurrentCurrencyCode();
if ($baseCode === $displayCode) {
return 1.0;
}
$currency = $this->currencyFactory->create()->load($baseCode);
return (float) $currency->getRate($displayCode);
}
}
3. Preisumfang: Website-Scope entscheidet über Kurse
Der Konfigurationswert catalog/price/scope steuert, ob Produktpreise global oder pro Website individuell gepflegt werden können, und diese Entscheidung wirkt sich direkt auf die Magento 2 Währungsumrechnung aus. Bei globalem Preisumfang existiert nur ein Basispreis pro Produkt, der bei mehreren Websites mit unterschiedlichen Basiswährungen automatisch über den hinterlegten Kurs umgerechnet wird. Bei Website-Preisumfang kann für jede Website ein eigener, manuell gepflegter Preis hinterlegt werden, der die automatische Kursumrechnung faktisch außer Kraft setzt.
Die Wahl zwischen beiden Modi ist eine strategische Entscheidung, die früh im Projekt getroffen werden sollte, denn ein nachträglicher Wechsel des Preisumfangs erfordert eine vollständige Neuindizierung und oft eine manuelle Nachpflege aller Website-spezifischen Preise. Für Shops, die tatsächlich unterschiedliche Preispunkte pro Markt benötigen, etwa aus Wettbewerbsgründen, ist Website-Preisumfang der richtige Weg. Für Shops, die konsistente, kursbasierte Preise über alle Märkte hinweg wollen, ist globaler Preisumfang mit automatischer Kursumrechnung die wartungsärmere Lösung.
4. Kurs-Import: Cron, Provider und eigene Connectoren
Magento liefert von Haus aus einen Kurs-Import-Mechanismus über bin/magento currency:import, der Wechselkurse von einem konfigurierten Provider abruft, etwa Fixer.io oder Webservicex, und diese in die Tabelle directory_currency_rate schreibt. Für produktive Shops ist dieser Import über einen Cron-Job zu automatisieren, damit die Magento 2 Währungsumrechnung jederzeit aktuelle Kurse verwendet, statt auf veralteten, manuell eingegebenen Werten zu basieren. Der Cron-Job selbst wird über system/currency/cron konfiguriert und läuft typischerweise einmal täglich.
Für Unternehmen mit eigenen Treasury-Prozessen oder speziellen vertraglichen Kursvereinbarungen mit Banken ist ein eigener Import-Connector sinnvoll, der das Interface Magento\Directory\Model\Currency\Import\ImportInterface implementiert. Damit lassen sich Kurse aus internen Systemen, etwa einem ERP mit Treasury-Modul, direkt in Magento einspeisen, ohne den Umweg über einen externen Kurs-Provider und dessen mögliche API-Limits oder Ausfallzeiten.
#!/usr/bin/env bash
set -euo pipefail
# Run the native currency import for the configured provider
bin/magento currency:import
# Cron entry (crontab -e), matches system/currency/cron config
# 0 4 * * * /usr/bin/php /var/www/magento/bin/magento currency:import >> /var/log/magento/currency-import.log 2>&1
# Simple monitoring wrapper: alert if the import silently stopped updating
LOG_FILE="/var/log/magento/currency-import.log"
MAX_AGE_HOURS=30
if [[ ! -f "$LOG_FILE" ]]; then
echo "[ALERT] Currency import log missing entirely" >&2
exit 1
fi
last_modified=$(stat -c %Y "$LOG_FILE")
now=$(date +%s)
age_hours=$(( (now - last_modified) / 3600 ))
if (( age_hours > MAX_AGE_HOURS )); then
echo "[ALERT] Currency import log not updated for ${age_hours}h" >&2
exit 1
fi
echo "[OK] Currency import ran ${age_hours}h ago"
<?php
declare(strict_types=1);
namespace Mironsoft\CurrencyTools\Model\Currency\Import;
use Magento\Directory\Model\Currency\Import\AbstractImport;
use Magento\Framework\HTTP\Client\CurlFactory;
/**
* Custom currency import connector reading rates from an internal treasury API.
*/
final class TreasuryImport extends AbstractImport
{
private const string TREASURY_ENDPOINT = 'https://treasury.internal.example/api/rates';
/**
* @param CurlFactory $curlFactory HTTP client factory for the internal API
* @param array $data Additional constructor data passed by Magento
*/
public function __construct(
private readonly CurlFactory $curlFactory,
array $data = []
) {
parent::__construct($data);
}
/**
* Fetch conversion rates for the given currencies from the treasury API.
*
* @param string $currencyFrom Base currency code, e.g. "EUR"
* @param array $currenciesTo Target currency codes, e.g. ["USD", "GBP"]
* @return array Map of target currency code to conversion rate
* @throws \Magento\Framework\Exception\LocalizedException
*/
protected function _convert($currencyFrom, $currenciesTo)
{
$client = $this->curlFactory->create();
$client->get(self::TREASURY_ENDPOINT . '?base=' . $currencyFrom);
$rates = json_decode($client->getBody(), true);
$result = [];
foreach ($currenciesTo as $code) {
$result[$code] = $rates['rates'][$code] ?? 0.0;
}
return $result;
}
}
5. Rundung und Preis-Anzeige ohne Nachkommastellen-Chaos
Ein häufig unterschätztes Detail der Magento 2 Währungsumrechnung ist die Rundung. Nicht jede Währung nutzt zwei Nachkommastellen, japanische Yen etwa kennt keine Nachkommastellen, während manche Kryptowährungs-Erweiterungen mit deutlich mehr arbeiten. Magento nutzt für die Formatierung die PHP-Intl-Erweiterung und die im Store view hinterlegte Locale, um Tausendertrennzeichen, Dezimaltrennzeichen und Nachkommastellen korrekt darzustellen, unabhängig vom internen Rechenwert.
Ein subtiler Fehler entsteht, wenn eigene Berechnungen, etwa in einem Preisregel-Plugin, den umgerechneten Wert direkt runden, statt Magentos PriceCurrencyInterface::round() zu nutzen. Diese Methode berücksichtigt die tatsächliche Nachkommastellen-Konfiguration der Zielwährung und vermeidet, dass in Yen plötzlich Nachkommawerte angezeigt werden, die für diese Währung schlicht nicht existieren. Wer eigene Preisberechnungen implementiert, sollte diese Methode konsequent statt einer eigenen round()-Implementierung verwenden.
6. Cart Price Rules pro Store view korrekt begrenzen
Cart Price Rules lassen sich in Magento auf bestimmte Websites und Kundengruppen einschränken, jedoch nicht direkt auf einzelne Store Views, da eine Rule technisch auf Website-Ebene wirkt. Für Preisregeln pro Store, die tatsächlich nur in einem bestimmten Store view greifen sollen, etwa eine regionale Rabattaktion für den österreichischen Markt bei gemeinsamer Website mit Deutschland, ist eine Erweiterung über einen eigenen sales_rule_validator-Plugin nötig, der zusätzlich den aktuellen Store view prüft.
Ein typisches Muster hierfür ist ein Plugin auf Magento\SalesRule\Model\Validator::canApplyRule(), das anhand einer in der Rule-Beschreibung hinterlegten Konvention, etwa einem Präfix wie [STORE:at], den erlaubten Store view extrahiert und die Rule nur dann anwendet, wenn der aktuelle Store view übereinstimmt. Diese Lösung ist pragmatisch, sollte aber gut dokumentiert werden, da sie eine Konvention außerhalb des regulären Magento-Backends einführt, die neue Redakteure sonst leicht übersehen.
<?php
declare(strict_types=1);
namespace Mironsoft\CurrencyTools\Plugin;
use Magento\SalesRule\Model\Validator;
use Magento\SalesRule\Model\Rule;
use Magento\Quote\Model\Quote\Address;
use Magento\Store\Model\StoreManagerInterface;
/**
* Restricts a cart price rule to a single store view via a naming
* convention in the rule description, e.g. "[STORE:at]".
*/
final class RestrictRuleToStoreView
{
/**
* @param StoreManagerInterface $storeManager Resolves the current store view
*/
public function __construct(
private readonly StoreManagerInterface $storeManager
) {
}
/**
* @param Validator $subject Native cart price rule validator
* @param bool $result Native validation result
* @param Rule $rule The rule being validated
* @param Address $address Quote address providing store context
* @return bool False if the rule is restricted to a different store view
* @throws \Magento\Framework\Exception\NoSuchEntityException
*/
public function afterCanApplyRule(
Validator $subject,
bool $result,
Rule $rule,
Address $address
): bool {
if (!$result || !preg_match('/\[STORE:([a-z_]+)]/', (string) $rule->getDescription(), $matches)) {
return $result;
}
$currentStoreCode = $this->storeManager->getStore()->getCode();
return $matches[1] === $currentStoreCode;
}
}
7. Steuerklassen und Währung im Zusammenspiel
Steuerregeln und Magento 2 Währungsumrechnung sind technisch unabhängige Systeme, wirken sich aber gemeinsam auf den final angezeigten Preis aus. Die Steuerberechnung erfolgt immer zuerst auf Basis des Basispreises in der Basiswährung, danach wird der steuerinklusive Betrag in die Anzeigewährung umgerechnet. Diese Reihenfolge ist wichtig, weil eine umgekehrte Reihenfolge, erst umrechnen und dann Steuer berechnen, bei krummen Kursen zu minimal abweichenden Endpreisen führen kann, die zwar in der Regel unter einem Cent liegen, in Summe über viele Bestellungen aber zu Rundungsdifferenzen in der Buchhaltung führen.
Für Shops mit US-amerikanischen Store Views kommt zusätzlich hinzu, dass Steuersätze dort oft auf Ebene von Bundesstaaten oder sogar Landkreisen unterschiedlich sind, während die Währungsumrechnung store-view-weit einheitlich funktioniert. Diese Kombination aus granularer Steuerlogik und globalerer Währungslogik erfordert bei der Konfiguration besondere Sorgfalt, insbesondere wenn eine Steuerklasse fälschlich auf Website- statt auf feingranularer Regel-Ebene gepflegt wird.
8. Typische Fallstricke bei Kursänderungen
Der offensichtlichste Fallstrick bei der Magento 2 Währungsumrechnung ist ein fehlgeschlagener oder nicht laufender Cron-Job für den Kurs-Import. Bleibt der Kurs wochenlang eingefroren, während sich der reale Wechselkurs deutlich verschiebt, entstehen für den Shop entweder unnötige Margenverluste oder für den Kunden überteuerte Preise im Vergleich zum Wettbewerb. Ein Monitoring, das den Zeitstempel der letzten erfolgreichen Kursaktualisierung prüft und bei Überschreiten eines Schwellwerts alarmiert, ist daher für internationale Shops praktisch Pflicht.
Ein zweiter Fallstrick betrifft feste Rabattbeträge in Preisregeln, die über mehrere Store Views mit unterschiedlichen Anzeigewährungen hinweg gelten sollen. Da diese Beträge in Basiswährung definiert sind und erst zur Anzeige umgerechnet werden, verschiebt sich der wahrgenommene Rabattwert bei jedem Kursupdate leicht. Für Marketing-Kampagnen mit einem klar kommunizierten, runden Rabattbetrag in der jeweiligen Landeswährung ist es oft sinnvoller, prozentuale statt fixe Rabatte einzusetzen, da Prozentsätze kursunabhängig konstant bleiben.
-- Detect currencies whose exchange rate has not been refreshed recently
SELECT currency_to, currency_from, rate
FROM directory_currency_rate
WHERE currency_from = 'EUR'
AND currency_to IN ('USD', 'GBP', 'CHF')
-- combine with an application-level check of the import timestamp,
-- directory_currency_rate itself has no built-in "last updated" column
;
-- Cross-check store base currencies against configured allowed currencies
SELECT s.code AS store_code, ccd.value AS base_currency
FROM store s
INNER JOIN core_config_data ccd
ON ccd.scope = 'stores'
AND ccd.scope_id = s.store_id
AND ccd.path = 'currency/options/base'
ORDER BY s.store_id;
9. Strategien im Vergleich: fester Kurs versus Live-Kurs
Ob ein Shop mit täglich aktualisierten Live-Kursen oder mit bewusst festgelegten, manuell gepflegten Kursen arbeitet, hängt stark vom Geschäftsmodell ab. Die folgende Tabelle stellt beide Strategien für die Magento 2 Währungsumrechnung gegenüber.
| Kriterium | Live-Kurs (täglicher Import) | Fester, manuell gepflegter Kurs |
|---|---|---|
| Preisstabilität für Kunden | Schwankt täglich minimal | Konstant, planbar |
| Margensicherheit | Immer marktnah | Risiko bei starken Kursbewegungen |
| Pflegeaufwand | Automatisiert per Cron | Manuelle Reviews nötig |
| Eignung für Preisregeln | Prozentuale Rabatte bevorzugen | Feste Rabattbeträge planbar |
In der Praxis entscheiden sich die meisten international tätigen Shops für einen automatisierten Kurs-Import mit einer definierten Toleranzgrenze, innerhalb derer Preise stabil bleiben, kombiniert mit einem manuellen Review-Prozess bei größeren Kursausschlägen. Diese hybride Strategie verbindet die Automatisierung des Live-Kurses mit der Preisstabilität eines festen Kurses, ohne die Nachteile beider Extremvarianten vollständig zu übernehmen.
Mironsoft
Magento 2 Multi Store und Internationalisierung
Währungsumrechnung ohne Margen- oder Rundungsrisiko?
Wir konfigurieren Kurs-Import, Preisumfang und Cart Price Rules für internationale Magento-2-Shops, damit Preise pro Store view konsistent bleiben und Rabattaktionen wie geplant funktionieren.
Kurs-Automatisierung
Cron-Import mit Monitoring und Toleranzgrenzen einrichten
Preisregel-Audit
Bestehende Cart Price Rules auf Store-Scope-Probleme prüfen
Eigene Connectoren
Treasury- oder ERP-Kursanbindung als eigenes Import-Modul
10. Zusammenfassung
Die Magento 2 Währungsumrechnung funktioniert zuverlässig, wenn Basiswährung und Anzeigewährung als getrennte Konzepte behandelt werden und der Kurs-Import automatisiert und überwacht läuft. Der Preisumfang auf Website-Ebene entscheidet grundsätzlich, ob Kurse automatisch angewendet oder durch manuell gepflegte Preise ersetzt werden. Preisregeln pro Store, die feste Rabattbeträge nutzen, sollten sich der Kursabhängigkeit bewusst sein und wo sinnvoll auf prozentuale Rabatte umsteigen.
Rundung sollte konsequent über Magentos eigene Preis-Utilities laufen, nicht über eigene Implementierungen, um Nachkommastellen-Fehler bei Währungen wie Yen zu vermeiden. Wer Store-view-spezifische Preisregeln benötigt, die über die native Website-Beschränkung hinausgehen, kommt um ein eigenes Validator-Plugin nicht herum, sollte diese Erweiterung aber sauber dokumentieren, damit sie im Tagesgeschäft nachvollziehbar bleibt.
Magento 2 Währungsumrechnung — Das Wichtigste auf einen Blick
Basis- vs. Anzeigewährung
Preise werden in Basiswährung der Website gespeichert, erst zur Darstellung in die Store-View-Anzeigewährung umgerechnet.
Kurs-Import automatisieren
bin/magento currency:import per Cron, mit Monitoring des Zeitstempels der letzten erfolgreichen Aktualisierung.
Preisregeln kursabhängig prüfen
Feste Rabattbeträge verschieben sich mit jedem Kursupdate. Prozentuale Rabatte bleiben kursunabhängig stabil.
Rundung über PriceCurrencyInterface
Immer Magentos round()-Methode nutzen, damit Nachkommastellen pro Zielwährung korrekt behandelt werden.