]> gitweb.erp-flowers.ru Git - erp24_rep/yii-erp24/.git/commitdiff
Merge branch 'refs/heads/develop' into feature_fomichev_erp-361_week_write_offs_share...
authorfomichev <vladimir.fomichev@erp-flowers.ru>
Fri, 6 Jun 2025 11:56:55 +0000 (14:56 +0300)
committerfomichev <vladimir.fomichev@erp-flowers.ru>
Fri, 6 Jun 2025 11:56:55 +0000 (14:56 +0300)
# Conflicts:
# erp24/controllers/AutoPlannogrammaController.php
# erp24/services/AutoPlannogrammaService.php

1  2 
erp24/controllers/AutoPlannogrammaController.php
erp24/services/AutoPlannogrammaService.php

index 5c01737a4107e51b1c95ec005138d22c04d7641f,62b2c83311c7651035a5f7cf5bd05a521468aaf9..e927abfc377a15882708cbfdaca7caf2e6c89e24
@@@ -8,10 -7,13 +8,14 @@@ use yii\data\ArrayDataProvider
  use yii\db\Expression;
  use yii\db\Query;
  use yii\helpers\ArrayHelper;
 +use yii\web\Response;
  use yii_app\records\CityStore;
+ use yii_app\records\MatrixBouquetForecast;
+ use yii_app\records\PricesDynamic;
+ use yii_app\records\Products1c;
  use yii_app\records\Products1cNomenclature;
  use yii_app\services\AutoPlannogrammaService;
+ use yii_app\services\StorePlanService;
  
  class AutoPlannogrammaController extends BaseController
  {
          ]);
      }
  
