]> gitweb.erp-flowers.ru Git - erp24_rep/yii-erp24/.git/commitdiff
Детальный отчет по МП
authorVladimir Fomichev <vladimir.fomichev@erp-flowers.ru>
Thu, 6 Nov 2025 10:12:37 +0000 (13:12 +0300)
committerVladimir Fomichev <vladimir.fomichev@erp-flowers.ru>
Thu, 6 Nov 2025 10:12:37 +0000 (13:12 +0300)
erp24/actions/dashboard/MarketplaceSalesReportAction.php [new file with mode: 0644]
erp24/controllers/DashboardController.php
erp24/records/MarketplaceOrderStatusTypes.php
erp24/services/MarketplaceSalesMatchingService.php [new file with mode: 0644]
erp24/views/dashboard/marketplace-sales-report.php [new file with mode: 0644]
erp24/views/dashboard/sales.php

diff --git a/erp24/actions/dashboard/MarketplaceSalesReportAction.php b/erp24/actions/dashboard/MarketplaceSalesReportAction.php
new file mode 100644 (file)
index 0000000..78fb954
--- /dev/null
@@ -0,0 +1,169 @@
+<?php
+declare(strict_types = 1);
+
+namespace yii_app\actions\dashboard;
+
+use Yii;
+use yii\base\Action;
+use yii\helpers\ArrayHelper;
+use yii\widgets\ActiveForm;
+use yii_app\forms\dashboard\DaysSearchForm;
+use yii_app\records\CityStore;
+use yii_app\records\Products1c;
+use yii_app\services\ExportImportService;
+use yii_app\services\MarketplaceSalesMatchingService;
+
+/**
+ * Отчет по сопоставлению заказов маркетплейсов с чеками продаж
+ *
+ * @package yii_app\actions\dashboard
+ */
+class MarketplaceSalesReportAction extends Action
+{
+    /**
+     * @throws \Exception
+     */
+    public function run()
+    {
+        $request = Yii::$app->getRequest();
+
+        $date1 = date("Y-m-d", time());
+        $date2 = date("Y-m-d", time());
+
+        $daysSearchForm = new DaysSearchForm();
+
+        if (Yii::$app->request->isAjax && $daysSearchForm->load(Yii::$app->request->post())) {
+            Yii::$app->response->format = \yii\web\Response::FORMAT_JSON;
+            return ActiveForm::validate($daysSearchForm);
+        }
+
+        if ($request->isPost && !empty($this->controller->request->post())) {
+            $daysSearchForm->load($this->controller->request->post());
+            $daysSearchForm->validate();
+        }
+
+        if (empty($daysSearchForm->dateFrom)) {
+            $daysSearchForm->dateFrom = $date1;
+        }
+
+        if (empty($daysSearchForm->dateTo)) {
+            $daysSearchForm->dateTo = $date2;
+        }
+
+        $getDate1 = $daysSearchForm->dateFrom;
+
+        if (!empty($getDate1)) {
+            $date_s = htmlentities($getDate1);
+            $date1 = "$date_s";
+            $date2 = "$date_s";
+        }
+
+        $getDate2 = $daysSearchForm->dateTo;
+
+        if (!empty($getDate2)) {
+            $date_s = htmlentities($getDate2);
+            $date2 = "$date_s";
+        }
+
+        $entityByCityStore = ExportImportService::getEntityByCityStore();
+        $exportData = ExportImportService::getExportData($entityByCityStore);
+
+        $export = ArrayHelper::getValue($exportData, 'export');
+        $export_revers = ArrayHelper::getValue($exportData, 'export_revers');
+
+        $storeIds = array_values($export_revers);
+
+        $cityStores = CityStore::find()
+            ->select(['id', 'name', 'export_val'])
+            ->joinWith('storeGuid')
+            ->andWhere(['id' => $storeIds])
+            ->indexBy('id')
+            ->asArray()
+            ->all();
+
+        $city_stores = Products1c::find()
+            ->select(['name', 'id'])
+            ->indexBy('id')
+            ->andWhere('tip=:tip', [':tip' => 'city_store'])
+            ->column();
+
+        // Получение заказов МП и чеков продаж
+        $matchingService = new MarketplaceSalesMatchingService();
+
+        $marketplaceOrders = $matchingService->getMarketplaceOrdersForPeriod($date1, $date2);
+
+        $sales = $matchingService->getSalesForPeriod($date1, $date2);
+
+        // Сопоставление заказов с чеками
+        $matchedData = $matchingService->matchOrdersWithSales($marketplaceOrders, $sales);
+
+        // Группировка по магазинам
+        $marketplaceOrdersByStore = $this->groupOrdersByStore($matchedData);
+        $params = [
+            'date1' => $date1,
+            'date2' => $date2,
+            'daysSearchForm' => $daysSearchForm,
+            'cityStores' => $cityStores,
+            'city_stores' => $city_stores,
+            'export_revers' => $export_revers,
+            'marketplaceOrdersByStore' => $marketplaceOrdersByStore,
+        ];
+
+        return $this->controller->render('/dashboard/marketplace-sales-report.php', $params);
+    }
+
+    /**
+     * Группировка заказов по магазинам
+     *
+     * @param array $matchedData
+     * @return array
+     */
+    private function groupOrdersByStore(array $matchedData): array
+    {
+        $grouped = [];
+
+        foreach ($matchedData as $orderId => $orderData) {
+            $order = $orderData['order'];
+            
+            // Получаем store_id_1c через связь с магазином
+            $storeId1c = null;
+            if ($order->store && $order->store->storeGuid) {
+                $storeId1c = $order->store->storeGuid->export_val;
+            }
+            
+            // Если не удалось получить store_id_1c, пропускаем заказ
+            if (empty($storeId1c)) {
+                continue;
+            }
+
+            if (!isset($grouped[$storeId1c])) {
+                $grouped[$storeId1c] = [
+                    'store_id_1c' => $storeId1c,
+                    'orders' => [],
+                    'total_orders' => 0,
+                    'total_matched' => 0,
+                    'total_unmatched' => 0,
+                    'total_summ' => 0,
+                ];
+            }
+
+            $grouped[$storeId1c]['orders'][$orderId] = $orderData;
+            $grouped[$storeId1c]['total_orders']++;
+            $grouped[$storeId1c]['total_summ'] += $order->total;
+
+            if (!empty($orderData['matched_sales'])) {
+                $grouped[$storeId1c]['total_matched']++;
+            } else {
+                $grouped[$storeId1c]['total_unmatched']++;
+            }
+        }
+
+        // Сортировка по сумме (убывание)
+        uasort($grouped, function ($a, $b) {
+            return $b['total_summ'] <=> $a['total_summ'];
+        });
+
+        return $grouped;
+    }
+}
+
index 052c9e658639592bcc6a00c7f2d676f7d65b303b..8db9b464efd59e5531e1809bb9680f37d81e783c 100755 (executable)
@@ -13,6 +13,7 @@ class DashboardController extends \yii\web\Controller
             'index' => \yii_app\actions\dashboard\IndexAction::class,
             'sales' => \yii_app\actions\dashboard\SalesAction::class,
             'sales-detail' => \yii_app\actions\dashboard\SalesDetailAction::class,
