Your IP : 52.14.196.242


Current Path : /home/bitrix/ext_www/klimatlend.ua/bitrix/modules/sale/lib/location/
Upload File :
Current File : /home/bitrix/ext_www/klimatlend.ua/bitrix/modules/sale/lib/location/connector.php

<?php
/**
* Bitrix Framework
* @package bitrix
* @subpackage sale
* @copyright 2001-2014 Bitrix
*/

namespace Bitrix\Sale\Location;

use Bitrix\Main;
use Bitrix\Main\DB;
use Bitrix\Main\Config;
use Bitrix\Main\Entity;
use Bitrix\Main\Localization\Loc;

use Bitrix\Sale\Location\Util\Assert;
use Bitrix\Sale\Location\DB\BlockInserter;

Loc::loadMessages(__FILE__);

abstract class Connector extends Entity\DataManager
{
	const LINK_ID = 								0x01;
	const LINK_CODE = 								0x02;

	const DB_LOCATION_FLAG = 						'L';
	const DB_GROUP_FLAG = 							'G';

	const LSTAT_IN_NOT_CONNECTED_BRANCH = 			0x01;
	const LSTAT_IS_CONNECTOR = 						0x02;
	const LSTAT_ABOVE_CONNECTOR = 					0x03;
	const LSTAT_BELOW_CONNECTOR =					0x04;

	#####################################
	#### Entity settings
	#####################################

	/** 
	* Method returns name of table column that provides linking between location table and entity table. For example, for delivery it should return 'DELIVERY_ID'
	* Limitation: mechanism does not support compound primary keys
	*
	* @return string field name
	*/
	abstract public function getLinkField();

	/** Method should return a name of the "target" entity plus namespace but without trailing 'Table'. For example, for delivery this should be 'Bitrix\Sale\Delivery\Delivery'
	*
	* @return string table name
	*/
	abstract public function getTargetEntityName();

	/**
	* Returns a name of type filed. By default 'LOCATION_TYPE' is used for the most of. Does not make any sense if getUseGroups() returns false
	* 
	* @return string
	*/
	public static function getTypeField()
	{
		return 'LOCATION_TYPE';
	}

	/**
	* Returns a name of the filed that keep location id or code. By default 'LOCATION_ID' is used for the most. For newly-created entites this should be LOCATION_CODE (string)
	* 
	* @return string
	*/
	public static function getLocationLinkField()
	{
		return 'LOCATION_ID';
	}

	/**
	* Returns a name of the primary field of the "target" entity. By default 'ID' is used for the most. See target entity class for sure.
	* 
	* @return string
	*/
	public static function getTargetEntityPrimaryField()
	{
		return 'ID';
	}

	/**
	* Show whether or not entity use linking through groups. E.g. delivery would return true, 
	* but location groups (which can be treated as entity too) - always return false (groups cannot be connected with groups)
	* 
	* @return boolean
	*/
	public static function getUseGroups()
	{
		return true;
	}

	/**
	* Show whether or not entity use link tracking. Link tracking allows keeping information about link existence in option strings.
	* So, this is some kind of cacheing mechanism. It can be usefull, but may exceed option string length when there are lots of entity records exist.
	* 
	* @return boolean
	*/
	public static function getUseLinkTracking()
	{
		return false;
	}

	/**
	* Return the connection type for the current entity. Return value that is equal to self::LINK_ID stands for ID, otherwise - for CODE link type
	* 
	* @return integer
	*/
	public static function getConnectType()
	{
		$map = static::getMap();
		$locLinkFld = static::getLocationLinkField();

		if(isset($map[$locLinkFld]))
			return $map[$locLinkFld]['data_type'] == 'integer' ? self::LINK_ID : self::LINK_CODE;

		Assert::announceNotImplemented(Loc::getMessage('SALE_LOCATION_CONNECTOR_ENTITY_LINK_FLD_NOT_FOUND_EXCEPTION'));
	}

	/**
	* Just a shortcut to link type identification
	* 
	* @return boolean
	*/
	public static function getUseCodes()
	{
		return static::getConnectType() == static::LINK_CODE;
	}

	#####################################
	#### CRUD wrappers
	#####################################

	/**
	* Adds a new connection between location and entity
	* 
	* @param mixed[] $data data to add (See getMap() of a certain implementation for $data key details)
	* 
	* @return Bitrix\Main\Entity\AddResult
	*/
	public static function add(array $data)
	{
		$res = parent::add($data);
		if($res->isSuccess())
			static::setLinkUsage($data[static::getLinkField()], $data[static::getTypeField()], true);

		$GLOBALS['CACHE_MANAGER']->ClearByTag('sale-location-data');
		static::onAfterModifiy();

		return $res;
	}

	/**
	* Updates a connection between location and entity
	* 
	* @param mixed $primary relation primary key value
	* @param mixed[] $data data to update with (See getMap() of a certain implementation for $data key details)
	* 
	* @return Bitrix\Main\Entity\UpdateResult
	*/
	public static function update($primary, array $data)
	{
		$linkFld = static::getLinkField();

		if($primary && isset($data[$linkFld])) // it will break below, at parent::delete()
			$link = static::getByPrimary($primary)->fetch();

		$res = parent::update($primary, $data);
		if($res->isSuccess() && isset($data[$linkFld]) && $data[$linkFld] != $link[$linkFld]) // if switched from one entity to another
		{
			static::resetLinkUsage($link[static::getLinkField()]); // for donor entity we need to recalc link existence
			static::setLinkUsage($data[$linkFld], $data[static::getTypeField()], true); // we know there ARE links for retsepient entity
		}

		$GLOBALS['CACHE_MANAGER']->ClearByTag('sale-location-data');
		static::onAfterModifiy();

		return $res;
	}

