]> git.ktnx.net Git - mobile-ledger.git/blob - app/src/main/java/net/ktnx/mobileledger/ui/activity/NewTransactionItemsAdapter.java
more pronounced day/month delimiters in the transaction list
[mobile-ledger.git] / app / src / main / java / net / ktnx / mobileledger / ui / activity / NewTransactionItemsAdapter.java
1 /*
2  * Copyright © 2020 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.year desc, t.month desc, t.day 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 year desc, month desc, day 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.isAmountValid()) {
477                     Logger.debug("submittable",
478                             String.format("Not submittable: row %d has an invalid amount", i + 1));
479                     submittable = false;
480                 }
481                 else if (acc.isAmountSet()) {
482                     itemsWithAmountForCurrency.add(currName, item);
483                     balance.add(currName, acc.getAmount());
484                 }
485                 else {
486                     itemsWithEmptyAmountForCurrency.add(currName, item);
487
488                     if (!acc_name.isEmpty())
489                         itemsWithAccountAndEmptyAmountForCurrency.add(currName, item);
490                 }
491             }
492
493             // 1) has at least two account names
494             if (accounts < 2) {
495                 if (accounts == 0)
496                     Logger.debug("submittable",
497                             "Transaction not submittable: no account " + "names");
498                 else if (accounts == 1)
499                     Logger.debug("submittable",
500                             "Transaction not submittable: only one account name");
501                 else
502                     Logger.debug("submittable",
503                             String.format("Transaction not submittable: only %d account names",
504                                     accounts));
505                 submittable = false;
506             }
507
508             // 3) for each commodity:
509             // 3a) amount must balance to 0, or
510             // 3b) there must be exactly one empty amount (with account)
511             for (String balCurrency : itemsForCurrency.currencies()) {
512                 float currencyBalance = balance.get(balCurrency);
513                 if (Misc.isZero(currencyBalance)) {
514                     // remove hints from all amount inputs in that currency
515                     for (NewTransactionModel.Item item : model.items) {
516                         if (Currency.equal(item.getCurrency(), balCurrency))
517                             item.setAmountHint(null);
518                     }
519                 }
520                 else {
521                     List<NewTransactionModel.Item> list =
522                             itemsWithAccountAndEmptyAmountForCurrency.getList(balCurrency);
523                     int balanceReceiversCount = list.size();
524                     if (balanceReceiversCount != 1) {
525                         if (BuildConfig.DEBUG) {
526                             if (balanceReceiversCount == 0)
527                                 Logger.debug("submittable", String.format(
528                                         "Transaction not submittable [%s]: non-zero balance " +
529                                         "with no empty amounts with accounts", balCurrency));
530                             else
531                                 Logger.debug("submittable", String.format(
532                                         "Transaction not submittable [%s]: non-zero balance " +
533                                         "with multiple empty amounts with accounts", balCurrency));
534                         }
535                         submittable = false;
536                     }
537
538                     List<NewTransactionModel.Item> emptyAmountList =
539                             itemsWithEmptyAmountForCurrency.getList(balCurrency);
540
541                     // suggest off-balance amount to a row and remove hints on other rows
542                     NewTransactionModel.Item receiver = null;
543                     if (!list.isEmpty())
544                         receiver = list.get(0);
545                     else if (!emptyAmountList.isEmpty())
546                         receiver = emptyAmountList.get(0);
547
548                     for (NewTransactionModel.Item item : model.items) {
549                         if (!Currency.equal(item.getCurrency(), balCurrency))
550                             continue;
551
552                         if (item.equals(receiver)) {
553                             if (BuildConfig.DEBUG)
554                                 Logger.debug("submittable",
555                                         String.format("Setting amount hint to %1.2f [%s]",
556                                                 -currencyBalance, balCurrency));
557                             item.setAmountHint(String.format("%1.2f", -currencyBalance));
558                         }
559                         else {
560                             if (BuildConfig.DEBUG)
561                                 Logger.debug("submittable",
562                                         String.format("Resetting hint of '%s' [%s]",
563                                                 (item.getAccount() == null) ? "" : item.getAccount()
564                                                                                        .getAccountName(),
565                                                 balCurrency));
566                             item.setAmountHint(null);
567                         }
568                     }
569                 }
570             }
571
572             // 5) a row with an empty account name or empty amount is guaranteed to exist for
573             // each commodity
574             for (String balCurrency : balance.currencies()) {
575                 int currEmptyRows = itemsWithEmptyAccountForCurrency.size(balCurrency);
576                 int currRows = itemsForCurrency.size(balCurrency);
577                 int currAccounts = itemsWithAccountForCurrency.size(balCurrency);
578                 int currAmounts = itemsWithAmountForCurrency.size(balCurrency);
579                 if ((currEmptyRows == 0) &&
580                     ((currRows == currAccounts) || (currRows == currAmounts)))
581                 {
582                     // perhaps there already is an unused empty row for another currency that
583                     // is not used?
584 //                        boolean foundIt = false;
585 //                        for (Item item : emptyRows) {
586 //                            Currency itemCurrency = item.getCurrency();
587 //                            String itemCurrencyName =
588 //                                    (itemCurrency == null) ? "" : itemCurrency.getName();
589 //                            if (Misc.isZero(balance.get(itemCurrencyName))) {
590 //                                item.setCurrency(Currency.loadByName(balCurrency));
591 //                                item.setAmountHint(
592 //                                        String.format("%1.2f", -balance.get(balCurrency)));
593 //                                foundIt = true;
594 //                                break;
595 //                            }
596 //                        }
597 //
598 //                        if (!foundIt)
599                     addRow(balCurrency);
600                 }
601             }
602
603             // drop extra empty rows, not needed
604             for (String currName : emptyRowsForCurrency.currencies()) {
605                 List<NewTransactionModel.Item> emptyItems = emptyRowsForCurrency.getList(currName);
606                 while ((model.items.size() > 2) && (emptyItems.size() > 1)) {
607                     NewTransactionModel.Item item = emptyItems.get(1);
608                     emptyItems.remove(1);
609                     model.removeRow(item, this);
610                 }
611
612                 // unused currency, remove last item (which is also an empty one)
613                 if ((model.items.size() > 2) && (emptyItems.size() == 1)) {
614                     List<NewTransactionModel.Item> currItems = itemsForCurrency.getList(currName);
615
616                     if (currItems.size() == 1) {
617                         NewTransactionModel.Item item = emptyItems.get(0);
618                         model.removeRow(item, this);
619                     }
620                 }
621             }
622
623             // 6) at least two rows need to be present in the ledger
624             while (model.items.size() < 2)
625                 addRow();
626
627
628             debug("submittable", submittable ? "YES" : "NO");
629             model.isSubmittable.setValue(submittable);
630
631             if (BuildConfig.DEBUG) {
632                 debug("submittable", "== Dump of all items");
633                 for (int i = 0; i < model.items.size(); i++) {
634                     NewTransactionModel.Item item = model.items.get(i);
635                     LedgerTransactionAccount acc = item.getAccount();
636                     debug("submittable", String.format("Item %2d: [%4.2f(%s) %s] %s ; %s", i,
637                             acc.isAmountSet() ? acc.getAmount() : 0,
638                             item.isAmountHintSet() ? item.getAmountHint() : "ø", acc.getCurrency(),
639                             acc.getAccountName(), acc.getComment()));
640                 }
641             }
642         }
643         catch (NumberFormatException e) {
644             debug("submittable", "NO (because of NumberFormatException)");
645             model.isSubmittable.setValue(false);
646         }
647         catch (Exception e) {
648             e.printStackTrace();
649             debug("submittable", "NO (because of an Exception)");
650             model.isSubmittable.setValue(false);
651         }
652     }
653
654     private static class BalanceForCurrency {
655         private HashMap<String, Float> hashMap = new HashMap<>();
656         float get(String currencyName) {
657             Float f = hashMap.get(currencyName);
658             if (f == null) {
659                 f = 0f;
660                 hashMap.put(currencyName, f);
661             }
662             return f;
663         }
664         void add(String currencyName, float amount) {
665             hashMap.put(currencyName, get(currencyName) + amount);
666         }
667         Set<String> currencies() {
668             return hashMap.keySet();
669         }
670         boolean containsCurrency(String currencyName) {
671             return hashMap.containsKey(currencyName);
672         }
673     }
674
675     private static class ItemsForCurrency {
676         private HashMap<String, List<NewTransactionModel.Item>> hashMap = new HashMap<>();
677         @NonNull
678         List<NewTransactionModel.Item> getList(@Nullable String currencyName) {
679             List<NewTransactionModel.Item> list = hashMap.get(currencyName);
680             if (list == null) {
681                 list = new ArrayList<>();
682                 hashMap.put(currencyName, list);
683             }
684             return list;
685         }
686         void add(@Nullable String currencyName, @NonNull NewTransactionModel.Item item) {
687             getList(currencyName).add(item);
688         }
689         int size(@Nullable String currencyName) {
690             return this.getList(currencyName)
691                        .size();
692         }
693         Set<String> currencies() {
694             return hashMap.keySet();
695         }
696     }
697 }