Как реализовать резервирование товаров — различия между версиями

Материал из Umicms
Перейти к:навигация, поиск
Строка 31: Строка 31:
 
=== Резервирование при работе с корзиной ===
 
=== Резервирование при работе с корзиной ===
 
   
 
   
К сожалению, стандартный метод emarket basket() совершенно не подойдет для решения функциональности, поэтому придется создаваться кастомный, пример ниже:
+
К сожалению, стандартный метод emarket basket() совершенно не подойдет для решения функциональности, поэтому придется создаваться кастомный, пример ниже.
 +
Чтобы его применять нужно будет поменять формы добавления товара в корзину и формы для изменения количества и удаления позиции из корзины.
  
 
<source lang="php">
 
<source lang="php">

Версия 15:10, 14 апреля 2014

Актуально для версии 2.9.6

Задача

Допустим у Вас есть ряд товаров, количество которых ограничено и Вам необходимо создать для них резервирование, то есть, нужно реализовать примерно следующие возможности:

  • Включить резервирование товара;
  • Задать количество зарезервированного товара;
  • Указать дату, до которой этот товар должен быть оплачен;
  • Сделать, чтобы было невозможно совершить заказов, в котором количество товаров будет превышать резерв;
  • Все действия с корзиной перед покупкой (изменения количества, удаление и добавление позиций) должны сразу отражаться на количество резерва;
  • Если заказ с зарезервированным товаром не был переведен в статус "Готов" до даты, заданной в 3 пункте, то товарная позиция должна быть удалена из заказа;
  • Если заказ был отмене или отклонен, то у зарезервированного товара должно быть восполнено его количество.

Все эти задачи можно решить только кастомно, ниже даются примеры.

Решение

Управление резервирование у товара

Сначала создадим поля, в которых будут храниться настройки резервирования для товаров. Для этого, создадим в типе данных "объект каталога" следующие поля:

Reservation fields.png

Теперь у товаров появились настройки:

Reservation page.png

Резервирование при работе с корзиной

К сожалению, стандартный метод emarket basket() совершенно не подойдет для решения функциональности, поэтому придется создаваться кастомный, пример ниже. Чтобы его применять нужно будет поменять формы добавления товара в корзину и формы для изменения количества и удаления позиции из корзины.

/*
*  Замена стандартного метода emarket basket()
*
*  Отличия от ориганала:
*  1) в зависимости от $mode дополнительно вызываются:
*     checkAmountAndPut()  для 'put'
*     checkAmountAndRemove()  для 'remove'
*     checkAmountAndRemoveAll()  для 'remove_all'
*  2) нет тройного вызова метода refresh();
*/
public function smartBasket($mode = false, $itemType = false, $itemId = false){			
	$mode = $mode ? $mode : getRequest('param0');
	$order = emarket::getBasketOrder(!in_array($mode, array('put', 'remove')));
	$itemType = $itemType ? $itemType : getRequest('param1');
	$itemId = (int) ($itemId ? $itemId : getRequest('param2'));
	$amount = (int) getRequest('amount');
	$options = getRequest('options');

	if($mode == 'put') {
		$newElement = false;
		if ($itemType == 'element') {
			$orderItem = emarket::getBasketItem($itemId, false);
			if (!$orderItem) {
				$orderItem = emarket::getBasketItem($itemId);
				$newElement = true;
			}
		} else {
			$orderItem = order::getItem($itemId);
		}

		if (!$orderItem) {
			throw new publicException("Order item is not defined");
		}

		if(is_array($options)) {
			if($itemType != 'element') {
				throw new publicException("Put basket method required element id of optionedOrderItem");
			}

			// Get all orderItems
			$orderItems = order::getItems();

			foreach($orderItems as $tOrderItem) {
				if (!$tOrderItem instanceOf optionedOrderItem) {
					$itemOptions = null;
					$tOrderItem = null;
					continue;
				}

				$itemOptions = $tOrderItem->getOptions();

				if(sizeof($itemOptions) != sizeof($options)) {
					$itemOptions = null;
					$tOrderItem = null;
					continue;
				}

				if($tOrderItem->getItemElement()->id != $orderItem->getItemElement()->id) {
					$itemOptions = null;
					$tOrderItem = null;
					continue;
				}

				// Compare each tOrderItem with options list
				foreach($options as $optionName => $optionId) {
					$itemOption = getArrayKey($itemOptions, $optionName);

					if(getArrayKey($itemOption, 'option-id') != $optionId) {
						$tOrderItem = null;
						continue 2;		// If does not match, create new item using options specified
					}
				}

				break;	// If matches, stop loop and continue to amount change
			}

			if(!isset($tOrderItem) || is_null($tOrderItem)) {
				$tOrderItem = orderItem::create($itemId);
				$order->appendItem($tOrderItem);
				if ($newElement) {
					$orderItem->remove();
				}
			}

			if($tOrderItem instanceof optionedOrderItem) {
				foreach($options as $optionName => $optionId) {
					if($optionId) {
						$tOrderItem->appendOption($optionName, $optionId);
					} else {
						$tOrderItem->removeOption($optionName);
					}
				}
			}

			if($tOrderItem) {
				$orderItem = $tOrderItem;
			}
		}
		self::checkAmountAndPut($itemId, $amount, $orderItem);				

		if($itemType == 'element') {
			$order->appendItem($orderItem);
		}
		$order->refresh();
	}

	if($mode == 'remove') {
		
		$orderItem = ($itemType == 'element') ? emarket::getBasketItem($itemId, false) : orderItem::get($itemId);
		self::checkAmountAndRemove($itemId, $orderItem, $order, $itemType);
	}

	if ($mode == 'remove_all') {
		self::checkAmountAndRemoveAll($order);
	}

	$referer = getServer('HTTP_REFERER');
	$noRedirect = getRequest('no-redirect');

	if($redirectUri = getRequest('redirect-uri')) {
		$this->redirect($redirectUri);
	} else if (!defined('VIA_HTTP_SCHEME') && !$noRedirect && $referer) {
		$current = $_SERVER['REQUEST_URI'];
		if(substr($referer, -strlen($current)) == $current) {
			if($itemType == 'element') {
				$referer = umiHierarchy::getInstance()->getPathById($itemId);
			} else {
				$referer = "/";
			}
		}
		$this->redirect($referer);
	}
	return $this->order($order->getId());
}

