Implemented exporting account transactions as .csv at least for JVM and Android
This commit is contained in:
parent
704ac55239
commit
bc3439baa5
|
@ -23,6 +23,8 @@ ext {
|
|||
|
||||
textInfoExtractorVersion = "1.0.1"
|
||||
|
||||
commonsCsvVersion = "1.8"
|
||||
|
||||
|
||||
hbci4jVersion = '3.1.37'
|
||||
|
||||
|
@ -45,7 +47,7 @@ ext {
|
|||
androidTargetSdkVersion = 28
|
||||
|
||||
|
||||
fileChooserDialogVersion = "1.2.0-androidx"
|
||||
fileChooserDialogVersion = "1.3.0-androidx"
|
||||
|
||||
androidUtilsVersion = '1.1.1-SNAPSHOT'
|
||||
|
||||
|
|
|
@ -190,7 +190,7 @@ open class RoomBankingPersistence(protected open val applicationContext: Context
|
|||
|
||||
override fun saveOrUpdateAppSettings(appSettings: AppSettings) {
|
||||
val mapped = net.dankito.banking.persistence.model.AppSettings(appSettings.automaticallyUpdateAccountsAfterMinutes,
|
||||
appSettings.lockAppAfterMinutes, appSettings.screenshotsAllowed)
|
||||
appSettings.lockAppAfterMinutes, appSettings.screenshotsAllowed, appSettings.lastSelectedExportFolder)
|
||||
database.appSettingsDao().saveOrUpdate(mapped)
|
||||
|
||||
saveOrUpdateTanMethodSettings(appSettings.flickerCodeSettings, FlickerCodeTanMethodSettingsId)
|
||||
|
@ -215,6 +215,7 @@ open class RoomBankingPersistence(protected open val applicationContext: Context
|
|||
settings.automaticallyUpdateAccountsAfterMinutes = persistedSettings.automaticallyUpdateAccountsAfterMinutes
|
||||
settings.lockAppAfterMinutes = persistedSettings.lockAppAfterMinutes
|
||||
settings.screenshotsAllowed = persistedSettings.screenshotsAllowed
|
||||
settings.lastSelectedExportFolder = persistedSettings.lastSelectedExportFolder
|
||||
}
|
||||
|
||||
settings.flickerCodeSettings = findTanMethodSettings(FlickerCodeTanMethodSettingsId, tanMethodSettings)
|
||||
|
|
|
@ -10,7 +10,8 @@ import net.dankito.banking.ui.model.settings.AppSettings
|
|||
open class AppSettings(
|
||||
open var automaticallyUpdateAccountsAfterMinutes: Int? = AppSettings.DefaultAutomaticallyUpdateAccountsAfterMinutes,
|
||||
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)
|
||||
|
|
|
@ -71,10 +71,12 @@ project(':fints4kRest').projectDir = "$rootDir/rest/fints4kRest/" as File
|
|||
include ':BankFinder'
|
||||
include ':LuceneBankFinder'
|
||||
include ':BankListCreator'
|
||||
include ':CsvAccountTransactionsImporterAndExporter'
|
||||
include ':EpcQrCodeParser'
|
||||
|
||||
|
||||
project(':BankFinder').projectDir = "$rootDir/tools/BankFinder/" as File
|
||||
project(':LuceneBankFinder').projectDir = "$rootDir/tools/LuceneBankFinder/" 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
|
||||
|
|
|
@ -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"
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -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>)
|
||||
|
||||
}
|
|
@ -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>
|
||||
|
||||
}
|
|
@ -0,0 +1,4 @@
|
|||
package net.codinux.banking.tools.importerexporter
|
||||
|
||||
|
||||
interface IAccountTransactionsImporterExporter : IAccountTransactionsImporter, IAccountTransactionsExporter
|
|
@ -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?
|
||||
) {
|
||||
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -97,6 +97,8 @@ dependencies {
|
|||
implementation project(':LuceneBankingPersistence')
|
||||
implementation project(':RoomBankingPersistence')
|
||||
|
||||
implementation project(':CsvAccountTransactionsImporterAndExporter')
|
||||
|
||||
|
||||
implementation "net.dankito.text.extraction:itext2-text-extractor:$textExtractorVersion"
|
||||
implementation "net.dankito.text.extraction:pdfbox-android-text-extractor:$textExtractorVersion"
|
||||
|
|
|
@ -40,12 +40,13 @@ class MainActivity : BaseActivity() {
|
|||
|
||||
private lateinit var floatingActionMenuButton: MainActivityFloatingActionMenuButton
|
||||
|
||||
private val permissionsService: IPermissionsService = PermissionsService(this)
|
||||
|
||||
|
||||
@Inject
|
||||
protected lateinit var presenter: BankingPresenter
|
||||
|
||||
@Inject
|
||||
protected lateinit var permissionsService: IPermissionsService
|
||||
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
|
|
@ -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.TransferMoneyDialog
|
||||
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.home.HomeFragment
|
||||
import net.dankito.banking.ui.android.views.BiometricAuthenticationButton
|
||||
|
@ -43,6 +44,8 @@ interface BankingComponent {
|
|||
|
||||
fun inject(settingsDialogBase: SettingsDialogBase)
|
||||
|
||||
fun inject(settingsDialog: SettingsDialog)
|
||||
|
||||
fun inject(protectAppSettingsDialog: ProtectAppSettingsDialog)
|
||||
|
||||
fun inject(biometricAuthenticationButton: BiometricAuthenticationButton)
|
||||
|
|
|
@ -29,6 +29,8 @@ import net.dankito.text.extraction.TextExtractorRegistry
|
|||
import net.dankito.text.extraction.pdf.PdfBoxAndroidPdfTextExtractor
|
||||
import net.dankito.text.extraction.pdf.iText2PdfTextExtractor
|
||||
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.OkHttpWebClient
|
||||
import javax.inject.Named
|
||||
|
@ -87,6 +89,13 @@ class BankingModule(private val applicationContext: Context) {
|
|||
}
|
||||
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun providePermissionsService() : IPermissionsService {
|
||||
return PermissionsService(mainActivity)
|
||||
}
|
||||
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideAuthenticationService(biometricAuthenticationService: IBiometricAuthenticationService, persistence: IBankingPersistence,
|
||||
|
|
|
@ -1,24 +1,41 @@
|
|||
package net.dankito.banking.ui.android.dialogs.settings
|
||||
|
||||
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.fragment.app.FragmentActivity
|
||||
import kotlinx.android.synthetic.main.dialog_settings.*
|
||||
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.adapter.BankDataAdapterItem
|
||||
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.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() {
|
||||
|
||||
companion object {
|
||||
val ExportTransactionsDateFormat = SimpleDateFormat("yyyyMMdd")
|
||||
|
||||
const val DialogTag = "SettingsDialog"
|
||||
}
|
||||
|
||||
|
||||
@Inject
|
||||
protected lateinit var permissionsService: IPermissionsService
|
||||
|
||||
protected lateinit var banksAdapter: FastAdapterRecyclerView<BankDataAdapterItem>
|
||||
|
||||
protected var banksChangedListener = { _: List<TypedBankData> ->
|
||||
|
@ -26,6 +43,11 @@ open class SettingsDialog : SettingsDialogBase() {
|
|||
}
|
||||
|
||||
|
||||
init {
|
||||
BankingComponent.component.inject(this)
|
||||
}
|
||||
|
||||
|
||||
fun show(activity: FragmentActivity) {
|
||||
show(activity, DialogTag)
|
||||
}
|
||||
|
@ -59,6 +81,8 @@ open class SettingsDialog : SettingsDialogBase() {
|
|||
btnSetAppProtection.setOnClickListener { navigateToProtectAppSettingsDialog() }
|
||||
selectLockAppAfter.periodInMinutes = presenter.appSettings.lockAppAfterMinutes
|
||||
|
||||
btnExportAccountTransactions.setOnClickListener { exportAccountTransactions() }
|
||||
|
||||
// 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)
|
||||
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) {
|
||||
oldItem.displayIndex = oldPosition
|
||||
newItem.displayIndex = newPosition
|
||||
|
|
|
@ -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 -->
|
||||
<Button
|
||||
android:id="@+id/btnShowSendMessageLogDialog"
|
||||
|
|
|
@ -144,6 +144,8 @@
|
|||
<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_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_protect_app_settings_title">Appzugangsschutz</string>
|
||||
|
|
|
@ -144,6 +144,8 @@
|
|||
<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_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_protect_app_settings_title">App protection</string>
|
||||
|
|
|
@ -39,6 +39,8 @@ dependencies {
|
|||
|
||||
implementation project(':LuceneBankingPersistence')
|
||||
|
||||
implementation project(':CsvAccountTransactionsImporterAndExporter')
|
||||
|
||||
|
||||
implementation "net.dankito.text.extraction:poppler-text-extractor:$textExtractorVersion"
|
||||
implementation "net.dankito.text.extraction:pdfbox-text-extractor:$textExtractorVersion"
|
||||
|
|
|
@ -5,6 +5,9 @@ import javafx.scene.input.KeyCode
|
|||
import javafx.scene.input.KeyCodeCombination
|
||||
import javafx.scene.input.KeyCombination
|
||||
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.banking.ui.model.moneytransfer.ExtractTransferMoneyDataFromPdfResult
|
||||
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 tornadofx.*
|
||||
import java.io.File
|
||||
import java.text.SimpleDateFormat
|
||||
|
||||
|
||||
open class MainMenuBar(protected val presenter: BankingPresenter) : View() {
|
||||
|
||||
companion object {
|
||||
val ExportTransactionsDateFormat = SimpleDateFormat("yyyyMMdd")
|
||||
}
|
||||
|
||||
|
||||
protected val areAccountsThatCanTransferMoneyAdded = SimpleBooleanProperty()
|
||||
|
||||
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()
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
|
||||
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
|
||||
)
|
||||
}
|
||||
|
||||
}
|
|
@ -14,6 +14,9 @@ main.window.menu.file.new=New...
|
|||
main.window.menu.file.new.account=Account
|
||||
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.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
|
||||
|
||||
|
||||
|
|
|
@ -14,6 +14,9 @@ main.window.menu.file.new=Neu...
|
|||
main.window.menu.file.new.account=Konto
|
||||
main.window.menu.file.new.cash.transfer=Überweisung
|
||||
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
|
||||
|
||||
|
||||
|
|
|
@ -9,7 +9,8 @@ open class AppSettings(
|
|||
open var screenshotsAllowed: Boolean = false, // TODO: implement
|
||||
open var flickerCodeSettings: 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 {
|
||||
|
|
Loading…
Reference in New Issue