WordPress · 9 мин чтения

Вес заказа WooCommerce: как посчитать общий вес и передать его в доставку

Вес заказа WooCommerce складывается из веса каждой позиции, умноженного на количество, и дальше его читают тарифы доставки и плагины служб вроде СДЭК. Сам по себе магазин ничего не округляет и не добавляет за упаковку, поэтому цифра, которую получает служба доставки, почти всегда расходится с реальным весом посылки. Ниже я показываю, где лежит вес, как получить сумму для корзины и для готового заказа, как добавить вес упаковки и как использовать результат в расчёте доставки.

Где WooCommerce хранит вес товара и вариаций

Вес задаётся в карточке товара, на вкладке «Доставка» блока «Данные товара». Рядом лежат длина, ширина, высота и класс доставки. Для простого товара это одно поле. Для вариативного вес можно указать у каждой вариации отдельно, а можно оставить пустым: тогда вариация берёт вес родительского товара. Из-за этого в каталоге с сотнями вариаций легко получить ситуацию, когда у родителя стоит 0,3 кг, а реальные вариации весят по килограмму, и никто этого не замечает до первой жалобы от службы доставки.

В коде вес читается методом get_weight() у объекта товара. Метод возвращает строку, причём пустую, если вес не заполнен. Это важно: приведение пустой строки к числу даёт ноль, и заказ из десяти позиций без веса спокойно уходит в доставку как «нулевой».

Место Что там лежит Как достать
Карточка простого товара Вес в единице магазина $product->get_weight()
Вариация Свой вес или вес родителя, если поле пустое $variation->get_weight()
Корзина Сумма по позициям с учётом количества WC()->cart->get_cart_contents_weight()
Готовый заказ Отдельного поля общего веса нет, есть позиции цикл по $order->get_items()

Последняя строка таблицы это то, на чём спотыкаются чаще всего. У корзины готовый метод суммы есть, а у объекта WC_Order я такого метода не встречал, поэтому вес заказа приходится считать самому или сохранять в мету в момент оформления.

Единицы измерения: как не перепутать граммы и килограммы

Единица веса задана для всего магазина одна, в настройках товаров. Значение лежит в опции woocommerce_weight_unit, допустимы kg, g, lbs и oz. Если магазин настроен в граммах, а служба принимает килограммы (или наоборот), нужна конвертация. Для неё в WooCommerce есть функция wc_get_weight(): она принимает число, целевую единицу и, необязательно, исходную. Если исходную не указать, берётся единица магазина.

// вес корзины в килограммах, независимо от настройки магазина
$cart_kg = wc_get_weight( WC()->cart->get_cart_contents_weight(), 'kg' );

// вес в граммах для API, которое ждёт целое число
$cart_g = (int) round( wc_get_weight( WC()->cart->get_cart_contents_weight(), 'g' ) );

Я всегда конвертирую на выходе, а не меняю единицу магазина под конкретную службу. Единица магазина влияет на то, как вес показывается покупателю и как его вводит менеджер в админке, и переключать её ради интеграции значит заново перебивать все карточки.

Как посчитать общий вес корзины и готового заказа

Вес корзины

Пока заказа нет, всё считает корзина. Метод get_cart_contents_weight() перебирает позиции, берёт вес товара или вариации, умножает на количество и суммирует. Виртуальные товары без веса в сумму ничего не добавляют, что нас устраивает. Вызывать метод можно в хуках расчёта доставки и на странице чекаута.

Вес уже созданного заказа

Для заказа, который лежит в базе (например, чтобы отправить его в службу доставки из хука смены статуса), я считаю вес циклом по позициям. Обращаться к таблицам напрямую не стоит: на современных магазинах заказы живут в отдельных таблицах HPOS, поэтому работать надо через объект WC_Order.

function kd_order_weight_kg( WC_Order $order ) {
    $total = 0;
    foreach ( $order->get_items() as $item ) {
        $product = $item->get_product();
        if ( ! $product ) {
            continue; // товар удалён после оформления
        }
        $total += (float) $product->get_weight() * $item->get_quantity();
    }
    return wc_get_weight( $total, 'kg' );
}

Проверка if ( ! $product ) нужна не для красоты. Если товар удалили из каталога, get_product() вернёт false, и без проверки получится фатальная ошибка в момент, когда менеджер нажимает кнопку в старом заказе.

