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 f73c14df2c..3e6b8ac9ec 100644 --- a/org.adempiere.base/src/org/compiere/acct/Doc_AllocationHdr.java +++ b/org.adempiere.base/src/org/compiere/acct/Doc_AllocationHdr.java @@ -510,6 +510,9 @@ public class Doc_AllocationHdr extends Doc fact.remove(factline); } } + + if (getC_Currency_ID() != as.getC_Currency_ID()) + balanceAccounting(as, fact); // reset line info setC_BPartner_ID(0); @@ -1718,6 +1721,37 @@ public class Doc_AllocationHdr extends Doc } return null; } + + /** + * Balance Accounting + * @param as accounting schema + * @param fact + * @return + */ + private FactLine balanceAccounting(MAcctSchema as, Fact fact) + { + FactLine line = null; + if (!fact.isAcctBalanced()) + { + MAccount gain = MAccount.get(as.getCtx(), as.getAcctSchemaDefault().getRealizedGain_Acct()); + MAccount loss = MAccount.get(as.getCtx(), as.getAcctSchemaDefault().getRealizedLoss_Acct()); + + BigDecimal totalAmtAcctDr = Env.ZERO; + BigDecimal totalAmtAcctCr = Env.ZERO; + for (FactLine factLine : fact.getLines()) + { + totalAmtAcctDr = totalAmtAcctDr.add(factLine.getAmtAcctDr()); + totalAmtAcctCr = totalAmtAcctCr.add(factLine.getAmtAcctCr()); + } + + BigDecimal acctDifference = totalAmtAcctDr.subtract(totalAmtAcctCr); + if (as.isCurrencyBalancing() && acctDifference.abs().compareTo(TOLERANCE) < 0) + line = fact.createLine (null, as.getCurrencyBalancing_Acct(), as.getC_Currency_ID(), acctDifference.negate()); + else + line = fact.createLine(null, loss, gain, as.getC_Currency_ID(), acctDifference.negate()); + } + return line; + } } // Doc_Allocation /** 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 d4395aebb0..c08058f33e 100644 --- a/org.idempiere.test/src/org/idempiere/test/model/AllocationTest.java +++ b/org.idempiere.test/src/org/idempiere/test/model/AllocationTest.java @@ -30,17 +30,29 @@ import static org.junit.jupiter.api.Assertions.assertTrue; import java.math.BigDecimal; import java.sql.Timestamp; +import java.util.Calendar; import java.util.Properties; import java.util.logging.LogRecord; +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.MAllocationLine; import org.compiere.model.MBPartner; +import org.compiere.model.MBankAccount; +import org.compiere.model.MConversionRate; +import org.compiere.model.MCurrency; import org.compiere.model.MDocType; +import org.compiere.model.MFactAcct; import org.compiere.model.MInvoice; import org.compiere.model.MInvoiceLine; import org.compiere.model.MPayment; +import org.compiere.model.PO; +import org.compiere.model.Query; import org.compiere.process.DocAction; +import org.compiere.process.DocumentEngine; import org.compiere.process.ProcessInfo; import org.compiere.util.CLogErrorBuffer; import org.compiere.util.Env; @@ -333,4 +345,268 @@ public class AllocationTest extends AbstractTestCase { rollback(); } + @Test + /** + * https://idempiere.atlassian.net/browse/IDEMPIERE-4696 + */ + public void testPaymentReversePosting() { + MBPartner bpartner = MBPartner.get(Env.getCtx(), 114); // Tree Farm Inc. + Timestamp currentDate = Env.getContextAsDate(Env.getCtx(), "#Date"); + + Calendar cal = Calendar.getInstance(); + cal.setTimeInMillis(currentDate.getTime()); + cal.add(Calendar.DAY_OF_MONTH, -1); + Timestamp date1 = new Timestamp(cal.getTimeInMillis()); + Timestamp date2 = currentDate; + + int C_ConversionType_ID = 201; // Company + + MCurrency usd = MCurrency.get(100); // USD + MCurrency euro = MCurrency.get("EUR"); // EUR + BigDecimal eurToUsd1 = new BigDecimal(30); + MConversionRate cr1 = createConversionRate(usd.getC_Currency_ID(), euro.getC_Currency_ID(), C_ConversionType_ID, date1, eurToUsd1, false); + + BigDecimal eurToUsd2 = new BigDecimal(31); + MConversionRate cr2 = createConversionRate(usd.getC_Currency_ID(), euro.getC_Currency_ID(), C_ConversionType_ID, date2, eurToUsd2, false); + + try { + String whereClause = "AD_Org_ID=? AND C_Currency_ID=?"; + MBankAccount ba = new Query(Env.getCtx(),MBankAccount.Table_Name, whereClause, getTrxName()) + .setParameters(Env.getAD_Org_ID(Env.getCtx()), usd.getC_Currency_ID()) + .setOrderBy("IsDefault DESC") + .first(); + assertTrue(ba != null, "@NoAccountOrgCurrency@"); + + BigDecimal payAmt = new BigDecimal(1000); + MPayment payment = createReceiptPayment(bpartner.getC_BPartner_ID(), ba.getC_BankAccount_ID(), date1, euro.getC_Currency_ID(), C_ConversionType_ID, payAmt); + completeDocument(payment); + postDocument(payment); + + reverseAccrualDocument(payment); + MPayment reversalPayment = new MPayment(Env.getCtx(), payment.getReversal_ID(), getTrxName()); + postDocument(reversalPayment); + + MAllocationHdr[] allocations = MAllocationHdr.getOfPayment(Env.getCtx(), payment.getC_Payment_ID(), getTrxName()); + assertTrue(allocations.length == 1); + + MAllocationHdr allocation = allocations[0]; + postDocument(allocation); + + MAcctSchema[] ass = MAcctSchema.getClientAcctSchema(Env.getCtx(), Env.getAD_Client_ID(Env.getCtx())); + for (MAcctSchema as : ass) { + if (as.getC_Currency_ID() != usd.getC_Currency_ID()) + continue; + + Doc doc = DocManager.getDocument(as, MAllocationHdr.Table_ID, allocation.get_ID(), getTrxName()); + doc.setC_BankAccount_ID(ba.getC_BankAccount_ID()); + MAccount acctUC = doc.getAccount(Doc.ACCTTYPE_UnallocatedCash, as); + MAccount acctLoss = MAccount.get(as.getCtx(), as.getAcctSchemaDefault().getRealizedLoss_Acct()); + BigDecimal ucAmtAcctDr = new BigDecimal(30000); + 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()); + if (acctUC.getAccount_ID() == fa.getAccount_ID()) { + if (fa.getAmtAcctDr().signum() > 0) + assertTrue(fa.getAmtAcctDr().compareTo(ucAmtAcctDr) == 0, fa.getAmtAcctDr().toPlainString() + "!=" + ucAmtAcctDr.toPlainString()); + else if (fa.getAmtAcctDr().signum() < 0) + assertTrue(fa.getAmtAcctDr().compareTo(ucAmtAcctCr.negate()) == 0, fa.getAmtAcctDr().toPlainString() + "!=" + ucAmtAcctCr.negate().toPlainString()); + else if (fa.getAmtAcctCr().signum() > 0) + assertTrue(fa.getAmtAcctCr().compareTo(ucAmtAcctCr) == 0, fa.getAmtAcctCr().toPlainString() + "!=" + ucAmtAcctCr.toPlainString()); + } + else if (acctLoss.getAccount_ID() == fa.getAccount_ID()) + assertTrue(fa.getAmtAcctDr().compareTo(lossAmtAcct) == 0, fa.getAmtAcctDr().toPlainString() + "!=" + lossAmtAcct.toPlainString()); + } + } + + } finally { + deleteConversionRate(cr1); + deleteConversionRate(cr2); + + rollback(); + } + } + + @Test + public void testAllocatePaymentPosting() { + MBPartner bpartner = MBPartner.get(Env.getCtx(), 114); // Tree Farm Inc. + Timestamp currentDate = Env.getContextAsDate(Env.getCtx(), "#Date"); + + Calendar cal = Calendar.getInstance(); + cal.setTimeInMillis(currentDate.getTime()); + cal.add(Calendar.DAY_OF_MONTH, -1); + Timestamp date1 = new Timestamp(cal.getTimeInMillis()); + Timestamp date2 = currentDate; + + int C_ConversionType_ID = 201; // Company + + MCurrency usd = MCurrency.get(100); // USD + MCurrency euro = MCurrency.get("EUR"); // EUR + BigDecimal eurToUsd1 = new BigDecimal(30); + MConversionRate cr1 = createConversionRate(usd.getC_Currency_ID(), euro.getC_Currency_ID(), C_ConversionType_ID, date1, eurToUsd1, false); + + BigDecimal eurToUsd2 = new BigDecimal(31); + MConversionRate cr2 = createConversionRate(usd.getC_Currency_ID(), euro.getC_Currency_ID(), C_ConversionType_ID, date2, eurToUsd2, false); + + try { + String whereClause = "AD_Org_ID=? AND C_Currency_ID=?"; + MBankAccount ba = new Query(Env.getCtx(),MBankAccount.Table_Name, whereClause, getTrxName()) + .setParameters(Env.getAD_Org_ID(Env.getCtx()), usd.getC_Currency_ID()) + .setOrderBy("IsDefault DESC") + .first(); + assertTrue(ba != null, "@NoAccountOrgCurrency@"); + + BigDecimal payAmt = new BigDecimal(1000); + MPayment payment1 = createReceiptPayment(bpartner.getC_BPartner_ID(), ba.getC_BankAccount_ID(), date1, euro.getC_Currency_ID(), C_ConversionType_ID, payAmt); + completeDocument(payment1); + postDocument(payment1); + + MPayment payment2 = createReceiptPayment(bpartner.getC_BPartner_ID(), ba.getC_BankAccount_ID(), date2, euro.getC_Currency_ID(), C_ConversionType_ID, payAmt.negate()); + completeDocument(payment2); + postDocument(payment2); + + MAllocationHdr alloc = new MAllocationHdr(Env.getCtx(), true, date2, euro.getC_Currency_ID(), Env.getContext(Env.getCtx(), Env.AD_USER_NAME), getTrxName()); + alloc.setAD_Org_ID(payment2.getAD_Org_ID()); + int doctypeAlloc = MDocType.getDocType("CMA"); + alloc.setC_DocType_ID(doctypeAlloc); + alloc.setDescription(alloc.getDescriptionForManualAllocation(payment2.getC_BPartner_ID(), getTrxName())); + alloc.saveEx(); + + MAllocationLine aLine1 = new MAllocationLine(alloc, payment1.getPayAmt(), Env.ZERO, Env.ZERO, Env.ZERO); + aLine1.setDocInfo(payment1.getC_BPartner_ID(), 0, 0); + aLine1.setPaymentInfo(payment1.getC_Payment_ID(), 0); + aLine1.saveEx(); + + MAllocationLine aLine2 = new MAllocationLine(alloc, payment2.getPayAmt(), Env.ZERO, Env.ZERO, Env.ZERO); + aLine2.setDocInfo(payment2.getC_BPartner_ID(), 0, 0); + aLine2.setPaymentInfo(payment2.getC_Payment_ID(), 0); + aLine2.saveEx(); + + completeDocument(alloc); + postDocument(alloc); + + MAllocationHdr[] allocations = MAllocationHdr.getOfPayment(Env.getCtx(), payment1.getC_Payment_ID(), getTrxName()); + assertTrue(allocations.length == 1); + + MAllocationHdr allocation = allocations[0]; + postDocument(allocation); + + MAcctSchema[] ass = MAcctSchema.getClientAcctSchema(Env.getCtx(), Env.getAD_Client_ID(Env.getCtx())); + for (MAcctSchema as : ass) { + if (as.getC_Currency_ID() != usd.getC_Currency_ID()) + continue; + + Doc doc = DocManager.getDocument(as, MAllocationHdr.Table_ID, allocation.get_ID(), getTrxName()); + doc.setC_BankAccount_ID(ba.getC_BankAccount_ID()); + MAccount acctUC = doc.getAccount(Doc.ACCTTYPE_UnallocatedCash, as); + MAccount acctLoss = MAccount.get(as.getCtx(), as.getAcctSchemaDefault().getRealizedLoss_Acct()); + BigDecimal ucAmtAcctDr = new BigDecimal(30000); + 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()); + if (acctUC.getAccount_ID() == fa.getAccount_ID()) { + if (fa.getAmtAcctDr().signum() > 0) + assertTrue(fa.getAmtAcctDr().compareTo(ucAmtAcctDr) == 0, fa.getAmtAcctDr().toPlainString() + "!=" + ucAmtAcctDr.toPlainString()); + else if (fa.getAmtAcctDr().signum() < 0) + assertTrue(fa.getAmtAcctDr().compareTo(ucAmtAcctCr.negate()) == 0, fa.getAmtAcctDr().toPlainString() + "!=" + ucAmtAcctCr.negate().toPlainString()); + else if (fa.getAmtAcctCr().signum() > 0) + assertTrue(fa.getAmtAcctCr().compareTo(ucAmtAcctCr) == 0, fa.getAmtAcctCr().toPlainString() + "!=" + ucAmtAcctCr.toPlainString()); + } + else if (acctLoss.getAccount_ID() == fa.getAccount_ID()) + assertTrue(fa.getAmtAcctDr().compareTo(lossAmtAcct) == 0, fa.getAmtAcctDr().toPlainString() + "!=" + lossAmtAcct.toPlainString()); + } + } + + } finally { + deleteConversionRate(cr1); + deleteConversionRate(cr2); + + rollback(); + } + } + + private MConversionRate createConversionRate(int C_Currency_ID, int C_Currency_ID_To, int C_ConversionType_ID, + Timestamp date, BigDecimal rate, boolean isMultiplyRate) { + MConversionRate cr = new MConversionRate(Env.getCtx(), 0, null); + cr.setC_Currency_ID(C_Currency_ID); + cr.setC_Currency_ID_To(C_Currency_ID_To); + cr.setC_ConversionType_ID(C_ConversionType_ID); + cr.setValidFrom(date); + cr.setValidTo(date); + if (isMultiplyRate) + cr.setMultiplyRate(rate); + else + cr.setDivideRate(rate); + cr.saveEx(); + return cr; + } + + private void deleteConversionRate(MConversionRate cr) { + String whereClause = "ValidFrom=? AND ValidTo=? " + + "AND C_Currency_ID=? AND C_Currency_ID_To=? " + + "AND C_ConversionType_ID=? " + + "AND AD_Client_ID=? AND AD_Org_ID=?"; + MConversionRate reciprocal = new Query(Env.getCtx(), MConversionRate.Table_Name, whereClause, null) + .setParameters(cr.getValidFrom(), cr.getValidTo(), + cr.getC_Currency_ID_To(), cr.getC_Currency_ID(), + cr.getC_ConversionType_ID(), + cr.getAD_Client_ID(), cr.getAD_Org_ID()) + .firstOnly(); + if (reciprocal != null) + reciprocal.deleteEx(true); + cr.deleteEx(true); + } + + private MPayment createReceiptPayment(int C_BPartner_ID, int C_BankAccount_ID, Timestamp date, int C_Currency_ID, int C_ConversionType_ID, BigDecimal payAmt) { + MPayment payment = new MPayment(Env.getCtx(), 0, getTrxName()); + payment.setC_BankAccount_ID(C_BankAccount_ID); + payment.setC_DocType_ID(true); + payment.setDateTrx(date); + payment.setDateAcct(date); + payment.setC_BPartner_ID(C_BPartner_ID); + payment.setPayAmt(payAmt); + payment.setC_Currency_ID(C_Currency_ID); + payment.setC_ConversionType_ID(C_ConversionType_ID); + payment.setTenderType(MPayment.TENDERTYPE_Check); + payment.setDocStatus(DocAction.STATUS_Drafted); + payment.setDocAction(DocAction.ACTION_Complete); + payment.saveEx(); + return payment; + } + + private void completeDocument(PO po) { + ProcessInfo info = MWorkflow.runDocumentActionWorkflow(po, DocAction.ACTION_Complete); + po.load(getTrxName()); + assertFalse(info.isError(), info.getSummary()); + String docStatus = (String) po.get_Value("DocStatus"); + assertEquals(DocAction.STATUS_Completed, docStatus, DocAction.STATUS_Completed + " != " + docStatus); + } + + private void reverseAccrualDocument(PO po) { + ProcessInfo info = MWorkflow.runDocumentActionWorkflow(po, DocAction.ACTION_Reverse_Accrual); + po.load(getTrxName()); + assertFalse(info.isError(), info.getSummary()); + String docStatus = (String) po.get_Value("DocStatus"); + assertEquals(DocAction.STATUS_Reversed, docStatus, DocAction.STATUS_Reversed + " != " + docStatus); + } + + private void postDocument(PO po) { + if (!po.get_ValueAsBoolean("Posted")) { + String error = DocumentEngine.postImmediate(Env.getCtx(), po.getAD_Client_ID(), po.get_Table_ID(), po.get_ID(), false, getTrxName()); + assertTrue(error == null, error); + } + po.load(getTrxName()); + assertTrue(po.get_ValueAsBoolean("Posted")); + } }