beancount-gs/service/transactions.go

380 lines
11 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package service
import (
"crypto/sha1"
"encoding/hex"
"encoding/json"
"errors"
"fmt"
"io"
"strings"
"time"
"github.com/beancount-gs/script"
"github.com/gin-gonic/gin"
"github.com/shopspring/decimal"
)
type Transaction struct {
Id string `bql:"id" json:"id"`
Account string `bql:"account" json:"account"`
Date string `bql:"date" json:"date"`
Payee string `bql:"payee" json:"payee"`
Narration string `bql:"narration" json:"desc"`
Number string `bql:"number" json:"number"`
Balance string `bql:"balance" json:"balance"`
Currency string `bql:"currency" json:"currency"`
CostDate string `bql:"cost_date" json:"costDate"`
CostPrice string `bql:"cost_number" json:"costPrice"` // 交易净值
CostCurrency string `bql:"cost_currency" json:"costCurrency"`
Price string `bql:"price" json:"price"`
Tags []string `bql:"tags" json:"tags"`
CurrencySymbol string `json:"currencySymbol,omitempty"`
CostCurrencySymbol string `json:"costCurrencySymbol,omitempty"`
}
func QueryTransactions(c *gin.Context) {
ledgerConfig := script.GetLedgerConfigFromContext(c)
queryParams := script.GetQueryParams(c)
// 倒序查询
queryParams.OrderBy = "date desc"
transactions := make([]Transaction, 0)
err := script.BQLQueryList(ledgerConfig, &queryParams, &transactions)
if err != nil {
InternalError(c, err.Error())
return
}
// 格式化金额
for i := 0; i < len(transactions); i++ {
symbol := script.GetCommoditySymbol(ledgerConfig.Id, transactions[i].Currency)
transactions[i].CurrencySymbol = symbol
transactions[i].CostCurrencySymbol = symbol
if transactions[i].Price != "" {
transactions[i].Price = strings.Fields(transactions[i].Price)[0]
}
if transactions[i].Balance != "" {
transactions[i].Balance = strings.Fields(transactions[i].Balance)[0]
}
}
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"`
DivideDateList []string `form:"divideDateList"`
Entries []AddTransactionEntryForm `form:"entries"`
}
type AddTransactionEntryForm struct {
Account string `form:"account" binding:"required" json:"account"`
Number decimal.Decimal `form:"number" json:"number"`
Currency string `form:"currency" json:"currency"`
Price decimal.Decimal `form:"price" json:"price"`
PriceCurrency string `form:"priceCurrency" json:"priceCurrency"`
}
func sum(entries []AddTransactionEntryForm, openingBalances string) decimal.Decimal {
sumVal := decimal.NewFromInt(0)
for _, entry := range entries {
if entry.Account == openingBalances {
return decimal.NewFromInt(0)
}
if entry.Price.Exponent() == 0 {
sumVal = entry.Number.Add(sumVal)
} else {
sumVal = entry.Number.Mul(entry.Price).Add(sumVal)
}
}
return sumVal
}
func AddBatchTransactions(c *gin.Context) {
var addTransactionForms []AddTransactionForm
if err := c.ShouldBindJSON(&addTransactionForms); err != nil {
BadRequest(c, err.Error())
return
}
result := make([]string, 0)
ledgerConfig := script.GetLedgerConfigFromContext(c)
for _, form := range addTransactionForms {
err := saveTransaction(nil, form, ledgerConfig)
if err == nil {
result = append(result, form.Date+form.Payee+form.Desc)
} else {
script.LogError(ledgerConfig.Mail, err.Error())
}
}
OK(c, result)
}
func AddTransactions(c *gin.Context) {
var addTransactionForm AddTransactionForm
if err := c.ShouldBindJSON(&addTransactionForm); err != nil {
BadRequest(c, err.Error())
return
}
ledgerConfig := script.GetLedgerConfigFromContext(c)
// 判断是否分期
var err error
var divideCount = len(addTransactionForm.DivideDateList)
if divideCount <= 0 {
err = saveTransaction(c, addTransactionForm, ledgerConfig)
} else {
for idx, entry := range addTransactionForm.Entries {
// 保留 3 位小数
addTransactionForm.Entries[idx].Number = entry.Number.Div(decimal.NewFromInt(int64(divideCount))).Round(3)
}
for _, date := range addTransactionForm.DivideDateList {
addTransactionForm.Date = date
err = saveTransaction(c, addTransactionForm, ledgerConfig)
if err != nil {
break
}
}
}
if err != nil {
script.LogError(ledgerConfig.Mail, err.Error())
return
}
OK(c, nil)
}
func saveTransaction(c *gin.Context, addTransactionForm AddTransactionForm, ledgerConfig *script.Config) error {
// 账户是否平衡
sumVal := sum(addTransactionForm.Entries, ledgerConfig.OpeningBalances)
val, _ := decimal.NewFromString("0.1")
if sumVal.Abs().GreaterThan(val) {
if c != nil {
TransactionNotBalance(c)
}
return errors.New("transaction not balance")
}
// 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 + " "
}
}
currencyMap := script.GetLedgerCurrencyMap(ledgerConfig.Id)
var autoBalance bool
for _, entry := range addTransactionForm.Entries {
account := script.GetLedgerAccount(ledgerConfig.Id, entry.Account)
if entry.Account == ledgerConfig.OpeningBalances {
autoBalance = false
line += fmt.Sprintf("\r\n %s", entry.Account)
} else {
line += fmt.Sprintf("\r\n %s %s %s", entry.Account, entry.Number.Round(2).StringFixedBank(2), account.Currency)
}
zero := decimal.NewFromInt(0)
// 判断是否涉及多币种的转换
if account.Currency != ledgerConfig.OperatingCurrency && entry.Account != ledgerConfig.OpeningBalances {
autoBalance = true
// 汇率值小于等于0则不进行汇率转换
if entry.Price.LessThanOrEqual(zero) {
continue
}
// 货币跳过汇率转换
if _, ok := currencyMap[account.Currency]; !ok {
// 根据 number 的正负来判断是买入还是卖出
if entry.Number.GreaterThan(zero) {
// {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 {
if c != nil {
InternalError(c, err.Error())
}
return errors.New("internal error")
}
}
}
// 平衡小数点误差
if autoBalance {
line += "\r\n " + ledgerConfig.OpeningBalances
}
// 记账的日期
month, err := time.Parse("2006-01-02", addTransactionForm.Date)
if err != nil {
if c != nil {
InternalError(c, err.Error())
}
return errors.New("internal error")
}
// 交易的月份信息
monthStr := month.Format("2006-01")
err = CreateMonthBeanFileIfNotExist(ledgerConfig.DataPath, monthStr)
if err != nil {
if c != nil {
InternalError(c, err.Error())
}
return err
}
err = script.AppendFileInNewLine(script.GetLedgerMonthFilePath(ledgerConfig.DataPath, monthStr), line)
if err != nil {
if c != nil {
InternalError(c, err.Error())
}
return errors.New("internal error")
}
return nil
}
type transactionPayee struct {
Value string `bql:"distinct payee" json:"value"`
}
func QueryTransactionPayees(c *gin.Context) {
ledgerConfig := script.GetLedgerConfigFromContext(c)
payeeList := make([]transactionPayee, 0)
queryParams := script.QueryParams{Where: false, OrderBy: "date desc", Limit: 100}
err := script.BQLQueryList(ledgerConfig, &queryParams, &payeeList)
if err != nil {
InternalError(c, err.Error())
return
}
result := make([]string, 0)
for _, payee := range payeeList {
if payee.Value != "" {
result = append(result, payee.Value)
}
}
OK(c, result)
}
type TransactionTemplate struct {
Id string `json:"id"`
Date string `form:"date" binding:"required" json:"date"`
TemplateName string `form:"templateName" binding:"required" json:"templateName"`
Payee string `form:"payee" json:"payee"`
Desc string `form:"desc" binding:"required" json:"desc"`
Entries []AddTransactionEntryForm `form:"entries" json:"entries"`
}
func QueryTransactionTemplates(c *gin.Context) {
ledgerConfig := script.GetLedgerConfigFromContext(c)
filePath := script.GetLedgerTransactionsTemplateFilePath(ledgerConfig.DataPath)
templates, err := getLedgerTransactionTemplates(filePath)
if err != nil {
InternalError(c, err.Error())
return
}
OK(c, templates)
}
func AddTransactionTemplate(c *gin.Context) {
var transactionTemplate TransactionTemplate
if err := c.ShouldBindJSON(&transactionTemplate); err != nil {
BadRequest(c, err.Error())
return
}
ledgerConfig := script.GetLedgerConfigFromContext(c)
filePath := script.GetLedgerTransactionsTemplateFilePath(ledgerConfig.DataPath)
templates, err := getLedgerTransactionTemplates(filePath)
if err != nil {
InternalError(c, err.Error())
return
}
t := sha1.New()
_, err = io.WriteString(t, time.Now().String())
if err != nil {
InternalError(c, err.Error())
return
}
transactionTemplate.Id = hex.EncodeToString(t.Sum(nil))
templates = append(templates, transactionTemplate)
err = writeLedgerTransactionTemplates(filePath, templates)
if err != nil {
InternalError(c, err.Error())
return
}
OK(c, transactionTemplate)
}
func DeleteTransactionTemplate(c *gin.Context) {
templateId := c.Query("id")
if templateId == "" {
BadRequest(c, "templateId is not blank")
return
}
ledgerConfig := script.GetLedgerConfigFromContext(c)
filePath := script.GetLedgerTransactionsTemplateFilePath(ledgerConfig.DataPath)
oldTemplates, err := getLedgerTransactionTemplates(filePath)
if err != nil {
InternalError(c, err.Error())
return
}
newTemplates := make([]TransactionTemplate, 0)
for _, template := range oldTemplates {
if template.Id != templateId {
newTemplates = append(newTemplates, template)
}
}
err = writeLedgerTransactionTemplates(filePath, newTemplates)
if err != nil {
InternalError(c, err.Error())
return
}
OK(c, templateId)
}
func getLedgerTransactionTemplates(filePath string) ([]TransactionTemplate, error) {
result := make([]TransactionTemplate, 0)
if script.FileIfExist(filePath) {
bytes, err := script.ReadFile(filePath)
if err != nil {
return nil, err
}
err = json.Unmarshal(bytes, &result)
if err != nil {
return nil, err
}
}
return result, nil
}
func writeLedgerTransactionTemplates(filePath string, templates []TransactionTemplate) error {
if !script.FileIfExist(filePath) {
err := script.CreateFile(filePath)
if err != nil {
return err
}
}
bytes, err := json.Marshal(templates)
if err != nil {
return err
}
err = script.WriteFile(filePath, string(bytes))
if err != nil {
return err
}
return nil
}