diff --git a/migration/i8.2z/oracle/202111210850_IDEMPIERE-5040.sql b/migration/i8.2z/oracle/202111210850_IDEMPIERE-5040.sql new file mode 100644 index 0000000000..5672970006 --- /dev/null +++ b/migration/i8.2z/oracle/202111210850_IDEMPIERE-5040.sql @@ -0,0 +1,23 @@ +SET SQLBLANKLINES ON +SET DEFINE OFF + +-- IDEMPIERE-5040 Stall M_InOutLine.QtyOverReceipt +-- Nov 19, 2021, 11:00:44 PM MYT +INSERT INTO AD_TableIndex (AD_Client_ID,AD_Org_ID,AD_TableIndex_ID,AD_TableIndex_UU,Created,CreatedBy,EntityType,IsActive,Name,Updated,UpdatedBy,AD_Table_ID,IsCreateConstraint,IsUnique,Processing,TableIndexDrop,IsKey) VALUES (0,0,201096,'c97e6bf9-5754-485c-a983-5cb01429032f',TO_DATE('2021-11-19 23:00:43','YYYY-MM-DD HH24:MI:SS'),100,'D','Y','m_inoutline_orderline_idx',TO_DATE('2021-11-19 23:00:43','YYYY-MM-DD HH24:MI:SS'),100,320,'N','N','N','N','N') +; + +-- Nov 19, 2021, 11:00: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,201443,'4c920b11-27f2-40a5-b30f-cc8b525d942b',TO_DATE('2021-11-19 23:00:57','YYYY-MM-DD HH24:MI:SS'),100,'D','Y',TO_DATE('2021-11-19 23:00:57','YYYY-MM-DD HH24:MI:SS'),100,3811,201096,10) +; + +-- Nov 19, 2021, 11:01:04 PM MYT +CREATE INDEX m_inoutline_orderline_idx ON M_InOutLine (C_OrderLine_ID) +; + +-- Nov 26, 2021, 6:14:22 AM MYT +UPDATE AD_Column SET IsActive='N',Updated=TO_DATE('2021-11-26 06:14:22','YYYY-MM-DD HH24:MI:SS'),UpdatedBy=100 WHERE AD_Column_ID=210888 +; + +SELECT register_migration_script('202111210850_IDEMPIERE-5040.sql') FROM dual +; + diff --git a/migration/i8.2z/postgresql/202111210850_IDEMPIERE-5040.sql b/migration/i8.2z/postgresql/202111210850_IDEMPIERE-5040.sql new file mode 100644 index 0000000000..a3671c28e4 --- /dev/null +++ b/migration/i8.2z/postgresql/202111210850_IDEMPIERE-5040.sql @@ -0,0 +1,20 @@ +-- IDEMPIERE-5040 Stall M_InOutLine.QtyOverReceipt +-- Nov 19, 2021, 11:00:44 PM MYT +INSERT INTO AD_TableIndex (AD_Client_ID,AD_Org_ID,AD_TableIndex_ID,AD_TableIndex_UU,Created,CreatedBy,EntityType,IsActive,Name,Updated,UpdatedBy,AD_Table_ID,IsCreateConstraint,IsUnique,Processing,TableIndexDrop,IsKey) VALUES (0,0,201096,'c97e6bf9-5754-485c-a983-5cb01429032f',TO_TIMESTAMP('2021-11-19 23:00:43','YYYY-MM-DD HH24:MI:SS'),100,'D','Y','m_inoutline_orderline_idx',TO_TIMESTAMP('2021-11-19 23:00:43','YYYY-MM-DD HH24:MI:SS'),100,320,'N','N','N','N','N') +; + +-- Nov 19, 2021, 11:00: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,201443,'4c920b11-27f2-40a5-b30f-cc8b525d942b',TO_TIMESTAMP('2021-11-19 23:00:57','YYYY-MM-DD HH24:MI:SS'),100,'D','Y',TO_TIMESTAMP('2021-11-19 23:00:57','YYYY-MM-DD HH24:MI:SS'),100,3811,201096,10) +; + +-- Nov 19, 2021, 11:01:04 PM MYT +CREATE INDEX m_inoutline_orderline_idx ON M_InOutLine (C_OrderLine_ID) +; + +-- Nov 26, 2021, 6:14:22 AM MYT +UPDATE AD_Column SET IsActive='N',Updated=TO_TIMESTAMP('2021-11-26 06:14:22','YYYY-MM-DD HH24:MI:SS'),UpdatedBy=100 WHERE AD_Column_ID=210888 +; + +SELECT register_migration_script('202111210850_IDEMPIERE-5040.sql') FROM dual +; + diff --git a/org.adempiere.base/src/org/compiere/model/I_M_InOutLine.java b/org.adempiere.base/src/org/compiere/model/I_M_InOutLine.java index 1c9f001d8f..f9cf876ba1 100644 --- a/org.adempiere.base/src/org/compiere/model/I_M_InOutLine.java +++ b/org.adempiere.base/src/org/compiere/model/I_M_InOutLine.java @@ -447,19 +447,6 @@ public interface I_M_InOutLine */ public BigDecimal getQtyEntered(); - /** Column name QtyOverReceipt */ - public static final String COLUMNNAME_QtyOverReceipt = "QtyOverReceipt"; - - /** Set Over Receipt. - * Over Receipt Quantity - */ - public void setQtyOverReceipt (BigDecimal QtyOverReceipt); - - /** Get Over Receipt. - * Over Receipt Quantity - */ - public BigDecimal getQtyOverReceipt(); - /** Column name Ref_InOutLine_ID */ public static final String COLUMNNAME_Ref_InOutLine_ID = "Ref_InOutLine_ID"; diff --git a/org.adempiere.base/src/org/compiere/model/MInOut.java b/org.adempiere.base/src/org/compiere/model/MInOut.java index 59d4be44fa..d01c4d948f 100644 --- a/org.adempiere.base/src/org/compiere/model/MInOut.java +++ b/org.adempiere.base/src/org/compiere/model/MInOut.java @@ -762,8 +762,6 @@ public class MInOut extends X_M_InOut implements DocAction } } - line.setQtyOverReceipt(fromLine.getQtyOverReceipt()); - // line.setProcessed(false); if (line.save(get_TrxName())) @@ -1358,8 +1356,6 @@ public class MInOut extends X_M_InOut implements DocAction log.fine("Material Transaction"); MTransaction mtrx = null; - // - BigDecimal overReceipt = BigDecimal.ZERO; if (!isReversal()) { if (oLine != null) @@ -1368,21 +1364,25 @@ public class MInOut extends X_M_InOut implements DocAction .subtract(oLine.getQtyDelivered()); if (toDelivered.signum() < 0) // IDEMPIERE-2889 toDelivered = Env.ZERO; - if (sLine.getMovementQty().compareTo(toDelivered) > 0) - overReceipt = sLine.getMovementQty().subtract( - toDelivered); - if (overReceipt.signum() != 0) - { - sLine.setQtyOverReceipt(overReceipt); - sLine.saveEx(); - } } } - else + + BigDecimal storageReservationToUpdate = sLine.getMovementQty(); + if (oLine != null) { - overReceipt = sLine.getQtyOverReceipt(); + if (!isReversal()) + { + if (storageReservationToUpdate.compareTo(oLine.getQtyReserved()) > 0) + storageReservationToUpdate = oLine.getQtyReserved(); + } + else + { + BigDecimal tmp = storageReservationToUpdate.negate().add(oLine.getQtyReserved()); + if (tmp.compareTo(oLine.getQtyOrdered()) > 0) + storageReservationToUpdate = oLine.getQtyOrdered().subtract(oLine.getQtyReserved()); + } } - BigDecimal orderedQtyToUpdate = sLine.getMovementQty().subtract(overReceipt); + // if (sLine.getM_AttributeSetInstance_ID() == 0) { @@ -1421,7 +1421,8 @@ public class MInOut extends X_M_InOut implements DocAction } } - if (oLine!=null && mtrx!=null && oLine.getQtyOrdered().signum() >= 0) + if (oLine!=null && mtrx!=null && + ((!isReversal() && oLine.getQtyReserved().signum() > 0) || (isReversal() && oLine.getQtyOrdered().signum() > 0))) { if (sLine.getC_OrderLine_ID() != 0 && oLine.getM_Product_ID() > 0) { @@ -1436,7 +1437,7 @@ public class MInOut extends X_M_InOut implements DocAction if (!MStorageReservation.add(getCtx(), oLine.getM_Warehouse_ID(), oLine.getM_Product_ID(), oLine.getM_AttributeSetInstance_ID(), - orderedQtyToUpdate.negate(), + storageReservationToUpdate.negate(), isSOTrx(), get_TrxName(), tracer)) { @@ -1510,7 +1511,8 @@ public class MInOut extends X_M_InOut implements DocAction m_processMsg = "Cannot correct Inventory OnHand [" + product.getValue() + "] - " + lastError; return DocAction.STATUS_Invalid; } - if (oLine!=null && oLine.getQtyOrdered().signum() > 0 && oLine.getM_Product_ID() > 0) + if (oLine!=null && oLine.getM_Product_ID() > 0 && + ((!isReversal() && oLine.getQtyReserved().signum() > 0) || (isReversal() && oLine.getQtyOrdered().signum() > 0))) { IReservationTracer tracer = null; IReservationTracerFactory factory = Core.getReservationTracerFactory(); @@ -1523,7 +1525,7 @@ public class MInOut extends X_M_InOut implements DocAction if (!MStorageReservation.add(getCtx(), oLine.getM_Warehouse_ID(), oLine.getM_Product_ID(), oLine.getM_AttributeSetInstance_ID(), - orderedQtyToUpdate.negate(), isSOTrx(), get_TrxName(), tracer)) + storageReservationToUpdate.negate(), isSOTrx(), get_TrxName(), tracer)) { m_processMsg = "Cannot correct Inventory Reserved " + (isSOTrx()? "Reserved [" :"Ordered [") + product.getValue() + "]"; return DocAction.STATUS_Invalid; @@ -1549,7 +1551,7 @@ public class MInOut extends X_M_InOut implements DocAction { if (oLine.getQtyOrdered().signum() >= 0) { - oLine.setQtyReserved(oLine.getQtyReserved().subtract(sLine.getMovementQty().subtract(sLine.getQtyOverReceipt()))); + oLine.setQtyReserved(oLine.getQtyReserved().subtract(sLine.getMovementQty())); if (oLine.getQtyReserved().signum() == -1) oLine.setQtyReserved(Env.ZERO); @@ -2312,7 +2314,6 @@ public class MInOut extends X_M_InOut implements DocAction MInOutLine rLine = rLines[i]; rLine.setQtyEntered(rLine.getQtyEntered().negate()); rLine.setMovementQty(rLine.getMovementQty().negate()); - rLine.setQtyOverReceipt(rLine.getQtyOverReceipt().negate()); rLine.setM_AttributeSetInstance_ID(sLines[i].getM_AttributeSetInstance_ID()); // Goodwill: store original (voided/reversed) document line rLine.setReversalLine_ID(sLines[i].getM_InOutLine_ID()); diff --git a/org.adempiere.base/src/org/compiere/model/MOrder.java b/org.adempiere.base/src/org/compiere/model/MOrder.java index 4497572aa2..b8e7de0b2b 100644 --- a/org.adempiere.base/src/org/compiere/model/MOrder.java +++ b/org.adempiere.base/src/org/compiere/model/MOrder.java @@ -2003,6 +2003,8 @@ public class MOrder extends X_C_Order implements DocAction } } + updateOverReceipt(); + setProcessed(true); m_processMsg = info.toString(); // @@ -2010,7 +2012,15 @@ public class MOrder extends X_C_Order implements DocAction return DocAction.STATUS_Completed; } // completeIt - + private void updateOverReceipt() { + for(MOrderLine line : m_lines) { + if (line.getM_Product_ID() <= 0) continue; + if (line.getQtyDelivered().signum() > 0 && line.getQtyOrdered().compareTo(line.getQtyDelivered()) >= 0) { + DB.executeUpdateEx("UPDATE M_InOutLine Set QtyOverReceipt=0 WHERE C_OrderLine_ID=? AND QtyOverReceipt>0", + new Object[] {line.getC_OrderLine_ID()}, get_TrxName()); + } + } + } protected String landedCostAllocation() { MOrderLandedCost[] landedCosts = MOrderLandedCost.getOfOrder(getC_Order_ID(), get_TrxName()); diff --git a/org.adempiere.base/src/org/compiere/model/X_M_InOutLine.java b/org.adempiere.base/src/org/compiere/model/X_M_InOutLine.java index 3ce80c9893..a06efd58e6 100644 --- a/org.adempiere.base/src/org/compiere/model/X_M_InOutLine.java +++ b/org.adempiere.base/src/org/compiere/model/X_M_InOutLine.java @@ -33,7 +33,7 @@ public class X_M_InOutLine extends PO implements I_M_InOutLine, I_Persistent /** * */ - private static final long serialVersionUID = 20211106L; + private static final long serialVersionUID = 20211126L; /** Standard Constructor */ public X_M_InOutLine (Properties ctx, int M_InOutLine_ID, String trxName) @@ -742,26 +742,6 @@ public class X_M_InOutLine extends PO implements I_M_InOutLine, I_Persistent return bd; } - /** Set Over Receipt. - @param QtyOverReceipt - Over Receipt Quantity - */ - public void setQtyOverReceipt (BigDecimal QtyOverReceipt) - { - set_Value (COLUMNNAME_QtyOverReceipt, QtyOverReceipt); - } - - /** Get Over Receipt. - @return Over Receipt Quantity - */ - public BigDecimal getQtyOverReceipt () - { - BigDecimal bd = (BigDecimal)get_Value(COLUMNNAME_QtyOverReceipt); - if (bd == null) - return Env.ZERO; - return bd; - } - /** Set Referenced Shipment Line. @param Ref_InOutLine_ID Referenced Shipment Line */ public void setRef_InOutLine_ID (int Ref_InOutLine_ID) diff --git a/org.adempiere.ui/src/org/compiere/apps/form/Match.java b/org.adempiere.ui/src/org/compiere/apps/form/Match.java index b729de1eea..b3748d77d5 100644 --- a/org.adempiere.ui/src/org/compiere/apps/form/Match.java +++ b/org.adempiere.ui/src/org/compiere/apps/form/Match.java @@ -487,14 +487,23 @@ public class Match { // Update Order Line MOrderLine oLine = new MOrderLine(Env.getCtx(), Line_ID, trxName); + BigDecimal storageReservationToUpdate = null; if (oLine.get_ID() != 0) // other in MInOut.completeIt { + storageReservationToUpdate = oLine.getQtyReserved(); oLine.setQtyReserved(oLine.getQtyReserved().subtract(qty)); + if (oLine.getQtyReserved().signum() == -1) + oLine.setQtyReserved(Env.ZERO); + else if (oLine.getQtyDelivered().compareTo(oLine.getQtyOrdered()) > 0) + oLine.setQtyReserved(Env.ZERO); oLine.saveEx(); + storageReservationToUpdate = storageReservationToUpdate.subtract(oLine.getQtyReserved()); } // Update Shipment Line BigDecimal toDeliver = oLine.getQtyOrdered().subtract(oLine.getQtyDelivered()); + if (toDeliver.signum() < 0) + toDeliver = Env.ZERO; if (sLine.getMovementQty().compareTo(toDeliver) <= 0) { sLine.setC_OrderLine_ID(Line_ID); @@ -525,7 +534,7 @@ public class Match { success = true; // Correct Ordered Qty for Stocked Products (see MOrder.reserveStock / MInOut.processIt) - if (oLine.get_ID() > 0 && oLine.getM_Product_ID() > 0 && oLine.getProduct().isStocked()) { + if (oLine.get_ID() > 0 && oLine.getM_Product_ID() > 0 && oLine.getProduct().isStocked() && storageReservationToUpdate != null) { IReservationTracer tracer = null; IReservationTracerFactory factory = Core.getReservationTracerFactory(); if (factory != null) { @@ -537,7 +546,7 @@ public class Match success = MStorageReservation.add (Env.getCtx(), oLine.getM_Warehouse_ID(), oLine.getM_Product_ID(), oLine.getM_AttributeSetInstance_ID(), - qty.negate(), oLine.getParent().isSOTrx(), trxName, tracer); + storageReservationToUpdate.negate(), oLine.getParent().isSOTrx(), trxName, tracer); } } } diff --git a/org.idempiere.test/src/org/idempiere/test/base/MatchInvTest.java b/org.idempiere.test/src/org/idempiere/test/base/MatchInvTest.java index e88a237d49..588acb1f38 100644 --- a/org.idempiere.test/src/org/idempiere/test/base/MatchInvTest.java +++ b/org.idempiere.test/src/org/idempiere/test/base/MatchInvTest.java @@ -168,7 +168,7 @@ public class MatchInvTest extends AbstractTestCase { info = MWorkflow.runDocumentActionWorkflow(delivery, DocAction.ACTION_Complete); delivery.load(getTrxName()); - assertFalse(info.isError()); + assertFalse(info.isError(), info.getSummary()); assertEquals(DocAction.STATUS_Completed, delivery.getDocStatus()); if (!delivery.isPosted()) { 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 d1dbfa6cdf..d6e480dbfb 100644 --- a/org.idempiere.test/src/org/idempiere/test/model/PurchaseOrderTest.java +++ b/org.idempiere.test/src/org/idempiere/test/model/PurchaseOrderTest.java @@ -46,9 +46,12 @@ import org.compiere.model.MProduct; import org.compiere.model.MStorageOnHand; import org.compiere.model.MStorageReservation; import org.compiere.model.MStorageReservationLog; +import org.compiere.model.MSysConfig; import org.compiere.model.Query; import org.compiere.process.DocAction; import org.compiere.process.ProcessInfo; +import org.compiere.util.CacheMgt; +import org.compiere.util.DB; import org.compiere.util.Env; import org.compiere.util.TimeUtil; import org.compiere.wf.MWorkflow; @@ -482,4 +485,106 @@ public class PurchaseOrderTest extends AbstractTestCase { ordered = MStorageReservation.get(Env.getCtx(), line1.getM_Warehouse_ID(), PRODUCT_WEEDER, 0, false, getTrxName()); assertTrue(log.getNewQty().equals(ordered.getQty()), "New Qty from MStorageReservationLog != Qty from MStorageReservation"); } + + @Test + public void testQtyOverReceipt() { + Properties ctx = Env.getCtx(); + String trxName = getTrxName(); + + DB.executeUpdateEx("UPDATE AD_SysConfig SET Value='N' WHERE AD_Client_ID=0 AND Name=?", + new Object[] {MSysConfig.VALIDATE_MATCHING_TO_ORDERED_QTY}, null); + CacheMgt.get().reset(); + + BigDecimal initialQtyOrdered = getQtyOrdered(Env.getCtx(), PRODUCT_MULCH, getTrxName()); + try { + MOrder order = new MOrder(ctx, 0, trxName); + order.setBPartner(MBPartner.get(ctx, BP_PATIO)); + order.setC_DocTypeTarget_ID(DOCTYPE_PO); + order.setIsSOTrx(false); + order.setSalesRep_ID(USER_GARDENADMIN); + order.setDocStatus(DocAction.STATUS_Drafted); + order.setDocAction(DocAction.ACTION_Complete); + Timestamp today = TimeUtil.getDay(System.currentTimeMillis()); + order.setDateOrdered(today); + order.setDatePromised(today); + order.saveEx(); + + MOrderLine line1 = new MOrderLine(order); + line1.setLine(10); + line1.setProduct(MProduct.get(ctx, PRODUCT_MULCH)); + line1.setQty(new BigDecimal("1")); + line1.setDatePromised(today); + line1.saveEx(); + + ProcessInfo info = MWorkflow.runDocumentActionWorkflow(order, DocAction.ACTION_Complete); + assertFalse(info.isError(), info.getSummary()); + order.load(trxName); + assertEquals(DocAction.STATUS_Completed, order.getDocStatus(), "Order not completed"); + line1.load(trxName); + assertEquals(1, line1.getQtyReserved().intValue(), "Wrong Order line qty reserved value"); + BigDecimal newQtyOrdered = getQtyOrdered(Env.getCtx(), PRODUCT_MULCH, getTrxName()); + assertEquals(initialQtyOrdered.intValue()+1, newQtyOrdered.intValue(), "Quantiy Ordered not updated as expected"); + + MInOut receipt1 = new MInOut(order, DOCTYPE_RECEIPT, order.getDateOrdered()); + receipt1.setDocStatus(DocAction.STATUS_Drafted); + receipt1.setDocAction(DocAction.ACTION_Complete); + receipt1.saveEx(); + + MInOutLine receiptLine1 = new MInOutLine(receipt1); + receiptLine1.setOrderLine(line1, 0, new BigDecimal("2")); + receiptLine1.setQty(new BigDecimal("2")); + receiptLine1.saveEx(); + + info = MWorkflow.runDocumentActionWorkflow(receipt1, DocAction.ACTION_Complete); + assertFalse(info.isError(), info.getSummary()); + receipt1.load(trxName); + assertEquals(DocAction.STATUS_Completed, receipt1.getDocStatus(), "Material receipt not completed"); + + line1.load(trxName); + assertEquals(0, line1.getQtyReserved().intValue(), "Wrong order line qty reserved value"); + newQtyOrdered = getQtyOrdered(Env.getCtx(), PRODUCT_MULCH, getTrxName()); + assertEquals(initialQtyOrdered.intValue(), newQtyOrdered.intValue(), "Quantiy Ordered not updated as expected"); + + // reactivate the purchase order + info = MWorkflow.runDocumentActionWorkflow(order, DocAction.ACTION_ReActivate); + assertFalse(info.isError(), info.getSummary()); + order.load(trxName); + assertEquals(DocAction.STATUS_InProgress, order.getDocStatus(), "Order not reactivated"); + + // change the line quantity to 2 + line1.load(trxName); + line1.setQty(new BigDecimal("2")); + line1.saveEx(); + + // complete the order again + info = MWorkflow.runDocumentActionWorkflow(order, DocAction.ACTION_Complete); + assertFalse(info.isError(), info.getSummary()); + order.load(trxName); + assertEquals(DocAction.STATUS_Completed, order.getDocStatus(), "Order not completed"); + line1.load(trxName); + assertEquals(0, line1.getQtyReserved().intValue(), "Wrong order line qty reserved value"); + assertEquals(2, line1.getQtyOrdered().intValue(), "Wrong order line qty ordered value"); + newQtyOrdered = getQtyOrdered(Env.getCtx(), PRODUCT_MULCH, getTrxName()); + assertEquals(initialQtyOrdered.intValue(), newQtyOrdered.intValue(), "Quantiy Ordered not updated as expected"); + + //reverse MR + receiptLine1.load(trxName); + assertEquals(2, receiptLine1.getMovementQty().intValue(), "Wrong receipt line movement qty value"); + receipt1.load(trxName); + receipt1.getLines(true); + info = MWorkflow.runDocumentActionWorkflow(receipt1, DocAction.ACTION_Reverse_Accrual); + assertFalse(info.isError(), info.getSummary()); + receipt1.load(trxName); + assertEquals(DocAction.STATUS_Reversed, receipt1.getDocStatus(), "Material receipt not reversed"); + line1.load(trxName); + assertEquals(2, line1.getQtyReserved().intValue(), "Wrong order line qty reserved value"); + assertEquals(0, line1.getQtyDelivered().intValue(), "Wrong order line qty delivered value"); + newQtyOrdered = getQtyOrdered(Env.getCtx(), PRODUCT_MULCH, getTrxName()); + assertEquals(initialQtyOrdered.intValue()+2, newQtyOrdered.intValue(), "Quantiy Ordered not updated as expected"); + } finally { + DB.executeUpdateEx("UPDATE AD_SysConfig SET Value='Y' WHERE AD_Client_ID=0 AND Name=?", + new Object[] {MSysConfig.VALIDATE_MATCHING_TO_ORDERED_QTY}, null); + CacheMgt.get().reset(); + } + } }