Есть и вторая особенность. Считая вес по текущим карточкам, вы получаете вес на сегодня, а не на момент покупки. Если в марте товар весил 400 г, а в июне вы поправили карточку на 450 г, пересчёт старого заказа даст другую цифру. Поэтому вес лучше зафиксировать в мете при оформлении:

add_action( 'woocommerce_checkout_create_order', function ( $order, $data ) {
    $kg = wc_get_weight( WC()->cart->get_cart_contents_weight(), 'kg' );
    $order->update_meta_data( '_kd_total_weight_kg', round( $kg, 3 ) );
}, 10, 2 );

Хук woocommerce_checkout_create_order получает объект заказа и данные формы, а сохранение произойдёт вместе с самим заказом, отдельный save() не нужен. Дальше вес читается через $order->get_meta( '_kd_total_weight_kg' ).

Бесплатный материал

🎁 Полезный скрипт в подарок

Подпишитесь на Telegram - пришлю готовый скрипт по этой теме.

Без спама. Отписка в 1 клик.

Вес упаковки и запас на погрешность

Служба доставки тарифицирует посылку целиком, а не только товар. Коробка, наполнитель и скотч добавляют вес, и для тяжёлых и мелких заказов эта добавка заметна. Базового поля «вес упаковки» в карточке нет, поэтому я добавляю его константой или опцией в той же функции расчёта. Типичная схема у меня такая: фиксированный вес коробки плюс небольшой процент на наполнитель.

function kd_package_weight_kg( $goods_kg ) {
    $box_kg = 0.25;                 // пустая коробка, взвесьте свою
    $filler = $goods_kg * 0.05;     // наполнитель, подберите по факту
    return round( $goods_kg + $box_kg + $filler, 3 );
}

Числа в примере мои рабочие заготовки, а не нормативы: коробку и наполнитель надо взвесить на своём складе. Разумно сверить расчётный вес с реальным на первых 20-30 отправках и поправить коэффициенты. Если расхождения идут в одну сторону (расчёт всегда меньше), служба будет присылать доплаты, и вы это увидите по первому же отчёту.

Как передать вес в расчёт доставки WooCommerce

Что умеют штатные методы

В WooCommerce три встроенных метода: Flat Rate, Free Shipping и Local Pickup. В формуле стоимости Flat Rate доступны подстановки [qty], [cost] и [fee], а общего веса там нет. Поэтому для тарифов по весу есть два пути: разнести товары по классам доставки (подходит, когда весовых групп две-три) или подключить плагин конкретной службы, который сам читает вес. Для тарифов по весу, которые вы определяете сами, остаётся фильтр.

Фильтр woocommerce_package_rates

Хук woocommerce_package_rates получает массив уже рассчитанных тарифов и пакет доставки, и возвращает массив, который увидит покупатель. На нём удобно делать правила по весу. Например, убрать бесплатную доставку для тяжёлых заказов и добавить надбавку за вес сверх порога:

add_filter( 'woocommerce_package_rates', function ( $rates, $package ) {
    $kg = wc_get_weight( WC()->cart->get_cart_contents_weight(), 'kg' );

    foreach ( $rates as $rate_id => $rate ) {
        if ( $kg > 30 && 'free_shipping' === $rate->get_method_id() ) {
            unset( $rates[ $rate_id ] );
        }
        if ( $kg > 10 && 'flat_rate' === $rate->get_method_id() ) {
            $rate->set_cost( $rate->get_cost() + ( $kg - 10 ) * 20 );
        }
    }
    return $rates;
}, 10, 2 );

Пороги 30 и 10 кг и ставка 20 ₽ за каждый лишний килограмм здесь условные, подставьте свои. Метод get_method_id() возвращает тип метода (flat_rate, free_shipping), а ключ массива содержит ещё и идентификатор экземпляра в зоне. Если сравнивать ключ целиком, правило сработает только в одной зоне.

Отдельное предупреждение про кэш. WooCommerce кэширует рассчитанные тарифы для пакета, и хэш пакета зависит от содержимого корзины и адреса. Вес меняется вместе с содержимым, так что обычно всё обновляется само, но если вы берёте в расчёт что-то внешнее (вес упаковки из опции, курс), после изменения этих значений тарифы в тестовой корзине надо перепроверять, а не доверять первой же цифре.

Вес заказа для СДЭК и других служб доставки

