old_amount; } /** * Геттер для приватного свойства $old_deal */ public function getOld_deal() { return $this->old_deal; } /** * Возвращает ссылку на объект модели * * @return Requests */ public static function model($className = __CLASS__) { return parent::model($className); } /** * Определяет имя таблицы, в которой хранятся запросы * * @return string */ public function tableName() { return 'requests'; } /** * Определяет правила валидации входящих данных для определения значения атрибутов модели. * * @return array */ public function rules() { return array( array( 'users_id, plantations_id, types_id, sorts_id, boxes_amount, exp_time, exp_date, status, uniq_number, aport_delivery, order_id', 'required' ), array( 'order_id, size_mask, submarks_id', 'numerical', //'integerOnly' => true ), /*array ( 'size_50, size_60, size_70, size_80, size_90, size_100, size_100p, size_std, size_fncy, size_sel', 'numerical' ),*/ array( 'users_id, plantations_id, types_id, sorts_id, boxes_amount, status, uniq_number, order_type', 'length', 'max' => 15 ), array( 'update_date', 'safe' ), // The following rule is used by search(). // Please remove those attributes that should not be searched. array( 'id, users_id, plantations_id, types_id, sorts_id, boxes_amount, exp_time, exp_date, status, create_date, update_date, uniq_number, aport_delivery, order_type, order_id, size_50, size_60, size_70, size_80, size_90, size_100, size_100p, size_std, size_fncy, size_sel, size_mask, submarks_id', 'safe', 'on' => 'search' ) ); } /** * Определяет связи с другими моделями в системе * * @return array */ public function relations() { return array( 'order' => array( self::BELONGS_TO, 'Orders', 'order_id' ), 'sorts' => array( // связь с моделью {@link Sizes} self::BELONGS_TO, 'Sorts', 'sorts_id' ), 'types' => array( // связь с моделью {@link Types} self::BELONGS_TO, 'Types', 'types_id' ), 'plantation' => array( // связь с моделью {@link Plantation} self::BELONGS_TO, 'Plantations', 'plantations_id' ), 'sizemask' => array( // связь с моделью {@link Sizes} self::BELONGS_TO, 'Sizes', 'size_mask' ), 'sumboxes' => array( self::STAT, 'Requests', 'boxes_amount', //'select' => ' SUM(boxes_amount) ' ), 'requests_stats' => array( // связь с моделью {@link RequestsStats} self::BELONGS_TO, 'RequestsStats', 'plantations_id, types_id, sorts_id', ), 'users' => array( // связь с моделью {@link RequestsStats} self::BELONGS_TO, 'Users', 'users_id', ), 'agents' => array( // связь с моделью {@link RequestsStats} self::BELONGS_TO, 'Users', 'agent_id', ), 'submarks' => array( // связь с моделью {@link RequestsStats} self::BELONGS_TO, 'Submarks', 'submarks_id', ), ); } /** * Определяет имена атрибутов модели * * @return array */ public function attributeLabels() { return array( 'id' => 'ID', 'users_id' => Yii::t('tableItems', 'Users'), 'plantations_id' => Yii::t('tableItems', 'Plantation'), 'farm_name' => Yii::t('tableItems', 'Plantation'), 'plantation.name' => Yii::t('tableItems', 'Plantation'), 'types_id' => Yii::t('tableItems', 'Type'), 'sorts_id' => Yii::t('tableItems', 'Sort'), 'sorts.name' => Yii::t('tableItems', 'Sort'), 'sort_name' => Yii::t('tableItems', 'Sort'), 'boxes_amount' => Yii::t('tableItems', 'HB'), 'exp_time' => Yii::t('requests', 'Time to exp'), 'exp_date' => Yii::t('requests', 'Exp Date'), 'status' => Yii::t('tableItems', 'Status'), 'create_date' => Yii::t('tableItems', 'Create Date'), 'update_date' => Yii::t('tableItems', 'Update Date'), 'uniq_number' => Yii::t('requests', 'Offer id'), 'aport_delivery' => Yii::t('requests', 'Aport Delivery'), 'order_type' => Yii::t('requests', 'Order Type'), 'order_id' => Yii::t('requests', 'Order Group'), 'size_50' => Yii::t('tableItems', '50'), 'size_60' => Yii::t('tableItems', '60'), 'size_70' => Yii::t('tableItems', '70'), 'size_80' => Yii::t('tableItems', '80'), 'size_90' => Yii::t('tableItems', '90'), 'size_100' => Yii::t('tableItems', '100'), 'size_100p' => Yii::t('tableItems', '100+'), 'size_std' => Yii::t('tableItems', 'Std'), 'size_fncy' => Yii::t('tableItems', 'Fncy'), 'size_sel' => Yii::t('tableItems', 'Sel'), 'size_mask' => Yii::t('tableItems', 'Size Mask'), 'submarks_id' => Yii::t('requests', 'Submark'), ); } /** * Организует поиск и возвращает список объектов моделей, соответствующих критериям поиска. * * @return CActiveDataProvider */ public function search($with = array(), $order = '') { $criteria = new CDbCriteria(); $criteria->compare('id', $this->id); $criteria->compare('users_id', $this->users_id); $criteria->compare('plantations_id', $this->plantations_id, false); $criteria->compare('types_id', $this->types_id, false); $criteria->compare('sorts_id', $this->sorts_id, false); $criteria->compare('boxes_amount', $this->boxes_amount); $criteria->compare('exp_time', $this->exp_time); $criteria->compare('exp_date', $this->exp_date); if (is_array( $this->status)) $criteria->addInCondition('t.status', $this->status); else $criteria->addSearchCondition('t.status', $this->status); $criteria->compare('create_date', $this->create_date, true); $criteria->compare('update_date', $this->update_date, true); $criteria->compare('uniq_number', $this->uniq_number, true); $criteria->compare('aport_delivery', $this->aport_delivery, true); $criteria->compare('order_type', $this->order_type); $criteria->compare('order_id', $this->order_id); $criteria->compare('size_50', $this->size_50); $criteria->compare('size_60', $this->size_60); $criteria->compare('size_70', $this->size_70); $criteria->compare('size_80', $this->size_80); $criteria->compare('size_90', $this->size_90); $criteria->compare('size_100', $this->size_100); $criteria->compare('size_100p', $this->size_100p); $criteria->compare('size_mask', $this->size_mask); $criteria->compare('submarks_id', $this->submarks_id); if (!empty($order)) { $criteria->order = $order; } $aWith = $this->relations(); foreach ($with as $condition) { if (in_array($condition, $aWith)) { $criteria->with[$condition] = array( 'select' => 'name' ); } } if ($this->hideCompetitors) { // Скрыть конкурентов (не выводить заявки от той роли, что и моя), но мои показать if (Yii::app()->user->role == Users::ROLE_BUYER) { $criteria->addCondition('(`order_type`="' . self::TYPE_SELLER. '" OR `users_id`=' . Yii::app()->user->id . ')', 'AND'); } elseif (Yii::app()->user->role == Users::ROLE_SELLER) { $criteria->addCondition('(`order_type`="' . self::TYPE_BUYER. '" OR `users_id`=' . Yii::app()->user->id . ')', 'AND'); } } if ($this->onlyTrusted) { $criteria->with['users'] = array( 'join' => 'LEFT JOIN relations r1 ON users.id = r1.u_1_id LEFT JOIN relations r2 ON users.id = r2.u_2_id', 'condition' => "(r1.type = 'b2s' AND r1.status = 'on' AND r1.u_2_id =" . Yii::app()->user->id . ") OR (r2.type = 'b2s' AND r2.status = 'on' AND r2.u_1_id =" . Yii::app()->user->id . ")" ); } $criteria->limit = $this->limit; $criteria->offset = $this->offset; return new CActiveDataProvider(get_class($this), array( 'criteria' => $criteria, 'pagination' => criteriaToPaginator::convert($criteria) )); } /** * Список полей, которые нужно привести к/из UTC * afterFind - при выводе все timestamp, а также некоторые time * преобразуются из UTC (в которой хранятся), к пользовательской timezone * beforeSave - перед сохранением некоторые time преобразуются из * пользовательской timezone в UTC * по-умолчанию поля типа time закоментированы * @return array */ public function convertDateTime() { return array( 'afterFind' => array( //'exp_time', 'exp_date', 'create_date', 'update_date', ), 'beforeSave' => array( //'exp_time', ), ); } /** * Возвращает список плантаций для текущего пользователя * * @return array */ public function getPlantationsList() { $oPlantation = new Plantations(); return CHtml::listData($oPlantation->getPlantationsForUser(Yii::app()->user->id)->getData(), 'id', 'name'); } /** * Возвращает список заказов текущего пользователя * * @return array */ public function getOrdersList() { $oOrders = new Orders(); return CHtml::listData($oOrders->getOrdersForUser(Yii::app()->user->id)->getData(), 'id', 'name'); } /** * Возвращает список типов цветов по указанной плантации * * @param integer $iPlantation Идентификатор плантации * @return array */ public function getTypesListByPlantation($iPlantation = -1) { if (empty($iPlantation) or $iPlantation == -1) return array(); return PriceList::model()->getTypeForPlantation($iPlantation); } /** * Строит список сортов по идентификатору плантации и типу * * @param integer $iType Тип, для которого нужно построить список * @param integer $iPlantation Идентификатор плантации * @return array */ public function getSortsListByTypesAndPlantation($iType, $iPlantation) { $aReturn = PriceList::model()->getSortForPlantation($iPlantation, $iType); if (!empty($aReturn)) return $aReturn['names']; else return array(); } /** * Метод вызывается перед валидацией данных и подготавливает данные запроса к валидации * * @param CModelEvent $oEvent */ public function onBeforeValidate($oEvent) { if (!isset($this->order_id)) { throw new CHttpException(400, 'Invalid request. Please do not repeat this request again.'); } $oOrder = new Orders(); $aOrder = $oOrder->findAllByPk($this->order_id); if (count($aOrder) != 1) { throw new CHttpException(400, 'Invalid request. Please do not repeat this request again.'); } $oExpOrderDate = DateTime::createFromFormat(MYSQL_DATETIME_FORMAT, $aOrder[0]->exp_date); // рассчитываем протухание $oExpReqDate = new DateTime(); $aExpTime = explode(':', $this->exp_time); if (!isset($aExpTime[1])) { $aExpTime[1] = 0; // можем ввести только часы } $oExpReqDate = $oExpReqDate->add(new DateInterval('PT' . $aExpTime[0] . 'H' . $aExpTime[1] . 'M')); if ($oExpReqDate > $oExpOrderDate) { $oExpReqDate = $oExpOrderDate; } $this->exp_date = $oExpReqDate->format(MYSQL_DATETIME_FORMAT); $this->aport_delivery = $aOrder[0]->air_date; $this->uniq_number = $this->generateUniqNumber(); if (!($this->size_50 instanceof Money)) $this->size_50 = new Money($this->size_50, Currency::USD_CURRENCY, false); if (!($this->size_60 instanceof Money)) $this->size_60 = new Money($this->size_60, Currency::USD_CURRENCY, false); if (!($this->size_70 instanceof Money)) $this->size_70 = new Money($this->size_70, Currency::USD_CURRENCY, false); if (!($this->size_80 instanceof Money)) $this->size_80 = new Money($this->size_80, Currency::USD_CURRENCY, false); if (!($this->size_90 instanceof Money)) $this->size_90 = new Money($this->size_90, Currency::USD_CURRENCY, false); if (!($this->size_100 instanceof Money)) $this->size_100 = new Money($this->size_100, Currency::USD_CURRENCY, false); if (!($this->size_100p instanceof Money)) $this->size_100p = new Money($this->size_100p, Currency::USD_CURRENCY, false); return parent::onBeforeValidate($oEvent); } /** * Сохраняет запрос и отправляет (если необходимо) информацию о нем в очередь. * Внимание! Выполнение метода должно проходить в рамках транзакции!!! * * Необходимость: * - запрос доступен для покупки * - запрос был от продавца, * - в нем еще есть ящики для покупки * - если заявка новая - для нее будет поставлена задача на протухание */ public function saveAndSendToQueue() { $bReturn = false; $q = new QueueTask(); $isNew = false; if ($this->getIsNewRecord()) { $isNew = true; } if (!($id = parent::save())) { $bReturn = false; } else { if ($isNew) { $date = new DateTime($this->exp_date); if ($q->add('ProcessingRequestExpired', array( $this->id ), $date->getTimestamp(), //date(MYSQL_DATETIME_FORMAT, $this->exp_date), QueueTask::LEVEL_HIGHT)) { $bReturn = true; } } if ($this->status === self::STATUS_ON_MARKET and $this->boxes_amount > 0 and $this->order_type == self::TYPE_SELLER) { if ($q->add('ProcessingDeal', array( $this->id ), time(), QueueTask::LEVEL_IMMEDIATE)) { $bReturn = true; } else { $bReturn = false; } } else { $bReturn = true; } } unset($q); return $bReturn; } /** * Возвращает тип запроса (по сути - кто его сделал) * * @param string $sUserRole Роль пользователя * @return string * @exception Для ролей, отличных от покупателя или продавца будет брошено исключение */ public function getOrderType($sUserRole) { if ($sUserRole == Users::ROLE_BUYER) { return self::TYPE_BUYER; } elseif ($sUserRole == Users::ROLE_SELLER) { return self::TYPE_SELLER; } else { throw new CHttpException(400, 'Invalid request. Please do not repeat this request again.'); } } /** * Генерирует уникальный идентификатор запроса * @link http://redmine.biflorica.com/projects/bif/wiki/Идентификатор_сделки * * @return string */ private function generateUniqNumber() { $oSequence = new Sequence(); return $oSequence->getUniqNumber(Yii::app()->user->role); } /** * Возвращает true, если заявка выполнена, иначе - false * * @return boolean */ public function isCompleted() { return ($this->boxes_amount == 0); } /** * Выставляет или снимает (в зависимости от текущего статуса) заявку на маркет. * Если заявка просрочена или уже куплена будет возвращено false * * @return boolean */ public function moveToOrFromMarketPlace() { if ($this->status !== self::STATUS_OFF_MARKET and $this->status !== self::STATUS_ON_MARKET) { return false; } $order = Orders::model()->findByPk($this->order_id); if (is_null($order)) { return false; } if ($this->status == self::STATUS_OFF_MARKET) { $this->status = self::STATUS_ON_MARKET; $iDeltaBoxes = array( $this->boxes_amount, -$this->boxes_amount ); } elseif ($this->status == self::STATUS_ON_MARKET) { $this->status = self::STATUS_OFF_MARKET; $iDeltaBoxes = array( -$this->boxes_amount, $this->boxes_amount ); } $bReturn = $this->saveAndSendToQueue(); if ($bReturn) { $order->changeCountMarketRequests($iDeltaBoxes[0], $iDeltaBoxes[1]); } return $bReturn; } /** * Клонирует объект * * @param Requests $oFrom Объект, от которого происходит клонирование */ public function cloneFrom(Requests $oFrom) { $aAtributes = $oFrom->getAttributes(); if ($aAtributes['boxes_amount']==0) { $aAtributes['boxes_amount'] = $aAtributes['boxes_deal']; $aAtributes['boxes_deal'] = 0; } $aAtributes['uniq_number'] = $this->generateUniqNumber(); $aAtributes['update_date'] = null; $aAtributes['status'] = self::STATUS_OFF_MARKET; unset($aAtributes['create_date']); unset($aAtributes['id']); $this->setAttributes($aAtributes, false); return $this->saveAndSendToQueue(); } /** * Добавляет коробки обратно в заявку (после отмены сделки) * * @param integer $boxes_amount количество коробок * @param array $dealData Массив с исходным цена для сделки. Пример array('size_50'=>'1.56', и т.д. для всех размеров) */ public function addCanceledBoxesFromDeal($boxes_amount, $dealData) { $currentTime = new DateTime("now"); // Проверим изменились ли исходные данные заявки $isReqChanged = false; foreach (FlowersSizes::getAllSize() as $size) { $size = 'size_' . $size; if (!empty($this->$size) AND !empty($dealData->$size) AND $this->$size != $dealData->$size) { $isReqChanged = true; break; } } if ($isReqChanged) { // исходная заявка изменилась // Значит создаём новую заявку $newReq = clone $this; $newReq->boxes_amount = $this->boxes_amount + $boxes_amount; $newReq->boxes_deal = $newReq->boxes_deal - $boxes_amount; $newReq->status = (($newReq->order_type == Requests::TYPE_SELLER AND Yii::app()->user->role == Users::ROLE_BUYER) OR ($newReq->order_type == Requests::TYPE_BUYER AND Yii::app()->user->role == Users::ROLE_SELLER)) ? Requests::STATUS_ON_MARKET : Requests::STATUS_OFF_MARKET; $newReq->create_date = $currentTime->format(MYSQL_DATETIME_FORMAT); foreach (FlowersSizes::getAllSize() as $size) { $newReq->{'size_' . $size} = $dealData->{'size_' . $size}; } $newReq->save(); } else { // исходная заявка не изменилась // Значит просто добавим ящики $this->boxes_amount = $this->boxes_amount + $boxes_amount; $this->boxes_deal = $this->boxes_deal - $boxes_amount; $this->status = (($this->order_type == Requests::TYPE_SELLER AND Yii::app()->user->role == Users::ROLE_BUYER) OR ($this->order_type == Requests::TYPE_BUYER AND Yii::app()->user->role == Users::ROLE_SELLER)) ? Requests::STATUS_ON_MARKET : Requests::STATUS_OFF_MARKET; $this->update_date = $currentTime->format(MYSQL_DATETIME_FORMAT); $this->save(); } } /** * Вызывается перед сохранение данных, которые и приводятся к правильному виду * * @todo Вынести проверку заказа в валидацию * @return boolean */ public function beforeSave() { $this->size_50 = $this->size_50->getValue(); $this->size_60 = $this->size_60->getValue(); $this->size_70 = $this->size_70->getValue(); $this->size_80 = $this->size_80->getValue(); $this->size_90 = $this->size_90->getValue(); $this->size_100 = $this->size_100->getValue(); $this->size_100p = $this->size_100p->getValue(); $this->size_mask = $this->countSizeMask(); // Проверка на то, что заявка попадает в заказ того пользователя, который с ней работает if (!Orders::model()->isBelongsToUser($this->order_id, $this->users_id)) { $this->addError('order_id', 'Access denied'); return false; } // пересчитать кол-во ящиков для всего заказа $oOrder = Orders::model()->findByPk($this->order_id); /* @var $oOrder Orders */ if (!$this->getIsNewRecord()) { // изменение существующей заявки Yii::app()->getComponent('ChangeBoxesCountHelper')->prepareBoxesChange($this); } else { // создание новой заявки if ($this->status == self::STATUS_ON_MARKET) { $result = $oOrder->changeCountMarketRequests($this->boxes_amount, null); if (!$result) return false; } elseif ($this->status == self::STATUS_OFF_MARKET) { $result = $oOrder->changeCountMarketRequests(null, $this->boxes_amount); if (!$result) return false; } } return parent::beforeSave(); } /** * Вызывается перед удалением заявки. * Используется для изменения кол-ва ящиков в заказе * */ public function beforeDelete() { parent::beforeDelete(); $this->boxes_amount = 0; $this->boxes_deal = 0; Yii::app()->getComponent('ChangeBoxesCountHelper')->prepareBoxesChange($this); return true; } /** * Вычисляет size_mark * @return integer */ private function countSizeMask() { $sizeMask = 0; if ($this->size_50 > 0) { $sizeMask += 1; } if ($this->size_60 > 0) { $sizeMask += 2; } if ($this->size_70 > 0) { $sizeMask += 4; } if ($this->size_80 > 0) { $sizeMask += 8; } if ($this->size_90 > 0) { $sizeMask += 16; } if ($this->size_100 > 0) { $sizeMask += 32; } if ($this->size_100p > 0) { $sizeMask += 64; } return $sizeMask; } /** * Высчитывает размер цветов в заявке по ее маске * */ public function getSizeByMask() { $size = array(); $this->getMaxSize($this->size_mask, 64, $size); return $size; } /** * Высчитывает размер цветов в заявке по ее маске * Используется для отделения основного метода от рекурсивной реализации */ private function getMaxSize($size, $factor, &$aSize=array()) { if ($factor<1) return; if ($size>$factor) { $aSize[] = $factor; $this->getMaxSize($size-$factor, $factor/2, $aSize); } else { $this->getMaxSize($size, $factor/2, $aSize); } } /** * Организует сохранение запроса * * @param boolean $runValidation * @param array $attributes */ public function save($runValidation = true, $attributes = null) { if (empty($this->oppositeRequestId) and $this->status == self::STATUS_ON_MARKET and $this->order_type == self::TYPE_SELLER and $this->isNewRecord) { return $this->saveAndSendToQueue(); } else { return parent::save($runValidation, $attributes); } } /** * Подготавливает данные для работы - приводит типы и т.д. */ function afterSave() { $return = parent::afterSave(); if ($return === false) return false; $this->size_50 = new Money($this->size_50); $this->size_60 = new Money($this->size_60); $this->size_70 = new Money($this->size_70); $this->size_80 = new Money($this->size_80); $this->size_90 = new Money($this->size_90); $this->size_100 = new Money($this->size_100); $this->size_100p = new Money($this->size_100p); RequestsStats::model()->updateByRequest($this); if ($this->order_type == Requests::TYPE_SELLER) { RequestsDaily::model()->updateByRequest($this); } return $return; } /** * Метод вызывается после поиска данных и подготавливает данные * */ public function afterFind() { $return = parent::afterFind(); $this->size_50 = new Money($this->size_50); $this->size_60 = new Money($this->size_60); $this->size_70 = new Money($this->size_70); $this->size_80 = new Money($this->size_80); $this->size_90 = new Money($this->size_90); $this->size_100 = new Money($this->size_100); $this->size_100p = new Money($this->size_100p); // закешили данные, при сохранении будем смотреть что поменялось $this->old_amount = $this->boxes_amount; $this->old_status = $this->status; $this->old_deal = $this->boxes_deal; $this->old_order = $this->order_id; return $return; } /** * Проверяет название размера * * @param string $name * @return boolean */ public function checkSize($name) { return (isset($this->{$name}) && $this->{$name}->getValue() > 0); } /** * Получается статистические данные по конкретному заказу * * @param array $iOrderId номер заказ * @return array */ public function buildStats($aOrderId) { $rows = Yii::app()->db->createCommand("SELECT COUNT(*) as count, status FROM `requests` WHERE order_id IN(" . implode(',',$aOrderId) . ") GROUP BY `status`")->queryAll(); foreach ($rows as $row) { $result[$row['status']] = $row['count']; } return array( 'on_market' => (isset($result['on_market'])) ? $result['on_market'] : 0, 'off_market' => (isset($result['off_market'])) ? $result['off_market'] : 0, 'deals' => (isset($result['executed'])) ? $result['executed'] : 0, 'cancel' => (isset($result['expired'])) ? $result['expired'] : 0, ); } public function isExpired() { return $this->status==self::STATUS_EXPIRED; } public function isOffMarket() { return $this->status==self::STATUS_OFF_MARKET; } }