From 509370eaeb623462be6ec0ed918401faae1a2975 Mon Sep 17 00:00:00 2001 From: Nitesh Yadav Date: Mon, 31 Aug 2026 15:06:57 +0530 Subject: [PATCH] fix(shares,voucher,cashreport): route redeem/withdrawal through Sundry, fix voucher cashier attribution and GL-name resolution - 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 --- .../api/BranchDayRegisterApiResource.java | 77 +++++- .../cashreport/api/CashReportApiResource.java | 129 ++-------- .../service/VaultPositionService.java | 169 +++++++++++++ ...lerkShareWithdrawalRequestApiResource.java | 202 ++++++++++++++- .../ClerkWithdrawalRequestApiResource.java | 5 + ...WritePlatformServiceJpaRepositoryImpl.java | 12 +- .../voucher/service/VoucherService.java | 15 ++ .../db/changelog/tenant/changelog-tenant.xml | 1 - .../tenant/final-changelog-tenant.xml | 3 + .../tenant/parts/0451_seed_branch_offices.xml | 59 ----- ..._branch_day_register_vault_only_detail.xml | 38 +++ ...22_seed_opening_balance_contra_account.xml | 36 +++ ...523_seed_basoda_bank_chart_of_accounts.xml | 237 ++++++++++++++++++ 13 files changed, 797 insertions(+), 186 deletions(-) create mode 100644 fineract-provider/src/main/java/org/apache/fineract/portfolio/cashreport/service/VaultPositionService.java delete mode 100644 fineract-provider/src/main/resources/db/changelog/tenant/parts/0451_seed_branch_offices.xml create mode 100644 fineract-provider/src/main/resources/db/changelog/tenant/parts/0521_branch_day_register_vault_only_detail.xml create mode 100644 fineract-provider/src/main/resources/db/changelog/tenant/parts/0522_seed_opening_balance_contra_account.xml create mode 100644 fineract-provider/src/main/resources/db/changelog/tenant/parts/0523_seed_basoda_bank_chart_of_accounts.xml diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/branchday/api/BranchDayRegisterApiResource.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/branchday/api/BranchDayRegisterApiResource.java index 3fc675902..cd36ae858 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/branchday/api/BranchDayRegisterApiResource.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/branchday/api/BranchDayRegisterApiResource.java @@ -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 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 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> 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> 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 tellerIds = jdbcTemplate.queryForList("SELECT id FROM m_tellers WHERE office_id = ?", Long.class, + officeId); + for (final Long tellerId : tellerIds) { + final List 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> 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 tellerIds = jdbcTemplate.queryForList("SELECT id FROM m_tellers WHERE office_id = ?", Long.class, + officeId); + for (final Long tellerId : tellerIds) { + final Map position = vaultPositionService.buildPositionForVault(tellerId, businessDate.toString()); + if (!Boolean.TRUE.equals(position.get("hasBreakdown"))) { + continue; + } + @SuppressWarnings("unchecked") + final List> denoms = (List>) position.get("denominations"); + final List> rows = new ArrayList<>(); + for (final Map d : denoms) { + final int count = ((Number) d.get("count")).intValue(); + if (count <= 0) { + continue; + } + final Map 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); } } diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/cashreport/api/CashReportApiResource.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/cashreport/api/CashReportApiResource.java index aa73ec92e..794cf15c6 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/cashreport/api/CashReportApiResource.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/cashreport/api/CashReportApiResource.java @@ -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 tellerIds = jdbcTemplate.queryForList("SELECT id FROM m_tellers ORDER BY id", Long.class); final List> 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 buildPositionForVault(final Long tellerId, final String dateFilter) { - final Map result = new LinkedHashMap<>(); - result.put("tellerId", tellerId); - result.put("asOfDate", dateFilter); - - String tellerName = null; - Long officeId = null; - try { - final Map 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> 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 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> rows = new ArrayList<>(); - balance.entrySet().stream() - .sorted((a, b) -> Long.compare(b.getValue()[0], a.getValue()[0])) - .forEach(e -> { - final Map 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, " diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/cashreport/service/VaultPositionService.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/cashreport/service/VaultPositionService.java new file mode 100644 index 000000000..1e7510f31 --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/cashreport/service/VaultPositionService.java @@ -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 DENOM_ROW = (rs, r) -> new Object[] { rs.getLong("denomination_id"), + rs.getInt("denomination_value"), rs.getString("kind"), rs.getLong("total_count") }; + + public Map buildPositionForVault(final Long tellerId, final String dateFilter) { + final Map result = new LinkedHashMap<>(); + result.put("tellerId", tellerId); + result.put("asOfDate", dateFilter); + + String tellerName = null; + Long officeId = null; + try { + final Map 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> 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 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> rows = new ArrayList<>(); + balance.entrySet().stream().sorted((a, b) -> Long.compare(b.getValue()[0], a.getValue()[0])).forEach(e -> { + final Map 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 balance, final List 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; + } + } +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/clerkwithdrawal/api/ClerkShareWithdrawalRequestApiResource.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/clerkwithdrawal/api/ClerkShareWithdrawalRequestApiResource.java index 6301a771a..2b3cf5a23 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/clerkwithdrawal/api/ClerkShareWithdrawalRequestApiResource.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/clerkwithdrawal/api/ClerkShareWithdrawalRequestApiResource.java @@ -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. + * + *

+ * 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 whichever GL the settlement payment type's Fund Source maps to / Cr Savings Control) is then + * reversed in full 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 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. + * + *

+ * 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 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 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 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> 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()) { diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/clerkwithdrawal/api/ClerkWithdrawalRequestApiResource.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/clerkwithdrawal/api/ClerkWithdrawalRequestApiResource.java index 8a5985e8b..03dcd8d24 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/clerkwithdrawal/api/ClerkWithdrawalRequestApiResource.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/clerkwithdrawal/api/ClerkWithdrawalRequestApiResource.java @@ -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); diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/shareaccounts/service/ShareAccountWritePlatformServiceJpaRepositoryImpl.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/shareaccounts/service/ShareAccountWritePlatformServiceJpaRepositoryImpl.java index cde09257e..37410ae78 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/shareaccounts/service/ShareAccountWritePlatformServiceJpaRepositoryImpl.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/shareaccounts/service/ShareAccountWritePlatformServiceJpaRepositoryImpl.java @@ -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) // diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/voucher/service/VoucherService.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/voucher/service/VoucherService.java index 9190879ce..e56d5981f 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/voucher/service/VoucherService.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/voucher/service/VoucherService.java @@ -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); + } } diff --git a/fineract-provider/src/main/resources/db/changelog/tenant/changelog-tenant.xml b/fineract-provider/src/main/resources/db/changelog/tenant/changelog-tenant.xml index 66e5c3fee..4b383b1b2 100644 --- a/fineract-provider/src/main/resources/db/changelog/tenant/changelog-tenant.xml +++ b/fineract-provider/src/main/resources/db/changelog/tenant/changelog-tenant.xml @@ -502,7 +502,6 @@ - diff --git a/fineract-provider/src/main/resources/db/changelog/tenant/final-changelog-tenant.xml b/fineract-provider/src/main/resources/db/changelog/tenant/final-changelog-tenant.xml index a226f501d..09a09f1a0 100644 --- a/fineract-provider/src/main/resources/db/changelog/tenant/final-changelog-tenant.xml +++ b/fineract-provider/src/main/resources/db/changelog/tenant/final-changelog-tenant.xml @@ -47,4 +47,7 @@ + + + diff --git a/fineract-provider/src/main/resources/db/changelog/tenant/parts/0451_seed_branch_offices.xml b/fineract-provider/src/main/resources/db/changelog/tenant/parts/0451_seed_branch_offices.xml deleted file mode 100644 index 577dcd871..000000000 --- a/fineract-provider/src/main/resources/db/changelog/tenant/parts/0451_seed_branch_offices.xml +++ /dev/null @@ -1,59 +0,0 @@ - - - - - - - - SELECT COUNT(*) FROM m_office WHERE external_id IN ('0002','0003','0004','0005','0006') - - - 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'); - - - - - - - SELECT COUNT(*) FROM m_office WHERE external_id = 'HO' - - - UPDATE m_office SET external_id = 'HO' WHERE hierarchy = '.'; - - - - diff --git a/fineract-provider/src/main/resources/db/changelog/tenant/parts/0521_branch_day_register_vault_only_detail.xml b/fineract-provider/src/main/resources/db/changelog/tenant/parts/0521_branch_day_register_vault_only_detail.xml new file mode 100644 index 000000000..51436a6a6 --- /dev/null +++ b/fineract-provider/src/main/resources/db/changelog/tenant/parts/0521_branch_day_register_vault_only_detail.xml @@ -0,0 +1,38 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/fineract-provider/src/main/resources/db/changelog/tenant/parts/0522_seed_opening_balance_contra_account.xml b/fineract-provider/src/main/resources/db/changelog/tenant/parts/0522_seed_opening_balance_contra_account.xml new file mode 100644 index 000000000..660f7f003 --- /dev/null +++ b/fineract-provider/src/main/resources/db/changelog/tenant/parts/0522_seed_opening_balance_contra_account.xml @@ -0,0 +1,36 @@ + + + + + + + + SELECT COUNT(*) FROM acc_gl_account WHERE gl_code = '300000' + + + 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); + + + + + + SELECT COUNT(*) FROM acc_gl_financial_activity_account WHERE financial_activity_type = 300 + + + 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')); + + + + diff --git a/fineract-provider/src/main/resources/db/changelog/tenant/parts/0523_seed_basoda_bank_chart_of_accounts.xml b/fineract-provider/src/main/resources/db/changelog/tenant/parts/0523_seed_basoda_bank_chart_of_accounts.xml new file mode 100644 index 000000000..03e022fe6 --- /dev/null +++ b/fineract-provider/src/main/resources/db/changelog/tenant/parts/0523_seed_basoda_bank_chart_of_accounts.xml @@ -0,0 +1,237 @@ + + + + + + + + SELECT COUNT(*) FROM acc_gl_account WHERE gl_code != '300000' + + + + + + +