From de69e1582c16ce0254ce178ba12330057425edf8 Mon Sep 17 00:00:00 2001
From: Neil <55785687+carkom@users.noreply.github.com>
Date: Wed, 1 Mar 2023 19:49:48 +0000
Subject: [PATCH] Added option to include exchange rate multiplier during
 import (#690)

* Addd option to include exchange rate during import

* Added changes to multipler option within form

* Use Input component

* build fixes

* Fix parsing errors

* lint fixes

* Update packages/loot-design/src/components/modals/ImportTransactions.js

Co-authored-by: Matiss Janis Aboltins <matiss@mja.lv>

* Update packages/loot-design/src/components/modals/ImportTransactions.js

Co-authored-by: Matiss Janis Aboltins <matiss@mja.lv>

* Update packages/loot-design/src/components/modals/ImportTransactions.js

Co-authored-by: Matiss Janis Aboltins <matiss@mja.lv>

* Variable name updates

* Record a trace when retrying failing tests

---------

Co-authored-by: Matiss Janis Aboltins <matiss@mja.lv>
Co-authored-by: Jed Fox <git@jedfox.com>
---
 packages/desktop-client/playwright.config.js  |   5 +-
 .../components/modals/ImportTransactions.js   | 184 +++++++++++++-----
 2 files changed, 141 insertions(+), 48 deletions(-)

diff --git a/packages/desktop-client/playwright.config.js b/packages/desktop-client/playwright.config.js
index d8647df28..fd4d84f00 100644
--- a/packages/desktop-client/playwright.config.js
+++ b/packages/desktop-client/playwright.config.js
@@ -4,6 +4,7 @@ module.exports = {
   use: {
     screenshot: 'on',
     browserName: 'chromium',
-    baseURL: process.env.E2E_START_URL ?? 'http://localhost:3001'
-  }
+    baseURL: process.env.E2E_START_URL ?? 'http://localhost:3001',
+    trace: 'on-first-retry',
+  },
 };
diff --git a/packages/loot-design/src/components/modals/ImportTransactions.js b/packages/loot-design/src/components/modals/ImportTransactions.js
index 73bd6c47d..78680d981 100644
--- a/packages/loot-design/src/components/modals/ImportTransactions.js
+++ b/packages/loot-design/src/components/modals/ImportTransactions.js
@@ -18,6 +18,7 @@ import {
   Stack,
   Modal,
   Select,
+  Input,
   Button,
   ButtonWithLoading,
 } from '../common';
@@ -218,13 +219,17 @@ function parseAmount(amount, mapper) {
   return value;
 }
 
