Version: current

       1 <?php
       2
       3 /**
       4  * Запчасть в каталоге
       5  *
       6  * @package Catalog
       7  * @author inxo, dvp, menedem
       8  */
       9 class Product extends MediawebPage{
      10
      11     //static $icon = 'cms/images/treeicons/element'; //!!!!
      12     static $can_be_root = false;
      13     static $allowed_children = 'none';
      14     static $default_parent = 'Catalog';
      15
      16     static $db = array(
      17
      18         'Description' => 'Text', // Краткое описание
      19         'Vendor' => 'Varchar', // Производитель
      20
      21         'Price' => 'CatalogPrice',
      22         'BasePrice' => 'CatalogPrice', // Обычная цена
      23         'CostPrice' => 'CatalogPrice', // Цена по акции
      24         'Discount' => 'CatalogPrice', // Скидка
      25
      26         'Available' => 'Boolean', // Флаг доступности
      27         'AllowPurchase' => 'Boolean', // Флаг "можно заказывать"
      28
      29         'ImportID' => 'Varchar(100)', // ID-импорта, нигде не редактируется и не отображается используется только для импорта
      30         'SKU' => 'Varchar', // Артикул товара
      31
      32         'Quantity' => 'Int',
      33
      34         'Weight' => 'Int', // Вес товара в граммах
      35
      36         'GroupID' => 'Varchar(255)', //Группировка схожих товаров
      37         'GroupTitle' => 'Varchar(255)', //Название товара в блоке группировки, по-умолчанию — MenuTitle
      38     );
      39
      40     static $defaults = array(
      41         'ShowInMenus' => 1,
      42         'ShowOnlyInTab' => 1,
      43         'Available' => 1,
      44         'AllowPurchase' => 1,
      45         'BasePrice' => 0,
      46         'CostPrice' => 0,
      47     );
      48
      49     static $casting = array(
      50         'KilosWeight' => 'Decimal',
      51     );
      52
      53     static $has_one = array(
      54         'Photo' => 'Image',
      55         'ProductVAT' => 'VAT', // Ставка НДС продукта
      56     );
      57
      58     static $has_many = array(
      59         'ParamValues' => 'ProductParamValue',
      60         'Variations' => 'ProductVariation',
      61     );
      62
      63     static $indexes = array(
      64         'ImportID' => true,
      65         'Vendor' => true,
      66     );
      67
      68     static $summary_fields = array('SKU', 'Title', 'BasePrice', 'CostPrice', 'Vendor', 'ImportID', 'Available', 'AllowPurchase');
      69
      70     static $fulltext_fields = array('Title', 'Content', 'Description', 'BasePrice', 'CostPrice', 'Vendor');
      71
      72     static $searchable_fields = array(
      73         'Title' => 'PartialMatchFilter',
      74         'ImportID' => array('field' => 'TextFieldWithEmptyFlag', 'filter'=>'ExactMatchFilterWithEmpty'),
      75         'SKU' => 'ExactMatchFilter',
      76         'Available' => 'ExactMatchFilter',
      77         'Price' => array('field'=>'RangeField', 'filter'=>'WithinRangeFilter'),
      78         'Vendor' => array('field' => 'TextFieldWithEmptyFlag', 'filter'=>'ExactMatchFilterWithEmpty')
      79     );
      80
      81     // возможные поля для использования в фильтрах каталога
      82     static $possible_filter_fields = array('Title', 'Content', 'Description', 'Price', 'Vendor', 'Available', 'AllowPurchase');
      83
      84     static function get_possible_filter_fields() {
      85         $rs = array();
      86         foreach(self::$possible_filter_fields as $field) {
      87             $rs[$field] = singleton(Catalog::$subpage_children)->fieldLabel($field);
      88         }
      89         return $rs;
      90     }
      91
      92     /**
      93      * Позволяет задать поля для фильтра товаров
      94      * Фильтр исопльзуется в админке и при фильтрации товаров в каталоге
      95      *
      96      * @param array $data
      97      */
      98     static function set_searchable_fields(array $data) {
      99         self::$searchable_fields = $data;
     100     }
     101
     102     /**
     103      * Возвращает текущие настроки полей для фильтрации
     104      *
     105      * @return array
     106      */
     107     static function get_searchable_fields() {
     108         return self::$searchable_fields;
     109     }
     110
     111     /**
     112      * Настройки сортировки товаров в рубриках
     113      * Формат: служебное_имя => "строка для ORDER BY"
     114      */
     115     private static $sort_options = array('title' => 'Title ASC', 'pricea' => 'Price', 'priced' => 'Price DESC');
     116
     117     /**
     118      * Изменяет список сортировок товаров.
     119      * Используется в _config.php для настройки параметров каталога
     120      *
     121      * @param array $data - новый список сортировок
     122      */
     123     static function set_sort_options(Array $data) {
     124         self::$sort_options = $data;
     125     }
     126
     127     /**
     128      * Возвращает текущий список сортировок товаров
     129      *
     130      * @return array - текущий список сортировок
     131      */
     132     static function get_sort_options() {
     133         return self::$sort_options;
     134     }
     135
     136     /**
     137      * Возвращает выражение order by для выбранной сортировки
     138      *
     139      * @return string
     140      */
     141     static function sort_options_orderby($sort) {
     142         return (array_key_exists($sort, self::$sort_options)) ? self::$sort_options[$sort] : '';
     143     }
     144
     145     /**
     146      * Возвращает локализованный список сортировок для использования в селектах
     147      *
     148      * @param bool $addDefault - добавлять ли пункт "по-умолчанию"
     149      *
     150      * @return array - список для селектов
     151      */
     152     static function sort_options_dropdown_map($addDefault = false) {
     153         $map = array();
     154         if ($addDefault) {
     155             $map[''] = _t('Product.SortOption_default', 'Default');
     156         }
     157         foreach (self::get_sort_options() as $key => $val) {
     158             $map[$key] = _t('Product.SortOption_'.$key, ucfirst($key));
     159         }
     160         return $map;
     161     }
     162
     163     /**
     164     * устанавливает поведение при нулевой (0) цене продукта
     165     * если false, то не добавлять в корзину
     166     * если true, то перенаправлять на
     167     **/
     168     static $order_empty_price = false;
     169
     170     static function allow_order_empty_price($val = true) {
     171         self::$order_empty_price = $val;
     172     }
     173
     174     /**
     175      * Класс кнопки заказа, класс должен реализовать OrderButtonInterface
     176      */
     177     static $order_button_class = 'SimpleOrderButton';
     178
     179     static function set_order_button_class($value) {
     180         self::$order_button_class = $value;
     181     }
     182
     183     /**
     184      * Класс кнопки заказа для товаров без цены, класс должен реализовать OrderButtonInterface
     185      */
     186     static $empty_order_button_class = 'SimpleOrderButton';
     187
     188     static function set_empty_order_button_class($value) {
     189         self::$empty_order_button_class = $value;
     190     }
     191
     192     // список дополнительных полей для формы простого заказа
     193     protected static $simple_order_additional_fields = array(
     194     );
     195
     196     /**
     197      * Получить список дополнительных полей для формы простого заказа
     198      *
     199      * @return array
     200      */
     201     static function get_simple_order_additional_fields() {
     202         return self::$simple_order_additional_fields;
     203     }
     204
     205     /**
     206      * Добавить дополнительное поле в форму простого заказа
     207      *
     208      * @param string $name - название
     209      * @param array $params - параметры
     210      */
     211     static function add_simple_order_additional_field($name, $params) {
     212         self::$simple_order_additional_fields[$name] = $params;
     213     }
     214
     215     /**
     216      * Задать дополнительные поля формы простого заказа
     217      *
     218      * @param array $fields - поля формы
     219      */
     220     static function set_simple_order_additional_fields($fields) {
     221         self::$simple_order_additional_fields = $fields;
     222     }
     223
     224     function WeightNice() {
     225         if ($this->Weight > 100000) { // Если вес больше 100 кг, то выводим в кг, тип Int
     226             return sprintf(_t("Product.NiceKiloWeight"), $this->dbObject('KilosWeight')->Nice(0));
     227         }
     228         elseif ($this->Weight > 1000) { // Если вес больше 1000 грамм, то выводим в кг, тип Decimal
     229             return sprintf(_t("Product.NiceKiloWeight"), $this->dbObject('KilosWeight')->Nice(1));
     230         }
     231         return sprintf(_t("Product.NiceGrammWeight"), $this->dbObject('Weight')->Nice()); // иначе в граммах красиво, тип Int
     232     }
     233
     234     function getKilosWeight() {
     235         return $this->Weight / 1000;
     236     }
     237
     238     /**
     239      * Возвращаем основное фото товара
     240      * Если есть has_one Photo, то его, иначе первое из прикрепленной галереи
     241      *
     242      * @return Image
     243      */
     244
     245     function MainPhoto() {
     246         if ($this->PhotoID && $this->Photo()->ID) {
     247             return $this->Photo();
     248         }
     249         if ($this->Photos()->Count()) {
     250             return $this->Photos()->First()->Photo();
     251         }
     252         return false;
     253     }
     254
     255     /**
     256      * Список всех фото товара
     257      * Если есть has_one Photo, то вставляем его первым
     258      *
     259      * @return ComponentSet
     260      */
     261
     262     function AllPhotos() {
     263         $photos = $this->Photos();
     264         if ($this->PhotoID && $this->Photo()->ID) {
     265             $mainPhoto = new MediawebPage_Photo();
     266             $mainPhoto->Caption = $this->Title;
     267             $mainPhoto->MediawebPageID = $this->ID;
     268             $mainPhoto->PhotoID = $this->Photo()->ID;
     269             $photos->unshift($mainPhoto);
     270         }
     271         return $photos;
     272     }
     273
     274     // --------- ПАРАМЕТРЫ --------
     275
     276     // Список параметров для вывода в списке товаров
     277     function ListParamValues() {
     278         $join = "JOIN ProductParam ON ProductParam.ID = ProductParamValue.ProductParamID";
     279         if ($this->Parent()->OwnParams) {
     280             $join .= " JOIN Catalog_EnabledParams ON ProductParam.ID = Catalog_EnabledParams.ProductParamID AND Catalog_EnabledParams.CatalogID = {$this->ParentID}";
     281         }
     282         $values = DataObject::get('ProductParamValue', "ProductID = {$this->ID} AND ProductParam.ShowInList = 1", "ProductParam.Sort", $join);
     283         // группируем разные значения одного параметра (если ProductParam->MultiValues == true) в строку через запятую
     284         $rs = ProductParam::group_values_list($values);
     285         return $rs;
     286     }
     287
     288     // Список параметров для вывода на карточке товара
     289     function ViewParamValues() {
     290         $join = "JOIN ProductParam ON ProductParam.ID = ProductParamValue.ProductParamID";
     291         if ($this->Parent()->OwnParams) {
     292             $join .= " JOIN Catalog_EnabledParams ON ProductParam.ID = Catalog_EnabledParams.ProductParamID AND Catalog_EnabledParams.CatalogID = {$this->ParentID}";
     293         }
     294         $values = DataObject::get('ProductParamValue', "ProductID = {$this->ID} AND ProductParam.ShowInView = 1", "ProductParam.Sort", $join);
     295         // группируем разные значения одного параметра (если ProductParam->MultiValues == true) в строку через запятую
     296         $rs = ProductParam::group_values_list($values);
     297         return $rs;
     298     }
     299
     300     // Список параметров для вывода на карточке товара
     301     function ParamValue($title) {
     302         $join = "JOIN ProductParam ON ProductParam.ID = ProductParamValue.ProductParamID";
     303         if ($this->Parent()->OwnParams) {
     304             $join .= " JOIN Catalog_EnabledParams ON ProductParam.ID = Catalog_EnabledParams.ProductParamID AND Catalog_EnabledParams.CatalogID = {$this->ParentID}";
     305         }
     306         $title = Convert::raw2sql($title);
     307         $value = DataObject::get('ProductParamValue', "ProductVariationID = {$this->ID} AND ProductParam.TechTitle = '{$title}' ", "", $join);
     308         if ($value) {
     309             return $value->First();
     310         }
     311         return false;
     312     }
     313
     314     //Связанные товары
     315     function GroupedProducts() {
     316         if ($this->GroupID) {
     317             return DataObject::get('Product', "GroupID = '".Convert::raw2sql($this->GroupID)."'");  // AND ImportID <> '{$this->ImportID}' - вроде не выкидываем сам товар из списка связанных
     318         }
     319     }
     320
     321     function AvailableTitle() {
     322         return ($this->Available) ? 'Есть' : 'Нет';
     323     }
     324
     325     public function defaultSearchColumns() {
     326         return implode(',', $this->stat('fulltext_fields'));
     327     }
     328
     329     public function __construct($record = null, $isSingleton = false) {
     330         if (!isset($record['Price']) && isset($record['ID'])) {
     331             $p = DataObject::get_by_id('Product', $record['ID']);
     332             $record = $p->record;
     333         }
     334         parent::__construct($record, $isSingleton);
     335     }
     336
     337
     338     /*--------------- Функции для импорта -------------------*/
     339     static function import_find($importID) {
     340         return DataObject::get_one('Product', "ImportID = '" . Convert::raw2sql($importID) . "'");
     341     }
     342
     343     /**
     344      * Список полей, которые могут быть в данных импорта  !!!!! У текущего Product нет поля SKU!!!!!
     345      */
     346     static $possibleFields = array(
     347         'Title', 'Available', 'AllowPurchase', 'BasePrice', 'CostPrice', 'Vendor', 'SKU', 'Description', 'Content', 'URLSegment', 'MenuTitle', 'MetaTitle', 'MetaDescription', 'MetaKeywords', 'Sort'
     348     );
     349
     350     /**
     351      * Добавление полей, которые могут быть в данных импорта
     352      *
     353      * @param array $fields
     354      */
     355     static function addPossibleFields($fields) {
     356         if ($fields)
     357             foreach($fields as $field)
     358                 self::$possibleFields[] = $field;
     359     }
     360
     361     function getProductAdminLink(){
     362         return "<a target='_blank' href='/admin/show/{$this->ID}'>{$this->Title}</a>";
     363     }
     364
     365      /**
     366      * Обновляет объект,
     367      * @param $importLog - объект для протоколиорвания импорта (или сама задача), для возможности записать сообщения об ошибках
     368      * @param $data - массив с данными для импорта
     369      * @return bool - флаг можно ли продолжать импорт
     370      */
     371     function importUpdate($importLog, $data) {
     372         if (!$this->importValidate($importLog, $data)) {
     373             return false;
     374         }
     375
     376         $rs = $this->extend('onBeforeImport', $importLog, $data);
     377         if ($rs && max($rs) == false) return false;
     378
     379         $this->ImportID = $data['id'];
     380         foreach(self::$possibleFields as $field) {
     381             if (isset($data[$field]))
     382             $this->{$field} = Convert::raw2sql($data[$field]);
     383         }
     384
     385         //цепляем фотку
     386         if (isset($data['Photo'])) {
     387             $this->PhotoID = ($data['Photo']) ? $data['Photo']->ID : 0;
     388         }
     389
     390         //Обрабатываем списочные параметры (если есть)
     391         if (isset($data['params'])) {
     392             foreach($data['params'] as $param) {
     393                 // !!! то есть не проверяем есть ли такое поле и разрешено оно для записи?
     394                 $name = Convert::raw2sql($param->attributes()->name);
     395                 //Проверяем, есть ли такое поле у Product
     396                 if ($this->db($name)) {
     397                     $value = Convert::raw2sql($param);
     398                     $this->{$name} = $value;
     399                 } else {
     400                     $importLog->addLog("У товара не определено поле {$name}!", 'error');
     401                 }
     402             }
     403         }
     404         //Сначала сохраняем, чтоб потом привязывать изображения и файлы
     405         if (!$this->ID) //не обновляем родителя у уже импортированных товаров  (для возможности ручного изменения структуры каталога на сайте).
     406             $this->ParentID = ($data['ParentID'] !== false) ? $data['ParentID'] : 0;
     407
     408         if (isset($data['ParentID']) && $data['ParentID'] === false) { // Если нету родителя, хотя должен быть (не корневой раздел), то не публикуем
     409             $this->write();
     410         } else {
     411             $this->doPublish();
     412         }
     413
     414         //Обрабатываем списочные фотографии (если есть)
     415         // !!!!!!
     416         if ($data['Photos']) {
     417             //прикрепляем фотки
     418             $oldPhotos = $this->Photos();
     419             $oldPhotosIDs = $oldPhotos->getIdList(); //сохраняем список ID прикрепленных фоток
     420             $this->Photos()->removeAll();
     421
     422             foreach($data['Photos'] as $importedImage) {
     423                 if ($mwPhoto = $oldPhotos->find('PhotoID', $importedImage->ID)) {
     424                     unset($oldPhotosIDs[$mwPhoto->ID]); //если фотка осталась, то удаляем ее ID из списка
     425                 } else {
     426                     $mwPhoto = new MediawebPage_Photo();
     427                     $mwPhoto->PhotoID = $importedImage->ID;
     428                 }
     429                 $mwPhoto->MediawebPageID = $this->ID; //Цепляем к Product
     430                 $mwPhoto->Caption = $importedImage->Title;
     431                 $mwPhoto->write();
     432             }
     433
     434             if (count($oldPhotosIDs) > 0) {
     435                 foreach($oldPhotosIDs as $oldPhotosID) { // все, что осталось в списке - чистим!
     436                     if ($oldPhoto = $oldPhotos->find('ID', $oldPhotosID)) {
     437                         $oldPhoto->delete();
     438                     }
     439                 }
     440             }
     441         }
     442
     443         //Обрабатываем списочные файлы (если есть)
     444         if ($data['Files']) {
     445             //прикрепляем файлы
     446             $oldFiles = $this->Files();
     447             $oldFilesIDs = $oldFiles->getIdList(); //сохраняем список ID прикрепленных фоток
     448             $this->Files()->removeAll();
     449
     450             foreach($data['Files'] as $importedFile) {
     451                 if ($mwFile = $oldFiles->find('AttachID', $importedFile->ID)) {
     452                     unset($oldFilesIDs[$mwFile->ID]); //если фотка осталась, то удаляем ее ID из списка
     453                 } else {
     454                     $mwFile = new MediawebPage_File();
     455                     $mwFile->AttachID = $importedFile->ID; //Цепляем к Product
     456                 }
     457                 $mwFile->Caption = $importedFile->Title;
     458                 $mwFile->MediawebPageID = $this->ID; //Цепляем к Product
     459                 $mwFile->write();
     460             }
     461
     462             if (count($oldFilesIDs) > 0) {
     463                 foreach($oldFilesIDs as $oldFilesID) { // все, что осталось в списке - чистим!
     464                     if ($oldFile = $oldFiles->find('ID', $oldFilesID)) {
     465                         $oldFile->delete();
     466                     }
     467                 }
     468             }
     469         }
     470
     471         //Обрабатываем списочные спец.каталоги (если есть)
     472         if ($data['SpecialCatalogs']) {
     473             $productSpecialCatalogs = array(); //список ID-шники спецкаталогов, в которых изначально был продукт
     474             //складываем в список ID всех спец.каталогов, в которых есть товар
     475             $allSpecialCatalogs = DataObject::get('SpecialCatalog');
     476             if ($allSpecialCatalogs) {
     477                 foreach($allSpecialCatalogs as $specialCatalog) {
     478                     $specialCatalogProducts = $specialCatalog->Products()->getIdList();
     479                     if (isset($specialCatalogProducts[$this->ID])) {
     480                         $productSpecialCatalogs[$specialCatalog->ID] = $specialCatalog->ID;
     481                     }
     482                 }
     483             }
     484
     485             //Добавляем товар в указанные спец.каталоги
     486             foreach($data['SpecialCatalogs'] as $specialCatalog) {
     487                 if (isset($productSpecialCatalogs[$specialCatalog->ID])) { //если товар уже есто в новом спец.каталоге
     488                     unset($productSpecialCatalogs[$specialCatalog->ID]); //то удаляем ID спец.каталога из списка
     489                 } else {
     490                     $specialCatalog->Products()->add($this); // иначе добавляем товар в спец.каталог
     491                 }
     492             }
     493
     494             if (count($productSpecialCatalogs)) {
     495                 foreach($productSpecialCatalogs as $productSpecialCatalog) { //чистим все, что осталось в списке
     496                     if ($oldSpecialCatalog = $allSpecialCatalogs->find('ID', $productSpecialCatalog)) {
     497                         $oldSpecialCatalog->Products()->remove($this);
     498                     }
     499                 }
     500             }
     501         }
     502         $this->extend('onAfterImport');
     503         return true;
     504     }
     505
     506     static function import_parse_bool_value($value) {
     507         if (in_array($rs, array('true', '1'))) {
     508             return 1;
     509         }
     510         if (in_array($rs, array('false', '0'))) {
     511             return 0;
     512         }
     513         return null;
     514     }
     515
     516     /**
     517      * Проверяет данные полей объекта на соответствие типам
     518      * @param $importLog - объект для протоколиорвания импорта (или сама задача), для возможности записать сообщения об ошибках
     519      * @return bool - флаг можно ли продолжать импорт
     520      */
     521     function importValidate($importLog, & $data) {
     522         // !!! по идее Title обязательно отлько для новых товаров
     523         if ((!$this->Title) && (!isset($data['Title']) || trim($data['Title']) == '')) { // если у каталога нет Title и Title нет в импорте, то ругаемся
     524             $importLog->addLog("Товар с id='{$data['id']}' не имеет названия!", 'error');
     525             return false;
     526         }
     527         if (isset($data['Title']) && trim($data['Title']) == '') { // если тег <Title> задан, но пустой, то ругаемся
     528             $importLog->addLog("Товар с id='{$data['id']}' не имеет названия!", 'error');
     529             return false;
     530         }
     531         if (isset($data['Available'])) {
     532             $rs = self::import_parse_bool_value($data['Available']);
     533             if ($rs === null) {
     534                 $importLog->addLog(sprintf(_t('Product.ImportBadBoolValue'), "Наличие товара на складе", $data['Title']), 'warning');
     535             } else {
     536                 $data['Available'] = $rs;
     537             }
     538         }
     539         if (isset($data['AllowPurchase'])) {
     540             $rs = self::import_parse_bool_value($data['AllowPurchase']);
     541             if ($rs === null) {
     542                 $importLog->addLog(sprintf(_t('Product.ImportBadBoolValue'), "Разрешено покупать", $data['Title']), 'warning');
     543             } else {
     544                 $data['AllowPurchase'] = $rs;
     545             }
     546         }
     547         if (isset($data['BasePrice']) && !is_numeric($data['BasePrice'])) {
     548             $importLog->addLog("Параметр 'Цена' товара {$data['Title']} должен быть числовым!", 'error');
     549             return false;
     550         }
     551         if (isset($data['CostPrice']) && !is_numeric($data['CostPrice'])) {
     552             $importLog->addLog("Параметр 'Цена по акции' товара {$data['Title']} должен быть числовым!", 'error');
     553             $data['CostPrice'] = 0;
     554         }
     555         if (isset($data['Sort']) && $data['Sort'] != (int)$data['Sort']) {
     556             $importLog->addLog("Параметр Sort товара {$data['Title']} не является целым числом!", 'warning');
     557             $data['Sort'] = 0;
     558         }
     559
     560         $rs = $this->extend('importValidate', $importLog, $data);
     561         if ($rs && max($rs) == false) return false;
     562
     563         return true;
     564     }
     565
     566      /**
     567      * Выполняет удаление всех объектов перед импортом
     568      */
     569     function importClearAll($importLog) {
     570         $oldMode = Versioned::get_reading_mode();
     571
     572         Versioned::reading_stage('Stage');
     573         $products = DataObject::get('Product');
     574         if ($products)
     575             foreach($products as $product) {
     576                 $product->doUnpublish();
     577                 $product->delete();
     578             }
     579
     580         Versioned::reading_stage('Live');
     581         $products = DataObject::get('Product');
     582         if ($products)
     583             foreach($products as $product) {
     584                 $product->doUnpublish();
     585                 $product->delete();
     586             }
     587
     588         Versioned::set_reading_mode($oldMode);
     589     }
     590     /*--------------- Конец функций для импорта -------------------*/
     591
     592
     593
     594     public function getCMSFields() {
     595         SiteTree::disableCMSFieldsExtensions();
     596         $fields = parent::getCMSFields();
     597         SiteTree::enableCMSFieldsExtensions();
     598
     599         $fields->replaceField('ParentType', new HiddenField('ParentType', '', 'subpage'));
     600
     601         $fields->replaceField('Title', new TextField('Title', _t('Product.db_Title', 'Наименование')));
     602
     603         if ($allVATs = DataObject::get('VAT')) {
     604             $fields->addFieldToTab('Root.Content.Main', new DropdownField("ProductVATID", $this->fieldLabel('ProductVAT'), $allVATs->map('ID', 'Title', _t('VAT.SelectVAT'))));
     605         }
     606
     607         // фильтруем каталоги
     608         // !!! сейчас показывается только куст текущего каталога
     609         $cat = $this->Parent();
     610         while ($cat->Parent() && $cat->Parent()->ClassName == self::$default_parent) {
     611             $cat = $cat->Parent();
     612         }
     613         $parentIDField = new TreeDropdownField('ParentID', $this->fieldLabel('ParentID'), 'SiteTree');
     614         $parentIDField->setTreeBaseID($cat->ParentID);
     615         $parentIDField->setFilterFunction(create_function('$node', 'return ($node->ClassName == "'.self::$default_parent.'");')); //  || $node->ClassName == "StartCatalog"
     616
     617         $fields->replaceField('ParentID', $parentIDField);
     618
     619         $fields->addFieldToTab('Root.Content.Main', new NumericField('BasePrice', $this->fieldLabel('BasePrice')));
     620         $fields->addFieldToTab('Root.Content.Main', new NumericField('CostPrice', $this->fieldLabel('CostPrice')));
     621
     622         $fields->addFieldToTab('Root.Content.Main', new CheckboxField('Available', $this->fieldLabel('Available')));
     623         $fields->addFieldToTab('Root.Content.Main', new CheckboxField('AllowPurchase', $this->fieldLabel('AllowPurchase')));
     624
     625         $fields->addFieldToTab('Root.Content.Main', $imgField = new ImageField('Photo', $this->fieldLabel('Photo')));
     626         if ($folder = $this->getAssociatedFolder()) {
     627             $folderPath = substr_replace(str_replace(ASSETS_DIR.'/', '', $folder->Filename), "", -1);
     628             $imgField->setFolderName($folderPath);
     629         }
     630
     631         $fields->addFieldToTab('Root.Content.Main', new TextField('Vendor', $this->fieldLabel('Vendor')));
     632         $fields->addFieldToTab('Root.Content.Main', new TextField('ImportID', $this->fieldLabel('ImportID')));
     633         $fields->addFieldToTab('Root.Content.Main', new TextField('SKU', $this->fieldLabel('SKU')));
     634         $fields->addFieldToTab('Root.Content.Main', new TextField('GroupID', $this->fieldLabel('GroupID')));
     635
     636         if ($allVATs = DataObject::get('VAT')) {
     637             $fields->addFieldToTab('Root.Content.Main', new DropdownField("ProductVATID", $this->fieldLabel('ProductVAT'), $allVATs->map('ID', 'Title', _t('VAT.SelectVAT'))));
     638         }
     639
     640         $fields->addFieldToTab('Root.Content.Main', new TextareaField('Description', $this->fieldLabel('Description')));
     641         $fields->addFieldToTab('Root.Content.Main', new TextField('Quantity', $this->fieldLabel('Quantity')));
     642         $fields->addFieldToTab('Root.Content.Main', new NumericField('Weight', $this->fieldLabel('Weight')));
     643         $fields->addFieldToTab('Root.Content.Main', new TextField('GroupID', $this->fieldLabel('GroupID')));
     644
     645         if (HtmlEditorConfig::get_active() != 'cms') {
     646             HtmlEditorConfig::set_active('cms');
     647         }
     648
     649         $fields->addFieldToTab('Root.Content', new Tab("FullContent", $this->fieldLabel('Content')), 'Metadata');
     650         $fields->addFieldToTab('Root.Content.FullContent', $fields->dataFieldByName('Content'));
     651
     652         if (Catalog::$use_additional_params) {
     653             //параметры товаров
     654             if ($this->Parent() && $this->Parent()->hasMethod('Params') && ($categoryParams = $this->Parent()->getNonVariationCatalogParams()) && $categoryParams->Count()) {
     655                 $tab = $fields->findOrMakeTab('Root.Content.Params', 'Параметры');
     656                 $paramValues = new DataObjectSet();
     657                 foreach($categoryParams as $categoryParam) {
     658                     if ($categoryParam->Type == 'bool') {
     659                         $tab->push(new ProductParamValue_BoolValueField('ParamValues', $categoryParam, $this->ID));
     660                     } else {
     661                         if ($categoryParam->MultiValues) {
     662                             if ($categoryParam->PossibleValuesList()) {
     663                                 $tab->push(new ProductParamValue_MultiValueSetField('ParamValues', $categoryParam, $this->ID));
     664                             } else {
     665                                 $tab->push(new LiteralField('ParamTitle', "<h4>{$categoryParam->Title}</h4>"));
     666                                 $rate = new ProductParamValue();
     667                                 $fieldList = array(
     668                                     "Value" => $rate->fieldLabel('Value'),
     669                                 );
     670                                 $fieldTypes = array(
     671                                     'Value' => 'TextField',
     672                                 );
     673                                 // полю добавили суффикс _$categoryParam->ID, чтобы выводить несколько списков параметров
     674                                 $ctf = new TableField("ParamValues_{$categoryParam->ID}", "ProductParamValue", $fieldList, $fieldTypes, 'ProductParamID', $categoryParam->ID);
     675                                 $ctf->setExtraData(array('ProductID' => $this->ID)); // чтобы сохранялись новые значения
     676                                 $params = new DataObjectSet();
     677                                 if ($allParamValues = DataObject::get('ProductParamValue', "ProductParamID = {$categoryParam->ID} AND ProductID = {$this->ID}")) {
     678                                     $params = $allParamValues;
     679                                 }
     680                                 $ctf->setCustomSourceItems($params);
     681                                 $tab->push($ctf);
     682                             }
     683                         } else {
     684                             if ($categoryParam->PossibleValuesList()) {
     685                                 $tab->push(new ProductParamValue_MultiValueField('ParamValues', $categoryParam, $this->ID));
     686                             } else {
     687                                 $tab->push(new ProductParamValue_ValueField('ParamValues', $categoryParam, $this->ID));
     688                             }
     689                         }
     690                     }
     691                     //$paramValues->push($t);
     692                 }
     693
     694                 $rate = new ProductParamValue();
     695                 $fieldList = array(
     696                     "ID" => $rate->fieldLabel('ID'),
     697                     "ParamTitle" => $rate->fieldLabel('Title'),
     698                     "Value" => $rate->fieldLabel('Value'),
     699
     700                 );
     701                 $fieldTypes = array(
     702                     'ID' => 'ReadonlyField',
     703                     'ParamTitle' => 'ReadonlyField',
     704                     'Value' => 'TextField',
     705                 );
     706                 $tablefield = new TableField("ParamValues", "ProductParamValue", $fieldList, $fieldTypes);
     707                 $tablefield->setCustomSourceItems($paramValues);
     708             }
     709         }
     710
     711         // вариации
     712         if (Catalog::$use_variations) {
     713             if ($this->Parent() && $this->Parent()->hasMethod('Params') && ($variationParams = $this->Parent()->getVariationCatalogParams())) {
     714                 $tab = $fields->findOrMakeTab('Root.Content.Variations', 'Вариации');
     715                 $ctf = $this->getVariationsTable();
     716                 $ctf->setPopupSize(1000, 600);
     717                 $tab->push($ctf);
     718             }
     719         }
     720         $this->extend('updateCMSFields', $fields);
     721
     722         $this->extend('hideCMSFields', $fields); // extend для скрытия полей из админки
     723         return $fields;
     724     }
     725
     726     function getVariationsTable() {
     727         $singleton = singleton('ProductVariation');
     728         $query = $singleton->buildSQL("\"ProductID\" = '{$this->ID}'");
     729         $variations = $singleton->buildDataObjectSet($query->execute());
     730         $filter = $variations ? "\"ID\" IN ('" . implode("','", $variations->column('ID')) . "')" : "\"ID\" < '0'";
     731
     732         $summaryfields= $singleton->summaryFields();
     733
     734         if ($this->Parent() && $this->Parent()->hasMethod('Params') && ($variationParams = $this->Parent()->getVariationCatalogParams())) {
     735             foreach($variationParams as $attribute){
     736                 $summaryfields["AttributeProxy.Val".$attribute->TechTitle] = $attribute->Title; //!! понять, как получается значение !!
     737             }
     738         }
     739         $tableField = new HasManyComplexTableField(
     740             $this,
     741             'Variations',
     742             'ProductVariation',
     743             $summaryfields,
     744             null,
     745             $filter
     746         );
     747         $tableField->Markable = 0;
     748         $tableField->setRelationAutoSetting(true);
     749         return $tableField;
     750     }
     751
     752     public function getCMSActions() {
     753         Requirements::javascript('catalog/javascript/ProductAction.js');
     754         $fields = parent::getCMSActions();
     755         $fields->push(new HiddenField('backLink', '', $this->Parent()->ID));
     756         $fields->insertFirst(new FormAction('goBack', _t('Product.BACKBUTTON', 'Back')));
     757         return $fields;
     758     }
     759
     760     public function populateDefaults() {
     761         parent::populateDefaults();
     762
     763         $sc = SiteConfig::current_site_config();
     764         $this->ShowInMenus = ($sc->ProductShowInTree) ? 1 : 0;
     765         $this->ShowOnlyInTab = ($sc->ProductShowInTree) ? 0 : 1;
     766     }
     767
     768     function onBeforeWrite() {
     769         parent::onBeforeWrite();
     770         // если вариации есть, то высчитываем цену основного товара, как минимальную по вариациям
     771         if (Catalog::$use_variations && $this->Variations()->exists()) {
     772             if (($allVariations = $this->AllVariations()) && $allVariations->Count()) {
     773                 $allVariations->sort('VariationPrice');
     774                 $this->BasePrice = $allVariations->First()->getBasePrice();
     775                 $this->CostPrice = $allVariations->First()->getCostPrice();
     776             } else {
     777                 $this->BasePrice = 0;
     778                 $this->CostPrice = 0;
     779             }
     780             $this->Price = ($this->CostPrice > 0) ? $this->CostPrice : $this->BasePrice;
     781         } else {
     782             if ($this->isChanged('BasePrice') || $this->isChanged('CostPrice')) {
     783                 $this->Price = ($this->CostPrice > 0) ? $this->CostPrice : $this->BasePrice;
     784             }
     785         }
     786         $this->Discount = (($this->CostPrice > 0) ? ($this->BasePrice - $this->CostPrice) : 0);
     787
     788         $sc = SiteConfig::current_site_config();
     789         if ($sc->ProductShowInTree) {
     790             $this->ShowOnlyInTab = 0;
     791         }
     792         else {
     793             $this->ShowInMenus = 0;
     794         }
     795     }
     796
     797     function onAfterDelete() {
     798         parent::onAfterDelete();
     799
     800         if ($this->IsDeletedFromStage && !$this->ExistsOnLive) {
     801             DB::Query("DELETE FROM `ProductParamValue` WHERE ProductID = {$this->ID}");
     802         }
     803     }
     804
     805     function getVAT(){
     806         if ($this->ProductVATID && ($vat = $this->ProductVAT())) {
     807             return $vat;
     808         }
     809         if ($this->ParentID && ($parent = $this->Parent())) {
     810             return $parent->getVAT();
     811         }
     812         return false;
     813     }
     814
     815     /**
     816      * Список виртуальных товаров связанных с текущим
     817      *
     818      * @return DataObjectSet
     819      */
     820     function VirtualPages() {
     821         if (!$this->ID)
     822             return null;
     823         if (class_exists('Subsite')) {
     824             return Subsite::get_from_all_subsites('VirtualProduct', "\"CopyContentFromID\" = " . (int) $this->ID);
     825         } else {
     826             return DataObject::get('VirtualProduct', "\"CopyContentFromID\" = " . (int) $this->ID);
     827         }
     828     }
     829
     830     public function canCreate($member = null) {
     831         return (DataObject::get_one('Catalog')) ? parent::canCreate($member) : false;
     832     }
     833
     834     function fieldLabels($includerelations = true) {
     835         $labels = parent::fieldLabels($includerelations);
     836         $labels['Title'] = _t('Product.db_Title', 'Title');
     837         return $labels;
     838     }
     839
     840     /**
     841      * Возвращает класс для кнопки заказа товара в зависимости от цены и значений Product::$empty_order_button_class и Product::$order_button_class
     842      *
     843      * @return string
     844      */
     845     function orderButtonClass() {
     846         if (!$this->AllowPurchase) {
     847             return false;
     848         }
     849         if ($this->Price == 0) {
     850             if (self::$order_empty_price)
     851                 return self::$empty_order_button_class;
     852             return false;
     853         }
     854         $class = self::$order_button_class;
     855         $this->extend('updateOrderButtonClass', $class);
     856         return $class;
     857     }
     858
     859     /**
     860      * Возвращает объект для генерации кнопки заказа
     861      *
     862      * @return OrderButtonInterface
     863      */
     864
     865     function OrderButton() {
     866         $buttonClass = $this->orderButtonClass();
     867         if (!$buttonClass)
     868             return false;
     869         return new $buttonClass($this->ID);
     870     }
     871
     872     /**
     873      * Возвращает html для кнопки заказа 1 товара
     874      *
     875      * @return string
     876      */
     877     function OrderButtonOne() {
     878         if ($button = $this->OrderButton())
     879             return $button->One();
     880         return false;
     881     }
     882
     883     /**
     884      * Возвращает html для кнопки заказа товара с оплем для кол-ва
     885      *
     886      * @return string
     887      */
     888     function OrderButtonWithNum() {
     889         if ($button = $this->OrderButton()) {
     890             return $button->WithNum();
     891         }
     892         return false;
     893     }
     894
     895     /**
     896      * возвращает цену товара без скидок
     897      *
     898      * @return CatalogPrice
     899      */
     900
     901     function getRealPrice() {
     902         $price = $this->Price;
     903         $this->extend('updatePrice', $price);
     904         return $price;
     905     }
     906
     907     /**
     908      * Возвращает центу товара с учетом скидок
     909      *
     910      * @param bool $onlyreal - вернуть только 1 цену
     911      *
     912      * @return mixed
     913      */
     914     function Price($onlyreal=false) {
     915         // Значения по умолчанию
     916         $real = $this->getRealPrice();
     917         $client = $real;
     918         $delta = 0;
     919         $percent = 0;
     920         $decimal = 2;
     921
     922         if ($this->CostPrice == 0) {
     923             // персональная скидка (скидки на группу учтены там же)
     924             $member = Member::currentUser();
     925             if ($member){
     926                 if ($member->hasMethod('getPersonalDiscount') && $percent < $member->getPersonalDiscount()) {
     927                     $percent = $member->getPersonalDiscount();
     928                 }
     929             }
     930
     931             $this->extend('updateDiscount', $percent);
     932         }
     933
     934         $sc = SiteConfig::current_site_config();
     935         $ceil = $sc->CatalogDiscountCeil;
     936
     937         if ($ceil) {
     938             $real = ceil($real);
     939             $client = ceil($client);
     940             $decimal = 0;
     941         }
     942
     943         if ($percent) {
     944             $delta = ($ceil) ? ceil(($real / 100) * $percent) : round(($real / 100) * $percent, 2);
     945             $client = $real - $delta;
     946         }
     947
     948         if ($onlyreal !== false) {
     949             return $client;
     950         }
     951
     952         return new ArrayData(array(
     953             'Real' => DBField::create('CatalogPrice', $real),
     954             'Client' => DBField::create('CatalogPrice', $client),
     955             'Delta' => DBField::create('CatalogPrice', $delta),
     956             'Percent' => $percent,
     957             'Discount' => ($percent > 0) ? 1 : 0,
     958             'Currency' => $sc->CatalogCurrency,
     959         ));
     960     }
     961
     962     // Старая цена товара
     963     function OldPrice() {
     964         if ($this->HasDiscount()) {
     965             return $this->BasePrice;
     966         }
     967         return 0;
     968     }
     969
     970     function HasDiscount() {
     971         return ($this->CostPrice > 0);
     972     }
     973
     974     function Currency() {
     975         return SiteConfig::current_site_config()->CatalogCurrency;
     976     }
     977
     978     function getOrderItem() {
     979         //!!! TODO генерировать OrderItem самим
     980     }
     981
     982     // Доступные вариации товара
     983     function AvailableVariations() {
     984         return $this->Variations("Available = 1");
     985     }
     986
     987     // Разрешенные для покупки вариации товара
     988     function AllowPurchaseVariations() {
     989         return $this->Variations("AllowPurchase = 1");
     990     }
     991
     992     /**
     993      * Вариации товара
     994      * В зависимости от настроек либо все вариации, либо только доступные (Available = 1)
     995      *
     996      * @return ComponentSet
     997      */
     998     function AllVariations() {
     999         if (SiteConfig::current_site_config()->UseOnlyAllowPurchaseVariations) {
    1000             return $this->AllowPurchaseVariations();
    1001         }
    1002         return $this->Variations();
    1003     }
    1004
    1005     // цена вариации с минимальной ценой
    1006     function MinPriceVariation() {
    1007         if ($allVariations = $this->AllVariations()) {
    1008             $allVariations->sort('VariationPrice');
    1009             return $allVariations->First()->Price();
    1010         }
    1011     }
    1012
    1013     // цена вариации с максимальной ценой
    1014     function MaxPriceVariation() {
    1015         if (($allVariations = $this->AllVariations()) && ($allVariations->Count() > 1)) { // только если больше одной вариации
    1016             $allVariations->sort('VariationPrice');
    1017             return $allVariations->Last()->Price();
    1018         }
    1019         return false;
    1020     }
    1021
    1022     function HasSomePrices() {
    1023         if ($this->MinPriceVariation() && $this->MaxPriceVariation() && ($this->MinPriceVariation()->Real != $this->MaxPriceVariation()->Real)) {
    1024             return true;
    1025         }
    1026         return false;
    1027     }
    1028
    1029     /**
    1030      * Селектор для выбора вариации
    1031      *
    1032      *
    1033      * @return html
    1034      */
    1035
    1036     function VariationsSelector() {
    1037         return $this->customise(array(
    1038             'Variations' => $this->VariationsParamValues(),
    1039         ))->renderWith('VariationsSelector');
    1040     }
    1041
    1042     // список уникальных значений по каждому параметру вариаций
    1043     function VariationsParamValues() {
    1044         return self::combine_variations($this->AllVariations());
    1045     }
    1046
    1047     // возвращаем список уникальных значений по каждому параметру вариаций
    1048     // используем для построения списков выбора значений параметров в карточке товара
    1049     static function combine_variations($variations) {
    1050         $rs = array();
    1051         foreach($variations as $variation) {
    1052             foreach($variation->ParamValues() as $paramValue) {
    1053                 if (!isset($rs[$paramValue->ProductParamID])) {
    1054                     $rs[$paramValue->ProductParamID] = new DataObjectSet();
    1055                 }
    1056                 $t = $rs[$paramValue->ProductParamID];
    1057                 if (!$t->find('Value', $paramValue->Value)) {
    1058                     $t->push($paramValue);
    1059                 }
    1060                 $t->sort('Value');
    1061             }
    1062         }
    1063         $list = new DataObjectSet();
    1064         foreach($rs as $paramID=>$paramValues) {
    1065             if ($paramValues && $paramValues->Count()) {
    1066                 $list->push(new ArrayData(array(
    1067                     'Param' => $paramValues->First()->ProductParam(),
    1068                     'Values' => $paramValues
    1069                 )));
    1070             }
    1071         }
    1072         return $list;
    1073     }
    1074
    1075     function VariationOrderButtonLink($ID='') {
    1076         return $this->Link("variation_order_button/{$ID}");
    1077     }
    1078
    1079     function PurchaseStatus() {
    1080         if (Catalog::$use_variations && $this->Variations()) {
    1081             return 'HasVariations';
    1082         }
    1083         if (!$this->Available && !$this->AllowPurchase) {
    1084             return 'NoOrder';
    1085         }
    1086         if (!$this->Available && $this->AllowPurchase) {
    1087             return 'PreOrder';
    1088         }
    1089         if ($this->Available && !$this->AllowPurchase) {
    1090             return 'NoOrder';
    1091         }
    1092         if ($this->Available && $this->AllowPurchase) {
    1093             return 'InStock';
    1094         }
    1095         return 'NoOrder';
    1096     }
    1097
    1098     function PurchaseStatusTitle() {
    1099         return _t('Product.PurchaseStatus_' . $this->PurchaseStatus());
    1100     }
    1101
    1102     function canOrder() {
    1103         if (in_array($this->PurchaseStatus(), array('PreOrder', 'InStock'))) {
    1104             return true;
    1105         }
    1106         return false;
    1107     }
    1108 }
    1109
    1110 class Product_Controller extends Page_Controller {
    1111     private $siblings = null;
    1112     private $next = null;
    1113     private $prev = null;
    1114
    1115     /**
    1116      * Возвращает "братьев" товара
    1117      *
    1118      * @return DataObjectSet
    1119      */
    1120     function Siblings() {
    1121         if (!isset($this->siblings)) {
    1122             $catalog = new Catalog_Controller($this->Parent());
    1123             $catalog->init();
    1124             $this->siblings = $this->Parent()->filteredProducts($catalog->CurrentSort, $catalog->FilterData);
    1125         }
    1126         return $this->siblings;
    1127     }
    1128
    1129     /**
    1130      * Возвращает следующий товар в списке
    1131      *
    1132      * @return Product
    1133      */
    1134     function NextProduct() {
    1135         if (!isset($this->next)) {
    1136             $list = $this->Siblings()->toArray();
    1137             while ($row = array_shift($list)) {
    1138                 if ($row->ID == $this->dataRecord->ID) break;
    1139             }
    1140             $this->next = array_shift($list);
    1141         }
    1142         return $this->next;
    1143     }
    1144
    1145     /**
    1146      * Возвращает предыдущий товар в списке
    1147      *
    1148      * @return Product
    1149      */
    1150     function PrevProduct() {
    1151         if (!isset($this->prev)) {
    1152             $this->prev = false;
    1153             $list = $this->Siblings()->toArray();
    1154             $prev = array_shift($list);
    1155             while ($row = array_shift($list)) {
    1156                 if ($row->ID == $this->dataRecord->ID) {
    1157                     $this->prev = $prev;
    1158                     break;
    1159                 }
    1160                 $prev = $row;
    1161             }
    1162         }
    1163         return $this->prev;
    1164     }
    1165
    1166     /**
    1167      * Возвращает список вариаций товара
    1168      *
    1169      * @return json
    1170      */
    1171     function product_variations() {
    1172         if ($this->Variations()->exists()) {
    1173             $variations = $this->Variations()->data();
    1174             return json_encode(array('Variations' => $variations));
    1175         }
    1176     }
    1177
    1178     function variation_order_button($request) {
    1179         if ($variation = DataObject::get_by_id('ProductVariation', (int)$request->param('ID'))) {
    1180             return $this->customise(array(
    1181                 'Variation' => $variation
    1182             ));
    1183         }
    1184         return $this->httpError(404);
    1185     }
    1186 }
    1187

[Raise a SilverStripe Framework issue/bug](https://github.com/silverstripe/silverstripe-framework/issues/new)
- [Raise a SilverStripe CMS issue/bug](https://github.com/silverstripe/silverstripe-cms/issues/new)
- Please use the Silverstripe Forums to ask development related questions. -