beancount-gs/service/stats.go

499 lines
15 KiB
Go
Raw Normal View History

2021-11-19 09:54:02 +00:00
package service
import (
2021-11-26 09:12:07 +00:00
"encoding/json"
2021-11-22 14:50:10 +00:00
"fmt"
2021-12-22 15:13:37 +00:00
"sort"
"strings"
"time"
2021-11-19 09:54:02 +00:00
"github.com/beancount-gs/script"
"github.com/gin-gonic/gin"
2021-11-26 09:12:07 +00:00
"github.com/shopspring/decimal"
2021-11-19 09:54:02 +00:00
)
2021-11-22 08:47:49 +00:00
type YearMonth struct {
Year string `bql:"distinct year(date)" json:"year"`
Month string `bql:"month(date)" json:"month"`
}
2021-11-19 09:54:02 +00:00
2021-11-22 08:47:49 +00:00
func MonthsList(c *gin.Context) {
2021-11-21 14:37:13 +00:00
ledgerConfig := script.GetLedgerConfigFromContext(c)
2022-09-03 15:21:25 +00:00
// 添加排序
queryParams := script.GetQueryParams(c)
queryParams.OrderBy = "year, month desc"
2021-11-22 08:47:49 +00:00
yearMonthList := make([]YearMonth, 0)
2022-09-03 15:21:25 +00:00
err := script.BQLQueryList(ledgerConfig, &queryParams, &yearMonthList)
2021-11-19 09:54:02 +00:00
if err != nil {
2021-11-22 08:47:49 +00:00
InternalError(c, err.Error())
2021-11-19 09:54:02 +00:00
return
}
2021-11-22 08:47:49 +00:00
months := make([]string, 0)
for _, yearMonth := range yearMonthList {
months = append(months, yearMonth.Year+"-"+yearMonth.Month)
2021-11-21 14:37:13 +00:00
}
OK(c, months)
2021-11-19 09:54:02 +00:00
}
2021-11-22 14:50:10 +00:00
2021-11-24 15:45:45 +00:00
type StatsResult struct {
Key string
Value string
2021-11-22 14:50:10 +00:00
}
func StatsTotal(c *gin.Context) {
ledgerConfig := script.GetLedgerConfigFromContext(c)
queryParams := script.GetQueryParams(c)
selectBql := fmt.Sprintf("SELECT '\\', root(account, 1), '\\', sum(convert(value(position), '%s')), '\\'", ledgerConfig.OperatingCurrency)
2021-11-24 15:45:45 +00:00
accountTypeTotalList := make([]StatsResult, 0)
2021-11-22 14:50:10 +00:00
err := script.BQLQueryListByCustomSelect(ledgerConfig, selectBql, &queryParams, &accountTypeTotalList)
if err != nil {
InternalError(c, err.Error())
return
}
2022-07-03 15:26:48 +00:00
result := make(map[string]string)
2021-11-22 14:50:10 +00:00
for _, total := range accountTypeTotalList {
2021-11-24 15:45:45 +00:00
fields := strings.Fields(total.Value)
2021-11-22 14:50:10 +00:00
if len(fields) > 1 {
2021-11-24 15:45:45 +00:00
result[total.Key] = fields[0]
2021-11-22 14:50:10 +00:00
}
}
OK(c, result)
}
2021-11-24 15:45:45 +00:00
type StatsQuery struct {
Prefix string `form:"prefix" binding:"required"`
Year int `form:"year"`
Month int `form:"month"`
Level int `form:"level"`
Type string `form:"type"`
}
2021-11-26 09:12:07 +00:00
type AccountPercentQueryResult struct {
Account string
Position string
}
2021-11-24 15:45:45 +00:00
type AccountPercentResult struct {
2021-11-26 09:12:07 +00:00
Account string `json:"account"`
Amount json.Number `json:"amount"`
OperatingCurrency string `json:"operatingCurrency"`
2021-11-24 15:45:45 +00:00
}
func StatsAccountPercent(c *gin.Context) {
ledgerConfig := script.GetLedgerConfigFromContext(c)
var statsQuery StatsQuery
if err := c.ShouldBindQuery(&statsQuery); err != nil {
BadRequest(c, err.Error())
return
}
queryParams := script.QueryParams{
AccountLike: statsQuery.Prefix,
Year: statsQuery.Year,
Month: statsQuery.Month,
Where: true,
}
var bql string
if statsQuery.Level != 0 {
prefixNodeLen := len(strings.Split(strings.Trim(statsQuery.Prefix, ":"), ":"))
bql = fmt.Sprintf("SELECT '\\', root(account, %d) as subAccount, '\\', sum(convert(value(position), '%s')), '\\'", statsQuery.Level+prefixNodeLen, ledgerConfig.OperatingCurrency)
} else {
bql = fmt.Sprintf("SELECT '\\', account, '\\', sum(convert(value(position), '%s')), '\\'", ledgerConfig.OperatingCurrency)
}
2021-11-26 09:12:07 +00:00
statsQueryResultList := make([]AccountPercentQueryResult, 0)
err := script.BQLQueryListByCustomSelect(ledgerConfig, bql, &queryParams, &statsQueryResultList)
2021-11-24 15:45:45 +00:00
if err != nil {
InternalError(c, err.Error())
return
}
2021-11-26 09:12:07 +00:00
result := make([]AccountPercentResult, 0)
for _, queryRes := range statsQueryResultList {
if queryRes.Position != "" {
fields := strings.Fields(queryRes.Position)
result = append(result, AccountPercentResult{Account: queryRes.Account, Amount: json.Number(fields[0]), OperatingCurrency: fields[1]})
}
2021-11-24 15:45:45 +00:00
}
2021-11-26 09:12:07 +00:00
OK(c, result)
2021-11-24 15:45:45 +00:00
}
type AccountTrendResult struct {
2021-11-26 09:12:07 +00:00
Date string `json:"date"`
Amount json.Number `json:"amount"`
OperatingCurrency string `json:"operatingCurrency"`
2021-11-24 15:45:45 +00:00
}
func StatsAccountTrend(c *gin.Context) {
ledgerConfig := script.GetLedgerConfigFromContext(c)
var statsQuery StatsQuery
if err := c.ShouldBindQuery(&statsQuery); err != nil {
BadRequest(c, err.Error())
return
}
queryParams := script.QueryParams{
AccountLike: statsQuery.Prefix,
Year: statsQuery.Year,
Month: statsQuery.Month,
Where: true,
}
var bql string
2022-07-03 15:26:48 +00:00
switch {
case statsQuery.Type == "day":
2021-11-24 15:45:45 +00:00
bql = fmt.Sprintf("SELECT '\\', date, '\\', sum(convert(value(position), '%s')), '\\'", ledgerConfig.OperatingCurrency)
2022-07-03 15:26:48 +00:00
case statsQuery.Type == "month":
bql = fmt.Sprintf("SELECT '\\', year, '-', month, '\\', sum(convert(value(position), '%s')), '\\'", ledgerConfig.OperatingCurrency)
2022-07-03 15:26:48 +00:00
case statsQuery.Type == "year":
bql = fmt.Sprintf("SELECT '\\', year, '\\', sum(convert(value(position), '%s')), '\\'", ledgerConfig.OperatingCurrency)
2022-07-03 15:26:48 +00:00
case statsQuery.Type == "sum":
2021-11-24 15:45:45 +00:00
bql = fmt.Sprintf("SELECT '\\', date, '\\', convert(balance, '%s'), '\\'", ledgerConfig.OperatingCurrency)
2022-07-03 15:26:48 +00:00
default:
2021-11-24 15:45:45 +00:00
OK(c, new([]string))
return
}
statsResultList := make([]StatsResult, 0)
err := script.BQLQueryListByCustomSelect(ledgerConfig, bql, &queryParams, &statsResultList)
if err != nil {
InternalError(c, err.Error())
return
}
result := make([]AccountTrendResult, 0)
for _, stats := range statsResultList {
commodities := strings.Split(stats.Value, ",")
// 多币种的处理方式:例如 75799.78 USD, 18500.00 IRAUSD, 176 VACHR
// 选择账本默认ledgerConfig.OperatingCurrency币种的值
var selectedCommodity = commodities[0]
for _, commodity := range commodities {
if strings.Contains(commodity, " "+ledgerConfig.OperatingCurrency) {
selectedCommodity = commodity
break
}
}
fields := strings.Fields(selectedCommodity)
2021-11-26 09:12:07 +00:00
amount, _ := decimal.NewFromString(fields[0])
var date = stats.Key
// 月格式化日期
if statsQuery.Type == "month" {
yearMonth := strings.Split(date, "-")
date = fmt.Sprintf("%s-%s", strings.Trim(yearMonth[0], " "), strings.Trim(yearMonth[1], " "))
}
result = append(result, AccountTrendResult{Date: date, Amount: json.Number(amount.Round(2).String()), OperatingCurrency: fields[1]})
2021-11-24 15:45:45 +00:00
}
OK(c, result)
}
2021-11-25 07:41:28 +00:00
2021-12-02 14:48:45 +00:00
type AccountBalanceBQLResult struct {
Year string `bql:"year" json:"year"`
Month string `bql:"month" json:"month"`
Day string `bql:"day" json:"day"`
Balance string `bql:"balance" json:"balance"`
}
type AccountBalanceResult struct {
Date string `json:"date"`
Amount json.Number `json:"amount"`
OperatingCurrency string `json:"operatingCurrency"`
}
func StatsAccountBalance(c *gin.Context) {
ledgerConfig := script.GetLedgerConfigFromContext(c)
var statsQuery StatsQuery
if err := c.ShouldBindQuery(&statsQuery); err != nil {
BadRequest(c, err.Error())
return
}
queryParams := script.QueryParams{
AccountLike: statsQuery.Prefix,
Where: true,
}
balResultList := make([]AccountBalanceBQLResult, 0)
bql := fmt.Sprintf("select '\\', year, '\\', month, '\\', day, '\\', last(convert(balance, '%s')), '\\'", ledgerConfig.OperatingCurrency)
err := script.BQLQueryListByCustomSelect(ledgerConfig, bql, &queryParams, &balResultList)
if err != nil {
InternalError(c, err.Error())
return
}
resultList := make([]AccountBalanceResult, 0)
for _, bqlResult := range balResultList {
if bqlResult.Balance != "" {
fields := strings.Fields(bqlResult.Balance)
amount, _ := decimal.NewFromString(fields[0])
resultList = append(resultList, AccountBalanceResult{
Date: bqlResult.Year + "-" + bqlResult.Month + "-" + bqlResult.Day,
Amount: json.Number(amount.Round(2).String()),
OperatingCurrency: fields[1],
})
}
}
OK(c, resultList)
}
2021-11-25 07:41:28 +00:00
type MonthTotalBQLResult struct {
Year int
Month int
Value string
}
type MonthTotal struct {
2021-11-26 09:12:07 +00:00
Type string `json:"type"`
Month string `json:"month"`
Amount json.Number `json:"amount"`
OperatingCurrency string `json:"operatingCurrency"`
2021-11-25 07:41:28 +00:00
}
2021-12-22 15:13:37 +00:00
type MonthTotalSort []MonthTotal
func (s MonthTotalSort) Len() int {
return len(s)
}
func (s MonthTotalSort) Swap(i, j int) {
s[i], s[j] = s[j], s[i]
}
func (s MonthTotalSort) Less(i, j int) bool {
iYearMonth, _ := time.Parse("2006-1", s[i].Month)
jYearMonth, _ := time.Parse("2006-1", s[j].Month)
return iYearMonth.Before(jYearMonth)
}
2021-11-25 07:41:28 +00:00
func StatsMonthTotal(c *gin.Context) {
ledgerConfig := script.GetLedgerConfigFromContext(c)
monthSet := make(map[string]bool)
queryParams := script.QueryParams{
AccountLike: "Income",
Where: true,
OrderBy: "year, month",
}
// 按月查询收入
queryIncomeBql := fmt.Sprintf("select '\\', year, '\\', month, '\\', neg(sum(convert(value(position), '%s'))), '\\'", ledgerConfig.OperatingCurrency)
monthIncomeTotalResultList := make([]MonthTotalBQLResult, 0)
err := script.BQLQueryListByCustomSelect(ledgerConfig, queryIncomeBql, &queryParams, &monthIncomeTotalResultList)
if err != nil {
InternalError(c, err.Error())
return
}
monthIncomeMap := make(map[string]MonthTotalBQLResult)
for _, income := range monthIncomeTotalResultList {
month := fmt.Sprintf("%d-%d", income.Year, income.Month)
monthSet[month] = true
monthIncomeMap[month] = income
}
// 按月查询支出
queryParams.AccountLike = "Expenses"
queryExpensesBql := fmt.Sprintf("select '\\', year, '\\', month, '\\', sum(convert(value(position), '%s')), '\\'", ledgerConfig.OperatingCurrency)
monthExpensesTotalResultList := make([]MonthTotalBQLResult, 0)
err = script.BQLQueryListByCustomSelect(ledgerConfig, queryExpensesBql, &queryParams, &monthExpensesTotalResultList)
if err != nil {
InternalError(c, err.Error())
return
}
monthExpensesMap := make(map[string]MonthTotalBQLResult)
for _, expenses := range monthExpensesTotalResultList {
month := fmt.Sprintf("%d-%d", expenses.Year, expenses.Month)
monthSet[month] = true
monthExpensesMap[month] = expenses
}
monthTotalResult := make([]MonthTotal, 0)
// 合并结果
var monthIncome, monthExpenses MonthTotal
2021-11-26 09:12:07 +00:00
var monthIncomeAmount, monthExpensesAmount decimal.Decimal
2021-11-25 07:41:28 +00:00
for month := range monthSet {
if monthIncomeMap[month].Value != "" {
fields := strings.Fields(monthIncomeMap[month].Value)
2021-11-26 09:12:07 +00:00
amount, _ := decimal.NewFromString(fields[0])
monthIncomeAmount = amount
monthIncome = MonthTotal{Type: "收入", Month: month, Amount: json.Number(amount.Round(2).String()), OperatingCurrency: fields[1]}
2021-11-25 07:41:28 +00:00
} else {
2021-11-26 09:12:07 +00:00
monthIncome = MonthTotal{Type: "收入", Month: month, Amount: "0", OperatingCurrency: ledgerConfig.OperatingCurrency}
2021-11-25 07:41:28 +00:00
}
monthTotalResult = append(monthTotalResult, monthIncome)
if monthExpensesMap[month].Value != "" {
fields := strings.Fields(monthExpensesMap[month].Value)
2021-11-26 09:12:07 +00:00
amount, _ := decimal.NewFromString(fields[0])
monthExpensesAmount = amount
monthExpenses = MonthTotal{Type: "支出", Month: month, Amount: json.Number(amount.Round(2).String()), OperatingCurrency: fields[1]}
2021-11-25 07:41:28 +00:00
} else {
2021-11-26 09:12:07 +00:00
monthExpenses = MonthTotal{Type: "支出", Month: month, Amount: "0", OperatingCurrency: ledgerConfig.OperatingCurrency}
2021-11-25 07:41:28 +00:00
}
monthTotalResult = append(monthTotalResult, monthExpenses)
2021-11-26 09:12:07 +00:00
monthTotalResult = append(monthTotalResult, MonthTotal{Type: "结余", Month: month, Amount: json.Number(monthIncomeAmount.Sub(monthExpensesAmount).Round(2).String()), OperatingCurrency: ledgerConfig.OperatingCurrency})
2021-11-25 07:41:28 +00:00
}
2021-12-22 15:13:37 +00:00
sort.Sort(MonthTotalSort(monthTotalResult))
2021-11-25 07:41:28 +00:00
OK(c, monthTotalResult)
}
2021-11-26 09:12:07 +00:00
2022-06-05 03:07:03 +00:00
type StatsMonthQuery struct {
Year int `form:"year"`
Month int `form:"month"`
}
type StatsCalendarQueryResult struct {
Date string
Account string
Position string
}
type StatsCalendarResult struct {
Date string `json:"date"`
Account string `json:"account"`
Amount json.Number `json:"amount"`
Currency string `json:"currency"`
CurrencySymbol string `json:"currencySymbol"`
}
func StatsMonthCalendar(c *gin.Context) {
ledgerConfig := script.GetLedgerConfigFromContext(c)
var statsMonthQuery StatsMonthQuery
if err := c.ShouldBindQuery(&statsMonthQuery); err != nil {
BadRequest(c, err.Error())
return
}
queryParams := script.QueryParams{
Year: statsMonthQuery.Year,
Month: statsMonthQuery.Month,
Where: true,
}
bql := fmt.Sprintf("SELECT '\\', date, '\\', root(account, 1), '\\', sum(convert(value(position), '%s')), '\\'", ledgerConfig.OperatingCurrency)
statsCalendarQueryResult := make([]StatsCalendarQueryResult, 0)
err := script.BQLQueryListByCustomSelect(ledgerConfig, bql, &queryParams, &statsCalendarQueryResult)
if err != nil {
InternalError(c, err.Error())
return
}
resultList := make([]StatsCalendarResult, 0)
for _, queryRes := range statsCalendarQueryResult {
if queryRes.Position != "" {
fields := strings.Fields(queryRes.Position)
resultList = append(resultList,
StatsCalendarResult{
Date: queryRes.Date,
Account: queryRes.Account,
Amount: json.Number(fields[0]),
Currency: fields[1],
CurrencySymbol: script.GetCommoditySymbol(fields[1]),
})
}
}
OK(c, resultList)
}
2021-11-26 09:12:07 +00:00
type StatsPayeeQueryResult struct {
Payee string
Count int32
Position string
}
type StatsPayeeResult struct {
Payee string `json:"payee"`
Currency string `json:"operatingCurrency"`
Value json.Number `json:"value"`
}
type StatsPayeeResultSort []StatsPayeeResult
func (s StatsPayeeResultSort) Len() int {
return len(s)
}
func (s StatsPayeeResultSort) Swap(i, j int) {
s[i], s[j] = s[j], s[i]
}
func (s StatsPayeeResultSort) Less(i, j int) bool {
a, _ := s[i].Value.Float64()
b, _ := s[j].Value.Float64()
return a <= b
}
func StatsPayee(c *gin.Context) {
ledgerConfig := script.GetLedgerConfigFromContext(c)
var statsQuery StatsQuery
if err := c.ShouldBindQuery(&statsQuery); err != nil {
BadRequest(c, err.Error())
return
}
queryParams := script.QueryParams{
AccountLike: statsQuery.Prefix,
Year: statsQuery.Year,
Month: statsQuery.Month,
Where: true,
Currency: ledgerConfig.OperatingCurrency,
}
bql := fmt.Sprintf("SELECT '\\', payee, '\\', count(payee), '\\', sum(convert(value(position), '%s')), '\\'", ledgerConfig.OperatingCurrency)
statsPayeeQueryResultList := make([]StatsPayeeQueryResult, 0)
err := script.BQLQueryListByCustomSelect(ledgerConfig, bql, &queryParams, &statsPayeeQueryResultList)
if err != nil {
InternalError(c, err.Error())
return
}
result := make([]StatsPayeeResult, 0)
for _, l := range statsPayeeQueryResultList {
if l.Payee != "" {
payee := StatsPayeeResult{
Payee: l.Payee,
Currency: ledgerConfig.OperatingCurrency,
}
if statsQuery.Type == "cot" {
payee.Value = json.Number(decimal.NewFromInt32(l.Count).String())
} else {
fields := strings.Fields(l.Position)
total, err := decimal.NewFromString(fields[0])
if err != nil {
panic(err)
}
if statsQuery.Type == "avg" {
payee.Value = json.Number(total.Div(decimal.NewFromInt32(l.Count)).Round(2).String())
} else {
payee.Value = json.Number(fields[0])
}
}
result = append(result, payee)
}
}
sort.Sort(StatsPayeeResultSort(result))
OK(c, result)
}
2022-04-12 16:30:38 +00:00
type StatsPricesResult struct {
2022-04-13 16:23:08 +00:00
Date string `json:"date"`
Commodity string `json:"commodity"`
Currency string `json:"operatingCurrency"`
Value string `json:"value"`
2022-04-12 16:30:38 +00:00
}
2022-04-13 16:23:08 +00:00
func StatsCommodityPrice(c *gin.Context) {
2022-04-12 16:30:38 +00:00
ledgerConfig := script.GetLedgerConfigFromContext(c)
output := script.BeanReportAllPrices(ledgerConfig)
script.LogInfo(ledgerConfig.Mail, output)
statsPricesResultList := make([]StatsPricesResult, 0)
2022-06-04 05:31:40 +00:00
lines := strings.Split(output, "\n")
2022-04-12 16:30:38 +00:00
// foreach lines
for _, line := range lines {
if strings.Trim(line, " ") == "" {
continue
}
// split line by " "
words := strings.Fields(line)
statsPricesResultList = append(statsPricesResultList, StatsPricesResult{
2022-04-13 16:23:08 +00:00
Date: words[0],
Commodity: words[2],
Value: words[3],
Currency: words[4],
2022-04-12 16:30:38 +00:00
})
}
OK(c, statsPricesResultList)
}