From: fomichev Date: Wed, 28 May 2025 12:53:07 +0000 (+0300) Subject: Merge branch 'refs/heads/develop' into feature_fomichev_erp-361_week_write_offs_share... X-Git-Url: https://gitweb.erp-flowers.ru/?a=commitdiff_plain;h=d03d1cb2777c8bb20c6cde4cc9d713fb7796eccd;p=erp24_rep%2Fyii-erp24%2F.git Merge branch 'refs/heads/develop' into feature_fomichev_erp-361_week_write_offs_share_calculation # Conflicts: # erp24/services/AutoPlannogrammaService.php --- d03d1cb2777c8bb20c6cde4cc9d713fb7796eccd diff --cc erp24/controllers/AutoPlannogrammaController.php index c623cd75,38f87910..5c01737a --- a/erp24/controllers/AutoPlannogrammaController.php +++ b/erp24/controllers/AutoPlannogrammaController.php @@@ -205,385 -202,338 +205,716 @@@ class AutoPlannogrammaController extend return $this->render('control', [ 'dataProvider' => $dataProvider, - 'filters' => $filters, + 'filters' => $filters, + ]); + } + + public function action1() + { + $request = Yii::$app->request; + + $filters = [ + 'category' => $request->get('category'), + 'store_id' => $request->get('store_id'), + 'year' => $request->get('year'), + 'month' => $request->get('month'), + 'type' => $request->get('type'), + ]; + + $dataProvider = new ArrayDataProvider([ + 'allModels' => [], + 'pagination' => ['pageSize' => 100], + ]); + + // Обработка даты на год и месяц + if (!empty($filters['year']) && !empty($filters['month'])) { + $filters['plan_date'] = $filters['year'] . '-' . str_pad($filters['month'], 2, '0', STR_PAD_LEFT) . '-01'; + + $service = new AutoPlannogrammaService(); + $data = $service->getMonthCategoryShareOrWriteOff($filters['plan_date'], $filters, $filters['type']); + + $flatData = []; + foreach ($data as $storeId => $categories) { + foreach ($categories as $row) { + $flatData[] = [ + 'store_id' => (string)$storeId, + 'category' => $row['category'] ?? null, + 'total_sum' => $row['total_sum'] ?? null, + 'percent' => $row['percent'] ?? null, + ]; + } + } + + $flatData = array_filter($flatData, function ($row) use ($filters) { + foreach ($filters as $key => $value) { + if (empty($value)) continue; + if (!isset($row[$key])) continue; + + if (stripos((string)$row[$key], (string)$value) === false) { + return false; + } + } + return true; + }); + + $dataProvider = new ArrayDataProvider([ + 'allModels' => array_values($flatData), + 'pagination' => ['pageSize' => 100], + ]); + } + + return $this->render('1', [ + 'dataProvider' => $dataProvider, + 'filters' => $filters, + ]); + } + + public function action2() + { + $request = Yii::$app->request; + + $filters = [ + 'category' => $request->get('category'), + 'store_id' => $request->get('store_id'), + 'year' => $request->get('year'), + 'month' => $request->get('month'), + 'type' => $request->get('type'), + ]; + + $dataProvider = new ArrayDataProvider([ + 'allModels' => [], + 'pagination' => ['pageSize' => 100], + ]); + + // Обработка даты на год и месяц + if (!empty($filters['year']) && !empty($filters['month'])) { + $filters['plan_date'] = $filters['year'] . '-' . str_pad($filters['month'], 2, '0', STR_PAD_LEFT) . '-01'; + + $service = new AutoPlannogrammaService(); + + $monthCategoryShare = $service->getMonthCategoryShareOrWriteOff($filters['plan_date'], $filters, $filters['type']); + $data = $service->getMonthCategoryGoal($monthCategoryShare, $filters['plan_date'], $filters['type']); + + $flatData = array_filter($data, function ($row) use ($filters) { + foreach ($filters as $key => $value) { + if (empty($value)) continue; + if (!isset($row[$key])) continue; + + if (stripos((string)$row[$key], (string)$value) === false) { + return false; + } + } + return true; + }); + + $dataProvider = new ArrayDataProvider([ + 'allModels' => $flatData, + 'pagination' => ['pageSize' => 100], + ]); + } + + return $this->render('2', [ + 'dataProvider' => $dataProvider, + 'filters' => $filters, + ]); + } + + public function action3() + { + $request = Yii::$app->request; + + $filters = [ + 'category' => $request->get('category'), + 'subcategory' => $request->get('subcategory'), + 'store_id' => $request->get('store_id'), + 'year' => $request->get('year'), + 'month' => $request->get('month'), + 'type' => $request->get('type'), + ]; + + $dataProvider = new ArrayDataProvider([ + 'allModels' => [], + 'pagination' => ['pageSize' => 100], + ]); + + + // Обработка даты на год и месяц + if (!empty($filters['year']) && !empty($filters['month'])) { + $filters['plan_date'] = $filters['year'] . '-' . str_pad($filters['month'], 2, '0', STR_PAD_LEFT) . '-01'; + $service = new AutoPlannogrammaService(); + + $data = $service->getMonthSubcategoryShareOrWriteOff($filters['plan_date'], $filters, $filters['type']); + + $flatData = array_filter($data, function ($row) use ($filters) { + foreach ($filters as $key => $value) { + if (empty($value)) continue; + if (!isset($row[$key])) continue; + + if (stripos((string)$row[$key], (string)$value) === false) { + return false; + } + } + return true; + }); + + $dataProvider = new ArrayDataProvider([ + 'allModels' => $flatData, + 'pagination' => ['pageSize' => 100], + ]); + } + return $this->render('3', [ + 'dataProvider' => $dataProvider, + 'filters' => $filters, + ]); + } + + public function action4() + { + $request = Yii::$app->request; + + $filters = [ + 'category' => $request->get('category'), + 'subcategory' => $request->get('subcategory'), + 'store_id' => $request->get('store_id'), + 'year' => $request->get('year'), + 'month' => $request->get('month'), + 'type' => $request->get('type'), + ]; + + $dataProvider = new ArrayDataProvider([ + 'allModels' => [], + 'pagination' => ['pageSize' => 100], + ]); + + if (!empty($filters['year']) && !empty($filters['month'])) { + $filters['plan_date'] = $filters['year'] . '-' . str_pad($filters['month'], 2, '0', STR_PAD_LEFT) . '-01'; + + $service = new AutoPlannogrammaService(); + + $monthCategoryShare = $service->getMonthCategoryShareOrWriteOff($filters['plan_date'], $filters); + $monthCategoryGoal = $service->getMonthCategoryGoal($monthCategoryShare, $filters['plan_date']); + $monthSubcategoryShare = $service->getMonthSubcategoryShareOrWriteOff($filters['plan_date'], $filters); + $data = $service->getMonthSubcategoryGoal($monthSubcategoryShare, $monthCategoryGoal); + + if ($filters['type'] == AutoPlannogrammaService::TYPE_WRITE_OFFS) { + $monthCategoryWriteOffsShare = $service->getMonthCategoryShareOrWriteOff($filters['plan_date'], $filters, $filters['type']); + $monthCategoryWriteOffsGoal = $service->getMonthCategoryGoal($monthCategoryWriteOffsShare, $filters['plan_date'], $filters['type']); + $monthSubcategoryWriteOffsShare = $service->getMonthSubcategoryShareOrWriteOff($filters['plan_date'], $filters, $filters['type']); + $data = $service->getMonthSubcategoryGoal($monthSubcategoryWriteOffsShare, $monthCategoryWriteOffsGoal, $filters['type'], $data); + } + + $flatData = array_filter($data, function ($row) use ($filters) { + foreach ($filters as $key => $value) { + if (empty($value)) continue; + if (!isset($row[$key])) continue; + + if (stripos((string)$row[$key], (string)$value) === false) { + return false; + } + } + return true; + }); + + $dataProvider = new ArrayDataProvider([ + 'allModels' => $flatData, + 'pagination' => ['pageSize' => 100], + ]); + } + return $this->render('4', [ + 'dataProvider' => $dataProvider, + 'filters' => $filters, + ]); + } + + public function action5() + { + $request = Yii::$app->request; + + $filters = [ + 'category' => $request->get('category'), + 'subcategory' => $request->get('subcategory'), + 'species' => $request->get('species'), + 'store_id' => $request->get('store_id'), + 'year' => $request->get('year'), + 'month' => $request->get('month'), + 'type' => $request->get('type'), + ]; + + $dataProvider = new ArrayDataProvider([ + 'allModels' => [], + 'pagination' => ['pageSize' => 100], + ]); + + if (!empty($filters['year']) && !empty($filters['month'])) { + $filters['plan_date'] = $filters['year'] . '-' . str_pad($filters['month'], 2, '0', STR_PAD_LEFT) . '-01'; + + $service = new AutoPlannogrammaService(); + + $data = $service->getMonthSpeciesShareOrWriteOff($filters['plan_date'], $filters, $filters['type']); + + $flatData = array_filter($data, function ($row) use ($filters) { + foreach ($filters as $key => $value) { + if (empty($value)) continue; + if (!isset($row[$key])) continue; + + if (stripos((string)$row[$key], (string)$value) === false) { + return false; + } + } + return true; + }); + + + $dataProvider = new ArrayDataProvider([ + 'allModels' => $flatData, + 'pagination' => ['pageSize' => 100], + ]); + } + return $this->render('5', [ + 'dataProvider' => $dataProvider, + 'filters' => $filters, + ]); + } + + public function action6() + { + $request = Yii::$app->request; + + $filters = [ + 'category' => $request->get('category'), + 'subcategory' => $request->get('subcategory'), + 'species' => $request->get('species'), + 'store_id' => $request->get('store_id'), + 'year' => $request->get('year'), + 'month' => $request->get('month'), + 'type' => $request->get('type'), + ]; + + $dataProvider = new ArrayDataProvider([ + 'allModels' => [], + 'pagination' => ['pageSize' => 100], + ]); + + + // Обработка даты на год и месяц + if (!empty($filters['year']) && !empty($filters['month'])) { + $filters['plan_date'] = $filters['year'] . '-' . str_pad($filters['month'], 2, '0', STR_PAD_LEFT) . '-01'; + $service = new AutoPlannogrammaService(); + + $monthCategoryShare = $service->getMonthCategoryShareOrWriteOff($filters['plan_date'], $filters); + $monthCategoryGoal = $service->getMonthCategoryGoal($monthCategoryShare, $filters['plan_date']); + $monthSubcategoryShare = $service->getMonthSubcategoryShareOrWriteOff($filters['plan_date'], $filters); + $monthSubcategoryGoal = $service->getMonthSubcategoryGoal($monthSubcategoryShare, $monthCategoryGoal); + $monthSpeciesShare = $service->getMonthSpeciesShareOrWriteOff($filters['plan_date'], $filters); + $data = $service->getMonthSpeciesGoalDirty($monthSpeciesShare, $monthSubcategoryGoal); + + if ($filters['type'] == AutoPlannogrammaService::TYPE_WRITE_OFFS) { + $monthCategoryWriteOffsShare = $service->getMonthCategoryShareOrWriteOff($filters['plan_date'], $filters, $filters['type']); + $monthCategoryWriteOffsGoal = $service->getMonthCategoryGoal($monthCategoryWriteOffsShare, $filters['plan_date'], $filters['type']); + $monthSubcategoryWriteOffsShare = $service->getMonthSubcategoryShareOrWriteOff($filters['plan_date'], $filters, $filters['type']); + $monthSubcategoryWriteOffsGoals = $service->getMonthSubcategoryGoal($monthSubcategoryWriteOffsShare, $monthCategoryWriteOffsGoal, $filters['type']); + $monthSpeciesWriteOffShare = $service->getMonthSpeciesShareOrWriteOff($filters['plan_date'], $filters, $filters['type']); + $data = $service->getMonthSpeciesGoalDirty($monthSpeciesWriteOffShare, $monthSubcategoryWriteOffsGoals, $filters['type'], $data); + } + + $flatData = array_filter($data, function ($row) use ($filters) { + foreach ($filters as $key => $value) { + if (empty($value)) continue; + if (!isset($row[$key])) continue; + + if (stripos((string)$row[$key], (string)$value) === false) { + return false; + } + } + return true; + }); + + $dataProvider = new ArrayDataProvider([ + 'allModels' => $flatData, + 'pagination' => ['pageSize' => 100], + ]); + } + return $this->render('6', [ + 'dataProvider' => $dataProvider, + 'filters' => $filters, ]); } + + + public function actionControlSpeciesOld() + { + $model = new DynamicModel([ + 'storeId', 'month', 'type', + + ]); + $model->addRule(['month', 'type'], 'required') + ->addRule('storeId', 'integer'); + + $storeList = CityStore::find() + ->select(['name','id']) + ->where(['visible' => CityStore::IS_VISIBLE]) + ->indexBy('id') + ->column(); + + $monthsList = []; + for ($i = 0; $i < 12; $i++) { + // получаем метку вида "03-2025" + $ts = strtotime("first day of -{$i} month"); + $key = date('m-Y', $ts); + $monthsList[$key] = $key; + } + + $monthResult = []; + $totals = []; + $weeksData = []; + $weeksShareResult = []; + $weeksGoalResult = []; + $monthCategoryShareResult = []; + $weeksProductForecast = []; + + if ($model->load(Yii::$app->request->post()) && $model->validate()) { + $filters = []; + + list($m, $y) = explode('-', $model->month); + $dateFrom = date("Y-m-d 00:00:00", strtotime(sprintf('%04d-%02d-01', $y, $m))); + $dateTo = date("Y-m-t 23:59:59", strtotime($dateFrom)); + + if ($model->storeId) { + $filters['store_id'] = $model->storeId; + $filters['type'] = $model->type; + $filters['plan_date'] = $dateFrom; + } + + $service = new AutoPlannogrammaService(); + + if ($model->storeId) { + $totals = $service->getStoreTotals( + [$model->storeId], + $dateFrom, + null, + $model->type, + $dateTo + ); + } + + + + $monthSpeciesGoals = $service->calculateFullGoalChainWeighted($filters); + $monthSpeciesGoalsMap = []; + foreach ($monthSpeciesGoals as $monthSpeciesGoal) { + $monthSpeciesGoalsMap[$monthSpeciesGoal['store_id']] + [$monthSpeciesGoal['category']] + [$monthSpeciesGoal['subcategory']] + [$monthSpeciesGoal['species']] = $monthSpeciesGoal['goal'] ; + } + + + + + $weeksShareResult = $service->getHistoricalWeeklySpeciesShare($model->month, $filters, null, 'writeOffs'); + $weeksData = $service->calculateWeeklySpeciesGoals($weeksShareResult['weeksData'], $monthSpeciesGoals) ; + + $datePlan = $filters['plan_date']; + $monthCategoryShare = $service->getMonthCategoryShareOrWriteOffWeighted($datePlan, $filters, null, $filters['type']); + $monthCategoryGoal = $service->getMonthCategoryGoal($monthCategoryShare, $datePlan, $filters); + foreach ($monthCategoryShare as $sid => $cats) { + foreach($cats as $cat) { + $monthCategoryShareResult[$sid][$cat['category']]['total_sum_cat'] = $cat['total_sum_cat']; + $monthCategoryShareResult[$sid][$cat['category']]['share_of_total'] = $cat['share_of_total']; + } + + } + foreach ($monthCategoryGoal as $cats) { + $monthCategoryShareResult[$cats['store_id']][$cats['category']]['goal'] = $cats['goal']; + + } + //var_dump($monthCategoryShareResult); die(); + $monthSubcategoryShare = $service->getMonthSubcategoryShareOrWriteOffWeighted($datePlan, $filters, null, $filters['type']); + $monthSubcategoryGoal = $service->getMonthSubcategoryGoal($monthSubcategoryShare, $monthCategoryGoal); + + if ($filters['type'] === 'writeOffs') { + $salesSubShare = $service->getMonthSubcategoryShareOrWriteOffWeighted($datePlan, $filters, null, 'sales'); + $salesSubGoal = $service->getMonthSubcategoryGoal($salesSubShare, $monthCategoryGoal); + + $catGoalMap = []; + foreach ($monthCategoryGoal as $row) { + $catGoalMap[$row['category']] = $row['goal']; + } + $salesSubGoalMap = []; + foreach ($salesSubGoal as $row) { + $salesSubGoalMap[$row['category']][$row['subcategory']] = $row['goal']; + } + + + foreach ($monthSubcategoryShare as &$row) { + $cat = $row['category']; + $sub = $row['subcategory']; + + $writeShare = $row['percent_of_month']; + + $writeGoal = ($catGoalMap[$cat] ?? 0) * $writeShare; + $saleGoal = $salesSubGoalMap[$cat][$sub] ?? 0; + + if ($saleGoal > 0 && $writeGoal > 0.1 * $saleGoal) { + $row['share'] = 0.1; + } + } + unset($row); + $monthSubcategoryGoal = $service->getMonthSubcategoryGoal($monthSubcategoryShare, $monthCategoryGoal); + } + // var_dump($monthSubcategoryShare); die(); + foreach ($monthSubcategoryShare as $subcat) { + $monthCategoryShareResult[$subcat['store_id']][$subcat['category']][$subcat['subcategory']]['total_sum'] = $subcat['total_sum']; + $monthCategoryShareResult[$subcat['store_id']][$subcat['category']][$subcat['subcategory']]['percent_of_month'] = $subcat['percent_of_month']; + } + foreach ($monthSubcategoryGoal as $cats) { + $monthCategoryShareResult[$cats['store_id']][$cats['category']][$cats['subcategory']]['goal'] = $cats['goal']; + + } + $monthSpeciesShare = $service->getMonthSpeciesShareOrWriteOffWeighted($datePlan, $datePlan, $filters, null, $filters['type']); + $monthSpeciesGoal = $service->getMonthSpeciesGoalDirty($monthSpeciesShare, $monthSubcategoryGoal); + if ($filters['type'] === 'writeOffs') { + $salesSpecShare = $service->getMonthSpeciesShareOrWriteOffWeighted($datePlan, $datePlan, $filters, null, 'sales'); + $salesSpecGoal = $service->getMonthSpeciesGoalDirty($salesSpecShare, $monthSubcategoryGoal); + + $subGoalMap = []; + foreach ($monthSubcategoryGoal as $row) { + $subGoalMap[$row['category']][$row['subcategory']] = $row['goal']; + } + $salesSpecGoalMap = []; + foreach ($salesSpecGoal as $row) { + $salesSpecGoalMap[$row['category']][$row['subcategory']][$row['species']] = $row['goal']; + } + + foreach ($monthSpeciesShare as &$row) { + $cat = $row['category']; + $sub = $row['subcategory']; + $spec = $row['species']; + + $writeShare = $row['percent_of_month']; + $writeGoal = ($subGoalMap[$cat][$sub] ?? 0) * $writeShare; + $saleGoal = $salesSpecGoalMap[$cat][$sub][$spec] ?? 0; + + if ($saleGoal > 0 && $writeGoal > 0.1 * $saleGoal) { + $row['share'] = 0.1; + } + } + unset($row); + + $monthSpeciesGoal = $service->getMonthSpeciesGoalDirty($monthSpeciesShare, $monthSubcategoryGoal); + } + foreach ($monthSpeciesShare as $species) { + $monthCategoryShareResult[$species['store_id']][$species['category']][$species['subcategory']][$species['species']]['total_sum'] = $species['total_sum']; + $monthCategoryShareResult[$species['store_id']][$species['category']][$species['subcategory']][$species['species']]['percent_of_month'] = $species['percent_of_month']; + + } + foreach ($weeksShareResult['weeksData'] as $row) { + $monthCategoryShareResult[$row['store_id']][$row['category']][$row['subcategory']][$row['species']][$row['week']]['sumWeek'] = $row['sumWeek']; + + } + + + foreach ($weeksData as $r) { + $forecasts = $service->calculateWeekForecastSpeciesProducts($r['category'], $r['subcategory'], $r['species'], $r['store_id'], $r['weekly_goal']); + foreach ($forecasts as $forecast) { + $weeksProductForecast[] = [ + 'category' => $forecast['category'] ?? '', + 'subcategory' => $forecast['subcategory'] ?? '', + 'species' => $forecast['species'] ?? '', + 'product_id' => $forecast['product_id'] ?? '', + 'name' => $forecast['name'] ?? '', + 'price' => $forecast['price'] ?? '', + 'goal' => $forecast['goal'] ?? 0, + 'forecast' => $forecast['forecast'] ?? 0, + 'week' => $r['week'], + ]; + } + } + + usort($weeksProductForecast, function($a, $b) { + foreach (['category','subcategory','species','name','week'] as $key) { + $va = $a[$key]; + $vb = $b[$key]; + if ($va < $vb) return -1; + if ($va > $vb) return 1; + } + return 0; + }); + + } +//var_dump($weeksProductForecast); die(); + return $this->render('control-species-old', [ + 'model' => $model, + 'result' => $monthResult, + 'weeksData' => $weeksData, + 'monthCategoryShare' => $monthCategoryShareResult, + 'weeksProductForecast' => $weeksProductForecast, + 'totals' => $totals, + 'storeList' => $storeList, + 'monthsList' => $monthsList, + + ]); + } + + + public function actionControlSpecies() + { + $model = new DynamicModel([ + 'storeId', 'month', 'type', + + ]); + $model->addRule(['month', 'type'], 'required') + ->addRule('storeId', 'integer'); + + $storeList = CityStore::find() + ->select(['name','id']) + ->where(['visible' => CityStore::IS_VISIBLE]) + ->indexBy('id') + ->column(); + + $monthsList = []; + for ($i = 0; $i < 12; $i++) { + // получаем метку вида "03-2025" + $ts = strtotime("first day of -{$i} month"); + $key = date('m-Y', $ts); + $monthsList[$key] = $key; + } + + $monthResult = []; + $totals = []; + $weeksData = []; + $weeksShareResult = []; + $weeksGoalResult = []; + $monthCategoryShareResult = []; + $weeksProductForecast = []; + + if ($model->load(Yii::$app->request->post()) && $model->validate()) { + $filters = []; + + list($m, $y) = explode('-', $model->month); + $dateFrom = date("Y-m-d 00:00:00", strtotime(sprintf('%04d-%02d-01', $y, $m))); + $dateTo = date("Y-m-t 23:59:59", strtotime($dateFrom)); + + if ($model->storeId) { + $filters['store_id'] = $model->storeId; + $filters['type'] = $model->type; + $filters['plan_date'] = $dateFrom; + } + + $service = new AutoPlannogrammaService(); + + $monthSpeciesGoals = $service->calculateFullGoalChain($filters); + $monthSpeciesGoalsMap = []; + foreach ($monthSpeciesGoals as $monthSpeciesGoal) { + $monthSpeciesGoalsMap[$monthSpeciesGoal['store_id']] + [$monthSpeciesGoal['category']] + [$monthSpeciesGoal['subcategory']] + [$monthSpeciesGoal['species']] = $monthSpeciesGoal['goal']; + } + + + $weeksShareResult = $service->getHistoricalWeeklySpeciesShare($model->month, $filters, null, 'writeOffs'); + $weeksData = $service->calculateWeeklySpeciesGoals($weeksShareResult['weeksData'], $monthSpeciesGoals) ; + + $datePlan = $filters['plan_date']; + $monthCategoryShare = $service->getMonthCategoryShareOrWriteOff($datePlan, $filters, $filters['type']); + $monthCategoryGoal = $service->getMonthCategoryGoal($monthCategoryShare, $datePlan, $filters['type']); + + $monthCategorySalesShare = $service->getMonthCategoryShareOrWriteOff($datePlan, $filters); + $monthCategorySalesGoal = $service->getMonthCategoryGoal($monthCategorySalesShare, $datePlan); + + foreach ($monthCategoryShare as $sid => $cats) { + foreach($cats as $cat) { + $monthCategoryShareResult[$sid][$cat['category']]['total_sum'] = $cat['total_sum']; + $monthCategoryShareResult[$sid][$cat['category']]['percent'] = $cat['percent']; + } + + } + foreach ($monthCategoryGoal as $cats) { + $monthCategoryShareResult[$cats['store_id']][$cats['category']]['goal'] = $cats['goal']; + + } + $monthSubcategorySalesShare = $service->getMonthSubcategoryShareOrWriteOff($datePlan, $filters); + $monthSubcategorySalesGoal = $service->getMonthSubcategoryGoal($monthSubcategorySalesShare, $monthCategorySalesGoal); + + $monthSubcategoryShare = $service->getMonthSubcategoryShareOrWriteOff($datePlan, $filters, $filters['type']); + $monthSubcategoryGoal = $service->getMonthSubcategoryGoal($monthSubcategoryShare, $monthCategoryGoal, $filters['type'], $monthSubcategorySalesGoal); + + + + // var_dump($monthSubcategoryShare); die(); + foreach ($monthSubcategoryShare as $subcat) { + $monthCategoryShareResult[$subcat['store_id']][$subcat['category']][$subcat['subcategory']]['total_sum'] = $subcat['total_sum']; + $monthCategoryShareResult[$subcat['store_id']][$subcat['category']][$subcat['subcategory']]['percent'] = $subcat['percent']; + } + foreach ($monthSubcategoryGoal as $cats) { + $monthCategoryShareResult[$cats['store_id']][$cats['category']][$cats['subcategory']]['goal'] = $cats['goal']; + + } + $monthSpeciesSalesShare = $service->getMonthSpeciesShareOrWriteOff($datePlan, $filters); + $monthSpeciesSalesGoal = $service->getMonthSpeciesGoalDirty($monthSpeciesSalesShare, $monthSubcategoryGoal); + + $monthSpeciesShare = $service->getMonthSpeciesShareOrWriteOff($datePlan, $filters, $filters['type']); + $monthSpeciesGoal = $service->getMonthSpeciesGoalDirty($monthSpeciesShare, $monthSubcategoryGoal, $filters['type'], $monthSpeciesSalesGoal); + + foreach ($monthSpeciesShare as $species) { + $monthCategoryShareResult[$species['store_id']][$species['category']][$species['subcategory']][$species['species']]['total_sum'] = $species['total_sum']; + $monthCategoryShareResult[$species['store_id']][$species['category']][$species['subcategory']][$species['species']]['percent'] = $species['percent']; + + } + foreach ($monthSpeciesGoal as $cats) { + $monthCategoryShareResult[$cats['store_id']][$cats['category']][$cats['subcategory']][$cats['species']]['goal'] = $cats['goal']; + + } + + foreach ($weeksShareResult['weeksData'] as $row) { + $monthCategoryShareResult[$row['store_id']][$row['category']][$row['subcategory']][$row['species']][$row['week']]['sumWeek'] = $row['sumWeek']; + + } + //var_dump($monthCategoryShareResult); die(); + + foreach ($weeksData as $r) { + $forecasts = $service->calculateWeekForecastSpeciesProducts($r['category'], $r['subcategory'], $r['species'], $r['store_id'], $r['weekly_goal']); + foreach ($forecasts as $forecast) { + $weeksProductForecast[] = [ + 'category' => $forecast['category'] ?? '', + 'subcategory' => $forecast['subcategory'] ?? '', + 'species' => $forecast['species'] ?? '', + 'product_id' => $forecast['product_id'] ?? '', + 'name' => $forecast['name'] ?? '', + 'price' => $forecast['price'] ?? '', + 'goal' => $forecast['goal'] ?? 0, + 'forecast' => $forecast['forecast'] ?? 0, + 'week' => $r['week'], + ]; + } + } + + usort($weeksProductForecast, function($a, $b) { + foreach (['category','subcategory','species','name','week'] as $key) { + $va = $a[$key]; + $vb = $b[$key]; + if ($va < $vb) return -1; + if ($va > $vb) return 1; + } + return 0; + }); + + } +//var_dump($weeksProductForecast); die(); + return $this->render('control-species', [ + 'model' => $model, + 'result' => $monthResult, + 'weeksData' => $weeksData, + 'monthCategoryShare' => $monthCategoryShareResult, + 'weeksProductForecast' => $weeksProductForecast, + 'totals' => $totals, + 'storeList' => $storeList, + 'monthsList' => $monthsList, + + ]); + } + + + } diff --cc erp24/services/AutoPlannogrammaService.php index d077f97e,3481cf59..41f60b4d --- a/erp24/services/AutoPlannogrammaService.php +++ b/erp24/services/AutoPlannogrammaService.php @@@ -97,9 -98,8 +100,8 @@@ class AutoPlannogrammaServic ':month2' => $month2, ':month3' => $month3, ]); - } - + // Основной запрос с CTE $query = (new Query()) ->select([ @@@ -542,800 -554,4 +556,801 @@@ return array_values($filtered); } + + + + - + /** + * Считает взвешенную долю категорий за целевой месяц: + * берёт три предыдущих к «месяцу-цели» месяца (пропуская сразу предыдущий), + * присваивает им веса 3, 2 и 1 (самый старый месяц — 3, самый свежий — 1), + * суммирует взвешенные итоги по каждой категории и выдаёт их доли + * от общего взвешенного итога. + * + * @param string $month Целевой месяц в формате 'YYYY-MM' + * @param array|null $filters ['store_id'=>…, …] + * @param array|null $productFilter Опционально: [product_id, …] + * @param string $type 'sales' или 'writeOffs' + * @return array [ + * => [ + * ['category'=>string, 'total_sum'=>float, 'share_of_total'=>float], + * … + * ], + * … + * ] + */ + public function getMonthCategoryShareOrWriteOffWeighted( + string $month, + ?array $filters = null, + ?array $productFilter = null, + string $type = 'sales' + ): array + { + $stores = $this->getVisibleStores(); + $storeIds = array_map(fn($s) => $s->id, $stores); + if (!empty($filters['store_id'])) { + $storeIds = array_intersect($storeIds, [(int)$filters['store_id']]); + } + if (empty($storeIds)) { + return []; + } + + $baseMonth = strtotime("{$month}-01"); + + $monthOffsets = [3, 4, 5]; + $monthWeights = [3, 2, 1]; + + + $weightedSums = []; + $monthStoreTotalsWeighted = []; + + foreach ($monthOffsets as $idx => $offsetMonths) { + $w = $monthWeights[$idx]; + $start = date('Y-m-01 00:00:00', strtotime("-{$offsetMonths} months", $baseMonth)); + $end = date('Y-m-t 23:59:59', strtotime($start)); + + + + $q = (new Query()) + ->select([ + 'store_id' => 'ex.entity_id', + 'category' => 'p1c.category', + 'month_sum' => new Expression('SUM(CAST(wop.summ AS NUMERIC))'), + ]) + ->from(['w' => 'write_offs']); + + if ($type === 'writeOffs') { + $q->leftJoin(['ex' => 'export_import_table'], 'ex.export_val = w.store_id') + ->leftJoin(['wop' => 'write_offs_products'], 'wop.write_offs_id = w.id') + ->leftJoin(['p1c' => 'products_1c_nomenclature'], 'p1c.id = wop.product_id') + ->andWhere(['>=', 'w.date', $start]) + ->andWhere(['<=', 'w.date', $end]); + + if ($productFilter !== null) { + $q->andWhere(['wop.product_id' => $productFilter]); + } + } else { + $q->leftJoin(['sp' => 'sales_products'], 'sp.check_id = s.id') + ->leftJoin(['ex' => 'export_import_table'], 'ex.export_val = s.store_id_1c') + ->leftJoin(['p1c' => 'products_1c_nomenclature'], 'p1c.id = sp.product_id') + ->andWhere(['>=', 's.date', $start]) + ->andWhere(['<=', 's.date', $end]); + + if ($productFilter !== null) { + $q->andWhere(['sp.product_id' => $productFilter]); + } + } + + $q->andWhere(['ex.entity_id' => $storeIds]) + // ->andWhere(['<>', 'p1c.category', '']) + ->groupBy(['ex.entity_id', 'p1c.category']); + + $rows = $q->all(); + + foreach ($rows as $r) { + $sid = $r['store_id']; + $cat = $r['category']; + $sum = (float)$r['month_sum'] * $w; + $weightedSums[$sid][$cat] = ($weightedSums[$sid][$cat] ?? 0) + $sum; + } + + } + + $result = []; + foreach ($weightedSums as $storeId => $cats) { + $grand = array_sum($cats) ?: 1; + + $unlabeledSum = $cats[''] ?? 0; + $labeledCount = count($cats) - (isset($cats['']) ? 1 : 0); + + $distribution = $labeledCount > 0 ? $unlabeledSum / $labeledCount : 0; + + foreach ($cats as $category => $weightedSum) { + if ($category === '') { + continue; + } + + $adjustedSum = $weightedSum + $distribution; + + $result[$storeId][] = [ + 'category' => $category, + 'total_sum_cat' => $weightedSum, + 'total_sum_store' => $grand, + 'share_of_total' => round($adjustedSum / $grand, 4), + ]; + } + } + return $result; + } + + public function getMonthSubcategoryShareOrWriteOffWeighted(string $dateFrom, ?array $filters = null, ?array $productFilter = null, string $type = 'sales'): array + { + try { + $dt = new \DateTime($dateFrom); + } catch (\Exception $e) { + // Неверный формат даты + return []; + } + $month = (int)$dt->format('m'); + $year = (int)$dt->format('Y'); + + $stores = $this->getVisibleStores(); + $storeIds = array_map(fn($s) => $s->id, $stores); + if (!empty($filters['store_id'])) { + $storeIds = array_intersect($storeIds, [(int)$filters['store_id']]); + } + if (empty($storeIds)) { + return []; + } + + $years = [$year - 2, $year - 1]; + + $query = (new Query()) + ->select([ + 'store_id' => 'ex.entity_id', + 'subcategory' => 'p1c.subcategory', + 'category' => 'p1c.category', + 'total_sum' => new Expression( + $type === 'writeOffs' + ? 'SUM(CAST(wop.summ AS NUMERIC))' + : 'SUM(sp.summ)' + ), + ]); + + if ($type === 'writeOffs') { + $query->from(['w' => 'write_offs']) + ->leftJoin(['ex' => 'export_import_table'], 'ex.export_val = w.store_id') + ->leftJoin(['wop'=> 'write_offs_products'], 'wop.write_offs_id = w.id') + ->leftJoin(['p1c'=> 'products_1c_nomenclature'], 'p1c.id = wop.product_id') + + ->andWhere(['in', new Expression('EXTRACT(YEAR FROM w.date)'), $years]) + ->andWhere(['=', new Expression('EXTRACT(MONTH FROM w.date)'), $month]); + if ($productFilter !== null) { + $query->andWhere(['wop.product_id' => $productFilter]); + } + } else { + $query->from(['s' => 'sales']) + ->leftJoin(['sp' => 'sales_products'], 'sp.check_id = s.id') + ->leftJoin(['ex' => 'export_import_table'], 'ex.export_val = s.store_id_1c') + ->leftJoin(['p1c' => 'products_1c_nomenclature'],'p1c.id = sp.product_id') + ->andWhere(['in', new Expression('EXTRACT(YEAR FROM s.date)'), $years]) + ->andWhere(['=', new Expression('EXTRACT(MONTH FROM s.date)'), $month]); + if ($productFilter !== null) { + $query->andWhere(['sp.product_id' => $productFilter]); + } + } + + $query->andWhere(['ex.entity_id' => $storeIds]) + ->andWhere(['<>', 'p1c.subcategory', '']) + ->groupBy(['ex.entity_id', 'p1c.subcategory', 'p1c.category']); + + $rows = $query->all(); + if (empty($rows)) { + return []; + } + + $sumByStoreCategory = []; + foreach ($rows as $r) { + $sid = $r['store_id']; + $cat = $r['category']; + $sumByStoreCategory[$sid][$cat] = ($sumByStoreCategory[$sid][$cat] ?? 0) + $r['total_sum']; + } + + + $result = []; + foreach ($rows as $r) { + $sid = $r['store_id']; + $cat = $r['category']; + $total = $sumByStoreCategory[$sid][$cat] ?: 1; + $result[] = [ + 'store_id' => $sid, + 'category' => $cat, + 'subcategory' => $r['subcategory'], + 'total_sum' => $r['total_sum'], + 'percent_of_month' => round($r['total_sum'] / $total, 4), + ]; + } + + return $result; + } + + public function getMonthSpeciesShareOrWriteOffWeighted( + string $dateFrom, + string $dateTo, + ?array $filters = null, + ?array $productFilter = null, + string $type = 'sales' + ): array + { + try { + $dt = new \DateTime($dateFrom); + } catch (\Exception $e) { + return []; + } + $month = (int)$dt->format('m'); + $year = (int)$dt->format('Y'); + + $stores = $this->getVisibleStores(); + $storeIds = array_map(fn($s) => $s->id, $stores); + if (!empty($filters['store_id'])) { + $storeIds = array_intersect($storeIds, [(int)$filters['store_id']]); + } + if (empty($storeIds)) { + return []; + } + + $years = [$year - 2, $year - 1]; + + $query = (new Query())->select([ + 'store_id' => 'ex.entity_id', + 'category' => 'p1c.category', + 'subcategory'=> 'p1c.subcategory', + 'species' => 'p1c.species', + 'total_sum' => new Expression( + $type === 'writeOffs' + ? 'SUM(CAST(wop.summ AS NUMERIC))' + : 'SUM(sp.summ)' + ), + ]); + + if ($type === 'writeOffs') { + $query->from(['w' => 'write_offs']) + ->leftJoin(['ex' => 'export_import_table'], 'ex.export_val = w.store_id') + ->leftJoin(['wop' => 'write_offs_products'], 'wop.write_offs_id = w.id') + ->leftJoin(['p1c' => 'products_1c_nomenclature'], 'p1c.id = wop.product_id') + ->andWhere(['IN', new Expression('EXTRACT(YEAR FROM w.date)'), $years]) + ->andWhere(['=', new Expression('EXTRACT(MONTH FROM w.date)'), $month]); + if ($productFilter !== null) { + $query->andWhere(['wop.product_id' => $productFilter]); + } + } else { + $query->from(['s' => 'sales']) + ->leftJoin(['sp' => 'sales_products'], 'sp.check_id = s.id') + ->leftJoin(['ex' => 'export_import_table'], 'ex.export_val = s.store_id_1c') + ->leftJoin(['p1c' => 'products_1c_nomenclature'], 'p1c.id = sp.product_id') + ->andWhere(['IN', new Expression('EXTRACT(YEAR FROM s.date)'), $years]) + ->andWhere(['=', new Expression('EXTRACT(MONTH FROM s.date)'), $month]); + if ($productFilter !== null) { + $query->andWhere(['sp.product_id' => $productFilter]); + } + } + + $query->andWhere(['ex.entity_id' => $storeIds]) + ->andWhere(['<>', 'p1c.species', '']) + ->groupBy([ + 'ex.entity_id', + 'p1c.category', + 'p1c.subcategory', + 'p1c.species', + ]); + + $rows = $query->all(); + if (empty($rows)) { + return []; + } + + $sumByStoreSubcategory = []; + foreach ($rows as $r) { + $sid = $r['store_id']; + $sub = $r['subcategory']; + $sumByStoreSubcategory[$sid][$sub] = + ($sumByStoreSubcategory[$sid][$sub] ?? 0) + $r['total_sum']; + } + + $result = []; + foreach ($rows as $r) { + $sid = $r['store_id']; + $sub = $r['subcategory']; + $total = $sumByStoreSubcategory[$sid][$sub] ?: 1; + $result[] = [ + 'store_id' => $sid, + 'category' => $r['category'], + 'subcategory' => $sub, + 'species' => $r['species'], + 'total_sum' => (float)$r['total_sum'], + 'percent_of_month' => round($r['total_sum'] / $total, 4), + ]; + } + + return $result; + } + + // Недельные расчеты + + + /** + * Получает суммы продаж или списаний по видам (species) для каждой недели указанного месяца. + * + * @param string $monthYear месяц-год в формате MM-YYYY, например '03-2025' + * @param array|null $filters опциональные фильтры ['store_id'=>...] + * @param array|null $productFilter опциональный фильтр по product_id + * @param string $type 'sales' или 'writeOffs' + * @return array массив строк: [ + * ['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; + + } + + // альтернативные методы расчета списаний + + public function calculateFullGoalChainWeighted(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('Y-m-d'); + + $monthCategoryShare = $this->getMonthCategoryShareOrWriteOffWeighted($datePlan, $filters, null, $filters['type']); + $monthCategoryGoal = $this->getMonthCategoryGoal($monthCategoryShare, $datePlan, $filters); ++ ++ $monthSubcategoryShare = $this->getMonthSubcategoryShareOrWriteOff($datePlan, $filters); + $monthSubcategoryShare = $this->getMonthSubcategoryShareOrWriteOffWeighted($datePlan, $filters, null, $filters['type']); + $monthSubcategoryGoal = $this->getMonthSubcategoryGoal($monthSubcategoryShare, $monthCategoryGoal); + ++ $monthSpeciesShare = $this->getMonthSpeciesShareOrWriteOff($datePlan, $filters); + if ($filters['type'] === 'writeOffs') { + $salesSubShare = $this->getMonthSubcategoryShareOrWriteOffWeighted($datePlan, $filters, null, 'sales'); + $salesSubGoal = $this->getMonthSubcategoryGoal($salesSubShare, $monthCategoryGoal); + + $catGoalMap = []; + foreach ($monthCategoryGoal as $row) { + $catGoalMap[$row['category']] = $row['goal']; + } + $salesSubGoalMap = []; + foreach ($salesSubGoal as $row) { + $salesSubGoalMap[$row['category']][$row['subcategory']] = $row['goal']; + } + + + foreach ($monthSubcategoryShare as &$row) { + $cat = $row['category']; + $sub = $row['subcategory']; + + $writeShare = $row['percent_of_month']; + + $writeGoal = ($catGoalMap[$cat] ?? 0) * $writeShare; + $saleGoal = $salesSubGoalMap[$cat][$sub] ?? 0; + + if ($saleGoal > 0 && $writeGoal > 0.1 * $saleGoal) { + $row['share'] = 0.1; + } + } + unset($row); + $monthSubcategoryGoal = $this->getMonthSubcategoryGoal($monthSubcategoryShare, $monthCategoryGoal); + } + + $monthSpeciesShare = $this->getMonthSpeciesShareOrWriteOffWeighted($datePlan, $datePlan, $filters, null, $filters['type']); + $monthSpeciesGoal = $this->getMonthSpeciesGoalDirty($monthSpeciesShare, $monthSubcategoryGoal); + if ($filters['type'] === 'writeOffs') { + $salesSpecShare = $this->getMonthSpeciesShareOrWriteOffWeighted($datePlan, $datePlan, $filters, null, 'sales'); + $salesSpecGoal = $this->getMonthSpeciesGoalDirty($salesSpecShare, $monthSubcategoryGoal); + + $subGoalMap = []; + foreach ($monthSubcategoryGoal as $row) { + $subGoalMap[$row['category']][$row['subcategory']] = $row['goal']; + } + $salesSpecGoalMap = []; + foreach ($salesSpecGoal as $row) { + $salesSpecGoalMap[$row['category']][$row['subcategory']][$row['species']] = $row['goal']; + } + + foreach ($monthSpeciesShare as &$row) { + $cat = $row['category']; + $sub = $row['subcategory']; + $spec = $row['species']; + + $writeShare = $row['percent_of_month']; + $writeGoal = ($subGoalMap[$cat][$sub] ?? 0) * $writeShare; + $saleGoal = $salesSpecGoalMap[$cat][$sub][$spec] ?? 0; + + if ($saleGoal > 0 && $writeGoal > 0.1 * $saleGoal) { + $row['share'] = 0.1; + } + } + unset($row); + + $monthSpeciesGoal = $this->getMonthSpeciesGoalDirty($monthSpeciesShare, $monthSubcategoryGoal); + } + + $filtered = array_filter($monthSpeciesGoal, function ($row) use ($filters) { + foreach ($filters as $key => $value) { + if ($value === null || $value === '') { + continue; + } + + if (!array_key_exists($key, $row)) { + continue; + } + + if (is_numeric($row[$key]) && is_numeric($value)) { + if ((float)$row[$key] !== (float)$value) { + return false; + } + } else { + if (stripos((string)$row[$key], (string)$value) === false) { + return false; + } + } + } + return true; + }); + + return array_values($filtered); + } + - } + }