]> gitweb.erp-flowers.ru Git - erp24_rep/yii-erp24/.git/commitdiff
ERP-413 Обработка ОС Планограмма (месяц)
authormarina <m.zozirova@gmail.com>
Mon, 12 May 2025 12:13:46 +0000 (15:13 +0300)
committermarina <m.zozirova@gmail.com>
Mon, 12 May 2025 12:13:46 +0000 (15:13 +0300)
erp24/controllers/AutoPlannogrammaController.php
erp24/services/AutoPlannogrammaService.php
erp24/views/auto-plannogramma/1.php
erp24/views/auto-plannogramma/3.php
erp24/views/auto-plannogramma/5.php
erp24/views/auto-plannogramma/6.php

index 0976c1ab843aa3e6dcf24df8879fe9af4c1edec9..05489a8490564ee3378d86c328e67ff4f10e275c 100644 (file)
@@ -229,7 +229,7 @@ class AutoPlannogrammaController extends BaseController
             }
 
             $service = new AutoPlannogrammaService();
-            $data = $service->getMonthCategoryShareOrWriteOff($filters['plan_date'], $filters, null, $filters['type']);
+            $data = $service->getMonthCategoryShareOrWriteOff($filters['plan_date'], $filters, $filters['type']);
 
             $flatData = [];
             foreach ($data as $storeId => $categories) {
@@ -238,7 +238,7 @@ class AutoPlannogrammaController extends BaseController
                         'store_id' => (string)$storeId,
                         'category' => $row['category'] ?? null,
                         'total_sum' => $row['total_sum'] ?? null,
-                        'share_of_total' => $row['share_of_total'] ?? null,
+                        'percent' => $row['percent'] ?? null,
                     ];
                 }
             }
@@ -346,7 +346,7 @@ class AutoPlannogrammaController extends BaseController
             }
             $service = new AutoPlannogrammaService();
 
