From ad7d16721a80da5a9bd728f868c8e34813058fc3 Mon Sep 17 00:00:00 2001 From: fomichev Date: Tue, 27 May 2025 18:53:26 +0300 Subject: [PATCH] =?utf8?q?=D0=92=D1=8B=D0=B2=D0=BE=D0=B4=20=D0=B4=D0=BE?= =?utf8?q?=D0=BB=D0=B5=D0=B9=20=D0=B8=20=D0=BF=D1=80=D0=BE=D0=B3=D0=BD?= =?utf8?q?=D0=BE=D0=B7=D0=BE=D0=B2?= MIME-Version: 1.0 Content-Type: text/plain; charset=utf8 Content-Transfer-Encoding: 8bit --- .../AutoPlannogrammaController.php | 47 ++++-- erp24/services/AutoPlannogrammaService.php | 134 ++++++++++++++++++ erp24/services/StorePlanService.php | 9 +- erp24/views/auto-plannogramma/81.php | 15 +- erp24/views/auto-plannogramma/82.php | 126 ++++++++++++++++ 5 files changed, 312 insertions(+), 19 deletions(-) create mode 100644 erp24/views/auto-plannogramma/82.php diff --git a/erp24/controllers/AutoPlannogrammaController.php b/erp24/controllers/AutoPlannogrammaController.php index d0dfa07f..3a9d2265 100644 --- a/erp24/controllers/AutoPlannogrammaController.php +++ b/erp24/controllers/AutoPlannogrammaController.php @@ -679,13 +679,13 @@ class AutoPlannogrammaController extends BaseController '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']); @@ -713,12 +713,7 @@ class AutoPlannogrammaController extends BaseController $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'], @@ -736,13 +731,43 @@ class AutoPlannogrammaController extends BaseController $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; @@ -832,7 +857,7 @@ class AutoPlannogrammaController extends BaseController } //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(); diff --git a/erp24/services/AutoPlannogrammaService.php b/erp24/services/AutoPlannogrammaService.php index 34d3de89..2731cbd0 100644 --- a/erp24/services/AutoPlannogrammaService.php +++ b/erp24/services/AutoPlannogrammaService.php @@ -904,4 +904,138 @@ class AutoPlannogrammaService 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 diff --git a/erp24/services/StorePlanService.php b/erp24/services/StorePlanService.php index f24f6dac..ccf53f78 100755 --- a/erp24/services/StorePlanService.php +++ b/erp24/services/StorePlanService.php @@ -478,7 +478,6 @@ class StorePlanService $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); @@ -715,14 +714,12 @@ class StorePlanService $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); diff --git a/erp24/views/auto-plannogramma/81.php b/erp24/views/auto-plannogramma/81.php index 885a80ed..36934c01 100644 --- a/erp24/views/auto-plannogramma/81.php +++ b/erp24/views/auto-plannogramma/81.php @@ -10,7 +10,7 @@ use yii_app\records\CityStore; use yii_app\records\Products1cNomenclature; ?> -

+

'get']); ?>
@@ -102,7 +102,18 @@ $columns = [ ['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' => 'Статус товара', ], ]; diff --git a/erp24/views/auto-plannogramma/82.php b/erp24/views/auto-plannogramma/82.php new file mode 100644 index 00000000..6ccb7448 --- /dev/null +++ b/erp24/views/auto-plannogramma/82.php @@ -0,0 +1,126 @@ + +
+ +

+ 'get']); ?> +
+
+ 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('Категория') ?> +
+
+ 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('Подкатегория') ?> +
+
+ 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('Товар') ?> +
+
+ 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('Магазин') ?> +
+
+ field(new \yii\base\DynamicModel(['month' => $filters['month'] ?? '']), 'month')->dropDownList(\yii_app\helpers\DateHelper::MONTH_NUMBER_NAMES, [ + 'prompt' => 'Месяц', + 'name' => 'month', + ])->label('Плановый месяц') ?> +
+ +
+ field(new \yii\base\DynamicModel(['year' => $filters['year'] ?? '']), 'year')->dropDownList(['2025' => 2025, '2026' => 2026], [ + 'prompt' => 'Год', + 'name' => 'year', + ])->label('Плановый год') ?> +
+
+ field(new \yii\base\DynamicModel(['type' => $filters['type'] ?? '']), 'type')->widget(Select2::class, [ + 'data' => [ + 'writeOffs' => 'Списания', + 'sales' => 'Продажи' + ], + 'options' => ['placeholder' => 'Тип', 'name' => 'type'], + 'pluginOptions' => ['allowClear' => true], + ])->label('По дефолту продажи!') ?> +
+
+ 'btn btn-primary']) ?> +
+
+ 'btn btn-default']) ?> +
+
+ + +
+ + + + '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' => 'Статус товара', ], +]; + + +?> + $dataProvider, + 'columns' => $columns, +]); ?> +