]> git.ktnx.net Git - mobile-ledger.git/commitdiff
support multiple currencies in new transaction activity
authorDamyan Ivanov <dam+mobileledger@ktnx.net>
Sun, 5 Apr 2020 20:36:50 +0000 (23:36 +0300)
committerDamyan Ivanov <dam+mobileledger@ktnx.net>
Sun, 5 Apr 2020 20:36:50 +0000 (23:36 +0300)
with mandatory empty rows for easy entering and ensuring transaction balance

this doesn't allow hidden commodity conversion, explicit one should be
used, possibly with dedicated FX accounts

app/src/main/java/net/ktnx/mobileledger/ui/activity/NewTransactionFragment.java
app/src/main/java/net/ktnx/mobileledger/ui/activity/NewTransactionItemHolder.java
app/src/main/java/net/ktnx/mobileledger/ui/activity/NewTransactionModel.java

index 55b0e6106c91ac3d0b22337df5def7f8072522d3..f9ea5c2a360df48c47f6d1a6c5c91a502385fda3 100644 (file)
@@ -128,7 +128,7 @@ public class NewTransactionFragment extends Fragment {
                          }
                      }
                  });
                          }
                      }
                  });
-        viewModel.checkTransactionSubmittable(listAdapter);
+//        viewModel.checkTransactionSubmittable(listAdapter);
 
         fab = activity.findViewById(R.id.fab);
         fab.setOnClickListener(v -> onFabPressed());
 
         fab = activity.findViewById(R.id.fab);
         fab.setOnClickListener(v -> onFabPressed());
index 53e97e826c0092cb23ec1eb4136b69a6944bdcd5..a72de5d09d12cc5b9ccb1d36aab98e91ab05fa90 100644 (file)
@@ -95,9 +95,11 @@ class NewTransactionItemHolder extends RecyclerView.ViewHolder
     private boolean inUpdate = false;
     private boolean syncingData = false;
     private View commentButton;
     private boolean inUpdate = false;
     private boolean syncingData = false;
     private View commentButton;
+    private NewTransactionItemsAdapter adapter;
     //TODO multiple amounts with different currencies per posting
     NewTransactionItemHolder(@NonNull View itemView, NewTransactionItemsAdapter adapter) {
         super(itemView);
     //TODO multiple amounts with different currencies per posting
     NewTransactionItemHolder(@NonNull View itemView, NewTransactionItemsAdapter adapter) {
         super(itemView);
+        this.adapter = adapter;
         tvAccount = itemView.findViewById(R.id.account_row_acc_name);
         tvComment = itemView.findViewById(R.id.comment);
         new TextViewClearHelper().attachToTextView((EditText) tvComment);
         tvAccount = itemView.findViewById(R.id.account_row_acc_name);
         tvComment = itemView.findViewById(R.id.comment);
         new TextViewClearHelper().attachToTextView((EditText) tvComment);
@@ -195,11 +197,7 @@ class NewTransactionItemHolder extends RecyclerView.ViewHolder
                                 start, count, after));
             }
             @Override
                                 start, count, after));
             }
             @Override
-            public void onTextChanged(CharSequence s, int start, int before, int count) {
-                Logger.debug("num",
-                        String.format(Locale.US, "onTextChanged: start=%d, before=%d, count=%d",
-                                start, before, count));
-            }
+            public void onTextChanged(CharSequence s, int start, int before, int count) {}
             @Override
             public void afterTextChanged(Editable s) {
                 if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
             @Override
             public void afterTextChanged(Editable s) {
                 if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
@@ -207,16 +205,17 @@ class NewTransactionItemHolder extends RecyclerView.ViewHolder
                     // plus and minus are allowed only at the beginning
                     String allowed = "0123456789";
                     String val = s.toString();
                     // plus and minus are allowed only at the beginning
                     String allowed = "0123456789";
                     String val = s.toString();
+                    Logger.debug("input", val);
                     if (val.isEmpty() || (tvAmount.getSelectionStart() == 0))
                         allowed += "-";
                     if (!val.contains(decimalSeparator) && !val.contains(decimalDot))
                         allowed += decimalSeparator + decimalDot;
 
                     tvAmount.setKeyListener(DigitsKeyListener.getInstance(allowed));
                     if (val.isEmpty() || (tvAmount.getSelectionStart() == 0))
                         allowed += "-";
                     if (!val.contains(decimalSeparator) && !val.contains(decimalDot))
                         allowed += decimalSeparator + decimalDot;
 
                     tvAmount.setKeyListener(DigitsKeyListener.getInstance(allowed));
+                }
 
 
-                    syncData();
+                if (syncData())
                     adapter.model.checkTransactionSubmittable(adapter);
                     adapter.model.checkTransactionSubmittable(adapter);
-                }
             }
         };
         tvDescription.addTextChangedListener(tw);
             }
         };
         tvDescription.addTextChangedListener(tw);
@@ -340,14 +339,6 @@ class NewTransactionItemHolder extends RecyclerView.ViewHolder
                 tvAmount.setImeOptions(EditorInfo.IME_ACTION_NEXT);
         };
 
                 tvAmount.setImeOptions(EditorInfo.IME_ACTION_NEXT);
         };
 
-        currencyPositionObserver = position -> {
-            updateCurrencyPositionAndPadding(position, Data.currencyGap.getValue());
-        };
-
-        currencyGapObserver = hasGap -> {
-            updateCurrencyPositionAndPadding(Data.currencySymbolPosition.getValue(), hasGap);
-        };
-
         localeObserver = locale -> {
             if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O)
                 tvAmount.setKeyListener(DigitsKeyListener.getInstance(locale, true, true));
         localeObserver = locale -> {
             if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O)
                 tvAmount.setKeyListener(DigitsKeyListener.getInstance(locale, true, true));
@@ -358,15 +349,21 @@ class NewTransactionItemHolder extends RecyclerView.ViewHolder
             adapter.model.checkTransactionSubmittable(adapter);
         };
 
             adapter.model.checkTransactionSubmittable(adapter);
         };
 
