*/ class Mage_Bundle_Model_Resource_Price_Index extends Mage_Core_Model_Resource_Db_Abstract { /** * EAV attributes cache * * @var array */ protected $_attributes = array(); /** * Websites cache * * @var array */ protected $_websites; /** * Customer Groups cache * * @var array */ protected $_customerGroups; /** * Initialize connection and define main table * */ protected function _construct() { $this->_init('bundle/price_index', 'entity_id'); } /** * Retrieve attribute object * * @param string $attributeCode * @return Mage_Catalog_Model_Resource_Eav_Attribute */ protected function _getAttribute($attributeCode) { if (!isset($this->_attributes[$attributeCode])) { $this->_attributes[$attributeCode] = Mage::getSingleton('catalog/config') ->getAttribute(Mage_Catalog_Model_Product::ENTITY, $attributeCode); } return $this->_attributes[$attributeCode]; } /** * Retrieve websites collection array * * @return array */ protected function _getWebsites() { if (is_null($this->_websites)) { $this->_websites = Mage::app()->getWebsites(false); } return $this->_websites; } /** * Retrieve customer groups collection array * * @return array */ protected function _getCustomerGroups() { if (is_null($this->_customerGroups)) { $this->_customerGroups = array(); foreach (Mage::getModel('customer/group')->getCollection() as $group) { $this->_customerGroups[$group->getId()] = $group; } } return $this->_customerGroups; } /** * Retrieve product ids array by product condition * * @param Mage_Core_Model_Product|Mage_Catalog_Model_Product_Condition_Interface|array|int $product * @param int $lastEntityId * @param int $limit * @return array */ public function getProducts($product = null, $lastEntityId = 0, $limit = 100) { $select = $this->_getReadAdapter()->select() ->from( array('e' => $this->getTable('catalog/product')), array('entity_id') ) ->where('e.type_id=?', Mage_Catalog_Model_Product_Type::TYPE_BUNDLE); if ($product instanceof Mage_Catalog_Model_Product) { $select->where('e.entity_id=?', $product->getId()); } elseif ($product instanceof Mage_Catalog_Model_Product_Condition_Interface) { $value = new Zend_Db_Expr($product->getIdsSelect($this->_getReadAdapter())); $select->where('e.entity_id IN(?)', $value); } elseif (is_numeric($product) || is_array($product)) { $select->where('e.entity_id IN(?)', $product); } $priceType = $this->_getAttribute('price_type'); $priceTypeAlias = 't_' . $priceType->getAttributeCode(); $joinConds = array( $priceTypeAlias . '.attribute_id=:attribute_id', $priceTypeAlias . '.store_id=0', $priceTypeAlias . '.entity_id=e.entity_id' ); $select->joinLeft( array($priceTypeAlias => $priceType->getBackend()->getTable()), join(' AND ', $joinConds), array('price_type' => $priceTypeAlias . '.value') ); $select->where('e.entity_id>:last_entity_id', $lastEntityId) ->order('e.entity_id') ->limit($limit); $bind = array( 'attribute_id' => $priceType->getAttributeId(), 'last_entity_id' => $lastEntityId ); return $this->_getReadAdapter()->fetchPairs($select, $bind); } /** * Reindex Bundle product Price Index * * @param Mage_Core_Model_Product|Mage_Catalog_Model_Product_Condition_Interface|array|int $products * @return Mage_Bundle_Model_Resource_Price_Index */ public function reindex($products = null) { $lastEntityId = 0; while (true) { $productsData = $this->getProducts($products, $lastEntityId); if (!$productsData) { break; } foreach ($productsData as $productId => $priceType) { $this->_reindexProduct($productId, $priceType); $lastEntityId = $productId; } } return $this; } /** * Reindex product price * * @param int $productId * @param int $priceType * @return Mage_Bundle_Model_Resource_Price_Index */ protected function _reindexProduct($productId, $priceType) { $options = $this->getSelections($productId); $selectionProducts = array(); foreach ($options as $option) { foreach ($option['selections'] as $selection) { $selectionProducts[$selection['product_id']] = $selection['product_id']; } } $priceIndex = array(); if ($priceType == Mage_Bundle_Model_Product_Price::PRICE_TYPE_DYNAMIC) { // load selection product prices from index for dynamic bundle $priceIndex = $this->getProductsPriceFromIndex($selectionProducts); } foreach ($this->_getWebsites() as $website) { if (!$website->getDefaultStore()) { continue; } $salableStatus = $this->getProductsSalableStatus($selectionProducts, $website); $priceData = $this->getProductsPriceData($productId, $website); $priceData = $priceData[$productId]; /* @var $website Mage_Core_Model_Website */ foreach ($this->_getCustomerGroups() as $group) { /* @var $group Mage_Customer_Model_Group */ if ($priceType == Mage_Bundle_Model_Product_Price::PRICE_TYPE_FIXED) { $basePrice = $this->_getBasePrice($productId, $priceData, $website, $group); $customOptions = $this->getCustomOptions($productId, $website); } elseif ($priceType == Mage_Bundle_Model_Product_Price::PRICE_TYPE_DYNAMIC) { $basePrice = 0; } list($minPrice, $maxPrice) = $this->_calculateBundleSelections($options, $salableStatus, $productId, $priceType, $basePrice, $priceData, $priceIndex, $website, $group ); if ($priceType == Mage_Bundle_Model_Product_Price::PRICE_TYPE_FIXED) { list($minPrice, $maxPrice) = $this->_calculateCustomOptions($customOptions, $basePrice, $minPrice, $maxPrice); } $this->_savePriceIndex($productId, $website->getId(), $group->getId(), $minPrice, $maxPrice); } } return $this; } /** * Save price index * * @param int $productId * @param int $websiteId * @param int $groupId * @param float $minPrice * @param float $maxPrice * @return Mage_Bundle_Model_Resource_Price_Index */ protected function _savePriceIndex($productId, $websiteId, $groupId, $minPrice, $maxPrice) { $adapter = $this->_getWriteAdapter(); $adapter->beginTransaction(); $bind = array($productId, $websiteId, $groupId, $minPrice, $maxPrice); $adapter->insertOnDuplicate($this->getMainTable(), $bind, array('min_price', 'max_price')); $adapter->commit(); return $this; } /** * Retrieve bundle options with selections and prices by product * * @param int $productId * @return array */ public function getSelections($productId) { $options = array(); $read = $this->_getReadAdapter(); $select = $read->select() ->from( array('option_table' => $this->getTable('bundle/option')), array('option_id', 'required', 'type') ) ->join( array('selection_table' => $this->getTable('bundle/selection')), 'selection_table.option_id=option_table.option_id', array('selection_id', 'product_id', 'selection_price_type', 'selection_price_value', 'selection_qty', 'selection_can_change_qty') ) ->join( array('e' => $this->getTable('catalog/product')), 'e.entity_id=selection_table.product_id AND e.required_options=0', array() ) ->where('option_table.parent_id=:product_id'); $query = $read->query($select, array('product_id' => $productId)); while ($row = $query->fetch()) { if (!isset($options[$row['option_id']])) { $options[$row['option_id']] = array( 'option_id' => $row['option_id'], 'required' => $row['required'], 'type' => $row['type'], 'selections' => array() ); } $options[$row['option_id']]['selections'][$row['selection_id']] = array( 'selection_id' => $row['selection_id'], 'product_id' => $row['product_id'], 'price_type' => $row['selection_price_type'], 'price_value' => $row['selection_price_value'], 'qty' => $row['selection_qty'], 'can_change_qty' => $row['selection_can_change_qty'] ); } return $options; } /** * Retrieve salable product statuses * * @param int|array $products * @param Mage_Core_Model_Website $website * @return array */ public function getProductsSalableStatus($products, Mage_Core_Model_Website $website) { $read = $this->_getReadAdapter(); $productsData = array(); $select = $read->select() ->from(array('e' => $this->getTable('catalog/product')), 'entity_id') ->where('e.entity_id IN(?)', $products); // add belong to website $select->joinLeft( array('pw' => $this->getTable('catalog/product_website')), 'e.entity_id=pw.product_id AND pw.website_id=:website_id', array('pw.website_id') ); $store = $website->getDefaultStore(); // add product status $status = $this->_getAttribute('status'); if ($status->isScopeGlobal()) { $select->join( array('t_status' => $status->getBackend()->getTable()), 'e.entity_id=t_status.entity_id' . ' AND t_status.attribute_id=:status_attribute_id' . ' AND t_status.store_id=0', array('status' => 't_status.value') ); } else { $statusField = $read->getCheckSql( 't2_status.value_id > 0', 't2_status.value', 't1_status.value' ); $statusTable = $status->getBackend()->getTable(); $select->join( array('t1_status' => $statusTable), 'e.entity_id=t1_status.entity_id' . ' AND t1_status.attribute_id=:status_attribute_id' . ' AND t1_status.store_id=0', array('status' => $statusField) ) ->joinLeft( array('t2_status' => $statusTable), 't1_status.entity_id = t2_status.entity_id' . ' AND t1_status.attribute_id = t2_status.attribute_id' . ' AND t2_status.store_id=:store_id', array() ); } $bind = array( 'status_attribute_id' => $status->getAttributeId(), 'website_id' => $website->getId(), 'store_id' => $store->getId() ); Mage::dispatchEvent('catalog_product_prepare_index_select', array( 'website' => $website, 'select' => $select, 'bind' => $bind )); $query = $read->query($select, $bind); while ($row = $query->fetch()) { $salable = isset($row['salable']) ? $row['salable'] : true; $website = $row['website_id'] > 0 ? true : false; $status = $row['status']; $productsData[$row['entity_id']] = $salable && $status && $website; } return $productsData; } /** * Retrieve Selection Product price from Price Index * Return index key {entity_id}-{website_id}-{customer_group_id} * * @param int|array $productIds * @return array */ public function getProductsPriceFromIndex($productIds) { $price = $this->_getAttribute('price'); $read = $this->_getReadAdapter(); $key = $read->getConcatSql(array('entity_id', 'customer_group_id', 'website_id'), '-'); $select = $read->select() ->from( array('price_index' => $this->getTable('catalogindex/price')), array('index_key' => $key, 'value') ) ->where('entity_id IN(?)', $productIds) ->where('attribute_id= :attribute_id'); $index = $read->fetchPairs($select, array('attribute_id' => $price->getAttributeId())); return $index; } /** * Retrieve product(s) price data * * @param int|array $products * @param Mage_Core_Model_Website $website * @return array */ public function getProductsPriceData($products, Mage_Core_Model_Website $website) { $productsData = array(); $read = $this->_getReadAdapter(); $select = $read->select() ->from(array('e' => $this->getTable('catalog/product')), 'entity_id') ->where('e.entity_id IN(?)', $products); $this->_addAttributeDataToSelect($select, 'price', $website); $this->_addAttributeDataToSelect($select, 'special_price', $website); $this->_addAttributeDataToSelect($select, 'special_from_date', $website); $this->_addAttributeDataToSelect($select, 'special_to_date', $website); $query = $read->query($select); while ($row = $query->fetch()) { $productsData[$row['entity_id']] = array( 'price' => $row['price'], 'special_price' => $row['special_price'], 'special_from_date' => $row['special_from_date'], 'special_to_date' => $row['special_to_date'] ); } return $productsData; } /** * Add attribute data to select * * @param Varien_Db_Select $select * @param string $attributeCode * @param Mage_Core_Model_Website $website * @return Mage_Bundle_Model_Resource_Price_Index */ protected function _addAttributeDataToSelect(Varien_Db_Select $select, $attributeCode, Mage_Core_Model_Website $website) { $attribute = $this->_getAttribute($attributeCode); $store = $website->getDefaultStore(); if ($attribute->isScopeGlobal()) { $table = 't_' . $attribute->getAttributeCode(); $select->joinLeft( array($table => $attribute->getBackend()->getTable()), "e.entity_id={$table}.entity_id" . " AND {$table}.attribute_id={$attribute->getAttributeId()}" . " AND {$table}.store_id=0", array($attribute->getAttributeCode() => $table . '.value') ); } else { $tableName = $attribute->getBackend()->getTable(); $tableGlobal = 't1_' . $attribute->getAttributeCode(); $tableStore = 't2_' . $attribute->getAttributeCode(); $attributeCond = $this->getReadConnection()->getCheckSql( $tableStore . '.value_id > 0', $tableStore . '.value', $tableGlobal . '.value' ); $select->joinLeft( array($tableGlobal => $tableName), "e.entity_id = {$tableGlobal}.entity_id" . " AND {$tableGlobal}.attribute_id = {$attribute->getAttributeId()}" . " AND {$tableGlobal}.store_id = 0", array($attribute->getAttributeCode() => $attributeCond) ) ->joinLeft( array($tableStore => $tableName), "{$tableGlobal}.entity_id = {$tableStore}.entity_id" . " AND {$tableGlobal}.attribute_id = {$tableStore}.attribute_id" . " AND {$tableStore}.store_id = " . $store->getId(), array() ); } return $this; } /** * Retrieve fixed bundle base price (with special price and rules) * * @param int $productId * @param array $priceData * @param Mage_Core_Model_Website $website * @param Mage_Customer_Model_Group $customerGroup * @return float */ protected function _getBasePrice($productId, array $priceData, $website, $customerGroup) { $store = $website->getDefaultStore(); $storeTimeStamp = Mage::app()->getLocale()->storeTimeStamp($store); $finalPrice = $this->_calculateSpecialPrice($priceData['price'], $priceData, $website); $rulePrice = Mage::getResourceModel('catalogrule/rule') ->getRulePrice($storeTimeStamp, $website->getId(), $customerGroup->getId(), $productId); if ($rulePrice !== null && $rulePrice !== false) { $finalPrice = min($finalPrice, $rulePrice); } return $finalPrice; } /** * Retrieve custom options for product * * @param int $productId * @param Mage_Core_Model_Website $website * @return array */ public function getCustomOptions($productId, Mage_Core_Model_Website $website) { $options = array(); $store = $website->getDefaultStore(); $price = $this->_getAttribute('price'); $adapter = $this->_getReadAdapter(); $bind = array( ':product_id' => $productId, ':store_id' => $store->getId(), ); $select = $adapter->select() ->from( array('option_table' => $this->getTable('catalog/product_option')), array('option_id', 'is_require', 'type') ) ->where('option_table.product_id=:product_id'); if ($price->isScopeGlobal()) { $select->join( array('price_table' => $this->getTable('catalog/product_option_price')), 'option_table.option_id = price_table.option_id' . ' AND price_table.store_id = 0', array('value_id' => 'option_price_id', 'price', 'price_type') ); } else { $valueIdCond = $adapter->getCheckSql( 'price_store_table.option_price_id IS NOT NULL', 'price_store_table.option_price_id', 'price_global_table.option_price_id' ); $priceCond = $adapter->getCheckSql( 'price_store_table.price IS NOT NULL', 'price_store_table.price', 'price_global_table.price' ); $priceTypeCond = $adapter->getCheckSql( 'price_store_table.price_type IS NOT NULL', 'price_store_table.price_type', 'price_global_table.price_type' ); $select ->join( array('price_global_table' => $this->getTable('catalog/product_option_price')), 'option_table.option_id=price_global_table.option_id' . ' AND price_global_table.store_id=0', array( 'value_id' => $valueIdCond, 'price' => $priceCond, 'price_type' => $priceTypeCond )) ->joinLeft( array('price_store_table' => $this->getTable('catalog/product_option_price')), 'option_table.option_id = price_store_table.option_id' . ' AND price_store_table.store_id=:store_id', array() ); } $query = $adapter->query($select, $bind); while ($row = $query->fetch()) { if (!isset($options[$row['option_id']])) { $options[$row['option_id']] = array( 'option_id' => $row['option_id'], 'is_require' => $row['is_require'], 'type' => $row['type'], 'values' => array() ); } $options[$row['option_id']]['values'][$row['value_id']] = array( 'price_type' => $row['price_type'], 'price_value' => $row['price'] ); } $select = $adapter->select() ->from( array('option_table' => $this->getTable('catalog/product_option')), array('option_id', 'is_require', 'type') ) ->join( array('type_table' => $this->getTable('catalog/product_option_type_value')), 'option_table.option_id=type_table.option_id', array() ) ->where('option_table.product_id=:product_id'); if ($price->isScopeGlobal()) { $select->join( array('price_table' => $this->getTable('catalog/product_option_type_price')), 'type_table.option_type_id=price_table.option_type_id' . ' AND price_table.store_id=0', array('value_id' => 'option_type_id', 'price', 'price_type') ); } else { $select ->join( array('price_global_table' => $this->getTable('catalog/product_option_type_price')), 'type_table.option_type_id=price_global_table.option_type_id' . ' AND price_global_table.store_id=0', array( 'value_id' => $valueIdCond, 'price' => $priceCond, 'price_type' => $priceTypeCond ) ) ->joinLeft( array('price_store_table' => $this->getTable('catalog/product_option_type_price')), 'type_table.option_type_id=price_store_table.option_type_id' . ' AND price_store_table.store_id=:store_id', array() ); } $query = $adapter->query($select, $bind); while ($row = $query->fetch()) { if (!isset($options[$row['option_id']])) { $options[$row['option_id']] = array( 'option_id' => $row['option_id'], 'is_require' => $row['is_require'], 'type' => $row['type'], 'values' => array() ); } $options[$row['option_id']]['values'][$row['value_id']] = array( 'price_type' => $row['price_type'], 'price_value' => $row['price'] ); } return $options; } /** * Calculate custom options price * Return array with indexes(0 -> min_price, 1 -> max_price) * * @param array $options * @param float $basePrice * @param float $minPrice * @param float $maxPrice * @return array */ public function _calculateCustomOptions(array $options, $basePrice, $minPrice, $maxPrice) { foreach ($options as $option) { $optionPrices = array(); foreach ($option['values'] as $value) { if ($value['price_type'] == 'percent') { $valuePrice = $basePrice * $value['price_value'] / 100; } else { $valuePrice = $value['price_value']; } $optionPrices[] = $valuePrice; } if ($option['is_require']) { $minPrice += min($optionPrices); } $multiTypes = array( Mage_Catalog_Model_Product_Option::OPTION_TYPE_DROP_DOWN, Mage_Catalog_Model_Product_Option::OPTION_TYPE_CHECKBOX, Mage_Catalog_Model_Product_Option::OPTION_TYPE_MULTIPLE ); if ($optionPrices) { if (in_array($option['type'], $multiTypes)) { $maxPrice += array_sum($optionPrices); } else { $maxPrice += max($optionPrices); } } } return array($minPrice, $maxPrice); } /** * Calculate minimal and maximal price for bundle selections * Return array with prices (0 -> min_price, 1 -> max_price) * * @param array $options * @param array $salableStatus * @param int $productId * @param int $priceType * @param float $basePrice * @param array $priceData * @param array $priceIndex * @param Mage_Core_Model_Website $website * @param Mage_Customer_Model_Group $group * @return array */ public function _calculateBundleSelections(array $options, array $salableStatus, $productId, $priceType, $basePrice, $priceData, $priceIndex, $website, $group) { $minPrice = $maxPrice = $basePrice; $optPrice = 0; foreach ($options as $option) { $optionPrices = array(); foreach ($option['selections'] as $selection) { if (!$selection['product_id']) { continue; } if (!$salableStatus[$selection['product_id']]) { continue; } if ($priceType == Mage_Bundle_Model_Product_Price::PRICE_TYPE_FIXED) { $basePrice = $this->_getBasePrice($productId, $priceData, $website, $group); } // calculate selection price if ($priceType == Mage_Bundle_Model_Product_Price::PRICE_TYPE_DYNAMIC) { $priceIndexKey = join('-', array( $selection['product_id'], $website->getId(), $group->getId() )); $selectionPrice = isset($priceIndex[$priceIndexKey]) ? $priceIndex[$priceIndexKey] : 0; $selectionPrice = $this->_calculateSpecialPrice($selectionPrice, $priceData, $website); } else { if ($selection['price_type']) { // percent $selectionPrice = $basePrice * $selection['price_value'] / 100; } else { $selectionPrice = $this->_calculateSpecialPrice($selection['price_value'], $priceData, $website); } } // calculate selection qty if ($selection['can_change_qty'] && $option['type'] != 'multi' && $option['type'] != 'checkbox') { $qty = 1; } else { $qty = $selection['qty']; } $selectionPrice = $selectionPrice * $qty; $optionPrices[$selection['selection_id']] = $selectionPrice; } if ($optionPrices) { if ($option['required']) { $minPrice += min($optionPrices); } else { $optPrice = $optPrice && $optPrice < min($optionPrices) ? $optPrice : min($optionPrices); } if (in_array($option['type'], array('multi', 'checkbox'))) { $maxPrice += array_sum($optionPrices); } else { $maxPrice += max($optionPrices); } } } if ($minPrice == 0) { $minPrice = $optPrice; } return array($minPrice, $maxPrice); } /** * Apply special price * * @param float $finalPrice * @param array $priceData * @param Mage_Core_Model_Website $website * @return float */ public function _calculateSpecialPrice($finalPrice, array $priceData, Mage_Core_Model_Website $website) { $store = $website->getDefaultStore(); $specialPrice = $priceData['special_price']; if (!is_null($specialPrice) && $specialPrice != false) { if (Mage::app()->getLocale()->isStoreDateInInterval($store, $priceData['special_from_date'], $priceData['special_to_date'])) { $specialPrice = ($finalPrice * $specialPrice) / 100; $finalPrice = min($finalPrice, $specialPrice); } } return $finalPrice; } /** * Retrieve price index for products * * @param int|array $productIds * @param int $websiteId * @param int $groupId * @return array */ public function loadPriceIndex($productIds, $websiteId, $groupId) { $prices = array(); $adapter = $this->_getReadAdapter(); $select = $adapter->select() ->from( array('pi' => $this->getMainTable()), array('entity_id', 'min_price', 'max_price') ) ->where('entity_id IN(?)', $productIds) ->where('website_id=:website_id') ->where('customer_group_id=:group_id'); $bind = array( 'website_id' => $websiteId, 'group_id' => $groupId ); $query = $adapter->query($select, $bind); while ($row = $query->fetch()) { $prices[$row['entity_id']] = array( 'min_price' => $row['min_price'], 'max_price' => $row['max_price'] ); } return $prices; } }