Персональные цены для договоров

Материал из BiTel WiKi

(Различия между версиями)
Перейти к: навигация, поиск
(Альтернативный узел "Стоимость" модуля NPay)
 
(7 промежуточных версий не показаны.)
Строка 894: Строка 894:
Но нужно понимать, что при любом обновлении нужно будет следить за изменениями стандартного Npay.xml и соответствующим образом менять собственный файл.
Но нужно понимать, что при любом обновлении нужно будет следить за изменениями стандартного Npay.xml и соответствующим образом менять собственный файл.
 +
 +
== Интерфейс ==
 +
 +
=== Редактирование цен ===
 +
 +
Отлично, теперь цены у нас переопределяются по табличке custom_tariff_cost. Теперь нужно написать удобный интерфейс работы с этой табличкой.
 +
 +
==== Веб-сервис переопределения цен ====
 +
 +
Вспомогательные классы:
 +
 +
<source lang=java>
 +
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;
 +
    }
 +
}
 +
</source>
 +
 +
<source lang=java>
 +
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;
 +
}
 +
</source>
 +
 +
<source lang=java>
 +
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));
 +
    }
 +
}
 +
</source>
 +
 +
Веб-сервис работы с переопределёнными ценами:
 +
 +
<source lang=java>
 +
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;
 +
}
 +
</source>
 +
 +
<source lang=java>
 +
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;
 +
    }
 +
 +
}
 +
</source>
 +
 +
==== Клиетский интерфейс ====
 +
 +
Для начала немного костылей.
 +
 +
Поскольку услуг в биллинге заведено много, а в конкретном тарифе обычно можно переопределять всего несколько, то для облегчения интерфейса веб-сервис выдаёт только те услуги, которые разрешено переопределять для данного тарифа. Но получать список таких услуг через разбор тарифного дерева представляется излишне сложной задачей, поэтому было решено реализовать список разрешённых к переопределению услуг через тарифные метки:
 +
 +
[[Файл:custom_cost_labels.jpg]]
 +
 +
Т.е. чтобы услуга появилась в интерфейсе, нужно завести метку вида "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:
 +
 +
<source lang=ini>
 +
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
 +
</source>
 +
 +
Панель:
 +
<source lang=java>
 +
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();
 +
                    }
 +
                };
 +
            }
 +
        }
 +
    }
 +
 +
}
 +
 +
</source>
 +
 +
Клиентские узлы тарифа:
 +
<source lang=java>
 +
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);
 +
    }
 +
}
 +
 +
</source>
 +
 +
=== Отображение на договоре ===
 +
 +
На договоре персональные цены на услуги будем писать другим шрифтом в скобках через запятую после наименования тарифа в дереве.
 +
 +
Для этого переопределим через dynaction стандартный action вывода карточки договора:
 +
 +
<source lang=ini>
 +
dynaction:contract.hierarchy.ActionContractInfo=ru.dsi.bgbilling.kernel.scripts.action.ActionContractInfo
 +
</source>
 +
 +
Переопределённый action делает много всяких штук, кроме подсветки персональных цен. Привожу целиком, лишнее можно убрать по необходимости:
 +
 +
<source lang=java>
 +
package ru.dsi.bgbilling.kernel.scripts.action.contract;
 +
 +
import bitel.billing.common.TimeUtils;
 +
import bitel.billing.server.ApplicationModule;
 +
import bitel.billing.server.call.bean.Login;
 +
import bitel.billing.server.contract.bean.ContractModuleManager;
 +
import bitel.billing.server.contract.bean.PersonalTariff;
 +
import bitel.billing.server.contract.bean.PersonalTariffManager;
 +
import bitel.billing.server.dialup.bean.DialUpLoginManager;
 +
import bitel.billing.server.email.bean.Account;
 +
import bitel.billing.server.email.bean.AccountManager;
 +
import bitel.billing.server.npay.bean.ServiceObject;
 +
import bitel.billing.server.npay.bean.ServiceObjectManager;
 +
import bitel.billing.server.phone.bean.ClientItem;
 +
import bitel.billing.server.phone.bean.ClientItemManager;
 +
import bitel.billing.server.voiceip.bean.VoiceIpLoginManager;
 +
import org.w3c.dom.Element;
 +
import org.w3c.dom.NodeList;
 +
import ru.bitel.bgbilling.common.BGException;
 +
import ru.bitel.bgbilling.kernel.base.server.DefaultContext;
 +
import ru.bitel.bgbilling.kernel.container.security.server.ModuleAction;
 +
import ru.bitel.bgbilling.kernel.container.security.server.PermissionChecker;
 +
