Implemented exporting account transactions as .csv at least for JVM and Android

This commit is contained in:
dankito 2020-10-26 23:56:11 +01:00
parent 704ac55239
commit bc3439baa5
24 changed files with 535 additions and 7 deletions

View File

@ -23,6 +23,8 @@ ext {
textInfoExtractorVersion = "1.0.1" textInfoExtractorVersion = "1.0.1"
commonsCsvVersion = "1.8"
hbci4jVersion = '3.1.37' hbci4jVersion = '3.1.37'
@ -45,7 +47,7 @@ ext {
androidTargetSdkVersion = 28 androidTargetSdkVersion = 28
fileChooserDialogVersion = "1.2.0-androidx" fileChooserDialogVersion = "1.3.0-androidx"
androidUtilsVersion = '1.1.1-SNAPSHOT' androidUtilsVersion = '1.1.1-SNAPSHOT'

View File

@ -190,7 +190,7 @@ open class RoomBankingPersistence(protected open val applicationContext: Context
override fun saveOrUpdateAppSettings(appSettings: AppSettings) { override fun saveOrUpdateAppSettings(appSettings: AppSettings) {
val mapped = net.dankito.banking.persistence.model.AppSettings(appSettings.automaticallyUpdateAccountsAfterMinutes, val mapped = net.dankito.banking.persistence.model.AppSettings(appSettings.automaticallyUpdateAccountsAfterMinutes,
appSettings.lockAppAfterMinutes, appSettings.screenshotsAllowed) appSettings.lockAppAfterMinutes, appSettings.screenshotsAllowed, appSettings.lastSelectedExportFolder)
database.appSettingsDao().saveOrUpdate(mapped) database.appSettingsDao().saveOrUpdate(mapped)
saveOrUpdateTanMethodSettings(appSettings.flickerCodeSettings, FlickerCodeTanMethodSettingsId) saveOrUpdateTanMethodSettings(appSettings.flickerCodeSettings, FlickerCodeTanMethodSettingsId)
@ -215,6 +215,7 @@ open class RoomBankingPersistence(protected open val applicationContext: Context
settings.automaticallyUpdateAccountsAfterMinutes = persistedSettings.automaticallyUpdateAccountsAfterMinutes settings.automaticallyUpdateAccountsAfterMinutes = persistedSettings.automaticallyUpdateAccountsAfterMinutes
settings.lockAppAfterMinutes = persistedSettings.lockAppAfterMinutes settings.lockAppAfterMinutes = persistedSettings.lockAppAfterMinutes
settings.screenshotsAllowed = persistedSettings.screenshotsAllowed settings.screenshotsAllowed = persistedSettings.screenshotsAllowed
settings.lastSelectedExportFolder = persistedSettings.lastSelectedExportFolder
} }
settings.flickerCodeSettings = findTanMethodSettings(FlickerCodeTanMethodSettingsId, tanMethodSettings) settings.flickerCodeSettings = findTanMethodSettings(FlickerCodeTanMethodSettingsId, tanMethodSettings)

View File

@ -10,7 +10,8 @@ import net.dankito.banking.ui.model.settings.AppSettings
open class AppSettings( open class AppSettings(
open var automaticallyUpdateAccountsAfterMinutes: Int? = AppSettings.DefaultAutomaticallyUpdateAccountsAfterMinutes, open var automaticallyUpdateAccountsAfterMinutes: Int? = AppSettings.DefaultAutomaticallyUpdateAccountsAfterMinutes,
open var lockAppAfterMinutes: Int? = null, open var lockAppAfterMinutes: Int? = null,
open var screenshotsAllowed: Boolean = false open var screenshotsAllowed: Boolean = false,
open var lastSelectedExportFolder: String? = null
) { ) {
internal constructor() : this(AppSettings.DefaultAutomaticallyUpdateAccountsAfterMinutes, null, false) internal constructor() : this(AppSettings.DefaultAutomaticallyUpdateAccountsAfterMinutes, null, false)

View File

@ -71,10 +71,12 @@ project(':fints4kRest').projectDir = "$rootDir/rest/fints4kRest/" as File
include ':BankFinder' include ':BankFinder'
include ':LuceneBankFinder' include ':LuceneBankFinder'
include ':BankListCreator' include ':BankListCreator'
include ':CsvAccountTransactionsImporterAndExporter'
include ':EpcQrCodeParser' include ':EpcQrCodeParser'
project(':BankFinder').projectDir = "$rootDir/tools/BankFinder/" as File project(':BankFinder').projectDir = "$rootDir/tools/BankFinder/" as File
project(':LuceneBankFinder').projectDir = "$rootDir/tools/LuceneBankFinder/" as File project(':LuceneBankFinder').projectDir = "$rootDir/tools/LuceneBankFinder/" as File
project(':BankListCreator').projectDir = "$rootDir/tools/BankListCreator/" as File project(':BankListCreator').projectDir = "$rootDir/tools/BankListCreator/" as File
project(':CsvAccountTransactionsImporterAndExporter').projectDir = "$rootDir/tools/CsvAccountTransactionsImporterAndExporter/" as File
project(':EpcQrCodeParser').projectDir = "$rootDir/tools/EpcQrCodeParser/" as File project(':EpcQrCodeParser').projectDir = "$rootDir/tools/EpcQrCodeParser/" as File

View File

@ -0,0 +1,24 @@
plugins {
id 'org.jetbrains.kotlin.jvm'
id "maven-publish"
}
group 'net.codinux.banking.tools'
ext.artifactName = "csv-account-transactions-importer-exporter"
dependencies {
implementation "org.jetbrains.kotlin:kotlin-stdlib"
implementation "org.apache.commons:commons-csv:$commonsCsvVersion"
implementation "org.slf4j:slf4j-api:$slf4jVersion"
testImplementation "org.junit.jupiter:junit-jupiter:$junit5Version"
testRuntimeOnly "org.junit.jupiter:junit-jupiter-engine:$junit5Version"
testImplementation "org.assertj:assertj-core:$assertJVersion"
testImplementation "org.mockito:mockito-core:$mockitoVersion"
}

View File

@ -0,0 +1,83 @@
package net.codinux.banking.tools.importerexporter
import net.codinux.banking.tools.importerexporter.model.AccountTransaction
import org.apache.commons.csv.CSVFormat
import org.apache.commons.csv.CSVPrinter
import org.slf4j.LoggerFactory
import java.io.Writer
import java.math.BigDecimal
import java.text.DateFormat
import java.text.NumberFormat
import java.util.*
open class CsvAccountTransactionsExporter : IAccountTransactionsExporter {
companion object {
// TODO: translate
val Headers = listOf(
"Auftragskonto", "Buchungstag", "Wertstellungstag", "Umsatzart", "Empfänger / Auftraggeber",
"Verwendungszweck", "IBAN/Kontonummer", "BIC", "Umsatz", "Währung"
)
const val DateSeparators = "[./ -]"
val DateOnlyContainsNumbersRegex = Regex("\\d{1,4}$DateSeparators\\d{1,2}$DateSeparators\\d{2,4}")
private val log = LoggerFactory.getLogger(CsvAccountTransactionsExporter::class.java)
}
// set as fields not as companion object members so they use the Locale set at CsvAccountTransactionsExporter instantiation time not when the first CsvAccountTransactionsExporter has been created
protected open val DateFormatter: DateFormat = findLongestDateFormatWithoutWrittenOutMonth() // ensure converted dates only contain numbers, not dates like 27 Mar 2020
protected open val DecimalFormat = NumberFormat.getNumberInstance()
override fun export(writer: Writer, transactions: Collection<AccountTransaction>) {
try {
writer.use {
val csvPrinter = CSVPrinter(writer, CSVFormat.DEFAULT.withHeader(*Headers.toTypedArray()))
transactions.forEach { transaction ->
csvPrinter.printRecord(transaction.account, format(transaction.bookingDate), format(transaction.valueDate),
format(transaction.bookingText), format(transaction.otherPartyName), transaction.reference,
format(transaction.otherPartyAccountId), format(transaction.otherPartyBankCode), format(transaction.amount), transaction.currency
)
}
csvPrinter.flush()
}
} catch (e: Exception) {
log.error("Could not export ${transactions.size} transactions to CSV", e)
}
}
protected open fun format(date: Date): String {
return DateFormatter.format(date)
}
protected open fun format(bigDecimal: BigDecimal): String {
return DecimalFormat.format(bigDecimal)
}
protected open fun format(string: String?): String {
return string ?: ""
}
protected open fun findLongestDateFormatWithoutWrittenOutMonth(): DateFormat {
val mediumDateFormat = DateFormat.getDateInstance(DateFormat.MEDIUM)
val dateFormatTest = mediumDateFormat.format(Date())
return if (DateOnlyContainsNumbersRegex.matches(dateFormatTest)) {
mediumDateFormat
}
else {
DateFormat.getDateInstance(DateFormat.SHORT)
}
}
}

View File

@ -0,0 +1,16 @@
package net.codinux.banking.tools.importerexporter
import net.codinux.banking.tools.importerexporter.model.AccountTransaction
import java.io.File
import java.io.Writer
interface IAccountTransactionsExporter {
fun export(file: File, transactions: Collection<AccountTransaction>) {
return export(file.outputStream().bufferedWriter(), transactions)
}
fun export(writer: Writer, transactions: Collection<AccountTransaction>)
}

View File

@ -0,0 +1,11 @@
package net.codinux.banking.tools.importerexporter
import net.codinux.banking.tools.importerexporter.model.AccountTransaction
import java.io.File
interface IAccountTransactionsImporter {
fun import(file: File): List<AccountTransaction>
}

View File

@ -0,0 +1,4 @@
package net.codinux.banking.tools.importerexporter
interface IAccountTransactionsImporterExporter : IAccountTransactionsImporter, IAccountTransactionsExporter

View File

@ -0,0 +1,20 @@
package net.codinux.banking.tools.importerexporter.model
import java.math.BigDecimal
import java.util.*
open class AccountTransaction(
open val account: String,
open val amount: BigDecimal,
open val currency: String,
open val reference: String,
open val bookingDate: Date,
open val valueDate: Date,
open val otherPartyName: String?,
open val otherPartyBankCode: String?,
open val otherPartyAccountId: String?,
open val bookingText: String?
) {
}

View File

@ -0,0 +1,172 @@
package net.codinux.banking.tools.importerexporter
import net.codinux.banking.tools.importerexporter.model.AccountTransaction
import org.assertj.core.api.Assertions.assertThat
import org.junit.jupiter.api.AfterEach
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.Assertions.*
import java.awt.print.Book
import java.io.StringWriter
import java.math.BigDecimal
import java.util.*
import java.util.concurrent.ThreadLocalRandom
internal class CsvAccountTransactionsExporterTest {
companion object {
const val AccountId = "DE00876543210123456789"
const val Currency = "EUR"
val BookingDateDay = 26 // one day before value date
val ValueDateDay = 27
val DateMonth = 3
val DateYear = 1988
val BookingDate = Date(DateYear - 1900, DateMonth - 1, BookingDateDay)
val ValueDate = Date(DateYear - 1900, DateMonth - 1, ValueDateDay)
const val ReferenceWithUmlaute = "Was für ein schöner Verwendungszweck!"
const val AsciiReference = "A total normal reference"
const val Amount1String = "84.23"
val Amount1 = BigDecimal(Amount1String)
const val OtherParty1Name = "Nelson Mandela"
const val OtherParty1BankCode = "ABCDEFGH123"
const val OtherParty1AccountId = "SA99012345679876543210"
const val BookingText1 = "Überweisung"
const val Amount2String = "-123.45"
val Amount2 = BigDecimal(Amount2String)
val OtherParty2Name: String? = null
val OtherParty2BankCode: String? = null
val OtherParty2AccountId : String? = null
const val BookingText2 = "Bargeldabhebung"
}
private val defaultLocale = Locale.getDefault(Locale.Category.FORMAT)
@AfterEach
fun tearDown() {
setLocale(defaultLocale) // restore locale
}
@Test
fun exportWithEnglishLocale() {
setLocale(Locale.US)
val underTest = CsvAccountTransactionsExporter() // has to be created after locale is set as otherwise DateFormat works with the wrong locale
val transactions = createTransactions(2)
val writer = StringWriter()
underTest.export(writer, transactions)
val result = writer.toString()
assertThat(result).contains(Amount1String)
assertThat(result).contains(Amount2String)
assertThat(result).contains("$DateMonth/$BookingDateDay/${DateYear - 1900}")
assertThat(result).contains("$DateMonth/$ValueDateDay/${DateYear - 1900}")
assertThat(result).contains(ReferenceWithUmlaute, AsciiReference)
// assertThat(countOccurrences(result, ',')).isEqualTo(calculateCountSeparators(2)) // actually first thought using the German standard delimiter ';', but now sticking with ','
}
@Test
fun exportWithGermanLocale() {
setLocale(Locale.GERMANY)
val underTest = CsvAccountTransactionsExporter() // has to be created after locale is set as otherwise DateFormat works with the wrong locale
val transactions = createTransactions(2)
val writer = StringWriter()
underTest.export(writer, transactions)
val result = writer.toString()
assertThat(result).contains(Amount1String.replace('.', ','))
assertThat(result).contains(Amount2String.replace('.', ','))
assertThat(result).contains("$BookingDateDay.0$DateMonth.$DateYear")
assertThat(result).contains("$ValueDateDay.0$DateMonth.$DateYear")
assertThat(result).contains(ReferenceWithUmlaute, AsciiReference)
// assertThat(countOccurrences(result, ',')).isEqualTo(calculateCountSeparators(2)) // actually first thought using the German standard delimiter ';', but now sticking with ','
}
private fun createTransactions(count: Int): List<AccountTransaction> {
val transactions = mutableListOf<AccountTransaction>()
IntRange(0, count - 1).forEach { index ->
transactions.add(createTransaction(index))
}
return transactions
}
private fun createTransaction(index: Int): AccountTransaction {
return when (index) {
0 -> createTransaction(Amount1, ReferenceWithUmlaute, OtherParty1Name, OtherParty1BankCode, OtherParty1AccountId, BookingText1)
1 -> createTransaction(Amount2, AsciiReference, OtherParty2Name, OtherParty2BankCode, OtherParty2AccountId, BookingText2)
else -> createRandomTransaction()
}
}
private fun createRandomTransaction(): AccountTransaction {
val random = ThreadLocalRandom.current()
val amount = random.nextDouble(-1_000_000.0, 1_000_000.0)
return createTransaction(BigDecimal.valueOf(amount), "")
}
private fun createTransaction(amount: BigDecimal, reference: String, otherPartyName: String? = null, otherPartyBankCode: String? = null,
otherPartyAccountId: String? = null, bookingText: String? = null): AccountTransaction {
return AccountTransaction(
AccountId, amount, Currency, reference, BookingDate, ValueDate, otherPartyName, otherPartyBankCode, otherPartyAccountId, bookingText
)
}
private fun countOccurrences(string: String, characterToFind: Char): Int {
var countOccurrences = 0
for (char in string) {
if (char == characterToFind) {
countOccurrences++
}
}
return countOccurrences
}
private fun calculateCountSeparators(countTransactions: Int): Int {
return (countTransactions + 1) * // + 1 cause of header row
(10 - 1) // - 1 cause for the last column no separator gets printed
}
private fun setLocale(locale: Locale) {
Locale.setDefault(Locale.Category.FORMAT, locale)
}
}

View File

@ -97,6 +97,8 @@ dependencies {
implementation project(':LuceneBankingPersistence') implementation project(':LuceneBankingPersistence')
implementation project(':RoomBankingPersistence') implementation project(':RoomBankingPersistence')
implementation project(':CsvAccountTransactionsImporterAndExporter')
implementation "net.dankito.text.extraction:itext2-text-extractor:$textExtractorVersion" implementation "net.dankito.text.extraction:itext2-text-extractor:$textExtractorVersion"
implementation "net.dankito.text.extraction:pdfbox-android-text-extractor:$textExtractorVersion" implementation "net.dankito.text.extraction:pdfbox-android-text-extractor:$textExtractorVersion"

View File

@ -40,12 +40,13 @@ class MainActivity : BaseActivity() {
private lateinit var floatingActionMenuButton: MainActivityFloatingActionMenuButton private lateinit var floatingActionMenuButton: MainActivityFloatingActionMenuButton
private val permissionsService: IPermissionsService = PermissionsService(this)
@Inject @Inject
protected lateinit var presenter: BankingPresenter protected lateinit var presenter: BankingPresenter
@Inject
protected lateinit var permissionsService: IPermissionsService
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)

View File

@ -10,6 +10,7 @@ import net.dankito.banking.ui.android.dialogs.EnterTanDialog
import net.dankito.banking.ui.android.dialogs.SendMessageLogDialog import net.dankito.banking.ui.android.dialogs.SendMessageLogDialog
import net.dankito.banking.ui.android.dialogs.TransferMoneyDialog import net.dankito.banking.ui.android.dialogs.TransferMoneyDialog
import net.dankito.banking.ui.android.dialogs.settings.ProtectAppSettingsDialog import net.dankito.banking.ui.android.dialogs.settings.ProtectAppSettingsDialog
import net.dankito.banking.ui.android.dialogs.settings.SettingsDialog
import net.dankito.banking.ui.android.dialogs.settings.SettingsDialogBase import net.dankito.banking.ui.android.dialogs.settings.SettingsDialogBase
import net.dankito.banking.ui.android.home.HomeFragment import net.dankito.banking.ui.android.home.HomeFragment
import net.dankito.banking.ui.android.views.BiometricAuthenticationButton import net.dankito.banking.ui.android.views.BiometricAuthenticationButton
@ -43,6 +44,8 @@ interface BankingComponent {
fun inject(settingsDialogBase: SettingsDialogBase) fun inject(settingsDialogBase: SettingsDialogBase)
fun inject(settingsDialog: SettingsDialog)
fun inject(protectAppSettingsDialog: ProtectAppSettingsDialog) fun inject(protectAppSettingsDialog: ProtectAppSettingsDialog)
fun inject(biometricAuthenticationButton: BiometricAuthenticationButton) fun inject(biometricAuthenticationButton: BiometricAuthenticationButton)

View File

@ -29,6 +29,8 @@ import net.dankito.text.extraction.TextExtractorRegistry
import net.dankito.text.extraction.pdf.PdfBoxAndroidPdfTextExtractor import net.dankito.text.extraction.pdf.PdfBoxAndroidPdfTextExtractor
import net.dankito.text.extraction.pdf.iText2PdfTextExtractor import net.dankito.text.extraction.pdf.iText2PdfTextExtractor
import net.dankito.utils.ThreadPool import net.dankito.utils.ThreadPool
import net.dankito.utils.android.permissions.IPermissionsService
import net.dankito.utils.android.permissions.PermissionsService
import net.dankito.utils.web.client.IWebClient import net.dankito.utils.web.client.IWebClient
import net.dankito.utils.web.client.OkHttpWebClient import net.dankito.utils.web.client.OkHttpWebClient
import javax.inject.Named import javax.inject.Named
@ -87,6 +89,13 @@ class BankingModule(private val applicationContext: Context) {
} }
@Provides
@Singleton
fun providePermissionsService() : IPermissionsService {
return PermissionsService(mainActivity)
}
@Provides @Provides
@Singleton @Singleton
fun provideAuthenticationService(biometricAuthenticationService: IBiometricAuthenticationService, persistence: IBankingPersistence, fun provideAuthenticationService(biometricAuthenticationService: IBiometricAuthenticationService, persistence: IBankingPersistence,

View File

@ -1,24 +1,41 @@
package net.dankito.banking.ui.android.dialogs.settings package net.dankito.banking.ui.android.dialogs.settings
import android.os.Bundle import android.os.Bundle
import android.view.* import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.appcompat.content.res.AppCompatResources import androidx.appcompat.content.res.AppCompatResources
import androidx.fragment.app.FragmentActivity import androidx.fragment.app.FragmentActivity
import kotlinx.android.synthetic.main.dialog_settings.* import kotlinx.android.synthetic.main.dialog_settings.*
import kotlinx.android.synthetic.main.dialog_settings.view.* import kotlinx.android.synthetic.main.dialog_settings.view.*
import net.codinux.banking.tools.importerexporter.CsvAccountTransactionsExporter
import net.codinux.banking.tools.importerexporter.model.AccountTransaction
import net.dankito.banking.ui.android.R import net.dankito.banking.ui.android.R
import net.dankito.banking.ui.android.adapter.BankDataAdapterItem import net.dankito.banking.ui.android.adapter.BankDataAdapterItem
import net.dankito.banking.ui.android.adapter.FastAdapterRecyclerView import net.dankito.banking.ui.android.adapter.FastAdapterRecyclerView
import net.dankito.banking.ui.android.di.BankingComponent
import net.dankito.banking.ui.model.IAccountTransaction
import net.dankito.banking.ui.model.TypedBankData import net.dankito.banking.ui.model.TypedBankData
import net.dankito.filechooserdialog.FileChooserDialog
import net.dankito.filechooserdialog.model.FileChooserDialogConfig
import net.dankito.utils.android.permissions.IPermissionsService
import java.io.File
import java.text.SimpleDateFormat
import javax.inject.Inject
open class SettingsDialog : SettingsDialogBase() { open class SettingsDialog : SettingsDialogBase() {
companion object { companion object {
val ExportTransactionsDateFormat = SimpleDateFormat("yyyyMMdd")
const val DialogTag = "SettingsDialog" const val DialogTag = "SettingsDialog"
} }
@Inject
protected lateinit var permissionsService: IPermissionsService
protected lateinit var banksAdapter: FastAdapterRecyclerView<BankDataAdapterItem> protected lateinit var banksAdapter: FastAdapterRecyclerView<BankDataAdapterItem>
protected var banksChangedListener = { _: List<TypedBankData> -> protected var banksChangedListener = { _: List<TypedBankData> ->
@ -26,6 +43,11 @@ open class SettingsDialog : SettingsDialogBase() {
} }
init {
BankingComponent.component.inject(this)
}
fun show(activity: FragmentActivity) { fun show(activity: FragmentActivity) {
show(activity, DialogTag) show(activity, DialogTag)
} }
@ -59,6 +81,8 @@ open class SettingsDialog : SettingsDialogBase() {
btnSetAppProtection.setOnClickListener { navigateToProtectAppSettingsDialog() } btnSetAppProtection.setOnClickListener { navigateToProtectAppSettingsDialog() }
selectLockAppAfter.periodInMinutes = presenter.appSettings.lockAppAfterMinutes selectLockAppAfter.periodInMinutes = presenter.appSettings.lockAppAfterMinutes
btnExportAccountTransactions.setOnClickListener { exportAccountTransactions() }
// on Pre Lollipop devices setting vector drawables in xml is not supported -> set left drawable here // on Pre Lollipop devices setting vector drawables in xml is not supported -> set left drawable here
val sendIcon = AppCompatResources.getDrawable(context, R.drawable.ic_baseline_send_24) val sendIcon = AppCompatResources.getDrawable(context, R.drawable.ic_baseline_send_24)
btnShowSendMessageLogDialog.setCompoundDrawablesWithIntrinsicBounds(sendIcon, null, null, null) btnShowSendMessageLogDialog.setCompoundDrawablesWithIntrinsicBounds(sendIcon, null, null, null)
@ -97,6 +121,62 @@ open class SettingsDialog : SettingsDialogBase() {
} }
} }
protected open fun exportAccountTransactions() {
val initialDirectory = presenter.appSettings.lastSelectedExportFolder?.let { File(it) }
val suggestedFilename = getExportCsvSuggestedFilename()
// val intent = Intent(Intent.ACTION_CREATE_DOCUMENT)
// intent.addCategory(Intent.CATEGORY_OPENABLE)
// intent.type = "text/csv"
//
// intent.putExtra(Intent.EXTRA_TITLE, suggestedFilename)
//
// startActivityForResult(intent, 1)
activity?.let { activity ->
val config = FileChooserDialogConfig(initialDirectory = initialDirectory, suggestedFilenameForSaveFileDialog = suggestedFilename)
FileChooserDialog().showSaveFileInFullscreenDialog(activity, permissionsService, config) { _, selectedFile ->
selectedFile?.let {
val transactions = presenter.allTransactions.map { mapTransaction(it) }
CsvAccountTransactionsExporter().export(selectedFile, transactions)
presenter.appSettings.lastSelectedExportFolder = selectedFile.parentFile.absolutePath
presenter.appSettingsChanged()
}
}
}
}
// TODO: this is almost the same code as in JavaFX MainMenuBar.getExportCsvSuggestedFilename() -> merge
protected open fun getExportCsvSuggestedFilename(): String? {
val transactions = presenter.allTransactions
val transactionsDates = transactions.map { it.valueDate }
val transactionsStartDate = transactionsDates.min()
val transactionsEndDate = transactionsDates.max()
return context?.getString(R.string.dialog_settings_export_account_transactions_suggested_file_name,
transactionsStartDate?.let { ExportTransactionsDateFormat.format(it) } ?: "", transactionsEndDate?.let { ExportTransactionsDateFormat.format(it) } ?: "")
}
// TODO: this is exactly the same code as in JavaFX MainMenuBar.mapTransaction() -> merge
protected open fun mapTransaction(transaction: IAccountTransaction): AccountTransaction {
return AccountTransaction(
transaction.account.iban ?: transaction.account.identifier,
transaction.amount,
transaction.currency,
transaction.reference,
transaction.bookingDate,
transaction.valueDate,
transaction.otherPartyName,
transaction.otherPartyBankCode,
transaction.otherPartyAccountId,
transaction.bookingText
)
}
protected open fun reorderedBanks(oldPosition: Int, oldItem: TypedBankData, newPosition: Int, newItem: TypedBankData) { protected open fun reorderedBanks(oldPosition: Int, oldItem: TypedBankData, newPosition: Int, newItem: TypedBankData) {
oldItem.displayIndex = oldPosition oldItem.displayIndex = oldPosition
newItem.displayIndex = newPosition newItem.displayIndex = newPosition

View File

@ -115,6 +115,21 @@
/> />
<Button
android:id="@+id/btnExportAccountTransactions"
android:layout_width="match_parent"
android:layout_height="@dimen/dialog_settings_navigate_to_sub_dialog_button_height"
android:layout_marginTop="@dimen/dialog_settings_navigate_to_sub_dialog_button_margin_top"
style="?android:attr/buttonBarButtonStyle"
android:gravity="start|center_vertical"
android:textAlignment="gravity"
android:textAllCaps="false"
android:textColor="@color/formButtonsTextColor"
android:textSize="@dimen/dialog_settings_navigate_to_sub_dialog_button_text_size"
android:text="@string/dialog_settings_export_account_transactions_title"
/>
<!-- left drawable is set in code as pre Lollipop devices don't support setting vector drawables in xml --> <!-- left drawable is set in code as pre Lollipop devices don't support setting vector drawables in xml -->
<Button <Button
android:id="@+id/btnShowSendMessageLogDialog" android:id="@+id/btnShowSendMessageLogDialog"

View File

@ -144,6 +144,8 @@
<string name="dialog_settings_update_accounts_automatically_after">Konten aktualisieren nach (kommt noch)</string> <string name="dialog_settings_update_accounts_automatically_after">Konten aktualisieren nach (kommt noch)</string>
<string name="dialog_settings_secure_app_data">Appdaten schützen</string> <string name="dialog_settings_secure_app_data">Appdaten schützen</string>
<string name="dialog_settings_lock_app_after">App sperren nach (kommt noch)</string> <string name="dialog_settings_lock_app_after">App sperren nach (kommt noch)</string>
<string name="dialog_settings_export_account_transactions_title">Umsätze exportieren</string>
<string name="dialog_settings_export_account_transactions_suggested_file_name">umsaetze_%1$s-%2$s.csv</string>
<string name="dialog_settings_send_message_log_title">Message Log senden</string> <string name="dialog_settings_send_message_log_title">Message Log senden</string>
<string name="dialog_protect_app_settings_title">Appzugangsschutz</string> <string name="dialog_protect_app_settings_title">Appzugangsschutz</string>

View File

@ -144,6 +144,8 @@
<string name="dialog_settings_update_accounts_automatically_after">Update accounts after (to be implemented)</string> <string name="dialog_settings_update_accounts_automatically_after">Update accounts after (to be implemented)</string>
<string name="dialog_settings_secure_app_data">Secure app data</string> <string name="dialog_settings_secure_app_data">Secure app data</string>
<string name="dialog_settings_lock_app_after">Lock app after (to be implemented)</string> <string name="dialog_settings_lock_app_after">Lock app after (to be implemented)</string>
<string name="dialog_settings_export_account_transactions_title">Export account transactions</string>
<string name="dialog_settings_export_account_transactions_suggested_file_name">transactions_%1$s-%2$s.csv</string>
<string name="dialog_settings_send_message_log_title">Send message log</string> <string name="dialog_settings_send_message_log_title">Send message log</string>
<string name="dialog_protect_app_settings_title">App protection</string> <string name="dialog_protect_app_settings_title">App protection</string>

View File

@ -39,6 +39,8 @@ dependencies {
implementation project(':LuceneBankingPersistence') implementation project(':LuceneBankingPersistence')
implementation project(':CsvAccountTransactionsImporterAndExporter')
implementation "net.dankito.text.extraction:poppler-text-extractor:$textExtractorVersion" implementation "net.dankito.text.extraction:poppler-text-extractor:$textExtractorVersion"
implementation "net.dankito.text.extraction:pdfbox-text-extractor:$textExtractorVersion" implementation "net.dankito.text.extraction:pdfbox-text-extractor:$textExtractorVersion"

View File

@ -5,6 +5,9 @@ import javafx.scene.input.KeyCode
import javafx.scene.input.KeyCodeCombination import javafx.scene.input.KeyCodeCombination
import javafx.scene.input.KeyCombination import javafx.scene.input.KeyCombination
import javafx.stage.FileChooser import javafx.stage.FileChooser
import net.codinux.banking.tools.importerexporter.CsvAccountTransactionsExporter
import net.codinux.banking.tools.importerexporter.model.AccountTransaction
import net.dankito.banking.ui.model.IAccountTransaction
import net.dankito.utils.multiplatform.toFile import net.dankito.utils.multiplatform.toFile
import net.dankito.banking.ui.model.moneytransfer.ExtractTransferMoneyDataFromPdfResult import net.dankito.banking.ui.model.moneytransfer.ExtractTransferMoneyDataFromPdfResult
import net.dankito.banking.ui.model.moneytransfer.ExtractTransferMoneyDataFromPdfResultType import net.dankito.banking.ui.model.moneytransfer.ExtractTransferMoneyDataFromPdfResultType
@ -13,10 +16,16 @@ import net.dankito.utils.javafx.ui.dialogs.JavaFXDialogService
import net.dankito.utils.javafx.ui.extensions.fixedHeight import net.dankito.utils.javafx.ui.extensions.fixedHeight
import tornadofx.* import tornadofx.*
import java.io.File import java.io.File
import java.text.SimpleDateFormat
open class MainMenuBar(protected val presenter: BankingPresenter) : View() { open class MainMenuBar(protected val presenter: BankingPresenter) : View() {
companion object {
val ExportTransactionsDateFormat = SimpleDateFormat("yyyyMMdd")
}
protected val areAccountsThatCanTransferMoneyAdded = SimpleBooleanProperty() protected val areAccountsThatCanTransferMoneyAdded = SimpleBooleanProperty()
protected var lastSelectedFolder: File? = null protected var lastSelectedFolder: File? = null
@ -58,6 +67,12 @@ open class MainMenuBar(protected val presenter: BankingPresenter) : View() {
} }
} }
menu(messages["main.window.menu.file.export"]) {
item(messages["main.window.menu.file.export.csv"]) {
action { exportAccountTransactions() }
}
}
separator() separator()
item(messages["main.window.menu.file.quit"], KeyCodeCombination(KeyCode.Q, KeyCombination.SHORTCUT_DOWN)) { item(messages["main.window.menu.file.quit"], KeyCodeCombination(KeyCode.Q, KeyCombination.SHORTCUT_DOWN)) {
@ -101,4 +116,58 @@ open class MainMenuBar(protected val presenter: BankingPresenter) : View() {
JavaFXDialogService().showErrorMessage(errorMessage, exception = result.error) JavaFXDialogService().showErrorMessage(errorMessage, exception = result.error)
} }
protected open fun exportAccountTransactions() {
val fileChooser = FileChooser()
fileChooser.extensionFilters.addAll(
FileChooser.ExtensionFilter("CSV files", "*.csv"),
FileChooser.ExtensionFilter("All files", "*.*")
)
fileChooser.initialDirectory = presenter.appSettings.lastSelectedExportFolder?.let { net.dankito.utils.multiplatform.File(it) }
fileChooser.initialFileName = getExportCsvSuggestedFilename()
fileChooser.showSaveDialog(currentWindow)?.let { selectedFile ->
presenter.appSettings.lastSelectedExportFolder = selectedFile.parent
presenter.appSettingsChanged()
var destinationFile = selectedFile
if (destinationFile.extension.isNullOrBlank()) {
destinationFile = File(destinationFile.absolutePath + ".csv")
}
val transactions = presenter.allTransactions.map { mapTransaction(it) }
CsvAccountTransactionsExporter().export(destinationFile, transactions)
}
}
// TODO: this is almost the same code as in JAndroid SettingsDialog.getExportCsvSuggestedFilename() -> merge
protected open fun getExportCsvSuggestedFilename(): String? {
val transactions = presenter.allTransactions
val transactionsDates = transactions.map { it.valueDate }
val transactionsStartDate = transactionsDates.min()
val transactionsEndDate = transactionsDates.max()
return String.format(messages["main.window.menu.file.export.csv.suggested.filename"], transactionsStartDate?.let { ExportTransactionsDateFormat.format(it) } ?: "",
transactionsEndDate?.let { ExportTransactionsDateFormat.format(it) } ?: "")
}
// TODO: this is exactly the same code as in Android SettingsDialog.mapTransaction() -> merge
protected open fun mapTransaction(transaction: IAccountTransaction): AccountTransaction {
return AccountTransaction(
transaction.account.iban ?: transaction.account.identifier,
transaction.amount,
transaction.currency,
transaction.reference,
transaction.bookingDate,
transaction.valueDate,
transaction.otherPartyName,
transaction.otherPartyBankCode,
transaction.otherPartyAccountId,
transaction.bookingText
)
}
} }

View File

@ -14,6 +14,9 @@ main.window.menu.file.new=New...
main.window.menu.file.new.account=Account main.window.menu.file.new.account=Account
main.window.menu.file.new.cash.transfer=Cash transfer main.window.menu.file.new.cash.transfer=Cash transfer
main.window.menu.file.new.cash.transfer.from.pdf=Cash transfer from PDF main.window.menu.file.new.cash.transfer.from.pdf=Cash transfer from PDF
main.window.menu.file.export=Export
main.window.menu.file.export.csv=CSV
main.window.menu.file.export.csv.suggested.filename=transactions_%1$s-%2$s.csv
main.window.menu.file.quit=Quit main.window.menu.file.quit=Quit

View File

@ -14,6 +14,9 @@ main.window.menu.file.new=Neu...
main.window.menu.file.new.account=Konto main.window.menu.file.new.account=Konto
main.window.menu.file.new.cash.transfer=Überweisung main.window.menu.file.new.cash.transfer=Überweisung
main.window.menu.file.new.cash.transfer.from.pdf=Überweisung aus PDF main.window.menu.file.new.cash.transfer.from.pdf=Überweisung aus PDF
main.window.menu.file.export=Export
main.window.menu.file.export.csv=CSV
main.window.menu.file.export.csv.suggested.filename=umsaetze_%1$s-%2$s.csv
main.window.menu.file.quit=Beenden main.window.menu.file.quit=Beenden

View File

@ -9,7 +9,8 @@ open class AppSettings(
open var screenshotsAllowed: Boolean = false, // TODO: implement open var screenshotsAllowed: Boolean = false, // TODO: implement
open var flickerCodeSettings: TanMethodSettings? = null, open var flickerCodeSettings: TanMethodSettings? = null,
open var qrCodeSettings: TanMethodSettings? = null, open var qrCodeSettings: TanMethodSettings? = null,
open var photoTanSettings: TanMethodSettings? = null open var photoTanSettings: TanMethodSettings? = null,
open var lastSelectedExportFolder: String? = null // File is not that easily persistable so modeled it as string
) { ) {
companion object { companion object {