]> gitweb.erp-flowers.ru Git - erp24_rep/yii-erp24/.git/commitdiff
Рефакторинг
authorfomichev <vladimir.fomichev@erp-flowers.ru>
Fri, 23 May 2025 15:44:20 +0000 (18:44 +0300)
committerfomichev <vladimir.fomichev@erp-flowers.ru>
Fri, 23 May 2025 15:44:20 +0000 (18:44 +0300)
erp24/controllers/AutoPlannogrammaController.php
erp24/controllers/CategoryPlanController.php
erp24/services/AutoPlannogrammaService.php
erp24/services/StorePlanService.php
erp24/views/auto-plannogramma/81.php [new file with mode: 0644]

index bee0c8519d4ce5ae29b4a98fe13d4942216acb3f..0baeb488f7a8b27eaea8c04fd5dd0328b8f6671c 100644 (file)
@@ -696,7 +696,12 @@ class AutoPlannogrammaController extends BaseController
                 $filters['species']
             );
             // var_dump($result); die();
-
+            $withoutHistoryResults = StorePlanService::calculateMedianSalesForProductsWithoutHistoryExtended(
+                $filters['store_id'],
+                $filters['month'],
+                $filters['year'],
+                $result['without_history']
+            );
             $productSalesShare = StorePlanService::calculateProductSalesShare(
                 $filters['store_id'],
                 $filters['month'],
@@ -704,7 +709,7 @@ class AutoPlannogrammaController extends BaseController
             );
 
 
-            var_dump( $productSalesShare); die();
+            var_dump( $withoutHistoryResults); die();
             $flatData = array_filter($data, function ($row) use ($filters) {
                 foreach ($filters as $key => $value) {
                     if (empty($value)) continue;
@@ -722,7 +727,7 @@ class AutoPlannogrammaController extends BaseController
                 'pagination' => ['pageSize' => 100],
             ]);
         }
