Бонусная программа для покупателей
Модуль позволяет начислять клиентам бонусы и списывать их для оплаты последующих покупок
Что вам даст программа лояльности
увеличение оборота за счет роста числа повторных продаж
повышение прибыли (использование бонусов более рентабельно, чем предоставление скидки)
рост числа клиентов (довольные покупатели охотнее рекомендуют вас знакомым)
Настройки бонусной программы
базовый процент начисления бонусов и специальные проценты для отдельных товаров или групп товаров
исключение определенных товаров или групп товаров (не начислять за них бонусы)
срок в днях с момента покупки для начисления бонусов (для учета возвратов по гарантийному сроку)
разрешенный процент списания бонусов (какую часть покупки можно оплатить бонусами)
разрешить или запретить одновременное начисление и списание бонусов в одной покупке
Инструкция по работе
После поиска клиента выводится информация о доступных к списанию бонусах. При клике на них, в документ подставляется товар "Оплата бонусами" с отрицательной ценой, уменьшающей сумму документа.
Инструкция по установке
Установка выполняется нами бесплатно. Вы также можете установить модуль самостоятельно по инструкции ниже:
ИНСТРУКЦИЯ ДЛЯ САМОСТОЯТЕЛЬНОЙ УСТАНОВКИ2 Распакуйте файлы архива в папку установки СКИФ на вашем сайте с сохранением структуры директорий.
3 Создайте таблицу и добавьте поля, выполнив SQL (это можно сделать через /admin/phpmyadmin/): КОМАНДЫ SQL
CREATE TABLE IF NOT EXISTS `wn_scif1_bonuses` ( `id` int(10) unsigned NOT NULL AUTO_INCREMENT, `doc_date` datetime NOT NULL, `date_insert` int(10) unsigned NOT NULL DEFAULT '0', `user_insert` smallint(5) unsigned NOT NULL DEFAULT '0', `summa` decimal(11,2) NOT NULL DEFAULT '0.00', `note` text NOT NULL, `contr` smallint(5) unsigned NOT NULL DEFAULT '0', PRIMARY KEY (`id`), KEY `contr_date` (`contr`,`doc_date`), KEY `date_contr` (`doc_date`,`contr`) ) ENGINE=MyISAM DEFAULT CHARSET=utf8 COMMENT='Бонусы'; ALTER TABLE `wn_scif1_doc` ADD `my_phone` VARCHAR( 10 ) NOT NULL DEFAULT '' COMMENT 'Телефон для регистрации анкеты', ADD `my_bonuses` MEDIUMINT NULL COMMENT 'Начисление/Списание бонусных баллов'; ALTER TABLE `wn_scif1_doc` ADD INDEX ( `contr` ); ALTER TABLE `wn_scif1_spr_noms` ADD `my_per` TINYINT NOT NULL DEFAULT '0' COMMENT 'Процент начисления бонусных баллов'; ALTER TABLE `wn_scif1_spr_noms_gr` ADD `my_per` TINYINT NOT NULL DEFAULT '0' COMMENT 'Процент начисления бонусных баллов'; ALTER TABLE `wn_scif1_spr_contrs` ADD `my_bonuses` SMALLINT NOT NULL DEFAULT '0' COMMENT 'Баланс бонусов', ADD `my_birthday` DATE NULL COMMENT 'Дата рождения'; ALTER TABLE `wn_scif1_doc_det` ADD `my_base_price` DECIMAL( 11, 2 ) UNSIGNED NOT NULL DEFAULT '0.00' COMMENT 'Базовая продажная цена'; ALTER TABLE `wn_scif1_spr_contrs` CHANGE `phone` `phone` VARCHAR( 60 ) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL; UPDATE `wn_scif1_spr_contrs` SET phone=NULL WHERE phone=""; ALTER TABLE `wn_scif1_spr_contrs` ADD UNIQUE (`phone`);
4 Создайте услугу "Оплата бонусами" в справочнике номенклатуры. Эта позиция будет добавляться в документ с минусовой ценой и уменьшать сумму документа к оплате.
5 Разрешите использование минусовых цен в меню "Сервис-Настройки".
6 Добавьте программный код в файл настроек wn_settings.php в корне установки (это можно сделать через меню Сервис-Редактор кода) и укажите в массиве $my_bonuses_system ваши параметры: ПРОГРАММНЫЙ КОД
// Бонусная система $my_bonuses_system=array( 'default_contr'=>1, // покупатель, начисление бонусов на которого не производится (розничный покупатель) 'nom_id'=>0, // код товара-услуги "Оплата бонусами", создайте такую услугу в справочнике номенклатуры 'per_use'=>20, // разрешенный процент списания бонусов от суммы продажи 'days'=>14, // через сколько дней после продажи начисляем бонусы (обычно равен сроку, в течение которого покупатель может вернуть товар. 0-на следующий день) //'where_order'=>'', // доп.условие выбора документов (например, исключаем товары из интернет-магазина: 'AND d.shop_id IS NULL') //'where_contr'=>'', // доп.условие выбора клиентов (например, клиенты только из группы 1: 'AND c.parent IN (1)') 'quick_notes'=>array('За отзыв'), // для подстановки примечаний ручного начисления/списания бонусов 'bonuses_notify'=>false // отправлять уведомления клиентам в бот Telegram https://www.webnice.biz/catalog/product/telegram-clients/ ); // формат и уникальность телефона контрагента $sprs[6]['fields']['phone']['size']=10; $sprs[6]['fields']['phone']['maxlength']=10; $sprs[6]['fields']['phone']['pattern']='\d{10}'; $sprs[6]['fields']['phone']['title']='В поле Телефон необходимо ввести 10 цифр без пробелов'; $sprs[6]['fields']['phone']['default']='NULL'; // Дата рождения клиента $sprs[6]['userfields']['my_birthday']=array('name'=>'Дата рождения:','type'=>'date','size'=>9,'maxlength'=>10,'format'=>'date_sql','my_edit_field'=>'my_birthday'); $sprs[6]['userfields']['my_bonuses']=array('name'=>'Бонусы:','type'=>'virtual','my_edit_field'=>'my_bonuses'); // Проценты начисления бонусных баллов $scif_ext_config=array( 'my_per'=>array('name'=>'Процент бонусов','size'=>3,'maxlength'=>2,'format'=>'intval','type'=>'number','class'=>'numeric','attr'=>'max="99" min="0"', 'note'=>'Базовый процент начисления бонусных баллов. Применяется, если для группы или товара не указан персональный процент.') ); $sprs[5]['userfields']['my_per']=array('name'=>'% бонусов:','type'=>'number','size'=>3,'maxlength'=>2,'format'=>'intval','class'=>'numeric nomin','attr'=>'max="99" min="-1"','note'=>'-1 если не нужно начислять бонусов на данный товар'); $sprs[5]['userfields_gr']['my_per']=array('name'=>'% бонусов:','type'=>'number','size'=>3,'maxlength'=>2,'format'=>'intval','class'=>'numeric nomin','attr'=>'max="99" min="-1"','note'=>'-1 если не нужно начислять бонусов на данную группу'); // Телефон в складском документе для последующей регистрации клиента по заполненной анкете $docs_cat['store']['userfields']['my_phone']=array('name'=>'Телефон:','format'=>'text','size'=>9,'maxlength'=>10,'placeholder'=>'10 цифр', 'note'=>'для регистрации в бонусной программе', 'attr'=>'pattern="\d{10}" title="В поле Телефон необходимо ввести 10 цифр без пробелов"'); // выводим телефон в журнале документов с возможностью поиска по нему $scif_actions[3]['my_doclist']=array('head'=>1,'filters'=>1,'sql'=>1,'row'=>1,'foot'=>1); function my_doclist($type) { global $cells, $sqls, $row; switch ($type) { case 'head': // заголовок $cells[9].='<th data-sorter="digit">Бонусы</th>'; $cells[15].='<th data-sorter="digit">Телефон</th>'; break; case 'filters': // строка фильтров $cells[9].='<td> </td>'; $cells[15].='<td><input id="f_my_phone" size="9" maxlength="10" name="f_my_phone" type="text" value="'.(!empty($_REQUEST['f_my_phone'])?htmlclean($_REQUEST['f_my_phone']):'').'" onchange="items(0)"></td></td>'; break; case 'sql': // SQL-запрос (здесь добавляем условие поиска по заданному коду) if (!empty($_REQUEST['f_my_phone'])) { $sqls['where'].=($sqls['where']?' AND ':'').'d.my_phone LIKE "'.htmlclean($_REQUEST['f_my_phone']).'"'; } break; case 'row': // строка таблицы $cells[9].='<td class="num">'.quant($row['my_bonuses']).'</td>'; $cells[15].='<td>'.($row['my_phone']?$row['my_phone']:' ').'</td>'; break; case 'foot': // итоги в подвале $cells[9].='<td> </td>'; $cells[15].='<td> </td>'; break; } } // Добавим в справочник контрагентов колонку "Бонусы" $sprs[6]['my_sprlist']=array('head'=>1,'sql'=>1,'row'=>1); function my_sprlist($type) { global $t, $row, $cells, $sqls; switch ($t) { case 6: // контрагенты switch ($type) { case 'head': $cells[4].='<th data-sorter="digit"><acronym title="Баланс бонусных баллов">Бонусы</acronym></th>'; break; case 'sql': $sqls['select'].=', s.my_bonuses'; break; case 'row': $cells[4].='<td class="num">'.quant($row['my_bonuses']).'</td>'; break; } break; } } function my_edit_field($key,$val,$cur_val='') { global $v, $repl; $return=''; switch ($val['my_edit_field']) { case 'my_bonuses': if ($v AND !$repl) { $return='<a href="?act=reports&t=4200&choice_items[6]=<'.$v.'>&submit" target="_blank" id="my_bonuses" title="Отчет по бонусам">'.$cur_val.'</a> <a href="?act=bonusitem&def[contr]='.$v.'" onclick="return create_dialog(`popup`,`Бонусы`,this.href)" title="Начислить или списать бонусы"><i class="si si_item_new"></i></a>'; } break; case 'my_birthday': $return='<input type="text" value="'.(($cur_val AND $cur_val!='NULL')?sql_date($cur_val,'d.m.Y'):'').'" id="my_birthday" name="my_birthday" size="9" maxlength="10"> <script type="text/javascript"> $(document).ready(function() { $("#my_birthday").datepicker({ changeMonth: true, changeYear: true, yearRange: "-60:-16" }); }); </script>'; break; } return $return; } function scif_before_act() { global $db, $act, $userdata, $v, $t, $my_bonuses_system, $sprs; // при сохранении продажи с указанным номером телефона создаем контрагента if ($act=='docitem' AND $t==2 AND !$v AND !empty($_POST['submit']) AND !empty($_POST['my_phone']) AND $_POST['contr']==$my_bonuses_system['default_contr'] AND preg_match('#^'.$sprs[6]['fields']['phone']['pattern'].'$#',$_POST['my_phone'])) { $my_phone=htmlclean($_POST['my_phone']); // поищем по номеру телефона $check=$db->sql_fetch_assoc($db->sql_query('SELECT id FROM '.SCIF_PREFIX.'spr_contrs WHERE phone="'.$my_phone.'"')); if ($check AND $check['id']) { $_POST['contr']=$check['id']; } else { // поищем по названию $check=$db->sql_fetch_assoc($db->sql_query('SELECT id FROM '.SCIF_PREFIX.'spr_contrs WHERE name="'.$my_phone.'"')); if ($check AND $check['id']) { $_POST['contr']=$check['id']; } else { // на нашли ни по телефону, ни по названию, создаем нового контрагента $db->sql_query('INSERT IGNORE INTO '.SCIF_PREFIX.'spr_contrs SET name="'.$my_phone.'", phone="'.$my_phone.'", parent="1", date_insert="'.time().'", user_insert="'.$userdata['id'].'", manager="'.$userdata['id'].'"'); $new_contr=$db->sql_insert_id(); if ($new_contr) { $_POST['contr']=$new_contr; } } } } // если из retail передан телефон, контрагент созданной продажи может быть не тот, что передан из retail // поэтому, проверим здесь переданный номер документа при смешанной оплате if ($act=='finitem' AND $t==21 AND !empty($_POST['submit']) AND !empty($_POST['docs']) AND !empty($_POST['external']) AND $_POST['contr']==$my_bonuses_system['default_contr']) { $check=$db->sql_fetch_assoc($db->sql_query('SELECT contr FROM '.SCIF_PREFIX.'doc WHERE id="'.intval(key($_POST['docs'])).'"')); if ($check AND $check['contr']) { $_POST['contr']=$check['contr']; } } } function scif_after_act() { global $act, $db, $t, $v, $row_old, $row_new, $my_bonuses_system; if ($act=='docitem' AND in_array($t,array(1,2)) AND $v AND !empty($_POST['submit'])) { // сохраняем базовую цену продажи, чтобы при расчете бонусов исключать товары, проданные со скидкой if ($t==2) { $db->sql_query('UPDATE '.SCIF_PREFIX.'doc_det dd LEFT JOIN '.SCIF_PREFIX.'spr_noms n ON dd.nom_id=n.id SET dd.my_base_price=n.price'.intval($_POST['price_type']) .' WHERE dd.doc_id="'.$v.'" AND dd.my_base_price="0"'); } /* массовый пересчет: UPDATE wn_scif1_doc_det dd JOIN wn_scif1_doc d ON dd.doc_id=d.id JOIN wn_scif1_spr_noms n ON dd.nom_id=n.id SET dd.my_base_price=n.price2 WHERE d.type="2" AND d.contr!=1 AND d.doc_date>="2019-11-29" AND dd.my_base_price="0" */ // если в документе есть товар "Списание бонусов" (или был), списываем бонусы (нужно сделать это сразу, т.к. покупатель может пойти в другой отдел и там нам нужен актуальный баланс) $my_row=$db->sql_fetch_assoc($db->sql_query('SELECT my_bonuses FROM '.SCIF_PREFIX.'doc WHERE id="'.$v.'"')); if (($my_row['my_bonuses']!='NULL' AND $my_row['my_bonuses']<0) OR $row_new['contr']!=$my_bonuses_system['default_contr'] OR (!empty($row_old['contr']) AND $row_old['contr']!=$my_bonuses_system['default_contr'])) { $check=$db->sql_fetch_assoc($db->sql_query('SELECT SUM(quant*price) AS summa FROM '.SCIF_PREFIX.'doc_det WHERE doc_id="'.$v.'" AND nom_id="'.$my_bonuses_system['nom_id'].'" AND quant!="0"')); // if ($check AND $check['summa']<0) { // не проверяем, т.к списание могло быть, а потом удалено, тогда нужно поставить NULL для расчета начислений $check['summa']=(!empty($check['summa'])?round($check['summa']):0); $db->sql_query('UPDATE '.SCIF_PREFIX.'doc SET my_bonuses='.($check['summa']<0?'"'.$check['summa'].'"':'NULL').' WHERE id="'.$v.'"'); // пересчитаем балансы контрагентов (просто изменить баланс не можем, т.к. это может быть как создание, так и изменение документа update_bonuses_balance($row_new['contr']); if (!empty($row_old['contr']) AND $row_old['contr']!=$row_new['contr']) { // изменился контрагент update_bonuses_balance($row_old['contr']); } } // конец расчета списания бонусов } } // пересчитываем баланс бонусных баллов function update_bonuses_balance($contr) { global $db; $my_bonuses=$db->sql_fetch_assoc($db->sql_query('SELECT SUM(summa) summa FROM (SELECT IF(type=1,-my_bonuses,my_bonuses) AS summa FROM '.SCIF_PREFIX.'doc WHERE contr="'.$contr.'" AND type IN (1,2) AND my_bonuses IS NOT NULL UNION ALL SELECT summa FROM '.SCIF_PREFIX.'bonuses WHERE contr="'.$contr.'") AS t')); $db->sql_query('UPDATE '.SCIF_PREFIX.'spr_contrs SET my_bonuses="'.(!empty($my_bonuses['summa'])?round($my_bonuses['summa']):0).'" WHERE id="'.$contr.'"'); } $scif_actions[4200]=array('name'=>'Бонусы','desc'=>'Начисления и списания бонусов','file'=>'bonusitem'); $scif_reps[4200]=array('name'=>'Бонусы','desc'=>'Начисления и списания бонусов','submenu'=>1); $scif_actions[2600]=array('name'=>'Продавец-кассир','desc'=>'Интерфейс продавца-кассира','menu'=>3,'ico'=>'fr','file'=>'retail'); $scif_actions[2601]=array('name'=>'Настройка интерфейса продавца-кассира','desc'=>'Настройка интерфейса продавца-кассира','file'=>'retail_settings');
7 Добавьте задачу в планировщик CRON на выполнение раз в сутки скрипта /cron/my_bonuses.php. Он будет рассчитывать бонусы за предыдущие дни (в зависимости от значения days в массиве настроек $my_bonuses_system). Например, поставьте на 1:01 каждый день (сдвиг в 1 час на случай смещения часового пояса на сервере). По умолчанию, бонусы не начисляются, если в Продаже есть списание бонусов. Если вам нужно начислять бонусы даже за продажи, в которых использовалось списание бонусов, необходимо внести изменения в данный скрипт! Сумма бонусов округляется до целых по правилам мат.округления (0,5 до 1)
Инструкция по настройке
1 Укажите базовый процент начисления бонусных баллов через меню Сервис-Настройки.2 Для групп и товаров, процент начисления по которым отличается от базового, установите персональный процент в карточке товара/группы. Если на какие-то группы или товары не нужно начислять бонусы, установите значение "-1". Вложенность групп при этом не проверяется, если нужно исключить для начисления бонусов какую-то группу вместе с подгруппами, нужно поставить "-1" в карточке каждой подгруппы.
3 Настройте интерфейс продавца-кассира НАСТРОЙКИ ИНТЕРФЕЙСА КАССИРА
На этом настройка интерфейса продавца-кассира завершена. Ссылка на него доступна через меню Торговля – «Продавец-Кассир». Если вы хотите, чтобы продавец после авторизации сразу попадал в интерфейс кассира, установите эту ссылку в закладки его браузера или стартовой страницей в браузере.
- разрешенный процент списания бонусов от суммы продажи (какую часть покупки можно оплатить бонусами). Значение по умолчанию 20.
- через сколько дней после продажи начисляем бонусы (обычно равен сроку, в течение которого покупатель может вернуть товар). Значение по умолчанию 14.
- требуется ли исключать какие-либо продажи из начисления бонусов
Проверьте также ниже параметры, общепринятые по умолчанию, и сообщите, если они вам не подходят (для их изменения требуется редактирование кода модуля):
- одновременное списание и начисление бонусов в одном документе не производится, т.е. если в какой-то продаже есть списание бонусов, начисление баллов по ней уже не производится.
- начислямые бонусные баллы округляются до целого рубля по правилам мат.округления (0.5 до 1)
- бонусы не начисляются на товары, проданные со скидкой (цена в документе на момент его сохранения меньше цены в справочнике, т.е. была уменьшена в документе продавцом вручную или предоставлением скидки)
Скрипт модуля имеет открытый исходный код, вы можете вносить в него любые требующиеся вам изменения или заказывать эту работу нам.