Пожалуй, даже не бета, а альфа, т.к. наверняка традиционно куча ошибок .
Вводная:
========
В ShopCMS нет раздельного количественного учета по вариантам.
Например, товар Сапоги-скороходы может быть синим или красным, а также разных размеров - 40, 42 и 45.
Однако, в ShopCMS эти шесть разных товаров (два варианта цвета на три варианта размера)
учитываются скопом как один товар Сапоги-скороходы.
Задача модуля - раздельный учет таких товаров.
Есть подобный модуль от namer-а, но там учет только по ОДНОЙ характеристике товара, т.е. если надо учитывать
только по размеру или только по цвету, то он подойдет. А вот если сразу по двум (или более) характеристикам,
то увы.
Идея:
-----
Товару (если это надо) задается Несколько разных наборов (наборов!) вариантов характеристик и для каждого набора
цена и количество на складе.
Для вышеупомянутых Сапогов-скороходов это будет, к примеру:
(Синий/40), 123руб, 10шт;
(Красный/40), 125руб, 15шт;
(Синий/45), 140руб, 5шт;
(Красный/45), 150руб, 4шт;
(Синий/42), 154руб, 4шт;
Предполагается, что для товара количество характеристик в наборе неизменно. Т.е. если есть наборы вариантов
(Синий/40) и (Синий/45), то набора (Красный) - любые красные сапоги не зависимо от размера - быть не может.
Также предполагается, что задействованных для товарного учета характеристик врядли будет больше трех-четырех.
Работать-то будет хоть десять-двадцать задай, но растянет экран при настройке наборов .
Если ни одного набора вариантов не задано, то учет товара и расчет цены идет обычным штатным способом.
Сomment: Задавать набору именно полную цену, а не добавление к дефолтовой цене (как штатно сделано для вариантов) мне показалось более удобным.
Админка:
----------
В настройке товара появляется вышеуказанный (название модуля) раздел.
Если у товара нет ни одной характеристики, для которой задан хотя бы один вариант,
то в этом разделе будет сообщение "НЕ ЗАДАНО НИ ОДНОГО ВАРИАНТА НИ У ОДНОЙ ХАРАКТЕРИСТИКИ".
Если характеристики есть, то в мультиселекте характеристик надо выбрать,
по каким характеристикам будет производиться количественный учет товара и нажать "Сохранить".
После этого справа внизу появится возможность создать набор (товар) из вариантов выбранных характеристик,
а также задать цену такого товара и количество такого товара.
Пока ни одного набора не задано кнопка "Сохранить" сохраняет состояние мультиселекта (выбранный набор характеристик).
Как только задан хотя бы один набор вариантов изменение мультиселекта с характеристикам запрещается, а кнопка "Сохранить"
сохраняет настройки наборов вариантов (цену и количество).
Также когда задан хотя бы один набор вариантов дизаблится штатное поле "На складе" и в нем
показывается суммарное количество товара во всех наборах вариантов.
Клиентская часть:
-----------------
Селекты выбора вариантов характеристик, входящих в набор помечаются красным (не выбран) либо зеленым (выбран) цветом.
Положить товар в корзину можно только тогда, когда задан выбор для всех "наборных" характеристик.
Все "наборные" характеристики не имеют дефолтового варианта и сначала стоят в состоянии "не выбрано".
При выборе варианта одной из "наборных" характеристик в остальных характеристиках варианты, уже не подходящие под выбор
дизаблятся, а если разрешенный вариант всего один, то он автоматически станет выбранным.
На примере вышеуказанных сапог:
- выбрав сначала "Красный" получим в селекте размера задизабленный 42-й размер, т.к. такого набора для варианта "Синий" нет.
- выбрав сначала 42-й размер сразу получим и выбранный цвет "Синий", т.к. других наборов для варианта "42" нет.
Если однозначность выбора еще не достигнута, а у доступных к выбору наборов цены отличны, то цена показывается как
min-max. Например, "123-456руб".
Штатные добавления к цене по вариантам тоже работают, в том числе и для "наборных" характеристик.
Хотя не представляю, зачем бы это могло понадобиться .
Пример учета сразу по трем характеристикам: https://cpu.badisoft...skorohodyi.html
Админка этого товара выглядит так:
1.JPG 94.41К 61 Количество загрузок:
Не сделано, т.к. не интересно:
------------------------------
1. Вообще никак не отработан момент удаления-изменения характеристик и вариантов в настройках товара и админке характеристик. На предмет "этот вариант (характеристику) удалять-изменять нельзя, т.к. она встречается в наборе у товара".
2. Не отправляется емейл админу если закончился один из наборов. Отправляется (штатно) только если закончились все, т.е. общее количество ноль, а не один из наборов закончился.
Можно содрать из моей инструкции к namer-овскому аналогичному модулю, там я это доделывал.
3. Инструкция написана только для модификации product_detailed.tpl.html (подробного описания товара). Для product_brief.tpl.html все, в принципе, примерно так же. Достаточно понять саму идею, что ко всем повторяющимся в цикле, т.е. ставшим не уникальными идентификаторам (name, id, имена JS-функций) добавляется productID.
В планах:
---------
Примененный способ учета (алгоритм) довольно хорошо развивается дальше для создания НАБОРОВ в терминологии 1С.
Т.е. когда товар может представлять собой не только учетную единицу, но и набор, собранный из других товаров (учетных единиц)
в заданном количестве. Например, товар "Заправка цветного картриджа, 1шт, 200руб" состоит из
"Чернила желтые, 10гр", "Чернила малиновые, 10гр", "Чернила синие, 10гр" и соответствующим образом учитывается-списывается.
Установка:
----------
0. копируем файлы и каталоги
/varset/ -> /
addon.php -> /core/includes/admin/
varset_functions.php -> /core/functions/
Файл addon.php нужен ОДИН РАЗ для создания в таблицах новых полей.
После первого же захода в админку его можно удалить, если он не удалился сам.
1. заходим в админку чтобы выполнился и стерся addon.php
2. в файле admin.php
2.1. перед строками (искать удобно по echo ADMIN_SETADD;)
<table class="adn">
<tr class="lineb">
<td align="left"><?php
echo ADMIN_SETADD;
?></td></tr>
вставляем
<!-- BEGIN Раздельный учет товара по комбинациям вариантов характеристик -->
<?php require_once "varset/varset_cat.php";?>
<!-- END Раздельный учет товара по комбинациям вариантов характеристик -->
2.2. перед строками (искать по ADMIN_DIGIT_PRODUCTS)
<table class="adn"><tr><td class="se6"></td></tr></table>
<table class="adn"><tr class="lineb"><td align="left" colspan="6"><?php
echo ADMIN_DIGIT_PRODUCTS;
вставляем
<!-- BEGIN Раздельный учет товара по комбинациям вариантов характеристик -->
<?php require_once "varset/varset_prd.php";?>
<!-- END Раздельный учет товара по комбинациям вариантов характеристик -->
3. в файле catalog_products_categories.tpl.html
вместо
<td align="right">{if $products[i].in_stock <= 0}<input type="text" name="left_{$products[i].productID}" value="{$products[i].in_stock}" class="prc prcss{if !$products[i].enabled} gryy{/if}">{else}<input type="text" name="left_{$products[i].productID}" value="{$products[i].in_stock}" class="prc prcss {if !$products[i].enabled}gryy{/if}">{/if}</td>
вставляем
{* BEGIN Раздельный учет товара по комбинациям вариантов характеристик *}
{*<td align="right">{if $products[i].in_stock <= 0}<input type="text" name="left_{$products[i].productID}" value="{$products[i].in_stock}" class="prc prcss{if !$products[i].enabled} gryy{/if}">{else}<input type="text" name="left_{$products[i].productID}" value="{$products[i].in_stock}" class="prc prcss {if !$products[i].enabled}gryy{/if}">{/if}</td>*}
<td align="right"><input type="text" name="left_{$products[i].productID}" value="{$products[i].in_stock}" class="prc prcss{if !$products[i].enabled} gryy{/if}"{if $products[i].productID|ProductIsVarsetAdmin} disabled{/if}></td>
{* END Раздельный учет товара по комбинациям вариантов характеристик *}
4. в файле cart_functions.php
4.1. в функции GetPriceProductWithOption
перед
foreach($variants as $vars)
вставляем
# BEGIN Раздельный учет товара по комбинациям вариантов характеристик
$full_price = GetPriceOfVariants($variants,$productID,$base_price);
# END Раздельный учет товара по комбинациям вариантов характеристик
4.2. в функции cartMoveContentFromShoppingCartsToOrderedCarts
4.2.1. перед
db_query("INSERT INTO ".ORDERED_CARTS_TABLE.
вставляем
# BEGIN Раздельный учет товара по комбинациям вариантов характеристик
if ($setID = GetSetOfVariants($variants,$productID))
db_query("INSERT INTO ".ORDERED_CARTS_TABLE."( itemID, orderID, name, Price, Quantity, tax, setID ) ".
" VALUES (".(int)$item["itemID"].",".(int)$orderID.", '".xEscSQL($productComplexName)."', ".
xEscSQL($price).", ".(int)$item["Quantity"].", ".xEscSQL($tax).",$setID )");
else
# END Раздельный учет товара по комбинациям вариантов характеристик
4.2.2. перед
db_query( "update ".PRODUCTS_TABLE." set in_stock = in_stock - ".(int)$item["Quantity"].
вставляем
# BEGIN Раздельный учет товара по комбинациям вариантов характеристик
if ($setID) db_query( "UPDATE ".DB_PRFX."varset_sets SET instock=instock-".(int)$item["Quantity"]." WHERE setID=$setID");
# END Раздельный учет товара по комбинациям вариантов характеристик
4.3. в функции cartAddToCart
перед
$is = GetProductInStockCount( $productID );
вставляем
# BEGIN Раздельный учет товара по комбинациям вариантов характеристик
if (($is = GetInStockOfVariants($variants,$productID)) === false)
# END Раздельный учет товара по комбинациям вариантов характеристик
5. в файле order_functions.php
5.1. в функции _moveSessionCartContentToOrderedCart
перед
db_query( "insert into ".ORDERED_CARTS_TABLE." ( itemID, orderID, name, Price, Quantity, tax ) ".
вставляем
# BEGIN Раздельный учет товара по комбинациям вариантов характеристик
if ($setID = GetSetOfVariants($variants,$productID))
db_query( "insert into ".ORDERED_CARTS_TABLE." ( itemID, orderID, name, Price, Quantity, tax, setID ) ".
"values( ".(int)$itemID.", ".(int)$orderID.", '".xEscSQL($productComplexName)."', '".xEscSQL($price)."', ".
(int)$quantity.", ".xEscSQL($tax).", $setID) " );
else
# END Раздельный учет товара по комбинациям вариантов характеристик
5.2. в функции ordOrderProcessing
5.2.1. вместо
$q1 = db_query("select itemID, Quantity FROM ".ORDERED_CARTS_TABLE." WHERE orderID=".(int)$orderID);
вставляем
# BEGIN Раздельный учет товара по комбинациям вариантов характеристик
#$q1 = db_query("select itemID, Quantity FROM ".ORDERED_CARTS_TABLE." WHERE orderID=".(int)$orderID);
$q1 = db_query("select itemID, Quantity, setID FROM ".ORDERED_CARTS_TABLE." WHERE orderID=".(int)$orderID);
# END Раздельный учет товара по комбинациям вариантов характеристик
5.2.2. перед
db_query( "update ".PRODUCTS_TABLE." set in_stock = in_stock - ".(int)$item["Quantity"].
вставляем
# BEGIN Раздельный учет товара по комбинациям вариантов характеристик
if ($item["setID"]) db_query( "UPDATE ".DB_PRFX."varset_sets SET instock=instock-".(int)$item["Quantity"]." WHERE setID=".(int)$item["setID"]);
# END Раздельный учет товара по комбинациям вариантов характеристик
6. в файле order_status_functions.php в функции _changeIn_stock
6.1. вместо
$q = db_query( "select itemID, Quantity from ".ORDERED_CARTS_TABLE.
вставляем
# BEGIN Раздельный учет товара по комбинациям вариантов характеристик
#$q = db_query( "select itemID, Quantity from ".ORDERED_CARTS_TABLE.
$q = db_query( "select itemID, Quantity, setID from ".ORDERED_CARTS_TABLE.
# END Раздельный учет товара по комбинациям вариантов характеристик
6.2. перед
if ( $increase ) {
вставляем
# BEGIN Раздельный учет товара по комбинациям вариантов характеристик
if ($item["setID"]) db_query( "UPDATE ".DB_PRFX."varset_sets SET instock=instock".($increase?'+':'-').(int)$item["Quantity"]." WHERE setID=".(int)$item["setID"]);
# END Раздельный учет товара по комбинациям вариантов характеристик
7. в файле product_detailed.php
перед
$smarty->assign("product_extra", $extra);
вставляем
# BEGIN Раздельный учет товара по комбинациям вариантов характеристик
$varset_sets = GetSetsVarset($productID);
$smarty->assign("varset", $varset_sets);
if (count($varset_sets) && is_array($extra))
{
$one_set = current($varset_sets);
foreach ($extra as $key => $val)
if (array_key_exists($val['optionID'],$one_set['options']))
$extra[$key]['is_set'] = true;
}
# END Раздельный учет товара по комбинациям вариантов характеристик
8. в файле product_detailed.tpl.html
8.1. в код
onclick="doLoad('do=cart&
вставляем довесок, вот так:
onclick="if (varsetNotSetted()) return false; else doLoad('do=cart&
Это для AJAX-корзины. Для двух других корзин делаем аналогично - добавляем в onclick код if (varsetNotSetted()) return false; else
8.2. перед
{counter name='option_show_times' start=0 skip=1 print=false}
вставляем
{* BEGIN Раздельный учет товара по комбинациям вариантов характеристик *}
{if $product_extra[i].is_set}
{counter name=select_counter}
<select class='varset_select' id='option_select_{$product_extra[i].optionID}' name='option_select_{$select_counter_var}' onchange='VarsetSelect(this.value);'{* multiple size="{1+$product_extra[i].values_to_select|@count}*}">
<option id='not_selected_{$product_extra[i].optionID}' value='0:0' selected>Не выбрано</option>
{section name=j loop=$product_extra[i].values_to_select}
<option class='varset_option' id='variant_{$product_extra[i].values_to_select[j].variantID}' value='{$product_extra[i].values_to_select[j].price_surplus}:{$product_extra[i].values_to_select[j].variantID}'>{$product_extra[i].values_to_select[j].option_value}</option>
{/section}
</select>
<br>
{else}
{* END Раздельный учет товара по комбинациям вариантов характеристик *}
8.3. чуть ниже в код
{/if}
{/if}
{/section}
{/if}
{/section}
вставляем еще один {/if}. Должно получиться так:
{/if}
{/if}
{/section}
{* BEGIN Раздельный учет товара по комбинациям вариантов характеристик *}
{/if}
{* END Раздельный учет товара по комбинациям вариантов характеристик *}
{/if}
{/section}
8.4. вместо
{if $select_counter_var != 0}
{literal}
<script type="text/javascript">
function GetCurrentCurrency()
[.......]
GetCurrentCurrency();
</script>
{/if}
вставляем
{* BEGIN Раздельный учет товара по комбинациям вариантов характеристик *}
<input type="hidden" name="PriceWithOutUnit" id="PriceWithOutUnit" value="{$product_info.PriceWithOutUnit}">
{if $varset|@count}<input type="hidden" name="varset" id="varset" value="0">{/if}
{if $select_counter_var != 0}
<script type="text/javascript">
function formatSum(sum)
{ldelim}
sum = Math.round(sum*100)/100;
sumStr = new String(sum);
commaIndex = sumStr.indexOf(".");
if ( commaIndex == -1 ) sumStr = sum;
else sumStr = sumStr.substr(0, commaIndex + 3);
return _formatPrice(sumStr, {$currency_roundval});
{rdelim}
function GetCurrentCurrency()
{ldelim}
var price = document.getElementById('PriceWithOutUnit').value;
var price_surplus = 0;
{counter name='select_counter2' start=1 skip=1 print=false assign='select_counter_var2'}
{section name=i loop=$product_extra}
{section name=k loop=$product_extra[i].option_show_times}
_value = document.MainForm.option_select_{$select_counter_var2}.value;
price_surplus += new Number((_value.split(":"))[0]);
variantID = ( _value.split(":") )[1];
document.HiddenFieldsForm.option_select_hidden_{$select_counter_var2}.value = variantID;
{counter name=select_counter2}
{/section}
{/section}
{php}echo("locationPriceUnit=".getLocationPriceUnit().";\n");{/php}
_sum = new Number((price.split(":"))[0]);
_sumStr = formatSum(_sum+price_surplus);
_sum1 = new Number((price.split(":"))[1]);
if (_sum1 > 0) _sumStr += '-' + formatSum(_sum1+price_surplus);
if ( locationPriceUnit ) document.getElementById('optionPrice').innerHTML = _sumStr + document.MainForm.priceUnit.value;
else document.getElementById('optionPrice').innerHTML = document.MainForm.priceUnit.value + _sumStr;
{rdelim}
{literal}
function count(array) {
var cnt=0;
for (var i in array) if (i) cnt++;
return cnt;
}
function VarsetSelect(value) {
// построим массивы:
// двумерный массив наборов sets[набор][характеристика](вариант),
// массив разрешения/запрета наборов по мере выбора клиентом вариантов sets_true,
// массив используемых в наборах хар-к opts,
// массивы цен price и количества на сладе instock
{/literal}
var sets = [];
var sets_true = [];
{counter start=0 print=false assign=setID}
{foreach from=$varset item=set name=set}
{if !$smarty.const.CONF_SHOW_NULL_STOCK || $set.instock != 0}
sets[{$setID}] = [];
{foreach from=$set.options key=optID item=varID}
sets[{$setID}][{$optID}] = {$varID};
{/foreach}
{counter print=false}
{/if}
{if $smarty.foreach.set.last}
var opts = [{foreach from=$set.options key=optID item=varID name=var}{$optID}{if !$smarty.foreach.var.last},{/if}{/foreach}];
{/if}
{/foreach}
{assign var=delim value=''}
var price = [{foreach from=$varset item=set name=set}{if !$smarty.const.CONF_SHOW_NULL_STOCK || $set.instock != 0}{$delim}{assign var=delim value=','}{$set.price}{/if}{/foreach}];
{assign var=delim value=''}
var sets_true = [{foreach from=$varset item=set name=set}{if !$smarty.const.CONF_SHOW_NULL_STOCK || $set.instock != 0}{$delim}{assign var=delim value=','}true{/if}{/foreach}];
{assign var=delim value=''}
var sets_id = [{foreach from=$varset item=set name=set key=key}{if !$smarty.const.CONF_SHOW_NULL_STOCK || $set.instock != 0}{$delim}{assign var=delim value=','}{$key}{/if}{/foreach}];
{literal}
// пройдемся по выбранным селектам (характеристикам, участвующим в наборах) и запретим те наборы, где нет выбранного варианта
var sets_length = sets.length;
var selects = document.getElementsByClassName('varset_select');
var selects_length = selects.length;
for (var i = 0; i < selects_length; i++) {
if (selects[i].value != '0:0') {
var optID = (selects[i].id.split('_'))[2];
var varID = (selects[i].value.split(':'))[1];
for (var j = 0; j < sets_length; j++) {
if(sets[j][optID] != varID) sets_true[j] = false;
}
}
}
// найдем минимальную и максимальную цену, занесем их (или ее - при одинаковости) в скрытое поле, оттуда ее возьмет GetCurrentCurrency()
var price_min = Infinity;
var price_max = 0;
for (var i = 0; i < sets_length; i++) {
if(sets_true[i]) {
price_min = Math.min(price_min,price[i]);
price_max = Math.max(price_max,price[i]);
}
}
if (price_min == price_max) document.getElementById('PriceWithOutUnit').value = price_min;
else document.getElementById('PriceWithOutUnit').value = price_min+':'+price_max;
// запретим все варианты выбора во всех селектах (участвующих..)
var options = document.getElementsByClassName('varset_option')
var options_length = options.length;
for (var i = 0; i < options_length; i++) options[i].disabled=true;
// разрешим в не выбранных еще селектах только те варианты, которые возможны при уже выбранных селектах
var opts_length = opts.length;
for (var i = 0; i < sets_length; i++) {
if (sets_true[i]) {
for (var j = 0; j < opts_length; j++) {
eval('document.getElementById("variant_'+sets[i][opts[j]]+'").disabled=false');
}
}
}
// если вызов не по выбору "не задано", то пройдемся по таблице наборов и автоматом зададим (выберем) варианты в селектах, если он все равно единственный разрешенный.
if (value != '0:0') {
for (var i = 0; i < opts_length; i++) {
var num_true = [];
var opt_true;
for (var j = 0; j < sets_length; j++) {
if (sets_true[j]) num_true[sets[j][opts[i]]] = sets[j][opts[i]];
}
if (count(num_true) == 1) {
eval('document.getElementById("not_selected_'+opts[i]+'").selected=false');
eval('document.getElementById("variant_'+num_true.pop()+'").selected=true');
}
}
}
// пройдемся по селектам, отметим красным не выбранные, установим (или не установим) признак "все выбраны"
var all_selected = true;
for (var i = 0; i < selects_length; i++) {
if (selects[i].value == '0:0') {
selects[i].style.border = "solid red 1px";
var all_selected = false;
}
else selects[i].style.border = "solid green 1px";
}
// если все селекты выбраны, т.е. есть конкретный набор, то занесем в скрытое поле номер набора, иначе -1
if (all_selected) {
for (var i = 0; i < sets_length; i++) {
if (sets_true[i]) document.getElementById('varset').value = sets_id[i];
}
}
else document.getElementById('varset').value = -1;
// запустим штатный пересчет цен исходя из выбранных вариантов
GetCurrentCurrency();
}
function varsetNotSetted() {
if (document.getElementById('varset') != undefined && document.getElementById('varset').value == -1) {
alert('Не все обязательные к выбору характеристики выбраны!');
return true;
}
}
// создадим метод document.getElementsByClassName, если его нет (IE8 и ниже)
if(document.getElementsByClassName == undefined) {
document.getElementsByClassName = function(cl) {
var retnode = [];
var myclass = new RegExp('\\b'+cl+'\\b');
var elem = this.getElementsByTagName('*');
for (var i = 0; i < elem.length; i++) {
var classes = elem[i].className;
if (myclass.test(classes)) {
retnode.push(elem[i]);
}
}
return retnode;
}
};
{/literal}
// выполним при начальной загрузке либо все это, либо старый штатный вариант .
{if $varset|@count}
VarsetSelect('');
{else}
GetCurrentCurrency();
{/if}
</script>
{/if}
{* END Раздельный учет товара по комбинациям вариантов характеристик *}