Персональные цены для договоров
Материал из BiTel WiKi
Внимание! Данное решение/метод/статья относится к версии 5.2 и для других версий может быть неактуальна! Вам нужно самостоятельно поправить решение под свои нужды или воспользоваться помощью на форуме. Будем признательны, если внизу страницы или отдельной статьёй вы разместите исправленное решение для другой версии или подсказки что надо исправить.
Содержание |
Введение
Очень часто, особенно при работе с юр лицами, возникает потребность установки персональной цены абонплат для стандартных тарифов. В текущей реализации биллинга это можно сделать только через персональные тарифы. Но персональные тарифы очень неудобны:
- Ломают отчёты по тарифам
- Осложняют внесение изменений в глобальные тарифы, от которых они наследованы
- Требуют специальных знаний и прав доступа для корректного использования
В результате было решено разработать собственное решение для поддержки персональных цен без применения персональных тарифов
Описание решения
Тезисно решение выглядит следующим образом
- Храним персональные цены на услуги в собственной таблице custom_tariff_cost: contract_tariff_id, sid -> value
- Меняем стандартную логику тарифного узла "стоимость" модуля абонплат: при наличии записи в custom_tariff_cost используем её вместо стандартной цены
- Пишем собственную серверную и клиентскую части интерфейса для управления переопределёнными ценами
- Дорабатываем выдачу списка тарифов договора для визуального отображения переопределённых цен
!!! Внимание! пп. 2 и 3 являются "хаками" стандартного поведения биллинга: требуется подмена стандартных классов своими !!!
Скриншоты:
Интерфейс переопределения цен:
Отображение на договоре:
В тарифном дереве:
Реализация
Переопределение цены
Таблица для хранения переопределений цен
CREATE TABLE `custom_tariff_cost` ( `sid` int(10) UNSIGNED NOT NULL, `value` decimal(15,5) NOT NULL, `contract_tariff_id` int(10) UNSIGNED NOT NULL, PRIMARY KEY (`sid`,`contract_tariff_id`), KEY `sid` (`sid`)
Обратите внимания, что переопределяется цена на услугу не для договора (и всех его тарифов), а для _тарифа_ договора (custom_tariff_cost.contract_tariff_id->contract_tariff.id).
Это сделано осознанно, по опыту проблем с тарифными опциями, которые на деле являются опциями договора, хотя и ориентированы на конкретные тарифы.
Другой момент - в таблице нет дат. Это тоже сделано осознанно, чтобы не плодить сущности. Т.о. если требуется изменить на том же тарифе для договора персональную цену, то нужно закрыть датой тариф договора с предыдущей ценой и открыть датой тот же глобальный тариф с новой ценой.
Кэш таблицы переопределений цен в памяти для ускорения тарификации
Для начала напишем DAO для работы с таблицей.
Объект:
package ru.dsi.bgbilling.kernel.discount.api.common.bean; import java.io.Serializable; import java.math.BigDecimal; import java.util.Date; /** * @author cromeshnic@gmail.com */ public class CustomTariffCost implements Serializable { private int contract_tariff_id; private int sid; private BigDecimal value; private Date date1; private Date date2; public CustomTariffCost(){ this.contract_tariff_id = -1; this.sid = -1; this.value = null; this.date1 = null; this.date2 = null; } @Override public String toString() { StringBuilder sb = new StringBuilder("["); sb.append("contract_tariff_id = ") .append(this.contract_tariff_id) .append(", sid = ") .append(this.sid) .append(", value = ") .append(this.value) .append("]"); return sb.toString(); } //public ru.dsi.bgbilling.kernel.discount.api.common.bean.CustomTariffCost(int cid, int sid, int contract_tariff_id, BigDecimal value, Date date1, Date date2) { public CustomTariffCost(int contract_tariff_id, int sid, BigDecimal value, Date date1, Date date2) { //this.cid = cid; this.contract_tariff_id = contract_tariff_id; this.sid = sid; this.value = value; this.date1 = date1; this.date2 = date2; } public CustomTariffCost(int contract_tariff_id, int sid, BigDecimal value){ this.contract_tariff_id = contract_tariff_id; this.sid = sid; this.value = value; this.date1 = null; this.date2 = null; } public int getContract_tariff_id() { return contract_tariff_id; } public void setContract_tariff_id(int contract_tariff_id) { this.contract_tariff_id = contract_tariff_id; } public int getSid() { return sid; } public void setSid(int sid) { this.sid = sid; } public BigDecimal getValue() { return value; } public void setValue(BigDecimal value) { this.value = value; } public Date getDate1() { return date1; } public void setDate1(Date date1) { this.date1 = date1; } public Date getDate2() { return date2; } public void setDate2(Date date2) { this.date2 = date2; } }
События изменения (для кэша):
package ru.dsi.bgbilling.kernel.discount.bean.event; import ru.bitel.bgbilling.kernel.event.Event; import ru.bitel.common.SerialUtils; import javax.xml.bind.annotation.XmlRootElement; /** * @author cromeshnic@gmail.com * Событие при изменении персональной цены на договоре */ @XmlRootElement public class ContractCustomTariffCostsChangedEvent extends Event//QueueEvent { private static final long serialVersionUID = SerialUtils.generateSerialVersionUID(ContractCustomTariffCostsChangedEvent.class); public ContractCustomTariffCostsChangedEvent(int cid, int userId) { super(0, cid, userId); } protected ContractCustomTariffCostsChangedEvent(){ } }
package ru.dsi.bgbilling.kernel.discount.bean.event; import ru.bitel.bgbilling.kernel.event.Event; import ru.bitel.common.SerialUtils; import ru.dsi.bgbilling.kernel.discount.api.common.bean.CustomTariffCost; import javax.xml.bind.annotation.XmlRootElement; /** * @author cromeshnic@gmail.com * Событие при изменении персональной цены на тарифе договора */ @XmlRootElement public class CustomTariffCostChangedEvent extends Event//QueueEvent { private static final long serialVersionUID = SerialUtils.generateSerialVersionUID(ContractCustomTariffCostsChangedEvent.class); private CustomTariffCost cost; public CustomTariffCostChangedEvent(CustomTariffCost cost, int cid, int userId) { super(0, cid, userId); this.cost = cost; } public CustomTariffCost getCost(){ return this.cost; } protected CustomTariffCostChangedEvent(){ this.cost = null; } }
DAO:
package ru.dsi.bgbilling.kernel.discount.bean; import bitel.billing.common.TimeUtils; import bitel.billing.server.contract.bean.ContractTariff; import bitel.billing.server.contract.bean.ContractTariffManager; import org.apache.log4j.Logger; import ru.bitel.bgbilling.common.BGException; import ru.bitel.bgbilling.kernel.event.EventProcessor; import ru.bitel.bgbilling.server.util.ServerUtils; import ru.dsi.bgbilling.kernel.discount.api.common.bean.CustomTariffCost; import ru.dsi.bgbilling.kernel.discount.bean.event.ContractCustomTariffCostsChangedEvent; import ru.dsi.bgbilling.kernel.discount.bean.event.CustomTariffCostChangedEvent; import java.sql.Connection; import java.sql.PreparedStatement; import java.sql.ResultSet; import java.sql.SQLException; import java.util.ArrayList; import java.util.List; /** * @author cromeshnic@gmail.com * Dao для работы с персональными ценами */ public class CustomTariffCostDao{ protected Connection con; private static final Logger logger = Logger.getLogger(CustomTariffCostDao.class); public CustomTariffCostDao(Connection con) { this.con = con; } private void removeContractCustomTariffCosts(int contract_tariff_id) throws SQLException{ PreparedStatement ps = this.con.prepareStatement("delete from custom_tariff_cost where contract_tariff_id=?"); ps.setInt(1, contract_tariff_id); ps.executeUpdate(); ps.close(); } public void updateContractCustomTariffCosts(ContractTariff ct, List<CustomTariffCost> costs, int userId) throws SQLException, BGException{ this.removeContractCustomTariffCosts(ct.getId()); if(null!=costs){ for(CustomTariffCost cost : costs){ if(ct.getId()!=cost.getContract_tariff_id()){ throw new BGException("Attempt to update custom tariff cost: "+cost.toString()+" for another contract tariff = "+ct.getId()); } this.update0(cost); } } EventProcessor.getInstance().publish(new ContractCustomTariffCostsChangedEvent(ct.getContractId(), userId)); } public CustomTariffCost update(CustomTariffCost cost, int userId) throws SQLException, BGException { ContractTariffManager ctm = new ContractTariffManager(this.con); ContractTariff ct = ctm.getContractTariffById(cost.getContract_tariff_id()); cost.setDate1(TimeUtils.convertCalendarToDate(ct.getDate1())); cost.setDate2(TimeUtils.convertCalendarToDate(ct.getDate2())); update0(cost); EventProcessor.getInstance().publish(new CustomTariffCostChangedEvent(cost, ct.getContractId(), userId)); return cost; } private void update0(CustomTariffCost cost) throws SQLException, BGException{ PreparedStatement ps = this.con.prepareStatement( "insert into custom_tariff_cost (contract_tariff_id, sid, value) values(?,?,?) " + "on duplicate key update value=?"); ps.setInt(1, cost.getContract_tariff_id()); ps.setInt(2, cost.getSid()); ps.setBigDecimal(3, cost.getValue()); ps.setBigDecimal(4, cost.getValue()); ps.executeUpdate(); ps.close(); ServerUtils.commitConnection(this.con); } public List<CustomTariffCost> getContractTariffCosts(ContractTariff ct) throws BGException { List<CustomTariffCost> result = new ArrayList<CustomTariffCost>(); try{ PreparedStatement ps = this.con.prepareStatement("select " + " contract_tariff_id, " + " sid, " + " value " + "from " + " custom_tariff_cost ctc " + "where " + " ctc.contract_tariff_id=? "); ps.setInt(1, ct.getContractId()); ResultSet rs = ps.executeQuery(); while(rs.next()){ result.add(new CustomTariffCost( rs.getInt(1), rs.getInt(2), rs.getBigDecimal(3), TimeUtils.convertCalendarToDate(ct.getDate1()), TimeUtils.convertCalendarToDate(ct.getDate2()))); } rs.close(); ps.close(); }catch (SQLException e){ processException(e); } return result; } public List<CustomTariffCost> getContractTariffCosts(int contract_tariff_id) throws BGException{ List<CustomTariffCost> result = new ArrayList<CustomTariffCost>(); try{ PreparedStatement ps = this.con.prepareStatement("select " + " ctc.contract_tariff_id, " + " ctc.sid, " + " ctc.value," + " ct.date1," + " ct.date2 " + "from " + " custom_tariff_cost ctc left join " + " contract_tariff ct on ctc.contract_tariff_id=ct.id " + "where " + " ctc.contract_tariff_id=? "); ps.setInt(1, contract_tariff_id); ResultSet rs = ps.executeQuery(); while(rs.next()){ result.add(new CustomTariffCost( rs.getInt(1), rs.getInt(2), rs.getBigDecimal(3), TimeUtils.convertSqlDateToDate(rs.getDate(4)), TimeUtils.convertSqlDateToDate(rs.getDate(5)))); } rs.close(); ps.close(); }catch (SQLException e){ processException(e); } return result; } protected void processException(SQLException e) throws BGException { throw new BGException(e.getMessage() + " [" + e.getSQLState() + ", " + e.getErrorCode() + "]", e); } }
Сам кэш переопределений цен:
package ru.dsi.bgbilling.kernel.discount.bean; import bitel.billing.common.TimeUtils; import org.apache.log4j.Logger; import ru.bitel.bgbilling.common.BGException; import ru.bitel.bgbilling.kernel.event.EventListener; import ru.bitel.bgbilling.kernel.event.EventListenerContext; import ru.bitel.bgbilling.kernel.event.EventProcessor; import ru.bitel.bgbilling.kernel.tariff.server.event.ContractTariffChangedEvent; import ru.dsi.bgbilling.kernel.discount.api.common.bean.CustomTariffCost; import ru.dsi.bgbilling.kernel.discount.bean.event.ContractCustomTariffCostsChangedEvent; import ru.dsi.bgbilling.kernel.discount.bean.event.CustomTariffCostChangedEvent; import java.lang.ref.SoftReference; import java.math.BigDecimal; import java.sql.Connection; import java.sql.PreparedStatement; import java.sql.ResultSet; import java.sql.SQLException; import java.util.ArrayList; import java.util.Date; import java.util.List; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; import java.util.concurrent.atomic.AtomicReference; /** * Кэш переопределённых цен на договоре */ public class CustomTariffCostCache { private static volatile CustomTariffCostCache instance = new CustomTariffCostCache(); private static final Logger logger = Logger.getLogger(CustomTariffCostCache.class); //cid -> tree_id -> sid -> List<ru.dsi.bgbilling.kernel.discount.api.common.bean.CustomTariffCost> private final ConcurrentMap<Integer, SoftReference<AtomicReference<ConcurrentMap<Integer, ConcurrentMap<Integer, List<CustomTariffCost>>>>>> tariffCostMap = new ConcurrentHashMap<Integer, SoftReference<AtomicReference<ConcurrentMap<Integer, ConcurrentMap<Integer, List<CustomTariffCost>>>>>>(); private CustomTariffCostCache() { new Thread("custom-tariff-cache-reload") { public void run() { try { //При изменении переопределённых цен на тарифе обновляем все переопределения для cid EventProcessor.getInstance().addListener(new EventListener<CustomTariffCostChangedEvent>() { @Override public void notify(CustomTariffCostChangedEvent e, EventListenerContext ctx) throws BGException { try { CustomTariffCostCache.this.reloadTariffCostMapForCid(ctx.getConnection(), e.getContractId()); } catch (SQLException ex) { throw new BGException(ex); } } } , CustomTariffCostChangedEvent.class); //При изменении переопределённых цен на договоре обновляем все переопределения для cid EventProcessor.getInstance().addListener(new EventListener<ContractCustomTariffCostsChangedEvent>() { @Override public void notify(ContractCustomTariffCostsChangedEvent e, EventListenerContext ctx) throws BGException { try { CustomTariffCostCache.this.reloadTariffCostMapForCid(ctx.getConnection(), e.getContractId()); } catch (SQLException ex) { throw new BGException(ex); } } } , ContractCustomTariffCostsChangedEvent.class); //При изменении тарифа договора обновляем все переопределения для cid EventProcessor.getInstance().addListener(new EventListener<ContractTariffChangedEvent>() { @Override public void notify(ContractTariffChangedEvent e, EventListenerContext ctx) throws BGException { try{ CustomTariffCostCache.this.reloadTariffCostMapForCid(ctx.getConnection(), e.getContractId()); } catch (SQLException ex) { throw new BGException(ex); } } } , ContractTariffChangedEvent.class); }catch (BGException e){ CustomTariffCostCache.logger.error(e.getMessage(), e); } } } .start(); } public static CustomTariffCostCache getInstance(){ return instance; } /** * Возвращаем переопределение цены услуги sid по договору cid на дату date для тарифа с tree_id. * Если таких переопределений несколько - возвращаем первое. * (Предполагаем, что переопределение только одно в каждый момент времени) * @param cid cid * @param sid sid * @param tree_id id тарифного дерева * @param dt дата, на которую должно быть активно переопределение * @return объект переопределения цены */ public CustomTariffCost get(Connection con, int cid, int tree_id, int sid, Date dt) throws SQLException { SoftReference<AtomicReference<ConcurrentMap<Integer, ConcurrentMap<Integer, List<CustomTariffCost>>>>> cidCostMapRefRef = this.tariffCostMap.get(cid); AtomicReference<ConcurrentMap<Integer, ConcurrentMap<Integer, List<CustomTariffCost>>>> cidCostMapRef = null; if(cidCostMapRefRef!=null){ cidCostMapRef = cidCostMapRefRef.get(); } if(cidCostMapRef==null){//нет - значит нужно получить cidCostMapRef = this.reloadTariffCostMapForCid(con, cid); } if(cidCostMapRef==null || cidCostMapRef.get()==null){ //В кэше ничего нет для этого cid, // но есть пустой cidCostMapRef => переопределений цены на договоре нет //Либо, если cidCostMapRef==null, то по какой-то причине reloadTariffCostMapForCid не вернул нормального значения (такого не должно быть по идее) return null; } ConcurrentMap<Integer, List<CustomTariffCost>> cidTreeIdCostMap = cidCostMapRef.get().get(tree_id); if(cidTreeIdCostMap!=null){ List<CustomTariffCost> costMapList = cidTreeIdCostMap.get(sid); if(costMapList!=null){ //ищем в списке подходящее по датам переопределение for(CustomTariffCost cost : costMapList){ if((cost.getDate1()==null || TimeUtils.dateBeforeOrEq(cost.getDate1(),dt))&& (cost.getDate2()==null || TimeUtils.dateBeforeOrEq(dt,cost.getDate2()))){ return cost; } } } } return null; } private AtomicReference<ConcurrentMap<Integer, ConcurrentMap<Integer, List<CustomTariffCost>>>> reloadTariffCostMapForCid(Connection con, int cid) throws SQLException { //Берём новую мэпу с костами для cid ConcurrentMap<Integer, ConcurrentMap<Integer, List<CustomTariffCost>>> cidCustomCosts = loadCidCustomCosts(con, cid); SoftReference<AtomicReference<ConcurrentMap<Integer, ConcurrentMap<Integer, List<CustomTariffCost>>>>> costMapRefRef = this.tariffCostMap.get(cid); if(costMapRefRef==null){ costMapRefRef = new SoftReference<AtomicReference<ConcurrentMap<Integer, ConcurrentMap<Integer, List<CustomTariffCost>>>>>(new AtomicReference<ConcurrentMap<Integer, ConcurrentMap<Integer, List<CustomTariffCost>>>>(cidCustomCosts)); this.tariffCostMap.put(cid,costMapRefRef); }else{ AtomicReference<ConcurrentMap<Integer, ConcurrentMap<Integer, List<CustomTariffCost>>>> costMapRef = costMapRefRef.get(); if(costMapRef!=null){ costMapRef.set(cidCustomCosts); }else{ costMapRefRef = new SoftReference<AtomicReference<ConcurrentMap<Integer, ConcurrentMap<Integer, List<CustomTariffCost>>>>>(new AtomicReference<ConcurrentMap<Integer, ConcurrentMap<Integer, List<CustomTariffCost>>>>(cidCustomCosts)); this.tariffCostMap.put(cid,costMapRefRef); } } return costMapRefRef.get(); } private ConcurrentMap<Integer, ConcurrentMap<Integer, List<CustomTariffCost>>> loadCidCustomCosts(Connection con, int cid) throws SQLException { ConcurrentMap<Integer, ConcurrentMap<Integer, List<CustomTariffCost>>> result = null; PreparedStatement ps = con.prepareStatement( "select " + "tp.tree_id, " + "ctc.sid, " + "ct.id, " + "ctc.value, " + "ct.date1, " + "ct.date2 " + "from " + "contract_tariff ct left join " + "custom_tariff_cost ctc on ct.id=ctc.contract_tariff_id left join " + "tariff_plan tp on ct.tpid=tp.id "+ "where " + "ct.cid=? AND " + "not ctc.contract_tariff_id is null AND " + "not tp.tree_id is null "); ps.setInt(1,cid); ResultSet rs = ps.executeQuery(); int tree_id; int sid; int contract_tariff_id; BigDecimal value; Date date1; Date date2; ConcurrentMap<Integer, List<CustomTariffCost>> integerListConcurrentMap; List<CustomTariffCost> customTariffCosts; while(rs.next()){ tree_id = rs.getInt(1); sid = rs.getInt(2); contract_tariff_id = rs.getInt(3); value = rs.getBigDecimal(4); date1 = TimeUtils.convertSqlDateToDate(rs.getDate(5)); date2 = TimeUtils.convertSqlDateToDate(rs.getDate(6)); if(null==result){ result = new ConcurrentHashMap<Integer, ConcurrentMap<Integer, List<CustomTariffCost>>>(); } integerListConcurrentMap = result.get(tree_id); if(null==integerListConcurrentMap){ integerListConcurrentMap = new ConcurrentHashMap<Integer, List<CustomTariffCost>>(); result.put(tree_id, integerListConcurrentMap); } customTariffCosts = integerListConcurrentMap.get(sid); if(null==customTariffCosts){ customTariffCosts = new ArrayList<CustomTariffCost>(); integerListConcurrentMap.put(sid, customTariffCosts); } customTariffCosts.add(new CustomTariffCost(contract_tariff_id,sid,value,date1, date2)); } rs.close(); ps.close(); return result; } }
Альтернативный узел "Стоимость" модуля NPay
Берём стандартный узел "Стоимость" и пишем свой аналогичный, но с добавленной логикой использования кэша переопределений.
Вернее, на самом деле стандартных узлов у Битела 3: дневной, месячный и годовой.
Все их нужно переопределить по-отдельности.
Дневной:
package ru.bitel.bgbilling.modules.npay.tariff.server; import ru.bitel.bgbilling.kernel.tariff.tree.server.TariffTreeNode; import ru.bitel.bgbilling.kernel.tariff.tree.server.TreeContext; import ru.bitel.common.ParameterMap; import ru.bitel.common.worker.ThreadContext; import ru.dsi.bgbilling.kernel.discount.api.common.bean.CustomTariffCost; import ru.dsi.bgbilling.kernel.discount.bean.CustomTariffCostCache; import java.math.BigDecimal; /** * Стандартный DayModeCostTariffTreeNode, но с возможностью переопределения цены * Если в узле указано custom=1, то ищем по cid, sid, date новую цену на договоре через CustomTariffCostCache */ public class DayModeCustomCostTariffTreeNode extends TariffTreeNode<NPayTariffRequest, NPayTariffContext, TreeContext, ThreadContext>{ /** * cost, указанный в тарифе */ private final BigDecimal cost; private final int costType; //Может ли быть переопределена цена персонально в договоре private final boolean isCustomCostAllowed; /** * cost на конкретном договоре, используемый для рассчетов */ private BigDecimal currentCost; public DayModeCustomCostTariffTreeNode(int id, ParameterMap parameterMap) { super(id, parameterMap); this.cost = parameterMap.getBigDecimal("cost", BigDecimal.ZERO); this.currentCost = this.cost; this.costType = parameterMap.getInt("type", 0); this.isCustomCostAllowed = parameterMap.getBoolean("custom", false); } @Override protected int executeImpl(Long treeNodeId, Long parentTreeNodeId, NPayTariffRequest req, NPayTariffContext ctx, TreeContext treeContext, ThreadContext workerContext) { this.currentCost = this.cost; //Определяем, нужно ли менять цену if(this.isCustomCostAllowed){ //Ищем на договоре переопределение цены для услуги /*List<TariffTreeNodeHolder> path = treeContext.getPath(new TariffTreeNodeHolder(treeNodeId, -1, null, null)); for (TariffTreeNodeHolder node : path){ logger.info(String.valueOf(treeNodeId)+" : "+String.valueOf(node.treeNodeId)); }*/ try{ CustomTariffCost customTariffCost = CustomTariffCostCache.getInstance().get(req.getConnection(), req.cid, treeContext.getTariffModuleTreeId(), req.serviceCost.serviceId,req.getTime().getTime()); if(customTariffCost!=null){ this.currentCost=customTariffCost.getValue(); logger.debug("custom cost for node_id="+treeNodeId+", cid="+req.cid+", sid="+req.serviceCost.serviceId+", value="+this.currentCost+" (was "+this.cost+")"); } }catch (Exception e){ logger.error(e.getMessage(), e); } } //req.addServiceCost(new NPayTariffRequest.NPayServiceCost(-req.serviceCost.serviceId,)); req.serviceCost.cost = (this.costType == 0 ? this.currentCost : this.currentCost.divide(BigDecimal.valueOf(req.getAccountingMonthDays()), ctx.mc)); return 1; } }
Месячный:
package ru.bitel.bgbilling.modules.npay.tariff.server; import ru.bitel.bgbilling.kernel.tariff.tree.server.TariffTreeNode; import ru.bitel.bgbilling.kernel.tariff.tree.server.TreeContext; import ru.bitel.common.ParameterMap; import ru.bitel.common.worker.ThreadContext; import ru.dsi.bgbilling.kernel.discount.api.common.bean.CustomTariffCost; import ru.dsi.bgbilling.kernel.discount.bean.CustomTariffCostCache; import java.math.BigDecimal; /** * Стандартный MonthModeCostTariffTreeNode, но с возможностью переопределения цены * Если в узле указано custom=1, то ищем по cid, sid, date новую цену на договоре через CustomTariffCostCache */ public class MonthModeCustomCostTariffTreeNode extends TariffTreeNode<NPayTariffRequest, NPayTariffContext, TreeContext, ThreadContext> { /** * cost, указанный в тарифе */ private final BigDecimal cost; private final int type; //Может ли быть переопределена цена персонально в договоре private final boolean isCustomCostAllowed; /** * cost на конкретном договоре, используемый для рассчетов */ private BigDecimal currentCost; public MonthModeCustomCostTariffTreeNode(int id, ParameterMap parameterMap) { super(id, parameterMap); this.cost = parameterMap.getBigDecimal("cost", BigDecimal.ZERO); this.currentCost = this.cost; this.type = parameterMap.getInt("type", 0); this.isCustomCostAllowed = parameterMap.getBoolean("custom", false); } @Override protected int executeImpl(Long treeNodeId, Long parentTreeNodeId, NPayTariffRequest req, NPayTariffContext ctx, TreeContext treeContext, ThreadContext workerContext) { this.currentCost = this.cost; //Определяем, нужно ли менять цену if(this.isCustomCostAllowed){ //Ищем на договоре переопределение цены для услуги try{ CustomTariffCost customTariffCost = CustomTariffCostCache.getInstance().get(req.getConnection(), req.cid, treeContext.getTariffModuleTreeId(), req.serviceCost.serviceId,req.getTime().getTime()); if(customTariffCost!=null){ this.currentCost=customTariffCost.getValue(); logger.debug("custom cost for node_id="+treeNodeId+", cid="+req.cid+", sid="+req.serviceCost.serviceId+", value="+this.currentCost+" (was "+this.cost+")"); } }catch (Exception e){ logger.error(e.getMessage(), e); } } int monthDays = req.getAccountingMonthDays(); int periodDays = req.serviceCost.accountingPeriodDays; BigDecimal monthAmount = req.serviceCost.monthAmount; BigDecimal periodAmount = req.serviceCost.periodAmount; BigDecimal cost; switch (this.type) { case 0: if (periodDays > 0) { cost = this.currentCost; } else { cost = BigDecimal.ZERO; } break; case 1: cost = this.currentCost.multiply(BigDecimal.valueOf(periodDays), ctx.mc).divide( BigDecimal.valueOf(monthDays), ctx.mc); break; case 2: if ((monthAmount != null) && (periodAmount != null)) { cost = this.currentCost.multiply(periodAmount, ctx.mc).divide(monthAmount, ctx.mc); } else { cost = BigDecimal.ZERO; } break; case 3: cost = this.currentCost.multiply(BigDecimal.valueOf(periodDays), ctx.mc).divide(BigDecimal.valueOf(monthDays), ctx.mc); if ((monthAmount != null) && (periodAmount != null)) { BigDecimal costPropAmount = this.currentCost.multiply(periodAmount, ctx.mc).divide(monthAmount, ctx.mc); if (costPropAmount.compareTo(cost) > 0) { cost = costPropAmount; } } break; default: cost = BigDecimal.ZERO; } req.serviceCost.cost = cost; return 1; } }
Годовой:
package ru.bitel.bgbilling.modules.npay.tariff.server; import ru.bitel.bgbilling.kernel.tariff.tree.server.TariffTreeNode; import ru.bitel.bgbilling.kernel.tariff.tree.server.TreeContext; import ru.bitel.common.ParameterMap; import ru.bitel.common.worker.ThreadContext; import ru.dsi.bgbilling.kernel.discount.api.common.bean.CustomTariffCost; import ru.dsi.bgbilling.kernel.discount.bean.CustomTariffCostCache; import java.math.BigDecimal; /** * Стандартный YearModeCostTariffTreeNode, но с возможностью переопределения цены * Если в узле указано custom=1, то ищем по cid, sid, date новую цену на договоре через CustomTariffCostCache */ public class YearModeCustomCostTariffTreeNode extends TariffTreeNode<NPayTariffRequest, NPayTariffContext, TreeContext, ThreadContext> { /** * cost, указанный в тарифе */ private final BigDecimal cost; //Может ли быть переопределена цена персонально в договоре private final boolean isCustomCostAllowed; /** * cost на конкретном договоре, используемый для рассчетов */ private BigDecimal currentCost; public YearModeCustomCostTariffTreeNode(int id, ParameterMap parameterMap) { super(id, parameterMap); this.cost = parameterMap.getBigDecimal("cost", BigDecimal.ZERO); this.currentCost = this.cost; this.isCustomCostAllowed = parameterMap.getBoolean("custom", false); } protected int executeImpl(Long treeNodeId, Long parentTreeNodeId, NPayTariffRequest req, NPayTariffContext ctx, TreeContext treeContext, ThreadContext workerContext) { this.currentCost = this.cost; //Определяем, нужно ли менять цену if(this.isCustomCostAllowed){ //Ищем на договоре переопределение цены для услуги try{ CustomTariffCost customTariffCost = CustomTariffCostCache.getInstance().get(req.getConnection(), req.cid, treeContext.getTariffModuleTreeId(), req.serviceCost.serviceId,req.getTime().getTime()); if(customTariffCost!=null){ this.currentCost=customTariffCost.getValue(); logger.debug("custom cost for node_id="+treeNodeId+", cid="+req.cid+", sid="+req.serviceCost.serviceId+", value="+this.currentCost+" (was "+this.cost+")"); } }catch (Exception e){ logger.error(e.getMessage(), e); } } req.serviceCost.cost = this.currentCost; return 1; } }
Теперь заменим стандартные узлы тарифов абонплат нашими самописными. Для этого нужно распаковать из npay.jar файл bitel/billing/common/tariff/npay.xml и заменить в нём определения day_cost, month_cost и year_cost :
<node type="day_cost" class1="bitel.billing.module.services.npay.DayModeCostTariffTreeNode" title="СÑоимоÑÑÑ" class2="ru.bitel.bgbilling.modules.npay.tariff.server.DayModeCostTariffTreeNode"/> ... <node type="month_cost" class1="bitel.billing.module.services.npay.MonthModeCostTariffTreeNode" title="СÑоимоÑÑÑ" class2="ru.bitel.bgbilling.modules.npay.tariff.server.MonthModeCostTariffTreeNode"/> ... <node type="year_cost" class1="bitel.billing.module.services.npay.YearModeCostTariffTreeNode" title="СÑоимоÑÑÑ" class2="ru.bitel.bgbilling.modules.npay.tariff.server.YearModeCostTariffTreeNode"/>
На наши:
<node type="day_cost" class1="bitel.billing.module.services.npay.DayModeCustomCostTariffTreeNode" title="Стоимость" class2="ru.bitel.bgbilling.modules.npay.tariff.server.DayModeCustomCostTariffTreeNode"/> <node type="month_cost" class1="bitel.billing.module.services.npay.MonthModeCustomCostTariffTreeNode" title="Стоимость" class2="ru.bitel.bgbilling.modules.npay.tariff.server.MonthModeCustomCostTariffTreeNode"/> <node type="year_cost" class1="bitel.billing.module.services.npay.YearModeCustomCostTariffTreeNode" title="Стоимость" class2="ru.bitel.bgbilling.modules.npay.tariff.server.YearModeCustomCostTariffTreeNode"/>
Как это сделать?
С одной стороны, можно внести изменения в npay.xml и запаковать его обратно в npay.xml. Но тогда это придётся делать при каждом обновлении. С другой стороны, я просто положил это файл в /usr/local/BGBillingServer/bitel/billing/common/tariff на сервере и перезапустил сервисы - всё заработало: файл загружается приоритетнее стандартного из npay.jar
Но нужно понимать, что при любом обновлении нужно будет следить за изменениями стандартного Npay.xml и соответствующим образом менять собственный файл.
Интерфейс
Редактирование цен
Отлично, теперь цены у нас переопределяются по табличке custom_tariff_cost. Теперь нужно написать удобный интерфейс работы с этой табличкой.
Веб-сервис переопределения цен
Вспомогательные классы:
package ru.dsi.bgbilling.kernel.discount.api.common.bean; import ru.bitel.common.model.Id; import java.io.Serializable; import java.util.Date; /** * @author cromeshnic@gmail.com * Тарифный план договора для использования в веб-сервисе. * Пока бител не перевёл стандартный экшн получения списка тарифов догвора на вебэкшен, будем делать так у себя * */ public class ContractTariff extends Id implements Serializable { private int contractId; private int tariffPlanId; private Date date1; private Date date2; private String comment = ""; private String tariffName; private int pos; private int entityMid; private int entityId; private int replacedFromContractTariffId; public ContractTariff() { this.id = -1; this.contractId = -1; this.tariffPlanId = -1; this.date1 = null; this.date2 = null; this.tariffName = null; this.pos = 0; this.entityId = 0; this.entityMid = 0; this.replacedFromContractTariffId = -1; } public int getContractId() { return contractId; } public void setContractId(int contractId) { this.contractId = contractId; } public int getTariffPlanId() { return tariffPlanId; } public String getTariffName() { return tariffName; } public void setTariffName(String tariffName) { this.tariffName = tariffName; } public void setTariffPlanId(int tariffPlanId) { this.tariffPlanId = tariffPlanId; } public Date getDate1() { return date1; } public void setDate1(Date date1) { this.date1 = date1; } public Date getDate2() { return date2; } public void setDate2(Date date2) { this.date2 = date2; } public String getComment() { return comment; } public void setComment(String comment) { this.comment = comment; } public int getPos() { return pos; } public void setPos(int pos) { this.pos = pos; } public int getEntityMid() { return entityMid; } public void setEntityMid(int entityMid) { this.entityMid = entityMid; } public int getEntityId() { return entityId; } public void setEntityId(int entityId) { this.entityId = entityId; } public int getReplacedFromContractTariffId() { return replacedFromContractTariffId; } public void setReplacedFromContractTariffId(int replacedFromContractTariffId) { this.replacedFromContractTariffId = replacedFromContractTariffId; } }
package ru.bitel.bgbilling.kernel.discount.api.common.service; import ru.bitel.bgbilling.common.BGException; import ru.dsi.bgbilling.kernel.discount.api.common.bean.ContractTariff; import javax.jws.WebMethod; import javax.jws.WebParam; import javax.jws.WebService; import javax.xml.bind.annotation.XmlSeeAlso; import java.util.List; /** * @author cromeshnic@gmail.com * Пока бител не перевёл стандартный экшн получения списка тарифов догвора на вебэкшен, будем пользоваться собственным сервисом * */ @WebService @XmlSeeAlso({ContractTariff.class}) public abstract interface ContractTariffService { @WebMethod public abstract List<ContractTariff> contractTariffList(@WebParam(name="contractId") int contracId) throws BGException; }
package ru.bitel.bgbilling.kernel.discount.api.server.service; import ru.bitel.bgbilling.common.BGException; import ru.bitel.bgbilling.kernel.container.service.server.AbstractService; import ru.bitel.bgbilling.kernel.discount.api.common.service.ContractTariffService; import ru.dsi.bgbilling.kernel.discount.api.common.bean.ContractTariff; import ru.dsi.bgbilling.kernel.discount.bean.ContractTariffDao; import javax.jws.WebParam; import javax.jws.WebService; import java.util.List; /** * @see ru.bitel.bgbilling.kernel.discount.api.common.service.ContractTariffService */ @WebService(endpointInterface="ru.bitel.bgbilling.kernel.discount.api.common.service.ContractTariffService") public class ContractTariffServiceImpl extends AbstractService implements ContractTariffService { @Override public List<ContractTariff> contractTariffList(@WebParam(name = "contractId") int contracId) throws BGException { return (new ContractTariffDao(getConnection()).list(contracId)); } }
Веб-сервис работы с переопределёнными ценами:
package ru.bitel.bgbilling.kernel.discount.api.common.service; import ru.bitel.bgbilling.common.BGException; import ru.dsi.bgbilling.kernel.discount.api.common.bean.CustomTariffCost; import javax.jws.WebMethod; import javax.jws.WebParam; import javax.jws.WebService; import javax.xml.bind.annotation.XmlSeeAlso; import java.util.List; import java.util.Set; /** * @author cromeshnic@gmail.com * Интерфейс веб-сервиса для работы с персональными ценами */ @WebService @XmlSeeAlso({CustomTariffCost.class}) public abstract interface CustomTariffCostService { @WebMethod public abstract List<CustomTariffCost> customTariffCostList(@WebParam(name="contractTariffId") int contract_tariff_id) throws BGException; @WebMethod public abstract void updateCustomTariffCostList(@WebParam(name="contractTariffId") int contract_tariff_id, @WebParam(name="costs") List<CustomTariffCost> costs) throws BGException; /** * Возвращает список id услуг, для которых на этом тарифном плане разрешено переопределять цену */ @WebMethod public abstract Set<Integer> availableServiceList(@WebParam(name="tariffPlanId") int tpid) throws BGException; }
package ru.bitel.bgbilling.kernel.discount.api.server.service; import bitel.billing.server.contract.bean.ContractTariff; import bitel.billing.server.contract.bean.ContractTariffManager; import ru.bitel.bgbilling.common.BGException; import ru.bitel.bgbilling.kernel.container.service.server.AbstractService; import ru.bitel.bgbilling.kernel.discount.api.common.service.CustomTariffCostService; import ru.bitel.bgbilling.kernel.tariff.common.bean.TariffLabelItem; import ru.bitel.bgbilling.kernel.tariff.server.bean.TariffLabelManager; import ru.bitel.bgbilling.server.util.ClosedDateChecker; import ru.dsi.bgbilling.kernel.discount.api.common.bean.CustomTariffCost; import ru.dsi.bgbilling.kernel.discount.bean.CustomTariffCostDao; import javax.jws.WebParam; import javax.jws.WebService; import java.sql.SQLException; import java.util.Calendar; import java.util.List; import java.util.Set; import java.util.TreeSet; import java.util.regex.Matcher; import java.util.regex.Pattern; /** * Реализация веб-сервиса для работы с персональными ценам */ @WebService(endpointInterface="ru.bitel.bgbilling.kernel.discount.api.common.service.CustomTariffCostService") public class CustomTariffCostServiceImpl extends AbstractService implements CustomTariffCostService { @Override public List<CustomTariffCost> customTariffCostList(@WebParam(name = "contractTariffId") int contract_tariff_id) throws BGException { return (new CustomTariffCostDao(getConnection())).getContractTariffCosts(contract_tariff_id); } @Override public void updateCustomTariffCostList(@WebParam(name = "contractTariffId")int contract_tariff_id, @WebParam(name="costs") List<CustomTariffCost> costs) throws BGException{ Calendar closedDate; ContractTariffManager ctm = new ContractTariffManager(getConnection()); ContractTariff ct = ctm.getContractTariffById(contract_tariff_id); if ((closedDate = ClosedDateChecker.getClosePeriodDateIfChecking("updateCustomTariffCostList", 0, this.userId)) != null) { ClosedDateChecker.checkDatesForUpdate(closedDate, null, null, ct.getDate1(), ct.getDate2()); } CustomTariffCostDao ctcDao = new CustomTariffCostDao(getConnection()); try { ctcDao.updateContractCustomTariffCosts(ct, costs, this.userId); } catch (SQLException e) { throw new BGException(e); } } /** * Возвращает список услуг, для которых на этом тарифном плане разрешено переопределять цену * На данный момент реализовано через метки - ищем все метки тарифного плана вида "id - Название", * где id - это id услуги. Например "5 - Интернет". */ @Override public Set<Integer> availableServiceList(@WebParam(name = "tariffPlanId") int tpid) throws BGException { Set<Integer> result = new TreeSet<Integer>(); TariffLabelManager tlm = new TariffLabelManager(getConnection()); //Получаем список id меток тарифа Set<Integer> tariffLabelIds = tlm.getTariffLabelIds(tpid); if(tariffLabelIds.size()>0){ //Берём список всех меток с названиями List<TariffLabelItem> tariffLabelItemList = tlm.getTariffLabelItemList(); //Ищем среди меток тарифа метки с названиями вида "id - Название" /*for(Integer label_id : tariffLabelIds){ TariffLabelItem label = tariffLabelItemList.get() }*/ Pattern pattern = Pattern.compile("^(\\d+) - .+$"); Matcher matcher; //Перебираем все метки из справочника for(TariffLabelItem label : tariffLabelItemList){ //Если метка есть на нашем тарифе ... if(tariffLabelIds.contains(label.getId())){ //...то смотрим её название matcher = pattern.matcher(label.getTitle()); if(matcher.find()){//определился sid услуги - отлично, добавляем в результат try{ result.add(Integer.valueOf(matcher.group(1))); }catch (Exception e){} } } } } return result; } }
Клиетский интерфейс
Для начала немного костылей.
Поскольку услуг в биллинге заведено много, а в конкретном тарифе обычно можно переопределять всего несколько, то для облегчения интерфейса веб-сервис выдаёт только те услуги, которые разрешено переопределять для данного тарифа. Но получать список таких услуг через разбор тарифного дерева представляется излишне сложной задачей, поэтому было решено реализовать список разрешённых к переопределению услуг через тарифные метки:
Т.е. чтобы услуга появилась в интерфейсе, нужно завести метку вида "xxx - Наименование", где xxx - sid услуги, дополненный нулями до 3 символов, и проставить эту метку для всех необходимых тарифов.
Переходим собственно к интерфейсу.
Менять клиентский интерфейс биллинга стандартным образом нельзя - только через переопределение jar-файлов клиента.
Я долго думал, куда бы запихать управление персональными ценами малой кровью. По-идее, будь я разработчиком Битела, я бы сделал это в ядре (т.к. в общем случае можно реализовать переопределение не только абонплат). Соответственно, редактирование персональных цен должно быть где-то в разделе "Тарифные планы" договора - например, отдельной вкладкой после "Персональных тарифов" и "Тарифных опций". Но малой кровью модифицировать эту клиентскую панель не получилось, поэтому было решено сделать отдельную вкладку "Переопределение цен" на странице модуля Npay - см http://wiki.bitel.ru/index.php/%D0%A4%D0%B0%D0%B9%D0%BB:Custom_cost.jpg
Привожу клиентский код для 5.2
Что с ним делать - решайте сами на свой страх и риск :)
Добавляем нашу панель - прописываем в клиентский файл bitel/billing/module/services/npay/setup_user.properties:
module.tab.3.class=bitel.billing.module.services.npay.CustomCostModulePanel module.tab.3.title=\u041F\u0435\u0440\u0435\u043E\u043F\u0440\u0435\u0434\u0435\u043B\u0435\u043D\u0438\u0435 \u0446\u0435\u043D
Панель:
package bitel.billing.module.services.npay; import bitel.billing.common.TimeUtils; import bitel.billing.module.common.BGControlPanelContractSelect; import bitel.billing.module.common.BGTitleBorder; import bitel.billing.module.common.DialogToolBar; import bitel.billing.module.common.FloatTextField; import ru.bitel.bgbilling.client.common.BGEditor; import ru.bitel.bgbilling.client.common.BGUPanel; import ru.bitel.bgbilling.client.common.BGUTable; import ru.bitel.bgbilling.client.common.ClientContext; import ru.bitel.bgbilling.client.util.ClientUtils; import ru.bitel.bgbilling.common.BGException; import ru.bitel.bgbilling.kernel.discount.api.common.service.ContractTariffService; import ru.bitel.bgbilling.kernel.discount.api.common.service.CustomTariffCostService; import ru.bitel.bgbilling.kernel.module.common.bean.Service; import ru.bitel.bgbilling.kernel.module.common.service.ServiceService; import ru.bitel.common.Utils; import ru.bitel.common.client.AbstractBGUPanel; import ru.bitel.common.client.BGSwingUtilites; import ru.bitel.common.client.BGUComboBox; import ru.bitel.common.client.table.BGTableModel; import ru.bitel.common.model.KeyValue; import ru.dsi.bgbilling.kernel.discount.api.common.bean.ContractTariff; import ru.dsi.bgbilling.kernel.discount.api.common.bean.CustomTariffCost; import javax.swing.*; import java.awt.*; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.math.BigDecimal; import java.util.*; import java.util.List; /** * @author cromeshnic@gmail.com * Панелька на клиенте для редактирования переопределений стоимости тарифных планов договоров. */ public class CustomCostModulePanel extends BGUPanel { private BGControlPanelContractSelect contractSelect; private ContractTariffService wsContractTariff = getContext().getPort(ContractTariffService.class, 0); private BGTableModel<ContractTariff> model = new BGTableModel<ContractTariff>("contractTariff") { protected void initColumns() { addColumn("ID", Integer.class, 0, 0, 0, "id", true, false); addColumn("TPID", Integer.class, 0, 0, 0, "tpid", true, false); addColumn("Название", "tariffName", true); addColumn("Период", "period", true); addColumn("Комментарий", "comment", false); addColumn("Поз.", "pos", true); } @Override public Object getValue(ContractTariff val, int column) { Object result = ""; switch (column) { case 3: result = TimeUtils.formatPeriod(val.getDate1(), val.getDate2()); break; default: result = super.getValue(val, column); } return result; } }; public CustomCostModulePanel() { super(new BorderLayout()); //setLayout(new GridBagLayout()); } protected void initActions() { new AbstractBGUPanel.DefaultAction("refresh", "Обновить") { public void actionPerformedImpl(ActionEvent e) throws Exception { CustomCostModulePanel.this.setData(); } }; } public void setData() throws BGException { String cids = CustomCostModulePanel.this.contractSelect.getContracts(); int cid = -1; try{ cid = Utils.toIntegerList(cids).get(0); }catch(Exception ex){ this.processException(ex); } CustomCostModulePanel.this.model.setData(CustomCostModulePanel.this.wsContractTariff.contractTariffList(cid)); } @Override protected void jbInit() throws Exception { setLayout(new GridBagLayout()); BGUTable table = new BGUTable(this.model); JButton okButton = new JButton("OK"); okButton.setMargin(new Insets(2, 2, 2, 2)); okButton.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent e) { try { CustomCostModulePanel.this.setData(); } catch (BGException e1) { processException(e1); } } }); this.contractSelect = new BGControlPanelContractSelect(true, true); this.contractSelect.add(okButton, new GridBagConstraints(2, 0, 1, 1, 0.0D, 0.0D, 10, 0, new Insets(0, 0, 5, 5), 0, 0)); add(BGSwingUtilites.wrapBorder(this.contractSelect, "Выберите договор"), new GridBagConstraints(0, 0, 1, 1, 1.0D, 0.0D, GridBagConstraints.NORTH, GridBagConstraints.HORIZONTAL, new Insets(0, 0, 0, 0), 0, 0)); BGEditor editor = new BGEditor(); editor.setVisible(false); editor.addForm(new CustomCostTariffPanel(getContext())); add(BGSwingUtilites.wrapBorder(new JScrollPane(table), "Тарифные планы договора"), new GridBagConstraints(0, 1, 1, 1, 1.0D, 0.6D, 10, GridBagConstraints.BOTH, new Insets(0, 0, 0, 0), 0, 0)); add(editor, new GridBagConstraints(0, 2, 1, 1, 1.0D, 1.0D, GridBagConstraints.SOUTH, GridBagConstraints.BOTH, new Insets(0, 0, 0, 0), 0, 0)); BGSwingUtilites.handleEdit(table, editor); } /** * Панель редактирования собственно списка переопределений услуг конкретного тарифа договора */ public class CustomCostTariffPanel extends BGUPanel { private BGUTable table; CustomTariffCostService wsCustomCost = this.getContext().getPort(CustomTariffCostService.class, 0); private Map<Integer, Service> servicesMap;//Справочник услуг всех модулей : sid -> Service private ContractTariff current; private BGTableModel<CustomCostRow> model = new BGTableModel<CustomCostRow>("customCosts") { protected void initColumns() { addColumn("ID", 0, 0, 0, null, true); addColumn("Услуга", -1, 120, -1, null, false); addColumn("Цена", BigDecimal.class, -1, 180, -1, null, false); } public Object getValue(CustomCostRow val, int column) { switch (column) { case 0: return val.sid; case 1: return val.title; case 2: return val.value; default: return super.getValue(val, column); } } }; protected void initActions(){ new AbstractBGUPanel.DefaultAction("edit", "Редактировать") { public void actionPerformedImpl(ActionEvent e) throws Exception { if ((CustomCostTariffPanel.this.current = CustomCostModulePanel.this.model.getSelectedRow()) != null) { CustomCostTariffPanel.this.performActionOpen(); } } }; new AbstractBGUPanel.DefaultAction("ok", "Сохранить") { public void actionPerformedImpl(ActionEvent e) throws Exception { if(CustomCostTariffPanel.this.current!=null){ //CustomCostTariffPanel.this.model.getRows(); List<CustomTariffCost> costs = new ArrayList<CustomTariffCost>(); if(CustomCostTariffPanel.this.model.getRows()!=null){ CustomTariffCost cost; for(CustomCostRow row : CustomCostTariffPanel.this.model.getRows()){ cost = new CustomTariffCost(CustomCostTariffPanel.this.current.getId(),row.sid,row.value); costs.add(cost); } } CustomCostTariffPanel.this.wsCustomCost.updateCustomTariffCostList( CustomCostTariffPanel.this.current.getId(), costs ); } CustomCostTariffPanel.this.performActionClose(); } }; } @Override public void performActionOpen() { //расставляем данные в форме редактирования, затем отображаем её setData(this.current.getId(), this.current.getTariffPlanId()); super.performActionOpen(); } @Override protected void jbInit() throws Exception { //To change body of implemented methods use File | Settings | File Templates. setLayout(new GridBagLayout()); this.table = new BGUTable(this.model); DialogToolBar toolBar = new DialogToolBar(); ParamForm paramForm = new ParamForm(getContext()); BGEditor editor = new BGEditor(); editor.addForm(paramForm); editor.setVisible(false); BGSwingUtilites.buildToolBar(toolBar, paramForm); toolBar.compact(); BGSwingUtilites.handleEdit(this.table, editor); add(toolBar, new GridBagConstraints(0, 0, 1, 1, 0.0D, 0.0D, 17, 0, new Insets(0, 0, 0, 0), 0, 0)); add(new JScrollPane(this.table), new GridBagConstraints(0, 1, 1, 1, 1.0D, 1.0D, 10, 1, new Insets(0, 0, 0, 0), 0, 0)); add(editor, new GridBagConstraints(0, 2, 1, 1, 1.0D, 0.0D, 10, 2, new Insets(5, 5, 5, 5), 0, 0)); } //Сравниваем строчки цен для сортировки в таблице private final Comparator<CustomCostRow> COMPARATOR = new Comparator<CustomCostRow>() { public int compare(CustomCostRow o1, CustomCostRow o2) { int result = Integer.valueOf(o1.sid).compareTo(o2.sid); if (result != 0) { return result; } return o1.title.compareTo(o2.title); } }; @SuppressWarnings("unchecked") private BGUComboBox<KeyValue<Integer, String>> keyCombo = new BGUComboBox( KeyValue.class); public CustomCostTariffPanel(ClientContext context) { super(context); //Кэшируем список всех услуг биллинга. Если возникнет ошибка при его получении - ругаемся try { java.util.List<Service> serviceList = CustomCostTariffPanel.this.getContext().getPort(ServiceService.class, 0).serviceList(0); this.servicesMap = new HashMap<Integer, Service>(serviceList.size()); for(Service s : serviceList){ this.servicesMap.put(s.getId(),s); } } catch (BGException e) { this.processException(e); } } public void setData(int id, int tpid){ java.util.List<CustomTariffCost> costList; try { costList = wsCustomCost.customTariffCostList(id); } catch (BGException e) { this.processException(e); return; } java.util.List<CustomCostRow> data = new ArrayList<CustomCostRow>(costList.size()); String title; Service s; for(CustomTariffCost cost : costList){ s = this.servicesMap.get(cost.getSid()); if(s!=null){ title = s.getTitle(); }else{ title = String.valueOf(cost.getSid()); } data.add(new CustomCostRow(cost.getSid(), title, cost.getValue())); } Collections.sort(data, COMPARATOR); this.model.setData(data); //Получаем для комбобокса список услуг, для которых разрешено переопределение на этом тарифе java.util.List<KeyValue<Integer, String>> typeList = new ArrayList<KeyValue<Integer, String>>(CustomCostTariffPanel.this.servicesMap.size()); try{ for(Integer sid : this.wsCustomCost.availableServiceList(tpid)){ s = this.servicesMap.get(sid); if(null!=s){ title = s.getTitle(); }else{ title = "<"+sid+">"; } typeList.add(new KeyValue<Integer, String>(sid, title)); } } catch (BGException e) { this.processException(e); return; } CustomCostTariffPanel.this.keyCombo.setData(typeList); } class CustomCostRow { public int sid; public String title; public BigDecimal value; public CustomCostRow(int sid, String title, BigDecimal value) { this.sid = sid; this.title = title; this.value = value; } /*public CustomCostRow(int sid, BigDecimal value) { this.sid = sid; this.value = value; }*/ } class ParamForm extends BGUPanel { private FloatTextField value = new FloatTextField(); private CustomCostRow current; private boolean edit; public ParamForm(ClientContext context) { super(context); } protected void jbInit() { setBorder(BorderFactory.createCompoundBorder(new BGTitleBorder(" Редактор "), BorderFactory.createEmptyBorder(0, 3, 3, 3))); add(CustomCostTariffPanel.this.keyCombo, new GridBagConstraints(0, 0, 1, 1, 0.0D, 0.0D, 17, 0, new Insets(0, 0, 0, 5), 0, 0)); add(this.value, new GridBagConstraints(1, 0, 1, 1, 1.0D, 0.0D, 10, 2, new Insets(0, 0, 0, 0), 0, 0)); } protected void initActions() { new AbstractBGUPanel.DefaultAction("new", "Добавить", ClientUtils.getIcon("item_edit")) { public void actionPerformedImpl(ActionEvent e) throws Exception { CustomCostTariffPanel.ParamForm.this.edit = false; CustomCostTariffPanel.ParamForm.this.current = new CustomCostRow(0,"",BigDecimal.ZERO); CustomCostTariffPanel.ParamForm.this.value.setText(""); CustomCostTariffPanel.this.keyCombo.setEnabled(true); CustomCostTariffPanel.ParamForm.this.performActionOpen(); } }; new AbstractBGUPanel.DefaultAction("edit", "Редактировать", ClientUtils.getIcon("item_edit")) { public void actionPerformedImpl(ActionEvent e) throws Exception { CustomCostTariffPanel.ParamForm.this.edit = true; CustomCostTariffPanel.ParamForm.this.current = CustomCostTariffPanel.this.model.getSelectedRow(); if (CustomCostTariffPanel.ParamForm.this.current != null) { CustomCostTariffPanel.this.keyCombo.setSelectedItem(ParamForm.this.current.sid); CustomCostTariffPanel.this.keyCombo.setEnabled(false); ParamForm.this.value.setText(String.valueOf(ParamForm.this.current.value)); CustomCostTariffPanel.ParamForm.this.performActionOpen(); } } }; new AbstractBGUPanel.DefaultAction("delete", "Удалить", ClientUtils.getIcon("item_delete")) { public void actionPerformedImpl(ActionEvent e) throws Exception { CustomCostTariffPanel.ParamForm.this.current = CustomCostTariffPanel.this.model.getSelectedRow(); if (CustomCostTariffPanel.ParamForm.this.current != null) { CustomCostTariffPanel.this.model.deleteSelectedRow(); } } }; new AbstractBGUPanel.DefaultAction("ok", "OK") { public void actionPerformedImpl(ActionEvent e) throws Exception { KeyValue<Integer, String> key = CustomCostTariffPanel.this.keyCombo.getSelectedItem(); if (key == null) { return; } CustomCostTariffPanel.ParamForm.this.current.sid = key.getKey(); CustomCostTariffPanel.ParamForm.this.current.title = key.getValue(); CustomCostTariffPanel.ParamForm.this.current.value = Utils.parseBigDecimal(CustomCostTariffPanel.ParamForm.this.value.getText(), BigDecimal.ZERO); if (!CustomCostTariffPanel.ParamForm.this.edit) { CustomCostTariffPanel.this.model.addRow(CustomCostTariffPanel.ParamForm.this.current); } CustomCostTariffPanel.ParamForm.this.performActionClose(); } }; } } } }
Клиентские узлы тарифа:
package bitel.billing.module.services.npay; import bitel.billing.module.common.BGComboBox; import bitel.billing.module.common.ComboBoxItem; import bitel.billing.module.common.FloatTextField; import bitel.billing.module.tariff.DefaultTariffTreeNode; import java.awt.Component; import java.util.HashMap; import java.util.Map; import javax.swing.*; import ru.bitel.bgbilling.client.util.ClientUtils; import ru.bitel.common.Utils; /** * Стандартный узел DayModeCostTariffTreeNode на 22.03.2013, с добавлением чекбокса "Разрешено переопределение в договоре" * (custom=0|1 [default=0]) */ public class DayModeCustomCostTariffTreeNode extends DefaultTariffTreeNode { private static Icon icon = ClientUtils.getIcon("coin"); private JLabel view; private String cost; private String type; private boolean custom; private FloatTextField costTf; private BGComboBox typeCombo; private JCheckBox customChb; private void initEdit() { if (this.costTf == null) { this.costTf = new FloatTextField(); this.typeCombo = new BGComboBox(); this.typeCombo.addItem(new ComboBoxItem("0", "за день")); this.typeCombo.addItem(new ComboBoxItem("1", "за месяц")); this.customChb = new JCheckBox("Разрешено переопределение в договоре"); } } protected JPanel getEditorPanel() { initEdit(); JPanel edit = new JPanel(); edit.add(new JLabel("Цена ")); edit.add(this.costTf); edit.add(new JLabel(" начислять ")); edit.add(this.typeCombo); edit.add(this.customChb); return edit; } public Component getView() { if (this.view == null) { this.view = new JLabel(icon, SwingConstants.LEFT); } extractData(); StringBuilder title = new StringBuilder(); title.append(this.cost); title.append(" "); if (this.type.equals("0")) { title.append("за день"); } else if (this.type.equals("1")) { title.append("за месяц"); } if (this.custom) { title.append(" (возможно переопределение)"); } this.view.setText(title.toString()); return this.view; } private void extractData() { Map data = getDataInHash(); this.cost = ((String)data.get("cost")); this.type = Utils.maskNull((String)data.get("type")); this.custom = Utils.parseBoolean((String)data.get("custom"), false); } protected void loadData() { extractData(); this.costTf.setText(this.cost); ClientUtils.setComboBoxSelection(this.typeCombo, this.type); this.customChb.setSelected(this.custom); } protected void serializeData() { Map<String, String> data = new HashMap<String, String>(); data.put("cost", this.costTf.getText()); data.put("type", ClientUtils.getIdFromComboBox(this.typeCombo)); data.put("custom", Utils.booleanToStringInt(this.customChb.isSelected())); setDataInHash(data); } } package bitel.billing.module.services.npay; import bitel.billing.module.common.BGComboBox; import bitel.billing.module.common.ComboBoxItem; import bitel.billing.module.common.FloatTextField; import bitel.billing.module.tariff.DefaultTariffTreeNode; import java.awt.Component; import java.util.HashMap; import java.util.Map; import javax.swing.*; import ru.bitel.bgbilling.client.util.ClientUtils; import ru.bitel.common.Utils; /** * Стандартный узел MonthModeCostTariffTreeNode на 22.03.2013, с добавлением чекбокса "Разрешено переопределение в договоре" * (custom=0|1 [default=0]) */ public class MonthModeCustomCostTariffTreeNode extends DefaultTariffTreeNode { private static Icon icon = ClientUtils.getIcon("coin"); private JLabel view; private String cost; private String type; private boolean custom; private FloatTextField costTf; private BGComboBox typeCombo; private JCheckBox customChb; private void initEdit() { if (this.costTf == null) { this.costTf = new FloatTextField(); this.typeCombo = new BGComboBox(); this.typeCombo.addItem(new ComboBoxItem("0", "безусловно")); this.typeCombo.addItem(new ComboBoxItem("1", "пропорц. периоду")); this.typeCombo.addItem(new ComboBoxItem("2", "пропорц. объему")); this.typeCombo.addItem(new ComboBoxItem("3", "как выгоднее")); this.customChb = new JCheckBox("Разрешено переопределение в договоре"); } } protected JPanel getEditorPanel() { initEdit(); JPanel edit = new JPanel(); edit.add(new JLabel("Цена ")); edit.add(this.costTf); edit.add(new JLabel(" начислять ")); edit.add(this.typeCombo); edit.add(this.customChb); return edit; } public Component getView() { if (this.view == null) { this.view = new JLabel(icon, SwingConstants.LEFT); } extractData(); StringBuilder title = new StringBuilder(); title.append(this.cost); title.append(" "); if (this.type.equals("0")) { title.append("безусловно"); } else if (this.type.equals("1")) { title.append("пропорц. периоду"); } else if (this.type.equals("2")) { title.append("пропорц. объему"); } else if (this.type.equals("3")) { title.append("как выгоднее"); } if (this.custom) { title.append(" (возможно переопределение)"); } this.view.setText(title.toString()); return this.view; } private void extractData() { Map data = getDataInHash(); this.cost = ((String)data.get("cost")); this.type = Utils.maskNull((String)data.get("type")); this.custom = Utils.parseBoolean((String)data.get("custom"), false); } protected void loadData() { extractData(); this.costTf.setText(this.cost); ClientUtils.setComboBoxSelection(this.typeCombo, this.type); this.customChb.setSelected(this.custom); } protected void serializeData() { Map<String, String> data = new HashMap<String, String>(); data.put("cost", this.costTf.getText()); data.put("type", ClientUtils.getIdFromComboBox(this.typeCombo)); data.put("custom", Utils.booleanToStringInt(this.customChb.isSelected())); setDataInHash(data); } } package bitel.billing.module.services.npay; import bitel.billing.module.common.FloatTextField; import bitel.billing.module.tariff.DefaultTariffTreeNode; import java.awt.Component; import java.util.HashMap; import java.util.Map; import javax.swing.*; import ru.bitel.bgbilling.client.util.ClientUtils; import ru.bitel.common.Utils; /** * Стандартный узел YearModeCostTariffTreeNode на 22.03.2013, с добавлением чекбокса "Разрешено переопределение в договоре" * (custom=0|1 [default=0]) */ public class YearModeCustomCostTariffTreeNode extends DefaultTariffTreeNode { private static Icon icon = ClientUtils.getIcon("coin"); private JLabel view; private String cost; private boolean custom; private FloatTextField costTf; private JCheckBox customChb; private void initEdit() { if (this.costTf == null) { this.costTf = new FloatTextField(); this.customChb = new JCheckBox("Разрешено переопределение в договоре"); } } protected JPanel getEditorPanel() { initEdit(); JPanel edit = new JPanel(); edit.add(new JLabel("Стоимость: ")); edit.add(this.costTf); edit.add(this.customChb); return edit; } public Component getView() { if (this.view == null) { this.view = new JLabel(icon, SwingConstants.LEFT); } extractData(); StringBuilder title = new StringBuilder(); title.append("Стоимость: "); title.append(this.cost); if (this.custom) { title.append(" (возможно переопределение)"); } this.view.setText(title.toString()); return this.view; } private void extractData() { Map data = getDataInHash(); this.cost = ((String)data.get("cost")); this.custom = Utils.parseBoolean((String) data.get("custom"), false); } protected void loadData() { extractData(); this.costTf.setText(this.cost); this.customChb.setSelected(this.custom); } protected void serializeData() { Map<String, String> data = new HashMap<String, String>(); data.put("cost", this.costTf.getText()); data.put("custom", Utils.booleanToStringInt(this.customChb.isSelected())); setDataInHash(data); } }