Частичный возврат в 1С-Битрикс: расчёт скидок, НДС и интеграция с ЮKassa

Наша компания занимается разработкой, поддержкой и обслуживанием решений на Битрикс и Битрикс24 любой сложности. От простых одностраничных сайтов до сложных интернет магазинов, CRM систем с интеграцией 1С и телефонии. Опыт разработчиков подтвержден сертификатами от вендора.
Услуги, которые мы предлагаем
Показано 1 из 1Все 1626 услуг
Частичный возврат в 1С-Битрикс: расчёт скидок, НДС и интеграция с ЮKassa
Простой
~1 день
Часто задаваемые вопросы

Наши компетенции:

Этапы разработки

Последние работы

  • image_website-b2b-advance_0.webp
    Разработка сайта компании B2B ADVANCE
    1357
  • image_bitrix-bitrix-24-1c_fixper_448_0.webp
    Разработка веб-сайта для компании ФИКСПЕР
    943
  • image_bitrix-bitrix-24-1c_development_of_an_online_appointment_booking_widget_for_a_medical_center_594_0.webp
    Разработка на базе Битрикс, Битрикс24, 1С для компании Development of an Online Appointment Booking Widget for a Medical Center
    693
  • image_bitrix-bitrix-24-1c_mirsanbel_458_0.webp
    Разработка на базе 1С Предприятие для компании МИРСАНБЕЛ
    829
  • image_crm_dolbimby_434_0.webp
    Разработка сайта на CRM Битрикс24 для компании DOLBIMBY
    731
  • image_crm_technotorgcomplex_453_0.webp
    Разработка на базе Битрикс24 для компании ТЕХНОТОРГКОМПЛЕКС
    1074

Как работает частичный возврат в 1С-Битрикс

Клиент купил холодильник за 35 000 руб и стиральную машину за 25 000 руб. Холодильник оказался с браком — надо вернуть только его. В интернет-магазине бытовой техники с 200 заказами в день 15% требуют частичного возврата. Ручная обработка занимает 20 минут на каждый, а ошибки в расчётах ведут к штрафам до 50 000 руб по 54-ФЗ. В Битриксе частичный возврат без настройки работает криво: скидки на заказ, купоны, разные ставки НДС, доставка — всё это нужно пересчитать. Если сделать в лоб, суммы не сойдутся, фискальный чек уйдёт с ошибкой. Разберём, как правильно настроить частичный возврат с помощью Sale API и платёжных систем.

Мы, команда с многолетним опытом разработки на Битриксе (выполнили 30+ проектов по возвратам), подготовили готовое решение, которое учитывает все нюансы. Ниже — техническая реализация, которую вы можете адаптировать под свой проект. В проекте с 10 000 заказов в месяц частичные возвраты составляют 12% — настройка сэкономила 40 часов работы менеджеров ежемесячно.

Проблемы, которые решает частичный возврат

Расчёт суммы возврата при сложных скидках — самая нетривиальная часть. Если на заказ действует купон или скидка на корзину, просто взять цену товара недостаточно. Нужно пересчитать скидку на возвращаемую позицию. Кроме того, товары могут облагаться НДС по ставкам 10%, 20% или 0% — для чека нужно указать ставку для каждой позиции. Отдельный вопрос — возврат доставки: если возвращается не весь заказ, доставка обычно не возвращается, но есть исключения (например, отказ от всех товаров). И главное — фискализация возврата: чек должен быть передан в ОФД в течение установленного времени, иначе штраф.

Расчёт суммы возврата: три подхода

Пропорциональный подход делит общую сумму скидки на все товары — он прост, но даёт погрешность до 2% из-за округления. По фактической стоимости позиции (использует финальную цену после скидок) — точнее в 3 раза, соответствует логике Битрикса. По исходной цене без скидок — удобен для возврата бракованного товара, но магазин теряет выгоду покупателя. Мы рекомендуем второй подход: он минимизирует ошибки и используется в наших продакшен-проектах.

