1
0
mirror of https://github.com/aclindsa/moneygo.git synced 2025-06-13 13:39:23 -04:00

Initial pass at OFX imports

Still needs some fixups:
 * UI is incomplete
 * Investment transactions are unbalanced initially
 * OFX imports don't detect if one of the description fields for a
   transaction is empty (to fall back on another)
 * I'm sure plenty of other issues I haven't discovered yet
This commit is contained in:
2016-02-02 21:46:27 -05:00
parent 2e9828cc23
commit 58c7c17727
9 changed files with 638 additions and 48 deletions

View File

@ -13,6 +13,8 @@ var Col = ReactBootstrap.Col;
var Button = ReactBootstrap.Button;
var ButtonToolbar = ReactBootstrap.ButtonToolbar;
var ProgressBar = ReactBootstrap.ProgressBar;
var DateTimePicker = ReactWidgets.DateTimePicker;
const TransactionRow = React.createClass({
@ -45,7 +47,11 @@ const TransactionRow = React.createClass({
var otherSplit = this.props.transaction.Splits[0];
if (otherSplit.AccountId == this.props.account.AccountId)
var otherSplit = this.props.transaction.Splits[1];
var accountName = getAccountDisplayName(this.props.account_map[otherSplit.AccountId], this.props.account_map);
if (otherSplit.AccountId == -1)
var accountName = "Unbalanced " + this.props.security_map[otherSplit.SecurityId].Symbol + " transaction";
else
var accountName = getAccountDisplayName(this.props.account_map[otherSplit.AccountId], this.props.account_map);
} else {
accountName = "--Split Transaction--";
}
@ -224,6 +230,7 @@ const AddEditTransactionModal = React.createClass({
handleUpdateAccount: function(account, split) {
var transaction = this.state.transaction;
transaction.Splits[split] = React.addons.update(transaction.Splits[split], {
SecurityId: {$set: -1},
AccountId: {$set: account.AccountId}
});
this.setState({
@ -290,11 +297,14 @@ const AddEditTransactionModal = React.createClass({
var accountValidation = "";
if (s.AccountId in this.props.account_map) {
security = this.props.security_map[this.props.account_map[s.AccountId].SecurityId];
if (security.SecurityId in imbalancedSecurityMap)
amountValidation = "error";
} else {
if (s.SecurityId in this.props.security_map) {
security = this.props.security_map[s.SecurityId];
}
accountValidation = "has-error";
}
if (security != null && security.SecurityId in imbalancedSecurityMap)
amountValidation = "error";
// Define all closures for calling split-updating functions
var deleteSplitFn = (function() {
@ -423,9 +433,108 @@ const AddEditTransactionModal = React.createClass({
}
});
const ImportTransactionsModal = React.createClass({
getInitialState: function() {
return {
importFile: "",
uploadProgress: -1};
},
handleCancel: function() {
this.setState({
importFile: "",
uploadProgress: -1
});
if (this.props.onCancel != null)
this.props.onCancel();
},
onImportChanged: function() {
this.setState({importFile: this.refs.importfile.getValue()});
},
handleSubmit: function() {
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 = this.refs.importfile.getInputDOMNode().files[0];
var formData = new FormData();
formData.append('importfile', file, this.state.importFile);
$.ajax({
type: "POST",
url: "account/"+this.props.account.AccountId+"/import",
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),
beforeSend: function() {
console.log("before send");
},
success: function() {
this.setState({uploadProgress: 100});
console.log("success");
}.bind(this),
error: function(e) {
console.log("error handler", e);
},
// So jQuery doesn't try to process teh data or content-type
cache: false,
contentType: false,
processData: false
});
},
render: function() {
var accountNameLabel = ""
if (this.props.account != null )
accountNameLabel = "Import File to '" + getAccountDisplayName(this.props.account, this.props.account_map) + "':";
var progressBar = [];
if (this.state.uploadProgress != -1)
progressBar = (<ProgressBar now={this.state.uploadProgress} label="%(percent)s%" />);
return (
<Modal show={this.props.show} onHide={this.handleCancel} bsSize="medium">
<Modal.Header closeButton>
<Modal.Title>Import Transactions</Modal.Title>
</Modal.Header>
<Modal.Body>
<form onSubmit={this.handleImportTransactions}
encType="multipart/form-data"
ref="importform">
<Input type="file"
ref="importfile"
value={this.state.importFile}
label={accountNameLabel}
help="Select an OFX/QFX file to upload."
onChange={this.onImportChanged} />
</form>
{progressBar}
</Modal.Body>
<Modal.Footer>
<ButtonGroup>
<Button onClick={this.handleCancel} bsStyle="warning">Cancel</Button>
<Button onClick={this.handleImportTransactions} bsStyle="success">Import</Button>
</ButtonGroup>
</Modal.Footer>
</Modal>
);
}
});
const AccountRegister = React.createClass({
getInitialState: function() {
return {
importingTransactions: false,
editingTransaction: false,
selectedTransaction: new Transaction(),
transactions: [],
@ -468,6 +577,16 @@ const AccountRegister = React.createClass({
selectedTransaction: 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;
@ -593,6 +712,9 @@ const AccountRegister = React.createClass({
error: this.ajaxError
});
},
handleImportComplete: function() {
this.setState({importingTransactions: false});
},
handleDeleteTransaction: function(transaction) {
this.setState({
editingTransaction: false
@ -676,6 +798,13 @@ const AccountRegister = React.createClass({
onDelete={this.handleDeleteTransaction}
securities={this.props.securities}
security_map={this.props.security_map}/>
<ImportTransactionsModal
show={this.state.importingTransactions}
account={this.props.selectedAccount}
accounts={this.props.accounts}
account_map={this.props.account_map}
onCancel={this.handleImportingCancel}
onSubmit={this.handleImportComplete}/>
<div className="transactions-register-toolbar">
Transactions for '{name}'
<ButtonToolbar className="pull-right">
@ -695,6 +824,12 @@ const AccountRegister = React.createClass({
disabled={disabled}>
<Glyphicon glyph='plus-sign' /> New Transaction
</Button>
<Button
onClick={this.handleImportClicked}
bsStyle="primary"
disabled={disabled}>
<Glyphicon glyph='import' /> Import
</Button>
</ButtonGroup>
</ButtonToolbar>
</div>

View File

@ -77,10 +77,8 @@ Session.prototype.isSession = function() {
}
const SecurityType = {
Banknote: 1,
Bond: 2,
Stock: 3,
MutualFund: 4
Currency: 1,
Stock: 2
}
var SecurityTypeList = [];
for (var type in SecurityType) {
@ -197,6 +195,7 @@ function Split() {
this.SplitId = -1;
this.TransactionId = -1;
this.AccountId = -1;
this.SecurityId = -1;
this.Number = "";
this.Memo = "";
this.Amount = new Big(0.0);
@ -208,6 +207,7 @@ Split.prototype.toJSONobj = function() {
json_obj.SplitId = this.SplitId;
json_obj.TransactionId = this.TransactionId;
json_obj.AccountId = this.AccountId;
json_obj.SecurityId = this.SecurityId;
json_obj.Number = this.Number;
json_obj.Memo = this.Memo;
json_obj.Amount = this.Amount.toFixed();
@ -222,6 +222,8 @@ Split.prototype.fromJSONobj = function(json_obj) {
this.TransactionId = json_obj.TransactionId;
if (json_obj.hasOwnProperty("AccountId"))
this.AccountId = json_obj.AccountId;
if (json_obj.hasOwnProperty("SecurityId"))
this.SecurityId = json_obj.SecurityId;
if (json_obj.hasOwnProperty("Number"))
this.Number = json_obj.Number;
if (json_obj.hasOwnProperty("Memo"))
@ -236,14 +238,16 @@ Split.prototype.isSplit = function() {
var empty_split = new Split();
return this.SplitId != empty_split.SplitId ||
this.TransactionId != empty_split.TransactionId ||
this.AccountId != empty_split.AccountId;
this.AccountId != empty_split.AccountId ||
this.SecurityId != empty_split.SecurityId;
}
const TransactionStatus = {
Entered: 1,
Cleared: 2,
Reconciled: 3,
Voided: 4
Imported: 1,
Entered: 2,
Cleared: 3,
Reconciled: 4,
Voided: 5
}
var TransactionStatusList = [];
for (var type in TransactionStatus) {
@ -331,10 +335,14 @@ Transaction.prototype.imbalancedSplitSecurities = function(account_map) {
const emptySplit = new Split();
for (var i = 0; i < this.Splits.length; i++) {
split = this.Splits[i];
if (split.AccountId == emptySplit.AccountId) {
var securityId = -1;
if (split.AccountId != emptySplit.AccountId) {
securityId = account_map[split.AccountId].SecurityId;
} else if (split.SecurityId != emptySplit.SecurityId) {
securityId = split.SecurityId;
} else {
continue;
}
var securityId = account_map[split.AccountId].SecurityId;
if (securityId in splitBalances) {
splitBalances[securityId] = split.Amount.plus(splitBalances[securityId]);
} else {