]> git.ktnx.net Git - mobile-ledger.git/blob - app/src/main/java/net/ktnx/mobileledger/ui/activity/NewTransactionItemsAdapter.java
show a message when no transactions are found containing the preferred account name
[mobile-ledger.git] / app / src / main / java / net / ktnx / mobileledger / ui / activity / NewTransactionItemsAdapter.java
1 /*
2  * Copyright © 2019 Damyan Ivanov.
3  * This file is part of MoLe.
4  * MoLe is free software: you can distribute it and/or modify it
5  * under the term of the GNU General Public License as published by
6  * the Free Software Foundation, either version 3 of the License, or
7  * (at your opinion), any later version.
8  *
9  * MoLe is distributed in the hope that it will be useful,
10  * but WITHOUT ANY WARRANTY; without even the implied warranty of
11  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12  * GNU General Public License terms for details.
13  *
14  * You should have received a copy of the GNU General Public License
15  * along with MoLe. If not, see <https://www.gnu.org/licenses/>.
16  */
17
18 package net.ktnx.mobileledger.ui.activity;
19
20 import android.annotation.SuppressLint;
21 import android.app.Activity;
22 import android.database.Cursor;
23 import android.view.LayoutInflater;
24 import android.view.ViewGroup;
25 import android.widget.LinearLayout;
26
27 import androidx.annotation.NonNull;
28 import androidx.annotation.Nullable;
29 import androidx.recyclerview.widget.ItemTouchHelper;
30 import androidx.recyclerview.widget.RecyclerView;
31
32 import com.google.android.material.snackbar.Snackbar;
33
34 import net.ktnx.mobileledger.BuildConfig;
35 import net.ktnx.mobileledger.R;
36 import net.ktnx.mobileledger.async.DescriptionSelectedCallback;
37 import net.ktnx.mobileledger.model.Currency;
38 import net.ktnx.mobileledger.model.Data;
39 import net.ktnx.mobileledger.model.LedgerTransaction;
40 import net.ktnx.mobileledger.model.LedgerTransactionAccount;
41 import net.ktnx.mobileledger.model.MobileLedgerProfile;
42 import net.ktnx.mobileledger.utils.Logger;
43 import net.ktnx.mobileledger.utils.MLDB;
44 import net.ktnx.mobileledger.utils.Misc;
45
46 import java.util.ArrayList;
47 import java.util.HashMap;
48 import java.util.List;
49 import java.util.Locale;
50 import java.util.Set;
51
52 import static net.ktnx.mobileledger.utils.Logger.debug;
53
54 class NewTransactionItemsAdapter extends RecyclerView.Adapter<NewTransactionItemHolder>
55         implements DescriptionSelectedCallback {
56     private NewTransactionModel model;
57     private MobileLedgerProfile mProfile;
58     private ItemTouchHelper touchHelper;
59     private RecyclerView recyclerView;
60     private int checkHoldCounter = 0;
61     NewTransactionItemsAdapter(NewTransactionModel viewModel, MobileLedgerProfile profile) {
62         super();
63         model = viewModel;
64         mProfile = profile;
65         int size = model.getAccountCount();
66         while (size < 2) {
67             Logger.debug("new-transaction",
68                     String.format(Locale.US, "%d accounts is too little, Calling addRow()", size));
69             size = addRow();
70         }
71
72         NewTransactionItemsAdapter adapter = this;
73
74         touchHelper = new ItemTouchHelper(new ItemTouchHelper.Callback() {
75             @Override
76             public boolean isLongPressDragEnabled() {
77                 return true;
78             }
79             @Override
80             public boolean canDropOver(@NonNull RecyclerView recyclerView,
81                                        @NonNull RecyclerView.ViewHolder current,
82                                        @NonNull RecyclerView.ViewHolder target) {
83                 final int adapterPosition = target.getAdapterPosition();
84
85                 // first and last items are immovable
86                 if (adapterPosition == 0)
87                     return false;
88                 if (adapterPosition == adapter.getItemCount() - 1)
89                     return false;
90
91                 return super.canDropOver(recyclerView, current, target);
92             }
93             @Override
94             public int getMovementFlags(@NonNull RecyclerView recyclerView,
95                                         @NonNull RecyclerView.ViewHolder viewHolder) {
96                 int flags = makeFlag(ItemTouchHelper.ACTION_STATE_IDLE, ItemTouchHelper.END);
97                 // the top (date and description) and the bottom (padding) items are always there
98                 final int adapterPosition = viewHolder.getAdapterPosition();
99                 if ((adapterPosition > 0) && (adapterPosition < adapter.getItemCount() - 1)) {
100                     flags |= makeFlag(ItemTouchHelper.ACTION_STATE_DRAG,
101                             ItemTouchHelper.UP | ItemTouchHelper.DOWN) |
102                              makeFlag(ItemTouchHelper.ACTION_STATE_SWIPE,
103                                      ItemTouchHelper.START | ItemTouchHelper.END);
104                 }
105
106                 return flags;
107             }
108             @Override
109             public boolean onMove(@NonNull RecyclerView recyclerView,
110                                   @NonNull RecyclerView.ViewHolder viewHolder,
111                                   @NonNull RecyclerView.ViewHolder target) {
112
113                 model.swapItems(viewHolder.getAdapterPosition(), target.getAdapterPosition());
114                 notifyItemMoved(viewHolder.getAdapterPosition(), target.getAdapterPosition());
115                 return true;
116             }
117             @Override
118             public void onSwiped(@NonNull RecyclerView.ViewHolder viewHolder, int direction) {
119                 int pos = viewHolder.getAdapterPosition();
120                 viewModel.removeItem(pos - 1);
121                 notifyItemRemoved(pos);
122                 viewModel.sendCountNotifications(); // needed after items re-arrangement
123                 checkTransactionSubmittable();
124             }
125         });
126     }
127     public void setProfile(MobileLedgerProfile profile) {
128         mProfile = profile;
129     }
130     private int addRow() {
131         return addRow(null);
132     }
133     private int addRow(String commodity) {
134         final int newAccountCount = model.addAccount(new LedgerTransactionAccount("", commodity));
135         Logger.debug("new-transaction",
136                 String.format(Locale.US, "invoking notifyItemInserted(%d)", newAccountCount));
137         // the header is at position 0
138         notifyItemInserted(newAccountCount);
139         model.sendCountNotifications(); // needed after holders' positions have changed
140         return newAccountCount;
141     }
142     @NonNull
143     @Override
144     public NewTransactionItemHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
145         LinearLayout row = (LinearLayout) LayoutInflater.from(parent.getContext())
146                                                         .inflate(R.layout.new_transaction_row,
147                                                                 parent, false);
148
149         return new NewTransactionItemHolder(row, this);
150     }
151     @Override
152     public void onBindViewHolder(@NonNull NewTransactionItemHolder holder, int position) {
153         Logger.debug("bind", String.format(Locale.US, "Binding item at position %d", position));
154         NewTransactionModel.Item item = model.getItem(position);
155         holder.setData(item);
156         Logger.debug("bind", String.format(Locale.US, "Bound %s item at position %d", item.getType()
157                                                                                           .toString(),
158                 position));
159     }
160     @Override
161     public int getItemCount() {
162         return model.getAccountCount() + 2;
163     }
164     private boolean accountListIsEmpty() {
165         for (int i = 0; i < model.getAccountCount(); i++) {
166             LedgerTransactionAccount acc = model.getAccount(i);
167             if (!acc.getAccountName()
168                     .isEmpty())
169                 return false;
170             if (acc.isAmountSet())
171                 return false;
172         }
173
174         return true;
175     }
176     @Override
177     public void onAttachedToRecyclerView(@NonNull RecyclerView recyclerView) {
178         super.onAttachedToRecyclerView(recyclerView);
179         this.recyclerView = recyclerView;
180         touchHelper.attachToRecyclerView(recyclerView);
181     }
182     @Override
183     public void onDetachedFromRecyclerView(@NonNull RecyclerView recyclerView) {
184         touchHelper.attachToRecyclerView(null);
185         super.onDetachedFromRecyclerView(recyclerView);
186         this.recyclerView = null;
187     }
188     public void descriptionSelected(String description) {
189         debug("descr selected", description);
190         if (!accountListIsEmpty())
191             return;
192
193         String accFilter = mProfile.getPreferredAccountsFilter();
194
195         ArrayList<String> params = new ArrayList<>();
196         StringBuilder sb = new StringBuilder("select t.profile, t.id from transactions t");
197
198         if (!Misc.isEmptyOrNull(accFilter)) {
199             sb.append(" JOIN transaction_accounts ta")
200               .append(" ON ta.profile = t.profile")
201               .append(" AND ta.transaction_id = t.id");
202         }
203
204         sb.append(" WHERE t.description=?");
205         params.add(description);
206
207         if (!Misc.isEmptyOrNull(accFilter)) {
208             sb.append(" AND ta.account_name LIKE '%'||?||'%'");
209             params.add(accFilter);
210         }
211
212         sb.append(" ORDER BY t.date DESC LIMIT 1");
213
214         final String sql = sb.toString();
215         debug("descr", sql);
216         debug("descr", params.toString());
217
218         Activity activity = (Activity) recyclerView.getContext();
219         // FIXME: handle exceptions?
220         MLDB.queryInBackground(sql, params.toArray(new String[]{}), new MLDB.CallbackHelper() {
221             @Override
222             public void onStart() {
223                 model.incrementBusyCounter();
224             }
225             @Override
226             public void onDone() {
227                 model.decrementBusyCounter();
228             }
229             @Override
230             public boolean onRow(@NonNull Cursor cursor) {
231                 final String profileUUID = cursor.getString(0);
232                 final int transactionId = cursor.getInt(1);
233                 activity.runOnUiThread(() -> loadTransactionIntoModel(profileUUID, transactionId));
234                 return false; // limit 1, by the way
235             }
236             @Override
237             public void onNoRows() {
238                 if (Misc.isEmptyOrNull(accFilter))
239                     return;
240
241                 debug("descr", "Trying transaction search without preferred account filter");
242
243                 final String broaderSql =
244                         "select t.profile, t.id from transactions t where t.description=?" +
245                         " ORDER BY date desc LIMIT 1";
246                 params.remove(1);
247                 debug("descr", broaderSql);
248                 debug("descr", description);
249
250                 activity.runOnUiThread(() -> {
251                     Snackbar.make(recyclerView, R.string.ignoring_preferred_account,
252                             Snackbar.LENGTH_LONG)
253                             .show();
254                 });
255
256                 MLDB.queryInBackground(broaderSql, new String[]{description},
257                         new MLDB.CallbackHelper() {
258                             @Override
259                             public void onStart() {
260                                 model.incrementBusyCounter();
261                             }
262                             @Override
263                             public boolean onRow(@NonNull Cursor cursor) {
264                                 final String profileUUID = cursor.getString(0);
265                                 final int transactionId = cursor.getInt(1);
266                                 activity.runOnUiThread(
267                                         () -> loadTransactionIntoModel(profileUUID, transactionId));
268                                 return false;
269                             }
270                             @Override
271                             public void onDone() {
272                                 model.decrementBusyCounter();
273                             }
274                         });
275             }
276         });
277     }
278     private void loadTransactionIntoModel(String profileUUID, int transactionId) {
279         LedgerTransaction tr;
280         MobileLedgerProfile profile = Data.getProfile(profileUUID);
281         if (profile == null)
282             throw new RuntimeException(String.format(
283                     "Unable to find profile %s, which is supposed to contain transaction %d",
284                     profileUUID, transactionId));
285
286         tr = profile.loadTransaction(transactionId);
287         ArrayList<LedgerTransactionAccount> accounts = tr.getAccounts();
288         NewTransactionModel.Item firstNegative = null;
289         NewTransactionModel.Item firstPositive = null;
290         int singleNegativeIndex = -1;
291         int singlePositiveIndex = -1;
292         int negativeCount = 0;
293         for (int i = 0; i < accounts.size(); i++) {
294             LedgerTransactionAccount acc = accounts.get(i);
295             NewTransactionModel.Item item;
296             if (model.getAccountCount() < i + 1) {
297                 model.addAccount(acc);
298                 notifyItemInserted(i + 1);
299             }
300             item = model.getItem(i + 1);
301
302             item.getAccount()
303                 .setAccountName(acc.getAccountName());
304             item.setComment(acc.getComment());
305             if (acc.isAmountSet()) {
306                 item.getAccount()
307                     .setAmount(acc.getAmount());
308                 if (acc.getAmount() < 0) {
309                     if (firstNegative == null) {
310                         firstNegative = item;
311                         singleNegativeIndex = i;
312                     }
313                     else
314                         singleNegativeIndex = -1;
315                 }
316                 else {
317                     if (firstPositive == null) {
318                         firstPositive = item;
319                         singlePositiveIndex = i;
320                     }
321                     else
322                         singlePositiveIndex = -1;
323                 }
324             }
325             else
326                 item.getAccount()
327                     .resetAmount();
328             notifyItemChanged(i + 1);
329         }
330
331         if (singleNegativeIndex != -1) {
332             firstNegative.getAccount()
333                          .resetAmount();
334             model.moveItemLast(singleNegativeIndex);
335         }
336         else if (singlePositiveIndex != -1) {
337             firstPositive.getAccount()
338                          .resetAmount();
339             model.moveItemLast(singlePositiveIndex);
340         }
341
342         checkTransactionSubmittable();
343         model.setFocusedItem(1);
344     }
345     public void toggleAllEditing(boolean editable) {
346         // item 0 is the header
347         for (int i = 0; i <= model.getAccountCount(); i++) {
348             model.getItem(i)
349                  .setEditable(editable);
350             notifyItemChanged(i);
351             // TODO perhaps do only one notification about the whole range (notifyDatasetChanged)?
352         }
353     }
354     void reset() {
355         int presentItemCount = model.getAccountCount();
356         model.reset();
357         notifyItemChanged(0);       // header changed
358         notifyItemRangeChanged(1, 2);    // the two empty rows
359         if (presentItemCount > 2)
360             notifyItemRangeRemoved(3, presentItemCount - 2); // all the rest are gone
361     }
362     void updateFocusedItem(int position) {
363         model.updateFocusedItem(position);
364     }
365     void noteFocusIsOnAccount(int position) {
366         model.noteFocusChanged(position, NewTransactionModel.FocusedElement.Account);
367     }
368     void noteFocusIsOnAmount(int position) {
369         model.noteFocusChanged(position, NewTransactionModel.FocusedElement.Amount);
370     }
371     void noteFocusIsOnComment(int position) {
372         model.noteFocusChanged(position, NewTransactionModel.FocusedElement.Comment);
373     }
374     private void holdSubmittableChecks() {
375         checkHoldCounter++;
376     }
377     private void releaseSubmittableChecks() {
378         if (checkHoldCounter == 0)
379             throw new RuntimeException("Asymmetrical call to releaseSubmittableChecks");
380         checkHoldCounter--;
381     }
382     void setItemCurrency(NewTransactionModel.Item item, Currency newCurrency) {
383         Currency oldCurrency = item.getCurrency();
384         if (!Currency.equal(newCurrency, oldCurrency)) {
385             holdSubmittableChecks();
386             try {
387                 item.setCurrency(newCurrency);
388 //                for (Item i : items) {
389 //                    if (Currency.equal(i.getCurrency(), oldCurrency))
390 //                        i.setCurrency(newCurrency);
391 //                }
392             }
393             finally {
394                 releaseSubmittableChecks();
395             }
396
397             checkTransactionSubmittable();
398         }
399     }
400     /*
401          A transaction is submittable if:
402          0) has description
403          1) has at least two account names
404          2) each row with amount has account name
405          3) for each commodity:
406          3a) amounts must balance to 0, or
407          3b) there must be exactly one empty amount (with account)
408          4) empty accounts with empty amounts are ignored
409          Side effects:
410          5) a row with an empty account name or empty amount is guaranteed to exist for each
411          commodity
412          6) at least two rows need to be present in the ledger
413
414         */
415     @SuppressLint("DefaultLocale")
416     void checkTransactionSubmittable() {
417         if (checkHoldCounter > 0)
418             return;
419
420         int accounts = 0;
421         final BalanceForCurrency balance = new BalanceForCurrency();
422         final String descriptionText = model.getDescription();
423         boolean submittable = true;
424         final ItemsForCurrency itemsForCurrency = new ItemsForCurrency();
425         final ItemsForCurrency itemsWithEmptyAmountForCurrency = new ItemsForCurrency();
426         final ItemsForCurrency itemsWithAccountAndEmptyAmountForCurrency = new ItemsForCurrency();
427         final ItemsForCurrency itemsWithEmptyAccountForCurrency = new ItemsForCurrency();
428         final ItemsForCurrency itemsWithAmountForCurrency = new ItemsForCurrency();
429         final ItemsForCurrency itemsWithAccountForCurrency = new ItemsForCurrency();
430         final ItemsForCurrency emptyRowsForCurrency = new ItemsForCurrency();
431         final List<NewTransactionModel.Item> emptyRows = new ArrayList<>();
432
433         try {
434             if ((descriptionText == null) || descriptionText.trim()
435                                                             .isEmpty())
436             {
437                 Logger.debug("submittable", "Transaction not submittable: missing description");
438                 submittable = false;
439             }
440
441             for (int i = 0; i < model.items.size(); i++) {
442                 NewTransactionModel.Item item = model.items.get(i);
443
444                 LedgerTransactionAccount acc = item.getAccount();
445                 String acc_name = acc.getAccountName()
446                                      .trim();
447                 String currName = acc.getCurrency();
448
449                 itemsForCurrency.add(currName, item);
450
451                 if (acc_name.isEmpty()) {
452                     itemsWithEmptyAccountForCurrency.add(currName, item);
453
454                     if (acc.isAmountSet()) {
455                         // 2) each amount has account name
456                         Logger.debug("submittable", String.format(
457                                 "Transaction not submittable: row %d has no account name, but" +
458                                 " has" + " amount %1.2f", i + 1, acc.getAmount()));
459                         submittable = false;
460                     }
461                     else {
462                         emptyRowsForCurrency.add(currName, item);
463                     }
464                 }
465                 else {
466                     accounts++;
467                     itemsWithAccountForCurrency.add(currName, item);
468                 }
469
470                 if (acc.isAmountSet()) {
471                     itemsWithAmountForCurrency.add(currName, item);
472                     balance.add(currName, acc.getAmount());
473                 }
474                 else {
475                     itemsWithEmptyAmountForCurrency.add(currName, item);
476
477                     if (!acc_name.isEmpty())
478                         itemsWithAccountAndEmptyAmountForCurrency.add(currName, item);
479                 }
480             }
481
482             // 1) has at least two account names
483             if (accounts < 2) {
484                 if (accounts == 0)
485                     Logger.debug("submittable",
486                             "Transaction not submittable: no account " + "names");
487                 else if (accounts == 1)
488                     Logger.debug("submittable",
489                             "Transaction not submittable: only one account name");
490                 else
491                     Logger.debug("submittable",
492                             String.format("Transaction not submittable: only %d account names",
493                                     accounts));
494                 submittable = false;
495             }
496
497             // 3) for each commodity:
498             // 3a) amount must balance to 0, or
499             // 3b) there must be exactly one empty amount (with account)
500             for (String balCurrency : itemsForCurrency.currencies()) {
501                 float currencyBalance = balance.get(balCurrency);
502                 if (Misc.isZero(currencyBalance)) {
503                     // remove hints from all amount inputs in that currency
504                     for (NewTransactionModel.Item item : model.items) {
505                         if (Currency.equal(item.getCurrency(), balCurrency))
506                             item.setAmountHint(null);
507                     }
508                 }
509                 else {
510                     List<NewTransactionModel.Item> list =
511                             itemsWithAccountAndEmptyAmountForCurrency.getList(balCurrency);
512                     int balanceReceiversCount = list.size();
513                     if (balanceReceiversCount != 1) {
514                         if (BuildConfig.DEBUG) {
515                             if (balanceReceiversCount == 0)
516                                 Logger.debug("submittable", String.format(
517                                         "Transaction not submittable [%s]: non-zero balance " +
518                                         "with no empty amounts with accounts", balCurrency));
519                             else
520                                 Logger.debug("submittable", String.format(
521                                         "Transaction not submittable [%s]: non-zero balance " +
522                                         "with multiple empty amounts with accounts", balCurrency));
523                         }
524                         submittable = false;
525                     }
526
527                     List<NewTransactionModel.Item> emptyAmountList =
528                             itemsWithEmptyAmountForCurrency.getList(balCurrency);
529
530                     // suggest off-balance amount to a row and remove hints on other rows
531                     NewTransactionModel.Item receiver = null;
532                     if (!list.isEmpty())
533                         receiver = list.get(0);
534                     else if (!emptyAmountList.isEmpty())
535                         receiver = emptyAmountList.get(0);
536
537                     for (NewTransactionModel.Item item : model.items) {
538                         if (!Currency.equal(item.getCurrency(), balCurrency))
539                             continue;
540
541                         if (item.equals(receiver)) {
542                             if (BuildConfig.DEBUG)
543                                 Logger.debug("submittable",
544                                         String.format("Setting amount hint to %1.2f [%s]",
545                                                 -currencyBalance, balCurrency));
546                             item.setAmountHint(String.format("%1.2f", -currencyBalance));
547                         }
548                         else {
549                             if (BuildConfig.DEBUG)
550                                 Logger.debug("submittable",
551                                         String.format("Resetting hint of '%s' [%s]",
552                                                 (item.getAccount() == null) ? "" : item.getAccount()
553                                                                                        .getAccountName(),
554                                                 balCurrency));
555                             item.setAmountHint(null);
556                         }
557                     }
558                 }
559             }
560
561             // 5) a row with an empty account name or empty amount is guaranteed to exist for
562             // each commodity
563             for (String balCurrency : balance.currencies()) {
564                 int currEmptyRows = itemsWithEmptyAccountForCurrency.size(balCurrency);
565                 int currRows = itemsForCurrency.size(balCurrency);
566                 int currAccounts = itemsWithAccountForCurrency.size(balCurrency);
567                 int currAmounts = itemsWithAmountForCurrency.size(balCurrency);
568                 if ((currEmptyRows == 0) &&
569                     ((currRows == currAccounts) || (currRows == currAmounts)))
570                 {
571                     // perhaps there already is an unused empty row for another currency that
572                     // is not used?
573 //                        boolean foundIt = false;
574 //                        for (Item item : emptyRows) {
575 //                            Currency itemCurrency = item.getCurrency();
576 //                            String itemCurrencyName =
577 //                                    (itemCurrency == null) ? "" : itemCurrency.getName();
578 //                            if (Misc.isZero(balance.get(itemCurrencyName))) {
579 //                                item.setCurrency(Currency.loadByName(balCurrency));
580 //                                item.setAmountHint(
581 //                                        String.format("%1.2f", -balance.get(balCurrency)));
582 //                                foundIt = true;
583 //                                break;
584 //                            }
585 //                        }
586 //
587 //                        if (!foundIt)
588                     addRow(balCurrency);
589                 }
590             }
591
592             // drop extra empty rows, not needed
593             for (String currName : emptyRowsForCurrency.currencies()) {
594                 List<NewTransactionModel.Item> emptyItems = emptyRowsForCurrency.getList(currName);
595                 while ((model.items.size() > 2) && (emptyItems.size() > 1)) {
596                     NewTransactionModel.Item item = emptyItems.get(1);
597                     emptyItems.remove(1);
598                     model.removeRow(item, this);
599                 }
600
601                 // unused currency, remove last item (which is also an empty one)
602                 if ((model.items.size() > 2) && (emptyItems.size() == 1)) {
603                     List<NewTransactionModel.Item> currItems = itemsForCurrency.getList(currName);
604
605                     if (currItems.size() == 1) {
606                         NewTransactionModel.Item item = emptyItems.get(0);
607                         model.removeRow(item, this);
608                     }
609                 }
610             }
611
612             // 6) at least two rows need to be present in the ledger
613             while (model.items.size() < 2)
614                 addRow();
615
616
617             debug("submittable", submittable ? "YES" : "NO");
618             model.isSubmittable.setValue(submittable);
619
620             if (BuildConfig.DEBUG) {
621                 debug("submittable", "== Dump of all items");
622                 for (int i = 0; i < model.items.size(); i++) {
623                     NewTransactionModel.Item item = model.items.get(i);
624                     LedgerTransactionAccount acc = item.getAccount();
625                     debug("submittable", String.format("Item %2d: [%4.2f(%s) %s] %s ; %s", i,
626                             acc.isAmountSet() ? acc.getAmount() : 0,
627                             item.isAmountHintSet() ? item.getAmountHint() : "ø", acc.getCurrency(),
628                             acc.getAccountName(), acc.getComment()));
629                 }
630             }
631         }
632         catch (NumberFormatException e) {
633             debug("submittable", "NO (because of NumberFormatException)");
634             model.isSubmittable.setValue(false);
635         }
636         catch (Exception e) {
637             e.printStackTrace();
638             debug("submittable", "NO (because of an Exception)");
639             model.isSubmittable.setValue(false);
640         }
641     }
642
643     private static class BalanceForCurrency {
644         private HashMap<String, Float> hashMap = new HashMap<>();
645         float get(String currencyName) {
646             Float f = hashMap.get(currencyName);
647             if (f == null) {
648                 f = 0f;
649                 hashMap.put(currencyName, f);
650             }
651             return f;
652         }
653         void add(String currencyName, float amount) {
654             hashMap.put(currencyName, get(currencyName) + amount);
655         }
656         Set<String> currencies() {
657             return hashMap.keySet();
658         }
659         boolean containsCurrency(String currencyName) {
660             return hashMap.containsKey(currencyName);
661         }
662     }
663
664     private static class ItemsForCurrency {
665         private HashMap<String, List<NewTransactionModel.Item>> hashMap = new HashMap<>();
666         @NonNull
667         List<NewTransactionModel.Item> getList(@Nullable String currencyName) {
668             List<NewTransactionModel.Item> list = hashMap.get(currencyName);
669             if (list == null) {
670                 list = new ArrayList<>();
671                 hashMap.put(currencyName, list);
672             }
673             return list;
674         }
675         void add(@Nullable String currencyName, @NonNull NewTransactionModel.Item item) {
676             getList(currencyName).add(item);
677         }
678         int size(@Nullable String currencyName) {
679             return this.getList(currencyName)
680                        .size();
681         }
682         Set<String> currencies() {
683             return hashMap.keySet();
684         }
685     }
686 }