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