import ru.bitel.bgbilling.kernel.contract.balance.server.ConvergenceBalance;
 +
import ru.bitel.bgbilling.kernel.contract.balance.server.ConvergenceBalanceManager;
 +
import ru.bitel.bgbilling.kernel.module.common.bean.BGModule;
 +
import ru.bitel.bgbilling.modules.inet.api.common.bean.InetServ;
 +
import ru.bitel.bgbilling.modules.inet.api.server.bean.InetServDao;
 +
import ru.bitel.bgbilling.modules.ipn.common.bean.AddressRange;
 +
import ru.bitel.bgbilling.modules.ipn.server.bean.AddressRangeManager;
 +
import ru.bitel.bgbilling.server.util.Setup;
 +
import ru.bitel.common.Utils;
 +
import ru.bitel.common.XMLUtils;
 +
import ru.bitel.common.worker.ThreadContext;
 +
 +
import java.sql.PreparedStatement;
 +
import java.sql.ResultSet;
 +
import java.sql.SQLException;
 +
import java.util.Calendar;
 +
import java.util.Date;
 +
import java.util.HashMap;
 +
import java.util.Map;
 +
 +
/**
 +
* @author cromeshnic@gmail.com
 +
*/
 +
public class ActionContractInfo extends bitel.billing.server.contract.action.ActionContractInfo{
 +
    @Override
 +
    public void doAction() throws SQLException, BGException
 +
    {
 +
        super.doAction();
 +
 +
        Date now = new Date();
 +
 +
        //Удаляем модули
 +
        Element modulesNode = XMLUtils.selectElement(rootNode, "/data/info/modules");
 +
        if(modulesNode!=null) {
 +
            NodeList list = modulesNode.getChildNodes();
 +
            while (modulesNode.hasChildNodes()){
 +
                modulesNode.removeChild(list.item(0));
 +
            }
 +
        }
 +
        //Количество сущностей модуля
 +
        int itemCount;
 +
        //Количество открытых датой сущностей модуля
 +
        int openItemCount;
 +
        InetServDao servDao;
 +
        InetServ tree;
 +
        ClientItemManager clientItemManager;
 +
        DialUpLoginManager lm;
 +
        ServiceObjectManager manager;
 +
        AccountManager accountManager;
 +
        AddressRangeManager arm;
 +
 +
        //Добавляем модули заново с доп данными:
 +
        for (BGModule module : new ContractModuleManager(this.con).getContractModules(cid))
 +
        {
 +
            Element item = createElement(modulesNode, "item");
 +
            String className = module.getInstalledModule().getPackageServer() + ".Module";
 +
            ApplicationModule moduleClass = (ApplicationModule) Utils.newInstance(className, ApplicationModule.class);
 +
            itemCount=-1;
 +
            openItemCount=-1;
 +
            //Inet
 +
            if(moduleClass instanceof ru.bitel.bgbilling.modules.inet.api.server.Module){
 +
                itemCount=0;
 +
                openItemCount=0;
 +
                servDao = new InetServDao(con, module.getId(), 0);
 +
                tree = servDao.tree(this.cid);
 +
                if(tree.getChildren()!=null){
 +
                    itemCount = tree.getChildren().size();
 +
                    for (InetServ inetServ : tree.getChildren()) {
 +
                        if(TimeUtils.dateBefore(now, inetServ.getDateTo()) || inetServ.getDateTo()==null){
 +
                            openItemCount++;
 +
                        }
 +
                    }
 +
                }
 +
                servDao.recycle();
 +
            }
 +
 +
            //Phone
 +
            if(moduleClass instanceof bitel.billing.server.phone.Module){
 +
                itemCount=0;
 +
                openItemCount=0;
 +
                clientItemManager = new ClientItemManager(con, module.getId());
 +
                for (ClientItem clientItem : clientItemManager.getItemList(this.cid)) {
 +
                    itemCount++;
 +
                    if(TimeUtils.dateBefore(Calendar.getInstance(), clientItem.getDate2()) || clientItem.getDate2()==null){
 +
                        openItemCount++;
 +
                    }
 +
                }
 +
            }
 +
            //Dialup
 +
            if(moduleClass instanceof bitel.billing.server.dialup.Module){
 +
                itemCount=0;
 +
                openItemCount=0;
 +
                lm = new DialUpLoginManager(con, module.getId());
 +
                for (Login login : lm.getContractLogins(this.cid)) {
 +
                    itemCount++;
 +
                    if(login.getDate2()==null || TimeUtils.dateBefore(now, login.getDate2())){
 +
                        openItemCount++;
 +
                    }
 +
                }
 +
            }
 +
            //Npay
 +
            if(moduleClass instanceof bitel.billing.server.npay.Module){
 +
                itemCount=0;
 +
                openItemCount=0;
 +
                manager = new ServiceObjectManager(con, module.getId());
 +
                for (ServiceObject so : manager.getServiceObjectList(this.cid, null)) {
 +
                    itemCount++;
 +
                    if(so.getDate2()==null || TimeUtils.dateBefore(now, so.getDate2())){
 +
                        openItemCount++;
 +
                    }
 +
                }
 +
            }
 +
            //Email
 +
            if(moduleClass instanceof bitel.billing.server.email.Module){
 +
                itemCount=0;
 +
                openItemCount=0;
 +
                accountManager = new AccountManager(con, module.getId());
 +
                for (Account acc : accountManager.getContractAccountList(this.cid)) {
 +
                    itemCount++;
 +
                    if(acc.getDate2()==null || TimeUtils.dateBefore(now, acc.getDate2())){
 +
                        openItemCount++;
 +
                    }
 +
                }
 +
            }
 +
            //IPN
 +
            if(moduleClass instanceof bitel.billing.server.ipn.Module){
 +
                itemCount=0;
 +
                openItemCount=0;
 +
                arm = new AddressRangeManager(con, module.getId());
 +
                for (AddressRange range: arm.getContractRanges(this.cid, null, 0)) {
 +
                    itemCount++;
 +
                    if(range.getDate2()==null || TimeUtils.dateBefore(Calendar.getInstance(), range.getDate2())){
 +
                        openItemCount++;
 +
                    }
 +
                }
 +
                for (AddressRange range: arm.getContractNets(this.cid, null, 0)) {
 +
                    itemCount++;
 +
                    if(range.getDate2()==null || TimeUtils.dateBefore(Calendar.getInstance(), range.getDate2())){
 +
                        openItemCount++;
 +
                    }
 +
                }
 +
            }
 +
            //VoiceIP
 +
            if(moduleClass instanceof bitel.billing.server.voiceip.Module){
 +
                itemCount=0;
 +
                openItemCount=0;
 +
                for (Login login : new VoiceIpLoginManager(con, module.getId()).getContractLogins(this.cid)) {
 +
                    itemCount++;
 +
                    if(login.getDate2()==null || TimeUtils.dateBefore(now, login.getDate2())){
 +
                        openItemCount++;
 +
                    }
 +
                }
 +
            }
 +
 +
            item.setAttribute("id", String.valueOf(module.getId()));
 +
            if(itemCount>=0 && openItemCount>=0){
 +
                item.setAttribute("title", "<html>"+module.getTitle()+" <font style='color: grey;' size='2'>["+openItemCount+"/"+itemCount+"]</font></htmp>");
 +
            }else{
 +
                item.setAttribute("title", module.getTitle());
 +
            }
 +
            item.setAttribute("package", module.getInstalledModule().getPackageClient());
 +
            if (moduleClass != null)
 +
            {
 +
                item.setAttribute("status", moduleClass.getStatus(this.con, module.getId(), cid));
 +
            }
 +
        }
 +
 +
        Element tariff = XMLUtils.selectElement(rootNode, "/data/info/tariff");
 +
        //Удаляем все тарифы из списка - будем строить свой список
 +
        if(tariff!=null){
 +
            NodeList list = tariff.getChildNodes();
 +
            while(tariff.hasChildNodes()){
 +
                tariff.removeChild(list.item(0));
 +
            }
 +
            /*for(int i = 0;i<list.getLength();i++){
 +
                tariff.removeChild(list.item(i));
 +
            }*/
 +
            //Составляем свой список тарифов с красивостями
 +
            Map<String, String> mapRequest = new HashMap<String, String>();
 +
            mapRequest.put("action", "PersonalTariffTable");
 +
            mapRequest.put("module", "contract.tariff");
 +
 +
            ModuleAction action = PermissionChecker.getInstance().findAction(new String[] { "0" }, mapRequest);
 +
            if ((Setup.getSetup().getInt("bgsecure.check", 1) == 0) ||
 +
                    (action == null) || (this.userID == 1) ||
 +
                    (PermissionChecker.getInstance().checkActionAllow(mapRequest, this.con, "0", action, this.userID, cid) == null))
 +
            {
 +
                for (PersonalTariff personalTariff : new PersonalTariffManager(this.con)
 +
                        .getPersonalTariffList(cid, new Date())) {
 +
                    addListItem(tariff, personalTariff.getId(), "<html><font style='color: green;'>" + "ПТ: " + personalTariff.getTitle()+"</font></html>");
 +
                }
 +
            }
 +
 +
            mapRequest.clear();
 +
            mapRequest.put("action", "ContractTariffPlans");
 +
            mapRequest.put("module", "contract");
 +
 +
            action = PermissionChecker.getInstance().findAction(new String[] { "0" }, mapRequest);
 +
            if ((Setup.getSetup().getInt("bgsecure.check", 1) == 0) ||
 +
                    (action == null) || (this.userID == 1) ||
 +
                    (PermissionChecker.getInstance().checkActionAllow(mapRequest, this.con, "0", action, this.userID, cid) == null))
 +
            {
 +
                String query = "SELECT t2.id, t2.title, group_concat(TRIM(TRAILING '.' FROM TRIM(TRAILING '0' FROM ctc.value))) value, t1.date1 FROM contract_tariff AS t1 left join tariff_plan AS t2 on t1.tpid=t2.id left join custom_tariff_cost ctc on t1.id=ctc.contract_tariff_id " +
 +
                        " WHERE t1.cid=? AND emid=0 AND eid=0 AND ( isNull( date2 ) OR date2>=CURDATE() ) group by t1.id";
 +
                //AND (isNull( date1 ) OR date1<=CURDATE())
 +
                PreparedStatement ps = this.con.prepareStatement(query);
 +
                ps.setInt(1, cid);
 +
                ResultSet rs = ps.executeQuery();
 +
                String value;
 +
                Date dt1;
 +
                String name;
 +
                while (rs.next())
 +
                {
 +
                    value = rs.getString(3);
 +
                    name = rs.getString(2);
 +
                    //21.12.2016 by semyon@dsi.ru - show future tariffs too, but with another color
 +
                    dt1=TimeUtils.convertSqlDateToDate(rs.getDate(4));
 +
                    if(TimeUtils.dateBefore(now,dt1)){
 +
                        name = "<font style='color: #cc99ff;'>"+name+"</font>";
 +
                    }
 +
                    if(value==null){
 +
                        addListItem(tariff, rs.getInt(1), "<html>"+name+"</html>");
 +
                    }else{
 +
                        addListItem(tariff, rs.getInt(1), "<html><font style='color: green;'>*</font> "+name+" <font style='color: green;'>("+value+")</font></html>" );
 +
                    }
 +
                }
 +
                rs.close();
 +
                ps.close();
 +
            }
 +
        }
 +
 +
        //Подсвечиваем статус "закрыт"
 +
        Element contractElement = XMLUtils.selectElement(rootNode, "contract");
 +
        if(contractElement!=null){
 +
            if(contractElement.getAttribute("status").equals("закрыт")){
 +
                contractElement.setAttribute("status", "<html><font style='color: red;'>закрыт</font></html>");
 +
            }else if(contractElement.getAttribute("status").equals("приостановлен")){
 +
                contractElement.setAttribute("status", "<html><font style='color: blue;'>приостановлен</font></html>");
 +
            }else if(contractElement.getAttribute("status").equals("в отключении")){
 +
                contractElement.setAttribute("status", "<html><font style='color: #5F84B6;'>в отключении</font></html>");
 +
            }
 +
        }
 +
 +
        //Проверяем баланс и лимит на договоре - выделяем лимит красным, если баланс<лимита
 +
        if(contractElement!=null){
 +
            ConvergenceBalanceManager convergenceBalanceManager = ConvergenceBalanceManager.getInstance();
 +
            if (convergenceBalanceManager != null) {
 +
                ConvergenceBalance balance = convergenceBalanceManager.getBalance((ThreadContext.get(DefaultContext.class)).getConnectionSet(), this.cid, System.currentTimeMillis());
 +
                if(balance!=null && !balance.isBalanceUnderLimit()){
 +
                    contractElement.setAttribute("limit", "<html><font style='color: red;'>"+contractElement.getAttribute("limit")+"</font></html>");
 +
                }
 +
            }
 +
        }
 +
    }
 +
}
 +
 +
