How Partial Refund Works in 1C-Bitrix
A customer bought a refrigerator for 35,000 rubles and a washing machine for 25,000 rubles. The refrigerator is defective — only that item needs to be returned. In an online electronics store with 200 orders per day, 15% require partial returns. Manual processing takes 20 minutes per return, and calculation errors lead to fines of up to 50,000 rubles under Federal Law 54-FZ. In Bitrix, partial return without custom setup works poorly: order discounts, coupons, different VAT rates, delivery — all need to be recalculated. If done naively, sums won't match, and the fiscal receipt will contain errors. Let's dive into how to properly configure partial returns using Sale API and payment systems.
We, a team with years of Bitrix development experience (completed 30+ return projects), have prepared a ready-made solution that covers all nuances. Below is the technical implementation you can adapt for your project. In a project with 10,000 orders per month, partial returns accounted for 12% — the setup saved 40 hours of managers' work monthly.
Problems Solved by Partial Return
Calculating the refund amount with complex discounts is the trickiest part. If an order has a coupon or a cart discount, simply using the item's price is insufficient. The discount must be recalculated for the returned item. Additionally, items may have VAT rates of 10%, 20%, or 0% — the receipt must specify the rate for each position. Another issue is delivery refund: if not the entire order is returned, delivery is usually not refunded, but there are exceptions (e.g., canceling all items). And most importantly — fiscalization: the receipt must be sent to the OFD within a set time, otherwise a fine.
Three Approaches to Calculating the Refund Amount
The proportional approach divides the total discount among all items — simple but has up to 2% rounding error. The actual item cost approach (uses the final price after discounts) is 3 times more accurate and matches Bitrix logic. The original price approach (without discounts) is convenient for defective items but the store loses the buyer's benefit. We recommend the second approach: it minimizes errors and is used in our production projects.
| Approach | Accuracy | Complexity | When to Use |
|---|---|---|---|
| Proportional | ±2% | Low | Small number of items |
| Actual cost | High | Medium | Most cases |
| Original price | High | Low | Defective items |
Below is an example implementation of a calculator.
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;
}
}
How to Create a Partial Return via Sale API?
After calculating the amount, create an OrderReturn object and attach items to it. Here is a full example:
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);
}
}
Why Payment System Integration Matters?
Most payment systems (YooKassa, Tinkoff, Sber) support partial refund via API. The main risk: if you simply return the money but fail to send a receipt, tax authorities may impose a fine of up to 50,000 rubles for non-compliance with 54-FZ. Our solution automatically generates a refund receipt with correct codes and sends it through the OFD. According to 1C-Bitrix documentation, OrderReturn correctly handles ties to payment transactions.
Example for YooKassa:
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,
];
}
}
The key point: during a partial refund, a receipt must be sent to the Federal Tax Service via the OFD. YooKassa does this automatically if you pass the receipt in the refund request.
What's Included in the Work?
| Component | Description |
|---|---|
| Refund amount calculator | Accounts for discounts, coupons, VAT, quantity |
| Return creation API | PartialReturnManager with validation and saving |
| Item selection interface | In customer and admin panels |
| Payment system integration | YooKassa, Sber, Tinkoff (partial refund) |
| Fiscal receipt generation | Correct receipt with 54-FZ codes |
| Delivery refund logic | Configurable per store policy |
Implementation Process and Timeline
- Analysis — study current orders, discounts, payment systems.
- Design — choose calculation approach, design API.
- Implementation — write calculator, return manager, payment integration.
- Testing — verify on real orders with various scenarios (single item, multiple items, with/without discounts).
- Deployment and documentation — deploy to production, write operator instructions.
Timeline: basic mechanics — 1–2 weeks; full version with multiple payment system integration and 54-FZ compliance — 3–5 weeks. We provide a 6-month warranty on all work.
If you want to implement partial returns on your project, contact us — we will evaluate the project for free and give exact timelines. Start implementing partial returns today: your customers will be able to return only unwanted items, and the tax authorities will receive correct receipts.







