Как работает частичный возврат в 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-ФЗ |
| Логика возврата доставки | Настраивается под политику магазина |
Процесс реализации и сроки
- Анализ — изучаем текущие заказы, скидки, платёжные системы.
- Проектирование — выбираем подход расчёта, проектируем API.
- Реализация — пишем калькулятор, менеджер возврата, интеграцию с платёжкой.
- Тестирование — проверяем на реальных заказах с разными сценариями (один товар, несколько, со скидкой, без).
- Деплой и документация — выгружаем на боевой, пишем инструкцию для операторов.
Сроки: базовая механика — 1–2 недели; полная версия с интеграцией нескольких платёжных систем и 54-ФЗ — 3–5 недель. На все работы даём гарантию 6 месяцев.
Если вы хотите внедрить частичный возврат на своём проекте, свяжитесь с нами — бесплатно оценим проект и назовём точные сроки. Закажите настройку частичного возврата уже сегодня: ваши покупатели смогут возвращать только ненужные позиции, а налоговая получит правильные чеки.







