Skip to content

Commit

Permalink
add recurring calendar view
Browse files Browse the repository at this point in the history
  • Loading branch information
ananthakumaran committed Oct 1, 2023
1 parent 610336c commit acdada2
Show file tree
Hide file tree
Showing 22 changed files with 639 additions and 199 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/docker.yml
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
name: Release Docker Image
name: Docker Image Release
on:
push:
branches:
Expand Down
1 change: 0 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@ it again via new version. If you have already upgraded, you can still
get the data directly from the db file using the following query
`sqlite3 paisa.db "select * from templates";`


### 0.5.2 (2023-09-22)

* Add Desktop app
Expand Down
2 changes: 1 addition & 1 deletion docs/stylesheets/extra.css
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
/* --md-code-hl-name-color */
/* --md-code-hl-operator-color */
/* --md-code-hl-punctuation-color */
--md-code-hl-comment-color: #940;
--md-code-hl-comment-color: #7a7a7a;
/* --md-code-hl-generic-color */
--md-code-hl-variable-color: #085;
}
Expand Down
79 changes: 55 additions & 24 deletions internal/ledger/ledger.go
Original file line number Diff line number Diff line change
Expand Up @@ -258,7 +258,23 @@ func parseAmount(amount string) (string, decimal.Decimal, error) {
func execLedgerCommand(journalPath string, flags []string) ([]*posting.Posting, error) {
var postings []*posting.Posting

args := append(append([]string{"--args-only", "-f", journalPath}, flags...), "csv", "--csv-format", "%(quoted(date)),%(quoted(payee)),%(quoted(display_account)),%(quoted(commodity(scrub(display_amount)))),%(quoted(quantity(scrub(display_amount)))),%(quoted(scrub(market(amount,date,'"+config.DefaultCurrency()+"') * 100000000))),%(quoted(xact.filename)),%(quoted(xact.id)),%(quoted(cleared ? \"*\" : (pending ? \"!\" : \"\"))),%(quoted(tag('Recurring'))),%(quoted(xact.beg_line)),%(quoted(xact.end_line)),%(quoted(lot_price(amount)))\n")
const (
Date = iota
Payee
Account
Commodity
Quantity
Amount
FileName
SequenceID
Status
TransactionBeginLine
TransactionEndLine
LotPrice
TagRecurring
TagPeriod
)
args := append(append([]string{"--args-only", "-f", journalPath}, flags...), "csv", "--csv-format", "%(quoted(date)),%(quoted(payee)),%(quoted(display_account)),%(quoted(commodity(scrub(display_amount)))),%(quoted(quantity(scrub(display_amount)))),%(quoted(scrub(market(amount,date,'"+config.DefaultCurrency()+"') * 100000000))),%(quoted(xact.filename)),%(quoted(xact.id)),%(quoted(cleared ? \"*\" : (pending ? \"!\" : \"\"))),%(quoted(xact.beg_line)),%(quoted(xact.end_line)),%(quoted(lot_price(amount))),%(quoted(tag('Recurring'))),%(quoted(tag('Period')))\n")

var output, error bytes.Buffer
err := utils.Exec(binary.LedgerBinaryPath(), &output, &error, args...)
Expand All @@ -278,22 +294,22 @@ func execLedgerCommand(journalPath string, flags []string) ([]*posting.Posting,
dir := filepath.Dir(config.GetJournalPath())

for _, record := range records {
date, err := time.ParseInLocation("2006/01/02", record[0], time.Local)
date, err := time.ParseInLocation("2006/01/02", record[Date], time.Local)
if err != nil {
return nil, err
}

quantity, err := decimal.NewFromString(record[4])
quantity, err := decimal.NewFromString(record[Quantity])
if err != nil {
return nil, err
}

var amount decimal.Decimal
amountAvailable := false

lotString := record[12]
lotString := record[LotPrice]
if lotString != "" {
lotCurrency, lotAmount, err := parseAmount(record[12])
lotCurrency, lotAmount, err := parseAmount(record[LotPrice])
if err != nil {
return nil, err
}
Expand All @@ -305,14 +321,14 @@ func execLedgerCommand(journalPath string, flags []string) ([]*posting.Posting,
}

if !amountAvailable {
_, amount, err = parseAmount(record[5])
_, amount, err = parseAmount(record[Amount])
if err != nil {
return nil, err
}
amount = amount.Div(decimal.NewFromInt(100000000))
}

if record[1] == "Budget transaction" {
if record[Payee] == "Budget transaction" {
amount = amount.Neg()
}

Expand All @@ -321,53 +337,59 @@ func execLedgerCommand(journalPath string, flags []string) ([]*posting.Posting,
var fileName string
var forecast bool

if record[1] == "Budget transaction" || record[1] == "Forecast transaction" {
transactionID = uuid.NewV5(namespace, record[0]+":"+record[1]).String()
if record[Payee] == "Budget transaction" || record[Payee] == "Forecast transaction" {
transactionID = uuid.NewV5(namespace, record[Date]+":"+record[Payee]).String()
forecast = true
} else {
fileName, err = filepath.Rel(dir, record[6])
fileName, err = filepath.Rel(dir, record[FileName])
if err != nil {
return nil, err
}

transactionID = uuid.NewV5(namespace, fileName+":"+record[7]).String()
transactionID = uuid.NewV5(namespace, fileName+":"+record[SequenceID]).String()
forecast = false
}

var status string
if record[8] == "*" {
if record[Status] == "*" {
status = "cleared"
} else if record[8] == "!" {
} else if record[Status] == "!" {
status = "pending"
} else {
status = "unmarked"
}

var tagRecurring string
if record[9] != "" {
tagRecurring = record[9]
}

transactionBeginLine, err := strconv.ParseUint(record[10], 10, 64)
transactionBeginLine, err := strconv.ParseUint(record[TransactionBeginLine], 10, 64)
if err != nil {
return nil, err
}

transactionEndLine, err := strconv.ParseUint(record[11], 10, 64)
transactionEndLine, err := strconv.ParseUint(record[TransactionEndLine], 10, 64)
if err != nil {
return nil, err
}

var tagRecurring string
if record[TagRecurring] != "" {
tagRecurring = record[TagRecurring]
}

var tagPeriod string
if record[TagPeriod] != "" {
tagPeriod = record[TagPeriod]
}

posting := posting.Posting{
Date: date,
Payee: record[1],
Account: record[2],
Commodity: utils.UnQuote(record[3]),
Payee: record[Payee],
Account: record[Account],
Commodity: utils.UnQuote(record[Commodity]),
Quantity: quantity,
Amount: amount,
TransactionID: transactionID,
Status: status,
TagRecurring: tagRecurring,
TagPeriod: tagPeriod,
TransactionBeginLine: transactionBeginLine,
TransactionEndLine: transactionEndLine,
Forecast: forecast,
Expand Down Expand Up @@ -463,14 +485,18 @@ func execHLedgerCommand(journalPath string, prices []price.Price, flags []string
}
}

var tagRecurring string
var tagRecurring, tagPeriod string

for _, tag := range t.Tags {
if len(tag) == 2 {
if tag[0] == "Recurring" {
tagRecurring = tag[1]
}

if tag[0] == "Period" {
tagPeriod = tag[1]
}

if tag[0] == "_generated-transaction" {
forecast = true
}
Expand All @@ -482,6 +508,10 @@ func execHLedgerCommand(journalPath string, prices []price.Price, flags []string
if len(tag) == 2 && tag[0] == "Recurring" {
tagRecurring = tag[1]
}

if len(tag) == 2 && tag[0] == "Period" {
tagPeriod = tag[1]
}
break
}

Expand All @@ -504,6 +534,7 @@ func execHLedgerCommand(journalPath string, prices []price.Price, flags []string
TransactionID: strconv.FormatInt(t.ID, 10),
Status: strings.ToLower(t.Status),
TagRecurring: tagRecurring,
TagPeriod: tagPeriod,
TransactionBeginLine: t.TSourcePos[0].SourceLine,
TransactionEndLine: t.TSourcePos[1].SourceLine,
Forecast: forecast,
Expand Down
1 change: 1 addition & 0 deletions internal/model/posting/posting.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ type Posting struct {
Amount decimal.Decimal `json:"amount"`
Status string `json:"status"`
TagRecurring string `json:"tag_recurring"`
TagPeriod string `json:"tag_period"`
TransactionBeginLine uint64 `json:"transaction_begin_line"`
TransactionEndLine uint64 `json:"transaction_end_line"`
FileName string `json:"file_name"`
Expand Down
19 changes: 17 additions & 2 deletions internal/model/transaction/transaction.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ type Transaction struct {
Payee string `json:"payee"`
Postings []posting.Posting `json:"postings"`
TagRecurring string `json:"tag_recurring"`
TagPeriod string `json:"tag_period"`
BeginLine uint64 `json:"beginLine"`
EndLine uint64 `json:"endLine"`
FileName string `json:"fileName"`
Expand All @@ -23,13 +24,27 @@ func Build(postings []posting.Posting) []Transaction {
return lo.Map(lo.Values(grouped), func(ps []posting.Posting, _ int) Transaction {
sample := ps[0]
var tagRecurring string
var tagPeriod string
for _, p := range ps {
if p.TagRecurring != "" {
tagRecurring = p.TagRecurring
break
}

if p.TagPeriod != "" {
tagPeriod = p.TagPeriod
}
}
return Transaction{
ID: sample.TransactionID,
Date: sample.Date,
Payee: sample.Payee,
Postings: ps,
TagRecurring: tagRecurring,
TagPeriod: tagPeriod,
BeginLine: sample.TransactionBeginLine,
EndLine: sample.TransactionEndLine,
FileName: sample.FileName,
}
return Transaction{ID: sample.TransactionID, Date: sample.Date, Payee: sample.Payee, Postings: ps, TagRecurring: tagRecurring, BeginLine: sample.TransactionBeginLine, EndLine: sample.TransactionEndLine, FileName: sample.FileName}
})

}
20 changes: 11 additions & 9 deletions internal/server/recurring.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,15 +18,12 @@ func GetRecurringTransactions(db *gorm.DB) gin.H {

type TransactionSequence struct {
Transactions []transaction.Transaction `json:"transactions"`
Key TransactionSequenceKey `json:"key"`
Key string `json:"key"`
Period string `json:"period"`
Interval int `json:"interval"`
DaysSinceLastTransaction int `json:"days_since_last_transaction"`
}

type TransactionSequenceKey struct {
TagRecurring string `json:"tagRecurring"`
}

func ComputeRecurringTransactions(postings []posting.Posting) []TransactionSequence {
now := utils.EndOfToday()

Expand All @@ -38,23 +35,28 @@ func ComputeRecurringTransactions(postings []posting.Posting) []TransactionSeque
transactions = lo.Filter(transactions, func(t transaction.Transaction, _ int) bool {
return t.TagRecurring != ""
})
transactionsGrouped := lo.GroupBy(transactions, func(t transaction.Transaction) TransactionSequenceKey {
return TransactionSequenceKey{TagRecurring: t.TagRecurring}
transactionsGrouped := lo.GroupBy(transactions, func(t transaction.Transaction) string {
return t.TagRecurring
})

transaction_sequences := lo.MapToSlice(transactionsGrouped, func(key TransactionSequenceKey, ts []transaction.Transaction) TransactionSequence {
transaction_sequences := lo.MapToSlice(transactionsGrouped, func(key string, ts []transaction.Transaction) TransactionSequence {
sort.SliceStable(ts, func(i, j int) bool {
return ts[i].Date.After(ts[j].Date)
})

interval := 0
daysSinceLastTransaction := 0
var period string
if ts[0].TagPeriod != "" {
period = ts[0].TagPeriod
}

if len(ts) > 1 {
interval = int(ts[0].Date.Sub(ts[1].Date).Hours() / 24)
daysSinceLastTransaction = int(now.Sub(ts[0].Date).Hours() / 24)
}

return TransactionSequence{Transactions: ts, Key: key, Interval: interval, DaysSinceLastTransaction: daysSinceLastTransaction}
return TransactionSequence{Transactions: ts, Key: key, Interval: interval, DaysSinceLastTransaction: daysSinceLastTransaction, Period: period}
})

sort.SliceStable(transaction_sequences, func(i, j int) bool {
Expand Down
43 changes: 43 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@
"dependencies": {
"@cityssm/bulma-sticky-table": "^2.1.0",
"@codemirror/autocomplete": "^6.4.2",
"@datasert/cronjs-matcher": "^1.2.0",
"@datasert/cronjs-parser": "^1.2.0",
"@egjs/svelte-grid": "^1.14.2",
"@fontsource-variable/roboto-flex": "^5.0.8",
"@fontsource-variable/roboto-mono": "^5.0.9",
Expand Down
Loading

0 comments on commit acdada2

Please sign in to comment.