-        return $this->render('8', [
+        return $this->render('81', [
             'dataProvider' => $dataProvider,
             'filters' => $filters,
         ]);
index 9a368a5f9bc456cd3eded97e22ab02f456f034f6..ee292ebb0e1d955c8d9d16362a0f21612d471307 100644 (file)
@@ -348,7 +348,7 @@ class CategoryPlanController extends Controller {
                         $species
                     );
                    // var_dump($result); die();
-                    $weightedResults = StorePlanService::calculateWeightedSalesForProductsWithoutHistory(
+                    $weightedResults = StorePlanService::calculateMedianSalesForProductsWithoutHistoryExtended(
                         $storeId,
                         $selectedMonth,
                         $selectedYear,
index 4cdaf1a0b02447770d688327a2c65e2be6e78e58..d39bbf85fc21a6605b0543675ab2738fdc6f3223 100644 (file)
@@ -7,6 +7,9 @@ use yii\db\Expression;
 use yii\db\Query;
 use yii\helpers\ArrayHelper;
 use yii_app\records\CityStore;
+use yii_app\records\PricesDynamic;
+use yii_app\records\Products1cNomenclature;
+use yii_app\records\SalesProducts;
 use yii_app\records\SalesWriteOffsPlan;
 
 class AutoPlannogrammaService
@@ -604,20 +607,20 @@ class AutoPlannogrammaService
 
             // ——————— WEIGHTED SALES ————————
             $t2 = hrtime(true);
-            $weightedResults = StorePlanService::calculateMedianSalesForProductsWithoutHistory(
+            $medianResults = StorePlanService::calculateMedianSalesForProductsWithoutHistoryExtended(
                 $storeId, $month, $year, $productsWithoutHistory
             );
             $dur = (hrtime(true) - $t2) / 1e6;
             Yii::warning("calculateMedianSalesForProductsWithoutHistory for store {$storeId}: {$dur} ms\n");
 
-            if (empty($weightedResults)) {
+            if (empty($medianResults)) {
                 continue;
             }
 
             // ——————— COST CALCULATION ————————
             $t3 = hrtime(true);
             $costs = StorePlanService::calculateCostForProductsWithoutHistory(
-                $storeId, $month, $year, $weightedResults
+                $storeId, $month, $year, $medianResults
             );
             $dur = (hrtime(true) - $t3) / 1e6;
             Yii::warning( "calculateCostForProductsWithoutHistory for store {$storeId}: {$dur} ms\n");
@@ -674,6 +677,123 @@ class AutoPlannogrammaService
         return $result;
     }
 
+    /**
+     * @param array|int $storeIds
+     * @param string    $dateFrom   формат 'Y-m-d H:i:s'
+     * @param string    $dateTo     формат 'Y-m-d H:i:s'
+     * @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(array|int $storeIds, string $dateFrom, string $dateTo, int $regionId = 52, $typeFilter = null): array
+    {
+        $salesProducts = SalesProducts::find()
+            ->alias('sp')
+            ->innerJoinWith([
+                'sale s',
+                'product_1c p1c',
+                'product.nomenclature nom',
+            ])
+            ->andWhere(['s.store_id'   => $storeIds])
+            ->andWhere(['between', 's.date', $dateFrom, $dateTo])
+            ->andWhere(['nom.category' => null]);
+
+        if ($typeFilter) {
+            $salesProducts->andWhere(['p1c.type'     => $typeFilter]);
+            }
+        $salesProducts->all();
+
 
+        $components = [];
+        $rows       = [];
+        foreach ($salesProducts as $sp) {
+            /** @var SalesProducts $sp */
+            $sale    = $sp->check_id;
+            $product = $sp->product_id;
+            $js      = trim($product->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'     => $sale->id,
+                    'sale_date'   => $sale->date,
+                    'product_id'  => $product->id,
+                    'product_name'=> $product->name,
+                    'component_guid' => $guid,
+                    'quantity'    => $qty,
+                ];
+            }
+        }
+        if (empty($rows)) {
+            return [];
+        }
+        $guids = array_keys($components);
+
+        $nomenclatures = Products1cNomenclature::find()
+            ->andWhere(['guid' => $guids])
+            ->indexBy('guid')
+            ->all();
+
+        $priceDynamics = PricesDynamic::find()
+            ->andWhere(['region_id' => $regionId])
+            ->andWhere(['product_id' => array_values( ArrayHelper::getColumn($nomenclatures, 'id') )])
+            ->andWhere(['<=', 'date_from', $dateTo])
+            ->andWhere(['>=', 'date_to', $dateFrom])
+            ->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[] = [
+                '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,
+            ];
+        }
+
+        return $result;
+    }
 
 }
\ No newline at end of file
index d660a6d31d4ecccc5a807b44655ce310aa76a831..dd26b2cc910917738bfad67e786832522dec81d7 100755 (executable)
@@ -455,7 +455,7 @@ class StorePlanService
 
 
     /**
-     * Ð\9cеÑ\82од Ð²Ñ\8bÑ\87иÑ\81лÑ\8fеÑ\82 Ð²Ð·Ð²ÐµÑ\88енное Ð·Ð½Ð°Ñ\87ение Ð¿Ñ\80одаж Ð´Ð»Ñ\8f Ñ\82оваÑ\80ов Ð±ÐµÐ· Ð¸Ñ\81Ñ\82оÑ\80ии.
+     * Метод вычисляет значение продаж для товаров без истории.
      *
      * @param int    $storeId                Идентификатор магазина.
      * @param string $selectedMonth          Выбранный месяц в формате "mm" (целевой месяц).
@@ -468,13 +468,13 @@ class StorePlanService
      *
      * @return array Возвращает массив, где ключ – GUID товара, а значение – рассчитанное взвешенное значение продаж.
      */
