feat(savings): support rounding adjustment GL mapping and floor rounding on clerk closure
Fineract E2E Tests / E2E Tests (Shard 8 of 10) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 9 of 10) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MySQL / test (test-core-4) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MySQL / test (test-core-5) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - PostgreSQL / test (test-core-1) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - PostgreSQL / test (test-core-2) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - PostgreSQL / test (test-core-3) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - PostgreSQL / test (test-core-4) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - PostgreSQL / test (test-core-5) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 2 of 10) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 3 of 10) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 4 of 10) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 5 of 10) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 6 of 10) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 7 of 10) (push) Canceled after 0s
Fineract Build & Cucumber tests (without E2E tests) / verify (progressive-loan, build-progressive-loan) (push) Canceled after 0s
Fineract Docker Builds / build (true, docker-compose.yml, mariadb) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MySQL / test (test-core-2) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MySQL / test (test-core-3) (push) Canceled after 0s
Fineract Liquibase Only mode - PostgreSQL / test (push) Canceled after 0s
Fineract Messaging Smoke Tests / Smoke Test with ActiveMQ (push) Canceled after 0s
Fineract Messaging Smoke Tests / Smoke Test with Kafka (push) Canceled after 0s
Fineract Build & Cucumber tests (without E2E tests) / verify (main, build-core) (push) Canceled after 0s
Fineract Build & Cucumber tests (without E2E tests) / verify (main, cucumber) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 1 of 10) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 10 of 10) (push) Canceled after 0s
Fineract Docker Builds / build (false, docker-compose-postgresql.yml, postgresql) (push) Canceled after 0s
Fineract Documentation build / build (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MariaDB / test (test-core-1) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MariaDB / test (test-core-2) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MariaDB / test (test-core-3) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MariaDB / test (test-core-4) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MariaDB / test (test-core-5) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MySQL / test (test-core-1) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 8 of 10) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 9 of 10) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MySQL / test (test-core-4) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MySQL / test (test-core-5) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - PostgreSQL / test (test-core-1) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - PostgreSQL / test (test-core-2) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - PostgreSQL / test (test-core-3) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - PostgreSQL / test (test-core-4) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - PostgreSQL / test (test-core-5) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 2 of 10) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 3 of 10) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 4 of 10) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 5 of 10) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 6 of 10) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 7 of 10) (push) Canceled after 0s
Fineract Build & Cucumber tests (without E2E tests) / verify (progressive-loan, build-progressive-loan) (push) Canceled after 0s
Fineract Docker Builds / build (true, docker-compose.yml, mariadb) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MySQL / test (test-core-2) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MySQL / test (test-core-3) (push) Canceled after 0s
Fineract Liquibase Only mode - PostgreSQL / test (push) Canceled after 0s
Fineract Messaging Smoke Tests / Smoke Test with ActiveMQ (push) Canceled after 0s
Fineract Messaging Smoke Tests / Smoke Test with Kafka (push) Canceled after 0s
Fineract Build & Cucumber tests (without E2E tests) / verify (main, build-core) (push) Canceled after 0s
Fineract Build & Cucumber tests (without E2E tests) / verify (main, cucumber) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 1 of 10) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 10 of 10) (push) Canceled after 0s
Fineract Docker Builds / build (false, docker-compose-postgresql.yml, postgresql) (push) Canceled after 0s
Fineract Documentation build / build (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MariaDB / test (test-core-1) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MariaDB / test (test-core-2) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MariaDB / test (test-core-3) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MariaDB / test (test-core-4) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MariaDB / test (test-core-5) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MySQL / test (test-core-1) (push) Canceled after 0s
This commit is contained in:
+4
@@ -426,6 +426,8 @@ public class ProductToGLAccountMappingReadPlatformServiceImpl implements Product
|
|||||||
accountMappingDetails.put(SavingProductAccountingDataParams.INCOME_FROM_INTEREST.getValue(), glAccountData);
|
accountMappingDetails.put(SavingProductAccountingDataParams.INCOME_FROM_INTEREST.getValue(), glAccountData);
|
||||||
} else if (glAccountForSavings.equals(AccrualAccountsForSavings.ESCHEAT_LIABILITY)) {
|
} else if (glAccountForSavings.equals(AccrualAccountsForSavings.ESCHEAT_LIABILITY)) {
|
||||||
accountMappingDetails.put(SavingProductAccountingDataParams.ESCHEAT_LIABILITY.getValue(), glAccountData);
|
accountMappingDetails.put(SavingProductAccountingDataParams.ESCHEAT_LIABILITY.getValue(), glAccountData);
|
||||||
|
} else if (glAccountForSavings.equals(AccrualAccountsForSavings.ROUNDING_ADJUSTMENT)) {
|
||||||
|
accountMappingDetails.put(SavingProductAccountingDataParams.ROUNDING_ADJUSTMENT.getValue(), glAccountData);
|
||||||
// Expense
|
// Expense
|
||||||
} else if (glAccountForSavings.equals(AccrualAccountsForSavings.INTEREST_ON_SAVINGS)) {
|
} else if (glAccountForSavings.equals(AccrualAccountsForSavings.INTEREST_ON_SAVINGS)) {
|
||||||
accountMappingDetails.put(SavingProductAccountingDataParams.INTEREST_ON_SAVINGS.getValue(), glAccountData);
|
accountMappingDetails.put(SavingProductAccountingDataParams.INTEREST_ON_SAVINGS.getValue(), glAccountData);
|
||||||
@@ -470,6 +472,8 @@ public class ProductToGLAccountMappingReadPlatformServiceImpl implements Product
|
|||||||
accountMappingDetails.put(SavingProductAccountingDataParams.INCOME_FROM_INTEREST.getValue(), glAccountData);
|
accountMappingDetails.put(SavingProductAccountingDataParams.INCOME_FROM_INTEREST.getValue(), glAccountData);
|
||||||
} else if (glAccountForSavings.equals(CashAccountsForSavings.ESCHEAT_LIABILITY)) {
|
} else if (glAccountForSavings.equals(CashAccountsForSavings.ESCHEAT_LIABILITY)) {
|
||||||
accountMappingDetails.put(SavingProductAccountingDataParams.ESCHEAT_LIABILITY.getValue(), glAccountData);
|
accountMappingDetails.put(SavingProductAccountingDataParams.ESCHEAT_LIABILITY.getValue(), glAccountData);
|
||||||
|
} else if (glAccountForSavings.equals(CashAccountsForSavings.ROUNDING_ADJUSTMENT)) {
|
||||||
|
accountMappingDetails.put(SavingProductAccountingDataParams.ROUNDING_ADJUSTMENT.getValue(), glAccountData);
|
||||||
// Expense
|
// Expense
|
||||||
} else if (glAccountForSavings.equals(CashAccountsForSavings.INTEREST_ON_SAVINGS)) {
|
} else if (glAccountForSavings.equals(CashAccountsForSavings.INTEREST_ON_SAVINGS)) {
|
||||||
accountMappingDetails.put(SavingProductAccountingDataParams.INTEREST_ON_SAVINGS.getValue(), glAccountData);
|
accountMappingDetails.put(SavingProductAccountingDataParams.INTEREST_ON_SAVINGS.getValue(), glAccountData);
|
||||||
|
|||||||
+18
@@ -106,6 +106,20 @@ public class SavingsProductToGLAccountMappingHelper extends ProductToGLAccountMa
|
|||||||
PortfolioProductType.SAVING);
|
PortfolioProductType.SAVING);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create-or-merge, for an expense row a product may not have yet.
|
||||||
|
*
|
||||||
|
* The plain merge variant assumes the mapping already exists, which is true of every expense
|
||||||
|
* row that shipped with the product screen. A row added later — the rounding adjustment — is
|
||||||
|
* absent on every product created before it, so editing one of those has to create the
|
||||||
|
* mapping rather than fail looking for it.
|
||||||
|
*/
|
||||||
|
public void createOrmergeSavingsToExpenseAccountMappingChanges(final JsonElement element, final String paramName,
|
||||||
|
final Long productId, final int accountTypeId, final Map<String, Object> changes) {
|
||||||
|
createOrmergeProductToAccountMappingChanges(element, paramName, productId, accountTypeId, changes, GLAccountType.EXPENSE,
|
||||||
|
PortfolioProductType.SAVING);
|
||||||
|
}
|
||||||
|
|
||||||
/*** Abstractions for payments channel related to savings products ***/
|
/*** Abstractions for payments channel related to savings products ***/
|
||||||
|
|
||||||
public void savePaymentChannelToFundSourceMappings(final JsonCommand command, final JsonElement element, final Long productId,
|
public void savePaymentChannelToFundSourceMappings(final JsonCommand command, final JsonElement element, final Long productId,
|
||||||
@@ -248,6 +262,8 @@ public class SavingsProductToGLAccountMappingHelper extends ProductToGLAccountMa
|
|||||||
CashAccountsForSavings.TRANSFERS_SUSPENSE.toString(), changes);
|
CashAccountsForSavings.TRANSFERS_SUSPENSE.toString(), changes);
|
||||||
createOrmergeSavingsToLiabilityAccountMappingChanges(element, SavingProductAccountingParams.ESCHEAT_LIABILITY.getValue(),
|
createOrmergeSavingsToLiabilityAccountMappingChanges(element, SavingProductAccountingParams.ESCHEAT_LIABILITY.getValue(),
|
||||||
savingsProductId, CashAccountsForSavings.ESCHEAT_LIABILITY.getValue(), changes);
|
savingsProductId, CashAccountsForSavings.ESCHEAT_LIABILITY.getValue(), changes);
|
||||||
|
createOrmergeSavingsToExpenseAccountMappingChanges(element, SavingProductAccountingParams.ROUNDING_ADJUSTMENT.getValue(),
|
||||||
|
savingsProductId, CashAccountsForSavings.ROUNDING_ADJUSTMENT.getValue(), changes);
|
||||||
break;
|
break;
|
||||||
case ACCRUAL_PERIODIC:
|
case ACCRUAL_PERIODIC:
|
||||||
// asset
|
// asset
|
||||||
@@ -308,6 +324,8 @@ public class SavingsProductToGLAccountMappingHelper extends ProductToGLAccountMa
|
|||||||
|
|
||||||
createOrmergeSavingsToLiabilityAccountMappingChanges(element, SavingProductAccountingParams.ESCHEAT_LIABILITY.getValue(),
|
createOrmergeSavingsToLiabilityAccountMappingChanges(element, SavingProductAccountingParams.ESCHEAT_LIABILITY.getValue(),
|
||||||
savingsProductId, AccrualAccountsForSavings.ESCHEAT_LIABILITY.getValue(), changes);
|
savingsProductId, AccrualAccountsForSavings.ESCHEAT_LIABILITY.getValue(), changes);
|
||||||
|
createOrmergeSavingsToExpenseAccountMappingChanges(element, SavingProductAccountingParams.ROUNDING_ADJUSTMENT.getValue(),
|
||||||
|
savingsProductId, AccrualAccountsForSavings.ROUNDING_ADJUSTMENT.getValue(), changes);
|
||||||
|
|
||||||
break;
|
break;
|
||||||
case ACCRUAL_UPFRONT:
|
case ACCRUAL_UPFRONT:
|
||||||
|
|||||||
+16
-4
@@ -269,7 +269,15 @@ public final class AccountingConstants {
|
|||||||
OVERDRAFT_PORTFOLIO_CONTROL(11), //
|
OVERDRAFT_PORTFOLIO_CONTROL(11), //
|
||||||
INCOME_FROM_INTEREST(12), //
|
INCOME_FROM_INTEREST(12), //
|
||||||
LOSSES_WRITTEN_OFF(13), //
|
LOSSES_WRITTEN_OFF(13), //
|
||||||
ESCHEAT_LIABILITY(14); //
|
ESCHEAT_LIABILITY(14), //
|
||||||
|
/**
|
||||||
|
* Where the paise dropped from a cash closure are absorbed.
|
||||||
|
*
|
||||||
|
* Cash has no paise in circulation, so a closing balance of 9,883.79 is handed over as
|
||||||
|
* 9,883 and the 0.79 has to land somewhere other than the member's own ledger. Numbered
|
||||||
|
* well clear of the accrual slots so the two sets never collide as either grows.
|
||||||
|
*/
|
||||||
|
ROUNDING_ADJUSTMENT(26); //
|
||||||
|
|
||||||
private final Integer value;
|
private final Integer value;
|
||||||
|
|
||||||
@@ -317,7 +325,9 @@ public final class AccountingConstants {
|
|||||||
FEES_RECEIVABLE(15), //
|
FEES_RECEIVABLE(15), //
|
||||||
PENALTIES_RECEIVABLE(16), //
|
PENALTIES_RECEIVABLE(16), //
|
||||||
INTEREST_PAYABLE(17), //
|
INTEREST_PAYABLE(17), //
|
||||||
INTEREST_RECEIVABLE(18);
|
INTEREST_RECEIVABLE(18), //
|
||||||
|
/** Where the paise dropped from a cash closure are absorbed — see the cash enum. */
|
||||||
|
ROUNDING_ADJUSTMENT(26);
|
||||||
|
|
||||||
private final Integer value;
|
private final Integer value;
|
||||||
|
|
||||||
@@ -372,7 +382,8 @@ public final class AccountingConstants {
|
|||||||
PENALTIES_RECEIVABLE("penaltiesReceivableAccountId"), //
|
PENALTIES_RECEIVABLE("penaltiesReceivableAccountId"), //
|
||||||
INTEREST_RECEIVABLE("interestReceivableAccountId"), //
|
INTEREST_RECEIVABLE("interestReceivableAccountId"), //
|
||||||
FEES_RECEIVABLE("feesReceivableAccountId"), //
|
FEES_RECEIVABLE("feesReceivableAccountId"), //
|
||||||
INTEREST_PAYABLE("interestPayableAccountId");
|
INTEREST_PAYABLE("interestPayableAccountId"), //
|
||||||
|
ROUNDING_ADJUSTMENT("roundingAdjustmentAccountId");
|
||||||
|
|
||||||
private final String value;
|
private final String value;
|
||||||
|
|
||||||
@@ -410,7 +421,8 @@ public final class AccountingConstants {
|
|||||||
FEES_RECEIVABLE("feeReceivableAccount"), //
|
FEES_RECEIVABLE("feeReceivableAccount"), //
|
||||||
PENALTIES_RECEIVABLE("penaltyReceivableAccount"), //
|
PENALTIES_RECEIVABLE("penaltyReceivableAccount"), //
|
||||||
INTEREST_PAYABLE("interestPayableAccount"), //
|
INTEREST_PAYABLE("interestPayableAccount"), //
|
||||||
INTEREST_RECEIVABLE("interestReceivableAccount"); //
|
INTEREST_RECEIVABLE("interestReceivableAccount"), //
|
||||||
|
ROUNDING_ADJUSTMENT("roundingAdjustmentAccount"); //
|
||||||
|
|
||||||
private final String value;
|
private final String value;
|
||||||
|
|
||||||
|
|||||||
+9
@@ -301,6 +301,15 @@ public class ProductToGLAccountMappingWritePlatformServiceImpl implements Produc
|
|||||||
CashAccountsForSavings.ESCHEAT_LIABILITY.getValue());
|
CashAccountsForSavings.ESCHEAT_LIABILITY.getValue());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Optional: only mapped when the product screen supplies it. Every savings product that
|
||||||
|
// existed before this row was added has none, and a closure on one of those still has to
|
||||||
|
// work — see the closure's own fallback.
|
||||||
|
if (this.fromApiJsonHelper.parameterExists(SavingProductAccountingParams.ROUNDING_ADJUSTMENT.getValue(), element)) {
|
||||||
|
this.savingsProductToGLAccountMappingHelper.saveSavingsToExpenseAccountMapping(element,
|
||||||
|
SavingProductAccountingParams.ROUNDING_ADJUSTMENT.getValue(), savingProductId,
|
||||||
|
CashAccountsForSavings.ROUNDING_ADJUSTMENT.getValue());
|
||||||
|
}
|
||||||
|
|
||||||
// advanced accounting mappings
|
// advanced accounting mappings
|
||||||
this.savingsProductToGLAccountMappingHelper.savePaymentChannelToFundSourceMappings(command, element, savingProductId, null);
|
this.savingsProductToGLAccountMappingHelper.savePaymentChannelToFundSourceMappings(command, element, savingProductId, null);
|
||||||
this.savingsProductToGLAccountMappingHelper.saveChargesToIncomeAccountMappings(command, element, savingProductId, null);
|
this.savingsProductToGLAccountMappingHelper.saveChargesToIncomeAccountMappings(command, element, savingProductId, null);
|
||||||
|
|||||||
+60
-16
@@ -76,10 +76,13 @@ import org.springframework.transaction.annotation.Transactional;
|
|||||||
* Close Savings Account screen), and a cashier hands over the physical cash against the token,
|
* Close Savings Account screen), and a cashier hands over the physical cash against the token,
|
||||||
* recording the denomination breakdown.
|
* recording the denomination breakdown.
|
||||||
*
|
*
|
||||||
* Cash has no paisa coins in circulation, so the exact (paisa-correct) closing amount is rounded to the
|
* Cash has no paisa coins in circulation, so the exact (paisa-correct) closing amount is rounded DOWN
|
||||||
* nearest rupee for the physical hand-over; any few-paise difference is absorbed by the dedicated
|
* to the whole rupee for the physical hand-over -- 9,883.79 is paid as 9,883 -- and the dropped paise
|
||||||
* "Round Off Account" GL account via one small adjusting journal entry, never touching the member's own
|
* are absorbed by the Rounding Adjustment account the savings product names, via one small adjusting
|
||||||
* ledger (which stays exactly correct to the paisa).
|
* journal entry. The member's own ledger stays exactly correct to the paisa.
|
||||||
|
*
|
||||||
|
* Down rather than to the nearest rupee, so the branch never pays out more cash than the account
|
||||||
|
* actually held: rounding 9,883.79 up to 9,884 hands over 21 paise the member never had.
|
||||||
*/
|
*/
|
||||||
@Path("/v1/clerk/closure-requests")
|
@Path("/v1/clerk/closure-requests")
|
||||||
@Component
|
@Component
|
||||||
@@ -91,6 +94,13 @@ public class ClerkClosureRequestApiResource {
|
|||||||
// and a field of the same name reads as that variable at a glance.
|
// and a field of the same name reads as that variable at a glance.
|
||||||
private static final Logger LOG = LoggerFactory.getLogger(ClerkClosureRequestApiResource.class);
|
private static final Logger LOG = LoggerFactory.getLogger(ClerkClosureRequestApiResource.class);
|
||||||
|
|
||||||
|
/** acc_product_mapping.product_type for a savings product. */
|
||||||
|
private static final int PRODUCT_TYPE_SAVING = 2;
|
||||||
|
/** {@code CashAccountsForSavings.ROUNDING_ADJUSTMENT} */
|
||||||
|
private static final int ROUNDING_ADJUSTMENT_ACCOUNT_TYPE = 26;
|
||||||
|
/** Where the paise go on a product that has no mapping of its own yet. */
|
||||||
|
private static final String DEFAULT_ROUND_OFF_GL_CODE = "501200";
|
||||||
|
|
||||||
private final JdbcTemplate jdbcTemplate;
|
private final JdbcTemplate jdbcTemplate;
|
||||||
private final FromJsonHelper fromJsonHelper;
|
private final FromJsonHelper fromJsonHelper;
|
||||||
private final PlatformSecurityContext context;
|
private final PlatformSecurityContext context;
|
||||||
@@ -170,7 +180,7 @@ public class ClerkClosureRequestApiResource {
|
|||||||
final BigDecimal closingAmount = postClosureInterest(savingsAccountId, closureDate);
|
final BigDecimal closingAmount = postClosureInterest(savingsAccountId, closureDate);
|
||||||
// Rounded the same way approve() rounds it -- so the manager sees, while it is still PENDING,
|
// Rounded the same way approve() rounds it -- so the manager sees, while it is still PENDING,
|
||||||
// exactly what cash the cashier will end up handing over, not just the exact-to-the-paisa figure.
|
// exactly what cash the cashier will end up handing over, not just the exact-to-the-paisa figure.
|
||||||
final BigDecimal cashierDisbursedAmount = closingAmount.setScale(0, RoundingMode.HALF_UP);
|
final BigDecimal cashierDisbursedAmount = closingAmount.setScale(0, RoundingMode.FLOOR);
|
||||||
final BigDecimal roundingAmount = cashierDisbursedAmount.subtract(closingAmount);
|
final BigDecimal roundingAmount = cashierDisbursedAmount.subtract(closingAmount);
|
||||||
|
|
||||||
final String activityLog = buildActivityLog(null, "SUBMITTED", currentUser.getUsername(), null);
|
final String activityLog = buildActivityLog(null, "SUBMITTED", currentUser.getUsername(), null);
|
||||||
@@ -316,7 +326,7 @@ public class ClerkClosureRequestApiResource {
|
|||||||
res.addProperty("total", total);
|
res.addProperty("total", total);
|
||||||
// What a cashier could physically count out: no paisa coins circulate, so the hand-over is
|
// What a cashier could physically count out: no paisa coins circulate, so the hand-over is
|
||||||
// always whole rupees (the difference lands on the Round Off Account at approval).
|
// always whole rupees (the difference lands on the Round Off Account at approval).
|
||||||
res.addProperty("roundedTotal", total.setScale(0, RoundingMode.HALF_UP));
|
res.addProperty("roundedTotal", total.setScale(0, RoundingMode.FLOOR));
|
||||||
res.addProperty("interestUpToDate", true);
|
res.addProperty("interestUpToDate", true);
|
||||||
res.addProperty("asOfDate", closureDate.toString());
|
res.addProperty("asOfDate", closureDate.toString());
|
||||||
return gson.toJson(res);
|
return gson.toJson(res);
|
||||||
@@ -534,12 +544,12 @@ public class ClerkClosureRequestApiResource {
|
|||||||
if (txn != null) {
|
if (txn != null) {
|
||||||
txnId = txn.get("id").toString();
|
txnId = txn.get("id").toString();
|
||||||
closingAmount = new BigDecimal(txn.get("amount").toString());
|
closingAmount = new BigDecimal(txn.get("amount").toString());
|
||||||
cashierDisbursedAmount = closingAmount.setScale(0, RoundingMode.HALF_UP);
|
cashierDisbursedAmount = closingAmount.setScale(0, RoundingMode.FLOOR);
|
||||||
roundingAmount = cashierDisbursedAmount.subtract(closingAmount);
|
roundingAmount = cashierDisbursedAmount.subtract(closingAmount);
|
||||||
|
|
||||||
if (roundingAmount.compareTo(BigDecimal.ZERO) != 0) {
|
if (roundingAmount.compareTo(BigDecimal.ZERO) != 0) {
|
||||||
final Long officeId = ((Number) txn.get("office_id")).longValue();
|
final Long officeId = ((Number) txn.get("office_id")).longValue();
|
||||||
postRoundingAdjustment(officeId, roundingAmount, currentUser);
|
postRoundingAdjustment(officeId, roundingAmount, currentUser, savingsAccountId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -795,11 +805,11 @@ public class ClerkClosureRequestApiResource {
|
|||||||
lines.add(VoucherLine.debitGl(controlGlId, memberSideName, accountNo, closingAmount));
|
lines.add(VoucherLine.debitGl(controlGlId, memberSideName, accountNo, closingAmount));
|
||||||
final int roundingDirection = roundingAmount.compareTo(BigDecimal.ZERO);
|
final int roundingDirection = roundingAmount.compareTo(BigDecimal.ZERO);
|
||||||
if (roundingDirection > 0) {
|
if (roundingDirection > 0) {
|
||||||
lines.add(VoucherLine.debitGl(resolveRoundOffGlAccount(), "Round Off Account", null, roundingAmount.abs()));
|
lines.add(VoucherLine.debitGl(resolveRoundOffGlAccount(savingsAccountId), "Round Off Account", null, roundingAmount.abs()));
|
||||||
}
|
}
|
||||||
lines.add(VoucherLine.creditGl(null, "Cash in Hand", null, cashierDisbursedAmount));
|
lines.add(VoucherLine.creditGl(null, "Cash in Hand", null, cashierDisbursedAmount));
|
||||||
if (roundingDirection < 0) {
|
if (roundingDirection < 0) {
|
||||||
lines.add(VoucherLine.creditGl(resolveRoundOffGlAccount(), "Round Off Account", null, roundingAmount.abs()));
|
lines.add(VoucherLine.creditGl(resolveRoundOffGlAccount(savingsAccountId), "Round Off Account", null, roundingAmount.abs()));
|
||||||
}
|
}
|
||||||
|
|
||||||
final String voucherNo = voucherService.createVoucherWithLines(VoucherService.TYPE_WITHDRAWAL,
|
final String voucherNo = voucherService.createVoucherWithLines(VoucherService.TYPE_WITHDRAWAL,
|
||||||
@@ -824,9 +834,10 @@ public class ClerkClosureRequestApiResource {
|
|||||||
* extra cash out is a further CREDIT to cash, against a DEBIT to Round Off (the bank wears the
|
* extra cash out is a further CREDIT to cash, against a DEBIT to Round Off (the bank wears the
|
||||||
* difference as an expense). Rounded down, both legs flip.
|
* difference as an expense). Rounded down, both legs flip.
|
||||||
*/
|
*/
|
||||||
private void postRoundingAdjustment(final Long officeId, final BigDecimal roundingAmount, final AppUser currentUser) {
|
private void postRoundingAdjustment(final Long officeId, final BigDecimal roundingAmount, final AppUser currentUser,
|
||||||
|
final Long savingsAccountId) {
|
||||||
final Map<String, Object> cashGl = resolveCashGlAccount();
|
final Map<String, Object> cashGl = resolveCashGlAccount();
|
||||||
final Long roundOffGlId = resolveRoundOffGlAccount();
|
final Long roundOffGlId = resolveRoundOffGlAccount(savingsAccountId);
|
||||||
final String currencyCode = jdbcTemplate.queryForObject(
|
final String currencyCode = jdbcTemplate.queryForObject(
|
||||||
"SELECT code FROM m_organisation_currency ORDER BY code LIMIT 1", String.class);
|
"SELECT code FROM m_organisation_currency ORDER BY code LIMIT 1", String.class);
|
||||||
final BigDecimal amount = roundingAmount.abs();
|
final BigDecimal amount = roundingAmount.abs();
|
||||||
@@ -839,10 +850,15 @@ public class ClerkClosureRequestApiResource {
|
|||||||
// type_enum: 2 = DEBIT, 1 = CREDIT.
|
// type_enum: 2 = DEBIT, 1 = CREDIT.
|
||||||
if (roundingAmount.compareTo(BigDecimal.ZERO) > 0) {
|
if (roundingAmount.compareTo(BigDecimal.ZERO) > 0) {
|
||||||
// Rounded up: extra cash left the drawer. Dr Round Off / Cr Cash
|
// Rounded up: extra cash left the drawer. Dr Round Off / Cr Cash
|
||||||
|
//
|
||||||
|
// Unreachable while the hand-over rounds down, and kept so: an amount already whole to
|
||||||
|
// the rupee produces no adjustment at all, and if the rounding rule is ever changed
|
||||||
|
// back this leg is still correct rather than silently posting the wrong way.
|
||||||
insertJournalEntry(officeId, roundOffGlId, currencyCode, transactionId, entryDate, 2, amount, currentUser);
|
insertJournalEntry(officeId, roundOffGlId, currencyCode, transactionId, entryDate, 2, amount, currentUser);
|
||||||
insertJournalEntry(officeId, cashGlId, currencyCode, transactionId, entryDate, 1, amount, currentUser);
|
insertJournalEntry(officeId, cashGlId, currencyCode, transactionId, entryDate, 1, amount, currentUser);
|
||||||
} else {
|
} else {
|
||||||
// Rounded down: less cash left the drawer than the member's ledger gave up.
|
// Rounded down, which is now every case: the member's ledger gave up the full paisa
|
||||||
|
// amount but less cash left the drawer, so the paise stay with the bank.
|
||||||
// Dr Cash / Cr Round Off
|
// Dr Cash / Cr Round Off
|
||||||
insertJournalEntry(officeId, cashGlId, currencyCode, transactionId, entryDate, 2, amount, currentUser);
|
insertJournalEntry(officeId, cashGlId, currencyCode, transactionId, entryDate, 2, amount, currentUser);
|
||||||
insertJournalEntry(officeId, roundOffGlId, currencyCode, transactionId, entryDate, 1, amount, currentUser);
|
insertJournalEntry(officeId, roundOffGlId, currencyCode, transactionId, entryDate, 1, amount, currentUser);
|
||||||
@@ -862,11 +878,39 @@ public class ClerkClosureRequestApiResource {
|
|||||||
return rows.get(0);
|
return rows.get(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
private Long resolveRoundOffGlAccount() {
|
/**
|
||||||
|
* The GL the dropped paise are absorbed by, from the closing account's own savings product.
|
||||||
|
*
|
||||||
|
* <p>
|
||||||
|
* Read from the product's Accounting tab like every other GL this closure touches, so a bank
|
||||||
|
* on its own chart of accounts posts to the head it chose. This used to be
|
||||||
|
* {@code gl_code = '501200'} written into the query, which meant a tenant whose chart has no
|
||||||
|
* such code could not close an account at all, and a bank that keeps its round-off elsewhere
|
||||||
|
* had to create a 501200 anyway.
|
||||||
|
*
|
||||||
|
* <p>
|
||||||
|
* Falls back to that code where the product has no mapping yet — every product created before
|
||||||
|
* the row existed is in that position, and a closure must not fail over a few paise.
|
||||||
|
*/
|
||||||
|
private Long resolveRoundOffGlAccount(final Long savingsAccountId) {
|
||||||
|
if (savingsAccountId != null) {
|
||||||
|
final List<Long> mapped = jdbcTemplate.queryForList("""
|
||||||
|
SELECT m.gl_account_id
|
||||||
|
FROM acc_product_mapping m
|
||||||
|
JOIN m_savings_account sa ON sa.product_id = m.product_id
|
||||||
|
WHERE sa.id = ? AND m.product_type = ? AND m.financial_account_type = ?
|
||||||
|
LIMIT 1
|
||||||
|
""", Long.class, savingsAccountId, PRODUCT_TYPE_SAVING, ROUNDING_ADJUSTMENT_ACCOUNT_TYPE);
|
||||||
|
if (!mapped.isEmpty()) {
|
||||||
|
return mapped.get(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
final List<Long> ids = jdbcTemplate.queryForList(
|
final List<Long> ids = jdbcTemplate.queryForList(
|
||||||
"SELECT id FROM acc_gl_account WHERE gl_code = '501200' LIMIT 1", Long.class);
|
"SELECT id FROM acc_gl_account WHERE gl_code = ? LIMIT 1", Long.class, DEFAULT_ROUND_OFF_GL_CODE);
|
||||||
if (ids.isEmpty()) {
|
if (ids.isEmpty()) {
|
||||||
throw apiError(500, "Round Off Account (gl_code 501200) is not configured for this tenant yet.");
|
throw apiError(500, "No Rounding Adjustment account is mapped on the savings product, and the default "
|
||||||
|
+ "Round Off Account (gl_code " + DEFAULT_ROUND_OFF_GL_CODE + ") does not exist in this tenant. "
|
||||||
|
+ "Map one on the product's Accounting tab.");
|
||||||
}
|
}
|
||||||
return ids.get(0);
|
return ids.get(0);
|
||||||
}
|
}
|
||||||
|
|||||||
+10
@@ -105,6 +105,7 @@ public class SavingsProductDataValidator {
|
|||||||
SavingProductAccountingParams.OVERDRAFT_PORTFOLIO_CONTROL.getValue(),
|
SavingProductAccountingParams.OVERDRAFT_PORTFOLIO_CONTROL.getValue(),
|
||||||
SavingProductAccountingParams.LOSSES_WRITTEN_OFF.getValue(), SavingProductAccountingParams.INCOME_FROM_INTEREST.getValue(),
|
SavingProductAccountingParams.LOSSES_WRITTEN_OFF.getValue(), SavingProductAccountingParams.INCOME_FROM_INTEREST.getValue(),
|
||||||
SavingProductAccountingParams.ESCHEAT_LIABILITY.getValue(), SavingProductAccountingParams.INTEREST_RECEIVABLE.getValue(),
|
SavingProductAccountingParams.ESCHEAT_LIABILITY.getValue(), SavingProductAccountingParams.INTEREST_RECEIVABLE.getValue(),
|
||||||
|
SavingProductAccountingParams.ROUNDING_ADJUSTMENT.getValue(),
|
||||||
isDormancyTrackingActiveParamName, daysToDormancyParamName, daysToInactiveParamName, daysToEscheatParamName,
|
isDormancyTrackingActiveParamName, daysToDormancyParamName, daysToInactiveParamName, daysToEscheatParamName,
|
||||||
allowOverdraftParamName, overdraftLimitParamName, nominalAnnualInterestRateOverdraftParamName,
|
allowOverdraftParamName, overdraftLimitParamName, nominalAnnualInterestRateOverdraftParamName,
|
||||||
minOverdraftForInterestCalculationParamName, SavingsApiConstants.minRequiredBalanceParamName,
|
minOverdraftForInterestCalculationParamName, SavingsApiConstants.minRequiredBalanceParamName,
|
||||||
@@ -460,6 +461,15 @@ public class SavingsProductDataValidator {
|
|||||||
baseDataValidator.reset().parameter(SavingProductAccountingParams.ESCHEAT_LIABILITY.getValue())
|
baseDataValidator.reset().parameter(SavingProductAccountingParams.ESCHEAT_LIABILITY.getValue())
|
||||||
.value(escheatLiabilityAccountId).notNull().integerGreaterThanZero();
|
.value(escheatLiabilityAccountId).notNull().integerGreaterThanZero();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Optional — a product saved without one still closes, on the fallback the closure
|
||||||
|
// carries. Only the shape is checked, so clearing the field is allowed.
|
||||||
|
if (this.fromApiJsonHelper.parameterExists(SavingProductAccountingParams.ROUNDING_ADJUSTMENT.getValue(), element)) {
|
||||||
|
final Long roundingAdjustmentAccountId = this.fromApiJsonHelper
|
||||||
|
.extractLongNamed(SavingProductAccountingParams.ROUNDING_ADJUSTMENT.getValue(), element);
|
||||||
|
baseDataValidator.reset().parameter(SavingProductAccountingParams.ROUNDING_ADJUSTMENT.getValue())
|
||||||
|
.value(roundingAdjustmentAccountId).ignoreIfNull().integerGreaterThanZero();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
validateOverdraftParams(baseDataValidator, element);
|
validateOverdraftParams(baseDataValidator, element);
|
||||||
|
|||||||
Reference in New Issue
Block a user