Der Rundungsfehler wurde in Magento 1.8 behoben. Bei starker Modifikation durch die vielen Erweiterungen kann ein Update von Magento aufwändig werden, sodass die Rundungsfehlerbehebung mit einer leichtgewichtigen Erweiterung für viele Shop-Betreiber weiterhin interessant bleibt.
Rounding Error Fix for Magento and PayPal auf Magento Connect
Ursprung des Rundungsfehlers
Nach PayPal-Anforderungen müssen alle Preise als kaufmännisch auf zwei Nachkommastellen gerundete Fixpunktzahlen übermittelt werden.
Bei kaufmännischer Rundung wird beispielsweise 1,005 auf 1,01 gerundet und 1,004 auf 1,00. Offensichtlich kann dies zu einem Rundungsfehler führen, je nach dem in welchem Rechenschritt gerundet wird:
- Rundung der Summe: 1,004 + 1,004 = 2,008 ≈ 2,01
- Rundung der Summanden: 1,004 ? 1,00; 1,00 + 1,00 = 2,00 ≠ 2,01
Wenn bei PayPal-Zahlungen Warenkorbartikel übermittelt werden, werden ihre Nettopreise und der Mehrwertsteuerbetrag übermittelt, sodass PayPal auf seiner Seite den Brutto-Gesamtbetrag berechnet. Durch die Umrechnung der Bruttopreise kann es zu einem Rundungsfehler kommen:
- Angenommen der Warenkorb enthält drei verschiedene virtuelle Artikel (d. h. keine Versandkosten).
- Jeder Artikel kostet brutto 9,99 €, sodass der Brutto-Warenkorbwert 3 * 9,99 € = 29,97 € beträgt.
- An PayPal werden Nettopreise und der Mehrwertsteuerbetrag übertragen:
- 29,97 € / 1,19 * 0,19 = 4,7851 € ≈ 4,79 €
- 9,99 € / 1,19 = 8,3949 ≈ 8,39 €
- 3 * 8,39 € + 4,79 € = 29,96 € ≠ 29,97 €
- Durch den Rundungsfehler ist eine Abweichung um 0,01 € entstanden!
Doch in der Praxis bekommt PayPal die Daten so übermittelt, dass der Brutto-Gesamtbetrag mit dem von Magento bis auf Cent genau übereinstimmt:
Obwohl der Bruttopreis bei allen drei Produkten gleich war, wurde in einem der drei Fälle ein anderer Nettopreis berechnet!
Die Korrektur des Rundungsfehlers findet allerdings nicht im PayPal-Modul statt, sondern bereits im Warenkorb. In der Methode Mage_Tax_Model_Sales_Total_Quote_Subtotal ::_totalBaseCalculation wird der Rundungsfehler auf den Nettopreis des nächsten Produkts in der Liste vor der Rundung aufaddiert wird. Die Rundungsfehler werden im Array _roundingDeltas von _deltaRound verwaltet. Im obigen Beispiel rechnet Magento, reduziert auf das Wesentliche, wie folgt:
- 1. Artikel: 9,99 / 1,19 = 8,39495798
- Kaufmännisch gerundet: 8,39
- Rundungsfehler: 8,39495798 – 8,39 = 0,00495798
- 2. Artikel: 9,99 / 1,19 + 0,00495798 = 8,39991597
- Kaufmännisch gerundet: 8,40
- Rundungsfehler: 8,39991597 – 8,40 = -0,000084034
- 3. Artikel: 9,99 / 1,19 – 0,000084034 = 8,39487395
- Kaufmännisch gerundet: 8,39
- Rundungsfehler: 8,39487395 – 8,39 = 0,00487395
Die Ergebnisse dieser Berechnung lassen sich bereits beim Befüllen des Warenkorbs beobachten. In der Datenbank sind die gerundeten und korrigierten Nettopreise beispielsweise in der Spalte base_price der Tabelle sales_flat_quote_item sichtbar.
Der Ursprung des Rundungsfehlers ist also nicht das PayPal-Modul. Wir haben beobachtet, dass beim Auftreten des Rudnungsfehlers dieser von dem Mehrwertsteuerbetrag ausgeht. Die beschriebene Rudnungsfehlerkorrektur wirkt sich auch auf die Berechnung des Mehrwertsteuerbetrags aus.
Bisherige Lösungsansätze
Im Web gibt es zahlreiche Berichte und Lösungsansätze für die Behebung des Rundungsfehlers. In allen Fällen wird die Rundungsgenauigkeit in Mage_Core_Model_Store::roundPrice von zwei auf vier Nachkommastellen erhöht.
Diese Anpassung verursacht einen Rundungsfehler an anderen Stellen, dem durch weitere Anpassungen entgegengewirkt werden muss. In einigen Lösungen werden Formulardaten des PayPal-Moduls durch Addieren oder Subtrahieren von 0.01 vor dem Absenden manipuliert.
Keines dieser Ansätze behebt den Rundungsfehler vollständig und für alle Konstellationen von Produktpreisen und Steuereinstellungen. Dennoch danken wir allen Authoren, die ihre Lösungen veröffentlicht haben, denn sie haben uns bei der Entwicklung dieser Erweiterung sehr geholfen!
Behebung des Rundungsfehlers mit Xonu_RoundingErrorFix
Auch unsere Erweiterung überschreibt Mage_Core_Model_Store::roundPrice und manipuliert dort die Rundungsgenauigkeit.
Dabei wird abhängig von der aufrufenden Klasse entschieden, auf wieviele Nachkommastellen gerundet wird: Für Aufrufe, die von der Klasse Mage_Tax_Model_Sales_Total_Quote_Subtotal ausgehen wird auf zwei, für alle restlichen Aufrufe auf vier Nachkommastellen genau gerundet.
Wie oben gezeigt, ist diese Klasse in die Berechnung der kaufmännisch gerundeten Nettopreise mit Rundungsfehlerkorrektur involviert. Erhöhung der Rundungsgenauigkeit auf vier Nachkommastellen stört diese Rechenlogik erheblich!
PayPal verwendet für die Verarbeitung der Zahlung die Methode Mage_Sales_Model_Order_Payment::registerCaptureNotification. U. a. hier wird der der „Betrugsverdacht“ erkannt, wenn _isCaptureFinal einen false zurückgibt:
public function registerCaptureNotification($amount) { .... if (!$invoice) { if ($this->_isCaptureFinal($amount)) { $invoice = $order->prepareInvoice()->register(); $order->addRelatedObject($invoice); $this->setCreatedInvoice($invoice); } else { $this->setIsFraudDetected(true); $this->_updateTotals(array('base_amount_paid_online' => $amount)); } } .... }
In Mage_Sales_Model_Order_Payment::_isCaptureFinal wird der über PayPal gezahlter Betrag, übermittelt in der IPN, mit dem Gesamtbetrag der Bestellung verglichen:
protected function _isCaptureFinal($amountToCapture) { $amountToCapture = $this->_formatAmount($amountToCapture, true); $orderGrandTotal = $this->_formatAmount($this->getOrder()->getBaseGrandTotal(), true); if ($orderGrandTotal == $this->_formatAmount($this->getBaseAmountPaid(), true) + $amountToCapture) { if (false !== $this->getShouldCloseParentTransaction()) { $this->setShouldCloseParentTransaction(true); } return true; } return false; }
Dabei wird die Methode Mage_Sales_Model_Order_Payment::_formatAmount verwendet, sodass wenn roundPrice auf vier statt zwei Nachkommastellen rundet, die Beträge nicht übereinstimmen könnten, weil der von PayPal übermittelte Betrag immer auf zwei Nachkommastellen gerundet ist:
protected function _formatAmount($amount, $asFloat = false) { $amount = Mage::app()->getStore()->roundPrice($amount); return !$asFloat ? (string)$amount : $amount; }
Die Erweiterung überschreibt auch diese Methode, sodass sie auf zwei Nachkommastellen genau rundet:
protected function _formatAmount($amount, $asFloat = false) { $amount = round($amount,2); return !$asFloat ? (string)$amount : $amount; }
Eine weitere Methode im PayPal-Modul scheitert an der Erhöhung der Nachkommastellen in roundPrice: Mage_Paypal_Model_Api_Abstract::_filterAmount und liefert in bestimmten Fällen ein Ergebnis, welches wiederum einen Rundungsfehler verursacht:
protected function _filterAmount($value) { return sprintf('%.2F', $value); }
Auch diese Methode überschreiben wir:
protected function _filterAmount($value) { return sprintf('%.2F', round($value, 2)); }
In dem Update der Erweiterung werden zusätzlich noch die PayPal-Formulardaten vor der Übermittlung mit den Bestelldaten verglichen. Bei einer Abweichung wird der Rundungsfehler durch gezielte Anpassung der Formulardaten korrigiert.
Rounding Error Fix for Magento and PayPal auf Magento Connect