From a8abc33ba71bf361a414d623c9a3ad8680d86e03 Mon Sep 17 00:00:00 2001 From: Vladimir Fomichev Date: Thu, 2 Oct 2025 17:42:55 +0300 Subject: [PATCH] =?utf8?q?=D0=A2=D0=B5=D1=81=D1=82=D0=B8=D1=80=D0=BE=D0=B2?= =?utf8?q?=D0=B0=D0=BD=D0=B8=D0=B5=20=D1=80=D0=B0=D0=B7=D0=B4=D0=B5=D0=BB?= =?utf8?q?=D0=B8=D1=82=D0=B5=D0=BB=D1=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=utf8 Content-Transfer-Encoding: 8bit --- .../MarketplaceOrdersController.php | 54 ++- erp24/services/MarketplaceService.php | 409 +++++++++++------- .../test-article-processing.php | 158 +++++++ .../marketplace-orders/test-order-parsing.php | 226 ++++++++++ 4 files changed, 701 insertions(+), 146 deletions(-) create mode 100644 erp24/views/marketplace-orders/test-article-processing.php create mode 100644 erp24/views/marketplace-orders/test-order-parsing.php diff --git a/erp24/controllers/MarketplaceOrdersController.php b/erp24/controllers/MarketplaceOrdersController.php index e1981ab7..3bbf07f8 100644 --- a/erp24/controllers/MarketplaceOrdersController.php +++ b/erp24/controllers/MarketplaceOrdersController.php @@ -3,6 +3,7 @@ namespace app\controllers; use app\records\OrdersUnion; +use yii\base\DynamicModel; use OpenAPI\Client\Configuration; use OpenAPI\Client\ObjectSerializer; @@ -578,7 +579,7 @@ class MarketplaceOrdersController extends Controller $data = json_decode($order->raw_data, true); if ($order->marketplace_id == 1 && isset($data['delivery'])) { $deliveryText = (explode(',', $data['delivery']))[1] ?? ''; - $deliveryText = trim(str_replace('Нижний Новгород', '', $deliveryText)); + $deliveryText = trim(str_replace('Нижний Новгород', '', $deliveryText ?? '')); $months = [ 'января' => '01', 'февраля' => '02', 'марта' => '03', @@ -974,4 +975,55 @@ class MarketplaceOrdersController extends Controller return $this->redirect(['view', 'id' => $item->order_id]); } } + + public function actionTestOrderParsing() + { + $parsedOrder = null; + $htmlBody = ''; + $parsingError = null; + + $model = new DynamicModel(['html_body']); + $model->addRule(['html_body'], 'required', ['message' => 'HTML тело письма не может быть пустым']); + $model->addRule(['html_body'], 'string', ['min' => 1, 'message' => 'HTML тело письма содержит только пробелы или невидимые символы']); + + if ($model->load(Yii::$app->request->post()) && $model->validate()) { + $htmlBody = $model->html_body; + try { + $message = [ + 'body' => $htmlBody, + 'date' => date('Y-m-d H:i:s'), + 'to' => 'test@example.com' + ]; + + $parsedOrder = MarketplaceService::getOrdersDataFromMessage($message); + + // Если результат null или пустой массив, это не ошибка разбора, + // а просто отсутствие заказов в HTML + if ($parsedOrder === null) { + $parsingError = 'Не удалось разобрать заказы из HTML. Проверьте корректность HTML структуры.'; + } + } catch (\Exception $e) { + $parsingError = 'Ошибка при разборе: ' . $e->getMessage(); + } + } elseif (Yii::$app->request->isPost) { + // Если POST-запрос был, но валидация не прошла + $parsingError = implode('
', $model->getFirstErrors()); + } + + return $this->render('test-order-parsing', [ + 'model' => $model, + 'parsedOrder' => $parsedOrder, + 'htmlBody' => $htmlBody, + 'parsingError' => $parsingError, + ]); + } + + public function actionTestArticleProcessing() + { + $testResults = MarketplaceService::testArticleProcessing(); + + return $this->render('test-article-processing', [ + 'testResults' => $testResults, + ]); + } } diff --git a/erp24/services/MarketplaceService.php b/erp24/services/MarketplaceService.php index 0781b17c..2a0bd24b 100644 --- a/erp24/services/MarketplaceService.php +++ b/erp24/services/MarketplaceService.php @@ -684,6 +684,78 @@ class MarketplaceService return $name; } + /** + * Тестовый метод для проверки обработки артикулов + */ + public static function testArticleProcessing() + { + $testCases = [ + // Тесты для normalizeArticleInName (убираем дефис) + [ + 'input' => 'Букет из 11 розовых пионов (FW-0076)', + 'expected' => 'Букет из 11 розовых пионов (FW0076)', + 'method' => 'normalizeArticleInName' + ], + [ + 'input' => 'Букет из 11 розовых пионов (FW0076)', + 'expected' => 'Букет из 11 розовых пионов (FW0076)', + 'method' => 'normalizeArticleInName' + ], + [ + 'input' => 'Букет из 11 розовых пионов', + 'expected' => 'Букет из 11 розовых пионов', + 'method' => 'normalizeArticleInName' + ], + [ + 'input' => 'Букет из 11 розовых пионов (ABC-123)', + 'expected' => 'Букет из 11 розовых пионов (ABC123)', + 'method' => 'normalizeArticleInName' + ], + + // Тесты для processDisplayName (добавляем дефис) + [ + 'input' => 'Букет из 11 розовых пионов (FW0076)', + 'expected' => 'Букет из 11 розовых пионов (FW-0076)', + 'method' => 'processDisplayName' + ], + [ + 'input' => 'Букет из 11 розовых пионов (FW-0076)', + 'expected' => 'Букет из 11 розовых пионов (FW-0076)', + 'method' => 'processDisplayName' + ], + [ + 'input' => 'Букет из 11 розовых пионов', + 'expected' => 'Букет из 11 розовых пионов', + 'method' => 'processDisplayName' + ], + [ + 'input' => 'Букет из 11 розовых пионов (ABC123)', + 'expected' => 'Букет из 11 розовых пионов (ABC-123)', + 'method' => 'processDisplayName' + ], + ]; + + $results = []; + foreach ($testCases as $testCase) { + $method = $testCase['method']; + $input = $testCase['input']; + $expected = $testCase['expected']; + + $actual = self::$method($input); + $passed = $actual === $expected; + + $results[] = [ + 'method' => $method, + 'input' => $input, + 'expected' => $expected, + 'actual' => $actual, + 'passed' => $passed + ]; + } + + return $results; + } + public static function getProductImageUrl($imageId) { $image = Images::findOne($imageId); @@ -1919,185 +1991,232 @@ class MarketplaceService $html = $message['body']; $orderDetails = null; $order = null; - if (!empty($html)) { - $html = preg_replace('/\s+/', ' ', $html); - // Декодируем HTML-сущности - $html = html_entity_decode($html, ENT_COMPAT, 'UTF-8'); - - $doc = new HtmlDomParser($html); - $orderNumber = ''; - $main = $doc->findOneOrFalse("body"); - - if ($main !== false) { - $orderTitleNode = $main->findOne("h1"); - if ($orderTitleNode && preg_match('/№(\d+)/', $orderTitleNode->innertext, $matches)) { - $orderNumber = (int)$matches[1]; - } - } - $orderDetails['number'] = $orderNumber; - $orderDetails['date'] = $message['date']; - $deliveryText = ''; - $clientText = ''; - $orderItems = []; - $totalSum = 0; - $deliverySum = 0; - - $linkBlock = $main->findOneOrFalse('a:contains("Перейти в заказ ")'); - if ($linkBlock) { - $link = $linkBlock->getAttribute('href'); - $orderDetails['orderLink'] = $link; - } + // Детальная проверка HTML + if (empty($html)) { + Yii::error('HTML тело письма пустое', __METHOD__); + return []; + } - $deliveryLabel = $main->findOne('p:contains("Доставить")'); - $pickupLabel = $main->findOne('p:contains("Самовывоз")'); + if (empty(trim($html))) { + Yii::error('HTML тело письма содержит только пробелы', __METHOD__); + return []; + } - $targetLabel = $deliveryLabel ?? $pickupLabel; - $labelPrefix = $deliveryLabel ? 'Доставка:' : ($pickupLabel ? 'Самовывоз:' : null); + // Очищаем от лишних пробелов, но сохраняем структуру + $html = preg_replace('/\s+/', ' ', $html); + $html = trim($html); - if ($targetLabel && $targetLabel->nextNonWhitespaceSibling()) { - $infoBlock = $targetLabel->nextNonWhitespaceSibling(); - $textParts = []; + if (empty($html)) { + Yii::error('HTML тело письма стало пустым после очистки', __METHOD__); + return []; + } - $lines = explode('
', $infoBlock->innerHtml()); + // Декодируем HTML-сущности + $html = html_entity_decode($html, ENT_COMPAT, 'UTF-8'); - foreach ($lines as $line) { - $clean = trim(strip_tags($line)); - if ($clean !== '') { - $textParts[] = $clean; - } - } + // Проверяем наличие основных HTML тегов + if (stripos($html, 'findOneOrFalse("body"); - if (isset($textParts[1])) { - $deliveryText .= ' ' . $textParts[1]; - } + // Если не найден body, пробуем найти по другим селекторам + if ($main === false) { + Yii::warning('Тег body не найден, пробуем найти контент по другим селекторам', __METHOD__); - if (isset($textParts[2])) { - $deliveryText .= ', ' . $textParts[2]; - } - } - } + // Пробуем найти div или другой контейнер с контентом + $main = $doc->findOneOrFalse("div") ?: $doc->findOneOrFalse("*"); - if (!empty($deliveryText)) { - $deliveryText = preg_replace('/\s+/', ' ', $deliveryText); - $deliveryText = trim($deliveryText); - $orderDetails['delivery'] = $deliveryText; + if ($main === false) { + Yii::error('Не удалось найти подходящий контейнер для парсинга HTML', __METHOD__); + return []; } + } + + $orderTitleNode = $main->findOne("h1"); + if ($orderTitleNode && preg_match('/№(\d+)/', $orderTitleNode->innertext, $matches)) { + $orderNumber = (int)$matches[1]; + } elseif (!$orderTitleNode) { + Yii::warning('Не найден заголовок заказа (h1) в HTML', __METHOD__); + } + + $orderDetails['number'] = $orderNumber; + $orderDetails['date'] = $message['date']; + $deliveryText = ''; + $clientText = ''; + $orderItems = []; + $totalSum = 0; + $deliverySum = 0; + + $linkBlock = $main->findOneOrFalse('a:contains("Перейти в заказ ")'); + if ($linkBlock) { + $link = $linkBlock->getAttribute('href'); + $orderDetails['orderLink'] = $link; + } + + $deliveryLabel = $main->findOne('p:contains("Доставить")'); + $pickupLabel = $main->findOne('p:contains("Самовывоз")'); - $commentBlock = $main->findOne('p:contains("Комментарий")'); - if ($commentBlock && $commentBlock->nextNonWhitespaceSibling()) { - $commentBlock = $commentBlock->nextNonWhitespaceSibling(); - $commentText = preg_replace('/\s+/', ' ', $commentBlock->innerText()); - $orderDetails['comment'] = trim($commentText); + $targetLabel = $deliveryLabel ?? $pickupLabel; + $labelPrefix = $deliveryLabel ? 'Доставка:' : ($pickupLabel ? 'Самовывоз:' : null); + + if ($targetLabel && $targetLabel->nextNonWhitespaceSibling()) { + $infoBlock = $targetLabel->nextNonWhitespaceSibling(); + $textParts = []; + + $lines = explode('
', $infoBlock->innerHtml()); + + foreach ($lines as $line) { + $clean = trim(strip_tags($line)); + if ($clean !== '') { + $textParts[] = $clean; + } } - $clientBlock = $main->findOne('p:contains("Клиент")'); - $senderBlock = $main->findOne('p:contains("Отправитель")'); + if (!empty($textParts)) { + $deliveryText = $labelPrefix . ' ' . $textParts[0]; - if ($clientBlock && $clientBlock->nextNonWhitespaceSibling()) { - $clientBlock = $clientBlock->nextNonWhitespaceSibling(); - $clientText = "Клиент: " . strip_tags($clientBlock->innerText()); - $phoneLink = $clientBlock->find('a', 0); - if ($phoneLink) { - $clientText .= ' ' . preg_replace('/tel:/', ' ', $phoneLink->getAttribute('href')); + if (isset($textParts[1])) { + $deliveryText .= ' ' . $textParts[1]; } - } elseif ($senderBlock && $senderBlock->nextNonWhitespaceSibling()) { - $senderBlock = $senderBlock->nextNonWhitespaceSibling(); - $clientText = "Отправитель: " . strip_tags($senderBlock->innerText()); - $phoneLink = $senderBlock->find('a', 0); - if ($phoneLink) { - $clientText .= ' ' . preg_replace('/tel:/', ' ', $phoneLink->getAttribute('href')); + + if (isset($textParts[2])) { + $deliveryText .= ', ' . $textParts[2]; } } + } - if ($clientText) { - $orderDetails['client'] = str_replace('Позвонить', '', $clientText); - } + if (!empty($deliveryText)) { + $deliveryText = preg_replace('/\s+/', ' ', $deliveryText); + $deliveryText = trim($deliveryText); + $orderDetails['delivery'] = $deliveryText; + } - $recipientBlock = $main->findOne('p:contains("Получатель")'); - if ($recipientBlock && $recipientBlock->nextNonWhitespaceSibling()) { - $recipientBlock = $recipientBlock->nextNonWhitespaceSibling(); - $recipientText = strip_tags( - str_replace('Позвонить', '', $recipientBlock->innerText()) - ) . ' ' . preg_replace('/tel:/', ' ', $recipientBlock->find('a', 0)->getAttribute('href')); - $orderDetails['recipient'] = $recipientText; - } + $commentBlock = $main->findOne('p:contains("Комментарий")'); + if ($commentBlock && $commentBlock->nextNonWhitespaceSibling()) { + $commentBlock = $commentBlock->nextNonWhitespaceSibling(); + $commentText = preg_replace('/\s+/', ' ', $commentBlock->innerText()); + $orderDetails['comment'] = trim($commentText); + } + + $clientBlock = $main->findOne('p:contains("Клиент")'); + $senderBlock = $main->findOne('p:contains("Отправитель")'); - $itemsBlock = false; - if ($main->findOneOrFalse('table h2:contains("Детали заказа")') != false) { - $itemsBlock = $main->findOneOrFalse('table h2:contains("Детали заказа")'); - } elseif ($main->findOneOrFalse('table p:contains("Детали заказа")') != false) { - $itemsBlock = $main->findOneOrFalse('table p:contains("Детали заказа")'); + if ($clientBlock && $clientBlock->nextNonWhitespaceSibling()) { + $clientBlock = $clientBlock->nextNonWhitespaceSibling(); + $clientText = "Клиент: " . strip_tags($clientBlock->innerText()); + $phoneLink = $clientBlock->find('a', 0); + if ($phoneLink) { + $clientText .= ' ' . preg_replace('/tel:/', ' ', $phoneLink->getAttribute('href')); } + } elseif ($senderBlock && $senderBlock->nextNonWhitespaceSibling()) { + $senderBlock = $senderBlock->nextNonWhitespaceSibling(); + $clientText = "Отправитель: " . strip_tags($senderBlock->innerText()); + $phoneLink = $senderBlock->find('a', 0); + if ($phoneLink) { + $clientText .= ' ' . preg_replace('/tel:/', ' ', $phoneLink->getAttribute('href')); + } + } - if ($itemsBlock) { - $itemsTable = $itemsBlock->parentNode(); + if ($clientText) { + $orderDetails['client'] = str_replace('Позвонить', '', $clientText); + } - $itemsRows = $itemsTable->find('tr'); - foreach ($itemsRows as $itemsRow) { - $itemData = [ - 'name' => '', - 'count' => '', - 'price' => '', - ]; + $recipientBlock = $main->findOne('p:contains("Получатель")'); + if ($recipientBlock && $recipientBlock->nextNonWhitespaceSibling()) { + $recipientBlock = $recipientBlock->nextNonWhitespaceSibling(); + $recipientText = strip_tags( + str_replace('Позвонить', '', $recipientBlock->innerText()) + ) . ' ' . preg_replace('/tel:/', ' ', $recipientBlock->find('a', 0)->getAttribute('href')); + $orderDetails['recipient'] = $recipientText; + } - // Извлекаем название и количество из второго - $tds = $itemsRow->find('td'); - if (count($tds) >= 2) { - $rawName = trim(str_replace("\u{00A0}", ' ', strip_tags(preg_replace('/\s+/', ' ', $tds[1]->find('p', 0)->innerText())))); - $itemData['name'] = self::normalizeArticleInName($rawName); - $itemData['count'] = trim(str_replace(["\u{00A0}", 'шт.'], '', strip_tags(preg_replace('/\s+/', '', $tds[1]->find('p', 1)->innerText())))); - } - // Извлекаем цену из третьего - if (count($tds) >= 3) { - $itemData['price'] = (float)trim(str_replace(["\u{00A0}", '₽', ' '], '', strip_tags(preg_replace('/\s+/', ' ', $tds[2]->find('p', 0)->innerText())))); - } - // Добавляем данные в массив - $orderItems[] = $itemData; - } + $itemsBlock = false; + if ($main->findOneOrFalse('table h2:contains("Детали заказа")') != false) { + $itemsBlock = $main->findOneOrFalse('table h2:contains("Детали заказа")'); + } elseif ($main->findOneOrFalse('p:contains("Детали заказа")') != false) { + $itemsBlock = $main->findOneOrFalse('p:contains("Детали заказа")'); + } + + if ($itemsBlock) { + $itemsTable = $itemsBlock->parentNode(); + + $itemsRows = $itemsTable->find('tr'); + foreach ($itemsRows as $itemsRow) { + $itemData = [ + 'name' => '', + 'count' => '', + 'price' => '', + ]; - $sumBlock = $main->findOneOrFalse('p:contains("Итого оплачено")'); + // Извлекаем название и количество из второго + $tds = $itemsRow->find('td'); + if (count($tds) >= 2) { + $rawName = trim(str_replace("\u{00A0}", ' ', strip_tags(preg_replace('/\s+/', ' ', $tds[1]->find('p', 0)->innerText())))); + $itemData['name'] = self::normalizeArticleInName($rawName); + $itemData['count'] = trim(str_replace(["\u{00A0}", 'шт.'], '', strip_tags(preg_replace('/\s+/', '', $tds[1]->find('p', 1)->innerText())))); + } + // Извлекаем цену из третьего + if (count($tds) >= 3) { + $itemData['price'] = (float)trim(str_replace(["\u{00A0}", '₽', ' '], '', strip_tags(preg_replace('/\s+/', ' ', $tds[2]->find('p', 0)->innerText())))); + } + // Добавляем данные в массив + $orderItems[] = $itemData; + } - if ($sumBlock) { - $sumBlock = $sumBlock->parentNode()->nextNonWhitespaceSibling(); - $totalSum = (float)trim - ( - str_replace( - ["\u{00A0}", '₽', ' '], - '', - strip_tags + $sumBlock = $main->findOneOrFalse('p:contains("Итого оплачено")'); + + if ($sumBlock) { + $sumBlock = $sumBlock->parentNode()->nextNonWhitespaceSibling(); + $totalSum = (float)trim + ( + str_replace( + ["\u{00A0}", '₽', ' '], + '', + strip_tags + ( + preg_replace ( - preg_replace - ( - '/\s+/', - ' ', - $sumBlock->innerText() - ) + '/\s+/', + ' ', + $sumBlock->innerText() ) ) - ); - } + ) + ); + } - $devSumBlock = $main->findOneOrFalse('p:contains("Доставка")'); + $devSumBlock = $main->findOneOrFalse('p:contains("Доставка")'); - if ($devSumBlock) { - $devSumBlock = $devSumBlock->parentNode()->nextNonWhitespaceSibling(); - $deliverySum = (float)trim( - str_replace(["\u{00A0}", '₽', ' '], - '', - strip_tags(preg_replace('/\s+/', ' ', $devSumBlock->innerText()))) - ); - } + if ($devSumBlock) { + $devSumBlock = $devSumBlock->parentNode()->nextNonWhitespaceSibling(); + $deliverySum = (float)trim( + str_replace(["\u{00A0}", '₽', ' '], + '', + strip_tags(preg_replace('/\s+/', ' ', $devSumBlock->innerText()))) + ); } - $orderDetails['items'] = $orderItems; - $orderDetails['deliverySum'] = $deliverySum; - $orderDetails['totalSum'] = $totalSum; - $order[$orderNumber] = $orderDetails; - return $order; } + $orderDetails['items'] = $orderItems; + $orderDetails['deliverySum'] = $deliverySum; + $orderDetails['totalSum'] = $totalSum; + $order[$orderNumber] = $orderDetails; + + if (empty($order)) { + Yii::warning('Не удалось извлечь данные заказа из HTML', __METHOD__); + } else { + Yii::info('Успешно распарен заказ №' . $orderNumber, __METHOD__); + } + + return $order; + + // Если дошли до сюда, значит HTML не содержит валидный контент для парсинга + Yii::error('HTML не содержит валидный контент для парсинга заказов', __METHOD__); return []; } diff --git a/erp24/views/marketplace-orders/test-article-processing.php b/erp24/views/marketplace-orders/test-article-processing.php new file mode 100644 index 00000000..4657f428 --- /dev/null +++ b/erp24/views/marketplace-orders/test-article-processing.php @@ -0,0 +1,158 @@ +title = 'Тестирование обработки артикулов'; +$this->params['breadcrumbs'][] = ['label' => 'Заказы маркетплейсов', 'url' => ['index']]; +$this->params['breadcrumbs'][] = ['label' => 'Тестирование разбора заказов', 'url' => ['test-order-parsing']]; +$this->params['breadcrumbs'][] = $this->title; +?> + +
+ +

title) ?>

+ +

+ Результаты тестирования методов обработки артикулов в названиях товаров. +

+ +
+
+ + +
+ Общий результат: + +
+ +
+ + +
+

+ +

+
+
+
+ + + + + + + + + + + + + + + + + + + +
Входные данныеОжидаемый результатФактический результатСтатус
+ + ✓ Пройден + + ✗ Не пройден + +
+
+
+
+
+ + +
+

+ +

+
+
+
+ + + + + + + + + + + + + + + + + + + +
Входные данныеОжидаемый результатФактический результатСтатус
+ + ✓ Пройден + + ✗ Не пройден + +
+
+
+
+
+ +
+ +
+

Описание методов:

+
    +
  • + normalizeArticleInName() - используется при разборе заказов из email. + Если в названии товара есть артикул в скобках с дефисом (FW-0076), убирает дефис (FW0076). + Это обеспечивает корректное сопоставление с товарами в базе данных. +
  • +
  • + processDisplayName() - используется при формировании ответа API. + Если в названии товара есть артикул в скобках без дефиса (FW0076), добавляет дефис (FW-0076). + Это обеспечивает красивый формат отображения для внешних систем. +
  • +
+
+ +
+ 'btn btn-secondary']) ?> + 'btn btn-primary']) ?> +
+
+
+ +
+ diff --git a/erp24/views/marketplace-orders/test-order-parsing.php b/erp24/views/marketplace-orders/test-order-parsing.php new file mode 100644 index 00000000..d06bea9a --- /dev/null +++ b/erp24/views/marketplace-orders/test-order-parsing.php @@ -0,0 +1,226 @@ +title = 'Тестирование разбора заказов'; +$this->params['breadcrumbs'][] = ['label' => 'Заказы маркетплейсов', 'url' => ['index']]; +$this->params['breadcrumbs'][] = $this->title; +?> + +
+ +

