Настройка частичного возврата заказа 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 недель.







