Index: branches/5.2.x/units/pricing/pricing_event_handler.php
===================================================================
--- branches/5.2.x/units/pricing/pricing_event_handler.php	(revision 16774)
+++ branches/5.2.x/units/pricing/pricing_event_handler.php	(revision 16775)
@@ -1,524 +1,523 @@
 <?php
 /**
 * @version	$Id$
 * @package	In-Commerce
 * @copyright	Copyright (C) 1997 - 2011 Intechnic. All rights reserved.
 * @license	Commercial License
 * This software is protected by copyright law and international treaties.
 * Unauthorized reproduction or unlicensed usage of the code of this program,
 * or any portion of it may result in severe civil and criminal penalties,
 * and will be prosecuted to the maximum extent possible under the law
 * See http://www.in-portal.org/commercial-license for copyright notices and details.
 */
 
 defined('FULL_PATH') or die('restricted access!');
 
 // include globals.php from current folder
 kUtil::includeOnce(MODULES_PATH . '/in-commerce/units/pricing/globals.php');
 
 class PricingEventHandler extends kDBEventHandler {
 
 	/**
 	 * Allows to override standard permission mapping
 	 *
 	 * @return void
 	 * @access protected
 	 * @see kEventHandler::$permMapping
 	 */
 	protected function mapPermissions()
 	{
 		parent::mapPermissions();
 
 		$permissions = Array (
 			'OnMoreBrackets' => Array ('subitem' => 'add|edit'),
 			'OnInfinity' => Array ('subitem' => 'add|edit'),
 			'OnArrange' => Array ('subitem' => 'add|edit'),
 			'OnDeleteBrackets' => Array ('subitem' => 'add|edit'),
 		);
 
 		$this->permMapping = array_merge($this->permMapping, $permissions);
 	}
 
 	/**
 	 * Define alternative event processing method names
 	 *
 	 * @return void
 	 * @see kEventHandler::$eventMethods
 	 * @access protected
 	 */
 	protected function mapEvents()
 	{
 		parent::mapEvents();	// ensure auto-adding of approve/decline and so on events
 
 		$brackets_events = Array(
 			'OnMoreBrackets' => 'PricingBracketsAction',
 			'OnArrange' => 'PricingBracketsAction',
 			'OnInfinity' => 'PricingBracketsAction',
 			'OnDeleteBrackets' => 'PricingBracketsAction',
 		);
 
 		$this->eventMethods = array_merge($this->eventMethods, $brackets_events);
 	}
 
 	function PricingBracketsAction($event)
 	{
 		$event->redirect=false;
 		$temp = $this->Application->GetVar($event->getPrefixSpecial(true));
 
 //		$object = $event->getObject();
 //		$formatter = $this->Application->recallObject('kFormatter');
 //		$temp = $formatter->TypeCastArray($temp, $object);
 
 		//uasort($temp, 'pr_bracket_comp');
 		$bracket = $this->Application->recallObject($event->getPrefixSpecial());
 		foreach($temp as $id => $record)
 		{
 			if( $record['MaxQty'] == '&#8734;' || $record['MaxQty'] == '∞')
 			{
 				$temp[$id]['MaxQty'] = -1;
 			}
 		}
 
 		$group_id = $this->Application->getVar('group_id');
 		if($group_id>0){
 			$where_group=' GroupId = '.$group_id.' ';
 		}
 		else {
 			$where_group= ' TRUE ';
 		}
 
 		switch ($event->Name)
 		{
 			case 'OnMoreBrackets':
 
 				$new_id = (int)$this->Conn->GetOne('SELECT MIN('.$bracket->IDField.') FROM '.$bracket->TableName);
 				if($new_id > 0) $new_id = 0;
 				do
 				{
 					$new_id--;
 				} while
 				($this->check_array($this->Application->GetVar($event->getPrefixSpecial(true)), 'PriceId', $new_id));
 
 
 				$last_max_qty = $this->Conn->GetOne('SELECT MAX(MaxQty) FROM '.$bracket->TableName.' WHERE '.$where_group);
 				$min_qty = $this->Conn->GetOne('SELECT MIN(MaxQty) FROM '.$bracket->TableName.' WHERE '.$where_group);
 
 				if ($min_qty==-1) $last_max_qty = -1;
 				if (!$last_max_qty) $last_max_qty=1;
 
 				for($i = $new_id; $i > $new_id - 5; $i--)
 				{
 					$temp[$i]['PriceId'] = $i;
 					$temp[$i]['MinQty'] = ($i == $new_id-4 && $last_max_qty != -1) ? $last_max_qty : '';
 					$temp[$i]['MaxQty'] = ($i == $new_id-4 && $last_max_qty != -1) ? -1 : '';
 					$temp[$i]['Price'] = '';
 					$temp[$i]['Cost'] = '';
 					$temp[$i]['Points'] = '';
 					$temp[$i]['Negotiated'] = '0';
 					$temp[$i]['IsPrimary'] = '0';
 					$temp[$i]['GroupId'] = $group_id;
 				}
 
 				$this->Application->SetVar($event->getPrefixSpecial(true), $temp);
 				$event->CallSubEvent('OnPreSaveBrackets');
 				break;
 
 			case 'OnArrange':
 				$temp=$this->OnArrangeBrackets($event, $temp, $bracket);
 				$this->Application->SetVar($event->getPrefixSpecial(true), $temp);
 				$event->CallSubEvent('OnPreSaveBrackets');
 				break;
 
 			case 'OnInfinity':
 				$temp=$this->OnArrangeBrackets($event, $temp, $bracket);
 				$this->Application->SetVar($event->getPrefixSpecial(true), $temp);
 				$event->CallSubEvent('OnPreSaveBrackets');
 
 				$infinite_exists = $this->Conn->GetOne('SELECT count(*) FROM '.$bracket->TableName.' WHERE MaxQty=-1 '.' AND '.$where_group);
 
 				if($infinite_exists==0){
 					reset($temp);
 					$last_bracket=end($temp);
 					$new_id = (int)$this->Conn->GetOne('SELECT MIN('.$bracket->IDField.') FROM '.$bracket->TableName);
 
 					$brackets_exist = (int)$this->Conn->GetOne('SELECT COUNT(*) FROM '.$bracket->TableName.' WHERE '.$where_group);
 
 					if($new_id > 0) $new_id = 0;
 					do
 					{
 						$new_id--;
 					} while
 					($this->check_array($this->Application->GetVar($event->getPrefixSpecial(true)), 'PriceId', $new_id));
 
 
 					$infinite_bracket['PriceId'] = $new_id;
 					$infinite_bracket['MinQty'] = ($brackets_exist>0)?$last_bracket['MaxQty']:1;
 					$infinite_bracket['MaxQty'] = '-1';
 					$infinite_bracket['Price'] = '';
 					$infinite_bracket['Cost'] = '';
 					$infinite_bracket['Points'] = '';
 					$infinite_bracket['Negotiated'] = '0';
 					$infinite_bracket['IsPrimary'] = '0';
 					$infinite_bracket['GroupId'] = $group_id;
 					$temp[$new_id]=$infinite_bracket;
 					reset($temp);
 				}
 
 				$this->Application->SetVar($event->getPrefixSpecial(true), $temp);
 				$event->CallSubEvent('OnPreSaveBrackets');
 				break;
 
 			case 'OnDeleteBrackets':
 				if ($group_id) {
 					$temp = ''; // delete all pricings from "pr_tang" var
 
 					$sql = 'DELETE FROM ' . $bracket->TableName . '
 							WHERE ProductId = ' . $this->Application->GetVar('p_id') . ' AND GroupId = ' . $group_id;
 					$this->Conn->Query($sql);
 				}
 				break;
 
 			default:
 		}
 
 		$this->Application->SetVar($event->getPrefixSpecial(true), $temp); // store pr_tang var
 	}
 
 	function OnPreSaveBrackets(kEvent $event)
 	{
 		if( $this->Application->GetVar('pr_tang') ) {
 
 			/** @var kDBItem $object */
 			$object = $event->getObject();
 
 			$product_id = $this->Application->GetVar('p_id');
 			$group_id = $this->Application->getVar('group_id');
 
 			$sql = 'SELECT PriceId
 					FROM ' . $object->TableName . '
 					WHERE ProductId = ' . $product_id . ' ' . ($group_id? 'AND GroupId = ' . $group_id : '');
 			$stored_ids = $this->Conn->GetCol($sql);
 
 			$items_info = $this->Application->GetVar( $event->getPrefixSpecial(true) ); // get pr_tang var
 			uasort($items_info, 'pr_bracket_comp');
 
 			foreach ($items_info as $item_id => $field_values) {
 
 				if (in_array($item_id, $stored_ids)) { //if it's already exist
 					$object->Load($item_id);
 					$object->SetFieldsFromHash($field_values);
 					$event->setEventParam('form_data', $field_values);
 
 					if (!$object->Validate()) {
 						unset($stored_ids[array_search($item_id, $stored_ids)]);
 						$event->redirect = false;
 						continue;
 					}
 					if( $object->Update($item_id) ) {
 						$event->status=kEvent::erSUCCESS;
 					}
 					else {
 						$event->status=kEvent::erFAIL;
 						$event->redirect=false;
 						break;
 					}
 					unset($stored_ids[array_search($item_id, $stored_ids)]);
 				}
 				else {
 					$object->Clear(0);
 					$object->SetFieldsFromHash($field_values);
 					$event->setEventParam('form_data', $field_values);
 
 					$object->SetDBField('ProductId', $product_id);
 
 					if( $object->Create() ) {
 						$event->status=kEvent::erSUCCESS;
 					}
 				}
 			}
 
 			// delete
 			foreach ($stored_ids as $stored_id) {
 				$this->Conn->Query('DELETE FROM ' . $object->TableName . ' WHERE PriceId = ' . $stored_id);
 			}
 
 		}
 	}
 
 	/**
 	 * Apply custom processing to item
 	 *
 	 * @param kEvent $event
 	 * @param string $type
 	 * @return void
 	 * @access protected
 	 */
 	protected function customProcessing(kEvent $event, $type)
 	{
 		/** @var kDBItem $bracket */
 		$bracket = $event->getObject();
 
 		switch ($type) {
 			case 'before':
 				$bracket->SetDBField('ProductId', $this->Application->GetVar('p_id'));
 
 				if ( $bracket->GetDBField('MaxQty') == '&#8734;' || $bracket->GetDBField('MaxQty') == '∞' ) {
 					$bracket->SetDBField('MaxQty', -1);
 				}
 				break;
 		}
 	}
 
 	function OnArrangeBrackets($event, &$temp, &$bracket)
 	{
 		$temp_orig = $temp;
 		reset($temp);
 		if (is_array($temp))
 		{
 			// array to store max values (2nd column)
 			$end_values = Array();
 
 			// get minimal value of Min
 			$first_elem=current($temp);
 			$start = $first_elem['MinQty'];
 			if (!$start){
 				$start = 1;
 			}
 			foreach($temp as $id => $record)
 			{
 
 				/*
 				This 3-ifs logic fixes collision with invalid input values having
 				1 pricing record.
 				The logic is:
 				1) If we got Max less than Min, we set Min to 1 that gives us
 				integrity.
 				2) If we got equal values for Min and Max, we set range 1..Max like
 				in previous. But if Min was 1 and Max was 1 we set full range 1..infinity
 				3) If we got Max = 0 we just set it tom infinity because we can't
 				guess what user meant
 				*/
 
 				if (sizeof($temp) == 1 && $record['MinQty'] > ($record['MaxQty'] == -1 ? $record['MinQty']+1 : $record['MaxQty']) ){
 					$record['MinQty'] = 1;
 					$temp[$id]['MinQty'] = 1;
 					$start = 1;
 				}
 
 				if (sizeof($temp) == 1 && $record['MinQty'] == $record['MaxQty']){
 					if ($record['MaxQty'] == 1){
 						$record['MaxQty'] = -1;
 						$temp[$id]['MaxQty'] = -1;
 					}
 					else {
 						$record['MinQty'] = 1;
 						$temp[$id]['MinQty'] = 1;
 					}
 				}
 
 				if (sizeof($temp) == 1 && $record['MaxQty'] == 0){
 					$record['MaxQty'] = -1;
 					$temp[$id]['MaxQty'] = -1;
 				}
 
 				if(
 				// MAX is less than start
 				($record['MaxQty'] <= $start && $record['MaxQty'] != -1) ||
 				// Max is empty
 				!$record['MaxQty'] ||
 				// Max already defined in $end_values
 				(array_search($record['MaxQty'], $end_values) !== false)
 				) {	// then delete from brackets list
 					unset($temp[$id]);
 				}
 				else {	// this is when ok - add to end_values list
 					$end_values[] = $record['MaxQty'];
 				}
 			}
 
 			// sort brackets by 2nd column (Max values)
 			uasort($temp, 'pr_bracket_comp');
 			reset($temp);
 			$first_item_key = key($temp);
 
 			$group_id = $this->Application->getVar('group_id');
 
 
 			$default_group = $this->Application->ConfigValue('User_LoggedInGroup');
 			if($group_id>0){
 				$where_group=' AND GroupId = '.$group_id.' ';
 			}
 
 			$ids = $this->Conn->GetCol('SELECT PriceId FROM '.$bracket->TableName.' WHERE ProductId='.$this->Application->GetVar('p_id').' '.$where_group);
 			if(is_array($ids)) {
 				usort($ids, 'pr_bracket_id_sort');
 			}
 			$min_id = min( min($ids) - 1, -1 );
 
 
 			foreach($temp as $key => $record)
 			{
 				$temp[$key]['MinQty']=$start;
 				$temp[$key]['IsPrimary']=0;
 				$temp[$key]['GroupId']=$group_id;
 				$start=$temp[$key]['MaxQty'];
 
 			}
 			if ($temp[$first_item_key]['GroupId'] == $default_group) {
 				$temp[$first_item_key]['IsPrimary']=1;
 			}
 
 		}
 		return $temp;
 	}
 
 	/**
 	 * Set's price as primary for product
 	 *
 	 * @param kEvent $event
 	 */
 	function OnSetPrimary($event)
 	{
 		$object = $event->getObject( Array('skip_autoload' => true) );
 		$this->StoreSelectedIDs($event);
 		$ids=$this->getSelectedIDs($event);
 		if($ids)
 		{
 			$id = array_shift($ids);
 			$table_info = $object->getLinkedInfo();
 
 			$this->Conn->Query('UPDATE '.$object->TableName.' SET IsPrimary = 0 WHERE '.$table_info['ForeignKey'].' = '.$table_info['ParentId']);
 			$this->Conn->Query('UPDATE '.$object->TableName.' SET IsPrimary = 1 WHERE ('.$table_info['ForeignKey'].' = '.$table_info['ParentId'].') AND (PriceId = '.$id.')');
 		}
 		$event->SetRedirectParam('opener', 's');
 	}
 
 	/**
 	 * Resets primary mark for other prices of given product, when current pricing is primary
 	 *
 	 * @param kEvent $event
 	 * @return void
 	 * @access protected
 	 */
 	protected function OnBeforeItemUpdate(kEvent $event)
 	{
 		parent::OnBeforeItemUpdate($event);
 
 		/** @var kDBItem $object */
 		$object = $event->getObject();
 
 		if ( $object->GetDBField('IsPrimary') == 1 ) {
 			// make all prices non primary, when this one is
 			$sql = 'UPDATE ' . $object->TableName . '
 					SET IsPrimary = 0
 					WHERE (ProductId = ' . $object->GetDBField('ProductId') . ') AND (' . $object->IDField . ' <> ' . $object->GetID() . ')';
 			$this->Conn->Query($sql);
 		}
 	}
 
 	/**
 	 * Occurs before creating item
 	 *
 	 * @param kEvent $event
 	 * @return void
 	 * @access protected
 	 */
 	protected function OnBeforeItemCreate(kEvent $event)
 	{
 		parent::OnBeforeItemCreate($event);
 
 		/** @var kDBItem $object */
 		$object = $event->getObject();
 
 		$table_info = $object->getLinkedInfo($event->Special);
 
 		$table_info['ParentId'] = ($table_info['ParentId'] ? $table_info['ParentId'] : 0);
 
 		if ( $object->GetDBField('IsPrimary') == 1 ) {
 			$sql = 'UPDATE ' . $object->TableName . '
 					SET IsPrimary = 0
 					WHERE ' . $table_info['ForeignKey'] . ' = ' . $table_info['ParentId'];
 			$this->Conn->Query($sql);
 		}
 		else {
 			$sql = 'SELECT COUNT(*)
 					FROM ' . $object->TableName . '
 					WHERE ' . $table_info['ForeignKey'] . ' = ' . $table_info['ParentId'];
 			$prices_qty = $this->Conn->GetOne($sql);
 
 			if ( $prices_qty == 0 ) {
 				$object->SetDBField('IsPrimary', 1);
 			}
 		}
 	}
 
 	/**
 	 * Apply any custom changes to list's sql query
 	 *
 	 * @param kEvent $event
 	 * @return void
 	 * @access protected
 	 * @see kDBEventHandler::OnListBuild()
 	 */
 	protected function SetCustomQuery(kEvent $event)
 	{
 		/** @var kDBList $object */
 		$object = $event->getObject();
 
 		if ( $this->Application->isAdminUser ) {
 			return;
 		}
 
 		if ( $this->Application->ConfigValue('Comm_PriceBracketCalculation') == 1 ) {
-			$sql = 'SELECT PrimaryGroupId
-					FROM ' . TABLE_PREFIX . 'Users
-					WHERE PortalUserId = ' . $this->Application->GetVar('u_id');
-			$pricing_group = $this->Conn->GetOne($sql);
+			/** @var UserHelper $user_helper */
+			$user_helper = $this->Application->recallObject('UserHelper');
+			$pricing_group = $user_helper->getPrimaryGroup($this->Application->RecallVar('user_id'));
 
-			if ( $pricing_group ) {
+			if ( $this->Application->LoggedIn() ) {
 				$sql = 'SELECT COUNT(*)
 						FROM ' . TABLE_PREFIX . 'ProductsPricing
 						WHERE ProductId = ' . $this->Application->GetVar('p_id') . ' AND GroupId = ' . $pricing_group . ' AND Price IS NOT NULL';
 				$pricing_for_group_exists = $this->Conn->GetOne($sql);
-			}
 
-			if ( !$pricing_group || !$pricing_for_group_exists ) {
-				$pricing_group = $this->Application->ConfigValue('User_LoggedInGroup');
+				if ( !$pricing_for_group_exists ) {
+					$pricing_group = $this->Application->ConfigValue('User_LoggedInGroup');
+				}
 			}
 		}
 		else {
 			$user_groups = $this->Application->RecallVar('UserGroups');
 
 			//$cheapest_group = $this->Conn->GetOne('SELECT GroupId FROM '.$object->TableName.' WHERE ProductId='.$this->Application->GetVar('p_id').' AND Price IS NOT NULL AND GroupId IN ('.$user_groups.') AND MinQty = 1 GROUP BY GroupId ORDER BY Price ASC');
 
 			$sql = 'SELECT PriceId, Price, GroupId
 					FROM ' . $object->TableName . '
 					WHERE ProductId = ' . $this->Application->GetVar('p_id') . ' AND Price IS NOT NULL AND GroupId IN (' . $user_groups . ')
 					ORDER BY GroupId ASC, MinQty ASC';
 			$effective_brackets = $this->Conn->Query($sql, 'PriceId');
 
 			$group_prices = array ();
 			$min_price = -1;
 			$cheapest_group = 0;
 
 			foreach ($effective_brackets as $bracket) {
 				if ( !isset($group_prices[$bracket['GroupId']]) ) {
 					$group_prices[$bracket['GroupId']] = $bracket['Price'];
 					if ( $bracket['Price'] < $min_price || $min_price == -1 ) {
 						$min_price = $bracket['Price'];
 						$cheapest_group = $bracket['GroupId'];
 					}
 				}
 			}
 
 			if ( !$cheapest_group ) {
 				$cheapest_group = $this->Application->ConfigValue('User_LoggedInGroup');
 			}
 
 			$pricing_group = $cheapest_group;
 		}
 
 		$object->addFilter('price_user_group', $object->TableName . '.GroupId=' . $pricing_group);
 	}
 
 }
Index: branches/5.2.x/units/orders/order_calculator.php
===================================================================
--- branches/5.2.x/units/orders/order_calculator.php	(revision 16774)
+++ branches/5.2.x/units/orders/order_calculator.php	(revision 16775)
@@ -1,859 +1,844 @@
 <?php
 /**
 * @version   $Id$
 * @package   In-Commerce
 * @copyright   Copyright (C) 1997 - 2009 Intechnic. All rights reserved.
 * @license   Commercial License
 * This software is protected by copyright law and international treaties.
 * Unauthorized reproduction or unlicensed usage of the code of this program,
 * or any portion of it may result in severe civil and criminal penalties,
 * and will be prosecuted to the maximum extent possible under the law
 * See http://www.in-portal.org/commercial-license for copyright notices and details.
 */
 
 	defined('FULL_PATH') or die('restricted access!');
 
 	/**
 	 * Performs order price calculations
 	 *
 	 */
 	class OrderCalculator extends kBase {
 
 		/**
 		 * Order manager instance
 		 *
 		 * @var OrderManager
 		 */
 		protected $manager = null;
 
 		/**
 		 * Items, associated with current order
 		 *
 		 * @var Array
 		 */
 		protected $items = Array ();
 
 		/**
 		 * Creates new clean instance of calculator
 		 *
 		 */
 		public function __construct()
 		{
 			parent::__construct();
 
 			$this->reset();
 		}
 
 		/**
 		 * Sets order manager instance to calculator
 		 *
 		 * @param OrderManager $manager
 		 */
 		public function setManager(&$manager)
 		{
 			$this->manager =& $manager;
 		}
 
 		public function reset()
 		{
 			$this->items = Array ();
 		}
 
 		/**
 		 * Returns order object used in order manager
 		 *
 		 * @return OrdersItem
 		 */
 		protected function &getOrder()
 		{
 			$order =& $this->manager->getOrder();
 
 			return $order;
 		}
 
 		/**
 		 * Sets checkout error
 		 *
 		 * @param int $error_type = {product,coupon,gc}
 		 * @param int $error_code
 		 * @param int $product_id - {ProductId}:{OptionsSalt}:{BackOrderFlag}:{FieldName}
 		 * @return void
 		 * @access protected
 		 */
 		protected function setError($error_type, $error_code, $product_id = null)
 		{
 			$this->manager->setError($error_type, $error_code, $product_id);
 		}
 
 		/**
 		 * Perform order calculations and prepares operations for order manager
 		 *
 		 */
 		public function calculate()
 		{
 			$this->queryItems();
 			$this->groupItems();
 
 			$this->generateOperations();
 			$this->applyWholeOrderFlatDiscount();
 		}
 
 		/**
 		 * Groups order items, when requested
 		 *
 		 * @return Array
 		 */
 		protected function groupItems()
 		{
 			$skipped_items = Array ();
 
 			foreach ($this->items as $item_id => $item_data) {
 				if ( in_array($item_id, $skipped_items) ) {
 					continue;
 				}
 
 				$group_items = $this->getItemsToGroupWith($item_id);
 
 				if (!$group_items) {
 					continue;
 				}
 
 				foreach ($group_items as $group_item_id) {
 					$this->items[$item_id]['Quantity'] += $this->items[$group_item_id]['Quantity'];
 					$this->items[$group_item_id]['Quantity'] = 0;
 				}
 
 				$skipped_items = array_merge($skipped_items, $group_items);
 			}
 		}
 
 		/**
 		 * Returns order item ids, that can be grouped with given order item id
 		 *
 		 * @param int $target_item_id
 		 * @return Array
 		 * @see OrderCalculator::canBeGrouped
 		 */
 		protected function getItemsToGroupWith($target_item_id)
 		{
 			$ret = Array ();
 
 			foreach ($this->items as $item_id => $item_data) {
 				if ( $this->canBeGrouped($this->items[$item_id], $this->items[$target_item_id]) ) {
 					$ret[] = $item_id;
 				}
 			}
 
 			return array_diff($ret, Array ($target_item_id));
 		}
 
 		/**
 		 * Checks if 2 given order items can be grouped together
 		 *
 		 * @param Array $src_item
 		 * @param Array $dst_item
 		 * @return bool
 		 */
 		public function canBeGrouped($src_item, $dst_item)
 		{
 			if ($dst_item['Type'] != PRODUCT_TYPE_TANGIBLE) {
 				return false;
 			}
 
 			return ($src_item['ProductId'] == $dst_item['ProductId']) && ($src_item['OptionsSalt'] == $dst_item['OptionsSalt']);
 		}
 
 		/**
 		 * Retrieves order contents from database
 		 *
 		 */
 		protected function queryItems()
 		{
 			$poc_table = $this->Application->getUnitOption('poc', 'TableName');
 
 			$query = '	SELECT 	oi.ProductId, oi.OptionsSalt, oi.ItemData, oi.Quantity,
 								IF(p.InventoryStatus = ' . ProductInventory::BY_OPTIONS . ', poc.QtyInStock, p.QtyInStock) AS QtyInStock,
 								p.QtyInStockMin, p.BackOrder, p.InventoryStatus,
 								p.Type, oi.OrderItemId
 						FROM ' . $this->getTable('orditems') . ' AS oi
 						LEFT JOIN ' . TABLE_PREFIX . 'Products AS p ON oi.ProductId = p.ProductId
 						LEFT JOIN ' . $poc_table . ' poc ON (poc.CombinationCRC = oi.OptionsSalt) AND (oi.ProductId = poc.ProductId)
 						WHERE oi.OrderId = ' . $this->getOrder()->GetID();
 
 			$this->items = $this->Conn->Query($query, 'OrderItemId');
 		}
 
 		/**
 		 * Generates operations and returns true, when something was changed
 		 *
 		 * @return bool
 		 */
 		protected function generateOperations()
 		{
 			$this->manager->resetOperationTotals();
 
 			foreach ($this->items as $item) {
 				$this->ensureMinQty($item);
 
 				$to_order = $back_order = 0;
 				$available = $this->getAvailableQty($item);
 
 				if ( $this->allowBackordering($item) ) {
 					// split order into order & backorder
 					if ($item['BackOrder'] == ProductBackorder::ALWAYS) {
 						$to_order = $available = 0;
 						$back_order = $item['Quantity'];
 					}
 					elseif ($item['BackOrder'] == ProductBackorder::AUTO) {
 						$to_order = $available;
 						$back_order = $item['Quantity'] - $available;
 					}
 
 					$qty = $to_order + $back_order;
 
 					$price = $this->getPlainProductPrice($item, $qty);
 					$cost = $this->getProductCost($item, $qty);
 					$discount_info = $this->getDiscountInfo( $item['ProductId'], $price, $qty );
 
 					$this->manager->addOperation($item, 0, $to_order, $price, $cost, $discount_info);
 					$this->manager->addOperation($item, 1, $back_order, $price, $cost, $discount_info);
 				}
 				else {
 					// store as normal order (and remove backorder)
 					// we could get here with backorder=never then we should order only what's available
 					$to_order = min($item['Quantity'], $available);
 
 					$price = $this->getPlainProductPrice($item, $to_order);
 					$cost = $this->getProductCost($item, $to_order);
 					$discount_info = $this->getDiscountInfo( $item['ProductId'], $price, $to_order );
 
 					$this->manager->addOperation($item, 0, $to_order, $price, $cost, $discount_info, $item['OrderItemId']);
 					$this->manager->addOperation($item, 1, 0, $price, $cost, $discount_info); // remove backorder record
 
 					if ($to_order < $item['Quantity']) {
 						// ordered less, then requested -> inform user
 						if ( $to_order > 0 ) {
 							$this->setError(OrderCheckoutErrorType::PRODUCT, OrderCheckoutError::QTY_UNAVAILABLE, $item['ProductId'] . ':' . $item['OptionsSalt'] . ':0:Quantity');
 						}
 						else {
 							$this->setError(OrderCheckoutErrorType::PRODUCT, OrderCheckoutError::QTY_OUT_OF_STOCK, $item['ProductId'] . ':' . $item['OptionsSalt'] . ':0:Quantity');
 						}
 					}
 				}
 			}
 		}
 
 		/**
 		 * Adds product to order (not to db)
 		 *
 		 * @param Array $item
 		 * @param kCatDBItem $product
 		 * @param int $qty
 		 */
 		public function addProduct($item, &$product, $qty)
 		{
 			$this->updateItemDataFromProduct($item, $product);
 
 			$price = $this->getPlainProductPrice($item, $qty);
 			$cost = $this->getProductCost($item, $qty);
 			$discount_info = $this->getDiscountInfo( $item['ProductId'], $price, $qty );
 
 			$this->manager->addOperation( $item, 0, $qty, $price, $cost, $discount_info, $item['OrderItemId'] );
 		}
 
 		/**
 		 * Apply whole order flat discount after sub-total been calculated
 		 *
 		 */
 		protected function applyWholeOrderFlatDiscount()
 		{
 			$sub_total_flat = $this->manager->getOperationTotal('SubTotalFlat');
 			$flat_discount = min( $sub_total_flat, $this->getWholeOrderPlainDiscount($global_discount_id) );
 			$coupon_flat_discount = min( $sub_total_flat, $this->getWholeOrderCouponDiscount() );
 
 			if ($coupon_flat_discount && $coupon_flat_discount > $flat_discount) {
 				$global_discount_type = 'coupon';
 				$flat_discount = $coupon_flat_discount;
 				$global_discount_id = $coupon_id;
 			}
 			else {
 				$global_discount_type = 'discount';
 			}
 
 			$sub_total = $this->manager->getOperationTotal('SubTotal');
 
 			if ($sub_total_flat - $sub_total < $flat_discount) {
 				// individual item discounts together are smaller when order flat discount
 				$this->manager->setOperationTotal('CouponDiscount', $flat_discount == $coupon_flat_discount ? $flat_discount : 0);
 				$this->manager->setOperationTotal('SubTotal', $sub_total_flat - $flat_discount);
 
 				// replace discount for each operation
 				foreach ($this->operations as $index => $operation) {
 					$discounted_price = ($operation['Price'] / $sub_total_flat) * $sub_total;
 					$this->operations[$index]['DiscountInfo'] = Array ($global_discount_id, $global_discount_type, $discounted_price, 0);
 				}
 			}
 		}
 
 		/**
 		 * Returns discount information for given product price and qty
 		 *
 		 * @param int $product_id
 		 * @param float $price
 		 * @param int $qty
 		 * @return Array
 		 */
 		protected function getDiscountInfo($product_id, $price, $qty)
 		{
 			$discounted_price = $this->getDiscountedProductPrice($product_id, $price, $discount_id);
 			$couponed_price = $this->getCouponDiscountedPrice($product_id, $price);
 
 			if ($couponed_price < $discounted_price) {
 				$discount_type = 'coupon';
 				$discount_id = $coupon_id;
 
 				$discounted_price =	$couponed_price;
 				$coupon_discount = ($price - $couponed_price) * $qty;
 			}
 			else {
 				$coupon_discount = 0;
 				$discount_type = 'discount';
 			}
 
 			return Array ($discount_id, $discount_type, $discounted_price, $coupon_discount);
 		}
 
 		/**
 		 * Returns product qty, available for ordering
 		 *
 		 * @param Array $item
 		 * @return int
 		 */
 		protected function getAvailableQty($item)
 		{
 			if ( $item['InventoryStatus'] == ProductInventory::DISABLED ) {
 				// always available
 				return $item['Quantity'] * 2;
 			}
 
 			return max(0, $item['QtyInStock'] - $item['QtyInStockMin']);
 		}
 
 		/**
 		 * Checks, that product in given order item can be backordered
 		 *
 		 * @param Array $item
 		 * @return bool
 		 */
 		protected function allowBackordering($item)
 		{
 			if ($item['BackOrder'] == ProductBackorder::ALWAYS) {
 				return true;
 			}
 
 			$available = $this->getAvailableQty($item);
 			$backordering = $this->Application->ConfigValue('Comm_Enable_Backordering');
 
 			return $backordering && ($item['Quantity'] > $available) && ($item['BackOrder'] == ProductBackorder::AUTO);
 		}
 
 		/**
 		 * Make sure, that user can't order less, then minimal required qty of product
 		 *
 		 * @param Array $item
 		 */
 		protected function ensureMinQty(&$item)
 		{
 			$sql = 'SELECT MIN(MinQty)
 					FROM ' . TABLE_PREFIX . 'ProductsPricing
 					WHERE ProductId = ' . $item['ProductId'];
 			$min_qty = max(1, $this->Conn->GetOne($sql));
 
 			$qty = $item['Quantity'];
 
 			if ($qty > 0 && $qty < $min_qty) {
 				// qty in cart increased to meat minimal qry requirements of given product
 				$this->setError(OrderCheckoutErrorType::PRODUCT, OrderCheckoutError::QTY_CHANGED_TO_MINIMAL, $item['ProductId'] . ':' . $item['OptionsSalt'] . ':0:Quantity');
 
 				$item['Quantity'] = $min_qty;
 			}
 		}
 
 		/**
 		 * Return product price for given qty, taking no discounts into account
 		 *
 		 * @param Array $item
 		 * @param int $qty
 		 * @return float
 		 */
 		public function getPlainProductPrice($item, $qty)
 		{
 			$item_data = $this->getItemData($item);
 
 			if ( isset($item_data['ForcePrice']) ) {
 				return $item_data['ForcePrice'];
 			}
 
 			$pricing_id = $this->getPriceBracketByQty($item, $qty);
 
 			$sql = 'SELECT Price
 					FROM ' . TABLE_PREFIX . 'ProductsPricing
 					WHERE PriceId = ' . $pricing_id;
 			$price = (float)$this->Conn->GetOne($sql);
 
 			if ( isset($item_data['Options']) ) {
 				$price += $this->getOptionPriceAddition($price, $item_data);
 				$price = $this->getCombinationPriceOverride($price, $item_data);
 			}
 
 			return max($price, 0);
 		}
 
 		/**
 		 * Return product cost for given qty, taking no discounts into account
 		 *
 		 * @param Array $item
 		 * @param int $qty
 		 * @return float
 		 */
 		public function getProductCost($item, $qty)
 		{
 			$pricing_id = $this->getPriceBracketByQty($item, $qty);
 
 			$sql = 'SELECT Cost
 					FROM ' . TABLE_PREFIX . 'ProductsPricing
 					WHERE PriceId = ' . $pricing_id;
 
 			return (float)$this->Conn->GetOne($sql);
 		}
 
 		/**
 		 * Return product price for given qty, taking no discounts into account
 		 *
 		 * @param Array $item
 		 * @param int $qty
 		 * @return float
 		 */
 		protected function getPriceBracketByQty($item, $qty)
 		{
 			$orderby_clause = '';
 			$where_clause = Array ();
 			$product_id = $item['ProductId'];
 
 			if ( $this->usePriceBrackets($item) ) {
 				$user_id = $this->getOrder()->GetDBField('PortalUserId');
 
 				$where_clause = Array (
 					'GroupId IN (' . $this->Application->getUserGroups($user_id) . ')',
 					'pp.ProductId = ' . $product_id,
 					'pp.MinQty <= ' . $qty,
 					$qty . ' < pp.MaxQty OR pp.MaxQty = -1',
 				);
 
 				$orderby_clause = $this->getPriceBracketOrderClause($user_id);
 			}
 			else {
 				$item_data = $this->getItemData($item);
 
 				$where_clause = Array(
 					'pp.ProductId = ' . $product_id,
 					'pp.PriceId = ' . $this->getPriceBracketFromRequest($product_id, $item_data),
 				);
 			}
 
 			$sql = 'SELECT pp.PriceId
 					FROM ' . TABLE_PREFIX . 'ProductsPricing AS pp
 					LEFT JOIN ' . TABLE_PREFIX . 'Products AS p ON p.ProductId = pp.ProductId
 					WHERE (' . implode(') AND (', $where_clause) . ')';
 
 			if ($orderby_clause) {
 				$sql .= ' ORDER BY ' . $orderby_clause;
 			}
 
 			return (float)$this->Conn->GetOne($sql);
 		}
 
 		/**
 		 * Checks if price brackets should be used in price calculations
 		 *
 		 * @param Array $item
 		 * @return bool
 		 */
 		protected function usePriceBrackets($item)
 		{
 			return $item['Type'] == PRODUCT_TYPE_TANGIBLE;
 		}
 
 		/**
 		 * Return product pricing id for given product.
 		 * If not passed - return primary pricing ID
 		 *
 		 * @param int $product_id
 		 * @return int
 		 */
 		public function getPriceBracketFromRequest($product_id, $item_data)
 		{
 			if ( !is_array($item_data) ) {
 				$item_data = unserialize($item_data);
 			}
 
 			// remembered pricing during checkout
 			if ( isset($item_data['PricingId']) && $item_data['PricingId'] ) {
 				return $item_data['PricingId'];
 			}
 
 			// selected pricing from product detail page
 			$price_id = $this->Application->GetVar('pr_id');
 
 			if ($price_id) {
 				return $price_id;
 			}
 
 			$sql = 'SELECT PriceId
 					FROM ' . TABLE_PREFIX . 'ProductsPricing
 					WHERE ProductId = ' . $product_id . ' AND IsPrimary = 1';
 
 			return $this->Conn->GetOne($sql);
 		}
 
 		/**
 		 * Returns order clause for price bracket selection based on configration
 		 *
 		 * @param int $user_id
 		 * @return string
 		 */
 		protected function getPriceBracketOrderClause($user_id)
 		{
 			if ($this->Application->ConfigValue('Comm_PriceBracketCalculation') == 1) {
 				// if we have to stick to primary group, then its pricing will go first,
 				// but if there is no pricing for primary group, then next optimal will be taken
-				$primary_group = $this->getUserPrimaryGroup($user_id);
+
+				/** @var UserHelper $user_helper */
+				$user_helper = $this->Application->recallObject('UserHelper');
+				$primary_group = $user_helper->getPrimaryGroup($user_id);
 
 				return '( IF(GroupId = ' . $primary_group . ', 1, 2) ) ASC, pp.Price ASC';
 			}
 
 			return 'pp.Price ASC';
 		}
 
 		/**
 		 * Returns addition to product price based on used product option
 		 *
 		 * @param float $price
 		 * @param Array $item_data
 		 * @return float
 		 */
 		protected function getOptionPriceAddition($price, $item_data)
 		{
 			$addition = 0;
 
 			/** @var kProductOptionsHelper $opt_helper */
 			$opt_helper = $this->Application->recallObject('kProductOptionsHelper');
 
 			foreach ($item_data['Options'] as $opt => $val) {
 				$sql = 'SELECT *
 						FROM ' . TABLE_PREFIX . 'ProductOptions
 						WHERE ProductOptionId = ' . $opt;
 				$data = $this->Conn->GetRow($sql);
 
 				$parsed = $opt_helper->ExplodeOptionValues($data);
 
 				if ( !$parsed ) {
 					continue;
 				}
 
 				if ( is_array($val) ) {
 					foreach ($val as $a_val) {
 						$addition += $this->formatPrice($a_val, $price, $parsed);
 					}
 				}
 				else {
 					$addition += $this->formatPrice($val, $price, $parsed);
 				}
 			}
 
 			return $addition;
 		}
 
 		protected function formatPrice($a_val, $price, $parsed)
 		{
 			$a_val = kUtil::unescape($a_val, kUtil::ESCAPE_HTML); // TODO: Not sure why we're unescaping.
 
 			$addition = 0;
 			$conv_prices = $parsed['Prices'];
 			$conv_price_types = $parsed['PriceTypes'];
 
 			if ( isset($conv_prices[$a_val]) && $conv_prices[$a_val] ) {
 				if ($conv_price_types[$a_val] == '$') {
 					$addition += $conv_prices[$a_val];
 				}
 				elseif ($conv_price_types[$a_val] == '%') {
 					$addition += $price * $conv_prices[$a_val] / 100;
 				}
 			}
 
 			return $addition;
 		}
 
 		/**
 		 * Returns product price after applying combination price override
 		 *
 		 * @param float $price
 		 * @param Array $item_data
 		 * @return float
 		 */
 		protected function getCombinationPriceOverride($price, $item_data)
 		{
 			$combination_salt = $this->generateOptionsSalt( $item_data['Options'] );
 
 			if (!$combination_salt) {
 				return $price;
 			}
 
 			$sql = 'SELECT *
 					FROM ' . TABLE_PREFIX . 'ProductOptionCombinations
 					WHERE CombinationCRC = ' . $combination_salt;
 			$combination = $this->Conn->GetRow($sql);
 
 			if (!$combination) {
 				return $price;
 			}
 
 			switch ( $combination['PriceType'] ) {
 				case OptionCombinationPriceType::EQUALS:
 					return $combination['Price'];
 					break;
 
 				case OptionCombinationPriceType::FLAT:
 					return $price + $combination['Price'];
 					break;
 
 				case OptionCombinationPriceType::PECENT:
 					return $price * (1 + $combination['Price'] / 100);
 					break;
 			}
 
 			return $price;
 		}
 
 		/**
 		 * Generates salt for given option set
 		 *
 		 * @param Array $options
 		 * @return int
 		 */
 		public function generateOptionsSalt($options)
 		{
 			/** @var kProductOptionsHelper $opt_helper */
 			$opt_helper = $this->Application->recallObject('kProductOptionsHelper');
 
 			return $opt_helper->OptionsSalt($options, true);
 		}
 
 		/**
 		 * Return product price for given qty, taking possible discounts into account
 		 *
 		 * @param int $product_id
 		 * @param int $price
 		 * @param int $discount_id
 		 * @return float
 		 */
 		public function getDiscountedProductPrice($product_id, $price, &$discount_id)
 		{
 			$discount_id = 0;
 			$user_id = $this->getOrder()->GetDBField('PortalUserId');
 
 			$join_clause = Array (
 				'd.DiscountId = di.DiscountId',
 				'di.ItemType = ' . DiscountItemType::PRODUCT . ' OR (di.ItemType = ' . DiscountItemType::WHOLE_ORDER . ' AND d.Type = ' . DiscountType::PERCENT . ')',
 				'd.Status = ' . STATUS_ACTIVE,
 				'd.GroupId IN (' . $this->Application->getUserGroups($user_id) . ')',
 				'd.Start IS NULL OR d.Start < ' . $this->getOrder()->GetDBField('OrderDate'),
 				'd.End IS NULL OR d.End > ' . $this->getOrder()->GetDBField('OrderDate'),
 			);
 
 			$sql = 'SELECT
 						ROUND(CASE d.Type
 							WHEN ' . DiscountType::FLAT . ' THEN ' . $price . ' - d.Amount
 							WHEN ' . DiscountType::PERCENT . ' THEN ' . $price . ' * (1 - d.Amount / 100)
 							ELSE ' . $price . '
 						END, 4), d.DiscountId
 					FROM ' . TABLE_PREFIX . 'Products AS p
 					LEFT JOIN ' . TABLE_PREFIX . 'ProductsDiscountItems AS di ON (di.ItemResourceId = p.ResourceId) OR (di.ItemType = ' . DiscountItemType::WHOLE_ORDER . ')
 					LEFT JOIN ' . TABLE_PREFIX . 'ProductsDiscounts AS d ON (' . implode(') AND (', $join_clause) . ')
 					WHERE (p.ProductId = ' . $product_id . ') AND (d.DiscountId IS NOT NULL)';
 			$pricing = $this->Conn->GetCol($sql, 'DiscountId');
 
 			if (!$pricing) {
 				return $price;
 			}
 
 			// get minimal price + discount
 			$discounted_price = min($pricing);
 			$pricing = array_flip($pricing);
 			$discount_id = $pricing[$discounted_price];
 
 			// optimal discount, but prevent negative price
 			return max( min($discounted_price, $price), 0 );
 		}
 
 		public function getWholeOrderPlainDiscount(&$discount_id)
 		{
 			$discount_id = 0;
 			$user_id = $this->getOrder()->GetDBField('PortalUserId');
 
 			$join_clause = Array (
 				'd.DiscountId = di.DiscountId',
 				'di.ItemType = ' . DiscountItemType::WHOLE_ORDER . ' AND d.Type = ' . DiscountType::FLAT,
 				'd.Status = ' . STATUS_ACTIVE,
 				'd.GroupId IN (' . $this->Application->getUserGroups($user_id) . ')',
 				'd.Start IS NULL OR d.Start < ' . $this->getOrder()->GetDBField('OrderDate'),
 				'd.End IS NULL OR d.End > ' . $this->getOrder()->GetDBField('OrderDate'),
 			);
 
 			$sql = 'SELECT d.Amount AS Discount, d.DiscountId
 					FROM ' . TABLE_PREFIX . 'ProductsDiscountItems AS di
 					LEFT JOIN ' . TABLE_PREFIX . 'ProductsDiscounts AS d ON (' . implode(') AND (', $join_clause) . ')
 					WHERE d.DiscountId IS NOT NULL';
 			$pricing = $this->Conn->GetCol($sql, 'DiscountId');
 
 			if (!$pricing) {
 				return 0;
 			}
 
 			$discounted_price = max($pricing);
 			$pricing = array_flip($pricing);
 			$discount_id = $pricing[$discounted_price];
 
 			return max($discounted_price, 0);
 		}
 
 		public function getCouponDiscountedPrice($product_id, $price)
 		{
 			if ( !$this->getCoupon() ) {
 				return $price;
 			}
 
 			$join_clause = Array (
 				'c.CouponId = ci.CouponId',
 				'ci.ItemType = ' . CouponItemType::PRODUCT . ' OR (ci.ItemType = ' . CouponItemType::WHOLE_ORDER . ' AND c.Type = ' . CouponType::PERCENT . ')',
 			);
 
 			$sql = 'SELECT
 						MIN(
 							ROUND(CASE c.Type
 								WHEN ' . CouponType::FLAT . ' THEN ' . $price . ' - c.Amount
 								WHEN ' . CouponType::PERCENT . ' THEN ' . $price . ' * (1 - c.Amount / 100)
 								ELSE ' . $price . '
 							END, 4)
 						)
 					FROM ' . TABLE_PREFIX . 'Products AS p
 					LEFT JOIN ' . TABLE_PREFIX . 'ProductsCouponItems AS ci ON (ci.ItemResourceId = p.ResourceId) OR (ci.ItemType = ' . CouponItemType::WHOLE_ORDER . ')
 					LEFT JOIN ' . TABLE_PREFIX . 'ProductsCoupons AS c ON (' . implode(') AND (', $join_clause) . ')
 					WHERE p.ProductId = ' . $product_id . ' AND ci.CouponId = ' . $this->getCoupon() . '
 					GROUP BY p.ProductId';
 
 			$coupon_price = $this->Conn->GetOne($sql);
 
 			if ($coupon_price === false) {
 				return $price;
 			}
 
 			return max( min($price, $coupon_price), 0 );
 		}
 
 		public function getWholeOrderCouponDiscount()
 		{
 			if ( !$this->getCoupon() ) {
 				return 0;
 			}
 
 			$where_clause = Array (
 				'ci.CouponId = ' . $this->getCoupon(),
 				'ci.ItemType = ' . CouponItemType::WHOLE_ORDER,
 				'c.Type = ' . CouponType::FLAT,
 			);
 
 			$sql = 'SELECT Amount
 					FROM ' . TABLE_PREFIX . 'ProductsCouponItems AS ci
 					LEFT JOIN ' . TABLE_PREFIX . 'ProductsCoupons AS c ON c.CouponId = ci.CouponId
 					WHERE (' . implode(') AND (', $where_clause) . ')';
 
 			return $this->Conn->GetOne($sql);
 		}
 
 		protected function getCoupon()
 		{
 			return $this->getOrder()->GetDBField('CouponId');
 		}
 
 		/**
-		 * Returns primary group of given user
-		 *
-		 * @param int $user_id
-		 * @return int
-		 */
-		protected function getUserPrimaryGroup($user_id)
-		{
-			if ($user_id > 0) {
-				$sql = 'SELECT PrimaryGroupId
-						FROM ' . TABLE_PREFIX . 'Users
-						WHERE PortalUserId = ' . $user_id;
-				return $this->Conn->GetOne($sql);
-			}
-
-			return $this->Application->ConfigValue('User_LoggedInGroup');
-		}
-
-		/**
 		 * Returns ItemData associated with given order item
 		 *
 		 * @param Array $item
 		 * @return Array
 		 */
 		protected function getItemData($item)
 		{
 			$item_data = $item['ItemData'];
 
 			if ( is_array($item_data) ) {
 				return $item_data;
 			}
 
 			return $item_data ? unserialize($item_data) : Array ();
 		}
 
 		/**
 		 * Sets ItemData according to product
 		 *
 		 * @param Array $item
 		 * @param kCatDBItem $product
 		 */
 		protected function updateItemDataFromProduct(&$item, &$product)
 		{
 			$item_data = $this->getItemData($item);
 			$item_data['IsRecurringBilling'] = $product->GetDBField('IsRecurringBilling');
 
 			// it item is processed in order using new style, then put such mark in orderitem record
 			$processing_data = $product->GetDBField('ProcessingData');
 
 			if ($processing_data) {
 				$processing_data = unserialize($processing_data);
 
 				if ( isset($processing_data['HasNewProcessing']) ) {
 					$item_data['HasNewProcessing'] = 1;
 				}
 			}
 
 			$item['ItemData'] = serialize($item_data);
 		}
 
 		/**
 		 * Returns table name according to order temp mode
 		 *
 		 * @param string $prefix
 		 * @return string
 		 */
 		protected function getTable($prefix)
 		{
 			return $this->manager->getTable($prefix);
 		}
 	}
Index: branches/5.2.x/units/products/products_event_handler.php
===================================================================
--- branches/5.2.x/units/products/products_event_handler.php	(revision 16774)
+++ branches/5.2.x/units/products/products_event_handler.php	(revision 16775)
@@ -1,1600 +1,1597 @@
 <?php
 /**
 * @version	$Id$
 * @package	In-Commerce
 * @copyright	Copyright (C) 1997 - 2009 Intechnic. All rights reserved.
 * @license	Commercial License
 * This software is protected by copyright law and international treaties.
 * Unauthorized reproduction or unlicensed usage of the code of this program,
 * or any portion of it may result in severe civil and criminal penalties,
 * and will be prosecuted to the maximum extent possible under the law
 * See http://www.in-portal.org/commercial-license for copyright notices and details.
 */
 
 defined('FULL_PATH') or die('restricted access!');
 
 class ProductsEventHandler extends kCatDBEventHandler {
 
 	/**
 	 * Allows to override standard permission mapping
 	 *
 	 * @return void
 	 * @access protected
 	 * @see kEventHandler::$permMapping
 	 */
 	protected function mapPermissions()
 	{
 		parent::mapPermissions();
 
 		$permissions = Array(
 			// front
 			'OnCancelAction'		=>	Array('self' => true),
 			'OnRateProduct'			=>	Array('self' => true),
 			'OnClearRecent'			=>	Array('self' => true),
 			'OnRecommendProduct'	=>	Array('self' => true),
 			'OnAddToCompare'		=>	Array('self' => true),
 			'OnRemoveFromCompare'	=>	Array('self' => true),
 			'OnCancelCompare'		=>	Array('self' => true),
 
 			// admin
 			'OnQtyAdd'			=>	Array('self' => 'add|edit'),
 			'OnQtyRemove'		=>	Array('self' => 'add|edit'),
 			'OnQtyOrder'		=>	Array('self' => 'add|edit'),
 			'OnQtyReceiveOrder'	=>	Array('self' => 'add|edit'),
 			'OnQtyCancelOrder'	=>	Array('self' => 'add|edit'),
 		);
 
 		$this->permMapping = array_merge($this->permMapping, $permissions);
 	}
 
 	/**
 	 * Define alternative event processing method names
 	 *
 	 * @return void
 	 * @see kEventHandler::$eventMethods
 	 * @access protected
 	 */
 	protected function mapEvents()
 	{
 		parent::mapEvents();	// ensure auto-adding of approve/decine and so on events
 
 		$product_events = Array (
 			'OnQtyAdd'=>'InventoryAction',
 			'OnQtyRemove'=>'InventoryAction',
 			'OnQtyOrder'=>'InventoryAction',
 			'OnQtyReceiveOrder'=>'InventoryAction',
 			'OnQtyCancelOrder'=>'InventoryAction',
 		);
 
 		$this->eventMethods = array_merge($this->eventMethods, $product_events);
 	}
 
 	/**
 	 * Sets default processing data for subscriptions
 	 *
 	 * @param kEvent $event
 	 * @return void
 	 * @access protected
 	 */
 	protected function OnBeforeItemCreate(kEvent $event)
 	{
 		parent::OnBeforeItemCreate($event);
 
 		/** @var kDBItem $object */
 		$object = $event->getObject();
 
 		$product_approve_events = Array (
 			2 => 'p:OnSubscriptionApprove',
 			4 => 'p:OnDownloadableApprove',
 			5 => 'p:OnPackageApprove'
 		);
 
 		$product_type = $object->GetDBField('Type');
 
 		$type_found = in_array($product_type, array_keys($product_approve_events));
 
 		if ( $type_found && !$object->GetDBField('ProcessingData') ) {
 			$processing_data = Array ('ApproveEvent' => $product_approve_events[$product_type]);
 			$object->SetDBField('ProcessingData', serialize($processing_data));
 		}
 	}
 
 	/**
 	 * Process product count manipulations
 	 *
 	 * @param kEvent $event
 	 * @access private
 	 */
 	function InventoryAction($event)
 	{
 		/** @var kDBItem $object */
 		$object = $event->getObject();
 
 		$field_values = $this->getSubmittedFields($event);
 		$object->SetFieldsFromHash($field_values);
 		$event->setEventParam('form_data', $field_values);
 
 		if ($object->GetDBField('InventoryStatus') == 2) {
 			// inventory by options (use first selected combination in grid)
 			$combinations = $this->Application->GetVar('poc_grid');
 			$combination_id = key($combinations);
 		}
 		else {
 			// inventory by product
 			$combination_id = 0;
 		}
 
 		// save id of selected option combination & preselect it in grid
 		$this->Application->SetVar('combination_id', $combination_id);
 
 		$this->ScheduleInventoryAction($event->Name, $object->GetId(), $object->GetDBField('Qty'), $combination_id);
 
 		$object->Validate();
 
 		if ( !$object->GetErrorPseudo('Qty') ){
 			// only update, when no error on that field
 			$this->modifyInventory($event->Name, $object, $object->GetDBField('Qty'), $combination_id);
 		}
 
 		$object->SetDBField('Qty', null);
 		$event->redirect = false;
 	}
 
 	/**
 	 * Perform inventory action on supplied object
 	 *
 	 * @param string $action event name which is actually called by user
 	 * @param ProductsItem $product
 	 * @param int $qty
 	 * @param int $combination_id
 	 */
 	function modifyInventory($action, &$product, $qty, $combination_id)
 	{
 		if ($product->GetDBField('InventoryStatus') == 2) {
 			// save inventory changes to option combination instead of product
 			$object = $this->Application->recallObject('poc.-item', null, Array('skip_autoload' => true));
 			$object->Load($combination_id);
 		}
 		elseif ($combination_id > 0) {
 			// combination id present, but not inventory by combinations => skip
 			return false;
 		}
 		elseif ($product->GetDBField('InventoryStatus') == 1) {
 			// save inventory changes to product
 			$object =& $product;
 		}
 		else {
 			// product has inventory actions, but don't use inventory => skip
 			return false;
 		}
 
 		if (!$object->isLoaded()) {
 			// product/combination in action doesn't exist in database by now
 			return false;
 		}
 
 		switch ($action) {
 			case 'OnQtyAdd':
 				$object->SetDBField('QtyInStock', $object->GetDBField('QtyInStock') + $qty);
 				break;
 
 			case 'OnQtyRemove':
 				if ($object->GetDBField('QtyInStock') < $qty) {
 					$qty = $object->GetDBField('QtyInStock');
 				}
 				$object->SetDBField('QtyInStock', $object->GetDBField('QtyInStock') - $qty);
 				break;
 
 			case 'OnQtyOrder':
 				$object->SetDBField('QtyOnOrder', $object->GetDBField('QtyOnOrder') + $qty);
 				break;
 
 			case 'OnQtyReceiveOrder':
 				$object->SetDBField('QtyOnOrder', $object->GetDBField('QtyOnOrder') - $qty);
 				$object->SetDBField('QtyInStock', $object->GetDBField('QtyInStock') + $qty);
 				break;
 
 			case 'OnQtyCancelOrder':
 				$object->SetDBField('QtyOnOrder', $object->GetDBField('QtyOnOrder') - $qty);
 				break;
 		}
 
 		return $object->Update();
 	}
 
 	function ScheduleInventoryAction($action, $prod_id, $qty, $combination_id = 0)
 	{
 		$inv_actions = $this->Application->RecallVar('inventory_actions');
 		if (!$inv_actions) {
 			$inv_actions = Array();
 		}
 		else {
 			$inv_actions = unserialize($inv_actions);
 		}
 
 		array_push($inv_actions, Array('action' => $action, 'product_id' => $prod_id, 'combination_id' => $combination_id, 'qty' => $qty));
 
 		$this->Application->StoreVar('inventory_actions', serialize($inv_actions));
 	}
 
 	function RealInventoryAction($action, $prod_id, $qty, $combination_id)
 	{
 		$product = $this->Application->recallObject('p.liveitem', null, Array('skip_autoload' => true));
 		$product->SwitchToLive();
 		$product->Load($prod_id);
 
 		$this->modifyInventory($action, $product, $qty, $combination_id);
 	}
 
 	function RunScheduledInventoryActions($event)
 	{
 		$inv_actions = $this->Application->GetVar('inventory_actions');
 		if (!$inv_actions) {
 			return;
 		}
 		$inv_actions = unserialize($inv_actions);
 
 		$products = array();
 		foreach($inv_actions as $an_action) {
 			$this->RealInventoryAction($an_action['action'], $an_action['product_id'], $an_action['qty'], $an_action['combination_id']);
 			array_push($products, $an_action['product_id'].'_'.$an_action['combination_id']);
 		}
 
 		$products = array_unique($products);
 		if ($products) {
 			$product_obj = $this->Application->recallObject('p.liveitem', null, Array('skip_autoload' => true));
 			$product_obj->SwitchToLive();
 			foreach ($products as $product_key) {
 				list($prod_id, $combination_id) = explode('_', $product_key);
 			$product_obj->Load($prod_id);
 				$this->FullfillBackOrders($product_obj, $combination_id);
 			}
 		}
 	}
 
 	/**
 	 * In case if products arrived into inventory and they are required by old (non processed) orders, then use them (products) in that orders
 	 *
 	 * @param ProductsItem $product
 	 * @param int $combination_id
 	 */
 	function FullfillBackOrders(&$product, $combination_id)
 	{
 		if ( !$this->Application->ConfigValue('Comm_Process_Backorders_Auto') ) return;
 
 		if ($combination_id && ($product->GetDBField('InventoryStatus') == 2)) {
 			// if combination id present and inventory by combinations
 			$poc_idfield = $this->Application->getUnitOption('poc', 'IDField');
 			$poc_tablename = $this->Application->getUnitOption('poc', 'TableName');
 			$sql = 'SELECT QtyInStock
 					FROM '.$poc_tablename.'
 					WHERE '.$poc_idfield.' = '.$combination_id;
 			$stock_qty = $this->Conn->GetOne($sql);
 		}
 		else {
 			// inventory by product
 			$stock_qty = $product->GetDBField('QtyInStock');
 		}
 
 		$qty = (int) $stock_qty - $product->GetDBField('QtyInStockMin');
 		$prod_id = $product->GetID();
 		if ($prod_id <= 0 || !$prod_id || $qty <= 0) return;
 
 		//selecting up to $qty backorders with $prod_id where full qty is not reserved
 		$query = 'SELECT '.TABLE_PREFIX.'Orders.OrderId
 							FROM '.TABLE_PREFIX.'OrderItems
 					LEFT JOIN '.TABLE_PREFIX.'Orders ON '.TABLE_PREFIX.'Orders.OrderId = '.TABLE_PREFIX.'OrderItems.OrderId
 					WHERE (ProductId = '.$prod_id.') AND (Quantity > QuantityReserved) AND (Status = '.ORDER_STATUS_BACKORDERS.')
 							GROUP BY '.TABLE_PREFIX.'Orders.OrderId
 							ORDER BY OrderDate ASC
 							LIMIT 0,'.$qty; //assuming 1 item per order - minimum possible
 
 		$orders = $this->Conn->GetCol($query);
 
 		if ( !$orders ) {
 			return;
 		}
 
 		/** @var OrdersItem $order */
 		$order = $this->Application->recallObject('ord.-inv', null, array('skip_autoload' => true));
 
 		foreach ($orders as $ord_id) {
 			$order->Load($ord_id);
 
 			$this->Application->emailAdmin('BACKORDER.FULLFILL');
 
 			// Reserve what's possible in any case.
 			$reserve_event = new kEvent('ord:OnReserveItems');
 			$this->Application->HandleEvent($reserve_event);
 
 			// In case the order is ready to process - process it.
 			if ( $reserve_event->status == kEvent::erSUCCESS ) {
 				$this->Application->HandleEvent(new kEvent('ord:OnOrderProcess'));
 			}
 		}
 	}
 
 	/**
 	 * Occurs before an item is deleted from live table when copying from temp
 	 * (temp handler deleted all items from live and then copy over all items from temp)
 	 * Id of item being deleted is passed as event' 'id' param
 	 *
 	 * @param kEvent $event
 	 * @return void
 	 * @access protected
 	 */
 	protected function OnBeforeDeleteFromLive(kEvent $event)
 	{
 		parent::OnBeforeDeleteFromLive($event);
 
 		/** @var kCatDBItem $product */
 		$product = $this->Application->recallObject($event->Prefix . '.itemlive', null, Array ('skip_autoload' => true));
 
 		$product->SwitchToLive();
 		$id = $event->getEventParam('id');
 
 		if ( !$product->Load($id) ) {
 			// this will make sure New product will not be overwritten with empty data
 			return ;
 		}
 
 		/** @var kCatDBItem $temp */
 		$temp = $this->Application->recallObject($event->Prefix . '.itemtemp', null, Array ('skip_autoload' => true));
 
 		$temp->SwitchToTemp();
 		$temp->Load($id);
 
 		$temp->SetDBFieldsFromHash($product->GetFieldValues(), Array ('QtyInStock', 'QtyReserved', 'QtyBackOrdered', 'QtyOnOrder'));
 		$temp->Update();
 	}
 
 	/**
 	 * Removes any information about current/selected ids
 	 * from Application variables and Session
 	 *
 	 * @param kEvent $event
 	 * @return void
 	 * @access protected
 	 */
 	protected function clearSelectedIDs(kEvent $event)
 	{
 		parent::clearSelectedIDs($event);
 
 		$this->Application->SetVar('inventory_actions', $this->Application->RecallVar('inventory_actions'));
 		$this->Application->RemoveVar('inventory_actions');
 	}
 
 	/**
 	 * Saves content of temp table into live and
 	 * redirects to event' default redirect (normally grid template)
 	 *
 	 * @param kEvent $event
 	 * @return void
 	 * @access protected
 	 */
 	protected function OnSave(kEvent $event)
 	{
 		parent::OnSave($event);
 
 		if ( $event->status == kEvent::erSUCCESS ) {
 			$this->RunScheduledInventoryActions($event);
 		}
 	}
 
 	/**
 	 * Prepare temp tables for creating new item
 	 * but does not create it. Actual create is
 	 * done in OnPreSaveCreated
 	 *
 	 * @param kEvent $event
 	 * @return void
 	 * @access protected
 	 */
 	protected function OnPreCreate(kEvent $event)
 	{
 		parent::onPreCreate($event);
 
 		/** @var kDBItem $object */
 		$object = $event->getObject();
 
 		$object->SetDBField('Type', $this->Application->GetVar($event->getPrefixSpecial(true) . '_new_type'));
 	}
 
 	/**
 	 * Saves edited item in temp table and loads
 	 * item with passed id in current template
 	 * Used in Prev/Next buttons
 	 *
 	 * @param kEvent $event
 	 * @return void
 	 * @access protected
 	 */
 	protected function OnPreSaveAndGo(kEvent $event)
 	{
 		$event->CallSubEvent('OnPreSave');
 		$this->LoadItem($event);
 
 		/** @var kDBItem $object */
 		$object = $event->getObject();
 
 		$from_type = $object->GetDBField('Type');
 		if ( $event->status == kEvent::erSUCCESS ) {
 			$this->Application->SetVar($event->getPrefixSpecial() . '_id', $this->Application->GetVar($event->getPrefixSpecial(true) . '_GoId'));
 			$this->LoadItem($event);
 			$to_type = $object->GetDBField('Type');
 
 			if ( $from_type != $to_type ) {
 				$from_tabs = $this->GetTabs($from_type);
 				$from_tab_i = array_search($this->Application->GetVar('t'), $from_tabs);
 
 				$to_tabs = $this->GetTabs($to_type);
 				$to_tab = $this->Application->GetVar('t');
 
 				$found = false;
 				while (!isset($to_tabs[$from_tab_i]) && $from_tab_i < count($to_tabs)) {
 					$from_tab_i++;
 				}
 
 				if ( !isset($to_tabs[$from_tab_i]) ) {
 					$from_tab_i = 0;
 				}
 
 				$to_tab = $to_tabs[$from_tab_i];
 
 				$event->redirect = $to_tab;
 			}
 		}
 	}
 
 	function GetTabs($type)
 	{
 		switch($type)
 		{
 			case 1:
 				return Array(
 					0 => 'in-commerce/products/products_edit',
 					1 => 'in-commerce/products/products_inventory',
 					2 => 'in-commerce/products/products_pricing',
 					3 => 'in-commerce/products/products_categories',
 					4 => 'in-commerce/products/products_images',
 					5 => 'in-commerce/products/products_reviews',
 					6 => 'in-commerce/products/products_custom',
 				);
 
 			case 2:
 				return Array(
 					0 => 'in-commerce/products/products_edit',
 					1 => 'in-commerce/products/products_access',
 					/*2 => 'in-commerce/products/products_access_pricing',*/
 					3 => 'in-commerce/products/products_categories',
 					4 => 'in-commerce/products/products_images',
 					5 => 'in-commerce/products/products_reviews',
 					6 => 'in-commerce/products/products_custom',
 				);
 
 			case 3:
 				return Array(
 					0 => 'in-commerce/products/products_edit',
 
 					2 => 'in-commerce/products/products_access_pricing',
 					3 => 'in-commerce/products/products_categories',
 					4 => 'in-commerce/products/products_images',
 					5 => 'in-commerce/products/products_reviews',
 					6 => 'in-commerce/products/products_custom',
 				);
 
 			case 4:
 				return Array(
 					0 => 'in-commerce/products/products_edit',
 
 					2 => 'in-commerce/products/products_files',
 					3 => 'in-commerce/products/products_categories',
 					4 => 'in-commerce/products/products_images',
 					5 => 'in-commerce/products/products_reviews',
 					6 => 'in-commerce/products/products_custom',
 				);
 		}
 	}
 
 	/**
 	 * Return type clauses for list bulding on front
 	 *
 	 * @param kEvent $event
 	 * @return Array
 	 */
 	function getTypeClauses($event)
 	{
 		$types = $event->getEventParam('types');
 		$types = $types ? explode(',', $types) : Array ();
 
 		$except_types = $event->getEventParam('except');
 		$except_types = $except_types ? explode(',', $except_types) : Array ();
 
 		/** @var kDBList $object */
 		$object = $event->getObject();
 
 		$type_clauses = parent::getTypeClauses($event);
 
 		$type_clauses['featured']['include'] = '%1$s.Featured = 1 AND ' . TABLE_PREFIX . 'CategoryItems.PrimaryCat = 1';
 		$type_clauses['featured']['except'] = '%1$s.Featured != 1 AND ' . TABLE_PREFIX . 'CategoryItems.PrimaryCat = 1';
 		$type_clauses['featured']['having_filter'] = false;
 
 		$type_clauses['onsale']['include'] = '%1$s.OnSale = 1 AND ' . TABLE_PREFIX . 'CategoryItems.PrimaryCat = 1';
 		$type_clauses['onsale']['except'] = '%1$s.OnSale != 1 AND ' . TABLE_PREFIX . 'CategoryItems.PrimaryCat = 1';
 		$type_clauses['onsale']['having_filter'] = false;
 
 		// products from selected manufacturer: begin
 		$manufacturer = $event->getEventParam('manufacturer');
 		if ( !$manufacturer ) {
 			$manufacturer = $this->Application->GetVar('manuf_id');
 		}
 
 		if ( $manufacturer ) {
 			$type_clauses['manufacturer']['include'] = '%1$s.ManufacturerId = ' . $manufacturer . ' AND PrimaryCat = 1';
 			$type_clauses['manufacturer']['except'] = '%1$s.ManufacturerId != ' . $manufacturer . ' AND PrimaryCat = 1';
 			$type_clauses['manufacturer']['having_filter'] = false;
 		}
 		// products from selected manufacturer: end
 
 		// recent products: begin
 		$recent = $this->Application->RecallVar('recent_products');
 		if ( $recent ) {
 			$recent = unserialize($recent);
 			$type_clauses['recent']['include'] = '%1$s.ProductId IN (' . implode(',', $recent) . ') AND PrimaryCat = 1';
 			$type_clauses['recent']['except'] = '%1$s.ProductId NOT IN (' . implode(',', $recent) . ') AND PrimaryCat = 1';
 		}
 		else {
 			$type_clauses['recent']['include'] = '0';
 			$type_clauses['recent']['except'] = '1';
 		}
 		$type_clauses['recent']['having_filter'] = false;
 		// recent products: end
 
 		// compare products: begin
 		if ( in_array('compare', $types) || in_array('compare', $except_types) ) {
 			$compare_products = $this->getCompareProducts();
 
 			if ( $compare_products ) {
 				$compare_products = $this->Conn->qstrArray($compare_products);
 				$type_clauses['compare']['include'] = '%1$s.ProductId IN (' . implode(',', $compare_products) . ') AND PrimaryCat = 1';
 				$type_clauses['compare']['except'] = '%1$s.ProductId NOT IN (' . implode(',', $compare_products) . ') AND PrimaryCat = 1';
 			}
 			else {
 				$type_clauses['compare']['include'] = '0';
 				$type_clauses['compare']['except'] = '1';
 			}
 
 			$type_clauses['compare']['having_filter'] = false;
 
 			if ( $event->getEventParam('per_page') === false ) {
 				$event->setEventParam('per_page', $this->Application->ConfigValue('MaxCompareProducts'));
 			}
 		}
 		// compare products: end
 
 		// products already in shopping cart: begin
 		if ( in_array('in_cart', $types) || in_array('in_cart', $except_types) ) {
 			$order_id = $this->Application->RecallVar('ord_id');
 
 			if ( $order_id ) {
 				$sql = 'SELECT ProductId
 						FROM ' . TABLE_PREFIX . 'OrderItems
 						WHERE OrderId = ' . $order_id;
 				$in_cart = $this->Conn->GetCol($sql);
 
 				if ( $in_cart ) {
 					$type_clauses['in_cart']['include'] = '%1$s.ProductId IN (' . implode(',', $in_cart) . ') AND PrimaryCat = 1';
 					$type_clauses['in_cart']['except'] = '%1$s.ProductId NOT IN (' . implode(',', $in_cart) . ') AND PrimaryCat = 1';
 				}
 				else {
 					$type_clauses['in_cart']['include'] = '0';
 					$type_clauses['in_cart']['except'] = '1';
 				}
 			}
 			else {
 				$type_clauses['in_cart']['include'] = '0';
 				$type_clauses['in_cart']['except'] = '1';
 			}
 
 			$type_clauses['in_cart']['having_filter'] = false;
 		}
 		// products already in shopping cart: end
 
 		// my downloadable products: begin
 		if ( in_array('my_downloads', $types) || in_array('my_downloads', $except_types) ) {
 			$user_id = $this->Application->RecallVar('user_id');
 
 			$sql = 'SELECT ProductId
 					FROM ' . TABLE_PREFIX . 'UserFileAccess
 					WHERE PortalUserId = ' . $user_id;
 			$my_downloads = $user_id > 0 ? $this->Conn->GetCol($sql) : false;
 
 			if ( $my_downloads ) {
 				$type_clauses['my_downloads']['include'] = '%1$s.ProductId IN (' . implode(',', $my_downloads) . ') AND PrimaryCat = 1';
 				$type_clauses['my_downloads']['except'] = '%1$s.ProductId NOT IN (' . implode(',', $my_downloads) . ') AND PrimaryCat = 1';
 			}
 			else {
 				$type_clauses['my_downloads']['include'] = '0';
 				$type_clauses['my_downloads']['except'] = '1';
 			}
 
 			$type_clauses['my_downloads']['having_filter'] = false;
 		}
 		// my downloadable products: end
 
 		// my favorite products: begin
 		if ( in_array('wish_list', $types) || in_array('wish_list', $except_types) ) {
 			$sql = 'SELECT ResourceId
 					FROM ' . $this->Application->getUnitOption('fav', 'TableName') . '
 					WHERE PortalUserId = ' . (int)$this->Application->RecallVar('user_id');
 			$wishlist_ids = $this->Conn->GetCol($sql);
 
 			if ( $wishlist_ids ) {
 				$type_clauses['wish_list']['include'] = '%1$s.ResourceId IN (' . implode(',', $wishlist_ids) . ') AND PrimaryCat = 1';
 				$type_clauses['wish_list']['except'] = '%1$s.ResourceId NOT IN (' . implode(',', $wishlist_ids) . ') AND PrimaryCat = 1';
 			}
 			else {
 				$type_clauses['wish_list']['include'] = '0';
 				$type_clauses['wish_list']['except'] = '1';
 			}
 
 			$type_clauses['wish_list']['having_filter'] = false;
 		}
 		// my favorite products: end
 
 		// products from package: begin
 		if ( in_array('content', $types) || in_array('content', $except_types) ) {
 			$object->removeFilter('category_filter');
 			$object->AddGroupByField('%1$s.ProductId');
 
 			/** @var ProductsItem $object_product */
 			$object_product = $this->Application->recallObject($event->Prefix);
 
 			$content_ids_array = $object_product->GetPackageContentIds();
 
 			if ( sizeof($content_ids_array) == 0 ) {
 				$content_ids_array = array ('-1');
 			}
 
 			if ( sizeof($content_ids_array) > 0 ) {
 				$type_clauses['content']['include'] = '%1$s.ProductId IN (' . implode(',', $content_ids_array) . ')';
 			}
 			else {
 				$type_clauses['content']['include'] = '0';
 			}
 
 			$type_clauses['related']['having_filter'] = false;
 		}
 		// products from package: end
 
 		$object->addFilter('not_virtual', '%1$s.Virtual = 0');
 
 		if ( !$this->Application->isAdminUser ) {
 			$object->addFilter('expire_filter', '%1$s.Expire IS NULL OR %1$s.Expire > ' . adodb_mktime());
 		}
 
 		return $type_clauses;
 	}
 
 	function OnClearRecent($event)
 	{
 		$this->Application->RemoveVar('recent_products');
 	}
 
 	/**
 	 * Occurs, when user rates a product
 	 *
 	 * @param kEvent $event
 	 */
 	function OnRateProduct($event)
 	{
 		$event->SetRedirectParam('pass', 'all,p');
 		$event->redirect = $this->Application->GetVar('success_template');
 
 		/** @var kDBItem $object */
 		$object = $event->getObject();
 
 		$user_id = $this->Application->RecallVar('user_id');
 
 		$sql = '	SELECT * FROM ' . TABLE_PREFIX . 'SpamControl
 					WHERE ItemResourceId=' . $object->GetDBField('ResourceId') . '
 					AND IPaddress="' . $this->Application->getClientIp() . '"
 					AND PortalUserId=' . $user_id . '
 					AND DataType="Rating"';
 		$res = $this->Conn->GetRow($sql);
 
 		if ( $res && $res['Expire'] < adodb_mktime() ) {
 			$sql = '	DELETE FROM ' . TABLE_PREFIX . 'SpamControl
 						WHERE ItemResourceId=' . $object->GetDBField('ResourceId') . '
 						AND IPaddress="' . $this->Application->getClientIp() . '"
 						AND PortalUserId=' . $user_id . '
 						AND DataType="Rating"';
 			$this->Conn->Query($sql);
 			unset($res);
 		}
 
 		$new_rating = $this->Application->GetVar('rating');
 
 		if ( $new_rating !== false && !$res ) {
 			$rating = $object->GetDBField('CachedRating');
 			$votes = $object->GetDBField('CachedVotesQty');
 			$new_votes = $votes + 1;
 
 			$rating = (($rating * $votes) + $new_rating) / $new_votes;
 			$object->SetDBField('CachedRating', $rating);
 			$object->SetDBField('CachedVotesQty', $new_votes);
 			$object->Update();
 
 			$expire = adodb_mktime() + $this->Application->ConfigValue('product_ReviewDelay_Value') * $this->Application->ConfigValue('product_ReviewDelay_Interval');
 			$sql = '	INSERT INTO ' . TABLE_PREFIX . 'SpamControl
 							(ItemResourceId, IPaddress, PortalUserId, DataType, Expire)
 						VALUES (' . $object->GetDBField('ResourceId') . ',
 								"' . $this->Application->getClientIp() . '",
 								' . $user_id . ',
 								"Rating",
 								' . $expire . ')';
 			$this->Conn->Query($sql);
 		}
 		else {
 			$event->status == kEvent::erFAIL;
 			$event->redirect = false;
 			$object->SetError('CachedRating', 'too_frequent', 'lu_ferror_rate_duplicate');
 		}
 	}
 
 	/**
 	 * Enter description here...
 	 *
 	 * @param kEvent $event
 	 */
 	function OnCancelAction($event)
 	{
 		$event->SetRedirectParam('pass', 'all,p');
 		$event->redirect = $this->Application->GetVar('cancel_template');
 	}
 
 	/**
 	 * Enter description here...
 	 *
 	 * @param kEvent $event
 	 */
 	function OnRecommendProduct($event)
 	{
 		// used for error reporting only -> rewrite code + theme (by Alex)
 		$object = $this->Application->recallObject('u', null, Array('skip_autoload' => true)); // TODO: change theme too
 		/** @var kDBItem $object */
 
 		$friend_email = $this->Application->GetVar('friend_email');
 		$friend_name = $this->Application->GetVar('friend_name');
 		$my_email = $this->Application->GetVar('your_email');
 		$my_name = $this->Application->GetVar('your_name');
 		$my_message = $this->Application->GetVar('your_message');
 
 		$send_params = array();
 		$send_params['to_email']=$friend_email;
 		$send_params['to_name']=$friend_name;
 		$send_params['from_email']=$my_email;
 		$send_params['from_name']=$my_name;
 		$send_params['message']=$my_message;
 
 		if ( preg_match('/' . REGEX_EMAIL_USER . '@' . REGEX_EMAIL_DOMAIN . '/', $friend_email) ) {
 			$user_id = $this->Application->RecallVar('user_id');
 			$email_sent = $this->Application->emailUser('PRODUCT.SUGGEST', $user_id, $send_params);
 			$this->Application->emailAdmin('PRODUCT.SUGGEST');
 
 			if ( $email_sent ) {
 				$event->setRedirectParams(Array ('opener' => 's', 'pass' => 'all'));
 				$event->redirect = $this->Application->GetVar('template_success');
 			}
 			else {
 //				$event->setRedirectParams(Array('opener' => 's', 'pass' => 'all'));
 //				$event->redirect = $this->Application->GetVar('template_fail');
 
 				$object->SetError('Email', 'send_error', 'lu_email_send_error');
 				$event->status = kEvent::erFAIL;
 			}
 		}
 		else {
 			$object->SetError('Email', 'invalid_email', 'lu_InvalidEmail');
 			$event->status = kEvent::erFAIL;
 		}
 	}
 
 	/**
 	 * Creates/updates virtual product based on listing type data
 	 *
 	 * @param kEvent $event
 	 */
 	function OnSaveVirtualProduct($event)
 	{
 		$object = $event->getObject( Array('skip_autoload' => true) );
 		$listing_type = $this->Application->recallObject('lst', null, Array('skip_autoload' => true));
 		$listing_type->Load($event->MasterEvent->getEventParam('id'));
 
 		$product_id = $listing_type->GetDBField('VirtualProductId');
 
 		if ($product_id) {
 			$object->Load($product_id);
 		}
 
 		if (!$listing_type->GetDBField('EnableBuying')) {
 			if ($product_id) {
 				// delete virtual product here
 				$temp_handler = $this->Application->recallObject($event->getPrefixSpecial().'_TempHandler', 'kTempTablesHandler');
 				$temp_handler->DeleteItems($event->Prefix, $event->Special, Array($product_id));
 
 				$listing_type->SetDBField('VirtualProductId', 0);
 				$listing_type->Update();
 			}
 			return true;
 		}
 
 		$ml_formatter = $this->Application->recallObject('kMultiLanguage');
 		$object->SetDBField($ml_formatter->LangFieldName('Name'), $listing_type->GetDBField('ShopCartName') );
 		$object->SetDBField($ml_formatter->LangFieldName('Description'), $listing_type->GetDBField('Description'));
 		$object->SetDBField('SKU', 'ENHANCE_LINK_'.abs( crc32( $listing_type->GetDBField('Name') ) ) );
 
 		if ($product_id) {
 			$object->Update();
 		}
 		else {
 			$object->SetDBField('Type', 2);
 			$object->SetDBField('Status', 1);
 			$object->SetDBField('HotItem', 0);
 			$object->SetDBField('PopItem', 0);
 			$object->SetDBField('NewItem', 0);
 			$object->SetDBField('Virtual', 1);
 
 //			$processing_data = Array('ApproveEvent' => 'ls:EnhanceLinkAfterOrderApprove', 'ExpireEvent' => 'ls:ExpireLink');
 			$processing_data = Array(	'ApproveEvent'			=>	'ls:EnhanceLinkAfterOrderApprove',
 										'DenyEvent'				=>	'ls:EnhanceLinkAfterOrderDeny',
 										'CompleteOrderEvent'	=>	'ls:EnhancedLinkOnCompleteOrder',
 										'ExpireEvent'			=>	'ls:ExpireLink',
 										'HasNewProcessing'		=>	1);
 			$object->SetDBField('ProcessingData', serialize($processing_data));
 			$object->Create();
 
 			$listing_type->SetDBField('VirtualProductId', $object->GetID());
 			$listing_type->Update();
 		}
 
 		$additiona_fields = Array(	'AccessDuration'	=>	$listing_type->GetDBField('Duration'),
 									'AccessUnit'		=>	$listing_type->GetDBField('DurationType'),
 							);
 		$this->setPrimaryPrice($object->GetID(), (double)$listing_type->GetDBField('Price'), $additiona_fields);
 	}
 
 	/**
 	 * [HOOK] Deletes virtual product when listing type is deleted
 	 *
 	 * @param kEvent $event
 	 */
 	function OnDeleteListingType($event)
 	{
 		/** @var kDBItem $listing_type */
 		$listing_type = $event->MasterEvent->getObject();
 
 		$product_id = $listing_type->GetDBField('VirtualProductId');
 
 		if ( $product_id ) {
 			$temp_handler = $this->Application->recallObject($event->getPrefixSpecial() . '_TempHandler', 'kTempTablesHandler');
 			$temp_handler->DeleteItems($event->Prefix, $event->Special, Array ($product_id));
 		}
 	}
 
 	/**
 	 * Extends user membership in group when his order is approved
 	 *
 	 * @param kEvent $event
 	 */
 	function OnSubscriptionApprove($event)
 	{
 		$field_values = $event->getEventParam('field_values');
 		$item_data = unserialize($field_values['ItemData']);
 
 		if ( !getArrayValue($item_data,'PortalGroupId') ) {
 			// is subscription product, but no group defined in it's properties
 			trigger_error('Invalid product <b>'.$field_values['ProductName'].'</b> (id: '.$field_values['ProductId'].')', E_USER_WARNING);
 			return false;
 		}
 
 		$sql = 'SELECT PortalUserId
 				FROM ' . $this->Application->getUnitOption('ord', 'TableName') . '
 				WHERE ' . $this->Application->getUnitOption('ord', 'IDField') . ' = ' . $field_values['OrderId'];
 		$user_id = $this->Conn->GetOne($sql);
 
 		$group_id = $item_data['PortalGroupId'];
 		$duration = $item_data['Duration'];
 
 		$sql = 'SELECT *
 				FROM ' . TABLE_PREFIX . 'UserGroupRelations
 				WHERE PortalUserId = ' . $user_id;
 		$user_groups = $this->Conn->Query($sql, 'GroupId');
 
 		if ( !isset($user_groups[$group_id]) ) {
 			$expire = adodb_mktime() + $duration;
 		}
 		else {
 			$expire = $user_groups[$group_id]['MembershipExpires'];
 			$expire = $expire < adodb_mktime() ? adodb_mktime() + $duration : $expire + $duration;
 		}
 
 		/*// Customization healtheconomics.org
 		if ($item_data['DurationType'] == 2) {
 			$expire = $item_data['AccessExpiration'];
 		}
 		// Customization healtheconomics.org --*/
 
 		$fields_hash = Array (
 			'PortalUserId' => $user_id,
 			'GroupId' => $group_id,
 			'MembershipExpires' => $expire,
 		);
 
 		$this->Conn->doInsert($fields_hash, TABLE_PREFIX . 'UserGroupRelations', 'REPLACE');
 
 		$sub_order = $this->Application->recallObject('ord.-sub'.$event->getEventParam('next_sub_number'), 'ord');
 		$sub_order->SetDBField('IsRecurringBilling', getArrayValue($item_data, 'IsRecurringBilling') ? 1 : 0);
 		$sub_order->SetDBField('GroupId', $group_id);
 		$sub_order->SetDBField('NextCharge_date', $expire);
 		$sub_order->SetDBField('NextCharge_time', $expire);
 	}
 
 	function OnDownloadableApprove($event)
 	{
 		$field_values = $event->getEventParam('field_values');
 		$product_id = $field_values['ProductId'];
 		$sql = 'SELECT PortalUserId FROM '.$this->Application->getUnitOption('ord', 'TableName').'
 				WHERE OrderId = '.$field_values['OrderId'];
 		$user_id = $this->Conn->GetOne($sql);
 		$sql = 'INSERT INTO '.TABLE_PREFIX.'UserFileAccess VALUES("", '.$product_id.', '.$user_id.')';
 		$this->Conn->Query($sql);
 	}
 
 	protected function OnPackageApprove(kEvent $event)
 	{
 		$field_values = $event->getEventParam('field_values');
 		$item_data = unserialize($field_values['ItemData']);
 		$package_content_ids = $item_data['PackageContent'];
 
 		/** @var ProductsItem $object_item */
 		$object_item = $this->Application->recallObject('p.packageitem', null, array ('skip_autoload' => true));
 
 		foreach ($package_content_ids as $package_item_id) {
 			$object_field_values = array ();
 
 			// query processing data from product and run approve event
 			$sql = 'SELECT ProcessingData
 					FROM ' . TABLE_PREFIX . 'Products
 					WHERE ProductId = ' . $package_item_id;
 			$processing_data = $this->Conn->GetOne($sql);
 
 			if ( $processing_data ) {
 				$processing_data = unserialize($processing_data);
 				$approve_event = new kEvent($processing_data['ApproveEvent']);
 
 				//$order_item_fields = $this->Conn->GetRow('SELECT * FROM '.TABLE_PREFIX.'OrderItems WHERE OrderItemId = '.$grouping_data[1]);
 				$object_item->Load($package_item_id);
 
 				$object_field_values['OrderId'] = $field_values['OrderId'];
 				$object_field_values['ProductId'] = $package_item_id;
 
 				$object_field_values['ItemData'] = serialize($item_data['PackageItemsItemData'][$package_item_id]);
 
 				$approve_event->setEventParam('field_values', $object_field_values);
 				$this->Application->HandleEvent($approve_event);
 			}
 		}
 	}
 
 	/**
 	 * Saves edited item into temp table
 	 * If there is no id, new item is created in temp table
 	 *
 	 * @param kEvent $event
 	 * @return void
 	 * @access protected
 	 */
 	protected function OnPreSave(kEvent $event)
 	{
 		$this->CheckRequiredOptions($event);
 
 		parent::OnPreSave($event);
 	}
 
 	/**
 	 * Set new price to ProductsPricing
 	 *
 	 * @param kEvent $event
 	 * @return void
 	 * @access protected
 	 */
 	protected function OnAfterItemCreate(kEvent $event)
 	{
 		parent::OnAfterItemCreate($event);
 
 		$this->_updateProductPrice($event);
 	}
 
 	/**
 	 * Set new price to ProductsPricing
 	 *
 	 * @param kEvent $event
 	 * @return void
 	 * @access protected
 	 */
 	protected function OnAfterItemUpdate(kEvent $event)
 	{
 		parent::OnAfterItemUpdate($event);
 
 		$this->_updateProductPrice($event);
 	}
 
 	/**
 	 * Updates product's primary price based on Price virtual field value
 	 *
 	 * @param kEvent $event
 	 */
 	function _updateProductPrice($event)
 	{
 		/** @var kDBItem $object */
 		$object = $event->getObject();
 
 		$price = $object->GetDBField('Price');
 
 		// always create primary pricing, to show on Pricing tab (in admin) for tangible products
 		$force_create = ($object->GetDBField('Type') == PRODUCT_TYPE_TANGIBLE) && is_null($price);
 
 		if ($force_create || ($price != $object->GetOriginalField('Price'))) {
 			// new product OR price was changed in virtual field
 			$this->setPrimaryPrice($object->GetID(), (float)$price);
 		}
 	}
 
 	function CheckRequiredOptions($event)
 	{
 		$object = $event->getObject();
 		if ($object->GetDBField('ProductId') == '') return ; // if product does not have ID - it's not yet created
 		$opt_object = $this->Application->recallObject('po', null, Array('skip_autoload' => true) );
 		$has_required = $this->Conn->GetOne('SELECT COUNT(*) FROM '.$opt_object->TableName.' WHERE Required = 1 AND ProductId = '.$object->GetDBField('ProductId'));
 		//we need to imitate data sumbit, as parent' PreSave sets object values from $items_info
 		$items_info = $this->Application->GetVar( $event->getPrefixSpecial(true) );
 		$items_info[$object->GetDBField('ProductId')]['HasRequiredOptions'] = $has_required ? '1' : '0';
 		$this->Application->SetVar($event->getPrefixSpecial(true), $items_info);
 		$object->SetDBField('HasRequiredOptions', $has_required ? 1 : 0);
 	}
 
 	/**
 	 * Sets required price in primary price backed, if it's missing, then create it
 	 *
 	 * @param int $product_id
 	 * @param double $price
 	 * @param Array $additional_fields
 	 * @return bool
 	 */
 	function setPrimaryPrice($product_id, $price, $additional_fields = Array())
 	{
 		/** @var kDBItem $pr_object */
 		$pr_object = $this->Application->recallObject('pr.-item', null, Array('skip_autoload' => true) );
 
 		$pr_object->Load( Array('ProductId' => $product_id, 'IsPrimary' => 1) );
 
 		$sql = 'SELECT COUNT(*) FROM '.$pr_object->TableName.' WHERE ProductId = '.$product_id;
 		$has_pricings = $this->Conn->GetOne($sql);
 
 		if ($additional_fields) {
 			$pr_object->SetDBFieldsFromHash($additional_fields);
 		}
 
 		if( ($price === false) && $has_pricings ) return false;
 
 		if( $pr_object->isLoaded() )
 		{
 			$pr_object->SetField('Price', $price);
 			return $pr_object->Update();
 		}
 		else
 		{
 			$group_id = $this->Application->ConfigValue('User_LoggedInGroup');
 			$field_values = Array('ProductId' => $product_id, 'IsPrimary' => 1, 'MinQty' => 1, 'MaxQty' => -1, 'GroupId'=>$group_id);
 			$pr_object->SetDBFieldsFromHash($field_values);
 			$pr_object->SetField('Price', $price);
 
 			return $pr_object->Create();
 		}
 	}
 
 	/**
 	 * Occurs after deleting item, id of deleted item
 	 * is stored as 'id' param of event
 	 *
 	 * @param kEvent $event
 	 * @return void
 	 * @access protected
 	 */
 	protected function OnAfterItemDelete(kEvent $event)
 	{
 		parent::OnAfterItemDelete($event);
 
 		$product_id = $event->getEventParam('id');
 		if ( !$product_id ) {
 			return;
 		}
 
 		$sql = 'DELETE FROM ' . TABLE_PREFIX . 'UserFileAccess
 				WHERE ProductId = ' . $product_id;
 		$this->Conn->Query($sql);
 	}
 
 	/**
 	 * Load price from temp table if product mode is temp table
 	 *
 	 * @param kEvent $event
 	 */
 
 	/**
 	 * Load price from temp table if product mode is temp table
 	 *
 	 * @param kEvent $event
 	 * @return void
 	 * @access protected
 	 */
 	protected function OnAfterItemLoad(kEvent $event)
 	{
 		parent::OnAfterItemLoad($event);
 
 		/** @var ProductsItem $object */
 		$object = $event->getObject();
 
 		$a_pricing = $object->getPrimaryPricing();
 		if ( !$a_pricing ) {
 			// pricing doesn't exist for new products
 			$price = $cost = null;
 		}
 		else {
 			$price = (float)$a_pricing['Price'];
 			$cost = (float)$a_pricing['Cost'];
 		}
 
 		// set original fields to use them in OnAfterItemCreate/OnAfterItemUpdate later
 		$object->SetDBField('Price', $price);
 		$object->SetOriginalField('Price', $price);
 
 		$object->SetDBField('Cost', $cost);
 		$object->SetOriginalField('Cost', $cost);
 	}
 
 	/**
 	 * Allows to add products to package besides all that parent method does
 	 *
 	 * @param kEvent $event
 	 */
 	function OnProcessSelected($event)
 	{
 		$dst_field = $this->Application->RecallVar('dst_field');
 
 		if ($dst_field == 'PackageContent') {
 			$this->OnAddToPackage($event);
 		}
 		elseif ($dst_field == 'AssignedCoupon') {
 			$coupon_id = $this->Application->GetVar('selected_ids');
 			$object = $event->getObject();
 			$object->SetDBField('AssignedCoupon', $coupon_id);
 			$this->RemoveRequiredFields($object);
 			$object->Update();
 		}
 		else {
 			parent::OnProcessSelected($event);
 		}
 		$this->finalizePopup($event);
 	}
 
 	/**
 	 * Called when some products are selected in products selector for this prefix
 	 *
 	 * @param kEvent $event
 	 */
 	function OnAddToPackage($event)
 	{
 		$selected_ids = $this->Application->GetVar('selected_ids');
 
 		// update current package content with selected products
 
 		/** @var ProductsItem $object */
 		$object = $event->getObject();
 
 		$product_ids = $selected_ids['p'] ? explode(',', $selected_ids['p']) : Array();
 
 		if ($product_ids) {
 			$current_ids = $object->GetPackageContentIds();
 			$current_ids = array_unique(array_merge($current_ids, $product_ids));
 
 			// remove package product from selected list
 			$this_product = array_search($object->GetID(), $current_ids);
 			if ($this_product !== false) {
 				unset($current_ids[$this_product]);
 			}
 
 			$dst_field = $this->Application->RecallVar('dst_field');
 			$object->SetDBField($dst_field, '|'.implode('|', $current_ids).'|');
 
 			$object->Update();
 			$this->ProcessPackageItems($event);
 		}
 
 		$this->finalizePopup($event);
 	}
 
 
 	function ProcessPackageItems(kEvent $event)
 	{
 		//$this->Application->SetVar('p_mode', 't');
 
 		/** @var ProductsItem $object */
 		$object = $event->getObject();
 
 		$content_ids = $object->GetPackageContentIds();
 
 		if (sizeof($content_ids) > 0) {
 			$total_weight = $this->Conn->GetOne('SELECT SUM(Weight) FROM '.TABLE_PREFIX.'Products WHERE ProductId IN ('.implode(', ', $content_ids).') AND Type=1');
 
 			if (!$total_weight) $total_weight = 0;
 
 			$this->Conn->Query('UPDATE '.$object->TableName.' SET Weight='.$total_weight.' WHERE ProductId='.$object->GetID());
 		}
 
 		/*
 		$this->Application->SetVar('p_mode', false);
 
 		$list = $this->Application->recallObject('p.content', 'p_List', array('types'=>'content'));
 
 		$this->Application->SetVar('p_mode', 't');
 
 		$list->Query();
 
 		$total_weight_a = 0;
 		$total_weight_b = 0;
 
 		$list->GoFirst();
 
 		while (!$list->EOL())
 		{
 			if ($list->GetDBField('Type')==1){
 				$total_weight_a += $list->GetField('Weight_a');
 				$total_weight_b += $list->GetField('Weight_b');
 			}
 			$list->GoNext();
 		}
 
 		$object->SetField('Weight_a', $total_weight_a);
 		$object->SetField('Weight_b', $total_weight_b);
 		*/
 		//$object->Update();
 
 
 	}
 
 	/**
 	 * Enter description here...
 	 *
 	 * @param kEvent $event
 	 */
 
 	function OnSaveItems($event)
 	{
 		//$event->CallSubEvent('OnUpdate');
 		$event->redirect = false;
 		//$event->setRedirectParams(Array ('opener' => 's', 'pass' => 'all,p'));
 	}
 
 	/**
 	 * Removes product from package
 	 *
 	 * @param kEvent $event
 	 */
 	function OnRemovePackageItem($event) {
 
 		$this->Application->SetVar('p_mode', 't');
 
 		$object = $event->getObject();
 
 		$items_info = $this->Application->GetVar('p_content');
 
 		if($items_info)
 		{
 			$product_ids = array_keys($items_info);
 
 			$current_ids = $object->GetPackageContentIds();
 
 			$current_ids_flip = array_flip($current_ids);
 			foreach($product_ids as $key=>$val){
 				unset($current_ids_flip[$val]);
 			}
 			$current_ids = array_keys($current_ids_flip);
 			$current_ids_str = '|'.implode('|', array_unique($current_ids)).'|';
 			$object->SetDBField('PackageContent', $current_ids_str);
 		}
 
 		$object->Update();
 		$this->ProcessPackageItems($event);
 	}
 
 	/**
 	 * Occurs before deleting item, id of item being
 	 * deleted is stored as 'id' event param
 	 *
 	 * @param kEvent $event
 	 * @return void
 	 * @access protected
 	 */
 	protected function OnBeforeItemDelete(kEvent $event)
 	{
 		parent::OnBeforeItemDelete($event);
 
 		/** @var kDBItem $object */
 		$object = $event->getObject();
 
 		$sql = 'SELECT COUNT(*)
 				FROM ' . TABLE_PREFIX . 'Products
 				WHERE PackageContent LIKE "%|' . $object->GetID() . '%"';
 		$product_includes_in = $this->Conn->GetOne($sql);
 
 		if ( $product_includes_in > 0 ) {
 			$event->status = kEvent::erFAIL;
 		}
 	}
 
 	/**
 	 * Returns specific to each item type columns only
 	 *
 	 * @param kEvent $event
 	 * @return Array
 	 * @access protected
 	 */
 	public function getCustomExportColumns(kEvent $event)
 	{
 		$columns = parent::getCustomExportColumns($event);
 
 		$new_columns = Array (
 			'__VIRTUAL__Price' => 'Price',
 			'__VIRTUAL__Cost' => 'Cost',
 		);
 
 		return array_merge($columns, $new_columns);
 	}
 
 /**
 	 * Sets non standart virtual fields (e.g. to other tables)
 	 *
 	 * @param kEvent $event
 	 */
 	function setCustomExportColumns($event)
 	{
 		parent::setCustomExportColumns($event);
 
 		/** @var kDBItem $object */
 		$object = $event->getObject();
 
 		$this->setPrimaryPrice($object->GetID(), (double)$object->GetDBField('Price'), Array ('Cost' => (double)$object->GetDBField('Cost')));
 	}
 
 	function OnPreSaveAndOpenPopup($event)
 	{
 		/** @var kDBItem $object */
 		$object = $event->getObject();
 
 		$this->RemoveRequiredFields($object);
 		$event->CallSubEvent('OnPreSave');
 
 		$event->redirect = $this->Application->GetVar('t');
 		// pass ID too, in case if product is created by OnPreSave call to ensure proper editing
 		$event->SetRedirectParam('pass', 'all');
 		$event->SetRedirectParam($event->getPrefixSpecial(true) . '_id', $object->GetID());
 	}
 
 
 	/**
 	 * Returns ID of current item to be edited
 	 * by checking ID passed in get/post as prefix_id
 	 * or by looking at first from selected ids, stored.
 	 * Returned id is also stored in Session in case
 	 * it was explicitly passed as get/post
 	 *
 	 * @param kEvent $event
 	 * @return int
 	 * @access public
 	 */
 	public function getPassedID(kEvent $event)
 	{
 		if ( $this->Application->isAdminUser ) {
 			$event->setEventParam('raise_warnings', 0);
 		}
 
 		$passed = parent::getPassedID($event);
 
 		if ( $passed ) {
 			return $passed;
 		}
 
 		if ( $this->Application->isAdminUser ) {
 			// we may get product id out of OrderItem, if it exists
 			/** @var OrdersItem $ord_item */
 			$ord_item = $this->Application->recallObject('orditems', null, Array ('raise_warnings' => 0));
 
 			if ( $ord_item->GetDBField('ProductId') ) {
 				$passed = $ord_item->GetDBField('ProductId');
 			}
 		}
 
 		return $passed;
 	}
 
 	/**
 	 * Occurs, when config was parsed, allows to change config data dynamically
 	 *
 	 * @param kEvent $event
 	 * @return void
 	 * @access protected
 	 */
 	protected function OnAfterConfigRead(kEvent $event)
 	{
 		parent::OnAfterConfigRead($event);
 
 		if (!$this->Application->LoggedIn()) {
 			return ;
 		}
 
-		$user_id = $this->Application->RecallVar('user_id');
-
-		$sql = 'SELECT PrimaryGroupId
-				FROM ' . TABLE_PREFIX . 'Users
-				WHERE PortalUserId = ' . $user_id;
-		$primary_group_id = $this->Conn->GetOne($sql);
+		/** @var UserHelper $user_helper */
+		$user_helper = $this->Application->recallObject('UserHelper');
+		$primary_group_id = $user_helper->getPrimaryGroup($this->Application->RecallVar('user_id'));
 
 		if (!$primary_group_id) {
 			return;
 		}
 
 		$sub_select = '	SELECT pp.Price
 						FROM ' . TABLE_PREFIX . 'ProductsPricing AS pp
 			 			WHERE pp.ProductId = %1$s.ProductId AND GroupId = ' . $primary_group_id . '
 			 			ORDER BY MinQty
 			 			LIMIT 0,1';
 
 		$calculated_fields = $this->Application->getUnitOption($event->Prefix, 'CalculatedFields');
 		$calculated_fields['']['Price'] = 'IFNULL((' . $sub_select . '), ' . $calculated_fields['']['Price'] . ')';
 		$this->Application->setUnitOption($event->Prefix, 'CalculatedFields', $calculated_fields);
 	}
 
 	/**
 	 * Starts product editing, remove any pending inventory actions
 	 *
 	 * @param kEvent $event
 	 * @return void
 	 * @access protected
 	 */
 	protected function OnEdit(kEvent $event)
 	{
 		$this->Application->RemoveVar('inventory_actions');
 
 		parent::OnEdit($event);
 	}
 
 	/**
 	 * Adds "Shop Cart" tab on paid listing type editing tab
 	 *
 	 * @param kEvent $event
 	 */
 	function OnModifyPaidListingConfig($event)
 	{
 		$edit_tab_presets = $this->Application->getUnitOption($event->MasterEvent->Prefix, 'EditTabPresets');
 		$edit_tab_presets['Default']['shopping_cart'] = Array ('title' => 'la_tab_ShopCartEntry', 't' => 'in-commerce/paid_listings/paid_listing_type_shopcart', 'priority' => 2);
 		$this->Application->setUnitOption($event->MasterEvent->Prefix, 'EditTabPresets', $edit_tab_presets);
 	}
 
 	/**
 	 * [HOOK] Allows to add cloned subitem to given prefix
 	 *
 	 * @param kEvent $event
 	 * @return void
 	 * @access protected
 	 */
 	protected function OnCloneSubItem(kEvent $event)
 	{
 		parent::OnCloneSubItem($event);
 
 		if ( $event->MasterEvent->Prefix == 'rev' ) {
 			$clones = $this->Application->getUnitOption($event->MasterEvent->Prefix, 'Clones');
 			$subitem_prefix = $event->Prefix . '-' . $event->MasterEvent->Prefix;
 
 			$clones[$subitem_prefix]['ConfigMapping'] = Array (
 				'PerPage'				=>	'Comm_Perpage_Reviews',
 
 				'ReviewDelayInterval'	=>	'product_ReviewDelay_Value',
 				'ReviewDelayValue'		=>	'product_ReviewDelay_Interval',
 			);
 
 			$this->Application->setUnitOption($event->MasterEvent->Prefix, 'Clones', $clones);
 		}
 	}
 
 	/**
 	 * Adds product to comparison list
 	 *
 	 * @param kEvent $event
 	 * @return void
 	 * @access protected
 	 */
 	protected function OnAddToCompare(kEvent $event)
 	{
 		$products = $this->getCompareProducts();
 		$product_id = (int)$this->Application->GetVar($event->Prefix . '_id');
 
 		if ( $product_id ) {
 			$max_products = $this->Application->ConfigValue('MaxCompareProducts');
 
 			if ( count($products) < $max_products ) {
 				$products[] = $product_id;
 				$this->Application->Session->SetCookie('compare_products', implode('|', array_unique($products)));
 
 				$event->SetRedirectParam('result', 'added');
 			}
 			else {
 				$event->SetRedirectParam('result', 'error');
 			}
 		}
 
 		$event->SetRedirectParam('pass', 'm,p');
 	}
 
 	/**
 	 * Adds product to comparison list
 	 *
 	 * @param kEvent $event
 	 * @return void
 	 * @access protected
 	 */
 	protected function OnRemoveFromCompare(kEvent $event)
 	{
 		$products = $this->getCompareProducts();
 
 		$product_id = (int)$this->Application->GetVar($event->Prefix . '_id');
 
 		if ( $product_id && in_array($product_id, $products) ) {
 			$products = array_diff($products, Array ($product_id));
 			$this->Application->Session->SetCookie('compare_products', implode('|', array_unique($products)));
 
 			$event->SetRedirectParam('result', 'removed');
 		}
 
 		$event->SetRedirectParam('pass', 'm,p');
 	}
 
 	/**
 	 * Cancels product compare
 	 *
 	 * @param kEvent $event
 	 * @return void
 	 * @access protected
 	 */
 	protected function OnCancelCompare(kEvent $event)
 	{
 		$this->Application->Session->SetCookie('compare_products', '', -1);
 
 		$event->SetRedirectParam('result', 'all_removed');
 	}
 
 	/**
 	 * Returns products, that needs to be compared with each other
 	 *
 	 * @return Array
 	 * @access protected
 	 */
 	protected function getCompareProducts()
 	{
 		$products = $this->Application->GetVarDirect('compare_products', 'Cookie');
 		$products = $products ? explode('|', $products) : Array ();
 
 		return $products;
 	}
 }
Index: branches/5.2.x/units/products/products_item.php
===================================================================
--- branches/5.2.x/units/products/products_item.php	(revision 16774)
+++ branches/5.2.x/units/products/products_item.php	(revision 16775)
@@ -1,85 +1,87 @@
 <?php
 /**
 * @version	$Id$
 * @package	In-Commerce
 * @copyright	Copyright (C) 1997 - 2009 Intechnic. All rights reserved.
 * @license	Commercial License
 * This software is protected by copyright law and international treaties.
 * Unauthorized reproduction or unlicensed usage of the code of this program,
 * or any portion of it may result in severe civil and criminal penalties,
 * and will be prosecuted to the maximum extent possible under the law
 * See http://www.in-portal.org/commercial-license for copyright notices and details.
 */
 
 	defined('FULL_PATH') or die('restricted access!');
 
 	class ProductsItem extends kCatDBItem
 	{
 		function GetPackageContentIds()
 		{
 			$ids_string = trim($this->GetDBField('PackageContent'), '|');
 			if ($ids_string) {
 				$ids_array = explode('|', $ids_string);
 				return $ids_array;
 			}
 			else {
 				return array();
 			}
 		}
 
 		/**
 		 * Returns field values from primary pricing for product
 		 *
 		 * @return array
 		 */
 		function getPrimaryPricing()
 		{
 			// product + pricing based
 			$cache_key = 'product_primary_pricing[%PIDSerial:' . $this->GetID() . '%][%PrIDSerial:ProductId:' . $this->GetID() . '%]';
 
 			if (!$this->Application->isAdmin && $this->Application->LoggedIn()) {
 				// also group based
-				$primary_group = (int)$this->Application->Session->GetField('GroupId');
+				/** @var UserHelper $user_helper */
+				$user_helper = $this->Application->recallObject('UserHelper');
+				$primary_group = $user_helper->getPrimaryGroup($this->Application->RecallVar('user_id'));
 				$cache_key .= ':group=' . $primary_group;
 			}
 
 			// don't cache, while in temp table
 			$price_info = $this->IsTempTable() ? false : $this->Application->getCache($cache_key);
 
 			if ($price_info === false) {
 				if (!$this->Application->isAdmin && $this->Application->LoggedIn()) {
 					// logged in user on front-end
 					$this->Conn->nextQueryCachable = true;
 					$sql = 'SELECT Price, Cost
 							FROM ' . TABLE_PREFIX . 'ProductsPricing
 							WHERE (ProductId = ' . $this->GetID() . ') AND (GroupId = ' . $primary_group . ')
 							ORDER BY MinQty';
 					$price_info = $this->Conn->GetRow($sql);
 
 					if ($price_info !== false) {
 						$this->Application->setCache($cache_key, $price_info);
 
 						return $price_info;
 					}
 				}
 
 				// not logged-in user on front-end or in administrative console
 				$pr_table = $this->Application->getUnitOption('pr', 'TableName');
 
 				if ($this->IsTempTable()) {
 					$pr_table = $this->Application->GetTempName($pr_table, 'prefix:' . $this->Prefix);
 				}
 
 				$this->Conn->nextQueryCachable = true;
 				$sql = 'SELECT Price, Cost
 						FROM ' . $pr_table . '
 						WHERE (' . $this->IDField . ' = ' . $this->GetID() . ') AND (IsPrimary = 1)';
 				$price_info = $this->Conn->GetRow($sql);
 
 				$this->Application->setCache($cache_key, $price_info);
 			}
 
 			return $price_info;
 		}
 
-	}
\ No newline at end of file
+	}