Ab dafür …

Das altbekannte Problem mit den Staffelpreisen in xt:Commerce: Da bietet man einen deutlichen Mengenrabatt an - Aber dort, wo sich der potentielle Kunde das erste Mal entscheidet, ob er weiter klicken möchte - wird nur der normale Artikelpreis angezeigt.

Die Preisstaffelungen werden erst in der Detail-Ansicht eines Artikels sichtbar, nur soweit muss ein Besucher erst einmal kommen. Besonders bei Produkten, von denen üblicherweise ohnehin gleich mehrere gekauft werden (z.B. Vereins-Trikots, Büromaterial, Leichtmetall-Felgen), ist es schade, wenn man nicht schon in den Produkt-Übersichten mit dem günstigsten Preis werben kann. Der Einzelpreis sollte dabei natürlich nicht verschwiegen werden, ganz klar …

 

Wie kommt man an den günstigsten Staffelpreis?

Leider ist xt:Commerce teilweise etwas verwirrend: Während man beispielsweise in der “product_listing_v1.html” neben den üblichen Produkt-Daten auch an die ID des jeweiligen Artikels herankommt, sind die anderen Produkt-Auflistungen anders ausgestattet. Obwohl sie exakt das selbe anzeigen.

Also kommt man über die Produkt-ID und eine reine Template-Lösung schon mal nicht so recht zum Ziel. Schade eigentlich. Wenn man von jedem angezeigten Produkt die ID wüsste, könnte man sich so ziemlich alle anderen Eckdaten auch ohne Änderungen an System-Files herausziehen.

Daher muss man wieder mal ein bisschen am System herumbasteln. Der Eingriff ist aber recht unproblematisch und nimmt nur eine Anzeige-Änderung vorweg, die xt:Commerce ohnehin durchführt, sobald man sich ein Produkt zu einem Mengenrabatt-Preis gekauft hat …

 

Ausgangssituation:

Produkt A kostet im Einzelpreis 10,00 €, ab 5 Stück 8,00 € und ab 10 Stück 7,50 €. Der Shop zeigt dann normalerweise in allen Produktübersichten Folgendes an:

Ab 10,00 €

Aber das bleibt nicht immer so - denn (Hoppla?) sobald man mehr als 5 Stück vom Produkt A in den Warenkorb gelegt hat, springen alle Preis-Anzeigen für diesen Artikel um:

UVP 10,00 €
Ihr Preis: 8,00 €

xt:Commerce entscheidet in der Datei xtcPrice.php selbständig, dass ein Sonderpreis vorliegt, sobald der tatsächlich zu zahlende Einzelpreis Variable $sPrice geringer als der Artikelgrundpreis $pPrice ist. Und dieser Fall tritt bei Staffelpreisen dann ein, wenn eine Staffelung überschritten ist - denn ab dann liegen die gültigen Einzelpreise unter dem Normalpreis.

Und damit haben wir schon fast, was wir wollen. Der günstigste Preis wäre in unserem Fall zwar noch einmal um 50 Cent geringer - Aber immerhin - jetzt weiß man, wo man suchen und was man versuchen muss.

Wir müssen also für jeden angezeigten Preis abfragen …

  • ob es Staffelpreise gibt
  • ob der Besucher in einer Kundengruppe ist, die auch Staffelpreise sehen darf
  • und letztlich wie hoch die Abnahmemenge ist, um in die beste, für die jeweilige Kundengruppe gültige Staffelung zu kommen

 

Der Ansatz:

Wo auch immer ein Preis ausgegeben wird - Er durchläuft immer die Funktionen in der “xtcPrice.php” zu finden im Ordner “includes/classes/”.

Da es sich bei unseren Staffelpreisen ja um einen reduzierten Preis handelt - jedenfalls soweit eine bestimmte Abnahme-Menge überschritten worden ist - wird die Funktion xtcFormatSpecialGraduated(); ausgeführt. Diese greift bei Sonder- und bei Staffelpreisen und sorgt dafür, dass reduzierte Preise anders dargestellt werden als normale.

In xt:Commerce 3.04 SP1 findet man diese Funktion in den Zeilen 361 - 380:


