diff --git a/org.adempiere.base/src/org/compiere/model/MInventory.java b/org.adempiere.base/src/org/compiere/model/MInventory.java index 4b7ebbd7c7..bd296dca52 100644 --- a/org.adempiere.base/src/org/compiere/model/MInventory.java +++ b/org.adempiere.base/src/org/compiere/model/MInventory.java @@ -722,6 +722,7 @@ public class MInventory extends X_M_Inventory implements DocAction if (line.getM_AttributeSetInstance_ID() == 0) { MProduct product = MProduct.get(getCtx(), line.getM_Product_ID(), get_TrxName()); + boolean serial = product.isSerial(); if (qtyDiff.signum() > 0) // Incoming Trx { //auto balance negative on hand @@ -729,6 +730,12 @@ public class MInventory extends X_M_Inventory implements DocAction null, MClient.MMPOLICY_FiFo.equals(product.getMMPolicy()), line.getM_Locator_ID(), get_TrxName(), false); for (MStorageOnHand storage : storages) { + if (storage.getM_AttributeSetInstance_ID() > 0 && serial) + { + MAttributeSetInstance asi = new MAttributeSetInstance(Env.getCtx(), storage.getM_AttributeSetInstance_ID(), get_TrxName()); + if (!Util.isEmpty(asi.getSerNo(), true)) + continue; + } if (storage.getQtyOnHand().signum() < 0) { BigDecimal maQty = qtyDiff; @@ -758,6 +765,17 @@ public class MInventory extends X_M_Inventory implements DocAction false, true, 0, get_TrxName()); for (MStorageOnHand storage : storages) { + if (storage.getM_AttributeSetInstance_ID() == 0) + continue; + + if (serial) + { + MAttributeSetInstance asi = new MAttributeSetInstance(Env.getCtx(), storage.getM_AttributeSetInstance_ID(), get_TrxName()); + if (!Util.isEmpty(asi.getSerNo(), true)) + { + continue; + } + } BigDecimal maQty = qtyDiff; //backward compatibility: -ve in MA is incoming trx, +ve in MA is outgoing trx MInventoryLineMA lineMA = new MInventoryLineMA(line, storage.getM_AttributeSetInstance_ID(), maQty.negate(), storage.getDateMaterialPolicy(),true); @@ -827,6 +845,12 @@ public class MInventory extends X_M_Inventory implements DocAction BigDecimal qtyToDeliver = qtyDiff.negate(); for (MStorageOnHand storage: storages) { + if (serial && storage.getM_AttributeSetInstance_ID() > 0) + { + MAttributeSetInstance asi = new MAttributeSetInstance(Env.getCtx(), storage.getM_AttributeSetInstance_ID(), get_TrxName()); + if (!Util.isEmpty(asi.getSerNo(), true)) + continue; + } if (storage.getQtyOnHand().compareTo(qtyToDeliver) >= 0) { MInventoryLineMA ma = new MInventoryLineMA (line, diff --git a/org.idempiere.test/src/org/idempiere/test/model/InventoryTest.java b/org.idempiere.test/src/org/idempiere/test/model/InventoryTest.java index 680db205ed..0caf944e8b 100644 --- a/org.idempiere.test/src/org/idempiere/test/model/InventoryTest.java +++ b/org.idempiere.test/src/org/idempiere/test/model/InventoryTest.java @@ -29,12 +29,15 @@ import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertNotEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; import java.math.BigDecimal; import java.sql.Timestamp; import java.util.Properties; import org.compiere.model.MAcctSchema; +import org.compiere.model.MAttributeSet; +import org.compiere.model.MAttributeSetInstance; import org.compiere.model.MBPartner; import org.compiere.model.MClient; import org.compiere.model.MCost; @@ -44,10 +47,16 @@ import org.compiere.model.MInventory; import org.compiere.model.MInventoryLine; import org.compiere.model.MOrder; import org.compiere.model.MOrderLine; +import org.compiere.model.MPriceList; +import org.compiere.model.MPriceListVersion; import org.compiere.model.MProduct; +import org.compiere.model.MProductPrice; +import org.compiere.model.MStorageOnHand; +import org.compiere.model.MWarehouse; import org.compiere.process.DocAction; import org.compiere.process.DocumentEngine; import org.compiere.process.ProcessInfo; +import org.compiere.util.CacheMgt; import org.compiere.util.Env; import org.compiere.util.TimeUtil; import org.compiere.wf.MWorkflow; @@ -74,6 +83,9 @@ public class InventoryTest extends AbstractTestCase { private static final int BP_PATIO = 121; private static final int USER_GARDENADMIN = 101; private static final int MULCH_PRODUCT_ID = 137; + private final static int FERTILIZER_LOT_ATTRIBUTESET_ID = 101; + private static final int CHEMICALS_CATEGORY_ID = 109; + private static final int PURCHASE_PRICE_LIST_ID = 102; /** * https://idempiere.atlassian.net/browse/IDEMPIERE-4596 @@ -142,6 +154,10 @@ public class InventoryTest extends AbstractTestCase { } private void createPOAndMRForProduct(int productId) { + createPOAndMRForProduct(productId, null); + } + + private void createPOAndMRForProduct(int productId, MAttributeSetInstance asi) { MOrder order = new MOrder(Env.getCtx(), 0, getTrxName()); order.setBPartner(MBPartner.get(Env.getCtx(), BP_PATIO)); order.setC_DocTypeTarget_ID(DOCTYPE_PO); @@ -156,7 +172,7 @@ public class InventoryTest extends AbstractTestCase { MOrderLine line1 = new MOrderLine(order); line1.setLine(10); - line1.setProduct(MProduct.get(Env.getCtx(), productId)); + line1.setProduct(new MProduct(Env.getCtx(), productId, getTrxName())); line1.setQty(new BigDecimal("1")); line1.setDatePromised(today); line1.saveEx(); @@ -174,6 +190,8 @@ public class InventoryTest extends AbstractTestCase { MInOutLine receiptLine1 = new MInOutLine(receipt1); receiptLine1.setOrderLine(line1, 0, new BigDecimal("1")); receiptLine1.setQty(new BigDecimal("1")); + if (asi != null) + receiptLine1.setM_AttributeSetInstance_ID(asi.get_ID()); receiptLine1.saveEx(); info = MWorkflow.runDocumentActionWorkflow(receipt1, DocAction.ACTION_Complete); @@ -185,4 +203,192 @@ public class InventoryTest extends AbstractTestCase { assertNull(error, error); } } + + @Test + public void testSkipProductWithSerial() { + Properties ctx = Env.getCtx(); + String trxName = getTrxName(); + + MAttributeSet set = new MAttributeSet(Env.getCtx(), FERTILIZER_LOT_ATTRIBUTESET_ID, null); + set.setIsSerNo(true); + set.saveEx(); + + MWarehouse wh = new MWarehouse(Env.getCtx(), WAREHOUSE_HQ, null); + boolean disallow = wh.isDisallowNegativeInv(); + MProduct product = null; + try { + + if (!disallow) { + wh.setIsDisallowNegativeInv(true); + wh.saveEx(); + CacheMgt.get().reset(MWarehouse.Table_Name, wh.get_ID()); + } + + product = new MProduct(ctx, 0, null); + product.setM_Product_Category_ID(CHEMICALS_CATEGORY_ID); + product.setName("testSkipProductWithSerial"); + product.setValue("testSkipProductWithSerial"); + product.setProductType(MProduct.PRODUCTTYPE_Item); + product.setIsStocked(true); + product.setIsSold(true); + product.setIsPurchased(true); + product.setC_UOM_ID(UOM_EACH); + product.setC_TaxCategory_ID(TAXCAT_STANDARD); + product.setM_AttributeSet_ID(FERTILIZER_LOT_ATTRIBUTESET_ID); + product.saveEx(); + + MPriceListVersion plv = MPriceList.get(PURCHASE_PRICE_LIST_ID).getPriceListVersion(null); + MProductPrice pp = new MProductPrice(Env.getCtx(), 0, getTrxName()); + pp.setM_PriceList_Version_ID(plv.getM_PriceList_Version_ID()); + pp.setM_Product_ID(product.get_ID()); + pp.setPriceStd(new BigDecimal("2")); + pp.setPriceList(new BigDecimal("2")); + pp.saveEx(); + + MAttributeSetInstance asi = new MAttributeSetInstance(Env.getCtx(), 0, getTrxName()); + asi.setM_AttributeSet_ID(FERTILIZER_LOT_ATTRIBUTESET_ID); + asi.setSerNo("testSkipProductWithSerial #1"); + asi.saveEx(); + + createPOAndMRForProduct(product.get_ID(), asi); + + MStorageOnHand[] onhands = MStorageOnHand.getOfProduct(Env.getCtx(), product.get_ID(), getTrxName()); + assertEquals(1, onhands.length, "Unexpected number of on hand records"); + assertEquals(onhands[0].getM_AttributeSetInstance_ID(), asi.get_ID(), "Unexpected M_AttributeSetInstance_ID for on hand record"); + + MInventory inventory = new MInventory(ctx, 0, trxName); + inventory.setM_Warehouse_ID(WAREHOUSE_HQ); + inventory.setC_DocType_ID(DOCTYPE_PHYSICAL_INV); + inventory.saveEx(); + + MInventoryLine iline = new MInventoryLine(inventory, + LOCATOR_HQ, + product.getM_Product_ID(), + 0, // M_AttributeSetInstance_ID + Env.ONE, // QtyBook + Env.ZERO); + iline.saveEx(); + + //show error out with negative on hand (skip the only asi record with serno) + ProcessInfo info = MWorkflow.runDocumentActionWorkflow(inventory, DocAction.ACTION_Complete); + assertTrue(info.isError(), info.getSummary()); + } finally { + rollback(); + set.setIsSerNo(false); + set.saveEx(); + + if (product != null) + product.deleteEx(true); + + if (!disallow) { + wh.setIsDisallowNegativeInv(false); + wh.saveEx(); + CacheMgt.get().reset(MWarehouse.Table_Name, wh.get_ID()); + } + } + } + + @Test + public void testSkipProductWithSerial2() { + Properties ctx = Env.getCtx(); + String trxName = getTrxName(); + + MAttributeSet set = new MAttributeSet(Env.getCtx(), FERTILIZER_LOT_ATTRIBUTESET_ID, null); + set.setIsSerNo(true); + set.saveEx(); + + MWarehouse wh = new MWarehouse(Env.getCtx(), WAREHOUSE_HQ, null); + boolean disallow = wh.isDisallowNegativeInv(); + MProduct product = null; + try { + + if (!disallow) { + wh.setIsDisallowNegativeInv(true); + wh.saveEx(); + CacheMgt.get().reset(MWarehouse.Table_Name, wh.get_ID()); + } + + product = new MProduct(ctx, 0, null); + product.setM_Product_Category_ID(CHEMICALS_CATEGORY_ID); + product.setName("testSkipProductWithSerial"); + product.setValue("testSkipProductWithSerial"); + product.setProductType(MProduct.PRODUCTTYPE_Item); + product.setIsStocked(true); + product.setIsSold(true); + product.setIsPurchased(true); + product.setC_UOM_ID(UOM_EACH); + product.setC_TaxCategory_ID(TAXCAT_STANDARD); + product.setM_AttributeSet_ID(FERTILIZER_LOT_ATTRIBUTESET_ID); + product.saveEx(); + + MPriceListVersion plv = MPriceList.get(PURCHASE_PRICE_LIST_ID).getPriceListVersion(null); + MProductPrice pp = new MProductPrice(Env.getCtx(), 0, getTrxName()); + pp.setM_PriceList_Version_ID(plv.getM_PriceList_Version_ID()); + pp.setM_Product_ID(product.get_ID()); + pp.setPriceStd(new BigDecimal("2")); + pp.setPriceList(new BigDecimal("2")); + pp.saveEx(); + + MAttributeSetInstance asi = new MAttributeSetInstance(Env.getCtx(), 0, getTrxName()); + asi.setM_AttributeSet_ID(FERTILIZER_LOT_ATTRIBUTESET_ID); + asi.setSerNo("testSkipProductWithSerial #1"); + asi.saveEx(); + + createPOAndMRForProduct(product.get_ID(), asi); + + MStorageOnHand[] onhands = MStorageOnHand.getOfProduct(Env.getCtx(), product.get_ID(), getTrxName()); + assertEquals(1, onhands.length, "Unexpected number of on hand records"); + assertEquals(onhands[0].getM_AttributeSetInstance_ID(), asi.get_ID(), "Unexpected M_AttributeSetInstance_ID for on hand record"); + + MAttributeSetInstance asi1 = new MAttributeSetInstance(Env.getCtx(), 0, getTrxName()); + asi1.setM_AttributeSet_ID(FERTILIZER_LOT_ATTRIBUTESET_ID); + asi1.saveEx(); + + createPOAndMRForProduct(product.get_ID(), asi1); + + onhands = MStorageOnHand.getOfProduct(Env.getCtx(), product.get_ID(), getTrxName()); + assertEquals(2, onhands.length, "Unexpected number of on hand records"); + assertEquals(onhands[0].getM_AttributeSetInstance_ID(), asi.get_ID(), "Unexpected M_AttributeSetInstance_ID for first on hand record"); + assertEquals(onhands[1].getM_AttributeSetInstance_ID(), asi1.get_ID(), "Unexpected M_AttributeSetInstance_ID for second on hand record"); + + MInventory inventory = new MInventory(ctx, 0, trxName); + inventory.setM_Warehouse_ID(WAREHOUSE_HQ); + inventory.setC_DocType_ID(DOCTYPE_PHYSICAL_INV); + inventory.saveEx(); + + MInventoryLine iline = new MInventoryLine(inventory, + LOCATOR_HQ, + product.getM_Product_ID(), + 0, // M_AttributeSetInstance_ID + new BigDecimal("2"), // QtyBook + new BigDecimal("1")); + iline.saveEx(); + + //should success with qty difference being applied to the asi with null serno record + ProcessInfo info = MWorkflow.runDocumentActionWorkflow(inventory, DocAction.ACTION_Complete); + assertFalse(info.isError(), info.getSummary()); + inventory.load(getTrxName()); + assertEquals(DocAction.STATUS_Completed, inventory.getDocStatus()); + + onhands = MStorageOnHand.getOfProduct(Env.getCtx(), product.get_ID(), getTrxName()); + assertEquals(2, onhands.length, "Unexpected number of on hand records"); + assertEquals(onhands[0].getM_AttributeSetInstance_ID(), asi.get_ID(), "Unexpected M_AttributeSetInstance_ID for first on hand record"); + assertEquals(onhands[1].getM_AttributeSetInstance_ID(), asi1.get_ID(), "Unexpected M_AttributeSetInstance_ID for second on hand record"); + assertEquals(1, onhands[0].getQtyOnHand().intValue()); + assertEquals(0, onhands[1].getQtyOnHand().intValue()); + } finally { + rollback(); + set.setIsSerNo(false); + set.saveEx(); + + if (product != null) + product.deleteEx(true); + + if (!disallow) { + wh.setIsDisallowNegativeInv(false); + wh.saveEx(); + CacheMgt.get().reset(MWarehouse.Table_Name, wh.get_ID()); + } + } + } }