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); + } + } +}