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 = (); - } else if (this.state.importing && this.state.uploadProgress != -1) { - progressBar = (); + if (this.props.imports.importing && this.props.imports.uploadProgress == 100) { + progressBar = (); + } else if (this.props.imports.importing) { + progressBar = (); } // Create panel, possibly displaying error or success messages var panel = []; - if (this.state.error != null) { - panel = ({this.state.error}); - } else if (this.state.imported) { + if (this.props.imports.importFailed) { + panel = ({this.props.imports.errorMessage}); + } else if (this.props.imports.importFinished) { panel = (Your import is now complete.); } // Display proper buttons, possibly disabling them if an import is in progress var button1 = []; var button2 = []; - if (!this.state.imported && this.state.error == null) { - button1 = (); - button2 = (); + if (!this.props.imports.importFinished && !this.props.imports.importFailed) { + var importingDisabled = this.props.imports.importing || (this.state.importType != ImportType.OFX && this.state.importFile == "") || (this.state.importType == ImportType.OFX && this.state.password == ""); + button1 = (); + button2 = (); } else { - button1 = (); + button1 = (); } - var inputDisabled = (this.state.importing || this.state.error != null || this.state.imported) ? true : false; + var inputDisabled = (this.props.imports.importing || this.props.imports.importFailed || this.props.imports.importFinished) ? true : false; // Disable OFX/QFX imports if no account is selected var disabledTypes = false; if (this.props.account == null) - disabledTypes = [ImportTypeList[ImportType.OFX - 1]]; + disabledTypes = [ImportTypeList[ImportType.OFX - 1], ImportTypeList[ImportType.OFXFile - 1]]; + + var importForm = []; + if (this.state.importType == ImportType.OFX) { + importForm = ( +
+ + OFX Password + + + + + + Start Date + + + + + + End Date + + + + +
+ ); + } else { + importForm = ( + + File + + + Select a file to upload. + + + ); + } return ( - + Import Transactions -
+ + + {accountNameLabel} + + + + Import Type + - - {accountNameLabel} - - Select a file to upload. + - + {importForm} + {progressBar} {panel}
@@ -654,7 +674,6 @@ module.exports = React.createClass({ displayName: "AccountRegister", getInitialState: function() { return { - importingTransactions: false, newTransaction: null, height: 0 }; @@ -695,16 +714,6 @@ module.exports = React.createClass({ newTransaction: newTransaction }); }, - handleImportClicked: function() { - this.setState({ - importingTransactions: true - }); - }, - handleImportingCancel: function() { - this.setState({ - importingTransactions: false - }); - }, ajaxError: function(jqXHR, status, error) { var e = new Error(); e.ErrorId = 5; @@ -725,7 +734,8 @@ module.exports = React.createClass({ } }, handleImportComplete: function() { - this.setState({importingTransactions: false}); + this.props.onCloseImportModal(); + this.props.onFetchAllAccounts(); this.props.onFetchTransactionPage(this.props.accounts[this.props.selectedAccount], this.props.pageSize, this.props.transactionPage.page); }, handleDeleteTransaction: function(transaction) { @@ -810,11 +820,16 @@ module.exports = React.createClass({ onDelete={this.handleDeleteTransaction} securities={this.props.securities} /> + onCancel={this.props.onCloseImportModal} + onHide={this.props.onCloseImportModal} + onSubmit={this.handleImportComplete} + onImportOFX={this.props.onImportOFX} + onImportOFXFile={this.props.onImportOFXFile} + onImportGnucash={this.props.onImportGnucash} />
Transactions for '{name}' @@ -835,7 +850,7 @@ module.exports = React.createClass({ New Transaction diff --git a/js/components/AccountsTab.js b/js/components/AccountsTab.js index d5544be..b968a68 100644 --- a/js/components/AccountsTab.js +++ b/js/components/AccountsTab.js @@ -139,7 +139,7 @@ const AddEditAccountModal = React.createClass({ a.OFXAppID = this.state.ofxappid; a.OFXAppVer = this.state.ofxappver; a.OFXVersion = this.state.ofxversion; - a.OFXNoIndent = this.state.ofxNoIndent; + a.OFXNoIndent = this.state.ofxnoindent; if (this.props.onSubmit != null) this.props.onSubmit(a); @@ -169,6 +169,7 @@ const AddEditAccountModal = React.createClass({ ref="ofxaccttype"> + @@ -674,12 +675,19 @@ module.exports = React.createClass({ securities={this.props.securities} transactions={this.props.transactions} transactionPage={this.props.transactionPage} + imports={this.props.imports} + onFetchAllAccounts={this.props.onFetchAllAccounts} onCreateTransaction={this.props.onCreateTransaction} onUpdateTransaction={this.props.onUpdateTransaction} onDeleteTransaction={this.props.onDeleteTransaction} onSelectTransaction={this.props.onSelectTransaction} onUnselectTransaction={this.props.onUnselectTransaction} - onFetchTransactionPage={this.props.onFetchTransactionPage}/> + onFetchTransactionPage={this.props.onFetchTransactionPage} + onOpenImportModal={this.props.onOpenImportModal} + onCloseImportModal={this.props.onCloseImportModal} + onImportOFX={this.props.onImportOFX} + onImportOFXFile={this.props.onImportOFXFile} + onImportGnucash={this.props.onImportGnucash} /> ); diff --git a/js/constants/ImportConstants.js b/js/constants/ImportConstants.js new file mode 100644 index 0000000..02539ee --- /dev/null +++ b/js/constants/ImportConstants.js @@ -0,0 +1,10 @@ +var keyMirror = require('keymirror'); + +module.exports = keyMirror({ + OPEN_IMPORT_MODAL: null, + CLOSE_IMPORT_MODAL: null, + BEGIN_IMPORT: null, + UPDATE_IMPORT_PROGRESS: null, + IMPORT_FINISHED: null, + IMPORT_FAILED: null +}); diff --git a/js/containers/AccountsTabContainer.js b/js/containers/AccountsTabContainer.js index 7180f0a..af1f672 100644 --- a/js/containers/AccountsTabContainer.js +++ b/js/containers/AccountsTabContainer.js @@ -2,6 +2,7 @@ var connect = require('react-redux').connect; var AccountActions = require('../actions/AccountActions'); var TransactionActions = require('../actions/TransactionActions'); +var ImportActions = require('../actions/ImportActions'); var AccountsTab = require('../components/AccountsTab'); @@ -18,12 +19,14 @@ function mapStateToProps(state) { security_list: security_list, selectedAccount: state.selectedAccount, transactions: state.transactions, - transactionPage: state.transactionPage + transactionPage: state.transactionPage, + imports: state.imports } } function mapDispatchToProps(dispatch) { return { + onFetchAllAccounts: function() {dispatch(AccountActions.fetchAll())}, onCreateAccount: function(account) {dispatch(AccountActions.create(account))}, onUpdateAccount: function(account) {dispatch(AccountActions.update(account))}, onDeleteAccount: function(account) {dispatch(AccountActions.remove(account))}, @@ -34,6 +37,11 @@ function mapDispatchToProps(dispatch) { onSelectTransaction: function(transactionId) {dispatch(TransactionActions.select(transactionId))}, onUnselectTransaction: function() {dispatch(TransactionActions.unselect())}, onFetchTransactionPage: function(account, pageSize, page) {dispatch(TransactionActions.fetchPage(account, pageSize, page))}, + onOpenImportModal: function() {dispatch(ImportActions.openModal())}, + onCloseImportModal: function() {dispatch(ImportActions.closeModal())}, + onImportOFX: function(account, password, startDate, endDate) {dispatch(ImportActions.importOFX(account, password, startDate, endDate))}, + onImportOFXFile: function(inputElement, account) {dispatch(ImportActions.importOFXFile(inputElement, account))}, + onImportGnucash: function(inputElement) {dispatch(ImportActions.importGnucash(inputElement))}, } } diff --git a/js/models.js b/js/models.js index 62c8a64..caf31da 100644 --- a/js/models.js +++ b/js/models.js @@ -573,6 +573,20 @@ Report.prototype.mapReduceSeries = function(mapFn, reduceFn) { return this.mapReduceChildren(mapFn, reduceFn); } +function OFXDownload() { + this.OFXPassword = ""; + this.StartDate = new Date(); + this.EndDate = new Date(); +} + +OFXDownload.prototype.toJSON = function() { + var json_obj = {}; + json_obj.OFXPassword = this.OFXPassword; + json_obj.StartDate = this.StartDate.toJSON(); + json_obj.EndDate = this.EndDate.toJSON(); + return JSON.stringify(json_obj); +} + module.exports = models = { // Classes @@ -583,6 +597,7 @@ module.exports = models = { Split: Split, Transaction: Transaction, Report: Report, + OFXDownload: OFXDownload, Error: Error, // Enums, Lists diff --git a/js/reducers/ImportReducer.js b/js/reducers/ImportReducer.js new file mode 100644 index 0000000..0f73ad0 --- /dev/null +++ b/js/reducers/ImportReducer.js @@ -0,0 +1,47 @@ +var assign = require('object-assign'); + +var ImportConstants = require('../constants/ImportConstants'); +var UserConstants = require('../constants/UserConstants'); + +const initialState = { + showModal: false, + importing: false, + uploadProgress: 0, + importFinished: false, + importFailed: false, + errorMessage: null +}; + +module.exports = function(state = initialState, action) { + switch (action.type) { + case ImportConstants.OPEN_IMPORT_MODAL: + return assign({}, initialState, { + showModal: true + }); + case ImportConstants.CLOSE_IMPORT_MODAL: + case UserConstants.USER_LOGGEDOUT: + return initialState; + case ImportConstants.BEGIN_IMPORT: + return assign({}, state, { + importing: true + }); + case ImportConstants.UPDATE_IMPORT_PROGRESS: + return assign({}, state, { + uploadProgress: action.progress + }); + case ImportConstants.IMPORT_FINISHED: + return assign({}, state, { + importing: false, + uploadProgress: 100, + importFinished: true + }); + case ImportConstants.IMPORT_FAILED: + return assign({}, state, { + importing: false, + importFailed: true, + errorMessage: action.error + }); + default: + return state; + } +}; diff --git a/js/reducers/MoneyGoReducer.js b/js/reducers/MoneyGoReducer.js index 62f1b3f..456d665 100644 --- a/js/reducers/MoneyGoReducer.js +++ b/js/reducers/MoneyGoReducer.js @@ -11,6 +11,7 @@ var ReportReducer = require('./ReportReducer'); var SelectedReportReducer = require('./SelectedReportReducer'); var TransactionReducer = require('./TransactionReducer'); var TransactionPageReducer = require('./TransactionPageReducer'); +var ImportReducer = require('./ImportReducer'); var ErrorReducer = require('./ErrorReducer'); module.exports = Redux.combineReducers({ @@ -25,5 +26,6 @@ module.exports = Redux.combineReducers({ selectedReport: SelectedReportReducer, transactions: TransactionReducer, transactionPage: TransactionPageReducer, + imports: ImportReducer, error: ErrorReducer }); diff --git a/ofx.go b/ofx.go index 8408da2..b1ac1ef 100644 --- a/ofx.go +++ b/ofx.go @@ -113,9 +113,11 @@ func (i *OFXImport) importOFXBank(stmt *ofxgo.StatementResponse) error { Type: Bank, } - for _, tran := range stmt.BankTranList.Transactions { - if err := i.AddTransaction(&tran, &account); err != nil { - return err + if stmt.BankTranList != nil { + for _, tran := range stmt.BankTranList.Transactions { + if err := i.AddTransaction(&tran, &account); err != nil { + return err + } } } @@ -139,9 +141,11 @@ func (i *OFXImport) importOFXCC(stmt *ofxgo.CCStatementResponse) error { } i.Accounts = append(i.Accounts, account) - for _, tran := range stmt.BankTranList.Transactions { - if err := i.AddTransaction(&tran, &account); err != nil { - return err + if stmt.BankTranList != nil { + for _, tran := range stmt.BankTranList.Transactions { + if err := i.AddTransaction(&tran, &account); err != nil { + return err + } } } diff --git a/securities.go b/securities.go index a8d5f89..9843825 100644 --- a/securities.go +++ b/securities.go @@ -96,6 +96,16 @@ func GetSecurity(securityid int64, userid int64) (*Security, error) { return &s, nil } +func GetSecurityTx(transaction *gorp.Transaction, securityid int64, userid int64) (*Security, error) { + var s Security + + err := transaction.SelectOne(&s, "SELECT * from securities where UserId=? AND SecurityId=?", userid, securityid) + if err != nil { + return nil, err + } + return &s, nil +} + func GetSecurities(userid int64) (*[]*Security, error) { var securities []*Security @@ -180,8 +190,8 @@ func DeleteSecurity(s *Security) error { return nil } -func ImportGetCreateSecurity(transaction *gorp.Transaction, user *User, security *Security) (*Security, error) { - security.UserId = user.UserId +func ImportGetCreateSecurity(transaction *gorp.Transaction, userid int64, security *Security) (*Security, error) { + security.UserId = userid if len(security.AlternateId) == 0 { // Always create a new local security if we can't match on the AlternateId err := InsertSecurityTx(transaction, security) @@ -193,7 +203,7 @@ func ImportGetCreateSecurity(transaction *gorp.Transaction, user *User, security var securities []*Security - _, err := transaction.Select(&securities, "SELECT * from securities where UserId=? AND Type=? AND AlternateId=? AND Precision=?", user.UserId, security.Type, security.AlternateId, security.Precision) + _, err := transaction.Select(&securities, "SELECT * from securities where UserId=? AND Type=? AND AlternateId=? AND Precision=?", userid, security.Type, security.AlternateId, security.Precision) if err != nil { return nil, err }