Virtuemart дробное количество товаров

Сформулируем задачу:
Нам нужно, чтобы скрипт интуитивно добавлял в корзину и дробное, и целое число товаров в зависимости от того, как установлена цена: за штуку (Integer) или за метр квадратный (Float).

Потрогать решение этой задачи (Joomla 2.5 + Virtuemart 2) можно на этом сайте.

Для Opencart(OcStore) решение смотрите здесь.

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

Тем, кто хочет получить готовое решение в коробочке с бантиком, следует написать нам письмо.

В любом случае следует помнить всегда, что содержимое файлов Virtuemart (не всех конечно) меняется от версии к версии. Даже в рамках версии 2.х.х появляются новые классы и функции, поэтому теряет всякий смысл описывать подробно в каком файле и в какой строке менять то на это. Как говорится, "вскрытие покажет..."

В стандартной поставке компонент Virtuemart настроен на работу только с целым количеством товаров – "штуками". Очень часто магазины продают товары дробным количеством: метры квадратные, килограммы, литры. При этом цена указывается за единицу измерения, например за 1 квадратный метр или за 1 килограмм. Например магазину нужно продать остаток линолеума, площадь которого составляет 3,27 кв.м. Стандартный виртумарт не понимает количество товара 3,27. Бывают и более сложные ситуации с количеством товаров, например продажа керамической плитки. Базовая плитка продаётся квадратными метрами (и цена указывается за 1кв.м.), а декоративные элементы (вставки, бордюры, плинтусы) продаются поштучно и цена указывается за 1 штуку. Но вся прелесть ситуации в том, что в отличие от линолеума, вы не можете дробить каждую плитку! Другими словами, вы всегда продаёте плитку только штуками: и базовую, и декоры. Такая ситуация проиллюстрирована на Картинке-1, здесь пользователю разрешено заказывать только целое число плиток, так как нельзя продавать половинку или четвертинку плитки. Продать-то можно, но... продавать нельзя. При этом расчет количества заказываемого товара ведется в метрах квадратных для базовой плитки, и в штуках для декоративных элементов. На Картинке-2 показана корзина с добавленными товарами

 

картинка1
Картинка-1.
Заказать можно только целое количество товара (плиток). Но в корзину пойдет дробное число квадратных метров, если цена указана за кв.м.
 картинка2
Картинка-2.
В корзине плитка и в штуках, и в квадратных метрах. Обратите внимание, что мы удалили возможность изменения количества товара на странице "Корзина". Для изменения количества нужно перейти по ссылке в карточку товара.

 