+        currencyGapObserver = hasGap -> {
+            updateCurrencyPositionAndPadding(Data.currencySymbolPosition.getValue(), hasGap);
+        };
+
+        currencyPositionObserver = position -> {
+            updateCurrencyPositionAndPadding(position, Data.currencyGap.getValue());
+        };
+
         showCurrencyObserver = showCurrency -> {
         showCurrencyObserver = showCurrency -> {
-            if (item.getType() == ItemType.transactionRow) {
-                if (showCurrency) {
-                    tvCurrency.setVisibility(View.VISIBLE);
-                }
-                else {
-                    tvCurrency.setVisibility(View.GONE);
-                    item.setCurrency(null);
-                }
+            if (showCurrency) {
+                tvCurrency.setVisibility(View.VISIBLE);
+            }
+            else {
+                tvCurrency.setVisibility(View.GONE);
+                item.setCurrency(null);
             }
         };
     }
             }
         };
     }
@@ -475,14 +472,16 @@ class NewTransactionItemHolder extends RecyclerView.ViewHolder
      * syncData()
      * <p>
      * Stores the data from the UI elements into the model item
      * syncData()
      * <p>
      * Stores the data from the UI elements into the model item
+     * Returns true if there were changes made that suggest transaction has to be
+     * checked for being submittable
      */
      */
-    private void syncData() {
+    private boolean syncData() {
         if (item == null)
         if (item == null)
-            return;
+            return false;
 
         if (syncingData) {
             Logger.debug("new-trans", "skipping syncData() loop");
 
         if (syncingData) {
             Logger.debug("new-trans", "skipping syncData() loop");
-            return;
+            return false;
         }
 
         syncingData = true;
         }
 
         syncingData = true;
@@ -499,7 +498,6 @@ class NewTransactionItemHolder extends RecyclerView.ViewHolder
 
                     item.setComment(String.valueOf(tvComment.getText()));
 
 
                     item.setComment(String.valueOf(tvComment.getText()));
 
-                    // TODO: handle multiple amounts
                     String amount = String.valueOf(tvAmount.getText());
                     amount = amount.trim();
 
                     String amount = String.valueOf(tvAmount.getText());
                     amount = amount.trim();
 
@@ -532,6 +530,8 @@ class NewTransactionItemHolder extends RecyclerView.ViewHolder
                 case bottomFiller:
                     throw new RuntimeException("Should not happen");
             }
                 case bottomFiller:
                     throw new RuntimeException("Should not happen");
             }
+
+            return true;
         }
         finally {
             syncingData = false;
         }
         finally {
             syncingData = false;
@@ -607,26 +607,34 @@ class NewTransactionItemHolder extends RecyclerView.ViewHolder
                     setEditable(false);
                     break;
             }
                     setEditable(false);
                     break;
             }
-
             if (this.item == null) { // was null or has changed
                 this.item = item;
                 final NewTransactionActivity activity =
                         (NewTransactionActivity) tvDescription.getContext();
             if (this.item == null) { // was null or has changed
                 this.item = item;
                 final NewTransactionActivity activity =
                         (NewTransactionActivity) tvDescription.getContext();
-                item.observeDate(activity, dateObserver);
-                item.observeDescription(activity, descriptionObserver);
-                item.observeAmountHint(activity, hintObserver);
-                item.observeEditableFlag(activity, editableObserver);
-                item.observeCommentVisible(activity, commentVisibleObserver);
-                item.observeComment(activity, commentObserver);
-                item.getModel()
-                    .observeFocusedItem(activity, focusedAccountObserver);
-                item.getModel()
-                    .observeAccountCount(activity, accountCountObserver);
-                Data.currencySymbolPosition.observe(activity, currencyPositionObserver);
-                Data.currencyGap.observe(activity, currencyGapObserver);
-                Data.locale.observe(activity, localeObserver);
-                item.observeCurrency(activity, currencyObserver);
-                item.getModel().showCurrency.observe(activity, showCurrencyObserver);
+
+                if (!item.isOfType(ItemType.bottomFiller)) {
+                    item.observeEditableFlag(activity, editableObserver);
+                    item.getModel()
+                        .observeFocusedItem(activity, focusedAccountObserver);
+                }
+                switch (item.getType()) {
+                    case generalData:
+                        item.observeDate(activity, dateObserver);
+                        item.observeDescription(activity, descriptionObserver);
+                        break;
+                    case transactionRow:
+                        item.observeAmountHint(activity, hintObserver);
+                        item.observeCommentVisible(activity, commentVisibleObserver);
+                        item.observeComment(activity, commentObserver);
+                        Data.currencySymbolPosition.observe(activity, currencyPositionObserver);
+                        Data.currencyGap.observe(activity, currencyGapObserver);
+                        Data.locale.observe(activity, localeObserver);
+                        item.observeCurrency(activity, currencyObserver);
+                        item.getModel().showCurrency.observe(activity, showCurrencyObserver);
+                        item.getModel()
+                            .observeAccountCount(activity, accountCountObserver);
+                        break;
+                }
             }
         }
         finally {
             }
         }
         finally {
@@ -644,8 +652,8 @@ class NewTransactionItemHolder extends RecyclerView.ViewHolder
 
     }
     @Override
 
     }
     @Override
