X-Git-Url: https://git.ktnx.net/?p=mobile-ledger.git;a=blobdiff_plain;f=app%2Fsrc%2Fmain%2Fjava%2Fnet%2Fktnx%2Fmobileledger%2Fui%2Factivity%2FNewTransactionModel.java;h=4c200fc592af0ea2610c890d2f54b53d67d822fd;hp=66b01276eb58094a9de66d9035ffae414532383e;hb=ba6513e75e8cfc577f333fb38c0e6c597f9c8875;hpb=470cb67d9e13ab9cabdfb9f203a429f72e364907 diff --git a/app/src/main/java/net/ktnx/mobileledger/ui/activity/NewTransactionModel.java b/app/src/main/java/net/ktnx/mobileledger/ui/activity/NewTransactionModel.java index 66b01276..4c200fc5 100644 --- a/app/src/main/java/net/ktnx/mobileledger/ui/activity/NewTransactionModel.java +++ b/app/src/main/java/net/ktnx/mobileledger/ui/activity/NewTransactionModel.java @@ -17,13 +17,17 @@ package net.ktnx.mobileledger.ui.activity; +import android.annotation.SuppressLint; + import androidx.annotation.NonNull; import androidx.lifecycle.LiveData; import androidx.lifecycle.MutableLiveData; import androidx.lifecycle.Observer; import androidx.lifecycle.ViewModel; +import net.ktnx.mobileledger.BuildConfig; import net.ktnx.mobileledger.model.LedgerTransactionAccount; +import net.ktnx.mobileledger.utils.Logger; import net.ktnx.mobileledger.utils.Misc; import org.jetbrains.annotations.NotNull; @@ -32,12 +36,12 @@ import java.util.ArrayList; import java.util.Calendar; import java.util.Date; import java.util.GregorianCalendar; +import java.util.List; import java.util.Locale; import java.util.regex.Matcher; import java.util.regex.Pattern; import static net.ktnx.mobileledger.utils.Logger.debug; -import static net.ktnx.mobileledger.utils.Misc.isZero; public class NewTransactionModel extends ViewModel { static final Pattern reYMD = Pattern.compile("^\\s*(\\d+)\\d*/\\s*(\\d+)\\s*/\\s*(\\d+)\\s*$"); @@ -97,10 +101,12 @@ public class NewTransactionModel extends ViewModel { boolean accountsInInitialState() { for (Item item : items) { LedgerTransactionAccount acc = item.getAccount(); - if (acc.isAmountSet()) return false; + if (acc.isAmountSet()) + return false; if (!acc.getAccountName() .trim() - .isEmpty()) return false; + .isEmpty()) + return false; } return true; @@ -113,90 +119,139 @@ public class NewTransactionModel extends ViewModel { if (index == 0) { return header; } - else if (index <= items.size()) return items.get(index - 1); - else return trailer; + + if (index <= items.size()) + return items.get(index - 1); + + return trailer; } - // rules: - // 1) at least two account names - // 2) each amount must have account name - // 3) amounts must balance to 0, or - // 3a) there must be exactly one empty amount - // 4) empty accounts with empty amounts are ignored - // 5) a row with an empty account name or empty amount is guaranteed to exist + /* + A transaction is submittable if: + 0) has description + 1) has at least two account names + 2) each amount has account name + 3) amounts must balance to 0, or + 3a) there must be exactly one empty amount (with account) + 4) empty accounts with empty amounts are ignored + 5) a row with an empty account name or empty amount is guaranteed to exist + */ + @SuppressLint("DefaultLocale") public void checkTransactionSubmittable(NewTransactionItemsAdapter adapter) { int accounts = 0; - int accounts_with_values = 0; int amounts = 0; - int amounts_with_accounts = 0; int empty_rows = 0; - Item empty_amount = null; - boolean single_empty_amount = false; - boolean single_empty_amount_has_account = false; - float running_total = 0f; + float balance = 0f; final String descriptionText = getDescription(); - final boolean have_description = ((descriptionText != null) && !descriptionText.isEmpty()); + boolean submittable = true; + List itemsWithEmptyAmount = new ArrayList<>(); + List itemsWithAccountAndEmptyAmount = new ArrayList<>(); try { + if ((descriptionText == null) || descriptionText.trim() + .isEmpty()) + { + Logger.debug("submittable", "Transaction not submittable: missing description"); + submittable = false; + } + for (int i = 0; i < this.items.size(); i++) { Item item = this.items.get(i); LedgerTransactionAccount acc = item.getAccount(); String acc_name = acc.getAccountName() .trim(); - if (!acc_name.isEmpty()) { - accounts++; + if (acc_name.isEmpty()) { + empty_rows++; if (acc.isAmountSet()) { - accounts_with_values++; + // 2) each amount has account name + Logger.debug("submittable", String.format( + "Transaction not submittable: row %d has no account name, but has" + + " amount %1.2f", i + 1, acc.getAmount())); + submittable = false; } } - else empty_rows++; + else { + accounts++; + } - if (!acc.isAmountSet()) { - if (empty_amount == null) { - empty_amount = item; - single_empty_amount = true; - single_empty_amount_has_account = !acc_name.isEmpty(); - } - else if (!acc_name.isEmpty()) single_empty_amount = false; + if (acc.isAmountSet()) { + amounts++; + balance += acc.getAmount(); } else { - amounts++; - if (!acc_name.isEmpty()) amounts_with_accounts++; - running_total += acc.getAmount(); + itemsWithEmptyAmount.add(item); + + if (!acc_name.isEmpty()) { + itemsWithAccountAndEmptyAmount.add(item); + } } } + // 1) has at least two account names + if (accounts < 2) { + Logger.debug("submittable", + String.format("Transaction not submittable: only %d account names", + accounts)); + submittable = false; + } + + // 3) amount must balance to 0, or + // 3a) there must be exactly one empty amount (with account) + if (Misc.isZero(balance)) { + for (Item item : items) { + item.setAmountHint(null); + } + } + else { + int balanceReceiversCount = itemsWithAccountAndEmptyAmount.size(); + if (balanceReceiversCount != 1) { + Logger.debug("submittable", (balanceReceiversCount == 0) ? + "Transaction not submittable: non-zero balance " + + "with no empty amounts with accounts" : + "Transaction not submittable: non-zero balance " + + "with multiple empty amounts with accounts"); + submittable = false; + } + + // suggest off-balance amount to a row and remove hints on other rows + Item receiver = null; + if (!itemsWithAccountAndEmptyAmount.isEmpty()) + receiver = itemsWithAccountAndEmptyAmount.get(0); + else if (!itemsWithEmptyAmount.isEmpty()) + receiver = itemsWithEmptyAmount.get(0); + + for (Item item : items) { + if (item.equals(receiver)) { + Logger.debug("submittable", + String.format("Setting amount hint to %1.2f", -balance)); + item.setAmountHint(String.format("%1.2f", -balance)); + } + else + item.setAmountHint(null); + } + } + + // 5) a row with an empty account name or empty amount is guaranteed to exist if ((empty_rows == 0) && ((this.items.size() == accounts) || (this.items.size() == amounts))) { adapter.addRow(); } - if (single_empty_amount) { - empty_amount.setAmountHint(String.format(Locale.US, "%1.2f", - Misc.isZero(running_total) ? 0f : -running_total)); - } - debug("submittable", String.format(Locale.US, - "%s, accounts=%d, accounts_with_values=%s, " + - "amounts_with_accounts=%d, amounts=%d, running_total=%1.2f, " + - "single_empty_with_acc=%s", have_description ? "description" : "NO description", - accounts, accounts_with_values, amounts_with_accounts, amounts, running_total, - (single_empty_amount && single_empty_amount_has_account) ? "true" : "false")); + debug("submittable", submittable ? "YES" : "NO"); + isSubmittable.setValue(submittable); - if (have_description && (accounts >= 2) && (accounts_with_values >= (accounts - 1)) && - (amounts_with_accounts == amounts) && - (single_empty_amount && single_empty_amount_has_account || isZero(running_total))) - { - debug("submittable", "YES"); - isSubmittable.setValue(true); - } - else { - debug("submittable", "NO"); - isSubmittable.setValue(false); + if (BuildConfig.DEBUG) { + debug("submittable", "== Dump of all items"); + for (int i = 0; i < items.size(); i++) { + Item item = items.get(i); + LedgerTransactionAccount acc = item.getAccount(); + debug("submittable", String.format("Item %2d: [%4.2f] %s", i, + acc.isAmountSet() ? acc.getAmount() : 0, acc.getAccountName())); + } } - } catch (NumberFormatException e) { debug("submittable", "NO (because of NumberFormatException)"); @@ -217,12 +272,14 @@ public class NewTransactionModel extends ViewModel { } enum ItemType {generalData, transactionRow, bottomFiller} + //========================================================================================== + class Item extends Object { private ItemType type; private MutableLiveData date = new MutableLiveData<>(); private MutableLiveData description = new MutableLiveData<>(); private LedgerTransactionAccount account; - private MutableLiveData amountHint = new MutableLiveData<>(); + private MutableLiveData amountHint = new MutableLiveData<>(null); private NewTransactionModel model; private MutableLiveData editable = new MutableLiveData<>(true); public Item(NewTransactionModel model) { @@ -246,20 +303,34 @@ public class NewTransactionModel extends ViewModel { public NewTransactionModel getModel() { return model; } - public boolean isEditable() { - ensureType(ItemType.transactionRow); - return this.editable.getValue(); - } public void setEditable(boolean editable) { - ensureType(ItemType.transactionRow); + ensureType(ItemType.generalData, ItemType.transactionRow); this.editable.setValue(editable); } + private void ensureType(ItemType type1, ItemType type2) { + if ((type != type1) && (type != type2)) { + throw new RuntimeException( + String.format("Actual type (%s) differs from wanted (%s or %s)", type, + type1, type2)); + } + } public String getAmountHint() { ensureType(ItemType.transactionRow); return amountHint.getValue(); } public void setAmountHint(String amountHint) { ensureType(ItemType.transactionRow); + + // avoid unnecessary triggers + if (amountHint == null) { + if (this.amountHint.getValue() == null) + return; + } + else { + if (amountHint.equals(this.amountHint.getValue())) + return; + } + this.amountHint.setValue(amountHint); } public void observeAmountHint(@NonNull @NotNull androidx.lifecycle.LifecycleOwner owner, @@ -277,7 +348,7 @@ public class NewTransactionModel extends ViewModel { public void ensureType(ItemType wantedType) { if (type != wantedType) { throw new RuntimeException( - String.format("Actual type (%d) differs from wanted (%s)", type, + String.format("Actual type (%s) differs from wanted (%s)", type, wantedType)); } } @@ -358,9 +429,11 @@ public class NewTransactionModel extends ViewModel { * @return nicely formatted, shortest available date representation */ public String getFormattedDate() { - if (date == null) return null; + if (date == null) + return null; Date time = date.getValue(); - if (time == null) return null; + if (time == null) + return null; Calendar c = GregorianCalendar.getInstance(); c.setTime(time); @@ -371,11 +444,11 @@ public class NewTransactionModel extends ViewModel { final int myDay = c.get(Calendar.DAY_OF_MONTH); if (today.get(Calendar.YEAR) != myYear) { - return String.format(Locale.US, "%d/%02d/%02d", myYear, myMonth, myDay); + return String.format(Locale.US, "%d/%02d/%02d", myYear, myMonth + 1, myDay); } if (today.get(Calendar.MONTH) != myMonth) { - return String.format(Locale.US, "%d/%02d", myMonth, myDay); + return String.format(Locale.US, "%d/%02d", myMonth + 1, myDay); } return String.valueOf(myDay);