feat(shares, cashier): tag GL counter entries by purpose, close DD/Share voucher cross-use
Fineract Build & Cucumber tests (without E2E tests) / verify (main, build-core) (push) Canceled after 0s
Fineract Build & Cucumber tests (without E2E tests) / verify (main, cucumber) (push) Canceled after 0s
Fineract Build & Cucumber tests (without E2E tests) / verify (progressive-loan, build-progressive-loan) (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 E2E Tests / E2E Tests (Shard 5 of 10) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 6 of 10) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 7 of 10) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 8 of 10) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 9 of 10) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MariaDB / test (test-core-1) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MariaDB / test (test-core-2) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MariaDB / test (test-core-3) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MariaDB / test (test-core-4) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MariaDB / test (test-core-5) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MySQL / test (test-core-1) (push) Canceled after 0s
Fineract 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 Cargo & Unit- & Integration tests - PostgreSQL / test (test-core-1) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - PostgreSQL / test (test-core-2) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - PostgreSQL / test (test-core-3) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - PostgreSQL / test (test-core-4) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - PostgreSQL / test (test-core-5) (push) Canceled after 0s
Fineract Liquibase Only mode - PostgreSQL / test (push) Canceled after 0s
Fineract Messaging Smoke Tests / Smoke Test with ActiveMQ (push) Canceled after 0s
Fineract Messaging Smoke Tests / Smoke Test with Kafka (push) Canceled after 0s
Fineract Build & Cucumber tests (without E2E tests) / verify (main, build-core) (push) Canceled after 0s
Fineract Build & Cucumber tests (without E2E tests) / verify (main, cucumber) (push) Canceled after 0s
Fineract Build & Cucumber tests (without E2E tests) / verify (progressive-loan, build-progressive-loan) (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 E2E Tests / E2E Tests (Shard 5 of 10) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 6 of 10) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 7 of 10) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 8 of 10) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 9 of 10) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MariaDB / test (test-core-1) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MariaDB / test (test-core-2) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MariaDB / test (test-core-3) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MariaDB / test (test-core-4) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MariaDB / test (test-core-5) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MySQL / test (test-core-1) (push) Canceled after 0s
Fineract 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 Cargo & Unit- & Integration tests - PostgreSQL / test (test-core-1) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - PostgreSQL / test (test-core-2) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - PostgreSQL / test (test-core-3) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - PostgreSQL / test (test-core-4) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - PostgreSQL / test (test-core-5) (push) Canceled after 0s
Fineract 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
A cashier Receipt or clerk Collection into a GL account never named who or what it was for, so a manager applying a share request could adopt a voucher a cashier had actually collected for a Demand Draft, and vice versa. Adds a GLEntryType code (General/Share Collection/DD Issue, extensible via Manage Codes) that the counter sets at save time; Share Advance Collection and Sponsor DD issuance both check it before adopting a voucher, and also cross-check each other's own consumption tables directly so the guard holds even for untagged legacy rows. Also: - Share-Collection-tagged vouchers auto-surface on Apply Share / Additional Share screens instead of requiring a manual GL-account search, but only when a matching client_id already exists on the row or the request is scoped to a specific member's screen -- a clientless GL receipt is never auto-attributed to whoever's screen happened to be open. - gl_transaction_id column on m_clerk_disbursement_request captures the real GL posting a Collection approval makes, previously generated and discarded. - New GET .../gl-entries endpoint exposes a receipt's actual Dr/Cr legs for detail popups. - Additional-share tranches restricted to a single funding voucher for now. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
+24
-6
@@ -128,6 +128,12 @@ public class CashierDepositRequestApiResource {
|
|||||||
final String bankName = fromJsonHelper.extractStringNamed("bankName", root);
|
final String bankName = fromJsonHelper.extractStringNamed("bankName", root);
|
||||||
final String txnDate = fromJsonHelper.extractStringNamed("txnDate", root);
|
final String txnDate = fromJsonHelper.extractStringNamed("txnDate", root);
|
||||||
final String denominationData = fromJsonHelper.extractStringNamed("denominationData", root);
|
final String denominationData = fromJsonHelper.extractStringNamed("denominationData", root);
|
||||||
|
// What the counter says this GL credit is actually for — a code value under the
|
||||||
|
// "GLEntryType" code (Admin > System > Manage Codes), defaulting to "General" unless the row
|
||||||
|
// is one of the flows below picks up its own money later (Share Advance Collection, Sponsor
|
||||||
|
// DD), in which case that adoption checks this tag so it can never take a voucher meant for
|
||||||
|
// the other one. See 0565_convert_entry_type_to_code_value.xml.
|
||||||
|
final Long entryTypeCvId = resolveEntryTypeCvId(fromJsonHelper.extractLongNamed("entryTypeCvId", root));
|
||||||
// DD + Cash + GL breakdown from Transaction Entry - null for ordinary deposit requests.
|
// DD + Cash + GL breakdown from Transaction Entry - null for ordinary deposit requests.
|
||||||
// Persisted so the Sponsor DD issuance screen can read the breakdown back instead of
|
// Persisted so the Sponsor DD issuance screen can read the breakdown back instead of
|
||||||
// retyping face value/charges once this request is approved and selected as cash evidence.
|
// retyping face value/charges once this request is approved and selected as cash evidence.
|
||||||
@@ -160,8 +166,8 @@ public class CashierDepositRequestApiResource {
|
|||||||
client_id, client_name, amount,
|
client_id, client_name, amount,
|
||||||
payment_type_id, payment_type_name, description, cheque_no, bank_name, txn_date,
|
payment_type_id, payment_type_name, description, cheque_no, bank_name, txn_date,
|
||||||
status, cashier_id, cashier_name, cashier_username, denomination_data, activity_log, submitted_at,
|
status, cashier_id, cashier_name, cashier_username, denomination_data, activity_log, submitted_at,
|
||||||
dd_face_value, dd_charges, dd_gst_percentage, dd_gst_amount)
|
dd_face_value, dd_charges, dd_gst_percentage, dd_gst_amount, entry_type_cv_id)
|
||||||
VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)
|
VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)
|
||||||
RETURNING id
|
RETURNING id
|
||||||
""",
|
""",
|
||||||
Long.class,
|
Long.class,
|
||||||
@@ -173,7 +179,7 @@ public class CashierDepositRequestApiResource {
|
|||||||
"PENDING",
|
"PENDING",
|
||||||
currentUser.getId(), currentUser.getDisplayName(), currentUser.getUsername(),
|
currentUser.getId(), currentUser.getDisplayName(), currentUser.getUsername(),
|
||||||
denominationData, activityLog, Timestamp.from(Instant.now()),
|
denominationData, activityLog, Timestamp.from(Instant.now()),
|
||||||
ddFaceValue, ddCharges, ddGstPercentage, ddGstAmount
|
ddFaceValue, ddCharges, ddGstPercentage, ddGstAmount, entryTypeCvId
|
||||||
);
|
);
|
||||||
|
|
||||||
// Globally unique voucher number from the central m_voucher register
|
// Globally unique voucher number from the central m_voucher register
|
||||||
@@ -228,7 +234,7 @@ public class CashierDepositRequestApiResource {
|
|||||||
+ " txn_date, status, cashier_id, cashier_name, cashier_username,"
|
+ " txn_date, status, cashier_id, cashier_name, cashier_username,"
|
||||||
+ " manager_id, manager_name, manager_username, rejection_reason, txn_id,"
|
+ " manager_id, manager_name, manager_username, rejection_reason, txn_id,"
|
||||||
+ " denomination_data, activity_log, submitted_at, actioned_at,"
|
+ " denomination_data, activity_log, submitted_at, actioned_at,"
|
||||||
+ " dd_face_value, dd_charges, dd_gst_percentage, dd_gst_amount"
|
+ " dd_face_value, dd_charges, dd_gst_percentage, dd_gst_amount, entry_type_cv_id"
|
||||||
+ " FROM m_cashier_deposit_request " + where
|
+ " FROM m_cashier_deposit_request " + where
|
||||||
+ " ORDER BY submitted_at DESC LIMIT ? OFFSET ?",
|
+ " ORDER BY submitted_at DESC LIMIT ? OFFSET ?",
|
||||||
pageParams.toArray());
|
pageParams.toArray());
|
||||||
@@ -282,7 +288,7 @@ public class CashierDepositRequestApiResource {
|
|||||||
payment_type_name, description, txn_date, status,
|
payment_type_name, description, txn_date, status,
|
||||||
cashier_name, manager_name, manager_username, rejection_reason, txn_id,
|
cashier_name, manager_name, manager_username, rejection_reason, txn_id,
|
||||||
submitted_at, actioned_at,
|
submitted_at, actioned_at,
|
||||||
dd_face_value, dd_charges, dd_gst_percentage, dd_gst_amount
|
dd_face_value, dd_charges, dd_gst_percentage, dd_gst_amount, entry_type_cv_id
|
||||||
FROM m_cashier_deposit_request
|
FROM m_cashier_deposit_request
|
||||||
""" + where + " ORDER BY submitted_at DESC LIMIT ? OFFSET ?",
|
""" + where + " ORDER BY submitted_at DESC LIMIT ? OFFSET ?",
|
||||||
pageParams.toArray()
|
pageParams.toArray()
|
||||||
@@ -669,7 +675,7 @@ public class CashierDepositRequestApiResource {
|
|||||||
txn_date, status, cashier_id, cashier_name, cashier_username,
|
txn_date, status, cashier_id, cashier_name, cashier_username,
|
||||||
manager_id, manager_name, manager_username, rejection_reason, txn_id,
|
manager_id, manager_name, manager_username, rejection_reason, txn_id,
|
||||||
denomination_data, activity_log, submitted_at, actioned_at,
|
denomination_data, activity_log, submitted_at, actioned_at,
|
||||||
dd_face_value, dd_charges, dd_gst_percentage, dd_gst_amount
|
dd_face_value, dd_charges, dd_gst_percentage, dd_gst_amount, entry_type_cv_id
|
||||||
FROM m_cashier_deposit_request WHERE id = ?
|
FROM m_cashier_deposit_request WHERE id = ?
|
||||||
""", id);
|
""", id);
|
||||||
if (rows.isEmpty()) {
|
if (rows.isEmpty()) {
|
||||||
@@ -1032,6 +1038,18 @@ public class CashierDepositRequestApiResource {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** The counter's choice, when they made one — otherwise "General" under the GLEntryType code,
|
||||||
|
* resolved by name rather than a hardcoded id since Manage Codes owns these rows now. */
|
||||||
|
private Long resolveEntryTypeCvId(final Long provided) {
|
||||||
|
if (provided != null) {
|
||||||
|
return provided;
|
||||||
|
}
|
||||||
|
return jdbcTemplate.queryForObject(
|
||||||
|
"SELECT cv.id FROM m_code_value cv JOIN m_code c ON c.id = cv.code_id"
|
||||||
|
+ " WHERE c.code_name = 'GLEntryType' AND cv.code_value = 'General'",
|
||||||
|
Long.class);
|
||||||
|
}
|
||||||
|
|
||||||
private String buildActivityLog(final String existingJson, final String action,
|
private String buildActivityLog(final String existingJson, final String action,
|
||||||
final String username, final String note) {
|
final String username, final String note) {
|
||||||
JsonArray log = new JsonArray();
|
JsonArray log = new JsonArray();
|
||||||
|
|||||||
+233
-18
@@ -188,11 +188,25 @@ public class ShareAdvanceCollectionApiResource {
|
|||||||
// GET — Unapplied receipts, optionally for one client
|
// GET — Unapplied receipts, optionally for one client
|
||||||
// ══════════════════════════════════════════════════════════════════════
|
// ══════════════════════════════════════════════════════════════════════
|
||||||
@GET
|
@GET
|
||||||
|
@Transactional
|
||||||
public Response listUnapplied(@QueryParam("clientId") final Long clientId) {
|
public Response listUnapplied(@QueryParam("clientId") final Long clientId) {
|
||||||
this.context.authenticatedUser();
|
this.context.authenticatedUser();
|
||||||
|
|
||||||
|
// A voucher explicitly tagged Share Collection at save time no longer waits for the counter
|
||||||
|
// to search it up by hand -- it's pulled onto this list itself, the first time anyone asks
|
||||||
|
// for it. Anything not tagged this way (an older row, or one deliberately left General) still
|
||||||
|
// needs the by-voucher search below, same as before.
|
||||||
|
autoAdoptShareCollectionTagged(clientId);
|
||||||
|
|
||||||
final StringBuilder sql = new StringBuilder("SELECT c.id, c.receipt_no, c.voucher_no, c.client_id,"
|
final StringBuilder sql = new StringBuilder("SELECT c.id, c.receipt_no, c.voucher_no, c.client_id,"
|
||||||
+ " cl.display_name AS client_name, " + " c.collection_date, c.amount, c.currency_code, c.status, c.note "
|
+ " cl.display_name AS client_name, "
|
||||||
|
+ " c.collection_date, c.amount, c.currency_code, c.status, c.note, c.collected_by_name,"
|
||||||
|
// The GL account this specific receipt's money actually sits in -- its own posting's
|
||||||
|
// credit leg, read back from acc_gl_journal_entry rather than guessed, so the GL
|
||||||
|
// Account picker above can be kept in sync with whichever receipt gets ticked.
|
||||||
|
+ " (SELECT ga.gl_code FROM acc_gl_journal_entry je JOIN acc_gl_account ga ON ga.id = je.account_id"
|
||||||
|
+ " WHERE je.transaction_id = c.gl_transaction_id AND je.type_enum = " + CREDIT + " AND je.reversed = false"
|
||||||
|
+ " ORDER BY je.id LIMIT 1) AS gl_code "
|
||||||
+ " FROM m_share_advance_collection c JOIN m_client cl ON cl.id = c.client_id " + " WHERE c.status = 'UNAPPLIED' ");
|
+ " FROM m_share_advance_collection c JOIN m_client cl ON cl.id = c.client_id " + " WHERE c.status = 'UNAPPLIED' ");
|
||||||
if (clientId != null) {
|
if (clientId != null) {
|
||||||
sql.append(" AND c.client_id = ").append(clientId);
|
sql.append(" AND c.client_id = ").append(clientId);
|
||||||
@@ -233,7 +247,8 @@ public class ShareAdvanceCollectionApiResource {
|
|||||||
// Matched on voucher_no or receipt_no so a pre-voucher receipt number still resolves.
|
// Matched on voucher_no or receipt_no so a pre-voucher receipt number still resolves.
|
||||||
final List<Map<String, Object>> rows = jdbcTemplate
|
final List<Map<String, Object>> rows = jdbcTemplate
|
||||||
.queryForList("SELECT c.id, c.receipt_no, c.voucher_no, c.client_id, cl.display_name AS client_name,"
|
.queryForList("SELECT c.id, c.receipt_no, c.voucher_no, c.client_id, cl.display_name AS client_name,"
|
||||||
+ " c.collection_date, c.amount, c.currency_code, c.status, c.note," + " c.applied_share_account_id "
|
+ " c.collection_date, c.amount, c.currency_code, c.status, c.note, c.collected_by_name,"
|
||||||
|
+ " c.applied_share_account_id "
|
||||||
+ " FROM m_share_advance_collection c JOIN m_client cl ON cl.id = c.client_id "
|
+ " FROM m_share_advance_collection c JOIN m_client cl ON cl.id = c.client_id "
|
||||||
+ " WHERE c.voucher_no = ? OR c.receipt_no = ? ORDER BY c.id DESC", voucherNo, voucherNo);
|
+ " WHERE c.voucher_no = ? OR c.receipt_no = ? ORDER BY c.id DESC", voucherNo, voucherNo);
|
||||||
|
|
||||||
@@ -273,19 +288,42 @@ public class ShareAdvanceCollectionApiResource {
|
|||||||
// GET — GL accounts selectable for a by-voucher lookup
|
// GET — GL accounts selectable for a by-voucher lookup
|
||||||
// ══════════════════════════════════════════════════════════════════════
|
// ══════════════════════════════════════════════════════════════════════
|
||||||
/**
|
/**
|
||||||
* Every active, postable GL account, for the GL account picker on the Apply Collection screen.
|
* The GL account picker on the Apply Collection screen — narrowed to accounts that actually have
|
||||||
|
* an APPROVED, not-yet-consumed cashier deposit or clerk collection sitting in them right now,
|
||||||
|
* so the counter is choosing from real, live money rather than the entire chart of accounts
|
||||||
|
* (which is every postable GL account in the bank, share-related or not, well over a hundred of
|
||||||
|
* them, Cash at Vault included, with nothing to say which one has anything to find).
|
||||||
*
|
*
|
||||||
* Deliberately not filtered to whatever a share product's own Share Suspense mapping happens to
|
* Still never resolved from a share product's own Share Suspense mapping -- that mapping is
|
||||||
* point at today — that mapping is exactly what this lookup no longer trusts, since it can go
|
* exactly what this lookup no longer trusts, since it can go missing or wrong without anyone
|
||||||
* missing or wrong without anyone noticing until a voucher search fails at the counter. The person
|
* noticing until a voucher search fails at the counter. This filters by what genuinely sits in
|
||||||
* applying a receipt picks the account the deposit actually went to, every time.
|
* each account today, not by what any product's configuration claims should. Falls back to the
|
||||||
|
* unfiltered full list on the rare chance nothing currently qualifies, so the picker is never a
|
||||||
|
* dead end -- a voucher search should not become unreachable just because this narrowing has
|
||||||
|
* nothing to narrow to yet.
|
||||||
*/
|
*/
|
||||||
@GET
|
@GET
|
||||||
@Path("/glaccounts")
|
@Path("/glaccounts")
|
||||||
public Response glAccounts() {
|
public Response glAccounts() {
|
||||||
this.context.authenticatedUser();
|
this.context.authenticatedUser();
|
||||||
final List<Map<String, Object>> rows = jdbcTemplate.queryForList(
|
List<Map<String, Object>> rows = jdbcTemplate.queryForList(
|
||||||
"SELECT id, gl_code, name FROM acc_gl_account WHERE disabled = false AND account_usage = 1 ORDER BY gl_code");
|
"SELECT DISTINCT ga.id, ga.gl_code, ga.name"
|
||||||
|
+ " FROM acc_gl_account ga"
|
||||||
|
+ " WHERE ga.disabled = false AND ga.account_usage = 1"
|
||||||
|
+ " AND ("
|
||||||
|
+ " EXISTS (SELECT 1 FROM m_cashier_deposit_request d WHERE d.gl_account_id = ga.id"
|
||||||
|
+ " AND d.status = 'APPROVED' AND d.share_account_id IS NULL"
|
||||||
|
+ " AND NOT EXISTS (SELECT 1 FROM m_share_advance_collection sc WHERE sc.voucher_no = d.voucher_no)"
|
||||||
|
+ " AND NOT EXISTS (SELECT 1 FROM m_sponsor_dd_issue_request s WHERE s.cashier_deposit_request_id = d.id))"
|
||||||
|
+ " OR EXISTS (SELECT 1 FROM m_clerk_disbursement_request d WHERE d.gl_account_id = ga.id"
|
||||||
|
+ " AND d.status = 'APPROVED' AND d.request_type = 'COLLECTION'"
|
||||||
|
+ " AND NOT EXISTS (SELECT 1 FROM m_share_advance_collection sc WHERE sc.voucher_no = d.voucher_no))"
|
||||||
|
+ " )"
|
||||||
|
+ " ORDER BY ga.gl_code");
|
||||||
|
if (rows.isEmpty()) {
|
||||||
|
rows = jdbcTemplate.queryForList(
|
||||||
|
"SELECT id, gl_code, name FROM acc_gl_account WHERE disabled = false AND account_usage = 1 ORDER BY gl_code");
|
||||||
|
}
|
||||||
final Map<String, Object> out = new LinkedHashMap<>();
|
final Map<String, Object> out = new LinkedHashMap<>();
|
||||||
out.put("glAccounts", rows);
|
out.put("glAccounts", rows);
|
||||||
return Response.ok(gson.toJson(out)).build();
|
return Response.ok(gson.toJson(out)).build();
|
||||||
@@ -318,6 +356,47 @@ public class ShareAdvanceCollectionApiResource {
|
|||||||
return Response.ok(gson.toJson(out)).build();
|
return Response.ok(gson.toJson(out)).build();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ══════════════════════════════════════════════════════════════════════
|
||||||
|
// GET — The actual Dr/Cr GL legs a receipt posted, for the details popup
|
||||||
|
// ══════════════════════════════════════════════════════════════════════
|
||||||
|
/**
|
||||||
|
* What this receipt actually did to the ledger — the real Dr/Cr legs under its own
|
||||||
|
* {@code gl_transaction_id}, not the GL account currently picked in the by-voucher search box
|
||||||
|
* (a different, unrelated piece of screen state that a details popup must never be shown
|
||||||
|
* against, since it can name a completely different account than the one this receipt sits in).
|
||||||
|
*/
|
||||||
|
@GET
|
||||||
|
@Path("/{collectionId}/gl-entries")
|
||||||
|
public Response glEntries(@PathParam("collectionId") final Long collectionId) {
|
||||||
|
this.context.authenticatedUser();
|
||||||
|
final List<Map<String, Object>> collRows = jdbcTemplate
|
||||||
|
.queryForList("SELECT gl_transaction_id FROM m_share_advance_collection WHERE id = ?", collectionId);
|
||||||
|
if (collRows.isEmpty()) {
|
||||||
|
throw new PlatformDataIntegrityException("error.msg.shareadvance.not.found", "Receipt " + collectionId + " not found.");
|
||||||
|
}
|
||||||
|
final String glTransactionId = (String) collRows.get(0).get("gl_transaction_id");
|
||||||
|
final List<Map<String, Object>> legs = glTransactionId == null ? List.of()
|
||||||
|
: jdbcTemplate.queryForList(
|
||||||
|
"SELECT ga.gl_code, ga.name AS account_name, je.type_enum, je.amount"
|
||||||
|
+ " FROM acc_gl_journal_entry je JOIN acc_gl_account ga ON ga.id = je.account_id"
|
||||||
|
+ " WHERE je.transaction_id = ? AND je.reversed = false ORDER BY je.type_enum DESC, je.id",
|
||||||
|
glTransactionId);
|
||||||
|
|
||||||
|
final Map<String, Object> out = new LinkedHashMap<>();
|
||||||
|
out.put("transactionId", glTransactionId);
|
||||||
|
final List<Map<String, Object>> entries = new java.util.ArrayList<>();
|
||||||
|
for (final Map<String, Object> leg : legs) {
|
||||||
|
final Map<String, Object> e = new LinkedHashMap<>();
|
||||||
|
e.put("glCode", leg.get("gl_code"));
|
||||||
|
e.put("accountName", leg.get("account_name"));
|
||||||
|
e.put("drCr", toInt(leg.get("type_enum")) == DEBIT ? "Dr" : "Cr");
|
||||||
|
e.put("amount", leg.get("amount"));
|
||||||
|
entries.add(e);
|
||||||
|
}
|
||||||
|
out.put("entries", entries);
|
||||||
|
return Response.ok(gson.toJson(out)).build();
|
||||||
|
}
|
||||||
|
|
||||||
// ══════════════════════════════════════════════════════════════════════
|
// ══════════════════════════════════════════════════════════════════════
|
||||||
// POST — Apply a receipt to an activated share account
|
// POST — Apply a receipt to an activated share account
|
||||||
// ══════════════════════════════════════════════════════════════════════
|
// ══════════════════════════════════════════════════════════════════════
|
||||||
@@ -609,6 +688,108 @@ public class ShareAdvanceCollectionApiResource {
|
|||||||
* missing product mapping can never make a genuine voucher unreachable.
|
* missing product mapping can never make a genuine voucher unreachable.
|
||||||
* @return the newly created collection row, or null when no approved deposit matches.
|
* @return the newly created collection row, or null when no approved deposit matches.
|
||||||
*/
|
*/
|
||||||
|
/**
|
||||||
|
* Pulls every APPROVED, not-yet-consumed deposit or clerk collection explicitly tagged "Share
|
||||||
|
* Collection" into m_share_advance_collection, so it shows up on the Apply Share / Additional
|
||||||
|
* Share screen on its own -- the point of tagging a voucher this way at save time in the first
|
||||||
|
* place.
|
||||||
|
*
|
||||||
|
* A cashier Receipt straight into a GL account (AC.TP=GL) names no member of its own --
|
||||||
|
* buildDepositPayload() in transaction-entry.component.ts sends clientId:null for every one of
|
||||||
|
* these, deliberately, since a GL account isn't anybody's account. Such a row is NEVER
|
||||||
|
* auto-adopted, for any client, under any circumstances -- attributing it to "whichever member's
|
||||||
|
* screen happened to be open when this ran" is a coincidence of timing, not evidence of whose
|
||||||
|
* money it is, and would silently hand one member's cash to a completely unrelated member simply
|
||||||
|
* because their Apply Share screen loaded first. A clientless receipt can only ever be claimed
|
||||||
|
* through the manual by-voucher search, where a person at the counter is actively vouching for it
|
||||||
|
* against a member they can see, by quoting the specific voucher number the member is holding --
|
||||||
|
* a deliberate, checkable act, not an automatic guess. A row that DOES carry its own client_id
|
||||||
|
* (a Clerk Collection, whose client comes from the savings account it actually debited -- real
|
||||||
|
* evidence, not a guess) is still only adopted against a matching client_id, never a different
|
||||||
|
* member than the one asking.
|
||||||
|
*/
|
||||||
|
private void autoAdoptShareCollectionTagged(final Long clientIdFilter) {
|
||||||
|
final List<Map<String, Object>> deposits = jdbcTemplate.queryForList(
|
||||||
|
"SELECT d.id, d.voucher_no, d.client_id, d.amount, d.office_id, d.currency_code, d.txn_date, d.txn_id,"
|
||||||
|
+ " d.cashier_id, d.cashier_name, d.description"
|
||||||
|
+ " FROM m_cashier_deposit_request d"
|
||||||
|
+ " JOIN m_code_value cv ON cv.id = d.entry_type_cv_id"
|
||||||
|
+ " WHERE cv.code_value = 'Share Collection' AND d.status = 'APPROVED' AND d.share_account_id IS NULL"
|
||||||
|
+ " AND d.client_id IS NOT NULL"
|
||||||
|
+ " AND NOT EXISTS (SELECT 1 FROM m_sponsor_dd_issue_request s WHERE s.cashier_deposit_request_id = d.id)"
|
||||||
|
+ " AND NOT EXISTS (SELECT 1 FROM m_share_advance_collection sc WHERE sc.voucher_no = d.voucher_no)");
|
||||||
|
for (final Map<String, Object> dep : deposits) {
|
||||||
|
final Long resolvedClientId = toLong(dep.get("client_id"));
|
||||||
|
// Guaranteed non-null by the WHERE clause above, but a genuine client_id must still match
|
||||||
|
// who is asking -- never adopted for a different member than the one whose screen it is.
|
||||||
|
if (clientIdFilter != null && !clientIdFilter.equals(resolvedClientId)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
final LocalDate collectionDate = dep.get("txn_date") instanceof java.sql.Date
|
||||||
|
? ((java.sql.Date) dep.get("txn_date")).toLocalDate()
|
||||||
|
: LocalDate.now(DateUtils.getDateTimeZoneOfTenant());
|
||||||
|
final String receiptNo = nextReceiptNo();
|
||||||
|
// What the cashier actually typed at save time, when they typed anything -- that is what
|
||||||
|
// the Apply Share screen's Description column exists to show. Falls back to a plain
|
||||||
|
// label only for a row nobody described, same as the manual by-voucher adoption below.
|
||||||
|
final String depDescription = (String) dep.get("description");
|
||||||
|
final String note = depDescription != null && !depDescription.isBlank() ? depDescription
|
||||||
|
: "Share collection - Voucher " + dep.get("voucher_no");
|
||||||
|
jdbcTemplate.update(
|
||||||
|
"INSERT INTO m_share_advance_collection "
|
||||||
|
+ "(receipt_no, voucher_no, client_id, office_id, collection_date, amount, currency_code, status, "
|
||||||
|
+ " gl_transaction_id, collected_by_user, collected_by_name, note) "
|
||||||
|
+ "VALUES (?,?,?,?,?,?,?,'UNAPPLIED',?,?,?,?)",
|
||||||
|
receiptNo, dep.get("voucher_no"), resolvedClientId, toLong(dep.get("office_id")),
|
||||||
|
java.sql.Date.valueOf(collectionDate), dep.get("amount"), dep.get("currency_code"), dep.get("txn_id"),
|
||||||
|
toLong(dep.get("cashier_id")), dep.get("cashier_name"), note);
|
||||||
|
}
|
||||||
|
|
||||||
|
final List<Map<String, Object>> collections = jdbcTemplate.queryForList(
|
||||||
|
"SELECT d.id, d.voucher_no, d.dr_amount, d.currency_code, d.office_id, d.credit_entries, d.clerk_id, d.clerk_name,"
|
||||||
|
+ " d.submitted_at, d.actioned_at, d.description, d.gl_transaction_id"
|
||||||
|
+ " FROM m_clerk_disbursement_request d"
|
||||||
|
+ " JOIN m_code_value cv ON cv.id = d.entry_type_cv_id"
|
||||||
|
+ " WHERE cv.code_value = 'Share Collection' AND d.status = 'APPROVED' AND d.request_type = 'COLLECTION'"
|
||||||
|
+ " AND NOT EXISTS (SELECT 1 FROM m_share_advance_collection sc WHERE sc.voucher_no = d.voucher_no)");
|
||||||
|
for (final Map<String, Object> req : collections) {
|
||||||
|
final Long savingsAccountId = debitSavingsAccountId((String) req.get("credit_entries"));
|
||||||
|
if (savingsAccountId == null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
final Long memberClientId = jdbcTemplate.queryForObject("SELECT client_id FROM m_savings_account WHERE id = ?", Long.class,
|
||||||
|
savingsAccountId);
|
||||||
|
if (clientIdFilter != null && !clientIdFilter.equals(memberClientId)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
final Long officeId = req.get("office_id") != null ? toLong(req.get("office_id")) : loadClientOfficeId(memberClientId);
|
||||||
|
final String currency = req.get("currency_code") != null ? (String) req.get("currency_code") : "INR";
|
||||||
|
final Object actionedAt = req.get("actioned_at");
|
||||||
|
final Object submittedAt = req.get("submitted_at");
|
||||||
|
final LocalDate collectionDate = (actionedAt != null ? actionedAt : submittedAt) instanceof java.sql.Timestamp
|
||||||
|
? ((java.sql.Timestamp) (actionedAt != null ? actionedAt : submittedAt)).toLocalDateTime().toLocalDate()
|
||||||
|
: LocalDate.now(DateUtils.getDateTimeZoneOfTenant());
|
||||||
|
final String voucherNo = (String) req.get("voucher_no");
|
||||||
|
final String receiptNo = nextReceiptNo();
|
||||||
|
final String reqDescription = (String) req.get("description");
|
||||||
|
final String note = reqDescription != null && !reqDescription.isBlank() ? reqDescription
|
||||||
|
: "Share collection - Voucher " + voucherNo;
|
||||||
|
// The real acc_gl_journal_entry.transaction_id this approval posted (see
|
||||||
|
// 0566_add_gl_transaction_id_to_clerk_disbursement_request.xml) -- falls back to the
|
||||||
|
// voucher number only for a row approved before that column existed, since something has
|
||||||
|
// to go in a NOT NULL-shaped identity column and the voucher number at least resolves
|
||||||
|
// back to this same row.
|
||||||
|
final String glTxnId = req.get("gl_transaction_id") != null ? (String) req.get("gl_transaction_id") : voucherNo;
|
||||||
|
jdbcTemplate.update(
|
||||||
|
"INSERT INTO m_share_advance_collection "
|
||||||
|
+ "(receipt_no, voucher_no, client_id, office_id, collection_date, amount, currency_code, status, "
|
||||||
|
+ " gl_transaction_id, collected_by_user, collected_by_name, note) "
|
||||||
|
+ "VALUES (?,?,?,?,?,?,?,'UNAPPLIED',?,?,?,?)",
|
||||||
|
receiptNo, voucherNo, memberClientId, officeId, java.sql.Date.valueOf(collectionDate), req.get("dr_amount"), currency,
|
||||||
|
glTxnId, toLong(req.get("clerk_id")), req.get("clerk_name"), note);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private Map<String, Object> adoptApprovedDeposit(final String voucherNo, final Long clientIdHint, final String glAccountNoOverride) {
|
private Map<String, Object> adoptApprovedDeposit(final String voucherNo, final Long clientIdHint, final String glAccountNoOverride) {
|
||||||
if (glAccountNoOverride == null || glAccountNoOverride.isBlank()) {
|
if (glAccountNoOverride == null || glAccountNoOverride.isBlank()) {
|
||||||
throw new PlatformDataIntegrityException("error.msg.shareadvance.gl.account.required",
|
throw new PlatformDataIntegrityException("error.msg.shareadvance.gl.account.required",
|
||||||
@@ -618,8 +799,10 @@ public class ShareAdvanceCollectionApiResource {
|
|||||||
final List<Map<String, Object>> deposits = jdbcTemplate
|
final List<Map<String, Object>> deposits = jdbcTemplate
|
||||||
.queryForList(
|
.queryForList(
|
||||||
"SELECT d.id, d.voucher_no, d.client_id, d.amount, d.status, d.txn_date, d.txn_id,"
|
"SELECT d.id, d.voucher_no, d.client_id, d.amount, d.status, d.txn_date, d.txn_id,"
|
||||||
+ " d.office_id, d.currency_code, d.cashier_id, d.cashier_name, d.share_account_id"
|
+ " d.office_id, d.currency_code, d.cashier_id, d.cashier_name, d.share_account_id,"
|
||||||
|
+ " cv.code_value AS entry_type"
|
||||||
+ " FROM m_cashier_deposit_request d"
|
+ " FROM m_cashier_deposit_request d"
|
||||||
|
+ " LEFT JOIN m_code_value cv ON cv.id = d.entry_type_cv_id"
|
||||||
+ " WHERE d.voucher_no = ? AND d.account_no = ? AND d.status = 'APPROVED'" + " ORDER BY d.id DESC",
|
+ " WHERE d.voucher_no = ? AND d.account_no = ? AND d.status = 'APPROVED'" + " ORDER BY d.id DESC",
|
||||||
voucherNo, shareSundryAccountNo);
|
voucherNo, shareSundryAccountNo);
|
||||||
|
|
||||||
@@ -632,6 +815,23 @@ public class ShareAdvanceCollectionApiResource {
|
|||||||
throw new PlatformDataIntegrityException("error.msg.shareadvance.voucher.already.applied",
|
throw new PlatformDataIntegrityException("error.msg.shareadvance.voucher.already.applied",
|
||||||
"Voucher " + voucherNo + " has already been applied to a share account.");
|
"Voucher " + voucherNo + " has already been applied to a share account.");
|
||||||
}
|
}
|
||||||
|
// The counter tags what a GL receipt was actually collected for at save time, from the
|
||||||
|
// GLEntryType code (Admin > System > Manage Codes), precisely so a manager here can never
|
||||||
|
// pick up money that was really meant for something else (a Demand Draft, say) just because
|
||||||
|
// it happens to sit in the same GL account. "General" and a missing tag (pre-existing rows
|
||||||
|
// from before this tag existed) are still adoptable -- only an explicit, different purpose
|
||||||
|
// blocks it.
|
||||||
|
final String depositEntryType = (String) dep.get("entry_type");
|
||||||
|
if (depositEntryType != null && !"General".equals(depositEntryType) && !"Share Collection".equals(depositEntryType)) {
|
||||||
|
throw new PlatformDataIntegrityException("error.msg.shareadvance.voucher.wrong.purpose",
|
||||||
|
"Voucher " + voucherNo + " was recorded as " + depositEntryType + ", not a share collection. "
|
||||||
|
+ "Choose a different voucher, or ask the cashier to correct the entry type.");
|
||||||
|
}
|
||||||
|
if (!jdbcTemplate.queryForList("SELECT id FROM m_sponsor_dd_issue_request WHERE cashier_deposit_request_id = ?",
|
||||||
|
toLong(dep.get("id"))).isEmpty()) {
|
||||||
|
throw new PlatformDataIntegrityException("error.msg.shareadvance.voucher.already.applied",
|
||||||
|
"Voucher " + voucherNo + " has already been used to issue a Demand Draft.");
|
||||||
|
}
|
||||||
|
|
||||||
// Prefer the member recorded on the deposit; fall back to the account being settled.
|
// Prefer the member recorded on the deposit; fall back to the account being settled.
|
||||||
final Long clientId = dep.get("client_id") != null ? toLong(dep.get("client_id")) : clientIdHint;
|
final Long clientId = dep.get("client_id") != null ? toLong(dep.get("client_id")) : clientIdHint;
|
||||||
@@ -657,7 +857,7 @@ public class ShareAdvanceCollectionApiResource {
|
|||||||
|
|
||||||
return jdbcTemplate.queryForMap(
|
return jdbcTemplate.queryForMap(
|
||||||
"SELECT c.id, c.receipt_no, c.voucher_no, c.client_id, cl.display_name AS client_name,"
|
"SELECT c.id, c.receipt_no, c.voucher_no, c.client_id, cl.display_name AS client_name,"
|
||||||
+ " c.collection_date, c.amount, c.currency_code, c.status, c.note, c.applied_share_account_id"
|
+ " c.collection_date, c.amount, c.currency_code, c.status, c.note, c.collected_by_name, c.applied_share_account_id"
|
||||||
+ " FROM m_share_advance_collection c JOIN m_client cl ON cl.id = c.client_id" + " WHERE c.receipt_no = ?",
|
+ " FROM m_share_advance_collection c JOIN m_client cl ON cl.id = c.client_id" + " WHERE c.receipt_no = ?",
|
||||||
receiptNo);
|
receiptNo);
|
||||||
}
|
}
|
||||||
@@ -679,16 +879,27 @@ public class ShareAdvanceCollectionApiResource {
|
|||||||
private Map<String, Object> adoptApprovedClerkDisbursementCollection(final String voucherNo, final String glAccountNoOverride) {
|
private Map<String, Object> adoptApprovedClerkDisbursementCollection(final String voucherNo, final String glAccountNoOverride) {
|
||||||
final String glAccountNo = glAccountNoOverride.trim();
|
final String glAccountNo = glAccountNoOverride.trim();
|
||||||
final List<Map<String, Object>> requests = jdbcTemplate.queryForList(
|
final List<Map<String, Object>> requests = jdbcTemplate.queryForList(
|
||||||
"SELECT id, dr_amount, currency_code, office_id, credit_entries, clerk_id, clerk_name, submitted_at, actioned_at"
|
"SELECT d.id, d.dr_amount, d.currency_code, d.office_id, d.credit_entries, d.clerk_id, d.clerk_name,"
|
||||||
+ " FROM m_clerk_disbursement_request"
|
+ " d.submitted_at, d.actioned_at, d.description, d.gl_transaction_id, cv.code_value AS entry_type"
|
||||||
+ " WHERE voucher_no = ? AND TRIM(gl_account_no) = ? AND status = 'APPROVED' AND request_type = 'COLLECTION'"
|
+ " FROM m_clerk_disbursement_request d"
|
||||||
+ " ORDER BY id DESC",
|
+ " LEFT JOIN m_code_value cv ON cv.id = d.entry_type_cv_id"
|
||||||
|
+ " WHERE d.voucher_no = ? AND TRIM(d.gl_account_no) = ? AND d.status = 'APPROVED' AND d.request_type = 'COLLECTION'"
|
||||||
|
+ " ORDER BY d.id DESC",
|
||||||
voucherNo, glAccountNo);
|
voucherNo, glAccountNo);
|
||||||
if (requests.isEmpty()) {
|
if (requests.isEmpty()) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
final Map<String, Object> req = requests.get(0);
|
final Map<String, Object> req = requests.get(0);
|
||||||
|
|
||||||
|
// Same purpose guard as the cashier deposit adoption above -- a COLLECTION credits a GL
|
||||||
|
// account the counter picked by hand, so it carries the identical ambiguity.
|
||||||
|
final String reqEntryType = (String) req.get("entry_type");
|
||||||
|
if (reqEntryType != null && !"General".equals(reqEntryType) && !"Share Collection".equals(reqEntryType)) {
|
||||||
|
throw new PlatformDataIntegrityException("error.msg.shareadvance.voucher.wrong.purpose",
|
||||||
|
"Voucher " + voucherNo + " was recorded as " + reqEntryType + ", not a share collection. "
|
||||||
|
+ "Choose a different voucher, or ask the clerk to correct the entry type.");
|
||||||
|
}
|
||||||
|
|
||||||
final Long savingsAccountId = debitSavingsAccountId((String) req.get("credit_entries"));
|
final Long savingsAccountId = debitSavingsAccountId((String) req.get("credit_entries"));
|
||||||
if (savingsAccountId == null) {
|
if (savingsAccountId == null) {
|
||||||
throw new PlatformDataIntegrityException("error.msg.shareadvance.voucher.no.client",
|
throw new PlatformDataIntegrityException("error.msg.shareadvance.voucher.no.client",
|
||||||
@@ -706,16 +917,20 @@ public class ShareAdvanceCollectionApiResource {
|
|||||||
: LocalDate.now(DateUtils.getDateTimeZoneOfTenant());
|
: LocalDate.now(DateUtils.getDateTimeZoneOfTenant());
|
||||||
|
|
||||||
final String receiptNo = nextReceiptNo();
|
final String receiptNo = nextReceiptNo();
|
||||||
|
final String reqDescription = (String) req.get("description");
|
||||||
|
final String note = reqDescription != null && !reqDescription.isBlank() ? reqDescription
|
||||||
|
: "Adopted from clerk disbursement voucher " + voucherNo;
|
||||||
|
final String glTxnId = req.get("gl_transaction_id") != null ? (String) req.get("gl_transaction_id") : voucherNo;
|
||||||
jdbcTemplate.update(
|
jdbcTemplate.update(
|
||||||
"INSERT INTO m_share_advance_collection "
|
"INSERT INTO m_share_advance_collection "
|
||||||
+ "(receipt_no, voucher_no, client_id, office_id, collection_date, amount, currency_code, status, "
|
+ "(receipt_no, voucher_no, client_id, office_id, collection_date, amount, currency_code, status, "
|
||||||
+ " gl_transaction_id, collected_by_user, collected_by_name, note) " + "VALUES (?,?,?,?,?,?,?,'UNAPPLIED',?,?,?,?)",
|
+ " gl_transaction_id, collected_by_user, collected_by_name, note) " + "VALUES (?,?,?,?,?,?,?,'UNAPPLIED',?,?,?,?)",
|
||||||
receiptNo, voucherNo, clientId, officeId, java.sql.Date.valueOf(collectionDate), req.get("dr_amount"), currency, voucherNo,
|
receiptNo, voucherNo, clientId, officeId, java.sql.Date.valueOf(collectionDate), req.get("dr_amount"), currency, glTxnId,
|
||||||
toLong(req.get("clerk_id")), req.get("clerk_name"), "Adopted from clerk disbursement voucher " + voucherNo);
|
toLong(req.get("clerk_id")), req.get("clerk_name"), note);
|
||||||
|
|
||||||
return jdbcTemplate.queryForMap(
|
return jdbcTemplate.queryForMap(
|
||||||
"SELECT c.id, c.receipt_no, c.voucher_no, c.client_id, cl.display_name AS client_name,"
|
"SELECT c.id, c.receipt_no, c.voucher_no, c.client_id, cl.display_name AS client_name,"
|
||||||
+ " c.collection_date, c.amount, c.currency_code, c.status, c.note, c.applied_share_account_id"
|
+ " c.collection_date, c.amount, c.currency_code, c.status, c.note, c.collected_by_name, c.applied_share_account_id"
|
||||||
+ " FROM m_share_advance_collection c JOIN m_client cl ON cl.id = c.client_id" + " WHERE c.receipt_no = ?",
|
+ " FROM m_share_advance_collection c JOIN m_client cl ON cl.id = c.client_id" + " WHERE c.receipt_no = ?",
|
||||||
receiptNo);
|
receiptNo);
|
||||||
}
|
}
|
||||||
|
|||||||
+34
-8
@@ -116,6 +116,12 @@ public class ClerkDisbursementRequestApiResource {
|
|||||||
|| !java.util.Set.of("DISBURSEMENT", "COLLECTION", "BANK_TRANSFER", "FUND_TRANSFER").contains(requestType)) {
|
|| !java.util.Set.of("DISBURSEMENT", "COLLECTION", "BANK_TRANSFER", "FUND_TRANSFER").contains(requestType)) {
|
||||||
requestType = "DISBURSEMENT";
|
requestType = "DISBURSEMENT";
|
||||||
}
|
}
|
||||||
|
// What this GL leg is actually for — a code value under "GLEntryType" (Admin > System >
|
||||||
|
// Manage Codes). A COLLECTION credits a GL account the counter picks by hand, the same
|
||||||
|
// ambiguity a cashier Receipt has (see CashierDepositRequestApiResource and
|
||||||
|
// 0565_convert_entry_type_to_code_value.xml). ShareAdvanceCollectionApiResource checks this
|
||||||
|
// before adopting one of these rows as a share collection.
|
||||||
|
final Long entryTypeCvId = resolveEntryTypeCvId(fromJsonHelper.extractLongNamed("entryTypeCvId", root));
|
||||||
|
|
||||||
if (creditEntriesJson == null || creditEntriesJson.isBlank()) {
|
if (creditEntriesJson == null || creditEntriesJson.isBlank()) {
|
||||||
throw apiError(400, "At least one entry is required");
|
throw apiError(400, "At least one entry is required");
|
||||||
@@ -208,8 +214,8 @@ public class ClerkDisbursementRequestApiResource {
|
|||||||
INSERT INTO m_clerk_disbursement_request
|
INSERT INTO m_clerk_disbursement_request
|
||||||
(gl_account_id, gl_account_no, gl_account_name, dr_amount, office_id, currency_code,
|
(gl_account_id, gl_account_no, gl_account_name, dr_amount, office_id, currency_code,
|
||||||
credit_entries, payment_type_id, payment_type_name, description,
|
credit_entries, payment_type_id, payment_type_name, description,
|
||||||
status, clerk_id, clerk_name, clerk_username, activity_log, submitted_at, request_type)
|
status, clerk_id, clerk_name, clerk_username, activity_log, submitted_at, request_type, entry_type_cv_id)
|
||||||
VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)
|
VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)
|
||||||
RETURNING id
|
RETURNING id
|
||||||
""",
|
""",
|
||||||
Long.class,
|
Long.class,
|
||||||
@@ -218,7 +224,7 @@ public class ClerkDisbursementRequestApiResource {
|
|||||||
correctedCreditEntriesJson, paymentTypeId, paymentTypeName, description,
|
correctedCreditEntriesJson, paymentTypeId, paymentTypeName, description,
|
||||||
"PENDING",
|
"PENDING",
|
||||||
currentUser.getId(), currentUser.getDisplayName(), currentUser.getUsername(),
|
currentUser.getId(), currentUser.getDisplayName(), currentUser.getUsername(),
|
||||||
activityLog, Timestamp.from(Instant.now()), requestType
|
activityLog, Timestamp.from(Instant.now()), requestType, entryTypeCvId
|
||||||
);
|
);
|
||||||
|
|
||||||
// A collection pulls money into the GL, so it reads as a credit; a disbursement moves
|
// A collection pulls money into the GL, so it reads as a credit; a disbursement moves
|
||||||
@@ -306,7 +312,7 @@ public class ClerkDisbursementRequestApiResource {
|
|||||||
+ " credit_entries, payment_type_name, description,"
|
+ " credit_entries, payment_type_name, description,"
|
||||||
+ " clerk_id, clerk_name, clerk_username,"
|
+ " clerk_id, clerk_name, clerk_username,"
|
||||||
+ " manager_id, manager_name, manager_username,"
|
+ " manager_id, manager_name, manager_username,"
|
||||||
+ " rejection_reason, txn_refs, error_message, submitted_at, actioned_at"
|
+ " rejection_reason, txn_refs, error_message, submitted_at, actioned_at, entry_type_cv_id"
|
||||||
+ " FROM m_clerk_disbursement_request " + where
|
+ " FROM m_clerk_disbursement_request " + where
|
||||||
+ " ORDER BY submitted_at DESC LIMIT ? OFFSET ?",
|
+ " ORDER BY submitted_at DESC LIMIT ? OFFSET ?",
|
||||||
pageParams.toArray());
|
pageParams.toArray());
|
||||||
@@ -373,7 +379,7 @@ public class ClerkDisbursementRequestApiResource {
|
|||||||
final List<Map<String, Object>> rows = jdbcTemplate.queryForList(
|
final List<Map<String, Object>> rows = jdbcTemplate.queryForList(
|
||||||
"SELECT id, voucher_no, status, request_type, gl_account_no, gl_account_name, dr_amount,"
|
"SELECT id, voucher_no, status, request_type, gl_account_no, gl_account_name, dr_amount,"
|
||||||
+ " credit_entries, payment_type_name, description, clerk_name,"
|
+ " credit_entries, payment_type_name, description, clerk_name,"
|
||||||
+ " manager_name, rejection_reason, txn_refs, error_message, submitted_at, actioned_at"
|
+ " manager_name, rejection_reason, txn_refs, error_message, submitted_at, actioned_at, entry_type_cv_id"
|
||||||
+ " FROM m_clerk_disbursement_request " + where
|
+ " FROM m_clerk_disbursement_request " + where
|
||||||
+ " ORDER BY submitted_at DESC LIMIT ? OFFSET ?",
|
+ " ORDER BY submitted_at DESC LIMIT ? OFFSET ?",
|
||||||
pageParams.toArray());
|
pageParams.toArray());
|
||||||
@@ -497,6 +503,11 @@ public class ClerkDisbursementRequestApiResource {
|
|||||||
final List<String> withdrawalSavingsTxnRefs = new ArrayList<>();
|
final List<String> withdrawalSavingsTxnRefs = new ArrayList<>();
|
||||||
BigDecimal withdrawalSavingsTotal = BigDecimal.ZERO;
|
BigDecimal withdrawalSavingsTotal = BigDecimal.ZERO;
|
||||||
Long collectionTargetGlId = null;
|
Long collectionTargetGlId = null;
|
||||||
|
// The real acc_gl_journal_entry.transaction_id this approval posts -- captured here because
|
||||||
|
// nothing else on the row remembers it otherwise (txn_refs only holds the account-side
|
||||||
|
// transaction ids). Whoever needs the actual GL posting afterwards (Share Advance Collection
|
||||||
|
// adopting a tagged voucher, a GL detail view) reads it back from gl_transaction_id.
|
||||||
|
String approvedGlTxnId = null;
|
||||||
|
|
||||||
// Savings-control GL data per account for the direct combined journal entry.
|
// Savings-control GL data per account for the direct combined journal entry.
|
||||||
// collectionGlEntries: DR SavingsControl per savings account (collection/withdrawal mode).
|
// collectionGlEntries: DR SavingsControl per savings account (collection/withdrawal mode).
|
||||||
@@ -562,6 +573,7 @@ public class ClerkDisbursementRequestApiResource {
|
|||||||
linkGlEntriesToSavingsAccounts(btGlTxnId, btDebitGlEntries, 2);
|
linkGlEntriesToSavingsAccounts(btGlTxnId, btDebitGlEntries, 2);
|
||||||
linkGlEntriesToSavingsAccounts(btGlTxnId, btCreditGlEntries, 1);
|
linkGlEntriesToSavingsAccounts(btGlTxnId, btCreditGlEntries, 1);
|
||||||
}
|
}
|
||||||
|
approvedGlTxnId = btGlTxnId;
|
||||||
|
|
||||||
// Audit log in m_bank_transfer_log
|
// Audit log in m_bank_transfer_log
|
||||||
final String btTxnRefsStr = String.join(",", txnRefs);
|
final String btTxnRefsStr = String.join(",", txnRefs);
|
||||||
@@ -707,6 +719,7 @@ public class ClerkDisbursementRequestApiResource {
|
|||||||
if (disbGlTxnId != null) {
|
if (disbGlTxnId != null) {
|
||||||
linkGlEntriesToSavingsAccounts(disbGlTxnId, disbursementGlEntries, 1);
|
linkGlEntriesToSavingsAccounts(disbGlTxnId, disbursementGlEntries, 1);
|
||||||
}
|
}
|
||||||
|
approvedGlTxnId = disbGlTxnId;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Collection GL entry ───────────────────────────────────────────────
|
// ── Collection GL entry ───────────────────────────────────────────────
|
||||||
@@ -729,6 +742,7 @@ public class ClerkDisbursementRequestApiResource {
|
|||||||
if (collGlTxnId != null) {
|
if (collGlTxnId != null) {
|
||||||
linkGlEntriesToSavingsAccounts(collGlTxnId, collectionGlEntries, 2);
|
linkGlEntriesToSavingsAccounts(collGlTxnId, collectionGlEntries, 2);
|
||||||
}
|
}
|
||||||
|
approvedGlTxnId = collGlTxnId;
|
||||||
} else {
|
} else {
|
||||||
final Long cashVaultGlId = findCashVaultGlId(withdrawalSavingsTxnRefs, 1);
|
final Long cashVaultGlId = findCashVaultGlId(withdrawalSavingsTxnRefs, 1);
|
||||||
if (cashVaultGlId != null) {
|
if (cashVaultGlId != null) {
|
||||||
@@ -780,11 +794,11 @@ public class ClerkDisbursementRequestApiResource {
|
|||||||
jdbcTemplate.update("""
|
jdbcTemplate.update("""
|
||||||
UPDATE m_clerk_disbursement_request SET
|
UPDATE m_clerk_disbursement_request SET
|
||||||
status = 'APPROVED', manager_id = ?, manager_name = ?, manager_username = ?,
|
status = 'APPROVED', manager_id = ?, manager_name = ?, manager_username = ?,
|
||||||
txn_refs = ?, activity_log = ?, actioned_at = ?
|
txn_refs = ?, gl_transaction_id = ?, activity_log = ?, actioned_at = ?
|
||||||
WHERE id = ?
|
WHERE id = ?
|
||||||
""",
|
""",
|
||||||
currentUser.getId(), currentUser.getDisplayName(), currentUser.getUsername(),
|
currentUser.getId(), currentUser.getDisplayName(), currentUser.getUsername(),
|
||||||
txnRefsStr, updatedLog, Timestamp.from(Instant.now()), id);
|
txnRefsStr, approvedGlTxnId, updatedLog, Timestamp.from(Instant.now()), id);
|
||||||
|
|
||||||
final JsonObject res = new JsonObject();
|
final JsonObject res = new JsonObject();
|
||||||
res.addProperty("resourceId", id);
|
res.addProperty("resourceId", id);
|
||||||
@@ -1262,7 +1276,7 @@ public class ClerkDisbursementRequestApiResource {
|
|||||||
office_id, currency_code, credit_entries, payment_type_id, payment_type_name,
|
office_id, currency_code, credit_entries, payment_type_id, payment_type_name,
|
||||||
description, clerk_id, clerk_name, clerk_username,
|
description, clerk_id, clerk_name, clerk_username,
|
||||||
manager_id, manager_name, manager_username, rejection_reason,
|
manager_id, manager_name, manager_username, rejection_reason,
|
||||||
txn_refs, error_message, activity_log, submitted_at, actioned_at
|
txn_refs, error_message, activity_log, submitted_at, actioned_at, entry_type_cv_id
|
||||||
FROM m_clerk_disbursement_request WHERE id = ?
|
FROM m_clerk_disbursement_request WHERE id = ?
|
||||||
""", id);
|
""", id);
|
||||||
if (rows.isEmpty()) {
|
if (rows.isEmpty()) {
|
||||||
@@ -1321,6 +1335,18 @@ public class ClerkDisbursementRequestApiResource {
|
|||||||
try { return new BigDecimal(raw); } catch (NumberFormatException e) { return null; }
|
try { return new BigDecimal(raw); } catch (NumberFormatException e) { return null; }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** The counter's choice, when they made one — otherwise "General" under the GLEntryType code,
|
||||||
|
* resolved by name rather than a hardcoded id since Manage Codes owns these rows now. */
|
||||||
|
private Long resolveEntryTypeCvId(final Long provided) {
|
||||||
|
if (provided != null) {
|
||||||
|
return provided;
|
||||||
|
}
|
||||||
|
return jdbcTemplate.queryForObject(
|
||||||
|
"SELECT cv.id FROM m_code_value cv JOIN m_code c ON c.id = cv.code_id"
|
||||||
|
+ " WHERE c.code_name = 'GLEntryType' AND cv.code_value = 'General'",
|
||||||
|
Long.class);
|
||||||
|
}
|
||||||
|
|
||||||
private String buildActivityLog(final String existingJson, final String action,
|
private String buildActivityLog(final String existingJson, final String action,
|
||||||
final String username, final String note) {
|
final String username, final String note) {
|
||||||
JsonArray log = new JsonArray();
|
JsonArray log = new JsonArray();
|
||||||
|
|||||||
+1
-1
@@ -171,7 +171,7 @@ public class ShareAdditionRequestsApiResource {
|
|||||||
final String placeholders = String.join(",", Collections.nCopies(ids.size(), "?"));
|
final String placeholders = String.join(",", Collections.nCopies(ids.size(), "?"));
|
||||||
return this.jdbcTemplate.queryForList("""
|
return this.jdbcTemplate.queryForList("""
|
||||||
SELECT id, receipt_no AS "receiptNo", voucher_no AS "voucherNo", amount, collection_date AS "collectionDate",
|
SELECT id, receipt_no AS "receiptNo", voucher_no AS "voucherNo", amount, collection_date AS "collectionDate",
|
||||||
collected_by_name AS "collectedByName", status
|
collected_by_name AS "collectedByName", status, note
|
||||||
FROM m_share_advance_collection WHERE id IN (%s) ORDER BY id
|
FROM m_share_advance_collection WHERE id IN (%s) ORDER BY id
|
||||||
""".formatted(placeholders), ids.toArray());
|
""".formatted(placeholders), ids.toArray());
|
||||||
}
|
}
|
||||||
|
|||||||
+9
@@ -799,6 +799,15 @@ public class ShareAccountDataSerializer {
|
|||||||
advanceCollectionIds = extractAdvanceCollectionIds(element);
|
advanceCollectionIds = extractAdvanceCollectionIds(element);
|
||||||
if (advanceCollectionIds.isEmpty()) {
|
if (advanceCollectionIds.isEmpty()) {
|
||||||
baseDataValidator.reset().parameter(ShareAccountApiConstants.advancecollectionid_paramname).value(null).notNull();
|
baseDataValidator.reset().parameter(ShareAccountApiConstants.advancecollectionid_paramname).value(null).notNull();
|
||||||
|
} else if (advanceCollectionIds.size() > 1) {
|
||||||
|
// Additional-share tranches fund from exactly one receipt for now -- combining
|
||||||
|
// instalments across several is the first-application Apply Collection dialog's own
|
||||||
|
// behaviour, not this one's, until this screen is asked to support it too.
|
||||||
|
baseDataValidator.reset().parameter(ShareAccountApiConstants.additionalcollectionids_paramname)
|
||||||
|
.value(advanceCollectionIds.size())
|
||||||
|
.failWithCodeAndMessage("share.tranche.single.voucher.only",
|
||||||
|
"Only one voucher can currently fund an additional-share tranche. " + advanceCollectionIds.size()
|
||||||
|
+ " were submitted.");
|
||||||
} else {
|
} else {
|
||||||
validateAdvanceCollectionsForTranche(advanceCollectionIds, account, sharesRequested, requestedDate, baseDataValidator);
|
validateAdvanceCollectionsForTranche(advanceCollectionIds, account, sharesRequested, requestedDate, baseDataValidator);
|
||||||
}
|
}
|
||||||
|
|||||||
+16
-1
@@ -429,7 +429,9 @@ public class SponsorDDIssueRequestWritePlatformServiceImpl implements SponsorDDI
|
|||||||
+ "(AC.TP = GL, target account = the DD Cash Suspense GL account, amount = " + expectedTotal + ").");
|
+ "(AC.TP = GL, target account = the DD Cash Suspense GL account, amount = " + expectedTotal + ").");
|
||||||
}
|
}
|
||||||
final List<Map<String, Object>> rows = this.jdbcTemplate.queryForList(
|
final List<Map<String, Object>> rows = this.jdbcTemplate.queryForList(
|
||||||
"SELECT id, status, gl_account_id, amount, txn_id FROM m_cashier_deposit_request WHERE id = ?", cashierDepositRequestId);
|
"SELECT d.id, d.status, d.gl_account_id, d.amount, d.txn_id, d.share_account_id, cv.code_value AS entry_type"
|
||||||
|
+ " FROM m_cashier_deposit_request d LEFT JOIN m_code_value cv ON cv.id = d.entry_type_cv_id WHERE d.id = ?",
|
||||||
|
cashierDepositRequestId);
|
||||||
if (rows.isEmpty()) {
|
if (rows.isEmpty()) {
|
||||||
throw new IllegalArgumentException("Cashier deposit request not found: " + cashierDepositRequestId);
|
throw new IllegalArgumentException("Cashier deposit request not found: " + cashierDepositRequestId);
|
||||||
}
|
}
|
||||||
@@ -438,6 +440,19 @@ public class SponsorDDIssueRequestWritePlatformServiceImpl implements SponsorDDI
|
|||||||
throw new IllegalStateException(
|
throw new IllegalStateException(
|
||||||
"The referenced cashier deposit request is not yet approved by a manager. Current status: " + deposit.get("status"));
|
"The referenced cashier deposit request is not yet approved by a manager. Current status: " + deposit.get("status"));
|
||||||
}
|
}
|
||||||
|
if (deposit.get("share_account_id") != null) {
|
||||||
|
throw new IllegalStateException("This cashier deposit request has already been used for a share application.");
|
||||||
|
}
|
||||||
|
// The counter tags what a GL receipt was actually collected for at save time -- a code value
|
||||||
|
// under GLEntryType (Admin > System > Manage Codes). See
|
||||||
|
// 0565_convert_entry_type_to_code_value.xml. "General" and a missing tag (rows predating the
|
||||||
|
// column) are still usable; only an explicit, different purpose blocks it here.
|
||||||
|
final Object entryType = deposit.get("entry_type");
|
||||||
|
if (entryType != null && !"General".equals(entryType) && !"DD Issue".equals(entryType)) {
|
||||||
|
throw new IllegalStateException(
|
||||||
|
"This cashier deposit request was recorded as " + entryType + ", not a Demand Draft issuance. Ask the cashier to "
|
||||||
|
+ "correct the entry type, or select a different deposit request.");
|
||||||
|
}
|
||||||
final Object depositGlAccountObj = deposit.get("gl_account_id");
|
final Object depositGlAccountObj = deposit.get("gl_account_id");
|
||||||
final Long depositGlAccountId = depositGlAccountObj != null ? ((Number) depositGlAccountObj).longValue() : null;
|
final Long depositGlAccountId = depositGlAccountObj != null ? ((Number) depositGlAccountObj).longValue() : null;
|
||||||
if (!cashSuspenseGlAccountId.equals(depositGlAccountId)) {
|
if (!cashSuspenseGlAccountId.equals(depositGlAccountId)) {
|
||||||
|
|||||||
+6
@@ -626,10 +626,16 @@ public class SponsorDDReadPlatformServiceImpl implements SponsorDDReadPlatformSe
|
|||||||
final String sql = "select cdr.id, cdr.voucher_no, cdr.amount, cdr.description, cdr.cashier_username, cdr.actioned_at, "
|
final String sql = "select cdr.id, cdr.voucher_no, cdr.amount, cdr.description, cdr.cashier_username, cdr.actioned_at, "
|
||||||
+ "cdr.dd_face_value, cdr.dd_charges, cdr.dd_gst_percentage, cdr.dd_gst_amount "
|
+ "cdr.dd_face_value, cdr.dd_charges, cdr.dd_gst_percentage, cdr.dd_gst_amount "
|
||||||
+ "from m_cashier_deposit_request cdr "
|
+ "from m_cashier_deposit_request cdr "
|
||||||
|
+ "left join m_code_value cv on cv.id = cdr.entry_type_cv_id "
|
||||||
+ "where cdr.status = 'APPROVED' "
|
+ "where cdr.status = 'APPROVED' "
|
||||||
+ "and cdr.gl_account_id = (select cash_suspense_gl_account_id from m_sponsor_dd_gl_mapping limit 1) "
|
+ "and cdr.gl_account_id = (select cash_suspense_gl_account_id from m_sponsor_dd_gl_mapping limit 1) "
|
||||||
+ "and cdr.id not in (select cashier_deposit_request_id from m_sponsor_dd_issue_request "
|
+ "and cdr.id not in (select cashier_deposit_request_id from m_sponsor_dd_issue_request "
|
||||||
+ "where cashier_deposit_request_id is not null) "
|
+ "where cashier_deposit_request_id is not null) "
|
||||||
|
// Already spoken for by a share application, or tagged for something else at save
|
||||||
|
// time -- kept out of the picker rather than only rejected at submit, so the counter
|
||||||
|
// never even offers the wrong voucher. See 0565_convert_entry_type_to_code_value.xml.
|
||||||
|
+ "and cdr.share_account_id is null "
|
||||||
|
+ "and (cv.code_value IS NULL OR cv.code_value IN ('General', 'DD Issue')) "
|
||||||
+ "order by cdr.actioned_at desc";
|
+ "order by cdr.actioned_at desc";
|
||||||
return this.jdbcTemplate.query(sql, (rs, rowNum) -> new SponsorDDCashierDepositOptionData(
|
return this.jdbcTemplate.query(sql, (rs, rowNum) -> new SponsorDDCashierDepositOptionData(
|
||||||
rs.getLong("id"),
|
rs.getLong("id"),
|
||||||
|
|||||||
@@ -109,4 +109,7 @@
|
|||||||
<include file="parts/0553_add_voucher_no_to_dividend_claim_and_dd_issue_request.xml" relativeToChangelogFile="true"/>
|
<include file="parts/0553_add_voucher_no_to_dividend_claim_and_dd_issue_request.xml" relativeToChangelogFile="true"/>
|
||||||
<include file="parts/0554_add_gl_account_to_sponsor_dd_issue_request.xml" relativeToChangelogFile="true"/>
|
<include file="parts/0554_add_gl_account_to_sponsor_dd_issue_request.xml" relativeToChangelogFile="true"/>
|
||||||
<include file="parts/0563_ledger_txn_sequence.xml" relativeToChangelogFile="true"/>
|
<include file="parts/0563_ledger_txn_sequence.xml" relativeToChangelogFile="true"/>
|
||||||
|
<include file="parts/0564_add_entry_type_to_gl_requests.xml" relativeToChangelogFile="true"/>
|
||||||
|
<include file="parts/0565_convert_entry_type_to_code_value.xml" relativeToChangelogFile="true"/>
|
||||||
|
<include file="parts/0566_add_gl_transaction_id_to_clerk_disbursement_request.xml" relativeToChangelogFile="true"/>
|
||||||
</databaseChangeLog>
|
</databaseChangeLog>
|
||||||
|
|||||||
+66
@@ -0,0 +1,66 @@
|
|||||||
|
<?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">
|
||||||
|
|
||||||
|
<!--
|
||||||
|
A cashier "Receipt" and a clerk "Collection" both let the counter pick, by hand, any GL
|
||||||
|
account to post cash into — deliberately, so a broken product mapping never blocks a genuine
|
||||||
|
voucher (see ShareAdvanceCollectionApiResource). That freedom is also the hole: two totally
|
||||||
|
unrelated flows (a Demand Draft cash intake, a share advance collection) can land in the same
|
||||||
|
GL account with nothing on the row itself saying which one the money was actually collected
|
||||||
|
for. A manager applying a share request has already picked up a voucher that was really meant
|
||||||
|
to fund a DD, because nothing distinguished them.
|
||||||
|
|
||||||
|
entry_type is that distinguishing tag, set by the counter at save time and carried on the
|
||||||
|
request row for its whole life. Every consumer that adopts one of these rows into its own
|
||||||
|
flow (Share Advance Collection, Sponsor DD issuance) checks it before adopting, and it is
|
||||||
|
exactly what lets a day-end reconciliation group GL-affecting counter activity by what it was
|
||||||
|
actually for.
|
||||||
|
|
||||||
|
Defaults to GENERAL rather than being required, so every existing row and every caller that
|
||||||
|
does not yet send it keeps working exactly as before.
|
||||||
|
-->
|
||||||
|
|
||||||
|
<changeSet id="0564_add_entry_type_to_cashier_deposit_request" author="CBS">
|
||||||
|
<preConditions onFail="MARK_RAN">
|
||||||
|
<not><columnExists tableName="m_cashier_deposit_request" columnName="entry_type"/></not>
|
||||||
|
</preConditions>
|
||||||
|
<addColumn tableName="m_cashier_deposit_request">
|
||||||
|
<column name="entry_type" type="VARCHAR(30)" defaultValue="GENERAL">
|
||||||
|
<constraints nullable="false"/>
|
||||||
|
</column>
|
||||||
|
</addColumn>
|
||||||
|
</changeSet>
|
||||||
|
|
||||||
|
<changeSet id="0564_index_entry_type_cashier_deposit_request" author="CBS">
|
||||||
|
<preConditions onFail="MARK_RAN">
|
||||||
|
<not><indexExists indexName="idx_cdr_entry_type"/></not>
|
||||||
|
</preConditions>
|
||||||
|
<createIndex tableName="m_cashier_deposit_request" indexName="idx_cdr_entry_type">
|
||||||
|
<column name="entry_type"/>
|
||||||
|
</createIndex>
|
||||||
|
</changeSet>
|
||||||
|
|
||||||
|
<changeSet id="0564_add_entry_type_to_clerk_disbursement_request" author="CBS">
|
||||||
|
<preConditions onFail="MARK_RAN">
|
||||||
|
<not><columnExists tableName="m_clerk_disbursement_request" columnName="entry_type"/></not>
|
||||||
|
</preConditions>
|
||||||
|
<addColumn tableName="m_clerk_disbursement_request">
|
||||||
|
<column name="entry_type" type="VARCHAR(30)" defaultValue="GENERAL">
|
||||||
|
<constraints nullable="false"/>
|
||||||
|
</column>
|
||||||
|
</addColumn>
|
||||||
|
</changeSet>
|
||||||
|
|
||||||
|
<changeSet id="0564_index_entry_type_clerk_disbursement_request" author="CBS">
|
||||||
|
<preConditions onFail="MARK_RAN">
|
||||||
|
<not><indexExists indexName="idx_disb_entry_type"/></not>
|
||||||
|
</preConditions>
|
||||||
|
<createIndex tableName="m_clerk_disbursement_request" indexName="idx_disb_entry_type">
|
||||||
|
<column name="entry_type"/>
|
||||||
|
</createIndex>
|
||||||
|
</changeSet>
|
||||||
|
|
||||||
|
</databaseChangeLog>
|
||||||
+124
@@ -0,0 +1,124 @@
|
|||||||
|
<?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">
|
||||||
|
|
||||||
|
<!--
|
||||||
|
entry_type (0564_add_entry_type_to_gl_requests.xml) was a fixed three-value VARCHAR, edited
|
||||||
|
only in code. Moved onto the standard m_code/m_code_value mechanism instead, so an admin can
|
||||||
|
add another entry type later (Admin > System > Manage Codes) without a deployment, the same
|
||||||
|
way every other extensible dropdown in this system already works.
|
||||||
|
-->
|
||||||
|
|
||||||
|
<changeSet id="0565_create_gl_entry_type_code" author="CBS">
|
||||||
|
<sql>
|
||||||
|
INSERT INTO m_code (code_name, is_system_defined)
|
||||||
|
SELECT 'GLEntryType', true
|
||||||
|
WHERE NOT EXISTS (SELECT 1 FROM m_code WHERE code_name = 'GLEntryType');
|
||||||
|
</sql>
|
||||||
|
</changeSet>
|
||||||
|
|
||||||
|
<changeSet id="0565_seed_gl_entry_type_values" author="CBS">
|
||||||
|
<sql>
|
||||||
|
INSERT INTO m_code_value (code_id, code_value, order_position, is_active, is_mandatory)
|
||||||
|
SELECT c.id, v.code_value, v.order_position, true, false
|
||||||
|
FROM m_code c
|
||||||
|
CROSS JOIN (VALUES
|
||||||
|
('General', 1),
|
||||||
|
('Share Collection', 2),
|
||||||
|
('DD Issue', 3)
|
||||||
|
) AS v(code_value, order_position)
|
||||||
|
WHERE c.code_name = 'GLEntryType'
|
||||||
|
AND NOT EXISTS (
|
||||||
|
SELECT 1 FROM m_code_value cv
|
||||||
|
WHERE cv.code_id = c.id AND cv.code_value = v.code_value
|
||||||
|
);
|
||||||
|
</sql>
|
||||||
|
</changeSet>
|
||||||
|
|
||||||
|
<changeSet id="0565_add_entry_type_cv_id_cashier_deposit" author="CBS">
|
||||||
|
<preConditions onFail="MARK_RAN">
|
||||||
|
<not><columnExists tableName="m_cashier_deposit_request" columnName="entry_type_cv_id"/></not>
|
||||||
|
</preConditions>
|
||||||
|
<addColumn tableName="m_cashier_deposit_request">
|
||||||
|
<column name="entry_type_cv_id" type="INTEGER"/>
|
||||||
|
</addColumn>
|
||||||
|
<addForeignKeyConstraint baseTableName="m_cashier_deposit_request" baseColumnNames="entry_type_cv_id"
|
||||||
|
constraintName="fk_cdr_entry_type_cv" referencedTableName="m_code_value" referencedColumnNames="id"/>
|
||||||
|
</changeSet>
|
||||||
|
|
||||||
|
<changeSet id="0565_add_entry_type_cv_id_clerk_disbursement" author="CBS">
|
||||||
|
<preConditions onFail="MARK_RAN">
|
||||||
|
<not><columnExists tableName="m_clerk_disbursement_request" columnName="entry_type_cv_id"/></not>
|
||||||
|
</preConditions>
|
||||||
|
<addColumn tableName="m_clerk_disbursement_request">
|
||||||
|
<column name="entry_type_cv_id" type="INTEGER"/>
|
||||||
|
</addColumn>
|
||||||
|
<addForeignKeyConstraint baseTableName="m_clerk_disbursement_request" baseColumnNames="entry_type_cv_id"
|
||||||
|
constraintName="fk_disb_entry_type_cv" referencedTableName="m_code_value" referencedColumnNames="id"/>
|
||||||
|
</changeSet>
|
||||||
|
|
||||||
|
<!-- Carries forward what 0564 already tagged, mapping the fixed old strings onto the new
|
||||||
|
code_value rows one time. Any row still on GENERAL (the default) resolves to "General". -->
|
||||||
|
<changeSet id="0565_backfill_entry_type_cv_id" author="CBS">
|
||||||
|
<sql>
|
||||||
|
UPDATE m_cashier_deposit_request d
|
||||||
|
SET entry_type_cv_id = cv.id
|
||||||
|
FROM m_code_value cv JOIN m_code c ON c.id = cv.code_id
|
||||||
|
WHERE c.code_name = 'GLEntryType'
|
||||||
|
AND cv.code_value = CASE d.entry_type
|
||||||
|
WHEN 'SHARE_COLLECTION' THEN 'Share Collection'
|
||||||
|
WHEN 'DD_ISSUE' THEN 'DD Issue'
|
||||||
|
ELSE 'General'
|
||||||
|
END
|
||||||
|
AND d.entry_type_cv_id IS NULL;
|
||||||
|
|
||||||
|
UPDATE m_clerk_disbursement_request d
|
||||||
|
SET entry_type_cv_id = cv.id
|
||||||
|
FROM m_code_value cv JOIN m_code c ON c.id = cv.code_id
|
||||||
|
WHERE c.code_name = 'GLEntryType'
|
||||||
|
AND cv.code_value = CASE d.entry_type
|
||||||
|
WHEN 'SHARE_COLLECTION' THEN 'Share Collection'
|
||||||
|
WHEN 'DD_ISSUE' THEN 'DD Issue'
|
||||||
|
ELSE 'General'
|
||||||
|
END
|
||||||
|
AND d.entry_type_cv_id IS NULL;
|
||||||
|
</sql>
|
||||||
|
</changeSet>
|
||||||
|
|
||||||
|
<changeSet id="0565_index_entry_type_cv_cashier_deposit" author="CBS">
|
||||||
|
<preConditions onFail="MARK_RAN">
|
||||||
|
<not><indexExists indexName="idx_cdr_entry_type_cv"/></not>
|
||||||
|
</preConditions>
|
||||||
|
<createIndex tableName="m_cashier_deposit_request" indexName="idx_cdr_entry_type_cv">
|
||||||
|
<column name="entry_type_cv_id"/>
|
||||||
|
</createIndex>
|
||||||
|
</changeSet>
|
||||||
|
|
||||||
|
<changeSet id="0565_index_entry_type_cv_clerk_disbursement" author="CBS">
|
||||||
|
<preConditions onFail="MARK_RAN">
|
||||||
|
<not><indexExists indexName="idx_disb_entry_type_cv"/></not>
|
||||||
|
</preConditions>
|
||||||
|
<createIndex tableName="m_clerk_disbursement_request" indexName="idx_disb_entry_type_cv">
|
||||||
|
<column name="entry_type_cv_id"/>
|
||||||
|
</createIndex>
|
||||||
|
</changeSet>
|
||||||
|
|
||||||
|
<changeSet id="0565_drop_entry_type_varchar_cashier_deposit" author="CBS">
|
||||||
|
<preConditions onFail="MARK_RAN">
|
||||||
|
<columnExists tableName="m_cashier_deposit_request" columnName="entry_type"/>
|
||||||
|
</preConditions>
|
||||||
|
<dropIndex tableName="m_cashier_deposit_request" indexName="idx_cdr_entry_type"/>
|
||||||
|
<dropColumn tableName="m_cashier_deposit_request" columnName="entry_type"/>
|
||||||
|
</changeSet>
|
||||||
|
|
||||||
|
<changeSet id="0565_drop_entry_type_varchar_clerk_disbursement" author="CBS">
|
||||||
|
<preConditions onFail="MARK_RAN">
|
||||||
|
<columnExists tableName="m_clerk_disbursement_request" columnName="entry_type"/>
|
||||||
|
</preConditions>
|
||||||
|
<dropIndex tableName="m_clerk_disbursement_request" indexName="idx_disb_entry_type"/>
|
||||||
|
<dropColumn tableName="m_clerk_disbursement_request" columnName="entry_type"/>
|
||||||
|
</changeSet>
|
||||||
|
|
||||||
|
</databaseChangeLog>
|
||||||
+37
@@ -0,0 +1,37 @@
|
|||||||
|
<?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">
|
||||||
|
|
||||||
|
<!--
|
||||||
|
approve() in ClerkDisbursementRequestApiResource posts a real, balanced GL entry for both
|
||||||
|
Disbursement and Collection mode (postCombinedJournalEntry) and gets back its own
|
||||||
|
acc_gl_journal_entry.transaction_id, then only ever uses that id to link the entries back
|
||||||
|
to the savings/current accounts involved, and lets it fall out of scope. Nothing on the row
|
||||||
|
remembers it, so anyone needing the real posting afterwards (Share Advance Collection
|
||||||
|
adopting a tagged voucher, an audit screen, a reversal) had nothing reliable to query by;
|
||||||
|
txn_refs holds the account-side transaction ids, not the GL one.
|
||||||
|
|
||||||
|
gl_transaction_id is that missing reference, written once at approval time next to txn_refs.
|
||||||
|
-->
|
||||||
|
|
||||||
|
<changeSet id="0566_add_gl_transaction_id_to_clerk_disbursement_request" author="CBS">
|
||||||
|
<preConditions onFail="MARK_RAN">
|
||||||
|
<not><columnExists tableName="m_clerk_disbursement_request" columnName="gl_transaction_id"/></not>
|
||||||
|
</preConditions>
|
||||||
|
<addColumn tableName="m_clerk_disbursement_request">
|
||||||
|
<column name="gl_transaction_id" type="VARCHAR(60)"/>
|
||||||
|
</addColumn>
|
||||||
|
</changeSet>
|
||||||
|
|
||||||
|
<changeSet id="0566_index_gl_transaction_id_clerk_disbursement" author="CBS">
|
||||||
|
<preConditions onFail="MARK_RAN">
|
||||||
|
<not><indexExists indexName="idx_disb_gl_transaction_id"/></not>
|
||||||
|
</preConditions>
|
||||||
|
<createIndex tableName="m_clerk_disbursement_request" indexName="idx_disb_gl_transaction_id">
|
||||||
|
<column name="gl_transaction_id"/>
|
||||||
|
</createIndex>
|
||||||
|
</changeSet>
|
||||||
|
|
||||||
|
</databaseChangeLog>
|
||||||
Reference in New Issue
Block a user