Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Lock transactions after reconcilliation #1789

Merged
merged 40 commits into from
Nov 22, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
40 commits
Select commit Hold shift + click to select a range
84eb1e9
Initial work for locking transactions.
zachwhelchel Sep 18, 2023
3094f66
Changed language for test.
zachwhelchel Oct 3, 2023
c6bb9a2
Merge branch 'actualbudget:master' into master
zachwhelchel Oct 11, 2023
9accd18
Merge branch 'zach/lock-transactions' into master
zachwhelchel Oct 11, 2023
5040c80
Merge pull request #1 from zachwhelchel/master
zachwhelchel Oct 11, 2023
55fd5e6
Locked transactions on web working.
zachwhelchel Oct 12, 2023
171d21a
Locked transactions working on mobile.
zachwhelchel Oct 12, 2023
690e934
Cleaned up confirm language for various cases when editing reconciled…
zachwhelchel Oct 12, 2023
44590f7
Scope calls to correct account.
zachwhelchel Oct 12, 2023
da7af9d
Clean up for locked transactions PR.
zachwhelchel Oct 12, 2023
897bb55
Release notes for locked transactions PR.
zachwhelchel Oct 12, 2023
f3f75f4
Fixed spelling.
zachwhelchel Oct 12, 2023
4b52d53
Cleanup for lint and typecheck.
zachwhelchel Oct 16, 2023
76e70d1
Updated snapshots for tests.
zachwhelchel Oct 17, 2023
7f372de
Fix for enabled/disabled checkbox input.
zachwhelchel Oct 17, 2023
01007c2
Merge branch 'master' into zach/lock-transactions
zachwhelchel Oct 17, 2023
88f385f
Fix for editing individual locked transactions.
zachwhelchel Oct 19, 2023
2d91704
Moved locking to "Done Reconciling" button.
zachwhelchel Oct 24, 2023
3f0c453
Merge branch 'master' into zach/lock-transactions
zachwhelchel Oct 24, 2023
70c5c66
Lint fix.
zachwhelchel Oct 24, 2023
6ac704e
Merge branch 'zach/lock-transactions' of https://github.com/zachwhelc…
zachwhelchel Oct 24, 2023
41d3ead
Updated lock icon to match rest of app.
zachwhelchel Oct 24, 2023
f6a2d37
Reconciliation transaction is no longer locked automatically.
zachwhelchel Oct 24, 2023
5e1e59e
Merge branch 'master' into zach/lock-transactions
zachwhelchel Oct 31, 2023
603fe1d
Apply suggestions from code review
zachwhelchel Nov 9, 2023
b95c431
Code review fixes.
zachwhelchel Nov 9, 2023
970abb3
Merge branch 'zach/lock-transactions' of https://github.com/zachwhelc…
zachwhelchel Nov 9, 2023
8dd87e0
More cleanup from code review.
zachwhelchel Nov 9, 2023
7acde9f
Refactored to remove duplicate code.
zachwhelchel Nov 9, 2023
cb57011
Removed disabled.
zachwhelchel Nov 9, 2023
2a0d634
Merge branch 'master' into zach/lock-transactions
zachwhelchel Nov 9, 2023
2bae5fa
Lint cleanup.
zachwhelchel Nov 9, 2023
2857a32
Used "undefined" to fix logic.
zachwhelchel Nov 20, 2023
c4775d4
Account change now prompts warning. Modal switched to tsx.
zachwhelchel Nov 20, 2023
46ba43e
Merge branch 'master' into zach/lock-transactions
zachwhelchel Nov 20, 2023
8875fd7
Clean up type check.
zachwhelchel Nov 20, 2023
2296a90
Typecheck cleanup for table background.
zachwhelchel Nov 20, 2023
c0b3845
Update packages/desktop-client/src/components/accounts/Account.js
zachwhelchel Nov 22, 2023
9a5805b
Merge branch 'master' into zach/lock-transactions
zachwhelchel Nov 22, 2023
eb0178b
Switched back from reduce.
zachwhelchel Nov 22, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions packages/desktop-client/src/components/Modals.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { type CommonModalProps } from '../types/modals';

import CloseAccount from './modals/CloseAccount';
import ConfirmCategoryDelete from './modals/ConfirmCategoryDelete';
import ConfirmTransactionEdit from './modals/ConfirmTransactionEdit';
import CreateAccount from './modals/CreateAccount';
import CreateEncryptionKey from './modals/CreateEncryptionKey';
import CreateLocalAccount from './modals/CreateLocalAccount';
Expand Down Expand Up @@ -124,6 +125,15 @@ export default function Modals() {
/>
);

