mirror of
https://github.com/aclindsa/moneygo.git
synced 2024-12-26 15:42:27 -05:00
Aaron Lindsay
89d045a51f
This is very preliminary support, and is likely to still fail for many cases - even those with nominal support. Most transaction splits end up being filed under 'imbalanced' accounts instead of something that makes more sense.
871 lines
26 KiB
Go
871 lines
26 KiB
Go
package main
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"github.com/aclindsa/ofxgo"
|
|
"io"
|
|
"math/big"
|
|
)
|
|
|
|
type OFXImport struct {
|
|
Securities []Security
|
|
Accounts []Account
|
|
Transactions []Transaction
|
|
// Balances map[int64]string // map AccountIDs to ending balances
|
|
}
|
|
|
|
func (i *OFXImport) GetSecurity(ofxsecurityid int64) (*Security, error) {
|
|
if ofxsecurityid < 0 || ofxsecurityid > int64(len(i.Securities)) {
|
|
return nil, errors.New("OFXImport.GetSecurity: SecurityID out of range")
|
|
}
|
|
return &i.Securities[ofxsecurityid], nil
|
|
}
|
|
|
|
func (i *OFXImport) GetSecurityAlternateId(alternateid string, securityType int64) (*Security, error) {
|
|
for _, security := range i.Securities {
|
|
if alternateid == security.AlternateId && securityType == security.Type {
|
|
return &security, nil
|
|
}
|
|
}
|
|
|
|
return nil, errors.New("OFXImport.FindSecurity: Unable to find security")
|
|
}
|
|
|
|
func (i *OFXImport) GetAddCurrency(isoname string) (*Security, error) {
|
|
for _, security := range i.Securities {
|
|
if isoname == security.Name && Currency == security.Type {
|
|
return &security, nil
|
|
}
|
|
}
|
|
|
|
template := FindSecurityTemplate(isoname, Currency)
|
|
if template == nil {
|
|
return nil, fmt.Errorf("Failed to find Security for \"%s\"", isoname)
|
|
}
|
|
var security Security = *template
|
|
security.SecurityId = int64(len(i.Securities) + 1)
|
|
i.Securities = append(i.Securities, security)
|
|
|
|
return &security, nil
|
|
}
|
|
|
|
func (i *OFXImport) AddTransaction(tran *ofxgo.Transaction, account *Account) error {
|
|
var t Transaction
|
|
|
|
t.Date = tran.DtPosted.UTC()
|
|
|
|
// Construct the description from whichever of the descriptive OFX fields are present
|
|
if len(tran.Name) > 0 {
|
|
t.Description = string(tran.Name)
|
|
} else if tran.Payee != nil {
|
|
t.Description = string(tran.Payee.Name)
|
|
}
|
|
if len(tran.Memo) > 0 {
|
|
if len(t.Description) > 0 {
|
|
t.Description = t.Description + " - " + string(tran.Memo)
|
|
} else {
|
|
t.Description = string(tran.Memo)
|
|
}
|
|
}
|
|
|
|
var s1, s2 Split
|
|
if len(tran.ExtdName) > 0 {
|
|
s1.Memo = tran.ExtdName.String()
|
|
}
|
|
if len(tran.CheckNum) > 0 {
|
|
s1.Number = tran.CheckNum.String()
|
|
} else if len(tran.RefNum) > 0 {
|
|
s1.Number = tran.RefNum.String()
|
|
}
|
|
|
|
amt := big.NewRat(0, 1)
|
|
// Convert TrnAmt to account's currency if Currency is set
|
|
if ok, _ := tran.Currency.Valid(); ok {
|
|
amt.Mul(&tran.Currency.CurRate.Rat, &tran.TrnAmt.Rat)
|
|
} else {
|
|
amt.Set(&tran.TrnAmt.Rat)
|
|
}
|
|
if account.SecurityId < 1 || account.SecurityId > int64(len(i.Securities)) {
|
|
return errors.New("Internal error: security index not found in OFX import\n")
|
|
}
|
|
|
|
s1.RemoteId = "ofx:" + tran.FiTID.String()
|
|
// TODO CorrectFiTID/CorrectAction?
|
|
|
|
s1.ImportSplitType = ImportAccount
|
|
s2.ImportSplitType = ExternalAccount
|
|
|
|
security := i.Securities[account.SecurityId-1]
|
|
s1.Amount = amt.FloatString(security.Precision)
|
|
s2.Amount = amt.Neg(amt).FloatString(security.Precision)
|
|
|
|
s1.Status = Imported
|
|
s2.Status = Imported
|
|
|
|
s1.AccountId = account.AccountId
|
|
s2.AccountId = -1
|
|
s1.SecurityId = -1
|
|
s2.SecurityId = security.SecurityId
|
|
|
|
t.Splits = append(t.Splits, &s1)
|
|
t.Splits = append(t.Splits, &s2)
|
|
i.Transactions = append(i.Transactions, t)
|
|
|
|
return nil
|
|
}
|
|
|
|
func (i *OFXImport) importOFXBank(stmt *ofxgo.StatementResponse) error {
|
|
security, err := i.GetAddCurrency(stmt.CurDef.String())
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
account := Account{
|
|
AccountId: int64(len(i.Accounts) + 1),
|
|
ExternalAccountId: stmt.BankAcctFrom.AcctID.String(),
|
|
SecurityId: security.SecurityId,
|
|
ParentAccountId: -1,
|
|
Type: Bank,
|
|
}
|
|
|
|
if stmt.BankTranList != nil {
|
|
for _, tran := range stmt.BankTranList.Transactions {
|
|
if err := i.AddTransaction(&tran, &account); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
}
|
|
|
|
i.Accounts = append(i.Accounts, account)
|
|
|
|
return nil
|
|
}
|
|
|
|
func (i *OFXImport) importOFXCC(stmt *ofxgo.CCStatementResponse) error {
|
|
security, err := i.GetAddCurrency(stmt.CurDef.String())
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
account := Account{
|
|
AccountId: int64(len(i.Accounts) + 1),
|
|
ExternalAccountId: stmt.CCAcctFrom.AcctID.String(),
|
|
SecurityId: security.SecurityId,
|
|
ParentAccountId: -1,
|
|
Type: Liability,
|
|
}
|
|
i.Accounts = append(i.Accounts, account)
|
|
|
|
if stmt.BankTranList != nil {
|
|
for _, tran := range stmt.BankTranList.Transactions {
|
|
if err := i.AddTransaction(&tran, &account); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
}
|
|
|
|
// TODO balance(s)
|
|
|
|
return nil
|
|
}
|
|
|
|
func (i *OFXImport) importSecurities(seclist *ofxgo.SecurityList) error {
|
|
for _, security := range seclist.Securities {
|
|
var si ofxgo.SecInfo
|
|
if sec, ok := (security).(ofxgo.DebtInfo); ok {
|
|
si = sec.SecInfo
|
|
} else if sec, ok := (security).(ofxgo.MFInfo); ok {
|
|
si = sec.SecInfo
|
|
} else if sec, ok := (security).(ofxgo.OptInfo); ok {
|
|
si = sec.SecInfo
|
|
} else if sec, ok := (security).(ofxgo.OtherInfo); ok {
|
|
si = sec.SecInfo
|
|
} else if sec, ok := (security).(ofxgo.StockInfo); ok {
|
|
si = sec.SecInfo
|
|
} else {
|
|
return errors.New("Can't import unrecognized type satisfying ofxgo.Security interface")
|
|
}
|
|
s := Security{
|
|
SecurityId: int64(len(i.Securities) + 1),
|
|
Name: string(si.SecName),
|
|
Description: string(si.Memo),
|
|
Symbol: string(si.Ticker),
|
|
Precision: 5, // TODO How to actually determine this?
|
|
Type: Stock,
|
|
AlternateId: string(si.SecID.UniqueID),
|
|
}
|
|
if len(s.Description) == 0 {
|
|
s.Description = s.Name
|
|
}
|
|
if len(s.Symbol) == 0 {
|
|
s.Symbol = s.Name
|
|
}
|
|
|
|
i.Securities = append(i.Securities, s)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (i *OFXImport) GetInvTran(invtran *ofxgo.InvTran) Transaction {
|
|
var t Transaction
|
|
t.Description = string(invtran.Memo)
|
|
t.Date = invtran.DtTrade.UTC()
|
|
return t
|
|
}
|
|
|
|
func (i *OFXImport) GetInvBuyTran(buy *ofxgo.InvBuy, curdef *Security, account *Account) (*Transaction, error) {
|
|
t := i.GetInvTran(&buy.InvTran)
|
|
|
|
security, err := i.GetSecurityAlternateId(string(buy.SecID.UniqueID), Stock)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
memo := string(buy.InvTran.Memo)
|
|
if len(memo) > 0 {
|
|
memo += " "
|
|
}
|
|
|
|
var commission, taxes, fees, load, total, tradingTotal big.Rat
|
|
commission.Set(&buy.Commission.Rat)
|
|
taxes.Set(&buy.Taxes.Rat)
|
|
fees.Set(&buy.Fees.Rat)
|
|
load.Set(&buy.Load.Rat)
|
|
total.Set(&buy.Total.Rat)
|
|
|
|
tradingTotal.Neg(&total)
|
|
tradingTotal.Sub(&tradingTotal, &commission)
|
|
tradingTotal.Sub(&tradingTotal, &taxes)
|
|
tradingTotal.Sub(&tradingTotal, &fees)
|
|
tradingTotal.Sub(&tradingTotal, &load)
|
|
|
|
// Convert amounts to account's currency if Currency is set
|
|
if ok, _ := buy.Currency.Valid(); ok {
|
|
commission.Mul(&commission, &buy.Currency.CurRate.Rat)
|
|
taxes.Mul(&taxes, &buy.Currency.CurRate.Rat)
|
|
fees.Mul(&fees, &buy.Currency.CurRate.Rat)
|
|
load.Mul(&load, &buy.Currency.CurRate.Rat)
|
|
total.Mul(&total, &buy.Currency.CurRate.Rat)
|
|
tradingTotal.Mul(&tradingTotal, &buy.Currency.CurRate.Rat)
|
|
}
|
|
|
|
if num := commission.Num(); !num.IsInt64() || num.Int64() != 0 {
|
|
t.Splits = append(t.Splits, &Split{
|
|
// TODO ReversalFiTID?
|
|
Status: Imported,
|
|
ImportSplitType: Commission,
|
|
AccountId: -1,
|
|
SecurityId: curdef.SecurityId,
|
|
RemoteId: "ofx:" + buy.InvTran.FiTID.String(),
|
|
Memo: memo + "(commission)",
|
|
Amount: commission.FloatString(curdef.Precision),
|
|
})
|
|
}
|
|
if num := taxes.Num(); !num.IsInt64() || num.Int64() != 0 {
|
|
t.Splits = append(t.Splits, &Split{
|
|
// TODO ReversalFiTID?
|
|
Status: Imported,
|
|
ImportSplitType: Taxes,
|
|
AccountId: -1,
|
|
SecurityId: curdef.SecurityId,
|
|
RemoteId: "ofx:" + buy.InvTran.FiTID.String(),
|
|
Memo: memo + "(taxes)",
|
|
Amount: taxes.FloatString(curdef.Precision),
|
|
})
|
|
}
|
|
if num := fees.Num(); !num.IsInt64() || num.Int64() != 0 {
|
|
t.Splits = append(t.Splits, &Split{
|
|
// TODO ReversalFiTID?
|
|
Status: Imported,
|
|
ImportSplitType: Fees,
|
|
AccountId: -1,
|
|
SecurityId: curdef.SecurityId,
|
|
RemoteId: "ofx:" + buy.InvTran.FiTID.String(),
|
|
Memo: memo + "(fees)",
|
|
Amount: fees.FloatString(curdef.Precision),
|
|
})
|
|
}
|
|
if num := load.Num(); !num.IsInt64() || num.Int64() != 0 {
|
|
t.Splits = append(t.Splits, &Split{
|
|
// TODO ReversalFiTID?
|
|
Status: Imported,
|
|
ImportSplitType: Load,
|
|
AccountId: -1,
|
|
SecurityId: curdef.SecurityId,
|
|
RemoteId: "ofx:" + buy.InvTran.FiTID.String(),
|
|
Memo: memo + "(load)",
|
|
Amount: load.FloatString(curdef.Precision),
|
|
})
|
|
}
|
|
t.Splits = append(t.Splits, &Split{
|
|
// TODO ReversalFiTID?
|
|
Status: Imported,
|
|
ImportSplitType: ImportAccount,
|
|
AccountId: account.AccountId,
|
|
SecurityId: -1,
|
|
RemoteId: "ofx:" + buy.InvTran.FiTID.String(),
|
|
Memo: memo,
|
|
Amount: total.FloatString(curdef.Precision),
|
|
})
|
|
t.Splits = append(t.Splits, &Split{
|
|
// TODO ReversalFiTID?
|
|
Status: Imported,
|
|
ImportSplitType: TradingAccount,
|
|
AccountId: -1,
|
|
SecurityId: curdef.SecurityId,
|
|
RemoteId: "ofx:" + buy.InvTran.FiTID.String(),
|
|
Memo: memo,
|
|
Amount: tradingTotal.FloatString(curdef.Precision),
|
|
})
|
|
|
|
units := big.NewRat(0, 1)
|
|
units.Set(&buy.Units.Rat)
|
|
t.Splits = append(t.Splits, &Split{
|
|
// TODO ReversalFiTID?
|
|
Status: Imported,
|
|
ImportSplitType: SubAccount,
|
|
AccountId: -1,
|
|
SecurityId: security.SecurityId,
|
|
RemoteId: "ofx:" + buy.InvTran.FiTID.String(),
|
|
Memo: memo,
|
|
Amount: units.FloatString(security.Precision),
|
|
})
|
|
units.Neg(units)
|
|
t.Splits = append(t.Splits, &Split{
|
|
// TODO ReversalFiTID?
|
|
Status: Imported,
|
|
ImportSplitType: TradingAccount,
|
|
AccountId: -1,
|
|
SecurityId: security.SecurityId,
|
|
RemoteId: "ofx:" + buy.InvTran.FiTID.String(),
|
|
Memo: memo,
|
|
Amount: units.FloatString(security.Precision),
|
|
})
|
|
|
|
return &t, nil
|
|
}
|
|
|
|
func (i *OFXImport) GetIncomeTran(income *ofxgo.Income, curdef *Security, account *Account) (*Transaction, error) {
|
|
t := i.GetInvTran(&income.InvTran)
|
|
|
|
security, err := i.GetSecurityAlternateId(string(income.SecID.UniqueID), Stock)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
memo := string(income.InvTran.Memo)
|
|
if len(memo) > 0 {
|
|
memo += " "
|
|
} else {
|
|
memo = income.IncomeType.String() + " on " + security.Symbol
|
|
}
|
|
|
|
var total big.Rat
|
|
total.Set(&income.Total.Rat)
|
|
if ok, _ := income.Currency.Valid(); ok {
|
|
total.Mul(&total, &income.Currency.CurRate.Rat)
|
|
}
|
|
|
|
t.Splits = append(t.Splits, &Split{
|
|
// TODO ReversalFiTID?
|
|
Status: Imported,
|
|
ImportSplitType: ImportAccount,
|
|
AccountId: account.AccountId,
|
|
SecurityId: -1,
|
|
RemoteId: "ofx:" + income.InvTran.FiTID.String(),
|
|
Memo: memo,
|
|
Amount: total.FloatString(curdef.Precision),
|
|
})
|
|
total.Neg(&total)
|
|
t.Splits = append(t.Splits, &Split{
|
|
// TODO ReversalFiTID?
|
|
Status: Imported,
|
|
ImportSplitType: IncomeAccount,
|
|
AccountId: -1,
|
|
SecurityId: curdef.SecurityId,
|
|
RemoteId: "ofx:" + income.InvTran.FiTID.String(),
|
|
Memo: memo,
|
|
Amount: total.FloatString(curdef.Precision),
|
|
})
|
|
|
|
return &t, nil
|
|
}
|
|
|
|
func (i *OFXImport) GetReinvestTran(reinvest *ofxgo.Reinvest, curdef *Security, account *Account) (*Transaction, error) {
|
|
t := i.GetInvTran(&reinvest.InvTran)
|
|
|
|
security, err := i.GetSecurityAlternateId(string(reinvest.SecID.UniqueID), Stock)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
memo := string(reinvest.InvTran.Memo)
|
|
if len(memo) > 0 {
|
|
memo += " "
|
|
}
|
|
|
|
var commission, taxes, fees, load, total, tradingTotal big.Rat
|
|
commission.Set(&reinvest.Commission.Rat)
|
|
taxes.Set(&reinvest.Taxes.Rat)
|
|
fees.Set(&reinvest.Fees.Rat)
|
|
load.Set(&reinvest.Load.Rat)
|
|
total.Set(&reinvest.Total.Rat)
|
|
|
|
tradingTotal.Neg(&total)
|
|
tradingTotal.Sub(&tradingTotal, &commission)
|
|
tradingTotal.Sub(&tradingTotal, &taxes)
|
|
tradingTotal.Sub(&tradingTotal, &fees)
|
|
tradingTotal.Sub(&tradingTotal, &load)
|
|
|
|
// Convert amounts to account's currency if Currency is set
|
|
if ok, _ := reinvest.Currency.Valid(); ok {
|
|
commission.Mul(&commission, &reinvest.Currency.CurRate.Rat)
|
|
taxes.Mul(&taxes, &reinvest.Currency.CurRate.Rat)
|
|
fees.Mul(&fees, &reinvest.Currency.CurRate.Rat)
|
|
load.Mul(&load, &reinvest.Currency.CurRate.Rat)
|
|
total.Mul(&total, &reinvest.Currency.CurRate.Rat)
|
|
tradingTotal.Mul(&tradingTotal, &reinvest.Currency.CurRate.Rat)
|
|
}
|
|
|
|
if num := commission.Num(); !num.IsInt64() || num.Int64() != 0 {
|
|
t.Splits = append(t.Splits, &Split{
|
|
// TODO ReversalFiTID?
|
|
Status: Imported,
|
|
ImportSplitType: Commission,
|
|
AccountId: -1,
|
|
SecurityId: curdef.SecurityId,
|
|
RemoteId: "ofx:" + reinvest.InvTran.FiTID.String(),
|
|
Memo: memo + "(commission)",
|
|
Amount: commission.FloatString(curdef.Precision),
|
|
})
|
|
}
|
|
if num := taxes.Num(); !num.IsInt64() || num.Int64() != 0 {
|
|
t.Splits = append(t.Splits, &Split{
|
|
// TODO ReversalFiTID?
|
|
Status: Imported,
|
|
ImportSplitType: Taxes,
|
|
AccountId: -1,
|
|
SecurityId: curdef.SecurityId,
|
|
RemoteId: "ofx:" + reinvest.InvTran.FiTID.String(),
|
|
Memo: memo + "(taxes)",
|
|
Amount: taxes.FloatString(curdef.Precision),
|
|
})
|
|
}
|
|
if num := fees.Num(); !num.IsInt64() || num.Int64() != 0 {
|
|
t.Splits = append(t.Splits, &Split{
|
|
// TODO ReversalFiTID?
|
|
Status: Imported,
|
|
ImportSplitType: Fees,
|
|
AccountId: -1,
|
|
SecurityId: curdef.SecurityId,
|
|
RemoteId: "ofx:" + reinvest.InvTran.FiTID.String(),
|
|
Memo: memo + "(fees)",
|
|
Amount: fees.FloatString(curdef.Precision),
|
|
})
|
|
}
|
|
if num := load.Num(); !num.IsInt64() || num.Int64() != 0 {
|
|
t.Splits = append(t.Splits, &Split{
|
|
// TODO ReversalFiTID?
|
|
Status: Imported,
|
|
ImportSplitType: Load,
|
|
AccountId: -1,
|
|
SecurityId: curdef.SecurityId,
|
|
RemoteId: "ofx:" + reinvest.InvTran.FiTID.String(),
|
|
Memo: memo + "(load)",
|
|
Amount: load.FloatString(curdef.Precision),
|
|
})
|
|
}
|
|
t.Splits = append(t.Splits, &Split{
|
|
// TODO ReversalFiTID?
|
|
Status: Imported,
|
|
ImportSplitType: ImportAccount,
|
|
AccountId: account.AccountId,
|
|
SecurityId: -1,
|
|
RemoteId: "ofx:" + reinvest.InvTran.FiTID.String(),
|
|
Memo: memo,
|
|
Amount: total.FloatString(curdef.Precision),
|
|
})
|
|
|
|
t.Splits = append(t.Splits, &Split{
|
|
// TODO ReversalFiTID?
|
|
Status: Imported,
|
|
ImportSplitType: IncomeAccount,
|
|
AccountId: -1,
|
|
SecurityId: curdef.SecurityId,
|
|
RemoteId: "ofx:" + reinvest.InvTran.FiTID.String(),
|
|
Memo: memo,
|
|
Amount: total.FloatString(curdef.Precision),
|
|
})
|
|
total.Neg(&total)
|
|
t.Splits = append(t.Splits, &Split{
|
|
// TODO ReversalFiTID?
|
|
Status: Imported,
|
|
ImportSplitType: ImportAccount,
|
|
AccountId: account.AccountId,
|
|
SecurityId: -1,
|
|
RemoteId: "ofx:" + reinvest.InvTran.FiTID.String(),
|
|
Memo: memo,
|
|
Amount: total.FloatString(curdef.Precision),
|
|
})
|
|
t.Splits = append(t.Splits, &Split{
|
|
// TODO ReversalFiTID?
|
|
Status: Imported,
|
|
ImportSplitType: TradingAccount,
|
|
AccountId: -1,
|
|
SecurityId: curdef.SecurityId,
|
|
RemoteId: "ofx:" + reinvest.InvTran.FiTID.String(),
|
|
Memo: memo,
|
|
Amount: tradingTotal.FloatString(curdef.Precision),
|
|
})
|
|
|
|
var units big.Rat
|
|
units.Set(&reinvest.Units.Rat)
|
|
t.Splits = append(t.Splits, &Split{
|
|
// TODO ReversalFiTID?
|
|
Status: Imported,
|
|
ImportSplitType: SubAccount,
|
|
AccountId: -1,
|
|
SecurityId: security.SecurityId,
|
|
RemoteId: "ofx:" + reinvest.InvTran.FiTID.String(),
|
|
Memo: memo,
|
|
Amount: units.FloatString(security.Precision),
|
|
})
|
|
units.Neg(&units)
|
|
t.Splits = append(t.Splits, &Split{
|
|
// TODO ReversalFiTID?
|
|
Status: Imported,
|
|
ImportSplitType: TradingAccount,
|
|
AccountId: -1,
|
|
SecurityId: security.SecurityId,
|
|
RemoteId: "ofx:" + reinvest.InvTran.FiTID.String(),
|
|
Memo: memo,
|
|
Amount: units.FloatString(security.Precision),
|
|
})
|
|
|
|
return &t, nil
|
|
}
|
|
|
|
func (i *OFXImport) GetInvSellTran(sell *ofxgo.InvSell, curdef *Security, account *Account) (*Transaction, error) {
|
|
t := i.GetInvTran(&sell.InvTran)
|
|
|
|
security, err := i.GetSecurityAlternateId(string(sell.SecID.UniqueID), Stock)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
memo := string(sell.InvTran.Memo)
|
|
if len(memo) > 0 {
|
|
memo += " "
|
|
}
|
|
|
|
var commission, taxes, fees, load, total, tradingTotal big.Rat
|
|
commission.Set(&sell.Commission.Rat)
|
|
taxes.Set(&sell.Taxes.Rat)
|
|
fees.Set(&sell.Fees.Rat)
|
|
load.Set(&sell.Load.Rat)
|
|
total.Set(&sell.Total.Rat)
|
|
|
|
tradingTotal.Neg(&total)
|
|
tradingTotal.Sub(&tradingTotal, &commission)
|
|
tradingTotal.Sub(&tradingTotal, &taxes)
|
|
tradingTotal.Sub(&tradingTotal, &fees)
|
|
tradingTotal.Sub(&tradingTotal, &load)
|
|
|
|
// Convert amounts to account's currency if Currency is set
|
|
if ok, _ := sell.Currency.Valid(); ok {
|
|
commission.Mul(&commission, &sell.Currency.CurRate.Rat)
|
|
taxes.Mul(&taxes, &sell.Currency.CurRate.Rat)
|
|
fees.Mul(&fees, &sell.Currency.CurRate.Rat)
|
|
load.Mul(&load, &sell.Currency.CurRate.Rat)
|
|
total.Mul(&total, &sell.Currency.CurRate.Rat)
|
|
tradingTotal.Mul(&tradingTotal, &sell.Currency.CurRate.Rat)
|
|
}
|
|
|
|
if num := commission.Num(); !num.IsInt64() || num.Int64() != 0 {
|
|
t.Splits = append(t.Splits, &Split{
|
|
// TODO ReversalFiTID?
|
|
Status: Imported,
|
|
ImportSplitType: Commission,
|
|
AccountId: -1,
|
|
SecurityId: curdef.SecurityId,
|
|
RemoteId: "ofx:" + sell.InvTran.FiTID.String(),
|
|
Memo: memo + "(commission)",
|
|
Amount: commission.FloatString(curdef.Precision),
|
|
})
|
|
}
|
|
if num := taxes.Num(); !num.IsInt64() || num.Int64() != 0 {
|
|
t.Splits = append(t.Splits, &Split{
|
|
// TODO ReversalFiTID?
|
|
Status: Imported,
|
|
ImportSplitType: Taxes,
|
|
AccountId: -1,
|
|
SecurityId: curdef.SecurityId,
|
|
RemoteId: "ofx:" + sell.InvTran.FiTID.String(),
|
|
Memo: memo + "(taxes)",
|
|
Amount: taxes.FloatString(curdef.Precision),
|
|
})
|
|
}
|
|
if num := fees.Num(); !num.IsInt64() || num.Int64() != 0 {
|
|
t.Splits = append(t.Splits, &Split{
|
|
// TODO ReversalFiTID?
|
|
Status: Imported,
|
|
ImportSplitType: Fees,
|
|
AccountId: -1,
|
|
SecurityId: curdef.SecurityId,
|
|
RemoteId: "ofx:" + sell.InvTran.FiTID.String(),
|
|
Memo: memo + "(fees)",
|
|
Amount: fees.FloatString(curdef.Precision),
|
|
})
|
|
}
|
|
if num := load.Num(); !num.IsInt64() || num.Int64() != 0 {
|
|
t.Splits = append(t.Splits, &Split{
|
|
// TODO ReversalFiTID?
|
|
Status: Imported,
|
|
ImportSplitType: Load,
|
|
AccountId: -1,
|
|
SecurityId: curdef.SecurityId,
|
|
RemoteId: "ofx:" + sell.InvTran.FiTID.String(),
|
|
Memo: memo + "(load)",
|
|
Amount: load.FloatString(curdef.Precision),
|
|
})
|
|
}
|
|
t.Splits = append(t.Splits, &Split{
|
|
// TODO ReversalFiTID?
|
|
Status: Imported,
|
|
ImportSplitType: ImportAccount,
|
|
AccountId: account.AccountId,
|
|
SecurityId: -1,
|
|
RemoteId: "ofx:" + sell.InvTran.FiTID.String(),
|
|
Memo: memo,
|
|
Amount: total.FloatString(curdef.Precision),
|
|
})
|
|
t.Splits = append(t.Splits, &Split{
|
|
// TODO ReversalFiTID?
|
|
Status: Imported,
|
|
ImportSplitType: TradingAccount,
|
|
AccountId: -1,
|
|
SecurityId: curdef.SecurityId,
|
|
RemoteId: "ofx:" + sell.InvTran.FiTID.String(),
|
|
Memo: memo,
|
|
Amount: tradingTotal.FloatString(curdef.Precision),
|
|
})
|
|
|
|
var units big.Rat
|
|
units.Set(&sell.Units.Rat)
|
|
t.Splits = append(t.Splits, &Split{
|
|
// TODO ReversalFiTID?
|
|
Status: Imported,
|
|
ImportSplitType: SubAccount,
|
|
AccountId: -1,
|
|
SecurityId: security.SecurityId,
|
|
RemoteId: "ofx:" + sell.InvTran.FiTID.String(),
|
|
Memo: memo,
|
|
Amount: units.FloatString(security.Precision),
|
|
})
|
|
units.Neg(&units)
|
|
t.Splits = append(t.Splits, &Split{
|
|
// TODO ReversalFiTID?
|
|
Status: Imported,
|
|
ImportSplitType: TradingAccount,
|
|
AccountId: -1,
|
|
SecurityId: security.SecurityId,
|
|
RemoteId: "ofx:" + sell.InvTran.FiTID.String(),
|
|
Memo: memo,
|
|
Amount: units.FloatString(security.Precision),
|
|
})
|
|
|
|
return &t, nil
|
|
}
|
|
|
|
func (i *OFXImport) GetTransferTran(transfer *ofxgo.Transfer, account *Account) (*Transaction, error) {
|
|
t := i.GetInvTran(&transfer.InvTran)
|
|
|
|
security, err := i.GetSecurityAlternateId(string(transfer.SecID.UniqueID), Stock)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
memo := string(transfer.InvTran.Memo)
|
|
|
|
var units big.Rat
|
|
if transfer.TferAction == ofxgo.TferActionIn {
|
|
units.Set(&transfer.Units.Rat)
|
|
} else {
|
|
units.Neg(&transfer.Units.Rat)
|
|
}
|
|
|
|
t.Splits = append(t.Splits, &Split{
|
|
// TODO ReversalFiTID?
|
|
Status: Imported,
|
|
ImportSplitType: SubAccount,
|
|
AccountId: -1,
|
|
SecurityId: security.SecurityId,
|
|
RemoteId: "ofx:" + transfer.InvTran.FiTID.String(),
|
|
Memo: memo,
|
|
Amount: units.FloatString(security.Precision),
|
|
})
|
|
units.Neg(&units)
|
|
t.Splits = append(t.Splits, &Split{
|
|
// TODO ReversalFiTID?
|
|
Status: Imported,
|
|
ImportSplitType: ExternalAccount,
|
|
AccountId: -1,
|
|
SecurityId: security.SecurityId,
|
|
RemoteId: "ofx:" + transfer.InvTran.FiTID.String(),
|
|
Memo: memo,
|
|
Amount: units.FloatString(security.Precision),
|
|
})
|
|
|
|
return &t, nil
|
|
}
|
|
|
|
func (i *OFXImport) AddInvTransaction(invtran *ofxgo.InvTransaction, account *Account, curdef *Security) error {
|
|
if curdef.SecurityId < 1 || curdef.SecurityId > int64(len(i.Securities)) {
|
|
return errors.New("Internal error: security index not found in OFX import\n")
|
|
}
|
|
|
|
var t *Transaction
|
|
var err error
|
|
if tran, ok := (*invtran).(ofxgo.BuyDebt); ok {
|
|
t, err = i.GetInvBuyTran(&tran.InvBuy, curdef, account)
|
|
} else if tran, ok := (*invtran).(ofxgo.BuyMF); ok {
|
|
t, err = i.GetInvBuyTran(&tran.InvBuy, curdef, account)
|
|
} else if tran, ok := (*invtran).(ofxgo.BuyOpt); ok {
|
|
t, err = i.GetInvBuyTran(&tran.InvBuy, curdef, account)
|
|
} else if tran, ok := (*invtran).(ofxgo.BuyOther); ok {
|
|
t, err = i.GetInvBuyTran(&tran.InvBuy, curdef, account)
|
|
} else if tran, ok := (*invtran).(ofxgo.BuyStock); ok {
|
|
t, err = i.GetInvBuyTran(&tran.InvBuy, curdef, account)
|
|
// TODO implementme
|
|
// } else if tran, ok := (*invtran).(ofxgo.ClosureOpt); ok {
|
|
} else if tran, ok := (*invtran).(ofxgo.Income); ok {
|
|
t, err = i.GetIncomeTran(&tran, curdef, account)
|
|
// } else if tran, ok := (*invtran).(ofxgo.InvExpense); ok {
|
|
// } else if tran, ok := (*invtran).(ofxgo.JrnlFund); ok {
|
|
// } else if tran, ok := (*invtran).(ofxgo.JrnlSec); ok {
|
|
// } else if tran, ok := (*invtran).(ofxgo.MarginInterest); ok {
|
|
} else if tran, ok := (*invtran).(ofxgo.Reinvest); ok {
|
|
t, err = i.GetReinvestTran(&tran, curdef, account)
|
|
// } else if tran, ok := (*invtran).(ofxgo.RetOfCap); ok {
|
|
} else if tran, ok := (*invtran).(ofxgo.SellDebt); ok {
|
|
t, err = i.GetInvSellTran(&tran.InvSell, curdef, account)
|
|
} else if tran, ok := (*invtran).(ofxgo.SellMF); ok {
|
|
t, err = i.GetInvSellTran(&tran.InvSell, curdef, account)
|
|
} else if tran, ok := (*invtran).(ofxgo.SellOpt); ok {
|
|
t, err = i.GetInvSellTran(&tran.InvSell, curdef, account)
|
|
} else if tran, ok := (*invtran).(ofxgo.SellOther); ok {
|
|
t, err = i.GetInvSellTran(&tran.InvSell, curdef, account)
|
|
} else if tran, ok := (*invtran).(ofxgo.SellStock); ok {
|
|
t, err = i.GetInvSellTran(&tran.InvSell, curdef, account)
|
|
// TODO implementme
|
|
// } else if tran, ok := (*invtran).(ofxgo.Split); ok {
|
|
} else if tran, ok := (*invtran).(ofxgo.Transfer); ok {
|
|
t, err = i.GetTransferTran(&tran, account)
|
|
} else {
|
|
return errors.New("Unrecognized type satisfying ofxgo.InvTransaction interface: " + (*invtran).TransactionType())
|
|
return nil
|
|
|
|
}
|
|
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
i.Transactions = append(i.Transactions, *t)
|
|
|
|
return nil
|
|
}
|
|
|
|
func (i *OFXImport) importOFXInv(stmt *ofxgo.InvStatementResponse) error {
|
|
security, err := i.GetAddCurrency(stmt.CurDef.String())
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
account := Account{
|
|
AccountId: int64(len(i.Accounts) + 1),
|
|
ExternalAccountId: stmt.InvAcctFrom.AcctID.String(),
|
|
SecurityId: security.SecurityId,
|
|
ParentAccountId: -1,
|
|
Type: Investment,
|
|
}
|
|
i.Accounts = append(i.Accounts, account)
|
|
|
|
if stmt.InvTranList != nil {
|
|
for _, invtran := range stmt.InvTranList.InvTransactions {
|
|
if err := i.AddInvTransaction(&invtran, &account, security); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
for _, bt := range stmt.InvTranList.BankTransactions {
|
|
// TODO Should we do something different for the value of
|
|
// bt.SubAcctFund?
|
|
for _, tran := range bt.Transactions {
|
|
if err := i.AddTransaction(&tran, &account); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// TODO InvPosList
|
|
// TODO InvBal
|
|
// TODO Inv401K and INV401kBal???
|
|
|
|
return nil
|
|
}
|
|
|
|
func ImportOFX(r io.Reader) (*OFXImport, error) {
|
|
var i OFXImport
|
|
|
|
response, err := ofxgo.ParseResponse(r)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("Unexpected error parsing OFX response: %s\n", err)
|
|
}
|
|
|
|
if response.Signon.Status.Code != 0 {
|
|
meaning, _ := response.Signon.Status.CodeMeaning()
|
|
return nil, fmt.Errorf("Nonzero signon status (%d: %s) with message: %s\n", response.Signon.Status.Code, meaning, response.Signon.Status.Message)
|
|
}
|
|
|
|
for _, bank := range response.Bank {
|
|
if stmt, ok := bank.(*ofxgo.StatementResponse); ok {
|
|
err = i.importOFXBank(stmt)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return &i, nil
|
|
}
|
|
}
|
|
for _, cc := range response.CreditCard {
|
|
if stmt, ok := cc.(*ofxgo.CCStatementResponse); ok {
|
|
err = i.importOFXCC(stmt)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return &i, nil
|
|
}
|
|
}
|
|
for _, seclist := range response.SecList {
|
|
if securitylist, ok := seclist.(*ofxgo.SecurityList); ok {
|
|
err = i.importSecurities(securitylist)
|
|
// TODO actually import securities
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
}
|
|
for _, inv := range response.InvStmt {
|
|
if stmt, ok := inv.(*ofxgo.InvStatementResponse); ok {
|
|
err = i.importOFXInv(stmt)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return &i, nil
|
|
}
|
|
}
|
|
|
|
return nil, errors.New("No OFX statement found")
|
|
}
|