function xtcFormatSpecialGraduated($pID, $sPrice, $pPrice, $format, $vpeStatus = 0, $pID) {
	if ($pPrice == 0)
		return $this->xtcFormat($sPrice, $format, 0, false, $vpeStatus);
	if ($discount = $this->xtcCheckDiscount($pID))
		$sPrice -= $sPrice / 100 * $discount;
	if ($format) {
		if ($sPrice != $pPrice) {
			$price = '<span class="productOldPrice">'.MSRP.$this->xtcFormat($pPrice, $format).'</span><br />'.YOUR_PRICE.$this->checkAttributes($pID).$this->xtcFormat($sPrice, $format);
		} else {
			$price = FROM.$this->xtcFormat($sPrice, $format);
		}
		if ($vpeStatus == 0) {
			return $price;
		} else {
			return array ('formated' => $price, 'plain' => $sPrice);
		}
	} else {
		return round($sPrice, $this->currencies[$this->actualCurr]['decimal_places']);
	}
}

 

Die Analyse:

Zuerst gucken wir uns an, was “xtcFormatSpecialGraduated();” im Einzelnen macht und gehen dabei ein paar Fall-Unterscheidungen durch.

  1. Es wird geprüft, ob das Produkt überhaupt einen Grundpreis $pPrice hat. Wenn nicht, wird nichts weiter getan - sondern gleich Null ausgegeben
  2. Danach wird geprüft, ob ein Kundengruppen-Rabatt für den jeweiligen Kunden und den Artikel gilt. Wenn ja, wird der gültige Stückpreis $sPrice mit dem Rabattwert verrechnet
  3. Schließlich wird geprüft, ob $sPrice vom $pPrice abweicht - wenn ja, wird dieser zum Ausgabewert $price, den man auch im Shop nachher zu sehen bekommt
  4. Und wenn der der Stückpreis anders ist (auch höher) als der normale Produktpreis, wird der Preis so formatiert, wie sonst ein Sonderangebot ausgegeben wird. Was im Grunde genommen irritierend sein kann, denn nicht immer ist alles, was man billiger bekommt ein Special … Wie z.B. auch bei unseren Mengenrabatten …
  5. Ist der Stückpreis der gleiche wieder Normalpreis, wird die Meldung “Ab …” ausgegeben. Damit wird drauf hingewiesen, dass der zu zahlende Preis beeinflusst werden kann (beispielsweise über Optionen oder Mengen) - aber aktuell noch kein anderer geworden ist

 

Die Lösung:

… Und dort ist auch unser Einstiegspunkt. Im Grunde genommen kann auch nicht viel passieren, solange wir mit “eigenen” Variablen arbeiten. Von einer Änderung an $pPrice sowie $sPrice sollte man also die Finger lassen, denn mit diesen Werten wird nachher weitergerechnet.

Wir suchen also die Stelle, an der als Erstes geprüft wird, ob ein veränderter Stückpreis vorliegt.

Denn xt:Commerce hat schon soweit “recht”: Solange man eine gewisse Anzahl von Artikeln nicht überschritten hat, bleibt ja auch der Stückpreis gleich. Wir möchten aber dennoch einen anderen angezeigt bekommen - nämlich den günstigsten. Und wenn wir nur 6 statt 10 Artikel kaufen, soll auch weiterhin der günstigste angezeigt werden.

Uns interessiert jetzt: Ist ein Mengenrabatt möglich und gehört der Besucher zu einer Kundengruppe, für die ein Mengenrabatt zugelassen ist?

Aus diesen Zeilen …

...
if ($format) {
	if ($sPrice != $pPrice) {
		$price = '<span class="productOldPrice">'.MSRP.$this->xtcFormat($pPrice, $format).'</span><br />'.YOUR_PRICE.$this->checkAttributes($pID).$this->xtcFormat($sPrice, $format);
	} else {
		$price = FROM.$this->xtcFormat($sPrice, $format);
	}
...
}

… wird also Folgendes:

...
if ($format) {
	// NEU HINZUGEFÜGT
	if ($this->cStatus['customers_status_graduated_prices'] == '1') {
		$bestPrice = ''; // Hier den günstigsten StaffelPreis berechnen
	} elseif ($sPrice != $pPrice) { // if ($sPrice != $pPrice) {
		$price = '<span class="productOldPrice">'.MSRP.$this->xtcFormat($pPrice, $format).'</span><br />'.YOUR_PRICE.$this->checkAttributes($pID).$this->xtcFormat($sPrice, $format);
	} else {
		$price = FROM.$this->xtcFormat($sPrice, $format);
	}
...
}

Dann erweitern wir die Geschichte ein bisschen, da ja die günstigste Mengenstaffel herausgefunden werden soll. Natürlich nicht überhaupt die günstigste, sondern nur die für die aktuelle Kundengruppe gültige.

