From ae4b43afcddbc802a64d53600fe0cec6360e71d7 Mon Sep 17 00:00:00 2001 From: Vladimir Fomichev Date: Thu, 6 Nov 2025 13:12:37 +0300 Subject: [PATCH] =?utf8?q?=D0=94=D0=B5=D1=82=D0=B0=D0=BB=D1=8C=D0=BD=D1=8B?= =?utf8?q?=D0=B9=20=D0=BE=D1=82=D1=87=D0=B5=D1=82=20=D0=BF=D0=BE=20=D0=9C?= =?utf8?q?=D0=9F?= MIME-Version: 1.0 Content-Type: text/plain; charset=utf8 Content-Transfer-Encoding: 8bit --- .../MarketplaceSalesReportAction.php | 169 +++++++ erp24/controllers/DashboardController.php | 1 + erp24/records/MarketplaceOrderStatusTypes.php | 2 + .../MarketplaceSalesMatchingService.php | 471 ++++++++++++++++++ .../dashboard/marketplace-sales-report.php | 411 +++++++++++++++ erp24/views/dashboard/sales.php | 11 +- 6 files changed, 1062 insertions(+), 3 deletions(-) create mode 100644 erp24/actions/dashboard/MarketplaceSalesReportAction.php create mode 100644 erp24/services/MarketplaceSalesMatchingService.php create mode 100644 erp24/views/dashboard/marketplace-sales-report.php diff --git a/erp24/actions/dashboard/MarketplaceSalesReportAction.php b/erp24/actions/dashboard/MarketplaceSalesReportAction.php new file mode 100644 index 00000000..78fb9547 --- /dev/null +++ b/erp24/actions/dashboard/MarketplaceSalesReportAction.php @@ -0,0 +1,169 @@ +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; + } +} + diff --git a/erp24/controllers/DashboardController.php b/erp24/controllers/DashboardController.php index 052c9e65..8db9b464 100755 --- a/erp24/controllers/DashboardController.php +++ b/erp24/controllers/DashboardController.php @@ -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, diff --git a/erp24/records/MarketplaceOrderStatusTypes.php b/erp24/records/MarketplaceOrderStatusTypes.php index 3475db92..1b3c0bc9 100644 --- a/erp24/records/MarketplaceOrderStatusTypes.php +++ b/erp24/records/MarketplaceOrderStatusTypes.php @@ -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 index 00000000..3b3ac07f --- /dev/null +++ b/erp24/services/MarketplaceSalesMatchingService.php @@ -0,0 +1,471 @@ + 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 index 00000000..97c257e9 --- /dev/null +++ b/erp24/views/dashboard/marketplace-sales-report.php @@ -0,0 +1,411 @@ + + +

Отчет по заказам маркетплейсов и чекам продаж

+ + 'days-search-form-marketplace', + 'enableAjaxValidation' => true, + 'validationUrl' => 'validate', + 'options' => ['enctype' => 'multipart/form-data'] +]); ?> + +
+
+ field($daysSearchForm, 'dateFrom', [ + 'inputOptions' => [ + 'class' => 'form-control datetime', + 'type' => 'date', + 'placeholder' => 'начало', + ], + 'options' => ['tag' => null], + ])->label(false)->textInput() ?> +
+
+ field($daysSearchForm, 'dateTo', [ + 'inputOptions' => [ + 'class' => 'form-control datetime', + 'type' => 'date', + 'placeholder' => 'конец', + ], + 'options' => ['tag' => null], + ])->label(false)->textInput() ?> +
+
+
+ + 'btn btn-success']) ?> +
+
+
+ + + +
+ + +
+
+ Период: - + Всего магазинов: +
+
+ + +
+ $storeData): ?> + 0 ? round(($totalMatched / $totalOrders) * 100) : 0; + ?> +
+

+ +

+
+
+ + +
+ $orderData): ?> + 'Flowwow', + 2 => 'Яндекс Маркет', + ]; + $marketplaceName = $marketplaceNames[$order->marketplace_id] ?? 'Неизвестный МП'; + $creationDate = date('d.m.Y H:i:s', strtotime($order->creation_date)); + $hasMatches = !empty($matchedSales); + ?> +
+

+ +

+
+
+ + +
+
Информация о заказе МП
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
ID заказа:marketplace_order_id) ?>
Дата создания:
Дата обновления:updated_at)) ?>
Маркетплейс:
Сумма заказа:total, 2, '.', ' ') ?> ₽
Статус: + + + + + +
Суб-статус: + + + +
+
+ + +
+
Товары в заказе ()
+ + + + + + + + + + + + + + + + + + + + + +
НаименованиеКол-воЦенаСуммаАртикул
+ + + +
+ () + +
+
₽ ₽
+
+ + + +
+
Найденные чеки продаж ()
+ + $saleMatch): ?> + date)); + ?> +
+
+ Чек #number) ?> + + + Совпадение: % + Оценка: + +
+
+ + + + + + + + + + + + + + + + + +
ID чека:id) ?>
Дата чека:
Сумма чека:summ - $sale->skidka, 2, '.', ' ') ?> ₽
Разница в сумме: + + ₽ + +
+ +
Товары в чеке ():
+ + + + + + + + + + + + + + + + + + + + + +
НаименованиеКол-воЦенаСуммаСкидка
₽ ₽ ₽
+ + +
Анализ совпадения товаров:
+ + + + + + + + + + + $matchDetail): ?> + + + + + + + + +
ТоварВ заказеВ чекеСтатус
+ + ✓ Совпадает + + ⚠ Отличие + +
+ +
+
+ +
+ + + + +
+
+
+ +
+ +
+
+
+ +
+ + +
+ Нет данных за выбранный период - +
+ +
+ +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; +} +'); +?> + diff --git a/erp24/views/dashboard/sales.php b/erp24/views/dashboard/sales.php index 56332242..7c992bd3 100755 --- a/erp24/views/dashboard/sales.php +++ b/erp24/views/dashboard/sales.php @@ -56,9 +56,14 @@ tr.line.bg-danger>td>a.btn { ?>

Продажи

- - Детальный просмотр - +
+ + Детальный просмотр + + + Отчет по МП + +