Организация системы отслеживания и отключения КТВ должников на BGBS с использованием CRM плагина
Материал из BiTel WiKi
Admin (Обсуждение | вклад) |
Admin (Обсуждение | вклад) (Отмена правки № 553 участника Admin (обсуждение)) |
||
Строка 1: | Строка 1: | ||
+ | Ставится задача автоматического массового выявления, оповещения и отключения должников по КТВ. | ||
+ | |||
+ | В планировщике заданий добавляем генерацию события таймера. Событие генерируется каждые сутки в 0 часов 1 минуту. | ||
+ | |||
+ | {| | ||
+ | |- valign=top | ||
+ | | [[Изображение:ktv_debt_fix_timer.png|thumb|300px|Настройка задачи в планировщике]] | ||
+ | |} | ||
+ | |||
+ | Общий алгоритм работы следующий: | ||
+ | # Раз в месяц по таймеру запускается задача выявления должников. В параметрах договоров-должников проставляется дата фиксации долга и создается задача на обзвон должников. | ||
+ | # Далее производится обзвон (либо разнос квитанций), задачи помечаются выполненными и обрабатываются. | ||
+ | # Следюущая обработка события таймера зафиксировав выполненную задачу обзвона и констатировав, что долг еще есть - создает задачу на отключение должника. | ||
+ | # При обработке задачи отключения должника автоматически закрывается абонентская плата, устанавливается группа договора долг. | ||
+ | |||
+ | Типы задач: | ||
+ | # Отключение должника (код 3 в данном примере) | ||
+ | # Обзвон должника (код 23 в данном примере) | ||
+ | # Подключение должника (код 24 в данном примере) | ||
+ | |||
+ | Необходимые параметры договора: | ||
+ | # Адрес, тип '''Адрес''' (код 9) в данном примере | ||
+ | # Дата фиксации долга, тип '''Дата''' (код 34 в данном примере) | ||
+ | |||
+ | Необходимые группы договоров: | ||
+ | # Должник - пометка договора, октлюченного за долг (код группы 15 в данном примере) | ||
+ | |||
+ | В справочнике групп решения CRM могут быть определены одна или несколько групп, группа может определяться в зависимости от номера квартала. В данном примере есть две группы решения с кодами 1 и 2. | ||
+ | |||
+ | В меню '''Автоматизация=>Скрипты поведения''' добавляем обработчик данного события. | ||
+ | |||
+ | <source lang="java"> | ||
+ | import java.sql.*; | ||
+ | import java.util.*; | ||
+ | |||
+ | import bitel.billing.server.contract.bean.*; | ||
+ | import bitel.billing.server.script.bean.event.*; | ||
+ | import bitel.billing.server.util.*; | ||
+ | import ru.bitel.bgbilling.plugins.crm.server.dao.*; | ||
+ | import ru.bitel.bgbilling.plugins.crm.common.model.*; | ||
+ | |||
+ | // В зависимости от квартала клиента | ||
+ | // возможна передача различных групп решения | ||
+ | int getTaskGroup() { | ||
+ | quarter = ""; | ||
+ | |||
+ | query = | ||
+ | "SELECT quarter.title FROM contract_parameter_type_2 AS cp " + | ||
+ | " LEFT JOIN address_house AS house ON cp.hid=house.id " + | ||
+ | " LEFT JOIN address_quarter AS quarter ON house.quarterid=quarter.id " + | ||
+ | " WHERE cp.cid=? AND cp.pid=?"; | ||
+ | ps = con.prepareStatement( query ); | ||
+ | |||
+ | ps.setInt( 1, cid ); | ||
+ | ps.setInt( 2, TASK_ADDRESS_PARAM ); | ||
+ | |||
+ | rs = ps.executeQuery(); | ||
+ | if( rs.first() ) { | ||
+ | quarter = rs.getString( 1 ); | ||
+ | } | ||
+ | |||
+ | result = 0; | ||
+ | |||
+ | if( guarter.equals( "1" ) ) | ||
+ | { | ||
+ | result = GROUP_1; | ||
+ | } | ||
+ | else | ||
+ | { | ||
+ | result = GROUP_2; | ||
+ | } | ||
+ | |||
+ | return result; | ||
+ | } | ||
+ | |||
+ | // коды групп решения задач | ||
+ | GROUP_1 = 1; | ||
+ | GROUP_2 = 2; | ||
+ | |||
+ | // код типов задач на отключения и обзвон | ||
+ | DISCONNECT_TASK = 3; | ||
+ | CALL_TASK = 23; | ||
+ | |||
+ | // код параметра договора "Дата фиксации долга" | ||
+ | DEBT_FIX_PARAM = 34; | ||
+ | |||
+ | TASK_ADDRESS_PARAM = 9; | ||
+ | // сколько дней ждать после обзвона | ||
+ | DAYS_AFTER_CALL = 3; | ||
+ | // группа "Должник" | ||
+ | GROUP_DOLG = 15; | ||
+ | // группы "ЕРКЦ", "Условно расторгнут", "VIP" - договора с такими группами не наблюдаются | ||
+ | GROUP_ERKC = 43; | ||
+ | GROUP_USL_RAST = 21; | ||
+ | GROUP_VIP = 12; | ||
+ | |||
+ | // размер ежемесячного платежа (абонплаты) обычного | ||
+ | MONTHLY_CHARGE = 130; | ||
+ | // размер ежемесячного платежа (абонплаты) уменьшенного | ||
+ | MONTHLY_CHARGE_CHEAP = 105; | ||
+ | // дата подлкючения к ЕРКЦ | ||
+ | ERKC_ACTIVE_DATE_PARAM = 52; | ||
+ | |||
+ | cid = event.getContractID(); | ||
+ | time = event.getGenerateTime(); | ||
+ | |||
+ | print( "cid=" + cid ); | ||
+ | |||
+ | // проверка флага события таймера, обрабатываем только события с флагом = 1 | ||
+ | if( event.getFlag() != 1 ) { | ||
+ | print( "Flag != 1, skipping.." ); | ||
+ | return; | ||
+ | } | ||
+ | |||
+ | bu = new BalanceUtils( con ); | ||
+ | rtm = new RegisterTaskManager( con ); | ||
+ | cm = new ContractManager( con ); | ||
+ | cpu = new ContractParamUtils( con ); | ||
+ | tm = new ContractTariffManager( con ); | ||
+ | |||
+ | contract = cm.getContractByID( cid ); | ||
+ | |||
+ | // Проверки --------------------------------------------------------------------------------------------------------------- | ||
+ | |||
+ | // Договора ЕРКЦ не контролируются на долги, ЕРКЦ занимается этим сам | ||
+ | erkcGroup = ( contract.getGroups() & (1L<<GROUP_ERKC) ) > 0; | ||
+ | |||
+ | if( erkcGroup ) { | ||
+ | print( "erkc group" ); | ||
+ | erkcActiveDate = cpu.getDateParam( cid, ERKC_ACTIVE_DATE_PARAM ); | ||
+ | if ( erkcActiveDate == null ) { | ||
+ | error( "ERKC Activation Date is not set" ); | ||
+ | return; | ||
+ | } | ||
+ | else { | ||
+ | if ( time.before( erkcActiveDate ) ) { | ||
+ | print( "ERKC is not yet activated. Activation date: " + erkcActiveDate.get( Calendar.DAY_OF_MONTH ) + "." + ( erkcActiveDate.get( Calendar.MONTH ) + 1 ) + "." + erkcActiveDate.get( Calendar.YEAR ) ); | ||
+ | } | ||
+ | else { | ||
+ | print( "ERKC activated. No debts controlled" ); | ||
+ | //return; | ||
+ | } | ||
+ | } | ||
+ | } | ||
+ | |||
+ | // Условно расторгнутые | ||
+ | uslRastorg = ( contract.getGroups() & (1L<<GROUP_USL_RAST) ) > 0; | ||
+ | if( uslRastorg ) { | ||
+ | print( "usl rastorg group" ); | ||
+ | return; | ||
+ | } | ||
+ | |||
+ | // Должники | ||
+ | dolgGroup = ( contract.getGroups() & (1L<<GROUP_DOLG) ) > 0; | ||
+ | if( dolgGroup ) { | ||
+ | print( "dolg group" ); | ||
+ | return; | ||
+ | } | ||
+ | |||
+ | // VIP-клиенты | ||
+ | vipGroup = ( contract.getGroups() & (1L<<GROUP_VIP) ) > 0; | ||
+ | if( vipGroup ) { | ||
+ | print( "vip group" ); | ||
+ | return; | ||
+ | } | ||
+ | |||
+ | // Кредитные договора | ||
+ | if( contract.getBalanceMode() == Contract.CREDIT_BALANCE_MODE ) { | ||
+ | print( "credit mode" ); | ||
+ | return; | ||
+ | } | ||
+ | |||
+ | // Конец проверок --------------------------------------------------------------------------------------------------------- | ||
+ | |||
+ | debt = false; | ||
+ | float balance = bu.getBalance( time, cid ); | ||
+ | |||
+ | // Выбираем лимит под тариф | ||
+ | tp = tm.getContractTariff( cid, time ); | ||
+ | if( tp == null ) { | ||
+ | error("no active tariff plans" ); | ||
+ | return; | ||
+ | } | ||
+ | tpid = tp.getTariffPlanID(); | ||
+ | |||
+ | switch( tpid ) { | ||
+ | case 10: | ||
+ | monthlyCharge = MONTHLY_CHARGE; | ||
+ | break; | ||
+ | case 12: | ||
+ | monthlyCharge = MONTHLY_CHARGE_CHEAP; | ||
+ | break; | ||
+ | case 14: | ||
+ | monthlyCharge = MONTHLY_CHARGE_CHEAP; | ||
+ | break; | ||
+ | default: | ||
+ | monthlyCharge = MONTHLY_CHARGE; | ||
+ | break; | ||
+ | } | ||
+ | |||
+ | // до 20-го числа за должников считаем тех, у кого баланс меньше 3-х абонок, | ||
+ | // после 20-го -- меньше 2-х абонок | ||
+ | if( time.get( Calendar.DATE ) < 20 ) { | ||
+ | print("do 20"); | ||
+ | if( balance < - 3 * monthlyCharge + .01 ) { | ||
+ | debt = true; | ||
+ | } | ||
+ | } | ||
+ | else { | ||
+ | print("posle 20"); | ||
+ | if( balance < - 2 * monthlyCharge + .01 ) { | ||
+ | debt = true; | ||
+ | } | ||
+ | } | ||
+ | |||
+ | if( erkcGroup ) { | ||
+ | if( balance < - 6 * monthlyCharge + .01 ) { | ||
+ | debt = true; | ||
+ | print( "Debug ERKC < 6 month debt" ); | ||
+ | } | ||
+ | } | ||
+ | |||
+ | if( !debt ) { | ||
+ | return; | ||
+ | } | ||
+ | |||
+ | |||
+ | fixDate = cpu.getDateParam( cid, DEBT_FIX_PARAM ); | ||
+ | if( fixDate == null ) { | ||
+ | fixDate = (Calendar)time.clone(); | ||
+ | cpu.setDateParam( cid, DEBT_FIX_PARAM, fixDate ); | ||
+ | } | ||
+ | |||
+ | print( "fixDate=" + TimeUtils.formatDate( fixDate ) ); | ||
+ | |||
+ | callTask = null; | ||
+ | disconnectTask = null; | ||
+ | |||
+ | // список задач после даты фиксции долга | ||
+ | taskList = rtm.getAfterDateTaskList( cid, fixDate ); | ||
+ | for( RegisterTask task : taskList ) | ||
+ | { | ||
+ | // найдена задача на обзвон | ||
+ | if( task.getTypeID() == CALL_TASK ) | ||
+ | { | ||
+ | callTask = task; | ||
+ | } | ||
+ | // найдена задача на отключение | ||
+ | if( task.getTypeID() == DISCONNECT_TASK ) | ||
+ | { | ||
+ | disconnectTask = task; | ||
+ | } | ||
+ | } | ||
+ | |||
+ | print( "callTask = " + callTask + "; disconnectTask = " + disconnectTask ); | ||
+ | |||
+ | // долг есть а задачи на обзвон нет - создание задачи на обзвон | ||
+ | if( callTask == null ) | ||
+ | { | ||
+ | callTask = new RegisterTask(); | ||
+ | |||
+ | callTask.setContractID( cid ); | ||
+ | callTask.setAddressParamID( TASK_ADDRESS_PARAM ); | ||
+ | |||
+ | callTask.setOpenTime( time ); | ||
+ | callTask.setOpenUserID( 0 ); | ||
+ | |||
+ | callTask.setTypeID( CALL_TASK ); | ||
+ | callTask.setComment( "Долг: " + Utils.formatCost( balance ) ) ; | ||
+ | |||
+ | groupId = getTaskGroup(); | ||
+ | callTask.setGroupID( groupId ); | ||
+ | |||
+ | print( "creating call task" ) ; | ||
+ | rtm.updateTask( "new", callTask ); | ||
+ | } | ||
+ | // статус задачи открыт - обновляем информацию о долге в комментарии задачи | ||
+ | else if ( callTask.getStatus() == RegisterTask.STATUS_OPEN ) { | ||
+ | callTask.setComment( "Долг: " + Utils.formatCost( balance ) ) ; | ||
+ | rtm.updateTask( String.valueOf( callTask.getID() ), callTask ); | ||
+ | } | ||
+ | // задача дозвона выполнена | ||
+ | else if ( callTask.getStatus() == RegisterTask.STATUS_CLOSED ) { | ||
+ | // задачи на отключения нет | ||
+ | if( erkcGroup ) { | ||
+ | print( "For ERKC orders don't need taks for turn off" ) ; | ||
+ | return; | ||
+ | } | ||
+ | if ( disconnectTask == null ) { | ||
+ | // после обзвона прошел срок - создание задачи отключения | ||
+ | if ( TimeUtils.daysDelta( callTask.getExecuteDate(), time ) >= DAYS_AFTER_CALL ) { | ||
+ | disconnectTask = new RegisterTask(); | ||
+ | |||
+ | disconnectTask.setContractID( cid ); | ||
+ | disconnectTask.setAddressParamID( TASK_ADDRESS_PARAM ); | ||
+ | |||
+ | disconnectTask.setOpenTime( time ); | ||
+ | disconnectTask.setOpenUserID( 0 ); | ||
+ | |||
+ | disconnectTask.setTypeID( DISCONNECT_TASK ); | ||
+ | |||
+ | groupId = getTaskGroup(); | ||
+ | disconnectTask.setGroupID( groupId ); | ||
+ | disconnectTask.setComment( "Долг: " + Utils.formatCost( balance ) ) ; | ||
+ | |||
+ | print( "creating disconnect task." ); | ||
+ | rtm.updateTask( "new", disconnectTask ); | ||
+ | } | ||
+ | } | ||
+ | // открыта задача на отключение - обновление информации о долге в комментарии задачи | ||
+ | else if ( disconnectTask.getStatus() == RegisterTask.STATUS_OPEN ) { | ||
+ | disconnectTask.setComment( "Долг: " + Utils.formatCost( balance ) ) ; | ||
+ | rtm.updateTask( String.valueOf( disconnectTask.getID() ), disconnectTask ); | ||
+ | } | ||
+ | } | ||
+ | </source> | ||
+ | |||
+ | Предполагаем, что событие таймера обработано в результате чего получено множество задач типа '''Обзвон должника'''. | ||
+ | |||
+ | {| | ||
+ | |- valign=top | ||
+ | | [[Изображение:ktv_debt_debt_call_task.png|thumb|300px|Задачи обзвона должника]] | ||
+ | |} | ||
+ | |||
+ | Возможен непосредственный обзвон должников либо генерация квитанций и разнос их по квартирам. Как показывает практика, второй метод более эффективен. Для генерации квитанций подобного вида: | ||
+ | |||
+ | {| | ||
+ | |- valign=top | ||
+ | | [[Изображение:ktv_debt_debt_kvit.png|thumb|300px|Квитанции]] | ||
+ | |} | ||
+ | |||
+ | вы можете использовать шаблон отчета по задачам '''Квитанции'''. Для получения отчета необходимо выбрать тип шаблона над левым верхним углом таблицы задач, далее сохранить его в HTML файл и печатать. Непосредственная печать из биллинга невозможна, т.к. встроенный HTML компанент JAVA плохо поддерживает CSS, который используется для разделения страниц. | ||
+ | |||
+ | Шаблоны отчетов по задачам прописываются в конфигурации сервера биллинга следующим образом: | ||
+ | <pre> | ||
+ | register.task.report.format=register_tasks.xsl:Отчет по подключению;register_tasks_1.xsl:Отчет по обслуживанию;register_tasks_2.xsl:Отчет по должникам;register_tasks_3.xsl:Квитанции | ||
+ | </pre> | ||
+ | Файлы шаблонов вы можете загрузить здесь: [[Медиа:ktv_debt_xsl.zip]] | ||
+ | |||
+ | После разнесения квитанций/обзвонов должников оператор помечает задачи выполненными, указав дату выполнения. Для реагирования на оплаты клиентов создается скрипт на событие '''Приход платежа'''. Скрипт закрывает задачи обзвона и отключения, если клиент оплатил достаточную сумму. Для уже отключенных клиентов создается задача '''Подключение должника'''. Все действия сопровождаются письмами на почтовые рассылки. Задача на отключение закрывается только в том случае, если она не принята, т.е. монтажник не выехал к клиенту. | ||
+ | |||
+ | <source lang="java"> | ||
+ | import bitel.billing.server.contract.bean.*; | ||
+ | import bitel.billing.server.util.*; | ||
+ | import java.util.*; | ||
+ | import ru.bitel.bgbilling.plugins.crm.server.dao.*; | ||
+ | import ru.bitel.bgbilling.plugins.crm.common.model.*; | ||
+ | import bitel.billing.server.model.*; | ||
+ | |||
+ | cid = event.getContractID(); | ||
+ | time = event.getGenerateTime(); | ||
+ | |||
// В зависимости от квартала клиента | // В зависимости от квартала клиента | ||
// возможна передача различных групп решения | // возможна передача различных групп решения | ||
Строка 206: | Строка 558: | ||
<source lang="java"> | <source lang="java"> | ||
+ | |||
import java.sql.*; | import java.sql.*; | ||
import java.util.*; | import java.util.*; |
Версия 07:14, 25 сентября 2008
Ставится задача автоматического массового выявления, оповещения и отключения должников по КТВ.
В планировщике заданий добавляем генерацию события таймера. Событие генерируется каждые сутки в 0 часов 1 минуту.
Общий алгоритм работы следующий:
- Раз в месяц по таймеру запускается задача выявления должников. В параметрах договоров-должников проставляется дата фиксации долга и создается задача на обзвон должников.
- Далее производится обзвон (либо разнос квитанций), задачи помечаются выполненными и обрабатываются.
- Следюущая обработка события таймера зафиксировав выполненную задачу обзвона и констатировав, что долг еще есть - создает задачу на отключение должника.
- При обработке задачи отключения должника автоматически закрывается абонентская плата, устанавливается группа договора долг.
Типы задач:
- Отключение должника (код 3 в данном примере)
- Обзвон должника (код 23 в данном примере)
- Подключение должника (код 24 в данном примере)
Необходимые параметры договора:
- Адрес, тип Адрес (код 9) в данном примере
- Дата фиксации долга, тип Дата (код 34 в данном примере)
Необходимые группы договоров:
- Должник - пометка договора, октлюченного за долг (код группы 15 в данном примере)
В справочнике групп решения CRM могут быть определены одна или несколько групп, группа может определяться в зависимости от номера квартала. В данном примере есть две группы решения с кодами 1 и 2.
В меню Автоматизация=>Скрипты поведения добавляем обработчик данного события.
import java.sql.*; import java.util.*; import bitel.billing.server.contract.bean.*; import bitel.billing.server.script.bean.event.*; import bitel.billing.server.util.*; import ru.bitel.bgbilling.plugins.crm.server.dao.*; import ru.bitel.bgbilling.plugins.crm.common.model.*; // В зависимости от квартала клиента // возможна передача различных групп решения int getTaskGroup() { quarter = ""; query = "SELECT quarter.title FROM contract_parameter_type_2 AS cp " + " LEFT JOIN address_house AS house ON cp.hid=house.id " + " LEFT JOIN address_quarter AS quarter ON house.quarterid=quarter.id " + " WHERE cp.cid=? AND cp.pid=?"; ps = con.prepareStatement( query ); ps.setInt( 1, cid ); ps.setInt( 2, TASK_ADDRESS_PARAM ); rs = ps.executeQuery(); if( rs.first() ) { quarter = rs.getString( 1 ); } result = 0; if( guarter.equals( "1" ) ) { result = GROUP_1; } else { result = GROUP_2; } return result; } // коды групп решения задач GROUP_1 = 1; GROUP_2 = 2; // код типов задач на отключения и обзвон DISCONNECT_TASK = 3; CALL_TASK = 23; // код параметра договора "Дата фиксации долга" DEBT_FIX_PARAM = 34; TASK_ADDRESS_PARAM = 9; // сколько дней ждать после обзвона DAYS_AFTER_CALL = 3; // группа "Должник" GROUP_DOLG = 15; // группы "ЕРКЦ", "Условно расторгнут", "VIP" - договора с такими группами не наблюдаются GROUP_ERKC = 43; GROUP_USL_RAST = 21; GROUP_VIP = 12; // размер ежемесячного платежа (абонплаты) обычного MONTHLY_CHARGE = 130; // размер ежемесячного платежа (абонплаты) уменьшенного MONTHLY_CHARGE_CHEAP = 105; // дата подлкючения к ЕРКЦ ERKC_ACTIVE_DATE_PARAM = 52; cid = event.getContractID(); time = event.getGenerateTime(); print( "cid=" + cid ); // проверка флага события таймера, обрабатываем только события с флагом = 1 if( event.getFlag() != 1 ) { print( "Flag != 1, skipping.." ); return; } bu = new BalanceUtils( con ); rtm = new RegisterTaskManager( con ); cm = new ContractManager( con ); cpu = new ContractParamUtils( con ); tm = new ContractTariffManager( con ); contract = cm.getContractByID( cid ); // Проверки --------------------------------------------------------------------------------------------------------------- // Договора ЕРКЦ не контролируются на долги, ЕРКЦ занимается этим сам erkcGroup = ( contract.getGroups() & (1L<<GROUP_ERKC) ) > 0; if( erkcGroup ) { print( "erkc group" ); erkcActiveDate = cpu.getDateParam( cid, ERKC_ACTIVE_DATE_PARAM ); if ( erkcActiveDate == null ) { error( "ERKC Activation Date is not set" ); return; } else { if ( time.before( erkcActiveDate ) ) { print( "ERKC is not yet activated. Activation date: " + erkcActiveDate.get( Calendar.DAY_OF_MONTH ) + "." + ( erkcActiveDate.get( Calendar.MONTH ) + 1 ) + "." + erkcActiveDate.get( Calendar.YEAR ) ); } else { print( "ERKC activated. No debts controlled" ); //return; } } } // Условно расторгнутые uslRastorg = ( contract.getGroups() & (1L<<GROUP_USL_RAST) ) > 0; if( uslRastorg ) { print( "usl rastorg group" ); return; } // Должники dolgGroup = ( contract.getGroups() & (1L<<GROUP_DOLG) ) > 0; if( dolgGroup ) { print( "dolg group" ); return; } // VIP-клиенты vipGroup = ( contract.getGroups() & (1L<<GROUP_VIP) ) > 0; if( vipGroup ) { print( "vip group" ); return; } // Кредитные договора if( contract.getBalanceMode() == Contract.CREDIT_BALANCE_MODE ) { print( "credit mode" ); return; } // Конец проверок --------------------------------------------------------------------------------------------------------- debt = false; float balance = bu.getBalance( time, cid ); // Выбираем лимит под тариф tp = tm.getContractTariff( cid, time ); if( tp == null ) { error("no active tariff plans" ); return; } tpid = tp.getTariffPlanID(); switch( tpid ) { case 10: monthlyCharge = MONTHLY_CHARGE; break; case 12: monthlyCharge = MONTHLY_CHARGE_CHEAP; break; case 14: monthlyCharge = MONTHLY_CHARGE_CHEAP; break; default: monthlyCharge = MONTHLY_CHARGE; break; } // до 20-го числа за должников считаем тех, у кого баланс меньше 3-х абонок, // после 20-го -- меньше 2-х абонок if( time.get( Calendar.DATE ) < 20 ) { print("do 20"); if( balance < - 3 * monthlyCharge + .01 ) { debt = true; } } else { print("posle 20"); if( balance < - 2 * monthlyCharge + .01 ) { debt = true; } } if( erkcGroup ) { if( balance < - 6 * monthlyCharge + .01 ) { debt = true; print( "Debug ERKC < 6 month debt" ); } } if( !debt ) { return; } fixDate = cpu.getDateParam( cid, DEBT_FIX_PARAM ); if( fixDate == null ) { fixDate = (Calendar)time.clone(); cpu.setDateParam( cid, DEBT_FIX_PARAM, fixDate ); } print( "fixDate=" + TimeUtils.formatDate( fixDate ) ); callTask = null; disconnectTask = null; // список задач после даты фиксции долга taskList = rtm.getAfterDateTaskList( cid, fixDate ); for( RegisterTask task : taskList ) { // найдена задача на обзвон if( task.getTypeID() == CALL_TASK ) { callTask = task; } // найдена задача на отключение if( task.getTypeID() == DISCONNECT_TASK ) { disconnectTask = task; } } print( "callTask = " + callTask + "; disconnectTask = " + disconnectTask ); // долг есть а задачи на обзвон нет - создание задачи на обзвон if( callTask == null ) { callTask = new RegisterTask(); callTask.setContractID( cid ); callTask.setAddressParamID( TASK_ADDRESS_PARAM ); callTask.setOpenTime( time ); callTask.setOpenUserID( 0 ); callTask.setTypeID( CALL_TASK ); callTask.setComment( "Долг: " + Utils.formatCost( balance ) ) ; groupId = getTaskGroup(); callTask.setGroupID( groupId ); print( "creating call task" ) ; rtm.updateTask( "new", callTask ); } // статус задачи открыт - обновляем информацию о долге в комментарии задачи else if ( callTask.getStatus() == RegisterTask.STATUS_OPEN ) { callTask.setComment( "Долг: " + Utils.formatCost( balance ) ) ; rtm.updateTask( String.valueOf( callTask.getID() ), callTask ); } // задача дозвона выполнена else if ( callTask.getStatus() == RegisterTask.STATUS_CLOSED ) { // задачи на отключения нет if( erkcGroup ) { print( "For ERKC orders don't need taks for turn off" ) ; return; } if ( disconnectTask == null ) { // после обзвона прошел срок - создание задачи отключения if ( TimeUtils.daysDelta( callTask.getExecuteDate(), time ) >= DAYS_AFTER_CALL ) { disconnectTask = new RegisterTask(); disconnectTask.setContractID( cid ); disconnectTask.setAddressParamID( TASK_ADDRESS_PARAM ); disconnectTask.setOpenTime( time ); disconnectTask.setOpenUserID( 0 ); disconnectTask.setTypeID( DISCONNECT_TASK ); groupId = getTaskGroup(); disconnectTask.setGroupID( groupId ); disconnectTask.setComment( "Долг: " + Utils.formatCost( balance ) ) ; print( "creating disconnect task." ); rtm.updateTask( "new", disconnectTask ); } } // открыта задача на отключение - обновление информации о долге в комментарии задачи else if ( disconnectTask.getStatus() == RegisterTask.STATUS_OPEN ) { disconnectTask.setComment( "Долг: " + Utils.formatCost( balance ) ) ; rtm.updateTask( String.valueOf( disconnectTask.getID() ), disconnectTask ); } }
Предполагаем, что событие таймера обработано в результате чего получено множество задач типа Обзвон должника.
Возможен непосредственный обзвон должников либо генерация квитанций и разнос их по квартирам. Как показывает практика, второй метод более эффективен. Для генерации квитанций подобного вида:
вы можете использовать шаблон отчета по задачам Квитанции. Для получения отчета необходимо выбрать тип шаблона над левым верхним углом таблицы задач, далее сохранить его в HTML файл и печатать. Непосредственная печать из биллинга невозможна, т.к. встроенный HTML компанент JAVA плохо поддерживает CSS, который используется для разделения страниц.
Шаблоны отчетов по задачам прописываются в конфигурации сервера биллинга следующим образом:
register.task.report.format=register_tasks.xsl:Отчет по подключению;register_tasks_1.xsl:Отчет по обслуживанию;register_tasks_2.xsl:Отчет по должникам;register_tasks_3.xsl:Квитанции
Файлы шаблонов вы можете загрузить здесь: Медиа:ktv_debt_xsl.zip
После разнесения квитанций/обзвонов должников оператор помечает задачи выполненными, указав дату выполнения. Для реагирования на оплаты клиентов создается скрипт на событие Приход платежа. Скрипт закрывает задачи обзвона и отключения, если клиент оплатил достаточную сумму. Для уже отключенных клиентов создается задача Подключение должника. Все действия сопровождаются письмами на почтовые рассылки. Задача на отключение закрывается только в том случае, если она не принята, т.е. монтажник не выехал к клиенту.
import bitel.billing.server.contract.bean.*; import bitel.billing.server.util.*; import java.util.*; import ru.bitel.bgbilling.plugins.crm.server.dao.*; import ru.bitel.bgbilling.plugins.crm.common.model.*; import bitel.billing.server.model.*; cid = event.getContractID(); time = event.getGenerateTime(); // В зависимости от квартала клиента // возможна передача различных групп решения int getTaskGroup() { quarter = ""; query = "SELECT quarter.title FROM contract_parameter_type_2 AS cp " + " LEFT JOIN address_house AS house ON cp.hid=house.id " + " LEFT JOIN address_quarter AS quarter ON house.quarterid=quarter.id " + " WHERE cp.cid=? AND cp.pid=?"; ps = con.prepareStatement( query ); ps.setInt( 1, cid ); ps.setInt( 2, TASK_ADDRESS_PARAM ); rs = ps.executeQuery(); if( rs.first() ) { quarter = rs.getString( 1 ); } result = 0; if( guarter.equals( "1" ) ) { result = GROUP_1; } else { result = GROUP_2; } return result; } // коды групп решения задач GROUP_1 = 1; GROUP_2 = 2; DISCONNECT_TASK = 3; CALL_TASK = 23; CONNECT_TASK = 24; TASK_ADDRESS_PARAM = 9; GROUP_DOLG = 15; // коды параметров договора ФИО и Телефон FIO_PARAM = "1"; PHONE_PARAM = 2; DEBT_FIX_PARAM = 34; DEBT_LIMIT = 260; // минимальная сумма баланса, после которой подключать BALANCE_BORDER = 164.99f; cpu = new ContractParamUtils( con ); contractManager = new ContractManager( con ); taskManager = new RegisterTaskManager( con ); bu = new BalanceUtils( con ); balance = bu.getBalance( time, cid ); contract = contractManager.getContractByID( cid ); dolgGroup = (contract.getGroups() & (1<<GROUP_DOLG) ) > 0; print( "dolg group => " + dolgGroup ); // оплатил должник - можно подлкючать if( dolgGroup && balance > BALANCE_BORDER ) { // есть не закрытая задача "подключение должника" connectTask = false; filter = new RegisterTaskManager.TaskFilter(); filter.types = String.valueOf( CONNECT_TASK ); filter.cid = cid; activeTasks = taskManager.getNoClosedTaskList( cid ); for( RegisterTask task : activeTasks ) { // задача не закрыта либо закрыта но не обработана if( task.getStatus() != RegisterTask.STATUS_CLOSED || !task.isProcessed() ) { connectTask = true; print( "1" ); break; } } print( "connectTask => " + connectTask ); // создание задачи на подключение if( !connectTask ) { RegisterTask task = new RegisterTask(); task.setContractID( cid ); task.setTypeID( CONNECT_TASK ); groupId = getTaskGroup(); task.setGroupID( groupId ); task.setOpenUserID( 0 ) ; task.setOpenTime( event.getGenerateTime() ); task.setComment( "" ); task.setAddressParamID( TASK_ADDRESS_PARAM ); taskManager.updateTask( "new", task ); //отправка письма на почту filter = new RegisterTaskManager.TaskFilter(); filter.id = task.getID(); filter.fioParams = FIO_PARAM; filter.phoneParam = PHONE_PARAM; filter.processed = -1; filter.orders = new ArrayList(); print( "filter = " + filter + "; taskManager = " + taskManager ); personalTask = taskManager.getTaskList( filter, new Page( 1, 1 ) ).get( 0 ); message = new StringBuffer( 300 ); message.append( contract.getTitle() ); message.append( " " ); message.append( personalTask.getStreet() ); message.append( " " ); message.append( personalTask.getHouse() ); message.append( " " ); message.append( personalTask.getFlat() ); new MailMsg( setup ).sendMessageEx( "disp@disp.com", "Оплатил должник", message.toString(), "text/plain" ); // print( "Sending to mail.." ); } } // работающий клиент else { if( balance <= - ( DEBT_LIMIT - 1 ) ) { print( "it's debt" ); return; } // закрытие активных задач на дозвон и отключение должника List activeTasks = taskManager.getNoClosedTaskList( cid ); for( RegisterTask task : activeTasks ) { // собственно закрытие таких задач if( ( task.getTypeID() == DISCONNECT_TASK && task.getStatus() == RegisterTask.STATUS_OPEN ) || (task.getTypeID() == CALL_TASK && task.getStatus() != RegisterTask.STATUS_CLOSED ) ) { task.setResolution( task.getResolution() + "\nОплатил " + TimeUtils.formatDate( time ) ); task.setCloseUserID( 0 ); task.setCloseTime( time ); task.setExecuteDate( time ); task.setGroupID( DEFAULT_GROUP ); task.setStatus( RegisterTask.STATUS_CLOSED ); taskManager.updateTask( String.valueOf( task.getID() ), task ); print( "Closing DISCONNECT and CALL task: " + task.getID() ); } // если принятая задача на отключение - отправка письма if( task.getTypeID() == DISCONNECT_TASK && task.getStatus() == RegisterTask.STATUS_ACCEPTED ) { print( "The task already has an ACCEPTED status." ); //отправка письма на почту filter = new RegisterTaskManager.TaskFilter(); filter.id = task.getID(); filter.fioParams = FIO_PARAM; filter.phoneParam = PHONE_PARAM; filter.processed = -1; filter.orders = new ArrayList(); personalTask = taskManager.getTaskList( filter, new Page( 1, 1 ) ).get( 0 ); message = new StringBuffer( 300 ); message.append( personalTask.getStreet() ); message.append( " " ); message.append( personalTask.getHouse() ); message.append( " " ); message.append( personalTask.getFlat() ); message.append( " " ); message.append( personalTask.getContract() ); new MailMsg( setup ).sendMessageEx( "disp@disp.com", "Оплатил отключаемый", message.toString(), "text/plain" ); print( "Send message Pay in disconnect" ); } } print( "Deleting debt fix date.." ); cpu.deleteDateParam( cid, DEBT_FIX_PARAM ); }
Для не оплативших в течении указанного количества дней после выполненной задачи обзвона должников создаются задачи на Отключение должника. Они могут быть распечатаны в виде нарядов монтажникам, для чего используется шаблон отчета журнала задач Отчет по подключению (файл выложен выше в статье). Этот же шаблон можно использовать при создании нарядов на подключение.
Выполненные задачи помечаются выполненными и обрабатываются оператором. При обработке задач отрабатывает скрипт для задач типа Подключение должника и Отключение должника.
import java.sql.*; import java.util.*; import bitel.billing.server.contract.bean.*; import bitel.billing.server.util.*; import ru.bitel.bgbilling.plugins.crm.server.dao.*; import ru.bitel.bgbilling.plugins.crm.common.model.*; import bitel.billing.server.model.*; DISCONNECT_TASK = 3; CONNECT_TASK = 24; // группа должник GROUP_DOLG = 15; // группа "пенсионер" GROUP_PENS = 34; // код услуги "абонплата" SERVICE_PAY = 2; // код услуги "абонплата долг" SERVICE_PAY_DOLG = 3; // код услуги "абонплата пенсионеров" SERVICE_PAY_PENS = 8; // код расхода "за повторное включение" RECONNECT_CHARGE = 5; DEBT_FIX_PARAM = 34; // сумма расхода за повторное включение RECONNECT_SUM = 75; BALANCE_BORDER = 164.99f; ADDRESS_PARAM = 9; FIO_PARAM = "1"; PHONE_PARAM = 2; cid = event.getContractID(); task = event.getTask(); // В зависимости от квартала клиента // возможна передача различных групп решения int getTaskGroup() { quarter = ""; query = "SELECT quarter.title FROM contract_parameter_type_2 AS cp " + " LEFT JOIN address_house AS house ON cp.hid=house.id " + " LEFT JOIN address_quarter AS quarter ON house.quarterid=quarter.id " + " WHERE cp.cid=? AND cp.pid=?"; ps = con.prepareStatement( query ); ps.setInt( 1, cid ); ps.setInt( 2, TASK_ADDRESS_PARAM ); rs = ps.executeQuery(); if( rs.first() ) { quarter = rs.getString( 1 ); } result = 0; if( guarter.equals( "1" ) ) { result = GROUP_1; } else { result = GROUP_2; } return result; } // коды групп решения задач GROUP_1 = 1; GROUP_2 = 2; report = event.getReport(); serviceManager = new ContractServiceManager( con ); cu = new ContractUtils( con ); cpu = new ContractParamUtils( con ); chm = new ChargeManager( con ); contractManager = new ContractManager( con ); bu = new BalanceUtils( con ); taskManager = new RegisterTaskManager( con ); if( task.getTypeID() != DISCONNECT_TASK && task.getTypeID() != CONNECT_TASK ) { print( "This task type does't processing.." ); return; } if( task.getStatus() != RegisterTask.STATUS_CLOSED ) { report.append( cu.getContractTitle( cid, true ) ); report.append( " => задача не закрыта\n" ); return; } Calendar date = task.getExecuteDate(); if( date == null ) { report.append( cu.getContractTitle( cid, true ) ); report.append( " => не установлена дата исполнения\n" ); error( "executeDate == null" ); return; } beforeDay = task.getExecuteDate().clone(); beforeDay.add( Calendar.DAY_OF_YEAR, -1 ); // перечень услуг на дату services = serviceManager.getContractServiceList( cid, date ); contract = contractManager.getContractByID( cid ); dolgGroup = (contract.getGroups() & (1<<GROUP_DOLG) ) > 0; print( "dolg group => " + dolgGroup ); pensGroup = (contract.getGroups() & (1L<<GROUP_PENS) ) > 0; print( "pens group => " + pensGroup ); // отключение должника if( task.getTypeID() == DISCONNECT_TASK ) { if( dolgGroup ) { report.append( cu.getContractTitle( cid, true ) ); report.append( " => у договора уже стоит группа долг\n" ); return; } for( ContractService service : services ) { // закрытие абонплаты if( service.getServiceID() == SERVICE_PAY || service.getServiceID() == SERVICE_PAY_PENS ) { service.setDate2( beforeDay ); serviceManager.updateContractService( String.valueOf( service.getID() ), service ); } } // открытие абонплаты "долг" cs = new ContractService(); cs.setContractID( cid ); cs.setServiceID( SERVICE_PAY_DOLG ); cs.setDate1( task.getExecuteDate() ); cs.setComment( "Установлена скриптом" ); serviceManager.updateContractService( "new", cs ); // установка группы "долг" cpu.setGroup( cid, GROUP_DOLG ); // если баланс позволяет - сразу открытие задачи "Подключение должника" balance = bu.getBalance( new GregorianCalendar(), cid ); print( "balance=" + balance ); if( balance >= BALANCE_BORDER ) { // есть не закрытая задача "подключение должника" connectTask = false; List activeTasks = taskManager.getNoClosedTaskList( cid ); for( RegisterTask task : activeTasks ) { print( "task type: " + task.getTypeID() ) ; if( task.getTypeID() == CONNECT_TASK ) { connectTask = true; break; } } print( "connectTask => " + connectTask ); // создание задачи на подключение if( !connectTask ) { task = new RegisterTask(); task.setContractID( cid ); task.setTypeID( CONNECT_TASK ); task.setGroupID( getTaskGroup() ); task.setOpenUserID( 0 ) ; task.setOpenTime( event.getGenerateTime() ); task.setComment( "" ); task.setAddressParamID( ADDRESS_PARAM ); taskManager.updateTask( "new", task ); report.append( cu.getContractTitle( cid, true ) ); report.append( " => создана задача на подключение\n" ); //отправка письма на почту filter = new RegisterTaskManager.TaskFilter(); filter.id = task.getID(); filter.fioParams = FIO_PARAM; filter.phoneParam = PHONE_PARAM; filter.processed = -1; filter.orders = new ArrayList(); personalTask = taskManager.getTaskList( filter, new Page( 1, 1 ) ).get( 0 ); message = new StringBuffer( 300 ); message.append( personalTask.getStreet() ); message.append( " " ); message.append( personalTask.getHouse() ); message.append( " " ); message.append( personalTask.getFlat() ); message.append( " " ); message.append( personalTask.getContract() ); new MailMsg( setup ).sendMessageEx( "bill@ufanet.ru;disp@ufanet.ru;setks@ufanet.ru", "Оплатил отключенный", message.toString(), "text/plain" ); print( "Send message Pay in disconnect process" ); } } } // подключение должника else if( task.getTypeID() == CONNECT_TASK ) { if( !dolgGroup ) { report.append( cu.getContractTitle( cid, true ) ); report.append( " => у договора нет группы долг\n" ); return; } for( ContractService service : services ) { // закрытие абонплаты "долг" if( service.getServiceID() == SERVICE_PAY_DOLG ) { service.setDate2( beforeDay ); serviceManager.updateContractService( String.valueOf( service.getID() ), service ); } } // открытие абонплаты cs = new ContractService(); cs.setContractID( cid ); if( pensGroup ) { cs.setServiceID( SERVICE_PAY_PENS ); } else { cs.setServiceID( SERVICE_PAY ); } cs.setDate1( task.getExecuteDate() ); cs.setComment( "Установлена скриптом" ); serviceManager.updateContractService( "new", cs ); // снятие расхода за повторное подключение charge = new Charge(); charge.setContractID( cid ); charge.setChargeTypeID( RECONNECT_CHARGE ); charge.setSumma( RECONNECT_SUM ); charge.setDate( task.getExecuteDate() ); charge.setComment( "Установлена скриптом" ); chm.updateCharge( "new", charge ); print( "setting charge" ); bu.updateBalance( task.getExecuteDate(), cid ); // сброс группы "Должник" cpu.unsetGroup( cid, GROUP_DOLG ); cpu.deleteDateParam( cid, DEBT_FIX_PARAM ); } task.setProcessed( true );
Когда клиент отключен за долг у него устанавливается абонплата Долг с нулевой ценой. Разумного требованию этому нет, сделано с точки зрения аналитики базы. При обработке задач скрипт выдает отчет по ошибкам и нестыковкам оператору.