...
if ($format) {
	// NEU HINZUGEFÜGT
	if ($this->cStatus['customers_status_graduated_prices'] == '1') {
		$sQuery = "SELECT max(quantity) as qty
			FROM ".TABLE_PERSONAL_OFFERS_BY.$this->actualGroup."
			WHERE products_id='".$pID."'";
		$sQuery = xtDBquery($sQuery);
		$sQuery = xtc_db_fetch_array($sQuery, true);
		$bestPrice = $this->xtcGetGraduatedPrice($pID, $sQuery[qty]);
	} else if ($sPrice != $pPrice) { // if ($sPrice != $pPrice) {
		$price = '<span class="productOldPrice">'.MSRP.$this->xtcFormat($pPrice, $format).'</span><br />'.YOUR_PRICE.$this->checkAttributes($pID).$this->xtcFormat($sPrice, $format);
	} else {
		$price = FROM.$this->xtcFormat($sPrice, $format);
	}
...
}

Damit wissen wir also schon den $bestPrice, der für diesen Artikel und für diesen Kunden via Mengenstaffelung möglich ist.

Jetzt fehlen nur noch zwei Dinge: Da Discount-Angaben auch auf Staffelpreise angewandt werden, muss der $discount auch in unseren $bestPrice eingerechnet werden. Und natürlich möchten wir am Ende ja auch noch einen Preis angezeigt bekommen, also müssen wir die Ausgabe-Variable $price entsprechend füllen.

Ein Hinweis an dieser Stelle - xt:Commerce zeigt von Haus aus Staffelpreise ohne Rabatt, legt sie aber mit Rabatt in den Warenkorb. Wie man das verhindert, können Sie in diesem Beitrag lesen. Und in einigen weiteren Artikeln finden Sie noch mehr Informationen, wie xt:Commerce mit Preisnachlässen umgeht. Beziehungsweise wie nachlässig mit Nachlässen … aber lassen wir das …

Wieder zurück zu unserem Staffelpreis -
Statt des Grundpreises soll dort also stehen:

Ab … plus günstigster Preis
und darunter etwas kleiner: “Einzelpreis: soundsoviel”

Leider steht in der xtcPrice.php keine global verwendbarer Platzhalter für “Einzelpreis” zur Verfügung, wer mehrsprachige Shops betreibt, sollte die besser in den entsprechenden Language-Files nachtragen. Aber der Einfachheit halber schreibe ich den “Einzelpreis” einfach mal “hart” in den Code …

...
if ($format) {
	// NEU HINZUGEFÜGT
	if ($this->cStatus['customers_status_graduated_prices'] == '1') {
			$sQuery = "SELECT max(quantity) as qty
				FROM ".TABLE_PERSONAL_OFFERS_BY.$this->actualGroup."
				WHERE products_id='".$pID."'";
			$sQuery = xtDBquery($sQuery);
			$sQuery = xtc_db_fetch_array($sQuery, true);
			$bestPrice = $this->xtcGetGraduatedPrice($pID, $sQuery[qty]);
			if ($discount)
				$bestPrice -= $bestPrice / 100 * $discount;
			$price .= FROM.$this->xtcFormat($bestPrice, $format, 1)
				.' <br /><small>Einzelpreis: '
				.$this->xtcFormat($sPrice, $format)
				.'</small>';
	} else if ($sPrice != $pPrice) { // if ($sPrice != $pPrice) {
		$price = '<span class="productOldPrice">'.MSRP.$this->xtcFormat($pPrice, $format).'</span><br />'.YOUR_PRICE.$this->checkAttributes($pID).$this->xtcFormat($sPrice, $format);
	} else {
		$price = FROM.$this->xtcFormat($sPrice, $format);
	}
...
}

 

Fertig

So haben wir gleich mehrere Fliegen mit einer Klappe geschlagen - Und das ohne die weiteren Rechenwege von xt:Commerce zu beeinflussen.

  • Wenn Staffelpreise vorliegen, wird der günstigste als “Hauptpreis” angezeigt.
  • Liegt zusätzlich zu dem Staffelpreis noch ein Rabatt vor, stimmt wenigstens von diesem Preis die Anzeige.
    Die Staffelpreis-Tabelle spuckt zwar weiterhin falsche Werte aus, aber wie man das behebt, steht ja (wie erwähnt) in diesem Artikel
  • Und schließlich springt die Anzeige nicht wieder auf einen teureren Preis um, bloß weil man nicht die höchste Mengenstaffel eingekauft hat.

Die gesamte Funktion xtcFormatSpecialGraduated(); noch einmal im Zusammenhang:
Bitte die Version weiter unten benutzen!


	... Code-Beispiel entfernt (siehe unten) ...
	