+     public function action7()
+     {
+         $request = Yii::$app->request;
+         $filters = [
+             'category' => $request->get('category'),
+             'subcategory' => $request->get('subcategory'),
+             'species' => $request->get('species'),
+             'store_id' => $request->get('store_id'),
+             'year' => $request->get('year'),
+             'month' => $request->get('month'),
+             'type' => $request->get('type'),
+         ];
+         $dataProvider = new ArrayDataProvider([
+             'allModels' => [],
+             'pagination' => ['pageSize' => 100],
+         ]);
+         $bouquetSpeciesForecast = [];
+         // Обработка даты на год и месяц
+         if (!empty($filters['year']) && !empty($filters['month'])) {
+             $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
+                             ];
+                         }
+                     }
+                 }
+             }
+             //var_dump($matrixGroups); die();
+             $flatData = array_filter($bouquetSpeciesForecast, function ($row) use ($filters) {
+                 foreach ($filters as $key => $value) {
+                     if (empty($value)) continue;
+                     if (!isset($row[$key])) continue;
+                     if (stripos((string)$row[$key], (string)$value) === false) {
+                         return false;
+                     }
+                 }
+                 return true;
+             });
+             $dataProvider = new ArrayDataProvider([
+                 'allModels' => $flatData,
+                 'pagination' => ['pageSize' => 100],
+             ]);
+         }
+         return $this->render('7', [
+             'dataProvider' => $dataProvider,
+             'filters' => $filters,
+         ]);
+     }
+     public function action8()
+     {
+         $request = Yii::$app->request;
+         $filters = [
+             'category' => $request->get('category'),
+             'subcategory' => $request->get('subcategory') ?? null,
+             'species' => $request->get('species') ?? null,
+             'store_id' => $request->get('store_id') ?? [],
+             'year' => $request->get('year'),
+             'month' => $request->get('month'),
+             'type' => $request->get('type'),
+         ];
+         $dataProvider = new ArrayDataProvider([
+             'allModels' => [],
+             'pagination' => ['pageSize' => 100],
+         ]);
+         // Обработка даты на год и месяц
+         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);
+             //var_dump($data); die();
+             $flatData = array_filter($data, function ($row) use ($filters) {
+                 foreach ($filters as $key => $value) {
+                     if (empty($value)) continue;
+                     if (!isset($row[$key])) continue;
+                     if (stripos((string)$row[$key], (string)$value) === false) {
+                         return false;
+                     }
+                 }
+                 return true;
+             });
+             $dataProvider = new ArrayDataProvider([
+                 'allModels' => $flatData,
+                 'pagination' => ['pageSize' => 100],
+             ]);
+         }
+         return $this->render('8', [
+             'dataProvider' => $dataProvider,
+             'filters' => $filters,
+         ]);
+     }
+     public function actionMonthProductsSpeciesShare()
+     {
+         $request = Yii::$app->request;
+         $filters = [
+             'category' => $request->get('category'),
+             'subcategory' => $request->get('subcategory') ?? null,
+             'species' => $request->get('species') ?? null,
+             'store_id' => $request->get('store_id') ?? [],
+             'year' => $request->get('year'),
+             'month' => $request->get('month'),
+             'type' => $request->get('type'),
+         ];
+         $dataProvider = new ArrayDataProvider([
+             'allModels' => [],
+             '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();
+             //$goals = $service->calculateFullGoalChain($filters);
+             $monthCategoryShare = $service->getMonthCategoryShareOrWriteOff($filters['plan_date'], $filters);
+             $monthCategoryGoal = $service->getMonthCategoryGoal($monthCategoryShare, $filters['plan_date']);
+             $monthSubcategoryShare = $service->getMonthSubcategoryShareOrWriteOff($filters['plan_date'], $filters);
+             $monthSubcategoryGoal = $service->getMonthSubcategoryGoal($monthSubcategoryShare, $monthCategoryGoal);
+             $monthSpeciesShare = $service->getMonthSpeciesShareOrWriteOff($filters['plan_date'], $filters);
+             $goals = $service->getMonthSpeciesGoalDirty($monthSpeciesShare, $monthSubcategoryGoal);
+             if ($filters['type'] == AutoPlannogrammaService::TYPE_WRITE_OFFS) {
+                 $monthCategoryWriteOffsShare = $service->getMonthCategoryShareOrWriteOff($filters['plan_date'], $filters, $filters['type']);
+                 $monthCategoryWriteOffsGoal = $service->getMonthCategoryGoal($monthCategoryWriteOffsShare, $filters['plan_date'], $filters['type']);
+                 $monthSubcategoryWriteOffsShare = $service->getMonthSubcategoryShareOrWriteOff($filters['plan_date'], $filters, $filters['type']);
+                 $monthSubcategoryWriteOffsGoals = $service->getMonthSubcategoryGoal($monthSubcategoryWriteOffsShare, $monthCategoryWriteOffsGoal, $filters['type']);
+                 $monthSpeciesWriteOffShare = $service->getMonthSpeciesShareOrWriteOff($filters['plan_date'], $filters, $filters['type']);
+                 $goals = $service->getMonthSpeciesGoalDirty($monthSpeciesWriteOffShare, $monthSubcategoryWriteOffsGoals, $filters['type'], $data);
+             }
+             $result = StorePlanService::calculateHistoricalShare(
+                 $filters['store_id'],
+                 $filters['month'],
+                 $filters['year'],
+                 $filters['category'],
+                 $filters['subcategory'],
+                 $filters['species']
+             );
+             $noHistoryProductData = $service->calculateSpeciesForecastForProductsWithoutHistory($filters['plan_date'], $filters);
+             $productSalesShare = StorePlanService::calculateProductSalesShareProductsWithHistory(
+                 $filters['store_id'],
+                 $filters['month'],
+                 $result['with_history']
+             );
+             $productSalesForecast = $service->calculateProductForecastInPiecesProductsWithHistory(
+                 $filters['store_id'],
+                 $filters['month'],
+                 $productSalesShare,
+                 $goals,
+                 $filters['subcategory'],
+                 $filters['category'],
+                 $filters['species']
+             );
+ /*            $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($salesProductForecastShare, function ($row) use ($filters) {
+                 foreach ($filters as $key => $value) {
+                     if (empty($value)) continue;
+                     if (!isset($row[$key])) continue;
+                     if (stripos((string)$row[$key], (string)$value) === false) {
+                         return false;
+                     }
+                 }
+                 return true;
+             });
+             $dataProvider = new ArrayDataProvider([
+                 'allModels' => $flatData,
+                 'pagination' => ['pageSize' => 100],
+             ]);
+         }
+         return $this->render('month-products-species-share', [
+             'dataProvider' => $dataProvider,
+             'filters' => $filters,
+         ]);
+     }
+     public function actionMonthProductsSpeciesForecast()
+     {
+         $request = Yii::$app->request;
+         $filters = [
+             'category' => $request->get('category'),
+             'subcategory' => $request->get('subcategory') ?? null,
+             'species' => $request->get('species') ?? null,
+             'store_id' => $request->get('store_id') ?? [],
+             'year' => $request->get('year'),
+             'month' => $request->get('month'),
+             'type' => $request->get('type'),
+         ];
+         $dataProvider = new ArrayDataProvider([
+             'allModels' => [],
+             '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();
+             //$goals = $service->calculateFullGoalChain($filters);
+             $monthCategoryShare = $service->getMonthCategoryShareOrWriteOff($filters['plan_date'], $filters);
+             $monthCategoryGoal = $service->getMonthCategoryGoal($monthCategoryShare, $filters['plan_date']);
+             $monthSubcategoryShare = $service->getMonthSubcategoryShareOrWriteOff($filters['plan_date'], $filters);
+             $monthSubcategoryGoal = $service->getMonthSubcategoryGoal($monthSubcategoryShare, $monthCategoryGoal);
+             $monthSpeciesShare = $service->getMonthSpeciesShareOrWriteOff($filters['plan_date'], $filters);
+             $goals = $service->getMonthSpeciesGoalDirty($monthSpeciesShare, $monthSubcategoryGoal);
+             if ($filters['type'] == AutoPlannogrammaService::TYPE_WRITE_OFFS) {
+                 $monthCategoryWriteOffsShare = $service->getMonthCategoryShareOrWriteOff($filters['plan_date'], $filters, $filters['type']);
+                 $monthCategoryWriteOffsGoal = $service->getMonthCategoryGoal($monthCategoryWriteOffsShare, $filters['plan_date'], $filters['type']);
+                 $monthSubcategoryWriteOffsShare = $service->getMonthSubcategoryShareOrWriteOff($filters['plan_date'], $filters, $filters['type']);
+                 $monthSubcategoryWriteOffsGoals = $service->getMonthSubcategoryGoal($monthSubcategoryWriteOffsShare, $monthCategoryWriteOffsGoal, $filters['type']);
+                 $monthSpeciesWriteOffShare = $service->getMonthSpeciesShareOrWriteOff($filters['plan_date'], $filters, $filters['type']);
+                 $goals = $service->getMonthSpeciesGoalDirty($monthSpeciesWriteOffShare, $monthSubcategoryWriteOffsGoals, $filters['type'], $data);
+             }
+             $result = StorePlanService::calculateHistoricalShare(
+                 $filters['store_id'],
+                 $filters['month'],
+                 $filters['year'],
+                 $filters['category'],
+                 $filters['subcategory'],
+                 $filters['species']
+             );
+             $noHistoryProductData = $service->calculateSpeciesForecastForProductsWithoutHistory($filters['plan_date'], $filters);
+             $productSalesShare = StorePlanService::calculateProductSalesShareProductsWithHistory(
+                 $filters['store_id'],
+                 $filters['month'],
+                 $result['with_history']
+             );
+             $productSalesForecast = $service->calculateProductForecastInPiecesProductsWithHistory(
+                 $filters['store_id'],
+                 $filters['month'],
+                 $productSalesShare,
+                 $goals,
+                 $filters['subcategory'],
+                 $filters['category'],
+                 $filters['species']
+             );
+             $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($productForecastSpecies); die();
+             $flatData = array_filter($productForecastSpecies, function ($row) use ($filters) {
+                 foreach ($filters as $key => $value) {
+                     if (empty($value)) continue;
+                     if (!isset($row[$key])) continue;
+                     if (stripos((string)$row[$key], (string)$value) === false) {
+                         return false;
+                     }
+                 }
+                 return true;
+             });
+             $dataProvider = new ArrayDataProvider([
+                 'allModels' => $flatData,
+                 'pagination' => ['pageSize' => 100],
+             ]);
+         }
+         return $this->render('month-products-species-forecast', [
+             'dataProvider' => $dataProvider,
+             'filters' => $filters,
+         ]);
+     }
+     public function action9()
+     {
+         $request = Yii::$app->request;
+         $filters = [
+             'category' => $request->get('category'),
+             'subcategory' => $request->get('subcategory') ?? null,
+             'species' => $request->get('species') ?? null,
+             'store_id' => $request->get('store_id') ?? [],
+             'year' => $request->get('year'),
+             'month' => $request->get('month'),
+             'type' => $request->get('type'),
+         ];
+         $dataProvider = new ArrayDataProvider([
+             'allModels' => [],
+             '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';
+             $service = new AutoPlannogrammaService();
+             $monthCategoryShare = $service->getMonthCategoryShareOrWriteOff($filters['plan_date'], $filters, $filters['type']);
+             $monthCategoryGoal = $service->getMonthCategoryGoal($monthCategoryShare, $filters['plan_date']);
+             $monthSubcategoryShare = $service->getMonthSubcategoryShareOrWriteOff($filters['plan_date'], $filters);
+             $monthSubcategoryGoal = $service->getMonthSubcategoryGoal($monthSubcategoryShare, $monthCategoryGoal);
+             $monthSpeciesShare = $service->getMonthSpeciesShareOrWriteOff($filters['plan_date'], $filters);
+             $data = $service->getMonthSpeciesGoalDirty($monthSpeciesShare, $monthSubcategoryGoal);
+             if ($filters['type'] == AutoPlannogrammaService::TYPE_WRITE_OFFS) {
+                 $monthCategoryWriteOffsShare = $service->getMonthCategoryShareOrWriteOff($filters['plan_date'], $filters, $filters['type']);
+                 $monthCategoryWriteOffsGoal = $service->getMonthCategoryGoal($monthCategoryWriteOffsShare, $filters['plan_date'], $filters['type']);
+                 $monthSubcategoryWriteOffsShare = $service->getMonthSubcategoryShareOrWriteOff($filters['plan_date'], $filters, $filters['type']);
+                 $monthSubcategoryWriteOffsGoals = $service->getMonthSubcategoryGoal($monthSubcategoryWriteOffsShare, $monthCategoryWriteOffsGoal, $filters['type']);
+                 $monthSpeciesWriteOffShare = $service->getMonthSpeciesShareOrWriteOff($filters['plan_date'], $filters, $filters['type']);
+                 $data = $service->getMonthSpeciesGoalDirty($monthSpeciesWriteOffShare, $monthSubcategoryWriteOffsGoals, $filters['type'], $data);
+             }
+             $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
+                             ];
+                         }
+                     }
+                 }
+             }
+             //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();
+             $flatData = array_filter($cleanedSpeciesGoals, function ($row) use ($filters) {
+                 foreach ($filters as $key => $value) {
+                     if (empty($value)) continue;
+                     if (!isset($row[$key])) continue;
+                     if (stripos((string)$row[$key], (string)$value) === false) {
+                         return false;
+                     }
+                 }
+                 return true;
+             });
+             $dataProvider = new ArrayDataProvider([
+                 'allModels' => $flatData,
+                 'pagination' => ['pageSize' => 100],
+             ]);
+         }
+         return $this->render('9', [
+             'dataProvider' => $dataProvider,
+             'filters' => $filters,
+         ]);
+     }
+     public function actionWeekSalesSpeciesShare()
+     {
+         $request = Yii::$app->request;
+         $filters = [
+             'category' => $request->get('category'),
+             'subcategory' => $request->get('subcategory') ?? null,
+             'species' => $request->get('species') ?? null,
+             'store_id' => $request->get('store_id') ?? [],
+             'year' => $request->get('year'),
+             'month' => $request->get('month'),
+             'type' => $request->get('type'),
+         ];
+         $dataProvider = new ArrayDataProvider([
+             'allModels' => [],
+             '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['plan_date']); die();
+             $service = new AutoPlannogrammaService();
+             $weeklySales = $service->getHistoricalSpeciesShareByWeek($filters['plan_date'], $filters);
+             $flatData = array_filter($weeklySales, function ($row) use ($filters) {
+                 foreach ($filters as $key => $value) {
+                     if (empty($value)) continue;
+                     if (!isset($row[$key])) continue;
+                     if (stripos((string)$row[$key], (string)$value) === false) {
+                         return false;
+                     }
+                 }
+                 return true;
+             });
+             $dataProvider = new ArrayDataProvider([
+                 'allModels' => $flatData,
+                 'pagination' => ['pageSize' => 100],
+             ]);
+         }
+         return $this->render('week-sales-species-share', [
+             'dataProvider' => $dataProvider,
+             'filters' => $filters,
+         ]);
+     }
+     public function actionWeekSalesProductsForecast()
+     {
+         $request = Yii::$app->request;
+         $filters = [
+             'category' => $request->get('category'),
+             'subcategory' => $request->get('subcategory') ?? null,
+             'species' => $request->get('species') ?? null,
+             'store_id' => $request->get('store_id') ?? [],
+             'year' => $request->get('year'),
+             'month' => $request->get('month'),
+             'type' => $request->get('type'),
+         ];
+         $dataProvider = new ArrayDataProvider([
+             'allModels' => [],
+             '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['plan_date']); die();
+             $service = new AutoPlannogrammaService();
+ //$goals = $service->calculateFullGoalChain($filters);
+             $monthCategoryShare = $service->getMonthCategoryShareOrWriteOff($filters['plan_date'], $filters);
+             $monthCategoryGoal = $service->getMonthCategoryGoal($monthCategoryShare, $filters['plan_date']);
+             $monthSubcategoryShare = $service->getMonthSubcategoryShareOrWriteOff($filters['plan_date'], $filters);
+             $monthSubcategoryGoal = $service->getMonthSubcategoryGoal($monthSubcategoryShare, $monthCategoryGoal);
+             $monthSpeciesShare = $service->getMonthSpeciesShareOrWriteOff($filters['plan_date'], $filters);
+             $goals = $service->getMonthSpeciesGoalDirty($monthSpeciesShare, $monthSubcategoryGoal);
+             if ($filters['type'] == AutoPlannogrammaService::TYPE_WRITE_OFFS) {
+                 $monthCategoryWriteOffsShare = $service->getMonthCategoryShareOrWriteOff($filters['plan_date'], $filters, $filters['type']);
+                 $monthCategoryWriteOffsGoal = $service->getMonthCategoryGoal($monthCategoryWriteOffsShare, $filters['plan_date'], $filters['type']);
+                 $monthSubcategoryWriteOffsShare = $service->getMonthSubcategoryShareOrWriteOff($filters['plan_date'], $filters, $filters['type']);
+                 $monthSubcategoryWriteOffsGoals = $service->getMonthSubcategoryGoal($monthSubcategoryWriteOffsShare, $monthCategoryWriteOffsGoal, $filters['type']);
+                 $monthSpeciesWriteOffShare = $service->getMonthSpeciesShareOrWriteOff($filters['plan_date'], $filters, $filters['type']);
+                 $goals = $service->getMonthSpeciesGoalDirty($monthSpeciesWriteOffShare, $monthSubcategoryWriteOffsGoals, $filters['type'], $data);
+             }
+             $result = StorePlanService::calculateHistoricalShare(
+                 $filters['store_id'],
+                 $filters['month'],
+                 $filters['year'],
+                 $filters['category'],
+                 $filters['subcategory'],
+                 $filters['species']
+             );
+             $noHistoryProductData = $service->calculateSpeciesForecastForProductsWithoutHistory($filters['plan_date'], $filters);
+             $productSalesShare = StorePlanService::calculateProductSalesShareProductsWithHistory(
+                 $filters['store_id'],
+                 $filters['month'],
+                 $result['with_history']
+             );
+             $productSalesForecast = $service->calculateProductForecastInPiecesProductsWithHistory(
+                 $filters['store_id'],
+                 $filters['month'],
+                 $productSalesShare,
+                 $goals,
+                 $filters['subcategory'],
+                 $filters['category'],
+                 $filters['species']
+             );
+             $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);
+             $weeklySales = $service->getHistoricalSpeciesShareByWeek($filters['plan_date'], $filters);
+             $weeklySalesForecast = $service->calculateWeeklyProductForecastPieces($productForecastSpecies, $weeklySales);
+             $weeklySalesForecastFormated = $service->pivotWeeklyForecast($weeklySalesForecast);
+             $flatData = array_filter($weeklySalesForecastFormated, function ($row) use ($filters) {
+                 foreach ($filters as $key => $value) {
+                     if (empty($value)) continue;
+                     if (!isset($row[$key])) continue;
+                     if (stripos((string)$row[$key], (string)$value) === false) {
+                         return false;
+                     }
+                 }
+                 return true;
+             });
+             $dataProvider = new ArrayDataProvider([
+                 'allModels' => $flatData,
+                 'pagination' => ['pageSize' => 100],
+             ]);
+         }
+         return $this->render('week-sales-products_forecast', [
+             'dataProvider' => $dataProvider,
+             'filters' => $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);
 +            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'];
 +
 +            }
 +            //var_dump($monthCategoryShareResult); die();
 +            $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);
 +            }
 +           // var_dump($monthSubcategoryShare); die();
 +            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;
 +            });
 +
 +        }
 +//var_dump($weeksProductForecast); die();
 +        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()
 +    {
 +        $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();
 +
 +            $monthSpeciesGoals = $service->calculateFullGoalChain($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->getMonthCategoryShareOrWriteOff($datePlan, $filters,  $filters['type']);
 +            $monthCategoryGoal = $service->getMonthCategoryGoal($monthCategoryShare, $datePlan,  $filters['type']);
 +
 +            $monthCategorySalesShare = $service->getMonthCategoryShareOrWriteOff($datePlan, $filters);
 +            $monthCategorySalesGoal = $service->getMonthCategoryGoal($monthCategorySalesShare, $datePlan);
 +
 +            foreach ($monthCategoryShare as $sid => $cats) {
 +                foreach($cats as $cat) {
 +                    $monthCategoryShareResult[$sid][$cat['category']]['total_sum'] = $cat['total_sum'];
 +                    $monthCategoryShareResult[$sid][$cat['category']]['percent'] = $cat['percent'];
 +                }
 +
 +            }
 +            foreach ($monthCategoryGoal as $cats) {
 +                $monthCategoryShareResult[$cats['store_id']][$cats['category']]['goal'] = $cats['goal'];
 +
 +            }
 +            $monthSubcategorySalesShare = $service->getMonthSubcategoryShareOrWriteOff($datePlan, $filters);
 +            $monthSubcategorySalesGoal = $service->getMonthSubcategoryGoal($monthSubcategorySalesShare, $monthCategorySalesGoal);
 +
 +            $monthSubcategoryShare = $service->getMonthSubcategoryShareOrWriteOff($datePlan, $filters,  $filters['type']);
 +            $monthSubcategoryGoal = $service->getMonthSubcategoryGoal($monthSubcategoryShare, $monthCategoryGoal,  $filters['type'], $monthSubcategorySalesGoal);
 +
 +
 +
 +            // var_dump($monthSubcategoryShare); die();
 +            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'] = $subcat['percent'];
 +            }
 +            foreach ($monthSubcategoryGoal as $cats) {
 +                $monthCategoryShareResult[$cats['store_id']][$cats['category']][$cats['subcategory']]['goal'] = $cats['goal'];
 +
 +            }
 +            $monthSpeciesSalesShare = $service->getMonthSpeciesShareOrWriteOff($datePlan, $filters);
 +            $monthSpeciesSalesGoal = $service->getMonthSpeciesGoalDirty($monthSpeciesSalesShare, $monthSubcategoryGoal);
 +
 +            $monthSpeciesShare = $service->getMonthSpeciesShareOrWriteOff($datePlan, $filters, $filters['type']);
 +            $monthSpeciesGoal = $service->getMonthSpeciesGoalDirty($monthSpeciesShare, $monthSubcategoryGoal, $filters['type'], $monthSpeciesSalesGoal);
 +
 +            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'] = $species['percent'];
 +
 +            }
 +            foreach ($monthSpeciesGoal as $cats) {
 +                $monthCategoryShareResult[$cats['store_id']][$cats['category']][$cats['subcategory']][$cats['species']]['goal'] = $cats['goal'];
 +
 +            }
 +
 +            foreach ($weeksShareResult['weeksData'] as $row) {
 +                $monthCategoryShareResult[$row['store_id']][$row['category']][$row['subcategory']][$row['species']][$row['week']]['sumWeek'] = $row['sumWeek'];
 +
 +            }
 +            //var_dump($monthCategoryShareResult); die();
 +
 +            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;
 +            });
 +
 +        }
 +//var_dump($weeksProductForecast); die();
 +        return $this->render('control-species', [
 +            'model'           => $model,
 +            'result'          => $monthResult,
 +            'weeksData'       => $weeksData,
 +            'monthCategoryShare' => $monthCategoryShareResult,
 +            'weeksProductForecast' => $weeksProductForecast,
 +            'totals'          => $totals,
 +            'storeList'       => $storeList,
 +            'monthsList'      => $monthsList,
 +
 +        ]);
 +    }
 +
 +
 +
  }
index 41f60b4d3cf7fedf47c2fd946333dfbdc6633de0,e0ee09a241ab2d1eaaefa0f1fea8484a82d9f5c7..d33643e707cf6e667b207f2c4031058bdf5c7945
@@@ -2,13 -2,25 +2,25 @@@
  
  namespace yii_app\services;
  
+ use DateTime;
+ use Yii;
  use yii\db\Expression;
+ use yii\db\mssql\PDO;
  use yii\db\Query;
  use yii\helpers\ArrayHelper;
+ use yii_app\records\BouquetComposition;
  use yii_app\records\CityStore;
 -use yii_app\records\PricesDynamic;
 +use yii_app\records\PricesDynamic;
 +use yii_app\records\Products1cNomenclature;
+ use yii_app\records\CityStoreParams;
+ use yii_app\records\ExportImportTable;
 -use yii_app\records\Products1cNomenclature;
+ use yii_app\records\Products1c;
+ use yii_app\records\Sales;
+ use yii_app\records\SalesProducts;
  use yii_app\records\SalesWriteOffsPlan;
+ use yii_app\records\StorePlan;
+ use yii_app\records\WriteOffs;
+ use yii_app\records\WriteOffsProducts;
  
  class AutoPlannogrammaService
  {
                  ':month3' => $month3,
              ]);
          }
--
++        
          // Основной запрос с CTE
          $query = (new Query())
              ->select([
      }
  
  
+     // Недельные расчеты
+     /**
+      * Получает суммы продаж или списаний по видам (species) для каждой недели указанного месяца.
+      *
+      * @param string      $monthYear     месяц-год в формате MM-YYYY, например '03-2025'
+      * @param array|null  $filters       опциональные фильтры ['store_id'=>...]
+      * @param array|null  $productFilter опциональный фильтр по product_id
+      * @param string      $type          'sales' или 'writeOffs'
+      * @return array<int, array>         массив строк: [
+      *    ['week'=>1,'store_id'=>...,'category'=>...,'subcategory'=>...,'species'=>...,'sum'=>...],
+      *    ...
+      * ]
+      */
+     public function getWeeklySpeciesDataForMonth(
+         string $monthYear,
+         ?array $filters = null,
+         ?array $productFilter = null,
+         string $type = 'sales'
+     ): array {
+         [$yearStr, $monthStr ] = explode('-', $monthYear);
+         $month = (int)$monthStr;
+         $year  = (int)$yearStr;
+         $dateFrom = strtotime(sprintf('%04d-%02d-01 00:00:00', $year, $month));
+         $dateTo   = strtotime('+1 month -1 second', $dateFrom);
+         $stores   = $this->getVisibleStores();
+         $storeIds = array_map(fn($s)=>$s->id, $stores);
+         if (!empty($filters['store_id'])) {
+             $storeIds = array_intersect($storeIds, [(int)$filters['store_id']]);
+         }
+         if (empty($storeIds)) {
+             return [];
+         }
+         $dayOfWeek = (int)date('N', $dateFrom);
+         $firstMonday = $dayOfWeek === 1
+             ? $dateFrom
+             : strtotime('next monday', $dateFrom);
+         $weekRanges = [];
+         for ($wkStart = $firstMonday; $wkStart <= $dateTo; $wkStart += 7 * 86400) {
+             $wkEnd = $wkStart + 6 * 86400;
+             if ($wkEnd > $dateTo) {
+                 $wkEnd = $dateTo;
+             }
+             $periodStart = max($wkStart, $dateFrom);
+             $periodEnd   = min($wkEnd, $dateTo);
+             $daysInMonth   = floor(($periodEnd - $periodStart) / 86400) + 1;
+             if ($daysInMonth >= 4) {
+                 $weekRanges[] = [
+                     'index' => (int)date('W', $wkStart),
+                     'start' => date('Y-m-d H:i:s', $wkStart),
+                     'end'   => date('Y-m-d 23:59:59', $wkEnd),
+                 ];
+             }
+         }
+         $result = [];
+         foreach ($weekRanges as $range) {
+             $exprWeek = new Expression((string)$range['index']);
+             $query = (new Query())->select([
+                 'week'        => $exprWeek,
+                 'store_id'    => 'ex.entity_id',
+                 'category'    => 'p1c.category',
+                 'subcategory' => 'p1c.subcategory',
+                 'species'     => 'p1c.species',
+                 'total_sum'   => new Expression(
+                     $type === 'writeOffs'
+                         ? 'SUM(CAST(wop.summ AS NUMERIC))'
+                         : 'SUM(sp.summ)'
+                 ),
+             ]);
+             if ($type === 'writeOffs') {
+                 $query->from(['w'   => 'write_offs'])
+                     ->leftJoin(['ex'  => 'export_import_table'],      'ex.export_val = w.store_id')
+                     ->leftJoin(['wop'=> 'write_offs_products'],       'wop.write_offs_id = w.id')
+                     ->leftJoin(['p1c'=> 'products_1c_nomenclature'],  'p1c.id = wop.product_id')
+                     ->andWhere(['>=', 'w.date', $range['start']])
+                     ->andWhere(['<=', 'w.date', $range['end']]);
+                 if ($productFilter !== null) {
+                     $query->andWhere(['wop.product_id' => $productFilter]);
+                 }
+             } else {
+                 $query->from(['s' => 'sales'])
+                     ->leftJoin(['sp' => 'sales_products'],            'sp.check_id = s.id')
+                     ->leftJoin(['ex' => 'export_import_table'],       'ex.export_val = s.store_id_1c')
+                     ->leftJoin(['p1c'=> 'products_1c_nomenclature'],  'p1c.id = sp.product_id')
+                     ->andWhere(['>=', 's.date', $range['start']])
+                     ->andWhere(['<=', 's.date', $range['end']]);
+                 if ($productFilter !== null) {
+                     $query->andWhere(['sp.product_id' => $productFilter]);
+                 }
+             }
+             $query->andWhere(['ex.entity_id' => $storeIds])
+                 ->andWhere(['<>', 'p1c.species', ''])
+                 ->groupBy(['week','ex.entity_id','p1c.category','p1c.subcategory','p1c.species']);
+             $rows = $query->all();
+             foreach ($rows as $row) {
+                 $result[] = [
+                     'week'        => $row['week'],
+                     'store_id'    => $row['store_id'],
+                     'category'    => $row['category'],
+                     'subcategory' => $row['subcategory'],
+                     'species'     => $row['species'],
+                     'sum'         => (float)$row['total_sum'],
+                 ];
+             }
+         }
+         return $result;
+     }
+     /**
+      * Возвращает диапазоны недель (index, start, end) для указанного года и месяца,
+      * взятые по правилу «неделя считается, если в неё входит ≥4 дня из этого месяца».
+      *
+      * @param int $year  Год (например, 2025)
+      * @param int $month Месяц (1–12)
+      * @return array<array{index:int, start:string, end:string}>
+      */
+     public function getWeekRangesForMonth(int $year, int $month): array
+     {
+         $dateFrom = strtotime(sprintf('%04d-%02d-01 00:00:00', $year, $month));
+         $dateTo   = strtotime('+1 month -1 second', $dateFrom);
+         $dayOfWeek   = (int)date('N', $dateFrom);
+         $firstMonday = $dayOfWeek === 1
+             ? $dateFrom
+             : strtotime('next monday', $dateFrom);
+         $ranges = [];
+         for ($wkStart = $firstMonday; $wkStart <= $dateTo; $wkStart += 7 * 86400) {
+             $wkEnd = $wkStart + 6 * 86400;
+             $periodStart = max($wkStart, $dateFrom);
+             $periodEnd   = min($wkEnd,   $dateTo);
+             $daysInMonth = floor(($periodEnd - $periodStart) / 86400) + 1;
+             if ($daysInMonth >= 4) {
+                 $ranges[] = [
+                     'index' => (int)date('W', $wkStart),              // ISO-неделя от $wkStart
+                     'start' => date('Y-m-d H:i:s', $wkStart),         // “год-месяц-день 00:00:00”
+                     'end'   => date('Y-m-d 23:59:59', $wkEnd),        // “год-месяц-день 23:59:59”
+                 ];
+             }
+         }
+         return $ranges;
+     }
+     public function getHistoricalSpeciesShareByWeek(
+         string     $monthYear,
+         ?array     $filters = null,
+         string     $type    = self::TYPE_SALES
+     ): array {
+         [$yearStr, $monthStr, $_] = explode('-', $monthYear);
+         $year  = (int)$yearStr;
+         $month = (int)$monthStr;
+         $stores   = $this->getVisibleStores();
+         $storeIds = array_map(fn($s) => $s->id, $stores);
+         if (!empty($filters['store_id'])) {
+             $storeIds = array_intersect($storeIds, [(int)$filters['store_id']]);
+         }
+         if (empty($storeIds)) {
+             return [];
+         }
+         $sumExpression = $type === self::TYPE_WRITE_OFFS
+             ? 'SUM(wp.quantity)'
+             : 'SUM(sp.quantity)';
+         // Таблицы и условия join
+         $fromTable  = $type === self::TYPE_WRITE_OFFS ? ['w' => 'write_offs'] : ['s' => 'sales'];
+         $alias      = key($fromTable);
+         $productTableJoin               = $type === self::TYPE_WRITE_OFFS ? ['wp' => 'write_offs_products'] : ['sp' => 'sales_products'];
+         $productAlias                   = key($productTableJoin); // 'wp' или 'sp'
+         $productTableJoinCondition      = $type === self::TYPE_WRITE_OFFS ? 'wp.write_offs_id = w.id' : 'sp.check_id = s.id';
+         $storeJoinCondition             = $type === self::TYPE_WRITE_OFFS ? 'ex.export_val = w.store_id' : 'ex.export_val = s.store_id_1c';
+         $monthQtyBySpecies = [];
+         $weekQtyByPos      = [];
+         foreach ([$year - 2, $year - 1] as $histYear) {
+             $histMonthStart = sprintf('%04d-%02d-01 00:00:00', $histYear, $month);
+             $histMonthEnd   = date('Y-m-d 23:59:59', strtotime("$histMonthStart +1 month -1 second"));
+             $monthQuery = (new Query())
+                 ->select([
+                     'store_id'    => 'ex.entity_id',
+                     'category'    => 'p1c.category',
+                     'subcategory' => 'p1c.subcategory',
+                     'species'     => 'p1c.species',
+                     'month_sum'   => new Expression($sumExpression),
+                 ])
+                 ->from($fromTable)
+                 ->leftJoin($productTableJoin, $productTableJoinCondition)
+                 ->leftJoin('products_1c_nomenclature p1c', "p1c.id = {$productAlias}.product_id")
+                 ->leftJoin('products_1c p1', "p1.id = {$productAlias}.product_id")
+                 ->leftJoin('export_import_table ex', $storeJoinCondition)
+                 ->andWhere(['ex.entity_id' => $storeIds])
+                 ->andWhere(['p1.components' => ''])
+                 ->andWhere(['not in', 'p1c.category', ['', 'букет', 'сборка', 'сервис']])
+                 ->andWhere(['>=', "{$alias}.date", $histMonthStart])
+                 ->andWhere(['<=', "{$alias}.date", $histMonthEnd])
+                 ->groupBy(['ex.entity_id', 'p1c.category', 'p1c.subcategory', 'p1c.species']);
+             $monthRows = $monthQuery->all();
+             foreach ($monthRows as $row) {
+                 $sid      = $row['store_id'];
+                 $cat      = $row['category'];
+                 $sub      = $row['subcategory'];
+                 $spec     = $row['species'];
+                 $qty      = (float)$row['month_sum'];
+                 $monthQtyBySpecies[$sid][$cat][$sub][$spec] =
+                     ($monthQtyBySpecies[$sid][$cat][$sub][$spec] ?? 0.0) + $qty;
+             }
+             $histRanges = $this->getWeekRangesForMonth($histYear, $month);
+             $weekPos = 0;
+             foreach ($histRanges as $range) {
+                 $weekPos++;
+                 $startDt = new DateTime($range['start']);
+                 $startDt->setDate(
+                     $histYear,
+                     (int)date('n', strtotime($range['start'])),
+                     (int)date('j', strtotime($range['start']))
+                 );
+                 $endDt = new DateTime($range['end']);
+                 $endDt->setDate(
+                     $histYear,
+                     (int)date('n', strtotime($range['end'])),
+                     (int)date('j', strtotime($range['end']))
+                 );
+                 $weekQuery = (new Query())
+                     ->select([
+                         'store_id'    => 'ex.entity_id',
+                         'category'    => 'p1c.category',
+                         'subcategory' => 'p1c.subcategory',
+                         'species'     => 'p1c.species',
+                         'week_sum'    => new Expression($sumExpression),
+                     ])
+                     ->from($fromTable)
+                     ->leftJoin($productTableJoin, $productTableJoinCondition)
+                     ->leftJoin('products_1c_nomenclature p1c', "p1c.id = {$productAlias}.product_id")
+                     ->leftJoin('products_1c p1', "p1.id = {$productAlias}.product_id")
+                     ->leftJoin('export_import_table ex', $storeJoinCondition)
+                     ->andWhere(['ex.entity_id' => $storeIds])
+                     ->andWhere(['p1.components' => ''])
+                     ->andWhere(['not in', 'p1c.category', ['', 'букет', 'сборка', 'сервис']])
+                     ->andWhere(new Expression(
+                         "{$alias}.date BETWEEN :wstart AND :wend",
+                         [
+                             ':wstart' => $startDt->format('Y-m-d H:i:s'),
+                             ':wend'   => $endDt->format('Y-m-d H:i:s'),
+                         ]
+                     ))
+                     ->groupBy(['ex.entity_id', 'p1c.category', 'p1c.subcategory', 'p1c.species']);
+                 $weekRows = $weekQuery->all();
+                 foreach ($weekRows as $row) {
+                     $sid      = $row['store_id'];
+                     $cat      = $row['category'];
+                     $sub      = $row['subcategory'];
+                     $spec     = $row['species'];
+                     $qty      = (float)$row['week_sum'];
+                     if (!isset($weekQtyByPos[$weekPos])) {
+                         $weekQtyByPos[$weekPos] = [];
+                     }
+                     $weekQtyByPos[$weekPos][$sid][$cat][$sub][$spec] =
+                         ($weekQtyByPos[$weekPos][$sid][$cat][$sub][$spec] ?? 0.0) + $qty;
+                 }
+             }
+         }
+         $shareByPos = [];
+         foreach ($weekQtyByPos as $weekPos => $storesMap) {
+             foreach ($storesMap as $sid => $byCat) {
+                 foreach ($byCat as $cat => $bySub) {
+                     foreach ($bySub as $sub => $bySpec) {
+                         foreach ($bySpec as $spec => $weekQty) {
+                             $monthQty = $monthQtyBySpecies[$sid][$cat][$sub][$spec] ?? 0.0;
+                             if ($monthQty <= 0.0) {
+                                 continue;
+                             }
+                             $shareByPos[$weekPos][$sid][$cat][$sub][$spec] =
+                                 round($weekQty / $monthQty, 4);
+                         }
+                     }
+                 }
+             }
+         }
+         $targetRanges = $this->getWeekRangesForMonth($year, $month);
+         $result = [];
+         foreach ($targetRanges as $posIndex => $range) {
+             $weekPos       = $posIndex + 1;
+             $isoWeekNumber = $range['index'];
+             if (!isset($shareByPos[$weekPos])) {
+                 continue;
+             }
+             foreach ($shareByPos[$weekPos] as $sid => $byCat) {
+                 foreach ($byCat as $cat => $bySub) {
+                     foreach ($bySub as $sub => $bySpec) {
+                         foreach ($bySpec as $spec => $share) {
+                             $result[] = [
+                                 'store_id'    => $sid,
+                                 'category'    => $cat,
+                                 'subcategory' => $sub,
+                                 'species'     => $spec,
+                                 'week'        => $isoWeekNumber,
+                                 'share'       => $share,
+                                 'sumMonth' => $monthQtyBySpecies[$sid][$cat][$sub][$spec] ?? 0.0,
+                                 'sumWeek' => $weekQtyByPos[$weekPos][$sid][$cat][$sub][$spec] ?? 0.0
+                             ];
+                         }
+                     }
+                 }
+             }
+         }
+         $grouped = [];
+         foreach ($result as $idx => $row) {
+             $key = "{$row['store_id']}|{$row['category']}|{$row['subcategory']}|{$row['species']}";
+             $grouped[$key][] = $idx;
+         }
+         foreach ($grouped as $key => $indices) {
+             $sumPercent = 0.0;
+             foreach ($indices as $i) {
+                 $sumPercent += $result[$i]['share'];
+             }
+             if ($sumPercent < 1.0) {
+                 $diff = 1.0 - round($sumPercent, 4);
+                 $count = count($indices);
+                 $add = round($diff / $count, 4);
+                 foreach ($indices as $i) {
+                     $result[$i]['share'] = round($result[$i]['share'] + $add, 6);
+                 }
+             }
+         }
+         return $result;
+     }
+     public function calculateWeeklyProductForecastPieces(
+         array $productForecastSpecies,
+         array $weeklySales
+     ): array {
+         $forecastMap = [];
+         foreach ($productForecastSpecies as $item) {
+             $sid       = $item['store_id'];
+             $cat       = $item['category'];
+             $sub       = $item['subcategory'];
+             $spec      = $item['species'];
+             $pid       = $item['product_id'];
+             $piecesMon = (float)$item['product_sales_pieces'];
+             $forecastMap[$sid][$cat][$sub][$spec][$pid] = $piecesMon;
+         }
+         $result = [];
+         foreach ($weeklySales as $w) {
+             $sid   = $w['store_id'];
+             $cat   = $w['category'];
+             $sub   = $w['subcategory'];
+             $spec  = $w['species'];
+             $week  = $w['week'];
+             $wShare = (float)$w['share'];
+             if (
+                 ! isset(
+                     $forecastMap[$sid],
+                     $forecastMap[$sid][$cat],
+                     $forecastMap[$sid][$cat][$sub],
+                     $forecastMap[$sid][$cat][$sub][$spec]
+                 )
+             ) {
+                 continue;
+             }
+             $productsInSpec = $forecastMap[$sid][$cat][$sub][$spec];
+             foreach ($productsInSpec as $pid => $piecesMon) {
+                 $forecastWeekPieces = round($piecesMon * $wShare, 2);
+                 $result[] = [
+                     'week'                 => $week,
+                     'store_id'             => $sid,
+                     'category'             => $cat,
+                     'subcategory'          => $sub,
+                     'species'              => $spec,
+                     'product_id'           => $pid,
+                     'forecast_month_pieces' => $piecesMon,
+                     'forecast_week_pieces' => $forecastWeekPieces,
+                 ];
+             }
+         }
+         return $result;
+     }
+     public function pivotWeeklyForecast(array $flatRows): array
+     {
+         $grouped = [];
+         foreach ($flatRows as $row) {
+             $key = implode('|', [
+                 $row['store_id'],
+                 $row['category'],
+                 $row['subcategory'],
+                 $row['species'],
+                 $row['product_id'],
+             ]);
+             if (!isset($grouped[$key])) {
+                 $grouped[$key] = [
+                     'store_id'               => $row['store_id'],
+                     'category'               => $row['category'],
+                     'subcategory'            => $row['subcategory'],
+                     'species'                => $row['species'],
+                     'product_id'             => $row['product_id'],
+                     'forecast_month_pieces'  => $row['forecast_month_pieces'],
+                 ];
+             }
+             $weekNum   = (int)$row['week'];
+             $fieldName = 'week' . $weekNum;
+             $grouped[$key][$fieldName] = (float)$row['forecast_week_pieces'];
+         }
+         $allWeekFields = [];
+         foreach ($flatRows as $row) {
+             $allWeekFields['week' . ((int)$row['week'])] = true;
+         }
+         $allWeekFields = array_keys($allWeekFields);
+           foreach ($grouped as &$group) {
+             foreach ($allWeekFields as $fieldName) {
+                 if (! isset($group[$fieldName])) {
+                     $group[$fieldName] = 0.0;
+                 }
+             }
+         }
+         unset($group);
+         return array_values($grouped);
+     }
+     /**
+      * Исторический недельный отчёт и доли по видам с учётом store_id.
+      *
+      * @param string      $monthYear     месяц-год в формате MM-YYYY
+      * @param array|null  $filters
+      * @param array|null  $productFilter
+      * @param string      $type
+      * @return array{ 'weeksData': array<int, array> }
+      *   возвращает плоский список строк:
+      *   [
+      *     ['week'=>1, 'store_id'=>2, 'category'=>'...', 'subcategory'=>'...', 'species'=>'...', 'percent'=>0.32],
+      *     ...
+      *   ]
+      */
+     public function getHistoricalWeeklySpeciesShare(
+         string $monthYear,
+         ?array $filters = null,
+         ?array $productFilter = null,
+         string $type = 'sales'
+     ): array {
+         [$yearStr, $monthStr, $_ ] = explode('-', $monthYear);
+         $month = (int)$monthStr;
+         $year  = (int)$yearStr;
+         $yearData = [];
+         $historical = [];
+         for ($yr = $year - 2; $yr < $year; $yr++) {
+             $mYear = sprintf('%04d-%02d',$yr, $month);
+             $weeklyData = $this->getWeeklySpeciesDataForMonth(
+                 $mYear, $filters, $productFilter, $type
+             );
+             $yearData[$mYear] = $weeklyData;
+             foreach ($weeklyData as $row) {
+                 $week     = $row['week'];
+                 $sid      = $row['store_id'];
+                 $cat      = $row['category'];
+                 $sub      = $row['subcategory'];
+                 $spec     = $row['species'];
+                 $sumWeek  = $row['sum'];
+                 $historical[$week]           ??= [];
+                 $historical[$week][$sid]     ??= [];
+                 $historical[$week][$sid][$cat]   ??= [];
+                 $historical[$week][$sid][$cat][$sub] ??= [];
+                 $historical[$week][$sid][$cat][$sub][$spec] =
+                     ($historical[$week][$sid][$cat][$sub][$spec] ?? 0) + $sumWeek;
+             }
+         }
+         $dateFrom = sprintf('%04d-%02d-01 00:00:00', $year, $month);
+         $dateTo   = date('Y-m-d H:i:s', strtotime("$dateFrom +1 month -1 second"));
+         $monthWeighted = $this->getMonthSpeciesShareOrWriteOff(
+             $dateFrom, $filters, $type
+         );
+         $monthMap = [];
+         foreach ($monthWeighted as $m) {
+             $sid      = $m['store_id'];
+             $cat      = $m['category'];
+             $sub      = $m['subcategory'];
+             $spec     = $m['species'];
+             $sumMonth = $m['total_sum'];
+             $monthMap[$sid]            ??= [];
+             $monthMap[$sid][$cat]      ??= [];
+             $monthMap[$sid][$cat][$sub]??= [];
+             $monthMap[$sid][$cat][$sub][$spec] =
+                 ($monthMap[$sid][$cat][$sub][$spec] ?? 0) + $sumMonth;
+         }
+         $weeksList = array_keys($historical);
+         sort($weeksList, SORT_NUMERIC);
+         $speciesList = [];
+         foreach ($monthMap as $sid => $byCat) {
+             foreach ($byCat as $cat => $bySub) {
+                 foreach ($bySub as $sub => $bySpec) {
+                     foreach ($bySpec as $spec => $_) {
+                         $speciesList[] = compact('sid','cat','sub','spec');
+                     }
+                 }
+             }
+         }
+         $rows = [];
+         foreach ($speciesList as $comb) {
+             $sid  = $comb['sid'];
+             $cat  = $comb['cat'];
+             $sub  = $comb['sub'];
+             $spec = $comb['spec'];
+             $sumMonth = $monthMap[$sid][$cat][$sub][$spec] ?? 0;
+             if ($sumMonth <= 0) {
+                 continue; // нет месячного итога
+             }
+             foreach ($weeksList as $week) {
+                 $sumWeek = $historical[$week][$sid][$cat][$sub][$spec] ?? 0;
+                 $percent = $sumWeek > 0 ? round($sumWeek / $sumMonth, 4) : null;
+                 $rows[] = [
+                     'week'        => $week,
+                     'store_id'    => $sid,
+                     'category'    => $cat,
+                     'subcategory' => $sub,
+                     'species'     => $spec,
+                     'sumWeek'    => $sumWeek,
+                     'percent'     => $percent,
+                     'sumMonth' => $sumMonth
+                 ];
+             }
+         }
+         $grouped = [];
+         foreach ($rows as $idx => $row) {
+             $key = "{$row['store_id']}|{$row['category']}|{$row['subcategory']}|{$row['species']}";
+             $grouped[$key][] = $idx;
+         }
+         foreach ($grouped as $key => $indices) {
+             $sumPercent = 0.0;
+             foreach ($indices as $i) {
+                 $sumPercent += $rows[$i]['percent'];
+             }
+             if ($sumPercent < 1.0) {
+                 $diff = 1.0 - $sumPercent;
+                 $count = count($indices);
+                 $add = $diff / $count;
+                 foreach ($indices as $i) {
+                     $rows[$i]['percent'] = round($rows[$i]['percent'] + $add, 4);
+                 }
+             }
+         }
+         return  $rows;
+     }
+     /**
+      * Рассчитывает недельную цель для каждого вида (species) по данным недельных долей
+      * и целям месяца.
+      * @param array $weeksShareData
+      * @param array $monthSpeciesGoals
+      * @return array
+      *   Плоский массив строк с полями: week, store_id, category, subcategory,
+      *   species, percent, monthly_goal, weekly_goal
+      */
+     public function calculateWeeklySpeciesGoals(
+         array $weeksShareData,
+         array $monthSpeciesGoals
+     ): array {
+         $monthSpeciesGoalsMap = [];
+         foreach ($monthSpeciesGoals as $monthSpeciesGoal) {
+             $monthSpeciesGoalsMap[$monthSpeciesGoal['store_id']]
+             [$monthSpeciesGoal['category']]
+             [$monthSpeciesGoal['subcategory']]
+             [$monthSpeciesGoal['species']] = $monthSpeciesGoal['goal'] ;
+         }
+         $result = [];
+         foreach ($weeksShareData as $row) {
+             $week   = $row['week'];
+             $sid    = $row['store_id'];
+             $cat    = $row['category'];
+             $sub    = $row['subcategory'];
+             $spec   = $row['species'];
+             $percent = $row['percent'];
+             $monthlyGoal = $monthSpeciesGoalsMap[$sid][$cat][$sub][$spec] ?? null;
+             $weeklyGoal = 0;
+             if ($monthlyGoal !== null && $percent !== null) {
+                 $weeklyGoal =  round($percent * $monthlyGoal, 4);
+             }
+             $result[] = [
+                 'week'         => $week,
+                 'store_id'     => $sid,
+                 'category'     => $cat,
+                 'subcategory'  => $sub,
+                 'species'      => $spec,
+                 'percent'      => $percent,
+                 'monthly_goal' => $monthlyGoal,
+                 'weekly_goal'  => $weeklyGoal,
+             ];
+         }
+         return $result;
+     }
+     /**
+      * Возвращает дату понедельника ISO-недели в формате YYYY-MM-DD
+      *
+      * @param int $year  ISO-год (может отличаться от календарного в границах года)
+      * @param int $week  номер ISO-недели (1–53)
+      * @return string    дата понедельника, например '2025-03-10'
+      */
+     public static function getIsoWeekStart(int $year, int $week): string
+     {
+         $iso = $year . 'W' . str_pad($week, 2, '0', STR_PAD_LEFT) . '1';
+         return date('Y-m-d', strtotime($iso));
+     }
+     public static function calculateWeekForecastSpeciesProducts($category, $subcategory, $species, $storeId, $goal)
+     {
+         $speciesProductForecast = [];
+         $products = Products1cNomenclature::find()
+             ->select(['id', 'name'])
+             ->where(['category' => $category])
+             ->andWhere(['subcategory' => $subcategory])
+             ->andWhere(['species' => $species])
+             ->indexBy('id')
+             ->asArray()
+             ->all();
+         $productsIds = ArrayHelper::getColumn($products, 'id');
+         if (CityStore::find()->where(['id' => $storeId])->one()->city_id == 1342) {
+             $region = 52;
+         } elseif (CityStore::find()->where(['id' => $storeId])->one()->city_id == 1) {
+             $region = 77;
+         } else {
+             $region = null;
+         }
+         $priceRecords = PricesDynamic::find()
+             ->select(['product_id', 'price'])
+             ->where(['product_id' => $productsIds])
+             ->andWhere(['active' => 1])
+             ->andWhere(['or', ['region_id' => $region], ['region_id' => null]])
+             ->indexBy('product_id')
+             ->asArray()
+             ->all();
+         foreach ($priceRecords as $id => $record) {
+             if ($goal == 0 || (int)$record['price'] == 0) {
+                 $forecast = 0;
+             } else {
+                 $forecast = round(max($goal / (float)$record['price'], 1), 0);
+             }
+             $speciesProductForecast[] = [
+                 'category' => $category,
+                 'subcategory' => $subcategory,
+                 'species' => $species,
+                 'product_id' => $record['product_id'],
+                 'name' => $products[$id]['name'],
+                 'price' => $record['price'] ?? $goal ?? 1,
+                 'goal' => $goal ?? 0,
+                 'forecast' => $forecast
+             ];
+         }
+         return $speciesProductForecast;
+     }
+     /**
+      * Общий расчёт плана для заданной категории товаров без истории.
+      *
+      * @param int    $storeId
+      * @param string $yearMonth   строка "YYYY-MM", например "2025-03"
+      * @param string $category
+      * @param string|null $subcategory
+      * @param string|null $species
+      * @return array  [
+      *     [
+      *         'store_id'    => …,
+      *         'category'    => …,
+      *         'subcategory' => …,
+      *         'species'     => …,
+      *         'month'       => …,
+      *         'year'        => …,
+      *         'sum'         => …,
+      *     ],
+      *     …
+      * ]
+      *
+      */
+     public function calculateSpeciesForecastForProductsWithoutHistory($dateFrom, $filters): array
+     {
+         // Получение ID видимых магазинов
+         $storeIds = array_map(fn($store) => $store->id, $this->getVisibleStores());
+         $subcategory = !empty($filters['subcategory']) ? $filters['subcategory'] : null;
+         $species = !empty($filters['species']) ? $filters['species'] : null;
+         // Применение фильтра по магазину, если указан
+         if (!empty($filters['store_id'])) {
+             $storeIds = array_intersect($storeIds, [(int)$filters['store_id']]);
+         }
+         $date = new \DateTime($dateFrom);
+         $month = $date->format('m');
+         $year = $date->format('Y');
+         $result = [];
+         foreach ($storeIds as $storeId) {
+             $histResult = StorePlanService::calculateHistoricalShare(
+                 $storeId,
+                 $month,
+                 $year,
+                 $filters['category'],
+                 $subcategory,
+                 $species
+             );
+             $productsWithoutHistory = $histResult['without_history'] ?? [];
+             if (empty($productsWithoutHistory)) {
+                 continue;
+             }
+             // ——————— WEIGHTED SALES ————————
+             $medianResults = StorePlanService::calculateMedianSalesForProductsWithoutHistoryExtended(
+                 $storeId, $month, $year, $productsWithoutHistory
+             );
+             if (empty($medianResults)) {
+                 continue;
+             }
+             // ——————— COST CALCULATION ————————
+             $costs = StorePlanService::calculateCostForProductsWithoutHistory(
+                 $storeId, $month, $year, $medianResults
+             );
+             if (!empty($costs)) {
+                 $result = array_merge($result, $costs);
+             }
+         }
+         return $result;
+     }
+     public function mapGoalsBySpecies(array $rows): array {
+         $mapped = [];
+         foreach ($rows as $row) {
+             $mapped[$row['store_id']][$row['category']][$row['subcategory']][$row['species']] = $row['goal'];
+         }
+         return $mapped;
+     }
+     public function subtractSpeciesGoals(array $data, array $bouquetForecast, array $noHistory): array {
+         $bouquetForecastMap = $this->mapGoalsBySpecies($bouquetForecast);
+         $noHistoryMap = $this->mapGoalsBySpecies($noHistory);
+         $result = [];
+         foreach ($data as $row) {
+             $storeId = $row['store_id'];
+             $category = $row['category'];
+             $subcategory = $row['subcategory'];
+             $species = $row['species'];
+             $originalGoal = $row['goal'];
+             $bouquetForecastGoal = $bouquetForecastMap[$storeId][$category][$subcategory][$species] ?? 0;
+             $noHistoryGoal = $noHistoryMap[$storeId][$category][$subcategory][$species] ?? 0;
+             $cleanGoal = $originalGoal - ($bouquetForecastGoal + $noHistoryGoal);
+             $result[] = [
+                 'store_id' => $storeId,
+                 'category' => $category,
+                 'subcategory' => $subcategory,
+                 'species' => $species,
+                 'dirtyGoal' => $originalGoal,
+                 'bouquetGoal' => $bouquetForecastGoal,
+                 'noHistoryGoal' => $noHistoryGoal,
+                 'goal' => $cleanGoal
+             ];
+         }
+         return $result;
+     }
+     /**
+      * @param array|int $storeIds
+      * @param int $month
+      * @param int $year
+      * @param int       $regionId
+      * @return array   список строк с полями:
+      *                 sale_id, sale_date, product_id, product_name,
+      *                 component_guid, component_name, component_category,
+      *                 quantity, price, cost
+      */
+     public function getUnmarkedProductsComponents(int $storeId, string $month, string $year, int $regionId = null, $typeFilter = null): array
+     {
+         $date = new \DateTimeImmutable(sprintf('%04d-%02d-01', $year, $month));
+         $region = CityStoreParams::find()
+             ->where(['store_id' => $storeId])
+             ->one()->address_region;
+         if (!$regionId && !$region) {
+             // определяем регион по городу
+             $cityId = CityStore::find()->select('city_id')->where(['id' => $storeId])->scalar();
+             if ($cityId == 1) {
+                 $region = BouquetComposition::REGION_MSK;
+             } else {
+                 $region = BouquetComposition::REGION_NN;
+             }
+         }
+         $monthStart = sprintf('%04d-%02d-01 00:00:00', $year, $month);
+         $daysInMonth = cal_days_in_month(CAL_GREGORIAN, $month, $year);
+         $monthEnd   = sprintf('%04d-%02d-%02d 23:59:59', $year, $month, $daysInMonth);
+         $salesProducts = Sales::find()
+             ->alias('s')
+             ->select(['s.id', 's.date', 'sp.product_id', 'p1c.type', 'p1c.components' , 'p1c.name'])
+             ->innerJoin(
+                 ['sp' => SalesProducts::tableName()],
+                 's.id = sp.check_id'
+             )
+             ->innerJoin(
+                 ['p1c' => Products1c::tableName()],
+                 'p1c.id = sp.product_id'
+             )
+             ->leftJoin(
+                 ['nom' => Products1cNomenclature::tableName()],
+                 'nom.id = sp.product_id'
+             )
+             ->andWhere(['s.store_id'   => $storeId])
+             ->andWhere(['between', 's.date', $monthStart, $monthEnd])
+             ->andWhere(['not', ['p1c.components' => '']])
+             ->andWhere(['nom.category' => null])
+             ->asArray()
+             ->all();
+        // var_dump( $salesProducts); die();
+         $components = [];
+         $rows       = [];
+         foreach ($salesProducts as $sp) {
+             /** @var SalesProducts $sp */
+             $js      = trim($sp['components']);
+             if ($js === '' || $js[0] !== '{') {
+                 continue;
+             }
+             $data = @json_decode($js, true);
+             if (!is_array($data)) {
+                 continue;
+             }
+             foreach ($data as $guid => $qty) {
+                 $qty = (int)$qty;
+                 if ($qty <= 0) {
+                     continue;
+                 }
+                 $components[$guid] = true;
+                 $rows[] = [
+                     'sale_id'     => $sp['id'],
+                     'sale_date'   => $sp['date'],
+                     'product_id'  => $sp['product_id'],
+                     'product_name'=> $sp['name'],
+                     'component_guid' => $guid,
+                     'quantity'    => $qty,
+                 ];
+             }
+         }
+         if (empty($rows)) {
+             return [];
+         }
+         $guids = array_keys($components);
+         $nomenclatures = Products1cNomenclature::find()
+             ->andWhere(['id' => $guids])
+             ->indexBy('id')
+             ->all();
+         $priceDynamics = PricesDynamic::find()
+             ->andWhere(['region_id' => $region])
+             ->andWhere(['product_id' => array_values( ArrayHelper::getColumn($nomenclatures, 'id') )])
+            // ->andWhere(['<=', 'date_from', $monthStart])
+           //  ->andWhere(['>=', 'date_to', $monthEnd])
+             ->orderBy(['date_from' => SORT_DESC])
+             ->all();
+         $pricesByProduct = [];
+         foreach ($priceDynamics as $pd) {
+             /** @var PricesDynamic $pd */
+             $pid = $pd->product_id;
+             $pricesByProduct[$pid][] = $pd;
+         }
+         $result = [];
+         foreach ($rows as $r) {
+             $guid = $r['component_guid'];
+             $n   = $nomenclatures[$guid] ?? null;
+             $pid = $n?->id;
+             $price = 0;
+             if ($pid && isset($pricesByProduct[$pid])) {
+                 foreach ($pricesByProduct[$pid] as $pd) {
+                     if ($pd->date_from <= $r['sale_date'] && $pd->date_to >= $r['sale_date']) {
+                         $price = $pd->price;
+                         break;
+                     }
+                 }
+             }
+             $cost = $r['quantity'] * $price;
+             $result[] = [
+                 'sale_id'            => $r['sale_id'],
+                 'sale_date'          => $r['sale_date'],
+                 'product_id'         => $r['product_id'],
+                 'product_name'       => $r['product_name'],
+                 'component_guid'     => $guid,
+                 'component_name'     => $n?->name,
+                 'component_category' => $n?->category,
+                 'quantity'           => $r['quantity'],
+                 'price'              => $price,
+                 'cost'               => $cost,
+             ];
+         }
+         return $result;
+     }
+     public function calculateProductForecastInPiecesProductsWithHistory(
+         int $storeId,
+         string $month,
+         array $productSalesShare,
+         array $speciesGoals,
+         string $subcategory = null,
+         string $category = null,
+         string $species = null
+     ): array {
+         $result = [];
+         if (empty($speciesGoals)) {
+             return [];
+         }
+         $goalsMap = $this->mapGoalsBySpecies($speciesGoals);
+         $region = CityStoreParams::find()
+             ->where(['store_id' => $storeId])
+             ->one()->address_region ?? null;
+         if (!$region) {
+             $cityId = CityStore::find()->select('city_id')->where(['id' => $storeId])->scalar();
+             $region = ($cityId == 1)
+                 ? BouquetComposition::REGION_MSK
+                 : BouquetComposition::REGION_NN;
+         }
+         foreach ($productSalesShare as $productId => $data) {
+             $share = $data['share'] ?? 0.0;
+             if ($share <= 0.0) {
+                 continue;
+             }
+             $priceRecord = PricesDynamic::find()
+                 ->where([
+                     'product_id' => $productId,
+                     'region_id' => $region,
+                     'active' => 1,
+                 ])
+                 ->one();
+             if (!$priceRecord || $priceRecord->price <= 0) {
+                 continue;
+             }
+             $storeId = $data['store_id'];
+             $cat     = $data['category'];
+             $sub     = $data['subcategory'];
+             $spec    = $data['species'];
+             if (
+                 ! isset(
+                     $goalsMap[$storeId],
+                     $goalsMap[$storeId][$cat],
+                     $goalsMap[$storeId][$cat][$sub],
+                     $goalsMap[$storeId][$cat][$sub][$spec]
+                 )
+             ) {
+                 continue;
+             }
+             $goal = $goalsMap[$storeId][$cat][$sub][$spec];
+             $forecastSum = $goal * $share;
+             $forecastCount = $forecastSum / $priceRecord->price;
+             $result[] = [
+                 'product_id' => $productId,
+                 'goal' => $goal,
+                 'goal_share' => round($forecastSum, 2),
+                 'price' => $priceRecord->price,
+                 'forecast_pieces' => round($forecastCount),
+                 'store_id' => $data['store_id'],
+                 'category' => $data['category'],
+                 'subcategory' => $data['subcategory'],
+                 'species' => $data['species'],
+             ];
+         }
+         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'];
+             }
+         }
+         $regions = CityStoreParams::find()
+             ->select(['store_id', 'address_region'])
+             ->indexBy('store_id')
+             ->asArray()
+             ->all();
+         $products = ArrayHelper::getColumn($productShares, 'product_id');
+         $prices = PricesDynamic::find()
+             ->select(['product_id', 'price', 'region_id'])
+             ->where(['product_id' => $products])
+             ->andWhere(['active' => 1])
+             ->asArray()
+             ->all();
+         $pricesMap =  [];
+         foreach ($prices as $price) {
+             $pricesMap[$price['product_id']][$price['region_id']][] = $price['price'];
+         }
+         foreach ($productShares as $shareItem) {
+             $storeId = $shareItem['store_id'];
+             $region = $regions[$storeId]['address_region']
+                 ?? BouquetComposition::REGION_NN;
+             $priceList = $pricesMap[$shareItem['product_id']][$region] ?? null;
+             $price = is_array($priceList) && count($priceList) > 0
+                 ? $priceList[0] ?? 1
+                 : 1;
+             $key = implode('|', [
+                 $shareItem['store_id'],
+                 $shareItem['category'],
+                 $shareItem['subcategory'],
+                 $shareItem['species']
+             ]);
+             $cleanGoal     = $goalsMap[$key] ?? 0;
+             $productSales = $shareItem['share'] * $cleanGoal;
+             $productSalesPieces = round($productSales / $price, 2);
+             $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),
+                 'product_sales_pieces' => $productSalesPieces
+             ];
+         }
+         return $result;
+     }
+     /**
+      * @param array|int $storeIds
+      * @param int $month
+      * @param int $year
+      * @param int       $regionId
+      * @return array   список строк с полями:
+      *                 sale_id, sale_date, product_id, product_name,
+      *                 component_guid, component_name, component_category,
+      *                 quantity, price, cost
+      */
+     public function getProductsComponentsInCategory(int $storeId, string $month, string $year, string $type = self::TYPE_SALES): array
+     {
+         $region = CityStoreParams::find()
+             ->where(['store_id' => $storeId])
+             ->one()->address_region ?? null;
+         if (!$region) {
+             $cityId = CityStore::find()->select('city_id')->where(['id' => $storeId])->scalar();
+             $region = $cityId == 1
+                 ? BouquetComposition::REGION_MSK
+                 : BouquetComposition::REGION_NN;
+         }
+         $monthStart = sprintf('%04d-%02d-01 00:00:00', $year, $month);
+         $daysInMonth = cal_days_in_month(CAL_GREGORIAN, $month, $year);
+         $monthEnd   = sprintf('%04d-%02d-%02d 23:59:59', $year, $month, $daysInMonth);
+         $componentProducts = [];
+         if ($type == self::TYPE_SALES) {
+             $salesProducts = Sales::find()
+                 ->alias('s')
+                 ->select([
+                     's.id',
+                     's.date',
+                     's.operation',
+                     'sp.product_id',
+                     'p1c.type',
+                     'p1c.components',
+                     'p1c.name',
+                     'sp.quantity AS quantity_product'
+                 ])
+                 ->innerJoin(
+                     ['sp' => SalesProducts::tableName()],
+                     's.id = sp.check_id'
+                 )
+                 ->innerJoin(
+                     ['p1c' => Products1c::tableName()],
+                     'p1c.id = sp.product_id'
+                 )
+                 ->leftJoin(
+                     ['nom' => Products1cNomenclature::tableName()],
+                     'nom.id = sp.product_id'
+                 )
+                 ->andWhere(['s.store_id'   => $storeId])
+                 ->andWhere(['not', ['s.operation'   => ['Удален', 'Удаление']]])
+                 ->andWhere(['between', 's.date', $monthStart, $monthEnd])
+                 ->andWhere(['not', ['p1c.components' => '']])
+                 ->asArray()
+                 ->all();
+             $componentProducts = $salesProducts;
+         } else {
+             $writeOffsProducts = WriteOffs::find()
+                 ->alias('w')
+                 ->select([
+                     'w.id AS write_off_id',
+                     'w.date AS write_off_date',
+                     'wp.product_id',
+                     'p1c.type',
+                     'p1c.components',
+                     'p1c.name AS product_name',
+                     'ex.entity_id AS store_id',
+                     'wp.quantity AS quantity_product'
+                 ])
+                 ->innerJoin(
+                     ['wp' => WriteOffsProducts::tableName()],
+                     'wp.write_offs_id = w.id'
+                 )
+                 ->innerJoin(
+                     ['p1c' => Products1c::tableName()],
+                     'p1c.id = wp.product_id'
+                 )
+                 ->leftJoin(
+                     ['nom' => Products1cNomenclature::tableName()],
+                     'nom.id = wp.product_id'
+                 )
+                 ->leftJoin(
+                     ['ex' => ExportImportTable::tableName()],
+                     'ex.export_val = w.store_id'
+                 )
+                 ->andWhere(['between', 'w.date', $monthStart, $monthEnd])
+                 ->andWhere(['ex.entity_id' => $storeId])
+                 ->andWhere(['not', ['p1c.components' => '']])
+                 ->asArray()
+                 ->all();
+             $componentProducts = $writeOffsProducts;
+         }
+         $components = [];
+         $rows       = [];
+         foreach ($componentProducts as $cp) {
+             $js = trim($cp['components']);
+             if ($js === '' || $js[0] !== '{') {
+                 continue;
+             }
+             $data = @json_decode($js, true);
+             if (!is_array($data)) {
+                 continue;
+             }
+             foreach ($data as $guid => $qty) {
+                 $qty = (int)$qty;
+                 if ($qty <= 0) {
+                     continue;
+                 }
+                 $components[$guid] = true;
+                 $rows[] = [
+                     'record_id'        => $cp['id'] ?? $cp['write_off_id'],
+                     'sale_date'        => $cp['date'] ?? $cp['write_off_date'],
+                     'product_id'       => $cp['product_id'],
+                     'product_name'     => $cp['name'] ?? $cp['product_name'],
+                     'quantity_product' => $cp['quantity_product'],
+                     'component_guid'   => $guid,
+                     'quantity'         => $qty,
+                     'type'             => $type,
+                     'operation'        => $cp['operation'] ?? '',
+                 ];
+             }
+         }
+         if (empty($rows)) {
+             return [];
+         }
+         $guids = array_keys($components);
+         $nomenclatures = Products1cNomenclature::find()
+             ->andWhere(['id' => $guids])
+             ->andWhere(['not in', 'category', ['', 'букет', 'сборка', 'сервис']])
+             ->indexBy('id')
+             ->all();
+         $priceDynamics = PricesDynamic::find()
+             ->andWhere(['region_id' => $region])
+             ->andWhere(['product_id' => array_values(ArrayHelper::getColumn($nomenclatures, 'id'))])
+             ->orderBy(['date_from' => SORT_DESC])
+             ->all();
+         $pricesByProduct = [];
+         foreach ($priceDynamics as $pd) {
+             $pricesByProduct[$pd->product_id][] = $pd;
+         }
+         $result = [];
+         foreach ($rows as $r) {
+             $guid  = $r['component_guid'];
+             $n     = $nomenclatures[$guid] ?? null;
+             $pid   = $n?->id;
+             $price = 0;
+             foreach ($pricesByProduct[$pid] ?? [] as $pd) {
+                 if ($pd->date_from <= $r['sale_date'] && $pd->date_to >= $r['sale_date']) {
+                     if ($pid == '2b72702a-792f-11e8-9edd-1c6f659fb563') {
+                         $price = 8.66; // TODO: заглушка для цены гелия - исправить
+                     } else {
+                         $price = $pd->price;
+                     }
+                     break;
+                 }
+             }
+             $cost = $r['quantity'] * $price * $r['quantity_product'];
+             $costComponent = $r['quantity'] * $price;
+             $result[] = [
+                 'store_id'             => $storeId,
+                 'record_id'            => $r['record_id'],
+                 'sale_date'            => $r['sale_date'],
+                 'product_id'           => $r['product_id'],
+                 'product_name'         => $r['product_name'],
+                 'quantity_product'     => $r['quantity_product'],
+                 'component_guid'       => $guid,
+                 'component_name'       => $n?->name,
+                 'component_category'   => $n?->category,
+                 'component_subcategory'=> $n?->subcategory,
+                 'component_species'    => $n?->species,
+                 'quantity'             => $r['quantity'],
+                 'price'                => $price,
+                 'cost'                 => $cost,
+                 'component_cost'       => $costComponent,
+                 'type'                 => $type,
+                 'operation'            => $r['operation'],
+             ];
+         }
+         return $result;
+     }
+     public function sumProductsComponentsByGroup(array $items, string $type, string $group = 'category'): array
+     {
+         $aggregated = [];
+         foreach ($items as $row) {
+             $storeId     = $row['store_id'];
+             $category    = $row['component_category'];
+             $subcategory = $row['component_subcategory'];
+             $species     = $row['component_species'];
+             $operation   = $row['operation'] ?? null;
+             $cost = (float)$row['cost'];
+             if (
+                 $type === AutoPlannogrammaService::TYPE_SALES
+                 || ($row['type'] ?? null) === AutoPlannogrammaService::TYPE_SALES
+             ) {
+                 if ($operation === 'Возврат') {
+                     $cost = -$cost;
+                 }
+             }
+             $keyParts = [$storeId, $category];
+             if ($group === 'subcategory' || $group === 'species') {
+                 $keyParts[] = $subcategory;
+             }
+             if ($group === 'species') {
+                 $keyParts[] = $species;
+             }
+             $key = implode('|', $keyParts);
+             if (!isset($aggregated[$key])) {
+                 // базовая структура
+                 $aggregated[$key] = [
+                     'store_id'  => $storeId,
+                     'category'  => $category,
+                     'sum'       => 0.0,
+                     'type'      => $type,
+                 ];
+                 if ($group === 'subcategory' || $group === 'species') {
+                     $aggregated[$key]['subcategory'] = $subcategory;
+                 }
+                 if ($group === 'species') {
+                     $aggregated[$key]['species'] = $species;
+                 }
+             }
+             $aggregated[$key]['sum'] += $cost;
+         }
+         return array_values($aggregated);
+     }
++
++
 +
 +
 +    /**
 +     * Считает взвешенную долю категорий за целевой месяц:
 +     * берёт три предыдущих к «месяцу-цели» месяца (пропуская сразу предыдущий),
 +     * присваивает им веса 3, 2 и 1 (самый старый месяц — 3, самый свежий — 1),
 +     * суммирует взвешенные итоги по каждой категории и выдаёт их доли
 +     * от общего взвешенного итога.
 +     *
 +     * @param string $month Целевой месяц в формате 'YYYY-MM'
 +     * @param array|null $filters ['store_id'=>…, …]
 +     * @param array|null $productFilter Опционально: [product_id, …]
 +     * @param string $type 'sales' или 'writeOffs'
 +     * @return array      [
 +     *   <store_id> => [
 +     *     ['category'=>string, 'total_sum'=>float, 'share_of_total'=>float],
 +     *     …
 +     *   ],
 +     *   …
 +     * ]
 +     */
 +    public function getMonthCategoryShareOrWriteOffWeighted(
 +        string $month,
 +        ?array $filters = null,
 +        ?array $productFilter = null,
 +        string $type = 'sales'
 +    ): array
 +    {
 +        $stores = $this->getVisibleStores();
 +        $storeIds = array_map(fn($s) => $s->id, $stores);
 +        if (!empty($filters['store_id'])) {
 +            $storeIds = array_intersect($storeIds, [(int)$filters['store_id']]);
 +        }
 +        if (empty($storeIds)) {
 +            return [];
 +        }
 +
 +        $baseMonth = strtotime("{$month}-01");
 +
 +        $monthOffsets = [3, 4, 5];
 +        $monthWeights = [3, 2, 1];
 +
 +
 +        $weightedSums = [];
 +        $monthStoreTotalsWeighted = [];
 +
 +        foreach ($monthOffsets as $idx => $offsetMonths) {
 +            $w = $monthWeights[$idx];
 +            $start = date('Y-m-01 00:00:00', strtotime("-{$offsetMonths} months", $baseMonth));
 +            $end = date('Y-m-t 23:59:59', strtotime($start));
 +
 +
 +
 +            $q = (new Query())
 +                ->select([
 +                    'store_id' => 'ex.entity_id',
 +                    'category' => 'p1c.category',
 +                    'month_sum' => new Expression('SUM(CAST(wop.summ AS NUMERIC))'),
 +                ])
 +                ->from(['w' => 'write_offs']);
 +
 +            if ($type === 'writeOffs') {
 +                $q->leftJoin(['ex' => 'export_import_table'], 'ex.export_val = w.store_id')
 +                    ->leftJoin(['wop' => 'write_offs_products'], 'wop.write_offs_id = w.id')
 +                    ->leftJoin(['p1c' => 'products_1c_nomenclature'], 'p1c.id = wop.product_id')
 +                    ->andWhere(['>=', 'w.date', $start])
 +                    ->andWhere(['<=', 'w.date', $end]);
 +
 +                if ($productFilter !== null) {
 +                    $q->andWhere(['wop.product_id' => $productFilter]);
 +                }
 +            } else {
 +                $q->leftJoin(['sp' => 'sales_products'], 'sp.check_id = s.id')
 +                    ->leftJoin(['ex' => 'export_import_table'], 'ex.export_val = s.store_id_1c')
 +                    ->leftJoin(['p1c' => 'products_1c_nomenclature'], 'p1c.id = sp.product_id')
 +                    ->andWhere(['>=', 's.date', $start])
 +                    ->andWhere(['<=', 's.date', $end]);
 +
 +                if ($productFilter !== null) {
 +                    $q->andWhere(['sp.product_id' => $productFilter]);
 +                }
 +            }
 +
 +            $q->andWhere(['ex.entity_id' => $storeIds])
 +                // ->andWhere(['<>', 'p1c.category', ''])
 +                ->groupBy(['ex.entity_id', 'p1c.category']);
 +
 +            $rows = $q->all();
 +
 +            foreach ($rows as $r) {
 +                $sid = $r['store_id'];
 +                $cat = $r['category'];
 +                $sum = (float)$r['month_sum'] * $w;
 +                $weightedSums[$sid][$cat] = ($weightedSums[$sid][$cat] ?? 0) + $sum;
 +            }
 +
 +        }
 +
 +        $result = [];
 +        foreach ($weightedSums as $storeId => $cats) {
 +            $grand = array_sum($cats) ?: 1;
 +
 +            $unlabeledSum = $cats[''] ?? 0;
 +            $labeledCount = count($cats) - (isset($cats['']) ? 1 : 0);
 +
 +            $distribution = $labeledCount > 0 ? $unlabeledSum / $labeledCount : 0;
 +
 +            foreach ($cats as $category => $weightedSum) {
 +                if ($category === '') {
 +                    continue;
 +                }
 +
 +                $adjustedSum = $weightedSum + $distribution;
 +
 +                $result[$storeId][] = [
 +                    'category' => $category,
 +                    'total_sum_cat' => $weightedSum,
 +                    'total_sum_store' => $grand,
 +                    'share_of_total' => round($adjustedSum / $grand, 4),
 +                ];
 +            }
 +        }
 +        return $result;
 +    }
 +
 +    public function getMonthSubcategoryShareOrWriteOffWeighted(string $dateFrom, ?array $filters = null, ?array $productFilter = null, string $type = 'sales'): array
 +    {
 +        try {
 +            $dt = new \DateTime($dateFrom);
 +        } catch (\Exception $e) {
 +            // Неверный формат даты
 +            return [];
 +        }
 +        $month = (int)$dt->format('m');
 +        $year  = (int)$dt->format('Y');
 +
 +        $stores  = $this->getVisibleStores();
 +        $storeIds = array_map(fn($s) => $s->id, $stores);
 +        if (!empty($filters['store_id'])) {
 +            $storeIds = array_intersect($storeIds, [(int)$filters['store_id']]);
 +        }
 +        if (empty($storeIds)) {
 +            return [];
 +        }
 +
 +        $years = [$year - 2, $year - 1];
 +
 +        $query = (new Query())
 +            ->select([
 +                'store_id'     => 'ex.entity_id',
 +                'subcategory'  => 'p1c.subcategory',
 +                'category'     => 'p1c.category',
 +                'total_sum'    => new Expression(
 +                    $type === 'writeOffs'
 +                        ? 'SUM(CAST(wop.summ AS NUMERIC))'
 +                        : 'SUM(sp.summ)'
 +                ),
 +            ]);
 +
 +        if ($type === 'writeOffs') {
 +            $query->from(['w' => 'write_offs'])
 +                ->leftJoin(['ex'  => 'export_import_table'],      'ex.export_val = w.store_id')
 +                ->leftJoin(['wop'=> 'write_offs_products'],      'wop.write_offs_id = w.id')
 +                ->leftJoin(['p1c'=> 'products_1c_nomenclature'], 'p1c.id = wop.product_id')
 +
 +                ->andWhere(['in', new Expression('EXTRACT(YEAR FROM w.date)'), $years])
 +                ->andWhere(['=', new Expression('EXTRACT(MONTH FROM w.date)'), $month]);
 +            if ($productFilter !== null) {
 +                $query->andWhere(['wop.product_id' => $productFilter]);
 +            }
 +        } else {
 +            $query->from(['s' => 'sales'])
 +                ->leftJoin(['sp'  => 'sales_products'],         'sp.check_id = s.id')
 +                ->leftJoin(['ex'  => 'export_import_table'],    'ex.export_val = s.store_id_1c')
 +                ->leftJoin(['p1c' => 'products_1c_nomenclature'],'p1c.id = sp.product_id')
 +                ->andWhere(['in', new Expression('EXTRACT(YEAR FROM s.date)'), $years])
 +                ->andWhere(['=', new Expression('EXTRACT(MONTH FROM s.date)'), $month]);
 +            if ($productFilter !== null) {
 +                $query->andWhere(['sp.product_id' => $productFilter]);
 +            }
 +        }
 +
 +        $query->andWhere(['ex.entity_id' => $storeIds])
 +            ->andWhere(['<>', 'p1c.subcategory', ''])
 +            ->groupBy(['ex.entity_id', 'p1c.subcategory', 'p1c.category']);
 +
 +        $rows = $query->all();
 +        if (empty($rows)) {
 +            return [];
 +        }
 +
 +        $sumByStoreCategory = [];
 +        foreach ($rows as $r) {
 +            $sid = $r['store_id'];
 +            $cat = $r['category'];
 +            $sumByStoreCategory[$sid][$cat] = ($sumByStoreCategory[$sid][$cat] ?? 0) + $r['total_sum'];
 +        }
 +
 +
 +        $result = [];
 +        foreach ($rows as $r) {
 +            $sid   = $r['store_id'];
 +            $cat   = $r['category'];
 +            $total = $sumByStoreCategory[$sid][$cat] ?: 1;
 +            $result[] = [
 +                'store_id'         => $sid,
 +                'category'         => $cat,
 +                'subcategory'      => $r['subcategory'],
 +                'total_sum'        => $r['total_sum'],
 +                'percent_of_month' => round($r['total_sum'] / $total, 4),
 +            ];
 +        }
 +
 +        return $result;
 +    }
 +
 +    public function getMonthSpeciesShareOrWriteOffWeighted(
 +        string $dateFrom,
 +        string $dateTo,
 +        ?array $filters = null,
 +        ?array $productFilter = null,
 +        string $type = 'sales'
 +    ): array
 +    {
 +        try {
 +            $dt    = new \DateTime($dateFrom);
 +        } catch (\Exception $e) {
 +            return [];
 +        }
 +        $month = (int)$dt->format('m');
 +        $year  = (int)$dt->format('Y');
 +
 +        $stores   = $this->getVisibleStores();
 +        $storeIds = array_map(fn($s) => $s->id, $stores);
 +        if (!empty($filters['store_id'])) {
 +            $storeIds = array_intersect($storeIds, [(int)$filters['store_id']]);
 +        }
 +        if (empty($storeIds)) {
 +            return [];
 +        }
 +
 +        $years = [$year - 2, $year - 1];
 +
 +        $query = (new Query())->select([
 +            'store_id'   => 'ex.entity_id',
 +            'category'   => 'p1c.category',
 +            'subcategory'=> 'p1c.subcategory',
 +            'species'    => 'p1c.species',
 +            'total_sum'  => new Expression(
 +                $type === 'writeOffs'
 +                    ? 'SUM(CAST(wop.summ AS NUMERIC))'
 +                    : 'SUM(sp.summ)'
 +            ),
 +        ]);
 +
 +        if ($type === 'writeOffs') {
 +            $query->from(['w' => 'write_offs'])
 +                ->leftJoin(['ex'  => 'export_import_table'],       'ex.export_val = w.store_id')
 +                ->leftJoin(['wop' => 'write_offs_products'],       'wop.write_offs_id = w.id')
 +                ->leftJoin(['p1c' => 'products_1c_nomenclature'],  'p1c.id = wop.product_id')
 +                ->andWhere(['IN', new Expression('EXTRACT(YEAR FROM w.date)'), $years])
 +                ->andWhere(['=',  new Expression('EXTRACT(MONTH FROM w.date)'), $month]);
 +            if ($productFilter !== null) {
 +                $query->andWhere(['wop.product_id' => $productFilter]);
 +            }
 +        } else {
 +            $query->from(['s' => 'sales'])
 +                ->leftJoin(['sp'  => 'sales_products'],            'sp.check_id = s.id')
 +                ->leftJoin(['ex'  => 'export_import_table'],       'ex.export_val = s.store_id_1c')
 +                ->leftJoin(['p1c' => 'products_1c_nomenclature'],  'p1c.id = sp.product_id')
 +                ->andWhere(['IN', new Expression('EXTRACT(YEAR FROM s.date)'), $years])
 +                ->andWhere(['=',  new Expression('EXTRACT(MONTH FROM s.date)'), $month]);
 +            if ($productFilter !== null) {
 +                $query->andWhere(['sp.product_id' => $productFilter]);
 +            }
 +        }
 +
 +        $query->andWhere(['ex.entity_id' => $storeIds])
 +            ->andWhere(['<>', 'p1c.species', ''])
 +            ->groupBy([
 +                'ex.entity_id',
 +                'p1c.category',
 +                'p1c.subcategory',
 +                'p1c.species',
 +            ]);
 +
 +        $rows = $query->all();
 +        if (empty($rows)) {
 +            return [];
 +        }
 +
 +        $sumByStoreSubcategory = [];
 +        foreach ($rows as $r) {
 +            $sid = $r['store_id'];
 +            $sub = $r['subcategory'];
 +            $sumByStoreSubcategory[$sid][$sub] =
 +                ($sumByStoreSubcategory[$sid][$sub] ?? 0) + $r['total_sum'];
 +        }
 +
 +        $result = [];
 +        foreach ($rows as $r) {
 +            $sid   = $r['store_id'];
 +            $sub   = $r['subcategory'];
 +            $total = $sumByStoreSubcategory[$sid][$sub] ?: 1;
 +            $result[] = [
 +                'store_id'         => $sid,
 +                'category'         => $r['category'],
 +                'subcategory'      => $sub,
 +                'species'          => $r['species'],
 +                'total_sum'        => (float)$r['total_sum'],
 +                'percent_of_month' => round($r['total_sum'] / $total, 4),
 +            ];
 +        }
 +
 +        return $result;
 +    }
 +
 +    // Недельные расчеты
 +
 +
 +    /**
 +     * Получает суммы продаж или списаний по видам (species) для каждой недели указанного месяца.
 +     *
 +     * @param string      $monthYear     месяц-год в формате MM-YYYY, например '03-2025'
 +     * @param array|null  $filters       опциональные фильтры ['store_id'=>...]
 +     * @param array|null  $productFilter опциональный фильтр по product_id
 +     * @param string      $type          'sales' или 'writeOffs'
 +     * @return array<int, array>         массив строк: [
 +     *    ['week'=>1,'store_id'=>...,'category'=>...,'subcategory'=>...,'species'=>...,'sum'=>...],
 +     *    ...
 +     * ]
 +     */
 +    public function getWeeklySpeciesDataForMonth(
 +        string $monthYear,
 +        ?array $filters = null,
 +        ?array $productFilter = null,
 +        string $type = 'sales'
 +    ): array {
 +        [$monthStr, $yearStr] = explode('-', $monthYear);
 +        $month = (int)$monthStr;
 +        $year  = (int)$yearStr;
 +
 +        $dateFrom = strtotime(sprintf('%04d-%02d-01 00:00:00', $year, $month));
 +        $dateTo   = strtotime('+1 month -1 second', $dateFrom);
 +
 +        $stores   = $this->getVisibleStores();
 +        $storeIds = array_map(fn($s)=>$s->id, $stores);
 +        if (!empty($filters['store_id'])) {
 +            $storeIds = array_intersect($storeIds, [(int)$filters['store_id']]);
 +        }
 +        if (empty($storeIds)) {
 +            return [];
 +        }
 +
 +        $dayOfWeek = (int)date('N', $dateFrom);
 +        $firstMonday = $dayOfWeek === 1
 +            ? $dateFrom
 +            : strtotime('next monday', $dateFrom);
 +
 +
 +        $weekRanges = [];
 +        for ($wkStart = $firstMonday; $wkStart <= $dateTo; $wkStart += 7 * 86400) {
 +            $wkEnd = $wkStart + 6 * 86400;
 +            if ($wkEnd > $dateTo) {
 +                $wkEnd = $dateTo;
 +            }
 +            $periodStart = max($wkStart, $dateFrom);
 +            $periodEnd   = min($wkEnd, $dateTo);
 +            $daysInMonth   = floor(($periodEnd - $periodStart) / 86400) + 1;
 +            if ($daysInMonth >= 4) {
 +                $weekRanges[] = [
 +                    'index' => (int)date('W', $wkStart),
 +                    'start' => date('Y-m-d H:i:s', $wkStart),
 +                    'end'   => date('Y-m-d 23:59:59', $wkEnd),
 +                ];
 +            }
 +        }
 +
 +        $result = [];
 +
 +        foreach ($weekRanges as $range) {
 +            $exprWeek = new Expression((string)$range['index']);
 +            $query = (new Query())->select([
 +                'week'        => $exprWeek,
 +                'store_id'    => 'ex.entity_id',
 +                'category'    => 'p1c.category',
 +                'subcategory' => 'p1c.subcategory',
 +                'species'     => 'p1c.species',
 +                'total_sum'   => new Expression(
 +                    $type === 'writeOffs'
 +                        ? 'SUM(CAST(wop.summ AS NUMERIC))'
 +                        : 'SUM(sp.summ)'
 +                ),
 +            ]);
 +
 +            if ($type === 'writeOffs') {
 +                $query->from(['w'   => 'write_offs'])
 +                    ->leftJoin(['ex'  => 'export_import_table'],      'ex.export_val = w.store_id')
 +                    ->leftJoin(['wop'=> 'write_offs_products'],       'wop.write_offs_id = w.id')
 +                    ->leftJoin(['p1c'=> 'products_1c_nomenclature'],  'p1c.id = wop.product_id')
 +                    ->andWhere(['>=', 'w.date', $range['start']])
 +                    ->andWhere(['<=', 'w.date', $range['end']]);
 +                if ($productFilter !== null) {
 +                    $query->andWhere(['wop.product_id' => $productFilter]);
 +                }
 +            } else {
 +                $query->from(['s' => 'sales'])
 +                    ->leftJoin(['sp' => 'sales_products'],            'sp.check_id = s.id')
 +                    ->leftJoin(['ex' => 'export_import_table'],       'ex.export_val = s.store_id_1c')
 +                    ->leftJoin(['p1c'=> 'products_1c_nomenclature'],  'p1c.id = sp.product_id')
 +                    ->andWhere(['>=', 's.date', $range['start']])
 +                    ->andWhere(['<=', 's.date', $range['end']]);
 +                if ($productFilter !== null) {
 +                    $query->andWhere(['sp.product_id' => $productFilter]);
 +                }
 +            }
 +
 +            $query->andWhere(['ex.entity_id' => $storeIds])
 +                ->andWhere(['<>', 'p1c.species', ''])
 +                ->groupBy(['week','ex.entity_id','p1c.category','p1c.subcategory','p1c.species']);
 +
 +            $rows = $query->all();
 +            foreach ($rows as $row) {
 +                $result[] = [
 +                    'week'        => $row['week'],
 +                    'store_id'    => $row['store_id'],
 +                    'category'    => $row['category'],
 +                    'subcategory' => $row['subcategory'],
 +                    'species'     => $row['species'],
 +                    'sum'         => (float)$row['total_sum'],
 +                ];
 +            }
 +        }
 +
 +        return $result;
 +    }
 +
 +    /**
 +     * Исторический недельный отчёт и доли по видам с учётом store_id.
 +     *
 +     * @param string      $monthYear     месяц-год в формате MM-YYYY
 +     * @param array|null  $filters
 +     * @param array|null  $productFilter
 +     * @param string      $type
 +     * @return array{ 'weeksData': array<int, array> }
 +     *   возвращает плоский список строк:
 +     *   [
 +     *     ['week'=>1, 'store_id'=>2, 'category'=>'...', 'subcategory'=>'...', 'species'=>'...', 'percent'=>0.32],
 +     *     ...
 +     *   ]
 +     */
 +    public function getHistoricalWeeklySpeciesShare(
 +        string $monthYear,
 +        ?array $filters = null,
 +        ?array $productFilter = null,
 +        string $type = 'sales'
 +    ): array {
 +        [$monthStr, $yearStr] = explode('-', $monthYear);
 +        $month = (int)$monthStr;
 +        $year  = (int)$yearStr;
 +
 +        $historical = [];
 +        for ($yr = $year - 2; $yr < $year; $yr++) {
 +            $mYear = sprintf('%02d-%d', $month, $yr);
 +            $weeklyData = $this->getWeeklySpeciesDataForMonth(
 +                $mYear, $filters, $productFilter, $type
 +            );
 +
 +            foreach ($weeklyData as $row) {
 +                $week     = $row['week'];
 +                $sid      = $row['store_id'];
 +                $cat      = $row['category'];
 +                $sub      = $row['subcategory'];
 +                $spec     = $row['species'];
 +                $sumWeek  = $row['sum'];
 +
 +                $historical[$week]           ??= [];
 +                $historical[$week][$sid]     ??= [];
 +                $historical[$week][$sid][$cat]   ??= [];
 +                $historical[$week][$sid][$cat][$sub] ??= [];
 +                $historical[$week][$sid][$cat][$sub][$spec] =
 +                    ($historical[$week][$sid][$cat][$sub][$spec] ?? 0) + $sumWeek;
 +            }
 +        }
 +
 +        $dateFrom = sprintf('%04d-%02d-01 00:00:00', $year, $month);
 +        $dateTo   = date('Y-m-d H:i:s', strtotime("$dateFrom +1 month -1 second"));
 +        $monthWeighted = $this->getMonthSpeciesShareOrWriteOffWeighted(
 +            $dateFrom, $dateTo, $filters, $productFilter, $type
 +        );
 +        $monthMap = [];
 +        foreach ($monthWeighted as $m) {
 +            $sid      = $m['store_id'];
 +            $cat      = $m['category'];
 +            $sub      = $m['subcategory'];
 +            $spec     = $m['species'];
 +            $sumMonth = $m['total_sum'];
 +
 +            $monthMap[$sid]            ??= [];
 +            $monthMap[$sid][$cat]      ??= [];
 +            $monthMap[$sid][$cat][$sub]??= [];
 +            $monthMap[$sid][$cat][$sub][$spec] =
 +                ($monthMap[$sid][$cat][$sub][$spec] ?? 0) + $sumMonth;
 +        }
 +
 +
 +        $weeksList = array_keys($historical);
 +        sort($weeksList, SORT_NUMERIC);
 +
 +        $speciesList = [];
 +        foreach ($monthMap as $sid => $byCat) {
 +            foreach ($byCat as $cat => $bySub) {
 +                foreach ($bySub as $sub => $bySpec) {
 +                    foreach ($bySpec as $spec => $_) {
 +                        $speciesList[] = compact('sid','cat','sub','spec');
 +                    }
 +                }
 +            }
 +        }
 +
 +
 +        $rows = [];
 +        foreach ($speciesList as $comb) {
 +            $sid  = $comb['sid'];
 +            $cat  = $comb['cat'];
 +            $sub  = $comb['sub'];
 +            $spec = $comb['spec'];
 +
 +            $sumMonth = $monthMap[$sid][$cat][$sub][$spec] ?? 0;
 +            if ($sumMonth <= 0) {
 +                continue; // нет месячного итога
 +            }
 +
 +            foreach ($weeksList as $week) {
 +                $sumWeek = $historical[$week][$sid][$cat][$sub][$spec] ?? 0;
 +                $percent = $sumWeek > 0 ? round($sumWeek / $sumMonth, 4) : null;
 +
 +                $rows[] = [
 +                    'week'        => $week,
 +                    'store_id'    => $sid,
 +                    'category'    => $cat,
 +                    'subcategory' => $sub,
 +                    'species'     => $spec,
 +                    'sumWeek'    => $sumWeek,
 +                    'percent'     => $percent,
 +                ];
 +            }
 +        }
 +
 +        $grouped = [];
 +        foreach ($rows as $idx => $row) {
 +            $key = "{$row['store_id']}|{$row['category']}|{$row['subcategory']}|{$row['species']}";
 +            $grouped[$key][] = $idx;
 +        }
 +        foreach ($grouped as $key => $indices) {
 +            $sumPercent = 0.0;
 +            foreach ($indices as $i) {
 +                $sumPercent += $rows[$i]['percent'];
 +            }
 +            if ($sumPercent < 1.0) {
 +                $diff = 1.0 - $sumPercent;
 +                $count = count($indices);
 +                $add = $diff / $count;
 +                foreach ($indices as $i) {
 +                    $rows[$i]['percent'] = round($rows[$i]['percent'] + $add, 4);
 +                }
 +            }
 +        }
 +
 +        return ['weeksData' => $rows];
 +    }
 +
 +
 +    /**
 +     * Рассчитывает недельную цель для каждого вида (species) по данным недельных долей
 +     * и целям месяца.
 +     * @param array $weeksShareData
 +     * @param array $monthSpeciesGoals
 +     * @return array
 +     *   Плоский массив строк с полями: week, store_id, category, subcategory,
 +     *   species, percent, monthly_goal, weekly_goal
 +     */
 +    public function calculateWeeklySpeciesGoals(
 +        array $weeksShareData,
 +        array $monthSpeciesGoals
 +    ): array {
 +        $monthSpeciesGoalsMap = [];
 +        foreach ($monthSpeciesGoals as $monthSpeciesGoal) {
 +            $monthSpeciesGoalsMap[$monthSpeciesGoal['store_id']]
 +            [$monthSpeciesGoal['category']]
 +            [$monthSpeciesGoal['subcategory']]
 +            [$monthSpeciesGoal['species']] = $monthSpeciesGoal['goal'] ;
 +        }
 +        $result = [];
 +        foreach ($weeksShareData as $row) {
 +            $week   = $row['week'];
 +            $sid    = $row['store_id'];
 +            $cat    = $row['category'];
 +            $sub    = $row['subcategory'];
 +            $spec   = $row['species'];
 +            $percent = $row['percent'];
 +
 +            $monthlyGoal = $monthSpeciesGoalsMap[$sid][$cat][$sub][$spec] ?? null;
 +
 +            $weeklyGoal = 0;
 +            if ($monthlyGoal !== null && $percent !== null) {
 +                $weeklyGoal =  round($percent * $monthlyGoal, 4);
 +            }
 +
 +            $result[] = [
 +                'week'         => $week,
 +                'store_id'     => $sid,
 +                'category'     => $cat,
 +                'subcategory'  => $sub,
 +                'species'      => $spec,
 +                'percent'      => $percent,
 +                'monthly_goal' => $monthlyGoal,
 +                'weekly_goal'  => $weeklyGoal,
 +            ];
 +        }
 +        return $result;
 +    }
 +
 +    /**
 +     * Возвращает дату понедельника ISO-недели в формате YYYY-MM-DD
 +     *
 +     * @param int $year  ISO-год (может отличаться от календарного в границах года)
 +     * @param int $week  номер ISO-недели (1–53)
 +     * @return string    дата понедельника, например '2025-03-10'
 +     */
 +    public static function getIsoWeekStart(int $year, int $week): string
 +    {
 +        $iso = $year . 'W' . str_pad($week, 2, '0', STR_PAD_LEFT) . '1';
 +        return date('Y-m-d', strtotime($iso));
 +    }
 +
 +
 +    public static function calculateWeekForecastSpeciesProducts($category, $subcategory, $species, $storeId, $goal)
 +    {
 +        $speciesProductForecast = [];
 +        $products = Products1cNomenclature::find()
 +            ->select(['id', 'name'])
 +            ->where(['category' => $category])
 +            ->andWhere(['subcategory' => $subcategory])
 +            ->andWhere(['species' => $species])
 +            ->indexBy('id')
 +            ->asArray()
 +            ->all();
 +
 +        $productsIds = ArrayHelper::getColumn($products, 'id');
 +        if (CityStore::find()->where(['id' => $storeId])->one()->city_id == 1342) {
 +            $region = 52;
 +        } elseif (CityStore::find()->where(['id' => $storeId])->one()->city_id == 1) {
 +            $region = 77;
 +        } else {
 +            $region = null;
 +        }
 +        $priceRecords = PricesDynamic::find()
 +            ->select(['product_id', 'price'])
 +            ->where(['product_id' => $productsIds])
 +            ->andWhere(['active' => 1])
 +            ->andWhere(['or', ['region_id' => $region], ['region_id' => null]])
 +            ->indexBy('product_id')
 +            ->asArray()
 +            ->all();
 +
 +        foreach ($priceRecords as $id => $record) {
 +            if ($goal == 0 || (int)$record['price'] == 0) {
 +                $forecast = 0;
 +            } else {
 +                $forecast = round(max($goal / (float)$record['price'], 1), 0);
 +            }
 +            $speciesProductForecast[] = [
 +                'category' => $category,
 +                'subcategory' => $subcategory,
 +                'species' => $species,
 +                'product_id' => $record['product_id'],
 +                'name' => $products[$id]['name'],
 +                'price' => $record['price'] ?? $goal ?? 1,
 +                'goal' => $goal ?? 0,
 +                'forecast' => $forecast
 +
 +            ];
 +
 +        }
 +
 +        return $speciesProductForecast;
 +
 +    }
 +
 +    // альтернативные методы расчета списаний
 +
 +    public function calculateFullGoalChainWeighted(array $filters): array
 +    {
 +        $datePlan = $filters['plan_date'];
 +        $dateFromForCategory = (new \DateTime($datePlan))->modify('-' . (self::CATEGORY_LOOKBACK_MONTHS + self::LOOKBACK_MONTHS) . ' months')->format('Y-m-d');
 +
 +        $monthCategoryShare = $this->getMonthCategoryShareOrWriteOffWeighted($datePlan, $filters, null, $filters['type']);
 +        $monthCategoryGoal = $this->getMonthCategoryGoal($monthCategoryShare, $datePlan, $filters);
 +
 +        $monthSubcategoryShare = $this->getMonthSubcategoryShareOrWriteOff($datePlan, $filters);
 +        $monthSubcategoryShare = $this->getMonthSubcategoryShareOrWriteOffWeighted($datePlan, $filters, null, $filters['type']);
 +        $monthSubcategoryGoal = $this->getMonthSubcategoryGoal($monthSubcategoryShare, $monthCategoryGoal);
 +
 +        $monthSpeciesShare = $this->getMonthSpeciesShareOrWriteOff($datePlan, $filters);
 +        if ($filters['type'] === 'writeOffs') {
 +            $salesSubShare        = $this->getMonthSubcategoryShareOrWriteOffWeighted($datePlan, $filters, null, 'sales');
 +            $salesSubGoal         = $this->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 = $this->getMonthSubcategoryGoal($monthSubcategoryShare, $monthCategoryGoal);
 +        }
 +
 +        $monthSpeciesShare = $this->getMonthSpeciesShareOrWriteOffWeighted($datePlan, $datePlan, $filters, null, $filters['type']);
 +        $monthSpeciesGoal = $this->getMonthSpeciesGoalDirty($monthSpeciesShare, $monthSubcategoryGoal);
 +        if ($filters['type'] === 'writeOffs') {
 +            $salesSpecShare = $this->getMonthSpeciesShareOrWriteOffWeighted($datePlan, $datePlan, $filters, null, 'sales');
 +            $salesSpecGoal  = $this->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 = $this->getMonthSpeciesGoalDirty($monthSpeciesShare, $monthSubcategoryGoal);
 +        }
 +
 +        $filtered = array_filter($monthSpeciesGoal, function ($row) use ($filters) {
 +            foreach ($filters as $key => $value) {
 +                if ($value === null || $value === '') {
 +                    continue;
 +                }
 +
 +                if (!array_key_exists($key, $row)) {
 +                    continue;
 +                }
 +
 +                if (is_numeric($row[$key]) && is_numeric($value)) {
 +                    if ((float)$row[$key] !== (float)$value) {
 +                        return false;
 +                    }
 +                } else {
 +                    if (stripos((string)$row[$key], (string)$value) === false) {
 +                        return false;
 +                    }
 +                }
 +            }
 +            return true;
 +        });
 +
 +        return array_values($filtered);
 +    }
 +
  }