</source>
 +
 +
== Заключение ==
 +
 +
У читателя, вероятно, возникает резонный вопрос: "Где howto? Дай мне готовый файлик, чтобы пользоваться фичей!".
 +
Но пока <s>мне лень</s> я не буду выкладывать готовых компилированных файлов и инструкций по подмене jar клиента и сервера.
 +
 +
* Во-первых, это всё работает на 5.2 и на старших версиях не тестировалось - наверняка вам всё равно придётся многое переписывать.
 +
* Во-вторых, использовать подмену стандартных классов плохо для кармы - не хочу, чтобы этим массово пользовались
 +
* В-третьих, давайте сперва обсудим фичу на [https://forum.bitel.ru/viewtopic.php?f=66&t=9709 форуме]
 +
 +
Хотелось бы, чтобы что-то подобное появилось в стандартной реализации от Битела. Собственно, в первую очередь поэтому я и решил выложить решение спустя 4 года после внедрения :)
 +
 +
Сейчас у меня в табличке custom_tariff_cost уже 5700 записей, фича работает как часы. Но есть минус - из-за большого количества таких фич в продакшене осложняется обновление на версии 6 и 7: нужно предварительно тестировать и переписывать большой объём кода.
 +
 +
Если вам интересны персональные тарифы на договоре, <s>подписывайтесь, ставьте лайк</s> пишите на форум разработчикам - чем больше будет запросов, тем быстрее появится стандартная реализация.
 +
 +
