Настройка частичного возврата заказа 1С-Битрикс

Наша компания занимается разработкой, поддержкой и обслуживанием решений на Битрикс и Битрикс24 любой сложности. От простых одностраничных сайтов до сложных интернет магазинов, CRM систем с интеграцией 1С и телефонии. Опыт разработчиков подтвержден сертификатами от вендора.
Предлагаемые услуги
Показано 1 из 1 услугВсе 1626 услуг
Настройка частичного возврата заказа 1С-Битрикс
Простая
~1 рабочий день
Часто задаваемые вопросы
Наши компетенции:
Этапы разработки
Последние работы
  • image_website-b2b-advance_0.png
    Разработка сайта компании B2B ADVANCE
    1173
  • image_bitrix-bitrix-24-1c_fixper_448_0.png
    Разработка веб-сайта для компании ФИКСПЕР
    811
  • 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
    564
  • image_bitrix-bitrix-24-1c_mirsanbel_458_0.webp
    Разработка на базе 1С Предприятие для компании МИРСАНБЕЛ
    745
  • image_crm_dolbimby_434_0.webp
    Разработка сайта на CRM Битрикс24 для компании DOLBIMBY
    655
  • image_crm_technotorgcomplex_453_0.webp
    Разработка на базе Битрикс24 для компании ТЕХНОТОРГКОМПЛЕКС
    976

Настройка частичного возврата заказа 1С-Битрикс

Частичный возврат — возврат одной или нескольких позиций из заказа, а не всего заказа целиком. Распространённый сценарий: покупатель заказал 5 товаров, один оказался с дефектом, хочет вернуть только его. Или часть позиций не подошла. Модуль sale Битрикс поддерживает частичный возврат на уровне API, но интерфейс в личном кабинете и логика расчёта суммы к возврату требуют настройки.

Расчёт суммы частичного возврата

Это самая нетривиальная часть. В заказе может быть скидка на весь заказ, купоны, условия доставки, разные ставки НДС. При частичном возврате нужно пересчитать сумму с учётом всех этих факторов.

Подходы:

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

Подход 2: По фактической стоимости позиции — берём $basketItem->getFinalPrice() × количество. Это финальная цена после всех скидок на позицию. Рекомендуется.

Подход 3: По исходной цене без скидок — редко используется, только если условия возврата это требуют.

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

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, Тинькофф) поддерживают partial refund через отдельный метод API. Пример для Ю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 в запросе возврата.

Состав работ

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

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