]> gitweb.erp-flowers.ru Git - erp24_rep/yii-erp24/.git/commitdiff
[ERP-88] Загрузка и парсинг xlsx документа.
authorAlexander Smirnov <aleksandr.smirnov@erp-flowers.ru>
Wed, 31 Jul 2024 05:41:25 +0000 (05:41 +0000)
committerMarina Zozirova <marina.zozirova@erp-flowers.ru>
Wed, 31 Jul 2024 05:41:25 +0000 (05:41 +0000)
docker/php/Dockerfile
erp24/actions/motivation/IndexAction.php
erp24/composer.json
erp24/records/Motivation.php [new file with mode: 0644]
erp24/records/MotivationValue.php [new file with mode: 0644]
erp24/records/MotivationValueGroup.php [new file with mode: 0644]
erp24/services/MotivationService.php [new file with mode: 0644]
erp24/uploads/template_plan.xlsx [new file with mode: 0644]
erp24/views/motivation/index.php
erp24/web/js/motivation/index.js [new file with mode: 0644]

index d6386a79a16626bf29e30f4477718487cbd06ad7..2de2f1f105183566c37cd6e7530c230f8cbd7a1b 100644 (file)
@@ -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
 
index 94a06feb812d7dbcaf6123d2a71a30c91761c62a..6c5dab83b32ff85b516fff2a7f0b365507d4cdb5 100644 (file)
@@ -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('<br>', $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'));
index f6facf18f5670598c4ac771d7c6e2b4c15a63d1d..b32515289e4528bace9c92258546c89ffd987a9c 100644 (file)
@@ -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 (file)
index 0000000..e843bd0
--- /dev/null
@@ -0,0 +1,67 @@
+<?php
+
+namespace yii_app\records;
+
+use Yii;
+use yii\behaviors\TimestampBehavior;
+use yii\db\Expression;
+
+/**
+ * This is the model class for table "motivation".
+ *
+ * @property int $id
+ * @property int $store_id ID магазина
+ * @property int $year Год
+ * @property int $month Месяц
+ * @property string $updated_at Дата изменения записи
+ * @property string $created_at Дата создания записи
+ */
+class Motivation extends \yii\db\ActiveRecord
+{
+    /**
+     * {@inheritdoc}
+     */
+    public static function tableName()
+    {
+        return 'motivation';
+    }
+
+    public function behaviors()
+    {
+        return [
+            [
+                'class' => 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 (file)
index 0000000..da5dae0
--- /dev/null
@@ -0,0 +1,60 @@
+<?php
+
+namespace yii_app\records;
+
+use Yii;
+
+/**
+ * This is the model class for table "motivation_value".
+ *
+ * @property int $id
+ * @property int $motivation_id ID motivation
+ * @property int $motivation_group_id ID motivation
+ * @property int $value_id ID motivation
+ * @property string $value_type тип значения
+ * @property int|null $value_int value int
+ * @property float|null $value_float value float
+ * @property string|null $value_string value string
+ */
+class MotivationValue extends \yii\db\ActiveRecord
+{
+    /**
+     * {@inheritdoc}
+     */
+    public static function tableName()
+    {
+        return 'motivation_value';
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    public function rules()
+    {
+        return [
+            [['motivation_id', 'motivation_group_id', 'value_id', 'value_type'], 'required'],
+            [['motivation_id', 'motivation_group_id', 'value_id', 'value_int'], 'default', 'value' => 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 (file)
index 0000000..d4667e5
--- /dev/null
@@ -0,0 +1,46 @@
+<?php
+
+namespace yii_app\records;
+
+use Yii;
+
+/**
+ * This is the model class for table "motivation_value_group".
+ *
+ * @property int $id
+ * @property string $name Название
+ * @property string $alias Алиас
+ */
+class MotivationValueGroup extends \yii\db\ActiveRecord
+{
+    /**
+     * {@inheritdoc}
+     */
+    public static function tableName()
+    {
+        return 'motivation_value_group';
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    public function rules()
+    {
+        return [
+            [['name', 'alias'], 'required'],
+            [['name', 'alias'], 'string', 'max' => 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 (file)
index 0000000..43b7797
--- /dev/null
@@ -0,0 +1,159 @@
+<?php
+
+namespace yii_app\services;
+
+use PhpOffice\PhpSpreadsheet\IOFactory;
+use yii_app\records\Motivation;
+use yii_app\records\MotivationValue;
+use yii_app\records\MotivationValueGroup;
+use yii_app\records\CityStore;
+use yii_app\records\MotivationCostsItem;
+
+class MotivationService
+{
+    public static function uploadTemplatePlan($path) {
+        $motivationCostsItems = MotivationCostsItem::find()->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 (file)
index 0000000..b5e076c
Binary files /dev/null and b/erp24/uploads/template_plan.xlsx differ
index 04a1860dfaf4636d79304d40068134922123c4ca..5a91c9052eb01c485696b1ca66dc7e616e84b384 100644 (file)
@@ -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]);
+
 ?>
 
 <div class="motivationIndex m-5">
@@ -54,6 +56,10 @@ use yii\base\DynamicModel;
             <div class="d-flex justify-content-around align-items-center gap-2">
                 <div class="mb-3"><?= Html::submitButton('Применить', ['class' => 'btn btn-secondary btn-sm'])?></div>
             </div>
+            <div class="d-flex justify-content-around align-items-center gap-2">
+                <div class="mb-3"><?= Html::button('Загрузка плановых значений', ['class' => 'btn btn-success btn-sm',
+                        'onclick' => 'openUploadDictionary();'])?></div>
+            </div>
         </div>
     </div>
 
diff --git a/erp24/web/js/motivation/index.js b/erp24/web/js/motivation/index.js
new file mode 100644 (file)
index 0000000..e743201
--- /dev/null
@@ -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('<div class="row"><div class="col-12">'
+                   + '<form class="d-flex justify-content-left align-items-center" enctype="multipart/form-data">'
+                   + '<div class="d-none"><input type="file" name="myfile" accept=".xlsx"/></div>'
+                   + '<div><input class="btn btn-success btn-sm" type="submit" value="Загрузить" /></div></form></div></div>'
+                   + '<div class="row mt-2"><div class="col-12" id="infoModal"></div></div>'
+                   + '<div class="row mt-2"><div class="col-12 border-2 bg-gray-200"></div></div>'
+                   + '<div class="row mt-5"><div class="col-12" style="font-size: 1rem;">Скачать шаблон</div></div>'
+                   + '<div class="row mt-2"><div class="col-12">'
+                   + '<a href="/files/download?url=/uploads/template_plan.xlsx" style="font-size: 1rem; color: #23389c;'
+                   + ' text-decoration: underline; " target="_blank">Шаблон. Загрузка плановых значений</a></div></div>');
+    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 = '<span style="color:red">Некорректный файл. Загрузите файл, заполненный по шаблону.</span>';
+            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 = '<span style="color:red">Не смог загрузить файл</span>';
+            } else if (text.replaceAll('<br>', '') === '') {
+                info.innerHTML = '<span style="color:green">Успешно загружен</span>';
+            } else {
+                info.innerHTML = '<span style="color:red">' + text + '</span>';
+            }
+        } 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