	/**
	* Removes a connection between location and entity
	* 
	* @param mixed $primary relation primary key value
	* 
	* @return Bitrix\Main\Entity\DeleteResult
	*/
	public static function delete($primary)
	{
		if($primary) // it will break below, at parent::delete()
			$link = static::getByPrimary($primary)->fetch();

		$res = parent::delete($primary);
		if($res->isSuccess())
			static::resetLinkUsage($link[static::getLinkField()]);

		$GLOBALS['CACHE_MANAGER']->ClearByTag('sale-location-data');
		static::onAfterModifiy();

		return $res;
	}

	public static function onAfterModifiy()
	{
	}

	/**
	* Establishes several connections between entity and locations. Could be used to add and delete selected relations.
	* 
	* @param mixed $entityPrimary primary key for an entity
	* @param integer[][]|string[][] $links array of locations and groups to link an entity with
	* 
	* 	Argument format:
	* 	array(
	* 		'L' => array(), // an array of IDs or CODEs of locations
	* 		'G' => array() // an array of IDs or CODEs of location groups
	* 	)
	* 
	* @throws Bitrix\Main\ArgumentNullException
	* 
	* @return boolean
	*/
	public static function updateMultipleForOwner($entityPrimary, $links = array(), $behaviour = array('REMOVE_ABSENT' => true))
	{
		$entityPrimary = 	Assert::expectStringNotNull($entityPrimary, '$entityPrimary');
		$links = 			static::checkUpdateLinks($links);

		$updateLocations = 	is_array($links[self::DB_LOCATION_FLAG]);
		$updateGroups = 	is_array($links[self::DB_GROUP_FLAG]) && static::getUseGroups();

		// get existed relations
		$existed = 			static::getLinkedLocations($entityPrimary);

		if($updateLocations)
			static::updateMultipleLinkType($entityPrimary, $links[self::DB_LOCATION_FLAG], $existed[self::DB_LOCATION_FLAG], self::DB_LOCATION_FLAG, $behaviour['REMOVE_ABSENT']);

		if($updateGroups)
			static::updateMultipleLinkType($entityPrimary, $links[self::DB_GROUP_FLAG], $existed[self::DB_GROUP_FLAG], self::DB_GROUP_FLAG, $behaviour['REMOVE_ABSENT']);

		static::resetLinkUsage($entityPrimary);

		$GLOBALS['CACHE_MANAGER']->ClearByTag('sale-location-data');
		static::onAfterModifiy();

		return true;
	}

	// removes all links with a given entity
	public static function deleteAllForOwner($entityPrimary, $behaviour = array('BATCH_MODE' => false))
	{
		$entityPrimary = Assert::expectStringNotNull($entityPrimary, '$entityPrimary');

		if($behaviour['BATCH_MODE'])
		{
			// low-level drop
			$dbConnection = Main\HttpApplication::getConnection();
			$dbConnection->query('delete from '.static::getTableName().' where '.static::getLinkField().' = \''.$dbConnection->getSqlHelper()->forSql($entityPrimary).'\'');
		}
		else
		{
			// get existed relations
			$existed = static::getLinkedLocations($entityPrimary);

			static::updateMultipleLinkType($entityPrimary, array(), $existed[self::DB_LOCATION_FLAG], self::DB_LOCATION_FLAG, true);

			if(static::getUseGroups())
				static::updateMultipleLinkType($entityPrimary, array(), $existed[self::DB_GROUP_FLAG], self::DB_GROUP_FLAG, true);
		}

		static::setLinkUsage($entityPrimary, self::DB_LOCATION_FLAG, false);
		static::setLinkUsage($entityPrimary, self::DB_GROUP_FLAG, false);

		$GLOBALS['CACHE_MANAGER']->ClearByTag('sale-location-data');
		static::onAfterModifiy();
	}

	public static function deleteAll()
	{
		$dbConnection = Main\HttpApplication::getConnection();
		$dbConnection->query('truncate table '.static::getTableName());

		static::deleteLinkUsageOption();

		$GLOBALS['CACHE_MANAGER']->ClearByTag('sale-location-data');
		static::onAfterModifiy();
	}

	/**
	 * Removes all links and creates new ones
	 * 
	 */
	public static function resetMultipleForOwner($entityPrimary, $links = array())
	{
		$entityPrimary = 	Assert::expectStringNotNull($entityPrimary, '$entityPrimary');
		$links = 			static::checkUpdateLinks($links);

		static::deleteAllForOwner($entityPrimary, array('BATCH_MODE' => true));

		$map = 				static::getMap();
		$linkFld = 			static::getLinkField();
		$locationLinkFld = 	static::getLocationLinkField();
		$typeFld = 			static::getTypeField();

		$fields = array(
				$linkFld => $map[$linkFld], // DELIVERY_ID, SITE_ID, smth else
				$locationLinkFld => $map[$locationLinkFld] // LOCATION_ID or LOCATION_CODE or smth else
		);
		if($useGroups = static::getUseGroups())
			$fields[$typeFld] = $map[$typeFld]; // LOCATION_TYPE: L or G

		$inserter = new BlockInserter(array(
			'tableName' => static::getTableName(),
			'exactFields' => $fields
		));

		$smthAdded = false;
		if(is_array($links[self::DB_LOCATION_FLAG]))
		{
			foreach($links[self::DB_LOCATION_FLAG] as $id)
			{
				$data = array(
					$linkFld => $entityPrimary,
					$locationLinkFld => $id,
				);
				if($useGroups)
					$data[$typeFld] = self::DB_LOCATION_FLAG;

				$inserter->insert($data);
				$smthAdded = true;
			}
		}
		static::setLinkUsage($entityPrimary, self::DB_LOCATION_FLAG, $smthAdded);

		$smthAdded = false;
		if(is_array($links[self::DB_GROUP_FLAG]) && $useGroups)
		{
			foreach($links[self::DB_GROUP_FLAG] as $id)
			{
				$data = array(
					$linkFld => $entityPrimary,
					$locationLinkFld => $id,
				);
				if($useGroups)
					$data[$typeFld] = self::DB_GROUP_FLAG;

				$inserter->insert($data);
				$smthAdded = true;
			}
		}
		static::setLinkUsage($entityPrimary, self::DB_GROUP_FLAG, $smthAdded);

		$inserter->flush();

		$GLOBALS['CACHE_MANAGER']->ClearByTag('sale-location-data');
	}

