]> gitweb.erp-flowers.ru Git - erp24_rep/yii-erp24/.git/commitdiff
Детальный отчет по магазинам
authorVladimir Fomichev <vladimir.fomichev@erp-flowers.ru>
Wed, 5 Nov 2025 15:04:20 +0000 (18:04 +0300)
committerVladimir Fomichev <vladimir.fomichev@erp-flowers.ru>
Wed, 5 Nov 2025 15:04:20 +0000 (18:04 +0300)
erp24/actions/dashboard/SalesDetailAction.php [new file with mode: 0644]
erp24/controllers/DashboardController.php
erp24/views/dashboard/sales-detail.php [new file with mode: 0644]
erp24/views/dashboard/sales.php

diff --git a/erp24/actions/dashboard/SalesDetailAction.php b/erp24/actions/dashboard/SalesDetailAction.php
new file mode 100644 (file)
index 0000000..118e034
--- /dev/null
@@ -0,0 +1,214 @@
+<?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\helpers\DateHelper;
+use yii_app\records\CityStore;
+use yii_app\records\ExportImportTable;
+use yii_app\records\Products1c;
+use yii_app\records\Sales;
+use yii_app\services\ExportImportService;
+
+/**
+ * Детальный просмотр продаж по магазинам с записями из таблицы sales
+ * Показывает отдельные чеки для офлайн и доставки
+ * @package yii_app\actions\dashboard
+ */
+class SalesDetailAction 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();
+
+        // Получение продаж без доставки (офлайн)
+        $salesOffline = $this->getSalesDetailRecords($date1, $date2, false);
+        
+        // Получение продаж с доставкой
+        $salesDelivery = $this->getSalesDetailRecords($date1, $date2, true);
+
+        // Группировка по магазинам
+        $salesOfflineByStore = $this->groupSalesByStore($salesOffline);
+        $salesDeliveryByStore = $this->groupSalesByStore($salesDelivery);
+
+        $params = [
+            'date1' => $date1,
+            'date2' => $date2,
+            'daysSearchForm' => $daysSearchForm,
+            'cityStores' => $cityStores,
+            'city_stores' => $city_stores,
+            'export_revers' => $export_revers,
+            'salesOfflineByStore' => $salesOfflineByStore,
+            'salesDeliveryByStore' => $salesDeliveryByStore,
+        ];
+
+        return $this->controller->render('/dashboard/sales-detail.php', $params);
+    }
+
+    /**
+     * Получить детальные записи о продажах
+     *
+     * @param string $dateFrom
+     * @param string $dateTo
+     * @param bool $withDelivery - true для доставки, false для офлайн
+     * @return array
+     * @throws \Exception
+     */
+    private function getSalesDetailRecords(string $dateFrom, string $dateTo, bool $withDelivery = false): array
+    {
+        $query = Sales::find()
+            ->alias('s')
+            ->select([
+                's.id',
+                's.date',
+                's.summ',
+                's.skidka',
+                's.operation',
+                's.order_id',
+                's.store_id_1c',
+                's.store_id',
+                's.number',
+            ])
+            ->joinWith('saleCheck')
+            ->andWhere(['>=', 's.date', DateHelper::getDateTimeStartDay($dateFrom, true)])
+            ->andWhere(['<=', 's.date', DateHelper::getDateTimeEndDay($dateTo, true)])
+            ->orderBy(['s.date' => SORT_DESC, 's.id' => SORT_DESC]);
+
+        if (!$withDelivery) {
+            // Офлайн продажи - без заказов
+            $query->andWhere([
+                'and',
+                ['s.order_id' => ['', '0']],
+                [
+                    'or',
+                    'sc.order_id IS NULL',
+                    ['sc.order_id' => ['', '0']]
+                ]
+            ]);
+        } else {
+            // Продажи с доставкой - с заказами
+            $query->leftJoin('create_checks cc', 'CAST(cc.order_id AS TEXT) = s.order_id')
+                ->andFilterWhere(['or',
+                    ['not in', 's.order_id', ['', '0']],
+                    ['not in', 'sc.order_id', ['', '0']]
+                ])
+                ->andWhere(['or',
+                    ['cc.date' => null],
+                    ['DATE(cc.date)' => new \yii\db\Expression('DATE(s.date)')]
+                ]);
+        }
+
+        return $query->asArray()->all();
+    }
+
+    /**
+     * Группировка продаж по магазинам
+     *
+     * @param array $sales
+     * @return array
+     */
+    private function groupSalesByStore(array $sales): array
+    {
+        $grouped = [];
+
+        foreach ($sales as $sale) {
+            $storeId1c = $sale['store_id_1c'];
+            
+            if (!isset($grouped[$storeId1c])) {
+                $grouped[$storeId1c] = [
+                    'store_id_1c' => $storeId1c,
+                    'sales' => [],
+                    'total_summ' => 0,
+                    'total_skidka' => 0,
+                ];
+            }
+
+            $saleAmount = $sale['summ'] - $sale['skidka'];
+            
+            // Применяем операцию (продажа или возврат)
+            if ($sale['operation'] === Sales::OPERATION_SALE) {
+                $grouped[$storeId1c]['total_summ'] += $saleAmount;
+            } elseif ($sale['operation'] === Sales::OPERATION_RETURN) {
+                $grouped[$storeId1c]['total_summ'] -= $saleAmount;
+            }
+
+            $grouped[$storeId1c]['sales'][] = $sale;
+        }
+
+        // Сортировка магазинов по сумме продаж (убывание)
+        uasort($grouped, function ($a, $b) {
+            return $b['total_summ'] <=> $a['total_summ'];
+        });
+
+        return $grouped;
+    }
+}
+
index bb880b04ebb6b048bce10d20a89ac3912087e472..052c9e658639592bcc6a00c7f2d676f7d65b303b 100755 (executable)
@@ -12,6 +12,7 @@ class DashboardController extends \yii\web\Controller
         return [
             'index' => \yii_app\actions\dashboard\IndexAction::class,
             'sales' => \yii_app\actions\dashboard\SalesAction::class,
+            'sales-detail' => \yii_app\actions\dashboard\SalesDetailAction::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,
diff --git a/erp24/views/dashboard/sales-detail.php b/erp24/views/dashboard/sales-detail.php
new file mode 100644 (file)
index 0000000..2c74689
--- /dev/null
@@ -0,0 +1,278 @@
+<?php
+/**
+ * @var $date1
+ * @var $date2
+ * @var $daysSearchForm
+ * @var $cityStores
+ * @var $city_stores
+ * @var $export_revers
+ * @var $salesOfflineByStore
+ * @var $salesDeliveryByStore
+ * @var \yii\web\View $this
+ */
+
+use yii\helpers\Html;
+use yii\widgets\ActiveForm;
+?>
+
+<h2>Детальный просмотр продаж</h2>
+
+<?php $searchForm = ActiveForm::begin([
+    'id' => 'days-search-form-detail',
+    '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="sales-detail-container">
+    <?php if (!empty($salesOfflineByStore) || !empty($salesDeliveryByStore)): ?>
+
+        <!-- ПРОДАЖИ БЕЗ ДОСТАВКИ (ОФЛАЙН) -->
+        <div class="mb-5">
+            <h3 class="text-primary">
+                <i class="fas fa-store"></i> Офлайн продажи (без доставки) за <?= $date1 ?>
+            </h3>
+
+            <?php if (!empty($salesOfflineByStore)): ?>
+                <div class="accordion" id="accordionOffline">
+                    <?php foreach ($salesOfflineByStore as $storeId1c => $storeData): ?>
+                        <?php
+                            $storeNameExport = $city_stores[$storeId1c] ?? 'Неизвестный магазин';
+                            $storeName = $storeNameExport;
+                            $totalSumm = $storeData['total_summ'];
+                            $salesCount = count($storeData['sales']);
+                        ?>
+                        <div class="accordion-item">
+                            <h2 class="accordion-header" id="headingOffline<?= htmlspecialchars($storeId1c) ?>">
+                                <button class="accordion-button collapsed" type="button" data-bs-toggle="collapse"
+                                        data-bs-target="#collapseOffline<?= htmlspecialchars($storeId1c) ?>"
+                                        aria-expanded="false"
+                                        aria-controls="collapseOffline<?= htmlspecialchars($storeId1c) ?>">
+                                    <strong><?= Html::encode($storeName) ?></strong>
+                                    <span class="badge bg-info ms-2"><?= $salesCount ?> чеков</span>
+                                    <span class="badge bg-success ms-2"><?= number_format($totalSumm, 2, '.', ' ') ?> ₽</span>
+                                </button>
+                            </h2>
+                            <div id="collapseOffline<?= htmlspecialchars($storeId1c) ?>" class="accordion-collapse collapse"
+                                 aria-labelledby="headingOffline<?= htmlspecialchars($storeId1c) ?>"
+                                 data-bs-parent="#accordionOffline">
+                                <div class="accordion-body p-0">
+                                    <table class="table table-sm table-hover mb-0">
+                                        <thead class="table-light">
+                                        <tr>
+                                            <th style="width: 60px;">ID</th>
+                                            <th style="width: 180px;">Дата и время</th>
+                                            <th style="width: 100px;">Сумма</th>
+                                            <th style="width: 80px;">Операция</th>
+                                            <th style="width: 100px;">Номер</th>
+                                            <th style="width: 80px;">Order ID</th>
+                                        </tr>
+                                        </thead>
+                                        <tbody>
+                                        <?php foreach ($storeData['sales'] as $sale): ?>
+                                            <?php
+                                                $saleAmount = $sale['summ'] - $sale['skidka'];
+                                                $operationClass = $sale['operation'] === 'Продажа' ? 'text-success' : 'text-danger';
+                                                $operationIcon = $sale['operation'] === 'Продажа' ? '✓' : '✕';
+                                                $dateTime = date('d.m.Y H:i:s', strtotime($sale['date']));
+                                            ?>
+                                            <tr>
+                                                <td><small class="text-muted"><?= $sale['id'] ?></small></td>
+                                                <td><small><?= $dateTime ?></small></td>
+                                                <td>
+                                                    <strong class="<?= $operationClass ?>">
+                                                        <?= number_format($saleAmount, 2, '.', ' ') ?>
+                                                    </strong>
+                                                </td>
+                                                <td>
+                                                    <span class="badge <?= $sale['operation'] === 'Продажа' ? 'bg-success' : 'bg-danger' ?>">
+                                                        <?= Html::encode($sale['operation']) ?>
+                                                    </span>
+                                                </td>
+                                                <td><small><?= Html::encode($sale['number']) ?></small></td>
+                                                <td>
+                                                    <?php if (!empty($sale['order_id']) && $sale['order_id'] !== '0'): ?>
+                                                        <small class="badge bg-warning">
+                                                            <?= Html::encode($sale['order_id']) ?>
+                                                        </small>
+                                                    <?php else: ?>
+                                                        <small class="text-muted">—</small>
+                                                    <?php endif; ?>
+                                                </td>
+                                            </tr>
+                                        <?php endforeach; ?>
+                                        </tbody>
+                                    </table>
+                                </div>
+                            </div>
+                        </div>
+                    <?php endforeach; ?>
+                </div>
+            <?php else: ?>
+                <div class="alert alert-info">Нет данных об офлайн продажах за выбранный период</div>
+            <?php endif; ?>
+        </div>
+
+        <!-- ПРОДАЖИ С ДОСТАВКОЙ -->
+        <div class="mb-5">
+            <h3 class="text-info">
+                <i class="fas fa-truck"></i> Продажи с доставкой за <?= $date1 ?>
+            </h3>
+
+            <?php if (!empty($salesDeliveryByStore)): ?>
+                <div class="accordion" id="accordionDelivery">
+                    <?php foreach ($salesDeliveryByStore as $storeId1c => $storeData): ?>
+                        <?php
+                            $storeNameExport = $city_stores[$storeId1c] ?? 'Неизвестный магазин';
+                            $storeName = $storeNameExport;
+                            $totalSumm = $storeData['total_summ'];
+                            $salesCount = count($storeData['sales']);
+                        ?>
+                        <div class="accordion-item">
+                            <h2 class="accordion-header" id="headingDelivery<?= htmlspecialchars($storeId1c) ?>">
+                                <button class="accordion-button collapsed" type="button" data-bs-toggle="collapse"
+                                        data-bs-target="#collapseDelivery<?= htmlspecialchars($storeId1c) ?>"
+                                        aria-expanded="false"
+                                        aria-controls="collapseDelivery<?= htmlspecialchars($storeId1c) ?>">
+                                    <strong><?= Html::encode($storeName) ?></strong>
+                                    <span class="badge bg-info ms-2"><?= $salesCount ?> заказов</span>
+                                    <span class="badge bg-success ms-2"><?= number_format($totalSumm, 2, '.', ' ') ?> ₽</span>
+                                </button>
+                            </h2>
+                            <div id="collapseDelivery<?= htmlspecialchars($storeId1c) ?>" class="accordion-collapse collapse"
+                                 aria-labelledby="headingDelivery<?= htmlspecialchars($storeId1c) ?>"
+                                 data-bs-parent="#accordionDelivery">
+                                <div class="accordion-body p-0">
+                                    <table class="table table-sm table-hover mb-0">
+                                        <thead class="table-light">
+                                        <tr>
+                                            <th style="width: 60px;">ID</th>
+                                            <th style="width: 180px;">Дата и время</th>
+                                            <th style="width: 100px;">Сумма</th>
+                                            <th style="width: 80px;">Операция</th>
+                                            <th style="width: 100px;">Номер</th>
+                                            <th style="width: 150px;">Order ID</th>
+                                        </tr>
+                                        </thead>
+                                        <tbody>
+                                        <?php foreach ($storeData['sales'] as $sale): ?>
+                                            <?php
+                                                $saleAmount = $sale['summ'] - $sale['skidka'];
+                                                $operationClass = $sale['operation'] === 'Продажа' ? 'text-success' : 'text-danger';
+                                                $dateTime = date('d.m.Y H:i:s', strtotime($sale['date']));
+                                            ?>
+                                            <tr>
+                                                <td><small class="text-muted"><?= $sale['id'] ?></small></td>
+                                                <td><small><?= $dateTime ?></small></td>
+                                                <td>
+                                                    <strong class="<?= $operationClass ?>">
+                                                        <?= number_format($saleAmount, 2, '.', ' ') ?>
+                                                    </strong>
+                                                </td>
+                                                <td>
+                                                    <span class="badge <?= $sale['operation'] === 'Продажа' ? 'bg-success' : 'bg-danger' ?>">
+                                                        <?= Html::encode($sale['operation']) ?>
+                                                    </span>
+                                                </td>
+                                                <td><small><?= Html::encode($sale['number']) ?></small></td>
+                                                <td>
+                                                    <?php if (!empty($sale['order_id']) && $sale['order_id'] !== '0'): ?>
+                                                        <small class="badge bg-warning text-dark">
+                                                            <?= Html::encode($sale['order_id']) ?>
+                                                        </small>
+                                                    <?php else: ?>
+                                                        <small class="text-muted">—</small>
+                                                    <?php endif; ?>
+                                                </td>
+                                            </tr>
+                                        <?php endforeach; ?>
+                                        </tbody>
+                                    </table>
+                                </div>
+                            </div>
+                        </div>
+                    <?php endforeach; ?>
+                </div>
+            <?php else: ?>
+                <div class="alert alert-info">Нет данных о доставке за выбранный период</div>
+            <?php endif; ?>
+        </div>
+
+    <?php else: ?>
+        <div class="alert alert-warning">
+            <strong>Нет данных</strong> за выбранный период <?= $date1 ?> - <?= $date2 ?>
+        </div>
+    <?php endif; ?>
+</div>
+
+<?php
+// CSS для оформления
+$this->registerCss('
+.sales-detail-container {
+    padding: 15px;
+}
+
+.accordion-button {
+    padding: 12px 15px;
+    font-size: 14px;
+}
+
+.accordion-button:not(.collapsed) {
+    background-color: #f8f9fa;
+    color: #000;
+}
+
+.table-hover tbody tr:hover {
+    background-color: #f5f5f5;
+}
+
+.badge {
+    margin-left: 5px;
+    padding: 5px 8px;
+}
+
+.text-muted {
+    color: #999;
+}
+
+.text-success {
+    color: #28a745;
+}
+
+.text-danger {
+    color: #dc3545;
+}
+');
+?>
+
index 0f7308052e56f6a4ac4a50a3b8e7181574e094b0..5633224213b6daede1c04ecd66c7d4def764c4d3 100755 (executable)
@@ -54,7 +54,12 @@ tr.line.bg-danger>td>a.btn {
 ');
 
 ?>
-<h2>Продажи</h2>
+<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>
 
 <?php $searchForm = \yii\widgets\ActiveForm::begin([
         'id' => 'days-search-form',