diff --git a/CHANGELOG.md b/CHANGELOG.md index fcca47eafb..35b240c49a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ -------------------------- - #1865: Загрузка изображений в товар (@sabian) +- #2440: Массовое редактирование свойств товаров (@sabian, @samox1n) - #2606: Дебаг-панель не отображалась, если включить на сайте другой язык (@fazliddin, @sabian) - #2615: Текст уведомления при создании карты сайта (@dzhedai, @sabian) - #2616: Очистка кеша после изменения блока контента (@tmsk70, @sabian) diff --git a/composer.lock b/composer.lock index c7ffe94065..fd7b9672dc 100644 --- a/composer.lock +++ b/composer.lock @@ -431,7 +431,7 @@ }, { "name": "symfony/event-dispatcher", - "version": "v3.3.11", + "version": "v3.3.13", "source": { "type": "git", "url": "https://github.com/symfony/event-dispatcher.git", @@ -1089,30 +1089,33 @@ }, { "name": "facebook/webdriver", - "version": "1.4.1", + "version": "1.5.0", "source": { "type": "git", "url": "https://github.com/facebook/php-webdriver.git", - "reference": "eadb0b7a7c3e6578185197fd40158b08c3164c83" + "reference": "86b5ca2f67173c9d34340845dd690149c886a605" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/facebook/php-webdriver/zipball/eadb0b7a7c3e6578185197fd40158b08c3164c83", - "reference": "eadb0b7a7c3e6578185197fd40158b08c3164c83", + "url": "https://api.github.com/repos/facebook/php-webdriver/zipball/86b5ca2f67173c9d34340845dd690149c886a605", + "reference": "86b5ca2f67173c9d34340845dd690149c886a605", "shasum": "" }, "require": { "ext-curl": "*", "ext-zip": "*", - "php": "^5.5 || ~7.0", - "symfony/process": "^2.8 || ^3.1" + "php": "^5.6 || ~7.0", + "symfony/process": "^2.8 || ^3.1 || ^4.0" }, "require-dev": { "friendsofphp/php-cs-fixer": "^2.0", + "guzzle/guzzle": "^3.4.1", + "php-coveralls/php-coveralls": "^1.0.2", "php-mock/php-mock-phpunit": "^1.1", - "phpunit/phpunit": "4.6.* || ~5.0", - "satooshi/php-coveralls": "^1.0", - "squizlabs/php_codesniffer": "^2.6" + "phpunit/phpunit": "^5.7", + "sebastian/environment": "^1.3.4 || ^2.0 || ^3.0", + "squizlabs/php_codesniffer": "^2.6", + "symfony/var-dumper": "^3.3 || ^4.0" }, "type": "library", "extra": { @@ -1137,7 +1140,7 @@ "selenium", "webdriver" ], - "time": "2017-04-28T14:54:49+00:00" + "time": "2017-11-15T11:08:09+00:00" }, { "name": "guzzlehttp/guzzle", @@ -2772,7 +2775,7 @@ }, { "name": "symfony/browser-kit", - "version": "v3.3.11", + "version": "v3.3.13", "source": { "type": "git", "url": "https://github.com/symfony/browser-kit.git", @@ -2829,16 +2832,16 @@ }, { "name": "symfony/console", - "version": "v3.3.11", + "version": "v3.3.13", "source": { "type": "git", "url": "https://github.com/symfony/console.git", - "reference": "fd684d68f83568d8293564b4971928a2c4bdfc5c" + "reference": "63cd7960a0a522c3537f6326706d7f3b8de65805" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/console/zipball/fd684d68f83568d8293564b4971928a2c4bdfc5c", - "reference": "fd684d68f83568d8293564b4971928a2c4bdfc5c", + "url": "https://api.github.com/repos/symfony/console/zipball/63cd7960a0a522c3537f6326706d7f3b8de65805", + "reference": "63cd7960a0a522c3537f6326706d7f3b8de65805", "shasum": "" }, "require": { @@ -2893,11 +2896,11 @@ ], "description": "Symfony Console Component", "homepage": "https://symfony.com", - "time": "2017-11-07T14:16:22+00:00" + "time": "2017-11-16T15:24:32+00:00" }, { "name": "symfony/css-selector", - "version": "v3.3.11", + "version": "v3.3.13", "source": { "type": "git", "url": "https://github.com/symfony/css-selector.git", @@ -2950,7 +2953,7 @@ }, { "name": "symfony/debug", - "version": "v3.3.11", + "version": "v3.3.13", "source": { "type": "git", "url": "https://github.com/symfony/debug.git", @@ -3006,7 +3009,7 @@ }, { "name": "symfony/dom-crawler", - "version": "v3.3.11", + "version": "v3.3.13", "source": { "type": "git", "url": "https://github.com/symfony/dom-crawler.git", @@ -3062,7 +3065,7 @@ }, { "name": "symfony/finder", - "version": "v3.3.11", + "version": "v3.3.13", "source": { "type": "git", "url": "https://github.com/symfony/finder.git", @@ -3170,16 +3173,16 @@ }, { "name": "symfony/process", - "version": "v3.3.11", + "version": "v3.3.13", "source": { "type": "git", "url": "https://github.com/symfony/process.git", - "reference": "e14bb64d7559e6923fb13ee3b3d8fa763a2c0930" + "reference": "a56a3989fb762d7b19a0cf8e7693ee99a6ffb78d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/process/zipball/e14bb64d7559e6923fb13ee3b3d8fa763a2c0930", - "reference": "e14bb64d7559e6923fb13ee3b3d8fa763a2c0930", + "url": "https://api.github.com/repos/symfony/process/zipball/a56a3989fb762d7b19a0cf8e7693ee99a6ffb78d", + "reference": "a56a3989fb762d7b19a0cf8e7693ee99a6ffb78d", "shasum": "" }, "require": { @@ -3215,11 +3218,11 @@ ], "description": "Symfony Process Component", "homepage": "https://symfony.com", - "time": "2017-11-05T15:47:03+00:00" + "time": "2017-11-13T15:31:11+00:00" }, { "name": "symfony/yaml", - "version": "v3.3.11", + "version": "v3.3.13", "source": { "type": "git", "url": "https://github.com/symfony/yaml.git", diff --git a/protected/modules/store/components/helpers/ProductBatchHelper.php b/protected/modules/store/components/helpers/ProductBatchHelper.php new file mode 100644 index 0000000000..6f746466a9 --- /dev/null +++ b/protected/modules/store/components/helpers/ProductBatchHelper.php @@ -0,0 +1,55 @@ + Yii::t('StoreModule.store', 'equal'), + self::PRICE_ADD => Yii::t('StoreModule.store', 'increase'), + self::PRICE_SUB => Yii::t('StoreModule.store', 'decrease'), + ]; + } + + /** + * @return array + */ + public static function getOpUnits() + { + return [ + self::OP_UNIT => Yii::t('StoreModule.store', 'unit'), + self::OP_PERCENT => Yii::t('StoreModule.store', '%'), + ]; + } +} \ No newline at end of file diff --git a/protected/modules/store/components/repository/ProductRepository.php b/protected/modules/store/components/repository/ProductRepository.php index ec3a673084..9a1b4204e9 100644 --- a/protected/modules/store/components/repository/ProductRepository.php +++ b/protected/modules/store/components/repository/ProductRepository.php @@ -391,4 +391,67 @@ public function getLinkedProductsDataProvider(Product $product, $typeCode = null ] ); } + + /** + * @param ProductBatchForm $form + * @param array $ids + * @return int + */ + public function batchUpdate(ProductBatchForm $form, array $ids) + { + $attributes = $form->loadQueryAttributes(); + + if (null !== $form->price) { + $attributes['price'] = $this->getPriceQuery( + 'price', + $form->price, + (int)$form->price_op, + (int)$form->price_op_unit + ); + } + + if (null !== $form->discount_price) { + $attributes['discount_price'] = $this->getPriceQuery( + 'discount_price', + $form->discount_price, + (int)$form->discount_price_op, + (int)$form->discount_price_op_unit + ); + } + + if (count($attributes) === 0) { + return true; + } + + $criteria = new CDbCriteria(); + $criteria->addInCondition('id', $ids); + + return Product::model()->updateAll($attributes, $criteria); + } + + /** + * @param $field + * @param $price + * @param $operation + * @param $unit + * @return float|CDbExpression + */ + private function getPriceQuery($field, $price, $operation, $unit) + { + if (ProductBatchHelper::PRICE_EQUAL === $operation) { + return $price; + } + + $sign = ProductBatchHelper::PRICE_ADD === $operation ? '+' : '-'; + + if (ProductBatchHelper::OP_PERCENT === $unit) { + return new CDbExpression(sprintf('%s %s ((%s / 100) * :percent)', $field, $sign, $field), [ + ':percent' => $price + ]); + } + + return new CDbExpression(sprintf('%s %s :price', $field, $sign), [ + ':price' => $price + ]); + } } diff --git a/protected/modules/store/controllers/ProductBackendController.php b/protected/modules/store/controllers/ProductBackendController.php index 7762f0f5f1..57230a7824 100644 --- a/protected/modules/store/controllers/ProductBackendController.php +++ b/protected/modules/store/controllers/ProductBackendController.php @@ -61,10 +61,10 @@ public function accessRules() ['allow', 'roles' => ['admin'],], ['allow', 'actions' => ['index'], 'roles' => ['Store.ProductBackend.Index'],], ['allow', 'actions' => ['view'], 'roles' => ['Store.ProductBackend.View'],], - ['allow', 'actions' => ['create', 'copy'], 'roles' => ['Store.ProductBackend.Create'],], + ['allow', 'actions' => ['create', 'copy', 'batch'], 'roles' => ['Store.ProductBackend.Create'],], [ 'allow', - 'actions' => ['update', 'inline', 'sortable', 'deleteImage'], + 'actions' => ['update', 'inline', 'sortable', 'deleteImage', 'batch'], 'roles' => ['Store.ProductBackend.Update'], ], ['allow', 'actions' => ['delete', 'multiaction'], 'roles' => ['Store.ProductBackend.Delete'],], @@ -306,7 +306,10 @@ public function actionIndex() Yii::app()->getRequest()->getQuery('Product') ); } - $this->render('index', ['model' => $model]); + $this->render('index', [ + 'model' => $model, + 'batchModel' => new ProductBatchForm(), + ]); } /** @@ -408,4 +411,23 @@ public function actionCopy() Yii::app()->ajax->success(); } } + + /** + * + */ + public function actionBatch() + { + $form = new ProductBatchForm(); + $form->setAttributes(Yii::app()->getRequest()->getPost('ProductBatchForm')); + + if ($form->validate() === false) { + Yii::app()->ajax->failure(Yii::t('StoreModule.store', 'Wrong data')); + } + + if ($this->productRepository->batchUpdate($form, explode(',', Yii::app()->getRequest()->getPost('ids')))) { + Yii::app()->ajax->success('ok'); + } + + Yii::app()->ajax->failure(); + } } diff --git a/protected/modules/store/forms/ProductBatchForm.php b/protected/modules/store/forms/ProductBatchForm.php new file mode 100644 index 0000000000..064da19ba9 --- /dev/null +++ b/protected/modules/store/forms/ProductBatchForm.php @@ -0,0 +1,137 @@ + true], + ['status', 'in', 'range' => array_keys(Product::model()->getStatusList())], + ['in_stock', 'in', 'range' => array_keys(Product::model()->getInStockList())], + ['price_op, discount_price_op', 'in', 'range' => array_keys(ProductBatchHelper::getPericeOpList())], + ['price_op_unit, discount_price_op_unit', 'in', 'range' => array_keys(ProductBatchHelper::getOpUnits())], + ['price, discount_price', 'store\components\validators\NumberValidator'], + ['is_special', 'boolean'], + ['producer_id', 'exist', 'attributeName' => 'id', 'className' => 'Producer'], + ['category_id', 'exist', 'attributeName' => 'id', 'className' => 'StoreCategory'], + ['view', 'length', 'max' => 100], + ['view', 'filter', 'filter' => 'trim'], + ['view', 'filter', 'filter' => 'strip_tags'], + ['status, in_stock, quantity, producer_id, is_special, category_id, view, price, discount_price, discount', 'default', 'value' => null], + ['discount', 'numerical', 'integerOnly' => true, 'max' => 100], + ]; + } + + /** + * @return array + */ + public function attributeLabels() + { + return [ + 'status' => Yii::t('StoreModule.store', 'Status'), + 'in_stock' => Yii::t('StoreModule.store', 'Stock status'), + 'is_special' => Yii::t('StoreModule.store', 'Special'), + 'quantity' => Yii::t('StoreModule.store', 'Quantity'), + 'producer_id' => Yii::t('StoreModule.store', 'Producer'), + 'category_id' => Yii::t('StoreModule.store', 'Category'), + 'view' => Yii::t('StoreModule.store', 'Template'), + 'price' => Yii::t('StoreModule.store', 'Price'), + 'discount_price' => Yii::t('StoreModule.store', 'Discount price'), + 'discount' => Yii::t('StoreModule.store', 'Discount, %'), + ]; + } + + /** + * @return array + */ + public function loadQueryAttributes() + { + $result = []; + $allowed = ['status', 'in_stock', 'is_special', 'quantity', 'producer_id', 'category_id', 'view', 'discount']; + $attributes = $this->getAttributes(); + + foreach ($attributes as $name => $value) { + if (in_array($name, $allowed) && null !== $value) { + $result[$name] = $value; + } + } + + return $result; + } +} \ No newline at end of file diff --git a/protected/modules/store/messages/ru/store.php b/protected/modules/store/messages/ru/store.php index 09a6fae047..8d0b6eb473 100644 --- a/protected/modules/store/messages/ru/store.php +++ b/protected/modules/store/messages/ru/store.php @@ -27,6 +27,7 @@ 'Address' => 'Адрес', 'administration' => 'управление', 'Alias' => 'Алиас', + 'Apply' => 'Применить', 'are required' => 'обязательны.', 'ASC' => 'По возрастанию', 'Attribute created' => 'Атрибут создан', @@ -45,6 +46,7 @@ 'Average price' => 'Среднерыночная цена', 'Bad characters in {attribute} field' => 'Запрещенные символы в поле {attribute}', 'Bad request. Please don\'t use similar requests anymore' => 'Неверный запрос. Пожалуйста, больше не повторяйте такие запросы', + 'Batch actions' => 'Массовые действия', 'Canonical' => 'Каноническая ссылка', 'Catalog manage' => 'Управление каталогом', 'Catalog manager' => 'Управляющий каталогом товаров', @@ -88,6 +90,7 @@ 'Currency' => 'Валюта', 'Data for SEO' => 'Данные для поисковой оптимизации', 'Data' => 'Данные', + 'decrease' => 'уменьшить на', 'Default image' => 'Картинка по умолчанию', 'Default sort direction' => 'Направление сортировки', 'Default sort' => 'Сортировка по умолчанию', @@ -100,7 +103,7 @@ 'DESC' => 'По убыванию', 'Description' => 'Описание', 'Directory "{dir}" is not writeable! {link}' => 'Директория "{dir}" не доступна для записи! {link}', - 'Discount price' => 'Скидка', + 'Discount price' => 'Цена со скидкой', 'Discount' => 'Скидка', 'Discount, %' => 'Скидка, %', 'Do you really want to delete selected elements?' => 'Вы уверены, что хотите удалить выбранные элементы?', @@ -119,6 +122,7 @@ 'Editor' => 'Изменил', 'Email' => 'Email', 'Enter {field}' => 'Введите {field}', + 'equal' => 'равна', 'Error uploading some files...' => 'При загрузке некоторых файлов произошла ошибка...', 'Error uploading some images...' => 'При загрузке некоторых изображений произошла ошибка...', 'External id' => 'ID во внешней системе', @@ -154,6 +158,7 @@ 'in stock' => 'в наличии', 'Increase in %' => 'Увеличить на %', 'Increase in the amount of' => 'Увеличить на сумму', + 'increase' => 'увеличить на', 'Into cart' => 'В корзину', 'Items per page' => 'Товаров на странице', 'kg' => 'кг', @@ -190,6 +195,7 @@ 'Module for categories/sections management' => 'Модуль для управления категориями/разделами сайта', 'Name' => 'Название', 'New price' => 'Новая', + 'No one product selected' => 'Вы не выбрали ни одного продукта', 'No' => 'Нет', 'Not active' => 'Не доступен', 'Not available' => 'Нет в наличии', @@ -263,6 +269,7 @@ 'Short text (up to 250 characters)' => 'Короткий текст (до 250 символов)', 'Short title' => 'Короткое название', 'SKU' => 'Артикул', + 'Something going wrong. Sorry.' => 'При попытке изменить данные произошла ошибка!', 'Special' => 'Спецпредложение', 'Status' => 'Статус', 'Stock status' => 'На складе', @@ -291,6 +298,7 @@ 'Type' => 'Тип', 'Types list' => 'Типы товаров', 'Types' => 'Типы товаров', + 'unit' => 'единиц', 'Unit' => 'Единица измерения', 'Unknown request. Don\'t repeat it please!' => 'Неверный запрос. Пожалуйста, больше не повторяйте такие запросы!', 'Update attribute' => 'Редактировать', @@ -332,6 +340,7 @@ 'Width' => 'Ширина', 'Width, m.' => 'Ширина, м.', 'Without a group' => 'Без группы', + 'Wrong data' => 'Указаны некорректные данные', 'Yes' => 'Да', 'You are adding translate in to {lang}!' => 'Вы добавляете перевод на {lang} язык!', 'You must specify the attribute value' => 'Необходимо указать значение атрибута', diff --git a/protected/modules/store/views/productBackend/index.php b/protected/modules/store/views/productBackend/index.php index 603a398083..011718fc78 100644 --- a/protected/modules/store/views/productBackend/index.php +++ b/protected/modules/store/views/productBackend/index.php @@ -2,8 +2,12 @@ /** * @var $this ProductBackendController * @var $model Product + * @var $form ActiveForm + * @var $batchModel ProductBatchForm */ +use yupe\widgets\ActiveForm; + $this->layout = 'product'; $this->breadcrumbs = [ @@ -42,6 +46,12 @@ '#', ['id' => 'copy-products', 'class' => 'btn btn-sm btn-default pull-right', 'style' => 'margin-right: 4px;'] ), + 'batch' => CHtml::button(Yii::t('StoreModule.store', 'Batch actions'), [ + 'class' => 'btn btn-sm btn-primary pull-right', + 'style' => 'margin-right:7px', + 'data-toggle' => 'modal', + 'data-target' => '#batch-actions', + ]), ], 'columns' => [ [ @@ -195,6 +205,176 @@ ] ); ?> + + createUrl('/store/productBackend/copy'); $tokenName = Yii::app()->getRequest()->csrfTokenName; @@ -230,6 +410,56 @@ }); } }); + + $('#batch-actions-form').on('submit', function(e) { + e.preventDefault(); + + var checked = $.fn.yiiGridView.getCheckedRowsIds('product-grid'); + $('#batch-action-error').addClass('hidden'); + + if (checked.length == 0) { + return false; + } + + var data = $(this).serialize() + '&ids=' + checked; + + data['$tokenName'] = "$token"; + + $.ajax({ + url: $(this).attr('action'), + type: 'POST', + dataType: 'json', + data: data, + success: function(data) { + if (data.result === true) { + $.fn.yiiGridView.update("product-grid"); + $('#batch-actions').modal('hide'); + } + + if (data.result === false) { + $('#batch-action-error').removeClass('hidden'); + } + }, + error: function(data) { + + } + }); + }); + + $('#batch-actions').on('show.bs.modal', function(e) { + var checked = $.fn.yiiGridView.getCheckedRowsIds('product-grid'); + + if (checked.length > 0) { + return true; + } + + $('#no-product-selected').removeClass('hidden'); + $('#batch-apply').attr('disabled', true); + + }).on('hidden.bs.modal', function(e) { + $('#no-product-selected').addClass('hidden'); + $('#batch-apply').attr('disabled', false); + }); JS ); ?>