	private static function updateMultipleLinkType($entityPrimary, $links, $existed, $linkType = self::DB_LOCATION_FLAG, $doRemove = true)
	{
		$useGroups = static::getUseGroups();

		$smthAdded = false;
		foreach($links as $k => $loc)
		{
			if(!isset($existed[$loc]))
			{
				$data = array(
					static::getLinkField() => $entityPrimary,
					static::getLocationLinkField() => $loc
				);
				if($useGroups)
					$data[static::getTypeField()] = $linkType;

				$res = static::add($data);
				if(!$res->isSuccess())
					throw new Main\SystemException(Loc::getMessage('SALE_LOCATION_CONNECTOR_ENTITY_CANNOT_ADD_EXCEPTION'));

				$smthAdded = true;
			}
			else
				unset($existed[$loc]);
		}

		if($doRemove && !empty($existed))
		{
			foreach($existed as $loc => $k)
			{
				$data = array(
					static::getLinkField() => $entityPrimary,
					static::getLocationLinkField() => $loc
				);
				if($useGroups)
					$data[static::getTypeField()] = $linkType;

				$res = static::delete($data);
				if(!$res->isSuccess())
					throw new Main\SystemException(Loc::getMessage('SALE_LOCATION_CONNECTOR_ENTITY_CANNOT_DELETE_EXCEPTION'));
			}
		}
	}

	#####################################
	#### Getters
	#####################################

	/**
	* Returns a set of locations that connected with a given entity, directly and, optionally, through groups.
	*
	* @param mixed $entityPrimary primary key for an entity
	* @param mixed[] $parameters filter and select parameters in ORM getlist() style, functionality is enought for usage, but limited. See limitations in code comments.
	* @param string[] $behaviour a set of behaviour options:
	*	<ul>
	*		<li>GET_LINKED_THROUGH_GROUPS boolean (default true) - if set to true, locations will be searched also throurgh groups (if entity connector supports groups)</li>
	*	</ul>
	*
	* @throws ArgumentNullException
	* @throws NotImplementedException
	*
	* @return String an SQL-query string that, being called, returns a list of locations
	*/
	public static function getConnectedLocationsQuery($entityPrimary, $parameters = array(), $behaviour = array('GET_LINKED_THROUGH_GROUPS' => false))
	{
		$entityPrimary = Assert::expectStringNotNull($entityPrimary, '$entityPrimary');

		if(!is_array($parameters))
			$parameters = array();
		if(!is_array($behaviour))
			$behaviour = array();
		if(!isset($behaviour['GET_LINKED_THROUGH_GROUPS']))
			$behaviour['GET_LINKED_THROUGH_GROUPS'] = false;

		$useGroups = GroupTable::checkGroupUsage() && static::getUseGroups(); // check if we have groups in project and entity uses groups
		$getLinkedThroughGroups = $behaviour['GET_LINKED_THROUGH_GROUPS'];
		if(!$useGroups)
			$getLinkedThroughGroups = false;

		$connType = static::getConnectType();

		// proxy select
		$select = array();
		if(!is_array($parameters['select']))
			$select = array('' => 'LOCATION');
		else
		{
			foreach($parameters['select'] as $k => $v)
			{
				if($v == '*')
					$select[''] = 'LOCATION';
				else
				{
					if(is_numeric($k) && strpos((string) $v, '.') === false) // is NOT a reference
					{
						$k = $v;
					}

					$select[$k] = 'LOCATION.'.$v;
				}
			}
		}

		// proxy filter
		$filter = array();
		if(is_array($parameters['filter']))
		{
			foreach($parameters['filter'] as $k => $v)
			{
				$filter['LOCATION.'.$k] = $v;
			}
		}

		// proxy order
		$order = array();
		if(is_array($parameters['order']))
		{
			//if($getLinkedThroughGroups)
				Assert::announceNotImplemented('Sorry, order-over-union clause is not implemented currently.');

			foreach($parameters['order'] as $k => $v)
				$order['LOCATION.'.$k] = $v;
		}

		if(is_array($parameters['runtime']))
			Assert::announceNotImplemented('Sorry, runtime clause is not implemented currently.');
		
		$sqls = array();

		if(static::checkLinkUsage($entityPrimary, self::DB_LOCATION_FLAG))
		{
			$strictFilter = array_merge($filter, array(
				'='.static::getLinkField() => $entityPrimary
			));

			if($useGroups)
				$strictFilter['='.static::getTypeField()] = self::DB_LOCATION_FLAG;

			$query = new Entity\Query(self::getEntity());
			$query
				->setSelect($select)
				->setFilter($strictFilter)
				->setOrder($order);

			$sqls[] = $query->getQuery();
		}

		if(static::checkLinkUsage($entityPrimary, self::DB_GROUP_FLAG) && $getLinkedThroughGroups)
		{
			$query = new Entity\Query(static::getEntity());

			if($connType == self::LINK_CODE) // entity connected by CODE
			{
				$query
					->registerRuntimeField(
						'G',
						array(
							'data_type' => '\Bitrix\Sale\Location\GroupTable',
							'reference' => array(
								'=this.'.static::getLocationLinkField() => 'ref.CODE',
								'=this.'.static::getTypeField() => array('?', self::DB_GROUP_FLAG)
							),
							'join_type' => 'inner'
						)
					)
					->registerRuntimeField(
						'LG',
						array(
							'data_type' => '\Bitrix\Sale\Location\GroupLocation',
							'reference' => array(
								'=this.G.ID' => 'ref.'.GroupLocationTable::getLinkField(),
							),
							'join_type' => 'inner'
						)
					)
					->registerRuntimeField(
						'LOCATION',
						array(
							'data_type' => '\Bitrix\Sale\Location\Location',
							'reference' => array(
								'=this.LG.LOCATION_ID' => 'ref.ID'
							),
							'join_type' => 'inner'
						)
					)
					->setSelect($select)
					->setFilter(array_merge($filter, array(
						'='.static::getLinkField() => $entityPrimary
					)))
					->setOrder($order);

				/*
				select LOCATION.*
					from b_sale_delivery2location DL
						inner join b_sale_location_group G on (G.CODE = DL.LOCATION_CODE and DL.LOCATION_TYPE = self::DB_GROUP_FLAG)
						inner join b_sale_location2location_group LG on (LG.LOCATION_GROUP_ID = G.ID)
						inner join b_sale_location LOCATION on (LG.LOCATION_ID = LOCATION.ID)
					where
						DL.DELIVERY_ID = 2;
				*/
			}
			else // entity connected by ID
			{
				$query
					->registerRuntimeField(
						'LG',
						array(
							'data_type' => '\Bitrix\Sale\Location\GroupLocation',
							'reference' => array(
								'=this.'.static::getLocationLinkField() => 'ref.'.GroupLocationTable::getLinkField(),
								'=this.'.static::getTypeField() => array('?', self::DB_GROUP_FLAG)
							),
							'join_type' => 'inner'
						)
					)
					->registerRuntimeField(
						'LOCATION',
						array(
							'data_type' => '\Bitrix\Sale\Location\Location',
							'reference' => array(
								'=this.LG.LOCATION_ID' => 'ref.ID'
							),
							'join_type' => 'inner'
						)
					)
					->setSelect($select)
					->setFilter(array_merge($filter, array(
						'='.static::getLinkField() => $entityPrimary
					)))
					->setOrder($order);

				/*
				select LOCATION.*
					from b_sale_delivery2location DL
						inner join b_sale_location2location_group LG on (LG.LOCATION_GROUP_ID = DL.LOCATION_ID and DL.LOCATION_TYPE = self::DB_GROUP_FLAG)
						inner join b_sale_location LOCATION on (LG.LOCATION_ID = LOCATION.ID)
					where
						DL.DELIVERY_ID = 2;
				*/
			}

			$sqls[] = $query->getQuery();
		}

		if(empty($sqls)) // entity is not connected, so it means "all connected"
			return false;

		return static::unionize($sqls);
	}