-            $data = $service->getMonthSubcategoryShareOrWriteOff($filters['plan_date'], $filters, null, $filters['type']);
+            $data = $service->getMonthSubcategoryShareOrWriteOff($filters['plan_date'], $filters, $filters['type']);
 
             $flatData = array_filter($data, function ($row) use ($filters) {
                 foreach ($filters as $key => $value) {
@@ -463,7 +463,6 @@ class AutoPlannogrammaController extends BaseController
             $data = $service->getMonthSpeciesShareOrWriteOff(
                 date('Y-m-d', strtotime($filters['plan_date'] . ' -3 months')),
                 $filters,
-                null,
                 $filters['type']
             );
 
index 306066257e86231a259cc9cd4fc25bb289f7b314..9477ff328b237f7b4d86828d0bfce741adabef74 100644 (file)
@@ -10,120 +10,205 @@ use yii_app\records\SalesWriteOffsPlan;
 
 class AutoPlannogrammaService
 {
+    private const TYPE_SALES = 'sales'; // Тип операции: продажи
+    private const TYPE_WRITE_OFFS = 'writeOffs'; // Тип операции: списания
+    private const OPERATION_SALE = 'Продажа'; // Операция продажи
+    private const DATE_FORMAT = 'Y-m-d'; // Формат даты
+    private const CATEGORY_LOOKBACK_MONTHS = 3; // Период для анализа категорий (месяцы)
+    private const SUBCATEGORY_LOOKBACK_MONTHS = 24; // Период для анализа подкатегорий (месяцы)
+    private const LOOKBACK_MONTHS = 2; // Отступаемый шаг от плановой даты перед расчетами
+
+    /**
+     * Получение списка видимых магазинов
+     * @return CityStore[] Массив моделей магазинов
+     */
     private function getVisibleStores(): array
     {
         return CityStore::findAll(['visible' => CityStore::IS_VISIBLE]);
     }
 
-    public function getStoreTotals(array $storeIds, string $dateFrom, ?array $productFilter = null, string $type = 'sales', ?string $dateTo = null): array
+    /**
+     * Создание базового запроса для подсчета итогов
+     * @param string $type Тип операции (продажи или списания)
+     * @return Query Объект запроса
+     */
+    private function buildBaseTotalsQuery(string $type): Query
     {
-        $query = (new Query())
+        // Определяем SQL-выражение для SUM
+        $sumExpression = $type === self::TYPE_WRITE_OFFS
+            ? 'SUM(wp.summ)'
+            : 'SUM(CASE WHEN s.operation = :operation THEN sp.summ ELSE 0 END)';
+
+        // Определяем таблицу и псевдоним
+        $fromTable = $type === self::TYPE_WRITE_OFFS
+            ? ['w' => 'write_offs']
+            : ['s' => 'sales'];
+
+        return (new Query())
             ->select([
                 'store_id' => 'ex.entity_id',
-                'total_sum' => new Expression(
-                    $type === 'writeOffs'
-                        ? 'SUM(wp.summ)'
-                        : "SUM(CASE WHEN s.operation = 'Продажа' THEN sp.summ ELSE 0 END)"
-                )
+                'total_sum' => new Expression($sumExpression, ['operation' => self::OPERATION_SALE])
             ])
-            ->from(['s' => $type === 'writeOffs' ? 'write_offs' : 'sales']);
+            ->from($fromTable);
+    }
 
-        if ($type == 'writeOffs') {
-                $query->leftJoin('export_import_table ex', 'ex.export_val = w.store_id')
+    /**
+     * Применение соединений таблиц в зависимости от типа операции
+     * @param Query $query Объект запроса
+     * @param string $type Тип операции
+     * @return Query Обновленный запрос
+     */
+    private function applyJoins(Query $query, string $type): Query
+    {
+        if ($type === self::TYPE_WRITE_OFFS) {
+            $query->leftJoin('export_import_table ex', 'ex.export_val = w.store_id')
                 ->leftJoin('write_offs_products wp', 'wp.write_offs_id = w.id');
-
         } else {
             $query->leftJoin('sales_products sp', 'sp.check_id = s.id')
                 ->leftJoin('export_import_table ex', 'ex.export_val = s.store_id_1c');
         }
+        return $query;
+    }
+
+    /**
+     * Получение итогов по магазинам
+     * @param array $storeIds Список ID магазинов
+     * @param string $dateFrom Дата начала периода
+     * @param array|null $productFilter Фильтр по продуктам
+     * @param string $type Тип операции
+     * @param string|null $dateTo Дата окончания периода
+     * @return array Ассоциативный массив с итогами
+     * @throws \InvalidArgumentException
+     */
+    public function getStoreTotals(array $storeIds, string $dateFrom, ?array $productFilter = null, string $type = self::TYPE_SALES, ?string $dateTo = null): array
+    {
+        if (empty($storeIds)) {
+            return [];
+        }
+
+        if (!in_array($type, [self::TYPE_SALES, self::TYPE_WRITE_OFFS], true)) {
+            throw new \InvalidArgumentException("Недопустимый тип операции: $type");
+        }
+
+        $query = $this->buildBaseTotalsQuery($type);
+        $query = $this->applyJoins($query, $type);
 
         $query->where(['>=', 'date', $dateFrom])
-            ->andWhere(['ex.entity_id' => $storeIds]);
+            ->andWhere(['ex.entity_id' => $storeIds])
+            ->groupBy('ex.entity_id');
 
         if ($productFilter !== null) {
-            if ($type === 'writeOffs') {
-                $query->andWhere(['w.product_id'=> $productFilter]);
-            } else {
-                $query->andWhere(['sp.product_id' => $productFilter]);
-            }
+            $query->andWhere([$type === self::TYPE_WRITE_OFFS ? 'wp.product_id' : 'sp.product_id' => $productFilter]);
         }
 
         if ($dateTo !== null) {
-            $query->andWhere(['<=', 'w.date', $dateTo]);
+            $query->andWhere(['<=', 'date', $dateTo]);
         }
 
-        $query->groupBy('ex.entity_id');
-
-        $rows = $query->all();
-
-        return ArrayHelper::map($rows, 'store_id', 'total_sum');
+        return ArrayHelper::map($query->all(), 'store_id', 'total_sum');
     }
 
-    public function getMonthCategoryShareOrWriteOff(string $dateFrom, ?array $filters = null, ?array $productFilter = null, string $type = 'sales'): array
+    /**
+     * Получение доли категорий или списаний за месяц
+     * @param string $dateFrom Дата начала периода
+     * @param array|null $filters Дополнительные фильтры
+     * @param array|null $productFilter Фильтр по продуктам
+     * @param string $type Тип операции
+     * @return array Массив с долями категорий
+     * @throws \InvalidArgumentException
+     */
+    public function getMonthCategoryShareOrWriteOff(string $dateFrom, ?array $filters = null, string $type = self::TYPE_SALES): array
     {
-        $stores = $this->getVisibleStores();
-        $storeIds = array_map(fn($s) => $s->id, $stores);
+        // Проверка типа операции
+        if (!in_array($type, [self::TYPE_SALES, self::TYPE_WRITE_OFFS], true)) {
+            throw new \InvalidArgumentException("Недопустимый тип операции: $type");
+        }
 
-        if (!empty($filters) && array_key_exists('store_id', $filters) && $filters['store_id'] !== '') {
+        // Получение ID видимых магазинов
+        $storeIds = array_map(fn($store) => $store->id, $this->getVisibleStores());
+
+        // Применение фильтра по магазину, если указан
+        if (!empty($filters['store_id'])) {
             $storeIds = array_intersect($storeIds, [(int)$filters['store_id']]);
         }
 
-        $totals = $this->getStoreTotals($storeIds, $dateFrom, null, $type);
-
-        if (empty($totals)) return [];
+        // Формирование компонентов запроса
+        $sumExpression = $type === self::TYPE_WRITE_OFFS ? 'SUM(wp.summ)' : 'SUM(sp.summ)';
+        $fromTable = $type === self::TYPE_WRITE_OFFS ? ['w' => 'write_offs'] : ['s' => 'sales'];
+        $productJoinCondition = $type === self::TYPE_WRITE_OFFS ? 'wp.product_id' : 'sp.product_id';
+        $storeJoinCondition = $type === self::TYPE_WRITE_OFFS ? 'ex.export_val = w.store_id' : 'ex.export_val = s.store_id_1c';
+        $productTableJoin = $type === self::TYPE_WRITE_OFFS ? ['wp' => 'write_offs_products'] : ['sp' => 'sales_products'];
+        $productTableJoinCondition = $type === self::TYPE_WRITE_OFFS ? 'wp.write_offs_id = w.id' : 'sp.check_id = s.id';
 
+        // Создание запроса с CTE для итоговых сумм
         $query = (new Query())
             ->select([
-                'store_id' => 'ex.entity_id',
-                'category' => 'p1c.category',
-                'total_sum' => new Expression(
-                    $type === 'writeOffs' ? 'SUM(wp.summ)' : 'SUM(sp.summ)'
-                ),
+                'store_id' => 'main.ex_entity_id',
+                'category' => 'main.category',
+                'total_sum' => 'main.total_sum',
+                'percent' => new Expression('ROUND(CAST(main.total_sum AS DECIMAL) / NULLIF(totals.total, 0), 4)'),
+                'type' => new Expression(':type', ['type' => $type]),
             ])
-            ->from($type === 'writeOffs' ? ['w' => 'write_offs'] : ['s' => 'sales']);
-
-        if ($type === 'writeOffs') {
-            $query->join('LEFT JOIN', 'export_import_table ex', 'ex.export_val = w.store_id')
-                ->leftJoin('write_offs_products wp', 'wp.write_offs_id = w.id')
-                ->leftJoin('products_1c_nomenclature p1c', 'p1c.id = wp.product_id')
-                ->where(['>=', 'w.date', $dateFrom])
-                ->andWhere(['ex.entity_id' => $storeIds])
-                ->andWhere(['<>', 'p1c.category', ''])
-                ->groupBy(['ex.entity_id', 'p1c.category']);
-
-            if ($productFilter !== null) {
-                $query->andWhere(['wp.product_id' => $productFilter]);
-            }
-        } else {
-            $query->leftJoin('sales_products sp', 'sp.check_id = s.id')
-                ->leftJoin('products_1c_nomenclature p1c', 'p1c.id = sp.product_id')
-                ->leftJoin('export_import_table ex', 'ex.export_val = s.store_id_1c')
-                ->where(['>=', 's.date', $dateFrom])
-                ->andWhere(['ex.entity_id' => $storeIds])
-                ->andWhere(['<>', 'p1c.category', ''])
-                ->groupBy(['ex.entity_id', 'p1c.category']);
-
-            if ($productFilter !== null) {
-                $query->andWhere(['sp.product_id' => $productFilter]);
-            }
-        }
+            ->from([
+                'main' => (new Query())
+                    ->select([
+                        'ex_entity_id' => 'ex.entity_id',
+                        'category' => 'p1c.category',
+                        'total_sum' => new Expression($sumExpression),
+                    ])
+                    ->from($fromTable)
+                    ->leftJoin($productTableJoin, $productTableJoinCondition)
+                    ->leftJoin('products_1c_nomenclature p1c', "p1c.id = $productJoinCondition")
+                    ->leftJoin('export_import_table ex', $storeJoinCondition)
+                    ->where(['<=', key($fromTable) . '.date', $dateFrom])
+                    ->andWhere(['>=', key($fromTable) . '.date',(new \DateTime($dateFrom))->modify('-' . self::CATEGORY_LOOKBACK_MONTHS . ' months')->format('Y-m-d')])
+                    ->andWhere(['ex.entity_id' => $storeIds])
+                    ->andWhere(['<>', 'p1c.category', ''])
+                    ->groupBy(['ex.entity_id', 'p1c.category']),
+            ])
+            ->innerJoin(
+                ['totals' => (new Query())
+                    ->select([
+                        'store_id' => 'ex.entity_id',
+                        'total' => new Expression($sumExpression),
+                    ])
+                    ->from($fromTable)
+                    ->leftJoin($productTableJoin, $productTableJoinCondition)
+                    ->leftJoin('products_1c_nomenclature p1c', "p1c.id = $productJoinCondition")
+                    ->leftJoin('export_import_table ex', $storeJoinCondition)
+                    ->where(['<=', key($fromTable) . '.date', $dateFrom])
+                    ->andWhere(['>=', key($fromTable) . '.date',(new \DateTime($dateFrom))->modify('-' . self::CATEGORY_LOOKBACK_MONTHS . ' months')->format('Y-m-d')])
+                    ->andWhere(['ex.entity_id' => $storeIds])
+                    ->andWhere(['<>', 'p1c.category', ''])
+                    ->groupBy(['ex.entity_id'])],
+                'main.ex_entity_id = totals.store_id'
+            );
+
+        // Выполнение запроса и обработка результатов
         $rows = $query->all();
         $result = [];
 
         foreach ($rows as $row) {
             $storeId = $row['store_id'];
-            $total = $totals[$storeId] ?? 1;
             $result[$storeId][] = [
                 'category' => $row['category'],
                 'total_sum' => $row['total_sum'],
-                'share_of_total' => round($row['total_sum'] / $total, 4),
-                'type' => $type,
+                'percent' => $row['percent'],
+                'type' => $row['type'],
             ];
         }
+        
         return $result;
     }
 
-    public function getMonthCategoryGoal(array $categoryShare, $datePlan, $filters): array
+    /**
+     * Получение целей по категориям за месяц
+     * @param array $categoryShare Доли категорий
+     * @param string $datePlan Дата плана
+     * @param array $filters Фильтры
+     * @return array Массив с целями по категориям
+     */
+    public function getMonthCategoryGoal(array $categoryShare, string $datePlan, array $filters): array
     {
         $timestamp = strtotime($datePlan);
         $year = date('Y', $timestamp);
@@ -138,14 +223,15 @@ class AutoPlannogrammaService
         $result = [];
 
         foreach ($plans as $storeId => $plan) {
-            if (!isset($categoryShare[$storeId])) continue;
+            if (!isset($categoryShare[$storeId])) {
+                continue;
+            }
 
             foreach ($categoryShare[$storeId] as $item) {
                 $result[] = [
                     'category' => $item['category'],
                     'store_id' => $storeId,
-                    'store_name' => CityStore::findOne($storeId)->name ?? null,
-                    'goal' => round($item['share_of_total'] * ($filters['type'] == 'writeOffs' ? $plan['write_offs_plan'] : $plan['total_sales_plan']), 2),
+                    'goal' => round($item['percent'] * ($filters['type'] === self::TYPE_WRITE_OFFS ? $plan['write_offs_plan'] : $plan['total_sales_plan']), 2),
                 ];
             }
         }
@@ -153,74 +239,98 @@ class AutoPlannogrammaService
         return $result;
     }
 
-    public function getMonthSubcategoryShareOrWriteOff(string $dateFrom, ?array $filters = null, ?array $productFilter = null, string $type = 'sales'): array
+    /**
+     * Получение доли подкатегорий или списаний за месяц
+     * @param string $dateFrom Дата начала периода
+     * @param array|null $filters Дополнительные фильтры
+     * @param string $type Тип операции
+     * @return array Массив с долями подкатегорий
+     * @throws \InvalidArgumentException
+     */
+    public function getMonthSubcategoryShareOrWriteOff(string $dateFrom, ?array $filters = null, string $type = self::TYPE_SALES): array
     {
-        $stores = $this->getVisibleStores();
-        $storeIds = array_map(fn($s) => $s->id, $stores);
+        if (!in_array($type, [self::TYPE_SALES, self::TYPE_WRITE_OFFS], true)) {
+            throw new \InvalidArgumentException("Недопустимый тип операции: $type");
+        }
 
-        if (!empty($filters) && array_key_exists('store_id', $filters) && $filters['store_id'] !== '') {
+        $storeIds = array_map(fn($store) => $store->id, $this->getVisibleStores());
+        if (!empty($filters['store_id'])) {
             $storeIds = array_intersect($storeIds, [(int)$filters['store_id']]);
         }
 
-        $totals = $this->getStoreTotals($storeIds, $dateFrom, $productFilter, $type);
-        if (empty($totals)) return [];
+        $sumExpression = $type === self::TYPE_WRITE_OFFS ? 'SUM(wp.summ)' : 'SUM(sp.summ)';
+        $fromTable = $type === self::TYPE_WRITE_OFFS ? ['w' => 'write_offs'] : ['s' => 'sales'];
+        $productJoinCondition = $type === self::TYPE_WRITE_OFFS ? 'wp.product_id' : 'sp.product_id';
+        $storeJoinCondition = $type === self::TYPE_WRITE_OFFS ? 'ex.export_val = w.store_id' : 'ex.export_val = s.store_id_1c';
+        $productTableJoin = $type === self::TYPE_WRITE_OFFS ? ['wp' => 'write_offs_products'] : ['sp' => 'sales_products'];
+        $productTableJoinCondition = $type === self::TYPE_WRITE_OFFS ? 'wp.write_offs_id = w.id' : 'sp.check_id = s.id';
 
         $query = (new Query())
             ->select([
-                'store_id' => 'ex.entity_id',
-                'subcategory' => 'p1c.subcategory',
-                'category' => 'p1c.category',
-                'total_sum' => new Expression(
-                    $type === 'writeOffs' ? 'SUM(wp.summ)' : 'SUM(sp.summ)'
-                ),
-            ]);
-
-        if ($type === 'writeOffs') {
-            $query->from(['w' => 'write_offs'])
-                ->join('LEFT JOIN', 'export_import_table ex', 'ex.export_val = w.store_id')
-                ->leftJoin('write_offs_products wp', 'wp.write_offs_id = w.id')
-                ->leftJoin('products_1c_nomenclature p1c', 'p1c.id = wp.product_id')
-                ->where(['>=', 'w.date', $dateFrom])
-                ->andWhere(['ex.entity_id' => $storeIds])
-                ->andWhere(['<>', 'p1c.subcategory', ''])
-                ->groupBy(['ex.entity_id', 'p1c.subcategory', 'p1c.category']);
-
-            if ($productFilter !== null) {
-                $query->andWhere(['wp.product_id' => $productFilter]);
-            }
-        } else {
-            $query->from(['s' => 'sales'])
-                ->leftJoin('sales_products sp', 'sp.check_id = s.id')
-                ->leftJoin('products_1c_nomenclature p1c', 'p1c.id = sp.product_id')
-                ->leftJoin('export_import_table ex', 'ex.export_val = s.store_id_1c')
-                ->where(['>=', 's.date', $dateFrom])
-                ->andWhere(['ex.entity_id' => $storeIds])
-                ->andWhere(['<>', 'p1c.subcategory', ''])
-                ->groupBy(['ex.entity_id', 'p1c.subcategory', 'p1c.category']);
-
-            if ($productFilter !== null) {
-                $query->andWhere(['sp.product_id' => $productFilter]);
-            }
-        }
+                'store_id' => 'main.ex_entity_id',
+                'category' => 'main.category',
+                'subcategory' => 'main.subcategory',
+                'total_sum' => 'main.total_sum',
+                'percent' => new Expression('ROUND(CAST(main.total_sum AS DECIMAL) / NULLIF(totals.total, 0), 4)'),
+                'type' => new Expression(':type', ['type' => $type]),
+            ])
+            ->from([
+                'main' => (new Query())
+                    ->select([
+                        'ex_entity_id' => 'ex.entity_id',
+                        'category' => 'p1c.category',
+                        'subcategory' => 'p1c.subcategory',
+                        'total_sum' => new Expression($sumExpression),
+                    ])
+                    ->from($fromTable)
+                    ->leftJoin($productTableJoin, $productTableJoinCondition)
+                    ->leftJoin('products_1c_nomenclature p1c', "p1c.id = $productJoinCondition")
+                    ->leftJoin('export_import_table ex', $storeJoinCondition)
+                    ->where(['>=', 'date', $dateFrom])
+                    ->andWhere(['ex.entity_id' => $storeIds])
+                    ->andWhere(['<>', 'p1c.subcategory', ''])
+                    ->groupBy(['ex.entity_id', 'p1c.category', 'p1c.subcategory']),
+            ])
+            ->innerJoin(
+                ['totals' => (new Query())
+                    ->select([
+                        'store_id' => 'ex.entity_id',
+                        'total' => new Expression($sumExpression),
+                    ])
+                    ->from($fromTable)
+                    ->leftJoin($productTableJoin, $productTableJoinCondition)
+                    ->leftJoin('products_1c_nomenclature p1c', "p1c.id = $productJoinCondition")
+                    ->leftJoin('export_import_table ex', $storeJoinCondition)
+                    ->where(['>=', 'date', $dateFrom])
+                    ->andWhere(['ex.entity_id' => $storeIds])
+                    ->andWhere(['<>', 'p1c.subcategory', ''])
+                    ->groupBy(['ex.entity_id'])],
+                'main.ex_entity_id = totals.store_id'
+            );
 
         $rows = $query->all();
         $result = [];
 
         foreach ($rows as $row) {
-            $storeId = $row['store_id'];
-            $total = $totals[$storeId] ?? 1;
             $result[] = [
-                'store_id' => $storeId,
+                'store_id' => $row['store_id'],
                 'category' => $row['category'],
                 'subcategory' => $row['subcategory'],
                 'total_sum' => $row['total_sum'],
-                'percent_of_month' => round($row['total_sum'] / $total, 4),
+                'percent' => $row['percent'],
+                'type' => $row['type'],
             ];
         }
 
         return $result;
     }
 
+    /**
+     * Получение целей по подкатегориям за месяц
+     * @param array $subcategoryShare Доли подкатегорий
+     * @param array $categoryGoals Цели по категориям
+     * @return array Массив с целями по подкатегориям
+     */
     public function getMonthSubcategoryGoal(array $subcategoryShare, array $categoryGoals): array
     {
         $indexedGoals = [];
@@ -236,7 +346,7 @@ class AutoPlannogrammaService
                     'category' => $sub['category'],
                     'subcategory' => $sub['subcategory'],
                     'store_id' => $sub['store_id'],
-                    'goal' => round($sub['percent_of_month'] * $goal, 2),
+                    'goal' => round($sub['percent'] * $goal, 2),
                 ];
             }
         }
@@ -244,76 +354,100 @@ class AutoPlannogrammaService
         return $result;
     }
 
-    public function getMonthSpeciesShareOrWriteOff(string $dateFrom, ?array $filters = null, ?array $productFilter = null, string $type = 'sales'): array
+    /**
+     * Получение доли видов или списаний за месяц
+     * @param string $dateFrom Дата начала периода
+     * @param array|null $filters Дополнительные фильтры
+     * @param string $type Тип операции
+     * @return array Массив с долями видов
+     * @throws \InvalidArgumentException
+     */
+    public function getMonthSpeciesShareOrWriteOff(string $dateFrom, ?array $filters = null, string $type = self::TYPE_SALES): array
     {
-        $stores = $this->getVisibleStores();
-        $storeIds = array_map(fn($s) => $s->id, $stores);
+        if (!in_array($type, [self::TYPE_SALES, self::TYPE_WRITE_OFFS], true)) {
+            throw new \InvalidArgumentException("Недопустимый тип операции: $type");
+        }
 
-        if (!empty($filters) && array_key_exists('store_id', $filters) && $filters['store_id'] !== '') {
+        $storeIds = array_map(fn($store) => $store->id, $this->getVisibleStores());
+        if (!empty($filters['store_id'])) {
             $storeIds = array_intersect($storeIds, [(int)$filters['store_id']]);
         }
 
-        $totals = $this->getStoreTotals($storeIds, $dateFrom, $productFilter, $type);
-        if (empty($totals)) return [];
+        $sumExpression = $type === self::TYPE_WRITE_OFFS ? 'SUM(wp.summ)' : 'SUM(sp.summ)';
+        $fromTable = $type === self::TYPE_WRITE_OFFS ? ['w' => 'write_offs'] : ['s' => 'sales'];
+        $productJoinCondition = $type === self::TYPE_WRITE_OFFS ? 'wp.product_id' : 'sp.product_id';
+        $storeJoinCondition = $type === self::TYPE_WRITE_OFFS ? 'ex.export_val = w.store_id' : 'ex.export_val = s.store_id_1c';
+        $productTableJoin = $type === self::TYPE_WRITE_OFFS ? ['wp' => 'write_offs_products'] : ['sp' => 'sales_products'];
+        $productTableJoinCondition = $type === self::TYPE_WRITE_OFFS ? 'wp.write_offs_id = w.id' : 'sp.check_id = s.id';
 
         $query = (new Query())
             ->select([
-                'store_id' => 'ex.entity_id',
-                'species' => 'p1c.name',
-                'category' => 'p1c.category',
-                'subcategory' => 'p1c.subcategory',
-                'total_sum' => new Expression(
-                    $type === 'writeOffs' ?  'SUM(wp.summ)' : 'SUM(sp.summ)'
-                ),
-            ]);
-
-        if ($type === 'writeOffs') {
-            $query->from(['w' => 'write_offs'])
-                ->join('LEFT JOIN', 'export_import_table ex', 'ex.export_val = w.store_id')
-                ->leftJoin('write_offs_products wp', 'wp.write_offs_id = w.id')
-                ->leftJoin('products_1c_nomenclature p1c', 'p1c.id = wp.product_id')
-                ->where(['>=', 'w.date', $dateFrom])
-                ->andWhere(['ex.entity_id' => $storeIds])
-                ->andWhere(['<>', 'p1c.name', ''])
-                ->groupBy(['ex.entity_id', 'p1c.name', 'p1c.category', 'p1c.subcategory']);
-
-            if ($productFilter !== null) {
-                $query->andWhere(['item ->> \'product_id\'' => $productFilter]);
-            }
-        } else {
-            $query->from(['s' => 'sales'])
-                ->leftJoin('sales_products sp', 'sp.check_id = s.id')
-                ->leftJoin('products_1c_nomenclature p1c', 'p1c.id = sp.product_id')
-                ->leftJoin('export_import_table ex', 'ex.export_val = s.store_id_1c')
-                ->where(['>=', 's.date', $dateFrom])
-                ->andWhere(['ex.entity_id' => $storeIds])
-                ->andWhere(['<>', 'p1c.name', ''])
-                ->groupBy(['ex.entity_id', 'p1c.name', 'p1c.category', 'p1c.subcategory']);
-
-            if ($productFilter !== null) {
-                $query->andWhere(['sp.product_id' => $productFilter]);
-            }
-        }
+                'store_id' => 'main.ex_entity_id',
+                'category' => 'main.category',
+                'subcategory' => 'main.subcategory',
+                'species' => 'main.species',
+                'total_sum' => 'main.total_sum',
+                'percent' => new Expression('ROUND(CAST(main.total_sum AS DECIMAL) / NULLIF(totals.total, 0), 4)'),
+                'type' => new Expression(':type', ['type' => $type]),
+            ])
+            ->from([
+                'main' => (new Query())
+                    ->select([
+                        'ex_entity_id' => 'ex.entity_id',
+                        'category' => 'p1c.category',
+                        'subcategory' => 'p1c.subcategory',
+                        'species' => 'p1c.species',
+                        'total_sum' => new Expression($sumExpression),
+                    ])
+                    ->from($fromTable)
+                    ->leftJoin($productTableJoin, $productTableJoinCondition)
+                    ->leftJoin('products_1c_nomenclature p1c', "p1c.id = $productJoinCondition")
+                    ->leftJoin('export_import_table ex', $storeJoinCondition)
+                    ->andWhere(['ex.entity_id' => $storeIds])
+                    ->andWhere(['<>', 'p1c.species', ''])
+                    ->groupBy(['ex.entity_id', 'p1c.category', 'p1c.subcategory', 'p1c.species']),
+            ])
+            ->innerJoin(
+                ['totals' => (new Query())
+                    ->select([
+                        'store_id' => 'ex.entity_id',
+                        'total' => new Expression($sumExpression),
+                    ])
+                    ->from($fromTable)
+                    ->leftJoin($productTableJoin, $productTableJoinCondition)
+                    ->leftJoin('products_1c_nomenclature p1c', "p1c.id = $productJoinCondition")
+                    ->leftJoin('export_import_table ex', $storeJoinCondition)
+                    ->where(['>=', key($fromTable) . '.date', $dateFrom])
+                    ->andWhere(['ex.entity_id' => $storeIds])
+                    ->andWhere(['<>', 'p1c.species', ''])
+                    ->groupBy(['ex.entity_id'])],
+                'main.ex_entity_id = totals.store_id'
+            );
 
         $rows = $query->all();
         $result = [];
 
         foreach ($rows as $row) {
-            $storeId = $row['store_id'];
-            $total = $totals[$storeId] ?? 1;
             $result[] = [
-                'store_id' => $storeId,
+                'store_id' => $row['store_id'],
                 'category' => $row['category'],
                 'subcategory' => $row['subcategory'],
                 'species' => $row['species'],
                 'total_sum' => $row['total_sum'],
-                'percent_of_month' => round($row['total_sum'] / $total, 4),
+                'percent' => $row['percent'],
+                'type' => $row['type'],
             ];
         }
 
         return $result;
     }
 
+    /**
+     * Получение целей по видам за месяц
+     * @param array $speciesShare Доли видов
+     * @param array $subcategoryGoals Цели по подкатегориям
+     * @return array Массив с целями по видам
+     */
     public function getMonthSpeciesGoalDirty(array $speciesShare, array $subcategoryGoals): array
     {
         $indexedGoals = [];
@@ -330,7 +464,7 @@ class AutoPlannogrammaService
                     'subcategory' => $species['subcategory'],
                     'species' => $species['species'],
                     'store_id' => $species['store_id'],
-                    'goal' => round($species['percent_of_month'] * $goal, 2),
+                    'goal' => round($species['percent'] * $goal, 2),
                 ];
             }
         }