-    public void onCurrencySelected(Currency item) {
-        this.item.setCurrency(item);
+    public void onCurrencySelected(Currency currency) {
+        adapter.model.setItemCurrency(this.item, currency, adapter);
     }
     @Override
     public void descriptionSelected(String description) {
     }
     @Override
     public void descriptionSelected(String description) {
index 8a271fa7c2a2514783c63813314c33dec5a8eb5f..b0241b1ae0aaa8ff06b8cd2037ce765defea6c35 100644 (file)
@@ -20,6 +20,7 @@ package net.ktnx.mobileledger.ui.activity;
 import android.annotation.SuppressLint;
 
 import androidx.annotation.NonNull;
 import android.annotation.SuppressLint;
 
 import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
 import androidx.lifecycle.LiveData;
 import androidx.lifecycle.MutableLiveData;
 import androidx.lifecycle.Observer;
 import androidx.lifecycle.LiveData;
 import androidx.lifecycle.MutableLiveData;
 import androidx.lifecycle.Observer;
@@ -41,16 +42,17 @@ import java.util.GregorianCalendar;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Locale;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Locale;
-import java.util.Map;
+import java.util.Set;
 import java.util.regex.Matcher;
 import java.util.regex.Pattern;
 
 import static net.ktnx.mobileledger.utils.Logger.debug;
 
 public class NewTransactionModel extends ViewModel {
 import java.util.regex.Matcher;
 import java.util.regex.Pattern;
 
 import static net.ktnx.mobileledger.utils.Logger.debug;
 
 public class NewTransactionModel extends ViewModel {
-    static final Pattern reYMD = Pattern.compile("^\\s*(\\d+)\\d*/\\s*(\\d+)\\s*/\\s*(\\d+)\\s*$");
-    static final Pattern reMD = Pattern.compile("^\\s*(\\d+)\\s*/\\s*(\\d+)\\s*$");
-    static final Pattern reD = Pattern.compile("\\s*(\\d+)\\s*$");
+    private static final Pattern reYMD =
+            Pattern.compile("^\\s*(\\d+)\\d*/\\s*(\\d+)\\s*/\\s*(\\d+)\\s*$");
+    private static final Pattern reMD = Pattern.compile("^\\s*(\\d+)\\s*/\\s*(\\d+)\\s*$");
+    private static final Pattern reD = Pattern.compile("\\s*(\\d+)\\s*$");
     final MutableLiveData<Boolean> showCurrency = new MutableLiveData<>(false);
     private final Item header = new Item(this, null, "");
     private final Item trailer = new Item(this);
     final MutableLiveData<Boolean> showCurrency = new MutableLiveData<>(false);
     private final Item header = new Item(this, null, "");
     private final Item trailer = new Item(this);
@@ -59,21 +61,36 @@ public class NewTransactionModel extends ViewModel {
     private final MutableLiveData<Integer> focusedItem = new MutableLiveData<>(0);
     private final MutableLiveData<Integer> accountCount = new MutableLiveData<>(0);
     private final MutableLiveData<Boolean> simulateSave = new MutableLiveData<>(false);
     private final MutableLiveData<Integer> focusedItem = new MutableLiveData<>(0);
     private final MutableLiveData<Integer> accountCount = new MutableLiveData<>(0);
     private final MutableLiveData<Boolean> simulateSave = new MutableLiveData<>(false);
-    public boolean getSimulateSave() {
+    /*
+     Slots contain lists of items, all using the same currency, including the possible
+     item with no account/amount that is used to help balancing the transaction
+
+     There is one slot per currency
+     */
+    private final HashMap<String, List<Item>> slots = new HashMap<>();
+    private int checkHoldCounter = 0;
+    void holdSubmittableChecks() {
+        checkHoldCounter++;
+    }
+    void releaseSubmittableChecks() {
+        if (checkHoldCounter == 0)
+            throw new RuntimeException("Asymmetrical call to releaseSubmittableChecks");
+        checkHoldCounter--;
+    }
+    boolean getSimulateSave() {
         return simulateSave.getValue();
     }
     public void setSimulateSave(boolean simulateSave) {
         this.simulateSave.setValue(simulateSave);
     }
         return simulateSave.getValue();
     }
     public void setSimulateSave(boolean simulateSave) {
         this.simulateSave.setValue(simulateSave);
     }
-    public void toggleSimulateSave() {
+    void toggleSimulateSave() {
         simulateSave.setValue(!simulateSave.getValue());
     }
         simulateSave.setValue(!simulateSave.getValue());
     }
-    public void observeSimulateSave(@NonNull @NotNull androidx.lifecycle.LifecycleOwner owner,
-                                    @NonNull
-                                            androidx.lifecycle.Observer<? super Boolean> observer) {
+    void observeSimulateSave(@NonNull @NotNull androidx.lifecycle.LifecycleOwner owner,
+                             @NonNull androidx.lifecycle.Observer<? super Boolean> observer) {
         this.simulateSave.observe(owner, observer);
     }
         this.simulateSave.observe(owner, observer);
     }
-    public int getAccountCount() {
+    int getAccountCount() {
         return items.size();
     }
     public Date getDate() {
         return items.size();
     }
     public Date getDate() {
@@ -82,7 +99,7 @@ public class NewTransactionModel extends ViewModel {
     public String getDescription() {
         return header.description.getValue();
     }
     public String getDescription() {
         return header.description.getValue();
     }
-    public LiveData<Boolean> isSubmittable() {
+    LiveData<Boolean> isSubmittable() {
         return this.isSubmittable;
     }
     void reset() {
         return this.isSubmittable;
     }
     void reset() {
@@ -93,28 +110,25 @@ public class NewTransactionModel extends ViewModel {
         items.add(new Item(this, new LedgerTransactionAccount("")));
         focusedItem.setValue(0);
     }
         items.add(new Item(this, new LedgerTransactionAccount("")));
         focusedItem.setValue(0);
     }
-    public void observeFocusedItem(@NonNull @NotNull androidx.lifecycle.LifecycleOwner owner,
-                                   @NonNull androidx.lifecycle.Observer<? super Integer> observer) {
+    void observeFocusedItem(@NonNull @NotNull androidx.lifecycle.LifecycleOwner owner,
+                            @NonNull androidx.lifecycle.Observer<? super Integer> observer) {
         this.focusedItem.observe(owner, observer);
     }
         this.focusedItem.observe(owner, observer);
     }
-    public void stopObservingFocusedItem(
-            @NonNull androidx.lifecycle.Observer<? super Integer> observer) {
+    void stopObservingFocusedItem(@NonNull androidx.lifecycle.Observer<? super Integer> observer) {
         this.focusedItem.removeObserver(observer);
     }
         this.focusedItem.removeObserver(observer);
     }
-    public void observeAccountCount(@NonNull @NotNull androidx.lifecycle.LifecycleOwner owner,
-                                    @NonNull
-                                            androidx.lifecycle.Observer<? super Integer> observer) {
+    void observeAccountCount(@NonNull @NotNull androidx.lifecycle.LifecycleOwner owner,
+                             @NonNull androidx.lifecycle.Observer<? super Integer> observer) {
         this.accountCount.observe(owner, observer);
     }
         this.accountCount.observe(owner, observer);
     }
-    public void stopObservingAccountCount(
-            @NonNull androidx.lifecycle.Observer<? super Integer> observer) {
+    void stopObservingAccountCount(@NonNull androidx.lifecycle.Observer<? super Integer> observer) {
         this.accountCount.removeObserver(observer);
     }
         this.accountCount.removeObserver(observer);
     }
-    public int getFocusedItem() { return focusedItem.getValue(); }
-    public void setFocusedItem(int position) {
+    int getFocusedItem() { return focusedItem.getValue(); }
+    void setFocusedItem(int position) {
         focusedItem.setValue(position);
     }
         focusedItem.setValue(position);
     }
-    public int addAccount(LedgerTransactionAccount acc) {
+    int addAccount(LedgerTransactionAccount acc) {
         items.add(new Item(this, acc));
         accountCount.setValue(getAccountCount());
         return items.size();
         items.add(new Item(this, acc));
         accountCount.setValue(getAccountCount());
         return items.size();
@@ -136,7 +150,7 @@ public class NewTransactionModel extends ViewModel {
         return items.get(index)
                     .getAccount();
     }
         return items.get(index)
                     .getAccount();
     }
-    public Item getItem(int index) {
+    Item getItem(int index) {
         if (index == 0) {
             return header;
         }
         if (index == 0) {
             return header;
         }
@@ -155,20 +169,28 @@ public class NewTransactionModel extends ViewModel {
      3a) amounts must balance to 0, or
      3b) there must be exactly one empty amount (with account)
      4) empty accounts with empty amounts are ignored
      3a) amounts must balance to 0, or
      3b) there must be exactly one empty amount (with account)
      4) empty accounts with empty amounts are ignored
+     Side effects:
      5) a row with an empty account name or empty amount is guaranteed to exist for each commodity
      6) at least two rows need to be present in the ledger
 
     */
     @SuppressLint("DefaultLocale")
      5) a row with an empty account name or empty amount is guaranteed to exist for each commodity
      6) at least two rows need to be present in the ledger
 
     */
     @SuppressLint("DefaultLocale")
-    public void checkTransactionSubmittable(NewTransactionItemsAdapter adapter) {
+    void checkTransactionSubmittable(NewTransactionItemsAdapter adapter) {
+        if (checkHoldCounter > 0)
+            return;
+
         int accounts = 0;
         int accounts = 0;
-        int amounts = 0;
-        int empty_rows = 0;
-        final Map<String, Float> balance = new HashMap<>();
+        final BalanceForCurrency balance = new BalanceForCurrency();
         final String descriptionText = getDescription();
         boolean submittable = true;
         final String descriptionText = getDescription();
         boolean submittable = true;
-        final Map<String, List<Item>> itemsWithEmptyAmountForCurrency = new HashMap<>();
-        final Map<String, List<Item>> itemsWithAccountAndEmptyAmountForCurrency = new HashMap<>();
+        final ItemsForCurrency itemsForCurrency = new ItemsForCurrency();
+        final ItemsForCurrency itemsWithEmptyAmountForCurrency = new ItemsForCurrency();
+        final ItemsForCurrency itemsWithAccountAndEmptyAmountForCurrency = new ItemsForCurrency();
+        final ItemsForCurrency itemsWithEmptyAccountForCurrency = new ItemsForCurrency();
+        final ItemsForCurrency itemsWithAmountForCurrency = new ItemsForCurrency();
+        final ItemsForCurrency itemsWithAccountForCurrency = new ItemsForCurrency();
+        final ItemsForCurrency emptyRowsForCurrency = new ItemsForCurrency();
+        final List<Item> emptyRows = new ArrayList<>();
 
         try {
             if ((descriptionText == null) || descriptionText.trim()
 
         try {
             if ((descriptionText == null) || descriptionText.trim()
@@ -186,54 +208,44 @@ public class NewTransactionModel extends ViewModel {
                                      .trim();
                 String currName = acc.getCurrency();
 
                                      .trim();
                 String currName = acc.getCurrency();
 
+                itemsForCurrency.add(currName, item);
+
                 if (acc_name.isEmpty()) {
                 if (acc_name.isEmpty()) {
-                    empty_rows++;
+                    itemsWithEmptyAccountForCurrency.add(currName, item);
 
                     if (acc.isAmountSet()) {
                         // 2) each amount has account name
                         Logger.debug("submittable", String.format(
 
                     if (acc.isAmountSet()) {
                         // 2) each amount has account name
                         Logger.debug("submittable", String.format(
-                                "Transaction not submittable: row %d has no account name, but has" +
-                                " amount %1.2f", i + 1, acc.getAmount()));
+                                "Transaction not submittable: row %d has no account name, but" +
+                                " has" + " amount %1.2f", i + 1, acc.getAmount()));
                         submittable = false;
                     }
                         submittable = false;
                     }
+                    else {
+                        emptyRowsForCurrency.add(currName, item);
+                    }
                 }
                 else {
                     accounts++;
                 }
                 else {
                     accounts++;
+                    itemsWithAccountForCurrency.add(currName, item);
                 }
 
                 if (acc.isAmountSet()) {
                 }
 
                 if (acc.isAmountSet()) {
-                    amounts++;
-                    Float bal = balance.get(currName);
-                    if (bal == null)
-                        bal = 0f;
-                    bal += acc.getAmount();
-                    balance.put(currName, bal);
+                    itemsWithAmountForCurrency.add(currName, item);
+                    balance.add(currName, acc.getAmount());
                 }
                 else {
                 }
                 else {
-                    {
-                        List<Item> list = itemsWithEmptyAmountForCurrency.get(currName);
-                        if (list == null)
-                            list = new ArrayList<>();
-                        itemsWithEmptyAmountForCurrency.put(currName, list);
-                        list.add(item);
-                    }
+                    itemsWithEmptyAmountForCurrency.add(currName, item);
 
 
-                    if (!acc_name.isEmpty()) {
-                        List<Item> list =
-                                itemsWithAccountAndEmptyAmountForCurrency.get(acc.getCurrency());
-                        if (list == null) {
-                            list = new ArrayList<>();
-                            itemsWithAccountAndEmptyAmountForCurrency.put(acc.getCurrency(), list);
-                        }
-                        list.add(item);
-                    }
+                    if (!acc_name.isEmpty())
+                        itemsWithAccountAndEmptyAmountForCurrency.add(currName, item);
                 }
             }
 
             // 1) has at least two account names
             if (accounts < 2) {
                 if (accounts == 0)
                 }
             }
 
             // 1) has at least two account names
             if (accounts < 2) {
                 if (accounts == 0)
-                    Logger.debug("submittable", "Transaction not submittable: no account names");
+                    Logger.debug("submittable",
+                            "Transaction not submittable: no account " + "names");
                 else if (accounts == 1)
                     Logger.debug("submittable",
                             "Transaction not submittable: only one account name");
                 else if (accounts == 1)
                     Logger.debug("submittable",
                             "Transaction not submittable: only one account name");
@@ -247,63 +259,118 @@ public class NewTransactionModel extends ViewModel {
             // 3) for each commodity:
             // 3a) amount must balance to 0, or
             // 3b) there must be exactly one empty amount (with account)
             // 3) for each commodity:
             // 3a) amount must balance to 0, or
             // 3b) there must be exactly one empty amount (with account)
-            for (String balCurrency : balance.keySet()) {
-                Float bal = balance.get(balCurrency);
-                float currencyBalance = (bal == null) ? 0f : bal;
+            for (String balCurrency : itemsForCurrency.currencies()) {
+                float currencyBalance = balance.get(balCurrency);
                 if (Misc.isZero(currencyBalance)) {
                     // remove hints from all amount inputs in that currency
                     for (Item item : items) {
                 if (Misc.isZero(currencyBalance)) {
                     // remove hints from all amount inputs in that currency
                     for (Item item : items) {
-                        final String itemCurrency = item.getCurrency()
-                                                        .getName();
-                        if (((balCurrency == null) && (null == itemCurrency)) ||
-                            ((balCurrency != null) && balCurrency.equals(itemCurrency)))
+                        if (Currency.equal(item.getCurrency(), balCurrency))
                             item.setAmountHint(null);
                     }
                 }
                 else {
                             item.setAmountHint(null);
                     }
                 }
                 else {
-                    List<Item> list = itemsWithAccountAndEmptyAmountForCurrency.get(balCurrency);
-                    int balanceReceiversCount = (list == null) ? 0 : list.size();
+                    List<Item> list =
+                            itemsWithAccountAndEmptyAmountForCurrency.getList(balCurrency);
+                    int balanceReceiversCount = list.size();
                     if (balanceReceiversCount != 1) {
                     if (balanceReceiversCount != 1) {
-                        Logger.debug("submittable", (balanceReceiversCount == 0) ? String.format(
-                                "Transaction not submittable [%s]: non-zero balance " +
-                                "with no empty amounts with accounts", balCurrency) : String.format(
-                                "Transaction not submittable [%s]: non-zero balance " +
-                                "with multiple empty amounts with accounts", balCurrency));
+                        if (BuildConfig.DEBUG) {
+                            if (balanceReceiversCount == 0)
+                                Logger.debug("submittable", String.format(
+                                        "Transaction not submittable [%s]: non-zero balance " +
+                                        "with no empty amounts with accounts", balCurrency));
+                            else
+                                Logger.debug("submittable", String.format(
+                                        "Transaction not submittable [%s]: non-zero balance " +
+                                        "with multiple empty amounts with accounts", balCurrency));
+                        }
                         submittable = false;
                     }
 
                         submittable = false;
                     }
 
-                    List<Item> emptyAmountList = itemsWithEmptyAmountForCurrency.get(balCurrency);
+                    List<Item> emptyAmountList =
+                            itemsWithEmptyAmountForCurrency.getList(balCurrency);
 
                     // suggest off-balance amount to a row and remove hints on other rows
                     Item receiver = null;
 
                     // suggest off-balance amount to a row and remove hints on other rows
                     Item receiver = null;
-                    if ((list != null) && !list.isEmpty())
+                    if (!list.isEmpty())
                         receiver = list.get(0);
                         receiver = list.get(0);
-                    else if ((emptyAmountList != null) && !emptyAmountList.isEmpty())
+                    else if (!emptyAmountList.isEmpty())
                         receiver = emptyAmountList.get(0);
 
                     for (Item item : items) {
                         receiver = emptyAmountList.get(0);
 
                     for (Item item : items) {
+                        if (!Currency.equal(item.getCurrency(), balCurrency))
+                            continue;
+
                         if (item.equals(receiver)) {
                         if (item.equals(receiver)) {
-                            Logger.debug("submittable",
-                                    String.format("Setting amount hint to %1.2f",
-                                            -currencyBalance));
+                            if (BuildConfig.DEBUG)
+                                Logger.debug("submittable",
+                                        String.format("Setting amount hint to %1.2f [%s]",
+                                                -currencyBalance, balCurrency));
                             item.setAmountHint(String.format("%1.2f", -currencyBalance));
                         }
                             item.setAmountHint(String.format("%1.2f", -currencyBalance));
                         }
-                        else
+                        else {
+                            if (BuildConfig.DEBUG)
+                                Logger.debug("submittable",
+                                        String.format("Resetting hint of '%s' [%s]",
+                                                (item.getAccount() == null) ? "" : item.getAccount()
+                                                                                       .getAccountName(),
+                                                balCurrency));
                             item.setAmountHint(null);
                             item.setAmountHint(null);
+                        }
                     }
                 }
             }
 
             // 5) a row with an empty account name or empty amount is guaranteed to exist for
             // each commodity
                     }
                 }
             }
 
             // 5) a row with an empty account name or empty amount is guaranteed to exist for
             // each commodity
-            for (String balCurrency : balance.keySet()) {
-                if ((empty_rows == 0) &&
-                    ((this.items.size() == accounts) || (this.items.size() == amounts)))
+            for (String balCurrency : balance.currencies()) {
+                int currEmptyRows = itemsWithEmptyAccountForCurrency.size(balCurrency);
+                int currRows = itemsForCurrency.size(balCurrency);
+                int currAccounts = itemsWithAccountForCurrency.size(balCurrency);
+                int currAmounts = itemsWithAmountForCurrency.size(balCurrency);
+                if ((currEmptyRows == 0) &&
+                    ((currRows == currAccounts) || (currRows == currAmounts)))
                 {
                 {
+                    // perhaps there already is an unused empty row for another currency that
+                    // is not used?
+//                        boolean foundIt = false;
+//                        for (Item item : emptyRows) {
+//                            Currency itemCurrency = item.getCurrency();
+//                            String itemCurrencyName =
+//                                    (itemCurrency == null) ? "" : itemCurrency.getName();
+//                            if (Misc.isZero(balance.get(itemCurrencyName))) {
+//                                item.setCurrency(Currency.loadByName(balCurrency));
+//                                item.setAmountHint(
+//                                        String.format("%1.2f", -balance.get(balCurrency)));
+//                                foundIt = true;
+//                                break;
+//                            }
+//                        }
+//
+//                        if (!foundIt)
                     adapter.addRow(balCurrency);
                 }
             }
 
                     adapter.addRow(balCurrency);
                 }
             }
 
+            // drop extra empty rows, not needed
+            for (String currName : emptyRowsForCurrency.currencies()) {
+                List<Item> emptyItems = emptyRowsForCurrency.getList(currName);
+                while ((this.items.size() > 2) && (emptyItems.size() > 1)) {
+                    Item item = emptyItems.get(1);
+                    emptyItems.remove(1);
+                    removeRow(item, adapter);
+                }
+
+                // unused currency, remove last item (which is also an empty one)
+                if ((items.size() > 2) && (emptyItems.size() == 1)) {
+                    List<Item> currItems = itemsForCurrency.getList(currName);
+
+                    if (currItems.size() == 1){
+                        Item item = emptyItems.get(0);
+                        removeRow(item, adapter);
+                    }
+                }
+            }
+
             // 6) at least two rows need to be present in the ledger
             while (this.items.size() < 2)
                 adapter.addRow();
             // 6) at least two rows need to be present in the ledger
             while (this.items.size() < 2)
                 adapter.addRow();
@@ -317,8 +384,9 @@ public class NewTransactionModel extends ViewModel {
                 for (int i = 0; i < items.size(); i++) {
                     Item item = items.get(i);
                     LedgerTransactionAccount acc = item.getAccount();
                 for (int i = 0; i < items.size(); i++) {
                     Item item = items.get(i);
                     LedgerTransactionAccount acc = item.getAccount();
-                    debug("submittable", String.format("Item %2d: [%4.2f %s] %s // %s", i,
-                            acc.isAmountSet() ? acc.getAmount() : 0, acc.getCurrency(),
+                    debug("submittable", String.format("Item %2d: [%4.2f(%s) %s] %s ; %s", i,
+                            acc.isAmountSet() ? acc.getAmount() : 0,
+                            item.isAmountHintSet() ? item.getAmountHint() : "ø", acc.getCurrency(),
                             acc.getAccountName(), acc.getComment()));
                 }
             }
                             acc.getAccountName(), acc.getComment()));
                 }
             }
@@ -333,30 +401,38 @@ public class NewTransactionModel extends ViewModel {
             isSubmittable.setValue(false);
         }
     }
             isSubmittable.setValue(false);
         }
     }
-    public void removeItem(int pos) {
+    private void removeRow(Item item, NewTransactionItemsAdapter adapter) {
+        int pos = items.indexOf(item);
+        items.remove(pos);
+        if (adapter != null) {
+            adapter.notifyItemRemoved(pos + 1);
+            sendCountNotifications();
+        }
+    }
+    void removeItem(int pos) {
         items.remove(pos);
         accountCount.setValue(getAccountCount());
     }
         items.remove(pos);
         accountCount.setValue(getAccountCount());
     }
-    public void sendCountNotifications() {
+    void sendCountNotifications() {
         accountCount.setValue(getAccountCount());
     }
     public void sendFocusedNotification() {
         focusedItem.setValue(focusedItem.getValue());
     }
         accountCount.setValue(getAccountCount());
     }
     public void sendFocusedNotification() {
         focusedItem.setValue(focusedItem.getValue());
     }
-    public void updateFocusedItem(int position) {
+    void updateFocusedItem(int position) {
         focusedItem.setValue(position);
     }
         focusedItem.setValue(position);
     }
-    public void noteFocusChanged(int position, FocusedElement element) {
+    void noteFocusChanged(int position, FocusedElement element) {
         getItem(position).setFocusedElement(element);
     }
         getItem(position).setFocusedElement(element);
     }
-    public void swapItems(int one, int two) {
+    void swapItems(int one, int two) {
         Collections.swap(items, one - 1, two - 1);
     }
         Collections.swap(items, one - 1, two - 1);
     }
-    public void toggleComment(int position) {
+    void toggleComment(int position) {
         final MutableLiveData<Boolean> commentVisible = getItem(position).commentVisible;
         commentVisible.postValue(!commentVisible.getValue());
     }
         final MutableLiveData<Boolean> commentVisible = getItem(position).commentVisible;
         commentVisible.postValue(!commentVisible.getValue());
     }
-    public void moveItemLast(int index) {
+    void moveItemLast(int index) {
         /*   0
              1   <-- index
              2
         /*   0
              1   <-- index
              2
@@ -369,14 +445,77 @@ public class NewTransactionModel extends ViewModel {
             items.add(itemCount - 1, acc);
         }
     }
             items.add(itemCount - 1, acc);
         }
     }
-    public void toggleCurrencyVisible() {
+    void toggleCurrencyVisible() {
         showCurrency.setValue(!showCurrency.getValue());
     }
         showCurrency.setValue(!showCurrency.getValue());
     }
+    public void setItemCurrency(Item item, Currency newCurrency,
+                                NewTransactionItemsAdapter adapter) {
+        Currency oldCurrency = item.getCurrency();
+        if (!Currency.equal(newCurrency, oldCurrency)) {
+            holdSubmittableChecks();
+            try {
+                item.setCurrency(newCurrency);
+//                for (Item i : items) {
+//                    if (Currency.equal(i.getCurrency(), oldCurrency))
+//                        i.setCurrency(newCurrency);
+//                }
+            }
+            finally {
+                releaseSubmittableChecks();
+            }
+
+            checkTransactionSubmittable(adapter);
+        }
+    }
     enum ItemType {generalData, transactionRow, bottomFiller}
 
     enum ItemType {generalData, transactionRow, bottomFiller}
 
+    enum FocusedElement {Account, Comment, Amount}
+
+    private class ItemsForCurrency {
+        private HashMap<String, List<Item>> hashMap = new HashMap<>();
+        @NonNull
+        List<Item> getList(@Nullable String currencyName) {
+            List<Item> list = hashMap.get(currencyName);
+            if (list == null) {
+                list = new ArrayList<>();
+                hashMap.put(currencyName, list);
+            }
+            return list;
+        }
+        void add(@Nullable String currencyName, @NonNull Item item) {
+            getList(currencyName).add(item);
+        }
+        int size(@Nullable String currencyName) {
+            return this.getList(currencyName)
+                       .size();
+        }
+        Set<String> currencies() {
+            return hashMap.keySet();
+        }
+    }
+
     //==========================================================================================
 
     //==========================================================================================
 
-    enum FocusedElement {Account, Comment, Amount}
+    private class BalanceForCurrency {
+        private HashMap<String, Float> hashMap = new HashMap<>();
+        float get(String currencyName) {
+            Float f = hashMap.get(currencyName);
+            if (f == null) {
+                f = 0f;
+                hashMap.put(currencyName, f);
+            }
+            return f;
+        }
+        void add(String currencyName, float amount) {
+            hashMap.put(currencyName, get(currencyName) + amount);
+        }
+        Set<String> currencies() {
+            return hashMap.keySet();
+        }
+        boolean containsCurrency(String currencyName) {
+            return hashMap.containsKey(currencyName);
+        }
+    }
 
     class Item {
         private ItemType type;
 
     class Item {
         private ItemType type;
@@ -390,34 +529,40 @@ public class NewTransactionModel extends ViewModel {
         private MutableLiveData<String> comment = new MutableLiveData<>(null);
         private MutableLiveData<Boolean> commentVisible = new MutableLiveData<>(false);
         private MutableLiveData<Currency> currency = new MutableLiveData<>(null);
         private MutableLiveData<String> comment = new MutableLiveData<>(null);
         private MutableLiveData<Boolean> commentVisible = new MutableLiveData<>(false);
         private MutableLiveData<Currency> currency = new MutableLiveData<>(null);
-        public Item(NewTransactionModel model) {
+        private boolean amountHintIsSet = false;
+        Item(NewTransactionModel model) {
             this.model = model;
             type = ItemType.bottomFiller;
             editable.setValue(false);
         }
             this.model = model;
             type = ItemType.bottomFiller;
             editable.setValue(false);
         }
-        public Item(NewTransactionModel model, Date date, String description) {
+        Item(NewTransactionModel model, Date date, String description) {
             this.model = model;
             this.type = ItemType.generalData;
             this.date.setValue(date);
             this.description.setValue(description);
             this.editable.setValue(true);
         }
             this.model = model;
             this.type = ItemType.generalData;
             this.date.setValue(date);
             this.description.setValue(description);
             this.editable.setValue(true);
         }
-        public Item(NewTransactionModel model, LedgerTransactionAccount account) {
+        Item(NewTransactionModel model, LedgerTransactionAccount account) {
             this.model = model;
             this.type = ItemType.transactionRow;
             this.account = account;
             this.model = model;
             this.type = ItemType.transactionRow;
             this.account = account;
+            String currName = account.getCurrency();
+            Currency curr = null;
+            if ((currName != null) && !currName.isEmpty())
+                curr = Currency.loadByName(currName);
+            this.currency.setValue(curr);
             this.editable.setValue(true);
         }
             this.editable.setValue(true);
         }
-        public FocusedElement getFocusedElement() {
+        FocusedElement getFocusedElement() {
             return focusedElement;
         }
             return focusedElement;
         }
-        public void setFocusedElement(FocusedElement focusedElement) {
+        void setFocusedElement(FocusedElement focusedElement) {
             this.focusedElement = focusedElement;
         }
         public NewTransactionModel getModel() {
             return model;
         }
             this.focusedElement = focusedElement;
         }
         public NewTransactionModel getModel() {
             return model;
         }
-        public void setEditable(boolean editable) {
+        void setEditable(boolean editable) {
             ensureType(ItemType.generalData, ItemType.transactionRow);
             this.editable.setValue(editable);
         }
             ensureType(ItemType.generalData, ItemType.transactionRow);
             this.editable.setValue(editable);
         }
@@ -428,38 +573,39 @@ public class NewTransactionModel extends ViewModel {
                                 type1, type2));
             }
         }
                                 type1, type2));
             }
         }
-        public String getAmountHint() {
+        String getAmountHint() {
             ensureType(ItemType.transactionRow);
             return amountHint.getValue();
         }
             ensureType(ItemType.transactionRow);
             return amountHint.getValue();
         }
-        public void setAmountHint(String amountHint) {
+        void setAmountHint(String amountHint) {
             ensureType(ItemType.transactionRow);
 
             // avoid unnecessary triggers
             if (amountHint == null) {
                 if (this.amountHint.getValue() == null)
                     return;
             ensureType(ItemType.transactionRow);
 
             // avoid unnecessary triggers
             if (amountHint == null) {
                 if (this.amountHint.getValue() == null)
                     return;
+                amountHintIsSet = false;
             }
             else {
                 if (amountHint.equals(this.amountHint.getValue()))
                     return;
             }
             else {
                 if (amountHint.equals(this.amountHint.getValue()))
                     return;
+                amountHintIsSet = true;
             }
 
             this.amountHint.setValue(amountHint);
         }
             }
 
             this.amountHint.setValue(amountHint);
         }
-        public void observeAmountHint(@NonNull @NotNull androidx.lifecycle.LifecycleOwner owner,
-                                      @NonNull
-                                              androidx.lifecycle.Observer<? super String> observer) {
+        void observeAmountHint(@NonNull @NotNull androidx.lifecycle.LifecycleOwner owner,
+                               @NonNull androidx.lifecycle.Observer<? super String> observer) {
             this.amountHint.observe(owner, observer);
         }
             this.amountHint.observe(owner, observer);
         }
-        public void stopObservingAmountHint(
+        void stopObservingAmountHint(
                 @NonNull androidx.lifecycle.Observer<? super String> observer) {
             this.amountHint.removeObserver(observer);
         }
                 @NonNull androidx.lifecycle.Observer<? super String> observer) {
             this.amountHint.removeObserver(observer);
         }
-        public ItemType getType() {
+        ItemType getType() {
             return type;
         }
             return type;
         }
-        public void ensureType(ItemType wantedType) {
+        void ensureType(ItemType wantedType) {
             if (type != wantedType) {
                 throw new RuntimeException(
                         String.format("Actual type (%s) differs from wanted (%s)", type,
             if (type != wantedType) {
                 throw new RuntimeException(
                         String.format("Actual type (%s) differs from wanted (%s)", type,
@@ -513,11 +659,11 @@ public class NewTransactionModel extends ViewModel {
 
             this.setDate(c.getTime());
         }
 
             this.setDate(c.getTime());
         }
-        public void observeDate(@NonNull @NotNull androidx.lifecycle.LifecycleOwner owner,
-                                @NonNull androidx.lifecycle.Observer<? super Date> observer) {
+        void observeDate(@NonNull @NotNull androidx.lifecycle.LifecycleOwner owner,
+                         @NonNull androidx.lifecycle.Observer<? super Date> observer) {
             this.date.observe(owner, observer);
         }
             this.date.observe(owner, observer);
         }
-        public void stopObservingDate(@NonNull androidx.lifecycle.Observer<? super Date> observer) {
+        void stopObservingDate(@NonNull androidx.lifecycle.Observer<? super Date> observer) {
             this.date.removeObserver(observer);
         }
         public String getDescription() {
             this.date.removeObserver(observer);
         }
         public String getDescription() {
@@ -528,12 +674,11 @@ public class NewTransactionModel extends ViewModel {
             ensureType(ItemType.generalData);
             this.description.setValue(description);
         }
             ensureType(ItemType.generalData);
             this.description.setValue(description);
         }
-        public void observeDescription(@NonNull @NotNull androidx.lifecycle.LifecycleOwner owner,
-                                       @NonNull
-                                               androidx.lifecycle.Observer<? super String> observer) {
+        void observeDescription(@NonNull @NotNull androidx.lifecycle.LifecycleOwner owner,
+                                @NonNull androidx.lifecycle.Observer<? super String> observer) {
             this.description.observe(owner, observer);
         }
             this.description.observe(owner, observer);
         }
-        public void stopObservingDescription(
+        void stopObservingDescription(
                 @NonNull androidx.lifecycle.Observer<? super String> observer) {
             this.description.removeObserver(observer);
         }
                 @NonNull androidx.lifecycle.Observer<? super String> observer) {
             this.description.removeObserver(observer);
         }
@@ -549,7 +694,7 @@ public class NewTransactionModel extends ViewModel {
          *
          * @return nicely formatted, shortest available date representation
          */
          *
          * @return nicely formatted, shortest available date representation
          */
-        public String getFormattedDate() {
+        String getFormattedDate() {
             if (date == null)
                 return null;
             Date time = date.getValue();
             if (date == null)
                 return null;
             Date time = date.getValue();
@@ -574,24 +719,22 @@ public class NewTransactionModel extends ViewModel {
 
             return String.valueOf(myDay);
         }
 
             return String.valueOf(myDay);
         }
-        public void observeEditableFlag(NewTransactionActivity activity,
-                                        Observer<Boolean> observer) {
+        void observeEditableFlag(NewTransactionActivity activity, Observer<Boolean> observer) {
             editable.observe(activity, observer);
         }
             editable.observe(activity, observer);
         }
-        public void stopObservingEditableFlag(Observer<Boolean> observer) {
+        void stopObservingEditableFlag(Observer<Boolean> observer) {
             editable.removeObserver(observer);
         }
             editable.removeObserver(observer);
         }
-        public void observeCommentVisible(NewTransactionActivity activity,
-                                          Observer<Boolean> observer) {
+        void observeCommentVisible(NewTransactionActivity activity, Observer<Boolean> observer) {
             commentVisible.observe(activity, observer);
         }
             commentVisible.observe(activity, observer);
         }
-        public void stopObservingCommentVisible(Observer<Boolean> observer) {
+        void stopObservingCommentVisible(Observer<Boolean> observer) {
             commentVisible.removeObserver(observer);
         }
             commentVisible.removeObserver(observer);
         }
-        public void observeComment(NewTransactionActivity activity, Observer<String> observer) {
+        void observeComment(NewTransactionActivity activity, Observer<String> observer) {
             comment.observe(activity, observer);
         }
             comment.observe(activity, observer);
         }
-        public void stopObservingComment(Observer<String> observer) {
+        void stopObservingComment(Observer<String> observer) {
             comment.removeObserver(observer);
         }
         public void setComment(String comment) {
             comment.removeObserver(observer);
         }
         public void setComment(String comment) {
@@ -602,16 +745,27 @@ public class NewTransactionModel extends ViewModel {
             return this.currency.getValue();
         }
         public void setCurrency(Currency currency) {
             return this.currency.getValue();
         }
         public void setCurrency(Currency currency) {
-            getAccount().setCurrency((currency != null && !currency.getName()
-                                                                   .isEmpty()) ? currency.getName()
-                                                                               : null);
-            this.currency.setValue(currency);
+            Currency present = this.currency.getValue();
+            if ((currency == null) && (present != null) ||
+                (currency != null) && !currency.equals(present))
+            {
+                getAccount().setCurrency((currency != null && !currency.getName()
+                                                                       .isEmpty())
+                                         ? currency.getName() : null);
+                this.currency.setValue(currency);
+            }
         }
         }
-        public void observeCurrency(NewTransactionActivity activity, Observer<Currency> observer) {
+        void observeCurrency(NewTransactionActivity activity, Observer<Currency> observer) {
             currency.observe(activity, observer);
         }
             currency.observe(activity, observer);
         }
-        public void stopObservingCurrency(Observer<Currency> observer) {
+        void stopObservingCurrency(Observer<Currency> observer) {
             currency.removeObserver(observer);
         }
             currency.removeObserver(observer);
         }
+        boolean isOfType(ItemType type) {
+            return this.type == type;
+        }
+        boolean isAmountHintSet() {
+            return amountHintIsSet;
+        }
     }
 }
     }
 }