]> git.ktnx.net Git - mobile-ledger.git/blobdiff - app/src/main/java/net/ktnx/mobileledger/ui/new_transaction/NewTransactionModel.java
explicit assert of non-null value
[mobile-ledger.git] / app / src / main / java / net / ktnx / mobileledger / ui / new_transaction / NewTransactionModel.java
index fdcab48788b94f6336f41254298f92c02748e4c6..d8f387b72696983f1fe9f87d0ae12e0a5b4d1be8 100644 (file)
@@ -1,5 +1,5 @@
 /*
- * Copyright © 2021 Damyan Ivanov.
+ * Copyright © 2022 Damyan Ivanov.
  * This file is part of MoLe.
  * MoLe is free software: you can distribute it and/or modify it
  * under the term of the GNU General Public License as published by
@@ -18,8 +18,7 @@
 package net.ktnx.mobileledger.ui.new_transaction;
 
 import android.annotation.SuppressLint;
-import android.os.Handler;
-import android.os.Looper;
+import android.os.Build;
 import android.text.TextUtils;
 
 import androidx.annotation.NonNull;
@@ -31,15 +30,17 @@ import androidx.lifecycle.Observer;
 import androidx.lifecycle.ViewModel;
 
 import net.ktnx.mobileledger.BuildConfig;
+import net.ktnx.mobileledger.db.Currency;
 import net.ktnx.mobileledger.db.DB;
+import net.ktnx.mobileledger.db.Profile;
 import net.ktnx.mobileledger.db.TemplateAccount;
 import net.ktnx.mobileledger.db.TemplateHeader;
+import net.ktnx.mobileledger.db.TransactionWithAccounts;
 import net.ktnx.mobileledger.model.Data;
 import net.ktnx.mobileledger.model.InertMutableLiveData;
 import net.ktnx.mobileledger.model.LedgerTransaction;
 import net.ktnx.mobileledger.model.LedgerTransactionAccount;
 import net.ktnx.mobileledger.model.MatchedTemplate;
-import net.ktnx.mobileledger.model.MobileLedgerProfile;
 import net.ktnx.mobileledger.utils.Globals;
 import net.ktnx.mobileledger.utils.Logger;
 import net.ktnx.mobileledger.utils.Misc;
@@ -73,14 +74,15 @@ public class NewTransactionModel extends ViewModel {
     private final MutableLiveData<Boolean> simulateSave = new InertMutableLiveData<>(false);
     private final AtomicInteger busyCounter = new AtomicInteger(0);
     private final MutableLiveData<Boolean> busyFlag = new InertMutableLiveData<>(false);
-    private final Observer<MobileLedgerProfile> profileObserver = profile -> {
-        showCurrency.postValue(profile.getShowCommodityByDefault());
-        showComments.postValue(profile.getShowCommentsByDefault());
+    private final Observer<Profile> profileObserver = profile -> {
+        if (profile != null) {
+            showCurrency.postValue(profile.getShowCommodityByDefault());
+            showComments.postValue(profile.getShowCommentsByDefault());
+        }
     };
     private final MutableLiveData<FocusInfo> focusInfo = new MutableLiveData<>();
     private boolean observingDataProfile;
     public NewTransactionModel() {
-        reset();
     }
     public LiveData<Boolean> getShowCurrency() {
         return showCurrency;
@@ -101,7 +103,9 @@ public class NewTransactionModel extends ViewModel {
      * make old items replaceable in-place. makes the new values visually blend in
      */
     private void renumberItems() {
-        final List<Item> list = items.getValue();
+        renumberItems(items.getValue());
+    }
+    private void renumberItems(List<Item> list) {
         if (list == null) {
             return;
         }
@@ -160,7 +164,7 @@ public class NewTransactionModel extends ViewModel {
         return copy;
     }
     private List<Item> shallowCopyList() {
-        return new ArrayList<>(items.getValue());
+        return new ArrayList<>(Objects.requireNonNull(items.getValue()));
     }
     LiveData<Boolean> getShowComments() {
         return showComments;
@@ -190,8 +194,10 @@ public class NewTransactionModel extends ViewModel {
         List<Item> list = new ArrayList<>();
         Item.resetIdDispenser();
         list.add(new TransactionHead(""));
-        list.add(new TransactionAccount(""));
-        list.add(new TransactionAccount(""));
+        final String defaultCurrency = Objects.requireNonNull(Data.getProfile())
+                                              .getDefaultCommodity();
+        list.add(new TransactionAccount("", defaultCurrency));
+        list.add(new TransactionAccount("", defaultCurrency));
         noteFocusChanged(0, FocusedElement.Description);
         renumberItems();
         isSubmittable.setValue(false);
@@ -260,7 +266,6 @@ public class NewTransactionModel extends ViewModel {
         if (Misc.emptyIsNull(transactionComment) != null)
             head.setComment(transactionComment);
 
-        Item.resetIdDispenser();
         List<Item> newItems = new ArrayList<>();
 
         newItems.add(head);
@@ -291,25 +296,33 @@ public class NewTransactionModel extends ViewModel {
                   if (amount != null && acc.getNegateAmount() != null && acc.getNegateAmount())
                       amount = -amount;
 
-                  // TODO currency
                   TransactionAccount accRow = new TransactionAccount(accountName);
                   accRow.setComment(accountComment);
                   if (amount != null)
                       accRow.setAmount(amount);
+                  accRow.setCurrency(
+                          extractCurrencyFromMatches(matchResult, acc.getCurrencyMatchGroup(),
+                                  acc.getCurrencyObject()));
 
                   newItems.add(accRow);
               }
 
-              new Handler(Looper.getMainLooper()).post(() -> replaceItems(newItems));
+              renumberItems(newItems);
+              Misc.onMainThread(() -> replaceItems(newItems));
           });
     }
+    @NonNull
+    private String extractCurrencyFromMatches(MatchResult m, Integer group, Currency literal) {
+        return Misc.nullIsEmpty(
+                extractStringFromMatches(m, group, (literal == null) ? "" : literal.getName()));
+    }
     private int extractIntFromMatches(MatchResult m, Integer group, Integer literal) {
         if (literal != null)
             return literal;
 
         if (group != null) {
             int grp = group;
-            if (grp > 0 & grp <= m.groupCount())
+            if (grp > 0 && grp <= m.groupCount())
                 try {
                     return Integer.parseInt(m.group(grp));
                 }
@@ -320,13 +333,14 @@ public class NewTransactionModel extends ViewModel {
 
         return 0;
     }
+    @Nullable
     private String extractStringFromMatches(MatchResult m, Integer group, String literal) {
         if (literal != null)
             return literal;
 
         if (group != null) {
             int grp = group;
-            if (grp > 0 & grp <= m.groupCount())
+            if (grp > 0 && grp <= m.groupCount())
                 return m.group(grp);
         }
 
@@ -338,7 +352,7 @@ public class NewTransactionModel extends ViewModel {
 
         if (group != null) {
             int grp = group;
-            if (grp > 0 & grp <= m.groupCount())
+            if (grp > 0 && grp <= m.groupCount())
                 try {
                     return Float.valueOf(m.group(grp));
                 }
@@ -357,7 +371,7 @@ public class NewTransactionModel extends ViewModel {
             noteFocusChanged(fi.position - 1, fi.element);
         setItems(newList);
     }
-    void noteFocusChanged(int position, FocusedElement element) {
+    void noteFocusChanged(int position, @Nullable FocusedElement element) {
         FocusInfo present = focusInfo.getValue();
         if (present == null || present.position != position || present.element != element)
             focusInfo.setValue(new FocusInfo(position, element));
@@ -396,10 +410,10 @@ public class NewTransactionModel extends ViewModel {
         // visual changes -- the currency fields will be hidden or reset to default anyway
         // still, there may be changes in the submittable state
         final List<Item> list = Objects.requireNonNull(this.items.getValue());
+        final Profile profile = Objects.requireNonNull(Data.getProfile());
         for (int i = 1; i < list.size(); i++) {
-            ((TransactionAccount) list.get(i)).setCurrency(newValue ? Data.getProfile()
-                                                                          .getDefaultCommodity()
-                                                                    : null);
+            ((TransactionAccount) list.get(i)).setCurrency(
+                    newValue ? profile.getDefaultCommodity() : "");
         }
         checkTransactionSubmittable(null);
         showCurrency.setValue(newValue);
@@ -427,18 +441,17 @@ public class NewTransactionModel extends ViewModel {
         List<Item> list = Objects.requireNonNull(items.getValue());
         TransactionHead head = list.get(0)
                                    .toTransactionHead();
-        SimpleDate date = head.getDate();
         LedgerTransaction tr = head.asLedgerTransaction();
 
         tr.setComment(head.getComment());
-        LedgerTransactionAccount emptyAmountAccount = null;
-        float emptyAmountAccountBalance = 0;
+        HashMap<String, List<LedgerTransactionAccount>> emptyAmountAccounts = new HashMap<>();
+        HashMap<String, Float> emptyAmountAccountBalance = new HashMap<>();
         for (int i = 1; i < list.size(); i++) {
             TransactionAccount item = list.get(i)
                                           .toTransactionAccount();
+            String currency = item.getCurrency();
             LedgerTransactionAccount acc = new LedgerTransactionAccount(item.getAccountName()
-                                                                            .trim(),
-                    item.getCurrency());
+                                                                            .trim(), currency);
             if (acc.getAccountName()
                    .isEmpty())
                 continue;
@@ -447,47 +460,83 @@ public class NewTransactionModel extends ViewModel {
 
             if (item.isAmountSet()) {
                 acc.setAmount(item.getAmount());
-                emptyAmountAccountBalance += item.getAmount();
+                Float emptyCurrBalance = emptyAmountAccountBalance.get(currency);
+                if (emptyCurrBalance == null) {
+                    emptyAmountAccountBalance.put(currency, item.getAmount());
+                }
+                else {
+                    emptyAmountAccountBalance.put(currency, emptyCurrBalance + item.getAmount());
+                }
             }
             else {
-                emptyAmountAccount = acc;
+                List<LedgerTransactionAccount> emptyCurrAccounts =
+                        emptyAmountAccounts.get(currency);
+                if (emptyCurrAccounts == null)
+                    emptyAmountAccounts.put(currency, emptyCurrAccounts = new ArrayList<>());
+                emptyCurrAccounts.add(acc);
             }
 
             tr.addAccount(acc);
         }
 
-        if (emptyAmountAccount != null)
-            emptyAmountAccount.setAmount(-emptyAmountAccountBalance);
+        if (emptyAmountAccounts.size() > 0) {
+            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
+                emptyAmountAccounts.forEach((currency, accounts) -> {
+                    if (accounts.size() != 1)
+                        throw new RuntimeException(String.format(Locale.US,
+                                "Should not happen: approved transaction has %d accounts for " +
+                                "currency %s", accounts.size(), currency));
+                    accounts.get(0)
+                            .setAmount(-Objects.requireNonNull(
+                                    emptyAmountAccountBalance.get(currency)));
+                });
+            }
+            else {
+                for (String currency : emptyAmountAccounts.keySet()) {
+                    List<LedgerTransactionAccount> accounts =
+                            Objects.requireNonNull(emptyAmountAccounts.get(currency));
+
+                    if (accounts.size() != 1)
+                        throw new RuntimeException(String.format(Locale.US,
+                                "Should not happen: approved transaction has %d accounts for " +
+                                "currency %s", accounts.size(), currency));
+                    accounts.get(0)
+                            .setAmount(-Objects.requireNonNull(
+                                    emptyAmountAccountBalance.get(currency)));
+                }
+            }
+        }
 
         return tr;
     }
-    void loadTransactionIntoModel(long profileId, int transactionId) {
+    void loadTransactionIntoModel(@NonNull TransactionWithAccounts tr) {
         List<Item> newList = new ArrayList<>();
         Item.resetIdDispenser();
-        LedgerTransaction tr;
-        MobileLedgerProfile profile = Data.getProfile(profileId);
-        if (profile == null)
-            throw new RuntimeException(String.format(
-                    "Unable to find profile %s, which is supposed to contain transaction %d",
-                    profileId, transactionId));
 
-        tr = profile.loadTransaction(transactionId);
-        TransactionHead head = new TransactionHead(tr.getDescription());
-        head.setComment(tr.getComment());
+        Item currentHead = Objects.requireNonNull(items.getValue())
+                                  .get(0);
+        TransactionHead head = new TransactionHead(tr.transaction.getDescription());
+        head.setComment(tr.transaction.getComment());
+        if (currentHead instanceof TransactionHead)
+            head.setDate(((TransactionHead) currentHead).date);
 
         newList.add(head);
 
-        List<LedgerTransactionAccount> accounts = tr.getAccounts();
+        List<LedgerTransactionAccount> accounts = new ArrayList<>();
+        for (net.ktnx.mobileledger.db.TransactionAccount acc : tr.accounts) {
+            accounts.add(new LedgerTransactionAccount(acc));
+        }
 
         TransactionAccount firstNegative = null;
         TransactionAccount firstPositive = null;
         int singleNegativeIndex = -1;
         int singlePositiveIndex = -1;
         int negativeCount = 0;
+        boolean hasCurrency = false;
         for (int i = 0; i < accounts.size(); i++) {
             LedgerTransactionAccount acc = accounts.get(i);
-            TransactionAccount item =
-                    new TransactionAccount(acc.getAccountName(), acc.getCurrency());
+            TransactionAccount item = new TransactionAccount(acc.getAccountName(),
+                    Misc.nullIsEmpty(acc.getCurrency()));
             newList.add(item);
 
             item.setAccountName(acc.getAccountName());
@@ -513,6 +562,10 @@ public class NewTransactionModel extends ViewModel {
             }
             else
                 item.resetAmount();
+
+            if (item.getCurrency()
+                    .length() > 0)
+                hasCurrency = true;
         }
         if (BuildConfig.DEBUG)
             dumpItemList("Loaded previous transaction", newList);
@@ -526,9 +579,13 @@ public class NewTransactionModel extends ViewModel {
             moveItemLast(newList, singlePositiveIndex);
         }
 
-        setItems(newList);
-
-        noteFocusChanged(1, FocusedElement.Amount);
+        final boolean foundTransactionHasCurrency = hasCurrency;
+        Misc.onMainThread(() -> {
+            setItems(newList);
+            noteFocusChanged(1, FocusedElement.Amount);
+            if (foundTransactionHasCurrency)
+                showCurrency.setValue(true);
+        });
     }
     /**
      * A transaction is submittable if:
@@ -583,6 +640,8 @@ public class NewTransactionModel extends ViewModel {
                 submittable = false;
             }
 
+            boolean hasInvalidAmount = false;
+
             for (int i = 1; i < list.size(); i++) {
                 TransactionAccount item = list.get(i)
                                               .toTransactionAccount();
@@ -612,16 +671,18 @@ public class NewTransactionModel extends ViewModel {
                     itemsWithAccountForCurrency.add(currName, item);
                 }
 
-                if (!item.isAmountValid()) {
-                    Logger.debug("submittable",
-                            String.format("Not submittable: row %d has an invalid amount", i + 1));
-                    submittable = false;
-                }
-                else if (item.isAmountSet()) {
+                if (item.isAmountSet() && item.isAmountValid()) {
                     itemsWithAmountForCurrency.add(currName, item);
                     balance.add(currName, item.getAmount());
                 }
                 else {
+                    if (!item.isAmountValid()) {
+                        Logger.debug("submittable",
+                                String.format("Not submittable: row %d has an invalid amount", i));
+                        submittable = false;
+                        hasInvalidAmount = true;
+                    }
+
                     itemsWithEmptyAmountForCurrency.add(currName, item);
 
                     if (!accName.isEmpty())
@@ -678,11 +739,11 @@ public class NewTransactionModel extends ViewModel {
                         if (BuildConfig.DEBUG) {
                             if (balanceReceiversCount == 0)
                                 Logger.debug("submittable", String.format(
-                                        "Transaction not submittable [%s]: non-zero balance " +
+                                        "Transaction not submittable [curr:%s]: non-zero balance " +
                                         "with no empty amounts with accounts", balCurrency));
                             else
                                 Logger.debug("submittable", String.format(
-                                        "Transaction not submittable [%s]: non-zero balance " +
+                                        "Transaction not submittable [curr:%s]: non-zero balance " +
                                         "with multiple empty amounts with accounts", balCurrency));
                         }
                         submittable = false;
@@ -708,13 +769,13 @@ public class NewTransactionModel extends ViewModel {
                             continue;
 
                         if (item == receiver) {
-                            final String hint = String.format("%1.2f", -currencyBalance);
+                            final String hint = Data.formatNumber(-currencyBalance);
                             if (!acc.isAmountHintSet() ||
                                 !Misc.equalStrings(acc.getAmountHint(), hint))
                             {
                                 Logger.debug("submittable",
-                                        String.format("Setting amount hint of {%s} to %s [%s]",
-                                                acc.toString(), hint, balCurrency));
+                                        String.format("Setting amount hint of {%s} to %s [%s]", acc,
+                                                hint, balCurrency));
                                 acc.setAmountHint(hint);
                                 listChanged = true;
                             }
@@ -736,16 +797,17 @@ public class NewTransactionModel extends ViewModel {
 
             // 5) a row with an empty account name or empty amount is guaranteed to exist for
             // each commodity
-            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?
+            if (!hasInvalidAmount) {
+                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();
@@ -754,22 +816,23 @@ public class NewTransactionModel extends ViewModel {
 //                            if (Misc.isZero(balance.get(itemCurrencyName))) {
 //                                item.setCurrency(Currency.loadByName(balCurrency));
 //                                item.setAmountHint(
-//                                        String.format("%1.2f", -balance.get(balCurrency)));
+//                                        Data.formatNumber(-balance.get(balCurrency)));
 //                                foundIt = true;
 //                                break;
 //                            }
 //                        }
 //
 //                        if (!foundIt)
-                    final TransactionAccount newAcc = new TransactionAccount("", balCurrency);
-                    final float bal = balance.get(balCurrency);
-                    if (!Misc.isZero(bal) && currAmounts == currRows)
-                        newAcc.setAmountHint(String.format("%4.2f", -bal));
-                    Logger.debug("submittable",
-                            String.format("Adding new item with %s for currency %s",
-                                    newAcc.getAmountHint(), balCurrency));
-                    list.add(newAcc);
-                    listChanged = true;
+                        final TransactionAccount newAcc = new TransactionAccount("", balCurrency);
+                        final float bal = balance.get(balCurrency);
+                        if (!Misc.isZero(bal) && currAmounts == currRows)
+                            newAcc.setAmountHint(Data.formatNumber(-bal));
+                        Logger.debug("submittable",
+                                String.format("Adding new item with %s for currency %s",
+                                        newAcc.getAmountHint(), balCurrency));
+                        list.add(newAcc);
+                        listChanged = true;
+                    }
                 }
             }
 
@@ -877,7 +940,7 @@ public class NewTransactionModel extends ViewModel {
     public static class FocusInfo {
         int position;
         FocusedElement element;
-        public FocusInfo(int position, FocusedElement element) {
+        public FocusInfo(int position, @Nullable FocusedElement element) {
             this.position = position;
             this.element = element;
         }
@@ -1007,7 +1070,7 @@ public class NewTransactionModel extends ViewModel {
                 b.append(String.format(" '%s'", description));
 
             if (date != null)
-                b.append(String.format("@%s", date.toString()));
+                b.append(String.format("@%s", date));
 
             if (!TextUtils.isEmpty(comment))
                 b.append(String.format(" /%s/", comment));
@@ -1031,7 +1094,8 @@ public class NewTransactionModel extends ViewModel {
             return ItemType.generalData;
         }
         public LedgerTransaction asLedgerTransaction() {
-            return new LedgerTransaction(0, date, description, Data.getProfile());
+            return new LedgerTransaction(0, (date == null) ? SimpleDate.today() : date, description,
+                    Objects.requireNonNull(Data.getProfile()));
         }
         public boolean equalContents(TransactionHead other) {
             if (other == null)
@@ -1047,10 +1111,13 @@ public class NewTransactionModel extends ViewModel {
         private String accountName;
         private String amountHint;
         private String comment;
-        private String currency;
+        @NotNull
+        private String currency = "";
         private float amount;
         private boolean amountSet;
         private boolean amountValid = true;
+        @NotNull
+        private String amountText = "";
         private FocusedElement focusedElement = FocusedElement.Account;
         private boolean amountHintIsSet = false;
         private boolean isLast = false;
@@ -1062,6 +1129,7 @@ public class NewTransactionModel extends ViewModel {
             amountSet = origin.amountSet;
             amountHint = origin.amountHint;
             amountHintIsSet = origin.amountHintIsSet;
+            amountText = origin.amountText;
             comment = origin.comment;
             currency = origin.currency;
             amountValid = origin.amountValid;
@@ -1069,20 +1137,55 @@ public class NewTransactionModel extends ViewModel {
             isLast = origin.isLast;
             accountNameCursorPosition = origin.accountNameCursorPosition;
         }
-        public TransactionAccount(LedgerTransactionAccount account) {
-            super();
-            currency = account.getCurrency();
-            amount = account.getAmount();
-        }
         public TransactionAccount(String accountName) {
             super();
             this.accountName = accountName;
         }
-        public TransactionAccount(String accountName, String currency) {
+        public TransactionAccount(String accountName, @NotNull String currency) {
             super();
             this.accountName = accountName;
             this.currency = currency;
         }
+        public @NotNull String getAmountText() {
+            return amountText;
+        }
+        public void setAmountText(@NotNull String amountText) {
+            this.amountText = amountText;
+        }
+        public boolean setAndCheckAmountText(@NotNull String amountText) {
+            String amtText = amountText.trim();
+            this.amountText = amtText;
+
+            boolean significantChange = false;
+
+            if (amtText.isEmpty()) {
+                if (amountSet) {
+                    significantChange = true;
+                }
+                resetAmount();
+            }
+            else {
+                try {
+                    amtText = amtText.replace(Data.getDecimalSeparator(), Data.decimalDot);
+                    final float parsedAmount = Float.parseFloat(amtText);
+                    if (!amountSet || !amountValid || !Misc.equalFloats(parsedAmount, amount))
+                        significantChange = true;
+                    amount = parsedAmount;
+                    amountSet = true;
+                    amountValid = true;
+                }
+                catch (NumberFormatException e) {
+                    Logger.debug("new-trans", String.format(
+                            "assuming amount is not set due to number format exception. " +
+                            "input was '%s'", amtText));
+                    if (amountValid) // it was valid and now it's not
+                        significantChange = true;
+                    amountValid = false;
+                }
+            }
+
+            return significantChange;
+        }
         public boolean isLast() {
             return isLast;
         }
@@ -1103,9 +1206,13 @@ public class NewTransactionModel extends ViewModel {
         public void setAmount(float amount) {
             this.amount = amount;
             amountSet = true;
+            amountValid = true;
+            amountText = Data.formatNumber(amount);
         }
         public void resetAmount() {
             amountSet = false;
+            amountValid = true;
+            amountText = "";
         }
         @Override
         public ItemType getType() {
@@ -1124,11 +1231,12 @@ public class NewTransactionModel extends ViewModel {
         public void setComment(String comment) {
             this.comment = comment;
         }
+        @NotNull
         public String getCurrency() {
             return currency;
         }
-        public void setCurrency(String currency) {
-            this.currency = currency;
+        public void setCurrency(@org.jetbrains.annotations.Nullable String currency) {
+            this.currency = Misc.nullIsEmpty(currency);
         }
         public boolean isAmountValid() {
             return amountValid;
@@ -1154,6 +1262,7 @@ public class NewTransactionModel extends ViewModel {
         }
         @SuppressLint("DefaultLocale")
         @Override
+        @NotNull
         public String toString() {
             StringBuilder b = new StringBuilder();
             b.append(String.format("id:%d/%s", id, Integer.toHexString(hashCode())));
@@ -1161,9 +1270,13 @@ public class NewTransactionModel extends ViewModel {
                 b.append(String.format(" acc'%s'", accountName));
 
             if (amountSet)
-                b.append(String.format(" %4.2f", amount));
+                b.append(amountText)
+                 .append(" [")
+                 .append(amountValid ? "valid" : "invalid")
+                 .append("] ")
+                 .append(String.format(Locale.ROOT, " {raw %4.2f}", amount));
             else if (amountHintIsSet)
-                b.append(String.format(" (%s)", amountHint));
+                b.append(String.format(" (hint %s)", amountHint));
 
             if (!TextUtils.isEmpty(currency))
                 b.append(" ")
@@ -1183,7 +1296,9 @@ public class NewTransactionModel extends ViewModel {
 
             boolean equal = Misc.equalStrings(accountName, other.accountName);
             equal = equal && Misc.equalStrings(comment, other.comment) &&
-                    (amountSet ? other.amountSet && amount == other.amount : !other.amountSet);
+                    (amountSet ? other.amountSet && amountValid == other.amountValid &&
+                                 Misc.equalStrings(amountText, other.amountText)
+                               : !other.amountSet);
 
             // compare amount hint only if there is no amount
             if (!amountSet)
@@ -1193,8 +1308,7 @@ public class NewTransactionModel extends ViewModel {
             equal = equal && Misc.equalStrings(currency, other.currency) && isLast == other.isLast;
 
             Logger.debug("new-trans",
-                    String.format("Comparing {%s} and {%s}: %s", this.toString(), other.toString(),
-                            equal));
+                    String.format("Comparing {%s} and {%s}: %s", this, other, equal));
             return equal;
         }
         public int getAccountNameCursorPosition() {
@@ -1227,9 +1341,9 @@ public class NewTransactionModel extends ViewModel {
     }
 
     private static class ItemsForCurrency {
-        private final HashMap<String, List<Item>> hashMap = new HashMap<>();
+        private final HashMap<@NotNull String, List<Item>> hashMap = new HashMap<>();
         @NonNull
-        List<NewTransactionModel.Item> getList(@Nullable String currencyName) {
+        List<NewTransactionModel.Item> getList(@NotNull String currencyName) {
             List<NewTransactionModel.Item> list = hashMap.get(currencyName);
             if (list == null) {
                 list = new ArrayList<>();
@@ -1237,11 +1351,11 @@ public class NewTransactionModel extends ViewModel {
             }
             return list;
         }
-        void add(@Nullable String currencyName, @NonNull NewTransactionModel.Item item) {
-            getList(currencyName).add(item);
+        void add(@NotNull String currencyName, @NonNull NewTransactionModel.Item item) {
+            getList(Objects.requireNonNull(currencyName)).add(item);
         }
-        int size(@Nullable String currencyName) {
-            return this.getList(currencyName)
+        int size(@NotNull String currencyName) {
+            return this.getList(Objects.requireNonNull(currencyName))
                        .size();
         }
         Set<String> currencies() {