use yii\helpers\ArrayHelper;
use yii_app\records\BouquetComposition;
use yii_app\records\CityStore;
+use yii_app\records\PricesDynamic;
+use yii_app\records\Products1cNomenclature;
use yii_app\records\CityStoreParams;
use yii_app\records\ExportImportTable;
-use yii_app\records\PricesDynamic;
+ use yii_app\records\MatrixBouquetForecast;
use yii_app\records\Products1c;
-use yii_app\records\Products1cNomenclature;
use yii_app\records\Sales;
use yii_app\records\SalesProducts;
use yii_app\records\SalesWriteOffsPlan;
}
+ public function calculateFullForecastForWeek(array $filters): array
+ {
+ $bouquetSpeciesForecast = [];
+ $goals = $this->calculateFullGoalChain($filters);
+ $noHistoryProductData = $this->calculateSpeciesForecastForProductsWithoutHistory($filters['plan_date'], $filters);
+ $historyProductData = $this->calculateSpeciesForecastForProductsWithHistory($filters['plan_date'], $filters, $goals);
+ $matrixForecast = MatrixBouquetForecast::find()
+ ->where(['year' => $filters['year'], 'month' => $filters['month']])
+ ->asArray()
+ ->all();
+ $matrixGroups = array_unique(ArrayHelper::getColumn($matrixForecast, 'group'));
+ $bouquetForecast = StorePlanService::getBouquetSpiecesMonthGoalFromForecast($filters['month'], $filters['year'], $filters['store_id'], $matrixGroups);
+ $speciesData = $bouquetForecast['final'];
+ foreach ($speciesData as $store_id => $categoryData) {
+ foreach ($categoryData as $category => $subcategoryData) {
+ foreach ($subcategoryData as $subcategory => $species) {
+ foreach ($species as $speciesInd => $row) {
+ $bouquetSpeciesForecast[] = [
+ 'category' => $category,
+ 'subcategory' => $subcategory,
+ 'store_id' => $store_id,
+ 'species' => $speciesInd,
+ 'goal' => $row
+ ];
+ }
+ }
+ }
+
+ }
+ $cleanedSpeciesGoals = $this->subtractSpeciesGoals($goals, $bouquetSpeciesForecast, $noHistoryProductData);
+
+ $salesProductForecastShare = $this->calculateProductForecastShare($noHistoryProductData, $historyProductData);
+
+ $productForecastSpecies = $this->calculateProductSalesBySpecies($salesProductForecastShare, $cleanedSpeciesGoals);
+
+ $weeklySales = $this->getHistoricalSpeciesShareByWeek($filters['plan_date'], $filters);
+
+ $weeklySalesForecast = $this->calculateWeeklyProductForecastPieces($productForecastSpecies, $weeklySales);
+
+ return $weeklySalesForecast;
+ }
+
+
+
+
+
+
+ /**
+ * Считает взвешенную долю категорий за целевой месяц:
+ * берёт три предыдущих к «месяцу-цели» месяца (пропуская сразу предыдущий),
+ * присваивает им веса 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 [
+ * <store_id> => [
+ * ['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;
+ }
+
+ // Недельные расчеты
+
+
+
+
+ // альтернативные методы расчета списаний
+
+ public function calculateFullGoalChainWeighted(array $filters): array
+ {
+ $datePlan = $filters['plan_date'];
+ $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);
+ }
+
}