Skip to content

Commit

Permalink
Enable support for exporting transaction splits separately in CSV exp…
Browse files Browse the repository at this point in the history
…orts

Modified transactions CSV exporter to produce similar CSV like GnuCash desktop - codinguser#756
Localize transaction CSV headers
  • Loading branch information
codinguser committed Jun 4, 2018
1 parent 6788412 commit ad478f6
Show file tree
Hide file tree
Showing 4 changed files with 140 additions and 142 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -51,20 +49,7 @@ public class CsvTransactionsExporter extends Exporter{

private char mCsvSeparator;

private DateFormat dateFormat = new SimpleDateFormat("dd/MM/yyyy", Locale.US);

private Comparator<Split> splitComparator = new Comparator<Split>() {
@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
Expand All @@ -90,26 +75,13 @@ public CsvTransactionsExporter(ExportParams params, SQLiteDatabase db) {

@Override
public List<String> 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<String> exportedFiles = new ArrayList<>();
Expand All @@ -118,111 +90,63 @@ public List<String> 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<Split> 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<String> names = new ArrayList<String>();
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<Transaction> transactions = mTransactionsDbAdapter.getAllTransactions();

List<String> 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<Split> 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);
}
Expand Down
68 changes: 57 additions & 11 deletions app/src/main/java/org/gnucash/android/export/csv/CsvWriter.java
Original file line number Diff line number Diff line change
Expand Up @@ -17,32 +17,78 @@
package org.gnucash.android.export.csv;


import android.support.annotation.NonNull;

import java.io.BufferedWriter;
import java.io.IOException;
import java.io.Writer;

/**
* Format data to be CSV-compatible
*
* @author Semyannikov Gleb <nightdevgame@gmail.com>
* @author Ngewi Fet <ngewif@gmail.com>
*/
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();
}

}
14 changes: 12 additions & 2 deletions app/src/main/java/org/gnucash/android/model/Money.java
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,6 @@
import java.text.DecimalFormat;
import java.text.DecimalFormatSymbols;
import java.text.NumberFormat;
import java.util.Currency;
import java.util.Locale;

/**
Expand Down Expand Up @@ -427,13 +426,24 @@ public boolean isNegative(){

/**
* Returns the string representation of the amount (without currency) of the Money object.
* <p>This string is not locale-formatted. The decimal operator is a period (.)</p>
*
* <p>This string is not locale-formatted. The decimal operator is a period (.)
* For a locale-formatted version, see the method overload {@link #toLocaleString(Locale)}</p>
* @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
Expand Down
18 changes: 18 additions & 0 deletions app/src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -463,4 +463,22 @@
<string name="label_dropbox_export_destination">Export to \'/Apps/GnuCash Android/\' folder on Dropbox</string>
<string name="title_section_preferences">Preferences</string>
<string name="yes_sure">Yes, I\'m sure</string>
<string-array name="csv_transaction_headers">
<item>Date</item>
<item>Transaction ID</item>
<item>Number</item>
<item>Description</item>
<item>Notes</item>
<item>Commodity/Currency</item>
<item>Void Reason</item>
<item>Action</item>
<item>Memo</item>
<item>Full Account Name</item>
<item>Account Name</item>
<item>Amount With Sym.</item>
<item>Amount Num</item>
<item>Reconcile</item>
<item>Reconcile Date</item>
<item>Rate/Price</item>
</string-array>
</resources>

0 comments on commit ad478f6

Please sign in to comment.