Commit 7776ca75d091b76ffba313f3fad0db7145276fc8

Authored by Mihail
1 parent 53edab28

add form, model and controller for uploaded crosses files (details_crosses

backend/controllers/CrossingUploadController.php 0 → 100644
  1 +<?php
  2 +/**
  3 + * Created by PhpStorm.
  4 + * User: Tsurkanov
  5 + * Date: 15.10.2015
  6 + * Time: 12:27
  7 + */
  8 +
  9 +namespace backend\controllers;
  10 +
  11 +use backend\components\base\BaseController;
  12 +use common\components\CustomVarDamp;
  13 +use yii\filters\AccessControl;
  14 +use backend\models\UploadFileCrossingForm;
  15 +use backend\models\DetailsCrosses;
  16 +use yii\web\UploadedFile;
  17 +use \Yii;
  18 +
  19 +class CrossingUploadController extends BaseController
  20 +{
  21 + public $layout = "/column";
  22 +
  23 + /**
  24 + * @inheritdoc
  25 + */
  26 + public function behaviors()
  27 + {
  28 + return [
  29 + 'access' => [
  30 + 'class' => AccessControl::className(),
  31 + 'rules' => [
  32 + [
  33 + 'actions' => ['index', 'result'],
  34 + 'allow' => true,
  35 + 'roles' => ['@'],
  36 + ],
  37 + ],
  38 + ],
  39 +// 'verbs' => [
  40 +// 'class' => VerbFilter::className(),
  41 +// 'actions' => [
  42 +// 'logout' => ['post'],
  43 +// ],
  44 +// ],
  45 + ];
  46 + }
  47 +
  48 + /**
  49 + * @inheritdoc
  50 + */
  51 + public function actions()
  52 + {
  53 + return [
  54 + 'error' => [
  55 + 'class' => 'yii\web\ErrorAction',
  56 + ],
  57 + ];
  58 + }
  59 +
  60 +
  61 + public function actionIndex()
  62 + {
  63 + $model = new UploadFileCrossingForm();
  64 + return $this->render('index', ['model' => $model]);
  65 + }
  66 +
  67 + public function actionResult()
  68 + {
  69 + $model = new UploadFileCrossingForm();
  70 + $data = [];
  71 + if ($model->load(Yii::$app->request->post())) {
  72 + $model->file = UploadedFile::getInstance($model, 'file');
  73 +
  74 + if ($model->validate()) {
  75 + $file_name = $model->file->name;
  76 + $model->file_path = Yii::getAlias('@temp_upload') . '/' . $file_name;
  77 +
  78 + $model->file->saveAs($model->file_path);
  79 + //запускаем парсинг
  80 + // доп. опции для парсера - удаление префикса в артикулах
  81 + $options['mode'] = 'crosses';
  82 + $fields = [];
  83 + if ($model->delete_prefix1) {
  84 + $fields[] = 'ARTICLE';
  85 + }
  86 + if ($model->delete_prefix2) {
  87 + $fields[] = 'CROSS_ARTICLE';
  88 + }
  89 + if ( $fields ) {
  90 + $options [ 'converter_conf' ] = [ 'configuration' => [ "article" => $fields ,
  91 + "string" => ['ARTICLE', 'CROSS_ARTICLE'],] ];
  92 + } else {
  93 + $options [ 'converter_conf' ] = [ 'configuration' => [ "string" => ['ARTICLE', 'CROSS_ARTICLE'], ] ];
  94 + }
  95 +
  96 + $data = $model->readFile( $options );
  97 + $crosses_model = new DetailsCrosses();
  98 + $crosses_model->ManualInsertWithIgnore( $data );
  99 +
  100 + Yii::$app->session->setFlash('success', 'Файл кроссов успешно загружен');
  101 + return $this->render('index', ['model' => $model]);
  102 + }else{
  103 + // не прошла валидация форма загрузки файлов
  104 + $errors_str = '';
  105 + foreach ($model->getErrors() as $error) {
  106 + $errors_str .= implode( array_values($error) );
  107 + }
  108 + throw new \ErrorException( $errors_str );
  109 + }
  110 +
  111 + } else {
  112 + throw new \ErrorException( 'Ошибка загрузки данных' );
  113 + }
  114 + }
  115 +}
0 \ No newline at end of file 116 \ No newline at end of file
backend/controllers/ParserController.php
@@ -10,7 +10,6 @@ use backend\models\UploadFileParsingForm; @@ -10,7 +10,6 @@ use backend\models\UploadFileParsingForm;
10 use yii\web\UploadedFile; 10 use yii\web\UploadedFile;
11 use yii\data\ArrayDataProvider; 11 use yii\data\ArrayDataProvider;
12 use yii\multiparser\DynamicFormHelper; 12 use yii\multiparser\DynamicFormHelper;
13 -use backend\components\parsers\CustomParserConfigurator;  
14 use backend\models\ImportersFiles; 13 use backend\models\ImportersFiles;
15 use backend\models\Importers; 14 use backend\models\Importers;
16 use yii\base\ErrorException; 15 use yii\base\ErrorException;
backend/models/DetailsCrosses.php
@@ -2,6 +2,7 @@ @@ -2,6 +2,7 @@
2 2
3 namespace backend\models; 3 namespace backend\models;
4 4
  5 +use common\components\CustomVarDamp;
5 use Yii; 6 use Yii;
6 7
7 /** 8 /**
@@ -17,6 +18,10 @@ use Yii; @@ -17,6 +18,10 @@ use Yii;
17 class DetailsCrosses extends \backend\components\base\BaseActiveRecord 18 class DetailsCrosses extends \backend\components\base\BaseActiveRecord
18 { 19 {
19 /** 20 /**
  21 + * int - размер пакета запроса
  22 + */
  23 + const BATCH = 1000;
  24 + /**
20 * @inheritdoc 25 * @inheritdoc
21 */ 26 */
22 public static function tableName() 27 public static function tableName()
@@ -50,4 +55,31 @@ class DetailsCrosses extends \backend\components\base\BaseActiveRecord @@ -50,4 +55,31 @@ class DetailsCrosses extends \backend\components\base\BaseActiveRecord
50 'timestamp' => Yii::t('app', 'Timestamp'), 55 'timestamp' => Yii::t('app', 'Timestamp'),
51 ]; 56 ];
52 } 57 }
  58 +
  59 + /**
  60 + * вставка данных с игнором дублей прямым запросом SQL
  61 + * @param $data - массив вставляемых данный, вставка будет прозводится пакетами размером указанным в константе BATCH
  62 + * @throws \yii\db\Exception
  63 + */
  64 + //@todo - вынести все ручные инсерты в отдельный класс
  65 + public function ManualInsertWithIgnore( $data )
  66 + {
  67 + // \common\components\CustomVarDamp::dumpAndDie($data);
  68 + $table_name = self::tableName();
  69 + $keys_arr = array_keys($data[0]);
  70 +
  71 + // запросы будем выполнять пакетами
  72 + // размер пакета установлен в константе
  73 + // разобъем массив на пакеты и будем их проходить
  74 + $data = array_chunk($data, $this::BATCH);
  75 + foreach ($data as $current_batch_array) {
  76 +
  77 + //воспользуемся пакетной вставкой от фреймворка
  78 + $query = Yii::$app->db->createCommand()->batchInsert($table_name, $keys_arr, $current_batch_array)->sql;
  79 + // добавим ключевое слово - ignore
  80 + $query = preg_replace('/INSERT/','INSERT IGNORE', $query);
  81 + Yii::$app->db->createCommand($query)->execute();
  82 +
  83 + }
  84 + }