Когда нужна доставка СДЭК, я использую официальный плагин CDEKDelivery с wordpress.org, а не пишу интеграцию с нуля. Он берёт вес из карточек товаров, поэтому главная работа на моей стороне лежит в данных: у каждого товара и вариации должен стоять реальный вес. Перед запуском я прохожу каталог и ищу пустые значения, а также нули и подозрительно круглые цифры вроде «1» у товаров, которые явно легче или тяжелее. Как плагин поведёт себя с пустым весом на вашей версии, смотрите в его настройках и в тестовом заказе, не полагаясь на мои слова.

Для собственных интеграций (передача заказа в службу по API, выгрузка в таблицу склада, уведомление в Telegram) схема другая. Вес берётся из зафиксированной меты _kd_total_weight_kg, конвертируется под формат получателя и уходит в запросе. Для службы, которая ждёт граммы целым числом, я округляю и отдельно проверяю, что результат не меньше одного грамма: нулевой вес многие API отклоняют. Если обмен идёт по событиям, например при переходе заказа в статус «В обработке», подвешиваюсь к хуку woocommerce_order_status_processing, он получает id заказа и сам объект.

Перед передачей я также сверяю, не выросла ли цифра после упаковки. Покупателю в чекауте показывается вес товаров, а службе уходит вес с упаковкой, и это две разные величины, которые не нужно смешивать в одной переменной.

Если магазин ещё только планируется и вес, габариты и тарифы нужно заложить в архитектуру сразу, проще заказать разработку интернет-магазина под ключ от 80 000 ₽, чем потом переписывать каталог и чекаут под доставку.

Типичные ошибки при расчёте веса

  • Пустой вес у вариаций. Вариация наследует вес родителя, и если родитель заполнен «для порядка», все варианты считаются одинаково тяжёлыми.
  • Единицы не те. Вес 0,5 внесён как «500» при единице kg. Заказ из двух позиций получается весом в тонну, и служба доставки отказывается его считать.
  • Вес не умножается на количество. В самописном коде суммируют get_weight() без количества, это встречается чаще, чем хотелось бы.
  • Пересчёт старых заказов по новым карточкам. Решается фиксацией веса в мете при оформлении.
  • Виртуальные и скачиваемые товары. Их вес должен быть пустым. Если в карточке остался вес, он попадёт в сумму.
  • Блочный чекаут. На магазинах, запущенных после версии 8.3, корзина и чекаут по умолчанию блочные, а не шорткодовые. Хуки расчёта доставки при этом работают, но код, который правит поля через woocommerce_checkout_fields, на блочном чекауте может не сработать, и сначала нужно выяснить, какой чекаут стоит.

Для проверки я завожу в тестовой среде три заказа: один лёгкий, один на границе порога и один сильно тяжёлый, с вариативным товаром в каждом. Если вес на странице корзины, в тарифе и в мете заказа совпадает во всех трёх, схема собрана верно.

WooCommerce и плагин CDEKDelivery регулярно обновляются, поэтому перед внедрением я сверяюсь с актуальной документацией.

Частые вопросы

Есть ли в WooCommerce готовый метод для общего веса заказа?

Для корзины есть: WC()->cart->get_cart_contents_weight(). Для уже созданного заказа отдельного метода я не использую, считаю циклом по позициям или читаю вес, сохранённый в мету при оформлении.

Что будет, если у товара не указан вес?

Метод get_weight() вернёт пустую строку, и в сумму такая позиция даст ноль. Заказ уйдёт в доставку легче, чем есть на самом деле. Поэтому перед запуском нужно пройти каталог и заполнить вес у всех физических товаров.

Как учесть вес упаковки?

Отдельного поля в карточке для этого нет. Я добавляю вес коробки константой и процент на наполнитель в функции расчёта, а коэффициенты уточняю по первым реальным отправкам.

Можно ли сделать тарифы по весу без кода?

Частично. Если весовых групп мало, помогают классы доставки в Flat Rate. Для плавной шкалы по килограммам нужен либо плагин службы доставки, который сам считает по весу, либо фильтр woocommerce_package_rates, как в примере выше.

Есть задача?

Обсудим в мессенджере

Расскажите, что нужно сделать — отвечу в течение 4 часов в рабочее время. Первая консультация бесплатно.

Продолжая пользование настоящим сайтом Вы выражаете своё согласие на обработку Ваших персональных данных (файлов куки) с использованием Yandex.Metrika.
Понятно