Skip to content

Commit ad478f6

Browse files
committed
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
1 parent 6788412 commit ad478f6

File tree

4 files changed

+140
-142
lines changed

4 files changed

+140
-142
lines changed

app/src/main/java/org/gnucash/android/export/csv/CsvTransactionsExporter.java

Lines changed: 53 additions & 129 deletions
Original file line numberDiff line numberDiff line change
@@ -18,26 +18,24 @@
1818

1919
import android.database.Cursor;
2020
import android.database.sqlite.SQLiteDatabase;
21+
import android.support.annotation.NonNull;
2122

2223
import com.crashlytics.android.Crashlytics;
2324

25+
import org.gnucash.android.R;
2426
import org.gnucash.android.export.ExportParams;
2527
import org.gnucash.android.export.Exporter;
2628
import org.gnucash.android.model.Account;
27-
import org.gnucash.android.model.Money;
2829
import org.gnucash.android.model.Split;
2930
import org.gnucash.android.model.Transaction;
3031
import org.gnucash.android.model.TransactionType;
3132

32-
import java.io.BufferedOutputStream;
33-
import java.io.FileOutputStream;
33+
import java.io.FileWriter;
3434
import java.io.IOException;
35-
import java.io.OutputStreamWriter;
3635
import java.text.DateFormat;
3736
import java.text.SimpleDateFormat;
3837
import java.util.ArrayList;
39-
import java.util.Collections;
40-
import java.util.Comparator;
38+
import java.util.Arrays;
4139
import java.util.Date;
4240
import java.util.List;
4341
import java.util.Locale;
@@ -51,20 +49,7 @@ public class CsvTransactionsExporter extends Exporter{
5149

5250
private char mCsvSeparator;
5351

54-
private DateFormat dateFormat = new SimpleDateFormat("dd/MM/yyyy", Locale.US);
55-
56-
private Comparator<Split> splitComparator = new Comparator<Split>() {
57-
@Override
58-
public int compare(Split o1, Split o2) {
59-
if(o1.getType() == TransactionType.DEBIT
60-
&& o2.getType() == TransactionType.CREDIT)
61-
return -1;
62-
if (o1.getType() == TransactionType.CREDIT
63-
&& o2.getType() == TransactionType.DEBIT)
64-
return 1;
65-
return 0;
66-
}
67-
};
52+
private DateFormat dateFormat = new SimpleDateFormat("YYYY-MM-dd", Locale.US);
6853

6954
/**
7055
* Construct a new exporter with export parameters
@@ -90,26 +75,13 @@ public CsvTransactionsExporter(ExportParams params, SQLiteDatabase db) {
9075

9176
@Override
9277
public List<String> generateExport() throws ExporterException {
93-
OutputStreamWriter writerStream = null;
94-
CsvWriter writer = null;
9578
String outputFile = getExportCacheFilePath();
96-
try {
97-
FileOutputStream fileOutputStream = new FileOutputStream(outputFile);
98-
BufferedOutputStream bufferedOutputStream = new BufferedOutputStream(fileOutputStream);
99-
writerStream = new OutputStreamWriter(bufferedOutputStream);
100-
writer = new CsvWriter(writerStream);
101-
generateExport(writer);
79+
80+
try (CsvWriter csvWriter = new CsvWriter(new FileWriter(outputFile), "" + mCsvSeparator)){
81+
generateExport(csvWriter);
10282
} catch (IOException ex){
10383
Crashlytics.log("Error exporting CSV");
10484
Crashlytics.logException(ex);
105-
} finally {
106-
if (writerStream != null) {
107-
try {
108-
writerStream.close();
109-
} catch (IOException e) {
110-
throw new ExporterException(mExportParams, e);
111-
}
112-
}
11385
}
11486

11587
List<String> exportedFiles = new ArrayList<>();
@@ -118,111 +90,63 @@ public List<String> generateExport() throws ExporterException {
11890
return exportedFiles;
11991
}
12092

121-
private void write_split(final Transaction transaction, final Split split, final CsvWriter writer) throws IOException
122-
{
123-
String separator = mCsvSeparator + "";
124-
Account account = mAccountsDbAdapter.getRecord(split.getAccountUID());
125-
126-
// Date
127-
Date date = new Date(transaction.getTimeMillis());
128-
writer.write(dateFormat.format(date) + separator);
129-
// Account name
130-
writer.write(account.getName() + separator);
131-
// TODO:Number is not defined yet?
132-
writer.write( separator);
133-
// Description
134-
writer.write(transaction.getDescription() + separator);
135-
// Notes of transaction
136-
writer.write(transaction.getNote() + separator);
137-
// Memo
138-
writer.write(
139-
(split.getMemo()==null?
140-
"":split.getMemo()) + separator);
141-
// TODO:Category is not defined yet?
142-
writer.write(separator);
143-
// Type
144-
writer.write(split.getType().name() + separator);
145-
// TODO:Action is not defined yet?
146-
writer.write(separator);
147-
// Reconcile
148-
writer.write(split.getReconcileState() + separator);
149-
150-
// Changes
151-
Money change = split.getFormattedQuantity().withCurrency(transaction.getCommodity());
152-
Money zero = Money.getZeroInstance().withCurrency(transaction.getCommodity());
153-
// To currency; From currency; To; From
154-
if (change.isNegative()) {
155-
writer.write(zero.toPlainString() + separator);
156-
writer.write(change.abs().toPlainString() + separator);
157-
writer.write(Money.getZeroInstance().toPlainString() + separator);
158-
writer.write(split.getFormattedQuantity().abs().toPlainString() + separator);
159-
}
160-
else {
161-
writer.write(change.abs().toPlainString() + separator);
162-
writer.write(zero.toPlainString() + separator);
163-
writer.write(split.getFormattedQuantity().abs().toPlainString() + separator);
164-
writer.write(Money.getZeroInstance().toPlainString() + separator);
93+
/**
94+
* Write splits to CSV format
95+
* @param splits Splits to be written
96+
*/
97+
private void writeSplitsToCsv(@NonNull List<Split> splits, @NonNull CsvWriter writer) throws IOException {
98+
int index = 0;
99+
for (Split split : splits) {
100+
if (index++ > 0){ // the first split is on the same line as the transactions. But after that, we
101+
writer.write("" + mCsvSeparator + mCsvSeparator + mCsvSeparator + mCsvSeparator
102+
+ mCsvSeparator + mCsvSeparator + mCsvSeparator + mCsvSeparator);
103+
}
104+
writer.writeToken(split.getMemo());
105+
Account account = mAccountsDbAdapter.getRecord(split.getAccountUID());
106+
writer.writeToken(account.getFullName());
107+
writer.writeToken(account.getName());
108+
109+
String sign = split.getType() == TransactionType.CREDIT ? "-" : "";
110+
writer.writeToken(sign + split.getQuantity().formattedString());
111+
writer.writeToken(sign + split.getQuantity().toLocaleString());
112+
writer.writeToken("" + split.getReconcileState());
113+
if (split.getReconcileState() == Split.FLAG_RECONCILED) {
114+
String recDateString = dateFormat.format(new Date(split.getReconcileDate().getTime()));
115+
writer.writeToken(recDateString);
116+
} else {
117+
writer.writeToken(null);
118+
}
119+
writer.writeEndToken(split.getQuantity().divide(split.getValue()).toLocaleString());
165120
}
166-
167-
// TODO: What is price?
168-
writer.write(separator);
169-
writer.write(separator);
170121
}
171122

172-
public void generateExport(final CsvWriter writer) throws ExporterException {
123+
private void generateExport(final CsvWriter csvWriter) throws ExporterException {
173124
try {
174-
String separator = mCsvSeparator + "";
175-
List<String> names = new ArrayList<String>();
176-
names.add("Date");
177-
names.add("Account name");
178-
names.add("Number");
179-
names.add("Description");
180-
names.add("Notes");
181-
names.add("Memo");
182-
names.add("Category");
183-
names.add("Type");
184-
names.add("Action");
185-
names.add("Reconcile");
186-
names.add("To With Sym");
187-
names.add("From With Sym");
188-
names.add("To Num.");
189-
names.add("From Num.");
190-
names.add("To Rate/Price");
191-
names.add("From Rate/Price");
192-
193-
List<Transaction> transactions = mTransactionsDbAdapter.getAllTransactions();
194-
125+
List<String> names = Arrays.asList(mContext.getResources().getStringArray(R.array.csv_transaction_headers));
195126
for(int i = 0; i < names.size(); i++) {
196-
writer.write(names.get(i) + separator);
127+
csvWriter.writeToken(names.get(i));
197128
}
198-
writer.write("\n");
129+
csvWriter.newLine();
199130

200131

201132
Cursor cursor = mTransactionsDbAdapter.fetchAllRecords();
202-
while (cursor.moveToNext())
203-
{
133+
while (cursor.moveToNext()){
204134
Transaction transaction = mTransactionsDbAdapter.buildModelInstance(cursor);
205-
List<Split> splits = transaction.getSplits();
206-
Collections.sort(splits,splitComparator);
207-
for (int j = 0; j < splits.size()/2; j++) {
208-
Split split = splits.get(j);
209-
Split pair = null;
210-
for (int k = 0; k < splits.size(); k++) {
211-
if (split.isPairOf(splits.get(k))) {
212-
pair = splits.get(k);
213-
}
214-
}
215-
216-
write_split(transaction, split, writer);
217-
writer.write("\n");
218-
if (pair != null) {
219-
write_split(transaction, pair, writer);
220-
writer.write("\n");
221-
}
222-
}
135+
Date date = new Date(transaction.getTimeMillis());
136+
csvWriter.writeToken(dateFormat.format(date));
137+
csvWriter.writeToken(transaction.getUID());
138+
csvWriter.writeToken(null); //Transaction number
139+
140+
csvWriter.writeToken(transaction.getDescription());
141+
csvWriter.writeToken(transaction.getNote());
142+
143+
csvWriter.writeToken("CURRENCY::" + transaction.getCurrencyCode());
144+
csvWriter.writeToken(null); // Void Reason
145+
csvWriter.writeToken(null); // Action
146+
writeSplitsToCsv(transaction.getSplits(), csvWriter);
223147
}
224148

225-
} catch (Exception e) {
149+
} catch (IOException e) {
226150
Crashlytics.logException(e);
227151
throw new ExporterException(mExportParams, e);
228152
}

app/src/main/java/org/gnucash/android/export/csv/CsvWriter.java

Lines changed: 57 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -17,32 +17,78 @@
1717
package org.gnucash.android.export.csv;
1818

1919

20+
import android.support.annotation.NonNull;
21+
22+
import java.io.BufferedWriter;
2023
import java.io.IOException;
2124
import java.io.Writer;
2225

2326
/**
2427
* Format data to be CSV-compatible
2528
*
2629
* @author Semyannikov Gleb <[email protected]>
30+
* @author Ngewi Fet <[email protected]>
2731
*/
28-
public class CsvWriter {
29-
private Writer writer;
32+
public class CsvWriter extends BufferedWriter {
33+
private String separator = ",";
3034

3135
public CsvWriter(Writer writer){
32-
this.writer = writer;
36+
super(writer);
37+
}
38+
39+
public CsvWriter(Writer writer, String separator){
40+
super(writer);
41+
this.separator = separator;
42+
}
43+
44+
@Override
45+
public void write(@NonNull String str) throws IOException {
46+
this.write(str, 0, str.length());
3347
}
3448

35-
public void write(String str) throws IOException {
36-
if (str == null || str.length() < 1) {
37-
return;
49+
/**
50+
* Writes a CSV token and the separator to the underlying output stream.
51+
*
52+
* The token **MUST NOT** not contain the CSV separator. If the separator is found in the token, then
53+
* the token will be escaped as specified by RFC 4180
54+
* @param token Token to be written to file
55+
* @throws IOException if the token could not be written to the underlying stream
56+
*/
57+
public void writeToken(String token) throws IOException {
58+
if (token == null || token.isEmpty()){
59+
write(separator);
60+
} else {
61+
token = escape(token);
62+
write(token + separator);
3863
}
64+
}
3965

40-
String head = str.substring(0, str.length() - 1);
41-
char separator = str.charAt(str.length() - 1);
42-
if (head.indexOf(separator) > -1) {
43-
head = '"' + head + '"';
66+
/**
67+
* Escape any CSV separators by surrounding the token in double quotes
68+
* @param token String token to be written to CSV
69+
* @return Escaped CSV token
70+
*/
71+
@NonNull
72+
private String escape(@NonNull String token) {
73+
if (token.contains(separator)){
74+
return "\"" + token + "\"";
4475
}
76+
return token;
77+
}
4578

46-
writer.write(head + separator);
79+
/**
80+
* Writes a token to the CSV file and appends end of line to it.
81+
*
82+
* The token **MUST NOT** not contain the CSV separator. If the separator is found in the token, then
83+
* the token will be escaped as specified by RFC 4180
84+
* @param token The token to be written to the file
85+
* @throws IOException if token could not be written to underlying writer
86+
*/
87+
public void writeEndToken(String token) throws IOException {
88+
if (token != null && !token.isEmpty()) {
89+
write(escape(token));
90+
}
91+
this.newLine();
4792
}
93+
4894
}

app/src/main/java/org/gnucash/android/model/Money.java

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,6 @@
2828
import java.text.DecimalFormat;
2929
import java.text.DecimalFormatSymbols;
3030
import java.text.NumberFormat;
31-
import java.util.Currency;
3231
import java.util.Locale;
3332

3433
/**
@@ -427,13 +426,24 @@ public boolean isNegative(){
427426

428427
/**
429428
* Returns the string representation of the amount (without currency) of the Money object.
430-
* <p>This string is not locale-formatted. The decimal operator is a period (.)</p>
429+
*
430+
* <p>This string is not locale-formatted. The decimal operator is a period (.)
431+
* For a locale-formatted version, see the method overload {@link #toLocaleString(Locale)}</p>
431432
* @return String representation of the amount (without currency) of the Money object
432433
*/
433434
public String toPlainString(){
434435
return mAmount.setScale(mCommodity.getSmallestFractionDigits(), ROUNDING_MODE).toPlainString();
435436
}
436437

438+
/**
439+
* Returns a locale-specific representation of the amount of the Money object (excluding the currency)
440+
*
441+
* @return String representation of the amount (without currency) of the Money object
442+
*/
443+
public String toLocaleString(){
444+
return String.format(Locale.getDefault(), "%.2f", asDouble());
445+
}
446+
437447
/**
438448
* Returns the string representation of the Money object (value + currency) formatted according
439449
* to the default locale

app/src/main/res/values/strings.xml

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -463,4 +463,22 @@
463463
<string name="label_dropbox_export_destination">Export to \'/Apps/GnuCash Android/\' folder on Dropbox</string>
464464
<string name="title_section_preferences">Preferences</string>
465465
<string name="yes_sure">Yes, I\'m sure</string>
466+
<string-array name="csv_transaction_headers">
467+
<item>Date</item>
468+
<item>Transaction ID</item>
469+
<item>Number</item>
470+
<item>Description</item>
471+
<item>Notes</item>
472+
<item>Commodity/Currency</item>
473+
<item>Void Reason</item>
474+
<item>Action</item>
475+
<item>Memo</item>
476+
<item>Full Account Name</item>
477+
<item>Account Name</item>
478+
<item>Amount With Sym.</item>
479+
<item>Amount Num</item>
480+
<item>Reconcile</item>
481+
<item>Reconcile Date</item>
482+
<item>Rate/Price</item>
483+
</string-array>
466484
</resources>

0 commit comments

Comments
 (0)