namespace app\controllers;
+use DateTime;
+use Throwable;
use Yii;
use yii\base\DynamicModel;
use yii\data\ArrayDataProvider;
$service = new AutoPlannogrammaService();
//$goals = $service->calculateFullGoalChain($filters);
- //$forecast = $service->calculateFullForecastForWeek($filters);
- //var_dump( $forecast); die();
+ $forecast = $service->calculateFullForecastForWeek($filters);
+ var_dump( $forecast); die();
$monthCategoryShare = $service->getMonthCategoryShareOrWriteOff($filters['plan_date'], $filters);
$monthCategoryGoal = $service->getMonthCategoryGoal($monthCategoryShare, $filters['plan_date']);
$monthSubcategoryShare = $service->getMonthSubcategoryShareOrWriteOff($filters['plan_date'], $filters);
}
- public function actionControlSpeciesOld()
- {
- $model = new DynamicModel([
- 'storeId', 'month', 'type',
-
- ]);
- $model->addRule(['month', 'type'], 'required')
- ->addRule('storeId', 'integer');
-
- $storeList = CityStore::find()
- ->select(['name', 'id'])
- ->where(['visible' => CityStore::IS_VISIBLE])
- ->indexBy('id')
- ->column();
-
- $monthsList = [];
- for ($i = 0; $i < 12; $i++) {
- // получаем метку вида "03-2025"
- $ts = strtotime("first day of -{$i} month");
- $key = date('m-Y', $ts);
- $monthsList[$key] = $key;
- }
-
- $monthResult = [];
- $totals = [];
- $weeksData = [];
- $weeksShareResult = [];
- $weeksGoalResult = [];
- $monthCategoryShareResult = [];
- $weeksProductForecast = [];
-
- if ($model->load(Yii::$app->request->post()) && $model->validate()) {
- $filters = [];
-
- list($m, $y) = explode('-', $model->month);
- $dateFrom = date("Y-m-d 00:00:00", strtotime(sprintf('%04d-%02d-01', $y, $m)));
- $dateTo = date("Y-m-t 23:59:59", strtotime($dateFrom));
-
- if ($model->storeId) {
- $filters['store_id'] = $model->storeId;
- $filters['type'] = $model->type;
- $filters['plan_date'] = $dateFrom;
- }
-
- $service = new AutoPlannogrammaService();
-
- if ($model->storeId) {
- $totals = $service->getStoreTotals(
- [$model->storeId],
- $dateFrom,
- null,
- $model->type,
- $dateTo
- );
- }
-
- $monthSpeciesGoals = $service->calculateFullGoalChainWeighted($filters);
- $monthSpeciesGoalsMap = [];
- foreach ($monthSpeciesGoals as $monthSpeciesGoal) {
- $monthSpeciesGoalsMap[$monthSpeciesGoal['store_id']]
- [$monthSpeciesGoal['category']]
- [$monthSpeciesGoal['subcategory']]
- [$monthSpeciesGoal['species']] = $monthSpeciesGoal['goal'];
- }
-
- $weeksShareResult = $service->getHistoricalWeeklySpeciesShare($model->month, $filters, null, 'writeOffs');
- $weeksData = $service->calculateWeeklySpeciesGoals($weeksShareResult['weeksData'], $monthSpeciesGoals);
-
- $datePlan = $filters['plan_date'];
- $monthCategoryShare = $service->getMonthCategoryShareOrWriteOffWeighted($datePlan, $filters, null, $filters['type']);
- $monthCategoryGoal = $service->getMonthCategoryGoal($monthCategoryShare, $datePlan, $filters['type']);
- foreach ($monthCategoryShare as $sid => $cats) {
- foreach ($cats as $cat) {
- $monthCategoryShareResult[$sid][$cat['category']]['total_sum_cat'] = $cat['total_sum_cat'];
- $monthCategoryShareResult[$sid][$cat['category']]['share_of_total'] = $cat['share_of_total'];
- }
-
- }
- foreach ($monthCategoryGoal as $cats) {
- $monthCategoryShareResult[$cats['store_id']][$cats['category']]['goal'] = $cats['goal'];
-
- }
-
- $monthSubcategoryShare = $service->getMonthSubcategoryShareOrWriteOffWeighted($datePlan, $filters, null, $filters['type']);
- $monthSubcategoryGoal = $service->getMonthSubcategoryGoal($monthSubcategoryShare, $monthCategoryGoal);
-
- if ($filters['type'] === 'writeOffs') {
- $salesSubShare = $service->getMonthSubcategoryShareOrWriteOffWeighted($datePlan, $filters, null, 'sales');
- $salesSubGoal = $service->getMonthSubcategoryGoal($salesSubShare, $monthCategoryGoal);
-
- $catGoalMap = [];
- foreach ($monthCategoryGoal as $row) {
- $catGoalMap[$row['category']] = $row['goal'];
- }
- $salesSubGoalMap = [];
- foreach ($salesSubGoal as $row) {
- $salesSubGoalMap[$row['category']][$row['subcategory']] = $row['goal'];
- }
-
-
- foreach ($monthSubcategoryShare as &$row) {
- $cat = $row['category'];
- $sub = $row['subcategory'];
-
- $writeShare = $row['percent_of_month'];
-
- $writeGoal = ($catGoalMap[$cat] ?? 0) * $writeShare;
- $saleGoal = $salesSubGoalMap[$cat][$sub] ?? 0;
-
- if ($saleGoal > 0 && $writeGoal > 0.1 * $saleGoal) {
- $row['share'] = 0.1;
- }
- }
- unset($row);
- $monthSubcategoryGoal = $service->getMonthSubcategoryGoal($monthSubcategoryShare, $monthCategoryGoal);
- }
-
- foreach ($monthSubcategoryShare as $subcat) {
- $monthCategoryShareResult[$subcat['store_id']][$subcat['category']][$subcat['subcategory']]['total_sum'] = $subcat['total_sum'];
- $monthCategoryShareResult[$subcat['store_id']][$subcat['category']][$subcat['subcategory']]['percent_of_month'] = $subcat['percent_of_month'];
- }
- foreach ($monthSubcategoryGoal as $cats) {
- $monthCategoryShareResult[$cats['store_id']][$cats['category']][$cats['subcategory']]['goal'] = $cats['goal'];
-
- }
- $monthSpeciesShare = $service->getMonthSpeciesShareOrWriteOffWeighted($datePlan, $datePlan, $filters, null, $filters['type']);
- $monthSpeciesGoal = $service->getMonthSpeciesGoalDirty($monthSpeciesShare, $monthSubcategoryGoal);
- if ($filters['type'] === 'writeOffs') {
- $salesSpecShare = $service->getMonthSpeciesShareOrWriteOffWeighted($datePlan, $datePlan, $filters, null, 'sales');
- $salesSpecGoal = $service->getMonthSpeciesGoalDirty($salesSpecShare, $monthSubcategoryGoal);
-
- $subGoalMap = [];
- foreach ($monthSubcategoryGoal as $row) {
- $subGoalMap[$row['category']][$row['subcategory']] = $row['goal'];
- }
- $salesSpecGoalMap = [];
- foreach ($salesSpecGoal as $row) {
- $salesSpecGoalMap[$row['category']][$row['subcategory']][$row['species']] = $row['goal'];
- }
-
- foreach ($monthSpeciesShare as &$row) {
- $cat = $row['category'];
- $sub = $row['subcategory'];
- $spec = $row['species'];
-
- $writeShare = $row['percent_of_month'];
- $writeGoal = ($subGoalMap[$cat][$sub] ?? 0) * $writeShare;
- $saleGoal = $salesSpecGoalMap[$cat][$sub][$spec] ?? 0;
-
- if ($saleGoal > 0 && $writeGoal > 0.1 * $saleGoal) {
- $row['share'] = 0.1;
- }
- }
- unset($row);
-
- $monthSpeciesGoal = $service->getMonthSpeciesGoalDirty($monthSpeciesShare, $monthSubcategoryGoal);
- }
- foreach ($monthSpeciesShare as $species) {
- $monthCategoryShareResult[$species['store_id']][$species['category']][$species['subcategory']][$species['species']]['total_sum'] = $species['total_sum'];
- $monthCategoryShareResult[$species['store_id']][$species['category']][$species['subcategory']][$species['species']]['percent_of_month'] = $species['percent_of_month'];
-
- }
- foreach ($weeksShareResult['weeksData'] as $row) {
- $monthCategoryShareResult[$row['store_id']][$row['category']][$row['subcategory']][$row['species']][$row['week']]['sumWeek'] = $row['sumWeek'];
-
- }
-
-
- foreach ($weeksData as $r) {
- $forecasts = $service->calculateWeekForecastSpeciesProducts($r['category'], $r['subcategory'], $r['species'], $r['store_id'], $r['weekly_goal']);
- foreach ($forecasts as $forecast) {
- $weeksProductForecast[] = [
- 'category' => $forecast['category'] ?? '',
- 'subcategory' => $forecast['subcategory'] ?? '',
- 'species' => $forecast['species'] ?? '',
- 'product_id' => $forecast['product_id'] ?? '',
- 'name' => $forecast['name'] ?? '',
- 'price' => $forecast['price'] ?? '',
- 'goal' => $forecast['goal'] ?? 0,
- 'forecast' => $forecast['forecast'] ?? 0,
- 'week' => $r['week'],
- ];
- }
- }
-
- usort($weeksProductForecast, function ($a, $b) {
- foreach (['category', 'subcategory', 'species', 'name', 'week'] as $key) {
- $va = $a[$key];
- $vb = $b[$key];
- if ($va < $vb) return -1;
- if ($va > $vb) return 1;
- }
- return 0;
- });
-
- }
-
- return $this->render('control-species-old', [
- 'model' => $model,
- 'result' => $monthResult,
- 'weeksData' => $weeksData,
- 'monthCategoryShare' => $monthCategoryShareResult,
- 'weeksProductForecast' => $weeksProductForecast,
- 'totals' => $totals,
- 'storeList' => $storeList,
- 'monthsList' => $monthsList,
-
- ]);
- }
-
public function actionControlSpecies()
{
}
+
+ /**
+ * Расчет автопланограммы по одному магазину и рендер GridView
+ * @param int $store_id
+ * @param string|null $category
+ * @param string|null $subcategory
+ * @param string|null $species
+ * @param int|null $month
+ * @param int|null $year
+ */
+ public function actionCalculate( ) {
+ $request = Yii::$app->request;
+ $category = $request->get('category',null);
+ $subcategory = $request->get('subcategory') ?? null;
+ $species = $request->get('species') ?? null;
+ $store_id = $request->get('store_id',null) ;
+ $year = $request->get('year');
+ $month = $request->get('month');
+ $type = $request->get('type');
+
+
+ $dataProvider = new ArrayDataProvider([
+ 'allModels' => [],
+ 'pagination' => ['pageSize' => 100],
+ ]);
+
+
+ if (!empty($year) && !empty($month)) {
+ $planDate = sprintf('%04d-%02d-01', $year, $month);
+
+
+ $store = CityStore::findOne($store_id);
+ if (!$store) {
+ throw new \yii\web\NotFoundHttpException("Store with ID={$store_id} not found.");
+ }
+
+ $service = new AutoPlannogrammaService();
+
+ $params = [
+ 'month' => $month,
+ 'year' => $year,
+ 'type' => AutoPlannogrammaService::TYPE_SALES,
+ 'store_id' => $store_id,
+ 'category' => $category,
+ 'subcategory' => $subcategory,
+ 'species' => $species,
+ 'plan_date' => $planDate,
+ ];
+
+ try {
+ $forecast = $service->calculateFullForecastForWeek($params);
+ //var_dump($forecast);die();
+ $writeOffsForecast = $service->getWeeklyProductsWriteoffsForecast($month, $year, $forecast, $store_id);
+ $salesForecast = $service->getWeeklyBouquetProductsSalesForecast($month, $year, $store_id);
+
+ $existing = Autoplannogramma::find()
+ ->where(['month' => $month, 'year' => $year, 'store_id' => $store_id, 'week' => array_unique(array_column($forecast, 'week'))])
+ ->indexBy(fn($r) => $r->week . '_' . $r->product_id)
+ ->all();
+
+ $rows = [];
+ foreach ($forecast as $item) {
+ $key = $item['week'] . '_' . $item['product_id'];
+ $model = $existing[$key] ?? new Autoplannogramma();
+ $quantity = (float)($item['forecast_week_pieces'] ?? 0);
+ $productId = $item['product_id'];
+ $week = $item['week'];
+
+
+ $details = [];
+ $total = $quantity;
+ if (!empty($writeOffsForecast[$productId][$week]['writeOffs'])) {
+ $wo = $writeOffsForecast[$productId][$week]['writeOffs'];
+ $details['writeOffs']['quantity'] = $wo;
+ $total += is_array($wo) ? array_sum($wo) : (float)$wo;
+ } else {
+ $details['writeOffs']['quantity'] = 0;
+ }
+ foreach (['offline', 'online', 'marketplace'] as $type) {
+ $block = ['share' => 0, 'quantity' => 0, 'groups' => []];
+ if (isset($salesForecast[$store_id][$productId][$type]) && is_array($salesForecast[$store_id][$productId][$type])) {
+ $share = $salesForecast[$store_id][$type]['share'] ?? 0;
+ $block['share'] = (float)$share;
+ $block['quantity'] = (float)sprintf('%.2f', round($quantity * $share, 2));
+ foreach ($salesForecast[$store_id][$productId][$type] as $k => $v) {
+ $block['groups'][$k] = (float)$v;
+ $total += (float)$v;
+ }
+ }
+ $details[$type] = $block;
+ }
+ $details['forecast'] = ['quantity' => $quantity];
+ $total = (float)sprintf('%.2f', $total);
+
+ $model->setAttributes([
+ 'month' => $month,
+ 'year' => $year,
+ 'week' => $week,
+ 'product_id' => $productId,
+ 'store_id' => $store_id,
+ 'is_archive' => false,
+ 'capacity_type' => 1,
+ 'details' => json_encode($details, JSON_UNESCAPED_UNICODE),
+ 'calculate' => $quantity,
+ 'modify' => null,
+ 'total' => ceil($total),
+ ], false);
+ $model->save();
+
+
+ $rows[] = [
+ 'week' => $week,
+ 'product_id' => $productId,
+ 'calculate' => $model->calculate,
+ 'total' => $model->total,
+ 'details' => $model->details,
+ ];
+ }
+
+ } catch (Throwable $e) {
+ Yii::error($e->getMessage(), __METHOD__);
+ throw $e;
+ }
+
+
+ $dataProvider = new ArrayDataProvider([
+ 'allModels' => $rows,
+ 'pagination' => ['pageSize' => 50],
+ 'sort' => ['attributes' => ['week', 'product_id', 'calculate', 'total']],
+ ]);
+ }
+ return $this->render('calculate', [
+ 'store' => $store ?? null,
+ 'month' => $month,
+ 'year' => $year,
+ 'category' => $category,
+ 'subcategory' => $subcategory,
+ 'species' => $species,
+ 'dataProvider' => $dataProvider,
+ ]);
+ }
+
}
->where(['s.store_id' => $storeId])
->andWhere(['between', 's.date', $dateStart, $dateEnd])
->andWhere(['order_id' => ['', '0']])
- //->andWhere(['p1.components' => ''])
- ->andWhere(['not in', 'p1c.category', ['', 'букет', 'сборка', 'сервис']])
+ ->andWhere(['p1.components' => ''])
+ ->andWhere(['not in', 'p1c.category', ['', 'сервис']])
->andFilterWhere(['p1c.category' => $category])
->andFilterWhere(['p1c.subcategory' => $subcategory])
->andFilterWhere(['p1c.species' => $species])
}
}
+ foreach ($salesHistory as &$history) {
+ foreach ($periods as $periodKey => $periodData) {
+ if (!isset($history['data'][$periodKey])) {
+ $history['data'][$periodKey] = [];
+ }
+
+ foreach (array_keys($periodData['weeks']) as $weekIndex) {
+ if (!isset($history['data'][$periodKey][$weekIndex])) {
+ $history['data'][$periodKey][$weekIndex] = 0;
+ }
+ }
+ ksort($history['data'][$periodKey]);
+ }
+ }
+ unset($history);
+
return $salesHistory;
}
int $selectedYear,
array $medianProductsWithoutHistory
): array {
- $accumulator = [];
- $prices = [];
- $guidToGroup = [];
-
+ $accumulator = [];
foreach ($medianProductsWithoutHistory as $guid => $data) {
- $q = (float)$data['weightedValue'];
- if ($q <= 0) continue;
-
- $price = self::getPriceForProductAtOffsetMonthWeekly(
- $guid, $selectedYear, $selectedMonth, $storeId, 2
- );
- $prices[$guid] = $price;
-
- $cat = $data['category'];
- $sub = $data['subcategory'];
- $sp = $data['species'];
- $groupKey = implode('|', [$cat,$sub,$sp]);
- $guidToGroup[$guid] = $groupKey;
+ $cat = $data['category'];
+ $sub = $data['subcategory'];
+ $sp = $data['species'];
+ $groupKey = implode('|', [$cat, $sub, $sp]);
+ $q = (float)$data['weightedValue'];
if (!isset($accumulator[$groupKey])) {
$accumulator[$groupKey] = [
'forecasts' => [],
];
}
- $accumulator[$groupKey]['goal'] += $q * $price;
- }
- foreach ($medianProductsWithoutHistory as $guid => $qty) {
- $groupKey = $guidToGroup[$guid];
- $goal = $accumulator[$groupKey]['goal'];
- $price = $prices[$guid] ?? 0.0;
- $accumulator[$groupKey]['forecasts'][$guid] = $qty['weightedValue'];
+ $accumulator[$groupKey]['forecasts'][$guid] = $q;
+ if ($q > 0) {
+ $price = self::getPriceForProductAtOffsetMonthWeekly(
+ $guid, $selectedYear, $selectedMonth, $storeId, 2
+ );
+ $accumulator[$groupKey]['goal'] += $q * $price;
+ }
}
return array_values($accumulator);
--- /dev/null
+<?php
+/* @var yii\data\ArrayDataProvider $dataProvider */
+$allModels = $dataProvider->getModels();
+
+?>
+<div class="filter-form" style="margin-bottom: 20px;">
+ <?php
+ use kartik\date\DatePicker;
+ use kartik\grid\GridView;
+ use kartik\select2\Select2;
+ use yii\helpers\ArrayHelper;
+ use yii\helpers\Html;
+ use yii\widgets\ActiveForm;
+ use yii_app\records\CityStore;
+ use yii_app\records\Products1cNomenclature;
+ ?>
+ <h1><?= Html::encode($this->title) ?></h1>
+ <?php
+ $form = ActiveForm::begin(['method' => 'get']); ?>
+ <div class="row p-3">
+ <div class="col-md">
+ <?= $form->field(new \yii\base\DynamicModel(['category' => $filters['category'] ?? '']), 'category')->widget(Select2::class, [
+ 'data' => ArrayHelper::map(
+ Products1cNomenclature::find()->select('category')->distinct()->asArray()->all(),
+ 'category',
+ 'category'
+ ),
+ 'options' => ['placeholder' => 'Категория', 'name' => 'category'],
+ 'pluginOptions' => ['allowClear' => true],
+ ])->label('Категория') ?>
+ </div>
+ <div class="col-md">
+ <?= $form->field(new \yii\base\DynamicModel(['subcategory' => $filters['subcategory'] ?? '']), 'subcategory')->widget(Select2::class, [
+ 'data' => ArrayHelper::map(
+ Products1cNomenclature::find()->select('subcategory')->distinct()->asArray()->all(),
+ 'subcategory',
+ 'subcategory'
+ ),
+ 'options' => ['placeholder' => 'Подкатегория', 'name' => 'subcategory'],
+ 'pluginOptions' => ['allowClear' => true],
+ ])->label('Подкатегория') ?>
+ </div>
+ <div class="col-md">
+ <?= $form->field(new \yii\base\DynamicModel(['species' => $filters['species'] ?? '']), 'species')->widget(Select2::class, [
+ 'data' => ArrayHelper::map(
+ Products1cNomenclature::find()->select('species')->distinct()->asArray()->all(),
+ 'species',
+ 'species'
+ ),
+ 'options' => ['placeholder' => 'Тип товара', 'name' => 'species'],
+ 'pluginOptions' => ['allowClear' => true],
+ ])->label('Товар') ?>
+ </div>
+ <div class="col-md">
+ <?= $form->field(new \yii\base\DynamicModel(['store_id' => $filters['store_id'] ?? '']), 'store_id')->widget(Select2::class, [
+ 'data' => ArrayHelper::map(
+ CityStore::findAll(['visible' => CityStore::IS_VISIBLE]),
+ 'id',
+ 'name'
+ ),
+ 'options' => ['placeholder' => 'Магазин', 'name' => 'store_id'],
+ 'pluginOptions' => ['allowClear' => true],
+ ])->label('Магазин') ?>
+ </div>
+ <div class="col-md">
+ <?= $form->field(new \yii\base\DynamicModel(['month' => $filters['month'] ?? '']), 'month')->dropDownList(\yii_app\helpers\DateHelper::MONTH_NUMBER_NAMES, [
+ 'prompt' => 'Месяц',
+ 'name' => 'month',
+ ])->label('Плановый месяц') ?>
+ </div>
+
+ <div class="col-md">
+ <?= $form->field(new \yii\base\DynamicModel(['year' => $filters['year'] ?? '']), 'year')->dropDownList(['2025' => 2025, '2026' => 2026], [
+ 'prompt' => 'Год',
+ 'name' => 'year',
+ ])->label('Плановый год') ?>
+ </div>
+ <div class="col-md">
+ <?= $form->field(new \yii\base\DynamicModel(['type' => $filters['type'] ?? '']), 'type')->widget(Select2::class, [
+ 'data' => [
+ 'writeOffs' => 'Списания',
+ 'sales' => 'Продажи'
+ ],
+ 'options' => ['placeholder' => 'Тип', 'name' => 'type'],
+ 'pluginOptions' => ['allowClear' => true],
+ ])->label('По дефолту продажи!') ?>
+ </div>
+ <div class="col-md">
+ <?= Html::submitButton('Фильтровать', ['class' => 'btn btn-primary']) ?>
+ </div>
+ <div class="col-md">
+ <?= Html::a('Сбросить', ['auto-plannogramma/8'], ['class' => 'btn btn-default']) ?>
+ </div>
+ </div>
+
+ <?php ActiveForm::end(); ?>
+</div>
+<div class="autoplannogramma-calc">
+
+
+ <?= GridView::widget([
+ 'dataProvider' => $dataProvider,
+ 'columns' => [
+ ['attribute' => 'week', 'label' => 'Неделя'],
+ ['attribute' => 'product_id', 'label' => 'Product GUID'],
+ ['attribute' => 'calculate', 'label' => 'Calculate'],
+ ['attribute' => 'total', 'label' => 'Total'],
+ [
+ 'attribute' => 'details',
+ 'label' => 'Details',
+ 'format' => 'raw',
+ 'value' => function($model) {
+ return '<pre>' . Html::encode(json_encode($model['details'], JSON_UNESCAPED_UNICODE|JSON_PRETTY_PRINT)) . '</pre>';
+ }
+ ],
+ ],
+ ]); ?>