]> git.ktnx.net Git - mobile-ledger.git/blob - app/src/main/java/net/ktnx/mobileledger/ui/activity/NewTransactionItemsAdapter.java
5caa38a5581ae19d3b4fb403d5125a90e741ce0d
[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     void noteFocusIsOnTransactionComment(int position) {
375         model.noteFocusChanged(position, NewTransactionModel.FocusedElement.TransactionComment);
376     }
377     public void noteFocusIsOnDescription(int pos) {
378         model.noteFocusChanged(pos, NewTransactionModel.FocusedElement.Description);
379     }
380     private void holdSubmittableChecks() {
381         checkHoldCounter++;
382     }
383     private void releaseSubmittableChecks() {
384         if (checkHoldCounter == 0)
385             throw new RuntimeException("Asymmetrical call to releaseSubmittableChecks");
386         checkHoldCounter--;
387     }
388     void setItemCurrency(NewTransactionModel.Item item, Currency newCurrency) {
389         Currency oldCurrency = item.getCurrency();
390         if (!Currency.equal(newCurrency, oldCurrency)) {
391             holdSubmittableChecks();
392             try {
393                 item.setCurrency(newCurrency);
394 //                for (Item i : items) {
395 //                    if (Currency.equal(i.getCurrency(), oldCurrency))
396 //                        i.setCurrency(newCurrency);
397 //                }
398             }
399             finally {
400                 releaseSubmittableChecks();
401             }
402
403             checkTransactionSubmittable();
404         }
405     }
406     /*
407          A transaction is submittable if:
408          0) has description
409          1) has at least two account names
410          2) each row with amount has account name
411          3) for each commodity:
412          3a) amounts must balance to 0, or
413          3b) there must be exactly one empty amount (with account)
414          4) empty accounts with empty amounts are ignored
415          Side effects:
416          5) a row with an empty account name or empty amount is guaranteed to exist for each
417          commodity
418          6) at least two rows need to be present in the ledger
419
420         */
421     @SuppressLint("DefaultLocale")
422     void checkTransactionSubmittable() {
423         if (checkHoldCounter > 0)
424             return;
425
426         int accounts = 0;
427         final BalanceForCurrency balance = new BalanceForCurrency();
428         final String descriptionText = model.getDescription();
429         boolean submittable = true;
430         final ItemsForCurrency itemsForCurrency = new ItemsForCurrency();
431         final ItemsForCurrency itemsWithEmptyAmountForCurrency = new ItemsForCurrency();
432         final ItemsForCurrency itemsWithAccountAndEmptyAmountForCurrency = new ItemsForCurrency();
433         final ItemsForCurrency itemsWithEmptyAccountForCurrency = new ItemsForCurrency();
434         final ItemsForCurrency itemsWithAmountForCurrency = new ItemsForCurrency();
435         final ItemsForCurrency itemsWithAccountForCurrency = new ItemsForCurrency();
436         final ItemsForCurrency emptyRowsForCurrency = new ItemsForCurrency();
437         final List<NewTransactionModel.Item> emptyRows = new ArrayList<>();
438
439         try {
440             if ((descriptionText == null) || descriptionText.trim()
441                                                             .isEmpty())
442             {
443                 Logger.debug("submittable", "Transaction not submittable: missing description");
444                 submittable = false;
445             }
446
447             for (int i = 0; i < model.items.size(); i++) {
448                 NewTransactionModel.Item item = model.items.get(i);
449
450                 LedgerTransactionAccount acc = item.getAccount();
451                 String acc_name = acc.getAccountName()
452                                      .trim();
453                 String currName = acc.getCurrency();
454
455                 itemsForCurrency.add(currName, item);
456
457                 if (acc_name.isEmpty()) {
458                     itemsWithEmptyAccountForCurrency.add(currName, item);
459
460                     if (acc.isAmountSet()) {
461                         // 2) each amount has account name
462                         Logger.debug("submittable", String.format(
463                                 "Transaction not submittable: row %d has no account name, but" +
464                                 " has" + " amount %1.2f", i + 1, acc.getAmount()));
465                         submittable = false;
466                     }
467                     else {
468                         emptyRowsForCurrency.add(currName, item);
469                     }
470                 }
471                 else {
472                     accounts++;
473                     itemsWithAccountForCurrency.add(currName, item);
474                 }
475
476                 if (acc.isAmountSet()) {
477                     itemsWithAmountForCurrency.add(currName, item);
478                     balance.add(currName, acc.getAmount());
479                 }
480                 else {
481                     itemsWithEmptyAmountForCurrency.add(currName, item);
482
483                     if (!acc_name.isEmpty())
484                         itemsWithAccountAndEmptyAmountForCurrency.add(currName, item);
485                 }
486             }
487
488             // 1) has at least two account names
489             if (accounts < 2) {
490                 if (accounts == 0)
491                     Logger.debug("submittable",
492                             "Transaction not submittable: no account " + "names");
493                 else if (accounts == 1)
494                     Logger.debug("submittable",
495                             "Transaction not submittable: only one account name");
496                 else
497                     Logger.debug("submittable",
498                             String.format("Transaction not submittable: only %d account names",
499                                     accounts));
500                 submittable = false;
501             }
502
503             // 3) for each commodity:
504             // 3a) amount must balance to 0, or
505             // 3b) there must be exactly one empty amount (with account)
506             for (String balCurrency : itemsForCurrency.currencies()) {
507                 float currencyBalance = balance.get(balCurrency);
508                 if (Misc.isZero(currencyBalance)) {
509                     // remove hints from all amount inputs in that currency
510                     for (NewTransactionModel.Item item : model.items) {
511                         if (Currency.equal(item.getCurrency(), balCurrency))
512                             item.setAmountHint(null);
513                     }
514                 }
515                 else {
516                     List<NewTransactionModel.Item> list =
517                             itemsWithAccountAndEmptyAmountForCurrency.getList(balCurrency);
518                     int balanceReceiversCount = list.size();
519                     if (balanceReceiversCount != 1) {
520                         if (BuildConfig.DEBUG) {
521                             if (balanceReceiversCount == 0)
522                                 Logger.debug("submittable", String.format(
523                                         "Transaction not submittable [%s]: non-zero balance " +
524                                         "with no empty amounts with accounts", balCurrency));
525                             else
526                                 Logger.debug("submittable", String.format(
527                                         "Transaction not submittable [%s]: non-zero balance " +
528                                         "with multiple empty amounts with accounts", balCurrency));
529                         }
530                         submittable = false;
531                     }
532
533                     List<NewTransactionModel.Item> emptyAmountList =
534                             itemsWithEmptyAmountForCurrency.getList(balCurrency);
535
536                     // suggest off-balance amount to a row and remove hints on other rows
537                     NewTransactionModel.Item receiver = null;
538                     if (!list.isEmpty())
539                         receiver = list.get(0);
540                     else if (!emptyAmountList.isEmpty())
541                         receiver = emptyAmountList.get(0);
542
543                     for (NewTransactionModel.Item item : model.items) {
544                         if (!Currency.equal(item.getCurrency(), balCurrency))
545                             continue;
546
547                         if (item.equals(receiver)) {
548                             if (BuildConfig.DEBUG)
549                                 Logger.debug("submittable",
550                                         String.format("Setting amount hint to %1.2f [%s]",
551                                                 -currencyBalance, balCurrency));
552                             item.setAmountHint(String.format("%1.2f", -currencyBalance));
553                         }
554                         else {
555                             if (BuildConfig.DEBUG)
556                                 Logger.debug("submittable",
557                                         String.format("Resetting hint of '%s' [%s]",
558                                                 (item.getAccount() == null) ? "" : item.getAccount()
559                                                                                        .getAccountName(),
560                                                 balCurrency));
561                             item.setAmountHint(null);
562                         }
563                     }
564                 }
565             }
566
567             // 5) a row with an empty account name or empty amount is guaranteed to exist for
568             // each commodity
569             for (String balCurrency : balance.currencies()) {
570                 int currEmptyRows = itemsWithEmptyAccountForCurrency.size(balCurrency);
571                 int currRows = itemsForCurrency.size(balCurrency);
572                 int currAccounts = itemsWithAccountForCurrency.size(balCurrency);
573                 int currAmounts = itemsWithAmountForCurrency.size(balCurrency);
574                 if ((currEmptyRows == 0) &&
575                     ((currRows == currAccounts) || (currRows == currAmounts)))
576                 {
577                     // perhaps there already is an unused empty row for another currency that
578                     // is not used?
579 //                        boolean foundIt = false;
580 //                        for (Item item : emptyRows) {
581 //                            Currency itemCurrency = item.getCurrency();
582 //                            String itemCurrencyName =
583 //                                    (itemCurrency == null) ? "" : itemCurrency.getName();
584 //                            if (Misc.isZero(balance.get(itemCurrencyName))) {
585 //                                item.setCurrency(Currency.loadByName(balCurrency));
586 //                                item.setAmountHint(
587 //                                        String.format("%1.2f", -balance.get(balCurrency)));
588 //                                foundIt = true;
589 //                                break;
590 //                            }
591 //                        }
592 //
593 //                        if (!foundIt)
594                     addRow(balCurrency);
595                 }
596             }
597
598             // drop extra empty rows, not needed
599             for (String currName : emptyRowsForCurrency.currencies()) {
600                 List<NewTransactionModel.Item> emptyItems = emptyRowsForCurrency.getList(currName);
601                 while ((model.items.size() > 2) && (emptyItems.size() > 1)) {
602                     NewTransactionModel.Item item = emptyItems.get(1);
603                     emptyItems.remove(1);
604                     model.removeRow(item, this);
605                 }
606
607                 // unused currency, remove last item (which is also an empty one)
608                 if ((model.items.size() > 2) && (emptyItems.size() == 1)) {
609                     List<NewTransactionModel.Item> currItems = itemsForCurrency.getList(currName);
610
611                     if (currItems.size() == 1) {
612                         NewTransactionModel.Item item = emptyItems.get(0);
613                         model.removeRow(item, this);
614                     }
615                 }
616             }
617
618             // 6) at least two rows need to be present in the ledger
619             while (model.items.size() < 2)
620                 addRow();
621
622
623             debug("submittable", submittable ? "YES" : "NO");
624             model.isSubmittable.setValue(submittable);
625
626             if (BuildConfig.DEBUG) {
627                 debug("submittable", "== Dump of all items");
628                 for (int i = 0; i < model.items.size(); i++) {
629                     NewTransactionModel.Item item = model.items.get(i);
630                     LedgerTransactionAccount acc = item.getAccount();
631                     debug("submittable", String.format("Item %2d: [%4.2f(%s) %s] %s ; %s", i,
632                             acc.isAmountSet() ? acc.getAmount() : 0,
633                             item.isAmountHintSet() ? item.getAmountHint() : "ø", acc.getCurrency(),
634                             acc.getAccountName(), acc.getComment()));
635                 }
636             }
637         }
638         catch (NumberFormatException e) {
639             debug("submittable", "NO (because of NumberFormatException)");
640             model.isSubmittable.setValue(false);
641         }
642         catch (Exception e) {
643             e.printStackTrace();
644             debug("submittable", "NO (because of an Exception)");
645             model.isSubmittable.setValue(false);
646         }
647     }
648
649     private static class BalanceForCurrency {
650         private HashMap<String, Float> hashMap = new HashMap<>();
651         float get(String currencyName) {
652             Float f = hashMap.get(currencyName);
653             if (f == null) {
654                 f = 0f;
655                 hashMap.put(currencyName, f);
656             }
657             return f;
658         }
659         void add(String currencyName, float amount) {
660             hashMap.put(currencyName, get(currencyName) + amount);
661         }
662         Set<String> currencies() {
663             return hashMap.keySet();
664         }
665         boolean containsCurrency(String currencyName) {
666             return hashMap.containsKey(currencyName);
667         }
668     }
669
670     private static class ItemsForCurrency {
671         private HashMap<String, List<NewTransactionModel.Item>> hashMap = new HashMap<>();
672         @NonNull
673         List<NewTransactionModel.Item> getList(@Nullable String currencyName) {
674             List<NewTransactionModel.Item> list = hashMap.get(currencyName);
675             if (list == null) {
676                 list = new ArrayList<>();
677                 hashMap.put(currencyName, list);
678             }
679             return list;
680         }
681         void add(@Nullable String currencyName, @NonNull NewTransactionModel.Item item) {
682             getList(currencyName).add(item);
683         }
684         int size(@Nullable String currencyName) {
685             return this.getList(currencyName)
686                        .size();
687         }
688         Set<String> currencies() {
689             return hashMap.keySet();
690         }
691     }
692 }