Index: branches/5.0.x/units/orders/orders_event_handler.php
===================================================================
--- branches/5.0.x/units/orders/orders_event_handler.php	(revision 13268)
+++ branches/5.0.x/units/orders/orders_event_handler.php	(revision 13269)
@@ -1,4129 +1,4131 @@
 <?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 permissions of user
 	 *
 	 * @param kEvent $event
 	 */
 	function CheckPermission(&$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');
 				$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 = array_shift($this->Application->getUnitOption($event->Prefix, 'StatusField'));
 					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 == 'OnUpdate') {
 						// all checks were ok -> it's user's order -> allow to modify
 						return true;
 					}
 				}
 			}
 		}
 
 		return parent::CheckPermission($event);
 	}
 
 	/**
 	 * Allows to override standart permission mapping
 	 *
 	 */
 	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),
 								'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),
 								'OnCompleteOrder'		=>	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);
 	}
 
 	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)
 	{
 		$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 ;
 		}
 
 		$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) {
 			$cs_helper =& $this->Application->recallObject('CountryStatesHelper');
 			$address =& $this->Application->recallObject('addr.-item','addr', Array('skip_autoload' => true));
 			$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 == 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');
 		}
 	}
 
 	/**
 	 * Enter description here...
 	 *
 	 * @param unknown_type $event
 	 * @return unknown
 	 */
 	function OnUserLogin(&$event)
 	{
 		if( !($event->MasterEvent->status == erSUCCESS) ) {
 			return false;
 		}
 
 		$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');
 	}
 
 	function updateUserID($order_id, &$event)
 	{
 		$table = $this->Application->getUnitOption($event->Prefix,'TableName');
 		$id_field = $this->Application->getUnitOption($event->Prefix,'IDField');
 		$user_id = $this->Application->RecallVar('user_id');
 		$this->Conn->Query('UPDATE '.$table.' SET PortalUserId = '.$user_id.' WHERE '.$id_field.' = '.$order_id);
 
 		$affiliate_id = $this->isAffiliate($user_id);
 		if($affiliate_id)
 		{
 			$this->Conn->Query('UPDATE '.$table.' SET AffiliateId = '.$affiliate_id.' WHERE '.$id_field.' = '.$order_id);
 		}
 	}
 
 	function isAffiliate($user_id)
 	{
 		$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;
 	}
 
 	function ChargeOrder(&$order)
 	{
 		$gw_data = $order->getGatewayData();
 
 		$this->Application->registerClass( $gw_data['ClassName'], GW_CLASS_PATH.'/'.$gw_data['ClassFile'] );
 		$gateway_object =& $this->Application->recallObject( $gw_data['ClassName'] );
 
 		$payment_result = $gateway_object->DirectPayment($order->FieldValues, $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());
 	}
 
 	function OrderEmailParams(&$order)
 	{
 		$billing_email = $order->GetDBField('BillingEmail');
 		$user_email = $this->Conn->GetOne('	SELECT Email FROM '.$this->Application->getUnitOption('u', 'TableName').'
 											WHERE PortalUserId = '.$order->GetDBField('PortalUserId'));
 		$email_params = Array();
 		$email_params['_user_email'] = $user_email; //for use when shipping vs user is required in InvetnoryAction
 		$email_params['to_email'] = $billing_email ? $billing_email : $user_email;
 		$email_params['to_name'] = $order->GetDBField('BillingTo');
 		return $email_params;
 	}
 
 	function PrepareCoupons(&$event, &$order)
 	{
 		$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
 					$coupon =& $this->Application->recallObject('coup',null,array('skip_autload' => true));
 					/* @var $coupon kDBItem */
 					$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 $order OrdersItem */
 		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);
 		if (!$this->CheckQuantites($event)) return;
 
 		$this->ReserveItems($event);
 
 		$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->redirect_params['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 = erFAIL;
 			}
 			return false;
 		}
 
 		// call CompleteOrder events for items in order BEFORE SplitOrder (because ApproveEvents are called there)
 		$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);
 			}
 
 			$email_event_user 	=& $this->Application->EmailEventUser('ORDER.SUBMIT', $order->GetDBField('PortalUserId'), $this->OrderEmailParams($order));
 			$email_event_admin 	=& $this->Application->EmailEventAdmin('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->redirect_params['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);
 
 		$this->Application->StoreVar('front_order_id', $order_id);
 		$this->Application->RemoveVar('ord_id');
 	}
 
 	/**
 	 * Set billing address same as shipping
 	 *
 	 * @param kEvent $event
 	 */
 	function setBillingAddress(&$event)
 	{
 		$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);
 
 				$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)
 	{
 		$env = $this->Application->GetVar('continue_shopping_template');
 		if (!$env || $env == '__default__') {
 			$env = $this->Application->RecallVar('continue_shopping');
 		}
 		if (!$env) {
 			$env = 'in-commerce/index';
 		}
 
 		$event->redirect = $env;
 	}
 
 	/**
 	 * Enter description here...
 	 *
 	 * @param kEvent $event
 	 */
 	function OnCheckout(&$event)
 	{
 		$this->OnUpdateCart($event);
 		if ($event->getEventParam('RecalculateChangedCart'))
 		{
 			$event->SetRedirectParam('checkout_error', $event->redirect_params['checkout_error']);
 		}
 		else
 		{
 			$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->redirect_params['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);
 
 			$object =& $event->getObject();
 			$payment_type_id = $object->GetDBField('PaymentType');
 			if(!$payment_type_id)
 			{
 				$default_type = $this->Conn->GetOne('SELECT PaymentTypeId FROM '.TABLE_PREFIX.'PaymentTypes WHERE IsPrimary = 1');
 				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');
 	}
 
 	function OnCancelRecurring(&$event)
 	{
 		$order =& $event->GetObject();
 		$order->SetDBField('IsRecurringBilling', 0);
 		$order->Update();
 		if ($this->Application->GetVar('cancelrecurring_ok_template'))
 		{
 			$event->redirect = $this->Application->GetVar('cancelrecurring_ok_template');
 		}
 	}
 
 	/**
 	 * Enter description here...
 	 *
 	 * @param kEvent $event
 	 */
 	function OnAfterItemUpdate(&$event)
 	{
 		$object =& $event->getObject();
 
 		$cvv2 = $object->GetDBField('PaymentCVV2');
  		if($cvv2 !== false) $this->Application->StoreVar('CVV2Code', $cvv2);
 	}
 
 
 	/**
 	 * Enter description here...
 	 *
 	 * @param kEvent $event
 	 */
 	function OnUpdate(&$event)
 	{
 		$this->setBillingAddress($event);
 
 		$cs_helper =& $this->Application->recallObject('CountryStatesHelper');
 
 		$object =& $event->getObject();
 		if( $object->HasTangibleItems() )
 		{
 			$cs_helper->CheckStateField($event, 'ShippingState', 'ShippingCountry');
 		}
 
 		$cs_helper->CheckStateField($event, 'BillingState', 'BillingCountry');
 
 		parent::OnUpdate($event);
 
 		if ($this->Application->isAdminUser) {
 			return true;
 		}
 		else {
 			$event->redirect_params = Array('opener' => 's');
 		}
 
 		if ($event->status == erSUCCESS) {
 			$this->createMissingAddresses($event);
 		}
 		else {
 			// strange: recalculate total amount on error
 			$object->SetDBField('TotalAmount', $object->getTotalAmount());
 		}
 	}
 
 	/**
 	 * Creates new address
 	 *
 	 * @param kEvent $event
 	 */
 	function createMissingAddresses(&$event)
 	{
 		if (!$this->Application->LoggedIn()) {
 			return false;
 		}
 
 		$object =& $event->getObject();
 		$addr_list =& $this->Application->recallObject('addr', 'addr_List', Array('per_page'=>-1, 'skip_counting'=>true) );
 		$addr_list->Query();
 
 		$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();
 		}
 	}
 
 	/**
 	 * Enter description here...
 	 *
 	 * @param kEvent $event
 	 */
 	function OnPreSave(&$event)
 	{
 		$cs_helper =& $this->Application->recallObject('CountryStatesHelper');
 
 		$object =& $event->getObject();
 		if ( $object->GetID() !== false)
 		{
 			if( $object->HasTangibleItems() )
 			{
 				$cs_helper->CheckStateField($event, 'ShippingState', 'ShippingCountry');
 			}
 			$cs_helper->CheckStateField($event, 'BillingState', 'BillingCountry');
 		}
 
 		parent::OnPreSave($event);
 	}
 
 	function OnUpdateCart(&$event)
 	{
 		$this->Application->HandleEvent($items_event, 'orditems:OnUpdate');
 		return $event->CallSubEvent('OnRecalculateItems');
 	}
 
 	/**
 	 * 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;
 			$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();
 				$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 == erSUCCESS && !$event->redirect) {
 			$event->redirect_params['pass'] = 'm';
 			$event->redirect_params['pass_category'] = 0; //otherwise mod-rewrite shop-cart URL will include category
 			$event->redirect = true;
 		}
 		else {
 			if ($this->Application->isAdminUser) {
 				$event->redirect_params['opener'] = 'u';
 			}
 		}
 	}
 
 	/**
 	 * 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 = erSUCCESS;
 			$event->redirect = $this->Application->isAdminUser ? true : $this->Application->GetVar('shop_cart_template');
 		}
 		else {
 			$event->status = 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;
 
 		$ord_item =& $this->Application->recallObject('orditems.-opt', null, Array ('skip_autoload' => true));
 		/* @var $ord_item kDBItem */
 
 		$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 == erSUCCESS && $this->Application->isAdminUser) {
 			$event->redirect_params['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');
 //		$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);
 
 /*
 		// Customization healtheconomics.org
 		$item_data['DurationType'] = $pricing_info['DurationType'];
 		$item_data['AccessExpiration'] = $pricing_info['AccessExpiration'];
 		// Customization healtheconomics.org --
 */
 
 		$item_data['Duration'] = $pricing_info['AccessDuration'] * $unit_secs[ $pricing_info['AccessUnit'] ];
 
 		return $item_data;
 	}
 
 	function OnRemoveCoupon(&$event)
 	{
 		$object =& $event->getObject();
 		$this->RemoveCoupon($object);
 		$event->CallSubEvent('OnRecalculateItems');
 		$event->SetRedirectParam('checkout_error', 7);
 	}
 
 	function RemoveCoupon(&$object)
 	{
 		$coupon_id = $object->GetDBField('CouponId');
 		$coupon =& $this->Application->recallObject('coup', null, Array('skip_autoload' => true));
 		$res = $coupon->Load($coupon_id);
 		$uses = $coupon->GetDBField('NumberOfUses');
 
 		if($res && isset($uses))
 		{
 			$coupon->SetDBField('NumberOfUses', $uses + 1);
 			$coupon->SetDBField('Status', 1);
 			$coupon->Update();
 		}
 		$object->SetDBField('CouponId', 0);
 		$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);
 
 		$event->redirect = $this->Application->GetVar('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
 	 */
 	function getPassedId(&$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)
 			return $this->Application->RecallVar('front_order_id');
 		}
 
 		$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;
 			}
 		}
 		else {
 			// 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"
 		}
 
 		return $passed;
 	}
 
 	/**
 	 * Load item if id is available
 	 *
 	 * @param kEvent $event
 	 */
 	function LoadItem(&$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
 
 			$object =& $event->getObject();
 			/* @var $object kDBItem */
 
 			$object->Clear($id);
 			return ;
 		}
 
 		parent::LoadItem($event);
 	}
 
 	/**
 	 * Creates new shopping cart
 	 *
 	 * @param kEvent $event
 	 */
 	function _createNewCart(&$event)
 	{
 		$object =& $event->getObject( Array('skip_autoload' => true) );
 		/* @var $object kDBItem */
 
 		$object->SetDBField('Number', $this->getNextOrderNumber($event));
 		$object->SetDBField('SubNumber', 0);
 		$object->SetDBField('Type', ORDER_STATUS_INCOMPLETE);
 		$object->SetDBField('VisitId', $this->Application->RecallVar('visit_id') );
 
 		// get user
 		$user_id = $this->Application->RecallVar('user_id');
 		if (!$user_id) {
 			$user_id = -2; // 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');
 			$object->SetDBField('AffiliateId', $affiliate_storage_method == 1 ? (int)$this->Application->RecallVar('affiliate_id') : (int)$this->Application->GetVar('affiliate_id') );
 		}
 
 		// get payment type
 		$default_type = $this->Conn->GetOne('SELECT PaymentTypeId FROM '.TABLE_PREFIX.'PaymentTypes WHERE IsPrimary = 1');
 		if ($default_type) {
 			$object->SetDBField('PaymentType', $default_type);
 		}
 
 		$created = $object->Create();
 		if ($created) {
 			$id = $object->GetID();
 
 			$this->Application->SetVar($event->getPrefixSpecial(true) . '_id', $id);
 			$this->Application->StoreVar($event->getPrefixSpecial(true) . '_id', $id);
 
 			return $id;
 		}
 
 		return 0;
 	}
 
 	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 simultaniously
 	 *
 	 * @param kEvent $event
 	 */
 	function SetStepRequiredFields(&$event)
 	{
 		$order =& $event->getObject();
 
 		$cs_helper =& $this->Application->recallObject('CountryStatesHelper');
 		/* @var $cs_helper kCountryStatesHelper */
 
 		$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);
 		}
 		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');
 			foreach ($req_fields as $field) {
 				$order->Fields[$field]['required'] = $has_tangibles;
 			}
 			if ($cs_helper->CountryHasStates( getArrayValue($field_values, 'ShippingCountry') )) {
 				$order->Fields['ShippingState']['required'] = true; // $has_tangibles
 			}
 		}
 
 		// billing address required fields
 		if ($this->Application->GetVar('check_billing_address')) {
 			$req_fields = array('BillingTo', 'BillingAddress1', 'BillingCity', 'BillingZip', 'BillingCountry', 'BillingPhone');
 			foreach ($req_fields as $field) {
 				$order->Fields[$field]['required'] = true;
 			}
 			if ($cs_helper->CountryHasStates( getArrayValue($field_values, 'BillingCountry') )) {
 				$order->Fields['BillingState']['required'] = true;
 			}
 		}
 
 		$check_cc = $this->Application->GetVar('check_credit_card');
 		$ord_event = $this->Application->GetVar($event->getPrefixSpecial().'_event');
 		if (($ord_event !== 'OnProceedToPreview') && !$this->Application->isAdmin) {
 			// don't check credit card when going from "billing info" to "order preview" step
 			$check_cc = 0;
 		}
 
 		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');
 			}
 
 			foreach ($req_fields as $field) {
 				$order->Fields[$field]['required'] = true;
 			}
 		}
 	}
 
 	/**
 	 * Set's order's user_id to user from session or Guest otherwise
 	 *
 	 * @param kEvent $event
 	 */
 	function CheckUser(&$event)
 	{
 		if ($this->Application->isAdminUser) {
 			return;
 		}
 
 		$order =& $event->GetObject();
 
 		$ses_user = $this->Application->RecallVar('user_id');
 
 		if ($order->GetDBField('PortalUserId') != $ses_user) {
 			if ($ses_user == 0) $ses_user = -2; //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 and populate it
 	 * with items selected in the grid
 	 *
 	 * @param kEvent $event
 	 */
 	function OnPreCreate(&$event)
 	{
 		parent::OnPreCreate($event);
 
 		$object =& $event->getObject();
 		$new_number = $this->getNextOrderNumber($event);
 		$object->SetDBField('Number', $new_number);
 		$object->SetDBField('SubNumber', 0);
 		$object->SetDBField('OrderIP', $_SERVER['REMOTE_ADDR']);
 		$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
 	 */
 	function OnBeforeClone(&$event)
 	{
 		$object =& $event->getObject();
 
 		if (substr($event->Special, 0, 9) == 'recurring') {
 			$object->SetDBField('SubNumber', $object->getNextSubNumber());
 			$object->SetDBField('OriginalAmount', 0); // needed in this case ?
 		}
 		else {
 			$new_number = $this->getNextOrderNumber($event);
 			$object->SetDBField('Number', $new_number);
 			$object->SetDBField('SubNumber', 0);
 			$object->SetDBField('OriginalAmount', 0);
 		}
 		$object->SetDBField('OrderDate', adodb_mktime());
 		$object->UpdateFormattersSubFields();
 		$object->SetDBField('GWResult1', '');
 		$object->SetDBField('GWResult2', '');
 	}
 
 	function OnReserveItems(&$event)
 	{
 		$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
 
 
 		$product_object =& $this->Application->recallObject('p', null, Array('skip_autoload' => true));
 		$product_object->SwitchToLive();
 
 		$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);
 
 		$event->status = 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 ($to_reserve < $lack) $event->status = erFAIL; // if we can't reserve the full lack
 
 			//reserve in order
 			$order_item->SetDBFieldsFromHash($rec);
 			$order_item->SetDBField('QuantityReserved', $rec['QuantityReserved'] + $to_reserve);
 			$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->FieldValues;
 				}
 			}
 			$order_items->GoNext();
 		}
 		return true;
 	}
 
 	function OnOrderPrint(&$event)
 	{
 		$event->redirect_params = Array('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
 		$object =& $event->getObject();
 
 		// update values from submit
 		$items_info = $this->Application->GetVar( $event->getPrefixSpecial(true) );
 		if($items_info) $field_values = array_shift($items_info);
 		$object->SetFieldsFromHash($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);
 			}
 		}
 
 		$this->finalizePopup($event);
 	}
 
 	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 = 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->FieldValues;
 			}
 
 			$order_items->GoNext();
 		}
 		return true;
 	}
 
 	function restoreOrder(&$order_items)
 	{
 		$product_object =& $this->Application->recallObject('p', null, Array('skip_autoload' => true));
 		$product_object->SwitchToLive();
 
 		$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();
 
 			$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();
 
 			$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->FieldValues;
 
 				// using freed qty to fullfill possible backorders
 				$product_h->FullfillBackOrders($product_object, $inv_object->GetID());
 			}
 			else {
 				// using freed qty to fullfill 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 = erFAIL;
 			return;
 		}
 
 		// process order products
 		$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 = 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,
 		);
 		$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();
 
 		$object =& $this->Application->recallObject($event->Prefix.'.-inv');
 		/* @var $object OrdersItem */
 
 		if ($object->GetDBField('OnHold')) {
 			// any actions have no effect while on hold
 			return ;
 		}
 
 		// preparing new status, but not setting it yet
 		$object->SetDBField('Status', $event_status_map[$event->Name]);
 
 		$set_new_status = false;
 		$event->status = erSUCCESS;
 
 		$email_params = $this->OrderEmailParams($object);
 
 		switch ($event->Name) {
 			case 'OnMassOrderApprove':
 			case 'OnOrderApprove':
 				$set_new_status = false; //on succsessfull 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();
 
 				$this->Application->registerClass( $gw_data['ClassName'], GW_CLASS_PATH.'/'.$gw_data['ClassFile'] );
 				$gateway_object =& $this->Application->recallObject( $gw_data['ClassName'] );
 
 				$charge_result = $gateway_object->Charge($object->FieldValues, $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) {
 					$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) {
 						$email_event_user =& $this->Application->EmailEventUser('ORDER.APPROVE', $object->GetDBField('PortalUserId'), $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);
 				}
 				$email_event_user =& $this->Application->EmailEventUser('ORDER.DENY', $object->GetDBField('PortalUserId'), $email_params);
 
 				if ($event->Name == 'OnMassOrderDeny' || $event->Name == 'OnOrderDeny') {
 					// inform payment gateway that order was declined
 					$gw_data = $object->getGatewayData();
 
 					$this->Application->registerClass( $gw_data['ClassName'], GW_CLASS_PATH.'/'.$gw_data['ClassFile'] );
 					$gateway_object =& $this->Application->recallObject( $gw_data['ClassName'] );
 
 					$gateway_object->OrderDeclined($object->FieldValues, $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();
 
 				// try to create usps order
 				if ( $object->GetDBField('ShippingType') == 0 && strpos($object->GetDBField('ShippingInfo'), 'USPS')) {
 					$ses_usps_erros = Array();
 					$ret = $this->MakeUSPSOrder($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'];
 					$email_event_user =& $this->Application->EmailEventUser('ORDER.SHIP', $object->GetDBField('PortalUserId'), $email_params);
 
 					// inform payment gateway that order was shipped
 					$gw_data = $object->getGatewayData();
 
 					$this->Application->registerClass( $gw_data['ClassName'], GW_CLASS_PATH.'/'.$gw_data['ClassFile'] );
 					$gateway_object =& $this->Application->recallObject( $gw_data['ClassName'] );
 
 					$gateway_object->OrderShipped($object->FieldValues, $gw_data['gw_params']);
 				}
 				else {
 					$usps_errors[$object->GetField('OrderNumber')] = $ret['error_description'];
 					$ses_usps_erros = Array();
 					$ses_usps_erros = unserialize($this->Application->RecallVar('usps_errors'));
 					if ( is_array($ses_usps_erros) ) {
 						$usps_errors = array_merge($usps_errors, $ses_usps_erros);
 					}
 					$this->Application->StoreVar('usps_errors', serialize($usps_errors));
 				}
 
 				break;
 
 			case 'OnMassOrderProcess':
 			case 'OnOrderProcess':
 				if ($this->ReadyToProcess($object->GetID())) {
 					$event->CallSubEvent('OnReserveItems');
 					if ($event->status == erSUCCESS) $set_new_status = true;
 					$email_event_user =& $this->Application->EmailEventUser('BACKORDER.PROCESS', $object->GetDBField('PortalUserId'), $email_params);
 				} else {
 					$event->status = 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);
 	}
 
 	/**
 	 * Get next free order number
 	 *
 	 * @param kEvent $event
 	 */
 	function getNextOrderNumber(&$event)
 	{
 		$object =& $event->getObject();
 		$sql = 'SELECT MAX(Number)
 				FROM ' . $this->Application->GetLiveName($object->TableName);
 		return max($this->Conn->GetOne($sql) + 1, $this->Application->ConfigValue('Comm_Next_Order_Number'));
 	}
 
 	/**
 	 * 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 theese 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 additional view filters set from "Orders" => "Search" tab
 	 *
 	 * @param kEvent $event
 	 */
 	function AddFilters(&$event)
 	{
 		parent::AddFilters($event);
 
 		if($event->Special != 'search') return true;
 
 		$search_filter = $this->Application->RecallVar('ord.search_search_filter');
 		if(!$search_filter) return false;
 
 		$search_filter = unserialize($search_filter);
 		$event->setPseudoClass('_List');
 		$object =& $event->getObject();
 
 		foreach($search_filter as $filter_name => $filter_params)
 		{
 			$filter_type = $filter_params['type'] == 'where' ? WHERE_FILTER : HAVING_FILTER;
 			$object->addFilter($filter_name, $filter_params['value'], $filter_type, FLT_VIEW);
 		}
 	}
 
 	/**
 	 * Set's status incomplete to all cloned orders
 	 *
 	 * @param kEvent $event
 	 */
 	function 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
 	 * @access public
 	 */
 	function OnAfterItemLoad(&$event)
 	{
 		// get user fields
 		$object =& $event->getObject();
 		$user_id = $object->GetDBField('PortalUserId');
 		if($user_id)
 		{
 			$user_info = $this->Conn->GetRow('SELECT *, CONCAT(FirstName,\' \',LastName) AS UserTo FROM '.TABLE_PREFIX.'PortalUser WHERE PortalUserId = '.$user_id);
 
 			$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') );
 
 		$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'));
 	}
 
 	/**
 	 * Enter description here...
 	 *
 	 * @param kEvent $event
 	 */
 	function OnBeforeItemUpdate(&$event)
 	{
 		$cs_helper =& $this->Application->recallObject('CountryStatesHelper');
 		$cs_helper->PopulateStates($event, 'ShippingState', 'ShippingCountry');
 		$cs_helper->PopulateStates($event, 'BillingState', 'BillingCountry');
 
 		$object = &$event->getObject();
 		if ($object->GetDBField('Status') > ORDER_STATUS_PENDING) return;
 
 		$this->CheckUser($event);
 		if(!$object->GetDBField('OrderIP'))
 		{
 			$object->SetDBField('OrderIP', $_SERVER['REMOTE_ADDR']);
 		}
 
 		$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);
 	}
 
 	/**
 	 * Apply any custom changes to list's sql query
 	 *
 	 * @param kEvent $event
 	 * @access protected
 	 * @see OnListBuild
 	 */
 	function SetCustomQuery(&$event)
 	{
 		$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 As BackOrderFlag' : 'BackOrderFlag';
 
 
 		// setting PackageNum to 0 for Non-tangible items, for tangibles first package num is always 1
 		$query = '	SELECT OrderItemId
 					FROM '.$table_prefix.'OrderItems
 					LEFT JOIN '.TABLE_PREFIX.'Products
 					ON '.TABLE_PREFIX.'Products.ProductId = '.$table_prefix.'OrderItems.ProductId
 					WHERE '.TABLE_PREFIX.'Products.Type > 1 AND 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
 					'.$backorder_select.',
 					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 BackOrderFlag, Grouping
 				ORDER BY BackOrderFlag 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) {
 			$sub_order =& $this->Application->recallObject('ord.-sub'.$next_sub_number, 'ord');
 			/* @var $sub_order OrdersItem */
 
 			if ($this->UseTempTables($event) && $next_sub_number == 0) {
 				$sub_order =& $order;
 			}
 			$sub_order->SetDBFieldsFromHash($order->FieldValues);
 			$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['BackOrderFlag'] > 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
 					$email_event_user 	=& $this->Application->EmailEventUser('BACKORDER.ADD', $sub_order->GetDBField('PortalUserId'), $this->OrderEmailParams($sub_order));
 		    		$email_event_admin 	=& $this->Application->EmailEventAdmin('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 '.$backorder_select.', oi.*
 								FROM '.TABLE_PREFIX.'OrderItems oi
 								LEFT JOIN '.TABLE_PREFIX.'Products p ON p.ProductId = oi.ProductId
 								WHERE (OrderId = %s) AND (BackOrderFlag = 0) AND (p.Type = '.PRODUCT_TYPE_TANGIBLE.')';
 
 							$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 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 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 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++;
 			$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);
 			}
 		}
 	}
 
 	/**
 	 * Updates product info in shopping cart
 	 *
 	 * @param kEvent $event
 	 * @param unknown_type $prod_id
 	 * @param unknown_type $back_order
 	 * @param unknown_type $qty
 	 * @param unknown_type $price
 	 * @param unknown_type $discounted_price
 	 * @param unknown_type $discount_type
 	 * @param unknown_type $discount_id
 	 * @param unknown_type $order_item_id
 	 * @param unknown_type $options_salt
 	 * @param unknown_type $passed_item_data
 	 * @param unknown_type $cost
 	 * @return unknown
 	 */
 	function UpdateOrderItem(&$event, $prod_id, $back_order, $qty, $price, $discounted_price, $discount_type, $discount_id, $order_item_id = 0, $options_salt = 0, $passed_item_data=null, $cost=0)
 	{
 		$price = (float) $price;
 		$discounted_price = (float) $discounted_price;
 		$qty = (int) $qty;
 
 		$ord_id = $this->getPassedId($event);
 
 		$table_prefix = $this->TablePrefix($event);
 
 		if($order_item_id)
 		{
 			$query = '	SELECT OrderItemId, Quantity, FlatPrice, Price, BackOrderFlag, ItemData FROM '.$table_prefix.'OrderItems
 						WHERE OrderItemId = '.$order_item_id;
 		}
 		else
 		{
 			// try to load specified Product by its Id and BackOrderFlag in the order
 			$query = 'SELECT OrderItemId, Quantity, FlatPrice, Price, BackOrderFlag, ItemData FROM '.$table_prefix.'OrderItems
 								WHERE
 									OrderId = '.$ord_id.'
 									AND
 									ProductId = '.$prod_id.'
 									AND
 									BackOrderFlag '.($back_order ? ' >= 1' : ' = 0').'
 									AND
 									OptionsSalt = '.$options_salt;
 		}
 		$item_row = $this->Conn->GetRow($query);
 		$item_id = $item_row['OrderItemId'];
 
 		$object =& $this->Application->recallObject('orditems.-item', null, Array('skip_autoload' => true));
 		$item_data = $item_row['ItemData'];
 		if($item_data)
 		{
 			$item_data = unserialize($item_data);
 		}
 		$orig_discount_type = (int)getArrayValue($item_data, 'DiscountType');
 		$orig_discount_id = (int)getArrayValue($item_data, 'DiscountId');
 
 		if ($item_id) { // if Product already exists in the order
 			if ($qty > 0 &&
 				$item_row['Quantity'] == $qty &&
 				round($item_row['FlatPrice'], 3) == round($price, 3) &&
 				round($item_row['Price'], 3) == round($discounted_price, 3)	&&
 				$orig_discount_type == $discount_type &&
 				$orig_discount_id == $discount_id)
 			{
 				return false;
 			}
 			$object->Load($item_id);
 			if ($qty > 0) { // Update Price by _TOTAL_ qty
 				$object->SetDBField('Quantity', $qty);
 				$object->SetDBField('FlatPrice', $price );
 				$object->SetDBField('Price', $discounted_price );
 				$object->SetDBField('Cost', $cost);
 				if($item_data = $object->GetDBField('ItemData'))
 				{
 					$item_data = unserialize($item_data);
 				}
 				else
 				{
 					$item_data = Array();
 				}
 				$item_data['DiscountType'] = $discount_type;
 				$item_data['DiscountId'] = $discount_id;
 				$object->SetDBField('ItemData', serialize($item_data));
 				$object->Update();
 			}
 			else { // delete products with 0 qty
 				$object->Delete();
 			}
 		}
 		elseif ($qty > 0) { // if we are adding product
 			$product =& $this->Application->recallObject('p', null, Array ('skip_autoload' => true));
 			$product->Load($prod_id);
 			$object->SetDBField('ProductId', $prod_id);
 			$object->SetDBField('ProductName', $product->GetField('Name'));
 			$object->SetDBField('Quantity', $qty);
 			$object->SetDBField('FlatPrice', $price );
 			$object->SetDBField('Price', $discounted_price );
 			$object->SetDBField('Cost', $cost);
 			$object->SetDBField('OrderId', $ord_id);
 			$object->SetDBField('BackOrderFlag', $back_order);
 			if ($passed_item_data && !is_array($passed_item_data)) {
 				$passed_item_data = unserialize($passed_item_data);
 			}
 //			$item_data = Array('DiscountType' => $discount_type, 'DiscountId' => $discount_id);
 			$item_data = $passed_item_data;
 			$object->SetDBField('ItemData', serialize($item_data));
 			$object->Create();
 			if( $this->UseTempTables($event) ) $object->SetTempId();
 		}
 		else {
 			return false; // item requiring to set qty to 0, meaning already does not exist
 		}
 		return true;
 	}
 
 	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
 		$product =& $this->Application->recallObject('p.toadd', null, Array('skip_autoload' => true));
 		/* @var $product kDBItem */
 
 		$product->Load($item_id);
 
 		$object =& $this->Application->recallObject('orditems.-item', null, Array('skip_autoload' => true));
 		/* @var $object kDBItem */
 
 		$order =& $this->Application->recallObject('ord');
 		/* @var $order kDBItem */
 
 		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;
 		}
 
 		$ord_id = $order->GetID();
 
 		if ($item_data = $event->getEventParam('ItemData')) {
 			$item_data = unserialize($item_data);
 		}
 		else {
 			$item_data = Array ();
 		}
 
 		$options = getArrayValue($item_data, 'Options');
 		if (!$this->CheckOptions($event, $options, $item_id, $qty, $product->GetDBField('OptionsSelectionMode'))) return;
 
 		// Checking if such product already exists in the cart
 		$keys['OrderId'] = $ord_id;
 		$keys['ProductId'] = $product->GetId();
 
 		if (isset($item_data['Options'])) {
 			$options_salt = $this->OptionsSalt($item_data['Options']);
 			$keys['OptionsSalt'] = $options_salt;
 		}
 		else {
 			$options_salt = null;
 		}
 
 		$exists = $object->Load($keys);
 
 		$object->SetDBField('ProductId', $product->GetId());
 		$object->SetDBField('ProductName', $product->GetField('l'.$this->Application->GetDefaultLanguageId().'_Name'));
 		$object->SetDBField('Weight', $product->GetDBField('Weight'));
 		if (isset($item_data['Options'])) {
 			$object->SetDBField('OptionsSalt', $options_salt);
 		}
 
 		if (isset($package_num)) {
 			$object->SetDBField('PackageNum', $package_num);
 		}
 
 		if($product->GetDBField('Type') == PRODUCT_TYPE_TANGIBLE || $product->GetDBField('Type') == 6)
 		{
 			$object->SetDBField('Quantity', $object->GetDBField('Quantity') + $qty);
 		}
 		else // Types: 2,3,4
 		{
 			$object->SetDBField('Quantity', $qty); // 1
 			$exists = false;
 		}
 
 		if (isset($item_data['ForcePrice'])) {
 			$price = $item_data['ForcePrice'];
 		}
 		else {
 			$price = $this->GetPlainProductPrice($product->GetId(), $object->GetDBField('Quantity'), $product->GetDBField('Type'), $order, $options_salt, $item_data);
 		}
 
 		$cost = $this->GetProductCost($product->GetId(), $object->GetDBField('Quantity'), $product->GetDBField('Type'), $options_salt, $item_data);
 		$object->SetDBField('FlatPrice', $price);
 		$couponed_price = $this->GetCouponDiscountedPrice($order->GetDBField('CouponId'), $product->GetId(), $price);
 		$discounted_price = $this->GetDiscountedProductPrice($product->GetId(), $price, $discount_id, $order);
 		if( $couponed_price < $discounted_price )
 		{
 			$discounted_price = $couponed_price;
 			$discount_type = 'coupon';
 			$discount_id = $order->GetDBField('CouponId');
 		}
 		else
 		{
 			$discount_type = 'discount';
 			$discount_id = $discount_id;
 		}
 
 		$item_data['DiscountType'] = $discount_type;
 		$item_data['DiscountId'] = $discount_id;
 		$item_data['IsRecurringBilling'] = $product->GetDBField('IsRecurringBilling');
 
 		// it item is processed in order using new style, then put such mark in orderitem record
 		$processing_data = $product->GetDBField('ProcessingData');
 		if ($processing_data) {
 			$processing_data = unserialize($processing_data);
 			if (getArrayValue($processing_data, 'HasNewProcessing')) {
 				$item_data['HasNewProcessing'] = 1;
 			}
 		}
 		$object->SetDBField('ItemData', serialize($item_data));
 
 		$object->SetDBField('Price', $discounted_price);	// will be retrieved later
 		$object->SetDBField('Cost', $cost);
 
 		$object->SetDBField('BackOrderFlag', 0); // it will be updated in OnRecalculateItems later if needed
 		$object->SetDBField('OrderId', $ord_id);
 
 		if ($exists) {
 			if ($qty > 0) {
 				$object->Update();
 			}
 			else {
 				$object->Delete();
 			}
 		}
 		else {
 			$object->Create();
 			if ($this->UseTempTables($event)) {
 				$object->setTempID();
 			}
 		}
 
 		$this->Application->HandleEvent($ord_event, 'ord:OnRecalculateItems');
 		/*if ($ord_event->getEventParam('RecalculateChangedCart') && !$this->Application->isAdmin) {
 			$event->SetRedirectParam('checkout_error', $ord_event->redirect_params['checkout_error']);
 		}*/
 	}
 
 	/**
 	 * Enter description here...
 	 *
 	 * @param kEvent $event
 	 */
 	function UpdateShippingTotal(&$event)
 	{
 		if ($this->Application->GetVar('ebay_notification') == 1) {
 			// TODO: get rid of this "if"
 			return ;
 		}
 		$object =& $event->getObject();
 		$ord_id = $object->GetId();
 
 		$shipping_option = $object->GetDBField('ShippingOption');
 		$backorder_select = $shipping_option == 0 ? '0 As BackOrderFlag' : 'BackOrderFlag';
 
 		$table_prefix = $this->TablePrefix($event);
 
 		$shipping_info = $object->GetDBField('ShippingInfo') ? unserialize( $object->GetDBField('ShippingInfo') ) : false;
 		$shipping_total = 0;
 		$insurance_fee = 0;
 		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 quantityes.
 	 * 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 != erSUCCESS)) {
 			// e.g. master order update failed, don't recalculate order products
 			return ;
 		}
 
 		if($checkout_error = $this->Application->GetVar('set_checkout_error'))
 		{
 			$event->SetRedirectParam('checkout_error', $checkout_error);
 		}
 
 		$order =& $event->getObject();
 		/* @var $order OrdersItem */
 
 		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;
 		}
 
 		$table_prefix = $this->TablePrefix($event);
 
 		// process only tangible products here
 		$poc_table = $this->Application->getUnitOption('poc', 'TableName');
 		$query = '	SELECT 	oi.ProductId, oi.OptionsSalt, oi.ItemData, SUM(oi.Quantity) AS Quantity,
 							IF(p.InventoryStatus = 2, poc.QtyInStock, p.QtyInStock) AS QtyInStock,
 							p.QtyInStockMin, p.BackOrder, p.InventoryStatus
 					FROM '.$table_prefix.'OrderItems AS oi
 					LEFT JOIN '.TABLE_PREFIX.'Products AS p ON oi.ProductId = p.ProductId
 					LEFT JOIN '.$poc_table.' poc ON (poc.CombinationCRC = oi.OptionsSalt) AND (oi.ProductId = poc.ProductId)
 							WHERE (oi.OrderId = '.$ord_id.') AND (p.Type = 1)
 							GROUP BY oi.ProductId, OptionsSalt';
 		$items = $this->Conn->Query($query);
 
 		$result = false;
 		$cost_total = 0;
 		$sub_total = 0;
 		$sub_total_flat = 0;
 		$coupon_discount = 0;
 		$pending_operations = Array();
 
 		$backordering = $this->Application->ConfigValue('Comm_Enable_Backordering');
 		$coupon_id = $order->GetDBField('CouponId');
 
 		foreach ($items as $row) {
 			$a_item_data = isset($row['ItemData']) ? unserialize($row['ItemData']) : Array();
 
 			$min_qty = $this->GetMinQty($row['ProductId']);
 			if ($row['Quantity'] > 0 && $row['Quantity'] < $min_qty) {
 				$row['Quantity'] = $min_qty;
 				$event->SetRedirectParam('checkout_error', 6);
 			}
 
 			$back_order = 0;
 			$to_order = 0;
 			if (!$row['InventoryStatus']) {
 				$available = $row['Quantity']*2; // always available;
 			}
 			else {
 				// if there are not enough qty AND backorder is auto or backorder is always
 				$available = $row['QtyInStock'] - $row['QtyInStockMin'];
 				$available = max(0, $available); // just in case
 			}
 			if (
 					$backordering && // backordering generally enabled
 					(
 						($row['Quantity'] > $available)
 						&&
 						($row['BackOrder'] == 2) //auto
 					)
 					||
 					$row['BackOrder'] == 1 // always
 				)
 			{ // split order into order & backorder
 				if ($row['BackOrder'] == 1) { //Always backorder
 					$available = 0;
 					$to_order = 0;
 					$back_order = $row['Quantity'];
 				}
 				else { //Auto
 					$to_order = $available;
 					$back_order = $row['Quantity'] - $available;
 				}
 
 				if (isset($a_item_data['ForcePrice'])) {
 					$price = $a_item_data['ForcePrice'];
 				}
 				else {
 					$price = $this->GetPlainProductPrice( $row['ProductId'], $to_order + $back_order, 1, $order, $row['OptionsSalt'], $row['ItemData'] );
 				}
 				$cost = $this->GetProductCost( $row['ProductId'], $to_order + $back_order, 1, $row['OptionsSalt'], $row['ItemData'] );
 				$discounted_price = $this->GetDiscountedProductPrice( $row['ProductId'], $price, $discount_id, $order );
 				$couponed_price = $this->GetCouponDiscountedPrice( $coupon_id, $row['ProductId'], $price );
 				if($couponed_price < $discounted_price)
 				{
 					$discounted_price =	$couponed_price;
 					$coupon_discount += ($price - $couponed_price) * ($to_order + $back_order);
 					$discount_type = 'coupon';
 					$discount_id = $coupon_id;
 				}
 				else
 				{
 					$discount_type = 'discount';
 				}
 				$pending_operations[] = Array( $row['ProductId'], 0, $to_order, $price, $discounted_price, $discount_type, $discount_id, 0, $row['OptionsSalt'], $row['ItemData'], $cost );
 				$pending_operations[] = Array( $row['ProductId'], 1, $back_order, $price, $discounted_price, $discount_type, $discount_id, 0, $row['OptionsSalt'], $row['ItemData'], $cost);
 			}
 			else { // store as normal order (and remove backorder)
 				// we could get here with backorder=never then we should order only what's available
 				$to_order = min($row['Quantity'], $available);
 				if (isset($a_item_data['ForcePrice'])) {
 					$price = $a_item_data['ForcePrice'];
 				}
 				else {
 					$price = $this->GetPlainProductPrice( $row['ProductId'], $to_order + $back_order, 1, $order, $row['OptionsSalt'], $row['ItemData'] );
 				}
 				$cost = $this->GetProductCost( $row['ProductId'], $to_order + $back_order, 1, $row['OptionsSalt'], $row['ItemData'] );
 				$discounted_price = $this->GetDiscountedProductPrice( $row['ProductId'], $price, $discount_id, $order );
 				$couponed_price = $this->GetCouponDiscountedPrice( $coupon_id, $row['ProductId'], $price );
 				if($couponed_price < $discounted_price)
 				{
 					$discounted_price =	$couponed_price;
 					$coupon_discount += ($price - $couponed_price) * ($to_order + $back_order);
 					$discount_type = 'coupon';
 					$discount_id = $coupon_id;
 				}
 				else
 				{
 					$discount_type = 'discount';
 				}
 				$pending_operations[] = Array( $row['ProductId'], 0, $to_order, $price, $discounted_price, $discount_type, $discount_id, 0, $row['OptionsSalt'], $row['ItemData'], $cost );
 				$pending_operations[] = Array( $row['ProductId'], 1, 0, $price, $discounted_price, $discount_type, $discount_id, 0, $row['OptionsSalt'], $row['ItemData'], $cost );	// this removes backorders
 				if ($to_order < $row['Quantity']) { // has changed
 					if ($to_order > 0) {
 						$event->SetRedirectParam('checkout_error', 2);
 					}
 					else {
 						$event->SetRedirectParam('checkout_error', 3);
 					}
 					$result = true;
 				}
 			}
 			$sub_total_flat += ($to_order + $back_order) * $price;
 			$sub_total += ($to_order + $back_order) * $discounted_price;
 			$cost_total += ($to_order + $back_order) * $cost;
 		}
 
 		// process subscriptions, services and downloadable: begin
 		$poc_table = $this->Application->getUnitOption('poc', 'TableName');
 		$query = '	SELECT oi.OrderItemId, oi.ProductId, oi.Quantity, oi.OptionsSalt, oi.ItemData,
 					IF(p.InventoryStatus = 2, poc.QtyInStock, p.QtyInStock) AS QtyInStock,
 					p.QtyInStockMin, p.BackOrder, p.InventoryStatus, p.Type
 					FROM '.$table_prefix.'OrderItems AS oi
 					LEFT JOIN '.TABLE_PREFIX.'Products AS p ON oi.ProductId = p.ProductId
 					LEFT JOIN '.$poc_table.' poc ON (poc.CombinationCRC = oi.OptionsSalt) AND (oi.ProductId = poc.ProductId)
 							WHERE (oi.OrderId = '.$ord_id.') AND (p.Type IN (2,3,4,5,6))';
 		$items = $this->Conn->Query($query);
 
 		foreach ($items as $row)
 		{
 			$a_item_data = isset($row['ItemData']) ? unserialize($row['ItemData']) : Array();
 			if (isset($a_item_data['ForcePrice'])) {
 				$price = $a_item_data['ForcePrice'];
 			}
 			else {
 				$price = $this->GetPlainProductPrice( $row['ProductId'], $row['Quantity'], $row['Type'], $order, $row['OptionsSalt'], $row['ItemData'] );
 			}
 			$cost = $this->GetProductCost( $row['ProductId'], $row['Quantity'], $row['Type'], $row['OptionsSalt'], $row['ItemData'] );
 			$discounted_price = $this->GetDiscountedProductPrice( $row['ProductId'], $price, $discount_id, $order );
 			$couponed_price = $this->GetCouponDiscountedPrice( $coupon_id, $row['ProductId'], $price );
 			if($couponed_price < $discounted_price)
 			{
 				$discounted_price =	$couponed_price;
 				$coupon_discount += ($price - $couponed_price);
 				$discount_type = 'coupon';
 				$discount_id = $coupon_id;
 			}
 			else
 			{
 				$discount_type = 'discount';
 			}
 			$pending_operations[] = Array( $row['ProductId'], 0, $row['Quantity'], $price, $discounted_price, $discount_type, $discount_id, $row['OrderItemId'], 0, $row['ItemData'], $cost );
 
 			$sub_total_flat += $price * $row['Quantity'];
 			$sub_total += $discounted_price * $row['Quantity'];
 			$cost_total += $cost * $row['Quantity'];
 		}
 		// process subscriptions, services and downloadable: end
 
 		$flat_discount = $this->GetWholeOrderPlainDiscount($global_discount_id, $order);
 		$flat_discount = ($flat_discount < $sub_total_flat) ? $flat_discount : $sub_total_flat;
 		$coupon_flat_discount = $this->GetWholeOrderCouponDiscount($coupon_id);
 		$coupon_flat_discount = ($coupon_flat_discount < $sub_total_flat) ? $coupon_flat_discount : $sub_total_flat;
 		if($coupon_flat_discount && $coupon_flat_discount > $flat_discount)
 		{
 			$flat_discount = $coupon_flat_discount;
 			$global_discount_type = 'coupon';
 			$global_discount_id = $coupon_id;
 		}
 		else
 		{
 			$global_discount_type = 'discount';
 		}
 		if($sub_total_flat - $sub_total < $flat_discount)
 		{
 			$coupon_discount = ($flat_discount == $coupon_flat_discount) ? $flat_discount : 0;
 			$sub_total = $sub_total_flat - $flat_discount;
 			foreach ($pending_operations as $operation_row)
 			{
 				list($product_id, $backorder, $qty, $price, $discounted_price, $dummy, $dummy, $order_item_id, $options_salt, $item_data, $cost) = $operation_row;
 				$new_price = ($price / $sub_total_flat) * $sub_total;
 				$result = $this->UpdateOrderItem($event, $product_id, $backorder, $qty, $price, $new_price, $global_discount_type, $global_discount_id, $order_item_id, $options_salt, $item_data, $cost) || $result;
 			}
 		}
 		else
 		{
 			foreach ($pending_operations as $operation_row)
 			{
 				list($product_id, $backorder, $qty, $price, $discounted_price, $discount_type, $discount_id, $order_item_id, $options_salt, $item_data, $cost) = $operation_row;
 				$result = $this->UpdateOrderItem($event, $product_id, $backorder, $qty, $price, $discounted_price, $discount_type, $discount_id, $order_item_id, $options_salt, $item_data, $cost) || $result;
 			}
 		}
 
 		$order->SetDBField('SubTotal', $sub_total);
 		$order->SetDBField('CostTotal', $cost_total);
 	//	$this->CalculateDiscount($event);
 		$order->SetDBField('DiscountTotal', $sub_total_flat - $sub_total);
 
 		if($coupon_id && $coupon_discount == 0)
 		{
 			$this->RemoveCoupon($order);
 			$event->SetRedirectParam('checkout_error', 8);
 		}
 		$order->SetDBField('CouponDiscount', $coupon_discount);
 
 		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', 1);
 		}
 
 		if ($result && is_object($event->MasterEvent) && $event->MasterEvent->Name == 'OnUserLogin')
 		{
 			if( ($shop_cart_template = $this->Application->GetVar('shop_cart_template'))
 				&& is_object($event->MasterEvent->MasterEvent) )
 			{
 				$event->MasterEvent->MasterEvent->SetRedirectParam('checkout_error', 9);
 				$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'];
 	}*/
 
 	function GetMinQty($p_id)
 	{
 		$query = 'SELECT
 							MIN(pp.MinQty)
 							FROM '.TABLE_PREFIX.'ProductsPricing AS pp
 							WHERE pp.ProductId = '.$p_id;
 		$min_qty = $this->Conn->GetOne($query);
 		if (!$min_qty) return 1;
 		return $min_qty;
 	}
 
 	/**
 	 * Return product cost for given qty, taking no discounts into account
 	 *
 	 * @param int $p_id ProductId
 	 * @param int $qty Quantity
 	 * @return float
 	 */
 	function GetProductCost($p_id, $qty, $product_type, $options_salt=null, $item_data=null)
 	{
 		$user_groups = $this->Application->RecallVar('UserGroups');
 		if($product_type == 1)
 		{
 			// $where_clause = 'pp.ProductId = '.$p_id.' AND pp.MinQty <= '.$qty;
 			// $orderby_clause = 'ORDER BY ('.$qty.' - pp.MinQty) ASC';
 			$where_clause = 'GroupId IN ('.$user_groups.') AND pp.ProductId = '.$p_id.' AND pp.MinQty <= '.$qty.' AND ('.$qty.' < pp.MaxQty OR pp.MaxQty=-1)';
 			$orderby_clause = 'ORDER BY pp.Price ASC';
 		}
 		else
 		{
 			$price_id = $this->GetPricingId($p_id, $item_data);
 			$where_clause = 'pp.ProductId = '.$p_id.' AND pp.PriceId = '.$price_id;
 			$orderby_clause = '';
 		}
 
 		$sql = 'SELECT Cost
 				FROM '.TABLE_PREFIX.'ProductsPricing AS pp
 				LEFT JOIN '.TABLE_PREFIX.'Products AS p
 				ON p.ProductId = pp.ProductId
 				WHERE '.$where_clause.'
 				'.$orderby_clause;
 
 		// GROUP BY pp.ProductId - removed, this it qty pricing is caclucated incorrectly !!!
 
 		$cost = $this->Conn->GetOne($sql);
 		if (!$cost) $price = 0;
 
 		return $cost;
 	}
 
 	/**
 	 * Return product price for given qty, taking no discounts into account
 	 *
 	 * @param int $p_id ProductId
 	 * @param int $qty Quantity
 	 * @return float
 	 */
 	function GetPlainProductPrice($p_id, $qty, $product_type, &$order_object, $options_salt=null, $item_data=null)
 	{
 		$user_id = $order_object->GetDBField('PortalUserId');
 		$user_groups = $this->Application->getUserGroups($user_id);
 
 		if($product_type == 1)
 		{
 			// $where_clause = 'pp.ProductId = '.$p_id.' AND pp.MinQty <= '.$qty;
 			// $orderby_clause = 'ORDER BY ('.$qty.' - pp.MinQty) ASC';
 			$where_clause = 'GroupId IN ('.$user_groups.') AND pp.ProductId = '.$p_id.' AND pp.MinQty <= '.$qty.' AND ('.$qty.' < pp.MaxQty OR pp.MaxQty=-1)';
 
 			// if we have to stick ti primary group this order by clause force its pricing to go first,
 			// but if there is no pricing for primary group it will take next optimal
 			if ($this->Application->ConfigValue('Comm_PriceBracketCalculation') == 1){
 				if ($user_id <= 0) {
 					$primary_group = $this->Application->ConfigValue('User_LoggedInGroup'); // actually this is Everyone
 				}
 				else {
 					$primary_group = $this->Conn->GetOne('SELECT GroupId FROM '.TABLE_PREFIX.'UserGroup WHERE PortalUserId='.$user_id.' AND PrimaryGroup=1');
 				}
 				$orderby_clause = 'ORDER BY (IF(GroupId='.$primary_group.',1,2)) ASC, pp.Price ASC';
 			}
 			else {
 				$orderby_clause = 'ORDER BY pp.Price ASC';
 			}
 
 		}
 		else
 		{
 			$price_id = $this->GetPricingId($p_id, $item_data);
 			$where_clause = 'pp.ProductId = '.$p_id.' AND pp.PriceId = '.$price_id;
 			$orderby_clause = '';
 		}
 
 		$sql = 'SELECT Price
 				FROM '.TABLE_PREFIX.'ProductsPricing AS pp
 				LEFT JOIN '.TABLE_PREFIX.'Products AS p
 				ON p.ProductId = pp.ProductId
 				WHERE '.$where_clause.'
 				'.$orderby_clause;
 
 		// GROUP BY pp.ProductId - removed, this it qty pricing is caclucated incorrectly !!!
 
 		$price = $this->Conn->GetOne($sql);
 		if (!$price) $price = 0;
 
 		if (isset($item_data) && !is_array($item_data)) {
 			$item_data = unserialize($item_data);
 		}
 
 		if (isset($item_data['Options'])) {
 			$addtion = 0;
 			$opt_helper =& $this->Application->recallObject('kProductOptionsHelper');
 			foreach ($item_data['Options'] as $opt => $val) {
 				$data = $this->Conn->GetRow('SELECT * FROM '.TABLE_PREFIX.'ProductOptions WHERE ProductOptionId = '.$opt);
 
 				$parsed = $opt_helper->ExplodeOptionValues($data);
 				if (!$parsed) continue;
 				$conv_prices = $parsed['Prices'];
 				$conv_price_types = $parsed['PriceTypes'];
 
 				if (is_array($val)) {
 					foreach ($val as $a_val) {
 						if (isset($conv_prices[unhtmlentities($a_val)]) && $conv_prices[unhtmlentities($a_val)]) {
 							if ($conv_price_types[unhtmlentities($a_val)] == '$') {
 								$addtion += $conv_prices[unhtmlentities($a_val)];
 							}
 							elseif ($conv_price_types[unhtmlentities($a_val)] == '%') {
 								$addtion += $price * $conv_prices[unhtmlentities($a_val)] / 100;
 							}
 						}
 					}
 				}
 				else {
 					if (isset($conv_prices[unhtmlentities($val)]) && $conv_prices[unhtmlentities($val)]) {
 						if ($conv_price_types[unhtmlentities($val)] == '$') {
 							$addtion += $conv_prices[unhtmlentities($val)];
 						}
 						elseif ($conv_price_types[unhtmlentities($val)] == '%') {
 							$addtion += $price * $conv_prices[unhtmlentities($val)] / 100;
 						}
 					}
 				}
 			}
 			$price += $addtion;
 		}
 
 		$comb_salt = $this->OptionsSalt( getArrayValue($item_data, 'Options'), 1);
 		if ($comb_salt) {
 			$query = 'SELECT * FROM '.TABLE_PREFIX.'ProductOptionCombinations WHERE CombinationCRC = '.$comb_salt;
 			$comb = $this->Conn->GetRow($query);
 			if ($comb) {
 				switch ($comb['PriceType']) {
 					case 1: // = override
 						$price = $comb['Price'];
 						break;
 					case 2: // flat
 						$price = $price + $comb['Price'];
 						break;
 					case 3: // percent
 						$price = $price * (1 + $comb['Price'] / 100);
 						break;
 				}
 			}
 		}
 
 		return max($price, 0);
 	}
 
 	/**
 	 * Return product price for given qty, taking possible discounts into account
 	 *
 	 * @param int $p_id ProductId
 	 * @param int $qty Quantity
 	 * @return float
 	 */
 	function GetDiscountedProductPrice($p_id, $price, &$discount_id, &$order_object)
 	{
 		$discount_id = 0;
 		$user_id = $order_object->GetDBField('PortalUserId');
 		$user_groups = $this->Application->getUserGroups($user_id);
 		$sql = '
 				SELECT
 					IF(pd.Type = 1,
 				 		'.$price.' - pd.Amount,
 				 		IF(pd.Type = 2,
 				 			('.$price.' * (1-pd.Amount/100)),
 				 			'.$price.'
 						)
 					) AS DiscountedPrice,
 					pd.DiscountId
 				FROM '.TABLE_PREFIX.'Products AS p
 				LEFT JOIN '.TABLE_PREFIX.'ProductsDiscountItems AS pdi ON
 					pdi.ItemResourceId = p.ResourceId OR pdi.ItemType = 0
 				LEFT JOIN '.TABLE_PREFIX.'ProductsDiscounts AS pd ON
 					pd.DiscountId = pdi.DiscountId
 					AND
 					(pdi.ItemType = 1 OR (pdi.ItemType = 0 AND pd.Type = 2))
 					AND
 					pd.Status = 1
 					AND
 					(	pd.GroupId IN ('.$user_groups.') AND
 						( (pd.Start IS NULL OR pd.Start < UNIX_TIMESTAMP())
 								AND
 							(pd.End IS NULL OR pd.End > UNIX_TIMESTAMP())
 						)
 					)
 				WHERE p.ProductId = '.$p_id.' AND pd.DiscountId IS NOT NULL
 			';
 
 		$pricing = $this->Conn->GetCol($sql, 'DiscountId');
 		if (!$pricing) return $price;
 
 		$discounted_price = min($pricing);
 		$pricing = array_flip($pricing);
 		$discount_id = $pricing[$discounted_price];
 
 		$discounted_price = min($discounted_price, $price);
 		return max($discounted_price, 0);
 	}
 
 	function GetCouponDiscountedPrice($coupon_id, $p_id, $price)
 	{
 		if(!$coupon_id) return $price;
 
 		$sql = '
 				SELECT
 					'.$price.' AS Price,
 					MIN(IF(pc.Type = 1,
 				 		'.$price.' - pc.Amount,
 				 		IF(pc.Type = 2,
 				 			('.$price.' * (1-pc.Amount/100)),
 				 			'.$price.'
 						)
 					)) AS DiscountedPrice
 				FROM '.TABLE_PREFIX.'Products AS p
 				LEFT JOIN '.TABLE_PREFIX.'ProductsCouponItems AS pci ON
 					pci.ItemResourceId = p.ResourceId OR pci.ItemType = 0
 				LEFT JOIN '.TABLE_PREFIX.'ProductsCoupons AS pc ON
 					pc.CouponId = pci.CouponId
 					AND
 					(pci.ItemType = 1 OR (pci.ItemType = 0 AND pc.Type = 2))
 				WHERE p.ProductId = '.$p_id.' AND pci.CouponId = '.$coupon_id.'
 				GROUP BY p.ProductId
 			';
 
 		$pricing = $this->Conn->GetRow($sql);
 		if ($pricing === false) return $price;
 		$price = min($pricing['Price'], $pricing['DiscountedPrice']);
 		return max($price, 0);
 	}
 
 	function GetWholeOrderPlainDiscount(&$discount_id, &$order_object)
 	{
 		$user_id = $order_object->GetDBField('PortalUserId');
 		$user_groups = $this->Application->getUserGroups($user_id);
 		$sql = '
 				SELECT pd.Amount AS Discount, pd.DiscountId
 				FROM '.TABLE_PREFIX.'ProductsDiscountItems AS pdi
 				LEFT JOIN '.TABLE_PREFIX.'ProductsDiscounts AS pd
 				ON
 					pd.DiscountId = pdi.DiscountId
 					AND
 					pdi.ItemType = 0 AND pd.Type = 1
 					AND
 					pd.Status = 1
 					AND
 					(	pd.GroupId IN ('.$user_groups.') AND
 						( (pd.Start IS NULL OR pd.Start < '.$order_object->GetDBField('OrderDate').')
 								AND
 							(pd.End IS NULL OR pd.End > '.$order_object->GetDBField('OrderDate').')
 						)
 					)
 				WHERE pd.DiscountId IS NOT NULL
 			';
 		$pricing = $this->Conn->GetCol($sql, 'DiscountId');
 		if (!$pricing) return 0;
 
 		$discounted_price = max($pricing);
 		$pricing = array_flip($pricing);
 		$discount_id = $pricing[$discounted_price];
 
 		return max($discounted_price, 0);
 	}
 
 	function GetWholeOrderCouponDiscount($coupon_id)
 	{
 		if (!$coupon_id) return 0;
 
 		$sql = 'SELECT Amount
 				FROM '.TABLE_PREFIX.'ProductsCouponItems AS pci
 				LEFT JOIN '.TABLE_PREFIX.'ProductsCoupons AS pc
 				ON pc.CouponId = pci.CouponId
 				WHERE pci.CouponId = '.$coupon_id.' AND pci.ItemType = 0 AND pc.Type = 1';
 		return $this->Conn->GetOne($sql);
 	}
 
 	/**
 	 * 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++;
 			}
 		}
 	}
 
 	function UpdateShippingTypes(&$event)
 	{
 		$object =& $this->Application->recallObject($event->getPrefixSpecial());
 		$ord_id = $object->GetId();
 
 		$order_info = $this->Application->GetVar('ord');
 		$shipping_ids = getArrayValue($order_info, $ord_id, 'ShippingTypeId');
 
 		if (!$shipping_ids)
 		{
 			return;
 		}
 
 		$last_shippings = unserialize($this->Application->RecallVar('LastShippings'));
 		$shipping_types = Array();
 
 		$ret = true;
 		foreach($shipping_ids as $package => $id)
 		{
 			// try to validate
 			if ( $object->GetDBField('ShippingType') == 0 && (strpos($id, 'USPS') !== false) && in_array($this->Application->GetVar('t'), Array('in-commerce/checkout/shipping','in-commerce/orders/orders_edit_shipping'))) {
 				$current_usps_shipping_types = unserialize($this->Application->RecallVar('current_usps_shipping_types'));
 				$object->SetDBField('ShippingInfo', serialize(Array($package => $current_usps_shipping_types[$id])));
 				$usps_data = $this->MakeUSPSOrder($object, true);
 				if ( !isset($usps_data['error_number']) ) {
 					// update only international shipping
 					if ( $object->GetDBField('ShippingCountry') != 'USA') {
 						$last_shippings[$package][$id]['TotalCost'] = $usps_data['Postage'];
 					}
 				}
 				else {
 					$this->Application->SetVar('usps_errors', $usps_data['error_description']);
 					$ret = false;
 				}
 				$object->SetDBField('ShippingInfo', '');
 			}
 			$shipping_types[$package] = $last_shippings[$package][$id];
 		}
 		$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)
 	{
 		$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)
 	{
 		$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 = array_shift( $this->StoreSelectedIDs($event) );
 
 		$idfield = $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, $idfield, $id) );
 
 		$prefix_special = $event->Prefix.'.'.$this->getSpecialByType($order_status);
 
 		$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[$idfield] == $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 unknown_type $event
 	 */
 	function OnResetToPending(&$event)
 	{
 		$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=erSUCCESS;
 				}
 				else
 				{
 					$event->status=erFAIL;
 					$event->redirect=false;
 					break;
 				}
 			}
 		}
 	}
 
 	/**
 	 * Creates list from items selected in grid
 	 *
 	 * @param kEvent $event
 	 */
 	function OnLoadSelected(&$event)
 	{
 		$event->setPseudoClass('_List');
 		$object =& $event->getObject( Array('selected_only' => true) );
 		$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);
 	}
 
 	/**
 	 * Regular event: 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 == erSUCCESS) {
 					//send recurring ok email
 					$email_event_user 	=& $this->Application->EmailEventUser('ORDER.RECURRING.PROCESSED', $order->GetDBField('PortalUserId'), $this->OrderEmailParams($order));
 					$email_event_admin 	=& $this->Application->EmailEventAdmin('ORDER.RECURRING.PROCESSED');
 				}
 				else {
 					//send Recurring failed event
 					$order->SetDBField('Status', ORDER_STATUS_DENIED);
 					$order->Update();
 					$email_event_user 	=& $this->Application->EmailEventUser('ORDER.RECURRING.DENIED', $order->GetDBField('PortalUserId'), $this->OrderEmailParams($order));
 					$email_event_admin 	=& $this->Application->EmailEventAdmin('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);
 		}
 
 		$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'));
 		$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.';
 		}
 	}
 
 	function OnAfterConfigRead(&$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']);
 
 		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;
+				$user_groups[] = $default_group;
 			}
-			$fields['PaymentType']['options_sql'] .= ' AND ';
-			$fields['PaymentType']['options_sql'] .= ' (PortalGroups LIKE "%%,'.implode(',%%" OR PortalGroups LIKE "%%,', $user_groups).',%%")';
-
+			$fields['PaymentType']['options_sql'] = str_replace(
+				'ORDER BY ',
+				'AND (PortalGroups LIKE "%%,'.implode(',%%" OR PortalGroups LIKE "%%,', $user_groups).',%%") ORDER BY ',
+				$fields['PaymentType']['options_sql']
+			);
 		}
 
 		$this->Application->setUnitOption($event->Prefix, 'Fields', $fields);
 
 
 	}
 
 	/**
 	 * Allows configuring export options
 	 *
 	 * @param kEvent $event
 	 */
 	function OnBeforeExportBegin(&$event)
 	{
 		$options = $event->getEventParam('options') ;
 
 		$items_list =& $this->Application->recallObject($event->Prefix.'.'.$this->Application->RecallVar('export_oroginal_special'), $event->Prefix.'_List');
 		$items_list->SetPerPage(-1);
 		if ($options['export_ids'] != '') {
 			$items_list->AddFilter('export_ids', $items_list->TableName.'.'.$items_list->IDField.' IN ('.implode(',',$options['export_ids']).')');
 		}
 
 		$options['ForceCountSQL'] = $items_list->getCountSQL( $items_list->GetSelectSQL(true,false) );
 		$options['ForceSelectSQL'] = $items_list->GetSelectSQL();
 
 		$event->setEventParam('options',$options);
 
 		$object =& $this->Application->recallObject($event->Prefix.'.export');
 		/* @var $object kDBItem */
 
 		$object->SetField('Number', 999999);
 		$object->SetField('SubNumber', 999);
 	}
 
 	/**
 	 * Returns specific to each item type columns only
 	 *
 	 * @param kEvent $event
 	 * @return Array
 	 */
 	function getCustomExportColumns(&$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_recursive2($columns, $new_columns);
 	}
 
 	function OnSave(&$event)
 	{
 		$res = parent::OnSave($event);
 		if ($event->status == erSUCCESS) {
 			$copied_ids = unserialize($this->Application->RecallVar($event->Prefix.'_copied_ids'.$this->Application->GetVar('wid'), serialize(array())));
 			foreach ($copied_ids as $id) {
 				$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);
 			}
 		}
 		return $res;
 	}
 
 	/**
 	 * Occures before an item is copied to live table (after all foreign keys have been updated)
 	 * Id of item being copied is passed as event' 'id' param
 	 *
 	 * @param kEvent $event
 	 */
 	function OnBeforeCopyToLive(&$event)
 	{
 		$id = $event->getEventParam('id');
 		$copied_ids = unserialize($this->Application->RecallVar($event->Prefix.'_copied_ids'.$this->Application->GetVar('wid'), serialize(array())));
 		array_push($copied_ids, $id);
 
 		$this->Application->StoreVar($event->Prefix.'_copied_ids'.$this->Application->GetVar('wid'), serialize($copied_ids)	);
 	}
 
 	/**
 	 * Checks, that currently loaded item is allowed for viewing (non permission-based)
 	 *
 	 * @param kEvent $event
 	 * @return bool
 	 */
 	function checkItemStatus(&$event)
 	{
 		if ($this->Application->isAdminUser) {
 			return true;
 		}
 
 		$object =& $event->getObject();
 		if (!$object->isLoaded()) {
 			return true;
 		}
 
 		return $object->GetDBField('PortalUserId') == $this->Application->RecallVar('user_id');
 	}
 
 	// ===== Gift Certificates Related =====
 	function OnRemoveGiftCertificate(&$event)
 	{
 		$object =& $event->getObject();
 		$this->RemoveGiftCertificate($object);
 		$event->CallSubEvent('OnRecalculateItems');
 		$event->SetRedirectParam('checkout_error', 107);
 	}
 
 	function RemoveGiftCertificate(&$object)
 	{
 		$object->RemoveGiftCertificate();
 	}
 
 	function RecalculateGift(&$event)
 	{
 		$object =& $event->getObject();
 		/* @var $object OrdersItem */
 
 		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);
 	}
 
 	/**
 	 * Creates new USPS order
 	 *
 	 * @param OrdersItem $object
 	 * @param bool $fake_mode
 	 * @return Array
 	 */
 	function MakeUSPSOrder(&$object, $fake_mode = false)
 	{
 		$this->Application->recallObject('ShippingQuoteEngine');
 
 		$aUSPS = $this->Application->recallObject('USPS', 'USPS');
 		/* @var $aUSPS USPS */
 
 		$ShippingInfo = unserialize($object->GetDBField('ShippingInfo'));
 		$ShippingCode = $USPSMethod = '';
 		$ShippingCountry = $aUSPS->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' => $aUSPS->PhoneClean($object->GetDBField('ShippingPhone')),
 			'ShippingFax' =>  $aUSPS->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 = 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, $aUSPS->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 = 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
 		$aUSPS->order = $sOrder;
 		$usps_data = $aUSPS->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 ($fake_mode == 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;
 	}
 
 	/**
 	 * Downloads shipping tracking bar code, that was already generated by USPS service
 	 *
 	 * @param kEvent $event
 	 */
 	function OnDownloadLabel(&$event)
 	{
 		$event->status = erSTOP;
 		ini_set('memory_limit', '300M');
 		ini_set('max_execution_time', '0');
 
 		$object =& $event->getObject();
 		$file = $object->GetDBField('ShippingTracking').'.pdf';
 		$full_path = USPS_LABEL_FOLDER.$file;
 
 		if (!file_exists($full_path) || !is_file($full_path)) {
 			return ;
 		}
 
 		$mime = function_exists('mime_content_type') ? mime_content_type($full_path) : 'application/download';
 		header('Content-type: '.$mime);
 		header('Content-Disposition: attachment; filename="'.$file.'"');
 		readfile($full_path);
 	}
 }
