'pagination' => ['pageSize' => 100],
]);
-
+ $bouquetSpeciesForecast = [];
// Обработка даты на год и месяц
if (!empty($filters['year']) && !empty($filters['month'])) {
$filters['plan_date'] = $filters['year'] . '-' . str_pad($filters['month'], 2, '0', STR_PAD_LEFT) . '-01';
//var_dump($filters); die();
$service = new AutoPlannogrammaService();
- $data = $service->calculateSpeciesForecastForProductsWithoutHistory($filters['plan_date'], $filters);
+
//$goals = $service->calculateFullGoalChain($filters);
$monthCategoryShare = $service->getMonthCategoryShareOrWriteOff($filters['plan_date'], $filters);
$monthCategoryGoal = $service->getMonthCategoryGoal($monthCategoryShare, $filters['plan_date']);
$filters['species']
);
- $withoutHistoryResults = StorePlanService::calculateMedianSalesForProductsWithoutHistoryExtended(
- $filters['store_id'],
- $filters['month'],
- $filters['year'],
- $result['without_history']
- );
+ $noHistoryProductData = $service->calculateSpeciesForecastForProductsWithoutHistory($filters['plan_date'], $filters);
$productSalesShare = StorePlanService::calculateProductSalesShareProductsWithHistory(
$filters['store_id'],
$goals
);
- var_dump($productSalesForecast); die();
+ $matrixForecast = MatrixBouquetForecast::find()
+ ->where(['year' => $filters['year'], 'month' => $filters['month']])
+ ->asArray()
+ ->all();
+ $matrixGroups = array_unique(ArrayHelper::getColumn($matrixForecast, 'group'));
+ $bouquetForecast = StorePlanService::getBouquetSpiecesMonthGoalFromForecast($filters['month'], $filters['year'], $filters['store_id'], $matrixGroups);
+ $speciesData = $bouquetForecast['final'];
+ foreach ($speciesData as $store_id => $categoryData) {
+ foreach ($categoryData as $category => $subcategoryData) {
+ foreach ($subcategoryData as $subcategory => $species) {
+ foreach ($species as $speciesInd => $row) {
+ $bouquetSpeciesForecast[] = [
+ 'category' => $category,
+ 'subcategory' => $subcategory,
+ 'store_id' => $store_id,
+ 'species' => $speciesInd,
+ 'goal' => $row
+ ];
+ }
+ }
+ }
+
+ }
+ $cleanedSpeciesGoals = $service->subtractSpeciesGoals($goals, $bouquetSpeciesForecast, $noHistoryProductData);
+ $salesProductForecastShare = $service->calculateProductForecastShare($noHistoryProductData, $productSalesForecast);
+ // $productForecastSpecies = $service->calculateProductSalesBySpecies($salesProductForecastShare, $cleanedSpeciesGoals);
+ //var_dump($salesProductForecastShare); die();
- $flatData = array_filter($data, function ($row) use ($filters) {
+
+
+
+
+ $flatData = array_filter($salesProductForecastShare, function ($row) use ($filters) {
foreach ($filters as $key => $value) {
if (empty($value)) continue;
if (!isset($row[$key])) continue;
}
//var_dump($bouquetSpeciesForecast); die();
$noHistoryProductData = $service->calculateSpeciesForecastForProductsWithoutHistory($filters['plan_date'], $filters);
-
+ var_dump($noHistoryProductData); die();
$cleanedSpeciesGoals = $service->subtractSpeciesGoals($data, $bouquetSpeciesForecast, $noHistoryProductData);
//var_dump($cleanedSpeciesGoals); die();
return $result;
}
+ /**
+ * Рассчитывает долю каждого товара в общем прогнозе по штукам.
+ *
+ * Объединяет прогнозы без истории и с историей для полного списка товаров.
+ *
+ * @param array $pieciesForecastProductsNoHistyory Результат calculateSpeciesForecastForProductsWithoutHistory
+ * @param array $pieciesForecastProductWithHistory Результат calculateProductForecastInPiecesProductsWithHistory
+ * @return array
+ */
+ public function calculateProductForecastShare(
+ array $pieciesForecastProductsNoHistyory,
+ array $pieciesForecastProductWithHistory
+ ): array {
+ $shareResult = [];
+
+ $info = $pieciesForecastProductsNoHistyory[0] ?? null;
+ if (!$info) {
+ return [];
+ }
+
+ $noHistoryMap = $info['forecasts'] ?? [];
+
+ $piecesMap = [];
+ foreach ($pieciesForecastProductWithHistory as $item) {
+ if (isset($item['product_id'], $item['forecast_pieces'])) {
+ $piecesMap[$item['product_id']] = $item['forecast_pieces'];
+ }
+ }
+
+ $allProductIds = array_merge(
+ array_keys($noHistoryMap),
+ array_keys($piecesMap)
+ );
+
+ $quantityMap = [];
+ foreach ($allProductIds as $pid) {
+ if (isset($piecesMap[$pid])) {
+ $quantityMap[$pid] = $piecesMap[$pid];
+ } elseif (isset($noHistoryMap[$pid])) {
+ $quantityMap[$pid] = (float)$noHistoryMap[$pid];
+ } else {
+ $quantityMap[$pid] = 0;
+ }
+ }
+
+ $totalPieces = array_sum($quantityMap);
+ if ($totalPieces <= 0) {
+ return [];
+ }
+
+ $storeId = $info['store_id'];
+ $month = $info['month'];
+ $year = $info['year'];
+ $category = $info['category'];
+ $subcategory = $info['subcategory'];
+ $species = $info['species'];
+
+ foreach ($quantityMap as $pid => $count) {
+ $share = $count / $totalPieces;
+
+ $shareResult[] = [
+ 'store_id' => $storeId,
+ 'month' => $month,
+ 'year' => $year,
+ 'category' => $category,
+ 'subcategory' => $subcategory,
+ 'species' => $species,
+ 'product_id' => $pid,
+ 'forecast_pieces'=> $count,
+ 'share' => round($share, 4),
+ 'history_status' => in_array($pid, array_keys($noHistoryMap)) ? 'No history' : 'With history'
+ ];
+ }
+
+ return $shareResult;
+ }
+
+ /**
+ * Рассчитывает продажи по каждому товару внутри вида на основе долей и очищенной цели вида.
+ *
+ * @param array $productShares Результат calculateProductForecastShare
+ * @param array $speciesGoals Массив целей по видам с ключом 'goal'
+ * @return array
+ */
+ public static function calculateProductSalesBySpecies(
+ array $productShares,
+ array $speciesGoals
+ ): array {
+ $result = [];
+ $goalsMap = [];
+ foreach ($speciesGoals as $item) {
+ if (isset($item['store_id'], $item['category'], $item['subcategory'], $item['species'], $item['goal'])) {
+ $key = implode('|', [
+ $item['store_id'],
+ $item['category'],
+ $item['subcategory'],
+ $item['species']
+ ]);
+ $goalsMap[$key] = $item['goal'];
+ }
+ }
+
+ foreach ($productShares as $shareItem) {
+ $key = implode('|', [
+ $shareItem['store_id'],
+ $shareItem['category'],
+ $shareItem['subcategory'],
+ $shareItem['species']
+ ]);
+ if (!isset($goalsMap[$key])) {
+ continue;
+ }
+ $cleanGoal = $goalsMap[$key];
+ $productSales = $shareItem['share'] * $cleanGoal;
+
+ $result[] = [
+ 'store_id' => $shareItem['store_id'],
+ 'month' => $shareItem['month'] ?? null,
+ 'year' => $shareItem['year'] ?? null,
+ 'category' => $shareItem['category'],
+ 'subcategory' => $shareItem['subcategory'],
+ 'species' => $shareItem['species'],
+ 'product_id' => $shareItem['product_id'],
+ 'forecast_pieces' => $shareItem['forecast_pieces'],
+ 'share' => round($shareItem['share'], 4),
+ 'history_status' => $shareItem['history_status'],
+ 'cleanGoal' => $cleanGoal,
+ 'product_sales' => round($productSales, 2),
+ ];
+ }
+
+ return $result;
+ }
+
}
\ No newline at end of file
$initTime = (hrtime(true) - $t0) / 1e6; // миллисекунды
Yii::warning( "Init (periods): {$initTime} ms\n");
foreach ($productsWithoutHistory as $product) {
- var_dump($product);die();
$guid = $product['guid'];
$t1 = hrtime(true);
$similarProductIds = self::getSimilarProductIDs($guid);
$accumulator[$groupKey]['goal'] += $q * $price;
}
- foreach ($medianProductsWithoutHistory as $guid => $_) {
+ foreach ($medianProductsWithoutHistory as $guid => $qty) {
$groupKey = $guidToGroup[$guid];
$goal = $accumulator[$groupKey]['goal'];
$price = $prices[$guid] ?? 0.0;
- $accumulator[$groupKey]['forecasts'][$guid] =
- $price > 0.0
- ? ($goal / $price)
- : 0.0;
+ $accumulator[$groupKey]['forecasts'][$guid] = $qty['weightedValue'];
+
}
return array_values($accumulator);
use yii_app\records\CityStore;
use yii_app\records\Products1cNomenclature;
?>
- <h1 class="ms-3 mb-4"><?= Html::encode("РаÑ\81Ñ\87еÑ\82 доли Ñ\82оваÑ\80а без иÑ\81Ñ\82оÑ\80ии (month_nohistory_sale_frcst)") ?></h1>
+ <h1 class="ms-3 mb-4"><?= Html::encode("РаÑ\81Ñ\87еÑ\82 доли Ñ\82оваÑ\80а оÑ\82 обÑ\89его колиÑ\87еÑ\81Ñ\82ва внÑ\83Ñ\82Ñ\80и вида (month_goods_sales_share)") ?></h1>
<?php
$form = ActiveForm::begin(['method' => 'get']); ?>
<div class="row p-3">
['attribute' => 'category', 'label' => 'Категория'],
['attribute' => 'subcategory', 'label' => 'Подкатегория'],
['attribute' => 'species', 'label' => 'Тип'],
- ['attribute' => 'goal', 'label' => 'Сумма', 'format' => ['decimal', 2]],
+ ['attribute' => 'product_id', 'label' => 'GUID Товара', ],
+ ['attribute' => 'product_id', 'label' => 'Имя Товара',
+ 'value' => function ($data) {
+ return \yii_app\records\Products1c::findOne($data['product_id'])->name ?? null;
+ },
+ ],
+ ['attribute' => 'forecast_pieces', 'label' => 'Прогноз в шт', 'format' => ['decimal', 2]],
+ ['attribute' => 'share', 'label' => 'Доля',
+
+ 'format' => ['percent', 2]],
+
+ ['attribute' => 'history_status', 'label' => 'Статус товара', ],
];
--- /dev/null
+<?php
+?>
+ <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 class="ms-3 mb-4"><?= Html::encode("Расчет доли товара от общего количества внутри вида (month_goods_sales_share)") ?></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>
+
+
+
+<?php
+$columns = [
+ ['attribute' => 'store_id', 'label' => 'Магазин', 'value' => function ($data) {
+ return CityStore::findOne($data['store_id'])->name ?? null;
+ }],
+ ['attribute' => 'category', 'label' => 'Категория'],
+ ['attribute' => 'subcategory', 'label' => 'Подкатегория'],
+ ['attribute' => 'species', 'label' => 'Тип'],
+ ['attribute' => 'product_id', 'label' => 'GUID Товара', ],
+ ['attribute' => 'product_id', 'label' => 'Имя Товара',
+ 'value' => function ($data) {
+ return \yii_app\records\Products1c::findOne($data['product_id'])->name ?? null;
+ },
+ ],
+ ['attribute' => 'forecast_pieces', 'label' => 'Прогноз в шт', 'format' => ['decimal', 2]],
+ ['attribute' => 'share', 'label' => 'Доля',
+
+ 'format' => ['percent', 2]],
+ ['attribute' => 'cleanGoal', 'label' => 'Цель вида очищенная', 'format' => ['decimal', 2]],
+ ['attribute' => 'product_sales', 'label' => 'Прогноз в стоимости внутри вида', 'format' => ['decimal', 2]],
+ ['attribute' => 'history_status', 'label' => 'Статус товара', ],
+];
+
+
+?>
+<?= GridView::widget([
+ 'dataProvider' => $dataProvider,
+ 'columns' => $columns,
+]); ?>
+<?php