We often encounter projects where the standard component bitrix:sale.order.return.edit cannot handle the tasks: it lacks photo upload of defects, a step-by-step interface, and the ability to specify different reasons for each item. With 50–100 return requests per day, a cumbersome form directly translates to lost manager time spent on phone clarifications.
Imagine: a customer receives a defective product. They log into their personal account, find the order, but the standard return form does not allow attaching a photo of the defect. They have to call the manager, clarify the reason, email the photo. This increases processing time by an average of 15 minutes. With 100 returns per day—a loss of 25 man-hours. Two managers spend half a day on emails and calls instead of processing other tickets.
Our custom return form for 1C-Bitrix integrates a wizard, photo upload, and 1C integration to streamline return processing. We develop custom return forms turnkey for 1C-Bitrix, reducing processing time by 30%. Our experience with Bitrix—10+ years, over 50 return projects. Development cost starts at $1,500 for a basic form, and monthly savings from reduced processing time can exceed $2,000. Implementation of such a form pays off within 3–6 months.
Why do you need a custom return form?
A custom wizard processes a return 3 times faster than the built-in form—4 minutes instead of 15. The customer goes through 4 steps, the manager receives complete data without needing to call back.
| Characteristic | Standard form | Custom form |
|---|---|---|
| Photo upload | no | yes (up to 5 MB) |
| Reason selection per item | no | yes |
| Number of steps | 1 | 4 (wizard) |
| Time to fill | ~15 min | ~4 min |
| Integration with 1C | via exchange | direct via REST |
Wizard form structure and advantages
Optimal UX for a return form—3–4 steps:
- Order selection — customer chooses from their order history available for return.
- Product and reason selection — checkboxes for items, each with a reason and quantity.
- Additional information — comment, photo/document upload.
- Confirmation — final screen with request data and instructions.
Step 1: orders available for return
Return is only possible for paid orders within a certain period (usually 14 days by law). We load the list:
<?php
namespace Local\Returns;
class ReturnableOrdersProvider
{
private int $userId;
private int $returnWindowDays;
public function __construct(int $userId, int $returnWindowDays = 14)
{
$this->userId = $userId;
$this->returnWindowDays = $returnWindowDays;
}
public function getReturnableOrders(): array
{
\Bitrix\Main\Loader::includeModule('sale');
$dateFrom = new \Bitrix\Main\Type\Date();
$dateFrom->add('-' . $this->returnWindowDays . ' days');
$result = \Bitrix\Sale\OrderTable::getList([
'filter' => [
'USER_ID' => $this->userId,
'PAYED' => 'Y',
'>=DATE_PAY' => $dateFrom,
'!STATUS_ID' => ['CANCELED', 'RETURNED'],
],
'select' => ['ID', 'ACCOUNT_NUMBER', 'DATE_INSERT', 'PRICE', 'CURRENCY', 'STATUS_ID'],
'order' => ['DATE_INSERT' => 'DESC'],
]);
$orders = [];
while ($row = $result->fetch()) {
// Check if a full return already exists for this order
if (!$this->hasFullReturn($row['ID'])) {
$orders[] = $row;
}
}
return $orders;
}
private function hasFullReturn(int $orderId): bool
{
$existing = \Bitrix\Sale\OrderReturnTable::getList([
'filter' => ['ORDER_ID' => $orderId, 'STATUS_ID' => ['APPROVED', 'RECEIVED', 'REFUND']],
'select' => ['ID'],
'limit' => 1,
])->fetch();
return (bool)$existing;
}
}
Step 2: order items with reason selection
<?php
class OrderItemsProvider
{
public function getReturnableItems(int $orderId, int $userId): array
{
$order = \Bitrix\Sale\Order::load($orderId);
if (!$order || $order->getUserId() !== $userId) {
throw new \RuntimeException('Order not found or access denied');
}
$items = [];
foreach ($order->getBasket() as $item) {
// Calculate already returned quantity
$returnedQty = $this->getReturnedQuantity($orderId, $item->getId());
$availableQty = $item->getQuantity() - $returnedQty;
if ($availableQty <= 0) continue;
$items[] = [
'basket_id' => $item->getId(),
'product_id' => $item->getProductId(),
'name' => $item->getField('NAME'),
'quantity' => $item->getQuantity(),
'available_qty' => $availableQty,
'price' => $item->getFinalPrice(),
'image' => $this->getProductImage($item->getProductId()),
'article' => $item->getField('ARTICLE'),
];
}
return $items;
}
private function getReturnedQuantity(int $orderId, int $basketItemId): float
{
$result = \Bitrix\Sale\OrderReturnBasketTable::getList([
'filter' => [
'ORDER_RETURN.ORDER_ID' => $orderId,
'BASKET_ID' => $basketItemId,
'ORDER_RETURN.STATUS_ID' => ['WAIT', 'REVIEW', 'APPROVED', 'RECEIVED', 'REFUND'],
],
'runtime' => [
new \Bitrix\Main\ORM\Fields\ExpressionField('TOTAL_QTY', 'SUM(%s)', 'QUANTITY'),
],
'select' => ['TOTAL_QTY'],
])->fetch();
return (float)($result['TOTAL_QTY'] ?? 0);
}
}
How to implement the client-side wizard?
React component for the wizard (or Vue—your choice):
import React, { useState } from 'react';
function ReturnWizard({ orderId }) {
const [step, setStep] = useState(1);
const [selectedItems, setSelectedItems] = useState([]);
const [files, setFiles] = useState([]);
const returnReasons = [
{ id: 'defect', label: 'Manufacturing defect' },
{ id: 'wrong_item', label: 'Wrong item sent' },
{ id: 'damaged', label: 'Damaged during delivery' },
{ id: 'not_fit', label: 'Does not fit' },
{ id: 'other', label: 'Other reason' },
];
const canProceed = selectedItems.some(item => item.selected && item.reason);
async function submitReturn() {
const formData = new FormData();
formData.append('order_id', orderId);
formData.append('sessid', BX.bitrix_sessid());
formData.append('items', JSON.stringify(selectedItems.filter(i => i.selected)));
files.forEach((file, i) => formData.append(`files[${i}]`, file));
const res = await fetch('/local/api/return-submit.php', {
method: 'POST',
body: formData,
});
const data = await res.json();
if (data.success) {
setStep(4); // Success screen
}
}
// ... render steps
}
Server-side handler for final submission
<?php
// /local/api/return-submit.php
require_once($_SERVER['DOCUMENT_ROOT'] . '/bitrix/modules/main/include/prolog_before.php');
header('Content-Type: application/json');
if (!\CUser::IsAuthorized()) {
http_response_code(401);
exit(json_encode(['error' => 'Unauthorized']));
}
if (!\bitrix_sessid_check($_POST['sessid'] ?? '')) {
http_response_code(403);
exit(json_encode(['error' => 'Invalid session']));
}
$orderId = (int)($_POST['order_id'] ?? 0);
$items = json_decode($_POST['items'] ?? '[]', true);
$userId = (int)\CUser::GetID();
// Validate order belongs to user
$validator = new \Local\Returns\ReturnValidator($userId);
if (!$validator->canReturnOrder($orderId)) {
exit(json_encode(['success' => false, 'error' => 'Order not available for return']));
}
// Upload attached files
$fileIds = [];
$uploader = new \Local\Upload\FileUploader();
foreach ($_FILES as $key => $file) {
if (strpos($key, 'files') === 0 && $file['error'] === UPLOAD_ERR_OK) {
try {
$result = $uploader->handle($file);
$fileIds[] = $result['id'];
} catch (\Exception $e) {
// Log but do not interrupt
}
}
}
// Create return request
$manager = new \Local\Returns\ReturnManager();
$returnId = $manager->createReturn($orderId, $items, 'MONEY');
// Attach files to request
if ($fileIds) {
\Local\Returns\ReturnAttachments::attach($returnId, $fileIds);
}
// Send notifications
\Local\Returns\Notifications::sendToCustomer($returnId);
\Local\Returns\Notifications::sendToManager($returnId);
exit(json_encode([
'success' => true,
'return_id' => $returnId,
'message' => 'Request #' . $returnId . ' created. We will review it within 2 business days.',
]));
Attachments to the request: extending the table
The standard Bitrix return system does not store attached files. We extend via Highload-block:
<?php
class ReturnAttachmentTable extends \Bitrix\Main\ORM\Data\DataManager
{
public static function getTableName(): string { return 'local_return_attachments'; }
public static function getMap(): array
{
return [
new \Bitrix\Main\ORM\Fields\IntegerField('ID', ['primary' => true, 'autocomplete' => true]),
new \Bitrix\Main\ORM\Fields\IntegerField('RETURN_ID'),
new \Bitrix\Main\ORM\Fields\IntegerField('FILE_ID'), // b_file.ID
new \Bitrix\Main\ORM\Fields\DatetimeField('CREATED_AT'),
];
}
}
Security of request processing
It is critical to protect the AJAX handler from XSS and CSRF attacks. First, we check the session via bitrix_sessid_check. Second, we validate that the order belongs to the current user. Third, we filter uploaded files by type and size—only images up to 5 MB, others are rejected.
Integration with 1C
Integration with 1C is carried out via CommerceML or REST API. We configure automatic creation of return documents in 1C upon approval of the request. This eliminates double data entry and speeds up the return process.
Project scope and deliverables
| Phase | Activity | Responsible | Timeline |
|---|---|---|---|
| Analysis | Audit of current return business processes | Analyst | 1-3 days |
| Design | Agree on wizard logic and screens | Analyst + client | 2-5 days |
| Development | Backend API, wizard on React/Vue, integrations | Developer | 1-3 weeks |
| Testing | Unit tests, load testing, UAT | Tester | 3-5 days |
| Deployment | Deploy to production server | DevOps | 1 day |
| Training | Documentation, manager training | Analyst | up to 2 hours |
What's included
- Audit of the current return process and agreement on logic
- Development of wizard form with 4 steps (React/Vue)
- Server side: API for creation and statuses of returns
- Integration with email notifications (customer + manager)
- "My Returns" page in the personal account
- Documentation for each component
- Employee training (up to 2 hours)
- Technical support for 1 month after launch
Typical integration mistakes
- Forgetting to check the session in the AJAX handler—leads to XSS. - Not considering partial returns: you need to calculate already returned quantity. - Not checking file size during photo upload—files can exceed 5 MB.Estimated timelines
Full form with wizard and file upload—from 2 to 4 weeks. More complex integrations (1C, custom business processes)—up to 6 weeks. Cost is calculated individually. We will assess your project—get in touch.
Order a return form development today—get a free audit of current processes. We guarantee correct operation under high loads (1000+ returns per day) and compliance with 54-FZ for fiscalization. Contact us for a consultation—we will show a demo form.
Official documentation: Bitrix REST API for integrations.