-function parseAmountFields(trans, splitMode, flipAmount) {
+function parseAmountFields(trans, splitMode, flipAmount, multiplierAmount) {
+  const multiplier = parseFloat(multiplierAmount) || 1.0;
+
   if (splitMode) {
     // Split mode is a little weird; first we look for an outflow and
     // if that has a value, we never want to show a number in the
     // inflow. Same for `amount`; we choose outflow first and then inflow
-    let outflow = parseAmount(trans.outflow, n => -Math.abs(n));
-    let inflow = outflow ? 0 : parseAmount(trans.inflow, n => Math.abs(n));
+    let outflow = parseAmount(trans.outflow, n => -Math.abs(n)) * multiplier;
+    let inflow = outflow
+      ? 0
+      : parseAmount(trans.inflow, n => Math.abs(n)) * multiplier;
 
     return {
       amount: outflow || inflow,
@@ -233,7 +238,8 @@ function parseAmountFields(trans, splitMode, flipAmount) {
     };
   }
   return {
-    amount: parseAmount(trans.amount, n => (flipAmount ? n * -1 : n)),
+    amount:
+      parseAmount(trans.amount, n => (flipAmount ? n * -1 : n)) * multiplier,
     outflow: null,
     inflow: null,
   };
@@ -247,6 +253,7 @@ function Transaction({
   dateFormat,
   splitMode,
   flipAmount,
+  multiplierAmount,
 }) {
   let transaction = useMemo(
     () =>
@@ -260,6 +267,7 @@ function Transaction({
     transaction,
     splitMode,
     flipAmount,
+    multiplierAmount,
   );
   amount = amountToCurrency(amount);
   outflow = amountToCurrency(outflow);
@@ -380,6 +388,22 @@ function DateFormatSelect({
   );
 }
 
+function MultipliersOption({ value, onChange }) {
+  return (
+    <View
+      style={{
+        flex: 1,
+        flexDirection: 'row',
+        alignItems: 'center',
+        userSelect: 'none',
+      }}
+    >
+      <Checkbox id="add_multiplier" checked={value} onChange={onChange} />
+      <label htmlFor="add_multiplier">Add Multiplier</label>
+    </View>
+  );
+}
+
 function FlipAmountOption({ value, disabled, onChange }) {
   return (
     <View
@@ -508,6 +532,20 @@ function FieldMappings({ transactions, mappings, onChange, splitMode }) {
   );
 }
 
+function MultipliersField({ multiplierCB, value, onChange }) {
+  const styl = multiplierCB ? 'inherit' : 'none';
+
+  return (
+    <Input
+      type="text"
+      style={{ display: styl }}
+      value={value}
+      placeholder="Optional"
+      onUpdate={onChange}
+    />
+  );
+}
+
 export function ImportTransactions({
   modalProps,
   options,
@@ -518,6 +556,7 @@ export function ImportTransactions({
   getPayees,
   savePrefs,
 }) {
+  let [multiplierAmount, setMultiplierAmount] = useState('');
   let [loadingState, setLoadingState] = useState('parsing');
   let [error, setError] = useState(null);
   let [filename, setFilename] = useState(options.filename);
@@ -526,6 +565,7 @@ export function ImportTransactions({
   let [fieldMappings, setFieldMappings] = useState(null);
   let [splitMode, setSplitMode] = useState(false);
   let [flipAmount, setFlipAmount] = useState(false);
+  let [multiplierEnabled, setMultiplierEnabled] = useState(false);
   let { accountId, onImported } = options;
 
   // This cannot be set after parsing the file, because changing it
@@ -594,6 +634,13 @@ export function ImportTransactions({
     }
   }
 
+  function onMultiplierChange(e) {
+    const amt = e;
+    if (!amt || amt.match(/^\d{1,}(\.\d{0,4})?$/)) {
+      setMultiplierAmount(amt);
+    }
+  }
+
   useEffect(() => {
     parse(
       options.filename,
@@ -608,6 +655,10 @@ export function ImportTransactions({
       return;
     }
 
+    if (flipAmount === true) {
+      setFlipAmount(!flipAmount);
+    }
+
     let isSplit = !splitMode;
     setSplitMode(isSplit);
 
@@ -666,7 +717,12 @@ export function ImportTransactions({
         break;
       }
 
-      let { amount } = parseAmountFields(trans, splitMode, flipAmount);
+      let { amount } = parseAmountFields(
+        trans,
+        splitMode,
+        flipAmount,
+        multiplierAmount,
+      );
       if (amount == null) {
         errorMessage = `Transaction on ${trans.date} has no amount`;
         break;
@@ -782,6 +838,7 @@ export function ImportTransactions({
                   fieldMappings={fieldMappings}
                   splitMode={splitMode}
                   flipAmount={flipAmount}
+                  multiplierAmount={multiplierAmount}
                 />
               </View>
             )}
@@ -816,54 +873,89 @@ export function ImportTransactions({
         </View>
       )}
 
+      {/*Import Options */}
       {(filetype === 'qif' || filetype === 'csv') && (
         <View style={{ marginTop: 25 }}>
-          <SectionLabel title="IMPORT OPTIONS" />
-          <View style={{ marginTop: 5 }}>
-            <FlipAmountOption
-              value={flipAmount}
-              disabled={splitMode}
-              onChange={() => {
-                setFlipAmount(!flipAmount);
-              }}
-            />
-          </View>
-          {filetype === 'csv' && (
-            <View style={{ marginTop: 10 }}>
-              <SplitOption value={splitMode} onChange={onSplitMode} />
+          <Stack
+            direction="row"
+            align="flex-start"
+            spacing={1}
+            style={{ marginTop: 5 }}
+          >
+            {/*Date Format */}
+            <View>
+              {(filetype === 'qif' || filetype === 'csv') && (
+                <DateFormatSelect
+                  transactions={transactions}
+                  fieldMappings={fieldMappings}
+                  parseDateFormat={parseDateFormat}
+                  onChange={setParseDateFormat}
+                />
+              )}
             </View>
-          )}
-        </View>
-      )}
 
-      <View style={{ flexDirection: 'row', marginTop: 25 }}>
-        {(filetype === 'qif' || filetype === 'csv') && (
-          <DateFormatSelect
-            transactions={transactions}
-            fieldMappings={fieldMappings}
-            parseDateFormat={parseDateFormat}
-            onChange={setParseDateFormat}
-          />
-        )}
+            {/*csv Delimiter */}
+            <View>
+              {filetype === 'csv' && (
+                <View style={{ marginLeft: 25 }}>
+                  <SectionLabel title="CSV DELIMITER" />
+                  <Select
+                    value={csvDelimiter}
+                    onChange={e => {
+                      setCsvDelimiter(e.target.value);
+                      parse(filename, { delimiter: e.target.value });
+                    }}
+                  >
+                    <option value=",">,</option>
+                    <option value=";">;</option>
+                  </Select>
+                </View>
+              )}
+            </View>
 
-        {filetype === 'csv' && (
-          <View style={{ marginLeft: 25 }}>
-            <SectionLabel title="CSV DELIMITER" />
-            <Select
-              value={csvDelimiter}
-              onChange={e => {
-                setCsvDelimiter(e.target.value);
-                parse(filename, { delimiter: e.target.value });
-              }}
-            >
-              <option value=",">,</option>
-              <option value=";">;</option>
-            </Select>
-          </View>
-        )}
+            <View style={{ flex: 1 }} />
 
-        <View style={{ flex: 1 }} />
+            <View style={{ marginRight: 25 }}>
+              <SectionLabel title="IMPORT OPTIONS" />
+              <View style={{ marginTop: 5 }}>
+                <FlipAmountOption
+                  value={flipAmount}
+                  disabled={splitMode}
+                  onChange={() => {
+                    setFlipAmount(!flipAmount);
+                  }}
+                />
+              </View>
+              {filetype === 'csv' && (
+                <View style={{ marginTop: 10 }}>
+                  <SplitOption value={splitMode} onChange={onSplitMode} />
+                </View>
+              )}
+              <View style={{ flexDirection: 'row', marginTop: 10 }}>
+                <View style={{ marginRight: 30 }}>
+                  <MultipliersOption
+                    value={multiplierEnabled}
+                    onChange={() => {
+                      setMultiplierEnabled(!multiplierEnabled);
+                      setMultiplierAmount('');
+                    }}
+                  />
+                </View>
+                <View style={{ width: 75 }}>
+                  <MultipliersField
+                    multiplierCB={multiplierEnabled}
+                    value={multiplierAmount}
+                    onChange={onMultiplierChange}
+                  />
+                </View>
+              </View>
+            </View>
+          </Stack>
+        </View>
+      )}
 
+      <View style={{ flexDirection: 'row', marginTop: 5 }}>
+        {/*Submit Button */}
         <View
           style={{
             alignSelf: 'flex-end',
-- 
GitLab