+            'marketplace-sales-report' => \yii_app\actions\dashboard\MarketplaceSalesReportAction::class,
             'commercial' => \yii_app\actions\dashboard\CommercialAction::class,
             'commercial-detail-info' => \yii_app\actions\dashboard\CommercialDetailInfoAction::class,
             'commercial-sales-info' => \yii_app\actions\dashboard\CommercialSalesInfoAction::class,
index 3475db92a14f56d6f7d88ba925daba24261eccbc..1b3c0bc9962cf53db4128321756eee887af462d6 100644 (file)
@@ -19,6 +19,8 @@ class MarketplaceOrderStatusTypes extends \yii\db\ActiveRecord
 {
     const CANSELLED_CODE = 'CANCELLED';
     const READY_CODE = 'READY_TO_SHIP';
+    const DELIVERED_CODE = 'DELIVERED';
+    const DELIVERY_SERVICE_DELIVERED_CODE = 'DELIVERY_SERVICE_DELIVERED';
 
     public function behaviors()
     {
diff --git a/erp24/services/MarketplaceSalesMatchingService.php b/erp24/services/MarketplaceSalesMatchingService.php
new file mode 100644 (file)
index 0000000..3b3ac07
--- /dev/null
@@ -0,0 +1,471 @@
+<?php
+
+namespace yii_app\services;
+
+use Yii;
+use yii\helpers\ArrayHelper;
+use yii_app\helpers\DateHelper;
+use yii_app\records\MarketplaceOrders;
+use yii_app\records\MarketplaceOrderItems;
+use yii_app\records\MarketplaceOrderStatusHistory;
+use yii_app\records\MarketplaceOrderStatusTypes;
+use yii_app\records\Sales;
+use yii_app\records\SalesProducts;
+use yii_app\records\Products1c;
+use yii_app\records\Products1cOptions;
+
+/**
+ * Сервис для сопоставления заказов с маркетплейсов с чеками продаж
+ *
+ * @package yii_app\services
+ */
+class MarketplaceSalesMatchingService
+{
+    /**
+     * Получить заказы МП за период
+     *
+     * Учитывает дату завершения заказа из истории статусов (MarketplaceOrderStatusHistory)
+     * Фильтрует по суб-статусу DELIVERY_SERVICE_DELIVERED (гарантированно завершенные заказы)
+     *
+     * @param string $dateFrom
+     * @param string $dateTo
+     * @param int|null $storeId
+     * @param int|null $marketplaceId
+     * @return MarketplaceOrders[]
+     * @throws \Exception
+     */
+    public function getMarketplaceOrdersForPeriod(
+        string $dateFrom,
+        string $dateTo,
+        ?int $storeId = null,
+        ?int $marketplaceId = null
+    ): array {
+        // Получаем ID статусов для фильтрации
+        $deliveredStatus = MarketplaceOrderStatusTypes::findOne([
+            'code' => MarketplaceOrderStatusTypes::DELIVERED_CODE
+        ]);
+        
+        $deliveryServiceDeliveredSubstatus = MarketplaceOrderStatusTypes::findOne([
+            'code' => MarketplaceOrderStatusTypes::DELIVERY_SERVICE_DELIVERED_CODE
+        ]);
+        
+        if (!$deliveredStatus || !$deliveryServiceDeliveredSubstatus) {
+            Yii::warning('Не найдены статусы DELIVERED или DELIVERY_SERVICE_DELIVERED');
+            return [];
+        }
+
+        // Основная логика: получаем заказы, у которых дата завершения (из истории) в нужном диапазоне
+        $tableName = MarketplaceOrders::tableName();
+        $historyTableName = MarketplaceOrderStatusHistory::tableName();
+        
+        $query = MarketplaceOrders::find()
+            ->with(['items', 'store', 'store.storeGuid', 'status', 'substatus'])
+            ->innerJoin(
+                "$historyTableName mosh",
+                "$tableName.id = mosh.order_id"
+            )
+            ->andWhere(["$tableName.fake" => 0]) // Только реальные заказы
+            ->andWhere(["$tableName.status_id" => $deliveredStatus->id]) // Статус DELIVERED
+            ->andWhere(["$tableName.substatus_id" => $deliveryServiceDeliveredSubstatus->id]) // Суб-статус DELIVERY_SERVICE_DELIVERED
+            ->andWhere(['mosh.active' => 1]) // Активная запись в истории
+            ->andWhere(['>=', 'mosh.date_from', DateHelper::getDateTimeStartDay($dateFrom, true)])
+            ->andWhere(['<=', 'mosh.date_from', DateHelper::getDateTimeEndDay($dateTo, true)])
+            ->orderBy(['mosh.date_from' => SORT_DESC, "$tableName.id" => SORT_DESC]);
+
+        if (!empty($storeId)) {
+            $query->andWhere(["$tableName.store_id" => $storeId]);
+        }
+
+        if (!empty($marketplaceId)) {
+            $query->andWhere(["$tableName.marketplace_id" => $marketplaceId]);
+        }
+
+        return $query->all();
+    }
+
+    /**
+     * Получить чеки продаж за период (+ 3 дня)
+     *
+     * @param string $dateFrom
+     * @param string $dateTo
+     * @param int|null $storeId
+     * @return Sales[]
+     * @throws \Exception
+     */
+    public function getSalesForPeriod(
+        string $dateFrom,
+        string $dateTo,
+        ?int $storeId = null
+    ): array {
+        // Расширяем период на 3 дня для поиска чеков, которые могли поступить позже
+        $dateToExtended = date('Y-m-d', strtotime($dateTo) + 3 * 86400);
+
+        $query = Sales::find()
+            ->with(['products', 'store'])
+            ->andWhere(['>=', 'date', DateHelper::getDateTimeStartDay($dateFrom, true)])
+            ->andWhere(['<=', 'date', DateHelper::getDateTimeEndDay($dateToExtended, true)])
+            ->andWhere(['operation' => Sales::OPERATION_SALE])
+            ->orderBy(['date' => SORT_DESC, 'id' => SORT_DESC]);
+
+        if (!empty($storeId)) {
+            $query->andWhere(['store_id' => $storeId]);
+        }
+
+        return $query->all();
+    }
+
+    /**
+     * Сопоставить заказы МП с чеками продаж
+     *
+     * @param array $orders
+     * @param array $sales
+     * @return array
+     */
+    public function matchOrdersWithSales(array $orders, array $sales): array
+    {
+        $result = [];
+        $productsCache = $this->buildProductsCache();
+       
+        foreach ($orders as $order) {
+            // Гарантируем загрузку статусов
+            $statusName = 'Неизвестный статус';
+            $substatusName = 'Неизвестный суб-статус';
+
+            // Пытаемся получить статус
+            if (!empty($order->status_id)) {
+                // Сначала проверяем загруженную связь
+                $status = $order->isRelationPopulated('status') ? $order->status : null;
+
+                // Если не загружена, загружаем вручную
+                if (!$status) {
+                    $status = MarketplaceOrderStatusTypes::findOne(['id' => $order->status_id]);
+                    if ($status) {
+                        $order->populateRelation('status', $status);
+                        Yii::warning("Статус загружен вручную для заказа #{$order->id}: {$status->name}", __METHOD__);
+                    } else {
+                        Yii::error("Не найден статус с ID={$order->status_id} для заказа #{$order->id}", __METHOD__);
+                    }
+                }
+                
+                if ($status && !empty($status->name)) {
+                    $statusName = $status->name;
+                } else {
+                    $statusName = $status->code;
+                }
+
+            }
+            
+            // Пытаемся получить суб-статус
+            if (!empty($order->substatus_id)) {
+                // Сначала проверяем загруженную связь
+                $substatus = $order->isRelationPopulated('substatus') ? $order->substatus : null;
+                
+                // Если не загружена, загружаем вручную
+                if (!$substatus) {
+                    $substatus = MarketplaceOrderStatusTypes::findOne(['id' => $order->substatus_id]);
+                    if ($substatus) {
+                        $order->populateRelation('substatus', $substatus);
+                        Yii::warning("Суб-статус загружен вручную для заказа #{$order->id}: {$substatus->name}", __METHOD__);
+                    } else {
+                        Yii::error("Не найден суб-статус с ID={$order->substatus_id} для заказа #{$order->id}", __METHOD__);
+                    }
+                }
+                
+                if ($substatus && !empty($substatus->name)) {
+                    $substatusName = $substatus->name;
+                } else {
+                    $substatusName = $substatus->code;
+                }
+            }
+
+            $orderData = [
+                'order' => $order,
+                'order_items' => $this->enrichOrderItems($order->items, $productsCache),
+                'matched_sales' => [],
+                'status_name' => $statusName,
+                'substatus_name' => $substatusName,
+            ];
+
+            // Получаем сумму товаров в заказе (без доставки, только товары)
+            $orderTotalSum = (float)$order->total;
+
+            // Ищем подходящие чеки
+            foreach ($sales as $sale) {
+                $saleSum = (float)($sale->summ - $sale->skidka);
+
+                // Сравниваем суммы (точное совпадение)
+                if (abs($saleSum - $orderTotalSum) < 0.01) {
+                    // Получаем товары в чеке
+                    $saleProducts = $sale->products ?? [];
+                    $enrichedSaleProducts = $this->enrichSalesProducts($saleProducts, $productsCache);
+
+                    // Сравниваем товары
+                    $productMatch = $this->compareOrderProducts(
+                        $order->items,
+                        $saleProducts,
+                        $productsCache
+                    );
+
+                    if ($productMatch['match_percent'] >= 80) {
+                        $orderData['matched_sales'][] = [
+                            'sale' => $sale,
+                            'sale_products' => $enrichedSaleProducts,
+                            'match_score' => $this->calculateMatchScore(
+                                $order,
+                                $sale,
+                                $productMatch
+                            ),
+                            'sum_diff' => $saleSum - $orderTotalSum,
+                            'products_match_percent' => $productMatch['match_percent'],
+                            'products_match_details' => $productMatch['details'],
+                        ];
+                    }
+                }
+            }
+
+            // ОТЛАДКА перед добавлением в результат
+            Yii::info("OrderData for order #{$order->id}: Keys=" . implode(', ', array_keys($orderData)) . ", status_name={$statusName}, substatus_name={$substatusName}", __METHOD__);
+            
+            $result[$order->id] = $orderData;
+            
+            // Отладка: проверка содержимого orderData после добавления
+            if (count($orderData) !== 7) {
+                Yii::error("OrderData имеет " . count($orderData) . " ключей вместо 7 для заказа #{$order->id}. Keys: " . implode(', ', array_keys($orderData)), __METHOD__);
+            }
+            
+            if (empty($orderData['status_name']) || empty($orderData['substatus_name'])) {
+                Yii::error("OrderData не содержит status_name или substatus_name для заказа #{$order->id}. status_name=" . ($orderData['status_name'] ?? 'MISSING') . ", substatus_name=" . ($orderData['substatus_name'] ?? 'MISSING'), __METHOD__);
+            }
+        }
+
+        return $result;
+    }
+
+    /**
+     * Сравнить товары между заказом МП и чеком продаж
+     *
+     * @param MarketplaceOrderItems[] $orderItems
+     * @param SalesProducts[] $saleProducts
+     * @param array $productsCache
+     * @return array
+     */
+    public function compareOrderProducts(
+        array $orderItems,
+        array $saleProducts,
+        array $productsCache
+    ): array {
+        // Категория упаковки - исключаем из сравнения
+        $wrappingProductIds = $this->getWrappingProductIds();
+
+        // Преобразуем товары заказа в массив (offer_id => quantity)
+        $orderProductsMap = [];
+        foreach ($orderItems as $item) {
+            $productId = $this->getProductIdByArticle($item->offer_id, $productsCache);
+            if ($productId && !in_array($productId, $wrappingProductIds)) {
+                $key = $productId;
+                if (!isset($orderProductsMap[$key])) {
+                    $orderProductsMap[$key] = 0;
+                }
+                $orderProductsMap[$key] += (int)$item->count;
+            }
+        }
+
+        // Преобразуем товары чека в массив (product_id => quantity), исключая упаковку
+        $saleProductsMap = [];
+        foreach ($saleProducts as $product) {
+            if (!in_array($product->product_id, $wrappingProductIds)) {
+                $key = $product->product_id;
+                if (!isset($saleProductsMap[$key])) {
+                    $saleProductsMap[$key] = 0;
+                }
+                $saleProductsMap[$key] += (int)$product->quantity;
+            }
+        }
+
+        // Сравниваем товары
+        $matchedCount = 0;
+        $totalOrderProducts = count($orderProductsMap);
+        $details = [];
+
+        foreach ($orderProductsMap as $productId => $orderQty) {
+            $saleQty = $saleProductsMap[$productId] ?? 0;
+            if ($orderQty === $saleQty) {
+                $matchedCount++;
+                $details[$productId] = [
+                    'matched' => true,
+                    'order_qty' => $orderQty,
+                    'sale_qty' => $saleQty,
+                ];
+            } else {
+                $details[$productId] = [
+                    'matched' => false,
+                    'order_qty' => $orderQty,
+                    'sale_qty' => $saleQty,
+                ];
+            }
+        }
+
+        $matchPercent = $totalOrderProducts > 0 ? round(($matchedCount / $totalOrderProducts) * 100) : 0;
+
+        return [
+            'match_percent' => $matchPercent,
+            'matched_count' => $matchedCount,
+            'total_count' => $totalOrderProducts,
+            'details' => $details,
+        ];
+    }
+
+    /**
+     * Расчет степени совпадения
+     *
+     * @param MarketplaceOrders $order
+     * @param Sales $sale
+     * @param array $productMatch
+     * @return float
+     */
+    public function calculateMatchScore(
+        MarketplaceOrders $order,
+        Sales $sale,
+        array $productMatch
+    ): float {
+        // Базовая оценка совпадения товаров (80% от оценки)
+        $productScore = $productMatch['match_percent'] / 100 * 0.8;
+
+        // Проверяем совпадение дат (20% от оценки)
+        // Чем ближе даты, тем выше оценка
+        try {
+            $orderDate = new \DateTime($order->creation_date);
+            $saleDate = new \DateTime($sale->date);
+            $daysDiff = abs($orderDate->diff($saleDate)->days);
+
+            // Идеальное совпадение: 0 дней = 20%, за каждый день минус 3.3% (макс 3 дня = 0%)
+            $dateScore = max(0, (1 - ($daysDiff / 3)) * 0.2);
+        } catch (\Exception $e) {
+            $dateScore = 0;
+        }
+
+        return round(($productScore + $dateScore), 2);
+    }
+
+    /**
+     * Обогатить товары заказа информацией о названиях
+     *
+     * @param MarketplaceOrderItems[] $items
+     * @param array $productsCache
+     * @return array
+     */
+    private function enrichOrderItems(array $items, array $productsCache): array
+    {
+        $result = [];
+        foreach ($items as $item) {
+            $productId = $this->getProductIdByArticle($item->offer_id, $productsCache);
+            $productName = $this->getProductName($productId, $productsCache);
+
+            $result[] = [
+                'offer_id' => $item->offer_id,
+                'offer_name' => $item->offer_name,
+                'product_name' => $productName,
+                'product_id' => $productId,
+                'quantity' => $item->count,
+                'price' => $item->price,
+                'buyer_price' => $item->buyer_price,
+                'summ' => $item->count * $item->price,
+            ];
+        }
+        return $result;
+    }
+
+    /**
+     * Обогатить товары продажи информацией о названиях
+     *
+     * @param SalesProducts[] $products
+     * @param array $productsCache
+     * @return array
+     */
+    private function enrichSalesProducts(array $products, array $productsCache): array
+    {
+        $result = [];
+        foreach ($products as $product) {
+            $productName = $this->getProductName($product->product_id, $productsCache);
+
+            $result[] = [
+                'product_id' => $product->product_id,
+                'product_name' => $productName,
+                'quantity' => $product->quantity,
+                'price' => $product->price,
+                'discount' => $product->discount,
+                'summ' => $product->summ,
+                'color' => $product->color,
+            ];
+        }
+        return $result;
+    }
+
+    /**
+     * Получить ID товара по артикулу (offer_id)
+     *
+     * @param string $articule
+     * @param array $cache
+     * @return string|null
+     */
+    private function getProductIdByArticle(string $articule, array &$cache): ?string
+    {
+        $cacheKey = "article_{$articule}";
+        if (isset($cache[$cacheKey])) {
+            return $cache[$cacheKey];
+        }
+
+        $product = Products1c::findOne(['articule' => $articule]);
+        $productId = $product ? $product->id : null;
+
+        $cache[$cacheKey] = $productId;
+        return $productId;
+    }
+
+    /**
+     * Получить название товара
+     *
+     * @param string|null $productId
+     * @param array $cache
+     * @return string
+     */
+    private function getProductName(?string $productId, array &$cache): string
+    {
+        if (empty($productId)) {
+            return 'Неизвестный товар';
+        }
+
+        $cacheKey = "product_{$productId}";
+        if (isset($cache[$cacheKey])) {
+            return $cache[$cacheKey];
+        }
+
+        $product = Products1c::findOne(['id' => $productId]);
+        $name = $product ? $product->name : 'Неизвестный товар';
+
+        $cache[$cacheKey] = $name;
+        return $name;
+    }
+
+    /**
+     * Построить кэш товаров для оптимизации
+     *
+     * @return array
+     */
+    private function buildProductsCache(): array
+    {
+        return [];
+    }
+
+    /**
+     * Получить ID товаров категории "упаковка"
+     *
+     * @return array
+     */
+    private function getWrappingProductIds(): array
+    {
+        $productsArr = Products1c::getProductsFromClass(['wrap']);
+        return $productsArr['wrap'] ?? [];
+    }
+}
+
diff --git a/erp24/views/dashboard/marketplace-sales-report.php b/erp24/views/dashboard/marketplace-sales-report.php
new file mode 100644 (file)
index 0000000..97c257e
--- /dev/null
@@ -0,0 +1,411 @@
+<?php
+/**
+ * @var $date1
+ * @var $date2
+ * @var $daysSearchForm
+ * @var $cityStores
+ * @var $city_stores
+ * @var $export_revers
+ * @var $marketplaceOrdersByStore
+ * @var \yii\web\View $this
+ */
+
+use yii\helpers\Html;
+use yii\widgets\ActiveForm;
+?>
+
+<h2>Отчет по заказам маркетплейсов и чекам продаж</h2>
+
+<?php $searchForm = ActiveForm::begin([
+    'id' => 'days-search-form-marketplace',
+    'enableAjaxValidation' => true,
+    'validationUrl' => 'validate',
+    'options' => ['enctype' => 'multipart/form-data']
+]); ?>
+
+<div class="row mb-3">
+    <div class="col-lg-4">
+        <?= $searchForm->field($daysSearchForm, 'dateFrom', [
+            'inputOptions' => [
+                'class' => 'form-control datetime',
+                'type' => 'date',
+                'placeholder' => 'начало',
+            ],
+            'options' => ['tag' => null],
+        ])->label(false)->textInput() ?>
+    </div>
+    <div class="col-lg-4">
+        <?= $searchForm->field($daysSearchForm, 'dateTo', [
+            'inputOptions' => [
+                'class' => 'form-control datetime',
+                'type' => 'date',
+                'placeholder' => 'конец',
+            ],
+            'options' => ['tag' => null],
+        ])->label(false)->textInput() ?>
+    </div>
+    <div class="col-lg-4">
+        <div class="form-group">
+            <label class="control-label">&nbsp;</label>
+            <?= Html::submitButton('Показать', ['class' => 'btn btn-success']) ?>
+        </div>
+    </div>
+</div>
+
+<?php ActiveForm::end() ?>
+
+<div class="marketplace-report-container">
+    <?php if (!empty($marketplaceOrdersByStore)): ?>
+
+        <div class="mb-4">
+            <div class="alert alert-info">
+                <strong>Период:</strong> <?= $date1 ?> - <?= $date2 ?>
+                <strong class="ms-3">Всего магазинов:</strong> <?= count($marketplaceOrdersByStore) ?>
+            </div>
+        </div>
+
+        <!-- АККОРДЕОН ПО МАГАЗИНАМ -->
+        <div class="accordion" id="accordionStores">
+            <?php foreach ($marketplaceOrdersByStore as $storeId1c => $storeData): ?>
+                <?php
+                    $storeNameExport = $city_stores[$storeId1c] ?? 'Неизвестный магазин';
+                    $totalOrders = $storeData['total_orders'];
+                    $totalMatched = $storeData['total_matched'];
+                    $totalUnmatched = $storeData['total_unmatched'];
+                    $totalSumm = $storeData['total_summ'];
+                    $matchPercent = $totalOrders > 0 ? round(($totalMatched / $totalOrders) * 100) : 0;
+                ?>
+                <div class="accordion-item">
+                    <h2 class="accordion-header" id="headingStore<?= htmlspecialchars($storeId1c) ?>">
+                        <button class="accordion-button collapsed" type="button" data-bs-toggle="collapse"
+                                data-bs-target="#collapseStore<?= htmlspecialchars($storeId1c) ?>"
+                                aria-expanded="false"
+                                aria-controls="collapseStore<?= htmlspecialchars($storeId1c) ?>">
+                            <strong><?= Html::encode($storeNameExport) ?></strong>
+                            <span class="badge bg-primary ms-2"><?= $totalOrders ?> заказов</span>
+                            <span class="badge bg-success ms-2"><?= $totalMatched ?> найдено</span>
+                            <span class="badge bg-danger ms-2"><?= $totalUnmatched ?> не найдено</span>
+                            <span class="badge bg-warning ms-2 text-dark"><?= $matchPercent ?>%</span>
+                            <span class="badge bg-info ms-2"><?= number_format($totalSumm, 2, '.', ' ') ?> ₽</span>
+                        </button>
+                    </h2>
+                    <div id="collapseStore<?= htmlspecialchars($storeId1c) ?>" class="accordion-collapse collapse"
+                         aria-labelledby="headingStore<?= htmlspecialchars($storeId1c) ?>"
+                         data-bs-parent="#accordionStores">
+                        <div class="accordion-body p-0">
+
+                            <!-- ВЛОЖЕННЫЙ АККОРДЕОН ПО ЗАКАЗАМ -->
+                            <div class="accordion" id="accordionOrders<?= htmlspecialchars($storeId1c) ?>">
+                                <?php foreach ($storeData['orders'] as $orderId => $orderData): ?>
+                                    <?php
+                                        $order = $orderData['order'];
+                                        $orderItems = $orderData['order_items'];
+                                        $matchedSales = $orderData['matched_sales'];
+                                        $marketplaceNames = [
+                                            1 => 'Flowwow',
+                                            2 => 'Яндекс Маркет',
+                                        ];
+                                        $marketplaceName = $marketplaceNames[$order->marketplace_id] ?? 'Неизвестный МП';
+                                        $creationDate = date('d.m.Y H:i:s', strtotime($order->creation_date));
+                                        $hasMatches = !empty($matchedSales);
+                                    ?>
+                                    <div class="accordion-item">
+                                        <h2 class="accordion-header" id="headingOrder<?= htmlspecialchars($orderId) ?>">
+                                            <button class="accordion-button collapsed" type="button" data-bs-toggle="collapse"
+                                                    data-bs-target="#collapseOrder<?= htmlspecialchars($orderId) ?>"
+                                                    aria-expanded="false"
+                                                    aria-controls="collapseOrder<?= htmlspecialchars($orderId) ?>">
+                                                <strong>Заказ #<?= htmlspecialchars($order->marketplace_order_id) ?></strong>
+                                                <span class="badge <?= $hasMatches ? 'bg-success' : 'bg-warning' ?> ms-2">
+                                                    <?= $hasMatches ? '✓ Найдены чеки' : '⚠ Чеки не найдены' ?>
+                                                </span>
+                                                <span class="badge bg-secondary ms-2"><?= $marketplaceName ?></span>
+                                                <span class="badge bg-info ms-2"><?= number_format($order->total, 2, '.', ' ') ?> ₽</span>
+                                                <small class="text-muted ms-2"><?= $creationDate ?></small>
+                                            </button>
+                                        </h2>
+                                        <div id="collapseOrder<?= htmlspecialchars($orderId) ?>" class="accordion-collapse collapse"
+                                             aria-labelledby="headingOrder<?= htmlspecialchars($orderId) ?>"
+                                             data-bs-parent="#accordionOrders<?= htmlspecialchars($storeId1c) ?>">
+                                            <div class="accordion-body">
+
+                                                <!-- ИНФОРМАЦИЯ О ЗАКАЗЕ -->
+                                                <div class="mb-4">
+                                                    <h5 class="text-info">Информация о заказе МП</h5>
+                                                    <table class="table table-sm table-bordered">
+                                                        <tr>
+                                                            <td><strong>ID заказа:</strong></td>
+                                                            <td><?= Html::encode($order->marketplace_order_id) ?></td>
+                                                        </tr>
+                                                        <tr>
+                                                            <td><strong>Дата создания:</strong></td>
+                                                            <td><?= $creationDate ?></td>
+                                                        </tr>
+                                                        <tr>
+                                                            <td><strong>Дата обновления:</strong></td>
+                                                            <td><?= date('d.m.Y H:i:s', strtotime($order->updated_at)) ?></td>
+                                                        </tr>
+                                                        <tr>
+                                                            <td><strong>Маркетплейс:</strong></td>
+                                                            <td><?= $marketplaceName ?></td>
+                                                        </tr>
+                                                        <tr>
+                                                            <td><strong>Сумма заказа:</strong></td>
+                                                            <td><?= number_format($order->total, 2, '.', ' ') ?> ₽</td>
+                                                        </tr>
+                                                        <tr>
+                                                            <td><strong>Статус:</strong></td>
+                                                            <td>
+                                                                <span class="badge bg-primary">
+
+                                                                    <?= Html::encode($orderData['status_name'] ?? 'Неизвестный статус') ?>
+
+                                                                </span>
+                                                            </td>
+                                                        </tr>
+                                                        <tr>
+                                                            <td><strong>Суб-статус:</strong></td>
+                                                            <td>
+                                                                <span class="badge bg-success">
+                                                                    <?= Html::encode($orderData['substatus_name'] ?? 'Неизвестный суб-статус') ?>
+                                                                </span>
+                                                            </td>
+                                                        </tr>
+                                                    </table>
+                                                </div>
+
+                                                <!-- ТОВАРЫ В ЗАКАЗЕ -->
+                                                <div class="mb-4">
+                                                    <h5 class="text-info">Товары в заказе (<?= count($orderItems) ?>)</h5>
+                                                    <table class="table table-sm table-hover">
+                                                        <thead class="table-light">
+                                                        <tr>
+                                                            <th style="width: 40%">Наименование</th>
+                                                            <th style="width: 15%">Кол-во</th>
+                                                            <th style="width: 15%">Цена</th>
+                                                            <th style="width: 15%">Сумма</th>
+                                                            <th style="width: 15%">Артикул</th>
+                                                        </tr>
+                                                        </thead>
+                                                        <tbody>
+                                                        <?php foreach ($orderItems as $item): ?>
+                                                            <tr>
+                                                                <td>
+                                                                    <small>
+                                                                        <strong><?= Html::encode($item['product_name']) ?></strong>
+                                                                        <?php if ($item['product_name'] !== $item['offer_name']): ?>
+                                                                            <br/>
+                                                                            <span class="text-muted">(<?= Html::encode($item['offer_name']) ?>)</span>
+                                                                        <?php endif; ?>
+                                                                    </small>
+                                                                </td>
+                                                                <td><small><?= $item['quantity'] ?></small></td>
+                                                                <td><small><?= number_format($item['price'], 2, '.', ' ') ?> ₽</small></td>
+                                                                <td><small><?= number_format($item['summ'], 2, '.', ' ') ?> ₽</small></td>
+                                                                <td><small class="text-muted"><?= Html::encode($item['offer_id']) ?></small></td>
+                                                            </tr>
+                                                        <?php endforeach; ?>
+                                                        </tbody>
+                                                    </table>
+                                                </div>
+
+                                                <!-- НАЙДЕННЫЕ ЧЕКИ ИЛИ ПРЕДУПРЕЖДЕНИЕ -->
+                                                <?php if ($hasMatches): ?>
+                                                    <div class="mb-4">
+                                                        <h5 class="text-success">Найденные чеки продаж (<?= count($matchedSales) ?>)</h5>
+
+                                                        <?php foreach ($matchedSales as $matchIdx => $saleMatch): ?>
+                                                            <?php
+                                                                $sale = $saleMatch['sale'];
+                                                                $saleProducts = $saleMatch['sale_products'];
+                                                                $matchPercent = $saleMatch['products_match_percent'];
+                                                                $sumDiff = $saleMatch['sum_diff'];
+                                                                $matchScore = $saleMatch['match_score'];
+                                                                $saleDate = date('d.m.Y H:i:s', strtotime($sale->date));
+                                                            ?>
+                                                            <div class="card mb-3 border-success">
+                                                                <div class="card-header bg-success text-white">
+                                                                    <strong>Чек #<?= htmlspecialchars($sale->number) ?></strong>
+                                                                    <span class="float-end">
+                                                                        <span class="badge bg-light text-dark"><?= $saleDate ?></span>
+                                                                        <span class="badge bg-info">Совпадение: <?= $matchPercent ?>%</span>
+                                                                        <span class="badge bg-warning text-dark">Оценка: <?= $matchScore ?></span>
+                                                                    </span>
+                                                                </div>
+                                                                <div class="card-body">
+                                                                    <table class="table table-sm mb-3">
+                                                                        <tr>
+                                                                            <td><strong>ID чека:</strong></td>
+                                                                            <td><?= Html::encode($sale->id) ?></td>
+                                                                        </tr>
+                                                                        <tr>
+                                                                            <td><strong>Дата чека:</strong></td>
+                                                                            <td><?= $saleDate ?></td>
+                                                                        </tr>
+                                                                        <tr>
+                                                                            <td><strong>Сумма чека:</strong></td>
+                                                                            <td><?= number_format($sale->summ - $sale->skidka, 2, '.', ' ') ?> ₽</td>
+                                                                        </tr>
+                                                                        <tr>
+                                                                            <td><strong>Разница в сумме:</strong></td>
+                                                                            <td>
+                                                                                <span class="<?= abs($sumDiff) < 0.01 ? 'text-success' : 'text-warning' ?>">
+                                                                                    <?= number_format($sumDiff, 2, '.', ' ') ?> ₽
+                                                                                </span>
+                                                                            </td>
+                                                                        </tr>
+                                                                    </table>
+
+                                                                    <h6 class="mb-2">Товары в чеке (<?= count($saleProducts) ?>):</h6>
+                                                                    <table class="table table-sm table-hover">
+                                                                        <thead class="table-light">
+                                                                        <tr>
+                                                                            <th style="width: 40%">Наименование</th>
+                                                                            <th style="width: 15%">Кол-во</th>
+                                                                            <th style="width: 15%">Цена</th>
+                                                                            <th style="width: 15%">Сумма</th>
+                                                                            <th style="width: 15%">Скидка</th>
+                                                                        </tr>
+                                                                        </thead>
+                                                                        <tbody>
+                                                                        <?php foreach ($saleProducts as $product): ?>
+                                                                            <tr>
+                                                                                <td><small><?= Html::encode($product['product_name']) ?></small></td>
+                                                                                <td><small><?= $product['quantity'] ?></small></td>
+                                                                                <td><small><?= number_format($product['price'], 2, '.', ' ') ?> ₽</small></td>
+                                                                                <td><small><?= number_format($product['summ'], 2, '.', ' ') ?> ₽</small></td>
+                                                                                <td><small><?= number_format($product['discount'], 2, '.', ' ') ?> ₽</small></td>
+                                                                            </tr>
+                                                                        <?php endforeach; ?>
+                                                                        </tbody>
+                                                                    </table>
+
+                                                                    <?php if (!empty($saleMatch['products_match_details'])): ?>
+                                                                        <h6 class="mt-3 mb-2">Анализ совпадения товаров:</h6>
+                                                                        <table class="table table-sm table-bordered">
+                                                                            <thead class="table-light">
+                                                                            <tr>
+                                                                                <th>Товар</th>
+                                                                                <th style="width: 20%">В заказе</th>
+                                                                                <th style="width: 20%">В чеке</th>
+                                                                                <th style="width: 20%">Статус</th>
+                                                                            </tr>
+                                                                            </thead>
+                                                                            <tbody>
+                                                                            <?php foreach ($saleMatch['products_match_details'] as $productId => $matchDetail): ?>
+                                                                                <tr>
+                                                                                    <td><small><?= htmlspecialchars($productId) ?></small></td>
+                                                                                    <td><?= $matchDetail['order_qty'] ?></td>
+                                                                                    <td><?= $matchDetail['sale_qty'] ?></td>
+                                                                                    <td>
+                                                                                        <?php if ($matchDetail['matched']): ?>
+                                                                                            <span class="badge bg-success">✓ Совпадает</span>
+                                                                                        <?php else: ?>
+                                                                                            <span class="badge bg-warning text-dark">⚠ Отличие</span>
+                                                                                        <?php endif; ?>
+                                                                                    </td>
+                                                                                </tr>
+                                                                            <?php endforeach; ?>
+                                                                            </tbody>
+                                                                        </table>
+                                                                    <?php endif; ?>
+                                                                </div>
+                                                            </div>
+                                                        <?php endforeach; ?>
+                                                    </div>
+                                                <?php else: ?>
+                                                    <div class="alert alert-warning alert-dismissible fade show" role="alert">
+                                                        <strong>⚠ Чеки не найдены</strong>
+                                                        <p class="mb-0">Для этого заказа не найдены соответствующие чеки продаж за период с совпадением суммы и товаров.</p>
+                                                        <button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
+                                                    </div>
+                                                <?php endif; ?>
+
+                                            </div>
+                                        </div>
+                                    </div>
+                                <?php endforeach; ?>
+                            </div>
+
+                        </div>
+                    </div>
+                </div>
+            <?php endforeach; ?>
+        </div>
+
+    <?php else: ?>
+        <div class="alert alert-warning">
+            <strong>Нет данных</strong> за выбранный период <?= $date1 ?> - <?= $date2 ?>
+        </div>
+    <?php endif; ?>
+</div>
+
+<?php
+// CSS для оформления
+$this->registerCss('
+.marketplace-report-container {
+    padding: 15px;
+}
+
+.accordion-button {
+    padding: 12px 15px;
+    font-size: 14px;
+}
+
+.accordion-button:not(.collapsed) {
+    background-color: #f8f9fa;
+    color: #000;
+}
+
+.card {
+    box-shadow: 0 0.125rem 0.25rem rgba(0, 0, 0, 0.075);
+}
+
+.card-header {
+    font-weight: 500;
+    padding: 10px 15px;
+}
+
+.table-hover tbody tr:hover {
+    background-color: #f5f5f5;
+}
+
+.badge {
+    margin-left: 5px;
+    padding: 5px 8px;
+    font-size: 11px;
+}
+
+.text-muted {
+    color: #999;
+}
+
+.text-success {
+    color: #28a745;
+}
+
+.text-danger {
+    color: #dc3545;
+}
+
+.text-warning {
+    color: #ffc107;
+}
+
+.text-info {
+    color: #17a2b8;
+}
+
+h5 {
+    border-bottom: 2px solid #e0e0e0;
+    padding-bottom: 10px;
+    margin-bottom: 15px;
+}
+
+h6 {
+    font-weight: 600;
+    color: #333;
+}
+');
+?>
+
index 5633224213b6daede1c04ecd66c7d4def764c4d3..7c992bd339f5dd1461b415f0e781e39edd6cfef3 100755 (executable)
@@ -56,9 +56,14 @@ tr.line.bg-danger>td>a.btn {
 ?>
 <div class="d-flex justify-content-between align-items-center mb-3">
     <h2>Продажи</h2>
-    <a href="<?= \yii\helpers\Url::to(['dashboard/sales-detail']) ?>" class="btn btn-info btn-sm">
-        <i class="fas fa-list-ul"></i> Детальный просмотр
-    </a>
+    <div>
+        <a href="<?= \yii\helpers\Url::to(['dashboard/sales-detail']) ?>" class="btn btn-info btn-sm me-2">
+            <i class="fas fa-list-ul"></i> Детальный просмотр
+        </a>
+        <a href="<?= \yii\helpers\Url::to(['dashboard/marketplace-sales-report']) ?>" class="btn btn-warning btn-sm">
+            <i class="fas fa-shopping-cart"></i> Отчет по МП
+        </a>
+    </div>
 </div>
 
 <?php $searchForm = \yii\widgets\ActiveForm::begin([