Index: branches/5.2.x/units/pricing/pricing_event_handler.php
===================================================================
--- branches/5.2.x/units/pricing/pricing_event_handler.php	(revision 16692)
+++ branches/5.2.x/units/pricing/pricing_event_handler.php	(revision 16693)
@@ -1,525 +1,524 @@
 <?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=each($temp);
-			$first_item_key=$first_item['key'];
+			$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);
 
 			if ( $pricing_group ) {
 				$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');
 			}
 		}
 		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/shipping_quote_engines/usps.php
===================================================================
--- branches/5.2.x/units/shipping_quote_engines/usps.php	(revision 16692)
+++ branches/5.2.x/units/shipping_quote_engines/usps.php	(revision 16693)
@@ -1,1341 +1,1341 @@
 <?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!');
 
 define('MODULE_SHIPPING_USPS_TEXT_TITLE', 'United States Postal Service');
 define('MODULE_SHIPPING_USPS_TEXT_DESCRIPTION', 'You will need to have registered an account with USPS. Click <a target="_blank" href="https://secure.shippingapis.com/registration/"><strong>HERE</strong></a> for registration details. USPS expects you to use pounds as weight measure for your products.');
 define('MODULE_SHIPPING_USPS_TEXT_ERROR', 'An error occured with the USPS shipping calculations.<br>If you prefer to use USPS as your shipping method, please contact the store owner.');
 define('MODULE_SHIPPING_USPS_TEXT_DAY', 'Day');
 define('MODULE_SHIPPING_USPS_TEXT_DAYS', 'Days');
 define('MODULE_SHIPPING_USPS_TEXT_WEEKS', 'Weeks');
 
 define('MODULE_SHIPPING_USPS_STATUS', 'True'); // Do you want to offer USPS shipping?
 define('MODULE_SHIPPING_USPS_SERVER', 'production'); // An account at USPS is needed to use the Production server // production  othervise value may be 'test'
 define('MODULE_SHIPPING_USPS_HANDLING', '0'); // Handling fee for this shipping method
 define('MODULE_SHIPPING_USPS_TAX_CLASS', '0'); // Use the following tax class on the shipping fee
 define('MODULE_SHIPPING_USPS_ZONE', '0'); // If a zone is selected, only enable this shipping method for that zone.
 define('MODULE_SHIPPING_USPS_SORT_ORDER', '0'); // Sort order of display.
 define('MODULE_SHIPPING_USPS_TYPES', 'PRIORITY, PARCEL'); //  EXPRESS, FIRST CLASS, BMP, MEDIA  'Select the domestic services to be offered:
 define('MODULE_SHIPPING_USPS_TYPES_INTL', 'EXPRESS MAIL INTERNATIONAL (EMS), EXPRESS MAIL INT, EXPRESS MAIL INT FLAT RATE ENV, PRIORITY MAIL INT, PRIORITY MAIL INT FLAT RATE ENV, PRIORITY MAILINT FLAT RATE BOX, FIRST-CLASS MAIL INT');//  'GLOBAL EXPRESS, GLOBAL EXPRESS NON-DOC RECT, GLOBAL EXPRESS NON-DOC NON-RECT,  Select the international services to be offered:
 define('MODULE_SHIPPING_USPS_OPTIONS', 'Display weight, Display transit time'); //
 
 //configuration values for insurance
 define('MODULE_SHIPPING_USPS_INS1', '1.65');// 'US/Canada insurance for totals $.01-$50.00
 define('MODULE_SHIPPING_USPS_INS2', '2.05');// 'US/Canada insurance for totals $50.01-$100
 define('MODULE_SHIPPING_USPS_INS3', '2.45');// 'US/Canada insurance for totals $100.01-$200
 define('MODULE_SHIPPING_USPS_INS4', '4.60');// 'US/Canada insurance for totals $200.01-$300
 define('MODULE_SHIPPING_USPS_INS5', '.90');// 'US/Canada insurance for every $100 over $300 (add)
 define('MODULE_SHIPPING_USPS_INS6', '2.40');//  'International insurance for totals $.01-$50.00
 define('MODULE_SHIPPING_USPS_INS7', '3.30');// 'International insurance for totals $50.01-$100
 define('MODULE_SHIPPING_USPS_INS8', '4.20');// 'International insurance for totals $100.01-$200
 define('MODULE_SHIPPING_USPS_INS9', '5.10');// 'International insurance for totals $200.01-$300
 define('MODULE_SHIPPING_USPS_INS10', '.90');// 'International insurance for every $100 over $300 (add)
 define('MODULE_SHIPPING_USPS_INSURE', 'True');// 'Insure packages shipped by USPS?
 define('MODULE_SHIPPING_USPS_INSURE_TAX', 'True');// 'Insure tax on packages shipped by USPS?
 
 class USPS extends ShippingQuoteEngine
 {
 	var $countries, $pounds, $ounces, $insurance_cost = 0, $shipping_origin_country, $store_first_name, $store_last_name, $company_name, $store_name, $store_address1, $store_address2, $store_city, $store_state, $store_zip5, $store_zip4, $store_phone, $usps_userid;
 	var $order = Array();
 	var $types = Array();
 	var $intl_types = Array();
 
 	/**
 	 * Path to a request log file
 	 *
 	 * @var string
 	 */
 	var $logFilePath = '';
 
 	/**
 	 * Creates USPS processing class
 	 *
 	 */
 	public function __construct()
 	{
 		parent::__construct();
 
 		$this->logFilePath = (defined('RESTRICTED') ? RESTRICTED : WRITEABLE . '/user_files') . '/usps.log';
 
         // EXPRESS, FIRST CLASS, PRIORITY, PARCEL, BMP, MEDIA
 		$this->types = Array(
 			'EXPRESS' => 'Express Mail',
 			'FIRST CLASS' => 'First Class Mail',
 			'PRIORITY' => 'Priority Mail',
 			'PARCEL' => 'Parcel Post',
 			'BPM' => 'Bound Printed Matter',
 			'MEDIA' => 'Media Mail'
 		);
 
 		$this->intl_types = Array(
 			// 'GLOBAL EXPRESS' => 'Global Express Guaranteed',
 			// 'GLOBAL EXPRESS NON-DOC RECT' => 'Global Express Guaranteed Non-Document Rectangular',
 			// 'GLOBAL EXPRESS NON-DOC NON-RECT' => 'Global Express Guaranteed Non-Document Non-Rectangular',
 			'EXPRESS MAIL INT' => 'Express Mail International (EMS)',
 			'EXPRESS MAIL INT FLAT RATE ENV' => 'Express Mail International (EMS) Flat Rate Envelope',
 			'PRIORITY MAIL INT' => 'Priority Mail International',
 			'PRIORITY MAIL INT FLAT RATE ENV' => 'Priority Mail International Flat Rate Envelope',
 			'PRIORITY MAIL INT FLAT RATE BOX' => 'Priority Mail International Flat Rate Box',
 			'FIRST-CLASS MAIL INT' => 'First-Class Mail International'
 		);
 
 		// get 2-symbol country code
 		$country = $this->Application->ConfigValue('Comm_Shipping_Country');
 
 		if ($country != '') {
 			$this->shipping_origin_country = $this->GetUSPSCountry($country, '');
 		}
 
 		$contact_name = trim($this->_prepare_xml_param($this->Application->ConfigValue('Comm_Contacts_Name')));
 		$split_pos = strpos($contact_name, ' ');
 		if ($split_pos === false) {
 			$this->store_first_name = $contact_name;
 			$this->store_last_name = '';
 		} else {
 			$this->store_first_name = substr($contact_name, 0, $split_pos);
 			$this->store_last_name = trim(substr($contact_name, $split_pos));
 		}
 		$this->company_name = $this->_prepare_xml_param($this->Application->ConfigValue('Comm_CompanyName'));
 		$this->store_name = $this->_prepare_xml_param($this->Application->ConfigValue('Comm_StoreName'));
 		$this->store_address1 = $this->_prepare_xml_param($this->Application->ConfigValue('Comm_Shipping_AddressLine1'));
 		$this->store_address2 = $this->_prepare_xml_param($this->Application->ConfigValue('Comm_Shipping_AddressLine2'));
 
 		if ($this->store_address2 == '') {
 			$this->store_address2 = $this->store_address1;
 			$this->store_address1 = '';
 		}
 
 		$this->store_city = $this->_prepare_xml_param($this->Application->ConfigValue('Comm_Shipping_City'));
 		$this->store_state = $this->_prepare_xml_param($this->Application->ConfigValue('Comm_Shipping_State'));
 		$zip = $this->_prepare_xml_param($this->Application->ConfigValue('Comm_Shipping_ZIP'));
 		$this->store_zip5 = substr($zip, 0, 5);
 		$this->store_zip4 = trim(substr($zip, 6), '-');
 		$this->store_phone = $this->_prepare_xml_param($this->Application->ConfigValue('Comm_Contacts_Phone'));
 
 		// get username and password fron config.
 		$a_params = $this->LoadParams();
 		$this->usps_userid = $a_params['AccountLogin'];
 
 		// Note by Erik: DO NOT CHANGE THIS ARRAY. It's values are sent to USPS service and any changes may impact class main functionality.
 		$this->countries = array(
 			'AF' => 'Afghanistan',
             'AL' => 'Albania',
             'DZ' => 'Algeria',
             'AD' => 'Andorra',
             'AO' => 'Angola',
             'AI' => 'Anguilla',
             'AG' => 'Antigua and Barbuda',
             'AR' => 'Argentina',
             'AM' => 'Armenia',
             'AW' => 'Aruba',
             'AU' => 'Australia',
             'AT' => 'Austria',
             'AZ' => 'Azerbaijan',
             'BS' => 'Bahamas',
         	'BH' => 'Bahrain',
             'BD' => 'Bangladesh',
             'BB' => 'Barbados',
             'BY' => 'Belarus',
             'BE' => 'Belgium',
             'BZ' => 'Belize',
             'BJ' => 'Benin',
             'BM' => 'Bermuda',
             'BT' => 'Bhutan',
             'BO' => 'Bolivia',
             'BA' => 'Bosnia-Herzegovina',
             'BW' => 'Botswana',
             'BR' => 'Brazil',
             'VG' => 'British Virgin Islands',
             'BN' => 'Brunei Darussalam',
             'BG' => 'Bulgaria',
             'BF' => 'Burkina Faso',
             'MM' => 'Burma',
             'BI' => 'Burundi',
             'KH' => 'Cambodia',
             'CM' => 'Cameroon',
             'CA' => 'Canada',
             'CV' => 'Cape Verde',
             'KY' => 'Cayman Islands',
             'CF' => 'Central African Republic',
             'TD' => 'Chad',
             'CL' => 'Chile',
             'CN' => 'China',
             'CX' => 'Christmas Island (Australia)',
             'CC' => 'Cocos Island (Australia)',
             'CO' => 'Colombia',
             'KM' => 'Comoros',
             'CG' => 'Congo (Brazzaville),Republic of the',
             'ZR' => 'Congo, Democratic Republic of the',
             'CK' => 'Cook Islands (New Zealand)',
             'CR' => 'Costa Rica',
             'CI' => 'Cote d\'Ivoire (Ivory Coast)',
             'HR' => 'Croatia',
             'CU' => 'Cuba',
             'CY' => 'Cyprus',
             'CZ' => 'Czech Republic',
             'DK' => 'Denmark',
             'DJ' => 'Djibouti',
             'DM' => 'Dominica',
             'DO' => 'Dominican Republic',
             'TP' => 'East Timor (Indonesia)',
             'EC' => 'Ecuador',
             'EG' => 'Egypt',
             'SV' => 'El Salvador',
             'GQ' => 'Equatorial Guinea',
             'ER' => 'Eritrea',
             'EE' => 'Estonia',
             'ET' => 'Ethiopia',
             'FK' => 'Falkland Islands',
             'FO' => 'Faroe Islands',
             'FJ' => 'Fiji',
             'FI' => 'Finland',
             'FR' => 'France',
             'GF' => 'French Guiana',
             'PF' => 'French Polynesia',
             'GA' => 'Gabon',
             'GM' => 'Gambia',
             'GE' => 'Georgia, Republic of',
             'DE' => 'Germany',
             'GH' => 'Ghana',
             'GI' => 'Gibraltar',
             'GB' => 'Great Britain and Northern Ireland',
             'GR' => 'Greece',
             'GL' => 'Greenland',
             'GD' => 'Grenada',
             'GP' => 'Guadeloupe',
             'GT' => 'Guatemala',
             'GN' => 'Guinea',
             'GW' => 'Guinea-Bissau',
             'GY' => 'Guyana',
             'HT' => 'Haiti',
             'HN' => 'Honduras',
             'HK' => 'Hong Kong',
             'HU' => 'Hungary',
             'IS' => 'Iceland',
             'IN' => 'India',
             'ID' => 'Indonesia',
             'IR' => 'Iran',
             'IQ' => 'Iraq',
             'IE' => 'Ireland',
             'IL' => 'Israel',
             'IT' => 'Italy',
             'JM' => 'Jamaica',
             'JP' => 'Japan',
             'JO' => 'Jordan',
             'KZ' => 'Kazakhstan',
             'KE' => 'Kenya',
             'KI' => 'Kiribati',
             'KW' => 'Kuwait',
             'KG' => 'Kyrgyzstan',
             'LA' => 'Laos',
             'LV' => 'Latvia',
             'LB' => 'Lebanon',
             'LS' => 'Lesotho',
             'LR' => 'Liberia',
             'LY' => 'Libya',
             'LI' => 'Liechtenstein',
             'LT' => 'Lithuania',
             'LU' => 'Luxembourg',
             'MO' => 'Macao',
             'MK' => 'Macedonia, Republic of',
             'MG' => 'Madagascar',
             'MW' => 'Malawi',
             'MY' => 'Malaysia',
             'MV' => 'Maldives',
             'ML' => 'Mali',
             'MT' => 'Malta',
             'MQ' => 'Martinique',
             'MR' => 'Mauritania',
             'MU' => 'Mauritius',
             'YT' => 'Mayotte (France)',
             'MX' => 'Mexico',
             'MD' => 'Moldova',
             'MC' => 'Monaco (France)',
             'MN' => 'Mongolia',
             'MS' => 'Montserrat',
             'MA' => 'Morocco',
             'MZ' => 'Mozambique',
             'NA' => 'Namibia',
             'NR' => 'Nauru',
             'NP' => 'Nepal',
             'NL' => 'Netherlands',
             'AN' => 'Netherlands Antilles',
             'NC' => 'New Caledonia',
             'NZ' => 'New Zealand',
             'NI' => 'Nicaragua',
             'NE' => 'Niger',
             'NG' => 'Nigeria',
             'KP' => 'North Korea (Korea, Democratic People\'s Republic of)',
             'NO' => 'Norway',
             'OM' => 'Oman',
             'PK' => 'Pakistan',
             'PA' => 'Panama',
             'PG' => 'Papua New Guinea',
             'PY' => 'Paraguay',
             'PE' => 'Peru',
             'PH' => 'Philippines',
             'PN' => 'Pitcairn Island',
             'PL' => 'Poland',
             'PT' => 'Portugal',
             'QA' => 'Qatar',
             'RE' => 'Reunion',
             'RO' => 'Romania',
             'RU' => 'Russia',
             'RW' => 'Rwanda',
             'SH' => 'Saint Helena',
             'KN' => 'Saint Kitts (St. Christopher and Nevis)',
             'LC' => 'Saint Lucia',
             'PM' => 'Saint Pierre and Miquelon',
             'VC' => 'Saint Vincent and the Grenadines',
             'SM' => 'San Marino',
             'ST' => 'Sao Tome and Principe',
             'SA' => 'Saudi Arabia',
             'SN' => 'Senegal',
             'YU' => 'Serbia-Montenegro',
             'SC' => 'Seychelles',
             'SL' => 'Sierra Leone',
             'SG' => 'Singapore',
             'SK' => 'Slovak Republic',
             'SI' => 'Slovenia',
             'SB' => 'Solomon Islands',
             'SO' => 'Somalia',
             'ZA' => 'South Africa',
             'GS' => 'South Georgia (Falkland Islands)',
             'KR' => 'South Korea (Korea, Republic of)',
             'ES' => 'Spain',
             'LK' => 'Sri Lanka',
             'SD' => 'Sudan',
             'SR' => 'Suriname',
             'SZ' => 'Swaziland',
             'SE' => 'Sweden',
             'CH' => 'Switzerland',
             'SY' => 'Syrian Arab Republic',
             'TW' => 'Taiwan',
             'TJ' => 'Tajikistan',
             'TZ' => 'Tanzania',
             'TH' => 'Thailand',
             'TG' => 'Togo',
             'TK' => 'Tokelau (Union) Group (Western Samoa)',
             'TO' => 'Tonga',
             'TT' => 'Trinidad and Tobago',
             'TN' => 'Tunisia',
             'TR' => 'Turkey',
             'TM' => 'Turkmenistan',
             'TC' => 'Turks and Caicos Islands',
             'TV' => 'Tuvalu',
             'UG' => 'Uganda',
             'UA' => 'Ukraine',
             'AE' => 'United Arab Emirates',
             'UY' => 'Uruguay',
             'UZ' => 'Uzbekistan',
             'VU' => 'Vanuatu',
             'VA' => 'Vatican City',
             'VE' => 'Venezuela',
             'VN' => 'Vietnam',
             'WF' => 'Wallis and Futuna Islands',
             'WS' => 'Western Samoa',
             'YE' => 'Yemen',
             'ZM' => 'Zambia',
             'ZW' => 'Zimbabwe'
         );
 
 		$this->countryinsure = array(
 			'AF' => 0,
             'AL' => 0,
             'DZ' => 2185,
             'AD' => 5000,
             'AO' => 0,
             'AI' => 415,
             'AG' => 60,
             'AR' => 5000,
             'AM' => 1350,
             'AW' => 830,
             'AU' => 3370,
             'AT' => 5000,
             'AZ' => 5000,
             'BS' => 2795,
             'BH' => 0,
             'BD' => 5000,
             'BB' => 220,
             'BY' => 1323,
             'BE' => 5000,
             'BZ' => 1600,
             'BJ' => 170,
             'BM' => 440,
             'BT' => 440,
             'BO' => 0,
             'BA' => 5000,
             'BW' => 145,
             'BR' => 5000,
             'VG' => 165,
             'BN' => 4405,
             'BG' => 1030,
             'BF' => 530,
             'MM' => 4045,
             'BI' => 790,
             'KH' => 0,
             'CM' => 5000,
             'CA' => 675,
             'CV' => 0,
             'KY' => 0,
             'CF' => 4405,
             'TD' => 440,
             'CL' => 0,
             'CN' => 1130,
             'CX' => 3370,
             'CC' => 3370,
             'CO' => 0,
             'KM' => 690,
             'CG' => 1685,
             'ZR' => 0,
             'CK' => 980,
             'CR' => 0,
             'CI' => 5000,
             'HR' => 5000,
             'CU' => 0,
             'CY' => 5000,
             'CZ' => 5000,
             'DK' => 5000,
             'DJ' => 880,
             'DM' => 0,
             'DO' => 0,
             'TP' => 0,
             'EC' => 0,
             'EG' => 1685,
             'SV' => 0,
             'GQ' => 0,
             'ER' => 0,
             'EE' => 2020,
             'ET' => 1000,
             'FK' => 510,
             'FO' => 5000,
             'FJ' => 600,
             'FI' => 5000,
             'FR' => 5000,
             'GF' => 5000,
             'PF' => 1015,
             'GA' => 485,
             'GM' => 2575,
             'GE' => 1350,
             'DE' => 5000,
             'GH' => 5000,
             'GI' => 5000,
             'GB' => 857,
             'GR' => 5000,
             'GL' => 5000,
             'GD' => 350,
             'GP' => 5000,
             'GT' => 0,
             'GN' => 875,
             'GW' => 21,
             'GY' => 10,
             'HT' => 0,
             'HN' => 0,
             'HK' => 5000,
             'HU' => 5000,
             'IS' => 5000,
             'IN' => 2265,
             'ID' => 0,
             'IR' => 0,
             'IQ' => 0,
             'IE' => 5000,
             'IL' => 0,
             'IT' => 5000,
             'JM' => 0,
             'JP' => 5000,
             'JO' => 0,
             'KZ' => 5000,
             'KE' => 815,
             'KI' => 0,
             'KW' => 1765,
             'KG' => 1350,
             'LA' => 0,
             'LV' => 1350,
             'LB' => 440,
             'LS' => 440,
             'LR' => 440,
             'LY' => 0,
             'LI' => 5000,
             'LT' => 5000,
             'LU' => 5000,
             'MO' => 4262,
             'MK' => 2200,
             'MG' => 675,
             'MW' => 50,
             'MY' => 1320,
             'MV' => 0,
             'ML' => 950,
             'MT' => 5000,
             'MQ' => 5000,
             'MR' => 635,
             'MU' => 270,
             'YT' => 5000,
             'MX' => 0,
             'MD' => 1350,
             'MC' => 5000,
             'MN' => 440,
             'MS' => 2200,
             'MA' => 5000,
             'MZ' => 0,
             'NA' => 4405,
             'NR' => 220,
             'NP' => 0,
             'NL' => 5000,
             'AN' => 830,
             'NC' => 1615,
             'NZ' => 980,
             'NI' => 440,
             'NE' => 810,
             'NG' => 205,
             'KP' => 0,
             'NO' => 0,
             'OM' => 575,
             'PK' => 270,
             'PA' => 0,
             'PG' => 445,
             'PY' => 0,
             'PE' => 0,
             'PH' => 270,
             'PN' => 0,
             'PL' => 1350,
             'PT' => 5000,
             'QA' => 2515,
             'RE' => 5000,
             'RO' => 5000,
             'RU' => 5000,
             'RW' => 0,
             'SH' => 170,
             'KN' => 210,
             'LC' => 400,
             'PM' => 5000,
             'VC' => 130,
             'SM' => 5000,
             'ST' => 440,
             'SA' => 0,
             'SN' => 865,
             'YU' => 5000,
             'SC' => 0,
             'SL' => 0,
             'SG' => 4580,
             'SK' => 5000,
             'SI' => 4400,
             'SB' => 0,
             'SO' => 440,
             'ZA' => 1760,
             'GS' => 510,
             'KR' => 5000,
             'ES' => 5000,
             'LK' => 35,
             'SD' => 0,
             'SR' => 535,
             'SZ' => 560,
             'SE' => 5000,
             'CH' => 5000,
             'SY' => 3080,
             'TW' => 1350,
             'TJ' => 1350,
             'TZ' => 230,
             'TH' => 1350,
             'TG' => 2190,
             'TK' => 295,
             'TO' => 515,
             'TT' => 930,
             'TN' => 2200,
             'TR' => 880,
             'TM' => 675,
             'TC' => 0,
             'TV' => 4715,
             'UG' => 0,
             'UA' => 5000,
             'AE' => 5000,
             'UY' => 0,
             'UZ' => 5000,
             'VU' => 0,
             'VA' => 5000,
             'VE' => 0,
             'VN' => 0,
             'WF' => 1615,
             'WS' => 295,
             'YE' => 0,
             'ZM' => 540,
             'ZW' => 600,
             'US' => 5000
     	);
 	}
 
 	function SetInsurance()
 	{
 		$this->insurance_cost = 0;
 
 		// Insurance module by Kevin Shelton
 		// divide the value of the order among the packages based on the order total or subtotal depending on whether or not you have configured to insure tax
 		$shipping_weight = $this->order['ShippingWeight'];
 		$shipping_num_boxes = $this->order['ShippingNumBoxes'];
 		$costperpkg = $this->order['SubTotal'] / $shipping_num_boxes;
 
 		// retrieve the maximum allowed insurance for the destination country and if the package value exceeds it then set package value to the maximum allowed
 
 		$maxins = $this->countryinsure[$this->order['ShippingCountry']];
 		if ($costperpkg > $maxins) $costperpkg = $maxins;
 
 		// if insurance not allowed for destination or insurance is turned off add nothing to shipping cost
 
 		if (($maxins == 0) || (MODULE_SHIPPING_USPS_INSURE == 'False')) {
 			$insurance = 0;
 		}
 		// US and Canada share the same insurance calculation (though not the same maximum)
 		else if (($this->order['ShippingCountry'] == 'US') || ($this->order['ShippingCountry'] == 'CA'))
 		{
 			if ($costperpkg<=50) {
 				$insurance=MODULE_SHIPPING_USPS_INS1;
 			}
 			else if ($costperpkg<=100) {
 				$insurance=MODULE_SHIPPING_USPS_INS2;
 			}
 			else if ($costperpkg<=200) {
 				$insurance=MODULE_SHIPPING_USPS_INS3;
 			}
 			else if ($costperpkg<=300) {
 				$insurance=MODULE_SHIPPING_USPS_INS4;
 			}
 			else {
 				$insurance = MODULE_SHIPPING_USPS_INS4 + ((ceil($costperpkg/100) -3) * MODULE_SHIPPING_USPS_INS5);
 			}
 		}
 		// if insurance allowed and is not US or Canada then calculate international insurance
 		else {
 			if ($costperpkg<=50) {
 			    $insurance=MODULE_SHIPPING_USPS_INS6;
 			}
 			else if ($costperpkg<=100) {
 			    $insurance=MODULE_SHIPPING_USPS_INS7;
 			}
 			else if ($costperpkg<=200) {
 			    $insurance=MODULE_SHIPPING_USPS_INS8;
 			}
 			else if ($costperpkg<=300) {
 			    $insurance=MODULE_SHIPPING_USPS_INS9;
 			}
 			else {
 			    $insurance = MODULE_SHIPPING_USPS_INS9 + ((ceil($costperpkg/100) - 3) * MODULE_SHIPPING_USPS_INS10);
 			}
 		}
 		// usps doesnt accept zero weight
 		$shipping_weight = ($shipping_weight < 0.1 ? 0.1 : $shipping_weight);
 		$shipping_pounds = floor ($shipping_weight);
 		$shipping_ounces = round(16 * ($shipping_weight - floor($shipping_weight)));
 		$this->_setWeight($shipping_pounds, $shipping_ounces);
 		// Added by Kevin Chen (kkchen@uci.edu); Fixes the Parcel Post Bug July 1, 2004
 		// Refer to http://www.usps.com/webtools/htm/Domestic-Rates.htm documentation
 		// Thanks Ryan
 		if($shipping_pounds > 35 || ($shipping_pounds == 0 && $shipping_ounces < 6)){
 			$this->_setMachinable('False');
 		}
 		else{
 			$this->_setMachinable('True');
 		}
 
 		$this->insurance_cost = $insurance;
 		// End Kevin Chen July 1, 2004
 	}
 
 	function _setService($service)
 	{
 		$this->service = $service;
 	}
 
 	function _setWeight($pounds, $ounces=0)
 	{
 		$this->pounds = $pounds;
 		$this->ounces = $ounces;
 	}
 
 	function _setContainer($container)
 	{
 		$this->container = $container;
 	}
 
 	function _setSize($size)
 	{
 		$this->size = $size;
 	}
 
 	function _setMachinable($machinable)
 	{
 		$this->machinable = $machinable;
 	}
 
 	function PhoneClean($phone)
 	{
 		$res = preg_replace('/[(]|[)]|[\-]|[ ]|[#]|[\.]|[a-z](.*)|[A-Z](.*)/g', '', $phone);
 		if ( strlen($res) > 10 ) {
 			$res = substr($res, 0, 10);
 		}
 		return $res != '' ? $res : $phone;
 	}
 
 	function GetQuote($method = '')
 	{
 		if ( isset($this->types[$method]) || in_array($method, $this->intl_types)) {
 			$this->_setService($method);
 		}
 		$this -> _setContainer('None');
 		$this -> _setSize('REGULAR');
 		$this -> SetInsurance(); // ???
 		if ($this->order['ShippingCountry'] == $this->shipping_origin_country) {
 			$request='<?xml version="1.0"?>';
 			// PASSWORD="'.$this->usps_password.'"
 			$request.= '<RateV3Request USERID="'.$this->usps_userid.'">';
 			$services_count = 0;
 			if (isset($this->service)) {
 				$this->types = array($this->service => $this->types[$this->service]);
 			}
 			$dest_zip = str_replace(' ', '', $this->order['ShippingZip']);
 			$dest_zip = substr($dest_zip, 0, 5);
 			reset($this->types);
 			$allowed_types = explode(", ", MODULE_SHIPPING_USPS_TYPES);
 
-	    	while (list($key, $value) = each($this->types))
-	    	{
+			foreach ( array_keys($this->types) as $key ) {
 	 		 	if ( !in_array($key, $allowed_types) ) continue;
 				$request .= '<Package ID="'.$services_count.'">'.
 				          '<Service>'.$key.'</Service>'.
 				          '<ZipOrigination>'.$this->store_zip5.'</ZipOrigination>'.
 				          '<ZipDestination>'.$dest_zip.'</ZipDestination>'.
 				          '<Pounds>'.$this->pounds.'</Pounds>'.
 				          '<Ounces>'.$this->ounces.'</Ounces>'.
 				          '<Size>'.$this->size.'</Size>'.
 				          '<Machinable>'.$this->machinable.'</Machinable>'.
 				          '</Package>';
 	     	 	$services_count++;
 			}
+
 			$request .= '</RateV3Request>';
 			$api_query = 'RateV3';
 		}
 	  	else {
 			$request  = '<IntlRateRequest USERID="'.$this->usps_userid.'">'.
 			            '<Package ID="0">'.
 			            '<Pounds>'.$this->pounds.'</Pounds>'.
 			            '<Ounces>'.$this->ounces.'</Ounces>'.
 			            '<MailType>Package</MailType>'.
 			            '<Country>'.$this->countries[$this->order['ShippingCountry']].'</Country>'.
 			            '</Package>'.
 			            '</IntlRateRequest>';
 			$api_query = 'IntlRate';
 		}
 		$request = 'API='.$api_query.'&XML=' . kUtil::escape($request, kUtil::ESCAPE_URL);
 		$body = $this->PostQuery($request);
 		$body = str_replace(chr(146), '', $body); // for bad `
 
 		// check for errors
 		if (strpos($body, '<Error>') !== false) {
 			$errors = Array ();
 			preg_match_all('/<Number>(.*?)<\/Number>/s', $body, $error_numbers);
 			preg_match_all('/<Description>(.*?)<\/Description>/s', $body, $error_descriptions);
 
 			foreach ($error_numbers[1] as $index => $error_number) {
 				$errors[$index] = $error_descriptions[1][$index];
 				if ($this->Application->isDebugMode()) {
 					$errors[$index] .= ' (' . $error_number . ')';
 				}
 			}
 
 			$errors = array_unique($errors); // we may have same errors on many packages, so don't show duplicates
 			return Array('error' => implode('<br/>', $errors));
 		}
 
 		// parse response
 
 		/** @var kXMLHelper $xml_helper */
 		$xml_helper = $this->Application->recallObject('kXMLHelper');
 
 		/** @var kXMLNode $root_node */
 		$root_node =& $xml_helper->Parse($body);
 
 		$rates = Array();
 		// Domestic shipping
 		if ($this->order['ShippingCountry'] == $this->shipping_origin_country) {
 			$i = 0;
 			$postage_node =& $root_node->FindChild('Package');
 			do {
 				// $parcel_node =& $postage_node->firstChild;
 				$service = $postage_node->FindChildValue('MailService');
 				if ( $service != '' ) {
 					$i++;
 					$rates[$i] = Array();
 					$rates[$i]['Title'] = $service;
 					$rates[$i]['Rate'] = $this->insurance_cost + $postage_node->FindChildValue('Rate');
 				}
 			}
 			while ( $postage_node =& $postage_node->NextSibling());
 		}
 		else {
 			// for International Rates !!!
 		    $allowed_types = array();
 			foreach( explode(", ", MODULE_SHIPPING_USPS_TYPES_INTL) as $value ) {
 				$allowed_types[$value] = $this->intl_types[$value];
 			}
 			$i = 0;
 			$service_node =& $root_node->FindChild('Service');
 			do {
 				$service = trim($service_node->FindChildValue('SvcDescription'));
 				if( !in_array($service, $allowed_types) ) continue;
 				$i++;
 				if ( $service_node->FindChildValue('MaxWeight') >= $this->pounds ) {
 					$rates[$i] = Array();
 					$rates[$i]['Title'] = $service;
 					$rates[$i]['MaxDimensions'] = $service_node->FindChildValue('MaxDimensions');
 					$rates[$i]['MaxWeight'] = $service_node->FindChildValue('MaxWeight');
 					$rates[$i]['SvcCommitments'] = $service_node->FindChildValue('SvcCommitments');
 					$rates[$i]['Rate'] = $this->insurance_cost + $service_node->FindChildValue('Postage');
 				}
 			}
 			while ( $service_node =& $service_node->NextSibling());
 	  	}
 	  	// print_r($rates);
 	  	// die('here');
 		return $rates;
 	}
 
 	function PostOrder()
 	{
 		$request='';
 		$base_request = '';
 		$this->SetInsurance();
 		// $this->order['ShippingCountry'] = $this->GetUSPSCountry($this->order['ShippingCountry']);
 
 		// Domestic Order
 		if ($this->order['ShippingCountry'] == $this->shipping_origin_country) {
 
 			// $dest_zip = str_replace(' ', '', $this->order['ShippingZip5']);
 			$this->order['ShippingZip5'] = substr($this->order['ShippingZip5'], 0, 5);
 			$WeightInOunces = floor($this->pounds * 16 + $this->ounces);
 
 			$base_request ='
 			<Option>1</Option>
 			<ImageParameters></ImageParameters>
 			<FromName>'.$this->store_name.'</FromName>
 			<FromFirm>'.$this->company_name.'</FromFirm>
 			<FromAddress1>'.$this->store_address1.'</FromAddress1>
 			<FromAddress2>'.$this->store_address2.'</FromAddress2>
 			<FromCity>'.$this->store_city.'</FromCity>
 			<FromState>'.$this->store_state.'</FromState>
 			<FromZip5>'.$this->store_zip5.'</FromZip5>
 			<FromZip4>'.$this->store_zip4.'</FromZip4>
 			<ToName>'.$this->order['FirstName'].' '.$this->order['LastName'].'</ToName>
 			<ToFirm>'.$this->order['ShippingCompany'].'</ToFirm>
 			<ToAddress1>'.$this->order['ShippingAddress2'].'</ToAddress1>
 			<ToAddress2>'.$this->order['ShippingAddress1'].'</ToAddress2>
 			<ToCity>'.$this->order['ShippingCity'].'</ToCity>
 			<ToState>'.$this->order['ShippingState'].'</ToState>
 			<ToZip5>'.$this->order['ShippingZip5'].'</ToZip5>
 			<ToZip4>'.$this->order['ShippingZip4'].'</ToZip4>
 			<WeightInOunces>'.$WeightInOunces.'</WeightInOunces>
 			<ServiceType>'.$this->order['ShippingService'].'</ServiceType>
 			<ImageType>PDF</ImageType>
 			<LabelDate>'.date('m/d/Y',time()).'</LabelDate>
 			<CustomerRefNo></CustomerRefNo>
 			<AddressServiceRequested></AddressServiceRequested>
 			<SenderName></SenderName><SenderEMail></SenderEMail>
 			<RecipientName></RecipientName>
 			<RecipientEMail></RecipientEMail>
 			';
 
 			$api_query = 'DeliveryConfirmationV3';
 			$xml_request = 'DeliveryConfirmationV3.0Request';
 		}
 		else {
 
 			// International Order(s)
 			$shipping_service = strtolower($this->order['ShippingService']);
 
 		    $base_request = '<Option/>
 		    <ImageParameters/>
 		    <FromFirstName>'.$this->store_first_name.'</FromFirstName>
 		    <FromLastName>'.$this->store_last_name.'</FromLastName>
 		    <FromFirm>'.$this->company_name.'</FromFirm>
 		    <FromAddress1>'.$this->store_address1.'</FromAddress1>
 		    <FromAddress2>'.$this->store_address2.'</FromAddress2>
 		    <FromCity>'.$this->store_city.'</FromCity>
 		    <FromState>'.$this->store_state.'</FromState>
 		    <FromZip5>'.$this->store_zip5.'</FromZip5>
 		    <FromPhone>'.$this->PhoneClean($this->store_phone).'</FromPhone>
 		    <ToName>'.$this->order['FirstName'].' '.$this->order['LastName'].'</ToName>
 		    <ToFirm>'.$this->order['ShippingCompany'].'</ToFirm>
 		    <ToAddress1></ToAddress1>
 		    <ToAddress2>'.$this->order['ShippingAddress2'].'</ToAddress2>
 		    <ToAddress3>'.$this->order['ShippingAddress1'].'</ToAddress3>
 		    <ToCity>'.$this->order['ShippingCity'].'</ToCity>';
 		    if ( $this->order['ShippingProvince'] != '' ) {
 		    	$base_request.='
 		    	<ToProvince>'.$this->order['ShippingProvince'].'</ToProvince>';
 		    }
 		    $base_request.='
 		    <ToCountry>'.$this->countries[$this->order['ShippingCountry']].'</ToCountry>
 		    <ToPostalCode>'.$this->order['ShippingZip'].'</ToPostalCode>
 		    <ToPOBoxFlag>N</ToPOBoxFlag>
 		    <ToPhone>'.$this->PhoneClean($this->order['ShippingPhone']).'</ToPhone>
 		    <ToFax>'.$this->PhoneClean($this->order['ShippingFax']).'</ToFax>
 		    <ToEmail>'.$this->order['Email'].'</ToEmail>
 		    <ShippingContents>';
 
 		    // add items
 			foreach ( $this->order['Items'] as $k => $value ) {
 				 $base_request.='
 				 <ItemDetail>
 					<Description>Computer Parts</Description>
 					<Quantity>'.$value['Qty'].'</Quantity>
 					<Value>'.($value['Price'] * $value['Qty']).'</Value>
 					<NetPounds>'.$value['NetPounds'].'</NetPounds>
 					<NetOunces>'.$value['NetOunces'].'</NetOunces>
 					<HSTariffNumber>123456</HSTariffNumber>
 					<CountryOfOrigin>United States</CountryOfOrigin>
 				 </ItemDetail>';
 			}
 		    // end add items
 
 		    $base_request.='
 		    </ShippingContents>
 		    <GrossPounds>'.$this->pounds.'</GrossPounds>
 		    <GrossOunces>'.$this->ounces.'</GrossOunces>
 		    <ContentType>MERCHANDISE</ContentType>
 		    <Agreement>Y</Agreement>
 		    <InvoiceNumber>'.$this->order['InvoiceNumber'].'</InvoiceNumber>
 		    <ImageType>PDF</ImageType>
 		    <ImageLayout>ALLINONEFILE</ImageLayout>
 		    <LabelDate>'.date('m/d/Y',time()).'</LabelDate>
 		    ';
 
 			if (strpos($shipping_service, 'express') !== false) {
 				$xml_request = 'ExpressMailIntlRequest';
 				$api_query = 'ExpressMailIntl';
 			}
 			elseif (strpos($shipping_service, 'priority') !== false) {
 				$xml_request = 'PriorityMailIntlRequest';
 				$api_query = 'PriorityMailIntl';
 			}
 			else {
 				$xml_request = 'FirstClassMailIntlRequest';
 				$api_query = 'FirstClassMailIntl';
 			}
 		}
 
 		$request.= '<'.$xml_request.' USERID="'.$this->usps_userid.'">';
 		$request.= $base_request;
 		$request.= '</'.$xml_request.'>';
 
 		// die($request);
 
 		$request = 'API='.$api_query.'&XML='.kUtil::escape($request, kUtil::ESCAPE_URL);
 
 		$body = $this->PostQuery($request, 1);
 
 		// check for errors
 		if (strpos($body, '<Error>') !== false) {
 			$errors = Array ();
 			preg_match_all('/<Number>(.*?)<\/Number>/s', $body, $error_numbers);
 			preg_match_all('/<Description>(.*?)<\/Description>/s', $body, $error_descriptions);
 
 			foreach ($error_numbers[1] as $index => $error_number) {
 				$errors[$index] = Array ('error_number' => $error_number, 'error_description' => $error_descriptions[1][$index]);
 			}
 
 			// TODO: find a way to return other error messages in same package as well
 			return $errors[0];
 		}
 
 		// parse response
 		$xml_helper = $this->Application->recallObject('kXMLHelper');
 
 		/** @var kXMLNode $root_node */
 		$root_node =& $xml_helper->Parse($body);
 		$Postage = 0;
 
 		$label_file = $TrackingNumber = $PostnetBarCode = '';
 
 		// Domestic shipping
 		if ($this->order['ShippingCountry'] == $this->shipping_origin_country ) {
 			$delivery_node =& $root_node->FindChild('DeliveryConfirmationV3.0Response');
 			do {
 				$TrackingNumber = $delivery_node->FindChildValue('DeliveryConfirmationNumber');
 				$PostnetBarCode = $delivery_node->FindChildValue('Postnet');
 				$DeliveryConfirmationLabel = base64_decode($delivery_node->FindChildValue('DeliveryConfirmationLabel'));
 			}
 			while ( $delivery_node =& $delivery_node->NextSibling());
 		}
 		else {
 
 			if (strpos($shipping_service, 'express') !== false) {
 				$node_title = 'ExpressMailIntlResponse';
 			}
 			elseif (strpos($shipping_service, 'priority') !== false) {
 				$node_title = 'PriorityMailIntlResponse';
 			}
 			else {
  				$node_title = 'FirstClassMailIntlResponse';
 			}
 
 			$delivery_node =& $root_node->FindChild($node_title);
 			$PostnetBarCode = $delivery_node->FindChildValue('BarcodeNumber');
 			$Postage = $delivery_node->FindChildValue('Postage');
 			$DeliveryConfirmationLabel = base64_decode($delivery_node->FindChildValue('LabelImage'));
 
 		}
 
 		if ( $TrackingNumber != '' ) {
 			$label_file = USPS_LABEL_FOLDER.$TrackingNumber.".pdf";
 		}
 		elseif ( $PostnetBarCode != '' ) {
 			$label_file = USPS_LABEL_FOLDER.$PostnetBarCode.".pdf";
 		}
 
 		if ( $label_file != '' ) {
 			/** @var FileHelper $file_helper */
 			$file_helper = $this->Application->recallObject('FileHelper');
 
 			$file_helper->CheckFolder(USPS_LABEL_FOLDER);
 
 			if (!$handle = fopen($label_file, 'a')) echo "Cannot open file ($label_file)";
 			if ( @fwrite($handle, $DeliveryConfirmationLabel) === FALSE) echo "Cannot write to file ($label_file)";
 		}
 
 		return array('TrackingNumber' => $TrackingNumber, 'PostnetBarCode' => $PostnetBarCode, 'Postage' => $Postage);
 	}
 
 	function GetUSPSCountry($country, $default = 'US')
 	{
 		/** @var kCountryStatesHelper $cs_helper */
 		$cs_helper = $this->Application->recallObject('CountryStatesHelper');
 
 		$country = $cs_helper->getCountryIso($country);
 
 		return $country == '' ? $default : $country;
 	}
 
 	function GetShippingQuotes($params = null)
 	{
 		$weights = kUtil::Kg2Pounds($params['packages']['0']['weight']);
 		$weight = '';
 		$weight = $weights[0];
 		if ( $weights[1] != '' ) {
 			$weight.='.'.$weights[1];
 		}
 		$country = $this->GetUSPSCountry($params['dest_country']);
 
 		$this->order = Array();
 		$this->order['ShippingWeight'] = $weight;
 		$this->order['ShippingNumBoxes'] = 1;
 		$this->order['ShippingZip'] = $params['dest_postal'];
 		$this->order['SubTotal'] = $params['amount'];
 		$this->order['ShippingCountry'] = $country;
 
 		$shipping_types = Array();
 		$rates = $this->GetQuote();
 
 		if ( !isset($rates['error']) ) {
 			$this->Application->RemoveVar('sqe_error');
 
 			$i = 1;
 			foreach ($rates as $k => $rate ) {
 				 $shipping_types['USPS_'.$i] = Array(
 		            'ShippingId' => 'USPS_'.$i,
 		            'TotalCost' => $rate['Rate'],
 		            'ShippingName' => $rate['Title'],
 		            'Type' => '1',
 		            'CODFlat' => '0',
 		            'CODPercent' => '0',
 		            'PortalGroups' => ',15,',
 		            'InsuranceFee' => '',
 		            'COD' => '0',
 		            'SelectedOnly' => '0',
 		            'Code' => $rate['Title']
 				 );
 				 $i++;
 			}
 		}
 		else {
 			// for Front-End (shipping screen) and Admin (Shipping Tab on editing order)
 			$this->Application->StoreVar('sqe_error', $rates['error']);
 		}
 
 		$this->Application->StoreVar('current_usps_shipping_types', serialize($shipping_types));
 		return $shipping_types;
 	}
 
 	function TrackOrder($TrackingNumber='')
 	{
 		if ( $TrackingNumber != '' ) {
 			// http://testing.shippingapis.com/ShippingAPITest.dll?API=TrackV2&XML=<TrackFieldRequest USERID="402INTEC7634"><TrackID ID="EJ958083578US"></TrackID></TrackFieldRequest>
 
 			$request = '<TrackRequest USERID="'.$this->usps_userid.'"><TrackID ID="'.$TrackingNumber.'"></TrackID></TrackRequest>';
 			$api_query = 'TrackV2';
 			$request = 'API='.$api_query.'&XML='.kUtil::escape($request, kUtil::ESCAPE_URL);
 			$body = $this->PostQuery($request);
 
 			// check for errors
 			if (strpos($body, '<Error>') !== false) {
 				$errors = Array ();
 				preg_match_all('/<Number>(.*?)<\/Number>/s', $body, $error_numbers);
 				preg_match_all('/<Description>(.*?)<\/Description>/s', $body, $error_descriptions);
 
 				foreach ($error_numbers[1] as $index => $error_number) {
 					$errors[$index] = $error_descriptions[1][$index];
 					if ($this->Application->isDebugMode()) {
 						$errors[$index] .= ' (' . $error_number . ')';
 					}
 				}
 
 				$errors = array_unique($errors); // we may have same errors on many packages, so don't show duplicates
 				return Array('error' => implode('<br/>', $errors));
 			}
 
 			$xml_helper = $this->Application->recallObject('kXMLHelper');
 
 			/** @var kXMLNode $root_node */
 			$root_node =& $xml_helper->Parse($body);
 
 			// Tracking Shipping
 			$delivery_node =& $root_node->FindChild('TrackInfo');
 			$TrackSummary = $delivery_node->FindChildValue('TrackSummary');
 			// echo ' TrackSummary ('.$TrackingNumber.') = '.$TrackSummary.'<br>';
 
 			return strpos($TrackSummary, 'delivered') !== false ? 1 : 0;
 		}
 		else
 			return false;
 	}
 
 	function ProcessTrackOrders()
 	{
 		$sql = sprintf('SELECT `OrderId`, `ShippingTracking` FROM %s WHERE `Status` > 3 AND `Delivered` = 0', TABLE_PREFIX.'Orders');
 		$orders = $this->Application->Conn->Query($sql);
 		foreach ( $orders as $k => $order ) {
 			// try to track order
 			if ( $order['ShippingTracking'] != '' && $this->TrackOrder($order['ShippingTracking']) ) {
 				$update_order = sprintf("UPDATE %s SET `Delivered` = 1 WHERE %s = %s",
 					TABLE_PREFIX.'Orders',
 					$this->Application->getUnitOption('ord', 'IDField'),
 					$order[$this->Application->getUnitOption('ord', 'IDField')]
 				);
 				$this->Application->Conn->Query($update_order);
 			}
 		}
 	}
 
 	function PostQuery($request, $secure=0)
 	{
 		switch (MODULE_SHIPPING_USPS_SERVER) {
 			case 'production':
 				$usps_server = $secure > 0 ? 'https://secure.shippingapis.com' : 'http://production.shippingapis.com' ;
 			    $api_dll = 'ShippingAPI.dll';
 			break;
 
 			case 'test':
 				$usps_server = $secure > 0 ? 'https://secure.shippingapis.com' : 'http://testing.shippingapis.com';
 				$api_dll = 'ShippingAPITest.dll';
 			break;
 		}
 
 		$curl = curl_init();
 		curl_setopt($curl, CURLOPT_URL, $usps_server.'/'.$api_dll.'?'.$request);
 		curl_setopt($curl, CURLOPT_HEADER, 0);
 		curl_setopt($curl, CURLOPT_RETURNTRANSFER, 1);
 		$body = curl_exec($curl);
 		curl_close($curl);
 
 		if ($this->logFilePath) {
 			$fp = fopen($this->logFilePath, 'a');
 
 			if ($fp) {
 				$request_url = sprintf("Date %s : IP %s\n\nPost\n\n%s\n\nReplay\n\n%s\n\n",
 			   		adodb_date('m/d/Y H:i:s'),
 					$this->Application->getClientIp(),
 			   		$usps_server . '/' . $api_dll . '?' . urldecode($request),
 			   		$body
 			   	);
 
 			   	fwrite($fp, $request_url);
 			   	fclose($fp);
 			}
 		}
 
 		return $body;
 	}
 
 	/**
 	 * Returns available shipping types
 	 *
 	 * @return Array
 	 * @todo Get possible shipping types based on MODULE_SHIPPING_USPS_TYPES and MODULE_SHIPPING_USPS_TYPES_INTL consntants
 	 */
 	function GetAvailableTypes()
 	{
 		return Array (
 			Array (
 				'_ClassName' => get_class($this),
 				'_Id' => 'USPS',
 				'_Name' => 'USPS (Default)'
 			)
 		);
 	}
 
 	function LoadParams()
 	{
 		$sql = 'SELECT Properties FROM '.$this->Application->getUnitOption('sqe', 'TableName').'
 				WHERE ClassName="USPS"';
 		$params = $this->Conn->GetOne($sql);
 
 		return unserialize($params);
 	}
 
 	function _prepare_xml_param($value) {
 		return strip_tags($value);
 	}
 
 	/**
 	 * Returns virtual field names, that will be saved as properties
 	 *
 	 * @return Array
 	 */
 	function GetEngineFields()
 	{
 		return Array ('AccountLogin');
 	}
 
 	/**
 	 * Creates new USPS order
 	 *
 	 * @param OrdersItem $object
 	 * @param bool $dry_run
 	 * @return Array
 	 */
 	function MakeOrder(&$object, $dry_run = false)
 	{
 		$ShippingInfo = unserialize($object->GetDBField('ShippingInfo'));
 		$ShippingCode = $USPSMethod = '';
 		$ShippingCountry = $this->GetUSPSCountry($object->GetDBField('ShippingCountry'));
 
 		$UserName = explode(" ", $object->GetDBField('ShippingTo'));
 
 		$item_table = TABLE_PREFIX.'OrderItems';
 		if ($this->Application->isAdminUser) {
 			// this strange contraption actually uses temp table from object (when in temp mode)
 			$order_table = $object->TableName;
 			$item_table = str_replace('Orders', 'OrderItems', $order_table);
 		}
 
 		$sOrder = Array (
 			'FirstName' => $UserName[0],
 			'LastName' => $UserName[1],
 			'ShippingCompany' => $object->GetDBField('ShippingCompany'),
 			'ShippingAddress1' => $object->GetDBField('ShippingAddress1'),
 			'ShippingAddress2' => $object->GetDBField('ShippingAddress2'),
 			'ShippingCity' => $object->GetDBField('ShippingCity'),
 			'ShippingZip' => $object->GetDBField('ShippingZip'),
 			'ShippingCountry' => $ShippingCountry,
 			'ShippingPhone' => $this->PhoneClean($object->GetDBField('ShippingPhone')),
 			'ShippingFax' =>  $this->PhoneClean($object->GetDBField('ShippingFax')),
 			'ShippingNumBoxes' => '1',
 		);
 
 		$sql = 'SELECT SUM(`Quantity` * `Weight`)
 				FROM ' . $item_table . '
 				WHERE ' . $object->IDField . ' = ' . $object->GetID();
 		$weight = $this->Application->Conn->GetOne($sql);
 
 		$f_weight = kUtil::Kg2Pounds($weight);
 		$sOrder['ShippingWeight'] = $f_weight[0].'.'.$f_weight[1];
 
 		foreach ($ShippingInfo as $k => $ShippingRow) {
 			$ShippingCode = $ShippingRow['Code'];
 		}
 
 		if ( $object->GetDBField('ShippingCountry') == 'USA' ) {
 			$sOrder['ShippingState'] = $object->GetDBField('ShippingState');
 			$USPSMethod = $ShippingCode;
 			unset($sOrder['ShippingZip']);
 
 			$sOrder['ShippingZip5'] = substr(trim($object->GetDBField('ShippingZip')), 0, 5);
 			$sOrder['ShippingZip4'] = '';
 			$sOrder['SubTotal'] = $object->GetDBField('SubTotal');
 		}
 		else {
 			$USPSMethod = array_search($ShippingCode, $this->intl_types);
 			$sOrder['ShippingProvince'] = '';
 
 			if ( $ShippingCountry == 'CA' ) {
 				$sOrder['ShippingProvince'] = $object->GetField('ShippingState');
 			}
 
 			// add items
 			$sql = 'SELECT `Quantity`, `Weight`, `Price`
 					FROM ' . $item_table . '
 					WHERE ' . $object->IDField . ' = ' . $object->GetID();
 			$order_items = $this->Application->Conn->Query($sql);
 
 			$i = 1;
 			$Items = Array();
 
 			foreach ($order_items as $k => $order_item) {
 				$p_weight = Array();
 				$p_weight = kUtil::Kg2Pounds($order_item['Weight']);
 				$Items[$i] = Array('Qty' => $order_item['Quantity'], 'Price' => $order_item['Price'], 'NetPounds' => $p_weight[0], 'NetOunces' => $p_weight[1]);
 				$i++;
 			}
 
 			$sOrder['Items'] = $Items;
 			$sOrder['InvoiceNumber'] = $object->GetDBField('OrderNumber');
 		}
 
 		$sOrder['ShippingService'] = $USPSMethod;
 
 		// make USPS order
 		$this->order = $sOrder;
 		$usps_data = $this->PostOrder();
 
 		// if errors
 		if ( array_key_exists('error_number', $usps_data) ) {
 			return $usps_data;
 		}
 
 		if ( array_key_exists('Postage', $usps_data) ) {
 			$ShippingPrice = '';
 			$ShippingPrice = $usps_data['Postage'];
 		}
 
 		$ShippingTracking = '';
 
 		if ( isset($usps_data['TrackingNumber']) && $usps_data['TrackingNumber'] != '' ) {
 			$ShippingTracking = $usps_data['TrackingNumber'];
 		}
 
 		if ( isset($usps_data['PostnetBarCode']) && $usps_data['PostnetBarCode'] != '' && $ShippingTracking == '' ) {
 			$ShippingTracking = $usps_data['PostnetBarCode'];
 		}
 
 		if ($dry_run == false) {
 			$object->SetDBField('ShippingTracking', $ShippingTracking);
 			$object->Update();
 		}
 		else {
 			$full_path = USPS_LABEL_FOLDER . $ShippingTracking . ".pdf";
 
 			if (file_exists($full_path)) {
 				unlink($full_path);
 			}
 		}
 
 		return $usps_data;
 	}
-}
\ No newline at end of file
+}
Index: branches/5.2.x/units/product_option_combinations/product_option_combinations_event_handler.php
===================================================================
--- branches/5.2.x/units/product_option_combinations/product_option_combinations_event_handler.php	(revision 16692)
+++ branches/5.2.x/units/product_option_combinations/product_option_combinations_event_handler.php	(revision 16693)
@@ -1,448 +1,449 @@
 <?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 ProductOptionCombinationsEventHandler extends kDBEventHandler {
 
 	/**
 	 * Apply custom processing to item
 	 *
 	 * @param kEvent $event
 	 * @param string $type
 	 * @return void
 	 * @access protected
 	 */
 	protected function customProcessing(kEvent $event, $type)
 	{
 		if ( $type == 'after' ) {
 			return;
 		}
 
 		switch ($event->Name) {
 			case 'OnCreate':
 			case 'OnUpdate':
 				/** @var kDBItem $object */
 				$object = $event->getObject();
 
 				$options = unserialize($object->GetDBField('Combination'));
 				ksort($options);
 				$object->SetDBField('CombinationCRC', kUtil::crc32(serialize($options)));
 				break;
 
 			case 'OnMassDelete':
 				// delete only option combinations that has no associated inventory
 				/** @var kDBItem $object */
 				$object = $event->getObject();
 
 				$ids = $event->getEventParam('ids');
 
 				$sql = 'SELECT ' . $object->IDField . '
 						FROM ' . $object->TableName . '
 						WHERE 	(' . $object->IDField . ' IN (' . implode(',', $ids) . ')) AND
 								(QtyInStock = 0) AND (QtyReserved = 0) AND (QtyBackOrdered = 0) AND (QtyOnOrder = 0)';
 				$event->setEventParam('ids', $this->Conn->GetCol($sql));
 				break;
 		}
 	}
 
 	/**
 	 * GetOptionValues
 	 *
 	 * @param kEvent $event
 	 */
 	function GetOptionValues($event, $option_id)
 	{
 		$object = $event->getObject();
 		if ($object->IsTempTable()) {
 			$table = $this->Application->GetTempName(TABLE_PREFIX.'ProductOptions', 'prefix:'.$event->Prefix);
 		}
 		else {
 			$table = TABLE_PREFIX.'ProductOptions';
 		}
 		$query = 'SELECT `Values` FROM '.$table.' WHERE ProductOptionId = '.$option_id;
 		return explode(',', $this->Conn->GetOne($query));
 	}
 
 	function CreateCombinations(kEvent $event, $fields, $current_option=null)
 	{
 		$recursed = false;
 		$combination = $fields['Combination'];
 		foreach ($combination as $option_id => $option)
 		{
 			if ($option_id == $current_option || $recursed) continue;
 			if ($option == '_ANY_') {
 				$recursed = true;
 				$values = $this->GetOptionValues($event, $option_id);
 				foreach ($values as $a_value) {
 					$fields['Combination'][$option_id] = $a_value;
 					$this->CreateCombinations($event, $fields, $option_id);
 				}
 			}
 		}
 
 		if (!$recursed) {
 			/** @var kDBItem $object */
 			$object = $event->getObject();
 
 			$salt = $fields['Combination'];
 			ksort($salt);
 			$object->Load(kUtil::crc32(serialize($salt)), 'CombinationCRC');
 			$object->SetFieldsFromHash($fields);
 			$event->setEventParam('form_data', $fields);
 
 			$this->customProcessing($event,'before');
 			if ( $object->isLoaded() ) { // Update if such combination already exists
 				if( $object->Update() )
 				{
 					$this->customProcessing($event,'after');
 					$event->status=kEvent::erSUCCESS;
 				}
 			}
 			else {
 				if( $object->Create($event->getEventParam('ForceCreateId')) )
 				{
 					$this->customProcessing($event,'after');
 					$event->status=kEvent::erSUCCESS;
 				}
 			}
 		}
 	}
 
 	function UpdateCombinations(kEvent $event, $fields, $current_option=null)
 	{
 		$recursed = false;
 		$combination = $fields['Combination'];
 		foreach ($combination as $option_id => $option)
 		{
 			if ($option_id == $current_option || $recursed) continue;
 			if ($option == '_ANY_') {
 				$recursed = true;
 				$values = $this->GetOptionValues($event, $option_id);
 				foreach ($values as $a_value) {
 					$fields['Combination'][$option_id] = $a_value;
 					$this->UpdateCombinations($event, $fields, $option_id);
 				}
 			}
 		}
 
 		if (!$recursed) {
 			/** @var kDBItem $object */
 			$object = $event->getObject();
 
 			$edit_id = $object->GetId();
 			$salt = $fields['Combination'];
 			ksort($salt);
 			// try to load combination by salt - if loaded, it will update the combination
 			$object->Load(kUtil::crc32(serialize($salt)), 'CombinationCRC');
 			if ( !$object->isLoaded() ) {
 				$object->Load($edit_id);
 			}
 			$object->SetFieldsFromHash($fields);
 			$event->setEventParam('form_data', $fields);
 
 			$this->customProcessing($event,'before');
 			if( $object->Update() )
 			{
 				$this->customProcessing($event,'after');
 				$event->status=kEvent::erSUCCESS;
 			}
 		}
 	}
 
 	/**
 	 * Creates new kDBItem
 	 *
 	 * @param kEvent $event
 	 * @return void
 	 * @access protected
 	 */
 	protected function OnCreate(kEvent $event)
 	{
 		/** @var kDBItem $object */
 		$object = $event->getObject(Array ('skip_autoload' => true));
 
 		$items_info = $this->Application->GetVar($event->getPrefixSpecial(true));
 
 		if ( !$items_info ) {
 			return;
 		}
 
-		list($id, $field_values) = each($items_info);
+		$id = key($items_info);
+		$field_values = $items_info[$id];
 		$object->setID($id);
 		$object->SetFieldsFromHash($field_values);
 		$event->setEventParam('form_data', $field_values);
 
 		if ( !$object->Validate() ) {
 			$event->status = kEvent::erFAIL;
 			$event->redirect = false;
 			$this->Application->SetVar($event->getPrefixSpecial() . '_SaveEvent', 'OnCreate');
 			return;
 		}
 
 		$this->CreateCombinations($event, $field_values);
 	}
 
 	/**
 	 * Updates kDBItem
 	 *
 	 * @param kEvent $event
 	 * @return void
 	 * @access protected
 	 */
 	protected function OnUpdate(kEvent $event)
 	{
 		/** @var kDBItem $object */
 		$object = $event->getObject( Array('skip_autoload' => true) );
 
 		$items_info = $this->Application->GetVar( $event->getPrefixSpecial(true) );
 		if($items_info)
 		{
 			foreach($items_info as $id => $field_values)
 			{
 				$object->Load($id);
  				$object->SetFieldsFromHash($field_values);
 				$event->setEventParam('form_data', $field_values);
 
  				if (!$object->Validate()) {
 					$event->status = kEvent::erFAIL;
 					$event->redirect = false;
 					return;
 				}
 				$this->UpdateCombinations($event, $field_values);
 
  				/*$this->customProcessing($event, 'before');
 				if( $object->Update($id) )
 				{
 					$this->customProcessing($event, 'after');
 					$event->status=kEvent::erSUCCESS;
 				}
 				else
 				{
 					$event->status=kEvent::erFAIL;
 					$event->redirect=false;
 					break;
 				}*/
 			}
 		}
 		$this->Application->SetVar($event->GetPrefixSpecial().'_id', '');
 	}
 
 	/**
 	 * Builds item (loads if needed)
 	 *
 	 * Pattern: Prototype Manager
 	 *
 	 * @param kEvent $event
 	 * @access protected
 	 */
 	protected function OnItemBuild(kEvent $event)
 	{
 		/** @var kDBItem $object */
 		$object = $event->getObject();
 
 		$this->dbBuild($object, $event);
 
 		$sql = $this->ItemPrepareQuery($event);
 		$sql = $this->Application->ReplaceLanguageTags($sql);
 		$object->setSelectSQL($sql);
 
 		// 2. loads if allowed
 		$auto_load = $this->Application->getUnitOption($event->Prefix, 'AutoLoad');
 		$skip_autoload = $event->getEventParam('skip_autoload');
 
 		if ( $auto_load && !$skip_autoload ) {
 			$this->LoadItem($event);
 		}
 
 		/** @var Params $actions */
 		$actions = $this->Application->recallObject('kActions');
 
 		$actions->Set($event->getPrefixSpecial() . '_GoTab', '');
 		$actions->Set($event->getPrefixSpecial() . '_GoId', '');
 	}
 
 	/**
 	 * Load item if id is available
 	 *
 	 * @param kEvent $event
 	 * @return void
 	 * @access protected
 	 */
 	protected function LoadItem(kEvent $event)
 	{
 		/** @var kDBItem $object */
 		$object = $event->getObject();
 
 		$id = $this->getPassedID($event);
 
 		if ( !$id ) {
 			$event->CallSubEvent('OnNew');
 
 			return;
 		}
 
 		if ( $object->Load($id) ) {
 			/** @var Params $actions */
 			$actions = $this->Application->recallObject('kActions');
 
 			$actions->Set($event->getPrefixSpecial() . '_id', $object->GetId());
 		}
 	}
 
 	/**
 	 * Returns special of main item for linking with sub-item
 	 *
 	 * @param kEvent $event
 	 * @return string
 	 * @access protected
 	 */
 	protected function getMainSpecial(kEvent $event)
 	{
 		$special = $event->getEventParam('main_special');
 
 		if ( $special === false || $special == '$main_special' ) {
 			$special = $event->Special;
 		}
 
 		if ( $special == 'grid' ) {
 			$special = '';
 		}
 
 		return $special;
 	}
 
 	/**
 	 * Occurs before an item has been cloned
 	 * Id of newly created item is passed as event' 'id' param
 	 *
 	 * @param kEvent $event
 	 * @return void
 	 * @access protected
 	 */
 	protected function OnBeforeClone(kEvent $event)
 	{
 		parent::OnBeforeClone($event);
 
 		$event->Init($event->Prefix, '-item');
 
 		/** @var kDBItem $object */
 		$object = $event->getObject();
 
 		$options_mapping = $this->Application->GetVar('poc_mapping');
 		if ( !$options_mapping ) {
 			return;
 		}
 
 		foreach ($options_mapping as $original => $new) {
 			$n_combs = array ();
 			$comb_data = unserialize($object->GetDBField('Combination'));
 
 			foreach ($comb_data as $key => $val) {
 				$n_key = $key == $original ? $new : $key;
 				$n_combs[$n_key] = $val;
 			}
 
 			ksort($n_combs);
 			$n_combs = serialize($n_combs);
 			$n_crc = kUtil::crc32($n_combs);
 			$object->SetDBField('Combination', $n_combs);
 			$object->SetDBField('CombinationCRC', $n_crc);
 		}
 	}
 
 	/**
 	 * Restore back values from live table to temp table before overwriting live with temp
 	 *
 	 * @param kEvent $event
 	 * @return void
 	 * @access protected
 	 */
 	protected function OnBeforeDeleteFromLive(kEvent $event)
 	{
 		parent::OnBeforeDeleteFromLive($event);
 
 		// check if product inventory management is via options and then proceed
 
 		$id = $event->getEventParam('id');
 		$products_table = $this->Application->getUnitOption('p', 'TableName');
 		$table_name = $this->Application->getUnitOption($event->Prefix, 'TableName');
 		$id_field = $this->Application->getUnitOption($event->Prefix, 'IDField');
 
 		$sql = 'SELECT p.InventoryStatus
 				FROM ' . $products_table . ' p
 				LEFT JOIN ' . $table_name . ' poc ON poc.ProductId = p.ProductId
 				WHERE poc.' . $id_field . ' = ' . $id;
 		$inventory_status = $this->Conn->GetOne($sql);
 
 		if ( $inventory_status == ProductInventory::BY_OPTIONS ) {
 			/** @var kDBItem $live_object */
 			$live_object = $this->Application->recallObject($event->Prefix . '.itemlive', null, Array ('skip_autoload' => true));
 
 			$live_object->SwitchToLive();
 			$live_object->Load($id);
 
 			/** @var kDBItem $temp_object */
 			$temp_object = $this->Application->recallObject($event->Prefix . '.itemtemp', null, Array ('skip_autoload' => true));
 
 			$temp_object->SwitchToTemp();
 			$temp_object->Load($id);
 
 			$temp_object->SetDBFieldsFromHash($live_object->GetFieldValues(), Array ('QtyInStock', 'QtyReserved', 'QtyBackOrdered', 'QtyOnOrder'));
 			$temp_object->Update();
 		}
 	}
 
 	/**
 	 * Create search filters based on search query
 	 *
 	 * @param kEvent $event
 	 * @return void
 	 * @access protected
 	 */
 	protected function OnSearch(kEvent $event)
 	{
 		parent::OnSearch($event);
 
 		$this->_saveProduct($event);
 	}
 
 	/**
 	 * Clear search keywords
 	 *
 	 * @param kEvent $event
 	 * @return void
 	 * @access protected
 	 */
 	protected function OnSearchReset(kEvent $event)
 	{
 		parent::OnSearchReset($event);
 
 		$this->_saveProduct($event);
 	}
 
 	/**
 	 * Makes event remember product id (if passed)
 	 *
 	 * @param kEvent $event
 	 */
 	function _saveProduct($event)
 	{
 		$product_id = $this->Application->GetVar('p_id');
 
 		if ($product_id) {
 			$event->SetRedirectParam('p_id', $product_id);
 		}
 	}
 
 }
