]> gitweb.erp-flowers.ru Git - erp24_rep/yii-erp24/.git/commitdiff
Правки по ревью origin/feature_fomichev_erp_2025_sales_api_endpoint_api2
authorfomichev <vladimir.fomichev@erp-flowers.ru>
Wed, 6 May 2026 12:33:18 +0000 (15:33 +0300)
committerfomichev <vladimir.fomichev@erp-flowers.ru>
Wed, 6 May 2026 12:33:18 +0000 (15:33 +0300)
erp24/api2/controllers/DataTestController.php

index 9d6f8267e89bf0e48619f740f0a3eea79224b450..0b272dc430691ceff7d17b44521e239207245648 100644 (file)
@@ -1,27 +1,38 @@
 <?php
+declare(strict_types=1);
 
 namespace app\controllers;
 
-use app\controllers\BaseController;
 use Exception;
 use Yii;
 use yii\helpers\Json;
-use yii_app\records\ApiCronTest;
+use yii_app\records\Admin;
 use yii_app\records\AdminGradeHistory;
+use yii_app\records\ApiCronTest;
+use yii_app\records\CityStore;
+use yii_app\records\CityStoreParams;
+use yii_app\records\EmployeePosition;
 use yii_app\records\ExportImportTable;
+use yii_app\records\PricesDynamic;
+use yii_app\records\Products1c;
+use yii_app\records\Sales;
+use yii_app\records\SalesProducts;
+use yii_app\records\Timetable;
 use yii_app\records\TimetableFact;
 
 
