]> gitweb.erp-flowers.ru Git - erp24_rep/yii-erp24/.git/commitdiff
Добавление недельных методов
authorfomichev <vladimir.fomichev@erp-flowers.ru>
Wed, 28 May 2025 13:06:28 +0000 (16:06 +0300)
committerfomichev <vladimir.fomichev@erp-flowers.ru>
Wed, 28 May 2025 13:06:28 +0000 (16:06 +0300)
erp24/services/AutoPlannogrammaService.php

index fd5308b046bee927c453309c01b10f9f1259bd50..3c3c0d98577d62ade6b3b027c15938969e296d99 100644 (file)
@@ -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<int, 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<int, 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;
+
+    }
+
     /**
      * Общий расчёт плана для заданной категории товаров без истории.
      *