Index: branches/5.2.x/units/taxes/taxes_tag_processor.php
===================================================================
--- branches/5.2.x/units/taxes/taxes_tag_processor.php	(revision 16692)
+++ branches/5.2.x/units/taxes/taxes_tag_processor.php	(revision 16693)
@@ -1,273 +1,273 @@
 <?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 TaxesTagProcessor extends kDBTagProcessor
 {
 
 	function ShowCountries($params)
 	{
 		/** @var kDBItem $object */
 		$object = $this->getObject($params);
 
 		$destination_table = $this->getDestinationsTable($params);
 		$selected_country_id = (int)$this->Application->GetVar('CountrySelector');
 
 		$name_field = 'l' . $this->Application->GetVar('m_lang') . '_Name';
 		$id_field = $this->Application->getUnitOption('country-state', 'IDField');
 		$table_name = $this->Application->getUnitOption('country-state', 'TableName');
 
 		switch ($params['show']) {
 			case 'current':
 				// selected countries in current zone
 				$sql = 'SELECT cs.' . $name_field . ', cs.' . $id_field . '
 						FROM ' . $table_name . ' cs
 						LEFT JOIN ' . $destination_table . ' zd ON zd.StdDestId = cs.' . $id_field . '
 						WHERE cs.Type = ' . DESTINATION_TYPE_COUNTRY . ' AND zd.TaxZoneId = ' . $object->GetID() . '
 						ORDER BY cs.' . $name_field;
 				break;
 
 			case 'available':
 				// available countries in current zone
 				$sql = 'SELECT cs.' . $name_field . ', cs.' . $id_field . '
 						FROM ' . $table_name . ' cs
 						LEFT JOIN ' . $destination_table . ' zd ON zd.StdDestId = cs.' . $id_field . '
 						WHERE cs.Type = ' . DESTINATION_TYPE_COUNTRY . ' AND zd.TaxZoneId IS NULL
 						ORDER BY cs.' . $name_field;
 				break;
 
 			case 'all':
 				// always preselect 1st country, when user haven't selected any
 				if (!$selected_country_id) {
 					$sql = 'SELECT StdDestId
 							FROM ' . $destination_table . '
 							WHERE TaxZoneId = ' . $object->GetID();
 					$selected_country_id = $this->Conn->GetOne($sql);
 
 					if ($selected_country_id) {
 						$this->Application->SetVar('CountrySelector', $selected_country_id);
 					}
 				}
 
 				// all countries
 				$sql = 'SELECT ' . $name_field . ', ' . $id_field . '
 						FROM ' . $table_name . '
 						WHERE Type = ' . DESTINATION_TYPE_COUNTRY . '
 						ORDER BY ' . $name_field;
 				break;
 
 			case 'has_states':
 				/** @var kCountryStatesHelper $cs_helper */
 				$cs_helper = $this->Application->recallObject('CountryStatesHelper');
 
 				$has_states = $cs_helper->getCountriesWithStates();
 
 				if ($selected_country_id && !array_key_exists($selected_country_id, $has_states)) {
-					list ($selected_country_id, ) = each($has_states);
+					$selected_country_id = key($has_states);
 					$this->Application->SetVar('CountrySelector', $selected_country_id);
 				}
 
 				// preselect country from 1st found state
 				if (!$selected_country_id) {
 					$sql = 'SELECT cs.StateCountryId
 							FROM ' . $table_name . ' cs
 							LEFT JOIN ' . $destination_table . ' zd ON zd.StdDestId = cs.' . $id_field . '
 							WHERE (cs.Type = ' . DESTINATION_TYPE_STATE . ') AND (zd.TaxZoneId = ' . $object->GetID() . ')';
 					$selected_country_id = $this->Conn->GetOne($sql);
 
 					if ($selected_country_id) {
 						$this->Application->SetVar('CountrySelector', $selected_country_id);
 					}
 					else {
-						list ($selected_country_id, ) = each($has_states);
+						$selected_country_id = key($has_states);
 						$this->Application->SetVar('CountrySelector', $selected_country_id);
 					}
 				}
 
 				// gets only countries with states
 				$sql = 'SELECT ' . $name_field . ', ' . $id_field . '
 						FROM ' . $table_name . '
 						WHERE Type = ' . DESTINATION_TYPE_COUNTRY . ' AND ' . $id_field . ' IN (' . implode(',', array_keys($has_states)) . ')
 						ORDER BY ' . $name_field;
 				break;
 
 			default:
 				throw new Exception('Unknown "show" parameter value "' . $params['show'] . '" used');
 				break;
 		}
 
 		$ret = '';
 		$countries = $this->Conn->GetCol($sql, $id_field);
 
 		$block_params = $this->prepareTagParams($params);
 		$block_params['name'] = $params['block'];
 
 		foreach ($countries as $country_id => $country_name) {
 			$block_params['id'] = $country_id;
 			$block_params['destination_title'] = $country_name;
 			$block_params['selected'] = $selected_country_id == $country_id ? ' selected="selected"' : '';
 
 			$ret .= $this->Application->ParseBlock($block_params);
 		}
 
 		return $ret;
 	}
 
 	function ShowStates($params)
 	{
 		/** @var kDBItem $object */
 		$object = $this->getObject($params);
 
 		$destination_table = $this->getDestinationsTable($params);
 
 		$name_field = 'l' . $this->Application->GetVar('m_lang') . '_Name';
 		$id_field = $this->Application->getUnitOption('country-state', 'IDField');
 		$table_name = $this->Application->getUnitOption('country-state', 'TableName');
 
 		$country_id = $this->Application->GetVar('CountrySelector');
 
 		switch ($params['show']) {
 			case 'current':
 				// selected states for current country and zone
 				$sql = 'SELECT cs.' . $name_field . ', cs.' . $id_field . '
 						FROM ' . $table_name . ' cs
 						LEFT JOIN ' . $destination_table . ' zd ON zd.StdDestId = cs.' . $id_field . '
 						WHERE
 							cs.Type = ' . DESTINATION_TYPE_STATE . ' AND
 							cs.StateCountryId = ' . $country_id . ' AND
 							zd.TaxZoneId = ' . $object->GetID() . '
 						ORDER BY cs.' . $name_field;
 				break;
 
 			case 'available':
 				// available states for current country and zone
 				$sql = 'SELECT cs.' . $name_field . ', cs.' . $id_field . '
 						FROM ' . $table_name . ' cs
 						LEFT JOIN ' . $destination_table . ' zd ON zd.StdDestId = cs.' . $id_field . '
 						WHERE
 							cs.Type = ' . DESTINATION_TYPE_STATE . '
 							AND zd.TaxZoneId IS NULL
 							AND cs.StateCountryId = ' . $country_id . '
 						ORDER BY cs.' . $name_field;
 				break;
 
 			case 'all':
 				// all possible states for selected country
 				$sql = 'SELECT ' . $name_field . ', ' . $id_field . '
 						FROM ' . $table_name . '
 						WHERE Type = ' . DESTINATION_TYPE_STATE . ' AND StateCountryId = ' . $country_id . '
 						ORDER BY ' . $name_field;
 				break;
 
 			default:
 				throw new Exception('Unknown "show" parameter value "' . $params['show'] . '" used');
 				break;
 		}
 
 		$ret = '';
 		$states = $this->Conn->GetCol($sql, $id_field);
 
 		$block_params = $this->prepareTagParams($params);
 		$block_params['name'] = $params['block'];
 
 		foreach($states as $state_id => $state_name) {
 			$block_params['id'] = $state_id;
 			$block_params['destination_title'] = $state_name;
 
 			$ret .= $this->Application->ParseBlock($block_params);
 		}
 
 		return $ret;
 	}
 
 	function ShowZips($params)
 	{
 		/** @var kDBItem $object */
 		$object = $this->getObject($params);
 
 		$destination_table = $this->getDestinationsTable($params);
 
 		$country_id = (int)$this->Application->GetVar('CountrySelector');
 
 		$current_sql = 'SELECT DestValue
 						FROM ' . $destination_table . '
 						WHERE
 							COALESCE(DestValue, "") <> "" AND
 							TaxZoneId = ' . $object->GetID() . '
 						ORDER BY DestValue';
 
 		switch ($params['show']) {
 			case 'current':
 				$sql = $current_sql;
 				break;
 
 			case 'available':
 				$selected_zips = $this->Conn->GetCol($current_sql);
 				$selected_zips = $this->Conn->qstrArray($selected_zips);
 
 				$sql = 'SELECT DISTINCT DestValue
 						FROM ' . $this->Application->getUnitOption('taxdst', 'TableName') . '
 						WHERE
 							COALESCE(DestValue, "") <> "" AND
 							TaxZoneId <> ' . $object->GetID() . ' AND
 							' . ($selected_zips ? 'DestValue NOT IN (' . implode(',', $selected_zips) . ') AND' : '') . '
 							StdDestId = ' . $country_id . '
 							ORDER BY DestValue';
 				break;
 
 			default:
 				throw new Exception('Unknown "show" parameter value "' . $params['show'] . '" used');
 				break;
 		}
 
 		$zips = $this->Conn->GetCol($sql);
 
 		$ret = '';
 		$block_params = $this->prepareTagParams($params);
 		$block_params['name'] = $params['block'];
 
 		foreach($zips as $zip) {
 			$block_params['id'] = '0|' . $zip;
 			$block_params['destination_title'] = $zip;
 
 			$ret .= $this->Application->ParseBlock($block_params);
 		}
 
 		return $ret;
 	}
 
 	/**
 	 * Returns table for shipping zone destinations
 	 *
 	 * @param Array $params
 	 * @return string
 	 */
 	function getDestinationsTable($params)
 	{
 		static $table_name = '';
 
 		if (!$table_name) {
 			/** @var kDBItem $object */
 			$object = $this->getObject($params);
 
 			$table_name = $this->Application->getUnitOption('taxdst', 'TableName');
 
 			if ($object->IsTempTable()) {
 				$table_name = $this->Application->GetTempName($table_name, 'prefix:' . $this->Prefix);
 			}
 		}
 
 		return $table_name;
 	}
-}
\ No newline at end of file
+}
Index: branches/5.2.x/units/coupons/coupons_event_handler.php
===================================================================
--- branches/5.2.x/units/coupons/coupons_event_handler.php	(revision 16692)
+++ branches/5.2.x/units/coupons/coupons_event_handler.php	(revision 16693)
@@ -1,235 +1,236 @@
 <?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 CouponsEventHandler extends kDBEventHandler {
 
 	/**
 	 * Allows to override standard permission mapping
 	 *
 	 * @return void
 	 * @access protected
 	 * @see kEventHandler::$permMapping
 	 */
 	protected function mapPermissions()
 	{
 		parent::mapPermissions();
 
 		$permissions = Array (
 			'OnItemBuild' => Array ('self' => true),
 			'OnApplyClone' => Array ('self' => 'add'),
 			'OnPrepareClone' => Array ('self' => 'view'),
 		);
 
 		$this->permMapping = array_merge($this->permMapping, $permissions);
 	}
 
 	/**
 	 * Prepares coupon cloning
 	 *
 	 * @param kEvent $event
 	 */
 	function OnPrepareClone($event)
 	{
 		$this->StoreSelectedIDs($event);
 
 		$event->CallSubEvent('OnNew');
 
 		/** @var kDBItem $object */
 		$object = $event->getObject();
 
 		$this->setCloningRequired($object);
 		$clone_count = $this->Application->RecallVar('CoupLastCloneCount');
 
 		if ( is_numeric($clone_count) && $clone_count > 0 ) {
 			$object->SetDBField('CouponCount', $clone_count);
 		}
 
 		$expire_days = $this->Application->ConfigValue('Comm_DefaultCouponDuration');
 		$default_expiration = strtotime('+' . $expire_days . ' days');
 
 		$object->SetDBField('DefaultExpiration_date', $default_expiration);
 		$object->SetDBField('DefaultExpiration_time', $default_expiration);
 	}
 
 	/**
 	 * Occurs before an item has been cloned
 	 * Id of newly created item is passed as event' 'id' param
 	 *
 	 * @param kEvent $event
 	 * @return void
 	 * @access protected
 	 */
 	protected function OnBeforeClone(kEvent $event)
 	{
 		parent::OnBeforeClone($event);
 
 		/** @var kDBItem $object */
 		$object = $event->getObject();
 
 		$this->SetNewCode($object);
 
 		$object->SetDBField('LastUsedBy', NULL);
 		$object->SetDBField('LastUsedOn', NULL);
 		$object->SetDBField('NumberOfUses', NULL);
 
 		$expiration = $this->Application->GetVar('clone_coupon_expiration');
 		$object->SetDBField('Expiration_date', $expiration);
 		$object->SetDBField('Expiration_time', $expiration);
 	}
 
 	function OnApplyClone(kEvent $event)
 	{
 		if ($this->Application->CheckPermission('SYSTEM_ACCESS.READONLY', 1)) {
 			$event->status = kEvent::erFAIL;
 			return;
 		}
 
 		/** @var kDBItem $object */
 		$object = $event->getObject( Array ('skip_autoload' => true) );
 
 		$this->setCloningRequired($object);
 
 		$items_info = $this->Application->GetVar( $event->getPrefixSpecial(true) );
-		list($id, $field_values) = each($items_info);
+		$id = key($items_info);
+		$field_values = $items_info[$id];
 		$object->setID($id);
 		$object->SetFieldsFromHash($field_values);
 		$event->setEventParam('form_data', $field_values);
 
 		if ( !$object->Validate() ) {
 			$event->status = kEvent::erFAIL;
 			return ;
 		}
 
 		/** @var kTempTablesHandler $temp */
 		$temp = $this->Application->recallObject($event->getPrefixSpecial().'_TempHandler', 'kTempTablesHandler');
 
 		$original_coupon_ids = $this->getSelectedIDs($event, true);
 		$clone_count = $object->GetDBField('CouponCount');
 		$this->Application->StoreVar('CoupLastCloneCount', $clone_count);
 		$this->Application->SetVar('clone_coupon_expiration', $object->GetDBField('DefaultExpiration'));
 
 		for ($i = 0; $i < $clone_count; $i++) {
 			$temp->CloneItems($event->Prefix, $event->Special, $original_coupon_ids);
 		}
 
 		$event->SetRedirectParam('opener', 'u');
 	}
 
 	/**
 	 * Sets fields required during coupon cloning
 	 *
 	 * @param kDBItem $object
 	 * @return void
 	 * @access protected
 	 */
 	protected function setCloningRequired(&$object)
 	{
 		$this->RemoveRequiredFields($object);
 		$object->setRequired('CouponCount');
 		$object->setRequired('DefaultExpiration');
 	}
 
 	function SetNewCode(&$item)
 	{
 		do{
 			$new_code = $this->RandomCouponCode();
 			$exists = $this->Conn->GetOne('SELECT COUNT(*) FROM '.TABLE_PREFIX.'ProductsCoupons WHERE Code='.$this->Conn->qstr($new_code));
 			if ($exists){
 				$new_code = false;
 			}
 		} while (!$new_code);
 
 		$item->SetDBField('Code', $new_code);
 	}
 
 	function RandomCouponCode()
 	{
 		$rand_code = '';
 
 		for ($i = 0; $i < 10; $i++) {
 			$is_letter = rand(0, 1);
 			$rand_code .= ($is_letter ? chr(rand(65, 90)) : rand(0, 9));
 		}
 
 		return $rand_code;
 	}
 
 	/**
 	 * 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();
 
 		$exp_date = adodb_mktime();
 		$default_duration = $this->Application->ConfigValue('Comm_DefaultCouponDuration');
 
 		if ( $default_duration && $default_duration > 0 ) {
 			$exp_date += (int)$default_duration * 86400;
 		}
 
 		$object->SetDBField('Expiration_date', $exp_date);
 	}
 
 	/**
 	 * Occurs before updating item
 	 *
 	 * @param kEvent $event
 	 * @return void
 	 * @access protected
 	 */
 	protected function OnBeforeItemUpdate(kEvent $event)
 	{
 		parent::OnBeforeItemUpdate($event);
 
 		$this->itemChanged($event);
 	}
 
 	/**
 	 * Occurs before creating item
 	 *
 	 * @param kEvent $event
 	 * @return void
 	 * @access protected
 	 */
 	protected function OnBeforeItemCreate(kEvent $event)
 	{
 		parent::OnBeforeItemCreate($event);
 
 		$this->itemChanged($event);
 	}
 
 	/**
 	 * Occurs before item is changed
 	 *
 	 * @param kEvent $event
 	 */
 	function itemChanged($event)
 	{
 		/** @var kDBItem $object */
 		$object = $event->getObject();
 
 		$object->SetDBField('Amount', abs($object->GetDBField('Amount')));
 	}
 }
