]> gitweb.erp-flowers.ru Git - erp24_rep/yii-erp24/.git/commitdiff
Настройка в кроне
authorfomichev <vladimir.fomichev@erp-flowers.ru>
Mon, 30 Jun 2025 15:03:30 +0000 (18:03 +0300)
committerfomichev <vladimir.fomichev@erp-flowers.ru>
Mon, 30 Jun 2025 15:03:30 +0000 (18:03 +0300)
erp24/commands/CronController.php
erp24/controllers/CategoryPlanController.php
erp24/jobs/RebuildAutoplannogramJob.php
erp24/services/AutoPlannogrammaService.php
erp24/views/category-plan/new.php
erp24/web/js/category-plan/index.js

index f8a88280b1e5340dc055fe5e9e0c699b07b926fe..dcf5688059500f6905e66e8c32adf1da48856d72 100644 (file)
@@ -18,9 +18,14 @@ use yii\helpers\BaseConsole;
 use yii\helpers\Json;
 use yii_app\helpers\ClientHelper;
 use yii_app\helpers\DataHelper;
+use yii_app\helpers\HtmlHelper;
+use yii_app\records\Admin;
+use yii_app\records\AdminGroup;
+use yii_app\records\AdminPayroll;
 use yii_app\records\Autoplannogramma;
 use yii_app\records\BonusLevels;
 use yii_app\records\CityStore;
+use yii_app\records\EmployeePosition;
 use yii_app\records\EqualizationRemains;
 use yii_app\records\KogortStopList;
 use yii_app\records\LPTrackerApi;
@@ -31,6 +36,7 @@ use yii_app\records\ReplacementInvoice;
 use yii_app\records\ReplacementInvoiceProducts;
 use yii_app\records\MarketplaceOrder1cStatuses;
 use yii_app\records\Sales;
+use yii_app\records\ScriptLauncherLog;
 use yii_app\records\SentKogort;
 use yii_app\records\Users;
 use yii_app\records\UsersBonus;
@@ -39,6 +45,10 @@ use yii_app\records\UsersMessageManagement;
 use yii_app\records\UsersTelegram;
 use yii_app\records\UsersTelegramLog;
 use yii_app\services\AutoPlannogrammaService;
+use yii_app\services\CabinetService;
+use yii_app\services\ExportImportService;
+use yii_app\services\LogService;
+use yii_app\services\RatingService;
 use yii_app\services\StorePlanService;
 use yii_app\services\TelegramService;
 use yii_app\services\WhatsAppService;
@@ -51,6 +61,11 @@ class CronController extends Controller
     public $time;
     public $stepsBack;
     public $test;
+    public $year;
+    public $month;
+    public $storeId;
+
+
 
     public function actions()
     {
@@ -1193,6 +1208,9 @@ class CronController extends Controller
         $options[] = 'time';
         $options[] = 'stepsBack';
         $options[] = 'test';
+        $options[] = 'year';
+        $options[] = 'month';
+        $options[] = 'storeId';
 
         return $options;
     }
@@ -1720,4 +1738,185 @@ class CronController extends Controller
 
         $this->stdout("Расчет и сохранение автопланограммы завершены\n", BaseConsole::FG_GREEN);
     }
