Woocommerce. Изменение логики расчетов стоимости товара.

от | Фев 16, 2021 | Блог | Нет комментариев

Доброго времени суток, друзья. В общем, смысл моего повествования изложен в заголовке. Для одного проекта возникла необходимость полностью поменять логику расчетов стоимости товаров в woocommerce. Компании для которой я реализовывал данный проект все расчеты ведутся через 1С версии 7 (Сееемь! Карл!) и эту логику нужно было использовать и в woocommerce.

Логика подсчета выглядит примерно таким образом. Есть некий товар и у этого товара есть БазоваяЦена, далее БЦ, (читай как себестоимость). Именно от этой стоимости нужно рассчитывать итоговую стоимость. На БЦ воздейстуют несколько модификаторов, которые ее меняют:

Наценка (Н) – модификатор, который увеличивает БЦ до РозничнойЦены (РЦ). В моем случае Н была 50% от БЦ.

Пользовательская Скидка (ПС) – модификатор, который уменьшает РЦ на определенный процент, который индивидуален для каждого пользователя. При регистрации на сайте пользователь получает скидку 3%. Изменять данную скидку может только менеджер магазина вручную. Максимально ПС может быть 15%.

Оптовая скидка (ОС) — модификатор, который уменьшает РЦ на определенный процент, который зависит от общей стоимости корзины. Например от 0р. до 2000р. скидка 0%, от 2000р. до 5000р. 3% и тд. Данная шкала скидок была предоставлена заказчиком.

ПС и ОС в общем результате складываются и уменьшают РЦ до Конечной Цены (КЦ).

Важное замечание!!! Поскольку скидки складываются, то общая скидка не должна быть больше 35%.

Перейдем к технической части.

Дисклеймер! Подробных гайдов как это сделать  не нашел, поэтому расскажу о своем пути решения данной задачи. Я не претендую на истину в последней инстанции. Если у вас есть вопросы или предложения добро пожаловать в комменты или на chernichko.work@mail.ru

Все будет реализовано в function.php без вмешательства в ядро WP и WC.

Для реализации кастомного расчета стоимости нам нужно будет использовать несколько хуков и фильтров woocommerce.

Итак, для начала нужно разобраться как работает расчет стоимости товара. Исходя из своего опыта я понял, что WC на каждом этапе подсчитывает все стоимости только исходя из его базовой цены. Например, если с помощью фильтра измените  стоимость товара на странице непосредственно товара, то это совсем не значит, что на этапе оформления заказа WC возьмет именно то, что вы изменили с помощью фильтра. Он опять будет отталкиваться от своей базовой цены. Именно из-за этого было решено описать функцию, которая бы производила расчеты стоимости конкретного товара и возвращала бы его в нужном виде (в виде числа, если мы собираемся что-то еще посчитать, или в виде текста со знаком валюты и отформатированном нужным образом). По поводу стоимости стоит заметить что функция может вернуть как и КЦ товара, так и РЦ товара (нам это будет необходимо). Данную функцию мы будем вызвать в нужных нам местах (при помощи фильтров и хуков). Я определил следующие места, где данная функция вызывается:

woocommerce_get_price_html / filter – фильтр, который позволяет изменить цену товара на странице товара (Single Product). Меняет только значение на экране, не меняя его по сути.

woocommerce_after_calculate_totals / filter — фильтр, который позволяет изменить итоговую сумму корзины товаров.

woocommerce_cart_totals_before_order_total / filter — фильтр, который позволяет изменить данные в таблице итоговой суммы заказов в корзине.

woocommerce_cart_item_price / filter – фильтр, который меняет стоимость товара в корзине.

woocommerce_cart_total / action – зацепка, с помощью которой мы можем пересчитать итоговую сумму корзины

woocommerce_checkout_create_order / action — зацепка, с помощью которой мы создаем заказ с новыми ценами

Итак. Сначала разберемся с функцией подсчтета.

