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.

Figure 2

Coding

Instructions:

  1. Begin by creating a new class named CustomMConversionRate that extends from org.compiere.model.MConversionRate.
  2. Override two methods: beforeSave and afterSave, 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

By Ray Lee (System Analyst)

iDempeire ERP Contributor, 經濟部中小企業處財務管理顧問 李寶瑞

Leave a Reply

Your email address will not be published. Required fields are marked *