Также, если будет интерес, могу выложить свою реализацию процентных скидок на услуги. Она менее "костыльная" - не требуется подмена классов, только npay.xml для собственных тарифных узлов.
 +
 +
ps. Ещё можно скинуться автору на [https://money.yandex.ru/to/41001959637999 кофе] :)
 +
 +
--[[Участник:Cromeshnic|Cromeshnic]] 08:00, 8 мая 2017 (UTC)

Текущая версия на 08:02, 8 мая 2017

Внимание! Данное решение/метод/статья относится к версии 5.2 и для других версий может быть неактуальна! Вам нужно самостоятельно поправить решение под свои нужды или воспользоваться помощью на форуме. Будем признательны, если внизу страницы или отдельной статьёй вы разместите исправленное решение для другой версии или подсказки что надо исправить.

Содержание

Введение

Очень часто, особенно при работе с юр лицами, возникает потребность установки персональной цены абонплат для стандартных тарифов. В текущей реализации биллинга это можно сделать только через персональные тарифы. Но персональные тарифы очень неудобны:

  • Ломают отчёты по тарифам
  • Осложняют внесение изменений в глобальные тарифы, от которых они наследованы
  • Требуют специальных знаний и прав доступа для корректного использования

В результате было решено разработать собственное решение для поддержки персональных цен без применения персональных тарифов

Описание решения

Тезисно решение выглядит следующим образом

  1. Храним персональные цены на услуги в собственной таблице custom_tariff_cost: contract_tariff_id, sid -> value
  2. Меняем стандартную логику тарифного узла "стоимость" модуля абонплат: при наличии записи в custom_tariff_cost используем её вместо стандартной цены
  3. Пишем собственную серверную и клиентскую части интерфейса для управления переопределёнными ценами
  4. Дорабатываем выдачу списка тарифов договора для визуального отображения переопределённых цен

!!! Внимание! пп. 2 и 3 являются "хаками" стандартного поведения биллинга: требуется подмена стандартных классов своими !!!

Скриншоты:

Интерфейс переопределения цен:

Файл:custom_cost.jpg

Отображение на договоре:

Файл:custom_cost1.jpg

В тарифном дереве:

Файл:custom_cost2.jpg

Реализация

Переопределение цены

Таблица для хранения переопределений цен

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;
    }
 
}

