From 4893e8e45dfe980eb51710479a140df629dc9884 Mon Sep 17 00:00:00 2001 From: Ngewi Fet Date: Fri, 20 Nov 2015 10:47:27 +0100 Subject: [PATCH 001/121] Add support for budgeting and improve recurrence schedules Import/export budgets from/to GnuCash XML Create/display/edit budgets and budget amounts Add budget indicator to account list view Make parsing/computation of recurrence schedule more precise Add support for GnuCash non-business tables/entries Add support for split reconcile state/date to the database Import/Export all scheduled transaction flags from/to GnuCash XML Code refactoring Add CurrencyMismatchException to money operations Add unit and integration tests Refactor recurrence of scheduled actions into its own table Use recurrence for scheduled transactions, backups and budgets Update Android support / test library dependencies Upgrade betterpickers library - fixes #359 Upgrade database to version 12 Allow auto-backup of application data by Android system --- CHANGELOG.md | 6 + app/build.gradle | 23 +- .../android/test/ui/AccountsActivityTest.java | 7 +- .../test/ui/ExportTransactionsTest.java | 16 +- .../test/ui/FirstRunWizardActivityTest.java | 6 +- .../android/test/ui/PieChartReportTest.java | 14 +- .../test/ui/TransactionsActivityTest.java | 6 +- app/src/main/AndroidManifest.xml | 5 +- .../android/app/GnuCashApplication.java | 36 +- .../android/db/DatabaseCursorLoader.java | 4 +- .../gnucash/android/db/DatabaseHelper.java | 114 ++++- .../gnucash/android/db/DatabaseSchema.java | 67 ++- .../gnucash/android/db/MigrationHelper.java | 203 ++++++++- .../db/{ => adapter}/AccountsDbAdapter.java | 6 +- .../db/adapter/BudgetAmountsDbAdapter.java | 145 +++++++ .../android/db/adapter/BudgetsDbAdapter.java | 194 +++++++++ .../{ => adapter}/CommoditiesDbAdapter.java | 5 +- .../db/{ => adapter}/DatabaseAdapter.java | 27 +- .../db/{ => adapter}/PricesDbAdapter.java | 4 +- .../db/adapter/RecurrenceDbAdapter.java | 97 +++++ .../ScheduledActionDbAdapter.java | 93 +++- .../db/{ => adapter}/SplitsDbAdapter.java | 25 +- .../{ => adapter}/TransactionsDbAdapter.java | 4 +- .../android/export/ExportAsyncTask.java | 6 +- .../org/gnucash/android/export/Exporter.java | 32 +- .../android/export/ofx/OfxExporter.java | 2 +- .../android/export/qif/QifExporter.java | 4 +- .../android/export/xml/GncXmlExporter.java | 172 ++++++-- .../android/export/xml/GncXmlHelper.java | 58 ++- .../importer/CommoditiesXmlHandler.java | 2 +- .../android/importer/GncXmlHandler.java | 137 +++++- .../org/gnucash/android/model/Budget.java | 407 ++++++++++++++++++ .../gnucash/android/model/BudgetAmount.java | 104 +++++ .../org/gnucash/android/model/Commodity.java | 2 +- .../java/org/gnucash/android/model/Money.java | 36 +- .../org/gnucash/android/model/PeriodType.java | 43 +- .../org/gnucash/android/model/Recurrence.java | 323 ++++++++++++++ .../android/model/ScheduledAction.java | 281 ++++++------ .../java/org/gnucash/android/model/Split.java | 94 +++- .../gnucash/android/model/Transaction.java | 8 +- .../android/receivers/AccountCreator.java | 2 +- .../receivers/TransactionRecorder.java | 7 +- .../android/service/SchedulerService.java | 47 +- .../ui/account/AccountFormFragment.java | 4 +- .../android/ui/account/AccountsActivity.java | 6 +- .../ui/account/AccountsListFragment.java | 28 +- .../account/DeleteAccountDialogFragment.java | 6 +- .../ui/budget/BudgetDetailFragment.java | 307 +++++++++++++ .../android/ui/budget/BudgetFormFragment.java | 362 ++++++++++++++++ .../android/ui/budget/BudgetListFragment.java | 327 ++++++++++++++ .../android/ui/budget/BudgetsActivity.java | 85 ++++ .../android/ui/common/BaseDrawerActivity.java | 11 +- .../android/ui/common/FormActivity.java | 19 +- .../gnucash/android/ui/common/UxArgument.java | 5 + .../android/ui/export/ExportFormFragment.java | 66 +-- .../WidgetConfigurationActivity.java | 2 +- .../ui/report/BalanceSheetFragment.java | 2 +- .../android/ui/report/BarChartFragment.java | 4 +- .../android/ui/report/LineChartFragment.java | 4 +- .../android/ui/report/PieChartFragment.java | 4 +- .../ui/report/ReportSummaryFragment.java | 2 +- .../android/ui/report/ReportsActivity.java | 2 +- .../settings/AccountPreferencesFragment.java | 3 +- .../DeleteAllAccountsConfirmationDialog.java | 2 +- ...leteAllTransactionsConfirmationDialog.java | 4 +- .../android/ui/settings/SettingsActivity.java | 6 +- .../ScheduledActionsListFragment.java | 10 +- .../ui/transaction/SplitEditorFragment.java | 6 +- .../TransactionDetailActivity.java | 6 +- .../transaction/TransactionFormFragment.java | 111 ++--- .../ui/transaction/TransactionsActivity.java | 4 +- .../transaction/TransactionsListFragment.java | 6 +- .../dialog/BulkMoveDialogFragment.java | 4 +- ...tionsDeleteConfirmationDialogFragment.java | 4 +- .../dialog/TransferFundsDialogFragment.java | 4 +- .../android/ui/util/AccountBalanceTask.java | 3 +- .../android/ui/util/RecurrenceParser.java | 151 ++++--- .../ui/util/RecurrenceViewClickListener.java | 66 +++ .../ui/util/widget/CalculatorEditText.java | 4 +- .../ui/wizard/CurrencySelectFragment.java | 2 +- .../util/CommoditiesCursorAdapter.java | 5 +- .../QualifiedAccountNameCursorAdapter.java | 17 + .../drawable-hdpi/ic_dashboard_black_24dp.png | Bin 0 -> 126 bytes .../drawable-mdpi/ic_dashboard_black_24dp.png | Bin 0 -> 92 bytes .../ic_dashboard_black_24dp.png | Bin 0 -> 115 bytes .../ic_dashboard_black_24dp.png | Bin 0 -> 126 bytes .../ic_dashboard_black_24dp.png | Bin 0 -> 127 bytes .../drawable/budget_progress_indicator.xml | 56 +++ app/src/main/res/layout/activity_accounts.xml | 2 +- app/src/main/res/layout/activity_budgets.xml | 64 +++ app/src/main/res/layout/cardview_account.xml | 22 +- app/src/main/res/layout/cardview_budget.xml | 107 +++++ .../res/layout/cardview_budget_amount.xml | 139 ++++++ .../main/res/layout/fragment_account_form.xml | 2 +- .../res/layout/fragment_budget_detail.xml | 48 +++ .../main/res/layout/fragment_budget_form.xml | 121 ++++++ .../main/res/layout/fragment_budget_list.xml | 42 ++ .../main/res/layout/item_budget_amount.xml | 87 ++++ app/src/main/res/layout/list_item_2_lines.xml | 4 +- app/src/main/res/layout/nav_drawer_header.xml | 9 +- app/src/main/res/menu/budget_actions.xml | 17 + app/src/main/res/menu/budget_context_menu.xml | 33 ++ app/src/main/res/menu/nav_drawer_menu.xml | 33 +- app/src/main/res/values-af-rZA/strings.xml | 1 + app/src/main/res/values-ar-rSA/strings.xml | 1 + app/src/main/res/values-ca-rES/strings.xml | 1 + app/src/main/res/values-cs-rCZ/strings.xml | 1 + app/src/main/res/values-de/strings.xml | 1 + app/src/main/res/values-el-rGR/strings.xml | 1 + app/src/main/res/values-en-rGB/strings.xml | 1 + app/src/main/res/values-es-rMX/strings.xml | 1 + app/src/main/res/values-es/strings.xml | 1 + app/src/main/res/values-fi-rFI/strings.xml | 1 + app/src/main/res/values-fr/strings.xml | 1 + app/src/main/res/values-hu-rHU/strings.xml | 1 + app/src/main/res/values-it-rIT/strings.xml | 1 + app/src/main/res/values-iw-rIL/strings.xml | 1 + app/src/main/res/values-ja-rJP/strings.xml | 1 + app/src/main/res/values-ko-rKR/strings.xml | 1 + app/src/main/res/values-nl-rNL/strings.xml | 1 + app/src/main/res/values-no-rNO/strings.xml | 1 + app/src/main/res/values-pl-rPL/strings.xml | 1 + app/src/main/res/values-pt-rBR/strings.xml | 1 + app/src/main/res/values-pt-rPT/strings.xml | 1 + app/src/main/res/values-ro-rRO/strings.xml | 1 + app/src/main/res/values-ru/strings.xml | 1 + app/src/main/res/values-sr-rSP/strings.xml | 1 + app/src/main/res/values-sv-rSE/strings.xml | 1 + app/src/main/res/values-tr-rTR/strings.xml | 1 + app/src/main/res/values-uk-rUA/strings.xml | 1 + app/src/main/res/values-vi-rVN/strings.xml | 1 + app/src/main/res/values-zh-rCN/strings.xml | 1 + app/src/main/res/values-zh-rTW/strings.xml | 1 + app/src/main/res/values/strings.xml | 1 + .../test/unit/db/AccountsDbAdapterTest.java | 64 ++- .../test/unit/db/BudgetsDbAdapterTest.java | 173 ++++++++ .../test/unit/db/PriceDbAdapterTest.java | 60 +++ .../unit/db/ScheduledActionDbAdapterTest.java | 73 ++++ .../test/unit/db/SplitsDbAdapterTest.java | 21 +- .../unit/db/TransactionsDbAdapterTest.java | 6 +- .../android/test/unit/model/BudgetTest.java | 186 ++++++++ .../android/test/unit/model/MoneyTest.java | 8 +- .../test/unit/model/RecurrenceTest.java | 61 +++ .../test/unit/model/ScheduledActionTest.java | 82 ++++ 144 files changed, 5389 insertions(+), 700 deletions(-) rename app/src/main/java/org/gnucash/android/db/{ => adapter}/AccountsDbAdapter.java (99%) create mode 100644 app/src/main/java/org/gnucash/android/db/adapter/BudgetAmountsDbAdapter.java create mode 100644 app/src/main/java/org/gnucash/android/db/adapter/BudgetsDbAdapter.java rename app/src/main/java/org/gnucash/android/db/{ => adapter}/CommoditiesDbAdapter.java (97%) rename app/src/main/java/org/gnucash/android/db/{ => adapter}/DatabaseAdapter.java (96%) rename app/src/main/java/org/gnucash/android/db/{ => adapter}/PricesDbAdapter.java (98%) create mode 100644 app/src/main/java/org/gnucash/android/db/adapter/RecurrenceDbAdapter.java rename app/src/main/java/org/gnucash/android/db/{ => adapter}/ScheduledActionDbAdapter.java (66%) rename app/src/main/java/org/gnucash/android/db/{ => adapter}/SplitsDbAdapter.java (94%) rename app/src/main/java/org/gnucash/android/db/{ => adapter}/TransactionsDbAdapter.java (99%) create mode 100644 app/src/main/java/org/gnucash/android/model/Budget.java create mode 100644 app/src/main/java/org/gnucash/android/model/BudgetAmount.java create mode 100644 app/src/main/java/org/gnucash/android/model/Recurrence.java create mode 100644 app/src/main/java/org/gnucash/android/ui/budget/BudgetDetailFragment.java create mode 100644 app/src/main/java/org/gnucash/android/ui/budget/BudgetFormFragment.java create mode 100644 app/src/main/java/org/gnucash/android/ui/budget/BudgetListFragment.java create mode 100644 app/src/main/java/org/gnucash/android/ui/budget/BudgetsActivity.java create mode 100644 app/src/main/java/org/gnucash/android/ui/util/RecurrenceViewClickListener.java create mode 100644 app/src/main/res/drawable-hdpi/ic_dashboard_black_24dp.png create mode 100644 app/src/main/res/drawable-mdpi/ic_dashboard_black_24dp.png create mode 100644 app/src/main/res/drawable-xhdpi/ic_dashboard_black_24dp.png create mode 100644 app/src/main/res/drawable-xxhdpi/ic_dashboard_black_24dp.png create mode 100644 app/src/main/res/drawable-xxxhdpi/ic_dashboard_black_24dp.png create mode 100644 app/src/main/res/drawable/budget_progress_indicator.xml create mode 100644 app/src/main/res/layout/activity_budgets.xml create mode 100644 app/src/main/res/layout/cardview_budget.xml create mode 100644 app/src/main/res/layout/cardview_budget_amount.xml create mode 100644 app/src/main/res/layout/fragment_budget_detail.xml create mode 100644 app/src/main/res/layout/fragment_budget_form.xml create mode 100644 app/src/main/res/layout/fragment_budget_list.xml create mode 100644 app/src/main/res/layout/item_budget_amount.xml create mode 100644 app/src/main/res/menu/budget_actions.xml create mode 100644 app/src/main/res/menu/budget_context_menu.xml create mode 100644 app/src/test/java/org/gnucash/android/test/unit/db/BudgetsDbAdapterTest.java create mode 100644 app/src/test/java/org/gnucash/android/test/unit/db/PriceDbAdapterTest.java create mode 100644 app/src/test/java/org/gnucash/android/test/unit/db/ScheduledActionDbAdapterTest.java create mode 100644 app/src/test/java/org/gnucash/android/test/unit/model/BudgetTest.java create mode 100644 app/src/test/java/org/gnucash/android/test/unit/model/RecurrenceTest.java create mode 100644 app/src/test/java/org/gnucash/android/test/unit/model/ScheduledActionTest.java diff --git a/CHANGELOG.md b/CHANGELOG.md index 4c6ad17d7..ea6e0e2e8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ Change Log =============================================================================== +Version 2.1.0 *(2016-xx-xx)* +---------------------------- +* Feature: Budgets +* Improved: Scheduled transactions now have more accurate timestamps +* Improved: Generate all scheduled transactions even if a scheduled is missed (e.g. device off) + Version 2.0.2 *(2015-11-20)* ---------------------------- * Fixed: Exporting to external service does not work in some devices diff --git a/app/build.gradle b/app/build.gradle index 3e0644c23..15868c436 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -90,6 +90,7 @@ android { } debug { debuggable true + testCoverageEnabled true signingConfig signingConfigs.debug } } @@ -146,8 +147,8 @@ def adb = android.getAdbExe().toString() afterEvaluate { initCrashlyticsPropertiesIfNeeded() - - task grantAnimationPermissionDevel(type: Exec, dependsOn: 'installDevelopmentDebug') { // or install{productFlavour}{buildType} + + task grantTestPermissionsDevel(type: Exec, dependsOn: 'installDevelopmentDebug') { // or install{productFlavour}{buildType} commandLine "$adb", 'devices' standardOutput = new ByteArrayOutputStream() @@ -159,7 +160,7 @@ afterEvaluate { } } - task grantAnimationPermissionProduction(type: Exec, dependsOn: 'installProductionDebug'){ + task grantTestPermissionsProduction(type: Exec, dependsOn: 'installProductionDebug'){ commandLine "$adb -e shell pm grant $android.defaultConfig.applicationId android.permission.SET_ANIMATION_SCALE".split(' ') commandLine "$adb -e shell pm grant $android.defaultConfig.applicationId android.permission.WRITE_EXTERNAL_STORAGE".split(' ') } @@ -167,18 +168,18 @@ afterEvaluate { // get called directly, not the install* versions tasks.each { task -> if (task.name.startsWith('assembleDevelopmentDebugAndroidTest')){ - task.dependsOn grantAnimationPermissionDevel + task.dependsOn grantTestPermissionsDevel } else if (task.name.startsWith('assembleBetaDebugAndroidTest')){ - task.dependsOn grantAnimationPermissionProduction + task.dependsOn grantTestPermissionsProduction } else if (task.name.startsWith('assembleProductionDebugAndroidTest')){ - task.dependsOn grantAnimationPermissionProduction + task.dependsOn grantTestPermissionsProduction } } } -def androidSupportVersion = "22.2.1" -def androidEspressoVersion = "2.2" -def androidSupportTestVersion = "0.3" +def androidSupportVersion = "23.1.0" +def androidEspressoVersion = "2.2.1" +def androidSupportTestVersion = "0.4.1" dependencies { compile fileTree(dir: 'libs', include: ['*.jar']) @@ -188,7 +189,7 @@ dependencies { 'com.android.support:cardview-v7:' + androidSupportVersion, 'com.android.support:recyclerview-v7:' + androidSupportVersion, 'com.viewpagerindicator:library:2.4.1@aar', - 'com.code-troopers.betterpickers:library:2.0.3', + 'com.code-troopers.betterpickers:library:2.1.2', 'org.jraf:android-switch-backport:2.0.1@aar', 'com.github.PhilJay:MPAndroidChart:v2.1.3', 'joda-time:joda-time:2.7', @@ -223,7 +224,7 @@ dependencies { exclude module: 'recyclerview-v7' } - androidTestCompile('com.squareup.assertj:assertj-android:1.1.0'){ + androidTestCompile('com.squareup.assertj:assertj-android:1.1.1'){ exclude group: 'com.android.support', module:'support-annotations' } } diff --git a/app/src/androidTest/java/org/gnucash/android/test/ui/AccountsActivityTest.java b/app/src/androidTest/java/org/gnucash/android/test/ui/AccountsActivityTest.java index 3d2d8f97e..89c70f7dd 100644 --- a/app/src/androidTest/java/org/gnucash/android/test/ui/AccountsActivityTest.java +++ b/app/src/androidTest/java/org/gnucash/android/test/ui/AccountsActivityTest.java @@ -24,7 +24,6 @@ import android.preference.PreferenceManager; import android.support.test.InstrumentationRegistry; import android.support.test.espresso.Espresso; -import android.support.test.espresso.ViewInteraction; import android.support.test.espresso.matcher.ViewMatchers; import android.support.test.runner.AndroidJUnit4; import android.support.v4.app.Fragment; @@ -34,10 +33,10 @@ import com.kobakei.ratethisapp.RateThisApp; import org.gnucash.android.R; -import org.gnucash.android.db.AccountsDbAdapter; +import org.gnucash.android.db.adapter.AccountsDbAdapter; import org.gnucash.android.db.DatabaseHelper; -import org.gnucash.android.db.SplitsDbAdapter; -import org.gnucash.android.db.TransactionsDbAdapter; +import org.gnucash.android.db.adapter.SplitsDbAdapter; +import org.gnucash.android.db.adapter.TransactionsDbAdapter; import org.gnucash.android.model.Account; import org.gnucash.android.model.AccountType; import org.gnucash.android.model.Commodity; diff --git a/app/src/androidTest/java/org/gnucash/android/test/ui/ExportTransactionsTest.java b/app/src/androidTest/java/org/gnucash/android/test/ui/ExportTransactionsTest.java index 05c9bbedc..ca0cde768 100644 --- a/app/src/androidTest/java/org/gnucash/android/test/ui/ExportTransactionsTest.java +++ b/app/src/androidTest/java/org/gnucash/android/test/ui/ExportTransactionsTest.java @@ -33,11 +33,11 @@ import org.gnucash.android.R; import org.gnucash.android.app.GnuCashApplication; -import org.gnucash.android.db.AccountsDbAdapter; import org.gnucash.android.db.DatabaseHelper; -import org.gnucash.android.db.ScheduledActionDbAdapter; -import org.gnucash.android.db.SplitsDbAdapter; -import org.gnucash.android.db.TransactionsDbAdapter; +import org.gnucash.android.db.adapter.AccountsDbAdapter; +import org.gnucash.android.db.adapter.ScheduledActionDbAdapter; +import org.gnucash.android.db.adapter.SplitsDbAdapter; +import org.gnucash.android.db.adapter.TransactionsDbAdapter; import org.gnucash.android.export.ExportFormat; import org.gnucash.android.export.Exporter; import org.gnucash.android.model.Account; @@ -193,7 +193,7 @@ public void testExport(ExportFormat format){ file.delete(); } - DrawerActions.openDrawer(R.id.drawer_layout); + onView(withId(R.id.drawer_layout)).perform(DrawerActions.open()); onView(withText(R.string.nav_menu_export)).perform(click()); onView(withId(R.id.spinner_export_destination)).perform(click()); @@ -229,7 +229,7 @@ public void testDeleteTransactionsAfterExport(){ */ @Test public void testShouldCreateExportSchedule(){ - DrawerActions.openDrawer(R.id.drawer_layout); + onView(withId(R.id.drawer_layout)).perform(DrawerActions.open()); onView(withText(R.string.nav_menu_export)).perform(click()); onView(withText(ExportFormat.XML.name())).perform(click()); @@ -237,7 +237,7 @@ public void testShouldCreateExportSchedule(){ //switch on recurrence dialog onView(allOf(isAssignableFrom(CompoundButton.class), isDisplayed(), isEnabled())).perform(click()); - onView(withText("Done")).perform(click()); + onView(withText("OK")).perform(click()); onView(withId(R.id.menu_save)).perform(click()); ScheduledActionDbAdapter scheduledactionDbAdapter = new ScheduledActionDbAdapter(mDb); @@ -247,7 +247,7 @@ public void testShouldCreateExportSchedule(){ .extracting("mActionType").contains(ScheduledAction.ActionType.BACKUP); ScheduledAction action = scheduledActions.get(0); - assertThat(action.getPeriodType()).isEqualTo(PeriodType.WEEK); + assertThat(action.getRecurrence().getPeriodType()).isEqualTo(PeriodType.WEEK); assertThat(action.getEndTime()).isEqualTo(0); } diff --git a/app/src/androidTest/java/org/gnucash/android/test/ui/FirstRunWizardActivityTest.java b/app/src/androidTest/java/org/gnucash/android/test/ui/FirstRunWizardActivityTest.java index 96de31a91..a480b56ee 100644 --- a/app/src/androidTest/java/org/gnucash/android/test/ui/FirstRunWizardActivityTest.java +++ b/app/src/androidTest/java/org/gnucash/android/test/ui/FirstRunWizardActivityTest.java @@ -24,10 +24,10 @@ import org.gnucash.android.R; import org.gnucash.android.app.GnuCashApplication; -import org.gnucash.android.db.AccountsDbAdapter; +import org.gnucash.android.db.adapter.AccountsDbAdapter; import org.gnucash.android.db.DatabaseHelper; -import org.gnucash.android.db.SplitsDbAdapter; -import org.gnucash.android.db.TransactionsDbAdapter; +import org.gnucash.android.db.adapter.SplitsDbAdapter; +import org.gnucash.android.db.adapter.TransactionsDbAdapter; import org.gnucash.android.ui.wizard.FirstRunWizardActivity; import org.junit.Before; import org.junit.Test; diff --git a/app/src/androidTest/java/org/gnucash/android/test/ui/PieChartReportTest.java b/app/src/androidTest/java/org/gnucash/android/test/ui/PieChartReportTest.java index 72ce5e0fe..e2928529d 100644 --- a/app/src/androidTest/java/org/gnucash/android/test/ui/PieChartReportTest.java +++ b/app/src/androidTest/java/org/gnucash/android/test/ui/PieChartReportTest.java @@ -26,19 +26,17 @@ import android.support.test.espresso.action.GeneralClickAction; import android.support.test.espresso.action.Press; import android.support.test.espresso.action.Tap; -import android.support.test.espresso.contrib.PickerActions; import android.support.test.runner.AndroidJUnit4; import android.test.ActivityInstrumentationTestCase2; import android.util.Log; import android.view.View; -import android.widget.DatePicker; import org.gnucash.android.R; import org.gnucash.android.app.GnuCashApplication; -import org.gnucash.android.db.AccountsDbAdapter; +import org.gnucash.android.db.adapter.AccountsDbAdapter; import org.gnucash.android.db.DatabaseHelper; -import org.gnucash.android.db.SplitsDbAdapter; -import org.gnucash.android.db.TransactionsDbAdapter; +import org.gnucash.android.db.adapter.SplitsDbAdapter; +import org.gnucash.android.db.adapter.TransactionsDbAdapter; import org.gnucash.android.importer.GncXmlImporter; import org.gnucash.android.model.Account; import org.gnucash.android.model.AccountType; @@ -61,14 +59,8 @@ import static android.support.test.espresso.Espresso.onView; import static android.support.test.espresso.action.ViewActions.click; import static android.support.test.espresso.assertion.ViewAssertions.matches; -import static android.support.test.espresso.matcher.ViewMatchers.isEnabled; -import static android.support.test.espresso.matcher.ViewMatchers.withClassName; import static android.support.test.espresso.matcher.ViewMatchers.withId; import static android.support.test.espresso.matcher.ViewMatchers.withText; -import static org.hamcrest.Matchers.anyOf; -import static org.hamcrest.Matchers.containsString; -import static org.hamcrest.Matchers.equalTo; -import static org.hamcrest.Matchers.not; @RunWith(AndroidJUnit4.class) public class PieChartReportTest extends ActivityInstrumentationTestCase2 { diff --git a/app/src/androidTest/java/org/gnucash/android/test/ui/TransactionsActivityTest.java b/app/src/androidTest/java/org/gnucash/android/test/ui/TransactionsActivityTest.java index 6ec47912b..a3f93b2a3 100644 --- a/app/src/androidTest/java/org/gnucash/android/test/ui/TransactionsActivityTest.java +++ b/app/src/androidTest/java/org/gnucash/android/test/ui/TransactionsActivityTest.java @@ -30,11 +30,11 @@ import android.util.Log; import org.gnucash.android.R; -import org.gnucash.android.db.AccountsDbAdapter; +import org.gnucash.android.db.adapter.AccountsDbAdapter; import org.gnucash.android.db.DatabaseHelper; import org.gnucash.android.db.DatabaseSchema; -import org.gnucash.android.db.SplitsDbAdapter; -import org.gnucash.android.db.TransactionsDbAdapter; +import org.gnucash.android.db.adapter.SplitsDbAdapter; +import org.gnucash.android.db.adapter.TransactionsDbAdapter; import org.gnucash.android.model.Account; import org.gnucash.android.model.Commodity; import org.gnucash.android.model.Money; diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index b35c4450d..320bf6546 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -50,7 +50,8 @@ + android:theme="@style/Theme.GnucashTheme.NoActionBar" + android:allowBackup="true"> @@ -102,6 +103,8 @@ android:configChanges="orientation|screenSize"/> + diff --git a/app/src/main/java/org/gnucash/android/app/GnuCashApplication.java b/app/src/main/java/org/gnucash/android/app/GnuCashApplication.java index 8e9985637..6ba557bf0 100644 --- a/app/src/main/java/org/gnucash/android/app/GnuCashApplication.java +++ b/app/src/main/java/org/gnucash/android/app/GnuCashApplication.java @@ -34,13 +34,16 @@ import com.uservoice.uservoicesdk.UserVoice; import org.gnucash.android.R; -import org.gnucash.android.db.AccountsDbAdapter; -import org.gnucash.android.db.CommoditiesDbAdapter; import org.gnucash.android.db.DatabaseHelper; -import org.gnucash.android.db.PricesDbAdapter; -import org.gnucash.android.db.ScheduledActionDbAdapter; -import org.gnucash.android.db.SplitsDbAdapter; -import org.gnucash.android.db.TransactionsDbAdapter; +import org.gnucash.android.db.adapter.AccountsDbAdapter; +import org.gnucash.android.db.adapter.BudgetAmountsDbAdapter; +import org.gnucash.android.db.adapter.BudgetsDbAdapter; +import org.gnucash.android.db.adapter.CommoditiesDbAdapter; +import org.gnucash.android.db.adapter.PricesDbAdapter; +import org.gnucash.android.db.adapter.RecurrenceDbAdapter; +import org.gnucash.android.db.adapter.ScheduledActionDbAdapter; +import org.gnucash.android.db.adapter.SplitsDbAdapter; +import org.gnucash.android.db.adapter.TransactionsDbAdapter; import org.gnucash.android.model.Commodity; import org.gnucash.android.model.Money; import org.gnucash.android.service.SchedulerService; @@ -85,6 +88,12 @@ public class GnuCashApplication extends Application{ private static PricesDbAdapter mPricesDbAdapter; + private static BudgetsDbAdapter mBudgetsDbAdapter; + + private static BudgetAmountsDbAdapter mBudgetAmountsDbAdapter; + + private static RecurrenceDbAdapter mRecurrenceDbAdapter; + /** * Returns darker version of specified color. * Use for theming the status bar color when setting the color of the actionBar @@ -125,6 +134,9 @@ public void onCreate(){ mScheduledActionDbAdapter = new ScheduledActionDbAdapter(mDb); mCommoditiesDbAdapter = new CommoditiesDbAdapter(mDb); mPricesDbAdapter = new PricesDbAdapter(mDb); + mBudgetAmountsDbAdapter = new BudgetAmountsDbAdapter(mDb); + mBudgetsDbAdapter = new BudgetsDbAdapter(mDb); + mRecurrenceDbAdapter = new RecurrenceDbAdapter(mDb); setDefaultCurrencyCode(getDefaultCurrencyCode()); } @@ -153,6 +165,18 @@ public static PricesDbAdapter getPricesDbAdapter(){ return mPricesDbAdapter; } + public static BudgetsDbAdapter getBudgetDbAdapter() { + return mBudgetsDbAdapter; + } + + public static RecurrenceDbAdapter getRecurrenceDbAdapter() { + return mRecurrenceDbAdapter; + } + + public static BudgetAmountsDbAdapter getBudgetAmountsDbAdapter(){ + return mBudgetAmountsDbAdapter; + } + /** * Returns the application context * @return Application {@link Context} object diff --git a/app/src/main/java/org/gnucash/android/db/DatabaseCursorLoader.java b/app/src/main/java/org/gnucash/android/db/DatabaseCursorLoader.java index 21a426e17..849400e64 100644 --- a/app/src/main/java/org/gnucash/android/db/DatabaseCursorLoader.java +++ b/app/src/main/java/org/gnucash/android/db/DatabaseCursorLoader.java @@ -21,11 +21,13 @@ import android.support.v4.content.AsyncTaskLoader; import android.support.v4.content.Loader; +import org.gnucash.android.db.adapter.DatabaseAdapter; + /** * Abstract base class for asynchronously loads records from a database and manages the cursor. * In order to use this class, you must subclass it and implement the * {@link #loadInBackground()} method to load the particular records from the database. - * Ideally, the database has {@link DatabaseAdapter} which is used for managing access to the + * Ideally, the database has {@link DatabaseAdapter} which is used for managing access to the * records from the database * @author Ngewi Fet * @see DatabaseAdapter diff --git a/app/src/main/java/org/gnucash/android/db/DatabaseHelper.java b/app/src/main/java/org/gnucash/android/db/DatabaseHelper.java index 440e5c7e5..e3722c6cb 100644 --- a/app/src/main/java/org/gnucash/android/db/DatabaseHelper.java +++ b/app/src/main/java/org/gnucash/android/db/DatabaseHelper.java @@ -34,7 +34,16 @@ import javax.xml.parsers.ParserConfigurationException; -import static org.gnucash.android.db.DatabaseSchema.*; +import static org.gnucash.android.db.DatabaseSchema.AccountEntry; +import static org.gnucash.android.db.DatabaseSchema.BudgetAmountEntry; +import static org.gnucash.android.db.DatabaseSchema.BudgetEntry; +import static org.gnucash.android.db.DatabaseSchema.CommodityEntry; +import static org.gnucash.android.db.DatabaseSchema.CommonColumns; +import static org.gnucash.android.db.DatabaseSchema.PriceEntry; +import static org.gnucash.android.db.DatabaseSchema.RecurrenceEntry; +import static org.gnucash.android.db.DatabaseSchema.ScheduledActionEntry; +import static org.gnucash.android.db.DatabaseSchema.SplitEntry; +import static org.gnucash.android.db.DatabaseSchema.TransactionEntry; /** * Helper class for managing the SQLite database. * Creates the database and handles upgrades @@ -110,28 +119,36 @@ public class DatabaseHelper extends SQLiteOpenHelper { + SplitEntry.COLUMN_QUANTITY_DENOM + " integer not null, " + SplitEntry.COLUMN_ACCOUNT_UID + " varchar(255) not null, " + SplitEntry.COLUMN_TRANSACTION_UID + " varchar(255) not null, " - + SplitEntry.COLUMN_CREATED_AT + " TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, " - + SplitEntry.COLUMN_MODIFIED_AT + " TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, " + + SplitEntry.COLUMN_RECONCILE_STATE + " varchar(1) not null default 'n', " + + SplitEntry.COLUMN_RECONCILE_DATE + " timestamp not null default current_timestamp, " + + SplitEntry.COLUMN_CREATED_AT + " TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, " + + SplitEntry.COLUMN_MODIFIED_AT + " TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, " + "FOREIGN KEY (" + SplitEntry.COLUMN_ACCOUNT_UID + ") REFERENCES " + AccountEntry.TABLE_NAME + " (" + AccountEntry.COLUMN_UID + ") ON DELETE CASCADE, " + "FOREIGN KEY (" + SplitEntry.COLUMN_TRANSACTION_UID + ") REFERENCES " + TransactionEntry.TABLE_NAME + " (" + TransactionEntry.COLUMN_UID + ") ON DELETE CASCADE " + ");" + createUpdatedAtTrigger(SplitEntry.TABLE_NAME); public static final String SCHEDULED_ACTIONS_TABLE_CREATE = "CREATE TABLE " + ScheduledActionEntry.TABLE_NAME + " (" - + ScheduledActionEntry._ID + " integer primary key autoincrement, " - + ScheduledActionEntry.COLUMN_UID + " varchar(255) not null UNIQUE, " - + ScheduledActionEntry.COLUMN_ACTION_UID + " varchar(255) not null, " - + ScheduledActionEntry.COLUMN_TYPE + " varchar(255) not null, " - + ScheduledActionEntry.COLUMN_PERIOD + " integer not null, " - + ScheduledActionEntry.COLUMN_LAST_RUN + " integer default 0, " - + ScheduledActionEntry.COLUMN_START_TIME + " integer not null, " - + ScheduledActionEntry.COLUMN_END_TIME + " integer default 0, " - + ScheduledActionEntry.COLUMN_TAG + " text, " - + ScheduledActionEntry.COLUMN_ENABLED + " tinyint default 1, " //enabled by default - + ScheduledActionEntry.COLUMN_TOTAL_FREQUENCY + " integer default 0, " - + ScheduledActionEntry.COLUMN_EXECUTION_COUNT+ " integer default 0, " - + ScheduledActionEntry.COLUMN_CREATED_AT + " TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, " - + ScheduledActionEntry.COLUMN_MODIFIED_AT + " TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP " + + ScheduledActionEntry._ID + " integer primary key autoincrement, " + + ScheduledActionEntry.COLUMN_UID + " varchar(255) not null UNIQUE, " + + ScheduledActionEntry.COLUMN_ACTION_UID + " varchar(255) not null, " + + ScheduledActionEntry.COLUMN_TYPE + " varchar(255) not null, " + + ScheduledActionEntry.COLUMN_RECURRENCE_UID + " varchar(255) not null, " + + ScheduledActionEntry.COLUMN_TEMPLATE_ACCT_UID + " varchar(255) not null, " + + ScheduledActionEntry.COLUMN_LAST_RUN + " integer default 0, " + + ScheduledActionEntry.COLUMN_START_TIME + " integer not null, " + + ScheduledActionEntry.COLUMN_END_TIME + " integer default 0, " + + ScheduledActionEntry.COLUMN_TAG + " text, " + + ScheduledActionEntry.COLUMN_ENABLED + " tinyint default 1, " //enabled by default + + ScheduledActionEntry.COLUMN_AUTO_CREATE + " tinyint default 1, " + + ScheduledActionEntry.COLUMN_AUTO_NOTIFY + " tinyint default 0, " + + ScheduledActionEntry.COLUMN_ADVANCE_CREATION + " integer default 0, " + + ScheduledActionEntry.COLUMN_ADVANCE_NOTIFY + " integer default 0, " + + ScheduledActionEntry.COLUMN_TOTAL_FREQUENCY + " integer default 0, " + + ScheduledActionEntry.COLUMN_EXECUTION_COUNT + " integer default 0, " + + ScheduledActionEntry.COLUMN_CREATED_AT + " TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, " + + ScheduledActionEntry.COLUMN_MODIFIED_AT + " TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, " + + "FOREIGN KEY (" + ScheduledActionEntry.COLUMN_RECURRENCE_UID + ") REFERENCES " + RecurrenceEntry.TABLE_NAME + " (" + RecurrenceEntry.COLUMN_UID + ") " + ");" + createUpdatedAtTrigger(ScheduledActionEntry.TABLE_NAME); public static final String COMMODITIES_TABLE_CREATE = "CREATE TABLE " + DatabaseSchema.CommodityEntry.TABLE_NAME + " (" @@ -168,6 +185,47 @@ public class DatabaseHelper extends SQLiteOpenHelper { + "FOREIGN KEY (" + PriceEntry.COLUMN_CURRENCY_UID + ") REFERENCES " + CommodityEntry.TABLE_NAME + " (" + CommodityEntry.COLUMN_UID + ") ON DELETE CASCADE " + ");" + createUpdatedAtTrigger(PriceEntry.TABLE_NAME); + + private static final String BUDGETS_TABLE_CREATE = "CREATE TABLE " + BudgetEntry.TABLE_NAME + " (" + + BudgetEntry._ID + " integer primary key autoincrement, " + + BudgetEntry.COLUMN_UID + " varchar(255) not null UNIQUE, " + + BudgetEntry.COLUMN_NAME + " varchar(255) not null, " + + BudgetEntry.COLUMN_DESCRIPTION + " varchar(255), " + + BudgetEntry.COLUMN_RECURRENCE_UID + " varchar(255) not null, " + + BudgetEntry.COLUMN_NUM_PERIODS + " integer, " + + BudgetEntry.COLUMN_CREATED_AT + " TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, " + + BudgetEntry.COLUMN_MODIFIED_AT + " TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, " + + "FOREIGN KEY (" + BudgetEntry.COLUMN_RECURRENCE_UID + ") REFERENCES " + RecurrenceEntry.TABLE_NAME + " (" + RecurrenceEntry.COLUMN_UID + ") " + + ");" + createUpdatedAtTrigger(BudgetEntry.TABLE_NAME); + + private static final String BUDGET_AMOUNTS_TABLE_CREATE = "CREATE TABLE " + BudgetAmountEntry.TABLE_NAME + " (" + + BudgetAmountEntry._ID + " integer primary key autoincrement, " + + BudgetAmountEntry.COLUMN_UID + " varchar(255) not null UNIQUE, " + + BudgetAmountEntry.COLUMN_BUDGET_UID + " varchar(255) not null, " + + BudgetAmountEntry.COLUMN_ACCOUNT_UID + " varchar(255) not null, " + + BudgetAmountEntry.COLUMN_AMOUNT_NUM + " integer not null, " + + BudgetAmountEntry.COLUMN_AMOUNT_DENOM + " integer not null, " + + BudgetAmountEntry.COLUMN_PERIOD_NUM + " integer not null, " + + BudgetAmountEntry.COLUMN_CREATED_AT + " TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, " + + BudgetAmountEntry.COLUMN_MODIFIED_AT + " TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, " + + "FOREIGN KEY (" + BudgetAmountEntry.COLUMN_ACCOUNT_UID + ") REFERENCES " + AccountEntry.TABLE_NAME + " (" + AccountEntry.COLUMN_UID + ") ON DELETE CASCADE, " + + "FOREIGN KEY (" + BudgetAmountEntry.COLUMN_BUDGET_UID + ") REFERENCES " + BudgetEntry.TABLE_NAME + " (" + BudgetEntry.COLUMN_UID + ") ON DELETE CASCADE " + + ");" + createUpdatedAtTrigger(BudgetAmountEntry.TABLE_NAME); + + + private static final String RECURRENCE_TABLE_CREATE = "CREATE TABLE " + RecurrenceEntry.TABLE_NAME + " (" + + RecurrenceEntry._ID + " integer primary key autoincrement, " + + RecurrenceEntry.COLUMN_UID + " varchar(255) not null UNIQUE, " + + RecurrenceEntry.COLUMN_MULTIPLIER + " integer not null default 1, " + + RecurrenceEntry.COLUMN_PERIOD_TYPE + " varchar(255) not null, " + + RecurrenceEntry.COLUMN_BYDAY + " varchar(255), " + + RecurrenceEntry.COLUMN_PERIOD_START + " timestamp not null, " + + RecurrenceEntry.COLUMN_PERIOD_END + " timestamp, " + + RecurrenceEntry.COLUMN_CREATED_AT + " TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, " + + RecurrenceEntry.COLUMN_MODIFIED_AT + " TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP); " + + createUpdatedAtTrigger(RecurrenceEntry.TABLE_NAME); + + /** * Constructor * @param context Application context @@ -212,8 +270,8 @@ public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion){ /* * NOTE: In order to modify the database, create a new static method in the MigrationHelper class * called upgradeDbToVersion<#>, e.g. int upgradeDbToVersion10(SQLiteDatabase) in order to upgrade to version 10. - * The upgrade method should return the upgraded database version as the return value. - * Then all you need to do is increment the DatabaseSchema.DATABASE_VERSION to the appropriate number. + * The upgrade method should return the new (upgraded) database version as the return value. + * Then all you need to do is increment the DatabaseSchema.DATABASE_VERSION to the appropriate number to trigger an upgrade. */ if (oldVersion > newVersion) { throw new IllegalArgumentException("Database downgrades are not supported at the moment"); @@ -246,7 +304,7 @@ public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion){ /** - * Creates the tables in the database + * Creates the tables in the database and import default commodities into the database * @param db Database instance */ private void createDatabaseTables(SQLiteDatabase db) { @@ -257,6 +315,10 @@ private void createDatabaseTables(SQLiteDatabase db) { db.execSQL(SCHEDULED_ACTIONS_TABLE_CREATE); db.execSQL(COMMODITIES_TABLE_CREATE); db.execSQL(PRICES_TABLE_CREATE); + db.execSQL(RECURRENCE_TABLE_CREATE); + db.execSQL(BUDGETS_TABLE_CREATE); + db.execSQL(BUDGET_AMOUNTS_TABLE_CREATE); + String createAccountUidIndex = "CREATE UNIQUE INDEX '" + AccountEntry.INDEX_UID + "' ON " + AccountEntry.TABLE_NAME + "(" + AccountEntry.COLUMN_UID + ")"; @@ -276,12 +338,24 @@ private void createDatabaseTables(SQLiteDatabase db) { String createPriceUidIndex = "CREATE UNIQUE INDEX '" + PriceEntry.INDEX_UID + "' ON " + PriceEntry.TABLE_NAME + "(" + PriceEntry.COLUMN_UID + ")"; + String createBudgetUidIndex = "CREATE UNIQUE INDEX '" + BudgetEntry.INDEX_UID + + "' ON " + BudgetEntry.TABLE_NAME + "(" + BudgetEntry.COLUMN_UID + ")"; + + String createBudgetAmountUidIndex = "CREATE UNIQUE INDEX '" + BudgetAmountEntry.INDEX_UID + + "' ON " + BudgetAmountEntry.TABLE_NAME + "(" + BudgetAmountEntry.COLUMN_UID + ")"; + + String createRecurrenceUidIndex = "CREATE UNIQUE INDEX '" + RecurrenceEntry.INDEX_UID + + "' ON " + RecurrenceEntry.TABLE_NAME + "(" + RecurrenceEntry.COLUMN_UID + ")"; + db.execSQL(createAccountUidIndex); db.execSQL(createTransactionUidIndex); db.execSQL(createSplitUidIndex); db.execSQL(createScheduledEventUidIndex); db.execSQL(createCommodityUidIndex); db.execSQL(createPriceUidIndex); + db.execSQL(createBudgetUidIndex); + db.execSQL(createRecurrenceUidIndex); + db.execSQL(createBudgetAmountUidIndex); try { MigrationHelper.importCommodities(db); diff --git a/app/src/main/java/org/gnucash/android/db/DatabaseSchema.java b/app/src/main/java/org/gnucash/android/db/DatabaseSchema.java index 15d7870cc..78afc95fb 100644 --- a/app/src/main/java/org/gnucash/android/db/DatabaseSchema.java +++ b/app/src/main/java/org/gnucash/android/db/DatabaseSchema.java @@ -28,12 +28,7 @@ public class DatabaseSchema { * Database version. * With any change to the database schema, this number must increase */ - public static final int DATABASE_VERSION = 11; - - /** - * Database version where Splits were introduced - */ - public static final int SPLITS_DB_VERSION = 7; + public static final int DATABASE_VERSION = 12; //no instances are to be instantiated private DatabaseSchema(){} @@ -116,6 +111,9 @@ public static abstract class SplitEntry implements CommonColumns { public static final String COLUMN_ACCOUNT_UID = "account_uid"; public static final String COLUMN_TRANSACTION_UID = "transaction_uid"; + public static final String COLUMN_RECONCILE_STATE = "reconcile_state"; + public static final String COLUMN_RECONCILE_DATE = "reconcile_date"; + public static final String INDEX_UID = "split_uid_index"; } @@ -127,12 +125,27 @@ public static abstract class ScheduledActionEntry implements CommonColumns { public static final String COLUMN_START_TIME = "start_time"; public static final String COLUMN_END_TIME = "end_time"; public static final String COLUMN_LAST_RUN = "last_run"; - public static final String COLUMN_PERIOD = "period"; - public static final String COLUMN_TAG = "tag"; //for any action-specific information + + /** + * Tag for scheduledAction-specific information e.g. backup parameters for backup + */ + public static final String COLUMN_TAG = "tag"; public static final String COLUMN_ENABLED = "is_enabled"; public static final String COLUMN_TOTAL_FREQUENCY = "total_frequency"; + + /** + * Number of times this scheduledAction has been run. Analogous to instance_count in GnuCash desktop SQL + */ public static final String COLUMN_EXECUTION_COUNT = "execution_count"; + public static final String COLUMN_RECURRENCE_UID = "recurrence_uid"; + public static final String COLUMN_AUTO_CREATE = "auto_create"; + public static final String COLUMN_AUTO_NOTIFY = "auto_notify"; + public static final String COLUMN_ADVANCE_CREATION = "adv_creation"; + public static final String COLUMN_ADVANCE_NOTIFY = "adv_notify"; + public static final String COLUMN_TEMPLATE_ACCT_UID = "template_act_uid"; + + public static final String INDEX_UID = "scheduled_action_uid_index"; } @@ -191,4 +204,42 @@ public static abstract class PriceEntry implements CommonColumns { public static final String INDEX_UID = "prices_uid_index"; } + + + public static abstract class BudgetEntry implements CommonColumns { + public static final String TABLE_NAME = "budgets"; + + public static final String COLUMN_NAME = "name"; + public static final String COLUMN_DESCRIPTION = "description"; + public static final String COLUMN_NUM_PERIODS = "num_periods"; + public static final String COLUMN_RECURRENCE_UID = "recurrence_uid"; + + public static final String INDEX_UID = "budgets_uid_index"; + } + + + public static abstract class BudgetAmountEntry implements CommonColumns { + public static final String TABLE_NAME = "budget_amounts"; + + public static final String COLUMN_BUDGET_UID = "budget_uid"; + public static final String COLUMN_ACCOUNT_UID = "account_uid"; + public static final String COLUMN_PERIOD_NUM = "period_num"; + public static final String COLUMN_AMOUNT_NUM = "amount_num"; + public static final String COLUMN_AMOUNT_DENOM = "amount_denom"; + + public static final String INDEX_UID = "budget_amounts_uid_index"; + } + + + public static abstract class RecurrenceEntry implements CommonColumns { + public static final String TABLE_NAME = "recurrences"; + + public static final String COLUMN_MULTIPLIER = "recurrence_mult"; + public static final String COLUMN_PERIOD_TYPE = "recurrence_period_type"; + public static final String COLUMN_PERIOD_START = "recurrence_period_start"; + public static final String COLUMN_PERIOD_END = "recurrence_period_end"; + public static final String COLUMN_BYDAY = "recurrence_byday"; + + public static final String INDEX_UID = "recurrence_uid_index"; + } } diff --git a/app/src/main/java/org/gnucash/android/db/MigrationHelper.java b/app/src/main/java/org/gnucash/android/db/MigrationHelper.java index f8d91f088..1617ccb1b 100644 --- a/app/src/main/java/org/gnucash/android/db/MigrationHelper.java +++ b/app/src/main/java/org/gnucash/android/db/MigrationHelper.java @@ -32,6 +32,7 @@ import org.gnucash.android.R; import org.gnucash.android.app.GnuCashApplication; +import org.gnucash.android.db.adapter.AccountsDbAdapter; import org.gnucash.android.export.ExportFormat; import org.gnucash.android.export.ExportParams; import org.gnucash.android.export.Exporter; @@ -40,6 +41,8 @@ import org.gnucash.android.model.BaseModel; import org.gnucash.android.model.Commodity; import org.gnucash.android.model.Money; +import org.gnucash.android.model.PeriodType; +import org.gnucash.android.model.Recurrence; import org.gnucash.android.model.ScheduledAction; import org.gnucash.android.model.Transaction; import org.xml.sax.InputSource; @@ -65,9 +68,12 @@ import javax.xml.parsers.SAXParserFactory; import static org.gnucash.android.db.DatabaseSchema.AccountEntry; +import static org.gnucash.android.db.DatabaseSchema.BudgetAmountEntry; +import static org.gnucash.android.db.DatabaseSchema.BudgetEntry; import static org.gnucash.android.db.DatabaseSchema.CommodityEntry; import static org.gnucash.android.db.DatabaseSchema.CommonColumns; import static org.gnucash.android.db.DatabaseSchema.PriceEntry; +import static org.gnucash.android.db.DatabaseSchema.RecurrenceEntry; import static org.gnucash.android.db.DatabaseSchema.ScheduledActionEntry; import static org.gnucash.android.db.DatabaseSchema.SplitEntry; import static org.gnucash.android.db.DatabaseSchema.TransactionEntry; @@ -83,7 +89,7 @@ public class MigrationHelper { /** * Performs same function as {@link AccountsDbAdapter#getFullyQualifiedAccountName(String)} - *

This method is only necessary because we cannot open the database again (by instantiating {@link org.gnucash.android.db.AccountsDbAdapter} + *

This method is only necessary because we cannot open the database again (by instantiating {@link AccountsDbAdapter} * while it is locked for upgrades. So we re-implement the method here.

* @param db SQLite database * @param accountUID Unique ID of account whose fully qualified name is to be determined @@ -495,7 +501,7 @@ static int upgradeDbToVersion8(SQLiteDatabase db) { + ScheduledActionEntry.COLUMN_UID + " varchar(255) not null UNIQUE, " + ScheduledActionEntry.COLUMN_ACTION_UID + " varchar(255) not null, " + ScheduledActionEntry.COLUMN_TYPE + " varchar(255) not null, " - + ScheduledActionEntry.COLUMN_PERIOD + " integer not null, " + + "period " + " integer not null, " + ScheduledActionEntry.COLUMN_LAST_RUN + " integer default 0, " + ScheduledActionEntry.COLUMN_START_TIME + " integer not null, " + ScheduledActionEntry.COLUMN_END_TIME + " integer default 0, " @@ -711,7 +717,7 @@ static int upgradeDbToVersion8(SQLiteDatabase db) { contentValues.put(CommonColumns.COLUMN_UID, BaseModel.generateUID()); contentValues.put(CommonColumns.COLUMN_CREATED_AT, timestamp); contentValues.put(ScheduledActionEntry.COLUMN_ACTION_UID, cursor.getString(cursor.getColumnIndexOrThrow(TransactionEntry.COLUMN_UID))); - contentValues.put(ScheduledActionEntry.COLUMN_PERIOD, cursor.getLong(cursor.getColumnIndexOrThrow("recurrence_period"))); + contentValues.put("period", cursor.getLong(cursor.getColumnIndexOrThrow("recurrence_period"))); contentValues.put(ScheduledActionEntry.COLUMN_START_TIME, timestampT.getTime()); contentValues.put(ScheduledActionEntry.COLUMN_END_TIME, 0); contentValues.put(ScheduledActionEntry.COLUMN_LAST_RUN, lastRun); @@ -850,7 +856,7 @@ static int upgradeDbToVersion9(SQLiteDatabase db){ db.beginTransaction(); try { - String createCommoditiesSql = "CREATE TABLE " + CommodityEntry.TABLE_NAME + " (" + db.execSQL("CREATE TABLE " + CommodityEntry.TABLE_NAME + " (" + CommodityEntry._ID + " integer primary key autoincrement, " + CommodityEntry.COLUMN_UID + " varchar(255) not null UNIQUE, " + CommodityEntry.COLUMN_NAMESPACE + " varchar(255) not null default " + Commodity.Namespace.ISO4217.name() + ", " @@ -862,8 +868,10 @@ static int upgradeDbToVersion9(SQLiteDatabase db){ + CommodityEntry.COLUMN_QUOTE_FLAG + " integer not null, " + CommodityEntry.COLUMN_CREATED_AT + " TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, " + CommodityEntry.COLUMN_MODIFIED_AT + " TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP " - + ");" + DatabaseHelper.createUpdatedAtTrigger(CommodityEntry.TABLE_NAME); - db.execSQL(createCommoditiesSql); + + ");" + DatabaseHelper.createUpdatedAtTrigger(CommodityEntry.TABLE_NAME)); + db.execSQL("CREATE UNIQUE INDEX '" + CommodityEntry.INDEX_UID + + "' ON " + CommodityEntry.TABLE_NAME + "(" + CommodityEntry.COLUMN_UID + ")"); + try { importCommodities(db); } catch (SAXException | ParserConfigurationException | IOException e) { @@ -892,7 +900,7 @@ static int upgradeDbToVersion9(SQLiteDatabase db){ + " WHERE " + TransactionEntry.TABLE_NAME + "." + TransactionEntry.COLUMN_COMMODITY_UID + " = " + CommodityEntry.TABLE_NAME + "." + CommodityEntry.COLUMN_UID + ")"); - String createPricesSql = "CREATE TABLE " + PriceEntry.TABLE_NAME + " (" + db.execSQL("CREATE TABLE " + PriceEntry.TABLE_NAME + " (" + PriceEntry._ID + " integer primary key autoincrement, " + PriceEntry.COLUMN_UID + " varchar(255) not null UNIQUE, " + PriceEntry.COLUMN_COMMODITY_UID + " varchar(255) not null, " @@ -907,12 +915,13 @@ static int upgradeDbToVersion9(SQLiteDatabase db){ + "UNIQUE (" + PriceEntry.COLUMN_COMMODITY_UID + ", " + PriceEntry.COLUMN_CURRENCY_UID + ") ON CONFLICT REPLACE, " + "FOREIGN KEY (" + PriceEntry.COLUMN_COMMODITY_UID + ") REFERENCES " + CommodityEntry.TABLE_NAME + " (" + CommodityEntry.COLUMN_UID + ") ON DELETE CASCADE, " + "FOREIGN KEY (" + PriceEntry.COLUMN_CURRENCY_UID + ") REFERENCES " + CommodityEntry.TABLE_NAME + " (" + CommodityEntry.COLUMN_UID + ") ON DELETE CASCADE " - + ");" + DatabaseHelper.createUpdatedAtTrigger(PriceEntry.TABLE_NAME); - db.execSQL(createPricesSql); + + ");" + DatabaseHelper.createUpdatedAtTrigger(PriceEntry.TABLE_NAME)); + db.execSQL("CREATE UNIQUE INDEX '" + PriceEntry.INDEX_UID + + "' ON " + PriceEntry.TABLE_NAME + "(" + PriceEntry.COLUMN_UID + ")"); //store split amounts as integer components numerator and denominator - + db.execSQL("ALTER TABLE " + SplitEntry.TABLE_NAME + " RENAME TO " + SplitEntry.TABLE_NAME + "_bak"); // create new split table db.execSQL("CREATE TABLE " + SplitEntry.TABLE_NAME + " (" @@ -1170,4 +1179,178 @@ static int upgradeDbToVersion11(SQLiteDatabase db){ } return oldVersion; } + + /** + * Upgrades the database to version 12. + *

This migration makes the following changes to the database: + *

    + *
  • Adds a table for budgets
  • + *
  • Adds an extra table for recurrences
  • + *
  • Migrate scheduled transaction recurrences to own table
  • + *
  • Adds flags for reconciled status to split table
  • + *
  • Add flags for auto-/advance- create and notification to scheduled actions
  • + *
+ *

+ * @param db SQlite database to be upgraded + * @return New database version, 12 if migration succeeds, 11 otherwise + */ + static int upgradeDbToVersion12(SQLiteDatabase db){ + Log.i(DatabaseHelper.LOG_TAG, "Upgrading database to version 9"); + int oldVersion = 11; + + db.beginTransaction(); + try { + db.execSQL("CREATE TABLE " + RecurrenceEntry.TABLE_NAME + " (" + + RecurrenceEntry._ID + " integer primary key autoincrement, " + + RecurrenceEntry.COLUMN_UID + " varchar(255) not null UNIQUE, " + + RecurrenceEntry.COLUMN_MULTIPLIER + " integer not null default 1, " + + RecurrenceEntry.COLUMN_PERIOD_TYPE + " varchar(255) not null, " + + RecurrenceEntry.COLUMN_BYDAY + " varchar(255), " + + RecurrenceEntry.COLUMN_PERIOD_START + " timestamp not null, " + + RecurrenceEntry.COLUMN_PERIOD_END + " timestamp, " + + RecurrenceEntry.COLUMN_CREATED_AT + " TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, " + + RecurrenceEntry.COLUMN_MODIFIED_AT + " TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP); " + + DatabaseHelper.createUpdatedAtTrigger(RecurrenceEntry.TABLE_NAME)); + + db.execSQL("CREATE TABLE " + BudgetEntry.TABLE_NAME + " (" + + BudgetEntry._ID + " integer primary key autoincrement, " + + BudgetEntry.COLUMN_UID + " varchar(255) not null UNIQUE, " + + BudgetEntry.COLUMN_NAME + " varchar(255) not null, " + + BudgetEntry.COLUMN_DESCRIPTION + " varchar(255), " + + BudgetEntry.COLUMN_RECURRENCE_UID + " varchar(255) not null, " + + BudgetEntry.COLUMN_NUM_PERIODS + " integer, " + + BudgetEntry.COLUMN_CREATED_AT + " TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, " + + BudgetEntry.COLUMN_MODIFIED_AT + " TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, " + + "FOREIGN KEY (" + BudgetEntry.COLUMN_RECURRENCE_UID + ") REFERENCES " + RecurrenceEntry.TABLE_NAME + " (" + RecurrenceEntry.COLUMN_UID + ") " + + ");" + DatabaseHelper.createUpdatedAtTrigger(BudgetEntry.TABLE_NAME)); + + db.execSQL("CREATE UNIQUE INDEX '" + BudgetEntry.INDEX_UID + + "' ON " + BudgetEntry.TABLE_NAME + "(" + BudgetEntry.COLUMN_UID + ")"); + + db.execSQL("CREATE TABLE " + BudgetAmountEntry.TABLE_NAME + " (" + + BudgetAmountEntry._ID + " integer primary key autoincrement, " + + BudgetAmountEntry.COLUMN_UID + " varchar(255) not null UNIQUE, " + + BudgetAmountEntry.COLUMN_BUDGET_UID + " varchar(255) not null, " + + BudgetAmountEntry.COLUMN_ACCOUNT_UID + " varchar(255) not null, " + + BudgetAmountEntry.COLUMN_AMOUNT_NUM + " integer not null, " + + BudgetAmountEntry.COLUMN_AMOUNT_DENOM + " integer not null, " + + BudgetAmountEntry.COLUMN_PERIOD_NUM + " integer not null, " + + BudgetAmountEntry.COLUMN_CREATED_AT + " TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, " + + BudgetAmountEntry.COLUMN_MODIFIED_AT + " TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, " + + "FOREIGN KEY (" + BudgetAmountEntry.COLUMN_ACCOUNT_UID + ") REFERENCES " + AccountEntry.TABLE_NAME + " (" + AccountEntry.COLUMN_UID + ") ON DELETE CASCADE, " + + "FOREIGN KEY (" + BudgetAmountEntry.COLUMN_BUDGET_UID + ") REFERENCES " + BudgetEntry.TABLE_NAME + " (" + BudgetEntry.COLUMN_UID + ") ON DELETE CASCADE " + + ");" + DatabaseHelper.createUpdatedAtTrigger(BudgetAmountEntry.TABLE_NAME)); + + db.execSQL("CREATE UNIQUE INDEX '" + BudgetAmountEntry.INDEX_UID + + "' ON " + BudgetAmountEntry.TABLE_NAME + "(" + BudgetAmountEntry.COLUMN_UID + ")"); + + + //extract recurrences from scheduled actions table and put in the recurrence table + db.execSQL("ALTER TABLE " + ScheduledActionEntry.TABLE_NAME + " RENAME TO " + ScheduledActionEntry.TABLE_NAME + "_bak"); + + db.execSQL("CREATE TABLE " + ScheduledActionEntry.TABLE_NAME + " (" + + ScheduledActionEntry._ID + " integer primary key autoincrement, " + + ScheduledActionEntry.COLUMN_UID + " varchar(255) not null UNIQUE, " + + ScheduledActionEntry.COLUMN_ACTION_UID + " varchar(255) not null, " + + ScheduledActionEntry.COLUMN_TYPE + " varchar(255) not null, " + + ScheduledActionEntry.COLUMN_RECURRENCE_UID + " varchar(255) not null, " + + ScheduledActionEntry.COLUMN_TEMPLATE_ACCT_UID + " varchar(255) not null, " + + ScheduledActionEntry.COLUMN_LAST_RUN + " integer default 0, " + + ScheduledActionEntry.COLUMN_START_TIME + " integer not null, " + + ScheduledActionEntry.COLUMN_END_TIME + " integer default 0, " + + ScheduledActionEntry.COLUMN_TAG + " text, " + + ScheduledActionEntry.COLUMN_ENABLED + " tinyint default 1, " //enabled by default + + ScheduledActionEntry.COLUMN_AUTO_CREATE + " tinyint default 1, " + + ScheduledActionEntry.COLUMN_AUTO_NOTIFY + " tinyint default 0, " + + ScheduledActionEntry.COLUMN_ADVANCE_CREATION + " integer default 0, " + + ScheduledActionEntry.COLUMN_ADVANCE_NOTIFY + " integer default 0, " + + ScheduledActionEntry.COLUMN_TOTAL_FREQUENCY + " integer default 0, " + + ScheduledActionEntry.COLUMN_EXECUTION_COUNT + " integer default 0, " + + ScheduledActionEntry.COLUMN_CREATED_AT + " TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, " + + ScheduledActionEntry.COLUMN_MODIFIED_AT + " TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, " + + "FOREIGN KEY (" + ScheduledActionEntry.COLUMN_RECURRENCE_UID + ") REFERENCES " + RecurrenceEntry.TABLE_NAME + " (" + RecurrenceEntry.COLUMN_UID + ") " + + ");" + DatabaseHelper.createUpdatedAtTrigger(ScheduledActionEntry.TABLE_NAME)); + + + // initialize new transaction table with data from old table + db.execSQL("INSERT INTO " + ScheduledActionEntry.TABLE_NAME + " ( " + + ScheduledActionEntry._ID + " , " + + ScheduledActionEntry.COLUMN_UID + " , " + + ScheduledActionEntry.COLUMN_ACTION_UID + " , " + + ScheduledActionEntry.COLUMN_TYPE + " , " + + ScheduledActionEntry.COLUMN_LAST_RUN + " , " + + ScheduledActionEntry.COLUMN_START_TIME + " , " + + ScheduledActionEntry.COLUMN_END_TIME + " , " + + ScheduledActionEntry.COLUMN_ENABLED + " , " + + ScheduledActionEntry.COLUMN_TOTAL_FREQUENCY + " , " + + ScheduledActionEntry.COLUMN_EXECUTION_COUNT + " , " + + ScheduledActionEntry.COLUMN_CREATED_AT + " , " + + ScheduledActionEntry.COLUMN_MODIFIED_AT + " , " + + ScheduledActionEntry.COLUMN_RECURRENCE_UID + " , " + + ScheduledActionEntry.COLUMN_TEMPLATE_ACCT_UID + " , " + + ScheduledActionEntry.COLUMN_TAG + + ") SELECT " + + ScheduledActionEntry.TABLE_NAME + "_bak." + ScheduledActionEntry._ID + " , " + + ScheduledActionEntry.TABLE_NAME + "_bak." + ScheduledActionEntry.COLUMN_UID + " , " + + ScheduledActionEntry.TABLE_NAME + "_bak." + ScheduledActionEntry.COLUMN_ACTION_UID + " , " + + ScheduledActionEntry.TABLE_NAME + "_bak." + ScheduledActionEntry.COLUMN_TYPE + " , " + + ScheduledActionEntry.TABLE_NAME + "_bak." + ScheduledActionEntry.COLUMN_LAST_RUN + " , " + + ScheduledActionEntry.TABLE_NAME + "_bak." + ScheduledActionEntry.COLUMN_START_TIME + " , " + + ScheduledActionEntry.TABLE_NAME + "_bak." + ScheduledActionEntry.COLUMN_END_TIME + " , " + + ScheduledActionEntry.TABLE_NAME + "_bak." + ScheduledActionEntry.COLUMN_ENABLED + " , " + + ScheduledActionEntry.TABLE_NAME + "_bak." + ScheduledActionEntry.COLUMN_TOTAL_FREQUENCY + " , " + + ScheduledActionEntry.TABLE_NAME + "_bak." + ScheduledActionEntry.COLUMN_EXECUTION_COUNT + " , " + + ScheduledActionEntry.TABLE_NAME + "_bak." + ScheduledActionEntry.COLUMN_CREATED_AT + " , " + + ScheduledActionEntry.TABLE_NAME + "_bak." + ScheduledActionEntry.COLUMN_MODIFIED_AT + " , " + + " 'dummy-string' ," //will be updated in next steps + + " 'dummy-string' ," + + ScheduledActionEntry.TABLE_NAME + "_bak." + ScheduledActionEntry.COLUMN_TAG + + " FROM " + ScheduledActionEntry.TABLE_NAME + "_bak;"); + + //update the template-account-guid and the recurrence guid for all scheduled actions + Cursor cursor = db.query(ScheduledActionEntry.TABLE_NAME + "_bak", + new String[]{ScheduledActionEntry.COLUMN_UID, + "period", + ScheduledActionEntry.COLUMN_START_TIME + }, + null, null, null, null, null); + + ContentValues contentValues = new ContentValues(); + while (cursor.moveToNext()){ + String uid = cursor.getString(cursor.getColumnIndexOrThrow(ScheduledActionEntry.COLUMN_UID)); + long period = cursor.getLong(cursor.getColumnIndexOrThrow("period")); + long startTime = cursor.getLong(cursor.getColumnIndexOrThrow(ScheduledActionEntry.COLUMN_START_TIME)); + PeriodType periodType = PeriodType.parse(period); + Recurrence recurrence = new Recurrence(periodType); + recurrence.setPeriodStart(new Timestamp(startTime)); + + contentValues.clear(); + contentValues.put(RecurrenceEntry.COLUMN_UID, recurrence.getUID()); + contentValues.put(RecurrenceEntry.COLUMN_MULTIPLIER, recurrence.getPeriodType().getMultiplier()); + contentValues.put(RecurrenceEntry.COLUMN_PERIOD_TYPE, recurrence.getPeriodType().name()); + contentValues.put(RecurrenceEntry.COLUMN_PERIOD_START, recurrence.getPeriodStart().toString()); + db.insert(RecurrenceEntry.TABLE_NAME, null, contentValues); + + contentValues.clear(); + contentValues.put(ScheduledActionEntry.COLUMN_RECURRENCE_UID, recurrence.getUID()); + contentValues.put(ScheduledActionEntry.COLUMN_TEMPLATE_ACCT_UID, BaseModel.generateUID()); + db.update(ScheduledActionEntry.TABLE_NAME, contentValues, + ScheduledActionEntry.COLUMN_UID + " = ?", new String[]{uid}); + } + cursor.close(); + + db.execSQL("DROP TABLE " + ScheduledActionEntry.TABLE_NAME + "_bak"); + + db.execSQL(" ALTER TABLE " + SplitEntry.TABLE_NAME + + " ADD COLUMN " + SplitEntry.COLUMN_RECONCILE_STATE + " varchar(1) not null default 'n' "); + db.execSQL(" ALTER TABLE " + SplitEntry.TABLE_NAME + + " ADD COLUMN " + SplitEntry.COLUMN_RECONCILE_DATE + " timestamp not null default CURRENT_TIMESTAMP "); + + db.setTransactionSuccessful(); + oldVersion = 12; + } finally { + db.endTransaction(); + } + return oldVersion; + } } diff --git a/app/src/main/java/org/gnucash/android/db/AccountsDbAdapter.java b/app/src/main/java/org/gnucash/android/db/adapter/AccountsDbAdapter.java similarity index 99% rename from app/src/main/java/org/gnucash/android/db/AccountsDbAdapter.java rename to app/src/main/java/org/gnucash/android/db/adapter/AccountsDbAdapter.java index 63b189a34..906b96737 100644 --- a/app/src/main/java/org/gnucash/android/db/AccountsDbAdapter.java +++ b/app/src/main/java/org/gnucash/android/db/adapter/AccountsDbAdapter.java @@ -15,7 +15,7 @@ * limitations under the License. */ -package org.gnucash.android.db; +package org.gnucash.android.db.adapter; import android.content.ContentValues; import android.content.Context; @@ -30,6 +30,7 @@ import org.gnucash.android.R; import org.gnucash.android.app.GnuCashApplication; +import org.gnucash.android.db.DatabaseSchema; import org.gnucash.android.model.Account; import org.gnucash.android.model.AccountType; import org.gnucash.android.model.Commodity; @@ -932,6 +933,7 @@ public String getOrCreateGnuCashRootAccountUID() { rootAccount.setAccountType(AccountType.ROOT); rootAccount.setFullName(ROOT_ACCOUNT_FULL_NAME); rootAccount.setHidden(true); + rootAccount.setPlaceHolderFlag(true); ContentValues contentValues = new ContentValues(); contentValues.put(AccountEntry.COLUMN_UID, rootAccount.getUID()); contentValues.put(AccountEntry.COLUMN_NAME, rootAccount.getName()); @@ -1102,7 +1104,7 @@ public List getAllOpeningBalanceTransactions(){ transaction.setCurrencyCode(currencyCode); TransactionType transactionType = Transaction.getTypeForBalance(getAccountType(accountUID), balance.isNegative()); - Split split = new Split(balance.absolute(), accountUID); + Split split = new Split(balance.abs(), accountUID); split.setType(transactionType); transaction.addSplit(split); transaction.addSplit(split.createPair(getOrCreateOpeningBalanceAccountUID())); diff --git a/app/src/main/java/org/gnucash/android/db/adapter/BudgetAmountsDbAdapter.java b/app/src/main/java/org/gnucash/android/db/adapter/BudgetAmountsDbAdapter.java new file mode 100644 index 000000000..60ff4f306 --- /dev/null +++ b/app/src/main/java/org/gnucash/android/db/adapter/BudgetAmountsDbAdapter.java @@ -0,0 +1,145 @@ +/* + * Copyright (c) 2015 Ngewi Fet + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.gnucash.android.db.adapter; + +import android.database.Cursor; +import android.database.sqlite.SQLiteDatabase; +import android.database.sqlite.SQLiteStatement; +import android.support.annotation.NonNull; + +import org.gnucash.android.app.GnuCashApplication; +import org.gnucash.android.model.BudgetAmount; +import org.gnucash.android.model.Money; + +import java.util.ArrayList; +import java.util.List; + +import static org.gnucash.android.db.DatabaseSchema.BudgetAmountEntry; + +/** + * Database adapter for {@link BudgetAmount}s + */ +public class BudgetAmountsDbAdapter extends DatabaseAdapter { + + + /** + * Opens the database adapter with an existing database + * + * @param db SQLiteDatabase object + */ + public BudgetAmountsDbAdapter(SQLiteDatabase db) { + super(db, BudgetAmountEntry.TABLE_NAME); + } + + public static BudgetAmountsDbAdapter getInstance(){ + return GnuCashApplication.getBudgetAmountsDbAdapter(); + } + + @Override + public BudgetAmount buildModelInstance(@NonNull Cursor cursor) { + String budgetUID = cursor.getString(cursor.getColumnIndexOrThrow(BudgetAmountEntry.COLUMN_BUDGET_UID)); + String accountUID = cursor.getString(cursor.getColumnIndexOrThrow(BudgetAmountEntry.COLUMN_ACCOUNT_UID)); + long amountNum = cursor.getLong(cursor.getColumnIndexOrThrow(BudgetAmountEntry.COLUMN_AMOUNT_NUM)); + long amountDenom = cursor.getLong(cursor.getColumnIndexOrThrow(BudgetAmountEntry.COLUMN_AMOUNT_DENOM)); + long periodNum = cursor.getLong(cursor.getColumnIndexOrThrow(BudgetAmountEntry.COLUMN_PERIOD_NUM)); + + BudgetAmount budgetAmount = new BudgetAmount(budgetUID, accountUID); + budgetAmount.setAmount(new Money(amountNum, amountDenom, getAccountCurrencyCode(accountUID))); + budgetAmount.setPeriodNum(periodNum); + populateBaseModelAttributes(cursor, budgetAmount); + + return budgetAmount; + } + + @Override + protected SQLiteStatement compileReplaceStatement(@NonNull BudgetAmount budgetAmount) { + if (mReplaceStatement == null){ + mReplaceStatement = mDb.compileStatement("REPLACE INTO " + BudgetAmountEntry.TABLE_NAME + " ( " + + BudgetAmountEntry.COLUMN_UID + " , " + + BudgetAmountEntry.COLUMN_BUDGET_UID + " , " + + BudgetAmountEntry.COLUMN_ACCOUNT_UID + " , " + + BudgetAmountEntry.COLUMN_AMOUNT_NUM + " , " + + BudgetAmountEntry.COLUMN_AMOUNT_DENOM + " , " + + BudgetAmountEntry.COLUMN_PERIOD_NUM + " ) VALUES ( ? , ? , ? , ? , ? , ? ) "); + } + + mReplaceStatement.clearBindings(); + mReplaceStatement.bindString(1, budgetAmount.getUID()); + mReplaceStatement.bindString(2, budgetAmount.getBudgetUID()); + mReplaceStatement.bindString(3, budgetAmount.getAccountUID()); + mReplaceStatement.bindLong(4, budgetAmount.getAmount().getNumerator()); + mReplaceStatement.bindLong(5, budgetAmount.getAmount().getDenominator()); + mReplaceStatement.bindLong(6, budgetAmount.getPeriodNum()); + + return mReplaceStatement; + } + + /** + * Return budget amounts for the specific budget + * @param budgetUID GUID of the budget + * @return List of budget amounts + */ + public List getBudgetAmountsForBudget(String budgetUID){ + Cursor cursor = fetchAllRecords(BudgetAmountEntry.COLUMN_BUDGET_UID + "=?", + new String[]{budgetUID}, null); + + List budgetAmounts = new ArrayList<>(); + while (cursor.moveToNext()){ + budgetAmounts.add(buildModelInstance(cursor)); + } + cursor.close(); + return budgetAmounts; + } + + /** + * Delete all the budget amounts for a budget + * @param budgetUID GUID of the budget + * @return Number of records deleted + */ + public int deleteBudgetAmountsForBudget(String budgetUID){ + return mDb.delete(mTableName, BudgetAmountEntry.COLUMN_BUDGET_UID + "=?", + new String[]{budgetUID}); + } + + /** + * Returns the budgets associated with a specific account + * @param accountUID GUID of the account + * @return List of {@link BudgetAmount}s for the account + */ + public List getBudgetAmounts(String accountUID) { + Cursor cursor = fetchAllRecords(BudgetAmountEntry.COLUMN_ACCOUNT_UID + " = ?", new String[]{accountUID}, null); + List budgetAmounts = new ArrayList<>(); + while(cursor.moveToNext()){ + budgetAmounts.add(buildModelInstance(cursor)); + } + cursor.close(); + return budgetAmounts; + } + + /** + * Returns the sum of the budget amounts for a particular account + * @param accountUID GUID of the account + * @return Sum of the budget amounts + */ + public Money getBudgetAmountSum(String accountUID){ + List budgetAmounts = getBudgetAmounts(accountUID); + Money sum = Money.createZeroInstance(getAccountCurrencyCode(accountUID)); + for (BudgetAmount budgetAmount : budgetAmounts) { + sum = sum.add(budgetAmount.getAmount()); + } + return sum; + } +} diff --git a/app/src/main/java/org/gnucash/android/db/adapter/BudgetsDbAdapter.java b/app/src/main/java/org/gnucash/android/db/adapter/BudgetsDbAdapter.java new file mode 100644 index 000000000..9f03d0f27 --- /dev/null +++ b/app/src/main/java/org/gnucash/android/db/adapter/BudgetsDbAdapter.java @@ -0,0 +1,194 @@ +/* + * Copyright (c) 2015 Ngewi Fet + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.gnucash.android.db.adapter; + +import android.database.Cursor; +import android.database.sqlite.SQLiteDatabase; +import android.database.sqlite.SQLiteQueryBuilder; +import android.database.sqlite.SQLiteStatement; +import android.support.annotation.NonNull; + +import org.gnucash.android.app.GnuCashApplication; +import org.gnucash.android.db.DatabaseSchema.BudgetAmountEntry; +import org.gnucash.android.db.DatabaseSchema.BudgetEntry; +import org.gnucash.android.model.Budget; +import org.gnucash.android.model.BudgetAmount; +import org.gnucash.android.model.Money; +import org.gnucash.android.model.Recurrence; + +import java.util.ArrayList; +import java.util.List; + + +/** + * Database adapter for accessing {@link org.gnucash.android.model.Budget} records + */ +public class BudgetsDbAdapter extends DatabaseAdapter{ + + private RecurrenceDbAdapter mRecurrenceDbAdapter; + private BudgetAmountsDbAdapter mBudgetAmountsDbAdapter; + + /** + * Opens the database adapter with an existing database + * + * @param db SQLiteDatabase object + */ + public BudgetsDbAdapter(SQLiteDatabase db) { + super(db, BudgetEntry.TABLE_NAME); + mRecurrenceDbAdapter = new RecurrenceDbAdapter(db); + mBudgetAmountsDbAdapter = new BudgetAmountsDbAdapter(db); + } + + /** + * Returns an instance of the budget database adapter + * @return BudgetsDbAdapter instance + */ + public static BudgetsDbAdapter getInstance(){ + return GnuCashApplication.getBudgetDbAdapter(); + } + + @Override + public void addRecord(@NonNull Budget budget) { + if (budget.getBudgetAmounts().size() == 0) + throw new IllegalArgumentException("Budgets must have budget amounts"); + + mRecurrenceDbAdapter.addRecord(budget.getRecurrence()); + super.addRecord(budget); + mBudgetAmountsDbAdapter.deleteBudgetAmountsForBudget(budget.getUID()); + for (BudgetAmount budgetAmount : budget.getBudgetAmounts()) { + mBudgetAmountsDbAdapter.addRecord(budgetAmount); + } + } + + @Override + public long bulkAddRecords(@NonNull List budgetList) { + List budgetAmountList = new ArrayList<>(budgetList.size()*2); + for (Budget budget : budgetList) { + budgetAmountList.addAll(budget.getBudgetAmounts()); + } + + //first add the recurrences, they have no dependencies (foreign key constraints) + List recurrenceList = new ArrayList<>(budgetList.size()); + for (Budget budget : budgetList) { + recurrenceList.add(budget.getRecurrence()); + } + mRecurrenceDbAdapter.bulkAddRecords(recurrenceList); + + //now add the budgets themselves + long nRow = super.bulkAddRecords(budgetList); + + //then add the budget amounts, they require the budgets to exist + if (nRow > 0 && !budgetAmountList.isEmpty()){ + mBudgetAmountsDbAdapter.bulkAddRecords(budgetAmountList); + } + + return nRow; + } + + @Override + public Budget buildModelInstance(@NonNull Cursor cursor) { + String name = cursor.getString(cursor.getColumnIndexOrThrow(BudgetEntry.COLUMN_NAME)); + String description = cursor.getString(cursor.getColumnIndexOrThrow(BudgetEntry.COLUMN_DESCRIPTION)); + String recurrenceUID = cursor.getString(cursor.getColumnIndexOrThrow(BudgetEntry.COLUMN_RECURRENCE_UID)); + long numPeriods = cursor.getLong(cursor.getColumnIndexOrThrow(BudgetEntry.COLUMN_NUM_PERIODS)); + + + Budget budget = new Budget(name); + budget.setDescription(description); + budget.setRecurrence(mRecurrenceDbAdapter.getRecord(recurrenceUID)); + budget.setNumberOfPeriods(numPeriods); + populateBaseModelAttributes(cursor, budget); + budget.setBudgetAmounts(mBudgetAmountsDbAdapter.getBudgetAmountsForBudget(budget.getUID())); + + return budget; + } + + @Override + protected SQLiteStatement compileReplaceStatement(@NonNull Budget budget) { + if (mReplaceStatement == null){ + mReplaceStatement = mDb.compileStatement("REPLACE INTO " + BudgetEntry.TABLE_NAME + " ( " + + BudgetEntry.COLUMN_UID + " , " + + BudgetEntry.COLUMN_NAME + " , " + + BudgetEntry.COLUMN_DESCRIPTION + " , " + + BudgetEntry.COLUMN_RECURRENCE_UID + " , " + + BudgetEntry.COLUMN_NUM_PERIODS + " ) VALUES (? , ? , ? , ? , ? ) "); + } + + mReplaceStatement.clearBindings(); + mReplaceStatement.bindString(1, budget.getUID()); + mReplaceStatement.bindString(2, budget.getName()); + if (budget.getDescription() != null) + mReplaceStatement.bindString(3, budget.getDescription()); + mReplaceStatement.bindString(4, budget.getRecurrence().getUID()); + mReplaceStatement.bindLong(5, budget.getNumberOfPeriods()); + + return mReplaceStatement; + } + + /** + * Fetch all budgets which have an amount specified for the account + * @param accountUID GUID of account + * @return Cursor with budgets data + */ + public Cursor fetchBudgetsForAccount(String accountUID){ + SQLiteQueryBuilder queryBuilder = new SQLiteQueryBuilder(); + queryBuilder.setTables(BudgetEntry.TABLE_NAME + "," + BudgetAmountEntry.TABLE_NAME + + " ON " + BudgetEntry.TABLE_NAME + "." + BudgetEntry.COLUMN_UID + " = " + + BudgetAmountEntry.TABLE_NAME + "." + BudgetAmountEntry.COLUMN_BUDGET_UID); + + queryBuilder.setDistinct(true); + String[] projectionIn = new String[]{BudgetEntry.TABLE_NAME + ".*"}; + String selection = BudgetAmountEntry.TABLE_NAME + "." + BudgetAmountEntry.COLUMN_ACCOUNT_UID + " = ?"; + String[] selectionArgs = new String[]{accountUID}; + String sortOrder = BudgetEntry.TABLE_NAME + "." + BudgetEntry.COLUMN_NAME + " ASC"; + + return queryBuilder.query(mDb, projectionIn, selection, selectionArgs, null, null, sortOrder); + } + + /** + * Returns the budgets associated with a specific account + * @param accountUID GUID of the account + * @return List of budgets for the account + */ + public List getAccountBudgets(String accountUID) { + Cursor cursor = fetchBudgetsForAccount(accountUID); + List budgets = new ArrayList<>(); + while(cursor.moveToNext()){ + budgets.add(buildModelInstance(cursor)); + } + cursor.close(); + return budgets; + } + + /** + * Returns the sum of the account balances for all accounts in a budget for a specified time period + *

This represents the total amount spent within the account of this budget in a given period

+ * @param budgetUID GUID of budget + * @param periodStart Start of the budgeting period in millis + * @param periodEnd End of the budgeting period in millis + * @return Balance of all the accounts + */ + public Money getAccountSum(String budgetUID, long periodStart, long periodEnd){ + List budgetAmounts = mBudgetAmountsDbAdapter.getBudgetAmountsForBudget(budgetUID); + List accountUIDs = new ArrayList<>(); + for (BudgetAmount budgetAmount : budgetAmounts) { + accountUIDs.add(budgetAmount.getAccountUID()); + } + + return AccountsDbAdapter.getInstance().getAccountsBalance(accountUIDs, periodStart, periodEnd); + } +} diff --git a/app/src/main/java/org/gnucash/android/db/CommoditiesDbAdapter.java b/app/src/main/java/org/gnucash/android/db/adapter/CommoditiesDbAdapter.java similarity index 97% rename from app/src/main/java/org/gnucash/android/db/CommoditiesDbAdapter.java rename to app/src/main/java/org/gnucash/android/db/adapter/CommoditiesDbAdapter.java index b4e6d7583..ade7fab76 100644 --- a/app/src/main/java/org/gnucash/android/db/CommoditiesDbAdapter.java +++ b/app/src/main/java/org/gnucash/android/db/adapter/CommoditiesDbAdapter.java @@ -1,4 +1,4 @@ -package org.gnucash.android.db; +package org.gnucash.android.db.adapter; import android.database.Cursor; import android.database.sqlite.SQLiteDatabase; @@ -6,6 +6,7 @@ import android.support.annotation.NonNull; import org.gnucash.android.app.GnuCashApplication; +import org.gnucash.android.db.DatabaseSchema; import org.gnucash.android.model.Commodity; import static org.gnucash.android.db.DatabaseSchema.CommodityEntry; @@ -107,7 +108,7 @@ public Cursor fetchAllRecords(String orderBy) { * @return Commodity associated with code or null if none is found */ public Commodity getCommodity(String currencyCode){ - Cursor cursor = fetchAllRecords(CommodityEntry.COLUMN_MNEMONIC + "=?", new String[]{currencyCode}); + Cursor cursor = fetchAllRecords(CommodityEntry.COLUMN_MNEMONIC + "=?", new String[]{currencyCode}, null); Commodity commodity = null; if (cursor.moveToNext()){ commodity = buildModelInstance(cursor); diff --git a/app/src/main/java/org/gnucash/android/db/DatabaseAdapter.java b/app/src/main/java/org/gnucash/android/db/adapter/DatabaseAdapter.java similarity index 96% rename from app/src/main/java/org/gnucash/android/db/DatabaseAdapter.java rename to app/src/main/java/org/gnucash/android/db/adapter/DatabaseAdapter.java index 5281404ff..8881114fd 100644 --- a/app/src/main/java/org/gnucash/android/db/DatabaseAdapter.java +++ b/app/src/main/java/org/gnucash/android/db/adapter/DatabaseAdapter.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.gnucash.android.db; +package org.gnucash.android.db.adapter; import android.content.ContentValues; import android.database.Cursor; @@ -23,6 +23,7 @@ import android.support.annotation.NonNull; import android.util.Log; +import org.gnucash.android.db.DatabaseSchema; import org.gnucash.android.db.DatabaseSchema.AccountEntry; import org.gnucash.android.db.DatabaseSchema.CommonColumns; import org.gnucash.android.db.DatabaseSchema.SplitEntry; @@ -69,6 +70,7 @@ public DatabaseAdapter(SQLiteDatabase db, @NonNull String tableName) { if (mDb.getVersion() >= 9) { createTempView(); } + LOG_TAG = getClass().getSimpleName(); } private void createTempView() { @@ -81,6 +83,8 @@ private void createTempView() { // // create a temporary view, combining accounts, transactions and splits, as this is often used // in the queries + + //todo: would it be useful to add the split reconciled_state and reconciled_date to this view? mDb.execSQL("CREATE TEMP VIEW IF NOT EXISTS trans_split_acct AS SELECT " + TransactionEntry.TABLE_NAME + "." + CommonColumns.COLUMN_MODIFIED_AT + " AS " + TransactionEntry.TABLE_NAME + "_" + CommonColumns.COLUMN_MODIFIED_AT + " , " @@ -227,11 +231,11 @@ public long bulkAddRecords(@NonNull List modelList) { /** * Builds an instance of the model from the database record entry - *

This method should not modify the cursor in any way

+ *

When implementing this method, remember to call {@link #populateBaseModelAttributes(Cursor, BaseModel)}

* @param cursor Cursor pointing to the record - * @return + * @return New instance of the model from database record */ - protected abstract Model buildModelInstance(@NonNull final Cursor cursor); + public abstract Model buildModelInstance(@NonNull final Cursor cursor); /** * Generates an {@link SQLiteStatement} with values from the {@code model}. @@ -259,7 +263,7 @@ public Model getRecord(@NonNull String uid){ return buildModelInstance(cursor); } else { - throw new IllegalArgumentException("Record with " + uid + " does not exist"); + throw new IllegalArgumentException(LOG_TAG + ": Record with " + uid + " does not exist"); } } finally { cursor.close(); @@ -294,12 +298,12 @@ public List getAllRecords(){ } /** - * Adds the attributes of the base model to the ContentValues object provided + * Extracts the attributes of the base model and adds them to the ContentValues object provided * @param contentValues Content values to which to add attributes * @param model {@link org.gnucash.android.model.BaseModel} from which to extract values * @return {@link android.content.ContentValues} with the data to be inserted into the db */ - protected ContentValues populateBaseModelAttributes(@NonNull ContentValues contentValues, @NonNull Model model){ + protected ContentValues extractBaseModelAttributes(@NonNull ContentValues contentValues, @NonNull Model model){ contentValues.put(CommonColumns.COLUMN_UID, model.getUID()); contentValues.put(CommonColumns.COLUMN_CREATED_AT, model.getCreatedTimestamp().toString()); //there is a trigger in the database for updated the modified_at column @@ -350,17 +354,18 @@ public Cursor fetchRecord(@NonNull String uid){ * @return {@link Cursor} to all records in table tableName */ public Cursor fetchAllRecords(){ - return fetchAllRecords(null, null); + return fetchAllRecords(null, null, null); } /** * Fetch all records from database matching conditions * @param where SQL where clause * @param whereArgs String arguments for where clause + * @param orderBy SQL orderby clause * @return Cursor to records matching conditions */ - public Cursor fetchAllRecords(String where, String[] whereArgs){ - return mDb.query(mTableName, null, where, whereArgs, null, null, null); + public Cursor fetchAllRecords(String where, String[] whereArgs, String orderBy){ + return mDb.query(mTableName, null, where, whereArgs, null, null, orderBy); } /** @@ -423,7 +428,7 @@ public String getUID(long id){ if (cursor.moveToFirst()) { uid = cursor.getString(cursor.getColumnIndexOrThrow(DatabaseSchema.CommonColumns.COLUMN_UID)); } else { - throw new IllegalArgumentException("Account record ID " + id + " does not exist in the db"); + throw new IllegalArgumentException("Record with ID " + id + " does not exist in the db"); } } finally { cursor.close(); diff --git a/app/src/main/java/org/gnucash/android/db/PricesDbAdapter.java b/app/src/main/java/org/gnucash/android/db/adapter/PricesDbAdapter.java similarity index 98% rename from app/src/main/java/org/gnucash/android/db/PricesDbAdapter.java rename to app/src/main/java/org/gnucash/android/db/adapter/PricesDbAdapter.java index 83645f4f5..86d92886a 100644 --- a/app/src/main/java/org/gnucash/android/db/PricesDbAdapter.java +++ b/app/src/main/java/org/gnucash/android/db/adapter/PricesDbAdapter.java @@ -1,11 +1,9 @@ -package org.gnucash.android.db; +package org.gnucash.android.db.adapter; -import android.content.ContentValues; import android.database.Cursor; import android.database.sqlite.SQLiteDatabase; import android.database.sqlite.SQLiteStatement; import android.support.annotation.NonNull; -import android.util.Log; import android.util.Pair; import org.gnucash.android.app.GnuCashApplication; diff --git a/app/src/main/java/org/gnucash/android/db/adapter/RecurrenceDbAdapter.java b/app/src/main/java/org/gnucash/android/db/adapter/RecurrenceDbAdapter.java new file mode 100644 index 000000000..8235be4ca --- /dev/null +++ b/app/src/main/java/org/gnucash/android/db/adapter/RecurrenceDbAdapter.java @@ -0,0 +1,97 @@ +/* + * Copyright (c) 2015 Ngewi Fet + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.gnucash.android.db.adapter; + +import android.database.Cursor; +import android.database.sqlite.SQLiteDatabase; +import android.database.sqlite.SQLiteStatement; +import android.support.annotation.NonNull; + +import org.gnucash.android.app.GnuCashApplication; +import org.gnucash.android.model.PeriodType; +import org.gnucash.android.model.Recurrence; + +import java.sql.Timestamp; + +import static org.gnucash.android.db.DatabaseSchema.RecurrenceEntry; + +/** + * Database adapter for {@link Recurrence} entries + */ +public class RecurrenceDbAdapter extends DatabaseAdapter { + /** + * Opens the database adapter with an existing database + * + * @param db SQLiteDatabase object + */ + public RecurrenceDbAdapter(SQLiteDatabase db) { + super(db, RecurrenceEntry.TABLE_NAME); + } + + public static RecurrenceDbAdapter getInstance(){ + return GnuCashApplication.getRecurrenceDbAdapter(); + } + + @Override + public Recurrence buildModelInstance(@NonNull Cursor cursor) { + String type = cursor.getString(cursor.getColumnIndexOrThrow(RecurrenceEntry.COLUMN_PERIOD_TYPE)); + long multiplier = cursor.getLong(cursor.getColumnIndexOrThrow(RecurrenceEntry.COLUMN_MULTIPLIER)); + String periodStart = cursor.getString(cursor.getColumnIndexOrThrow(RecurrenceEntry.COLUMN_PERIOD_START)); + String periodEnd = cursor.getString(cursor.getColumnIndexOrThrow(RecurrenceEntry.COLUMN_PERIOD_END)); + String byDay = cursor.getString(cursor.getColumnIndexOrThrow(RecurrenceEntry.COLUMN_BYDAY)); + + PeriodType periodType = PeriodType.valueOf(type); + periodType.setMultiplier((int) multiplier); + + Recurrence recurrence = new Recurrence(periodType); + recurrence.setPeriodStart(Timestamp.valueOf(periodStart)); + if (periodEnd != null) + recurrence.setPeriodEnd(Timestamp.valueOf(periodEnd)); + recurrence.setByDay(byDay); + + populateBaseModelAttributes(cursor, recurrence); + + return recurrence; + } + + @Override + protected SQLiteStatement compileReplaceStatement(@NonNull Recurrence recurrence) { + if (mReplaceStatement == null) { + mReplaceStatement = mDb.compileStatement("REPLACE INTO " + RecurrenceEntry.TABLE_NAME + " ( " + + RecurrenceEntry.COLUMN_UID + " , " + + RecurrenceEntry.COLUMN_MULTIPLIER + " , " + + RecurrenceEntry.COLUMN_PERIOD_TYPE + " , " + + RecurrenceEntry.COLUMN_BYDAY + " , " + + RecurrenceEntry.COLUMN_PERIOD_START + " , " + + RecurrenceEntry.COLUMN_PERIOD_END + " ) VALUES ( ? , ? , ? , ? , ? , ? ) "); + } + + mReplaceStatement.clearBindings(); + mReplaceStatement.bindString(1, recurrence.getUID()); + mReplaceStatement.bindLong(2, recurrence.getPeriodType().getMultiplier()); + mReplaceStatement.bindString(3, recurrence.getPeriodType().name()); + if (recurrence.getByDay() != null) + mReplaceStatement.bindString(4, recurrence.getByDay()); + //recurrence should always have a start date + mReplaceStatement.bindString(5, recurrence.getPeriodStart().toString()); + + if (recurrence.getPeriodEnd() != null) + mReplaceStatement.bindString(6, recurrence.getPeriodEnd().toString()); + + return mReplaceStatement; + } +} diff --git a/app/src/main/java/org/gnucash/android/db/ScheduledActionDbAdapter.java b/app/src/main/java/org/gnucash/android/db/adapter/ScheduledActionDbAdapter.java similarity index 66% rename from app/src/main/java/org/gnucash/android/db/ScheduledActionDbAdapter.java rename to app/src/main/java/org/gnucash/android/db/adapter/ScheduledActionDbAdapter.java index 8a98500d2..ade23f808 100644 --- a/app/src/main/java/org/gnucash/android/db/ScheduledActionDbAdapter.java +++ b/app/src/main/java/org/gnucash/android/db/adapter/ScheduledActionDbAdapter.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.gnucash.android.db; +package org.gnucash.android.db.adapter; import android.content.ContentValues; import android.database.Cursor; @@ -23,6 +23,8 @@ import android.util.Log; import org.gnucash.android.app.GnuCashApplication; +import org.gnucash.android.db.DatabaseSchema; +import org.gnucash.android.model.Recurrence; import org.gnucash.android.model.ScheduledAction; import java.util.ArrayList; @@ -37,8 +39,11 @@ */ public class ScheduledActionDbAdapter extends DatabaseAdapter { + RecurrenceDbAdapter mRecurrenceDbAdapter; + public ScheduledActionDbAdapter(SQLiteDatabase db){ super(db, ScheduledActionEntry.TABLE_NAME); + mRecurrenceDbAdapter = new RecurrenceDbAdapter(db); LOG_TAG = "ScheduledActionDbAdapter"; } @@ -50,6 +55,26 @@ public static ScheduledActionDbAdapter getInstance(){ return GnuCashApplication.getScheduledEventDbAdapter(); } + @Override + public void addRecord(@NonNull ScheduledAction scheduledAction) { + mRecurrenceDbAdapter.addRecord(scheduledAction.getRecurrence()); + super.addRecord(scheduledAction); + } + + @Override + public long bulkAddRecords(@NonNull List scheduledActions) { + List recurrenceList = new ArrayList<>(scheduledActions.size()); + for (ScheduledAction scheduledAction : scheduledActions) { + recurrenceList.add(scheduledAction.getRecurrence()); + } + + //first add the recurrences, they have no dependencies (foreign key constraints) + long nRecurrences = mRecurrenceDbAdapter.bulkAddRecords(recurrenceList); + Log.d(LOG_TAG, String.format("Added %d recurrences for scheduled actions", nRecurrences)); + + return super.bulkAddRecords(scheduledActions); + } + /** * Updates only the recurrence attributes of the scheduled action. * The recurrence attributes are the period, start time, end time and/or total frequency. @@ -60,9 +85,17 @@ public static ScheduledActionDbAdapter getInstance(){ * @return Database record ID of the edited scheduled action */ public long updateRecurrenceAttributes(ScheduledAction scheduledAction){ + //since we are updating, first fetch the existing recurrence UID and set it to the object + //so that it will be updated and not a new one created + RecurrenceDbAdapter recurrenceDbAdapter = RecurrenceDbAdapter.getInstance(); + String recurrenceUID = recurrenceDbAdapter.getAttribute(scheduledAction.getUID(), ScheduledActionEntry.COLUMN_RECURRENCE_UID); + + Recurrence recurrence = scheduledAction.getRecurrence(); + recurrence.setUID(recurrenceUID); + recurrenceDbAdapter.addRecord(recurrence); + ContentValues contentValues = new ContentValues(); - populateBaseModelAttributes(contentValues, scheduledAction); - contentValues.put(ScheduledActionEntry.COLUMN_PERIOD, scheduledAction.getPeriod()); + extractBaseModelAttributes(contentValues, scheduledAction); contentValues.put(ScheduledActionEntry.COLUMN_START_TIME, scheduledAction.getStartTime()); contentValues.put(ScheduledActionEntry.COLUMN_END_TIME, scheduledAction.getEndTime()); contentValues.put(ScheduledActionEntry.COLUMN_TAG, scheduledAction.getTag()); @@ -85,12 +118,17 @@ protected SQLiteStatement compileReplaceStatement(@NonNull final ScheduledAction + ScheduledActionEntry.COLUMN_START_TIME + " , " + ScheduledActionEntry.COLUMN_END_TIME + " , " + ScheduledActionEntry.COLUMN_LAST_RUN + " , " - + ScheduledActionEntry.COLUMN_PERIOD + " , " + ScheduledActionEntry.COLUMN_ENABLED + " , " + ScheduledActionEntry.COLUMN_CREATED_AT + " , " + ScheduledActionEntry.COLUMN_TAG + " , " - + ScheduledActionEntry.COLUMN_TOTAL_FREQUENCY + " , " - + ScheduledActionEntry.COLUMN_EXECUTION_COUNT + " ) VALUES ( ? , ? , ? , ? , ? , ? , ? , ? , ? , ? , ? , ? )"); + + ScheduledActionEntry.COLUMN_TOTAL_FREQUENCY + " , " + + ScheduledActionEntry.COLUMN_RECURRENCE_UID + " , " + + ScheduledActionEntry.COLUMN_AUTO_CREATE + " , " + + ScheduledActionEntry.COLUMN_AUTO_NOTIFY + " , " + + ScheduledActionEntry.COLUMN_ADVANCE_CREATION + " , " + + ScheduledActionEntry.COLUMN_ADVANCE_NOTIFY + " , " + + ScheduledActionEntry.COLUMN_TEMPLATE_ACCT_UID + " , " + + ScheduledActionEntry.COLUMN_EXECUTION_COUNT + " ) VALUES ( ? , ? , ? , ? , ? , ? , ? , ? , ? , ? , ? , ? , ? , ? , ? , ? , ? )"); } mReplaceStatement.clearBindings(); @@ -98,17 +136,23 @@ protected SQLiteStatement compileReplaceStatement(@NonNull final ScheduledAction mReplaceStatement.bindString(2, schedxAction.getActionUID()); mReplaceStatement.bindString(3, schedxAction.getActionType().name()); mReplaceStatement.bindLong(4, schedxAction.getStartTime()); - mReplaceStatement.bindLong(5, schedxAction.getEndTime()); - mReplaceStatement.bindLong(6, schedxAction.getLastRun()); - mReplaceStatement.bindLong(7, schedxAction.getPeriod()); - mReplaceStatement.bindLong(8, schedxAction.isEnabled() ? 1 : 0); - mReplaceStatement.bindString(9, schedxAction.getCreatedTimestamp().toString()); + mReplaceStatement.bindLong(5, schedxAction.getEndTime()); + mReplaceStatement.bindLong(6, schedxAction.getLastRunTime()); + mReplaceStatement.bindLong(7, schedxAction.isEnabled() ? 1 : 0); + mReplaceStatement.bindString(8, schedxAction.getCreatedTimestamp().toString()); if (schedxAction.getTag() == null) - mReplaceStatement.bindNull(10); + mReplaceStatement.bindNull(9); else - mReplaceStatement.bindString(10, schedxAction.getTag()); - mReplaceStatement.bindString(11, Integer.toString(schedxAction.getTotalFrequency())); - mReplaceStatement.bindString(12, Integer.toString(schedxAction.getExecutionCount())); + mReplaceStatement.bindString(9, schedxAction.getTag()); + mReplaceStatement.bindString(10, Integer.toString(schedxAction.getTotalFrequency())); + mReplaceStatement.bindString(11, schedxAction.getRecurrence().getUID()); + mReplaceStatement.bindLong(12, schedxAction.shouldAutoCreate() ? 1 : 0); + mReplaceStatement.bindLong(13, schedxAction.shouldAutoNotify() ? 1 : 0); + mReplaceStatement.bindLong(14, schedxAction.getAdvanceCreateDays()); + mReplaceStatement.bindLong(15, schedxAction.getAdvanceNotifyDays()); + mReplaceStatement.bindString(16, schedxAction.getTemplateAccountUID()); + + mReplaceStatement.bindString(17, Integer.toString(schedxAction.getExecutionCount())); return mReplaceStatement; } @@ -122,7 +166,6 @@ protected SQLiteStatement compileReplaceStatement(@NonNull final ScheduledAction @Override public ScheduledAction buildModelInstance(@NonNull final Cursor cursor){ String actionUid = cursor.getString(cursor.getColumnIndexOrThrow(ScheduledActionEntry.COLUMN_ACTION_UID)); - long period = cursor.getLong(cursor.getColumnIndexOrThrow(ScheduledActionEntry.COLUMN_PERIOD)); long startTime = cursor.getLong(cursor.getColumnIndexOrThrow(ScheduledActionEntry.COLUMN_START_TIME)); long endTime = cursor.getLong(cursor.getColumnIndexOrThrow(ScheduledActionEntry.COLUMN_END_TIME)); long lastRun = cursor.getLong(cursor.getColumnIndexOrThrow(ScheduledActionEntry.COLUMN_LAST_RUN)); @@ -131,10 +174,15 @@ public ScheduledAction buildModelInstance(@NonNull final Cursor cursor){ boolean enabled = cursor.getInt(cursor.getColumnIndexOrThrow(ScheduledActionEntry.COLUMN_ENABLED)) > 0; int numOccurrences = cursor.getInt(cursor.getColumnIndexOrThrow(ScheduledActionEntry.COLUMN_TOTAL_FREQUENCY)); int execCount = cursor.getInt(cursor.getColumnIndexOrThrow(ScheduledActionEntry.COLUMN_EXECUTION_COUNT)); + int autoCreate = cursor.getInt(cursor.getColumnIndexOrThrow(ScheduledActionEntry.COLUMN_AUTO_CREATE)); + int autoNotify = cursor.getInt(cursor.getColumnIndexOrThrow(ScheduledActionEntry.COLUMN_AUTO_NOTIFY)); + int advanceCreate = cursor.getInt(cursor.getColumnIndexOrThrow(ScheduledActionEntry.COLUMN_ADVANCE_CREATION)); + int advanceNotify = cursor.getInt(cursor.getColumnIndexOrThrow(ScheduledActionEntry.COLUMN_ADVANCE_NOTIFY)); + String recurrenceUID = cursor.getString(cursor.getColumnIndexOrThrow(ScheduledActionEntry.COLUMN_RECURRENCE_UID)); + String templateActUID = cursor.getString(cursor.getColumnIndexOrThrow(ScheduledActionEntry.COLUMN_TEMPLATE_ACCT_UID)); ScheduledAction event = new ScheduledAction(ScheduledAction.ActionType.valueOf(typeString)); populateBaseModelAttributes(cursor, event); - event.setPeriod(period); event.setStartTime(startTime); event.setEndTime(endTime); event.setActionUID(actionUid); @@ -143,6 +191,13 @@ public ScheduledAction buildModelInstance(@NonNull final Cursor cursor){ event.setEnabled(enabled); event.setTotalFrequency(numOccurrences); event.setExecutionCount(execCount); + event.setAutoCreate(autoCreate == 1); + event.setAutoNotify(autoNotify == 1); + event.setAdvanceCreateDays(advanceCreate); + event.setAdvanceNotifyDays(advanceNotify); + //TODO: optimize by doing overriding fetchRecord(String) and join the two tables + event.setRecurrence(mRecurrenceDbAdapter.getRecord(recurrenceUID)); + event.setTemplateAccountUID(templateActUID); return event; } @@ -158,7 +213,7 @@ public List getScheduledActionsWithUID(@NonNull String actionUI ScheduledActionEntry.COLUMN_ACTION_UID + "= ?", new String[]{actionUID}, null, null, null); - List scheduledActions = new ArrayList(); + List scheduledActions = new ArrayList<>(); try { while (cursor.moveToNext()) { scheduledActions.add(buildModelInstance(cursor)); @@ -176,7 +231,7 @@ public List getScheduledActionsWithUID(@NonNull String actionUI public List getAllEnabledScheduledActions(){ Cursor cursor = mDb.query(mTableName, null, ScheduledActionEntry.COLUMN_ENABLED + "=1", null, null, null, null); - List scheduledActions = new ArrayList(); + List scheduledActions = new ArrayList<>(); while (cursor.moveToNext()){ scheduledActions.add(buildModelInstance(cursor)); } diff --git a/app/src/main/java/org/gnucash/android/db/SplitsDbAdapter.java b/app/src/main/java/org/gnucash/android/db/adapter/SplitsDbAdapter.java similarity index 94% rename from app/src/main/java/org/gnucash/android/db/SplitsDbAdapter.java rename to app/src/main/java/org/gnucash/android/db/adapter/SplitsDbAdapter.java index 84215fc81..5a0f2b6c8 100644 --- a/app/src/main/java/org/gnucash/android/db/SplitsDbAdapter.java +++ b/app/src/main/java/org/gnucash/android/db/adapter/SplitsDbAdapter.java @@ -15,7 +15,7 @@ * limitations under the License. */ -package org.gnucash.android.db; +package org.gnucash.android.db.adapter; import android.database.Cursor; import android.database.sqlite.SQLiteDatabase; @@ -27,7 +27,7 @@ import android.util.Pair; import org.gnucash.android.app.GnuCashApplication; -import org.gnucash.android.model.AccountType; +import org.gnucash.android.db.DatabaseSchema; import org.gnucash.android.model.Commodity; import org.gnucash.android.model.Money; import org.gnucash.android.model.Split; @@ -95,8 +95,10 @@ protected SQLiteStatement compileReplaceStatement(@NonNull final Split split) { + SplitEntry.COLUMN_QUANTITY_NUM + " , " + SplitEntry.COLUMN_QUANTITY_DENOM + " , " + SplitEntry.COLUMN_CREATED_AT + " , " + + SplitEntry.COLUMN_RECONCILE_STATE + " , " + + SplitEntry.COLUMN_RECONCILE_DATE + " , " + SplitEntry.COLUMN_ACCOUNT_UID + " , " - + SplitEntry.COLUMN_TRANSACTION_UID + " ) VALUES ( ? , ? , ? , ? , ? , ? , ? , ? , ? , ? ) "); + + SplitEntry.COLUMN_TRANSACTION_UID + " ) VALUES ( ? , ? , ? , ? , ? , ? , ? , ? , ? , ? , ? , ? ) "); } mReplaceStatement.clearBindings(); @@ -105,13 +107,15 @@ protected SQLiteStatement compileReplaceStatement(@NonNull final Split split) { mReplaceStatement.bindString(2, split.getMemo()); } mReplaceStatement.bindString(3, split.getType().name()); - mReplaceStatement.bindLong(4, split.getValue().getNumerator()); + mReplaceStatement.bindLong(4, split.getValue().getNumerator()); mReplaceStatement.bindLong(5, split.getValue().getDenominator()); - mReplaceStatement.bindLong(6, split.getQuantity().getNumerator()); - mReplaceStatement.bindLong(7, split.getQuantity().getDenominator()); + mReplaceStatement.bindLong(6, split.getQuantity().getNumerator()); + mReplaceStatement.bindLong(7, split.getQuantity().getDenominator()); mReplaceStatement.bindString(8, split.getCreatedTimestamp().toString()); - mReplaceStatement.bindString(9, split.getAccountUID()); - mReplaceStatement.bindString(10, split.getTransactionUID()); + mReplaceStatement.bindString(9, String.valueOf(split.getReconcileState())); + mReplaceStatement.bindString(10, split.getReconcileDate().toString()); + mReplaceStatement.bindString(11, split.getAccountUID()); + mReplaceStatement.bindString(12, split.getTransactionUID()); return mReplaceStatement; } @@ -131,6 +135,8 @@ public Split buildModelInstance(@NonNull final Cursor cursor){ String accountUID = cursor.getString(cursor.getColumnIndexOrThrow(SplitEntry.COLUMN_ACCOUNT_UID)); String transxUID = cursor.getString(cursor.getColumnIndexOrThrow(SplitEntry.COLUMN_TRANSACTION_UID)); String memo = cursor.getString(cursor.getColumnIndexOrThrow(SplitEntry.COLUMN_MEMO)); + String reconcileState = cursor.getString(cursor.getColumnIndexOrThrow(SplitEntry.COLUMN_RECONCILE_STATE)); + String reconcileDate = cursor.getString(cursor.getColumnIndexOrThrow(SplitEntry.COLUMN_RECONCILE_DATE)); String transactionCurrency = TransactionsDbAdapter.getInstance().getAttribute(transxUID, TransactionEntry.COLUMN_CURRENCY); Money value = new Money(valueNum, valueDenom, transactionCurrency); @@ -143,6 +149,9 @@ public Split buildModelInstance(@NonNull final Cursor cursor){ split.setTransactionUID(transxUID); split.setType(TransactionType.valueOf(typeName)); split.setMemo(memo); + split.setReconcileState(reconcileState.charAt(0)); + if (reconcileDate != null) + split.setReconcileDate(Timestamp.valueOf(reconcileDate)); return split; } diff --git a/app/src/main/java/org/gnucash/android/db/TransactionsDbAdapter.java b/app/src/main/java/org/gnucash/android/db/adapter/TransactionsDbAdapter.java similarity index 99% rename from app/src/main/java/org/gnucash/android/db/TransactionsDbAdapter.java rename to app/src/main/java/org/gnucash/android/db/adapter/TransactionsDbAdapter.java index b522ea7f7..1f25f5ccd 100644 --- a/app/src/main/java/org/gnucash/android/db/TransactionsDbAdapter.java +++ b/app/src/main/java/org/gnucash/android/db/adapter/TransactionsDbAdapter.java @@ -15,7 +15,7 @@ * limitations under the License. */ -package org.gnucash.android.db; +package org.gnucash.android.db.adapter; import android.content.ContentValues; import android.database.Cursor; @@ -143,7 +143,7 @@ public long bulkAddRecords(@NonNull List transactionList){ try { start = System.nanoTime(); long nSplits = mSplitsDbAdapter.bulkAddRecords(splitList); - Log.d(LOG_TAG, String.format("%d splits inserted in %d ns", splitList.size(), System.nanoTime()-start)); + Log.d(LOG_TAG, String.format("%d splits inserted in %d ns", nSplits, System.nanoTime()-start)); } finally { SQLiteStatement deleteEmptyTransaction = mDb.compileStatement("DELETE FROM " + diff --git a/app/src/main/java/org/gnucash/android/export/ExportAsyncTask.java b/app/src/main/java/org/gnucash/android/export/ExportAsyncTask.java index 0e17ef283..3fcb3e104 100644 --- a/app/src/main/java/org/gnucash/android/export/ExportAsyncTask.java +++ b/app/src/main/java/org/gnucash/android/export/ExportAsyncTask.java @@ -49,8 +49,8 @@ import org.gnucash.android.R; import org.gnucash.android.app.GnuCashApplication; -import org.gnucash.android.db.AccountsDbAdapter; -import org.gnucash.android.db.TransactionsDbAdapter; +import org.gnucash.android.db.adapter.AccountsDbAdapter; +import org.gnucash.android.db.adapter.TransactionsDbAdapter; import org.gnucash.android.export.ofx.OfxExporter; import org.gnucash.android.export.qif.QifExporter; import org.gnucash.android.export.xml.GncXmlExporter; @@ -151,7 +151,7 @@ protected Boolean doInBackground(ExportParams... params) { } catch (final Exception e) { Log.e(TAG, "Error exporting: " + e.getMessage()); Crashlytics.logException(e); - + e.printStackTrace(); if (mContext instanceof Activity) { ((Activity)mContext).runOnUiThread(new Runnable() { @Override diff --git a/app/src/main/java/org/gnucash/android/export/Exporter.java b/app/src/main/java/org/gnucash/android/export/Exporter.java index 9727f1013..fd41a78f4 100644 --- a/app/src/main/java/org/gnucash/android/export/Exporter.java +++ b/app/src/main/java/org/gnucash/android/export/Exporter.java @@ -27,12 +27,13 @@ import org.gnucash.android.BuildConfig; import org.gnucash.android.app.GnuCashApplication; -import org.gnucash.android.db.AccountsDbAdapter; -import org.gnucash.android.db.CommoditiesDbAdapter; -import org.gnucash.android.db.PricesDbAdapter; -import org.gnucash.android.db.ScheduledActionDbAdapter; -import org.gnucash.android.db.SplitsDbAdapter; -import org.gnucash.android.db.TransactionsDbAdapter; +import org.gnucash.android.db.adapter.AccountsDbAdapter; +import org.gnucash.android.db.adapter.BudgetsDbAdapter; +import org.gnucash.android.db.adapter.CommoditiesDbAdapter; +import org.gnucash.android.db.adapter.PricesDbAdapter; +import org.gnucash.android.db.adapter.ScheduledActionDbAdapter; +import org.gnucash.android.db.adapter.SplitsDbAdapter; +import org.gnucash.android.db.adapter.TransactionsDbAdapter; import java.io.File; import java.sql.Timestamp; @@ -102,22 +103,29 @@ public abstract class Exporter { protected ScheduledActionDbAdapter mScheduledActionDbAdapter; protected PricesDbAdapter mPricesDbAdapter; protected CommoditiesDbAdapter mCommoditiesDbAdapter; + protected BudgetsDbAdapter mBudgetsDbAdapter; protected Context mContext; public Exporter(ExportParams params, SQLiteDatabase db) { this.mExportParams = params; mContext = GnuCashApplication.getAppContext(); if (db == null) { - mAccountsDbAdapter = AccountsDbAdapter.getInstance(); - mTransactionsDbAdapter = TransactionsDbAdapter.getInstance(); - mSplitsDbAdapter = SplitsDbAdapter.getInstance(); + mAccountsDbAdapter = AccountsDbAdapter.getInstance(); + mTransactionsDbAdapter = TransactionsDbAdapter.getInstance(); + mSplitsDbAdapter = SplitsDbAdapter.getInstance(); + mPricesDbAdapter = PricesDbAdapter.getInstance(); + mCommoditiesDbAdapter = CommoditiesDbAdapter.getInstance(); + mBudgetsDbAdapter = BudgetsDbAdapter.getInstance(); mScheduledActionDbAdapter = ScheduledActionDbAdapter.getInstance(); mPricesDbAdapter = PricesDbAdapter.getInstance(); mCommoditiesDbAdapter = CommoditiesDbAdapter.getInstance(); } else { - mSplitsDbAdapter = new SplitsDbAdapter(db); - mTransactionsDbAdapter = new TransactionsDbAdapter(db, mSplitsDbAdapter); - mAccountsDbAdapter = new AccountsDbAdapter(db, mTransactionsDbAdapter); + mSplitsDbAdapter = new SplitsDbAdapter(db); + mTransactionsDbAdapter = new TransactionsDbAdapter(db, mSplitsDbAdapter); + mAccountsDbAdapter = new AccountsDbAdapter(db, mTransactionsDbAdapter); + mPricesDbAdapter = new PricesDbAdapter(db); + mCommoditiesDbAdapter = new CommoditiesDbAdapter(db); + mBudgetsDbAdapter = new BudgetsDbAdapter(db); mScheduledActionDbAdapter = new ScheduledActionDbAdapter(db); mPricesDbAdapter = new PricesDbAdapter(db); mCommoditiesDbAdapter = new CommoditiesDbAdapter(db); diff --git a/app/src/main/java/org/gnucash/android/export/ofx/OfxExporter.java b/app/src/main/java/org/gnucash/android/export/ofx/OfxExporter.java index c29e426bd..d719a3cc5 100644 --- a/app/src/main/java/org/gnucash/android/export/ofx/OfxExporter.java +++ b/app/src/main/java/org/gnucash/android/export/ofx/OfxExporter.java @@ -24,7 +24,7 @@ import org.gnucash.android.R; import org.gnucash.android.app.GnuCashApplication; -import org.gnucash.android.db.AccountsDbAdapter; +import org.gnucash.android.db.adapter.AccountsDbAdapter; import org.gnucash.android.export.ExportParams; import org.gnucash.android.export.Exporter; import org.gnucash.android.model.Account; diff --git a/app/src/main/java/org/gnucash/android/export/qif/QifExporter.java b/app/src/main/java/org/gnucash/android/export/qif/QifExporter.java index 6c1fcfde6..6070bda2e 100644 --- a/app/src/main/java/org/gnucash/android/export/qif/QifExporter.java +++ b/app/src/main/java/org/gnucash/android/export/qif/QifExporter.java @@ -20,8 +20,8 @@ import android.database.Cursor; import android.preference.PreferenceManager; -import org.gnucash.android.db.AccountsDbAdapter; -import org.gnucash.android.db.TransactionsDbAdapter; +import org.gnucash.android.db.adapter.AccountsDbAdapter; +import org.gnucash.android.db.adapter.TransactionsDbAdapter; import org.gnucash.android.export.ExportParams; import org.gnucash.android.export.Exporter; diff --git a/app/src/main/java/org/gnucash/android/export/xml/GncXmlExporter.java b/app/src/main/java/org/gnucash/android/export/xml/GncXmlExporter.java index 407d65ad8..1ac2d7b81 100644 --- a/app/src/main/java/org/gnucash/android/export/xml/GncXmlExporter.java +++ b/app/src/main/java/org/gnucash/android/export/xml/GncXmlExporter.java @@ -23,9 +23,10 @@ import com.crashlytics.android.Crashlytics; -import org.gnucash.android.db.CommoditiesDbAdapter; +import org.gnucash.android.db.adapter.CommoditiesDbAdapter; import org.gnucash.android.db.DatabaseSchema; -import org.gnucash.android.db.TransactionsDbAdapter; +import org.gnucash.android.db.adapter.RecurrenceDbAdapter; +import org.gnucash.android.db.adapter.TransactionsDbAdapter; import org.gnucash.android.export.ExportFormat; import org.gnucash.android.export.ExportParams; import org.gnucash.android.export.Exporter; @@ -33,8 +34,11 @@ import org.gnucash.android.model.AccountType; import org.gnucash.android.model.BaseModel; import org.gnucash.android.model.Commodity; +import org.gnucash.android.model.Budget; +import org.gnucash.android.model.BudgetAmount; import org.gnucash.android.model.Money; import org.gnucash.android.model.PeriodType; +import org.gnucash.android.model.Recurrence; import org.gnucash.android.model.ScheduledAction; import org.gnucash.android.model.TransactionType; import org.xmlpull.v1.XmlPullParserFactory; @@ -125,21 +129,21 @@ private void exportAccounts(XmlSerializer xmlSerializer) throws IOException { xmlSerializer.startTag(null, GncXmlHelper.TAG_ACCOUNT); xmlSerializer.attribute(null, GncXmlHelper.ATTR_KEY_VERSION, GncXmlHelper.BOOK_VERSION); // account name - xmlSerializer.startTag(null, GncXmlHelper.TAG_NAME); + xmlSerializer.startTag(null, GncXmlHelper.TAG_ACCT_NAME); xmlSerializer.text(cursor.getString(cursor.getColumnIndexOrThrow(DatabaseSchema.AccountEntry.COLUMN_NAME))); - xmlSerializer.endTag(null, GncXmlHelper.TAG_NAME); + xmlSerializer.endTag(null, GncXmlHelper.TAG_ACCT_NAME); // account guid xmlSerializer.startTag(null, GncXmlHelper.TAG_ACCT_ID); xmlSerializer.attribute(null, GncXmlHelper.ATTR_KEY_TYPE, GncXmlHelper.ATTR_VALUE_GUID); xmlSerializer.text(cursor.getString(cursor.getColumnIndexOrThrow(DatabaseSchema.AccountEntry.COLUMN_UID))); xmlSerializer.endTag(null, GncXmlHelper.TAG_ACCT_ID); // account type - xmlSerializer.startTag(null, GncXmlHelper.TAG_TYPE); + xmlSerializer.startTag(null, GncXmlHelper.TAG_ACCT_TYPE); String acct_type = cursor.getString(cursor.getColumnIndexOrThrow(DatabaseSchema.AccountEntry.COLUMN_TYPE)); xmlSerializer.text(acct_type); - xmlSerializer.endTag(null, GncXmlHelper.TAG_TYPE); + xmlSerializer.endTag(null, GncXmlHelper.TAG_ACCT_TYPE); // commodity - xmlSerializer.startTag(null, GncXmlHelper.TAG_ACCOUNT_COMMODITY); + xmlSerializer.startTag(null, GncXmlHelper.TAG_ACCT_COMMODITY); xmlSerializer.startTag(null, GncXmlHelper.TAG_COMMODITY_SPACE); xmlSerializer.text("ISO4217"); xmlSerializer.endTag(null, GncXmlHelper.TAG_COMMODITY_SPACE); @@ -147,7 +151,7 @@ private void exportAccounts(XmlSerializer xmlSerializer) throws IOException { String acctCurrencyCode = cursor.getString(cursor.getColumnIndexOrThrow(DatabaseSchema.AccountEntry.COLUMN_CURRENCY)); xmlSerializer.text(acctCurrencyCode); xmlSerializer.endTag(null, GncXmlHelper.TAG_COMMODITY_ID); - xmlSerializer.endTag(null, GncXmlHelper.TAG_ACCOUNT_COMMODITY); + xmlSerializer.endTag(null, GncXmlHelper.TAG_ACCT_COMMODITY); // commodity scu Commodity commodity = CommoditiesDbAdapter.getInstance().getCommodity(acctCurrencyCode); xmlSerializer.startTag(null, GncXmlHelper.TAG_COMMODITY_SCU); @@ -186,9 +190,9 @@ private void exportAccounts(XmlSerializer xmlSerializer) throws IOException { slotType.add(GncXmlHelper.ATTR_VALUE_STRING); slotValue.add(Boolean.toString(cursor.getInt(cursor.getColumnIndexOrThrow(DatabaseSchema.AccountEntry.COLUMN_FAVORITE)) != 0)); - xmlSerializer.startTag(null, GncXmlHelper.TAG_ACT_SLOTS); + xmlSerializer.startTag(null, GncXmlHelper.TAG_ACCT_SLOTS); exportSlots(xmlSerializer, slotKey, slotType, slotValue); - xmlSerializer.endTag(null, GncXmlHelper.TAG_ACT_SLOTS); + xmlSerializer.endTag(null, GncXmlHelper.TAG_ACCT_SLOTS); // parent uid String parentUID = cursor.getString(cursor.getColumnIndexOrThrow(DatabaseSchema.AccountEntry.COLUMN_PARENT_ACCOUNT_UID)); @@ -217,20 +221,20 @@ private void exportTemplateAccounts(XmlSerializer xmlSerializer, Collection 0; - xmlSerializer.text(enabled ? "y" : "n"); + xmlSerializer.text(scheduledAction.isEnabled() ? "y" : "n"); xmlSerializer.endTag(null, GncXmlHelper.TAG_SX_ENABLED); xmlSerializer.startTag(null, GncXmlHelper.TAG_SX_AUTO_CREATE); - xmlSerializer.text("n"); //we do not want transactions auto-created on the desktop. + xmlSerializer.text(scheduledAction.shouldAutoCreate() ? "y" : "n"); xmlSerializer.endTag(null, GncXmlHelper.TAG_SX_AUTO_CREATE); xmlSerializer.startTag(null, GncXmlHelper.TAG_SX_AUTO_CREATE_NOTIFY); - xmlSerializer.text("n"); //TODO: if we ever support notifying before creating a scheduled transaction, then update this + xmlSerializer.text(scheduledAction.shouldAutoNotify() ? "y" : "n"); xmlSerializer.endTag(null, GncXmlHelper.TAG_SX_AUTO_CREATE_NOTIFY); xmlSerializer.startTag(null, GncXmlHelper.TAG_SX_ADVANCE_CREATE_DAYS); - xmlSerializer.text("0"); + xmlSerializer.text(Integer.toString(scheduledAction.getAdvanceCreateDays())); xmlSerializer.endTag(null, GncXmlHelper.TAG_SX_ADVANCE_CREATE_DAYS); xmlSerializer.startTag(null, GncXmlHelper.TAG_SX_ADVANCE_REMIND_DAYS); - xmlSerializer.text("0"); + xmlSerializer.text(Integer.toString(scheduledAction.getAdvanceNotifyDays())); xmlSerializer.endTag(null, GncXmlHelper.TAG_SX_ADVANCE_REMIND_DAYS); xmlSerializer.startTag(null, GncXmlHelper.TAG_SX_INSTANCE_COUNT); String scheduledActionUID = cursor.getString(cursor.getColumnIndexOrThrow(ScheduledActionEntry.COLUMN_UID)); @@ -581,22 +590,15 @@ private void exportScheduledTransactions(XmlSerializer xmlSerializer) throws IOE xmlSerializer.text(accountUID.getUID()); xmlSerializer.endTag(null, GncXmlHelper.TAG_SX_TEMPL_ACCOUNT); + //// FIXME: 11.10.2015 Retrieve the information for this section from the recurrence table xmlSerializer.startTag(null, GncXmlHelper.TAG_SX_SCHEDULE); - xmlSerializer.startTag(null, GncXmlHelper.TAG_RECURRENCE); + xmlSerializer.startTag(null, GncXmlHelper.TAG_GNC_RECURRENCE); xmlSerializer.attribute(null, GncXmlHelper.ATTR_KEY_VERSION, GncXmlHelper.RECURRENCE_VERSION); - long period = cursor.getLong(cursor.getColumnIndexOrThrow(ScheduledActionEntry.COLUMN_PERIOD)); - PeriodType periodType = ScheduledAction.getPeriodType(period); - xmlSerializer.startTag(null, GncXmlHelper.TAG_RX_MULT); - xmlSerializer.text(String.valueOf(periodType.getMultiplier())); - xmlSerializer.endTag(null, GncXmlHelper.TAG_RX_MULT); - xmlSerializer.startTag(null, GncXmlHelper.TAG_RX_PERIOD_TYPE); - xmlSerializer.text(periodType.name().toLowerCase()); - xmlSerializer.endTag(null, GncXmlHelper.TAG_RX_PERIOD_TYPE); - - long recurrenceStartTime = cursor.getLong(cursor.getColumnIndexOrThrow(ScheduledActionEntry.COLUMN_START_TIME)); - serializeDate(xmlSerializer, GncXmlHelper.TAG_RX_START, recurrenceStartTime); - - xmlSerializer.endTag(null, GncXmlHelper.TAG_RECURRENCE); + + String recurrenceUID = cursor.getString(cursor.getColumnIndexOrThrow(ScheduledActionEntry.COLUMN_RECURRENCE_UID)); + Recurrence recurrence = RecurrenceDbAdapter.getInstance().getRecord(recurrenceUID); + exportRecurrence(xmlSerializer, recurrence); + xmlSerializer.endTag(null, GncXmlHelper.TAG_GNC_RECURRENCE); xmlSerializer.endTag(null, GncXmlHelper.TAG_SX_SCHEDULE); xmlSerializer.endTag(null, GncXmlHelper.TAG_SCHEDULED_ACTION); @@ -619,10 +621,10 @@ private void serializeDate(XmlSerializer xmlSerializer, String tag, long timeMil xmlSerializer.endTag(null, tag); } - private void exportCommodity(XmlSerializer xmlSerializer, List currencies) throws IOException { + private void exportCommodities(XmlSerializer xmlSerializer, List currencies) throws IOException { for (Currency currency : currencies) { xmlSerializer.startTag(null, GncXmlHelper.TAG_COMMODITY); - xmlSerializer.attribute(null, GncXmlHelper.ATTR_KEY_VERSION, "2.0.0"); + xmlSerializer.attribute(null, GncXmlHelper.ATTR_KEY_VERSION, GncXmlHelper.BOOK_VERSION); xmlSerializer.startTag(null, GncXmlHelper.TAG_COMMODITY_SPACE); xmlSerializer.text("ISO4217"); xmlSerializer.endTag(null, GncXmlHelper.TAG_COMMODITY_SPACE); @@ -694,6 +696,85 @@ private void exportPrices(XmlSerializer xmlSerializer) throws IOException { xmlSerializer.endTag(null, GncXmlHelper.TAG_PRICEDB); } + /** + * Exports the recurrence to GnuCash XML, except the recurrence tags itself i.e. the actual recurrence attributes only + *

This is because there are different recurrence start tags for transactions and budgets.
+ * So make sure to write the recurrence start/closing tags before/after calling this method.

+ * @param xmlSerializer XML serializer + * @param recurrence Recurrence object + */ + private void exportRecurrence(XmlSerializer xmlSerializer, Recurrence recurrence) throws IOException{ + PeriodType periodType = recurrence.getPeriodType(); + xmlSerializer.startTag(null, GncXmlHelper.TAG_RX_MULT); + xmlSerializer.text(String.valueOf(periodType.getMultiplier())); + xmlSerializer.endTag(null, GncXmlHelper.TAG_RX_MULT); + xmlSerializer.startTag(null, GncXmlHelper.TAG_RX_PERIOD_TYPE); + xmlSerializer.text(periodType.name().toLowerCase()); + xmlSerializer.endTag(null, GncXmlHelper.TAG_RX_PERIOD_TYPE); + + long recurrenceStartTime = recurrence.getPeriodStart().getTime(); + serializeDate(xmlSerializer, GncXmlHelper.TAG_RX_START, recurrenceStartTime); + } + + private void exportBudgets(XmlSerializer xmlSerializer) throws IOException { + Cursor cursor = mBudgetsDbAdapter.fetchAllRecords(); + while(cursor.moveToNext()) { + Budget budget = mBudgetsDbAdapter.buildModelInstance(cursor); + xmlSerializer.startTag(null, GncXmlHelper.TAG_BUDGET); + xmlSerializer.attribute(null, GncXmlHelper.ATTR_KEY_VERSION, GncXmlHelper.BOOK_VERSION); + xmlSerializer.startTag(null, GncXmlHelper.TAG_BUDGET_ID); + xmlSerializer.attribute(null, GncXmlHelper.ATTR_KEY_TYPE, GncXmlHelper.ATTR_VALUE_GUID); + xmlSerializer.text(budget.getUID()); + xmlSerializer.endTag(null, GncXmlHelper.TAG_BUDGET_ID); + xmlSerializer.startTag(null, GncXmlHelper.TAG_BUDGET_NAME); + xmlSerializer.text(budget.getName()); + xmlSerializer.endTag(null, GncXmlHelper.TAG_BUDGET_NAME); + xmlSerializer.startTag(null, GncXmlHelper.TAG_BUDGET_DESCRIPTION); + xmlSerializer.text(budget.getDescription() == null ? "" : budget.getDescription()); + xmlSerializer.endTag(null, GncXmlHelper.TAG_BUDGET_DESCRIPTION); + xmlSerializer.startTag(null, GncXmlHelper.TAG_BUDGET_NUM_PERIODS); + xmlSerializer.text(Long.toString(budget.getNumberOfPeriods())); + xmlSerializer.endTag(null, GncXmlHelper.TAG_BUDGET_NUM_PERIODS); + xmlSerializer.startTag(null, GncXmlHelper.TAG_BUDGET_RECURRENCE); + exportRecurrence(xmlSerializer, budget.getRecurrence()); + xmlSerializer.endTag(null, GncXmlHelper.TAG_BUDGET_RECURRENCE); + + //export budget slots + ArrayList slotKey = new ArrayList<>(); + ArrayList slotType = new ArrayList<>(); + ArrayList slotValue = new ArrayList<>(); + + xmlSerializer.startTag(null, GncXmlHelper.TAG_BUDGET_SLOTS); + for (BudgetAmount budgetAmount : budget.getExpandedBudgetAmounts()) { + xmlSerializer.startTag(null, GncXmlHelper.TAG_SLOT); + xmlSerializer.startTag(null, GncXmlHelper.TAG_SLOT_KEY); + xmlSerializer.text(budgetAmount.getAccountUID()); + xmlSerializer.endTag(null, GncXmlHelper.TAG_SLOT_KEY); + + Money amount = budgetAmount.getAmount(); + slotKey.clear(); + slotType.clear(); + slotValue.clear(); + for (int period = 0; period < budget.getNumberOfPeriods(); period++) { + slotKey.add(String.valueOf(period)); + slotType.add(GncXmlHelper.ATTR_VALUE_NUMERIC); + slotValue.add(amount.getNumerator() + "/" + amount.getDenominator()); + } + //budget slots + + xmlSerializer.startTag(null, GncXmlHelper.TAG_SLOT_VALUE); + xmlSerializer.attribute(null, GncXmlHelper.ATTR_KEY_TYPE, GncXmlHelper.ATTR_VALUE_FRAME); + exportSlots(xmlSerializer, slotKey, slotType, slotValue); + xmlSerializer.endTag(null, GncXmlHelper.TAG_SLOT_VALUE); + xmlSerializer.endTag(null, GncXmlHelper.TAG_SLOT); + } + + xmlSerializer.endTag(null, GncXmlHelper.TAG_BUDGET_SLOTS); + xmlSerializer.endTag(null, GncXmlHelper.TAG_BUDGET); + } + cursor.close(); + } + @Override public List generateExport() throws ExporterException { OutputStreamWriter writer = null; @@ -731,7 +812,7 @@ public List generateExport() throws ExporterException { public void generateExport(Writer writer) throws ExporterException { try { String[] namespaces = new String[]{"gnc", "act", "book", "cd", "cmdty", "price", "slot", - "split", "trn", "ts", "sx", "recurrence"}; + "split", "trn", "ts", "sx", "bgt", "recurrence"}; XmlSerializer xmlSerializer = XmlPullParserFactory.newInstance().newSerializer(); xmlSerializer.setOutput(writer); xmlSerializer.startDocument("utf-8", true); @@ -783,7 +864,7 @@ public void generateExport(Writer writer) throws ExporterException { xmlSerializer.endTag(null, GncXmlHelper.TAG_COUNT_DATA); } // export the commodities used in the DB - exportCommodity(xmlSerializer, currencies); + exportCommodities(xmlSerializer, currencies); // prices if (priceCount > 0) { exportPrices(xmlSerializer); @@ -802,6 +883,9 @@ public void generateExport(Writer writer) throws ExporterException { //scheduled actions exportScheduledTransactions(xmlSerializer); + //budgets + exportBudgets(xmlSerializer); + xmlSerializer.endTag(null, GncXmlHelper.TAG_BOOK); xmlSerializer.endTag(null, GncXmlHelper.TAG_ROOT); xmlSerializer.endDocument(); diff --git a/app/src/main/java/org/gnucash/android/export/xml/GncXmlHelper.java b/app/src/main/java/org/gnucash/android/export/xml/GncXmlHelper.java index 6a13908ca..f1eee4af8 100644 --- a/app/src/main/java/org/gnucash/android/export/xml/GncXmlHelper.java +++ b/app/src/main/java/org/gnucash/android/export/xml/GncXmlHelper.java @@ -17,9 +17,6 @@ package org.gnucash.android.export.xml; -import android.support.annotation.NonNull; - -import org.gnucash.android.db.CommoditiesDbAdapter; import org.gnucash.android.model.Commodity; import org.gnucash.android.ui.transaction.TransactionFormFragment; @@ -28,11 +25,8 @@ import java.text.NumberFormat; import java.text.ParseException; import java.text.SimpleDateFormat; -import java.util.Currency; import java.util.Date; import java.util.Locale; -import java.util.regex.Matcher; -import java.util.regex.Pattern; /** * Collection of helper tags and methods for Gnc XML export @@ -50,6 +44,7 @@ public abstract class GncXmlHelper { public static final String ATTR_VALUE_NUMERIC = "numeric"; public static final String ATTR_VALUE_GUID = "guid"; public static final String ATTR_VALUE_BOOK = "book"; + public static final String ATTR_VALUE_FRAME = "frame"; public static final String TAG_GDATE = "gdate"; /* @@ -61,18 +56,20 @@ public abstract class GncXmlHelper { public static final String TAG_COUNT_DATA = "gnc:count-data"; public static final String TAG_COMMODITY = "gnc:commodity"; - public static final String TAG_NAME = "act:name"; - public static final String TAG_ACCT_ID = "act:id"; - public static final String TAG_TYPE = "act:type"; public static final String TAG_COMMODITY_ID = "cmdty:id"; public static final String TAG_COMMODITY_SPACE = "cmdty:space"; - public static final String TAG_ACCOUNT_COMMODITY = "act:commodity"; + + public static final String TAG_ACCOUNT = "gnc:account"; + public static final String TAG_ACCT_NAME = "act:name"; + public static final String TAG_ACCT_ID = "act:id"; + public static final String TAG_ACCT_TYPE = "act:type"; + public static final String TAG_ACCT_COMMODITY = "act:commodity"; public static final String TAG_COMMODITY_SCU = "act:commodity-scu"; public static final String TAG_PARENT_UID = "act:parent"; - public static final String TAG_ACCOUNT = "gnc:account"; + public static final String TAG_SLOT_KEY = "slot:key"; public static final String TAG_SLOT_VALUE = "slot:value"; - public static final String TAG_ACT_SLOTS = "act:slots"; + public static final String TAG_ACCT_SLOTS = "act:slots"; public static final String TAG_SLOT = "slot"; public static final String TAG_ACCT_DESCRIPTION = "act:description"; @@ -91,21 +88,27 @@ public abstract class GncXmlHelper { public static final String TAG_SPLIT_ID = "split:id"; public static final String TAG_SPLIT_MEMO = "split:memo"; public static final String TAG_RECONCILED_STATE = "split:reconciled-state"; + public static final String TAG_RECONCILED_DATE = "split:recondiled-date"; public static final String TAG_SPLIT_ACCOUNT = "split:account"; public static final String TAG_SPLIT_VALUE = "split:value"; public static final String TAG_SPLIT_QUANTITY = "split:quantity"; public static final String TAG_SPLIT_SLOTS = "split:slots"; - public static final String TAG_PRICEDB = "gnc:pricedb"; - public static final String TAG_PRICE = "price"; - public static final String TAG_PRICE_ID = "price:id"; - public static final String TAG_PRICE_COMMODITY = "price:commodity"; - public static final String TAG_PRICE_CURRENCY = "price:currency"; - public static final String TAG_PRICE_TIME = "price:time"; - public static final String TAG_PRICE_SOURCE = "price:source"; - public static final String TAG_PRICE_TYPE = "price:type"; - public static final String TAG_PRICE_VALUE = "price:value"; + public static final String TAG_PRICEDB = "gnc:pricedb"; + public static final String TAG_PRICE = "price"; + public static final String TAG_PRICE_ID = "price:id"; + public static final String TAG_PRICE_COMMODITY = "price:commodity"; + public static final String TAG_PRICE_CURRENCY = "price:currency"; + public static final String TAG_PRICE_TIME = "price:time"; + public static final String TAG_PRICE_SOURCE = "price:source"; + public static final String TAG_PRICE_TYPE = "price:type"; + public static final String TAG_PRICE_VALUE = "price:value"; + /** + * Periodicity of the recurrence. + *

Only currently used for reading old backup files. May be removed in the future.

+ * @deprecated Use {@link #TAG_GNC_RECURRENCE} instead + */ @Deprecated public static final String TAG_RECURRENCE_PERIOD = "trn:recurrence_period"; @@ -126,12 +129,22 @@ public abstract class GncXmlHelper { public static final String TAG_SX_TAG = "sx:tag"; public static final String TAG_SX_TEMPL_ACCOUNT = "sx:templ-acct"; public static final String TAG_SX_SCHEDULE = "sx:schedule"; - public static final String TAG_RECURRENCE = "gnc:recurrence"; + public static final String TAG_GNC_RECURRENCE = "gnc:recurrence"; + public static final String TAG_RX_MULT = "recurrence:mult"; public static final String TAG_RX_PERIOD_TYPE = "recurrence:period_type"; public static final String TAG_RX_START = "recurrence:start"; + public static final String TAG_BUDGET = "gnc:budget"; + public static final String TAG_BUDGET_ID = "bgt:id"; + public static final String TAG_BUDGET_NAME = "bgt:name"; + public static final String TAG_BUDGET_DESCRIPTION = "bgt:description"; + public static final String TAG_BUDGET_NUM_PERIODS = "bgt:num-periods"; + public static final String TAG_BUDGET_RECURRENCE = "bgt:recurrence"; + public static final String TAG_BUDGET_SLOTS = "bgt:slots"; + + public static final String RECURRENCE_VERSION = "1.0.0"; public static final String BOOK_VERSION = "2.0.0"; public static final SimpleDateFormat TIME_FORMATTER = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss Z", Locale.US); @@ -198,6 +211,7 @@ public static BigDecimal parseSplitAmount(String amountString) throws ParseExcep * @param amount Split amount as BigDecimal * @param commodity Commodity of the transaction * @return Formatted split amount + * @deprecated Just use the values for numerator and denominator which are saved in the database */ public static String formatSplitAmount(BigDecimal amount, Commodity commodity){ int denomInt = commodity.getSmallestFraction(); diff --git a/app/src/main/java/org/gnucash/android/importer/CommoditiesXmlHandler.java b/app/src/main/java/org/gnucash/android/importer/CommoditiesXmlHandler.java index 34e75e032..7f16dabca 100644 --- a/app/src/main/java/org/gnucash/android/importer/CommoditiesXmlHandler.java +++ b/app/src/main/java/org/gnucash/android/importer/CommoditiesXmlHandler.java @@ -18,7 +18,7 @@ import android.database.sqlite.SQLiteDatabase; import org.gnucash.android.app.GnuCashApplication; -import org.gnucash.android.db.CommoditiesDbAdapter; +import org.gnucash.android.db.adapter.CommoditiesDbAdapter; import org.gnucash.android.model.Commodity; import org.xml.sax.Attributes; import org.xml.sax.SAXException; diff --git a/app/src/main/java/org/gnucash/android/importer/GncXmlHandler.java b/app/src/main/java/org/gnucash/android/importer/GncXmlHandler.java index b4ac1235f..677656c43 100644 --- a/app/src/main/java/org/gnucash/android/importer/GncXmlHandler.java +++ b/app/src/main/java/org/gnucash/android/importer/GncXmlHandler.java @@ -24,20 +24,24 @@ import com.crashlytics.android.Crashlytics; import org.gnucash.android.app.GnuCashApplication; -import org.gnucash.android.db.AccountsDbAdapter; -import org.gnucash.android.db.CommoditiesDbAdapter; -import org.gnucash.android.db.PricesDbAdapter; -import org.gnucash.android.db.ScheduledActionDbAdapter; -import org.gnucash.android.db.SplitsDbAdapter; -import org.gnucash.android.db.TransactionsDbAdapter; +import org.gnucash.android.db.adapter.AccountsDbAdapter; +import org.gnucash.android.db.adapter.BudgetsDbAdapter; +import org.gnucash.android.db.adapter.CommoditiesDbAdapter; +import org.gnucash.android.db.adapter.PricesDbAdapter; +import org.gnucash.android.db.adapter.ScheduledActionDbAdapter; +import org.gnucash.android.db.adapter.SplitsDbAdapter; +import org.gnucash.android.db.adapter.TransactionsDbAdapter; import org.gnucash.android.export.xml.GncXmlHelper; import org.gnucash.android.model.Account; import org.gnucash.android.model.AccountType; import org.gnucash.android.model.BaseModel; import org.gnucash.android.model.Commodity; +import org.gnucash.android.model.Budget; +import org.gnucash.android.model.BudgetAmount; import org.gnucash.android.model.Money; import org.gnucash.android.model.PeriodType; import org.gnucash.android.model.Price; +import org.gnucash.android.model.Recurrence; import org.gnucash.android.model.ScheduledAction; import org.gnucash.android.model.Split; import org.gnucash.android.model.Transaction; @@ -180,6 +184,13 @@ public class GncXmlHandler extends DefaultHandler { */ List mScheduledActionsList; + /** + * List of budgets which have been parsed from XML + */ + List mBudgetList; + Budget mBudget; + Recurrence mRecurrence; + BudgetAmount mBudgetAmount; boolean mInColorSlot = false; boolean mInPlaceHolderSlot = false; @@ -198,6 +209,15 @@ public class GncXmlHandler extends DefaultHandler { boolean mIsScheduledEnd = false; boolean mIsLastRun = false; boolean mIsRecurrenceStart = false; + boolean mInBudgetSlot = false; + + /** + * Saves the attribute of the slot tag + * Used for determining where we are in the budget amounts + */ + String mSlotTagAttribute = null; + + String mBudgetAmountAccountUID = null; /** * Multiplier for the recurrence period type. e.g. period type of week and multiplier of 2 means bi-weekly @@ -233,6 +253,8 @@ public class GncXmlHandler extends DefaultHandler { private Map mCurrencyCount; + private BudgetsDbAdapter mBudgetsDbAdapter; + /** * Creates a handler for handling XML stream events when parsing the XML backup file */ @@ -256,12 +278,14 @@ private void init(@Nullable SQLiteDatabase db) { mScheduledActionsDbAdapter = ScheduledActionDbAdapter.getInstance(); mCommoditiesDbAdapter = CommoditiesDbAdapter.getInstance(); mPricesDbAdapter = PricesDbAdapter.getInstance(); + mBudgetsDbAdapter = BudgetsDbAdapter.getInstance(); } else { mTransactionsDbAdapter = new TransactionsDbAdapter(db, new SplitsDbAdapter(db)); mAccountsDbAdapter = new AccountsDbAdapter(db, mTransactionsDbAdapter); mScheduledActionsDbAdapter = new ScheduledActionDbAdapter(db); mCommoditiesDbAdapter = new CommoditiesDbAdapter(db); mPricesDbAdapter = new PricesDbAdapter(db); + mBudgetsDbAdapter = new BudgetsDbAdapter(db); } mContent = new StringBuilder(); @@ -270,6 +294,7 @@ private void init(@Nullable SQLiteDatabase db) { mAccountMap = new HashMap<>(); mTransactionList = new ArrayList<>(); mScheduledActionsList = new ArrayList<>(); + mBudgetList = new ArrayList<>(); mTemplatAccountList = new ArrayList<>(); mTemplateTransactions = new ArrayList<>(); @@ -335,11 +360,33 @@ public void startElement(String uri, String localName, mPriceCommodity = true; mISO4217Currency = false; break; + + case GncXmlHelper.TAG_BUDGET: + mBudget = new Budget(); + break; + + case GncXmlHelper.TAG_GNC_RECURRENCE: + case GncXmlHelper.TAG_BUDGET_RECURRENCE: + mRecurrenceMultiplier = 1; + mRecurrence = new Recurrence(PeriodType.MONTH); + break; + case GncXmlHelper.TAG_BUDGET_SLOTS: + mInBudgetSlot = true; + break; + case GncXmlHelper.TAG_SLOT: + if (mInBudgetSlot){ + mBudgetAmount = new BudgetAmount(mBudget.getUID(), mBudgetAmountAccountUID); + } + break; + case GncXmlHelper.TAG_SLOT_VALUE: + mSlotTagAttribute = attributes.getValue(GncXmlHelper.ATTR_KEY_TYPE); + break; } } @Override public void endElement(String uri, String localName, String qualifiedName) throws SAXException { + // FIXME: 22.10.2015 First parse the number of accounts/transactions and use the numer to init the array lists String characterString = mContent.toString().trim(); if (mIgnoreElement != null) { @@ -352,14 +399,14 @@ public void endElement(String uri, String localName, String qualifiedName) throw } switch (qualifiedName) { - case GncXmlHelper.TAG_NAME: + case GncXmlHelper.TAG_ACCT_NAME: mAccount.setName(characterString); mAccount.setFullName(characterString); break; case GncXmlHelper.TAG_ACCT_ID: mAccount.setUID(characterString); break; - case GncXmlHelper.TAG_TYPE: + case GncXmlHelper.TAG_ACCT_TYPE: AccountType accountType = AccountType.valueOf(characterString); mAccount.setAccountType(accountType); mAccount.setHidden(accountType == AccountType.ROOT); //flag root account as hidden @@ -420,6 +467,8 @@ public void endElement(String uri, String localName, String qualifiedName) throw mISO4217Currency = false; } break; + case GncXmlHelper.TAG_SLOT: + break; case GncXmlHelper.TAG_SLOT_KEY: switch (characterString) { case GncXmlHelper.KEY_PLACEHOLDER: @@ -450,6 +499,12 @@ public void endElement(String uri, String localName, String qualifiedName) throw mInDebitNumericSlot = true; break; } + if (mInBudgetSlot && mBudgetAmountAccountUID == null){ + mBudgetAmountAccountUID = characterString; + mBudgetAmount.setAccountUID(characterString); + } else if (mInBudgetSlot){ + mBudgetAmount.setPeriodNum(Long.parseLong(characterString)); + } break; case GncXmlHelper.TAG_SLOT_VALUE: if (mInPlaceHolderSlot) { @@ -498,8 +553,29 @@ public void endElement(String uri, String localName, String qualifiedName) throw handleEndOfTemplateNumericSlot(characterString, TransactionType.CREDIT); } else if (mInTemplates && mInDebitNumericSlot) { handleEndOfTemplateNumericSlot(characterString, TransactionType.DEBIT); + } else if (mInBudgetSlot){ + if (mSlotTagAttribute.equals(GncXmlHelper.ATTR_VALUE_NUMERIC)) { + try { + BigDecimal bigDecimal = GncXmlHelper.parseSplitAmount(characterString); + //currency doesn't matter since we don't persist it in the budgets table + mBudgetAmount.setAmount(new Money(bigDecimal, Commodity.DEFAULT_COMMODITY)); + } catch (ParseException e) { + mBudgetAmount.setAmount(Money.getZeroInstance()); //just put zero, in case it was a formula we couldnt parse + e.printStackTrace(); + } finally { + mBudget.addBudgetAmount(mBudgetAmount); + } + mSlotTagAttribute = GncXmlHelper.ATTR_VALUE_FRAME; + } else { + mBudgetAmountAccountUID = null; + } } break; + + case GncXmlHelper.TAG_BUDGET_SLOTS: + mInBudgetSlot = false; + break; + //================ PROCESSING OF TRANSACTION TAGS ===================================== case GncXmlHelper.TAG_TRX_ID: mTransaction.setUID(characterString); @@ -583,6 +659,7 @@ public void endElement(String uri, String localName, String qualifiedName) throw mTemplateAccountToTransactionMap.put(characterString, mTransaction.getUID()); } break; + //todo: import split reconciled state and date case GncXmlHelper.TAG_TRN_SPLIT: mTransaction.addSplit(mSplit); break; @@ -627,6 +704,7 @@ public void endElement(String uri, String localName, String qualifiedName) throw case GncXmlHelper.TAG_SX_AUTO_CREATE: mScheduledAction.setAutoCreate(characterString.equals("y")); break; + //todo: export auto_notify, advance_create, advance_notify case GncXmlHelper.TAG_SX_NUM_OCCUR: mScheduledAction.setTotalFrequency(Integer.parseInt(characterString)); break; @@ -637,8 +715,7 @@ public void endElement(String uri, String localName, String qualifiedName) throw try { PeriodType periodType = PeriodType.valueOf(characterString.toUpperCase()); periodType.setMultiplier(mRecurrenceMultiplier); - if (mScheduledAction != null) //there might be recurrence tags for bugdets and other stuff - mScheduledAction.setPeriod(periodType); + mRecurrence.setPeriodType(periodType); } catch (IllegalArgumentException ex){ //the period type constant is not supported String msg = "Unsupported period constant: " + characterString; Log.e(LOG_TAG, msg); @@ -665,7 +742,7 @@ public void endElement(String uri, String localName, String qualifiedName) throw } if (mIsRecurrenceStart && mScheduledAction != null){ - mScheduledAction.setStartTime(date); + mRecurrence.setPeriodStart(new Timestamp(date)); mIsRecurrenceStart = false; } } catch (ParseException e) { @@ -683,13 +760,18 @@ public void endElement(String uri, String localName, String qualifiedName) throw mScheduledAction.setActionUID(BaseModel.generateUID()); } break; + case GncXmlHelper.TAG_GNC_RECURRENCE: + if (mScheduledAction != null){ + mScheduledAction.setRecurrence(mRecurrence); + } + break; + case GncXmlHelper.TAG_SCHEDULED_ACTION: if (mScheduledAction.getActionUID() != null && !mIgnoreScheduledAction) { mScheduledActionsList.add(mScheduledAction); int count = generateMissedScheduledTransactions(mScheduledAction); Log.i(LOG_TAG, String.format("Generated %d transactions from scheduled action", count)); } - mRecurrenceMultiplier = 1; //reset it, even though it will be parsed from XML each time mIgnoreScheduledAction = false; break; // price table @@ -728,6 +810,28 @@ public void endElement(String uri, String localName, String qualifiedName) throw mPrice = null; } break; + + case GncXmlHelper.TAG_BUDGET: + if (mBudget.getBudgetAmounts().size() > 0) //ignore if no budget amounts exist for the budget + mBudgetList.add(mBudget); + break; + + case GncXmlHelper.TAG_BUDGET_NAME: + mBudget.setName(characterString); + break; + + case GncXmlHelper.TAG_BUDGET_DESCRIPTION: + mBudget.setDescription(characterString); + break; + + case GncXmlHelper.TAG_BUDGET_NUM_PERIODS: + mBudget.setNumberOfPeriods(Long.parseLong(characterString)); + break; + + case GncXmlHelper.TAG_BUDGET_RECURRENCE: + mBudget.setRecurrence(mRecurrence); + break; + } //reset the accumulated characters @@ -846,6 +950,9 @@ public void endDocument() throws SAXException { long nPrices = mPricesDbAdapter.bulkAddRecords(mPriceList); Log.d(getClass().getSimpleName(), String.format("%d prices inserted", nPrices)); + long nBudgets = mBudgetsDbAdapter.bulkAddRecords(mBudgetList); + Log.d(getClass().getSimpleName(), String.format("%d budgets inserted", nBudgets)); + long endTime = System.nanoTime(); Log.d(getClass().getSimpleName(), String.format("bulk insert time: %d", endTime - startTime)); @@ -891,7 +998,7 @@ private void handleEndOfTemplateNumericSlot(String characterString, TransactionT try { BigDecimal amountBigD = GncXmlHelper.parseSplitAmount(characterString); Money amount = new Money(amountBigD, getCommodityForAccount(mSplit.getAccountUID())); - mSplit.setValue(amount.absolute()); + mSplit.setValue(amount.abs()); mSplit.setType(splitType); mIgnoreTemplateTransaction = false; //we have successfully parsed an amount } catch (NumberFormatException | ParseException e) { @@ -922,8 +1029,8 @@ private int generateMissedScheduledTransactions(ScheduledAction scheduledAction) } long lastRuntime = scheduledAction.getStartTime(); - if (scheduledAction.getLastRun() > 0){ - lastRuntime = scheduledAction.getLastRun(); + if (scheduledAction.getLastRunTime() > 0){ + lastRuntime = scheduledAction.getLastRunTime(); } int generatedTransactionCount = 0; diff --git a/app/src/main/java/org/gnucash/android/model/Budget.java b/app/src/main/java/org/gnucash/android/model/Budget.java new file mode 100644 index 000000000..8605146e1 --- /dev/null +++ b/app/src/main/java/org/gnucash/android/model/Budget.java @@ -0,0 +1,407 @@ +/* + * Copyright (c) 2015 Ngewi Fet + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.gnucash.android.model; + +import android.support.annotation.NonNull; +import android.util.Log; + +import org.joda.time.LocalDateTime; + +import java.math.BigDecimal; +import java.util.ArrayList; +import java.util.Currency; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +/** + * Budgets model + * @author Ngewi Fet + */ +public class Budget extends BaseModel { + + private String mName; + private String mDescription; + private Recurrence mRecurrence; + private List mBudgetAmounts = new ArrayList<>(); + private long mNumberOfPeriods = 12; //default to 12 periods per year + + /** + * Default constructor + */ + public Budget(){ + //nothing to see here, move along + } + + /** + * Overloaded constructor. + * Initializes the name and amount of this budget + * @param name String name of the budget + */ + public Budget(@NonNull String name){ + this.mName = name; + } + + public Budget(@NonNull String name, @NonNull Recurrence recurrence){ + this.mName = name; + this.mRecurrence = recurrence; + } + + /** + * Returns the name of the budget + * @return name of the budget + */ + public String getName() { + return mName; + } + + /** + * Sets the name of the budget + * @param name String name of budget + */ + public void setName(@NonNull String name) { + this.mName = name; + } + + /** + * Returns the description of the budget + * @return String description of budget + */ + public String getDescription() { + return mDescription; + } + + /** + * Sets the description of the budget + * @param description String description + */ + public void setDescription(String description) { + this.mDescription = description; + } + + /** + * Returns the recurrence for this budget + * @return Recurrence object for this budget + */ + public Recurrence getRecurrence() { + return mRecurrence; + } + + /** + * Set the recurrence pattern for this budget + * @param recurrence Recurrence object + */ + public void setRecurrence(@NonNull Recurrence recurrence) { + this.mRecurrence = recurrence; + } + + /** + * Return list of budget amounts associated with this budget + * @return List of budget amounts + */ + public List getBudgetAmounts() { + return mBudgetAmounts; + } + + /** + * Set the list of budget amounts + * @param budgetAmounts List of budget amounts + */ + public void setBudgetAmounts(List budgetAmounts) { + this.mBudgetAmounts = budgetAmounts; + for (BudgetAmount budgetAmount : mBudgetAmounts) { + budgetAmount.setBudgetUID(getUID()); + } + } + + /** + * Adds a BudgetAmount to this budget + * @param budgetAmount Budget amount + */ + public void addBudgetAmount(BudgetAmount budgetAmount){ + budgetAmount.setBudgetUID(getUID()); + mBudgetAmounts.add(budgetAmount); + } + + /** + * Returns the budget amount for a specific account + * @param accountUID GUID of the account + * @return Money amount of the budget or null if the budget has no amount for the account + */ + public Money getAmount(@NonNull String accountUID){ + for (BudgetAmount budgetAmount : mBudgetAmounts) { + if (budgetAmount.getAccountUID().equals(accountUID)) + return budgetAmount.getAmount(); + } + return null; + } + + /** + * Returns the budget amount for a specific account and period + * @param accountUID GUID of the account + * @param periodNum Budgeting period, zero-based index + * @return Money amount or zero if no matching {@link BudgetAmount} is found for the period + */ + public Money getAmount(@NonNull String accountUID, int periodNum){ + for (BudgetAmount budgetAmount : mBudgetAmounts) { + if (budgetAmount.getAccountUID().equals(accountUID) + && (budgetAmount.getPeriodNum() == periodNum || budgetAmount.getPeriodNum() == -1)){ + return budgetAmount.getAmount(); + } + } + return Money.getZeroInstance(); + } + + /** + * Returns the sum of all budget amounts in this budget + *

NOTE: This method ignores budgets of accounts which are in different currencies

+ * @return Money sum of all amounts + */ + public Money getAmountSum(){ + Money sum = null; //we explicitly allow this null instead of a money instance, because this method should never return null for a budget + for (BudgetAmount budgetAmount : mBudgetAmounts) { + if (sum == null){ + sum = budgetAmount.getAmount(); + } else { + try { + sum = sum.add(budgetAmount.getAmount().abs()); + } catch (Money.CurrencyMismatchException ex){ + Log.i(getClass().getSimpleName(), "Skip some budget amounts with different currency"); + } + } + } + return sum; + } + + /** + * Returns the number of periods covered by this budget + * @return Number of periods + */ + public long getNumberOfPeriods() { + return mNumberOfPeriods; + } + + /** + * Returns the timestamp of the start of current period of the budget + * @return Start timestamp in milliseconds + */ + public long getStartofCurrentPeriod(){ + LocalDateTime localDate = new LocalDateTime(); + int interval = mRecurrence.getPeriodType().getMultiplier(); + switch (mRecurrence.getPeriodType()){ + case DAY: + localDate = localDate.millisOfDay().withMinimumValue().plusDays(interval); + break; + case WEEK: + localDate = localDate.dayOfWeek().withMinimumValue().minusDays(interval); + break; + case MONTH: + localDate = localDate.dayOfMonth().withMinimumValue().minusMonths(interval); + break; + case YEAR: + localDate = localDate.dayOfYear().withMinimumValue().minusYears(interval); + break; + } + return localDate.toDate().getTime(); + } + + /** + * Returns the end timestamp of the current period + * @return End timestamp in milliseconds + */ + public long getEndOfCurrentPeriod(){ + LocalDateTime localDate = new LocalDateTime(); + int interval = mRecurrence.getPeriodType().getMultiplier(); + switch (mRecurrence.getPeriodType()){ + case DAY: + localDate = localDate.millisOfDay().withMaximumValue().plusDays(interval); + break; + case WEEK: + localDate = localDate.dayOfWeek().withMaximumValue().plusWeeks(interval); + break; + case MONTH: + localDate = localDate.dayOfMonth().withMaximumValue().plusMonths(interval); + break; + case YEAR: + localDate = localDate.dayOfYear().withMaximumValue().plusYears(interval); + break; + } + return localDate.toDate().getTime(); + } + + public long getStartOfPeriod(int periodNum){ + LocalDateTime localDate = new LocalDateTime(mRecurrence.getPeriodStart().getTime()); + int interval = mRecurrence.getPeriodType().getMultiplier() * periodNum; + switch (mRecurrence.getPeriodType()){ + case DAY: + localDate = localDate.millisOfDay().withMinimumValue().plusDays(interval); + break; + case WEEK: + localDate = localDate.dayOfWeek().withMinimumValue().minusDays(interval); + break; + case MONTH: + localDate = localDate.dayOfMonth().withMinimumValue().minusMonths(interval); + break; + case YEAR: + localDate = localDate.dayOfYear().withMinimumValue().minusYears(interval); + break; + } + return localDate.toDate().getTime(); + } + + /** + * Returns the end timestamp of the period + * @param periodNum Number of the period + * @return End timestamp in milliseconds of the period + */ + public long getEndOfPeriod(int periodNum){ + LocalDateTime localDate = new LocalDateTime(); + int interval = mRecurrence.getPeriodType().getMultiplier() * periodNum; + switch (mRecurrence.getPeriodType()){ + case DAY: + localDate = localDate.millisOfDay().withMaximumValue().plusDays(interval); + break; + case WEEK: + localDate = localDate.dayOfWeek().withMaximumValue().plusWeeks(interval); + break; + case MONTH: + localDate = localDate.dayOfMonth().withMaximumValue().plusMonths(interval); + break; + case YEAR: + localDate = localDate.dayOfYear().withMaximumValue().plusYears(interval); + break; + } + return localDate.toDate().getTime(); + } + + /** + * Sets the number of periods for the budget + * @param numberOfPeriods Number of periods as long + */ + public void setNumberOfPeriods(long numberOfPeriods) { + this.mNumberOfPeriods = numberOfPeriods; + } + + /** + * Returns the number of accounts in this budget + * @return Number of budgeted accounts + */ + public int getNumberOfAccounts(){ + Set accountSet = new HashSet<>(); + for (BudgetAmount budgetAmount : mBudgetAmounts) { + accountSet.add(budgetAmount.getAccountUID()); + } + return accountSet.size(); + } + + /** + * Returns the list of budget amounts where only one BudgetAmount is present if the amount of the budget amount + * is the same for all periods in the budget. + * BudgetAmounts with different amounts per period are still return separately + *

+ * This method is used during import because GnuCash desktop saves one BudgetAmount per period for the whole budgeting period. + * While this can be easily displayed in a table form on the desktop, it is not feasible in the Android app. + * So we display only one BudgetAmount if it covers all periods in the budgeting period + *

+ * @return List of {@link BudgetAmount}s + */ + public List getCompactedBudgetAmounts(){ + + Map> accountAmountMap = new HashMap<>(); + for (BudgetAmount budgetAmount : mBudgetAmounts) { + String accountUID = budgetAmount.getAccountUID(); + BigDecimal amount = budgetAmount.getAmount().asBigDecimal(); + if (accountAmountMap.containsKey(accountUID)){ + accountAmountMap.get(accountUID).add(amount); + } else { + List amounts = new ArrayList<>(); + amounts.add(amount); + accountAmountMap.put(accountUID, amounts); + } + } + + List compactBudgetAmounts = new ArrayList<>(); + for (Map.Entry> entry : accountAmountMap.entrySet()) { + List amounts = entry.getValue(); + BigDecimal first = amounts.get(0); + boolean allSame = true; + for (BigDecimal bigDecimal : amounts) { + allSame &= bigDecimal.equals(first); + } + + if (allSame){ + if (amounts.size() == 1) { + for (BudgetAmount bgtAmount : mBudgetAmounts) { + if (bgtAmount.getAccountUID().equals(entry.getKey())) { + compactBudgetAmounts.add(bgtAmount); + break; + } + } + } else { + BudgetAmount bgtAmount = new BudgetAmount(getUID(), entry.getKey()); + bgtAmount.setAmount(new Money(first, Commodity.DEFAULT_COMMODITY)); + bgtAmount.setPeriodNum(-1); + compactBudgetAmounts.add(bgtAmount); + } + } else { + //if not all amounts are the same, then just add them as we read them + for (BudgetAmount bgtAmount : mBudgetAmounts) { + if (bgtAmount.getAccountUID().equals(entry.getKey())){ + compactBudgetAmounts.add(bgtAmount); + } + } + } + } + + return compactBudgetAmounts; + } + + /** + * Returns a list of budget amounts where each period has it's own budget amount + *

Any budget amounts in the database with a period number of -1 are expanded to individual budget amounts for all periods

+ *

This method is useful with exporting budget amounts to XML

+ * @return List of expande + */ + public List getExpandedBudgetAmounts(){ + List amountsToAdd = new ArrayList<>(); + List amountsToRemove = new ArrayList<>(); + for (BudgetAmount budgetAmount : mBudgetAmounts) { + if (budgetAmount.getPeriodNum() == -1){ + amountsToRemove.add(budgetAmount); + String accountUID = budgetAmount.getAccountUID(); + for (int period = 0; period < mNumberOfPeriods; period++) { + BudgetAmount bgtAmount = new BudgetAmount(getUID(), accountUID); + bgtAmount.setAmount(budgetAmount.getAmount()); + bgtAmount.setPeriodNum(period); + amountsToAdd.add(bgtAmount); + } + } + } + + List expandedBudgetAmounts = new ArrayList<>(mBudgetAmounts); + for (BudgetAmount bgtAmount : amountsToRemove) { + expandedBudgetAmounts.remove(bgtAmount); + } + + for (BudgetAmount bgtAmount : amountsToAdd) { + expandedBudgetAmounts.add(bgtAmount); + } + return expandedBudgetAmounts; + } +} diff --git a/app/src/main/java/org/gnucash/android/model/BudgetAmount.java b/app/src/main/java/org/gnucash/android/model/BudgetAmount.java new file mode 100644 index 000000000..8aa42b26d --- /dev/null +++ b/app/src/main/java/org/gnucash/android/model/BudgetAmount.java @@ -0,0 +1,104 @@ +/* + * Copyright (c) 2015 Ngewi Fet + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.gnucash.android.model; + +/** + * Budget amounts for the different accounts. + * The {@link Money} amounts are absolute values + * @see Budget + */ +public class BudgetAmount extends BaseModel { + + private String mBudgetUID; + private String mAccountUID; + /** + * Period number for this budget amount + * A value of -1 indicates that this budget amount applies to all periods + */ + private long mPeriodNum; + private Money mAmount; + + /** + * Create a new budget amount + * @param budgetUID GUID of the budget + * @param accountUID GUID of the account + */ + public BudgetAmount(String budgetUID, String accountUID){ + this.mBudgetUID = budgetUID; + this.mAccountUID = accountUID; + } + + /** + * Creates a new budget amount with the absolute value of {@code amount} + * @param amount Money amount of the budget + * @param accountUID GUID of the account + */ + public BudgetAmount(Money amount, String accountUID){ + this.mAmount = amount.abs(); + this.mAccountUID = accountUID; + } + + public String getBudgetUID() { + return mBudgetUID; + } + + public void setBudgetUID(String budgetUID) { + this.mBudgetUID = budgetUID; + } + + public String getAccountUID() { + return mAccountUID; + } + + public void setAccountUID(String accountUID) { + this.mAccountUID = accountUID; + } + + /** + * Returns the period number of this budget amount + *

The period is zero-based index, and a value of -1 indicates that this budget amount is applicable to all budgeting periods

+ * @return Period number + */ + public long getPeriodNum() { + return mPeriodNum; + } + + /** + * Set the period number for this budget amount + *

A value of -1 indicates that this BudgetAmount is for all periods

+ * @param periodNum Zero-based period number of the budget amount + */ + public void setPeriodNum(long periodNum) { + this.mPeriodNum = periodNum; + } + + /** + * Returns the Money amount of this budget amount + * @return Money amount + */ + public Money getAmount() { + return mAmount; + } + + /** + * Sets the amount for the budget + *

The absolute value of the amount is used

+ * @param amount Money amount + */ + public void setAmount(Money amount) { + this.mAmount = amount.abs(); + } +} diff --git a/app/src/main/java/org/gnucash/android/model/Commodity.java b/app/src/main/java/org/gnucash/android/model/Commodity.java index f60b787e3..fb2d7ac2b 100644 --- a/app/src/main/java/org/gnucash/android/model/Commodity.java +++ b/app/src/main/java/org/gnucash/android/model/Commodity.java @@ -16,7 +16,7 @@ package org.gnucash.android.model; import org.gnucash.android.app.GnuCashApplication; -import org.gnucash.android.db.CommoditiesDbAdapter; +import org.gnucash.android.db.adapter.CommoditiesDbAdapter; /** * Commodities are the currencies used in the application. diff --git a/app/src/main/java/org/gnucash/android/model/Money.java b/app/src/main/java/org/gnucash/android/model/Money.java index 02c6930ca..7c5e0a700 100644 --- a/app/src/main/java/org/gnucash/android/model/Money.java +++ b/app/src/main/java/org/gnucash/android/model/Money.java @@ -22,8 +22,6 @@ import com.crashlytics.android.Crashlytics; -import org.gnucash.android.app.GnuCashApplication; - import java.math.BigDecimal; import java.math.BigInteger; import java.math.RoundingMode; @@ -84,8 +82,7 @@ public final class Money implements Comparable{ */ public static Money getZeroInstance(){ if (sDefaultZero == null) { - String currencyCode = GnuCashApplication.getDefaultCurrencyCode(); - sDefaultZero = new Money(BigDecimal.ZERO, Commodity.getInstance(currencyCode)); + sDefaultZero = new Money(BigDecimal.ZERO, Commodity.DEFAULT_COMMODITY); } return sDefaultZero; } @@ -249,10 +246,11 @@ private int getScale() { /** * Returns the amount represented by this Money object + *

The scale and rounding mode of the returned value are set to that of this Money object

* @return {@link BigDecimal} valure of amount in object */ public BigDecimal asBigDecimal() { - return mAmount; + return mAmount.setScale(mCommodity.getSmallestFractionDigits(), RoundingMode.HALF_EVEN); } /** @@ -339,11 +337,11 @@ private void setAmount(@NonNull BigDecimal amount) { * * @param addend Second operand in the addition. * @return Money object whose value is the sum of this object and money - * @throws IllegalArgumentException if the Money objects to be added have different Currencies + * @throws CurrencyMismatchException if the Money objects to be added have different Currencies */ public Money add(Money addend){ if (!mCommodity.equals(addend.mCommodity)) - throw new IllegalArgumentException("Only Money with same currency can be added"); + throw new CurrencyMismatchException(); BigDecimal bigD = mAmount.add(addend.mAmount); return new Money(bigD, mCommodity); @@ -355,11 +353,11 @@ public Money add(Money addend){ * This object is the minuend and the parameter is the subtrahend * @param subtrahend Second operand in the subtraction. * @return Money object whose value is the difference of this object and subtrahend - * @throws IllegalArgumentException if the Money objects to be added have different Currencies + * @throws CurrencyMismatchException if the Money objects to be added have different Currencies */ public Money subtract(Money subtrahend){ if (!mCommodity.equals(subtrahend.mCommodity)) - throw new IllegalArgumentException("Operation can only be performed on money with same currency"); + throw new CurrencyMismatchException(); BigDecimal bigD = mAmount.subtract(subtrahend.mAmount); return new Money(bigD, mCommodity); @@ -369,13 +367,14 @@ public Money subtract(Money subtrahend){ * Returns a new Money object whose value is the quotient of the values of * this object and divisor. * This object is the dividend and divisor is the divisor + *

This method uses the rounding mode {@link BigDecimal#ROUND_HALF_EVEN}

* @param divisor Second operand in the division. * @return Money object whose value is the quotient of this object and divisor - * @throws IllegalArgumentException if the Money objects to be added have different Currencies + * @throws CurrencyMismatchException if the Money objects to be added have different Currencies */ public Money divide(Money divisor){ if (!mCommodity.equals(divisor.mCommodity)) - throw new IllegalArgumentException("Operation can only be performed on money with same currency"); + throw new CurrencyMismatchException(); BigDecimal bigD = mAmount.divide(divisor.mAmount, mCommodity.getSmallestFractionDigits(), ROUNDING_MODE); return new Money(bigD, mCommodity); @@ -398,11 +397,11 @@ public Money divide(int divisor){ * * @param money Second operand in the multiplication. * @return Money object whose value is the product of this object and money - * @throws IllegalArgumentException if the Money objects to be added have different Currencies + * @throws CurrencyMismatchException if the Money objects to be added have different Currencies */ public Money multiply(Money money){ if (!mCommodity.equals(money.mCommodity)) - throw new IllegalArgumentException("Operation can only be performed on money with same currency"); + throw new CurrencyMismatchException(); BigDecimal bigD = mAmount.multiply(money.mAmount); return new Money(bigD, mCommodity); @@ -490,7 +489,7 @@ public boolean equals(Object obj) { @Override public int compareTo(@NonNull Money another) { if (!mCommodity.equals(another.mCommodity)) - throw new IllegalArgumentException("Cannot compare different currencies yet"); + throw new CurrencyMismatchException(); return mAmount.compareTo(another.mAmount); } @@ -498,7 +497,7 @@ public int compareTo(@NonNull Money another) { * Returns a new instance of {@link Money} object with the absolute value of the current object * @return Money object with absolute value of this instance */ - public Money absolute() { + public Money abs() { return new Money(mAmount.abs(), mCommodity); } @@ -509,4 +508,11 @@ public Money absolute() { public boolean isAmountZero() { return mAmount.compareTo(BigDecimal.ZERO) == 0; } + + public class CurrencyMismatchException extends IllegalArgumentException{ + @Override + public String getMessage() { + return "Cannot perform operation on Money instances with different currencies"; + } + } } diff --git a/app/src/main/java/org/gnucash/android/model/PeriodType.java b/app/src/main/java/org/gnucash/android/model/PeriodType.java index dc3b2d25f..06b13cc4b 100644 --- a/app/src/main/java/org/gnucash/android/model/PeriodType.java +++ b/app/src/main/java/org/gnucash/android/model/PeriodType.java @@ -20,6 +20,7 @@ import org.gnucash.android.R; import org.gnucash.android.app.GnuCashApplication; +import org.gnucash.android.ui.util.RecurrenceParser; import java.text.SimpleDateFormat; import java.util.Date; @@ -31,10 +32,48 @@ * @see org.gnucash.android.model.ScheduledAction */ public enum PeriodType { - DAY, WEEK, MONTH, YEAR; + DAY, WEEK, MONTH, YEAR; // TODO: 22.10.2015 add support for hourly int mMultiplier = 1; //multiplier for the period type + /** + * Computes the {@link PeriodType} for a given {@code period} + * @param period Period in milliseconds since Epoch + * @return PeriodType corresponding to the period + */ + public static PeriodType parse(long period){ + PeriodType periodType = DAY; + int result = (int) (period/ RecurrenceParser.YEAR_MILLIS); + if (result > 0) { + periodType = YEAR; + periodType.setMultiplier(result); + return periodType; + } + + result = (int) (period/RecurrenceParser.MONTH_MILLIS); + if (result > 0) { + periodType = MONTH; + periodType.setMultiplier(result); + return periodType; + } + + result = (int) (period/RecurrenceParser.WEEK_MILLIS); + if (result > 0) { + periodType = WEEK; + periodType.setMultiplier(result); + return periodType; + } + + result = (int) (period/RecurrenceParser.DAY_MILLIS); + if (result > 0) { + periodType = DAY; + periodType.setMultiplier(result); + return periodType; + } + + return periodType; + } + /** * Sets the multiplier for this period type * e.g. bi-weekly actions have period type {@link PeriodType#WEEK} and multiplier 2 @@ -79,7 +118,7 @@ public String getFrequencyDescription() { */ public String getFrequencyRepeatString(){ Resources res = GnuCashApplication.getAppContext().getResources(); - + //todo: take multiplier into account here switch (this) { case DAY: return res.getQuantityString(R.plurals.label_every_x_days, mMultiplier, mMultiplier); diff --git a/app/src/main/java/org/gnucash/android/model/Recurrence.java b/app/src/main/java/org/gnucash/android/model/Recurrence.java new file mode 100644 index 000000000..87dcb5623 --- /dev/null +++ b/app/src/main/java/org/gnucash/android/model/Recurrence.java @@ -0,0 +1,323 @@ +/* + * Copyright (c) 2015 Ngewi Fet + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.gnucash.android.model; + +import android.support.annotation.NonNull; + +import org.gnucash.android.ui.util.RecurrenceParser; +import org.joda.time.Days; +import org.joda.time.LocalDate; +import org.joda.time.LocalDateTime; +import org.joda.time.Months; +import org.joda.time.Weeks; +import org.joda.time.Years; + +import java.sql.Timestamp; +import java.text.SimpleDateFormat; +import java.util.Date; +import java.util.Locale; + +/** + * Model for recurrences in the database + *

Basically a wrapper around {@link PeriodType}

+ */ +public class Recurrence extends BaseModel { + + private PeriodType mPeriodType; + + /** + * Start time of the recurrence + */ + private Timestamp mPeriodStart; + + /** + * End time of this recurrence + *

This value is not persisted to the database

+ */ + private Timestamp mPeriodEnd; + + /** + * Describes which day on which to run the recurrence + */ + private String mByDay; + + public Recurrence(@NonNull PeriodType periodType){ + setPeriodType(periodType); + mPeriodStart = new Timestamp(System.currentTimeMillis()); + } + + /** + * Return the PeriodType for this recurrence + * @return PeriodType for the recurrence + */ + public PeriodType getPeriodType() { + return mPeriodType; + } + + /** + * Sets the period type for the recurrence + * @param periodType PeriodType + */ + public void setPeriodType(PeriodType periodType) { + this.mPeriodType = periodType; + } + + /** + * Return the start time for this recurrence + * @return Timestamp of start of recurrence + */ + public Timestamp getPeriodStart() { + return mPeriodStart; + } + + /** + * Set the start time of this recurrence + * @param periodStart {@link Timestamp} of recurrence + */ + public void setPeriodStart(Timestamp periodStart) { + this.mPeriodStart = periodStart; + } + + + /** + * Returns an approximate period for this recurrence + *

The period is approximate because months do not all have the same number of days, + * but that is assumed

+ * @return Milliseconds since Epoch representing the period + */ + public long getPeriod(){ + long baseMillis = 0; + switch (mPeriodType){ + case DAY: + baseMillis = RecurrenceParser.DAY_MILLIS; + break; + case WEEK: + baseMillis = RecurrenceParser.WEEK_MILLIS; + break; + case MONTH: + baseMillis = RecurrenceParser.MONTH_MILLIS; + break; + case YEAR: + baseMillis = RecurrenceParser.YEAR_MILLIS; + break; + } + return mPeriodType.getMultiplier() * baseMillis; + } + + /** + * Returns the event schedule (start, end and recurrence) + * @return String description of repeat schedule + */ + public String getRepeatString(){ + String dayOfWeek = new SimpleDateFormat("EEEE", Locale.US).format(new Date(mPeriodStart.getTime())); + + StringBuilder ruleBuilder = new StringBuilder(mPeriodType.getFrequencyRepeatString()); + + if (mPeriodType == PeriodType.WEEK) { + ruleBuilder.append(" on ").append(dayOfWeek); + } + + return ruleBuilder.toString(); + } + + /** + * Creates an RFC 2445 string which describes this recurring event. + *

See http://recurrance.sourceforge.net/

+ *

The output of this method is not meant for human consumption

+ * @return String describing event + */ + public String getRuleString(){ + String separator = ";"; + + StringBuilder ruleBuilder = new StringBuilder(); + +// ======================================================================= + //This section complies with the formal rules, but the betterpickers library doesn't like/need it + +// SimpleDateFormat startDateFormat = new SimpleDateFormat("'TZID'=zzzz':'yyyyMMdd'T'HHmmss", Locale.US); +// ruleBuilder.append("DTSTART;"); +// ruleBuilder.append(startDateFormat.format(new Date(mStartDate))); +// ruleBuilder.append("\n"); +// ruleBuilder.append("RRULE:"); +// ======================================================================== + + + ruleBuilder.append("FREQ=").append(mPeriodType.getFrequencyDescription()).append(separator); + ruleBuilder.append("INTERVAL=").append(mPeriodType.getMultiplier()).append(separator); + ruleBuilder.append(mPeriodType.getByParts(mPeriodStart.getTime())).append(separator); + + return ruleBuilder.toString(); + } + + /** + * Return the number of days left in this period + * @return Number of days left in period + */ + public int getDaysLeftInCurrentPeriod(){ + LocalDate startDate = new LocalDate(System.currentTimeMillis()); + int interval = mPeriodType.getMultiplier() - 1; + LocalDate endDate = null; + switch (mPeriodType){ + case DAY: + endDate = new LocalDate(System.currentTimeMillis()).plusDays(interval); + break; + case WEEK: + endDate = startDate.dayOfWeek().withMaximumValue().plusWeeks(interval); + break; + case MONTH: + endDate = startDate.dayOfMonth().withMaximumValue().plusMonths(interval); + break; + case YEAR: + endDate = startDate.dayOfYear().withMaximumValue().plusYears(interval); + break; + } + + return Days.daysBetween(startDate, endDate).getDays(); + } + + /** + * Returns the number of periods from the start date of this occurence until the end of the + * interval multiplier specified in the {@link PeriodType} + * @return Number of periods in this recurrence + */ + public int getNumberOfPeriods(int numberOfPeriods) { + LocalDate startDate = new LocalDate(mPeriodStart.getTime()); + LocalDate endDate; + int interval = mPeriodType.getMultiplier(); + switch (mPeriodType){ + + case DAY: + return 1; + case WEEK: + endDate = startDate.dayOfWeek().withMaximumValue().plusWeeks(numberOfPeriods); + return Weeks.weeksBetween(startDate, endDate).getWeeks() / interval; + case MONTH: + endDate = startDate.dayOfMonth().withMaximumValue().plusMonths(numberOfPeriods); + return Months.monthsBetween(startDate, endDate).getMonths() / interval; + case YEAR: + endDate = startDate.dayOfYear().withMaximumValue().plusYears(numberOfPeriods); + return Years.yearsBetween(startDate, endDate).getYears() / interval; + } + + return 0; + } + + /** + * Return the name of the current period + * @return String of current period + */ + public String getTextOfCurrentPeriod(int periodNum){ + LocalDate startDate = new LocalDate(mPeriodStart.getTime()); + switch (mPeriodType){ + + case DAY: + return startDate.dayOfWeek().getAsText(); + case WEEK: + return startDate.weekOfWeekyear().getAsText(); + case MONTH: + return startDate.monthOfYear().getAsText(); + case YEAR: + return startDate.year().getAsText(); + } + return "Period " + periodNum; + } + + /** + * Sets the string which determines on which day the recurrence will be run + * @param byDay Byday string of recurrence rule (RFC 2445) + */ + public void setByDay(String byDay){ + this.mByDay = byDay; + } + + /** + * Return the byDay string of recurrence rule (RFC 2445) + * @return String with by day specification + */ + public String getByDay(){ + return mByDay; + } + + /** + * Computes the number of occurrences of this recurrences between start and end date + *

If there is no end date, it returns -1

+ * @return Number of occurrences, or -1 if there is no end date + */ + public int getCount(){ + int count = 0; + LocalDate startDate = new LocalDate(mPeriodStart.getTime()); + LocalDate endDate = new LocalDate(mPeriodEnd.getTime()); + switch (mPeriodType){ + case DAY: + count = Days.daysBetween(startDate, endDate).getDays(); + break; + case WEEK: + count = Weeks.weeksBetween(startDate, endDate).getWeeks(); + break; + case MONTH: + count = Months.monthsBetween(startDate, endDate).getMonths(); + break; + case YEAR: + count = Years.yearsBetween(startDate, endDate).getYears(); + break; + } + return count; + } + + /** + * Sets the end time of this recurrence by specifying the number of occurences + * @param numberOfOccurences Number of occurences from the start time + */ + public void setPeriodEnd(int numberOfOccurences){ + LocalDateTime localDate = new LocalDateTime(mPeriodStart.getTime()); + LocalDateTime endDate; + switch (mPeriodType){ + case DAY: + endDate = localDate.dayOfWeek().getLocalDateTime().plusDays(numberOfOccurences); + break; + case WEEK: + endDate = localDate.dayOfWeek().getLocalDateTime().plusWeeks(numberOfOccurences); + break; + case MONTH: + endDate = localDate.dayOfMonth().getLocalDateTime().plusMonths(numberOfOccurences); + break; + case YEAR: + endDate = localDate.monthOfYear().getLocalDateTime().plusYears(numberOfOccurences); + break; + default: //default to monthly + endDate = localDate.dayOfMonth().getLocalDateTime().plusMonths(numberOfOccurences); + break; + } + mPeriodEnd = new Timestamp(endDate.toDate().getTime()); + } + + /** + * Return the end date of the period in milliseconds + * @return End date of the recurrence period + */ + public Timestamp getPeriodEnd(){ + return mPeriodEnd; + } + + /** + * Set period end date + * @param endTimestamp End time in milliseconds + */ + public void setPeriodEnd(Timestamp endTimestamp){ + mPeriodEnd = endTimestamp; + } +} diff --git a/app/src/main/java/org/gnucash/android/model/ScheduledAction.java b/app/src/main/java/org/gnucash/android/model/ScheduledAction.java index bea49f09c..8bb062f72 100644 --- a/app/src/main/java/org/gnucash/android/model/ScheduledAction.java +++ b/app/src/main/java/org/gnucash/android/model/ScheduledAction.java @@ -15,11 +15,9 @@ */ package org.gnucash.android.model; -import org.gnucash.android.ui.util.RecurrenceParser; -import org.joda.time.format.DateTimeFormat; -import org.joda.time.format.DateTimeFormatter; +import org.joda.time.LocalDate; -import java.io.IOException; +import java.sql.Timestamp; import java.text.DateFormat; import java.text.SimpleDateFormat; import java.util.Date; @@ -33,16 +31,19 @@ */ public class ScheduledAction extends BaseModel{ - private long mPeriod; private long mStartDate; private long mEndDate; private String mTag; + /** + * Recurrence of this scheduled action + */ + private Recurrence mRecurrence; + /** * Types of events which can be scheduled */ - public enum ActionType {TRANSACTION, BACKUP - } + public enum ActionType {TRANSACTION, BACKUP} /** * Next scheduled run of Event @@ -79,11 +80,14 @@ public enum ActionType {TRANSACTION, BACKUP * Flag for whether the scheduled transaction should be auto-created * TODO: Add this flag to the database. At the moment we always treat it as true */ - private boolean autoCreate = true; + private boolean mAutoCreate = true; + private boolean mAutoNotify = false; + private int mAdvanceCreateDays = 0; + private int mAdvanceNotifyDays = 0; + private String mTemplateAccountUID; public ScheduledAction(ActionType actionType){ mActionType = actionType; - mStartDate = System.currentTimeMillis(); mEndDate = 0; mIsEnabled = true; //all actions are enabled by default } @@ -124,102 +128,55 @@ public void setActionUID(String actionUID) { * Returns the timestamp of the last execution of this scheduled action * @return Timestamp in milliseconds since Epoch */ - public long getLastRun() { + public long getLastRunTime() { return mLastRun; } /** - * Set time of last execution of the scheduled action - * @param nextRun Timestamp in milliseconds since Epoch + * Computes the next time that this scheduled action is supposed to be executed, taking the + * last execution time into account + *

This method does not consider the end time, or number of times it should be run. + * It only considers when the next execution would theoretically be due

+ * @return Next run time in milliseconds */ - public void setLastRun(long nextRun) { - this.mLastRun = nextRun; - } - - /** - * Returns the period of this scheduled action - * @return Period in milliseconds since Epoch - */ - public long getPeriod() { - return mPeriod; - } - - /** - * Sets the period of the scheduled action - * @param period Period in milliseconds since Epoch - */ - public void setPeriod(long period) { - this.mPeriod = period; - } - - /** - * Sets the period given the period type. - * The {@link PeriodType} should have the multiplier set, - * e.g. bi-weekly actions have period type {@link PeriodType#WEEK} and multiplier 2 - * @param periodType Type of period - */ - public void setPeriod(PeriodType periodType){ - int multiplier = periodType.getMultiplier(); - switch (periodType){ + public long computeNextRunTime(){ + int multiplier = mRecurrence.getPeriodType().getMultiplier(); + long time = mLastRun; + if (time == 0) { + time = mStartDate; + } + LocalDate localDate = LocalDate.fromDateFields(new Date(mLastRun)); + switch (mRecurrence.getPeriodType()) { case DAY: - mPeriod = RecurrenceParser.DAY_MILLIS * multiplier; + localDate.plusDays(multiplier); break; case WEEK: - mPeriod = RecurrenceParser.WEEK_MILLIS * multiplier; + localDate.plusWeeks(multiplier); break; case MONTH: - mPeriod = RecurrenceParser.MONTH_MILLIS * multiplier; + localDate.plusMonths(multiplier); break; case YEAR: - mPeriod = RecurrenceParser.YEAR_MILLIS * multiplier; + localDate.plusYears(multiplier); break; } + return localDate.toDate().getTime(); } /** - * Returns the period type for this scheduled action - * @return Period type of the action + * Set time of last execution of the scheduled action + * @param nextRun Timestamp in milliseconds since Epoch */ - public PeriodType getPeriodType(){ - return getPeriodType(mPeriod); + public void setLastRun(long nextRun) { + this.mLastRun = nextRun; } /** - * Computes the {@link PeriodType} for a given {@code period} - * @param period Period in milliseconds since Epoch - * @return PeriodType corresponding to the period - */ - public static PeriodType getPeriodType(long period){ - PeriodType periodType = PeriodType.DAY; - int result = (int) (period/RecurrenceParser.YEAR_MILLIS); - if (result > 0) { - periodType = PeriodType.YEAR; - periodType.setMultiplier(result); - return periodType; - } - - result = (int) (period/RecurrenceParser.MONTH_MILLIS); - if (result > 0) { - periodType = PeriodType.MONTH; - periodType.setMultiplier(result); - return periodType; - } - - result = (int) (period/RecurrenceParser.WEEK_MILLIS); - if (result > 0) { - periodType = PeriodType.WEEK; - periodType.setMultiplier(result); - return periodType; - } - - result = (int) (period/RecurrenceParser.DAY_MILLIS); - if (result > 0) { - periodType = PeriodType.DAY; - periodType.setMultiplier(result); - return periodType; - } - - return periodType; + * Returns the period of this scheduled action + * @return Period in milliseconds since Epoch + */ + public long getPeriod() { + return mRecurrence.getPeriod(); } /** @@ -236,6 +193,9 @@ public long getStartTime() { */ public void setStartTime(long startDate) { this.mStartDate = startDate; + if (mRecurrence != null) { + mRecurrence.setPeriodStart(new Timestamp(startDate)); + } } /** @@ -246,22 +206,15 @@ public long getEndTime() { return mEndDate; } - /** - * Returns the approximate end time of this scheduled action. - *

This is useful when the number of occurences was set, rather than a specific end time. - * The end time is then computed from the start time, period and number of occurrences.

- * @return End time in milliseconds for the scheduled action - */ - public long getApproxEndTime(){ - return mStartDate + (mPeriod * mTotalFrequency); - } - /** * Sets the end time of the scheduled action * @param endDate Timestamp in milliseconds since Epoch */ public void setEndTime(long endDate) { this.mEndDate = endDate; + if (mRecurrence != null){ + mRecurrence.setPeriodStart(new Timestamp(mEndDate)); + } } /** @@ -335,18 +288,94 @@ public void setExecutionCount(int executionCount){ /** * Returns flag if transactions should be automatically created or not + *

This flag is currently unused in the app. It is only included here for compatibility with GnuCash desktop XML

* @return {@code true} if the transaction should be auto-created, {@code false} otherwise */ public boolean shouldAutoCreate() { - return autoCreate; + return mAutoCreate; } /** * Set flag for automatically creating transaction based on this scheduled action + *

This flag is currently unused in the app. It is only included here for compatibility with GnuCash desktop XML

* @param autoCreate Flag for auto creating transactions */ public void setAutoCreate(boolean autoCreate) { - this.autoCreate = autoCreate; + this.mAutoCreate = autoCreate; + } + + /** + * Check if user will be notified of creation of scheduled transactions + *

This flag is currently unused in the app. It is only included here for compatibility with GnuCash desktop XML

+ * @return {@code true} if user will be notified, {@code false} otherwise + */ + public boolean shouldAutoNotify() { + return mAutoNotify; + } + + /** + * Sets whether to notify the user that scheduled transactions have been created + *

This flag is currently unused in the app. It is only included here for compatibility with GnuCash desktop XML

+ * @param autoNotify Boolean flag + */ + public void setAutoNotify(boolean autoNotify) { + this.mAutoNotify = autoNotify; + } + + /** + * Returns number of days in advance to create the transaction + *

This flag is currently unused in the app. It is only included here for compatibility with GnuCash desktop XML

+ * @return Number of days in advance to create transaction + */ + public int getAdvanceCreateDays() { + return mAdvanceCreateDays; + } + + /** + * Set number of days in advance to create the transaction + *

This flag is currently unused in the app. It is only included here for compatibility with GnuCash desktop XML

+ * @param advanceCreateDays Number of days + */ + public void setAdvanceCreateDays(int advanceCreateDays) { + this.mAdvanceCreateDays = advanceCreateDays; + } + + /** + * Returns the number of days in advance to notify of scheduled transactions + *

This flag is currently unused in the app. It is only included here for compatibility with GnuCash desktop XML

+ * @return {@code true} if user will be notified, {@code false} otherwise + */ + public int getAdvanceNotifyDays() { + return mAdvanceNotifyDays; + } + + /** + * Set number of days in advance to notify of scheduled transactions + *

This flag is currently unused in the app. It is only included here for compatibility with GnuCash desktop XML

+ * @param advanceNotifyDays Number of days + */ + public void setAdvanceNotifyDays(int advanceNotifyDays) { + this.mAdvanceNotifyDays = advanceNotifyDays; + } + + /** + * Return the template account GUID for this scheduled action + *

This method generates one if none was set

+ * @return String GUID of template account + */ + public String getTemplateAccountUID() { + if (mTemplateAccountUID == null) + return mTemplateAccountUID = generateUID(); + else + return mTemplateAccountUID; + } + + /** + * Set the template account GUID + * @param templateAccountUID String GUID of template account + */ + public void setTemplateAccountUID(String templateAccountUID) { + this.mTemplateAccountUID = templateAccountUID; } /** @@ -354,13 +383,7 @@ public void setAutoCreate(boolean autoCreate) { * @return String description of repeat schedule */ public String getRepeatString(){ - String dayOfWeek = new SimpleDateFormat("EEEE", Locale.US).format(new Date(mStartDate)); - PeriodType periodType = getPeriodType(); - StringBuilder ruleBuilder = new StringBuilder(periodType.getFrequencyRepeatString()); - - if (periodType == PeriodType.WEEK) { - ruleBuilder.append(" on ").append(dayOfWeek); - } + StringBuilder ruleBuilder = new StringBuilder(mRecurrence.getRepeatString()); if (mEndDate > 0){ ruleBuilder.append(", "); @@ -380,23 +403,8 @@ public String getRepeatString(){ */ public String getRuleString(){ String separator = ";"; - PeriodType periodType = getPeriodType(); - - StringBuilder ruleBuilder = new StringBuilder(); - -// ======================================================================= - //This section complies with the formal rules, but the betterpickers library doesn't like/need it - -// SimpleDateFormat startDateFormat = new SimpleDateFormat("'TZID'=zzzz':'yyyyMMdd'T'HHmmss", Locale.US); -// ruleBuilder.append("DTSTART;"); -// ruleBuilder.append(startDateFormat.format(new Date(mStartDate))); -// ruleBuilder.append("\n"); -// ruleBuilder.append("RRULE:"); -// ======================================================================== - ruleBuilder.append("FREQ=").append(periodType.getFrequencyDescription()).append(separator); - ruleBuilder.append("INTERVAL=").append(periodType.getMultiplier()).append(separator); - ruleBuilder.append(periodType.getByParts(mStartDate)).append(separator); + StringBuilder ruleBuilder = new StringBuilder(mRecurrence.getRuleString()); if (mEndDate > 0){ SimpleDateFormat df = new SimpleDateFormat("yyyyMMdd'T'HHmmss'Z'", Locale.US); @@ -409,16 +417,49 @@ public String getRuleString(){ return ruleBuilder.toString(); } + /** + * Return GUID of recurrence pattern for this scheduled action + * @return {@link Recurrence} object + */ + public Recurrence getRecurrence() { + return mRecurrence; + } + + /** + * Sets the recurrence pattern of this scheduled action + *

This also sets the start period of the recurrence object, if there is one

+ * @param recurrence {@link Recurrence} object + */ + public void setRecurrence(Recurrence recurrence) { + this.mRecurrence = recurrence; + //if we were parsing XML and parsed the start and end date from the scheduled action first, + //then use those over the values which might be gotten from the recurrence + if (mStartDate > 0){ + mRecurrence.setPeriodStart(new Timestamp(mStartDate)); + } else { + mStartDate = mRecurrence.getPeriodStart().getTime(); + } + + if (mEndDate > 0){ + mRecurrence.setPeriodEnd(new Timestamp(mEndDate)); + } else if (mRecurrence.getPeriodEnd() != null){ + mEndDate = mRecurrence.getPeriodEnd().getTime(); + } + } + /** * Creates a ScheduledAction from a Transaction and a period * @param transaction Transaction to be scheduled * @param period Period in milliseconds since Epoch * @return Scheduled Action + * @deprecated Used for parsing legacy backup files. Use {@link Recurrence} instead */ + @Deprecated public static ScheduledAction parseScheduledAction(Transaction transaction, long period){ ScheduledAction scheduledAction = new ScheduledAction(ActionType.TRANSACTION); scheduledAction.mActionUID = transaction.getUID(); - scheduledAction.mPeriod = period; + Recurrence recurrence = new Recurrence(PeriodType.parse(period)); + scheduledAction.setRecurrence(recurrence); return scheduledAction; } diff --git a/app/src/main/java/org/gnucash/android/model/Split.java b/app/src/main/java/org/gnucash/android/model/Split.java index 5e19936c5..94eaf370c 100644 --- a/app/src/main/java/org/gnucash/android/model/Split.java +++ b/app/src/main/java/org/gnucash/android/model/Split.java @@ -3,7 +3,9 @@ import android.support.annotation.NonNull; -import org.gnucash.android.db.AccountsDbAdapter; +import org.gnucash.android.db.adapter.AccountsDbAdapter; + +import java.sql.Timestamp; /** * A split amount in a transaction. @@ -16,6 +18,23 @@ * @author Ngewi Fet */ public class Split extends BaseModel{ + + /** + * Flag indicating that the split has been reconciled + */ + public static final char FLAG_RECONCILED = 'y'; + + /** + * Flag indicating that the split has not been reconciled + */ + public static final char FLAG_NOT_RECONCILED = 'n'; + + /** + * Flag indicating that the split has been cleared, but not reconciled + */ + public static final char FLAG_CLEARED = 'c'; + + /** * Amount value of this split which is in the currency of the transaction */ @@ -46,6 +65,13 @@ public class Split extends BaseModel{ */ private String mMemo; + private char mReconcileState = FLAG_NOT_RECONCILED; + + /** + * Database required non-null field + */ + private Timestamp mReconcileDate = new Timestamp(System.currentTimeMillis()); + /** * Initialize split with a value amount and account * @param value Money value amount of this split @@ -209,7 +235,7 @@ public void setMemo(String memo) { * @see TransactionType#invert() */ public Split createPair(String accountUID){ - Split pair = new Split(mValue.absolute(), accountUID); + Split pair = new Split(mValue.abs(), accountUID); pair.setType(mSplitType.invert()); pair.setMemo(mMemo); pair.setTransactionUID(mTransactionUID); @@ -239,7 +265,7 @@ protected Split clone() throws CloneNotSupportedException { * @return whether the two splits are a pair */ public boolean isPairOf(Split other) { - return mValue.absolute().equals(other.mValue.absolute()) + return mValue.abs().equals(other.mValue.abs()) && mSplitType.invert().equals(other.mSplitType); } @@ -272,7 +298,7 @@ public Money getFormattedQuantity(){ */ public static Money getFormattedAmount(Money amount, String accountUID, TransactionType splitType){ boolean isDebitAccount = AccountsDbAdapter.getInstance().getAccountType(accountUID).hasDebitNormalBalance(); - Money absAmount = amount.absolute(); + Money absAmount = amount.abs(); boolean isDebitSplit = splitType == TransactionType.DEBIT; if (isDebitAccount) { @@ -290,6 +316,63 @@ public static Money getFormattedAmount(Money amount, String accountUID, Transact } } + /** + * Return the reconciled state of this split + *

+ * The reconciled state is one of the following values: + *

    + *
  • y: means this split has been reconciled
  • + *
  • n: means this split is not reconciled
  • + *
  • c: means split has been cleared, but not reconciled
  • + *
+ *


+ * You can check the return value against the reconciled flags {@link #FLAG_RECONCILED}, {@link #FLAG_NOT_RECONCILED}, {@link #FLAG_CLEARED} + * @return Character showing reconciled state + */ + public char getReconcileState() { + return mReconcileState; + } + + /** + * Check if this split is reconciled + * @return {@code true} if the split is reconciled, {@code false} otherwise + */ + public boolean isReconciled(){ + return mReconcileState == FLAG_RECONCILED; + } + + /** + * Set reconciled state of this split. + *

+ * The reconciled state is one of the following values: + *

    + *
  • y: means this split has been reconciled
  • + *
  • n: means this split is not reconciled
  • + *
  • c: means split has been cleared, but not reconciled
  • + *
+ *

+ * @param reconcileState One of the following flags {@link #FLAG_RECONCILED}, {@link #FLAG_NOT_RECONCILED}, {@link #FLAG_CLEARED} + */ + public void setReconcileState(char reconcileState) { + this.mReconcileState = reconcileState; + } + + /** + * Return the date of reconciliation + * @return Timestamp + */ + public Timestamp getReconcileDate() { + return mReconcileDate; + } + + /** + * Set reconciliation date for this split + * @param reconcileDate Timestamp of reconciliation + */ + public void setReconcileDate(Timestamp reconcileDate) { + this.mReconcileDate = reconcileDate; + } + @Override public String toString() { return mSplitType.name() + " of " + mValue.toString() + " in account: " + mAccountUID; @@ -305,7 +388,7 @@ public String toString() { */ public String toCsv(){ String sep = ";"; - + //TODO: add reconciled state and date String splitString = getUID() + sep + mValue.getNumerator() + sep + mValue.getDenominator() + sep + mValue.getCurrency().getCurrencyCode() + sep + mQuantity.getNumerator() + sep + mQuantity.getDenominator() + sep + mQuantity.getCurrency().getCurrencyCode() + sep + mTransactionUID + sep + mAccountUID + sep + mSplitType.name(); @@ -325,6 +408,7 @@ public String toCsv(){ * @return Split instance parsed from the string */ public static Split parseSplit(String splitCsvString) { + //TODO: parse reconciled state and date String[] tokens = splitCsvString.split(";"); if (tokens.length < 8) { //old format splits Money amount = new Money(tokens[0], tokens[1]); diff --git a/app/src/main/java/org/gnucash/android/model/Transaction.java b/app/src/main/java/org/gnucash/android/model/Transaction.java index fd7528e45..e3ee904ba 100644 --- a/app/src/main/java/org/gnucash/android/model/Transaction.java +++ b/app/src/main/java/org/gnucash/android/model/Transaction.java @@ -19,7 +19,7 @@ import android.content.Intent; import org.gnucash.android.BuildConfig; -import org.gnucash.android.db.AccountsDbAdapter; +import org.gnucash.android.db.adapter.AccountsDbAdapter; import org.gnucash.android.export.ofx.OfxHelper; import org.gnucash.android.model.Account.OfxAccountType; import org.w3c.dom.Document; @@ -262,7 +262,7 @@ public Money getImbalance(){ // so imbalance split should not be generated for them return Money.createZeroInstance(mCurrencyCode); } - Money amount = split.getValue().absolute(); + Money amount = split.getValue().abs(); if (split.getType() == TransactionType.DEBIT) imbalance = imbalance.subtract(amount); else @@ -293,9 +293,9 @@ public static Money computeBalance(String accountUID, List splitList) { continue; Money absAmount; if (split.getValue().getCurrency() == accountCurrency){ - absAmount = split.getValue().absolute(); + absAmount = split.getValue().abs(); } else { //if this split belongs to the account, then either its value or quantity is in the account currency - absAmount = split.getQuantity().absolute(); + absAmount = split.getQuantity().abs(); } boolean isDebitSplit = split.getType() == TransactionType.DEBIT; if (isDebitAccount) { diff --git a/app/src/main/java/org/gnucash/android/receivers/AccountCreator.java b/app/src/main/java/org/gnucash/android/receivers/AccountCreator.java index 100ae5891..7c406111c 100644 --- a/app/src/main/java/org/gnucash/android/receivers/AccountCreator.java +++ b/app/src/main/java/org/gnucash/android/receivers/AccountCreator.java @@ -22,7 +22,7 @@ import android.os.Bundle; import android.util.Log; -import org.gnucash.android.db.AccountsDbAdapter; +import org.gnucash.android.db.adapter.AccountsDbAdapter; import org.gnucash.android.model.Account; import org.gnucash.android.model.Commodity; diff --git a/app/src/main/java/org/gnucash/android/receivers/TransactionRecorder.java b/app/src/main/java/org/gnucash/android/receivers/TransactionRecorder.java index 469b0a997..aa0659cc4 100644 --- a/app/src/main/java/org/gnucash/android/receivers/TransactionRecorder.java +++ b/app/src/main/java/org/gnucash/android/receivers/TransactionRecorder.java @@ -24,8 +24,9 @@ import com.crashlytics.android.Crashlytics; -import org.gnucash.android.db.CommoditiesDbAdapter; -import org.gnucash.android.db.TransactionsDbAdapter; +import org.gnucash.android.db.adapter.TransactionsDbAdapter; +import org.gnucash.android.db.adapter.CommoditiesDbAdapter; + import org.gnucash.android.model.Account; import org.gnucash.android.model.Commodity; import org.gnucash.android.model.Money; @@ -78,7 +79,7 @@ public void onReceive(Context context, Intent intent) { Commodity commodity = CommoditiesDbAdapter.getInstance().getCommodity(currencyCode); amountBigDecimal = amountBigDecimal.setScale(commodity.getSmallestFractionDigits(), BigDecimal.ROUND_HALF_EVEN).round(MathContext.DECIMAL128); Money amount = new Money(amountBigDecimal, Commodity.getInstance(currencyCode)); - Split split = new Split(amount.absolute(), accountUID); + Split split = new Split(amount.abs(), accountUID); split.setType(type); transaction.addSplit(split); diff --git a/app/src/main/java/org/gnucash/android/service/SchedulerService.java b/app/src/main/java/org/gnucash/android/service/SchedulerService.java index 232f48c6f..9456100de 100644 --- a/app/src/main/java/org/gnucash/android/service/SchedulerService.java +++ b/app/src/main/java/org/gnucash/android/service/SchedulerService.java @@ -27,8 +27,8 @@ import org.gnucash.android.app.GnuCashApplication; import org.gnucash.android.db.DatabaseSchema; -import org.gnucash.android.db.ScheduledActionDbAdapter; -import org.gnucash.android.db.TransactionsDbAdapter; +import org.gnucash.android.db.adapter.ScheduledActionDbAdapter; +import org.gnucash.android.db.adapter.TransactionsDbAdapter; import org.gnucash.android.export.ExportAsyncTask; import org.gnucash.android.export.ExportParams; import org.gnucash.android.model.ScheduledAction; @@ -69,20 +69,20 @@ protected void onHandleIntent(Intent intent) { List scheduledActions = scheduledActionDbAdapter.getAllEnabledScheduledActions(); for (ScheduledAction scheduledAction : scheduledActions) { - long lastRun = scheduledAction.getLastRun(); - long period = scheduledAction.getPeriod(); long endTime = scheduledAction.getEndTime(); - - long now = System.currentTimeMillis(); - - if (((endTime > 0 && now < endTime) //if and endTime is set and we did not reach it yet - || (scheduledAction.getExecutionCount() < scheduledAction.getTotalFrequency()) //or the number of scheduled runs - || (endTime == 0 && scheduledAction.getTotalFrequency() == 0)) //or the action is to run forever - && ((lastRun + period) <= now) //one period has passed since last execution - && scheduledAction.getStartTime() <= now - && scheduledAction.isEnabled()){ //the start time has arrived - executeScheduledEvent(scheduledAction); - } + long now = System.currentTimeMillis(); + long nextRunTime; + do { //loop so that we can add transactions which were missed while device was off + nextRunTime = scheduledAction.computeNextRunTime(); + if (((endTime > 0 && now < endTime) //if and endTime is set and we did not reach it yet + || (scheduledAction.getExecutionCount() < scheduledAction.getTotalFrequency()) //or the number of scheduled runs + || (endTime == 0 && scheduledAction.getTotalFrequency() == 0)) //or the action is to run forever + && (nextRunTime <= now) //one period has passed since last execution + && scheduledAction.getStartTime() <= now + && scheduledAction.isEnabled()) { //the start time has arrived + executeScheduledEvent(scheduledAction); + } + } while (nextRunTime <= now && scheduledAction.getActionType() == ScheduledAction.ActionType.TRANSACTION); } Log.i(LOG_TAG, "Completed service @ " + SystemClock.elapsedRealtime()); @@ -95,6 +95,7 @@ protected void onHandleIntent(Intent intent) { * @param scheduledAction ScheduledEvent to be executed */ private void executeScheduledEvent(ScheduledAction scheduledAction){ + Log.i(LOG_TAG, "Executing scheduled action: " + scheduledAction.toString()); switch (scheduledAction.getActionType()){ case TRANSACTION: String eventUID = scheduledAction.getActionUID(); @@ -104,12 +105,7 @@ private void executeScheduledEvent(ScheduledAction scheduledAction){ //we may be executing scheduled action significantly after scheduled time (depending on when Android fires the alarm) //so compute the actual transaction time from pre-known values - long transactionTime; //default - if (scheduledAction.getLastRun() > 0){ - transactionTime = scheduledAction.getLastRun() + scheduledAction.getPeriod(); - } else { - transactionTime = scheduledAction.getStartTime() + scheduledAction.getPeriod(); - } + long transactionTime = scheduledAction.computeNextRunTime(); //default recurringTrxn.setTime(transactionTime); recurringTrxn.setCreatedTimestamp(new Timestamp(transactionTime)); transactionsDbAdapter.addRecord(recurringTrxn); @@ -129,10 +125,15 @@ private void executeScheduledEvent(ScheduledAction scheduledAction){ break; } + long lastRun = scheduledAction.computeNextRunTime(); + int executionCount = scheduledAction.getExecutionCount() + 1; //update the last run time and execution count ContentValues contentValues = new ContentValues(); - contentValues.put(DatabaseSchema.ScheduledActionEntry.COLUMN_LAST_RUN, System.currentTimeMillis()); - contentValues.put(DatabaseSchema.ScheduledActionEntry.COLUMN_EXECUTION_COUNT, scheduledAction.getExecutionCount()+1); + contentValues.put(DatabaseSchema.ScheduledActionEntry.COLUMN_LAST_RUN, lastRun); + contentValues.put(DatabaseSchema.ScheduledActionEntry.COLUMN_EXECUTION_COUNT, executionCount); ScheduledActionDbAdapter.getInstance().updateRecord(scheduledAction.getUID(), contentValues); + + scheduledAction.setLastRun(lastRun); + scheduledAction.setExecutionCount(executionCount); } } diff --git a/app/src/main/java/org/gnucash/android/ui/account/AccountFormFragment.java b/app/src/main/java/org/gnucash/android/ui/account/AccountFormFragment.java index b83b0288f..9c98be705 100644 --- a/app/src/main/java/org/gnucash/android/ui/account/AccountFormFragment.java +++ b/app/src/main/java/org/gnucash/android/ui/account/AccountFormFragment.java @@ -54,8 +54,8 @@ import android.widget.Spinner; import org.gnucash.android.R; -import org.gnucash.android.db.AccountsDbAdapter; -import org.gnucash.android.db.CommoditiesDbAdapter; +import org.gnucash.android.db.adapter.AccountsDbAdapter; +import org.gnucash.android.db.adapter.CommoditiesDbAdapter; import org.gnucash.android.db.DatabaseSchema; import org.gnucash.android.model.Account; import org.gnucash.android.model.AccountType; diff --git a/app/src/main/java/org/gnucash/android/ui/account/AccountsActivity.java b/app/src/main/java/org/gnucash/android/ui/account/AccountsActivity.java index 494d817ba..126094a1d 100644 --- a/app/src/main/java/org/gnucash/android/ui/account/AccountsActivity.java +++ b/app/src/main/java/org/gnucash/android/ui/account/AccountsActivity.java @@ -40,11 +40,11 @@ import android.support.design.widget.Snackbar; import android.support.design.widget.TabLayout; import android.support.v4.app.Fragment; -import android.support.v4.app.FragmentActivity; import android.support.v4.app.FragmentManager; import android.support.v4.app.FragmentPagerAdapter; import android.support.v4.view.PagerAdapter; import android.support.v4.view.ViewPager; +import android.support.v7.app.AppCompatActivity; import android.support.v7.widget.Toolbar; import android.util.Log; import android.util.SparseArray; @@ -61,8 +61,8 @@ import org.gnucash.android.BuildConfig; import org.gnucash.android.R; import org.gnucash.android.app.GnuCashApplication; -import org.gnucash.android.db.AccountsDbAdapter; import org.gnucash.android.db.DatabaseSchema; +import org.gnucash.android.db.adapter.AccountsDbAdapter; import org.gnucash.android.export.xml.GncXmlExporter; import org.gnucash.android.importer.ImportAsyncTask; import org.gnucash.android.ui.common.BaseDrawerActivity; @@ -444,7 +444,7 @@ public void onClick(DialogInterface dialog, int which) { /** * Displays the dialog for exporting transactions */ - public static void openExportFragment(FragmentActivity activity) { + public static void openExportFragment(AppCompatActivity activity) { Intent intent = new Intent(activity, FormActivity.class); intent.putExtra(UxArgument.FORM_TYPE, FormActivity.FormType.EXPORT.name()); activity.startActivity(intent); diff --git a/app/src/main/java/org/gnucash/android/ui/account/AccountsListFragment.java b/app/src/main/java/org/gnucash/android/ui/account/AccountsListFragment.java index a1623f233..4c6128bce 100644 --- a/app/src/main/java/org/gnucash/android/ui/account/AccountsListFragment.java +++ b/app/src/main/java/org/gnucash/android/ui/account/AccountsListFragment.java @@ -46,21 +46,28 @@ import android.view.View; import android.view.ViewGroup; import android.widget.ImageView; +import android.widget.ProgressBar; import android.widget.TextView; import org.gnucash.android.R; import org.gnucash.android.app.GnuCashApplication; -import org.gnucash.android.db.AccountsDbAdapter; + import org.gnucash.android.db.DatabaseCursorLoader; import org.gnucash.android.db.DatabaseSchema; +import org.gnucash.android.db.adapter.AccountsDbAdapter; +import org.gnucash.android.db.adapter.BudgetsDbAdapter; import org.gnucash.android.model.Account; +import org.gnucash.android.model.Budget; +import org.gnucash.android.model.Money; import org.gnucash.android.ui.common.FormActivity; import org.gnucash.android.ui.common.UxArgument; import org.gnucash.android.ui.util.AccountBalanceTask; import org.gnucash.android.ui.util.CursorRecyclerAdapter; -import org.gnucash.android.ui.util.widget.EmptyRecyclerView; import org.gnucash.android.ui.util.OnAccountClickedListener; import org.gnucash.android.ui.util.Refreshable; +import org.gnucash.android.ui.util.widget.EmptyRecyclerView; + +import java.util.List; import butterknife.Bind; import butterknife.ButterKnife; @@ -468,7 +475,7 @@ public void onBindViewHolderCursor(final AccountViewHolder holder, final Cursor // add a summary of transactions to the account view if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) { - // Make sure the balance task is truely multithread + // Make sure the balance task is truly multithread new AccountBalanceTask(holder.accountBalance).executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, accountUID); } else { new AccountBalanceTask(holder.accountBalance).execute(accountUID); @@ -494,6 +501,20 @@ public void onClick(View v) { }); } + List budgets = BudgetsDbAdapter.getInstance().getAccountBudgets(accountUID); + //TODO: include fetch only active budgets + if (budgets.size() == 1){ + Budget budget = budgets.get(0); + Money balance = mAccountsDbAdapter.getAccountBalance(accountUID, budget.getStartofCurrentPeriod(), budget.getEndOfCurrentPeriod()); + double budgetProgress = balance.divide(budget.getAmount(accountUID)).asBigDecimal().doubleValue() * 100; + + holder.budgetIndicator.setVisibility(View.VISIBLE); + holder.budgetIndicator.setProgress((int) budgetProgress); + } else { + holder.budgetIndicator.setVisibility(View.GONE); + } + + if (mAccountsDbAdapter.isFavoriteAccount(accountUID)){ holder.favoriteStatus.setImageResource(R.drawable.ic_star_black_24dp); } else { @@ -534,6 +555,7 @@ class AccountViewHolder extends RecyclerView.ViewHolder implements PopupMenu.OnM @Bind(R.id.favorite_status) ImageView favoriteStatus; @Bind(R.id.options_menu) ImageView optionsMenu; @Bind(R.id.account_color_strip) View colorStripView; + @Bind(R.id.budget_indicator) ProgressBar budgetIndicator; long accoundId; public AccountViewHolder(View itemView) { diff --git a/app/src/main/java/org/gnucash/android/ui/account/DeleteAccountDialogFragment.java b/app/src/main/java/org/gnucash/android/ui/account/DeleteAccountDialogFragment.java index c06d02823..97e11f3fb 100644 --- a/app/src/main/java/org/gnucash/android/ui/account/DeleteAccountDialogFragment.java +++ b/app/src/main/java/org/gnucash/android/ui/account/DeleteAccountDialogFragment.java @@ -32,10 +32,10 @@ import org.gnucash.android.R; import org.gnucash.android.app.GnuCashApplication; -import org.gnucash.android.db.AccountsDbAdapter; +import org.gnucash.android.db.adapter.AccountsDbAdapter; import org.gnucash.android.db.DatabaseSchema; -import org.gnucash.android.db.SplitsDbAdapter; -import org.gnucash.android.db.TransactionsDbAdapter; +import org.gnucash.android.db.adapter.SplitsDbAdapter; +import org.gnucash.android.db.adapter.TransactionsDbAdapter; import org.gnucash.android.model.AccountType; import org.gnucash.android.ui.util.Refreshable; import org.gnucash.android.ui.homescreen.WidgetConfigurationActivity; diff --git a/app/src/main/java/org/gnucash/android/ui/budget/BudgetDetailFragment.java b/app/src/main/java/org/gnucash/android/ui/budget/BudgetDetailFragment.java new file mode 100644 index 000000000..d103ab712 --- /dev/null +++ b/app/src/main/java/org/gnucash/android/ui/budget/BudgetDetailFragment.java @@ -0,0 +1,307 @@ +/* + * Copyright (c) 2015 Ngewi Fet + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.gnucash.android.ui.budget; + +import android.app.Activity; +import android.content.Intent; +import android.content.res.Configuration; +import android.graphics.Color; +import android.os.Bundle; +import android.support.annotation.Nullable; +import android.support.v4.app.Fragment; +import android.support.v7.app.AppCompatActivity; +import android.support.v7.widget.GridLayoutManager; +import android.support.v7.widget.LinearLayoutManager; +import android.support.v7.widget.RecyclerView; +import android.view.LayoutInflater; +import android.view.Menu; +import android.view.MenuInflater; +import android.view.MenuItem; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ProgressBar; +import android.widget.TextView; + +import com.github.mikephil.charting.charts.BarChart; +import com.github.mikephil.charting.components.LimitLine; +import com.github.mikephil.charting.data.BarData; +import com.github.mikephil.charting.data.BarDataSet; +import com.github.mikephil.charting.data.BarEntry; + +import org.gnucash.android.R; +import org.gnucash.android.db.DatabaseSchema; +import org.gnucash.android.db.adapter.AccountsDbAdapter; +import org.gnucash.android.db.adapter.BudgetsDbAdapter; +import org.gnucash.android.model.Budget; +import org.gnucash.android.model.BudgetAmount; +import org.gnucash.android.model.Money; +import org.gnucash.android.ui.common.FormActivity; +import org.gnucash.android.ui.common.UxArgument; +import org.gnucash.android.ui.transaction.TransactionsActivity; +import org.gnucash.android.ui.util.Refreshable; +import org.gnucash.android.ui.util.widget.EmptyRecyclerView; + +import java.math.BigDecimal; +import java.math.RoundingMode; +import java.util.ArrayList; +import java.util.List; + +import butterknife.Bind; +import butterknife.ButterKnife; + +/** + * Fragment for displaying budget details + */ +public class BudgetDetailFragment extends Fragment implements Refreshable { + @Bind(R.id.primary_text) TextView mBudgetNameTextView; + @Bind(R.id.secondary_text) TextView mBudgetDescriptionTextView; + @Bind(R.id.budget_recurrence) TextView mBudgetRecurrence; + @Bind(R.id.budget_amount_recycler) EmptyRecyclerView mRecyclerView; + + private String mBudgetUID; + private BudgetsDbAdapter mBudgetsDbAdapter; + + public static BudgetDetailFragment newInstance(String budgetUID){ + BudgetDetailFragment fragment = new BudgetDetailFragment(); + Bundle args = new Bundle(); + args.putString(UxArgument.BUDGET_UID, budgetUID); + fragment.setArguments(args); + return fragment; + } + + @Nullable + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { + View view = inflater.inflate(R.layout.fragment_budget_detail, container, false); + ButterKnife.bind(this, view); + mBudgetDescriptionTextView.setMaxLines(3); + + mRecyclerView.setHasFixedSize(true); + + if (getResources().getConfiguration().orientation == Configuration.ORIENTATION_LANDSCAPE) { + GridLayoutManager gridLayoutManager = new GridLayoutManager(getActivity(), 2); + mRecyclerView.setLayoutManager(gridLayoutManager); + } else { + LinearLayoutManager mLayoutManager = new LinearLayoutManager(getActivity()); + mRecyclerView.setLayoutManager(mLayoutManager); + } + return view; + } + + + @Override + public void onActivityCreated(@Nullable Bundle savedInstanceState) { + super.onActivityCreated(savedInstanceState); + + mBudgetsDbAdapter = BudgetsDbAdapter.getInstance(); + mBudgetUID = getArguments().getString(UxArgument.BUDGET_UID); + bindViews(); + + setHasOptionsMenu(true); + } + + private void bindViews(){ + Budget budget = mBudgetsDbAdapter.getRecord(mBudgetUID); + mBudgetNameTextView.setText(budget.getName()); + + String description = budget.getDescription(); + if (description != null && !description.isEmpty()) + mBudgetDescriptionTextView.setText(description); + else { + mBudgetDescriptionTextView.setVisibility(View.GONE); + } + mBudgetRecurrence.setText(budget.getRecurrence().getRepeatString()); + + mRecyclerView.setAdapter(new BudgetAmountAdapter()); + } + + @Override + public void onResume() { + super.onResume(); + refresh(); + + View view = getActivity().findViewById(R.id.fab_create_budget); + if (view != null){ + view.setVisibility(View.GONE); + } + } + + @Override + public void refresh() { + bindViews(); + String budgetName = mBudgetsDbAdapter.getAttribute(mBudgetUID, DatabaseSchema.BudgetEntry.COLUMN_NAME); + ((AppCompatActivity) getActivity()).getSupportActionBar().setTitle(budgetName); + } + + @Override + public void refresh(String budgetUID) { + mBudgetUID = budgetUID; + refresh(); + } + + @Override + public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { + inflater.inflate(R.menu.budget_actions, menu); + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + switch (item.getItemId()){ + case R.id.menu_edit_budget: + Intent addAccountIntent = new Intent(getActivity(), FormActivity.class); + addAccountIntent.setAction(Intent.ACTION_INSERT_OR_EDIT); + addAccountIntent.putExtra(UxArgument.FORM_TYPE, FormActivity.FormType.BUDGET.name()); + addAccountIntent.putExtra(UxArgument.BUDGET_UID, mBudgetUID); + startActivityForResult(addAccountIntent, 0x11); + return true; + + default: + return false; + } + } + + @Override + public void onActivityResult(int requestCode, int resultCode, Intent data) { + if (resultCode == Activity.RESULT_OK){ + refresh(); + } + } + + + public class BudgetAmountAdapter extends RecyclerView.Adapter{ + private List mBudgetAmounts; + private Budget mBudget; + + public BudgetAmountAdapter(){ + mBudget = mBudgetsDbAdapter.getRecord(mBudgetUID); + mBudgetAmounts = mBudget.getCompactedBudgetAmounts(); + } + + @Override + public BudgetAmountViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { + View view = LayoutInflater.from(getActivity()).inflate(R.layout.cardview_budget_amount, parent, false); + return new BudgetAmountViewHolder(view); + } + + @Override + public void onBindViewHolder(BudgetAmountViewHolder holder, final int position) { + BudgetAmount budgetAmount = mBudgetAmounts.get(position); + Money projectedAmount = budgetAmount.getAmount(); + AccountsDbAdapter accountsDbAdapter = AccountsDbAdapter.getInstance(); + + holder.budgetAccount.setText(accountsDbAdapter.getAccountFullName(budgetAmount.getAccountUID())); + holder.budgetAmount.setText(projectedAmount.formattedString()); + + Money spentAmount = accountsDbAdapter.getAccountBalance(budgetAmount.getAccountUID(), + mBudget.getStartofCurrentPeriod(), mBudget.getEndOfCurrentPeriod()); + + holder.budgetSpent.setText(spentAmount.abs().formattedString()); + holder.budgetLeft.setText(projectedAmount.subtract(spentAmount.abs()).formattedString()); + + double budgetProgress = 0; + if (projectedAmount.asDouble() != 0){ + budgetProgress = spentAmount.asBigDecimal().divide(projectedAmount.asBigDecimal(), + spentAmount.getCurrency().getDefaultFractionDigits(), RoundingMode.HALF_EVEN) + .doubleValue(); + } + + holder.budgetIndicator.setProgress((int) (budgetProgress * 100)); + holder.budgetSpent.setTextColor(BudgetsActivity.getBudgetProgressColor(1 - budgetProgress)); + holder.budgetLeft.setTextColor(BudgetsActivity.getBudgetProgressColor(1 - budgetProgress)); + + generateChartData(holder.budgetChart, budgetAmount); + + holder.itemView.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + Intent intent = new Intent(getActivity(), TransactionsActivity.class); + intent.putExtra(UxArgument.SELECTED_ACCOUNT_UID, mBudgetAmounts.get(position).getAccountUID()); + startActivityForResult(intent, 0x10); + } + }); + } + + /** + * Generate the chart data for the chart + * @param barChart View where to display the chart + * @param budgetAmount BudgetAmount to visualize + */ + public void generateChartData(BarChart barChart, BudgetAmount budgetAmount) { + // FIXME: 25.10.15 chart is broken + + AccountsDbAdapter accountsDbAdapter = AccountsDbAdapter.getInstance(); + + List barEntries = new ArrayList<>(); + List xVals = new ArrayList<>(); + + //todo: refactor getNumberOfPeriods into budget + int budgetPeriods = (int) mBudget.getNumberOfPeriods(); + budgetPeriods = budgetPeriods == 0 ? 12 : budgetPeriods; + int periods = mBudget.getRecurrence().getNumberOfPeriods(budgetPeriods); + + for (int periodNum = 1; periodNum <= periods; periodNum++) { + BigDecimal amount = accountsDbAdapter.getAccountBalance(budgetAmount.getAccountUID(), + mBudget.getStartOfPeriod(periodNum), mBudget.getEndOfPeriod(periodNum)) + .asBigDecimal(); + + if (amount.equals(BigDecimal.ZERO)) + continue; + + barEntries.add(new BarEntry(amount.floatValue(), periodNum)); + xVals.add(mBudget.getRecurrence().getTextOfCurrentPeriod(periodNum)); + } + + String label = accountsDbAdapter.getAccountName(budgetAmount.getAccountUID()); + BarDataSet barDataSet = new BarDataSet(barEntries, label); + + BarData barData = new BarData(xVals, barDataSet); + LimitLine limitLine = new LimitLine(budgetAmount.getAmount().asBigDecimal().floatValue()); + limitLine.setLineWidth(2f); + limitLine.setLineColor(Color.RED); + + + barChart.setData(barData); + barChart.getAxisLeft().addLimitLine(limitLine); + BigDecimal maxValue = budgetAmount.getAmount().add(budgetAmount.getAmount().multiply(new BigDecimal("0.2"))).asBigDecimal(); + barChart.getAxisLeft().setAxisMaxValue(maxValue.floatValue()); + barChart.animateX(1000); + barChart.setAutoScaleMinMaxEnabled(true); + barChart.setDrawValueAboveBar(true); + barChart.invalidate(); + } + + @Override + public int getItemCount() { + return mBudgetAmounts.size(); + } + + class BudgetAmountViewHolder extends RecyclerView.ViewHolder { + @Bind(R.id.budget_account) TextView budgetAccount; + @Bind(R.id.budget_amount) TextView budgetAmount; + @Bind(R.id.budget_spent) TextView budgetSpent; + @Bind(R.id.budget_left) TextView budgetLeft; + @Bind(R.id.budget_indicator) ProgressBar budgetIndicator; + @Bind(R.id.budget_chart) BarChart budgetChart; + + public BudgetAmountViewHolder(View itemView) { + super(itemView); + ButterKnife.bind(this, itemView); + } + + } + } +} diff --git a/app/src/main/java/org/gnucash/android/ui/budget/BudgetFormFragment.java b/app/src/main/java/org/gnucash/android/ui/budget/BudgetFormFragment.java new file mode 100644 index 000000000..4ec87947d --- /dev/null +++ b/app/src/main/java/org/gnucash/android/ui/budget/BudgetFormFragment.java @@ -0,0 +1,362 @@ +/* + * Copyright (c) 2015 Ngewi Fet + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.gnucash.android.ui.budget; + +import android.database.Cursor; +import android.inputmethodservice.KeyboardView; +import android.os.Bundle; +import android.support.annotation.Nullable; +import android.support.design.widget.TextInputLayout; +import android.support.v4.app.Fragment; +import android.support.v7.app.ActionBar; +import android.support.v7.app.AppCompatActivity; +import android.view.LayoutInflater; +import android.view.Menu; +import android.view.MenuInflater; +import android.view.MenuItem; +import android.view.View; +import android.view.ViewGroup; +import android.widget.AdapterView; +import android.widget.Button; +import android.widget.EditText; +import android.widget.ImageView; +import android.widget.Spinner; +import android.widget.TableLayout; +import android.widget.TextView; +import android.widget.Toast; + +import com.codetroopers.betterpickers.recurrencepicker.EventRecurrence; +import com.codetroopers.betterpickers.recurrencepicker.EventRecurrenceFormatter; +import com.codetroopers.betterpickers.recurrencepicker.RecurrencePickerDialogFragment; + +import org.gnucash.android.R; +import org.gnucash.android.db.DatabaseSchema; +import org.gnucash.android.db.adapter.AccountsDbAdapter; +import org.gnucash.android.db.adapter.BudgetsDbAdapter; +import org.gnucash.android.model.Budget; +import org.gnucash.android.model.BudgetAmount; +import org.gnucash.android.model.Commodity; +import org.gnucash.android.model.Money; +import org.gnucash.android.model.ScheduledAction; +import org.gnucash.android.ui.common.UxArgument; +import org.gnucash.android.ui.util.RecurrenceParser; +import org.gnucash.android.ui.util.RecurrenceViewClickListener; +import org.gnucash.android.ui.util.widget.CalculatorEditText; +import org.gnucash.android.util.QualifiedAccountNameCursorAdapter; + +import java.math.BigDecimal; +import java.util.ArrayList; +import java.util.Currency; +import java.util.List; + +import butterknife.Bind; +import butterknife.ButterKnife; + +/** + * Fragment for creating or editing Budgets + */ +public class BudgetFormFragment extends Fragment implements RecurrencePickerDialogFragment.OnRecurrenceSetListener { + + @Bind(R.id.input_budget_name) EditText mBudgetNameInput; + @Bind(R.id.input_description) EditText mDescriptionInput; + @Bind(R.id.input_recurrence) TextView mRecurrenceInput; + @Bind(R.id.name_text_input_layout) TextInputLayout mNameTextInputLayout; + @Bind(R.id.calculator_keyboard) KeyboardView mKeyboardView; + @Bind(R.id.budget_amount_table_layout) TableLayout mBudgetAmountTableLayout; + @Bind(R.id.btn_add_budget_amount) Button mAddBudgetAmount; + + EventRecurrence mEventRecurrence = new EventRecurrence(); + String mRecurrenceRule; + + private Cursor mAccountCursor; + private AccountsDbAdapter mAccountsDbAdapter; + private BudgetsDbAdapter mBudgetsDbAdapter; + + private Budget mBudget; + private QualifiedAccountNameCursorAdapter mAccountCursorAdapter; + + private List mBudgetAmountViews = new ArrayList<>(); + + @Nullable + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { + View view = inflater.inflate(R.layout.fragment_budget_form, container, false); + ButterKnife.bind(this, view); + + setupAccountSpinnerAdapter(); + mRecurrenceInput.setOnClickListener( + new RecurrenceViewClickListener((AppCompatActivity) getActivity(), mRecurrenceRule, this)); + + mAddBudgetAmount.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + addBudgetAmountView(null); + } + }); + return view; + } + + @Override + public void onCreate(@Nullable Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + mAccountsDbAdapter = AccountsDbAdapter.getInstance(); + mBudgetsDbAdapter = BudgetsDbAdapter.getInstance(); + } + + @Override + public void onActivityCreated(@Nullable Bundle savedInstanceState) { + super.onActivityCreated(savedInstanceState); + ActionBar actionbar = ((AppCompatActivity) getActivity()).getSupportActionBar(); + actionbar.setTitle("Create Budget"); + + setHasOptionsMenu(true); + + String budgetUID = getArguments().getString(UxArgument.BUDGET_UID); + if (budgetUID != null){ //if we are editing the budget + initViews(mBudget = mBudgetsDbAdapter.getRecord(budgetUID)); + loadBudgetAmountViews(mBudget.getCompactedBudgetAmounts()); + } else { + BudgetAmountViewHolder viewHolder = (BudgetAmountViewHolder) addBudgetAmountView(null).getTag(); + viewHolder.removeItemBtn.setVisibility(View.GONE); //there should always be at least one + } + } + + /** + * Load views for the budget amounts + * @param budgetAmounts List of {@link BudgetAmount}s + */ + private void loadBudgetAmountViews(List budgetAmounts){ + for (BudgetAmount budgetAmount : budgetAmounts) { + addBudgetAmountView(budgetAmount); + } + } + + /** + * Extract {@link BudgetAmount}s from the views + * @return List of budget amounts + */ + private List extractBudgetAmounts(){ + List budgetAmounts = new ArrayList<>(); + for (View view : mBudgetAmountViews) { + BudgetAmountViewHolder viewHolder = (BudgetAmountViewHolder) view.getTag(); + BigDecimal amountValue = viewHolder.amountEditText.getValue(); + if (amountValue == null) + continue; + Money amount = new Money(amountValue, Commodity.DEFAULT_COMMODITY); + String accountUID = mAccountsDbAdapter.getUID(viewHolder.budgetAccountSpinner.getSelectedItemId()); + BudgetAmount budgetAmount = new BudgetAmount(amount, accountUID); + budgetAmounts.add(budgetAmount); + } + return budgetAmounts; + } + + /** + * Inflates a new BudgetAmount item view and adds it to the UI. + *

If the {@code budgetAmount} is not null, then it is used to initialize the view

+ * @param budgetAmount Budget amount + */ + private View addBudgetAmountView(BudgetAmount budgetAmount){ + LayoutInflater layoutInflater = getActivity().getLayoutInflater(); + View budgetAmountView = layoutInflater.inflate(R.layout.item_budget_amount, + mBudgetAmountTableLayout, false); + BudgetAmountViewHolder viewHolder = new BudgetAmountViewHolder(budgetAmountView); + if (budgetAmount != null){ + viewHolder.bindViews(budgetAmount); + } + mBudgetAmountTableLayout.addView(budgetAmountView, 0); + mBudgetAmountViews.add(budgetAmountView); +// mScrollView.fullScroll(ScrollView.FOCUS_DOWN); + return budgetAmountView; + } + + /** + * Initialize views when editing an existing budget + * @param budget Budget to use to initialize the views + */ + private void initViews(Budget budget){ + mBudgetNameInput.setText(budget.getName()); + mDescriptionInput.setText(budget.getDescription()); + + String recurrenceRuleString = budget.getRecurrence().getRuleString(); + mRecurrenceRule = recurrenceRuleString; + mEventRecurrence.parse(recurrenceRuleString); + mRecurrenceInput.setText(budget.getRecurrence().getRepeatString()); + } + + /** + * Loads the accounts in the spinner + */ + private void setupAccountSpinnerAdapter(){ + String conditions = "(" + DatabaseSchema.AccountEntry.COLUMN_HIDDEN + " = 0 )"; + + if (mAccountCursor != null) { + mAccountCursor.close(); + } + mAccountCursor = mAccountsDbAdapter.fetchAccountsOrderedByFullName(conditions, null); + + mAccountCursorAdapter = new QualifiedAccountNameCursorAdapter(getActivity(), mAccountCursor); + } + + /** + * Checks that this budget can be saved + * Also sets the appropriate error messages on the relevant views + *

For a budget to be saved, it needs to have a name, an amount and a schedule

+ * @return {@code true} if the budget can be saved, {@code false} otherwise + */ + private boolean canSave(){ + for (View budgetAmountView : mBudgetAmountViews) { + BudgetAmountViewHolder viewHolder = (BudgetAmountViewHolder) budgetAmountView.getTag(); + viewHolder.amountEditText.evaluate(); + if (viewHolder.amountEditText.getError() != null){ + return false; + } + //at least one account should be loaded (don't create budget with empty account tree + if (viewHolder.budgetAccountSpinner.getCount() == 0){ + Toast.makeText(getActivity(), "You need an account hierarchy to create a budget!", + Toast.LENGTH_SHORT).show(); + return false; + } + } + + if (mEventRecurrence.until != null && mEventRecurrence.until.length() > 0 + || mEventRecurrence.count <= 0){ + Toast.makeText(getActivity(), + "Set a number periods in the recurrence dialog to save the budget", + Toast.LENGTH_SHORT).show(); + return false; + } + + String budgetName = mBudgetNameInput.getText().toString(); + boolean canSave = mRecurrenceRule != null + && !budgetName.isEmpty(); + if (!canSave){ + + if (budgetName.isEmpty()){ + mNameTextInputLayout.setError("A name is required"); + mNameTextInputLayout.setErrorEnabled(true); + } else { + mNameTextInputLayout.setErrorEnabled(false); + } + + if (mRecurrenceRule == null){ + Toast.makeText(getActivity(), "Set a repeat pattern to create a budget!", Toast.LENGTH_SHORT).show(); + } + } + + return canSave; + } + + /** + * Extracts the information from the form and saves the budget + */ + private void saveBudget(){ + if (!canSave()) + return; + String name = mBudgetNameInput.getText().toString().trim(); + + + if (mBudget == null){ + mBudget = new Budget(name); + } else { + mBudget.setName(name); + } + + // TODO: 22.10.2015 set the period num of the budget amount + mBudget.setBudgetAmounts(extractBudgetAmounts()); + + mBudget.setDescription(mDescriptionInput.getText().toString().trim()); + + mBudget.setRecurrence(RecurrenceParser.parse(mEventRecurrence)); + + mBudgetsDbAdapter.addRecord(mBudget); + getActivity().finish(); + } + + @Override + public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { + inflater.inflate(R.menu.default_save_actions, menu); + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + switch (item.getItemId()){ + case R.id.menu_save: + saveBudget(); + return true; + } + return false; + } + + @Override + public void onRecurrenceSet(String rrule) { + mRecurrenceRule = rrule; + String repeatString = getString(R.string.label_tap_to_create_schedule); + if (mRecurrenceRule != null){ + mEventRecurrence.parse(mRecurrenceRule); + repeatString = EventRecurrenceFormatter.getRepeatString(getActivity(), getResources(), mEventRecurrence, true); + } + + mRecurrenceInput.setText(repeatString); + } + + /** + * View holder for budget amounts + */ + class BudgetAmountViewHolder{ + @Bind(R.id.currency_symbol) TextView currencySymbolTextView; + @Bind(R.id.input_budget_amount) CalculatorEditText amountEditText; + @Bind(R.id.input_budget_account_spinner) Spinner budgetAccountSpinner; + @Bind(R.id.btn_remove_item) ImageView removeItemBtn; + View itemView; + + public BudgetAmountViewHolder(View view){ + itemView = view; + ButterKnife.bind(this, view); + itemView.setTag(this); + + amountEditText.bindListeners(mKeyboardView); + budgetAccountSpinner.setAdapter(mAccountCursorAdapter); + + budgetAccountSpinner.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() { + @Override + public void onItemSelected(AdapterView parent, View view, int position, long id) { + String currencyCode = mAccountsDbAdapter.getCurrencyCode(mAccountsDbAdapter.getUID(id)); + Currency currency = Currency.getInstance(currencyCode); + currencySymbolTextView.setText(currency.getSymbol()); + } + + @Override + public void onNothingSelected(AdapterView parent) { + //nothing to see here, move along + } + }); + + removeItemBtn.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + mBudgetAmountTableLayout.removeView(itemView); + mBudgetAmountViews.remove(itemView); + } + }); + } + + public void bindViews(BudgetAmount budgetAmount){ + amountEditText.setValue(budgetAmount.getAmount().asBigDecimal()); + budgetAccountSpinner.setSelection(mAccountCursorAdapter.getPosition(budgetAmount.getAccountUID())); + } + } +} diff --git a/app/src/main/java/org/gnucash/android/ui/budget/BudgetListFragment.java b/app/src/main/java/org/gnucash/android/ui/budget/BudgetListFragment.java new file mode 100644 index 000000000..ab90c9d40 --- /dev/null +++ b/app/src/main/java/org/gnucash/android/ui/budget/BudgetListFragment.java @@ -0,0 +1,327 @@ +/* + * Copyright (c) 2015 Ngewi Fet + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.gnucash.android.ui.budget; + +import android.app.Activity; +import android.content.Context; +import android.content.Intent; +import android.content.res.Configuration; +import android.database.Cursor; +import android.os.Bundle; +import android.support.annotation.Nullable; +import android.support.v4.app.Fragment; +import android.support.v4.app.FragmentManager; +import android.support.v4.app.FragmentTransaction; +import android.support.v4.app.LoaderManager; +import android.support.v4.content.Loader; +import android.support.v7.app.AppCompatActivity; +import android.support.v7.widget.GridLayoutManager; +import android.support.v7.widget.LinearLayoutManager; +import android.support.v7.widget.PopupMenu; +import android.support.v7.widget.RecyclerView; +import android.util.Log; +import android.view.LayoutInflater; +import android.view.MenuInflater; +import android.view.MenuItem; +import android.view.View; +import android.view.ViewGroup; +import android.widget.Button; +import android.widget.ImageView; +import android.widget.ProgressBar; +import android.widget.TextView; + +import org.gnucash.android.R; +import org.gnucash.android.db.DatabaseCursorLoader; +import org.gnucash.android.db.DatabaseSchema; +import org.gnucash.android.db.adapter.AccountsDbAdapter; +import org.gnucash.android.db.adapter.BudgetsDbAdapter; +import org.gnucash.android.model.Budget; +import org.gnucash.android.model.BudgetAmount; +import org.gnucash.android.model.Money; +import org.gnucash.android.ui.common.FormActivity; +import org.gnucash.android.ui.common.UxArgument; +import org.gnucash.android.ui.util.CursorRecyclerAdapter; +import org.gnucash.android.ui.util.Refreshable; +import org.gnucash.android.ui.util.widget.EmptyRecyclerView; + +import java.math.BigDecimal; +import java.math.RoundingMode; +import java.util.Currency; + +import butterknife.Bind; +import butterknife.ButterKnife; + +/** + * Budget list fragment + */ +public class BudgetListFragment extends Fragment implements Refreshable, + LoaderManager.LoaderCallbacks { + + private static final String LOG_TAG = "BudgetListFragment"; + private static final int REQUEST_EDIT_BUDGET = 0xB; + private static final int REQUEST_OPEN_ACCOUNT = 0xC; + + private BudgetRecyclerAdapter mBudgetRecyclerAdapter; + + private BudgetsDbAdapter mBudgetsDbAdapter; + + @Bind(R.id.budget_recycler_view) EmptyRecyclerView mRecyclerView; + @Bind(R.id.empty_view) Button mProposeBudgets; + + @Nullable + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { + View view = inflater.inflate(R.layout.fragment_budget_list, container, false); + ButterKnife.bind(this, view); + + mRecyclerView.setHasFixedSize(true); + mRecyclerView.setEmptyView(mProposeBudgets); + + if (getResources().getConfiguration().orientation == Configuration.ORIENTATION_LANDSCAPE) { + GridLayoutManager gridLayoutManager = new GridLayoutManager(getActivity(), 2); + mRecyclerView.setLayoutManager(gridLayoutManager); + } else { + LinearLayoutManager mLayoutManager = new LinearLayoutManager(getActivity()); + mRecyclerView.setLayoutManager(mLayoutManager); + } + return view; + } + + @Override + public void onActivityCreated(Bundle savedInstanceState) { + super.onActivityCreated(savedInstanceState); + + mBudgetsDbAdapter = BudgetsDbAdapter.getInstance(); + mBudgetRecyclerAdapter = new BudgetRecyclerAdapter(null); + + mRecyclerView.setAdapter(mBudgetRecyclerAdapter); + + getLoaderManager().initLoader(0, null, this); + } + + @Override + public Loader onCreateLoader(int id, Bundle args) { + Log.d(LOG_TAG, "Creating the accounts loader"); + return new BudgetsCursorLoader(getActivity()); + } + + @Override + public void onLoadFinished(Loader loaderCursor, Cursor cursor) { + Log.d(LOG_TAG, "Budget loader finished. Swapping in cursor"); + mBudgetRecyclerAdapter.swapCursor(cursor); + mBudgetRecyclerAdapter.notifyDataSetChanged(); + } + + @Override + public void onLoaderReset(Loader arg0) { + Log.d(LOG_TAG, "Resetting the accounts loader"); + mBudgetRecyclerAdapter.swapCursor(null); + } + + @Override + public void onResume() { + super.onResume(); + refresh(); + getActivity().findViewById(R.id.fab_create_budget).setVisibility(View.VISIBLE); + ((AppCompatActivity)getActivity()).getSupportActionBar().setTitle("Budgets"); + } + + @Override + public void refresh() { + getLoaderManager().restartLoader(0, null, this); + } + + /** + * This method does nothing with the GUID. + * Is equivalent to calling {@link #refresh()} + * @param uid GUID of relevant item to be refreshed + */ + @Override + public void refresh(String uid) { + refresh(); + } + + /** + * Opens the budget detail fragment + * @param budgetUID GUID of budget + */ + public void onClickBudget(String budgetUID){ + FragmentManager fragmentManager = getActivity().getSupportFragmentManager(); + FragmentTransaction fragmentTransaction = fragmentManager + .beginTransaction(); + + fragmentTransaction.replace(R.id.fragment_container, BudgetDetailFragment.newInstance(budgetUID)); + fragmentTransaction.addToBackStack(null); + fragmentTransaction.commit(); + } + + /** + * Launches the FormActivity for editing the budget + * @param budgetId Db record Id of the budget + */ + private void editBudget(long budgetId){ + Intent addAccountIntent = new Intent(getActivity(), FormActivity.class); + addAccountIntent.setAction(Intent.ACTION_INSERT_OR_EDIT); + addAccountIntent.putExtra(UxArgument.FORM_TYPE, FormActivity.FormType.BUDGET.name()); + addAccountIntent.putExtra(UxArgument.BUDGET_UID, mBudgetsDbAdapter.getUID(budgetId)); + startActivityForResult(addAccountIntent, REQUEST_EDIT_BUDGET); + } + + /** + * Delete the budget from the database + * @param budgetId Database record ID + */ + private void deleteBudget(long budgetId){ + BudgetsDbAdapter.getInstance().deleteRecord(budgetId); + refresh(); + } + + @Override + public void onActivityResult(int requestCode, int resultCode, Intent data) { + if (resultCode == Activity.RESULT_OK){ + refresh(); + } + } + + class BudgetRecyclerAdapter extends CursorRecyclerAdapter{ + + public BudgetRecyclerAdapter(Cursor cursor) { + super(cursor); + } + + @Override + public void onBindViewHolderCursor(BudgetViewHolder holder, Cursor cursor) { + final Budget budget = mBudgetsDbAdapter.buildModelInstance(cursor); + holder.budgetId = mBudgetsDbAdapter.getID(budget.getUID()); + + holder.budgetName.setText(budget.getName()); + + AccountsDbAdapter accountsDbAdapter = AccountsDbAdapter.getInstance(); + String accountString; + int numberOfAccounts = budget.getNumberOfAccounts(); + if (numberOfAccounts == 1){ + accountString = accountsDbAdapter.getAccountFullName(budget.getBudgetAmounts().get(0).getAccountUID()); + } else { + accountString = numberOfAccounts + " budgeted accounts"; + } + holder.accountName.setText(accountString); + + holder.budgetRecurrence.setText(budget.getRecurrence().getRepeatString() + " - " + + budget.getRecurrence().getDaysLeftInCurrentPeriod() + " days left"); + + BigDecimal spentAmountValue = BigDecimal.ZERO; + for (BudgetAmount budgetAmount : budget.getCompactedBudgetAmounts()) { + Money balance = accountsDbAdapter.getAccountBalance(budgetAmount.getAccountUID(), + budget.getStartofCurrentPeriod(), budget.getEndOfCurrentPeriod()); + spentAmountValue = spentAmountValue.add(balance.asBigDecimal()); + } + + Money budgetTotal = budget.getAmountSum(); + Currency currency = budgetTotal.getCurrency(); + String usedAmount = currency.getSymbol() + spentAmountValue+ " of " + + budgetTotal.formattedString(); + holder.budgetAmount.setText(usedAmount); + + double budgetProgress = spentAmountValue.divide(budgetTotal.asBigDecimal(), + currency.getDefaultFractionDigits(), RoundingMode.HALF_EVEN) + .doubleValue(); + holder.budgetIndicator.setProgress((int) (budgetProgress * 100)); + + holder.budgetAmount.setTextColor(BudgetsActivity.getBudgetProgressColor(1 - budgetProgress)); + + holder.itemView.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + onClickBudget(budget.getUID()); + } + }); + } + + @Override + public BudgetViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { + View v = LayoutInflater.from(parent.getContext()) + .inflate(R.layout.cardview_budget, parent, false); + + return new BudgetViewHolder(v); + } + + class BudgetViewHolder extends RecyclerView.ViewHolder implements PopupMenu.OnMenuItemClickListener{ + @Bind(R.id.primary_text) TextView budgetName; + @Bind(R.id.secondary_text) TextView accountName; + @Bind(R.id.budget_amount) TextView budgetAmount; + @Bind(R.id.options_menu) ImageView optionsMenu; + @Bind(R.id.budget_indicator) ProgressBar budgetIndicator; + @Bind(R.id.budget_recurrence) TextView budgetRecurrence; + long budgetId; + + public BudgetViewHolder(View itemView) { + super(itemView); + ButterKnife.bind(this, itemView); + + optionsMenu.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + android.support.v7.widget.PopupMenu popup = new android.support.v7.widget.PopupMenu(getActivity(), v); + popup.setOnMenuItemClickListener(BudgetViewHolder.this); + MenuInflater inflater = popup.getMenuInflater(); + inflater.inflate(R.menu.budget_context_menu, popup.getMenu()); + popup.show(); + } + }); + + } + + @Override + public boolean onMenuItemClick(MenuItem item) { + switch (item.getItemId()){ + case R.id.context_menu_edit_budget: + editBudget(budgetId); + return true; + + case R.id.context_menu_delete: + deleteBudget(budgetId); + return true; + + default: + return false; + } + } + } + } + + /** + * Loads Budgets asynchronously from the database + */ + private static class BudgetsCursorLoader extends DatabaseCursorLoader { + + /** + * Constructor + * Initializes the content observer + * + * @param context Application context + */ + public BudgetsCursorLoader(Context context) { + super(context); + } + + @Override + public Cursor loadInBackground() { + mDatabaseAdapter = BudgetsDbAdapter.getInstance(); + return mDatabaseAdapter.fetchAllRecords(null, null, DatabaseSchema.BudgetEntry.COLUMN_NAME + " ASC"); + } + } +} diff --git a/app/src/main/java/org/gnucash/android/ui/budget/BudgetsActivity.java b/app/src/main/java/org/gnucash/android/ui/budget/BudgetsActivity.java new file mode 100644 index 000000000..7edd4110a --- /dev/null +++ b/app/src/main/java/org/gnucash/android/ui/budget/BudgetsActivity.java @@ -0,0 +1,85 @@ +/* + * Copyright (c) 2015 Ngewi Fet + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.gnucash.android.ui.budget; + +import android.content.Intent; +import android.os.Bundle; +import android.support.v4.app.FragmentManager; +import android.support.v4.app.FragmentTransaction; +import android.support.v7.app.ActionBar; +import android.support.v7.widget.Toolbar; +import android.view.View; + +import org.gnucash.android.R; +import org.gnucash.android.app.GnuCashApplication; +import org.gnucash.android.ui.common.BaseDrawerActivity; +import org.gnucash.android.ui.common.FormActivity; +import org.gnucash.android.ui.common.UxArgument; + +import butterknife.ButterKnife; + +/** + * Activity for managing display and editing of budgets + */ +public class BudgetsActivity extends BaseDrawerActivity { + + public static final int REQUEST_CREATE_BUDGET = 0xA; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_budgets); + setUpDrawer(); + ButterKnife.bind(this); + + Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar); + setSupportActionBar(toolbar); + + final ActionBar actionBar = getSupportActionBar(); + assert actionBar != null; + actionBar.setTitle("Budgets"); + actionBar.setDisplayHomeAsUpEnabled(true); + + if (savedInstanceState == null) { + FragmentManager fragmentManager = getSupportFragmentManager(); + FragmentTransaction fragmentTransaction = fragmentManager + .beginTransaction(); + + fragmentTransaction.replace(R.id.fragment_container, new BudgetListFragment()); + fragmentTransaction.commit(); + } + } + + /** + * Callback when create budget floating action button is clicked + * @param view View which was clicked + */ + public void onCreateBudgetClick(View view){ + Intent addAccountIntent = new Intent(BudgetsActivity.this, FormActivity.class); + addAccountIntent.setAction(Intent.ACTION_INSERT_OR_EDIT); + addAccountIntent.putExtra(UxArgument.FORM_TYPE, FormActivity.FormType.BUDGET.name()); + startActivityForResult(addAccountIntent, REQUEST_CREATE_BUDGET); + } + + /** + * Returns a color between red and green depending on the value parameter + * @param value Value between 0 and 1 indicating the red to green ratio + * @return Color between red and green + */ + public static int getBudgetProgressColor(double value){ + return GnuCashApplication.darken(android.graphics.Color.HSVToColor(new float[]{(float)value*120f,1f,1f})); + } +} diff --git a/app/src/main/java/org/gnucash/android/ui/common/BaseDrawerActivity.java b/app/src/main/java/org/gnucash/android/ui/common/BaseDrawerActivity.java index 49ffe5adf..6c1f4cf59 100644 --- a/app/src/main/java/org/gnucash/android/ui/common/BaseDrawerActivity.java +++ b/app/src/main/java/org/gnucash/android/ui/common/BaseDrawerActivity.java @@ -32,6 +32,7 @@ import org.gnucash.android.R; import org.gnucash.android.ui.account.AccountsActivity; +import org.gnucash.android.ui.budget.BudgetsActivity; import org.gnucash.android.ui.passcode.PasscodeLockActivity; import org.gnucash.android.ui.report.ReportsActivity; import org.gnucash.android.ui.settings.SettingsActivity; @@ -133,6 +134,7 @@ public boolean onOptionsItemSelected(MenuItem item) { * Handler for the navigation drawer items * */ protected void onDrawerMenuItemClicked(int itemId) { + mNavigationView.getMenu().findItem(itemId).setChecked(true); switch (itemId){ case R.id.nav_item_open: { //Open... files AccountsActivity.startXmlFileChooser(this); @@ -152,6 +154,10 @@ protected void onDrawerMenuItemClicked(int itemId) { startActivity(new Intent(this, ReportsActivity.class)); break; + case R.id.nav_item_budgets: + startActivity(new Intent(this, BudgetsActivity.class)); + break; + case R.id.nav_item_scheduled_actions: { //show scheduled transactions Intent intent = new Intent(this, ScheduledActionsActivity.class); intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_SINGLE_TOP); @@ -180,13 +186,16 @@ protected void onDrawerMenuItemClicked(int itemId) { @Override protected void onActivityResult(int requestCode, int resultCode, Intent data) { if (resultCode == Activity.RESULT_CANCELED){ - return; + super.onActivityResult(requestCode, resultCode, data); } switch (requestCode) { case AccountsActivity.REQUEST_PICK_ACCOUNTS_FILE: AccountsActivity.importXmlFileFromIntent(this, data); break; + default: + super.onActivityResult(requestCode, resultCode, data); + break; } } diff --git a/app/src/main/java/org/gnucash/android/ui/common/FormActivity.java b/app/src/main/java/org/gnucash/android/ui/common/FormActivity.java index 4af305350..a079e8cdb 100644 --- a/app/src/main/java/org/gnucash/android/ui/common/FormActivity.java +++ b/app/src/main/java/org/gnucash/android/ui/common/FormActivity.java @@ -28,8 +28,9 @@ import org.gnucash.android.R; import org.gnucash.android.app.GnuCashApplication; -import org.gnucash.android.db.AccountsDbAdapter; +import org.gnucash.android.db.adapter.AccountsDbAdapter; import org.gnucash.android.ui.account.AccountFormFragment; +import org.gnucash.android.ui.budget.BudgetFormFragment; import org.gnucash.android.ui.export.ExportFormFragment; import org.gnucash.android.ui.passcode.PasscodeLockActivity; import org.gnucash.android.ui.transaction.TransactionFormFragment; @@ -48,7 +49,7 @@ public class FormActivity extends PasscodeLockActivity { private CalculatorKeyboard mOnBackListener; - public enum FormType {ACCOUNT, TRANSACTION, EXPORT, SPLIT_EDITOR} + public enum FormType {ACCOUNT, TRANSACTION, EXPORT, SPLIT_EDITOR, BUDGET} @Override protected void onCreate(Bundle savedInstanceState) { @@ -95,6 +96,10 @@ protected void onCreate(Bundle savedInstanceState) { showSplitEditorFragment(intent.getExtras()); break; + case BUDGET: + showBudgetFormFragment(intent.getExtras()); + break; + default: throw new IllegalArgumentException("No form display type specified"); } @@ -164,6 +169,16 @@ private void showSplitEditorFragment(Bundle args){ showFormFragment(splitEditor); } + /** + * Load the budget form + * @param args View arguments + */ + private void showBudgetFormFragment(Bundle args){ + BudgetFormFragment budgetFormFragment = new BudgetFormFragment(); + budgetFormFragment.setArguments(args); + showFormFragment(budgetFormFragment); + } + /** * Loads the fragment into the fragment container, replacing whatever was there before * @param fragment Fragment to be displayed diff --git a/app/src/main/java/org/gnucash/android/ui/common/UxArgument.java b/app/src/main/java/org/gnucash/android/ui/common/UxArgument.java index 9226a5715..7d29f6852 100644 --- a/app/src/main/java/org/gnucash/android/ui/common/UxArgument.java +++ b/app/src/main/java/org/gnucash/android/ui/common/UxArgument.java @@ -92,6 +92,11 @@ public final class UxArgument { */ public static final String SPLIT_LIST = "split_list"; + /** + * GUID of a budget + */ + public static final String BUDGET_UID = "budget_uid"; + /** * GUID of splits which have been removed from the split editor */ diff --git a/app/src/main/java/org/gnucash/android/ui/export/ExportFormFragment.java b/app/src/main/java/org/gnucash/android/ui/export/ExportFormFragment.java index 410b48a76..733f56018 100644 --- a/app/src/main/java/org/gnucash/android/ui/export/ExportFormFragment.java +++ b/app/src/main/java/org/gnucash/android/ui/export/ExportFormFragment.java @@ -21,16 +21,13 @@ import android.content.Intent; import android.content.SharedPreferences; import android.content.pm.PackageManager; -import android.content.res.Resources; import android.os.Build; import android.os.Bundle; import android.preference.PreferenceManager; import android.support.v4.app.Fragment; -import android.support.v4.app.FragmentManager; import android.support.v7.app.ActionBar; import android.support.v7.app.AppCompatActivity; import android.support.v7.widget.SwitchCompat; -import android.text.format.Time; import android.util.Log; import android.view.LayoutInflater; import android.view.Menu; @@ -47,17 +44,16 @@ import android.widget.Spinner; import android.widget.TextView; -import com.codetroopers.betterpickers.calendardatepicker.CalendarDatePickerDialog; -import com.codetroopers.betterpickers.radialtimepicker.RadialTimePickerDialog; +import com.codetroopers.betterpickers.calendardatepicker.CalendarDatePickerDialogFragment; +import com.codetroopers.betterpickers.radialtimepicker.RadialTimePickerDialogFragment; import com.codetroopers.betterpickers.recurrencepicker.EventRecurrence; import com.codetroopers.betterpickers.recurrencepicker.EventRecurrenceFormatter; -import com.codetroopers.betterpickers.recurrencepicker.RecurrencePickerDialog; -import com.crashlytics.android.Crashlytics; +import com.codetroopers.betterpickers.recurrencepicker.RecurrencePickerDialogFragment; import com.dropbox.sync.android.DbxAccountManager; import org.gnucash.android.R; import org.gnucash.android.app.GnuCashApplication; -import org.gnucash.android.db.ScheduledActionDbAdapter; +import org.gnucash.android.db.adapter.ScheduledActionDbAdapter; import org.gnucash.android.export.ExportAsyncTask; import org.gnucash.android.export.ExportFormat; import org.gnucash.android.export.ExportParams; @@ -69,14 +65,13 @@ import org.gnucash.android.ui.settings.SettingsActivity; import org.gnucash.android.ui.transaction.TransactionFormFragment; import org.gnucash.android.ui.util.RecurrenceParser; +import org.gnucash.android.ui.util.RecurrenceViewClickListener; import java.sql.Timestamp; -import java.text.DateFormat; import java.text.ParseException; import java.util.Calendar; import java.util.Date; import java.util.GregorianCalendar; -import java.util.List; import butterknife.Bind; import butterknife.ButterKnife; @@ -89,9 +84,9 @@ * @author Ngewi Fet */ public class ExportFormFragment extends Fragment implements - RecurrencePickerDialog.OnRecurrenceSetListener, - CalendarDatePickerDialog.OnDateSetListener, - RadialTimePickerDialog.OnTimeSetListener { + RecurrencePickerDialogFragment.OnRecurrenceSetListener, + CalendarDatePickerDialogFragment.OnDateSetListener, + RadialTimePickerDialogFragment.OnTimeSetListener { /** * Spinner for selecting destination for the exported file. @@ -275,13 +270,11 @@ private void startExport(){ exportParameters.setExportTarget(mExportTarget); exportParameters.setDeleteTransactionsAfterExport(mDeleteAllCheckBox.isChecked()); - List scheduledActions = RecurrenceParser.parse(mEventRecurrence, - ScheduledAction.ActionType.BACKUP); - for (ScheduledAction scheduledAction : scheduledActions) { - scheduledAction.setTag(exportParameters.toCsv()); - scheduledAction.setActionUID(BaseModel.generateUID()); - ScheduledActionDbAdapter.getInstance().addRecord(scheduledAction); - } + ScheduledAction scheduledAction = new ScheduledAction(ScheduledAction.ActionType.BACKUP); + scheduledAction.setRecurrence(RecurrenceParser.parse(mEventRecurrence)); + scheduledAction.setTag(exportParameters.toCsv()); + scheduledAction.setActionUID(BaseModel.generateUID()); + ScheduledActionDbAdapter.getInstance().addRecord(scheduledAction); Log.i(TAG, "Commencing async export of transactions"); new ExportAsyncTask(getActivity()).execute(exportParameters); @@ -376,7 +369,7 @@ public void onClick(View v) { int year = calendar.get(Calendar.YEAR); int monthOfYear = calendar.get(Calendar.MONTH); int dayOfMonth = calendar.get(Calendar.DAY_OF_MONTH); - CalendarDatePickerDialog datePickerDialog = CalendarDatePickerDialog.newInstance( + CalendarDatePickerDialogFragment datePickerDialog = CalendarDatePickerDialogFragment.newInstance( ExportFormFragment.this, year, monthOfYear, dayOfMonth); datePickerDialog.show(getFragmentManager(), "date_picker_fragment"); @@ -398,7 +391,7 @@ public void onClick(View v) { Calendar calendar = Calendar.getInstance(); calendar.setTimeInMillis(timeMillis); - RadialTimePickerDialog timePickerDialog = RadialTimePickerDialog.newInstance( + RadialTimePickerDialogFragment timePickerDialog = RadialTimePickerDialogFragment.newInstance( ExportFormFragment.this, calendar.get(Calendar.HOUR_OF_DAY), calendar.get(Calendar.MINUTE), true); timePickerDialog.show(getFragmentManager(), "time_picker_dialog_fragment"); @@ -420,30 +413,7 @@ public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) { mExportAllSwitch.setChecked(sharedPrefs.getBoolean(getString(R.string.key_export_all_transactions), false)); mDeleteAllCheckBox.setChecked(sharedPrefs.getBoolean(getString(R.string.key_delete_transactions_after_export), false)); - mRecurrenceTextView.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View view) { - FragmentManager fm = getActivity().getSupportFragmentManager(); - Bundle b = new Bundle(); - Time t = new Time(); - t.setToNow(); - b.putLong(RecurrencePickerDialog.BUNDLE_START_TIME_MILLIS, t.toMillis(false)); - b.putString(RecurrencePickerDialog.BUNDLE_TIME_ZONE, t.timezone); - - // may be more efficient to serialize and pass in EventRecurrence - b.putString(RecurrencePickerDialog.BUNDLE_RRULE, mRecurrenceRule); - - RecurrencePickerDialog rpd = (RecurrencePickerDialog) fm.findFragmentByTag( - "recurrence_picker"); - if (rpd != null) { - rpd.dismiss(); - } - rpd = new RecurrencePickerDialog(); - rpd.setArguments(b); - rpd.setOnRecurrenceSetListener(ExportFormFragment.this); - rpd.show(fm, "recurrence_picker"); - } - }); + mRecurrenceTextView.setOnClickListener(new RecurrenceViewClickListener((AppCompatActivity) getActivity(), mRecurrenceRule, this)); //this part (setting the export format) must come after the recurrence view bindings above String defaultExportFormat = sharedPrefs.getString(getString(R.string.key_default_export_format), ExportFormat.QIF.name()); @@ -502,7 +472,7 @@ public void onActivityResult(int requestCode, int resultCode, Intent data) { } @Override - public void onDateSet(CalendarDatePickerDialog dialog, int year, int monthOfYear, int dayOfMonth) { + public void onDateSet(CalendarDatePickerDialogFragment dialog, int year, int monthOfYear, int dayOfMonth) { Calendar cal = new GregorianCalendar(year, monthOfYear, dayOfMonth); mExportStartDate.setText(TransactionFormFragment.DATE_FORMATTER.format(cal.getTime())); mExportStartCalendar.set(Calendar.YEAR, year); @@ -511,7 +481,7 @@ public void onDateSet(CalendarDatePickerDialog dialog, int year, int monthOfYear } @Override - public void onTimeSet(RadialTimePickerDialog dialog, int hourOfDay, int minute) { + public void onTimeSet(RadialTimePickerDialogFragment dialog, int hourOfDay, int minute) { Calendar cal = new GregorianCalendar(0, 0, 0, hourOfDay, minute); mExportStartTime.setText(TransactionFormFragment.TIME_FORMATTER.format(cal.getTime())); mExportStartCalendar.set(Calendar.HOUR_OF_DAY, hourOfDay); diff --git a/app/src/main/java/org/gnucash/android/ui/homescreen/WidgetConfigurationActivity.java b/app/src/main/java/org/gnucash/android/ui/homescreen/WidgetConfigurationActivity.java index 99f55eab7..f37e52115 100644 --- a/app/src/main/java/org/gnucash/android/ui/homescreen/WidgetConfigurationActivity.java +++ b/app/src/main/java/org/gnucash/android/ui/homescreen/WidgetConfigurationActivity.java @@ -36,7 +36,7 @@ import android.widget.Toast; import org.gnucash.android.R; -import org.gnucash.android.db.AccountsDbAdapter; +import org.gnucash.android.db.adapter.AccountsDbAdapter; import org.gnucash.android.model.Account; import org.gnucash.android.model.Money; import org.gnucash.android.receivers.TransactionAppWidgetProvider; diff --git a/app/src/main/java/org/gnucash/android/ui/report/BalanceSheetFragment.java b/app/src/main/java/org/gnucash/android/ui/report/BalanceSheetFragment.java index 79f77a5ee..fed3004d2 100644 --- a/app/src/main/java/org/gnucash/android/ui/report/BalanceSheetFragment.java +++ b/app/src/main/java/org/gnucash/android/ui/report/BalanceSheetFragment.java @@ -30,7 +30,7 @@ import android.widget.TextView; import org.gnucash.android.R; -import org.gnucash.android.db.AccountsDbAdapter; +import org.gnucash.android.db.adapter.AccountsDbAdapter; import org.gnucash.android.db.DatabaseSchema; import org.gnucash.android.model.AccountType; import org.gnucash.android.model.Money; diff --git a/app/src/main/java/org/gnucash/android/ui/report/BarChartFragment.java b/app/src/main/java/org/gnucash/android/ui/report/BarChartFragment.java index 0cd2f3dd9..8cee3a92f 100644 --- a/app/src/main/java/org/gnucash/android/ui/report/BarChartFragment.java +++ b/app/src/main/java/org/gnucash/android/ui/report/BarChartFragment.java @@ -45,8 +45,8 @@ import org.gnucash.android.R; import org.gnucash.android.app.GnuCashApplication; -import org.gnucash.android.db.AccountsDbAdapter; -import org.gnucash.android.db.TransactionsDbAdapter; +import org.gnucash.android.db.adapter.AccountsDbAdapter; +import org.gnucash.android.db.adapter.TransactionsDbAdapter; import org.gnucash.android.model.Account; import org.gnucash.android.model.AccountType; import org.joda.time.LocalDate; diff --git a/app/src/main/java/org/gnucash/android/ui/report/LineChartFragment.java b/app/src/main/java/org/gnucash/android/ui/report/LineChartFragment.java index 5c9e12aa5..0da608673 100644 --- a/app/src/main/java/org/gnucash/android/ui/report/LineChartFragment.java +++ b/app/src/main/java/org/gnucash/android/ui/report/LineChartFragment.java @@ -43,8 +43,8 @@ import org.gnucash.android.R; import org.gnucash.android.app.GnuCashApplication; -import org.gnucash.android.db.AccountsDbAdapter; -import org.gnucash.android.db.TransactionsDbAdapter; +import org.gnucash.android.db.adapter.AccountsDbAdapter; +import org.gnucash.android.db.adapter.TransactionsDbAdapter; import org.gnucash.android.model.Account; import org.gnucash.android.model.AccountType; import org.gnucash.android.ui.report.ReportsActivity.GroupInterval; diff --git a/app/src/main/java/org/gnucash/android/ui/report/PieChartFragment.java b/app/src/main/java/org/gnucash/android/ui/report/PieChartFragment.java index f4b4ea5e9..7453262bf 100644 --- a/app/src/main/java/org/gnucash/android/ui/report/PieChartFragment.java +++ b/app/src/main/java/org/gnucash/android/ui/report/PieChartFragment.java @@ -41,8 +41,8 @@ import org.gnucash.android.R; import org.gnucash.android.app.GnuCashApplication; -import org.gnucash.android.db.AccountsDbAdapter; -import org.gnucash.android.db.TransactionsDbAdapter; +import org.gnucash.android.db.adapter.AccountsDbAdapter; +import org.gnucash.android.db.adapter.TransactionsDbAdapter; import org.gnucash.android.model.Account; import org.gnucash.android.model.AccountType; diff --git a/app/src/main/java/org/gnucash/android/ui/report/ReportSummaryFragment.java b/app/src/main/java/org/gnucash/android/ui/report/ReportSummaryFragment.java index 88d2257a7..5c5234190 100644 --- a/app/src/main/java/org/gnucash/android/ui/report/ReportSummaryFragment.java +++ b/app/src/main/java/org/gnucash/android/ui/report/ReportSummaryFragment.java @@ -41,7 +41,7 @@ import org.gnucash.android.R; import org.gnucash.android.app.GnuCashApplication; -import org.gnucash.android.db.AccountsDbAdapter; +import org.gnucash.android.db.adapter.AccountsDbAdapter; import org.gnucash.android.model.Account; import org.gnucash.android.model.AccountType; import org.gnucash.android.model.Money; diff --git a/app/src/main/java/org/gnucash/android/ui/report/ReportsActivity.java b/app/src/main/java/org/gnucash/android/ui/report/ReportsActivity.java index a01c9be5a..5e7fbeef2 100644 --- a/app/src/main/java/org/gnucash/android/ui/report/ReportsActivity.java +++ b/app/src/main/java/org/gnucash/android/ui/report/ReportsActivity.java @@ -38,7 +38,7 @@ import org.gnucash.android.R; import org.gnucash.android.app.GnuCashApplication; -import org.gnucash.android.db.TransactionsDbAdapter; +import org.gnucash.android.db.adapter.TransactionsDbAdapter; import org.gnucash.android.model.AccountType; import org.gnucash.android.ui.common.BaseDrawerActivity; import org.gnucash.android.ui.report.dialog.DateRangePickerDialogFragment; diff --git a/app/src/main/java/org/gnucash/android/ui/settings/AccountPreferencesFragment.java b/app/src/main/java/org/gnucash/android/ui/settings/AccountPreferencesFragment.java index 5242df81a..b1215f19c 100644 --- a/app/src/main/java/org/gnucash/android/ui/settings/AccountPreferencesFragment.java +++ b/app/src/main/java/org/gnucash/android/ui/settings/AccountPreferencesFragment.java @@ -29,9 +29,8 @@ import org.gnucash.android.R; import org.gnucash.android.app.GnuCashApplication; -import org.gnucash.android.db.CommoditiesDbAdapter; import org.gnucash.android.db.DatabaseSchema; -import org.gnucash.android.model.Commodity; +import org.gnucash.android.db.adapter.CommoditiesDbAdapter; import org.gnucash.android.model.Money; import org.gnucash.android.ui.account.AccountsActivity; diff --git a/app/src/main/java/org/gnucash/android/ui/settings/DeleteAllAccountsConfirmationDialog.java b/app/src/main/java/org/gnucash/android/ui/settings/DeleteAllAccountsConfirmationDialog.java index 11001b531..fb928ee82 100644 --- a/app/src/main/java/org/gnucash/android/ui/settings/DeleteAllAccountsConfirmationDialog.java +++ b/app/src/main/java/org/gnucash/android/ui/settings/DeleteAllAccountsConfirmationDialog.java @@ -26,7 +26,7 @@ import android.widget.Toast; import org.gnucash.android.R; -import org.gnucash.android.db.AccountsDbAdapter; +import org.gnucash.android.db.adapter.AccountsDbAdapter; import org.gnucash.android.export.xml.GncXmlExporter; import org.gnucash.android.ui.homescreen.WidgetConfigurationActivity; diff --git a/app/src/main/java/org/gnucash/android/ui/settings/DeleteAllTransactionsConfirmationDialog.java b/app/src/main/java/org/gnucash/android/ui/settings/DeleteAllTransactionsConfirmationDialog.java index 22ae73078..6bb419438 100644 --- a/app/src/main/java/org/gnucash/android/ui/settings/DeleteAllTransactionsConfirmationDialog.java +++ b/app/src/main/java/org/gnucash/android/ui/settings/DeleteAllTransactionsConfirmationDialog.java @@ -28,8 +28,8 @@ import org.gnucash.android.R; import org.gnucash.android.app.GnuCashApplication; -import org.gnucash.android.db.AccountsDbAdapter; -import org.gnucash.android.db.TransactionsDbAdapter; +import org.gnucash.android.db.adapter.AccountsDbAdapter; +import org.gnucash.android.db.adapter.TransactionsDbAdapter; import org.gnucash.android.export.xml.GncXmlExporter; import org.gnucash.android.model.Transaction; import org.gnucash.android.ui.homescreen.WidgetConfigurationActivity; diff --git a/app/src/main/java/org/gnucash/android/ui/settings/SettingsActivity.java b/app/src/main/java/org/gnucash/android/ui/settings/SettingsActivity.java index 250a799c0..f558623ae 100644 --- a/app/src/main/java/org/gnucash/android/ui/settings/SettingsActivity.java +++ b/app/src/main/java/org/gnucash/android/ui/settings/SettingsActivity.java @@ -53,10 +53,10 @@ import org.gnucash.android.R; import org.gnucash.android.app.GnuCashApplication; -import org.gnucash.android.db.AccountsDbAdapter; -import org.gnucash.android.db.CommoditiesDbAdapter; +import org.gnucash.android.db.adapter.AccountsDbAdapter; +import org.gnucash.android.db.adapter.CommoditiesDbAdapter; import org.gnucash.android.db.DatabaseSchema; -import org.gnucash.android.db.TransactionsDbAdapter; +import org.gnucash.android.db.adapter.TransactionsDbAdapter; import org.gnucash.android.export.Exporter; import org.gnucash.android.export.xml.GncXmlExporter; import org.gnucash.android.importer.ImportAsyncTask; diff --git a/app/src/main/java/org/gnucash/android/ui/transaction/ScheduledActionsListFragment.java b/app/src/main/java/org/gnucash/android/ui/transaction/ScheduledActionsListFragment.java index 2358ddbf1..418129a28 100644 --- a/app/src/main/java/org/gnucash/android/ui/transaction/ScheduledActionsListFragment.java +++ b/app/src/main/java/org/gnucash/android/ui/transaction/ScheduledActionsListFragment.java @@ -51,8 +51,8 @@ import org.gnucash.android.app.GnuCashApplication; import org.gnucash.android.db.DatabaseCursorLoader; import org.gnucash.android.db.DatabaseSchema; -import org.gnucash.android.db.ScheduledActionDbAdapter; -import org.gnucash.android.db.TransactionsDbAdapter; +import org.gnucash.android.db.adapter.ScheduledActionDbAdapter; +import org.gnucash.android.db.adapter.TransactionsDbAdapter; import org.gnucash.android.export.ExportParams; import org.gnucash.android.model.ScheduledAction; import org.gnucash.android.model.Transaction; @@ -470,7 +470,7 @@ public void bindView(View view, Context context, Cursor cursor) { if (endTime > 0 && endTime < System.currentTimeMillis()){ ((TextView)view.findViewById(R.id.primary_text)).setTextColor(getResources().getColor(android.R.color.darker_gray)); descriptionTextView.setText(getString(R.string.label_scheduled_action_ended, - DateFormat.getInstance().format(new Date(scheduledAction.getLastRun())))); + DateFormat.getInstance().format(new Date(scheduledAction.getLastRunTime())))); } else { descriptionTextView.setText(scheduledAction.getRepeatString()); } @@ -565,7 +565,7 @@ public void bindView(View view, Context context, Cursor cursor) { if (endTime > 0 && endTime < System.currentTimeMillis()){ ((TextView)view.findViewById(R.id.primary_text)).setTextColor(getResources().getColor(android.R.color.darker_gray)); descriptionTextView.setText(getString(R.string.label_scheduled_action_ended, - DateFormat.getInstance().format(new Date(scheduledAction.getLastRun())))); + DateFormat.getInstance().format(new Date(scheduledAction.getLastRunTime())))); } else { descriptionTextView.setText(scheduledAction.getRepeatString()); } @@ -610,7 +610,7 @@ public Cursor loadInBackground() { Cursor c = mDatabaseAdapter.fetchAllRecords( DatabaseSchema.ScheduledActionEntry.COLUMN_TYPE + "=?", - new String[]{ScheduledAction.ActionType.BACKUP.name()}); + new String[]{ScheduledAction.ActionType.BACKUP.name()}, null); registerContentObserver(c); return c; diff --git a/app/src/main/java/org/gnucash/android/ui/transaction/SplitEditorFragment.java b/app/src/main/java/org/gnucash/android/ui/transaction/SplitEditorFragment.java index 0b6416069..b42099688 100644 --- a/app/src/main/java/org/gnucash/android/ui/transaction/SplitEditorFragment.java +++ b/app/src/main/java/org/gnucash/android/ui/transaction/SplitEditorFragment.java @@ -43,8 +43,8 @@ import android.widget.Toast; import org.gnucash.android.R; -import org.gnucash.android.db.AccountsDbAdapter; -import org.gnucash.android.db.CommoditiesDbAdapter; +import org.gnucash.android.db.adapter.AccountsDbAdapter; +import org.gnucash.android.db.adapter.CommoditiesDbAdapter; import org.gnucash.android.db.DatabaseSchema; import org.gnucash.android.model.AccountType; import org.gnucash.android.model.BaseModel; @@ -379,7 +379,7 @@ private List extractSplitsFromView(){ split.setType(viewHolder.splitTypeSwitch.getTransactionType()); split.setUID(viewHolder.splitUidTextView.getText().toString().trim()); if (viewHolder.quantity != null) - split.setQuantity(viewHolder.quantity.absolute()); + split.setQuantity(viewHolder.quantity.abs()); splitList.add(split); } return splitList; diff --git a/app/src/main/java/org/gnucash/android/ui/transaction/TransactionDetailActivity.java b/app/src/main/java/org/gnucash/android/ui/transaction/TransactionDetailActivity.java index 364966a04..173730fe5 100644 --- a/app/src/main/java/org/gnucash/android/ui/transaction/TransactionDetailActivity.java +++ b/app/src/main/java/org/gnucash/android/ui/transaction/TransactionDetailActivity.java @@ -15,9 +15,9 @@ import org.gnucash.android.R; import org.gnucash.android.app.GnuCashApplication; -import org.gnucash.android.db.AccountsDbAdapter; -import org.gnucash.android.db.ScheduledActionDbAdapter; -import org.gnucash.android.db.TransactionsDbAdapter; +import org.gnucash.android.db.adapter.AccountsDbAdapter; +import org.gnucash.android.db.adapter.ScheduledActionDbAdapter; +import org.gnucash.android.db.adapter.TransactionsDbAdapter; import org.gnucash.android.model.Money; import org.gnucash.android.model.ScheduledAction; import org.gnucash.android.model.Split; diff --git a/app/src/main/java/org/gnucash/android/ui/transaction/TransactionFormFragment.java b/app/src/main/java/org/gnucash/android/ui/transaction/TransactionFormFragment.java index 6cd8d00b3..844643bde 100644 --- a/app/src/main/java/org/gnucash/android/ui/transaction/TransactionFormFragment.java +++ b/app/src/main/java/org/gnucash/android/ui/transaction/TransactionFormFragment.java @@ -26,12 +26,10 @@ import android.os.Bundle; import android.preference.PreferenceManager; import android.support.v4.app.Fragment; -import android.support.v4.app.FragmentManager; import android.support.v4.widget.SimpleCursorAdapter; import android.support.v7.app.ActionBar; import android.support.v7.app.AppCompatActivity; import android.text.format.DateUtils; -import android.text.format.Time; import android.util.Log; import android.view.LayoutInflater; import android.view.Menu; @@ -51,22 +49,23 @@ import android.widget.TextView; import android.widget.Toast; -import com.codetroopers.betterpickers.calendardatepicker.CalendarDatePickerDialog; -import com.codetroopers.betterpickers.radialtimepicker.RadialTimePickerDialog; +import com.codetroopers.betterpickers.calendardatepicker.CalendarDatePickerDialogFragment; +import com.codetroopers.betterpickers.radialtimepicker.RadialTimePickerDialogFragment; import com.codetroopers.betterpickers.recurrencepicker.EventRecurrence; import com.codetroopers.betterpickers.recurrencepicker.EventRecurrenceFormatter; -import com.codetroopers.betterpickers.recurrencepicker.RecurrencePickerDialog; +import com.codetroopers.betterpickers.recurrencepicker.RecurrencePickerDialogFragment; import org.gnucash.android.R; import org.gnucash.android.app.GnuCashApplication; -import org.gnucash.android.db.AccountsDbAdapter; -import org.gnucash.android.db.CommoditiesDbAdapter; +import org.gnucash.android.db.adapter.CommoditiesDbAdapter; import org.gnucash.android.db.DatabaseSchema; -import org.gnucash.android.db.ScheduledActionDbAdapter; -import org.gnucash.android.db.TransactionsDbAdapter; +import org.gnucash.android.db.adapter.AccountsDbAdapter; +import org.gnucash.android.db.adapter.ScheduledActionDbAdapter; +import org.gnucash.android.db.adapter.TransactionsDbAdapter; import org.gnucash.android.model.AccountType; import org.gnucash.android.model.Commodity; import org.gnucash.android.model.Money; +import org.gnucash.android.model.Recurrence; import org.gnucash.android.model.ScheduledAction; import org.gnucash.android.model.Split; import org.gnucash.android.model.Transaction; @@ -77,6 +76,7 @@ import org.gnucash.android.ui.transaction.dialog.TransferFundsDialogFragment; import org.gnucash.android.ui.util.OnTransferFundsListener; import org.gnucash.android.ui.util.RecurrenceParser; +import org.gnucash.android.ui.util.RecurrenceViewClickListener; import org.gnucash.android.ui.util.widget.CalculatorEditText; import org.gnucash.android.ui.util.widget.TransactionTypeSwitch; import org.gnucash.android.util.QualifiedAccountNameCursorAdapter; @@ -101,10 +101,9 @@ * @author Ngewi Fet */ public class TransactionFormFragment extends Fragment implements - CalendarDatePickerDialog.OnDateSetListener, RadialTimePickerDialog.OnTimeSetListener, - RecurrencePickerDialog.OnRecurrenceSetListener, OnTransferFundsListener { + CalendarDatePickerDialogFragment.OnDateSetListener, RadialTimePickerDialogFragment.OnTimeSetListener, + RecurrencePickerDialogFragment.OnRecurrenceSetListener, OnTransferFundsListener { - private static final String FRAGMENT_TAG_RECURRENCE_PICKER = "recurrence_picker"; private static final int REQUEST_SPLIT_EDITOR = 0x11; /** @@ -120,7 +119,7 @@ public class TransactionFormFragment extends Fragment implements /** * Adapter for transfer account spinner */ - private SimpleCursorAdapter mCursorAdapter; + private QualifiedAccountNameCursorAdapter mAccountCursorAdapter; /** * Cursor for transfer account spinner @@ -275,7 +274,7 @@ private void startTransferFunds() { BigDecimal amountBigd = mAmountEditText.getValue(); if (amountBigd.equals(BigDecimal.ZERO)) return; - Money amount = new Money(amountBigd, Commodity.getInstance(fromCurrency.getCurrencyCode())).absolute(); + Money amount = new Money(amountBigd, Commodity.getInstance(fromCurrency.getCurrencyCode())).abs(); TransferFundsDialogFragment fragment = TransferFundsDialogFragment.getInstance(amount, targetCurrency, this); @@ -574,8 +573,8 @@ private void updateTransferAccountsList(){ } mCursor = mAccountsDbAdapter.fetchAccountsOrderedByFullName(conditions, new String[]{mAccountUID, AccountType.ROOT.name()}); - mCursorAdapter = new QualifiedAccountNameCursorAdapter(getActivity(), mCursor); - mTransferAccountSpinner.setAdapter(mCursorAdapter); + mAccountCursorAdapter = new QualifiedAccountNameCursorAdapter(getActivity(), mCursor); + mTransferAccountSpinner.setAdapter(mAccountCursorAdapter); } /** @@ -638,7 +637,7 @@ public void onClick(View v) { int year = calendar.get(Calendar.YEAR); int monthOfYear = calendar.get(Calendar.MONTH); int dayOfMonth = calendar.get(Calendar.DAY_OF_MONTH); - CalendarDatePickerDialog datePickerDialog = CalendarDatePickerDialog.newInstance( + CalendarDatePickerDialogFragment datePickerDialog = CalendarDatePickerDialogFragment.newInstance( TransactionFormFragment.this, year, monthOfYear, dayOfMonth); datePickerDialog.show(getFragmentManager(), "date_picker_fragment"); @@ -660,37 +659,14 @@ public void onClick(View v) { Calendar calendar = Calendar.getInstance(); calendar.setTimeInMillis(timeMillis); - RadialTimePickerDialog timePickerDialog = RadialTimePickerDialog.newInstance( + RadialTimePickerDialogFragment timePickerDialog = RadialTimePickerDialogFragment.newInstance( TransactionFormFragment.this, calendar.get(Calendar.HOUR_OF_DAY), calendar.get(Calendar.MINUTE), true); timePickerDialog.show(getFragmentManager(), "time_picker_dialog_fragment"); } }); - mRecurrenceTextView.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View view) { - FragmentManager fm = getActivity().getSupportFragmentManager(); - Bundle b = new Bundle(); - Time t = new Time(); - t.setToNow(); - b.putLong(RecurrencePickerDialog.BUNDLE_START_TIME_MILLIS, t.toMillis(false)); - b.putString(RecurrencePickerDialog.BUNDLE_TIME_ZONE, t.timezone); - - // may be more efficient to serialize and pass in EventRecurrence - b.putString(RecurrencePickerDialog.BUNDLE_RRULE, mRecurrenceRule); - - RecurrencePickerDialog rpd = (RecurrencePickerDialog) fm.findFragmentByTag( - FRAGMENT_TAG_RECURRENCE_PICKER); - if (rpd != null) { - rpd.dismiss(); - } - rpd = new RecurrencePickerDialog(); - rpd.setArguments(b); - rpd.setOnRecurrenceSetListener(TransactionFormFragment.this); - rpd.show(fm, FRAGMENT_TAG_RECURRENCE_PICKER); - } - }); + mRecurrenceTextView.setOnClickListener(new RecurrenceViewClickListener((AppCompatActivity) getActivity(), mRecurrenceRule, this)); } /** @@ -698,18 +674,9 @@ public void onClick(View view) { * @param accountId Database ID of the transfer account */ private void setSelectedTransferAccount(long accountId){ - for (int pos = 0; pos < mCursorAdapter.getCount(); pos++) { - if (mCursorAdapter.getItemId(pos) == accountId){ - final int position = pos; - mTransferAccountSpinner.postDelayed(new Runnable() { - @Override - public void run() { - mTransferAccountSpinner.setSelection(position); - } - }, 100); - break; - } - } + int position = mAccountCursorAdapter.getPosition(mAccountsDbAdapter.getUID(accountId)); + if (position >= 0) + mTransferAccountSpinner.setSelection(position); } /** @@ -736,7 +703,7 @@ private void saveNewTransaction() { } Currency currency = Currency.getInstance(mTransactionsDbAdapter.getAccountCurrencyCode(mAccountUID)); - Money amount = new Money(amountBigd, Commodity.getInstance(currency.getCurrencyCode())).absolute(); + Money amount = new Money(amountBigd, Commodity.getInstance(currency.getCurrencyCode())).abs(); if (mSplitsList.size() == 1){ //means split editor was opened but no split was added String transferAcctUID; @@ -852,33 +819,29 @@ private void saveNewTransaction() { private void scheduleRecurringTransaction(String transactionUID) { ScheduledActionDbAdapter scheduledActionDbAdapter = ScheduledActionDbAdapter.getInstance(); - List events = RecurrenceParser.parse(mEventRecurrence, - ScheduledAction.ActionType.TRANSACTION); + Recurrence recurrence = RecurrenceParser.parse(mEventRecurrence); + + ScheduledAction scheduledAction = new ScheduledAction(ScheduledAction.ActionType.TRANSACTION); + scheduledAction.setRecurrence(recurrence); String scheduledActionUID = getArguments().getString(UxArgument.SCHEDULED_ACTION_UID); if (scheduledActionUID != null) { //if we are editing an existing schedule - if ( events.size() == 1) { - ScheduledAction scheduledAction = events.get(0); + if (recurrence == null){ + scheduledActionDbAdapter.deleteRecord(scheduledActionUID); + } else { scheduledAction.setUID(scheduledActionUID); scheduledActionDbAdapter.updateRecurrenceAttributes(scheduledAction); Toast.makeText(getActivity(), "Updated transaction schedule", Toast.LENGTH_SHORT).show(); - return; - } else { - //if user changed scheduled action so that more than one new schedule would be saved, - // then remove the old one - ScheduledActionDbAdapter.getInstance().deleteRecord(scheduledActionUID); + } + } else { + if (recurrence != null) { + scheduledAction.setActionUID(transactionUID); + scheduledActionDbAdapter.addRecord(scheduledAction); + Toast.makeText(getActivity(), R.string.toast_scheduled_recurring_transaction, Toast.LENGTH_SHORT).show(); } } - for (ScheduledAction event : events) { - event.setActionUID(transactionUID); - scheduledActionDbAdapter.addRecord(event); - - Log.i("TransactionFormFragment", event.toString()); - } - Toast.makeText(getActivity(), R.string.toast_scheduled_recurring_transaction, Toast.LENGTH_SHORT).show(); - } @@ -970,7 +933,7 @@ private void finish(int resultCode) { } @Override - public void onDateSet(CalendarDatePickerDialog calendarDatePickerDialog, int year, int monthOfYear, int dayOfMonth) { + public void onDateSet(CalendarDatePickerDialogFragment calendarDatePickerDialog, int year, int monthOfYear, int dayOfMonth) { Calendar cal = new GregorianCalendar(year, monthOfYear, dayOfMonth); mDateTextView.setText(DATE_FORMATTER.format(cal.getTime())); mDate.set(Calendar.YEAR, year); @@ -979,7 +942,7 @@ public void onDateSet(CalendarDatePickerDialog calendarDatePickerDialog, int yea } @Override - public void onTimeSet(RadialTimePickerDialog radialTimePickerDialog, int hourOfDay, int minute) { + public void onTimeSet(RadialTimePickerDialogFragment radialTimePickerDialog, int hourOfDay, int minute) { Calendar cal = new GregorianCalendar(0, 0, 0, hourOfDay, minute); mTimeTextView.setText(TIME_FORMATTER.format(cal.getTime())); mTime.set(Calendar.HOUR_OF_DAY, hourOfDay); diff --git a/app/src/main/java/org/gnucash/android/ui/transaction/TransactionsActivity.java b/app/src/main/java/org/gnucash/android/ui/transaction/TransactionsActivity.java index 350f0a503..0ea57b93f 100644 --- a/app/src/main/java/org/gnucash/android/ui/transaction/TransactionsActivity.java +++ b/app/src/main/java/org/gnucash/android/ui/transaction/TransactionsActivity.java @@ -45,9 +45,9 @@ import org.gnucash.android.R; import org.gnucash.android.app.GnuCashApplication; -import org.gnucash.android.db.AccountsDbAdapter; +import org.gnucash.android.db.adapter.AccountsDbAdapter; import org.gnucash.android.db.DatabaseSchema; -import org.gnucash.android.db.TransactionsDbAdapter; +import org.gnucash.android.db.adapter.TransactionsDbAdapter; import org.gnucash.android.model.Account; import org.gnucash.android.model.Money; import org.gnucash.android.ui.common.BaseDrawerActivity; diff --git a/app/src/main/java/org/gnucash/android/ui/transaction/TransactionsListFragment.java b/app/src/main/java/org/gnucash/android/ui/transaction/TransactionsListFragment.java index e59155ea3..70ce38aa2 100644 --- a/app/src/main/java/org/gnucash/android/ui/transaction/TransactionsListFragment.java +++ b/app/src/main/java/org/gnucash/android/ui/transaction/TransactionsListFragment.java @@ -42,11 +42,11 @@ import android.widget.TextView; import org.gnucash.android.R; -import org.gnucash.android.db.AccountsDbAdapter; +import org.gnucash.android.db.adapter.AccountsDbAdapter; import org.gnucash.android.db.DatabaseCursorLoader; import org.gnucash.android.db.DatabaseSchema; -import org.gnucash.android.db.SplitsDbAdapter; -import org.gnucash.android.db.TransactionsDbAdapter; +import org.gnucash.android.db.adapter.SplitsDbAdapter; +import org.gnucash.android.db.adapter.TransactionsDbAdapter; import org.gnucash.android.model.Money; import org.gnucash.android.model.Split; import org.gnucash.android.model.Transaction; diff --git a/app/src/main/java/org/gnucash/android/ui/transaction/dialog/BulkMoveDialogFragment.java b/app/src/main/java/org/gnucash/android/ui/transaction/dialog/BulkMoveDialogFragment.java index 2122c96c9..c74be57c6 100644 --- a/app/src/main/java/org/gnucash/android/ui/transaction/dialog/BulkMoveDialogFragment.java +++ b/app/src/main/java/org/gnucash/android/ui/transaction/dialog/BulkMoveDialogFragment.java @@ -29,9 +29,9 @@ import android.widget.Toast; import org.gnucash.android.R; -import org.gnucash.android.db.AccountsDbAdapter; +import org.gnucash.android.db.adapter.AccountsDbAdapter; import org.gnucash.android.db.DatabaseSchema; -import org.gnucash.android.db.TransactionsDbAdapter; +import org.gnucash.android.db.adapter.TransactionsDbAdapter; import org.gnucash.android.ui.common.UxArgument; import org.gnucash.android.ui.homescreen.WidgetConfigurationActivity; import org.gnucash.android.ui.transaction.TransactionsActivity; diff --git a/app/src/main/java/org/gnucash/android/ui/transaction/dialog/TransactionsDeleteConfirmationDialogFragment.java b/app/src/main/java/org/gnucash/android/ui/transaction/dialog/TransactionsDeleteConfirmationDialogFragment.java index 3b9bcdbbb..06998d7d5 100644 --- a/app/src/main/java/org/gnucash/android/ui/transaction/dialog/TransactionsDeleteConfirmationDialogFragment.java +++ b/app/src/main/java/org/gnucash/android/ui/transaction/dialog/TransactionsDeleteConfirmationDialogFragment.java @@ -24,8 +24,8 @@ import org.gnucash.android.R; import org.gnucash.android.app.GnuCashApplication; -import org.gnucash.android.db.AccountsDbAdapter; -import org.gnucash.android.db.TransactionsDbAdapter; +import org.gnucash.android.db.adapter.AccountsDbAdapter; +import org.gnucash.android.db.adapter.TransactionsDbAdapter; import org.gnucash.android.export.xml.GncXmlExporter; import org.gnucash.android.model.Transaction; import org.gnucash.android.ui.common.UxArgument; diff --git a/app/src/main/java/org/gnucash/android/ui/transaction/dialog/TransferFundsDialogFragment.java b/app/src/main/java/org/gnucash/android/ui/transaction/dialog/TransferFundsDialogFragment.java index 20b28c6fa..4cf1f4a8b 100644 --- a/app/src/main/java/org/gnucash/android/ui/transaction/dialog/TransferFundsDialogFragment.java +++ b/app/src/main/java/org/gnucash/android/ui/transaction/dialog/TransferFundsDialogFragment.java @@ -35,8 +35,8 @@ import android.widget.TextView; import org.gnucash.android.R; -import org.gnucash.android.db.CommoditiesDbAdapter; -import org.gnucash.android.db.PricesDbAdapter; +import org.gnucash.android.db.adapter.CommoditiesDbAdapter; +import org.gnucash.android.db.adapter.PricesDbAdapter; import org.gnucash.android.model.Commodity; import org.gnucash.android.model.Money; import org.gnucash.android.model.Price; diff --git a/app/src/main/java/org/gnucash/android/ui/util/AccountBalanceTask.java b/app/src/main/java/org/gnucash/android/ui/util/AccountBalanceTask.java index 9dbcb6a5a..45f02d4f0 100644 --- a/app/src/main/java/org/gnucash/android/ui/util/AccountBalanceTask.java +++ b/app/src/main/java/org/gnucash/android/ui/util/AccountBalanceTask.java @@ -24,9 +24,8 @@ import com.crashlytics.android.Crashlytics; -import org.gnucash.android.R; import org.gnucash.android.app.GnuCashApplication; -import org.gnucash.android.db.AccountsDbAdapter; +import org.gnucash.android.db.adapter.AccountsDbAdapter; import org.gnucash.android.model.Money; import org.gnucash.android.ui.transaction.TransactionsActivity; diff --git a/app/src/main/java/org/gnucash/android/ui/util/RecurrenceParser.java b/app/src/main/java/org/gnucash/android/ui/util/RecurrenceParser.java index 660419344..f408a8dd8 100644 --- a/app/src/main/java/org/gnucash/android/ui/util/RecurrenceParser.java +++ b/app/src/main/java/org/gnucash/android/ui/util/RecurrenceParser.java @@ -20,8 +20,11 @@ import com.codetroopers.betterpickers.recurrencepicker.EventRecurrence; +import org.gnucash.android.model.PeriodType; +import org.gnucash.android.model.Recurrence; import org.gnucash.android.model.ScheduledAction; +import java.sql.Timestamp; import java.util.ArrayList; import java.util.Calendar; import java.util.List; @@ -40,98 +43,61 @@ public class RecurrenceParser { public static final long MONTH_MILLIS = 30*DAY_MILLIS; public static final long YEAR_MILLIS = 12*MONTH_MILLIS; - /** - * Parses an event recurrence to produce {@link org.gnucash.android.model.ScheduledAction}s for each recurrence. - *

Each {@link org.gnucash.android.model.ScheduledAction} represents just one simple repeating schedule, e.g. every Monday. - * If there are multiple schedules in the recurrence e.g. every Monday and Tuesday, then two ScheduledEvents will be generated

- * @param eventRecurrence Event recurrence pattern obtained from dialog - * @param actionType Type of event recurrence - * @return List of ScheduledEvents + * Parse an {@link EventRecurrence} into a {@link Recurrence} object + * @param eventRecurrence EventRecurrence object + * @return Recurrence object */ - public static List parse(EventRecurrence eventRecurrence, ScheduledAction.ActionType actionType){ - long period; - List scheduledActionList = new ArrayList(); + public static Recurrence parse(EventRecurrence eventRecurrence){ if (eventRecurrence == null) - return scheduledActionList; + return null; + PeriodType periodType; switch(eventRecurrence.freq){ - case EventRecurrence.DAILY: { - if (eventRecurrence.interval == 0) //I assume this is a bug from the picker library - period = DAY_MILLIS; - else - period = eventRecurrence.interval * DAY_MILLIS; - - ScheduledAction scheduledAction = new ScheduledAction(actionType); - scheduledAction.setPeriod(period); - parseEndTime(eventRecurrence, scheduledAction); - scheduledActionList.add(scheduledAction); - } + case EventRecurrence.DAILY: + periodType = PeriodType.DAY; break; - case EventRecurrence.WEEKLY: { - if (eventRecurrence.interval == 0) - period = WEEK_MILLIS; - else - period = eventRecurrence.interval * WEEK_MILLIS; - for (int day : eventRecurrence.byday) { - ScheduledAction scheduledAction = new ScheduledAction(actionType); - scheduledAction.setPeriod(period); - - scheduledAction.setStartTime(nextDayOfWeek(day2CalendarDay(day)).getTimeInMillis()); - parseEndTime(eventRecurrence, scheduledAction); - scheduledActionList.add(scheduledAction); - } - } - break; - - case EventRecurrence.MONTHLY: { - if (eventRecurrence.interval == 0) - period = MONTH_MILLIS; - else - period = eventRecurrence.interval * MONTH_MILLIS; - ScheduledAction event = new ScheduledAction(actionType); - event.setPeriod(period); - Calendar now = Calendar.getInstance(); - now.add(Calendar.MONTH, 1); - event.setStartTime(now.getTimeInMillis()); - parseEndTime(eventRecurrence, event); - - scheduledActionList.add(event); - } + case EventRecurrence.WEEKLY: + periodType = PeriodType.WEEK; break; - case EventRecurrence.YEARLY: { - if (eventRecurrence.interval == 0) - period = YEAR_MILLIS; - else - period = eventRecurrence.interval * YEAR_MILLIS; - ScheduledAction event = new ScheduledAction(actionType); - event.setPeriod(period); - Calendar now = Calendar.getInstance(); - now.add(Calendar.YEAR, 1); - event.setStartTime(now.getTimeInMillis()); - parseEndTime(eventRecurrence, event); - scheduledActionList.add(event); - } + case EventRecurrence.MONTHLY: + periodType = PeriodType.MONTH; + break; + + case EventRecurrence.YEARLY: + periodType = PeriodType.YEAR; + break; + + default: + periodType = PeriodType.MONTH; break; } - return scheduledActionList; + + int interval = eventRecurrence.interval == 0 ? 1 : eventRecurrence.interval; //bug from betterpickers library sometimes returns 0 as the interval + periodType.setMultiplier(interval); + Recurrence recurrence = new Recurrence(periodType); + parseEndTime(eventRecurrence, recurrence); + recurrence.setByDay(parseByDay(eventRecurrence.byday)); + recurrence.setPeriodStart(new Timestamp(eventRecurrence.startDate.toMillis(false))); + + return recurrence; } /** * Parses the end time from an EventRecurrence object and sets it to the scheduledEvent. * The end time is specified in the dialog either by number of occurences or a date. * @param eventRecurrence Event recurrence pattern obtained from dialog - * @param scheduledAction ScheduledEvent to be to updated + * @param recurrence Recurrence event to set the end period to */ - private static void parseEndTime(EventRecurrence eventRecurrence, ScheduledAction scheduledAction) { + private static void parseEndTime(EventRecurrence eventRecurrence, Recurrence recurrence) { if (eventRecurrence.until != null && eventRecurrence.until.length() > 0) { Time endTime = new Time(); endTime.parse(eventRecurrence.until); - scheduledAction.setEndTime(endTime.toMillis(false)); + recurrence.setPeriodEnd(new Timestamp(endTime.toMillis(false))); } else if (eventRecurrence.count > 0){ - scheduledAction.setTotalFrequency(eventRecurrence.count); + recurrence.setPeriodEnd(eventRecurrence.count); } } @@ -150,6 +116,51 @@ private static Calendar nextDayOfWeek(int dow) { return date; } + /** + * Parses an array of byday values to return the string concatenation of days of the week. + *

Currently only supports byDay values for weeks

+ * @param byday Array of byday values + * @return String concat of days of the week or null if {@code byday} was empty + */ + private static String parseByDay(int[] byday){ + if (byday == null || byday.length == 0){ + return null; + } + //todo: parse for month and year as well, when our dialog supports those + StringBuilder builder = new StringBuilder(); + for (int day : byday) { + switch (day) + { + case EventRecurrence.SU: + builder.append("SU"); + break; + case EventRecurrence.MO: + builder.append("MO"); + break; + case EventRecurrence.TU: + builder.append("TU"); + break; + case EventRecurrence.WE: + builder.append("WE"); + break; + case EventRecurrence.TH: + builder.append("TH"); + break; + case EventRecurrence.FR: + builder.append("FR"); + break; + case EventRecurrence.SA: + builder.append("SA"); + break; + default: + throw new RuntimeException("bad day of week: " + day); + } + builder.append(","); + } + builder.deleteCharAt(builder.length()); + return builder.toString(); + } + /** * Converts one of the SU, MO, etc. constants to the Calendar.SUNDAY * constants. btw, I think we should switch to those here too, to diff --git a/app/src/main/java/org/gnucash/android/ui/util/RecurrenceViewClickListener.java b/app/src/main/java/org/gnucash/android/ui/util/RecurrenceViewClickListener.java new file mode 100644 index 000000000..738919971 --- /dev/null +++ b/app/src/main/java/org/gnucash/android/ui/util/RecurrenceViewClickListener.java @@ -0,0 +1,66 @@ +/* + * Copyright (c) 2015 Ngewi Fet + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.gnucash.android.ui.util; + +import android.os.Bundle; +import android.support.v4.app.FragmentManager; +import android.support.v7.app.AppCompatActivity; +import android.text.format.Time; +import android.view.View; + +import com.codetroopers.betterpickers.recurrencepicker.RecurrencePickerDialogFragment; +import com.codetroopers.betterpickers.recurrencepicker.RecurrencePickerDialogFragment.OnRecurrenceSetListener; + +/** + * Shows the recurrence dialog when the recurrence view is clicked + */ +public class RecurrenceViewClickListener implements View.OnClickListener{ + private static final String FRAGMENT_TAG_RECURRENCE_PICKER = "recurrence_picker"; + + AppCompatActivity mActivity; + String mRecurrenceRule; + OnRecurrenceSetListener mRecurrenceSetListener; + + public RecurrenceViewClickListener(AppCompatActivity activity, String recurrenceRule, OnRecurrenceSetListener recurrenceSetListener){ + this.mActivity = activity; + this.mRecurrenceRule = recurrenceRule; + this.mRecurrenceSetListener = recurrenceSetListener; + } + + @Override + public void onClick(View v) { + FragmentManager fm = mActivity.getSupportFragmentManager(); + Bundle b = new Bundle(); + Time t = new Time(); + t.setToNow(); + b.putLong(RecurrencePickerDialogFragment.BUNDLE_START_TIME_MILLIS, t.toMillis(false)); + b.putString(RecurrencePickerDialogFragment.BUNDLE_TIME_ZONE, t.timezone); + + // may be more efficient to serialize and pass in EventRecurrence + b.putString(RecurrencePickerDialogFragment.BUNDLE_RRULE, mRecurrenceRule); + + RecurrencePickerDialogFragment rpd = (RecurrencePickerDialogFragment) fm.findFragmentByTag( + FRAGMENT_TAG_RECURRENCE_PICKER); + if (rpd != null) { + rpd.dismiss(); + } + rpd = new RecurrencePickerDialogFragment(); + rpd.setArguments(b); + rpd.setOnRecurrenceSetListener(mRecurrenceSetListener); + rpd.show(fm, FRAGMENT_TAG_RECURRENCE_PICKER); + } +} diff --git a/app/src/main/java/org/gnucash/android/ui/util/widget/CalculatorEditText.java b/app/src/main/java/org/gnucash/android/ui/util/widget/CalculatorEditText.java index cad60a0b6..ffc2b9db7 100644 --- a/app/src/main/java/org/gnucash/android/ui/util/widget/CalculatorEditText.java +++ b/app/src/main/java/org/gnucash/android/ui/util/widget/CalculatorEditText.java @@ -37,6 +37,8 @@ import org.gnucash.android.R; import org.gnucash.android.app.GnuCashApplication; +import org.gnucash.android.model.Money; +import org.gnucash.android.db.adapter.CommoditiesDbAdapter; import org.gnucash.android.model.Commodity; import org.gnucash.android.ui.common.FormActivity; @@ -150,7 +152,7 @@ public void onClick(View v) { setOnLongClickListener(new View.OnLongClickListener() { @Override public boolean onLongClick(View v) { - if (v != null) + if (v != null && !isInEditMode()) ((InputMethodManager) GnuCashApplication.getAppContext() .getSystemService(Activity.INPUT_METHOD_SERVICE)) .hideSoftInputFromWindow(v.getWindowToken(), 0); diff --git a/app/src/main/java/org/gnucash/android/ui/wizard/CurrencySelectFragment.java b/app/src/main/java/org/gnucash/android/ui/wizard/CurrencySelectFragment.java index 00cfe08fe..fb0203bbe 100644 --- a/app/src/main/java/org/gnucash/android/ui/wizard/CurrencySelectFragment.java +++ b/app/src/main/java/org/gnucash/android/ui/wizard/CurrencySelectFragment.java @@ -27,7 +27,7 @@ import com.tech.freak.wizardpager.ui.PageFragmentCallbacks; import org.gnucash.android.R; -import org.gnucash.android.db.CommoditiesDbAdapter; +import org.gnucash.android.db.adapter.CommoditiesDbAdapter; import org.gnucash.android.util.CommoditiesCursorAdapter; import butterknife.ButterKnife; diff --git a/app/src/main/java/org/gnucash/android/util/CommoditiesCursorAdapter.java b/app/src/main/java/org/gnucash/android/util/CommoditiesCursorAdapter.java index 0a5e246cd..ddc548950 100644 --- a/app/src/main/java/org/gnucash/android/util/CommoditiesCursorAdapter.java +++ b/app/src/main/java/org/gnucash/android/util/CommoditiesCursorAdapter.java @@ -21,15 +21,12 @@ import android.support.annotation.LayoutRes; import android.support.v4.widget.SimpleCursorAdapter; import android.text.TextUtils; -import android.view.ContextMenu; import android.view.View; import android.widget.TextView; -import org.gnucash.android.db.CommoditiesDbAdapter; +import org.gnucash.android.db.adapter.CommoditiesDbAdapter; import org.gnucash.android.db.DatabaseSchema; -import java.util.Currency; - /** * Cursor adapter for displaying list of commodities. *

You should provide the layout and the layout should contain a view with the id {@code android:id/text1}, diff --git a/app/src/main/java/org/gnucash/android/util/QualifiedAccountNameCursorAdapter.java b/app/src/main/java/org/gnucash/android/util/QualifiedAccountNameCursorAdapter.java index c2183a25c..8f4cc5feb 100644 --- a/app/src/main/java/org/gnucash/android/util/QualifiedAccountNameCursorAdapter.java +++ b/app/src/main/java/org/gnucash/android/util/QualifiedAccountNameCursorAdapter.java @@ -18,12 +18,14 @@ import android.content.Context; import android.database.Cursor; +import android.support.annotation.NonNull; import android.support.v4.widget.SimpleCursorAdapter; import android.text.TextUtils; import android.view.View; import android.widget.TextView; import org.gnucash.android.db.DatabaseSchema; +import org.gnucash.android.db.adapter.AccountsDbAdapter; /** * Cursor adapter which looks up the fully qualified account name and returns that instead of just the simple name. @@ -46,4 +48,19 @@ public void bindView(View view, Context context, Cursor cursor) { TextView textView = (TextView) view.findViewById(android.R.id.text1); textView.setEllipsize(TextUtils.TruncateAt.MIDDLE); } + + /** + * Returns the position of a given account in the adapter + * @param accountUID GUID of the account + * @return Position of the account or -1 if the account is not found + */ + public int getPosition(@NonNull String accountUID){ + long accountId = AccountsDbAdapter.getInstance().getID(accountUID); + for (int pos = 0; pos < getCount(); pos++) { + if (getItemId(pos) == accountId){ + return pos; + } + } + return -1; + } } diff --git a/app/src/main/res/drawable-hdpi/ic_dashboard_black_24dp.png b/app/src/main/res/drawable-hdpi/ic_dashboard_black_24dp.png new file mode 100644 index 0000000000000000000000000000000000000000..b832916f5e97b28900f42852d245eead209cdb4e GIT binary patch literal 126 zcmeAS@N?(olHy`uVBq!ia0vp^Dj>|k0wldT1B8K;tEY=&NCjiE0%H#odmG!D3ylpr zRt#MKj&n(KobHoyi730G@JiK#so@_R@5?=H_dhxtYH57Gm9b3LwBui8Z-dUo1Q7;? Y7^36mdKI;Vst0C>J5Hvj+t literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-xxhdpi/ic_dashboard_black_24dp.png b/app/src/main/res/drawable-xxhdpi/ic_dashboard_black_24dp.png new file mode 100644 index 0000000000000000000000000000000000000000..ad14dfeb9fcb3669d59c0077ab851f2cc95eff6b GIT binary patch literal 126 zcmeAS@N?(olHy`uVBq!ia0vp^9w5xf3?%cF6 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/activity_accounts.xml b/app/src/main/res/layout/activity_accounts.xml index 838e12f95..96bd30bae 100644 --- a/app/src/main/res/layout/activity_accounts.xml +++ b/app/src/main/res/layout/activity_accounts.xml @@ -1,6 +1,6 @@ + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/cardview_account.xml b/app/src/main/res/layout/cardview_account.xml index a689e0250..36a1452ca 100644 --- a/app/src/main/res/layout/cardview_account.xml +++ b/app/src/main/res/layout/cardview_account.xml @@ -94,14 +94,34 @@ + + \ No newline at end of file diff --git a/app/src/main/res/layout/cardview_budget.xml b/app/src/main/res/layout/cardview_budget.xml new file mode 100644 index 000000000..689d1da9c --- /dev/null +++ b/app/src/main/res/layout/cardview_budget.xml @@ -0,0 +1,107 @@ + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/cardview_budget_amount.xml b/app/src/main/res/layout/cardview_budget_amount.xml new file mode 100644 index 000000000..c920774bd --- /dev/null +++ b/app/src/main/res/layout/cardview_budget_amount.xml @@ -0,0 +1,139 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_account_form.xml b/app/src/main/res/layout/fragment_account_form.xml index 44d4f6fcb..62230c94e 100644 --- a/app/src/main/res/layout/fragment_account_form.xml +++ b/app/src/main/res/layout/fragment_account_form.xml @@ -1,6 +1,6 @@ + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_budget_form.xml b/app/src/main/res/layout/fragment_budget_form.xml new file mode 100644 index 000000000..386397e89 --- /dev/null +++ b/app/src/main/res/layout/fragment_budget_form.xml @@ -0,0 +1,121 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +