diff --git a/accounts.go b/accounts.go
index c4e9341..17ff4c1 100644
--- a/accounts.go
+++ b/accounts.go
@@ -225,11 +225,19 @@ func GetTradingAccount(transaction *gorp.Transaction, userid int64, securityid i
func GetImbalanceAccount(transaction *gorp.Transaction, userid int64, securityid int64) (*Account, error) {
var imbalanceAccount Account
var account Account
+ xxxtemplate := FindSecurityTemplate("XXX", Currency)
+ if xxxtemplate == nil {
+ return nil, errors.New("Couldn't find XXX security template")
+ }
+ xxxsecurity, err := ImportGetCreateSecurity(transaction, userid, xxxtemplate)
+ if err != nil {
+ return nil, errors.New("Couldn't create XXX security")
+ }
imbalanceAccount.UserId = userid
imbalanceAccount.Name = "Imbalances"
imbalanceAccount.ParentAccountId = -1
- imbalanceAccount.SecurityId = 840 /*USD*/ //FIXME SecurityId shouldn't matter for top-level imbalance account, but maybe we should grab the user's default
+ imbalanceAccount.SecurityId = xxxsecurity.SecurityId
imbalanceAccount.Type = Bank
// Find/create the top-level trading account
@@ -238,7 +246,7 @@ func GetImbalanceAccount(transaction *gorp.Transaction, userid int64, securityid
return nil, err
}
- security, err := GetSecurity(securityid, userid)
+ security, err := GetSecurityTx(transaction, securityid, userid)
if err != nil {
return nil, err
}
diff --git a/gnucash.go b/gnucash.go
index 2549400..c0e408b 100644
--- a/gnucash.go
+++ b/gnucash.go
@@ -302,7 +302,7 @@ func GnucashImportHandler(w http.ResponseWriter, r *http.Request) {
securityMap := make(map[int64]int64)
for _, security := range gnucashImport.Securities {
securityId := security.SecurityId // save off because it could be updated
- s, err := ImportGetCreateSecurity(sqltransaction, user, &security)
+ s, err := ImportGetCreateSecurity(sqltransaction, user.UserId, &security)
if err != nil {
sqltransaction.Rollback()
WriteError(w, 6 /*Import Error*/)
diff --git a/imports.go b/imports.go
index 6d1c2ad..513c590 100644
--- a/imports.go
+++ b/imports.go
@@ -1,37 +1,29 @@
package main
import (
+ "encoding/json"
+ "github.com/aclindsa/ofxgo"
"io"
"log"
"math/big"
"net/http"
+ "strings"
+ "time"
)
-/*
- * Assumes the User is a valid, signed-in user, but accountid has not yet been validated
- */
-func AccountImportHandler(w http.ResponseWriter, r *http.Request, user *User, accountid int64, importtype string) {
- //TODO branch off for different importtype's
+type OFXDownload struct {
+ OFXPassword string
+ StartDate time.Time
+ EndDate time.Time
+}
- multipartReader, err := r.MultipartReader()
- if err != nil {
- WriteError(w, 3 /*Invalid Request*/)
- return
- }
+func (od *OFXDownload) Read(json_str string) error {
+ dec := json.NewDecoder(strings.NewReader(json_str))
+ return dec.Decode(od)
+}
- // assume there is only one 'part'
- part, err := multipartReader.NextPart()
- if err != nil {
- if err == io.EOF {
- WriteError(w, 3 /*Invalid Request*/)
- } else {
- WriteError(w, 999 /*Internal Error*/)
- log.Print(err)
- }
- return
- }
-
- itl, err := ImportOFX(part)
+func ofxImportHelper(r io.Reader, w http.ResponseWriter, user *User, accountid int64) {
+ itl, err := ImportOFX(r)
if err != nil {
//TODO is this necessarily an invalid request (what if it was an error on our end)?
@@ -86,14 +78,17 @@ func AccountImportHandler(w http.ResponseWriter, r *http.Request, user *User, ac
// SecurityIds to the actual SecurityIDs
var securitymap = make(map[int64]*Security)
for _, ofxsecurity := range itl.Securities {
- security, err := ImportGetCreateSecurity(sqltransaction, user, &ofxsecurity)
+ // save off since ImportGetCreateSecurity overwrites SecurityId on
+ // ofxsecurity
+ oldsecurityid := ofxsecurity.SecurityId
+ security, err := ImportGetCreateSecurity(sqltransaction, user.UserId, &ofxsecurity)
if err != nil {
sqltransaction.Rollback()
WriteError(w, 999 /*Internal Error*/)
log.Print(err)
return
}
- securitymap[ofxsecurity.SecurityId] = security
+ securitymap[oldsecurityid] = security
}
if account.SecurityId != securitymap[importedAccount.SecurityId].SecurityId {
@@ -236,3 +231,151 @@ func AccountImportHandler(w http.ResponseWriter, r *http.Request, user *User, ac
WriteSuccess(w)
}
+
+func OFXImportHandler(w http.ResponseWriter, r *http.Request, user *User, accountid int64) {
+ download_json := r.PostFormValue("ofxdownload")
+ if download_json == "" {
+ log.Print("download_json")
+ WriteError(w, 3 /*Invalid Request*/)
+ return
+ }
+
+ var ofxdownload OFXDownload
+ err := ofxdownload.Read(download_json)
+ if err != nil {
+ log.Print("ofxdownload.Read")
+ WriteError(w, 3 /*Invalid Request*/)
+ return
+ }
+
+ account, err := GetAccount(accountid, user.UserId)
+ if err != nil {
+ log.Print("GetAccount")
+ WriteError(w, 3 /*Invalid Request*/)
+ return
+ }
+
+ ofxver := ofxgo.OfxVersion203
+ if len(account.OFXVersion) != 0 {
+ ofxver, err = ofxgo.NewOfxVersion(account.OFXVersion)
+ if err != nil {
+ log.Print("NewOfxVersion")
+ WriteError(w, 3 /*Invalid Request*/)
+ return
+ }
+ }
+
+ var client = ofxgo.Client{
+ AppID: account.OFXAppID,
+ AppVer: account.OFXAppVer,
+ SpecVersion: ofxver,
+ NoIndent: account.OFXNoIndent,
+ }
+
+ var query ofxgo.Request
+ query.URL = account.OFXURL
+ query.Signon.ClientUID = ofxgo.UID(account.OFXClientUID)
+ query.Signon.UserID = ofxgo.String(account.OFXUser)
+ query.Signon.UserPass = ofxgo.String(ofxdownload.OFXPassword)
+ query.Signon.Org = ofxgo.String(account.OFXORG)
+ query.Signon.Fid = ofxgo.String(account.OFXFID)
+
+ transactionuid, err := ofxgo.RandomUID()
+ if err != nil {
+ WriteError(w, 999 /*Internal Error*/)
+ log.Println("Error creating uid for transaction:", err)
+ return
+ }
+
+ if account.Type == Investment {
+ // Investment account
+ statementRequest := ofxgo.InvStatementRequest{
+ TrnUID: *transactionuid,
+ InvAcctFrom: ofxgo.InvAcct{
+ BrokerID: ofxgo.String(account.OFXBankID),
+ AcctID: ofxgo.String(account.OFXAcctID),
+ },
+ Include: true,
+ IncludeOO: true,
+ IncludePos: true,
+ IncludeBalance: true,
+ Include401K: true,
+ Include401KBal: true,
+ }
+ query.InvStmt = append(query.InvStmt, &statementRequest)
+ } else if account.OFXAcctType == "CC" {
+ // Import credit card transactions
+ statementRequest := ofxgo.CCStatementRequest{
+ TrnUID: *transactionuid,
+ CCAcctFrom: ofxgo.CCAcct{
+ AcctID: ofxgo.String(account.OFXAcctID),
+ },
+ Include: true,
+ }
+ query.CreditCard = append(query.CreditCard, &statementRequest)
+ } else {
+ // Import generic bank transactions
+ acctTypeEnum, err := ofxgo.NewAcctType(account.OFXAcctType)
+ if err != nil {
+ WriteError(w, 3 /*Invalid Request*/)
+ return
+ }
+ statementRequest := ofxgo.StatementRequest{
+ TrnUID: *transactionuid,
+ BankAcctFrom: ofxgo.BankAcct{
+ BankID: ofxgo.String(account.OFXBankID),
+ AcctID: ofxgo.String(account.OFXAcctID),
+ AcctType: acctTypeEnum,
+ },
+ Include: true,
+ }
+ query.Bank = append(query.Bank, &statementRequest)
+ }
+
+ response, err := client.RequestNoParse(&query)
+ if err != nil {
+ // TODO this could be an error talking with the OFX server...
+ WriteError(w, 3 /*Invalid Request*/)
+ return
+ }
+ defer response.Body.Close()
+
+ ofxImportHelper(response.Body, w, user, accountid)
+}
+
+func OFXFileImportHandler(w http.ResponseWriter, r *http.Request, user *User, accountid int64) {
+ multipartReader, err := r.MultipartReader()
+ if err != nil {
+ WriteError(w, 3 /*Invalid Request*/)
+ return
+ }
+
+ // assume there is only one 'part'
+ part, err := multipartReader.NextPart()
+ if err != nil {
+ if err == io.EOF {
+ WriteError(w, 3 /*Invalid Request*/)
+ } else {
+ WriteError(w, 999 /*Internal Error*/)
+ log.Print(err)
+ }
+ return
+ }
+
+ ofxImportHelper(part, w, user, accountid)
+}
+
+/*
+ * Assumes the User is a valid, signed-in user, but accountid has not yet been validated
+ */
+func AccountImportHandler(w http.ResponseWriter, r *http.Request, user *User, accountid int64, importtype string) {
+
+ switch importtype {
+ case "ofx":
+ OFXImportHandler(w, r, user, accountid)
+ case "ofxfile":
+ OFXFileImportHandler(w, r, user, accountid)
+ default:
+ WriteError(w, 3 /*Invalid Request*/)
+ }
+}
diff --git a/js/actions/ImportActions.js b/js/actions/ImportActions.js
new file mode 100644
index 0000000..7c2b6dd
--- /dev/null
+++ b/js/actions/ImportActions.js
@@ -0,0 +1,163 @@
+var ImportConstants = require('../constants/ImportConstants');
+
+var models = require('../models.js');
+var OFXDownload = models.OFXDownload;
+var Error = models.Error;
+
+function beginImport() {
+ return {
+ type: ImportConstants.BEGIN_IMPORT
+ }
+}
+
+function updateProgress(progress) {
+ return {
+ type: ImportConstants.UPDATE_IMPORT_PROGRESS,
+ progress: progress
+ }
+}
+
+function importFinished() {
+ return {
+ type: ImportConstants.IMPORT_FINISHED
+ }
+}
+
+function importFailed(error) {
+ return {
+ type: ImportConstants.IMPORT_FAILED,
+ error: error
+ }
+}
+
+function openModal() {
+ return function(dispatch) {
+ dispatch({
+ type: ImportConstants.OPEN_IMPORT_MODAL
+ });
+ };
+}
+
+function closeModal() {
+ return function(dispatch) {
+ dispatch({
+ type: ImportConstants.CLOSE_IMPORT_MODAL
+ });
+ };
+}
+
+function importOFX(account, password, startDate, endDate) {
+ return function(dispatch) {
+ dispatch(beginImport());
+ dispatch(updateProgress(50));
+
+ var ofxdownload = new OFXDownload();
+ ofxdownload.OFXPassword = password;
+ ofxdownload.StartDate = startDate;
+ ofxdownload.EndDate = endDate;
+
+ $.ajax({
+ type: "POST",
+ dataType: "json",
+ url: "account/"+account.AccountId+"/import/ofx",
+ data: {ofxdownload: ofxdownload.toJSON()},
+ success: function(data, status, jqXHR) {
+ var e = new Error();
+ e.fromJSON(data);
+ if (e.isError()) {
+ var errString = e.ErrorString;
+ if (e.ErrorId == 3 /* Invalid Request */) {
+ errString = "Please check that your password and all other OFX login credentials are correct.";
+ }
+ dispatch(importFailed(errString));
+ } else {
+ dispatch(importFinished());
+ }
+ },
+ error: function(jqXHR, status, error) {
+ dispatch(importFailed(error));
+ }
+ });
+ };
+}
+
+function importFile(url, inputElement) {
+ return function(dispatch) {
+ dispatch(beginImport());
+
+ if (inputElement.files.length == 0) {
+ dispatch(importFailed("No files specified to be imported"))
+ return;
+ }
+ if (inputElement.files.length > 1) {
+ dispatch(importFailed("More than one file specified for import, only one allowed at a time"))
+ return;
+ }
+
+ var file = inputElement.files[0];
+ var formData = new FormData();
+ formData.append('importfile', file, file.name);
+
+ var handleSetProgress = function(e) {
+ if (e.lengthComputable) {
+ var pct = Math.round(e.loaded/e.total*100);
+ dispatch(updateProgress(pct));
+ } else {
+ dispatch(updateProgress(50));
+ }
+ }
+
+ $.ajax({
+ type: "POST",
+ url: url,
+ data: formData,
+ xhr: function() {
+ var xhrObject = $.ajaxSettings.xhr();
+ if (xhrObject.upload) {
+ xhrObject.upload.addEventListener('progress', handleSetProgress, false);
+ } else {
+ dispatch(importFailed("File upload failed because xhr.upload isn't supported by your browser."));
+ }
+ return xhrObject;
+ },
+ success: function(data, status, jqXHR) {
+ var e = new Error();
+ e.fromJSON(data);
+ if (e.isError()) {
+ var errString = e.ErrorString;
+ if (e.ErrorId == 3 /* Invalid Request */) {
+ errString = "Please check that the file you uploaded is valid and try again.";
+ }
+ dispatch(importFailed(errString));
+ } else {
+ dispatch(importFinished());
+ }
+ },
+ error: function(jqXHR, status, error) {
+ dispatch(importFailed(error));
+ },
+ // So jQuery doesn't try to process the data or content-type
+ cache: false,
+ contentType: false,
+ processData: false
+ });
+ };
+}
+
+function importOFXFile(inputElement, account) {
+ url = "account/"+account.AccountId+"/import/ofxfile";
+ return importFile(url, inputElement);
+}
+
+function importGnucash(inputElement) {
+ url = "import/gnucash";
+ return importFile(url, inputElement);
+}
+
+module.exports = {
+ openModal: openModal,
+ closeModal: closeModal,
+ importOFX: importOFX,
+ importOFXFile: importOFXFile,
+ importGnucash: importGnucash
+};
diff --git a/js/components/AccountRegister.js b/js/components/AccountRegister.js
index d31dc46..70e7909 100644
--- a/js/components/AccountRegister.js
+++ b/js/components/AccountRegister.js
@@ -473,25 +473,29 @@ const AddEditTransactionModal = React.createClass({
const ImportType = {
OFX: 1,
- Gnucash: 2
+ OFXFile: 2,
+ Gnucash: 3
};
var ImportTypeList = [];
for (var type in ImportType) {
if (ImportType.hasOwnProperty(type)) {
- var name = ImportType[type] == ImportType.OFX ? "OFX/QFX" : type; //QFX is a special snowflake
+ var name = ImportType[type] == ImportType.OFX ? "Direct OFX" : type;
+ var name = ImportType[type] == ImportType.OFXFile ? "OFX/QFX File" : type; //QFX is a special snowflake
ImportTypeList.push({'TypeId': ImportType[type], 'Name': name});
}
}
const ImportTransactionsModal = React.createClass({
getInitialState: function() {
- return {
- importing: false,
- imported: false,
+ var startDate = new Date();
+ startDate.setMonth(startDate.getMonth() - 1);
+ return {
importFile: "",
importType: ImportType.Gnucash,
- uploadProgress: -1,
- error: null};
+ startDate: startDate,
+ endDate: new Date(),
+ password: "",
+ };
},
handleCancel: function() {
this.setState(this.getInitialState());
@@ -504,73 +508,36 @@ const ImportTransactionsModal = React.createClass({
handleTypeChange: function(type) {
this.setState({importType: type.TypeId});
},
+ handlePasswordChange: function() {
+ this.setState({password: ReactDOM.findDOMNode(this.refs.password).value});
+ },
+ handleStartDateChange: function(date, string) {
+ if (date == null)
+ return;
+ this.setState({
+ startDate: date
+ });
+ },
+ handleEndDateChange: function(date, string) {
+ if (date == null)
+ return;
+ this.setState({
+ endDate: date
+ });
+ },
handleSubmit: function() {
+ this.setState(this.getInitialState());
if (this.props.onSubmit != null)
this.props.onSubmit(this.props.account);
},
- handleSetProgress: function(e) {
- if (e.lengthComputable) {
- var pct = Math.round(e.loaded/e.total*100);
- this.setState({uploadProgress: pct});
- } else {
- this.setState({uploadProgress: 50});
- }
- },
handleImportTransactions: function() {
- var file = ReactDOM.findDOMNode(this.refs.importfile).files[0];
- var formData = new FormData();
- formData.append('importfile', file, this.state.importFile);
- var url = ""
- if (this.state.importType == ImportType.OFX)
- url = "account/"+this.props.account.AccountId+"/import/ofx";
- else if (this.state.importType == ImportType.Gnucash)
- url = "import/gnucash";
-
- this.setState({importing: true});
-
- $.ajax({
- type: "POST",
- url: url,
- data: formData,
- xhr: function() {
- var xhrObject = $.ajaxSettings.xhr();
- if (xhrObject.upload) {
- xhrObject.upload.addEventListener('progress', this.handleSetProgress, false);
- } else {
- console.log("File upload failed because !xhr.upload")
- }
- return xhrObject;
- }.bind(this),
- success: function(data, status, jqXHR) {
- var e = new Error();
- e.fromJSON(data);
- if (e.isError()) {
- var errString = e.ErrorString;
- if (e.ErrorId == 3 /* Invalid Request */) {
- errString = "Please check that the file you uploaded is valid and try again.";
- }
- this.setState({
- importing: false,
- error: errString
- });
- return;
- }
-
- this.setState({
- uploadProgress: 100,
- importing: false,
- imported: true
- });
- }.bind(this),
- error: function(e) {
- this.setState({importing: false});
- console.log("error handler", e);
- }.bind(this),
- // So jQuery doesn't try to process teh data or content-type
- cache: false,
- contentType: false,
- processData: false
- });
+ if (this.state.importType == ImportType.OFX) {
+ this.props.onImportOFX(this.props.account, this.state.password, this.state.startDate, this.state.endDate);
+ } else if (this.state.importType == ImportType.OFXFile) {
+ this.props.onImportOFXFile(ReactDOM.findDOMNode(this.refs.importfile), this.props.account);
+ } else if (this.state.importType == ImportType.Gnucash) {
+ this.props.onImportGnucash(ReactDOM.findDOMNode(this.refs.importfile));
+ }
},
render: function() {
var accountNameLabel = "Performing global import:"
@@ -579,45 +546,104 @@ const ImportTransactionsModal = React.createClass({
// Display the progress bar if an upload/import is in progress
var progressBar = [];
- if (this.state.importing && this.state.uploadProgress == 100) {
- progressBar = (