Клиетский интерфейс

Для начала немного костылей.

Поскольку услуг в биллинге заведено много, а в конкретном тарифе обычно можно переопределять всего несколько, то для облегчения интерфейса веб-сервис выдаёт только те услуги, которые разрешено переопределять для данного тарифа. Но получать список таких услуг через разбор тарифного дерева представляется излишне сложной задачей, поэтому было решено реализовать список разрешённых к переопределению услуг через тарифные метки:

Файл:custom_cost_labels.jpg

Т.е. чтобы услуга появилась в интерфейсе, нужно завести метку вида "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);
    }
}

Отображение на договоре

На договоре персональные цены на услуги будем писать другим шрифтом в скобках через запятую после наименования тарифа в дереве.

Для этого переопределим через dynaction стандартный action вывода карточки договора:

dynaction:contract.hierarchy.ActionContractInfo=ru.dsi.bgbilling.kernel.scripts.action.ActionContractInfo

Переопределённый action делает много всяких штук, кроме подсветки персональных цен. Привожу целиком, лишнее можно убрать по необходимости:

package ru.dsi.bgbilling.kernel.scripts.action.contract;
 
import bitel.billing.common.TimeUtils;
import bitel.billing.server.ApplicationModule;
import bitel.billing.server.call.bean.Login;
import bitel.billing.server.contract.bean.ContractModuleManager;
import bitel.billing.server.contract.bean.PersonalTariff;
import bitel.billing.server.contract.bean.PersonalTariffManager;
import bitel.billing.server.dialup.bean.DialUpLoginManager;
import bitel.billing.server.email.bean.Account;
import bitel.billing.server.email.bean.AccountManager;
import bitel.billing.server.npay.bean.ServiceObject;
import bitel.billing.server.npay.bean.ServiceObjectManager;
import bitel.billing.server.phone.bean.ClientItem;
import bitel.billing.server.phone.bean.ClientItemManager;
import bitel.billing.server.voiceip.bean.VoiceIpLoginManager;
import org.w3c.dom.Element;
import org.w3c.dom.NodeList;
import ru.bitel.bgbilling.common.BGException;
import ru.bitel.bgbilling.kernel.base.server.DefaultContext;
import ru.bitel.bgbilling.kernel.container.security.server.ModuleAction;
import ru.bitel.bgbilling.kernel.container.security.server.PermissionChecker;
import ru.bitel.bgbilling.kernel.contract.balance.server.ConvergenceBalance;
import ru.bitel.bgbilling.kernel.contract.balance.server.ConvergenceBalanceManager;
import ru.bitel.bgbilling.kernel.module.common.bean.BGModule;
import ru.bitel.bgbilling.modules.inet.api.common.bean.InetServ;
import ru.bitel.bgbilling.modules.inet.api.server.bean.InetServDao;
import ru.bitel.bgbilling.modules.ipn.common.bean.AddressRange;
import ru.bitel.bgbilling.modules.ipn.server.bean.AddressRangeManager;
import ru.bitel.bgbilling.server.util.Setup;
import ru.bitel.common.Utils;
import ru.bitel.common.XMLUtils;
import ru.bitel.common.worker.ThreadContext;
 
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.Calendar;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
 
