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:
parent
73f0cea85a
commit
3be5c0ac92
|
@ -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;
|
||||
|
|
|
@ -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);
|
||||
//
|
||||
|
|
|
@ -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());
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue