diff --git a/org.adempiere.base/src/org/compiere/model/MInOut.java b/org.adempiere.base/src/org/compiere/model/MInOut.java index 32c6882672..807587b4c9 100644 --- a/org.adempiere.base/src/org/compiere/model/MInOut.java +++ b/org.adempiere.base/src/org/compiere/model/MInOut.java @@ -1441,29 +1441,59 @@ public class MInOut extends X_M_InOut implements DocAction if (mtrx == null) { Timestamp dateMPolicy= null; - MStorageOnHand[] storages = MStorageOnHand.getWarehouse(getCtx(), 0, - sLine.getM_Product_ID(), sLine.getM_AttributeSetInstance_ID(), null, - MClient.MMPOLICY_FiFo.equals(product.getMMPolicy()), false, - sLine.getM_Locator_ID(), get_TrxName()); - for (MStorageOnHand storage : storages) { - if (storage.getQtyOnHand().compareTo(sLine.getMovementQty()) >= 0) { - dateMPolicy = storage.getDateMaterialPolicy(); - break; + BigDecimal pendingQty = Qty; + if (pendingQty.signum() < 0) { // taking from inventory + MStorageOnHand[] storages = MStorageOnHand.getWarehouse(getCtx(), 0, + sLine.getM_Product_ID(), sLine.getM_AttributeSetInstance_ID(), null, + MClient.MMPOLICY_FiFo.equals(product.getMMPolicy()), false, + sLine.getM_Locator_ID(), get_TrxName()); + for (MStorageOnHand storage : storages) { + if (pendingQty.signum() == 0) + break; + if (storage.getQtyOnHand().compareTo(pendingQty.negate()) >= 0) { + dateMPolicy = storage.getDateMaterialPolicy(); + break; + } else if (storage.getQtyOnHand().signum() > 0) { + BigDecimal onHand = storage.getQtyOnHand(); + // this locator has less qty than required, ship all qtyonhand and iterate to next locator + if (!MStorageOnHand.add(getCtx(), getM_Warehouse_ID(), + sLine.getM_Locator_ID(), + sLine.getM_Product_ID(), + sLine.getM_AttributeSetInstance_ID(), + onHand.negate(),storage.getDateMaterialPolicy(),get_TrxName())) + { + String lastError = CLogger.retrieveErrorString(""); + m_processMsg = "Cannot correct Inventory OnHand [" + product.getValue() + "] - " + lastError; + return DocAction.STATUS_Invalid; + } + pendingQty = pendingQty.add(onHand); + } } + + if (dateMPolicy == null && storages.length > 0) + dateMPolicy = storages[0].getDateMaterialPolicy(); } - if (dateMPolicy == null && storages.length > 0) - dateMPolicy = storages[0].getDateMaterialPolicy(); - - if(dateMPolicy==null) + if (dateMPolicy == null && product.getM_AttributeSet_ID() > 0) { + MAttributeSet as = MAttributeSet.get(getCtx(), product.getM_AttributeSet_ID()); + if (as.isUseGuaranteeDateForMPolicy()) { + MAttributeSetInstance asi = new MAttributeSetInstance(getCtx(), sLine.getM_AttributeSetInstance_ID(), get_TrxName()); + if (asi != null && asi.getGuaranteeDate() != null) { + dateMPolicy = asi.getGuaranteeDate(); + } + } + } + + if (dateMPolicy == null) dateMPolicy = getMovementDate(); - + // Fallback: Update Storage - see also VMatch.createMatchRecord - if (!MStorageOnHand.add(getCtx(), getM_Warehouse_ID(), + if (pendingQty.signum() != 0 && + !MStorageOnHand.add(getCtx(), getM_Warehouse_ID(), sLine.getM_Locator_ID(), sLine.getM_Product_ID(), sLine.getM_AttributeSetInstance_ID(), - Qty,dateMPolicy,get_TrxName())) + pendingQty,dateMPolicy,get_TrxName())) { String lastError = CLogger.retrieveErrorString(""); m_processMsg = "Cannot correct Inventory OnHand [" + product.getValue() + "] - " + lastError; diff --git a/org.adempiere.ui.zk/WEB-INF/src/org/adempiere/webui/window/WPAttributeInstance.java b/org.adempiere.ui.zk/WEB-INF/src/org/adempiere/webui/window/WPAttributeInstance.java index a3bf09304a..d6d6356838 100644 --- a/org.adempiere.ui.zk/WEB-INF/src/org/adempiere/webui/window/WPAttributeInstance.java +++ b/org.adempiere.ui.zk/WEB-INF/src/org/adempiere/webui/window/WPAttributeInstance.java @@ -179,6 +179,7 @@ public class WPAttributeInstance extends Window implements EventListener new ColumnInfo(Msg.translate(Env.getCtx(), "Lot"), "asi.Lot", String.class), new ColumnInfo(Msg.translate(Env.getCtx(), "SerNo"), "asi.SerNo", String.class), new ColumnInfo(Msg.translate(Env.getCtx(), "GuaranteeDate"), "asi.GuaranteeDate", Timestamp.class), + new ColumnInfo(Msg.translate(Env.getCtx(), "DateMaterialPolicy"), "s.DateMaterialPolicy", Timestamp.class), new ColumnInfo(Msg.translate(Env.getCtx(), "M_Locator_ID"), "l.Value", KeyNamePair.class, "s.M_Locator_ID"), new ColumnInfo(Msg.translate(Env.getCtx(), "QtyOnHand"), "s.QtyOnHand", Double.class), new ColumnInfo(Msg.translate(Env.getCtx(), "QtyReserved"), "s.QtyReserved", Double.class), @@ -255,7 +256,7 @@ public class WPAttributeInstance extends Window implements EventListener m_sql = m_table.prepareTable (s_layout, s_sqlFrom, m_M_Warehouse_ID == 0 ? s_sqlWhereWithoutWarehouse : s_sqlWhere, false, "s") - + " ORDER BY asi.GuaranteeDate, s.QtyOnHand"; // oldest, smallest first + + " ORDER BY s.DateMaterialPolicy, s.QtyOnHand"; // oldest, smallest first // m_table.addEventListener(Events.ON_SELECT, this); // diff --git a/org.idempiere.test/src/org/idempiere/test/model/PurchaseOrderTest.java b/org.idempiere.test/src/org/idempiere/test/model/PurchaseOrderTest.java index f02c69e418..01fd376ee3 100644 --- a/org.idempiere.test/src/org/idempiere/test/model/PurchaseOrderTest.java +++ b/org.idempiere.test/src/org/idempiere/test/model/PurchaseOrderTest.java @@ -31,7 +31,9 @@ import java.math.BigDecimal; import java.sql.Timestamp; import java.util.Properties; +import org.compiere.model.MAttributeSetInstance; import org.compiere.model.MBPartner; +import org.compiere.model.MClient; import org.compiere.model.MInOut; import org.compiere.model.MInOutLine; import org.compiere.model.MInvoice; @@ -39,6 +41,7 @@ import org.compiere.model.MInvoiceLine; import org.compiere.model.MOrder; import org.compiere.model.MOrderLine; import org.compiere.model.MProduct; +import org.compiere.model.MStorageOnHand; import org.compiere.model.MStorageReservation; import org.compiere.process.DocAction; import org.compiere.process.ProcessInfo; @@ -60,13 +63,17 @@ public class PurchaseOrderTest extends AbstractTestCase { private static final int DOCTYPE_PO = 126; private static final int DOCTYPE_RECEIPT = 122; private static final int DOCTYPE_AP_INVOICE = 123; + private static final int PRODUCT_FERT50 = 136; + private static final int PRODUCT_MULCH = 137; private static final int PRODUCT_SEEDER = 143; private static final int PRODUCT_WEEDER = 141; - private static final int PRODUCT_MULCH = 137; private static final int USER_GARDENADMIN = 101; private static final BigDecimal THREE = new BigDecimal("3"); private static final BigDecimal MINUS_THREE = new BigDecimal("-3"); + private static final int ORG_FERTILIZER = 50001; + private static final int WAREHOUSE_FERTILIZER = 50002; + /** * https://idempiere.atlassian.net/browse/IDEMPIERE-4575 */ @@ -316,4 +323,100 @@ public class PurchaseOrderTest extends AbstractTestCase { return qtyOrdered; } + @Test + /** + * https://idempiere.atlassian.net/browse/IDEMPIERE-4768 + */ + public void testMultiDateMaterialReceipt() { + Properties ctx = Env.getCtx(); + String trxName = getTrxName(); + MProduct fert50 = new MProduct(ctx, PRODUCT_FERT50, trxName); + + Timestamp today = TimeUtil.getDay(System.currentTimeMillis()); + Timestamp past_month = TimeUtil.addMonths(today, -1); + + // create an ASI for Fertilizer Lot with Lot 2020 + MAttributeSetInstance asi = new MAttributeSetInstance(ctx, 0, trxName); + asi.setM_AttributeSet_ID(fert50.getM_AttributeSet_ID()); + asi.setLot("2020"); + asi.saveEx(); + + MOrder order = new MOrder(ctx, 0, trxName); + order.setAD_Org_ID(ORG_FERTILIZER); + order.setBPartner(MBPartner.get(ctx, BP_PATIO)); + order.setIsSOTrx(false); + order.setC_DocTypeTarget_ID(MOrder.DocSubTypeSO_Warehouse); + // ?? why setC_DocTypeTarget_ID sets back IsSOTrx=true + order.setIsSOTrx(false); + order.setM_Warehouse_ID(WAREHOUSE_FERTILIZER); + order.setDocStatus(DocAction.STATUS_Drafted); + order.setDocAction(DocAction.ACTION_Complete); + order.setPaymentRule(MOrder.PAYMENTRULE_OnCredit); // this is the default, just making it explicit + order.setDateOrdered(past_month); + order.saveEx(); + + MOrderLine line1 = new MOrderLine(order); + line1.setLine(10); + line1.setProduct(MProduct.get(ctx, PRODUCT_FERT50)); + line1.setM_AttributeSetInstance_ID(asi.getM_AttributeSetInstance_ID()); + line1.setQty(new BigDecimal("1")); + line1.setDatePromised(past_month); + line1.saveEx(); + + ProcessInfo info = MWorkflow.runDocumentActionWorkflow(order, DocAction.ACTION_Complete); + assertFalse(info.isError(), info.getSummary()); + order.load(trxName); + assertEquals(DocAction.STATUS_Completed, order.getDocStatus()); + line1.load(trxName); + assertEquals(0, line1.getQtyReserved().intValue()); + assertEquals(1, line1.getQtyDelivered().intValue()); + assertEquals(0, line1.getQtyInvoiced().intValue()); + + MOrder order2 = new MOrder(ctx, 0, trxName); + order2.setAD_Org_ID(ORG_FERTILIZER); + order2.setBPartner(MBPartner.get(ctx, BP_PATIO)); + order2.setIsSOTrx(false); + order2.setC_DocTypeTarget_ID(MOrder.DocSubTypeSO_Warehouse); + // ?? why setC_DocTypeTarget_ID sets back IsSOTrx=true + order2.setIsSOTrx(false); + order2.setM_Warehouse_ID(WAREHOUSE_FERTILIZER); + order2.setDocStatus(DocAction.STATUS_Drafted); + order2.setDocAction(DocAction.ACTION_Complete); + order2.setPaymentRule(MOrder.PAYMENTRULE_OnCredit); // this is the default, just making it explicit + order2.setDateOrdered(today); + order2.saveEx(); + + MOrderLine line2 = new MOrderLine(order2); + line2.setLine(10); + line2.setProduct(MProduct.get(ctx, PRODUCT_FERT50)); + line2.setM_AttributeSetInstance_ID(asi.getM_AttributeSetInstance_ID()); + line2.setQty(new BigDecimal("1")); + line2.setDatePromised(today); + line2.saveEx(); + + ProcessInfo info2 = MWorkflow.runDocumentActionWorkflow(order2, DocAction.ACTION_Complete); + assertFalse(info2.isError(), info2.getSummary()); + order2.load(trxName); + assertEquals(DocAction.STATUS_Completed, order2.getDocStatus()); + line2.load(trxName); + assertEquals(0, line2.getQtyReserved().intValue()); + assertEquals(1, line2.getQtyDelivered().intValue()); + assertEquals(0, line2.getQtyInvoiced().intValue()); + + // Expected to create two entries in storage because of the different dates + MStorageOnHand[] storages = MStorageOnHand.getWarehouse(ctx, WAREHOUSE_FERTILIZER, + PRODUCT_FERT50, asi.getM_AttributeSetInstance_ID(), null, + MClient.MMPOLICY_FiFo.equals(fert50.getMMPolicy()), false, + 0, trxName); + assertEquals(2, storages.length); + for (int i = 0; i < storages.length; i++) { + MStorageOnHand storage = storages[i]; + assertEquals(1, storage.getQtyOnHand().intValue()); + if (i == 0) + assertEquals(past_month, storage.getDateMaterialPolicy()); + else + assertEquals(today, storage.getDateMaterialPolicy()); + } + } + } diff --git a/org.idempiere.test/src/org/idempiere/test/model/SalesOrderTest.java b/org.idempiere.test/src/org/idempiere/test/model/SalesOrderTest.java index d310f3edfd..20a74bd414 100644 --- a/org.idempiere.test/src/org/idempiere/test/model/SalesOrderTest.java +++ b/org.idempiere.test/src/org/idempiere/test/model/SalesOrderTest.java @@ -34,7 +34,9 @@ import java.sql.Timestamp; import java.util.Properties; import org.compiere.model.MAllocationHdr; +import org.compiere.model.MAttributeSetInstance; import org.compiere.model.MBPartner; +import org.compiere.model.MClient; import org.compiere.model.MInOut; import org.compiere.model.MInOutLine; import org.compiere.model.MInvoice; @@ -44,10 +46,13 @@ import org.compiere.model.MPInstance; import org.compiere.model.MPInstancePara; import org.compiere.model.MPayment; import org.compiere.model.MProduct; +import org.compiere.model.MStorageOnHand; +import org.compiere.model.MWarehouse; import org.compiere.model.SystemIDs; import org.compiere.process.DocAction; import org.compiere.process.ProcessInfo; import org.compiere.process.ServerProcessCtl; +import org.compiere.util.CacheMgt; import org.compiere.util.DB; import org.compiere.util.Env; import org.compiere.util.TimeUtil; @@ -66,6 +71,10 @@ public class SalesOrderTest extends AbstractTestCase { private final static int BP_JOE_BLOCK = 118; private static final int PRODUCT_OAK_TREE = 123; private static final int PRODUCT_AZALEA = 128; + private static final int PRODUCT_FERT50 = 136; + private static final int ORG_FERTILIZER = 50001; + private static final int WAREHOUSE_FERTILIZER = 50002; + private static final int LOCATOR_FERTILIZER = 50001; @Test /** @@ -643,4 +652,86 @@ public class SalesOrderTest extends AbstractTestCase { assertEquals(0, line1.getQtyReserved().intValue()); assertEquals(1, line1.getQtyDelivered().intValue()); } + + @Test + /** + * https://idempiere.atlassian.net/browse/IDEMPIERE-4768 + */ + public void testMultiASIShipment() { + Properties ctx = Env.getCtx(); + String trxName = getTrxName(); + MProduct fert50 = new MProduct(ctx, PRODUCT_FERT50, trxName); + + Timestamp today = TimeUtil.getDay(System.currentTimeMillis()); + Timestamp past_month = TimeUtil.addMonths(today, -1); + + MWarehouse wh = new MWarehouse(ctx, WAREHOUSE_FERTILIZER, trxName); + wh.setIsDisallowNegativeInv(true); + wh.saveEx(); + CacheMgt.get().reset(MWarehouse.Table_Name, WAREHOUSE_FERTILIZER); + // Put the modified record into cache + MWarehouse.get(ctx, WAREHOUSE_FERTILIZER, trxName); + + // create an ASI for Fertilizer Lot with Lot 1010 + MAttributeSetInstance asi = new MAttributeSetInstance(ctx, 0, trxName); + asi.setM_AttributeSet_ID(fert50.getM_AttributeSet_ID()); + asi.setLot("1010"); + asi.saveEx(); + + MStorageOnHand.add(ctx, WAREHOUSE_FERTILIZER, LOCATOR_FERTILIZER, PRODUCT_FERT50, asi.getM_AttributeSetInstance_ID(), Env.ONE, past_month, trxName); + MStorageOnHand.add(ctx, WAREHOUSE_FERTILIZER, LOCATOR_FERTILIZER, PRODUCT_FERT50, asi.getM_AttributeSetInstance_ID(), Env.ONE, today, trxName); + + // Expected to create two entries in storage because of the different dates + MStorageOnHand[] storages = MStorageOnHand.getWarehouse(ctx, WAREHOUSE_FERTILIZER, + PRODUCT_FERT50, asi.getM_AttributeSetInstance_ID(), null, + MClient.MMPOLICY_FiFo.equals(fert50.getMMPolicy()), false, + 0, trxName); + assertEquals(2, storages.length); + for (int i = 0; i < storages.length; i++) { + MStorageOnHand storage = storages[i]; + assertEquals(1, storage.getQtyOnHand().intValue()); + if (i == 0) + assertEquals(past_month, storage.getDateMaterialPolicy()); + else + assertEquals(today, storage.getDateMaterialPolicy()); + } + + MOrder order = new MOrder(ctx, 0, trxName); + order.setAD_Org_ID(ORG_FERTILIZER); + order.setBPartner(MBPartner.get(ctx, BP_JOE_BLOCK)); + order.setC_DocTypeTarget_ID(MOrder.DocSubTypeSO_POS); + order.setDeliveryRule(MOrder.DELIVERYRULE_CompleteOrder); + order.setM_Warehouse_ID(WAREHOUSE_FERTILIZER); + order.setDocStatus(DocAction.STATUS_Drafted); + order.setDocAction(DocAction.ACTION_Complete); + order.setPaymentRule(MOrder.PAYMENTRULE_OnCredit); // this is the default, just making it explicit + order.setDatePromised(today); + order.saveEx(); + + MOrderLine line1 = new MOrderLine(order); + line1.setLine(10); + line1.setProduct(MProduct.get(ctx, PRODUCT_FERT50)); + line1.setM_AttributeSetInstance_ID(asi.getM_AttributeSetInstance_ID()); + line1.setQty(new BigDecimal("2")); + line1.setDatePromised(today); + line1.saveEx(); + + // Expected to complete without problems + ProcessInfo info = MWorkflow.runDocumentActionWorkflow(order, DocAction.ACTION_Complete); + assertFalse(info.isError(), info.getSummary()); + order.load(trxName); + assertEquals(DocAction.STATUS_Completed, order.getDocStatus()); + line1.load(trxName); + assertEquals(0, line1.getQtyReserved().intValue()); + assertEquals(2, line1.getQtyDelivered().intValue()); + assertEquals(2, line1.getQtyInvoiced().intValue()); + + // Expected to have cleared both storage entries on shipment + storages = MStorageOnHand.getWarehouse(ctx, WAREHOUSE_FERTILIZER, + PRODUCT_FERT50, asi.getM_AttributeSetInstance_ID(), null, + MClient.MMPOLICY_FiFo.equals(fert50.getMMPolicy()), false, + 0, trxName); + assertEquals(0, storages.length); + } + }