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.
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 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/fragment_budget_list.xml b/app/src/main/res/layout/fragment_budget_list.xml
new file mode 100644
index 000000000..b4617c265
--- /dev/null
+++ b/app/src/main/res/layout/fragment_budget_list.xml
@@ -0,0 +1,42 @@
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/item_budget_amount.xml b/app/src/main/res/layout/item_budget_amount.xml
new file mode 100644
index 000000000..e8393881a
--- /dev/null
+++ b/app/src/main/res/layout/item_budget_amount.xml
@@ -0,0 +1,87 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/list_item_2_lines.xml b/app/src/main/res/layout/list_item_2_lines.xml
index e9badd10e..f728b4571 100644
--- a/app/src/main/res/layout/list_item_2_lines.xml
+++ b/app/src/main/res/layout/list_item_2_lines.xml
@@ -29,7 +29,7 @@ limitations under the License.
android:layout_height="wrap_content"
android:singleLine="true"
android:ellipsize="end"
- android:text="@string/label_transaction_name"
+ tools:text="Primary text"
style="@style/ListItemText"/>
+ tools:text="Secondary text"/>
\ No newline at end of file
diff --git a/app/src/main/res/layout/nav_drawer_header.xml b/app/src/main/res/layout/nav_drawer_header.xml
index ca8e4a095..1a5962418 100644
--- a/app/src/main/res/layout/nav_drawer_header.xml
+++ b/app/src/main/res/layout/nav_drawer_header.xml
@@ -14,16 +14,13 @@
See the License for the specific language governing permissions and
limitations under the License.
-->
-
-
-
-
\ No newline at end of file
+
\ No newline at end of file
diff --git a/app/src/main/res/menu/budget_actions.xml b/app/src/main/res/menu/budget_actions.xml
new file mode 100644
index 000000000..251218781
--- /dev/null
+++ b/app/src/main/res/menu/budget_actions.xml
@@ -0,0 +1,17 @@
+
+
\ No newline at end of file
diff --git a/app/src/main/res/menu/budget_context_menu.xml b/app/src/main/res/menu/budget_context_menu.xml
new file mode 100644
index 000000000..258a69de4
--- /dev/null
+++ b/app/src/main/res/menu/budget_context_menu.xml
@@ -0,0 +1,33 @@
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/menu/nav_drawer_menu.xml b/app/src/main/res/menu/nav_drawer_menu.xml
index 5160f99c4..161cff00e 100644
--- a/app/src/main/res/menu/nav_drawer_menu.xml
+++ b/app/src/main/res/menu/nav_drawer_menu.xml
@@ -15,18 +15,22 @@
limitations under the License.
-->
\ No newline at end of file
diff --git a/app/src/main/res/values-af-rZA/strings.xml b/app/src/main/res/values-af-rZA/strings.xml
index 54aa639ba..1f422350e 100644
--- a/app/src/main/res/values-af-rZA/strings.xml
+++ b/app/src/main/res/values-af-rZA/strings.xml
@@ -385,6 +385,7 @@
No compatible apps to receive the exported transactions!Move...Duplicate
+ BudgetsCurrent monthLast 3 months
diff --git a/app/src/main/res/values-ar-rSA/strings.xml b/app/src/main/res/values-ar-rSA/strings.xml
index 107153ca9..fd7573d1d 100644
--- a/app/src/main/res/values-ar-rSA/strings.xml
+++ b/app/src/main/res/values-ar-rSA/strings.xml
@@ -405,6 +405,7 @@
No compatible apps to receive the exported transactions!Move...Duplicate
+ BudgetsCurrent monthLast 3 months
diff --git a/app/src/main/res/values-ca-rES/strings.xml b/app/src/main/res/values-ca-rES/strings.xml
index 7b935d0bc..76ad35a86 100644
--- a/app/src/main/res/values-ca-rES/strings.xml
+++ b/app/src/main/res/values-ca-rES/strings.xml
@@ -385,6 +385,7 @@
No compatible apps to receive the exported transactions!Move...Duplicate
+ BudgetsMes actualÚltims 3 mesos
diff --git a/app/src/main/res/values-cs-rCZ/strings.xml b/app/src/main/res/values-cs-rCZ/strings.xml
index 92c01bd4b..2522cfa44 100644
--- a/app/src/main/res/values-cs-rCZ/strings.xml
+++ b/app/src/main/res/values-cs-rCZ/strings.xml
@@ -390,6 +390,7 @@
No compatible apps to receive the exported transactions!Move...Duplicate
+ BudgetsCurrent monthLast 3 months
diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml
index b16e201b2..803247a15 100644
--- a/app/src/main/res/values-de/strings.xml
+++ b/app/src/main/res/values-de/strings.xml
@@ -382,6 +382,7 @@ No user-identifiable information will be collected as part of this process!No compatible apps to receive the exported transactions!
Move...Duplicate
+ BudgetsCurrent monthLast 3 months
diff --git a/app/src/main/res/values-el-rGR/strings.xml b/app/src/main/res/values-el-rGR/strings.xml
index 9b033a1bd..8d50732d8 100644
--- a/app/src/main/res/values-el-rGR/strings.xml
+++ b/app/src/main/res/values-el-rGR/strings.xml
@@ -396,6 +396,7 @@ No user-identifiable information will be collected as part of this process!
No compatible apps to receive the exported transactions!Move...Duplicate
+ BudgetsCurrent monthLast 3 months
diff --git a/app/src/main/res/values-en-rGB/strings.xml b/app/src/main/res/values-en-rGB/strings.xml
index 54aa639ba..1f422350e 100644
--- a/app/src/main/res/values-en-rGB/strings.xml
+++ b/app/src/main/res/values-en-rGB/strings.xml
@@ -385,6 +385,7 @@
No compatible apps to receive the exported transactions!Move...Duplicate
+ BudgetsCurrent monthLast 3 months
diff --git a/app/src/main/res/values-es-rMX/strings.xml b/app/src/main/res/values-es-rMX/strings.xml
index 276b7d0e6..c261ae5af 100644
--- a/app/src/main/res/values-es-rMX/strings.xml
+++ b/app/src/main/res/values-es-rMX/strings.xml
@@ -382,6 +382,7 @@ Este proceso solo recoge información que no permite identificar al usuarioNo compatible apps to receive the exported transactions!Move...Duplicate
+ BudgetsMes actualÚltimos 3 meses
diff --git a/app/src/main/res/values-es/strings.xml b/app/src/main/res/values-es/strings.xml
index d0f5da332..1fa57f89e 100644
--- a/app/src/main/res/values-es/strings.xml
+++ b/app/src/main/res/values-es/strings.xml
@@ -382,6 +382,7 @@ Este proceso solo recoge información que no permite identificar al usuario¡No hay aplicaciones compatibles para recibir las transacciones exportadas!Move...Duplicate
+ BudgetsMes actualÚltimos 3 meses
diff --git a/app/src/main/res/values-fi-rFI/strings.xml b/app/src/main/res/values-fi-rFI/strings.xml
index 54aa639ba..1f422350e 100644
--- a/app/src/main/res/values-fi-rFI/strings.xml
+++ b/app/src/main/res/values-fi-rFI/strings.xml
@@ -385,6 +385,7 @@
No compatible apps to receive the exported transactions!Move...Duplicate
+ BudgetsCurrent monthLast 3 months
diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml
index 547ffb19c..0d9415651 100644
--- a/app/src/main/res/values-fr/strings.xml
+++ b/app/src/main/res/values-fr/strings.xml
@@ -382,6 +382,7 @@ Aucune information permettant d\'identifier l\'utilisateur ne sera recueillis da
No compatible apps to receive the exported transactions!Move...Duplicate
+ BudgetsMois actuel3 derniers mois
diff --git a/app/src/main/res/values-hu-rHU/strings.xml b/app/src/main/res/values-hu-rHU/strings.xml
index f6e4e0ffa..7fbd2da03 100644
--- a/app/src/main/res/values-hu-rHU/strings.xml
+++ b/app/src/main/res/values-hu-rHU/strings.xml
@@ -387,6 +387,7 @@ No user-identifiable information will be collected as part of this process!
No compatible apps to receive the exported transactions!Move...Duplicate
+ BudgetsCurrent monthLast 3 months
diff --git a/app/src/main/res/values-it-rIT/strings.xml b/app/src/main/res/values-it-rIT/strings.xml
index 313ddee27..8b22157d8 100644
--- a/app/src/main/res/values-it-rIT/strings.xml
+++ b/app/src/main/res/values-it-rIT/strings.xml
@@ -386,6 +386,7 @@ No user-identifiable information will be collected as part of this process!
No compatible apps to receive the exported transactions!Move...Duplicate
+ BudgetsMese correnteUltimi 3 mesi
diff --git a/app/src/main/res/values-iw-rIL/strings.xml b/app/src/main/res/values-iw-rIL/strings.xml
index 54aa639ba..1f422350e 100644
--- a/app/src/main/res/values-iw-rIL/strings.xml
+++ b/app/src/main/res/values-iw-rIL/strings.xml
@@ -385,6 +385,7 @@
No compatible apps to receive the exported transactions!Move...Duplicate
+ BudgetsCurrent monthLast 3 months
diff --git a/app/src/main/res/values-ja-rJP/strings.xml b/app/src/main/res/values-ja-rJP/strings.xml
index 52fe7ce51..3e24c66b2 100644
--- a/app/src/main/res/values-ja-rJP/strings.xml
+++ b/app/src/main/res/values-ja-rJP/strings.xml
@@ -380,6 +380,7 @@
No compatible apps to receive the exported transactions!Move...Duplicate
+ BudgetsCurrent monthLast 3 months
diff --git a/app/src/main/res/values-ko-rKR/strings.xml b/app/src/main/res/values-ko-rKR/strings.xml
index 1656ad737..237687083 100644
--- a/app/src/main/res/values-ko-rKR/strings.xml
+++ b/app/src/main/res/values-ko-rKR/strings.xml
@@ -380,6 +380,7 @@
No compatible apps to receive the exported transactions!Move...Duplicate
+ BudgetsCurrent monthLast 3 months
diff --git a/app/src/main/res/values-nl-rNL/strings.xml b/app/src/main/res/values-nl-rNL/strings.xml
index 6654dae56..0adb2052c 100644
--- a/app/src/main/res/values-nl-rNL/strings.xml
+++ b/app/src/main/res/values-nl-rNL/strings.xml
@@ -386,6 +386,7 @@ No user-identifiable information will be collected as part of this process!
No compatible apps to receive the exported transactions!Move...Duplicate
+ BudgetsHuidige maandAfgelopen 3 maanden
diff --git a/app/src/main/res/values-no-rNO/strings.xml b/app/src/main/res/values-no-rNO/strings.xml
index 05ee7407b..cab3c1fd7 100644
--- a/app/src/main/res/values-no-rNO/strings.xml
+++ b/app/src/main/res/values-no-rNO/strings.xml
@@ -376,6 +376,7 @@
Ingen kompatible apper til å motta de eksporterte transaksjonene!Move...Duplicate
+ BudgetsGjeldende månedSiste 3 måneder
diff --git a/app/src/main/res/values-pl-rPL/strings.xml b/app/src/main/res/values-pl-rPL/strings.xml
index aa7366017..a1bddedc8 100644
--- a/app/src/main/res/values-pl-rPL/strings.xml
+++ b/app/src/main/res/values-pl-rPL/strings.xml
@@ -383,6 +383,7 @@
No compatible apps to receive the exported transactions!Move...Duplicate
+ BudgetsBieżący miesiącOstatnie 3 miesiące
diff --git a/app/src/main/res/values-pt-rBR/strings.xml b/app/src/main/res/values-pt-rBR/strings.xml
index 443b85b81..690459483 100644
--- a/app/src/main/res/values-pt-rBR/strings.xml
+++ b/app/src/main/res/values-pt-rBR/strings.xml
@@ -380,6 +380,7 @@ Neste processo não serão recolhidas informações do utilizador!Não existem apps compatíveis para receber as transações exportadas!Mover...Duplicar
+ BudgetsMês atualLast 3 meses
diff --git a/app/src/main/res/values-pt-rPT/strings.xml b/app/src/main/res/values-pt-rPT/strings.xml
index ce055634b..af4dd0336 100644
--- a/app/src/main/res/values-pt-rPT/strings.xml
+++ b/app/src/main/res/values-pt-rPT/strings.xml
@@ -380,6 +380,7 @@ Neste processo não serão recolhidas informações do utilizador!Não existem aplicações compatíveis para receber as transações exportadas!Mover...Duplicar
+ BudgetsMês actualÚltimos 3 meses
diff --git a/app/src/main/res/values-ro-rRO/strings.xml b/app/src/main/res/values-ro-rRO/strings.xml
index 92c01bd4b..2522cfa44 100644
--- a/app/src/main/res/values-ro-rRO/strings.xml
+++ b/app/src/main/res/values-ro-rRO/strings.xml
@@ -390,6 +390,7 @@
No compatible apps to receive the exported transactions!Move...Duplicate
+ BudgetsCurrent monthLast 3 months
diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml
index 764b17f0d..429503f57 100644
--- a/app/src/main/res/values-ru/strings.xml
+++ b/app/src/main/res/values-ru/strings.xml
@@ -388,6 +388,7 @@
No compatible apps to receive the exported transactions!Move...Duplicate
+ BudgetsТекущий месяцПоследний квартал
diff --git a/app/src/main/res/values-sr-rSP/strings.xml b/app/src/main/res/values-sr-rSP/strings.xml
index c92cb1056..413e6b9cb 100644
--- a/app/src/main/res/values-sr-rSP/strings.xml
+++ b/app/src/main/res/values-sr-rSP/strings.xml
@@ -390,6 +390,7 @@
No compatible apps to receive the exported transactions!Move...Duplicate
+ BudgetsCurrent monthLast 3 months
diff --git a/app/src/main/res/values-sv-rSE/strings.xml b/app/src/main/res/values-sv-rSE/strings.xml
index 54aa639ba..1f422350e 100644
--- a/app/src/main/res/values-sv-rSE/strings.xml
+++ b/app/src/main/res/values-sv-rSE/strings.xml
@@ -385,6 +385,7 @@
No compatible apps to receive the exported transactions!Move...Duplicate
+ BudgetsCurrent monthLast 3 months
diff --git a/app/src/main/res/values-tr-rTR/strings.xml b/app/src/main/res/values-tr-rTR/strings.xml
index a9c761986..6e8132c50 100644
--- a/app/src/main/res/values-tr-rTR/strings.xml
+++ b/app/src/main/res/values-tr-rTR/strings.xml
@@ -385,6 +385,7 @@
No compatible apps to receive the exported transactions!Move...Duplicate
+ BudgetsCurrent monthLast 3 months
diff --git a/app/src/main/res/values-uk-rUA/strings.xml b/app/src/main/res/values-uk-rUA/strings.xml
index 0f8436621..0015fd165 100644
--- a/app/src/main/res/values-uk-rUA/strings.xml
+++ b/app/src/main/res/values-uk-rUA/strings.xml
@@ -386,6 +386,7 @@
No compatible apps to receive the exported transactions!Move...Duplicate
+ BudgetsПоточний місяцьОстанній квартал
diff --git a/app/src/main/res/values-vi-rVN/strings.xml b/app/src/main/res/values-vi-rVN/strings.xml
index 1656ad737..237687083 100644
--- a/app/src/main/res/values-vi-rVN/strings.xml
+++ b/app/src/main/res/values-vi-rVN/strings.xml
@@ -380,6 +380,7 @@
No compatible apps to receive the exported transactions!Move...Duplicate
+ BudgetsCurrent monthLast 3 months
diff --git a/app/src/main/res/values-zh-rCN/strings.xml b/app/src/main/res/values-zh-rCN/strings.xml
index 83f48ddfb..019b9a6de 100644
--- a/app/src/main/res/values-zh-rCN/strings.xml
+++ b/app/src/main/res/values-zh-rCN/strings.xml
@@ -377,6 +377,7 @@ No user-identifiable information will be collected as part of this process!
No compatible apps to receive the exported transactions!Move...Duplicate
+ BudgetsCurrent monthLast 3 months
diff --git a/app/src/main/res/values-zh-rTW/strings.xml b/app/src/main/res/values-zh-rTW/strings.xml
index fc5f6b5d7..0a3e4c1c5 100644
--- a/app/src/main/res/values-zh-rTW/strings.xml
+++ b/app/src/main/res/values-zh-rTW/strings.xml
@@ -377,6 +377,7 @@ No user-identifiable information will be collected as part of this process!
No compatible apps to receive the exported transactions!Move...Duplicate
+ BudgetsCurrent monthLast 3 months
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index 3f0394ec5..75c837d82 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -391,6 +391,7 @@
No compatible apps to receive the exported transactions!Move...Duplicate
+ BudgetsCurrent monthLast 3 months
diff --git a/app/src/test/java/org/gnucash/android/test/unit/db/AccountsDbAdapterTest.java b/app/src/test/java/org/gnucash/android/test/unit/db/AccountsDbAdapterTest.java
index 260f98b5c..cbb6ab786 100644
--- a/app/src/test/java/org/gnucash/android/test/unit/db/AccountsDbAdapterTest.java
+++ b/app/src/test/java/org/gnucash/android/test/unit/db/AccountsDbAdapterTest.java
@@ -4,16 +4,20 @@
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.CommoditiesDbAdapter;
-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.importer.GncXmlImporter;
import org.gnucash.android.model.Account;
import org.gnucash.android.model.AccountType;
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.Split;
import org.gnucash.android.model.Transaction;
@@ -222,6 +226,7 @@ public void shouldClearAllTablesWhenDeletingAllAccounts(){
ScheduledAction scheduledAction = new ScheduledAction(ScheduledAction.ActionType.BACKUP);
scheduledAction.setActionUID("Test-uid");
+ scheduledAction.setRecurrence(new Recurrence(PeriodType.WEEK));
ScheduledActionDbAdapter scheduledActionDbAdapter = ScheduledActionDbAdapter.getInstance();
scheduledActionDbAdapter.addRecord(scheduledAction);
@@ -232,6 +237,9 @@ public void shouldClearAllTablesWhenDeletingAllAccounts(){
assertThat(mTransactionsDbAdapter.getRecordsCount()).isZero();
assertThat(mSplitsDbAdapter.getRecordsCount()).isZero();
assertThat(scheduledActionDbAdapter.getRecordsCount()).isZero();
+ assertThat(PricesDbAdapter.getInstance().getRecordsCount()).isZero();
+ assertThat(BudgetsDbAdapter.getInstance().getRecordsCount()).isZero();
+ assertThat(CommoditiesDbAdapter.getInstance().getRecordsCount()).isGreaterThan(50); //commodities should remain
}
@Test
@@ -376,6 +384,52 @@ public void shouldCreateImbalanceAccountOnDemand(){
assertThat(mAccountsDbAdapter.getRecordsCount()).isEqualTo(2);
}
+ @Test
+ public void editingAccountShouldNotDeleteTemplateSplits(){
+ Account account = new Account("First", Commodity.EUR);
+ Account transferAccount = new Account("Transfer", Commodity.EUR);
+
+ mAccountsDbAdapter.addRecord(account);
+ mAccountsDbAdapter.addRecord(transferAccount);
+
+ assertThat(mAccountsDbAdapter.getRecordsCount()).isEqualTo(3); //plus root account
+
+ Money money = new Money(BigDecimal.TEN, Commodity.EUR);
+ Transaction transaction = new Transaction("Template");
+ transaction.setTemplate(true);
+ transaction.setCommodity(Commodity.EUR);
+ Split split = new Split(money, account.getUID());
+ transaction.addSplit(split);
+ transaction.addSplit(split.createPair(transferAccount.getUID()));
+
+ mTransactionsDbAdapter.addRecord(transaction);
+ List transactions = mTransactionsDbAdapter.getAllRecords();
+
+ assertThat(mTransactionsDbAdapter.getScheduledTransactionsForAccount(account.getUID())).hasSize(1);
+
+ //edit the account
+ account.setName("Edited account");
+ mAccountsDbAdapter.addRecord(account);
+
+ assertThat(mTransactionsDbAdapter.getScheduledTransactionsForAccount(account.getUID())).hasSize(1);
+ assertThat(mSplitsDbAdapter.getSplitsForTransaction(transaction.getUID())).hasSize(2);
+ }
+
+ @Test
+ public void testGetCurrenciesInUse(){
+ int expectedSize = 0;
+ List currencies = mAccountsDbAdapter.getCurrenciesInUse();
+ assertThat(currencies).hasSize(expectedSize);
+
+ Account account = new Account("Dummy", Commodity.USD);
+ mAccountsDbAdapter.addRecord(account);
+ assertThat(mAccountsDbAdapter.getCurrenciesInUse()).hasSize(++expectedSize);
+
+ account = new Account("Dummy", Commodity.EUR);
+ mAccountsDbAdapter.addRecord(account);
+ assertThat(mAccountsDbAdapter.getCurrenciesInUse()).hasSize(++expectedSize);
+
+ }
/**
* Opening an XML file should set the default currency to that used by the most accounts in the file
diff --git a/app/src/test/java/org/gnucash/android/test/unit/db/BudgetsDbAdapterTest.java b/app/src/test/java/org/gnucash/android/test/unit/db/BudgetsDbAdapterTest.java
new file mode 100644
index 000000000..de4c4ccd7
--- /dev/null
+++ b/app/src/test/java/org/gnucash/android/test/unit/db/BudgetsDbAdapterTest.java
@@ -0,0 +1,173 @@
+/*
+ * 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.test.unit.db;
+
+import android.support.annotation.NonNull;
+
+import org.gnucash.android.BuildConfig;
+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.RecurrenceDbAdapter;
+import org.gnucash.android.model.Account;
+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.test.unit.util.GnucashTestRunner;
+import org.gnucash.android.test.unit.util.ShadowCrashlytics;
+import org.gnucash.android.test.unit.util.ShadowUserVoice;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.annotation.Config;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+/**
+ * Tests for the budgets database adapter
+ */
+@RunWith(GnucashTestRunner.class) //package is required so that resources can be found in dev mode
+@Config(constants = BuildConfig.class, sdk = 21, packageName = "org.gnucash.android", shadows = {ShadowCrashlytics.class, ShadowUserVoice.class})
+public class BudgetsDbAdapterTest {
+
+ private BudgetsDbAdapter mBudgetsDbAdapter;
+ private RecurrenceDbAdapter mRecurrenceDbAdapter;
+ private BudgetAmountsDbAdapter mBudgetAmountsDbAdapter;
+ private AccountsDbAdapter mAccountsDbAdapter;
+
+ private Account mAccount;
+ private Account mSecondAccount;
+
+ @Before
+ public void setUp(){
+ mAccountsDbAdapter = AccountsDbAdapter.getInstance();
+ mBudgetsDbAdapter = BudgetsDbAdapter.getInstance();
+ mBudgetAmountsDbAdapter = BudgetAmountsDbAdapter.getInstance();
+ mRecurrenceDbAdapter = RecurrenceDbAdapter.getInstance();
+
+ mAccount = new Account("Budgeted account");
+ mSecondAccount = new Account("Another account");
+ mAccountsDbAdapter.addRecord(mAccount);
+ mAccountsDbAdapter.addRecord(mSecondAccount);
+ }
+
+ @After
+ public void tearDown(){
+ mBudgetsDbAdapter.deleteAllRecords();
+ mBudgetAmountsDbAdapter.deleteAllRecords();
+ mRecurrenceDbAdapter.deleteAllRecords();
+ }
+
+ @Test
+ public void testAddingBudget(){
+ assertThat(mBudgetsDbAdapter.getRecordsCount()).isZero();
+ assertThat(mBudgetAmountsDbAdapter.getRecordsCount()).isZero();
+ assertThat(mRecurrenceDbAdapter.getRecordsCount()).isZero();
+
+ Budget budget = new Budget("Test");
+ budget.addBudgetAmount(new BudgetAmount(Money.getZeroInstance(), mAccount.getUID()));
+ budget.addBudgetAmount(new BudgetAmount(new Money("10", Money.DEFAULT_CURRENCY_CODE), mSecondAccount.getUID()));
+ Recurrence recurrence = new Recurrence(PeriodType.MONTH);
+ budget.setRecurrence(recurrence);
+
+ mBudgetsDbAdapter.addRecord(budget);
+ assertThat(mBudgetsDbAdapter.getRecordsCount()).isEqualTo(1);
+ assertThat(mBudgetAmountsDbAdapter.getRecordsCount()).isEqualTo(2);
+ assertThat(mRecurrenceDbAdapter.getRecordsCount()).isEqualTo(1);
+
+ budget.getBudgetAmounts().clear();
+ BudgetAmount budgetAmount = new BudgetAmount(new Money("5", Money.DEFAULT_CURRENCY_CODE), mAccount.getUID());
+ budget.addBudgetAmount(budgetAmount);
+ mBudgetsDbAdapter.addRecord(budget);
+
+ assertThat(mBudgetAmountsDbAdapter.getRecordsCount()).isEqualTo(1);
+ assertThat(mBudgetAmountsDbAdapter.getAllRecords().get(0).getUID()).isEqualTo(budgetAmount.getUID());
+ }
+
+ /**
+ * Test that when bulk adding budgets, all the associated budgetAmounts and recurrences are saved
+ */
+ @Test
+ public void testBulkAddBudgets(){
+ assertThat(mBudgetsDbAdapter.getRecordsCount()).isZero();
+ assertThat(mBudgetAmountsDbAdapter.getRecordsCount()).isZero();
+ assertThat(mRecurrenceDbAdapter.getRecordsCount()).isZero();
+
+ List budgets = bulkCreateBudgets();
+
+ mBudgetsDbAdapter.bulkAddRecords(budgets);
+
+ assertThat(mBudgetsDbAdapter.getRecordsCount()).isEqualTo(2);
+ assertThat(mBudgetAmountsDbAdapter.getRecordsCount()).isEqualTo(3);
+ assertThat(mRecurrenceDbAdapter.getRecordsCount()).isEqualTo(2);
+
+ }
+
+ @Test
+ public void testGetAccountBudgets(){
+ mBudgetsDbAdapter.bulkAddRecords(bulkCreateBudgets());
+
+ List budgets = mBudgetsDbAdapter.getAccountBudgets(mAccount.getUID());
+ assertThat(budgets).hasSize(2);
+
+ assertThat(mBudgetsDbAdapter.getAccountBudgets(mSecondAccount.getUID())).hasSize(1);
+ }
+
+ @NonNull
+ private List bulkCreateBudgets() {
+ List budgets = new ArrayList<>();
+ Budget budget = new Budget("", new Recurrence(PeriodType.MONTH));
+ budget.addBudgetAmount(new BudgetAmount(Money.getZeroInstance(), mAccount.getUID()));
+ budgets.add(budget);
+
+ budget = new Budget("Random", new Recurrence(PeriodType.WEEK));
+ budget.addBudgetAmount(new BudgetAmount(new Money("10.50", Money.DEFAULT_CURRENCY_CODE), mAccount.getUID()));
+ budget.addBudgetAmount(new BudgetAmount(new Money("32.35", Money.DEFAULT_CURRENCY_CODE), mSecondAccount.getUID()));
+
+ budgets.add(budget);
+ return budgets;
+ }
+
+ @Test(expected = NullPointerException.class)
+ public void savingBudget_shouldRequireExistingAccount(){
+ Budget budget = new Budget("");
+ budget.addBudgetAmount(new BudgetAmount(Money.getZeroInstance(), "unknown-account"));
+
+ mBudgetsDbAdapter.addRecord(budget);
+ }
+
+ @Test(expected = NullPointerException.class)
+ public void savingBudget_shouldRequireRecurrence(){
+ Budget budget = new Budget("");
+ budget.addBudgetAmount(new BudgetAmount(Money.getZeroInstance(), mAccount.getUID()));
+
+ mBudgetsDbAdapter.addRecord(budget);
+ }
+
+ @Test(expected = IllegalArgumentException.class)
+ public void savingBudget_shouldRequireBudgetAmount(){
+ Budget budget = new Budget("");
+ budget.setRecurrence(new Recurrence(PeriodType.MONTH));
+
+ mBudgetsDbAdapter.addRecord(budget);
+ }
+}
diff --git a/app/src/test/java/org/gnucash/android/test/unit/db/PriceDbAdapterTest.java b/app/src/test/java/org/gnucash/android/test/unit/db/PriceDbAdapterTest.java
new file mode 100644
index 000000000..8264390a3
--- /dev/null
+++ b/app/src/test/java/org/gnucash/android/test/unit/db/PriceDbAdapterTest.java
@@ -0,0 +1,60 @@
+package org.gnucash.android.test.unit.db;
+
+import org.gnucash.android.BuildConfig;
+import org.gnucash.android.db.adapter.CommoditiesDbAdapter;
+import org.gnucash.android.db.adapter.PricesDbAdapter;
+import org.gnucash.android.model.Price;
+import org.gnucash.android.test.unit.util.GnucashTestRunner;
+import org.gnucash.android.test.unit.util.ShadowCrashlytics;
+import org.gnucash.android.test.unit.util.ShadowUserVoice;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.annotation.Config;
+
+import static org.assertj.core.api.Assertions.assertThat;
+/**
+ * Test price functions
+ */
+@RunWith(GnucashTestRunner.class) //package is required so that resources can be found in dev mode
+@Config(constants = BuildConfig.class, sdk = 21, packageName = "org.gnucash.android", shadows = {ShadowCrashlytics.class, ShadowUserVoice.class})
+public class PriceDbAdapterTest {
+
+ /**
+ * The price table should override price for any commodity/currency pair
+ * todo: maybe move this to UI testing. Not sure how Robolectric handles this
+ */
+ @Test
+ public void shouldOnlySaveOnePricePerCommodityPair(){
+ String commodityUID = CommoditiesDbAdapter.getInstance().getCommodityUID("EUR");
+ String currencyUID = CommoditiesDbAdapter.getInstance().getCommodityUID("USD");
+ Price price = new Price(commodityUID, currencyUID);
+ price.setValueNum(134);
+ price.setValueDenom(100);
+
+ PricesDbAdapter pricesDbAdapter = PricesDbAdapter.getInstance();
+ pricesDbAdapter.addRecord(price);
+
+ price = pricesDbAdapter.getRecord(price.getUID());
+ assertThat(pricesDbAdapter.getRecordsCount()).isEqualTo(1);
+ assertThat(price.getValueNum()).isEqualTo(134);
+
+ Price price1 = new Price(commodityUID, currencyUID);
+ price1.setValueNum(187);
+ price1.setValueDenom(100);
+ pricesDbAdapter.addRecord(price1);
+
+ assertThat(pricesDbAdapter.getRecordsCount()).isEqualTo(1);
+ Price savedPrice = pricesDbAdapter.getAllRecords().get(0);
+ assertThat(savedPrice.getUID()).isEqualTo(price1.getUID()); //different records
+ assertThat(savedPrice.getValueNum()).isEqualTo(187);
+ assertThat(savedPrice.getValueDenom()).isEqualTo(100);
+
+
+ Price price2 = new Price(currencyUID, commodityUID);
+ price2.setValueNum(190);
+ price2.setValueDenom(100);
+ pricesDbAdapter.addRecord(price2);
+
+ assertThat(pricesDbAdapter.getRecordsCount()).isEqualTo(2);
+ }
+}
diff --git a/app/src/test/java/org/gnucash/android/test/unit/db/ScheduledActionDbAdapterTest.java b/app/src/test/java/org/gnucash/android/test/unit/db/ScheduledActionDbAdapterTest.java
new file mode 100644
index 000000000..1f93e745a
--- /dev/null
+++ b/app/src/test/java/org/gnucash/android/test/unit/db/ScheduledActionDbAdapterTest.java
@@ -0,0 +1,73 @@
+package org.gnucash.android.test.unit.db;
+
+import org.gnucash.android.BuildConfig;
+import org.gnucash.android.db.adapter.ScheduledActionDbAdapter;
+import org.gnucash.android.model.BaseModel;
+import org.gnucash.android.model.PeriodType;
+import org.gnucash.android.model.Recurrence;
+import org.gnucash.android.model.ScheduledAction;
+import org.gnucash.android.test.unit.util.GnucashTestRunner;
+import org.gnucash.android.test.unit.util.ShadowCrashlytics;
+import org.gnucash.android.test.unit.util.ShadowUserVoice;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.annotation.Config;
+
+import java.util.List;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+/**
+ * Test the scheduled actions database adapter
+ */
+@RunWith(GnucashTestRunner.class) //package is required so that resources can be found in dev mode
+@Config(constants = BuildConfig.class, sdk = 21, packageName = "org.gnucash.android", shadows = {ShadowCrashlytics.class, ShadowUserVoice.class})
+public class ScheduledActionDbAdapterTest {
+
+ ScheduledActionDbAdapter mScheduledActionDbAdapter;
+
+ @Before
+ public void setUp(){
+ mScheduledActionDbAdapter = ScheduledActionDbAdapter.getInstance();
+ }
+
+ public void shouldFetchOnlyEnabledScheduledActions(){
+ ScheduledAction scheduledAction = new ScheduledAction(ScheduledAction.ActionType.TRANSACTION);
+ scheduledAction.setRecurrence(new Recurrence(PeriodType.MONTH));
+ scheduledAction.setEnabled(false);
+
+ mScheduledActionDbAdapter.addRecord(scheduledAction);
+
+ scheduledAction = new ScheduledAction(ScheduledAction.ActionType.TRANSACTION);
+ scheduledAction.setRecurrence(new Recurrence(PeriodType.WEEK));
+ mScheduledActionDbAdapter.addRecord(scheduledAction);
+
+ assertThat(mScheduledActionDbAdapter.getAllRecords()).hasSize(2);
+
+ List enabledActions = mScheduledActionDbAdapter.getAllEnabledScheduledActions();
+ assertThat(enabledActions).hasSize(1);
+ assertThat(enabledActions.get(0).getRecurrence().getPeriodType()).isEqualTo(PeriodType.WEEK);
+ }
+
+ @Test(expected = NullPointerException.class) //no recurrence is set
+ public void everyScheduledActionShouldHaveRecurrence(){
+ ScheduledAction scheduledAction = new ScheduledAction(ScheduledAction.ActionType.TRANSACTION);
+ scheduledAction.setActionUID(BaseModel.generateUID());
+ mScheduledActionDbAdapter.addRecord(scheduledAction);
+ }
+
+ @Test
+ public void testGenerateRepeatString(){
+ ScheduledAction scheduledAction = new ScheduledAction(ScheduledAction.ActionType.TRANSACTION);
+ PeriodType periodType = PeriodType.MONTH;
+ periodType.setMultiplier(2);
+ scheduledAction.setRecurrence(new Recurrence(periodType));
+ scheduledAction.setTotalFrequency(4);
+
+ String repeatString = "Every 2 months, for 4 times";
+ assertThat(scheduledAction.getRepeatString().trim()).isEqualTo(repeatString);
+
+ }
+
+}
diff --git a/app/src/test/java/org/gnucash/android/test/unit/db/SplitsDbAdapterTest.java b/app/src/test/java/org/gnucash/android/test/unit/db/SplitsDbAdapterTest.java
index 1685ee242..df3b954cd 100644
--- a/app/src/test/java/org/gnucash/android/test/unit/db/SplitsDbAdapterTest.java
+++ b/app/src/test/java/org/gnucash/android/test/unit/db/SplitsDbAdapterTest.java
@@ -1,12 +1,27 @@
+/*
+ * 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.test.unit.db;
import android.database.sqlite.SQLiteException;
import org.gnucash.android.BuildConfig;
-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.Account;
import org.gnucash.android.model.Money;
import org.gnucash.android.model.Split;
diff --git a/app/src/test/java/org/gnucash/android/test/unit/db/TransactionsDbAdapterTest.java b/app/src/test/java/org/gnucash/android/test/unit/db/TransactionsDbAdapterTest.java
index 6b5432c77..2fedbe0cf 100644
--- a/app/src/test/java/org/gnucash/android/test/unit/db/TransactionsDbAdapterTest.java
+++ b/app/src/test/java/org/gnucash/android/test/unit/db/TransactionsDbAdapterTest.java
@@ -2,9 +2,9 @@
import org.assertj.core.data.Index;
import org.gnucash.android.BuildConfig;
-import org.gnucash.android.db.AccountsDbAdapter;
-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.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/test/java/org/gnucash/android/test/unit/model/BudgetTest.java b/app/src/test/java/org/gnucash/android/test/unit/model/BudgetTest.java
new file mode 100644
index 000000000..7b2c4bcc0
--- /dev/null
+++ b/app/src/test/java/org/gnucash/android/test/unit/model/BudgetTest.java
@@ -0,0 +1,186 @@
+/*
+ * 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.test.unit.model;
+
+import org.gnucash.android.model.Budget;
+import org.gnucash.android.model.BudgetAmount;
+import org.gnucash.android.model.Money;
+import org.junit.Test;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+/**
+ * Tests for budgets
+ */
+public class BudgetTest {
+
+ @Test
+ public void addingBudgetAmount_shouldSetBudgetUID(){
+ Budget budget = new Budget("Test");
+
+ assertThat(budget.getBudgetAmounts()).isNotNull();
+ BudgetAmount budgetAmount = new BudgetAmount(Money.getZeroInstance(), "test");
+ budget.addBudgetAmount(budgetAmount);
+
+ assertThat(budget.getBudgetAmounts()).hasSize(1);
+ assertThat(budgetAmount.getBudgetUID()).isEqualTo(budget.getUID());
+
+ //setting a whole list should also set the budget UIDs
+ List budgetAmounts = new ArrayList<>();
+ budgetAmounts.add(new BudgetAmount(Money.getZeroInstance(),"test"));
+ budgetAmounts.add(new BudgetAmount(Money.getZeroInstance(), "second"));
+
+ budget.setBudgetAmounts(budgetAmounts);
+
+ assertThat(budget.getBudgetAmounts()).extracting("mBudgetUID")
+ .contains(budget.getUID());
+ }
+
+ @Test
+ public void shouldComputeAbsoluteAmountSum(){
+ Budget budget = new Budget("Test");
+ Money accountAmount = new Money("-20", "USD");
+ BudgetAmount budgetAmount = new BudgetAmount(accountAmount, "account1");
+ BudgetAmount budgetAmount1 = new BudgetAmount(new Money("10", "USD"), "account2");
+
+ budget.addBudgetAmount(budgetAmount);
+ budget.addBudgetAmount(budgetAmount1);
+
+ assertThat(budget.getAmount("account1")).isEqualTo(accountAmount.abs());
+ assertThat(budget.getAmountSum()).isEqualTo(new Money("30", "USD"));
+ }
+
+ /**
+ * Tests that the method {@link Budget#getCompactedBudgetAmounts()} does not aggregate
+ * {@link BudgetAmount}s which have different money amounts
+ */
+ @Test
+ public void shouldNotCompactBudgetAmountsWithDifferentAmounts(){
+ Budget budget = new Budget("Test");
+ budget.setNumberOfPeriods(6);
+ BudgetAmount budgetAmount = new BudgetAmount(new Money("10", "USD"), "test");
+ budgetAmount.setPeriodNum(1);
+ budget.addBudgetAmount(budgetAmount);
+
+ budgetAmount = new BudgetAmount(new Money("15", "USD"), "test");
+ budgetAmount.setPeriodNum(2);
+ budget.addBudgetAmount(budgetAmount);
+
+ budgetAmount = new BudgetAmount(new Money("5", "USD"), "secondAccount");
+ budgetAmount.setPeriodNum(5);
+ budget.addBudgetAmount(budgetAmount);
+
+ List compactedBudgetAmounts = budget.getCompactedBudgetAmounts();
+ assertThat(compactedBudgetAmounts).hasSize(3);
+ assertThat(compactedBudgetAmounts).extracting("mAccountUID")
+ .contains("test", "secondAccount");
+
+ assertThat(compactedBudgetAmounts).extracting("mPeriodNum")
+ .contains(1L, 2L, 5L).doesNotContain(-1L);
+ }
+
+ /**
+ * Tests that the method {@link Budget#getCompactedBudgetAmounts()} aggregates {@link BudgetAmount}s
+ * with the same amount but leaves others untouched
+ */
+ @Test
+ public void addingSameAmounts_shouldCompactOnRetrieval(){
+ Budget budget = new Budget("Test");
+ budget.setNumberOfPeriods(6);
+ BudgetAmount budgetAmount = new BudgetAmount(new Money("10", "USD"), "first");
+ budgetAmount.setPeriodNum(1);
+ budget.addBudgetAmount(budgetAmount);
+
+ budgetAmount = new BudgetAmount(new Money("10", "USD"), "first");
+ budgetAmount.setPeriodNum(2);
+ budget.addBudgetAmount(budgetAmount);
+
+ budgetAmount = new BudgetAmount(new Money("10", "USD"), "first");
+ budgetAmount.setPeriodNum(5);
+ budget.addBudgetAmount(budgetAmount);
+
+ budgetAmount = new BudgetAmount(new Money("10", "EUR"), "second");
+ budgetAmount.setPeriodNum(4);
+ budget.addBudgetAmount(budgetAmount);
+
+ budgetAmount = new BudgetAmount(new Money("13", "EUR"), "third");
+ budgetAmount.setPeriodNum(-1);
+ budget.addBudgetAmount(budgetAmount);
+
+ List compactedBudgetAmounts = budget.getCompactedBudgetAmounts();
+
+ assertThat(compactedBudgetAmounts).hasSize(3);
+ assertThat(compactedBudgetAmounts).extracting("mPeriodNum").hasSize(3)
+ .contains(-1L, 4L).doesNotContain(1L, 2L, 3L);
+
+ assertThat(compactedBudgetAmounts).extracting("mAccountUID").hasSize(3)
+ .contains("first", "second", "third");
+
+ }
+
+ /**
+ * Test that when we set a periodNumber of -1 to a budget amount, the method {@link Budget#getExpandedBudgetAmounts()}
+ * should create new budget amounts for each of the periods in the budgeting period
+ */
+ @Test
+ public void addingNegativePeriodNum_shouldExpandOnRetrieval(){
+ Budget budget = new Budget("Test");
+ budget.setNumberOfPeriods(6);
+ BudgetAmount budgetAmount = new BudgetAmount(new Money("10", "USD"), "first");
+ budgetAmount.setPeriodNum(-1);
+ budget.addBudgetAmount(budgetAmount);
+
+ List expandedBudgetAmount = budget.getExpandedBudgetAmounts();
+
+ assertThat(expandedBudgetAmount).hasSize(6);
+
+ assertThat(expandedBudgetAmount).extracting("mPeriodNum").hasSize(6)
+ .contains(0L,1L,2L,3L,4L,5L).doesNotContain(-1L);
+
+ assertThat(expandedBudgetAmount).extracting("mAccountUID").hasSize(6);
+ }
+
+ @Test
+ public void testGetNumberOfAccounts(){
+ Budget budget = new Budget("Test");
+ budget.setNumberOfPeriods(6);
+ BudgetAmount budgetAmount = new BudgetAmount(new Money("10", "USD"), "first");
+ budgetAmount.setPeriodNum(1);
+ budget.addBudgetAmount(budgetAmount);
+
+ budgetAmount = new BudgetAmount(new Money("10", "USD"), "first");
+ budgetAmount.setPeriodNum(2);
+ budget.addBudgetAmount(budgetAmount);
+
+ budgetAmount = new BudgetAmount(new Money("10", "USD"), "first");
+ budgetAmount.setPeriodNum(5);
+ budget.addBudgetAmount(budgetAmount);
+
+ budgetAmount = new BudgetAmount(new Money("10", "EUR"), "second");
+ budgetAmount.setPeriodNum(4);
+ budget.addBudgetAmount(budgetAmount);
+
+ budgetAmount = new BudgetAmount(new Money("13", "EUR"), "third");
+ budgetAmount.setPeriodNum(-1);
+ budget.addBudgetAmount(budgetAmount);
+
+ assertThat(budget.getNumberOfAccounts()).isEqualTo(3);
+ }
+}
diff --git a/app/src/test/java/org/gnucash/android/test/unit/model/MoneyTest.java b/app/src/test/java/org/gnucash/android/test/unit/model/MoneyTest.java
index fd5eb6a81..f84048a7e 100644
--- a/app/src/test/java/org/gnucash/android/test/unit/model/MoneyTest.java
+++ b/app/src/test/java/org/gnucash/android/test/unit/model/MoneyTest.java
@@ -76,7 +76,7 @@ public void testAddition(){
validateImmutability();
}
- @Test(expected = IllegalArgumentException.class)
+ @Test(expected = Money.CurrencyMismatchException.class)
public void testAdditionWithIncompatibleCurrency(){
Money addend = new Money("4", "USD");
mMoneyInEur.add(addend);
@@ -90,7 +90,7 @@ public void testSubtraction(){
validateImmutability();
}
- @Test(expected = IllegalArgumentException.class)
+ @Test(expected = Money.CurrencyMismatchException.class)
public void testSubtractionWithDifferentCurrency(){
Money addend = new Money("4", "USD");
mMoneyInEur.subtract(addend);
@@ -104,7 +104,7 @@ public void testMultiplication(){
validateImmutability();
}
- @Test(expected = IllegalArgumentException.class)
+ @Test(expected = Money.CurrencyMismatchException.class)
public void testMultiplicationWithDifferentCurrencies(){
Money addend = new Money("4", "USD");
mMoneyInEur.multiply(addend);
@@ -118,7 +118,7 @@ public void testDivision(){
validateImmutability();
}
- @Test(expected = IllegalArgumentException.class)
+ @Test(expected = Money.CurrencyMismatchException.class)
public void testDivisionWithDifferentCurrency(){
Money addend = new Money("4", "USD");
mMoneyInEur.divide(addend);
diff --git a/app/src/test/java/org/gnucash/android/test/unit/model/RecurrenceTest.java b/app/src/test/java/org/gnucash/android/test/unit/model/RecurrenceTest.java
new file mode 100644
index 000000000..ccc54f9b1
--- /dev/null
+++ b/app/src/test/java/org/gnucash/android/test/unit/model/RecurrenceTest.java
@@ -0,0 +1,61 @@
+/*
+ * 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.test.unit.model;
+
+import org.gnucash.android.model.PeriodType;
+import org.gnucash.android.model.Recurrence;
+import org.junit.Test;
+
+import java.sql.Timestamp;
+import java.util.Calendar;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+/**
+ * Test {@link Recurrence}s
+ */
+public class RecurrenceTest {
+
+ @Test
+ public void settingCount_shouldComputeCorrectEndTime(){
+ Recurrence recurrence = new Recurrence(PeriodType.MONTH);
+ Calendar cal = Calendar.getInstance();
+ cal.set(2015, Calendar.OCTOBER, 5);
+
+ recurrence.setPeriodStart(new Timestamp(cal.getTimeInMillis()));
+ recurrence.setPeriodEnd(3);
+
+ cal.set(2016, Calendar.JANUARY, 5);
+ assertThat(recurrence.getPeriodEnd().getTime()).isEqualTo(cal.getTimeInMillis());
+ }
+
+ /**
+ * When the end date of a recurrence is set, we should be able to correctly get the number of occurrences
+ */
+ @Test
+ public void testRecurrenceCountComputation(){
+ Recurrence recurrence = new Recurrence(PeriodType.MONTH);
+ Calendar cal = Calendar.getInstance();
+ cal.set(2015, Calendar.OCTOBER, 5);
+
+ recurrence.setPeriodStart(new Timestamp(cal.getTimeInMillis()));
+ cal.set(2016, Calendar.AUGUST, 5);
+ recurrence.setPeriodEnd(new Timestamp(cal.getTimeInMillis()));
+
+ assertThat(recurrence.getCount()).isEqualTo(10);
+ }
+}
diff --git a/app/src/test/java/org/gnucash/android/test/unit/model/ScheduledActionTest.java b/app/src/test/java/org/gnucash/android/test/unit/model/ScheduledActionTest.java
new file mode 100644
index 000000000..f4bf0af56
--- /dev/null
+++ b/app/src/test/java/org/gnucash/android/test/unit/model/ScheduledActionTest.java
@@ -0,0 +1,82 @@
+/*
+ * 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.test.unit.model;
+
+import org.gnucash.android.model.PeriodType;
+import org.gnucash.android.model.Recurrence;
+import org.gnucash.android.model.ScheduledAction;
+import org.junit.Test;
+
+import java.sql.Timestamp;
+import java.util.Calendar;
+
+import static org.assertj.core.api.Assertions.assertThat;
+/**
+ * Test scheduled actions
+ */
+public class ScheduledActionTest {
+
+ @Test
+ public void settingStartTime_shouldSetRecurrenceStart(){
+ ScheduledAction scheduledAction = new ScheduledAction(ScheduledAction.ActionType.TRANSACTION);
+ Calendar calendar = Calendar.getInstance();
+ calendar.set(2014, 8, 26);
+ long startTime = calendar.getTimeInMillis();
+ scheduledAction.setStartTime(startTime);
+ assertThat(scheduledAction.getRecurrence()).isNull();
+
+ Recurrence recurrence = new Recurrence(PeriodType.MONTH);
+ assertThat(recurrence.getPeriodStart().getTime()).isNotEqualTo(startTime);
+ scheduledAction.setRecurrence(recurrence);
+ assertThat(recurrence.getPeriodStart().getTime()).isEqualTo(startTime);
+
+ calendar.clear();
+ calendar.set(2015, 6, 6);
+ long newStartTime = calendar.getTimeInMillis();
+ scheduledAction.setStartTime(newStartTime);
+ assertThat(recurrence.getPeriodStart().getTime()).isEqualTo(newStartTime);
+ }
+
+ @Test
+ public void settingRecurrence_shouldSetScheduledActionStartTime(){
+ ScheduledAction scheduledAction = new ScheduledAction(ScheduledAction.ActionType.BACKUP);
+ assertThat(scheduledAction.getStartTime()).isEqualTo(0);
+
+ Calendar calendar = Calendar.getInstance();
+ calendar.set(2014, 8, 26);
+ Recurrence recurrence = new Recurrence(PeriodType.WEEK);
+ recurrence.setPeriodStart(new Timestamp(calendar.getTimeInMillis()));
+ scheduledAction.setRecurrence(recurrence);
+ assertThat(scheduledAction.getStartTime()).isEqualTo(calendar.getTimeInMillis());
+ }
+
+ @Test
+ public void settingRecurrence_shouldSetEndTime(){
+ ScheduledAction scheduledAction = new ScheduledAction(ScheduledAction.ActionType.BACKUP);
+ assertThat(scheduledAction.getStartTime()).isEqualTo(0);
+
+ Calendar calendar = Calendar.getInstance();
+ calendar.set(2017, 8, 26);
+ Recurrence recurrence = new Recurrence(PeriodType.WEEK);
+ recurrence.setPeriodEnd(new Timestamp(calendar.getTimeInMillis()));
+ scheduledAction.setRecurrence(recurrence);
+
+ assertThat(scheduledAction.getEndTime()).isEqualTo(calendar.getTimeInMillis());
+ }
+
+ //todo add test for computing the scheduledaction endtime from the recurrence count
+}
From da8b36d22630bb27f209f75efc0b1414faa7d67d Mon Sep 17 00:00:00 2001
From: Ngewi Fet
Date: Mon, 23 Nov 2015 00:41:10 +0100
Subject: [PATCH 002/121] Fix crash when creating budgets
Add view for setting budget start
---
.../gnucash/android/db/MigrationHelper.java | 3 +-
.../android/ui/budget/BudgetFormFragment.java | 52 +++++++++++++++++--
.../android/ui/util/RecurrenceParser.java | 6 +--
.../main/res/layout/fragment_budget_form.xml | 24 +++++++++
4 files changed, 77 insertions(+), 8 deletions(-)
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 1617ccb1b..d9462664d 100644
--- a/app/src/main/java/org/gnucash/android/db/MigrationHelper.java
+++ b/app/src/main/java/org/gnucash/android/db/MigrationHelper.java
@@ -1343,8 +1343,9 @@ static int upgradeDbToVersion12(SQLiteDatabase db){
db.execSQL(" ALTER TABLE " + SplitEntry.TABLE_NAME
+ " ADD COLUMN " + SplitEntry.COLUMN_RECONCILE_STATE + " varchar(1) not null default 'n' ");
+ //// FIXME: 22.11.15 Cannot add a column with non-constant default. Create new structure and migrate whole table
db.execSQL(" ALTER TABLE " + SplitEntry.TABLE_NAME
- + " ADD COLUMN " + SplitEntry.COLUMN_RECONCILE_DATE + " timestamp not null default CURRENT_TIMESTAMP ");
+ + " ADD COLUMN " + SplitEntry.COLUMN_RECONCILE_DATE + " timestamp not null default '' ");
db.setTransactionSuccessful();
oldVersion = 12;
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
index 4ec87947d..fe50d3227 100644
--- a/app/src/main/java/org/gnucash/android/ui/budget/BudgetFormFragment.java
+++ b/app/src/main/java/org/gnucash/android/ui/budget/BudgetFormFragment.java
@@ -24,6 +24,7 @@
import android.support.v4.app.Fragment;
import android.support.v7.app.ActionBar;
import android.support.v7.app.AppCompatActivity;
+import android.util.Log;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuInflater;
@@ -39,6 +40,7 @@
import android.widget.TextView;
import android.widget.Toast;
+import com.codetroopers.betterpickers.calendardatepicker.CalendarDatePickerDialogFragment;
import com.codetroopers.betterpickers.recurrencepicker.EventRecurrence;
import com.codetroopers.betterpickers.recurrencepicker.EventRecurrenceFormatter;
import com.codetroopers.betterpickers.recurrencepicker.RecurrencePickerDialogFragment;
@@ -51,25 +53,32 @@
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.model.Recurrence;
import org.gnucash.android.ui.common.UxArgument;
+import org.gnucash.android.ui.transaction.TransactionFormFragment;
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.sql.Timestamp;
+import java.text.ParseException;
import java.util.ArrayList;
+import java.util.Calendar;
import java.util.Currency;
+import java.util.Date;
+import java.util.GregorianCalendar;
import java.util.List;
import butterknife.Bind;
import butterknife.ButterKnife;
+import butterknife.OnClick;
/**
* Fragment for creating or editing Budgets
*/
-public class BudgetFormFragment extends Fragment implements RecurrencePickerDialogFragment.OnRecurrenceSetListener {
+public class BudgetFormFragment extends Fragment implements RecurrencePickerDialogFragment.OnRecurrenceSetListener, CalendarDatePickerDialogFragment.OnDateSetListener {
@Bind(R.id.input_budget_name) EditText mBudgetNameInput;
@Bind(R.id.input_description) EditText mDescriptionInput;
@@ -78,6 +87,7 @@ public class BudgetFormFragment extends Fragment implements RecurrencePickerDial
@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;
+ @Bind(R.id.input_start_date) TextView mStartDateInput;
EventRecurrence mEventRecurrence = new EventRecurrence();
String mRecurrenceRule;
@@ -91,6 +101,8 @@ public class BudgetFormFragment extends Fragment implements RecurrencePickerDial
private List mBudgetAmountViews = new ArrayList<>();
+ private Calendar mStartDate;
+
@Nullable
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
@@ -107,6 +119,7 @@ public void onClick(View v) {
addBudgetAmountView(null);
}
});
+ mStartDateInput.setText(TransactionFormFragment.DATE_FORMATTER.format(mStartDate.getTime()));
return view;
}
@@ -115,6 +128,7 @@ public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
mAccountsDbAdapter = AccountsDbAdapter.getInstance();
mBudgetsDbAdapter = BudgetsDbAdapter.getInstance();
+ mStartDate = Calendar.getInstance();
}
@Override
@@ -280,7 +294,9 @@ private void saveBudget(){
mBudget.setDescription(mDescriptionInput.getText().toString().trim());
- mBudget.setRecurrence(RecurrenceParser.parse(mEventRecurrence));
+ Recurrence recurrence = RecurrenceParser.parse(mEventRecurrence);
+ recurrence.setPeriodStart(new Timestamp(mStartDate.getTimeInMillis()));
+ mBudget.setRecurrence(recurrence);
mBudgetsDbAdapter.addRecord(mBudget);
getActivity().finish();
@@ -301,6 +317,27 @@ public boolean onOptionsItemSelected(MenuItem item) {
return false;
}
+ @OnClick(R.id.input_start_date)
+ public void onClick(View v) {
+ long dateMillis = 0;
+ try {
+ Date date = TransactionFormFragment.DATE_FORMATTER.parse(((TextView) v).getText().toString());
+ dateMillis = date.getTime();
+ } catch (ParseException e) {
+ Log.e(getTag(), "Error converting input time to Date object");
+ }
+ Calendar calendar = Calendar.getInstance();
+ calendar.setTimeInMillis(dateMillis);
+
+ int year = calendar.get(Calendar.YEAR);
+ int monthOfYear = calendar.get(Calendar.MONTH);
+ int dayOfMonth = calendar.get(Calendar.DAY_OF_MONTH);
+ CalendarDatePickerDialogFragment datePickerDialog = CalendarDatePickerDialogFragment.newInstance(
+ BudgetFormFragment.this,
+ year, monthOfYear, dayOfMonth);
+ datePickerDialog.show(getFragmentManager(), "date_picker_fragment");
+ }
+
@Override
public void onRecurrenceSet(String rrule) {
mRecurrenceRule = rrule;
@@ -313,6 +350,15 @@ public void onRecurrenceSet(String rrule) {
mRecurrenceInput.setText(repeatString);
}
+ @Override
+ public void onDateSet(CalendarDatePickerDialogFragment dialog, int year, int monthOfYear, int dayOfMonth) {
+ Calendar cal = new GregorianCalendar(year, monthOfYear, dayOfMonth);
+ mStartDateInput.setText(TransactionFormFragment.DATE_FORMATTER.format(cal.getTime()));
+ mStartDate.set(Calendar.YEAR, year);
+ mStartDate.set(Calendar.MONTH, monthOfYear);
+ mStartDate.set(Calendar.DAY_OF_MONTH, dayOfMonth);
+ }
+
/**
* View holder for budget amounts
*/
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 f408a8dd8..3e19812d1 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
@@ -22,12 +22,9 @@
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;
/**
* Parses {@link EventRecurrence}s to generate
@@ -80,7 +77,8 @@ public static Recurrence parse(EventRecurrence eventRecurrence){
Recurrence recurrence = new Recurrence(periodType);
parseEndTime(eventRecurrence, recurrence);
recurrence.setByDay(parseByDay(eventRecurrence.byday));
- recurrence.setPeriodStart(new Timestamp(eventRecurrence.startDate.toMillis(false)));
+ if (eventRecurrence.startDate != null)
+ recurrence.setPeriodStart(new Timestamp(eventRecurrence.startDate.toMillis(false)));
return recurrence;
}
diff --git a/app/src/main/res/layout/fragment_budget_form.xml b/app/src/main/res/layout/fragment_budget_form.xml
index 386397e89..439556964 100644
--- a/app/src/main/res/layout/fragment_budget_form.xml
+++ b/app/src/main/res/layout/fragment_budget_form.xml
@@ -16,6 +16,7 @@
-->
+
@@ -78,6 +80,28 @@
style="@style/Dropdown.TextView" />
+
+
+
+
+
+
+
+
Date: Mon, 23 Nov 2015 22:18:32 +0100
Subject: [PATCH 003/121] Fix crash when cancelling the open file intent.
Fixes https://github.com/codinguser/gnucash-android/issues/433
---
.../android/ui/common/BaseDrawerActivity.java | 17 +++++++++++------
1 file changed, 11 insertions(+), 6 deletions(-)
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 1f702e25b..5181e7cb6 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
@@ -40,10 +40,13 @@
/**
- * Base activity implementing the navigation drawer, to be extended by all activities requiring one
- *
All subclasses should call the {@link #setUpDrawer()} method in {@link #onCreate(Bundle)}, after the
- * activity layout has been set.
- * The activity layout of the subclass is expected to contain {@code DrawerLayout} and a {@code NavigationView}
+ * Base activity implementing the navigation drawer, to be extended by all activities requiring one.
+ *
+ * All subclasses should call the {@link #setUpDrawer()} method in {@link #onCreate(Bundle)},
+ * after the activity layout has been set.
+ *
+ * The activity layout of the subclass is expected to contain {@code DrawerLayout} and
+ * a {@code NavigationView}.
*
* @author Ngewi Fet
*/
@@ -81,8 +84,8 @@ protected void setUpDrawer() {
if (actionBar != null){
actionBar.setHomeButtonEnabled(true);
actionBar.setDisplayHomeAsUpEnabled(true);
-
}
+
mDrawerLayout = (DrawerLayout) findViewById(R.id.drawer_layout);
mNavigationView = (NavigationView) findViewById(R.id.nav_view);
@@ -108,6 +111,7 @@ public void onDrawerOpened(View drawerView) {
mDrawerLayout.setDrawerListener(mDrawerToggle);
}
+
@Override
protected void onPostCreate(Bundle savedInstanceState) {
super.onPostCreate(savedInstanceState);
@@ -192,8 +196,9 @@ protected void onDrawerMenuItemClicked(int itemId) {
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
- if (resultCode == Activity.RESULT_CANCELED){
+ if (resultCode == Activity.RESULT_CANCELED) {
super.onActivityResult(requestCode, resultCode, data);
+ return;
}
switch (requestCode) {
From 789333b8a7304b4af8cb8b79c548bee10a305fd1 Mon Sep 17 00:00:00 2001
From: Yongxin Wang
Date: Fri, 27 Nov 2015 14:00:30 +0800
Subject: [PATCH 004/121] fix the bug that modify account name will delete all
transaction under the account
---
.../android/db/adapter/DatabaseAdapter.java | 18 ++++++++++++++++++
.../ui/account/AccountFormFragment.java | 2 ++
2 files changed, 20 insertions(+)
diff --git a/app/src/main/java/org/gnucash/android/db/adapter/DatabaseAdapter.java b/app/src/main/java/org/gnucash/android/db/adapter/DatabaseAdapter.java
index 8881114fd..a834379e0 100644
--- a/app/src/main/java/org/gnucash/android/db/adapter/DatabaseAdapter.java
+++ b/app/src/main/java/org/gnucash/android/db/adapter/DatabaseAdapter.java
@@ -57,6 +57,10 @@ public abstract class DatabaseAdapter {
protected SQLiteStatement mReplaceStatement;
+ public enum UpdateMethod {
+ insert, update, replace
+ };
+
/**
* Opens the database adapter with an existing database
* @param db SQLiteDatabase object
@@ -625,6 +629,20 @@ public void setTransactionSuccessful() {
mDb.setTransactionSuccessful();
}
+ /// Foreign key constraits should be enabled in general.
+ /// But if it affects speed (check constraints takes time)
+ /// and the constrained can be assured by the program,
+ /// or if some SQL exec will cause deletion of records
+ /// (like use replace in accounts update will delete all transactions)
+ /// that need not be deleted, then it can be disabled temporarily
+ public void enableForeignKey(boolean enable) {
+ if (enable){
+ mDb.execSQL("PRAGMA foreign_keys=ON");
+ } else {
+ mDb.execSQL("PRAGMA foreign_keys=OFF");
+ }
+ }
+
/**
* Expose mDb.endTransaction()
*/
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 9c98be705..b1a48f45a 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
@@ -846,7 +846,9 @@ private void saveAccount() {
if (mAccountsDbAdapter == null)
mAccountsDbAdapter = AccountsDbAdapter.getInstance();
// bulk update, will not update transactions
+ mAccountsDbAdapter.enableForeignKey(false);
mAccountsDbAdapter.bulkAddRecords(accountsToUpdate);
+ mAccountsDbAdapter.enableForeignKey(true);
finishFragment();
}
From 5bbe6310f8969d4c6099e6924dc02123c4222bf9 Mon Sep 17 00:00:00 2001
From: Ngewi Fet
Date: Fri, 27 Nov 2015 15:49:11 +0100
Subject: [PATCH 005/121] Add new editor for budget amounts (using
FormActivity)
Fix crash when creating/editing budgets
Fix recurrence rule not including count/end time
Fix crash when parsing weekly recurrences
Upgrade gradle wrapper to version 2.9
Update Android build tools and plugin (for use with Android Studio 2.0.0)
---
app/build.gradle | 2 +-
.../android/db/adapter/AccountsDbAdapter.java | 6 +-
.../org/gnucash/android/model/Account.java | 4 -
.../org/gnucash/android/model/Budget.java | 1 -
.../gnucash/android/model/BudgetAmount.java | 50 +++-
.../java/org/gnucash/android/model/Money.java | 2 -
.../org/gnucash/android/model/Recurrence.java | 25 +-
.../android/ui/account/AccountsActivity.java | 47 +--
.../ui/budget/BudgetAmountEditorFragment.java | 278 ++++++++++++++++++
.../ui/budget/BudgetDetailFragment.java | 5 +-
.../android/ui/budget/BudgetFormFragment.java | 244 +++++++--------
.../android/ui/common/FormActivity.java | 16 +-
.../gnucash/android/ui/common/UxArgument.java | 5 +
.../android/ui/util/RecurrenceParser.java | 2 +-
.../ui/util/RecurrenceViewClickListener.java | 3 +-
.../layout/fragment_budget_amount_editor.xml | 42 +++
.../main/res/layout/fragment_budget_form.xml | 174 +++++------
.../main/res/layout/fragment_split_editor.xml | 2 +-
.../main/res/layout/item_budget_amount.xml | 5 +-
.../res/menu/budget_amount_editor_actions.xml | 27 ++
.../test/unit/db/AccountsDbAdapterTest.java | 13 +-
build.gradle | 2 +-
gradle/wrapper/gradle-wrapper.jar | Bin 52266 -> 53636 bytes
gradle/wrapper/gradle-wrapper.properties | 4 +-
gradlew | 10 +-
25 files changed, 669 insertions(+), 300 deletions(-)
create mode 100644 app/src/main/java/org/gnucash/android/ui/budget/BudgetAmountEditorFragment.java
create mode 100644 app/src/main/res/layout/fragment_budget_amount_editor.xml
create mode 100644 app/src/main/res/menu/budget_amount_editor_actions.xml
diff --git a/app/build.gradle b/app/build.gradle
index c0952bdcd..62c5c1c88 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -21,7 +21,7 @@ def gitSha() {
android {
compileSdkVersion 23
- buildToolsVersion "23.0.1"
+ buildToolsVersion '23.0.2'
defaultConfig {
applicationId "org.gnucash.android"
testApplicationId 'org.gnucash.android.test'
diff --git a/app/src/main/java/org/gnucash/android/db/adapter/AccountsDbAdapter.java b/app/src/main/java/org/gnucash/android/db/adapter/AccountsDbAdapter.java
index 906b96737..5ecc726f4 100644
--- a/app/src/main/java/org/gnucash/android/db/adapter/AccountsDbAdapter.java
+++ b/app/src/main/java/org/gnucash/android/db/adapter/AccountsDbAdapter.java
@@ -1203,14 +1203,18 @@ public List getCurrenciesInUse(){
*/
@Override
public int deleteAllRecords() {
- mDb.delete(DatabaseSchema.PriceEntry.TABLE_NAME, null, null);
// Relies "ON DELETE CASCADE" takes too much time
// It take more than 300s to complete the deletion on my dataset without
// clearing the split table first, but only needs a little more that 1s
// if the split table is cleared first.
+ mDb.delete(DatabaseSchema.PriceEntry.TABLE_NAME, null, null);
mDb.delete(SplitEntry.TABLE_NAME, null, null);
mDb.delete(TransactionEntry.TABLE_NAME, null, null);
mDb.delete(DatabaseSchema.ScheduledActionEntry.TABLE_NAME, null, null);
+ mDb.delete(DatabaseSchema.BudgetAmountEntry.TABLE_NAME, null, null);
+ mDb.delete(DatabaseSchema.BudgetEntry.TABLE_NAME, null, null);
+ mDb.delete(DatabaseSchema.RecurrenceEntry.TABLE_NAME, null, null);
+
return mDb.delete(AccountEntry.TABLE_NAME, null, null);
}
diff --git a/app/src/main/java/org/gnucash/android/model/Account.java b/app/src/main/java/org/gnucash/android/model/Account.java
index 6df050366..5ae6bc420 100644
--- a/app/src/main/java/org/gnucash/android/model/Account.java
+++ b/app/src/main/java/org/gnucash/android/model/Account.java
@@ -17,11 +17,7 @@
package org.gnucash.android.model;
-import android.preference.PreferenceManager;
-
import org.gnucash.android.BuildConfig;
-import org.gnucash.android.app.GnuCashApplication;
-import org.gnucash.android.export.Exporter;
import org.gnucash.android.export.ofx.OfxHelper;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
diff --git a/app/src/main/java/org/gnucash/android/model/Budget.java b/app/src/main/java/org/gnucash/android/model/Budget.java
index 8605146e1..db199b366 100644
--- a/app/src/main/java/org/gnucash/android/model/Budget.java
+++ b/app/src/main/java/org/gnucash/android/model/Budget.java
@@ -23,7 +23,6 @@
import java.math.BigDecimal;
import java.util.ArrayList;
-import java.util.Currency;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
diff --git a/app/src/main/java/org/gnucash/android/model/BudgetAmount.java b/app/src/main/java/org/gnucash/android/model/BudgetAmount.java
index 8aa42b26d..714e44789 100644
--- a/app/src/main/java/org/gnucash/android/model/BudgetAmount.java
+++ b/app/src/main/java/org/gnucash/android/model/BudgetAmount.java
@@ -15,12 +15,19 @@
*/
package org.gnucash.android.model;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+import org.gnucash.android.app.GnuCashApplication;
+
+import java.math.BigDecimal;
+
/**
* Budget amounts for the different accounts.
* The {@link Money} amounts are absolute values
* @see Budget
*/
-public class BudgetAmount extends BaseModel {
+public class BudgetAmount extends BaseModel implements Parcelable {
private String mBudgetUID;
private String mAccountUID;
@@ -101,4 +108,45 @@ public Money getAmount() {
public void setAmount(Money amount) {
this.mAmount = amount.abs();
}
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(Parcel dest, int flags) {
+ dest.writeString(getUID());
+ dest.writeString(mBudgetUID);
+ dest.writeString(mAccountUID);
+ dest.writeString(mAmount.toPlainString());
+ dest.writeLong(mPeriodNum);
+ }
+
+ public static final Parcelable.Creator CREATOR = new Parcelable.Creator(){
+
+ @Override
+ public BudgetAmount createFromParcel(Parcel source) {
+ return new BudgetAmount(source);
+ }
+
+ @Override
+ public BudgetAmount[] newArray(int size) {
+ return new BudgetAmount[size];
+ }
+ };
+
+ /**
+ * Private constructor for creating new BudgetAmounts from a Parcel
+ * @param source Parcel
+ */
+ private BudgetAmount(Parcel source){
+ setUID(source.readString());
+ mBudgetUID = source.readString();
+ mAccountUID = source.readString();
+ mAmount = new Money(new BigDecimal(source.readString()), Commodity.DEFAULT_COMMODITY);
+ mPeriodNum = source.readLong();
+ }
+
+
}
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 7c5e0a700..ef7cec86d 100644
--- a/app/src/main/java/org/gnucash/android/model/Money.java
+++ b/app/src/main/java/org/gnucash/android/model/Money.java
@@ -25,11 +25,9 @@
import java.math.BigDecimal;
import java.math.BigInteger;
import java.math.RoundingMode;
-import java.security.InvalidParameterException;
import java.text.DecimalFormat;
import java.text.DecimalFormatSymbols;
import java.text.NumberFormat;
-import java.text.ParseException;
import java.util.Currency;
import java.util.Locale;
diff --git a/app/src/main/java/org/gnucash/android/model/Recurrence.java b/app/src/main/java/org/gnucash/android/model/Recurrence.java
index 87dcb5623..331a0bd1b 100644
--- a/app/src/main/java/org/gnucash/android/model/Recurrence.java
+++ b/app/src/main/java/org/gnucash/android/model/Recurrence.java
@@ -125,13 +125,16 @@ public long getPeriod(){
public String getRepeatString(){
String dayOfWeek = new SimpleDateFormat("EEEE", Locale.US).format(new Date(mPeriodStart.getTime()));
- StringBuilder ruleBuilder = new StringBuilder(mPeriodType.getFrequencyRepeatString());
+ StringBuilder repeatBuilder = new StringBuilder(mPeriodType.getFrequencyRepeatString());
if (mPeriodType == PeriodType.WEEK) {
- ruleBuilder.append(" on ").append(dayOfWeek);
+ repeatBuilder.append(" on ").append(dayOfWeek);
}
- return ruleBuilder.toString();
+ if (mPeriodEnd != null){
+ repeatBuilder.append(" until " + SimpleDateFormat.getDateInstance().format(new Date(mPeriodEnd.getTime())));
+ }
+ return repeatBuilder.toString();
}
/**
@@ -158,6 +161,8 @@ public String getRuleString(){
ruleBuilder.append("FREQ=").append(mPeriodType.getFrequencyDescription()).append(separator);
ruleBuilder.append("INTERVAL=").append(mPeriodType.getMultiplier()).append(separator);
+ if (getCount() > 0)
+ ruleBuilder.append("COUNT=").append(getCount()).append(separator);
ruleBuilder.append(mPeriodType.getByParts(mPeriodStart.getTime())).append(separator);
return ruleBuilder.toString();
@@ -275,7 +280,8 @@ public int getCount(){
count = Years.yearsBetween(startDate, endDate).getYears();
break;
}
- return count;
+
+ return count/mPeriodType.getMultiplier();
}
/**
@@ -285,21 +291,22 @@ public int getCount(){
public void setPeriodEnd(int numberOfOccurences){
LocalDateTime localDate = new LocalDateTime(mPeriodStart.getTime());
LocalDateTime endDate;
+ int occurrenceDuration = numberOfOccurences * mPeriodType.getMultiplier();
switch (mPeriodType){
case DAY:
- endDate = localDate.dayOfWeek().getLocalDateTime().plusDays(numberOfOccurences);
+ endDate = localDate.dayOfWeek().getLocalDateTime().plusDays(occurrenceDuration);
break;
case WEEK:
- endDate = localDate.dayOfWeek().getLocalDateTime().plusWeeks(numberOfOccurences);
+ endDate = localDate.dayOfWeek().getLocalDateTime().plusWeeks(occurrenceDuration);
break;
case MONTH:
- endDate = localDate.dayOfMonth().getLocalDateTime().plusMonths(numberOfOccurences);
+ endDate = localDate.dayOfMonth().getLocalDateTime().plusMonths(occurrenceDuration);
break;
case YEAR:
- endDate = localDate.monthOfYear().getLocalDateTime().plusYears(numberOfOccurences);
+ endDate = localDate.monthOfYear().getLocalDateTime().plusYears(occurrenceDuration);
break;
default: //default to monthly
- endDate = localDate.dayOfMonth().getLocalDateTime().plusMonths(numberOfOccurences);
+ endDate = localDate.dayOfMonth().getLocalDateTime().plusMonths(occurrenceDuration);
break;
}
mPeriodEnd = new Timestamp(endDate.toDate().getTime());
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 126094a1d..493ca6e24 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
@@ -163,23 +163,25 @@ public AccountViewPagerAdapter(FragmentManager fm){
@Override
public Fragment getItem(int i) {
- AccountsListFragment currentFragment;
- switch (i){
- case INDEX_RECENT_ACCOUNTS_FRAGMENT:
- currentFragment = AccountsListFragment.newInstance(AccountsListFragment.DisplayMode.RECENT);
- break;
-
- case INDEX_FAVORITE_ACCOUNTS_FRAGMENT:
- currentFragment = AccountsListFragment.newInstance(AccountsListFragment.DisplayMode.FAVORITES);
- break;
-
- case INDEX_TOP_LEVEL_ACCOUNTS_FRAGMENT:
- default:
- currentFragment = AccountsListFragment.newInstance(AccountsListFragment.DisplayMode.TOP_LEVEL);
- break;
+ AccountsListFragment currentFragment = (AccountsListFragment) mFragmentPageReferenceMap.get(i);
+ if (currentFragment == null) {
+ switch (i) {
+ case INDEX_RECENT_ACCOUNTS_FRAGMENT:
+ currentFragment = AccountsListFragment.newInstance(AccountsListFragment.DisplayMode.RECENT);
+ break;
+
+ case INDEX_FAVORITE_ACCOUNTS_FRAGMENT:
+ currentFragment = AccountsListFragment.newInstance(AccountsListFragment.DisplayMode.FAVORITES);
+ break;
+
+ case INDEX_TOP_LEVEL_ACCOUNTS_FRAGMENT:
+ default:
+ currentFragment = AccountsListFragment.newInstance(AccountsListFragment.DisplayMode.TOP_LEVEL);
+ break;
+ }
+ mFragmentPageReferenceMap.put(i, currentFragment);
}
- mFragmentPageReferenceMap.put(i, currentFragment);
return currentFragment;
}
@@ -259,10 +261,7 @@ public void onTabReselected(TabLayout.Tab tab) {
}
});
- SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(this);
- int lastTabIndex = preferences.getInt(LAST_OPEN_TAB_INDEX, INDEX_TOP_LEVEL_ACCOUNTS_FRAGMENT);
- int index = intent.getIntExtra(EXTRA_TAB_INDEX, lastTabIndex);
- mViewPager.setCurrentItem(index);
+ setCurrentTab();
mFloatingActionButton.setOnClickListener(new View.OnClickListener() {
@Override
@@ -348,17 +347,19 @@ public void onRequestPermissionsResult(int requestCode, String[] permissions, in
@Override
protected void onNewIntent(Intent intent) {
super.onNewIntent(intent);
- int index = intent.getIntExtra(EXTRA_TAB_INDEX, INDEX_TOP_LEVEL_ACCOUNTS_FRAGMENT);
- setTab(index);
+ setIntent(intent);
+ setCurrentTab();
handleOpenFileIntent(intent);
}
/**
* Sets the current tab in the ViewPager
- * @param index Index of fragment to be loaded
*/
- public void setTab(int index){
+ public void setCurrentTab(){
+ SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(this);
+ int lastTabIndex = preferences.getInt(LAST_OPEN_TAB_INDEX, INDEX_TOP_LEVEL_ACCOUNTS_FRAGMENT);
+ int index = getIntent().getIntExtra(EXTRA_TAB_INDEX, lastTabIndex);
mViewPager.setCurrentItem(index);
}
diff --git a/app/src/main/java/org/gnucash/android/ui/budget/BudgetAmountEditorFragment.java b/app/src/main/java/org/gnucash/android/ui/budget/BudgetAmountEditorFragment.java
new file mode 100644
index 000000000..522ab0db7
--- /dev/null
+++ b/app/src/main/java/org/gnucash/android/ui/budget/BudgetAmountEditorFragment.java
@@ -0,0 +1,278 @@
+/*
+ * 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.database.Cursor;
+import android.inputmethodservice.KeyboardView;
+import android.os.Bundle;
+import android.support.annotation.Nullable;
+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.ImageView;
+import android.widget.LinearLayout;
+import android.widget.Spinner;
+import android.widget.TextView;
+import android.widget.Toast;
+
+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.BudgetAmount;
+import org.gnucash.android.model.Commodity;
+import org.gnucash.android.model.Money;
+import org.gnucash.android.ui.common.UxArgument;
+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 editing budgeting amounts
+ */
+public class BudgetAmountEditorFragment extends Fragment {
+
+ private Cursor mAccountCursor;
+ private QualifiedAccountNameCursorAdapter mAccountCursorAdapter;
+ private List mBudgetAmountViews = new ArrayList<>();
+ private AccountsDbAdapter mAccountsDbAdapter;
+
+ @Bind(R.id.budget_amount_layout) LinearLayout mBudgetAmountTableLayout;
+ @Bind(R.id.calculator_keyboard) KeyboardView mKeyboardView;
+
+ public static BudgetAmountEditorFragment newInstance(Bundle args){
+ BudgetAmountEditorFragment fragment = new BudgetAmountEditorFragment();
+ fragment.setArguments(args);
+ return fragment;
+ }
+
+ @Nullable
+ @Override
+ public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container,
+ @Nullable Bundle savedInstanceState) {
+ View view = inflater.inflate(R.layout.fragment_budget_amount_editor, container, false);
+ ButterKnife.bind(this, view);
+ setupAccountSpinnerAdapter();
+ return view;
+ }
+
+ @Override
+ public void onCreate(@Nullable Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ mAccountsDbAdapter = AccountsDbAdapter.getInstance();
+ }
+
+ @Override
+ public void onActivityCreated(@Nullable Bundle savedInstanceState) {
+ super.onActivityCreated(savedInstanceState);
+
+ ActionBar actionBar = ((AppCompatActivity)getActivity()).getSupportActionBar();
+ assert actionBar != null;
+ actionBar.setTitle("Edit Budget Amounts");
+ setHasOptionsMenu(true);
+
+ ArrayList budgetAmounts = getArguments().getParcelableArrayList(UxArgument.BUDGET_AMOUNT_LIST);
+ if (budgetAmounts != null) {
+ if (budgetAmounts.isEmpty()){
+ BudgetAmountViewHolder viewHolder = (BudgetAmountViewHolder) addBudgetAmountView(null).getTag();
+ viewHolder.removeItemBtn.setVisibility(View.GONE); //there should always be at least one
+ } else {
+ loadBudgetAmountViews(budgetAmounts);
+ }
+ } else {
+ BudgetAmountViewHolder viewHolder = (BudgetAmountViewHolder) addBudgetAmountView(null).getTag();
+ viewHolder.removeItemBtn.setVisibility(View.GONE); //there should always be at least one
+ }
+
+ }
+
+ @Override
+ public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
+ inflater.inflate(R.menu.budget_amount_editor_actions, menu);
+ }
+
+ @Override
+ public boolean onOptionsItemSelected(MenuItem item) {
+ switch (item.getItemId()){
+ case R.id.menu_add_budget_amount:
+ addBudgetAmountView(null);
+ return true;
+
+ case R.id.menu_save:
+ saveBudgetAmounts();
+ return true;
+
+ default:
+ return super.onOptionsItemSelected(item);
+ }
+ }
+
+ /**
+ * Checks if the budget amounts can be saved
+ * @return {@code true} if all amounts a properly entered, {@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;
+ }
+ }
+ return true;
+ }
+
+ private void saveBudgetAmounts() {
+ if (canSave()){
+ ArrayList budgetAmounts = (ArrayList) extractBudgetAmounts();
+ Intent data = new Intent();
+ data.putParcelableArrayListExtra(UxArgument.BUDGET_AMOUNT_LIST, budgetAmounts);
+ getActivity().setResult(Activity.RESULT_OK, data);
+ getActivity().finish();
+ }
+ }
+
+ /**
+ * Load views for the budget amounts
+ * @param budgetAmounts List of {@link BudgetAmount}s
+ */
+ private void loadBudgetAmountViews(List budgetAmounts){
+ for (BudgetAmount budgetAmount : budgetAmounts) {
+ addBudgetAmountView(budgetAmount);
+ }
+ }
+
+ /**
+ * 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