From: fomichev Date: Wed, 28 May 2025 13:06:28 +0000 (+0300) Subject: Добавление недельных методов X-Git-Url: https://gitweb.erp-flowers.ru/?a=commitdiff_plain;h=546538c03971646508759f7a07ee9fab6312d176;p=erp24_rep%2Fyii-erp24%2F.git Добавление недельных методов --- diff --git a/erp24/services/AutoPlannogrammaService.php b/erp24/services/AutoPlannogrammaService.php index fd5308b0..3c3c0d98 100644 --- a/erp24/services/AutoPlannogrammaService.php +++ b/erp24/services/AutoPlannogrammaService.php @@ -549,6 +549,380 @@ class AutoPlannogrammaService return array_values($filtered); } + + // Недельные расчеты + + + /** + * Получает суммы продаж или списаний по видам (species) для каждой недели указанного месяца. + * + * @param string $monthYear месяц-год в формате MM-YYYY, например '03-2025' + * @param array|null $filters опциональные фильтры ['store_id'=>...] + * @param array|null $productFilter опциональный фильтр по product_id + * @param string $type 'sales' или 'writeOffs' + * @return array массив строк: [ + * ['week'=>1,'store_id'=>...,'category'=>...,'subcategory'=>...,'species'=>...,'sum'=>...], + * ... + * ] + */ + public function getWeeklySpeciesDataForMonth( + string $monthYear, + ?array $filters = null, + ?array $productFilter = null, + string $type = 'sales' + ): array { + [$monthStr, $yearStr] = explode('-', $monthYear); + $month = (int)$monthStr; + $year = (int)$yearStr; + + $dateFrom = strtotime(sprintf('%04d-%02d-01 00:00:00', $year, $month)); + $dateTo = strtotime('+1 month -1 second', $dateFrom); + + $stores = $this->getVisibleStores(); + $storeIds = array_map(fn($s)=>$s->id, $stores); + if (!empty($filters['store_id'])) { + $storeIds = array_intersect($storeIds, [(int)$filters['store_id']]); + } + if (empty($storeIds)) { + return []; + } + + $dayOfWeek = (int)date('N', $dateFrom); + $firstMonday = $dayOfWeek === 1 + ? $dateFrom + : strtotime('next monday', $dateFrom); + + + $weekRanges = []; + for ($wkStart = $firstMonday; $wkStart <= $dateTo; $wkStart += 7 * 86400) { + $wkEnd = $wkStart + 6 * 86400; + if ($wkEnd > $dateTo) { + $wkEnd = $dateTo; + } + $periodStart = max($wkStart, $dateFrom); + $periodEnd = min($wkEnd, $dateTo); + $daysInMonth = floor(($periodEnd - $periodStart) / 86400) + 1; + if ($daysInMonth >= 4) { + $weekRanges[] = [ + 'index' => (int)date('W', $wkStart), + 'start' => date('Y-m-d H:i:s', $wkStart), + 'end' => date('Y-m-d 23:59:59', $wkEnd), + ]; + } + } + + $result = []; + + foreach ($weekRanges as $range) { + $exprWeek = new Expression((string)$range['index']); + $query = (new Query())->select([ + 'week' => $exprWeek, + 'store_id' => 'ex.entity_id', + 'category' => 'p1c.category', + 'subcategory' => 'p1c.subcategory', + 'species' => 'p1c.species', + 'total_sum' => new Expression( + $type === 'writeOffs' + ? 'SUM(CAST(wop.summ AS NUMERIC))' + : 'SUM(sp.summ)' + ), + ]); + + if ($type === 'writeOffs') { + $query->from(['w' => 'write_offs']) + ->leftJoin(['ex' => 'export_import_table'], 'ex.export_val = w.store_id') + ->leftJoin(['wop'=> 'write_offs_products'], 'wop.write_offs_id = w.id') + ->leftJoin(['p1c'=> 'products_1c_nomenclature'], 'p1c.id = wop.product_id') + ->andWhere(['>=', 'w.date', $range['start']]) + ->andWhere(['<=', 'w.date', $range['end']]); + if ($productFilter !== null) { + $query->andWhere(['wop.product_id' => $productFilter]); + } + } else { + $query->from(['s' => 'sales']) + ->leftJoin(['sp' => 'sales_products'], 'sp.check_id = s.id') + ->leftJoin(['ex' => 'export_import_table'], 'ex.export_val = s.store_id_1c') + ->leftJoin(['p1c'=> 'products_1c_nomenclature'], 'p1c.id = sp.product_id') + ->andWhere(['>=', 's.date', $range['start']]) + ->andWhere(['<=', 's.date', $range['end']]); + if ($productFilter !== null) { + $query->andWhere(['sp.product_id' => $productFilter]); + } + } + + $query->andWhere(['ex.entity_id' => $storeIds]) + ->andWhere(['<>', 'p1c.species', '']) + ->groupBy(['week','ex.entity_id','p1c.category','p1c.subcategory','p1c.species']); + + $rows = $query->all(); + foreach ($rows as $row) { + $result[] = [ + 'week' => $row['week'], + 'store_id' => $row['store_id'], + 'category' => $row['category'], + 'subcategory' => $row['subcategory'], + 'species' => $row['species'], + 'sum' => (float)$row['total_sum'], + ]; + } + } + + return $result; + } + + /** + * Исторический недельный отчёт и доли по видам с учётом store_id. + * + * @param string $monthYear месяц-год в формате MM-YYYY + * @param array|null $filters + * @param array|null $productFilter + * @param string $type + * @return array{ 'weeksData': array } + * возвращает плоский список строк: + * [ + * ['week'=>1, 'store_id'=>2, 'category'=>'...', 'subcategory'=>'...', 'species'=>'...', 'percent'=>0.32], + * ... + * ] + */ + public function getHistoricalWeeklySpeciesShare( + string $monthYear, + ?array $filters = null, + ?array $productFilter = null, + string $type = 'sales' + ): array { + [$monthStr, $yearStr] = explode('-', $monthYear); + $month = (int)$monthStr; + $year = (int)$yearStr; + + $historical = []; + for ($yr = $year - 2; $yr < $year; $yr++) { + $mYear = sprintf('%02d-%d', $month, $yr); + $weeklyData = $this->getWeeklySpeciesDataForMonth( + $mYear, $filters, $productFilter, $type + ); + + foreach ($weeklyData as $row) { + $week = $row['week']; + $sid = $row['store_id']; + $cat = $row['category']; + $sub = $row['subcategory']; + $spec = $row['species']; + $sumWeek = $row['sum']; + + $historical[$week] ??= []; + $historical[$week][$sid] ??= []; + $historical[$week][$sid][$cat] ??= []; + $historical[$week][$sid][$cat][$sub] ??= []; + $historical[$week][$sid][$cat][$sub][$spec] = + ($historical[$week][$sid][$cat][$sub][$spec] ?? 0) + $sumWeek; + } + } + + $dateFrom = sprintf('%04d-%02d-01 00:00:00', $year, $month); + $dateTo = date('Y-m-d H:i:s', strtotime("$dateFrom +1 month -1 second")); + $monthWeighted = $this->getMonthSpeciesShareOrWriteOffWeighted( + $dateFrom, $dateTo, $filters, $productFilter, $type + ); + $monthMap = []; + foreach ($monthWeighted as $m) { + $sid = $m['store_id']; + $cat = $m['category']; + $sub = $m['subcategory']; + $spec = $m['species']; + $sumMonth = $m['total_sum']; + + $monthMap[$sid] ??= []; + $monthMap[$sid][$cat] ??= []; + $monthMap[$sid][$cat][$sub]??= []; + $monthMap[$sid][$cat][$sub][$spec] = + ($monthMap[$sid][$cat][$sub][$spec] ?? 0) + $sumMonth; + } + + + $weeksList = array_keys($historical); + sort($weeksList, SORT_NUMERIC); + + $speciesList = []; + foreach ($monthMap as $sid => $byCat) { + foreach ($byCat as $cat => $bySub) { + foreach ($bySub as $sub => $bySpec) { + foreach ($bySpec as $spec => $_) { + $speciesList[] = compact('sid','cat','sub','spec'); + } + } + } + } + + + $rows = []; + foreach ($speciesList as $comb) { + $sid = $comb['sid']; + $cat = $comb['cat']; + $sub = $comb['sub']; + $spec = $comb['spec']; + + $sumMonth = $monthMap[$sid][$cat][$sub][$spec] ?? 0; + if ($sumMonth <= 0) { + continue; // нет месячного итога + } + + foreach ($weeksList as $week) { + $sumWeek = $historical[$week][$sid][$cat][$sub][$spec] ?? 0; + $percent = $sumWeek > 0 ? round($sumWeek / $sumMonth, 4) : null; + + $rows[] = [ + 'week' => $week, + 'store_id' => $sid, + 'category' => $cat, + 'subcategory' => $sub, + 'species' => $spec, + 'sumWeek' => $sumWeek, + 'percent' => $percent, + ]; + } + } + + $grouped = []; + foreach ($rows as $idx => $row) { + $key = "{$row['store_id']}|{$row['category']}|{$row['subcategory']}|{$row['species']}"; + $grouped[$key][] = $idx; + } + foreach ($grouped as $key => $indices) { + $sumPercent = 0.0; + foreach ($indices as $i) { + $sumPercent += $rows[$i]['percent']; + } + if ($sumPercent < 1.0) { + $diff = 1.0 - $sumPercent; + $count = count($indices); + $add = $diff / $count; + foreach ($indices as $i) { + $rows[$i]['percent'] = round($rows[$i]['percent'] + $add, 4); + } + } + } + + return ['weeksData' => $rows]; + } + + + /** + * Рассчитывает недельную цель для каждого вида (species) по данным недельных долей + * и целям месяца. + * @param array $weeksShareData + * @param array $monthSpeciesGoals + * @return array + * Плоский массив строк с полями: week, store_id, category, subcategory, + * species, percent, monthly_goal, weekly_goal + */ + public function calculateWeeklySpeciesGoals( + array $weeksShareData, + array $monthSpeciesGoals + ): array { + $monthSpeciesGoalsMap = []; + foreach ($monthSpeciesGoals as $monthSpeciesGoal) { + $monthSpeciesGoalsMap[$monthSpeciesGoal['store_id']] + [$monthSpeciesGoal['category']] + [$monthSpeciesGoal['subcategory']] + [$monthSpeciesGoal['species']] = $monthSpeciesGoal['goal'] ; + } + $result = []; + foreach ($weeksShareData as $row) { + $week = $row['week']; + $sid = $row['store_id']; + $cat = $row['category']; + $sub = $row['subcategory']; + $spec = $row['species']; + $percent = $row['percent']; + + $monthlyGoal = $monthSpeciesGoalsMap[$sid][$cat][$sub][$spec] ?? null; + + $weeklyGoal = 0; + if ($monthlyGoal !== null && $percent !== null) { + $weeklyGoal = round($percent * $monthlyGoal, 4); + } + + $result[] = [ + 'week' => $week, + 'store_id' => $sid, + 'category' => $cat, + 'subcategory' => $sub, + 'species' => $spec, + 'percent' => $percent, + 'monthly_goal' => $monthlyGoal, + 'weekly_goal' => $weeklyGoal, + ]; + } + return $result; + } + + /** + * Возвращает дату понедельника ISO-недели в формате YYYY-MM-DD + * + * @param int $year ISO-год (может отличаться от календарного в границах года) + * @param int $week номер ISO-недели (1–53) + * @return string дата понедельника, например '2025-03-10' + */ + public static function getIsoWeekStart(int $year, int $week): string + { + $iso = $year . 'W' . str_pad($week, 2, '0', STR_PAD_LEFT) . '1'; + return date('Y-m-d', strtotime($iso)); + } + + + public static function calculateWeekForecastSpeciesProducts($category, $subcategory, $species, $storeId, $goal) + { + $speciesProductForecast = []; + $products = Products1cNomenclature::find() + ->select(['id', 'name']) + ->where(['category' => $category]) + ->andWhere(['subcategory' => $subcategory]) + ->andWhere(['species' => $species]) + ->indexBy('id') + ->asArray() + ->all(); + + $productsIds = ArrayHelper::getColumn($products, 'id'); + if (CityStore::find()->where(['id' => $storeId])->one()->city_id == 1342) { + $region = 52; + } elseif (CityStore::find()->where(['id' => $storeId])->one()->city_id == 1) { + $region = 77; + } else { + $region = null; + } + $priceRecords = PricesDynamic::find() + ->select(['product_id', 'price']) + ->where(['product_id' => $productsIds]) + ->andWhere(['active' => 1]) + ->andWhere(['or', ['region_id' => $region], ['region_id' => null]]) + ->indexBy('product_id') + ->asArray() + ->all(); + + foreach ($priceRecords as $id => $record) { + if ($goal == 0 || (int)$record['price'] == 0) { + $forecast = 0; + } else { + $forecast = round(max($goal / (float)$record['price'], 1), 0); + } + $speciesProductForecast[] = [ + 'category' => $category, + 'subcategory' => $subcategory, + 'species' => $species, + 'product_id' => $record['product_id'], + 'name' => $products[$id]['name'], + 'price' => $record['price'] ?? $goal ?? 1, + 'goal' => $goal ?? 0, + 'forecast' => $forecast + + ]; + + } + + return $speciesProductForecast; + + } + /** * Общий расчёт плана для заданной категории товаров без истории. *