IDEMPIERE-4696 Wrong GL Currency Balancing in payment allocation for payment reverse/accrual (#621)

* IDEMPIERE-4696 Wrong GL Currency Balancing in payment allocation for payment reverse/accrual
This commit is contained in:
Elaine Tan 2021-03-15 21:18:25 +08:00 committed by GitHub
parent 61478ad594
commit 27f21e0384
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 310 additions and 0 deletions

View File

@ -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
/**

View File

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