add account and transaction api impl
This commit is contained in:
parent
27accd1fb9
commit
571c3303c8
|
|
@ -3,5 +3,6 @@
|
||||||
"dataPath": "E:\\beancount",
|
"dataPath": "E:\\beancount",
|
||||||
"operatingCurrency": "CNY",
|
"operatingCurrency": "CNY",
|
||||||
"startDate": "1970-01-01",
|
"startDate": "1970-01-01",
|
||||||
"isBak": true
|
"isBak": true,
|
||||||
|
"openingBalances": "Equity:OpeningBalances"
|
||||||
}
|
}
|
||||||
|
|
@ -23,15 +23,16 @@ type Config struct {
|
||||||
OperatingCurrency string `json:"operatingCurrency"`
|
OperatingCurrency string `json:"operatingCurrency"`
|
||||||
StartDate string `json:"startDate"`
|
StartDate string `json:"startDate"`
|
||||||
IsBak bool `json:"isBak"`
|
IsBak bool `json:"isBak"`
|
||||||
|
OpeningBalances string `json:"openingBalances"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type Account struct {
|
type Account struct {
|
||||||
Acc string `json:"account"`
|
Acc string `json:"account"`
|
||||||
StartDate string `json:"startDate"`
|
StartDate string `json:"startDate"`
|
||||||
Commodity string `json:"commodity,omitempty"`
|
Currency string `json:"currency,omitempty"`
|
||||||
PriceAmount string `json:"priceAmount,omitempty"`
|
MarketNumber string `json:"marketNumber,omitempty"`
|
||||||
PriceCommodity string `json:"priceCommodity,omitempty"`
|
MarketCurrency string `json:"marketCurrency,omitempty"`
|
||||||
PriceCommoditySymbol string `json:"priceCommoditySymbol,omitempty"`
|
MarketCurrencySymbol string `json:"marketCurrencySymbol,omitempty"`
|
||||||
EndDate string `json:"endDate,omitempty"`
|
EndDate string `json:"endDate,omitempty"`
|
||||||
Type *AccountType `json:"type,omitempty"`
|
Type *AccountType `json:"type,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
@ -77,10 +78,28 @@ func GetLedgerAccounts(ledgerId string) []Account {
|
||||||
return ledgerAccountsMap[ledgerId]
|
return ledgerAccountsMap[ledgerId]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func GetLedgerAccount(ledgerId string, account string) Account {
|
||||||
|
accounts := ledgerAccountsMap[ledgerId]
|
||||||
|
for _, acc := range accounts {
|
||||||
|
if acc.Acc == account {
|
||||||
|
return acc
|
||||||
|
}
|
||||||
|
}
|
||||||
|
panic("Invalid account")
|
||||||
|
}
|
||||||
|
|
||||||
|
func UpdateLedgerAccounts(ledgerId string, accounts []Account) {
|
||||||
|
ledgerAccountsMap[ledgerId] = accounts
|
||||||
|
}
|
||||||
|
|
||||||
func GetLedgerAccountTypes(ledgerId string) map[string]string {
|
func GetLedgerAccountTypes(ledgerId string) map[string]string {
|
||||||
return ledgerAccountTypesMap[ledgerId]
|
return ledgerAccountTypesMap[ledgerId]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func UpdateLedgerAccountTypes(ledgerId string, accountTypesMap map[string]string) {
|
||||||
|
ledgerAccountTypesMap[ledgerId] = accountTypesMap
|
||||||
|
}
|
||||||
|
|
||||||
func GetAccountType(ledgerId string, acc string) AccountType {
|
func GetAccountType(ledgerId string, acc string) AccountType {
|
||||||
accountTypes := ledgerAccountTypesMap[ledgerId]
|
accountTypes := ledgerAccountTypesMap[ledgerId]
|
||||||
accNodes := strings.Split(acc, ":")
|
accNodes := strings.Split(acc, ":")
|
||||||
|
|
@ -122,6 +141,10 @@ func LoadServerConfig() error {
|
||||||
LogSystemError("Failed unmarshall config file (/config/config.json)")
|
LogSystemError("Failed unmarshall config file (/config/config.json)")
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
// 兼容旧版本数据,设置默认平衡账户
|
||||||
|
if serverConfig.OpeningBalances == "" {
|
||||||
|
serverConfig.OpeningBalances = "Equity:OpeningBalances"
|
||||||
|
}
|
||||||
LogSystemInfo("Success load config file (/config/config.json)")
|
LogSystemInfo("Success load config file (/config/config.json)")
|
||||||
// load white list
|
// load white list
|
||||||
fileContent, err = ReadFile("./config/white_list.json")
|
fileContent, err = ReadFile("./config/white_list.json")
|
||||||
|
|
@ -148,6 +171,15 @@ func LoadLedgerConfigMap() error {
|
||||||
LogSystemError("Failed unmarshal config file (" + path + ")")
|
LogSystemError("Failed unmarshal config file (" + path + ")")
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
// 兼容旧数据,初始化 平衡账户
|
||||||
|
temp := make(map[string]Config)
|
||||||
|
for key, val := range ledgerConfigMap {
|
||||||
|
if val.OpeningBalances == "" {
|
||||||
|
val.OpeningBalances = serverConfig.OpeningBalances
|
||||||
|
}
|
||||||
|
temp[key] = val
|
||||||
|
}
|
||||||
|
ledgerConfigMap = temp
|
||||||
LogSystemInfo("Success load ledger_config file (" + path + ")")
|
LogSystemInfo("Success load ledger_config file (" + path + ")")
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
@ -185,7 +217,7 @@ func LoadLedgerAccountsMap() error {
|
||||||
account := Account{Acc: key, Type: nil}
|
account := Account{Acc: key, Type: nil}
|
||||||
// 货币单位
|
// 货币单位
|
||||||
if len(words) >= 4 {
|
if len(words) >= 4 {
|
||||||
account.Commodity = words[3]
|
account.Currency = words[3]
|
||||||
}
|
}
|
||||||
if words[1] == "open" {
|
if words[1] == "open" {
|
||||||
account.StartDate = words[0]
|
account.StartDate = words[0]
|
||||||
|
|
@ -257,3 +289,8 @@ func GetCommoditySymbol(commodity string) string {
|
||||||
}
|
}
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func GetAccountPrefix(account string) string {
|
||||||
|
nodes := strings.Split(account, ":")
|
||||||
|
return nodes[0]
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -27,15 +27,35 @@ func ReadFile(filePath string) ([]byte, error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func WriteFile(filePath string, content string) error {
|
func WriteFile(filePath string, content string) error {
|
||||||
err := ioutil.WriteFile(filePath, []byte(content), os.ModePerm)
|
content = "\r\n" + content
|
||||||
|
file, err := os.OpenFile(filePath, os.O_CREATE, 0644)
|
||||||
|
if err == nil {
|
||||||
|
_, err = file.WriteString(content)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
LogSystemError("Failed to write file (" + filePath + ")")
|
LogSystemError("Failed to write file (" + filePath + ")")
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
defer file.Close()
|
||||||
LogSystemInfo("Success write file (" + filePath + ")")
|
LogSystemInfo("Success write file (" + filePath + ")")
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func AppendFileInNewLine(filePath string, content string) error {
|
||||||
|
content = "\r\n" + content
|
||||||
|
file, err := os.OpenFile(filePath, os.O_APPEND, 0644)
|
||||||
|
if err == nil {
|
||||||
|
_, err = file.WriteString(content)
|
||||||
|
if err != nil {
|
||||||
|
LogSystemError("Failed to append file (" + filePath + ")")
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
defer file.Close()
|
||||||
|
LogSystemInfo("Success append file (" + filePath + ")")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func CreateFile(filePath string) error {
|
func CreateFile(filePath string) error {
|
||||||
f, err := os.Create(filePath)
|
f, err := os.Create(filePath)
|
||||||
if nil != err {
|
if nil != err {
|
||||||
|
|
|
||||||
|
|
@ -21,3 +21,11 @@ func GetLedgerTransactionsTemplateFilePath(dataPath string) string {
|
||||||
func GetLedgerAccountTypeFilePath(dataPath string) string {
|
func GetLedgerAccountTypeFilePath(dataPath string) string {
|
||||||
return dataPath + "/.beancount-ns/account_type.json"
|
return dataPath + "/.beancount-ns/account_type.json"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func GetLedgerPriceFilePath(dataPath string) string {
|
||||||
|
return dataPath + "/price/prices.bean"
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetLedgerMonthsFilePath(dataPath string) string {
|
||||||
|
return dataPath + "/month/months.bean"
|
||||||
|
}
|
||||||
|
|
|
||||||
12
server.go
12
server.go
|
|
@ -52,6 +52,12 @@ func RegisterRouter(router *gin.Engine) {
|
||||||
authorized.GET("/account/valid", service.QueryValidAccount)
|
authorized.GET("/account/valid", service.QueryValidAccount)
|
||||||
authorized.GET("/account/all", service.QueryAllAccount)
|
authorized.GET("/account/all", service.QueryAllAccount)
|
||||||
authorized.GET("/account/type", service.QueryAccountType)
|
authorized.GET("/account/type", service.QueryAccountType)
|
||||||
|
authorized.POST("/account", service.AddAccount)
|
||||||
|
authorized.POST("/account/type", service.AddAccountType)
|
||||||
|
authorized.POST("/account/close", service.CloseAccount)
|
||||||
|
authorized.POST("/account/icon", service.ChangeAccountIcon)
|
||||||
|
authorized.POST("/account/balance", service.BalanceAccount)
|
||||||
|
authorized.POST("/commodity/price", service.SyncCommodityPrice)
|
||||||
authorized.GET("/stats/months", service.MonthsList)
|
authorized.GET("/stats/months", service.MonthsList)
|
||||||
authorized.GET("/stats/total", service.StatsTotal)
|
authorized.GET("/stats/total", service.StatsTotal)
|
||||||
authorized.GET("/stats/payee", service.StatsPayee)
|
authorized.GET("/stats/payee", service.StatsPayee)
|
||||||
|
|
@ -59,17 +65,13 @@ func RegisterRouter(router *gin.Engine) {
|
||||||
authorized.GET("/stats/account/trend", service.StatsAccountTrend)
|
authorized.GET("/stats/account/trend", service.StatsAccountTrend)
|
||||||
authorized.GET("/stats/month/total", service.StatsMonthTotal)
|
authorized.GET("/stats/month/total", service.StatsMonthTotal)
|
||||||
authorized.GET("/transaction", service.QueryTransactions)
|
authorized.GET("/transaction", service.QueryTransactions)
|
||||||
|
authorized.POST("/transaction", service.AddTransactions)
|
||||||
authorized.GET("/transaction/payee", service.QueryTransactionPayees)
|
authorized.GET("/transaction/payee", service.QueryTransactionPayees)
|
||||||
authorized.GET("/transaction/template", service.QueryTransactionTemplates)
|
authorized.GET("/transaction/template", service.QueryTransactionTemplates)
|
||||||
authorized.GET("/tags", service.QueryTags)
|
authorized.GET("/tags", service.QueryTags)
|
||||||
authorized.GET("/file/dir", service.QueryLedgerSourceFileDir)
|
authorized.GET("/file/dir", service.QueryLedgerSourceFileDir)
|
||||||
authorized.GET("/file/content", service.QueryLedgerSourceFileContent)
|
authorized.GET("/file/content", service.QueryLedgerSourceFileContent)
|
||||||
authorized.POST("/file", service.UpdateLedgerSourceFileContent)
|
authorized.POST("/file", service.UpdateLedgerSourceFileContent)
|
||||||
|
|
||||||
// 兼容旧版本
|
|
||||||
authorized.GET("/entry", service.QueryTransactions)
|
|
||||||
authorized.GET("/payee", service.QueryTransactionPayees)
|
|
||||||
authorized.GET("/stats/month/incomeExpenses", service.StatsMonthTotal)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,16 +1,25 @@
|
||||||
package service
|
package service
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"github.com/beancount-gs/script"
|
"github.com/beancount-gs/script"
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"sort"
|
"sort"
|
||||||
"strings"
|
"strings"
|
||||||
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
func QueryValidAccount(c *gin.Context) {
|
func QueryValidAccount(c *gin.Context) {
|
||||||
ledgerConfig := script.GetLedgerConfigFromContext(c)
|
ledgerConfig := script.GetLedgerConfigFromContext(c)
|
||||||
OK(c, script.GetLedgerAccounts(ledgerConfig.Id))
|
allAccounts := script.GetLedgerAccounts(ledgerConfig.Id)
|
||||||
|
result := make([]script.Account, 0)
|
||||||
|
for _, account := range allAccounts {
|
||||||
|
if account.EndDate == "" {
|
||||||
|
result = append(result, account)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
OK(c, result)
|
||||||
}
|
}
|
||||||
|
|
||||||
type accountPosition struct {
|
type accountPosition struct {
|
||||||
|
|
@ -37,21 +46,22 @@ func QueryAllAccount(c *gin.Context) {
|
||||||
accounts := script.GetLedgerAccounts(ledgerConfig.Id)
|
accounts := script.GetLedgerAccounts(ledgerConfig.Id)
|
||||||
result := make([]script.Account, 0, len(accounts))
|
result := make([]script.Account, 0, len(accounts))
|
||||||
for i := 0; i < len(accounts); i++ {
|
for i := 0; i < len(accounts); i++ {
|
||||||
|
account := accounts[i]
|
||||||
// 过滤已结束的账户
|
// 过滤已结束的账户
|
||||||
if accounts[i].EndDate != "" {
|
if account.EndDate != "" {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
key := accounts[i].Acc
|
key := account.Acc
|
||||||
typ := script.GetAccountType(ledgerConfig.Id, key)
|
typ := script.GetAccountType(ledgerConfig.Id, key)
|
||||||
accounts[i].Type = &typ
|
account.Type = &typ
|
||||||
position := strings.Trim(accountPositionMap[key].Position, " ")
|
position := strings.Trim(accountPositionMap[key].Position, " ")
|
||||||
if position != "" {
|
if position != "" {
|
||||||
fields := strings.Fields(position)
|
fields := strings.Fields(position)
|
||||||
accounts[i].PriceAmount = fields[0]
|
account.MarketNumber = fields[0]
|
||||||
accounts[i].PriceCommodity = fields[1]
|
account.MarketCurrency = fields[1]
|
||||||
accounts[i].PriceCommoditySymbol = script.GetCommoditySymbol(fields[1])
|
account.MarketCurrencySymbol = script.GetCommoditySymbol(fields[1])
|
||||||
}
|
}
|
||||||
result = append(result, accounts[i])
|
result = append(result, account)
|
||||||
}
|
}
|
||||||
OK(c, result)
|
OK(c, result)
|
||||||
}
|
}
|
||||||
|
|
@ -67,3 +77,165 @@ func QueryAccountType(c *gin.Context) {
|
||||||
sort.Sort(script.AccountTypeSort(result))
|
sort.Sort(script.AccountTypeSort(result))
|
||||||
OK(c, result)
|
OK(c, result)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type AddAccountForm struct {
|
||||||
|
Date string `form:"date" binding:"required"`
|
||||||
|
Account string `form:"account" binding:"required"`
|
||||||
|
// 账户计量单位可以为空
|
||||||
|
Currency string `form:"currency"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func AddAccount(c *gin.Context) {
|
||||||
|
var accountForm AddAccountForm
|
||||||
|
if err := c.ShouldBindJSON(&accountForm); err != nil {
|
||||||
|
BadRequest(c, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
ledgerConfig := script.GetLedgerConfigFromContext(c)
|
||||||
|
// 判断账户是否已存在
|
||||||
|
accounts := script.GetLedgerAccounts(ledgerConfig.Id)
|
||||||
|
for _, acc := range accounts {
|
||||||
|
if acc.Acc == accountForm.Account {
|
||||||
|
DuplicateAccount(c)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
line := fmt.Sprintf("%s open %s %s", accountForm.Date, accountForm.Account, accountForm.Currency)
|
||||||
|
if accountForm.Currency != "" && accountForm.Currency != ledgerConfig.OperatingCurrency {
|
||||||
|
line += " \"FIFO\""
|
||||||
|
}
|
||||||
|
// 写入文件
|
||||||
|
filePath := ledgerConfig.DataPath + "/account/" + script.GetAccountPrefix(accountForm.Account) + ".bean"
|
||||||
|
err := script.AppendFileInNewLine(filePath, line)
|
||||||
|
if err != nil {
|
||||||
|
InternalError(c, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// 更新缓存
|
||||||
|
typ := script.GetAccountType(ledgerConfig.Id, accountForm.Account)
|
||||||
|
account := script.Account{Acc: accountForm.Account, StartDate: accountForm.Date, Currency: accountForm.Currency, Type: &typ}
|
||||||
|
accounts = append(accounts, account)
|
||||||
|
script.UpdateLedgerAccounts(ledgerConfig.Id, accounts)
|
||||||
|
OK(c, account)
|
||||||
|
}
|
||||||
|
|
||||||
|
type AddAccountTypeForm struct {
|
||||||
|
Type string `form:"type" binding:"required"`
|
||||||
|
Name string `form:"name" binding:"required"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func AddAccountType(c *gin.Context) {
|
||||||
|
var addAccountTypeForm AddAccountTypeForm
|
||||||
|
if err := c.ShouldBindJSON(&addAccountTypeForm); err != nil {
|
||||||
|
BadRequest(c, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
ledgerConfig := script.GetLedgerConfigFromContext(c)
|
||||||
|
accountTypesMap := script.GetLedgerAccountTypes(ledgerConfig.Id)
|
||||||
|
typ := addAccountTypeForm.Type
|
||||||
|
accountTypesMap[typ] = addAccountTypeForm.Name
|
||||||
|
// 更新文件
|
||||||
|
pathFile := script.GetLedgerAccountTypeFilePath(ledgerConfig.DataPath)
|
||||||
|
bytes, err := json.Marshal(accountTypesMap)
|
||||||
|
if err != nil {
|
||||||
|
InternalError(c, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
err = script.WriteFile(pathFile, string(bytes))
|
||||||
|
if err != nil {
|
||||||
|
InternalError(c, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// 更新缓存
|
||||||
|
script.UpdateLedgerAccountTypes(ledgerConfig.Id, accountTypesMap)
|
||||||
|
OK(c, script.AccountType{
|
||||||
|
Key: addAccountTypeForm.Type,
|
||||||
|
Name: addAccountTypeForm.Name,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
type CloseAccountForm struct {
|
||||||
|
Date string `form:"date" binding:"required"`
|
||||||
|
Account string `form:"account" binding:"required"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func CloseAccount(c *gin.Context) {
|
||||||
|
var accountForm CloseAccountForm
|
||||||
|
if err := c.ShouldBindJSON(&accountForm); err != nil {
|
||||||
|
BadRequest(c, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
ledgerConfig := script.GetLedgerConfigFromContext(c)
|
||||||
|
line := fmt.Sprintf("%s close %s", accountForm.Date, accountForm.Account)
|
||||||
|
// 写入文件
|
||||||
|
filePath := ledgerConfig.DataPath + "/account/" + script.GetAccountPrefix(accountForm.Account) + ".bean"
|
||||||
|
err := script.AppendFileInNewLine(filePath, line)
|
||||||
|
if err != nil {
|
||||||
|
InternalError(c, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// 更新缓存
|
||||||
|
accounts := script.GetLedgerAccounts(ledgerConfig.Id)
|
||||||
|
for i := 0; i < len(accounts); i++ {
|
||||||
|
if accounts[i].Acc == accountForm.Account {
|
||||||
|
accounts[i].EndDate = accountForm.Date
|
||||||
|
}
|
||||||
|
}
|
||||||
|
script.UpdateLedgerAccounts(ledgerConfig.Id, accounts)
|
||||||
|
OK(c, script.Account{
|
||||||
|
Acc: accountForm.Account, EndDate: accountForm.Date,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func ChangeAccountIcon(c *gin.Context) {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
type BalanceAccountForm struct {
|
||||||
|
Date string `form:"date" binding:"required" json:"date"`
|
||||||
|
Account string `form:"account" binding:"required" json:"account"`
|
||||||
|
Number string `form:"number" binding:"required" json:"number"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func BalanceAccount(c *gin.Context) {
|
||||||
|
var accountForm BalanceAccountForm
|
||||||
|
if err := c.ShouldBindJSON(&accountForm); err != nil {
|
||||||
|
BadRequest(c, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
ledgerConfig := script.GetLedgerConfigFromContext(c)
|
||||||
|
|
||||||
|
// 获取当前账户信息
|
||||||
|
var acc script.Account
|
||||||
|
accounts := script.GetLedgerAccounts(ledgerConfig.Id)
|
||||||
|
for _, account := range accounts {
|
||||||
|
if account.Acc == accountForm.Account {
|
||||||
|
acc = account
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
today, err := time.Parse("2006-01-02", accountForm.Date)
|
||||||
|
if err != nil {
|
||||||
|
InternalError(c, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
todayStr := today.Format("2006-01-02")
|
||||||
|
yesterdayStr := today.AddDate(0, 0, -1).Format("2006-01-02")
|
||||||
|
month := today.Format("2006-01")
|
||||||
|
line := fmt.Sprintf("\r\n%s pad %s Equity:OpeningBalances", yesterdayStr, accountForm.Account)
|
||||||
|
line += fmt.Sprintf("\r\n%s balance %s %s %s", todayStr, accountForm.Account, accountForm.Number, acc.Currency)
|
||||||
|
|
||||||
|
filePath := fmt.Sprintf("%s/month/%s.bean", ledgerConfig.DataPath, month)
|
||||||
|
err = script.AppendFileInNewLine(filePath, line)
|
||||||
|
if err != nil {
|
||||||
|
InternalError(c, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
result := make(map[string]string)
|
||||||
|
result["account"] = accountForm.Account
|
||||||
|
result["date"] = accountForm.Date
|
||||||
|
result["marketNumber"] = accountForm.Number
|
||||||
|
result["marketCurrency"] = ledgerConfig.OperatingCurrency
|
||||||
|
result["marketCurrencySymbol"] = script.GetCommoditySymbol(ledgerConfig.OperatingCurrency)
|
||||||
|
OK(c, result)
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,32 @@
|
||||||
|
package service
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"github.com/beancount-gs/script"
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
)
|
||||||
|
|
||||||
|
type SyncCommodityPriceForm struct {
|
||||||
|
Commodity string `form:"commodity" binding:"required" json:"commodity"`
|
||||||
|
Date string `form:"date" binding:"required" json:"date"`
|
||||||
|
Price string `form:"price" binding:"required" json:"price"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func SyncCommodityPrice(c *gin.Context) {
|
||||||
|
var syncCommodityPriceForm SyncCommodityPriceForm
|
||||||
|
if err := c.ShouldBindJSON(&syncCommodityPriceForm); err != nil {
|
||||||
|
BadRequest(c, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
ledgerConfig := script.GetLedgerConfigFromContext(c)
|
||||||
|
filePath := script.GetLedgerPriceFilePath(ledgerConfig.DataPath)
|
||||||
|
line := fmt.Sprintf("%s price %s %s %s", syncCommodityPriceForm.Date, syncCommodityPriceForm.Commodity, syncCommodityPriceForm.Price, ledgerConfig.OperatingCurrency)
|
||||||
|
// 写入文件
|
||||||
|
err := script.AppendFileInNewLine(filePath, line)
|
||||||
|
if err != nil {
|
||||||
|
InternalError(c, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
OK(c, syncCommodityPriceForm)
|
||||||
|
}
|
||||||
|
|
@ -21,10 +21,18 @@ func InternalError(c *gin.Context, message string) {
|
||||||
c.JSON(http.StatusOK, gin.H{"code": 500, "message": message})
|
c.JSON(http.StatusOK, gin.H{"code": 500, "message": message})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TransactionNotBalance(c *gin.Context) {
|
||||||
|
c.JSON(http.StatusOK, gin.H{"code": 1001})
|
||||||
|
}
|
||||||
|
|
||||||
func LedgerIsNotExist(c *gin.Context) {
|
func LedgerIsNotExist(c *gin.Context) {
|
||||||
c.JSON(http.StatusOK, gin.H{"code": 1006, "message": "ledger is not exist"})
|
c.JSON(http.StatusOK, gin.H{"code": 1006})
|
||||||
}
|
}
|
||||||
|
|
||||||
func LedgerIsNotAllowAccess(c *gin.Context) {
|
func LedgerIsNotAllowAccess(c *gin.Context) {
|
||||||
c.JSON(http.StatusOK, gin.H{"code": 1006, "message": "ledger is not allow access"})
|
c.JSON(http.StatusOK, gin.H{"code": 1006})
|
||||||
|
}
|
||||||
|
|
||||||
|
func DuplicateAccount(c *gin.Context) {
|
||||||
|
c.JSON(http.StatusOK, gin.H{"code": 1007})
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,9 +2,12 @@ package service
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
"github.com/beancount-gs/script"
|
"github.com/beancount-gs/script"
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
|
"github.com/shopspring/decimal"
|
||||||
"strings"
|
"strings"
|
||||||
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Transaction struct {
|
type Transaction struct {
|
||||||
|
|
@ -47,6 +50,124 @@ func QueryTransactions(c *gin.Context) {
|
||||||
OK(c, transactions)
|
OK(c, transactions)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type AddTransactionForm struct {
|
||||||
|
Date string `form:"date" binding:"required"`
|
||||||
|
Payee string `form:"payee"`
|
||||||
|
Desc string `form:"desc" binding:"required"`
|
||||||
|
Tags []string `form:"tags"`
|
||||||
|
Entries []AddTransactionEntryForm `form:"entries"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type AddTransactionEntryForm struct {
|
||||||
|
Account string `form:"account" binding:"required"`
|
||||||
|
Number decimal.Decimal `form:"number"`
|
||||||
|
//Currency string `form:"currency"`
|
||||||
|
Price decimal.Decimal `form:"price"`
|
||||||
|
//PriceCurrency string `form:"priceCurrency"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func sum(entries []AddTransactionEntryForm, openingBalances string) decimal.Decimal {
|
||||||
|
sumVal := decimal.NewFromInt(0)
|
||||||
|
for _, entry := range entries {
|
||||||
|
if entry.Account == openingBalances {
|
||||||
|
return sumVal
|
||||||
|
}
|
||||||
|
if entry.Price.IntPart() == 0 {
|
||||||
|
sumVal = entry.Number.Add(sumVal)
|
||||||
|
} else {
|
||||||
|
sumVal = entry.Number.Mul(entry.Price).Add(sumVal)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return sumVal
|
||||||
|
}
|
||||||
|
|
||||||
|
func AddTransactions(c *gin.Context) {
|
||||||
|
var addTransactionForm AddTransactionForm
|
||||||
|
if err := c.ShouldBindJSON(&addTransactionForm); err != nil {
|
||||||
|
BadRequest(c, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
ledgerConfig := script.GetLedgerConfigFromContext(c)
|
||||||
|
// 账户是否平衡
|
||||||
|
sumVal := sum(addTransactionForm.Entries, ledgerConfig.OpeningBalances)
|
||||||
|
val, _ := decimal.NewFromString("0.01")
|
||||||
|
if sumVal.Abs().GreaterThan(val) {
|
||||||
|
TransactionNotBalance(c)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2021-09-29 * "支付宝" "黄金补仓X元" #Invest
|
||||||
|
line := fmt.Sprintf("\r\n%s * \"%s\" \"%s\"", addTransactionForm.Date, addTransactionForm.Payee, addTransactionForm.Desc)
|
||||||
|
if len(addTransactionForm.Tags) > 0 {
|
||||||
|
for _, tag := range addTransactionForm.Tags {
|
||||||
|
line += "#" + tag + " "
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var autoBalance bool
|
||||||
|
for _, entry := range addTransactionForm.Entries {
|
||||||
|
account := script.GetLedgerAccount(ledgerConfig.Id, entry.Account)
|
||||||
|
if entry.Account == ledgerConfig.OpeningBalances {
|
||||||
|
line += fmt.Sprintf("\r\n %s", entry.Account)
|
||||||
|
} else {
|
||||||
|
line += fmt.Sprintf("\r\n %s %s %s", entry.Account, entry.Number.Round(2).String(), account.Currency)
|
||||||
|
}
|
||||||
|
// 判断是否设计多币种的转换
|
||||||
|
if account.Currency != ledgerConfig.OperatingCurrency && entry.Account != ledgerConfig.OpeningBalances {
|
||||||
|
autoBalance = true
|
||||||
|
// 根据 number 的正负来判断是买入还是卖出
|
||||||
|
if entry.Number.GreaterThan(decimal.NewFromInt(0)) {
|
||||||
|
// {351.729 CNY, 2021-09-29}
|
||||||
|
line += fmt.Sprintf(" {%s %s, %s}", entry.Price, ledgerConfig.OperatingCurrency, addTransactionForm.Date)
|
||||||
|
} else {
|
||||||
|
// {} @ 359.019 CNY
|
||||||
|
line += fmt.Sprintf(" {} @ %s %s", entry.Price, ledgerConfig.OperatingCurrency)
|
||||||
|
}
|
||||||
|
priceLine := fmt.Sprintf("%s price %s %s %s", addTransactionForm.Date, account.Currency, entry.Price, ledgerConfig.OperatingCurrency)
|
||||||
|
err := script.AppendFileInNewLine(script.GetLedgerPriceFilePath(ledgerConfig.DataPath), priceLine)
|
||||||
|
if err != nil {
|
||||||
|
InternalError(c, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 平衡小数点误差
|
||||||
|
if autoBalance {
|
||||||
|
line += "\r\n " + ledgerConfig.OpeningBalances
|
||||||
|
}
|
||||||
|
// 记账的日期
|
||||||
|
month, err := time.Parse("2006-01-02", addTransactionForm.Date)
|
||||||
|
if err != nil {
|
||||||
|
InternalError(c, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
monthStr := month.Format("2006-01")
|
||||||
|
filePath := fmt.Sprintf("%s/month/%s.bean", ledgerConfig.DataPath, monthStr)
|
||||||
|
|
||||||
|
// 文件不存在,则创建
|
||||||
|
if !script.FileIfExist(filePath) {
|
||||||
|
err = script.CreateFile(filePath)
|
||||||
|
if err != nil {
|
||||||
|
InternalError(c, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// include ./2021-11.bean
|
||||||
|
err = script.AppendFileInNewLine(script.GetLedgerMonthsFilePath(ledgerConfig.DataPath), fmt.Sprintf("include \"./%s.bean\"", monthStr))
|
||||||
|
if err != nil {
|
||||||
|
InternalError(c, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
err = script.AppendFileInNewLine(filePath, line)
|
||||||
|
if err != nil {
|
||||||
|
InternalError(c, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
OK(c, nil)
|
||||||
|
}
|
||||||
|
|
||||||
type transactionPayee struct {
|
type transactionPayee struct {
|
||||||
Value string `bql:"distinct payee" json:"value"`
|
Value string `bql:"distinct payee" json:"value"`
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue