Разработка системы RMA (Return Merchandise Authorization) на 1С-Битрикс
Возвраты без системы — хаос. Клиент звонит или пишет на общую почту, менеджер вручную ищет заказ, договаривается о возврате, отправляет инструкции. Отследить статус возврата невозможно, аналитики нет. RMA-система автоматизирует этот процесс: клиент создаёт заявку на возврат через личный кабинет, менеджер обрабатывает её в административной части, система отслеживает статусы. Битрикс не имеет встроенного RMA — его строят поверх модуля sale.
Юридический контекст
Возврат товара регулируется статьями 18, 25 Закона о защите прав потребителей (ЗоЗПП). Два основных основания:
- Ненадлежащее качество (ст. 18) — дефект, брак. Срок предъявления — в пределах гарантийного срока.
- Надлежащее качество (ст. 25) — «передумал». Только для непродовольственных товаров, 14 дней, товар не должен быть в употреблении.
В системе RMA оба основания должны быть представлены отдельными типами заявок с разными полями и процессами.
Модель данных
Таблица RMA-заявок:
class RmaRequestTable extends \Bitrix\Main\ORM\Data\DataManager
{
public static function getTableName(): string { return 'b_local_rma_request'; }
public static function getMap(): array
{
return [
new IntegerField('ID', ['primary' => true, 'autocomplete' => true]),
new StringField('NUMBER'), // RMA-YYYYMMDD-XXXX
new IntegerField('ORDER_ID'), // b_sale_order
new IntegerField('USER_ID'),
new EnumField('TYPE', ['values' => ['DEFECT', 'EXCHANGE', 'REFUND']]),
new EnumField('STATUS', ['values' => ['NEW', 'UNDER_REVIEW', 'APPROVED', 'REJECTED', 'RETURNED', 'REFUNDED', 'CLOSED']]),
new TextField('DESCRIPTION'), // Описание причины от клиента
new StringField('DEFECT_PHOTO_IDS'), // JSON-массив b_file ID
new StringField('REJECT_REASON'), // Причина отказа (если REJECTED)
new FloatField('REFUND_AMOUNT'), // Сумма к возврату
new IntegerField('RESPONSIBLE_ID'), // Ответственный менеджер
new DatetimeField('CREATED_AT'),
new DatetimeField('UPDATED_AT'),
new DatetimeField('DEADLINE_AT'), // Дедлайн обработки (14 дней)
];
}
}
Позиции возврата:
class RmaItemTable extends \Bitrix\Main\ORM\Data\DataManager
{
public static function getTableName(): string { return 'b_local_rma_item'; }
public static function getMap(): array
{
return [
new IntegerField('ID', ['primary' => true, 'autocomplete' => true]),
new IntegerField('RMA_ID'),
new IntegerField('ORDER_BASKET_ID'), // b_sale_basket
new IntegerField('PRODUCT_ID'),
new StringField('PRODUCT_NAME'),
new FloatField('QUANTITY'),
new FloatField('PRICE'),
new StringField('REASON_CODE'), // DEFECTIVE, NOT_AS_DESCRIBED, CHANGED_MIND, WRONG_ITEM
new StringField('CONDITION'), // NEW, USED, DAMAGED
];
}
}
История статусов:
class RmaHistoryTable extends \Bitrix\Main\ORM\Data\DataManager
{
public static function getTableName(): string { return 'b_local_rma_history'; }
// RMA_ID, OLD_STATUS, NEW_STATUS, USER_ID, COMMENT, CREATED_AT
}
Генерация номера RMA
class RmaNumberGenerator
{
public static function generate(): string
{
$date = date('Ymd');
$lastId = RmaRequestTable::getList([
'order' => ['ID' => 'DESC'],
'limit' => 1,
'select' => ['ID'],
])->fetch()['ID'] ?? 0;
return 'RMA-' . $date . '-' . str_pad($lastId + 1, 4, '0', STR_PAD_LEFT);
}
}
Создание заявки из личного кабинета
Страница /personal/rma/create/?order_id=12345:
class RmaCreateComponent extends \CBitrixComponent
{
public function executeComponent(): void
{
$orderId = (int)$this->arParams['ORDER_ID'];
if (!$orderId || !$this->isOrderOwner($orderId)) {
LocalRedirect('/personal/orders/');
return;
}
// Загрузить товары заказа
$order = \Bitrix\Sale\Order::load($orderId);
$basket = $order->getBasket();
$items = [];
foreach ($basket as $item) {
// Проверить: не истёк ли срок для возврата
$daysSinceOrder = (new \DateTime())->diff(new \DateTime($order->getField('DATE_INSERT')))->days;
if ($daysSinceOrder > 365) continue; // Пропустить товары старше года
$items[] = [
'BASKET_ID' => $item->getId(),
'PRODUCT_ID' => $item->getProductId(),
'NAME' => $item->getField('NAME'),
'QUANTITY' => $item->getQuantity(),
'PRICE' => $item->getPrice(),
'CAN_REFUND' => $daysSinceOrder <= 14, // 14 дней для возврата без дефекта
];
}
if ($this->request->isPost() && check_bitrix_sessid()) {
$this->createRmaRequest($orderId, $items);
}
$this->arResult['ORDER'] = $order;
$this->arResult['ITEMS'] = $items;
$this->includeComponentTemplate();
}
private function createRmaRequest(int $orderId, array $items): void
{
$selectedItems = [];
foreach ($this->request->getPost('items') as $basketId => $itemData) {
if (empty($itemData['selected'])) continue;
$selectedItems[] = [
'ORDER_BASKET_ID' => $basketId,
'QUANTITY' => (float)$itemData['quantity'],
'REASON_CODE' => $itemData['reason'],
'CONDITION' => $itemData['condition'],
];
}
if (empty($selectedItems)) {
$this->arResult['ERROR'] = 'Выберите хотя бы один товар для возврата';
return;
}
// Загрузить фотографии дефектов
$photoIds = [];
foreach ($_FILES['defect_photos']['tmp_name'] as $i => $tmpName) {
if (is_uploaded_file($tmpName)) {
$fileId = \CFile::SaveFile([
'name' => $_FILES['defect_photos']['name'][$i],
'size' => $_FILES['defect_photos']['size'][$i],
'tmp_name' => $tmpName,
'type' => $_FILES['defect_photos']['type'][$i],
], 'rma');
if ($fileId) $photoIds[] = $fileId;
}
}
$addResult = RmaRequestTable::add([
'NUMBER' => RmaNumberGenerator::generate(),
'ORDER_ID' => $orderId,
'USER_ID' => $GLOBALS['USER']->GetID(),
'TYPE' => $this->request->getPost('type'),
'STATUS' => 'NEW',
'DESCRIPTION' => htmlspecialchars($this->request->getPost('description')),
'DEFECT_PHOTO_IDS' => json_encode($photoIds),
'CREATED_AT' => new \Bitrix\Main\Type\DateTime(),
'DEADLINE_AT' => \Bitrix\Main\Type\DateTime::createFromPhp(new \DateTime('+14 days')),
]);
$rmaId = $addResult->getId();
foreach ($selectedItems as $item) {
RmaItemTable::add(array_merge($item, ['RMA_ID' => $rmaId]));
}
// Уведомить менеджера
$this->notifyManager($rmaId);
$this->arResult['SUCCESS'] = true;
$this->arResult['RMA_NUMBER'] = 'RMA-...' ;
}
}
Административная обработка
Страница /local/admin/rma_list.php — список всех RMA с фильтрами по статусу, дедлайну, ответственному. Действия менеджера:
// Одобрить возврат
public function approve(int $rmaId, float $refundAmount, int $managerId): void
{
RmaRequestTable::update($rmaId, [
'STATUS' => 'APPROVED',
'REFUND_AMOUNT' => $refundAmount,
'RESPONSIBLE_ID' => $managerId,
'UPDATED_AT' => new \Bitrix\Main\Type\DateTime(),
]);
RmaHistoryTable::add([
'RMA_ID' => $rmaId,
'OLD_STATUS' => 'UNDER_REVIEW',
'NEW_STATUS' => 'APPROVED',
'USER_ID' => $managerId,
'COMMENT' => 'Заявка одобрена',
]);
// Инициировать возврат средств через платёжную систему
$this->initiateRefund($rmaId, $refundAmount);
// Уведомить клиента
$this->notifyClient($rmaId, 'APPROVED');
}
Интеграция с возвратом платежа
Для возврата денег через платёжную систему — используем API того же шлюза, через который принята оплата. Большинство российских шлюзов (ЮKassa, CloudPayments, СБПЭЙ) имеют метод рефанда:
// Пример для ЮKassa
$client = new \YooKassa\Client();
$client->setAuth($shopId, $secretKey);
$originalPaymentId = $this->getOriginalPaymentId($orderId); // из b_sale_pay_system_action
$refund = $client->createRefund([
'payment_id' => $originalPaymentId,
'amount' => ['value' => $refundAmount, 'currency' => 'RUB'],
'description' => 'Возврат по RMA #' . $rmaNumber,
]);
После успешного рефанда — статус RMA → REFUNDED, пополнить баланс склада (если товар возвращён на склад).
Сроки разработки
| Вариант | Состав | Срок |
|---|---|---|
| Базовая RMA | Форма заявки, список в ЛК, статусы | 8–12 дней |
| С обработкой в админке | Интерфейс менеджера, история, уведомления | 12–18 дней |
| Полная система | + Возврат платежей, интеграция со складом | 18–28 дней |







