Mastering Currency Rate Check Skipping: Configuration and Coding Guide
iDempiere

Mastering Currency Rate Check Skipping: Configuration and Coding Guide

2023-08-19 最後更新:2026-02-20) · 12 分鐘 · Ray Lee (System Analyst)

摘要

在 iDempiere 7.1 之後的版本中,系統引入了一項新功能:在輸入和儲存匯率後,會自動檢查是否存在重疊的匯率。然而,有時候您需要建立長期匯率與即期匯率並存的情況,這可能會觸發不必要的重疊警告,如圖 1 所示。本教學深入探討了此情境的實用解決方案,提供逐步指導,教您如何有效地撰寫程式碼並實作繞過重疊檢查的機制。透過清晰的說明和實際範例,本指南將幫助您在 iDempiere 中順利調整匯率檢查流程,以滿足您特定的業務需求。

Mastering Currency Rate Check Skipping: Configuration and Coding Guide

使用方式

在系統設定中,建立一個名為「Is_CurrencyRate_Overlap」的新設定,並將其值設為「N」。

使用「Y」將啟用重疊檢查。使用「N」將停用重疊檢查。
如圖 2 所示。

Mastering Currency Rate Check Skipping: Configuration and Coding Guide
圖 2

程式碼

操作步驟:

  1. 首先建立一個名為 CustomMConversionRate 的新類別,繼承自 org.compiere.model.MConversionRate
  2. 覆寫兩個方法:beforeSaveafterSave,如下方程式碼片段所示。
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);
		// 自動生成的建構子
	}

	public MConversionRate(Properties ctx, int C_Conversion_Rate_ID, String trxName) {
		super(ctx, C_Conversion_Rate_ID, trxName);
		// 自動生成的建構子
	}

	public MConversionRate(Properties ctx, ResultSet rs, String trxName) {
		super(ctx, rs, trxName);
		// 自動生成的建構子
	}

	@Override
	protected boolean beforeSave(boolean newRecord) {
		//	來源與目標幣別相同
		if (getC_Currency_ID() == getC_Currency_ID_To())
		{
			log.saveError("Error", Msg.parseTranslation(getCtx(), "@C_Currency_ID@ = @C_Currency_ID@"));
			return false;
		}
		//	無可轉換項目
		if (getMultiplyRate().compareTo(Env.ZERO) <= 0)
		{
			log.saveError("Error", Msg.parseTranslation(getCtx(), "@MultiplyRate@ <= 0"));
			return false;
		}

		//	日期範圍檢查
		Timestamp from = getValidFrom();
		if (getValidTo() == null) {
			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;
		}
		// 使用 MSysconfig 中的設定來決定是否跳過重疊檢查。
		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) {
				// 建立反向匯率
				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());
				// 反轉
				reciprocal.setC_Currency_ID(getC_Currency_ID_To());
				reciprocal.setC_Currency_ID_To(getC_Currency_ID());
			}
			// 避免重複計算
			reciprocal.set_Value(COLUMNNAME_DivideRate, getMultiplyRate());
			reciprocal.set_Value(COLUMNNAME_MultiplyRate, getDivideRate());
			recursiveCall = true;
			try {
				reciprocal.saveEx();
			} finally {
				recursiveCall = false;
			}
		}
		return success;
	}

}

實作方式

當然,您可以使用 ModelFactory 將模型類別放入外掛中。有關詳細的逐步流程,請參閱下方提供的文章。

歡迎瀏覽該文章,以獲得有關如何使用 ModelFactory 將模型類別整合到外掛中的完整指導。

https://wiki.idempiere.org/en/Developing_plug-ins_without_affecting_the_trunk

https://wiki.idempiere.org/en/Developing_Plug-Ins_-_IModelFactory

English Version

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.

Mastering Currency Rate Check Skipping: Configuration and Coding Guide

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.

Mastering Currency Rate Check Skipping: Configuration and Coding Guide
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

日本語版

概要

iDempiere 7.1 以降のバージョンでは、通貨レートの入力・保存後に重複する通貨レートをチェックする新機能が導入されました。しかし、長期レートとスポットレートを併用する必要がある場合、不要な重複警告が発生する可能性があります(図1参照)。本チュートリアルでは、このシナリオに対する実用的な解決策を詳しく解説し、重複チェックを回避するメカニズムを効果的にコーディング・実装する方法をステップバイステップで説明します。明確な解説と実践的な例を通じて、iDempiere の通貨レートチェックプロセスをお客様の特定のビジネスニーズに合わせてスムーズに調整する方法を習得できます。

Mastering Currency Rate Check Skipping: Configuration and Coding Guide

使用方法

システム設定において、「Is_CurrencyRate_Overlap」という名前の新しい設定を作成し、値を「N」に設定します。

「Y」を使用すると重複チェックが有効になります。「N」を使用すると重複チェックが無効になります。
図2に示すとおりです。

Mastering Currency Rate Check Skipping: Configuration and Coding Guide
図2

コーディング

手順:

  1. まず、org.compiere.model.MConversionRate を継承する CustomMConversionRate という新しいクラスを作成します。
  2. 以下のコードスニペットに示すように、beforeSaveafterSave の2つのメソッドをオーバーライドします。
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);
		// 自動生成されたコンストラクタ
	}

	public MConversionRate(Properties ctx, int C_Conversion_Rate_ID, String trxName) {
		super(ctx, C_Conversion_Rate_ID, trxName);
		// 自動生成されたコンストラクタ
	}

	public MConversionRate(Properties ctx, ResultSet rs, String trxName) {
		super(ctx, rs, trxName);
		// 自動生成されたコンストラクタ
	}

	@Override
	protected boolean beforeSave(boolean newRecord) {
		//	変換元と変換先が同じ通貨
		if (getC_Currency_ID() == getC_Currency_ID_To())
		{
			log.saveError("Error", Msg.parseTranslation(getCtx(), "@C_Currency_ID@ = @C_Currency_ID@"));
			return false;
		}
		//	変換対象なし
		if (getMultiplyRate().compareTo(Env.ZERO) <= 0)
		{
			log.saveError("Error", Msg.parseTranslation(getCtx(), "@MultiplyRate@ <= 0"));
			return false;
		}

		//	日付範囲チェック
		Timestamp from = getValidFrom();
		if (getValidTo() == null) {
			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;
		}
		// MSysconfigの設定を使用して、重複チェックをスキップするかどうかを判断します。
		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) {
				// 逆方向のレートを作成
				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());
				// 反転
				reciprocal.setC_Currency_ID(getC_Currency_ID_To());
				reciprocal.setC_Currency_ID_To(getC_Currency_ID());
			}
			// 再計算を回避
			reciprocal.set_Value(COLUMNNAME_DivideRate, getMultiplyRate());
			reciprocal.set_Value(COLUMNNAME_MultiplyRate, getDivideRate());
			recursiveCall = true;
			try {
				reciprocal.saveEx();
			} finally {
				recursiveCall = false;
			}
		}
		return success;
	}

}

実装方法

ModelFactory を使用してモデルクラスをプラグインに組み込むことができます。詳しい手順については、以下の記事をご参照ください。

ModelFactory を使用してモデルクラスをプラグインに統合する方法について、包括的なガイダンスを記事でご確認いただけます。

https://wiki.idempiere.org/en/Developing_plug-ins_without_affecting_the_trunk

https://wiki.idempiere.org/en/Developing_Plug-Ins_-_IModelFactory

Ray Lee (System Analyst)
作者 Ray Lee (System Analyst)

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