func AccountsHandler(w http.ResponseWriter, r *http.Request) { t, err := parseAssets("templates/template.accounts.html", "templates/template.nav.html") if err != nil { http.Error(w, err.Error(), 500) return } trans, terr := getTransactions() if terr != nil { http.Error(w, terr.Error(), 500) return } balances := ledger.GetBalances(trans, []string{}) var pData pageData pData.Reports = reportConfigData.Reports pData.Accounts = balances pData.Transactions = trans err = t.Execute(w, pData) if err != nil { http.Error(w, err.Error(), 500) } }
func main() { var startDate, endDate time.Time startDate = time.Date(1970, 1, 1, 0, 0, 0, 0, time.Local) endDate = time.Now().Add(time.Hour * 24) var startString, endString string var columnWidth, transactionDepth int var showEmptyAccounts bool var columnWide bool var ledgerFileName string ledger.TransactionDateFormat = "2006/01/02" TransactionDateFormat := ledger.TransactionDateFormat flag.StringVar(&ledgerFileName, "f", "", "Ledger file name (*Required).") flag.StringVar(&startString, "b", startDate.Format(TransactionDateFormat), "Begin date of transaction processing.") flag.StringVar(&endString, "e", endDate.Format(TransactionDateFormat), "End date of transaction processing.") flag.BoolVar(&showEmptyAccounts, "empty", false, "Show empty (zero balance) accounts.") flag.IntVar(&transactionDepth, "depth", -1, "Depth of transaction output (balance).") flag.IntVar(&columnWidth, "columns", 80, "Set a column width for output.") flag.BoolVar(&columnWide, "wide", false, "Wide output (same as --columns=132).") flag.Parse() if columnWidth == 80 && columnWide { columnWidth = 132 } if len(ledgerFileName) == 0 { flag.Usage() return } parsedStartDate, tstartErr := time.Parse(TransactionDateFormat, startString) parsedEndDate, tendErr := time.Parse(TransactionDateFormat, endString) if tstartErr != nil || tendErr != nil { fmt.Println("Unable to parse start or end date string argument.") fmt.Println("Expected format: YYYY/MM/dd") return } args := flag.Args() if len(args) == 0 { fmt.Println("Specify a command.") return } ledgerFileReader, err := os.Open(ledgerFileName) if err != nil { fmt.Println(err) return } defer ledgerFileReader.Close() generalLedger, parseError := ledger.ParseLedger(ledgerFileReader) if parseError != nil { fmt.Println(parseError) return } timeStartIndex, timeEndIndex := 0, 0 for idx := 0; idx < len(generalLedger); idx++ { if generalLedger[idx].Date.After(parsedStartDate) { timeStartIndex = idx break } } for idx := len(generalLedger) - 1; idx >= 0; idx-- { if generalLedger[idx].Date.Before(parsedEndDate) { timeEndIndex = idx break } } generalLedger = generalLedger[timeStartIndex : timeEndIndex+1] containsFilterArray := args[1:] switch strings.ToLower(args[0]) { case "balance", "bal": PrintBalances(ledger.GetBalances(generalLedger, containsFilterArray), showEmptyAccounts, transactionDepth, columnWidth) case "print": PrintLedger(generalLedger, columnWidth) case "register", "reg": PrintRegister(generalLedger, containsFilterArray, columnWidth) case "stats": PrintStats(generalLedger) } }
func reportHandler(w http.ResponseWriter, r *http.Request, params martini.Params) { reportName := params["reportName"] trans, terr := getTransactions() if terr != nil { http.Error(w, terr.Error(), 500) return } var rConf reportConfig for _, reportConf := range reportConfigData.Reports { if reportConf.Name == reportName { rConf = reportConf } } rStart, rEnd, rPeriod := getRangeAndPeriod(rConf.DateRange, rConf.DateFreq) trans = ledger.TransactionsInDateRange(trans, rStart, rEnd) var rtrans []*ledger.Transaction for _, tran := range trans { include := true for _, accChange := range tran.AccountChanges { for _, excludeName := range rConf.ExcludeAccountTrans { if strings.Contains(accChange.Name, excludeName) { include = false } } } if include { rtrans = append(rtrans, tran) } } balances := ledger.GetBalances(rtrans, []string{}) var initialAccounts []*ledger.Account for _, confAccount := range rConf.Accounts { initialAccounts = append(initialAccounts, getAccounts(confAccount, balances)...) } initialAccounts = append(initialAccounts, calcBalances(rConf.CalculatedAccounts, balances)...) var reportSummaryAccounts []*ledger.Account for _, account := range initialAccounts { include := true for _, excludeName := range rConf.ExcludeAccountsSummary { if strings.Contains(account.Name, excludeName) { include = false } } if include { reportSummaryAccounts = append(reportSummaryAccounts, account) } } // Filter report to only show transactions that are for the accounts in the summary of the report var vtrans []*ledger.Transaction for _, trans := range rtrans { include := false for _, accChange := range trans.AccountChanges { for _, account := range reportSummaryAccounts { if strings.Contains(accChange.Name, account.Name) { include = true } } } if include { vtrans = append(vtrans, trans) } } colorPalette, cerr := colorful.HappyPalette(len(reportSummaryAccounts)) if cerr != nil { http.Error(w, cerr.Error(), 500) return } colorBlack := colorful.Color{R: 1, G: 1, B: 1} switch rConf.Chart { case "pie", "polar", "doughnut": type pieAccount struct { Name string Balance *big.Rat Color string Highlight string } var values []pieAccount type pieColor struct { Color string Highlight string } for colorIdx, account := range reportSummaryAccounts { accName := account.Name value := big.NewRat(1, 1).Set(account.Balance) values = append(values, pieAccount{Name: accName, Balance: value, Color: colorPalette[colorIdx].Hex(), Highlight: colorPalette[colorIdx].BlendRgb(colorBlack, 0.1).Hex()}) } type piePageData struct { pageData ReportName string RangeStart, RangeEnd time.Time ChartType string ChartAccounts []pieAccount } var pData piePageData pData.Reports = reportConfigData.Reports pData.Transactions = vtrans pData.ChartAccounts = values pData.RangeStart = rStart pData.RangeEnd = rEnd pData.ReportName = reportName switch rConf.Chart { case "pie": pData.ChartType = "Pie" case "polar": pData.ChartType = "Polar" case "doughnut": pData.ChartType = "Doughnut" } funcMap := template.FuncMap{ "abbrev": abbrev, } t, err := parseAssetsWithFunc(funcMap, "templates/template.piechart.html", "templates/template.nav.html") if err != nil { http.Error(w, err.Error(), 500) return } err = t.Execute(w, pData) if err != nil { http.Error(w, err.Error(), 500) } case "line", "radar", "bar", "stackedbar": type lineData struct { AccountName string RGBColor string Values []*big.Rat } type linePageData struct { pageData ReportName string RangeStart, RangeEnd time.Time ChartType string Labels []string DataSets []lineData } var lData linePageData lData.Reports = reportConfigData.Reports lData.ReportName = reportName for colorIdx, repAccount := range reportSummaryAccounts { r, g, b := colorPalette[colorIdx].RGB255() lData.DataSets = append(lData.DataSets, lineData{AccountName: repAccount.Name, RGBColor: fmt.Sprintf("%d, %d, %d", r, g, b)}) } var rType ledger.RangeType switch rConf.Chart { case "line": rType = ledger.RangeSnapshot lData.ChartType = "Line" case "radar": rType = ledger.RangePartition lData.ChartType = "Radar" case "bar": rType = ledger.RangePartition lData.ChartType = "Bar" case "stackedbar": rType = ledger.RangePartition lData.ChartType = "StackedBar" } rangeBalances := ledger.BalancesByPeriod(rtrans, rPeriod, rType) for _, rb := range rangeBalances { if lData.RangeStart.IsZero() { lData.RangeStart = rb.Start } lData.RangeEnd = rb.End lData.Labels = append(lData.Labels, rb.End.Format("2006-01-02")) accVals := make(map[string]*big.Rat) for _, confAccount := range rConf.Accounts { for _, freqAccount := range getAccounts(confAccount, rb.Balances) { accVals[freqAccount.Name] = big.NewRat(1, 1).Abs(freqAccount.Balance) } } for _, calcAccount := range calcBalances(rConf.CalculatedAccounts, rb.Balances) { accVals[calcAccount.Name] = calcAccount.Balance } for dIdx := range lData.DataSets { aval, afound := accVals[lData.DataSets[dIdx].AccountName] if !afound || aval == nil { aval = big.NewRat(0, 1) } lData.DataSets[dIdx].Values = append(lData.DataSets[dIdx].Values, aval) } } // Radar chart flips everything. Dates are each dataset and the accounts become the labels if rConf.Chart == "radar" { dates := lData.Labels dateAccountMap := make(map[string]*big.Rat) var accNames []string for dsIdx := range lData.DataSets { for dIdx := range dates { dateAccountMap[dates[dIdx]+","+lData.DataSets[dsIdx].AccountName] = lData.DataSets[dsIdx].Values[dIdx] } accNames = append(accNames, lData.DataSets[dsIdx].AccountName) } lData.DataSets = []lineData{} radarcolorPalette, rcerr := colorful.HappyPalette(len(dates)) if rcerr != nil { http.Error(w, cerr.Error(), 500) return } for colorIdx, date := range dates { r, g, b := radarcolorPalette[colorIdx].RGB255() var vals []*big.Rat for _, repAccount := range reportSummaryAccounts { vals = append(vals, dateAccountMap[date+","+repAccount.Name]) } lData.DataSets = append(lData.DataSets, lineData{AccountName: date, RGBColor: fmt.Sprintf("%d, %d, %d", r, g, b), Values: vals}) } lData.Labels = accNames } lData.Transactions = vtrans funcMap := template.FuncMap{ "abbrev": abbrev, "lastaccount": lastaccount, } t, err := parseAssetsWithFunc(funcMap, "templates/template.barlinechart.html", "templates/template.nav.html") if err != nil { http.Error(w, err.Error(), 500) return } err = t.Execute(w, lData) if err != nil { http.Error(w, err.Error(), 500) } default: fmt.Fprint(w, "Unsupported chart type.") } }
func main() { var ledgerFileName string var accountSubstring, csvFileName, csvDateFormat string var negateAmount bool var fieldDelimiter string flag.BoolVar(&negateAmount, "neg", false, "Negate amount column value.") flag.StringVar(&ledgerFileName, "f", "", "Ledger file name (*Required).") flag.StringVar(&csvDateFormat, "date-format", "01/02/2006", "Date format.") flag.StringVar(&fieldDelimiter, "delimiter", ",", "Field delimiter.") flag.Parse() args := flag.Args() if len(args) != 2 { usage() } else { accountSubstring = args[0] csvFileName = args[1] } csvFileReader, err := os.Open(csvFileName) if err != nil { fmt.Println("CSV: ", err) return } defer csvFileReader.Close() ledgerFileReader, err := os.Open(ledgerFileName) if err != nil { fmt.Println("Ledger: ", err) return } defer ledgerFileReader.Close() generalLedger, parseError := ledger.ParseLedger(ledgerFileReader) if parseError != nil { fmt.Println(parseError) return } var matchingAccount string matchingAccounts := ledger.GetBalances(generalLedger, []string{accountSubstring}) if len(matchingAccounts) < 1 { fmt.Println("Unable to find matching account.") return } else { matchingAccount = matchingAccounts[len(matchingAccounts)-1].Name } allAccounts := ledger.GetBalances(generalLedger, []string{}) csvReader := csv.NewReader(csvFileReader) csvReader.Comma, _ = utf8.DecodeRuneInString(fieldDelimiter) csvRecords, _ := csvReader.ReadAll() classes := make([]bayesian.Class, len(allAccounts)) for i, bal := range allAccounts { classes[i] = bayesian.Class(bal.Name) } classifier := bayesian.NewClassifier(classes...) for _, tran := range generalLedger { payeeWords := strings.Split(tran.Payee, " ") for _, accChange := range tran.AccountChanges { if strings.Contains(accChange.Name, "Expense") { classifier.Learn(payeeWords, bayesian.Class(accChange.Name)) } } } // Find columns from header var dateColumn, payeeColumn, amountColumn int dateColumn, payeeColumn, amountColumn = -1, -1, -1 for fieldIndex, fieldName := range csvRecords[0] { fieldName = strings.ToLower(fieldName) if strings.Contains(fieldName, "date") { dateColumn = fieldIndex } else if strings.Contains(fieldName, "description") { payeeColumn = fieldIndex } else if strings.Contains(fieldName, "payee") { payeeColumn = fieldIndex } else if strings.Contains(fieldName, "amount") { amountColumn = fieldIndex } else if strings.Contains(fieldName, "expense") { amountColumn = fieldIndex } } if dateColumn < 0 || payeeColumn < 0 || amountColumn < 0 { fmt.Println("Unable to find columns required from header field names.") return } expenseAccount := ledger.Account{Name: "unknown:unknown", Balance: new(big.Rat)} csvAccount := ledger.Account{Name: matchingAccount, Balance: new(big.Rat)} for _, record := range csvRecords[1:] { inputPayeeWords := strings.Split(record[payeeColumn], " ") csvDate, _ := time.Parse(csvDateFormat, record[dateColumn]) if !existingTransaction(generalLedger, csvDate, inputPayeeWords[0]) { // Classify into expense account _, likely, _ := classifier.LogScores(inputPayeeWords) if likely >= 0 { expenseAccount.Name = string(classifier.Classes[likely]) } // Negate amount if required expenseAccount.Balance.SetString(record[amountColumn]) if negateAmount { expenseAccount.Balance.Neg(expenseAccount.Balance) } // Csv amount is the negative of the expense amount csvAccount.Balance.Neg(expenseAccount.Balance) // Create valid transaction for print in ledger format trans := &ledger.Transaction{Date: csvDate, Payee: record[payeeColumn]} trans.AccountChanges = []ledger.Account{csvAccount, expenseAccount} PrintTransaction(trans, 80) } } }
func ReportHandler(w http.ResponseWriter, r *http.Request, params martini.Params) { reportName := params["reportName"] trans, terr := getTransactions() if terr != nil { http.Error(w, terr.Error(), 500) return } var rConf reportConfig for _, reportConf := range reportConfigData.Reports { if reportConf.Name == reportName { rConf = reportConf } } rStart, rEnd, rPeriod := getRangeAndPeriod(rConf.DateRange, rConf.DateFreq) trans = ledger.TransactionsInDateRange(trans, rStart, rEnd) switch rConf.Chart { case "pie": accountName := rConf.Accounts[0] balances := ledger.GetBalances(trans, []string{accountName}) skipCount := 0 for _, account := range balances { if !strings.HasPrefix(account.Name, accountName) { skipCount++ } if account.Name == accountName { skipCount++ } } accStartLen := len(accountName) type pieAccount struct { Name string Balance float64 Color string Highlight string } values := make([]pieAccount, 0) type pieColor struct { Color string Highlight string } colorlist := []pieColor{{"#F7464A", "#FF5A5E"}, {"#46BFBD", "#5AD3D1"}, {"#FDB45C", "#FFC870"}, {"#B48EAD", "#C69CBE"}, {"#949FB1", "#A8B3C5"}, {"#4D5360", "#616774"}, {"#23A1A3", "#34B3b5"}, {"#bf9005", "#D1A216"}, {"#1742d1", "#2954e2"}, {"#E228BA", "#E24FC2"}, {"#A52A2A", "#B73C3C"}, {"#3EB73C", "#4CBA4A"}, {"#A014CE", "#AB49CC"}, {"#F9A200", "#F9B12A"}, {"#075400", "#4B7C47"}, } colorIdx := 0 for _, account := range balances[skipCount:] { accName := account.Name[accStartLen+1:] value, _ := account.Balance.Float64() include := true for _, excludeName := range rConf.Exclude { if strings.Contains(accName, excludeName) { include = false } } if include && !strings.Contains(accName, ":") { values = append(values, pieAccount{Name: accName, Balance: value, Color: colorlist[colorIdx].Color, Highlight: colorlist[colorIdx].Highlight}) colorIdx++ } } type piePageData struct { pageData ReportName string RangeStart, RangeEnd time.Time ChartAccounts []pieAccount } var pData piePageData pData.Reports = reportConfigData.Reports pData.Transactions = trans pData.ChartAccounts = values pData.RangeStart = rStart pData.RangeEnd = rEnd pData.ReportName = reportName t, err := parseAssets("templates/template.piechart.html", "templates/template.nav.html") if err != nil { http.Error(w, err.Error(), 500) return } err = t.Execute(w, pData) if err != nil { http.Error(w, err.Error(), 500) } case "line", "bar": colorlist := []string{"220,220,220", "151,187,205", "70, 191, 189", "191, 71, 73", "191, 71, 133", "71, 191, 129", "165,42,42"} type lineData struct { AccountName string RGBColor string Values []float64 } type linePageData struct { pageData ReportName string RangeStart, RangeEnd time.Time ChartType string Labels []string DataSets []lineData } var lData linePageData lData.Reports = reportConfigData.Reports lData.Transactions = trans lData.ReportName = reportName colorIdx := 0 for _, freqAccountName := range rConf.Accounts { lData.DataSets = append(lData.DataSets, lineData{AccountName: freqAccountName, RGBColor: colorlist[colorIdx]}) colorIdx++ } var rType ledger.RangeType switch rConf.Chart { case "line": rType = ledger.RangeSnapshot lData.ChartType = "Line" case "bar": rType = ledger.RangePartition lData.ChartType = "Bar" } rangeBalances := ledger.BalancesByPeriod(trans, rPeriod, rType) for _, rb := range rangeBalances { if lData.RangeStart.IsZero() { lData.RangeStart = rb.Start } lData.RangeEnd = rb.End lData.Labels = append(lData.Labels, rb.End.Format("2006-01-02")) accVals := make(map[string]float64) for _, freqAccountName := range rConf.Accounts { accVals[freqAccountName] = 0 } for _, freqAccountName := range rConf.Accounts { for _, bal := range rb.Balances { if bal.Name == freqAccountName { fval, _ := bal.Balance.Float64() fval = math.Abs(fval) accVals[freqAccountName] = fval } } } for dIdx, _ := range lData.DataSets { lData.DataSets[dIdx].Values = append(lData.DataSets[dIdx].Values, accVals[lData.DataSets[dIdx].AccountName]) } } t, err := parseAssets("templates/template.barlinechart.html", "templates/template.nav.html") if err != nil { http.Error(w, err.Error(), 500) return } err = t.Execute(w, lData) if err != nil { http.Error(w, err.Error(), 500) } default: fmt.Fprint(w, "Unsupported chart type.") } }