Drupal → Цены в Commerce

17.05.2014

Работа с ценами в Commerce ведётся с помощью пяти модулей:

Price — реализует поле типа Price.
Product Pricing + Product Pricing UI — позволяют динамически менять цену товаров.
Tax + Tax UI — рассчитывают различные виды налогов.

Поле Price по умолчанию имеют следующие сущности: Product (Price), Line item (Unit price, Total), Order (Order total). Схематично здесь.

Каждое значение поля Price состоит из:
amount — цена в копейках или сумма цен всех компонентов, если они есть.
currency_code — код валюты.
data — массив с компонентами цены (price components).

Вся работа с ценами ведётся в копейках. Для правильного перерасчёта в у.е. и обратно есть функции commerce_currency_amount_to_decimal() и commerce_currency_decimal_to_amount().

Модули и администраторы сайта могут динамически изменять цены товаров и заказов. Каждое изменение цены записывается в так называемый price component (компонент цены) — data['components'] из которых и высчитывается главный amount (итоговая цена):

Каждый price component имеет свой тип (поле name в массиве). По умолчанию в Commerce три типа компонентов цены:
base_price — базовая цена
discount — скидка
fee — наценка

Модули Tax + Tax UI позволяют из админки добавлять свои типы компонентов цены. Другие модули так же могут создавать типы price components с помощью хука hook_commerce_price_component_type_info().

Price components добавляются к цене с помощью commerce_price_component_add(). Изменять цену в обход этой функции не рекомендуется.

Цена товара рассчитывается в момент вывода (см. commerce_product_pricing_commerce_price_field_formatter_prepare_view()). В базе хранится только цена, указанная при создании товара. Отсюда сложности с сортировкой и фильтром по выводимой цене, а не по реальной.

Цена показываемого товара рассчитывается на основе "виртуального" line item-а. "Виртуального" — потому что созданный line item не сохраняется в базе и у него нет line_item_id (см. commerce_product_calculate_sell_price()).

Цена товара в корзине (а точнее цена line item-а) высчитывается при добавлении его в корзину (см. commerce_cart_add_to_cart_form_submit()). Следует помнить, что цена высчитывается перед сохранением line item-а в базу. В дальнейшем цена line item-ов в корзине перерассчитывается каждые 30 секунд (по умолчанию) и уже на основе "реальных" line item-ов, которые есть в базе и имеют line_item_id.

Цена корзины рассчитывается после добавления товара (line item-а) в корзину, после сохранения корзины и каждые 30 секунд.

На страницах /cart и /checkout* цены перерассчитываются каждое открытие.

Для гурманов — как работает вывод цены:

Вывод цены товара

commerce_product_reference_field_extra_fields()
commerce_product_reference_entity_view()
└ field_view_field('commerce_product', $product, 'имя_поля_цены', ...)
  ├ commerce_price_field_formatter_prepare_view()
  │ └ hook_commerce_price_field_formatter_prepare_view()
  │   └ commerce_product_pricing_commerce_price_field_formatter_prepare_view()
  │     └ commerce_product_calculate_sell_price($product)
  │       ├ commerce_product_line_item_new($product)
  │       │ ├ entity_create('commerce_line_item', ...)
  │       │ └ commerce_product_line_item_populate($line_item, $product)
  │       │   └ commerce_price_component_add(..., 'base_price', ...)
  │       ├ drupal_alter('commerce_product_calculate_sell_price_line_item', $line_item)
  │       └ rules_invoke_event('commerce_product_calculate_sell_price', $line_item);
  │         ├ пользовательский rules + commerce_price_component_add()
  │         ├ пользовательский rules + commerce_price_component_add()
  │         └ ...
  └ commerce_price_field_formatter_view()
    └ commerce_currency_format()
      или
    ├ commerce_price_component_total()
    ├ drupal_alter('commerce_price_formatted_components', $components, $item, $entity);
    └ theme('commerce_price_formatted_components', array('components' => $components, 'price' => $item))

Форма добавление товара в корзину

commerce_cart_field_formatter_view()
├ commerce_product_reference_default_product($products)
└ commerce_product_line_item_new($default_product, ...)
  ├ entity_create('commerce_line_item', ...)
  └ commerce_product_line_item_populate($line_item, $product)
    └ commerce_price_component_add(..., 'base_price', ...)
commerce_cart_field_attach_view_alter()
└ drupal_get_form('commerce_cart_add_to_cart_form_...', $line_item, ...)
  └ commerce_cart_add_to_cart_form($form, &$form_state, $line_item, ...)
    ├ field_attach_form('commerce_line_item', $form_state['line_item'], $form['line_item_fields'], ...)
    └ commerce_product_calculate_sell_price($form_state['default_product'])
      ├ commerce_product_line_item_new($product)
      │ ├ entity_create('commerce_line_item', ...)
      │ └ commerce_product_line_item_populate($line_item, $product)
      │   └ commerce_price_component_add(..., 'base_price', ...)
      ├ drupal_alter('commerce_product_calculate_sell_price_line_item', $line_item)
      └ rules_invoke_event('commerce_product_calculate_sell_price', $line_item);
        ├ пользовательский rules + commerce_price_component_add()
        ├ пользовательский rules + commerce_price_component_add()
        └ ...

Добавление товара в корзину

