2021-11-21 14:37:13 +00:00
|
|
|
package service
|
|
|
|
|
|
|
|
|
|
import (
|
2021-11-29 14:21:25 +00:00
|
|
|
"crypto/sha1"
|
|
|
|
|
"encoding/hex"
|
2021-11-22 10:05:12 +00:00
|
|
|
"encoding/json"
|
2021-12-12 14:42:07 +00:00
|
|
|
"errors"
|
2021-11-28 12:19:40 +00:00
|
|
|
"fmt"
|
2021-11-29 14:21:25 +00:00
|
|
|
"io"
|
2021-11-22 08:47:49 +00:00
|
|
|
"strings"
|
2021-11-28 12:19:40 +00:00
|
|
|
"time"
|
2022-01-09 05:48:18 +00:00
|
|
|
|
|
|
|
|
"github.com/beancount-gs/script"
|
|
|
|
|
"github.com/gin-gonic/gin"
|
|
|
|
|
"github.com/shopspring/decimal"
|
2021-11-21 14:37:13 +00:00
|
|
|
)
|
|
|
|
|
|
2021-11-24 09:32:24 +00:00
|
|
|
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"`
|
2022-01-09 05:48:18 +00:00
|
|
|
Balance string `bql:"balance" json:"balance"`
|
2021-11-24 09:32:24 +00:00
|
|
|
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"`
|
2021-11-22 10:05:12 +00:00
|
|
|
}
|
|
|
|
|
|
2021-11-21 14:37:13 +00:00
|
|
|
func QueryTransactions(c *gin.Context) {
|
|
|
|
|
ledgerConfig := script.GetLedgerConfigFromContext(c)
|
2021-11-22 14:50:10 +00:00
|
|
|
queryParams := script.GetQueryParams(c)
|
2021-11-22 10:05:12 +00:00
|
|
|
// 倒序查询
|
|
|
|
|
queryParams.OrderBy = "date desc"
|
2021-11-24 09:32:24 +00:00
|
|
|
transactions := make([]Transaction, 0)
|
2021-11-22 08:47:49 +00:00
|
|
|
err := script.BQLQueryList(ledgerConfig, &queryParams, &transactions)
|
2021-11-21 14:37:13 +00:00
|
|
|
if err != nil {
|
|
|
|
|
InternalError(c, err.Error())
|
|
|
|
|
return
|
|
|
|
|
}
|
2021-11-22 08:47:49 +00:00
|
|
|
// 格式化金额
|
|
|
|
|
for i := 0; i < len(transactions); i++ {
|
2021-11-24 09:32:24 +00:00
|
|
|
symbol := script.GetCommoditySymbol(transactions[i].Currency)
|
|
|
|
|
transactions[i].CurrencySymbol = symbol
|
|
|
|
|
transactions[i].CostCurrencySymbol = symbol
|
|
|
|
|
if transactions[i].Price != "" {
|
|
|
|
|
transactions[i].Price = strings.Fields(transactions[i].Price)[0]
|
2021-11-22 08:47:49 +00:00
|
|
|
}
|
2022-01-09 05:48:18 +00:00
|
|
|
if transactions[i].Balance != "" {
|
|
|
|
|
transactions[i].Balance = strings.Fields(transactions[i].Balance)[0]
|
|
|
|
|
}
|
2021-11-22 08:47:49 +00:00
|
|
|
}
|
2021-11-21 14:37:13 +00:00
|
|
|
OK(c, transactions)
|
|
|
|
|
}
|
2021-11-22 10:05:12 +00:00
|
|
|
|
2021-11-28 12:19:40 +00:00
|
|
|
type AddTransactionForm struct {
|
2022-03-20 15:55:05 +00:00
|
|
|
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"`
|
2021-11-28 12:19:40 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
type AddTransactionEntryForm struct {
|
2021-11-29 14:21:25 +00:00
|
|
|
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"`
|
2021-11-28 12:19:40 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func sum(entries []AddTransactionEntryForm, openingBalances string) decimal.Decimal {
|
|
|
|
|
sumVal := decimal.NewFromInt(0)
|
|
|
|
|
for _, entry := range entries {
|
|
|
|
|
if entry.Account == openingBalances {
|
2021-11-29 14:21:25 +00:00
|
|
|
return decimal.NewFromInt(0)
|
2021-11-28 12:19:40 +00:00
|
|
|
}
|
|
|
|
|
if entry.Price.IntPart() == 0 {
|
|
|
|
|
sumVal = entry.Number.Add(sumVal)
|
|
|
|
|
} else {
|
|
|
|
|
sumVal = entry.Number.Mul(entry.Price).Add(sumVal)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return sumVal
|
|
|
|
|
}
|
|
|
|
|
|
2021-12-12 14:42:07 +00:00
|
|
|
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)
|
|
|
|
|
}
|
|
|
|
|
|
2021-11-28 12:19:40 +00:00
|
|
|
func AddTransactions(c *gin.Context) {
|
|
|
|
|
var addTransactionForm AddTransactionForm
|
|
|
|
|
if err := c.ShouldBindJSON(&addTransactionForm); err != nil {
|
|
|
|
|
BadRequest(c, err.Error())
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
ledgerConfig := script.GetLedgerConfigFromContext(c)
|
2022-03-20 15:55:05 +00:00
|
|
|
// 判断是否分期
|
|
|
|
|
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
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2021-12-12 14:42:07 +00:00
|
|
|
if err != nil {
|
2022-03-20 15:55:05 +00:00
|
|
|
script.LogError(ledgerConfig.Mail, err.Error())
|
|
|
|
|
InternalError(c, "failed to add transaction")
|
2021-12-12 14:42:07 +00:00
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
OK(c, nil)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func saveTransaction(c *gin.Context, addTransactionForm AddTransactionForm, ledgerConfig *script.Config) error {
|
2021-11-28 12:19:40 +00:00
|
|
|
// 账户是否平衡
|
|
|
|
|
sumVal := sum(addTransactionForm.Entries, ledgerConfig.OpeningBalances)
|
2021-12-16 07:10:39 +00:00
|
|
|
val, _ := decimal.NewFromString("0.1")
|
2021-11-28 12:19:40 +00:00
|
|
|
if sumVal.Abs().GreaterThan(val) {
|
2021-12-12 14:42:07 +00:00
|
|
|
if c != nil {
|
|
|
|
|
TransactionNotBalance(c)
|
|
|
|
|
}
|
|
|
|
|
return errors.New("transaction not balance")
|
2021-11-28 12:19:40 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 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 {
|
2022-01-09 06:22:28 +00:00
|
|
|
autoBalance = false
|
2021-11-28 12:19:40 +00:00
|
|
|
line += fmt.Sprintf("\r\n %s", entry.Account)
|
|
|
|
|
} else {
|
2021-12-16 07:24:05 +00:00
|
|
|
line += fmt.Sprintf("\r\n %s %s %s", entry.Account, entry.Number.Round(2).StringFixedBank(2), account.Currency)
|
2021-11-28 12:19:40 +00:00
|
|
|
}
|
2022-01-09 06:22:28 +00:00
|
|
|
// 判断是否涉及多币种的转换
|
2021-11-28 12:19:40 +00:00
|
|
|
if account.Currency != ledgerConfig.OperatingCurrency && entry.Account != ledgerConfig.OpeningBalances {
|
|
|
|
|
// 根据 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 {
|
2021-12-12 14:42:07 +00:00
|
|
|
if c != nil {
|
|
|
|
|
InternalError(c, err.Error())
|
|
|
|
|
}
|
|
|
|
|
return errors.New("internal error")
|
2021-11-28 12:19:40 +00:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 平衡小数点误差
|
|
|
|
|
if autoBalance {
|
|
|
|
|
line += "\r\n " + ledgerConfig.OpeningBalances
|
|
|
|
|
}
|
|
|
|
|
// 记账的日期
|
|
|
|
|
month, err := time.Parse("2006-01-02", addTransactionForm.Date)
|
|
|
|
|
if err != nil {
|
2021-12-12 14:42:07 +00:00
|
|
|
if c != nil {
|
|
|
|
|
InternalError(c, err.Error())
|
|
|
|
|
}
|
|
|
|
|
return errors.New("internal error")
|
2021-11-28 12:19:40 +00:00
|
|
|
}
|
|
|
|
|
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 {
|
2021-12-12 14:42:07 +00:00
|
|
|
if c != nil {
|
|
|
|
|
InternalError(c, err.Error())
|
|
|
|
|
}
|
|
|
|
|
return errors.New("internal error")
|
2021-11-28 12:19:40 +00:00
|
|
|
}
|
|
|
|
|
// include ./2021-11.bean
|
|
|
|
|
err = script.AppendFileInNewLine(script.GetLedgerMonthsFilePath(ledgerConfig.DataPath), fmt.Sprintf("include \"./%s.bean\"", monthStr))
|
|
|
|
|
if err != nil {
|
2021-12-12 14:42:07 +00:00
|
|
|
if c != nil {
|
|
|
|
|
InternalError(c, err.Error())
|
|
|
|
|
}
|
|
|
|
|
return errors.New("internal error")
|
2021-11-28 12:19:40 +00:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
err = script.AppendFileInNewLine(filePath, line)
|
|
|
|
|
if err != nil {
|
2021-12-12 14:42:07 +00:00
|
|
|
if c != nil {
|
|
|
|
|
InternalError(c, err.Error())
|
|
|
|
|
}
|
|
|
|
|
return errors.New("internal error")
|
2021-11-28 12:19:40 +00:00
|
|
|
}
|
2021-12-12 14:42:07 +00:00
|
|
|
return nil
|
2021-11-28 12:19:40 +00:00
|
|
|
}
|
|
|
|
|
|
2021-11-24 09:32:24 +00:00
|
|
|
type transactionPayee struct {
|
2021-11-22 10:05:12 +00:00
|
|
|
Value string `bql:"distinct payee" json:"value"`
|
|
|
|
|
}
|
|
|
|
|
|
2021-11-24 09:32:24 +00:00
|
|
|
func QueryTransactionPayees(c *gin.Context) {
|
2021-11-22 10:05:12 +00:00
|
|
|
ledgerConfig := script.GetLedgerConfigFromContext(c)
|
2021-11-24 09:32:24 +00:00
|
|
|
payeeList := make([]transactionPayee, 0)
|
2021-11-22 10:05:12 +00:00
|
|
|
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)
|
|
|
|
|
}
|
|
|
|
|
|
2021-11-29 14:21:25 +00:00
|
|
|
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"`
|
2021-11-22 10:05:12 +00:00
|
|
|
}
|
|
|
|
|
|
2021-11-29 14:21:25 +00:00
|
|
|
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)
|
2021-11-22 10:05:12 +00:00
|
|
|
}
|
|
|
|
|
|
2021-11-29 14:21:25 +00:00
|
|
|
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
|
|
|
|
|
}
|
|
|
|
|
|
2021-11-22 10:05:12 +00:00
|
|
|
ledgerConfig := script.GetLedgerConfigFromContext(c)
|
|
|
|
|
filePath := script.GetLedgerTransactionsTemplateFilePath(ledgerConfig.DataPath)
|
2021-11-29 14:21:25 +00:00
|
|
|
|
|
|
|
|
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)
|
2021-11-22 10:05:12 +00:00
|
|
|
if script.FileIfExist(filePath) {
|
|
|
|
|
bytes, err := script.ReadFile(filePath)
|
|
|
|
|
if err != nil {
|
2021-11-29 14:21:25 +00:00
|
|
|
return nil, err
|
2021-11-22 10:05:12 +00:00
|
|
|
}
|
|
|
|
|
err = json.Unmarshal(bytes, &result)
|
|
|
|
|
if err != nil {
|
2021-11-29 14:21:25 +00:00
|
|
|
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
|
2021-11-22 10:05:12 +00:00
|
|
|
}
|
|
|
|
|
}
|
2021-11-29 14:21:25 +00:00
|
|
|
|
|
|
|
|
bytes, err := json.Marshal(templates)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
err = script.WriteFile(filePath, string(bytes))
|
|
|
|
|
if err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
return nil
|
2021-11-22 10:05:12 +00:00
|
|
|
}
|