	/**
	 * More preferable alias for getConnectedLocationsQuery()
	 */
	public static function getConnectedLocationsSql($entityPrimary, $parameters = array(), $behaviour = array('GET_LINKED_THROUGH_GROUPS' => false))
	{
		return static::getConnectedLocationsQuery($entityPrimary, $parameters, $behaviour);
	}

	public static function getConnectedLocations($entityPrimary, $parameters = array(), $behaviour = array('GET_LINKED_THROUGH_GROUPS' => false))
	{
		$query = static::getConnectedLocationsQuery($entityPrimary, $parameters, $behaviour);

		if(!$query)
			return new DB\ArrayResult(array());

		return static::queryPage(
			$query,
			$parameters['limit'],
			$parameters['offset']);
	}

	/**
	* Returns a set of location groups that connected with a given entity.
	*
	* @param mixed $entityPrimary primary key for an entity
	* @param mixed[] $parameters for ORM getList()
	*
	* @throws ArgumentNullException
	* @throws NotImplementedException
	*
	* @return \Bitrix\Main\DB\Result list of locations
	*/
	public static function getConnectedGroups($entityPrimary, $parameters = array())
	{
		$entityPrimary = Assert::expectStringNotNull($entityPrimary, '$entityPrimary');

		if(!static::getUseGroups())
			Assert::announceNotSupported(Loc::getMessage('SALE_LOCATION_CONNECTOR_ENTITY_DOESNT_SUPPORT_GROUPS'));

		if(!is_array($parameters))
			$parameters = array();

		$parameters['runtime']['C'] = array(
			'data_type' => static::getEntity()->getFullName(),
			'reference' => array(
				'=ref.'.static::getLinkField() => array('?', $entityPrimary),
				'=ref.'.static::getTypeField() => array('?', 'G')
			),
			'join_type' => 'inner'
		);

		$parameters['runtime']['C']['reference']['=ref.'.static::getLocationLinkField()] = (static::getConnectType() == self::LINK_CODE ? 'this.CODE' : 'this.ID');

		return GroupTable::getList($parameters);
	}

	// returns list of connected entities for location with ID == $locationPrimary, with an optional filter applied 
	public static function getConnectedEntites($locationPrimary, $parameters = array())
	{
		return static::getConnectedEntitiesByCondition($locationPrimary, 'id', $parameters);
	}

	// returns list of connected entities for location with CODE == $locationPrimary, with an optional filter applied 
	public static function getConnectedEntitesByCode($locationPrimary, $parameters = array()) // getConnectedEntitiesByLocationCode
	{
		return static::getConnectedEntitiesByCondition($locationPrimary, 'code', $parameters);
	}