title) ?>

+ +

+ Этот инструмент позволяет протестировать обработку артикулов при разборе заказов. + Вставьте HTML тела письма с заказом и нажмите "Разобрать заказ". +

+ +
+
+ 'post']); ?> + + field($model, 'html_body')->textarea([ + 'rows' => 20, + 'placeholder' => 'Вставьте HTML тела письма с заказом...' + ])->label('HTML тела письма') ?> + +
+ 'btn btn-success']) ?> +
+ + +
+ +
+ request->isPost): ?> + +
+ Ошибка разбора: +
+ +
+ Заказ успешно разобран! +
+ +

Информация о заказе:

+
+
+                    
+ + +

Элементы заказа:

+
+ + + + + + + + + + + + + + + + + +
Название товараКоличествоЦена
+ + Артикул: ' . Html::encode($article) . ''; + } + ?> + ₽
+
+ + +
+ Заказы не найдены в предоставленном HTML. Возможно, структура HTML не соответствует ожидаемой или заказы отсутствуют. +
+ + +
+ Заполните форму слева и нажмите "Разобрать заказ" для тестирования. +
+ +
+
+ +
+ +
+
+

Примеры HTML для тестирования:

+
+ +
+

+ +

+
+
+ +
<html>
+<body>
+<table>
+<p style="font-weight: 700;">Детали заказа </p>
+<tr style="margin-bottom: 12px;">
+<td style="width: 56px; padding-right:12px;">
+<img src="https://example.com/image.jpg" width="" alt="img" border="0" style="max-width:56px;">
+</td>
+<td>
+<p style="margin-bottom: 4px;">Букет из 11 розовых пионов (FW-0076)</p>
+<p style="color: #8C8C8C;">1 шт. </p>
+</td>
+<td>
+<p style="text-align: right;">4500₽ </p>
+</td>
+</tr>
+</table>
+</body>
+</html>
+
+
+
+ +
+