+
+
+    public function actionAutoplannogrammaRecalculate(): int
+    {
+        $cacheKey = 'apRecalculateTask';
+        $cache    = Yii::$app->cache;
+        $task     = $cache->get($cacheKey);
+
+
+        if (!is_array($task) || empty($task['taskName']) || $task['taskName'] !== 'apRecalculate') {
+            $this->stdout("No pending apRecalculateTask\n");
+            return ExitCode::OK;
+        }
+        if (!isset($task['status'])) {
+            $task['status'] = 'pending';
+        }
+        if ($task['status'] === 'running') {
+            $this->stdout("Task already running since {$task['startTime']}\n");
+            return ExitCode::OK;
+        }
+
+
+        foreach (['year','month','storeId','startTime'] as $key) {
+            if (empty($task[$key])) {
+                $this->stdout("Invalid task payload: missing {$key}\n");
+                $task['status'] = 'error';
+                $task['error']  = "Missing {$key}";
+                $cache->set($cacheKey, $task);
+                return ExitCode::UNSPECIFIED_ERROR;
+            }
+        }
+
+        if (abs(time() - $task['startTime']) > 86400) {
+            $this->stdout("Task startTime is out of allowed window\n");
+            $task['status'] = 'error';
+            $task['error']  = 'Invalid startTime';
+            $cache->set($cacheKey, $task);
+            return ExitCode::UNSPECIFIED_ERROR;
+        }
+
+        $task['status']    = 'running';
+        $cache->set($cacheKey, $task);
+
+        // 4) Логируем старт
+        $log = new ScriptLauncherLog();
+        $log->source     = 'CronController';
+        $log->category   = 'autoplannogramma';
+        $log->prefix     = 'actionAutoplannogrammaRecalculate';
+        $log->name       = 'taskApRecalculate';
+        $log->context    = json_encode($task, JSON_UNESCAPED_UNICODE);
+        $log->year       = (int)$task['year'];
+        $log->month      = (int)$task['month'];
+        $log->active     = 1;
+        $log->date_start = date('Y-m-d H:i:s');
+        $log->save(false);
+
+        try {
+
+            $year    = (int)$task['year'];
+            $month   = (int)$task['month'];
+            $storeId = (int)$task['storeId'];
+
+            $date     = (new DateTime())->setDate($year, $month, 1);
+            $planDate = $date->format('Y-m-01');
+
+            $service = new AutoPlannogrammaService();
+
+
+            $forecast        = $service->calculateFullForecastForWeek([
+                'year'     => $year,
+                'month'    => $month,
+                'type'     => AutoPlannogrammaService::TYPE_SALES,
+                'store_id' => $storeId,
+                'plan_date'=> $planDate,
+            ]);
+            $writeOffsF      = $service->getWeeklyProductsWriteoffsForecast($month, $year, $forecast, $storeId);
+            $salesForecast   = $service->getWeeklyBouquetProductsSalesForecast($month, $year, $storeId);
+
+
+            $existing = Autoplannogramma::find()
+                ->where([
+                    'store_id' => $storeId,
+                    'year'     => $year,
+                    'month'    => $month,
+                    'week'     => array_unique(ArrayHelper::getColumn($forecast, 'week')),
+                ])
+                ->indexBy(fn($m) => $m->week . '_' . $m->product_id)
+                ->all();
+
+            foreach ($forecast as $item) {
+                $key      = $item['week'] . '_' . $item['product_id'];
+                $model    = $existing[$key] ?? new Autoplannogramma();
+                $quantity = (float)($item['forecast_week_pieces'] ?? 0);
+                $details  = [];
+                $total    = $quantity;
+
+
+                if (!empty($writeOffsF[$item['product_id']][$item['week']]['writeOffs'])) {
+                    $w = $writeOffsF[$item['product_id']][$item['week']]['writeOffs'];
+                    $details['writeOffs']['quantity'] = $w;
+                    $total += is_array($w) ? array_sum($w) : (float)$w;
+                }
+
+
+                foreach (['offline','online','marketplace'] as $t) {
+                    $block = ['share'=>0,'quantity'=>0,'groups'=>[]];
+                    if (!empty($salesForecast[$storeId][$item['product_id']][$t])) {
+                        $share        = $salesForecast[$storeId][$t]['share'] ?? 0;
+                        $block['share']    = $share;
+                        $block['quantity'] = round($quantity * $share, 2);
+                        $total += $block['quantity'];
+                        foreach ($salesForecast[$storeId][$item['product_id']][$t] as $k=>$v) {
+                            $block['groups'][$k] = (float)$v;
+                            $total += (float)$v;
+                        }
+                    }
+                    $details[$t] = $block;
+                }
+
+                $details['forecast'] = ['quantity' => $quantity];
+                $total = (float) sprintf('%.2f', $total);
+
+                $needsUpdate = $model->isNewRecord
+                    || $model->calculate != $quantity
+                    || ceil($model->total) != ceil($total)
+                    || json_encode($model->details, JSON_UNESCAPED_UNICODE)
+                    !== json_encode($details, JSON_UNESCAPED_UNICODE);
+
+                if ($needsUpdate) {
+                    $model->setAttributes([
+                        'year'          => $year,
+                        'month'         => $month,
+                        'week'          => $item['week'],
+                        'product_id'    => $item['product_id'],
+                        'store_id'      => $storeId,
+                        'is_archive'    => false,
+                        'capacity_type' => 1,
+                        'details'       => json_encode($details, JSON_UNESCAPED_UNICODE),
+                        'calculate'     => $quantity,
+                        'modify'        => ceil($total),
+                        'total'         => ceil($total),
+                    ]);
+                    if (!$model->save()) {
+                        throw new \RuntimeException('Save failed: ' . json_encode($model->getErrors(), JSON_UNESCAPED_UNICODE));
+                    }
+                }
+            }
+
+            $task['status']   = 'done';
+            $task['progress'] = 100;
+            $cache->set($cacheKey, $task);
+
+            $log->message      = 'Finished successfully';
+            $log->progress     = 100;
+            $log->active       = 0;
+            $log->date_finish  = date('Y-m-d H:i:s');
+            $log->status       = 1;
+            $log->save(false);
+
+            $this->stdout("Recalculate complete for store {$storeId}\n");
+            return ExitCode::OK;
+
+        } catch (\Throwable $e) {
+            $task['status'] = 'error';
+            $task['error']  = $e->getMessage();
+            $cache->set($cacheKey, $task);
+
+            $log->message      = 'Error: ' . $e->getMessage();
+            $log->error_count  = 1;
+            $log->error_message= $e->getMessage();
+            $log->active       = 0;
+            $log->date_finish  = date('Y-m-d H:i:s');
+            $log->status       = 2;
+            $log->save(false);
+
+            $this->stderr("Error during recalc: {$e->getMessage()}\n");
+            return ExitCode::UNSPECIFIED_ERROR;
+        }
+    }
+
+
 }