@@ -338,11 +472,17 @@ class AutoPlannogrammaService
         return $result;
     }
 
+    /**
+     * Расчет полной цепочки целей
+     * @param array $filters Фильтры
+     * @return array Отфильтрованный массив целей
+     */
     public function calculateFullGoalChain(array $filters): array
     {
         $datePlan = $filters['plan_date'];
-        $dateFromForCategory = (new \DateTime($datePlan))->modify('-12 months')->format('Y-m-d');
-        $dateFrom = (new \DateTime($datePlan))->modify('-3 months')->format('Y-m-d');
+        $dateFromForCategory = (new \DateTime($datePlan))->modify('-' . (self::CATEGORY_LOOKBACK_MONTHS + self::LOOKBACK_MONTHS) . ' months')->format(self::DATE_FORMAT);
+        $dateToForCategory = (new \DateTime($datePlan))->modify('-' . (self::CATEGORY_LOOKBACK_MONTHS + self::LOOKBACK_MONTHS) . ' months')->format(self::DATE_FORMAT);
+        $dateFrom = (new \DateTime($datePlan))->modify('-' . (self::SUBCATEGORY_LOOKBACK_MONTHS + self::LOOKBACK_MONTHS). ' months')->format(self::DATE_FORMAT);
 
         $monthCategoryShare = $this->getMonthCategoryShareOrWriteOff($dateFromForCategory, $filters);
         $monthCategoryGoal = $this->getMonthCategoryGoal($monthCategoryShare, $datePlan, $filters);
@@ -378,4 +518,4 @@ class AutoPlannogrammaService
 
         return array_values($filtered);
     }
