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 b0fb129df..951516841 100644 --- a/app/src/main/java/org/gnucash/android/export/Exporter.java +++ b/app/src/main/java/org/gnucash/android/export/Exporter.java @@ -53,7 +53,7 @@ * @author Ngewi Fet * @author Yongxin Wang */ -public abstract class Exporter implements Closeable { +public abstract class Exporter { /** * Application folder on external storage @@ -260,8 +260,7 @@ public String getExportMimeType() { return "text/plain"; } - @Override - public void close() throws IOException { + protected void close() throws IOException { mAccountsDbAdapter.close(); mBudgetsDbAdapter.close(); mCommoditiesDbAdapter.close(); diff --git a/app/src/main/java/org/gnucash/android/export/csv/CsvTransactionsExporter.java b/app/src/main/java/org/gnucash/android/export/csv/CsvTransactionsExporter.java deleted file mode 100644 index dbe0ca098..000000000 --- a/app/src/main/java/org/gnucash/android/export/csv/CsvTransactionsExporter.java +++ /dev/null @@ -1,172 +0,0 @@ -/* - * Copyright (c) 2018 Semyannikov Gleb - * - * 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.export.csv; - -import android.content.Context; -import android.database.Cursor; - -import androidx.annotation.NonNull; - -import org.gnucash.android.R; -import org.gnucash.android.export.ExportParams; -import org.gnucash.android.export.Exporter; -import org.gnucash.android.model.Account; -import org.gnucash.android.model.Money; -import org.gnucash.android.model.Split; -import org.gnucash.android.model.Transaction; -import org.gnucash.android.model.TransactionType; -import org.gnucash.android.util.PreferencesHelper; -import org.gnucash.android.util.TimestampHelper; -import org.joda.time.format.DateTimeFormat; -import org.joda.time.format.DateTimeFormatter; - -import java.io.FileWriter; -import java.io.IOException; -import java.util.Arrays; -import java.util.HashMap; -import java.util.List; -import java.util.Map; - -import timber.log.Timber; - -/** - * Creates a GnuCash CSV transactions representation of the accounts and transactions - * - * @author Semyannikov Gleb - */ -public class CsvTransactionsExporter extends Exporter { - - private final char mCsvSeparator; - - private final DateTimeFormatter dateFormat = DateTimeFormat.forPattern("yyyy-MM-dd"); - - /** - * Overloaded constructor. - * Creates an exporter with an already open database instance. - * - * @param context The context. - * @param params Parameters for the export - * @param bookUID The book UID. - */ - public CsvTransactionsExporter(@NonNull Context context, - @NonNull ExportParams params, - @NonNull String bookUID) { - super(context, params, bookUID); - mCsvSeparator = params.getCsvSeparator(); - } - - @Override - public List generateExport() throws ExporterException { - String outputFile = getExportCacheFilePath(); - - try (CsvWriter csvWriter = new CsvWriter(new FileWriter(outputFile), String.valueOf(mCsvSeparator))) { - generateExport(csvWriter); - close(); - } catch (IOException ex) { - Timber.e(ex, "Error exporting CSV"); - throw new ExporterException(mExportParams, ex); - } - - return Arrays.asList(outputFile); - } - - /** - * Write splits to CSV format - * - * @param splits Splits to be written - */ - private void writeSplitsToCsv(@NonNull List splits, @NonNull CsvWriter writer) throws IOException, Money.CurrencyMismatchException { - int index = 0; - - Map uidAccountMap = new HashMap<>(); - - for (Split split : splits) { - if (index++ > 0) { // the first split is on the same line as the transactions. But after that, we - writer.write("" // Date - + mCsvSeparator // Transaction ID - + mCsvSeparator // Number - + mCsvSeparator // Description - + mCsvSeparator // Notes - + mCsvSeparator // Commodity/Currency - + mCsvSeparator // Void Reason - + mCsvSeparator // Action - + mCsvSeparator // Memo - ); - } - writer.writeToken(split.getMemo()); - - //cache accounts so that we do not have to go to the DB each time - String accountUID = split.getAccountUID(); - Account account; - if (uidAccountMap.containsKey(accountUID)) { - account = uidAccountMap.get(accountUID); - } else { - account = mAccountsDbAdapter.getRecord(accountUID); - uidAccountMap.put(accountUID, account); - } - - writer.writeToken(account.getFullName()); - writer.writeToken(account.getName()); - - String sign = split.getType() == TransactionType.CREDIT ? "-" : ""; - writer.writeToken(sign + split.getQuantity().formattedString()); - writer.writeToken(sign + split.getQuantity().toLocaleString()); - writer.writeToken(String.valueOf(split.getReconcileState())); - if (split.getReconcileState() == Split.FLAG_RECONCILED) { - String recDateString = dateFormat.print(split.getReconcileDate().getTime()); - writer.writeToken(recDateString); - } else { - writer.writeToken(null); - } - writer.writeEndToken(split.getQuantity().div(split.getValue().toDouble()).toLocaleString()); - } - } - - private void generateExport(final CsvWriter csvWriter) throws ExporterException { - try { - List names = Arrays.asList(mContext.getResources().getStringArray(R.array.csv_transaction_headers)); - for (int i = 0; i < names.size(); i++) { - csvWriter.writeToken(names.get(i)); - } - csvWriter.newLine(); - - - Cursor cursor = mTransactionsDbAdapter.fetchTransactionsModifiedSince(mExportParams.getExportStartTime()); - Timber.d("Exporting %d transactions to CSV", cursor.getCount()); - while (cursor.moveToNext()) { - Transaction transaction = mTransactionsDbAdapter.buildModelInstance(cursor); - csvWriter.writeToken(dateFormat.print(transaction.getTimeMillis())); - csvWriter.writeToken(transaction.getUID()); - csvWriter.writeToken(null); //Transaction number - - csvWriter.writeToken(transaction.getDescription()); - csvWriter.writeToken(transaction.getNote()); - - csvWriter.writeToken("CURRENCY::" + transaction.getCurrencyCode()); - csvWriter.writeToken(null); // Void Reason - csvWriter.writeToken(null); // Action - writeSplitsToCsv(transaction.getSplits(), csvWriter); - } - cursor.close(); - - PreferencesHelper.setLastExportTime(TimestampHelper.getTimestampFromNow()); - } catch (Exception e) { - Timber.e(e); - throw new ExporterException(mExportParams, e); - } - } -} diff --git a/app/src/main/java/org/gnucash/android/export/csv/CsvTransactionsExporter.kt b/app/src/main/java/org/gnucash/android/export/csv/CsvTransactionsExporter.kt new file mode 100644 index 000000000..e691b6253 --- /dev/null +++ b/app/src/main/java/org/gnucash/android/export/csv/CsvTransactionsExporter.kt @@ -0,0 +1,130 @@ +/* + * Copyright (c) 2018-2024 GnuCash Android developers + * + * 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.export.csv + +import android.content.Context +import org.gnucash.android.R +import org.gnucash.android.export.ExportParams +import org.gnucash.android.export.Exporter +import org.gnucash.android.model.Account +import org.gnucash.android.model.Money.CurrencyMismatchException +import org.gnucash.android.model.Split +import org.gnucash.android.model.TransactionType +import org.gnucash.android.util.PreferencesHelper +import org.gnucash.android.util.TimestampHelper +import org.joda.time.format.DateTimeFormat +import timber.log.Timber +import java.io.FileWriter +import java.io.IOException +import java.util.Arrays + +/** + * Creates a GnuCash CSV transactions representation of the accounts and transactions + * + * @author Semyannikov Gleb @gmail.com> + */ +class CsvTransactionsExporter(context: Context, + params: ExportParams, + bookUID: String) : Exporter(context, params, bookUID) { + private val mCsvSeparator = params.csvSeparator + private val dateFormat = DateTimeFormat.forPattern("yyyy-MM-dd") + + @Throws(ExporterException::class) + override fun generateExport(): List { + val outputFile = getExportCacheFilePath() + var csvWriter: CsvWriter? = null + + try { + csvWriter = CsvWriter(FileWriter(outputFile), mCsvSeparator.toString()) + generateExport(csvWriter) + return listOf(outputFile) + } catch (ex: IOException) { + Timber.e(ex, "Error exporting CSV") + throw ExporterException(mExportParams, ex) + } finally { + csvWriter?.close() + close() + } + } + + @Throws(IOException::class, CurrencyMismatchException::class) + private fun writeSplitsToCsv(splits: List, writer: CsvWriter) { + val accountCache: MutableMap = HashMap() + for ((index, split) in splits.withIndex()) { + if (index > 0) { + // The first split is on the same line as the transactions. But after that, the + // transaction-specific fields are empty. + writer.write("" // Date + + mCsvSeparator // Transaction ID + + mCsvSeparator // Number + + mCsvSeparator // Description + + mCsvSeparator // Notes + + mCsvSeparator // Commodity/Currency + + mCsvSeparator // Void Reason + + mCsvSeparator // Action + + mCsvSeparator // Memo + ) + } + writer.writeToken(split.memo) + val accountUID = split.accountUID!! + val account = accountCache.getOrPut(accountUID) { + mAccountsDbAdapter.getRecord(accountUID) + } + writer.writeToken(account.fullName) + writer.writeToken(account.name) + val sign = if (split.type == TransactionType.CREDIT) "-" else "" + writer.writeToken(sign + split.quantity!!.formattedString()) + writer.writeToken(sign + split.quantity!!.formattedStringWithoutSymbol()) + writer.writeToken(split.reconcileState.toString()) + if (split.reconcileState == Split.FLAG_RECONCILED) { + val recDateString = dateFormat.print(split.reconcileDate.getTime()) + writer.writeToken(recDateString) + } else { + writer.writeToken(null) + } + writer.writeEndToken(split.quantity!!.div(split.value!!).formattedStringWithoutSymbol()) + } + } + + @Throws(ExporterException::class) + private fun generateExport(csvWriter: CsvWriter) { + try { + mContext.resources.getStringArray(R.array.csv_transaction_headers).forEach { + csvWriter.writeToken(it) + } + csvWriter.newLine() + val cursor = mTransactionsDbAdapter.fetchTransactionsModifiedSince(mExportParams.exportStartTime) + Timber.d("Exporting %d transactions to CSV", cursor.count) + while (cursor.moveToNext()) { + val transaction = mTransactionsDbAdapter.buildModelInstance(cursor) + csvWriter.writeToken(dateFormat.print(transaction.timeMillis)) + csvWriter.writeToken(transaction.uID) + csvWriter.writeToken(null) // Transaction number + csvWriter.writeToken(transaction.description) + csvWriter.writeToken(transaction.note) + csvWriter.writeToken("CURRENCY::${transaction.currencyCode}") + csvWriter.writeToken(null) // Void Reason + csvWriter.writeToken(null) // Action + writeSplitsToCsv(transaction.splits, csvWriter) + } + cursor.close() + PreferencesHelper.setLastExportTime(TimestampHelper.getTimestampFromNow()) + } catch (e: Exception) { + Timber.e(e, "Error while exporting transactions to CSV") + throw ExporterException(mExportParams, e) + } + } +} diff --git a/app/src/main/kotlin/org/gnucash/android/model/Money.kt b/app/src/main/kotlin/org/gnucash/android/model/Money.kt index 1726d5d6b..27d45f8b3 100644 --- a/app/src/main/kotlin/org/gnucash/android/model/Money.kt +++ b/app/src/main/kotlin/org/gnucash/android/model/Money.kt @@ -276,6 +276,22 @@ class Money : Number, Comparable, Parcelable { return currencyFormat.format(_amount) } + /** + * Returns a string representation of the Money object formatted according to + * the `locale` without the currency symbol. + * The output precision is limited to the number of fractional digits supported by the currency + * + * @param locale Locale to use when formatting the object. Defaults to Locale.getDefault(). + * @return String containing formatted Money representation + */ + @JvmOverloads + fun formattedStringWithoutSymbol(locale: Locale = Locale.getDefault()): String { + val format = NumberFormat.getNumberInstance(locale) + format.setMinimumFractionDigits(commodity.smallestFractionDigits) + format.setMaximumFractionDigits(commodity.smallestFractionDigits) + return format.format(_amount) + } + /** * Returns a new Money object whose amount is the negated value of this object amount. * The original `Money` object remains unchanged. @@ -419,7 +435,7 @@ class Money : Number, Comparable, Parcelable { * * * This string is not locale-formatted. The decimal operator is a period (.) - * For a locale-formatted version, see the method [.toLocaleString] + * For a locale-formatted version, see the method `formattedStringWithoutSymbol()`. * * @return String representation of the amount (without currency) of the Money object */ @@ -427,15 +443,6 @@ class Money : Number, Comparable, Parcelable { return _amount.setScale(commodity.smallestFractionDigits, roundingMode).toPlainString() } - /** - * Returns a locale-specific representation of the amount of the Money object (excluding the currency) - * - * @return String representation of the amount (without currency) of the Money object - */ - fun toLocaleString(): String { - return String.format(Locale.getDefault(), "%.2f", toDouble()) - } - /** * Returns the string representation of the Money object (value + currency) formatted according * to the default locale