3 namespace app\controllers;
10 use app\models\Locations;
11 use app\models\ItemsSearch;
12 use app\models\MovingSearch;
13 use app\models\Status;
15 use yii\web\Controller;
16 use yii\web\NotFoundHttpException;
17 use yii\web\UploadedFile;
18 use yii\filters\VerbFilter;
23 #require "/vendor/phpoffice/phpexcel/Classes/PHPExcel.php";
26 * ItemsController implements the CRUD actions for Items model.
28 class ItemsController extends Controller
33 public function behaviors()
37 'class' => VerbFilter::className(),
39 'delete' => [ 'POST' ],
46 * Добавление предмета/оборудование если его нет
47 * @param array $options
52 * string|NULL 'netname'
55 * string|NULL 'serial'
56 * string|NULL 'product'
57 * string|NULL 'modelnumber'
58 * @return integer|FALSE
60 public static function addIfNeed($options)
64 'error' => Yii::t('items', 'Items: Key field missing "invent" :') . print_r($options, TRUE),
66 // Если указан инвентарный номер
67 if (is_array($options) && isset($options[ 'invent' ]))
70 ->where([ 'invent' => $options[ 'invent' ]]); // Ищем оборудование с инвентарным номером.
71 // Если указан серийный номер
72 if (isset($options[ 'serial' ])) {
73 $item = $item->andWhere([ 'like', 'serial', $options[ 'serial' ]]); // Ищем дополнительно с серийным номером
75 $item = $item->all(); // Получаем все записи
77 if (count($item) > 0) // Записи найдены, выводим первую совпавшую
79 $result[ 'id' ] = $item[ 0 ]->id;
80 $result[ 'error' ] = '';
84 // Внесённого оборудования не найдено. Добавим новую запись
85 if (isset($options[ 'model' ]))
87 $model = ModelsController::addIfNeed($options);
88 if ($model[ 'id' ] === FALSE)
90 $result[ 'error' ] .= '<br />' . $model[ 'error' ];
94 // Создаём новую запись предмета/оборудования
96 $item->name = isset($options[ 'netName' ]) ? $options[ 'netName' ] : NULL; // Сетевое имя
98 $item->model_id = $model[ 'id' ]; // идентификатор модели (Подготовлено для преобразования)
99 $item->invent = isset($options[ 'invent' ]) ? $options[ 'invent' ] : NULL; // Инвентарный номер
100 $item->comment = isset($options[ 'comment' ]) ? $options[ 'comment' ] : NULL; // Коментарий
101 $item->os = isset($options[ 'os' ]) ? $options[ 'os' ] : NULL; // Операционная система
102 $item->mac = isset($options[ 'mac' ]) ? $options[ 'mac' ] : NULL; // MAC-адрес
103 $item->serial = isset($options[ 'serial' ]) ? $options[ 'serial' ] : NULL; // Серийный номер
104 $item->checked = 2; // Не инвентризирован (требует внимания после импорта)
106 if ($item->validate() && $item->save())
108 $result[ 'id' ] = $item->id; // Возвращаем идентификатор записанного оборудования
109 $result[ 'error' ] = '';
113 $result[ 'error' ] .= Yii::t('items', 'Items: Failed to add entry: ') . print_r($item->errors, TRUE) . '<br />';
123 * Формирование PDF файла для печати QR-кодов для наклеек
124 * @param integer|array|null id
127 public function actionPrint()
129 if (! User::canPermission('takingInventory') ) {
130 return Yii::$app->response->redirect(['site/index']);
132 // Yii::$app->response->format = \yii\web\Response::FORMAT_RAW;
133 // Yii::$app->response->headers->add('Content-Type', 'application/pdf');
134 // Список предметов/оборудования, если есть
135 $id = Yii::$app->request->get('id');
137 $models = Items::find();
141 $models = $models->where([ 'in', 'id', $id ]); // Несколько предметов/оборудования
144 $models = $models->where([ 'id' => $id ]); // Один предмет/оборудование
146 $models = $models->all(); // Формирование списка
148 $pdf = Yii::$app->pdf; // Pабота с PDF
150 $pdf->methods[ 'SetHeader' ] = ''; // Yii::t('items', 'Items');
151 $pdf->methods[ 'SetFooter' ] = ''; // ['{PAGENO}'];
153 $pdf->marginLeft = 5;
154 $pdf->marginRight = 5;
156 $pdf->marginBottom = 15;
157 // Имя файла для выгрузки, по умолчанию document.pdf
158 $pdf->filename = Yii::t('app', Yii::$app->name) . ' (' . Yii::t('items', 'Items') . ').pdf';
160 // Заполнение страницы данными
161 $pdf->content = $this->renderPartial('print', [ 'models' => $models ]);
165 return $pdf->render();
169 * Процедура начала инвентаризации.
172 public function actionStart_checking()
174 // Проверка доступа для проведения инвентаризации
175 if (! User::canPermission('takingInventory') ) {
176 // Переход к списку предметов/оборудования, если доступ не разрешён.
177 return $this->redirect(['index']);
179 // Запрос на получение списка идентификаторов предметов/оборудования, которые списаны
180 $modelS = Moving::find()
183 ->Where([ 'ilike', Status::tableName() . '.name', 'Списано' ])
184 ->orWhere( [ 'checked' => 2 ] );
186 // Получаем список всех предметов/оборудования, кроме списанного
187 $model = Items::find()
189 ->innerJoin([ 'm' => $modelS ], 'not m.item_id = id')
192 // Устанавливаем флаг непроинвентаризированных для всех предметов/оборудования из полученного списка.
193 Items::updateAll([ 'checked' => 0 ], [ 'in', 'id', $model ]);
195 // Переход к списку предметов/оборудования.
196 return $this->redirect([ 'index' ]);
201 * @param string|null $qrcheck считанный QR-код
204 public function actionCheck()
206 // Проверка доступа для проведения инвентаризации
207 if (! User::canPermission('takingInventory') ) {
208 // Показ стартовой страницы, если доступ не разрешён.
209 return $this->redirect(['site/index']);
212 $model = new Check();
214 if ($model->load(Yii::$app->request->post()))
216 if ((! empty($model->qrcheck)) && strpos($model->qrcheck, ',') !== false)
218 $keys = explode(',', $model->qrcheck);
219 $color=''; // Цветовая метка по умолчанию
220 // Получаем все предметы/оборудование с совпадением инвентарного и серийного номеров
221 $items = Items::find()->where([ 'invent' => trim($keys[ 0 ]), 'serial' => trim($keys[ 1 ]) ])->all();
222 if (count($items) > 0 )
224 foreach ($items as $row)
226 if ( $row->checked == 1 )
228 // Этот инвентарный номер был учтён
229 $color = ' color="#FF0000"';
231 // Ищем местоположение
232 $moving_id = Moving::find()->select('MAX(id) as id')->groupBy([ 'item_id' ])->where([ 'item_id' => $row->id ])->one()->id;
233 $moving = Moving::find()->where([ 'id' => $moving_id ])->one();
234 // Показываем название и местоположение
235 $message .= $row->models->name . ' (' . $moving->locations->name . ' [' . $moving->regions->name . '])';
240 // Неизвестный инвентарный номер
241 $color = ' color = "#FF8830"';
242 $message .= Yii::t('items', 'Not found!') . ' ' . Yii::t('items', 'Inventory number') . ': ' . trim($keys[ 0 ]);
244 // Отмечаем проинвентаризированными все найденные предметы/оборудование
245 Items::updateAll([ 'checked' => 1 ], [ 'invent' => trim($keys[ 0 ]), 'serial' => trim($keys[ 1 ]) ]);
248 if ( $color == ' color = "#FF8830"')
250 $message = '<font' . $color . '>' . $message . '</font>';
254 $message = '<font' . $color . '>' . Yii::t('items', 'Checked item(s): ') . $message . '</font>';
256 $model->qrcheck = '';
259 $searchModel = new ItemsSearch();
260 //$dataProvider = $searchModel->noinvent($model);
261 $dataProvider = $searchModel->noinvent(Yii::$app->request->queryParams);
263 return $this->render('check', [
264 'message' => $message,
266 'searchModel' => $searchModel,
267 'dataProvider' => $dataProvider,
272 * Список всех предметов/оборудования.
275 public function actionIndex()
277 if (! User::canPermission('createRecord') )
279 return $this->redirect(['site/index']);
281 if (isset($_GET[ 'pageSize' ]))
283 Yii::$app->session['pageSize'] = (int) $_GET[ 'pageSize' ];
284 unset($_GET[ 'pageSize' ]);
286 $searchModel = new ItemsSearch();
287 if (isset(Yii::$app->request->queryParams['id']))
289 $id = Yii::$app->request->queryParams['id'];
290 $dataProvider = $searchModel->search(Yii::$app->request->queryParams);
291 $pageSize = $dataProvider->pagination->pageSize;
292 $dataProvider->pagination = FALSE;
293 $rows = $dataProvider->getModels();
295 foreach ($rows as $key => $val)
299 $page = ceil(($key + 1) / $pageSize);
303 return $this->redirect(['index', 'page' => $page]);
305 $dataProvider = $searchModel->search(Yii::$app->request->queryParams);
307 return $this->render('index', [
308 'searchModel' => $searchModel,
309 'dataProvider' => $dataProvider,
314 * Импортирование строк товаров из массива
316 public function doImport($arrayRows)
318 // Инициализация счётчиков
320 'countRows' => count($arrayRows),
321 'countImported' => 0,
327 // Проверка наличия ключевых полей
328 if ((!isset($arrayRows[ 0 ][ 'model' ]))
329 || (!isset($arrayRows[ 0 ][ 'type' ]))
330 || (!isset($arrayRows[ 0 ][ 'invent' ]))
331 || (!isset($arrayRows[ 0 ][ 'location' ]))
332 || (!isset($arrayRows[ 0 ][ 'region' ]))
333 || (!isset($arrayRows[ 0 ][ 'date' ]))
336 // Сообщение об ошибке
337 $arrayReturn[ 'countErrors' ] = count($arrayRows);
338 $arrayReturn[ 'errors' ] .= '<br />' . Yii::t('import', 'Skip all. Key column(s) "model", "type", "invent", "location", "region", "date" not found: ') . print_r($arrayRows[0], TRUE);
342 // Просмотрим весь массив
343 foreach($arrayRows as $row)
345 // ПОлучим местоположения
346 $location = LocationsController::addIfNeed($row); // Получение идентификатора расположения
347 if ( $location[ 'id' ] === FALSE)
350 $arrayReturn[ 'countErrors' ]++;
351 $arrayReturn[ 'errors' ] .= '<br />' . $location[ 'error' ];
355 // Попробуем найти или добавить предмет/оборудование
356 $item = $this->addIfNeed($row);
357 if ($item[ 'id' ] === FALSE)
359 $arrayReturn[ 'countErrors' ]++;
360 $arrayReturn[ 'errors' ] .= '<br />' . $item[ 'error' ];
364 // Проверка, что предмет/оборудование уже были в базе
365 $item = Items::find()->where([ 'id' => $item[ 'id' ]])->one();
366 if ($item->checked < 2)
368 $arrayReturn[ 'countExists' ]++;
372 $state = isset($row[ 'status' ]) ? StatusController::addIfNeed($row) : StatusController::addIfNeed([ 'status' => 'Склад' ]);
373 if ( $state[ 'id' ] === FALSE )
376 $arrayReturn[ 'countErrors' ]++;
377 $arrayReturn[ 'errors' ] .= '<br />' . $state[ 'error' ];
381 // Новый предмет/оборудование. Пробуем добавить первое перемещение
382 // Проверка что последнее перемещение такое же, как и импортируемое
383 $moving = Moving::find()->where([ 'item_id' => $item[ 'id' ]])->all();
385 if (count($moving) > 0)
387 // Признак, что последнее перемещение совпало с импортируемым
388 $flag = $moving[ count($moving) - 1 ]->location_id == $location[ 'id' ];
390 // Проверим, что существующая запись перемещения больше добавляемой
391 $moving = Moving::find()->where([ 'item_id' => $item[ 'id' ]])->andWhere([ '>', 'date', $row[ 'date' ]])->all();
392 if ($flag || count($moving) > 0)
394 // Добавлять запись до существующих нельзя.
395 $arrayReturn[ 'countExists' ]++;
399 // Проверим, есть ли уже такое перемещение
400 $moving = Moving::find()->where([ 'item_id' => $item[ 'id' ], 'date' => $row[ 'date' ]])->orderBy([ 'id' => SORT_ASC ])->all();
401 if (count($moving) == 0)
403 // Такой транзакции нет ещё.
404 $moving = new Moving();
405 $moving->date = $row[ 'date' ];
406 $moving->state_id = $state[ 'id' ];
407 $moving->item_id = $item[ 'id' ];
408 $moving->location_id = $location[ 'id' ];
409 $moving->comment = Yii::t('import', 'Import: {comment}', $row);
411 if ($moving->validate() && $moving->save())
413 // Записаали первое движение
414 $arrayReturn[ 'countImported' ]++;
418 // Запись не удалась, пробуем удалить предмет/оборудование
419 Items::find()->where([ 'id' => $item[ 'id' ], 'checked' => 2 ])->one()->delete();
421 $arrayReturn[ 'countErrors' ]++;
422 $arrayReturn[ 'errors' ] .= '<br />' . Yii::t('import', 'Moving: {date} (') . $moving->errors['date'][0]. Yii::t('import', '), Inventory number:{invent}, model: {model}, location: {location} ( {region} )' , $row);
427 // Такое перемещение уже было
428 $arrayReturn[ 'countExists' ]++;
429 // Удаление дубликатов, если вдруг они образовались
430 if (count($moving) > 1)
432 // переберём все записи со второй и удалим их.
433 for ($i = 1; $i < count($moving); $i++)
435 $moving[$i]->delete();
448 // Возврат результата импорта
453 * Импорт данных из файла csv
454 * Структура файла данных при выгрузке из 1С: (Колонки могут меняться.
455 * | № п/п | | Предмет/оборудование | | | | | | | Инвентарный номер | Материально отвественное лицо | | | Место размещения | Регион/подразделение | Количество |
456 * Так как 1С из коробки не умеет выгружать форму в .csv, то приходится сначала выгрузить в .xls(x), и уже из MS Excel/Lible office Calc сохранять в .csv
458 public function actionImport()
460 if (! User::canPermission('updateRecord') ) {
461 return $this->redirect(['site/index']);
463 $model = new Import();
465 $searchModel = new ItemsSearch();
466 $dataProvider = $searchModel->search(Yii::$app->request->queryParams);
467 if (Yii::$app->request->isPost)
472 'npp' => Yii::t('import', 'No. in order'),
473 'model' => Yii::t('import', 'Primary means'),
474 'netname' => Yii::t('import', 'Network name'),
475 'invent' => Yii::t('import', 'Inventory number'),
476 'comment' => Yii::t('import', 'Financially responsible person'),
477 'os' => Yii::t('import', 'Operation system'),
478 'mac' => Yii::t('import', 'MAC address'),
479 'serial' => Yii::t('import', 'Serial number'),
480 'product' => Yii::t('import', 'Product number'),
481 'modelnumber' => Yii::t('import', 'Model number'),
482 'date' => Yii::t('import', 'Date of acceptance for registration'),
483 'location' => Yii::t('import', 'Location'),
484 'region' => Yii::t('import', 'Region'),
485 'type' => Yii::t('import', 'Type'),
486 'status' => Yii::t('import', 'State'),
488 $model->filecsv = UploadedFile::getInstance($model, 'filecsv');
489 if ($model->upload())
491 $fileName = 'upload/' . $model->filecsv->baseName . '.' . $model->filecsv->extension;
492 $handle = fopen($fileName, 'r');
493 if ($handle !== FALSE)
495 if (strcasecmp($model->filecsv->extension, 'csv') === 0 )
497 // Построчное чтение CSV файла
498 while (($row = fgetcsv($handle, 2048, ';')) !== false )
500 // Пока не собраны индексы столбцов из шапки
501 if (count($columns) == 0)
503 // Ищем строку с заголовком таблицы
504 if ( stripos($row[0], $columnsNames[ 'npp' ]) !== FALSE )
506 // Перебираем все колонки
507 foreach ($row as $key => $item)
509 // Перебираем все названия заголовков колонок
510 foreach($columnsNames as $name => $text)
512 // Если название совпало,
513 if (stripos($item, $text) !== FALSE)
515 // Сохраняем индек колонки
516 $columns[ $name ] = $key;
524 // Перебираем предметы/оборудование (Номер по порядку должен быть целым числом)
525 if (ctype_digit(str_replace(' ', '', $row[ $columns[ 'npp' ]])))
527 // Заполняем очередную строку для таблицы
529 foreach ($columns as $key => $index)
531 $line[ $key ] = trim(str_replace("\t", ' ', $row[ $index ])); // Заменяем табуляторы на пробелы и удаляем ведущие и ведомые пробелы.
534 if (isset($line[ 'date' ]))
536 if ($line[ 'date' ] == '#NULL!') $line[ 'date' ] = date('d.m.Y');
540 $line[ 'date' ] = date('d.m.Y');
542 array_push($rows, $line);
545 } // Перебор строк файла
549 $inputFileType = \PHPExcel_IOFactory::identify($fileName); // Получение типа данных в файле
550 $excelReader = \PHPExcel_IOFactory::createReader($inputFileType); // Создание потока чтения из файла
551 $excelObj = $excelReader->load($fileName); // Открытие файла
552 $worksheet = $excelObj->getSheet(0); // Работаем только с первым листом (обычно туда выгружает 1С)
555 // Цикл по всем строкам
556 foreach ($worksheet->getRowIterator() as $row)
558 $cellIterator = $row->getCellIterator(); // Получаем итератор ячеек в строке
559 $cellIterator->setIterateOnlyExistingCells(FALSE); // Указываем проверять даже не установленные ячейки
561 if (count($columns) == 0) // Пока не найдена шапка, проверяем строку
564 foreach ($cellIterator as $key => $item)
566 if (($key == 'A') && (stripos($item->getCalculatedValue(), $columnsNames[ 'npp' ]) !== FALSE)) $flag = TRUE;
569 foreach ($columnsNames as $name => $text)
571 if (stripos($item->getCalculatedValue(), $text) !== FALSE)
573 $columns[ $name ] = $key;
583 foreach ($cellIterator as $key => $item)
585 if ($key == $columns[ 'npp' ])
587 $npp = str_replace(' ', '', $item->getCalculatedValue());
588 if (ctype_digit($npp)) $flag = TRUE;
592 foreach($columns as $keym => $index)
594 if ($index == $key) $line[ $keym ] = trim(str_replace("\t", ' ', $item->getCalculatedValue()));
600 if (isset($line[ 'date' ]))
602 if ($line[ 'date' ] == '#NULL!') $line[ 'date' ] = date('d.m.Y');
606 $line[ 'date' ] = date('d.m.Y');
608 array_push($rows, $line);
614 $res = $this->doImport($rows);
616 $message .= Yii::t('items', 'Read {countRows} records.<br />Imported {countImported} Items.<br />Exists {countExists} Items.<br />Error read {countErrors} records.<br />{errors}', $res);
619 return $this->render('import',[
620 'message' => $message,
622 'searchModel' => $searchModel,
623 'dataProvider' => $dataProvider,
628 * Показ одного предмета/оборудования. (не используется)
631 * @throws NotFoundHttpException если предмет/оборудование отсутствует
633 public function actionView($id)
635 if (! User::canPermission('updateRecord') ) {
636 return $this->redirect([ 'index', 'id' => $id ]);
638 return $this->render('view', [
639 'model' => $this->findModel($id),
644 * Создание нового предмета/оборудования.
647 public function actionCreate()
649 if (! User::canPermission('createRecord') ) {
650 return $this->redirect([ 'site/index' ]);
652 $model = new Items(); // Новый предмет/оборудование
654 $modelm = new Moving();
655 if ($model->load(Yii::$app->request->post()) && $model->save())
657 // Удалось сохранить, создаём первую запись движения
658 if ($modelm->load(Yii::$app->request->post()))
660 $modelm->item_id = $model->id;
661 $modelm->comment = 'Поступление';
663 if ( $modelm->save() ) // Пробуем сохранить движение
665 return $this->redirect([ 'index', 'id' => $model->id ]); // Если удалось, показываем список оборудования
668 $this->findModel($model->id)->delete(); // Иначе удаляем созданную запись предмета/оборудования
669 unset($model->id); // Очищаем идентификатор предмета/оборудования
670 $model->isNewRecord = TRUE;
671 return $this->render('create', [ // Показываем форму создания нового предмета/оборудования
678 $this->findModel($model->id)->delete(); // Иначе удаляем созданную запись предмета/оборудования
679 unset($model->id); // Очищаем идентификатор предмета/оборудования
680 $model->isNewRecord = TRUE;
681 return $this->render('create', [ // Показываем форму создания нового предмета/оборудования
686 } else // не удалось сохранить - отображаем форму создания нового предмета/оборудования
688 return $this->render('create', [
697 * Добавление новой записи как копии существующей
698 * @param integer $id - идентификатор существующей записи предмета/оборудования
701 public function actionAddcopy($is)
703 if (! User::canPermission('createRecord') )
705 return $this->redirect([ 'site/index' ]);
707 $origin = Items::find()->where([ 'id' => $is ])->one();
708 $model = new Items();
710 $modelm = new Moving();
711 if ($model->load(Yii::$app->request->post()) && $model->save())
713 // Удалось сохранить, создаём первую запись движения
714 if ($modelm->load(Yii::$app->request->post()))
716 $modelm->item_id = $model->id;
717 $modelm->comment = 'Поступление';
719 if ( $modelm->save() ) // Пробуем сохранить движение
721 return $this->redirect([ 'index', 'id' => $model->id ]); // Если удалось, показываем список оборудования
724 $this->findModel($model->id)->delete(); // Иначе удаляем созданную запись предмета/оборудования
725 unset($model->id); // Очищаем идентификатор предмета/оборудования
726 $model->isNewRecord = TRUE;
727 return $this->render('create', [ // Показываем форму создания нового предмета/оборудования
734 $this->findModel($model->id)->delete(); // Иначе удаляем созданную запись предмета/оборудования
735 unset($model->id); // Очищаем идентификатор предмета/оборудования
736 $model->isNewRecord = TRUE;
737 return $this->render('create', [ // Показываем форму создания нового предмета/оборудования
745 $model->isNewRecord = TRUE;
746 $model->model_id = $origin->model_id;
747 $model->invent = $origin->invent;
748 return $this->render('create', [
756 * Изменение существующего предмета/оборудвания.
757 * Если премет/обрудование сохранён, то возвращаемся на страницу списка всех предметов/оборудования.
760 * @throws NotFoundHttpException если предмет/оборудование отсутствует
762 public function actionUpdate($id)
764 if (! User::canPermission('updateRecord') ) {
765 return $this->redirect(['index']);
767 $model = $this->findModel($id);
769 if ($model->load(Yii::$app->request->post()) && $model->save())
771 return $this->redirect([ 'index', 'id' => $model->id ]);
774 $searchModelM = new MovingSearch([ 'item_id' => $model->id ]);
775 $dataProviderM = $searchModelM->search(Yii::$app->request->queryParams);
777 return $this->render('update', [
778 'searchModelM' => $searchModelM,
779 'dataProviderM' => $dataProviderM,
785 * Удаляет сушествующий предмет/оборудование.
786 * Если премет/обрудование удалён, то возвращаемся на страницу списка всех предметов/оборудования.
789 * @throws NotFoundHttpException if the model cannot be found
791 public function actionDelete($id)
793 if (! User::canPermission('updateRecord') ) {
794 return $this->redirect(['site/index']);
796 $this->findModel($id)->delete();
798 return $this->redirect([ 'index' ]);
802 * Finds the Items model based on its primary key value.
803 * If the model is not found, a 404 HTTP exception will be thrown.
805 * @return Items the loaded model
806 * @throws NotFoundHttpException если предмет/оборудование отсутствует
808 protected function findModel($id)
810 if (($model = Items::findOne($id)) !== null)
815 throw new NotFoundHttpException(Yii::t('app', 'The requested page does not exist.'));