fix(shares,voucher,cashreport): route redeem/withdrawal through Sundry, fix voucher cashier attribution and GL-name resolution
Fineract Build & Cucumber tests (without E2E tests) / verify (main, cucumber) (push) Canceled after 0s
Fineract Build & Cucumber tests (without E2E tests) / verify (progressive-loan, build-progressive-loan) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - PostgreSQL / test (test-core-5) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 7 of 10) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 8 of 10) (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-5) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MySQL / test (test-core-1) (push) Canceled after 0s
Fineract Docker Builds / build (false, docker-compose-postgresql.yml, postgresql) (push) Canceled after 0s
Fineract Docker Builds / build (true, docker-compose.yml, mariadb) (push) Canceled after 0s
Fineract Documentation build / build (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 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 Build & Cucumber tests (without E2E tests) / verify (main, build-core) (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 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 9 of 10) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MariaDB / test (test-core-4) (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 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 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, cucumber) (push) Canceled after 0s
Fineract Build & Cucumber tests (without E2E tests) / verify (progressive-loan, build-progressive-loan) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - PostgreSQL / test (test-core-5) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 7 of 10) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 8 of 10) (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-5) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MySQL / test (test-core-1) (push) Canceled after 0s
Fineract Docker Builds / build (false, docker-compose-postgresql.yml, postgresql) (push) Canceled after 0s
Fineract Docker Builds / build (true, docker-compose.yml, mariadb) (push) Canceled after 0s
Fineract Documentation build / build (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 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 Build & Cucumber tests (without E2E tests) / verify (main, build-core) (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 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 9 of 10) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MariaDB / test (test-core-4) (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 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 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
- ClerkShareWithdrawalRequestApiResource: reverse native redeem GL legs and reroute through Sundry (Dr Share Reference / Cr Sundry, then Dr Sundry / Cr Savings Control), reusing the native transaction_id so both legs display as balanced; wholesale-reverse and reroute the linked savings credit the same way. - ShareAccountWritePlatformServiceJpaRepositoryImpl: undoApproveShareAccount no longer re-triggers the native "isApplied()" phantom GL posting Fineract's accounting bridge makes on approval. - VoucherService/ClerkWithdrawalRequestApiResource: attribute a voucher to the cashier who actually disbursed cash, not the submitting clerk, once disbursal completes. - CashReportApiResource: scope the GL-entries deposit/withdrawal joins by txn_type to stop a hex/decimal txn_id collision from borrowing an unrelated transaction's voucher number; resolve GL account names (not just client names) in the "A/C NAME" column. fix(branchday,cashreport): stop the vault position from double-counting a cashier's unsettled cash BranchDayRegisterApiResource's day-close folded every cashier's still-held cash into the branch's combined closing figure, which then carried forward as next day's vault opening. When that cash was later settled back to the vault, CashReportApiResource added it a second time - once already baked into the carried-forward opening, once as the settlement itself - manufacturing a phantom "Excess" equal to whatever was unsettled. Adds a vault-only OPENING_VAULT/CLOSING_VAULT leg (new VaultPositionService, shared by both resources) alongside the existing branch-total OPENING/CLOSING legs, so the vault's own anchor never includes cash that still physically sits with a cashier. feat(accounting): seed a missing opening-balance contra account and a real bank's chart of accounts - Financial Activity 300 (opening-balance transfer contra) was never mapped in any tenant's chart of accounts, so any opening-balance flow failed outright; backfilled it and added a migration to seed it for every tenant going forward. - Seeds a brand-new, otherwise-empty tenant with a real cooperative bank's chart of accounts (transcribed from its balance sheet), guarded to never touch a tenant that already has its own. - Removes 0451's hardcoded demo branch offices, which were seeding into every new tenant regardless of institution. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
+74
-3
@@ -40,6 +40,7 @@ import org.apache.fineract.infrastructure.core.exception.PlatformDataIntegrityEx
|
||||
import org.apache.fineract.infrastructure.core.serialization.FromJsonHelper;
|
||||
import org.apache.fineract.infrastructure.core.service.DateUtils;
|
||||
import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext;
|
||||
import org.apache.fineract.portfolio.cashreport.service.VaultPositionService;
|
||||
import org.apache.fineract.useradministration.domain.AppUser;
|
||||
import org.springframework.jdbc.core.JdbcTemplate;
|
||||
import org.springframework.stereotype.Component;
|
||||
@@ -64,6 +65,7 @@ public class BranchDayRegisterApiResource {
|
||||
private final JdbcTemplate jdbcTemplate;
|
||||
private final PlatformSecurityContext context;
|
||||
private final FromJsonHelper fromJsonHelper;
|
||||
private final VaultPositionService vaultPositionService;
|
||||
private final Gson gson = new Gson();
|
||||
|
||||
// ── GET /v1/branchday ── the register itself ──────────────────────────────
|
||||
@@ -271,6 +273,7 @@ public class BranchDayRegisterApiResource {
|
||||
Timestamp.from(Instant.now()), carried, opensWithADifference ? openingDifferenceReason.trim() : null);
|
||||
|
||||
saveDenominationDetail(id, "OPENING", effectiveDenominations);
|
||||
carryForwardVaultDenominations(officeId, id, businessDate);
|
||||
|
||||
final Map<String, Object> res = new LinkedHashMap<>();
|
||||
res.put("resourceId", id);
|
||||
@@ -342,6 +345,7 @@ public class BranchDayRegisterApiResource {
|
||||
|
||||
final Long registerId = ((Number) found.get(0).get("id")).longValue();
|
||||
saveDenominationDetail(registerId, "CLOSING", denominations);
|
||||
snapshotVaultDenominations(officeId, registerId, businessDate);
|
||||
|
||||
final Map<String, Object> res = new LinkedHashMap<>();
|
||||
res.put("officeId", officeId);
|
||||
@@ -448,6 +452,16 @@ public class BranchDayRegisterApiResource {
|
||||
}
|
||||
|
||||
private void saveDenominationDetail(final Long registerId, final String leg, final List<Map<String, Object>> denominations) {
|
||||
saveDenominationDetail(registerId, leg, null, denominations);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@code tellerId} is null for the branch-total OPENING/CLOSING legs (vault + every cashier
|
||||
* added together) and set for the vault-only OPENING_VAULT/CLOSING_VAULT legs — see
|
||||
* {@link VaultPositionService} for why the two are never allowed to mix.
|
||||
*/
|
||||
private void saveDenominationDetail(final Long registerId, final String leg, final Long tellerId,
|
||||
final List<Map<String, Object>> denominations) {
|
||||
if (denominations == null || denominations.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
@@ -456,10 +470,67 @@ public class BranchDayRegisterApiResource {
|
||||
final int count = (int) row.get("count");
|
||||
jdbcTemplate.update("""
|
||||
INSERT INTO m_branch_day_register_detail
|
||||
(register_id, leg, denomination_id, denomination_value, count, total_amount)
|
||||
VALUES (?,?,?,?,?,?)
|
||||
(register_id, leg, denomination_id, denomination_value, count, total_amount, teller_id)
|
||||
VALUES (?,?,?,?,?,?,?)
|
||||
""", registerId, leg, row.get("denominationId"), value, count,
|
||||
BigDecimal.valueOf((long) value * count));
|
||||
BigDecimal.valueOf((long) value * count), tellerId);
|
||||
}
|
||||
}
|
||||
|
||||
/** Copies one register's vault-only CLOSING_VAULT rows into another register's OPENING_VAULT,
|
||||
* per teller — the vault-scoped analogue of {@link #lastClosingDenominations}. */
|
||||
private void carryForwardVaultDenominations(final Long officeId, final Long newRegisterId, final LocalDate businessDate) {
|
||||
final List<Long> tellerIds = jdbcTemplate.queryForList("SELECT id FROM m_tellers WHERE office_id = ?", Long.class,
|
||||
officeId);
|
||||
for (final Long tellerId : tellerIds) {
|
||||
final List<Long> priorRegisterIds = jdbcTemplate.queryForList("""
|
||||
SELECT r.id FROM m_branch_day_register r
|
||||
WHERE r.office_id = ? AND r.business_date < ?
|
||||
AND EXISTS (SELECT 1 FROM m_branch_day_register_detail d
|
||||
WHERE d.register_id = r.id AND d.leg = 'CLOSING_VAULT' AND d.teller_id = ?)
|
||||
ORDER BY r.business_date DESC LIMIT 1
|
||||
""", Long.class, officeId, Date.valueOf(businessDate), tellerId);
|
||||
if (priorRegisterIds.isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
final List<Map<String, Object>> rows = jdbcTemplate.queryForList("""
|
||||
SELECT denomination_id AS "denominationId", denomination_value AS "denominationValue", count
|
||||
FROM m_branch_day_register_detail
|
||||
WHERE register_id = ? AND leg = 'CLOSING_VAULT' AND teller_id = ?
|
||||
""", priorRegisterIds.get(0), tellerId);
|
||||
saveDenominationDetail(newRegisterId, "OPENING_VAULT", tellerId, rows);
|
||||
}
|
||||
}
|
||||
|
||||
/** Snapshots what each teller under this office is holding right now, as CLOSING_VAULT — the
|
||||
* input {@link #carryForwardVaultDenominations} carries into tomorrow's OPENING_VAULT. Read
|
||||
* from {@link VaultPositionService} rather than from whatever was physically counted at close,
|
||||
* so the vault-only chain stays self-consistent even when a closer's manual count differs from
|
||||
* what the books show — that difference is a book-vs-physical question, not a vault-vs-cashier
|
||||
* split question. */
|
||||
private void snapshotVaultDenominations(final Long officeId, final Long registerId, final LocalDate businessDate) {
|
||||
final List<Long> tellerIds = jdbcTemplate.queryForList("SELECT id FROM m_tellers WHERE office_id = ?", Long.class,
|
||||
officeId);
|
||||
for (final Long tellerId : tellerIds) {
|
||||
final Map<String, Object> position = vaultPositionService.buildPositionForVault(tellerId, businessDate.toString());
|
||||
if (!Boolean.TRUE.equals(position.get("hasBreakdown"))) {
|
||||
continue;
|
||||
}
|
||||
@SuppressWarnings("unchecked")
|
||||
final List<Map<String, Object>> denoms = (List<Map<String, Object>>) position.get("denominations");
|
||||
final List<Map<String, Object>> rows = new ArrayList<>();
|
||||
for (final Map<String, Object> d : denoms) {
|
||||
final int count = ((Number) d.get("count")).intValue();
|
||||
if (count <= 0) {
|
||||
continue;
|
||||
}
|
||||
final Map<String, Object> row = new LinkedHashMap<>();
|
||||
row.put("denominationId", d.get("denominationId"));
|
||||
row.put("denominationValue", ((Number) d.get("denominationValue")).intValue());
|
||||
row.put("count", count);
|
||||
rows.add(row);
|
||||
}
|
||||
saveDenominationDetail(registerId, "CLOSING_VAULT", tellerId, rows);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+21
-108
@@ -15,6 +15,7 @@ import java.util.Map;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext;
|
||||
import org.apache.fineract.portfolio.cashreport.service.CashierRoleHelper;
|
||||
import org.apache.fineract.portfolio.cashreport.service.VaultPositionService;
|
||||
import org.apache.fineract.portfolio.denominationmaster.service.DenominationBreakdownWriter;
|
||||
import org.springframework.jdbc.core.JdbcTemplate;
|
||||
import org.springframework.stereotype.Component;
|
||||
@@ -30,6 +31,7 @@ public class CashReportApiResource {
|
||||
private final PlatformSecurityContext context;
|
||||
private final DenominationBreakdownWriter denominationBreakdownWriter;
|
||||
private final CashierRoleHelper cashierRoleHelper;
|
||||
private final VaultPositionService vaultPositionService;
|
||||
private final Gson gson = new Gson();
|
||||
|
||||
/**
|
||||
@@ -542,11 +544,7 @@ public class CashReportApiResource {
|
||||
* GET /cashreports/vault-position?tellerId=X&asOfDate=yyyy-MM-dd
|
||||
* Denomination-wise stock a vault (teller) is currently holding — "how much of what does the
|
||||
* bank have", as opposed to cashier-position which answers the same question for a cashier.
|
||||
* Anchored on the most recent business day (on or before asOfDate) whose branch day register
|
||||
* recorded an OPENING denomination breakdown, then walked forward by everything settled back
|
||||
* into this vault and handed out of it since. A vault whose opening days never recorded a
|
||||
* breakdown (plain lump-sum opens only) has nothing to report per denomination — the response
|
||||
* says so explicitly rather than silently returning zeros that look like an empty vault.
|
||||
* See {@link VaultPositionService} for how the anchor is chosen.
|
||||
*/
|
||||
@GET
|
||||
@Path("vault-position")
|
||||
@@ -559,107 +557,11 @@ public class CashReportApiResource {
|
||||
final List<Long> tellerIds = jdbcTemplate.queryForList("SELECT id FROM m_tellers ORDER BY id", Long.class);
|
||||
final List<Map<String, Object>> all = new ArrayList<>();
|
||||
for (final Long id : tellerIds) {
|
||||
all.add(buildPositionForVault(id, dateFilter));
|
||||
all.add(vaultPositionService.buildPositionForVault(id, dateFilter));
|
||||
}
|
||||
return gson.toJson(all);
|
||||
}
|
||||
return gson.toJson(buildPositionForVault(tellerId, dateFilter));
|
||||
}
|
||||
|
||||
private Map<String, Object> buildPositionForVault(final Long tellerId, final String dateFilter) {
|
||||
final Map<String, Object> result = new LinkedHashMap<>();
|
||||
result.put("tellerId", tellerId);
|
||||
result.put("asOfDate", dateFilter);
|
||||
|
||||
String tellerName = null;
|
||||
Long officeId = null;
|
||||
try {
|
||||
final Map<String, Object> teller = jdbcTemplate.queryForMap(
|
||||
"SELECT name, office_id FROM m_tellers WHERE id = ?", tellerId);
|
||||
tellerName = (String) teller.get("name");
|
||||
officeId = ((Number) teller.get("office_id")).longValue();
|
||||
} catch (Exception ignored) {
|
||||
// intentionally ignored
|
||||
}
|
||||
result.put("tellerName", tellerName);
|
||||
|
||||
if (officeId == null) {
|
||||
result.put("hasBreakdown", false);
|
||||
result.put("totalAmount", BigDecimal.ZERO);
|
||||
result.put("denominations", new ArrayList<>());
|
||||
return result;
|
||||
}
|
||||
|
||||
// The anchor: the most recent register on or before asOfDate that actually recorded an
|
||||
// opening breakdown. Its breakdown already reflects every prior day's carry-forward (see
|
||||
// BranchDayRegisterApiResource#lastClosingDenominations), so only movements from that one
|
||||
// anchor date forward need to be added on top of it.
|
||||
final List<Map<String, Object>> anchorRows = jdbcTemplate.queryForList("""
|
||||
SELECT r.id AS register_id, r.business_date FROM m_branch_day_register r
|
||||
WHERE r.office_id = ? AND r.business_date <= CAST(? AS DATE)
|
||||
AND EXISTS (SELECT 1 FROM m_branch_day_register_detail d
|
||||
WHERE d.register_id = r.id AND d.leg = 'OPENING')
|
||||
ORDER BY r.business_date DESC LIMIT 1
|
||||
""", officeId, dateFilter);
|
||||
|
||||
if (anchorRows.isEmpty()) {
|
||||
result.put("hasBreakdown", false);
|
||||
result.put("totalAmount", BigDecimal.ZERO);
|
||||
result.put("denominations", new ArrayList<>());
|
||||
return result;
|
||||
}
|
||||
|
||||
final Long registerId = ((Number) anchorRows.get(0).get("register_id")).longValue();
|
||||
final String anchorDate = anchorRows.get(0).get("business_date").toString();
|
||||
|
||||
final Map<Long, long[]> balance = new LinkedHashMap<>();
|
||||
|
||||
addToBalance(balance, jdbcTemplate.query("""
|
||||
SELECT denomination_id, denomination_value, COALESCE(dm.kind, 'note') AS kind, count AS total_count
|
||||
FROM m_branch_day_register_detail d LEFT JOIN m_denomination_master dm ON dm.id = d.denomination_id
|
||||
WHERE d.register_id = ? AND d.leg = 'OPENING'
|
||||
""", DENOM_ROW, registerId), 1);
|
||||
|
||||
addToBalance(balance, jdbcTemplate.query("""
|
||||
SELECT csd.denomination_id, csd.denomination_value, COALESCE(dm.kind, 'note') AS kind, SUM(csd.count) AS total_count
|
||||
FROM m_cash_settlement_detail csd JOIN m_cash_settlement cs ON cs.id = csd.settlement_id
|
||||
LEFT JOIN m_denomination_master dm ON dm.id = csd.denomination_id
|
||||
WHERE cs.to_teller_id = ? AND (cs.is_reversed IS NULL OR cs.is_reversed = false)
|
||||
AND cs.settlement_date >= CAST(? AS DATE) AND cs.settlement_date <= CAST(? AS DATE)
|
||||
GROUP BY csd.denomination_id, csd.denomination_value, dm.kind
|
||||
""", DENOM_ROW, tellerId, anchorDate, dateFilter), 1);
|
||||
|
||||
addToBalance(balance, jdbcTemplate.query("""
|
||||
SELECT ctd.denomination_id, ctd.denomination_value, COALESCE(dm.kind, 'note') AS kind, SUM(ctd.count) AS total_count
|
||||
FROM m_cash_transfer_detail ctd JOIN m_cash_transfer ct ON ct.id = ctd.transfer_id
|
||||
LEFT JOIN m_denomination_master dm ON dm.id = ctd.denomination_id
|
||||
WHERE ct.from_teller_id = ? AND (ct.is_reversed IS NULL OR ct.is_reversed = false)
|
||||
AND ct.transfer_date >= CAST(? AS DATE) AND ct.transfer_date <= CAST(? AS DATE)
|
||||
GROUP BY ctd.denomination_id, ctd.denomination_value, dm.kind
|
||||
""", DENOM_ROW, tellerId, anchorDate, dateFilter), -1);
|
||||
|
||||
final List<Map<String, Object>> rows = new ArrayList<>();
|
||||
balance.entrySet().stream()
|
||||
.sorted((a, b) -> Long.compare(b.getValue()[0], a.getValue()[0]))
|
||||
.forEach(e -> {
|
||||
final Map<String, Object> row = new LinkedHashMap<>();
|
||||
row.put("denominationId", e.getKey());
|
||||
row.put("denominationValue", (int) e.getValue()[0]);
|
||||
row.put("kind", e.getValue()[1] == 1L ? "coin" : "note");
|
||||
row.put("count", e.getValue()[2]);
|
||||
row.put("amount", BigDecimal.valueOf(e.getValue()[0] * e.getValue()[2]));
|
||||
rows.add(row);
|
||||
});
|
||||
|
||||
final BigDecimal totalAmount = rows.stream()
|
||||
.map(r -> (BigDecimal) r.get("amount"))
|
||||
.reduce(BigDecimal.ZERO, BigDecimal::add);
|
||||
|
||||
result.put("hasBreakdown", true);
|
||||
result.put("anchorDate", anchorDate);
|
||||
result.put("totalAmount", totalAmount);
|
||||
result.put("denominations", rows);
|
||||
return result;
|
||||
return gson.toJson(vaultPositionService.buildPositionForVault(tellerId, dateFilter));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -914,12 +816,23 @@ public class CashReportApiResource {
|
||||
// contains a-f, is never sent through the hex cast at all).
|
||||
final String baseGroupSql = "FROM m_savings_txn_denomination std "
|
||||
+ "LEFT JOIN m_client mc ON mc.id = std.client_id "
|
||||
+ "LEFT JOIN m_cashier_deposit_request cdr ON cdr.txn_id = CAST(std.txn_id AS VARCHAR) "
|
||||
// A GL-type row (no client_id) carries the GL account's own code in account_no
|
||||
// rather than a savings/current account number, so its "name" has to come from the
|
||||
// chart of accounts, not from a client that was never party to the transaction.
|
||||
+ "LEFT JOIN acc_gl_account gla ON std.client_id IS NULL AND gla.gl_code = std.account_no "
|
||||
// Scoped to std.txn_type: without it, a deposit's hex txn_id and a withdrawal's
|
||||
// decimal txn_id can coincidentally hex-decode to the exact same number (confirmed
|
||||
// live -- a2a0888f2bcf and a genuine withdrawal reference both resolved to
|
||||
// 178810369551311), and an unscoped join matched the withdrawal row to that unrelated
|
||||
// deposit's voucher, which COALESCE then preferred over the withdrawal's own real one.
|
||||
+ "LEFT JOIN m_cashier_deposit_request cdr ON std.txn_type = 'DEPOSIT' AND "
|
||||
+ " (cdr.txn_id = CAST(std.txn_id AS VARCHAR) "
|
||||
+ " OR (cdr.txn_id ~ '^[0-9a-fA-F]{1,16}$' AND cdr.txn_id ~ '[a-fA-F]' "
|
||||
+ " AND ('x' || lpad(cdr.txn_id, 16, '0'))::bit(64)::bigint = std.txn_id) "
|
||||
+ "LEFT JOIN m_clerk_withdrawal_request cwr ON cwr.txn_id = CAST(std.txn_id AS VARCHAR) "
|
||||
+ " AND ('x' || lpad(cdr.txn_id, 16, '0'))::bit(64)::bigint = std.txn_id)) "
|
||||
+ "LEFT JOIN m_clerk_withdrawal_request cwr ON std.txn_type = 'WITHDRAWAL' AND "
|
||||
+ " (cwr.txn_id = CAST(std.txn_id AS VARCHAR) "
|
||||
+ " OR (cwr.txn_id ~ '^[0-9a-fA-F]{1,16}$' AND cwr.txn_id ~ '[a-fA-F]' "
|
||||
+ " AND ('x' || lpad(cwr.txn_id, 16, '0'))::bit(64)::bigint = std.txn_id) "
|
||||
+ " AND ('x' || lpad(cwr.txn_id, 16, '0'))::bit(64)::bigint = std.txn_id)) "
|
||||
+ "WHERE std.txn_date = CAST(? AS DATE) "
|
||||
+ officerFilter
|
||||
+ " GROUP BY std.txn_id, std.account_no, mc.display_name, std.cif_no ";
|
||||
@@ -942,7 +855,7 @@ public class CashReportApiResource {
|
||||
final String dataSql = "SELECT "
|
||||
+ " COALESCE(MAX(cdr.voucher_no), MAX(cwr.voucher_no), CAST(std.txn_id AS VARCHAR)) AS voucher_no, "
|
||||
+ " std.account_no AS ac_no, "
|
||||
+ " COALESCE(mc.display_name, std.cif_no, '') AS ac_name, "
|
||||
+ " COALESCE(mc.display_name, std.cif_no, MAX(gla.name), '') AS ac_name, "
|
||||
+ " MAX(std.txn_date) AS txn_date, "
|
||||
+ " TO_CHAR(MAX(std.created_at), 'HH24:MI:SS') AS txn_time, "
|
||||
+ " MAX(std.officer_username) AS teller_name, "
|
||||
|
||||
+169
@@ -0,0 +1,169 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
package org.apache.fineract.portfolio.cashreport.service;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.util.ArrayList;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.jdbc.core.JdbcTemplate;
|
||||
import org.springframework.jdbc.core.RowMapper;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
/**
|
||||
* Denomination-wise cash a vault (teller) is currently holding.
|
||||
*
|
||||
* Anchored on the most recent business day (on or before the target date) whose branch day
|
||||
* register recorded a vault-only opening breakdown ({@code OPENING_VAULT}, tagged with this exact
|
||||
* teller), then walked forward by everything settled into this vault and handed out of it since.
|
||||
* Vault-only rows exist separately from the branch-total {@code OPENING}/{@code CLOSING} rows
|
||||
* (which mix the vault with whatever every cashier under it is holding) precisely so this anchor
|
||||
* never includes cash that still physically sits with a cashier — mixing the two double-counts
|
||||
* whatever a cashier settles back later, once already folded into the branch total, again as the
|
||||
* settlement itself. A day recorded before vault-only tracking existed has no {@code OPENING_VAULT}
|
||||
* row at all; the branch-total {@code OPENING} is used for it as a last resort so an old anchor
|
||||
* still returns something rather than zero.
|
||||
*/
|
||||
@Component
|
||||
@RequiredArgsConstructor
|
||||
public class VaultPositionService {
|
||||
|
||||
private final JdbcTemplate jdbcTemplate;
|
||||
|
||||
// Row mapper: returns Object[]{Long denomId, Integer denomValue, String kind, Long count}
|
||||
private static final RowMapper<Object[]> DENOM_ROW = (rs, r) -> new Object[] { rs.getLong("denomination_id"),
|
||||
rs.getInt("denomination_value"), rs.getString("kind"), rs.getLong("total_count") };
|
||||
|
||||
public Map<String, Object> buildPositionForVault(final Long tellerId, final String dateFilter) {
|
||||
final Map<String, Object> result = new LinkedHashMap<>();
|
||||
result.put("tellerId", tellerId);
|
||||
result.put("asOfDate", dateFilter);
|
||||
|
||||
String tellerName = null;
|
||||
Long officeId = null;
|
||||
try {
|
||||
final Map<String, Object> teller = jdbcTemplate.queryForMap("SELECT name, office_id FROM m_tellers WHERE id = ?",
|
||||
tellerId);
|
||||
tellerName = (String) teller.get("name");
|
||||
officeId = ((Number) teller.get("office_id")).longValue();
|
||||
} catch (final Exception ignored) {
|
||||
// intentionally ignored
|
||||
}
|
||||
result.put("tellerName", tellerName);
|
||||
|
||||
if (officeId == null) {
|
||||
result.put("hasBreakdown", false);
|
||||
result.put("totalAmount", BigDecimal.ZERO);
|
||||
result.put("denominations", new ArrayList<>());
|
||||
return result;
|
||||
}
|
||||
|
||||
// Prefer a vault-scoped anchor: it already reflects only this vault's own cash, carried
|
||||
// forward day to day, with no cashier's held cash folded in. Fall back to the old
|
||||
// branch-combined OPENING leg only when this office has no vault-scoped row at all on or
|
||||
// before the target date (a day closed before vault-only tracking existed).
|
||||
List<Map<String, Object>> anchorRows = jdbcTemplate.queryForList("""
|
||||
SELECT r.id AS register_id, r.business_date FROM m_branch_day_register r
|
||||
WHERE r.office_id = ? AND r.business_date <= CAST(? AS DATE)
|
||||
AND EXISTS (SELECT 1 FROM m_branch_day_register_detail d
|
||||
WHERE d.register_id = r.id AND d.leg = 'OPENING_VAULT' AND d.teller_id = ?)
|
||||
ORDER BY r.business_date DESC LIMIT 1
|
||||
""", officeId, dateFilter, tellerId);
|
||||
String anchorLeg = "OPENING_VAULT";
|
||||
if (anchorRows.isEmpty()) {
|
||||
anchorLeg = "OPENING";
|
||||
anchorRows = jdbcTemplate.queryForList("""
|
||||
SELECT r.id AS register_id, r.business_date FROM m_branch_day_register r
|
||||
WHERE r.office_id = ? AND r.business_date <= CAST(? AS DATE)
|
||||
AND EXISTS (SELECT 1 FROM m_branch_day_register_detail d
|
||||
WHERE d.register_id = r.id AND d.leg = 'OPENING')
|
||||
ORDER BY r.business_date DESC LIMIT 1
|
||||
""", officeId, dateFilter);
|
||||
}
|
||||
|
||||
if (anchorRows.isEmpty()) {
|
||||
result.put("hasBreakdown", false);
|
||||
result.put("totalAmount", BigDecimal.ZERO);
|
||||
result.put("denominations", new ArrayList<>());
|
||||
return result;
|
||||
}
|
||||
|
||||
final Long registerId = ((Number) anchorRows.get(0).get("register_id")).longValue();
|
||||
final String anchorDate = anchorRows.get(0).get("business_date").toString();
|
||||
|
||||
final Map<Long, long[]> balance = new LinkedHashMap<>();
|
||||
|
||||
addToBalance(balance, jdbcTemplate.query("""
|
||||
SELECT denomination_id, denomination_value, COALESCE(dm.kind, 'note') AS kind, count AS total_count
|
||||
FROM m_branch_day_register_detail d LEFT JOIN m_denomination_master dm ON dm.id = d.denomination_id
|
||||
WHERE d.register_id = ? AND d.leg = ? AND (d.teller_id IS NULL OR d.teller_id = ?)
|
||||
""", DENOM_ROW, registerId, anchorLeg, tellerId), 1);
|
||||
|
||||
addToBalance(balance, jdbcTemplate.query("""
|
||||
SELECT csd.denomination_id, csd.denomination_value, COALESCE(dm.kind, 'note') AS kind, SUM(csd.count) AS total_count
|
||||
FROM m_cash_settlement_detail csd JOIN m_cash_settlement cs ON cs.id = csd.settlement_id
|
||||
LEFT JOIN m_denomination_master dm ON dm.id = csd.denomination_id
|
||||
WHERE cs.to_teller_id = ? AND (cs.is_reversed IS NULL OR cs.is_reversed = false)
|
||||
AND cs.settlement_date >= CAST(? AS DATE) AND cs.settlement_date <= CAST(? AS DATE)
|
||||
GROUP BY csd.denomination_id, csd.denomination_value, dm.kind
|
||||
""", DENOM_ROW, tellerId, anchorDate, dateFilter), 1);
|
||||
|
||||
addToBalance(balance, jdbcTemplate.query("""
|
||||
SELECT ctd.denomination_id, ctd.denomination_value, COALESCE(dm.kind, 'note') AS kind, SUM(ctd.count) AS total_count
|
||||
FROM m_cash_transfer_detail ctd JOIN m_cash_transfer ct ON ct.id = ctd.transfer_id
|
||||
LEFT JOIN m_denomination_master dm ON dm.id = ctd.denomination_id
|
||||
WHERE ct.from_teller_id = ? AND (ct.is_reversed IS NULL OR ct.is_reversed = false)
|
||||
AND ct.transfer_date >= CAST(? AS DATE) AND ct.transfer_date <= CAST(? AS DATE)
|
||||
GROUP BY ctd.denomination_id, ctd.denomination_value, dm.kind
|
||||
""", DENOM_ROW, tellerId, anchorDate, dateFilter), -1);
|
||||
|
||||
final List<Map<String, Object>> rows = new ArrayList<>();
|
||||
balance.entrySet().stream().sorted((a, b) -> Long.compare(b.getValue()[0], a.getValue()[0])).forEach(e -> {
|
||||
final Map<String, Object> row = new LinkedHashMap<>();
|
||||
row.put("denominationId", e.getKey());
|
||||
row.put("denominationValue", (int) e.getValue()[0]);
|
||||
row.put("kind", e.getValue()[1] == 1L ? "coin" : "note");
|
||||
row.put("count", e.getValue()[2]);
|
||||
row.put("amount", BigDecimal.valueOf(e.getValue()[0] * e.getValue()[2]));
|
||||
rows.add(row);
|
||||
});
|
||||
|
||||
final BigDecimal totalAmount = rows.stream().map(r -> (BigDecimal) r.get("amount")).reduce(BigDecimal.ZERO,
|
||||
BigDecimal::add);
|
||||
|
||||
result.put("hasBreakdown", true);
|
||||
result.put("anchorDate", anchorDate);
|
||||
result.put("totalAmount", totalAmount);
|
||||
result.put("denominations", rows);
|
||||
return result;
|
||||
}
|
||||
|
||||
private void addToBalance(final Map<Long, long[]> balance, final List<Object[]> rows, final int sign) {
|
||||
for (final Object[] row : rows) {
|
||||
final Long denomId = (Long) row[0];
|
||||
final int denomValue = (Integer) row[1];
|
||||
final long kindFlag = "coin".equalsIgnoreCase((String) row[2]) ? 1L : 0L;
|
||||
final long count = (Long) row[3];
|
||||
final long[] entry = balance.computeIfAbsent(denomId, k -> new long[] { denomValue, kindFlag, 0L });
|
||||
entry[2] += count * sign;
|
||||
}
|
||||
}
|
||||
}
|
||||
+189
-13
@@ -41,6 +41,8 @@ import java.sql.Date;
|
||||
import java.sql.Timestamp;
|
||||
import java.time.Instant;
|
||||
import java.time.LocalDate;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.ZoneOffset;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
@@ -55,6 +57,7 @@ import org.apache.fineract.infrastructure.core.serialization.FromJsonHelper;
|
||||
import org.apache.fineract.infrastructure.core.service.DateUtils;
|
||||
import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext;
|
||||
import org.apache.fineract.useradministration.domain.AppUser;
|
||||
import org.springframework.dao.IncorrectResultSizeDataAccessException;
|
||||
import org.springframework.jdbc.core.JdbcTemplate;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
@@ -97,6 +100,20 @@ public class ClerkShareWithdrawalRequestApiResource {
|
||||
// instead of silently posting its settlement with none at all.
|
||||
private static final String SHARE_REDEMPTION_PAYMENT_TYPE_CODE = "TRANSFER";
|
||||
|
||||
// ── Sundry pass-through for the redemption settlement ───────────────────
|
||||
// Fineract convention: 1 = CREDIT, 2 = DEBIT.
|
||||
private static final int CREDIT = 1;
|
||||
private static final int DEBIT = 2;
|
||||
|
||||
// acc_product_mapping.financial_account_type for share products.
|
||||
private static final int FA_SHARES_REFERENCE = 1;
|
||||
private static final int FA_SHARES_SUSPENSE = 2;
|
||||
private static final int PRODUCT_TYPE_SHARES = 4;
|
||||
|
||||
// acc_product_mapping for savings: product_type 2, financial_account_type 2 = Savings Control.
|
||||
private static final int PRODUCT_TYPE_SAVINGS = 2;
|
||||
private static final int FA_SAVINGS_CONTROL = 2;
|
||||
|
||||
// ── POST /v1/clerk/share-withdrawal-requests ── Clerk submits new request ───
|
||||
|
||||
@POST
|
||||
@@ -629,11 +646,20 @@ public class ClerkShareWithdrawalRequestApiResource {
|
||||
final Long shareTransactionId = shareTxnIdRaw instanceof Number ? ((Number) shareTxnIdRaw).longValue() : null;
|
||||
final String shareTxnId = shareTransactionId != null ? shareTransactionId.toString() : null;
|
||||
|
||||
// Redirects the native redeem posting's Cr Shares Reference (Cash at Vault) leg into
|
||||
// Cr Sundry instead — see routeRedeemIntoSundry() for why: every other way money reaches or
|
||||
// leaves share capital in this codebase passes through Sundry, and leaving redeem on its own
|
||||
// "give cash back over the counter" assumption breaks that the moment the settlement below
|
||||
// lands on a different GL than this leg did.
|
||||
if (shareTransactionId != null) {
|
||||
routeRedeemIntoSundry(shareAccountId, shareTransactionId, LocalDate.parse(requestedDateObj.toString().substring(0, 10)),
|
||||
currentUser.getId());
|
||||
}
|
||||
|
||||
// Settlement: within this SAME approval transaction, immediately credit the redemption value
|
||||
// to the member's linked savings account. Two Fineract journal entries get posted — native
|
||||
// redeem's Dr Share Equity / Cr Shares Reference, then this deposit's Dr Shares Reference /
|
||||
// Cr Savings Control — but the Shares Reference leg nets to zero instantly, so the combined
|
||||
// financial result is exactly Dr Share Equity / Cr Savings Control, same as one direct entry.
|
||||
// to the member's linked savings account, routed Dr Sundry / Cr Savings Control -- explicitly,
|
||||
// not by relying on the settlement payment type's own GL Fund Source mapping to coincidentally
|
||||
// land on the same account the leg above credited. See creditLinkedSavingsAccount().
|
||||
final Object storedSettlementPaymentTypeId = record.get("settlement_payment_type_id");
|
||||
final Long settlementPaymentTypeId = storedSettlementPaymentTypeId instanceof Number
|
||||
? ((Number) storedSettlementPaymentTypeId).longValue()
|
||||
@@ -715,14 +741,17 @@ public class ClerkShareWithdrawalRequestApiResource {
|
||||
* Reads the amount straight off the just-created redeem transaction row rather than re-deriving
|
||||
* the unit price ourselves — the transaction was already priced by Fineract's own
|
||||
* {@code ShareProduct#deriveMarketPrice(requestedDate)} (the requested date, not necessarily
|
||||
* today) and already nets out any redemption charge. Uses Fineract's own native savings deposit
|
||||
* command so the transaction is fully audited and posts its own GL entry the normal way — the
|
||||
* debit side lands on whichever GL account is mapped to the settlement payment type the clerk
|
||||
* chose at submit time (validated to be non-cash and persisted on the request row) — matching the
|
||||
* Share Product's own "Shares Reference" mapping (Share Sundry) — so the two entries net to zero
|
||||
* on that bridge account. Falls back to the legacy hardcoded
|
||||
* {@link #SHARE_REDEMPTION_PAYMENT_TYPE_CODE} lookup only for requests submitted before this
|
||||
* column existed.
|
||||
* today) and already nets out any redemption charge.
|
||||
*
|
||||
* <p>
|
||||
* Posted through Fineract's own native savings deposit command first, so the transaction is fully audited and the
|
||||
* member's passbook/statement carry it exactly as an over-the-counter deposit would. That command's own GL entry
|
||||
* (Dr <i>whichever GL the settlement payment type's Fund Source maps to</i> / Cr Savings Control) is then
|
||||
* <b>reversed in full</b> and replaced with an explicit Dr Sundry / Cr Savings Control pair — the debit side used
|
||||
* to be left to the payment type's own mapping in the hope it would land on the same account
|
||||
* {@link #routeRedeemIntoSundry} just credited, netting to zero; when that mapping pointed anywhere else (as it
|
||||
* did the day this was found — Sundry, when it should have matched Cash at Vault) the two legs never actually
|
||||
* met, leaving Sundry debited for money that was never credited to it. Explicit beats coincidental.
|
||||
*/
|
||||
private Settlement creditLinkedSavingsAccount(final Long shareAccountId, final Long shareTransactionId, final String requestedDateStr,
|
||||
final String resolutionNumber, final Long settlementPaymentTypeId) {
|
||||
@@ -761,7 +790,35 @@ public class ClerkShareWithdrawalRequestApiResource {
|
||||
final CommandWrapper commandRequest = new CommandWrapperBuilder().savingsAccountDeposit(savingsAccountId)
|
||||
.withJson(payload.toString()).build();
|
||||
final CommandProcessingResult result = commandsSourceWritePlatformService.logCommandSource(commandRequest);
|
||||
return new Settlement(settlementAmount, result.getResourceId());
|
||||
final Long savingsTransactionId = result.getResourceId();
|
||||
|
||||
// The deposit's own GL entries (whatever they turned out to be) are reversed wholesale rather
|
||||
// than picked apart leg-by-leg — the correct Dr Sundry / Cr Savings Control pair below stands
|
||||
// on its own regardless of which GL the payment type's Fund Source mapping happened to hit.
|
||||
jdbcTemplate.update("UPDATE acc_gl_journal_entry SET reversed = true WHERE transaction_id = ?", "S" + savingsTransactionId);
|
||||
|
||||
final Map<String, Object> acct = loadShareAccountGlContext(shareAccountId);
|
||||
final Long productId = toLong(acct.get("product_id"));
|
||||
final Long officeId = toLong(acct.get("office_id"));
|
||||
final String currencyCode = (String) acct.get("currency_code");
|
||||
final Long glSundry = requireShareGl(productId, FA_SHARES_SUSPENSE, "Share Sundry (Suspense)");
|
||||
final Long glSavingsControl = savingsControlGlOf(savingsAccountId);
|
||||
if (glSavingsControl == null) {
|
||||
throw apiError(409, "The Savings Control GL is not mapped for this savings account's product.");
|
||||
}
|
||||
|
||||
final LocalDate entryDate = LocalDate.parse(requestedDateStr.substring(0, 10));
|
||||
final long userId = context.authenticatedUser().getId();
|
||||
final LocalDateTime now = LocalDateTime.now(DateUtils.getDateTimeZoneOfTenant());
|
||||
final String txnId = "SHRDMSTL-" + System.nanoTime();
|
||||
final String accountNo = (String) acct.get("account_no");
|
||||
|
||||
insertEntry(officeId, glSundry, DEBIT, settlementAmount, txnId, entryDate, shareAccountId, userId, now, currencyCode,
|
||||
"Share redemption settled to savings | " + accountNo);
|
||||
insertEntry(officeId, glSavingsControl, CREDIT, settlementAmount, txnId, entryDate, shareAccountId, userId, now, currencyCode,
|
||||
"Share redemption settlement credited to savings | " + accountNo);
|
||||
|
||||
return new Settlement(settlementAmount, savingsTransactionId);
|
||||
}
|
||||
|
||||
// ── POST /v1/clerk/share-withdrawal-requests/{id}/reject ── Manager rejects ─
|
||||
@@ -832,6 +889,125 @@ public class ClerkShareWithdrawalRequestApiResource {
|
||||
}
|
||||
}
|
||||
|
||||
// ── Sundry pass-through helpers ──────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Redirects the native {@code redeemshares} posting's Cr Shares Reference (Cash at Vault) leg into Cr Sundry.
|
||||
*
|
||||
* <p>
|
||||
* Fineract's own accounting bridge assumes a redemption hands cash back over the counter (Dr Share Equity / Cr
|
||||
* Shares Reference) — right for a walk-in cash redemption, wrong here, where the value is heading to the
|
||||
* member's linked savings instead. Left alone, that Cr Shares Reference leg has no partner: the settlement
|
||||
* posted in {@link #creditLinkedSavingsAccount} debits Sundry, not Shares Reference, so Sundry would be debited
|
||||
* for money it was never credited with. Reversing just this one leg (matched by the specific "Shares Reference"
|
||||
* GL account, not merely "the credit leg" — a chargeable redemption also credits Income from Fees, which must be
|
||||
* left standing) and replacing it with Cr Sundry closes that gap: the account genuinely passes through Sundry
|
||||
* the same way every other share-capital movement in this codebase does.
|
||||
*/
|
||||
private void routeRedeemIntoSundry(final Long shareAccountId, final Long shareTransactionId, final LocalDate entryDate,
|
||||
final long userId) {
|
||||
final Map<String, Object> acct = loadShareAccountGlContext(shareAccountId);
|
||||
final Long productId = toLong(acct.get("product_id"));
|
||||
final Long officeId = toLong(acct.get("office_id"));
|
||||
final String currencyCode = (String) acct.get("currency_code");
|
||||
final String accountNo = (String) acct.get("account_no");
|
||||
|
||||
final Long glReference = requireShareGl(productId, FA_SHARES_REFERENCE, "Shares Reference");
|
||||
final Long glSundry = requireShareGl(productId, FA_SHARES_SUSPENSE, "Share Sundry (Suspense)");
|
||||
|
||||
final Map<String, Object> reversedLeg = jdbcTemplate
|
||||
.queryForList(
|
||||
"SELECT amount, transaction_id FROM acc_gl_journal_entry WHERE share_transaction_id = ? AND account_id = ? "
|
||||
+ " AND type_enum = ? AND reversed = false",
|
||||
shareTransactionId, glReference, CREDIT)
|
||||
.stream().findFirst().orElse(null);
|
||||
if (reversedLeg == null) {
|
||||
// Nothing to redirect -- the native posting didn't credit Shares Reference at all (an
|
||||
// unexpected accounting configuration), so there is no leg here to reroute.
|
||||
return;
|
||||
}
|
||||
final BigDecimal amount = (BigDecimal) reversedLeg.get("amount");
|
||||
// Reuses the native posting's OWN transaction_id rather than minting a new one -- entries are
|
||||
// grouped and checked for balance by transaction_id, so a fresh id here would leave the Dr
|
||||
// Share Equity leg standing alone as "unbalanced" and this new Cr Sundry leg equally alone,
|
||||
// instead of the two reading as the one balanced entry they actually are.
|
||||
final String nativeTxnId = (String) reversedLeg.get("transaction_id");
|
||||
|
||||
jdbcTemplate.update(
|
||||
"UPDATE acc_gl_journal_entry SET reversed = true WHERE share_transaction_id = ? AND account_id = ? "
|
||||
+ " AND type_enum = ? AND reversed = false",
|
||||
shareTransactionId, glReference, CREDIT);
|
||||
|
||||
final LocalDateTime now = LocalDateTime.now(DateUtils.getDateTimeZoneOfTenant());
|
||||
insertEntry(officeId, glSundry, CREDIT, amount, nativeTxnId, entryDate, shareAccountId, userId, now, currencyCode,
|
||||
"Share redemption held in Sundry pending settlement | " + accountNo);
|
||||
}
|
||||
|
||||
private Map<String, Object> loadShareAccountGlContext(final Long shareAccountId) {
|
||||
return jdbcTemplate.queryForMap("""
|
||||
SELECT sa.account_no, sa.product_id, sa.currency_code, c.office_id
|
||||
FROM m_share_account sa JOIN m_client c ON c.id = sa.client_id
|
||||
WHERE sa.id = ?
|
||||
""", shareAccountId);
|
||||
}
|
||||
|
||||
private Long requireShareGl(final Long productId, final int financialAccountType, final String label) {
|
||||
final Long glId = resolveGl(productId, financialAccountType);
|
||||
if (glId == null) {
|
||||
throw apiError(409, label + " GL is not mapped for share product " + productId + ".");
|
||||
}
|
||||
return glId;
|
||||
}
|
||||
|
||||
private Long resolveGl(final Long productId, final int financialAccountType) {
|
||||
try {
|
||||
return jdbcTemplate.queryForObject(
|
||||
"SELECT gl_account_id FROM acc_product_mapping "
|
||||
+ " WHERE product_id = ? AND product_type = ? AND financial_account_type = ? AND charge_id IS NULL",
|
||||
Long.class, productId, PRODUCT_TYPE_SHARES, financialAccountType);
|
||||
} catch (final IncorrectResultSizeDataAccessException e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private Long savingsControlGlOf(final Long savingsAccountId) {
|
||||
final List<Map<String, Object>> rows = jdbcTemplate.queryForList("""
|
||||
SELECT apm.gl_account_id FROM m_savings_account sa
|
||||
JOIN acc_product_mapping apm ON apm.product_id = sa.product_id
|
||||
WHERE sa.id = ? AND apm.product_type = ? AND apm.financial_account_type = ? LIMIT 1
|
||||
""", savingsAccountId, PRODUCT_TYPE_SAVINGS, FA_SAVINGS_CONTROL);
|
||||
return rows.isEmpty() ? null : toLong(rows.get(0).get("gl_account_id"));
|
||||
}
|
||||
|
||||
private void insertEntry(final Long officeId, final Long glAccountId, final int typeEnum, final BigDecimal amount, final String txnId,
|
||||
final LocalDate entryDate, final Long shareAccountId, final long userId, final LocalDateTime now, final String currencyCode,
|
||||
final String description) {
|
||||
jdbcTemplate.update(
|
||||
"INSERT INTO acc_gl_journal_entry " + "(account_id, office_id, currency_code, transaction_id, reversed, manual_entry, "
|
||||
+ " entry_date, type_enum, amount, description, entity_id, "
|
||||
+ " created_by, last_modified_by, created_date, lastmodified_date, "
|
||||
+ " is_running_balance_calculated, office_running_balance, "
|
||||
+ " organization_running_balance, submitted_on_date, transaction_date, " + " created_on_utc, last_modified_on_utc) "
|
||||
+ "VALUES (?,?,?,?,false,true,?,?,?,?,?,?,?,?,?,false,0,0,?,?,?,?)",
|
||||
glAccountId, officeId, currencyCode, txnId, java.sql.Date.valueOf(entryDate), typeEnum, amount, description, shareAccountId,
|
||||
userId, userId, now, now, java.sql.Date.valueOf(entryDate), java.sql.Date.valueOf(entryDate), toUtc(now), toUtc(now));
|
||||
}
|
||||
|
||||
/**
|
||||
* A tenant-local timestamp converted to the real UTC instant, for the {@code *_on_utc} columns.
|
||||
*
|
||||
* {@code atOffset(ZoneOffset.UTC)} would only relabel it — stamping 18:34 IST as "18:34 UTC", which reads back
|
||||
* as 00:04 the next day. The instant has to be resolved in the tenant's own zone first, which is the zone
|
||||
* {@code now} was taken in.
|
||||
*/
|
||||
private java.time.OffsetDateTime toUtc(final LocalDateTime tenantLocalTime) {
|
||||
return tenantLocalTime.atZone(DateUtils.getDateTimeZoneOfTenant()).toOffsetDateTime().withOffsetSameInstant(ZoneOffset.UTC);
|
||||
}
|
||||
|
||||
private Long toLong(final Object o) {
|
||||
return o instanceof Number ? ((Number) o).longValue() : null;
|
||||
}
|
||||
|
||||
private String buildActivityLog(final String existingJson, final String action, final String username, final String note) {
|
||||
JsonArray log = new JsonArray();
|
||||
if (existingJson != null && !existingJson.isBlank()) {
|
||||
|
||||
+5
@@ -605,6 +605,11 @@ public class ClerkWithdrawalRequestApiResource {
|
||||
throw apiError(409, "This withdrawal has already been withdrawn by another cashier. Double disbursement prevented.");
|
||||
}
|
||||
|
||||
// The voucher was raised in the clerk's name at submission, before any cashier was involved --
|
||||
// reattribute it to whoever is actually handing over the cash now, so it shows up in their own
|
||||
// voucher register rather than only in the clerk's.
|
||||
voucherService.updateCashier(VoucherService.REF_CLERK_WITHDRAWAL_REQUEST, id, currentUser);
|
||||
|
||||
// Record denomination breakdown in m_savings_txn_denomination so cashier vault position updates
|
||||
if (denominationData != null && !denominationData.isBlank()) {
|
||||
saveDisbursementDenomination(denominationData, record, currentUser);
|
||||
|
||||
+10
-2
@@ -340,9 +340,17 @@ public class ShareAccountWritePlatformServiceJpaRepositoryImpl implements ShareA
|
||||
}
|
||||
}
|
||||
LocalDate transactionDate = DateUtils.getBusinessLocalDate();
|
||||
// Reverts whatever approveShareAccount() posted -- which, per the comment there, is
|
||||
// nothing. Kept anyway as a no-op safety net for any transaction that predates that fix.
|
||||
this.journalEntryWritePlatformService.revertShareAccountJournalEntries(journalEntryTransactions, transactionDate);
|
||||
journalEntryWritePlatformService.createJournalEntriesForShares(
|
||||
populateJournalEntries(account, account.getPendingForApprovalSharePurchaseTransactions()));
|
||||
// Deliberately NOT re-running the accounting bridge for the transactions this undo just
|
||||
// put back to APPLIED. That bridge's "isApplied" branch posts Dr Shares Reference (Cash at
|
||||
// Vault) / Cr Share Sundry, assuming cash was already received at the counter for these
|
||||
// shares -- exactly the phantom entry approveShareAccount()/createShareAccount() were
|
||||
// fixed to stop posting. Calling it here reintroduced the same phantom entry by a side
|
||||
// door: undo-approve reverting a transaction to APPLIED status looks, to the bridge, just
|
||||
// like a brand-new application being submitted. Money only ever reaches or leaves share
|
||||
// capital through ShareAdvanceCollectionApiResource's own explicit Sundry-routed postings.
|
||||
return new CommandProcessingResultBuilder() //
|
||||
.withCommandId(jsonCommand.commandId()) //
|
||||
.withEntityId(accountId) //
|
||||
|
||||
+15
@@ -467,4 +467,19 @@ public class VoucherService {
|
||||
"UPDATE m_voucher SET amount = ? WHERE reference_type = ? AND reference_id = ?",
|
||||
amount, referenceType, referenceId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Reattributes a voucher to the cashier who actually disbursed the cash, once that is known.
|
||||
*
|
||||
* A clerk withdrawal is raised by the clerk who takes the request, so that is who
|
||||
* {@link #createVoucher} stamps as the creator at submission time -- but no cash has moved yet,
|
||||
* and it is a cashier, at their own counter, who later hands it over. A voucher names whoever's
|
||||
* till the money actually came out of, so the per-user "your own vouchers" register a cashier
|
||||
* sees is complete rather than silently missing every withdrawal they personally paid out.
|
||||
*/
|
||||
public void updateCashier(final String referenceType, final Long referenceId, final AppUser cashier) {
|
||||
jdbcTemplate.update(
|
||||
"UPDATE m_voucher SET created_by = ?, created_by_username = ? WHERE reference_type = ? AND reference_id = ?",
|
||||
cashier.getId(), cashier.getUsername(), referenceType, referenceId);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -502,7 +502,6 @@
|
||||
<include file="parts/0447_add_tiered_thresholds_and_low_balance_rule.xml" relativeToChangelogFile="true"/>
|
||||
<include file="parts/0448_fill_client_risk_rule_income_turnover_nulls.xml" relativeToChangelogFile="true"/>
|
||||
<include file="parts/0450_add_aml_rule_limit_bands.xml" relativeToChangelogFile="true"/>
|
||||
<include file="parts/0451_seed_branch_offices.xml" relativeToChangelogFile="true"/>
|
||||
<include file="parts/0452_seed_aml_manual_referral_rule.xml" relativeToChangelogFile="true"/>
|
||||
<include file="parts/0453_add_email_tracking_to_client_pending_edit.xml" relativeToChangelogFile="true"/>
|
||||
<include file="parts/0454_add_email_from_to_client_pending_edit.xml" relativeToChangelogFile="true"/>
|
||||
|
||||
@@ -47,4 +47,7 @@
|
||||
<include file="parts/0518_create_savings_interest_posting_schedule.xml" relativeToChangelogFile="true"/>
|
||||
<include file="parts/0519_post_savings_interest_at_period_end.xml" relativeToChangelogFile="true"/>
|
||||
<include file="parts/0520_drop_share_savings_debit_request.xml" relativeToChangelogFile="true"/>
|
||||
<include file="parts/0521_branch_day_register_vault_only_detail.xml" relativeToChangelogFile="true"/>
|
||||
<include file="parts/0522_seed_opening_balance_contra_account.xml" relativeToChangelogFile="true"/>
|
||||
<include file="parts/0523_seed_basoda_bank_chart_of_accounts.xml" relativeToChangelogFile="true"/>
|
||||
</databaseChangeLog>
|
||||
|
||||
-59
@@ -1,59 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!--
|
||||
|
||||
Licensed to the Apache Software Foundation (ASF) under one
|
||||
or more contributor license agreements. See the NOTICE file
|
||||
distributed with this work for additional information
|
||||
regarding copyright ownership. The ASF licenses this file
|
||||
to you under the Apache License, Version 2.0 (the
|
||||
"License"); you may not use this file except in compliance
|
||||
with the License. You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing,
|
||||
software distributed under the License is distributed on an
|
||||
"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
KIND, either express or implied. See the License for the
|
||||
specific language governing permissions and limitations
|
||||
under the License.
|
||||
|
||||
-->
|
||||
<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-4.13.xsd">
|
||||
|
||||
<!--
|
||||
Branch offices under Head Office.
|
||||
|
||||
Until now the tenant had a single office, so "see only my branch" had nothing to
|
||||
scope to. external_id carries the branch code used by the General Ledger and the
|
||||
RBI return screens, which is what lets a logged-in user's office be resolved to a
|
||||
branch without matching on display names.
|
||||
-->
|
||||
<changeSet author="fineract" id="0451-1">
|
||||
<preConditions onFail="MARK_RAN">
|
||||
<sqlCheck expectedResult="0">SELECT COUNT(*) FROM m_office WHERE external_id IN ('0002','0003','0004','0005','0006')</sqlCheck>
|
||||
</preConditions>
|
||||
<sql>
|
||||
INSERT INTO m_office (parent_id, hierarchy, external_id, name, opening_date)
|
||||
VALUES
|
||||
((SELECT id FROM m_office WHERE hierarchy = '.'), '.2.', '0002', 'Andheri (E)', DATE '2018-04-01'),
|
||||
((SELECT id FROM m_office WHERE hierarchy = '.'), '.3.', '0003', 'Bandra (W)', DATE '2018-04-01'),
|
||||
((SELECT id FROM m_office WHERE hierarchy = '.'), '.4.', '0004', 'Borivali', DATE '2019-07-01'),
|
||||
((SELECT id FROM m_office WHERE hierarchy = '.'), '.5.', '0005', 'Dadar', DATE '2020-01-02'),
|
||||
((SELECT id FROM m_office WHERE hierarchy = '.'), '.6.', '0006', 'Thane', DATE '2021-06-01');
|
||||
</sql>
|
||||
</changeSet>
|
||||
|
||||
<!-- Head Office carries the branch code the GL ledger uses for it. -->
|
||||
<changeSet author="fineract" id="0451-2">
|
||||
<preConditions onFail="MARK_RAN">
|
||||
<sqlCheck expectedResult="0">SELECT COUNT(*) FROM m_office WHERE external_id = 'HO'</sqlCheck>
|
||||
</preConditions>
|
||||
<sql>
|
||||
UPDATE m_office SET external_id = 'HO' WHERE hierarchy = '.';
|
||||
</sql>
|
||||
</changeSet>
|
||||
|
||||
</databaseChangeLog>
|
||||
+38
@@ -0,0 +1,38 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog
|
||||
http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-4.1.xsd">
|
||||
|
||||
<!-- OPENING/CLOSING rows in m_branch_day_register_detail (0510) are the branch total: vault plus
|
||||
every cashier's cash added together. That total is correct for the day's book reconciliation
|
||||
(closing_cash vs book_cash is a branch-wide question), but the vault-position report reused
|
||||
the same rows as if they were the vault's own cash alone. Whatever a cashier was still
|
||||
holding, unsettled, at close got carried forward as if it belonged to the vault, then counted
|
||||
again the day that cash was actually settled back to the vault, inflating the vault's reported
|
||||
position by exactly the unsettled amount. This adds a second, vault-only pair of legs so the
|
||||
vault-position report can anchor on the vault's real cash instead of the branch total. -->
|
||||
|
||||
<changeSet id="0521_widen_branch_day_detail_leg" author="cbs-nitesh">
|
||||
<preConditions onFail="MARK_RAN">
|
||||
<columnExists tableName="m_branch_day_register_detail" columnName="leg"/>
|
||||
</preConditions>
|
||||
<modifyDataType tableName="m_branch_day_register_detail" columnName="leg" newDataType="VARCHAR(20)"/>
|
||||
</changeSet>
|
||||
|
||||
<changeSet id="0521_add_branch_day_detail_teller" author="cbs-nitesh">
|
||||
<preConditions onFail="MARK_RAN">
|
||||
<not><columnExists tableName="m_branch_day_register_detail" columnName="teller_id"/></not>
|
||||
</preConditions>
|
||||
<addColumn tableName="m_branch_day_register_detail">
|
||||
<!-- NULL on every existing (and every future branch-total) row - only the new
|
||||
OPENING_VAULT/CLOSING_VAULT rows carry a teller_id, which is what tells
|
||||
buildPositionForVault which rows are its own. -->
|
||||
<column name="teller_id" type="BIGINT"/>
|
||||
</addColumn>
|
||||
<addForeignKeyConstraint baseTableName="m_branch_day_register_detail" baseColumnNames="teller_id"
|
||||
constraintName="fk_branch_day_detail_teller" referencedTableName="m_tellers"
|
||||
referencedColumnNames="id" onDelete="CASCADE"/>
|
||||
</changeSet>
|
||||
|
||||
</databaseChangeLog>
|
||||
+36
@@ -0,0 +1,36 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog
|
||||
http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-4.1.xsd">
|
||||
|
||||
<!-- Financial activity 300 (OPENING_BALANCES_TRANSFER_CONTRA) is the equity contra Fineract
|
||||
posts against whenever an account is opened with a non-zero opening balance - exactly the
|
||||
"conversion suspense" leg a live bank's opening balances get migrated through. No chart of
|
||||
accounts in this fork ever seeded a GL account for it, so any screen that touches an opening
|
||||
balance (including plain report/account-open flows) fails with "Financial Activity account
|
||||
... with Id 300 does not exist" the first time it is hit. This backfills the missing account
|
||||
and its mapping for every tenant - existing ones now, and any new tenant going forward. -->
|
||||
|
||||
<changeSet id="0522_seed_opening_balance_contra_account" author="cbs-nitesh">
|
||||
<preConditions onFail="MARK_RAN">
|
||||
<sqlCheck expectedResult="0">SELECT COUNT(*) FROM acc_gl_account WHERE gl_code = '300000'</sqlCheck>
|
||||
</preConditions>
|
||||
<sql>
|
||||
INSERT INTO acc_gl_account
|
||||
(name, gl_code, disabled, manual_journal_entries_allowed, account_usage, classification_enum)
|
||||
VALUES ('Opening Balances Transfer Contra', '300000', false, true, 1, 3);
|
||||
</sql>
|
||||
</changeSet>
|
||||
|
||||
<changeSet id="0522_map_opening_balance_contra_financial_activity" author="cbs-nitesh">
|
||||
<preConditions onFail="MARK_RAN">
|
||||
<sqlCheck expectedResult="0">SELECT COUNT(*) FROM acc_gl_financial_activity_account WHERE financial_activity_type = 300</sqlCheck>
|
||||
</preConditions>
|
||||
<sql>
|
||||
INSERT INTO acc_gl_financial_activity_account (financial_activity_type, gl_account_id)
|
||||
VALUES (300, (SELECT id FROM acc_gl_account WHERE gl_code = '300000'));
|
||||
</sql>
|
||||
</changeSet>
|
||||
|
||||
</databaseChangeLog>
|
||||
+237
@@ -0,0 +1,237 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog
|
||||
http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-4.1.xsd">
|
||||
|
||||
<!-- Seeds a brand-new tenant with Basoda Nagrik Sahakari Bank's real chart of accounts,
|
||||
transcribed line by line from its 31/03/2026 balance sheet. Guarded on "no GL account exists
|
||||
other than the opening-balance contra" rather than "GL is completely empty" - 0522
|
||||
(immediately before this one) seeds every tenant, including a fresh one, with that one GL
|
||||
account before this changeset runs, so a plain empty-table check would never be true and
|
||||
this would silently never seed a single future tenant, regardless of which of the two runs
|
||||
first. An existing tenant that already built its own chart of accounts (socity_bank,
|
||||
default) has plenty of other accounts and is never touched - only a genuinely fresh one is
|
||||
(this is how abc_bank got seeded, and how any future new tenant will be too). The
|
||||
opening-balance contra account and its financial-activity-300 mapping are 0522's job, not
|
||||
repeated here. -->
|
||||
|
||||
<changeSet id="0523_seed_basoda_bank_chart_of_accounts" author="cbs-nitesh">
|
||||
<preConditions onFail="MARK_RAN">
|
||||
<sqlCheck expectedResult="0">SELECT COUNT(*) FROM acc_gl_account WHERE gl_code != '300000'</sqlCheck>
|
||||
</preConditions>
|
||||
<sql>
|
||||
<![CDATA[
|
||||
|
||||
-- classification_enum: 1=ASSET, 2=LIABILITY, 3=EQUITY, 4=INCOME, 5=EXPENSE
|
||||
-- account_usage: 2=header (group, not postable), 1=detail (postable leaf)
|
||||
|
||||
-- ============================= EQUITY (3) =============================
|
||||
|
||||
INSERT INTO acc_gl_account (name, gl_code, disabled, manual_journal_entries_allowed, account_usage, classification_enum) VALUES
|
||||
('CAPITAL', '301000', false, true, 2, 3);
|
||||
INSERT INTO acc_gl_account (name, gl_code, disabled, manual_journal_entries_allowed, account_usage, classification_enum, parent_id) VALUES
|
||||
('LINKING SHARE A/C', '301001', false, true, 1, 3, (SELECT id FROM acc_gl_account WHERE gl_code='301000')),
|
||||
('SHARE HOLDER''S A/C', '301002', false, true, 1, 3, (SELECT id FROM acc_gl_account WHERE gl_code='301000'));
|
||||
|
||||
INSERT INTO acc_gl_account (name, gl_code, disabled, manual_journal_entries_allowed, account_usage, classification_enum) VALUES
|
||||
('RESERVES & OTHER RESERVES', '302000', false, true, 2, 3);
|
||||
INSERT INTO acc_gl_account (name, gl_code, disabled, manual_journal_entries_allowed, account_usage, classification_enum, parent_id) VALUES
|
||||
('BAD & DOUBTFUL RESERVE FUND', '302001', false, true, 1, 3, (SELECT id FROM acc_gl_account WHERE gl_code='302000')),
|
||||
('BUILDING FUND A/C', '302002', false, true, 1, 3, (SELECT id FROM acc_gl_account WHERE gl_code='302000')),
|
||||
('CHARITABLE FUND A/C', '302003', false, true, 1, 3, (SELECT id FROM acc_gl_account WHERE gl_code='302000')),
|
||||
('COMPUTER FUND A/C', '302004', false, true, 1, 3, (SELECT id FROM acc_gl_account WHERE gl_code='302000')),
|
||||
('DEVELOPMENT FUND A/C', '302005', false, true, 1, 3, (SELECT id FROM acc_gl_account WHERE gl_code='302000')),
|
||||
('DIVIDEND EQUALIZATION FUND', '302006', false, true, 1, 3, (SELECT id FROM acc_gl_account WHERE gl_code='302000')),
|
||||
('EMPLOYEE WELFARE FUND A/C', '302007', false, true, 1, 3, (SELECT id FROM acc_gl_account WHERE gl_code='302000')),
|
||||
('INVESTMENT DEPRECIATION RESERVE', '302008', false, true, 1, 3, (SELECT id FROM acc_gl_account WHERE gl_code='302000')),
|
||||
('INVESTMENT FLUCTUATION FUND', '302009', false, true, 1, 3, (SELECT id FROM acc_gl_account WHERE gl_code='302000')),
|
||||
('STATUTORY RESERVE FUND', '302010', false, true, 1, 3, (SELECT id FROM acc_gl_account WHERE gl_code='302000')),
|
||||
('TRAINING FUND A/C', '302011', false, true, 1, 3, (SELECT id FROM acc_gl_account WHERE gl_code='302000')),
|
||||
('VEHICLE FUND A/C', '302012', false, true, 1, 3, (SELECT id FROM acc_gl_account WHERE gl_code='302000'));
|
||||
|
||||
INSERT INTO acc_gl_account (name, gl_code, disabled, manual_journal_entries_allowed, account_usage, classification_enum) VALUES
|
||||
('P & L A/C', '303000', false, true, 2, 3);
|
||||
INSERT INTO acc_gl_account (name, gl_code, disabled, manual_journal_entries_allowed, account_usage, classification_enum, parent_id) VALUES
|
||||
('PROFIT A/C', '303001', false, true, 1, 3, (SELECT id FROM acc_gl_account WHERE gl_code='303000'));
|
||||
|
||||
-- ============================= LIABILITY (2) =============================
|
||||
|
||||
INSERT INTO acc_gl_account (name, gl_code, disabled, manual_journal_entries_allowed, account_usage, classification_enum) VALUES
|
||||
('DEPOSIT', '201000', false, true, 2, 2);
|
||||
INSERT INTO acc_gl_account (name, gl_code, disabled, manual_journal_entries_allowed, account_usage, classification_enum, parent_id) VALUES
|
||||
('CURRENT DEPOSIT INDIVIDUAL', '201001', false, true, 1, 2, (SELECT id FROM acc_gl_account WHERE gl_code='201000')),
|
||||
('FIXED DEPOSIT DOUBLE', '201002', false, true, 1, 2, (SELECT id FROM acc_gl_account WHERE gl_code='201000')),
|
||||
('FIXED DEPOSIT SINGLE', '201003', false, true, 1, 2, (SELECT id FROM acc_gl_account WHERE gl_code='201000')),
|
||||
('MANGAL PARINAY A/C', '201004', false, true, 1, 2, (SELECT id FROM acc_gl_account WHERE gl_code='201000')),
|
||||
('MASIK DHAN VARSHA', '201005', false, true, 1, 2, (SELECT id FROM acc_gl_account WHERE gl_code='201000')),
|
||||
('NAVEEN MINI DEPOSIT', '201006', false, true, 1, 2, (SELECT id FROM acc_gl_account WHERE gl_code='201000')),
|
||||
('RECURRING DEPOSIT INDIVIDUAL', '201007', false, true, 1, 2, (SELECT id FROM acc_gl_account WHERE gl_code='201000')),
|
||||
('SAVING DEPOSIT INDIVIDUAL', '201008', false, true, 1, 2, (SELECT id FROM acc_gl_account WHERE gl_code='201000')),
|
||||
('SUPER MINI DEPOSIT A/C', '201009', false, true, 1, 2, (SELECT id FROM acc_gl_account WHERE gl_code='201000'));
|
||||
|
||||
INSERT INTO acc_gl_account (name, gl_code, disabled, manual_journal_entries_allowed, account_usage, classification_enum) VALUES
|
||||
('OTHER LIABILITIES', '202000', false, true, 2, 2);
|
||||
INSERT INTO acc_gl_account (name, gl_code, disabled, manual_journal_entries_allowed, account_usage, classification_enum, parent_id) VALUES
|
||||
('DIVIDEND PAYABLE', '202001', false, true, 1, 2, (SELECT id FROM acc_gl_account WHERE gl_code='202000')),
|
||||
('DONATION TO JILA SAHAKARI SANGH', '202002', false, true, 1, 2, (SELECT id FROM acc_gl_account WHERE gl_code='202000')),
|
||||
('SHARE CAPITAL PAYABLE', '202003', false, true, 1, 2, (SELECT id FROM acc_gl_account WHERE gl_code='202000')),
|
||||
('SUNDRY CREDITORS A/C', '202004', false, true, 1, 2, (SELECT id FROM acc_gl_account WHERE gl_code='202000')),
|
||||
('SUSPENSE A/C', '202005', false, true, 1, 2, (SELECT id FROM acc_gl_account WHERE gl_code='202000')),
|
||||
('INCOME TAX OF BANK EMPLOYEE', '202006', false, true, 1, 2, (SELECT id FROM acc_gl_account WHERE gl_code='202000')),
|
||||
('T.D.S. TAX DEDUCTED AT SOURCE', '202007', false, true, 1, 2, (SELECT id FROM acc_gl_account WHERE gl_code='202000')),
|
||||
('ARREAR OF BANK EMPLOYEES', '202008', false, true, 1, 2, (SELECT id FROM acc_gl_account WHERE gl_code='202000')),
|
||||
('CGST PAYABLE A/C', '202009', false, true, 1, 2, (SELECT id FROM acc_gl_account WHERE gl_code='202000')),
|
||||
('SGST PAYABLE A/C', '202010', false, true, 1, 2, (SELECT id FROM acc_gl_account WHERE gl_code='202000')),
|
||||
('CREDIT BALANCE OF CASH', '202011', false, true, 1, 2, (SELECT id FROM acc_gl_account WHERE gl_code='202000')),
|
||||
('CREDIT BALANCE OF FCC LOAN', '202012', false, true, 1, 2, (SELECT id FROM acc_gl_account WHERE gl_code='202000')),
|
||||
('CREDIT BALANCE OF ML LOAN', '202013', false, true, 1, 2, (SELECT id FROM acc_gl_account WHERE gl_code='202000'));
|
||||
|
||||
INSERT INTO acc_gl_account (name, gl_code, disabled, manual_journal_entries_allowed, account_usage, classification_enum) VALUES
|
||||
('BILLS FOR COLLECTION', '203000', false, true, 2, 2);
|
||||
INSERT INTO acc_gl_account (name, gl_code, disabled, manual_journal_entries_allowed, account_usage, classification_enum, parent_id) VALUES
|
||||
('DD/BILLS PAYABLE A/C', '203001', false, true, 1, 2, (SELECT id FROM acc_gl_account WHERE gl_code='203000'));
|
||||
|
||||
INSERT INTO acc_gl_account (name, gl_code, disabled, manual_journal_entries_allowed, account_usage, classification_enum) VALUES
|
||||
('INTEREST PAYABLE', '204000', false, true, 2, 2);
|
||||
INSERT INTO acc_gl_account (name, gl_code, disabled, manual_journal_entries_allowed, account_usage, classification_enum, parent_id) VALUES
|
||||
('INTEREST ACCRUED FD SINGLE', '204001', false, true, 1, 2, (SELECT id FROM acc_gl_account WHERE gl_code='204000')),
|
||||
('INTEREST ACCRUED FD DOUBLE', '204002', false, true, 1, 2, (SELECT id FROM acc_gl_account WHERE gl_code='204000'));
|
||||
|
||||
INSERT INTO acc_gl_account (name, gl_code, disabled, manual_journal_entries_allowed, account_usage, classification_enum) VALUES
|
||||
('PROVISION', '205000', false, true, 2, 2);
|
||||
INSERT INTO acc_gl_account (name, gl_code, disabled, manual_journal_entries_allowed, account_usage, classification_enum, parent_id) VALUES
|
||||
('AUDIT FEE PROVISION', '205001', false, true, 1, 2, (SELECT id FROM acc_gl_account WHERE gl_code='205000')),
|
||||
('PROVISION FOR INCOME TAX LIABILITY', '205002', false, true, 1, 2, (SELECT id FROM acc_gl_account WHERE gl_code='205000'));
|
||||
|
||||
INSERT INTO acc_gl_account (name, gl_code, disabled, manual_journal_entries_allowed, account_usage, classification_enum) VALUES
|
||||
('CONTINGENT LIABILITIES', '206000', false, true, 2, 2);
|
||||
INSERT INTO acc_gl_account (name, gl_code, disabled, manual_journal_entries_allowed, account_usage, classification_enum, parent_id) VALUES
|
||||
('CONTINGENT PROVISION', '206001', false, true, 1, 2, (SELECT id FROM acc_gl_account WHERE gl_code='206000')),
|
||||
('PROVISION FOR NPA A/C', '206002', false, true, 1, 2, (SELECT id FROM acc_gl_account WHERE gl_code='206000'));
|
||||
|
||||
INSERT INTO acc_gl_account (name, gl_code, disabled, manual_journal_entries_allowed, account_usage, classification_enum) VALUES
|
||||
('CONTRA ACCOUNTS', '207000', false, true, 2, 2);
|
||||
INSERT INTO acc_gl_account (name, gl_code, disabled, manual_journal_entries_allowed, account_usage, classification_enum, parent_id) VALUES
|
||||
('INTEREST PROVISION A/C', '207001', false, true, 1, 2, (SELECT id FROM acc_gl_account WHERE gl_code='207000'));
|
||||
|
||||
-- ============================= ASSET (1) =============================
|
||||
|
||||
INSERT INTO acc_gl_account (name, gl_code, disabled, manual_journal_entries_allowed, account_usage, classification_enum) VALUES
|
||||
('CASH ON HAND', '101000', false, true, 2, 1);
|
||||
INSERT INTO acc_gl_account (name, gl_code, disabled, manual_journal_entries_allowed, account_usage, classification_enum, parent_id) VALUES
|
||||
('BRANCH CASH BALANCE', '101001', false, true, 1, 1, (SELECT id FROM acc_gl_account WHERE gl_code='101000'));
|
||||
|
||||
INSERT INTO acc_gl_account (name, gl_code, disabled, manual_journal_entries_allowed, account_usage, classification_enum) VALUES
|
||||
('BALANCE WITH OTHER BANK - CURRENT A/C', '102000', false, true, 2, 1);
|
||||
INSERT INTO acc_gl_account (name, gl_code, disabled, manual_journal_entries_allowed, account_usage, classification_enum, parent_id) VALUES
|
||||
('CURRENT A/C IDBI BANK 00007252', '102001', false, true, 1, 1, (SELECT id FROM acc_gl_account WHERE gl_code='102000')),
|
||||
('CURRENT A/C CO-OP. BANK MAIN BR.41', '102002', false, true, 1, 1, (SELECT id FROM acc_gl_account WHERE gl_code='102000')),
|
||||
('CURRENT A/C CO-OP. BANK CITI BR.', '102003', false, true, 1, 1, (SELECT id FROM acc_gl_account WHERE gl_code='102000')),
|
||||
('CURRENT A/C UNION BANK OF INDIA', '102004', false, true, 1, 1, (SELECT id FROM acc_gl_account WHERE gl_code='102000')),
|
||||
('TRUSTY BASODA NAGRIK SAHAKARI BANK', '102005', false, true, 1, 1, (SELECT id FROM acc_gl_account WHERE gl_code='102000')),
|
||||
('CURRENT A/C HDFC BANK 0380000015', '102006', false, true, 1, 1, (SELECT id FROM acc_gl_account WHERE gl_code='102000')),
|
||||
('CURRENT A/C APEX BANK 187000801142', '102007', false, true, 1, 1, (SELECT id FROM acc_gl_account WHERE gl_code='102000')),
|
||||
('CURRENT A/C A.U. SMALL FINANCE BANK', '102008', false, true, 1, 1, (SELECT id FROM acc_gl_account WHERE gl_code='102000')),
|
||||
('CURRENT A/C DCB BANK 12822400150865', '102009', false, true, 1, 1, (SELECT id FROM acc_gl_account WHERE gl_code='102000')),
|
||||
('CURRENT A/C AXIS BANK LTD 071256359', '102010', false, true, 1, 1, (SELECT id FROM acc_gl_account WHERE gl_code='102000')),
|
||||
('IDBI BANK LTD CTS CLG A/C 000019', '102011', false, true, 1, 1, (SELECT id FROM acc_gl_account WHERE gl_code='102000')),
|
||||
('CURRENT A/C RBL BANK LTD 9001785747', '102012', false, true, 1, 1, (SELECT id FROM acc_gl_account WHERE gl_code='102000')),
|
||||
('CURRENT A/C BANK OF BARODA 001523', '102013', false, true, 1, 1, (SELECT id FROM acc_gl_account WHERE gl_code='102000')),
|
||||
('CURRENT A/C ICICI BANK BR. BASODA', '102014', false, true, 1, 1, (SELECT id FROM acc_gl_account WHERE gl_code='102000')),
|
||||
('CURRENT A/C YES BANK 001562 (ATM)', '102015', false, true, 1, 1, (SELECT id FROM acc_gl_account WHERE gl_code='102000')),
|
||||
('CURRENT A/C YES BANK-0071 (OUTWARD RTGS)', '102016', false, true, 1, 1, (SELECT id FROM acc_gl_account WHERE gl_code='102000')),
|
||||
('CURRENT A/C YES BANK-1552 (IMPS)', '102017', false, true, 1, 1, (SELECT id FROM acc_gl_account WHERE gl_code='102000')),
|
||||
('CURRENT A/C YES BANK-000198 (UPI)', '102018', false, true, 1, 1, (SELECT id FROM acc_gl_account WHERE gl_code='102000')),
|
||||
('CURRENT A/C DNSB BANK BBPS 645', '102019', false, true, 1, 1, (SELECT id FROM acc_gl_account WHERE gl_code='102000'));
|
||||
|
||||
INSERT INTO acc_gl_account (name, gl_code, disabled, manual_journal_entries_allowed, account_usage, classification_enum) VALUES
|
||||
('FIX DEPOSIT WITH BANK', '103000', false, true, 2, 1);
|
||||
INSERT INTO acc_gl_account (name, gl_code, disabled, manual_journal_entries_allowed, account_usage, classification_enum, parent_id) VALUES
|
||||
('FIXED DEPOSIT CO-OPER. BANK MAIN', '103001', false, true, 1, 1, (SELECT id FROM acc_gl_account WHERE gl_code='103000')),
|
||||
('INVESTMENT IDBI BANK', '103002', false, true, 1, 1, (SELECT id FROM acc_gl_account WHERE gl_code='103000')),
|
||||
('INVESTMENT WITH A.U. SMALL FINANCE BANK', '103003', false, true, 1, 1, (SELECT id FROM acc_gl_account WHERE gl_code='103000')),
|
||||
('INVESTMENT WITH D.C.B. BANK', '103004', false, true, 1, 1, (SELECT id FROM acc_gl_account WHERE gl_code='103000')),
|
||||
('INVESTMENT WITH R.B.L. BANK LTD', '103005', false, true, 1, 1, (SELECT id FROM acc_gl_account WHERE gl_code='103000'));
|
||||
|
||||
INSERT INTO acc_gl_account (name, gl_code, disabled, manual_journal_entries_allowed, account_usage, classification_enum) VALUES
|
||||
('ACCRUED INTEREST', '104000', false, true, 2, 1);
|
||||
INSERT INTO acc_gl_account (name, gl_code, disabled, manual_journal_entries_allowed, account_usage, classification_enum, parent_id) VALUES
|
||||
('INTEREST A/C', '104001', false, true, 1, 1, (SELECT id FROM acc_gl_account WHERE gl_code='104000'));
|
||||
|
||||
INSERT INTO acc_gl_account (name, gl_code, disabled, manual_journal_entries_allowed, account_usage, classification_enum) VALUES
|
||||
('INVESTMENT', '105000', false, true, 2, 1);
|
||||
INSERT INTO acc_gl_account (name, gl_code, disabled, manual_journal_entries_allowed, account_usage, classification_enum, parent_id) VALUES
|
||||
('INVESTMENT WITH SIDBI FOR MSE', '105001', false, true, 1, 1, (SELECT id FROM acc_gl_account WHERE gl_code='105000')),
|
||||
('NON-SLR INVESTMENT', '105002', false, true, 1, 1, (SELECT id FROM acc_gl_account WHERE gl_code='105000'));
|
||||
|
||||
INSERT INTO acc_gl_account (name, gl_code, disabled, manual_journal_entries_allowed, account_usage, classification_enum) VALUES
|
||||
('LOAN & ADVANCES', '106000', false, true, 2, 1);
|
||||
INSERT INTO acc_gl_account (name, gl_code, disabled, manual_journal_entries_allowed, account_usage, classification_enum, parent_id) VALUES
|
||||
('CASH CREDIT LOAN A/C', '106001', false, true, 1, 1, (SELECT id FROM acc_gl_account WHERE gl_code='106000')),
|
||||
('CC AGAINST DEPOSIT', '106002', false, true, 1, 1, (SELECT id FROM acc_gl_account WHERE gl_code='106000')),
|
||||
('KISHAN CREDIT CARD', '106003', false, true, 1, 1, (SELECT id FROM acc_gl_account WHERE gl_code='106000')),
|
||||
('LOAN AGAINST DEPOSITE', '106004', false, true, 1, 1, (SELECT id FROM acc_gl_account WHERE gl_code='106000')),
|
||||
('LOAN AGAINST SECURITY', '106005', false, true, 1, 1, (SELECT id FROM acc_gl_account WHERE gl_code='106000')),
|
||||
('LOAN AGAINST WARE HOUSE RECEIPT', '106006', false, true, 1, 1, (SELECT id FROM acc_gl_account WHERE gl_code='106000')),
|
||||
('MEDIUM TERM LOAN', '106007', false, true, 1, 1, (SELECT id FROM acc_gl_account WHERE gl_code='106000')),
|
||||
('LONG TERM LOAN A/C', '106008', false, true, 1, 1, (SELECT id FROM acc_gl_account WHERE gl_code='106000')),
|
||||
('PROPERTY MORTGAGE LIMIT', '106009', false, true, 1, 1, (SELECT id FROM acc_gl_account WHERE gl_code='106000'));
|
||||
|
||||
INSERT INTO acc_gl_account (name, gl_code, disabled, manual_journal_entries_allowed, account_usage, classification_enum) VALUES
|
||||
('INVESTMENTS', '107000', false, true, 2, 1);
|
||||
INSERT INTO acc_gl_account (name, gl_code, disabled, manual_journal_entries_allowed, account_usage, classification_enum, parent_id) VALUES
|
||||
('INVESTMENT IN GOVT. SECURITY', '107001', false, true, 1, 1, (SELECT id FROM acc_gl_account WHERE gl_code='107000')),
|
||||
('INVESTMENT IN STATE GOVT. SECURITY', '107002', false, true, 1, 1, (SELECT id FROM acc_gl_account WHERE gl_code='107000')),
|
||||
('INVESTMENT IN AFS CATEGORIES', '107003', false, true, 1, 1, (SELECT id FROM acc_gl_account WHERE gl_code='107000'));
|
||||
|
||||
INSERT INTO acc_gl_account (name, gl_code, disabled, manual_journal_entries_allowed, account_usage, classification_enum) VALUES
|
||||
('OTHER ASSETS', '108000', false, true, 2, 1);
|
||||
INSERT INTO acc_gl_account (name, gl_code, disabled, manual_journal_entries_allowed, account_usage, classification_enum, parent_id) VALUES
|
||||
('ADVANCES A/C', '108001', false, true, 1, 1, (SELECT id FROM acc_gl_account WHERE gl_code='108000')),
|
||||
('INTEREST RECEIVABLE', '108002', false, true, 1, 1, (SELECT id FROM acc_gl_account WHERE gl_code='108000')),
|
||||
('LIBRARY A/C', '108003', false, true, 1, 1, (SELECT id FROM acc_gl_account WHERE gl_code='108000')),
|
||||
('STATIONERY & PRINTING', '108004', false, true, 1, 1, (SELECT id FROM acc_gl_account WHERE gl_code='108000')),
|
||||
('TELEPHONE RENT DEPOSIT', '108005', false, true, 1, 1, (SELECT id FROM acc_gl_account WHERE gl_code='108000')),
|
||||
('PLANT AND MACHINERY', '108006', false, true, 1, 1, (SELECT id FROM acc_gl_account WHERE gl_code='108000')),
|
||||
('ELECTRIFICATION BANK BUILDING', '108007', false, true, 1, 1, (SELECT id FROM acc_gl_account WHERE gl_code='108000')),
|
||||
('BANK SECURITY SYSTEM', '108008', false, true, 1, 1, (SELECT id FROM acc_gl_account WHERE gl_code='108000'));
|
||||
|
||||
INSERT INTO acc_gl_account (name, gl_code, disabled, manual_journal_entries_allowed, account_usage, classification_enum) VALUES
|
||||
('LAND & BUILDING', '109000', false, true, 2, 1);
|
||||
INSERT INTO acc_gl_account (name, gl_code, disabled, manual_journal_entries_allowed, account_usage, classification_enum, parent_id) VALUES
|
||||
('LAND & BUILDING UC', '109001', false, true, 1, 1, (SELECT id FROM acc_gl_account WHERE gl_code='109000'));
|
||||
|
||||
INSERT INTO acc_gl_account (name, gl_code, disabled, manual_journal_entries_allowed, account_usage, classification_enum) VALUES
|
||||
('FURNITURE & FIXTURE', '110000', false, true, 2, 1);
|
||||
INSERT INTO acc_gl_account (name, gl_code, disabled, manual_journal_entries_allowed, account_usage, classification_enum, parent_id) VALUES
|
||||
('FURNITURE & FIXTURES A/C', '110001', false, true, 1, 1, (SELECT id FROM acc_gl_account WHERE gl_code='110000'));
|
||||
|
||||
INSERT INTO acc_gl_account (name, gl_code, disabled, manual_journal_entries_allowed, account_usage, classification_enum) VALUES
|
||||
('COMPUTER SOFTWARE AND HARDWARE', '111000', false, true, 2, 1);
|
||||
INSERT INTO acc_gl_account (name, gl_code, disabled, manual_journal_entries_allowed, account_usage, classification_enum, parent_id) VALUES
|
||||
('COMPUTER SOFTWARE & HARDWARE', '111001', false, true, 1, 1, (SELECT id FROM acc_gl_account WHERE gl_code='111000'));
|
||||
|
||||
INSERT INTO acc_gl_account (name, gl_code, disabled, manual_journal_entries_allowed, account_usage, classification_enum) VALUES
|
||||
('SHARE OF OTHER CO.OP INSTITUTIONS', '112000', false, true, 2, 1);
|
||||
INSERT INTO acc_gl_account (name, gl_code, disabled, manual_journal_entries_allowed, account_usage, classification_enum, parent_id) VALUES
|
||||
('SHARE A/C DCCB', '112001', false, true, 1, 1, (SELECT id FROM acc_gl_account WHERE gl_code='112000'));
|
||||
|
||||
-- ============================= Financial Activity mappings =============================
|
||||
-- Maps Fineract's internal cash/dividend mechanisms onto the closest matching account actually
|
||||
-- present on this bank's balance sheet. Financial activity 300 (opening-balance contra) is 0522's
|
||||
-- job, not repeated here - it is seeded generically for every tenant regardless of chart of
|
||||
-- accounts, and always exists by the time this changeset runs.
|
||||
|
||||
INSERT INTO acc_gl_financial_activity_account (financial_activity_type, gl_account_id) VALUES
|
||||
(101, (SELECT id FROM acc_gl_account WHERE gl_code='101001')),
|
||||
(102, (SELECT id FROM acc_gl_account WHERE gl_code='101001')),
|
||||
(201, (SELECT id FROM acc_gl_account WHERE gl_code='202001')),
|
||||
(202, (SELECT id FROM acc_gl_account WHERE gl_code='202001')),
|
||||
(301, (SELECT id FROM acc_gl_account WHERE gl_code='302006'));
|
||||
|
||||
]]>
|
||||
</sql>
|
||||
</changeSet>
|
||||
|
||||
</databaseChangeLog>
|
||||
Reference in New Issue
Block a user