	// returns sql-select query that can be embeded to another sql query in 'ID in (select * from ...)' manner
	public static function getConnectedEntitiesQuery($locationPrimary, $linkType = 'id', $parameters = array()) // // getConnectedEntitiesSql
	{
		if($linkType == 'id')
			$locationPrimary = Assert::expectIntegerPositive($locationPrimary, '$locationPrimary');
		else
			$locationPrimary = Assert::expectStringNotNull($locationPrimary, '$locationPrimary');

		$useGroups = 			GroupTable::checkGroupUsage() && static::getUseGroups(); // check if we have groups in project and entity uses groups
		$useCodes = 			static::getUseCodes(); // this entity uses codes
		$groupUseCodes =		GroupLocationTable::getUseCodes(); // group entity uses codes
		
		$typeFld = 				static::getTypeField();/*LOCATION_TYPE*/
		$linkFld = 				static::getLinkField();/*DELIVERY_ID*/
		$locationLinkFld = 		static::getLocationLinkField();/*LOCATION_ID*/
		$targetPrimaryFld = 	static::getTargetEntityPrimaryField();/*ID*/
		$groupLocationLinkFld = GroupLocationTable::getLocationLinkField();/*LOCATION_ID*/
		$groupLinkFld = 		GroupLocationTable::getLinkField();/*LOCATION_GROUP_ID*/

		$seachById = $linkType == 'id';

		$dbConnection = Main\HttpApplication::getConnection();

		if(!is_array($parameters))
			$parameters = array();

		if(is_array($parameters['runtime']))
			Assert::announceNotImplemented('Sorry, runtime clause is not implemented currently.');

		$order = array();
		if(is_array($parameters['order']))
			Assert::announceNotImplemented('Sorry, order-over-union clause is not implemented currently.');

		$filter = array();
		if(is_array($parameters['filter']) && !empty($parameters['filter']))
			$filter = $parameters['filter'];

		$select = array('*');
		if(is_array($parameters['select']) && !empty($parameters['select']))
			$select = $parameters['select'];

		/*
		query example when working with delivery:

		select distinct D.* from b_sale_delivery D
			inner join b_sale_delivery2location DL on D.ID = DL.DELIVERY_ID and DL.LOCATION_TYPE = 'L'
			inner join b_sale_location L1 on L1.CODE = DL.LOCATION_ID
			inner join b_sale_location L2 on L2.ID(there will be CODE, if we search by code) = 65683 and L2.LEFT_MARGIN >= L1.LEFT_MARGIN and L2.RIGHT_MARGIN <= L1.RIGHT_MARGIN;
		*/

		$query = new Entity\Query(static::getTargetEntityName());

		$DLCondition = array(
			'=this.'.$targetPrimaryFld/*ID*/ => 'ref.'.$linkFld/*DELIVERY_ID*/
		);
		if($useGroups)
			$DLCondition['=ref.'.$typeFld/*LOCATION_TYPE*/] = array('?', self::DB_LOCATION_FLAG);

		$query
			->registerRuntimeField(
				'DL',
				array(
					'data_type' => get_called_class(),
					'reference' => $DLCondition,
					'join_type' => 'inner'
				)
			)
			->registerRuntimeField(
				'L1',
				array(
					'data_type' => '\Bitrix\Sale\Location\Location',
					'reference' => array(
						'=this.DL.'.$locationLinkFld/*LOCATION_ID*/ => 'ref.'.($useCodes ? 'CODE' : 'ID'),
					),
					'join_type' => 'inner'
				)
			)
			->registerRuntimeField(
				'L2',
				array(
					'data_type' => '\Bitrix\Sale\Location\Location',
					'reference' => array(
						'=ref.'.($seachById ? 'ID' : 'CODE') =>  array('?', $locationPrimary), // among parents we have element with ID or CODE is equal to $locationPrimary
						'>=ref.LEFT_MARGIN' => 'this.L1.LEFT_MARGIN', // and its left_margin
						'<=ref.RIGHT_MARGIN' => 'this.L1.RIGHT_MARGIN' // and right_margin fit our restrictions
					),
					'join_type' => 'inner'
				)
			)
			->setSelect($select)
			->setFilter($filter)
			->setOrder($order);

		if(!$useGroups)
		{
			// emulate "select distinct"
			$query->setGroup($select);

			return $query->getQuery();
		}
		else
		{
			$sqls = array($query->getQuery());

			$query = new Entity\Query(static::getTargetEntityName());

			/*
			query example when working with delivery:

			select D.* from b_sale_delivery D
				inner join b_sale_delivery2location DL on D.ID = DL.DELIVERY_ID and DL.LOCATION_TYPE = 'G'
				inner join b_sale_location_group G on G.CODE = DL.LOCATION_ID (if this entity uses ID, skip this join)
				inner join b_sale_grouplocation GL on GL.LOCATION_GROUP_ID = G.ID (if this entity uses ID, there will be DL.LOCATION_ID)
				inner join b_sale_location L1 on L1.ID (there will be CODE, if grouplocation entity uses CODE) = GL.LOCATION_ID
				inner join b_sale_location L2 on L2.ID (there will be CODE, if we seach by code) = 65683 and L2.LEFT_MARGIN >= L1.LEFT_MARGIN and L2.RIGHT_MARGIN <= L1.RIGHT_MARGIN;
			*/

			$query
				->registerRuntimeField(
					'DL',
					array(
						'data_type' => get_called_class(),
						'reference' => array(
							'=this.'.$targetPrimaryFld/*ID*/ => 'ref.'.$linkFld/*DELIVERY_ID*/,
							'=ref.'.$typeFld/*LOCATION_TYPE*/ => array('?', self::DB_GROUP_FLAG)
						),
						'join_type' => 'inner'
					)
				);

			if($useCodes)
			{
				$query
					->registerRuntimeField(
						'G',
						array(
							'data_type' => '\Bitrix\Sale\Location\Group',
							'reference' => array(
								'=this.DL.'.$locationLinkFld/*LOCATION_ID*/ => 'ref.CODE', //$useCodes == true here, so always CODE
							),
							'join_type' => 'inner'
						)
					);
			}

			$query
				->registerRuntimeField(
					'GL',
					array(
						'data_type' => '\Bitrix\Sale\Location\GroupLocation',
						'reference' => array(
							($useCodes ? '=this.G.ID' : '=this.DL.'.$locationLinkFld/*LOCATION_ID*/) => 'ref.'.$groupLinkFld/*LOCATION_GROUP_ID*/
						),
						'join_type' => 'inner'
					)
				)
				->registerRuntimeField(
					'L1',
					array(
						'data_type' => '\Bitrix\Sale\Location\Location',
						'reference' => array(
							'=this.GL.'.$groupLocationLinkFld/*LOCATION_ID*/ => 'ref.'.($groupUseCodes ? 'CODE' : 'ID'),
						),
						'join_type' => 'inner'
					)
				)
				->registerRuntimeField(
					'L2',
					array(
						'data_type' => '\Bitrix\Sale\Location\Location',
						'reference' => array(
							'=ref.'.($seachById ? 'ID' : 'CODE') =>  array('?', $locationPrimary),
							'>=ref.LEFT_MARGIN' => 'this.L1.LEFT_MARGIN',
							'<=ref.RIGHT_MARGIN' => 'this.L1.RIGHT_MARGIN'
						),
						'join_type' => 'inner'
					)
				)
				->setSelect($select)
				->setFilter($filter)
				->setOrder($order);

			$sqls[] = $query->getQuery();

			return static::unionize($sqls);
		}
	}