\ No newline at end of file
Index: branches/5.0.x/units/orders/orders_config.php
===================================================================
--- branches/5.0.x/units/orders/orders_config.php	(revision 13268)
+++ branches/5.0.x/units/orders/orders_config.php	(revision 13269)
@@ -1,550 +1,561 @@
 <?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!');
 
 	$config =	Array (
 					'Prefix'			=>	'ord',
 					'ItemClass'			=>	Array ('class' => 'OrdersItem', 'file' => 'orders_item.php', 'build_event' => 'OnItemBuild'),
 					'ListClass'			=>	Array ('class' => 'kDBList', 'file' => '', 'build_event' => 'OnListBuild'),
 					'EventHandlerClass'	=>	Array ('class' => 'OrdersEventHandler', 'file' => 'orders_event_handler.php', 'build_event' => 'OnBuild'),
 					'TagProcessorClass' =>	Array ('class' => 'OrdersTagProcessor', 'file' => 'orders_tag_processor.php', 'build_event' => 'OnBuild'),
 					'AutoLoad'			=>	true,
 
 					'Hooks'				=>	Array (
-												Array (
-													'Mode' => hAFTER,
-													'Conditional' => false,
-													'HookToPrefix' => 'ord',
-													'HookToSpecial' => '',
-													'HookToEvent' => Array ( 'OnPreSave' ),
-													'DoPrefix' => '',
-													'DoSpecial' => '',
-													'DoEvent' => 'OnRecalculateItems',
-												),
-
-												/* OnApplyCoupon is called as hook for OnUpdateCart/OnCheckout, which calls OnRecalcualate themself
-												Array (
-													'Mode' => hAFTER,
-													'Conditional' => false,
-													'HookToPrefix' => 'coup',
-													'HookToSpecial' => '',
-													'HookToEvent' => Array ( 'OnApplyCoupon' ),
-													'DoPrefix' => '',
-													'DoSpecial' => '',
-													'DoEvent' => 'OnRecalculateItems',
-												),*/
-
-												Array (
-													'Mode' => hAFTER,
-													'Conditional' => false,
-													'HookToPrefix' => 'u',
-													'HookToSpecial' => '',
-													'HookToEvent' => Array ( 'OnCreate' ),
-													'DoPrefix' => '',
-													'DoSpecial' => '',
-													'DoEvent' => 'OnUserCreate',
-												),
-
-												Array (
-													'Mode' => hBEFORE,
-													'Conditional' => false,
-													'HookToPrefix' => 'u',
-													'HookToSpecial' => '',
-													'HookToEvent' => Array ('OnCheckExpiredMembership'),
-													'DoPrefix' => '',
-													'DoSpecial' => '',
-													'DoEvent' => 'OnCheckRecurringOrders',
-												),
-
-												Array (
-													'Mode' => hAFTER,
-													'Conditional' => false,
-													'HookToPrefix' => 'u',
-													'HookToSpecial' => '',
-													'HookToEvent' => Array ( 'OnLogin' ),
-													'DoPrefix' => '',
-													'DoSpecial' => '',
-													'DoEvent' => 'OnUserLogin',
-												),
-
-												Array (
-													'Mode' => hBEFORE, // before because OnInpLogin is called after real in-portal login and uses data from hooks
-													'Conditional' => false,
-													'HookToPrefix' => 'u',
-													'HookToSpecial' => '',
-													'HookToEvent' => Array ( 'OnInpLogin' ),
-													'DoPrefix' => '',
-													'DoSpecial' => '',
-													'DoEvent' => 'OnUserLogin',
-												),
-											),
+						Array (
+							'Mode' => hAFTER,
+							'Conditional' => false,
+							'HookToPrefix' => 'ord',
+							'HookToSpecial' => '',
+							'HookToEvent' => Array ( 'OnPreSave' ),
+							'DoPrefix' => '',
+							'DoSpecial' => '',
+							'DoEvent' => 'OnRecalculateItems',
+						),
+
+						/* OnApplyCoupon is called as hook for OnUpdateCart/OnCheckout, which calls OnRecalcualate themself
+						Array (
+							'Mode' => hAFTER,
+							'Conditional' => false,
+							'HookToPrefix' => 'coup',
+							'HookToSpecial' => '',
+							'HookToEvent' => Array ( 'OnApplyCoupon' ),
+							'DoPrefix' => '',
+							'DoSpecial' => '',
+							'DoEvent' => 'OnRecalculateItems',
+						),*/
+
+						Array (
+							'Mode' => hAFTER,
+							'Conditional' => false,
+							'HookToPrefix' => 'u',
+							'HookToSpecial' => '',
+							'HookToEvent' => Array ( 'OnCreate' ),
+							'DoPrefix' => '',
+							'DoSpecial' => '',
+							'DoEvent' => 'OnUserCreate',
+						),
+
+						Array (
+							'Mode' => hBEFORE,
+							'Conditional' => false,
+							'HookToPrefix' => 'u',
+							'HookToSpecial' => '',
+							'HookToEvent' => Array ('OnCheckExpiredMembership'),
+							'DoPrefix' => '',
+							'DoSpecial' => '',
+							'DoEvent' => 'OnCheckRecurringOrders',
+						),
+
+						Array (
+							'Mode' => hAFTER,
+							'Conditional' => false,
+							'HookToPrefix' => 'u',
+							'HookToSpecial' => '',
+							'HookToEvent' => Array ( 'OnLogin' ),
+							'DoPrefix' => '',
+							'DoSpecial' => '',
+							'DoEvent' => 'OnUserLogin',
+						),
+
+						Array (
+							'Mode' => hBEFORE, // before because OnInpLogin is called after real in-portal login and uses data from hooks
+							'Conditional' => false,
+							'HookToPrefix' => 'u',
+							'HookToSpecial' => '',
+							'HookToEvent' => Array ( 'OnInpLogin' ),
+							'DoPrefix' => '',
+							'DoSpecial' => '',
+							'DoEvent' => 'OnUserLogin',
+						),
+					),
 					'AggregateTags' => Array (
-															Array (
-																'AggregateTo' => 'orditems',
-																'AggregatedTagName' => 'LinkRemoveFromCart',
-																'LocalTagName' => 'Orditems_LinkRemoveFromCart',
-															),
-															Array (
-																'AggregateTo' => 'orditems',
-																'AggregatedTagName' => 'ProductLink',
-																'LocalTagName' => 'Orderitems_ProductLink',
-															),
-															Array (
-																'AggregateTo' => 'orditems',
-																'AggregatedTagName' => 'ProductExists',
-																'LocalTagName' => 'Orderitems_ProductExists',
-															),
-													),
+							Array (
+								'AggregateTo' => 'orditems',
+								'AggregatedTagName' => 'LinkRemoveFromCart',
+								'LocalTagName' => 'Orditems_LinkRemoveFromCart',
+							),
+							Array (
+								'AggregateTo' => 'orditems',
+								'AggregatedTagName' => 'ProductLink',
+								'LocalTagName' => 'Orderitems_ProductLink',
+							),
+							Array (
+								'AggregateTo' => 'orditems',
+								'AggregatedTagName' => 'ProductExists',
+								'LocalTagName' => 'Orderitems_ProductExists',
+							),
+					),
 
 					'QueryString'		=>	Array (
-												1	=>	'id',
-												2	=>	'Page',
-												3	=>	'event',
-												4 	=>	'mode',
-											),
+						1	=>	'id',
+						2	=>	'Page',
+						3	=>	'event',
+						4 	=>	'mode',
+					),
 					'IDField'			=>	'OrderId',
 					'StatusField'		=>	Array ('Status'),	// field, that is affected by Approve/Decline events
 
 					'ViewMenuPhrase'	=>	'la_title_Orders',
 					'CatalogTabIcon' => 'icon16_item.png',
 
 					'TitleField' => 'OrderNumber',
 					'TitlePresets'		=>	Array (
-												'default'	=>	Array (	'new_status_labels'		=> Array ('ord' => '!la_title_Adding_Order!'),
-																		'edit_status_labels'	=> Array ('ord' => '!la_title_Editing_Order!'),
-																		'new_titlefield'		=> Array ('ord' => '!la_title_New_Order!'),
-																),
-
-												'orders_incomplete'	=>	Array (	'prefixes'				=>	Array ('ord.incomplete_List'),
-																				'format'				=>	"!la_title_IncompleteOrders!",
-																),
-
-												'orders_pending'	=>	Array (	'prefixes'				=>	Array ('ord.pending_List'),
-																				'format'				=>	"!la_title_PendingOrders!",
-																),
-
-												'orders_backorders'	=>	Array (	'prefixes'				=>	Array ('ord.backorders_List'),
-																				'format'				=>	"!la_title_BackOrders!",
-																),
-
-												'orders_toship'		=>	Array (	'prefixes'				=>	Array ('ord.toship_List'),
-																				'format'				=>	"!la_title_OrdersToShip!",
-																),
-
-												'orders_processed'	=>	Array (	'prefixes'				=>	Array ('ord.processed_List'),
-																				'format'				=>	"!la_title_OrdersProcessed!",
-																),
-
-												'orders_returns'	=>	Array (	'prefixes'				=>	Array ('ord.returns_List'),
-																				'format'				=>	"!la_title_OrdersReturns!",
-																),
-
-												'orders_denied'		=>	Array (	'prefixes'				=>	Array ('ord.denied_List'),
-																				'format'				=>	"!la_title_OrdersDenied!",
-																),
-												'orders_archived'	=>	Array (	'prefixes'				=>	Array ('ord.archived_List'),
-																				'format'				=>	"!la_title_OrdersArchived!",
-																),
-
-												'orders_search'		=>	Array (	'prefixes'				=>	Array ('ord.search_List'),
-																				'format'				=>	"!la_title_OrdersSearch!",
-																),
-
-												'orders_edit_general'	=>	Array ('prefixes' => Array ('ord'), 'format' => "#ord_status# '#ord_titlefield#' - !la_title_General!"),
-												'orders_edit_billing'	=>	Array ('prefixes' => Array ('ord'), 'format' => "#ord_status# '#ord_titlefield#' - !la_title_OrderBilling!"),
-												'orders_edit_shipping'	=>	Array ('prefixes' => Array ('ord'), 'format' => "#ord_status# '#ord_titlefield#' - !la_title_OrderShipping!"),
-												'orders_edit_items'		=>	Array ('prefixes' => Array ('ord', 'orditems_List'), 'format' => "#ord_status# '#ord_titlefield#' - !la_title_OrderItems!"),
-												'orders_edit_preview'	=>	Array ('prefixes' => Array ('ord'), 'format' => "#ord_status# '#ord_titlefield#' - !la_title_OrderPreview!"),
-
-												'orders_gw_result' => Array ('prefixes' => Array ('ord'), 'format' => "!la_title_OrderGWResult!"),
-
-												'order_items_edit'	=>	Array (	'prefixes'				=>	Array ('ord', 'orditems'),
-																				'new_status_labels'		=>	Array ('orditems' => '!la_title_Adding_Order_Item!'),
-																				'edit_status_labels'	=>	Array ('orditems' => '!la_title_Editing_Order_Item!'),
-																				'new_titlefield'		=>	Array ('orditems' => '!la_title_New_Order_Item!'),
-																				'format'				=>	"#ord_status# '#ord_titlefield#' - #orditems_status# '#orditems_titlefield#'",
-																		),
+						'default'	=>	Array (
+							'new_status_labels'		=> Array ('ord' => '!la_title_Adding_Order!'),
+							'edit_status_labels'	=> Array ('ord' => '!la_title_Editing_Order!'),
+							'new_titlefield'		=> Array ('ord' => '!la_title_New_Order!'),
+						),
+
+						'orders_incomplete'	=>	Array (	'prefixes'				=>	Array ('ord.incomplete_List'),
+														'format'				=>	"!la_title_IncompleteOrders!",
+										),
+
+						'orders_pending'	=>	Array (	'prefixes'				=>	Array ('ord.pending_List'),
+														'format'				=>	"!la_title_PendingOrders!",
+										),
+
+						'orders_backorders'	=>	Array (	'prefixes'				=>	Array ('ord.backorders_List'),
+														'format'				=>	"!la_title_BackOrders!",
+										),
+
+						'orders_toship'		=>	Array (	'prefixes'				=>	Array ('ord.toship_List'),
+														'format'				=>	"!la_title_OrdersToShip!",
+										),
+
+						'orders_processed'	=>	Array (	'prefixes'				=>	Array ('ord.processed_List'),
+														'format'				=>	"!la_title_OrdersProcessed!",
+										),
+
+						'orders_returns'	=>	Array (	'prefixes'				=>	Array ('ord.returns_List'),
+														'format'				=>	"!la_title_OrdersReturns!",
+										),
+
+						'orders_denied'		=>	Array (	'prefixes'				=>	Array ('ord.denied_List'),
+														'format'				=>	"!la_title_OrdersDenied!",
+										),
+						'orders_archived'	=>	Array (	'prefixes'				=>	Array ('ord.archived_List'),
+														'format'				=>	"!la_title_OrdersArchived!",
+										),
+
+						'orders_search'		=>	Array (	'prefixes'				=>	Array ('ord.search_List'),
+														'format'				=>	"!la_title_OrdersSearch!",
+										),
+
+						'orders_edit_general'	=>	Array ('prefixes' => Array ('ord'), 'format' => "#ord_status# '#ord_titlefield#' - !la_title_General!"),
+						'orders_edit_billing'	=>	Array ('prefixes' => Array ('ord'), 'format' => "#ord_status# '#ord_titlefield#' - !la_title_OrderBilling!"),
+						'orders_edit_shipping'	=>	Array ('prefixes' => Array ('ord'), 'format' => "#ord_status# '#ord_titlefield#' - !la_title_OrderShipping!"),
+						'orders_edit_items'		=>	Array ('prefixes' => Array ('ord', 'orditems_List'), 'format' => "#ord_status# '#ord_titlefield#' - !la_title_OrderItems!"),
+						'orders_edit_preview'	=>	Array ('prefixes' => Array ('ord'), 'format' => "#ord_status# '#ord_titlefield#' - !la_title_OrderPreview!"),
+
+						'orders_gw_result' => Array ('prefixes' => Array ('ord'), 'format' => "!la_title_OrderGWResult!"),
+
+						'order_items_edit'	=>	Array (	'prefixes'				=>	Array ('ord', 'orditems'),
+														'new_status_labels'		=>	Array ('orditems' => '!la_title_Adding_Order_Item!'),
+														'edit_status_labels'	=>	Array ('orditems' => '!la_title_Editing_Order_Item!'),
+														'new_titlefield'		=>	Array ('orditems' => '!la_title_New_Order_Item!'),
+														'format'				=>	"#ord_status# '#ord_titlefield#' - #orditems_status# '#orditems_titlefield#'",
+												),
 
-												'orders_export' =>  Array ('format' => '!la_title_OrdersExport!'),
+						'orders_export' =>  Array ('format' => '!la_title_OrdersExport!'),
 
-												'orders_product_edit'	=>	Array ('format' => '!la_title_Editing_Order_Item!'),
+						'orders_product_edit'	=>	Array ('format' => '!la_title_Editing_Order_Item!'),
 
-											),
+					),
 
 					'EditTabPresets' => Array (
 						'Default' => Array (
 							'general' => Array ('title' => 'la_tab_General', 't' => 'in-commerce/orders/orders_edit', 'priority' => 1),
 							'items' => Array ('title' => 'la_tab_Items', 't' => 'in-commerce/orders/orders_edit_items', 'priority' => 2),
 							'shipping' => Array ('title' => 'la_tab_Shipping', 't' => 'in-commerce/orders/orders_edit_shipping', 'priority' => 3),
 							'billing' => Array ('title' => 'la_tab_Billing', 't' => 'in-commerce/orders/orders_edit_billing', 'priority' => 4),
 							'preview' => Array ('title' => 'la_tab_Preview', 't' => 'in-commerce/orders/orders_edit_preview', 'priority' => 5),
 						),
 					),
 
 					'PermSection'		=>	Array ('main' => 'in-commerce:orders'),
 
 
 					'Sections'			=>	Array (
 						'in-commerce:orders'	=>	Array (
 								'parent'		=>	'in-commerce',
 								'icon'			=>	'in-commerce:orders',
 								'label'			=>	'la_tab_Orders',
 								'url'			=>	Array ('t' => 'in-commerce/orders/orders_pending_list', 'pass' => 'm'),
 								'permissions'	=>	Array ('view', 'add', 'edit', 'delete', 'advanced:approve', 'advanced:deny', 'advanced:archive', 'advanced:place', 'advanced:process', 'advanced:ship', 'advanced:reset_to_pending'),
 								'priority'		=>	1,
 								'type'			=>	stTREE,
 						),
 					),
 
 					'SectionAdjustments' => Array (
 						'in-portal:visits' => Array (
 							'url' => Array ('t' => 'in-commerce/visits/visits_list_incommerce', 'pass' => 'm'),
 						),
 					),
 
 					'StatisticsInfo'	=>	Array (
 													'pending'	=>	Array (
 																			'icon'		=>	'core:icon16_item.png',
 																			'label'		=>	'la_title_Orders',
 																			'js_url' 	=>	"#url#",
 																			'url'		=>	Array ('t' => 'in-commerce/orders/orders_pending_list', 'pass' => 'm'),
 																			'status'	=>	ORDER_STATUS_PENDING,
 
 																	),
 											),
 
 					'TableName'			=>	TABLE_PREFIX.'Orders',
 
 					'FilterMenu'		=>	Array (
-												'Groups' => Array (
-													Array ('mode' => 'AND', 'filters' => Array (0,1,2,3,4,5,6), 'type' => WHERE_FILTER),
-												),
+						'Groups' => Array (
+							Array ('mode' => 'AND', 'filters' => Array (0,1,2,3,4,5,6), 'type' => WHERE_FILTER),
+						),
 
-												'Filters' => Array (
-													0	=>	Array ('label' => 'la_Incomplete', 'on_sql' => '', 'off_sql' => '%1$s.Status != 0' ),
-													1	=>	Array ('label' => 'la_Pending', 'on_sql' => '', 'off_sql' => '%1$s.Status != 1'  ),
-													2	=>	Array ('label' => 'la_BackOrders', 'on_sql' => '', 'off_sql' => '%1$s.Status != 2'  ),
-													3	=>	Array ('label' => 'la_ToShip', 'on_sql' => '', 'off_sql' => '%1$s.Status != 3' ),
-													4	=>	Array ('label' => 'la_Processed', 'on_sql' => '', 'off_sql' => '%1$s.Status != 4'  ),
-													5	=>	Array ('label' => 'la_Denied', 'on_sql' => '', 'off_sql' => '%1$s.Status != 5'  ),
-													6	=>	Array ('label' => 'la_Archived', 'on_sql' => '', 'off_sql' => '%1$s.Status != 6'  ),
-												)
-											),
+						'Filters' => Array (
+							0	=>	Array ('label' => 'la_Incomplete', 'on_sql' => '', 'off_sql' => '%1$s.Status != 0' ),
+							1	=>	Array ('label' => 'la_Pending', 'on_sql' => '', 'off_sql' => '%1$s.Status != 1'  ),
+							2	=>	Array ('label' => 'la_BackOrders', 'on_sql' => '', 'off_sql' => '%1$s.Status != 2'  ),
+							3	=>	Array ('label' => 'la_ToShip', 'on_sql' => '', 'off_sql' => '%1$s.Status != 3' ),
+							4	=>	Array ('label' => 'la_Processed', 'on_sql' => '', 'off_sql' => '%1$s.Status != 4'  ),
+							5	=>	Array ('label' => 'la_Denied', 'on_sql' => '', 'off_sql' => '%1$s.Status != 5'  ),
+							6	=>	Array ('label' => 'la_Archived', 'on_sql' => '', 'off_sql' => '%1$s.Status != 6'  ),
+						)
+					),
 
 					'CalculatedFields' => Array (
 						'' => Array (
 							'CustomerName'			=>	'IF( ISNULL(u.Login), IF (%1$s.PortalUserId = -1, \'root\', IF (%1$s.PortalUserId = -2, \'Guest\', \'n/a\')), CONCAT(u.FirstName,\' \',u.LastName) )',
 							'Username'				=>	'IF( ISNULL(u.Login),\'root\',u.Login)',
 							'OrderNumber'			=>	'CONCAT(LPAD(Number,6,"0"),\'-\',LPAD(SubNumber,3,"0") )',
 							'SubtotalWithoutDiscount'	=>	'(SubTotal + DiscountTotal)',
 							'SubtotalWithDiscount'	=>	'(SubTotal)',
 							'AmountWithoutVAT'		=>	'(SubTotal+IF(ShippingTaxable=1, ShippingCost, 0)+IF(ProcessingTaxable=1, ProcessingFee, 0))',
 							'TotalAmount'			=>	'SubTotal+ShippingCost+VAT+ProcessingFee+InsuranceFee-GiftCertificateDiscount',
 							'CouponCode'			=>	'pc.Code',
 							'CouponName'			=>	'pc.Name',
 							'AffiliateUser'			=>	'IF( LENGTH(au.Login),au.Login,\'!la_None!\')',
 							'AffiliatePortalUserId'	=>	'af.PortalUserId',
 							'GiftCertificateCode'	=>	'gc.Code',
 							'GiftCertificateRecipient' => 'gc.Recipient',
 						),
 
 						'myorders' => Array (
 							'OrderNumber'			=>	'CONCAT(LPAD(Number,6,"0"),\'-\',LPAD(SubNumber,3,"0") )',
 							'SubtotalWithoutDiscount'	=>	'(SubTotal + DiscountTotal)',
 							'SubtotalWithDiscount'	=>	'(SubTotal)',
 							'AmountWithoutVAT'		=>	'(SubTotal+IF(ShippingTaxable=1, ShippingCost, 0)+IF(ProcessingTaxable=1, ProcessingFee, 0))',
 							'TotalAmount'			=>	'SubTotal+ShippingCost+VAT+ProcessingFee+InsuranceFee-GiftCertificateDiscount',
 							/*'ItemsCount'			=>	'COUNT(%1$s.OrderId)',*/
 						),
 					),
 					// %1$s - table name of object
 					// %2$s - calculated fields
 					'ListSQLs' => Array (
 						'' => '	SELECT %1$s.* %2$s
 								FROM %1$s
 								LEFT JOIN '.TABLE_PREFIX.'PortalUser u ON %1$s.PortalUserId = u.PortalUserId
 								LEFT JOIN '.TABLE_PREFIX.'ProductsCoupons pc ON %1$s.CouponId = pc.CouponId
 								LEFT JOIN '.TABLE_PREFIX.'GiftCertificates gc ON %1$s.GiftCertificateId = gc.GiftCertificateId
 								LEFT JOIN '.TABLE_PREFIX.'Affiliates af ON %1$s.AffiliateId = af.AffiliateId
 								LEFT JOIN '.TABLE_PREFIX.'PortalUser au ON af.PortalUserId = au.PortalUserId',
 
 						'myorders' => '	SELECT %1$s.* %2$s
 										FROM %1$s
 										LEFT JOIN '.TABLE_PREFIX.'PortalUser u ON %1$s.PortalUserId = u.PortalUserId',
 //										LEFT JOIN '.TABLE_PREFIX.'OrderItems ON %1$s.OrderId = '.TABLE_PREFIX.'OrderItems.OrderId',
 					),
 
 					'ItemSQLs' => Array (
 						'' => '	SELECT %1$s.* %2$s FROM %1$s
 								LEFT JOIN '.TABLE_PREFIX.'PortalUser u ON %1$s.PortalUserId = u.PortalUserId
 								LEFT JOIN '.TABLE_PREFIX.'ProductsCoupons pc ON %1$s.CouponId = pc.CouponId
 								LEFT JOIN '.TABLE_PREFIX.'GiftCertificates gc ON %1$s.GiftCertificateId = gc.GiftCertificateId
 								LEFT JOIN '.TABLE_PREFIX.'Affiliates af ON %1$s.AffiliateId = af.AffiliateId
 								LEFT JOIN '.TABLE_PREFIX.'PortalUser au ON af.PortalUserId = au.PortalUserId',
 					),
 
 					'SubItems'	=> Array ('orditems'),
 					'ListSortings'	=> 	Array (
 																'' => Array (
 																	'Sorting' => Array ('OrderDate' => 'desc'),
 																)
 															),
 					'Fields' => Array (
 			            'OrderId' => Array ('type' => 'int', 'not_null' => 1, 'default' => 0, 'filter_type' => 'equals'),
 			            'Number' => Array ('type' => 'int', 'required' =>1, 'formatter' => 'kFormatter', 'unique' =>Array ('SubNumber'), 'format' => '%06d', 'max_value_inc'>999999, 'not_null' => 1, 'default' => 0),
 			            'SubNumber' => Array ('type' => 'int', 'required' =>1, 'formatter' => 'kFormatter', 'unique' =>Array ('Number'), 'format' => '%03d', 'max_value_inc'>999, 'not_null' => 1, 'default' => 0),
 			            'Status' => Array ('type' => 'int', 'formatter' => 'kOptionsFormatter', 'options' =>Array (0=> 'la_Incomplete',1=> 'la_Pending',2=> 'la_BackOrders',3=> 'la_ToShip',4=> 'la_Processed',5=> 'la_Denied',6=> 'la_Archived'), 'use_phrases' =>1, 'not_null' => 1, 'default' => 0, 'filter_type' => 'equals'),
 						'OnHold' => Array (
 							'type' => 'int',
 							'formatter' => 'kOptionsFormatter',
 							'options' => Array (0 => 'la_No', 1 => 'la_Yes'), 'use_phrases' => 1,
 							'not_null' => 1, 'default' => 0,
 						),
 			            'OrderDate' => Array ('type' => 'int', 'formatter' => 'kDateFormatter', 'required' => 1, 'default' => '#NOW#'),
 			            'PortalUserId' =>Array ('type' => 'int', 'formatter' => 'kLEFTFormatter', 'error_msgs' => Array ('invalid_option' => '!la_error_UserNotFound!'), 'options' =>Array (-1=> 'root',-2=> 'Guest'), 'left_sql' => 'SELECT %s FROM '.TABLE_PREFIX.'PortalUser WHERE `%s` = \'%s\'', 'left_key_field' => 'PortalUserId', 'left_title_field' => 'Login', 'required' =>1, 'not_null' =>1, 'default' =>-1),
 			            'OrderIP' => Array ('type' => 'string', 'not_null' => 1, 'default' => '', 'filter_type' => 'like'),
 			            'UserComment' => Array ('type' => 'string', 'formatter' => 'kFormatter', 'using_fck' => 1, 'default' => NULL),
 			            'AdminComment' => Array ('type' => 'string', 'formatter' => 'kFormatter', 'using_fck' => 1, 'default' => NULL),
 			            'BillingTo' => Array ('type' => 'string', 'not_null' => 1, 'default' => ''),
 			            'BillingCompany' => Array ('type' => 'string', 'not_null' => 1, 'default' => ''),
 			            'BillingPhone' => Array ('type' => 'string', 'not_null' => 1, 'default' => ''),
 			            'BillingFax' => Array ('type' => 'string', 'not_null' => 1, 'default' => ''),
 			            'BillingEmail' => Array (
 			            	'type' => 'string',
 			            	'formatter' => 'kFormatter',
 			            	'regexp' => '/^(' . REGEX_EMAIL_USER . '@' . REGEX_EMAIL_DOMAIN . ')$/i',
 			            	'not_null' => 1, 'default' => '',
 						),
 			            'BillingAddress1' => Array ('type' => 'string', 'not_null' => 1, 'default' => ''),
 			            'BillingAddress2' => Array ('type' => 'string', 'not_null' => 1, 'default' => ''),
 			            'BillingCity' => Array ('type' => 'string', 'not_null' => 1, 'default' => ''),
 			            'BillingState' => Array (
 			            	'type' => 'string',
 			            	'formatter' => 'kOptionsFormatter',
 			            	'options' => Array (),
 							'option_key_field' => 'DestAbbr',
 							'option_title_field' => 'Translation',
 							'not_null' => 1, 'default' => '',
 						),
 			            'BillingZip' => Array ('type' => 'string', 'not_null' => 1, 'default' => ''),
 			            'BillingCountry' => Array (
 			            	'type' => 'string',
 			            	'formatter' => 'kOptionsFormatter',
 			            	'options_sql' => 'SELECT %1$s
 													FROM '.TABLE_PREFIX.'StdDestinations
 												LEFT JOIN '.TABLE_PREFIX.'Phrase
 													ON '.TABLE_PREFIX.'Phrase.Phrase = '.TABLE_PREFIX.'StdDestinations.DestName
 												WHERE
 													DestType = 1 AND LanguageId = %2$s
 												ORDER BY Translation',
 							'option_key_field' => 'DestAbbr',
 							'option_title_field' => 'Translation',
 							'not_null' => 1, 'default' => 'USA',
 						),
 			            'VAT' => Array ('type' => 'float', 'formatter' => 'kFormatter', 'not_null' =>1, 'default' => '0', 'format' => '%01.2f'),
 			            'VATPercent' => Array ('type' => 'float', 'formatter' => 'kFormatter', 'not_null' =>1, 'default' => '0', 'format' => '%01.3f'),
-			            'PaymentType' => Array ('type' => 'int', 'formatter' => 'kOptionsFormatter', 'options_sql' => 'SELECT %s FROM '.TABLE_PREFIX.'PaymentTypes WHERE Status = 1', 'option_key_field' => 'PaymentTypeId', 'option_title_field' => 'Description', 'not_null' => 1, 'default' => 0),
+			            'PaymentType' => Array (
+			            	'type' => 'int',
+			            	'formatter' => 'kOptionsFormatter',
+			            	'options_sql' => 'SELECT %s 
+			            						FROM ' . TABLE_PREFIX . 'PaymentTypes 
+	        		    						WHERE Status = 1 
+	            								ORDER BY Priority DESC, Name ASC',
+							'option_key_field' => 'PaymentTypeId', 'option_title_field' => 'Description',
+							'not_null' => 1, 'default' => 0
+						),
+						
 			            'PaymentAccount' => Array ('type' => 'string', 'not_null' => 1, 'cardtype_field' => 'PaymentCardType', 'default' => '', 'filter_type' => 'like'),
 			            'PaymentNameOnCard' => Array ('type' => 'string', 'not_null' => 1, 'default' => ''),
 			            'PaymentCCExpDate' => Array ('type' => 'string', 'formatter' => 'kCCDateFormatter', 'month_field' => 'PaymentCCExpMonth', 'year_field' => 'PaymentCCExpYear', 'not_null' => 1, 'default' => ''),
 			            'PaymentCardType' => Array ('type' => 'string', 'not_null' => 1, 'formatter' => 'kOptionsFormatter', 'options' => Array ('' => '', '1' => 'Visa', '2' => 'Mastercard', '3' => 'Amex', '4' => 'Discover', '5' => 'Diners Club', '6' => 'JBC'), 'default' => ''),
 			            'PaymentExpires' => Array ('type' => 'int', 'formatter' => 'kDateFormatter', 'default' => '#NOW#'),
 			            'ShippingTo' => Array ('type' => 'string', 'not_null' => 1, 'default' => ''),
 			            'ShippingCompany' => Array ('type' => 'string', 'not_null' => 1, 'default' => ''),
 			            'ShippingPhone' => Array ('type' => 'string', 'not_null' => 1, 'default' => ''),
 			            'ShippingFax' => Array ('type' => 'string', 'not_null' => 1, 'default' => ''),
 			            'ShippingEmail' => Array (
 			            	'type' => 'string', 'not_null' => 1, 'default' => '',
 			            	'formatter' => 'kOptionsFormatter',
 			            	'options' => Array (),
 							'option_key_field' => 'DestAbbr',
 							'option_title_field' => 'Translation',											
 						),
 			            'ShippingAddress1' => Array ('type' => 'string', 'not_null' => 1, 'default' => ''),
 			            'ShippingAddress2' => Array ('type' => 'string', 'not_null' => 1, 'default' => ''),
 			            'ShippingCity' => Array ('type' => 'string', 'not_null' => 1, 'default' => ''),
 			            'ShippingState' => Array (
 			            	'type' => 'string', 'formatter' => 'kOptionsFormatter',
 			            							'options' => Array (),
 								            		'option_key_field' => 'DestAbbr', 'option_title_field' => 'Translation',
 								            		'not_null' => 1, 'default' => ''),
 			            'ShippingZip' => Array ('type' => 'string', 'not_null' => 1, 'default' => ''),
 			            'ShippingCountry' => Array (
 			            	'type' => 'string',
 			            	'formatter' => 'kOptionsFormatter',
 			            	'options_sql' => 'SELECT %1$s
 													FROM '.TABLE_PREFIX.'StdDestinations
 												LEFT JOIN '.TABLE_PREFIX.'Phrase
 													ON '.TABLE_PREFIX.'Phrase.Phrase = '.TABLE_PREFIX.'StdDestinations.DestName
 												WHERE
 													DestType = 1 AND LanguageId = %2$s
 												ORDER BY Translation',
 							'option_key_field' => 'DestAbbr',
 							'option_title_field' => 'Translation',
 							'not_null' => 1, 'default' => 'USA',
 						),
 			            'ShippingType' => Array (
 			            	'type' => 'int',
 			            	'formatter' => 'kOptionsFormatter',
 			            	'options_sql' => 'SELECT %s 
 			            							FROM ' . TABLE_PREFIX . 'ShippingType 
 			            						WHERE Status = 1',
 							'option_key_field' => 'ShippingID',
 							'option_title_field' => 'Name',
 							'not_null' => 1, 'default' => 0,
 						),
 			            'ShippingCost' => Array ('type' => 'double', 'formatter' => 'kFormatter', 'format' => '%01.2f', 'not_null' => 1, 'default' => '0.00'),
 			            'ShippingCustomerAccount' => Array ('type' => 'string', 'not_null' => 1, 'default' => ''),
 			            'ShippingTracking' => Array ('type' => 'string', 'not_null' => 1, 'default' => ''),
 			            'ShippingDate' => Array ('type' => 'int', 'formatter' => 'kDateFormatter', 'default' => null),
 			            'SubTotal' => Array ('type' => 'float', 'formatter' => 'kFormatter', 'format' => '%01.2f', 'not_null' => 1, 'default' => '0.00'),
 			            'ReturnTotal' => Array ('type' => 'float', 'formatter' => 'kFormatter', 'format' => '%01.2f', 'not_null' => 1, 'default' => '0.00'),
 			            'CostTotal' => Array ('type' => 'float', 'formatter' => 'kFormatter', 'format' => '%01.2f', 'not_null' => 1, 'default' => '0.00'),
 			            'OriginalAmount'	=>	Array ('type' => 'float', 'formatter' => 'kFormatter', 'format' => '%01.2f', 'not_null' => 1, 'default' => '0.00'),
 			            'ShippingOption' => Array ('type' => 'int', 'formatter' => 'kOptionsFormatter', 'use_phrases' => 1, 'options' => Array (0 => 'la_ship_all_together', 1 => 'la_ship_backorder_separately', 2 => 'la_ship_backorders_upon_avail'), 'default' =>0),
 			            'ShippingGroupOption' => Array ('type' => 'int', 'not_null' => 1, 'formatter' => 'kOptionsFormatter', 'use_phrases' => 1, 'options' => Array (0 => 'la_auto_group_shipments', 1 => 'la_manual_group_shipments'), 'default' =>0),
 			            'GiftCertificateId' => Array ('type' => 'int', 'default' => null),
 			            'GiftCertificateDiscount' => Array ('type' => 'float', 'formatter' => 'kFormatter', 'format' => '%01.2f', 'not_null' => 1, 'default' => '0.00',),
 			            'ShippingInfo' => Array ('type' => 'string', 'default' => NULL),
 			            'CouponId' => Array ('type' => 'int', 'default' => null),
 			            'CouponDiscount' => Array ('type' => 'float', 'not_null' => 1, 'default' => '0.00', 'formatter' => 'kFormatter', 'format' => '%01.2f'),
 			            'DiscountTotal' => Array ('type' => 'float', 'not_null' => 1, 'default' => '0.00', 'formatter' => 'kFormatter', 'format' => '%01.2f'),
 			            'TransactionStatus' => Array ('type' => 'int', 'formatter' => 'kOptionsFormatter', 'options' => Array (0=> 'la_Invalid', 1 => 'la_Verified', 2 => 'la_Penging'), 'use_phrases' =>1, 'not_null' => 1, 'default' => 2),
 						'GWResult1' => Array ('type' => 'string', 'formatter' => 'kSerializedFormatter', 'default' => NULL),
 			            'GWResult2' => Array ('type' => 'string', 'formatter' => 'kSerializedFormatter', 'default' => NULL),
 			            'AffiliateId' => Array ('type' => 'int', 'formatter' => 'kLEFTFormatter', 'error_msgs' => Array ('invalid_option' => '!la_error_UserNotFound!'),  'options' => Array (0 => 'lu_None'), 'left_sql' => 'SELECT %s FROM '.TABLE_PREFIX.'Affiliates af LEFT JOIN '.TABLE_PREFIX.'PortalUser pu ON pu.PortalUserId = af.PortalUserId WHERE `%s` = \'%s\'', 'left_key_field' => 'AffiliateId', 'left_title_field' => 'Login', 'not_null' =>1, 'default' =>0),
 			            'VisitId' => Array ('type' => 'int', 'not_null' => 1, 'default' => 0),
             			'AffiliateCommission' => Array ('type' => 'double', 'formatter' => 'kFormatter', 'format' => '%.02f', 'not_null' => 1, 'default' => '0.0000'),
             			'ProcessingFee' => Array ('type' => 'double', 'formatter' => 'kFormatter', 'format' => '%.02f', 'not_null' => '0', 'default' => '0.0000'),
             			'InsuranceFee' => Array ('type' => 'float', 'formatter' => 'kFormatter', 'format' => '%01.2f', 'not_null' => 1, 'default' => '0.00'),
             			'ShippingTaxable'		=>	Array ('type' => 'int', 'not_null' => 0, 'default' => 0),
             			'ProcessingTaxable'		=>	Array ('type' => 'int', 'not_null' => 0, 'default' => 0),
             			'IsRecurringBilling'	=> Array (
 							'type' => 'int',
 							'formatter' => 'kOptionsFormatter',
 							'options' => Array (0 => 'la_No', 1 => 'la_Yes',), 'use_phrases' => 1,
 							'default' => 0, 'not_null' => 1,
 						),
             			'ChargeOnNextApprove'	=> Array (
 							'type' => 'int',
 							'formatter' => 'kOptionsFormatter',
 							'options' => Array (0 => 'la_No', 1 => 'la_Yes',), 'use_phrases' => 1,
 							'default' => 0, 'not_null' => 1,
 						),
             			'NextCharge'			=>	Array ('type' => 'int', 'formatter' => 'kDateFormatter', 'default' => null),
             			'GroupId'			=>	Array ('type' => 'int', 'not_null' => 1, 'default' => 0),
 						'GoogleOrderNumber' => Array ('type' => 'string', 'default' => NULL), // MySQL BIGINT UNSIGNED = 8 Bytes, PHP int = 4 Bytes -> threat as string
 			        ),
 
 					'VirtualFields'	=> 	Array (
 						'CustomerName'			=>	Array ('type' => 'string', 'default' => '', 'filter_type' => 'like'),
 						'TotalAmount'			=>	Array ('type' => 'float', 'formatter' => 'kFormatter', 'format' => '%01.2f', 'not_null' => 1, 'default' => '0.00'),
 						'AmountWithoutVAT'		=>	Array ('type' => 'float', 'formatter' => 'kFormatter', 'format' => '%01.2f', 'not_null' => 1, 'default' => '0.00'),
 						'SubtotalWithDiscount'	=>	Array ('type' => 'float', 'formatter' => 'kFormatter', 'format' => '%01.2f', 'not_null' => 1, 'default' => '0.00'),
 						'SubtotalWithoutDiscount'	=>	Array ('type' => 'float', 'formatter' => 'kFormatter', 'format' => '%01.2f', 'not_null' => 1, 'default' => '0.00'),
 						'OrderNumber'			=>	Array ('type' => 'string', 'default' => '', 'filter_type' => 'like'),
 
 						// for ResetToUser
 						'UserTo'		=>	Array ('type' => 'string', 'default' => ''),
 						'UserCompany'	=>	Array ('type' => 'string', 'default' => ''),
 						'UserPhone'		=>	Array ('type' => 'string', 'default' => ''),
 						'UserFax'		=>	Array ('type' => 'string', 'default' => ''),
 						'UserEmail'		=>	Array ('type' => 'string', 'default' => ''),
 						'UserAddress1'	=>	Array ('type' => 'string', 'default' => ''),
 						'UserAddress2'	=>	Array ('type' => 'string', 'default' => ''),
 						'UserCity'		=>	Array ('type' => 'string', 'default' => ''),
 						'UserState'		=>	Array ('type' => 'string', 'default' => ''),
 						'UserZip'		=>	Array ('type' => 'string', 'default' => ''),
 						'UserCountry'	=>	Array ('type' => 'string', 'default' => ''),
 
 						// for Search
 						'Username'		=>	Array ('type' => 'string', 'filter_type' => 'like'),
 						'OrderSearchId' =>	Array ('type' => 'int', 'filter_type' => 'equals', 'filter_field' => 'OrderId'),
 						'FromDateTime'	=>	Array ('formatter' => 'kDateFormatter', 'default' => '', 'filter_type' => 'range_from', 'filter_field' => 'OrderDate' ),
 						'ToDateTime'	=>	Array ('formatter' => 'kDateFormatter', 'default' => '', 'filter_type' => 'range_to', 'filter_field' => 'OrderDate', 'empty_time' => adodb_mktime(23,59,59) ),
 						'FromAmount'	=>	Array ('type' => 'double', 'formatter' => 'kFormatter', 'format' => '%01.2f', 'filter_type' => 'range_from', 'filter_field' => 'TotalAmount'),
 						'ToAmount'	=>	Array ('type' => 'double', 'formatter' => 'kFormatter', 'format' => '%01.2f', 'filter_type' => 'range_to', 'filter_field' => 'TotalAmount'),
 						'HasBackOrders' => Array ('default' =>false),
 						'PaymentCVV2'	=>	Array ('type' => 'string', 'default' =>false),
 						'AffiliateUser' =>	Array ('type' => 'string', 'filter_type' => 'like'),
 						'AffiliatePortalUserId' => Array ('type' => 'int'),
 
 						// export related fields: begin
 						'ExportFormat'			=>	Array ('type' => 'int', 'formatter' => 'kOptionsFormatter', 'options' => Array (1 => 'CSV', /*2 => 'XML'*/), 'default' => 1),
 						'ExportFilename'		=>	Array ('type' => 'string', 'default' => ''),
 						'FieldsSeparatedBy'		=>	Array ('type' => 'string', 'default' => ', '),
 						'FieldsEnclosedBy'		=>	Array ('type' => 'string', 'default' => '"'),
 						'LineEndings'			=>	Array ('type' => 'int', 'formatter' => 'kOptionsFormatter', 'options' => Array (1 => 'Windows', 2 => 'UNIX'), 'default' => 1),
 						'LineEndingsInside'		=>	Array ('type' => 'int', 'formatter' => 'kOptionsFormatter', 'options' => Array (1 => 'CRLF', 2 => 'LF'), 'default' => 2),
 						'IncludeFieldTitles'	=>	Array (
 					    	'type' => 'int', 
 					    	'formatter' => 'kOptionsFormatter', 
 					    	'options' => Array (0 => 'la_No', 1 => 'la_Yes'),
 					    	'use_phrases' => 1, 'not_null' => 1, 'default' => 1,
 						),
 						'ExportColumns'			=>	Array ('type' => 'string', 'formatter' => 'kOptionsFormatter', 'options' => Array ()),
 						'AvailableColumns'		=>	Array ('type' => 'string', 'formatter' => 'kOptionsFormatter', 'options' => Array ()),
 						'ExportPresets' => Array ('type' => 'string', 'formatter' => 'kOptionsFormatter', 'options' => Array ()),
 						'ExportSavePreset' => Array (
 					    	'type' => 'int', 
 					    	'formatter' => 'kOptionsFormatter', 
 					    	'options' => Array (0 => 'la_No', 1 => 'la_Yes'),
 					    	'use_phrases' => 1, 'not_null' => 1, 'default' => 0,
 						),
 						'ExportPresetName' =>	Array ('type' => 'string'),
 						// export related fields: end
 					),
 					'Grids'	=> Array (
 						'Default' => Array (
 							'Icons' => Array (
 								'default' => 'icon16_item.png',
 								1 => 'icon16_pending.png',
 								5 => 'icon16_disabled.png',
 								'module' => 'core',
 							),
 
 							'Fields' => Array (
 								'OrderId'		=>	Array ('title' => 'la_col_Id', 'data_block' => 'grid_checkbox_td', 'filter_block' => 'grid_range_filter', 'width' => 70, ),																										'OrderNumber'	=> Array ( 'title' => 'la_col_OrderNumber', 'data_block' => 'grid_ordernumber_td', 'filter_block' => 'grid_like_filter', 'width' => 100, ),
 								'OrderDate' => Array ( 'title' => 'la_col_OrderDate', 'data_block' => 'grid_checkbox_td', 'filter_block' => 'grid_date_range_filter', 'width' => 140, ),
 								'CustomerName'	=> Array ( 'title' => 'la_col_CustomerName', 'data_block' => 'grid_userlink_td', 'user_field' => 'PortalUserId', 'filter_block' => 'grid_like_filter', 'width' => 140, ),
 								'PaymentType'	=> Array ( 'title' => 'la_col_PaymentType', 'data_block' => 'grid_billinglink_td', 'filter_block' => 'grid_options_filter', 'width' => 140, ),
 								'TotalAmount'	=> Array ( 'title' => 'la_col_TotalAmount', 'data_block' => 'grid_previewlink_td', 'filter_block' => 'grid_range_filter', 'width' => 140, ),
 								'AffiliateUser'	=> Array ( 'title' => 'la_col_AffiliateUser', 'data_block' => 'grid_userlink_td', 'user_field' => 'AffiliatePortalUserId', 'filter_block' => 'grid_like_filter', 'width' => 140, ),
 								'OnHold' => Array ('title' => 'la_col_OnHold', 'filter_block' => 'grid_options_filter', 'width' => 100, ),
 							),
 						),
 
 						'Search' => Array (
 							'Icons' => Array (
 								'default' => 'icon16_item.png',
 								1 => 'icon16_pending.png',
 								5 => 'icon16_disabled.png',
 								'module' => 'core',
 							),
 							'Fields' => Array (
 								'OrderId'		=>	Array ('title' => 'la_col_Id', 'data_block' => 'grid_checkbox_td', 'filter_block' => 'grid_range_filter', 'width' => 70, ),
 								'OrderNumber'	=>	Array ('title' => 'la_col_OrderNumber', 'data_block' => 'grid_ordernumber_td', 'filter_block' => 'grid_like_filter', 'width' => 100, ),
 								'Status'		=>	Array ('title' => 'la_col_Status', 'filter_block' => 'grid_options_filter', 'filter_block' => 'grid_options_filter', 'width' => 100, ),
 								'OrderDate'	=>	Array ('title' => 'la_col_OrderDate', 'filter_block' => 'grid_date_range_filter', 'width' => 140, ),
 								'CustomerName'		=>	Array ('title' => 'la_col_CustomerName', 'data_block' => 'grid_userlink_td', 'user_field' => 'PortalUserId', 'filter_block' => 'grid_like_filter'),
 								'PaymentType'	=> 	Array ('title' => 'la_col_PaymentType', 'data_block' => 'grid_billinglink_td', 'filter_block' => 'grid_options_filter'),
 								'TotalAmount'	=>	Array ('title' => 'la_col_TotalAmount', 'data_block' => 'grid_previewlink_td', 'filter_block' => 'grid_float_range_filter'),
 								'AffiliateUser'	=>	Array ( 'title' => 'la_col_AffiliateUser', 'data_block' => 'grid_userlink_td', 'user_field' => 'AffiliatePortalUserId', 'filter_block' => 'grid_user_like_filter'),
 								'OrderIP'		=>	Array ('title' => 'la_col_OrderIP', 'filter_block' => 'grid_like_filter'),
 								'Username'		=>	Array ('title' => 'la_col_Username', 'filter_block' => 'grid_user_like_filter'),
 								'PaymentAccount'	=>	Array ('title' => 'la_col_CreditCardNumber', 'filter_block' => 'grid_like_filter'),
 							),
 						),
 					),
 	);
\ No newline at end of file