Код метода нужно поместить в файл /classes/modules/emarket/__custom.php и не забудьте про пермишены.

Метод работает практически также, как и стандартный, поэтому подробно его описывать не имеет смысла. В зависимости от $mode будет вызван тот или иной служебный метод. Код служебных методов дан ниже, его нужно добавить в тот же файл.

/*
*  Вызывается при полном очищении корзины, то есть:
*  /udata/emarket/smartBasket/remove_all
*  Проверяет товары, связанные с итемами,
*  если у товара включена резервация, то
*  его "запас" восполняется, все итемы убиваются.
*/
private function checkAmountAndRemoveAll($order){
	$hierarchy = umiHierarchy::getInstance();
	$items = $order->getItems();
	if(count($items) == 0){
		return;
	}
	foreach($items as $orderItem){
		$element_id = intval($orderItem->getItemElement()->getId());
		$element = $hierarchy->getElement($element_id, true, true);
		if(intval($element->getValue('reservation_on')) == 0){
			$order->removeItem($orderItem);
		}else{
			$reserved_amount = intval($element->getValue('reservation_amount'));
			$puted_amount = intval($orderItem->getAmount());
			$element->setValue('reservation_amount', $reserved_amount + $puted_amount);
			$element->commit();
			$order->removeItem($orderItem);
		}
		unset($element_id, $element);
	}
	$order->refresh();
}

/*
*  Вызывается при удалении позиции или товара из корзины, то есть:
*  /udata/emarket/smartBasket/remove/item/1045
*  или
*  /udata/emarket/smartBasket/remove/element/69
*  Проверяет включена ли резервация у связанного товара, если включена,
*  то его запас восполняется, итемы убиваются.
*/
private function checkAmountAndRemove($itemId, $orderItem, $order, $itemType){
	if($itemType == 'element'){
		$hierarchy = umiHierarchy::getInstance();
		$element = $hierarchy->getElement($itemId, true, true);
	}else{
		$element = $orderItem->getItemElement();
	}
	if(intval($element->getValue('reservation_on')) == 0){
		if($orderItem instanceof orderItem) {
			$order->removeItem($orderItem);
			$order->refresh();			
		}
	}else{
		$reservation_amount = intval($element->getValue('reservation_amount'));
		$basket_amount = intval($orderItem->getAmount());
		$element->setValue('reservation_amount', $reservation_amount + $basket_amount);
		$element->commit();
		$order->removeItem($orderItem);
		$order->refresh();
	}
}

/*
*  Вызывается при добавлении товара или позиции в корзину, с учетом $amount
*  Примеры вызовов:
*  /udata/emarket/smartBasket/put/element/69
*  /udata/emarket/smartBasket/put/element/69?amount=10
*  /udata/emarket/smartBasket/put/item/1045
*  /udata/emarket/smartBasket/put/item/1045?amount=10
*  Если у товаров включена резервация, то вызывается метод checkReservedAmount()
*/
private function checkAmountAndPut($itemId, $amount, $orderItem){
	$basket_amount = intval($orderItem->getAmount());
	$hierarchy = umiHierarchy::getInstance();
	$element = $hierarchy->getElement($itemId, true, true);
	if(intval($element->getValue('reservation_on')) == 0){
		$amount = $amount ? $amount : ($basket_amount + 1);
		$orderItem->setAmount($amount ? $amount : 1);
		$orderItem->refresh();
	}else{
		self::checkReservedAmount($element, $basket_amount, $amount, $orderItem);
	}
}

