mirror of
https://github.com/aclindsa/moneygo.git
synced 2024-10-31 16:00:05 -04:00
Aaron Lindsay
58c7c17727
Still needs some fixups: * UI is incomplete * Investment transactions are unbalanced initially * OFX imports don't detect if one of the description fields for a transaction is empty (to fall back on another) * I'm sure plenty of other issues I haven't discovered yet
284 lines
8.6 KiB
Go
284 lines
8.6 KiB
Go
package main
|
|
|
|
//#cgo LDFLAGS: -lofx
|
|
//
|
|
//#include <stdlib.h>
|
|
//
|
|
// //The next line disables the definition of static variables to allow for it to
|
|
// //be included here (see libofx commit bd24df15531e52a2858f70487443af8b9fa407f4)
|
|
//#define OFX_AQUAMANIAC_UGLY_HACK1
|
|
//#include <libofx/libofx.h>
|
|
//
|
|
// typedef int (*ofx_statement_cb_fn) (const struct OfxStatementData, void *);
|
|
// extern int ofx_statement_callback(const struct OfxStatementData, void *);
|
|
// typedef int (*ofx_account_cb_fn) (const struct OfxAccountData, void *);
|
|
// extern int ofx_account_callback(const struct OfxAccountData, void *);
|
|
// typedef int (*ofx_transaction_cb_fn) (const struct OfxTransactionData, void *);
|
|
// extern int ofx_transaction_callback(const struct OfxTransactionData, void *);
|
|
import "C"
|
|
|
|
import (
|
|
"errors"
|
|
"math/big"
|
|
"time"
|
|
"unsafe"
|
|
)
|
|
|
|
type ImportObject struct {
|
|
TransactionList ImportTransactionsList
|
|
Error error
|
|
}
|
|
|
|
type ImportTransactionsList struct {
|
|
Account *Account
|
|
Transactions *[]Transaction
|
|
TotalTransactions int64
|
|
BeginningBalance string
|
|
EndingBalance string
|
|
}
|
|
|
|
func init() {
|
|
// Turn off all libofx info/debug messages
|
|
C.ofx_PARSER_msg = 0
|
|
C.ofx_DEBUG_msg = 0
|
|
C.ofx_DEBUG1_msg = 0
|
|
C.ofx_DEBUG2_msg = 0
|
|
C.ofx_DEBUG3_msg = 0
|
|
C.ofx_DEBUG4_msg = 0
|
|
C.ofx_DEBUG5_msg = 0
|
|
C.ofx_STATUS_msg = 0
|
|
C.ofx_INFO_msg = 0
|
|
C.ofx_WARNING_msg = 0
|
|
C.ofx_ERROR_msg = 0
|
|
}
|
|
|
|
//export OFXStatementCallback
|
|
func OFXStatementCallback(statement_data C.struct_OfxStatementData, data unsafe.Pointer) C.int {
|
|
// import := (*ImportObject)(data)
|
|
return 0
|
|
}
|
|
|
|
//export OFXAccountCallback
|
|
func OFXAccountCallback(account_data C.struct_OfxAccountData, data unsafe.Pointer) C.int {
|
|
iobj := (*ImportObject)(data)
|
|
itl := iobj.TransactionList
|
|
if account_data.account_id_valid != 0 {
|
|
account_name := C.GoString(&account_data.account_name[0])
|
|
account_id := C.GoString(&account_data.account_id[0])
|
|
itl.Account.Name = account_name
|
|
itl.Account.ExternalAccountId = account_id
|
|
} else {
|
|
if iobj.Error == nil {
|
|
iobj.Error = errors.New("OFX account ID invalid")
|
|
}
|
|
return 1
|
|
}
|
|
if account_data.account_type_valid != 0 {
|
|
switch account_data.account_type {
|
|
case C.OFX_CHECKING, C.OFX_SAVINGS, C.OFX_MONEYMRKT, C.OFX_CMA:
|
|
itl.Account.Type = Bank
|
|
case C.OFX_CREDITLINE, C.OFX_CREDITCARD:
|
|
itl.Account.Type = Liability
|
|
case C.OFX_INVESTMENT:
|
|
itl.Account.Type = Investment
|
|
}
|
|
} else {
|
|
if iobj.Error == nil {
|
|
iobj.Error = errors.New("OFX account type invalid")
|
|
}
|
|
return 1
|
|
}
|
|
if account_data.currency_valid != 0 {
|
|
currency_name := C.GoString(&account_data.currency[0])
|
|
currency, err := GetSecurityByName(currency_name)
|
|
if err != nil {
|
|
if iobj.Error == nil {
|
|
iobj.Error = err
|
|
}
|
|
return 1
|
|
}
|
|
itl.Account.SecurityId = currency.SecurityId
|
|
} else {
|
|
if iobj.Error == nil {
|
|
iobj.Error = errors.New("OFX account currency invalid")
|
|
}
|
|
return 1
|
|
}
|
|
return 0
|
|
}
|
|
|
|
//export OFXTransactionCallback
|
|
func OFXTransactionCallback(transaction_data C.struct_OfxTransactionData, data unsafe.Pointer) C.int {
|
|
iobj := (*ImportObject)(data)
|
|
itl := iobj.TransactionList
|
|
transaction := new(Transaction)
|
|
|
|
if transaction_data.name_valid != 0 {
|
|
transaction.Description = C.GoString(&transaction_data.name[0])
|
|
}
|
|
// if transaction_data.reference_number_valid != 0 {
|
|
// fmt.Println("reference_number: ", C.GoString(&transaction_data.reference_number[0]))
|
|
// }
|
|
if transaction_data.date_posted_valid != 0 {
|
|
transaction.Date = time.Unix(int64(transaction_data.date_posted), 0)
|
|
} else if transaction_data.date_initiated_valid != 0 {
|
|
transaction.Date = time.Unix(int64(transaction_data.date_initiated), 0)
|
|
}
|
|
if transaction_data.fi_id_valid != 0 {
|
|
transaction.RemoteId = C.GoString(&transaction_data.fi_id[0])
|
|
}
|
|
|
|
if transaction_data.amount_valid != 0 {
|
|
split := new(Split)
|
|
r := new(big.Rat)
|
|
r.SetFloat64(float64(transaction_data.amount))
|
|
security := GetSecurity(itl.Account.SecurityId)
|
|
split.Amount = r.FloatString(security.Precision)
|
|
if transaction_data.memo_valid != 0 {
|
|
split.Memo = C.GoString(&transaction_data.memo[0])
|
|
}
|
|
if transaction_data.check_number_valid != 0 {
|
|
split.Number = C.GoString(&transaction_data.check_number[0])
|
|
}
|
|
split.SecurityId = -1
|
|
split.AccountId = itl.Account.AccountId
|
|
transaction.Splits = append(transaction.Splits, split)
|
|
} else {
|
|
if iobj.Error == nil {
|
|
iobj.Error = errors.New("OFX transaction amount invalid")
|
|
}
|
|
return 1
|
|
}
|
|
|
|
var security *Security
|
|
split := new(Split)
|
|
units := new(big.Rat)
|
|
|
|
if transaction_data.units_valid != 0 {
|
|
units.SetFloat64(float64(transaction_data.units))
|
|
if transaction_data.security_data_valid != 0 {
|
|
security_data := transaction_data.security_data_ptr
|
|
if security_data.ticker_valid != 0 {
|
|
s, err := GetSecurityByName(C.GoString(&security_data.ticker[0]))
|
|
if err != nil {
|
|
if iobj.Error == nil {
|
|
iobj.Error = errors.New("Failed to find OFX transaction security: " + C.GoString(&security_data.ticker[0]))
|
|
}
|
|
return 1
|
|
}
|
|
security = s
|
|
} else {
|
|
if iobj.Error == nil {
|
|
iobj.Error = errors.New("OFX security ticker invalid")
|
|
}
|
|
return 1
|
|
}
|
|
if security.Type == Stock && security_data.unique_id_valid != 0 && security_data.unique_id_type_valid != 0 && C.GoString(&security_data.unique_id_type[0]) == "CUSIP" {
|
|
// Validate the security CUSIP, if possible
|
|
if security.AlternateId != C.GoString(&security_data.unique_id[0]) {
|
|
if iobj.Error == nil {
|
|
iobj.Error = errors.New("OFX transaction security CUSIP failed to validate")
|
|
}
|
|
return 1
|
|
}
|
|
}
|
|
} else {
|
|
security = GetSecurity(itl.Account.SecurityId)
|
|
}
|
|
} else {
|
|
// Calculate units from other available fields if its not present
|
|
// units = - (amount + various fees) / unitprice
|
|
units.SetFloat64(float64(transaction_data.amount))
|
|
fees := new(big.Rat)
|
|
if transaction_data.fees_valid != 0 {
|
|
fees.SetFloat64(float64(-transaction_data.fees))
|
|
}
|
|
if transaction_data.commission_valid != 0 {
|
|
commission := new(big.Rat)
|
|
commission.SetFloat64(float64(-transaction_data.commission))
|
|
fees.Add(fees, commission)
|
|
}
|
|
units.Add(units, fees)
|
|
units.Neg(units)
|
|
if transaction_data.unitprice_valid != 0 && transaction_data.unitprice != 0 {
|
|
unitprice := new(big.Rat)
|
|
unitprice.SetFloat64(float64(transaction_data.unitprice))
|
|
units.Quo(units, unitprice)
|
|
}
|
|
|
|
// If 'units' wasn't present, assume we're using the account's security
|
|
security = GetSecurity(itl.Account.SecurityId)
|
|
}
|
|
|
|
split.Amount = units.FloatString(security.Precision)
|
|
split.SecurityId = security.SecurityId
|
|
split.AccountId = -1
|
|
transaction.Splits = append(transaction.Splits, split)
|
|
|
|
if transaction_data.fees_valid != 0 {
|
|
split := new(Split)
|
|
r := new(big.Rat)
|
|
r.SetFloat64(float64(-transaction_data.fees))
|
|
security := GetSecurity(itl.Account.SecurityId)
|
|
split.Amount = r.FloatString(security.Precision)
|
|
split.Memo = "fees"
|
|
split.SecurityId = itl.Account.SecurityId
|
|
split.AccountId = -1
|
|
transaction.Splits = append(transaction.Splits, split)
|
|
}
|
|
|
|
if transaction_data.commission_valid != 0 {
|
|
split := new(Split)
|
|
r := new(big.Rat)
|
|
r.SetFloat64(float64(-transaction_data.commission))
|
|
security := GetSecurity(itl.Account.SecurityId)
|
|
split.Amount = r.FloatString(security.Precision)
|
|
split.Memo = "commission"
|
|
split.SecurityId = itl.Account.SecurityId
|
|
split.AccountId = -1
|
|
transaction.Splits = append(transaction.Splits, split)
|
|
}
|
|
|
|
// if transaction_data.payee_id_valid != 0 {
|
|
// fmt.Println("payee_id: ", C.GoString(&transaction_data.payee_id[0]))
|
|
// }
|
|
|
|
transaction_list := append(*itl.Transactions, *transaction)
|
|
iobj.TransactionList.Transactions = &transaction_list
|
|
|
|
return 0
|
|
}
|
|
|
|
func ImportOFX(filename string, account *Account) (*ImportTransactionsList, error) {
|
|
var a Account
|
|
var t []Transaction
|
|
var iobj ImportObject
|
|
iobj.TransactionList.Account = &a
|
|
iobj.TransactionList.Transactions = &t
|
|
|
|
a.AccountId = account.AccountId
|
|
|
|
context := C.libofx_get_new_context()
|
|
defer C.libofx_free_context(context)
|
|
|
|
C.ofx_set_statement_cb(context, C.ofx_statement_cb_fn(C.ofx_statement_callback), unsafe.Pointer(&iobj))
|
|
C.ofx_set_account_cb(context, C.ofx_account_cb_fn(C.ofx_account_callback), unsafe.Pointer(&iobj))
|
|
C.ofx_set_transaction_cb(context, C.ofx_transaction_cb_fn(C.ofx_transaction_callback), unsafe.Pointer(&iobj))
|
|
|
|
filename_cstring := C.CString(filename)
|
|
defer C.free(unsafe.Pointer(filename_cstring))
|
|
C.libofx_proc_file(context, filename_cstring, C.OFX) // unconditionally returns 0.
|
|
|
|
iobj.TransactionList.TotalTransactions = int64(len(*iobj.TransactionList.Transactions))
|
|
|
|
if iobj.TransactionList.TotalTransactions == 0 {
|
|
return nil, errors.New("No OFX transactions found")
|
|
}
|
|
|
|
if iobj.Error != nil {
|
|
return nil, iobj.Error
|
|
} else {
|
|
return &iobj.TransactionList, nil
|
|
}
|
|
}
|