]> gitweb.erp-flowers.ru Git - erp24_rep/yii-erp24/.git/commitdiff
Merge branch 'refs/heads/feature_fomichev_erp-419_month_clear_goal' into feature_fomi...
authorfomichev <vladimir.fomichev@erp-flowers.ru>
Mon, 26 May 2025 13:34:19 +0000 (16:34 +0300)
committerfomichev <vladimir.fomichev@erp-flowers.ru>
Mon, 26 May 2025 13:34:19 +0000 (16:34 +0300)
# Conflicts:
# erp24/services/AutoPlannogrammaService.php

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

index 4cdaf1a0b02447770d688327a2c65e2be6e78e58,0fe26cbef477b540fd580b759d66cb8b8d3b5303..92eaf45783582e4cb36fe27550f36c2c4bb19acc
@@@ -542,138 -593,238 +594,373 @@@ class AutoPlannogrammaServic
          return array_values($filtered);
      }
  
 +    /**
 +     * Общий расчёт плана для заданной категории товаров без истории.
 +     *
 +     * @param int    $storeId
 +     * @param string $yearMonth   строка "YYYY-MM", например "2025-03"
 +     * @param string $category
 +     * @param string|null $subcategory
 +     * @param string|null $species
 +     * @return array  [
 +     *     [
 +     *         'store_id'    => …,
 +     *         'category'    => …,
 +     *         'subcategory' => …,
 +     *         'species'     => …,
 +     *         'month'       => …,
 +     *         'year'        => …,
 +     *         'sum'         => …,
 +     *     ],
 +     *     …
 +     * ]
 +     *
 +     */
 +    public function calculateSpeciesForecastForProductsWithoutHistory($dateFrom, $filters): array
 +    {
 +        $t0 = hrtime(true);
 +        // Получение ID видимых магазинов
 +        $storeIds = array_map(fn($store) => $store->id, $this->getVisibleStores());
 +
 +        $subcategory = !empty($filters['subcategory']) ? $filters['subcategory'] : null;
 +        $species = !empty($filters['species']) ? $filters['species'] : null;
 +
 +        // Применение фильтра по магазину, если указан
 +        if (!empty($filters['store_id'])) {
 +            $storeIds = array_intersect($storeIds, [(int)$filters['store_id']]);
 +        }
 +        $date = new \DateTime($dateFrom);
 +        $month = $date->format('m');
 +        $year = $date->format('Y');
 +
 +        $result = [];
 +        $initTime = (hrtime(true) - $t0) / 1e6; // миллисекунды
 +        Yii::warning( "Init (getVisibleStores + filters): {$initTime} ms\n");
 +        foreach ($storeIds as $storeId) {
 +            $t1 = hrtime(true);
 +            $histResult = StorePlanService::calculateHistoricalShare(
 +                $storeId,
 +                $month,
 +                $year,
 +                $filters['category'],
 +                $subcategory,
 +                $species
 +            );
 +            $dur = (hrtime(true) - $t1) / 1e6;
 +            Yii::warning( "calculateHistoricalShare for store {$storeId}: {$dur} ms\n");
 +
 +            $productsWithoutHistory = $histResult['without_history'] ?? [];
 +            if (empty($productsWithoutHistory)) {
 +                continue;
 +            }
 +
 +            // ——————— WEIGHTED SALES ————————
 +            $t2 = hrtime(true);
 +            $weightedResults = StorePlanService::calculateMedianSalesForProductsWithoutHistory(
 +                $storeId, $month, $year, $productsWithoutHistory
 +            );
 +            $dur = (hrtime(true) - $t2) / 1e6;
 +            Yii::warning("calculateMedianSalesForProductsWithoutHistory for store {$storeId}: {$dur} ms\n");
 +
 +            if (empty($weightedResults)) {
 +                continue;
 +            }
 +
 +            // ——————— COST CALCULATION ————————
 +            $t3 = hrtime(true);
 +            $costs = StorePlanService::calculateCostForProductsWithoutHistory(
 +                $storeId, $month, $year, $weightedResults
 +            );
 +            $dur = (hrtime(true) - $t3) / 1e6;
 +            Yii::warning( "calculateCostForProductsWithoutHistory for store {$storeId}: {$dur} ms\n");
 +
 +            if (!empty($costs)) {
 +                $result = array_merge($result, $costs);
 +            }
 +        }
 +
 +        $totalTime = (hrtime(true) - $t0) / 1e6;
 +        Yii::warning( "Total calculateSpeciesForecastForProductsWithoutHistory: {$totalTime} ms\n");
 +
 +        return $result;
 +    }
 +
 +    public function mapGoalsBySpecies(array $rows): array {
 +        $mapped = [];
 +        foreach ($rows as $row) {
 +            $mapped[$row['store_id']][$row['category']][$row['subcategory']][$row['species']] = $row['goal'];
 +        }
 +        return $mapped;
 +    }
 +
 +    public function subtractSpeciesGoals(array $data, array $bouquetForecast, array $noHistory): array {
 +        $bouquetForecastMap = $this->mapGoalsBySpecies($bouquetForecast);
 +        $noHistoryMap = $this->mapGoalsBySpecies($noHistory);
 +        $result = [];
 +
 +        foreach ($data as $row) {
 +            $storeId = $row['store_id'];
 +            $category = $row['category'];
 +            $subcategory = $row['subcategory'];
 +            $species = $row['species'];
 +            $originalGoal = $row['goal'];
 +
 +            $bouquetForecastGoal = $bouquetForecastMap[$storeId][$category][$subcategory][$species] ?? 0;
 +            $noHistoryGoal = $noHistoryMap[$storeId][$category][$subcategory][$species] ?? 0;
 +
 +            $cleanGoal = $originalGoal - ($bouquetForecastGoal + $noHistoryGoal);
 +
 +
 +            $result[] = [
 +                'store_id' => $storeId,
 +                'category' => $category,
 +                'subcategory' => $subcategory,
 +                'species' => $species,
 +                'dirtyGoal' => $originalGoal,
 +                'bouquetGoal' => $bouquetForecastGoal,
 +                'noHistoryGoal' => $noHistoryGoal,
 +                'goal' => $cleanGoal
 +            ];
 +        }
 +
 +        return $result;
 +    }
 +
 +
 +