-}
+}
\ No newline at end of file
index 7f5fae0555135f3ec0ca5a0f83e06481c4a14616..265601f92ddc19cafcce495e53f267dc1377dfe6 100644 (file)
@@ -74,6 +74,6 @@
         }],
         ['attribute' => 'category', 'label' => 'Категория'],
         ['attribute' => 'total_sum', 'label' => 'Сумма', 'format' => ['decimal', 2]],
-        ['attribute' => 'share_of_total', 'label' => 'Доля', 'format' => ['percent', 2]],
+        ['attribute' => 'percent', 'label' => 'Доля', 'format' => ['percent', 2]],
     ],
 ]); ?>
index 502fc2c0459aa9d5076f6ca0947f7054ec693616..5caf65b650cba96d01b7d0f12180d56cfc209b0d 100644 (file)
@@ -88,6 +88,6 @@
         ['attribute' => 'category', 'label' => 'Категория'],
         ['attribute' => 'subcategory', 'label' => 'Подкатегория'],
         ['attribute' => 'total_sum', 'label' => 'Сумма', 'format' => ['decimal', 2]],
-        ['attribute' => 'percent_of_month', 'label' => 'Доля', 'format' => ['decimal', 2]],
+        ['attribute' => 'percent', 'label' => 'Доля', 'format' => ['percent', 2]],
     ],
 ]); ?>