index 86c608b114beec2752f597b894dddc5cc7a9151d..916cfbc14c513a09165600641e505af26f0121bf 100644 (file)
@@ -502,11 +502,31 @@ class CategoryPlanController extends Controller {
 
             if (Yii::$app->request->get('rebuild') === '1'
             ) {
-                Yii::$app->queue->push(new RebuildAutoplannogramJob([
+                /*Yii::$app->queue->push(new RebuildAutoplannogramJob([
                     'year'    => (int)$model->year,
                     'month'   => (int)$model->month,
                     'storeId' => (int)$model->store_id,
-                ]));
+                ]));*/
+                $yii = escapeshellarg(Yii::getAlias('@app/yii'));
+                $year  = (int)$model->year;
+                $month = (int)$model->month;
+                $store = (int)$model->store_id;
+
+
+                $cmd = "php {$yii}  cron/autoplannogramma-recalculate "
+                    . " --year={$year} --month={$month} --storeId={$store} "
+                    . "> /dev/null 2>&1 &";
+
+                exec($cmd, $output, $exitCode);
+
+                if ($exitCode === 0) {
+                    Yii::$app->session->setFlash('success','Задача запущена в фоне.');
+                    Yii::info('Задача запущена в фоне.');
+                } else {
+                    Yii::$app->session->setFlash('error','Не удалось запустить задачу.');
+                    Yii::error('Не удалось запустить задачу.');
+                }
+
                 $params = [
                     'DynamicModel' => $model->attributes,
                 ];
@@ -990,5 +1010,41 @@ class CategoryPlanController extends Controller {
         return $offlinePlannedSales;
     }
 
+    public function actionRebuild()
+    {
+        $year  = (int)Yii::$app->request->get('year');
+        $month = (int)Yii::$app->request->get('month');
+        $store = (int)Yii::$app->request->get('store_id');
+        $taskName = "apRecalculateTask";
+
+        $cacheValue = [
+            'taskName'  => $taskName,
+            'year'      => $year,
+            'month'     => $month,
+            'storeId'   => $store,
+            'status'    => 'running',
+            'startTime' => date('Y-m-d H:i:s'),
+            'progress'  => 0,
+            'error'     => null
+        ];
+        Yii::$app->cache->set($taskName, $cacheValue, 3600);
+
+        return $this->asJson(['status' => 'started' ]);
+    }
+
+    public function actionCheckTask()
+    {
+        $task = Yii::$app->cache->get('apRecalculateTask');
+
+        if (!$task) {
+            return $this->asJson(['status' => 'not_found']);
+        }
+
+        return $this->asJson([
+            'status'   => $task['status'],
+            'progress' => $task['progress'] ?? 0,
+            'error'    => $task['error'] ?? null,
+        ]);
+    }
 
 }
index afa66d454513a422a36e5a971f7fe8dd87bc2265..651d999e2c618d8f35ed5326cca7ddc3ad4d8e12 100644 (file)
@@ -37,7 +37,7 @@ class RebuildAutoplannogramJob extends BaseObject implements JobInterface
                 'year'             => $this->year,
                 'type'             => AutoPlannogrammaService::TYPE_SALES,
                 'store_id'         => $this->storeId,
-                'category'         => null,
+                'category'         => 'Срезка',
                 'subcategory'      => null,
                 'species'          => null,
                 'plan_date'        => $planDate,
index b44fc7ff6c5a16426e1f8f77f8ac4a0c22b60225..b6c3ebee4439f39c4530349c9398513f5745e0d1 100644 (file)
@@ -852,7 +852,13 @@ class AutoPlannogrammaService
 
         //$monthCategoryGoal = $this->getMonthCategoryGoal($monthCategoryShare, $datePlan, $filters['type']);
         $monthCategoryGoal = [];
-        $categoryPlan = CategoryPlan::find()->where(['year' => date('Y', strtotime($datePlan)), 'month' => date('m', strtotime($datePlan)), 'store_id' => $filters['store_id']])->indexBy('category')->asArray()->all();
+        $categoryPlan = CategoryPlan::find()
+            ->where(
+            ['year' => date('Y', strtotime($datePlan)),
+                'month' => date('m', strtotime($datePlan)),
+                'store_id' => $filters['store_id']])->indexBy('category')
+            ->asArray()
+            ->all();
         foreach ($categoryPlan as $category) {
             if ($category['category'] === 'Матрица') {
                 continue;
index 4b58e8d29b18aae2cc27c2a6c4ef118c2aec1ea6..884f54ef7ac88b4362ea8e0c92d73d581d5ab595 100644 (file)
@@ -163,6 +163,7 @@ input[readonly] {
                     <?= Html::submitButton('Вернуть автоплан', [
                         'class' => 'btn btn-danger ms-2',
                         'name'  => 'delete',
+                        'id' => 'delete',
                         'value' => 1,
                         'data'  => [
                             'confirm' => 'Вы уверены, что хотите вернуть автоплан за '
@@ -170,6 +171,17 @@ input[readonly] {
                         ],
                     ])
                     ?>
+                       <!-- --><?php /*= Html::a('Пересчитать автопланограмму', [
+                            'rebuild',
+                            'year'     => $model->year,
+                            'month'    => $model->month,
+                            'store_id' => $model->store_id,
+                        ],
+                            [
+                                'class'=>'btn btn-success',
+                                'data-confirm'=>'Запустить пересчет?'
+                            ]
+                        ) */?>
                     <?= Html::submitButton('Пересчитать автопланограмму', [
                         'class' => 'btn btn-success ms-2',
                         'disabled' => true,
@@ -183,6 +195,8 @@ input[readonly] {
                     ])
                     ?>
                     <?php } ?>
+                    <div id="changes-count"></div>
+                    <div id="changes" style="display:none;"></div>
                 </div>
             </div>
         </div>
index 72274c70476af53de51977d108303d0d27f311ee..5aeb723bf18c9dde164b23658178902a09f123ee 100644 (file)
@@ -67,6 +67,7 @@ function editProcent(zis) {
         }
     });
 
+
     $.ajax({
         method: "POST",
         url: '/category-plan/save-fields',
@@ -83,6 +84,17 @@ function editProcent(zis) {
         dataType: "text",
         success: function(response) {
             // включаем кнопку при успешном сохранении
+            let changes = JSON.parse(localStorage.getItem('planChanges') || '{}');
+            if (!changes[store_id]) {
+                changes[store_id] = {};
+            }
+            changes[store_id][type] = {
+                offline: offlineVal,
+                internet_shop: internetVal,
+                write_offs: writeoffsVal
+            };
+            localStorage.setItem('planChanges', JSON.stringify(changes, null, 2));
+            updateChangesLog(store_id);
             $('#rebuild').prop('disabled', false);
         },
         error: function() {
@@ -92,6 +104,35 @@ function editProcent(zis) {
 
 }
 
+function updateChangesLog(store_id) {
+    const changes = JSON.parse(localStorage.getItem('planChanges') || '{}');
+    const changesBox = $('#changes');
+    const changesCount = $('#changes-count');
+
+    if (!changes[store_id]) {
+        changes[store_id] = {};
+    }
+
+    const count = Object.keys(changes[store_id]).length;
+    let listItems = '';
+    Object.entries(changes[store_id]).forEach(([category, values]) => {
+        if (values.offline !== undefined)
+            listItems += `<li>${category} offline</li>`;
+        if (values.internet_shop !== undefined)
+            listItems += `<li>${category} internet_shop</li>`;
+        if (values.write_offs !== undefined)
+            listItems += `<li>${category} write_offs</li>`;
+    });
+
+    changesBox.html(`<ul>${listItems}</ul>`).hide();
+    changesCount.html(`Изменения - ${count} <button id="show-changes">Подробнее</button>`);
+
+    $('#show-changes').on('click', (event) => {
+        event.preventDefault();
+        $('#changes').toggle();
+    });
+}
+
 $(document).ready(() => {
     $('#categoryPlan').DataTable({
         sorting: false,
@@ -100,4 +141,68 @@ $(document).ready(() => {
         searching: true,
         language: data_table_language
     });
+    const store_id = $('#selected-store').val();
+    const changes = JSON.parse(localStorage.getItem('planChanges') || '{}');
+
+    if (store_id && changes[store_id]) {
+        updateChangesLog(store_id);
+        $('#rebuild').prop('disabled', false);
+        $('#changes-hint').remove();
+        $('#changes').after('<div id="changes-hint" style="color:red; margin-top:10px;">Пересчитайте планограмму после внесения изменений</div>');
+    }
+
+    $('#delete').on('click', function () {
+        localStorage.removeItem('planChanges');
+    });
+
+    $('#rebuild').on('click', function (event) {
+        event.preventDefault();
+        localStorage.removeItem('planChanges');
+        $('#rebuild').prop('disabled', true).text('Пересчёт запущен...');
+
+        $.ajax({
+            url: '/category-plan/rebuild',
+            type: 'GET',
+            data: {
+                year: $('#dynamicmodel-year').val(),
+                month: $('#dynamicmodel-month').val(),
+                store_id: store_id
+            },
+            success: function () {
+                startTaskPolling();
+                $('#rebuild').prop('disabled', true);
+            },
+            error: function () {
+                alert('Ошибка запуска пересчёта!');
+                $('#rebuild').prop('disabled', false).text('Пересчитать автопланограмму');
+            }
+        });
+    });
 });
+
+let taskPollInterval = null;
+
+function startTaskPolling() {
+    if (taskPollInterval) return;
+
+    taskPollInterval = setInterval(() => {
+        $.ajax({
+            url: '/category-plan/check-task',
+            type: 'GET',
+            dataType: 'json',
+            success: function (data) {
+                if (data.status === 'done' || data.status === 'error') {
+                    clearInterval(taskPollInterval);
+                    taskPollInterval = null;
+                    $('#rebuild').prop('disabled', false).text('Пересчитать автопланограмму');
+
+                    if (data.status === 'done') {
+                        alert('Задача успешно завершена');
+                    } else {
+                        alert('Задача завершилась с ошибкой: ' + (data.error || ''));
+                    }
+                }
+            }
+        });
+    }, 3000);
+}
\ No newline at end of file