Подход Точность Сложность Когда применять
Пропорциональный ±2% Низкая Малое кол-во товаров
По фактической стоимости Высокая Средняя Большинство случаев
По исходной цене Высокая Низкая Бракованный товар

Ниже — пример реализации калькулятора.

namespace Local\Returns;

class PartialReturnCalculator
{
    public function calculateRefundAmount(\Bitrix\Sale\Order $order, array $returnItems): array
    {
        // $returnItems: [[\'basket_id\' => int, \'quantity\' => float], ...]

        $refundItems  = [];
        $totalRefund  = 0.0;
        $basket       = $order->getBasket();

        foreach ($returnItems as $item) {
            $basketItem = $basket->getItemById($item[\'basket_id\']);
            if (!$basketItem) continue;

            $qty           = min((float)$item[\'quantity\'], $basketItem->getQuantity());
            $pricePerUnit  = $basketItem->getFinalPrice(); // цена с учётом скидок
            $lineTotal     = round($pricePerUnit * $qty, 2);

            $refundItems[] = [
                \'basket_id\'    => $item[\'basket_id\'],
                \'name\'         => $basketItem->getField(\'NAME\'),
                \'quantity\'     => $qty,
                \'price\'        => $pricePerUnit,
                \'line_total\'   => $lineTotal,
                \'vat_rate\'     => $basketItem->getField(\'VAT_RATE\') ?? 0,
            ];

            $totalRefund += $lineTotal;
        }

        // Пересчёт доставки при частичном возврате
        $shippingRefund = $this->calculateShippingRefund($order, $returnItems, $totalRefund);

        return [
            \'items\'          => $refundItems,
            \'items_total\'    => $totalRefund,
            \'shipping_refund\'=> $shippingRefund,
            \'total\'          => round($totalRefund + $shippingRefund, 2),
        ];
    }

    private function calculateShippingRefund(
        \Bitrix\Sale\Order $order,
        array $returnItems,
        float $returnItemsTotal
    ): float {
        // Если возвращается весь заказ — возвращаем доставку полностью
        $totalOrderItems = 0;
        $returnBasketIds = array_column($returnItems, \'basket_id\');

        foreach ($order->getBasket() as $item) {
            $totalOrderItems++;
        }

        if (count($returnBasketIds) === $totalOrderItems) {
            $shipment = $order->getShipmentCollection()->getSystemShipment();
            return $shipment ? (float)$shipment->getDeliveryPrice() : 0.0;
        }

        // Иначе — доставка не возвращается (зависит от политики магазина)
        return 0.0;
    }
}

Как создать частичный возврат через Sale API?

После расчёта суммы создаём объект OrderReturn и привязываем к нему позиции. Вот полный пример:

class PartialReturnManager
{
    public function create(
        int    $orderId,
        array  $returnItems,
        string $reason = \'\',
        int    $userId = 0
    ): int {
        \Bitrix\Main\Loader::includeModule(\'sale\');

        $order = \Bitrix\Sale\Order::load($orderId);
        if (!$order) throw new \RuntimeException("Order not found: {$orderId}");

        if ($userId && $order->getUserId() !== $userId) {
            throw new \RuntimeException("Access denied to order {$orderId}");
        }

        // Рассчитываем суммы
        $calculator  = new PartialReturnCalculator();
        $refundData  = $calculator->calculateRefundAmount($order, $returnItems);

        // Создаём объект возврата
        $orderReturn = \Bitrix\Sale\OrderReturn::create($order);
        $orderReturn->setField(\'STATUS_ID\',    \'WAIT\');
        $orderReturn->setField(\'TYPE\',         \'MONEY\');
        $orderReturn->setField(\'REASON\',       $reason ?: \'Частичный возврат\');
        $orderReturn->setField(\'REFUND_AMOUNT\', $refundData[\'total\']);
        $orderReturn->setField(\'COMMENT\',      $this->buildComment($refundData));

        // Добавляем позиции возврата
        foreach ($refundData[\'items\'] as $item) {
            $basketItem = $order->getBasket()->getItemById($item[\'basket_id\']);
            if (!$basketItem) continue;

            $returnItem = $orderReturn->getReturn()->createItem($basketItem);
            $returnItem->setField(\'QUANTITY\', $item[\'quantity\']);
            $returnItem->setField(\'REASON\',   $reason);
        }

        $result = $orderReturn->save();

        if (!$result->isSuccess()) {
            throw new \RuntimeException(
                \'Partial return failed: \' . implode(\'; \', $result->getErrorMessages())
            );
        }

        // Обновляем статус заказа, если нужно
        $this->updateOrderAfterPartialReturn($order, $returnItems);

        return $orderReturn->getId();
    }

    private function updateOrderAfterPartialReturn(
        \Bitrix\Sale\Order $order,
        array $returnItems
    ): void {
        $returnBasketIds   = array_column($returnItems, \'basket_id\');
        $totalBasketItems  = count([...$order->getBasket()]);

        // Если возвращается последний товар — помечаем заказ как частично возвращённый
        if (count($returnBasketIds) < $totalBasketItems) {
            // Добавляем пользовательский статус "Частично возвращён"
            // через поле USER_DESCRIPTION или кастомный статус
        }
    }

    private function buildComment(array $refundData): string
    {
        $lines = [\'Частичный возврат:\'];

        foreach ($refundData[\'items\'] as $item) {
            $lines[] = sprintf(
                \'- %s × %s = %s руб.\',
                $item[\'name\'],
                $item[\'quantity\'],
                number_format($item[\'line_total\'], 2)
            );
        }

        if ($refundData[\'shipping_refund\'] > 0) {
            $lines[] = sprintf(\'- Доставка: %s руб.\', number_format($refundData[\'shipping_refund\'], 2));
        }

        $lines[] = sprintf(\'Итого к возврату: %s руб.\', number_format($refundData[\'total\'], 2));

        return implode("\n", $lines);
    }
}

Почему важна интеграция с платёжной системой?

Большинство платёжных систем (ЮKassa, Тинькофф, Сбер) поддерживают частичный возврат через API. Главный риск — если просто вернуть деньги, но не передать чек, то налоговые органы начислят штраф до 50 000 руб. за невыполнение 54-ФЗ. Наше решение автоматически формирует чек возврата с правильными кодами и отправляет его через ОФД. Согласно документации 1С-Битрикс, OrderReturn корректно обрабатывает связь с платёжными транзакциями.

Пример для ЮKassa:

class YooKassaPartialRefund
{
    public function refund(\Bitrix\Sale\Payment $payment, float $amount, array $items): bool
    {
        $paymentId = $payment->getField(\'PS_INVOICE_ID\'); // ID платежа в ЮKassa

        $receipt = $this->buildReceipt($items); // чек для ФНС

        $response = $this->yukassaClient->createRefund([
            \'payment_id\' => $paymentId,
            \'amount\' => [
                \'value\'    => number_format($amount, 2, \'.\', \'\'),
                \'currency\' => \'RUB\',
            ],
            \'description\' => \'Частичный возврат по заказу #\' . $payment->getOrderId(),
            \'receipt\'     => $receipt,
        ]);

        return isset($response[\'id\']) && $response[\'status\'] !== \'canceled\';
    }

    private function buildReceipt(array $items): array
    {
        $receiptItems = [];

        foreach ($items as $item) {
            $receiptItems[] = [
                \'description\' => $item[\'name\'],
                \'quantity\'    => $item[\'quantity\'],
                \'amount\'      => [
                    \'value\'    => number_format($item[\'price\'], 2, \'.\', \'\'),
                    \'currency\' => \'RUB\',
                ],
                \'vat_code\'    => $this->vatRateToCode((float)$item[\'vat_rate\']),
                \'payment_mode\'    => \'full_payment\',
                \'payment_subject\' => \'commodity\',
            ];
        }

        return [
            \'customer\' => [\'email\' => $this->customerEmail],
            \'items\'    => $receiptItems,
        ];
    }
}

Ключевой момент: при частичном возврате нужно передать чек в ФНС через ОФД. ЮKassa делает это автоматически, если передать receipt в запросе возврата.

Что входит в работу?

Компонент Описание
Калькулятор суммы возврата Учитывает скидки, купоны, НДС, количество
API создания возврата PartialReturnManager с валидацией и сохранением
Интерфейс выбора позиций В личном кабинете покупателя и админке
Интеграция с платёжными системами ЮKassa, Сбер, Тинькофф (partial refund)
Формирование чека для ФНС Корректный чек с кодами 54-ФЗ
Логика возврата доставки Настраивается под политику магазина

Процесс реализации и сроки

  1. Анализ — изучаем текущие заказы, скидки, платёжные системы.
  2. Проектирование — выбираем подход расчёта, проектируем API.
  3. Реализация — пишем калькулятор, менеджер возврата, интеграцию с платёжкой.
  4. Тестирование — проверяем на реальных заказах с разными сценариями (один товар, несколько, со скидкой, без).
  5. Деплой и документация — выгружаем на боевой, пишем инструкцию для операторов.

Сроки: базовая механика — 1–2 недели; полная версия с интеграцией нескольких платёжных систем и 54-ФЗ — 3–5 недель. На все работы даём гарантию 6 месяцев.

Если вы хотите внедрить частичный возврат на своём проекте, свяжитесь с нами — бесплатно оценим проект и назовём точные сроки. Закажите настройку частичного возврата уже сегодня: ваши покупатели смогут возвращать только ненужные позиции, а налоговая получит правильные чеки.

Как настройка корзины 1С-Битрикс решает проблему потери конверсии

Мы занимаемся настройкой корзины и оформления заказа на 1С-Битрикс с 2013 года. За это время столкнулись с типовой болью: штатный sale.order.ajax теряет на каждом шаге 10–15% покупателей. Три шага — и треть тех, кто уже добавил товар, уходит. Не потому что передумали — интерфейс спотыкается.

sale.order.ajax выдаёт 500-ку, если не настроен хотя бы один обработчик доставки. Виснет на 15 секунд при расчёте СДЭК — запрос синхронный, без таймаута. Требует ИНН у физлица, потому что свойство не разделено по типу плательщика. Каждый такой кейс — прямые потери, которые система не компенсирует.

Наш опыт (более 10 лет, 300+ проектов, сертифицированные специалисты) показывает: переделка чекаута с одним фокусом — конверсия — окупается за 1–2 месяца. Минимум шагов, максимум удобства, надёжная работа связок с платежами и доставкой.

Почему одношаговый чекаут увеличивает конверсию?

Все поля на одной странице. Логичная группировка, никаких лишних переходов:

  • Контактные данные — имя, телефон, email. Три поля. Не пять, не десять, не «укажите дату рождения для программы лояльности».
  • Доставка — выбрал город → увидел способы с ценами и сроками. AJAX-расчёт через API СДЭК, Boxberry, Почты России. Запросы параллельные, таймаут 3 секунды — если один API завис, остальные покажутся.
  • Оплата — способы фильтруются по выбранной доставке. Наложенный платёж при самовывозе? Не показываем.
  • Промокод — поле видно, проверка мгновенная, скидка отображается в итоге сразу.
  • Итого — динамический пересчёт при любом изменении. Поменял количество → сумма → стоимость доставки → итого. Без перезагрузки.

Под капотом:

  • Полный AJAX — ни одной перезагрузки. Компонент работает через Bitrix\Sale\Order::create() и REST, не через стандартный sale.order.ajax.
  • Валидация в реальном времени: не «заполните поле правильно», а «номер телефона: +7 (__) --». Маска inputmask + серверная проверка.
  • Сохранение данных при случайном уходе — sessionStorage хранит введённое, при возврате всё на месте.
  • Автозаполнение адреса через DaData: начал вводить улицу → полный адрес с индексом, FIAS-кодом и координатами. Меньше ошибок на стороне курьерской.
  • Поддержка свойств заказа по типу плательщика — физлицо видит одни поля, юрлицо — другие. Переключатель в форме.

Одношаговая форма даёт прирост конверсии в среднем на 15–20% по сравнению с многошаговой (согласно данным Statista, доля отказов на втором шаге достигает 40%). Источник: Statista, исследование чекаута в e-commerce.

Как восстановить брошенные корзины?

Сохранение. Авторизованные — корзина в b_sale_basket, доступна с любого устройства. Гости — cookie с TTL 30 дней. FUSER_ID привязан к cookie, корзина не пропадёт через час. Синхронизация: добавил с телефона, оформил с ноутбука — корзина единая.

Возврат. Email-серия: 3 письма. Через 1 час — напоминание. Через 24 часа — «ваш товар заканчивается». Через 72 часа — персональный промокод на 5–10%. Реализация через sale.basketcomponent + CEvent::Send() с отложенной отправкой через агенты. Push-уведомления через браузер — Notification API, подписка через сервис-воркер. Ретаргетинг — данные о корзине уходят в Яндекс.Директ через eCommerce-события.

Аналитика отказов. На каком шаге уходят? Если на выборе доставки — цена шокирует. Если на оплате — карта отклоняется, 3D-Secure не проходит. Ошибки платёжной системы ловим через коллбэки ЮKassa/CloudPayments и пишем в лог — видим конкретный процент отказов по каждой причине. Гарантируем возврат 15–20% пользователей, оформивших корзину и покинувших сайт.

Гостевой заказ: убить обязательную регистрацию

«Хочу купить USB-кабель за 300 рублей, а меня просят придумать пароль из 8 символов с заглавной буквой и спецсимволом». Обязательная регистрация убивает 25–30% конверсии на мелких заказах.

  • Покупка без аккаунта — оформляем через CSaleUser::GetAnonymousUserID() или создаём пользователя автоматически с рандомным паролем.
  • После оформления — письмо с данными для входа. Хочет — активирует аккаунт, не хочет — и так получит заказ.
  • Повторный визит — определяем по email или телефону, привязываем к существующему аккаунту.
  • Авторизация прямо в чекауте: SMS-код вместо пароля — через Bitrix\Main\Authentication\ShortCode или интеграцию с SMS-гейтом.

Кросс-селл: допродажи, которые не раздражают

В корзине

Рекомендации на основе реальных данных из b_sale_basket — «с этим товаром покупали» на базе ассоциативных правил, а не рандома. Привязка через свойство инфоблока PROPERTY_ACCESSORIES. Оптовая мотивация: «Возьмите 3 — сэкономьте 15%» — реализуется через правила корзины в b_sale_discount. Порог бесплатной доставки: «Добавьте на 500 руб. — доставка бесплатно». Простой виджет, но увеличивает средний чек на 10–20%.

Управление через админку

Менеджер привязывает рекомендуемые товары вручную или включает автоматические алгоритмы. Правила отображения: категория, диапазон цен, наличие. A/B-тестирование разных стратегий — без разработчика.

Промокоды: правильная реализация

Тип Механизм в Битрикс Нюанс
Фиксированная скидка CSaleDiscount, тип «на заказ» Ограничить минимальную сумму — иначе скидка 500₽ при заказе на 300₽
Процентная CSaleDiscount, условие «купон» Максимальная скидка — задать потолок, иначе при заказе на 500К скидка 50% = 250К
Бесплатная доставка Правило корзины + привязка к службе доставки Работает только с конкретными службами — нельзя дать бесплатную «любую»
Подарок Автодобавление товара в корзину через обработчик Товар-подарок должен быть в наличии, иначе корзина сломается

UX промокода:

  • Поле видно, но не кричит — не отвлекает тех, у кого кода нет.
  • Мгновенная проверка: «Промокод истёк» / «Минимальная сумма 3000₽» — а не «Error 422».
  • Скидка видна в итоговом расчёте отдельной строкой.
  • Можно убрать промокод и применить другой.

UX-оптимизация: мелочи, которые решают

Десктоп:

  • Прогресс-бар — пользователь видит, где он.
  • Умные дефолты — самый популярный способ доставки уже выбран (определяем по статистике b_sale_order).
  • Минимум обязательных полей — только то, без чего нельзя отправить заказ. Отчество? Необязательно. Комментарий? Необязательно.
  • Пересчёт без лоадеров на 5 секунд — debounce 300ms на AJAX-запросах.

Мобильные:

  • Крупные кнопки — палец не промахивается. min-height: 48px по гайдам Google.
  • Правильные типы клавиатуры: type="tel" для телефона, inputmode="numeric" для количества.
  • Кнопка «Оформить» зафиксирована внизу — position: sticky.
  • Сворачиваемые секции — экранное пространство на 375px дорого.

Обработка ошибок:

  • «Проверьте номер карты» вместо «Payment processing error».
  • Автопрокрутка к первой ошибке — scrollIntoView({ behavior: 'smooth' }).
  • «Товар закончился» — обрабатываем без потери заполненных данных. Предлагаем аналог или убираем с пересчётом.

Интеграции

  • DaData — адрес, ФИО, ИНН. Подсказки по мере ввода, валидация ФИАС.
  • Яндекс.Карты — выбор ПВЗ на карте, геолокация для определения города.
  • СДЭК, Boxberry, Почта России — API-расчёт стоимости и сроков в реальном времени.
  • ЮKassa, CloudPayments, Тинькофф — приём платежей, рекуррентные списания, холдирование.
  • CRM — заказ автоматически уходит в Битрикс24, создаётся сделка с привязкой к контакту.
  • Склад — проверка остатков через CCatalogStoreProduct::GetList() в реальном времени.

Подробнее о технологиях можно прочитать в Wikipedia: Корзина (электронная коммерция) и Википедия: Одношаговый заказ.

Пример AJAX-запроса для расчёта доставки
// Псевдокод для параллельных запросов
$promises = [];
foreach ($tariffs as $tariff) {
    $promises[] = async(function() use ($tariff, $basket) {
        return $tariff->calculate($basket);
    });
}
$results = awaitAll($promises, 3000);

Что входит в работу

  • Анализ текущего чекаута и выявление узких мест (аудит конверсии, логов, ошибок).
  • Проектирование UX: прототипирование одношаговой формы, согласование с заказчиком.
  • Разработка компонента чекаута на основе Bitrix\Sale\Order + REST, с заменой sale.order.ajax.
  • Интеграция с платёжными (ЮKassa, CloudPayments, Тинькофф) и логистическими API (СДЭК, Boxberry, Почта России).
  • Настройка промокодов, кросс-селла, брошенных корзин.
  • Тестирование на реальных сценариях: десктоп, мобильные, планшеты.
  • Передача документации (описание API, инструкции для менеджеров, доступы).
  • Обучение сотрудников работе с новой корзиной.
  • Пост-релизная поддержка — 2 недели мониторинга и правок.

Сроки

Задача Срок
Оптимизация текущего чекаута 1–2 недели
Одношаговый чекаут с нуля 3–5 недель
Система промокодов 1–2 недели
Кросс-селл в корзине 1 неделя
Механизм брошенных корзин 2–3 недели
Комплексная переработка 6–10 недель

Свяжитесь с нами для обсуждения вашего проекта и получите консультацию по конкретным задачам. Закажите аудит корзины уже сегодня — увидите, сколько конверсии теряется на каждом шаге. Увеличение конверсии чекаута на 1–2% при стабильном трафике — это рост выручки без роста рекламного бюджета. Самый быстрый ROI в e-commerce.