++
+     /**
+      * @param array|int $storeIds
+      * @param int $month
+      * @param int $year
+      * @param int       $regionId
+      * @return array   список строк с полями:
+      *                 sale_id, sale_date, product_id, product_name,
+      *                 component_guid, component_name, component_category,
+      *                 quantity, price, cost
+      */
+     public function getUnmarkedProductsComponents(int $storeId, string $month, string $year, $type = 'sales'): array
+     {
+         $region = CityStoreParams::find()
+             ->where(['store_id' => $storeId])
+             ->one()->address_region;
+         if (!$region) {
+             // определяем регион по городу
+             $cityId = CityStore::find()->select('city_id')->where(['id' => $storeId])->scalar();
+             if ($cityId == 1) {
+                 $region = BouquetComposition::REGION_MSK;
+             } else {
+                 $region = BouquetComposition::REGION_NN;
+             }
+         }
+         $monthStart = sprintf('%04d-%02d-01 00:00:00', $year, $month);
+         $daysInMonth = cal_days_in_month(CAL_GREGORIAN, $month, $year);
+         $monthEnd   = sprintf('%04d-%02d-%02d 23:59:59', $year, $month, $daysInMonth);
+         $unmarkedProducts = [];
+         if ($type == self::TYPE_SALES) {
+             $salesProducts = Sales::find()
+                 ->alias('s')
+                 ->select(['s.id', 's.date', 's.operation', 'sp.product_id', 'p1c.type', 'p1c.components' , 'p1c.name'])
+                 ->innerJoin(
+                     ['sp' => SalesProducts::tableName()],
+                     's.id = sp.check_id'
+                 )
+                 ->innerJoin(
+                     ['p1c' => Products1c::tableName()],
+                     'p1c.id = sp.product_id'
+                 )
+                 ->leftJoin(
+                     ['nom' => Products1cNomenclature::tableName()],
+                     'nom.id = sp.product_id'
+                 )
+                 ->andWhere(['s.store_id'   => $storeId])
+                 ->andWhere(['not', ['s.operation'   => ['Удален', 'Удаление']]])
+                 ->andWhere(['between', 's.date', $monthStart, $monthEnd])
+                 ->andWhere(['not', ['p1c.components' => '']])
+                 ->andWhere(['nom.category' => null])
+                 ->asArray()
+                 ->all();
+         $unmarkedProducts = $salesProducts;
+         } else {
+             $storeJoinCondition = $type === self::TYPE_WRITE_OFFS ? 'ex.export_val = w.store_id' : 'ex.export_val = s.store_id_1c';
+             $writeOffsProducts = WriteOffs::find()
+                 ->alias('w')
+                 ->select([
+                     'write_off_id'    => 'w.id',
+                     'write_off_date'  => 'w.date',
+                     'product_id'      => 'wp.product_id',
+                     'type'            => 'p1c.type',
+                     'components'      => 'p1c.components',
+                     'product_name'    => 'p1c.name',
+                     'store_id'        => 'ex.entity_id',
+                 ])
+                 ->innerJoin(
+                     ['wp' => WriteOffsProducts::tableName()],
+                     'wp.write_offs_id = w.id'
+                 )
+                 ->innerJoin(
+                     ['p1c' => Products1c::tableName()],
+                     'p1c.id = wp.product_id'
+                 )
+                 ->leftJoin(
+                     ['nom' => Products1cNomenclature::tableName()],
+                     'nom.id = wp.product_id'
+                 )
+                 ->leftJoin(
+                     ['ex' => ExportImportTable::tableName()],
+                     'ex.export_val = w.store_id'
+                 )
+                 ->andWhere(['between', 'w.date', $monthStart, $monthEnd])
+                 ->andWhere(['ex.entity_id' => $storeId])
+                 ->andWhere(['nom.category' => null])
+                 ->andWhere(['<>', 'p1c.components', ''])
+                 ->asArray()
+                 ->all();
+             //var_dump($writeOffsProducts); die();
+             $unmarkedProducts = $writeOffsProducts;
+         }
+          //var_dump( $salesProducts); die();
+         $components = [];
+         $rows       = [];
+         foreach ($unmarkedProducts as $up) {
+             $js      = trim($up['components']);
+             if ($js === '' || $js[0] !== '{') {
+                 continue;
+             }
+             $data = @json_decode($js, true);
+             if (!is_array($data)) {
+                 continue;
+             }
+             foreach ($data as $guid => $qty) {
+                 $qty = (int)$qty;
+                 if ($qty <= 0) {
+                     continue;
+                 }
+                 $components[$guid] = true;
+                 $rows[] = [
+                     'sale_id'     => $up['id'],
+                     'sale_date'   => $up['date'],
+                     'product_id'  => $up['product_id'],
+                     'product_name'=> $up['name'],
+                     'component_guid' => $guid,
+                     'quantity'    => $qty,
+                     'type' => $type,
+                     'operation' => $up['operation'] ?? '',
+                 ];
+             }
+         }
+         if (empty($rows)) {
+             return [];
+         }
+         $guids = array_keys($components);
+         $nomenclatures = Products1cNomenclature::find()
+             ->andWhere(['id' => $guids])
+             ->indexBy('id')
+             ->all();
+         $priceDynamics = PricesDynamic::find()
+             ->andWhere(['region_id' => $region])
+             ->andWhere(['product_id' => array_values( ArrayHelper::getColumn($nomenclatures, 'id') )])
+             ->orderBy(['date_from' => SORT_DESC])
+             ->all();
+         $pricesByProduct = [];
+         foreach ($priceDynamics as $pd) {
+             /** @var PricesDynamic $pd */
+             $pid = $pd->product_id;
+             $pricesByProduct[$pid][] = $pd;
+         }
+         $result = [];
+         foreach ($rows as $r) {
+             $guid = $r['component_guid'];
+             $n   = $nomenclatures[$guid] ?? null;
+             $pid = $n?->id;
+             $price = 0;
+             if ($pid && isset($pricesByProduct[$pid])) {
+                 foreach ($pricesByProduct[$pid] as $pd) {
+                     if ($pd->date_from <= $r['sale_date'] && $pd->date_to >= $r['sale_date']) {
+                         $price = $pd->price;
+                         break;
+                     }
+                 }
+             }
+             $cost = $r['quantity'] * $price;
+             $result[] = [
+                 'store_id'   => $storeId,
+                 'sale_id'            => $r['sale_id'],
+                 'sale_date'          => $r['sale_date'],
+                 'product_id'         => $r['product_id'],
+                 'product_name'       => $r['product_name'],
+                 'component_guid'     => $guid,
+                 'component_name'     => $n?->name,
+                 'component_category' => $n?->category,
+                 'quantity'           => $r['quantity'],
+                 'price'              => $price,
+                 'cost'               => $cost,
+                 'month' => $month,
+                 'year' => $year,
+                 'type' => $r['type'],
+                 'operation' => $r['operation'] ?? '',
+             ];
+         }
+         return $result;
+     }
+     public function sumUnmarkedProductsComponentsByCategory(array $items, string $type): array
+     {
+         $aggregated = [];
+         foreach ($items as $row) {
+             $storeId  = $row['store_id'];
+             $category = $row['component_category'];
+             $month    = $row['month'];
+             $year     = $row['year'];
+             $operation = $row['operation'] ?? null;
+             $cost = (float)$row['cost'];
+             if (
+                 $type === AutoPlannogrammaService::TYPE_SALES
+                 || ($row['type'] ?? null) === AutoPlannogrammaService::TYPE_SALES
+             ) {
+                 if ($operation === 'Возврат') {
+                     $cost = -$cost;
+                 }
+             }
+             $key = implode('|', [$storeId, $category, $year, $month]);
+             if (!isset($aggregated[$key])) {
+                 $aggregated[$key] = [
+                     'store_id' => $storeId,
+                     'category' => $category,
+                     'sum'      => 0.0,
+                     'month'    => $month,
+                     'year'     => $year,
+                     'type'     => $type,
+                 ];
+             }
+             $aggregated[$key]['sum'] += $cost;
+         }
+         return array_values($aggregated);
+     }
  }