commerce_cart_add_to_cart_form($form, &$form_state, $line_item, ...)
├ field_attach_form('commerce_line_item', $form_state['line_item'], $form['line_item_fields'], ...)
└ commerce_product_calculate_sell_price($form_state['default_product'])
  ├ commerce_product_line_item_new($product)
  │ ├ entity_create('commerce_line_item', ...)
  │ └ commerce_product_line_item_populate($line_item, $product)
  │   └ commerce_price_component_add(..., 'base_price', ...)
  ├ drupal_alter('commerce_product_calculate_sell_price_line_item', $line_item)
  └ rules_invoke_event('commerce_product_calculate_sell_price', $line_item);
    ├ пользовательский rules + commerce_price_component_add()
    ├ пользовательский rules + commerce_price_component_add()
    └ ...
commerce_cart_add_to_cart_form_submit()
├ commerce_product_line_item_new($product, ...)
│ ├ entity_create('commerce_line_item', ...)
│ └ commerce_product_line_item_populate($line_item, $product)
│   └ commerce_price_component_add(..., 'base_price', ...)
├ drupal_alter('commerce_product_calculate_sell_price_line_item', $line_item);
├ field_attach_submit('commerce_line_item', $line_item, $form['line_item_fields'], $form_state)
├ rules_invoke_event('commerce_product_calculate_sell_price', $line_item);
│ ├ пользовательский rules + commerce_price_component_add()
│ ├ пользовательский rules + commerce_price_component_add()
│ └ ...
└ commerce_cart_product_add($form_state['values']['uid'], $line_item, ...)
  ├ commerce_cart_order_new($uid)
  ├ rules_invoke_all('commerce_cart_product_prepare', $order, $product, ...);
  ├ commerce_line_item_save($line_item)
  ├ commerce_order_save($order)
  │ └ commerce_order_calculate_total($order)
  └ rules_invoke_all('commerce_cart_product_add', $order, $product, $quantity, $line_item);

Обновление цены корзины и её содержимого

commerce_order_load()
└ hook_commerce_order_load()
  └ commerce_cart_commerce_order_load($orders)
    └ commerce_cart_order_refresh($order)
      ├ commerce_product_line_item_populate()
      ├ rules_invoke_event('commerce_product_calculate_sell_price', $cloned_line_item);
      │ ├ пользовательский rules + commerce_price_component_add()
      │ ├ пользовательский rules + commerce_price_component_add()
      │ └ ...
      ├ hook_commerce_cart_line_item_refresh()
      ├ commerce_line_item_save()
      ├ hook_commerce_cart_order_refresh()
      └ commerce_order_save()
        └ commerce_order_calculate_total($order)

Бросается в глаза, что при выводе цены товара вместе с формой добавления товара в корзину, виртуальных line item-ов создаётся аж три штуки, а цена высчитывается два раза. Помните это, создавая правила для расчёта цены — не забывайте про кеширование и всё такое.

Для дальнейшего изучения будет полезен доклад о скидках и материалы по тегу работа с ценой.

Написанное актуально для
Commerce 1
Похожие записи

Комментарии

Андрей
17.05.2014, 23:20

Хорошую работу проделал, спасибо
Xandeadx, скажи, а зачем так углублялся в дебри комерца? Спортивный интерес или реально нужда по проекту?

Xandeadx, спасибо огромное. Понятно и очень наглядно. В свое время пришлось разбираться с формированием цен в Commerce практически методом тыка - на beta-версиях, почти без документации и с кучей глюков. Такая полезная статья тогда сэкономила бы несколько дней...

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

Гость
14.11.2014, 19:40

На одном из буржуйских форумов нашел решение для скидки на конретный товар. Делается просто, например так:

function my_module_commerce_product_calculate_sell_price_line_item_alter($line_item){
    $price = 100; //1 dollar
    $line_item->commerce_unit_price[LANGUAGE_NONE]['0']['amount']=$price;
    $line_item->commerce_unit_price[LANGUAGE_NONE]['0']['data']['components']['0']['price']['amount']=$price;
}

//alter the price in cart & order
function my_module_commerce_cart_line_item_refresh($line_item, $order_wrapper){
    $price = 100; //1 dollar
    $line_item->commerce_unit_price[LANGUAGE_NONE]['0']['amount']=$price;
    $line_item->commerce_unit_price[LANGUAGE_NONE]['0']['data']['components']['0']['price']['amount']=$price; //alter the base_price component
}

Работает!
Как по вашему, какие подводные камни в таком решении?

Гость
20.12.2014, 10:18

Интересно, а можно как-то завести еще один тип цены - себестоимость, чтобы вести складской учет? Или вообще делать "покупки" в минус - оприходование (в том числе с разными ценами закупа для одной позиции в зависимости от поставки), а потом уже реализацию со склада оприходованных товаров (по новой общей цене)? Мне кажется это то, чего не хватает инет-магазинам для полного контроля над торговлей.

Гость
20.12.2014, 10:21

К предыдущему комменту: оприходование - это прям и есть заказ наоборот, только вместо скидок - разные закупочные цены, и товары не уменьшаются, а увеличиваются, также заказчик и заказ становятся поставщиком и поставкой. Может уже есть что-то подобное?

Пожалуйста, подскажите, каким образом можно изменить (программно) порядок вывода Product variation во view Display Products. По умолчанию отображается product variation с min весом, нужно - с минимальной ценой.

Добавить комментарий