mirror of
https://github.com/aclindsa/moneygo.git
synced 2024-12-26 15:42:27 -05:00
Add direct OFX imports
This commit is contained in:
parent
bf284dc591
commit
fb59f9b3c5
12
accounts.go
12
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) {
|
func GetImbalanceAccount(transaction *gorp.Transaction, userid int64, securityid int64) (*Account, error) {
|
||||||
var imbalanceAccount Account
|
var imbalanceAccount Account
|
||||||
var account 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.UserId = userid
|
||||||
imbalanceAccount.Name = "Imbalances"
|
imbalanceAccount.Name = "Imbalances"
|
||||||
imbalanceAccount.ParentAccountId = -1
|
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
|
imbalanceAccount.Type = Bank
|
||||||
|
|
||||||
// Find/create the top-level trading account
|
// Find/create the top-level trading account
|
||||||
@ -238,7 +246,7 @@ func GetImbalanceAccount(transaction *gorp.Transaction, userid int64, securityid
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
security, err := GetSecurity(securityid, userid)
|
security, err := GetSecurityTx(transaction, securityid, userid)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
@ -302,7 +302,7 @@ func GnucashImportHandler(w http.ResponseWriter, r *http.Request) {
|
|||||||
securityMap := make(map[int64]int64)
|
securityMap := make(map[int64]int64)
|
||||||
for _, security := range gnucashImport.Securities {
|
for _, security := range gnucashImport.Securities {
|
||||||
securityId := security.SecurityId // save off because it could be updated
|
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 {
|
if err != nil {
|
||||||
sqltransaction.Rollback()
|
sqltransaction.Rollback()
|
||||||
WriteError(w, 6 /*Import Error*/)
|
WriteError(w, 6 /*Import Error*/)
|
||||||
|
193
imports.go
193
imports.go
@ -1,37 +1,29 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"github.com/aclindsa/ofxgo"
|
||||||
"io"
|
"io"
|
||||||
"log"
|
"log"
|
||||||
"math/big"
|
"math/big"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
/*
|
type OFXDownload struct {
|
||||||
* Assumes the User is a valid, signed-in user, but accountid has not yet been validated
|
OFXPassword string
|
||||||
*/
|
StartDate time.Time
|
||||||
func AccountImportHandler(w http.ResponseWriter, r *http.Request, user *User, accountid int64, importtype string) {
|
EndDate time.Time
|
||||||
//TODO branch off for different importtype's
|
}
|
||||||
|
|
||||||
multipartReader, err := r.MultipartReader()
|
func (od *OFXDownload) Read(json_str string) error {
|
||||||
if err != nil {
|
dec := json.NewDecoder(strings.NewReader(json_str))
|
||||||
WriteError(w, 3 /*Invalid Request*/)
|
return dec.Decode(od)
|
||||||
return
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// assume there is only one 'part'
|
func ofxImportHelper(r io.Reader, w http.ResponseWriter, user *User, accountid int64) {
|
||||||
part, err := multipartReader.NextPart()
|
itl, err := ImportOFX(r)
|
||||||
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)
|
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
//TODO is this necessarily an invalid request (what if it was an error on our end)?
|
//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
|
// SecurityIds to the actual SecurityIDs
|
||||||
var securitymap = make(map[int64]*Security)
|
var securitymap = make(map[int64]*Security)
|
||||||
for _, ofxsecurity := range itl.Securities {
|
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 {
|
if err != nil {
|
||||||
sqltransaction.Rollback()
|
sqltransaction.Rollback()
|
||||||
WriteError(w, 999 /*Internal Error*/)
|
WriteError(w, 999 /*Internal Error*/)
|
||||||
log.Print(err)
|
log.Print(err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
securitymap[ofxsecurity.SecurityId] = security
|
securitymap[oldsecurityid] = security
|
||||||
}
|
}
|
||||||
|
|
||||||
if account.SecurityId != securitymap[importedAccount.SecurityId].SecurityId {
|
if account.SecurityId != securitymap[importedAccount.SecurityId].SecurityId {
|
||||||
@ -236,3 +231,151 @@ func AccountImportHandler(w http.ResponseWriter, r *http.Request, user *User, ac
|
|||||||
|
|
||||||
WriteSuccess(w)
|
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*/)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
163
js/actions/ImportActions.js
Normal file
163
js/actions/ImportActions.js
Normal file
@ -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
|
||||||
|
};
|
@ -473,25 +473,29 @@ const AddEditTransactionModal = React.createClass({
|
|||||||
|
|
||||||
const ImportType = {
|
const ImportType = {
|
||||||
OFX: 1,
|
OFX: 1,
|
||||||
Gnucash: 2
|
OFXFile: 2,
|
||||||
|
Gnucash: 3
|
||||||
};
|
};
|
||||||
var ImportTypeList = [];
|
var ImportTypeList = [];
|
||||||
for (var type in ImportType) {
|
for (var type in ImportType) {
|
||||||
if (ImportType.hasOwnProperty(type)) {
|
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});
|
ImportTypeList.push({'TypeId': ImportType[type], 'Name': name});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const ImportTransactionsModal = React.createClass({
|
const ImportTransactionsModal = React.createClass({
|
||||||
getInitialState: function() {
|
getInitialState: function() {
|
||||||
return {
|
var startDate = new Date();
|
||||||
importing: false,
|
startDate.setMonth(startDate.getMonth() - 1);
|
||||||
imported: false,
|
return {
|
||||||
importFile: "",
|
importFile: "",
|
||||||
importType: ImportType.Gnucash,
|
importType: ImportType.Gnucash,
|
||||||
uploadProgress: -1,
|
startDate: startDate,
|
||||||
error: null};
|
endDate: new Date(),
|
||||||
|
password: "",
|
||||||
|
};
|
||||||
},
|
},
|
||||||
handleCancel: function() {
|
handleCancel: function() {
|
||||||
this.setState(this.getInitialState());
|
this.setState(this.getInitialState());
|
||||||
@ -504,73 +508,36 @@ const ImportTransactionsModal = React.createClass({
|
|||||||
handleTypeChange: function(type) {
|
handleTypeChange: function(type) {
|
||||||
this.setState({importType: type.TypeId});
|
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() {
|
handleSubmit: function() {
|
||||||
|
this.setState(this.getInitialState());
|
||||||
if (this.props.onSubmit != null)
|
if (this.props.onSubmit != null)
|
||||||
this.props.onSubmit(this.props.account);
|
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() {
|
handleImportTransactions: function() {
|
||||||
var file = ReactDOM.findDOMNode(this.refs.importfile).files[0];
|
if (this.state.importType == ImportType.OFX) {
|
||||||
var formData = new FormData();
|
this.props.onImportOFX(this.props.account, this.state.password, this.state.startDate, this.state.endDate);
|
||||||
formData.append('importfile', file, this.state.importFile);
|
} else if (this.state.importType == ImportType.OFXFile) {
|
||||||
var url = ""
|
this.props.onImportOFXFile(ReactDOM.findDOMNode(this.refs.importfile), this.props.account);
|
||||||
if (this.state.importType == ImportType.OFX)
|
} else if (this.state.importType == ImportType.Gnucash) {
|
||||||
url = "account/"+this.props.account.AccountId+"/import/ofx";
|
this.props.onImportGnucash(ReactDOM.findDOMNode(this.refs.importfile));
|
||||||
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
|
|
||||||
});
|
|
||||||
},
|
},
|
||||||
render: function() {
|
render: function() {
|
||||||
var accountNameLabel = "Performing global import:"
|
var accountNameLabel = "Performing global import:"
|
||||||
@ -579,45 +546,104 @@ const ImportTransactionsModal = React.createClass({
|
|||||||
|
|
||||||
// Display the progress bar if an upload/import is in progress
|
// Display the progress bar if an upload/import is in progress
|
||||||
var progressBar = [];
|
var progressBar = [];
|
||||||
if (this.state.importing && this.state.uploadProgress == 100) {
|
if (this.props.imports.importing && this.props.imports.uploadProgress == 100) {
|
||||||
progressBar = (<ProgressBar now={this.state.uploadProgress} active label="Importing transactions..." />);
|
progressBar = (<ProgressBar now={this.props.imports.uploadProgress} active label="Importing transactions..." />);
|
||||||
} else if (this.state.importing && this.state.uploadProgress != -1) {
|
} else if (this.props.imports.importing) {
|
||||||
progressBar = (<ProgressBar now={this.state.uploadProgress} active label="Uploading... %(percent)s%" />);
|
progressBar = (<ProgressBar now={this.props.imports.uploadProgress} active label={`Uploading... ${this.props.imports.uploadProgress}%`} />);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create panel, possibly displaying error or success messages
|
// Create panel, possibly displaying error or success messages
|
||||||
var panel = [];
|
var panel = [];
|
||||||
if (this.state.error != null) {
|
if (this.props.imports.importFailed) {
|
||||||
panel = (<Panel header="Error Importing Transactions" bsStyle="danger">{this.state.error}</Panel>);
|
panel = (<Panel header="Error Importing Transactions" bsStyle="danger">{this.props.imports.errorMessage}</Panel>);
|
||||||
} else if (this.state.imported) {
|
} else if (this.props.imports.importFinished) {
|
||||||
panel = (<Panel header="Successfully Imported Transactions" bsStyle="success">Your import is now complete.</Panel>);
|
panel = (<Panel header="Successfully Imported Transactions" bsStyle="success">Your import is now complete.</Panel>);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Display proper buttons, possibly disabling them if an import is in progress
|
// Display proper buttons, possibly disabling them if an import is in progress
|
||||||
var button1 = [];
|
var button1 = [];
|
||||||
var button2 = [];
|
var button2 = [];
|
||||||
if (!this.state.imported && this.state.error == null) {
|
if (!this.props.imports.importFinished && !this.props.imports.importFailed) {
|
||||||
button1 = (<Button onClick={this.handleCancel} disabled={this.state.importing} bsStyle="warning">Cancel</Button>);
|
var importingDisabled = this.props.imports.importing || (this.state.importType != ImportType.OFX && this.state.importFile == "") || (this.state.importType == ImportType.OFX && this.state.password == "");
|
||||||
button2 = (<Button onClick={this.handleImportTransactions} disabled={this.state.importing || this.state.importFile == ""} bsStyle="success">Import</Button>);
|
button1 = (<Button onClick={this.handleCancel} disabled={this.props.imports.importing} bsStyle="warning">Cancel</Button>);
|
||||||
|
button2 = (<Button onClick={this.handleImportTransactions} disabled={importingDisabled} bsStyle="success">Import</Button>);
|
||||||
} else {
|
} else {
|
||||||
button1 = (<Button onClick={this.handleCancel} disabled={this.state.importing} bsStyle="success">OK</Button>);
|
button1 = (<Button onClick={this.handleSubmit} disabled={this.props.imports.importing} bsStyle="success">OK</Button>);
|
||||||
}
|
}
|
||||||
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
|
// Disable OFX/QFX imports if no account is selected
|
||||||
var disabledTypes = false;
|
var disabledTypes = false;
|
||||||
if (this.props.account == null)
|
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 = (
|
||||||
|
<div>
|
||||||
|
<FormGroup>
|
||||||
|
<Col componentClass={ControlLabel} xs={2}>OFX Password</Col>
|
||||||
|
<Col xs={10}>
|
||||||
|
<FormControl type="password"
|
||||||
|
value={this.state.password}
|
||||||
|
placeholder="Password..."
|
||||||
|
ref="password"
|
||||||
|
onChange={this.handlePasswordChange} />
|
||||||
|
</Col>
|
||||||
|
</FormGroup>
|
||||||
|
<FormGroup>
|
||||||
|
<Col componentClass={ControlLabel} xs={2}>Start Date</Col>
|
||||||
|
<Col xs={10}>
|
||||||
|
<DateTimePicker
|
||||||
|
time={false}
|
||||||
|
defaultValue={this.state.startDate}
|
||||||
|
onChange={this.handleStartDateChange} />
|
||||||
|
</Col>
|
||||||
|
</FormGroup>
|
||||||
|
<FormGroup>
|
||||||
|
<Col componentClass={ControlLabel} xs={2}>End Date</Col>
|
||||||
|
<Col xs={10}>
|
||||||
|
<DateTimePicker
|
||||||
|
time={false}
|
||||||
|
defaultValue={this.state.endDate}
|
||||||
|
onChange={this.handleEndDateChange} />
|
||||||
|
</Col>
|
||||||
|
</FormGroup>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
importForm = (
|
||||||
|
<FormGroup>
|
||||||
|
<Col componentClass={ControlLabel} xs={2}>File</Col>
|
||||||
|
<Col xs={10}>
|
||||||
|
<FormControl type="file"
|
||||||
|
ref="importfile"
|
||||||
|
disabled={inputDisabled}
|
||||||
|
value={this.state.importFile}
|
||||||
|
onChange={this.handleImportChange} />
|
||||||
|
<HelpBlock>Select a file to upload.</HelpBlock>
|
||||||
|
</Col>
|
||||||
|
</FormGroup>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Modal show={this.props.show} onHide={this.handleCancel} bsSize="small">
|
<Modal show={this.props.show} onHide={this.handleCancel}>
|
||||||
<Modal.Header closeButton>
|
<Modal.Header closeButton>
|
||||||
<Modal.Title>Import Transactions</Modal.Title>
|
<Modal.Title>Import Transactions</Modal.Title>
|
||||||
</Modal.Header>
|
</Modal.Header>
|
||||||
<Modal.Body>
|
<Modal.Body>
|
||||||
<form onSubmit={this.handleImportTransactions}
|
<Form horizontal onSubmit={this.handleImportTransactions}
|
||||||
encType="multipart/form-data"
|
encType="multipart/form-data"
|
||||||
ref="importform">
|
ref="importform">
|
||||||
|
<FormGroup>
|
||||||
|
<Col xs={12}>
|
||||||
|
<ControlLabel>{accountNameLabel}</ControlLabel>
|
||||||
|
</Col>
|
||||||
|
</FormGroup>
|
||||||
|
<FormGroup>
|
||||||
|
<Col componentClass={ControlLabel} xs={2}>Import Type</Col>
|
||||||
|
<Col xs={10}>
|
||||||
<DropdownList
|
<DropdownList
|
||||||
data={ImportTypeList}
|
data={ImportTypeList}
|
||||||
valueField='TypeId'
|
valueField='TypeId'
|
||||||
@ -626,16 +652,10 @@ const ImportTransactionsModal = React.createClass({
|
|||||||
defaultValue={this.state.importType}
|
defaultValue={this.state.importType}
|
||||||
disabled={disabledTypes}
|
disabled={disabledTypes}
|
||||||
ref="importtype" />
|
ref="importtype" />
|
||||||
<FormGroup>
|
</Col>
|
||||||
<ControlLabel>{accountNameLabel}</ControlLabel>
|
|
||||||
<FormControl type="file"
|
|
||||||
ref="importfile"
|
|
||||||
disabled={inputDisabled}
|
|
||||||
value={this.state.importFile}
|
|
||||||
onChange={this.handleImportChange} />
|
|
||||||
<HelpBlock>Select a file to upload.</HelpBlock>
|
|
||||||
</FormGroup>
|
</FormGroup>
|
||||||
</form>
|
{importForm}
|
||||||
|
</Form>
|
||||||
{progressBar}
|
{progressBar}
|
||||||
{panel}
|
{panel}
|
||||||
</Modal.Body>
|
</Modal.Body>
|
||||||
@ -654,7 +674,6 @@ module.exports = React.createClass({
|
|||||||
displayName: "AccountRegister",
|
displayName: "AccountRegister",
|
||||||
getInitialState: function() {
|
getInitialState: function() {
|
||||||
return {
|
return {
|
||||||
importingTransactions: false,
|
|
||||||
newTransaction: null,
|
newTransaction: null,
|
||||||
height: 0
|
height: 0
|
||||||
};
|
};
|
||||||
@ -695,16 +714,6 @@ module.exports = React.createClass({
|
|||||||
newTransaction: newTransaction
|
newTransaction: newTransaction
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
handleImportClicked: function() {
|
|
||||||
this.setState({
|
|
||||||
importingTransactions: true
|
|
||||||
});
|
|
||||||
},
|
|
||||||
handleImportingCancel: function() {
|
|
||||||
this.setState({
|
|
||||||
importingTransactions: false
|
|
||||||
});
|
|
||||||
},
|
|
||||||
ajaxError: function(jqXHR, status, error) {
|
ajaxError: function(jqXHR, status, error) {
|
||||||
var e = new Error();
|
var e = new Error();
|
||||||
e.ErrorId = 5;
|
e.ErrorId = 5;
|
||||||
@ -725,7 +734,8 @@ module.exports = React.createClass({
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
handleImportComplete: function() {
|
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);
|
this.props.onFetchTransactionPage(this.props.accounts[this.props.selectedAccount], this.props.pageSize, this.props.transactionPage.page);
|
||||||
},
|
},
|
||||||
handleDeleteTransaction: function(transaction) {
|
handleDeleteTransaction: function(transaction) {
|
||||||
@ -810,11 +820,16 @@ module.exports = React.createClass({
|
|||||||
onDelete={this.handleDeleteTransaction}
|
onDelete={this.handleDeleteTransaction}
|
||||||
securities={this.props.securities} />
|
securities={this.props.securities} />
|
||||||
<ImportTransactionsModal
|
<ImportTransactionsModal
|
||||||
show={this.state.importingTransactions}
|
imports={this.props.imports}
|
||||||
|
show={this.props.imports.showModal}
|
||||||
account={this.props.accounts[this.props.selectedAccount]}
|
account={this.props.accounts[this.props.selectedAccount]}
|
||||||
accounts={this.props.accounts}
|
accounts={this.props.accounts}
|
||||||
onCancel={this.handleImportingCancel}
|
onCancel={this.props.onCloseImportModal}
|
||||||
onSubmit={this.handleImportComplete}/>
|
onHide={this.props.onCloseImportModal}
|
||||||
|
onSubmit={this.handleImportComplete}
|
||||||
|
onImportOFX={this.props.onImportOFX}
|
||||||
|
onImportOFXFile={this.props.onImportOFXFile}
|
||||||
|
onImportGnucash={this.props.onImportGnucash} />
|
||||||
<div className="transactions-register-toolbar">
|
<div className="transactions-register-toolbar">
|
||||||
Transactions for '{name}'
|
Transactions for '{name}'
|
||||||
<ButtonToolbar className="pull-right">
|
<ButtonToolbar className="pull-right">
|
||||||
@ -835,7 +850,7 @@ module.exports = React.createClass({
|
|||||||
<Glyphicon glyph='plus-sign' /> New Transaction
|
<Glyphicon glyph='plus-sign' /> New Transaction
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
onClick={this.handleImportClicked}
|
onClick={this.props.onOpenImportModal}
|
||||||
bsStyle="primary">
|
bsStyle="primary">
|
||||||
<Glyphicon glyph='import' /> Import
|
<Glyphicon glyph='import' /> Import
|
||||||
</Button>
|
</Button>
|
||||||
|
@ -139,7 +139,7 @@ const AddEditAccountModal = React.createClass({
|
|||||||
a.OFXAppID = this.state.ofxappid;
|
a.OFXAppID = this.state.ofxappid;
|
||||||
a.OFXAppVer = this.state.ofxappver;
|
a.OFXAppVer = this.state.ofxappver;
|
||||||
a.OFXVersion = this.state.ofxversion;
|
a.OFXVersion = this.state.ofxversion;
|
||||||
a.OFXNoIndent = this.state.ofxNoIndent;
|
a.OFXNoIndent = this.state.ofxnoindent;
|
||||||
|
|
||||||
if (this.props.onSubmit != null)
|
if (this.props.onSubmit != null)
|
||||||
this.props.onSubmit(a);
|
this.props.onSubmit(a);
|
||||||
@ -169,6 +169,7 @@ const AddEditAccountModal = React.createClass({
|
|||||||
ref="ofxaccttype">
|
ref="ofxaccttype">
|
||||||
<option value="CHECKING">Checking</option>
|
<option value="CHECKING">Checking</option>
|
||||||
<option value="SAVINGS">Savings</option>
|
<option value="SAVINGS">Savings</option>
|
||||||
|
<option value="CC">Credit Card</option>
|
||||||
<option value="MONEYMRKT">Money Market</option>
|
<option value="MONEYMRKT">Money Market</option>
|
||||||
<option value="CREDITLINE">Credit Line</option>
|
<option value="CREDITLINE">Credit Line</option>
|
||||||
<option value="CD">CD</option>
|
<option value="CD">CD</option>
|
||||||
@ -674,12 +675,19 @@ module.exports = React.createClass({
|
|||||||
securities={this.props.securities}
|
securities={this.props.securities}
|
||||||
transactions={this.props.transactions}
|
transactions={this.props.transactions}
|
||||||
transactionPage={this.props.transactionPage}
|
transactionPage={this.props.transactionPage}
|
||||||
|
imports={this.props.imports}
|
||||||
|
onFetchAllAccounts={this.props.onFetchAllAccounts}
|
||||||
onCreateTransaction={this.props.onCreateTransaction}
|
onCreateTransaction={this.props.onCreateTransaction}
|
||||||
onUpdateTransaction={this.props.onUpdateTransaction}
|
onUpdateTransaction={this.props.onUpdateTransaction}
|
||||||
onDeleteTransaction={this.props.onDeleteTransaction}
|
onDeleteTransaction={this.props.onDeleteTransaction}
|
||||||
onSelectTransaction={this.props.onSelectTransaction}
|
onSelectTransaction={this.props.onSelectTransaction}
|
||||||
onUnselectTransaction={this.props.onUnselectTransaction}
|
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} />
|
||||||
</Col>
|
</Col>
|
||||||
</Row></Grid>
|
</Row></Grid>
|
||||||
);
|
);
|
||||||
|
10
js/constants/ImportConstants.js
Normal file
10
js/constants/ImportConstants.js
Normal file
@ -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
|
||||||
|
});
|
@ -2,6 +2,7 @@ var connect = require('react-redux').connect;
|
|||||||
|
|
||||||
var AccountActions = require('../actions/AccountActions');
|
var AccountActions = require('../actions/AccountActions');
|
||||||
var TransactionActions = require('../actions/TransactionActions');
|
var TransactionActions = require('../actions/TransactionActions');
|
||||||
|
var ImportActions = require('../actions/ImportActions');
|
||||||
|
|
||||||
var AccountsTab = require('../components/AccountsTab');
|
var AccountsTab = require('../components/AccountsTab');
|
||||||
|
|
||||||
@ -18,12 +19,14 @@ function mapStateToProps(state) {
|
|||||||
security_list: security_list,
|
security_list: security_list,
|
||||||
selectedAccount: state.selectedAccount,
|
selectedAccount: state.selectedAccount,
|
||||||
transactions: state.transactions,
|
transactions: state.transactions,
|
||||||
transactionPage: state.transactionPage
|
transactionPage: state.transactionPage,
|
||||||
|
imports: state.imports
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function mapDispatchToProps(dispatch) {
|
function mapDispatchToProps(dispatch) {
|
||||||
return {
|
return {
|
||||||
|
onFetchAllAccounts: function() {dispatch(AccountActions.fetchAll())},
|
||||||
onCreateAccount: function(account) {dispatch(AccountActions.create(account))},
|
onCreateAccount: function(account) {dispatch(AccountActions.create(account))},
|
||||||
onUpdateAccount: function(account) {dispatch(AccountActions.update(account))},
|
onUpdateAccount: function(account) {dispatch(AccountActions.update(account))},
|
||||||
onDeleteAccount: function(account) {dispatch(AccountActions.remove(account))},
|
onDeleteAccount: function(account) {dispatch(AccountActions.remove(account))},
|
||||||
@ -34,6 +37,11 @@ function mapDispatchToProps(dispatch) {
|
|||||||
onSelectTransaction: function(transactionId) {dispatch(TransactionActions.select(transactionId))},
|
onSelectTransaction: function(transactionId) {dispatch(TransactionActions.select(transactionId))},
|
||||||
onUnselectTransaction: function() {dispatch(TransactionActions.unselect())},
|
onUnselectTransaction: function() {dispatch(TransactionActions.unselect())},
|
||||||
onFetchTransactionPage: function(account, pageSize, page) {dispatch(TransactionActions.fetchPage(account, pageSize, page))},
|
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))},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
15
js/models.js
15
js/models.js
@ -573,6 +573,20 @@ Report.prototype.mapReduceSeries = function(mapFn, reduceFn) {
|
|||||||
return this.mapReduceChildren(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 = {
|
module.exports = models = {
|
||||||
|
|
||||||
// Classes
|
// Classes
|
||||||
@ -583,6 +597,7 @@ module.exports = models = {
|
|||||||
Split: Split,
|
Split: Split,
|
||||||
Transaction: Transaction,
|
Transaction: Transaction,
|
||||||
Report: Report,
|
Report: Report,
|
||||||
|
OFXDownload: OFXDownload,
|
||||||
Error: Error,
|
Error: Error,
|
||||||
|
|
||||||
// Enums, Lists
|
// Enums, Lists
|
||||||
|
47
js/reducers/ImportReducer.js
Normal file
47
js/reducers/ImportReducer.js
Normal file
@ -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;
|
||||||
|
}
|
||||||
|
};
|
@ -11,6 +11,7 @@ var ReportReducer = require('./ReportReducer');
|
|||||||
var SelectedReportReducer = require('./SelectedReportReducer');
|
var SelectedReportReducer = require('./SelectedReportReducer');
|
||||||
var TransactionReducer = require('./TransactionReducer');
|
var TransactionReducer = require('./TransactionReducer');
|
||||||
var TransactionPageReducer = require('./TransactionPageReducer');
|
var TransactionPageReducer = require('./TransactionPageReducer');
|
||||||
|
var ImportReducer = require('./ImportReducer');
|
||||||
var ErrorReducer = require('./ErrorReducer');
|
var ErrorReducer = require('./ErrorReducer');
|
||||||
|
|
||||||
module.exports = Redux.combineReducers({
|
module.exports = Redux.combineReducers({
|
||||||
@ -25,5 +26,6 @@ module.exports = Redux.combineReducers({
|
|||||||
selectedReport: SelectedReportReducer,
|
selectedReport: SelectedReportReducer,
|
||||||
transactions: TransactionReducer,
|
transactions: TransactionReducer,
|
||||||
transactionPage: TransactionPageReducer,
|
transactionPage: TransactionPageReducer,
|
||||||
|
imports: ImportReducer,
|
||||||
error: ErrorReducer
|
error: ErrorReducer
|
||||||
});
|
});
|
||||||
|
16
ofx.go
16
ofx.go
@ -113,9 +113,11 @@ func (i *OFXImport) importOFXBank(stmt *ofxgo.StatementResponse) error {
|
|||||||
Type: Bank,
|
Type: Bank,
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, tran := range stmt.BankTranList.Transactions {
|
if stmt.BankTranList != nil {
|
||||||
if err := i.AddTransaction(&tran, &account); err != nil {
|
for _, tran := range stmt.BankTranList.Transactions {
|
||||||
return err
|
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)
|
i.Accounts = append(i.Accounts, account)
|
||||||
|
|
||||||
for _, tran := range stmt.BankTranList.Transactions {
|
if stmt.BankTranList != nil {
|
||||||
if err := i.AddTransaction(&tran, &account); err != nil {
|
for _, tran := range stmt.BankTranList.Transactions {
|
||||||
return err
|
if err := i.AddTransaction(&tran, &account); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -96,6 +96,16 @@ func GetSecurity(securityid int64, userid int64) (*Security, error) {
|
|||||||
return &s, nil
|
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) {
|
func GetSecurities(userid int64) (*[]*Security, error) {
|
||||||
var securities []*Security
|
var securities []*Security
|
||||||
|
|
||||||
@ -180,8 +190,8 @@ func DeleteSecurity(s *Security) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func ImportGetCreateSecurity(transaction *gorp.Transaction, user *User, security *Security) (*Security, error) {
|
func ImportGetCreateSecurity(transaction *gorp.Transaction, userid int64, security *Security) (*Security, error) {
|
||||||
security.UserId = user.UserId
|
security.UserId = userid
|
||||||
if len(security.AlternateId) == 0 {
|
if len(security.AlternateId) == 0 {
|
||||||
// Always create a new local security if we can't match on the AlternateId
|
// Always create a new local security if we can't match on the AlternateId
|
||||||
err := InsertSecurityTx(transaction, security)
|
err := InsertSecurityTx(transaction, security)
|
||||||
@ -193,7 +203,7 @@ func ImportGetCreateSecurity(transaction *gorp.Transaction, user *User, security
|
|||||||
|
|
||||||
var securities []*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 {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user