From ad478f69aa2eed37f8f98c66fa6fd5523f7c1d32 Mon Sep 17 00:00:00 2001 From: Ngewi Fet Date: Mon, 4 Jun 2018 18:58:03 +0200 Subject: [PATCH] Enable support for exporting transaction splits separately in CSV exports Modified transactions CSV exporter to produce similar CSV like GnuCash desktop - #756 Localize transaction CSV headers --- .../export/csv/CsvTransactionsExporter.java | 182 +++++------------- .../gnucash/android/export/csv/CsvWriter.java | 68 +++++-- .../java/org/gnucash/android/model/Money.java | 14 +- app/src/main/res/values/strings.xml | 18 ++ 4 files changed, 140 insertions(+), 142 deletions(-) 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 index 46ef62ab2..c0419f7c7 100644 --- a/app/src/main/java/org/gnucash/android/export/csv/CsvTransactionsExporter.java +++ b/app/src/main/java/org/gnucash/android/export/csv/CsvTransactionsExporter.java @@ -18,26 +18,24 @@ import android.database.Cursor; import android.database.sqlite.SQLiteDatabase; +import android.support.annotation.NonNull; import com.crashlytics.android.Crashlytics; +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 java.io.BufferedOutputStream; -import java.io.FileOutputStream; +import java.io.FileWriter; import java.io.IOException; -import java.io.OutputStreamWriter; import java.text.DateFormat; import java.text.SimpleDateFormat; import java.util.ArrayList; -import java.util.Collections; -import java.util.Comparator; +import java.util.Arrays; import java.util.Date; import java.util.List; import java.util.Locale; @@ -51,20 +49,7 @@ public class CsvTransactionsExporter extends Exporter{ private char mCsvSeparator; - private DateFormat dateFormat = new SimpleDateFormat("dd/MM/yyyy", Locale.US); - - private Comparator splitComparator = new Comparator() { - @Override - public int compare(Split o1, Split o2) { - if(o1.getType() == TransactionType.DEBIT - && o2.getType() == TransactionType.CREDIT) - return -1; - if (o1.getType() == TransactionType.CREDIT - && o2.getType() == TransactionType.DEBIT) - return 1; - return 0; - } - }; + private DateFormat dateFormat = new SimpleDateFormat("YYYY-MM-dd", Locale.US); /** * Construct a new exporter with export parameters @@ -90,26 +75,13 @@ public CsvTransactionsExporter(ExportParams params, SQLiteDatabase db) { @Override public List generateExport() throws ExporterException { - OutputStreamWriter writerStream = null; - CsvWriter writer = null; String outputFile = getExportCacheFilePath(); - try { - FileOutputStream fileOutputStream = new FileOutputStream(outputFile); - BufferedOutputStream bufferedOutputStream = new BufferedOutputStream(fileOutputStream); - writerStream = new OutputStreamWriter(bufferedOutputStream); - writer = new CsvWriter(writerStream); - generateExport(writer); + + try (CsvWriter csvWriter = new CsvWriter(new FileWriter(outputFile), "" + mCsvSeparator)){ + generateExport(csvWriter); } catch (IOException ex){ Crashlytics.log("Error exporting CSV"); Crashlytics.logException(ex); - } finally { - if (writerStream != null) { - try { - writerStream.close(); - } catch (IOException e) { - throw new ExporterException(mExportParams, e); - } - } } List exportedFiles = new ArrayList<>(); @@ -118,111 +90,63 @@ public List generateExport() throws ExporterException { return exportedFiles; } - private void write_split(final Transaction transaction, final Split split, final CsvWriter writer) throws IOException - { - String separator = mCsvSeparator + ""; - Account account = mAccountsDbAdapter.getRecord(split.getAccountUID()); - - // Date - Date date = new Date(transaction.getTimeMillis()); - writer.write(dateFormat.format(date) + separator); - // Account name - writer.write(account.getName() + separator); - // TODO:Number is not defined yet? - writer.write( separator); - // Description - writer.write(transaction.getDescription() + separator); - // Notes of transaction - writer.write(transaction.getNote() + separator); - // Memo - writer.write( - (split.getMemo()==null? - "":split.getMemo()) + separator); - // TODO:Category is not defined yet? - writer.write(separator); - // Type - writer.write(split.getType().name() + separator); - // TODO:Action is not defined yet? - writer.write(separator); - // Reconcile - writer.write(split.getReconcileState() + separator); - - // Changes - Money change = split.getFormattedQuantity().withCurrency(transaction.getCommodity()); - Money zero = Money.getZeroInstance().withCurrency(transaction.getCommodity()); - // To currency; From currency; To; From - if (change.isNegative()) { - writer.write(zero.toPlainString() + separator); - writer.write(change.abs().toPlainString() + separator); - writer.write(Money.getZeroInstance().toPlainString() + separator); - writer.write(split.getFormattedQuantity().abs().toPlainString() + separator); - } - else { - writer.write(change.abs().toPlainString() + separator); - writer.write(zero.toPlainString() + separator); - writer.write(split.getFormattedQuantity().abs().toPlainString() + separator); - writer.write(Money.getZeroInstance().toPlainString() + separator); + /** + * Write splits to CSV format + * @param splits Splits to be written + */ + private void writeSplitsToCsv(@NonNull List splits, @NonNull CsvWriter writer) throws IOException { + int index = 0; + for (Split split : splits) { + if (index++ > 0){ // the first split is on the same line as the transactions. But after that, we + writer.write("" + mCsvSeparator + mCsvSeparator + mCsvSeparator + mCsvSeparator + + mCsvSeparator + mCsvSeparator + mCsvSeparator + mCsvSeparator); + } + writer.writeToken(split.getMemo()); + Account account = mAccountsDbAdapter.getRecord(split.getAccountUID()); + 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("" + split.getReconcileState()); + if (split.getReconcileState() == Split.FLAG_RECONCILED) { + String recDateString = dateFormat.format(new Date(split.getReconcileDate().getTime())); + writer.writeToken(recDateString); + } else { + writer.writeToken(null); + } + writer.writeEndToken(split.getQuantity().divide(split.getValue()).toLocaleString()); } - - // TODO: What is price? - writer.write(separator); - writer.write(separator); } - public void generateExport(final CsvWriter writer) throws ExporterException { + private void generateExport(final CsvWriter csvWriter) throws ExporterException { try { - String separator = mCsvSeparator + ""; - List names = new ArrayList(); - names.add("Date"); - names.add("Account name"); - names.add("Number"); - names.add("Description"); - names.add("Notes"); - names.add("Memo"); - names.add("Category"); - names.add("Type"); - names.add("Action"); - names.add("Reconcile"); - names.add("To With Sym"); - names.add("From With Sym"); - names.add("To Num."); - names.add("From Num."); - names.add("To Rate/Price"); - names.add("From Rate/Price"); - - List transactions = mTransactionsDbAdapter.getAllTransactions(); - + List names = Arrays.asList(mContext.getResources().getStringArray(R.array.csv_transaction_headers)); for(int i = 0; i < names.size(); i++) { - writer.write(names.get(i) + separator); + csvWriter.writeToken(names.get(i)); } - writer.write("\n"); + csvWriter.newLine(); Cursor cursor = mTransactionsDbAdapter.fetchAllRecords(); - while (cursor.moveToNext()) - { + while (cursor.moveToNext()){ Transaction transaction = mTransactionsDbAdapter.buildModelInstance(cursor); - List splits = transaction.getSplits(); - Collections.sort(splits,splitComparator); - for (int j = 0; j < splits.size()/2; j++) { - Split split = splits.get(j); - Split pair = null; - for (int k = 0; k < splits.size(); k++) { - if (split.isPairOf(splits.get(k))) { - pair = splits.get(k); - } - } - - write_split(transaction, split, writer); - writer.write("\n"); - if (pair != null) { - write_split(transaction, pair, writer); - writer.write("\n"); - } - } + Date date = new Date(transaction.getTimeMillis()); + csvWriter.writeToken(dateFormat.format(date)); + 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); } - } catch (Exception e) { + } catch (IOException e) { Crashlytics.logException(e); throw new ExporterException(mExportParams, e); } diff --git a/app/src/main/java/org/gnucash/android/export/csv/CsvWriter.java b/app/src/main/java/org/gnucash/android/export/csv/CsvWriter.java index 5e6570d37..4c885aa53 100644 --- a/app/src/main/java/org/gnucash/android/export/csv/CsvWriter.java +++ b/app/src/main/java/org/gnucash/android/export/csv/CsvWriter.java @@ -17,6 +17,9 @@ package org.gnucash.android.export.csv; +import android.support.annotation.NonNull; + +import java.io.BufferedWriter; import java.io.IOException; import java.io.Writer; @@ -24,25 +27,68 @@ * Format data to be CSV-compatible * * @author Semyannikov Gleb + * @author Ngewi Fet */ -public class CsvWriter { - private Writer writer; +public class CsvWriter extends BufferedWriter { + private String separator = ","; public CsvWriter(Writer writer){ - this.writer = writer; + super(writer); + } + + public CsvWriter(Writer writer, String separator){ + super(writer); + this.separator = separator; + } + + @Override + public void write(@NonNull String str) throws IOException { + this.write(str, 0, str.length()); } - public void write(String str) throws IOException { - if (str == null || str.length() < 1) { - return; + /** + * Writes a CSV token and the separator to the underlying output stream. + * + * The token **MUST NOT** not contain the CSV separator. If the separator is found in the token, then + * the token will be escaped as specified by RFC 4180 + * @param token Token to be written to file + * @throws IOException if the token could not be written to the underlying stream + */ + public void writeToken(String token) throws IOException { + if (token == null || token.isEmpty()){ + write(separator); + } else { + token = escape(token); + write(token + separator); } + } - String head = str.substring(0, str.length() - 1); - char separator = str.charAt(str.length() - 1); - if (head.indexOf(separator) > -1) { - head = '"' + head + '"'; + /** + * Escape any CSV separators by surrounding the token in double quotes + * @param token String token to be written to CSV + * @return Escaped CSV token + */ + @NonNull + private String escape(@NonNull String token) { + if (token.contains(separator)){ + return "\"" + token + "\""; } + return token; + } - writer.write(head + separator); + /** + * Writes a token to the CSV file and appends end of line to it. + * + * The token **MUST NOT** not contain the CSV separator. If the separator is found in the token, then + * the token will be escaped as specified by RFC 4180 + * @param token The token to be written to the file + * @throws IOException if token could not be written to underlying writer + */ + public void writeEndToken(String token) throws IOException { + if (token != null && !token.isEmpty()) { + write(escape(token)); + } + this.newLine(); } + } 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 1f7fbfaf4..eedc67ac1 100644 --- a/app/src/main/java/org/gnucash/android/model/Money.java +++ b/app/src/main/java/org/gnucash/android/model/Money.java @@ -28,7 +28,6 @@ import java.text.DecimalFormat; import java.text.DecimalFormatSymbols; import java.text.NumberFormat; -import java.util.Currency; import java.util.Locale; /** @@ -427,13 +426,24 @@ public boolean isNegative(){ /** * Returns the string representation of the amount (without currency) of the Money object. - *

This string is not locale-formatted. The decimal operator is a period (.)

+ * + *

This string is not locale-formatted. The decimal operator is a period (.) + * For a locale-formatted version, see the method overload {@link #toLocaleString(Locale)}

* @return String representation of the amount (without currency) of the Money object */ public String toPlainString(){ return mAmount.setScale(mCommodity.getSmallestFractionDigits(), ROUNDING_MODE).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 + */ + public String toLocaleString(){ + return String.format(Locale.getDefault(), "%.2f", asDouble()); + } + /** * Returns the string representation of the Money object (value + currency) formatted according * to the default locale diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 5c1fe98ee..4c159f49c 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -463,4 +463,22 @@ Export to \'/Apps/GnuCash Android/\' folder on Dropbox Preferences Yes, I\'m sure + + Date + Transaction ID + Number + Description + Notes + Commodity/Currency + Void Reason + Action + Memo + Full Account Name + Account Name + Amount With Sym. + Amount Num + Reconcile + Reconcile Date + Rate/Price +