mirror of
				https://github.com/aclindsa/moneygo.git
				synced 2025-10-30 01:23:26 -04:00 
			
		
		
		
	Add Initial Gnucash importing
There are still a number of bugs, but the basic functionality is there
This commit is contained in:
		
							
								
								
									
										175
									
								
								accounts.go
									
									
									
									
									
								
							
							
						
						
									
										175
									
								
								accounts.go
									
									
									
									
									
								
							| @@ -48,7 +48,7 @@ var accountImportRE *regexp.Regexp | |||||||
|  |  | ||||||
| func init() { | func init() { | ||||||
| 	accountTransactionsRE = regexp.MustCompile(`^/account/[0-9]+/transactions/?$`) | 	accountTransactionsRE = regexp.MustCompile(`^/account/[0-9]+/transactions/?$`) | ||||||
| 	accountImportRE = regexp.MustCompile(`^/account/[0-9]+/import/?$`) | 	accountImportRE = regexp.MustCompile(`^/account/[0-9]+/import/[a-z]+/?$`) | ||||||
| } | } | ||||||
|  |  | ||||||
| func (a *Account) Write(w http.ResponseWriter) error { | func (a *Account) Write(w http.ResponseWriter) error { | ||||||
| @@ -97,138 +97,98 @@ func GetAccounts(userid int64) (*[]Account, error) { | |||||||
| 	return &accounts, nil | 	return &accounts, nil | ||||||
| } | } | ||||||
|  |  | ||||||
| // Get (and attempt to create if it doesn't exist) the security/currency | // Get (and attempt to create if it doesn't exist). Matches on UserId, | ||||||
| // trading account for the supplied security/currency | // SecurityId, Type, Name, and ParentAccountId | ||||||
| func GetTradingAccount(userid int64, securityid int64) (*Account, error) { | func GetCreateAccountTx(transaction *gorp.Transaction, a Account) (*Account, error) { | ||||||
| 	var tradingAccounts []Account //top-level 'Trading' account(s) | 	var accounts []Account | ||||||
| 	var tradingAccount Account |  | ||||||
| 	var accounts []Account //second-level security-specific trading account(s) |  | ||||||
| 	var account Account | 	var account Account | ||||||
|  |  | ||||||
| 	transaction, err := DB.Begin() |  | ||||||
| 	if err != nil { |  | ||||||
| 		return nil, err |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	// Try to find the top-level trading account | 	// Try to find the top-level trading account | ||||||
| 	_, err = transaction.Select(&tradingAccounts, "SELECT * from accounts where UserId=? AND Name='Trading' AND ParentAccountId=-1 AND Type=? ORDER BY AccountId ASC LIMIT 1", userid, Trading) | 	_, err := transaction.Select(&accounts, "SELECT * from accounts where UserId=? AND SecurityId=? AND Type=? AND Name=? AND ParentAccountId=? ORDER BY AccountId ASC LIMIT 1", a.UserId, a.SecurityId, a.Type, a.Name, a.ParentAccountId) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		transaction.Rollback() |  | ||||||
| 		return nil, err |  | ||||||
| 	} |  | ||||||
| 	if len(tradingAccounts) == 1 { |  | ||||||
| 		tradingAccount = tradingAccounts[0] |  | ||||||
| 	} else { |  | ||||||
| 		tradingAccount.UserId = userid |  | ||||||
| 		tradingAccount.Name = "Trading" |  | ||||||
| 		tradingAccount.ParentAccountId = -1 |  | ||||||
| 		tradingAccount.SecurityId = 840 /*USD*/ //FIXME SecurityId shouldn't matter for top-level trading account, but maybe we should grab the user's default |  | ||||||
| 		tradingAccount.Type = Trading |  | ||||||
|  |  | ||||||
| 		err = transaction.Insert(&tradingAccount) |  | ||||||
| 		if err != nil { |  | ||||||
| 			transaction.Rollback() |  | ||||||
| 			return nil, err |  | ||||||
| 		} |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	// Now, try to find the security-specific trading account |  | ||||||
| 	_, err = transaction.Select(&accounts, "SELECT * from accounts where UserId=? AND SecurityId=? AND ParentAccountId=? ORDER BY AccountId ASC LIMIT 1", userid, securityid, tradingAccount.AccountId) |  | ||||||
| 	if err != nil { |  | ||||||
| 		transaction.Rollback() |  | ||||||
| 		return nil, err | 		return nil, err | ||||||
| 	} | 	} | ||||||
| 	if len(accounts) == 1 { | 	if len(accounts) == 1 { | ||||||
| 		account = accounts[0] | 		account = accounts[0] | ||||||
| 	} else { | 	} else { | ||||||
| 		security := GetSecurity(securityid) | 		account.UserId = a.UserId | ||||||
| 		account.UserId = userid | 		account.SecurityId = a.SecurityId | ||||||
| 		account.Name = security.Name | 		account.Type = a.Type | ||||||
| 		account.ParentAccountId = tradingAccount.AccountId | 		account.Name = a.Name | ||||||
| 		account.SecurityId = securityid | 		account.ParentAccountId = a.ParentAccountId | ||||||
| 		account.Type = Trading |  | ||||||
|  |  | ||||||
| 		err = transaction.Insert(&account) | 		err = transaction.Insert(&account) | ||||||
| 		if err != nil { | 		if err != nil { | ||||||
| 			transaction.Rollback() |  | ||||||
| 			return nil, err | 			return nil, err | ||||||
| 		} | 		} | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	err = transaction.Commit() |  | ||||||
| 	if err != nil { |  | ||||||
| 		transaction.Rollback() |  | ||||||
| 		return nil, err |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	return &account, nil | 	return &account, nil | ||||||
| } | } | ||||||
|  |  | ||||||
| // Get (and attempt to create if it doesn't exist) the security/currency | // Get (and attempt to create if it doesn't exist) the security/currency | ||||||
| // imbalance account for the supplied security/currency | // trading account for the supplied security/currency | ||||||
| func GetImbalanceAccount(userid int64, securityid int64) (*Account, error) { | func GetTradingAccount(transaction *gorp.Transaction, userid int64, securityid int64) (*Account, error) { | ||||||
| 	var imbalanceAccounts []Account //top-level imbalance account(s) | 	var tradingAccount Account | ||||||
| 	var imbalanceAccount Account |  | ||||||
| 	var accounts []Account //second-level security-specific imbalance account(s) |  | ||||||
| 	var account Account | 	var account Account | ||||||
|  |  | ||||||
| 	transaction, err := DB.Begin() | 	tradingAccount.UserId = userid | ||||||
|  | 	tradingAccount.Type = Trading | ||||||
|  | 	tradingAccount.Name = "Trading" | ||||||
|  | 	tradingAccount.SecurityId = 840 /*USD*/ //FIXME SecurityId shouldn't matter for top-level trading account, but maybe we should grab the user's default | ||||||
|  | 	tradingAccount.ParentAccountId = -1 | ||||||
|  |  | ||||||
|  | 	// Find/create the top-level trading account | ||||||
|  | 	ta, err := GetCreateAccountTx(transaction, tradingAccount) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return nil, err | 		return nil, err | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	// Try to find the top-level imbalance account | 	security := GetSecurity(securityid) | ||||||
| 	_, err = transaction.Select(&imbalanceAccounts, "SELECT * from accounts where UserId=? AND Name='Imbalances' AND ParentAccountId=-1 AND Type=? ORDER BY AccountId ASC LIMIT 1", userid, Bank) | 	account.UserId = userid | ||||||
|  | 	account.Name = security.Name | ||||||
|  | 	account.ParentAccountId = ta.AccountId | ||||||
|  | 	account.SecurityId = securityid | ||||||
|  | 	account.Type = Trading | ||||||
|  |  | ||||||
|  | 	a, err := GetCreateAccountTx(transaction, account) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		transaction.Rollback() |  | ||||||
| 		return nil, err |  | ||||||
| 	} |  | ||||||
| 	if len(imbalanceAccounts) == 1 { |  | ||||||
| 		imbalanceAccount = imbalanceAccounts[0] |  | ||||||
| 	} else { |  | ||||||
| 		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.Type = Bank |  | ||||||
|  |  | ||||||
| 		err = transaction.Insert(&imbalanceAccount) |  | ||||||
| 		if err != nil { |  | ||||||
| 			transaction.Rollback() |  | ||||||
| 			return nil, err |  | ||||||
| 		} |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	// Now, try to find the security-specific imbalances account |  | ||||||
| 	_, err = transaction.Select(&accounts, "SELECT * from accounts where UserId=? AND SecurityId=? AND ParentAccountId=? ORDER BY AccountId ASC LIMIT 1", userid, securityid, imbalanceAccount.AccountId) |  | ||||||
| 	if err != nil { |  | ||||||
| 		transaction.Rollback() |  | ||||||
| 		return nil, err |  | ||||||
| 	} |  | ||||||
| 	if len(accounts) == 1 { |  | ||||||
| 		account = accounts[0] |  | ||||||
| 	} else { |  | ||||||
| 		security := GetSecurity(securityid) |  | ||||||
| 		account.UserId = userid |  | ||||||
| 		account.Name = security.Name |  | ||||||
| 		account.ParentAccountId = imbalanceAccount.AccountId |  | ||||||
| 		account.SecurityId = securityid |  | ||||||
| 		account.Type = Bank |  | ||||||
|  |  | ||||||
| 		err = transaction.Insert(&account) |  | ||||||
| 		if err != nil { |  | ||||||
| 			transaction.Rollback() |  | ||||||
| 			return nil, err |  | ||||||
| 		} |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	err = transaction.Commit() |  | ||||||
| 	if err != nil { |  | ||||||
| 		transaction.Rollback() |  | ||||||
| 		return nil, err | 		return nil, err | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	return &account, nil | 	return a, nil | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // Get (and attempt to create if it doesn't exist) the security/currency | ||||||
|  | // imbalance account for the supplied security/currency | ||||||
|  | func GetImbalanceAccount(transaction *gorp.Transaction, userid int64, securityid int64) (*Account, error) { | ||||||
|  | 	var imbalanceAccount Account | ||||||
|  | 	var account Account | ||||||
|  |  | ||||||
|  | 	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.Type = Bank | ||||||
|  |  | ||||||
|  | 	// Find/create the top-level trading account | ||||||
|  | 	ia, err := GetCreateAccountTx(transaction, imbalanceAccount) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return nil, err | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	security := GetSecurity(securityid) | ||||||
|  | 	account.UserId = userid | ||||||
|  | 	account.Name = security.Name | ||||||
|  | 	account.ParentAccountId = ia.AccountId | ||||||
|  | 	account.SecurityId = securityid | ||||||
|  | 	account.Type = Bank | ||||||
|  |  | ||||||
|  | 	a, err := GetCreateAccountTx(transaction, account) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return nil, err | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	return a, nil | ||||||
| } | } | ||||||
|  |  | ||||||
| type ParentAccountMissingError struct{} | type ParentAccountMissingError struct{} | ||||||
| @@ -358,14 +318,15 @@ func AccountHandler(w http.ResponseWriter, r *http.Request) { | |||||||
| 		// import handler | 		// import handler | ||||||
| 		if accountImportRE.MatchString(r.URL.Path) { | 		if accountImportRE.MatchString(r.URL.Path) { | ||||||
| 			var accountid int64 | 			var accountid int64 | ||||||
| 			n, err := GetURLPieces(r.URL.Path, "/account/%d", &accountid) | 			var importtype string | ||||||
|  | 			n, err := GetURLPieces(r.URL.Path, "/account/%d/import/%s", &accountid, &importtype) | ||||||
|  |  | ||||||
| 			if err != nil || n != 1 { | 			if err != nil || n != 2 { | ||||||
| 				WriteError(w, 999 /*Internal Error*/) | 				WriteError(w, 999 /*Internal Error*/) | ||||||
| 				log.Print(err) | 				log.Print(err) | ||||||
| 				return | 				return | ||||||
| 			} | 			} | ||||||
| 			AccountImportHandler(w, r, user, accountid) | 			AccountImportHandler(w, r, user, accountid, importtype) | ||||||
| 			return | 			return | ||||||
| 		} | 		} | ||||||
|  |  | ||||||
|   | |||||||
							
								
								
									
										372
									
								
								gnucash.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										372
									
								
								gnucash.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,372 @@ | |||||||
|  | package main | ||||||
|  |  | ||||||
|  | import ( | ||||||
|  | 	"encoding/xml" | ||||||
|  | 	"fmt" | ||||||
|  | 	"io" | ||||||
|  | 	"log" | ||||||
|  | 	"math" | ||||||
|  | 	"math/big" | ||||||
|  | 	"net/http" | ||||||
|  | 	"time" | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | type GnucashXMLCommodity struct { | ||||||
|  | 	Name        string `xml:"http://www.gnucash.org/XML/cmdty id"` | ||||||
|  | 	Description string `xml:"http://www.gnucash.org/XML/cmdty name"` | ||||||
|  | 	Type        string `xml:"http://www.gnucash.org/XML/cmdty space"` | ||||||
|  | 	Fraction    int    `xml:"http://www.gnucash.org/XML/cmdty fraction"` | ||||||
|  | 	XCode       string `xml:"http://www.gnucash.org/XML/cmdty xcode"` | ||||||
|  | } | ||||||
|  |  | ||||||
|  | type GnucashCommodity struct{ Security } | ||||||
|  |  | ||||||
|  | func (gc *GnucashCommodity) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error { | ||||||
|  | 	var gxc GnucashXMLCommodity | ||||||
|  | 	if err := d.DecodeElement(&gxc, &start); err != nil { | ||||||
|  | 		return err | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	gc.Security.Type = Stock // assumed default | ||||||
|  | 	if gxc.Type == "ISO4217" { | ||||||
|  | 		gc.Security.Type = Currency | ||||||
|  | 	} | ||||||
|  | 	gc.Name = gxc.Name | ||||||
|  | 	gc.Symbol = gxc.Name | ||||||
|  | 	gc.Description = gxc.Description | ||||||
|  | 	gc.AlternateId = gxc.XCode | ||||||
|  | 	if gxc.Fraction > 0 { | ||||||
|  | 		gc.Precision = int(math.Ceil(math.Log10(float64(gxc.Fraction)))) | ||||||
|  | 	} else { | ||||||
|  | 		gc.Precision = 0 | ||||||
|  | 	} | ||||||
|  | 	return nil | ||||||
|  | } | ||||||
|  |  | ||||||
|  | type GnucashTime struct{ time.Time } | ||||||
|  |  | ||||||
|  | func (g *GnucashTime) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error { | ||||||
|  | 	var s string | ||||||
|  | 	if err := d.DecodeElement(&s, &start); err != nil { | ||||||
|  | 		return fmt.Errorf("date should be a string") | ||||||
|  | 	} | ||||||
|  | 	t, err := time.Parse("2006-01-02 15:04:05 -0700", s) | ||||||
|  | 	g.Time = t | ||||||
|  | 	return err | ||||||
|  | } | ||||||
|  |  | ||||||
|  | type GnucashDate struct { | ||||||
|  | 	Date GnucashTime `xml:"http://www.gnucash.org/XML/ts date"` | ||||||
|  | } | ||||||
|  |  | ||||||
|  | type GnucashAccount struct { | ||||||
|  | 	Version         string              `xml:"version,attr"` | ||||||
|  | 	accountid       int64               // Used to map Gnucash guid's to integer ones | ||||||
|  | 	AccountId       string              `xml:"http://www.gnucash.org/XML/act id"` | ||||||
|  | 	ParentAccountId string              `xml:"http://www.gnucash.org/XML/act parent"` | ||||||
|  | 	Name            string              `xml:"http://www.gnucash.org/XML/act name"` | ||||||
|  | 	Description     string              `xml:"http://www.gnucash.org/XML/act description"` | ||||||
|  | 	Type            string              `xml:"http://www.gnucash.org/XML/act type"` | ||||||
|  | 	Commodity       GnucashXMLCommodity `xml:"http://www.gnucash.org/XML/act commodity"` | ||||||
|  | } | ||||||
|  |  | ||||||
|  | type GnucashTransaction struct { | ||||||
|  | 	TransactionId string              `xml:"http://www.gnucash.org/XML/trn id"` | ||||||
|  | 	Description   string              `xml:"http://www.gnucash.org/XML/trn description"` | ||||||
|  | 	DatePosted    GnucashDate         `xml:"http://www.gnucash.org/XML/trn date-posted"` | ||||||
|  | 	DateEntered   GnucashDate         `xml:"http://www.gnucash.org/XML/trn date-entered"` | ||||||
|  | 	Commodity     GnucashXMLCommodity `xml:"http://www.gnucash.org/XML/trn currency"` | ||||||
|  | 	Splits        []GnucashSplit      `xml:"http://www.gnucash.org/XML/trn splits>split"` | ||||||
|  | } | ||||||
|  |  | ||||||
|  | type GnucashSplit struct { | ||||||
|  | 	SplitId   string `xml:"http://www.gnucash.org/XML/split id"` | ||||||
|  | 	AccountId string `xml:"http://www.gnucash.org/XML/split account"` | ||||||
|  | 	Memo      string `xml:"http://www.gnucash.org/XML/split memo"` | ||||||
|  | 	Amount    string `xml:"http://www.gnucash.org/XML/split quantity"` | ||||||
|  | 	Value     string `xml:"http://www.gnucash.org/XML/split value"` | ||||||
|  | } | ||||||
|  |  | ||||||
|  | type GnucashXMLImport struct { | ||||||
|  | 	XMLName      xml.Name             `xml:"gnc-v2"` | ||||||
|  | 	Commodities  []GnucashCommodity   `xml:"http://www.gnucash.org/XML/gnc book>commodity"` | ||||||
|  | 	Accounts     []GnucashAccount     `xml:"http://www.gnucash.org/XML/gnc book>account"` | ||||||
|  | 	Transactions []GnucashTransaction `xml:"http://www.gnucash.org/XML/gnc book>transaction"` | ||||||
|  | } | ||||||
|  |  | ||||||
|  | type GnucashImport struct { | ||||||
|  | 	Securities   []Security | ||||||
|  | 	Accounts     []Account | ||||||
|  | 	Transactions []Transaction | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func ImportGnucash(r io.Reader) (*GnucashImport, error) { | ||||||
|  | 	var gncxml GnucashXMLImport | ||||||
|  | 	var gncimport GnucashImport | ||||||
|  |  | ||||||
|  | 	// Perform initial parsing of xml into structs | ||||||
|  | 	decoder := xml.NewDecoder(r) | ||||||
|  | 	err := decoder.Decode(&gncxml) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return nil, err | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	// Fixup securities, making a map of them as we go | ||||||
|  | 	securityMap := make(map[string]Security) | ||||||
|  | 	for i := range gncxml.Commodities { | ||||||
|  | 		s := gncxml.Commodities[i].Security | ||||||
|  | 		s.SecurityId = int64(i + 1) | ||||||
|  | 		securityMap[s.Name] = s | ||||||
|  |  | ||||||
|  | 		// Ignore gnucash's "template" commodity | ||||||
|  | 		if s.Name != "template" || | ||||||
|  | 			s.Description != "template" || | ||||||
|  | 			s.AlternateId != "template" { | ||||||
|  | 			gncimport.Securities = append(gncimport.Securities, s) | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	//find root account, while simultaneously creating map of GUID's to | ||||||
|  | 	//accounts | ||||||
|  | 	var rootAccount GnucashAccount | ||||||
|  | 	accountMap := make(map[string]GnucashAccount) | ||||||
|  | 	for i := range gncxml.Accounts { | ||||||
|  | 		gncxml.Accounts[i].accountid = int64(i + 1) | ||||||
|  | 		if gncxml.Accounts[i].Type == "ROOT" { | ||||||
|  | 			rootAccount = gncxml.Accounts[i] | ||||||
|  | 		} else { | ||||||
|  | 			accountMap[gncxml.Accounts[i].AccountId] = gncxml.Accounts[i] | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	//Translate to our account format, figuring out parent relationships | ||||||
|  | 	for guid := range accountMap { | ||||||
|  | 		ga := accountMap[guid] | ||||||
|  | 		var a Account | ||||||
|  |  | ||||||
|  | 		a.AccountId = ga.accountid | ||||||
|  | 		if ga.ParentAccountId == rootAccount.AccountId { | ||||||
|  | 			a.ParentAccountId = -1 | ||||||
|  | 		} else { | ||||||
|  | 			parent, ok := accountMap[ga.ParentAccountId] | ||||||
|  | 			if ok { | ||||||
|  | 				a.ParentAccountId = parent.accountid | ||||||
|  | 			} else { | ||||||
|  | 				a.ParentAccountId = -1 // Ugly, but assign to top-level if we can't find its parent | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
|  | 		a.Name = ga.Name | ||||||
|  | 		security, ok := securityMap[ga.Commodity.Name] | ||||||
|  | 		if ok { | ||||||
|  | 		} else { | ||||||
|  | 			return nil, fmt.Errorf("Unable to find security: %s", ga.Commodity.Name) | ||||||
|  | 		} | ||||||
|  | 		a.SecurityId = security.SecurityId | ||||||
|  |  | ||||||
|  | 		//TODO find account types | ||||||
|  | 		switch ga.Type { | ||||||
|  | 		default: | ||||||
|  | 			a.Type = Bank | ||||||
|  | 		case "ASSET": | ||||||
|  | 			a.Type = Asset | ||||||
|  | 		case "BANK": | ||||||
|  | 			a.Type = Bank | ||||||
|  | 		case "CASH": | ||||||
|  | 			a.Type = Cash | ||||||
|  | 		case "CREDIT", "LIABILITY": | ||||||
|  | 			a.Type = Liability | ||||||
|  | 		case "EQUITY": | ||||||
|  | 			a.Type = Equity | ||||||
|  | 		case "EXPENSE": | ||||||
|  | 			a.Type = Expense | ||||||
|  | 		case "INCOME": | ||||||
|  | 			a.Type = Income | ||||||
|  | 		case "PAYABLE": | ||||||
|  | 			a.Type = Payable | ||||||
|  | 		case "RECEIVABLE": | ||||||
|  | 			a.Type = Receivable | ||||||
|  | 		case "MUTUAL", "STOCK": | ||||||
|  | 			a.Type = Investment | ||||||
|  | 		case "TRADING": | ||||||
|  | 			a.Type = Trading | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		gncimport.Accounts = append(gncimport.Accounts, a) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	//Translate transactions to our format | ||||||
|  | 	for i := range gncxml.Transactions { | ||||||
|  | 		gt := gncxml.Transactions[i] | ||||||
|  |  | ||||||
|  | 		t := new(Transaction) | ||||||
|  | 		t.Description = gt.Description | ||||||
|  | 		t.Date = gt.DatePosted.Date.Time | ||||||
|  | 		t.Status = Imported | ||||||
|  | 		for j := range gt.Splits { | ||||||
|  | 			gs := gt.Splits[j] | ||||||
|  | 			s := new(Split) | ||||||
|  | 			s.Memo = gs.Memo | ||||||
|  | 			account, ok := accountMap[gs.AccountId] | ||||||
|  | 			if !ok { | ||||||
|  | 				return nil, fmt.Errorf("Unable to find account: %s", gs.AccountId) | ||||||
|  | 			} | ||||||
|  | 			s.AccountId = account.accountid | ||||||
|  |  | ||||||
|  | 			security, ok := securityMap[account.Commodity.Name] | ||||||
|  | 			if !ok { | ||||||
|  | 				return nil, fmt.Errorf("Unable to find security: %s", account.Commodity.Name) | ||||||
|  | 			} | ||||||
|  | 			s.SecurityId = -1 | ||||||
|  |  | ||||||
|  | 			var r big.Rat | ||||||
|  | 			_, ok = r.SetString(gs.Amount) | ||||||
|  | 			if ok { | ||||||
|  | 				s.Amount = r.FloatString(security.Precision) | ||||||
|  | 			} else { | ||||||
|  | 				return nil, fmt.Errorf("Can't set split Amount: %s", gs.Amount) | ||||||
|  | 			} | ||||||
|  |  | ||||||
|  | 			t.Splits = append(t.Splits, s) | ||||||
|  | 		} | ||||||
|  | 		gncimport.Transactions = append(gncimport.Transactions, *t) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	return &gncimport, nil | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func GnucashImportHandler(w http.ResponseWriter, r *http.Request) { | ||||||
|  | 	user, err := GetUserFromSession(r) | ||||||
|  | 	if err != nil { | ||||||
|  | 		WriteError(w, 1 /*Not Signed In*/) | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	if r.Method != "POST" { | ||||||
|  | 		WriteError(w, 3 /*Invalid Request*/) | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	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 | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	gnucashImport, err := ImportGnucash(part) | ||||||
|  | 	if err != nil { | ||||||
|  | 		WriteError(w, 3 /*Invalid Request*/) | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	sqltransaction, err := DB.Begin() | ||||||
|  | 	if err != nil { | ||||||
|  | 		WriteError(w, 999 /*Internal Error*/) | ||||||
|  | 		log.Print(err) | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	// Import securities, building map from Gnucash security IDs to our | ||||||
|  | 	// internal IDs | ||||||
|  | 	securityMap := make(map[int64]int64) | ||||||
|  | 	for _, security := range gnucashImport.Securities { | ||||||
|  | 		//TODO FIXME check on AlternateID also, and convert to the case | ||||||
|  | 		//where users have their own internal securities | ||||||
|  | 		s, err := GetSecurityByNameAndType(security.Name, security.Type) | ||||||
|  | 		if err != nil { | ||||||
|  | 			//TODO attempt to create security if it doesn't exist | ||||||
|  | 			sqltransaction.Rollback() | ||||||
|  | 			WriteError(w, 999 /*Internal Error*/) | ||||||
|  | 			log.Print(err) | ||||||
|  | 			return | ||||||
|  | 		} | ||||||
|  | 		securityMap[security.SecurityId] = s.SecurityId | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	// Get/create accounts in the database, building a map from Gnucash account | ||||||
|  | 	// IDs to our internal IDs as we go | ||||||
|  | 	accountMap := make(map[int64]int64) | ||||||
|  | 	accountsRemaining := len(gnucashImport.Accounts) | ||||||
|  | 	accountsRemainingLast := accountsRemaining | ||||||
|  | 	for accountsRemaining > 0 { | ||||||
|  | 		for _, account := range gnucashImport.Accounts { | ||||||
|  |  | ||||||
|  | 			// If the account has already been added to the map, skip it | ||||||
|  | 			_, ok := accountMap[account.AccountId] | ||||||
|  | 			if ok { | ||||||
|  | 				continue | ||||||
|  | 			} | ||||||
|  |  | ||||||
|  | 			// If it hasn't been added, but its parent has, add it to the map | ||||||
|  | 			_, ok = accountMap[account.ParentAccountId] | ||||||
|  | 			if ok || account.ParentAccountId == -1 { | ||||||
|  | 				account.UserId = user.UserId | ||||||
|  | 				if account.ParentAccountId != -1 { | ||||||
|  | 					account.ParentAccountId = accountMap[account.ParentAccountId] | ||||||
|  | 				} | ||||||
|  | 				account.SecurityId = securityMap[account.SecurityId] | ||||||
|  | 				a, err := GetCreateAccountTx(sqltransaction, account) | ||||||
|  | 				if err != nil { | ||||||
|  | 					sqltransaction.Rollback() | ||||||
|  | 					WriteError(w, 999 /*Internal Error*/) | ||||||
|  | 					log.Print(err) | ||||||
|  | 					return | ||||||
|  | 				} | ||||||
|  | 				accountMap[account.AccountId] = a.AccountId | ||||||
|  | 				accountsRemaining-- | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
|  | 		if accountsRemaining == accountsRemainingLast { | ||||||
|  | 			//We didn't make any progress in importing the next level of accounts, so there must be a circular parent-child relationship, so give up and tell the user they're wrong | ||||||
|  | 			sqltransaction.Rollback() | ||||||
|  | 			WriteError(w, 999 /*Internal Error*/) | ||||||
|  | 			log.Print(fmt.Errorf("Circular account parent-child relationship when importing %s", part.FileName())) | ||||||
|  | 			return | ||||||
|  | 		} | ||||||
|  | 		accountsRemainingLast = accountsRemaining | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	// Insert transactions, fixing up account IDs to match internal ones from | ||||||
|  | 	// above | ||||||
|  | 	for _, transaction := range gnucashImport.Transactions { | ||||||
|  | 		for _, split := range transaction.Splits { | ||||||
|  | 			acctId, ok := accountMap[split.AccountId] | ||||||
|  | 			if !ok { | ||||||
|  | 				sqltransaction.Rollback() | ||||||
|  | 				WriteError(w, 999 /*Internal Error*/) | ||||||
|  | 				log.Print(fmt.Errorf("Error: Split's AccountID Doesn't exist: %d\n", split.AccountId)) | ||||||
|  | 				return | ||||||
|  | 			} | ||||||
|  | 			split.AccountId = acctId | ||||||
|  | 			fmt.Printf("Setting split AccountId to %d\n", acctId) | ||||||
|  | 		} | ||||||
|  | 		err := InsertTransactionTx(sqltransaction, &transaction, user) | ||||||
|  | 		if err != nil { | ||||||
|  | 			sqltransaction.Rollback() | ||||||
|  | 			WriteError(w, 999 /*Internal Error*/) | ||||||
|  | 			log.Print(err) | ||||||
|  | 			return | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	err = sqltransaction.Commit() | ||||||
|  | 	if err != nil { | ||||||
|  | 		sqltransaction.Rollback() | ||||||
|  | 		WriteError(w, 999 /*Internal Error*/) | ||||||
|  | 		log.Print(err) | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	WriteSuccess(w) | ||||||
|  | } | ||||||
							
								
								
									
										42
									
								
								imports.go
									
									
									
									
									
								
							
							
						
						
									
										42
									
								
								imports.go
									
									
									
									
									
								
							| @@ -12,7 +12,9 @@ import ( | |||||||
| /* | /* | ||||||
|  * Assumes the User is a valid, signed-in user, but accountid has not yet been validated |  * 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) { | func AccountImportHandler(w http.ResponseWriter, r *http.Request, user *User, accountid int64, importtype string) { | ||||||
|  | 	//TODO branch off for different importtype's | ||||||
|  |  | ||||||
| 	// Return Account with this Id | 	// Return Account with this Id | ||||||
| 	account, err := GetAccount(accountid, user.UserId) | 	account, err := GetAccount(accountid, user.UserId) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| @@ -58,23 +60,32 @@ func AccountImportHandler(w http.ResponseWriter, r *http.Request, user *User, ac | |||||||
| 	itl, err := ImportOFX(tmpFilename, account) | 	itl, err := ImportOFX(tmpFilename, account) | ||||||
|  |  | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		//TODO is this necessarily an invalid request? | 		//TODO is this necessarily an invalid request (what if it was an error on our end)? | ||||||
| 		WriteError(w, 3 /*Invalid Request*/) | 		WriteError(w, 3 /*Invalid Request*/) | ||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
|  | 	sqltransaction, err := DB.Begin() | ||||||
|  | 	if err != nil { | ||||||
|  | 		WriteError(w, 999 /*Internal Error*/) | ||||||
|  | 		log.Print(err) | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  |  | ||||||
| 	var transactions []Transaction | 	var transactions []Transaction | ||||||
| 	for _, transaction := range *itl.Transactions { | 	for _, transaction := range *itl.Transactions { | ||||||
| 		transaction.UserId = user.UserId | 		transaction.UserId = user.UserId | ||||||
| 		transaction.Status = Imported | 		transaction.Status = Imported | ||||||
|  |  | ||||||
| 		if !transaction.Valid() { | 		if !transaction.Valid() { | ||||||
|  | 			sqltransaction.Rollback() | ||||||
| 			WriteError(w, 3 /*Invalid Request*/) | 			WriteError(w, 3 /*Invalid Request*/) | ||||||
| 			return | 			return | ||||||
| 		} | 		} | ||||||
|  |  | ||||||
| 		imbalances, err := transaction.GetImbalances() | 		imbalances, err := transaction.GetImbalancesTx(sqltransaction) | ||||||
| 		if err != nil { | 		if err != nil { | ||||||
|  | 			sqltransaction.Rollback() | ||||||
| 			WriteError(w, 999 /*Internal Error*/) | 			WriteError(w, 999 /*Internal Error*/) | ||||||
| 			log.Print(err) | 			log.Print(err) | ||||||
| 			return | 			return | ||||||
| @@ -95,11 +106,12 @@ func AccountImportHandler(w http.ResponseWriter, r *http.Request, user *User, ac | |||||||
| 				// If we're dealing with exactly two securities, assume any imbalances | 				// If we're dealing with exactly two securities, assume any imbalances | ||||||
| 				// from imports are from trading currencies/securities | 				// from imports are from trading currencies/securities | ||||||
| 				if num_imbalances == 2 { | 				if num_imbalances == 2 { | ||||||
| 					imbalanced_account, err = GetTradingAccount(user.UserId, imbalanced_security) | 					imbalanced_account, err = GetTradingAccount(sqltransaction, user.UserId, imbalanced_security) | ||||||
| 				} else { | 				} else { | ||||||
| 					imbalanced_account, err = GetImbalanceAccount(user.UserId, imbalanced_security) | 					imbalanced_account, err = GetImbalanceAccount(sqltransaction, user.UserId, imbalanced_security) | ||||||
| 				} | 				} | ||||||
| 				if err != nil { | 				if err != nil { | ||||||
|  | 					sqltransaction.Rollback() | ||||||
| 					WriteError(w, 999 /*Internal Error*/) | 					WriteError(w, 999 /*Internal Error*/) | ||||||
| 					log.Print(err) | 					log.Print(err) | ||||||
| 					return | 					return | ||||||
| @@ -121,8 +133,9 @@ func AccountImportHandler(w http.ResponseWriter, r *http.Request, user *User, ac | |||||||
| 		// accounts | 		// accounts | ||||||
| 		for _, split := range transaction.Splits { | 		for _, split := range transaction.Splits { | ||||||
| 			if split.SecurityId != -1 || split.AccountId == -1 { | 			if split.SecurityId != -1 || split.AccountId == -1 { | ||||||
| 				imbalanced_account, err := GetImbalanceAccount(user.UserId, split.SecurityId) | 				imbalanced_account, err := GetImbalanceAccount(sqltransaction, user.UserId, split.SecurityId) | ||||||
| 				if err != nil { | 				if err != nil { | ||||||
|  | 					sqltransaction.Rollback() | ||||||
| 					WriteError(w, 999 /*Internal Error*/) | 					WriteError(w, 999 /*Internal Error*/) | ||||||
| 					log.Print(err) | 					log.Print(err) | ||||||
| 					return | 					return | ||||||
| @@ -133,23 +146,24 @@ func AccountImportHandler(w http.ResponseWriter, r *http.Request, user *User, ac | |||||||
| 			} | 			} | ||||||
| 		} | 		} | ||||||
|  |  | ||||||
| 		balanced, err := transaction.Balanced() |  | ||||||
| 		if !balanced || err != nil { |  | ||||||
| 			WriteError(w, 999 /*Internal Error*/) |  | ||||||
| 			log.Print(err) |  | ||||||
| 			return |  | ||||||
| 		} |  | ||||||
|  |  | ||||||
| 		transactions = append(transactions, transaction) | 		transactions = append(transactions, transaction) | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	for _, transaction := range transactions { | 	for _, transaction := range transactions { | ||||||
| 		err := InsertTransaction(&transaction, user) | 		err := InsertTransactionTx(sqltransaction, &transaction, user) | ||||||
| 		if err != nil { | 		if err != nil { | ||||||
| 			WriteError(w, 999 /*Internal Error*/) | 			WriteError(w, 999 /*Internal Error*/) | ||||||
| 			log.Print(err) | 			log.Print(err) | ||||||
| 		} | 		} | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
|  | 	err = sqltransaction.Commit() | ||||||
|  | 	if err != nil { | ||||||
|  | 		sqltransaction.Rollback() | ||||||
|  | 		WriteError(w, 999 /*Internal Error*/) | ||||||
|  | 		log.Print(err) | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  |  | ||||||
| 	WriteSuccess(w) | 	WriteSuccess(w) | ||||||
| } | } | ||||||
|   | |||||||
| @@ -9,6 +9,7 @@ module.exports = React.createClass({ | |||||||
| 	getDefaultProps: function() { | 	getDefaultProps: function() { | ||||||
| 		return { | 		return { | ||||||
| 			includeRoot: true, | 			includeRoot: true, | ||||||
|  | 			disabled: false, | ||||||
| 			rootName: "New Top-level Account" | 			rootName: "New Top-level Account" | ||||||
| 		}; | 		}; | ||||||
| 	}, | 	}, | ||||||
| @@ -33,6 +34,7 @@ module.exports = React.createClass({ | |||||||
| 				defaultValue={this.props.value} | 				defaultValue={this.props.value} | ||||||
| 				onChange={this.handleAccountChange} | 				onChange={this.handleAccountChange} | ||||||
| 				ref="account" | 				ref="account" | ||||||
|  | 				disabled={this.props.disabled} | ||||||
| 				className={className} /> | 				className={className} /> | ||||||
| 	   ); | 	   ); | ||||||
| 	} | 	} | ||||||
|   | |||||||
| @@ -20,8 +20,10 @@ var ButtonToolbar = ReactBootstrap.ButtonToolbar; | |||||||
| var ProgressBar = ReactBootstrap.ProgressBar; | var ProgressBar = ReactBootstrap.ProgressBar; | ||||||
| var Glyphicon = ReactBootstrap.Glyphicon; | var Glyphicon = ReactBootstrap.Glyphicon; | ||||||
|  |  | ||||||
| var DateTimePicker = require('react-widgets').DateTimePicker; | var ReactWidgets = require('react-widgets') | ||||||
| var Combobox = require('react-widgets').Combobox; | var DateTimePicker = ReactWidgets.DateTimePicker; | ||||||
|  | var Combobox = ReactWidgets.Combobox; | ||||||
|  | var DropdownList = ReactWidgets.DropdownList; | ||||||
|  |  | ||||||
| var Big = require('big.js'); | var Big = require('big.js'); | ||||||
|  |  | ||||||
| @@ -455,29 +457,39 @@ const AddEditTransactionModal = React.createClass({ | |||||||
| 	} | 	} | ||||||
| }); | }); | ||||||
|  |  | ||||||
|  | const ImportType = { | ||||||
|  | 	OFX: 1, | ||||||
|  | 	Gnucash: 2 | ||||||
|  | }; | ||||||
|  | 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 | ||||||
|  | 		ImportTypeList.push({'TypeId': ImportType[type], 'Name': name}); | ||||||
|  |    } | ||||||
|  | } | ||||||
|  |  | ||||||
| const ImportTransactionsModal = React.createClass({ | const ImportTransactionsModal = React.createClass({ | ||||||
| 	getInitialState: function() { | 	getInitialState: function() { | ||||||
| 		 return { | 		 return { | ||||||
| 			importing: false, | 			importing: false, | ||||||
| 			imported: false, | 			imported: false, | ||||||
| 			importFile: "", | 			importFile: "", | ||||||
|  | 			importType: ImportType.Gnucash, | ||||||
| 			uploadProgress: -1, | 			uploadProgress: -1, | ||||||
| 			error: null}; | 			error: null}; | ||||||
| 	}, | 	}, | ||||||
| 	handleCancel: function() { | 	handleCancel: function() { | ||||||
| 		this.setState({ | 		this.setState(this.getInitialState()); | ||||||
| 			importing: false, |  | ||||||
| 			imported: false, |  | ||||||
| 			importFile: "", |  | ||||||
| 			uploadProgress: -1, |  | ||||||
| 			error: null |  | ||||||
| 		}); |  | ||||||
| 		if (this.props.onCancel != null) | 		if (this.props.onCancel != null) | ||||||
| 			this.props.onCancel(); | 			this.props.onCancel(); | ||||||
| 	}, | 	}, | ||||||
| 	onImportChanged: function() { | 	handleImportChange: function() { | ||||||
| 		this.setState({importFile: this.refs.importfile.getValue()}); | 		this.setState({importFile: this.refs.importfile.getValue()}); | ||||||
| 	}, | 	}, | ||||||
|  | 	handleTypeChange: function(type) { | ||||||
|  | 		this.setState({importType: type.TypeId}); | ||||||
|  | 	}, | ||||||
| 	handleSubmit: function() { | 	handleSubmit: function() { | ||||||
| 		if (this.props.onSubmit != null) | 		if (this.props.onSubmit != null) | ||||||
| 			this.props.onSubmit(this.props.account); | 			this.props.onSubmit(this.props.account); | ||||||
| @@ -493,11 +505,18 @@ const ImportTransactionsModal = React.createClass({ | |||||||
| 	handleImportTransactions: function() { | 	handleImportTransactions: function() { | ||||||
| 		var file = this.refs.importfile.getInputDOMNode().files[0]; | 		var file = this.refs.importfile.getInputDOMNode().files[0]; | ||||||
| 		var formData = new FormData(); | 		var formData = new FormData(); | ||||||
| 		this.setState({importing: true}); |  | ||||||
| 		formData.append('importfile', file, this.state.importFile); | 		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({ | 		$.ajax({ | ||||||
| 			type: "POST", | 			type: "POST", | ||||||
| 			url: "account/"+this.props.account.AccountId+"/import", | 			url: url, | ||||||
| 			data: formData, | 			data: formData, | ||||||
| 			xhr: function() { | 			xhr: function() { | ||||||
| 				var xhrObject = $.ajaxSettings.xhr(); | 				var xhrObject = $.ajaxSettings.xhr(); | ||||||
| @@ -514,7 +533,7 @@ const ImportTransactionsModal = React.createClass({ | |||||||
| 				if (e.isError()) { | 				if (e.isError()) { | ||||||
| 					var errString = e.ErrorString; | 					var errString = e.ErrorString; | ||||||
| 					if (e.ErrorId == 3 /* Invalid Request */) { | 					if (e.ErrorId == 3 /* Invalid Request */) { | ||||||
| 						errString = "Please check that the file you uploaded is a valid OFX file for this account and try again."; | 						errString = "Please check that the file you uploaded is valid and try again."; | ||||||
| 					} | 					} | ||||||
| 					this.setState({ | 					this.setState({ | ||||||
| 						importing: false, | 						importing: false, | ||||||
| @@ -540,9 +559,11 @@ const ImportTransactionsModal = React.createClass({ | |||||||
| 		}); | 		}); | ||||||
| 	}, | 	}, | ||||||
| 	render: function() { | 	render: function() { | ||||||
| 		var accountNameLabel = "" | 		var accountNameLabel = "Performing global import:" | ||||||
| 		if (this.props.account != null ) | 		if (this.props.account != null && this.state.importType != ImportType.Gnucash) | ||||||
| 			accountNameLabel = "Importing to '" + getAccountDisplayName(this.props.account, this.props.account_map) + "' account:"; | 			accountNameLabel = "Importing to '" + getAccountDisplayName(this.props.account, this.props.account_map) + "' account:"; | ||||||
|  |  | ||||||
|  | 		// 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.state.importing && this.state.uploadProgress == 100) { | ||||||
| 			progressBar = (<ProgressBar now={this.state.uploadProgress} active label="Importing transactions..." />); | 			progressBar = (<ProgressBar now={this.state.uploadProgress} active label="Importing transactions..." />); | ||||||
| @@ -550,6 +571,7 @@ const ImportTransactionsModal = React.createClass({ | |||||||
| 			progressBar = (<ProgressBar now={this.state.uploadProgress} active label="Uploading... %(percent)s%" />); | 			progressBar = (<ProgressBar now={this.state.uploadProgress} active label="Uploading... %(percent)s%" />); | ||||||
| 		} | 		} | ||||||
|  |  | ||||||
|  | 		// Create panel, possibly displaying error or success messages | ||||||
| 		var panel = []; | 		var panel = []; | ||||||
| 		if (this.state.error != null) { | 		if (this.state.error != null) { | ||||||
| 			panel = (<Panel header="Error Importing Transactions" bsStyle="danger">{this.state.error}</Panel>); | 			panel = (<Panel header="Error Importing Transactions" bsStyle="danger">{this.state.error}</Panel>); | ||||||
| @@ -557,16 +579,22 @@ const ImportTransactionsModal = React.createClass({ | |||||||
| 			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>); | ||||||
| 		} | 		} | ||||||
|  |  | ||||||
| 		var buttonsDisabled = (this.state.importing) ? true : false; | 		// 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.state.imported && this.state.error == null) { | ||||||
| 			button1 = (<Button onClick={this.handleCancel} disabled={buttonsDisabled} bsStyle="warning">Cancel</Button>); | 			button1 = (<Button onClick={this.handleCancel} disabled={this.state.importing} bsStyle="warning">Cancel</Button>); | ||||||
| 			button2 = (<Button onClick={this.handleImportTransactions} disabled={buttonsDisabled} bsStyle="success">Import</Button>); | 			button2 = (<Button onClick={this.handleImportTransactions} disabled={this.state.importing || this.state.importFile == ""} bsStyle="success">Import</Button>); | ||||||
| 		} else { | 		} else { | ||||||
| 			button1 = (<Button onClick={this.handleCancel} disabled={buttonsDisabled} bsStyle="success">OK</Button>); | 			button1 = (<Button onClick={this.handleCancel} disabled={this.state.importing} bsStyle="success">OK</Button>); | ||||||
| 		} | 		} | ||||||
| 		var inputDisabled = (this.state.importing || this.state.error != null || this.state.imported) ? true : false; | 		var inputDisabled = (this.state.importing || this.state.error != null || this.state.imported) ? true : false; | ||||||
|  |  | ||||||
|  | 		// Disable OFX/QFX imports if no account is selected | ||||||
|  | 		var disabledTypes = false; | ||||||
|  | 		if (this.props.account == null) | ||||||
|  | 			disabledTypes = [ImportTypeList[ImportType.OFX - 1]]; | ||||||
|  |  | ||||||
| 		return ( | 		return ( | ||||||
| 			<Modal show={this.props.show} onHide={this.handleCancel} bsSize="small"> | 			<Modal show={this.props.show} onHide={this.handleCancel} bsSize="small"> | ||||||
| 				<Modal.Header closeButton> | 				<Modal.Header closeButton> | ||||||
| @@ -576,13 +604,21 @@ const ImportTransactionsModal = React.createClass({ | |||||||
| 				<form onSubmit={this.handleImportTransactions} | 				<form onSubmit={this.handleImportTransactions} | ||||||
| 						encType="multipart/form-data" | 						encType="multipart/form-data" | ||||||
| 						ref="importform"> | 						ref="importform"> | ||||||
|  | 					<DropdownList | ||||||
|  | 						data={ImportTypeList} | ||||||
|  | 						valueField='TypeId' | ||||||
|  | 						textField='Name' | ||||||
|  | 						onSelect={this.handleTypeChange} | ||||||
|  | 						defaultValue={this.state.importType} | ||||||
|  | 						disabled={disabledTypes} | ||||||
|  | 						ref="importtype" /> | ||||||
| 					<Input type="file" | 					<Input type="file" | ||||||
| 							ref="importfile" | 							ref="importfile" | ||||||
| 							disabled={inputDisabled} | 							disabled={inputDisabled} | ||||||
| 							value={this.state.importFile} | 							value={this.state.importFile} | ||||||
| 							label={accountNameLabel} | 							label={accountNameLabel} | ||||||
| 							help="Select an OFX/QFX file to upload." | 							help="Select an OFX/QFX file to upload." | ||||||
| 							onChange={this.onImportChanged} /> | 							onChange={this.handleImportChange} /> | ||||||
| 				</form> | 				</form> | ||||||
| 				{progressBar} | 				{progressBar} | ||||||
| 				{panel} | 				{panel} | ||||||
| @@ -897,8 +933,7 @@ module.exports = React.createClass({ | |||||||
| 					</Button> | 					</Button> | ||||||
| 					<Button | 					<Button | ||||||
| 							onClick={this.handleImportClicked} | 							onClick={this.handleImportClicked} | ||||||
| 							bsStyle="primary" | 							bsStyle="primary"> | ||||||
| 							disabled={disabled}> |  | ||||||
| 						<Glyphicon glyph='import' /> Import | 						<Glyphicon glyph='import' /> Import | ||||||
| 					</Button> | 					</Button> | ||||||
| 					</ButtonGroup> | 					</ButtonGroup> | ||||||
|   | |||||||
| @@ -25,11 +25,11 @@ import ( | |||||||
| ) | ) | ||||||
|  |  | ||||||
| type ImportObject struct { | type ImportObject struct { | ||||||
| 	TransactionList ImportTransactionsList | 	TransactionList OFXImport | ||||||
| 	Error           error | 	Error           error | ||||||
| } | } | ||||||
|  |  | ||||||
| type ImportTransactionsList struct { | type OFXImport struct { | ||||||
| 	Account           *Account | 	Account           *Account | ||||||
| 	Transactions      *[]Transaction | 	Transactions      *[]Transaction | ||||||
| 	TotalTransactions int64 | 	TotalTransactions int64 | ||||||
| @@ -249,7 +249,7 @@ func OFXTransactionCallback(transaction_data C.struct_OfxTransactionData, data u | |||||||
| 	return 0 | 	return 0 | ||||||
| } | } | ||||||
|  |  | ||||||
| func ImportOFX(filename string, account *Account) (*ImportTransactionsList, error) { | func ImportOFX(filename string, account *Account) (*OFXImport, error) { | ||||||
| 	var a Account | 	var a Account | ||||||
| 	var t []Transaction | 	var t []Transaction | ||||||
| 	var iobj ImportObject | 	var iobj ImportObject | ||||||
|   | |||||||
							
								
								
									
										1
									
								
								main.go
									
									
									
									
									
								
							
							
						
						
									
										1
									
								
								main.go
									
									
									
									
									
								
							| @@ -69,6 +69,7 @@ func main() { | |||||||
| 	servemux.HandleFunc("/security/", SecurityHandler) | 	servemux.HandleFunc("/security/", SecurityHandler) | ||||||
| 	servemux.HandleFunc("/account/", AccountHandler) | 	servemux.HandleFunc("/account/", AccountHandler) | ||||||
| 	servemux.HandleFunc("/transaction/", TransactionHandler) | 	servemux.HandleFunc("/transaction/", TransactionHandler) | ||||||
|  | 	servemux.HandleFunc("/import/gnucash", GnucashImportHandler) | ||||||
|  |  | ||||||
| 	listener, err := net.Listen("tcp", ":"+strconv.Itoa(port)) | 	listener, err := net.Listen("tcp", ":"+strconv.Itoa(port)) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
|   | |||||||
| @@ -2,7 +2,7 @@ package main | |||||||
| 
 | 
 | ||||||
| import ( | import ( | ||||||
| 	"encoding/json" | 	"encoding/json" | ||||||
| 	"errors" | 	"fmt" | ||||||
| 	"log" | 	"log" | ||||||
| 	"net/http" | 	"net/http" | ||||||
| ) | ) | ||||||
| @@ -55703,7 +55703,16 @@ func GetSecurityByName(name string) (*Security, error) { | |||||||
| 			return value, nil | 			return value, nil | ||||||
| 		} | 		} | ||||||
| 	} | 	} | ||||||
| 	return nil, errors.New("Invalid Security Name") | 	return nil, fmt.Errorf("Invalid Security Name: \"%s\"", name) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func GetSecurityByNameAndType(name string, _type int64) (*Security, error) { | ||||||
|  | 	for _, value := range security_map { | ||||||
|  | 		if value.Name == name && value.Type == _type { | ||||||
|  | 			return value, nil | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 	return nil, fmt.Errorf("Invalid Security Name (%s) or Type (%d)", name, _type) | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| func GetSecurities() []*Security { | func GetSecurities() []*Security { | ||||||
|   | |||||||
| @@ -113,7 +113,7 @@ func (t *Transaction) Valid() bool { | |||||||
|  |  | ||||||
| // Return a map of security ID's to big.Rat's containing the amount that | // Return a map of security ID's to big.Rat's containing the amount that | ||||||
| // security is imbalanced by | // security is imbalanced by | ||||||
| func (t *Transaction) GetImbalances() (map[int64]big.Rat, error) { | func (t *Transaction) GetImbalancesTx(transaction *gorp.Transaction) (map[int64]big.Rat, error) { | ||||||
| 	sums := make(map[int64]big.Rat) | 	sums := make(map[int64]big.Rat) | ||||||
|  |  | ||||||
| 	if !t.Valid() { | 	if !t.Valid() { | ||||||
| @@ -123,7 +123,13 @@ func (t *Transaction) GetImbalances() (map[int64]big.Rat, error) { | |||||||
| 	for i := range t.Splits { | 	for i := range t.Splits { | ||||||
| 		securityid := t.Splits[i].SecurityId | 		securityid := t.Splits[i].SecurityId | ||||||
| 		if t.Splits[i].AccountId != -1 { | 		if t.Splits[i].AccountId != -1 { | ||||||
| 			account, err := GetAccount(t.Splits[i].AccountId, t.UserId) | 			var err error | ||||||
|  | 			var account *Account | ||||||
|  | 			if transaction != nil { | ||||||
|  | 				account, err = GetAccountTx(transaction, t.Splits[i].AccountId, t.UserId) | ||||||
|  | 			} else { | ||||||
|  | 				account, err = GetAccount(t.Splits[i].AccountId, t.UserId) | ||||||
|  | 			} | ||||||
| 			if err != nil { | 			if err != nil { | ||||||
| 				return nil, err | 				return nil, err | ||||||
| 			} | 			} | ||||||
| @@ -137,6 +143,10 @@ func (t *Transaction) GetImbalances() (map[int64]big.Rat, error) { | |||||||
| 	return sums, nil | 	return sums, nil | ||||||
| } | } | ||||||
|  |  | ||||||
|  | func (t *Transaction) GetImbalances() (map[int64]big.Rat, error) { | ||||||
|  | 	return t.GetImbalancesTx(nil) | ||||||
|  | } | ||||||
|  |  | ||||||
| // Returns true if all securities contained in this transaction are balanced, | // Returns true if all securities contained in this transaction are balanced, | ||||||
| // false otherwise | // false otherwise | ||||||
| func (t *Transaction) Balanced() (bool, error) { | func (t *Transaction) Balanced() (bool, error) { | ||||||
| @@ -235,23 +245,16 @@ func (ame AccountMissingError) Error() string { | |||||||
| 	return "Account missing" | 	return "Account missing" | ||||||
| } | } | ||||||
|  |  | ||||||
| func InsertTransaction(t *Transaction, user *User) error { | func InsertTransactionTx(transaction *gorp.Transaction, t *Transaction, user *User) error { | ||||||
| 	transaction, err := DB.Begin() |  | ||||||
| 	if err != nil { |  | ||||||
| 		return err |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	// Map of any accounts with transaction splits being added | 	// Map of any accounts with transaction splits being added | ||||||
| 	a_map := make(map[int64]bool) | 	a_map := make(map[int64]bool) | ||||||
| 	for i := range t.Splits { | 	for i := range t.Splits { | ||||||
| 		if t.Splits[i].AccountId != -1 { | 		if t.Splits[i].AccountId != -1 { | ||||||
| 			existing, err := transaction.SelectInt("SELECT count(*) from accounts where AccountId=?", t.Splits[i].AccountId) | 			existing, err := transaction.SelectInt("SELECT count(*) from accounts where AccountId=?", t.Splits[i].AccountId) | ||||||
| 			if err != nil { | 			if err != nil { | ||||||
| 				transaction.Rollback() |  | ||||||
| 				return err | 				return err | ||||||
| 			} | 			} | ||||||
| 			if existing != 1 { | 			if existing != 1 { | ||||||
| 				transaction.Rollback() |  | ||||||
| 				return AccountMissingError{} | 				return AccountMissingError{} | ||||||
| 			} | 			} | ||||||
| 			a_map[t.Splits[i].AccountId] = true | 			a_map[t.Splits[i].AccountId] = true | ||||||
| @@ -269,15 +272,14 @@ func InsertTransaction(t *Transaction, user *User) error { | |||||||
| 	if len(a_ids) < 1 { | 	if len(a_ids) < 1 { | ||||||
| 		return AccountMissingError{} | 		return AccountMissingError{} | ||||||
| 	} | 	} | ||||||
| 	err = incrementAccountVersions(transaction, user, a_ids) | 	err := incrementAccountVersions(transaction, user, a_ids) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		transaction.Rollback() |  | ||||||
| 		return err | 		return err | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
|  | 	t.UserId = user.UserId | ||||||
| 	err = transaction.Insert(t) | 	err = transaction.Insert(t) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		transaction.Rollback() |  | ||||||
| 		return err | 		return err | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| @@ -286,11 +288,24 @@ func InsertTransaction(t *Transaction, user *User) error { | |||||||
| 		t.Splits[i].SplitId = -1 | 		t.Splits[i].SplitId = -1 | ||||||
| 		err = transaction.Insert(t.Splits[i]) | 		err = transaction.Insert(t.Splits[i]) | ||||||
| 		if err != nil { | 		if err != nil { | ||||||
| 			transaction.Rollback() |  | ||||||
| 			return err | 			return err | ||||||
| 		} | 		} | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
|  | 	return nil | ||||||
|  | } | ||||||
|  | func InsertTransaction(t *Transaction, user *User) error { | ||||||
|  | 	transaction, err := DB.Begin() | ||||||
|  | 	if err != nil { | ||||||
|  | 		return err | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	err = InsertTransactionTx(transaction, t, user) | ||||||
|  | 	if err != nil { | ||||||
|  | 		transaction.Rollback() | ||||||
|  | 		return err | ||||||
|  | 	} | ||||||
|  |  | ||||||
| 	err = transaction.Commit() | 	err = transaction.Commit() | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		transaction.Rollback() | 		transaction.Rollback() | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user