-- Removes the duplicate vouchers left behind by the double-count bug. -- -- Until 2026-09-03 the cashier deposit and clerk withdrawal registers did not wrap their inner -- savings command in VoucherService.withOwnVoucher(...), so one counter transaction raised two -- vouchers over the same money: the register's own (CASHIER_DEPOSIT_REQUEST / CLERK_WITHDRAWAL_ -- REQUEST) and a second one from the savings command (SAVINGS_TRANSACTION). The cashier's Received -- and Payment reports counted both, doubling the day's cash. -- -- The code is fixed, so no new duplicates are created. This clears the ones already recorded. -- -- WHICH ONE IS DELETED: the SAVINGS_TRANSACTION voucher. The register's voucher is the keeper -- -- it is the one the register owns, attaches its DR/CR lines to, and stamps onto -- m_savings_account_transaction.voucher_no. Deleting the register's voucher instead would break -- that link and orphan the request row. -- -- Run: psql -h localhost -U postgres -d fineract_default -v ON_ERROR_STOP=1 -f cleanup_duplicate_vouchers.sql BEGIN; -- The duplicates: a SAVINGS_TRANSACTION voucher whose transaction is already claimed by a -- register voucher. Matching through savings_txn_id rather than by date or amount so that two -- genuinely separate transactions of the same value on the same day are never confused. CREATE TEMP TABLE dup_vouchers ON COMMIT DROP AS SELECT sav.voucher_no FROM m_voucher sav JOIN m_voucher reg ON reg.savings_txn_id = sav.reference_id AND reg.reference_type IN ('CLERK_WITHDRAWAL_REQUEST', 'CASHIER_DEPOSIT_REQUEST') WHERE sav.reference_type = 'SAVINGS_TRANSACTION' AND sav.voucher_type IN ('DEPOSIT', 'WITHDRAWAL'); -- Refuse to run if anything else has attached itself to a voucher about to be deleted: a -- verification or an attachment means someone acted on that slip, and it must be looked at by hand -- rather than removed silently. DO $$ DECLARE blocked INT; BEGIN SELECT COUNT(*) INTO blocked FROM (SELECT voucher_no FROM m_voucher_verification WHERE voucher_no IN (SELECT voucher_no FROM dup_vouchers) UNION ALL SELECT voucher_no FROM m_voucher_attachment WHERE voucher_no IN (SELECT voucher_no FROM dup_vouchers)) x; IF blocked > 0 THEN RAISE EXCEPTION 'Aborting: % duplicate voucher(s) carry a verification or attachment. Review these by hand.', blocked; END IF; END $$; -- Likewise refuse if a savings transaction is stamped with a voucher about to be deleted. It should -- carry the register's number, not the duplicate's; if it does not, the pair needs looking at. DO $$ DECLARE stamped INT; BEGIN SELECT COUNT(*) INTO stamped FROM m_savings_account_transaction WHERE voucher_no IN (SELECT voucher_no FROM dup_vouchers); IF stamped > 0 THEN RAISE EXCEPTION 'Aborting: % savings transaction(s) are stamped with a duplicate voucher number.', stamped; END IF; END $$; SELECT voucher_no AS "about to delete" FROM dup_vouchers ORDER BY voucher_no; DELETE FROM m_voucher_line WHERE voucher_no IN (SELECT voucher_no FROM dup_vouchers); DELETE FROM m_voucher WHERE voucher_no IN (SELECT voucher_no FROM dup_vouchers); COMMIT;