]> gitweb.erp-flowers.ru Git - erp24_rep/yii-erp24/.git/commitdiff
Выделение функций и правки по коду
authorfomichev <vladimir.fomichev@erp-flowers.ru>
Wed, 9 Apr 2025 13:24:53 +0000 (16:24 +0300)
committerfomichev <vladimir.fomichev@erp-flowers.ru>
Wed, 9 Apr 2025 13:24:53 +0000 (16:24 +0300)
erp24/services/StorePlanService.php

index c8b7f5ca376e450fdf19dd1e60b559bae16eaac8..efe67fab83fa4c599d47fe092dba1d9819e583db 100755 (executable)
@@ -212,32 +212,116 @@ class StorePlanService
         return $enablePlanAdd;
     }
 
-    public static function calculateHistoricalShare($storeId, $selectedMonth, $category, $subcategory = null, $species = null)
+    /**
+     * Рассчитывает недельные продажи товаров за 3 предыдущих месяца относительно переданного месяца и года
+     * для конкретного магазина и фильтрации по категориям/подкатегориям/видам.
+     *
+     * @param int         $storeId       ID магазина.
+     * @param string      $selectedMonth Месяц в формате "01" - "12".
+     * @param string      $selectedYear  Год в формате "YYYY".
+     * @param string      $category      Категория товаров.
+     * @param string|null $subcategory   Подкатегория товаров (опционально).
+     * @param string|null $species       Вид товаров (опционально).
+     *
+     * @return array [
+     *     'with_history' => [ // товары, у которых есть данные по продажам во всех периодах и активности минимум в 2 недели каждого периода.
+     *         [
+     *             'guid'         => string,    // идентификатор товара.
+     *             'weekly_sales' => [          // продажи по неделям для каждого периода.
+     *                 '2025-01' => [int, int, ...],
+     *                 '2024-12' => [int, int, ...],
+     *                 '2024-11' => [int, int, ...],
+     *             ]
+     *         ],
+     *         ...
+     *     ],
+     *     'without_history' => [ // товары с неполными данными по истории продаж.
+     *         // Структура аналогична.
+     *     ]
+     * ]
+     */
+    public static function calculateHistoricalShare($storeId, $selectedMonth, $selectedYear, $category, $subcategory = null, $species = null)
+    {
+        $baseDate = strtotime("{$selectedYear}-{$selectedMonth}-01");
+
+        // Получаем периоды за 3 предыдущих месяца.
+        $periods = self::getPeriods($baseDate, 3);
+
+        // Получаем историю продаж для каждого периода.
+        $salesHistory = self::getSalesHistory($storeId, $periods, $category, $subcategory, $species);
+
+        // Анализируем и разделяем товары по наличию полноценной истории продаж.
+        return self::analyzeHistory($salesHistory, $periods);
+    }
+
+    /**
+     * Формирует периоды (месяц + недели) за заданное количество предыдущих месяцев от базовой даты.
+     *
+     * @param int $baseDate Timestamp базовой даты.
+     * @param int $count    Количество периодов для получения (по умолчанию 3).
+     *
+     * @return array Массив периодов с ключами вида "YYYY-MM".
+     */
+    private static function getPeriods($baseDate, $count = 3)
     {
-        $currentTimestamp = time();
-        $year = date('Y', $currentTimestamp);
         $periods = [];
-        for ($i = 1; $i <= 3; $i++) {
-            $prevTimestamp = strtotime("-{$i} month", $currentTimestamp);
-            $month = date('m', $prevTimestamp);
-            $periods[$month] = [];
-        }
-        //var_dump($periods); die();
-        foreach ($periods as $monthKey => $month) {
-            $days = cal_days_in_month(CAL_GREGORIAN, (int)$monthKey, (int)$year);
-            $weeks = $days > 28 ? 5 : 4;
-            for ($weekNumber = 1; $weekNumber <= $weeks; $weekNumber++) {
-                $range = Motivation::getWeekRange(null, $weekNumber, (int)$monthKey, $year);
-                $periods[$monthKey][$weekNumber - 1] = $range;
+        for ($i = $count; $i >= 1; $i--) {
+            $timestamp = strtotime("-{$i} month", $baseDate);
+            $month = date('m', $timestamp);
+            $year  = date('Y', $timestamp);
+
+            $periodKey = "{$year}-{$month}";
+            $periods[$periodKey] = [
+                'year'  => $year,
+                'month' => $month,
+                'weeks' => [],
+            ];
+
+            $daysInMonth = cal_days_in_month(CAL_GREGORIAN, (int)$month, (int)$year);
+            $weeksCount = $daysInMonth > 28 ? 5 : 4;
+
+            for ($weekNumber = 1; $weekNumber <= $weeksCount; $weekNumber++) {
+                $range = Motivation::getWeekRange(null, $weekNumber, (int)$month, (int)$year);
+                $periods[$periodKey]['weeks'][$weekNumber - 1] = $range;
             }
         }
-       // var_dump($periods); die();
+
+        return $periods;
+    }
+
+    /**
+     * Получает историю продаж за каждый период по неделям.
+     *
+     * @param int    $storeId
+     * @param array  $periods     Массив периодов, сформированный функцией getPeriods.
+     * @param string $category
+     * @param string|null $subcategory
+     * @param string|null $species
+     *
+     * @return array Структура данных:
+     *      [
+     *          'product_guid1' => [
+     *              'YYYY-MM' => [
+     *                  0 => salesCount_week1,
+     *                  1 => salesCount_week2,
+     *                  ...
+     *              ],
+     *              ...
+     *          ],
+     *          ...
+     *      ]
+     */
+    private static function getSalesHistory($storeId, $periods, $category, $subcategory, $species)
+    {
         $salesHistory = [];
 
-        foreach ($periods as $monthKey => $weeks) {
-            foreach ($weeks as $weekIndex => $week) {
+        foreach ($periods as $periodKey => $periodData) {
+            $year  = $periodData['year'];
+            $month = $periodData['month'];
+
+            foreach ($periodData['weeks'] as $weekIndex => $week) {
                 $dateStart = $week['start_time'] . ' 00:00:00';
-                $dateEnd = $week['end_time'] . ' 23:59:59';
+                $dateEnd   = $week['end_time'] . ' 23:59:59';
 
                 $query = Sales::find()->alias('s')
                     ->select([
@@ -248,7 +332,6 @@ class StorePlanService
                     ->innerJoin('products_1c_nomenclature p1c', 'p1c.id = sp.product_id')
                     ->where(['s.store_id' => $storeId])
                     ->andWhere(['between', 's.date', $dateStart, $dateEnd])
-                    // Условие для чеков: order_id равен '' или '0'
                     ->andWhere(['order_id' => ['', '0']])
                     ->andWhere(['p1c.category' => $category]);
 
@@ -262,53 +345,74 @@ class StorePlanService
                 $query->groupBy('p1c.id');
                 $results = $query->asArray()->all();
 
-
                 foreach ($results as $result) {
                     $guid = $result['product_guid'];
                     if (!isset($salesHistory[$guid])) {
                         $salesHistory[$guid] = [];
                     }
-                    if (!isset($salesHistory[$guid][$monthKey])) {
-                        $salesHistory[$guid][$monthKey] = [];
+                    if (!isset($salesHistory[$guid][$periodKey])) {
+                        $salesHistory[$guid][$periodKey] = [];
                     }
-                    $salesHistory[$guid][$monthKey][$weekIndex] = (int)$result['sales_count'];
+                    $salesHistory[$guid][$periodKey][$weekIndex] = (int)$result['sales_count'];
                 }
             }
         }
-        $productsWithHistory = [];
+
+        return $salesHistory;
+    }
+
+    /**
+     * Анализирует историю продаж, разделяя товары на группы:
+     * - с историей продаж во всех периодах (активность не менее 2-х недель в каждом периоде),
+     * - без полноценной истории продаж.
+     *
+     * @param array $salesHistory Структура продаж, сформированная функцией getSalesHistory.
+     * @param array $periods      Массив периодов, полученный функцией getPeriods.
+     *
+     * @return array [
+     *         'with_history'    => [...],
+     *         'without_history' => [...],
+     * ]
+     */
+    private static function analyzeHistory($salesHistory, $periods)
+    {
+        $productsWithHistory    = [];
         $productsWithoutHistory = [];
 
         foreach ($salesHistory as $guid => $monthsData) {
-            $hasHistoryInAllMonths = true;
+            $hasHistoryInAllPeriods = true;
             $weeklySalesData = [];
 
-            foreach ($periods as $monthKey => $monthData) {
-                if (!isset($monthsData[$monthKey])) {
-                    $hasHistoryInAllMonths = false;
-                    $weekData = [0, 0, 0, 0];
+            foreach ($periods as $periodKey => $periodData) {
+                $weeksCount = count($periodData['weeks']);
+                $checkWeeksCount = min(4, $weeksCount);
+
+                if (!isset($monthsData[$periodKey])) {
+                    $hasHistoryInAllPeriods = false;
+                    $weekData = array_fill(0, $weeksCount, 0);
                 } else {
                     $weekData = [];
                     $activeWeeks = 0;
-                    for ($weekIndex = 0; $weekIndex < 5; $weekIndex++) {
-                        $salesCount = isset($monthsData[$monthKey][$weekIndex]) ? $monthsData[$monthKey][$weekIndex] : 0;
+                    for ($weekIndex = 0; $weekIndex < $checkWeeksCount; $weekIndex++) {
+                        $salesCount = isset($monthsData[$periodKey][$weekIndex]) ? $monthsData[$periodKey][$weekIndex] : 0;
                         $weekData[$weekIndex] = $salesCount;
                         if ($salesCount > 0) {
                             $activeWeeks++;
                         }
                     }
                     if ($activeWeeks < 2) {
-                        $hasHistoryInAllMonths = false;
+                        $hasHistoryInAllPeriods = false;
                     }
                 }
-                $weeklySalesData[$monthKey] = $weekData;
+                $weeklySalesData[$periodKey] = $weekData;
             }
 
             $productData = [
-                'guid' => $guid,
+                'guid'         => $guid,
                 'weekly_sales' => $weeklySalesData,
             ];
 
-            if ($hasHistoryInAllMonths) {
+            if ($hasHistoryInAllPeriods) {
                 $productsWithHistory[] = $productData;
             } else {
                 $productsWithoutHistory[] = $productData;
@@ -316,7 +420,7 @@ class StorePlanService
         }
 
         return [
-            'with_history' => $productsWithHistory,
+            'with_history'    => $productsWithHistory,
             'without_history' => $productsWithoutHistory,
         ];
     }
@@ -325,66 +429,27 @@ class StorePlanService
     /**
      * Метод вычисляет взвешенное значение продаж для товаров без истории.
      *
-     * @param int   $storeId Идентификатор магазина.
-     * @param string $selectedMonth Выбранный месяц в формате "mm" (целевой месяц).
+     * @param int    $storeId                Идентификатор магазина.
+     * @param string $selectedMonth          Выбранный месяц в формате "mm" (целевой месяц).
      * @param array  $productsWithoutHistory Массив товаров без истории, где каждый элемент имеет вид:
      *           [
      *              'guid' => <GUID товара>,
-     *              'weekly_sales' => [ 'YYYY-mm' => [ ... ], ... ]
+     *              'weekly_sales' => [ 'YYYY-MM' => [ ... ], ... ]
      *           ]
+     *
      * @return array Возвращает массив, где ключ – GUID товара, а значение – рассчитанное взвешенное значение продаж.
      */
     public static function calculateWeightedSalesForProductsWithoutHistory($storeId, $selectedMonth, $productsWithoutHistory)
     {
-
-        $year = date('Y');
-        $targetDate = strtotime("$year-$selectedMonth-01");
-
-        $months = [];
-        for ($i = 1; $i <= 3; $i++) {
-            $monthTimestamp = strtotime("-{$i} month", $targetDate);
-            $m = date('m', $monthTimestamp);
-            $y = date('Y', $monthTimestamp);
-            $months[] = ['year' => $y, 'month' => $m];
-        }
+        $targetDate = strtotime(date('Y') . "-$selectedMonth-01");
+        $periods = self::generateWeightedPeriods($targetDate, 3);
 
         $weightedResults = [];
 
         foreach ($productsWithoutHistory as $product) {
             $guid = $product['guid'];
-            //var_dump( $guid); die();
-            $myChars = Products1cAdditionalCharacteristics::find()
-                ->where(['product_id' => $guid])
-                ->asArray()
-                ->all();
-
-            $mySet = [];
-            foreach ($myChars as $char) {
-                $mySet[$char['property_id']] = $char['value'];
-            }
-            ksort($mySet);
-           // var_dump($mySet); die();
-            $countChars = count($mySet);
-           // var_dump($countChars); die();
-            if ($countChars == 0) {
-                $weightedResults[$guid] = 0;
-                continue;
-            }
 
-            $conditions = [];
-            foreach ($mySet as $propId => $val) {
-                $conditions[] = [$propId, $val];
-            }
-            $query = Products1cAdditionalCharacteristics::find()
-                ->select('product_id')
-                ->where(new \yii\db\Expression('(property_id, value) IN (' . implode(', ', array_map(function($pair) {
-                        return "('" . implode("','", $pair) . "')";
-                    }, $conditions)) . ')'))
-                ->groupBy('product_id')
-                ->having('COUNT(*) = :cnt', [':cnt' => $countChars]);
-
-            $similarProductIds = $query->column();
-           // var_dump($similarProductIds); die();
+            $similarProductIds = self::getSimilarProductIDs($guid);
             if (empty($similarProductIds)) {
                 $weightedResults[$guid] = 0;
                 continue;
@@ -392,57 +457,168 @@ class StorePlanService
 
             $medianSales = [];
             $salesValuesForEachMonth = [];
-            foreach ($months as $monthInfo) {
-                $startDate = sprintf('%04d-%02d-01', $monthInfo['year'], $monthInfo['month']);
-                $endDate = sprintf('%04d-%02d-%02d',
-                    $monthInfo['year'],
-                    $monthInfo['month'],
-                    cal_days_in_month(CAL_GREGORIAN, $monthInfo['month'], $monthInfo['year']));
-
-                $salesValues = [];
-
-                foreach ($similarProductIds as $simProdId) {
-                    $sales = Sales::find()->alias('s')
-                        ->innerJoin('sales_products sp', 's.id = sp.check_id')
-                        ->innerJoin('products_1c_nomenclature p1c', 'p1c.id = sp.product_id')
-                        ->where(['p1c.id' => $simProdId])
-                        ->andWhere(['s.store_id' => $storeId])
-                        ->andWhere(['between', 's.date', $startDate . ' 00:00:00', $endDate . ' 23:59:59'])
-                        // Условие для чеков: order_id равен '' или '0'
-                        ->andWhere(['order_id' => ['', '0']])
-                        ->count();
-                    $salesValues[] = (int)$sales;
-                }
-                $salesValuesForEachMonth[] = $salesValues;
-                $nonZeroSales = array_filter($salesValues, function($val) {
-                    return $val > 0;
-                });
-                sort($nonZeroSales, SORT_NUMERIC);
-                $n = count($nonZeroSales);
-                if ($n === 0) {
-                    $median = 0;
-                } elseif ($n % 2 == 1) {
-                    $median = $nonZeroSales[floor($n / 2)];
-                } else {
-                    $median = ($nonZeroSales[$n / 2 - 1] + $nonZeroSales[$n / 2]) / 2;
-                }
-                $medianSales[] = $median;
+            foreach ($periods as $periodKey => $monthInfo) {
+                list($median, $salesValues) = self::calculateMedianSalesForPeriod($storeId, $similarProductIds, $monthInfo);
+                $medianSales[$periodKey]             = $median;
+                $salesValuesForEachMonth[$periodKey]   = $salesValues;
             }
 
             $weights = [3, 2, 1];
-            $weightedValue = 0;
-            for ($i = 0; $i < count($medianSales); $i++) {
-                $weightedValue += $medianSales[$i] * $weights[$i];
-            }
+            $weightedValue = self::computeWeightedValue($medianSales, $weights);
+
             $weightedResults[$guid] = [
                 'weightedValue' => $weightedValue,
                 'medianSales'   => $medianSales,
                 'salesValues'   => $salesValuesForEachMonth,
             ];
         }
+
         return $weightedResults;
     }
 
+    /**
+     * Генерирует периоды для расчёта продаж.
+     *
+     * @param int  $targetDate Timestamp целевой даты.
+     * @param int  $count      Количество предыдущих месяцев.
+     * @param bool $withWeight Если true – к каждому периоду добавляется вес (ключ 'weight').
+     *
+     * @return array Ассоциативный массив, где ключ – "YYYY-MM", а значение – массив с ключами 'year', 'month'
+     *               и, если $withWeight равен true, 'weight'.
+     */
+    private static function generateWeightedPeriods($targetDate, $count = 3, $withWeight = false)
+    {
+        $periods = [];
+        for ($i = 1; $i <= $count; $i++) {
+            $timestamp = strtotime("-{$i} month", $targetDate);
+            $year = date('Y', $timestamp);
+            $month = date('m', $timestamp);
+            $periodKey = sprintf('%04d-%02d', $year, $month);
+
+            $periodData = [
+                'year'  => $year,
+                'month' => $month,
+            ];
+
+            if ($withWeight) {
+                // Вес рассчитывается как 4 - $i, то есть для ближайшего месяца – вес 3, затем 2 и 1.
+                $periodData['weight'] = 4 - $i;
+            }
+
+            $periods[$periodKey] = $periodData;
+        }
+        return $periods;
+    }
+
+    /**
+     * Получает идентификаторы товаров, похожих на указанный товар,
+     * исходя из набора его дополнительных характеристик.
+     *
+     * @param string $guid Идентификатор товара.
+     *
+     * @return array Массив идентификаторов похожих товаров.
+     */
+    private static function getSimilarProductIDs($guid)
+    {
+        $myChars = Products1cAdditionalCharacteristics::find()
+            ->where(['product_id' => $guid])
+            ->asArray()
+            ->all();
+
+        $mySet = [];
+        foreach ($myChars as $char) {
+            $mySet[$char['property_id']] = $char['value'];
+        }
+        ksort($mySet);
+        $countChars = count($mySet);
+        if ($countChars === 0) {
+            return [];
+        }
+
+        $conditions = [];
+        foreach ($mySet as $propId => $val) {
+            $conditions[] = [$propId, $val];
+        }
+        $query = Products1cAdditionalCharacteristics::find()
+            ->select('product_id')
+            ->where(new \yii\db\Expression('(property_id, value) IN (' .
+                implode(', ', array_map(function($pair) {
+                    return "('" . implode("','", $pair) . "')";
+                }, $conditions)) . ')'))
+            ->groupBy('product_id')
+            ->having('COUNT(*) = :cnt', [':cnt' => $countChars]);
+
+        return $query->column();
+    }
+
+    /**
+     * Вычисляет медианное значение продаж для похожих товаров в заданном периоде.
+     *
+     * @param int   $storeId           Идентификатор магазина.
+     * @param array $similarProductIds Массив идентификаторов похожих товаров.
+     * @param array $monthInfo         Массив с ключами 'year' и 'month'.
+     *
+     * @return array Возвращает массив: [медианное значение продаж, массив продаж по каждому товару]
+     */
+    private static function calculateMedianSalesForPeriod($storeId, $similarProductIds, $monthInfo)
+    {
+        $startDate = sprintf('%04d-%02d-01', $monthInfo['year'], $monthInfo['month']);
+        $endDate   = sprintf('%04d-%02d-%02d',
+            $monthInfo['year'],
+            $monthInfo['month'],
+            cal_days_in_month(CAL_GREGORIAN, $monthInfo['month'], $monthInfo['year']));
+
+        $salesValues = [];
+
+        foreach ($similarProductIds as $simProdId) {
+            $sales = Sales::find()->alias('s')
+                ->innerJoin('sales_products sp', 's.id = sp.check_id')
+                ->innerJoin('products_1c_nomenclature p1c', 'p1c.id = sp.product_id')
+                ->where(['p1c.id' => $simProdId])
+                ->andWhere(['s.store_id' => $storeId])
+                ->andWhere(['between', 's.date', $startDate . ' 00:00:00', $endDate . ' 23:59:59'])
+                ->andWhere(['order_id' => ['', '0']])
+                ->count();
+            $salesValues[] = (int)$sales;
+        }
+
+        $nonZeroSales = array_filter($salesValues, function($val) {
+            return $val > 0;
+        });
+        sort($nonZeroSales, SORT_NUMERIC);
+        $n = count($nonZeroSales);
+        if ($n === 0) {
+            $median = 0;
+        } elseif ($n % 2 === 1) {
+            $median = $nonZeroSales[floor($n / 2)];
+        } else {
+            $median = ($nonZeroSales[$n / 2 - 1] + $nonZeroSales[$n / 2]) / 2;
+        }
+
+        return [$median, $salesValues];
+    }
+
+    /**
+     * Вычисляет взвешенное значение на основании медианных продаж по периодам.
+     *
+     * @param array $medianSales Ассоциативный массив медианных продаж, ключи – периоды ("YYYY-MM").
+     * @param array $weights     Массив весов (порядок должен соответствовать порядку перебора медианных продаж).
+     *
+     * @return float Взвешенное значение.
+     */
+    private static function computeWeightedValue($medianSales, $weights)
+    {
+        $weightedValue = 0;
+        $i = 0;
+        foreach ($medianSales as $periodKey => $median) {
+            if (isset($weights[$i])) {
+                $weightedValue += $median * $weights[$i];
+            }
+            $i++;
+        }
+        return $weightedValue;
+    }
+
     /**
      * Получает цену для товара для указанного месяца.
      *
@@ -465,6 +641,8 @@ class StorePlanService
                 ['>=', 'date_to', $monthStart],
                 ['date_to' => '2100-01-01 03:00:00+03']
             ])->all();
+
+
         if (CityStore::find()->where(['id' => $storeId])->one()->city_id == 1342) {
             $region = 52;
         } elseif (CityStore::find()->where(['id' => $storeId])->one()->city_id == 1) {
@@ -472,11 +650,9 @@ class StorePlanService
         } else {
             $region = null;
         }
-
             //$priceRecords->andWhere(['or',['region_id' => $region],['region_id' => null]])
             //->all();
 
-
         if (!empty($priceRecords)) {
             $totalPrice = 0;
             $count = 0;
@@ -491,10 +667,10 @@ class StorePlanService
             $averagePrice = 0;
         }
 
-
         return $averagePrice;
     }
 
+
     /**
      * Вычисляет для каждого товара с историей:
      * - месячные продажи (сумма недельных продаж за месяц),
@@ -502,65 +678,37 @@ class StorePlanService
      * - итоговую взвешенную сумму,
      * - долю товара от общей взвешенной суммы по всем товарам.
      *
-     * @param int   $storeId Идентификатор магазина.
-     * @param string $selectedMonth Целевой месяц в формате "mm".
-     * @param array  $productsWithHistory Массив товаров с историей (из calculateHistoricalShare), каждый элемент содержит:
-     *          [
+     * @param int    $storeId              Идентификатор магазина.
+     * @param string $selectedMonth        Целевой месяц в формате "mm".
+     * @param array  $productsWithHistory  Массив товаров с историей, где каждый элемент имеет вид:
+     *           [
      *              'guid' => <GUID товара>,
-     *              'weekly_sales' => [ 'mm' => [week0, week1, week2, week3], ... ]
-     *          ]
+     *              'weekly_sales' => [ 'YYYY-MM' => [ ... ], ... ]
+     *           ]
+     *
      * @return array Массив, где ключ – GUID товара, а значение – массив с данными:
      *          [
-     *              'monthlySales'    => [ 'mm' => суммарные продажи за месяц, ... ],
-     *              'monthlyWeighted' => [ 'mm' => (продажи * цена * вес), ... ],
+     *              'monthlySales'    => [ 'YYYY-MM' => суммарные продажи за месяц, ... ],
+     *              'monthlyWeighted' => [ 'YYYY-MM' => (продажи * цена * вес), ... ],
      *              'weightedSum'     => итоговая взвешенная сумма,
-     *              'share'           => доля товара (от 0 до 1)
+     *              'share'           => доля товара (от 0 до 1),
+     *              'monthlyPrice'    => [ 'YYYY-MM' => цена ]
      *          ]
      */
     public static function calculateProductSalesShare($storeId, $selectedMonth, $productsWithHistory)
     {
-        $year = date('Y');
-
+        $targetDate = strtotime(date('Y') . "-$selectedMonth-01");
 
-        $weightedMonths = [];
-        for ($i = 1; $i <= 3; $i++) {
-            $timestamp = strtotime("-{$i} month", strtotime("$year-$selectedMonth-01"));
-            $weightedMonths[] = [
-                'year'   => date('Y', $timestamp),
-                'month'  => date('m', $timestamp),
-                'weight' => 4 - $i,
-            ];
-        }
+        $weightedPeriods = self::generateWeightedPeriods($targetDate, 3, true);
 
         $productsData = [];
         $globalTotal = 0;
 
-
         foreach ($productsWithHistory as $product) {
             $guid = $product['guid'];
-            $monthlySales = [];
-            $monthlyWeighted = [];
-            $weightedSum = 0;
-            $monthlyPrice = [];
-
-            foreach ($weightedMonths as $mInfo) {
-                $mKey = $mInfo['month'];
-                $sales = isset($product['weekly_sales'][$mKey]) ? array_sum($product['weekly_sales'][$mKey]) : 0;
-
-                $price = self::getPriceForProductAndMonth($guid, $mInfo['year'], $mInfo['month'], $storeId);
-                $monthlySales[$mKey] = $sales;
-                $weightedValue = $sales * $price * $mInfo['weight'];
-                $monthlyWeighted[$mKey] = $weightedValue;
-                $monthlyPrice[$mKey] = $price;
-                $weightedSum += $weightedValue;
-            }
-            $productsData[$guid] = [
-                'monthlySales'    => $monthlySales,
-                'monthlyWeighted' => $monthlyWeighted,
-                'weightedSum'     => $weightedSum,
-                'monthlyPrice'     => $monthlyPrice,
-            ];
-            $globalTotal += $weightedSum;
+            $result = self::processProductWithHistory($storeId, $product, $weightedPeriods);
+            $productsData[$guid] = $result;
+            $globalTotal += $result['weightedSum'];
         }
 
         foreach ($productsData as $guid => &$data) {
@@ -571,5 +719,52 @@ class StorePlanService
         return $productsData;
     }
 
+
+
+    /**
+     * Обрабатывает товар с историей, вычисляя для него:
+     * - месячные продажи,
+     * - взвешенные продажи (с учетом цены и заданного веса),
+     * - итоговую взвешенную сумму.
+     *
+     * @param int   $storeId         Идентификатор магазина.
+     * @param array $product         Массив с данными товара, содержащий 'guid' и 'weekly_sales' с ключами "YYYY-MM".
+     * @param array $weightedPeriods Ассоциативный массив периодов, с ключами "YYYY-MM" и весами.
+     *
+     * @return array Массив с ключами:
+     *         - monthlySales: [ 'YYYY-MM' => суммарные продажи за период, ... ]
+     *         - monthlyWeighted: [ 'YYYY-MM' => продажи * цена * вес, ... ]
+     *         - weightedSum: итоговая сумма по взвешенным продажам,
+     *         - monthlyPrice: [ 'YYYY-MM' => цена, ... ]
+     */
+    private static function processProductWithHistory($storeId, $product, $weightedPeriods)
+    {
+        $monthlySales    = [];
+        $monthlyWeighted = [];
+        $monthlyPrice    = [];
+        $weightedSum     = 0;
+        $guid            = $product['guid'];
+
+        foreach ($weightedPeriods as $periodKey => $periodInfo) {
+            $sales = isset($product['weekly_sales'][$periodKey])
+                ? array_sum($product['weekly_sales'][$periodKey])
+                : 0;
+            $price = self::getPriceForProductAndMonth($guid, $periodInfo['year'], $periodInfo['month'], $storeId);
+
+            $monthlySales[$periodKey] = $sales;
+            $weightedValue = $sales * $price * $periodInfo['weight'];
+            $monthlyWeighted[$periodKey] = $weightedValue;
+            $monthlyPrice[$periodKey] = $price;
+            $weightedSum += $weightedValue;
+        }
+
+        return [
+            'monthlySales'    => $monthlySales,
+            'monthlyWeighted' => $monthlyWeighted,
+            'weightedSum'     => $weightedSum,
+            'monthlyPrice'    => $monthlyPrice,
+        ];
+    }
+
 }