/**
 * @author cromeshnic@gmail.com
 */
public class ActionContractInfo extends bitel.billing.server.contract.action.ActionContractInfo{
    @Override
    public void doAction() throws SQLException, BGException
    {
        super.doAction();
 
        Date now = new Date();
 
        //Удаляем модули
        Element modulesNode = XMLUtils.selectElement(rootNode, "/data/info/modules");
        if(modulesNode!=null) {
            NodeList list = modulesNode.getChildNodes();
            while (modulesNode.hasChildNodes()){
                modulesNode.removeChild(list.item(0));
            }
        }
        //Количество сущностей модуля
        int itemCount;
        //Количество открытых датой сущностей модуля
        int openItemCount;
        InetServDao servDao;
        InetServ tree;
        ClientItemManager clientItemManager;
        DialUpLoginManager lm;
        ServiceObjectManager manager;
        AccountManager accountManager;
        AddressRangeManager arm;
 
        //Добавляем модули заново с доп данными:
        for (BGModule module : new ContractModuleManager(this.con).getContractModules(cid))
        {
            Element item = createElement(modulesNode, "item");
            String className = module.getInstalledModule().getPackageServer() + ".Module";
            ApplicationModule moduleClass = (ApplicationModule) Utils.newInstance(className, ApplicationModule.class);
            itemCount=-1;
            openItemCount=-1;
            //Inet
            if(moduleClass instanceof ru.bitel.bgbilling.modules.inet.api.server.Module){
                itemCount=0;
                openItemCount=0;
                servDao = new InetServDao(con, module.getId(), 0);
                tree = servDao.tree(this.cid);
                if(tree.getChildren()!=null){
                    itemCount = tree.getChildren().size();
                    for (InetServ inetServ : tree.getChildren()) {
                        if(TimeUtils.dateBefore(now, inetServ.getDateTo()) || inetServ.getDateTo()==null){
                            openItemCount++;
                        }
                    }
                }
                servDao.recycle();
            }
 
            //Phone
            if(moduleClass instanceof bitel.billing.server.phone.Module){
                itemCount=0;
                openItemCount=0;
                clientItemManager = new ClientItemManager(con, module.getId());
                for (ClientItem clientItem : clientItemManager.getItemList(this.cid)) {
                    itemCount++;
                    if(TimeUtils.dateBefore(Calendar.getInstance(), clientItem.getDate2()) || clientItem.getDate2()==null){
                        openItemCount++;
                    }
                }
            }
            //Dialup
            if(moduleClass instanceof bitel.billing.server.dialup.Module){
                itemCount=0;
                openItemCount=0;
                lm = new DialUpLoginManager(con, module.getId());
                for (Login login : lm.getContractLogins(this.cid)) {
                    itemCount++;
                    if(login.getDate2()==null || TimeUtils.dateBefore(now, login.getDate2())){
                        openItemCount++;
                    }
                }
            }
            //Npay
            if(moduleClass instanceof bitel.billing.server.npay.Module){
                itemCount=0;
                openItemCount=0;
                manager = new ServiceObjectManager(con, module.getId());
                for (ServiceObject so : manager.getServiceObjectList(this.cid, null)) {
                    itemCount++;
                    if(so.getDate2()==null || TimeUtils.dateBefore(now, so.getDate2())){
                        openItemCount++;
                    }
                }
            }
            //Email
            if(moduleClass instanceof bitel.billing.server.email.Module){
                itemCount=0;
                openItemCount=0;
                accountManager = new AccountManager(con, module.getId());
                for (Account acc : accountManager.getContractAccountList(this.cid)) {
                    itemCount++;
                    if(acc.getDate2()==null || TimeUtils.dateBefore(now, acc.getDate2())){
                        openItemCount++;
                    }
                }
            }
            //IPN
            if(moduleClass instanceof bitel.billing.server.ipn.Module){
                itemCount=0;
                openItemCount=0;
                arm = new AddressRangeManager(con, module.getId());
                for (AddressRange range: arm.getContractRanges(this.cid, null, 0)) {
                    itemCount++;
                    if(range.getDate2()==null || TimeUtils.dateBefore(Calendar.getInstance(), range.getDate2())){
                        openItemCount++;
                    }
                }
                for (AddressRange range: arm.getContractNets(this.cid, null, 0)) {
                    itemCount++;
                    if(range.getDate2()==null || TimeUtils.dateBefore(Calendar.getInstance(), range.getDate2())){
                        openItemCount++;
                    }
                }
            }
            //VoiceIP
            if(moduleClass instanceof bitel.billing.server.voiceip.Module){
                itemCount=0;
                openItemCount=0;
                for (Login login : new VoiceIpLoginManager(con, module.getId()).getContractLogins(this.cid)) {
                    itemCount++;
                    if(login.getDate2()==null || TimeUtils.dateBefore(now, login.getDate2())){
                        openItemCount++;
                    }
                }
            }
 
            item.setAttribute("id", String.valueOf(module.getId()));
            if(itemCount>=0 && openItemCount>=0){
                item.setAttribute("title", "<html>"+module.getTitle()+" <font style='color: grey;' size='2'>["+openItemCount+"/"+itemCount+"]</font></htmp>");
            }else{
                item.setAttribute("title", module.getTitle());
            }
            item.setAttribute("package", module.getInstalledModule().getPackageClient());
            if (moduleClass != null)
            {
                item.setAttribute("status", moduleClass.getStatus(this.con, module.getId(), cid));
            }
        }
 
        Element tariff = XMLUtils.selectElement(rootNode, "/data/info/tariff");
        //Удаляем все тарифы из списка - будем строить свой список
        if(tariff!=null){
            NodeList list = tariff.getChildNodes();
            while(tariff.hasChildNodes()){
                tariff.removeChild(list.item(0));
            }
            /*for(int i = 0;i<list.getLength();i++){
                tariff.removeChild(list.item(i));
            }*/
            //Составляем свой список тарифов с красивостями
            Map<String, String> mapRequest = new HashMap<String, String>();
            mapRequest.put("action", "PersonalTariffTable");
            mapRequest.put("module", "contract.tariff");
 
            ModuleAction action = PermissionChecker.getInstance().findAction(new String[] { "0" }, mapRequest);
            if ((Setup.getSetup().getInt("bgsecure.check", 1) == 0) ||
                    (action == null) || (this.userID == 1) ||
                    (PermissionChecker.getInstance().checkActionAllow(mapRequest, this.con, "0", action, this.userID, cid) == null))
            {
                for (PersonalTariff personalTariff : new PersonalTariffManager(this.con)
                        .getPersonalTariffList(cid, new Date())) {
                    addListItem(tariff, personalTariff.getId(), "<html><font style='color: green;'>" + "ПТ: " + personalTariff.getTitle()+"</font></html>");
                }
            }
 
            mapRequest.clear();
            mapRequest.put("action", "ContractTariffPlans");
            mapRequest.put("module", "contract");
 
            action = PermissionChecker.getInstance().findAction(new String[] { "0" }, mapRequest);
            if ((Setup.getSetup().getInt("bgsecure.check", 1) == 0) ||
                    (action == null) || (this.userID == 1) ||
                    (PermissionChecker.getInstance().checkActionAllow(mapRequest, this.con, "0", action, this.userID, cid) == null))
            {
                String query = "SELECT t2.id, t2.title, group_concat(TRIM(TRAILING '.' FROM TRIM(TRAILING '0' FROM ctc.value))) value, t1.date1 FROM contract_tariff AS t1 left join tariff_plan AS t2 on t1.tpid=t2.id left join custom_tariff_cost ctc on t1.id=ctc.contract_tariff_id " +
                        " WHERE t1.cid=? AND emid=0 AND eid=0 AND ( isNull( date2 ) OR date2>=CURDATE() ) group by t1.id";
                //AND (isNull( date1 ) OR date1<=CURDATE())
                PreparedStatement ps = this.con.prepareStatement(query);
                ps.setInt(1, cid);
                ResultSet rs = ps.executeQuery();
                String value;
                Date dt1;
                String name;
                while (rs.next())
                {
                    value = rs.getString(3);
                    name = rs.getString(2);
                    //21.12.2016 by semyon@dsi.ru - show future tariffs too, but with another color
                    dt1=TimeUtils.convertSqlDateToDate(rs.getDate(4));
                    if(TimeUtils.dateBefore(now,dt1)){
                        name = "<font style='color: #cc99ff;'>"+name+"</font>";
                    }
                    if(value==null){
                        addListItem(tariff, rs.getInt(1), "<html>"+name+"</html>");
                    }else{
                        addListItem(tariff, rs.getInt(1), "<html><font style='color: green;'>*</font> "+name+" <font style='color: green;'>("+value+")</font></html>" );
                    }
                }
                rs.close();
                ps.close();
            }
        }
 
        //Подсвечиваем статус "закрыт"
        Element contractElement = XMLUtils.selectElement(rootNode, "contract");
        if(contractElement!=null){
            if(contractElement.getAttribute("status").equals("закрыт")){
                contractElement.setAttribute("status", "<html><font style='color: red;'>закрыт</font></html>");
            }else if(contractElement.getAttribute("status").equals("приостановлен")){
                contractElement.setAttribute("status", "<html><font style='color: blue;'>приостановлен</font></html>");
            }else if(contractElement.getAttribute("status").equals("в отключении")){
                contractElement.setAttribute("status", "<html><font style='color: #5F84B6;'>в отключении</font></html>");
            }
        }
 
        //Проверяем баланс и лимит на договоре - выделяем лимит красным, если баланс<лимита
        if(contractElement!=null){
            ConvergenceBalanceManager convergenceBalanceManager = ConvergenceBalanceManager.getInstance();
            if (convergenceBalanceManager != null) {
                ConvergenceBalance balance = convergenceBalanceManager.getBalance((ThreadContext.get(DefaultContext.class)).getConnectionSet(), this.cid, System.currentTimeMillis());
                if(balance!=null && !balance.isBalanceUnderLimit()){
                    contractElement.setAttribute("limit", "<html><font style='color: red;'>"+contractElement.getAttribute("limit")+"</font></html>");
                }
            }
        }
    }
}