Вот наш план действий (упрощенный)

  1. Давайте скажем Виртумарту, что у нас бывает несколько единиц измерений товаров и они всегда дробные, даже те, которые целые. Ну и пусть, главное чтобы работало. Для этого:
    – Можно добавить новое поле в таблицу #__virtuemart_products для хранения единиц измерения. Но мы для быстроты и удобства использовали для этой цели уже существующее поле product_mpn, которое обычно никак не используется и, главное, оно отображается в админке в карточке товара на первой вкладе "Информация". В это поле и будем писать штуки, метры, литры, килограммы и так далее. Это показано на картинках 3 и 4. Умный читатель скажет: – Ха, так в этой таблице уже есть поле product_unit для хранения единиц товаров и оно легко заполняется из админки в той же карточке товара на вкладке "Габариты/Вес". Мы не против, – используйте на здоровье это поле. Мы исходили из практических соображений.

     

    картинка3
    Картинка-3.
    картинка2
    Картинка-4.

    – Теперь меняем тип данных с целых чисел на дробные ( float 10,4 ) в поле product_quantity в таблице #__virtuemart_order_items. Для хранения дробных чисел не рекомендуем использовать тип данных DECIMAL, так как не только по нашему мнению это не "бухгалтерский" формат.


     

  2. Решаем задачу "Плюсик" & "Минусик". Это две смешные кнопочки рядом с полем ввода количества товаров. При нажатии на "плюсик" количество увеличивается на единицу. При нажатии на "минусик" – уменьшается. Смешно, правда? Нам быстрее ввести число 100, чем сто раз нажимать на кнопочку. Поэтому мы пошли радикальным путем: убрали из кода страницы обе кнопки. НО! Сам Javascript, который обрабатывает события для этих кнопок мы модифицировали и использовали его для трех целей: 1. Проверка введенного числа только на ЦЕЛОЕ (никаких дробных "плиток" покупатель вводить не должен), ибо только целые плиточки продаёт наш магазин. 2. Копирование введённого целого значения в "целевое поле", если цена указана за "штуку"; или ввод в "целевое поле" рассчитанной "на лету" суммарной площади, если цена указана за квадратный метр; 3. Округление дробного значения до четвёртого знака после запятой.
    Да, для этого нам пришлось добавить на страницу дополнительное "целевое поле" input, закрытое от редактирования параметром readonly. Именно из этого поля POST отправляет данные на сервер.
    Если ваш магазин продает только дробные товары, то задача упрощается. Если вам нужны на странице "Плюсик" & "Минусик", то придется изменить значение переменной $step с 1 на дробное, например, на 0.01

    Задача пункта-2 решается в файлах: site.com/components/com_virtuemart/views/productdetails/tmpl/default.php и default_addtocart.php

    Для удобства работы мы объединили коды этих двух файлов в один. То есть ушли от конструкции echo $this->loadTemplate('addtocart');

    Нажмите

    Это только блок добавления товара в корзину

     <div class="addtocart-area-detail">
    <form method="post" class="product js-recalculate" action="<?php echo JRoute::_ ('index.php',false); ?>">
    <?php

    if (!VmConfig::get('use_as_catalog', 0) ) { ?>
    <div class="addtocart-bar">
    <?php
    if ($this->product->product_mpn == "м2") {
    $csqr = $calcsqr;
    } else {
    $csqr = 0;
    }
    ?>
    <label style="font-size:10px;">Здесь Вы можете указать только целое число плиток (комплектов).<br />
    Если цена указана за 1кв.м., то автоматически будет рассчитана площадь для заказа.
    <input id="<?php echo $this->product->product_sku; ?>" type="text" class="prokl" name="prokl" onblur="check(this,id,<?php echo $csqr;?>)" value="1"/>
    </label>
    <div class="sqrqndiv">
    <b>Будет заказано:</b><br />
    <input id="<?php echo $this->product->product_sku . 'q'; ?>" class="quantity-input js-recalculate" type="text" name="quantity[]" value="" readonly />
    <?php if ($this->product->product_mpn == "м2") { ?>
    <strong> м2</strong>
    <?php } elseif ($this->product->product_mpn == "шт.") { ?>
    <strong> шт.</strong>
    <?php } elseif ($this->product->product_mpn == "комплект"){ ?>
    <strong> компл.</strong>
    <?php } ?>
    </div>

    <span class="addtocart-button">
    <?php echo shopFunctionsF::getAddToCartButton ($this->product->orderable); ?>
    </span>
    <noscript><input type="hidden" name="task" value="add"/></noscript>
    </div>
    <script type="text/javascript">
    function check(obj,idme,csqr) {
    remainder=obj.value % 1;
    qnt=obj.value;
    tarid = idme +'q';

    if (remainder != 0) {
    alert('Вы можете указать только целое количество плитки. Автоматически рассчитается итоговая площадь для заказа.');
    obj.value = '1';
    input = document.getElementById(tarid);
    input.value = '';
    return false;
    }
    input = document.getElementById(tarid);
    if (csqr == 0) {
    input.value = qnt;
    } else {
    input.value = Math.round(csqr * qnt * 10000) / 10000 ; // Округление до четвертого знака после запятой
    }

    return true;
    }
    </script>

    <?php } ?>

    <input type="hidden" name="option" value="com_virtuemart"/>
    <input type="hidden" name="view" value="cart"/>
    <input type="hidden" name="virtuemart_product_id[]" value="<?php echo $this->product->virtuemart_product_id; ?>"/>
    <input type="hidden" class="pname" value="<?php echo htmlentities($this->product->product_name, ENT_QUOTES, 'utf-8') ?>"/>
    <?php
    $itemId=vRequest::getInt('Itemid',false);
    if($itemId){
    echo '<input type="hidden" name="Itemid" value="'.$itemId.'"/>';
    } ?>
    </form>
    </div>

     

  3. Добавление в корзину и апдейт дробного числа товаров.
    Править нужно четыре функции в файле:

    site.com/components/com_virtuemart/helpers/cart.php


    1) public function add($virtuemart_product_ids=null,&$errorMsg='')

    в строке: $quantityPost = (int) $post['quantity'][$p_key];
    меняем на: $quantityPost = (float) $post['quantity'][$p_key];

    после строки: $product -> product_sku = $tmpProduct -> product_sku;
    Добавляем строку: $product -> product_mpn = $tmpProduct -> product_mpn;

    в строке: $product->quantity = (int)$quantityPost;
    меняем на: $product->quantity = (float)$quantityPost;


    2) public function updateProductCart($cart_virtuemart_product_id=0,$quantity = null)

    в строке: if ($quantity === null) $quantity = vRequest::getInt('quantity');
    меняем на: if ($quantity === null) $quantity = vRequest::getFloat('quantity');

    3) private function checkForQuantities($product, &$quantity=0,&$errorMsg ='')

    в строке: if ($quantity < 1) {
    меняем на: if ($quantity < 0.01) {   // Здесь вы можете указать свой минимальный порог

    4) function prepareAjaxData($checkAutomaticSelected=false)

                находим: $this->data->products[$i]['product_sku'] = $product->product_sku;
    ниже добавляем: $this->data->products[$i]['product_mpn'] = $product->product_mpn;




  4. Вывод дробного числа товаров на странице "Корзина".
    Редактируем файл: site.com/components/com_virtuemart/views/cart/tmpl/default_pricelist.php

    После редактирования функции function prepareAjaxData() в объект $cart попадает переменная product_mpn.
    И в файле default_pricelist.php она становится доступна:

    foreach ($this->cart->products as $pkey => $prow) {
    .......
    echo $prow->product_mpn;
    .......
    }



  5. Выводим единицы измерения товаров в админке на странице Заказа.
    Для этого нужно:
    добавить в таблицу базы данных #__virtuemart_order_items поле, например, order_item_mpn varchar(255)
    и отредактировать три файла:
    1) site.com/administrator/components/com_virtuemart/tables/order_items.php
    после строки:
    var $order_item_sku = NULL;
    добавить:
    var $order_item_mpn = NULL;

    2) site.com/administrator/components/com_virtuemart/models/orders.php
    в функцию (примерно 1145 строка) private function _createOrderLines($_id, $_cart)
    после строки:
    $_orderItems->order_item_sku = $_prod->product_sku;
    добавить:
    $_orderItems->order_item_mpn = $_prod->product_mpn;
     
    в функцию (примерно 173 строка) public function getOrder($virtuemart_order_id) в запросе
    (примерно 201 строка) $q = 'SELECT virtuemart_order_item_id, product_quantity,....
    добавить
    $q = 'SELECT virtuemart_order_item_id, product_quantity,order_item_mpn,....

    3) site.com/administrator/components/com_virtuemart/views/orders/tmpl/order.php
    добавить заголовок колонки в таблицу (примерно 430 строка)
    <th class="title" width="47" align="left">Ед.изм.</th>
    и в цикл вывода списка товаров добавить колонку (примерно 500 строка):
    <td><?php echo $item->order_item_mpn; ?></td>

    После добавления колонки не лишним будет подправить объединение ячеек в нижних строках таблицы.
    У нас ниже расположена только одна строка таблицы с итоговыми значениями.
    Поэтому в коде правим <td colspan="4"> на <td colspan="5">, это примерно 566 строка.




  6. Осталось добавить product_mpn в тело письма покупателю и в счёт. Как вы уже догадались, нужно править файл шаблона.
    А именно
    site.com/components/com_virtuemart/views/invoice/tmpl/invoice_items.php


    В нужное место таблицы вставляем заголовок столбца (примерно 40 сторока):
    <td style="border-collapse: collapse; border: 1px dotted black;" align="left" width="5%"><strong>Ед.изм.</strong></td>

    и сам столбец (в цикле foreach):
                <td style="border-collapse: collapse; border: 1px dotted black;" align="left" >
                    <?php echo $item->order_item_mpn; //Единицы измерения товара ?>
                </td>