Задача функции возвращать некую скалькулированную цену товара (БЦ, РЦ, КЦ) в необходимом формате (в формате float числа  или в формате отформатированной строки с денежным знаком). Поскольку функция всегда вызывается там, где мы можем получить ID товара, то обязательным параметром функции будет product_id. В качестве дополнительного необязательного параметра функции будет массив, с помощью которого функция будет определять формат числа и тип цены, который нам необходим.

В итоге описание функции будет примерно следующим

global_calc_price ($product_id, $attr)

$attr[

        'format'=>'html'            | html (string) - отформатированный вывод числа с денежным знаком

                                    | number (float) - Нецелочисленное значение

        'price_type'=> 'final'      | final     - КЦ

                                    | starting  - РЦ

                                    | base      - БЦ

    ]

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

Представляю листинг кода с комментариями

Листинг фнукции global_calc_price()

function global_calc_price ($product_id, $attr = array('format'=>'html', 'price_type'=>'final')){

    if (!isset($attr['format'])) $attr['format'] = 'html';
    if (!isset($attr['price_type'])) $attr['format'] = 'final';

    $sale_cat_ids = array(21,22,44,17,19,18,43,28,27,26,25,24); // id категорий товаров по которым нужна скидка
    $markup = 0.5;// % Наценки
    $user = wp_get_current_user(); // Получаем текущего пользователя
    $cart = WC()->cart; // Получаем объект корзины
    
    $product = wc_get_product($product_id); // Получаем товар по ID
    

    $base_price = $product->get_regular_price(); // Получаем базовую цену
    if ($attr['price_type'] == 'base') {
        switch ($attr['format']){
            case 'html'  : return number_format($base_price, 2, ',', ' ');
            case 'number': return $base_price;
            default      : return false;
        }
    }
    
    $starting_price = $base_price+$base_price*$markup; // Считаем розничную цену
    if ($attr['price_type'] == 'starting') {
        switch ($attr['format']){
            case 'html'  : return number_format($starting_price, 2, ',', ' ');
            case 'number': return $starting_price;
            default      : return false;
        }
    }
    
    
    $cart_total_price = (float) $cart->total; // общая стоимость корзины


    if ( has_term( $sale_cat_ids, 'product_cat' ,$product->get_id() )){ // если товар находится в скидочной категории
        if ($user->ID){
            $personal_discount = (int)get_the_author_meta('discount',$user->ID)/100;
        }else{
            $personal_discount = 0;
        }
        /*
         * Расчитываем оптовую скидку
         */
        
        if ($cart_total_price >= 0 && $cart_total_price < 2000){
            $wholesale_discount = 0;
        }elseif($cart_total_price >= 2000 && $cart_total_price < 5000){
            $wholesale_discount =  0.05;
        }elseif($cart_total_price >= 5000 && $cart_total_price < 10000){
            $wholesale_discount = 0.1;
        }elseif($cart_total_price >= 10000 && $cart_total_price < 20000){
            $wholesale_discount = 0.15;
        }elseif($cart_total_price >= 20000 && $cart_total_price < 35000){
            $wholesale_discount = 0.2;
        }elseif($cart_total_price >= 35000 && $cart_total_price < 40000){
            $wholesale_discount = 0.25;
        }elseif($cart_total_price >= 40000 && $cart_total_price < 100000){
            $wholesale_discount = 0.3;
        }elseif($cart_total_price >= 100000){
            $wholesale_discount = 0.3;
        }else{
            $wholesale_discount = 0;
        }
        /*
         * Расчитвыаем общую скидку total_discount. Ограничение скидки 30%
         */
        $total_discount = $wholesale_discount + $personal_discount;
        if ($total_discount >= 0.3) {
            $total_discount = 0.3;
        }
        $result = $starting_price - $total_discount*$starting_price;
    }else{
        $result = $starting_price;

    }
    if ($attr['price_type'] == 'final') {
        switch ($attr['format']){
            case 'html'  : return number_format($result, 2, ',', ' ');
            case 'number': return $result;
            default      : return false;
        }
    }


}