Vielleicht für einige Nutzer wichtig: Ich habe noch nicht die Abfrage integriert, ob für den jeweiligen Artikel ein “Sonderangebot” vorliegt. Sonderangebote hebeln nämlich alle anderen Preisnachlässe aus. Egal ob Rabatt oder Staffelpreis - Allerdings hat xt:Commerce das vor dieser Änderung auch nie beachtet.

Deswegen habe ich in der Vergangenheit auch noch ein paar andere Dateien, die für die Preisanzeige zuständig sind, bearbeitet - Und ich ergänze das alles lieber mal mit etwas mehr Ruhe … dann gibt’s hier auch wieder ein aktualisiertes Datei-Set zum Downloaden.

 

Nachtrag und Korrektur

Vielen Dank für den Hinweis (siehe » unten) - Folgendes habe ich übersehen: Ein “Ab”-Preis wird immer angegeben, wenn für die jeweilige Kundengruppe Staffelpreise erlaubt sind. Ob es allerdings auch wirklich welche gibt, ist ein anderes paar Schuhe und … wurde nicht überprüft.

Aber mit dieser Version sollte es funktionieren:


	... Code-Beispiel entfernt (siehe unten) ...
	

 

Zweite Korrektur
(14. Juni 2007)

“Kaufi” hat einen » weiteren Fehler entdeckt: Die Änderung geht davon aus, dass das jeweilige Produkt mit dem Standard-Steuersatz besteuert wird. Solche Bugs findet man meist nur durch Zufall, da zu einem anderen Satz besteuerte Artikel in deutschen Online-Shops eher selten sind.

Vielen Dank für das Feedback - und der Fehler sollte mit dieser Version behoben sein:


function xtcFormatSpecialGraduated($pID, $sPrice, $pPrice, $format, $vpeStatus = 0, $pID) {
	// NEU HINZUGEFÜGT "Steuerklasse ermitteln"
	$tQuery = "SELECT products_tax_class_id
		FROM ".TABLE_PRODUCTS." WHERE
		products_id='".$pID."'";
	$tQuery = xtc_db_query($tQuery);
   	$tQuery = xtc_db_fetch_array($tQuery);
   	$tax_class = $tQuery[products_tax_class_id];
	// ENDE "Steuerklasse ermitteln"

	if ($pPrice == 0)
		return $this->xtcFormat($sPrice, $format, 0, false, $vpeStatus);
	if ($discount = $this->xtcCheckDiscount($pID))
		$sPrice -= $sPrice / 100 * $discount;
	if ($format) {
		// NEU HINZUGEFÜGT
		$sQuery = "SELECT max(quantity) as qty
			FROM ".TABLE_PERSONAL_OFFERS_BY.$this->actualGroup."
			WHERE products_id='".$pID."'";
		$sQuery = xtDBquery($sQuery);
		$sQuery = xtc_db_fetch_array($sQuery, true);
		if ( ($this->cStatus['customers_status_graduated_prices'] == '1') || ($Query[qty] > 1) ) {
			$bestPrice = $this->xtcGetGraduatedPrice($pID, $sQuery[qty]);
			if ($discount)
				$bestPrice -= $bestPrice / 100 * $discount;
			$price .= FROM.$this->xtcFormat($bestPrice, $format, $tax_class)
				.' <br /><small>Einzelpreis: '
				.$this->xtcFormat($sPrice, $format)
				.'</small>';
		} else if ($sPrice != $pPrice) { // if ($sPrice != $pPrice) {
			$price = '<span class="productOldPrice">'.MSRP.$this->xtcFormat($pPrice, $format).'</span><br />'.YOUR_PRICE.$this->checkAttributes($pID).$this->xtcFormat($sPrice, $format);
		} else {
			$price = FROM.$this->xtcFormat($sPrice, $format);
		}
		if ($vpeStatus == 0) {
			return $price;
		} else {
			return array ('formated' => $price, 'plain' => $sPrice);
		}
	} else {
		return round($sPrice, $this->currencies[$this->actualCurr]['decimal_places']);
	}
}
	

Neu hinzugekommen ist eine Abfrage nach der für $bestPrice gültigen “tax_class”-ID. Die “Überlegung”, ob für den jeweiligen Besucher ein Steuersatz angezeigt werden soll oder nicht, “übernimmt” dann wieder die Funktion xtcFormat().

 

Dritte Korrektur
(9. Juli 2007)

