IDEMPIERE-4768 Cannot ship in some cases when multiple ASI with different material policy (#660)

* IDEMPIERE-4768 Cannot ship in some cases when multiple ASI with different material policy

* IDEMPIERE-4768 Cannot ship in some cases when multiple ASI with different material policy

* Fix wrong selection of material date policy, must take into account isUseGuaranteeDateForMPolicy
* Fix wrong material receipt, assigning material date policy based on inventory instead of document/ASI

* * Unit tests
This commit is contained in:
Carlos Ruiz 2021-04-21 15:42:10 +02:00 committed by GitHub
parent 73f0cea85a
commit 3be5c0ac92
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 242 additions and 17 deletions

View File

@ -1441,29 +1441,59 @@ public class MInOut extends X_M_InOut implements DocAction
if (mtrx == null) if (mtrx == null)
{ {
Timestamp dateMPolicy= null; Timestamp dateMPolicy= null;
BigDecimal pendingQty = Qty;
if (pendingQty.signum() < 0) { // taking from inventory
MStorageOnHand[] storages = MStorageOnHand.getWarehouse(getCtx(), 0, MStorageOnHand[] storages = MStorageOnHand.getWarehouse(getCtx(), 0,
sLine.getM_Product_ID(), sLine.getM_AttributeSetInstance_ID(), null, sLine.getM_Product_ID(), sLine.getM_AttributeSetInstance_ID(), null,
MClient.MMPOLICY_FiFo.equals(product.getMMPolicy()), false, MClient.MMPOLICY_FiFo.equals(product.getMMPolicy()), false,
sLine.getM_Locator_ID(), get_TrxName()); sLine.getM_Locator_ID(), get_TrxName());
for (MStorageOnHand storage : storages) { for (MStorageOnHand storage : storages) {
if (storage.getQtyOnHand().compareTo(sLine.getMovementQty()) >= 0) { if (pendingQty.signum() == 0)
break;
if (storage.getQtyOnHand().compareTo(pendingQty.negate()) >= 0) {
dateMPolicy = storage.getDateMaterialPolicy(); dateMPolicy = storage.getDateMaterialPolicy();
break; 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) if (dateMPolicy == null && storages.length > 0)
dateMPolicy = storages[0].getDateMaterialPolicy(); dateMPolicy = storages[0].getDateMaterialPolicy();
}
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) if (dateMPolicy == null)
dateMPolicy = getMovementDate(); dateMPolicy = getMovementDate();
// Fallback: Update Storage - see also VMatch.createMatchRecord // 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_Locator_ID(),
sLine.getM_Product_ID(), sLine.getM_Product_ID(),
sLine.getM_AttributeSetInstance_ID(), sLine.getM_AttributeSetInstance_ID(),
Qty,dateMPolicy,get_TrxName())) pendingQty,dateMPolicy,get_TrxName()))
{ {
String lastError = CLogger.retrieveErrorString(""); String lastError = CLogger.retrieveErrorString("");
m_processMsg = "Cannot correct Inventory OnHand [" + product.getValue() + "] - " + lastError; m_processMsg = "Cannot correct Inventory OnHand [" + product.getValue() + "] - " + lastError;

View File

@ -179,6 +179,7 @@ public class WPAttributeInstance extends Window implements EventListener<Event>
new ColumnInfo(Msg.translate(Env.getCtx(), "Lot"), "asi.Lot", String.class), 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(), "SerNo"), "asi.SerNo", String.class),
new ColumnInfo(Msg.translate(Env.getCtx(), "GuaranteeDate"), "asi.GuaranteeDate", Timestamp.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(), "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(), "QtyOnHand"), "s.QtyOnHand", Double.class),
new ColumnInfo(Msg.translate(Env.getCtx(), "QtyReserved"), "s.QtyReserved", Double.class), new ColumnInfo(Msg.translate(Env.getCtx(), "QtyReserved"), "s.QtyReserved", Double.class),
@ -255,7 +256,7 @@ public class WPAttributeInstance extends Window implements EventListener<Event>
m_sql = m_table.prepareTable (s_layout, s_sqlFrom, m_sql = m_table.prepareTable (s_layout, s_sqlFrom,
m_M_Warehouse_ID == 0 ? s_sqlWhereWithoutWarehouse : s_sqlWhere, false, "s") 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); m_table.addEventListener(Events.ON_SELECT, this);
// //

View File

@ -31,7 +31,9 @@ import java.math.BigDecimal;
import java.sql.Timestamp; import java.sql.Timestamp;
import java.util.Properties; import java.util.Properties;
import org.compiere.model.MAttributeSetInstance;
import org.compiere.model.MBPartner; import org.compiere.model.MBPartner;
import org.compiere.model.MClient;
import org.compiere.model.MInOut; import org.compiere.model.MInOut;
import org.compiere.model.MInOutLine; import org.compiere.model.MInOutLine;
import org.compiere.model.MInvoice; import org.compiere.model.MInvoice;
@ -39,6 +41,7 @@ import org.compiere.model.MInvoiceLine;
import org.compiere.model.MOrder; import org.compiere.model.MOrder;
import org.compiere.model.MOrderLine; import org.compiere.model.MOrderLine;
import org.compiere.model.MProduct; import org.compiere.model.MProduct;
import org.compiere.model.MStorageOnHand;
import org.compiere.model.MStorageReservation; import org.compiere.model.MStorageReservation;
import org.compiere.process.DocAction; import org.compiere.process.DocAction;
import org.compiere.process.ProcessInfo; 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_PO = 126;
private static final int DOCTYPE_RECEIPT = 122; private static final int DOCTYPE_RECEIPT = 122;
private static final int DOCTYPE_AP_INVOICE = 123; 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_SEEDER = 143;
private static final int PRODUCT_WEEDER = 141; private static final int PRODUCT_WEEDER = 141;
private static final int PRODUCT_MULCH = 137;
private static final int USER_GARDENADMIN = 101; private static final int USER_GARDENADMIN = 101;
private static final BigDecimal THREE = new BigDecimal("3"); private static final BigDecimal THREE = new BigDecimal("3");
private static final BigDecimal MINUS_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 * https://idempiere.atlassian.net/browse/IDEMPIERE-4575
*/ */
@ -316,4 +323,100 @@ public class PurchaseOrderTest extends AbstractTestCase {
return qtyOrdered; 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());
}
}
} }