Index: branches/5.2.x/units/zones/zones_tag_processor.php
===================================================================
--- branches/5.2.x/units/zones/zones_tag_processor.php	(revision 16692)
+++ branches/5.2.x/units/zones/zones_tag_processor.php	(revision 16693)
@@ -1,272 +1,272 @@
 <?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 ZonesTagProcessor extends kDBTagProcessor
 {
 	function ShowCountries($params)
 	{
 		/** @var kDBItem $object */
 		$object = $this->getObject($params);
 
 		$destination_table = $this->getDestinationsTable($params);
 		$selected_country_id = (int)$this->Application->GetVar('CountrySelector');
 
 		$name_field = 'l' . $this->Application->GetVar('m_lang') . '_Name';
 		$id_field = $this->Application->getUnitOption('country-state', 'IDField');
 		$table_name = $this->Application->getUnitOption('country-state', 'TableName');
 
 		switch ($params['show']) {
 			case 'current':
 				// selected countries in current zone
 				$sql = 'SELECT cs.' . $name_field . ', cs.' . $id_field . '
 						FROM ' . $table_name . ' cs
 						LEFT JOIN ' . $destination_table . ' zd ON zd.StdDestId = cs.' . $id_field . '
 						WHERE cs.Type = ' . DESTINATION_TYPE_COUNTRY . ' AND zd.ShippingZoneId = ' . $object->GetID() . '
 						ORDER BY cs.' . $name_field;
 				break;
 
 			case 'available':
 				// available countries in current zone
 				$sql = 'SELECT cs.' . $name_field . ', cs.' . $id_field . '
 						FROM ' . $table_name . ' cs
 						LEFT JOIN ' . $destination_table . ' zd ON zd.StdDestId = cs.' . $id_field . '
 						WHERE cs.Type = ' . DESTINATION_TYPE_COUNTRY . ' AND zd.ShippingZoneId IS NULL
 						ORDER BY cs.' . $name_field;
 				break;
 
 			case 'all':
 				// always preselect 1st country, when user haven't selected any
 				if (!$selected_country_id) {
 					$sql = 'SELECT StdDestId
 							FROM ' . $destination_table . '
 							WHERE ShippingZoneId = ' . $object->GetID();
 					$selected_country_id = $this->Conn->GetOne($sql);
 
 					if ($selected_country_id) {
 						$this->Application->SetVar('CountrySelector', $selected_country_id);
 					}
 				}
 
 				// all countries
 				$sql = 'SELECT ' . $name_field . ', ' . $id_field . '
 						FROM ' . $table_name . '
 						WHERE Type = ' . DESTINATION_TYPE_COUNTRY . '
 						ORDER BY ' . $name_field;
 				break;
 
 			case 'has_states':
 				/** @var kCountryStatesHelper $cs_helper */
 				$cs_helper = $this->Application->recallObject('CountryStatesHelper');
 
 				$has_states = $cs_helper->getCountriesWithStates();
 
 				if ($selected_country_id && !array_key_exists($selected_country_id, $has_states)) {
-					list ($selected_country_id, ) = each($has_states);
+					$selected_country_id = key($has_states);
 					$this->Application->SetVar('CountrySelector', $selected_country_id);
 				}
 
 				// preselect country from 1st found state
 				if (!$selected_country_id) {
 					$sql = 'SELECT cs.StateCountryId
 							FROM ' . $table_name . ' cs
 							LEFT JOIN ' . $destination_table . ' zd ON zd.StdDestId = cs.' . $id_field . '
 							WHERE (cs.Type = ' . DESTINATION_TYPE_STATE . ') AND (zd.ShippingZoneId = ' . $object->GetID() . ')';
 					$selected_country_id = $this->Conn->GetOne($sql);
 
 					if ($selected_country_id) {
 						$this->Application->SetVar('CountrySelector', $selected_country_id);
 					}
 					else {
-						list ($selected_country_id, ) = each($has_states);
+						$selected_country_id = key($has_states);
 						$this->Application->SetVar('CountrySelector', $selected_country_id);
 					}
 				}
 
 				// gets only countries with states
 				$sql = 'SELECT ' . $name_field . ', ' . $id_field . '
 						FROM ' . $table_name . '
 						WHERE Type = ' . DESTINATION_TYPE_COUNTRY . ' AND ' . $id_field . ' IN (' . implode(',', array_keys($has_states)) . ')
 						ORDER BY ' . $name_field;
 				break;
 
 			default:
 				throw new Exception('Unknown "show" parameter value "' . $params['show'] . '" used');
 				break;
 		}
 
 		$ret = '';
 		$countries = $this->Conn->GetCol($sql, $id_field);
 
 		$block_params = $this->prepareTagParams($params);
 		$block_params['name'] = $params['block'];
 
 		foreach ($countries as $country_id => $country_name) {
 			$block_params['id'] = $country_id;
 			$block_params['destination_title'] = $country_name;
 			$block_params['selected'] = $selected_country_id == $country_id ? ' selected="selected"' : '';
 
 			$ret .= $this->Application->ParseBlock($block_params);
 		}
 
 		return $ret;
 	}
 
 	function ShowStates($params)
 	{
 		/** @var kDBItem $object */
 		$object = $this->getObject($params);
 
 		$destination_table = $this->getDestinationsTable($params);
 
 		$name_field = 'l' . $this->Application->GetVar('m_lang') . '_Name';
 		$id_field = $this->Application->getUnitOption('country-state', 'IDField');
 		$table_name = $this->Application->getUnitOption('country-state', 'TableName');
 
 		$country_id = $this->Application->GetVar('CountrySelector');
 
 		switch ($params['show']) {
 			case 'current':
 				// selected states for current country and zone
 				$sql = 'SELECT cs.' . $name_field . ', cs.' . $id_field . '
 						FROM ' . $table_name . ' cs
 						LEFT JOIN ' . $destination_table . ' zd ON zd.StdDestId = cs.' . $id_field . '
 						WHERE
 							cs.Type = ' . DESTINATION_TYPE_STATE . ' AND
 							cs.StateCountryId = ' . $country_id . ' AND
 							zd.ShippingZoneId = ' . $object->GetID() . '
 						ORDER BY cs.' . $name_field;
 				break;
 
 			case 'available':
 				// available states for current country and zone
 				$sql = 'SELECT cs.' . $name_field . ', cs.' . $id_field . '
 						FROM ' . $table_name . ' cs
 						LEFT JOIN ' . $destination_table . ' zd ON zd.StdDestId = cs.' . $id_field . '
 						WHERE
 							cs.Type = ' . DESTINATION_TYPE_STATE . '
 							AND zd.ShippingZoneId IS NULL
 							AND cs.StateCountryId = ' . $country_id . '
 						ORDER BY cs.' . $name_field;
 				break;
 
 			case 'all':
 				// all possible states for selected country
 				$sql = 'SELECT ' . $name_field . ', ' . $id_field . '
 						FROM ' . $table_name . '
 						WHERE Type = ' . DESTINATION_TYPE_STATE . ' AND StateCountryId = ' . $country_id . '
 						ORDER BY ' . $name_field;
 				break;
 
 			default:
 				throw new Exception('Unknown "show" parameter value "' . $params['show'] . '" used');
 				break;
 		}
 
 		$ret = '';
 		$states = $this->Conn->GetCol($sql, $id_field);
 
 		$block_params = $this->prepareTagParams($params);
 		$block_params['name'] = $params['block'];
 
 		foreach($states as $state_id => $state_name) {
 			$block_params['id'] = $state_id;
 			$block_params['destination_title'] = $state_name;
 
 			$ret .= $this->Application->ParseBlock($block_params);
 		}
 
 		return $ret;
 	}
 
 	function ShowZips($params)
 	{
 		/** @var kDBItem $object */
 		$object = $this->getObject($params);
 
 		$destination_table = $this->getDestinationsTable($params);
 
 		$country_id = (int)$this->Application->GetVar('CountrySelector');
 
 		$current_sql = 'SELECT DestValue
 						FROM ' . $destination_table . '
 						WHERE
 							COALESCE(DestValue, "") <> "" AND
 							ShippingZoneID = ' . $object->GetID() . '
 						ORDER BY DestValue';
 
 		switch ($params['show']) {
 			case 'current':
 				$sql = $current_sql;
 				break;
 
 			case 'available':
 				$selected_zips = $this->Conn->GetCol($current_sql);
 				$selected_zips = $this->Conn->qstrArray($selected_zips);
 
 				$sql = 'SELECT DISTINCT DestValue
 						FROM ' . $this->Application->getUnitOption('dst', 'TableName') . '
 						WHERE
 							COALESCE(DestValue, "") <> "" AND
 							ShippingZoneID <> ' . $object->GetID() . ' AND
 							' . ($selected_zips ? 'DestValue NOT IN (' . implode(',', $selected_zips) . ') AND' : '') . '
 							StdDestId = ' . $country_id . '
 							ORDER BY DestValue';
 				break;
 
 			default:
 				throw new Exception('Unknown "show" parameter value "' . $params['show'] . '" used');
 				break;
 		}
 
 		$zips = $this->Conn->GetCol($sql);
 
 		$ret = '';
 		$block_params = $this->prepareTagParams($params);
 		$block_params['name'] = $params['block'];
 
 		foreach($zips as $zip) {
 			$block_params['id'] = '0|' . $zip;
 			$block_params['destination_title'] = $zip;
 
 			$ret .= $this->Application->ParseBlock($block_params);
 		}
 
 		return $ret;
 	}
 
 	/**
 	 * Returns table for shipping zone destinations
 	 *
 	 * @param Array $params
 	 * @return string
 	 */
 	function getDestinationsTable($params)
 	{
 		static $table_name = '';
 
 		if (!$table_name) {
 			/** @var kDBItem $object */
 			$object = $this->getObject($params);
 
 			$table_name = $this->Application->getUnitOption('dst', 'TableName');
 
 			if ($object->IsTempTable()) {
 				$table_name = $this->Application->GetTempName($table_name, 'prefix:' . $this->Prefix);
 			}
 		}
 
 		return $table_name;
 	}
-}
\ No newline at end of file
+}
Index: branches/5.2.x/units/orders/orders_event_handler.php
===================================================================
--- branches/5.2.x/units/orders/orders_event_handler.php	(revision 16692)
+++ branches/5.2.x/units/orders/orders_event_handler.php	(revision 16693)
@@ -1,4116 +1,4117 @@
 <?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 OrdersEventHandler extends kDBEventHandler
 {
 
 	/**
 	 * Checks user permission to execute given $event
 	 *
 	 * @param kEvent $event
 	 * @return bool
 	 * @access public
 	 */
 	public function CheckPermission(kEvent $event)
 	{
 		if ( !$this->Application->isAdminUser ) {
 			if ( $event->Name == 'OnCreate' ) {
 				// user can't initiate custom order creation directly
 				return false;
 			}
 
 			$user_id = $this->Application->RecallVar('user_id');
 			$items_info = $this->Application->GetVar($event->getPrefixSpecial(true));
 			if ( $items_info ) {
 				// when POST is present, then check when is beeing submitted
 				$order_session_id = $this->Application->RecallVar($event->getPrefixSpecial(true) . '_id');
 
 				/** @var OrdersItem $order_dummy */
 				$order_dummy = $this->Application->recallObject($event->Prefix . '.-item', null, Array ('skip_autoload' => true));
 
 				foreach ($items_info as $id => $field_values) {
 					if ( $order_session_id != $id ) {
 						// user is trying update not his order, even order from other guest
 						return false;
 					}
 
 					$order_dummy->Load($id);
 
 					// session_id matches order_id from submit
 					if ( $order_dummy->GetDBField('PortalUserId') != $user_id ) {
 						// user performs event on other user order
 						return false;
 					}
 
 					$status_field = $order_dummy->getStatusField();
 
 					if ( isset($field_values[$status_field]) && $order_dummy->GetDBField($status_field) != $field_values[$status_field] ) {
 						// user can't change status by himself
 						return false;
 					}
 
 					if ( $order_dummy->GetDBField($status_field) != ORDER_STATUS_INCOMPLETE ) {
 						// user can't edit orders being processed
 						return false;
 					}
 				}
 			}
 		}
 
 		if ( $event->Name == 'OnQuietPreSave' ) {
 			$section = $event->getSection();
 
 			if ( $this->isNewItemCreate($event) ) {
 				return $this->Application->CheckPermission($section . '.add', 1);
 			}
 			else {
 				return $this->Application->CheckPermission($section . '.add', 1) || $this->Application->CheckPermission($section . '.edit', 1);
 			}
 		}
 
 		return parent::CheckPermission($event);
 	}
 
 	/**
 	 * Allows to override standard permission mapping
 	 *
 	 * @return void
 	 * @access protected
 	 * @see kEventHandler::$permMapping
 	 */
 	protected function mapPermissions()
 	{
 		parent::mapPermissions();
 
 		$permissions = Array (
 			// admin
 			'OnRecalculateItems'	=>	Array('self' => 'add|edit'),
 			'OnResetToUser'			=>	Array('self' => 'add|edit'),
 			'OnResetToBilling'		=>	Array('self' => 'add|edit'),
 			'OnResetToShipping'		=>	Array('self' => 'add|edit'),
 			'OnMassOrderApprove'	=>	Array('self' => 'advanced:approve'),
 			'OnMassOrderDeny'		=>	Array('self' => 'advanced:deny'),
 			'OnMassOrderArchive'	=>	Array('self' => 'advanced:archive'),
 			'OnMassPlaceOrder'		=>	Array('self' => 'advanced:place'),
 			'OnMassOrderProcess'	=>	Array('self' => 'advanced:process'),
 			'OnMassOrderShip'		=>	Array('self' => 'advanced:ship'),
 			'OnResetToPending'		=>	Array('self' => 'advanced:reset_to_pending'),
 			'OnLoadSelected'		=>	Array('self' => 'view'),	// print in this case
 			'OnGoToOrder'			=>	Array('self' => 'view'),
 
 			// front-end
 			'OnViewCart'			=>	Array('self' => true),
 			'OnAddToCart'			=>	Array('self' => true),
 			'OnRemoveFromCart'		=>	Array('self' => true),
 			'OnUpdateCart'			=>	Array('self' => true),
 			'OnUpdateCartJSON'		=>	Array('self' => true),
 			'OnUpdateItemOptions'	=>	Array('self' => true),
 			'OnCleanupCart'			=>	Array('self' => true),
 			'OnContinueShopping'	=>	Array('self' => true),
 			'OnCheckout'			=>	Array('self' => true),
 			'OnSelectAddress'		=>	Array('self' => true),
 			'OnProceedToBilling'	=>	Array('self' => true),
 			'OnProceedToPreview'	=>	Array('self' => true),
 			'OnProceedToPreviewAjax' => array('self' => true),
 			'OnCompleteOrder'		=>	Array('self' => true),
 			'OnUpdate' => array('self' => true),
 			'OnUpdateAjax'			=>	Array('self' => true),
 
 			'OnRemoveCoupon'		=>	Array('self' => true),
 			'OnRemoveGiftCertificate'		=>	Array('self' => true),
 
 			'OnCancelRecurring'		=>	Array('self' => true),
 			'OnAddVirtualProductToCart'		=>	Array('self' => true),
 			'OnItemBuild'		=>	Array('self' => true),
 			'OnDownloadLabel' 	=>  Array('self' => true, 'subitem' => true),
 		);
 
 		$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();
 
 		$common_events = Array (
 			'OnResetToUser'		=>	'OnResetAddress',
 			'OnResetToBilling'	=>	'OnResetAddress',
 			'OnResetToShipping'	=>	'OnResetAddress',
 
 			'OnMassOrderProcess'	=>	'MassInventoryAction',
 			'OnMassOrderApprove'	=>	'MassInventoryAction',
 			'OnMassOrderDeny'		=>	'MassInventoryAction',
 			'OnMassOrderArchive'	=>	'MassInventoryAction',
 			'OnMassOrderShip'		=>	'MassInventoryAction',
 
 			'OnOrderProcess'	=>	'InventoryAction',
 			'OnOrderApprove'	=>	'InventoryAction',
 			'OnOrderDeny'		=>	'InventoryAction',
 			'OnOrderArchive'	=>	'InventoryAction',
 			'OnOrderShip'		=>	'InventoryAction',
 		);
 
 		$this->eventMethods = array_merge($this->eventMethods, $common_events);
 	}
 
 	/* ======================== FRONT ONLY ======================== */
 
 	function OnQuietPreSave($event)
 	{
 		/** @var kDBItem $object */
 		$object = $event->getObject();
 
 		$object->IgnoreValidation = true;
 		$event->CallSubEvent('OnPreSave');
 		$object->IgnoreValidation = false;
 	}
 
 	/**
 	 * Sets new address to order
 	 *
 	 * @param kEvent $event
 	 */
 	function OnSelectAddress($event)
 	{
 		if ($this->Application->isAdminUser) {
 			return ;
 		}
 
 		/** @var OrdersItem $object */
 		$object = $event->getObject();
 
 		$shipping_address_id = $this->Application->GetVar('shipping_address_id');
 		$billing_address_id = $this->Application->GetVar('billing_address_id');
 
 		if ($shipping_address_id || $billing_address_id) {
 			/** @var kCountryStatesHelper $cs_helper */
 			$cs_helper = $this->Application->recallObject('CountryStatesHelper');
 
 			/** @var AddressesItem $address */
 			$address = $this->Application->recallObject('addr.-item','addr', Array('skip_autoload' => true));
 
 			/** @var AddressesList $addr_list */
 			$addr_list = $this->Application->recallObject('addr', 'addr_List', Array('per_page'=>-1, 'skip_counting'=>true) );
 
 			$addr_list->Query();
 		}
 
 		if ($shipping_address_id > 0) {
 			$addr_list->CopyAddress($shipping_address_id, 'Shipping');
 			$address->Load($shipping_address_id);
 			$address->MarkAddress('Shipping');
 
 			$cs_helper->PopulateStates($event, 'ShippingState', 'ShippingCountry');
 			$object->setRequired('ShippingState', false);
 		}
 		elseif ($shipping_address_id == -1) {
 			$object->ResetAddress('Shipping');
 		}
 
 		if ($billing_address_id > 0) {
 			$addr_list->CopyAddress($billing_address_id, 'Billing');
 			$address->Load($billing_address_id);
 			$address->MarkAddress('Billing');
 
 			$cs_helper->PopulateStates($event, 'BillingState', 'BillingCountry');
 			$object->setRequired('BillingState', false);
 		}
 		elseif ($billing_address_id == -1) {
 			$object->ResetAddress('Billing');
 		}
 
 		$event->redirect = false;
 
 		$object->IgnoreValidation = true;
 		$this->RecalculateTax($event);
 		$object->Update();
 	}
 
 	/**
 	 * Updates order with registred user id
 	 *
 	 * @param kEvent $event
 	 */
 	function OnUserCreate($event)
 	{
 		if( !($event->MasterEvent->status == kEvent::erSUCCESS) ) return false;
 
 		$ses_id = $this->Application->RecallVar('front_order_id');
 		if($ses_id)
 		{
 			$this->updateUserID($ses_id, $event);
 			$this->Application->RemoveVar('front_order_id');
 		}
 	}
 
 	/**
 	 * Updates shopping cart with logged-in user details
 	 *
 	 * @param kEvent $event
 	 * @return void
 	 * @access protected
 	 */
 	protected function OnUserLogin($event)
 	{
 		if ( ($event->MasterEvent->status != kEvent::erSUCCESS) || kUtil::constOn('IS_INSTALL') ) {
 			// login failed OR login during installation
 			return;
 		}
 
 		$ses_id = $this->Application->RecallVar('ord_id');
 
 		if ( $ses_id ) {
 			$this->updateUserID($ses_id, $event);
 		}
 
 		$user_id = $this->Application->RecallVar('user_id');
 		$affiliate_id = $this->isAffiliate($user_id);
 
 		if ( $affiliate_id ) {
 			$this->Application->setVisitField('AffiliateId', $affiliate_id);
 		}
 
 		$event->CallSubEvent('OnRecalculateItems');
 	}
 
 	/**
 	 * Puts ID of just logged-in user into current order
 	 *
 	 * @param int $order_id
 	 * @param kEvent $event
 	 * @return void
 	 */
 	function updateUserID($order_id, $event)
 	{
 		/** @var UsersItem $user */
 		$user = $this->Application->recallObject('u.current');
 
 		$affiliate_id = $this->isAffiliate( $user->GetID() );
 
 		$fields_hash = Array (
 			'PortalUserId' => $user->GetID(),
 			'BillingEmail' => $user->GetDBField('Email'),
 		);
 
 		if ( $affiliate_id ) {
 			$fields_hash['AffiliateId'] = $affiliate_id;
 		}
 
 		/** @var OrdersItem $object */
 		$object = $this->Application->recallObject($event->Prefix . '.-item', null, array('skip_autoload' => true));
 		$object->Load($order_id);
 
 		if ( $object->isLoaded() ) {
 			$object->SetDBFieldsFromHash($fields_hash);
 			$object->Update();
 		}
 	}
 
 	function isAffiliate($user_id)
 	{
 		/** @var kDBItem $affiliate_user */
 		$affiliate_user = $this->Application->recallObject('affil.-item', null, Array('skip_autoload' => true) );
 
 		$affiliate_user->Load($user_id, 'PortalUserId');
 
 		return $affiliate_user->isLoaded() ? $affiliate_user->GetDBField('AffiliateId') : 0;
 	}
 
 	/**
 	 * Charge order
 	 *
 	 * @param OrdersItem $order
 	 * @return Array
 	 */
 	function ChargeOrder(&$order)
 	{
 		$gw_data = $order->getGatewayData();
 
 		/** @var kGWBase $gateway_object */
 		$gateway_object = $this->Application->recallObject($gw_data['ClassName']);
 
 		$payment_result = $gateway_object->DirectPayment($order->GetFieldValues(), $gw_data['gw_params']);
 		$sql = 'UPDATE %s SET GWResult1 = %s WHERE %s = %s';
 		$sql = sprintf($sql, $order->TableName, $this->Conn->qstr($gateway_object->getGWResponce()), $order->IDField, $order->GetID() );
 		$this->Conn->Query($sql);
 		$order->SetDBField('GWResult1', $gateway_object->getGWResponce() );
 
 		return array('result'=>$payment_result, 'data'=>$gateway_object->parsed_responce, 'gw_data' => $gw_data, 'error_msg'=>$gateway_object->getErrorMsg());
 	}
 
 	/**
 	 * Returns parameters, used to send order-related e-mails
 	 *
 	 * @param OrdersItem $order
 	 * @return array
 	 */
 	function OrderEmailParams(&$order)
 	{
 		$billing_email = $order->GetDBField('BillingEmail');
 
 		$sql = 'SELECT Email
 				FROM ' . $this->Application->getUnitOption('u', 'TableName') . '
 				WHERE PortalUserId = ' . $order->GetDBField('PortalUserId');
 		$user_email = $this->Conn->GetOne($sql);
 
 		$ret = Array (
 			'_user_email' => $user_email, // for use when shipping vs user is required in InventoryAction
 			'to_name' => $order->GetDBField('BillingTo'),
 			'to_email' => $billing_email ? $billing_email : $user_email,
 		);
 
 		return $ret;
 	}
 
 	function PrepareCoupons($event, &$order)
 	{
 		/** @var kDBList $order_items */
 		$order_items = $this->Application->recallObject('orditems.-inv','orditems_List',Array('skip_counting'=>true,'per_page'=>-1) );
 
 		$order_items->linkToParent($order->Special);
 		$order_items->Query();
 		$order_items->GoFirst();
 
 		$assigned_coupons = array();
 		$coup_handler = $this->Application->recallObject('coup_EventHandler');
 		foreach($order_items->Records as $product_item)
 		{
 			if ($product_item['ItemData']) {
 				$item_data = unserialize($product_item['ItemData']);
 				if (isset($item_data['AssignedCoupon']) && $item_data['AssignedCoupon']) {
 					$coupon_id = $item_data['AssignedCoupon'];
 					// clone coupon, get new coupon ID
 					/** @var kDBItem $coupon */
 					$coupon = $this->Application->recallObject('coup',null,array('skip_autload' => true));
 
 					$coupon->Load($coupon_id);
 					if (!$coupon->isLoaded()) continue;
 
 					$coup_handler->SetNewCode($coupon);
 					$coupon->NameCopy();
 					$coupon->SetDBField('Name', $coupon->GetDBField('Name').' (Order #'.$order->GetField('OrderNumber').')');
 					$coupon->Create();
 
 					// add coupon code to array
 					array_push($assigned_coupons, $coupon->GetDBField('Code'));
 				}
 			}
 		}
 
 		/** @var OrdersItem $order */
 		if ($assigned_coupons) {
 			$comments = $order->GetDBField('AdminComment');
 			if ($comments) $comments .= "\r\n";
 			$comments .= "Issued coupon(s): ". join(',', $assigned_coupons);
 			$order->SetDBField('AdminComment', $comments);
 			$order->Update();
 		}
 
 		if ($assigned_coupons) $this->Application->SetVar('order_coupons', join(',', $assigned_coupons));
 	}
 
 	/**
 	 * Completes order if possible
 	 *
 	 * @param kEvent $event
 	 * @return bool
 	 */
 	function OnCompleteOrder($event)
 	{
 		$this->LockTables($event);
 		$reoccurring_order = substr($event->Special, 0, 9) == 'recurring';
 
 		if ( !$reoccurring_order && !$this->CheckQuantites($event) ) {
 			// don't check quantities (that causes recalculate) for reoccurring orders
 			return;
 		}
 
 		$this->ReserveItems($event);
 
 		/** @var OrdersItem $order */
 		$order = $event->getObject();
 
 		$charge_result = $this->ChargeOrder($order);
 
 		if (!$charge_result['result']) {
 			$this->FreeItems($event);
 			$this->Application->StoreVar('gw_error', $charge_result['error_msg']);
 
 			//$this->Application->StoreVar('gw_error', getArrayValue($charge_result, 'data', 'responce_reason_text') );
 			$event->redirect = $this->Application->GetVar('failure_template');
 			$event->SetRedirectParam('m_cat_id', 0);
 			if ($event->Special == 'recurring') { // if we set failed status for other than recurring special the redirect will not occur
 				$event->status = kEvent::erFAIL;
 			}
 			return false;
 		}
 
 		// call CompleteOrder events for items in order BEFORE SplitOrder (because ApproveEvents are called there)
 		/** @var kDBList $order_items */
 		$order_items = $this->Application->recallObject('orditems.-inv','orditems_List',Array('skip_counting'=>true,'per_page'=>-1) );
 
 		$order_items->linkToParent($order->Special);
 		$order_items->Query(true);
 		$order_items->GoFirst();
 
 		foreach($order_items->Records as $product_item)
 		{
 			if (!$product_item['ProductId']) continue; // product may have been deleted
 			$this->raiseProductEvent('CompleteOrder', $product_item['ProductId'], $product_item);
 		}
 
 		$shipping_control = getArrayValue($charge_result, 'gw_data', 'gw_params', 'shipping_control');
 		if ($event->Special != 'recurring') {
 			if ($shipping_control && $shipping_control != SHIPPING_CONTROL_PREAUTH ) {
 				// we have to do it here, because the coupons are used in the e-mails
 				$this->PrepareCoupons($event, $order);
 			}
 
 			$this->Application->emailUser('ORDER.SUBMIT', null, $this->OrderEmailParams($order));
 			$this->Application->emailAdmin('ORDER.SUBMIT');
 		}
 
 		if ($shipping_control === false || $shipping_control == SHIPPING_CONTROL_PREAUTH ) {
 			$order->SetDBField('Status', ORDER_STATUS_PENDING);
 			$order->Update();
 		}
 		else {
 			$this->SplitOrder($event, $order);
 		}
 
 		if (!$this->Application->isAdminUser) {
 			// for tracking code
 			$this->Application->StoreVar('last_order_amount', $order->GetDBField('TotalAmount'));
 			$this->Application->StoreVar('last_order_number', $order->GetDBField('OrderNumber'));
 			$this->Application->StoreVar('last_order_customer', $order->GetDBField('BillingTo'));
 			$this->Application->StoreVar('last_order_user', $order->GetDBField('Username'));
 
 			$event->redirect = $this->Application->GetVar('success_template');
 			$event->SetRedirectParam('m_cat_id', 0);
 		}
 		else
 		{
 //			$event->CallSubEvent('OnSave');
 		}
 
 		$order_id = $order->GetId();
 		$order_idfield = $this->Application->getUnitOption('ord','IDField');
 		$order_table = $this->Application->getUnitOption('ord','TableName');
 		$original_amount = $order->GetDBField('SubTotal') + $order->GetDBField('ShippingCost') + $order->GetDBField('VAT') + $order->GetDBField('ProcessingFee') + $order->GetDBField('InsuranceFee') - $order->GetDBField('GiftCertificateDiscount');
 		$sql = 'UPDATE '.$order_table.'
 				SET OriginalAmount = '.$original_amount.'
 				WHERE '.$order_idfield.' = '.$order_id;
 		$this->Conn->Query($sql);
 
 		// Remember order ID for use on "Thank You" page.
 		$this->Application->StoreVar('front_order_id', $order_id);
 
 		// Remove globals, set from "_createNewCart" method.
 		$this->Application->DeleteVar('ord_id');
 		$this->Application->RemoveVar('ord_id');
 
 		// Prevent accidental access to non-Incomplete order.
 		$this->Application->removeObject($event->getPrefixSpecial());
 		$this->Application->Session->SetCookie('shop_cart_cookie', '', strtotime('-1 month'));
 	}
 
 	/**
 	 * Set billing address same as shipping
 	 *
 	 * @param kEvent $event
 	 */
 	function setBillingAddress($event)
 	{
 		/** @var OrdersItem $object */
 		$object = $event->getObject();
 
 		if ( $object->HasTangibleItems() ) {
 			if ( $this->Application->GetVar('same_address') ) {
 				// copy shipping address to billing
 				$items_info = $this->Application->GetVar($event->getPrefixSpecial(true));
-				list($id, $field_values) = each($items_info);
+				$id = key($items_info);
 
 				$address_fields = Array (
 					'To', 'Company', 'Phone', 'Fax', 'Email',
 					'Address1', 'Address2', 'City', 'State',
 					'Zip', 'Country'
 				);
 
 				foreach ($address_fields as $address_field) {
 					$items_info[$id]['Billing' . $address_field] = $object->GetDBField('Shipping' . $address_field);
 				}
 
 				$this->Application->SetVar($event->getPrefixSpecial(true), $items_info);
 			}
 		}
 	}
 
 	/**
 	 * Enter description here...
 	 *
 	 * @param kEvent $event
 	 */
 	function OnProceedToPreview($event)
 	{
 		$this->setBillingAddress($event);
 
 		$event->CallSubEvent('OnUpdate');
 		$event->redirect = $this->Application->GetVar('preview_template');
 	}
 
 
 	function OnViewCart($event)
 	{
 		$this->StoreContinueShoppingLink();
 		$event->redirect = $this->Application->GetVar('viewcart_template');
 	}
 
 	function OnContinueShopping($event)
 	{
 		/** @var OrderHelper $order_helper */
 		$order_helper = $this->Application->recallObject('OrderHelper');
 
 		$template = $this->Application->GetVar('continue_shopping_template');
 
 		$event->redirect = $order_helper->getContinueShoppingTemplate($template);
 	}
 
 	/**
 	 * Enter description here...
 	 *
 	 * @param kEvent $event
 	 */
 	function OnCheckout($event)
 	{
 		$this->OnUpdateCart($event);
 		if ( !$event->getEventParam('RecalculateChangedCart') ) {
 			/** @var OrdersItem $object */
 			$object = $event->getObject();
 
 			if ( !$object->HasTangibleItems() ) {
 				$object->SetDBField('ShippingTo', '');
 				$object->SetDBField('ShippingCompany', '');
 				$object->SetDBField('ShippingPhone', '');
 				$object->SetDBField('ShippingFax', '');
 				$object->SetDBField('ShippingEmail', '');
 				$object->SetDBField('ShippingAddress1', '');
 				$object->SetDBField('ShippingAddress2', '');
 				$object->SetDBField('ShippingCity', '');
 				$object->SetDBField('ShippingState', '');
 				$object->SetDBField('ShippingZip', '');
 				$object->SetDBField('ShippingCountry', '');
 				$object->SetDBField('ShippingType', 0);
 				$object->SetDBField('ShippingCost', 0);
 				$object->SetDBField('ShippingCustomerAccount', '');
 				$object->SetDBField('ShippingTracking', '');
 				$object->SetDBField('ShippingDate', 0);
 				$object->SetDBField('ShippingOption', 0);
 				$object->SetDBField('ShippingInfo', '');
 				$object->Update();
 			}
 
 			$event->redirect = $this->Application->GetVar('next_step_template');
 
 			$order_id = $this->Application->GetVar('order_id');
 
 			if ( $order_id !== false ) {
 				$event->SetRedirectParam('ord_id', $order_id);
 			}
 		}
 	}
 
 	/**
 	 * Restores order from cookie
 	 *
 	 * @param kEvent $event
 	 * @return void
 	 * @access protected
 	 */
 	protected function OnRestoreOrder(kEvent $event)
 	{
 		if ( $this->Application->isAdmin || $this->Application->RecallVar('ord_id') ) {
 			// admin OR there is an active order -> don't restore from cookie
 			return;
 		}
 
 		$shop_cart_cookie = $this->Application->GetVarDirect('shop_cart_cookie', 'Cookie');
 
 		if ( !$shop_cart_cookie ) {
 			return;
 		}
 
 		$user_id = $this->Application->RecallVar('user_id');
 
 		$sql = 'SELECT OrderId
 				FROM ' . TABLE_PREFIX . 'Orders
 				WHERE (OrderId = ' . (int)$shop_cart_cookie . ') AND (Status = ' . ORDER_STATUS_INCOMPLETE . ') AND (PortalUserId = ' . $user_id . ')';
 		$order_id = $this->Conn->GetOne($sql);
 
 		if ( $order_id ) {
 			$this->Application->StoreVar('ord_id', $order_id);
 		}
 	}
 
 	/**
 	 * Redirect user to Billing checkout step
 	 *
 	 * @param kEvent $event
 	 */
 	function OnProceedToBilling($event)
 	{
 		$items_info = $this->Application->GetVar($event->getPrefixSpecial(true));
 		if ( $items_info ) {
-			list($id, $field_values) = each($items_info);
+			$id = key($items_info);
+			$field_values = $items_info[$id];
 
 			/** @var kDBItem $object */
 			$object = $event->getObject();
 
 			$payment_type_id = $object->GetDBField('PaymentType');
 
 			if ( !$payment_type_id ) {
 				$default_type = $this->_getDefaultPaymentType();
 
 				if ( $default_type ) {
 					$field_values['PaymentType'] = $default_type;
 					$items_info[$id] = $field_values;
 					$this->Application->SetVar($event->getPrefixSpecial(true), $items_info);
 				}
 			}
 		}
 
 		$event->CallSubEvent('OnUpdate');
 		$event->redirect = $this->Application->GetVar('next_step_template');
 	}
 
 	/**
 	 * Removes reoccurring mark from the order
 	 *
 	 * @param kEvent $event
 	 * @return void
 	 */
 	protected function OnCancelRecurring($event)
 	{
 		/** @var OrdersItem $order */
 		$order = $event->getObject();
 
 		$order->SetDBField('IsRecurringBilling', 0);
 		$order->Update();
 
 		if ( $this->Application->GetVar('cancelrecurring_ok_template') ) {
 			$event->redirect = $this->Application->GetVar('cancelrecurring_ok_template');
 		}
 	}
 
 	/**
 	 * Occurs after updating item
 	 *
 	 * @param kEvent $event
 	 * @return void
 	 * @access protected
 	 */
 	protected function OnAfterItemUpdate(kEvent $event)
 	{
 		parent::OnAfterItemUpdate($event);
 
 		/** @var OrdersItem $object */
 		$object = $event->getObject();
 
 		$cvv2 = $object->GetDBField('PaymentCVV2');
 
 		if ( $cvv2 !== false ) {
 			$this->Application->StoreVar('CVV2Code', $cvv2);
 		}
 	}
 
 
 	/**
 	 * Updates kDBItem
 	 *
 	 * @param kEvent $event
 	 * @return void
 	 * @access protected
 	 */
 	protected function OnUpdate(kEvent $event)
 	{
 		$this->setBillingAddress($event);
 
 		parent::OnUpdate($event);
 
 		if ($this->Application->isAdminUser) {
 			return ;
 		}
 		else {
 			$event->SetRedirectParam('opener', 's');
 		}
 
 		if ($event->status == kEvent::erSUCCESS) {
 			$this->createMissingAddresses($event);
 		}
 		else {
 			// strange: recalculate total amount on error
 			/** @var OrdersItem $object */
 			$object = $event->getObject();
 
 			$object->SetDBField('TotalAmount', $object->getTotalAmount());
 		}
 	}
 
 	/**
 	 * Creates new address
 	 *
 	 * @param kEvent $event
 	 */
 	function createMissingAddresses($event)
 	{
 		if ( !$this->Application->LoggedIn() ) {
 			return ;
 		}
 
 		/** @var kDBItem $object */
 		$object = $event->getObject();
 
 		/** @var kDBList $addr_list */
 		$addr_list = $this->Application->recallObject('addr', 'addr_List', Array ('per_page' => -1, 'skip_counting' => true));
 
 		$addr_list->Query();
 
 		/** @var AddressesItem $address_dummy */
 		$address_dummy = $this->Application->recallObject('addr.-item', null, Array ('skip_autoload' => true));
 
 		$address_prefixes = Array ('Billing', 'Shipping');
 		$address_fields = Array (
 			'To', 'Company', 'Phone', 'Fax', 'Email', 'Address1',
 			'Address2', 'City', 'State', 'Zip', 'Country'
 		);
 
 		foreach ($address_prefixes as $address_prefix) {
 			$address_id = $this->Application->GetVar(strtolower($address_prefix) . '_address_id');
 
 			if ( !$this->Application->GetVar('check_' . strtolower($address_prefix) . '_address') ) {
 				// form type doesn't match check type, e.g. shipping check on billing form
 				continue;
 			}
 
 			if ( $address_id > 0 ) {
 				$address_dummy->Load($address_id);
 			}
 			else {
 				$address_dummy->SetDBField('PortalUserId', $this->Application->RecallVar('user_id'));
 			}
 
 			foreach ($address_fields as $address_field) {
 				$address_dummy->SetDBField($address_field, $object->GetDBField($address_prefix . $address_field));
 			}
 
 			$address_dummy->MarkAddress($address_prefix, false);
 
 			$ret = ($address_id > 0) ? $address_dummy->Update() : $address_dummy->Create();
 		}
 	}
 
 	/**
 	 * Updates shopping cart content
 	 *
 	 * @param kEvent $event
 	 * @return void
 	 * @access protected
 	 */
 	protected function OnUpdateCart($event)
 	{
 		$this->Application->HandleEvent(new kEvent('orditems:OnUpdate'));
 
 		$event->CallSubEvent('OnRecalculateItems');
 	}
 
 	/**
 	 * Updates cart and returns various info in JSON format
 	 *
 	 * @param kEvent $event
 	 */
 	function OnUpdateCartJSON($event)
 	{
 		if ( $this->Application->GetVar('ajax') != 'yes' ) {
 			return;
 		}
 
 		/** @var kDBItem $object */
 		$object = $event->getObject();
 
 		// 1. delete given order item by id
 		$delete_id = $this->Application->GetVar('delete_id');
 
 		if ( $delete_id !== false ) {
 			$sql = 'DELETE FROM ' . TABLE_PREFIX . 'OrderItems
 					WHERE OrderId = ' . $object->GetID() . ' AND OrderItemId = ' . (int)$delete_id;
 			$this->Conn->Query($sql);
 		}
 
 		// 2. remove coupon
 		$remove = $this->Application->GetVar('remove');
 
 		if ( $remove == 'coupon' ) {
 			$this->RemoveCoupon($object);
 			$object->setCheckoutError(OrderCheckoutErrorType::COUPON, OrderCheckoutError::COUPON_REMOVED);
 		}
 		elseif ( $remove == 'gift_certificate' ) {
 			$this->RemoveGiftCertificate($object);
 			$object->setCheckoutError(OrderCheckoutErrorType::GIFT_CERTIFICATE, OrderCheckoutError::GC_REMOVED);
 		}
 
 		// 3. update product quantities and recalculate all discounts
 		$this->Application->HandleEvent(new kEvent('orditems:OnUpdate'));
 		$event->CallSubEvent('OnRecalculateItems');
 
 		// 4. remove "orditems" object of kDBItem class, since getOrderInfo uses kDBList object under same prefix
 		$this->Application->removeObject('orditems');
 
 		/** @var OrderHelper $order_helper */
 		$order_helper = $this->Application->recallObject('OrderHelper');
 
 		$event->status = kEvent::erSTOP;
 		$currency = $this->Application->GetVar('currency', 'selected');
 
 		echo json_encode( $order_helper->getOrderInfo($object, $currency) );
 	}
 
 	/**
 	 * Adds item to cart
 	 *
 	 * @param kEvent $event
 	 */
 	function OnAddToCart($event)
 	{
 		$this->StoreContinueShoppingLink();
 
 		$qty = $this->Application->GetVar('qty');
 		$options = $this->Application->GetVar('options');
 
 		// multiple or options add
 		$items = Array();
 		if (is_array($qty)) {
 			foreach ($qty as $item_id => $combinations)
 			{
 				if (is_array($combinations)) {
 					foreach ($combinations as $comb_id => $comb_qty) {
 						if ($comb_qty == 0) continue;
 						$items[] = array('item_id' => $item_id, 'qty' => $comb_qty, 'comb' => $comb_id);
 					}
 				}
 				else {
 					$items[] = array('item_id' => $item_id, 'qty' => $combinations);
 				}
 			}
 		}
 
 		if (!$items) {
 			if (!$qty || is_array($qty)) $qty = 1;
 			$item_id = $this->Application->GetVar('p_id');
 			if (!$item_id) return ;
 			$items = array(array('item_id' => $item_id, 'qty' => $qty));
 		}
 
 		// remember item data passed to event when called
 		$default_item_data = $event->getEventParam('ItemData');
 		$default_item_data = $default_item_data ? unserialize($default_item_data) : Array();
 
 		foreach ($items as $an_item) {
 			$item_id = $an_item['item_id'];
 			$qty = $an_item['qty'];
 			$comb = getArrayValue($an_item, 'comb');
 
 			$item_data = $default_item_data;
 
 			/** @var ProductsItem $product */
 			$product = $this->Application->recallObject('p', null, Array('skip_autoload' => true));
 
 			$product->Load($item_id);
 
 			$event->setEventParam('ItemData', null);
 
 			if ($product->GetDBField('AssignedCoupon')) {
 				$item_data['AssignedCoupon'] = $product->GetDBField('AssignedCoupon');
 			}
 
 			// 1. store options information OR
 			if ($comb) {
 				$combination = $this->Conn->GetOne('SELECT Combination FROM '.TABLE_PREFIX.'ProductOptionCombinations WHERE CombinationId = '.$comb);
 				$item_data['Options'] = unserialize($combination);
 			}
 			elseif (is_array($options)) {
 				$item_data['Options'] = $options[$item_id];
 			}
 
 			// 2. store subscription information OR
 			if( $product->GetDBField('Type') == 2 )	// subscriptions
 			{
 				$item_data = $this->BuildSubscriptionItemData($item_id, $item_data);
 			}
 
 			// 3. store package information
 			if( $product->GetDBField('Type') == 5 )	// package
 			{
 				$package_content_ids = $product->GetPackageContentIds();
 
 				/** @var ProductsItem $product_package_item */
 				$product_package_item = $this->Application->recallObject('p.-packageitem');
 
 				$package_item_data = array();
 
 				foreach ($package_content_ids as $package_item_id){
 					$product_package_item->Load($package_item_id);
 					$package_item_data[$package_item_id] = array();
 					if( $product_package_item->GetDBField('Type') == 2 )	// subscriptions
 					{
 						$package_item_data[$package_item_id] = $this->BuildSubscriptionItemData($package_item_id, $item_data);
 					}
 				}
 
 				$item_data['PackageContent'] = $product->GetPackageContentIds();
 				$item_data['PackageItemsItemData'] = $package_item_data;
 			}
 
 			$event->setEventParam('ItemData', serialize($item_data));
 			// 1 for PacakgeNum when in admin - temporary solution to overcome splitting into separate sub-orders
 			// of orders with items added through admin when approving them
 			$this->AddItemToOrder($event, $item_id, $qty, $this->Application->isAdminUser ? 1 : null);
 		}
 		if ($event->status == kEvent::erSUCCESS && !$event->redirect) {
 			$event->SetRedirectParam('pass', 'm');
 			$event->SetRedirectParam('pass_category', 0); //otherwise mod-rewrite shop-cart URL will include category
 			$event->redirect = true;
 		}
 		else {
 			if ($this->Application->isAdminUser) {
 				$event->SetRedirectParam('opener', 'u');
 			}
 		}
 	}
 
 	/**
 	 * Returns table prefix from event (temp or live)
 	 *
 	 * @param kEvent $event
 	 * @return string
 	 * @todo Needed? Should be refactored (by Alex)
 	 */
 	function TablePrefix(kEvent $event)
 	{
 		return $this->UseTempTables($event) ? $this->Application->GetTempTablePrefix('prefix:' . $event->Prefix) . TABLE_PREFIX : TABLE_PREFIX;
 	}
 
 	/**
 	 * Check if required options are selected & selected option combination is in stock
 	 *
 	 * @param kEvent $event
 	 * @param Array $options
 	 * @param int $product_id
 	 * @param int $qty
 	 * @param int $selection_mode
 	 * @return bool
 	 */
 	function CheckOptions($event, &$options, $product_id, $qty, $selection_mode)
 	{
 		// 1. check for required options
 		$selection_filter = $selection_mode == 1 ? ' AND OptionType IN (1,3,6) ' : '';
 		$req_options = $this->Conn->GetCol('SELECT ProductOptionId FROM '.TABLE_PREFIX.'ProductOptions WHERE ProductId = '.$product_id.' AND Required = 1 '.$selection_filter);
 		$result = true;
 		foreach ($req_options as $opt_id) {
 			if (!getArrayValue($options, $opt_id)) {
 				$this->Application->SetVar('opt_error', 1); //let the template know we have an error
 				$result = false;
 			}
 		}
 
 		// 2. check for option combinations in stock
 		$comb_salt = $this->OptionsSalt($options, true);
 		if ($comb_salt) {
 			// such option combination is defined explicitly
 			$poc_table = $this->Application->getUnitOption('poc', 'TableName');
 			$sql = 'SELECT Availability
 					FROM '.$poc_table.'
 					WHERE CombinationCRC = '.$comb_salt;
 			$comb_availble = $this->Conn->GetOne($sql);
 
 			// 2.1. check if Availability flag is set, then
 			if ($comb_availble == 1) {
 				// 2.2. check for quantity in stock
 				$table = Array();
 				$table['poc'] = $this->Application->getUnitOption('poc', 'TableName');
 				$table['p'] = $this->Application->getUnitOption('p', 'TableName');
 				$table['oi'] = $this->TablePrefix($event).'OrderItems';
 
 				$object = $event->getObject();
 				$ord_id = $object->GetID();
 
 				// 2.3. check if some amount of same combination & product are not already in shopping cart
 				$sql = 'SELECT '.
 								$table['p'].'.InventoryStatus,'.
 								$table['p'].'.BackOrder,
 								IF('.$table['p'].'.InventoryStatus = 2, '.$table['poc'].'.QtyInStock, '.$table['p'].'.QtyInStock) AS QtyInStock,
 								IF('.$table['oi'].'.OrderItemId IS NULL, 0, '.$table['oi'].'.Quantity) AS Quantity
 						FROM '.$table['p'].'
 						LEFT JOIN '.$table['poc'].' ON
 								'.$table['p'].'.ProductId = '.$table['poc'].'.ProductId
 						LEFT JOIN '.$table['oi'].' ON
 								('.$table['oi'].'.OrderId = '.$ord_id.') AND
 								('.$table['oi'].'.OptionsSalt = '.$comb_salt.') AND
 								('.$table['oi'].'.ProductId = '.$product_id.') AND
 								('.$table['oi'].'.BackOrderFlag = 0)
 						WHERE '.$table['poc'].'.CombinationCRC = '.$comb_salt;
 				$product_info = $this->Conn->GetRow($sql);
 
 				if ($product_info['InventoryStatus']) {
 					$backordering = $this->Application->ConfigValue('Comm_Enable_Backordering');
 					if (!$backordering || $product_info['BackOrder'] == 0) {
 						// backordering is not enabled generally or for this product directly, then check quantities in stock
 						if ($qty + $product_info['Quantity'] > $product_info['QtyInStock']) {
 							$this->Application->SetVar('opt_error', 2);
 							$result = false;
 						}
 					}
 				}
 			}
 			elseif ($comb_availble !== false) {
 				$this->Application->SetVar('opt_error', 2);
 				$result = false;
 			}
 		}
 
 		if ($result) {
 			$event->status = kEvent::erSUCCESS;
 			$shop_cart_template = $this->Application->GetVar('shop_cart_template');
 			$event->redirect = $this->Application->isAdminUser || !$shop_cart_template ? true : $shop_cart_template;
 		}
 		else {
 			$event->status = kEvent::erFAIL;
 		}
 		return $result;
 	}
 
 	/**
 	 * Enter description here...
 	 *
 	 * @param kEvent $event
 	 */
 	function OnUpdateItemOptions($event)
 	{
 		$opt_data = $this->Application->GetVar('options');
 		$options = getArrayValue($opt_data, $this->Application->GetVar('p_id'));
 
 		if (!$options) {
 			$qty_data = $this->Application->GetVar('qty');
 			$comb_id = key(getArrayValue($qty_data, $this->Application->GetVar('p_id')));
 			$options = unserialize($this->Conn->GetOne('SELECT Combination FROM '.TABLE_PREFIX.'ProductOptionCombinations WHERE CombinationId = '.$comb_id));
 		}
 
 		if (!$options) return;
 
 		/** @var kDBItem $ord_item */
 		$ord_item = $this->Application->recallObject('orditems.-opt', null, Array ('skip_autoload' => true));
 
 		$ord_item->Load($this->Application->GetVar('orditems_id'));
 
 		// assuming that quantity cannot be changed during order item editing
 		if (!$this->CheckOptions($event, $options, $ord_item->GetDBField('ProductId'), 0, $ord_item->GetDBField('OptionsSelectionMode'))) return;
 
 		$item_data = unserialize($ord_item->GetDBField('ItemData'));
 		$item_data['Options'] = $options;
 		$ord_item->SetDBField('ItemData', serialize($item_data));
 		$ord_item->SetDBField('OptionsSalt', $this->OptionsSalt($options));
 		$ord_item->Update();
 		$event->CallSubEvent('OnRecalculateItems');
 		if ($event->status == kEvent::erSUCCESS && $this->Application->isAdminUser) {
 			$event->SetRedirectParam('opener', 'u');
 		}
 	}
 
 	function BuildSubscriptionItemData($item_id, $item_data)
 	{
 		$products_table = $this->Application->getUnitOption('p', 'TableName');
 		$products_idfield = $this->Application->getUnitOption('p', 'IDField');
 		$sql = 'SELECT AccessGroupId FROM %s WHERE %s = %s';
 		$item_data['PortalGroupId'] = $this->Conn->GetOne( sprintf($sql, $products_table, $products_idfield, $item_id) );
 
 		$pricing_table = $this->Application->getUnitOption('pr', 'TableName');
 		$pricing_idfield = $this->Application->getUnitOption('pr', 'IDField');
 
 		/* TODO check on implementation
 		$sql = 'SELECT AccessDuration, AccessUnit, DurationType, AccessExpiration FROM %s WHERE %s = %s';
 		*/
 
 		$sql = 'SELECT * FROM %s WHERE %s = %s';
 		$pricing_id = $this->GetPricingId($item_id, $item_data);
 		$item_data['PricingId'] = $pricing_id;
 
 		$pricing_info = $this->Conn->GetRow( sprintf($sql, $pricing_table, $pricing_idfield, $pricing_id ) );
 		$unit_secs = Array(1 => 1, 2 => 60, 3 => 3600, 4 => 86400, 5 => 604800, 6 => 2592000, 7 => 31536000);
 
 		/* TODO check on implementation (code from customization healtheconomics.org)
 		$item_data['DurationType'] = $pricing_info['DurationType'];
 		$item_data['AccessExpiration'] = $pricing_info['AccessExpiration'];
 		*/
 
 		$item_data['Duration'] = $pricing_info['AccessDuration'] * $unit_secs[ $pricing_info['AccessUnit'] ];
 
 		return $item_data;
 	}
 
 	/**
 	 * Enter description here...
 	 *
 	 * @param kEvent $event
 	 */
 	function OnApplyCoupon($event)
 	{
 		$code = $this->Application->GetVar('coupon_code');
 
 		if ($code == '') {
 			return ;
 		}
 
 		/** @var OrdersItem $object */
 		$object = $event->getObject();
 
 		/** @var kDBItem $coupon */
 		$coupon = $this->Application->recallObject('coup', null, Array ('skip_autoload' => true));
 
 		$coupon->Load($code, 'Code');
 
 		if ( !$coupon->isLoaded() ) {
 			$event->status = kEvent::erFAIL;
 			$object->setCheckoutError(OrderCheckoutErrorType::COUPON, OrderCheckoutError::COUPON_CODE_INVALID);
 			$event->redirect = false; // check!!!
 
 			return ;
 		}
 
 		$expire_date = $coupon->GetDBField('Expiration');
 		$number_of_use = $coupon->GetDBField('NumberOfUses');
 		if ( $coupon->GetDBField('Status') != 1 || ($expire_date && $expire_date < adodb_mktime()) ||
 			(isset($number_of_use) && $number_of_use <= 0))
 		{
 			$event->status = kEvent::erFAIL;
 			$object->setCheckoutError(OrderCheckoutErrorType::COUPON, OrderCheckoutError::COUPON_CODE_EXPIRED);
 			$event->redirect = false;
 
 			return ;
 		}
 
 		$last_used = adodb_mktime();
 		$coupon->SetDBField('LastUsedBy', $this->Application->RecallVar('user_id'));
 		$coupon->SetDBField('LastUsedOn_date', $last_used);
 		$coupon->SetDBField('LastUsedOn_time', $last_used);
 
 
 		if ( isset($number_of_use) ) {
 			$coupon->SetDBField('NumberOfUses', $number_of_use - 1);
 
 			if ($number_of_use == 1) {
 				$coupon->SetDBField('Status', 2);
 			}
 		}
 
 		$coupon->Update();
 
 		$this->Application->setUnitOption('ord', 'AutoLoad', true);
 
 		/** @var OrdersItem $order */
 		$order = $this->Application->recallObject('ord');
 
 		$order->SetDBField('CouponId', $coupon->GetDBField('CouponId'));
 		$order->SetDBField('CouponName', $coupon->GetDBField('Name')); // calculated field
 
 		$order->Update();
 
 		$object->setCheckoutError(OrderCheckoutErrorType::COUPON, OrderCheckoutError::COUPON_APPLIED);
 //		OnApplyCoupon is called as hook for OnUpdateCart/OnCheckout, which calls OnRecalcualate themself
 	}
 
 	/**
 	 * Removes coupon from order
 	 *
 	 * @param kEvent $event
 	 * @deprecated
 	 */
 	function OnRemoveCoupon($event)
 	{
 		/** @var OrdersItem $object */
 		$object = $event->getObject();
 
 		$this->RemoveCoupon($object);
 		$object->setCheckoutError(OrderCheckoutErrorType::COUPON, OrderCheckoutError::COUPON_REMOVED);
 
 		$event->CallSubEvent('OnRecalculateItems');
 	}
 
 	/**
 	 * Removes coupon from a given order
 	 *
 	 * @param OrdersItem $object
 	 */
 	function RemoveCoupon(&$object)
 	{
 		/** @var kDBItem $coupon */
 		$coupon = $this->Application->recallObject('coup', null, Array('skip_autoload' => true));
 
 		$coupon->Load( $object->GetDBField('CouponId') );
 
 		if ( $coupon->isLoaded() ) {
 			$coupon->SetDBField('NumberOfUses', $coupon->GetDBField('NumberOfUses') + 1);
 			$coupon->SetDBField('Status', STATUS_ACTIVE);
 			$coupon->Update();
 		}
 
 		$object->SetDBField('CouponId', 0);
 		$object->SetDBField('CouponName', ''); // calculated field
 		$object->SetDBField('CouponDiscount', 0);
 	}
 
 	/**
 	 * Enter description here...
 	 *
 	 * @param kEvent $event
 	 */
 	function OnAddVirtualProductToCart($event)
 	{
 		$l_info = $this->Application->GetVar('l');
 		if($l_info)
 		{
 			foreach($l_info as $link_id => $link_info) {}
 			$item_data['LinkId'] = $link_id;
 			$item_data['ListingTypeId'] = $link_info['ListingTypeId'];
 		}
 		else
 		{
 			$link_id = $this->Application->GetVar('l_id');
 			$sql = 'SELECT ResourceId FROM '.$this->Application->getUnitOption('l', 'TableName').'
 					WHERE LinkId = '.$link_id;
 			$sql = 'SELECT ListingTypeId FROM '.$this->Application->getUnitOption('ls', 'TableName').'
 					WHERE ItemResourceId = '.$this->Conn->GetOne($sql);
 			$item_data['LinkId'] = $link_id;
 			$item_data['ListingTypeId'] = $this->Conn->GetOne($sql);
 		}
 
 		$sql = 'SELECT VirtualProductId FROM '.$this->Application->getUnitOption('lst', 'TableName').'
 				WHERE ListingTypeId = '.$item_data['ListingTypeId'];
 		$item_id = $this->Conn->GetOne($sql);
 
 		$event->setEventParam('ItemData', serialize($item_data));
 		$this->AddItemToOrder($event, $item_id);
 
 		$shop_cart_template = $this->Application->GetVar('shop_cart_template');
 
 		if ( $shop_cart_template ) {
 			$event->redirect = $shop_cart_template;
 		}
 
 		// don't pass unused info to shopping cart, brokes old mod-rewrites
 		$event->SetRedirectParam('pass', 'm'); // not to pass link id
 		$event->SetRedirectParam('m_cat_id', 0); // not to pass link id
 	}
 
 	function OnRemoveFromCart($event)
 	{
 		$ord_item_id = $this->Application->GetVar('orditems_id');
 		$ord_id = $this->getPassedID($event);
 		$this->Conn->Query('DELETE FROM '.TABLE_PREFIX.'OrderItems WHERE OrderId = '.$ord_id.' AND OrderItemId = '.$ord_item_id);
 		$this->OnRecalculateItems($event);
 	}
 
 	function OnCleanupCart($event)
 	{
 		$object = $event->getObject();
 
 		$sql = 'DELETE FROM '.TABLE_PREFIX.'OrderItems
 				WHERE OrderId = '.$this->getPassedID($event);
 		$this->Conn->Query($sql);
 
 		$this->RemoveCoupon($object);
 		$this->RemoveGiftCertificate($object);
 
 		$this->OnRecalculateItems($event);
 	}
 
 	/**
 	 * Returns order id from session or last used
 	 *
 	 * @param kEvent $event
 	 * @return int
 	 * @access public
 	 */
 	public function getPassedID(kEvent $event)
 	{
 		$event->setEventParam('raise_warnings', 0);
 		$passed = parent::getPassedID($event);
 
 		if ( $this->Application->isAdminUser ) {
 			// work as usual in admin
 			return $passed;
 		}
 
 		if ( $event->Special == 'last' ) {
 			// return last order id (for using on thank you page)
 			$order_id = $this->Application->RecallVar('front_order_id');
 
 			return $order_id > 0 ? $order_id : FAKE_ORDER_ID; // FAKE_ORDER_ID helps to keep parent filter for order items set in "kDBList::linkToParent"
 		}
 
 		$ses_id = $this->Application->RecallVar($event->getPrefixSpecial(true) . '_id');
 
 		if ( $passed && ($passed != $ses_id) ) {
 			// order id given in url doesn't match our current order id
 			$sql = 'SELECT PortalUserId
 					FROM ' . TABLE_PREFIX . 'Orders
 					WHERE OrderId = ' . $passed;
 			$user_id = $this->Conn->GetOne($sql);
 
 			if ( $user_id == $this->Application->RecallVar('user_id') ) {
 				// current user is owner of order with given id -> allow him to view order details
 				return $passed;
 			}
 			else {
 				// current user is not owner of given order -> hacking attempt
 				$this->Application->SetVar($event->getPrefixSpecial() . '_id', 0);
 				return 0;
 			}
 		}
 
 		// not passed or equals to ses_id
 		return $ses_id > 0 ? $ses_id : FAKE_ORDER_ID; // FAKE_ORDER_ID helps to keep parent filter for order items set in "kDBList::linkToParent"
 	}
 
 	/**
 	 * Load item if id is available
 	 *
 	 * @param kEvent $event
 	 * @return void
 	 * @access protected
 	 */
 	protected function LoadItem(kEvent $event)
 	{
 		$id = $this->getPassedID($event);
 
 		if ( $id == FAKE_ORDER_ID ) {
 			// if we already know, that there is no such order,
 			// then don't run database query, that will confirm that
 
 			/** @var kDBItem $object */
 			$object = $event->getObject();
 
 			$object->Clear($id);
 			return;
 		}
 
 		parent::LoadItem($event);
 	}
 
 	/**
 	 * Creates new shopping cart
 	 *
 	 * @param kEvent $event
 	 */
 	function _createNewCart($event)
 	{
 		/** @var kDBItem $object */
 		$object = $event->getObject( Array('skip_autoload' => true) );
 
 		$this->setNextOrderNumber($event);
 		$object->SetDBField('Status', ORDER_STATUS_INCOMPLETE);
 		$object->SetDBField('VisitId', $this->Application->RecallVar('visit_id') );
 
 		// get user
 		if ( $this->Application->LoggedIn() ) {
 			/** @var UsersItem $user */
 			$user = $this->Application->recallObject('u.current');
 
 			$user_id = $user->GetID();
 			$object->SetDBField('BillingEmail', $user->GetDBField('Email'));
 		}
 		else {
 			$user_id = USER_GUEST;
 		}
 
 		$object->SetDBField('PortalUserId', $user_id);
 
 		// get affiliate
 		$affiliate_id = $this->isAffiliate($user_id);
 		if ( $affiliate_id ) {
 			$object->SetDBField('AffiliateId', $affiliate_id);
 		}
 		else {
 			$affiliate_storage_method = $this->Application->ConfigValue('Comm_AffiliateStorageMethod');
 
 			if ( $affiliate_storage_method == 1 ) {
 				$object->SetDBField('AffiliateId', (int)$this->Application->RecallVar('affiliate_id'));
 			}
 			else {
 				$object->SetDBField('AffiliateId', (int)$this->Application->GetVar('affiliate_id'));
 			}
 		}
 
 		// get payment type
 		$default_type = $this->_getDefaultPaymentType();
 
 		if ( $default_type ) {
 			$object->SetDBField('PaymentType', $default_type);
 		}
 
 		// vat setting
 		$object->SetDBField('VATIncluded', $this->Application->ConfigValue('OrderVATIncluded'));
 
 		$created = $object->Create();
 
 		if ( $created ) {
 			$id = $object->GetID();
 
 			$this->Application->SetVar($event->getPrefixSpecial(true) . '_id', $id);
 			$this->Application->StoreVar($event->getPrefixSpecial(true) . '_id', $id);
 			$this->Application->Session->SetCookie('shop_cart_cookie', $id, strtotime('+1 month'));
 
 			return $id;
 		}
 
 		return 0;
 	}
 
 	/**
 	 * Returns default payment type for order
 	 *
 	 * @return int
 	 */
 	function _getDefaultPaymentType()
 	{
 		$default_type = $this->Application->siteDomainField('PrimaryPaymentTypeId');
 
 		if (!$default_type) {
 			$sql = 'SELECT PaymentTypeId
 					FROM ' . TABLE_PREFIX . 'PaymentTypes
 					WHERE IsPrimary = 1';
 			$default_type = $this->Conn->GetOne($sql);
 		}
 
 		return $default_type;
 	}
 
 	function StoreContinueShoppingLink()
 	{
 		$this->Application->StoreVar('continue_shopping', 'external:'.PROTOCOL.SERVER_NAME.$this->Application->RecallVar('last_url'));
 	}
 
 	/**
 	 * Sets required fields for order, based on current checkout step
 	 * !!! Do not use switch here, since all cases may be on the same form simultaneously
 	 *
 	 * @param kEvent $event
 	 */
 	function SetStepRequiredFields($event)
 	{
 		/** @var OrdersItem $order */
 		$order = $event->getObject();
 
 		/** @var kCountryStatesHelper $cs_helper */
 		$cs_helper = $this->Application->recallObject('CountryStatesHelper');
 
 		$items_info = $this->Application->GetVar($event->getPrefixSpecial(true));
 		if ($items_info) {
 			// updated address available from SUBMIT -> use it
-			list($id, $field_values) = each($items_info);
+			$field_values = current($items_info);
 		}
 		else {
 			// no updated address -> use current address
 			$field_values = Array (
 				'ShippingCountry' => $order->GetDBField('ShippingCountry'),
 				'BillingCountry' => $order->GetDBField('BillingCountry'),
 				'PaymentType' => $order->GetDBField('PaymentType'),
 			);
 		}
 
 		// shipping address required fields
 		if ($this->Application->GetVar('check_shipping_address')) {
 			$has_tangibles = $order->HasTangibleItems();
 			$req_fields = array('ShippingTo', 'ShippingAddress1', 'ShippingCity', 'ShippingZip', 'ShippingCountry', /*'ShippingPhone',*/ 'BillingEmail');
 			$order->setRequired($req_fields, $has_tangibles);
 			$order->setRequired('ShippingState', $cs_helper->CountryHasStates( $field_values['ShippingCountry'] ));
 		}
 
 		// billing address required fields
 		if ($this->Application->GetVar('check_billing_address')) {
 			$req_fields = array('BillingTo', 'BillingAddress1', 'BillingCity', 'BillingZip', 'BillingCountry', 'BillingPhone', 'BillingEmail');
 			$order->setRequired($req_fields);
 			$order->setRequired('BillingState', $cs_helper->CountryHasStates( $field_values['BillingCountry'] ));
 		}
 
 		$check_cc = $this->Application->GetVar('check_credit_card');
 
 		if ( $check_cc && ($field_values['PaymentType'] == $order->GetDBField('PaymentType')) ) {
 			// cc check required AND payment type was not changed during SUBMIT
 			if ( $this->Application->isAdminUser ) {
 				$req_fields = Array (/*'PaymentCardType',*/ 'PaymentAccount', /*'PaymentNameOnCard',*/ 'PaymentCCExpDate');
 			}
 			else {
 				$req_fields = Array (/*'PaymentCardType',*/ 'PaymentAccount', /*'PaymentNameOnCard',*/ 'PaymentCCExpDate', 'PaymentCVV2');
 			}
 
 			$order->setRequired($req_fields);
 		}
 	}
 
 	/**
 	 * Set's order's user_id to user from session or Guest otherwise
 	 *
 	 * @param kEvent $event
 	 */
 	function CheckUser($event)
 	{
 		if ( $this->Application->isAdminUser || defined('GW_NOTIFY') || defined('CRON') ) {
 			// 1. don't check, when Administrator is editing the order.
 			// 2. don't check while processing payment gateways, because they can do cross-domain ssl redirects.
 			// 3. don't check from CRON, because it's like Admin updates orders on other user behalf.
 			return;
 		}
 
 		/** @var OrdersItem $order */
 		$order = $event->getObject();
 
 		$ses_user = $this->Application->RecallVar('user_id');
 
 		if ( $order->GetDBField('PortalUserId') != $ses_user ) {
 			if ( $ses_user == 0 ) {
 				$ses_user = USER_GUEST;
 			}
 
 			$order->SetDBField('PortalUserId', $ses_user);
 			// since CheckUser is called in OnBeforeItemUpdate, we don't need to call udpate here, just set the field
 		}
 	}
 
 	/* ======================== ADMIN ONLY ======================== */
 
 	/**
 	 * 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();
 
 		$this->setNextOrderNumber($event);
 
 		$object->SetDBField('OrderIP', $this->Application->getClientIp());
 
 		$order_type = $this->getTypeBySpecial( $this->Application->GetVar('order_type') );
 		$object->SetDBField('Status', $order_type);
 	}
 
 	/**
 	 * When cloning orders set new order number to them
 	 *
 	 * @param kEvent $event
 	 * @return void
 	 * @access protected
 	 */
 	protected function OnBeforeClone(kEvent $event)
 	{
 		parent::OnBeforeClone($event);
 
 		/** @var OrdersItem $object */
 		$object = $event->getObject();
 
 		if ( substr($event->Special, 0, 9) == 'recurring' ) {
 			$object->SetDBField('SubNumber', $object->getNextSubNumber());
 		}
 		else {
 			$this->setNextOrderNumber($event);
 		}
 
 		$reset_fields = array(
 			'OnHold', 'OrderDate', 'ShippingCost', 'ShippingTracking', 'ShippingDate', 'ReturnTotal',
 			'OriginalAmount', 'ShippingInfo', 'GWResult1', 'GWResult2', 'AffiliateCommission',
 			'ProcessingFee', 'InsuranceFee',
 		);
 
 		foreach ( $reset_fields as $reset_field ) {
 			$field_options = $object->GetFieldOptions($reset_field);
 			$object->SetDBField($reset_field, $field_options['default']);
 		}
 
 		$object->SetDBField('OrderIP', $this->Application->getClientIp());
 		$object->UpdateFormattersSubFields();
 	}
 
 	function OnReserveItems($event)
 	{
 		/** @var kDBList $order_items */
 		$order_items = $this->Application->recallObject('orditems.-inv','orditems_List',Array('skip_counting'=>true,'per_page'=>-1) );
 
 		$order_items->linkToParent('-inv');
 		// force re-query, since we are updateing through orditem ITEM, not the list, and
 		// OnReserveItems may be called 2 times when fullfilling backorders through product edit - first time
 		// from FullFillBackorders and second time from OnOrderProcess
 		$order_items->Query(true);
 		$order_items->GoFirst();
 
 		// query all combinations used in this order
 
 
 		/** @var kCatDBItem $product_object */
 		$product_object = $this->Application->recallObject('p', null, Array('skip_autoload' => true));
 
 		$product_object->SwitchToLive();
 
 		/** @var kDBItem $order_item */
 		$order_item = $this->Application->recallObject('orditems.-item', null, Array('skip_autoload' => true));
 
 		/** @var kDBItem $combination_item */
 		$combination_item = $this->Application->recallObject('poc.-item', null, Array('skip_autoload' => true));
 
 		$combinations = $this->queryCombinations($order_items);
 
 		$event->status = kEvent::erSUCCESS;
 		while (!$order_items->EOL()) {
 			$rec = $order_items->getCurrentRecord();
 			$product_object->Load( $rec['ProductId'] );
 			if (!$product_object->GetDBField('InventoryStatus')) {
 				$order_items->GoNext();
 				continue;
 			}
 
 			$inv_object =& $this->getInventoryObject($product_object, $combination_item, $combinations[ $rec['ProductId'].'_'.$rec['OptionsSalt'] ]);
 
 			$lack = $rec['Quantity'] - $rec['QuantityReserved'];
 
 			if ( $lack > 0 ) {
 				// Reserve lack or what is available (in case if we need to reserve anything, by Alex).
 				$to_reserve = min(
 					$lack,
 					$inv_object->GetDBField('QtyInStock') - $product_object->GetDBField('QtyInStockMin')
 				);
 
 				// If we can't reserve the full lack.
 				if ( $to_reserve < $lack ) {
 					$event->status = kEvent::erFAIL;
 				}
 
 				// Reserve in order.
 				$order_item->SetDBFieldsFromHash($rec);
 				$order_item->SetDBField('QuantityReserved', $rec['QuantityReserved'] + $to_reserve);
 				$new_lack = $order_item->GetDBField('Quantity') - $order_item->GetDBField('QuantityReserved');
 				$order_item->SetDBField('BackOrderFlag', abs($new_lack) <= 0.0001 ? 0 : 1);
 				$order_item->SetId($rec['OrderItemId']);
 				$order_item->Update();
 
 				// Update product - increase reserved, decrease in stock.
 				$inv_object->SetDBField('QtyReserved', $inv_object->GetDBField('QtyReserved') + $to_reserve);
 				$inv_object->SetDBField('QtyInStock', $inv_object->GetDBField('QtyInStock') - $to_reserve);
 				$inv_object->SetDBField('QtyBackOrdered', $inv_object->GetDBField('QtyBackOrdered') - $to_reserve);
 				$inv_object->Update();
 
 				if ( $product_object->GetDBField('InventoryStatus') == 2 ) {
 					// Inventory by options, then restore changed combination
 					// values back to common $combinations array !!!
 					$combinations[$rec['ProductId'] . '_' . $rec['OptionsSalt']] = $inv_object->GetFieldValues();
 				}
 			}
 			$order_items->GoNext();
 		}
 		return true;
 	}
 
 	function OnOrderPrint($event)
 	{
 		$event->SetRedirectParam('opener', 's');
 	}
 
 	/**
 	 * Processes order each tab info resetting to other tab info / to user info
 	 *
 	 * @param kEvent $event
 	 * @access public
 	 */
 	function OnResetAddress($event)
 	{
 		$to_tab = $this->Application->GetVar('to_tab');
 		$from_tab = substr($event->Name, strlen('OnResetTo'));
 
 		// load values from db
 		/** @var kDBItem $object */
 		$object = $event->getObject();
 
 		// update values from submit
 		$field_values = $this->getSubmittedFields($event);
 		$object->SetFieldsFromHash($field_values);
 		$event->setEventParam('form_data', $field_values);
 
 		$this->DoResetAddress($object, $from_tab, $to_tab);
 
 		$object->Update();
 		$event->redirect = false;
 	}
 
 	/**
 	 * Processes item selection from popup item selector
 	 *
 	 * @todo Is this called ? (by Alex)
 	 * @param kEvent $event
 	 */
 	function OnProcessSelected($event)
 	{
 		$selected_ids = $this->Application->GetVar('selected_ids');
 		$product_ids = $selected_ids['p'];
 
 		if ($product_ids) {
 			$product_ids = explode(',', $product_ids);
 
 			// !!! LOOK OUT - Adding items to Order in admin is handled in order_ITEMS_event_handler !!!
 			foreach ($product_ids as $product_id) {
 				$this->AddItemToOrder($event, $product_id);
 			}
 		}
 
 		$event->SetRedirectParam('opener', 'u');
 	}
 
 	function OnMassPlaceOrder($event)
 	{
 		$object = $event->getObject( Array('skip_autoload' => true) );
 		$ids = $this->StoreSelectedIDs($event);
 
 		if($ids)
 		{
 			foreach($ids as $id)
 			{
 				$object->Load($id);
 				$this->DoPlaceOrder($event);
 			}
 		}
 		$event->status = kEvent::erSUCCESS;
 
 	}
 
 
 
 	/**
 	 * Universal
 	 * Checks if QtyInStock is enough to fullfill backorder (Qty - QtyReserved in order)
 	 *
 	 * @param int $ord_id
 	 * @return bool
 	 */
 	function ReadyToProcess($ord_id)
 	{
 		$poc_table = $this->Application->getUnitOption('poc', 'TableName');
 		$query = '	SELECT SUM(IF( IF('.TABLE_PREFIX.'Products.InventoryStatus = 2, '.$poc_table.'.QtyInStock, '.TABLE_PREFIX.'Products.QtyInStock) - '.TABLE_PREFIX.'Products.QtyInStockMin >= ('.TABLE_PREFIX.'OrderItems.Quantity - '.TABLE_PREFIX.'OrderItems.QuantityReserved), 0, 1))
 							FROM '.TABLE_PREFIX.'OrderItems
 					LEFT JOIN '.TABLE_PREFIX.'Products ON '.TABLE_PREFIX.'Products.ProductId = '.TABLE_PREFIX.'OrderItems.ProductId
 					LEFT JOIN '.$poc_table.' ON ('.$poc_table.'.CombinationCRC = '.TABLE_PREFIX.'OrderItems.OptionsSalt) AND ('.$poc_table.'.ProductId = '.TABLE_PREFIX.'OrderItems.ProductId)
 							WHERE OrderId = '.$ord_id.'
 							GROUP BY OrderId';
 
 		// IF (IF(InventoryStatus = 2, poc.QtyInStock, p.QtyInStock) - QtyInStockMin >= (Quantity - QuantityReserved), 0, 1
 		return ($this->Conn->GetOne($query) == 0);
 	}
 
 	/**
 	 * Return all option combinations used in order
 	 *
 	 * @param kDBList $order_items
 	 * @return Array
 	 */
 	function queryCombinations(&$order_items)
 	{
 		// 1. collect combination crc used in order
 		$combinations = Array();
 		while (!$order_items->EOL()) {
 			$row = $order_items->getCurrentRecord();
 			if ($row['OptionsSalt'] == 0) {
 				$order_items->GoNext();
 				continue;
 			}
 			$combinations[] = '(poc.ProductId = '.$row['ProductId'].') AND (poc.CombinationCRC = '.$row['OptionsSalt'].')';
 			$order_items->GoNext();
 		}
 		$order_items->GoFirst();
 		$combinations = array_unique($combinations); // if same combination+product found as backorder & normal order item
 
 		if ($combinations) {
 			// 2. query data about combinations
 			$poc_table = $this->Application->getUnitOption('poc', 'TableName');
 			$sql = 'SELECT CONCAT(poc.ProductId, "_", poc.CombinationCRC) AS CombinationKey, poc.*
 					FROM '.$poc_table.' poc
 					WHERE ('.implode(') OR (', $combinations).')';
 
 			return $this->Conn->Query($sql, 'CombinationKey');
 		}
 
 		return Array();
 	}
 
 	/**
 	 * Returns object to perform inventory actions on
 	 *
 	 * @param ProductsItem $product current product object in order
 	 * @param kDBItem $combination combination dummy object
 	 * @param Array $combination_data pre-queried combination data
 	 * @return kDBItem
 	 */
 	function &getInventoryObject(&$product, &$combination, $combination_data)
 	{
 		if ($product->GetDBField('InventoryStatus') == 2) {
 			// inventory by option combinations
 			$combination->SetDBFieldsFromHash($combination_data);
 			$combination->setID($combination_data['CombinationId']);
 			$change_item =& $combination;
 		}
 		else {
 			// inventory by product ifself
 			$change_item =& $product;
 		}
 
 		return $change_item;
 	}
 
 	/**
 	 * Approve order ("Pending" tab)
 	 *
 	 * @param kDBList $order_items
 	 * @return int new status of order if any
 	 */
 	function approveOrder(&$order_items)
 	{
 		$product_object = $this->Application->recallObject('p', null, Array('skip_autoload' => true));
 		$order_item = $this->Application->recallObject('orditems.-item', null, Array('skip_autoload' => true));
 		$combination_item = $this->Application->recallObject('poc.-item', null, Array('skip_autoload' => true));
 
 		$combinations = $this->queryCombinations($order_items);
 
 		while (!$order_items->EOL()) {
 			$rec = $order_items->getCurrentRecord();
 
 			$order_item->SetDBFieldsFromHash($rec);
 			$order_item->SetId($rec['OrderItemId']);
 			$order_item->SetDBField('QuantityReserved', 0);
 			$order_item->Update();
 
 			$product_object->Load( $rec['ProductId'] );
 			if (!$product_object->GetDBField('InventoryStatus')) {
 				// if no inventory info is collected, then skip this order item
 				$order_items->GoNext();
 				continue;
 			}
 
 			$inv_object =& $this->getInventoryObject($product_object, $combination_item, $combinations[ $rec['ProductId'].'_'.$rec['OptionsSalt'] ]);
 
 			// decrease QtyReserved by amount of product used in order
 			$inv_object->SetDBField('QtyReserved', $inv_object->GetDBField('QtyReserved') - $rec['Quantity']);
 			$inv_object->Update();
 
 			if ($product_object->GetDBField('InventoryStatus') == 2) {
 				// inventory by options, then restore changed combination values back to common $combinations array !!!
 				$combinations[ $rec['ProductId'].'_'.$rec['OptionsSalt'] ] = $inv_object->GetFieldValues();
 			}
 
 			$order_items->GoNext();
 		}
 		return true;
 	}
 
 	/**
 	 * Restores reserved items in the order
 	 *
 	 * @param kDBList $order_items
 	 * @return bool
 	 */
 	function restoreOrder(&$order_items)
 	{
 		/** @var kCatDBItem $product_object */
 		$product_object = $this->Application->recallObject('p', null, Array('skip_autoload' => true));
 
 		$product_object->SwitchToLive();
 
 		/** @var kDBItem $order_item */
 		$order_item = $this->Application->recallObject('orditems.-item', null, Array('skip_autoload' => true));
 
 		/** @var kDBItem $combination_item */
 		$combination_item = $this->Application->recallObject('poc.-item', null, Array('skip_autoload' => true));
 
 		$combinations = $this->queryCombinations($order_items);
 
 		while( !$order_items->EOL() )
 		{
 			$rec = $order_items->getCurrentRecord();
 
 			$product_object->Load( $rec['ProductId'] );
 			if (!$product_object->GetDBField('InventoryStatus')) {
 				// if no inventory info is collected, then skip this order item
 				$order_items->GoNext();
 				continue;
 			}
 
 			$inv_object =& $this->getInventoryObject($product_object, $combination_item, $combinations[ $rec['ProductId'].'_'.$rec['OptionsSalt'] ]);
 
 			// cancelling backorderd qty if any
 			$lack = $rec['Quantity'] - $rec['QuantityReserved'];
 			if ($lack > 0 && $rec['BackOrderFlag'] > 0) { // lack should have been recorded as QtyBackOrdered
 				$inv_object->SetDBField('QtyBackOrdered', $inv_object->GetDBField('QtyBackOrdered') - $lack);
 			}
 
 			// canceling reservation in stock
 			$inv_object->SetDBField('QtyReserved', $inv_object->GetDBField('QtyReserved') - $rec['QuantityReserved']);
 			// putting remaining freed qty back to stock
 			$inv_object->SetDBField('QtyInStock', $inv_object->GetDBField('QtyInStock') + $rec['QuantityReserved']);
 			$inv_object->Update();
 
 			/** @var ProductsEventHandler $product_h */
 			$product_h = $this->Application->recallObject('p_EventHandler');
 
 			if ($product_object->GetDBField('InventoryStatus') == 2) {
 				// inventory by options, then restore changed combination values back to common $combinations array !!!
 				$combinations[ $rec['ProductId'].'_'.$rec['OptionsSalt'] ] = $inv_object->GetFieldValues();
 
 				// using freed qty to fulfill possible backorders
 				$product_h->FullfillBackOrders($product_object, $inv_object->GetID());
 			}
 			else {
 				// using freed qty to fulfill possible backorders
 				$product_h->FullfillBackOrders($product_object, 0);
 			}
 
 			$order_item->SetDBFieldsFromHash($rec);
 			$order_item->SetId($rec['OrderItemId']);
 			$order_item->SetDBField('QuantityReserved', 0);
 			$order_item->Update();
 
 			$order_items->GoNext();
 		}
 
 		return true;
 	}
 
 	/**
 	 * Approve order + special processing
 	 *
 	 * @param kEvent $event
 	 */
 	function MassInventoryAction($event)
 	{
 		if ( $this->Application->CheckPermission('SYSTEM_ACCESS.READONLY', 1) ) {
 			$event->status = kEvent::erFAIL;
 			return;
 		}
 
 		// process order products
 		/** @var kDBItem $object */
 		$object = $this->Application->recallObject($event->Prefix . '.-inv', null, Array ('skip_autoload' => true));
 
 		$ids = $this->StoreSelectedIDs($event);
 
 		if ( $ids ) {
 			foreach ($ids as $id) {
 				$object->Load($id);
 				$this->InventoryAction($event);
 			}
 		}
 	}
 
 	function InventoryAction($event)
 	{
 		if ($this->Application->CheckPermission('SYSTEM_ACCESS.READONLY', 1)) {
 			$event->status = kEvent::erFAIL;
 			return;
 		}
 
 		$event_status_map = Array(
 			'OnMassOrderApprove'	=> ORDER_STATUS_TOSHIP,
 			'OnOrderApprove' 		=> ORDER_STATUS_TOSHIP,
 			'OnMassOrderDeny'		=> ORDER_STATUS_DENIED,
 			'OnOrderDeny'			=> ORDER_STATUS_DENIED,
 			'OnMassOrderArchive'	=> ORDER_STATUS_ARCHIVED,
 			'OnOrderArchive'		=> ORDER_STATUS_ARCHIVED,
 			'OnMassOrderShip' 		=> ORDER_STATUS_PROCESSED,
 			'OnOrderShip' 			=> ORDER_STATUS_PROCESSED,
 			'OnMassOrderProcess' 	=> ORDER_STATUS_TOSHIP,
 			'OnOrderProcess' 		=> ORDER_STATUS_TOSHIP,
 		);
 
 		/** @var kDBList $order_items */
 		$order_items = $this->Application->recallObject('orditems.-inv','orditems_List',Array('skip_counting'=>true,'per_page'=>-1) );
 
 		$order_items->linkToParent('-inv');
 		$order_items->Query();
 		$order_items->GoFirst();
 
 		/** @var OrdersItem $object */
 		$object = $this->Application->recallObject($event->Prefix.'.-inv');
 
 		if ($object->GetDBField('OnHold')) {
 			// any actions have no effect while on hold
 			return ;
 		}
 
 		// save original order status
 		$original_order_status = $object->GetDBField('Status');
 
 		// preparing new status, but not setting it yet
 		$object->SetDBField('Status', $event_status_map[$event->Name]);
 
 		$set_new_status = false;
 		$event->status = kEvent::erSUCCESS;
 
 		$email_params = $this->OrderEmailParams($object);
 
 		switch ($event->Name) {
 			case 'OnMassOrderApprove':
 			case 'OnOrderApprove':
 				$set_new_status = false; //on successful approve order will be split and new orders will have new statuses
 
 				if ($object->GetDBField('ChargeOnNextApprove')) {
 					$charge_info = $this->ChargeOrder($object);
 					if (!$charge_info['result']) {
 						break;
 					}
 
 					// removing ChargeOnNextApprove
 					$object->SetDBField('ChargeOnNextApprove', 0);
 					$sql = 'UPDATE '.$object->TableName.' SET ChargeOnNextApprove = 0 WHERE '.$object->IDField.' = '.$object->GetID();
 					$this->Conn->Query($sql);
 				}
 
 				// charge user for order in case if we user 2step charging (e.g. AUTH_ONLY + PRIOR_AUTH_CAPTURE)
 				$gw_data = $object->getGatewayData();
 
 				/** @var kGWBase $gateway_object */
 				$gateway_object = $this->Application->recallObject($gw_data['ClassName']);
 
 				$charge_result = $gateway_object->Charge($object->GetFieldValues(), $gw_data['gw_params']);
 				$sql = 'UPDATE %s SET GWResult2 = %s WHERE %s = %s';
 				$sql = sprintf($sql, $object->TableName, $this->Conn->qstr($gateway_object->getGWResponce()), $object->IDField, $object->GetID() );
 				$this->Conn->Query($sql);
 				$object->SetDBField('GWResult2', $gateway_object->getGWResponce() );
 
 				if ($charge_result) {
 					/** @var ProductsItem $product_object */
 					$product_object = $this->Application->recallObject('p', null, Array('skip_autoload' => true));
 
 					foreach ($order_items->Records as $product_item) {
 						if (!$product_item['ProductId']) {
 							 // product may have been deleted
 							continue;
 						}
 						$product_object->Load($product_item['ProductId']);
 						$hits = floor( $product_object->GetDBField('Hits') ) + 1;
 						$sql = 'SELECT MAX(Hits) FROM '.$this->Application->getUnitOption('p', 'TableName').'
 								WHERE FLOOR(Hits) = '.$hits;
 						$hits = ( $res = $this->Conn->GetOne($sql) ) ? $res + 0.000001 : $hits;
 						$product_object->SetDBField('Hits', $hits);
 						$product_object->Update();
 
 						/*$sql = 'UPDATE '.$this->Application->getUnitOption('p', 'TableName').'
 								SET Hits = Hits + '.$product_item['Quantity'].'
 								WHERE ProductId = '.$product_item['ProductId'];
 						$this->Conn->Query($sql);*/
 					}
 
 					$this->PrepareCoupons($event, $object);
 					$this->SplitOrder($event, $object);
 
 					if ( $object->GetDBField('IsRecurringBilling') != 1 ) {
 						$this->Application->emailUser('ORDER.APPROVE', null, $email_params);
 
 						// Mask credit card with XXXX
 						if ( $this->Application->ConfigValue('Comm_MaskProcessedCreditCards') ) {
 							$this->maskCreditCard($object, 'PaymentAccount');
 							$set_new_status = 1;
 						}
 					}
 				}
 
 				break;
 
 			case 'OnMassOrderDeny':
 			case 'OnOrderDeny':
 				foreach ($order_items->Records as $product_item) {
 					if (!$product_item['ProductId']) {
 						 // product may have been deleted
 						continue;
 					}
 					$this->raiseProductEvent('Deny', $product_item['ProductId'], $product_item);
 				}
 
 				if ( ($original_order_status != ORDER_STATUS_INCOMPLETE) && ($event->Name == 'OnMassOrderDeny' || $event->Name == 'OnOrderDeny') ) {
 					$this->Application->emailUser('ORDER.DENY', null, $email_params);
 
 					// inform payment gateway that order was declined
 					$gw_data = $object->getGatewayData();
 
 					if ( $gw_data ) {
 						/** @var kGWBase $gateway_object */
 						$gateway_object = $this->Application->recallObject($gw_data['ClassName']);
 						$gateway_object->OrderDeclined($object->GetFieldValues(), $gw_data['gw_params']);
 					}
 				}
 
 				// !!! LOOK HERE !!!
 				// !!!! no break !!!! here on purpose!!!
 			case 'OnMassOrderArchive':
 			case 'OnOrderArchive':
 				// it's critical to update status BEFORE processing items because
 				// FullfillBackorders could be called during processing and in case
 				// of order denial/archive fullfill could reserve the qtys back for current backorder
 				$object->Update();
 				$this->restoreOrder($order_items);
 				$set_new_status = false; // already set
 				break;
 
 			case 'OnMassOrderShip':
 			case 'OnOrderShip':
 				$ret = Array ();
 				$shipping_info = $object->GetDBField('ShippingInfo');
 
 				if ($shipping_info) {
 					/** @var ShippingQuoteCollector $quote_engine_collector */
 					$quote_engine_collector = $this->Application->recallObject('ShippingQuoteCollector');
 
 					$shipping_info = unserialize($shipping_info);
 					$sqe_class_name = $quote_engine_collector->GetClassByType($shipping_info, 1);
 				}
 
 				// try to create usps order
 				if (($object->GetDBField('ShippingType') == 0) && ($sqe_class_name !== false)) {
 					/** @var ShippingQuoteEngine $shipping_quote_engine */
 					$shipping_quote_engine = $this->Application->recallObject($sqe_class_name);
 
 					$ret = $shipping_quote_engine->MakeOrder($object);
 				}
 
 				if ( !array_key_exists('error_number', $ret) ) {
 					$set_new_status = $this->approveOrder($order_items);
 
 //					$set_new_status = $this->shipOrder($order_items);
 					$object->SetDBField('ShippingDate', adodb_mktime());
 					$object->UpdateFormattersSubFields();
 
 					$shipping_email = $object->GetDBField('ShippingEmail');
 					$email_params['to_email'] = $shipping_email ? $shipping_email : $email_params['_user_email'];
 					$this->Application->emailUser('ORDER.SHIP', null, $email_params);
 
 					// inform payment gateway that order was shipped
 					$gw_data = $object->getGatewayData();
 
 					/** @var kGWBase $gateway_object */
 					$gateway_object = $this->Application->recallObject($gw_data['ClassName']);
 					$gateway_object->OrderShipped($object->GetFieldValues(), $gw_data['gw_params']);
 				}
 				else {
 					$sqe_errors = $this->Application->RecallVar('sqe_errors');
 					$sqe_errors = $sqe_errors ? unserialize($sqe_errors) : Array ();
 					$sqe_errors[ $object->GetField('OrderNumber') ] = $ret['error_description'];
 
 					$this->Application->StoreVar('sqe_errors', serialize($sqe_errors));
 				}
 				break;
 
 			case 'OnMassOrderProcess':
 			case 'OnOrderProcess':
 				if ( $this->ReadyToProcess($object->GetID()) ) {
 					$event->CallSubEvent('OnReserveItems');
 
 					if ( $event->status == kEvent::erSUCCESS ) {
 						$set_new_status = true;
 					}
 
 					$this->Application->emailUser('BACKORDER.PROCESS', null, $email_params);
 				}
 				else {
 					$event->status = kEvent::erFAIL;
 				}
 				break;
 		}
 
 		if ( $set_new_status ) {
 			$object->Update();
 		}
 	}
 
 	/**
 	 * Hides last 4 digits from credit card number
 	 *
 	 * @param OrdersItem $object
 	 * @param string $field
 	 */
 	function maskCreditCard(&$object, $field)
 	{
 		$value = $object->GetDBField($field);
 		$value = preg_replace('/'.substr($value, -4).'$/', str_repeat('X', 4), $value);
 		$object->SetDBField($field, $value);
 	}
 
 	/**
 	 * Set next available order number.
 	 *
 	 * @param kEvent $event Event.
 	 *
 	 * @return void
 	 */
 	protected function setNextOrderNumber(kEvent $event)
 	{
 		/** @var OrdersItem $object */
 		$object = $event->getObject();
 
 		$next_order_number = $this->getNextOrderNumber();
 
 		$object->SetDBField('Number', $next_order_number);
 		$object->SetDBField('SubNumber', 0);
 
 		// set virtual field too
 		$number_format = (int)$this->Application->ConfigValue('Comm_Order_Number_Format_P');
 		$sub_number_format = (int)$this->Application->ConfigValue('Comm_Order_Number_Format_S');
 		$order_number = sprintf('%0' . $number_format . 'd', $next_order_number) . '-' . str_repeat('0', $sub_number_format);
 
 		$object->SetDBField('OrderNumber', $order_number);
 	}
 
 	/**
 	 * Returns order number to be used next.
 	 *
 	 * @return integer
 	 */
 	protected function getNextOrderNumber()
 	{
 		$config_table = $this->Application->getUnitOption('conf', 'TableName');
 		$this->Conn->Query('LOCK TABLES ' . $config_table . ' WRITE');
 
 		$sql = 'UPDATE ' . $config_table . '
 				SET VariableValue = VariableValue + 1
 				WHERE VariableName = "Comm_Next_Order_Number"';
 		$this->Conn->Query($sql);
 
 		$sql = 'SELECT VariableValue
 				FROM ' . $config_table . '
 				WHERE VariableName = "Comm_Next_Order_Number"';
 		$next_order_number = $this->Conn->GetOne($sql);
 
 		$this->Conn->Query('UNLOCK TABLES');
 		$this->Application->SetConfigValue('Comm_Next_Order_Number', $next_order_number);
 
 		return $next_order_number - 1;
 	}
 
 	/**
 	 * [HOOK] Ensures, that "Next Order Number" system setting is within allowed limits.
 	 *
 	 * @param kEvent $event Event.
 	 *
 	 * @return void
 	 */
 	protected function OnBeforeNextOrderNumberChange(kEvent $event)
 	{
 		/** @var kDBItem $system_setting */
 		$system_setting = $event->MasterEvent->getObject();
 
 		$old_value = $system_setting->GetOriginalField('VariableValue');
 		$new_value = $system_setting->GetDBField('VariableValue');
 
 		if ( $system_setting->GetDBField('VariableName') != 'Comm_Next_Order_Number' || $new_value == $old_value ) {
 			return;
 		}
 
 		$sql = 'SELECT MAX(Number)
 				FROM ' . $this->Application->getUnitOption($event->Prefix, 'TableName');
 		$next_order_number = (int)$this->Conn->GetOne($sql) + 1;
 
 		if ( $new_value < $next_order_number ) {
 			$system_setting->SetError('VariableValue', 'value_out_of_range', null, array(
 				'min_value' => $next_order_number,
 				'max_value' => '&infin;',
 			));
 		}
 	}
 
 	/**
 	 * Set's new order address based on another address from order (e.g. billing from shipping)
 	 *
 	 * @param unknown_type $object
 	 * @param unknown_type $from
 	 * @param unknown_type $to
 	 */
 	function DoResetAddress(&$object, $from, $to)
 	{
 		$fields = Array('To','Company','Phone','Fax','Email','Address1','Address2','City','State','Zip','Country');
 
 		if ($from == 'User') {
 			// skip these fields when coping from user, because they are not present in user profile
 			$tmp_fields = array_flip($fields);
 //			unset($tmp_fields['Company'], $tmp_fields['Fax'], $tmp_fields['Address2']);
 			$fields = array_flip($tmp_fields);
 		}
 
 		// apply modification
 		foreach ($fields as $field_name) {
 			$object->SetDBField($to.$field_name, $object->GetDBField($from.$field_name));
 		}
 	}
 
 	/**
 	 * Set's status incomplete to all cloned orders
 	 *
 	 * @param kEvent $event
 	 * @return void
 	 * @access protected
 	 */
 	protected function OnAfterClone(kEvent $event)
 	{
 		parent::OnAfterClone($event);
 
 		$id = $event->getEventParam('id');
 		$table = $this->Application->getUnitOption($event->Prefix, 'TableName');
 		$id_field = $this->Application->getUnitOption($event->Prefix, 'IDField');
 
 		// set cloned order status to Incomplete
 		$sql = 'UPDATE ' . $table . '
 				SET Status = 0
 				WHERE ' . $id_field . ' = ' . $id;
 		$this->Conn->Query($sql);
 	}
 
 
 	/* ======================== COMMON CODE ======================== */
 
 	/**
 	 * Split one timestamp field into 2 virtual fields
 	 *
 	 * @param kEvent $event
 	 * @return void
 	 * @access protected
 	 */
 	protected function OnAfterItemLoad(kEvent $event)
 	{
 		parent::OnAfterItemLoad($event);
 
 		/** @var kDBItem $object */
 		$object = $event->getObject();
 
 		// get user fields
 		$user_id = $object->GetDBField('PortalUserId');
 
 		if ( $user_id ) {
 			$sql = 'SELECT *, CONCAT(FirstName,\' \',LastName) AS UserTo
 					FROM ' . TABLE_PREFIX . 'Users
 					WHERE PortalUserId = ' . $user_id;
 			$user_info = $this->Conn->GetRow($sql);
 
 			$fields = Array(
 				'UserTo'=>'UserTo','UserPhone'=>'Phone','UserFax'=>'Fax','UserEmail'=>'Email',
 				'UserAddress1'=>'Street','UserAddress2'=>'Street2','UserCity'=>'City','UserState'=>'State',
 				'UserZip'=>'Zip','UserCountry'=>'Country','UserCompany'=>'Company'
 			);
 
 			foreach ($fields as $object_field => $user_field) {
 				$object->SetDBField($object_field, $user_info[$user_field]);
 			}
 		}
 
 		$object->SetDBField('PaymentCVV2', $this->Application->RecallVar('CVV2Code'));
 
 		/** @var kCountryStatesHelper $cs_helper */
 		$cs_helper = $this->Application->recallObject('CountryStatesHelper');
 
 		$cs_helper->PopulateStates($event, 'ShippingState', 'ShippingCountry');
 		$cs_helper->PopulateStates($event, 'BillingState', 'BillingCountry');
 
 		$this->SetStepRequiredFields($event);
 
 		// needed in OnAfterItemUpdate
 		$this->Application->SetVar('OriginalShippingOption', $object->GetDBField('ShippingOption'));
 	}
 
 	/**
 	 * Processes states
 	 *
 	 * @param kEvent $event
 	 * @return void
 	 * @access protected
 	 */
 	protected function OnBeforeItemCreate(kEvent $event)
 	{
 		parent::OnBeforeItemCreate($event);
 
 		/** @var kCountryStatesHelper $cs_helper */
 		$cs_helper = $this->Application->recallObject('CountryStatesHelper');
 
 		$cs_helper->PopulateStates($event, 'ShippingState', 'ShippingCountry');
 		$cs_helper->PopulateStates($event, 'BillingState', 'BillingCountry');
 	}
 
 	/**
 	 * Processes states
 	 *
 	 * @param kEvent $event
 	 * @return void
 	 * @access protected
 	 */
 	protected function OnBeforeItemUpdate(kEvent $event)
 	{
 		parent::OnBeforeItemUpdate($event);
 
 		/** @var OrdersItem $object */
 		$object = $event->getObject();
 
 		$old_payment_type = $object->GetOriginalField('PaymentType');
 		$new_payment_type = $object->GetDBField('PaymentType');
 
 		if ( $new_payment_type != $old_payment_type ) {
 			// payment type changed -> check that it's allowed
 			$available_payment_types = $this->Application->siteDomainField('PaymentTypes');
 
 			if ( $available_payment_types ) {
 				if ( strpos($available_payment_types, '|' . $new_payment_type . '|') === false ) {
 					// payment type isn't allowed in site domain
 					$object->SetDBField('PaymentType', $old_payment_type);
 				}
 			}
 		}
 
 		/** @var kCountryStatesHelper $cs_helper */
 		$cs_helper = $this->Application->recallObject('CountryStatesHelper');
 
 		$cs_helper->PopulateStates($event, 'ShippingState', 'ShippingCountry');
 		$cs_helper->PopulateStates($event, 'BillingState', 'BillingCountry');
 
 		if ( $object->HasTangibleItems() ) {
 			$cs_helper->CheckStateField($event, 'ShippingState', 'ShippingCountry', false);
 		}
 
 		$cs_helper->CheckStateField($event, 'BillingState', 'BillingCountry', false);
 
 		if ( $object->GetDBField('Status') > ORDER_STATUS_PENDING ) {
 			return ;
 		}
 
 		$this->CheckUser($event);
 
 		if ( !$object->GetDBField('OrderIP') ) {
 			$object->SetDBField('OrderIP', $this->Application->getClientIp());
 		}
 
 		$shipping_option = $this->Application->GetVar('OriginalShippingOption');
 		$new_shipping_option = $object->GetDBField('ShippingOption');
 
 		if ( $shipping_option != $new_shipping_option ) {
 			$this->UpdateShippingOption($event);
 		}
 		else {
 			$this->UpdateShippingTypes($event);
 		}
 		$this->RecalculateProcessingFee($event);
 		$this->UpdateShippingTotal($event);
 		$this->RecalculateGift($event);
 
 		// guess fields from "One Step Checkout" form
 		if ( $object->GetDBField('PaymentAccount') ) {
 			/** @var OrderHelper $order_helper */
 			$order_helper = $this->Application->recallObject('OrderHelper');
 
 			$object->SetDBField(
 				'PaymentCardType',
 				$order_helper->getCreditCardType($object->GetDBField('PaymentAccount'))
 			);
 		}
 		else {
 			$object->SetDBField('PaymentCardType', '');
 		}
 
 		if ( !$object->GetDBField('PaymentNameOnCard') ) {
 			$object->SetDBField('PaymentNameOnCard', $object->GetDBField('BillingTo'));
 		}
 
 		if ( is_object($event->MasterEvent) && $event->MasterEvent->Name == 'OnUpdateAjax' && $this->Application->GetVar('create_account') && $object->Validate() ) {
 			$this->createAccountFromOrder($event);
 		}
 	}
 
 	/**
 	 * Creates user account
 	 *
 	 * @param kEvent $event
 	 * @return void
 	 * @access protected
 	 */
 	protected function createAccountFromOrder($event)
 	{
 		/** @var OrdersItem $order */
 		$order = $event->getObject();
 
 		/** @var OrderHelper $order_helper */
 		$order_helper = $this->Application->recallObject('OrderHelper');
 
 		$user_fields = $order_helper->getUserFields($order);
 		$user_fields['Password'] = $order->GetDBField('UserPassword_plain');
 		$user_fields['VerifyPassword'] = $order->GetDBField('VerifyUserPassword_plain');
 
 		if ( $order->GetDBField('PortalUserId') == USER_GUEST ) {
 			// will also auto-login user when created
 			$this->Application->SetVar('u_register', Array (USER_GUEST => $user_fields));
 			$this->Application->HandleEvent(new kEvent('u.register:OnCreate'));
 		}
 		else {
 			/** @var UsersItem $user */
 			$user = $this->Application->recallObject('u.current');
 
 			$user->SetFieldsFromHash($user_fields);
 			if ( !$user->Update() ) {
 				$order->SetError('BillingEmail', $user->GetErrorPseudo('Email'));
 			}
 		}
 	}
 
 	/**
 	 * Apply any custom changes to list's sql query
 	 *
 	 * @param kEvent $event
 	 * @return void
 	 * @access protected
 	 * @see kDBEventHandler::OnListBuild()
 	 */
 	protected function SetCustomQuery(kEvent $event)
 	{
 		parent::SetCustomQuery($event);
 
 		/** @var kDBList $object */
 		$object = $event->getObject();
 
 		$types = $event->getEventParam('types');
 		if ( $types == 'myorders' || $types == 'myrecentorders' ) {
 			$user_id = $this->Application->RecallVar('user_id');
 			$object->addFilter('myitems_user1', '%1$s.PortalUserId = ' . $user_id);
 			$object->addFilter('myitems_user2', '%1$s.PortalUserId > 0');
 			$object->addFilter('Status', '%1$s.Status != 0');
 		}
 		else if ($event->Special == 'returns') {
 //			$object->addFilter('returns_filter',TABLE_PREFIX.'Orders.Status = '.ORDER_STATUS_PROCESSED.' AND (
 //				SELECT SUM(ReturnType)
 //				FROM '.TABLE_PREFIX.'OrderItems oi
 //				WHERE oi.OrderId = '.TABLE_PREFIX.'Orders.OrderId
 //			) > 0');
 			$object->addFilter('returns_filter', TABLE_PREFIX . 'Orders.Status = ' . ORDER_STATUS_PROCESSED . ' AND ' . TABLE_PREFIX . 'Orders.ReturnTotal > 0');
 		}
 		else if ( $event->Special == 'user' ) {
 			$user_id = $this->Application->GetVar('u_id');
 			$object->addFilter('user_filter', '%1$s.PortalUserId = ' . $user_id);
 		}
 		else {
 			$special = $event->Special ? $event->Special : $this->Application->GetVar('order_type');
 			if ( $special != 'search' ) {
 				// don't filter out orders by special in case of search tab
 				$object->addFilter('status_filter', '%1$s.Status=' . $this->getTypeBySpecial($special));
 			}
 
 			if ( $event->getEventParam('selected_only') ) {
 				$ids = $this->StoreSelectedIDs($event);
 				$object->addFilter('selected_filter', '%1$s.OrderId IN (' . implode(',', $ids) . ')');
 			}
 		}
 	}
 
 	function getTypeBySpecial($special)
 	{
 		$special2type = Array('incomplete'=>0,'pending'=>1,'backorders'=>2,'toship'=>3,'processed'=>4,'denied'=>5,'archived'=>6);
 		return $special2type[$special];
 	}
 
 	function getSpecialByType($type)
 	{
 		$type2special = Array(0=>'incomplete',1=>'pending',2=>'backorders',3=>'toship',4=>'processed',5=>'denied',6=>'archived');
 		return $type2special[$type];
 	}
 
 	function LockTables($event)
 	{
 		$read = Array();
 		$write_lock = '';
 		$read_lock = '';
 		$write = Array('Orders','OrderItems','Products');
 		foreach ($write as $tbl) {
 			$write_lock .= TABLE_PREFIX.$tbl.' WRITE,';
 		}
 		foreach ($read as $tbl) {
 			$read_lock .= TABLE_PREFIX.$tbl.' READ,';
 		}
 		$write_lock = rtrim($write_lock, ',');
 		$read_lock = rtrim($read_lock, ',');
 		$lock = trim($read_lock.','.$write_lock, ',');
 		//$this->Conn->Query('LOCK TABLES '.$lock);
 	}
 
 	/**
 	 * Checks shopping cart products quantities
 	 *
 	 * @param kEvent $event
 	 * @return bool
 	 */
 	function CheckQuantites($event)
 	{
 		if ( $this->OnRecalculateItems($event) ) { // if something has changed in the order
 			if ( $this->Application->isAdminUser ) {
 				if ( $this->UseTempTables($event) ) {
 					$event->redirect = 'in-commerce/orders/orders_edit_items';
 				}
 			}
 			else {
 				$event->redirect = $this->Application->GetVar('viewcart_template');
 			}
 
 			return false;
 		}
 
 		return true;
 	}
 
 	function DoPlaceOrder($event)
 	{
 		$order = $event->getObject();
 
 		$table_prefix = $this->TablePrefix($event);
 
 		$this->LockTables($event);
 
 		if (!$this->CheckQuantites($event)) return false;
 
 		//everything is fine - we could reserve items
 		$this->ReserveItems($event);
 		$this->SplitOrder($event, $order);
 		return true;
 	}
 
 	function &queryOrderItems($event, $table_prefix)
 	{
 		$order = $event->getObject();
 		$ord_id = $order->GetId();
 
 		// TABLE_PREFIX and $table_prefix are NOT the same !!!
 		$poc_table = $this->Application->getUnitOption('poc', 'TableName');
 		$query = '	SELECT
 							 BackOrderFlag, '.
 							 $table_prefix.'OrderItems.OrderItemId, '.
 							 $table_prefix.'OrderItems.Quantity, '.
 							 $table_prefix.'OrderItems.QuantityReserved,
 							 IF('.TABLE_PREFIX.'Products.InventoryStatus = 2, '.$poc_table.'.QtyInStock, '.TABLE_PREFIX.'Products.QtyInStock) AS QtyInStock, '.
 							 TABLE_PREFIX.'Products.QtyInStockMin, '.
 							 $table_prefix.'OrderItems.ProductId, '.
 							 TABLE_PREFIX.'Products.InventoryStatus,'.
 							 $table_prefix.'OrderItems.OptionsSalt AS CombinationCRC
 					FROM '.$table_prefix.'OrderItems
 					LEFT JOIN '.TABLE_PREFIX.'Products ON '.TABLE_PREFIX.'Products.ProductId = '.$table_prefix.'OrderItems.ProductId
 					LEFT JOIN '.$poc_table.' ON ('.$poc_table.'.CombinationCRC = '.$table_prefix.'OrderItems.OptionsSalt) AND ('.$poc_table.'.ProductId = '.$table_prefix.'OrderItems.ProductId)
 					WHERE OrderId = '.$ord_id.' AND '.TABLE_PREFIX.'Products.Type = 1
 					ORDER BY BackOrderFlag ASC';
 
 		$items = $this->Conn->Query($query);
 		return $items;
 	}
 
 	function ReserveItems($event)
 	{
 		$table_prefix = $this->TablePrefix($event);
 		$items =& $this->queryOrderItems($event, $table_prefix);
 
 		foreach ($items as $an_item) {
 			if (!$an_item['InventoryStatus']) {
 				$to_reserve = $an_item['Quantity'] - $an_item['QuantityReserved'];
 			}
 			else {
 				if ($an_item['BackOrderFlag'] > 0) { // we don't need to reserve if it's backordered item
 					$to_reserve = 0;
 				}
 				else {
 					$to_reserve = min($an_item['Quantity']-$an_item['QuantityReserved'], $an_item['QtyInStock']-$an_item['QtyInStockMin']); //it should be equal, but just in case
 				}
 
 				$to_backorder = $an_item['BackOrderFlag'] > 0 ? $an_item['Quantity']-$an_item['QuantityReserved'] : 0;
 			}
 
 			if ($to_backorder < 0) $to_backorder = 0; //just in case
 			$query = '	UPDATE '.$table_prefix.'OrderItems
 						SET QuantityReserved = IF(QuantityReserved IS NULL, '.$to_reserve.', QuantityReserved + '.$to_reserve.')
 						WHERE OrderItemId = '.$an_item['OrderItemId'];
 			$this->Conn->Query($query);
 
 			if (!$an_item['InventoryStatus']) continue;
 
 			$update_clause = '	QtyInStock = QtyInStock - '.$to_reserve.',
 							  	QtyReserved = QtyReserved + '.$to_reserve.',
 								QtyBackOrdered = QtyBackOrdered + '.$to_backorder;
 
 			if ($an_item['InventoryStatus'] == 1) {
 				// inventory by product, then update it's quantities
 				$query = '	UPDATE '.TABLE_PREFIX.'Products
 							SET '.$update_clause.'
 								WHERE ProductId = '.$an_item['ProductId'];
 			}
 			else {
 				// inventory = 2 -> by product option combinations
 				$poc_idfield = $this->Application->getUnitOption('poc', 'IDField');
 				$poc_table = $this->Application->getUnitOption('poc', 'TableName');
 				$query = '	UPDATE '.$poc_table.'
 							SET '.$update_clause.'
 							WHERE (ProductId = '.$an_item['ProductId'].') AND (CombinationCRC = '.$an_item['CombinationCRC'].')';
 			}
 			$this->Conn->Query($query);
 		}
 	}
 
 	function FreeItems($event)
 	{
 		$table_prefix = $this->TablePrefix($event);
 		$items =& $this->queryOrderItems($event, $table_prefix);
 
 		foreach ($items as $an_item) {
 				$to_free = $an_item['QuantityReserved'];
 
 				if ($an_item['InventoryStatus']) {
 				if ($an_item['BackOrderFlag'] > 0) { // we don't need to free if it's backordered item
 					$to_free = 0;
 				}
 
 				// what's not reserved goes to backorder in stock for orderitems marked with BackOrderFlag
 				$to_backorder_free = $an_item['BackOrderFlag'] > 0 ? $an_item['Quantity'] - $an_item['QuantityReserved'] : 0;
 				if ($to_backorder_free < 0) $to_backorder_free = 0; //just in case
 
 				$update_clause = '	QtyInStock = QtyInStock + '.$to_free.',
 								  	QtyReserved = QtyReserved - '.$to_free.',
 							  		QtyBackOrdered = QtyBackOrdered - '.$to_backorder_free;
 
 				if ($an_item['InventoryStatus'] == 1) {
 					// inventory by product
 					$query = '	UPDATE '.TABLE_PREFIX.'Products
 								SET '.$update_clause.'
 									WHERE ProductId = '.$an_item['ProductId'];
 				}
 				else {
 					// inventory by option combinations
 					$poc_idfield = $this->Application->getUnitOption('poc', 'IDField');
 					$poc_table = $this->Application->getUnitOption('poc', 'TableName');
 					$query = '	UPDATE '.$poc_table.'
 								SET '.$update_clause.'
 								WHERE (ProductId = '.$an_item['ProductId'].') AND (CombinationCRC = '.$an_item['CombinationCRC'].')';
 				}
 
 					$this->Conn->Query($query);
 				}
 
 			$query = '	UPDATE '.$table_prefix.'OrderItems
 						SET QuantityReserved = IF(QuantityReserved IS NULL, 0, QuantityReserved - '.$to_free.')
 									WHERE OrderItemId = '.$an_item['OrderItemId'];
 				$this->Conn->Query($query);
 		}
 	}
 
 	/**
 	 * Enter description here...
 	 *
 	 * @param kEvent $event
 	 * @param OrdersItem $object
 	 */
 	function SplitOrder($event, &$object)
 	{
 		$affiliate_event = new kEvent('affil:OnOrderApprove');
 		$affiliate_event->setEventParam('Order_PrefixSpecial', $object->getPrefixSpecial() );
 		$this->Application->HandleEvent($affiliate_event);
 
 		$table_prefix = $this->TablePrefix($event);
 		$order =& $object;
 		$ord_id = $order->GetId();
 
 		$shipping_option = $order->GetDBField('ShippingOption');
 		$backorder_select = $shipping_option == 0 ? '0' : '%s.BackOrderFlag';
 
 
 		// setting PackageNum to 0 for Non-tangible items, for tangibles first package num is always 1
 		$query = '	SELECT oi.OrderItemId
 					FROM ' . $table_prefix . 'OrderItems oi
 					LEFT JOIN ' . TABLE_PREFIX . 'Products p ON p.ProductId = oi.ProductId
 					WHERE p.Type > 1 AND oi.OrderId = ' . $ord_id;
 		$non_tangibles = $this->Conn->GetCol($query);
 
 		if ($non_tangibles) {
 			$query = '	UPDATE ' . $table_prefix . 'OrderItems
 						SET PackageNum = 0
 						WHERE OrderItemId IN (' . implode(',', $non_tangibles) . ')';
 			$this->Conn->Query($query);
 		}
 
 		// grouping_data:
 		// 0 => Product Type
 		// 1 => if NOT tangibale and NOT downloadable - OrderItemId,
 		//			2 => ProductId
 		// 3 => Shipping PackageNum
 		$query = 'SELECT
 					' . sprintf($backorder_select, $table_prefix . 'OrderItems') . ' AS BackOrderFlagCalc,
 					PackageNum,
 					ProductName,
 					ShippingTypeId,
 					CONCAT('.TABLE_PREFIX.'Products.Type,
 						"_",
 						IF ('.TABLE_PREFIX.'Products.Type NOT IN ('.PRODUCT_TYPE_DOWNLOADABLE.','.PRODUCT_TYPE_TANGIBLE.'),
 							CONCAT(OrderItemId, "_", '.TABLE_PREFIX.'Products.ProductId),
 							""),
 						"_",
 						PackageNum
 						) AS Grouping,
 					SUM(Quantity) AS TotalItems,
 					SUM('.$table_prefix.'OrderItems.Weight*Quantity) AS TotalWeight,
 					SUM(Price * Quantity) AS TotalAmount,
 					SUM(QuantityReserved) AS TotalReserved,
 					'.TABLE_PREFIX.'Products.Type AS ProductType
 				FROM '.$table_prefix.'OrderItems
 				LEFT JOIN '.TABLE_PREFIX.'Products
 					ON '.TABLE_PREFIX.'Products.ProductId = '.$table_prefix.'OrderItems.ProductId
 				WHERE OrderId = '.$ord_id.'
 				GROUP BY BackOrderFlagCalc, Grouping
 				ORDER BY BackOrderFlagCalc ASC, PackageNum ASC, ProductType ASC';
 
 		$sub_orders = $this->Conn->Query($query);
 
 		$processed_sub_orders = Array();
 
 		// in case of recurring billing this will not be 0 as usual
 		//$first_sub_number = ($event->Special == 'recurring') ? $object->getNextSubNumber() - 1 : 0;
 		$first_sub_number = $object->GetDBField('SubNumber');
 
 		$next_sub_number = $first_sub_number;
 		$group = 1;
 
 		$order_has_gift = $order->GetDBField('GiftCertificateDiscount') > 0 ? 1 : 0;
 
 		$skip_types = Array (PRODUCT_TYPE_TANGIBLE, PRODUCT_TYPE_DOWNLOADABLE);
 		foreach ($sub_orders as $sub_order_data) {
 			/** @var OrdersItem $sub_order */
 			$sub_order = $this->Application->recallObject('ord.-sub'.$next_sub_number, 'ord');
 
 			if ( $this->UseTempTables($event) && $next_sub_number == 0 ) {
 				$sub_order =& $order;
 			}
 			else {
 				foreach ( $order->GetFieldValues() as $field => $value ) {
 					$sub_order->SetOriginalField($field, $value);
 				}
 			}
 
 			$sub_order->SetDBFieldsFromHash($order->GetFieldValues());
 			$sub_order->SetDBField('SubNumber', $next_sub_number);
 			$sub_order->SetDBField('SubTotal', $sub_order_data['TotalAmount']);
 
 			$grouping_data = explode('_', $sub_order_data['Grouping']);
 			$named_grouping_data['Type'] = $grouping_data[0];
 
 			if (!in_array($named_grouping_data['Type'], $skip_types)) {
 				$named_grouping_data['OrderItemId'] = $grouping_data[1];
 				$named_grouping_data['ProductId'] = $grouping_data[2];
 				$named_grouping_data['PackageNum'] = $grouping_data[3];
 			}
 			else {
 				$named_grouping_data['PackageNum'] = $grouping_data[2];
 			}
 
 			if ($named_grouping_data['Type'] == PRODUCT_TYPE_TANGIBLE) {
 				$sub_order->SetDBField('ShippingCost', getArrayValue( unserialize($order->GetDBField('ShippingInfo')), $sub_order_data['PackageNum'], 'TotalCost') );
 				$sub_order->SetDBField('InsuranceFee', getArrayValue( unserialize($order->GetDBField('ShippingInfo')), $sub_order_data['PackageNum'], 'InsuranceFee') );
 				$sub_order->SetDBField('ShippingInfo', serialize(Array(1 => getArrayValue( unserialize($order->GetDBField('ShippingInfo')), $sub_order_data['PackageNum']))));
 			}
 			else {
 				$sub_order->SetDBField('ShippingCost', 0);
 				$sub_order->SetDBField('InsuranceFee', 0);
 				$sub_order->SetDBField('ShippingInfo', ''); //otherwise orders w/o shipping wills still have shipping info!
 			}
 
 			$amount_percent = $sub_order->getTotalAmount() * 100 / $order->getTotalAmount();
 			// proportional affiliate commission splitting
 			if ($order->GetDBField('AffiliateCommission') > 0) {
 				$sub_order->SetDBField('AffiliateCommission', $order->GetDBField('AffiliateCommission') * $amount_percent / 100 );
 			}
 
 			$amount_percent = ($sub_order->GetDBField('SubTotal') + $sub_order->GetDBField('ShippingCost')) * 100 / ($order->GetDBField('SubTotal') + $order->GetDBField('ShippingCost'));
 			if ($order->GetDBField('ProcessingFee') > 0) {
 				$sub_order->SetDBField('ProcessingFee', round($order->GetDBField('ProcessingFee') * $amount_percent / 100, 2));
 			}
 
 			$sub_order->RecalculateTax();
 
 			$original_amount = $sub_order->GetDBField('SubTotal') + $sub_order->GetDBField('ShippingCost') + $sub_order->GetDBField('VAT') + $sub_order->GetDBField('ProcessingFee') + $sub_order->GetDBField('InsuranceFee') - $sub_order->GetDBField('GiftCertificateDiscount');
 			$sub_order->SetDBField('OriginalAmount', $original_amount);
 
 			if ($named_grouping_data['Type'] == 1 && ($sub_order_data['BackOrderFlagCalc'] > 0
 					||
 					($sub_order_data['TotalItems'] != $sub_order_data['TotalReserved'])) ) {
 				$sub_order->SetDBField('Status', ORDER_STATUS_BACKORDERS);
 
 				if ($event->Special != 'recurring') { // just in case if admin uses tangible backordered products in recurring orders
 					$this->Application->emailUser('BACKORDER.ADD', null, $this->OrderEmailParams($sub_order));
 		    		$this->Application->emailAdmin('BACKORDER.ADD');
 				}
 			}
 			else {
 				switch ($named_grouping_data['Type']) {
 					case PRODUCT_TYPE_DOWNLOADABLE:
 						$sql = 'SELECT oi.*
 								FROM '.TABLE_PREFIX.'OrderItems oi
 								LEFT JOIN '.TABLE_PREFIX.'Products p ON p.ProductId = oi.ProductId
 								WHERE (OrderId = %s) AND (p.Type = '.PRODUCT_TYPE_DOWNLOADABLE.')';
 						$downl_products = $this->Conn->Query( sprintf($sql, $ord_id) );
 						$product_ids = Array();
 						foreach ($downl_products as $downl_product) {
 							$this->raiseProductEvent('Approve', $downl_product['ProductId'], $downl_product, $next_sub_number);
 							$product_ids[] = $downl_product['ProductId'];
 						}
 						break;
 
 					case PRODUCT_TYPE_TANGIBLE:
 						$sql = 'SELECT ' . sprintf($backorder_select, 'oi') . ' AS BackOrderFlagCalc, oi.*
 								FROM ' . TABLE_PREFIX . 'OrderItems oi
 								LEFT JOIN ' . TABLE_PREFIX . 'Products p ON p.ProductId = oi.ProductId
 								WHERE (OrderId = %s) AND (p.Type = ' . PRODUCT_TYPE_TANGIBLE . ')
 								HAVING BackOrderFlagCalc = 0';
 
 							$products = $this->Conn->Query( sprintf($sql, $ord_id) );
 							foreach ($products as $product) {
 								$this->raiseProductEvent('Approve', $product['ProductId'], $product, $next_sub_number);
 							}
 						break;
 
 					default:
 						$order_item_fields = $this->Conn->GetRow('SELECT * FROM '.TABLE_PREFIX.'OrderItems WHERE OrderItemId = '.$named_grouping_data['OrderItemId']);
 						$this->raiseProductEvent('Approve', $named_grouping_data['ProductId'], $order_item_fields, $next_sub_number);
 						break;
 				}
 
 				$sub_order->SetDBField('Status', $named_grouping_data['Type'] == PRODUCT_TYPE_TANGIBLE ? ORDER_STATUS_TOSHIP : ORDER_STATUS_PROCESSED);
 			}
 
 			if ($next_sub_number == $first_sub_number) {
 				$sub_order->SetId($order->GetId());
 				$sub_order->Update();
 			}
 			else {
 				$sub_order->Create();
 			}
 
 			switch ($named_grouping_data['Type']) {
 				case PRODUCT_TYPE_TANGIBLE:
 					$query = '	UPDATE ' . $table_prefix . 'OrderItems
 								SET OrderId = %s, PackageNum = 1
 								WHERE OrderId = %s AND PackageNum = %s';
 					$query = sprintf($query, $sub_order->GetID(), $ord_id, $sub_order_data['PackageNum']);
 					break;
 
 				case PRODUCT_TYPE_DOWNLOADABLE:
 					$query = '	UPDATE ' . $table_prefix . 'OrderItems
 								SET OrderId = %s, PackageNum = 1
 								WHERE OrderId = %s AND ProductId IN (%s)';
 					$query = sprintf($query, $sub_order->GetID(), $ord_id, implode(',', $product_ids));
 					break;
 
 				default:
 					$query = '	UPDATE ' . $table_prefix . 'OrderItems
 								SET OrderId = %s, PackageNum = 1
 								WHERE OrderId = %s AND OrderItemId = %s';
 					$query = sprintf($query, $sub_order->GetID(), $ord_id, $named_grouping_data['OrderItemId']);
 					break;
 			}
 
 			$this->Conn->Query($query);
 
 			if ($order_has_gift) {
 				// gift certificate can be applied only after items are assigned to suborder
 				$sub_order->RecalculateGift($event);
 				$original_amount = $sub_order->GetDBField('SubTotal') + $sub_order->GetDBField('ShippingCost') + $sub_order->GetDBField('VAT') + $sub_order->GetDBField('ProcessingFee') + $sub_order->GetDBField('InsuranceFee') - $sub_order->GetDBField('GiftCertificateDiscount');
 				$sub_order->SetDBField('OriginalAmount', $original_amount);
 				$sub_order->Update();
 			}
 
 			$processed_sub_orders[] = $sub_order->GetID();
 
 			$next_sub_number = $sub_order->getNextSubNumber();
 			$group++;
 		}
 
 		foreach ($processed_sub_orders as $sub_id) {
 			// update DiscountTotal field
 			$sql = 'SELECT SUM(ROUND(FlatPrice-Price,2)*Quantity) FROM '.$table_prefix.'OrderItems WHERE OrderId = '.$sub_id;
 			$discount_total = $this->Conn->GetOne($sql);
 
 			$sql = 'UPDATE '.$sub_order->TableName.'
 					SET DiscountTotal = '.$this->Conn->qstr($discount_total).'
 					WHERE OrderId = '.$sub_id;
 			$this->Conn->Query($sql);
 		}
 	}
 
 	/**
 	 * Call products linked event when spefcfic action is made to product in order
 	 *
 	 * @param string $event_type type of event to get from product ProcessingData = {Approve,Deny,CompleteOrder}
 	 * @param int $product_id ID of product to gather processing data from
 	 * @param Array $order_item_fields OrderItems table record fields (with needed product & order in it)
 	 */
 	function raiseProductEvent($event_type, $product_id, $order_item_fields, $next_sub_number=null)
 	{
 		$sql = 'SELECT ProcessingData
 				FROM '.TABLE_PREFIX.'Products
 				WHERE ProductId = '.$product_id;
 		$processing_data = $this->Conn->GetOne($sql);
 		if ($processing_data) {
 			$processing_data = unserialize($processing_data);
 			$event_key = getArrayValue($processing_data, $event_type.'Event');
 			// if requested type of event is defined for product, only then process it
 			if ($event_key) {
 				$event = new kEvent($event_key);
 				$event->setEventParam('field_values', $order_item_fields);
 				$event->setEventParam('next_sub_number', $next_sub_number);
 				$this->Application->HandleEvent($event);
 			}
 		}
 	}
 
 	function OptionsSalt($options, $comb_only=false)
 	{
 		$helper = $this->Application->recallObject('kProductOptionsHelper');
 		return $helper->OptionsSalt($options, $comb_only);
 	}
 
 	/**
 	 * Enter description here...
 	 *
 	 * @param kEvent $event
 	 * @param int $item_id
 	 */
 	function AddItemToOrder($event, $item_id, $qty = null, $package_num = null)
 	{
 		if (!isset($qty)) {
 			$qty = 1;
 		}
 
 		// Loading product to add
 		/** @var kDBItem $product */
 		$product = $this->Application->recallObject('p.toadd', null, Array('skip_autoload' => true));
 
 		$product->Load($item_id);
 
 		/** @var kDBItem $object */
 		$object = $this->Application->recallObject('orditems.-item', null, Array('skip_autoload' => true));
 
 		/** @var kDBItem $order */
 		$order = $this->Application->recallObject('ord');
 
 		if (!$order->isLoaded() && !$this->Application->isAdmin) {
 			// no order was created before -> create one now
 			if ($this->_createNewCart($event)) {
 				$this->LoadItem($event);
 			}
 		}
 
 		if (!$order->isLoaded()) {
 			// was unable to create new order
 			return false;
 		}
 
 		$item_data = $event->getEventParam('ItemData');
 		$item_data = $item_data ? unserialize($item_data) : Array ();
 		$options = getArrayValue($item_data, 'Options');
 
 		if ( !$this->CheckOptions($event, $options, $item_id, $qty, $product->GetDBField('OptionsSelectionMode')) ) {
 			return;
 		}
 
 		/** @var OrderManager $manager */
 		$manager = $this->Application->recallObject('OrderManager');
 
 		$manager->setOrder($order);
 		$manager->addProduct($product, $event->getEventParam('ItemData'), $qty, $package_num);
 
 		$this->Application->HandleEvent(new kEvent('ord:OnRecalculateItems'));
 	}
 
 	/**
 	 * Enter description here...
 	 *
 	 * @param kEvent $event
 	 */
 	function UpdateShippingTotal($event)
 	{
 		if ( $this->Application->GetVar('ebay_notification') == 1 ) {
 			// TODO: get rid of this "if"
 			return;
 		}
 
 		/** @var OrdersItem $object */
 		$object = $event->getObject();
 
 		$shipping_total = $insurance_fee = 0;
 		$shipping_info = $object->GetDBField('ShippingInfo') ? unserialize($object->GetDBField('ShippingInfo')) : false;
 
 		if ( is_array($shipping_info) ) {
 			foreach ($shipping_info as $a_shipping) {
 //				$id_elements = explode('_', $a_shipping['ShippingTypeId']);
 				$shipping_total += $a_shipping['TotalCost'];
 				$insurance_fee += $a_shipping['InsuranceFee'];
 			}
 		}
 
 		$object->SetDBField('ShippingCost', $shipping_total);
 		$object->SetDBField('InsuranceFee', $insurance_fee);
 		// no need to update, it will be called in calling method
 
 		$this->RecalculateTax($event);
 	}
 
 	/**
 	 * Recompile shopping cart, splitting or grouping orders and backorders depending on total quantities.
 	 * First it counts total qty for each ProductId, and then creates order for available items
 	 * and backorder for others. It also updates the sub-total for the order
 	 *
 	 * @param kEvent $event
 	 * @return bool Returns true if items splitting/grouping were changed
 	 */
 	function OnRecalculateItems($event)
 	{
 		if (is_object($event->MasterEvent) && ($event->MasterEvent->status != kEvent::erSUCCESS)) {
 			// e.g. master order update failed, don't recalculate order products
 			return ;
 		}
 
 		/** @var OrdersItem $order */
 		$order = $event->getObject();
 
 		if ( !$order->isLoaded() ) {
 			$this->LoadItem($event); // try to load
 		}
 
 		$ord_id = (int)$order->GetID();
 
 		if ( !$order->isLoaded() ) return; //order has not been created yet
 
 		if( $order->GetDBField('Status') != ORDER_STATUS_INCOMPLETE )
 		{
 			return;
 		}
 
 		/** @var OrderManager $manager */
 		$manager = $this->Application->recallObject('OrderManager');
 
 		$manager->setOrder($order);
 		$result = $manager->calculate();
 
 		if ( $order->GetDBField('CouponId') && $order->GetDBField('CouponDiscount') == 0 ) {
 			$this->RemoveCoupon($order);
 			$order->setCheckoutError(OrderCheckoutErrorType::COUPON, OrderCheckoutError::COUPON_REMOVED_AUTOMATICALLY);
 		}
 
 		if ( $result ) {
 			$this->UpdateShippingOption($event);
 		}
 
 		$this->UpdateShippingTotal($event);
 
 		$this->RecalculateProcessingFee($event);
 		$this->RecalculateTax($event);
 		$this->RecalculateGift($event);
 
 		if ( $event->Name != 'OnAfterItemUpdate' ) {
 			$order->Update();
 		}
 
 		$event->setEventParam('RecalculateChangedCart', $result);
 
 		if ( is_object($event->MasterEvent) ) {
 			$event->MasterEvent->setEventParam('RecalculateChangedCart', $result);
 		}
 
 		/*if ( $result && !getArrayValue($event->redirect_params, 'checkout_error') ) {
 			$event->SetRedirectParam('checkout_error', OrderCheckoutError::STATE_CHANGED);
 		}*/
 
 		if ( $result && is_object($event->MasterEvent) && $event->MasterEvent->Name == 'OnUserLogin' ) {
 			$shop_cart_template = $this->Application->GetVar('shop_cart_template');
 
 			if ( $shop_cart_template && is_object($event->MasterEvent->MasterEvent) ) {
 //				$event->MasterEvent->MasterEvent->SetRedirectParam('checkout_error', OrderCheckoutError::CHANGED_AFTER_LOGIN);
 				$event->MasterEvent->MasterEvent->redirect = $shop_cart_template;
 			}
 		}
 
 		return $result;
 	}
 
 /*	function GetShippingCost($user_country_id, $user_state_id, $user_zip, $weight, $items, $amount, $shipping_type)
 	{
 		$this->Application->recallObject('ShippingQuoteEngine');
 		$shipping_h = $this->Application->recallObject('CustomShippingQuoteEngine');
 		$query = $shipping_h->QueryShippingCost($user_country_id, $user_state_id, $user_zip, $weight, $items, $amount, $shipping_type);
 		$cost = $this->Conn->GetRow($query);
 		return $cost['TotalCost'];
 	}*/
 
 	/**
 	 * Return product pricing id for given product, if not passed - return primary pricing ID
 	 *
 	 * @param int $product_id ProductId
 	 * @return float
 	 */
 	function GetPricingId($product_id, $item_data)	{
 
 		if (!is_array($item_data)) {
 			$item_data = unserialize($item_data);
 		}
 		$price_id = getArrayValue($item_data, 'PricingId');
 		if (!$price_id) {
 		$price_id = $this->Application->GetVar('pr_id');
 		}
 		if (!$price_id){
 			$price_id = $this->Conn->GetOne('SELECT PriceId FROM '.TABLE_PREFIX.'ProductsPricing WHERE ProductId='.$product_id.' AND IsPrimary=1');
 		}
 		return $price_id;
 	}
 
 	function UpdateShippingOption($event)
 	{
 		$object = $event->getObject();
 		$shipping_option = $object->GetDBField('ShippingOption');
 
 		if($shipping_option == '') return;
 
 		$table_prefix = $this->TablePrefix($event);
 
 		if ($shipping_option == 1 || $shipping_option == 0) { // backorder separately
 			$query = 'UPDATE '.$table_prefix.'OrderItems SET BackOrderFlag = 1 WHERE OrderId = '.$object->GetId().' AND BackOrderFlag > 1';
 			$this->Conn->Query($query);
 		}
 		if ($shipping_option == 2) {
 			$query = 'SELECT * FROM '.$table_prefix.'OrderItems WHERE OrderId = '.$object->GetId().' AND BackOrderFlag >= 1 ORDER By ProductName asc';
 			$items = $this->Conn->Query($query);
 			$backorder_flag = 2;
 			foreach ($items as $an_item) {
 				$query = 'UPDATE '.$table_prefix.'OrderItems SET BackOrderFlag = '.$backorder_flag.' WHERE OrderItemId = '.$an_item['OrderItemId'];
 				$this->Conn->Query($query);
 				$backorder_flag++;
 			}
 		}
 	}
 
 	/**
 	 * Updates shipping types
 	 *
 	 * @param kEvent $event
 	 * @return bool
 	 */
 	function UpdateShippingTypes($event)
 	{
 		/** @var OrdersItem $object */
 		$object = $event->getObject();
 
 		$ord_id = $object->GetID();
 
 		$order_info = $this->Application->GetVar('ord');
 		$shipping_ids = getArrayValue($order_info, $ord_id, 'ShippingTypeId');
 
 		if (!$shipping_ids) {
 			return;
 		}
 
 		$ret = true;
 		$shipping_types = Array();
 		$last_shippings = unserialize( $this->Application->RecallVar('LastShippings') );
 
 		$template = $this->Application->GetVar('t');
 		$shipping_templates = Array ('in-commerce/checkout/shipping', 'in-commerce/orders/orders_edit_shipping');
 
 		/** @var ShippingQuoteCollector $quote_engine_collector */
 		$quote_engine_collector = $this->Application->recallObject('ShippingQuoteCollector');
 
 		foreach ($shipping_ids as $package => $id) {
 			// try to validate
 			$shipping_types[$package] = $last_shippings[$package][$id];
 			$sqe_class_name = $quote_engine_collector->GetClassByType($shipping_types, $package);
 
 			if (($object->GetDBField('ShippingType') == 0) && ($sqe_class_name !== false) && in_array($template, $shipping_templates)) {
 				/** @var ShippingQuoteEngine $shipping_quote_engine */
 				$shipping_quote_engine = $this->Application->recallObject($sqe_class_name);
 
 				// USPS related part
 				// TODO: remove USPS condition from here
 				// set first of found shippings just to check if any errors are returned
 				$current_usps_shipping_types = unserialize($this->Application->RecallVar('current_usps_shipping_types'));
 				$object->SetDBField('ShippingInfo', serialize( Array($package => $current_usps_shipping_types[$id])) );
 
 				$sqe_data = $shipping_quote_engine->MakeOrder($object, true);
 
 				if ( $sqe_data ) {
 					if ( !isset($sqe_data['error_number']) ) {
 						// update only international shipping
 						if ( $object->GetDBField('ShippingCountry') != 'USA') {
 							$shipping_types[$package]['TotalCost'] = $sqe_data['Postage'];
 						}
 					}
 					else {
 						$ret = false;
 						$this->Application->StoreVar('sqe_error', $sqe_data['error_description']);
 					}
 				}
 
 				$object->SetDBField('ShippingInfo', '');
 			}
 		}
 
 		$object->SetDBField('ShippingInfo', serialize($shipping_types));
 
 		return $ret;
 	}
 
 	/*function shipOrder(&$order_items)
 	{
 		$product_object = $this->Application->recallObject('p', null, Array('skip_autoload' => true));
 		$order_item = $this->Application->recallObject('orditems.-item');
 
 		while( !$order_items->EOL() )
 		{
 			$rec = $order_items->getCurrentRecord();
 
 			$order_item->SetDBFieldsFromHash($rec);
 			$order_item->SetId($rec['OrderItemId']);
 			$order_item->SetDBField('QuantityReserved', 0);
 			$order_item->Update();
 
 			$order_items->GoNext();
 		}
 		return true;
 	}*/
 
 	function RecalculateTax($event)
 	{
 		/** @var OrdersItem $object */
 		$object = $event->getObject();
 
 		if ($object->GetDBField('Status') > ORDER_STATUS_PENDING) {
 			return;
 		}
 
 		$object->RecalculateTax();
 	}
 
 	function RecalculateProcessingFee($event)
 	{
 		$object = $event->getObject();
 
 		// Do not reset processing fee while orders are being split (see SplitOrder)
 		if (preg_match("/^-sub/", $object->Special)) return;
 		if ($object->GetDBField('Status') > ORDER_STATUS_PENDING) return; //no changes for orders other than incomple or pending
 
 		$pt = $object->GetDBField('PaymentType');
 		$processing_fee = $this->Conn->GetOne('SELECT ProcessingFee FROM '.$this->Application->getUnitOption('pt', 'TableName').' WHERE PaymentTypeId = '.$pt);
 		$object->SetDBField( 'ProcessingFee', $processing_fee );
 		$this->UpdateTotals($event);
 	}
 
 	function UpdateTotals($event)
 	{
 		/** @var OrdersItem $object */
 		$object = $event->getObject();
 
 		$object->UpdateTotals();
 	}
 
 	/*function CalculateDiscount($event)
 	{
 		$object = $event->getObject();
 
 		$coupon = $this->Application->recallObject('coup', null, Array('skip_autoload' => true));
 		if(!$coupon->Load( $object->GetDBField('CouponId'), 'CouponId' ))
 		{
 			return false;
 		}
 
 		$sql = 'SELECT Price * Quantity AS Amount, ProductId FROM '.$this->Application->getUnitOption('orditems', 'TableName').'
 				WHERE OrderId = '.$object->GetDBField('OrderId');
 		$orditems = $this->Conn->GetCol($sql, 'ProductId');
 
 		$sql = 'SELECT coupi.ItemType, p.ProductId FROM '.$this->Application->getUnitOption('coupi', 'TableName').' coupi
 				LEFT JOIN '.$this->Application->getUnitOption('p', 'TableName').' p
 				ON coupi.ItemResourceId = p.ResourceId
 				WHERE CouponId = '.$object->GetDBField('CouponId');
 		$discounts = $this->Conn->GetCol($sql, 'ProductId');
 
 		$discount_amount = 0;
 
 		foreach($orditems as $product_id => $amount)
 		{
 			if(isset($discounts[$product_id]) || array_search('0', $discounts, true) !== false)
 			{
 				switch($coupon->GetDBField('Type'))
 				{
 					case 1:
 						$discount_amount += $coupon->GetDBField('Amount') < $amount ? $coupon->GetDBField('Amount') : $amount;
 					break;
 					case 2:
 						$discount_amount += $amount * $coupon->GetDBField('Amount') / 100;
 					break;
 					default:
 				}
 				break;
 			}
 		}
 
 		$object->SetDBField('CouponDiscount', $discount_amount);
 		return $discount_amount;
 	}*/
 
 	/**
 	 * Jumps to selected order in order's list from search tab
 	 *
 	 * @param kEvent $event
 	 */
 	function OnGoToOrder($event)
 	{
 		$id = current($this->StoreSelectedIDs($event));
 
 		$id_field = $this->Application->getUnitOption($event->Prefix,'IDField');
 		$table = $this->Application->getUnitOption($event->Prefix,'TableName');
 
 		$sql = 'SELECT Status FROM %s WHERE %s = %s';
 
 		$order_status = $this->Conn->GetOne( sprintf($sql, $table, $id_field, $id) );
 
 		$prefix_special = $event->Prefix.'.'.$this->getSpecialByType($order_status);
 
 		/** @var kDBList $orders_list */
 		$orders_list = $this->Application->recallObject($prefix_special, $event->Prefix.'_List', Array('per_page'=>-1) );
 
 		$orders_list->Query();
 
 		foreach ($orders_list->Records as $row_num => $record) {
 			if ( $record[$id_field] == $id ) {
 				break;
 			}
 		}
 
 		$per_page = $this->getPerPage( new kEvent($prefix_special.':OnDummy') );
 		$page = ceil( ($row_num+1) / $per_page );
 
 		$this->Application->StoreVar($prefix_special.'_Page', $page);
 		$event->redirect = 'in-commerce/orders/orders_'.$this->getSpecialByType($order_status).'_list';
 	}
 
 	/**
 	 * Reset's any selected order state to pending
 	 *
 	 * @param kEvent $event
 	 */
 	function OnResetToPending($event)
 	{
 		/** @var kDBItem $object */
 		$object = $event->getObject( Array('skip_autoload' => true) );
 
 		$items_info = $this->Application->GetVar($event->getPrefixSpecial(true));
 
 		if ( $items_info ) {
 			foreach ($items_info as $id => $field_values) {
 				$object->Load($id);
 				$object->SetDBField('Status', ORDER_STATUS_PENDING);
 
 				if ( $object->Update() ) {
 					$event->status = kEvent::erSUCCESS;
 				}
 				else {
 					$event->status = kEvent::erFAIL;
 					$event->redirect = false;
 					break;
 				}
 			}
 		}
 	}
 
 	/**
 	 * Creates list from items selected in grid
 	 *
 	 * @param kEvent $event
 	 */
 	function OnLoadSelected($event)
 	{
 		$event->setPseudoClass('_List');
 
 		/** @var kDBList $object */
 		$object = $event->getObject(array('selected_only' => true, 'per_page' => -1));
 
 		$event->redirect = false;
 	}
 
 	/**
 	 * Return orders list, that will expire in time specified
 	 *
 	 * @param int $pre_expiration timestamp
 	 * @return Array
 	 */
 	function getRecurringOrders($pre_expiration)
 	{
 		$ord_table = $this->Application->getUnitOption('ord', 'TableName');
 		$ord_idfield = $this->Application->getUnitOption('ord', 'IDField');
 
 		$processing_allowed = Array(ORDER_STATUS_PROCESSED, ORDER_STATUS_ARCHIVED);
 		$sql = 'SELECT '.$ord_idfield.', PortalUserId, GroupId, NextCharge
 				FROM '.$ord_table.'
 				WHERE (IsRecurringBilling = 1) AND (NextCharge < '.$pre_expiration.') AND Status IN ('.implode(',', $processing_allowed).')';
 		return $this->Conn->Query($sql, $ord_idfield);
 	}
 
 	/**
 	 * [SCHEDULED TASK] Checks what orders should expire and renew automatically (if such flag set)
 	 *
 	 * @param kEvent $event
 	 */
 	function OnCheckRecurringOrders($event)
 	{
 		$skip_clause = Array();
 		$ord_table = $this->Application->getUnitOption($event->Prefix, 'TableName');
 		$ord_idfield = $this->Application->getUnitOption($event->Prefix, 'IDField');
 
 		$pre_expiration = adodb_mktime() + $this->Application->ConfigValue('Comm_RecurringChargeInverval') * 3600 * 24;
 		$to_charge = $this->getRecurringOrders($pre_expiration);
 		if ($to_charge) {
 			$order_ids = Array();
 			foreach ($to_charge as $order_id => $record) {
 				// skip virtual users (e.g. root, guest, etc.) & invalid subscriptions (with no group specified, no next charge, but Recurring flag set)
 				if (!$record['PortalUserId'] || !$record['GroupId'] || !$record['NextCharge']) continue;
 
 				$order_ids[] = $order_id;
 				// prevent duplicate user+group pairs
 				$skip_clause[ 'PortalUserId = '.$record['PortalUserId'].' AND GroupId = '.$record['GroupId'] ] = $order_id;
 			}
 
 			// process only valid orders
 			$temp_handler = $this->Application->recallObject($event->Prefix.'_TempHandler', 'kTempTablesHandler');
 			$cloned_order_ids = $temp_handler->CloneItems($event->Prefix, 'recurring', $order_ids);
 			$order =&  $this->Application->recallObject($event->Prefix.'.recurring', null, Array('skip_autoload' => true));
 			foreach ($cloned_order_ids as $order_id) {
 				$order->Load($order_id);
 				$this->Application->HandleEvent($complete_event, $event->Prefix.'.recurring:OnCompleteOrder' );
 
 				if ($complete_event->status == kEvent::erSUCCESS) {
 					//send recurring ok email
 					$this->Application->emailUser('ORDER.RECURRING.PROCESSED', null, $this->OrderEmailParams($order));
 					$this->Application->emailAdmin('ORDER.RECURRING.PROCESSED');
 				}
 				else {
 					//send Recurring failed event
 					$order->SetDBField('Status', ORDER_STATUS_DENIED);
 					$order->Update();
 					$this->Application->emailUser('ORDER.RECURRING.DENIED', null, $this->OrderEmailParams($order));
 					$this->Application->emailAdmin('ORDER.RECURRING.DENIED');
 				}
 			}
 
 			// remove recurring flag from all orders found, not to select them next time script runs
 			$sql = 'UPDATE '.$ord_table.'
 					SET IsRecurringBilling = 0
 					WHERE '.$ord_idfield.' IN ('.implode(',', array_keys($to_charge)).')';
 			$this->Conn->Query($sql);
 		}
 
 		if ( !is_object($event->MasterEvent) ) {
 			// not called as hook
 			return ;
 		}
 
 		$pre_expiration = adodb_mktime() + $this->Application->ConfigValue('User_MembershipExpirationReminder') * 3600 * 24;
 		$to_charge = $this->getRecurringOrders($pre_expiration);
 
 		foreach ($to_charge as $order_id => $record) {
 			// skip virtual users (e.g. root, guest, etc.) & invalid subscriptions (with no group specified, no next charge, but Recurring flag set)
 			if (!$record['PortalUserId'] || !$record['GroupId'] || !$record['NextCharge']) continue;
 
 			// prevent duplicate user+group pairs
 			$skip_clause[ 'PortalUserId = '.$record['PortalUserId'].' AND GroupId = '.$record['GroupId'] ] = $order_id;
 		}
 		$skip_clause = array_flip($skip_clause);
 
 		$event->MasterEvent->setEventParam('skip_clause', $skip_clause);
 	}
 
 
 	function OnGeneratePDF($event)
 	{
 		$this->OnLoadSelected($event);
 
 		$this->Application->InitParser();
 		$o = $this->Application->ParseBlock(array('name'=>'in-commerce/orders/orders_pdf'));
 
 		/** @var FileHelper $file_helper */
 		$file_helper = $this->Application->recallObject('FileHelper');
 
 		$file_helper->CheckFolder(EXPORT_PATH);
 
 		$htmlFile = EXPORT_PATH . '/tmp.html';
 		$fh = fopen($htmlFile, 'w');
 		fwrite($fh, $o);
 		fclose($fh);
 //		return;
 
 
 //		require_once (FULL_PATH.'html2pdf/PDFEncryptor.php');
 
 		// Full path to the file to be converted
 //		$htmlFile = dirname(__FILE__) . '/test.html';
 
 		// The default domain for images that use a relative path
 		// (you'll need to change the paths in the test.html page
 		// to an image on your server)
 		$defaultDomain = DOMAIN;
 		// Full path to the PDF we are creating
 		$pdfFile = EXPORT_PATH . '/tmp.pdf';
 		// Remove old one, just to make sure we are making it afresh
 		@unlink($pdfFile);
 
 
 		$pdf_helper = $this->Application->recallObject('kPDFHelper');
 		$pdf_helper->FileToFile($htmlFile, $pdfFile);
 		return ;
 
 		// DOM PDF VERSION
 		/*require_once(FULL_PATH.'/dompdf/dompdf_config.inc.php');
 		$dompdf = new DOMPDF();
 		$dompdf->load_html_file($htmlFile);
 		if ( isset($base_path) ) {
 		  $dompdf->set_base_path($base_path);
 		}
 		$dompdf->set_paper($paper, $orientation);
 		$dompdf->render();
 		file_put_contents($pdfFile, $dompdf->output());
 		return ;*/
 
 		// Instnatiate the class with our variables
 		require_once (FULL_PATH.'/html2pdf/HTML_ToPDF.php');
 		$pdf = new HTML_ToPDF($htmlFile, $defaultDomain, $pdfFile);
 		$pdf->setHtml2Ps('/usr/bin/html2ps');
 		$pdf->setPs2Pdf('/usr/bin/ps2pdf');
 		$pdf->setGetUrl('/usr/local/bin/curl -i');
 		// Set headers/footers
 		$pdf->setHeader('color', 'black');
 		$pdf->setFooter('left', '');
 		$pdf->setFooter('right', '$D');
 
 		$pdf->setDefaultPath(BASE_PATH.'/kernel/admin_templates/');
 
 		$result = $pdf->convert();
 
 		// Check if the result was an error
 		if (PEAR::isError($result)) {
 		    $this->Application->ApplicationDie($result->getMessage());
 		}
 		else {
 			$download_url = rtrim($this->Application->BaseURL(), '/') . EXPORT_BASE_PATH . '/tmp.pdf';
 		    echo "PDF file created successfully: $result";
 		    echo '<br />Click <a href="' . $download_url . '">here</a> to view the PDF file.';
 		}
 	}
 
 	/**
 	 * 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 (defined('IS_INSTALL') && IS_INSTALL) {
 			return ;
 		}
 
 		$order_number = (int)$this->Application->ConfigValue('Comm_Order_Number_Format_P');
 		$order_sub_number = (int)$this->Application->ConfigValue('Comm_Order_Number_Format_S');
 
 		$calc_fields = $this->Application->getUnitOption($event->Prefix, 'CalculatedFields');
 		foreach ($calc_fields as $special => $fields) {
 			$calc_fields[$special]['OrderNumber'] = str_replace('6', $order_number, $calc_fields[$special]['OrderNumber']);
 			$calc_fields[$special]['OrderNumber'] = str_replace('3', $order_sub_number, $calc_fields[$special]['OrderNumber']);
 		}
 		$this->Application->setUnitOption($event->Prefix, 'CalculatedFields', $calc_fields);
 
 		$fields = $this->Application->getUnitOption($event->Prefix, 'Fields');
 		$fields['Number']['format'] = str_replace('%06d', '%0'.$order_number.'d', $fields['Number']['format']);
 		$fields['SubNumber']['format'] = str_replace('%03d', '%0'.$order_sub_number.'d', $fields['SubNumber']['format']);
 
 		/** @var SiteHelper $site_helper */
 		$site_helper = $this->Application->recallObject('SiteHelper');
 
 		$fields['BillingCountry']['default'] = $site_helper->getDefaultCountry('Billing');
 		$fields['ShippingCountry']['default'] = $site_helper->getDefaultCountry('Shipping');
 
 		if (!$this->Application->isAdminUser) {
 			$user_groups = explode(',', $this->Application->RecallVar('UserGroups'));
 			$default_group = $this->Application->ConfigValue('User_LoggedInGroup');
 			if (!in_array($default_group, $user_groups)){
 				$user_groups[] = $default_group;
 			}
 
 			$sql_part = '';
 
 			// limit payment types by domain
 			$payment_types = $this->Application->siteDomainField('PaymentTypes');
 
 			if (strlen($payment_types)) {
 				$payment_types = explode('|', substr($payment_types, 1, -1));
 				$sql_part .= ' AND PaymentTypeId IN (' . implode(',', $payment_types) . ')';
 			}
 
 			// limit payment types by user group
 			$sql_part .= ' AND (PortalGroups LIKE "%%,'.implode(',%%" OR PortalGroups LIKE "%%,', $user_groups).',%%")';
 
 			$fields['PaymentType']['options_sql'] = str_replace(
 				'ORDER BY ',
 				$sql_part . ' ORDER BY ',
 				$fields['PaymentType']['options_sql']
 			);
 		}
 
 		$this->Application->setUnitOption($event->Prefix, 'Fields', $fields);
 
 		$user_forms = $this->Application->getUnitOption('u', 'Forms');
 
 		$virtual_fields = $this->Application->getUnitOption($event->Prefix, 'VirtualFields');
 		$virtual_fields['UserPassword']['hashing_method'] = $user_forms['default']['Fields']['PasswordHashingMethod']['default'];
 		$this->Application->setUnitOption($event->Prefix, 'VirtualFields', $virtual_fields);
 	}
 
 	/**
 	 * Allows configuring export options
 	 *
 	 * @param kEvent $event
 	 * @return void
 	 * @access protected
 	 */
 	protected function OnBeforeExportBegin(kEvent $event)
 	{
 		parent::OnBeforeExportBegin($event);
 
 		/** @var kDBItem $object */
 		$object = $this->Application->recallObject($event->Prefix . '.export');
 
 		$object->SetField('Number', 999999);
 		$object->SetField('SubNumber', 999);
 	}
 
 	/**
 	 * 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__CustomerName' => 'CustomerName',
 			'__VIRTUAL__TotalAmount' => 'TotalAmount',
 			'__VIRTUAL__AmountWithoutVAT' =>	'AmountWithoutVAT',
 			'__VIRTUAL__SubtotalWithDiscount' =>	'SubtotalWithDiscount',
 			'__VIRTUAL__SubtotalWithoutDiscount' =>	'SubtotalWithoutDiscount',
 			'__VIRTUAL__OrderNumber' => 'OrderNumber',
 		);
 
 		return array_merge($columns, $new_columns);
 	}
 
 	/**
 	 * 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 ) {
 			return ;
 		}
 
 		foreach ( $this->trackCopiedOrderIDs($event) as $id ) {
 			$this->Application->removeObject($event->getPrefixSpecial());
 
 			$an_event = new kEvent($this->Prefix . ':Dummy');
 			$this->Application->SetVar($this->Prefix . '_id', $id);
 			$this->Application->SetVar($this->Prefix . '_mode', ''); // this is to fool ReserveItems to use live table
 			$this->ReserveItems($an_event);
 		}
 	}
 
 	/**
 	 * Occurs after an item has been copied to live table
 	 * Id of copied item is passed as event' 'id' param
 	 *
 	 * @param kEvent $event
 	 * @return void
 	 * @access protected
 	 */
 	protected function OnAfterCopyToLive(kEvent $event)
 	{
 		parent::OnAfterCopyToLive($event);
 
 		$this->trackCopiedOrderIDs($event, $event->getEventParam('id'));
 	}
 
 	/**
 	 * Tracks copied order IDs.
 	 *
 	 * @param kEvent  $event Event.
 	 * @param integer $id    Order ID.
 	 *
 	 * @return array
 	 */
 	protected function trackCopiedOrderIDs(kEvent $event, $id = null)
 	{
 		$setting_name = $event->Prefix . '_copied_ids' . $this->Application->GetVar('wid');
 		$ids = $this->Application->GetVar($setting_name, array());
 
 		if ( isset($id) ) {
 			array_push($ids, $id);
 			$this->Application->SetVar($setting_name, $ids);
 		}
 
 		return $ids;
 	}
 
 	/**
 	 * Checks, that currently loaded item is allowed for viewing (non permission-based)
 	 *
 	 * @param kEvent $event
 	 * @return bool
 	 * @access protected
 	 */
 	protected function checkItemStatus(kEvent $event)
 	{
 		if ( $this->Application->isAdminUser ) {
 			return true;
 		}
 
 		/** @var kDBItem $object */
 		$object = $event->getObject();
 
 		if ( !$object->isLoaded() ) {
 			return true;
 		}
 
 		return $object->GetDBField('PortalUserId') == $this->Application->RecallVar('user_id');
 	}
 
 	// ===== Gift Certificates Related =====
 	/**
 	 * Enter description here...
 	 *
 	 * @param kEvent $event
 	 */
 	function OnApplyGiftCertificate($event)
 	{
 		$code = $this->Application->GetVar('giftcert_code');
 
 		if ( $code == '' ) {
 			return;
 		}
 
 		/** @var OrdersItem $object */
 		$object = $event->getObject();
 
 		/** @var kDBItem $gift_certificate */
 		$gift_certificate = $this->Application->recallObject('gc', null, Array ('skip_autoload' => true));
 
 		$gift_certificate->Load($code, 'Code');
 
 		if ( !$gift_certificate->isLoaded() ) {
 			$event->status = kEvent::erFAIL;
 			$object->setCheckoutError(OrderCheckoutErrorType::GIFT_CERTIFICATE, OrderCheckoutError::GC_CODE_INVALID);
 			$event->redirect = false; // check!!!
 
 			return;
 		}
 
 		$debit = $gift_certificate->GetDBField('Debit');
 		$expire_date = $gift_certificate->GetDBField('Expiration');
 
 		if ( $gift_certificate->GetDBField('Status') != 1 || ($expire_date && $expire_date < adodb_mktime()) || ($debit <= 0) ) {
 			$event->status = kEvent::erFAIL;
 			$object->setCheckoutError(OrderCheckoutErrorType::GIFT_CERTIFICATE, OrderCheckoutError::GC_CODE_EXPIRED);
 			$event->redirect = false;
 
 			return;
 		}
 
 		$object->SetDBField('GiftCertificateId', $gift_certificate->GetDBField('GiftCertificateId'));
 		$object->Update();
 
 		$object->setCheckoutError(OrderCheckoutErrorType::GIFT_CERTIFICATE, OrderCheckoutError::GC_APPLIED);
 	}
 
 	/**
 	 * Removes gift certificate from order
 	 *
 	 * @param kEvent $event
 	 * @deprecated
 	 */
 	function OnRemoveGiftCertificate($event)
 	{
 		/** @var OrdersItem $object */
 		$object = $event->getObject();
 
 		$this->RemoveGiftCertificate($object);
 		$object->setCheckoutError(OrderCheckoutErrorType::GIFT_CERTIFICATE, OrderCheckoutError::GC_REMOVED);
 
 		$event->CallSubEvent('OnRecalculateItems');
 	}
 
 	function RemoveGiftCertificate(&$object)
 	{
 		$object->RemoveGiftCertificate();
 	}
 
 	function RecalculateGift($event)
 	{
 		/** @var OrdersItem $object */
 		$object = $event->getObject();
 
 		if ($object->GetDBField('Status') > ORDER_STATUS_PENDING) {
 			return ;
 		}
 		$object->RecalculateGift($event);
 	}
 
 	function GetWholeOrderGiftCertificateDiscount($gift_certificate_id)
 	{
 		if (!$gift_certificate_id) {
 			return 0;
 		}
 
 		$sql = 'SELECT Debit
 				FROM '.TABLE_PREFIX.'GiftCertificates
 				WHERE GiftCertificateId = '.$gift_certificate_id;
 		return $this->Conn->GetOne($sql);
 	}
 
 	/**
 	 * Downloads shipping tracking bar code, that was already generated by USPS service
 	 *
 	 * @param kEvent $event
 	 */
 	function OnDownloadLabel($event)
 	{
 		$event->status = kEvent::erSTOP;
 		ini_set('memory_limit', '300M');
 		ini_set('max_execution_time', '0');
 
 		/** @var kDBItem $object */
 		$object = $event->getObject();
 
 		$file = $object->GetDBField('ShippingTracking') . '.pdf';
 		$full_path = USPS_LABEL_FOLDER . $file;
 
 		if ( !file_exists($full_path) || !is_file($full_path) ) {
 			return;
 		}
 
 		$this->Application->setContentType(kUtil::mimeContentType($full_path), false);
 		header('Content-Disposition: attachment; filename="' . $file . '"');
 		readfile($full_path);
 	}
 
 	/**
 	 * Occurs before validation attempt
 	 *
 	 * @param kEvent $event
 	 * @return void
 	 * @access protected
 	 */
 	protected function OnBeforeItemValidate(kEvent $event)
 	{
 		parent::OnBeforeItemValidate($event);
 
 		$create_account = $this->Application->GetVar('create_account');
 
 		/** @var kDBItem $object */
 		$object = $event->getObject();
 
 		$required_fields = Array ('UserPassword', 'UserPassword_plain', 'VerifyUserPassword', 'VerifyUserPassword_plain');
 		$object->setRequired($required_fields, $create_account);
 
 		$billing_email = $object->GetDBField('BillingEmail');
 
 		if ( $create_account && $object->GetDBField('PortalUserId') == USER_GUEST && $billing_email ) {
 			// check that e-mail available
 			$sql = 'SELECT PortalUserId
 					FROM ' . TABLE_PREFIX . 'Users
 					WHERE Email = ' . $this->Conn->qstr($billing_email);
 			$user_id = $this->Conn->GetOne($sql);
 
 			if ( $user_id ) {
 				$object->SetError('BillingEmail', 'unique');
 			}
 		}
 	}
 
 	/**
 	 * Performs order update and returns results in format, needed by FormManager
 	 *
 	 * @param kEvent $event Event.
 	 *
 	 * @return void
 	 */
 	protected function OnUpdateAjax(kEvent $event)
 	{
 		/** @var AjaxFormHelper $ajax_form_helper */
 		$ajax_form_helper = $this->Application->recallObject('AjaxFormHelper');
 		$ajax_form_helper->transitEvent($event, 'OnUpdate');
 	}
 
 	/**
 	 * Performs order update after billing step submission and returns results in format, needed by FormManager
 	 *
 	 * @param kEvent $event Event.
 	 *
 	 * @return void
 	 */
 	protected function OnProceedToPreviewAjax(kEvent $event)
 	{
 		/** @var AjaxFormHelper $ajax_form_helper */
 		$ajax_form_helper = $this->Application->recallObject('AjaxFormHelper');
 		$ajax_form_helper->transitEvent($event, 'OnProceedToPreview');
 	}
 
 }
Index: branches/5.2.x/units/products/products_event_handler.php
===================================================================
--- branches/5.2.x/units/products/products_event_handler.php	(revision 16692)
+++ branches/5.2.x/units/products/products_event_handler.php	(revision 16693)
@@ -1,1600 +1,1600 @@
 <?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');
-			list ($combination_id, ) = each($combinations);
+			$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);
 
 		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;
 	}
 }