Wenn man beim Anlegen von Artikeln für eine Kundengruppe einen anderen Preis als den “normalen Einzelpreis” des Artikels einträgt (gehen wir mal von 10,- € aus), reagiert xt:Commerce darauf normalerweise mit folgender Anzeige:

UVP: 10,00 €
Ihr Preis: 8,00 €

Der ausgegebene Quellcode sieht dabei in etwa so aus:

<span class="productOldPrice">UVP 10,00 &euro; </span><br />Ihr Preis 8,00 &euro;


Problem “Kein UVP mehr?”

Mit der unter “Zweite Korrektur” beschriebenen Modifikation ist diese Anzeige nicht mehr möglich, eine solche Vergünstigung wird wie ein “Sonderpreis” wiedergegeben.


Lösung: “Einmal && statt ||”

Um diese Art des Preisnachlasses (also “Kundengruppenpreis kleiner als Artikelgrundpreis”) wieder wie gehabt automatisch als “UVP” und “Ihr Preis” kennzeichnen zu können, muss eine if-Bedingung leicht geändert werden - und zwar:

if ( ($this->cStatus['customers_status_graduated_prices'] == '1') || ($sQuery[qty] > 1) )

… in:

if ( ($this->cStatus['customers_status_graduated_prices'] == '1') && ($sQuery[qty] > 1) )


Der neue Code:

Version 3: Hier die komplette Funktion “xtcFormatSpecialGraduated();” noch einmal im Zusammenhang:


function xtcFormatSpecialGraduated($pID, $sPrice, $pPrice, $format, $vpeStatus = 0, $pID) {
	// NEU HINZUGEFÜGT "Steuerklasse ermitteln"
	$tQuery = "SELECT products_tax_class_id
		FROM ".TABLE_PRODUCTS." WHERE
		products_id='".$pID."'";
	$tQuery = xtc_db_query($tQuery);
   	$tQuery = xtc_db_fetch_array($tQuery);
   	$tax_class = $tQuery[products_tax_class_id];
	// ENDE "Steuerklasse ermitteln"

	if ($pPrice == 0)
		return $this->xtcFormat($sPrice, $format, 0, false, $vpeStatus);
	if ($discount = $this->xtcCheckDiscount($pID))
		$sPrice -= $sPrice / 100 * $discount;
	if ($format) {
		// NEU HINZUGEFÜGT
		$sQuery = "SELECT max(quantity) as qty
			FROM ".TABLE_PERSONAL_OFFERS_BY.$this->actualGroup."
			WHERE products_id='".$pID."'";
		$sQuery = xtDBquery($sQuery);
		$sQuery = xtc_db_fetch_array($sQuery, true);
		// NEU! Damit "UVP"-Anzeige wieder möglich ist
		// if ( ($this->cStatus['customers_status_graduated_prices'] == '1') || ($sQuery[qty] > 1) ) {
		if ( ($this->cStatus['customers_status_graduated_prices'] == '1') && ($sQuery[qty] > 1) ) {
			$bestPrice = $this->xtcGetGraduatedPrice($pID, $sQuery[qty]);
			if ($discount)
				$bestPrice -= $bestPrice / 100 * $discount;
			$price .= FROM.$this->xtcFormat($bestPrice, $format, $tax_class)
				.' <br /><small>Einzelpreis: '
				.$this->xtcFormat($sPrice, $format)
				.'</small>';
		} else if ($sPrice != $pPrice) { // if ($sPrice != $pPrice) {
			$price = '<span class="productOldPrice">'.MSRP.' '.$this->xtcFormat($pPrice, $format).'</span><br />'.YOUR_PRICE.$this->checkAttributes($pID).$this->xtcFormat($sPrice, $format);
		} else {
			$price = FROM.$this->xtcFormat($sPrice, $format);
		}
		if ($vpeStatus == 0) {
			return $price;
		} else {
			return array ('formated' => $price, 'plain' => $sPrice);
		}
	} else {
		return round($sPrice, $this->currencies[$this->actualCurr]['decimal_places']);
	}
}
	

Jetzt sollte (bis auf die Anzeige bei Staffelpreisen) wieder “wie gewohnt” reagieren.

 

 

Die beschriebene Änderung ist für xt:Commerce 3.04 SP1 und (NEU!) SP2.1 getestet. Sollten Sie eine andere Version des Shops installiert haben, könnten mir nicht bekannte Folge-Fehler entstehen. Ohnehin sollte man System-Dateien nicht sofort in einem Live-Shop bearbeiten.

Bitte beachten Sie auch unbedingt die allgemeinen Hinweise zur Verwendung hier veröffentlichter Code-Beispiele!