diff --git a/migration/i8.2z/oracle/202112210900_IDEMPIERE-5118.sql b/migration/i8.2z/oracle/202112210900_IDEMPIERE-5118.sql
new file mode 100644
index 0000000000..0a4df3bb24
--- /dev/null
+++ b/migration/i8.2z/oracle/202112210900_IDEMPIERE-5118.sql
@@ -0,0 +1,23 @@
+SET SQLBLANKLINES ON
+SET DEFINE OFF
+
+-- IDEMPIERE-5118 MUOMConversion fix and improvements
+-- Dec 21, 2021, 3:31:26 PM MYT
+UPDATE AD_IndexColumn SET SeqNo=4,Updated=TO_DATE('2021-12-21 15:31:26','YYYY-MM-DD HH24:MI:SS'),UpdatedBy=100 WHERE AD_IndexColumn_ID=200656
+;
+
+-- Dec 21, 2021, 3:32:58 PM MYT
+INSERT INTO AD_IndexColumn (AD_Client_ID,AD_Org_ID,AD_IndexColumn_ID,AD_IndexColumn_UU,Created,CreatedBy,EntityType,IsActive,Updated,UpdatedBy,AD_Column_ID,AD_TableIndex_ID,SeqNo) VALUES (0,0,201444,'9c133f49-0a9e-42ce-a8da-86ed15f7e40a',TO_DATE('2021-12-21 15:32:57','YYYY-MM-DD HH24:MI:SS'),100,'D','Y',TO_DATE('2021-12-21 15:32:57','YYYY-MM-DD HH24:MI:SS'),100,1003,200562,3)
+;
+
+-- Dec 21, 2021, 3:33:18 PM MYT
+DROP INDEX c_uom_conversion_product
+;
+
+-- Dec 21, 2021, 3:33:19 PM MYT
+CREATE UNIQUE INDEX c_uom_conversion_product ON C_UOM_Conversion (C_UOM_ID,C_UOM_To_ID,AD_Client_ID,COALESCE(M_Product_ID,-1))
+;
+
+SELECT register_migration_script('202112210900_IDEMPIERE-5118.sql') FROM dual
+;
+
diff --git a/migration/i8.2z/postgresql/202112210900_IDEMPIERE-5118.sql b/migration/i8.2z/postgresql/202112210900_IDEMPIERE-5118.sql
new file mode 100644
index 0000000000..f6c5ea296a
--- /dev/null
+++ b/migration/i8.2z/postgresql/202112210900_IDEMPIERE-5118.sql
@@ -0,0 +1,20 @@
+-- IDEMPIERE-5118 MUOMConversion fix and improvements
+-- Dec 21, 2021, 3:31:26 PM MYT
+UPDATE AD_IndexColumn SET SeqNo=4,Updated=TO_TIMESTAMP('2021-12-21 15:31:26','YYYY-MM-DD HH24:MI:SS'),UpdatedBy=100 WHERE AD_IndexColumn_ID=200656
+;
+
+-- Dec 21, 2021, 3:32:58 PM MYT
+INSERT INTO AD_IndexColumn (AD_Client_ID,AD_Org_ID,AD_IndexColumn_ID,AD_IndexColumn_UU,Created,CreatedBy,EntityType,IsActive,Updated,UpdatedBy,AD_Column_ID,AD_TableIndex_ID,SeqNo) VALUES (0,0,201444,'9c133f49-0a9e-42ce-a8da-86ed15f7e40a',TO_TIMESTAMP('2021-12-21 15:32:57','YYYY-MM-DD HH24:MI:SS'),100,'D','Y',TO_TIMESTAMP('2021-12-21 15:32:57','YYYY-MM-DD HH24:MI:SS'),100,1003,200562,3)
+;
+
+-- Dec 21, 2021, 3:33:18 PM MYT
+DROP INDEX c_uom_conversion_product
+;
+
+-- Dec 21, 2021, 3:33:19 PM MYT
+CREATE UNIQUE INDEX c_uom_conversion_product ON C_UOM_Conversion (C_UOM_ID,C_UOM_To_ID,AD_Client_ID,COALESCE(M_Product_ID,-1))
+;
+
+SELECT register_migration_script('202112210900_IDEMPIERE-5118.sql') FROM dual
+;
+
diff --git a/org.adempiere.base/src/org/compiere/model/CalloutEngine.java b/org.adempiere.base/src/org/compiere/model/CalloutEngine.java
index 41086338d9..67ed57890f 100644
--- a/org.adempiere.base/src/org/compiere/model/CalloutEngine.java
+++ b/org.adempiere.base/src/org/compiere/model/CalloutEngine.java
@@ -18,7 +18,6 @@ package org.compiere.model;
import java.lang.reflect.Method;
import java.math.BigDecimal;
-import java.math.RoundingMode;
import java.sql.Timestamp;
import java.util.Properties;
import java.util.logging.Level;
@@ -316,7 +315,7 @@ public class CalloutEngine implements Callout
BigDecimal rate2 = Env.ZERO;
if (rate1.signum() != 0.0) // no divide by zero
- rate2 = Env.ONE.divide(rate1, 12, RoundingMode.HALF_UP);
+ rate2 = MUOMConversion.getOppositeRate(rate1);
//
if (mField.getColumnName().equals("MultiplyRate"))
mTab.setValue("DivideRate", rate2);
diff --git a/org.adempiere.base/src/org/compiere/model/MUOMConversion.java b/org.adempiere.base/src/org/compiere/model/MUOMConversion.java
index 1b2dbb4930..100b44c1fe 100644
--- a/org.adempiere.base/src/org/compiere/model/MUOMConversion.java
+++ b/org.adempiere.base/src/org/compiere/model/MUOMConversion.java
@@ -449,7 +449,6 @@ public class MUOMConversion extends X_C_UOM_Conversion implements ImmutablePOSup
return retValue;
} // convert
-
/**************************************************************************
* Convert PRICE expressed in entered UoM to equivalent price in product UoM and round.
* OR Convert QTY in product UOM to qty in entered UoM and round.
@@ -464,7 +463,27 @@ public class MUOMConversion extends X_C_UOM_Conversion implements ImmutablePOSup
* @return Product: Qty/Price (precision rounded)
*/
static public BigDecimal convertProductTo (Properties ctx,
- int M_Product_ID, int C_UOM_To_ID, BigDecimal qtyPrice)
+ int M_Product_ID, int C_UOM_To_ID, BigDecimal qtyPrice)
+ {
+ return convertProductTo(ctx, M_Product_ID, C_UOM_To_ID, qtyPrice, -1);
+ }
+
+ /**************************************************************************
+ * Convert PRICE expressed in entered UoM to equivalent price in product UoM and round.
+ * OR Convert QTY in product UOM to qty in entered UoM and round.
+ *
+ * eg: $6/6pk => $1/ea
+ * OR 6 X ea => 1 X 6pk
+ *
+ * @param ctx context
+ * @param M_Product_ID product
+ * @param C_UOM_To_ID entered UOM
+ * @param qtyPrice quantity or price
+ * @param precision Rounding precision, -1 to use precision from UOM
+ * @return Product: Qty/Price (precision rounded)
+ */
+ static public BigDecimal convertProductTo (Properties ctx,
+ int M_Product_ID, int C_UOM_To_ID, BigDecimal qtyPrice, int precision)
{
if (qtyPrice == null || qtyPrice.signum() == 0
|| M_Product_ID == 0 || C_UOM_To_ID == 0)
@@ -475,10 +494,17 @@ public class MUOMConversion extends X_C_UOM_Conversion implements ImmutablePOSup
{
if (Env.ONE.compareTo(retValue) == 0)
return qtyPrice;
- MUOM uom = MUOM.get (ctx, C_UOM_To_ID);
- if (uom != null)
- return uom.round(retValue.multiply(qtyPrice), true);
- return retValue.multiply(qtyPrice);
+ if (precision >= 0)
+ {
+ return retValue.multiply(qtyPrice).setScale(precision, RoundingMode.HALF_UP);
+ }
+ else
+ {
+ MUOM uom = MUOM.get (ctx, C_UOM_To_ID);
+ if (uom != null)
+ return uom.round(retValue.multiply(qtyPrice), true);
+ return retValue.multiply(qtyPrice);
+ }
}
return null;
} // convertProductTo
@@ -496,6 +522,8 @@ public class MUOMConversion extends X_C_UOM_Conversion implements ImmutablePOSup
{
if (M_Product_ID == 0)
return null;
+
+ //first check product specific conversion
MUOMConversion[] rates = getProductConversions(ctx, M_Product_ID);
for (int i = 0; i < rates.length; i++)
@@ -505,8 +533,10 @@ public class MUOMConversion extends X_C_UOM_Conversion implements ImmutablePOSup
return rate.getMultiplyRate();
}
- List conversions = new Query(ctx, Table_Name, "C_UOM_ID=? AND C_UOM_TO_ID=?", null)
- .setParameters(MProduct.get(ctx, M_Product_ID).getC_UOM_ID(), C_UOM_To_ID)
+ //fall back to generic conversion
+ List conversions = new Query(ctx, Table_Name, "C_UOM_ID=? AND C_UOM_TO_ID=? AND M_Product_ID IS NULL AND AD_Client_ID IN (0, ?)", null)
+ .setParameters(MProduct.get(ctx, M_Product_ID).getC_UOM_ID(), C_UOM_To_ID, Env.getAD_Client_ID(ctx))
+ .setOrderBy("AD_Client_ID Desc")
.setOnlyActiveRecords(true)
.list();
for (int i = 0; i < conversions.size(); i++)
@@ -532,7 +562,27 @@ public class MUOMConversion extends X_C_UOM_Conversion implements ImmutablePOSup
* @return Product: Qty/Price (precision rounded)
*/
static public BigDecimal convertProductFrom (Properties ctx,
- int M_Product_ID, int C_UOM_To_ID, BigDecimal qtyPrice)
+ int M_Product_ID, int C_UOM_To_ID, BigDecimal qtyPrice)
+ {
+ return convertProductFrom(ctx, M_Product_ID, C_UOM_To_ID, qtyPrice, -1);
+ }
+
+ /**************************************************************************
+ * Convert PRICE expressed in product UoM to equivalent price in entered UoM and round.
+ * OR Convert QTY in entered UOM to qty in product UoM and round.
+ *
+ * eg: $1/ea => $6/6pk
+ * OR 1 X 6pk => 6 X ea
+ *
+ * @param ctx context
+ * @param M_Product_ID product
+ * @param C_UOM_To_ID entered UOM
+ * @param qtyPrice quantity or price
+ * @param precision Rounding precision, -1 to use precision from UOM
+ * @return Product: Qty/Price (precision rounded)
+ */
+ static public BigDecimal convertProductFrom (Properties ctx,
+ int M_Product_ID, int C_UOM_To_ID, BigDecimal qtyPrice, int precision)
{
// No conversion
if (qtyPrice == null || qtyPrice.compareTo(Env.ZERO)==0
@@ -547,10 +597,17 @@ public class MUOMConversion extends X_C_UOM_Conversion implements ImmutablePOSup
{
if (Env.ONE.compareTo(retValue) == 0)
return qtyPrice;
- MUOM uom = MUOM.get (ctx, C_UOM_To_ID);
- if (uom != null)
- return uom.round(retValue.multiply(qtyPrice), true);
- return retValue.multiply(qtyPrice);
+ if (precision >= 0)
+ {
+ return retValue.multiply(qtyPrice).setScale(precision, RoundingMode.HALF_UP);
+ }
+ else
+ {
+ MUOM uom = MUOM.get (ctx, C_UOM_To_ID);
+ if (uom != null)
+ return uom.round(retValue.multiply(qtyPrice), true);
+ return retValue.multiply(qtyPrice);
+ }
}
if (s_log.isLoggable(Level.FINE)) s_log.fine("No Rate M_Product_ID=" + M_Product_ID);
return null;
@@ -567,6 +624,10 @@ public class MUOMConversion extends X_C_UOM_Conversion implements ImmutablePOSup
static public BigDecimal getProductRateFrom (Properties ctx,
int M_Product_ID, int C_UOM_To_ID)
{
+ if (M_Product_ID == 0)
+ return null;
+
+ //first, check product specific conversion
MUOMConversion[] rates = getProductConversions(ctx, M_Product_ID);
for (int i = 0; i < rates.length; i++)
@@ -576,8 +637,10 @@ public class MUOMConversion extends X_C_UOM_Conversion implements ImmutablePOSup
return rate.getDivideRate();
}
- List conversions = new Query(ctx, Table_Name, "C_UOM_ID=? AND C_UOM_TO_ID=?", null)
- .setParameters(MProduct.get(ctx, M_Product_ID).getC_UOM_ID(), C_UOM_To_ID)
+ //fall back to generic conversion
+ List conversions = new Query(ctx, Table_Name, "C_UOM_ID=? AND C_UOM_TO_ID=? AND M_Product_ID IS NULL AND AD_Client_ID IN (0, ?)", null)
+ .setParameters(MProduct.get(ctx, M_Product_ID).getC_UOM_ID(), C_UOM_To_ID, Env.getAD_Client_ID(ctx))
+ .setOrderBy("AD_Client_ID Desc")
.setOnlyActiveRecords(true)
.list();
for (int i = 0; i < conversions.size(); i++)
@@ -746,6 +809,18 @@ public class MUOMConversion extends X_C_UOM_Conversion implements ImmutablePOSup
log.saveError("Error", Msg.parseTranslation(getCtx(), "@C_UOM_ID@ = @C_UOM_To_ID@"));
return false;
}
+
+ if (getMultiplyRate() != null && getMultiplyRate().signum() != 0)
+ {
+ if (getDivideRate() == null || getDivideRate().signum() == 0)
+ setDivideRate(getOppositeRate(getMultiplyRate()));
+ }
+ else if (getDivideRate() != null && getDivideRate().signum() != 0)
+ {
+ if (getMultiplyRate() == null || getMultiplyRate().signum() == 0)
+ setMultiplyRate(getOppositeRate(getDivideRate()));
+ }
+
// Nothing to convert
if (getMultiplyRate().compareTo(Env.ZERO) <= 0)
{
@@ -807,4 +882,12 @@ public class MUOMConversion extends X_C_UOM_Conversion implements ImmutablePOSup
return this;
}
+ /**
+ * Calculate opposite conversion rate, i.e calculate divide rate for multiply rate and vice versa.
+ * @param rate
+ * @return {@link BigDecimal}
+ */
+ public static BigDecimal getOppositeRate(BigDecimal rate) {
+ return Env.ONE.divide(rate, 12, RoundingMode.HALF_UP);
+ }
} // UOMConversion
diff --git a/org.idempiere.test/src/org/idempiere/test/model/MUOMConversionTest.java b/org.idempiere.test/src/org/idempiere/test/model/MUOMConversionTest.java
new file mode 100644
index 0000000000..07930d41f9
--- /dev/null
+++ b/org.idempiere.test/src/org/idempiere/test/model/MUOMConversionTest.java
@@ -0,0 +1,139 @@
+/***********************************************************************
+ * This file is part of iDempiere ERP Open Source *
+ * http://www.idempiere.org *
+ * *
+ * Copyright (C) Contributors *
+ * *
+ * This program is free software; you can redistribute it and/or *
+ * modify it under the terms of the GNU General Public License *
+ * as published by the Free Software Foundation; either version 2 *
+ * of the License, or (at your option) any later version. *
+ * *
+ * This program is distributed in the hope that it will be useful, *
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of *
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
+ * GNU General Public License for more details. *
+ * *
+ * You should have received a copy of the GNU General Public License *
+ * along with this program; if not, write to the Free Software *
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, *
+ * MA 02110-1301, USA. *
+ * *
+ * Contributors: *
+ * - hengsin *
+ **********************************************************************/
+package org.idempiere.test.model;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+import java.math.BigDecimal;
+import java.math.RoundingMode;
+
+import org.compiere.model.MUOM;
+import org.compiere.model.MUOMConversion;
+import org.compiere.model.PO;
+import org.compiere.util.CacheMgt;
+import org.compiere.util.DB;
+import org.compiere.util.Env;
+import org.idempiere.test.AbstractTestCase;
+import org.junit.jupiter.api.Test;
+
+/**
+ *
+ * @author hengsin
+ *
+ */
+public class MUOMConversionTest extends AbstractTestCase {
+
+ private final static int EACH_ID = 100;
+ private final static int HOUR_ID = 101;
+ private static final int PRODUCT_OAK_TREE = 123;
+
+ public MUOMConversionTest() {
+ }
+
+ @Test
+ public void testConversion() {
+
+ MUOM each = new MUOM(Env.getCtx(), EACH_ID, getTrxName());
+ MUOM hour = new MUOM(Env.getCtx(), HOUR_ID, getTrxName());
+
+ //conversion1 at system level
+ MUOMConversion conv1 = new MUOMConversion(each);
+ conv1.set_TrxName(null);
+ conv1.setC_UOM_To_ID(HOUR_ID);
+ conv1.setMultiplyRate(new BigDecimal("1.15"));
+ conv1.setDivideRate(BigDecimal.ZERO);
+ try {
+ PO.setCrossTenantSafe();
+ conv1.saveEx();
+ } finally {
+ PO.clearCrossTenantSafe();
+ }
+
+ MUOMConversion conv2 = null;
+ MUOMConversion conv3 = null;
+ try {
+ BigDecimal converted = MUOMConversion.convertProductTo(Env.getCtx(), PRODUCT_OAK_TREE, HOUR_ID, new BigDecimal("1"));
+ assertEquals(new BigDecimal("1.15"), converted);
+ converted = MUOMConversion.convertProductTo(Env.getCtx(), PRODUCT_OAK_TREE, HOUR_ID, new BigDecimal("1"), -1);
+ assertEquals(new BigDecimal("1.15"), converted);
+ converted = MUOMConversion.convertProductTo(Env.getCtx(), PRODUCT_OAK_TREE, HOUR_ID, new BigDecimal("1"), 1);
+ assertEquals(new BigDecimal("1.2"), converted);
+
+ //conversion2 at tenant level
+ conv2 = new MUOMConversion(Env.getCtx(), 0, null);
+ conv2.setC_UOM_ID(EACH_ID);
+ conv2.setC_UOM_To_ID(HOUR_ID);
+ conv2.setMultiplyRate(new BigDecimal("1.35"));
+ conv2.saveEx();
+
+ converted = MUOMConversion.convertProductTo(Env.getCtx(), PRODUCT_OAK_TREE, HOUR_ID, new BigDecimal("1"));
+ assertEquals(new BigDecimal("1.35"), converted);
+ converted = MUOMConversion.convertProductTo(Env.getCtx(), PRODUCT_OAK_TREE, HOUR_ID, new BigDecimal("1"), -1);
+ assertEquals(new BigDecimal("1.35"), converted);
+ converted = MUOMConversion.convertProductTo(Env.getCtx(), PRODUCT_OAK_TREE, HOUR_ID, new BigDecimal("1"), 1);
+ assertEquals(new BigDecimal("1.4"), converted);
+
+ //conversion3 at tenant and product level
+ conv3 = new MUOMConversion(Env.getCtx(), 0, null);
+ conv3.setM_Product_ID(PRODUCT_OAK_TREE);
+ conv3.setC_UOM_ID(EACH_ID);
+ conv3.setC_UOM_To_ID(HOUR_ID);
+ conv3.setMultiplyRate(new BigDecimal("0.75"));
+ conv3.saveEx();
+ CacheMgt.get().reset();
+
+ converted = MUOMConversion.convertProductTo(Env.getCtx(), PRODUCT_OAK_TREE, HOUR_ID, new BigDecimal("1"));
+ assertEquals(new BigDecimal("0.75"), converted);
+ converted = MUOMConversion.convertProductTo(Env.getCtx(), PRODUCT_OAK_TREE, HOUR_ID, new BigDecimal("1"), -1);
+ assertEquals(new BigDecimal("0.75"), converted);
+ converted = MUOMConversion.convertProductTo(Env.getCtx(), PRODUCT_OAK_TREE, HOUR_ID, new BigDecimal("1"), 1);
+ assertEquals(new BigDecimal("0.8"), converted);
+
+ converted = MUOMConversion.convertProductFrom(Env.getCtx(), PRODUCT_OAK_TREE, HOUR_ID, new BigDecimal("1"));
+ assertEquals(hour.round(conv3.getDivideRate(),true), converted);
+
+ conv3.deleteEx(true);
+ conv3 = null;
+ CacheMgt.get().reset();
+ converted = MUOMConversion.convertProductFrom(Env.getCtx(), PRODUCT_OAK_TREE, HOUR_ID, new BigDecimal("1"));
+ assertEquals(hour.round(conv2.getDivideRate(),true), converted);
+ converted = MUOMConversion.convertProductFrom(Env.getCtx(), PRODUCT_OAK_TREE, HOUR_ID, new BigDecimal("1"), 1);
+ assertEquals(conv2.getDivideRate().setScale(1, RoundingMode.HALF_UP), converted);
+
+ conv2.deleteEx(true);
+ conv2 = null;
+ converted = MUOMConversion.convertProductFrom(Env.getCtx(), PRODUCT_OAK_TREE, HOUR_ID, new BigDecimal("1"));
+ assertEquals(hour.round(conv1.getDivideRate(),true), converted);
+ converted = MUOMConversion.convertProductFrom(Env.getCtx(), PRODUCT_OAK_TREE, HOUR_ID, new BigDecimal("1"), 1);
+ assertEquals(conv1.getDivideRate().setScale(1, RoundingMode.HALF_UP), converted);
+ } finally {
+ DB.executeUpdateEx("DELETE FROM C_UOM_Conversion WHERE C_UOM_Conversion_ID=?", new Object[] {conv1.get_ID()}, null);
+ if (conv2 != null)
+ conv2.deleteEx(true);
+ if (conv3 != null)
+ conv3.deleteEx(true);
+ }
+ }
+}