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)
{
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 && 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 && storages.length > 0)
dateMPolicy = storages[0].getDateMaterialPolicy();
if(dateMPolicy==null)
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;

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(), "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<Event>
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);
//

View File

@ -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());
}
}
}

View File

@ -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);
}
}