index 0b4a1313fb24f19933c5093d07974cd18ec41563..000bdaa42e678356e419bf868dfc678021666d92 100644 (file)
         <div class="col-md">
             <?= $form->field(new \yii\base\DynamicModel(['species' => $filters['species'] ?? '']), 'species')->widget(Select2::class, [
                 'data' => ArrayHelper::map(
-                    Products1cNomenclature::find()->select('name')->distinct()->asArray()->all(),
-                    'name',
-                    'name'
+                    Products1cNomenclature::find()->select('species')->distinct()->asArray()->all(),
+                    'species',
+                    'species'
                 ),
-                'options' => ['placeholder' => 'Ð\9dазвание товара', 'name' => 'species'],
+                'options' => ['placeholder' => 'Тип товара', 'name' => 'species'],
                 'pluginOptions' => ['allowClear' => true],
             ])->label('Товар') ?>
         </div>
         ['attribute' => 'subcategory', 'label' => 'Подкатегория'],
         ['attribute' => 'species', 'label' => 'Тип'],
         ['attribute' => 'total_sum', 'label' => 'Сумма', 'format' => ['decimal', 2]],
-        ['attribute' => 'percent_of_month', 'label' => 'Доля', 'format' => ['percent', 2]],
+        ['attribute' => 'percent', 'label' => 'Доля', 'format' => ['percent', 2]],
     ],
 ]); ?>
index 4cb9bce383ce13e8f8ca517623e5d085599323f5..db1e5ea903be5a55ce5017560f02f6f7801682e4 100644 (file)
         <div class="col-md">
             <?= $form->field(new \yii\base\DynamicModel(['species' => $filters['species'] ?? '']), 'species')->widget(Select2::class, [
                 'data' => ArrayHelper::map(
-                    Products1cNomenclature::find()->select('name')->distinct()->asArray()->all(),
-                    'name',
-                    'name'
+                    Products1cNomenclature::find()->select('species')->distinct()->asArray()->all(),
+                    'species',
+                    'species'
                 ),
-                'options' => ['placeholder' => 'Ð\9dазвание товара', 'name' => 'species'],
+                'options' => ['placeholder' => 'Тип товара', 'name' => 'species'],
                 'pluginOptions' => ['allowClear' => true],
             ])->label('Товар') ?>
         </div>