+ +

+
+
+ +
<html>
+<body>
+<table>
+<p style="font-weight: 700;">Детали заказа </p>
+<tr style="margin-bottom: 12px;">
+<td style="width: 56px; padding-right:12px;">
+<img src="https://example.com/image.jpg" width="" alt="img" border="0" style="max-width:56px;">
+</td>
+<td>
+<p style="margin-bottom: 4px;">Букет из 11 розовых пионов (FW0076)</p>
+<p style="color: #8C8C8C;">1 шт. </p>
+</td>
+<td>
+<p style="text-align: right;">4500₽ </p>
+</td>
+</tr>
+</table>
+</body>
+</html>
+
+
+
+ +
+

+ +

+
+
+ +
<html>
+<body>
+<table>
+<p style="font-weight: 700;">Детали заказа </p>
+<tr style="margin-bottom: 12px;">
+<td style="width: 56px; padding-right:12px;">
+<img src="https://example.com/image.jpg" width="" alt="img" border="0" style="max-width:56px;">
+</td>
+<td>
+<p style="margin-bottom: 4px;">Букет из 11 розовых пионов</p>
+<p style="color: #8C8C8C;">1 шт. </p>
+</td>
+<td>
+<p style="text-align: right;">4500₽ </p>
+</td>
+</tr>
+</table>
+</body>
+</html>
+
+
+
+ +
+
+
+ +
+ 'btn btn-info']) ?> + 'btn btn-primary ms-2']) ?> +
+ +
+ + -- 2.39.5