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;
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;
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;
public $time;
public $stepsBack;
public $test;
+ public $year;
+ public $month;
+ public $storeId;
+
+
public function actions()
{
$options[] = 'time';
$options[] = 'stepsBack';
$options[] = 'test';
+ $options[] = 'year';
+ $options[] = 'month';
+ $options[] = 'storeId';
return $options;
}
$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;
+ }
+ }
+
+
}
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,
];
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,
+ ]);
+ }
}
}
});
+
$.ajax({
method: "POST",
url: '/category-plan/save-fields',
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() {
}
+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,
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