From e24254d790d8cb4aef09776d457fefdea8e75fdd Mon Sep 17 00:00:00 2001 From: Alexander Smirnov Date: Wed, 31 Jul 2024 05:41:25 +0000 Subject: [PATCH] =?utf8?q?[ERP-88]=20=D0=97=D0=B0=D0=B3=D1=80=D1=83=D0=B7?= =?utf8?q?=D0=BA=D0=B0=20=D0=B8=20=D0=BF=D0=B0=D1=80=D1=81=D0=B8=D0=BD?= =?utf8?q?=D0=B3=20xlsx=20=D0=B4=D0=BE=D0=BA=D1=83=D0=BC=D0=B5=D0=BD=D1=82?= =?utf8?q?=D0=B0.?= MIME-Version: 1.0 Content-Type: text/plain; charset=utf8 Content-Transfer-Encoding: 8bit --- docker/php/Dockerfile | 6 +- erp24/actions/motivation/IndexAction.php | 36 ++++- erp24/composer.json | 3 +- erp24/records/Motivation.php | 67 ++++++++++ erp24/records/MotivationValue.php | 60 +++++++++ erp24/records/MotivationValueGroup.php | 46 +++++++ erp24/services/MotivationService.php | 159 +++++++++++++++++++++++ erp24/uploads/template_plan.xlsx | Bin 0 -> 78855 bytes erp24/views/motivation/index.php | 6 + erp24/web/js/motivation/index.js | 67 ++++++++++ 10 files changed, 445 insertions(+), 5 deletions(-) create mode 100644 erp24/records/Motivation.php create mode 100644 erp24/records/MotivationValue.php create mode 100644 erp24/records/MotivationValueGroup.php create mode 100644 erp24/services/MotivationService.php create mode 100644 erp24/uploads/template_plan.xlsx create mode 100644 erp24/web/js/motivation/index.js 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 0000000000000000000000000000000000000000..b5e076cb248d31801aeddbcfd74ad5dac784fd1c GIT binary patch literal 78855 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