Summary
In the post-iDempiere 7.1 era, a new feature was introduced where the system checks for overlapping currency rates after input and save. However, there are instances when you need to establish a long-term rate alongside a spot currency rate, potentially triggering an unnecessary overlap alert. As shown in Figure 1. This tutorial delves into a practical solution for this scenario, offering step-by-step guidance on how to effectively code and implement a mechanism to bypass the overlap check. With clear explanations and hands-on examples, this guide equips you to seamlessly navigate and modify the currency rate check process in iDempiere to suit your specific business needs.
Usage
Within the System Configuration, create a new configuration named “Is_CurrencyRate_Overlap” and assign the value “N” to it.
Using “Y” will activate the Overlap check. Using “N” will deactivate the Overlap check.
As shown in Figure 2.
Coding
Instructions:
- Begin by creating a new class named
CustomMConversionRate
that extends fromorg.compiere.model.MConversionRate
. - Override two methods:
beforeSave
andafterSave
, as demonstrated in the code snippet below.
package tw.ninniku.trade.model;
import java.math.BigDecimal;
import java.sql.ResultSet;
import java.sql.Timestamp;
import java.text.SimpleDateFormat;
import java.util.List;
import java.util.Properties;
import org.compiere.model.MSysConfig;
import org.compiere.model.PO;
import org.compiere.model.Query;
import org.compiere.util.DisplayType;
import org.compiere.util.Env;
import org.compiere.util.Msg;
public class MConversionRate extends org.compiere.model.MConversionRate {
/**
*
*/
private static final long serialVersionUID = 6241095402675778073L;
private boolean recursiveCall = false;
public MConversionRate(PO po, int C_ConversionType_ID, int C_Currency_ID, int C_Currency_ID_To,
BigDecimal MultiplyRate, Timestamp ValidFrom) {
super(po, C_ConversionType_ID, C_Currency_ID, C_Currency_ID_To, MultiplyRate, ValidFrom);
// TODO Auto-generated constructor stub
}
public MConversionRate(Properties ctx, int C_Conversion_Rate_ID, String trxName) {
super(ctx, C_Conversion_Rate_ID, trxName);
// TODO Auto-generated constructor stub
}
public MConversionRate(Properties ctx, ResultSet rs, String trxName) {
super(ctx, rs, trxName);
// TODO Auto-generated constructor stub
}
@Override
protected boolean beforeSave(boolean newRecord) {
// From - To is the same
if (getC_Currency_ID() == getC_Currency_ID_To())
{
log.saveError("Error", Msg.parseTranslation(getCtx(), "@C_Currency_ID@ = @C_Currency_ID@"));
return false;
}
// Nothing to convert
if (getMultiplyRate().compareTo(Env.ZERO) <= 0)
{
log.saveError("Error", Msg.parseTranslation(getCtx(), "@MultiplyRate@ <= 0"));
return false;
}
// Date Range Check
Timestamp from = getValidFrom();
if (getValidTo() == null) {
// setValidTo (TimeUtil.getDay(2056, 1, 29)); // no exchange rates after my 100th birthday
log.saveError("FillMandatory", Msg.getElement(getCtx(), COLUMNNAME_ValidTo));
return false;
}
Timestamp to = getValidTo();
if (to.before(from))
{
SimpleDateFormat df = DisplayType.getDateFormat(DisplayType.Date);
log.saveError("Error", df.format(to) + " < " + df.format(from));
return false;
}
//Use the settings in MSysconfig to determine whether to skip the Overlap check.
if(!MSysConfig.getBooleanValue ("Is_CorrencyRate_Overlap", true,getAD_Client_ID()))
{
return true;
}
if (isActive()) {
String whereClause = "(? BETWEEN ValidFrom AND ValidTo OR ? BETWEEN ValidFrom AND ValidTo) "
+ "AND C_Currency_ID=? AND C_Currency_ID_To=? "
+ "AND C_Conversiontype_ID=? "
+ "AND AD_Client_ID=? AND AD_Org_ID=?";
List<MConversionRate> convs = new Query(getCtx(), MConversionRate.Table_Name, whereClause, get_TrxName())
.setOnlyActiveRecords(true)
.setParameters(getValidFrom(), getValidTo(),
getC_Currency_ID(), getC_Currency_ID_To(),
getC_ConversionType_ID(),
getAD_Client_ID(), getAD_Org_ID())
.list();
for (MConversionRate conv : convs) {
if (conv.getC_Conversion_Rate_ID() != getC_Conversion_Rate_ID()) {
log.saveError("Error", "Conversion rate overlaps with: " + conv.getValidFrom());
return false;
}
}
}
return true;
}
@Override
protected boolean afterSave(boolean newRecord, boolean success) {
if (success && !recursiveCall ) {
String whereClause = "ValidFrom=? AND ValidTo=? "
+ "AND C_Currency_ID=? AND C_Currency_ID_To=? "
+ "AND C_ConversionType_ID=? "
+ "AND AD_Client_ID=? AND AD_Org_ID=?";
List<MConversionRate> list = new Query(getCtx(), MConversionRate.Table_Name, whereClause, get_TrxName())
.setParameters(getValidFrom(), getValidTo(),
getC_Currency_ID_To(), getC_Currency_ID(),
getC_ConversionType_ID(),
getAD_Client_ID(), getAD_Org_ID())
.setOrderBy(MConversionRate.COLUMNNAME_ValidFrom + " DESC")
.list();
MConversionRate reciprocal = null;
for (MConversionRate rate : list) {
reciprocal = rate;
break;
}
if (reciprocal == null) {
// create reciprocal rate
reciprocal = new MConversionRate(getCtx(), 0, get_TrxName());
reciprocal.setValidFrom(getValidFrom());
reciprocal.setValidTo(getValidTo());
reciprocal.setC_ConversionType_ID(getC_ConversionType_ID());
reciprocal.setAD_Client_ID(getAD_Client_ID());
reciprocal.setAD_Org_ID(getAD_Org_ID());
// invert
reciprocal.setC_Currency_ID(getC_Currency_ID_To());
reciprocal.setC_Currency_ID_To(getC_Currency_ID());
}
// avoid recalculation
reciprocal.set_Value(COLUMNNAME_DivideRate, getMultiplyRate());
reciprocal.set_Value(COLUMNNAME_MultiplyRate, getDivideRate());
recursiveCall = true;
try {
reciprocal.saveEx();
} finally {
recursiveCall = false;
}
}
return success;
}
}
Implementation
Certainly, you can place the model class into a plugin using ModelFactory. For a detailed step-by-step process, you can refer to the article provided below.
Feel free to explore the article for comprehensive guidance on how to incorporate the model class into a plugin using ModelFactory.
https://wiki.idempiere.org/en/Developing_plug-ins_without_affecting_the_trunk
https://wiki.idempiere.org/en/Developing_Plug-Ins_-_IModelFactory