53 } 85 }
backend/models/UploadFileCrossingForm.php 0 → 100644
  1 +<?php
  2 +namespace backend\models;
  3 +
  4 +use yii\base\ErrorException;
  5 +use yii\base\Model;
  6 +use yii\web\UploadedFile;
  7 +use Yii;
  8 +use common\components\CustomVarDamp;
  9 +
  10 +/**
  11 + * UploadForm is the model behind the upload form.
  12 + */
  13 +class UploadFileCrossingForm extends Model
  14 +{
  15 + /**
  16 + * @var UploadedFile file attribute
  17 + */
  18 + // атрибуты формы
  19 + public $file;
  20 + public $delete_prefix1;
  21 + public $delete_prefix2;
  22 + public $file_path;
  23 +
  24 +
  25 +
  26 + public function rules()
  27 + {
  28 + return [
  29 + ['file', 'required', 'message' => 'Не выбран файл!' ],
  30 + [['file'], 'file', 'extensions' => 'csv', 'checkExtensionByMimeType'=>false ],
  31 + [['delete_prefix1', 'delete_prefix2'], 'boolean' ],
  32 +
  33 + ];
  34 + }
  35 +
  36 + public function attributeLabels()
  37 + {
  38 + return [
  39 + 'file' => Yii::t('app', 'Источник'),
  40 + 'delete_prefix1' => Yii::t('app', 'Удалять префикс в артикуле товара 1'),
  41 + 'delete_prefix2' => Yii::t('app', 'Удалять префикс в артикуле товара 2'),
  42 + ];
  43 + }
  44 +
  45 + public function readFile( $options = [] ){
  46 +
  47 + $data = Yii::$app->multiparser->parse( $this->file_path, $options );
  48 + if( !is_array( $data ) ){
  49 + throw new ErrorException("Ошибка чтения из файла кроссов {$this->file_path}");
  50 + }
  51 + // файл больше не нужен - данные прочитаны и сохранены в кеш
  52 + if( file_exists($this->file_path) )
  53 + unlink($this->file_path);
  54 +
  55 + return $data;
  56 + }
  57 +
  58 + public function fields()
  59 + {
  60 + return [
  61 +
  62 + 'delete_price1',
  63 + 'delete_price2'
  64 +
  65 + ];
  66 + }
  67 +
  68 +
  69 +}
