From: Alexander Smirnov Date: Wed, 31 Jul 2024 05:41:25 +0000 (+0000) Subject: [ERP-88] Загрузка и парсинг xlsx документа. X-Git-Tag: 1.4~58^2 X-Git-Url: https://gitweb.erp-flowers.ru/?a=commitdiff_plain;h=e24254d790d8cb4aef09776d457fefdea8e75fdd;p=erp24_rep%2Fyii-erp24%2F.git [ERP-88] Загрузка и парсинг xlsx документа. --- diff --git a/docker/php/Dockerfile b/docker/php/Dockerfile index d6386a79..2de2f1f1 100644 --- a/docker/php/Dockerfile +++ b/docker/php/Dockerfile @@ -1,11 +1,13 @@ -FROM php:7.4-rc-fpm-alpine +FROM php:8.1-fpm-alpine # Install tools required for build stage RUN apk add --update --no-cache \ bash curl wget rsync ca-certificates openssl openssh git tzdata openntpd \ libxrender fontconfig libc6-compat -RUN apk add --no-cache zlib libpng icu \ +RUN apk add --no-cache zlib libpng icu zip libzip-dev \ && apk add --no-cache --virtual .deps zlib-dev libpng-dev icu-dev \ + && docker-php-ext-configure zip \ + && docker-php-ext-install zip \ && docker-php-ext-install -j$(nproc) gd mysqli pdo pdo_mysql intl calendar opcache \ && apk del .deps diff --git a/erp24/actions/motivation/IndexAction.php b/erp24/actions/motivation/IndexAction.php index 94a06feb..6c5dab83 100644 --- a/erp24/actions/motivation/IndexAction.php +++ b/erp24/actions/motivation/IndexAction.php @@ -2,15 +2,35 @@ namespace yii_app\actions\motivation; +use PhpOffice\PhpSpreadsheet\IOFactory; +use PhpOffice\PhpSpreadsheet\Spreadsheet; use Yii; use yii\base\Action; use yii\base\DynamicModel; use yii\helpers\ArrayHelper; +use yii\web\UploadedFile; use yii_app\records\CityStore; +use yii_app\records\Motivation; +use yii_app\records\MotivationCostsItem; +use yii_app\services\MotivationService; class IndexAction extends Action { public function run() { + if (Yii::$app->request->isPost) { + $file = UploadedFile::getInstanceByName('myfile'); + if ($file) { + $path1 = Yii::getAlias('@uploads') . '/template_plan_temp.xlsx'; + $file->saveAs($path1); + + $data = MotivationService::uploadTemplatePlan($path1); + + return implode('
', $data['errors']); + } else { + return 'not ok'; + } + } + $model = DynamicModel::validateData([ 'store_id' => null, 'year' => null, 'month' => null ], [ @@ -18,10 +38,22 @@ class IndexAction extends Action ]); $model->load(Yii::$app->request->get()); + $motivations = Motivation::find()->all(); + $possibleStoreIds = ArrayHelper::getColumn($motivations, 'store_id'); + $stores = ArrayHelper::map(CityStore::find()->all(), 'id', 'name'); + $stores = array_filter($stores, function ($k, $v) use($possibleStoreIds) { + return in_array($v, $possibleStoreIds); + }, ARRAY_FILTER_USE_BOTH); - $years = [2023, 2024, 2025, 2026]; - $months = ['Январь',' Февраль', 'Март', 'Апрель', 'Май', 'Июнь', 'Июль', 'Август', 'Сентябрь', 'Октябрь', 'Ноябрь', 'Декабрь']; + $possibleYears = ArrayHelper::getColumn($motivations, 'year'); + $years = array_filter(range(2023, 20100), function ($k) use ($possibleYears) { + return in_array($k, $possibleYears); + }); + $possibleMonth = ArrayHelper::getColumn($motivations, 'month'); + $months = array_filter(['Январь',' Февраль', 'Март', 'Апрель', 'Май', 'Июнь', 'Июль', 'Август', 'Сентябрь', 'Октябрь', 'Ноябрь', 'Декабрь'], function ($k, $v) use ($possibleMonth) { + return in_array($v + 1, $possibleMonth); + }, ARRAY_FILTER_USE_BOTH); return $this->controller->render('index', compact('model', 'stores', 'years', 'months')); diff --git a/erp24/composer.json b/erp24/composer.json index f6facf18..b3251528 100644 --- a/erp24/composer.json +++ b/erp24/composer.json @@ -26,7 +26,8 @@ "unclead/yii2-multiple-input": "~2.0", "kartik-v/yii2-widget-fileinput": "dev-master", "yiisoft/yii2-imagine": "^2.3", - "kartik-v/yii2-builder": "dev-master" + "kartik-v/yii2-builder": "dev-master", + "phpoffice/phpspreadsheet": "^2.2" }, "require-dev": { "yiisoft/yii2-debug": "~2.1.0", diff --git a/erp24/records/Motivation.php b/erp24/records/Motivation.php new file mode 100644 index 00000000..e843bd00 --- /dev/null +++ b/erp24/records/Motivation.php @@ -0,0 +1,67 @@ + TimestampBehavior::class, + 'createdAtAttribute' => 'created_at', + 'updatedAtAttribute' => 'updated_at', + 'value' => new Expression('NOW()'), + ] + ]; + } + + /** + * {@inheritdoc} + */ + public function rules() + { + return [ + [['store_id', 'year', 'month'], 'required'], + [['store_id', 'year', 'month'], 'default', 'value' => null], + [['store_id', 'year', 'month'], 'integer'], + ]; + } + + /** + * {@inheritdoc} + */ + public function attributeLabels() + { + return [ + 'id' => 'ID', + 'store_id' => 'Store ID', + 'year' => 'Year', + 'month' => 'Month', + 'updated_at' => 'Updated At', + 'created_at' => 'Created At', + ]; + } +} diff --git a/erp24/records/MotivationValue.php b/erp24/records/MotivationValue.php new file mode 100644 index 00000000..da5dae07 --- /dev/null +++ b/erp24/records/MotivationValue.php @@ -0,0 +1,60 @@ + null], + [['motivation_id', 'motivation_group_id', 'value_id', 'value_int'], 'integer'], + [['value_float'], 'number'], + [['value_type'], 'string', 'max' => 10], + [['value_string'], 'string', 'max' => 255], + ]; + } + + /** + * {@inheritdoc} + */ + public function attributeLabels() + { + return [ + 'id' => 'ID', + 'motivation_id' => 'Motivation ID', + 'motivation_group_id' => 'Motivation Group ID', + 'value_id' => 'Value ID', + 'value_type' => 'Value Type', + 'value_int' => 'Value Int', + 'value_float' => 'Value Float', + 'value_string' => 'Value String', + ]; + } +} diff --git a/erp24/records/MotivationValueGroup.php b/erp24/records/MotivationValueGroup.php new file mode 100644 index 00000000..d4667e53 --- /dev/null +++ b/erp24/records/MotivationValueGroup.php @@ -0,0 +1,46 @@ + 80], + ]; + } + + /** + * {@inheritdoc} + */ + public function attributeLabels() + { + return [ + 'id' => 'ID', + 'name' => 'Name', + 'alias' => 'Alias', + ]; + } +} diff --git a/erp24/services/MotivationService.php b/erp24/services/MotivationService.php new file mode 100644 index 00000000..43b77979 --- /dev/null +++ b/erp24/services/MotivationService.php @@ -0,0 +1,159 @@ +indexBy('code')->all(); + try { + $spreadsheets = IOFactory::load($path); + } catch (\Exception $ex) { + return ['errors' => ['Некорректный файл. Загрузите файл, заполненный по шаблону.']]; + } + $errors = []; + foreach ($spreadsheets->getAllSheets() as $indSS => $spreadSheet) { + $rows = []; + $storeStr = true; + $error = ''; + $motivation = null; + foreach ($spreadSheet->getRowIterator() as $ind => $spreadSheetRow) { + if (in_array($ind, [1, 3])) { + continue; + } + $row = []; + foreach ($spreadSheetRow->getCellIterator() as $spreadSheetRowCell) { + $value = $spreadSheetRowCell->getValue(); + $row []= $value; + if ($row[0] == '###') { + break; + } + } + if ($row[0] == '###') { + break; + } + if ($storeStr) { + if (!is_int($row[0] ?? -1)) { + $error = "Индекс магазина не корректен [0,0] '" . $row[0] . "'"; + break; + } + $store = CityStore::find()->where(['id' => $row[0] ?? -1])->one(); + if (!$store) { + $error = "Не найден магазин с таким индексом [0,0]"; + break; + } elseif ($store->name != ($row[1] ?? 'NO_NAME')) { + $error = "Не найден магазин с таким названием [1,0]" . $row[1] . ' ' . $store->name; + break; + } + $year = ((int)$row[2]) ?? -1; + $month = ((int)$row[3]) ?? -1; + if ($year > 2030 || $year < 2023) { + $error = "Не корректно указан год [2,0]"; + break; + } + if ($month < 1 || $month > 12) { + $error = "Не корректно указан месяц [3,0]"; + break; + } + + $motivation = Motivation::find()->where(['store_id' => $store->id, 'year' => $year, 'month' => $month])->one(); + /** @var $motivation Motivation */ + if (!$motivation) { + $motivation = new Motivation; + $motivation->store_id = $store->id; + $motivation->year = $year; + $motivation->month = $month; + } + + $storeStr = false; + } else { + if (!isset($motivationCostsItems[$row[0] ?? -1])) { + $error = "Не корректен код элемента " . ($row[0] ?? '') . "[$ind,0]"; + break; + } + /** @var $motivationCostsItems MotivationCostsItem[] */ + if ($motivationCostsItems[$row[0]]->name != ($row[1] ?? 'NO_NAME')) { + $error = "Не корректно название элемента '" . ($row[1] ?? '') . "' Ожидается: '" . $motivationCostsItems[$row[0]]->name . "' [$ind,1]"; + break; + } + if (trim($row[2]) == '') { + $rows []= $row; + continue; + } + switch ($motivationCostsItems[$row[0]]->data_type) { + case MotivationCostsItem::DATA_TYPE_INT: { if (is_int($row[2])) { $value = (int)$row[2]; } else { $error = "Не целое число [$ind,2] '" . $row[2] . "'"; }; break; } + case MotivationCostsItem::DATA_TYPE_FLOAT: { if (is_int($row[2]) || is_float($row[2])) { $value = (float)$row[2]; } else {$error = "Не дробь [$ind,2] '" . $row[2] . "'"; } break; } + case MotivationCostsItem::DATA_TYPE_STRING: { $value = $row[2]; break; } + } + if (!empty($error)) { + break; + } + $rows []= $row; + } + } + if (empty($error)) { + if ($motivation) { + $motivation->save(); + if ($motivation->getErrors()) { + $error = json_encode($motivation->getErrors()); + } + } else { + $error = 'Не указан магазин, год и месяц [0,0]'; + } + } + $motivationCostsItemsCount = count(array_keys($motivationCostsItems)); + if (empty($error) && (count($rows) != $motivationCostsItemsCount)) { + $keys = array_keys($motivationCostsItems); + foreach ($rows as $row) { + if (($key = array_search($row[0], $keys)) !== false) { + unset($keys[$key]); + } + } + $keys = array_values($keys); + $error = "Указаны не все элементы справочника. В листе: " . count($rows) . " В справочнике: " . $motivationCostsItemsCount + . " Ожидается, например: '" . $motivationCostsItems[$keys[0]]->name . "'"; + } + if (empty($error)) { + $motivationValueGroupPlan = MotivationValueGroup::find()->where(['alias' => 'plan'])->one(); + /** @var $motivationValueGroupPlan MotivationValueGroup */ + foreach ($rows as $row) { + $motivationValue = MotivationValue::find()->where([ + 'motivation_id' => $motivation->id, + 'motivation_group_id' => $motivationValueGroupPlan->id, + 'value_id' => $row[0] + ])->one(); + /** @var $motivationValue MotivationValue */ + if (!$motivationValue) { + $motivationValue = new MotivationValue; + $motivationValue->motivation_id = $motivation->id; + $motivationValue->motivation_group_id = $motivationValueGroupPlan->id; + $motivationValue->value_id = $row[0]; + } + $motivationValue->value_type = $motivationCostsItems[$row[0]]->data_type; + switch ($motivationValue->value_type) { + case MotivationCostsItem::DATA_TYPE_INT: { $motivationValue->value_int = (int)$row[2]; break; } + case MotivationCostsItem::DATA_TYPE_FLOAT: { $motivationValue->value_float = (float)$row[2]; break; } + case MotivationCostsItem::DATA_TYPE_STRING: { $motivationValue->value_string = '' . $row[2]; break; } + } + $motivationValue->save(); + if ($motivationValue->getErrors()) { + $error = json_encode($motivationValue->getErrors()); + break; + } + } + } + if (!empty($error)) { + $errors []= ($indSS + 1) . ":" . $error; + } + } + + return compact('errors'); + } +} \ No newline at end of file diff --git a/erp24/uploads/template_plan.xlsx b/erp24/uploads/template_plan.xlsx new file mode 100644 index 00000000..b5e076cb Binary files /dev/null and b/erp24/uploads/template_plan.xlsx differ diff --git a/erp24/views/motivation/index.php b/erp24/views/motivation/index.php index 04a1860d..5a91c905 100644 --- a/erp24/views/motivation/index.php +++ b/erp24/views/motivation/index.php @@ -10,6 +10,8 @@ use yii\base\DynamicModel; /** @var $years array */ /** @var $months array */ +$this->registerJsFile('/js/motivation/index.js', ['position' => \yii\web\View::POS_END]); + ?>
@@ -54,6 +56,10 @@ use yii\base\DynamicModel;
'btn btn-secondary btn-sm'])?>
+
+
'btn btn-success btn-sm', + 'onclick' => 'openUploadDictionary();'])?>
+
diff --git a/erp24/web/js/motivation/index.js b/erp24/web/js/motivation/index.js new file mode 100644 index 00000000..e7432015 --- /dev/null +++ b/erp24/web/js/motivation/index.js @@ -0,0 +1,67 @@ +/* jshint esversion: 8 */ + +const param10 = $("meta[name=csrf-param]").attr("content"); +const token10 = $("meta[name=csrf-token]").attr("content"); + +/* jshint unused: false */ +function openUploadDictionary() { + 'use strict' + const $mainModal = $('#mainModal'); + const $modalBody = $mainModal.find('.modal-body'); + const $modalFooter = $mainModal.find('.modal-footer'); + $mainModal.find('.close').on('click', () => { $mainModal.modal('hide'); }); + $mainModal.find('.modal-title').html('Загрузка плановых значений'); + $modalFooter.html(''); + $modalBody.html('
' + + '
' + + '
' + + '
' + + '
' + + '
' + + '
Скачать шаблон
' + + '
'); + const browse = $modalBody.find('input[type=file]').get(0); + const btn = $modalBody.find('input[type=submit]').get(0); + const info = $modalBody.find('#infoModal').get(0); + const form = $modalBody.find('form').get(0); + async function UploadDict() { + info.innerHTML = 'загрузка...'; + if (browse.files[0].size > 200000) { + info.innerHTML = 'Некорректный файл. Загрузите файл, заполненный по шаблону.'; + return; + } + const formData = new FormData(form); + formData.append(param10, token10); + try { + const response = await fetch("/motivation/index", { + method: "POST", + body: formData, + }); + const text = await response.text(); + if (text === 'not ok') { + info.innerHTML = 'Не смог загрузить файл'; + } else if (text.replaceAll('
', '') === '') { + info.innerHTML = 'Успешно загружен'; + } else { + info.innerHTML = '' + text + ''; + } + } catch (e) { + console.error(e); + } + } + browse.addEventListener('change', (event) => { + event.preventDefault(); + event.stopPropagation(); + UploadDict(); + }) + + btn.addEventListener('click', (event) => { + event.preventDefault(); + event.stopPropagation(); + browse.click(); + }) + + $mainModal.modal('show'); +} \ No newline at end of file