]> git.ktnx.net Git - mobile-ledger.git/blob - app/src/main/java/net/ktnx/mobileledger/ui/activity/NewTransactionItemsAdapter.java
major rework of parsed transaction/descriptions/accounts storage
[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("description 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("description", sql);
216         debug("description", 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("description", "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("description", broaderSql);
248                 debug("description", description);
249
250                 activity.runOnUiThread(
251                         () -> Snackbar.make(recyclerView, R.string.ignoring_preferred_account,
252                                 Snackbar.LENGTH_LONG)
253                                       .show());
254
255                 MLDB.queryInBackground(broaderSql, new String[]{description},
256                         new MLDB.CallbackHelper() {
257                             @Override
258                             public void onStart() {
259                                 model.incrementBusyCounter();
260                             }
261                             @Override
262                             public boolean onRow(@NonNull Cursor cursor) {
263                                 final String profileUUID = cursor.getString(0);
264                                 final int transactionId = cursor.getInt(1);
265                                 activity.runOnUiThread(
266                                         () -> loadTransactionIntoModel(profileUUID, transactionId));
267                                 return false;
268                             }
269                             @Override
270                             public void onDone() {
271                                 model.decrementBusyCounter();
272                             }
273                         });
274             }
275         });
276     }
277     private void loadTransactionIntoModel(String profileUUID, int transactionId) {
278         LedgerTransaction tr;
279         MobileLedgerProfile profile = Data.getProfile(profileUUID);
280         if (profile == null)
281             throw new RuntimeException(String.format(
282                     "Unable to find profile %s, which is supposed to contain transaction %d",
283                     profileUUID, transactionId));
284
285         tr = profile.loadTransaction(transactionId);
286         List<LedgerTransactionAccount> accounts = tr.getAccounts();
287         NewTransactionModel.Item firstNegative = null;
288         NewTransactionModel.Item firstPositive = null;
289         int singleNegativeIndex = -1;
290         int singlePositiveIndex = -1;
291         int negativeCount = 0;
292         for (int i = 0; i < accounts.size(); i++) {
293             LedgerTransactionAccount acc = accounts.get(i);
294             NewTransactionModel.Item item;
295             if (model.getAccountCount() < i + 1) {
296                 model.addAccount(acc);
297                 notifyItemInserted(i + 1);
298             }
299             item = model.getItem(i + 1);
300
301             item.getAccount()
302                 .setAccountName(acc.getAccountName());
303             item.setComment(acc.getComment());
304             if (acc.isAmountSet()) {
305                 item.getAccount()
306                     .setAmount(acc.getAmount());
307                 if (acc.getAmount() < 0) {
308                     if (firstNegative == null) {
309                         firstNegative = item;
310                         singleNegativeIndex = i;
311                     }
312                     else
313                         singleNegativeIndex = -1;
314                 }
315                 else {
316                     if (firstPositive == null) {
317                         firstPositive = item;
318                         singlePositiveIndex = i;
319                     }
320                     else
321                         singlePositiveIndex = -1;
322                 }
323             }
324             else
325                 item.getAccount()
326                     .resetAmount();
327             notifyItemChanged(i + 1);
328         }
329
330         if (singleNegativeIndex != -1) {
331             firstNegative.getAccount()
332                          .resetAmount();
333             model.moveItemLast(singleNegativeIndex);
334         }
335         else if (singlePositiveIndex != -1) {
336             firstPositive.getAccount()
337                          .resetAmount();
338             model.moveItemLast(singlePositiveIndex);
339         }
340
341         checkTransactionSubmittable();
342         model.setFocusedItem(1);
343     }
344     public void toggleAllEditing(boolean editable) {
345         // item 0 is the header
346         for (int i = 0; i <= model.getAccountCount(); i++) {
347             model.getItem(i)
348                  .setEditable(editable);
349             notifyItemChanged(i);
350             // TODO perhaps do only one notification about the whole range (notifyDatasetChanged)?
351         }
352     }
353     void reset() {
354         int presentItemCount = model.getAccountCount();
355         model.reset();
356         notifyItemChanged(0);       // header changed
357         notifyItemRangeChanged(1, 2);    // the two empty rows
358         if (presentItemCount > 2)
359             notifyItemRangeRemoved(3, presentItemCount - 2); // all the rest are gone
360     }
361     void updateFocusedItem(int position) {
362         model.updateFocusedItem(position);
363     }
364     void noteFocusIsOnAccount(int position) {
365         model.noteFocusChanged(position, NewTransactionModel.FocusedElement.Account);
366     }
367     void noteFocusIsOnAmount(int position) {
368         model.noteFocusChanged(position, NewTransactionModel.FocusedElement.Amount);
369     }
370     void noteFocusIsOnComment(int position) {
371         model.noteFocusChanged(position, NewTransactionModel.FocusedElement.Comment);
372     }
373     void noteFocusIsOnTransactionComment(int position) {
374         model.noteFocusChanged(position, NewTransactionModel.FocusedElement.TransactionComment);
375     }
376     public void noteFocusIsOnDescription(int pos) {
377         model.noteFocusChanged(pos, NewTransactionModel.FocusedElement.Description);
378     }
379     private void holdSubmittableChecks() {
380         checkHoldCounter++;
381     }
382     private void releaseSubmittableChecks() {
383         if (checkHoldCounter == 0)
384             throw new RuntimeException("Asymmetrical call to releaseSubmittableChecks");
385         checkHoldCounter--;
386     }
387     void setItemCurrency(NewTransactionModel.Item item, Currency newCurrency) {
388         Currency oldCurrency = item.getCurrency();
389         if (!Currency.equal(newCurrency, oldCurrency)) {
390             holdSubmittableChecks();
391             try {
392                 item.setCurrency(newCurrency);
393 //                for (Item i : items) {
394 //                    if (Currency.equal(i.getCurrency(), oldCurrency))
395 //                        i.setCurrency(newCurrency);
396 //                }
397             }
398             finally {
399                 releaseSubmittableChecks();
400             }
401
402             checkTransactionSubmittable();
403         }
404     }
405     /*
406          A transaction is submittable if:
407          0) has description
408          1) has at least two account names
409          2) each row with amount has account name
410          3) for each commodity:
411          3a) amounts must balance to 0, or
412          3b) there must be exactly one empty amount (with account)
413          4) empty accounts with empty amounts are ignored
414          Side effects:
415          5) a row with an empty account name or empty amount is guaranteed to exist for each
416          commodity
417          6) at least two rows need to be present in the ledger
418
419         */
420     @SuppressLint("DefaultLocale")
421     void checkTransactionSubmittable() {
422         if (checkHoldCounter > 0)
423             return;
424
425         int accounts = 0;
426         final BalanceForCurrency balance = new BalanceForCurrency();
427         final String descriptionText = model.getDescription();
428         boolean submittable = true;
429         final ItemsForCurrency itemsForCurrency = new ItemsForCurrency();
430         final ItemsForCurrency itemsWithEmptyAmountForCurrency = new ItemsForCurrency();
431         final ItemsForCurrency itemsWithAccountAndEmptyAmountForCurrency = new ItemsForCurrency();
432         final ItemsForCurrency itemsWithEmptyAccountForCurrency = new ItemsForCurrency();
433         final ItemsForCurrency itemsWithAmountForCurrency = new ItemsForCurrency();
434         final ItemsForCurrency itemsWithAccountForCurrency = new ItemsForCurrency();
435         final ItemsForCurrency emptyRowsForCurrency = new ItemsForCurrency();
436         final List<NewTransactionModel.Item> emptyRows = new ArrayList<>();
437
438         try {
439             if ((descriptionText == null) || descriptionText.trim()
440                                                             .isEmpty())
441             {
442                 Logger.debug("submittable", "Transaction not submittable: missing description");
443                 submittable = false;
444             }
445
446             for (int i = 0; i < model.items.size(); i++) {
447                 NewTransactionModel.Item item = model.items.get(i);
448
449                 LedgerTransactionAccount acc = item.getAccount();
450                 String acc_name = acc.getAccountName()
451                                      .trim();
452                 String currName = acc.getCurrency();
453
454                 itemsForCurrency.add(currName, item);
455
456                 if (acc_name.isEmpty()) {
457                     itemsWithEmptyAccountForCurrency.add(currName, item);
458
459                     if (acc.isAmountSet()) {
460                         // 2) each amount has account name
461                         Logger.debug("submittable", String.format(
462                                 "Transaction not submittable: row %d has no account name, but" +
463                                 " has" + " amount %1.2f", i + 1, acc.getAmount()));
464                         submittable = false;
465                     }
466                     else {
467                         emptyRowsForCurrency.add(currName, item);
468                     }
469                 }
470                 else {
471                     accounts++;
472                     itemsWithAccountForCurrency.add(currName, item);
473                 }
474
475                 if (!acc.isAmountValid()) {
476                     Logger.debug("submittable",
477                             String.format("Not submittable: row %d has an invalid amount", i + 1));
478                     submittable = false;
479                 }
480                 else if (acc.isAmountSet()) {
481                     itemsWithAmountForCurrency.add(currName, item);
482                     balance.add(currName, acc.getAmount());
483                 }
484                 else {
485                     itemsWithEmptyAmountForCurrency.add(currName, item);
486
487                     if (!acc_name.isEmpty())
488                         itemsWithAccountAndEmptyAmountForCurrency.add(currName, item);
489                 }
490             }
491
492             // 1) has at least two account names
493             if (accounts < 2) {
494                 if (accounts == 0)
495                     Logger.debug("submittable",
496                             "Transaction not submittable: no account " + "names");
497                 else if (accounts == 1)
498                     Logger.debug("submittable",
499                             "Transaction not submittable: only one account name");
500                 else
501                     Logger.debug("submittable",
502                             String.format("Transaction not submittable: only %d account names",
503                                     accounts));
504                 submittable = false;
505             }
506
507             // 3) for each commodity:
508             // 3a) amount must balance to 0, or
509             // 3b) there must be exactly one empty amount (with account)
510             for (String balCurrency : itemsForCurrency.currencies()) {
511                 float currencyBalance = balance.get(balCurrency);
512                 if (Misc.isZero(currencyBalance)) {
513                     // remove hints from all amount inputs in that currency
514                     for (NewTransactionModel.Item item : model.items) {
515                         if (Currency.equal(item.getCurrency(), balCurrency))
516                             item.setAmountHint(null);
517                     }
518                 }
519                 else {
520                     List<NewTransactionModel.Item> list =
521                             itemsWithAccountAndEmptyAmountForCurrency.getList(balCurrency);
522                     int balanceReceiversCount = list.size();
523                     if (balanceReceiversCount != 1) {
524                         if (BuildConfig.DEBUG) {
525                             if (balanceReceiversCount == 0)
526                                 Logger.debug("submittable", String.format(
527                                         "Transaction not submittable [%s]: non-zero balance " +
528                                         "with no empty amounts with accounts", balCurrency));
529                             else
530                                 Logger.debug("submittable", String.format(
531                                         "Transaction not submittable [%s]: non-zero balance " +
532                                         "with multiple empty amounts with accounts", balCurrency));
533                         }
534                         submittable = false;
535                     }
536
537                     List<NewTransactionModel.Item> emptyAmountList =
538                             itemsWithEmptyAmountForCurrency.getList(balCurrency);
539
540                     // suggest off-balance amount to a row and remove hints on other rows
541                     NewTransactionModel.Item receiver = null;
542                     if (!list.isEmpty())
543                         receiver = list.get(0);
544                     else if (!emptyAmountList.isEmpty())
545                         receiver = emptyAmountList.get(0);
546
547                     for (NewTransactionModel.Item item : model.items) {
548                         if (!Currency.equal(item.getCurrency(), balCurrency))
549                             continue;
550
551                         if (item.equals(receiver)) {
552                             if (BuildConfig.DEBUG)
553                                 Logger.debug("submittable",
554                                         String.format("Setting amount hint to %1.2f [%s]",
555                                                 -currencyBalance, balCurrency));
556                             item.setAmountHint(String.format("%1.2f", -currencyBalance));
557                         }
558                         else {
559                             if (BuildConfig.DEBUG)
560                                 Logger.debug("submittable",
561                                         String.format("Resetting hint of '%s' [%s]",
562                                                 (item.getAccount() == null) ? "" : item.getAccount()
563                                                                                        .getAccountName(),
564                                                 balCurrency));
565                             item.setAmountHint(null);
566                         }
567                     }
568                 }
569             }
570
571             // 5) a row with an empty account name or empty amount is guaranteed to exist for
572             // each commodity
573             for (String balCurrency : balance.currencies()) {
574                 int currEmptyRows = itemsWithEmptyAccountForCurrency.size(balCurrency);
575                 int currRows = itemsForCurrency.size(balCurrency);
576                 int currAccounts = itemsWithAccountForCurrency.size(balCurrency);
577                 int currAmounts = itemsWithAmountForCurrency.size(balCurrency);
578                 if ((currEmptyRows == 0) &&
579                     ((currRows == currAccounts) || (currRows == currAmounts)))
580                 {
581                     // perhaps there already is an unused empty row for another currency that
582                     // is not used?
583 //                        boolean foundIt = false;
584 //                        for (Item item : emptyRows) {
585 //                            Currency itemCurrency = item.getCurrency();
586 //                            String itemCurrencyName =
587 //                                    (itemCurrency == null) ? "" : itemCurrency.getName();
588 //                            if (Misc.isZero(balance.get(itemCurrencyName))) {
589 //                                item.setCurrency(Currency.loadByName(balCurrency));
590 //                                item.setAmountHint(
591 //                                        String.format("%1.2f", -balance.get(balCurrency)));
592 //                                foundIt = true;
593 //                                break;
594 //                            }
595 //                        }
596 //
597 //                        if (!foundIt)
598                     addRow(balCurrency);
599                 }
600             }
601
602             // drop extra empty rows, not needed
603             for (String currName : emptyRowsForCurrency.currencies()) {
604                 List<NewTransactionModel.Item> emptyItems = emptyRowsForCurrency.getList(currName);
605                 while ((model.items.size() > 2) && (emptyItems.size() > 1)) {
606                     NewTransactionModel.Item item = emptyItems.get(1);
607                     emptyItems.remove(1);
608                     model.removeRow(item, this);
609                 }
610
611                 // unused currency, remove last item (which is also an empty one)
612                 if ((model.items.size() > 2) && (emptyItems.size() == 1)) {
613                     List<NewTransactionModel.Item> currItems = itemsForCurrency.getList(currName);
614
615                     if (currItems.size() == 1) {
616                         NewTransactionModel.Item item = emptyItems.get(0);
617                         model.removeRow(item, this);
618                     }
619                 }
620             }
621
622             // 6) at least two rows need to be present in the ledger
623             while (model.items.size() < 2)
624                 addRow();
625
626
627             debug("submittable", submittable ? "YES" : "NO");
628             model.isSubmittable.setValue(submittable);
629
630             if (BuildConfig.DEBUG) {
631                 debug("submittable", "== Dump of all items");
632                 for (int i = 0; i < model.items.size(); i++) {
633                     NewTransactionModel.Item item = model.items.get(i);
634                     LedgerTransactionAccount acc = item.getAccount();
635                     debug("submittable", String.format("Item %2d: [%4.2f(%s) %s] %s ; %s", i,
636                             acc.isAmountSet() ? acc.getAmount() : 0,
637                             item.isAmountHintSet() ? item.getAmountHint() : "ø", acc.getCurrency(),
638                             acc.getAccountName(), acc.getComment()));
639                 }
640             }
641         }
642         catch (NumberFormatException e) {
643             debug("submittable", "NO (because of NumberFormatException)");
644             model.isSubmittable.setValue(false);
645         }
646         catch (Exception e) {
647             e.printStackTrace();
648             debug("submittable", "NO (because of an Exception)");
649             model.isSubmittable.setValue(false);
650         }
651     }
652
653     private static class BalanceForCurrency {
654         private HashMap<String, Float> hashMap = new HashMap<>();
655         float get(String currencyName) {
656             Float f = hashMap.get(currencyName);
657             if (f == null) {
658                 f = 0f;
659                 hashMap.put(currencyName, f);
660             }
661             return f;
662         }
663         void add(String currencyName, float amount) {
664             hashMap.put(currencyName, get(currencyName) + amount);
665         }
666         Set<String> currencies() {
667             return hashMap.keySet();
668         }
669         boolean containsCurrency(String currencyName) {
670             return hashMap.containsKey(currencyName);
671         }
672     }
673
674     private static class ItemsForCurrency {
675         private HashMap<String, List<NewTransactionModel.Item>> hashMap = new HashMap<>();
676         @NonNull
677         List<NewTransactionModel.Item> getList(@Nullable String currencyName) {
678             List<NewTransactionModel.Item> list = hashMap.get(currencyName);
679             if (list == null) {
680                 list = new ArrayList<>();
681                 hashMap.put(currencyName, list);
682             }
683             return list;
684         }
685         void add(@Nullable String currencyName, @NonNull NewTransactionModel.Item item) {
686             getList(currencyName).add(item);
687         }
688         int size(@Nullable String currencyName) {
689             return this.getList(currencyName)
690                        .size();
691         }
692         Set<String> currencies() {
693             return hashMap.keySet();
694         }
695     }
696 }