From: marina Date: Mon, 12 May 2025 12:13:46 +0000 (+0300) Subject: ERP-413 Обработка ОС Планограмма (месяц) X-Git-Url: https://gitweb.erp-flowers.ru/?a=commitdiff_plain;h=1415369da11a6a197d4b8178326fa07595f482e5;p=erp24_rep%2Fyii-erp24%2F.git ERP-413 Обработка ОС Планограмма (месяц) --- diff --git a/erp24/controllers/AutoPlannogrammaController.php b/erp24/controllers/AutoPlannogrammaController.php index 0976c1ab..05489a84 100644 --- a/erp24/controllers/AutoPlannogrammaController.php +++ b/erp24/controllers/AutoPlannogrammaController.php @@ -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'] ); diff --git a/erp24/services/AutoPlannogrammaService.php b/erp24/services/AutoPlannogrammaService.php index 30606625..9477ff32 100644 --- a/erp24/services/AutoPlannogrammaService.php +++ b/erp24/services/AutoPlannogrammaService.php @@ -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 diff --git a/erp24/views/auto-plannogramma/1.php b/erp24/views/auto-plannogramma/1.php index 7f5fae05..265601f9 100644 --- a/erp24/views/auto-plannogramma/1.php +++ b/erp24/views/auto-plannogramma/1.php @@ -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]], ], ]); ?> diff --git a/erp24/views/auto-plannogramma/3.php b/erp24/views/auto-plannogramma/3.php index 502fc2c0..5caf65b6 100644 --- a/erp24/views/auto-plannogramma/3.php +++ b/erp24/views/auto-plannogramma/3.php @@ -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]], ], ]); ?> diff --git a/erp24/views/auto-plannogramma/5.php b/erp24/views/auto-plannogramma/5.php index 0b4a1313..000bdaa4 100644 --- a/erp24/views/auto-plannogramma/5.php +++ b/erp24/views/auto-plannogramma/5.php @@ -39,11 +39,11 @@
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' => 'Название товара', 'name' => 'species'], + 'options' => ['placeholder' => 'Тип товара', 'name' => 'species'], 'pluginOptions' => ['allowClear' => true], ])->label('Товар') ?>
@@ -102,6 +102,6 @@ ['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]], ], ]); ?> diff --git a/erp24/views/auto-plannogramma/6.php b/erp24/views/auto-plannogramma/6.php index 4cb9bce3..db1e5ea9 100644 --- a/erp24/views/auto-plannogramma/6.php +++ b/erp24/views/auto-plannogramma/6.php @@ -39,11 +39,11 @@
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' => 'Название товара', 'name' => 'species'], + 'options' => ['placeholder' => 'Тип товара', 'name' => 'species'], 'pluginOptions' => ['allowClear' => true], ])->label('Товар') ?>