]> gitweb.erp-flowers.ru Git - erp24_rep/yii-erp24/.git/commitdiff
Merge branch 'refs/heads/develop' into feature_fomichev_erp-361_week_write_offs_share... feature_fomichev_erp-361_week_write_offs_share_calculation origin/feature_fomichev_erp-361_week_write_offs_share_calculation
authorfomichev <vladimir.fomichev@erp-flowers.ru>
Mon, 9 Jun 2025 15:13:13 +0000 (18:13 +0300)
committerfomichev <vladimir.fomichev@erp-flowers.ru>
Mon, 9 Jun 2025 15:13:13 +0000 (18:13 +0300)
# Conflicts:
# erp24/controllers/AutoPlannogrammaController.php
# erp24/services/AutoPlannogrammaService.php

1  2 
erp24/controllers/AutoPlannogrammaController.php
erp24/services/AutoPlannogrammaService.php

index 544657880d93543f00e43354d059b5a8c2e05f35,5e44bd9bb5579f5082a64992c4a573806f518b09..5f672918eef92dad0c0ae89d4a54cefebccec0de
@@@ -1267,9 -1286,11 +1272,11 @@@ class AutoPlannogrammaController extend
          // Обработка даты на год и месяц
          if (!empty($filters['year']) && !empty($filters['month'])) {
              $filters['plan_date'] = $filters['year'] . '-' . str_pad($filters['month'], 2, '0', STR_PAD_LEFT) . '-01';
 -            // var_dump( $filters['plan_date']); die();
 +
              $service = new AutoPlannogrammaService();
+             //$goals = $service->calculateFullGoalChain($filters);
+             //$forecast = $service->calculateFullForecastForWeek($filters);
+             //var_dump( $forecast); die();
              $monthCategoryShare = $service->getMonthCategoryShareOrWriteOff($filters['plan_date'], $filters);
              $monthCategoryGoal = $service->getMonthCategoryGoal($monthCategoryShare, $filters['plan_date']);
              $monthSubcategoryShare = $service->getMonthSubcategoryShareOrWriteOff($filters['plan_date'], $filters);
index f868f682d15e3fbc6aeb24e578055b6e1c97d71e,7ab2e47306f5cd0654bf04709eb963dc7226d89d..0d7be3552187f1d3c84b59c28e3cff78345903c5
@@@ -10,11 -10,12 +10,12 @@@ use yii\db\Query
  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;
@@@ -2304,433 -2385,47 +2384,476 @@@ class AutoPlannogrammaServic
      }
  
  
+     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);
 +    }
 +
  }