-    public static function calculateWeightedSalesForProductsWithoutHistory($storeId, $selectedMonth, $selectedYear, $productsWithoutHistory)
+    public static function calculateMedianSalesForProductsWithoutHistoryExtended($storeId, $selectedMonth, $selectedYear, $productsWithoutHistory)
     {
         $t0 = hrtime(true);
         $targetDate = strtotime("{$selectedYear}-{$selectedMonth}-01");
         $periods = self::getPeriods($targetDate, 3);
 
-        $weightedResults = [];
+        $medianSalesResults = [];
         $initTime = (hrtime(true) - $t0) / 1e6; // миллисекунды
         Yii::warning( "Init (periods): {$initTime} ms\n");
         foreach ($productsWithoutHistory as $product) {
@@ -482,7 +482,7 @@ class StorePlanService
             $t1 = hrtime(true);
             $similarProductIds = self::getSimilarProductIDs($guid);
             if (empty($similarProductIds)) {
-                $weightedResults[$guid] = 0;
+                $medianSalesResults[$guid] = 0;
                 continue;
             }
             $dur = (hrtime(true) - $t1) / 1e6;
@@ -501,7 +501,7 @@ class StorePlanService
             $t3 = hrtime(true);
             $median3 = self::calculateMedianSalesOverPeriods($storeId, $similarProductIds, $periods);
 
-            $weightedResults[$guid] = [
+            $medianSalesResults[$guid] = [
                 'weightedValue' => $median3,
                 'medianSales'   => $medianSales,
                 'salesValues'   => $salesValuesForEachMonth,
@@ -513,7 +513,7 @@ class StorePlanService
         $totalTime = (hrtime(true) - $t0) / 1e6;
         Yii::warning( "Total  calculateWeightedSalesForProductsWithoutHistory: {$totalTime} ms\n");
 
-        return $weightedResults;
+        return $medianSalesResults;
     }
 
     /**
@@ -657,7 +657,7 @@ class StorePlanService
      * @param int   $storeId
      * @param int   $selectedMonth
      * @param int   $selectedYear
-     * @param array $productsWithoutHistory  массив ['guid' => …]
+     * @param array $medianProductsWithoutHistory  массив ['guid' => …]
      * @return array  плоский массив строк:
      *   [
      *     [
@@ -676,13 +676,13 @@ class StorePlanService
         int   $storeId,
         int   $selectedMonth,
         int   $selectedYear,
-        array $productsWithoutHistory
+        array $medianProductsWithoutHistory
     ): array
     {
 
         $accumulator = [];
-
-        foreach ($productsWithoutHistory as $guid => $info) {
+       // var_dump($productsWithoutHistory); die();
+        foreach ($medianProductsWithoutHistory as $guid => $info) {
             $quantity = (float)$info;
             if ($quantity <= 0) {
                 continue;
diff --git a/erp24/views/auto-plannogramma/81.php b/erp24/views/auto-plannogramma/81.php
new file mode 100644 (file)
index 0000000..885a80e
--- /dev/null
@@ -0,0 +1,114 @@
+<?php
+?>
+    <div class="filter-form" style="margin-bottom: 20px;">
+        <?php use kartik\date\DatePicker;
+        use kartik\grid\GridView;
+        use kartik\select2\Select2;
+        use yii\helpers\ArrayHelper;
+        use yii\helpers\Html;
+        use yii\widgets\ActiveForm;
+        use yii_app\records\CityStore;
+        use yii_app\records\Products1cNomenclature;
+        ?>
+        <h1 class="ms-3 mb-4"><?= Html::encode("Расчет доли товара без истории (month_nohistory_sale_frcst)") ?></h1>
+        <?php
+        $form = ActiveForm::begin(['method' => 'get']); ?>
+        <div class="row p-3">
+            <div class="col-md">
+                <?= $form->field(new \yii\base\DynamicModel(['category' => $filters['category'] ?? '']), 'category')->widget(Select2::class, [
+                    'data' => ArrayHelper::map(
+                        Products1cNomenclature::find()->select('category')->distinct()->asArray()->all(),
+                        'category',
+                        'category'
+                    ),
+                    'options' => ['placeholder' => 'Категория', 'name' => 'category'],
+                    'pluginOptions' => ['allowClear' => true],
+                ])->label('Категория') ?>
+            </div>
+            <div class="col-md">
+                <?= $form->field(new \yii\base\DynamicModel(['subcategory' => $filters['subcategory'] ?? '']), 'subcategory')->widget(Select2::class, [
+                    'data' => ArrayHelper::map(
+                        Products1cNomenclature::find()->select('subcategory')->distinct()->asArray()->all(),
+                        'subcategory',
+                        'subcategory'
+                    ),
+                    'options' => ['placeholder' => 'Подкатегория', 'name' => 'subcategory'],
+                    'pluginOptions' => ['allowClear' => true],
+                ])->label('Подкатегория') ?>
+            </div>
+            <div class="col-md">
+                <?= $form->field(new \yii\base\DynamicModel(['species' => $filters['species'] ?? '']), 'species')->widget(Select2::class, [
+                    'data' => ArrayHelper::map(
+                        Products1cNomenclature::find()->select('species')->distinct()->asArray()->all(),
+                        'species',
+                        'species'
+                    ),
+                    'options' => ['placeholder' => 'Тип товара', 'name' => 'species'],
+                    'pluginOptions' => ['allowClear' => true],
+                ])->label('Товар') ?>
+            </div>
+            <div class="col-md">
+                <?= $form->field(new \yii\base\DynamicModel(['store_id' => $filters['store_id'] ?? '']), 'store_id')->widget(Select2::class, [
+                    'data' => ArrayHelper::map(
+                        CityStore::findAll(['visible' => CityStore::IS_VISIBLE]),
+                        'id',
+                        'name'
+                    ),
+                    'options' => ['placeholder' => 'Магазин', 'name' => 'store_id'],
+                    'pluginOptions' => ['allowClear' => true],
+                ])->label('Магазин') ?>
+            </div>
+            <div class="col-md">
+                <?= $form->field(new \yii\base\DynamicModel(['month' => $filters['month'] ?? '']), 'month')->dropDownList(\yii_app\helpers\DateHelper::MONTH_NUMBER_NAMES, [
+                    'prompt' => 'Месяц',
+                    'name' => 'month',
+                ])->label('Плановый месяц') ?>
+            </div>
+
+            <div class="col-md">
+                <?= $form->field(new \yii\base\DynamicModel(['year' => $filters['year'] ?? '']), 'year')->dropDownList(['2025' => 2025, '2026' => 2026], [
+                    'prompt' => 'Год',
+                    'name' => 'year',
+                ])->label('Плановый год') ?>
+            </div>
+            <div class="col-md">
+                <?= $form->field(new \yii\base\DynamicModel(['type' => $filters['type'] ?? '']), 'type')->widget(Select2::class, [
+                    'data' => [
+                        'writeOffs' => 'Списания',
+                        'sales' => 'Продажи'
+                    ],
+                    'options' => ['placeholder' => 'Тип', 'name' => 'type'],
+                    'pluginOptions' => ['allowClear' => true],
+                ])->label('По дефолту продажи!') ?>
+            </div>
+            <div class="col-md">
+                <?= Html::submitButton('Фильтровать', ['class' => 'btn btn-primary']) ?>
+            </div>
+            <div class="col-md">
+                <?= Html::a('Сбросить', ['auto-plannogramma/8'], ['class' => 'btn btn-default']) ?>
+            </div>
+        </div>
+
+        <?php ActiveForm::end(); ?>
+    </div>
+
+
+
+<?php
+$columns = [
+    ['attribute' => 'store_id', 'label' => 'Магазин', 'value' => function ($data) {
+        return CityStore::findOne($data['store_id'])->name ?? null;
+    }],
+    ['attribute' => 'category', 'label' => 'Категория'],
+    ['attribute' => 'subcategory', 'label' => 'Подкатегория'],
+    ['attribute' => 'species', 'label' => 'Тип'],
+    ['attribute' => 'goal', 'label' => 'Сумма', 'format' => ['decimal', 2]],
+];
+
+
+?>
+<?= GridView::widget([
+    'dataProvider' => $dataProvider,
+    'columns' => $columns,
+]); ?>
+<?php