View File

@ -34,7 +34,9 @@ import java.sql.Timestamp;
import java.util.Properties; import java.util.Properties;
import org.compiere.model.MAllocationHdr; import org.compiere.model.MAllocationHdr;
import org.compiere.model.MAttributeSetInstance;
import org.compiere.model.MBPartner; import org.compiere.model.MBPartner;
import org.compiere.model.MClient;
import org.compiere.model.MInOut; import org.compiere.model.MInOut;
import org.compiere.model.MInOutLine; import org.compiere.model.MInOutLine;
import org.compiere.model.MInvoice; import org.compiere.model.MInvoice;
@ -44,10 +46,13 @@ import org.compiere.model.MPInstance;
import org.compiere.model.MPInstancePara; import org.compiere.model.MPInstancePara;
import org.compiere.model.MPayment; import org.compiere.model.MPayment;
import org.compiere.model.MProduct; import org.compiere.model.MProduct;
import org.compiere.model.MStorageOnHand;
import org.compiere.model.MWarehouse;
import org.compiere.model.SystemIDs; import org.compiere.model.SystemIDs;
import org.compiere.process.DocAction; import org.compiere.process.DocAction;
import org.compiere.process.ProcessInfo; import org.compiere.process.ProcessInfo;
import org.compiere.process.ServerProcessCtl; import org.compiere.process.ServerProcessCtl;
import org.compiere.util.CacheMgt;
import org.compiere.util.DB; import org.compiere.util.DB;
import org.compiere.util.Env; import org.compiere.util.Env;
import org.compiere.util.TimeUtil; import org.compiere.util.TimeUtil;
@ -66,6 +71,10 @@ public class SalesOrderTest extends AbstractTestCase {
private final static int BP_JOE_BLOCK = 118; private final static int BP_JOE_BLOCK = 118;
private static final int PRODUCT_OAK_TREE = 123; private static final int PRODUCT_OAK_TREE = 123;
private static final int PRODUCT_AZALEA = 128; 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 @Test
/** /**
@ -643,4 +652,86 @@ public class SalesOrderTest extends AbstractTestCase {
assertEquals(0, line1.getQtyReserved().intValue()); assertEquals(0, line1.getQtyReserved().intValue());
assertEquals(1, line1.getQtyDelivered().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);
}
} }