diff --git a/migration/i9/oracle/202201040800_IDEMPIERE-5139.sql b/migration/i9/oracle/202201040800_IDEMPIERE-5139.sql new file mode 100644 index 0000000000..a3dc2efce2 --- /dev/null +++ b/migration/i9/oracle/202201040800_IDEMPIERE-5139.sql @@ -0,0 +1,11 @@ +SET SQLBLANKLINES ON +SET DEFINE OFF + +-- IDEMPIERE-5139 BOM validation for Deactivation of Product +-- Jan 4, 2022, 3:43:47 PM MYT +INSERT INTO AD_Message (MsgType,MsgText,AD_Client_ID,AD_Org_ID,IsActive,Created,CreatedBy,Updated,UpdatedBy,AD_Message_ID,Value,EntityType,AD_Message_UU) VALUES ('E','This product is being used as a component in an active BOM. Please remove from BOM or inactivate BOM before marking this product as inactive.',0,0,'Y',TO_DATE('2022-01-04 15:43:42','YYYY-MM-DD HH24:MI:SS'),100,TO_DATE('2022-01-04 15:43:42','YYYY-MM-DD HH24:MI:SS'),100,200724,'DeActivateProductInActiveBOM','D','748a4e5b-95ec-4106-a949-ab700376a4a1') +; + +SELECT register_migration_script('202201040800_IDEMPIERE-5139.sql') FROM dual +; + diff --git a/migration/i9/postgresql/202201040800_IDEMPIERE-5139.sql b/migration/i9/postgresql/202201040800_IDEMPIERE-5139.sql new file mode 100644 index 0000000000..a393a52b70 --- /dev/null +++ b/migration/i9/postgresql/202201040800_IDEMPIERE-5139.sql @@ -0,0 +1,8 @@ +-- IDEMPIERE-5139 BOM validation for Deactivation of Product +-- Jan 4, 2022, 3:43:47 PM MYT +INSERT INTO AD_Message (MsgType,MsgText,AD_Client_ID,AD_Org_ID,IsActive,Created,CreatedBy,Updated,UpdatedBy,AD_Message_ID,Value,EntityType,AD_Message_UU) VALUES ('E','This product is being used as a component in an active BOM. Please remove from BOM or inactivate BOM before marking this product as inactive.',0,0,'Y',TO_TIMESTAMP('2022-01-04 15:43:42','YYYY-MM-DD HH24:MI:SS'),100,TO_TIMESTAMP('2022-01-04 15:43:42','YYYY-MM-DD HH24:MI:SS'),100,200724,'DeActivateProductInActiveBOM','D','748a4e5b-95ec-4106-a949-ab700376a4a1') +; + +SELECT register_migration_script('202201040800_IDEMPIERE-5139.sql') FROM dual +; + diff --git a/org.adempiere.base/src/org/compiere/model/MProduct.java b/org.adempiere.base/src/org/compiere/model/MProduct.java index 5bf475491d..0cba0402d6 100644 --- a/org.adempiere.base/src/org/compiere/model/MProduct.java +++ b/org.adempiere.base/src/org/compiere/model/MProduct.java @@ -28,6 +28,8 @@ import org.compiere.util.DB; import org.compiere.util.Env; import org.compiere.util.Msg; import org.compiere.util.Util; +import org.eevolution.model.MPPProductBOM; +import org.eevolution.model.MPPProductBOMLine; import org.idempiere.cache.ImmutableIntPOCache; import org.idempiere.cache.ImmutablePOSupport; @@ -653,6 +655,17 @@ public class MProduct extends X_M_Product implements ImmutablePOSupport } removeStorageRecords(); + + // check bom + if (is_ValueChanged("IsActive") && !isActive()) + { + errMsg = verifyBOM(); + if (! Util.isEmpty(errMsg)) + { + log.saveError("Error", errMsg); + return false; + } + } } // storage // it checks if UOM has been changed , if so disallow the change if the condition is true. @@ -753,6 +766,31 @@ public class MProduct extends X_M_Product implements ImmutablePOSupport } } + private String verifyBOM() { + Query query = new Query(getCtx(), MPPProductBOMLine.Table_Name, MPPProductBOMLine.COLUMNNAME_M_Product_ID+"=?", get_TrxName()); + List list = query.setOnlyActiveRecords(true) + .setClient_ID() + .setParameters(getM_Product_ID()) + .list(); + for(MPPProductBOMLine line : list) { + MPPProductBOM bom = line.getParent(); + if (bom.isActive()) { + StringBuilder errMsg = new StringBuilder(); + errMsg.append(Msg.getMsg(Env.getCtx(), "DeActivateProductInActiveBOM")); + String bomName = bom.getName(); + errMsg.append(" (BOM: ") + .append(bomName); + String parentValue = MProduct.get(bom.getM_Product_ID()).getValue(); + if (!parentValue.equals(bomName)) + errMsg.append(", ").append(parentValue); + errMsg.append(")"); + return errMsg.toString(); + } + } + + return null; + } + /** * HasInventoryOrCost * @return true if it has Inventory or Cost @@ -822,6 +860,25 @@ public class MProduct extends X_M_Product implements ImmutablePOSupport if (newRecord || is_ValueChanged("M_Product_Category_ID")) MCost.create(this); + + if (!newRecord && success && is_ValueChanged(COLUMNNAME_IsActive)) + { + if (!isActive() && isBOM()) + { + StringBuilder where = new StringBuilder(); + where.append("AD_Client_ID=? ") + .append("AND M_Product_ID=? ") + .append("AND IsActive='Y'"); + Query query = new Query(Env.getCtx(), MPPProductBOM.Table_Name, where.toString(), get_TrxName()); + List boms = query.setParameters(getAD_Client_ID(), getM_Product_ID()).list(); + for(MPPProductBOM bom : boms) + { + bom.setIsActive(false); + bom.saveEx(); + } + } + } + return success; } // afterSave diff --git a/org.idempiere.test/src/org/idempiere/test/model/MProductTest.java b/org.idempiere.test/src/org/idempiere/test/model/MProductTest.java index 59acc4165c..e64b715134 100644 --- a/org.idempiere.test/src/org/idempiere/test/model/MProductTest.java +++ b/org.idempiere.test/src/org/idempiere/test/model/MProductTest.java @@ -58,6 +58,8 @@ import org.compiere.util.DB; import org.compiere.util.Env; import org.compiere.util.TimeUtil; import org.compiere.wf.MWorkflow; +import org.eevolution.model.MPPProductBOM; +import org.eevolution.model.MPPProductBOMLine; import org.idempiere.test.AbstractTestCase; import org.junit.jupiter.api.Test; @@ -241,4 +243,20 @@ public class MProductTest extends AbstractTestCase { count = query.setParameters(product.get_ID()).count(); assertEquals(0, count, "Storage Reservation Record > 0"); } + + @Test + public void testDeactivateProductBOMValidation() { + Query query = new Query(Env.getCtx(), MPPProductBOM.Table_Name, MPPProductBOM.COLUMNNAME_PP_Product_BOM_ID+"<1000000", getTrxName()); + MPPProductBOM bom = query.setClient_ID().setOnlyActiveRecords(true).first(); + MPPProductBOMLine[] lines = bom.getLines(); + final MProduct product = new MProduct(Env.getCtx(), lines[0].getM_Product_ID(), getTrxName()); + product.setIsActive(false); + assertThrows(AdempiereException.class, () -> product.saveEx(), "No exception throw for deactivation of product in active BOM"); + + MProduct parent = new MProduct(Env.getCtx(), bom.getM_Product_ID(), getTrxName()); + parent.setIsActive(false); + parent.saveEx(); + bom.load(getTrxName()); + assertFalse(bom.isActive(), "BOM not auto deactivated after deactivation of parent product"); + } }