Заключение

У читателя, вероятно, возникает резонный вопрос: "Где howto? Дай мне готовый файлик, чтобы пользоваться фичей!". Но пока мне лень я не буду выкладывать готовых компилированных файлов и инструкций по подмене jar клиента и сервера.

  • Во-первых, это всё работает на 5.2 и на старших версиях не тестировалось - наверняка вам всё равно придётся многое переписывать.
  • Во-вторых, использовать подмену стандартных классов плохо для кармы - не хочу, чтобы этим массово пользовались
  • В-третьих, давайте сперва обсудим фичу на форуме

Хотелось бы, чтобы что-то подобное появилось в стандартной реализации от Битела. Собственно, в первую очередь поэтому я и решил выложить решение спустя 4 года после внедрения :)

Сейчас у меня в табличке custom_tariff_cost уже 5700 записей, фича работает как часы. Но есть минус - из-за большого количества таких фич в продакшене осложняется обновление на версии 6 и 7: нужно предварительно тестировать и переписывать большой объём кода.

Если вам интересны персональные тарифы на договоре, подписывайтесь, ставьте лайк пишите на форум разработчикам - чем больше будет запросов, тем быстрее появится стандартная реализация.

Также, если будет интерес, могу выложить свою реализацию процентных скидок на услуги. Она менее "костыльная" - не требуется подмена классов, только npay.xml для собственных тарифных узлов.

ps. Ещё можно скинуться автору на кофе :)

--Cromeshnic 08:00, 8 мая 2017 (UTC)

Личные инструменты