diff --git a/packages/desktop-client/e2e/data/ynab5-demo-budget.json b/packages/desktop-client/e2e/data/ynab5-demo-budget.json index 2f31164d64c..6dbe3ff5bfe 100644 --- a/packages/desktop-client/e2e/data/ynab5-demo-budget.json +++ b/packages/desktop-client/e2e/data/ynab5-demo-budget.json @@ -1671,9 +1671,70 @@ "import_payee_name_original": null, "debt_transaction_type": null, "deleted": false + }, + { + "id": "213526fc-ba49-4790-8a96-cc2a50182728", + "date": "2023-09-04", + "amount": -100000, + "memo": "Test transaction", + "cleared": "cleared", + "approved": true, + "flag_color": null, + "account_id": "bc1d862f-bab0-41c3-bd1e-6cee8c688e32", + "payee_id": "2a20470a-634f-4efa-a7f6-f1c0b0bdda41", + "category_id": "36120d44-6c61-4402-985a-891a8d267858", + "transfer_account_id": null, + "transfer_transaction_id": null, + "matched_transaction_id": null, + "import_id": null, + "import_payee_name": null, + "import_payee_name_original": null, + "debt_transaction_type": null, + "deleted": false + }, + { + "id": "024494a1-f1e0-4667-9fc0-91e4a4262193", + "date": "2023-09-04", + "amount": 50000, + "memo": "split part b", + "cleared": "cleared", + "approved": true, + "flag_color": null, + "account_id": "125f339b-2a63-481e-84c0-f04d898905d2", + "payee_id": "", + "category_id": null, + "transfer_account_id": "bc1d862f-bab0-41c3-bd1e-6cee8c688e32", + "transfer_transaction_id": "213526fc-ba49-4790-8a96-cc2a50182728", + "matched_transaction_id": "", + "import_id": null, + "import_payee_name": null, + "import_payee_name_original": null, + "debt_transaction_type": null, + "deleted": false + } + ], + "subtransactions": [ + { + "id": "d8ec8c84-5033-4f7e-8485-66bfe19a70d6", + "transaction_id": "213526fc-ba49-4790-8a96-cc2a50182728", + "amount": -50000, + "memo": "split part a", + "payee_id": "2a20470a-634f-4efa-a7f6-f1c0b0bdda41", + "category_id": "36120d44-6c61-4402-985a-891a8d267858", + "transfer_account_id": null, + "deleted": false + }, + { + "id": "870d8780-79cf-4197-a341-47d24b2b5a59", + "transaction_id": "213526fc-ba49-4790-8a96-cc2a50182728", + "amount": -50000, + "memo": "split part b", + "payee_id": "2a20470a-634f-4efa-a7f6-f1c0b0bdda41", + "category_id": null, + "transfer_account_id": "125f339b-2a63-481e-84c0-f04d898905d2", + "deleted": false } ], - "subtransactions": [], "scheduled_transactions": [], "scheduled_subtransactions": [] }, diff --git a/packages/desktop-client/e2e/onboarding.test.js b/packages/desktop-client/e2e/onboarding.test.js index c748ad93523..98c5b28c083 100644 --- a/packages/desktop-client/e2e/onboarding.test.js +++ b/packages/desktop-client/e2e/onboarding.test.js @@ -60,10 +60,10 @@ test.describe('Onboarding', () => { await expect(budgetPage.budgetTable).toBeVisible({ timeout: 30000 }); const accountPage = await navigation.goToAccountPage('Checking'); - await expect(accountPage.accountBalance).toHaveText('700.00'); + await expect(accountPage.accountBalance).toHaveText('600.00'); await navigation.goToAccountPage('Saving'); - await expect(accountPage.accountBalance).toHaveText('200.00'); + await expect(accountPage.accountBalance).toHaveText('250.00'); }); test('creates a new budget file by importing Actual budget', async () => { diff --git a/packages/desktop-client/src/components/modals/ImportTransactions.js b/packages/desktop-client/src/components/modals/ImportTransactions.js index e9f27f5c80a..2417553515b 100644 --- a/packages/desktop-client/src/components/modals/ImportTransactions.js +++ b/packages/desktop-client/src/components/modals/ImportTransactions.js @@ -193,11 +193,22 @@ function getInitialMappings(transactions) { ), ); + let inOutField = key( + fields.find( + ([name, value]) => + name !== dateField && + name !== amountField && + name !== payeeField && + name !== notesField, + ), + ); + return { date: dateField, amount: amountField, payee: payeeField, notes: notesField, + inOut: inOutField, }; } @@ -222,7 +233,14 @@ function parseAmount(amount, mapper) { return value; } -function parseAmountFields(trans, splitMode, flipAmount, multiplierAmount) { +function parseAmountFields( + trans, + splitMode, + inOutMode, + outValue, + flipAmount, + multiplierAmount, +) { const multiplier = parseFloat(multiplierAmount) || 1.0; if (splitMode) { @@ -240,6 +258,16 @@ function parseAmountFields(trans, splitMode, flipAmount, multiplierAmount) { inflow, }; } + if (inOutMode) { + return { + amount: + parseAmount(trans.amount, n => + trans.inOut === outValue ? Math.abs(n) * -1 : Math.abs(n), + ) * multiplier, + outflow: null, + inflow: null, + }; + } return { amount: parseAmount(trans.amount, n => (flipAmount ? n * -1 : n)) * multiplier, @@ -255,6 +283,8 @@ function Transaction({ parseDateFormat, dateFormat, splitMode, + inOutMode, + outValue, flipAmount, multiplierAmount, }) { @@ -269,6 +299,8 @@ function Transaction({ let { amount, outflow, inflow } = parseAmountFields( transaction, splitMode, + inOutMode, + outValue, flipAmount, multiplierAmount, ); @@ -320,13 +352,24 @@ function Transaction({ ) : ( - - {amount} - + <> + {inOutMode && ( + + {transaction.inOut} + + )} + + {amount} + + )} ); @@ -457,11 +500,43 @@ function MultiplierOption({ ); } +function InOutOption({ + inOutMode, + outValue, + disabled, + onToggle, + onChangeText, +}) { + return ( + + + {inOutMode + ? 'in/out identifier' + : 'Select column to specify if amount goes in/out'} + + {inOutMode && ( + + )} + + ); +} + function FieldMappings({ transactions, mappings, onChange, splitMode, + inOutMode, hasHeaderRow, }) { if (transactions.length === 0) { @@ -537,16 +612,30 @@ function FieldMappings({ ) : ( - - - onChange('amount', name)} - hasHeaderRow={hasHeaderRow} - firstTransaction={transactions[0]} - /> - + <> + {inOutMode && ( + + + onChange('inOut', name)} + hasHeaderRow={hasHeaderRow} + firstTransaction={transactions[0]} + /> + + )} + + + onChange('amount', name)} + hasHeaderRow={hasHeaderRow} + firstTransaction={transactions[0]} + /> + + )} @@ -569,6 +658,8 @@ export default function ImportTransactions({ modalProps, options }) { let [filetype, setFileType] = useState(null); let [fieldMappings, setFieldMappings] = useState(null); let [splitMode, setSplitMode] = useState(false); + let [inOutMode, setInOutMode] = useState(false); + let [outValue, setOutValue] = useState(''); let [flipAmount, setFlipAmount] = useState(false); let [multiplierEnabled, setMultiplierEnabled] = useState(false); let { accountId, onImported } = options; @@ -684,6 +775,8 @@ export default function ImportTransactions({ modalProps, options }) { let isSplit = !splitMode; setSplitMode(isSplit); + setInOutMode(false); + setFlipAmount(false); // Run auto-detection on the fields to try to detect the fields // automatically @@ -749,6 +842,8 @@ export default function ImportTransactions({ modalProps, options }) { let { amount } = parseAmountFields( trans, splitMode, + inOutMode, + outValue, flipAmount, multiplierAmount, ); @@ -757,7 +852,7 @@ export default function ImportTransactions({ modalProps, options }) { break; } - let { inflow, outflow, ...finalTransaction } = trans; + let { inflow, outflow, inOut, ...finalTransaction } = trans; finalTransactions.push({ ...finalTransaction, date, @@ -812,6 +907,9 @@ export default function ImportTransactions({ modalProps, options }) { { name: 'Notes', width: 'flex' }, ]; + if (inOutMode) { + headers.push({ name: 'In/Out', width: 90, style: { textAlign: 'left' } }); + } if (splitMode) { headers.push({ name: 'Outflow', width: 90, style: { textAlign: 'right' } }); headers.push({ name: 'Inflow', width: 90, style: { textAlign: 'right' } }); @@ -873,6 +971,8 @@ export default function ImportTransactions({ modalProps, options }) { dateFormat={dateFormat} fieldMappings={fieldMappings} splitMode={splitMode} + inOutMode={inOutMode} + outValue={outValue} flipAmount={flipAmount} multiplierAmount={multiplierAmount} /> @@ -905,6 +1005,7 @@ export default function ImportTransactions({ modalProps, options }) { onChange={onUpdateFields} mappings={fieldMappings} splitMode={splitMode} + inOutMode={inOutMode} hasHeaderRow={hasHeaderRow} /> @@ -1018,19 +1119,29 @@ export default function ImportTransactions({ modalProps, options }) { setFlipAmount(!flipAmount)} > Flip amount {filetype === 'csv' && ( - - Split amount into separate inflow/outflow columns - + <> + + Split amount into separate inflow/outflow columns + + setInOutMode(!inOutMode)} + onChangeText={setOutValue} + /> + )} payee?.transfer_acct) + .map((payee: YNAB5.Payee) => [payee.transfer_acct, payee]); + const payeeTransferAcctHashMap = new Map( + payeesByTransferAcct, + ); + // Go ahead and generate ids for all of the transactions so we can // reliably resolve transfers for (let transaction of data.transactions) { @@ -178,11 +185,22 @@ async function importTransactions( entityIdMap.get(transaction.transfer_transaction_id) || null, subtransactions: subtransactions ? subtransactions.map(subtrans => { + let payee = null; + if (subtrans.transfer_account_id) { + const mappedTransferAccountId = entityIdMap.get( + subtrans.transfer_account_id, + ); + payee = payeeTransferAcctHashMap.get( + mappedTransferAccountId, + )?.id; + } + return { id: entityIdMap.get(subtrans.id), amount: amountFromYnab(subtrans.amount), category: entityIdMap.get(subtrans.category_id) || null, notes: subtrans.memo, + payee, }; }) : null, diff --git a/upcoming-release-notes/1788.md b/upcoming-release-notes/1788.md new file mode 100644 index 00000000000..040114759db --- /dev/null +++ b/upcoming-release-notes/1788.md @@ -0,0 +1,6 @@ +--- +category: Enhancements +authors: [Jessseee] +--- + +Added option to select in/out field during import. diff --git a/upcoming-release-notes/1836.md b/upcoming-release-notes/1836.md new file mode 100644 index 00000000000..e6c6ff8735f --- /dev/null +++ b/upcoming-release-notes/1836.md @@ -0,0 +1,6 @@ +--- +category: Bugfix +authors: [Marethyu1] +--- + +UPDATES NYNAB import to support importing transactions that contain sub transactions that are account transfers