	protected static function getConnectedEntitiesByCondition($locationPrimary, $linkType = 'id', $parameters = array())
	{
		$useGroups = GroupTable::checkGroupUsage() && static::getUseGroups(); // check if we have groups in project and entity uses groups

		$sql = static::getConnectedEntitiesQuery($locationPrimary, $linkType, $parameters);
		$res = static::queryPage($sql, $parameters['limit'], $parameters['offset']);

		return $res;
	}

	protected static function getLinkedLocations($entityPrimary)
	{
		$entityPrimary = Assert::expectStringNotNull($entityPrimary, '$entityPrimary');

		$existed = array();
		$linkFld = static::getLocationLinkField();
		$typeFld = static::getTypeField();
		$res = static::getList(array('filter' => array(static::getLinkField() => $entityPrimary)));
		while($item = $res->fetch())
		{
			if(!in_array($item[static::getTypeField()], array(self::DB_GROUP_FLAG, self::DB_LOCATION_FLAG))) // strange record found. skip it
				continue;

			$existed[$item[static::getTypeField()]][$item[$linkFld]] = true;
		}

		return $existed;
	}

	/**
	 * Functions for massive check for link type
	 * 
	 * 
	 */
	public static function getLinkStatusForMultipleNodes($nodeInfo = array(), $entityPrimary, $connectors = false) // rename to: getConnectionStatusForMultipleNodes
	{
		$nodeInfo = Assert::expectArray($nodeInfo, '$nodeInfo');
		$entityPrimary = Assert::expectStringNotNull($entityPrimary, '$entityPrimary');

		$result = array();

		if(!static::checkLinkUsageAny($entityPrimary)) // if there are no links at all, connection virtually exists
		{
			foreach($nodeInfo as $node)
			{
				$result[$node['ID']] = self::LSTAT_BELOW_CONNECTOR;
			}

			return $result;
		}

		if(!is_array($connectors))
			$connectors = static::getConnectedLocationsInfo($entityPrimary);

		foreach($nodeInfo as $node)
		{
			$node = Assert::expectNotEmptyArray($node, '$nodeInfo[]');
			$node['ID'] = Assert::expectIntegerPositive($node['ID'], '$nodeInfo[][ID]');
			$node['LEFT_MARGIN'] = Assert::expectIntegerNonNegative($node['LEFT_MARGIN'], '$nodeInfo[][LEFT_MARGIN]');
			$node['RIGHT_MARGIN'] = Assert::expectIntegerPositive($node['RIGHT_MARGIN'], '$nodeInfo[][RIGHT_MARGIN]');

			$result[$node['ID']] = false;
			foreach($connectors as $connector)
			{
				if($connector['ID'] == $node['ID'])
				{
					$result[$node['ID']] = self::LSTAT_IS_CONNECTOR;
					break;
				}
				elseif($node['LEFT_MARGIN'] >= $connector['LEFT_MARGIN'] && $node['RIGHT_MARGIN'] <= $connector['RIGHT_MARGIN'])
				{
					$result[$node['ID']] = self::LSTAT_BELOW_CONNECTOR;
					break;
				}
				elseif($node['LEFT_MARGIN'] <= $connector['LEFT_MARGIN'] && $node['RIGHT_MARGIN'] >= $connector['RIGHT_MARGIN'])
				{
					$result[$node['ID']] = self::LSTAT_ABOVE_CONNECTOR;
					break;
				}
			}

			if(!$result[$node['ID']])
				$result[$node['ID']] = self::LSTAT_IN_NOT_CONNECTED_BRANCH;
		}

		return $result;
	}

	/**
	 * Check if location is connected with entity
	 * 
	 * @param $entityPrimary mixed Entity being checked
	 * @param $locationPrimary mixed Location being checked. Could be a value of ID or CODE depending on what connection type is selected (see below)
	 * @param $behaviour mixed[] A set of flags that modify function behaviour
	 * 		<li> LOCATION_LINK_TYPE string One of: ID, CODE, AUTO. 
	 * 			If ID, than match by ID is used (default, for compatibility), if CODE than match by CODE. 
	 * 			In case of AUTO the target field value depends on entity connect type.
	 * 
	 * @return boolean
	 */
	public static function checkConnectionExists($entityPrimary, $locationPrimary, array $behaviour = array('LOCATION_LINK_TYPE' => 'ID'))
	{
		$entityPrimary = Assert::expectStringNotNull($entityPrimary, '$entityPrimary');
		$locationPrimary = Assert::expectStringNotNull($locationPrimary, '$locationPrimary');

		if(!isset($behaviour['LOCATION_LINK_TYPE']))
		{
			$behaviour['LOCATION_LINK_TYPE'] = 'ID';
		}
		else
		{
			$behaviour['LOCATION_LINK_TYPE'] = Assert::expectEnumerationMember($behaviour['LOCATION_LINK_TYPE'], array('AUTO', 'ID', 'CODE'), '$behaviour[LOCATION_LINK_TYPE]');
		}

		if(!static::checkLinkUsageAny($entityPrimary)) // if there are no links at all, connection virtually exists
		{
			return true;
		}

		if($behaviour['LOCATION_LINK_TYPE'] == 'AUTO')
		{
			$field = static::getUseCodes() ? 'CODE' : 'ID';
		}
		else
		{
			$field = $behaviour['LOCATION_LINK_TYPE'];
		}

		$node = LocationTable::getList(
			array(
				'filter' => array('='.$field => $locationPrimary),
				'select' => array('ID', 'LEFT_MARGIN', 'RIGHT_MARGIN'),
				'limit' => 1
			)
		)->fetch();

		if(!intval($node['ID']))
		{
			throw new \Bitrix\Sale\Location\Tree\NodeNotFoundException(false, array('INFO' => array($field => $locationPrimary)));
		}

		$result = static::getLinkStatusForMultipleNodes(array($node), $entityPrimary);

		return $result[$node['ID']] == self::LSTAT_IS_CONNECTOR || $result[$node['ID']] == self::LSTAT_BELOW_CONNECTOR;
	}

	// a wrapper to getConnectedLocations which returns simple info about connected locations
	protected static function getConnectedLocationsInfo($entityPrimary)
	{
		$result = array();

		$res = static::getConnectedLocations($entityPrimary, array('select' => array(
				'ID' => 'ID',
				'LEFT_MARGIN' => 'LEFT_MARGIN',
				'RIGHT_MARGIN' => 'RIGHT_MARGIN',
				'LNAME' => 'NAME.NAME',
			),
			'filter' => array('NAME.LANGUAGE_ID' => LANGUAGE_ID)
		), array('GET_LINKED_THROUGH_GROUPS' => true));

		while($item = $res->fetch())
			$result[] = $item;

		return $result;
	}

	#####################################
	#### Link tracking
	#####################################

	public static function checkLinkUsageAny($entityPrimary)
	{
		$entityPrimary = Assert::expectStringNotNull($entityPrimary, '$entityPrimary');

		return static::checkLinkUsage($entityPrimary, self::DB_LOCATION_FLAG) || static::checkLinkUsage($entityPrimary, self::DB_GROUP_FLAG);
	}

	/**
	* Check if there is a link between entity and locations, optionally of a certain type (default: locations)
	*
	* @param mixed $entityPrimary primary key for an entity
	* @param string $linkType link type (G or L (default))
	*
	* @throws ArgumentNullException
	*
	* @return boolean
	*/
	public static function checkLinkUsage($entityPrimary, $linkType = self::DB_LOCATION_FLAG)
	{
		$entityPrimary = Assert::expectStringNotNull($entityPrimary, '$entityPrimary');
		$linkType = Assert::expectEnumerationMember(
			$linkType,
			array(self::DB_LOCATION_FLAG, self::DB_GROUP_FLAG),
			'$linkType'
		);

		if(!static::getUseLinkTracking())
			return true; // force to true if link tracking is off

		$useGroups = static::getUseGroups();

		if(!$useGroups && $linkType == self::DB_GROUP_FLAG)
			return false; // we know we dont use groups

		$usageFlags = static::getLinkUsageOptionValue();

		if(isset($usageFlags[$entityPrimary][$linkType]))
			return $usageFlags[$entityPrimary][$linkType];

		$strictFilter = array(
			static::getLinkField() => $entityPrimary
		);
		if($useGroups)
			$strictFilter['LOCATION_TYPE'] = self::DB_LOCATION_FLAG;

		$usageFlags[$entityPrimary][self::DB_LOCATION_FLAG] = !!static::getList(array(
			'limit' => 1,
			'filter' => $strictFilter
		))->fetch();

		if($useGroups)
		{
			$usageFlags[$entityPrimary][self::DB_GROUP_FLAG] = !!static::getList(array(
				'limit' => 1,
				'filter' => array(
					static::getLinkField() => $entityPrimary,
					'LOCATION_TYPE' => self::DB_GROUP_FLAG
				)
			))->fetch();
		}

		static::setLinkUsageOptionValue($usageFlags);

		return $usageFlags[$entityPrimary];
	}

	/**
	* Function switches link usage to "on\off" position, for a certain entity and connection type (to location or to group)
	*
	*/
	private static function setLinkUsage($entityPrimary, $linkType = self::DB_LOCATION_FLAG, $way = true)
	{
		if(!static::getUseLinkTracking())
			return;

		if(!in_array($linkType, array(self::DB_LOCATION_FLAG, self::DB_GROUP_FLAG)))
			$linkType = self::DB_LOCATION_FLAG;

		$usageFlags = static::getLinkUsageOptionValue();
		$usageFlags[$entityPrimary][$linkType] = $way;

		static::setLinkUsageOptionValue($usageFlags);
	}

	/**
	* Function re-reads link existence and set options correctly
	*
	*/
	private static function resetLinkUsage($entityPrimary)
	{
		if(!static::getUseLinkTracking())
			return;

		$useGroups = static::getUseGroups();
		$typeFld = static::getTypeField();
		$groupBy = array();
		if($useGroups)
			$groupBy[] = $typeFld;

		$res = static::getList(array(
			'filter' => array(
				static::getLinkField() => $entityPrimary
			),
			'group' => $groupBy
		));

		$hasLocationLink = false;
		$hasGroupLink = false;
		while($item = $res->fetch())
		{
			if(isset($item[$typeFld]) && $item[$typeFld] == self::DB_GROUP_FLAG)
				$hasGroupLink = true;
			else
				$hasLocationLink = true;
		}

		static::setLinkUsage($entityPrimary, self::DB_LOCATION_FLAG, $hasLocationLink);
		static::setLinkUsage($entityPrimary, self::DB_GROUP_FLAG, $hasGroupLink);
	}

	/**
	* Function returns option value (or an empty array, if option is not set)
	*
	*/
	private static function getLinkUsageOptionValue()
	{
		$usageFlagsOpt = Config\Option::get("sale", static::getLinkOptionName());
		if(!strlen($usageFlagsOpt) || !is_array($usageFlags = unserialize($usageFlagsOpt)))
			$usageFlags = array();

		return $usageFlags;
	}

	/**
	* Function sets option value
	*
	*/
	private static function setLinkUsageOptionValue($usageFlags)
	{
		Config\Option::set("sale", static::getLinkOptionName(), serialize($usageFlags), '');
	}

	private static function deleteLinkUsageOption()
	{
		Config\Option::delete("sale", array('name' => static::getLinkOptionName()));
	}

