Example #1
0
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)
	}
}
Example #2
0
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)
	}
}
Example #3
0
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.")
	}
}
Example #4
0
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)
		}
	}
}
Example #5
0
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.")
	}
}