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

使用方式
在系統設定中,建立一個名為「Is_CurrencyRate_Overlap」的新設定,並將其值設為「N」。
使用「Y」將啟用重疊檢查。使用「N」將停用重疊檢查。
如圖 2 所示。

程式碼
操作步驟:
- 首先建立一個名為
CustomMConversionRate的新類別,繼承自org.compiere.model.MConversionRate。 - 覆寫兩個方法:
beforeSave和afterSave,如下方程式碼片段所示。
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.

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
CustomMConversionRatethat extends fromorg.compiere.model.MConversionRate. - Override two methods:
beforeSaveandafterSave, 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 の通貨レートチェックプロセスをお客様の特定のビジネスニーズに合わせてスムーズに調整する方法を習得できます。

使用方法
システム設定において、「Is_CurrencyRate_Overlap」という名前の新しい設定を作成し、値を「N」に設定します。
「Y」を使用すると重複チェックが有効になります。「N」を使用すると重複チェックが無効になります。
図2に示すとおりです。

コーディング
手順:
- まず、
org.compiere.model.MConversionRateを継承するCustomMConversionRateという新しいクラスを作成します。 - 以下のコードスニペットに示すように、
beforeSaveとafterSaveの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
