diff --git a/accounts.go b/accounts.go
index 2cb578f..ab319ca 100644
--- a/accounts.go
+++ b/accounts.go
@@ -31,7 +31,7 @@ type Account struct {
// monotonically-increasing account transaction version number. Used for
// allowing a client to ensure they have a consistent version when paging
// through transactions.
- Version int64
+ AccountVersion int64 `json:"Version"`
}
type AccountList struct {
@@ -127,7 +127,7 @@ func insertUpdateAccount(a *Account, insert bool) error {
return err
}
- a.Version = oldacct.Version + 1
+ a.AccountVersion = oldacct.AccountVersion + 1
count, err := transaction.Update(a)
if err != nil {
@@ -227,7 +227,7 @@ func AccountHandler(w http.ResponseWriter, r *http.Request) {
}
account.AccountId = -1
account.UserId = user.UserId
- account.Version = 0
+ account.AccountVersion = 0
if GetSecurity(account.SecurityId) == nil {
WriteError(w, 3 /*Invalid Request*/)
diff --git a/static/account_register.js b/static/account_register.js
new file mode 100644
index 0000000..4fbd531
--- /dev/null
+++ b/static/account_register.js
@@ -0,0 +1,489 @@
+// Import all the objects we want to use from ReactBootstrap
+
+var Modal = ReactBootstrap.Modal;
+
+var Label = ReactBootstrap.Label;
+var Table = ReactBootstrap.Table;
+var Grid = ReactBootstrap.Grid;
+var Row = ReactBootstrap.Row;
+var Col = ReactBootstrap.Col;
+
+var DateTimePicker = ReactWidgets.DateTimePicker;
+
+const TransactionRow = React.createClass({
+ handleClick: function(e) {
+ const refs = ["date", "number", "description", "account", "status", "amount"];
+ for (var ref in refs) {
+ if (this.refs[refs[ref]].getDOMNode() == e.target) {
+ this.props.onEdit(this.props.transaction, refs[ref]);
+ return;
+ }
+ }
+ },
+ render: function() {
+ var date = this.props.transaction.Date;
+ var dateString = date.getFullYear() + "/" + (date.getMonth()+1) + "/" + date.getDate();
+ var number = ""
+ var accountName = "";
+ var status = "";
+ var security = this.props.security_map[this.props.account.SecurityId];
+
+ if (this.props.transaction.isTransaction()) {
+ var thisAccountSplit;
+ for (var i = 0; i < this.props.transaction.Splits.length; i++) {
+ if (this.props.transaction.Splits[i].AccountId == this.props.account.AccountId) {
+ thisAccountSplit = this.props.transaction.Splits[i];
+ break;
+ }
+ }
+ if (this.props.transaction.Splits.length == 2) {
+ 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);
+ } else {
+ accountName = "--Split Transaction--";
+ }
+
+ var amount = "$" + thisAccountSplit.Amount.toFixed(security.Precision);
+ status = TransactionStatusMap[this.props.transaction.Status];
+ number = thisAccountSplit.Number;
+ } else {
+ var amount = "$" + (new Big(0.0)).toFixed(security.Precision);
+ }
+
+ return (
+
+ {dateString} |
+ {number} |
+ {this.props.transaction.Description} |
+ {accountName} |
+ {status} |
+ {amount} |
+ $??.?? |
+
);
+ }
+});
+
+const AddEditTransactionModal = React.createClass({
+ _getInitialState: function(props) {
+ // Ensure we can edit this without screwing up other copies of it
+ var t = props.transaction.deepCopy();
+ return {transaction: t};
+ },
+ getInitialState: function() {
+ return this._getInitialState(this.props);
+ },
+ handleCancel: function() {
+ if (this.props.onCancel != null)
+ this.props.onCancel();
+ },
+ handleDescriptionChange: function() {
+ var transaction = this.state.transaction.deepCopy();
+ transaction.Description = this.refs.description.getValue();
+ this.setState({
+ transaction: transaction
+ });
+ },
+ handleDateChange: function(date, string) {
+ if (date == null)
+ return;
+ var transaction = this.state.transaction.deepCopy();
+ transaction.Date = date;
+ this.setState({
+ transaction: transaction
+ });
+ },
+ handleStatusChange: function(status) {
+ if (status.hasOwnProperty('StatusId')) {
+ var transaction = this.state.transaction.deepCopy();
+ transaction.Status = status.StatusId;
+ this.setState({
+ transaction: transaction
+ });
+ }
+ },
+ handleDeleteSplit: function(split) {
+ var transaction = this.state.transaction.deepCopy();
+ transaction.Splits.splice(split, 1);
+ this.setState({
+ transaction: transaction
+ });
+ },
+ handleUpdateNumber: function(split) {
+ var transaction = this.state.transaction.deepCopy();
+ transaction.Splits[split].Number = this.refs['number-'+split].getValue();
+ this.setState({
+ transaction: transaction
+ });
+ },
+ handleUpdateMemo: function(split) {
+ var transaction = this.state.transaction.deepCopy();
+ transaction.Splits[split].Memo = this.refs['memo-'+split].getValue();
+ this.setState({
+ transaction: transaction
+ });
+ },
+ handleUpdateAccount: function(account, split) {
+ var transaction = this.state.transaction.deepCopy();
+ transaction.Splits[split].AccountId = account.AccountId;
+ this.setState({
+ transaction: transaction
+ });
+ },
+ handleUpdateAmount: function(split) {
+ var transaction = this.state.transaction.deepCopy();
+ transaction.Splits[split].Amount = new Big(this.refs['amount-'+split].getValue());
+ this.setState({
+ transaction: transaction
+ });
+ },
+ handleSubmit: function() {
+ if (this.props.onSubmit != null)
+ this.props.onSubmit(this.state.transaction);
+ },
+ handleDelete: function() {
+ if (this.props.onDelete != null)
+ this.props.onDelete(this.state.transaction);
+ },
+ componentWillReceiveProps: function(nextProps) {
+ if (nextProps.show && !this.props.show) {
+ this.setState(this._getInitialState(nextProps));
+ }
+ },
+ render: function() {
+ var editing = this.props.transaction != null && this.props.transaction.isTransaction();
+ var headerText = editing ? "Edit" : "Create New";
+ var buttonText = editing ? "Save Changes" : "Create Transaction";
+ var deleteButton = [];
+ if (editing) {
+ deleteButton = (
+
+ );
+ }
+
+ splits = [];
+ for (var i = 0; i < this.state.transaction.Splits.length; i++) {
+ var self = this;
+ var s = this.state.transaction.Splits[i];
+
+ // Define all closures for calling split-updating functions
+ var deleteSplitFn = (function() {
+ var j = i;
+ return function() {self.handleDeleteSplit(j);};
+ })();
+ var updateNumberFn = (function() {
+ var j = i;
+ return function() {self.handleUpdateNumber(j);};
+ })();
+ var updateMemoFn = (function() {
+ var j = i;
+ return function() {self.handleUpdateMemo(j);};
+ })();
+ var updateAccountFn = (function() {
+ var j = i;
+ return function(account) {self.handleUpdateAccount(account, j);};
+ })();
+ var updateAmountFn = (function() {
+ var j = i;
+ return function() {self.handleUpdateAmount(j);};
+ })();
+
+ var deleteSplitButton = [];
+ if (this.state.transaction.Splits.length > 2) {
+ deleteSplitButton = (
+
+ );
+ }
+
+ splits.push((
+
+
+
+
+
+ {deleteSplitButton}
+
+ ));
+ }
+
+ return (
+
+
+ {headerText} Transaction
+
+
+
+
+
+
+
+ {deleteButton}
+
+
+
+
+ );
+ }
+});
+
+const AccountRegister = React.createClass({
+ getInitialState: function() {
+ return {
+ editingTransaction: false,
+ selectedTransaction: new Transaction(),
+ transactions: []
+ };
+ },
+ handleEditTransaction: function(transaction, fieldName) {
+ //TODO select fieldName first when editing
+ this.setState({
+ selectedTransaction: transaction,
+ editingTransaction: true
+ });
+ },
+ handleEditingCancel: function() {
+ this.setState({
+ editingTransaction: false
+ });
+ },
+ ajaxError: function(jqXHR, status, error) {
+ var e = new Error();
+ e.ErrorId = 5;
+ e.ErrorString = "Request Failed: " + status + error;
+ this.setState({error: e});
+ },
+ getTransactionPage: function(account, page) {
+ $.ajax({
+ type: "GET",
+ dataType: "json",
+ url: "account/"+account.AccountId+"/transactions?sort=date-desc&limit=50&page="+page,
+ success: function(data, status, jqXHR) {
+ var e = new Error();
+ var transactions = [];
+ e.fromJSON(data);
+ if (e.isError()) {
+ this.setState({error: e});
+ } else {
+ for (var i = 0; i < data.transactions.length; i++) {
+ var t = new Transaction();
+ t.fromJSON(data.transactions[i]);
+ transactions.push(t);
+ }
+ }
+ var a = new Account();
+ a.fromJSON(data.account);
+
+ this.setState({transactions: transactions});
+ }.bind(this),
+ error: this.ajaxError
+ });
+ },
+ onNewTransaction: function() {
+ this.getTransactionPage(this.props.selectedAccount, 0);
+ },
+ onUpdatedTransaction: function() {
+ this.getTransactionPage(this.props.selectedAccount, 0);
+ },
+ onDeletedTransaction: function() {
+ this.getTransactionPage(this.props.selectedAccount, 0);
+ },
+ createNewTransaction: function(transaction) {
+ $.ajax({
+ type: "POST",
+ dataType: "json",
+ url: "transaction/",
+ data: {transaction: transaction.toJSON()},
+ success: function(data, status, jqXHR) {
+ var e = new Error();
+ e.fromJSON(data);
+ if (e.isError()) {
+ this.setState({error: e});
+ } else {
+ this.onNewTransaction();
+ }
+ }.bind(this),
+ error: this.ajaxError
+ });
+ },
+ updateTransaction: function(transaction) {
+ $.ajax({
+ type: "PUT",
+ dataType: "json",
+ url: "transaction/"+transaction.TransactionId+"/",
+ data: {transaction: transaction.toJSON()},
+ success: function(data, status, jqXHR) {
+ var e = new Error();
+ e.fromJSON(data);
+ if (e.isError()) {
+ this.setState({error: e});
+ } else {
+ this.onUpdatedTransaction();
+ }
+ }.bind(this),
+ error: this.ajaxError
+ });
+ },
+ deleteTransaction: function(transaction) {
+ console.log("handleDeleteTransaction", transaction);
+ $.ajax({
+ type: "DELETE",
+ dataType: "json",
+ url: "transaction/"+transaction.TransactionId+"/",
+ success: function(data, status, jqXHR) {
+ var e = new Error();
+ e.fromJSON(data);
+ if (e.isError()) {
+ this.setState({error: e});
+ } else {
+ this.onDeletedTransaction();
+ }
+ }.bind(this),
+ error: this.ajaxError
+ });
+ },
+ handleDeleteTransaction: function(transaction) {
+ this.setState({
+ editingTransaction: false
+ });
+ this.deleteTransaction(transaction);
+ },
+ handleUpdateTransaction: function(transaction) {
+ this.setState({
+ editingTransaction: false
+ });
+ if (transaction.TransactionId != -1) {
+ this.updateTransaction(transaction);
+ } else {
+ this.createNewTransaction(transaction);
+ }
+ },
+ componentWillReceiveProps: function(nextProps) {
+ if (nextProps.selectedAccount != this.props.selectedAccount) {
+ this.getTransactionPage(nextProps.selectedAccount, 0);
+ console.log("TODO begin fetching transactions for new account");
+ }
+ },
+ render: function() {
+ var name = "Please select an account";
+ if (this.props.selectedAccount != null)
+ name = this.props.selectedAccount.Name;
+
+ register = [];
+ if (this.props.selectedAccount != null) {
+ var newTransaction = new Transaction();
+ newTransaction.Description = "Create New Transaction...";
+ newTransaction.Status = TransactionStatus.Entered;
+ newTransaction.Date = new Date();
+ newTransaction.Splits.push(new Split());
+ newTransaction.Splits.push(new Split());
+ newTransaction.Splits[0].AccountId = this.props.selectedAccount.AccountId;
+
+ var transactionRows = [];
+ var allTransactions = [newTransaction].concat(this.state.transactions);
+ for (var i = 0; i < allTransactions.length; i++) {
+ var t = allTransactions[i];
+ transactionRows.push((
+
+ ));
+ }
+
+ register = (
+
+
+ Date |
+ # |
+ Description |
+ Account |
+ Status |
+ Amount |
+ Balance |
+
+
+ {transactionRows}
+
+
+ );
+ }
+
+ return (
+
+
+ {name}
+ {register}
+
+ );
+ }
+});
diff --git a/static/accounts.js b/static/accounts.js
index 2dec758..8cf5022 100644
--- a/static/accounts.js
+++ b/static/accounts.js
@@ -10,30 +10,10 @@ var Button = ReactBootstrap.Button;
var ButtonGroup = ReactBootstrap.ButtonGroup;
var Glyphicon = ReactBootstrap.Glyphicon;
-var Modal = ReactBootstrap.Modal;
-
var CollapsibleMixin = ReactBootstrap.CollapsibleMixin;
var Combobox = ReactWidgets.Combobox;
-const recursiveAccountDisplayInfo = function(account, prefix) {
- var name = prefix + account.Name;
- var accounts = [{AccountId: account.AccountId, Name: name}];
- for (var i = 0; i < account.Children.length; i++)
- accounts = accounts.concat(recursiveAccountDisplayInfo(account.Children[i], name + "/"));
- return accounts
-};
-const getAccountDisplayList = function(account_list, includeRoot, rootName) {
- var accounts = []
- if (includeRoot)
- accounts.push({AccountId: -1, Name: rootName});
- for (var i = 0; i < account_list.length; i++) {
- if (account_list[i].isRootAccount())
- accounts = accounts.concat(recursiveAccountDisplayInfo(account_list[i], ""));
- }
- return accounts;
-};
-
const AccountCombobox = React.createClass({
getDefaultProps: function() {
return {
@@ -494,7 +474,12 @@ const AccountsTab = React.createClass({
- blah
+
);
diff --git a/static/index.html b/static/index.html
index aed8cc3..6989126 100644
--- a/static/index.html
+++ b/static/index.html
@@ -21,6 +21,7 @@
+
diff --git a/static/models.js b/static/models.js
index 11bde73..3a9b681 100644
--- a/static/models.js
+++ b/static/models.js
@@ -212,8 +212,6 @@ Split.prototype.toJSONobj = function() {
}
Split.prototype.fromJSONobj = function(json_obj) {
- var json_obj = getJSONObj(json_input);
-
if (json_obj.hasOwnProperty("SplitId"))
this.SplitId = json_obj.SplitId;
if (json_obj.hasOwnProperty("TransactionId"))
@@ -243,6 +241,18 @@ const TransactionStatus = {
Reconciled: 3,
Voided: 4
}
+var TransactionStatusList = [];
+for (var type in TransactionStatus) {
+ if (TransactionStatus.hasOwnProperty(type)) {
+ TransactionStatusList.push({'StatusId': TransactionStatus[type], 'Name': type});
+ }
+}
+var TransactionStatusMap = {};
+for (var status in TransactionStatus) {
+ if (TransactionStatus.hasOwnProperty(status)) {
+ TransactionStatusMap[TransactionStatus[status]] = status;
+ }
+}
function Transaction() {
this.TransactionId = -1;
@@ -262,8 +272,8 @@ Transaction.prototype.toJSON = function() {
json_obj.Date = this.Date.toJSON();
json_obj.Splits = [];
for (var i = 0; i < this.Splits.length; i++)
- json_obj.push(this.Splits[i].toJSONobj());
- return json_obj;
+ json_obj.Splits.push(this.Splits[i].toJSONobj());
+ return JSON.stringify(json_obj);
}
Transaction.prototype.fromJSON = function(json_input) {
@@ -289,8 +299,11 @@ Transaction.prototype.fromJSON = function(json_input) {
this.Date = new Date(0);
}
if (json_obj.hasOwnProperty("Splits")) {
- for (var i = 0; i < json_obj.Splits.length; i++)
- this.Splits.push(this.Splits[i].fromJSON());
+ for (var i = 0; i < json_obj.Splits.length; i++) {
+ var s = new Split();
+ s.fromJSONobj(json_obj.Splits[i]);
+ this.Splits.push(s);
+ }
}
}
@@ -300,6 +313,12 @@ Transaction.prototype.isTransaction = function() {
this.UserId != empty_transaction.UserId;
}
+Transaction.prototype.deepCopy = function() {
+ var t = new Transaction();
+ t.fromJSON(this.toJSON());
+ return t;
+}
+
function Error() {
this.ErrorId = -1;
this.ErrorString = "";
diff --git a/static/stylesheet.css b/static/stylesheet.css
index 7b7f6f8..486221f 100644
--- a/static/stylesheet.css
+++ b/static/stylesheet.css
@@ -75,3 +75,24 @@ div.accounttree-root div {
padding: 15px;
border-right: 1px solid #DDD;
}
+
+.register-row-editing {
+ background-color: #FFFFE0 !important;
+}
+.register-row-editing:hover {
+ background-color: #e8e8e8 !important;
+}
+.register-row-editing .form-group {
+ margin: 0;
+}
+
+.row > div > .form-group,
+.row > div > .rw-combobox {
+ margin-right: -7px;
+ margin-left: -7px;
+}
+
+.split-header {
+ font-weight: 700;
+ text-align: center;
+}
diff --git a/static/utils.js b/static/utils.js
new file mode 100644
index 0000000..47ad308
--- /dev/null
+++ b/static/utils.js
@@ -0,0 +1,27 @@
+const recursiveAccountDisplayInfo = function(account, prefix) {
+ var name = prefix + account.Name;
+ var accounts = [{AccountId: account.AccountId, Name: name}];
+ for (var i = 0; i < account.Children.length; i++)
+ accounts = accounts.concat(recursiveAccountDisplayInfo(account.Children[i], name + "/"));
+ return accounts
+};
+
+const getAccountDisplayList = function(account_list, includeRoot, rootName) {
+ var accounts = []
+ if (includeRoot)
+ accounts.push({AccountId: -1, Name: rootName});
+ for (var i = 0; i < account_list.length; i++) {
+ if (account_list[i].isRootAccount())
+ accounts = accounts.concat(recursiveAccountDisplayInfo(account_list[i], ""));
+ }
+ return accounts;
+};
+
+const getAccountDisplayName = function(account, account_map) {
+ var name = account.Name;
+ while (account.ParentAccountId >= 0) {
+ account = account_map[account.ParentAccountId];
+ name = account.Name + "/" + name;
+ }
+ return name;
+};
diff --git a/transactions.go b/transactions.go
index 93ac4de..481f445 100644
--- a/transactions.go
+++ b/transactions.go
@@ -18,7 +18,7 @@ type Split struct {
SplitId int64
TransactionId int64
AccountId int64
- Number int64 // Check or reference number
+ Number string // Check or reference number
Memo string
Amount string // String representation of decimal, suitable for passing to big.Rat.SetString()
Debit bool
@@ -38,10 +38,8 @@ func (s *Split) Valid() bool {
return err == nil
}
-type TransactionStatus int64
-
const (
- Entered TransactionStatus = 1
+ Entered int64 = 1
Cleared = 2
Reconciled = 3
Voided = 4
@@ -51,7 +49,7 @@ type Transaction struct {
TransactionId int64
UserId int64
Description string
- Status TransactionStatus
+ Status int64
Date time.Time
Splits []*Split `db:"-"`
}
@@ -118,7 +116,7 @@ func GetTransaction(transactionid int64, userid int64) (*Transaction, error) {
return nil, err
}
- err = transaction.SelectOne(&t, "SELECT * from transaction where UserId=? AND TransactionId=?", userid, transactionid)
+ err = transaction.SelectOne(&t, "SELECT * from transactions where UserId=? AND TransactionId=?", userid, transactionid)
if err != nil {
return nil, err
}
@@ -172,7 +170,7 @@ func incrementAccountVersions(transaction *gorp.Transaction, user *User, account
if err != nil {
return err
}
- account.Version++
+ account.AccountVersion++
count, err := transaction.Update(account)
if err != nil {
return err