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 1c233bca0d..ce93940071 100644 --- a/org.adempiere.base/src/org/compiere/acct/Doc_MatchInv.java +++ b/org.adempiere.base/src/org/compiere/acct/Doc_MatchInv.java @@ -23,6 +23,7 @@ import java.sql.SQLException; import java.sql.Savepoint; import java.sql.Timestamp; import java.util.ArrayList; +import java.util.HashMap; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; @@ -75,6 +76,9 @@ public class Doc_MatchInv extends Doc super(as, MMatchInv.class, rs, DOCTYPE_MatMatchInv, trxName); } // Doc_MatchInv + /** Tolerance G&L */ + private static final BigDecimal TOLERANCE = BigDecimal.valueOf(0.02); + /** Invoice Line */ private MInvoiceLine m_invoiceLine = null; /** Material Receipt */ @@ -142,6 +146,13 @@ public class Doc_MatchInv extends Doc public ArrayList createFacts (MAcctSchema as) { ArrayList facts = new ArrayList(); + // invoice gain/loss accounting fact line list + ArrayList invGainLossFactLines = new ArrayList(); + // invoice list + ArrayList invList = new ArrayList(); + // C_Invoice_ID and the current M_MatchInv inventory clearing/expense accounting fact lines + HashMap> htFactLineInv = new HashMap>(); + // Nothing to do if (getM_Product_ID() == 0 // no Product || getQty().signum() == 0 @@ -292,16 +303,32 @@ public class Doc_MatchInv extends Doc } } - // Rounding correction + // gain/loss + rounding adjustment if (m_receiptLine != null && m_invoiceLine.getParent().getC_Currency_ID() != as.getC_Currency_ID()) // in foreign currency { p_Error = createReceiptGainLoss(as, fact, getAccount(Doc.ACCTTYPE_NotInvoicedReceipts, as), m_receiptLine.getParent(), dr.getAmtSourceDr(), dr.getAmtAcctDr()); if (p_Error != null) return null; } + // gain/loss if (m_invoiceLine != null && m_invoiceLine.getParent().getC_Currency_ID() != as.getC_Currency_ID()) // in foreign currency { - p_Error = createInvoiceGainLoss(as, fact, expense, m_invoiceLine.getParent(), cr.getAmtSourceCr(), cr.getAmtAcctCr()); + MInvoice invoice = m_invoiceLine.getParent(); + if (!invList.contains(invoice)) + invList.add(invoice); + ArrayList factLineList = htFactLineInv.get(invoice.get_ID()); + if (factLineList == null) + factLineList = new ArrayList(); + factLineList.add(cr); + htFactLineInv.put(invoice.get_ID(), factLineList); + p_Error = createInvoiceGainLoss(as, fact, expense, invoice, cr.getAmtSourceCr(), cr.getAmtAcctCr(), invGainLossFactLines, htFactLineInv); + if (p_Error != null) + return null; + } + // rounding adjustment + if (!htFactLineInv.isEmpty()) + { + p_Error = createInvoiceRoundingCorrection(as, fact, expense, invGainLossFactLines, invList, htFactLineInv); if (p_Error != null) return null; } @@ -587,6 +614,12 @@ public class Doc_MatchInv extends Doc private ArrayList createMatShipmentFacts(MAcctSchema as) { ArrayList facts = new ArrayList(); + // invoice gain/loss accounting fact line list + ArrayList invGainLossFactLines = new ArrayList(); + // invoice list + ArrayList invList = new ArrayList(); + // C_Invoice_ID and the current M_MatchInv inventory clearing/expense accounting fact lines + HashMap> htFactLineInv = new HashMap>(); // create Fact Header Fact fact = new Fact(this, as, Fact.POST_Actual); @@ -708,16 +741,33 @@ public class Doc_MatchInv extends Doc } } - // Rounding correction + // gain/loss + rounding adjustment if (m_receiptLine != null && m_invoiceLine.getParent().getC_Currency_ID() != as.getC_Currency_ID()) // in foreign currency { p_Error = createReceiptGainLoss(as, fact, getAccount(Doc.ACCTTYPE_NotInvoicedReceipts, as), m_receiptLine.getParent(), dr.getAmtSourceCr(), dr.getAmtAcctCr()); if (p_Error != null) return null; } + + // gain/loss if (m_invoiceLine != null && m_invoiceLine.getParent().getC_Currency_ID() != as.getC_Currency_ID()) // in foreign currency { - p_Error = createInvoiceGainLoss(as, fact, expense, m_invoiceLine.getParent(), cr.getAmtSourceDr(), cr.getAmtAcctDr()); + MInvoice invoice = m_invoiceLine.getParent(); + if (!invList.contains(invoice)) + invList.add(invoice); + ArrayList factLineList = htFactLineInv.get(invoice.get_ID()); + if (factLineList == null) + factLineList = new ArrayList(); + factLineList.add(cr); + htFactLineInv.put(invoice.get_ID(), factLineList); + p_Error = createInvoiceGainLoss(as, fact, expense, invoice, cr.getAmtSourceDr(), cr.getAmtAcctDr(), invGainLossFactLines, htFactLineInv); + if (p_Error != null) + return null; + } + // rounding adjustment + if (!htFactLineInv.isEmpty()) + { + p_Error = createInvoiceRoundingCorrection(as, fact, expense, invGainLossFactLines, invList, htFactLineInv); if (p_Error != null) return null; } @@ -795,6 +845,12 @@ public class Doc_MatchInv extends Doc public ArrayList createCreditMemoFacts(MAcctSchema as) { ArrayList facts = new ArrayList(); + // invoice gain/loss accounting fact line list + ArrayList invGainLossFactLines = new ArrayList(); + // invoice list + ArrayList invList = new ArrayList(); + // C_Invoice_ID and the current M_MatchInv inventory clearing/expense accounting fact lines + HashMap> htFactLineInv = new HashMap>(); // create Fact Header Fact fact = new Fact(this, as, Fact.POST_Actual); @@ -975,16 +1031,40 @@ public class Doc_MatchInv extends Doc } } - // Rounding correction + // gain / loss if (refInvLine != null && refInvLine.getParent().getC_Currency_ID() != as.getC_Currency_ID()) // in foreign currency { - p_Error = createInvoiceGainLoss(as, fact, expense, refInvLine.getParent(), dr.getAmtSourceCr(), dr.getAmtAcctCr()); + MInvoice invoice = refInvLine.getParent(); + if (!invList.contains(invoice)) + invList.add(invoice); + ArrayList factLineList = htFactLineInv.get(invoice.get_ID()); + if (factLineList == null) + factLineList = new ArrayList(); + factLineList.add(dr); + htFactLineInv.put(invoice.get_ID(), factLineList); + p_Error = createInvoiceGainLoss(as, fact, expense, invoice, dr.getAmtSourceCr(), dr.getAmtAcctCr(), invGainLossFactLines, htFactLineInv); if (p_Error != null) return null; } if (m_invoiceLine != null && m_invoiceLine.getParent().getC_Currency_ID() != as.getC_Currency_ID()) // in foreign currency { - p_Error = createInvoiceGainLoss(as, fact, expense, m_invoiceLine.getParent(), cr.getAmtSourceDr(), cr.getAmtAcctDr()); + MInvoice invoice = m_invoiceLine.getParent(); + if (!invList.contains(invoice)) + invList.add(invoice); + ArrayList factLineList = htFactLineInv.get(invoice.get_ID()); + if (factLineList == null) + factLineList = new ArrayList(); + factLineList.add(cr); + htFactLineInv.put(invoice.get_ID(), factLineList); + p_Error = createInvoiceGainLoss(as, fact, expense, invoice, cr.getAmtSourceDr(), cr.getAmtAcctDr(), invGainLossFactLines, htFactLineInv); + if (p_Error != null) + return null; + } + + // rounding adjustment + if (!htFactLineInv.isEmpty()) + { + p_Error = createInvoiceRoundingCorrection(as, fact, expense, invGainLossFactLines, invList, htFactLineInv); if (p_Error != null) return null; } @@ -1024,7 +1104,7 @@ public class Doc_MatchInv extends Doc BigDecimal debit = dr.getAmtSourceDr(); BigDecimal credit = cr.getAmtSourceCr(); - if (debit.compareTo(credit) == 0) { + if (debit.compareTo(credit) == 0 && (cr.getAcctBalance().add(dr.getAcctBalance())).compareTo(Env.ZERO) == 0) { fact.remove(dr); fact.remove(cr); } @@ -1076,7 +1156,8 @@ public class Doc_MatchInv extends Doc } private String createInvoiceGainLoss(MAcctSchema as, Fact fact, MAccount acct, - MInvoice invoice, BigDecimal matchInvSource, BigDecimal matchInvAccounted) + MInvoice invoice, BigDecimal matchInvSource, BigDecimal matchInvAccounted, + ArrayList invGainLossFactLines, HashMap> htFactLineInv) { BigDecimal invoiceSource = null; BigDecimal invoiceAccounted = null; @@ -1105,126 +1186,46 @@ public class Doc_MatchInv extends Doc if (invoiceSource == null || invoiceAccounted == null) return null; // - - if (m_matchInv.getReversal_ID() == 0) - { - String matchInvLineSql = "SELECT M_MatchInv_ID FROM M_MatchInv " - + "WHERE C_InvoiceLine_ID IN (SELECT C_InvoiceLine_ID FROM C_InvoiceLine WHERE C_Invoice_ID=?) " - + "AND COALESCE(Reversal_ID,0)=0"; - List> list = DB.getSQLArrayObjectsEx(getTrxName(), matchInvLineSql, invoice.get_ID()); - StringBuilder s = new StringBuilder(); - - if (list == null) - return null; - - for (int index=0; index < list.size(); index++) - { - List l = list.get(index); - s.append(l.get(0)); - if (index != list.size()-1) - s.append(","); - } - - sql = new StringBuilder() - .append("SELECT SUM(AmtSourceDr), SUM(AmtAcctDr), SUM(AmtSourceCr), SUM(AmtAcctCr)") - .append(" FROM Fact_Acct ") - .append("WHERE AD_Table_ID=? AND Record_ID IN (").append(s).append(")") - .append(" AND Record_ID <> ?") - .append(" AND C_AcctSchema_ID=?") - .append(" AND Account_ID=?") - .append(" AND PostingType='A'"); - } - else - { - sql = new StringBuilder() - .append("SELECT SUM(AmtSourceDr), SUM(AmtAcctDr), SUM(AmtSourceCr), SUM(AmtAcctCr)") - .append(" FROM Fact_Acct ") - .append("WHERE AD_Table_ID=? AND Record_ID IN (").append(m_matchInv.getReversal_ID()).append(")") - .append(" AND Record_ID <> ?") - .append(" AND C_AcctSchema_ID=?") - .append(" AND Account_ID=?") - .append(" AND PostingType='A'"); - } - - BigDecimal acctDifference = null; // gain is negative - // For Match Invoice - valuesInv = DB.getSQLValueObjectsEx(getTrxName(), sql.toString(), - MMatchInv.Table_ID, get_ID(), as.getC_AcctSchema_ID(), acct.getAccount_ID()); - if (valuesInv != null) - { - BigDecimal totalAmtSourceDr = (BigDecimal) valuesInv.get(0); - if (totalAmtSourceDr == null) - totalAmtSourceDr = Env.ZERO; - BigDecimal totalAmtAcctDr = (BigDecimal) valuesInv.get(1); - if (totalAmtAcctDr == null) - totalAmtAcctDr = Env.ZERO; - BigDecimal totalAmtSourceCr = (BigDecimal) valuesInv.get(2); - if (totalAmtSourceCr == null) - totalAmtSourceCr = Env.ZERO; - BigDecimal totalAmtAcctCr = (BigDecimal) valuesInv.get(3); - if (totalAmtAcctCr == null) - totalAmtAcctCr = Env.ZERO; - - if (m_matchInv.getReversal_ID() == 0) - { - if (totalAmtSourceDr.signum() == 0 && totalAmtAcctDr.signum() == 0) - { - matchInvSource = matchInvSource.add(totalAmtSourceCr); - matchInvAccounted = matchInvAccounted.add(totalAmtAcctCr); - } - else if (totalAmtSourceCr.signum() == 0 && totalAmtAcctCr.signum() == 0) - { - matchInvSource = matchInvSource.add(totalAmtSourceDr); - matchInvAccounted = matchInvAccounted.add(totalAmtAcctDr); - } - else if (totalAmtAcctDr.compareTo(totalAmtAcctCr) > 0) - { - matchInvSource = matchInvSource.add(totalAmtSourceDr); - matchInvAccounted = matchInvAccounted.add(totalAmtAcctDr).subtract(totalAmtAcctCr); - } - else - { - matchInvSource = matchInvSource.add(totalAmtSourceCr); - matchInvAccounted = matchInvAccounted.add(totalAmtAcctCr).subtract(totalAmtAcctDr); - } - } - else - { - if (totalAmtAcctDr.compareTo(totalAmtAcctCr) > 0) - { - matchInvSource = matchInvSource.add(totalAmtSourceDr); - matchInvAccounted = matchInvAccounted.add(totalAmtAcctDr); - acctDifference = totalAmtAcctCr; - } - else - { - matchInvSource = matchInvSource.add(totalAmtSourceCr); - matchInvAccounted = matchInvAccounted.add(totalAmtAcctCr); - acctDifference = totalAmtAcctDr.negate(); - } - } - } - + StringBuilder description = new StringBuilder("Invoice=(").append(invoice.getC_Currency_ID()).append(")").append(invoiceSource).append("/").append(invoiceAccounted) .append(" - MatchInv=(").append(getC_Currency_ID()).append(")").append(matchInvSource).append("/").append(matchInvAccounted); if (log.isLoggable(Level.FINE)) log.fine(description.toString()); // - // Full Payment in currency - if (acctDifference == null && matchInvSource.compareTo(invoiceSource) == 0) + BigDecimal acctDifference = null; + // Full MR in currency + if (matchInvSource.compareTo(invoiceSource) == 0) { - acctDifference = matchInvAccounted.subtract(invoiceAccounted.abs()); // gain is negative + acctDifference = matchInvAccounted.abs().subtract(invoiceAccounted.abs()); // gain is negative StringBuilder d2 = new StringBuilder("(full) = ").append(acctDifference); if (log.isLoggable(Level.FINE)) log.fine(d2.toString()); description.append(" - ").append(d2); } + else // partial or MC + { + BigDecimal matchInvAccounted0 = MConversionRate.convert(getCtx(), + matchInvSource, invoice.getC_Currency_ID(), + as.getC_Currency_ID(), invoice.getDateAcct(), + invoice.getC_ConversionType_ID(), invoice.getAD_Client_ID(), invoice.getAD_Org_ID()); + acctDifference = matchInvAccounted0.abs().subtract(matchInvAccounted.abs()); + // ignore Tolerance + if (acctDifference.abs().compareTo(TOLERANCE) < 0) + acctDifference = Env.ZERO; + // Round + int precision = as.getStdPrecision(); + if (acctDifference.scale() > precision) + acctDifference = acctDifference.setScale(precision, RoundingMode.HALF_UP); + StringBuilder d2 = new StringBuilder("(partial) = ").append(acctDifference); + if (log.isLoggable(Level.FINE)) log.fine(d2.toString()); + description.append(" - ").append(d2); + } - if (acctDifference == null || acctDifference.signum() == 0) + if (acctDifference.signum() == 0) { log.fine("No Difference"); return null; } - + MAccount gain = MAccount.get (as.getCtx(), as.getAcctSchemaDefault().getRealizedGain_Acct()); MAccount loss = MAccount.get (as.getCtx(), as.getAcctSchemaDefault().getRealizedLoss_Acct()); // @@ -1233,32 +1234,409 @@ public class Doc_MatchInv extends Doc FactLine fl = fact.createLine (null, acct, as.getC_Currency_ID(), acctDifference.negate()); fl.setDescription(description.toString()); updateFactLine(fl); - - if (as.isCurrencyBalancing() && as.getC_Currency_ID() != invoice.getC_Currency_ID()) { - fl = fact.createLine (null, as.getCurrencyBalancing_Acct(), as.getC_Currency_ID(), acctDifference); - } else { - fl = fact.createLine (null, loss, gain, as.getC_Currency_ID(), acctDifference); - } + ArrayList factLineList = htFactLineInv.get(invoice.get_ID()); + if (factLineList == null) + factLineList = new ArrayList(); + factLineList.add(fl); + htFactLineInv.put(invoice.get_ID(), factLineList); + + fl = fact.createLine (null, loss, gain, as.getC_Currency_ID(), acctDifference); fl.setDescription(description.toString()); updateFactLine(fl); + invGainLossFactLines.add(fl); } else { FactLine fl = fact.createLine (null, acct, as.getC_Currency_ID(), acctDifference); fl.setDescription(description.toString()); updateFactLine(fl); + ArrayList factLineList = htFactLineInv.get(invoice.get_ID()); + if (factLineList == null) + factLineList = new ArrayList(); + factLineList.add(fl); + htFactLineInv.put(invoice.get_ID(), factLineList); - if (as.isCurrencyBalancing() && as.getC_Currency_ID() != invoice.getC_Currency_ID()) { - fl = fact.createLine (null, as.getCurrencyBalancing_Acct(), as.getC_Currency_ID(), acctDifference.negate()); - } else { - fl = fact.createLine (null, loss, gain, as.getC_Currency_ID(), acctDifference.negate()); - } + fl = fact.createLine (null, loss, gain, as.getC_Currency_ID(), acctDifference.negate()); fl.setDescription(description.toString()); updateFactLine(fl); + invGainLossFactLines.add(fl); } return null; } // createInvoiceGainLoss + private String createInvoiceRoundingCorrection(MAcctSchema as, Fact fact, MAccount acct, + ArrayList invGainLossFactLines, ArrayList invList, HashMap> htFactLineInv) + { + // C_Invoice_ID and the total source amount from C_Invoice accounting fact lines + HashMap htInvSource = new HashMap(); + // C_Invoice_ID and the total accounted amount from C_Invoice accounting fact lines + HashMap htInvAccounted = new HashMap(); + for (MInvoice invoice : invList) + { + StringBuilder sql = new StringBuilder() + .append("SELECT SUM(AmtSourceDr), SUM(AmtAcctDr), SUM(AmtSourceCr), SUM(AmtAcctCr)") + .append(" FROM Fact_Acct ") + .append("WHERE AD_Table_ID=? AND Record_ID=?") + .append(" AND C_AcctSchema_ID=?") + .append(" AND Account_ID=?") + .append(" AND PostingType='A'"); + + // For Invoice + List valuesInv = DB.getSQLValueObjectsEx(getTrxName(), sql.toString(), + MInvoice.Table_ID, invoice.getC_Invoice_ID(), as.getC_AcctSchema_ID(), acct.getAccount_ID()); + if (valuesInv != null) { + BigDecimal invoiceSource = (BigDecimal) valuesInv.get(0); // AmtSourceDr + BigDecimal invoiceAccounted = (BigDecimal) valuesInv.get(1); // AmtAcctDr + if (invoiceSource.signum() == 0 && invoiceAccounted.signum() == 0) { + invoiceSource = (BigDecimal) valuesInv.get(2); // AmtSourceCr + invoiceAccounted = (BigDecimal) valuesInv.get(3); // AmtAcctCr + } + htInvSource.put(invoice.getC_Invoice_ID(), invoiceSource); + htInvAccounted.put(invoice.getC_Invoice_ID(), invoiceAccounted); + } + } + + MAccount gain = MAccount.get (as.getCtx(), as.getAcctSchemaDefault().getRealizedGain_Acct()); + MAccount loss = MAccount.get (as.getCtx(), as.getAcctSchemaDefault().getRealizedLoss_Acct()); + + // C_Invoice_ID and the total source DR amount from the current M_MatchInv accounting fact lines + HashMap htTotalAmtSourceDr = new HashMap(); + // C_Invoice_ID and the total accounted DR amount from the current M_MatchInv accounting fact lines + HashMap htTotalAmtAcctDr = new HashMap(); + // C_Invoice_ID and the total source CR amount from the current M_MatchInv accounting fact lines + HashMap htTotalAmtSourceCr = new HashMap(); + // C_Invoice_ID and the total accounted CR amount from the current M_MatchInv accounting fact lines + HashMap htTotalAmtAcctCr = new HashMap(); + for (Integer C_Invoice_ID : htFactLineInv.keySet()) + { + ArrayList factLineList = htFactLineInv.get(C_Invoice_ID); + for (FactLine factLine : factLineList) + { + if (factLine.getAccount_ID() == acct.getAccount_ID()) + { + BigDecimal totalAmtSourceDr = htTotalAmtSourceDr.get(C_Invoice_ID); + if (totalAmtSourceDr == null) + totalAmtSourceDr = Env.ZERO; + BigDecimal totalAmtAcctDr = htTotalAmtAcctDr.get(C_Invoice_ID); + if (totalAmtAcctDr == null) + totalAmtAcctDr = Env.ZERO; + BigDecimal totalAmtSourceCr = htTotalAmtSourceCr.get(C_Invoice_ID); + if (totalAmtSourceCr == null) + totalAmtSourceCr = Env.ZERO; + BigDecimal totalAmtAcctCr = htTotalAmtAcctCr.get(C_Invoice_ID); + if (totalAmtAcctCr == null) + totalAmtAcctCr = Env.ZERO; + + totalAmtSourceDr = totalAmtSourceDr.add(factLine.getAmtSourceDr()); + totalAmtAcctDr = totalAmtAcctDr.add(factLine.getAmtAcctDr()); + totalAmtSourceCr = totalAmtSourceCr.add(factLine.getAmtSourceCr()); + totalAmtAcctCr = totalAmtAcctCr.add(factLine.getAmtAcctCr()); + + htTotalAmtSourceDr.put(C_Invoice_ID, totalAmtSourceDr); + htTotalAmtAcctDr.put(C_Invoice_ID, totalAmtAcctDr); + htTotalAmtSourceCr.put(C_Invoice_ID, totalAmtSourceCr); + htTotalAmtAcctCr.put(C_Invoice_ID, totalAmtAcctCr); + } + else if (factLine.getAccount_ID() == gain.getAccount_ID() || factLine.getAccount_ID() == loss.getAccount_ID()) + { + if (!invGainLossFactLines.contains(factLine)) + continue; + + BigDecimal totalAmtSourceDr = htTotalAmtSourceDr.get(C_Invoice_ID); + if (totalAmtSourceDr == null) + totalAmtSourceDr = Env.ZERO; + BigDecimal totalAmtSourceCr = htTotalAmtSourceCr.get(C_Invoice_ID); + if (totalAmtSourceCr == null) + totalAmtSourceCr = Env.ZERO; + + totalAmtSourceDr = totalAmtSourceDr.subtract(factLine.getAmtSourceCr()); + totalAmtSourceCr = totalAmtSourceCr.subtract(factLine.getAmtSourceDr()); + + htTotalAmtSourceDr.put(C_Invoice_ID, totalAmtSourceDr); + htTotalAmtSourceCr.put(C_Invoice_ID, totalAmtSourceCr); + } + } + } + + // C_Invoice_ID and the total source amount from M_MatchInv accounting fact lines + HashMap htMatchInvSource = new HashMap(); + // C_Invoice_ID and the total accounted amount from M_MatchInv accounting fact lines + HashMap htMatchInvAccounted = new HashMap(); + // C_Invoice_ID and the total source amount from M_MatchInv accounting fact lines + HashMap htMatchInvAcctDiff = new HashMap(); + for (MInvoice invoice : invList) + { + BigDecimal matchInvSource = Env.ZERO; + BigDecimal matchInvAccounted = Env.ZERO; + + BigDecimal totalAmtSourceDr = htTotalAmtSourceDr.get(invoice.getC_Invoice_ID()); + if (totalAmtSourceDr == null) + totalAmtSourceDr = Env.ZERO; + BigDecimal totalAmtAcctDr = htTotalAmtAcctDr.get(invoice.getC_Invoice_ID()); + if (totalAmtAcctDr == null) + totalAmtAcctDr = Env.ZERO; + BigDecimal totalAmtSourceCr = htTotalAmtSourceCr.get(invoice.getC_Invoice_ID()); + if (totalAmtSourceCr == null) + totalAmtSourceCr = Env.ZERO; + BigDecimal totalAmtAcctCr = htTotalAmtAcctCr.get(invoice.getC_Invoice_ID()); + if (totalAmtAcctCr == null) + totalAmtAcctCr = Env.ZERO; + + if (totalAmtSourceDr.signum() == 0 && totalAmtAcctDr.signum() == 0) + { + matchInvSource = matchInvSource.add(totalAmtSourceCr); + matchInvAccounted = matchInvAccounted.add(totalAmtAcctCr); + } + else if (totalAmtSourceCr.signum() == 0 && totalAmtAcctCr.signum() == 0) + { + matchInvSource = matchInvSource.add(totalAmtSourceDr); + matchInvAccounted = matchInvAccounted.add(totalAmtAcctDr); + } + else + { + if (totalAmtAcctDr.compareTo(totalAmtAcctCr) > 0) + { + matchInvSource = matchInvSource.add(totalAmtSourceDr).subtract(totalAmtSourceCr); + matchInvAccounted = matchInvAccounted.add(totalAmtAcctDr).subtract(totalAmtAcctCr); + } + else + { + matchInvSource = matchInvSource.add(totalAmtSourceCr).subtract(totalAmtSourceDr); + matchInvAccounted = matchInvAccounted.add(totalAmtAcctCr).subtract(totalAmtAcctDr); + } + } + + if (m_matchInv.getReversal_ID() == 0) + { + MMatchInv[] matchInvs = MMatchInv.getInvoice(getCtx(), invoice.get_ID(), getTrxName()); + + ArrayList skipMatchInvIdList = new ArrayList(); + skipMatchInvIdList.add(m_matchInv.get_ID()); + for (MMatchInv matchInv : matchInvs) + { + if (matchInv.getReversal_ID() > 0) + skipMatchInvIdList.add(matchInv.get_ID()); + } + + for (MMatchInv matchInv : matchInvs) + { + if (matchInv.get_ID() == m_matchInv.get_ID()) + continue; + + if (skipMatchInvIdList.contains(matchInv.get_ID())) + continue; + + StringBuilder sql = new StringBuilder() + .append("SELECT SUM(AmtSourceDr), SUM(AmtAcctDr), SUM(AmtSourceCr), SUM(AmtAcctCr)") + .append(" FROM Fact_Acct ") + .append("WHERE AD_Table_ID=? AND (Record_ID=? OR Record_ID=?)") // match inv + .append(" AND C_AcctSchema_ID=?") + .append(" AND PostingType='A'") + .append(" AND Account_ID=?"); + + if (matchInv.getRef_MatchInv_ID() > 0) + { + if (invoice.isCreditMemo() && matchInv.getQty().compareTo(BigDecimal.ZERO) < 0) + sql.append(" AND Qty > 0"); + else + sql.append(" AND Qty < 0"); + } + + // For Match Inv + List valuesMatchInv = DB.getSQLValueObjectsEx(getTrxName(), sql.toString(), + MMatchInv.Table_ID, matchInv.get_ID(), matchInv.getRef_MatchInv_ID() > 0 ? matchInv.getRef_MatchInv_ID() : -1, as.getC_AcctSchema_ID(), acct.getAccount_ID()); + if (valuesMatchInv != null) { + totalAmtSourceDr = (BigDecimal) valuesMatchInv.get(0); + if (totalAmtSourceDr == null) + totalAmtSourceDr = Env.ZERO; + totalAmtAcctDr = (BigDecimal) valuesMatchInv.get(1); + if (totalAmtAcctDr == null) + totalAmtAcctDr = Env.ZERO; + totalAmtSourceCr = (BigDecimal) valuesMatchInv.get(2); + if (totalAmtSourceCr == null) + totalAmtSourceCr = Env.ZERO; + totalAmtAcctCr = (BigDecimal) valuesMatchInv.get(3); + if (totalAmtAcctCr == null) + totalAmtAcctCr = Env.ZERO; + + if (totalAmtSourceDr.signum() == 0 && totalAmtAcctDr.signum() == 0) + { + matchInvSource = matchInvSource.add(totalAmtSourceCr); + matchInvAccounted = matchInvAccounted.add(totalAmtAcctCr); + } + else if (totalAmtSourceCr.signum() == 0 && totalAmtAcctCr.signum() == 0) + { + matchInvSource = matchInvSource.add(totalAmtSourceDr); + matchInvAccounted = matchInvAccounted.add(totalAmtAcctDr); + } + else + { + if (totalAmtAcctDr.compareTo(totalAmtAcctCr) > 0) + { + matchInvSource = matchInvSource.add(totalAmtSourceDr); + matchInvAccounted = matchInvAccounted.add(totalAmtAcctDr); + } + else + { + matchInvSource = matchInvSource.add(totalAmtSourceCr); + matchInvAccounted = matchInvAccounted.add(totalAmtAcctCr); + } + } + } + + sql = new StringBuilder() + .append("SELECT SUM(AmtSourceDr), SUM(AmtAcctDr), SUM(AmtSourceCr), SUM(AmtAcctCr)") + .append(" FROM Fact_Acct ") + .append("WHERE AD_Table_ID=? AND (Record_ID=? OR Record_ID=?)") // match inv + .append(" AND C_AcctSchema_ID=?") + .append(" AND PostingType='A'") + .append(" AND (Account_ID=? OR Account_ID=? OR Account_ID=?)") + .append(" AND Description LIKE 'Invoice%'"); + + // For Match Inv + valuesMatchInv = DB.getSQLValueObjectsEx(getTrxName(), sql.toString(), + MMatchInv.Table_ID, matchInv.get_ID(), matchInv.getRef_MatchInv_ID() > 0 ? matchInv.getRef_MatchInv_ID() : -1, as.getC_AcctSchema_ID(), + gain.getAccount_ID(), loss.getAccount_ID(), as.getCurrencyBalancing_Acct().getAccount_ID()); + if (valuesMatchInv != null) { + totalAmtSourceDr = (BigDecimal) valuesMatchInv.get(0); + if (totalAmtSourceDr == null) + totalAmtSourceDr = Env.ZERO; + totalAmtAcctDr = (BigDecimal) valuesMatchInv.get(1); + if (totalAmtAcctDr == null) + totalAmtAcctDr = Env.ZERO; + totalAmtSourceCr = (BigDecimal) valuesMatchInv.get(2); + if (totalAmtSourceCr == null) + totalAmtSourceCr = Env.ZERO; + totalAmtAcctCr = (BigDecimal) valuesMatchInv.get(3); + if (totalAmtAcctCr == null) + totalAmtAcctCr = Env.ZERO; + + matchInvAccounted = matchInvAccounted.subtract(totalAmtAcctDr).subtract(totalAmtAcctCr); + } + } + + htMatchInvSource.put(invoice.getC_Invoice_ID(), matchInvSource); + htMatchInvAccounted.put(invoice.getC_Invoice_ID(), matchInvAccounted); + } + else + { + BigDecimal acctDifference = Env.ZERO; + StringBuilder sql = new StringBuilder() + .append("SELECT SUM(AmtSourceDr), SUM(AmtAcctDr), SUM(AmtSourceCr), SUM(AmtAcctCr)") + .append(" FROM Fact_Acct ") + .append("WHERE AD_Table_ID=? AND Record_ID IN (").append(m_matchInv.getReversal_ID()).append(")") + .append(" AND Record_ID <> ?") + .append(" AND C_AcctSchema_ID=?") + .append(" AND PostingType='A'") + .append(" AND Account_ID=?"); + + // For Match Inv + List valuesMatchInv = DB.getSQLValueObjectsEx(getTrxName(), sql.toString(), + MMatchInv.Table_ID, get_ID(), as.getC_AcctSchema_ID(), acct.getAccount_ID()); + if (valuesMatchInv != null) { + totalAmtSourceDr = (BigDecimal) valuesMatchInv.get(0); + if (totalAmtSourceDr == null) + totalAmtSourceDr = Env.ZERO; + totalAmtAcctDr = (BigDecimal) valuesMatchInv.get(1); + if (totalAmtAcctDr == null) + totalAmtAcctDr = Env.ZERO; + totalAmtSourceCr = (BigDecimal) valuesMatchInv.get(2); + if (totalAmtSourceCr == null) + totalAmtSourceCr = Env.ZERO; + totalAmtAcctCr = (BigDecimal) valuesMatchInv.get(3); + if (totalAmtAcctCr == null) + totalAmtAcctCr = Env.ZERO; + + if (totalAmtAcctDr.compareTo(totalAmtAcctCr) > 0) + { + matchInvSource = matchInvSource.add(totalAmtSourceDr); + matchInvAccounted = matchInvAccounted.add(totalAmtAcctDr); + acctDifference = totalAmtAcctCr.negate(); + } + else + { + matchInvSource = matchInvSource.add(totalAmtSourceCr); + matchInvAccounted = matchInvAccounted.add(totalAmtAcctCr); + acctDifference = totalAmtAcctDr; + } + } + + htMatchInvSource.put(invoice.getC_Invoice_ID(), matchInvSource); + htMatchInvAccounted.put(invoice.getC_Invoice_ID(), matchInvAccounted); + htMatchInvAcctDiff.put(invoice.getC_Invoice_ID(), acctDifference); + } + } + + for (MInvoice invoice : invList) + { + BigDecimal invSource = htInvSource.get(invoice.getC_Invoice_ID()); + if (invSource == null) + invSource = Env.ZERO; + BigDecimal invAccounted = htInvAccounted.get(invoice.getC_Invoice_ID()); + if (invAccounted == null) + invAccounted = Env.ZERO; + BigDecimal matchInvSource = htMatchInvSource.get(invoice.getC_Invoice_ID()); + if (matchInvSource == null) + matchInvSource = Env.ZERO; + BigDecimal matchInvAccounted = htMatchInvAccounted.get(invoice.getC_Invoice_ID()); + if (matchInvAccounted == null) + matchInvAccounted = Env.ZERO; + BigDecimal acctDifference = htMatchInvAcctDiff.get(invoice.getC_Invoice_ID()); + + StringBuilder description = new StringBuilder("Invoice=(").append(getC_Currency_ID()).append(")").append(invSource).append("/").append(invAccounted) + .append(" - Match Invoice=(").append(getC_Currency_ID()).append(")").append(matchInvSource).append("/").append(matchInvAccounted); + if (log.isLoggable(Level.FINE)) log.fine(description.toString()); + if (acctDifference == null && matchInvSource.abs().compareTo(invSource.abs()) == 0) + { + acctDifference = invAccounted.abs().subtract(matchInvAccounted.abs()); // gain is negative + StringBuilder d2 = new StringBuilder("(full) = ").append(acctDifference); + if (log.isLoggable(Level.FINE)) log.fine(d2.toString()); + description.append(" - ").append(d2); + } + + if (acctDifference == null || acctDifference.signum() == 0) + { + log.fine("No Difference"); + continue; + } + + if (acctDifference.abs().compareTo(TOLERANCE) > 0) + { + log.fine("acctDifference="+acctDifference); + continue; + } + + // + if (invoice.isSOTrx()) + { + FactLine fl = fact.createLine (null, acct, as.getC_Currency_ID(), acctDifference); + fl.setDescription(description.toString()); + updateFactLine(fl); + + if (as.isCurrencyBalancing() && as.getC_Currency_ID() != invoice.getC_Currency_ID()) + fl = fact.createLine (null, as.getCurrencyBalancing_Acct(), as.getC_Currency_ID(), acctDifference.negate()); + else + fl = fact.createLine (null, loss, gain, as.getC_Currency_ID(), acctDifference.negate()); + fl.setDescription(description.toString()); + updateFactLine(fl); + } + else + { + FactLine fl = fact.createLine (null, acct, as.getC_Currency_ID(), acctDifference.negate()); + fl.setDescription(description.toString()); + updateFactLine(fl); + + if (as.isCurrencyBalancing() && as.getC_Currency_ID() != invoice.getC_Currency_ID()) + fl = fact.createLine (null, as.getCurrencyBalancing_Acct(), as.getC_Currency_ID(), acctDifference); + else + fl = fact.createLine (null, loss, gain, as.getC_Currency_ID(), acctDifference); + fl.setDescription(description.toString()); + updateFactLine(fl); + } + } + return null; + } + private String createReceiptGainLoss(MAcctSchema as, Fact fact, MAccount acct, MInOut receipt, BigDecimal matchInvSource, BigDecimal matchInvAccounted) { 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 c86f71da78..af9dcd60b3 100644 --- a/org.idempiere.test/src/org/idempiere/test/base/MatchInvTest.java +++ b/org.idempiere.test/src/org/idempiere/test/base/MatchInvTest.java @@ -339,4 +339,185 @@ public class MatchInvTest extends AbstractTestCase { rollback(); } + + @Test + /** + * Test the matched invoice posting for credit memo + * PO Qty=10 > IV Qty=10 > MR Qty=9 > CM Qty=1 + */ + public void testCreditMemoPosting() { + MBPartner bpartner = MBPartner.get(Env.getCtx(), 114); // Tree Farm Inc. + MProduct product = MProduct.get(Env.getCtx(), 124); // Elm Tree + + MOrder order = new MOrder(Env.getCtx(), 0, getTrxName()); + order.setBPartner(bpartner); + order.setIsSOTrx(false); + order.setC_DocTypeTarget_ID(); + 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()); + assertEquals(DocAction.STATUS_Completed, order.getDocStatus()); + + MInvoice invoice = new MInvoice(Env.getCtx(), 0, getTrxName()); + invoice.setOrder(order); + invoice.setDateAcct(order.getDateOrdered()); + invoice.setSalesRep_ID(order.getSalesRep_ID()); + invoice.setC_BPartner_ID(order.getBill_BPartner_ID()); + invoice.setC_BPartner_Location_ID(order.getBill_Location_ID()); + invoice.setAD_User_ID(order.getBill_User_ID()); + 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.setC_OrderLine_ID(orderLine.get_ID()); + invoiceLine.setLine(10); + invoiceLine.setProduct(product); + invoiceLine.setQty(BigDecimal.TEN); + invoiceLine.saveEx(); + + info = MWorkflow.runDocumentActionWorkflow(invoice, DocAction.ACTION_Complete); + invoice.load(getTrxName()); + assertFalse(info.isError()); + 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()); + + 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(new BigDecimal(9)); + 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()); + 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()); + + int C_AcctSchema_ID = MClientInfo.get(Env.getCtx()).getC_AcctSchema1_ID(); + MAcctSchema as = MAcctSchema.get(Env.getCtx(), C_AcctSchema_ID); + 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()); + + 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); + + String whereClause = 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, 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); + else if (fa.getAccount_ID() == acctInvClr.getAccount_ID()) + assertTrue(fa.getAmtAcctCr().compareTo(Env.ZERO) >= 0); + } + } + + MInvoice creditMemo = new MInvoice(Env.getCtx(), 0, getTrxName()); + creditMemo.setOrder(order); + creditMemo.setDateAcct(order.getDateOrdered()); + creditMemo.setSalesRep_ID(order.getSalesRep_ID()); + creditMemo.setC_BPartner_ID(order.getBill_BPartner_ID()); + creditMemo.setC_BPartner_Location_ID(order.getBill_Location_ID()); + creditMemo.setAD_User_ID(order.getBill_User_ID()); + creditMemo.setC_DocTypeTarget_ID(MDocType.DOCBASETYPE_APCreditMemo); + creditMemo.setDocStatus(DocAction.STATUS_Drafted); + creditMemo.setDocAction(DocAction.ACTION_Complete); + creditMemo.saveEx(); + + MInvoiceLine creditMemoLine = new MInvoiceLine(creditMemo); + creditMemoLine.setC_OrderLine_ID(orderLine.get_ID()); + creditMemoLine.setLine(10); + creditMemoLine.setProduct(product); + creditMemoLine.setQty(BigDecimal.ONE); + creditMemoLine.saveEx(); + + info = MWorkflow.runDocumentActionWorkflow(creditMemo, DocAction.ACTION_Complete); + creditMemo.load(getTrxName()); + assertFalse(info.isError()); + assertEquals(DocAction.STATUS_Completed, creditMemo.getDocStatus()); + + if (!creditMemo.isPosted()) { + String error = DocumentEngine.postImmediate(Env.getCtx(), creditMemo.getAD_Client_ID(), MInvoice.Table_ID, creditMemo.get_ID(), false, getTrxName()); + assertTrue(error == null); + } + creditMemo.load(getTrxName()); + assertTrue(creditMemo.isPosted()); + + miList = MMatchInv.getInvoiceLine(Env.getCtx(), creditMemoLine.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()); + + 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 + "=" + C_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) { + assertTrue(fa.getAmtAcctCr().compareTo(Env.ZERO) >= 0); + amtAcctCrInvClr = amtAcctCrInvClr.add(fa.getAmtAcctCr()); + } + else if (fa.getAccount_ID() == acctInvClr.getAccount_ID() && fa.getQty().compareTo(BigDecimal.ZERO) > 0) { + assertTrue(fa.getAmtAcctDr().compareTo(Env.ZERO) >= 0); + amtAcctDrInvClr = amtAcctDrInvClr.add(fa.getAmtAcctDr()); + } + } + assertTrue(amtAcctDrInvClr.compareTo(amtAcctCrInvClr) == 0); + } + + rollback(); + } }