-class DataTestController extends BaseController {
+class DataTestController extends BaseController
+{
 
-    public function actionRequest() {
+    public function actionRequest()
+    {
         Yii::$app->response->format = \yii\web\Response::FORMAT_JSON;
 
         $request = Yii::$app->request->getRawBody();
 
         try {
             $result = Json::decode($request);
-        } catch (Exception $ex) {
+        } catch (Exception) {
             return $this->asJson(['error' => ['code' => 400, 'message' => 'Json body invalid']]);
         }
 
@@ -45,39 +56,34 @@ class DataTestController extends BaseController {
                 $apiCronTest->status = 1;
                 $apiCronTest->date_up = date("Y-m-d H:i:s");
 
-                $validate = $apiCronTest->validate();
-                if ($validate) {
+                if ($apiCronTest->validate()) {
                     $apiCronTest->save();
                 }
             }
         } catch (Exception $e) {
-
+            Yii::error($e->getMessage(), 'api2.data-test');
+            return $this->asJson(["error_id" => 2, "error" => "Internal server error"]);
         }
 
         return $this->asJson(json_decode($json, true));
     }
 
 
-    public function actionUpload() {
-        set_time_limit(600);
+    public function actionUpload()
+    {
         Yii::$app->response->format = \yii\web\Response::FORMAT_JSON;
 
         $request = Yii::$app->request->getRawBody();
 
         try {
             $result = Json::decode($request);
-        } catch (Exception $ex) {
+        } catch (Exception) {
             return $this->asJson(['error' => ['code' => 400, 'message' => 'Json body invalid']]);
         }
 
-        $requestId = '';
-        if (!empty($result["request_id"])) {
-            $requestId = $result["request_id"];
-        }
+        $requestId = !empty($result["request_id"]) ? $result["request_id"] : '';
 
         try {
-
-
             $apiCronTest = new ApiCronTest();
             $apiCronTest->date = date("Y-m-d H:i:s");
             $apiCronTest->date_up = date("Y-m-d H:i:s");
@@ -85,13 +91,16 @@ class DataTestController extends BaseController {
             $apiCronTest->json_post = $request;
             $apiCronTest->request_id = $requestId;
             $apiCronTest->direct_id = 2;
-            $validate = $apiCronTest->validate();
-            if ($validate) {
-                $apiCronTest->save();
-            }
 
+            if ($apiCronTest->validate()) {
+                if (!$apiCronTest->save()) {
+                    Yii::error('ApiCronTest save failed: ' . Json::encode($apiCronTest->errors), 'api2.data-test');
+                    return $this->asJson(['success' => false, 'error' => 'Internal server error']);
+                }
+            }
         } catch (Exception $e) {
-
+            Yii::error($e->getMessage(), 'api2.data-test');
+            return $this->asJson(['success' => false, 'error' => 'Internal server error']);
         }
 
         return $this->asJson(['response' => true]);
@@ -101,11 +110,12 @@ class DataTestController extends BaseController {
      * Возвращает JSON со списком магазинов (id, name)
      * GET /api2/data-test/get-stores
      */
-    public function actionGetStores() {
+    public function actionGetStores()
+    {
         Yii::$app->response->format = \yii\web\Response::FORMAT_JSON;
 
         try {
-            $stores = \yii_app\records\CityStore::find()
+            $stores = CityStore::find()
                 ->select(['id', 'name'])
                 ->orderBy(['name' => SORT_ASC])
                 ->asArray()
@@ -116,9 +126,10 @@ class DataTestController extends BaseController {
                 'data' => $stores
             ]);
         } catch (Exception $e) {
+            Yii::error($e->getMessage(), 'api2.data-test');
             return $this->asJson([
                 'success' => false,
-                'error' => $e->getMessage()
+                'error' => 'Internal server error'
             ]);
         }
     }
@@ -139,22 +150,21 @@ class DataTestController extends BaseController {
      * 1 = с 8:00 до 20:00 (день)
      * 2 = с 20:00 до 8:00 (ночь)
      */
-    public function actionGetSales() {
-        set_time_limit(300);
+    public function actionGetSales()
+    {
         Yii::$app->response->format = \yii\web\Response::FORMAT_JSON;
 
         $request = Yii::$app->request->getRawBody();
 
         try {
             $input = Json::decode($request);
-        } catch (Exception $ex) {
+        } catch (Exception) {
             return $this->asJson([
                 'success' => false,
                 'error' => 'Invalid JSON body'
             ]);
         }
 
-        // Валидация обязательных параметров
         if (empty($input['date'])) {
             return $this->asJson([
                 'success' => false,
@@ -162,132 +172,139 @@ class DataTestController extends BaseController {
             ]);
         }
 
-        $date = $input['date'];
-        $shiftType = $input['shift_type'] ?? 0;
+        $date          = $input['date'];
+        $shiftType     = $input['shift_type'] ?? 0;
         $storeIdFilter = $input['store_id'] ?? null;
 
         try {
-            // Определяем диапазон времени для смены
-            $dateObj = new \DateTime($date);
+            $dateObj        = new \DateTime($date);
             $currentDateStr = $dateObj->format('Y-m-d');
+            $nextDateStr    = (clone $dateObj)->modify('+1 day')->format('Y-m-d');
 
-            // Получаем дату следующего дня
-            $nextDateObj = clone $dateObj;
-            $nextDateStr = $nextDateObj->modify('+1 day')->format('Y-m-d');
-
-            $dayStart = $currentDateStr . ' 08:00:00';
-            $dayEnd = $currentDateStr . ' 20:00:00';
-            $nightStart = $currentDateStr . ' 20:00:00';
-            $nightEnd = $currentDateStr . ' 23:59:59';
-            $nextDayStart = $nextDateStr . ' 00:00:00';
-            $nextDayEnd = $nextDateStr . ' 08:00:00';
-
-            // Построение базового запроса продаж
-            $query = \yii_app\records\Sales::find()
-                ->where(['or', ['order_id' => ''], ['order_id' => 0]]) // Только офлайн продажи
+            $query = Sales::find()
+                ->where(['or', ['order_id' => ''], ['order_id' => 0]])
                 ->orderBy(['store_id' => SORT_ASC, 'date' => SORT_ASC]);
 
-            // Фильтр по смене
             if ($shiftType === 1) {
-                // День: 8:00 - 20:00
-                $query->andWhere(['between', 'date', $dayStart, $dayEnd]);
+                $query->andWhere(['between', 'date', $currentDateStr . ' 08:00:00', $currentDateStr . ' 20:00:00']);
             } elseif ($shiftType === 2) {
-                // Ночь: 20:00 - 8:00 следующего дня
                 $query->andWhere(['OR',
-                    ['between', 'date', $nightStart, $nightEnd],
-                    ['between', 'date', $nextDayStart, $nextDayEnd]
+                    ['between', 'date', $currentDateStr . ' 20:00:00', $currentDateStr . ' 23:59:59'],
+                    ['between', 'date', $nextDateStr . ' 00:00:00', $nextDateStr . ' 08:00:00'],
                 ]);
             } else {
-                // Все смены (вся дата)
                 $query->andWhere(['between', 'date', $currentDateStr . ' 00:00:00', $currentDateStr . ' 23:59:59']);
             }
 
-            // Фильтр по магазину если задан
             if (!empty($storeIdFilter)) {
                 $query->andWhere(['store_id' => $storeIdFilter]);
             }
 
-            $sales = $query->with('products')->asArray()->all();
+            $sales = $query->asArray()->all();
 
             if (empty($sales)) {
-                return $this->asJson([
-                    'success' => true,
-                    'data' => []
-                ]);
+                return $this->asJson(['success' => true, 'data' => []]);
             }
 
-            // Агрегируем данные и присоединяем информацию о товарах
-            $aggregated = [];
-            $storeRegions = [];
+            // Bulk-load store regions (avoid per-sale query)
+            $storeIds       = array_unique(array_column($sales, 'store_id'));
+            $storeParamRows = CityStoreParams::find()
+                ->select(['store_id', 'address_region'])
+                ->where(['store_id' => $storeIds])
+                ->asArray()
+                ->all();
+            $storeRegions = array_column($storeParamRows, 'address_region', 'store_id');
 
-            foreach ($sales as $sale) {
-                $saleStoreId = $sale['store_id'];
+            // Bulk-load all products for all sales at once
+            $saleIds         = array_column($sales, 'id');
+            $allSaleProducts = SalesProducts::find()
+                ->where(['check_id' => $saleIds])
+                ->asArray()
+                ->all();
+
+            $productsBySale = [];
+            foreach ($allSaleProducts as $sp) {
+                $productsBySale[$sp['check_id']][] = $sp;
+            }
+
+            // Bulk-load product names
+            $allProductIds  = array_unique(array_column($allSaleProducts, 'product_id'));
+            $productInfoMap = [];
+            if (!empty($allProductIds)) {
+                $productInfoMap = Products1c::find()
+                    ->select(['id', 'name'])
+                    ->where(['id' => $allProductIds])
+                    ->indexBy('id')
+                    ->asArray()
+                    ->all();
+            }
 
-                // Кэшируем регион магазина
-                if (!isset($storeRegions[$saleStoreId])) {
-                    $storeParams = \yii_app\records\CityStoreParams::findOne(['store_id' => $saleStoreId]);
-                    $storeRegions[$saleStoreId] = $storeParams ? $storeParams->address_region : null;
+            // Bulk-load active prices; keep latest per (product_id, region_id)
+            $pricesByProduct = [];
+            if (!empty($allProductIds)) {
+                $priceRows = PricesDynamic::find()
+                    ->select(['product_id', 'region_id', 'price'])
+                    ->where(['product_id' => $allProductIds, 'active' => PricesDynamic::ACTIVE])
+                    ->orderBy(['date_from' => SORT_DESC])
+                    ->asArray()
+                    ->all();
+
+                foreach ($priceRows as $pr) {
+                    $pid = $pr['product_id'];
+                    $rid = $pr['region_id']; // null → '' as PHP array key
+                    if (!isset($pricesByProduct[$pid][$rid])) {
+                        $pricesByProduct[$pid][$rid] = (float)$pr['price'];
+                    }
+                    // Any-region fallback: first = latest
+                    if (!isset($pricesByProduct[$pid]['_latest'])) {
+                        $pricesByProduct[$pid]['_latest'] = (float)$pr['price'];
+                    }
                 }
+            }
 
-                $saleKey = $saleStoreId . '_' . $sale['date']; // Ключ для агрегации
+            $aggregated = [];
+
+            foreach ($sales as $sale) {
+                $saleStoreId = $sale['store_id'];
+                $regionId    = $storeRegions[$saleStoreId] ?? null;
+                $saleKey     = $saleStoreId . '_' . $sale['date'];
 
                 if (!isset($aggregated[$saleKey])) {
                     $aggregated[$saleKey] = [
-                        'id' => $sale['id'],
-                        'date' => $sale['date'],
-                        'store_id' => (int)$saleStoreId,
-                        'summ' => (float)$sale['summ'],
+                        'id'        => $sale['id'],
+                        'date'      => $sale['date'],
+                        'store_id'  => (int)$saleStoreId,
+                        'summ'      => (float)$sale['summ'],
                         'operation' => $sale['operation'],
-                        'status' => $sale['status'],
-                        'skidka' => (float)$sale['skidka'],
-                        'products' => []
+                        'status'    => $sale['status'],
+                        'skidka'    => (float)$sale['skidka'],
+                        'products'  => []
                     ];
                 } else {
-                    // Суммируем значения
-                    $aggregated[$saleKey]['summ'] += (float)$sale['summ'];
+                    $aggregated[$saleKey]['summ']   += (float)$sale['summ'];
                     $aggregated[$saleKey]['skidka'] += (float)$sale['skidka'];
                 }
 
-                // Получаем товары из этого чека
-                $products = \yii_app\records\SalesProducts::find()
-                    ->where(['check_id' => $sale['id']])
-                    ->asArray()
-                    ->all();
+                foreach ($productsBySale[$sale['id']] ?? [] as $product) {
+                    $pid = $product['product_id'];
 
-                foreach ($products as $product) {
-                    // Получаем информацию о товаре
-                    $productInfo = \yii_app\records\Products1c::find()
-                        ->where(['id' => $product['product_id']])
-                        ->select(['id', 'name'])
-                        ->asArray()
-                        ->one();
-
-                    // Получаем актуальную цену товара
-                    $regionId = $storeRegions[$saleStoreId];
-                    $priceQuery = \yii_app\records\PricesDynamic::find()
-                        ->where(['product_id' => $product['product_id']])
-                        ->andWhere(['active' => \yii_app\records\PricesDynamic::ACTIVE]);
-
-                    // Приоритет: сначала ищем с регионом, потом без
-                    if ($regionId) {
-                        $priceQuery->andWhere(['region_id' => $regionId]);
+                    // Region price → any-region latest → sale line price
+                    if ($regionId !== null && isset($pricesByProduct[$pid][$regionId])) {
+                        $price = $pricesByProduct[$pid][$regionId];
+                    } elseif (isset($pricesByProduct[$pid]['_latest'])) {
+                        $price = $pricesByProduct[$pid]['_latest'];
+                    } else {
+                        $price = (float)$product['price'];
                     }
 
-                    $priceData = $priceQuery->select(['price'])
-                        ->orderBy(['date_from' => SORT_DESC])
-                        ->asArray()
-                        ->one();
-
-                    $productData = [
-                        'product_id' => $product['product_id'],
-                        'name' => $productInfo['name'] ?? 'Unknown',
-                        'quantity' => (float)$product['quantity'],
-                        'price' => $priceData ? (float)$priceData['price'] : (float)$product['price'],
-                        'discount' => (float)$product['discount'],
-                        'summ' => (float)$product['summ']
+                    $aggregated[$saleKey]['products'][] = [
+                        'product_id' => $pid,
+                        'name'       => $productInfoMap[$pid]['name'] ?? 'Unknown',
+                        'quantity'   => (float)$product['quantity'],
+                        'price'      => $price,
+                        'discount'   => (float)$product['discount'],
+                        'summ'       => (float)$product['summ']
                     ];
-
-                    $aggregated[$saleKey]['products'][] = $productData;
                 }
             }
 
@@ -295,7 +312,6 @@ class DataTestController extends BaseController {
             $grouped = [];
             foreach ($aggregated as $sale) {
                 $storeId = $sale['store_id'];
-                // Убираем store_id из объекта продажи, так как он будет в ключе
                 unset($sale['store_id']);
                 $grouped[$storeId][] = $sale;
             }
@@ -306,9 +322,10 @@ class DataTestController extends BaseController {
             ]);
 
         } catch (Exception $e) {
+            Yii::error($e->getMessage(), 'api2.data-test');
             return $this->asJson([
                 'success' => false,
-                'error' => $e->getMessage()
+                'error' => 'Internal server error'
             ]);
         }
     }
@@ -350,7 +367,7 @@ class DataTestController extends BaseController {
 
         try {
             $input = Json::decode($request);
-        } catch (Exception $ex) {
+        } catch (Exception) {
             return $this->asJson(['success' => false, 'error' => 'Invalid JSON body']);
         }
 
@@ -362,32 +379,30 @@ class DataTestController extends BaseController {
             return $this->asJson(['success' => false, 'error' => 'date is required']);
         }
 
-        $storeId   = (int) $input['store_id'];
+        $storeId   = (int)$input['store_id'];
         $date      = $input['date'];
-        $shiftType = (int) ($input['shift_type'] ?? 0);
+        $shiftType = (int)($input['shift_type'] ?? 0);
 
         try {
-            $dateObj      = new \DateTime($date);
-            $nextDateStr  = (clone $dateObj)->modify('+1 day')->format('Y-m-d');
+            $nextDateStr = (new \DateTime($date))->modify('+1 day')->format('Y-m-d');
 
+            // Только фактически работавшие (не отпуск, не больничный, не выходной)
             $query = TimetableFact::find()
-                ->select(['timetable.admin_id'])
-                ->andWhere(['store_id' => $storeId]);
+                ->select(['admin_id'])
+                ->andWhere(['store_id' => $storeId])
+                ->andWhere(['slot_type_id' => [Timetable::TIMESLOT_WORK, Timetable::TIMESLOT_INTERNSHIP]]);
 
             if ($shiftType === 1) {
-                // Дневная: 08:00 – 20:00
                 $query->andWhere(['between', 'datetime_start',
                     $date . ' 08:00:00',
                     $date . ' 19:59:59',
                 ]);
             } elseif ($shiftType === 2) {
-                // Ночная: 20:00 – 08:00 следующего дня
                 $query->andWhere(['OR',
                     ['between', 'datetime_start', $date . ' 20:00:00', $date . ' 23:59:59'],
                     ['between', 'datetime_start', $nextDateStr . ' 00:00:00', $nextDateStr . ' 07:59:59'],
                 ]);
             } else {
-                // Все смены за дату
                 $query->andWhere(['between', 'datetime_start',
                     $date . ' 00:00:00',
                     $date . ' 23:59:59',
@@ -402,19 +417,17 @@ class DataTestController extends BaseController {
 
             $adminIds = array_unique(array_column($rows, 'admin_id'));
 
-            // Получаем данные сотрудников (имя, должность)
-            $admins = \yii_app\records\Admin::find()
+            $admins = Admin::find()
                 ->select(['id', 'name_full', 'employee_position_id'])
                 ->where(['id' => $adminIds])
                 ->indexBy('id')
                 ->asArray()
                 ->all();
 
-            // Получаем названия должностей
-            $positionIds = array_filter(array_column($admins, 'employee_position_id'));
-            $positions = [];
+            $positionIds = array_filter(array_unique(array_column($admins, 'employee_position_id')));
+            $positions   = [];
             if (!empty($positionIds)) {
-                $positions = \yii_app\records\EmployeePosition::find()
+                $positions = EmployeePosition::find()
                     ->select(['id', 'name'])
                     ->where(['id' => $positionIds])
                     ->indexBy('id')
@@ -422,7 +435,6 @@ class DataTestController extends BaseController {
                     ->all();
             }
 
-            // Получаем текущий грейд (активная запись: closed_at = '2100-01-01 00:00:00')
             $gradeRows = AdminGradeHistory::find()
                 ->alias('agh')
                 ->select(['agh.admin_id', 'g.id AS grade_id', 'g.name AS grade_name'])
@@ -434,7 +446,6 @@ class DataTestController extends BaseController {
 
             $grades = array_column($gradeRows, null, 'admin_id');
 
-            // Получаем seller_id (GUID из 1С) для каждого сотрудника
             $exportRows = ExportImportTable::find()
                 ->select(['entity_id', 'export_val'])
                 ->where([
@@ -467,7 +478,8 @@ class DataTestController extends BaseController {
             return $this->asJson(['success' => true, 'data' => $result]);
 
         } catch (Exception $e) {
-            return $this->asJson(['success' => false, 'error' => $e->getMessage()]);
+            Yii::error($e->getMessage(), 'api2.data-test');
+            return $this->asJson(['success' => false, 'error' => 'Internal server error']);
         }
     }
 }