Yandex Metrika
sanches.free

Обновление свойства заказа по городу пользователя (инфоблок, Sale D7)

Задача

Нужно периодически пройти по заказам и заполнить свойство оформления (в исходной заметке — «страна») по связке пользователь заказа → город в справочнике инфоблока → верхний уровень дерева разделов. Типичный сценарий: справочник городов лежит в инфоблоке с иерархией «страна → регион → город», у пользователя в поле UF_CITY хранится элемент города.

Ограничения и безопасность

  • Запускайте только из CLI (php script.php или cron), чтобы случайный веб-запрос не включил массовую перезапись.
  • Сначала проверка на копии БД и на узком фильтре по датам или ID заказов.
  • Константа COUNTRY_IBLOCK_ID и ID свойства заказа должны быть заменены на ваши значения из административки.

Современная схема на D7

Список заказов без legacy CSaleOrder удобнее строить через OrderTable, объект заказа — \Bitrix\Sale\Order::load(). Значение свойства меняется через коллекцию getPropertyCollection(); если строки значения для данного свойства ещё нет, создаём элемент методом createItem() с ключом ORDER_PROPS_ID и сохраняем заказ.

if (PHP_SAPI !== 'cli') {
    fwrite(STDERR, "CLI only\n");
    exit(1);
}

$_SERVER['DOCUMENT_ROOT'] = realpath(__DIR__ . '/..');

define('NO_KEEP_STATISTIC', true);
define('NOT_CHECK_PERMISSIONS', true);

require $_SERVER['DOCUMENT_ROOT'] . '/bitrix/modules/main/include/prolog_before.php';

use Bitrix\Main\Loader;
use Bitrix\Sale\Order;
use Bitrix\Sale\Internals\OrderTable;

Loader::includeModule('sale');
Loader::includeModule('iblock');

if (!defined('COUNTRY_IBLOCK_ID')) {
    throw new RuntimeException('Define COUNTRY_IBLOCK_ID');
}

final class OrdersCountrySync
{
    private int $countryPropertyId;

    /** корневой раздел ID => ожидаемое имя страны (контроль по белому списку) */
    private static array $countries = [
        26  => 'Россия',
        8   => 'Казахстан',
        1   => 'Беларусь',
        105 => 'Украина',
    ];

    public function __construct(int $countryOrderPropertyId)
    {
        $this->countryPropertyId = $countryOrderPropertyId;
    }

    public function run(): void
    {
        $cursor = OrderTable::getList([
            'select' => ['ID'],
            'order'  => ['ID' => 'DESC'],
        ]);
        while ($row = $cursor->fetch()) {
            $orderId = (int) $row['ID'];
            try {
                $this->syncOrder($orderId);
            } catch (Throwable $e) {
                fwrite(STDERR, $e->getMessage() . PHP_EOL);
            }
            echo 'Done: ' . $orderId . PHP_EOL;
        }
    }

    private function syncOrder(int $orderId): void
    {
        $orderEntity = Order::load($orderId);
        $userId = (int) $orderEntity->getUserId();

        $userRow = \CUser::GetByID($userId)->Fetch();
        if (!$userRow) {
            throw new RuntimeException('No user ' . $userId . ' for order ' . $orderId);
        }

        $cityElementId = (int) ($userRow['UF_CITY'] ?? 0);
        $countryName = $this->resolveCountryName($cityElementId);

        $props = $orderEntity->getPropertyCollection();
        $cell = $props->getItemByOrderPropertyId($this->countryPropertyId);

        if (!$cell) {
            $cell = $props->createItem([
                'ORDER_PROPS_ID' => $this->countryPropertyId,
            ]);
        }
        $cell->setValue($countryName);

        $save = $orderEntity->save();
        if (!$save->isSuccess()) {
            throw new RuntimeException(
                'Order ' . $orderId . ': ' . implode('; ', $save->getErrorMessages())
            );
        }
    }

    private function resolveCountryName(int $cityElementId): string
    {
        if ($cityElementId <= 0) {
            throw new RuntimeException('Empty UF_CITY');
        }

        $rs = \CIBlockElement::GetList(
            [],
            [
                'IBLOCK_ID'             => COUNTRY_IBLOCK_ID,
                'ID'                    => $cityElementId,
                'CHECK_PERMISSIONS'     => 'N',
                'MIN_PERMISSION'        => 'R',
            ],
            false,
            false,
            ['ID', 'IBLOCK_SECTION_ID', 'NAME']
        );
        $location = $rs->Fetch();
        if (!$location) {
            throw new RuntimeException('No location element ' . $cityElementId);
        }

        $nav = \CIBlockSection::GetNavChain(
            COUNTRY_IBLOCK_ID,
            (int) $location['IBLOCK_SECTION_ID']
        );
        $top = $nav->Fetch();
        if (!$top) {
            throw new RuntimeException('No section chain for element ' . $cityElementId);
        }

        $topId = (int) $top['ID'];
        if (!array_key_exists($topId, self::$countries)) {
            throw new RuntimeException('Unknown country section ID ' . $topId);
        }

        return (string) $top['NAME'];
    }
}

($sync = new OrdersCountrySync(10))->run();

Замечания по логике

  • Массив $countries в примере дублирует проверку из оригинала: в него попадают только «разрешённые» ID корневых разделов. Если справочник стабилен, допустимо убрать белый список и брать имя верхнего раздела как есть.
  • GetNavChain возвращает цепочку от корня к текущему разделу; первый Fetch() даёт корень — для схемы «страна сверху» этого достаточно.
  • Чтобы не грузить тысячи заказов за один проход, добавьте 'filter' по диапазону DATE_INSERT или обрабатывайте только заказы без заполненного свойства.

Связь со старым API

Скрипт с CSaleOrder, CSaleOrderPropsValue::Update/Add и тем же деревом городов эквивалентен по смыслу: переписанный вариант лишь выносит обход заказов и сохранение в API модуля Sale D7.

Не хотите копаться сами?

Починю за 1-3 дня. Без предоплаты — оплата по результату.

15+ лет опыта с 1С-Битрикс · Без предоплаты · 7 дней гарантии