case 'confirm-transaction-edit':
return (
<ConfirmTransactionEdit
modalProps={modalProps}
onConfirm={options.onConfirm}
confirmReason={options.confirmReason}
/>
);

case 'load-backup':
return (
<LoadBackup
Expand Down
228 changes: 174 additions & 54 deletions packages/desktop-client/src/components/accounts/Account.js
Original file line number Diff line number Diff line change
Expand Up @@ -693,11 +693,68 @@ class AccountInternal extends PureComponent {
return null;
};

onReconcile = balance => {
lockTransactions = async () => {
this.setState({ workingHard: true });

let { accountId } = this.props;

let { data } = await runQuery(
q('transactions')
.filter({ cleared: true, reconciled: false, account: accountId })
.select('*')
.options({ splits: 'grouped' }),
);
let transactions = ungroupTransactions(data);

let changes = { updated: [] };

transactions.forEach(trans => {
let { diff } = updateTransaction(transactions, {
...trans,
reconciled: true,
});

transactions = applyChanges(diff, transactions);

changes.updated = changes.updated
? changes.updated.concat(diff.updated)
: diff.updated;
});

await send('transactions-batch-update', changes);
await this.refetchTransactions();
};

onReconcile = async balance => {
this.setState({ reconcileAmount: balance });
};

onDoneReconciling = () => {
onDoneReconciling = async () => {
let { accountId } = this.props;
let { reconcileAmount } = this.state;

let { data } = await runQuery(
q('transactions')
.filter({ cleared: true, account: accountId })
.select('*')
.options({ splits: 'grouped' }),
);
let transactions = ungroupTransactions(data);

let cleared = 0;

transactions.forEach(trans => {
if (!trans.is_parent) {
cleared += trans.amount;
}
});

let targetDiff = reconcileAmount - cleared;

if (targetDiff === 0) {
await this.lockTransactions();
}

this.setState({ reconcileAmount: null });
};

Expand All @@ -708,6 +765,7 @@ class AccountInternal extends PureComponent {
id: 'temp',
account: this.props.accountId,
cleared: true,
reconciled: false,
amount: diff,
date: currentDay(),
notes: 'Reconciliation balance adjustment',
Expand All @@ -716,7 +774,7 @@ class AccountInternal extends PureComponent {

// Optimistic UI: update the transaction list before sending the data to the database
this.setState({
transactions: [...this.state.transactions, ...reconciliationTransactions],
transactions: [...reconciliationTransactions, ...this.state.transactions],
});

// sync the reconciliation transaction
Expand Down Expand Up @@ -756,6 +814,12 @@ class AccountInternal extends PureComponent {
const idSet = new Set(ids);

transactions.forEach(trans => {
if (name === 'cleared' && trans.reconciled) {
// Skip transactions that are reconciled. Don't want to set them as
// uncleared.
return;
}

if (!idSet.has(trans.id)) {
// Skip transactions which aren't actually selected, since the query
// above also retrieves the siblings & parent of any selected splits.
Expand Down Expand Up @@ -792,6 +856,26 @@ class AccountInternal extends PureComponent {
}
};

if (name === 'amount' || name === 'payee' || name === 'account') {
let { data } = await runQuery(
q('transactions')
.filter({ id: { $oneof: ids }, reconciled: true })
.select('*')
.options({ splits: 'grouped' }),
);
let transactions = ungroupTransactions(data);

if (transactions.length > 0) {
this.props.pushModal('confirm-transaction-edit', {
onConfirm: () => {
this.props.pushModal('edit-field', { name, onSubmit: onChange });
},
confirmReason: 'batchEditWithReconciled',
});
return;
}
}

if (name === 'cleared') {
// Cleared just toggles it on/off and it depends on the data
// loaded. Need to clean this up in the future.
Expand All @@ -802,72 +886,108 @@ class AccountInternal extends PureComponent {
};

onBatchDuplicate = async ids => {
this.setState({ workingHard: true });
let onConfirmDuplicate = async ids => {
this.setState({ workingHard: true });

let { data } = await runQuery(
q('transactions')
.filter({ id: { $oneof: ids } })
.select('*')
.options({ splits: 'grouped' }),
);
let { data } = await runQuery(
q('transactions')
.filter({ id: { $oneof: ids } })
.select('*')
.options({ splits: 'grouped' }),
);

let changes = {
added: data
.reduce((newTransactions, trans) => {
return newTransactions.concat(
realizeTempTransactions(ungroupTransaction(trans)),
);
}, [])
.map(({ sort_order, ...trans }) => ({ ...trans })),
};
let changes = {
added: data
.reduce((newTransactions, trans) => {
return newTransactions.concat(
realizeTempTransactions(ungroupTransaction(trans)),
);
}, [])
.map(({ sort_order, ...trans }) => ({ ...trans })),
};

await send('transactions-batch-update', changes);
await send('transactions-batch-update', changes);

await this.refetchTransactions();
await this.refetchTransactions();
};

await this.checkForReconciledTransactions(
ids,
'batchDuplicateWithReconciled',
onConfirmDuplicate,
);
};

onBatchDelete = async ids => {
this.setState({ workingHard: true });
let onConfirmDelete = async ids => {
this.setState({ workingHard: true });

let { data } = await runQuery(
q('transactions')
.filter({ id: { $oneof: ids } })
.select('*')
.options({ splits: 'grouped' }),
);
let transactions = ungroupTransactions(data);
let { data } = await runQuery(
q('transactions')
.filter({ id: { $oneof: ids } })
.select('*')
.options({ splits: 'grouped' }),
);
let transactions = ungroupTransactions(data);

let idSet = new Set(ids);
let changes = { deleted: [], updated: [] };
let idSet = new Set(ids);
let changes = { deleted: [], updated: [] };

transactions.forEach(trans => {
let parentId = trans.parent_id;
transactions.forEach(trans => {
let parentId = trans.parent_id;

// First, check if we're actually deleting this transaction by
// checking `idSet`. Then, we don't need to do anything if it's
// a child transaction and the parent is already being deleted
if (!idSet.has(trans.id) || (parentId && idSet.has(parentId))) {
return;
}
// First, check if we're actually deleting this transaction by
// checking `idSet`. Then, we don't need to do anything if it's
// a child transaction and the parent is already being deleted
if (!idSet.has(trans.id) || (parentId && idSet.has(parentId))) {
return;
}

let { diff } = deleteTransaction(transactions, trans.id);
let { diff } = deleteTransaction(transactions, trans.id);

// TODO: We need to keep an updated list of transactions so
// the logic in `updateTransaction`, particularly about
// updating split transactions, works. This isn't ideal and we
// should figure something else out
transactions = applyChanges(diff, transactions);
// TODO: We need to keep an updated list of transactions so
// the logic in `updateTransaction`, particularly about
// updating split transactions, works. This isn't ideal and we
// should figure something else out
transactions = applyChanges(diff, transactions);

changes.deleted = diff.deleted
? changes.deleted.concat(diff.deleted)
: diff.deleted;
changes.updated = diff.updated
? changes.updated.concat(diff.updated)
: diff.updated;
});
changes.deleted = diff.deleted
? changes.deleted.concat(diff.deleted)
: diff.deleted;
changes.updated = diff.updated
? changes.updated.concat(diff.updated)
: diff.updated;
});

await send('transactions-batch-update', changes);
await this.refetchTransactions();
await send('transactions-batch-update', changes);
await this.refetchTransactions();
};

await this.checkForReconciledTransactions(
ids,
'batchDeleteWithReconciled',
onConfirmDelete,
);
};

checkForReconciledTransactions = async (ids, confirmReason, onConfirm) => {
let { data } = await runQuery(
q('transactions')
.filter({ id: { $oneof: ids }, reconciled: true })
.select('*')
.options({ splits: 'grouped' }),
);
let transactions = ungroupTransactions(data);
if (transactions.length > 0) {
this.props.pushModal('confirm-transaction-edit', {
onConfirm: () => {
onConfirm(ids);
},
confirmReason: confirmReason,
});
} else {
onConfirm(ids);
}
};

onBatchUnlink = async ids => {
Expand Down
3 changes: 2 additions & 1 deletion packages/desktop-client/src/components/mobile/MobileForms.js
Original file line number Diff line number Diff line change
Expand Up @@ -111,9 +111,10 @@ export function TapField({
);
}

export function BooleanField({ checked, onUpdate, style }) {
export function BooleanField({ checked, onUpdate, style, disabled = false }) {
return (
<input
disabled={disabled ? true : undefined}
type="checkbox"
checked={checked}
onChange={e => onUpdate(e.target.checked)}
Expand Down
Loading