From 9f14c96134f5d70590f625dbd718313ea43993d9 Mon Sep 17 00:00:00 2001 From: hengsin Date: Tue, 27 Aug 2024 10:07:42 +0800 Subject: [PATCH] IDEMPIERE-3040 Invoice Price Variations for Average PO costing method (#2428) * IDEMPIERE-3040 Invoice Price Variations for Average PO costing method Co-authored-by: dpansheriya --- .../org/compiere/acct/Doc_AllocationHdr.java | 18 + .../src/org/compiere/acct/Doc_Invoice.java | 530 ++- .../src/org/compiere/acct/Doc_MatchInv.java | 161 +- .../src/org/compiere/model/MAccount.java | 13 +- .../src/org/compiere/model/MFactAcct.java | 1 + .../org/idempiere/test/AbstractTestCase.java | 106 + .../src/org/idempiere/test/FactAcct.java | 113 + .../org/idempiere/test/base/InOutTest.java | 85 +- .../test/base/MatchInv2ndAcctSchemaTest.java | 11 +- .../org/idempiere/test/base/MatchInvTest.java | 321 +- .../test/base/MatchInvTestIsolated.java | 645 ++- .../test/costing/AveragePOCostingTest.java | 3537 +++++++++++++++++ .../NonStockedExpTypeAvgPOCostingTest.java | 93 +- .../NonStockedExpTypeStdCostingTest.java | 114 +- .../model/Allocation2ndAcctSchemaTest.java | 14 +- .../idempiere/test/model/AllocationTest.java | 519 +-- .../org/idempiere/test/model/MTaxTest.java | 20 +- .../test/model/ProductionTestIsolated.java | 16 +- 18 files changed, 5372 insertions(+), 945 deletions(-) create mode 100644 org.idempiere.test/src/org/idempiere/test/FactAcct.java diff --git a/org.adempiere.base/src/org/compiere/acct/Doc_AllocationHdr.java b/org.adempiere.base/src/org/compiere/acct/Doc_AllocationHdr.java index e4a5ba0c70..74322274e6 100644 --- a/org.adempiere.base/src/org/compiere/acct/Doc_AllocationHdr.java +++ b/org.adempiere.base/src/org/compiere/acct/Doc_AllocationHdr.java @@ -195,6 +195,24 @@ public class Doc_AllocationHdr extends Doc invGainLossFactLines = new ArrayList(); payGainLossFactLines = new ArrayList(); + // Do not create fact lines for reversal of invoice + if (p_lines.length == 2) + { + DocLine_Allocation line1 = (DocLine_Allocation)p_lines[0]; + DocLine_Allocation line2 = (DocLine_Allocation)p_lines[1]; + if (line1.getC_Payment_ID() == 0 && line1.getC_Order_ID() == 0 && line1.getC_CashLine_ID() == 0 && line1.getC_Invoice_ID() > 0 + && line2.getC_Payment_ID() == 0 && line2.getC_Order_ID() == 0 && line2.getC_CashLine_ID() == 0 && line2.getC_Invoice_ID() > 0) + { + MInvoice invoice1 = new MInvoice(Env.getCtx(), line1.getC_Invoice_ID(), getTrxName()); + MInvoice invoice2 = new MInvoice(Env.getCtx(), line2.getC_Invoice_ID(), getTrxName()); + if (invoice1.getGrandTotal().equals(invoice2.getGrandTotal().negate()) + && invoice2.getReversal_ID() == invoice1.getC_Invoice_ID()) + { + return m_facts; + } + } + } + // create Fact Header Fact fact = new Fact(this, as, Fact.POST_Actual); Fact factForRGL = new Fact(this, as, Fact.POST_Actual); // dummy fact (not posted) to calculate Realized Gain & Loss diff --git a/org.adempiere.base/src/org/compiere/acct/Doc_Invoice.java b/org.adempiere.base/src/org/compiere/acct/Doc_Invoice.java index 1948462568..a16f32653c 100644 --- a/org.adempiere.base/src/org/compiere/acct/Doc_Invoice.java +++ b/org.adempiere.base/src/org/compiere/acct/Doc_Invoice.java @@ -25,6 +25,7 @@ import java.sql.Savepoint; import java.sql.Timestamp; import java.util.ArrayList; import java.util.HashMap; +import java.util.List; import java.util.Map; import java.util.logging.Level; @@ -34,14 +35,18 @@ import org.compiere.model.MAccount; import org.compiere.model.MAcctSchema; import org.compiere.model.MClientInfo; import org.compiere.model.MConversionRate; +import org.compiere.model.MCost; import org.compiere.model.MCostDetail; +import org.compiere.model.MCostElement; import org.compiere.model.MCurrency; +import org.compiere.model.MFactAcct; import org.compiere.model.MInvoice; import org.compiere.model.MInvoiceLine; import org.compiere.model.MLandedCostAllocation; import org.compiere.model.MOrderLandedCostAllocation; import org.compiere.model.MTax; import org.compiere.model.ProductCost; +import org.compiere.model.Query; import org.compiere.model.X_M_Cost; import org.compiere.util.DB; import org.compiere.util.Env; @@ -422,7 +427,7 @@ public class Doc_Invoice extends Doc // ** ARI, ARF if (getDocumentType().equals(DOCTYPE_ARInvoice) || getDocumentType().equals(DOCTYPE_ARProForma)) - { + { BigDecimal grossAmt = getAmount(Doc.AMTTYPE_Gross); BigDecimal serviceAmt = Env.ZERO; @@ -440,7 +445,7 @@ public class Doc_Invoice extends Doc FactLine tl = fact.createLine(null, m_taxes[i].getAccount(DocTax.ACCTTYPE_TaxDue, as), getC_Currency_ID(), null, amt); if (tl != null) - tl.setC_Tax_ID(m_taxes[i].getC_Tax_ID()); + tl.setC_Tax_ID(m_taxes[i].getC_Tax_ID()); } } // Revenue CR @@ -588,6 +593,12 @@ public class Doc_Invoice extends Doc // ** API else if (getDocumentType().equals(DOCTYPE_APInvoice)) { + MInvoice invoice = (MInvoice)getPO(); + MInvoice originalInvoice = null; + if (invoice.getReversal_ID() > 0 && invoice.getReversal_ID() < invoice.getC_Invoice_ID()) + { + originalInvoice = new MInvoice(Env.getCtx(), invoice.getReversal_ID(), invoice.get_TrxName()); + } BigDecimal grossAmt = getAmount(Doc.AMTTYPE_Gross); BigDecimal serviceAmt = Env.ZERO; @@ -601,6 +612,10 @@ public class Doc_Invoice extends Doc getC_Currency_ID(), m_taxes[i].getAmount(), null); if (tl != null) tl.setC_Tax_ID(m_taxes[i].getC_Tax_ID()); + if (tl != null && invoice.getReversal_ID() > 0 && invoice.getReversal_ID() < invoice.getC_Invoice_ID()) + { + tl.updateReverseLine(MInvoice.Table_ID, invoice.getReversal_ID(), 0, BigDecimal.ONE); + } } // Expense DR for (int i = 0; i < p_lines.length; i++) @@ -620,6 +635,17 @@ public class Doc_Invoice extends Doc else desc += " 100%"; fl.setDescription(desc); + if (invoice.getReversal_ID() > 0 && invoice.getReversal_ID() < invoice.getC_Invoice_ID()) + { + int lineId = 0; + if (originalInvoice != null) + { + MInvoiceLine[] lines = originalInvoice.getLines(); + if (lines.length > i) + lineId = lines[i].getC_InvoiceLine_ID(); + } + fl.updateReverseLine(MInvoice.Table_ID, invoice.getReversal_ID(), lineId, BigDecimal.ONE); + } } if (!landedCost) { @@ -636,12 +662,34 @@ public class Doc_Invoice extends Doc amt = amt.add(discount); dAmt = discount; MAccount tradeDiscountReceived = line.getAccount(ProductCost.ACCTTYPE_P_TDiscountRec, as); - fact.createLine (line, tradeDiscountReceived, + FactLine fl = fact.createLine (line, tradeDiscountReceived, getC_Currency_ID(), null, dAmt); + if (fl != null && invoice.getReversal_ID() > 0 && invoice.getReversal_ID() < invoice.getC_Invoice_ID()) + { + int lineId = 0; + if (originalInvoice != null) + { + MInvoiceLine[] lines = originalInvoice.getLines(); + if (lines.length > i) + lineId = lines[i].getC_InvoiceLine_ID(); + } + fl.updateReverseLine(MInvoice.Table_ID, invoice.getReversal_ID(), lineId, BigDecimal.ONE); + } } } - fact.createLine (line, expense, + FactLine fl = fact.createLine (line, expense, getC_Currency_ID(), amt, null); + if (fl != null && invoice.getReversal_ID() > 0 && invoice.getReversal_ID() < invoice.getC_Invoice_ID()) + { + int lineId = 0; + if (originalInvoice != null) + { + MInvoiceLine[] lines = originalInvoice.getLines(); + if (lines.length > i) + lineId = lines[i].getC_InvoiceLine_ID(); + } + fl.updateReverseLine(MInvoice.Table_ID, invoice.getReversal_ID(), lineId, BigDecimal.ONE); + } if (!line.isItem()) { grossAmt = grossAmt.subtract(amt); @@ -672,13 +720,23 @@ public class Doc_Invoice extends Doc serviceAmt = getAmount(Doc.AMTTYPE_Gross); grossAmt = Env.ZERO; } - if (grossAmt.signum() != 0) - fact.createLine(null, MAccount.get(getCtx(), payables_ID), + FactLine fl = null; + if (grossAmt.signum() > 0) + fl = fact.createLine(null, MAccount.get(getCtx(), payables_ID), getC_Currency_ID(), null, grossAmt); - if (serviceAmt.signum() != 0) - fact.createLine(null, MAccount.get(getCtx(), payablesServices_ID), + else if (grossAmt.signum() < 0) + fl = fact.createLine(null, MAccount.get(getCtx(), payables_ID), + getC_Currency_ID(), grossAmt.negate(), null); + if (serviceAmt.signum() > 0) + fl = fact.createLine(null, MAccount.get(getCtx(), payablesServices_ID), getC_Currency_ID(), null, serviceAmt); - + else if (serviceAmt.signum() < 0) + fl = fact.createLine(null, MAccount.get(getCtx(), payablesServices_ID), + getC_Currency_ID(), serviceAmt.negate(), null); + if (fl != null && invoice.getReversal_ID() > 0 && invoice.getReversal_ID() < invoice.getC_Invoice_ID()) + { + fl.updateReverseLine(MInvoice.Table_ID, invoice.getReversal_ID(), 0, BigDecimal.ONE); + } // Set Locations FactLine[] fLines = fact.getLines(); for (int i = 0; i < fLines.length; i++) @@ -934,7 +992,8 @@ public class Doc_Invoice extends Doc for (int i = 0; i < lcas.length; i++) totalBase += lcas[i].getBase().doubleValue(); - Map costDetailAmtMap = new HashMap(); + Map costDetailAmtMap = new HashMap<>(); + Map mcostQtyMap = new HashMap<>(); // Create New MInvoiceLine il = new MInvoiceLine (getCtx(), C_InvoiceLine_ID, getTrxName()); @@ -962,95 +1021,159 @@ public class Doc_Invoice extends Doc if (X_M_Cost.COSTINGMETHOD_AverageInvoice.equals(costingMethod) || X_M_Cost.COSTINGMETHOD_AveragePO.equals(costingMethod)) { - BigDecimal allocationAmt = lca.getAmt(); + BigDecimal allocationAmt = lca.getAmt(); + boolean reversal = false; + if (allocationAmt.signum() < 0) //reversal + { + allocationAmt = allocationAmt.negate(); + reversal = true; + } + BigDecimal estimatedAmt = BigDecimal.ZERO; - int oCurrencyId = 0; + BigDecimal costAdjustmentAmt = BigDecimal.ZERO; boolean usesSchemaCurrency = false; - Timestamp oDateAcct = getDateAcct(); - if (lca.getM_InOutLine_ID() > 0) + MInvoiceLine reversalLine = null; + if (reversal) { - I_M_InOutLine iol = lca.getM_InOutLine(); - if (iol.getC_OrderLine_ID() > 0) + MInvoice invoice = (MInvoice)getPO(); + MInvoice reversalInvoice = new MInvoice(getCtx(), invoice.getReversal_ID(), getTrxName()); + MInvoiceLine[] lines = invoice.getLines(); + MInvoiceLine[] reversalLines = reversalInvoice.getLines(); + for(int j = 0; j < lines.length; j++) { + if (lines[j].get_ID() == il.get_ID()) { + reversalLine = reversalLines[j]; + break; + } + } + } + else + { + int oCurrencyId = 0; + Timestamp oDateAcct = getDateAcct(); + if (lca.getM_InOutLine_ID() > 0) { - oCurrencyId = iol.getC_OrderLine().getC_Currency_ID(); - oDateAcct = iol.getC_OrderLine().getC_Order().getDateAcct(); - MOrderLandedCostAllocation[] allocations = MOrderLandedCostAllocation.getOfOrderLine(iol.getC_OrderLine_ID(), getTrxName()); - for(MOrderLandedCostAllocation allocation : allocations) + I_M_InOutLine iol = lca.getM_InOutLine(); + if (iol.getC_OrderLine_ID() > 0) { - if (allocation.getC_OrderLandedCost().getM_CostElement_ID() != lca.getM_CostElement_ID()) - continue; - - BigDecimal amt = allocation.getAmt(); - BigDecimal qty = allocation.getQty(); - if (qty.compareTo(iol.getMovementQty()) != 0) + oCurrencyId = iol.getC_OrderLine().getC_Currency_ID(); + oDateAcct = iol.getC_OrderLine().getC_Order().getDateAcct(); + MOrderLandedCostAllocation[] allocations = MOrderLandedCostAllocation.getOfOrderLine(iol.getC_OrderLine_ID(), getTrxName()); + for(MOrderLandedCostAllocation allocation : allocations) { - amt = amt.multiply(iol.getMovementQty()).divide(qty, 12, RoundingMode.HALF_UP); + if (allocation.getC_OrderLandedCost().getM_CostElement_ID() != lca.getM_CostElement_ID()) + continue; + + BigDecimal amt = allocation.getAmt(); + BigDecimal qty = allocation.getQty(); + if (qty.compareTo(iol.getMovementQty()) != 0) + { + amt = amt.multiply(iol.getMovementQty()).divide(qty, 12, RoundingMode.HALF_UP); + } + estimatedAmt = estimatedAmt.add(amt); } - estimatedAmt = estimatedAmt.add(amt); } } - } - - if (estimatedAmt.scale() > as.getCostingPrecision()) - { - estimatedAmt = estimatedAmt.setScale(as.getCostingPrecision(), RoundingMode.HALF_UP); - } - BigDecimal costAdjustmentAmt = allocationAmt; - if (estimatedAmt.signum() > 0) - { - //get other allocation amt - StringBuilder sql = new StringBuilder("SELECT Sum(Amt) FROM C_LandedCostAllocation WHERE M_InOutLine_ID=? ") - .append("AND C_LandedCostAllocation_ID<>? ") - .append("AND M_CostElement_ID=? ") - .append("AND AD_Client_ID=? "); - BigDecimal otherAmt = DB.getSQLValueBD(getTrxName(), sql.toString(), lca.getM_InOutLine_ID(), lca.getC_LandedCostAllocation_ID(), - lca.getM_CostElement_ID(), lca.getAD_Client_ID()); - if (otherAmt != null) + + if (estimatedAmt.scale() > as.getCostingPrecision()) { - estimatedAmt = estimatedAmt.subtract(otherAmt); - if (allocationAmt.signum() < 0) - { - //add back since the sum above would include the original trx - estimatedAmt = estimatedAmt.add(allocationAmt.negate()); - } - } - //added for IDEMPIERE-3014 - //convert to accounting schema currency - if (estimatedAmt.signum() > 0 && oCurrencyId != getC_Currency_ID()) - { - estimatedAmt = MConversionRate.convert(getCtx(), estimatedAmt, - oCurrencyId, as.getC_Currency_ID(), - oDateAcct, getC_ConversionType_ID(), - getAD_Client_ID(), getAD_Org_ID()); - - allocationAmt = MConversionRate.convert(getCtx(), allocationAmt, - getC_Currency_ID(), as.getC_Currency_ID(), - getDateAcct(), getC_ConversionType_ID(), - getAD_Client_ID(), getAD_Org_ID()); - setC_Currency_ID(as.getC_Currency_ID()); - usesSchemaCurrency = true; + estimatedAmt = estimatedAmt.setScale(as.getCostingPrecision(), RoundingMode.HALF_UP); } - + costAdjustmentAmt = allocationAmt; if (estimatedAmt.signum() > 0) - { - if (allocationAmt.signum() > 0) - costAdjustmentAmt = allocationAmt.subtract(estimatedAmt); - else if (allocationAmt.signum() < 0) - costAdjustmentAmt = allocationAmt.add(estimatedAmt); - } - } - - if (!dr) - costAdjustmentAmt = costAdjustmentAmt.negate(); + { + //get other allocation amt + StringBuilder sql = new StringBuilder("SELECT Sum(Amt) FROM C_LandedCostAllocation WHERE M_InOutLine_ID=? ") + .append("AND C_LandedCostAllocation_ID<>? ") + .append("AND M_CostElement_ID=? ") + .append("AND AD_Client_ID=? "); + BigDecimal otherAmt = DB.getSQLValueBD(getTrxName(), sql.toString(), lca.getM_InOutLine_ID(), lca.getC_LandedCostAllocation_ID(), + lca.getM_CostElement_ID(), lca.getAD_Client_ID()); + if (otherAmt != null) + { + estimatedAmt = estimatedAmt.subtract(otherAmt); + } + //added for IDEMPIERE-3014 + //convert to accounting schema currency + if (estimatedAmt.signum() > 0 && oCurrencyId != getC_Currency_ID()) + { + estimatedAmt = MConversionRate.convert(getCtx(), estimatedAmt, + oCurrencyId, as.getC_Currency_ID(), + oDateAcct, getC_ConversionType_ID(), + getAD_Client_ID(), getAD_Org_ID()); - boolean zeroQty = false; - if (costAdjustmentAmt.signum() != 0) + allocationAmt = MConversionRate.convert(getCtx(), allocationAmt, + getC_Currency_ID(), as.getC_Currency_ID(), + getDateAcct(), getC_ConversionType_ID(), + getAD_Client_ID(), getAD_Org_ID()); + setC_Currency_ID(as.getC_Currency_ID()); + usesSchemaCurrency = true; + } + + if (estimatedAmt.signum() > 0) + { + costAdjustmentAmt = allocationAmt.subtract(estimatedAmt); + } + } + + if (!dr) + costAdjustmentAmt = costAdjustmentAmt.negate(); + } + + BigDecimal amtAsset = Env.ZERO; + BigDecimal amtVariance = Env.ZERO; + BigDecimal costDetailQty = lca.getQty(); + if (costAdjustmentAmt.signum() != 0 && !reversal) { Trx trx = Trx.get(getTrxName(), false); Savepoint savepoint = null; try { savepoint = trx.setSavepoint(null); - BigDecimal costDetailAmt = costAdjustmentAmt; + + amtVariance = Env.ZERO; + amtAsset = costAdjustmentAmt; + + if(X_M_Cost.COSTINGMETHOD_AveragePO.equals(costingMethod)) + { + int AD_Org_ID = lca.getAD_Org_ID(); + int M_AttributeSetInstance_ID = lca.getM_AttributeSetInstance_ID(); + + if (MAcctSchema.COSTINGLEVEL_Client.equals(as.getCostingLevel())) + { + AD_Org_ID = 0; + M_AttributeSetInstance_ID = 0; + } + else if (MAcctSchema.COSTINGLEVEL_Organization.equals(as.getCostingLevel())) + M_AttributeSetInstance_ID = 0; + else if (MAcctSchema.COSTINGLEVEL_BatchLot.equals(as.getCostingLevel())) + AD_Org_ID = 0; + + MCostElement ce = MCostElement.getMaterialCostElement(getCtx(), as.getCostingMethod(), + AD_Org_ID); + MCost c = MCost.get(getCtx(), getAD_Client_ID(), AD_Org_ID, lca.getM_Product_ID(), + as.getM_CostType_ID(), as.getC_AcctSchema_ID(), ce.getM_CostElement_ID(), + M_AttributeSetInstance_ID, getTrxName()); + if (c != null) + { + BigDecimal mcostQty = c.getCurrentQty(); + if (mcostQtyMap.containsKey(c.get_UUID())) { + mcostQty = mcostQty.subtract(mcostQtyMap.get(c.get_UUID())); + if (mcostQty.signum() < 0) + mcostQty = new BigDecimal("0.00"); + } + if (mcostQty.compareTo(lca.getQty()) < 0) { + amtAsset = mcostQty.multiply(costAdjustmentAmt.divide(lca.getQty(), as.getCostingPrecision(), RoundingMode.HALF_UP)); + amtVariance = costAdjustmentAmt.subtract(amtAsset); + costDetailQty = mcostQty; + } + if (mcostQtyMap.containsKey(c.get_UUID())) { + mcostQtyMap.put(c.get_UUID(), mcostQtyMap.get(c.get_UUID()).add(costDetailQty)); + } else { + mcostQtyMap.put(c.get_UUID(), costDetailQty); + } + } + } + + BigDecimal costDetailAmt = amtAsset; //convert to accounting schema currency if (getC_Currency_ID() != as.getC_Currency_ID()) costDetailAmt = MConversionRate.convert(getCtx(), costDetailAmt, @@ -1066,18 +1189,20 @@ public class Doc_Invoice extends Doc costDetailAmt = costDetailAmt.add(prevAmt); } costDetailAmtMap.put(key, costDetailAmt); - if (!MCostDetail.createInvoice(as, lca.getAD_Org_ID(), + if (costDetailAmt.signum() != 0 && + !MCostDetail.createInvoice(as, lca.getAD_Org_ID(), lca.getM_Product_ID(), lca.getM_AttributeSetInstance_ID(), C_InvoiceLine_ID, lca.getM_CostElement_ID(), - costDetailAmt, lca.getQty(), + costDetailAmt, costDetailQty, desc, getTrxName())) { throw new RuntimeException("Failed to create cost detail record."); } } catch (SQLException e) { throw new RuntimeException(e.getLocalizedMessage(), e); } catch (AverageCostingZeroQtyException e) { - zeroQty = true; - try { + try { + amtAsset = BigDecimal.ZERO; + amtVariance = costAdjustmentAmt; trx.rollback(savepoint); savepoint = null; } catch (SQLException e1) { @@ -1090,16 +1215,113 @@ public class Doc_Invoice extends Doc } catch (SQLException e) {} } } + } else if (reversal) { + costDetailQty = BigDecimal.ZERO; + int AD_Org_ID = lca.getAD_Org_ID(); + int M_AttributeSetInstance_ID = lca.getM_AttributeSetInstance_ID(); + + if (MAcctSchema.COSTINGLEVEL_Client.equals(as.getCostingLevel())) + { + AD_Org_ID = 0; + M_AttributeSetInstance_ID = 0; + } + else if (MAcctSchema.COSTINGLEVEL_Organization.equals(as.getCostingLevel())) + M_AttributeSetInstance_ID = 0; + else if (MAcctSchema.COSTINGLEVEL_BatchLot.equals(as.getCostingLevel())) + AD_Org_ID = 0; + String key = lca.getM_Product_ID()+"_"+M_AttributeSetInstance_ID; + if (!costDetailAmtMap.containsKey(key)) { + costDetailAmtMap.put(key, BigDecimal.ZERO); + amtAsset = BigDecimal.ZERO; + amtVariance = BigDecimal.ZERO; + MAccount varianceAccount = pc.getAccount(ProductCost.ACCTTYPE_P_AverageCostVariance, as); + MAccount assetAccount = pc.getAccount(ProductCost.ACCTTYPE_P_Asset, as); + Query query = MFactAcct.createRecordIdQuery(MInvoice.Table_ID, reversalLine.getC_Invoice_ID(), as.getC_AcctSchema_ID(), getTrxName()); + List factAccts = query.list(); + for(MFactAcct factAcct : factAccts) { + if (factAcct.getM_Product_ID() != lca.getM_Product_ID()) + continue; + if (factAcct.getLine_ID() != reversalLine.get_ID()) + continue; + if (factAcct.getAccount_ID() == assetAccount.getAccount_ID()) { + if (factAcct.getAmtAcctDr().signum() != 0) + amtAsset = amtAsset.add(factAcct.getAmtAcctDr()); + else if (factAcct.getAmtAcctCr().signum() != 0) + amtAsset = amtAsset.subtract(factAcct.getAmtAcctCr()); + } else if (factAcct.getAccount_ID() == varianceAccount.getAccount_ID()) { + if (factAcct.getAmtAcctDr().signum() != 0) + amtVariance = amtVariance.add(factAcct.getAmtAcctDr()); + else if (factAcct.getAmtAcctCr().signum() != 0) + amtVariance = amtVariance.subtract(factAcct.getAmtAcctCr()); + } + } + if (lca.getM_AttributeSetInstance_ID() > 0 && M_AttributeSetInstance_ID == 0) { + String sql = + """ + SELECT SUM(Qty) + FROM M_CostDetail + WHERE C_InvoiceLine_ID=? AND Coalesce(M_CostElement_ID,0)=? + AND M_Product_ID=? AND C_AcctSchema_ID=? + """; + costDetailQty = DB.getSQLValueBDEx(getTrxName(), sql, reversalLine.get_ID(), lca.getM_CostElement_ID(), lca.getM_Product_ID(), as.getC_AcctSchema_ID()); + if (costDetailQty == null) + costDetailQty = BigDecimal.ZERO; + } else if (lca.getM_AttributeSetInstance_ID() > 0 && M_AttributeSetInstance_ID > 0) { + MCostDetail cd = MCostDetail.get (as.getCtx(), "C_InvoiceLine_ID=? AND Coalesce(M_CostElement_ID,0)="+lca.getM_CostElement_ID()+" AND M_Product_ID="+lca.getM_Product_ID(), + reversalLine.get_ID(), lca.getM_AttributeSetInstance_ID(), as.getC_AcctSchema_ID(), getTrxName()); + costDetailQty = cd != null ? cd.getQty() : BigDecimal.ZERO; + if (cd != null) { + amtAsset = cd.getAmt(); + } + if (i > 0) { + for(int j = 0; j < i; j++) { + if (lcas[j].getM_Product_ID() == lca.getM_Product_ID()) { + //variance have been posted by product + amtVariance = BigDecimal.ZERO; + } + } + } + } else { + MCostDetail cd = MCostDetail.get (as.getCtx(), "C_InvoiceLine_ID=? AND Coalesce(M_CostElement_ID,0)="+lca.getM_CostElement_ID()+" AND M_Product_ID="+lca.getM_Product_ID(), + reversalLine.get_ID(), lca.getM_AttributeSetInstance_ID(), as.getC_AcctSchema_ID(), getTrxName()); + costDetailQty = cd != null ? cd.getQty() : BigDecimal.ZERO; + } + if (costDetailQty.signum() != 0) + { + MCostElement ce = MCostElement.getMaterialCostElement(getCtx(), as.getCostingMethod(), + AD_Org_ID); + MCost c = MCost.get(getCtx(), getAD_Client_ID(), AD_Org_ID, lca.getM_Product_ID(), + as.getM_CostType_ID(), as.getC_AcctSchema_ID(), ce.getM_CostElement_ID(), + M_AttributeSetInstance_ID, getTrxName()); + if (c != null) { + if (c.getCurrentQty().signum() == 0) { + amtVariance = amtVariance.add(amtAsset); + amtAsset = BigDecimal.ZERO; + } else if (c.getCurrentQty().compareTo(costDetailQty) < 0) { + BigDecimal currentAmtAsset = amtAsset; + amtAsset = amtAsset.divide(costDetailQty, RoundingMode.HALF_UP).multiply(c.getCurrentQty()); + amtVariance = amtVariance.add(currentAmtAsset.subtract(amtAsset)); + costDetailQty = c.getCurrentQty(); + } + } + } + if (amtAsset.signum() != 0) { + if (!MCostDetail.createInvoice(as, lca.getAD_Org_ID(), + lca.getM_Product_ID(), lca.getM_AttributeSetInstance_ID(), + C_InvoiceLine_ID, lca.getM_CostElement_ID(), + amtAsset.negate(), costDetailQty, + desc, getTrxName())) { + throw new RuntimeException("Failed to create cost detail record."); + } + } + if (getC_Currency_ID() != as.getC_Currency_ID()) { + usesSchemaCurrency = true; + setC_Currency_ID(as.getC_Currency_ID()); + } + } } - boolean reversal = false; - if (allocationAmt.signum() < 0) //reversal - { - allocationAmt = allocationAmt.negate(); - reversal = true; - } - - if (allocationAmt.signum() > 0) + if (allocationAmt.signum() > 0 && !reversal) { if (allocationAmt.scale() > as.getStdPrecision()) { @@ -1109,46 +1331,53 @@ public class Doc_Invoice extends Doc { estimatedAmt = estimatedAmt.setScale(as.getStdPrecision(), RoundingMode.HALF_UP); } - int compare = allocationAmt.compareTo(estimatedAmt); - if (compare > 0) + if (allocationAmt.compareTo(estimatedAmt)!=0) { - drAmt = dr ? (reversal ? null : estimatedAmt): (reversal ? estimatedAmt : null); - crAmt = dr ? (reversal ? estimatedAmt : null): (reversal ? null : estimatedAmt); - account = pc.getAccount(ProductCost.ACCTTYPE_P_LandedCostClearing, as); - FactLine fl = fact.createLine (line, account, getC_Currency_ID(), drAmt, crAmt); - fl.setDescription(desc); - fl.setM_Product_ID(lca.getM_Product_ID()); - fl.setQty(line.getQty()); + if (estimatedAmt.signum() != 0) + { + drAmt = dr ? (reversal ? null : estimatedAmt): (reversal ? estimatedAmt : null); + crAmt = dr ? (reversal ? estimatedAmt : null): (reversal ? null : estimatedAmt); + account = pc.getAccount(ProductCost.ACCTTYPE_P_LandedCostClearing, as); + FactLine fl = fact.createLine (line, account, getC_Currency_ID(), drAmt, crAmt); + fl.setDescription(desc); + fl.setM_Product_ID(lca.getM_Product_ID()); + fl.setQty(line.getQty()); + } - BigDecimal overAmt = allocationAmt.subtract(estimatedAmt); - drAmt = dr ? (reversal ? null : overAmt) : (reversal ? overAmt : null); - crAmt = dr ? (reversal ? overAmt : null) : (reversal ? null : overAmt); - account = zeroQty ? pc.getAccount(ProductCost.ACCTTYPE_P_AverageCostVariance, as) : pc.getAccount(ProductCost.ACCTTYPE_P_Asset, as); - fl = fact.createLine (line, account, getC_Currency_ID(), drAmt, crAmt); - fl.setDescription(desc); - fl.setM_Product_ID(lca.getM_Product_ID()); - fl.setQty(line.getQty()); + if (amtVariance.signum() != 0) { + if (amtVariance.signum() > 0) { + drAmt = dr ? amtVariance : null; + crAmt = dr ? null : amtVariance; + } else { + BigDecimal underAmt = amtVariance.negate(); + drAmt = dr ? null : underAmt; + crAmt = dr ? underAmt : null; + } + + account = pc.getAccount(ProductCost.ACCTTYPE_P_AverageCostVariance, as); + FactLine fl = fact.createLine(line, account, getC_Currency_ID(), drAmt, crAmt); + fl.setDescription(desc); + fl.setM_Product_ID(lca.getM_Product_ID()); + fl.setQty(line.getQty()); + } + + if (amtAsset.signum() != 0) { + if (amtAsset.signum() > 0) { + drAmt = dr ? amtAsset : null; + crAmt = dr ? null : amtAsset; + } else { + BigDecimal underAmt = amtAsset.negate(); + drAmt = dr ? null : underAmt; + crAmt = dr ? underAmt : null; + } + account = pc.getAccount(ProductCost.ACCTTYPE_P_Asset, as); + FactLine fl = fact.createLine(line, account, getC_Currency_ID(), drAmt, crAmt); + fl.setDescription(desc); + fl.setM_Product_ID(lca.getM_Product_ID()); + fl.setQty(line.getQty()); + } } - else if (compare < 0) - { - drAmt = dr ? (reversal ? null : estimatedAmt) : (reversal ? estimatedAmt : null); - crAmt = dr ? (reversal ? estimatedAmt : null) : (reversal ? null : estimatedAmt); - account = pc.getAccount(ProductCost.ACCTTYPE_P_LandedCostClearing, as); - FactLine fl = fact.createLine (line, account, getC_Currency_ID(), drAmt, crAmt); - fl.setDescription(desc); - fl.setM_Product_ID(lca.getM_Product_ID()); - fl.setQty(line.getQty()); - - BigDecimal underAmt = estimatedAmt.subtract(allocationAmt); - drAmt = dr ? (reversal ? underAmt : null) : (reversal ? null : underAmt); - crAmt = dr ? (reversal ? null : underAmt) : (reversal ? underAmt : null); - account = zeroQty ? pc.getAccount(ProductCost.ACCTTYPE_P_AverageCostVariance, as) : pc.getAccount(ProductCost.ACCTTYPE_P_Asset, as); - fl = fact.createLine (line, account, getC_Currency_ID(), drAmt, crAmt); - fl.setDescription(desc); - fl.setM_Product_ID(lca.getM_Product_ID()); - fl.setQty(line.getQty()); - } - else + else if (allocationAmt.signum() != 0) { drAmt = dr ? (reversal ? null : allocationAmt) : (reversal ? allocationAmt : null); crAmt = dr ? (reversal ? allocationAmt : null) : (reversal ? null : allocationAmt); @@ -1158,7 +1387,46 @@ public class Doc_Invoice extends Doc fl.setM_Product_ID(lca.getM_Product_ID()); fl.setQty(line.getQty()); } - } + } else if (reversal) { + account = pc.getAccount(ProductCost.ACCTTYPE_P_LandedCostClearing, as); + FactLine fl = fact.createLine (line, account, getC_Currency_ID(), BigDecimal.ZERO, BigDecimal.ZERO); + fl.updateReverseLine(MInvoice.Table_ID, reversalLine.getC_Invoice_ID(), reversalLine.get_ID(), BigDecimal.ONE); + if (fl.getAmtAcctCr().signum() == 0 && fl.getAmtAcctDr().signum() == 0) + fact.remove(fl); + + if (amtVariance.signum() != 0) { + if (amtVariance.signum() > 0) { + drAmt = dr ? null : amtVariance; + crAmt = dr ? amtVariance : null; + } else { + BigDecimal underAmt = amtVariance.negate(); + drAmt = dr ? underAmt : null; + crAmt = dr ? null : underAmt; + } + + account = pc.getAccount(ProductCost.ACCTTYPE_P_AverageCostVariance, as); + fl = fact.createLine(line, account, getC_Currency_ID(), drAmt, crAmt); + fl.setDescription(desc); + fl.setM_Product_ID(lca.getM_Product_ID()); + fl.setQty(line.getQty()); + } + + if (amtAsset.signum() != 0) { + if (amtAsset.signum() > 0) { + drAmt = dr ? null : amtAsset; + crAmt = dr ? amtAsset : null; + } else { + BigDecimal underAmt = amtAsset.negate(); + drAmt = dr ? underAmt : null; + crAmt = dr ? null : underAmt; + } + account = pc.getAccount(ProductCost.ACCTTYPE_P_Asset, as); + fl = fact.createLine(line, account, getC_Currency_ID(), drAmt, crAmt); + fl.setDescription(desc); + fl.setM_Product_ID(lca.getM_Product_ID()); + fl.setQty(line.getQty()); + } + } if (usesSchemaCurrency) setC_Currency_ID(line.getC_Currency_ID()); } diff --git a/org.adempiere.base/src/org/compiere/acct/Doc_MatchInv.java b/org.adempiere.base/src/org/compiere/acct/Doc_MatchInv.java index 4628d49620..a07ee1bafd 100644 --- a/org.adempiere.base/src/org/compiere/acct/Doc_MatchInv.java +++ b/org.adempiere.base/src/org/compiere/acct/Doc_MatchInv.java @@ -36,7 +36,9 @@ import org.compiere.model.MAccount; import org.compiere.model.MAcctSchema; import org.compiere.model.MAcctSchemaElement; import org.compiere.model.MConversionRate; +import org.compiere.model.MCost; import org.compiere.model.MCostDetail; +import org.compiere.model.MCostElement; import org.compiere.model.MCurrency; import org.compiere.model.MFactAcct; import org.compiere.model.MInOut; @@ -389,7 +391,8 @@ public class Doc_MatchInv extends Doc // Invoice Price Variance difference BigDecimal ipv = cr.getAcctBalance().add(dr.getAcctBalance()).negate(); - processInvoicePriceVariance(as, fact, ipv); + BigDecimal ipvSource = dr.getAmtSourceDr().subtract(cr.getAmtSourceCr()).negate(); + processInvoicePriceVariance(as, fact, ipv, ipvSource); if (log.isLoggable(Level.FINE)) log.fine("IPV=" + ipv + "; Balance=" + fact.getSourceBalance()); String error = createMatchInvCostDetail(as); @@ -421,15 +424,70 @@ public class Doc_MatchInv extends Doc * @param ipv */ protected void processInvoicePriceVariance(MAcctSchema as, Fact fact, - BigDecimal ipv) { + BigDecimal ipv, BigDecimal ipvSource) { if (ipv.signum() == 0) return; - FactLine pv = fact.createLine(null, - m_pc.getAccount(ProductCost.ACCTTYPE_P_IPV, as), - as.getC_Currency_ID(), ipv); - updateFactLine(pv); - MMatchInv matchInv = (MMatchInv)getPO(); + String costingMethod = m_pc.getProduct().getCostingMethod(as); + BigDecimal amtVariance = Env.ZERO; + BigDecimal amtAsset = Env.ZERO; + BigDecimal qtyMatched = matchInv.getQty(); + BigDecimal qtyCost = null; + Boolean isStockCoverage = false; + + boolean isReversal = matchInv.getReversal_ID() > 0 && matchInv.getReversal_ID() < matchInv.get_ID(); + if (X_M_Cost.COSTINGMETHOD_AveragePO.equals(costingMethod) && m_invoiceLine.getM_Product_ID() > 0 && !isReversal) + { + isStockCoverage = true; + + int AD_Org_ID = m_receiptLine.getAD_Org_ID(); + int M_AttributeSetInstance_ID = matchInv.getM_AttributeSetInstance_ID(); + + if (MAcctSchema.COSTINGLEVEL_Client.equals(as.getCostingLevel())) + { + AD_Org_ID = 0; + M_AttributeSetInstance_ID = 0; + } + else if (MAcctSchema.COSTINGLEVEL_Organization.equals(as.getCostingLevel())) + M_AttributeSetInstance_ID = 0; + else if (MAcctSchema.COSTINGLEVEL_BatchLot.equals(as.getCostingLevel())) + AD_Org_ID = 0; + + MCostElement ce = MCostElement.getMaterialCostElement(getCtx(), costingMethod, AD_Org_ID); + + MCostDetail cd = MCostDetail.get (as.getCtx(), "M_MatchInv_ID=? AND Coalesce(M_CostElement_ID,0)=0", + matchInv.getM_MatchInv_ID(), M_AttributeSetInstance_ID, as.getC_AcctSchema_ID(), getTrxName()); + if(cd!=null){ + qtyCost = cd.getCurrentQty(); + }else{ + MCost c = MCost.get(getCtx(), getAD_Client_ID(), AD_Org_ID, m_invoiceLine.getM_Product_ID(), + as.getM_CostType_ID(), as.getC_AcctSchema_ID(), ce.getM_CostElement_ID(), + M_AttributeSetInstance_ID, getTrxName()); + qtyCost = (c!=null? c.getCurrentQty():Env.ZERO); + } + + if (qtyCost != null && qtyCost.compareTo(qtyMatched) < 0 ) + { + //If current cost qty < invoice qty + amtAsset = qtyCost.multiply(ipv).divide(qtyMatched,as.getCostingPrecision(),RoundingMode.HALF_UP); + amtVariance = ipv.subtract(amtAsset); + + }else{ + //If current qty >= invoice qty + amtAsset = ipv; + } + + } + else if (X_M_Cost.COSTINGMETHOD_AveragePO.equals(costingMethod) && m_invoiceLine.getM_Product_ID() > 0 && isReversal) + { + isStockCoverage = true; + int M_AttributeSetInstance_ID = matchInv.getM_AttributeSetInstance_ID(); + MCostDetail cd = MCostDetail.get (as.getCtx(), "M_MatchInv_ID=? AND Coalesce(M_CostElement_ID,0)=0", + matchInv.getReversal_ID(), M_AttributeSetInstance_ID, as.getC_AcctSchema_ID(), getTrxName()); + amtAsset = cd != null ? cd.getAmt().negate() : BigDecimal.ZERO; + amtVariance = ipv.subtract(amtAsset); + } + Trx trx = Trx.get(getTrxName(), false); Savepoint savepoint = null; boolean zeroQty = false; @@ -439,7 +497,7 @@ public class Doc_MatchInv extends Doc if (!MCostDetail.createMatchInvoice(as, m_invoiceLine.getAD_Org_ID(), m_invoiceLine.getM_Product_ID(), m_invoiceLine.getM_AttributeSetInstance_ID(), matchInv.getM_MatchInv_ID(), 0, - ipv, BigDecimal.ZERO, "Invoice Price Variance", getTrxName())) { + isStockCoverage ? amtAsset: ipv, BigDecimal.ZERO, "Invoice Price Variance", getTrxName())) { throw new RuntimeException("Failed to create cost detail record."); } } catch (SQLException e) { @@ -460,36 +518,57 @@ public class Doc_MatchInv extends Doc } } - String costingMethod = m_pc.getProduct().getCostingMethod(as); MAccount account = m_pc.getAccount(ProductCost.ACCTTYPE_P_Asset, as); if (m_pc.isService()) account = m_pc.getAccount(ProductCost.ACCTTYPE_P_Expense, as); if (X_M_Cost.COSTINGMETHOD_AveragePO.equals(costingMethod)) { - if (zeroQty) - account = m_pc.getAccount(ProductCost.ACCTTYPE_P_AverageCostVariance, as); - FactLine line = fact.createLine(null, - m_pc.getAccount(ProductCost.ACCTTYPE_P_IPV, as), - as.getC_Currency_ID(), ipv.negate()); - updateFactLine(line); - line.setQty(getQty().negate()); - - line = fact.createLine(null, account, as.getC_Currency_ID(), ipv); - updateFactLine(line); + FactLine varianceLine = null; + if (amtVariance.compareTo(Env.ZERO) != 0) + { + varianceLine = fact.createLine(null, + m_pc.getAccount(ProductCost.ACCTTYPE_P_AverageCostVariance, as), as.getC_Currency_ID(), + amtVariance); + updateFactLine(varianceLine); + + if (m_invoiceLine.getParent().getC_Currency_ID() != as.getC_Currency_ID()) + { + updateFactLineAmtSource(varianceLine, ipvSource.multiply(amtVariance).divide(ipv)); + } + } + if (amtAsset.compareTo(Env.ZERO) != 0) + { + FactLine line = fact.createLine(null, account, as.getC_Currency_ID(), amtAsset); + updateFactLine(line); + + if (m_invoiceLine.getParent().getC_Currency_ID() != as.getC_Currency_ID()) + { + updateFactLineAmtSource(line, ipvSource.multiply(amtAsset).divide(ipv)); + } + } } else if (X_M_Cost.COSTINGMETHOD_AverageInvoice.equals(costingMethod) && !zeroQty) { - FactLine line = fact.createLine(null, - m_pc.getAccount(ProductCost.ACCTTYPE_P_IPV, as), - as.getC_Currency_ID(), ipv.negate()); + //TODO test for avg Invoice costing method as here dropped posting of posting to IPV account + FactLine line = fact.createLine(null, account, as.getC_Currency_ID(), ipv); updateFactLine(line); - line.setQty(getQty().negate()); - line = fact.createLine(null, account, as.getC_Currency_ID(), ipv); - updateFactLine(line); + if (m_invoiceLine.getParent().getC_Currency_ID() != as.getC_Currency_ID()) + { + updateFactLineAmtSource(line, ipvSource); + } + }else{ + //For standard costing post to IPV account + FactLine pv = fact.createLine(null, + m_pc.getAccount(ProductCost.ACCTTYPE_P_IPV, as), + as.getC_Currency_ID(), ipv); + updateFactLine(pv); + if (m_invoiceLine.getParent().getC_Currency_ID() != as.getC_Currency_ID()) + { + updateFactLineAmtSource(pv, ipvSource); + } } } - /** - * Verify if the posting involves two or more organizations - * @return true if there are more than one org involved on the posting + /** Verify if the posting involves two or more organizations + @return true if there are more than one org involved on the posting */ private boolean isInterOrg(MAcctSchema as) { MAcctSchemaElement elementorg = as.getAcctSchemaElement(MAcctSchemaElement.ELEMENTTYPE_Organization); @@ -907,7 +986,8 @@ public class Doc_MatchInv extends Doc // Invoice Price Variance difference BigDecimal ipv = cr.getAcctBalance().add(dr.getAcctBalance()).negate(); - processInvoicePriceVariance(as, fact, ipv); + BigDecimal ipvSource = dr.getAmtSourceDr().subtract(cr.getAmtSourceCr()).negate(); + processInvoicePriceVariance(as, fact, ipv, ipvSource); if (log.isLoggable(Level.FINE)) log.fine("IPV=" + ipv + "; Balance=" + fact.getSourceBalance()); String error = createMatchInvCostDetail(as); @@ -1258,6 +1338,29 @@ public class Doc_MatchInv extends Doc factLine.setQty(getQty()); } + /** + * Invoice currency & acct schema currency are not same then update AmtSource value + * to avoid source not balanced error/ignore suspense balancing. + * + * @param factLine + * @param ipvSource + */ + protected void updateFactLineAmtSource(FactLine factLine, BigDecimal ipvSource) + { + // When only Rate differ then set Dr & Cr Source amount as zero. + factLine.setAmtSourceCr(Env.ZERO); + factLine.setAmtSourceDr(Env.ZERO); + + // Price is vary then set Source amount according to source variance + if (ipvSource.compareTo(Env.ZERO) != 0) + { + if (ipvSource.signum() < 0) + factLine.setAmtSourceCr(ipvSource); + else + factLine.setAmtSourceDr(ipvSource); + } + } + /** * Create Gain/Loss for invoice * @param as accounting schema diff --git a/org.adempiere.base/src/org/compiere/model/MAccount.java b/org.adempiere.base/src/org/compiere/model/MAccount.java index bd1682e19c..4317fc8240 100644 --- a/org.adempiere.base/src/org/compiere/model/MAccount.java +++ b/org.adempiere.base/src/org/compiere/model/MAccount.java @@ -276,6 +276,17 @@ public class MAccount extends X_C_ValidCombination implements ImmutablePOSupport * @return account */ public static MAccount get (X_Fact_Acct fa) + { + return get(fa, (String)null); + } + + /** + * Get from existing Accounting fact + * @param fa accounting fact + * @param trxName + * @return account + */ + public static MAccount get (X_Fact_Acct fa, String trxName) { MAccount acct = get (fa.getCtx(), fa.getAD_Client_ID(), fa.getAD_Org_ID(), fa.getC_AcctSchema_ID(), @@ -283,7 +294,7 @@ public class MAccount extends X_C_ValidCombination implements ImmutablePOSupport fa.getM_Product_ID(), fa.getC_BPartner_ID(), fa.getAD_OrgTrx_ID(), fa.getC_LocFrom_ID(), fa.getC_LocTo_ID(), fa.getC_SalesRegion_ID(), fa.getC_Project_ID(), fa.getC_Campaign_ID(), fa.getC_Activity_ID(), - fa.getUser1_ID(), fa.getUser2_ID(), fa.getUserElement1_ID(), fa.getUserElement2_ID()); + fa.getUser1_ID(), fa.getUser2_ID(), fa.getUserElement1_ID(), fa.getUserElement2_ID(), trxName); return acct; } // get diff --git a/org.adempiere.base/src/org/compiere/model/MFactAcct.java b/org.adempiere.base/src/org/compiere/model/MFactAcct.java index 1f179fbe8b..3cc4983be8 100644 --- a/org.adempiere.base/src/org/compiere/model/MFactAcct.java +++ b/org.adempiere.base/src/org/compiere/model/MFactAcct.java @@ -126,6 +126,7 @@ public class MFactAcct extends X_Fact_Acct sb.append(get_ID()).append("-Acct=").append(getAccount_ID()) .append(",Dr=").append(getAmtSourceDr()).append("|").append(getAmtAcctDr()) .append(",Cr=").append(getAmtSourceCr()).append("|").append(getAmtAcctCr()) + .append(",C_Currency_ID=").append(getC_Currency_ID()) .append ("]"); return sb.toString (); } // toString diff --git a/org.idempiere.test/src/org/idempiere/test/AbstractTestCase.java b/org.idempiere.test/src/org/idempiere/test/AbstractTestCase.java index cde0642607..0c1fd98fd1 100644 --- a/org.idempiere.test/src/org/idempiere/test/AbstractTestCase.java +++ b/org.idempiere.test/src/org/idempiere/test/AbstractTestCase.java @@ -24,10 +24,15 @@ **********************************************************************/ package org.idempiere.test; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; import static org.junit.jupiter.api.Assertions.fail; +import java.math.RoundingMode; import java.sql.SQLException; import java.sql.Timestamp; +import java.util.ArrayList; +import java.util.List; import java.util.Optional; import java.util.Properties; @@ -35,6 +40,7 @@ import org.adempiere.util.ServerContext; import org.compiere.Adempiere; import org.compiere.model.MAcctSchema; import org.compiere.model.MClientInfo; +import org.compiere.model.MFactAcct; import org.compiere.model.MRole; import org.compiere.util.Env; import org.compiere.util.Language; @@ -239,4 +245,104 @@ public abstract class AbstractTestCase { Adempiere.startup(false); } } + + /** + * Match expectedList against a list of MFactAcct records + * @param factAccts + * @param expectedList + */ + protected void assertFactAcctEntries(List factAccts, List expectedList) { + List found = new ArrayList(); + List matches = new ArrayList(); + expectedList.forEach(fa -> { + //LineId and account id match + List accountMatches = new ArrayList(); + //find exact match + for(MFactAcct mfa : factAccts) { + if (fa.account().getAccount().get_ID() == mfa.getAccount_ID()) { + if (fa.lineId() > 0 && fa.lineId() != mfa.getLine_ID()) + continue; + accountMatches.add(mfa); + if (fa.qty() != null && (mfa.getQty() == null || !mfa.getQty().setScale(fa.rounding(), RoundingMode.HALF_UP).equals(fa.qty().setScale(fa.rounding(), RoundingMode.HALF_UP)))) + continue; + if (fa.debit()) { + if (fa.accountedAmount() != null && !fa.accountedAmount().setScale(fa.rounding(), RoundingMode.HALF_UP).equals(mfa.getAmtAcctDr().setScale(fa.rounding(), RoundingMode.HALF_UP))) + continue; + if (fa.sourceAmount() != null && !fa.sourceAmount().setScale(fa.rounding(), RoundingMode.HALF_UP).equals(mfa.getAmtSourceDr().setScale(fa.rounding(), RoundingMode.HALF_UP))) + continue; + found.add(fa); + matches.add(mfa); + break; + } else { + if (fa.accountedAmount() != null && !fa.accountedAmount().setScale(fa.rounding(), RoundingMode.HALF_UP).equals(mfa.getAmtAcctCr().setScale(fa.rounding(), RoundingMode.HALF_UP))) + continue; + if (fa.sourceAmount() != null && !fa.sourceAmount().setScale(fa.rounding(), RoundingMode.HALF_UP).equals(mfa.getAmtSourceCr().setScale(fa.rounding(), RoundingMode.HALF_UP))) + continue; + found.add(fa); + matches.add(mfa); + break; + } + } + } + //assert qty mismatch + if (!found.contains(fa) && !accountMatches.isEmpty()) { + for(MFactAcct mfa : accountMatches) { + if (fa.debit()) { + if (fa.accountedAmount() != null && !fa.accountedAmount().setScale(fa.rounding(), RoundingMode.HALF_UP).equals(mfa.getAmtAcctDr().setScale(fa.rounding(), RoundingMode.HALF_UP))) + continue; + if (fa.sourceAmount() != null && !fa.sourceAmount().setScale(fa.rounding(), RoundingMode.HALF_UP).equals(mfa.getAmtSourceDr().setScale(fa.rounding(), RoundingMode.HALF_UP))) + continue; + } else { + if (fa.accountedAmount() != null && !fa.accountedAmount().setScale(fa.rounding(), RoundingMode.HALF_UP).equals(mfa.getAmtAcctCr().setScale(fa.rounding(), RoundingMode.HALF_UP))) + continue; + if (fa.sourceAmount() != null && !fa.sourceAmount().setScale(fa.rounding(), RoundingMode.HALF_UP).equals(mfa.getAmtSourceCr().setScale(fa.rounding(), RoundingMode.HALF_UP))) + continue; + } + assertEquals(fa.qty().setScale(fa.rounding(), RoundingMode.HALF_UP), mfa.getQty().setScale(fa.rounding(), RoundingMode.HALF_UP), "Unexpected Qty for " + fa); + found.add(fa); + } + } + }); + + //assert amount mismatch + expectedList.forEach(fa -> { + if (!found.contains(fa)) { + for(MFactAcct mfa : factAccts) { + if (matches.contains(mfa)) + continue; + if (fa.account().getAccount().get_ID() != mfa.getAccount_ID()) + continue; + if (fa.lineId() > 0 && fa.lineId() != mfa.getLine_ID()) + continue; + if (fa.debit()) { + if (fa.accountedAmount() != null && fa.accountedAmount().signum() == mfa.getAmtAcctDr().signum()) + assertEquals(fa.accountedAmount().setScale(fa.rounding(), RoundingMode.HALF_UP), mfa.getAmtAcctDr().setScale(fa.rounding(), RoundingMode.HALF_UP), "Unexpected Accounted Dr amount for " + fa); + else if (fa.accountedAmount() != null) + continue; + if (fa.sourceAmount() != null && fa.sourceAmount().signum() == mfa.getAmtSourceDr().signum()) + assertEquals(fa.accountedAmount().setScale(fa.rounding(), RoundingMode.HALF_UP), mfa.getAmtSourceDr().setScale(fa.rounding(), RoundingMode.HALF_UP), "Unexpected Source Dr amount for " + fa); + else if (fa.sourceAmount() != null) + continue; + } else { + if (fa.accountedAmount() != null && fa.accountedAmount().signum() == mfa.getAmtAcctCr().signum()) + assertEquals(fa.accountedAmount().setScale(fa.rounding(), RoundingMode.HALF_UP), mfa.getAmtAcctCr().setScale(fa.rounding(), RoundingMode.HALF_UP), "Unexpected Accounted Cr amount for " + fa); + else if (fa.accountedAmount() != null) + continue; + if (fa.sourceAmount() != null && fa.sourceAmount().signum() == mfa.getAmtSourceCr().signum()) + assertEquals(fa.accountedAmount().setScale(fa.rounding(), RoundingMode.HALF_UP), mfa.getAmtSourceCr().setScale(fa.rounding(), RoundingMode.HALF_UP), "Unexpected Source Cr amount for " + fa); + else if (fa.sourceAmount() != null) + continue; + } + if (fa.qty() != null && mfa.getQty() != null) + assertEquals(fa.qty().setScale(fa.rounding(), RoundingMode.HALF_UP), mfa.getQty().setScale(fa.rounding(), RoundingMode.HALF_UP), "Unexpected Qty for " + fa); + found.add(fa); + } + } + }); + + //assert not found + for(FactAcct factAcct : expectedList) { + assertTrue(found.contains(factAcct), "No fact acct record found for " + factAcct); + } + } } diff --git a/org.idempiere.test/src/org/idempiere/test/FactAcct.java b/org.idempiere.test/src/org/idempiere/test/FactAcct.java new file mode 100644 index 0000000000..c4d7db80f5 --- /dev/null +++ b/org.idempiere.test/src/org/idempiere/test/FactAcct.java @@ -0,0 +1,113 @@ +/*********************************************************************** + * This file is part of iDempiere ERP Open Source * + * http://www.idempiere.org * + * * + * Copyright (C) Contributors * + * * + * This program is free software; you can redistribute it and/or * + * modify it under the terms of the GNU General Public License * + * as published by the Free Software Foundation; either version 2 * + * of the License, or (at your option) any later version. * + * * + * This program is distributed in the hope that it will be useful, * + * but WITHOUT ANY WARRANTY; without even the implied warranty of * + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * + * GNU General Public License for more details. * + * * + * You should have received a copy of the GNU General Public License * + * along with this program; if not, write to the Free Software * + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, * + * MA 02110-1301, USA. * + **********************************************************************/ +package org.idempiere.test; + +import java.math.BigDecimal; +import java.math.RoundingMode; + +import org.compiere.model.MAccount; + +/** + * @param account + * @param accountedAmount + * @param sourceAmount + * @param rounding + * @param debit + * @param lineId + * @param qty + */ +public record FactAcct(MAccount account, BigDecimal accountedAmount, BigDecimal sourceAmount, int rounding, boolean debit, int lineId, BigDecimal qty) { + /** + * @param account + * @param accountedAmount + * @param sourceAmount + * @param rounding + * @param debit + * @param lineId + */ + public FactAcct(MAccount account, BigDecimal accountedAmount, BigDecimal sourceAmount, int rounding, boolean debit, int lineId) { + this(account, accountedAmount, sourceAmount, rounding, debit, lineId, (BigDecimal)null); + } + + /** + * @param account + * @param accountedAmount + * @param sourceAmount + * @param rounding + * @param debit + * @param qty + */ + public FactAcct(MAccount account, BigDecimal accountedAmount, BigDecimal sourceAmount, int rounding, boolean debit, BigDecimal qty) { + this(account, accountedAmount, sourceAmount, rounding, debit, 0, qty); + } + + /** + * @param account + * @param accountedAmount + * @param rounding + * @param debit + * @param qty + */ + public FactAcct(MAccount account, BigDecimal accountedAmount, int rounding, boolean debit, BigDecimal qty) { + this(account, accountedAmount, null, rounding, debit, 0, qty); + } + + /** + * @param account + * @param accountedAmount + * @param rounding + * @param debit + */ + public FactAcct(MAccount account, BigDecimal accountedAmount, int rounding, boolean debit) { + this(account, accountedAmount, null, rounding, debit, 0); + } + + /** + * @param account + * @param accountedAmount + * @param sourceAmount + * @param rounding + * @param debit + */ + public FactAcct(MAccount account, BigDecimal accountedAmount, BigDecimal sourceAmount, int rounding, boolean debit) { + this(account, accountedAmount, sourceAmount, rounding, debit, 0); + } + + @Override + public String toString() { + StringBuilder builder = new StringBuilder("Account=").append(account.getAccount_ID()).append(" | ") + .append(account.getAccount()); + builder.append(", ").append(debit ? "Dr" : "Cr").append("["); + if (accountedAmount != null) + builder.append("Accounted=").append(accountedAmount.setScale(rounding, RoundingMode.HALF_UP).toPlainString()); + if (sourceAmount != null) { + if (accountedAmount != null) builder.append(", "); + builder.append("Source=").append(sourceAmount.setScale(rounding, RoundingMode.HALF_UP).toPlainString()); + } + builder.append("]"); + if (lineId > 0) + builder.append(", LineId=").append(lineId); + if (qty != null) + builder.append(", Qty=").append(qty.setScale(rounding, RoundingMode.HALF_UP).toPlainString()); + return builder.toString(); + } +} diff --git a/org.idempiere.test/src/org/idempiere/test/base/InOutTest.java b/org.idempiere.test/src/org/idempiere/test/base/InOutTest.java index ea63d3a786..f27af152d8 100644 --- a/org.idempiere.test/src/org/idempiere/test/base/InOutTest.java +++ b/org.idempiere.test/src/org/idempiere/test/base/InOutTest.java @@ -31,6 +31,7 @@ import static org.junit.jupiter.api.Assertions.assertTrue; import java.math.BigDecimal; import java.math.RoundingMode; import java.sql.Timestamp; +import java.util.Arrays; import java.util.List; import org.compiere.acct.Doc; @@ -70,6 +71,7 @@ import org.compiere.wf.MWorkflow; import org.idempiere.test.AbstractTestCase; import org.idempiere.test.ConversionRateHelper; import org.idempiere.test.DictionaryIDs; +import org.idempiere.test.FactAcct; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.parallel.ResourceLock; @@ -146,23 +148,14 @@ public class InOutTest extends AbstractTestCase { doc.setC_BPartner_ID(receipt.getC_BPartner_ID()); MAccount acctNIR = doc.getAccount(Doc.ACCTTYPE_NotInvoicedReceipts, as); - String whereClause = MFactAcct.COLUMNNAME_AD_Table_ID + "=" + MInOut.Table_ID - + " AND " + MFactAcct.COLUMNNAME_Record_ID + "=" + receipt.get_ID() - + " AND " + MFactAcct.COLUMNNAME_C_AcctSchema_ID + "=" + as.getC_AcctSchema_ID(); - int[] ids = MFactAcct.getAllIDs(MFactAcct.Table_Name, whereClause, getTrxName()); - for (int id : ids) { - MFactAcct fa = new MFactAcct(Env.getCtx(), id, getTrxName()); - if (acctNIR.getAccount_ID() == fa.getAccount_ID()) { - if (receiptLine.get_ID() == fa.getLine_ID()) { - BigDecimal acctSource = orderLine.getPriceActual().multiply(receiptLine.getMovementQty()) - .setScale(as.getC_Currency().getStdPrecision(), RoundingMode.HALF_UP); - BigDecimal acctAmount = acctSource.multiply(rate) - .setScale(as.getC_Currency().getStdPrecision(), RoundingMode.HALF_UP); - assertTrue(fa.getAmtSourceCr().compareTo(acctSource) == 0, fa.getAmtSourceCr().toPlainString() + " != " + acctSource.toPlainString()); - assertTrue(fa.getAmtAcctCr().compareTo(acctAmount) == 0, fa.getAmtAcctCr().toPlainString() + " != " + acctAmount.toPlainString()); - } - } - } + BigDecimal acctSource = orderLine.getPriceActual().multiply(receiptLine.getMovementQty()) + .setScale(as.getC_Currency().getStdPrecision(), RoundingMode.HALF_UP); + BigDecimal acctAmount = acctSource.multiply(rate) + .setScale(as.getC_Currency().getStdPrecision(), RoundingMode.HALF_UP); + Query query = MFactAcct.createRecordIdQuery(MInOut.Table_ID, receipt.get_ID(), as.getC_AcctSchema_ID(), getTrxName()); + List factAccts = query.list(); + List expected = Arrays.asList(new FactAcct(acctNIR, acctAmount, acctSource, as.getC_Currency().getStdPrecision(), false, receiptLine.get_ID())); + assertFactAcctEntries(factAccts, expected); } order = createPurchaseOrder(bpartner, currentDate, priceList.getM_PriceList_ID(), Spot_ConversionType_ID); @@ -185,23 +178,14 @@ public class InOutTest extends AbstractTestCase { doc.setC_BPartner_ID(receipt.getC_BPartner_ID()); MAccount acctNIR = doc.getAccount(Doc.ACCTTYPE_NotInvoicedReceipts, as); - String whereClause = MFactAcct.COLUMNNAME_AD_Table_ID + "=" + MInOut.Table_ID - + " AND " + MFactAcct.COLUMNNAME_Record_ID + "=" + receipt.get_ID() - + " AND " + MFactAcct.COLUMNNAME_C_AcctSchema_ID + "=" + as.getC_AcctSchema_ID(); - int[] ids = MFactAcct.getAllIDs(MFactAcct.Table_Name, whereClause, getTrxName()); - for (int id : ids) { - MFactAcct fa = new MFactAcct(Env.getCtx(), id, getTrxName()); - if (acctNIR.getAccount_ID() == fa.getAccount_ID()) { - if (receiptLine.get_ID() == fa.getLine_ID()) { - BigDecimal acctSource = orderLine.getPriceActual().multiply(receiptLine.getMovementQty()) + BigDecimal acctSource = orderLine.getPriceActual().multiply(receiptLine.getMovementQty()) .setScale(as.getC_Currency().getStdPrecision(), RoundingMode.HALF_UP); - BigDecimal acctAmount = acctSource.multiply(rate) - .setScale(as.getC_Currency().getStdPrecision(), RoundingMode.HALF_UP); - assertTrue(fa.getAmtSourceCr().compareTo(acctSource) == 0, fa.getAmtSourceCr().toPlainString() + " != " + acctSource.toPlainString()); - assertTrue(fa.getAmtAcctCr().compareTo(acctAmount) == 0, fa.getAmtAcctCr().toPlainString() + " != " + acctAmount.toPlainString()); - } - } - } + BigDecimal acctAmount = acctSource.multiply(rate) + .setScale(as.getC_Currency().getStdPrecision(), RoundingMode.HALF_UP); + Query query = MFactAcct.createRecordIdQuery(MInOut.Table_ID, receipt.get_ID(), as.getC_AcctSchema_ID(), getTrxName()); + List factAccts = query.list(); + List expected = Arrays.asList(new FactAcct(acctNIR, acctAmount, acctSource, as.getC_Currency().getStdPrecision(), false, receiptLine.get_ID())); + assertFactAcctEntries(factAccts, expected); } } finally { rollback(); @@ -276,20 +260,14 @@ public class InOutTest extends AbstractTestCase { doc.setC_BPartner_ID(receipt.getC_BPartner_ID()); MAccount acctNIR = doc.getAccount(Doc.ACCTTYPE_NotInvoicedReceipts, as); + BigDecimal acctSource = orderLine.getPriceActual().multiply(receiptLine.getMovementQty()) + .setScale(as.getC_Currency().getStdPrecision(), RoundingMode.HALF_UP); + BigDecimal acctAmount = acctSource.multiply(rate) + .setScale(as.getC_Currency().getStdPrecision(), RoundingMode.HALF_UP); Query query = MFactAcct.createRecordIdQuery(MInOut.Table_ID, receipt.get_ID(), as.getC_AcctSchema_ID(), getTrxName()); List fas = query.list(); - for (MFactAcct fa : fas) { - if (acctNIR.getAccount_ID() == fa.getAccount_ID()) { - if (receiptLine.get_ID() == fa.getLine_ID()) { - BigDecimal acctSource = orderLine.getPriceActual().multiply(receiptLine.getMovementQty()) - .setScale(as.getC_Currency().getStdPrecision(), RoundingMode.HALF_UP); - BigDecimal acctAmount = acctSource.multiply(rate) - .setScale(as.getC_Currency().getStdPrecision(), RoundingMode.HALF_UP); - assertTrue(fa.getAmtSourceCr().compareTo(acctSource) == 0, fa.getAmtSourceCr().toPlainString() + " != " + acctSource.toPlainString()); - assertTrue(fa.getAmtAcctCr().compareTo(acctAmount) == 0, fa.getAmtAcctCr().toPlainString() + " != " + acctAmount.toPlainString()); - } - } - } + List expected = Arrays.asList(new FactAcct(acctNIR, acctAmount, acctSource, 2, false, receiptLine.get_ID())); + assertFactAcctEntries(fas, expected); } MRMA rma = new MRMA(Env.getCtx(), 0, getTrxName()); @@ -343,19 +321,14 @@ public class InOutTest extends AbstractTestCase { doc.setC_BPartner_ID(delivery.getC_BPartner_ID()); MAccount acctNIR = doc.getAccount(Doc.ACCTTYPE_NotInvoicedReceipts, as); + BigDecimal acctSource = orderLine.getPriceActual().multiply(deliveryLine.getMovementQty()) + .setScale(as.getC_Currency().getStdPrecision(), RoundingMode.HALF_UP); + BigDecimal acctAmount = acctSource.multiply(rate) + .setScale(as.getC_Currency().getStdPrecision(), RoundingMode.HALF_UP); Query query = MFactAcct.createRecordIdQuery(MInOut.Table_ID, delivery.get_ID(), as.getC_AcctSchema_ID(), getTrxName()); List fas = query.list(); - for (MFactAcct fa : fas) { - if (acctNIR.getAccount_ID() == fa.getAccount_ID()) { - if (deliveryLine.get_ID() == fa.getLine_ID()) { - BigDecimal acctSource = orderLine.getPriceActual().multiply(deliveryLine.getMovementQty()) - .setScale(as.getC_Currency().getStdPrecision(), RoundingMode.HALF_UP); - BigDecimal acctAmount = acctSource.multiply(rate) - .setScale(as.getC_Currency().getStdPrecision(), RoundingMode.HALF_UP); - assertTrue(fa.getAmtAcctDr().compareTo(acctAmount) == 0, fa.getAmtAcctDr().toPlainString() + " != " + acctAmount.toPlainString()); - } - } - } + List expected = Arrays.asList(new FactAcct(acctNIR, acctAmount, null, 2, true, deliveryLine.get_ID())); + assertFactAcctEntries(fas, expected); } } finally { rollback(); diff --git a/org.idempiere.test/src/org/idempiere/test/base/MatchInv2ndAcctSchemaTest.java b/org.idempiere.test/src/org/idempiere/test/base/MatchInv2ndAcctSchemaTest.java index 360fb087c4..ceb22ca0cb 100644 --- a/org.idempiere.test/src/org/idempiere/test/base/MatchInv2ndAcctSchemaTest.java +++ b/org.idempiere.test/src/org/idempiere/test/base/MatchInv2ndAcctSchemaTest.java @@ -34,6 +34,7 @@ import java.sql.Timestamp; import java.util.ArrayList; import java.util.Calendar; import java.util.HashMap; +import java.util.List; import org.compiere.acct.Doc; import org.compiere.acct.DocManager; @@ -59,6 +60,7 @@ import org.compiere.model.MProductPrice; import org.compiere.model.MWarehouse; import org.compiere.model.PO; import org.compiere.model.ProductCost; +import org.compiere.model.Query; import org.compiere.process.DocAction; import org.compiere.process.DocumentEngine; import org.compiere.process.ProcessInfo; @@ -1840,12 +1842,9 @@ public class MatchInv2ndAcctSchemaTest extends AbstractTestCase { BigDecimal totalNIRAmtAcct = Env.ZERO; BigDecimal totalInvClrAmtAcct = Env.ZERO; - String whereClause = MFactAcct.COLUMNNAME_AD_Table_ID + "=" + MMatchInv.Table_ID - + " AND " + MFactAcct.COLUMNNAME_Record_ID + "=" + mi.get_ID() - + " AND " + MFactAcct.COLUMNNAME_C_AcctSchema_ID + "=" + as.getC_AcctSchema_ID(); - int[] ids = MFactAcct.getAllIDs(MFactAcct.Table_Name, whereClause, getTrxName()); - for (int id : ids) { - MFactAcct fa = new MFactAcct(Env.getCtx(), id, getTrxName()); + Query query = MFactAcct.createRecordIdQuery(MMatchInv.Table_ID, mi.get_ID(), as.getC_AcctSchema_ID(), getTrxName()); + List factAccts = query.list(); + for (MFactAcct fa : factAccts) { if (acctNIR.getAccount_ID() == fa.getAccount_ID()) totalNIRAmtAcct = totalNIRAmtAcct.add(fa.getAmtAcctDr()).subtract(fa.getAmtAcctCr()); else if (acctInvClr.getAccount_ID() == fa.getAccount_ID()) 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 6cb12b5b0f..84e31de68f 100644 --- a/org.idempiere.test/src/org/idempiere/test/base/MatchInvTest.java +++ b/org.idempiere.test/src/org/idempiere/test/base/MatchInvTest.java @@ -31,6 +31,8 @@ import static org.junit.jupiter.api.Assertions.assertTrue; import java.math.BigDecimal; import java.math.RoundingMode; import java.sql.Timestamp; +import java.util.Arrays; +import java.util.List; import org.compiere.acct.Doc; import org.compiere.acct.DocManager; @@ -63,9 +65,11 @@ import org.compiere.process.DocAction; import org.compiere.process.DocumentEngine; import org.compiere.process.ProcessInfo; import org.compiere.util.Env; +import org.compiere.util.TimeUtil; import org.compiere.wf.MWorkflow; import org.idempiere.test.AbstractTestCase; import org.idempiere.test.DictionaryIDs; +import org.idempiere.test.FactAcct; import org.junit.jupiter.api.Test; /** @@ -223,17 +227,10 @@ public class MatchInvTest extends AbstractTestCase { ProductCost pc = new ProductCost (Env.getCtx(), mi.getM_Product_ID(), mi.getM_AttributeSetInstance_ID(), getTrxName()); MAccount acctInvClr = pc.getAccount(ProductCost.ACCTTYPE_P_InventoryClearing, as); - String whereClause = MFactAcct.COLUMNNAME_AD_Table_ID + "=" + MMatchInv.Table_ID - + " AND " + MFactAcct.COLUMNNAME_Record_ID + "=" + mi.get_ID() - + " AND " + MFactAcct.COLUMNNAME_C_AcctSchema_ID + "=" + as.getC_AcctSchema_ID(); - int[] ids = MFactAcct.getAllIDs(MFactAcct.Table_Name, whereClause, getTrxName()); - for (int id : ids) { - MFactAcct fa = new MFactAcct(Env.getCtx(), id, getTrxName()); - if (fa.getAccount_ID() == acctNIR.getAccount_ID()) - assertEquals(fa.getAmtAcctCr().setScale(2, RoundingMode.HALF_UP), credMatchAmt.setScale(2, RoundingMode.HALF_UP), "MatchInv incorrect amount posted "+fa.getAmtAcctCr().toPlainString()); - else if (fa.getAccount_ID() == acctInvClr.getAccount_ID()) - assertEquals(fa.getAmtAcctDr().setScale(2, RoundingMode.HALF_UP), credMatchAmt.setScale(2, RoundingMode.HALF_UP), "MatchInv incorrect amount posted "+fa.getAmtAcctDr().toPlainString()); - } + Query query = MFactAcct.createRecordIdQuery(MMatchInv.Table_ID, mi.get_ID(), as.getC_AcctSchema_ID(), getTrxName()); + List factAccts = query.list(); + List expected = Arrays.asList(new FactAcct(acctNIR, credMatchAmt, 2, false), new FactAcct(acctInvClr, credMatchAmt, 2, true)); + assertFactAcctEntries(factAccts, expected); } rollback(); @@ -334,17 +331,10 @@ public class MatchInvTest extends AbstractTestCase { ProductCost pc = new ProductCost (Env.getCtx(), mi.getM_Product_ID(), mi.getM_AttributeSetInstance_ID(), getTrxName()); MAccount acctInvClr = pc.getAccount(ProductCost.ACCTTYPE_P_InventoryClearing, as); - String whereClause = MFactAcct.COLUMNNAME_AD_Table_ID + "=" + MMatchInv.Table_ID - + " AND " + MFactAcct.COLUMNNAME_Record_ID + "=" + mi.get_ID() - + " AND " + MFactAcct.COLUMNNAME_C_AcctSchema_ID + "=" + as.getC_AcctSchema_ID(); - int[] ids = MFactAcct.getAllIDs(MFactAcct.Table_Name, whereClause, getTrxName()); - for (int id : ids) { - MFactAcct fa = new MFactAcct(Env.getCtx(), id, getTrxName()); - if (fa.getAccount_ID() == acctNIR.getAccount_ID()) - assertEquals(fa.getAmtAcctDr().setScale(2, RoundingMode.HALF_UP), invMatchAmt.setScale(2, RoundingMode.HALF_UP), "MatchInv incorrect amount posted "+fa.getAmtAcctCr().toPlainString()); - else if (fa.getAccount_ID() == acctInvClr.getAccount_ID()) - assertEquals(fa.getAmtAcctCr().setScale(2, RoundingMode.HALF_UP), invMatchAmt.setScale(2, RoundingMode.HALF_UP), "MatchInv incorrect amount posted "+fa.getAmtAcctCr().toPlainString()); - } + Query query = MFactAcct.createRecordIdQuery(MMatchInv.Table_ID, mi.get_ID(), as.getC_AcctSchema_ID(), getTrxName()); + List factAccts = query.list(); + List expected = Arrays.asList(new FactAcct(acctNIR, invMatchAmt, 2, true), new FactAcct(acctInvClr, invMatchAmt, 2, false)); + assertFactAcctEntries(factAccts, expected); } rollback(); @@ -452,21 +442,11 @@ public class MatchInvTest extends AbstractTestCase { ProductCost pc = new ProductCost (Env.getCtx(), mi.getM_Product_ID(), mi.getM_AttributeSetInstance_ID(), getTrxName()); MAccount acctInvClr = pc.getAccount(ProductCost.ACCTTYPE_P_InventoryClearing, as); - String whereClause = MFactAcct.COLUMNNAME_AD_Table_ID + "=" + MMatchInv.Table_ID - + " AND " + MFactAcct.COLUMNNAME_Record_ID + "=" + mi.get_ID() - + " AND " + MFactAcct.COLUMNNAME_C_AcctSchema_ID + "=" + as.getC_AcctSchema_ID(); - int[] ids = MFactAcct.getAllIDs(MFactAcct.Table_Name, whereClause, getTrxName()); - for (int id : ids) { - MFactAcct fa = new MFactAcct(Env.getCtx(), id, getTrxName()); - if (fa.getAccount_ID() == acctNIR.getAccount_ID()) { - assertEquals(fa.getAmtAcctDr().setScale(2, RoundingMode.HALF_UP), invMatchAmt.setScale(2, RoundingMode.HALF_UP), "MatchInv incorrect amount posted "+fa.getAmtAcctDr().toPlainString()); - assertEquals(mi.getQty(), fa.getQty(), "Accounting fact quantity incorrect"); - } - else if (fa.getAccount_ID() == acctInvClr.getAccount_ID()) { - assertEquals(fa.getAmtAcctCr().setScale(2, RoundingMode.HALF_UP), invMatchAmt.setScale(2, RoundingMode.HALF_UP), "MatchInv incorrect amount posted "+fa.getAmtAcctCr().toPlainString()); - assertEquals(mi.getQty().negate().setScale(2, RoundingMode.HALF_UP), fa.getQty().setScale(2, RoundingMode.HALF_UP), "Accounting fact quantity incorrect"); - } - } + Query query = MFactAcct.createRecordIdQuery(MMatchInv.Table_ID, mi.get_ID(), as.getC_AcctSchema_ID(), getTrxName()); + List factAccts = query.list(); + List expected = Arrays.asList(new FactAcct(acctInvClr, invMatchAmt, 2, false, mi.getQty().negate()), + new FactAcct(acctNIR, invMatchAmt, 2, true, mi.getQty())); + assertFactAcctEntries(factAccts, expected); } MInvoice creditMemo = new MInvoice(Env.getCtx(), 0, getTrxName()); @@ -513,26 +493,11 @@ public class MatchInvTest extends AbstractTestCase { ProductCost pc = new ProductCost (Env.getCtx(), mi.getM_Product_ID(), mi.getM_AttributeSetInstance_ID(), getTrxName()); MAccount acctInvClr = pc.getAccount(ProductCost.ACCTTYPE_P_InventoryClearing, as); - BigDecimal amtAcctDrInvClr = BigDecimal.ZERO; - BigDecimal amtAcctCrInvClr = BigDecimal.ZERO; - String whereClause = MFactAcct.COLUMNNAME_AD_Table_ID + "=" + MMatchInv.Table_ID - + " AND " + MFactAcct.COLUMNNAME_Record_ID + "=" + mi.get_ID() - + " AND " + MFactAcct.COLUMNNAME_C_AcctSchema_ID + "=" + as.getC_AcctSchema_ID(); - int[] ids = MFactAcct.getAllIDs(MFactAcct.Table_Name, whereClause, getTrxName()); - for (int id : ids) { - MFactAcct fa = new MFactAcct(Env.getCtx(), id, getTrxName()); - if (fa.getAccount_ID() == acctInvClr.getAccount_ID() && fa.getQty().compareTo(BigDecimal.ZERO) < 0) { - assertEquals(fa.getAmtAcctCr().setScale(2, RoundingMode.HALF_UP), credMatchAmt.setScale(2, RoundingMode.HALF_UP), "MatchInv incorrect amount posted "+fa.getAmtAcctCr().toPlainString()); - amtAcctCrInvClr = amtAcctCrInvClr.add(fa.getAmtAcctCr()); - assertEquals(mi.getQty(), fa.getQty(), "Accounting fact quantity incorrect"); - } - else if (fa.getAccount_ID() == acctInvClr.getAccount_ID() && fa.getQty().compareTo(BigDecimal.ZERO) > 0) { - assertEquals(fa.getAmtAcctDr().setScale(2, RoundingMode.HALF_UP), credMatchAmt.setScale(2, RoundingMode.HALF_UP), "MatchInv incorrect amount posted "+fa.getAmtAcctDr().toPlainString()); - amtAcctDrInvClr = amtAcctDrInvClr.add(fa.getAmtAcctDr()); - assertEquals(mi.getQty().negate(), fa.getQty(), "Accounting fact quantity incorrect"); - } - } - assertTrue(amtAcctDrInvClr.compareTo(amtAcctCrInvClr) == 0); + Query query = MFactAcct.createRecordIdQuery(MMatchInv.Table_ID, mi.get_ID(), as.getC_AcctSchema_ID(), getTrxName()); + List factAccts = query.list(); + List expected = Arrays.asList(new FactAcct(acctInvClr, credMatchAmt, 2, false, mi.getQty()), + new FactAcct(acctInvClr, credMatchAmt, 2, true, mi.getQty().negate())); + assertFactAcctEntries(factAccts, expected); } rollback(); @@ -679,25 +644,14 @@ public class MatchInvTest extends AbstractTestCase { ProductCost pc = new ProductCost (Env.getCtx(), mi.getM_Product_ID(), mi.getM_AttributeSetInstance_ID(), getTrxName()); MAccount acctInvClr = pc.getAccount(ProductCost.ACCTTYPE_P_InventoryClearing, as); - String whereClause = MFactAcct.COLUMNNAME_AD_Table_ID + "=" + MMatchInv.Table_ID - + " AND " + MFactAcct.COLUMNNAME_Record_ID + "=" + mi.get_ID() - + " AND " + MFactAcct.COLUMNNAME_C_AcctSchema_ID + "=" + as.getC_AcctSchema_ID(); - int[] ids = MFactAcct.getAllIDs(MFactAcct.Table_Name, whereClause, getTrxName()); - for (int id : ids) { - MFactAcct fa = new MFactAcct(Env.getCtx(), id, getTrxName()); - if (fa.getAccount_ID() == acctNIR.getAccount_ID()) { - assertTrue(fa.getAmtAcctDr().compareTo(Env.ZERO) >= 0); - assertEquals(acctAmount, fa.getAmtAcctDr(), fa.getAmtAcctDr().toPlainString() + " != " + acctAmount.toPlainString()); - // verify source amt and currency - assertTrue(fa.getC_Currency_ID() == japaneseYen.getC_Currency_ID()); - assertEquals(acctSource, fa.getAmtSourceDr(), fa.getAmtSourceDr().toPlainString() + " != " + acctSource.toPlainString()); - } else if (fa.getAccount_ID() == acctInvClr.getAccount_ID()) { - assertTrue(fa.getAmtAcctCr().compareTo(Env.ZERO) >= 0); - assertEquals(acctAmount, fa.getAmtAcctCr(), fa.getAmtAcctCr().toPlainString() + " != " + acctAmount.toPlainString()); - // verify source amt and currency - assertTrue(fa.getC_Currency_ID() == japaneseYen.getC_Currency_ID()); - assertEquals(acctSource, fa.getAmtSourceCr(), fa.getAmtSourceCr().toPlainString() + " != " + acctSource.toPlainString()); - } + Query query = MFactAcct.createRecordIdQuery(MMatchInv.Table_ID, mi.get_ID(), as.getC_AcctSchema_ID(), getTrxName()); + List factAccts = query.list(); + List expected = Arrays.asList(new FactAcct(acctNIR, acctAmount, acctSource, 2, true), + new FactAcct(acctInvClr, acctAmount, acctSource, 2, false)); + assertFactAcctEntries(factAccts, expected); + assertTrue(acctAmount.compareTo(Env.ZERO) >= 0); + for (MFactAcct fa : factAccts) { + assertTrue(fa.getC_Currency_ID() == japaneseYen.getC_Currency_ID()); } } } finally { @@ -1042,26 +996,205 @@ public class MatchInvTest extends AbstractTestCase { ProductCost pc = new ProductCost (Env.getCtx(), mi.getM_Product_ID(), mi.getM_AttributeSetInstance_ID(), getTrxName()); MAccount acctInvClr = pc.getAccount(ProductCost.ACCTTYPE_P_InventoryClearing, as); - String whereClause = MFactAcct.COLUMNNAME_AD_Table_ID + "=" + MMatchInv.Table_ID - + " AND " + MFactAcct.COLUMNNAME_Record_ID + "=" + mi.get_ID() - + " AND " + MFactAcct.COLUMNNAME_C_AcctSchema_ID + "=" + as.getC_AcctSchema_ID(); - int[] ids = MFactAcct.getAllIDs(MFactAcct.Table_Name, whereClause, getTrxName()); - for (int id : ids) { - MFactAcct fa = new MFactAcct(Env.getCtx(), id, getTrxName()); - if (fa.getAccount_ID() == acctNIR.getAccount_ID()) { - if (mi.isReversal()) - assertEquals(fa.getAmtAcctCr().setScale(2, RoundingMode.HALF_UP), invMatchAmt.setScale(2, RoundingMode.HALF_UP), "MatchInv incorrect amount posted "); - else - assertEquals(fa.getAmtAcctDr().setScale(2, RoundingMode.HALF_UP), invMatchAmt.setScale(2, RoundingMode.HALF_UP), "MatchInv incorrect amount posted "); - assertEquals(mi.getQty(), fa.getQty(), "Accounting fact quantity incorrect"); + Query query = MFactAcct.createRecordIdQuery(MMatchInv.Table_ID, mi.get_ID(), as.getC_AcctSchema_ID(), getTrxName()); + List factAccts = query.list(); + List expected = Arrays.asList(new FactAcct(acctNIR, invMatchAmt, 2, !mi.isReversal(), mi.getQty()), + new FactAcct(acctInvClr, invMatchAmt, 2, mi.isReversal(), mi.getQty().negate())); + assertFactAcctEntries(factAccts, expected); + } + } + + @Test + public void testReversalPostingWithZeroOnHand() { + MBPartner bpartner = MBPartner.get(Env.getCtx(), DictionaryIDs.C_BPartner.TREE_FARM.id); // Tree Farm Inc. + + MProduct product = null; + MClient client = MClient.get(Env.getCtx()); + MAcctSchema as = client.getAcctSchema(); + Timestamp today = TimeUtil.getDay(null); + try { + product = new MProduct(Env.getCtx(), 0, null); + product.setM_Product_Category_ID(DictionaryIDs.M_Product_Category.STANDARD.id); + product.setName("testReversalPostingWithZeroOnHand"); + product.setProductType(MProduct.PRODUCTTYPE_Item); + product.setIsStocked(true); + product.setIsSold(true); + product.setIsPurchased(true); + product.setC_UOM_ID(DictionaryIDs.C_UOM.EACH.id); + product.setC_TaxCategory_ID(DictionaryIDs.C_TaxCategory.STANDARD.id); + product.saveEx(); + + MPriceListVersion plv = MPriceList.get(DictionaryIDs.M_PriceList.PURCHASE.id).getPriceListVersion(null); + MProductPrice pp = new MProductPrice(Env.getCtx(), 0, getTrxName()); + pp.setM_PriceList_Version_ID(plv.getM_PriceList_Version_ID()); + pp.setM_Product_ID(product.get_ID()); + BigDecimal orderPrice = new BigDecimal("2.00"); + pp.setPriceStd(orderPrice); + pp.setPriceList(orderPrice); + pp.saveEx(); + + int purchaseId = DictionaryIDs.M_PriceList.PURCHASE.id; // Purchase Price List + MOrder order = new MOrder(Env.getCtx(), 0, getTrxName()); + order.setBPartner(bpartner); + order.setIsSOTrx(false); + order.setC_DocTypeTarget_ID(); + order.setM_PriceList_ID(purchaseId); + order.setDocStatus(DocAction.STATUS_Drafted); + order.setDocAction(DocAction.ACTION_Complete); + order.saveEx(); + + MOrderLine orderLine = new MOrderLine(order); + orderLine.setLine(10); + orderLine.setProduct(product); + orderLine.setQty(BigDecimal.TEN); + orderLine.saveEx(); + + ProcessInfo info = MWorkflow.runDocumentActionWorkflow(order, DocAction.ACTION_Complete); + order.load(getTrxName()); + assertFalse(info.isError(), info.getSummary()); + assertEquals(DocAction.STATUS_Completed, order.getDocStatus()); + + MInOut receipt = new MInOut(order, DictionaryIDs.C_DocType.MM_RECEIPT.id, order.getDateOrdered()); // MM Receipt + receipt.saveEx(); + + MInOutLine receiptLine = new MInOutLine(receipt); + receiptLine.setC_OrderLine_ID(orderLine.get_ID()); + receiptLine.setLine(10); + receiptLine.setProduct(product); + receiptLine.setQty(orderLine.getQtyOrdered()); + MWarehouse wh = MWarehouse.get(Env.getCtx(), receipt.getM_Warehouse_ID()); + int M_Locator_ID = wh.getDefaultLocator().getM_Locator_ID(); + receiptLine.setM_Locator_ID(M_Locator_ID); + receiptLine.saveEx(); + + info = MWorkflow.runDocumentActionWorkflow(receipt, DocAction.ACTION_Complete); + receipt.load(getTrxName()); + assertFalse(info.isError(), info.getSummary()); + assertEquals(DocAction.STATUS_Completed, receipt.getDocStatus()); + + if (!receipt.isPosted()) { + String error = DocumentEngine.postImmediate(Env.getCtx(), receipt.getAD_Client_ID(), MInOut.Table_ID, receipt.get_ID(), false, getTrxName()); + assertTrue(error == null); + } + receipt.load(getTrxName()); + assertTrue(receipt.isPosted()); + + //customer shipment + MOrder salesOrder = new MOrder(Env.getCtx(), 0, getTrxName()); + salesOrder.setBPartner(MBPartner.get(Env.getCtx(), DictionaryIDs.C_BPartner.JOE_BLOCK.id)); + salesOrder.setC_DocTypeTarget_ID(MOrder.DocSubTypeSO_Standard); + salesOrder.setDeliveryRule(MOrder.DELIVERYRULE_CompleteOrder); + salesOrder.setDocStatus(DocAction.STATUS_Drafted); + salesOrder.setDocAction(DocAction.ACTION_Complete); + salesOrder.setDatePromised(today); + salesOrder.saveEx(); + + MOrderLine salesLine1 = new MOrderLine(salesOrder); + salesLine1.setLine(10); + salesLine1.setProduct(product); + salesLine1.setQty(orderLine.getQtyOrdered()); + salesLine1.setDatePromised(today); + salesLine1.saveEx(); + + info = MWorkflow.runDocumentActionWorkflow(salesOrder, DocAction.ACTION_Complete); + salesOrder.load(getTrxName()); + assertFalse(info.isError(), info.getSummary()); + assertEquals(DocAction.STATUS_Completed, salesOrder.getDocStatus(), "Unexpected Document Status"); + + MInOut shipment = new MInOut(salesOrder, DictionaryIDs.C_DocType.MM_SHIPMENT.id, salesOrder.getDateOrdered()); + shipment.setDocStatus(DocAction.STATUS_Drafted); + shipment.setDocAction(DocAction.ACTION_Complete); + shipment.saveEx(); + + MInOutLine shipmentLine = new MInOutLine(shipment); + shipmentLine.setOrderLine(salesLine1, 0, orderLine.getQtyOrdered()); + shipmentLine.setQty(orderLine.getQtyOrdered()); + shipmentLine.saveEx(); + + info = MWorkflow.runDocumentActionWorkflow(shipment, DocAction.ACTION_Complete); + assertFalse(info.isError(), info.getSummary()); + shipment.load(getTrxName()); + assertEquals(DocAction.STATUS_Completed, shipment.getDocStatus(), "Unexpected Document Status"); + + MInvoice invoice = new MInvoice(receipt, receipt.getMovementDate()); + invoice.setC_DocTypeTarget_ID(MDocType.DOCBASETYPE_APInvoice); + invoice.setDocStatus(DocAction.STATUS_Drafted); + invoice.setDocAction(DocAction.ACTION_Complete); + invoice.saveEx(); + + //MR invoice + MInvoiceLine invoiceLine = new MInvoiceLine(invoice); + invoiceLine.setM_InOutLine_ID(receiptLine.get_ID()); + invoiceLine.setLine(10); + invoiceLine.setProduct(product); + invoiceLine.setQty(orderLine.getQtyOrdered()); + invoiceLine.setPrice(orderPrice.add(BigDecimal.ONE)); + invoiceLine.saveEx(); + + info = MWorkflow.runDocumentActionWorkflow(invoice, DocAction.ACTION_Complete); + invoice.load(getTrxName()); + assertFalse(info.isError(), info.getSummary()); + assertEquals(DocAction.STATUS_Completed, invoice.getDocStatus()); + + if (!invoice.isPosted()) { + String error = DocumentEngine.postImmediate(Env.getCtx(), invoice.getAD_Client_ID(), MInvoice.Table_ID, invoice.get_ID(), false, getTrxName()); + assertTrue(error == null, error); + } + BigDecimal invMatchAmt = invoiceLine.getMatchedQty().multiply(invoiceLine.getPriceActual()).setScale(as.getStdPrecision(), RoundingMode.HALF_UP); + BigDecimal poMatchAmt = invoiceLine.getMatchedQty().multiply(orderLine.getPriceActual()).setScale(as.getStdPrecision(), RoundingMode.HALF_UP); + + invoice.load(getTrxName()); + assertTrue(invoice.isPosted()); + + //reverse MR invoice + info = MWorkflow.runDocumentActionWorkflow(invoice, DocAction.ACTION_Reverse_Correct); + invoice.load(getTrxName()); + assertFalse(info.isError(), info.getSummary()); + assertEquals(DocAction.STATUS_Reversed, invoice.getDocStatus()); + + MInvoice reversalInvoice = new MInvoice(Env.getCtx(), invoice.getReversal_ID(), getTrxName()); + if (!reversalInvoice.isPosted()) { + String error = DocumentEngine.postImmediate(Env.getCtx(), reversalInvoice.getAD_Client_ID(), MInvoice.Table_ID, invoice.get_ID(), false, getTrxName()); + assertTrue(error == null, error); + } + + MMatchInv[] miList = MMatchInv.getInvoiceLine(Env.getCtx(), invoiceLine.get_ID(), getTrxName()); + assertEquals(2, miList.length); + for (MMatchInv mi : miList) { + if (!mi.isPosted()) { + String error = DocumentEngine.postImmediate(Env.getCtx(), mi.getAD_Client_ID(), MMatchInv.Table_ID, mi.get_ID(), false, getTrxName()); + assertTrue(error == null, error); } - else if (fa.getAccount_ID() == acctInvClr.getAccount_ID()) { - if (mi.isReversal()) - assertEquals(fa.getAmtAcctDr().setScale(2, RoundingMode.HALF_UP), invMatchAmt.setScale(2, RoundingMode.HALF_UP), "MatchInv incorrect amount posted "); - else - assertEquals(fa.getAmtAcctCr().setScale(2, RoundingMode.HALF_UP), invMatchAmt.setScale(2, RoundingMode.HALF_UP), "MatchInv incorrect amount posted "); - assertEquals(mi.getQty().negate(), fa.getQty(), "Accounting fact quantity incorrect"); + mi.load(getTrxName()); + assertTrue(mi.isPosted()); + MMatchInv mir = new MMatchInv(Env.getCtx(), mi.getReversal_ID(), getTrxName()); + if (!mir.isPosted()) { + String error = DocumentEngine.postImmediate(Env.getCtx(), mir.getAD_Client_ID(), MMatchInv.Table_ID, mir.get_ID(), false, getTrxName()); + assertTrue(error == null, error); } + mir.load(getTrxName()); + assertTrue(mir.isPosted()); + + Doc doc = DocManager.getDocument(as, MMatchInv.Table_ID, mi.get_ID(), getTrxName()); + doc.setC_BPartner_ID(mi.getC_InvoiceLine().getC_Invoice().getC_BPartner_ID()); + MAccount acctNIR = doc.getAccount(Doc.ACCTTYPE_NotInvoicedReceipts, as); + + ProductCost pc = new ProductCost (Env.getCtx(), mi.getM_Product_ID(), mi.getM_AttributeSetInstance_ID(), getTrxName()); + MAccount acctInvClr = pc.getAccount(ProductCost.ACCTTYPE_P_InventoryClearing, as); + MAccount acctIPV = pc.getAccount(ProductCost.ACCTTYPE_P_AverageCostVariance, as); + Query query = MFactAcct.createRecordIdQuery(MMatchInv.Table_ID, mi.get_ID(), as.getC_AcctSchema_ID(), getTrxName()); + List factAccts = query.list(); + List expected = Arrays.asList(new FactAcct(acctNIR, poMatchAmt, 2, !mi.isReversal(), mi.getQty()), + new FactAcct(acctInvClr, invMatchAmt, 2, mi.isReversal(), mi.getQty().negate()), + new FactAcct(acctIPV, invMatchAmt.subtract(poMatchAmt), 2, !mi.isReversal())); + assertFactAcctEntries(factAccts, expected); + } + } finally { + rollback(); + + if (product != null) { + product.set_TrxName(null); + product.deleteEx(true); } } } diff --git a/org.idempiere.test/src/org/idempiere/test/base/MatchInvTestIsolated.java b/org.idempiere.test/src/org/idempiere/test/base/MatchInvTestIsolated.java index b7c59daa34..210e51ed8c 100644 --- a/org.idempiere.test/src/org/idempiere/test/base/MatchInvTestIsolated.java +++ b/org.idempiere.test/src/org/idempiere/test/base/MatchInvTestIsolated.java @@ -27,12 +27,17 @@ package org.idempiere.test.base; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertTrue; import java.math.BigDecimal; import java.math.RoundingMode; +import java.sql.Timestamp; +import java.util.ArrayList; +import java.util.Arrays; import java.util.List; +import java.util.Optional; import org.compiere.acct.Doc; import org.compiere.acct.DocManager; @@ -42,6 +47,8 @@ import org.compiere.model.MBPartner; import org.compiere.model.MClient; import org.compiere.model.MConversionRate; import org.compiere.model.MCost; +import org.compiere.model.MCostElement; +import org.compiere.model.MCurrency; import org.compiere.model.MDocType; import org.compiere.model.MFactAcct; import org.compiere.model.MInOut; @@ -53,19 +60,26 @@ import org.compiere.model.MInvoiceLine; import org.compiere.model.MMatchInv; import org.compiere.model.MOrder; import org.compiere.model.MOrderLine; +import org.compiere.model.MPriceList; +import org.compiere.model.MPriceListVersion; import org.compiere.model.MProduct; import org.compiere.model.MProductCategory; import org.compiere.model.MProductCategoryAcct; +import org.compiere.model.MProductPrice; import org.compiere.model.MWarehouse; import org.compiere.model.ProductCost; import org.compiere.model.Query; import org.compiere.process.DocAction; import org.compiere.process.DocumentEngine; import org.compiere.process.ProcessInfo; +import org.compiere.util.CacheMgt; import org.compiere.util.Env; +import org.compiere.util.TimeUtil; import org.compiere.wf.MWorkflow; import org.idempiere.test.AbstractTestCase; +import org.idempiere.test.ConversionRateHelper; import org.idempiere.test.DictionaryIDs; +import org.idempiere.test.FactAcct; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.parallel.Isolated; @@ -240,21 +254,13 @@ public class MatchInvTestIsolated extends AbstractTestCase { MAccount acctIPV = pc.getAccount(ProductCost.ACCTTYPE_P_IPV, as); int C_AcctSchema_ID = as.getC_AcctSchema_ID(); - String whereClause2 = MFactAcct.COLUMNNAME_AD_Table_ID + "=" + MMatchInv.Table_ID - + " AND " + MFactAcct.COLUMNNAME_Record_ID + "=" + mi.get_ID() - + " AND " + MFactAcct.COLUMNNAME_C_AcctSchema_ID + "=" + C_AcctSchema_ID; - int[] ids = MFactAcct.getAllIDs(MFactAcct.Table_Name, whereClause2, getTrxName()); + Query query = MFactAcct.createRecordIdQuery(MMatchInv.Table_ID, mi.get_ID(), C_AcctSchema_ID, getTrxName()); + List factAccts = query.list(); BigDecimal invMatchAmt = invoiceLine.getMatchedQty().multiply(invoiceLine.getPriceActual()).setScale(as.getStdPrecision(), RoundingMode.HALF_UP); mulchCost = mulchCost.setScale(as.getStdPrecision(), RoundingMode.HALF_UP); - for (int id : ids) { - MFactAcct fa = new MFactAcct(Env.getCtx(), id, getTrxName()); - if (fa.getAccount_ID() == acctNIR.getAccount_ID()) - assertEquals(fa.getAmtAcctDr().setScale(2, RoundingMode.HALF_UP), mulchCost.setScale(2, RoundingMode.HALF_UP), ""); - else if (fa.getAccount_ID() == acctInvClr.getAccount_ID()) - assertEquals(fa.getAmtAcctCr().setScale(2, RoundingMode.HALF_UP), invMatchAmt.setScale(2, RoundingMode.HALF_UP), ""); - else if (fa.getAccount_ID() == acctIPV.getAccount_ID()) - assertEquals(fa.getAmtAcctDr().subtract(fa.getAmtAcctCr()).setScale(2, RoundingMode.HALF_UP), invMatchAmt.subtract(mulchCost).setScale(2, RoundingMode.HALF_UP), ""); - } + List expected = Arrays.asList(new FactAcct(acctNIR, mulchCost, 2, true), new FactAcct(acctInvClr, invMatchAmt, 2, false), + new FactAcct(acctIPV, invMatchAmt.subtract(mulchCost), 2, true)); + assertFactAcctEntries(factAccts, expected); } } finally { getTrx().rollback(); @@ -263,4 +269,617 @@ public class MatchInvTestIsolated extends AbstractTestCase { category.deleteEx(true); } } + + /** + * Test Average PO Cost and Invoice Price Variance posting + */ + @Test + public void testAverageCostingIPV() { + MProduct product = null; + MClient client = MClient.get(Env.getCtx()); + MAcctSchema as = client.getAcctSchema(); + assertEquals(as.getCostingMethod(), MCostElement.COSTINGMETHOD_AveragePO, "Default costing method not Average PO"); + + try { + product = new MProduct(Env.getCtx(), 0, null); + product.setM_Product_Category_ID(DictionaryIDs.M_Product_Category.STANDARD.id); + product.setName("testAverageCostingIPV"); + product.setProductType(MProduct.PRODUCTTYPE_Item); + product.setIsStocked(true); + product.setIsSold(true); + product.setIsPurchased(true); + product.setC_UOM_ID(DictionaryIDs.C_UOM.EACH.id); + product.setC_TaxCategory_ID(DictionaryIDs.C_TaxCategory.STANDARD.id); + product.saveEx(); + + MPriceListVersion plv = MPriceList.get(DictionaryIDs.M_PriceList.PURCHASE.id).getPriceListVersion(null); + MProductPrice pp = new MProductPrice(Env.getCtx(), 0, getTrxName()); + pp.setM_PriceList_Version_ID(plv.getM_PriceList_Version_ID()); + pp.setM_Product_ID(product.get_ID()); + BigDecimal orderPrice = new BigDecimal("2.00"); + pp.setPriceStd(orderPrice); + pp.setPriceList(orderPrice); + pp.saveEx(); + + int purchaseId = DictionaryIDs.M_PriceList.PURCHASE.id; // Purchase Price List + MBPartner bpartner = MBPartner.get(Env.getCtx(), DictionaryIDs.C_BPartner.SEED_FARM.id); + + MOrder order = new MOrder(Env.getCtx(), 0, getTrxName()); + order.setBPartner(bpartner); + order.setIsSOTrx(false); + order.setC_DocTypeTarget_ID(); + order.setM_PriceList_ID(purchaseId); + order.setDocStatus(DocAction.STATUS_Drafted); + order.setDocAction(DocAction.ACTION_Complete); + order.saveEx(); + + MOrderLine orderLine = new MOrderLine(order); + orderLine.setLine(10); + orderLine.setProduct(product); + orderLine.setQty(BigDecimal.TEN); + orderLine.saveEx(); + + ProcessInfo info = MWorkflow.runDocumentActionWorkflow(order, DocAction.ACTION_Complete); + order.load(getTrxName()); + assertFalse(info.isError(), info.getSummary()); + assertEquals(DocAction.STATUS_Completed, order.getDocStatus()); + + MInOut receipt = new MInOut(order, 122, order.getDateOrdered()); // MM Receipt + receipt.saveEx(); + + MInOutLine receiptLine = new MInOutLine(receipt); + receiptLine.setC_OrderLine_ID(orderLine.get_ID()); + receiptLine.setLine(10); + receiptLine.setProduct(product); + receiptLine.setQty(BigDecimal.TEN); + MWarehouse wh = MWarehouse.get(Env.getCtx(), receipt.getM_Warehouse_ID()); + int M_Locator_ID = wh.getDefaultLocator().getM_Locator_ID(); + receiptLine.setM_Locator_ID(M_Locator_ID); + receiptLine.saveEx(); + + info = MWorkflow.runDocumentActionWorkflow(receipt, DocAction.ACTION_Complete); + receipt.load(getTrxName()); + assertFalse(info.isError(), info.getSummary()); + assertEquals(DocAction.STATUS_Completed, receipt.getDocStatus()); + + if (!receipt.isPosted()) { + String error = DocumentEngine.postImmediate(Env.getCtx(), receipt.getAD_Client_ID(), MInOut.Table_ID, receipt.get_ID(), false, getTrxName()); + assertTrue(error == null); + } + receipt.load(getTrxName()); + assertTrue(receipt.isPosted()); + + product.set_TrxName(getTrxName()); + MCost cost = product.getCostingRecord(as, getAD_Org_ID(), 0, as.getCostingMethod()); + assertNotNull(cost, "No MCost record found"); + assertEquals(orderPrice, cost.getCurrentCostPrice().setScale(2, RoundingMode.HALF_UP), "Unexpected current cost price"); + + MInvoice invoice = new MInvoice(receipt, receipt.getMovementDate()); + invoice.setC_DocTypeTarget_ID(MDocType.DOCBASETYPE_APInvoice); + invoice.setDocStatus(DocAction.STATUS_Drafted); + invoice.setDocAction(DocAction.ACTION_Complete); + invoice.saveEx(); + + MInvoiceLine invoiceLine = new MInvoiceLine(invoice); + invoiceLine.setM_InOutLine_ID(receiptLine.get_ID()); + invoiceLine.setLine(10); + invoiceLine.setProduct(product); + invoiceLine.setQty(BigDecimal.TEN); + BigDecimal invoicePrice = new BigDecimal("2.50"); + invoiceLine.setPrice(invoicePrice); + invoiceLine.saveEx(); + + info = MWorkflow.runDocumentActionWorkflow(invoice, DocAction.ACTION_Complete); + invoice.load(getTrxName()); + assertFalse(info.isError(), info.getSummary()); + assertEquals(DocAction.STATUS_Completed, invoice.getDocStatus()); + + if (!invoice.isPosted()) { + String error = DocumentEngine.postImmediate(Env.getCtx(), invoice.getAD_Client_ID(), MInvoice.Table_ID, invoice.get_ID(), false, getTrxName()); + assertTrue(error == null); + } + invoice.load(getTrxName()); + assertTrue(invoice.isPosted()); + + MMatchInv[] miList = MMatchInv.getInvoiceLine(Env.getCtx(), invoiceLine.get_ID(), getTrxName()); + for (MMatchInv mi : miList) { + if (!mi.isPosted()) { + String error = DocumentEngine.postImmediate(Env.getCtx(), mi.getAD_Client_ID(), MMatchInv.Table_ID, mi.get_ID(), false, getTrxName()); + assertTrue(error == null); + } + mi.load(getTrxName()); + assertTrue(mi.isPosted()); + + cost = product.getCostingRecord(as, getAD_Org_ID(), 0, as.getCostingMethod()); + assertNotNull(cost, "No MCost record found"); + assertEquals(invoicePrice, cost.getCurrentCostPrice().setScale(2, RoundingMode.HALF_UP), "Unexpected current cost price"); + + ProductCost pc = new ProductCost (Env.getCtx(), mi.getM_Product_ID(), mi.getM_AttributeSetInstance_ID(), getTrxName()); + MAccount acctInvClr = pc.getAccount(ProductCost.ACCTTYPE_P_InventoryClearing, as); + MAccount acctAsset = pc.getAccount(ProductCost.ACCTTYPE_P_Asset, as); + Doc doc = DocManager.getDocument(as, MInvoice.Table_ID, invoice.get_ID(), getTrxName()); + MAccount nirAccount = doc.getAccount(Doc.ACCTTYPE_NotInvoicedReceipts, as); + int C_AcctSchema_ID = as.getC_AcctSchema_ID(); + + Query query = MFactAcct.createRecordIdQuery(MMatchInv.Table_ID, mi.get_ID(), C_AcctSchema_ID, getTrxName()); + List factAccts = query.list(); + BigDecimal ipvAmt = invoicePrice.subtract(orderPrice).multiply(BigDecimal.TEN); + List expected = Arrays.asList(new FactAcct(acctAsset, ipvAmt, 2, true), + new FactAcct(nirAccount, orderPrice.multiply(BigDecimal.TEN), 2, true), + new FactAcct(acctInvClr, invoicePrice.multiply(BigDecimal.TEN), 2, false)); + assertFactAcctEntries(factAccts, expected); + } + } finally { + rollback(); + + if (product != null) { + product.set_TrxName(null); + product.deleteEx(true); + } + } + } + + /** + * Test Average PO Cost and Invoice Price Variance posting (after customer shipment) + */ + @Test + public void testAverageCostingIPVAfterShipment() { + MProduct product = null; + MClient client = MClient.get(Env.getCtx()); + MAcctSchema[] ass = MAcctSchema.getClientAcctSchema(Env.getCtx(), Env.getAD_Client_ID(Env.getCtx())); + List allowNegatives = new ArrayList(); + Arrays.stream(ass).forEach(e -> { + MAcctSchema copy = MAcctSchema.getCopy(Env.getCtx(), e.getC_AcctSchema_ID(), null); + if (copy.isAllowNegativePosting()) + { + copy.setIsAllowNegativePosting(false); + copy.saveEx(); + allowNegatives.add(copy); + } + }); + if (allowNegatives.size() > 0) + CacheMgt.get().reset(MAcctSchema.Table_Name); + ass = MAcctSchema.getClientAcctSchema(Env.getCtx(), Env.getAD_Client_ID(Env.getCtx())); + MAcctSchema as = client.getAcctSchema(); + assertEquals(as.getCostingMethod(), MCostElement.COSTINGMETHOD_AveragePO, "Default costing method not Average PO"); + + MCurrency usd = MCurrency.get(DictionaryIDs.C_Currency.USD.id); + MCurrency euro = MCurrency.get(DictionaryIDs.C_Currency.EUR.id); + int C_ConversionType_ID = DictionaryIDs.C_ConversionType.SPOT.id; + Timestamp today = TimeUtil.getDay(null); + Timestamp tomorrow = TimeUtil.addDays(today, 1); + MConversionRate cr1 = ConversionRateHelper.createConversionRate(usd.getC_Currency_ID(), euro.getC_Currency_ID(), C_ConversionType_ID, today, new BigDecimal("0.91"), true); + MConversionRate cr2 = ConversionRateHelper.createConversionRate(usd.getC_Currency_ID(), euro.getC_Currency_ID(), C_ConversionType_ID, tomorrow, new BigDecimal("0.85"), true); + try { + product = new MProduct(Env.getCtx(), 0, null); + product.setM_Product_Category_ID(DictionaryIDs.M_Product_Category.STANDARD.id); + product.setName("testAverageCostingIPVAfterShipment"); + product.setProductType(MProduct.PRODUCTTYPE_Item); + product.setIsStocked(true); + product.setIsSold(true); + product.setIsPurchased(true); + product.setC_UOM_ID(DictionaryIDs.C_UOM.EACH.id); + product.setC_TaxCategory_ID(DictionaryIDs.C_TaxCategory.STANDARD.id); + product.saveEx(); + + MPriceListVersion plv = MPriceList.get(DictionaryIDs.M_PriceList.PURCHASE.id).getPriceListVersion(null); + MProductPrice pp = new MProductPrice(Env.getCtx(), 0, getTrxName()); + pp.setM_PriceList_Version_ID(plv.getM_PriceList_Version_ID()); + pp.setM_Product_ID(product.get_ID()); + BigDecimal orderPrice = new BigDecimal("2.00"); + pp.setPriceStd(orderPrice); + pp.setPriceList(orderPrice); + pp.saveEx(); + + //PO and MR + int purchaseId = DictionaryIDs.M_PriceList.PURCHASE.id; // Purchase Price List + MBPartner bpartner = MBPartner.get(Env.getCtx(), DictionaryIDs.C_BPartner.SEED_FARM.id); + + MOrder order = new MOrder(Env.getCtx(), 0, getTrxName()); + order.setBPartner(bpartner); + order.setIsSOTrx(false); + order.setC_DocTypeTarget_ID(); + order.setM_PriceList_ID(purchaseId); + order.setDocStatus(DocAction.STATUS_Drafted); + order.setDocAction(DocAction.ACTION_Complete); + order.saveEx(); + + MOrderLine orderLine = new MOrderLine(order); + orderLine.setLine(10); + orderLine.setProduct(product); + orderLine.setQty(BigDecimal.TEN); + orderLine.saveEx(); + + ProcessInfo info = MWorkflow.runDocumentActionWorkflow(order, DocAction.ACTION_Complete); + order.load(getTrxName()); + assertFalse(info.isError(), info.getSummary()); + assertEquals(DocAction.STATUS_Completed, order.getDocStatus()); + + MInOut receipt = new MInOut(order, 122, order.getDateOrdered()); // MM Receipt + receipt.saveEx(); + + MInOutLine receiptLine = new MInOutLine(receipt); + receiptLine.setC_OrderLine_ID(orderLine.get_ID()); + receiptLine.setLine(10); + receiptLine.setProduct(product); + receiptLine.setQty(BigDecimal.TEN); + MWarehouse wh = MWarehouse.get(Env.getCtx(), receipt.getM_Warehouse_ID()); + int M_Locator_ID = wh.getDefaultLocator().getM_Locator_ID(); + receiptLine.setM_Locator_ID(M_Locator_ID); + receiptLine.saveEx(); + + info = MWorkflow.runDocumentActionWorkflow(receipt, DocAction.ACTION_Complete); + receipt.load(getTrxName()); + assertFalse(info.isError(), info.getSummary()); + assertEquals(DocAction.STATUS_Completed, receipt.getDocStatus()); + + if (!receipt.isPosted()) { + String error = DocumentEngine.postImmediate(Env.getCtx(), receipt.getAD_Client_ID(), MInOut.Table_ID, receipt.get_ID(), false, getTrxName()); + assertTrue(error == null); + } + receipt.load(getTrxName()); + assertTrue(receipt.isPosted()); + + product.set_TrxName(getTrxName()); + MCost cost = product.getCostingRecord(as, getAD_Org_ID(), 0, as.getCostingMethod()); + assertNotNull(cost, "No MCost record found"); + assertEquals(orderPrice, cost.getCurrentCostPrice().setScale(2, RoundingMode.HALF_UP), "Unexpected current cost price"); + + //customer shipment + MOrder salesOrder = new MOrder(Env.getCtx(), 0, getTrxName()); + salesOrder.setBPartner(MBPartner.get(Env.getCtx(), DictionaryIDs.C_BPartner.JOE_BLOCK.id)); + salesOrder.setC_DocTypeTarget_ID(MOrder.DocSubTypeSO_Standard); + salesOrder.setDeliveryRule(MOrder.DELIVERYRULE_CompleteOrder); + salesOrder.setDocStatus(DocAction.STATUS_Drafted); + salesOrder.setDocAction(DocAction.ACTION_Complete); + salesOrder.setDatePromised(today); + salesOrder.saveEx(); + + BigDecimal salesQty = new BigDecimal("5"); + MOrderLine salesLine1 = new MOrderLine(salesOrder); + salesLine1.setLine(10); + salesLine1.setProduct(product); + salesLine1.setQty(salesQty); + salesLine1.setDatePromised(today); + salesLine1.saveEx(); + + info = MWorkflow.runDocumentActionWorkflow(salesOrder, DocAction.ACTION_Complete); + salesOrder.load(getTrxName()); + assertFalse(info.isError(), info.getSummary()); + assertEquals(DocAction.STATUS_Completed, salesOrder.getDocStatus(), "Unexpected Document Status"); + + MInOut shipment = new MInOut(salesOrder, DictionaryIDs.C_DocType.MM_SHIPMENT.id, salesOrder.getDateOrdered()); + shipment.setDocStatus(DocAction.STATUS_Drafted); + shipment.setDocAction(DocAction.ACTION_Complete); + shipment.saveEx(); + + MInOutLine shipmentLine = new MInOutLine(shipment); + shipmentLine.setOrderLine(salesLine1, 0, salesQty); + shipmentLine.setQty(salesQty); + shipmentLine.saveEx(); + + info = MWorkflow.runDocumentActionWorkflow(shipment, DocAction.ACTION_Complete); + assertFalse(info.isError(), info.getSummary()); + shipment.load(getTrxName()); + assertEquals(DocAction.STATUS_Completed, shipment.getDocStatus(), "Unexpected Document Status"); + + //MR invoice + MInvoice invoice = new MInvoice(receipt, receipt.getMovementDate()); + invoice.setC_DocTypeTarget_ID(MDocType.DOCBASETYPE_APInvoice); + invoice.setDocStatus(DocAction.STATUS_Drafted); + invoice.setDocAction(DocAction.ACTION_Complete); + invoice.saveEx(); + + MInvoiceLine invoiceLine = new MInvoiceLine(invoice); + invoiceLine.setM_InOutLine_ID(receiptLine.get_ID()); + invoiceLine.setLine(10); + invoiceLine.setProduct(product); + invoiceLine.setQty(BigDecimal.TEN); + BigDecimal invoicePrice = new BigDecimal("2.50"); + invoiceLine.setPrice(invoicePrice); + invoiceLine.saveEx(); + + info = MWorkflow.runDocumentActionWorkflow(invoice, DocAction.ACTION_Complete); + invoice.load(getTrxName()); + assertFalse(info.isError(), info.getSummary()); + assertEquals(DocAction.STATUS_Completed, invoice.getDocStatus()); + + if (!invoice.isPosted()) { + String error = DocumentEngine.postImmediate(Env.getCtx(), invoice.getAD_Client_ID(), MInvoice.Table_ID, invoice.get_ID(), false, getTrxName()); + assertTrue(error == null); + } + invoice.load(getTrxName()); + assertTrue(invoice.isPosted()); + + MMatchInv[] miList = MMatchInv.getInvoiceLine(Env.getCtx(), invoiceLine.get_ID(), getTrxName()); + for (MMatchInv mi : miList) { + if (!mi.isPosted()) { + String error = DocumentEngine.postImmediate(Env.getCtx(), mi.getAD_Client_ID(), MMatchInv.Table_ID, mi.get_ID(), false, getTrxName()); + assertTrue(error == null); + } + mi.load(getTrxName()); + assertTrue(mi.isPosted()); + + cost = product.getCostingRecord(as, getAD_Org_ID(), 0, as.getCostingMethod()); + assertNotNull(cost, "No MCost record found"); + assertEquals(invoicePrice, cost.getCurrentCostPrice().setScale(2, RoundingMode.HALF_UP), "Unexpected current cost price"); + + ProductCost pc = new ProductCost (Env.getCtx(), mi.getM_Product_ID(), mi.getM_AttributeSetInstance_ID(), getTrxName()); + MAccount acctInvClr = pc.getAccount(ProductCost.ACCTTYPE_P_InventoryClearing, as); + MAccount acctAsset = pc.getAccount(ProductCost.ACCTTYPE_P_Asset, as); + MAccount varianceAccount = pc.getAccount(ProductCost.ACCTTYPE_P_AverageCostVariance, as); + Doc doc = DocManager.getDocument(as, MInvoice.Table_ID, invoice.get_ID(), getTrxName()); + MAccount nirAccount = doc.getAccount(Doc.ACCTTYPE_NotInvoicedReceipts, as); + int C_AcctSchema_ID = as.getC_AcctSchema_ID(); + + Query query = MFactAcct.createRecordIdQuery(MMatchInv.Table_ID, mi.get_ID(), C_AcctSchema_ID, getTrxName()); + List factAccts = query.list(); + BigDecimal stockBalance = BigDecimal.TEN.subtract(salesQty); + BigDecimal assetAmt = invoicePrice.subtract(orderPrice).multiply(stockBalance); + List expected = Arrays.asList(new FactAcct(acctAsset, assetAmt, 2, true), + new FactAcct(varianceAccount, invoicePrice.subtract(orderPrice).multiply(BigDecimal.TEN.subtract(stockBalance)), 2, true), + new FactAcct(nirAccount, orderPrice.multiply(BigDecimal.TEN), 2, true), + new FactAcct(acctInvClr, invoicePrice.multiply(BigDecimal.TEN), 2, false)); + assertFactAcctEntries(factAccts, expected); + } + + //test reversal posting + Env.setContext(Env.getCtx(), Env.DATE, tomorrow); + info = MWorkflow.runDocumentActionWorkflow(invoice, DocAction.ACTION_Reverse_Accrual); + invoice.load(getTrxName()); + assertFalse(info.isError(), info.getSummary()); + assertEquals(DocAction.STATUS_Reversed, invoice.getDocStatus()); + assertTrue(invoice.getReversal_ID() > 0, "No reversal invoice id"); + + MInvoice reversalInvoice = new MInvoice(Env.getCtx(), invoice.getReversal_ID(), getTrxName()); + assertEquals(invoice.getReversal_ID(), reversalInvoice.get_ID(), "Failed to load reversal invoice"); + if (!reversalInvoice.isPosted()) { + String error = DocumentEngine.postImmediate(Env.getCtx(), reversalInvoice.getAD_Client_ID(), MInvoice.Table_ID, reversalInvoice.get_ID(), false, getTrxName()); + assertTrue(error == null, error); + } + reversalInvoice.load(getTrxName()); + assertTrue(reversalInvoice.isPosted()); + + for (MMatchInv mi : miList) { + mi.load(getTrxName()); + Query query = MFactAcct.createRecordIdQuery(MMatchInv.Table_ID, mi.get_ID(), as.get_ID(), getTrxName()); + List factAccts = query.list(); + query = MFactAcct.createRecordIdQuery(MMatchInv.Table_ID, mi.getReversal_ID(), as.get_ID(), getTrxName()); + List rFactAccts = query.list(); + ArrayList expected = new ArrayList(); + for(MFactAcct factAcct : factAccts) { + MAccount acct = MAccount.get(factAcct, getTrxName()); + if (factAcct.getAmtAcctDr().signum() != 0) { + expected.add(new FactAcct(acct, factAcct.getAmtAcctDr(), 2, false)); + } else if (factAcct.getAmtAcctCr().signum() != 0) { + expected.add(new FactAcct(acct, factAcct.getAmtAcctCr(), 2, true)); + } + } + assertFactAcctEntries(rFactAccts, expected); + + Optional optional = Arrays.stream(ass).filter(e -> e.getC_AcctSchema_ID() != as.get_ID()).findFirst(); + if (optional.isPresent()) { + MAcctSchema as2 = optional.get(); + query = MFactAcct.createRecordIdQuery(MMatchInv.Table_ID, mi.get_ID(), as2.get_ID(), getTrxName()); + factAccts = query.list(); + query = MFactAcct.createRecordIdQuery(MMatchInv.Table_ID, mi.getReversal_ID(), as2.get_ID(), getTrxName()); + rFactAccts = query.list(); + expected = new ArrayList(); + for(MFactAcct factAcct : factAccts) { + MAccount acct = MAccount.get(factAcct, getTrxName()); + if (factAcct.getAmtAcctDr().signum() != 0) { + expected.add(new FactAcct(acct, factAcct.getAmtAcctDr(), 2, false)); + } else if (factAcct.getAmtAcctCr().signum() != 0) { + expected.add(new FactAcct(acct, factAcct.getAmtAcctCr(), 2, true)); + } + } + assertFactAcctEntries(rFactAccts, expected); + } + } + + //assert reversal invoice posting + Query query = MFactAcct.createRecordIdQuery(MInvoice.Table_ID, invoice.get_ID(), as.get_ID(), getTrxName()); + List factAccts = query.list(); + query = MFactAcct.createRecordIdQuery(MInvoice.Table_ID, invoice.getReversal_ID(), as.get_ID(), getTrxName()); + List rFactAccts = query.list(); + ArrayList expected = new ArrayList(); + for(MFactAcct factAcct : factAccts) { + MAccount acct = MAccount.get(factAcct, getTrxName()); + if (factAcct.getAmtAcctDr().signum() != 0) { + expected.add(new FactAcct(acct, factAcct.getAmtAcctDr(), 2, false)); + } else if (factAcct.getAmtAcctCr().signum() != 0) { + expected.add(new FactAcct(acct, factAcct.getAmtAcctCr(), 2, true)); + } + } + assertFactAcctEntries(rFactAccts, expected); + + Optional optional = Arrays.stream(ass).filter(e -> e.getC_AcctSchema_ID() != as.get_ID()).findFirst(); + if (optional.isPresent()) { + MAcctSchema as2 = optional.get(); + query = MFactAcct.createRecordIdQuery(MInvoice.Table_ID, invoice.get_ID(), as2.get_ID(), getTrxName()); + factAccts = query.list(); + query = MFactAcct.createRecordIdQuery(MInvoice.Table_ID, invoice.getReversal_ID(), as2.get_ID(), getTrxName()); + rFactAccts = query.list(); + expected = new ArrayList(); + for(MFactAcct factAcct : factAccts) { + MAccount acct = MAccount.get(factAcct, getTrxName()); + if (factAcct.getAmtAcctDr().signum() != 0) { + expected.add(new FactAcct(acct, factAcct.getAmtAcctDr(), 2, false)); + } else if (factAcct.getAmtAcctCr().signum() != 0) { + expected.add(new FactAcct(acct, factAcct.getAmtAcctCr(), 2, true)); + } + } + assertFactAcctEntries(rFactAccts, expected); + } + } finally { + rollback(); + + if (product != null) { + product.set_TrxName(null); + product.deleteEx(true); + } + ConversionRateHelper.deleteConversionRate(cr1); + ConversionRateHelper.deleteConversionRate(cr2); + + if (allowNegatives.size() > 0) { + allowNegatives.forEach(e -> { + e.setIsAllowNegativePosting(true); + e.saveEx(); + }); + } + + } + } + + /** + * Test Average PO Cost and Invoice Price Variance posting for partial MR + */ + @Test + public void testAverageCostingIPVPartialMR() { + MProduct product = null; + MClient client = MClient.get(Env.getCtx()); + MAcctSchema as = client.getAcctSchema(); + assertEquals(as.getCostingMethod(), MCostElement.COSTINGMETHOD_AveragePO, "Default costing method not Average PO"); + + try { + product = new MProduct(Env.getCtx(), 0, null); + product.setM_Product_Category_ID(DictionaryIDs.M_Product_Category.STANDARD.id); + product.setName("testAverageCostingIPVPartialMR"); + product.setProductType(MProduct.PRODUCTTYPE_Item); + product.setIsStocked(true); + product.setIsSold(true); + product.setIsPurchased(true); + product.setC_UOM_ID(DictionaryIDs.C_UOM.EACH.id); + product.setC_TaxCategory_ID(DictionaryIDs.C_TaxCategory.STANDARD.id); + product.saveEx(); + + MPriceListVersion plv = MPriceList.get(DictionaryIDs.M_PriceList.PURCHASE.id).getPriceListVersion(null); + MProductPrice pp = new MProductPrice(Env.getCtx(), 0, getTrxName()); + pp.setM_PriceList_Version_ID(plv.getM_PriceList_Version_ID()); + pp.setM_Product_ID(product.get_ID()); + BigDecimal orderPrice = new BigDecimal("2.00"); + pp.setPriceStd(orderPrice); + pp.setPriceList(orderPrice); + pp.saveEx(); + + int purchaseId = DictionaryIDs.M_PriceList.PURCHASE.id; // Purchase Price List + MBPartner bpartner = MBPartner.get(Env.getCtx(), DictionaryIDs.C_BPartner.SEED_FARM.id); + + MOrder order = new MOrder(Env.getCtx(), 0, getTrxName()); + order.setBPartner(bpartner); + order.setIsSOTrx(false); + order.setC_DocTypeTarget_ID(); + order.setM_PriceList_ID(purchaseId); + order.setDocStatus(DocAction.STATUS_Drafted); + order.setDocAction(DocAction.ACTION_Complete); + order.saveEx(); + + MOrderLine orderLine = new MOrderLine(order); + orderLine.setLine(10); + orderLine.setProduct(product); + orderLine.setQty(BigDecimal.TEN); + orderLine.saveEx(); + + ProcessInfo info = MWorkflow.runDocumentActionWorkflow(order, DocAction.ACTION_Complete); + order.load(getTrxName()); + assertFalse(info.isError(), info.getSummary()); + assertEquals(DocAction.STATUS_Completed, order.getDocStatus()); + + //partial MR + MInOut receipt = new MInOut(order, 122, order.getDateOrdered()); // MM Receipt + receipt.saveEx(); + + BigDecimal mrQty = new BigDecimal("5"); + MInOutLine receiptLine = new MInOutLine(receipt); + receiptLine.setC_OrderLine_ID(orderLine.get_ID()); + receiptLine.setLine(10); + receiptLine.setProduct(product); + receiptLine.setQty(mrQty); + MWarehouse wh = MWarehouse.get(Env.getCtx(), receipt.getM_Warehouse_ID()); + int M_Locator_ID = wh.getDefaultLocator().getM_Locator_ID(); + receiptLine.setM_Locator_ID(M_Locator_ID); + receiptLine.saveEx(); + + info = MWorkflow.runDocumentActionWorkflow(receipt, DocAction.ACTION_Complete); + receipt.load(getTrxName()); + assertFalse(info.isError(), info.getSummary()); + assertEquals(DocAction.STATUS_Completed, receipt.getDocStatus()); + + if (!receipt.isPosted()) { + String error = DocumentEngine.postImmediate(Env.getCtx(), receipt.getAD_Client_ID(), MInOut.Table_ID, receipt.get_ID(), false, getTrxName()); + assertTrue(error == null); + } + receipt.load(getTrxName()); + assertTrue(receipt.isPosted()); + + product.set_TrxName(getTrxName()); + MCost cost = product.getCostingRecord(as, getAD_Org_ID(), 0, as.getCostingMethod()); + assertNotNull(cost, "No MCost record found"); + assertEquals(orderPrice, cost.getCurrentCostPrice().setScale(2, RoundingMode.HALF_UP), "Unexpected current cost price"); + + //ap invoce, full + MInvoice invoice = new MInvoice(receipt, receipt.getMovementDate()); + invoice.setC_DocTypeTarget_ID(MDocType.DOCBASETYPE_APInvoice); + invoice.setDocStatus(DocAction.STATUS_Drafted); + invoice.setDocAction(DocAction.ACTION_Complete); + invoice.saveEx(); + + MInvoiceLine invoiceLine = new MInvoiceLine(invoice); + invoiceLine.setM_InOutLine_ID(receiptLine.get_ID()); + invoiceLine.setLine(10); + invoiceLine.setProduct(product); + invoiceLine.setQty(BigDecimal.TEN); + BigDecimal invoicePrice = new BigDecimal("4.00"); + invoiceLine.setPrice(invoicePrice); + invoiceLine.saveEx(); + + info = MWorkflow.runDocumentActionWorkflow(invoice, DocAction.ACTION_Complete); + invoice.load(getTrxName()); + assertFalse(info.isError(), info.getSummary()); + assertEquals(DocAction.STATUS_Completed, invoice.getDocStatus()); + + if (!invoice.isPosted()) { + String error = DocumentEngine.postImmediate(Env.getCtx(), invoice.getAD_Client_ID(), MInvoice.Table_ID, invoice.get_ID(), false, getTrxName()); + assertTrue(error == null); + } + invoice.load(getTrxName()); + assertTrue(invoice.isPosted()); + + MMatchInv[] miList = MMatchInv.getInvoiceLine(Env.getCtx(), invoiceLine.get_ID(), getTrxName()); + for (MMatchInv mi : miList) { + if (!mi.isPosted()) { + String error = DocumentEngine.postImmediate(Env.getCtx(), mi.getAD_Client_ID(), MMatchInv.Table_ID, mi.get_ID(), false, getTrxName()); + assertTrue(error == null); + } + mi.load(getTrxName()); + assertTrue(mi.isPosted()); + + cost = product.getCostingRecord(as, getAD_Org_ID(), 0, as.getCostingMethod()); + assertNotNull(cost, "No MCost record found"); + assertEquals(invoicePrice, cost.getCurrentCostPrice().setScale(2, RoundingMode.HALF_UP), "Unexpected current cost price"); + + ProductCost pc = new ProductCost (Env.getCtx(), mi.getM_Product_ID(), mi.getM_AttributeSetInstance_ID(), getTrxName()); + MAccount acctInvClr = pc.getAccount(ProductCost.ACCTTYPE_P_InventoryClearing, as); + MAccount acctAsset = pc.getAccount(ProductCost.ACCTTYPE_P_Asset, as); + Doc doc = DocManager.getDocument(as, MInvoice.Table_ID, invoice.get_ID(), getTrxName()); + MAccount nirAccount = doc.getAccount(Doc.ACCTTYPE_NotInvoicedReceipts, as); + int C_AcctSchema_ID = as.getC_AcctSchema_ID(); + + Query query = MFactAcct.createRecordIdQuery(MMatchInv.Table_ID, mi.get_ID(), C_AcctSchema_ID, getTrxName()); + List factAccts = query.list(); + BigDecimal assetAmt = invoicePrice.subtract(orderPrice).multiply(mrQty); + List expected = Arrays.asList(new FactAcct(acctAsset, assetAmt, 2, true), + new FactAcct(nirAccount, orderPrice.multiply(mrQty), 2, true), + new FactAcct(acctInvClr, invoicePrice.multiply(mrQty), 2, false)); + assertFactAcctEntries(factAccts, expected); + } + } finally { + rollback(); + + if (product != null) { + product.set_TrxName(null); + product.deleteEx(true); + } + } + } } diff --git a/org.idempiere.test/src/org/idempiere/test/costing/AveragePOCostingTest.java b/org.idempiere.test/src/org/idempiere/test/costing/AveragePOCostingTest.java index 5d3fc2fb0b..99b6c11f78 100644 --- a/org.idempiere.test/src/org/idempiere/test/costing/AveragePOCostingTest.java +++ b/org.idempiere.test/src/org/idempiere/test/costing/AveragePOCostingTest.java @@ -29,30 +29,42 @@ import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.fail; import java.math.BigDecimal; import java.math.RoundingMode; import java.sql.Timestamp; +import java.util.ArrayList; +import java.util.Arrays; import java.util.List; +import java.util.Optional; import org.compiere.acct.Doc; import org.compiere.acct.DocManager; import org.compiere.model.MAccount; import org.compiere.model.MAcctSchema; +import org.compiere.model.MAllocationHdr; import org.compiere.model.MAttributeSet; import org.compiere.model.MAttributeSetExclude; import org.compiere.model.MAttributeSetInstance; import org.compiere.model.MBPartner; import org.compiere.model.MClient; +import org.compiere.model.MConversionRate; import org.compiere.model.MCost; import org.compiere.model.MCostDetail; import org.compiere.model.MCostElement; +import org.compiere.model.MCurrency; +import org.compiere.model.MDocType; import org.compiere.model.MFactAcct; import org.compiere.model.MInOut; import org.compiere.model.MInOutLine; import org.compiere.model.MInOutLineMA; import org.compiere.model.MInventory; import org.compiere.model.MInventoryLine; +import org.compiere.model.MInvoice; +import org.compiere.model.MInvoiceLine; +import org.compiere.model.MLandedCost; +import org.compiere.model.MLandedCostAllocation; import org.compiere.model.MOrder; import org.compiere.model.MOrderLandedCost; import org.compiere.model.MOrderLine; @@ -77,11 +89,14 @@ import org.compiere.process.ServerProcessCtl; import org.compiere.util.DB; import org.compiere.util.Env; import org.compiere.util.TimeUtil; +import org.compiere.util.Util; import org.compiere.wf.MWorkflow; import org.eevolution.model.MPPProductBOM; import org.eevolution.model.MPPProductBOMLine; import org.idempiere.test.AbstractTestCase; +import org.idempiere.test.ConversionRateHelper; import org.idempiere.test.DictionaryIDs; +import org.idempiere.test.FactAcct; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.parallel.Isolated; @@ -958,6 +973,18 @@ public class AveragePOCostingTest extends AbstractTestCase { assertNotNull(cost, "No MCost record found"); assertEquals(new BigDecimal("2.30"), cost.getCurrentCostPrice().setScale(2, RoundingMode.HALF_UP), "Unexpected current cost price"); + //check posting + ProductCost productCost = new ProductCost(Env.getCtx(), product.get_ID(), 0, getTrxName()); + MAccount assetAccount = productCost.getAccount(ProductCost.ACCTTYPE_P_Asset, as); + Doc doc = DocManager.getDocument(as, MInOut.Table_ID, receipt1.get_ID(), getTrxName()); + MAccount nivReceiptAccount = doc.getAccount(Doc.ACCTTYPE_NotInvoicedReceipts, as); + MAccount landedCostAccount = productCost.getAccount(ProductCost.ACCTTYPE_P_LandedCostClearing, as); + Query query = MFactAcct.createRecordIdQuery(MInOut.Table_ID, receipt1.get_ID(), as.get_ID(), getTrxName()); + List list = query.list(); + List expected = Arrays.asList(new FactAcct(assetAccount, new BigDecimal("2.30"), 2, true), + new FactAcct(nivReceiptAccount, new BigDecimal("2.00"), 2, false), new FactAcct(landedCostAccount, new BigDecimal("0.30"), 2, false)); + assertFactAcctEntries(list, expected); + //reverse receipt info = MWorkflow.runDocumentActionWorkflow(receipt1, DocAction.ACTION_Reverse_Accrual); assertFalse(info.isError(), info.getSummary()); @@ -975,6 +1002,13 @@ public class AveragePOCostingTest extends AbstractTestCase { assertEquals(new BigDecimal("0").setScale(2, RoundingMode.HALF_UP), cd.getAmt().setScale(2, RoundingMode.HALF_UP), "Unexpected MCostDetail Amt"); } } + + //check posting for reversal document + query = MFactAcct.createRecordIdQuery(MInOut.Table_ID, receipt1.getReversal_ID(), as.get_ID(), getTrxName()); + list = query.list(); + expected = Arrays.asList(new FactAcct(assetAccount, new BigDecimal("2.30"), 2, false), + new FactAcct(nivReceiptAccount, new BigDecimal("2.00"), 2, true), new FactAcct(landedCostAccount, new BigDecimal("0.30"), 2, true)); + assertFactAcctEntries(list, expected); } finally { rollback(); @@ -985,6 +1019,3509 @@ public class AveragePOCostingTest extends AbstractTestCase { } } + @Test + public void testLandedCostForPOAndInvoice() { + MClient client = MClient.get(Env.getCtx()); + MAcctSchema as = client.getAcctSchema(); + assertEquals(as.getCostingMethod(), MCostElement.COSTINGMETHOD_AveragePO, "Default costing method not Average PO"); + + MProduct product = null; + try { + product = new MProduct(Env.getCtx(), 0, null); + product.setM_Product_Category_ID(DictionaryIDs.M_Product_Category.STANDARD.id); + product.setName("testLandedCostForPOAndInvoice"); + product.setProductType(MProduct.PRODUCTTYPE_Item); + product.setIsStocked(true); + product.setIsSold(true); + product.setIsPurchased(true); + product.setC_UOM_ID(DictionaryIDs.C_UOM.EACH.id); + product.setC_TaxCategory_ID(DictionaryIDs.C_TaxCategory.STANDARD.id); + product.saveEx(); + + MPriceListVersion plv = MPriceList.get(DictionaryIDs.M_PriceList.PURCHASE.id).getPriceListVersion(null); + MProductPrice pp = new MProductPrice(Env.getCtx(), 0, getTrxName()); + pp.setM_PriceList_Version_ID(plv.getM_PriceList_Version_ID()); + pp.setM_Product_ID(product.get_ID()); + pp.setPriceStd(new BigDecimal("2")); + pp.setPriceList(new BigDecimal("2")); + pp.saveEx(); + + //create order + MOrder order = new MOrder(Env.getCtx(), 0, getTrxName()); + order.setBPartner(MBPartner.get(Env.getCtx(), DictionaryIDs.C_BPartner.PATIO.id)); + order.setC_DocTypeTarget_ID(DictionaryIDs.C_DocType.PURCHASE_ORDER.id); + order.setIsSOTrx(false); + order.setSalesRep_ID(DictionaryIDs.AD_User.GARDEN_ADMIN.id); + 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(new MProduct(Env.getCtx(), product.get_ID(), getTrxName())); + line1.setQty(new BigDecimal("1")); + line1.setDatePromised(today); + line1.setPrice(new BigDecimal("2")); + line1.saveEx(); + + MOrderLandedCost landedCost = new MOrderLandedCost(Env.getCtx(), 0, getTrxName()); + landedCost.setC_Order_ID(order.get_ID()); + landedCost.setLandedCostDistribution(MOrderLandedCost.LANDEDCOSTDISTRIBUTION_Costs); + landedCost.setM_CostElement_ID(DictionaryIDs.M_CostElement.FREIGHT.id); + landedCost.setAmt(new BigDecimal("0.30")); + landedCost.saveEx(); + + ProcessInfo info = MWorkflow.runDocumentActionWorkflow(order, DocAction.ACTION_Complete); + assertFalse(info.isError(), info.getSummary()); + order.load(getTrxName()); + assertEquals(DocAction.STATUS_Completed, order.getDocStatus()); + + MInOut receipt1 = new MInOut(order, DictionaryIDs.C_DocType.MM_RECEIPT.id, 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("1")); + receiptLine1.setQty(new BigDecimal("1")); + receiptLine1.saveEx(); + + info = MWorkflow.runDocumentActionWorkflow(receipt1, DocAction.ACTION_Complete); + assertFalse(info.isError(), info.getSummary()); + receipt1.load(getTrxName()); + assertEquals(DocAction.STATUS_Completed, receipt1.getDocStatus()); + if (!receipt1.isPosted()) { + String error = DocumentEngine.postImmediate(Env.getCtx(), receipt1.getAD_Client_ID(), receipt1.get_Table_ID(), receipt1.get_ID(), false, getTrxName()); + assertNull(error, error); + } + + List cds = MCostDetail.list(Env.getCtx(), "C_OrderLine_ID=?", line1.getC_OrderLine_ID(), 0, as.get_ID(), getTrxName()); + assertTrue(cds.size() == 2, "MCostDetail not found for order line1"); + for(MCostDetail cd : cds) { + if (cd.getM_CostElement_ID() == 0) { + assertEquals(1, cd.getQty().intValue(), "Unexpected MCostDetail Qty"); + assertEquals(new BigDecimal("2.00").setScale(2, RoundingMode.HALF_UP), cd.getAmt().setScale(2, RoundingMode.HALF_UP), "Unexpected MCostDetail Amt"); + } else if (cd.getM_CostElement_ID() == DictionaryIDs.M_CostElement.FREIGHT.id ) { + assertEquals(1, cd.getQty().intValue(), "Unexpected MCostDetail Qty"); + assertEquals(new BigDecimal("0.30").setScale(2, RoundingMode.HALF_UP), cd.getAmt().setScale(2, RoundingMode.HALF_UP), "Unexpected MCostDetail Amt"); + } + } + + product.set_TrxName(getTrxName()); + MCost cost = product.getCostingRecord(as, getAD_Org_ID(), 0, as.getCostingMethod()); + assertNotNull(cost, "No MCost record found"); + assertEquals(new BigDecimal("2.30"), cost.getCurrentCostPrice().setScale(2, RoundingMode.HALF_UP), "Unexpected current cost price"); + + ProductCost productCost = new ProductCost(Env.getCtx(), product.get_ID(), 0, getTrxName()); + MAccount assetAccount = productCost.getAccount(ProductCost.ACCTTYPE_P_Asset, as); + Doc doc = DocManager.getDocument(as, MInOut.Table_ID, receipt1.get_ID(), getTrxName()); + MAccount nivReceiptAccount = doc.getAccount(Doc.ACCTTYPE_NotInvoicedReceipts, as); + MAccount landedCostAccount = productCost.getAccount(ProductCost.ACCTTYPE_P_LandedCostClearing, as); + Query query = MFactAcct.createRecordIdQuery(MInOut.Table_ID, receipt1.get_ID(), as.get_ID(), getTrxName()); + List list = query.list(); + List expected = Arrays.asList(new FactAcct(assetAccount, new BigDecimal("2.30"), 2, true), + new FactAcct(nivReceiptAccount, new BigDecimal("2.00"), 2, false), new FactAcct(landedCostAccount, new BigDecimal("0.30"), 2, false)); + assertFactAcctEntries(list, expected); + + //invoice for MR + MInvoice invoice = new MInvoice(receipt1, receipt1.getMovementDate()); + invoice.setC_DocTypeTarget_ID(MDocType.DOCBASETYPE_APInvoice); + invoice.setDocStatus(DocAction.STATUS_Drafted); + invoice.setDocAction(DocAction.ACTION_Complete); + invoice.saveEx(); + + MInvoiceLine invoiceLine = new MInvoiceLine(invoice); + invoiceLine.setM_InOutLine_ID(receiptLine1.get_ID()); + invoiceLine.setLine(10); + invoiceLine.setProduct(product); + invoiceLine.setQty(BigDecimal.ONE); + invoiceLine.saveEx(); + + info = MWorkflow.runDocumentActionWorkflow(invoice, DocAction.ACTION_Complete); + invoice.load(getTrxName()); + assertFalse(info.isError(), info.getSummary()); + assertEquals(DocAction.STATUS_Completed, invoice.getDocStatus()); + + if (!invoice.isPosted()) { + String error = DocumentEngine.postImmediate(Env.getCtx(), invoice.getAD_Client_ID(), MInvoice.Table_ID, invoice.get_ID(), false, getTrxName()); + assertTrue(error == null); + } + invoice.load(getTrxName()); + assertTrue(invoice.isPosted()); + + Doc invoiceDoc = DocManager.getDocument(as, MInvoice.Table_ID, invoice.get_ID(), getTrxName()); + MAccount liabilityAccount = invoiceDoc.getAccount(Doc.ACCTTYPE_V_Liability, as); + MAccount inventoryClearingAccount = productCost.getAccount(ProductCost.ACCTTYPE_P_InventoryClearing, as); + query = MFactAcct.createRecordIdQuery(MInvoice.Table_ID, invoice.get_ID(), as.get_ID(), getTrxName()); + list = query.list(); + expected = Arrays.asList(new FactAcct(inventoryClearingAccount, new BigDecimal("2.00"), 2, true), + new FactAcct(liabilityAccount, new BigDecimal("2.00"), 2, false)); + assertFactAcctEntries(list, expected); + + //invoice for landed cost + MBPartner bp = MBPartner.get(Env.getCtx(), DictionaryIDs.C_BPartner.PATIO.id); + invoice = new MInvoice(Env.getCtx(), 0, getTrxName()); + invoice.setC_DocTypeTarget_ID(MDocType.DOCBASETYPE_APInvoice); + invoice.setBPartner(bp); + invoice.setDocStatus(DocAction.STATUS_Drafted); + invoice.setDocAction(DocAction.ACTION_Complete); + invoice.saveEx(); + + invoiceLine = new MInvoiceLine(invoice); + invoiceLine.setLine(10); + invoiceLine.setC_Charge_ID(DictionaryIDs.C_Charge.FREIGHT.id); + invoiceLine.setQty(BigDecimal.ONE); + invoiceLine.setPrice(new BigDecimal("0.40")); + invoiceLine.setC_UOM_ID(DictionaryIDs.C_UOM.EACH.id); + invoiceLine.saveEx(); + + MLandedCost invoiceLandedCost = new MLandedCost(Env.getCtx(), 0, getTrxName()); + invoiceLandedCost.setC_InvoiceLine_ID(invoiceLine.get_ID()); + invoiceLandedCost.setM_CostElement_ID(DictionaryIDs.M_CostElement.FREIGHT.id); + invoiceLandedCost.setM_InOut_ID(receipt1.get_ID()); + invoiceLandedCost.setM_InOutLine_ID(receiptLine1.get_ID()); + invoiceLandedCost.setLandedCostDistribution(MLandedCost.LANDEDCOSTDISTRIBUTION_Quantity); + invoiceLandedCost.saveEx(); + String error = invoiceLandedCost.allocateCosts(); + assertTrue(Util.isEmpty(error, true), error); + + info = MWorkflow.runDocumentActionWorkflow(invoice, DocAction.ACTION_Complete); + invoice.load(getTrxName()); + assertFalse(info.isError(), info.getSummary()); + assertEquals(DocAction.STATUS_Completed, invoice.getDocStatus()); + + if (!invoice.isPosted()) { + error = DocumentEngine.postImmediate(Env.getCtx(), invoice.getAD_Client_ID(), MInvoice.Table_ID, invoice.get_ID(), false, getTrxName()); + assertTrue(error == null, error); + } + invoice.load(getTrxName()); + assertTrue(invoice.isPosted()); + + doc = DocManager.getDocument(as, MInvoice.Table_ID, invoice.get_ID(), getTrxName()); + MAccount apAccount = doc.getAccount(Doc.ACCTTYPE_V_Liability, as); + MAccount landedCostClearingAccount = productCost.getAccount(ProductCost.ACCTTYPE_P_LandedCostClearing, as); + query = MFactAcct.createRecordIdQuery(MInvoice.Table_ID, invoice.get_ID(), as.get_ID(), getTrxName()); + list = query.list(); + expected = Arrays.asList(new FactAcct(assetAccount, new BigDecimal("0.10"), 2, true), + new FactAcct(landedCostClearingAccount, new BigDecimal("0.30"), 2, true), + new FactAcct(apAccount, new BigDecimal("0.40"), 2, false)); + assertFactAcctEntries(list, expected); + cost = product.getCostingRecord(as, getAD_Org_ID(), 0, as.getCostingMethod()); + assertEquals(new BigDecimal("2.40"), cost.getCurrentCostPrice().setScale(2, RoundingMode.HALF_UP), "Unexpected current cost price"); + } finally { + rollback(); + + if (product != null) { + product.set_TrxName(null); + product.deleteEx(true); + } + } + } + + @Test + public void testLandedCostWtihNoEstimateForPOAndInvoice() { + MClient client = MClient.get(Env.getCtx()); + MAcctSchema as = client.getAcctSchema(); + assertEquals(as.getCostingMethod(), MCostElement.COSTINGMETHOD_AveragePO, "Default costing method not Average PO"); + + MProduct product = null; + try { + product = new MProduct(Env.getCtx(), 0, null); + product.setM_Product_Category_ID(DictionaryIDs.M_Product_Category.STANDARD.id); + product.setName("testLandedCostWtihNoEstimateForPOAndInvoice"); + product.setProductType(MProduct.PRODUCTTYPE_Item); + product.setIsStocked(true); + product.setIsSold(true); + product.setIsPurchased(true); + product.setC_UOM_ID(DictionaryIDs.C_UOM.EACH.id); + product.setC_TaxCategory_ID(DictionaryIDs.C_TaxCategory.STANDARD.id); + product.saveEx(); + + MPriceListVersion plv = MPriceList.get(DictionaryIDs.M_PriceList.PURCHASE.id).getPriceListVersion(null); + MProductPrice pp = new MProductPrice(Env.getCtx(), 0, getTrxName()); + pp.setM_PriceList_Version_ID(plv.getM_PriceList_Version_ID()); + pp.setM_Product_ID(product.get_ID()); + pp.setPriceStd(new BigDecimal("2")); + pp.setPriceList(new BigDecimal("2")); + pp.saveEx(); + + //create order + MOrder order = new MOrder(Env.getCtx(), 0, getTrxName()); + order.setBPartner(MBPartner.get(Env.getCtx(), DictionaryIDs.C_BPartner.PATIO.id)); + order.setC_DocTypeTarget_ID(DictionaryIDs.C_DocType.PURCHASE_ORDER.id); + order.setIsSOTrx(false); + order.setSalesRep_ID(DictionaryIDs.AD_User.GARDEN_ADMIN.id); + 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(new MProduct(Env.getCtx(), product.get_ID(), getTrxName())); + line1.setQty(new BigDecimal("1")); + line1.setDatePromised(today); + line1.setPrice(new BigDecimal("2")); + line1.saveEx(); + + ProcessInfo info = MWorkflow.runDocumentActionWorkflow(order, DocAction.ACTION_Complete); + assertFalse(info.isError(), info.getSummary()); + order.load(getTrxName()); + assertEquals(DocAction.STATUS_Completed, order.getDocStatus()); + + MInOut receipt1 = new MInOut(order, DictionaryIDs.C_DocType.MM_RECEIPT.id, 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("1")); + receiptLine1.setQty(new BigDecimal("1")); + receiptLine1.saveEx(); + + info = MWorkflow.runDocumentActionWorkflow(receipt1, DocAction.ACTION_Complete); + assertFalse(info.isError(), info.getSummary()); + receipt1.load(getTrxName()); + assertEquals(DocAction.STATUS_Completed, receipt1.getDocStatus()); + if (!receipt1.isPosted()) { + String error = DocumentEngine.postImmediate(Env.getCtx(), receipt1.getAD_Client_ID(), receipt1.get_Table_ID(), receipt1.get_ID(), false, getTrxName()); + assertNull(error, error); + } + + List cds = MCostDetail.list(Env.getCtx(), "C_OrderLine_ID=?", line1.getC_OrderLine_ID(), 0, as.get_ID(), getTrxName()); + assertTrue(cds.size() == 1, "MCostDetail not found for order line1"); + for(MCostDetail cd : cds) { + if (cd.getM_CostElement_ID() == 0) { + assertEquals(1, cd.getQty().intValue(), "Unexpected MCostDetail Qty"); + assertEquals(new BigDecimal("2.00").setScale(2, RoundingMode.HALF_UP), cd.getAmt().setScale(2, RoundingMode.HALF_UP), "Unexpected MCostDetail Amt"); + } + } + + product.set_TrxName(getTrxName()); + MCost cost = product.getCostingRecord(as, getAD_Org_ID(), 0, as.getCostingMethod()); + assertNotNull(cost, "No MCost record found"); + assertEquals(new BigDecimal("2.00"), cost.getCurrentCostPrice().setScale(2, RoundingMode.HALF_UP), "Unexpected current cost price"); + + ProductCost productCost = new ProductCost(Env.getCtx(), product.get_ID(), 0, getTrxName()); + MAccount assetAccount = productCost.getAccount(ProductCost.ACCTTYPE_P_Asset, as); + Doc doc = DocManager.getDocument(as, MInOut.Table_ID, receipt1.get_ID(), getTrxName()); + MAccount nivReceiptAccount = doc.getAccount(Doc.ACCTTYPE_NotInvoicedReceipts, as); + Query query = MFactAcct.createRecordIdQuery(MInOut.Table_ID, receipt1.get_ID(), as.get_ID(), getTrxName()); + List list = query.list(); + List expected = Arrays.asList(new FactAcct(assetAccount, new BigDecimal("2.00"), 2, true), + new FactAcct(nivReceiptAccount, new BigDecimal("2.00"), 2, false)); + assertFactAcctEntries(list, expected); + + MInvoice invoice = new MInvoice(receipt1, receipt1.getMovementDate()); + invoice.setC_DocTypeTarget_ID(MDocType.DOCBASETYPE_APInvoice); + invoice.setDocStatus(DocAction.STATUS_Drafted); + invoice.setDocAction(DocAction.ACTION_Complete); + invoice.saveEx(); + + MInvoiceLine invoiceLine = new MInvoiceLine(invoice); + invoiceLine.setM_InOutLine_ID(receiptLine1.get_ID()); + invoiceLine.setLine(10); + invoiceLine.setProduct(product); + invoiceLine.setQty(BigDecimal.ONE); + invoiceLine.saveEx(); + + info = MWorkflow.runDocumentActionWorkflow(invoice, DocAction.ACTION_Complete); + invoice.load(getTrxName()); + assertFalse(info.isError(), info.getSummary()); + assertEquals(DocAction.STATUS_Completed, invoice.getDocStatus()); + + if (!invoice.isPosted()) { + String error = DocumentEngine.postImmediate(Env.getCtx(), invoice.getAD_Client_ID(), MInvoice.Table_ID, invoice.get_ID(), false, getTrxName()); + assertTrue(error == null); + } + invoice.load(getTrxName()); + assertTrue(invoice.isPosted()); + + Doc invoiceDoc = DocManager.getDocument(as, MInvoice.Table_ID, invoice.get_ID(), getTrxName()); + MAccount liabilityAccount = invoiceDoc.getAccount(Doc.ACCTTYPE_V_Liability, as); + MAccount inventoryClearingAccount = productCost.getAccount(ProductCost.ACCTTYPE_P_InventoryClearing, as); + query = MFactAcct.createRecordIdQuery(MInvoice.Table_ID, invoice.get_ID(), as.get_ID(), getTrxName()); + list = query.list(); + expected = Arrays.asList(new FactAcct(inventoryClearingAccount, new BigDecimal("2.00"), 2, true), + new FactAcct(liabilityAccount, new BigDecimal("2.00"), 2, false)); + assertFactAcctEntries(list, expected); + + MBPartner bp = MBPartner.get(Env.getCtx(), DictionaryIDs.C_BPartner.PATIO.id); + invoice = new MInvoice(Env.getCtx(), 0, getTrxName()); + invoice.setC_DocTypeTarget_ID(MDocType.DOCBASETYPE_APInvoice); + invoice.setBPartner(bp); + invoice.setDocStatus(DocAction.STATUS_Drafted); + invoice.setDocAction(DocAction.ACTION_Complete); + invoice.saveEx(); + + invoiceLine = new MInvoiceLine(invoice); + invoiceLine.setLine(10); + invoiceLine.setC_Charge_ID(DictionaryIDs.C_Charge.FREIGHT.id); + invoiceLine.setQty(BigDecimal.ONE); + invoiceLine.setPrice(new BigDecimal("0.30")); + invoiceLine.setC_UOM_ID(DictionaryIDs.C_UOM.EACH.id); + invoiceLine.saveEx(); + + MLandedCost landedCost = new MLandedCost(Env.getCtx(), 0, getTrxName()); + landedCost.setC_InvoiceLine_ID(invoiceLine.get_ID()); + landedCost.setM_CostElement_ID(DictionaryIDs.M_CostElement.FREIGHT.id); + landedCost.setM_InOut_ID(receipt1.get_ID()); + landedCost.setM_InOutLine_ID(receiptLine1.get_ID()); + landedCost.setLandedCostDistribution(MLandedCost.LANDEDCOSTDISTRIBUTION_Quantity); + landedCost.saveEx(); + String error = landedCost.allocateCosts(); + assertTrue(Util.isEmpty(error, true), error); + + info = MWorkflow.runDocumentActionWorkflow(invoice, DocAction.ACTION_Complete); + invoice.load(getTrxName()); + assertFalse(info.isError(), info.getSummary()); + assertEquals(DocAction.STATUS_Completed, invoice.getDocStatus()); + + if (!invoice.isPosted()) { + error = DocumentEngine.postImmediate(Env.getCtx(), invoice.getAD_Client_ID(), MInvoice.Table_ID, invoice.get_ID(), false, getTrxName()); + assertTrue(error == null, error); + } + invoice.load(getTrxName()); + assertTrue(invoice.isPosted()); + + doc = DocManager.getDocument(as, MInvoice.Table_ID, invoice.get_ID(), getTrxName()); + MAccount apAccount = doc.getAccount(Doc.ACCTTYPE_V_Liability, as); + query = MFactAcct.createRecordIdQuery(MInvoice.Table_ID, invoice.get_ID(), as.get_ID(), getTrxName()); + list = query.list(); + expected = Arrays.asList(new FactAcct(assetAccount, new BigDecimal("0.30"), 2, true), + new FactAcct(apAccount, new BigDecimal("0.30"), 2, false)); + assertFactAcctEntries(list, expected); + + cost = product.getCostingRecord(as, getAD_Org_ID(), 0, as.getCostingMethod()); + assertEquals(new BigDecimal("2.30"), cost.getCurrentCostPrice().setScale(2, RoundingMode.HALF_UP), "Unexpected current cost price"); + + info = MWorkflow.runDocumentActionWorkflow(invoice, DocAction.ACTION_Reverse_Correct); + invoice.load(getTrxName()); + assertFalse(info.isError(), info.getSummary()); + assertEquals(DocAction.STATUS_Reversed, invoice.getDocStatus()); + + MInvoice reversal = new MInvoice(Env.getCtx(), invoice.getReversal_ID(), getTrxName()); + assertEquals(invoice.getReversal_ID(), reversal.get_ID(), "Failed to load reversal invoice"); + if (!reversal.isPosted()) { + error = DocumentEngine.postImmediate(Env.getCtx(), reversal.getAD_Client_ID(), MInvoice.Table_ID, reversal.get_ID(), false, getTrxName()); + assertTrue(error == null, error); + reversal.load(getTrxName()); + } + query = MFactAcct.createRecordIdQuery(MInvoice.Table_ID, reversal.get_ID(), as.get_ID(), getTrxName()); + list = query.list(); + expected = Arrays.asList(new FactAcct(assetAccount, new BigDecimal("0.30"), 2, false), + new FactAcct(apAccount, new BigDecimal("0.30"), 2, true)); + assertFactAcctEntries(list, expected); + + cost = product.getCostingRecord(as, getAD_Org_ID(), 0, as.getCostingMethod()); + assertEquals(new BigDecimal("2.00"), cost.getCurrentCostPrice().setScale(2, RoundingMode.HALF_UP), "Unexpected current cost price"); + } finally { + rollback(); + + if (product != null) { + product.set_TrxName(null); + product.deleteEx(true); + } + } + } + + @Test + public void testUnplannedLandedCostWtihMultipleMRAndShipment() { + MClient client = MClient.get(Env.getCtx()); + MAcctSchema as = client.getAcctSchema(); + assertEquals(as.getCostingMethod(), MCostElement.COSTINGMETHOD_AveragePO, "Default costing method not Average PO"); + + MProduct p1 = null; + MProduct p2 = null; + try { + p1 = new MProduct(Env.getCtx(), 0, null); + p1.setM_Product_Category_ID(DictionaryIDs.M_Product_Category.STANDARD.id); + p1.setName("testUnplannedLandedCostWtihMultipleMRAndShipment1"); + p1.setProductType(MProduct.PRODUCTTYPE_Item); + p1.setIsStocked(true); + p1.setIsSold(true); + p1.setIsPurchased(true); + p1.setC_UOM_ID(DictionaryIDs.C_UOM.EACH.id); + p1.setC_TaxCategory_ID(DictionaryIDs.C_TaxCategory.STANDARD.id); + p1.saveEx(); + + MPriceListVersion plv = MPriceList.get(DictionaryIDs.M_PriceList.PURCHASE.id).getPriceListVersion(null); + MProductPrice pp = new MProductPrice(Env.getCtx(), 0, getTrxName()); + pp.setM_PriceList_Version_ID(plv.getM_PriceList_Version_ID()); + pp.setM_Product_ID(p1.get_ID()); + BigDecimal p1price = new BigDecimal("36.00"); + pp.setPriceStd(p1price); + pp.setPriceList(p1price); + pp.saveEx(); + + p2 = new MProduct(Env.getCtx(), 0, null); + p2.setM_Product_Category_ID(DictionaryIDs.M_Product_Category.STANDARD.id); + p2.setName("testUnplannedLandedCostWtihMultipleMRAndShipment2"); + p2.setProductType(MProduct.PRODUCTTYPE_Item); + p2.setIsStocked(true); + p2.setIsSold(true); + p2.setIsPurchased(true); + p2.setC_UOM_ID(DictionaryIDs.C_UOM.EACH.id); + p2.setC_TaxCategory_ID(DictionaryIDs.C_TaxCategory.STANDARD.id); + p2.saveEx(); + + pp = new MProductPrice(Env.getCtx(), 0, getTrxName()); + pp.setM_PriceList_Version_ID(plv.getM_PriceList_Version_ID()); + pp.setM_Product_ID(p2.get_ID()); + BigDecimal p2price = new BigDecimal("50.00"); + pp.setPriceStd(p2price); + pp.setPriceList(p2price); + pp.saveEx(); + + //create purchase order + MOrder purchaseOrder = new MOrder(Env.getCtx(), 0, getTrxName()); + purchaseOrder.setBPartner(MBPartner.get(Env.getCtx(), DictionaryIDs.C_BPartner.PATIO.id)); + purchaseOrder.setC_DocTypeTarget_ID(DictionaryIDs.C_DocType.PURCHASE_ORDER.id); + purchaseOrder.setIsSOTrx(false); + purchaseOrder.setSalesRep_ID(DictionaryIDs.AD_User.GARDEN_ADMIN.id); + purchaseOrder.setDocStatus(DocAction.STATUS_Drafted); + purchaseOrder.setDocAction(DocAction.ACTION_Complete); + Timestamp today = TimeUtil.getDay(System.currentTimeMillis()); + purchaseOrder.setDateOrdered(today); + purchaseOrder.setDatePromised(today); + purchaseOrder.saveEx(); + + MOrderLine poLine1 = new MOrderLine(purchaseOrder); + poLine1.setLine(10); + poLine1.setProduct(new MProduct(Env.getCtx(), p1.get_ID(), getTrxName())); + BigDecimal orderQty = new BigDecimal("100"); + poLine1.setQty(orderQty); + poLine1.setDatePromised(today); + poLine1.setPrice(p1price); + poLine1.saveEx(); + + MOrderLine poLine2 = new MOrderLine(purchaseOrder); + poLine2.setLine(10); + poLine2.setProduct(new MProduct(Env.getCtx(), p2.get_ID(), getTrxName())); + poLine2.setQty(orderQty); + poLine2.setDatePromised(today); + poLine2.setPrice(p2price); + poLine2.saveEx(); + + ProcessInfo info = MWorkflow.runDocumentActionWorkflow(purchaseOrder, DocAction.ACTION_Complete); + assertFalse(info.isError(), info.getSummary()); + purchaseOrder.load(getTrxName()); + assertEquals(DocAction.STATUS_Completed, purchaseOrder.getDocStatus()); + + //mr1 for 10 each + MInOut receipt1 = new MInOut(purchaseOrder, DictionaryIDs.C_DocType.MM_RECEIPT.id, purchaseOrder.getDateOrdered()); + receipt1.setDocStatus(DocAction.STATUS_Drafted); + receipt1.setDocAction(DocAction.ACTION_Complete); + receipt1.saveEx(); + + MInOutLine receipt1Line1 = new MInOutLine(receipt1); + BigDecimal mr1Qty = new BigDecimal("10"); + receipt1Line1.setOrderLine(poLine1, 0, mr1Qty); + receipt1Line1.setQty(mr1Qty); + receipt1Line1.saveEx(); + + MInOutLine receipt1Line2 = new MInOutLine(receipt1); + receipt1Line2.setOrderLine(poLine2, 0, mr1Qty); + receipt1Line2.setQty(mr1Qty); + receipt1Line2.saveEx(); + + info = MWorkflow.runDocumentActionWorkflow(receipt1, DocAction.ACTION_Complete); + assertFalse(info.isError(), info.getSummary()); + receipt1.load(getTrxName()); + assertEquals(DocAction.STATUS_Completed, receipt1.getDocStatus()); + if (!receipt1.isPosted()) { + String error = DocumentEngine.postImmediate(Env.getCtx(), receipt1.getAD_Client_ID(), receipt1.get_Table_ID(), receipt1.get_ID(), false, getTrxName()); + assertNull(error, error); + } + + //assert p1 cost and posting + List cds = MCostDetail.list(Env.getCtx(), "C_OrderLine_ID=?", poLine1.getC_OrderLine_ID(), 0, as.get_ID(), getTrxName()); + assertTrue(cds.size() == 1, "MCostDetail not found for order line1"); + for(MCostDetail cd : cds) { + if (cd.getM_CostElement_ID() == 0) { + assertEquals(10, cd.getQty().intValue(), "Unexpected MCostDetail Qty"); + assertEquals(p1price.multiply(mr1Qty).setScale(2, RoundingMode.HALF_UP), cd.getAmt().setScale(2, RoundingMode.HALF_UP), "Unexpected MCostDetail Amt"); + } + } + + p1.set_TrxName(getTrxName()); + MCost p1mcost = p1.getCostingRecord(as, getAD_Org_ID(), 0, as.getCostingMethod()); + assertNotNull(p1mcost, "No MCost record found"); + assertEquals(p1price, p1mcost.getCurrentCostPrice().setScale(2, RoundingMode.HALF_UP), "Unexpected current cost price"); + + ProductCost p1ProductCost = new ProductCost(Env.getCtx(), p1.get_ID(), 0, getTrxName()); + MAccount assetAccount = p1ProductCost.getAccount(ProductCost.ACCTTYPE_P_Asset, as); + Doc doc = DocManager.getDocument(as, MInOut.Table_ID, receipt1.get_ID(), getTrxName()); + MAccount nivReceiptAccount = doc.getAccount(Doc.ACCTTYPE_NotInvoicedReceipts, as); + Query query = MFactAcct.createRecordIdQuery(MInOut.Table_ID, receipt1.get_ID(), as.get_ID(), getTrxName()); + List factAccts = query.list(); + List expected = Arrays.asList(new FactAcct(assetAccount, p1price.multiply(mr1Qty), 2, true), + new FactAcct(nivReceiptAccount, p1price.multiply(mr1Qty), 2, false)); + assertFactAcctEntries(factAccts, expected); + + //assert p2 cost and posting + cds = MCostDetail.list(Env.getCtx(), "C_OrderLine_ID=?", poLine2.getC_OrderLine_ID(), 0, as.get_ID(), getTrxName()); + assertTrue(cds.size() == 1, "MCostDetail not found for order line2"); + for(MCostDetail cd : cds) { + if (cd.getM_CostElement_ID() == 0) { + assertEquals(10, cd.getQty().intValue(), "Unexpected MCostDetail Qty"); + assertEquals(p2price.multiply(mr1Qty).setScale(2, RoundingMode.HALF_UP), cd.getAmt().setScale(2, RoundingMode.HALF_UP), "Unexpected MCostDetail Amt"); + } + } + + p2.set_TrxName(getTrxName()); + MCost p2mcost = p2.getCostingRecord(as, getAD_Org_ID(), 0, as.getCostingMethod()); + assertNotNull(p2mcost, "No MCost record found"); + assertEquals(p2price, p2mcost.getCurrentCostPrice().setScale(2, RoundingMode.HALF_UP), "Unexpected current cost price"); + + ProductCost p2ProductCost = new ProductCost(Env.getCtx(), p2.get_ID(), 0, getTrxName()); + assetAccount = p2ProductCost.getAccount(ProductCost.ACCTTYPE_P_Asset, as); + expected = Arrays.asList(new FactAcct(assetAccount, p2price.multiply(mr1Qty), 2, true), + new FactAcct(nivReceiptAccount, p2price.multiply(mr1Qty), 2, false)); + assertFactAcctEntries(factAccts, expected); + + //mr2 for 90 each + MInOut receipt2 = new MInOut(purchaseOrder, DictionaryIDs.C_DocType.MM_RECEIPT.id, purchaseOrder.getDateOrdered()); + receipt2.setDocStatus(DocAction.STATUS_Drafted); + receipt2.setDocAction(DocAction.ACTION_Complete); + receipt2.saveEx(); + + MInOutLine receipt2Line1 = new MInOutLine(receipt2); + BigDecimal mr2Qty = new BigDecimal("90"); + receipt2Line1.setOrderLine(poLine1, 0, mr2Qty); + receipt2Line1.setQty(mr2Qty); + receipt2Line1.saveEx(); + + MInOutLine receipt2Line2 = new MInOutLine(receipt2); + receipt2Line2.setOrderLine(poLine2, 0, mr2Qty); + receipt2Line2.setQty(mr2Qty); + receipt2Line2.saveEx(); + + info = MWorkflow.runDocumentActionWorkflow(receipt2, DocAction.ACTION_Complete); + assertFalse(info.isError(), info.getSummary()); + receipt2.load(getTrxName()); + assertEquals(DocAction.STATUS_Completed, receipt2.getDocStatus()); + if (!receipt2.isPosted()) { + String error = DocumentEngine.postImmediate(Env.getCtx(), receipt2.getAD_Client_ID(), receipt2.get_Table_ID(), receipt2.get_ID(), false, getTrxName()); + assertNull(error, error); + } + + //full po invoice + MInvoice purchaseInvoice = new MInvoice(purchaseOrder, DictionaryIDs.C_DocType.AP_INVOICE.id, purchaseOrder.getDateOrdered()); + purchaseInvoice.setDocStatus(DocAction.STATUS_Drafted); + purchaseInvoice.setDocAction(DocAction.ACTION_Complete); + purchaseInvoice.saveEx(); + + MInvoiceLine piLine1 = new MInvoiceLine(purchaseInvoice); + piLine1.setOrderLine(poLine1); + piLine1.setLine(10); + piLine1.setProduct(p1); + piLine1.setQty(poLine1.getQtyOrdered()); + piLine1.saveEx(); + + MInvoiceLine piLine2 = new MInvoiceLine(purchaseInvoice); + piLine2.setOrderLine(poLine2); + piLine2.setLine(10); + piLine2.setProduct(p2); + piLine2.setQty(poLine2.getQtyOrdered()); + piLine2.saveEx(); + + info = MWorkflow.runDocumentActionWorkflow(purchaseInvoice, DocAction.ACTION_Complete); + purchaseInvoice.load(getTrxName()); + assertFalse(info.isError(), info.getSummary()); + assertEquals(DocAction.STATUS_Completed, purchaseInvoice.getDocStatus()); + + if (!purchaseInvoice.isPosted()) { + String error = DocumentEngine.postImmediate(Env.getCtx(), purchaseInvoice.getAD_Client_ID(), MInvoice.Table_ID, purchaseInvoice.get_ID(), false, getTrxName()); + assertTrue(error == null); + } + purchaseInvoice.load(getTrxName()); + assertTrue(purchaseInvoice.isPosted()); + + Doc invoiceDoc = DocManager.getDocument(as, MInvoice.Table_ID, purchaseInvoice.get_ID(), getTrxName()); + MAccount liabilityAccount = invoiceDoc.getAccount(Doc.ACCTTYPE_V_Liability, as); + MAccount inventoryClearingAccount = p1ProductCost.getAccount(ProductCost.ACCTTYPE_P_InventoryClearing, as); + query = MFactAcct.createRecordIdQuery(MInvoice.Table_ID, purchaseInvoice.get_ID(), as.get_ID(), getTrxName()); + factAccts = query.list(); + expected = Arrays.asList(new FactAcct(inventoryClearingAccount, p1price.multiply(orderQty), 2, true), + new FactAcct(inventoryClearingAccount, p2price.multiply(orderQty), 2, true), + new FactAcct(liabilityAccount, p1price.multiply(orderQty).add(p2price.multiply(orderQty)), 2, false)); + assertFactAcctEntries(factAccts, expected); + + //so and shipment + MBPartner customer = MBPartner.get(Env.getCtx(), DictionaryIDs.C_BPartner.JOE_BLOCK.id); + MOrder salesOrder = new MOrder(Env.getCtx(), 0, getTrxName()); + salesOrder.setC_DocTypeTarget_ID(MOrder.DocSubTypeSO_Standard); + salesOrder.setBPartner(customer); + salesOrder.setDeliveryRule(MOrder.DELIVERYRULE_CompleteOrder); + salesOrder.setDocStatus(DocAction.STATUS_Drafted); + salesOrder.setDocAction(DocAction.ACTION_Complete); + salesOrder.setDatePromised(today); + salesOrder.saveEx(); + + MOrderLine soLine1 = new MOrderLine(salesOrder); + soLine1.setLine(10); + soLine1.setProduct(p1); + BigDecimal p1ShipQty = new BigDecimal("76"); + soLine1.setQty(p1ShipQty); + soLine1.setDatePromised(today); + soLine1.setPrice(new BigDecimal("50")); + soLine1.saveEx(); + + MOrderLine soLine2 = new MOrderLine(salesOrder); + soLine2.setLine(20); + soLine2.setProduct(p2); + BigDecimal p2ShipQty = new BigDecimal("82"); + soLine2.setQty(p2ShipQty); + soLine2.setDatePromised(today); + soLine2.setPrice(new BigDecimal("70")); + soLine2.saveEx(); + + info = MWorkflow.runDocumentActionWorkflow(salesOrder, DocAction.ACTION_Complete); + salesOrder.load(getTrxName()); + assertFalse(info.isError(), info.getSummary()); + assertEquals(DocAction.STATUS_Completed, salesOrder.getDocStatus()); + + MInOut shipment = new MInOut(salesOrder, DictionaryIDs.C_DocType.MM_SHIPMENT.id, salesOrder.getDateOrdered()); + shipment.setDocStatus(DocAction.STATUS_Drafted); + shipment.setDocAction(DocAction.ACTION_Complete); + shipment.saveEx(); + + MInOutLine shipmentLine1 = new MInOutLine(shipment); + shipmentLine1.setOrderLine(soLine1, 0, soLine1.getQtyOrdered()); + shipmentLine1.setQty(soLine1.getQtyOrdered()); + shipmentLine1.saveEx(); + + MInOutLine shipmentLine2 = new MInOutLine(shipment); + shipmentLine2.setOrderLine(soLine2, 0, soLine2.getQtyOrdered()); + shipmentLine2.setQty(soLine2.getQtyOrdered()); + shipmentLine2.saveEx(); + + info = MWorkflow.runDocumentActionWorkflow(shipment, DocAction.ACTION_Complete); + assertFalse(info.isError(), info.getSummary()); + shipment.load(getTrxName()); + assertEquals(DocAction.STATUS_Completed, shipment.getDocStatus()); + + //landed cost invoice + MBPartner freightBP = MBPartner.get(Env.getCtx(), DictionaryIDs.C_BPartner.PATIO.id); + MInvoice freightInvoice = new MInvoice(Env.getCtx(), 0, getTrxName()); + freightInvoice.setC_DocTypeTarget_ID(MDocType.DOCBASETYPE_APInvoice); + freightInvoice.setBPartner(freightBP); + freightInvoice.setDocStatus(DocAction.STATUS_Drafted); + freightInvoice.setDocAction(DocAction.ACTION_Complete); + freightInvoice.saveEx(); + + MInvoiceLine fiLine = new MInvoiceLine(freightInvoice); + fiLine.setLine(10); + fiLine.setC_Charge_ID(DictionaryIDs.C_Charge.FREIGHT.id); + fiLine.setQty(BigDecimal.ONE); + BigDecimal freightPrice = new BigDecimal("1000.00"); + fiLine.setPrice(freightPrice); + fiLine.setC_UOM_ID(DictionaryIDs.C_UOM.EACH.id); + fiLine.saveEx(); + + MLandedCost landedCost = new MLandedCost(Env.getCtx(), 0, getTrxName()); + landedCost.setC_InvoiceLine_ID(fiLine.get_ID()); + landedCost.setM_CostElement_ID(DictionaryIDs.M_CostElement.FREIGHT.id); + landedCost.setM_InOut_ID(receipt1.get_ID()); + landedCost.setM_InOutLine_ID(receipt1Line1.get_ID()); + landedCost.setLandedCostDistribution(MLandedCost.LANDEDCOSTDISTRIBUTION_Costs); + landedCost.saveEx(); + + landedCost = new MLandedCost(Env.getCtx(), 0, getTrxName()); + landedCost.setC_InvoiceLine_ID(fiLine.get_ID()); + landedCost.setM_CostElement_ID(DictionaryIDs.M_CostElement.FREIGHT.id); + landedCost.setM_InOut_ID(receipt1.get_ID()); + landedCost.setM_InOutLine_ID(receipt1Line2.get_ID()); + landedCost.setLandedCostDistribution(MLandedCost.LANDEDCOSTDISTRIBUTION_Costs); + landedCost.saveEx(); + + landedCost = new MLandedCost(Env.getCtx(), 0, getTrxName()); + landedCost.setC_InvoiceLine_ID(fiLine.get_ID()); + landedCost.setM_CostElement_ID(DictionaryIDs.M_CostElement.FREIGHT.id); + landedCost.setM_InOut_ID(receipt2.get_ID()); + landedCost.setM_InOutLine_ID(receipt2Line1.get_ID()); + landedCost.setLandedCostDistribution(MLandedCost.LANDEDCOSTDISTRIBUTION_Costs); + landedCost.saveEx(); + + landedCost = new MLandedCost(Env.getCtx(), 0, getTrxName()); + landedCost.setC_InvoiceLine_ID(fiLine.get_ID()); + landedCost.setM_CostElement_ID(DictionaryIDs.M_CostElement.FREIGHT.id); + landedCost.setM_InOut_ID(receipt2.get_ID()); + landedCost.setM_InOutLine_ID(receipt2Line2.get_ID()); + landedCost.setLandedCostDistribution(MLandedCost.LANDEDCOSTDISTRIBUTION_Costs); + landedCost.saveEx(); + + String error = landedCost.allocateCosts(); + assertTrue(Util.isEmpty(error, true), error); + + BigDecimal totalBase = purchaseInvoice.getGrandTotal(); + BigDecimal p1a1 = p1price.multiply(mr1Qty).multiply(fiLine.getLineNetAmt()).divide(totalBase, 6, RoundingMode.HALF_UP); + BigDecimal p1a2 = p1price.multiply(mr2Qty).multiply(fiLine.getLineNetAmt()).divide(totalBase, 6, RoundingMode.HALF_UP); + BigDecimal p2a1 = p2price.multiply(mr1Qty).multiply(fiLine.getLineNetAmt()).divide(totalBase, 6, RoundingMode.HALF_UP); + BigDecimal p2a2 = p2price.multiply(mr2Qty).multiply(fiLine.getLineNetAmt()).divide(totalBase, 6, RoundingMode.HALF_UP); + + MLandedCostAllocation[] allocations = MLandedCostAllocation.getOfInvoiceLine(Env.getCtx(), fiLine.get_ID(), getTrxName()); + assertEquals(4, allocations.length, "Unexpected number of landed cost allocation line"); + for (MLandedCostAllocation allocation : allocations) { + if (allocation.getM_Product_ID() == p1.get_ID() && allocation.getQty().intValue() == 10) { + assertEquals(p1a1.setScale(2, RoundingMode.HALF_UP), allocation.getAmt().setScale(2, RoundingMode.HALF_UP), "Unexpected landed cost allocation amount"); + } else if (allocation.getM_Product_ID() == p1.get_ID() && allocation.getQty().intValue() == 90) { + assertEquals(p1a2.setScale(2, RoundingMode.HALF_UP), allocation.getAmt().setScale(2, RoundingMode.HALF_UP), "Unexpected landed cost allocation amount"); + } else if (allocation.getM_Product_ID() == p2.get_ID() && allocation.getQty().intValue() == 10) { + assertEquals(p2a1.setScale(2, RoundingMode.HALF_UP), allocation.getAmt().setScale(2, RoundingMode.HALF_UP), "Unexpected landed cost allocation amount"); + } else if (allocation.getM_Product_ID() == p2.get_ID() && allocation.getQty().intValue() == 90) { + assertEquals(p2a2.setScale(2, RoundingMode.HALF_UP), allocation.getAmt().setScale(2, RoundingMode.HALF_UP), "Unexpected landed cost allocation amount"); + } else { + fail("Unknown landed cost allocation line: " + allocation); + } + } + + info = MWorkflow.runDocumentActionWorkflow(freightInvoice, DocAction.ACTION_Complete); + freightInvoice.load(getTrxName()); + assertFalse(info.isError(), info.getSummary()); + assertEquals(DocAction.STATUS_Completed, freightInvoice.getDocStatus()); + + if (!freightInvoice.isPosted()) { + error = DocumentEngine.postImmediate(Env.getCtx(), freightInvoice.getAD_Client_ID(), MInvoice.Table_ID, freightInvoice.get_ID(), false, getTrxName()); + assertTrue(error == null, error); + } + freightInvoice.load(getTrxName()); + assertTrue(freightInvoice.isPosted()); + + //assert freight invoice posting + doc = DocManager.getDocument(as, MInvoice.Table_ID, freightInvoice.get_ID(), getTrxName()); + MAccount apAccount = doc.getAccount(Doc.ACCTTYPE_V_Liability, as); + query = MFactAcct.createRecordIdQuery(MInvoice.Table_ID, freightInvoice.get_ID(), as.get_ID(), getTrxName()); + factAccts = query.list(); + BigDecimal p1QtyOnHand = orderQty.subtract(p1ShipQty); + BigDecimal p2QtyOnHand = orderQty.subtract(p2ShipQty); + BigDecimal p1a1Qty = mr1Qty; + BigDecimal p1a2Qty = p1QtyOnHand.subtract(p1a1Qty); + BigDecimal p2a1Qty = mr1Qty; + BigDecimal p2a2Qty = p2QtyOnHand.subtract(p2a1Qty); + MAccount varianceAccount = p1ProductCost.getAccount(ProductCost.ACCTTYPE_P_AverageCostVariance, as); + BigDecimal p1a2Asset = p1a2.divide(mr2Qty, RoundingMode.HALF_UP).multiply(p1a2Qty); + BigDecimal p2a2Asset = p2a2.divide(mr2Qty, RoundingMode.HALF_UP).multiply(p2a2Qty); + expected = Arrays.asList(new FactAcct(assetAccount, p1a1, 2, true), + new FactAcct(assetAccount, p2a1, 2, true), + new FactAcct(assetAccount, p1a2Asset, 2, true), + new FactAcct(varianceAccount, p1a2.subtract(p1a2Asset), 0, true), + new FactAcct(assetAccount, p2a2Asset, 2, true), + new FactAcct(varianceAccount, p2a2.subtract(p2a2Asset), 0, true), + new FactAcct(apAccount, freightInvoice.getGrandTotal(), 2, false)); + assertFactAcctEntries(factAccts, expected); + + p1mcost = p1.getCostingRecord(as, getAD_Org_ID(), 0, as.getCostingMethod()); + assertEquals(p1price.add(p1a1.divide(p1QtyOnHand, 2, RoundingMode.HALF_UP)) + .add(p1a2Asset.divide(p1QtyOnHand, 2, RoundingMode.HALF_UP)) + .setScale(1, RoundingMode.HALF_UP), p1mcost.getCurrentCostPrice().setScale(1, RoundingMode.HALF_UP), "Unexpected current cost price"); + + p1mcost = p2.getCostingRecord(as, getAD_Org_ID(), 0, as.getCostingMethod()); + assertEquals(p2price.add(p2a1.divide(p2QtyOnHand, 2, RoundingMode.HALF_UP)) + .add(p2a2Asset.divide(p2QtyOnHand, 2, RoundingMode.HALF_UP)) + .setScale(1, RoundingMode.HALF_UP), p1mcost.getCurrentCostPrice().setScale(1, RoundingMode.HALF_UP), "Unexpected current cost price"); + } finally { + rollback(); + + if (p1 != null) { + p1.set_TrxName(null); + p1.deleteEx(true); + } + + if (p2 != null) { + p2.set_TrxName(null); + p2.deleteEx(true); + } + } + } + + @Test + public void testUnplannedLandedCostReversal() { + MClient client = MClient.get(Env.getCtx()); + MAcctSchema as = client.getAcctSchema(); + assertEquals(as.getCostingMethod(), MCostElement.COSTINGMETHOD_AveragePO, "Default costing method not Average PO"); + + MProduct p1 = null; + MProduct p2 = null; + try { + p1 = new MProduct(Env.getCtx(), 0, null); + p1.setM_Product_Category_ID(DictionaryIDs.M_Product_Category.STANDARD.id); + p1.setName("testUnplannedLandedCostReversal1"); + p1.setProductType(MProduct.PRODUCTTYPE_Item); + p1.setIsStocked(true); + p1.setIsSold(true); + p1.setIsPurchased(true); + p1.setC_UOM_ID(DictionaryIDs.C_UOM.EACH.id); + p1.setC_TaxCategory_ID(DictionaryIDs.C_TaxCategory.STANDARD.id); + p1.saveEx(); + + MPriceListVersion plv = MPriceList.get(DictionaryIDs.M_PriceList.PURCHASE.id).getPriceListVersion(null); + MProductPrice pp = new MProductPrice(Env.getCtx(), 0, getTrxName()); + pp.setM_PriceList_Version_ID(plv.getM_PriceList_Version_ID()); + pp.setM_Product_ID(p1.get_ID()); + BigDecimal p1price = new BigDecimal("30.00"); + pp.setPriceStd(p1price); + pp.setPriceList(p1price); + pp.saveEx(); + + p2 = new MProduct(Env.getCtx(), 0, null); + p2.setM_Product_Category_ID(DictionaryIDs.M_Product_Category.STANDARD.id); + p2.setName("testUnplannedLandedCostReversal2"); + p2.setProductType(MProduct.PRODUCTTYPE_Item); + p2.setIsStocked(true); + p2.setIsSold(true); + p2.setIsPurchased(true); + p2.setC_UOM_ID(DictionaryIDs.C_UOM.EACH.id); + p2.setC_TaxCategory_ID(DictionaryIDs.C_TaxCategory.STANDARD.id); + p2.saveEx(); + + pp = new MProductPrice(Env.getCtx(), 0, getTrxName()); + pp.setM_PriceList_Version_ID(plv.getM_PriceList_Version_ID()); + pp.setM_Product_ID(p2.get_ID()); + BigDecimal p2price = new BigDecimal("50.00"); + pp.setPriceStd(p2price); + pp.setPriceList(p2price); + pp.saveEx(); + + //create purchase order + MOrder purchaseOrder = new MOrder(Env.getCtx(), 0, getTrxName()); + purchaseOrder.setBPartner(MBPartner.get(Env.getCtx(), DictionaryIDs.C_BPartner.PATIO.id)); + purchaseOrder.setC_DocTypeTarget_ID(DictionaryIDs.C_DocType.PURCHASE_ORDER.id); + purchaseOrder.setIsSOTrx(false); + purchaseOrder.setSalesRep_ID(DictionaryIDs.AD_User.GARDEN_ADMIN.id); + purchaseOrder.setDocStatus(DocAction.STATUS_Drafted); + purchaseOrder.setDocAction(DocAction.ACTION_Complete); + Timestamp today = TimeUtil.getDay(System.currentTimeMillis()); + purchaseOrder.setDateOrdered(today); + purchaseOrder.setDatePromised(today); + purchaseOrder.saveEx(); + + MOrderLine poLine1 = new MOrderLine(purchaseOrder); + poLine1.setLine(10); + poLine1.setProduct(new MProduct(Env.getCtx(), p1.get_ID(), getTrxName())); + BigDecimal orderQty = new BigDecimal("10"); + poLine1.setQty(orderQty); + poLine1.setDatePromised(today); + poLine1.setPrice(p1price); + poLine1.saveEx(); + + MOrderLine poLine2 = new MOrderLine(purchaseOrder); + poLine2.setLine(10); + poLine2.setProduct(new MProduct(Env.getCtx(), p2.get_ID(), getTrxName())); + poLine2.setQty(orderQty); + poLine2.setDatePromised(today); + poLine2.setPrice(p2price); + poLine2.saveEx(); + + ProcessInfo info = MWorkflow.runDocumentActionWorkflow(purchaseOrder, DocAction.ACTION_Complete); + assertFalse(info.isError(), info.getSummary()); + purchaseOrder.load(getTrxName()); + assertEquals(DocAction.STATUS_Completed, purchaseOrder.getDocStatus()); + + //mr1 for 10 each + MInOut receipt1 = new MInOut(purchaseOrder, DictionaryIDs.C_DocType.MM_RECEIPT.id, purchaseOrder.getDateOrdered()); + receipt1.setDocStatus(DocAction.STATUS_Drafted); + receipt1.setDocAction(DocAction.ACTION_Complete); + receipt1.saveEx(); + + MInOutLine receipt1Line1 = new MInOutLine(receipt1); + BigDecimal mr1Qty = new BigDecimal("10"); + receipt1Line1.setOrderLine(poLine1, 0, mr1Qty); + receipt1Line1.setQty(mr1Qty); + receipt1Line1.saveEx(); + + MInOutLine receipt1Line2 = new MInOutLine(receipt1); + receipt1Line2.setOrderLine(poLine2, 0, mr1Qty); + receipt1Line2.setQty(mr1Qty); + receipt1Line2.saveEx(); + + info = MWorkflow.runDocumentActionWorkflow(receipt1, DocAction.ACTION_Complete); + assertFalse(info.isError(), info.getSummary()); + receipt1.load(getTrxName()); + assertEquals(DocAction.STATUS_Completed, receipt1.getDocStatus()); + if (!receipt1.isPosted()) { + String error = DocumentEngine.postImmediate(Env.getCtx(), receipt1.getAD_Client_ID(), receipt1.get_Table_ID(), receipt1.get_ID(), false, getTrxName()); + assertNull(error, error); + } + + //assert p1 cost and posting + List cds = MCostDetail.list(Env.getCtx(), "C_OrderLine_ID=?", poLine1.getC_OrderLine_ID(), 0, as.get_ID(), getTrxName()); + assertTrue(cds.size() == 1, "MCostDetail not found for order line1"); + for(MCostDetail cd : cds) { + if (cd.getM_CostElement_ID() == 0) { + assertEquals(10, cd.getQty().intValue(), "Unexpected MCostDetail Qty"); + assertEquals(p1price.multiply(mr1Qty).setScale(2, RoundingMode.HALF_UP), cd.getAmt().setScale(2, RoundingMode.HALF_UP), "Unexpected MCostDetail Amt"); + } + } + + p1.set_TrxName(getTrxName()); + MCost p1mcost = p1.getCostingRecord(as, getAD_Org_ID(), 0, as.getCostingMethod()); + assertNotNull(p1mcost, "No MCost record found"); + assertEquals(p1price, p1mcost.getCurrentCostPrice().setScale(2, RoundingMode.HALF_UP), "Unexpected current cost price"); + + ProductCost p1ProductCost = new ProductCost(Env.getCtx(), p1.get_ID(), 0, getTrxName()); + MAccount assetAccount = p1ProductCost.getAccount(ProductCost.ACCTTYPE_P_Asset, as); + Doc doc = DocManager.getDocument(as, MInOut.Table_ID, receipt1.get_ID(), getTrxName()); + MAccount nivReceiptAccount = doc.getAccount(Doc.ACCTTYPE_NotInvoicedReceipts, as); + Query query = MFactAcct.createRecordIdQuery(MInOut.Table_ID, receipt1.get_ID(), as.get_ID(), getTrxName()); + List factAccts = query.list(); + List expected = Arrays.asList(new FactAcct(assetAccount, p1price.multiply(mr1Qty), 2, true), + new FactAcct(nivReceiptAccount, p1price.multiply(mr1Qty), 2, false)); + assertFactAcctEntries(factAccts, expected); + + //assert p2 cost and posting + cds = MCostDetail.list(Env.getCtx(), "C_OrderLine_ID=?", poLine2.getC_OrderLine_ID(), 0, as.get_ID(), getTrxName()); + assertTrue(cds.size() == 1, "MCostDetail not found for order line2"); + for(MCostDetail cd : cds) { + if (cd.getM_CostElement_ID() == 0) { + assertEquals(10, cd.getQty().intValue(), "Unexpected MCostDetail Qty"); + assertEquals(p2price.multiply(mr1Qty).setScale(2, RoundingMode.HALF_UP), cd.getAmt().setScale(2, RoundingMode.HALF_UP), "Unexpected MCostDetail Amt"); + } + } + + p2.set_TrxName(getTrxName()); + MCost p2mcost = p2.getCostingRecord(as, getAD_Org_ID(), 0, as.getCostingMethod()); + assertNotNull(p2mcost, "No MCost record found"); + assertEquals(p2price, p2mcost.getCurrentCostPrice().setScale(2, RoundingMode.HALF_UP), "Unexpected current cost price"); + + ProductCost p2ProductCost = new ProductCost(Env.getCtx(), p2.get_ID(), 0, getTrxName()); + assetAccount = p2ProductCost.getAccount(ProductCost.ACCTTYPE_P_Asset, as); + expected = Arrays.asList(new FactAcct(assetAccount, p2price.multiply(mr1Qty), 2, true), + new FactAcct(nivReceiptAccount, p2price.multiply(mr1Qty), 2, false)); + assertFactAcctEntries(factAccts, expected); + + //full po invoice + MInvoice purchaseInvoice = new MInvoice(purchaseOrder, DictionaryIDs.C_DocType.AP_INVOICE.id, purchaseOrder.getDateOrdered()); + purchaseInvoice.setDocStatus(DocAction.STATUS_Drafted); + purchaseInvoice.setDocAction(DocAction.ACTION_Complete); + purchaseInvoice.saveEx(); + + MInvoiceLine piLine1 = new MInvoiceLine(purchaseInvoice); + piLine1.setOrderLine(poLine1); + piLine1.setLine(10); + piLine1.setProduct(p1); + piLine1.setQty(poLine1.getQtyOrdered()); + piLine1.saveEx(); + + MInvoiceLine piLine2 = new MInvoiceLine(purchaseInvoice); + piLine2.setOrderLine(poLine2); + piLine2.setLine(10); + piLine2.setProduct(p2); + piLine2.setQty(poLine2.getQtyOrdered()); + piLine2.saveEx(); + + info = MWorkflow.runDocumentActionWorkflow(purchaseInvoice, DocAction.ACTION_Complete); + purchaseInvoice.load(getTrxName()); + assertFalse(info.isError(), info.getSummary()); + assertEquals(DocAction.STATUS_Completed, purchaseInvoice.getDocStatus()); + + if (!purchaseInvoice.isPosted()) { + String error = DocumentEngine.postImmediate(Env.getCtx(), purchaseInvoice.getAD_Client_ID(), MInvoice.Table_ID, purchaseInvoice.get_ID(), false, getTrxName()); + assertTrue(error == null); + } + purchaseInvoice.load(getTrxName()); + assertTrue(purchaseInvoice.isPosted()); + + Doc invoiceDoc = DocManager.getDocument(as, MInvoice.Table_ID, purchaseInvoice.get_ID(), getTrxName()); + MAccount liabilityAccount = invoiceDoc.getAccount(Doc.ACCTTYPE_V_Liability, as); + MAccount inventoryClearingAccount = p1ProductCost.getAccount(ProductCost.ACCTTYPE_P_InventoryClearing, as); + query = MFactAcct.createRecordIdQuery(MInvoice.Table_ID, purchaseInvoice.get_ID(), as.get_ID(), getTrxName()); + factAccts = query.list(); + expected = Arrays.asList(new FactAcct(inventoryClearingAccount, p1price.multiply(orderQty), 2, true), + new FactAcct(inventoryClearingAccount, p2price.multiply(orderQty), 2, true), + new FactAcct(liabilityAccount, p1price.multiply(orderQty).add(p2price.multiply(orderQty)), 2, false)); + assertFactAcctEntries(factAccts, expected); + + //landed cost invoice + MBPartner freightBP = MBPartner.get(Env.getCtx(), DictionaryIDs.C_BPartner.PATIO.id); + MInvoice freightInvoice = new MInvoice(Env.getCtx(), 0, getTrxName()); + freightInvoice.setC_DocTypeTarget_ID(MDocType.DOCBASETYPE_APInvoice); + freightInvoice.setBPartner(freightBP); + freightInvoice.setDocStatus(DocAction.STATUS_Drafted); + freightInvoice.setDocAction(DocAction.ACTION_Complete); + freightInvoice.saveEx(); + + MInvoiceLine fiLine = new MInvoiceLine(freightInvoice); + fiLine.setLine(10); + fiLine.setC_Charge_ID(DictionaryIDs.C_Charge.FREIGHT.id); + fiLine.setQty(BigDecimal.ONE); + BigDecimal freightPrice = new BigDecimal("200.00"); + fiLine.setPrice(freightPrice); + fiLine.setC_UOM_ID(DictionaryIDs.C_UOM.EACH.id); + fiLine.saveEx(); + + MLandedCost landedCost = new MLandedCost(Env.getCtx(), 0, getTrxName()); + landedCost.setC_InvoiceLine_ID(fiLine.get_ID()); + landedCost.setM_CostElement_ID(DictionaryIDs.M_CostElement.FREIGHT.id); + landedCost.setM_InOut_ID(receipt1.get_ID()); + landedCost.setM_InOutLine_ID(receipt1Line1.get_ID()); + landedCost.setLandedCostDistribution(MLandedCost.LANDEDCOSTDISTRIBUTION_Costs); + landedCost.saveEx(); + + landedCost = new MLandedCost(Env.getCtx(), 0, getTrxName()); + landedCost.setC_InvoiceLine_ID(fiLine.get_ID()); + landedCost.setM_CostElement_ID(DictionaryIDs.M_CostElement.FREIGHT.id); + landedCost.setM_InOut_ID(receipt1.get_ID()); + landedCost.setM_InOutLine_ID(receipt1Line2.get_ID()); + landedCost.setLandedCostDistribution(MLandedCost.LANDEDCOSTDISTRIBUTION_Costs); + landedCost.saveEx(); + + String error = landedCost.allocateCosts(); + assertTrue(Util.isEmpty(error, true), error); + + BigDecimal totalBase = purchaseInvoice.getGrandTotal(); + BigDecimal p1a1 = p1price.multiply(mr1Qty).multiply(fiLine.getLineNetAmt()).divide(totalBase, 6, RoundingMode.HALF_UP); + BigDecimal p2a1 = p2price.multiply(mr1Qty).multiply(fiLine.getLineNetAmt()).divide(totalBase, 6, RoundingMode.HALF_UP); + + MLandedCostAllocation[] allocations = MLandedCostAllocation.getOfInvoiceLine(Env.getCtx(), fiLine.get_ID(), getTrxName()); + assertEquals(2, allocations.length, "Unexpected number of landed cost allocation line"); + for (MLandedCostAllocation allocation : allocations) { + if (allocation.getM_Product_ID() == p1.get_ID() && allocation.getQty().intValue() == 10) { + assertEquals(p1a1.setScale(2, RoundingMode.HALF_UP), allocation.getAmt().setScale(2, RoundingMode.HALF_UP), "Unexpected landed cost allocation amount"); + } else if (allocation.getM_Product_ID() == p2.get_ID() && allocation.getQty().intValue() == 10) { + assertEquals(p2a1.setScale(2, RoundingMode.HALF_UP), allocation.getAmt().setScale(2, RoundingMode.HALF_UP), "Unexpected landed cost allocation amount"); + } else { + fail("Unknown landed cost allocation line: " + allocation); + } + } + + info = MWorkflow.runDocumentActionWorkflow(freightInvoice, DocAction.ACTION_Complete); + freightInvoice.load(getTrxName()); + assertFalse(info.isError(), info.getSummary()); + assertEquals(DocAction.STATUS_Completed, freightInvoice.getDocStatus()); + + if (!freightInvoice.isPosted()) { + error = DocumentEngine.postImmediate(Env.getCtx(), freightInvoice.getAD_Client_ID(), MInvoice.Table_ID, freightInvoice.get_ID(), false, getTrxName()); + assertTrue(error == null, error); + } + freightInvoice.load(getTrxName()); + assertTrue(freightInvoice.isPosted()); + + //assert freight invoice posting + doc = DocManager.getDocument(as, MInvoice.Table_ID, freightInvoice.get_ID(), getTrxName()); + MAccount apAccount = doc.getAccount(Doc.ACCTTYPE_V_Liability, as); + query = MFactAcct.createRecordIdQuery(MInvoice.Table_ID, freightInvoice.get_ID(), as.get_ID(), getTrxName()); + factAccts = query.list(); + BigDecimal p1QtyOnHand = mr1Qty; + BigDecimal p2QtyOnHand = mr1Qty; + expected = Arrays.asList(new FactAcct(assetAccount, p1a1, 2, true), + new FactAcct(assetAccount, p2a1, 2, true), + new FactAcct(apAccount, freightInvoice.getGrandTotal(), 2, false)); + assertFactAcctEntries(factAccts, expected); + + p1mcost = p1.getCostingRecord(as, getAD_Org_ID(), 0, as.getCostingMethod()); + assertEquals(p1price.add(p1a1.divide(p1QtyOnHand, 2, RoundingMode.HALF_UP)) + .setScale(1, RoundingMode.HALF_UP), p1mcost.getCurrentCostPrice().setScale(1, RoundingMode.HALF_UP), "Unexpected current cost price"); + + p1mcost = p2.getCostingRecord(as, getAD_Org_ID(), 0, as.getCostingMethod()); + assertEquals(p2price.add(p2a1.divide(p2QtyOnHand, 2, RoundingMode.HALF_UP)) + .setScale(1, RoundingMode.HALF_UP), p1mcost.getCurrentCostPrice().setScale(1, RoundingMode.HALF_UP), "Unexpected current cost price"); + + //reverse freight invoice + info = MWorkflow.runDocumentActionWorkflow(freightInvoice, DocAction.ACTION_Reverse_Correct); + freightInvoice.load(getTrxName()); + assertFalse(info.isError(), info.getSummary()); + assertEquals(DocAction.STATUS_Reversed, freightInvoice.getDocStatus()); + assertTrue(freightInvoice.getReversal_ID() > 0, "Unexpected reversal id"); + MInvoice reversal = new MInvoice(Env.getCtx(), freightInvoice.getReversal_ID(), getTrxName()); + assertEquals(freightInvoice.getReversal_ID(), reversal.get_ID()); + query = MFactAcct.createRecordIdQuery(MInvoice.Table_ID, freightInvoice.get_ID(), as.get_ID(), getTrxName()); + factAccts = query.list(); + query = MFactAcct.createRecordIdQuery(MInvoice.Table_ID, reversal.get_ID(), as.get_ID(), getTrxName()); + List rFactAccts = query.list(); + expected = new ArrayList(); + for(MFactAcct factAcct : factAccts) { + MAccount acct = MAccount.get(factAcct, getTrxName()); + if (factAcct.getAmtAcctDr().signum() != 0) { + expected.add(new FactAcct(acct, factAcct.getAmtAcctDr(), 2, false)); + } else if (factAcct.getAmtAcctCr().signum() != 0) { + expected.add(new FactAcct(acct, factAcct.getAmtAcctCr(), 2, true)); + } + } + assertFactAcctEntries(rFactAccts, expected); + + MAcctSchema[] ass = MAcctSchema.getClientAcctSchema(Env.getCtx(), Env.getAD_Client_ID(Env.getCtx())); + Optional optional = Arrays.stream(ass).filter(e -> e.getC_AcctSchema_ID() != as.get_ID()).findFirst(); + if (optional.isPresent()) { + MAcctSchema as2 = optional.get(); + query = MFactAcct.createRecordIdQuery(MInvoice.Table_ID, freightInvoice.get_ID(), as2.get_ID(), getTrxName()); + factAccts = query.list(); + query = MFactAcct.createRecordIdQuery(MInvoice.Table_ID, reversal.get_ID(), as2.get_ID(), getTrxName()); + rFactAccts = query.list(); + expected = new ArrayList(); + for(MFactAcct factAcct : factAccts) { + MAccount acct = MAccount.get(factAcct, getTrxName()); + if (factAcct.getAmtAcctDr().signum() != 0) { + expected.add(new FactAcct(acct, factAcct.getAmtAcctDr(), 2, false)); + } else if (factAcct.getAmtAcctCr().signum() != 0) { + expected.add(new FactAcct(acct, factAcct.getAmtAcctCr(), 2, true)); + } + } + assertFactAcctEntries(rFactAccts, expected); + } + } finally { + rollback(); + + if (p1 != null) { + p1.set_TrxName(null); + p1.deleteEx(true); + } + + if (p2 != null) { + p2.set_TrxName(null); + p2.deleteEx(true); + } + } + } + + @Test + public void testUnplannedLandedCostReversalAfterShipment1() { + MClient client = MClient.get(Env.getCtx()); + MAcctSchema as = client.getAcctSchema(); + assertEquals(as.getCostingMethod(), MCostElement.COSTINGMETHOD_AveragePO, "Default costing method not Average PO"); + + MProduct p1 = null; + MProduct p2 = null; + try { + p1 = new MProduct(Env.getCtx(), 0, null); + p1.setM_Product_Category_ID(DictionaryIDs.M_Product_Category.STANDARD.id); + p1.setName("testUnplannedLandedCostReversalAfterShipment1.1"); + p1.setProductType(MProduct.PRODUCTTYPE_Item); + p1.setIsStocked(true); + p1.setIsSold(true); + p1.setIsPurchased(true); + p1.setC_UOM_ID(DictionaryIDs.C_UOM.EACH.id); + p1.setC_TaxCategory_ID(DictionaryIDs.C_TaxCategory.STANDARD.id); + p1.saveEx(); + + MPriceListVersion plv = MPriceList.get(DictionaryIDs.M_PriceList.PURCHASE.id).getPriceListVersion(null); + MProductPrice pp = new MProductPrice(Env.getCtx(), 0, getTrxName()); + pp.setM_PriceList_Version_ID(plv.getM_PriceList_Version_ID()); + pp.setM_Product_ID(p1.get_ID()); + BigDecimal p1price = new BigDecimal("30.00"); + pp.setPriceStd(p1price); + pp.setPriceList(p1price); + pp.saveEx(); + + p2 = new MProduct(Env.getCtx(), 0, null); + p2.setM_Product_Category_ID(DictionaryIDs.M_Product_Category.STANDARD.id); + p2.setName("testUnplannedLandedCostReversalAfterShipment1.2"); + p2.setProductType(MProduct.PRODUCTTYPE_Item); + p2.setIsStocked(true); + p2.setIsSold(true); + p2.setIsPurchased(true); + p2.setC_UOM_ID(DictionaryIDs.C_UOM.EACH.id); + p2.setC_TaxCategory_ID(DictionaryIDs.C_TaxCategory.STANDARD.id); + p2.saveEx(); + + pp = new MProductPrice(Env.getCtx(), 0, getTrxName()); + pp.setM_PriceList_Version_ID(plv.getM_PriceList_Version_ID()); + pp.setM_Product_ID(p2.get_ID()); + BigDecimal p2price = new BigDecimal("50.00"); + pp.setPriceStd(p2price); + pp.setPriceList(p2price); + pp.saveEx(); + + //create purchase order + MOrder purchaseOrder = new MOrder(Env.getCtx(), 0, getTrxName()); + purchaseOrder.setBPartner(MBPartner.get(Env.getCtx(), DictionaryIDs.C_BPartner.PATIO.id)); + purchaseOrder.setC_DocTypeTarget_ID(DictionaryIDs.C_DocType.PURCHASE_ORDER.id); + purchaseOrder.setIsSOTrx(false); + purchaseOrder.setSalesRep_ID(DictionaryIDs.AD_User.GARDEN_ADMIN.id); + purchaseOrder.setDocStatus(DocAction.STATUS_Drafted); + purchaseOrder.setDocAction(DocAction.ACTION_Complete); + Timestamp today = TimeUtil.getDay(System.currentTimeMillis()); + purchaseOrder.setDateOrdered(today); + purchaseOrder.setDatePromised(today); + purchaseOrder.saveEx(); + + MOrderLine poLine1 = new MOrderLine(purchaseOrder); + poLine1.setLine(10); + poLine1.setProduct(new MProduct(Env.getCtx(), p1.get_ID(), getTrxName())); + BigDecimal orderQty = new BigDecimal("10"); + poLine1.setQty(orderQty); + poLine1.setDatePromised(today); + poLine1.setPrice(p1price); + poLine1.saveEx(); + + MOrderLine poLine2 = new MOrderLine(purchaseOrder); + poLine2.setLine(10); + poLine2.setProduct(new MProduct(Env.getCtx(), p2.get_ID(), getTrxName())); + poLine2.setQty(orderQty); + poLine2.setDatePromised(today); + poLine2.setPrice(p2price); + poLine2.saveEx(); + + ProcessInfo info = MWorkflow.runDocumentActionWorkflow(purchaseOrder, DocAction.ACTION_Complete); + assertFalse(info.isError(), info.getSummary()); + purchaseOrder.load(getTrxName()); + assertEquals(DocAction.STATUS_Completed, purchaseOrder.getDocStatus()); + + //mr1 for 10 each + MInOut receipt1 = new MInOut(purchaseOrder, DictionaryIDs.C_DocType.MM_RECEIPT.id, purchaseOrder.getDateOrdered()); + receipt1.setDocStatus(DocAction.STATUS_Drafted); + receipt1.setDocAction(DocAction.ACTION_Complete); + receipt1.saveEx(); + + MInOutLine receipt1Line1 = new MInOutLine(receipt1); + BigDecimal mr1Qty = new BigDecimal("10"); + receipt1Line1.setOrderLine(poLine1, 0, mr1Qty); + receipt1Line1.setQty(mr1Qty); + receipt1Line1.saveEx(); + + MInOutLine receipt1Line2 = new MInOutLine(receipt1); + receipt1Line2.setOrderLine(poLine2, 0, mr1Qty); + receipt1Line2.setQty(mr1Qty); + receipt1Line2.saveEx(); + + info = MWorkflow.runDocumentActionWorkflow(receipt1, DocAction.ACTION_Complete); + assertFalse(info.isError(), info.getSummary()); + receipt1.load(getTrxName()); + assertEquals(DocAction.STATUS_Completed, receipt1.getDocStatus()); + if (!receipt1.isPosted()) { + String error = DocumentEngine.postImmediate(Env.getCtx(), receipt1.getAD_Client_ID(), receipt1.get_Table_ID(), receipt1.get_ID(), false, getTrxName()); + assertNull(error, error); + } + + //assert p1 cost and posting + List cds = MCostDetail.list(Env.getCtx(), "C_OrderLine_ID=?", poLine1.getC_OrderLine_ID(), 0, as.get_ID(), getTrxName()); + assertTrue(cds.size() == 1, "MCostDetail not found for order line1"); + for(MCostDetail cd : cds) { + if (cd.getM_CostElement_ID() == 0) { + assertEquals(10, cd.getQty().intValue(), "Unexpected MCostDetail Qty"); + assertEquals(p1price.multiply(mr1Qty).setScale(2, RoundingMode.HALF_UP), cd.getAmt().setScale(2, RoundingMode.HALF_UP), "Unexpected MCostDetail Amt"); + } + } + + p1.set_TrxName(getTrxName()); + MCost p1mcost = p1.getCostingRecord(as, getAD_Org_ID(), 0, as.getCostingMethod()); + assertNotNull(p1mcost, "No MCost record found"); + assertEquals(p1price, p1mcost.getCurrentCostPrice().setScale(2, RoundingMode.HALF_UP), "Unexpected current cost price"); + + ProductCost p1ProductCost = new ProductCost(Env.getCtx(), p1.get_ID(), 0, getTrxName()); + MAccount assetAccount = p1ProductCost.getAccount(ProductCost.ACCTTYPE_P_Asset, as); + Doc doc = DocManager.getDocument(as, MInOut.Table_ID, receipt1.get_ID(), getTrxName()); + MAccount nivReceiptAccount = doc.getAccount(Doc.ACCTTYPE_NotInvoicedReceipts, as); + Query query = MFactAcct.createRecordIdQuery(MInOut.Table_ID, receipt1.get_ID(), as.get_ID(), getTrxName()); + List factAccts = query.list(); + List expected = Arrays.asList(new FactAcct(assetAccount, p1price.multiply(mr1Qty), 2, true), + new FactAcct(nivReceiptAccount, p1price.multiply(mr1Qty), 2, false)); + assertFactAcctEntries(factAccts, expected); + + //assert p2 cost and posting + cds = MCostDetail.list(Env.getCtx(), "C_OrderLine_ID=?", poLine2.getC_OrderLine_ID(), 0, as.get_ID(), getTrxName()); + assertTrue(cds.size() == 1, "MCostDetail not found for order line2"); + for(MCostDetail cd : cds) { + if (cd.getM_CostElement_ID() == 0) { + assertEquals(10, cd.getQty().intValue(), "Unexpected MCostDetail Qty"); + assertEquals(p2price.multiply(mr1Qty).setScale(2, RoundingMode.HALF_UP), cd.getAmt().setScale(2, RoundingMode.HALF_UP), "Unexpected MCostDetail Amt"); + } + } + + p2.set_TrxName(getTrxName()); + MCost p2mcost = p2.getCostingRecord(as, getAD_Org_ID(), 0, as.getCostingMethod()); + assertNotNull(p2mcost, "No MCost record found"); + assertEquals(p2price, p2mcost.getCurrentCostPrice().setScale(2, RoundingMode.HALF_UP), "Unexpected current cost price"); + + ProductCost p2ProductCost = new ProductCost(Env.getCtx(), p2.get_ID(), 0, getTrxName()); + assetAccount = p2ProductCost.getAccount(ProductCost.ACCTTYPE_P_Asset, as); + expected = Arrays.asList(new FactAcct(assetAccount, p2price.multiply(mr1Qty), 2, true), + new FactAcct(nivReceiptAccount, p2price.multiply(mr1Qty), 2, false)); + assertFactAcctEntries(factAccts, expected); + + //full po invoice + MInvoice purchaseInvoice = new MInvoice(purchaseOrder, DictionaryIDs.C_DocType.AP_INVOICE.id, purchaseOrder.getDateOrdered()); + purchaseInvoice.setDocStatus(DocAction.STATUS_Drafted); + purchaseInvoice.setDocAction(DocAction.ACTION_Complete); + purchaseInvoice.saveEx(); + + MInvoiceLine piLine1 = new MInvoiceLine(purchaseInvoice); + piLine1.setOrderLine(poLine1); + piLine1.setLine(10); + piLine1.setProduct(p1); + piLine1.setQty(poLine1.getQtyOrdered()); + piLine1.saveEx(); + + MInvoiceLine piLine2 = new MInvoiceLine(purchaseInvoice); + piLine2.setOrderLine(poLine2); + piLine2.setLine(10); + piLine2.setProduct(p2); + piLine2.setQty(poLine2.getQtyOrdered()); + piLine2.saveEx(); + + info = MWorkflow.runDocumentActionWorkflow(purchaseInvoice, DocAction.ACTION_Complete); + purchaseInvoice.load(getTrxName()); + assertFalse(info.isError(), info.getSummary()); + assertEquals(DocAction.STATUS_Completed, purchaseInvoice.getDocStatus()); + + if (!purchaseInvoice.isPosted()) { + String error = DocumentEngine.postImmediate(Env.getCtx(), purchaseInvoice.getAD_Client_ID(), MInvoice.Table_ID, purchaseInvoice.get_ID(), false, getTrxName()); + assertTrue(error == null); + } + purchaseInvoice.load(getTrxName()); + assertTrue(purchaseInvoice.isPosted()); + + Doc invoiceDoc = DocManager.getDocument(as, MInvoice.Table_ID, purchaseInvoice.get_ID(), getTrxName()); + MAccount liabilityAccount = invoiceDoc.getAccount(Doc.ACCTTYPE_V_Liability, as); + MAccount inventoryClearingAccount = p1ProductCost.getAccount(ProductCost.ACCTTYPE_P_InventoryClearing, as); + query = MFactAcct.createRecordIdQuery(MInvoice.Table_ID, purchaseInvoice.get_ID(), as.get_ID(), getTrxName()); + factAccts = query.list(); + expected = Arrays.asList(new FactAcct(inventoryClearingAccount, p1price.multiply(orderQty), 2, true), + new FactAcct(inventoryClearingAccount, p2price.multiply(orderQty), 2, true), + new FactAcct(liabilityAccount, p1price.multiply(orderQty).add(p2price.multiply(orderQty)), 2, false)); + assertFactAcctEntries(factAccts, expected); + + //landed cost invoice + MBPartner freightBP = MBPartner.get(Env.getCtx(), DictionaryIDs.C_BPartner.PATIO.id); + MInvoice freightInvoice = new MInvoice(Env.getCtx(), 0, getTrxName()); + freightInvoice.setC_DocTypeTarget_ID(MDocType.DOCBASETYPE_APInvoice); + freightInvoice.setBPartner(freightBP); + freightInvoice.setDocStatus(DocAction.STATUS_Drafted); + freightInvoice.setDocAction(DocAction.ACTION_Complete); + freightInvoice.saveEx(); + + MInvoiceLine fiLine = new MInvoiceLine(freightInvoice); + fiLine.setLine(10); + fiLine.setC_Charge_ID(DictionaryIDs.C_Charge.FREIGHT.id); + fiLine.setQty(BigDecimal.ONE); + BigDecimal freightPrice = new BigDecimal("200.00"); + fiLine.setPrice(freightPrice); + fiLine.setC_UOM_ID(DictionaryIDs.C_UOM.EACH.id); + fiLine.saveEx(); + + MLandedCost landedCost = new MLandedCost(Env.getCtx(), 0, getTrxName()); + landedCost.setC_InvoiceLine_ID(fiLine.get_ID()); + landedCost.setM_CostElement_ID(DictionaryIDs.M_CostElement.FREIGHT.id); + landedCost.setM_InOut_ID(receipt1.get_ID()); + landedCost.setM_InOutLine_ID(receipt1Line1.get_ID()); + landedCost.setLandedCostDistribution(MLandedCost.LANDEDCOSTDISTRIBUTION_Costs); + landedCost.saveEx(); + + landedCost = new MLandedCost(Env.getCtx(), 0, getTrxName()); + landedCost.setC_InvoiceLine_ID(fiLine.get_ID()); + landedCost.setM_CostElement_ID(DictionaryIDs.M_CostElement.FREIGHT.id); + landedCost.setM_InOut_ID(receipt1.get_ID()); + landedCost.setM_InOutLine_ID(receipt1Line2.get_ID()); + landedCost.setLandedCostDistribution(MLandedCost.LANDEDCOSTDISTRIBUTION_Costs); + landedCost.saveEx(); + + String error = landedCost.allocateCosts(); + assertTrue(Util.isEmpty(error, true), error); + + BigDecimal totalBase = purchaseInvoice.getGrandTotal(); + BigDecimal p1a1 = p1price.multiply(mr1Qty).multiply(fiLine.getLineNetAmt()).divide(totalBase, 6, RoundingMode.HALF_UP); + BigDecimal p2a1 = p2price.multiply(mr1Qty).multiply(fiLine.getLineNetAmt()).divide(totalBase, 6, RoundingMode.HALF_UP); + + MLandedCostAllocation[] allocations = MLandedCostAllocation.getOfInvoiceLine(Env.getCtx(), fiLine.get_ID(), getTrxName()); + assertEquals(2, allocations.length, "Unexpected number of landed cost allocation line"); + for (MLandedCostAllocation allocation : allocations) { + if (allocation.getM_Product_ID() == p1.get_ID() && allocation.getQty().intValue() == 10) { + assertEquals(p1a1.setScale(2, RoundingMode.HALF_UP), allocation.getAmt().setScale(2, RoundingMode.HALF_UP), "Unexpected landed cost allocation amount"); + } else if (allocation.getM_Product_ID() == p2.get_ID() && allocation.getQty().intValue() == 10) { + assertEquals(p2a1.setScale(2, RoundingMode.HALF_UP), allocation.getAmt().setScale(2, RoundingMode.HALF_UP), "Unexpected landed cost allocation amount"); + } else { + fail("Unknown landed cost allocation line: " + allocation); + } + } + + info = MWorkflow.runDocumentActionWorkflow(freightInvoice, DocAction.ACTION_Complete); + freightInvoice.load(getTrxName()); + assertFalse(info.isError(), info.getSummary()); + assertEquals(DocAction.STATUS_Completed, freightInvoice.getDocStatus()); + + if (!freightInvoice.isPosted()) { + error = DocumentEngine.postImmediate(Env.getCtx(), freightInvoice.getAD_Client_ID(), MInvoice.Table_ID, freightInvoice.get_ID(), false, getTrxName()); + assertTrue(error == null, error); + } + freightInvoice.load(getTrxName()); + assertTrue(freightInvoice.isPosted()); + + //assert freight invoice posting + doc = DocManager.getDocument(as, MInvoice.Table_ID, freightInvoice.get_ID(), getTrxName()); + MAccount apAccount = doc.getAccount(Doc.ACCTTYPE_V_Liability, as); + query = MFactAcct.createRecordIdQuery(MInvoice.Table_ID, freightInvoice.get_ID(), as.get_ID(), getTrxName()); + factAccts = query.list(); + BigDecimal p1QtyOnHand = mr1Qty; + BigDecimal p2QtyOnHand = mr1Qty; + expected = Arrays.asList(new FactAcct(assetAccount, p1a1, 2, true), + new FactAcct(assetAccount, p2a1, 2, true), + new FactAcct(apAccount, freightInvoice.getGrandTotal(), 2, false)); + assertFactAcctEntries(factAccts, expected); + + p1mcost = p1.getCostingRecord(as, getAD_Org_ID(), 0, as.getCostingMethod()); + assertEquals(p1price.add(p1a1.divide(p1QtyOnHand, 2, RoundingMode.HALF_UP)) + .setScale(1, RoundingMode.HALF_UP), p1mcost.getCurrentCostPrice().setScale(1, RoundingMode.HALF_UP), "Unexpected current cost price"); + + p1mcost = p2.getCostingRecord(as, getAD_Org_ID(), 0, as.getCostingMethod()); + assertEquals(p2price.add(p2a1.divide(p2QtyOnHand, 2, RoundingMode.HALF_UP)) + .setScale(1, RoundingMode.HALF_UP), p1mcost.getCurrentCostPrice().setScale(1, RoundingMode.HALF_UP), "Unexpected current cost price"); + + //so and shipment + MBPartner customer = MBPartner.get(Env.getCtx(), DictionaryIDs.C_BPartner.JOE_BLOCK.id); + MOrder salesOrder = new MOrder(Env.getCtx(), 0, getTrxName()); + salesOrder.setC_DocTypeTarget_ID(MOrder.DocSubTypeSO_Standard); + salesOrder.setBPartner(customer); + salesOrder.setDeliveryRule(MOrder.DELIVERYRULE_CompleteOrder); + salesOrder.setDocStatus(DocAction.STATUS_Drafted); + salesOrder.setDocAction(DocAction.ACTION_Complete); + salesOrder.setDatePromised(today); + salesOrder.saveEx(); + + MOrderLine soLine1 = new MOrderLine(salesOrder); + soLine1.setLine(10); + soLine1.setProduct(p1); + BigDecimal p1ShipQty = new BigDecimal("10"); + soLine1.setQty(p1ShipQty); + soLine1.setDatePromised(today); + soLine1.setPrice(new BigDecimal("50")); + soLine1.saveEx(); + + MOrderLine soLine2 = new MOrderLine(salesOrder); + soLine2.setLine(20); + soLine2.setProduct(p2); + BigDecimal p2ShipQty = new BigDecimal("5"); + soLine2.setQty(p2ShipQty); + soLine2.setDatePromised(today); + soLine2.setPrice(new BigDecimal("70")); + soLine2.saveEx(); + + info = MWorkflow.runDocumentActionWorkflow(salesOrder, DocAction.ACTION_Complete); + salesOrder.load(getTrxName()); + assertFalse(info.isError(), info.getSummary()); + assertEquals(DocAction.STATUS_Completed, salesOrder.getDocStatus()); + + MInOut shipment = new MInOut(salesOrder, DictionaryIDs.C_DocType.MM_SHIPMENT.id, salesOrder.getDateOrdered()); + shipment.setDocStatus(DocAction.STATUS_Drafted); + shipment.setDocAction(DocAction.ACTION_Complete); + shipment.saveEx(); + + MInOutLine shipmentLine1 = new MInOutLine(shipment); + shipmentLine1.setOrderLine(soLine1, 0, soLine1.getQtyOrdered()); + shipmentLine1.setQty(soLine1.getQtyOrdered()); + shipmentLine1.saveEx(); + + MInOutLine shipmentLine2 = new MInOutLine(shipment); + shipmentLine2.setOrderLine(soLine2, 0, soLine2.getQtyOrdered()); + shipmentLine2.setQty(soLine2.getQtyOrdered()); + shipmentLine2.saveEx(); + + info = MWorkflow.runDocumentActionWorkflow(shipment, DocAction.ACTION_Complete); + assertFalse(info.isError(), info.getSummary()); + shipment.load(getTrxName()); + assertEquals(DocAction.STATUS_Completed, shipment.getDocStatus()); + + //reverse freight invoice + info = MWorkflow.runDocumentActionWorkflow(freightInvoice, DocAction.ACTION_Reverse_Correct); + freightInvoice.load(getTrxName()); + assertFalse(info.isError(), info.getSummary()); + assertEquals(DocAction.STATUS_Reversed, freightInvoice.getDocStatus()); + assertTrue(freightInvoice.getReversal_ID() > 0, "Unexpected reversal id"); + MInvoice reversal = new MInvoice(Env.getCtx(), freightInvoice.getReversal_ID(), getTrxName()); + assertEquals(freightInvoice.getReversal_ID(), reversal.get_ID()); + if (!reversal.isPosted()) { + String msg = DocumentEngine.postImmediate(Env.getCtx(), getAD_Client_ID(), MInvoice.Table_ID, reversal.get_ID(), false, getTrxName()); + assertNull(msg, msg); + } + + query = MFactAcct.createRecordIdQuery(MInvoice.Table_ID, reversal.get_ID(), as.get_ID(), getTrxName()); + List rFactAccts = query.list(); + MAccount varianceAccount = p1ProductCost.getAccount(ProductCost.ACCTTYPE_P_AverageCostVariance, as); + expected = Arrays.asList(new FactAcct(varianceAccount, p1a1, 2, false), + new FactAcct(assetAccount, p2a1.divide(new BigDecimal(2), RoundingMode.HALF_UP), 2, false), + new FactAcct(apAccount, freightInvoice.getGrandTotal(), 2, true)); + assertFactAcctEntries(rFactAccts, expected); + + } finally { + rollback(); + + if (p1 != null) { + p1.set_TrxName(null); + p1.deleteEx(true); + } + + if (p2 != null) { + p2.set_TrxName(null); + p2.deleteEx(true); + } + } + } + + @Test + public void testUnplannedLandedCostReversalAfterShipment3() { + MClient client = MClient.get(Env.getCtx()); + MAcctSchema as = client.getAcctSchema(); + assertEquals(as.getCostingMethod(), MCostElement.COSTINGMETHOD_AveragePO, "Default costing method not Average PO"); + + MProduct p1 = null; + MCurrency usd = MCurrency.get(DictionaryIDs.C_Currency.USD.id); + MCurrency euro = MCurrency.get(DictionaryIDs.C_Currency.EUR.id); + int C_ConversionType_ID = DictionaryIDs.C_ConversionType.SPOT.id; + Timestamp today = TimeUtil.getDay(null); + Timestamp tomorrow = TimeUtil.addDays(today, 1); + BigDecimal crate1 = new BigDecimal("1.05"); + BigDecimal crate2 = new BigDecimal("1.12"); + MConversionRate cr1 = ConversionRateHelper.createConversionRate(euro.getC_Currency_ID(), usd.getC_Currency_ID(), C_ConversionType_ID, today, crate1, true); + MConversionRate cr2 = ConversionRateHelper.createConversionRate(euro.getC_Currency_ID(), usd.getC_Currency_ID(), C_ConversionType_ID, tomorrow, crate2, true); + try { + p1 = new MProduct(Env.getCtx(), 0, null); + p1.setM_Product_Category_ID(DictionaryIDs.M_Product_Category.STANDARD.id); + p1.setName("testUnplannedLandedCostReversalAfterShipment3"); + p1.setProductType(MProduct.PRODUCTTYPE_Item); + p1.setIsStocked(true); + p1.setIsSold(true); + p1.setIsPurchased(true); + p1.setC_UOM_ID(DictionaryIDs.C_UOM.EACH.id); + p1.setC_TaxCategory_ID(DictionaryIDs.C_TaxCategory.STANDARD.id); + p1.saveEx(); + + MPriceListVersion plv = MPriceList.get(DictionaryIDs.M_PriceList.IMPORT.id).getPriceListVersion(null); + MProductPrice pp = new MProductPrice(Env.getCtx(), 0, getTrxName()); + pp.setM_PriceList_Version_ID(plv.getM_PriceList_Version_ID()); + pp.setM_Product_ID(p1.get_ID()); + BigDecimal p1price = new BigDecimal("30.00"); + pp.setPriceStd(p1price); + pp.setPriceList(p1price); + pp.saveEx(); + + //create purchase order + MOrder purchaseOrder = new MOrder(Env.getCtx(), 0, getTrxName()); + purchaseOrder.setBPartner(MBPartner.get(Env.getCtx(), DictionaryIDs.C_BPartner.PATIO.id)); + purchaseOrder.setC_DocTypeTarget_ID(DictionaryIDs.C_DocType.PURCHASE_ORDER.id); + purchaseOrder.setIsSOTrx(false); + purchaseOrder.setSalesRep_ID(DictionaryIDs.AD_User.GARDEN_ADMIN.id); + purchaseOrder.setDocStatus(DocAction.STATUS_Drafted); + purchaseOrder.setDocAction(DocAction.ACTION_Complete); + purchaseOrder.setDateOrdered(today); + purchaseOrder.setDatePromised(today); + purchaseOrder.setM_PriceList_ID(plv.getM_PriceList_ID()); + purchaseOrder.saveEx(); + + MOrderLine poLine1 = new MOrderLine(purchaseOrder); + poLine1.setLine(10); + poLine1.setProduct(new MProduct(Env.getCtx(), p1.get_ID(), getTrxName())); + BigDecimal orderQty = new BigDecimal("100"); + poLine1.setQty(orderQty); + poLine1.setDatePromised(today); + poLine1.setPrice(p1price); + poLine1.saveEx(); + + ProcessInfo info = MWorkflow.runDocumentActionWorkflow(purchaseOrder, DocAction.ACTION_Complete); + assertFalse(info.isError(), info.getSummary()); + purchaseOrder.load(getTrxName()); + assertEquals(DocAction.STATUS_Completed, purchaseOrder.getDocStatus()); + + //material receipt + MInOut receipt1 = new MInOut(purchaseOrder, DictionaryIDs.C_DocType.MM_RECEIPT.id, purchaseOrder.getDateOrdered()); + receipt1.setDocStatus(DocAction.STATUS_Drafted); + receipt1.setDocAction(DocAction.ACTION_Complete); + receipt1.saveEx(); + + MInOutLine receipt1Line1 = new MInOutLine(receipt1); + BigDecimal mr1Qty = new BigDecimal("100"); + receipt1Line1.setOrderLine(poLine1, 0, mr1Qty); + receipt1Line1.setQty(mr1Qty); + receipt1Line1.saveEx(); + + info = MWorkflow.runDocumentActionWorkflow(receipt1, DocAction.ACTION_Complete); + assertFalse(info.isError(), info.getSummary()); + receipt1.load(getTrxName()); + assertEquals(DocAction.STATUS_Completed, receipt1.getDocStatus()); + if (!receipt1.isPosted()) { + String error = DocumentEngine.postImmediate(Env.getCtx(), receipt1.getAD_Client_ID(), receipt1.get_Table_ID(), receipt1.get_ID(), false, getTrxName()); + assertNull(error, error); + } + + //assert p1 cost and posting + List cds = MCostDetail.list(Env.getCtx(), "C_OrderLine_ID=?", poLine1.getC_OrderLine_ID(), 0, as.get_ID(), getTrxName()); + assertTrue(cds.size() == 1, "MCostDetail not found for order line1"); + for(MCostDetail cd : cds) { + if (cd.getM_CostElement_ID() == 0) { + assertEquals(mr1Qty.intValue(), cd.getQty().intValue(), "Unexpected MCostDetail Qty"); + assertEquals(p1price.multiply(mr1Qty).multiply(crate1).setScale(2, RoundingMode.HALF_UP), cd.getAmt().setScale(2, RoundingMode.HALF_UP), "Unexpected MCostDetail Amt"); + } + } + + p1.set_TrxName(getTrxName()); + MCost p1mcost = p1.getCostingRecord(as, getAD_Org_ID(), 0, as.getCostingMethod()); + assertNotNull(p1mcost, "No MCost record found"); + assertEquals(p1price.multiply(crate1).setScale(2, RoundingMode.HALF_UP), p1mcost.getCurrentCostPrice().setScale(2, RoundingMode.HALF_UP), "Unexpected current cost price"); + + ProductCost p1ProductCost = new ProductCost(Env.getCtx(), p1.get_ID(), 0, getTrxName()); + MAccount assetAccount = p1ProductCost.getAccount(ProductCost.ACCTTYPE_P_Asset, as); + MAccount varianceAccount = p1ProductCost.getAccount(ProductCost.ACCTTYPE_P_AverageCostVariance, as); + Doc doc = DocManager.getDocument(as, MInOut.Table_ID, receipt1.get_ID(), getTrxName()); + MAccount nivReceiptAccount = doc.getAccount(Doc.ACCTTYPE_NotInvoicedReceipts, as); + Query query = MFactAcct.createRecordIdQuery(MInOut.Table_ID, receipt1.get_ID(), as.get_ID(), getTrxName()); + List factAccts = query.list(); + List expected = Arrays.asList(new FactAcct(assetAccount, p1price.multiply(mr1Qty).multiply(crate1).setScale(2, RoundingMode.HALF_UP), p1price.multiply(mr1Qty), 2, true), + new FactAcct(nivReceiptAccount, p1price.multiply(mr1Qty).multiply(crate1).setScale(2, RoundingMode.HALF_UP), p1price.multiply(mr1Qty), 2, false)); + assertFactAcctEntries(factAccts, expected); + + //PO invoice + MInvoice purchaseInvoice = new MInvoice(purchaseOrder, DictionaryIDs.C_DocType.AP_INVOICE.id, purchaseOrder.getDateOrdered()); + purchaseInvoice.setDocStatus(DocAction.STATUS_Drafted); + purchaseInvoice.setDocAction(DocAction.ACTION_Complete); + purchaseInvoice.saveEx(); + + MInvoiceLine piLine1 = new MInvoiceLine(purchaseInvoice); + piLine1.setOrderLine(poLine1); + piLine1.setLine(10); + piLine1.setProduct(p1); + piLine1.setQty(poLine1.getQtyOrdered()); + piLine1.saveEx(); + + info = MWorkflow.runDocumentActionWorkflow(purchaseInvoice, DocAction.ACTION_Complete); + purchaseInvoice.load(getTrxName()); + assertFalse(info.isError(), info.getSummary()); + assertEquals(DocAction.STATUS_Completed, purchaseInvoice.getDocStatus()); + + if (!purchaseInvoice.isPosted()) { + String error = DocumentEngine.postImmediate(Env.getCtx(), purchaseInvoice.getAD_Client_ID(), MInvoice.Table_ID, purchaseInvoice.get_ID(), false, getTrxName()); + assertTrue(error == null); + } + purchaseInvoice.load(getTrxName()); + assertTrue(purchaseInvoice.isPosted()); + + Doc invoiceDoc = DocManager.getDocument(as, MInvoice.Table_ID, purchaseInvoice.get_ID(), getTrxName()); + MAccount liabilityAccount = invoiceDoc.getAccount(Doc.ACCTTYPE_V_Liability, as); + MAccount inventoryClearingAccount = p1ProductCost.getAccount(ProductCost.ACCTTYPE_P_InventoryClearing, as); + query = MFactAcct.createRecordIdQuery(MInvoice.Table_ID, purchaseInvoice.get_ID(), as.get_ID(), getTrxName()); + factAccts = query.list(); + expected = Arrays.asList(new FactAcct(inventoryClearingAccount, p1price.multiply(orderQty).multiply(crate1), p1price.multiply(orderQty), 2, true), + new FactAcct(liabilityAccount, p1price.multiply(orderQty).multiply(crate1), p1price.multiply(orderQty), 2, false)); + assertFactAcctEntries(factAccts, expected); + + //so and shipment + plv = MPriceList.get(DictionaryIDs.M_PriceList.EXPORT.id).getPriceListVersion(null); + pp = new MProductPrice(Env.getCtx(), 0, getTrxName()); + pp.setM_PriceList_Version_ID(plv.getM_PriceList_Version_ID()); + pp.setM_Product_ID(p1.get_ID()); + BigDecimal orderPrice = new BigDecimal("100.00"); + pp.setPriceStd(orderPrice); + pp.setPriceList(orderPrice); + pp.saveEx(); + + MBPartner customer = MBPartner.get(Env.getCtx(), DictionaryIDs.C_BPartner.JOE_BLOCK.id); + MOrder salesOrder = new MOrder(Env.getCtx(), 0, getTrxName()); + salesOrder.setC_DocTypeTarget_ID(MOrder.DocSubTypeSO_Standard); + salesOrder.setBPartner(customer); + salesOrder.setDeliveryRule(MOrder.DELIVERYRULE_CompleteOrder); + salesOrder.setDocStatus(DocAction.STATUS_Drafted); + salesOrder.setDocAction(DocAction.ACTION_Complete); + salesOrder.setDatePromised(today); + salesOrder.setM_PriceList_ID(DictionaryIDs.M_PriceList.EXPORT.id); + salesOrder.saveEx(); + + MOrderLine soLine1 = new MOrderLine(salesOrder); + soLine1.setLine(10); + soLine1.setProduct(p1); + BigDecimal p1ShipQty = new BigDecimal("80"); + soLine1.setQty(p1ShipQty); + soLine1.setDatePromised(today); + soLine1.setPrice(orderPrice); + soLine1.saveEx(); + + info = MWorkflow.runDocumentActionWorkflow(salesOrder, DocAction.ACTION_Complete); + salesOrder.load(getTrxName()); + assertFalse(info.isError(), info.getSummary()); + assertEquals(DocAction.STATUS_Completed, salesOrder.getDocStatus()); + + MInOut shipment = new MInOut(salesOrder, DictionaryIDs.C_DocType.MM_SHIPMENT.id, salesOrder.getDateOrdered()); + shipment.setDocStatus(DocAction.STATUS_Drafted); + shipment.setDocAction(DocAction.ACTION_Complete); + shipment.saveEx(); + + MInOutLine shipmentLine1 = new MInOutLine(shipment); + shipmentLine1.setOrderLine(soLine1, 0, soLine1.getQtyOrdered()); + shipmentLine1.setQty(soLine1.getQtyOrdered()); + shipmentLine1.saveEx(); + + info = MWorkflow.runDocumentActionWorkflow(shipment, DocAction.ACTION_Complete); + assertFalse(info.isError(), info.getSummary()); + shipment.load(getTrxName()); + assertEquals(DocAction.STATUS_Completed, shipment.getDocStatus()); + + //landed cost invoice + MBPartner freightBP = MBPartner.get(Env.getCtx(), DictionaryIDs.C_BPartner.PATIO.id); + MInvoice freightInvoice = new MInvoice(Env.getCtx(), 0, getTrxName()); + freightInvoice.setC_DocTypeTarget_ID(MDocType.DOCBASETYPE_APInvoice); + freightInvoice.setBPartner(freightBP); + freightInvoice.setDocStatus(DocAction.STATUS_Drafted); + freightInvoice.setDocAction(DocAction.ACTION_Complete); + freightInvoice.setM_PriceList_ID(DictionaryIDs.M_PriceList.EXPORT.id); + freightInvoice.saveEx(); + + MInvoiceLine fiLine = new MInvoiceLine(freightInvoice); + fiLine.setLine(10); + fiLine.setC_Charge_ID(DictionaryIDs.C_Charge.FREIGHT.id); + fiLine.setQty(BigDecimal.ONE); + BigDecimal freightPrice = new BigDecimal("200.00"); + fiLine.setPrice(freightPrice); + fiLine.setC_UOM_ID(DictionaryIDs.C_UOM.EACH.id); + fiLine.saveEx(); + + MLandedCost landedCost = new MLandedCost(Env.getCtx(), 0, getTrxName()); + landedCost.setC_InvoiceLine_ID(fiLine.get_ID()); + landedCost.setM_CostElement_ID(DictionaryIDs.M_CostElement.FREIGHT.id); + landedCost.setM_InOut_ID(receipt1.get_ID()); + landedCost.setM_InOutLine_ID(receipt1Line1.get_ID()); + landedCost.setLandedCostDistribution(MLandedCost.LANDEDCOSTDISTRIBUTION_Costs); + landedCost.saveEx(); + + String error = landedCost.allocateCosts(); + assertTrue(Util.isEmpty(error, true), error); + + BigDecimal p1a1 = fiLine.getLineTotalAmt(); + + MLandedCostAllocation[] allocations = MLandedCostAllocation.getOfInvoiceLine(Env.getCtx(), fiLine.get_ID(), getTrxName()); + assertEquals(1, allocations.length, "Unexpected number of landed cost allocation line"); + for (MLandedCostAllocation allocation : allocations) { + if (allocation.getM_Product_ID() == p1.get_ID() && allocation.getQty().intValue() == orderQty.intValue()) { + assertEquals(p1a1.setScale(2, RoundingMode.HALF_UP), allocation.getAmt().setScale(2, RoundingMode.HALF_UP), "Unexpected landed cost allocation amount"); + } else { + fail("Unknown landed cost allocation line: " + allocation); + } + } + + info = MWorkflow.runDocumentActionWorkflow(freightInvoice, DocAction.ACTION_Complete); + freightInvoice.load(getTrxName()); + assertFalse(info.isError(), info.getSummary()); + assertEquals(DocAction.STATUS_Completed, freightInvoice.getDocStatus()); + + if (!freightInvoice.isPosted()) { + error = DocumentEngine.postImmediate(Env.getCtx(), freightInvoice.getAD_Client_ID(), MInvoice.Table_ID, freightInvoice.get_ID(), false, getTrxName()); + assertTrue(error == null, error); + } + freightInvoice.load(getTrxName()); + assertTrue(freightInvoice.isPosted()); + + //assert freight invoice posting + doc = DocManager.getDocument(as, MInvoice.Table_ID, freightInvoice.get_ID(), getTrxName()); + MAccount apAccount = doc.getAccount(Doc.ACCTTYPE_V_Liability, as); + query = MFactAcct.createRecordIdQuery(MInvoice.Table_ID, freightInvoice.get_ID(), as.get_ID(), getTrxName()); + factAccts = query.list(); + BigDecimal assetAmt = p1a1.divide(orderQty, RoundingMode.HALF_UP).multiply(orderQty.subtract(p1ShipQty)); + BigDecimal varianceAmt = p1a1.subtract(assetAmt); + expected = Arrays.asList(new FactAcct(varianceAccount, varianceAmt.multiply(crate1), varianceAmt, 2, true), + new FactAcct(assetAccount, assetAmt.multiply(crate1), assetAmt, 2, true), + new FactAcct(apAccount, freightInvoice.getGrandTotal().multiply(crate1), freightInvoice.getGrandTotal(), 2, false)); + assertFactAcctEntries(factAccts, expected); + + p1mcost = p1.getCostingRecord(as, getAD_Org_ID(), 0, as.getCostingMethod()); + assertEquals(p1price.multiply(crate1).add(assetAmt.multiply(crate1).divide(orderQty.subtract(p1ShipQty), RoundingMode.HALF_UP)).setScale(2, RoundingMode.HALF_UP), + p1mcost.getCurrentCostPrice().setScale(2, RoundingMode.HALF_UP), "Unexpected current cost price"); + + //reverse freight invoice + Env.setContext(Env.getCtx(), Env.DATE, tomorrow); + info = MWorkflow.runDocumentActionWorkflow(freightInvoice, DocAction.ACTION_Reverse_Correct); + freightInvoice.load(getTrxName()); + assertFalse(info.isError(), info.getSummary()); + assertEquals(DocAction.STATUS_Reversed, freightInvoice.getDocStatus()); + assertTrue(freightInvoice.getReversal_ID() > 0, "Unexpected reversal id"); + MInvoice reversal = new MInvoice(Env.getCtx(), freightInvoice.getReversal_ID(), getTrxName()); + assertEquals(freightInvoice.getReversal_ID(), reversal.get_ID()); + if (!reversal.isPosted()) { + String msg = DocumentEngine.postImmediate(Env.getCtx(), getAD_Client_ID(), MInvoice.Table_ID, reversal.get_ID(), false, getTrxName()); + assertNull(msg, msg); + } + + query = MFactAcct.createRecordIdQuery(MInvoice.Table_ID, freightInvoice.get_ID(), as.get_ID(), getTrxName()); + factAccts = query.list(); + query = MFactAcct.createRecordIdQuery(MInvoice.Table_ID, reversal.get_ID(), as.get_ID(), getTrxName()); + List rFactAccts = query.list(); + expected = new ArrayList(); + for(MFactAcct factAcct : factAccts) { + MAccount acct = MAccount.get(factAcct, getTrxName()); + if (factAcct.getAmtAcctDr().signum() != 0) { + expected.add(new FactAcct(acct, factAcct.getAmtAcctDr(), 1, false)); + } else if (factAcct.getAmtAcctCr().signum() != 0) { + expected.add(new FactAcct(acct, factAcct.getAmtAcctCr(), 1, true)); + } + } + assertFactAcctEntries(rFactAccts, expected); + + MAcctSchema[] ass = MAcctSchema.getClientAcctSchema(Env.getCtx(), Env.getAD_Client_ID(Env.getCtx())); + Optional optional = Arrays.stream(ass).filter(e -> e.getC_AcctSchema_ID() != as.get_ID()).findFirst(); + if (optional.isPresent()) { + MAcctSchema as2 = optional.get(); + query = MFactAcct.createRecordIdQuery(MInvoice.Table_ID, freightInvoice.get_ID(), as2.get_ID(), getTrxName()); + factAccts = query.list(); + query = MFactAcct.createRecordIdQuery(MInvoice.Table_ID, freightInvoice.getReversal_ID(), as2.get_ID(), getTrxName()); + rFactAccts = query.list(); + expected = new ArrayList(); + for(MFactAcct factAcct : factAccts) { + MAccount acct = MAccount.get(factAcct, getTrxName()); + if (factAcct.getAmtAcctDr().signum() != 0) { + expected.add(new FactAcct(acct, factAcct.getAmtAcctDr(), 1, false)); + } else if (factAcct.getAmtAcctCr().signum() != 0) { + expected.add(new FactAcct(acct, factAcct.getAmtAcctCr(), 1, true)); + } + } + assertFactAcctEntries(rFactAccts, expected); + } + } finally { + rollback(); + + if (p1 != null) { + p1.set_TrxName(null); + p1.deleteEx(true); + } + + ConversionRateHelper.deleteConversionRate(cr1); + ConversionRateHelper.deleteConversionRate(cr2); + } + } + + @Test + public void testUnplannedLandedCostReversalAfterShipment2() { + MClient client = MClient.get(Env.getCtx()); + MAcctSchema as = client.getAcctSchema(); + assertEquals(as.getCostingMethod(), MCostElement.COSTINGMETHOD_AveragePO, "Default costing method not Average PO"); + + MProduct p1 = null; + MProduct p2 = null; + MCurrency usd = MCurrency.get(DictionaryIDs.C_Currency.USD.id); + MCurrency euro = MCurrency.get(DictionaryIDs.C_Currency.EUR.id); + int C_ConversionType_ID = DictionaryIDs.C_ConversionType.SPOT.id; + Timestamp today = TimeUtil.getDay(null); + Timestamp tomorrow = TimeUtil.addDays(today, 1); + MConversionRate cr1 = ConversionRateHelper.createConversionRate(usd.getC_Currency_ID(), euro.getC_Currency_ID(), C_ConversionType_ID, today, new BigDecimal("0.91"), true); + MConversionRate cr2 = ConversionRateHelper.createConversionRate(usd.getC_Currency_ID(), euro.getC_Currency_ID(), C_ConversionType_ID, tomorrow, new BigDecimal("0.85"), true); + try { + p1 = new MProduct(Env.getCtx(), 0, null); + p1.setM_Product_Category_ID(DictionaryIDs.M_Product_Category.STANDARD.id); + p1.setName("testUnplannedLandedCostReversalAfterShipment2.1"); + p1.setProductType(MProduct.PRODUCTTYPE_Item); + p1.setIsStocked(true); + p1.setIsSold(true); + p1.setIsPurchased(true); + p1.setC_UOM_ID(DictionaryIDs.C_UOM.EACH.id); + p1.setC_TaxCategory_ID(DictionaryIDs.C_TaxCategory.STANDARD.id); + p1.saveEx(); + + MPriceListVersion plv = MPriceList.get(DictionaryIDs.M_PriceList.PURCHASE.id).getPriceListVersion(null); + MProductPrice pp = new MProductPrice(Env.getCtx(), 0, getTrxName()); + pp.setM_PriceList_Version_ID(plv.getM_PriceList_Version_ID()); + pp.setM_Product_ID(p1.get_ID()); + BigDecimal p1price = new BigDecimal("30.00"); + pp.setPriceStd(p1price); + pp.setPriceList(p1price); + pp.saveEx(); + + p2 = new MProduct(Env.getCtx(), 0, null); + p2.setM_Product_Category_ID(DictionaryIDs.M_Product_Category.STANDARD.id); + p2.setName("testUnplannedLandedCostReversalAfterShipment2.2"); + p2.setProductType(MProduct.PRODUCTTYPE_Item); + p2.setIsStocked(true); + p2.setIsSold(true); + p2.setIsPurchased(true); + p2.setC_UOM_ID(DictionaryIDs.C_UOM.EACH.id); + p2.setC_TaxCategory_ID(DictionaryIDs.C_TaxCategory.STANDARD.id); + p2.saveEx(); + + pp = new MProductPrice(Env.getCtx(), 0, getTrxName()); + pp.setM_PriceList_Version_ID(plv.getM_PriceList_Version_ID()); + pp.setM_Product_ID(p2.get_ID()); + BigDecimal p2price = new BigDecimal("50.00"); + pp.setPriceStd(p2price); + pp.setPriceList(p2price); + pp.saveEx(); + + //create purchase order + MOrder purchaseOrder = new MOrder(Env.getCtx(), 0, getTrxName()); + purchaseOrder.setBPartner(MBPartner.get(Env.getCtx(), DictionaryIDs.C_BPartner.PATIO.id)); + purchaseOrder.setC_DocTypeTarget_ID(DictionaryIDs.C_DocType.PURCHASE_ORDER.id); + purchaseOrder.setIsSOTrx(false); + purchaseOrder.setSalesRep_ID(DictionaryIDs.AD_User.GARDEN_ADMIN.id); + purchaseOrder.setDocStatus(DocAction.STATUS_Drafted); + purchaseOrder.setDocAction(DocAction.ACTION_Complete); + purchaseOrder.setDateOrdered(today); + purchaseOrder.setDatePromised(today); + purchaseOrder.saveEx(); + + MOrderLine poLine1 = new MOrderLine(purchaseOrder); + poLine1.setLine(10); + poLine1.setProduct(new MProduct(Env.getCtx(), p1.get_ID(), getTrxName())); + BigDecimal orderQty = new BigDecimal("10"); + poLine1.setQty(orderQty); + poLine1.setDatePromised(today); + poLine1.setPrice(p1price); + poLine1.saveEx(); + + MOrderLine poLine2 = new MOrderLine(purchaseOrder); + poLine2.setLine(10); + poLine2.setProduct(new MProduct(Env.getCtx(), p2.get_ID(), getTrxName())); + poLine2.setQty(orderQty); + poLine2.setDatePromised(today); + poLine2.setPrice(p2price); + poLine2.saveEx(); + + ProcessInfo info = MWorkflow.runDocumentActionWorkflow(purchaseOrder, DocAction.ACTION_Complete); + assertFalse(info.isError(), info.getSummary()); + purchaseOrder.load(getTrxName()); + assertEquals(DocAction.STATUS_Completed, purchaseOrder.getDocStatus()); + + //mr1 for 10 each + MInOut receipt1 = new MInOut(purchaseOrder, DictionaryIDs.C_DocType.MM_RECEIPT.id, purchaseOrder.getDateOrdered()); + receipt1.setDocStatus(DocAction.STATUS_Drafted); + receipt1.setDocAction(DocAction.ACTION_Complete); + receipt1.saveEx(); + + MInOutLine receipt1Line1 = new MInOutLine(receipt1); + BigDecimal mr1Qty = new BigDecimal("10"); + receipt1Line1.setOrderLine(poLine1, 0, mr1Qty); + receipt1Line1.setQty(mr1Qty); + receipt1Line1.saveEx(); + + MInOutLine receipt1Line2 = new MInOutLine(receipt1); + receipt1Line2.setOrderLine(poLine2, 0, mr1Qty); + receipt1Line2.setQty(mr1Qty); + receipt1Line2.saveEx(); + + info = MWorkflow.runDocumentActionWorkflow(receipt1, DocAction.ACTION_Complete); + assertFalse(info.isError(), info.getSummary()); + receipt1.load(getTrxName()); + assertEquals(DocAction.STATUS_Completed, receipt1.getDocStatus()); + if (!receipt1.isPosted()) { + String error = DocumentEngine.postImmediate(Env.getCtx(), receipt1.getAD_Client_ID(), receipt1.get_Table_ID(), receipt1.get_ID(), false, getTrxName()); + assertNull(error, error); + } + + //assert p1 cost and posting + List cds = MCostDetail.list(Env.getCtx(), "C_OrderLine_ID=?", poLine1.getC_OrderLine_ID(), 0, as.get_ID(), getTrxName()); + assertTrue(cds.size() == 1, "MCostDetail not found for order line1"); + for(MCostDetail cd : cds) { + if (cd.getM_CostElement_ID() == 0) { + assertEquals(10, cd.getQty().intValue(), "Unexpected MCostDetail Qty"); + assertEquals(p1price.multiply(mr1Qty).setScale(2, RoundingMode.HALF_UP), cd.getAmt().setScale(2, RoundingMode.HALF_UP), "Unexpected MCostDetail Amt"); + } + } + + p1.set_TrxName(getTrxName()); + MCost p1mcost = p1.getCostingRecord(as, getAD_Org_ID(), 0, as.getCostingMethod()); + assertNotNull(p1mcost, "No MCost record found"); + assertEquals(p1price, p1mcost.getCurrentCostPrice().setScale(2, RoundingMode.HALF_UP), "Unexpected current cost price"); + + ProductCost p1ProductCost = new ProductCost(Env.getCtx(), p1.get_ID(), 0, getTrxName()); + MAccount assetAccount = p1ProductCost.getAccount(ProductCost.ACCTTYPE_P_Asset, as); + MAccount varianceAccount = p1ProductCost.getAccount(ProductCost.ACCTTYPE_P_AverageCostVariance, as); + Doc doc = DocManager.getDocument(as, MInOut.Table_ID, receipt1.get_ID(), getTrxName()); + MAccount nivReceiptAccount = doc.getAccount(Doc.ACCTTYPE_NotInvoicedReceipts, as); + Query query = MFactAcct.createRecordIdQuery(MInOut.Table_ID, receipt1.get_ID(), as.get_ID(), getTrxName()); + List factAccts = query.list(); + List expected = Arrays.asList(new FactAcct(assetAccount, p1price.multiply(mr1Qty), 2, true), + new FactAcct(nivReceiptAccount, p1price.multiply(mr1Qty), 2, false)); + assertFactAcctEntries(factAccts, expected); + + //assert p2 cost and posting + cds = MCostDetail.list(Env.getCtx(), "C_OrderLine_ID=?", poLine2.getC_OrderLine_ID(), 0, as.get_ID(), getTrxName()); + assertTrue(cds.size() == 1, "MCostDetail not found for order line2"); + for(MCostDetail cd : cds) { + if (cd.getM_CostElement_ID() == 0) { + assertEquals(10, cd.getQty().intValue(), "Unexpected MCostDetail Qty"); + assertEquals(p2price.multiply(mr1Qty).setScale(2, RoundingMode.HALF_UP), cd.getAmt().setScale(2, RoundingMode.HALF_UP), "Unexpected MCostDetail Amt"); + } + } + + p2.set_TrxName(getTrxName()); + MCost p2mcost = p2.getCostingRecord(as, getAD_Org_ID(), 0, as.getCostingMethod()); + assertNotNull(p2mcost, "No MCost record found"); + assertEquals(p2price, p2mcost.getCurrentCostPrice().setScale(2, RoundingMode.HALF_UP), "Unexpected current cost price"); + + ProductCost p2ProductCost = new ProductCost(Env.getCtx(), p2.get_ID(), 0, getTrxName()); + assetAccount = p2ProductCost.getAccount(ProductCost.ACCTTYPE_P_Asset, as); + expected = Arrays.asList(new FactAcct(assetAccount, p2price.multiply(mr1Qty), 2, true), + new FactAcct(nivReceiptAccount, p2price.multiply(mr1Qty), 2, false)); + assertFactAcctEntries(factAccts, expected); + + //full po invoice + MInvoice purchaseInvoice = new MInvoice(purchaseOrder, DictionaryIDs.C_DocType.AP_INVOICE.id, purchaseOrder.getDateOrdered()); + purchaseInvoice.setDocStatus(DocAction.STATUS_Drafted); + purchaseInvoice.setDocAction(DocAction.ACTION_Complete); + purchaseInvoice.saveEx(); + + MInvoiceLine piLine1 = new MInvoiceLine(purchaseInvoice); + piLine1.setOrderLine(poLine1); + piLine1.setLine(10); + piLine1.setProduct(p1); + piLine1.setQty(poLine1.getQtyOrdered()); + piLine1.saveEx(); + + MInvoiceLine piLine2 = new MInvoiceLine(purchaseInvoice); + piLine2.setOrderLine(poLine2); + piLine2.setLine(10); + piLine2.setProduct(p2); + piLine2.setQty(poLine2.getQtyOrdered()); + piLine2.saveEx(); + + info = MWorkflow.runDocumentActionWorkflow(purchaseInvoice, DocAction.ACTION_Complete); + purchaseInvoice.load(getTrxName()); + assertFalse(info.isError(), info.getSummary()); + assertEquals(DocAction.STATUS_Completed, purchaseInvoice.getDocStatus()); + + if (!purchaseInvoice.isPosted()) { + String error = DocumentEngine.postImmediate(Env.getCtx(), purchaseInvoice.getAD_Client_ID(), MInvoice.Table_ID, purchaseInvoice.get_ID(), false, getTrxName()); + assertTrue(error == null); + } + purchaseInvoice.load(getTrxName()); + assertTrue(purchaseInvoice.isPosted()); + + Doc invoiceDoc = DocManager.getDocument(as, MInvoice.Table_ID, purchaseInvoice.get_ID(), getTrxName()); + MAccount liabilityAccount = invoiceDoc.getAccount(Doc.ACCTTYPE_V_Liability, as); + MAccount inventoryClearingAccount = p1ProductCost.getAccount(ProductCost.ACCTTYPE_P_InventoryClearing, as); + query = MFactAcct.createRecordIdQuery(MInvoice.Table_ID, purchaseInvoice.get_ID(), as.get_ID(), getTrxName()); + factAccts = query.list(); + expected = Arrays.asList(new FactAcct(inventoryClearingAccount, p1price.multiply(orderQty), 2, true), + new FactAcct(inventoryClearingAccount, p2price.multiply(orderQty), 2, true), + new FactAcct(liabilityAccount, p1price.multiply(orderQty).add(p2price.multiply(orderQty)), 2, false)); + assertFactAcctEntries(factAccts, expected); + + //so and shipment + MBPartner customer = MBPartner.get(Env.getCtx(), DictionaryIDs.C_BPartner.JOE_BLOCK.id); + MOrder salesOrder = new MOrder(Env.getCtx(), 0, getTrxName()); + salesOrder.setC_DocTypeTarget_ID(MOrder.DocSubTypeSO_Standard); + salesOrder.setBPartner(customer); + salesOrder.setDeliveryRule(MOrder.DELIVERYRULE_CompleteOrder); + salesOrder.setDocStatus(DocAction.STATUS_Drafted); + salesOrder.setDocAction(DocAction.ACTION_Complete); + salesOrder.setDatePromised(today); + salesOrder.saveEx(); + + MOrderLine soLine1 = new MOrderLine(salesOrder); + soLine1.setLine(10); + soLine1.setProduct(p1); + BigDecimal p1ShipQty = new BigDecimal("10"); + soLine1.setQty(p1ShipQty); + soLine1.setDatePromised(today); + soLine1.setPrice(new BigDecimal("50")); + soLine1.saveEx(); + + MOrderLine soLine2 = new MOrderLine(salesOrder); + soLine2.setLine(20); + soLine2.setProduct(p2); + BigDecimal p2ShipQty = new BigDecimal("5"); + soLine2.setQty(p2ShipQty); + soLine2.setDatePromised(today); + soLine2.setPrice(new BigDecimal("70")); + soLine2.saveEx(); + + info = MWorkflow.runDocumentActionWorkflow(salesOrder, DocAction.ACTION_Complete); + salesOrder.load(getTrxName()); + assertFalse(info.isError(), info.getSummary()); + assertEquals(DocAction.STATUS_Completed, salesOrder.getDocStatus()); + + MInOut shipment = new MInOut(salesOrder, DictionaryIDs.C_DocType.MM_SHIPMENT.id, salesOrder.getDateOrdered()); + shipment.setDocStatus(DocAction.STATUS_Drafted); + shipment.setDocAction(DocAction.ACTION_Complete); + shipment.saveEx(); + + MInOutLine shipmentLine1 = new MInOutLine(shipment); + shipmentLine1.setOrderLine(soLine1, 0, soLine1.getQtyOrdered()); + shipmentLine1.setQty(soLine1.getQtyOrdered()); + shipmentLine1.saveEx(); + + MInOutLine shipmentLine2 = new MInOutLine(shipment); + shipmentLine2.setOrderLine(soLine2, 0, soLine2.getQtyOrdered()); + shipmentLine2.setQty(soLine2.getQtyOrdered()); + shipmentLine2.saveEx(); + + info = MWorkflow.runDocumentActionWorkflow(shipment, DocAction.ACTION_Complete); + assertFalse(info.isError(), info.getSummary()); + shipment.load(getTrxName()); + assertEquals(DocAction.STATUS_Completed, shipment.getDocStatus()); + + //landed cost invoice + MBPartner freightBP = MBPartner.get(Env.getCtx(), DictionaryIDs.C_BPartner.PATIO.id); + MInvoice freightInvoice = new MInvoice(Env.getCtx(), 0, getTrxName()); + freightInvoice.setC_DocTypeTarget_ID(MDocType.DOCBASETYPE_APInvoice); + freightInvoice.setBPartner(freightBP); + freightInvoice.setDocStatus(DocAction.STATUS_Drafted); + freightInvoice.setDocAction(DocAction.ACTION_Complete); + freightInvoice.saveEx(); + + MInvoiceLine fiLine = new MInvoiceLine(freightInvoice); + fiLine.setLine(10); + fiLine.setC_Charge_ID(DictionaryIDs.C_Charge.FREIGHT.id); + fiLine.setQty(BigDecimal.ONE); + BigDecimal freightPrice = new BigDecimal("200.00"); + fiLine.setPrice(freightPrice); + fiLine.setC_UOM_ID(DictionaryIDs.C_UOM.EACH.id); + fiLine.saveEx(); + + MLandedCost landedCost = new MLandedCost(Env.getCtx(), 0, getTrxName()); + landedCost.setC_InvoiceLine_ID(fiLine.get_ID()); + landedCost.setM_CostElement_ID(DictionaryIDs.M_CostElement.FREIGHT.id); + landedCost.setM_InOut_ID(receipt1.get_ID()); + landedCost.setM_InOutLine_ID(receipt1Line1.get_ID()); + landedCost.setLandedCostDistribution(MLandedCost.LANDEDCOSTDISTRIBUTION_Costs); + landedCost.saveEx(); + + landedCost = new MLandedCost(Env.getCtx(), 0, getTrxName()); + landedCost.setC_InvoiceLine_ID(fiLine.get_ID()); + landedCost.setM_CostElement_ID(DictionaryIDs.M_CostElement.FREIGHT.id); + landedCost.setM_InOut_ID(receipt1.get_ID()); + landedCost.setM_InOutLine_ID(receipt1Line2.get_ID()); + landedCost.setLandedCostDistribution(MLandedCost.LANDEDCOSTDISTRIBUTION_Costs); + landedCost.saveEx(); + + String error = landedCost.allocateCosts(); + assertTrue(Util.isEmpty(error, true), error); + + BigDecimal totalBase = purchaseInvoice.getGrandTotal(); + BigDecimal p1a1 = p1price.multiply(mr1Qty).multiply(fiLine.getLineNetAmt()).divide(totalBase, 6, RoundingMode.HALF_UP); + BigDecimal p2a1 = p2price.multiply(mr1Qty).multiply(fiLine.getLineNetAmt()).divide(totalBase, 6, RoundingMode.HALF_UP); + + MLandedCostAllocation[] allocations = MLandedCostAllocation.getOfInvoiceLine(Env.getCtx(), fiLine.get_ID(), getTrxName()); + assertEquals(2, allocations.length, "Unexpected number of landed cost allocation line"); + for (MLandedCostAllocation allocation : allocations) { + if (allocation.getM_Product_ID() == p1.get_ID() && allocation.getQty().intValue() == 10) { + assertEquals(p1a1.setScale(2, RoundingMode.HALF_UP), allocation.getAmt().setScale(2, RoundingMode.HALF_UP), "Unexpected landed cost allocation amount"); + } else if (allocation.getM_Product_ID() == p2.get_ID() && allocation.getQty().intValue() == 10) { + assertEquals(p2a1.setScale(2, RoundingMode.HALF_UP), allocation.getAmt().setScale(2, RoundingMode.HALF_UP), "Unexpected landed cost allocation amount"); + } else { + fail("Unknown landed cost allocation line: " + allocation); + } + } + + info = MWorkflow.runDocumentActionWorkflow(freightInvoice, DocAction.ACTION_Complete); + freightInvoice.load(getTrxName()); + assertFalse(info.isError(), info.getSummary()); + assertEquals(DocAction.STATUS_Completed, freightInvoice.getDocStatus()); + + if (!freightInvoice.isPosted()) { + error = DocumentEngine.postImmediate(Env.getCtx(), freightInvoice.getAD_Client_ID(), MInvoice.Table_ID, freightInvoice.get_ID(), false, getTrxName()); + assertTrue(error == null, error); + } + freightInvoice.load(getTrxName()); + assertTrue(freightInvoice.isPosted()); + + //assert freight invoice posting + doc = DocManager.getDocument(as, MInvoice.Table_ID, freightInvoice.get_ID(), getTrxName()); + MAccount apAccount = doc.getAccount(Doc.ACCTTYPE_V_Liability, as); + query = MFactAcct.createRecordIdQuery(MInvoice.Table_ID, freightInvoice.get_ID(), as.get_ID(), getTrxName()); + factAccts = query.list(); + expected = Arrays.asList(new FactAcct(varianceAccount, p1a1, 2, true), + new FactAcct(assetAccount, p2a1.divide(new BigDecimal("2"), RoundingMode.HALF_UP), 2, true), + new FactAcct(apAccount, freightInvoice.getGrandTotal(), 2, false)); + assertFactAcctEntries(factAccts, expected); + + p1mcost = p1.getCostingRecord(as, getAD_Org_ID(), 0, as.getCostingMethod()); + assertEquals(p1price.setScale(1, RoundingMode.HALF_UP), + p1mcost.getCurrentCostPrice().setScale(1, RoundingMode.HALF_UP), "Unexpected current cost price"); + + p1mcost = p2.getCostingRecord(as, getAD_Org_ID(), 0, as.getCostingMethod()); + assertEquals(p2price.add(p2a1.divide(new BigDecimal("2"), 2, RoundingMode.HALF_UP).divide(new BigDecimal("5"), RoundingMode.HALF_UP)) + .setScale(1, RoundingMode.HALF_UP), p1mcost.getCurrentCostPrice().setScale(1, RoundingMode.HALF_UP), "Unexpected current cost price"); + + //reverse freight invoice + Env.setContext(Env.getCtx(), Env.DATE, tomorrow); + info = MWorkflow.runDocumentActionWorkflow(freightInvoice, DocAction.ACTION_Reverse_Correct); + freightInvoice.load(getTrxName()); + assertFalse(info.isError(), info.getSummary()); + assertEquals(DocAction.STATUS_Reversed, freightInvoice.getDocStatus()); + assertTrue(freightInvoice.getReversal_ID() > 0, "Unexpected reversal id"); + MInvoice reversal = new MInvoice(Env.getCtx(), freightInvoice.getReversal_ID(), getTrxName()); + assertEquals(freightInvoice.getReversal_ID(), reversal.get_ID()); + if (!reversal.isPosted()) { + String msg = DocumentEngine.postImmediate(Env.getCtx(), getAD_Client_ID(), MInvoice.Table_ID, reversal.get_ID(), false, getTrxName()); + assertNull(msg, msg); + } + + query = MFactAcct.createRecordIdQuery(MInvoice.Table_ID, reversal.get_ID(), as.get_ID(), getTrxName()); + List rFactAccts = query.list(); + expected = Arrays.asList(new FactAcct(varianceAccount, p1a1, 2, false), + new FactAcct(assetAccount, p2a1.divide(new BigDecimal(2), RoundingMode.HALF_UP), 2, false), + new FactAcct(apAccount, freightInvoice.getGrandTotal(), 2, true)); + assertFactAcctEntries(rFactAccts, expected); + + MAcctSchema[] ass = MAcctSchema.getClientAcctSchema(Env.getCtx(), Env.getAD_Client_ID(Env.getCtx())); + Optional optional = Arrays.stream(ass).filter(e -> e.getC_AcctSchema_ID() != as.get_ID()).findFirst(); + if (optional.isPresent()) { + MAcctSchema as2 = optional.get(); + query = MFactAcct.createRecordIdQuery(MInvoice.Table_ID, freightInvoice.get_ID(), as2.get_ID(), getTrxName()); + factAccts = query.list(); + query = MFactAcct.createRecordIdQuery(MInvoice.Table_ID, freightInvoice.getReversal_ID(), as2.get_ID(), getTrxName()); + rFactAccts = query.list(); + expected = new ArrayList(); + for(MFactAcct factAcct : factAccts) { + MAccount acct = MAccount.get(factAcct, getTrxName()); + if (factAcct.getAmtAcctDr().signum() != 0) { + expected.add(new FactAcct(acct, factAcct.getAmtAcctDr(), 1, false)); + } else if (factAcct.getAmtAcctCr().signum() != 0) { + expected.add(new FactAcct(acct, factAcct.getAmtAcctCr(), 1, true)); + } + } + assertFactAcctEntries(rFactAccts, expected); + } + } finally { + rollback(); + + if (p1 != null) { + p1.set_TrxName(null); + p1.deleteEx(true); + } + + if (p2 != null) { + p2.set_TrxName(null); + p2.deleteEx(true); + } + + ConversionRateHelper.deleteConversionRate(cr1); + ConversionRateHelper.deleteConversionRate(cr2); + } + } + + @Test + public void testUnplannedLandedCostReversalAfterInventoryUseASI() { + + MClient client = MClient.get(Env.getCtx()); + MAcctSchema as = client.getAcctSchema(); + assertEquals(as.getCostingMethod(), MCostElement.COSTINGMETHOD_AveragePO, "Default costing method not Average PO"); + + MProduct p1 = null; + MProduct p2 = null; + MCurrency usd = MCurrency.get(DictionaryIDs.C_Currency.USD.id); + MCurrency euro = MCurrency.get(DictionaryIDs.C_Currency.EUR.id); + int C_ConversionType_ID = DictionaryIDs.C_ConversionType.SPOT.id; + Timestamp today = TimeUtil.getDay(null); + Timestamp tomorrow = TimeUtil.addDays(today, 1); + BigDecimal crate1 = new BigDecimal("1.05"); + BigDecimal crate2 = new BigDecimal("1.12"); + MConversionRate cr1 = ConversionRateHelper.createConversionRate(euro.getC_Currency_ID(), usd.getC_Currency_ID(), C_ConversionType_ID, today, crate1, true); + MConversionRate cr2 = ConversionRateHelper.createConversionRate(euro.getC_Currency_ID(), usd.getC_Currency_ID(), C_ConversionType_ID, tomorrow, crate2, true); + try { + p1 = new MProduct(Env.getCtx(), 0, null); + p1.setM_Product_Category_ID(DictionaryIDs.M_Product_Category.STANDARD.id); + p1.setName("testUnplannedLandedCostReversalAfterInventoryUseASI.1"); + p1.setProductType(MProduct.PRODUCTTYPE_Item); + p1.setIsStocked(true); + p1.setIsSold(true); + p1.setIsPurchased(true); + p1.setC_UOM_ID(DictionaryIDs.C_UOM.EACH.id); + p1.setC_TaxCategory_ID(DictionaryIDs.C_TaxCategory.STANDARD.id); + p1.setM_AttributeSet_ID(DictionaryIDs.M_AttributeSet.FERTILIZER_LOT.id); + p1.saveEx(); + + MPriceListVersion plv = MPriceList.get(DictionaryIDs.M_PriceList.IMPORT.id).getPriceListVersion(null); + MProductPrice pp = new MProductPrice(Env.getCtx(), 0, getTrxName()); + pp.setM_PriceList_Version_ID(plv.getM_PriceList_Version_ID()); + pp.setM_Product_ID(p1.get_ID()); + BigDecimal p1price = new BigDecimal("30.00"); + pp.setPriceStd(p1price); + pp.setPriceList(p1price); + pp.saveEx(); + + p2 = new MProduct(Env.getCtx(), 0, null); + p2.setM_Product_Category_ID(DictionaryIDs.M_Product_Category.STANDARD.id); + p2.setName("testUnplannedLandedCostReversalAfterInventoryUseASI.2"); + p2.setProductType(MProduct.PRODUCTTYPE_Item); + p2.setIsStocked(true); + p2.setIsSold(true); + p2.setIsPurchased(true); + p2.setC_UOM_ID(DictionaryIDs.C_UOM.EACH.id); + p2.setC_TaxCategory_ID(DictionaryIDs.C_TaxCategory.STANDARD.id); + p2.setM_AttributeSet_ID(DictionaryIDs.M_AttributeSet.FERTILIZER_LOT.id); + p2.saveEx(); + + pp = new MProductPrice(Env.getCtx(), 0, getTrxName()); + pp.setM_PriceList_Version_ID(plv.getM_PriceList_Version_ID()); + pp.setM_Product_ID(p2.get_ID()); + BigDecimal p2price = new BigDecimal("50.00"); + pp.setPriceStd(p2price); + pp.setPriceList(p2price); + pp.saveEx(); + + //create purchase order + MOrder purchaseOrder = new MOrder(Env.getCtx(), 0, getTrxName()); + purchaseOrder.setBPartner(MBPartner.get(Env.getCtx(), DictionaryIDs.C_BPartner.PATIO.id)); + purchaseOrder.setC_DocTypeTarget_ID(DictionaryIDs.C_DocType.PURCHASE_ORDER.id); + purchaseOrder.setIsSOTrx(false); + purchaseOrder.setSalesRep_ID(DictionaryIDs.AD_User.GARDEN_ADMIN.id); + purchaseOrder.setDocStatus(DocAction.STATUS_Drafted); + purchaseOrder.setDocAction(DocAction.ACTION_Complete); + purchaseOrder.setDateOrdered(today); + purchaseOrder.setDatePromised(today); + purchaseOrder.setM_PriceList_ID(plv.getM_PriceList_ID()); + purchaseOrder.saveEx(); + + MOrderLine poLine1 = new MOrderLine(purchaseOrder); + poLine1.setLine(10); + poLine1.setProduct(new MProduct(Env.getCtx(), p1.get_ID(), getTrxName())); + BigDecimal orderQty = new BigDecimal("100"); + poLine1.setQty(orderQty); + poLine1.setDatePromised(today); + poLine1.setPrice(p1price); + poLine1.saveEx(); + + MOrderLine poLine2 = new MOrderLine(purchaseOrder); + poLine2.setLine(10); + poLine2.setProduct(new MProduct(Env.getCtx(), p2.get_ID(), getTrxName())); + poLine2.setQty(orderQty); + poLine2.setDatePromised(today); + poLine2.setPrice(p2price); + poLine2.saveEx(); + + ProcessInfo info = MWorkflow.runDocumentActionWorkflow(purchaseOrder, DocAction.ACTION_Complete); + assertFalse(info.isError(), info.getSummary()); + purchaseOrder.load(getTrxName()); + assertEquals(DocAction.STATUS_Completed, purchaseOrder.getDocStatus()); + + //material receipt 1 + MInOut receipt1 = new MInOut(purchaseOrder, DictionaryIDs.C_DocType.MM_RECEIPT.id, purchaseOrder.getDateOrdered()); + receipt1.setDocStatus(DocAction.STATUS_Drafted); + receipt1.setDocAction(DocAction.ACTION_Complete); + receipt1.saveEx(); + + MInOutLine mr1Line1 = new MInOutLine(receipt1); + BigDecimal mr1l1Qty = new BigDecimal("25"); + mr1Line1.setOrderLine(poLine1, 0, mr1l1Qty); + mr1Line1.setQty(mr1l1Qty); + MAttributeSetInstance mr1l1asi = new MAttributeSetInstance(Env.getCtx(), 0, getTrxName()); + mr1l1asi.setM_AttributeSet_ID(p1.getM_AttributeSet_ID()); + mr1l1asi.setLot("mr1l1asi"); + mr1l1asi.setDescription(); + mr1l1asi.saveEx(); + mr1Line1.setM_AttributeSetInstance_ID(mr1l1asi.get_ID()); + mr1Line1.saveEx(); + + MInOutLine mr1Line2 = new MInOutLine(receipt1); + BigDecimal mr1l2Qty = new BigDecimal("100"); + mr1Line2.setOrderLine(poLine2, 0, mr1l2Qty); + mr1Line2.setQty(mr1l2Qty); + MAttributeSetInstance mr1l2asi = new MAttributeSetInstance(Env.getCtx(), 0, getTrxName()); + mr1l2asi.setM_AttributeSet_ID(p2.getM_AttributeSet_ID()); + mr1l2asi.setLot("mr1l2asi"); + mr1l2asi.setDescription(); + mr1l2asi.saveEx(); + mr1Line2.setM_AttributeSetInstance_ID(mr1l2asi.get_ID()); + mr1Line2.saveEx(); + + info = MWorkflow.runDocumentActionWorkflow(receipt1, DocAction.ACTION_Complete); + assertFalse(info.isError(), info.getSummary()); + receipt1.load(getTrxName()); + assertEquals(DocAction.STATUS_Completed, receipt1.getDocStatus()); + if (!receipt1.isPosted()) { + String error = DocumentEngine.postImmediate(Env.getCtx(), receipt1.getAD_Client_ID(), receipt1.get_Table_ID(), receipt1.get_ID(), false, getTrxName()); + assertNull(error, error); + } + + //material receipt 2 + MInOut receipt2 = new MInOut(purchaseOrder, DictionaryIDs.C_DocType.MM_RECEIPT.id, purchaseOrder.getDateOrdered()); + receipt2.setDocStatus(DocAction.STATUS_Drafted); + receipt2.setDocAction(DocAction.ACTION_Complete); + receipt2.saveEx(); + + MInOutLine mr2Line1 = new MInOutLine(receipt2); + BigDecimal mr2l1Qty = new BigDecimal("75"); + mr2Line1.setOrderLine(poLine1, 0, mr2l1Qty); + mr2Line1.setQty(mr2l1Qty); + MAttributeSetInstance mr2l1asi = new MAttributeSetInstance(Env.getCtx(), 0, getTrxName()); + mr2l1asi.setM_AttributeSet_ID(p1.getM_AttributeSet_ID()); + mr2l1asi.setLot("mr2l1asi"); + mr2l1asi.setDescription(); + mr2l1asi.saveEx(); + mr2Line1.setM_AttributeSetInstance_ID(mr2l1asi.get_ID()); + mr2Line1.saveEx(); + + info = MWorkflow.runDocumentActionWorkflow(receipt2, DocAction.ACTION_Complete); + assertFalse(info.isError(), info.getSummary()); + receipt2.load(getTrxName()); + assertEquals(DocAction.STATUS_Completed, receipt2.getDocStatus()); + if (!receipt2.isPosted()) { + String error = DocumentEngine.postImmediate(Env.getCtx(), receipt2.getAD_Client_ID(), receipt2.get_Table_ID(), receipt2.get_ID(), false, getTrxName()); + assertNull(error, error); + } + + //assert p1 cost and posting + List cds = MCostDetail.list(Env.getCtx(), "C_OrderLine_ID=?", poLine1.getC_OrderLine_ID(), mr1l1asi.get_ID(), as.get_ID(), getTrxName()); + assertEquals(1, cds.size(), "Unexpected number of MCostDetail records for order line1"); + for(MCostDetail cd : cds) { + if (cd.getM_CostElement_ID() == 0) { + assertEquals(mr1Line1.getMovementQty().intValue(), cd.getQty().intValue(), "Unexpected MCostDetail Qty"); + assertEquals(p1price.multiply(mr1Line1.getMovementQty()).multiply(crate1).setScale(2, RoundingMode.HALF_UP), cd.getAmt().setScale(2, RoundingMode.HALF_UP), "Unexpected MCostDetail Amt"); + } + } + + //assert p1 mcost + p1.set_TrxName(getTrxName()); + MCost p1mcost = p1.getCostingRecord(as, getAD_Org_ID(), 0, as.getCostingMethod()); + assertNotNull(p1mcost, "No MCost record found"); + assertEquals(p1price.multiply(crate1).setScale(2, RoundingMode.HALF_UP), p1mcost.getCurrentCostPrice().setScale(2, RoundingMode.HALF_UP), "Unexpected current cost price"); + + ProductCost p1ProductCost = new ProductCost(Env.getCtx(), p1.get_ID(), 0, getTrxName()); + MAccount assetAccount = p1ProductCost.getAccount(ProductCost.ACCTTYPE_P_Asset, as); + MAccount varianceAccount = p1ProductCost.getAccount(ProductCost.ACCTTYPE_P_AverageCostVariance, as); + Doc doc = DocManager.getDocument(as, MInOut.Table_ID, receipt1.get_ID(), getTrxName()); + MAccount nivReceiptAccount = doc.getAccount(Doc.ACCTTYPE_NotInvoicedReceipts, as); + Query query = MFactAcct.createRecordIdQuery(MInOut.Table_ID, receipt1.get_ID(), as.get_ID(), getTrxName()); + List factAccts = query.list(); + List expected = Arrays.asList(new FactAcct(assetAccount, p1price.multiply(mr1l1Qty).multiply(crate1).setScale(2, RoundingMode.HALF_UP), p1price.multiply(mr1l1Qty), 2, true), + new FactAcct(nivReceiptAccount, p1price.multiply(mr1l1Qty).multiply(crate1).setScale(2, RoundingMode.HALF_UP), p1price.multiply(mr1l1Qty), 2, false)); + assertFactAcctEntries(factAccts, expected); + + //PO invoice + MInvoice purchaseInvoice = new MInvoice(purchaseOrder, DictionaryIDs.C_DocType.AP_INVOICE.id, purchaseOrder.getDateOrdered()); + purchaseInvoice.setDocStatus(DocAction.STATUS_Drafted); + purchaseInvoice.setDocAction(DocAction.ACTION_Complete); + purchaseInvoice.saveEx(); + + MInvoiceLine piLine1 = new MInvoiceLine(purchaseInvoice); + piLine1.setOrderLine(poLine1); + piLine1.setLine(10); + piLine1.setProduct(p1); + piLine1.setQty(poLine1.getQtyOrdered()); + piLine1.saveEx(); + + MInvoiceLine piLine2 = new MInvoiceLine(purchaseInvoice); + piLine2.setOrderLine(poLine2); + piLine2.setLine(20); + piLine2.setProduct(p2); + piLine2.setQty(poLine2.getQtyOrdered()); + piLine2.saveEx(); + + info = MWorkflow.runDocumentActionWorkflow(purchaseInvoice, DocAction.ACTION_Complete); + purchaseInvoice.load(getTrxName()); + assertFalse(info.isError(), info.getSummary()); + assertEquals(DocAction.STATUS_Completed, purchaseInvoice.getDocStatus()); + + if (!purchaseInvoice.isPosted()) { + String error = DocumentEngine.postImmediate(Env.getCtx(), purchaseInvoice.getAD_Client_ID(), MInvoice.Table_ID, purchaseInvoice.get_ID(), false, getTrxName()); + assertTrue(error == null); + } + purchaseInvoice.load(getTrxName()); + assertTrue(purchaseInvoice.isPosted()); + + Doc invoiceDoc = DocManager.getDocument(as, MInvoice.Table_ID, purchaseInvoice.get_ID(), getTrxName()); + MAccount liabilityAccount = invoiceDoc.getAccount(Doc.ACCTTYPE_V_Liability, as); + MAccount inventoryClearingAccount = p1ProductCost.getAccount(ProductCost.ACCTTYPE_P_InventoryClearing, as); + query = MFactAcct.createRecordIdQuery(MInvoice.Table_ID, purchaseInvoice.get_ID(), as.get_ID(), getTrxName()); + factAccts = query.list(); + expected = Arrays.asList(new FactAcct(inventoryClearingAccount, p1price.multiply(orderQty).multiply(crate1), p1price.multiply(orderQty), 2, true), + new FactAcct(liabilityAccount, p1price.multiply(orderQty).multiply(crate1).add(p2price.multiply(orderQty).multiply(crate1)), + p1price.multiply(orderQty).add(p2price.multiply(orderQty)), 2, false)); + assertFactAcctEntries(factAccts, expected); + + //inventory decrease + MInventory inventory = new MInventory(Env.getCtx(), 0, getTrxName()); + inventory.setC_DocType_ID(DictionaryIDs.C_DocType.INTERNAL_USE_INVENTORY.id); + inventory.setM_Warehouse_ID(DictionaryIDs.M_Warehouse.HQ.id); + inventory.setMovementDate(today); + inventory.saveEx(); + + MInventoryLine inventoryLine1 = new MInventoryLine(inventory, DictionaryIDs.M_Locator.HQ.id, p1.get_ID(), 0, null, null, new BigDecimal("25")); + inventoryLine1.setC_Charge_ID(DictionaryIDs.C_Charge.FREIGHT.id); + inventoryLine1.setM_AttributeSetInstance_ID(mr2l1asi.get_ID()); + inventoryLine1.saveEx(); + MInventoryLine inventoryLine2 = new MInventoryLine(inventory, DictionaryIDs.M_Locator.HQ.id, p2.get_ID(), 0, null, null, new BigDecimal("100")); + inventoryLine2.setC_Charge_ID(DictionaryIDs.C_Charge.FREIGHT.id); + inventoryLine2.setM_AttributeSetInstance_ID(mr1l2asi.get_ID()); + inventoryLine2.saveEx(); + + info = MWorkflow.runDocumentActionWorkflow(inventory, DocAction.ACTION_Complete); + assertFalse(info.isError(), info.getSummary()); + inventory.load(getTrxName()); + assertEquals(DocAction.STATUS_Completed, inventory.getDocStatus()); + if (!inventory.isPosted()) { + String error = DocumentEngine.postImmediate(Env.getCtx(), inventory.getAD_Client_ID(), inventory.get_Table_ID(), inventory.get_ID(), false, getTrxName()); + assertNull(error, error); + } + + //landed cost invoice + MBPartner freightBP = MBPartner.get(Env.getCtx(), DictionaryIDs.C_BPartner.PATIO.id); + MInvoice freightInvoice = new MInvoice(Env.getCtx(), 0, getTrxName()); + freightInvoice.setC_DocTypeTarget_ID(MDocType.DOCBASETYPE_APInvoice); + freightInvoice.setBPartner(freightBP); + freightInvoice.setDocStatus(DocAction.STATUS_Drafted); + freightInvoice.setDocAction(DocAction.ACTION_Complete); + freightInvoice.setM_PriceList_ID(DictionaryIDs.M_PriceList.STANDARD.id); + freightInvoice.saveEx(); + + MInvoiceLine fiLine = new MInvoiceLine(freightInvoice); + fiLine.setLine(10); + fiLine.setC_Charge_ID(DictionaryIDs.C_Charge.FREIGHT.id); + fiLine.setQty(BigDecimal.ONE); + BigDecimal freightPrice = new BigDecimal("1000.00"); + fiLine.setPrice(freightPrice); + fiLine.setC_UOM_ID(DictionaryIDs.C_UOM.EACH.id); + fiLine.saveEx(); + + MLandedCost landedCost = new MLandedCost(Env.getCtx(), 0, getTrxName()); + landedCost.setC_InvoiceLine_ID(fiLine.get_ID()); + landedCost.setM_CostElement_ID(DictionaryIDs.M_CostElement.FREIGHT.id); + landedCost.setM_InOut_ID(receipt1.get_ID()); + landedCost.setM_InOutLine_ID(mr1Line1.get_ID()); + landedCost.setLandedCostDistribution(MLandedCost.LANDEDCOSTDISTRIBUTION_Costs); + landedCost.saveEx(); + + landedCost = new MLandedCost(Env.getCtx(), 0, getTrxName()); + landedCost.setC_InvoiceLine_ID(fiLine.get_ID()); + landedCost.setM_CostElement_ID(DictionaryIDs.M_CostElement.FREIGHT.id); + landedCost.setM_InOut_ID(receipt1.get_ID()); + landedCost.setM_InOutLine_ID(mr1Line2.get_ID()); + landedCost.setLandedCostDistribution(MLandedCost.LANDEDCOSTDISTRIBUTION_Costs); + landedCost.saveEx(); + + landedCost = new MLandedCost(Env.getCtx(), 0, getTrxName()); + landedCost.setC_InvoiceLine_ID(fiLine.get_ID()); + landedCost.setM_CostElement_ID(DictionaryIDs.M_CostElement.FREIGHT.id); + landedCost.setM_InOut_ID(receipt1.get_ID()); + landedCost.setM_InOutLine_ID(mr2Line1.get_ID()); + landedCost.setLandedCostDistribution(MLandedCost.LANDEDCOSTDISTRIBUTION_Costs); + landedCost.saveEx(); + + String error = landedCost.allocateCosts(); + assertTrue(Util.isEmpty(error, true), error); + + BigDecimal totalBase = purchaseOrder.getGrandTotal(); + BigDecimal p1a1 = p1price.multiply(mr1l1Qty).multiply(fiLine.getLineNetAmt()).divide(totalBase, 6, RoundingMode.HALF_UP); + BigDecimal p1a2 = p1price.multiply(mr2l1Qty).multiply(fiLine.getLineNetAmt()).divide(totalBase, 6, RoundingMode.HALF_UP); + BigDecimal p2a1 = p2price.multiply(mr1l2Qty).multiply(fiLine.getLineNetAmt()).divide(totalBase, 6, RoundingMode.HALF_UP); + + MLandedCostAllocation[] allocations = MLandedCostAllocation.getOfInvoiceLine(Env.getCtx(), fiLine.get_ID(), getTrxName()); + assertEquals(3, allocations.length, "Unexpected number of landed cost allocation line"); + for (MLandedCostAllocation allocation : allocations) { + if (allocation.getM_Product_ID() == p1.get_ID() && allocation.getQty().intValue() == mr1l1Qty.intValue()) { + assertEquals(p1a1.setScale(2, RoundingMode.HALF_UP), allocation.getAmt().setScale(2, RoundingMode.HALF_UP), "Unexpected landed cost allocation amount"); + } else if (allocation.getM_Product_ID() == p1.get_ID() && allocation.getQty().intValue() == mr2l1Qty.intValue()) { + assertEquals(p1a2.setScale(2, RoundingMode.HALF_UP), allocation.getAmt().setScale(2, RoundingMode.HALF_UP), "Unexpected landed cost allocation amount"); + } else if (allocation.getM_Product_ID() == p2.get_ID() && allocation.getQty().intValue() == mr1l2Qty.intValue()) { + assertEquals(p2a1.setScale(2, RoundingMode.HALF_UP), allocation.getAmt().setScale(2, RoundingMode.HALF_UP), "Unexpected landed cost allocation amount"); + } else { + fail("Unknown landed cost allocation line: " + allocation); + } + } + + info = MWorkflow.runDocumentActionWorkflow(freightInvoice, DocAction.ACTION_Complete); + freightInvoice.load(getTrxName()); + assertFalse(info.isError(), info.getSummary()); + assertEquals(DocAction.STATUS_Completed, freightInvoice.getDocStatus()); + + if (!freightInvoice.isPosted()) { + error = DocumentEngine.postImmediate(Env.getCtx(), freightInvoice.getAD_Client_ID(), MInvoice.Table_ID, freightInvoice.get_ID(), false, getTrxName()); + assertTrue(error == null, error); + } + freightInvoice.load(getTrxName()); + assertTrue(freightInvoice.isPosted()); + + //assert freight invoice posting + doc = DocManager.getDocument(as, MInvoice.Table_ID, freightInvoice.get_ID(), getTrxName()); + MAccount apAccount = doc.getAccount(Doc.ACCTTYPE_V_Liability, as); + query = MFactAcct.createRecordIdQuery(MInvoice.Table_ID, freightInvoice.get_ID(), as.get_ID(), getTrxName()); + factAccts = query.list(); + BigDecimal p1OnHand = orderQty.add(inventoryLine1.getMovementQty()); + BigDecimal p1a1assetAmt = p1a1; + BigDecimal p1a2assetAmt = p1a2.divide(mr2l1Qty, RoundingMode.HALF_UP).multiply(p1OnHand.subtract(mr1l1Qty)); + BigDecimal p1a2varianceAmt = p1a2.subtract(p1a2assetAmt); + BigDecimal p2varianceAmt = p2a1; + expected = Arrays.asList(new FactAcct(varianceAccount, p1a2varianceAmt, p1a2varianceAmt, 2, true), + new FactAcct(assetAccount, p1a1assetAmt, p1a1assetAmt, 2, true), + new FactAcct(assetAccount, p1a2assetAmt, p1a2assetAmt, 2, true), + new FactAcct(varianceAccount, p2varianceAmt, p2varianceAmt, 2, true), + new FactAcct(varianceAccount, p1a2varianceAmt, p1a2varianceAmt, 2, true), + new FactAcct(apAccount, freightInvoice.getGrandTotal(), freightInvoice.getGrandTotal(), 2, false)); + assertFactAcctEntries(factAccts, expected); + + BigDecimal p1assetAmt = p1a1assetAmt.add(p1a2assetAmt); + p1mcost = p1.getCostingRecord(as, getAD_Org_ID(), 0, as.getCostingMethod()); + assertEquals(p1price.multiply(crate1).add(p1assetAmt.divide(p1OnHand, RoundingMode.HALF_UP)).setScale(2, RoundingMode.HALF_UP), + p1mcost.getCurrentCostPrice().setScale(2, RoundingMode.HALF_UP), "Unexpected current cost price"); + + //reverse freight invoice + Env.setContext(Env.getCtx(), Env.DATE, tomorrow); + info = MWorkflow.runDocumentActionWorkflow(freightInvoice, DocAction.ACTION_Reverse_Correct); + freightInvoice.load(getTrxName()); + assertFalse(info.isError(), info.getSummary()); + assertEquals(DocAction.STATUS_Reversed, freightInvoice.getDocStatus()); + assertTrue(freightInvoice.getReversal_ID() > 0, "Unexpected reversal id"); + MInvoice reversal = new MInvoice(Env.getCtx(), freightInvoice.getReversal_ID(), getTrxName()); + assertEquals(freightInvoice.getReversal_ID(), reversal.get_ID()); + if (!reversal.isPosted()) { + String msg = DocumentEngine.postImmediate(Env.getCtx(), getAD_Client_ID(), MInvoice.Table_ID, reversal.get_ID(), false, getTrxName()); + assertNull(msg, msg); + } + + query = MFactAcct.createRecordIdQuery(MInvoice.Table_ID, freightInvoice.get_ID(), as.get_ID(), getTrxName()); + factAccts = query.list(); + query = MFactAcct.createRecordIdQuery(MInvoice.Table_ID, reversal.get_ID(), as.get_ID(), getTrxName()); + List rFactAccts = query.list(); + expected = new ArrayList(); + for(MFactAcct factAcct : factAccts) { + MAccount acct = MAccount.get(factAcct, getTrxName()); + if (factAcct.getAmtAcctDr().signum() != 0) { + FactAcct fa = null; + for (FactAcct t : expected) { + if (t.account().getAccount_ID() == acct.getAccount_ID() && + t.account().getM_Product_ID() == acct.getM_Product_ID() && + t.debit() == false) { + fa = t; + break; + } + } + if (fa == null) { + expected.add(new FactAcct(acct, factAcct.getAmtAcctDr(), 1, false)); + } else { + expected.add(new FactAcct(acct, factAcct.getAmtAcctDr().add(fa.accountedAmount()), 1, false)); + expected.remove(fa); + } + } else if (factAcct.getAmtAcctCr().signum() != 0) { + FactAcct fa = null; + for (FactAcct t : expected) { + if (t.account().getAccount_ID() == acct.getAccount_ID() && + t.account().getM_Product_ID() == acct.getM_Product_ID() && + t.debit() == true) { + fa = t; + break; + } + } + if (fa == null) { + expected.add(new FactAcct(acct, factAcct.getAmtAcctCr(), 1, true)); + } else { + expected.add(new FactAcct(acct, factAcct.getAmtAcctCr().add(fa.accountedAmount()), 1, true)); + expected.remove(fa); + } + } + + //assert reversal allocation generate no entries + MAllocationHdr[] allocationHdrs = MAllocationHdr.getOfInvoice(Env.getCtx(), freightInvoice.getC_Invoice_ID(), getTrxName()); + assertEquals(1, allocationHdrs.length, "Unexpected number of allocations for freight invoice"); + if (!allocationHdrs[0].isPosted()) { + String msg = DocumentEngine.postImmediate(Env.getCtx(), getAD_Client_ID(), MAllocationHdr.Table_ID, allocationHdrs[0].get_ID(), false, getTrxName()); + assertNull(msg, msg); + } + assertTrue(allocationHdrs[0].isPosted(), "Allocation of freight invoice not posted"); + query = MFactAcct.createRecordIdQuery(MAllocationHdr.Table_ID, allocationHdrs[0].get_ID(), as.get_ID(), getTrxName()); + factAccts = query.list(); + assertEquals(0, factAccts.size(), "Unexpected number of fact entries generated by invoice reversal allocation"); + } + assertFactAcctEntries(rFactAccts, expected); + + MAcctSchema[] ass = MAcctSchema.getClientAcctSchema(Env.getCtx(), Env.getAD_Client_ID(Env.getCtx())); + Optional optional = Arrays.stream(ass).filter(e -> e.getC_AcctSchema_ID() != as.get_ID()).findFirst(); + if (optional.isPresent()) { + MAcctSchema as2 = optional.get(); + query = MFactAcct.createRecordIdQuery(MInvoice.Table_ID, freightInvoice.get_ID(), as2.get_ID(), getTrxName()); + factAccts = query.list(); + query = MFactAcct.createRecordIdQuery(MInvoice.Table_ID, freightInvoice.getReversal_ID(), as2.get_ID(), getTrxName()); + rFactAccts = query.list(); + expected = new ArrayList(); + for(MFactAcct factAcct : factAccts) { + MAccount acct = MAccount.get(factAcct, getTrxName()); + if (factAcct.getAmtAcctDr().signum() != 0) { + FactAcct fa = null; + for (FactAcct t : expected) { + if (t.account().getAccount_ID() == acct.getAccount_ID() && + t.account().getM_Product_ID() == acct.getM_Product_ID() && + t.debit() == false) { + fa = t; + break; + } + } + if (fa == null) { + expected.add(new FactAcct(acct, factAcct.getAmtAcctDr(), 1, false)); + } else { + expected.add(new FactAcct(acct, factAcct.getAmtAcctDr().add(fa.accountedAmount()), 1, false)); + expected.remove(fa); + } + } else if (factAcct.getAmtAcctCr().signum() != 0) { + FactAcct fa = null; + for (FactAcct t : expected) { + if (t.account().getAccount_ID() == acct.getAccount_ID() && + t.account().getM_Product_ID() == acct.getM_Product_ID() && + t.debit() == true) { + fa = t; + break; + } + } + if (fa == null) { + expected.add(new FactAcct(acct, factAcct.getAmtAcctCr(), 1, true)); + } else { + expected.add(new FactAcct(acct, factAcct.getAmtAcctCr().add(fa.accountedAmount()), 1, true)); + expected.remove(fa); + } + } + } + assertFactAcctEntries(rFactAccts, expected); + } + } finally { + rollback(); + + if (p1 != null) { + p1.set_TrxName(null); + p1.deleteEx(true); + } + + ConversionRateHelper.deleteConversionRate(cr1); + ConversionRateHelper.deleteConversionRate(cr2); + } + + } + + @Test + public void testUnplannedLandedCostReversalAfterInventoryUse() { + MClient client = MClient.get(Env.getCtx()); + MAcctSchema as = client.getAcctSchema(); + assertEquals(as.getCostingMethod(), MCostElement.COSTINGMETHOD_AveragePO, "Default costing method not Average PO"); + + MProduct p1 = null; + MProduct p2 = null; + MCurrency usd = MCurrency.get(DictionaryIDs.C_Currency.USD.id); + MCurrency euro = MCurrency.get(DictionaryIDs.C_Currency.EUR.id); + int C_ConversionType_ID = DictionaryIDs.C_ConversionType.SPOT.id; + Timestamp today = TimeUtil.getDay(null); + Timestamp tomorrow = TimeUtil.addDays(today, 1); + BigDecimal crate1 = new BigDecimal("1.05"); + BigDecimal crate2 = new BigDecimal("1.12"); + MConversionRate cr1 = ConversionRateHelper.createConversionRate(euro.getC_Currency_ID(), usd.getC_Currency_ID(), C_ConversionType_ID, today, crate1, true); + MConversionRate cr2 = ConversionRateHelper.createConversionRate(euro.getC_Currency_ID(), usd.getC_Currency_ID(), C_ConversionType_ID, tomorrow, crate2, true); + try { + p1 = new MProduct(Env.getCtx(), 0, null); + p1.setM_Product_Category_ID(DictionaryIDs.M_Product_Category.STANDARD.id); + p1.setName("testUnplannedLandedCostReversalAfterInventoryUse.1"); + p1.setProductType(MProduct.PRODUCTTYPE_Item); + p1.setIsStocked(true); + p1.setIsSold(true); + p1.setIsPurchased(true); + p1.setC_UOM_ID(DictionaryIDs.C_UOM.EACH.id); + p1.setC_TaxCategory_ID(DictionaryIDs.C_TaxCategory.STANDARD.id); + p1.saveEx(); + + MPriceListVersion plv = MPriceList.get(DictionaryIDs.M_PriceList.IMPORT.id).getPriceListVersion(null); + MProductPrice pp = new MProductPrice(Env.getCtx(), 0, getTrxName()); + pp.setM_PriceList_Version_ID(plv.getM_PriceList_Version_ID()); + pp.setM_Product_ID(p1.get_ID()); + BigDecimal p1price = new BigDecimal("30.00"); + pp.setPriceStd(p1price); + pp.setPriceList(p1price); + pp.saveEx(); + + p2 = new MProduct(Env.getCtx(), 0, null); + p2.setM_Product_Category_ID(DictionaryIDs.M_Product_Category.STANDARD.id); + p2.setName("testUnplannedLandedCostReversalAfterInventoryUse.2"); + p2.setProductType(MProduct.PRODUCTTYPE_Item); + p2.setIsStocked(true); + p2.setIsSold(true); + p2.setIsPurchased(true); + p2.setC_UOM_ID(DictionaryIDs.C_UOM.EACH.id); + p2.setC_TaxCategory_ID(DictionaryIDs.C_TaxCategory.STANDARD.id); + p2.saveEx(); + + pp = new MProductPrice(Env.getCtx(), 0, getTrxName()); + pp.setM_PriceList_Version_ID(plv.getM_PriceList_Version_ID()); + pp.setM_Product_ID(p2.get_ID()); + BigDecimal p2price = new BigDecimal("50.00"); + pp.setPriceStd(p2price); + pp.setPriceList(p2price); + pp.saveEx(); + + //create purchase order + MOrder purchaseOrder = new MOrder(Env.getCtx(), 0, getTrxName()); + purchaseOrder.setBPartner(MBPartner.get(Env.getCtx(), DictionaryIDs.C_BPartner.PATIO.id)); + purchaseOrder.setC_DocTypeTarget_ID(DictionaryIDs.C_DocType.PURCHASE_ORDER.id); + purchaseOrder.setIsSOTrx(false); + purchaseOrder.setSalesRep_ID(DictionaryIDs.AD_User.GARDEN_ADMIN.id); + purchaseOrder.setDocStatus(DocAction.STATUS_Drafted); + purchaseOrder.setDocAction(DocAction.ACTION_Complete); + purchaseOrder.setDateOrdered(today); + purchaseOrder.setDatePromised(today); + purchaseOrder.setM_PriceList_ID(plv.getM_PriceList_ID()); + purchaseOrder.saveEx(); + + MOrderLine poLine1 = new MOrderLine(purchaseOrder); + poLine1.setLine(10); + poLine1.setProduct(new MProduct(Env.getCtx(), p1.get_ID(), getTrxName())); + BigDecimal orderQty = new BigDecimal("100"); + poLine1.setQty(orderQty); + poLine1.setDatePromised(today); + poLine1.setPrice(p1price); + poLine1.saveEx(); + + MOrderLine poLine2 = new MOrderLine(purchaseOrder); + poLine2.setLine(10); + poLine2.setProduct(new MProduct(Env.getCtx(), p2.get_ID(), getTrxName())); + poLine2.setQty(orderQty); + poLine2.setDatePromised(today); + poLine2.setPrice(p2price); + poLine2.saveEx(); + + ProcessInfo info = MWorkflow.runDocumentActionWorkflow(purchaseOrder, DocAction.ACTION_Complete); + assertFalse(info.isError(), info.getSummary()); + purchaseOrder.load(getTrxName()); + assertEquals(DocAction.STATUS_Completed, purchaseOrder.getDocStatus()); + + //material receipt 1 + MInOut receipt1 = new MInOut(purchaseOrder, DictionaryIDs.C_DocType.MM_RECEIPT.id, purchaseOrder.getDateOrdered()); + receipt1.setDocStatus(DocAction.STATUS_Drafted); + receipt1.setDocAction(DocAction.ACTION_Complete); + receipt1.saveEx(); + + MInOutLine mr1Line1 = new MInOutLine(receipt1); + BigDecimal mr1l1Qty = new BigDecimal("25"); + mr1Line1.setOrderLine(poLine1, 0, mr1l1Qty); + mr1Line1.setQty(mr1l1Qty); + mr1Line1.saveEx(); + + MInOutLine mr1Line2 = new MInOutLine(receipt1); + BigDecimal mr1l2Qty = new BigDecimal("100"); + mr1Line2.setOrderLine(poLine2, 0, mr1l2Qty); + mr1Line2.setQty(mr1l2Qty); + mr1Line2.saveEx(); + + info = MWorkflow.runDocumentActionWorkflow(receipt1, DocAction.ACTION_Complete); + assertFalse(info.isError(), info.getSummary()); + receipt1.load(getTrxName()); + assertEquals(DocAction.STATUS_Completed, receipt1.getDocStatus()); + if (!receipt1.isPosted()) { + String error = DocumentEngine.postImmediate(Env.getCtx(), receipt1.getAD_Client_ID(), receipt1.get_Table_ID(), receipt1.get_ID(), false, getTrxName()); + assertNull(error, error); + } + + //material receipt 2 + MInOut receipt2 = new MInOut(purchaseOrder, DictionaryIDs.C_DocType.MM_RECEIPT.id, purchaseOrder.getDateOrdered()); + receipt2.setDocStatus(DocAction.STATUS_Drafted); + receipt2.setDocAction(DocAction.ACTION_Complete); + receipt2.saveEx(); + + MInOutLine mr2Line1 = new MInOutLine(receipt2); + BigDecimal mr2l1Qty = new BigDecimal("75"); + mr2Line1.setOrderLine(poLine1, 0, mr2l1Qty); + mr2Line1.setQty(mr2l1Qty); + mr2Line1.saveEx(); + + info = MWorkflow.runDocumentActionWorkflow(receipt2, DocAction.ACTION_Complete); + assertFalse(info.isError(), info.getSummary()); + receipt2.load(getTrxName()); + assertEquals(DocAction.STATUS_Completed, receipt2.getDocStatus()); + if (!receipt2.isPosted()) { + String error = DocumentEngine.postImmediate(Env.getCtx(), receipt2.getAD_Client_ID(), receipt2.get_Table_ID(), receipt2.get_ID(), false, getTrxName()); + assertNull(error, error); + } + + //assert p1 cost and posting + List cds = MCostDetail.list(Env.getCtx(), "C_OrderLine_ID=?", poLine1.getC_OrderLine_ID(), 0, as.get_ID(), getTrxName()); + assertTrue(cds.size() == 1, "MCostDetail not found for order line1"); + for(MCostDetail cd : cds) { + if (cd.getM_CostElement_ID() == 0) { + assertEquals(poLine1.getQtyOrdered().intValue(), cd.getQty().intValue(), "Unexpected MCostDetail Qty"); + assertEquals(p1price.multiply(orderQty).multiply(crate1).setScale(2, RoundingMode.HALF_UP), cd.getAmt().setScale(2, RoundingMode.HALF_UP), "Unexpected MCostDetail Amt"); + } + } + + //assert p1 mcost + p1.set_TrxName(getTrxName()); + MCost p1mcost = p1.getCostingRecord(as, getAD_Org_ID(), 0, as.getCostingMethod()); + assertNotNull(p1mcost, "No MCost record found"); + assertEquals(p1price.multiply(crate1).setScale(2, RoundingMode.HALF_UP), p1mcost.getCurrentCostPrice().setScale(2, RoundingMode.HALF_UP), "Unexpected current cost price"); + + ProductCost p1ProductCost = new ProductCost(Env.getCtx(), p1.get_ID(), 0, getTrxName()); + MAccount assetAccount = p1ProductCost.getAccount(ProductCost.ACCTTYPE_P_Asset, as); + MAccount varianceAccount = p1ProductCost.getAccount(ProductCost.ACCTTYPE_P_AverageCostVariance, as); + Doc doc = DocManager.getDocument(as, MInOut.Table_ID, receipt1.get_ID(), getTrxName()); + MAccount nivReceiptAccount = doc.getAccount(Doc.ACCTTYPE_NotInvoicedReceipts, as); + Query query = MFactAcct.createRecordIdQuery(MInOut.Table_ID, receipt1.get_ID(), as.get_ID(), getTrxName()); + List factAccts = query.list(); + List expected = Arrays.asList(new FactAcct(assetAccount, p1price.multiply(mr1l1Qty).multiply(crate1).setScale(2, RoundingMode.HALF_UP), p1price.multiply(mr1l1Qty), 2, true), + new FactAcct(nivReceiptAccount, p1price.multiply(mr1l1Qty).multiply(crate1).setScale(2, RoundingMode.HALF_UP), p1price.multiply(mr1l1Qty), 2, false)); + assertFactAcctEntries(factAccts, expected); + + //PO invoice + MInvoice purchaseInvoice = new MInvoice(purchaseOrder, DictionaryIDs.C_DocType.AP_INVOICE.id, purchaseOrder.getDateOrdered()); + purchaseInvoice.setDocStatus(DocAction.STATUS_Drafted); + purchaseInvoice.setDocAction(DocAction.ACTION_Complete); + purchaseInvoice.saveEx(); + + MInvoiceLine piLine1 = new MInvoiceLine(purchaseInvoice); + piLine1.setOrderLine(poLine1); + piLine1.setLine(10); + piLine1.setProduct(p1); + piLine1.setQty(poLine1.getQtyOrdered()); + piLine1.saveEx(); + + MInvoiceLine piLine2 = new MInvoiceLine(purchaseInvoice); + piLine2.setOrderLine(poLine2); + piLine2.setLine(20); + piLine2.setProduct(p2); + piLine2.setQty(poLine2.getQtyOrdered()); + piLine2.saveEx(); + + info = MWorkflow.runDocumentActionWorkflow(purchaseInvoice, DocAction.ACTION_Complete); + purchaseInvoice.load(getTrxName()); + assertFalse(info.isError(), info.getSummary()); + assertEquals(DocAction.STATUS_Completed, purchaseInvoice.getDocStatus()); + + if (!purchaseInvoice.isPosted()) { + String error = DocumentEngine.postImmediate(Env.getCtx(), purchaseInvoice.getAD_Client_ID(), MInvoice.Table_ID, purchaseInvoice.get_ID(), false, getTrxName()); + assertTrue(error == null); + } + purchaseInvoice.load(getTrxName()); + assertTrue(purchaseInvoice.isPosted()); + + Doc invoiceDoc = DocManager.getDocument(as, MInvoice.Table_ID, purchaseInvoice.get_ID(), getTrxName()); + MAccount liabilityAccount = invoiceDoc.getAccount(Doc.ACCTTYPE_V_Liability, as); + MAccount inventoryClearingAccount = p1ProductCost.getAccount(ProductCost.ACCTTYPE_P_InventoryClearing, as); + query = MFactAcct.createRecordIdQuery(MInvoice.Table_ID, purchaseInvoice.get_ID(), as.get_ID(), getTrxName()); + factAccts = query.list(); + expected = Arrays.asList(new FactAcct(inventoryClearingAccount, p1price.multiply(orderQty).multiply(crate1), p1price.multiply(orderQty), 2, true), + new FactAcct(liabilityAccount, p1price.multiply(orderQty).multiply(crate1).add(p2price.multiply(orderQty).multiply(crate1)), + p1price.multiply(orderQty).add(p2price.multiply(orderQty)), 2, false)); + assertFactAcctEntries(factAccts, expected); + + //inventory decrease + MInventory inventory = new MInventory(Env.getCtx(), 0, getTrxName()); + inventory.setC_DocType_ID(DictionaryIDs.C_DocType.INTERNAL_USE_INVENTORY.id); + inventory.setM_Warehouse_ID(DictionaryIDs.M_Warehouse.HQ.id); + inventory.setMovementDate(today); + inventory.saveEx(); + + MInventoryLine inventoryLine1 = new MInventoryLine(inventory, DictionaryIDs.M_Locator.HQ.id, p1.get_ID(), 0, null, null, new BigDecimal("25")); + inventoryLine1.setC_Charge_ID(DictionaryIDs.C_Charge.FREIGHT.id); + inventoryLine1.saveEx(); + MInventoryLine inventoryLine2 = new MInventoryLine(inventory, DictionaryIDs.M_Locator.HQ.id, p2.get_ID(), 0, null, null, new BigDecimal("100")); + inventoryLine2.setC_Charge_ID(DictionaryIDs.C_Charge.FREIGHT.id); + inventoryLine2.saveEx(); + + info = MWorkflow.runDocumentActionWorkflow(inventory, DocAction.ACTION_Complete); + assertFalse(info.isError(), info.getSummary()); + inventory.load(getTrxName()); + assertEquals(DocAction.STATUS_Completed, inventory.getDocStatus()); + if (!inventory.isPosted()) { + String error = DocumentEngine.postImmediate(Env.getCtx(), inventory.getAD_Client_ID(), inventory.get_Table_ID(), inventory.get_ID(), false, getTrxName()); + assertNull(error, error); + } + + //landed cost invoice + MBPartner freightBP = MBPartner.get(Env.getCtx(), DictionaryIDs.C_BPartner.PATIO.id); + MInvoice freightInvoice = new MInvoice(Env.getCtx(), 0, getTrxName()); + freightInvoice.setC_DocTypeTarget_ID(MDocType.DOCBASETYPE_APInvoice); + freightInvoice.setBPartner(freightBP); + freightInvoice.setDocStatus(DocAction.STATUS_Drafted); + freightInvoice.setDocAction(DocAction.ACTION_Complete); + freightInvoice.setM_PriceList_ID(DictionaryIDs.M_PriceList.STANDARD.id); + freightInvoice.saveEx(); + + MInvoiceLine fiLine = new MInvoiceLine(freightInvoice); + fiLine.setLine(10); + fiLine.setC_Charge_ID(DictionaryIDs.C_Charge.FREIGHT.id); + fiLine.setQty(BigDecimal.ONE); + BigDecimal freightPrice = new BigDecimal("1000.00"); + fiLine.setPrice(freightPrice); + fiLine.setC_UOM_ID(DictionaryIDs.C_UOM.EACH.id); + fiLine.saveEx(); + + MLandedCost landedCost = new MLandedCost(Env.getCtx(), 0, getTrxName()); + landedCost.setC_InvoiceLine_ID(fiLine.get_ID()); + landedCost.setM_CostElement_ID(DictionaryIDs.M_CostElement.FREIGHT.id); + landedCost.setM_InOut_ID(receipt1.get_ID()); + landedCost.setM_InOutLine_ID(mr1Line1.get_ID()); + landedCost.setLandedCostDistribution(MLandedCost.LANDEDCOSTDISTRIBUTION_Costs); + landedCost.saveEx(); + + landedCost = new MLandedCost(Env.getCtx(), 0, getTrxName()); + landedCost.setC_InvoiceLine_ID(fiLine.get_ID()); + landedCost.setM_CostElement_ID(DictionaryIDs.M_CostElement.FREIGHT.id); + landedCost.setM_InOut_ID(receipt1.get_ID()); + landedCost.setM_InOutLine_ID(mr1Line2.get_ID()); + landedCost.setLandedCostDistribution(MLandedCost.LANDEDCOSTDISTRIBUTION_Costs); + landedCost.saveEx(); + + landedCost = new MLandedCost(Env.getCtx(), 0, getTrxName()); + landedCost.setC_InvoiceLine_ID(fiLine.get_ID()); + landedCost.setM_CostElement_ID(DictionaryIDs.M_CostElement.FREIGHT.id); + landedCost.setM_InOut_ID(receipt1.get_ID()); + landedCost.setM_InOutLine_ID(mr2Line1.get_ID()); + landedCost.setLandedCostDistribution(MLandedCost.LANDEDCOSTDISTRIBUTION_Costs); + landedCost.saveEx(); + + String error = landedCost.allocateCosts(); + assertTrue(Util.isEmpty(error, true), error); + + BigDecimal totalBase = purchaseOrder.getGrandTotal(); + BigDecimal p1a1 = p1price.multiply(mr1l1Qty).multiply(fiLine.getLineNetAmt()).divide(totalBase, 6, RoundingMode.HALF_UP); + BigDecimal p1a2 = p1price.multiply(mr2l1Qty).multiply(fiLine.getLineNetAmt()).divide(totalBase, 6, RoundingMode.HALF_UP); + BigDecimal p2a1 = p2price.multiply(mr1l2Qty).multiply(fiLine.getLineNetAmt()).divide(totalBase, 6, RoundingMode.HALF_UP); + + MLandedCostAllocation[] allocations = MLandedCostAllocation.getOfInvoiceLine(Env.getCtx(), fiLine.get_ID(), getTrxName()); + assertEquals(3, allocations.length, "Unexpected number of landed cost allocation line"); + for (MLandedCostAllocation allocation : allocations) { + if (allocation.getM_Product_ID() == p1.get_ID() && allocation.getQty().intValue() == mr1l1Qty.intValue()) { + assertEquals(p1a1.setScale(2, RoundingMode.HALF_UP), allocation.getAmt().setScale(2, RoundingMode.HALF_UP), "Unexpected landed cost allocation amount"); + } else if (allocation.getM_Product_ID() == p1.get_ID() && allocation.getQty().intValue() == mr2l1Qty.intValue()) { + assertEquals(p1a2.setScale(2, RoundingMode.HALF_UP), allocation.getAmt().setScale(2, RoundingMode.HALF_UP), "Unexpected landed cost allocation amount"); + } else if (allocation.getM_Product_ID() == p2.get_ID() && allocation.getQty().intValue() == mr1l2Qty.intValue()) { + assertEquals(p2a1.setScale(2, RoundingMode.HALF_UP), allocation.getAmt().setScale(2, RoundingMode.HALF_UP), "Unexpected landed cost allocation amount"); + } else { + fail("Unknown landed cost allocation line: " + allocation); + } + } + + info = MWorkflow.runDocumentActionWorkflow(freightInvoice, DocAction.ACTION_Complete); + freightInvoice.load(getTrxName()); + assertFalse(info.isError(), info.getSummary()); + assertEquals(DocAction.STATUS_Completed, freightInvoice.getDocStatus()); + + if (!freightInvoice.isPosted()) { + error = DocumentEngine.postImmediate(Env.getCtx(), freightInvoice.getAD_Client_ID(), MInvoice.Table_ID, freightInvoice.get_ID(), false, getTrxName()); + assertTrue(error == null, error); + } + freightInvoice.load(getTrxName()); + assertTrue(freightInvoice.isPosted()); + + //assert freight invoice posting + doc = DocManager.getDocument(as, MInvoice.Table_ID, freightInvoice.get_ID(), getTrxName()); + MAccount apAccount = doc.getAccount(Doc.ACCTTYPE_V_Liability, as); + query = MFactAcct.createRecordIdQuery(MInvoice.Table_ID, freightInvoice.get_ID(), as.get_ID(), getTrxName()); + factAccts = query.list(); + BigDecimal p1OnHand = orderQty.add(inventoryLine1.getMovementQty()); + BigDecimal p1a1assetAmt = p1a1; + BigDecimal p1a2assetAmt = p1a2.divide(mr2l1Qty, RoundingMode.HALF_UP).multiply(p1OnHand.subtract(mr1l1Qty)); + BigDecimal p1a2varianceAmt = p1a2.subtract(p1a2assetAmt); + BigDecimal p2varianceAmt = p2a1; + expected = Arrays.asList(new FactAcct(varianceAccount, p1a2varianceAmt, p1a2varianceAmt, 2, true), + new FactAcct(assetAccount, p1a1assetAmt, p1a1assetAmt, 2, true), + new FactAcct(assetAccount, p1a2assetAmt, p1a2assetAmt, 2, true), + new FactAcct(varianceAccount, p2varianceAmt, p2varianceAmt, 2, true), + new FactAcct(varianceAccount, p1a2varianceAmt, p1a2varianceAmt, 2, true), + new FactAcct(apAccount, freightInvoice.getGrandTotal(), freightInvoice.getGrandTotal(), 2, false)); + assertFactAcctEntries(factAccts, expected); + + BigDecimal p1assetAmt = p1a1assetAmt.add(p1a2assetAmt); + p1mcost = p1.getCostingRecord(as, getAD_Org_ID(), 0, as.getCostingMethod()); + assertEquals(p1price.multiply(crate1).add(p1assetAmt.divide(p1OnHand, RoundingMode.HALF_UP)).setScale(2, RoundingMode.HALF_UP), + p1mcost.getCurrentCostPrice().setScale(2, RoundingMode.HALF_UP), "Unexpected current cost price"); + + //reverse freight invoice + Env.setContext(Env.getCtx(), Env.DATE, tomorrow); + info = MWorkflow.runDocumentActionWorkflow(freightInvoice, DocAction.ACTION_Reverse_Correct); + freightInvoice.load(getTrxName()); + assertFalse(info.isError(), info.getSummary()); + assertEquals(DocAction.STATUS_Reversed, freightInvoice.getDocStatus()); + assertTrue(freightInvoice.getReversal_ID() > 0, "Unexpected reversal id"); + MInvoice reversal = new MInvoice(Env.getCtx(), freightInvoice.getReversal_ID(), getTrxName()); + assertEquals(freightInvoice.getReversal_ID(), reversal.get_ID()); + if (!reversal.isPosted()) { + String msg = DocumentEngine.postImmediate(Env.getCtx(), getAD_Client_ID(), MInvoice.Table_ID, reversal.get_ID(), false, getTrxName()); + assertNull(msg, msg); + } + + query = MFactAcct.createRecordIdQuery(MInvoice.Table_ID, freightInvoice.get_ID(), as.get_ID(), getTrxName()); + factAccts = query.list(); + query = MFactAcct.createRecordIdQuery(MInvoice.Table_ID, reversal.get_ID(), as.get_ID(), getTrxName()); + List rFactAccts = query.list(); + expected = new ArrayList(); + for(MFactAcct factAcct : factAccts) { + MAccount acct = MAccount.get(factAcct, getTrxName()); + if (factAcct.getAmtAcctDr().signum() != 0) { + FactAcct fa = null; + for (FactAcct t : expected) { + if (t.account().getAccount_ID() == acct.getAccount_ID() && + t.account().getM_Product_ID() == acct.getM_Product_ID() && + t.debit() == false) { + fa = t; + break; + } + } + if (fa == null) { + expected.add(new FactAcct(acct, factAcct.getAmtAcctDr(), 1, false)); + } else { + expected.add(new FactAcct(acct, factAcct.getAmtAcctDr().add(fa.accountedAmount()), 1, false)); + expected.remove(fa); + } + } else if (factAcct.getAmtAcctCr().signum() != 0) { + FactAcct fa = null; + for (FactAcct t : expected) { + if (t.account().getAccount_ID() == acct.getAccount_ID() && + t.account().getM_Product_ID() == acct.getM_Product_ID() && + t.debit() == true) { + fa = t; + break; + } + } + if (fa == null) { + expected.add(new FactAcct(acct, factAcct.getAmtAcctCr(), 1, true)); + } else { + expected.add(new FactAcct(acct, factAcct.getAmtAcctCr().add(fa.accountedAmount()), 1, true)); + expected.remove(fa); + } + } + + //assert reversal allocation generate no entries + MAllocationHdr[] allocationHdrs = MAllocationHdr.getOfInvoice(Env.getCtx(), freightInvoice.getC_Invoice_ID(), getTrxName()); + assertEquals(1, allocationHdrs.length, "Unexpected number of allocations for freight invoice"); + if (!allocationHdrs[0].isPosted()) { + String msg = DocumentEngine.postImmediate(Env.getCtx(), getAD_Client_ID(), MAllocationHdr.Table_ID, allocationHdrs[0].get_ID(), false, getTrxName()); + assertNull(msg, msg); + } + assertTrue(allocationHdrs[0].isPosted(), "Allocation of freight invoice not posted"); + query = MFactAcct.createRecordIdQuery(MAllocationHdr.Table_ID, allocationHdrs[0].get_ID(), as.get_ID(), getTrxName()); + factAccts = query.list(); + assertEquals(0, factAccts.size(), "Unexpected number of fact entries generated by invoice reversal allocation"); + } + assertFactAcctEntries(rFactAccts, expected); + + MAcctSchema[] ass = MAcctSchema.getClientAcctSchema(Env.getCtx(), Env.getAD_Client_ID(Env.getCtx())); + Optional optional = Arrays.stream(ass).filter(e -> e.getC_AcctSchema_ID() != as.get_ID()).findFirst(); + if (optional.isPresent()) { + MAcctSchema as2 = optional.get(); + query = MFactAcct.createRecordIdQuery(MInvoice.Table_ID, freightInvoice.get_ID(), as2.get_ID(), getTrxName()); + factAccts = query.list(); + query = MFactAcct.createRecordIdQuery(MInvoice.Table_ID, freightInvoice.getReversal_ID(), as2.get_ID(), getTrxName()); + rFactAccts = query.list(); + expected = new ArrayList(); + for(MFactAcct factAcct : factAccts) { + MAccount acct = MAccount.get(factAcct, getTrxName()); + if (factAcct.getAmtAcctDr().signum() != 0) { + FactAcct fa = null; + for (FactAcct t : expected) { + if (t.account().getAccount_ID() == acct.getAccount_ID() && + t.account().getM_Product_ID() == acct.getM_Product_ID() && + t.debit() == false) { + fa = t; + break; + } + } + if (fa == null) { + expected.add(new FactAcct(acct, factAcct.getAmtAcctDr(), 1, false)); + } else { + expected.add(new FactAcct(acct, factAcct.getAmtAcctDr().add(fa.accountedAmount()), 1, false)); + expected.remove(fa); + } + } else if (factAcct.getAmtAcctCr().signum() != 0) { + FactAcct fa = null; + for (FactAcct t : expected) { + if (t.account().getAccount_ID() == acct.getAccount_ID() && + t.account().getM_Product_ID() == acct.getM_Product_ID() && + t.debit() == true) { + fa = t; + break; + } + } + if (fa == null) { + expected.add(new FactAcct(acct, factAcct.getAmtAcctCr(), 1, true)); + } else { + expected.add(new FactAcct(acct, factAcct.getAmtAcctCr().add(fa.accountedAmount()), 1, true)); + expected.remove(fa); + } + } + } + assertFactAcctEntries(rFactAccts, expected); + } + } finally { + rollback(); + + if (p1 != null) { + p1.set_TrxName(null); + p1.deleteEx(true); + } + + ConversionRateHelper.deleteConversionRate(cr1); + ConversionRateHelper.deleteConversionRate(cr2); + } + } + + @Test + public void testUnplannedLandedCostReversalWithZeroOnHand() { + + MClient client = MClient.get(Env.getCtx()); + MAcctSchema as = client.getAcctSchema(); + assertEquals(as.getCostingMethod(), MCostElement.COSTINGMETHOD_AveragePO, "Default costing method not Average PO"); + + MProduct p1 = null; + try { + p1 = new MProduct(Env.getCtx(), 0, null); + p1.setM_Product_Category_ID(DictionaryIDs.M_Product_Category.STANDARD.id); + p1.setName("testUnplannedLandedCostReversalWithZeroOnHand"); + p1.setProductType(MProduct.PRODUCTTYPE_Item); + p1.setIsStocked(true); + p1.setIsSold(true); + p1.setIsPurchased(true); + p1.setC_UOM_ID(DictionaryIDs.C_UOM.EACH.id); + p1.setC_TaxCategory_ID(DictionaryIDs.C_TaxCategory.STANDARD.id); + p1.saveEx(); + + MPriceListVersion plv = MPriceList.get(DictionaryIDs.M_PriceList.PURCHASE.id).getPriceListVersion(null); + MProductPrice pp = new MProductPrice(Env.getCtx(), 0, getTrxName()); + pp.setM_PriceList_Version_ID(plv.getM_PriceList_Version_ID()); + pp.setM_Product_ID(p1.get_ID()); + BigDecimal p1price = new BigDecimal("30.00"); + pp.setPriceStd(p1price); + pp.setPriceList(p1price); + pp.saveEx(); + + //create purchase order + MOrder purchaseOrder = new MOrder(Env.getCtx(), 0, getTrxName()); + purchaseOrder.setBPartner(MBPartner.get(Env.getCtx(), DictionaryIDs.C_BPartner.PATIO.id)); + purchaseOrder.setC_DocTypeTarget_ID(DictionaryIDs.C_DocType.PURCHASE_ORDER.id); + purchaseOrder.setIsSOTrx(false); + purchaseOrder.setSalesRep_ID(DictionaryIDs.AD_User.GARDEN_ADMIN.id); + purchaseOrder.setDocStatus(DocAction.STATUS_Drafted); + purchaseOrder.setDocAction(DocAction.ACTION_Complete); + Timestamp today = TimeUtil.getDay(System.currentTimeMillis()); + purchaseOrder.setDateOrdered(today); + purchaseOrder.setDatePromised(today); + purchaseOrder.saveEx(); + + MOrderLine poLine1 = new MOrderLine(purchaseOrder); + poLine1.setLine(10); + poLine1.setProduct(new MProduct(Env.getCtx(), p1.get_ID(), getTrxName())); + BigDecimal orderQty = new BigDecimal("10"); + poLine1.setQty(orderQty); + poLine1.setDatePromised(today); + poLine1.setPrice(p1price); + poLine1.saveEx(); + + ProcessInfo info = MWorkflow.runDocumentActionWorkflow(purchaseOrder, DocAction.ACTION_Complete); + assertFalse(info.isError(), info.getSummary()); + purchaseOrder.load(getTrxName()); + assertEquals(DocAction.STATUS_Completed, purchaseOrder.getDocStatus()); + + //mr1 for 10 each + MInOut receipt1 = new MInOut(purchaseOrder, DictionaryIDs.C_DocType.MM_RECEIPT.id, purchaseOrder.getDateOrdered()); + receipt1.setDocStatus(DocAction.STATUS_Drafted); + receipt1.setDocAction(DocAction.ACTION_Complete); + receipt1.saveEx(); + + MInOutLine receipt1Line1 = new MInOutLine(receipt1); + BigDecimal mr1Qty = new BigDecimal("10"); + receipt1Line1.setOrderLine(poLine1, 0, mr1Qty); + receipt1Line1.setQty(mr1Qty); + receipt1Line1.saveEx(); + + info = MWorkflow.runDocumentActionWorkflow(receipt1, DocAction.ACTION_Complete); + assertFalse(info.isError(), info.getSummary()); + receipt1.load(getTrxName()); + assertEquals(DocAction.STATUS_Completed, receipt1.getDocStatus()); + if (!receipt1.isPosted()) { + String error = DocumentEngine.postImmediate(Env.getCtx(), receipt1.getAD_Client_ID(), receipt1.get_Table_ID(), receipt1.get_ID(), false, getTrxName()); + assertNull(error, error); + } + + //assert p1 cost and posting + List cds = MCostDetail.list(Env.getCtx(), "C_OrderLine_ID=?", poLine1.getC_OrderLine_ID(), 0, as.get_ID(), getTrxName()); + assertTrue(cds.size() == 1, "MCostDetail not found for order line1"); + for(MCostDetail cd : cds) { + if (cd.getM_CostElement_ID() == 0) { + assertEquals(10, cd.getQty().intValue(), "Unexpected MCostDetail Qty"); + assertEquals(p1price.multiply(mr1Qty).setScale(2, RoundingMode.HALF_UP), cd.getAmt().setScale(2, RoundingMode.HALF_UP), "Unexpected MCostDetail Amt"); + } + } + + p1.set_TrxName(getTrxName()); + MCost p1mcost = p1.getCostingRecord(as, getAD_Org_ID(), 0, as.getCostingMethod()); + assertNotNull(p1mcost, "No MCost record found"); + assertEquals(p1price, p1mcost.getCurrentCostPrice().setScale(2, RoundingMode.HALF_UP), "Unexpected current cost price"); + + ProductCost p1ProductCost = new ProductCost(Env.getCtx(), p1.get_ID(), 0, getTrxName()); + MAccount assetAccount = p1ProductCost.getAccount(ProductCost.ACCTTYPE_P_Asset, as); + MAccount varianceAccount = p1ProductCost.getAccount(ProductCost.ACCTTYPE_P_AverageCostVariance, as); + Doc doc = DocManager.getDocument(as, MInOut.Table_ID, receipt1.get_ID(), getTrxName()); + MAccount nivReceiptAccount = doc.getAccount(Doc.ACCTTYPE_NotInvoicedReceipts, as); + Query query = MFactAcct.createRecordIdQuery(MInOut.Table_ID, receipt1.get_ID(), as.get_ID(), getTrxName()); + List factAccts = query.list(); + List expected = Arrays.asList(new FactAcct(assetAccount, p1price.multiply(mr1Qty), 2, true), + new FactAcct(nivReceiptAccount, p1price.multiply(mr1Qty), 2, false)); + assertFactAcctEntries(factAccts, expected); + + //full po invoice + MInvoice purchaseInvoice = new MInvoice(purchaseOrder, DictionaryIDs.C_DocType.AP_INVOICE.id, purchaseOrder.getDateOrdered()); + purchaseInvoice.setDocStatus(DocAction.STATUS_Drafted); + purchaseInvoice.setDocAction(DocAction.ACTION_Complete); + purchaseInvoice.saveEx(); + + MInvoiceLine piLine1 = new MInvoiceLine(purchaseInvoice); + piLine1.setOrderLine(poLine1); + piLine1.setLine(10); + piLine1.setProduct(p1); + piLine1.setQty(poLine1.getQtyOrdered()); + piLine1.saveEx(); + + info = MWorkflow.runDocumentActionWorkflow(purchaseInvoice, DocAction.ACTION_Complete); + purchaseInvoice.load(getTrxName()); + assertFalse(info.isError(), info.getSummary()); + assertEquals(DocAction.STATUS_Completed, purchaseInvoice.getDocStatus()); + + if (!purchaseInvoice.isPosted()) { + String error = DocumentEngine.postImmediate(Env.getCtx(), purchaseInvoice.getAD_Client_ID(), MInvoice.Table_ID, purchaseInvoice.get_ID(), false, getTrxName()); + assertTrue(error == null); + } + purchaseInvoice.load(getTrxName()); + assertTrue(purchaseInvoice.isPosted()); + + Doc invoiceDoc = DocManager.getDocument(as, MInvoice.Table_ID, purchaseInvoice.get_ID(), getTrxName()); + MAccount liabilityAccount = invoiceDoc.getAccount(Doc.ACCTTYPE_V_Liability, as); + MAccount inventoryClearingAccount = p1ProductCost.getAccount(ProductCost.ACCTTYPE_P_InventoryClearing, as); + query = MFactAcct.createRecordIdQuery(MInvoice.Table_ID, purchaseInvoice.get_ID(), as.get_ID(), getTrxName()); + factAccts = query.list(); + expected = Arrays.asList(new FactAcct(inventoryClearingAccount, p1price.multiply(orderQty), 2, true), + new FactAcct(liabilityAccount, p1price.multiply(orderQty), 2, false)); + assertFactAcctEntries(factAccts, expected); + + //so and shipment + MBPartner customer = MBPartner.get(Env.getCtx(), DictionaryIDs.C_BPartner.JOE_BLOCK.id); + MOrder salesOrder = new MOrder(Env.getCtx(), 0, getTrxName()); + salesOrder.setC_DocTypeTarget_ID(MOrder.DocSubTypeSO_Standard); + salesOrder.setBPartner(customer); + salesOrder.setDeliveryRule(MOrder.DELIVERYRULE_CompleteOrder); + salesOrder.setDocStatus(DocAction.STATUS_Drafted); + salesOrder.setDocAction(DocAction.ACTION_Complete); + salesOrder.setDatePromised(today); + salesOrder.saveEx(); + + MOrderLine soLine1 = new MOrderLine(salesOrder); + soLine1.setLine(10); + soLine1.setProduct(p1); + BigDecimal p1ShipQty = new BigDecimal("10"); + soLine1.setQty(p1ShipQty); + soLine1.setDatePromised(today); + soLine1.setPrice(new BigDecimal("50")); + soLine1.saveEx(); + + info = MWorkflow.runDocumentActionWorkflow(salesOrder, DocAction.ACTION_Complete); + salesOrder.load(getTrxName()); + assertFalse(info.isError(), info.getSummary()); + assertEquals(DocAction.STATUS_Completed, salesOrder.getDocStatus()); + + MInOut shipment = new MInOut(salesOrder, DictionaryIDs.C_DocType.MM_SHIPMENT.id, salesOrder.getDateOrdered()); + shipment.setDocStatus(DocAction.STATUS_Drafted); + shipment.setDocAction(DocAction.ACTION_Complete); + shipment.saveEx(); + + MInOutLine shipmentLine1 = new MInOutLine(shipment); + shipmentLine1.setOrderLine(soLine1, 0, soLine1.getQtyOrdered()); + shipmentLine1.setQty(soLine1.getQtyOrdered()); + shipmentLine1.saveEx(); + + info = MWorkflow.runDocumentActionWorkflow(shipment, DocAction.ACTION_Complete); + assertFalse(info.isError(), info.getSummary()); + shipment.load(getTrxName()); + assertEquals(DocAction.STATUS_Completed, shipment.getDocStatus()); + + //landed cost invoice + MBPartner freightBP = MBPartner.get(Env.getCtx(), DictionaryIDs.C_BPartner.PATIO.id); + MInvoice freightInvoice = new MInvoice(Env.getCtx(), 0, getTrxName()); + freightInvoice.setC_DocTypeTarget_ID(MDocType.DOCBASETYPE_APInvoice); + freightInvoice.setBPartner(freightBP); + freightInvoice.setDocStatus(DocAction.STATUS_Drafted); + freightInvoice.setDocAction(DocAction.ACTION_Complete); + freightInvoice.saveEx(); + + MInvoiceLine fiLine = new MInvoiceLine(freightInvoice); + fiLine.setLine(10); + fiLine.setC_Charge_ID(DictionaryIDs.C_Charge.FREIGHT.id); + fiLine.setQty(BigDecimal.ONE); + BigDecimal freightPrice = new BigDecimal("200.00"); + fiLine.setPrice(freightPrice); + fiLine.setC_UOM_ID(DictionaryIDs.C_UOM.EACH.id); + fiLine.saveEx(); + + MLandedCost landedCost = new MLandedCost(Env.getCtx(), 0, getTrxName()); + landedCost.setC_InvoiceLine_ID(fiLine.get_ID()); + landedCost.setM_CostElement_ID(DictionaryIDs.M_CostElement.FREIGHT.id); + landedCost.setM_InOut_ID(receipt1.get_ID()); + landedCost.setM_InOutLine_ID(receipt1Line1.get_ID()); + landedCost.setLandedCostDistribution(MLandedCost.LANDEDCOSTDISTRIBUTION_Costs); + landedCost.saveEx(); + + String error = landedCost.allocateCosts(); + assertTrue(Util.isEmpty(error, true), error); + + BigDecimal totalBase = purchaseInvoice.getGrandTotal(); + BigDecimal p1a1 = p1price.multiply(mr1Qty).multiply(fiLine.getLineNetAmt()).divide(totalBase, 6, RoundingMode.HALF_UP); + + MLandedCostAllocation[] allocations = MLandedCostAllocation.getOfInvoiceLine(Env.getCtx(), fiLine.get_ID(), getTrxName()); + assertEquals(1, allocations.length, "Unexpected number of landed cost allocation line"); + for (MLandedCostAllocation allocation : allocations) { + if (allocation.getM_Product_ID() == p1.get_ID() && allocation.getQty().intValue() == 10) { + assertEquals(p1a1.setScale(2, RoundingMode.HALF_UP), allocation.getAmt().setScale(2, RoundingMode.HALF_UP), "Unexpected landed cost allocation amount"); + } else { + fail("Unknown landed cost allocation line: " + allocation); + } + } + + info = MWorkflow.runDocumentActionWorkflow(freightInvoice, DocAction.ACTION_Complete); + freightInvoice.load(getTrxName()); + assertFalse(info.isError(), info.getSummary()); + assertEquals(DocAction.STATUS_Completed, freightInvoice.getDocStatus()); + + if (!freightInvoice.isPosted()) { + error = DocumentEngine.postImmediate(Env.getCtx(), freightInvoice.getAD_Client_ID(), MInvoice.Table_ID, freightInvoice.get_ID(), false, getTrxName()); + assertTrue(error == null, error); + } + freightInvoice.load(getTrxName()); + assertTrue(freightInvoice.isPosted()); + + //assert freight invoice posting + doc = DocManager.getDocument(as, MInvoice.Table_ID, freightInvoice.get_ID(), getTrxName()); + MAccount apAccount = doc.getAccount(Doc.ACCTTYPE_V_Liability, as); + query = MFactAcct.createRecordIdQuery(MInvoice.Table_ID, freightInvoice.get_ID(), as.get_ID(), getTrxName()); + factAccts = query.list(); + expected = Arrays.asList(new FactAcct(varianceAccount, p1a1, 2, true), + new FactAcct(apAccount, freightInvoice.getGrandTotal(), 2, false)); + assertFactAcctEntries(factAccts, expected); + + //reverse freight invoice + info = MWorkflow.runDocumentActionWorkflow(freightInvoice, DocAction.ACTION_Reverse_Correct); + freightInvoice.load(getTrxName()); + assertFalse(info.isError(), info.getSummary()); + assertEquals(DocAction.STATUS_Reversed, freightInvoice.getDocStatus()); + assertTrue(freightInvoice.getReversal_ID() > 0, "Unexpected reversal id"); + MInvoice reversal = new MInvoice(Env.getCtx(), freightInvoice.getReversal_ID(), getTrxName()); + assertEquals(freightInvoice.getReversal_ID(), reversal.get_ID()); + if (!reversal.isPosted()) { + String msg = DocumentEngine.postImmediate(Env.getCtx(), getAD_Client_ID(), MInvoice.Table_ID, reversal.get_ID(), false, getTrxName()); + assertNull(msg, msg); + } + + //assert reversal invoice posting + query = MFactAcct.createRecordIdQuery(MInvoice.Table_ID, freightInvoice.get_ID(), as.get_ID(), getTrxName()); + factAccts = query.list(); + query = MFactAcct.createRecordIdQuery(MInvoice.Table_ID, freightInvoice.getReversal_ID(), as.get_ID(), getTrxName()); + List rFactAccts = query.list(); + expected = new ArrayList(); + for(MFactAcct factAcct : factAccts) { + MAccount acct = MAccount.get(factAcct, getTrxName()); + if (factAcct.getAmtAcctDr().signum() != 0) { + expected.add(new FactAcct(acct, factAcct.getAmtAcctDr(), 2, false)); + } else if (factAcct.getAmtAcctCr().signum() != 0) { + expected.add(new FactAcct(acct, factAcct.getAmtAcctCr(), 2, true)); + } + } + assertFactAcctEntries(rFactAccts, expected); + + MAcctSchema[] ass = MAcctSchema.getClientAcctSchema(Env.getCtx(), Env.getAD_Client_ID(Env.getCtx())); + Optional optional = Arrays.stream(ass).filter(e -> e.getC_AcctSchema_ID() != as.get_ID()).findFirst(); + if (optional.isPresent()) { + MAcctSchema as2 = optional.get(); + query = MFactAcct.createRecordIdQuery(MInvoice.Table_ID, freightInvoice.get_ID(), as2.get_ID(), getTrxName()); + factAccts = query.list(); + query = MFactAcct.createRecordIdQuery(MInvoice.Table_ID, freightInvoice.getReversal_ID(), as2.get_ID(), getTrxName()); + rFactAccts = query.list(); + expected = new ArrayList(); + for(MFactAcct factAcct : factAccts) { + MAccount acct = MAccount.get(factAcct, getTrxName()); + if (factAcct.getAmtAcctDr().signum() != 0) { + expected.add(new FactAcct(acct, factAcct.getAmtAcctDr(), 2, false)); + } else if (factAcct.getAmtAcctCr().signum() != 0) { + expected.add(new FactAcct(acct, factAcct.getAmtAcctCr(), 2, true)); + } + } + assertFactAcctEntries(rFactAccts, expected); + } + + } finally { + rollback(); + + if (p1 != null) { + p1.set_TrxName(null); + p1.deleteEx(true); + } + } + } + private MInOutLine createPOAndMRForProduct(int productId, MAttributeSetInstance asi, BigDecimal price) { MOrder order = new MOrder(Env.getCtx(), 0, getTrxName()); order.setBPartner(MBPartner.get(Env.getCtx(), DictionaryIDs.C_BPartner.PATIO.id)); diff --git a/org.idempiere.test/src/org/idempiere/test/costing/NonStockedExpTypeAvgPOCostingTest.java b/org.idempiere.test/src/org/idempiere/test/costing/NonStockedExpTypeAvgPOCostingTest.java index 6ffa53b4a9..000d158ed2 100644 --- a/org.idempiere.test/src/org/idempiere/test/costing/NonStockedExpTypeAvgPOCostingTest.java +++ b/org.idempiere.test/src/org/idempiere/test/costing/NonStockedExpTypeAvgPOCostingTest.java @@ -21,6 +21,7 @@ import static org.junit.jupiter.api.Assertions.fail; import java.math.BigDecimal; import java.math.RoundingMode; import java.sql.Timestamp; +import java.util.Arrays; import java.util.List; import org.compiere.acct.Doc; @@ -67,6 +68,7 @@ import org.eevolution.model.MPPProductBOM; import org.eevolution.model.MPPProductBOMLine; import org.idempiere.test.AbstractTestCase; import org.idempiere.test.DictionaryIDs; +import org.idempiere.test.FactAcct; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.parallel.Isolated; @@ -133,38 +135,11 @@ public class NonStockedExpTypeAvgPOCostingTest extends AbstractTestCase ProductCost pc = new ProductCost(Env.getCtx(), rLine.getM_Product_ID(), 0, getTrxName()); MAccount productExpense = pc.getAccount(ProductCost.ACCTTYPE_P_Expense, as); - StringBuilder whereClause = new StringBuilder(); - whereClause - .append(MFactAcct.COLUMNNAME_AD_Table_ID) - .append("=") - .append(MInOut.Table_ID) - .append(" AND ") - .append(MFactAcct.COLUMNNAME_Record_ID) - .append("=") - .append(rLine.getM_InOut_ID()) - .append(" AND ") - .append(MFactAcct.COLUMNNAME_C_AcctSchema_ID) - .append("=") - .append(as.getC_AcctSchema_ID()); - - int[] ids = MFactAcct.getAllIDs(MFactAcct.Table_Name, whereClause.toString(), getTrxName()); - - for (int id : ids) - { - // Test Accounting for MR - MFactAcct fa = new MFactAcct(Env.getCtx(), id, getTrxName()); - if (fa.getAccount_ID() == acctNIR.getAccount_ID()) - { - assertEquals(BD_20, fa.getAmtAcctCr().setScale(2, RoundingMode.HALF_UP), ""); - assertEquals(new BigDecimal("-10.00"), fa.getQty().setScale(2, RoundingMode.HALF_UP), ""); - } - else if (fa.getAccount_ID() == productExpense.getAccount_ID()) - { - assertEquals(BD_20, fa.getAmtAcctDr().setScale(2, RoundingMode.HALF_UP), ""); - assertEquals(BigDecimal.TEN.setScale(2, RoundingMode.HALF_UP), fa.getQty().setScale(2, RoundingMode.HALF_UP), ""); - } - } - + Query query = MFactAcct.createRecordIdQuery(MInOut.Table_ID, rLine.getM_InOut_ID(), as.getC_AcctSchema_ID(), getTrxName()); + List factAccts = query.list(); + List expected = Arrays.asList(new FactAcct(acctNIR, BD_20, 2, false, new BigDecimal("-10.00")), + new FactAcct(productExpense, BD_20, 2, true, BigDecimal.TEN)); + assertFactAcctEntries(factAccts, expected); } finally { @@ -379,27 +354,11 @@ public class NonStockedExpTypeAvgPOCostingTest extends AbstractTestCase // get AccPayable of the Invoice MAccount acctPT = new MAccount(Env.getCtx(), as.getAcctSchemaDefault().getV_Liability_Acct(), getTrxName()); - String whereClause = "AD_Table_ID = " + MInvoice.Table_ID; - whereClause += " AND Record_ID = " + iLine.getC_Invoice_ID(); - whereClause += " AND C_AcctSchema_ID = " + as.getC_AcctSchema_ID(); - - int[] ids = MFactAcct.getAllIDs(MFactAcct.Table_Name, whereClause.toString(), getTrxName()); - - for (int id : ids) - { - // Test Accounting for Invoice - MFactAcct fa = new MFactAcct(Env.getCtx(), id, getTrxName()); - if (fa.getAccount_ID() == acctPT.getAccount_ID()) - { - assertEquals(BD_20, fa.getAmtAcctCr().setScale(2, RoundingMode.HALF_UP), ""); - assertEquals(BigDecimal.ZERO.setScale(2, RoundingMode.HALF_UP), fa.getQty().setScale(2, RoundingMode.HALF_UP), ""); - } - else if (fa.getAccount_ID() == productExpense.getAccount_ID()) - { - assertEquals(BD_20, fa.getAmtAcctDr().setScale(2, RoundingMode.HALF_UP), ""); - assertEquals(BigDecimal.TEN.setScale(2, RoundingMode.HALF_UP), fa.getQty().setScale(2, RoundingMode.HALF_UP), ""); - } - } + Query query = MFactAcct.createRecordIdQuery(MInvoice.Table_ID, iLine.getC_Invoice_ID(), as.getC_AcctSchema_ID(), getTrxName()); + List factAccts = query.list(); + List expected = Arrays.asList(new FactAcct(acctPT, BD_20, 2, false, BigDecimal.ZERO), + new FactAcct(productExpense, BD_20, 2, true, BigDecimal.TEN)); + assertFactAcctEntries(factAccts, expected); // Testing Accounting For MatchInv MMatchInv[] matchInvoices = MMatchInv.getInOut(Env.getCtx(), rLine.getM_InOut_ID(), getTrxName()); @@ -416,29 +375,11 @@ public class NonStockedExpTypeAvgPOCostingTest extends AbstractTestCase // get Product Expense for the MatchInv pc = new ProductCost(Env.getCtx(), matchInvoices[0].getM_Product_ID(), 0, getTrxName()); productExpense = pc.getAccount(ProductCost.ACCTTYPE_P_Expense, as); - whereClause = null; - whereClause = "AD_Table_ID = " + MMatchInv.Table_ID; - whereClause += " AND Record_ID = " + matchInvoices[0].get_ID(); - whereClause += " AND C_AcctSchema_ID = " + as.getC_AcctSchema_ID(); - - int[] idsMatchInv = MFactAcct.getAllIDs(MFactAcct.Table_Name, whereClause.toString(), getTrxName()); - - for (int id : idsMatchInv) - { - // Test Accounting for MatchINV - MFactAcct fa = new MFactAcct(Env.getCtx(), id, getTrxName()); - if (fa.getAccount_ID() == acctNIR.getAccount_ID()) - { - assertEquals(BD_20, fa.getAmtAcctDr().setScale(2, RoundingMode.HALF_UP), ""); - assertEquals(BigDecimal.TEN.setScale(2, RoundingMode.HALF_UP), fa.getQty().setScale(2, RoundingMode.HALF_UP), ""); - } - else if (fa.getAccount_ID() == productExpense.getAccount_ID()) - { - assertEquals(BD_20, fa.getAmtAcctCr().setScale(2, RoundingMode.HALF_UP), ""); - assertEquals(new BigDecimal("-10.00"), fa.getQty().setScale(2, RoundingMode.HALF_UP), ""); - } - } - + query = MFactAcct.createRecordIdQuery(MMatchInv.Table_ID, matchInvoices[0].get_ID(), as.getC_AcctSchema_ID(), getTrxName()); + factAccts = query.list(); + expected = Arrays.asList(new FactAcct(acctNIR, BD_20, 2, true, BigDecimal.TEN), + new FactAcct(productExpense, BD_20, 2, false, BigDecimal.TEN.negate())); + assertFactAcctEntries(factAccts, expected); } finally { diff --git a/org.idempiere.test/src/org/idempiere/test/costing/NonStockedExpTypeStdCostingTest.java b/org.idempiere.test/src/org/idempiere/test/costing/NonStockedExpTypeStdCostingTest.java index 9b7149432b..00deac0a1a 100644 --- a/org.idempiere.test/src/org/idempiere/test/costing/NonStockedExpTypeStdCostingTest.java +++ b/org.idempiere.test/src/org/idempiere/test/costing/NonStockedExpTypeStdCostingTest.java @@ -22,6 +22,7 @@ import static org.junit.jupiter.api.Assertions.fail; import java.math.BigDecimal; import java.math.RoundingMode; import java.sql.Timestamp; +import java.util.Arrays; import java.util.List; import org.compiere.acct.Doc; @@ -68,6 +69,7 @@ import org.eevolution.model.MPPProductBOM; import org.eevolution.model.MPPProductBOMLine; import org.idempiere.test.AbstractTestCase; import org.idempiere.test.DictionaryIDs; +import org.idempiere.test.FactAcct; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.parallel.Isolated; @@ -129,38 +131,11 @@ public class NonStockedExpTypeStdCostingTest extends AbstractTestCase ProductCost pc = new ProductCost(Env.getCtx(), rLine.getM_Product_ID(), 0, getTrxName()); MAccount productExpense = pc.getAccount(ProductCost.ACCTTYPE_P_Expense, as); - StringBuilder whereClause = new StringBuilder(); - whereClause - .append(MFactAcct.COLUMNNAME_AD_Table_ID) - .append("=") - .append(MInOut.Table_ID) - .append(" AND ") - .append(MFactAcct.COLUMNNAME_Record_ID) - .append("=") - .append(rLine.getM_InOut_ID()) - .append(" AND ") - .append(MFactAcct.COLUMNNAME_C_AcctSchema_ID) - .append("=") - .append(as.getC_AcctSchema_ID()); - - int[] ids = MFactAcct.getAllIDs(MFactAcct.Table_Name, whereClause.toString(), getTrxName()); - - for (int id : ids) - { - // Test Accounting for MR - MFactAcct fa = new MFactAcct(Env.getCtx(), id, getTrxName()); - if (fa.getAccount_ID() == acctNIR.getAccount_ID()) - { - assertEquals(BD_20, fa.getAmtAcctCr().setScale(2, RoundingMode.HALF_UP), ""); - assertEquals(new BigDecimal("-10.00"), fa.getQty().setScale(2, RoundingMode.HALF_UP), ""); - } - else if (fa.getAccount_ID() == productExpense.getAccount_ID()) - { - assertEquals(BD_20, fa.getAmtAcctDr().setScale(2, RoundingMode.HALF_UP), ""); - assertEquals(BigDecimal.TEN.setScale(2, RoundingMode.HALF_UP), fa.getQty().setScale(2, RoundingMode.HALF_UP), ""); - } - } - + Query query = MFactAcct.createRecordIdQuery(MInOut.Table_ID, rLine.getM_InOut_ID(), as.getC_AcctSchema_ID(), getTrxName()); + List factAccts = query.list(); + List expected = Arrays.asList(new FactAcct(acctNIR, BD_20, 2, false, BigDecimal.TEN.negate()), + new FactAcct(productExpense, BD_20, 2, true, BigDecimal.TEN)); + assertFactAcctEntries(factAccts, expected); } finally { @@ -377,38 +352,11 @@ public class NonStockedExpTypeStdCostingTest extends AbstractTestCase // get AccPayable of the Invoice MAccount acctPT = new MAccount(Env.getCtx(), as.getAcctSchemaDefault().getV_Liability_Acct(), getTrxName()); - StringBuilder whereClause = new StringBuilder(); - - whereClause - .append(MFactAcct.COLUMNNAME_AD_Table_ID) - .append("=") - .append(MInvoice.Table_ID) - .append(" AND ") - .append(MFactAcct.COLUMNNAME_Record_ID) - .append("=") - .append(iLine.getC_Invoice_ID()) - .append(" AND ") - .append(MFactAcct.COLUMNNAME_C_AcctSchema_ID) - .append("=") - .append(as.getC_AcctSchema_ID()); - - int[] ids = MFactAcct.getAllIDs(MFactAcct.Table_Name, whereClause.toString(), getTrxName()); - - for (int id : ids) - { - // Test Accounting for Invoice - MFactAcct fa = new MFactAcct(Env.getCtx(), id, getTrxName()); - if (fa.getAccount_ID() == acctPT.getAccount_ID()) - { - assertEquals(BD_20, fa.getAmtAcctCr().setScale(2, RoundingMode.HALF_UP), ""); - assertEquals(BigDecimal.ZERO.setScale(2, RoundingMode.HALF_UP), fa.getQty().setScale(2, RoundingMode.HALF_UP), ""); - } - else if (fa.getAccount_ID() == productExpense.getAccount_ID()) - { - assertEquals(BD_20, fa.getAmtAcctDr().setScale(2, RoundingMode.HALF_UP), ""); - assertEquals(BigDecimal.TEN.setScale(2, RoundingMode.HALF_UP), fa.getQty().setScale(2, RoundingMode.HALF_UP), ""); - } - } + Query query = MFactAcct.createRecordIdQuery(MInvoice.Table_ID, iLine.getC_Invoice_ID(), as.getC_AcctSchema_ID(), getTrxName()); + List factAccts = query.list(); + List expected = Arrays.asList(new FactAcct(acctPT, BD_20, 2, false, BigDecimal.ZERO), + new FactAcct(productExpense, BD_20, 2, true, BigDecimal.TEN)); + assertFactAcctEntries(factAccts, expected); //Accounting MatchInv MMatchInv[] matchInvoices = MMatchInv.getInOut(Env.getCtx(), rLine.getM_InOut_ID(), getTrxName()); @@ -426,39 +374,11 @@ public class NonStockedExpTypeStdCostingTest extends AbstractTestCase pc = new ProductCost(Env.getCtx(), matchInvoices[0].getM_Product_ID(), 0, getTrxName()); productExpense = pc.getAccount(ProductCost.ACCTTYPE_P_Expense, as); - whereClause = null; - whereClause = new StringBuilder(); - whereClause - .append(MFactAcct.COLUMNNAME_AD_Table_ID) - .append("=") - .append(MMatchInv.Table_ID) - .append(" AND ") - .append(MFactAcct.COLUMNNAME_Record_ID) - .append("=") - .append(matchInvoices[0].get_ID()) - .append(" AND ") - .append(MFactAcct.COLUMNNAME_C_AcctSchema_ID) - .append("=") - .append(as.getC_AcctSchema_ID()); - - int[] idsMatchInv = MFactAcct.getAllIDs(MFactAcct.Table_Name, whereClause.toString(), getTrxName()); - - for (int id : idsMatchInv) - { - // Test Accounting for MatchINV - MFactAcct fa = new MFactAcct(Env.getCtx(), id, getTrxName()); - if (fa.getAccount_ID() == acctNIR.getAccount_ID()) - { - assertEquals(BD_20, fa.getAmtAcctDr().setScale(2, RoundingMode.HALF_UP), ""); - assertEquals(BigDecimal.TEN.setScale(2, RoundingMode.HALF_UP), fa.getQty().setScale(2, RoundingMode.HALF_UP), ""); - } - else if (fa.getAccount_ID() == productExpense.getAccount_ID()) - { - assertEquals(BD_20, fa.getAmtAcctCr().setScale(2, RoundingMode.HALF_UP), ""); - assertEquals(new BigDecimal("-10.00"), fa.getQty().setScale(2, RoundingMode.HALF_UP), ""); - } - } - + query = MFactAcct.createRecordIdQuery(MMatchInv.Table_ID, matchInvoices[0].get_ID(), as.getC_AcctSchema_ID(), getTrxName()); + factAccts = query.list(); + expected = Arrays.asList(new FactAcct(acctNIR, BD_20, 2, true, BigDecimal.TEN), + new FactAcct(productExpense, BD_20, 2, false, BigDecimal.TEN.negate())); + assertFactAcctEntries(factAccts, expected); } finally { diff --git a/org.idempiere.test/src/org/idempiere/test/model/Allocation2ndAcctSchemaTest.java b/org.idempiere.test/src/org/idempiere/test/model/Allocation2ndAcctSchemaTest.java index 54ce6232a8..7efc543af0 100644 --- a/org.idempiere.test/src/org/idempiere/test/model/Allocation2ndAcctSchemaTest.java +++ b/org.idempiere.test/src/org/idempiere/test/model/Allocation2ndAcctSchemaTest.java @@ -34,6 +34,7 @@ import java.sql.Timestamp; import java.util.ArrayList; import java.util.Calendar; import java.util.HashMap; +import java.util.List; import org.compiere.acct.Doc; import org.compiere.acct.DocManager; @@ -1453,11 +1454,11 @@ public class Allocation2ndAcctSchemaTest extends AbstractTestCase { BigDecimal allocAmount = new BigDecimal(1000); ArrayList tradeLineList = new ArrayList(); ArrayList gainLossLineList = new ArrayList(); - BigDecimal accountedDrAmt = getAccountedAmount(usd, allocAmount, cr2.getMultiplyRate()); + BigDecimal accountedDrAmt = getAccountedAmount(usd, allocAmount, cr1.getMultiplyRate()); BigDecimal accountedCrAmt = getAccountedAmount(usd, allocAmount, cr1.getMultiplyRate()); tradeLineList.add(new PostingLine(usd, accountedDrAmt, Env.ZERO)); tradeLineList.add(new PostingLine(usd, Env.ZERO, accountedCrAmt)); - BigDecimal gainLossAmt = new BigDecimal(1000).setScale(usd.getStdPrecision(), RoundingMode.HALF_UP); + BigDecimal gainLossAmt = BigDecimal.ZERO; gainLossLineList.add(new PostingLine(usd, Env.ZERO, gainLossAmt)); testAllocationPosting(ass, allocList, null, tradeLineList, gainLossLineList, null); @@ -2181,12 +2182,9 @@ public class Allocation2ndAcctSchemaTest extends AbstractTestCase { BigDecimal totalTradeAmtAcct = Env.ZERO; BigDecimal totalGainLossAmtAcct = Env.ZERO; BigDecimal totalCurrBalAmtAcct = Env.ZERO; - String whereClause = MFactAcct.COLUMNNAME_AD_Table_ID + "=" + MAllocationHdr.Table_ID - + " AND " + MFactAcct.COLUMNNAME_Record_ID + "=" + alloc.get_ID() - + " AND " + MFactAcct.COLUMNNAME_C_AcctSchema_ID + "=" + as.getC_AcctSchema_ID(); - int[] ids = MFactAcct.getAllIDs(MFactAcct.Table_Name, whereClause, getTrxName()); - for (int id : ids) { - MFactAcct fa = new MFactAcct(Env.getCtx(), id, getTrxName()); + Query query = MFactAcct.createRecordIdQuery(MAllocationHdr.Table_ID, alloc.get_ID(), as.getC_AcctSchema_ID(), getTrxName()); + List factAccts = query.list(); + for (MFactAcct fa : factAccts) { if (C_Charge_ID != 0 && acctCharge != null && acctCharge.getAccount_ID() == fa.getAccount_ID()) totalPaymentAmtAcct = totalPaymentAmtAcct.add(fa.getAmtAcctDr()).subtract(fa.getAmtAcctCr()); else if (C_Charge_ID == 0 && acctUnallocatedCash != null && acctUnallocatedCash.getAccount_ID() == fa.getAccount_ID()) diff --git a/org.idempiere.test/src/org/idempiere/test/model/AllocationTest.java b/org.idempiere.test/src/org/idempiere/test/model/AllocationTest.java index 3a9e597889..f9fa8c1ce5 100644 --- a/org.idempiere.test/src/org/idempiere/test/model/AllocationTest.java +++ b/org.idempiere.test/src/org/idempiere/test/model/AllocationTest.java @@ -31,7 +31,9 @@ import static org.junit.jupiter.api.Assertions.assertTrue; import java.math.BigDecimal; import java.math.RoundingMode; import java.sql.Timestamp; +import java.util.Arrays; import java.util.Calendar; +import java.util.List; import java.util.Properties; import java.util.logging.LogRecord; @@ -64,6 +66,7 @@ import org.compiere.wf.MWorkflow; import org.idempiere.test.AbstractTestCase; import org.idempiere.test.ConversionRateHelper; import org.idempiere.test.DictionaryIDs; +import org.idempiere.test.FactAcct; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.parallel.Isolated; import org.junit.jupiter.api.parallel.ResourceLock; @@ -459,12 +462,9 @@ public class AllocationTest extends AbstractTestCase { BigDecimal ucAmtAcctCr = new BigDecimal(31000); BigDecimal lossAmtAcct = new BigDecimal(1000); - whereClause = MFactAcct.COLUMNNAME_AD_Table_ID + "=" + MAllocationHdr.Table_ID - + " AND " + MFactAcct.COLUMNNAME_Record_ID + "=" + allocation.get_ID() - + " AND " + MFactAcct.COLUMNNAME_C_AcctSchema_ID + "=" + as.getC_AcctSchema_ID(); - int[] ids = MFactAcct.getAllIDs(MFactAcct.Table_Name, whereClause, getTrxName()); - for (int id : ids) { - MFactAcct fa = new MFactAcct(Env.getCtx(), id, getTrxName()); + Query query = MFactAcct.createRecordIdQuery(MAllocationHdr.Table_ID, allocation.get_ID(), as.getC_AcctSchema_ID(), getTrxName()); + List factAccts = query.list(); + for (MFactAcct fa : factAccts) { if (acctUC.getAccount_ID() == fa.getAccount_ID()) { if (fa.getAmtAcctDr().signum() > 0) assertTrue(fa.getAmtAcctDr().compareTo(ucAmtAcctDr) == 0, fa.getAmtAcctDr().toPlainString() + "!=" + ucAmtAcctDr.toPlainString()); @@ -605,12 +605,9 @@ public class AllocationTest extends AbstractTestCase { MAccount acctLiability = doc.getAccount(Doc.ACCTTYPE_V_Liability, as); BigDecimal tradeAmtAcct = new BigDecimal(2.13).setScale(usd.getStdPrecision(), RoundingMode.HALF_UP);; - String whereClause = MFactAcct.COLUMNNAME_AD_Table_ID + "=" + MAllocationHdr.Table_ID - + " AND " + MFactAcct.COLUMNNAME_Record_ID + "=" + allocation.get_ID() - + " AND " + MFactAcct.COLUMNNAME_C_AcctSchema_ID + "=" + as.getC_AcctSchema_ID(); - int[] ids = MFactAcct.getAllIDs(MFactAcct.Table_Name, whereClause, getTrxName()); - for (int id : ids) { - MFactAcct fa = new MFactAcct(Env.getCtx(), id, getTrxName()); + Query query = MFactAcct.createRecordIdQuery(MAllocationHdr.Table_ID, allocation.get_ID(), as.getC_AcctSchema_ID(), getTrxName()); + List factAccts = query.list(); + for (MFactAcct fa : factAccts) { if (acctLiability.getAccount_ID() == fa.getAccount_ID()) { if (fa.getAmtAcctDr().signum() > 0) assertTrue(fa.getAmtAcctDr().compareTo(tradeAmtAcct) == 0, fa.getAmtAcctDr().toPlainString() + "!=" + tradeAmtAcct.toPlainString()); @@ -706,12 +703,9 @@ public class AllocationTest extends AbstractTestCase { BigDecimal ucAmtAcctCr = new BigDecimal(31000); BigDecimal lossAmtAcct = new BigDecimal(1000); - whereClause = MFactAcct.COLUMNNAME_AD_Table_ID + "=" + MAllocationHdr.Table_ID - + " AND " + MFactAcct.COLUMNNAME_Record_ID + "=" + allocation.get_ID() - + " AND " + MFactAcct.COLUMNNAME_C_AcctSchema_ID + "=" + as.getC_AcctSchema_ID(); - int[] ids = MFactAcct.getAllIDs(MFactAcct.Table_Name, whereClause, getTrxName()); - for (int id : ids) { - MFactAcct fa = new MFactAcct(Env.getCtx(), id, getTrxName()); + Query query = MFactAcct.createRecordIdQuery(MAllocationHdr.Table_ID, allocation.get_ID(), as.getC_AcctSchema_ID(), getTrxName()); + List factAccts = query.list(); + for (MFactAcct fa : factAccts) { if (acctUC.getAccount_ID() == fa.getAccount_ID()) { if (fa.getAmtAcctDr().signum() > 0) assertTrue(fa.getAmtAcctDr().compareTo(ucAmtAcctDr) == 0, fa.getAmtAcctDr().toPlainString() + "!=" + ucAmtAcctDr.toPlainString()); @@ -897,43 +891,14 @@ public class AllocationTest extends AbstractTestCase { MAccount acctART = new MAccount(Env.getCtx(), as.getAcctSchemaDefault().getC_Receivable_Acct(), getTrxName()); MAccount acctTD = new MAccount(Env.getCtx(), as.getAcctSchemaDefault().getT_Due_Acct(), getTrxName()); - whereClause = MFactAcct.COLUMNNAME_AD_Table_ID + "=" + MAllocationHdr.Table_ID - + " AND " + MFactAcct.COLUMNNAME_Record_ID + "=" + alloc.get_ID() - + " AND " + MFactAcct.COLUMNNAME_C_AcctSchema_ID + "=" + as.getC_AcctSchema_ID() - + " ORDER BY Created"; - int[] ids = MFactAcct.getAllIDs(MFactAcct.Table_Name, whereClause, getTrxName()); - - for (int id : ids) { - MFactAcct fa = new MFactAcct(Env.getCtx(), id, getTrxName()); - if(fa.getAccount_ID() == acctUC.getAccount_ID()) { - assertEquals(fa.getAmtAcctDr().setScale(2, RoundingMode.HALF_UP), new BigDecimal("102.00")); - assertEquals(fa.getAmtAcctCr().setScale(2, RoundingMode.HALF_UP), new BigDecimal("0.00")); - } else if(fa.getAccount_ID() == acctDEP.getAccount_ID()) { - if(fa.getAmtAcctDr().setScale(2, RoundingMode.HALF_UP).compareTo(Env.ZERO)>0) { - assertEquals(fa.getAmtAcctDr().setScale(2, RoundingMode.HALF_UP), new BigDecimal("2.00")); - assertEquals(fa.getAmtAcctCr().setScale(2, RoundingMode.HALF_UP), new BigDecimal("0.00")); - } else { - assertEquals(fa.getAmtAcctDr().setScale(2, RoundingMode.HALF_UP), new BigDecimal("0.00")); - assertEquals(fa.getAmtAcctCr().setScale(2, RoundingMode.HALF_UP), new BigDecimal("0.11")); - } - } else if(fa.getAccount_ID() == acctWO.getAccount_ID()) { - if(fa.getAmtAcctDr().setScale(2, RoundingMode.HALF_UP).compareTo(Env.ZERO)>0) { - assertEquals(fa.getAmtAcctDr().setScale(2, RoundingMode.HALF_UP), new BigDecimal("2.00")); - assertEquals(fa.getAmtAcctCr().setScale(2, RoundingMode.HALF_UP), new BigDecimal("0.00")); - } else { - assertEquals(fa.getAmtAcctDr().setScale(2, RoundingMode.HALF_UP), new BigDecimal("0.00")); - assertEquals(fa.getAmtAcctCr().setScale(2, RoundingMode.HALF_UP), new BigDecimal("0.11")); - } - - } else if(fa.getAccount_ID() == acctART.getAccount_ID()) { - assertEquals(fa.getAmtAcctDr().setScale(2, RoundingMode.HALF_UP), new BigDecimal("0.00")); - assertEquals(fa.getAmtAcctCr().setScale(2, RoundingMode.HALF_UP), new BigDecimal("106.00")); - } else if(fa.getAccount_ID() == acctTD.getAccount_ID()) { - assertEquals(fa.getAmtAcctDr().setScale(2, RoundingMode.HALF_UP), new BigDecimal("0.11")); - assertEquals(fa.getAmtAcctCr().setScale(2, RoundingMode.HALF_UP), new BigDecimal("0.00")); - } - - } + Query query = MFactAcct.createRecordIdQuery(MAllocationHdr.Table_ID, alloc.get_ID(), as.getC_AcctSchema_ID(), getTrxName()); + List factAccts = query.list(); + List expected = Arrays.asList(new FactAcct(acctUC, new BigDecimal("102.00"), 2, true), + new FactAcct(acctDEP, new BigDecimal("2.00"), 2, true), new FactAcct(acctDEP, new BigDecimal("0.11"), 2, false), + new FactAcct(acctWO, new BigDecimal("2.00"), 2, true), new FactAcct(acctWO, new BigDecimal("0.11"), 2, false), + new FactAcct(acctART, new BigDecimal("106.00"), 2, false), + new FactAcct(acctTD, new BigDecimal("0.11"), 2, true)); + assertFactAcctEntries(factAccts, expected); } } finally { @@ -1028,42 +993,14 @@ public class AllocationTest extends AbstractTestCase { MAccount acctART = new MAccount(Env.getCtx(), as.getAcctSchemaDefault().getC_Receivable_Acct(), getTrxName()); MAccount acctTD = new MAccount(Env.getCtx(), as.getAcctSchemaDefault().getT_Due_Acct(), getTrxName()); - whereClause = MFactAcct.COLUMNNAME_AD_Table_ID + "=" + MAllocationHdr.Table_ID - + " AND " + MFactAcct.COLUMNNAME_Record_ID + "=" + alloc.get_ID() - + " AND " + MFactAcct.COLUMNNAME_C_AcctSchema_ID + "=" + as.getC_AcctSchema_ID() - + " ORDER BY Created"; - int[] ids = MFactAcct.getAllIDs(MFactAcct.Table_Name, whereClause, getTrxName()); - - for (int id : ids) { - MFactAcct fa = new MFactAcct(Env.getCtx(), id, getTrxName()); - if(fa.getAccount_ID() == acctPS.getAccount_ID()) { - assertEquals(fa.getAmtAcctDr().setScale(2, RoundingMode.HALF_UP), new BigDecimal("102.00").negate()); - assertEquals(fa.getAmtAcctCr().setScale(2, RoundingMode.HALF_UP), new BigDecimal("0.00")); - } else if(fa.getAccount_ID() == acctDEP.getAccount_ID()) { - if(fa.getAmtAcctDr().setScale(2, RoundingMode.HALF_UP).compareTo(Env.ZERO)<0) { - assertEquals(fa.getAmtAcctDr().setScale(2, RoundingMode.HALF_UP), new BigDecimal("2.00").negate()); - assertEquals(fa.getAmtAcctCr().setScale(2, RoundingMode.HALF_UP), new BigDecimal("0.00")); - } else { - assertEquals(fa.getAmtAcctDr().setScale(2, RoundingMode.HALF_UP), new BigDecimal("0.11")); - assertEquals(fa.getAmtAcctCr().setScale(2, RoundingMode.HALF_UP), new BigDecimal("0.00")); - } - } else if(fa.getAccount_ID() == acctWO.getAccount_ID()) { - if(fa.getAmtAcctDr().setScale(2, RoundingMode.HALF_UP).compareTo(Env.ZERO)<0) { - assertEquals(fa.getAmtAcctDr().setScale(2, RoundingMode.HALF_UP), new BigDecimal("2.00").negate()); - assertEquals(fa.getAmtAcctCr().setScale(2, RoundingMode.HALF_UP), new BigDecimal("0.00")); - } else { - assertEquals(fa.getAmtAcctDr().setScale(2, RoundingMode.HALF_UP), new BigDecimal("0.11")); - assertEquals(fa.getAmtAcctCr().setScale(2, RoundingMode.HALF_UP), new BigDecimal("0.00")); - } - } else if(fa.getAccount_ID() == acctART.getAccount_ID()) { - assertEquals(fa.getAmtAcctDr().setScale(2, RoundingMode.HALF_UP), new BigDecimal("0.00")); - assertEquals(fa.getAmtAcctCr().setScale(2, RoundingMode.HALF_UP), new BigDecimal("106.00").negate()); - } else if(fa.getAccount_ID() == acctTD.getAccount_ID()) { - assertEquals(fa.getAmtAcctDr().setScale(2, RoundingMode.HALF_UP), new BigDecimal("0.00")); - assertEquals(fa.getAmtAcctCr().setScale(2, RoundingMode.HALF_UP), new BigDecimal("0.11")); - } - - } + Query query = MFactAcct.createRecordIdQuery(MAllocationHdr.Table_ID, alloc.get_ID(), as.getC_AcctSchema_ID(), getTrxName()); + List factAccts = query.list(); + List expected = Arrays.asList(new FactAcct(acctPS, new BigDecimal("-102.00"), 2, true), + new FactAcct(acctDEP, new BigDecimal("2.00").negate(), 2, true), new FactAcct(acctDEP, new BigDecimal("0.11"), 2, true), + new FactAcct(acctWO, new BigDecimal("2.00").negate(), 2, true), new FactAcct(acctWO, new BigDecimal("0.11"), 2, true), + new FactAcct(acctART, new BigDecimal("106.00").negate(), 2, false), + new FactAcct(acctTD, new BigDecimal("0.11"), 2, false)); + assertFactAcctEntries(factAccts, expected); } } finally { @@ -1158,42 +1095,14 @@ public class AllocationTest extends AbstractTestCase { MAccount acctPS = new MAccount(Env.getCtx(), as.getAcctSchemaDefault().getB_PaymentSelect_Acct(), getTrxName()); MAccount acctTD = new MAccount(Env.getCtx(), as.getAcctSchemaDefault().getT_Credit_Acct(), getTrxName()); - whereClause = MFactAcct.COLUMNNAME_AD_Table_ID + "=" + MAllocationHdr.Table_ID - + " AND " + MFactAcct.COLUMNNAME_Record_ID + "=" + alloc.get_ID() - + " AND " + MFactAcct.COLUMNNAME_C_AcctSchema_ID + "=" + as.getC_AcctSchema_ID() - + " ORDER BY Created"; - int[] ids = MFactAcct.getAllIDs(MFactAcct.Table_Name, whereClause, getTrxName()); - - for (int id : ids) { - MFactAcct fa = new MFactAcct(Env.getCtx(), id, getTrxName()); - if(fa.getAccount_ID() == acctPT.getAccount_ID()) { - assertEquals(fa.getAmtAcctDr().setScale(2, RoundingMode.HALF_UP), new BigDecimal("106.00")); - assertEquals(fa.getAmtAcctCr().setScale(2, RoundingMode.HALF_UP), new BigDecimal("0.00")); - } else if(fa.getAccount_ID() == acctDRE.getAccount_ID()) { - if(fa.getAmtAcctCr().setScale(2, RoundingMode.HALF_UP).compareTo(Env.ZERO)>0) { - assertEquals(fa.getAmtAcctDr().setScale(2, RoundingMode.HALF_UP), new BigDecimal("0.00")); - assertEquals(fa.getAmtAcctCr().setScale(2, RoundingMode.HALF_UP), new BigDecimal("2.00")); - } else { - assertEquals(fa.getAmtAcctDr().setScale(2, RoundingMode.HALF_UP), new BigDecimal("0.11")); - assertEquals(fa.getAmtAcctCr().setScale(2, RoundingMode.HALF_UP), new BigDecimal("0.00")); - } - } else if(fa.getAccount_ID() == acctWO.getAccount_ID()) { - if(fa.getAmtAcctCr().setScale(2, RoundingMode.HALF_UP).compareTo(Env.ZERO)>0) { - assertEquals(fa.getAmtAcctDr().setScale(2, RoundingMode.HALF_UP), new BigDecimal("0.00")); - assertEquals(fa.getAmtAcctCr().setScale(2, RoundingMode.HALF_UP), new BigDecimal("2.00")); - } else { - assertEquals(fa.getAmtAcctDr().setScale(2, RoundingMode.HALF_UP), new BigDecimal("0.11")); - assertEquals(fa.getAmtAcctCr().setScale(2, RoundingMode.HALF_UP), new BigDecimal("0.00")); - } - } else if(fa.getAccount_ID() == acctPS.getAccount_ID()) { - assertEquals(fa.getAmtAcctDr().setScale(2, RoundingMode.HALF_UP), new BigDecimal("0.00")); - assertEquals(fa.getAmtAcctCr().setScale(2, RoundingMode.HALF_UP), new BigDecimal("102.00")); - } else if(fa.getAccount_ID() == acctTD.getAccount_ID()) { - assertEquals(fa.getAmtAcctDr().setScale(2, RoundingMode.HALF_UP), new BigDecimal("0.00")); - assertEquals(fa.getAmtAcctCr().setScale(2, RoundingMode.HALF_UP), new BigDecimal("0.11")); - } - - } + Query query = MFactAcct.createRecordIdQuery(MAllocationHdr.Table_ID, alloc.get_ID(), as.getC_AcctSchema_ID(), getTrxName()); + List factAccts = query.list(); + List expected = Arrays.asList(new FactAcct(acctPT, new BigDecimal("106.00"), 2, true), + new FactAcct(acctDRE, new BigDecimal("2.00"), 2, false), new FactAcct(acctDRE, new BigDecimal("0.11"), 2, true), + new FactAcct(acctWO, new BigDecimal("2.00"), 2, false), new FactAcct(acctWO, new BigDecimal("0.11"), 2, true), + new FactAcct(acctPS, new BigDecimal("102.00"), 2, false), + new FactAcct(acctTD, new BigDecimal("0.11"), 2, false)); + assertFactAcctEntries(factAccts, expected); } } finally { @@ -1288,44 +1197,15 @@ public class AllocationTest extends AbstractTestCase { MAccount acctUC = new MAccount(Env.getCtx(), as.getAcctSchemaDefault().getB_UnallocatedCash_Acct(), getTrxName()); MAccount acctTD = new MAccount(Env.getCtx(), as.getAcctSchemaDefault().getT_Credit_Acct(), getTrxName()); - whereClause = MFactAcct.COLUMNNAME_AD_Table_ID + "=" + MAllocationHdr.Table_ID - + " AND " + MFactAcct.COLUMNNAME_Record_ID + "=" + alloc.get_ID() - + " AND " + MFactAcct.COLUMNNAME_C_AcctSchema_ID + "=" + as.getC_AcctSchema_ID() - + " ORDER BY Created"; - int[] ids = MFactAcct.getAllIDs(MFactAcct.Table_Name, whereClause, getTrxName()); - - for (int id : ids) { - MFactAcct fa = new MFactAcct(Env.getCtx(), id, getTrxName()); - if(fa.getAccount_ID() == acctPT.getAccount_ID()) { - assertEquals(fa.getAmtAcctDr().setScale(2, RoundingMode.HALF_UP), new BigDecimal("106.00").negate()); - assertEquals(fa.getAmtAcctCr().setScale(2, RoundingMode.HALF_UP), new BigDecimal("0.00")); - } else if(fa.getAccount_ID() == acctDRE.getAccount_ID()) { - if(fa.getAmtAcctCr().setScale(2, RoundingMode.HALF_UP).compareTo(Env.ZERO)<0) { - assertEquals(fa.getAmtAcctDr().setScale(2, RoundingMode.HALF_UP), new BigDecimal("0.00")); - assertEquals(fa.getAmtAcctCr().setScale(2, RoundingMode.HALF_UP), new BigDecimal("2.00").negate()); - } else { - assertEquals(fa.getAmtAcctDr().setScale(2, RoundingMode.HALF_UP), new BigDecimal("0.00")); - assertEquals(fa.getAmtAcctCr().setScale(2, RoundingMode.HALF_UP), new BigDecimal("0.11")); - } - } else if(fa.getAccount_ID() == acctWO.getAccount_ID()) { - if(fa.getAmtAcctCr().setScale(2, RoundingMode.HALF_UP).compareTo(Env.ZERO)<0) { - assertEquals(fa.getAmtAcctDr().setScale(2, RoundingMode.HALF_UP), new BigDecimal("0.00")); - assertEquals(fa.getAmtAcctCr().setScale(2, RoundingMode.HALF_UP), new BigDecimal("2.00").negate()); - } else { - assertEquals(fa.getAmtAcctDr().setScale(2, RoundingMode.HALF_UP), new BigDecimal("0.00")); - assertEquals(fa.getAmtAcctCr().setScale(2, RoundingMode.HALF_UP), new BigDecimal("0.11")); - } - } else if(fa.getAccount_ID() == acctUC.getAccount_ID()) { - assertEquals(fa.getAmtAcctDr().setScale(2, RoundingMode.HALF_UP), new BigDecimal("0.00")); - assertEquals(fa.getAmtAcctCr().setScale(2, RoundingMode.HALF_UP), new BigDecimal("102.00").negate()); - } else if(fa.getAccount_ID() == acctTD.getAccount_ID()) { - assertEquals(fa.getAmtAcctDr().setScale(2, RoundingMode.HALF_UP), new BigDecimal("0.11")); - assertEquals(fa.getAmtAcctCr().setScale(2, RoundingMode.HALF_UP), new BigDecimal("0.00")); - } - - } + Query query = MFactAcct.createRecordIdQuery(MAllocationHdr.Table_ID, alloc.get_ID(), as.getC_AcctSchema_ID(), getTrxName()); + List factAccts = query.list(); + List expected = Arrays.asList(new FactAcct(acctPT, new BigDecimal("106.00").negate(), 2, true), + new FactAcct(acctDRE, new BigDecimal("2.00").negate(), 2, false), new FactAcct(acctDRE, new BigDecimal("0.11"), 2, false), + new FactAcct(acctWO, new BigDecimal("2.00").negate(), 2, false), new FactAcct(acctWO, new BigDecimal("0.11"), 2, false), + new FactAcct(acctUC, new BigDecimal("102.00").negate(), 2, false), + new FactAcct(acctTD, new BigDecimal("0.11"), 2, true)); + assertFactAcctEntries(factAccts, expected); } - } finally { rollback(); @@ -1419,43 +1299,14 @@ public class AllocationTest extends AbstractTestCase { MAccount acctART = new MAccount(Env.getCtx(), as.getAcctSchemaDefault().getC_Receivable_Acct(), getTrxName()); MAccount acctTD = new MAccount(Env.getCtx(), as.getAcctSchemaDefault().getT_Due_Acct(), getTrxName()); - whereClause = MFactAcct.COLUMNNAME_AD_Table_ID + "=" + MAllocationHdr.Table_ID - + " AND " + MFactAcct.COLUMNNAME_Record_ID + "=" + allocationa[0].get_ID() - + " AND " + MFactAcct.COLUMNNAME_C_AcctSchema_ID + "=" + as.getC_AcctSchema_ID() - + " ORDER BY Created"; - int[] ids = MFactAcct.getAllIDs(MFactAcct.Table_Name, whereClause, getTrxName()); - - for (int id : ids) { - MFactAcct fa = new MFactAcct(Env.getCtx(), id, getTrxName()); - if(fa.getAccount_ID() == acctUC.getAccount_ID()) { - assertEquals(fa.getAmtAcctDr().setScale(2, RoundingMode.HALF_UP), new BigDecimal("102.00")); - assertEquals(fa.getAmtAcctCr().setScale(2, RoundingMode.HALF_UP), new BigDecimal("0.00")); - } else if(fa.getAccount_ID() == acctDEP.getAccount_ID()) { - if(fa.getAmtAcctDr().setScale(2, RoundingMode.HALF_UP).compareTo(Env.ZERO)>0) { - assertEquals(fa.getAmtAcctDr().setScale(2, RoundingMode.HALF_UP), new BigDecimal("2.00")); - assertEquals(fa.getAmtAcctCr().setScale(2, RoundingMode.HALF_UP), new BigDecimal("0.00")); - } else { - assertEquals(fa.getAmtAcctDr().setScale(2, RoundingMode.HALF_UP), new BigDecimal("0.00")); - assertEquals(fa.getAmtAcctCr().setScale(2, RoundingMode.HALF_UP), new BigDecimal("0.11")); - } - } else if(fa.getAccount_ID() == acctWO.getAccount_ID()) { - if(fa.getAmtAcctDr().setScale(2, RoundingMode.HALF_UP).compareTo(Env.ZERO)>0) { - assertEquals(fa.getAmtAcctDr().setScale(2, RoundingMode.HALF_UP), new BigDecimal("2.00")); - assertEquals(fa.getAmtAcctCr().setScale(2, RoundingMode.HALF_UP), new BigDecimal("0.00")); - } else { - assertEquals(fa.getAmtAcctDr().setScale(2, RoundingMode.HALF_UP), new BigDecimal("0.00")); - assertEquals(fa.getAmtAcctCr().setScale(2, RoundingMode.HALF_UP), new BigDecimal("0.11")); - } - - } else if(fa.getAccount_ID() == acctART.getAccount_ID()) { - assertEquals(fa.getAmtAcctDr().setScale(2, RoundingMode.HALF_UP), new BigDecimal("0.00")); - assertEquals(fa.getAmtAcctCr().setScale(2, RoundingMode.HALF_UP), new BigDecimal("106.00")); - } else if(fa.getAccount_ID() == acctTD.getAccount_ID()) { - assertEquals(fa.getAmtAcctDr().setScale(2, RoundingMode.HALF_UP), new BigDecimal("0.11")); - assertEquals(fa.getAmtAcctCr().setScale(2, RoundingMode.HALF_UP), new BigDecimal("0.00")); - } - - } + Query query = MFactAcct.createRecordIdQuery(MAllocationHdr.Table_ID, allocationa[0].get_ID(), as.getC_AcctSchema_ID(), getTrxName()); + List factAccts = query.list(); + List expected = Arrays.asList(new FactAcct(acctUC, new BigDecimal("102.00"), 2, true), + new FactAcct(acctDEP, new BigDecimal("2.00"), 2, true), new FactAcct(acctDEP, new BigDecimal("0.11"), 2, false), + new FactAcct(acctWO, new BigDecimal("2.00"), 2, true), new FactAcct(acctWO, new BigDecimal("0.11"), 2, false), + new FactAcct(acctART, new BigDecimal("106.00"), 2, false), + new FactAcct(acctTD, new BigDecimal("0.11"), 2, true)); + assertFactAcctEntries(factAccts, expected); } } finally { @@ -1545,48 +1396,20 @@ public class AllocationTest extends AbstractTestCase { // 78100_Bad Debts Write-off | 0.11 | 0.00 // 21610 Tax due | 0.00 | 0.11 // -------------------------------------------------------------------- - MAccount acctPS = new MAccount(Env.getCtx(), as.getAcctSchemaDefault().getB_PaymentSelect_Acct(), getTrxName()); + MAccount acctPS = new MAccount(Env.getCtx(), as.getAcctSchemaDefault().getB_UnallocatedCash_Acct(), getTrxName()); MAccount acctDEP = new MAccount(Env.getCtx(), as.getAcctSchemaDefault().getPayDiscount_Exp_Acct(), getTrxName()); MAccount acctWO = new MAccount(Env.getCtx(), as.getAcctSchemaDefault().getWriteOff_Acct(), getTrxName()); MAccount acctART = new MAccount(Env.getCtx(), as.getAcctSchemaDefault().getC_Receivable_Acct(), getTrxName()); MAccount acctTD = new MAccount(Env.getCtx(), as.getAcctSchemaDefault().getT_Due_Acct(), getTrxName()); - whereClause = MFactAcct.COLUMNNAME_AD_Table_ID + "=" + MAllocationHdr.Table_ID - + " AND " + MFactAcct.COLUMNNAME_Record_ID + "=" + allocationa[0].get_ID() - + " AND " + MFactAcct.COLUMNNAME_C_AcctSchema_ID + "=" + as.getC_AcctSchema_ID() - + " ORDER BY Created"; - int[] ids = MFactAcct.getAllIDs(MFactAcct.Table_Name, whereClause, getTrxName()); - - for (int id : ids) { - MFactAcct fa = new MFactAcct(Env.getCtx(), id, getTrxName()); - if(fa.getAccount_ID() == acctPS.getAccount_ID()) { - assertEquals(fa.getAmtAcctDr().setScale(2, RoundingMode.HALF_UP), new BigDecimal("102.00").negate()); - assertEquals(fa.getAmtAcctCr().setScale(2, RoundingMode.HALF_UP), new BigDecimal("0.00")); - } else if(fa.getAccount_ID() == acctDEP.getAccount_ID()) { - if(fa.getAmtAcctDr().setScale(2, RoundingMode.HALF_UP).compareTo(Env.ZERO)<0) { - assertEquals(fa.getAmtAcctDr().setScale(2, RoundingMode.HALF_UP), new BigDecimal("2.00").negate()); - assertEquals(fa.getAmtAcctCr().setScale(2, RoundingMode.HALF_UP), new BigDecimal("0.00")); - } else { - assertEquals(fa.getAmtAcctDr().setScale(2, RoundingMode.HALF_UP), new BigDecimal("0.11")); - assertEquals(fa.getAmtAcctCr().setScale(2, RoundingMode.HALF_UP), new BigDecimal("0.00")); - } - } else if(fa.getAccount_ID() == acctWO.getAccount_ID()) { - if(fa.getAmtAcctDr().setScale(2, RoundingMode.HALF_UP).compareTo(Env.ZERO)<0) { - assertEquals(fa.getAmtAcctDr().setScale(2, RoundingMode.HALF_UP), new BigDecimal("2.00").negate()); - assertEquals(fa.getAmtAcctCr().setScale(2, RoundingMode.HALF_UP), new BigDecimal("0.00")); - } else { - assertEquals(fa.getAmtAcctDr().setScale(2, RoundingMode.HALF_UP), new BigDecimal("0.11")); - assertEquals(fa.getAmtAcctCr().setScale(2, RoundingMode.HALF_UP), new BigDecimal("0.00")); - } - } else if(fa.getAccount_ID() == acctART.getAccount_ID()) { - assertEquals(fa.getAmtAcctDr().setScale(2, RoundingMode.HALF_UP), new BigDecimal("0.00")); - assertEquals(fa.getAmtAcctCr().setScale(2, RoundingMode.HALF_UP), new BigDecimal("106.00").negate()); - } else if(fa.getAccount_ID() == acctTD.getAccount_ID()) { - assertEquals(fa.getAmtAcctDr().setScale(2, RoundingMode.HALF_UP), new BigDecimal("0.00")); - assertEquals(fa.getAmtAcctCr().setScale(2, RoundingMode.HALF_UP), new BigDecimal("0.11")); - } - - } + Query query = MFactAcct.createRecordIdQuery(MAllocationHdr.Table_ID, allocationa[0].get_ID(), as.getC_AcctSchema_ID(), getTrxName()); + List factAccts = query.list(); + List expected = Arrays.asList(new FactAcct(acctPS, new BigDecimal("102.00").negate(), 2, true), + new FactAcct(acctDEP, new BigDecimal("2.00").negate(), 2, true), new FactAcct(acctDEP, new BigDecimal("0.11"), 2, true), + new FactAcct(acctWO, new BigDecimal("2.00").negate(), 2, true), new FactAcct(acctWO, new BigDecimal("0.11"), 2, true), + new FactAcct(acctART, new BigDecimal("106.00").negate(), 2, false), + new FactAcct(acctTD, new BigDecimal("0.11"), 2, false)); + assertFactAcctEntries(factAccts, expected); } } finally { @@ -1682,42 +1505,14 @@ public class AllocationTest extends AbstractTestCase { MAccount acctPS = new MAccount(Env.getCtx(), as.getAcctSchemaDefault().getB_PaymentSelect_Acct(), getTrxName()); MAccount acctTD = new MAccount(Env.getCtx(), as.getAcctSchemaDefault().getT_Credit_Acct(), getTrxName()); - whereClause = MFactAcct.COLUMNNAME_AD_Table_ID + "=" + MAllocationHdr.Table_ID - + " AND " + MFactAcct.COLUMNNAME_Record_ID + "=" + allocationa[0].get_ID() - + " AND " + MFactAcct.COLUMNNAME_C_AcctSchema_ID + "=" + as.getC_AcctSchema_ID() - + " ORDER BY Created"; - int[] ids = MFactAcct.getAllIDs(MFactAcct.Table_Name, whereClause, getTrxName()); - - for (int id : ids) { - MFactAcct fa = new MFactAcct(Env.getCtx(), id, getTrxName()); - if(fa.getAccount_ID() == acctPT.getAccount_ID()) { - assertEquals(fa.getAmtAcctDr().setScale(2, RoundingMode.HALF_UP), new BigDecimal("106.00")); - assertEquals(fa.getAmtAcctCr().setScale(2, RoundingMode.HALF_UP), new BigDecimal("0.00")); - } else if(fa.getAccount_ID() == acctDRE.getAccount_ID()) { - if(fa.getAmtAcctCr().setScale(2, RoundingMode.HALF_UP).compareTo(Env.ZERO)>0) { - assertEquals(fa.getAmtAcctDr().setScale(2, RoundingMode.HALF_UP), new BigDecimal("0.00")); - assertEquals(fa.getAmtAcctCr().setScale(2, RoundingMode.HALF_UP), new BigDecimal("2.00")); - } else { - assertEquals(fa.getAmtAcctDr().setScale(2, RoundingMode.HALF_UP), new BigDecimal("0.11")); - assertEquals(fa.getAmtAcctCr().setScale(2, RoundingMode.HALF_UP), new BigDecimal("0.00")); - } - } else if(fa.getAccount_ID() == acctWO.getAccount_ID()) { - if(fa.getAmtAcctCr().setScale(2, RoundingMode.HALF_UP).compareTo(Env.ZERO)>0) { - assertEquals(fa.getAmtAcctDr().setScale(2, RoundingMode.HALF_UP), new BigDecimal("0.00")); - assertEquals(fa.getAmtAcctCr().setScale(2, RoundingMode.HALF_UP), new BigDecimal("2.00")); - } else { - assertEquals(fa.getAmtAcctDr().setScale(2, RoundingMode.HALF_UP), new BigDecimal("0.11")); - assertEquals(fa.getAmtAcctCr().setScale(2, RoundingMode.HALF_UP), new BigDecimal("0.00")); - } - } else if(fa.getAccount_ID() == acctPS.getAccount_ID()) { - assertEquals(fa.getAmtAcctDr().setScale(2, RoundingMode.HALF_UP), new BigDecimal("0.00")); - assertEquals(fa.getAmtAcctCr().setScale(2, RoundingMode.HALF_UP), new BigDecimal("102.00")); - } else if(fa.getAccount_ID() == acctTD.getAccount_ID()) { - assertEquals(fa.getAmtAcctDr().setScale(2, RoundingMode.HALF_UP), new BigDecimal("0.00")); - assertEquals(fa.getAmtAcctCr().setScale(2, RoundingMode.HALF_UP), new BigDecimal("0.11")); - } - - } + Query query = MFactAcct.createRecordIdQuery(MAllocationHdr.Table_ID, allocationa[0].get_ID(), as.getC_AcctSchema_ID(), getTrxName()); + List factAccts = query.list(); + List expected = Arrays.asList(new FactAcct(acctPT, new BigDecimal("106.00"), 2, true), + new FactAcct(acctDRE, new BigDecimal("2.00"), 2, false), new FactAcct(acctDRE, new BigDecimal("0.11"), 2, true), + new FactAcct(acctWO, new BigDecimal("2.00"), 2, false), new FactAcct(acctWO, new BigDecimal("0.11"), 2, true), + new FactAcct(acctPS, new BigDecimal("102.00"), 2, false), + new FactAcct(acctTD, new BigDecimal("0.11"), 2, false)); + assertFactAcctEntries(factAccts, expected); } } finally { @@ -1810,45 +1605,17 @@ public class AllocationTest extends AbstractTestCase { MAccount acctPT = new MAccount(Env.getCtx(), as.getAcctSchemaDefault().getV_Liability_Acct(), getTrxName()); MAccount acctDRE = new MAccount(Env.getCtx(), as.getAcctSchemaDefault().getPayDiscount_Rev_Acct(), getTrxName()); MAccount acctWO = new MAccount(Env.getCtx(), as.getAcctSchemaDefault().getWriteOff_Acct(), getTrxName()); - MAccount acctUC = new MAccount(Env.getCtx(), as.getAcctSchemaDefault().getB_UnallocatedCash_Acct(), getTrxName()); + MAccount acctUC = new MAccount(Env.getCtx(), as.getAcctSchemaDefault().getB_PaymentSelect_Acct(), getTrxName()); MAccount acctTD = new MAccount(Env.getCtx(), as.getAcctSchemaDefault().getT_Credit_Acct(), getTrxName()); - whereClause = MFactAcct.COLUMNNAME_AD_Table_ID + "=" + MAllocationHdr.Table_ID - + " AND " + MFactAcct.COLUMNNAME_Record_ID + "=" + allocationa[0].get_ID() - + " AND " + MFactAcct.COLUMNNAME_C_AcctSchema_ID + "=" + as.getC_AcctSchema_ID() - + " ORDER BY Created"; - int[] ids = MFactAcct.getAllIDs(MFactAcct.Table_Name, whereClause, getTrxName()); - - for (int id : ids) { - MFactAcct fa = new MFactAcct(Env.getCtx(), id, getTrxName()); - if(fa.getAccount_ID() == acctPT.getAccount_ID()) { - assertEquals(fa.getAmtAcctDr().setScale(2, RoundingMode.HALF_UP), new BigDecimal("106.00").negate()); - assertEquals(fa.getAmtAcctCr().setScale(2, RoundingMode.HALF_UP), new BigDecimal("0.00")); - } else if(fa.getAccount_ID() == acctDRE.getAccount_ID()) { - if(fa.getAmtAcctCr().setScale(2, RoundingMode.HALF_UP).compareTo(Env.ZERO)<0) { - assertEquals(fa.getAmtAcctDr().setScale(2, RoundingMode.HALF_UP), new BigDecimal("0.00")); - assertEquals(fa.getAmtAcctCr().setScale(2, RoundingMode.HALF_UP), new BigDecimal("2.00").negate()); - } else { - assertEquals(fa.getAmtAcctDr().setScale(2, RoundingMode.HALF_UP), new BigDecimal("0.00")); - assertEquals(fa.getAmtAcctCr().setScale(2, RoundingMode.HALF_UP), new BigDecimal("0.11")); - } - } else if(fa.getAccount_ID() == acctWO.getAccount_ID()) { - if(fa.getAmtAcctCr().setScale(2, RoundingMode.HALF_UP).compareTo(Env.ZERO)<0) { - assertEquals(fa.getAmtAcctDr().setScale(2, RoundingMode.HALF_UP), new BigDecimal("0.00")); - assertEquals(fa.getAmtAcctCr().setScale(2, RoundingMode.HALF_UP), new BigDecimal("2.00").negate()); - } else { - assertEquals(fa.getAmtAcctDr().setScale(2, RoundingMode.HALF_UP), new BigDecimal("0.00")); - assertEquals(fa.getAmtAcctCr().setScale(2, RoundingMode.HALF_UP), new BigDecimal("0.11")); - } - } else if(fa.getAccount_ID() == acctUC.getAccount_ID()) { - assertEquals(fa.getAmtAcctDr().setScale(2, RoundingMode.HALF_UP), new BigDecimal("0.00")); - assertEquals(fa.getAmtAcctCr().setScale(2, RoundingMode.HALF_UP), new BigDecimal("104.00").negate()); - } else if(fa.getAccount_ID() == acctTD.getAccount_ID()) { - assertEquals(fa.getAmtAcctDr().setScale(2, RoundingMode.HALF_UP), new BigDecimal("0.11")); - assertEquals(fa.getAmtAcctCr().setScale(2, RoundingMode.HALF_UP), new BigDecimal("0.00")); - } - - } + Query query = MFactAcct.createRecordIdQuery(MAllocationHdr.Table_ID, allocationa[0].get_ID(), as.getC_AcctSchema_ID(), getTrxName()); + List factAccts = query.list(); + List expected = Arrays.asList(new FactAcct(acctPT, new BigDecimal("106.00").negate(), 2, true), + new FactAcct(acctDRE, new BigDecimal("2.00").negate(), 2, false), new FactAcct(acctDRE, new BigDecimal("0.11"), 2, false), + new FactAcct(acctWO, new BigDecimal("2.00").negate(), 2, false), new FactAcct(acctWO, new BigDecimal("0.11"), 2, false), + new FactAcct(acctUC, new BigDecimal("102.00").negate(), 2, false), + new FactAcct(acctTD, new BigDecimal("0.11"), 2, true)); + assertFactAcctEntries(factAccts, expected); } } finally { @@ -1932,60 +1699,29 @@ public class AllocationTest extends AbstractTestCase { // Account | Acct Debit | Acct Credit // -------------------------------------------------------------------- - // 59201_Payment discount revenue | 2.00 | 0.00 + // 59201_Payment discount expense | 2.00 | 0.00 // 78100_Bad Debts Write-off | 2.00 | 0.00 // 12110 Accounts Receivable - Trade | 0.00 | 106.00 // 21610 Tax due | 0.11 | 0.00 - // 59201_Payment discount revenue | 0.00 | 0.11 + // 59201_Payment discount expense | 0.00 | 0.11 // 21610 Tax due | 0.11 | 0.00 // 78100_Bad Debts Write-off | 0.00 | 0.11 // 12110_Accounts Receivable - Trade | 0.00 | -102.00 // -------------------------------------------------------------------- - // ToDo: set Account - MAccount acctDRE = new MAccount(Env.getCtx(), as.getAcctSchemaDefault().getPayDiscount_Rev_Acct(), getTrxName()); - MAccount acctWO = new MAccount(Env.getCtx(), as.getAcctSchemaDefault().getWriteOff_Acct(), getTrxName()); - MAccount acctART = new MAccount(Env.getCtx(), as.getAcctSchemaDefault().getC_Receivable_Acct(), getTrxName()); + doc.setC_BPartner_ID(bpartner.getC_BPartner_ID()); + MAccount acctDRE = doc.getAccount(Doc.ACCTTYPE_DiscountExp, as); + MAccount acctWO = doc.getAccount(Doc.ACCTTYPE_WriteOff, as); + MAccount acctART = doc.getAccount(Doc.ACCTTYPE_C_Receivable, as); MAccount acctTD = new MAccount(Env.getCtx(), as.getAcctSchemaDefault().getT_Due_Acct(), getTrxName()); - whereClause = MFactAcct.COLUMNNAME_AD_Table_ID + "=" + MAllocationHdr.Table_ID - + " AND " + MFactAcct.COLUMNNAME_Record_ID + "=" + alloc.get_ID() - + " AND " + MFactAcct.COLUMNNAME_C_AcctSchema_ID + "=" + as.getC_AcctSchema_ID() - + " ORDER BY Created"; - int[] ids = MFactAcct.getAllIDs(MFactAcct.Table_Name, whereClause, getTrxName()); - - for (int id : ids) { - MFactAcct fa = new MFactAcct(Env.getCtx(), id, getTrxName()); - if(fa.getAccount_ID() == acctDRE.getAccount_ID()) { - if(fa.getAmtAcctDr().setScale(2, RoundingMode.HALF_UP).compareTo(Env.ZERO)>0) { - assertEquals(fa.getAmtAcctDr().setScale(2, RoundingMode.HALF_UP), new BigDecimal("2.00")); - assertEquals(fa.getAmtAcctCr().setScale(2, RoundingMode.HALF_UP), new BigDecimal("0.00")); - } else { - assertEquals(fa.getAmtAcctDr().setScale(2, RoundingMode.HALF_UP), new BigDecimal("0.00")); - assertEquals(fa.getAmtAcctCr().setScale(2, RoundingMode.HALF_UP), new BigDecimal("0.11")); - } - } else if(fa.getAccount_ID() == acctWO.getAccount_ID()) { - if(fa.getAmtAcctDr().setScale(2, RoundingMode.HALF_UP).compareTo(Env.ZERO)>0) { - assertEquals(fa.getAmtAcctDr().setScale(2, RoundingMode.HALF_UP), new BigDecimal("2.00")); - assertEquals(fa.getAmtAcctCr().setScale(2, RoundingMode.HALF_UP), new BigDecimal("0.00")); - } else { - assertEquals(fa.getAmtAcctDr().setScale(2, RoundingMode.HALF_UP), new BigDecimal("0.00")); - assertEquals(fa.getAmtAcctCr().setScale(2, RoundingMode.HALF_UP), new BigDecimal("0.11")); - } - - } else if(fa.getAccount_ID() == acctART.getAccount_ID()) { - if(fa.getAmtAcctCr().setScale(2, RoundingMode.HALF_UP).compareTo(Env.ZERO)>0) { - assertEquals(fa.getAmtAcctDr().setScale(2, RoundingMode.HALF_UP), new BigDecimal("0.00")); - assertEquals(fa.getAmtAcctCr().setScale(2, RoundingMode.HALF_UP), new BigDecimal("106.00")); - } else { - assertEquals(fa.getAmtAcctDr().setScale(2, RoundingMode.HALF_UP), new BigDecimal("0.00")); - assertEquals(fa.getAmtAcctCr().setScale(2, RoundingMode.HALF_UP), new BigDecimal("-102.00")); - } - } else if(fa.getAccount_ID() == acctTD.getAccount_ID()) { - assertEquals(fa.getAmtAcctDr().setScale(2, RoundingMode.HALF_UP), new BigDecimal("0.11")); - assertEquals(fa.getAmtAcctCr().setScale(2, RoundingMode.HALF_UP), new BigDecimal("0.00")); - } - - } + Query query = MFactAcct.createRecordIdQuery(MAllocationHdr.Table_ID, alloc.get_ID(), as.getC_AcctSchema_ID(), getTrxName()); + List factAccts = query.list(); + List expected = Arrays.asList(new FactAcct(acctDRE, new BigDecimal("2.00"), 2, true), + new FactAcct(acctDRE, new BigDecimal("0.11"), 2, false), + new FactAcct(acctWO, new BigDecimal("2.00"), 2, true), new FactAcct(acctWO, new BigDecimal("0.11"), 2, false), + new FactAcct(acctART, new BigDecimal("106.00"), 2, false), new FactAcct(acctART, new BigDecimal("-102.00"), 2, false), + new FactAcct(acctTD, new BigDecimal("0.11"), 2, true)); + assertFactAcctEntries(factAccts, expected); } } finally { @@ -2085,43 +1821,14 @@ public class AllocationTest extends AbstractTestCase { MAccount acctWO = new MAccount(Env.getCtx(), as.getAcctSchemaDefault().getWriteOff_Acct(), getTrxName()); MAccount acctTD = new MAccount(Env.getCtx(), as.getAcctSchemaDefault().getT_Credit_Acct(), getTrxName()); - whereClause = MFactAcct.COLUMNNAME_AD_Table_ID + "=" + MAllocationHdr.Table_ID - + " AND " + MFactAcct.COLUMNNAME_Record_ID + "=" + alloc.get_ID() - + " AND " + MFactAcct.COLUMNNAME_C_AcctSchema_ID + "=" + as.getC_AcctSchema_ID() - + " ORDER BY Created"; - int[] ids = MFactAcct.getAllIDs(MFactAcct.Table_Name, whereClause, getTrxName()); - - for (int id : ids) { - MFactAcct fa = new MFactAcct(Env.getCtx(), id, getTrxName()); - if(fa.getAccount_ID() == acctPT.getAccount_ID()) { - if(fa.getAmtAcctDr().setScale(2, RoundingMode.HALF_UP).compareTo(Env.ZERO)>0) { - assertEquals(fa.getAmtAcctDr().setScale(2, RoundingMode.HALF_UP), new BigDecimal("106.00")); - assertEquals(fa.getAmtAcctCr().setScale(2, RoundingMode.HALF_UP), new BigDecimal("0.00")); - } else { - assertEquals(fa.getAmtAcctDr().setScale(2, RoundingMode.HALF_UP), new BigDecimal("-102.00")); - assertEquals(fa.getAmtAcctCr().setScale(2, RoundingMode.HALF_UP), new BigDecimal("0.00")); - } - } else if(fa.getAccount_ID() == acctDRE.getAccount_ID()) { - if(fa.getAmtAcctCr().setScale(2, RoundingMode.HALF_UP).compareTo(Env.ZERO)>0) { - assertEquals(fa.getAmtAcctDr().setScale(2, RoundingMode.HALF_UP), new BigDecimal("0.00")); - assertEquals(fa.getAmtAcctCr().setScale(2, RoundingMode.HALF_UP), new BigDecimal("2.00")); - } else { - assertEquals(fa.getAmtAcctDr().setScale(2, RoundingMode.HALF_UP), new BigDecimal("0.11")); - assertEquals(fa.getAmtAcctCr().setScale(2, RoundingMode.HALF_UP), new BigDecimal("0.00")); - } - } else if(fa.getAccount_ID() == acctWO.getAccount_ID()) { - if(fa.getAmtAcctCr().setScale(2, RoundingMode.HALF_UP).compareTo(Env.ZERO)>0) { - assertEquals(fa.getAmtAcctDr().setScale(2, RoundingMode.HALF_UP), new BigDecimal("0.00")); - assertEquals(fa.getAmtAcctCr().setScale(2, RoundingMode.HALF_UP), new BigDecimal("2.00")); - } else { - assertEquals(fa.getAmtAcctDr().setScale(2, RoundingMode.HALF_UP), new BigDecimal("0.11")); - assertEquals(fa.getAmtAcctCr().setScale(2, RoundingMode.HALF_UP), new BigDecimal("0.00")); - } - } else if(fa.getAccount_ID() == acctTD.getAccount_ID()) { - assertEquals(fa.getAmtAcctDr().setScale(2, RoundingMode.HALF_UP), new BigDecimal("0.00")); - assertEquals(fa.getAmtAcctCr().setScale(2, RoundingMode.HALF_UP), new BigDecimal("0.11")); - } - } + Query query = MFactAcct.createRecordIdQuery(MAllocationHdr.Table_ID, alloc.get_ID(), as.getC_AcctSchema_ID(), getTrxName()); + List factAccts = query.list(); + List expected = Arrays.asList(new FactAcct(acctPT, new BigDecimal("106.00"), 2, true), + new FactAcct(acctPT, new BigDecimal("-102.00"), 2, true), + new FactAcct(acctDRE, new BigDecimal("2.00"), 2, false), new FactAcct(acctDRE, new BigDecimal("0.11"), 2, true), + new FactAcct(acctWO, new BigDecimal("2.00"), 2, false), new FactAcct(acctWO, new BigDecimal("0.11"), 2, true), + new FactAcct(acctTD, new BigDecimal("0.11"), 2, false)); + assertFactAcctEntries(factAccts, expected); } } finally { @@ -2242,18 +1949,10 @@ public class AllocationTest extends AbstractTestCase { MAccount acctUC = doc.getAccount(Doc.ACCTTYPE_UnallocatedCash, as); BigDecimal ucAmtAcctDr = new BigDecimal(370.88).setScale(2, RoundingMode.HALF_UP); - whereClause = MFactAcct.COLUMNNAME_AD_Table_ID + "=" + MAllocationHdr.Table_ID - + " AND " + MFactAcct.COLUMNNAME_Record_ID + "=" + allocation.get_ID() - + " AND " + MFactAcct.COLUMNNAME_C_AcctSchema_ID + "=" + as.getC_AcctSchema_ID() - + " ORDER BY Created"; - int[] ids = MFactAcct.getAllIDs(MFactAcct.Table_Name, whereClause, getTrxName()); - for (int id : ids) { - MFactAcct fa = new MFactAcct(Env.getCtx(), id, getTrxName()); - if (acctUC.getAccount_ID() == fa.getAccount_ID()) { - if (fa.getAmtAcctDr().signum() > 0) - assertTrue(fa.getAmtAcctDr().compareTo(ucAmtAcctDr) == 0, fa.getAmtAcctDr().toPlainString() + "!=" + ucAmtAcctDr.toPlainString()); - } - } + Query query = MFactAcct.createRecordIdQuery(MAllocationHdr.Table_ID, allocation.get_ID(), as.getC_AcctSchema_ID(), getTrxName()); + List factAccts = query.list(); + List expected = Arrays.asList(new FactAcct(acctUC, ucAmtAcctDr, 2, true)); + assertFactAcctEntries(factAccts, expected); } MInvoice invoice2 = new MInvoice(Env.getCtx(), 0, getTrxName()); @@ -2313,13 +2012,9 @@ public class AllocationTest extends AbstractTestCase { BigDecimal ucAmtAcctDr = new BigDecimal(175.67).setScale(2, RoundingMode.HALF_UP); BigDecimal ucAmtAcctCr = new BigDecimal(0.01).setScale(2, RoundingMode.HALF_UP); - whereClause = MFactAcct.COLUMNNAME_AD_Table_ID + "=" + MAllocationHdr.Table_ID - + " AND " + MFactAcct.COLUMNNAME_Record_ID + "=" + allocation.get_ID() - + " AND " + MFactAcct.COLUMNNAME_C_AcctSchema_ID + "=" + as.getC_AcctSchema_ID() - + " ORDER BY Created"; - int[] ids = MFactAcct.getAllIDs(MFactAcct.Table_Name, whereClause, getTrxName()); - for (int id : ids) { - MFactAcct fa = new MFactAcct(Env.getCtx(), id, getTrxName()); + Query query = MFactAcct.createRecordIdQuery(MAllocationHdr.Table_ID, allocation.get_ID(), as.getC_AcctSchema_ID(), getTrxName()); + List factAccts = query.list(); + for (MFactAcct fa : factAccts) { if (acctUC.getAccount_ID() == fa.getAccount_ID()) { if (fa.getAmtAcctDr().signum() > 0) assertTrue(fa.getAmtAcctDr().compareTo(ucAmtAcctDr) == 0, fa.getAmtAcctDr().toPlainString() + "!=" + ucAmtAcctDr.toPlainString()); diff --git a/org.idempiere.test/src/org/idempiere/test/model/MTaxTest.java b/org.idempiere.test/src/org/idempiere/test/model/MTaxTest.java index 6886600bef..bfca4adac1 100644 --- a/org.idempiere.test/src/org/idempiere/test/model/MTaxTest.java +++ b/org.idempiere.test/src/org/idempiere/test/model/MTaxTest.java @@ -31,6 +31,7 @@ import static org.junit.jupiter.api.Assertions.assertTrue; import java.math.BigDecimal; import java.math.RoundingMode; +import java.util.List; import java.util.Properties; import org.adempiere.base.Core; @@ -58,6 +59,7 @@ import org.compiere.model.MTax; import org.compiere.model.MTaxCategory; import org.compiere.model.MWarehouse; import org.compiere.model.ProductCost; +import org.compiere.model.Query; import org.compiere.model.Tax; import org.compiere.model.X_C_Order; import org.compiere.process.DocAction; @@ -325,13 +327,10 @@ public class MTaxTest extends AbstractTestCase { assertEquals(expectedCost, averageCost, "Un-expected average cost"); MAccount acctAsset = productCost.getAccount(ProductCost.ACCTTYPE_P_Asset, schema); - String whereClause = MFactAcct.COLUMNNAME_AD_Table_ID + "=" + MInOut.Table_ID - + " AND " + MFactAcct.COLUMNNAME_Record_ID + "=" + receipt.get_ID() - + " AND " + MFactAcct.COLUMNNAME_C_AcctSchema_ID + "=" + schema.getC_AcctSchema_ID(); - int[] ids = MFactAcct.getAllIDs(MFactAcct.Table_Name, whereClause, getTrxName()); + Query query = MFactAcct.createRecordIdQuery(MInOut.Table_ID, receipt.get_ID(), schema.getC_AcctSchema_ID(), getTrxName()); + List factAccts = query.list(); BigDecimal totalDebit = new BigDecimal("0.00"); - for(int id : ids) { - MFactAcct fa = new MFactAcct(Env.getCtx(), id, getTrxName()); + for(MFactAcct fa : factAccts) { if (fa.getAccount_ID() == acctAsset.getAccount_ID()) { totalDebit = totalDebit.add(fa.getAmtAcctDr()); } @@ -470,13 +469,10 @@ public class MTaxTest extends AbstractTestCase { assertEquals(expectedCost, averageCost, "Un-expected average cost"); MAccount acctAsset = productCost.getAccount(ProductCost.ACCTTYPE_P_Asset, schema); - String whereClause = MFactAcct.COLUMNNAME_AD_Table_ID + "=" + MInOut.Table_ID - + " AND " + MFactAcct.COLUMNNAME_Record_ID + "=" + receipt.get_ID() - + " AND " + MFactAcct.COLUMNNAME_C_AcctSchema_ID + "=" + schema.getC_AcctSchema_ID(); - int[] ids = MFactAcct.getAllIDs(MFactAcct.Table_Name, whereClause, getTrxName()); + Query query = MFactAcct.createRecordIdQuery(MInOut.Table_ID, receipt.get_ID(), schema.getC_AcctSchema_ID(), getTrxName()); + List factAccts = query.list(); BigDecimal totalDebit = new BigDecimal("0.00"); - for(int id : ids) { - MFactAcct fa = new MFactAcct(Env.getCtx(), id, getTrxName()); + for(MFactAcct fa : factAccts) { if (fa.getAccount_ID() == acctAsset.getAccount_ID()) { totalDebit = totalDebit.add(fa.getAmtAcctDr()); } diff --git a/org.idempiere.test/src/org/idempiere/test/model/ProductionTestIsolated.java b/org.idempiere.test/src/org/idempiere/test/model/ProductionTestIsolated.java index b00751b05d..81fa73832f 100644 --- a/org.idempiere.test/src/org/idempiere/test/model/ProductionTestIsolated.java +++ b/org.idempiere.test/src/org/idempiere/test/model/ProductionTestIsolated.java @@ -33,6 +33,7 @@ import java.math.BigDecimal; import java.math.RoundingMode; import java.sql.Timestamp; import java.util.List; +import java.util.Optional; import org.compiere.model.MAccount; import org.compiere.model.MAcctSchema; @@ -312,17 +313,12 @@ public class ProductionTestIsolated extends AbstractTestCase { ProductCost pc = new ProductCost (Env.getCtx(), mulchX.getM_Product_ID(), 0, getTrxName()); MAccount acctVariance = pc.getAccount(ProductCost.ACCTTYPE_P_RateVariance, as); - whereClause = MFactAcct.COLUMNNAME_AD_Table_ID + "=" + MProduction.Table_ID - + " AND " + MFactAcct.COLUMNNAME_Record_ID + "=" + production.get_ID() - + " AND " + MFactAcct.COLUMNNAME_C_AcctSchema_ID + "=" + as.getC_AcctSchema_ID() - + " AND " + MFactAcct.COLUMNNAME_Account_ID + "=" + acctVariance.getAccount_ID(); - int[] ids = MFactAcct.getAllIDs(MFactAcct.Table_Name, whereClause, getTrxName()); + Query query = MFactAcct.createRecordIdQuery(MProduction.Table_ID, production.get_ID(), as.getC_AcctSchema_ID(), getTrxName()); + List factAccts = query.list(); BigDecimal variance = BigDecimal.ZERO; - for (int id : ids) { - MFactAcct fa = new MFactAcct(Env.getCtx(), id, getTrxName()); - variance = fa.getAmtAcctDr().subtract(fa.getAmtAcctCr()); - break; - } + Optional optional = factAccts.stream().filter(e -> e.getAccount_ID() == acctVariance.getAccount_ID()).findFirst(); + if (optional.isPresent()) + variance = optional.get().getAmtAcctDr().subtract(optional.get().getAmtAcctCr()); BigDecimal varianceExpected = componentCost.subtract(endProductCost).setScale(as.getStdPrecision(), RoundingMode.HALF_UP); assertEquals(varianceExpected.setScale(2, RoundingMode.HALF_UP), variance.setScale(2, RoundingMode.HALF_UP), "Variance not posted correctly."); } finally {