/*
*  Проверяется текущее содержание корзины, переданный $amount 
*  и зарезервированный $amount товара.
*  В зависимости от проверки вызывются методы reduceReservedAmount() или increaseReservedAmount()
*/
private function checkReservedAmount($element, $basket_amount, $amount, $orderItem){
	$reserved_amount = intval($element->getValue('reservation_amount'));
	if($reserved_amount == 0){
		throw new publicException('out of stock');
	}else{
		if($amount > $basket_amount && $amount !== 0){
			if($reserved_amount < $amount){
				throw new publicException('out of stock');
			}else{
				self::reduceReservedAmount($reserved_amount, $amount - $basket_amount, $element, $orderItem);
			}
		}elseif($amount == 0){
			self::reduceReservedAmount($reserved_amount, 1, $element, $orderItem);
		}else{
			self::increaseReservedAmount($reserved_amount, $basket_amount-$amount, $element, $orderItem);
		}
	}
}

/*
*  Уменьшает зарезервированное количество и увеличивает $amount для товарной позиции
*/
private function reduceReservedAmount($reserved_amount, $amount, $element, $orderItem){
	$element->setValue('reservation_amount', $reserved_amount - $amount);
	$element->commit();
	$orderItem->setAmount($orderItem->getAmount() + $amount);
	$orderItem->refresh();
	$orderItem->commit();
	return true;
}

/*
*  Увеличивает зарезервированное количество и уменьшает $amount для товарной позиции
*/
private function increaseReservedAmount($reserved_amount, $amount, $element, $orderItem){
	$element->setValue('reservation_amount', $reserved_amount + $amount);
	$element->commit();
	$orderItem->setAmount($orderItem->getAmount() - $amount);
	$orderItem->refresh();
	$orderItem->commit();
	return true;
}

Краткие описания методов даны в комментариях. Эти служебные методы реализуют работу с резервацией, если у товара резервация отключена, то с ним никаких дополнительных действий не происходит.

Установка времени жизни у позиций зарезервированных товаров

Для реализации данной задачи нам поможем событийная модель UMI.CMS, а конкретнее событие order-status-changed, которое вызывается при оформлении каждого заказа. Нам нужно повесить на событие обработчик, который будет проверять есть ли в заказе зарезервированные товары, если они есть то устанавливать время жизни товарных позиций с помощью класса umiObjectsExpiration.

Добавим в файл /classes/modules/emarket/custom_events.php следующие строки:

//создание времени жизни для товарных позиций заказов, содержащих зарезерированные товары
new umiEventListener('order-status-changed', 'emarket', 'makeExpiration');

А в кастом того же модуля добавим код обработчика:

/*
*  Вызывается, когда заказ меняет статус с пустого на "Редактируется", это происходит со всеми
*  заказами в процессе оформления.
*  Если в заказе есть зарезервированные товары, то товарной позиции устанавливается время жизни
*  время жизни = время, до которого зарезервирован товар - текущее время
*/ 
public function makeExpiration(iUmiEventPoint $eventPoint){		
	$new_status = $eventPoint->getParam('new-status-id');
	$old_status = $eventPoint->getParam('old-status-id');
	$mode = $eventPoint->getMode();
	if($mode == 'after' && $old_status == false && $new_status == order::getStatusByCode('waiting')){

		$order = $eventPoint->getRef('order');
		$items = $order->getItems();
		if(count($items) == 0){
			return;
		}
		$hierarchy = umiHierarchy::getInstance();
		$reserved_items = array();
		foreach($items as $item){
			$element_id = intval($item->getItemElement()->getId());
			$element = $hierarchy->getElement($element_id, true, true);
			if(intval($element->getValue('reservation_on')) == 1){
				$reserved_items[] = $item;
			}
			unset($element_id, $element);
		}
		if(count($reserved_items) == 0){
			return;
		}else{
			$object_expire = umiObjectsExpiration::getInstance();
			foreach($reserved_items as $reserved_item){
				$item_id = $reserved_item->getId();
				$element = $hierarchy->getElement($reserved_item->getItemElement()->getId());
				$expire_time = $element->getValue('reservation_time')->getDateTimeStamp();
				$umi_date = new umiDate;
				$current_time = $umi_date->getCurrentTimeStamp();
				if($current_time > $expire_time){
					break;
				}else{
					$expiration_time = $expire_time - $current_time;
					$object_expire->add($item_id, $expiration_time);
				}
			}
			return;
		}
	}else{
		return;
	}
}