0 \ No newline at end of file 70 \ No newline at end of file
backend/models/UploadFileParsingForm.php
@@ -48,9 +48,7 @@ class UploadFileParsingForm extends Model @@ -48,9 +48,7 @@ class UploadFileParsingForm extends Model
48 return [ 48 return [
49 ['importer_id', 'required', 'message' => 'Не указан поставщик!' ], 49 ['importer_id', 'required', 'message' => 'Не указан поставщик!' ],
50 ['file', 'required', 'message' => 'Не выбран файл!' ], 50 ['file', 'required', 'message' => 'Не выбран файл!' ],
51 - //@todo - not working this file validator!!! - fixed  
52 - [['file'], 'file'],// 'extensions' => ['csv', 'xml'] ],  
53 - // 'wrongMimeType' => 'Указан неподдерживаемый тип файла. Можно выбирать csv, xml файлы.' ], 51 + [['file'], 'file', 'extensions' => ['csv', 'xml'], 'checkExtensionByMimeType'=>false ],
54 ['importer_id', 'integer','max' => 999999, 'min' => 0 ], 52 ['importer_id', 'integer','max' => 999999, 'min' => 0 ],
55 [['action','delete_prefix', 'delete_price', 'success'], 'boolean', 'except' => 'auto' ], // только для ручной загрузки 53 [['action','delete_prefix', 'delete_price', 'success'], 'boolean', 'except' => 'auto' ], // только для ручной загрузки
56 ['delimiter', 'string', 'max' => 1], 54 ['delimiter', 'string', 'max' => 1],
backend/views/crossing-upload/index.php 0 → 100644
  1 +<?php
  2 +use yii\widgets\ActiveForm;
  3 +use yii\helpers\Html;
  4 +use yii\helpers\ArrayHelper;
  5 +
  6 +?>
  7 +<div class="row">
  8 + <div class="col-lg-5">
  9 + <?php $form = ActiveForm::begin(['options' => ['enctype' => 'multipart/form-data',],'action'=>['crossing-upload/result']]);
  10 +
  11 + if ($msg = \Yii::$app->session->getFlash('success')) { // вернулись после успешной загрузки данного файла
  12 + echo Html::tag('h3', $msg ,['class'=>'bg-success']);
  13 + }
  14 + ?>
  15 + <h3>Кросс файлы</h3>
  16 +
  17 + <?= $form->field($model, 'delete_prefix1')->checkbox() ?>
  18 + <?= $form->field($model, 'delete_prefix2')->checkbox() ?>
  19 + <?= $form->field($model, 'file')->fileInput()->label(false) ?>
  20 +
  21 + <div class="form-group">
  22 + <?= Html::submitButton(Yii::t( 'app', 'Выполнить' ), ['class' => 'btn btn-primary']) ?>
  23 + </div>
  24 +
  25 + <?php ActiveForm::end() ?>
  26 + </div>
  27 +</div>
  28 +
backend/views/layouts/column.php
@@ -283,6 +283,7 @@ $this-&gt;beginContent(&#39;@app/views/layouts/main.php&#39;); @@ -283,6 +283,7 @@ $this-&gt;beginContent(&#39;@app/views/layouts/main.php&#39;);
283 'options' => ['class' => 'sidebar-menu'], 283 'options' => ['class' => 'sidebar-menu'],
284 'items' => [ 284 'items' => [
285 ['label' => "Загрузка файлов", 'url' => ['#'], 'items' => [ 285 ['label' => "Загрузка файлов", 'url' => ['#'], 'items' => [
  286 + ['label' => 'Кросс-файлы', 'url' => ['crossing-upload/index']],
286 ['label' => 'Файлы на сервере', 'url' => ['parser/server-files']], 287 ['label' => 'Файлы на сервере', 'url' => ['parser/server-files']],
287 ['label' => 'Загрузить файл на сервер', 'url' => ['parser/index', 'mode' => 1]], 288 ['label' => 'Загрузить файл на сервер', 'url' => ['parser/index', 'mode' => 1]],
288 ['label' => 'Ручная загрузка', 'url' => ['parser/index']], 289 ['label' => 'Ручная загрузка', 'url' => ['parser/index']],
common/components/PriceWriter.php
@@ -65,8 +65,15 @@ class PriceWriter @@ -65,8 +65,15 @@ class PriceWriter
65 $row['PRICE'] = \Yii::$app->multiparser->convertToFloat($row['PRICE']); 65 $row['PRICE'] = \Yii::$app->multiparser->convertToFloat($row['PRICE']);
66 $row['BOX'] = \Yii::$app->multiparser->convertToInteger($row['BOX']); 66 $row['BOX'] = \Yii::$app->multiparser->convertToInteger($row['BOX']);
67 // присвоим полный артикул 67 // присвоим полный артикул
  68 +
68 $row['FULL_ARTICLE'] = $row['ARTICLE']; 69 $row['FULL_ARTICLE'] = $row['ARTICLE'];
69 - $row['ARTICLE'] = \Yii::$app->multiparser->convertToArticle( $row ); 70 + if ((int)$this->configuration['delete_prefix']) {
  71 + $row = \Yii::$app->multiparser->convertToArticle( $row, $this->configuration['importer_id'] );
  72 + } else {
  73 + $row['ARTICLE'] = \Yii::$app->multiparser->convertToArticle( $row['ARTICLE'] );
  74 + }
  75 +
  76 +
70 if (isset($row['ADD_BOX'])) 77 if (isset($row['ADD_BOX']))
71 $row['ADD_BOX'] = \Yii::$app->multiparser->convertToInteger($row['ADD_BOX']); 78 $row['ADD_BOX'] = \Yii::$app->multiparser->convertToInteger($row['ADD_BOX']);
72 79
@@ -84,9 +91,6 @@ class PriceWriter @@ -84,9 +91,6 @@ class PriceWriter
84 try { 91 try {
85 //@todo add transaction 92 //@todo add transaction
86 93
87 - if ((int)$this->configuration['delete_prefix']) {  
88 - $details_model->delete_prefix = true;  
89 - }  
90 if ((int)$this->configuration['delete_price']) { 94 if ((int)$this->configuration['delete_price']) {
91 $details_model->delete_price = true; 95 $details_model->delete_price = true;
92 } 96 }
common/components/parsers/CustomConverter.php
@@ -4,6 +4,7 @@ namespace common\components\parsers; @@ -4,6 +4,7 @@ namespace common\components\parsers;
4 use common\components\CustomVarDamp; 4 use common\components\CustomVarDamp;
5 use yii\multiparser\Converter; 5 use yii\multiparser\Converter;
6 use backend\models\Details; 6 use backend\models\Details;
  7 +use backend\models\DetailsCrosses;
7 use backend\models\ImportersPrefix; 8 use backend\models\ImportersPrefix;
8 9
9 class CustomConverter extends Converter 10 class CustomConverter extends Converter
@@ -93,6 +94,22 @@ class CustomConverter extends Converter @@ -93,6 +94,22 @@ class CustomConverter extends Converter
93 return $row; 94 return $row;
94 } 95 }
95 96
  97 + public static function convertToCrosses( array $row )
  98 + {
  99 +
  100 + $details_model = new DetailsCrosses();
  101 + // проверим все ли обязательные колонки были указаны пользователем
  102 + $details_model->load(['DetailsCrosses' => $row]);
  103 +
  104 + if (!$details_model->validate()) {
  105 + $errors = '';
  106 + foreach ($details_model->errors as $key => $arr_errors) {
  107 + $errors .= "Аттрибут $key - " . implode(' , ', $arr_errors);
  108 + }
  109 + throw new \ErrorException($errors);
  110 + }
  111 + return $row;
  112 + }
96 public function ConvertToMultiply(array $row) 113 public function ConvertToMultiply(array $row)
97 { 114 {
98 $PRICE = $row['PRICE']; 115 $PRICE = $row['PRICE'];
@@ -125,31 +142,47 @@ class CustomConverter extends Converter @@ -125,31 +142,47 @@ class CustomConverter extends Converter
125 142
126 } 143 }
127 144
128 - public static function convertToArticle( array $row ) 145 + public static function convertToArticle( $value, $importer_id = '' )
129 { 146 {
130 - if ( isset($row['ARTICLE']) ) { 147 + if(isset( $importer_id )){
  148 + self::$importer_id = $importer_id;
  149 + }
  150 +
  151 + if (is_array($value)) {
131 152
  153 + $row = $value;
132 // 1. Уберем префикс который разделен пробелом (если он есть) 154 // 1. Уберем префикс который разделен пробелом (если он есть)
133 $words = explode(" ", $row['ARTICLE']); 155 $words = explode(" ", $row['ARTICLE']);
134 if (count($words) > 1) { 156 if (count($words) > 1) {
135 array_shift($words); 157 array_shift($words);
136 $row['ARTICLE'] = implode(" ", $words); 158 $row['ARTICLE'] = implode(" ", $words);
137 } 159 }
138 - }  
139 - if( isset( $row['BRAND'] ) && isset( self::$importer_id ) ){  
140 - // 2. Уберем брендовый префикс (если он есть)  
141 - $prefix = '';  
142 - // запрос закешируем  
143 - $prefix = ImportersPrefix::getDb()->cache( function ($db, $configuration, $row ) {  
144 - return ImportersPrefix::find()->where([ 'importer_id' => self::$importer_id,  
145 - 'brand' => $row['BRAND'] ])->one(); 160 +
  161 + if( isset( $row['BRAND'] ) && isset( self::$importer_id ) ){
  162 + // 2. Уберем брендовый префикс (если он есть)
  163 + $prefix = '';
  164 + // запрос закешируем
  165 + $prefix = ImportersPrefix::getDb()->cache( function ($db, $configuration, $row ) {
  166 + return ImportersPrefix::find()->where([ 'importer_id' => self::$importer_id,
  167 + 'brand' => $row['BRAND'] ])->one();
146 }); 168 });
147 169
148 - if ($prefix) {  
149 - $row['BRAND'] = str_replace($prefix, "", $row['BRAND']); 170 + if ($prefix) {
  171 + $row['BRAND'] = str_replace($prefix, "", $row['BRAND']);
  172 + }
150 } 173 }
  174 +
  175 + return $row;
  176 +
  177 + } else {
  178 + $words = explode( " ", $value );
  179 + if ( count( $words ) > 1) {
  180 + array_shift( $words );
  181 + $value = implode( " ", $words );
  182 + }
  183 +
  184 + return $value;
151 } 185 }
152 - return $row;  
153 } 186 }
154 187
155 public static function convertToBrand($value) 188 public static function convertToBrand($value)
common/components/parsers/config.php
@@ -17,12 +17,11 @@ @@ -17,12 +17,11 @@
17 'hasKey' => 1, 17 'hasKey' => 1,
18 'configuration' => ["string" => 'DESCR', 18 'configuration' => ["string" => 'DESCR',
19 "float" => 'PRICE', 19 "float" => 'PRICE',
20 - "brand" => 'BRAND[', 20 + "brand" => 'BRAND',
21 "integer" => ['BOX','ADD_BOX'], 21 "integer" => ['BOX','ADD_BOX'],
22 "multiply" => [], 22 "multiply" => [],
23 - "details" => [],  
24 - "article" => []  
25 - 23 + "article" => [],
  24 + "details" => []
26 ] 25 ]
27 ],], 26 ],],
28 27
@@ -36,6 +35,20 @@ @@ -36,6 +35,20 @@
36 "ADD_BOX"=> 'В пути', 35 "ADD_BOX"=> 'В пути',
37 "GROUP" => 'Группа RG' 36 "GROUP" => 'Группа RG'
38 ], 37 ],
  38 +
  39 + 'crosses' => ['class' => 'common\components\parsers\CustomCsvParser',
  40 + 'auto_detect_first_line' => true,
  41 + 'min_column_quantity' => 4,
  42 + 'hasHeaderRow' => true,
  43 + 'keys' =>['ARTICLE', 'CROSS_ARTICLE', 'BRAND', 'CROSS_BRAND'],
  44 + 'converter_conf' => [
  45 + //'class' => ' common\components\parsers\CustomConverter',
  46 + 'hasKey' => 1,
  47 + 'configuration' => [
  48 + "brand" => ['BRAND', 'CROSS_BRAND'],
  49 + "crosses" => [],
  50 + ]
  51 + ],],
39 ], 52 ],
40 'xml' => 53 'xml' =>
41 ['console' => 54 ['console' =>