<?php
/**
* Shopware 4.0
* Copyright © 2012 shopware AG
*
* According to our dual licensing model, this program can be used either
* under the terms of the GNU Affero General Public License, version 3,
* or under a proprietary license.
*
* The texts of the GNU Affero General Public License with an additional
* permission and of our proprietary license can be found at and
* in the LICENSE file you have received along with this program.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* "Shopware" is a registered trademark of shopware AG.
* The licensing of the program under the AGPLv3 does not imply a
* trademark license. Therefore any rights, title and interest in
* our trademarks remain entirely with us.
*
* @category Shopware
* @package Shopware_Core
* @subpackage Class
* @copyright Copyright (c) 2012, shopware AG (http://www.shopware.de)
* @version $Id$
* @author Stefan Hamann
* @author $Author$
*/
/**
* hide@address.com: Documentation
*/
class sConfigurator
{
const TYPE_STANDARD = 0;
const TYPE_SELECTION = 1;
const TYPE_TABLE = 2;
/**
* The shopware system object.
*
* @var sSystem
*/
var $sSYSTEM;
/**
* DONE
* @param $id
* @param $articleData
* @param bool $recursiveCall
* @return array
*/
public function getArticleConfigurator($id, $articleData, $recursiveCall = false)
{
$id = intval($id);
//get posted groups and options
$selectedItems = $this->sSYSTEM->_POST["group"];
if (empty($selectedItems)) {
$selectedItems = array();
}
/**@var $repository \Shopware\Models\Article\Repository*/
$repository = Shopware()->Models()->Article();
/**@var $article \Shopware\Models\Article\Article*/
$article = Shopware()->Models()->find('Shopware\Models\Article\Article', $id);
//the data property contains now the configurator set. Set configurator set has the array keys "options" and "groups"
//where the assigned configurator options and groups are.
$data = $repository->getArticleConfiguratorSetByArticleIdIndexedByIdsQuery($id)
->getArrayResult();
$data = $data[0]['configuratorSet'];
$customerGroupKey = $this->sSYSTEM->sUSERGROUP;
if (empty($customerGroupKey)) {
$customerGroupKey = 'EK';
}
if (empty($data)) {
return $articleData;
}
//first we convert the configurator set settings from the new structure to the old structure.
$settings = $this->getConfiguratorSettings($data, $article);
$optionsIds = array();
//now we iterate all activated options and assign them to the corresponding group.
foreach($data['options'] as $option) {
//the convert functions changes the property names, so we save the ids in internal helper properties.
$groupId = $option['groupId'];
$optionId = $option['id'];
//the groups in the data property indexed by their ids, so we can use "array_key_exists" to check if the group id of the current options exists in our group array.
if (array_key_exists($groupId, $data['groups'])) {
//if the group exist, we save the option id into in helper array. This helper array is only used for "configurator - tables".
$optionsIds[] = $optionId;
if (empty($selectedItems)) {
$selected = 0;
} else {
$selected = (int) (array_key_exists($groupId, $selectedItems) && $selectedItems[$groupId] == $optionId);
}
//now we convert the configurator option data from the old property structure to new one.
$option = $this->getConvertedOptionData($option);
$option['user_selected'] = $selected;
$option['selected'] = $selected;
//now we assign the option into the options array element to corresponding group.
$data['groups'][$groupId]['options'][$optionId] = $option;
}
}
//now we iterate all groups to convert them from the old property structure to new one.
$sConfigurator = array();
foreach($data['groups'] as $group) {
$data = $this->getConvertGroupData($group);
//if the current group id exists in the post data, the group was selected already.
$data['user_selected'] = (int) array_key_exists($group['id'], $selectedItems) && !empty($selectedItems[$group['id']]);
$data['selected'] = (int) array_key_exists($group['id'], $selectedItems) && !empty($selectedItems[$group['id']]);
$sConfigurator[] = $data;
}
/**
* If the configurator set is configured as a table configurator, we have to create the "table structure array"
* this array looks like this:
* ['SIZE XXL']
* ['COLOR YELLOW']
* ['COLOR GREEN']
* ['COLOR RED']
* ['SIZE L']
* ['COLOR YELLOW']
* ['COLOR GREEN']
* ['COLOR RED']
* ...
*/
$sConfiguratorValues = array();
if ($settings['type'] == self::TYPE_TABLE) {
$sConfiguratorValues = $this->getTableConfiguratorData($id, $optionsIds, $articleData, $article, $customerGroupKey);
}
//now we check if the sQuantity property is set in the post.
$quantity = 1;
if(!empty($this->sSYSTEM->_POST["sQuantity"])&&is_numeric($this->sSYSTEM->_POST["sQuantity"])) {
$quantity = (int)$this->sSYSTEM->_POST["sQuantity"];
}
$articleData["quantity"] = $quantity;
//if the posted quantity is lesser then the min purchase we have to set the min purchase as quantity
if(empty($articleData["quantity"])||$articleData["quantity"]<$articleData["minpurchase"])
$articleData["quantity"] = $articleData["minpurchase"];
$selected = null;
//if some items was selected from the user, we have to select the first available variant
if (!empty($selectedItems)) {
//first we create a small query builder with the article details and the prices
$builder = Shopware()->Models()->createQueryBuilder();
$builder->select(array('detail', 'prices'))
->from('Shopware\Models\Article\Detail', 'detail')
->leftJoin('detail.prices', 'prices')
->leftJoin('prices.customerGroup', 'customerGroup')
->where('detail.articleId = ?1')
->andWhere('customerGroup.key = :customerGroup')
->setParameter(1, $id)
->setParameter('customerGroup', $customerGroupKey)
->orderBy('detail.kind', 'ASC')
->addOrderBy('customerGroup.id', 'ASC')
->addOrderBy('prices.from', 'ASC');
//now we iterate all selected groups with their options to filter the available variant
foreach($selectedItems as $optionId) {
if (empty($optionId)) {
continue;
}
$alias = 'option' . $optionId;
$builder->addSelect($alias);
$builder->innerJoin('detail.configuratorOptions', $alias);
$builder->andWhere($alias . '.id = :' . $alias);
$builder->addOrderBy($alias . '.position', 'ASC');
$builder->setParameter($alias, $optionId);
}
$selected = $builder->getQuery()->getArrayResult();
//we can only set one variant as select, so we select the first one
$detailData = $selected[0];
if (!empty($detailData)) {
if ($article->getLastStock() && $detailData['inStock'] < 1) {
$detailData['active'] = 0;
}
if (empty($detailData['prices'])) {
$detailData['prices'] = $this->getDefaultPrices($detailData['id']);
}
$detailData['prices'] = $this->getConvertedPrices($detailData['prices'], $articleData["tax"], $articleData["taxID"]);
$selected = $this->getConvertedDetail($detailData);
$attributeIndex = 1;
//at least we creates the old "attr1-X" attributes with the option id as value.
foreach($detailData['configuratorOptions'] as $option) {
$attribute = 'attr' . $attributeIndex;
$selected[$attribute] = $option['id'];
$attributeIndex++;
}
}
}
if (!empty($selectedItems) && empty($selected)) {
if ($settings['type'] == self::TYPE_STANDARD) {
unset($this->sSYSTEM->_POST["group"]);
return $this->getArticleConfigurator($id, $articleData, true);
} elseif ($settings['type'] == self::TYPE_SELECTION) {
array_pop($this->sSYSTEM->_POST["group"]);
return $this->getArticleConfigurator($id, $articleData, true);
}
}
if (empty($selected)) {
$detail = $repository->getConfiguratorTablePreSelectionItemQuery($article, $customerGroupKey)
->getOneOrNullResult(\Doctrine\ORM\AbstractQuery::HYDRATE_ARRAY);
if ($article->getLastStock() && $detail['inStock'] < 1) {
$detail['active'] = 0;
}
$preSelectedOptions = $detail['configuratorOptions'];
foreach($sConfigurator as &$group) {
$preSelectedOption = $preSelectedOptions[$group['groupID']];
$id = $preSelectedOption['id'];
if (array_key_exists($id, $group['values'])) {
$group['values'][$preSelectedOption['id']]['user_selected'] = 1;
$group['values'][$preSelectedOption['id']]['selected'] = 1;
}
}
if (!empty($detail)) {
if (empty($detail['prices'])) {
$detail['prices'] = $this->getDefaultPrices($detail['id']);
}
$detail['prices'] = $this->getConvertedPrices($detail['prices'], $articleData["tax"], $articleData["taxID"]);
$selected = $this->getConvertedDetail($detail);
}
}
//if one variant are selected we have to calculate the prices and get the price
if (!empty($selected)) {
$selectedPrice = $selected['price'][0];
foreach($selected['price'] as $price) {
if (!is_numeric($price['to'])) {
$selectedPrice = $price;
break;
} elseif ($quantity < $price['to']) {
$selectedPrice = $price;
break;
}
}
if (count($selected['price']) > 1) {
$articleData['sBlockPrices'] = $selected['price'];
} else {
$articleData['sBlockPrices'] = array();
}
$articleData = $this->mergeSelectedAndArticleData($articleData, $selected, $selectedPrice);
$articleData["sConfiguratorSelection"] = $selected;
}
$articleData['sConfiguratorValues'] = $sConfiguratorValues;
$articleData['sConfigurator'] = $sConfigurator;
$articleData['sConfiguratorSettings'] = $settings;
if ($recursiveCall) {
$articleData['sError']['variantNotAvailable'] = true;
}
return $articleData;
}
private function getDefaultPrices($detailId)
{
$builder = Shopware()->Models()->createQueryBuilder();
return $builder->select(array('prices'))
->from('Shopware\Models\Article\Price', 'prices')
->where('prices.articleDetailsId = :detailId')
->andWhere('prices.customerGroupKey = :key')
->setParameter('detailId', $detailId)
->setParameter('key', 'EK')
->orderBy('prices.from', 'ASC')
->getQuery()
->getArrayResult();
}
/**
* Internal helper function to set the variant data of the selected item into the article data array.
* @param $articleData
* @param $selected
* @param $selectedPrice
* @return mixed
*/
private function mergeSelectedAndArticleData($articleData, $selected, $selectedPrice)
{
$articleData['minpurchase'] = empty($selected['minpurchase']) ? $articleData['minpurchase'] : $selected['minpurchase'];
$articleData['maxpurchase'] = empty($selected['maxpurchase']) ? $articleData['maxpurchase'] : $selected['maxpurchase'];
$articleData['purchasesteps'] = empty($selected['purchasesteps']) ? $articleData['purchasesteps'] : $selected['purchasesteps'];
$articleData['packunit'] = empty($selected['packunit']) ? $articleData['packunit'] : $selected['packunit'];
// Calculating price for reference-unit
if ($selected["purchaseunit"] > 0 && $selected["referenceunit"]) {
$selected["purchaseunit"] = (float) $selected["purchaseunit"];
$selected["referenceunit"] = (float) $selected["referenceunit"];
$price = str_replace(",", ".", $selected["price"][0]["price"]);
$basePrice = $price / $selected["purchaseunit"] * $selected["referenceunit"];
$basePrice = $this->sSYSTEM->sMODULES['sArticles']->sFormatPrice($basePrice);;
$articleData['purchaseunit'] = $selected['purchaseunit'];
$articleData['referenceunit'] = $selected['referenceunit'];
$articleData['referenceprice'] = $basePrice;
}
if ($selected["unitID"]) {
$articleData["sUnit"] = $this->sSYSTEM->sMODULES['sArticles']->sGetUnit($selected["unitID"]);
}
$articleData['pricegroup'] = $selectedPrice['customerGroupKey'];
$articleData["pricenumeric"] = $selectedPrice['pricenumeric'];
$articleData["price"] = $selectedPrice['price'];
$articleData["pseudoprice"] = $selectedPrice['pseudoPrice'];
$articleData["ordernumber"] = $selected["ordernumber"];
$articleData["instock"] = $selected["instock"];
$articleData["active"] = $selected["active"];
$articleData["suppliernumber"] = empty($selected['suppliernumber']) ? $articleData['suppliernumber'] : $selected['suppliernumber'];
$articleData["stockmin"] = empty($selected['stockmin']) ? $articleData['stockmin'] : $selected['stockmin'];
$articleData["stockmin"] = empty($selected['stockmin']) ? $articleData['stockmin'] : $selected['stockmin'];
$articleData["width"] = empty($selected['width']) ? $articleData['width'] : $selected['width'];
$articleData["length"] = empty($selected['length']) ? $articleData['length'] : $selected['length'];
$articleData["height"] = empty($selected['height']) ? $articleData['height'] : $selected['height'];
$articleData["ean"] = empty($selected['ean']) ? $articleData['ean'] : $selected['ean'];
$articleData["releasedate"] = empty($selected['releasedate']) ? $articleData['releasedate'] : $selected['releasedate'];
$articleData["shippingtime"] = empty($selected['shippingtime']) ? $articleData['shippingtime'] : $selected['shippingtime'];
$articleData["shippingfree"] = isset($selected['shippingfree']) ? $selected['shippingfree'] : $articleData['shippingfree'];
return $articleData;
}
/**
* @param $data
* @param $tax
* @param $taxId
* @return array
*/
private function getConvertedPrices($data, $tax, $taxId)
{
$prices = array();
//iterate price to calculate the gross price.
foreach($data as $price) {
$price['price'] = $this->sSYSTEM->sMODULES['sArticles']->sCalculatingPrice($price["price"],$tax,$taxId);
$price['pricenumeric'] = $this->sSYSTEM->sMODULES['sArticles']->sCalculatingPriceNum($price["price"],$tax,false,false,$taxId,false);
$prices[] = $price;
}
return $prices;
}
/**
* Returns the "sConfiguratorValues" array element if the passed configurator set is and table configurator set.
* Example return value:
* <code>
* ARRAY (
* ['SIZE XXL']
* ['COLOR YELLOW']
* ['COLOR GREEN']
* ['COLOR RED']
* ['SIZE L']
* ['COLOR YELLOW']
* ['COLOR GREEN']
* ['COLOR RED']
* )
* </code>
* @param $id
* @param $optionsIds
* @param $articleData
* @param $article \Shopware\Models\Article\Article
* @param $customerGroupKey
* @return array
*/
private function getTableConfiguratorData($id, $optionsIds, $articleData, $article, $customerGroupKey)
{
//get posted groups and options
$selectedItems = $this->sSYSTEM->_POST["group"];
if (empty($selectedItems)) {
$selectedItems = array();
}
/**@var $repository \Shopware\Models\Article\Repository*/
$repository = Shopware()->Models()->Article();
$groups = $repository->getConfiguratorGroupsAndOptionsByOptionsIdsIndexedByOptionIdsQuery($optionsIds)
->getArrayResult();
//now we check if the sQuantity property is set in the post.
$quantity = 1;
if(!empty($this->sSYSTEM->_POST["sQuantity"])&&is_numeric($this->sSYSTEM->_POST["sQuantity"])) {
$quantity = (int)$this->sSYSTEM->_POST["sQuantity"];
}
$firstGroup = $groups[0];
$secondGroup = $groups[1];
$resultGroups = array();
foreach($firstGroup['options'] as $firstKey => $firstGroupOption) {
$mergedGroups = array();
foreach($secondGroup['options'] as $secondKey => $secondGroupOption) {
$detail = $repository->getArticleDetailForTableConfiguratorOptionCombinationQuery($id, $firstKey, $secondKey, $article, $customerGroupKey)
->getArrayResult();
$detail = $detail[0];
if (empty($detail['prices'])) {
$detail['prices'] = $this->getDefaultPrices($detail['id']);
}
$selectedPrice = null;
//iterate price to calculate the gross price.
foreach($detail['prices'] as $key => $price) {
$price['price'] = $this->sSYSTEM->sMODULES['sArticles']->sCalculatingPrice($price["price"],$articleData["tax"],$articleData["taxID"]);
if (!is_numeric($price['to'])) {
$selectedPrice = $price;
} elseif ($quantity < $price['to']) {
$selectedPrice = $price;
}
$detail['prices'][$key] = $price;
}
if ($selectedPrice === null) {
$selectedPrice = $detail['prices'][0];
}
$selected = false;
if (array_key_exists($firstGroup['id'], $selectedItems) &&
array_key_exists($secondGroup['id'], $selectedItems)) {
$selectedOptionIds = array($selectedItems[$secondGroup['id']], $selectedItems[$firstGroup['id']]);
$selected = (int) (in_array($firstKey, $selectedOptionIds) && in_array($secondKey, $selectedOptionIds));
}
if (empty($selectedItems)) {
$selected = (int) ($detail['kind'] === 1);
}
$standard = 0;
if ($detail['kind'] === 1) {
$standard = 1;
}
$mergedGroup = array(
'value1' => $firstKey,
'value2' => $secondKey,
'valueID' => $detail['id'],
'standard' => $standard,
'active' => $detail['active'],
'ordernumber' => $detail['number'],
'price' => $selectedPrice['price'],
'user_selected' => $selected,
'selected' => $selected,
'linkBasket' => $this->sSYSTEM->sCONFIG['sBASEFILE']."?sViewport=basket&sAdd=".$detail["number"]
);
if (count($detail['prices']) > 0) {
$mergedGroup['sBlockPrices'] = $detail['prices'];
}
$mergedGroups[$secondKey] = $mergedGroup;
}
$resultGroups[$firstKey] = $mergedGroups;
}
return $resultGroups;
}
/**
* Shopware 3.5 <=> Shopware 4.0 Mapping function. Converts the new article detail properties to the old property names.
* @param $data
* @return array
*/
private function getConvertedDetail($data)
{
$detail = array(
'valueID' => $data['id'],
'active' => $data['active'],
'kind' => $data['kind'],
'ordernumber' => $data['number'],
'instock' => $data['inStock'],
'price' => $data['prices'],
//additional information which was not given by the old configurator structure.
'id' => $data['id'],
'articleID' => $data['articleId'],
'unitID' => $data['unitId'],
'suppliernumber' => $data['supplierNumber'],
'additionaltext' => $data['additionalText'],
'stockmin' => $data['stockMin'],
'weight' => $data['weight'],
'width' => $data['width'],
'length' => $data['length'],
'height' => $data['height'],
'ean' => $data['ean'],
'position' => $data['position'],
'minpurchase' => $data['minPurchase'],
'purchasesteps' => $data['purchaseSteps'],
'maxpurchase' => $data['maxPurchase'],
'purchaseunit' => $data['purchaseUnit'],
'referenceunit' => $data['referenceUnit'],
'packunit' => $data['packUnit'],
'shippingfree' => $data['shippingFree'],
'releasedate' => $data['releaseDate'],
'shippingtime' => $data['shippingTime']
);
if (count($data['prices']) > 1) {
$detail['sBlockPrices'] = $data['prices'];
}
return $detail;
}
/**
* Shopware 3.5 <=> Shopware 4.0 Mapping function. Converts the new configurator group properties to the old
* property names.
* @param $data
* @param $article \Shopware\Models\Article\Article
* @return array
*/
private function getConvertedSettings($data, $article)
{
$settings = array(
'articleID' => $article->getId(),
'type' => $data['type'],
'template' => $data['template'],
'instock' => $article->getLastStock(),
'upprice' => 0
);
return $settings;
}
/**
* Shopware 3.5 <=> Shopware 4.0 Mapping function. Converts the new configurator group properties to the old
* property names.
* @param $data
* @return array
*/
private function getConvertGroupData($data)
{
$translation = $this->getGroupTranslation($data['id'], array('name' => $data['name'], 'description' => $data['description']));
return array(
'groupID' => $data['id'],
'groupname' => $translation['name'],
'groupnameOrig' => $data['name'],
'groupdescription' => $translation['description'],
'groupdescriptionOrig' => $data['description'],
'groupimage' => '',
'postion' => $data['position'],
'selected_value' => $data['selected_value'],
'selected' => $data['selected'],
'values' => $data['options']
);
}
/**
* Shopware 3.5 <=> Shopware 4.0 Mapping function. Converts the new configurator option properties to the old
* property names.
* @param $data
* @return array
*/
private function getConvertedOptionData($data)
{
$translation = $this->getOptionTranslation($data['id'], array('name' => $data['name']));
return array(
'optionID' => $data['id'],
'groupID' => $data['groupId'],
'optionnameOrig' => $data['name'],
'optionname' => $translation['name'],
'optionposition' => $data['position'],
'optionactive' => 1,
);
}
/**
* @param $optionId
* @param $fallback
* @return mixed
*/
public function getOptionTranslation($optionId, $fallback)
{
$sql= "SELECT objectdata
FROM s_core_translations
WHERE objecttype = ?
AND objectkey = ?
AND objectlanguage = ?";
$data = Shopware()->Db()->fetchOne($sql, array('configuratoroption', $optionId, Shopware()->Shop()->getId()));
if ($data) {
return unserialize($data);
} else {
return $fallback;
}
}
public function getGroupTranslation($groupId, $fallback)
{
$sql= "SELECT objectdata
FROM s_core_translations
WHERE objecttype = ?
AND objectkey = ?
AND objectlanguage = ?";
$data = Shopware()->Db()->fetchOne($sql, array('configuratorgroup', $groupId, Shopware()->Shop()->getId()));
if ($data) {
return unserialize($data);
} else {
return $fallback;
}
}
/**
* @param $data array
* @param $article \Shopware\Models\Article\Article
* @return array
*/
private function getConfiguratorSettings($data, $article)
{
$settings = $this->getConvertedSettings($data, $article);
//if no template configured use default templates.
if (empty($settings['template'])) {
//switch the template for the different configurator types.
if($settings["type"]== self::TYPE_SELECTION) {
//Selection configurator
$settings["template"] = "article_config_step.tpl";
} elseif($settings["type"]== self::TYPE_TABLE) {
//Table configurator
$settings["template"] = "article_config_table.tpl";
} else {
//Other configurator types
$settings["template"] = "article_config_upprice.tpl";
}
}
return $settings;
}
/**
* Returns the group options for the product configurator.
*
* @param unknown_type $id
* @param unknown_type $article
* @return unknown
*/
public function sGetArticleConfig ($id, $article)
{
return $this->getArticleConfigurator($id, $article);
}
}