	private static function getLinkOptionName()
	{
		return 'link_option_'.static::getTableName();
	}

	#####################################
	#### Util
	#####################################

	private static function checkUpdateLinks($links)
	{
		$useCodes = static::getUseCodes();
		$useGroups = static::getUseGroups();

		$locationArgName = 	'$links['.self::DB_LOCATION_FLAG.']';
		$groupArgName = 	'$links['.self::DB_GROUP_FLAG.']';

		if(is_array($links[self::DB_LOCATION_FLAG]))
		{
			if($useCodes)
				$links[self::DB_LOCATION_FLAG] = Assert::expectArrayOfUniqueStringNotNull($links[self::DB_LOCATION_FLAG], $locationArgName);
			else
				$links[self::DB_LOCATION_FLAG] = Assert::expectArrayOfUniqueIntegerNotNull($links[self::DB_LOCATION_FLAG], $locationArgName);
		}

		if(is_array($links[self::DB_GROUP_FLAG]))
		{
			if(!$useGroups)
				Assert::announceNotSupported(Loc::getMessage('SALE_LOCATION_CONNECTOR_ENTITY_DOESNT_SUPPORT_GROUPS'));

			if($useCodes)
				$links[self::DB_GROUP_FLAG] = Assert::expectArrayOfUniqueStringNotNull($links[self::DB_GROUP_FLAG], $groupArgName);
			else
				$links[self::DB_GROUP_FLAG] = Assert::expectArrayOfUniqueIntegerNotNull($links[self::DB_GROUP_FLAG], $groupArgName);
		}

		return $links;
	}

	/**
	* A special paginator for UNION-like queries
	*
	* @param string $sql SQL select query to execute, wrapped in union
	* @param integer $limit record limit to fetch (default: 0)
	* @param integer $offset offset to start fetching records from (default: 0)
	*
	* @return Bitrix\Main\DB\Result
	*/
	protected static function queryPage($sql, $limit = 0, $offset = 0)
	{
		$artificialNav = false;
		$limit = intval($limit);
		$offset = intval($offset);

		// todo: Luke, use Bitrix\Main\DB\Paginator here

		$dbConnection = Main\HttpApplication::getConnection();

		if($limit)
		{
			if($dbConnection->getType() == 'mysql')
			{
				$sql .= ' limit '.($offset ? $offset.', ' : '').$limit;
			}
			else
			{
				$artificialNav = true;
			}
		}

		$res = $dbConnection->query($sql);

		if($artificialNav)
		{
			$result = array();

			$i = -1;
			while($item = $res->fetch())
			{
				$i++;

				if($i < $offset)
					continue;

				if($i >= $offset + $limit)
					break;

				$result[] = $item;
			}

			return new DB\ArrayResult($result);
		}
		else
			return $res;
	}

	/**
	* Wraps a set of sqls to a union
	*
	* @param string[] $sqls a set of SQL select queries to wrap
	*
	* @return string
	*/
	public static function unionize($sqls)
	{
		if(!is_array($sqls))
			return $sqls;

		if(count($sqls) == 1)
			return array_shift($sqls);

		//foreach($sqls as &$sql)
		//	$sql = '('.$sql.')';

		unset($sql);

		return implode(' union ', $sqls);
	}

	// function accepts denormalized list of location IDs on input, then makes it normal and returns list of IDs or CODEs, depending on current entity settings
	public static function normalizeLocationList($denormalizedList = array())
	{
		if(empty($denormalizedList))
			return array();

		$useCodes = static::getUseCodes();

		$denormalizedList = array_flip($denormalizedList);

		$query = new Entity\Query('Bitrix\Sale\Location\Location');

		$query->setSelect(array(
			'PARENT_ID',
			'ID',
			'CODE',
			'LNAME' => 'NAME.NAME' // tmp
		));

		$query->setFilter(array('NAME.LANGUAGE_ID' => LANGUAGE_ID)); // tmp
		$query->setOrder(array('LEFT_MARGIN' => 'desc')); // important
		
		$res = $query->exec();

		// make table of children count for each node

		$relations = array();
		$selected = array();
		while($item = $res->fetch())
		{
			if(!isset($relations[$item['ID']]['name'])) // tmp
				$relations[$item['ID']]['name'] = $item['LNAME'];

			if(!isset($relations[$item['ID']]['total']))
				$relations[$item['ID']]['total'] = 0;

			if(!isset($relations[$item['ID']]['selected']))
				$relations[$item['ID']]['selected'] = 0;

			///
			if($item['PARENT_ID'] != 0)
			{
				if(!isset($relations[$item['PARENT_ID']]['total']))
					$relations[$item['PARENT_ID']]['total'] = 0;
				if(!isset($relations[$item['PARENT_ID']]['selected']))
					$relations[$item['PARENT_ID']]['selected'] = 0;
			}

			$relations[$item['PARENT_ID']]['total'] += (1 + $relations[$item['ID']]['total']);

			if(isset($denormalizedList[$item['ID']]))
			{
				$relations[$item['PARENT_ID']]['selected'] += (1 + $relations[$item['ID']]['selected']);
				$selected[$item['ID']] = $item;
			}
		}
		
		// now make up list of nodes to remove
		$removeItems = array();
		$removeChildrenOf = array();
		foreach($relations as $id => $rel)
		{
			if($rel['total'] > 0) // item has children
			{
				if($rel['total'] == $rel['selected']) // selected N of N of all children, remove all those children
				{
					if(isset($selected[$id])) // item should be selected itself to remove its children
						$removeChildrenOf[$id] = true;
				}
				elseif($rel['selected'] > 0) // selected M of N of children, where M > 0, so remove item itself
					$removeItems[$id] = true;
			}
		}

		$normalized = array();
		foreach($selected as $id => $item)
		{
			if(!($item['PARENT_ID'] && $removeChildrenOf[$item['PARENT_ID']]) && !$removeItems[$item['ID']])
				$normalized[] = $item[$useCodes ? 'CODE' : 'ID'];
		}

		return $normalized;
	}
}