[config] support price provider search option
This commit is contained in:
parent
1e54252df4
commit
f27f6b2f06
|
@ -4,7 +4,7 @@ paisa
|
|||
web/static/dist*
|
||||
node_modules
|
||||
example.ledger
|
||||
personal.ledger
|
||||
main.ledger
|
||||
Dockerfile
|
||||
fly.toml
|
||||
.dockerignore
|
||||
|
|
|
@ -3,7 +3,7 @@ paisa.yaml
|
|||
./paisa
|
||||
web/static
|
||||
example.ledger
|
||||
personal.ledger
|
||||
main.ledger
|
||||
.DS_Store
|
||||
node_modules
|
||||
/build
|
||||
|
|
2
Makefile
2
Makefile
|
@ -43,7 +43,7 @@ install:
|
|||
|
||||
fixture/main.transactions.json:
|
||||
cd /tmp && paisa init
|
||||
cp fixture/main.ledger /tmp/personal.ledger
|
||||
cp fixture/main.ledger /tmp/main.ledger
|
||||
cd /tmp && paisa update --journal && paisa serve -p 6500 &
|
||||
sleep 1
|
||||
curl http://localhost:6500/api/transaction | jq .transactions > fixture/main.transactions.json
|
||||
|
|
|
@ -2,9 +2,9 @@
|
|||
|
||||
[![Matrix](https://img.shields.io/matrix/paisa%3Amatrix.org?logo=matrix)](https://matrix.to/#/#paisa:matrix.org)
|
||||
|
||||
**Paisa** is a command line tool and Web UI to visualize personal
|
||||
finance. It builds on top of the [ledger](https://www.ledger-cli.org/) double entry accounting
|
||||
tool. Checkout [documentation](https://paisa.fyi) to get started.
|
||||
**Paisa** is a Personal finance manager. It builds on
|
||||
top of the [ledger](https://www.ledger-cli.org/) double entry accounting tool. Checkout
|
||||
[documentation](https://paisa.fyi) to get started.
|
||||
|
||||
# Demo
|
||||
|
||||
|
|
|
@ -1,88 +0,0 @@
|
|||
package cmd
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/ananthakumaran/paisa/internal/model"
|
||||
"github.com/ananthakumaran/paisa/internal/model/mutualfund/scheme"
|
||||
"github.com/ananthakumaran/paisa/internal/scraper/mutualfund"
|
||||
"github.com/ananthakumaran/paisa/internal/utils"
|
||||
"github.com/logrusorgru/aurora"
|
||||
"github.com/manifoldco/promptui"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"github.com/spf13/cobra"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
var update bool
|
||||
|
||||
var mutualfundCmd = &cobra.Command{
|
||||
Use: "mutualfund",
|
||||
Short: "Search mutual fund",
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
db, err := utils.OpenDB()
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
model.AutoMigrate(db)
|
||||
count := scheme.Count(db)
|
||||
if update || count == 0 {
|
||||
schemes, err := mutualfund.GetSchemes()
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
scheme.UpsertAll(db, schemes)
|
||||
} else {
|
||||
log.Info("Using cached results; pass '-u' to update the cache")
|
||||
}
|
||||
amc := promptAMC(db)
|
||||
name := promptName(db, amc)
|
||||
scheme := scheme.FindScheme(db, amc, name)
|
||||
log.Info("Mutual Fund Scheme Code: ", aurora.Bold(scheme.Code))
|
||||
},
|
||||
}
|
||||
|
||||
func init() {
|
||||
searchCmd.AddCommand(mutualfundCmd)
|
||||
mutualfundCmd.Flags().BoolVarP(&update, "update", "u", false, "update the Mutual Fund Scheme list")
|
||||
}
|
||||
|
||||
func promptAMC(db *gorm.DB) string {
|
||||
amcs := scheme.GetAMCs(db)
|
||||
return prompt("AMC", amcs)
|
||||
}
|
||||
|
||||
func promptName(db *gorm.DB, amc string) string {
|
||||
names := scheme.GetNAVNames(db, amc)
|
||||
return prompt("Fund Name", names)
|
||||
}
|
||||
|
||||
func prompt(label string, list []string) string {
|
||||
searcher := func(input string, index int) bool {
|
||||
item := list[index]
|
||||
item = strings.Replace(strings.ToLower(item), " ", "", -1)
|
||||
words := strings.Split(strings.ToLower(input), " ")
|
||||
for _, word := range words {
|
||||
if strings.TrimSpace(word) != "" && !strings.Contains(item, word) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
prompt := promptui.Select{
|
||||
Label: label,
|
||||
Items: list,
|
||||
Size: 10,
|
||||
Searcher: searcher,
|
||||
StartInSearchMode: true,
|
||||
}
|
||||
|
||||
_, item, err := prompt.Run()
|
||||
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
return item
|
||||
}
|
88
cmd/nps.go
88
cmd/nps.go
|
@ -1,88 +0,0 @@
|
|||
package cmd
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/ananthakumaran/paisa/internal/model"
|
||||
"github.com/ananthakumaran/paisa/internal/model/nps/scheme"
|
||||
"github.com/ananthakumaran/paisa/internal/scraper/nps"
|
||||
"github.com/ananthakumaran/paisa/internal/utils"
|
||||
"github.com/logrusorgru/aurora"
|
||||
"github.com/manifoldco/promptui"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"github.com/spf13/cobra"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
var npsUpdate bool
|
||||
|
||||
var npsCmd = &cobra.Command{
|
||||
Use: "nps",
|
||||
Short: "Search nps fund",
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
db, err := utils.OpenDB()
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
model.AutoMigrate(db)
|
||||
count := scheme.Count(db)
|
||||
if update || count == 0 {
|
||||
schemes, err := nps.GetSchemes()
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
scheme.UpsertAll(db, schemes)
|
||||
} else {
|
||||
log.Info("Using cached results; pass '-u' to update the cache")
|
||||
}
|
||||
pfm := promptPFM(db)
|
||||
name := npsPromptName(db, pfm)
|
||||
scheme := scheme.FindScheme(db, pfm, name)
|
||||
log.Info("NPS Fund Scheme Code: ", aurora.Bold(scheme.SchemeID))
|
||||
},
|
||||
}
|
||||
|
||||
func init() {
|
||||
searchCmd.AddCommand(npsCmd)
|
||||
npsCmd.Flags().BoolVarP(&update, "update", "u", false, "update the NPS Fund Scheme list")
|
||||
}
|
||||
|
||||
func promptPFM(db *gorm.DB) string {
|
||||
pfms := scheme.GetPFMs(db)
|
||||
return npsPrompt("Pension Fund Manager", pfms)
|
||||
}
|
||||
|
||||
func npsPromptName(db *gorm.DB, pfm string) string {
|
||||
names := scheme.GetSchemeNames(db, pfm)
|
||||
return npsPrompt("Fund Name", names)
|
||||
}
|
||||
|
||||
func npsPrompt(label string, list []string) string {
|
||||
searcher := func(input string, index int) bool {
|
||||
item := list[index]
|
||||
item = strings.Replace(strings.ToLower(item), " ", "", -1)
|
||||
words := strings.Split(strings.ToLower(input), " ")
|
||||
for _, word := range words {
|
||||
if strings.TrimSpace(word) != "" && !strings.Contains(item, word) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
prompt := promptui.Select{
|
||||
Label: label,
|
||||
Items: list,
|
||||
Size: 10,
|
||||
Searcher: searcher,
|
||||
StartInSearchMode: true,
|
||||
}
|
||||
|
||||
_, item, err := prompt.Run()
|
||||
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
return item
|
||||
}
|
|
@ -21,7 +21,7 @@ var configFile string
|
|||
|
||||
var rootCmd = &cobra.Command{
|
||||
Use: "paisa",
|
||||
Short: "A command line tool to manager personal finance",
|
||||
Short: "Personal finance manager",
|
||||
}
|
||||
|
||||
func Execute() {
|
||||
|
|
|
@ -1,14 +0,0 @@
|
|||
package cmd
|
||||
|
||||
import (
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var searchCmd = &cobra.Command{
|
||||
Use: "search",
|
||||
Short: "Search mutual fund",
|
||||
}
|
||||
|
||||
func init() {
|
||||
rootCmd.AddCommand(searchCmd)
|
||||
}
|
|
@ -13,6 +13,6 @@
|
|||
"productName": "Paisa",
|
||||
"productVersion": "0.5.0",
|
||||
"copyright": "Copyright © 2022 - 2023 Anantha Kumaran",
|
||||
"comments": "Personal Finance App"
|
||||
"comments": "Personal finance manager"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,5 +1,36 @@
|
|||
# Installation
|
||||
|
||||
Paisa is available in two formats: a Desktop Application and a CLI (Command Line Interface).
|
||||
|
||||
## Desktop Application
|
||||
|
||||
=== "Mac"
|
||||
|
||||
* Download the prebuilt [binary](https://github.com/ananthakumaran/paisa/releases/latest) named `paisa-app-macos-amd64.dmg`
|
||||
* Open the dmg file and drag the Paisa app into Application folder
|
||||
* Since the app is not signed[^1], Mac will show a warning when
|
||||
you try to open the app. You can check the
|
||||
[support](https://support.apple.com/en-us/HT202491) page for more
|
||||
details. If you don't get any option to open the app, go to the
|
||||
Application folder, right click on the icon and select
|
||||
open. Usually, this should present you with an option to open.
|
||||
* Paisa will store all your journals, configuration files, and other
|
||||
related files in a folder named `paisa` which will be located in your
|
||||
`Documents` folder. When you open the app on your Mac for the
|
||||
first time, a permission dialog will appear. Click Allow, then close and reopen the app.
|
||||
|
||||
=== "Windows"
|
||||
|
||||
* Download the prebuilt [binary](https://github.com/ananthakumaran/paisa/releases/latest) named `paisa-app-windows-amd64.exe`
|
||||
* Since the app is not signed[^1], Windows will show multiple
|
||||
warnings, You might have to click `Keep anyway`, `More info`, `Run
|
||||
anyway` etc.
|
||||
* Paisa will store all your journals, configuration files, and other
|
||||
related files in a folder named `paisa` which will be located in your
|
||||
`Documents` folder.
|
||||
|
||||
## CLI
|
||||
|
||||
=== "Linux"
|
||||
|
||||
* Download the prebuilt [binary](https://github.com/ananthakumaran/paisa/releases/latest) named `paisa-cli-linux-amd64`
|
||||
|
@ -28,7 +59,7 @@
|
|||
=== "Windows"
|
||||
|
||||
* Download the prebuilt [binary](https://github.com/ananthakumaran/paisa/releases/latest) named `paisa-cli-windows-amd64.exe`
|
||||
* Since the binary is not signed with a certificate, you might get
|
||||
* Since the binary is not signed[^1] with a certificate, you might get
|
||||
a warning from Windows. You would have to click `keep anyway`.
|
||||
* Run the following command in a Windows PowerShell. Make sure
|
||||
you are in the correct folder. You should see something like `PS C:\Users\yourname>`
|
||||
|
@ -48,7 +79,7 @@
|
|||
it's not already installed on your system. If you prefer to install the
|
||||
ledger yourself, follow the installation instructions on [ledger](https://www.ledger-cli.org/download.html) site.
|
||||
|
||||
## Quick Start
|
||||
## CLI Quick Start
|
||||
|
||||
Paisa will store all your journals, configuration files, and other
|
||||
related files in a folder named `paisa` which will be located in your
|
||||
|
@ -73,3 +104,10 @@ related files in a folder named `paisa` which will be located in your
|
|||
```
|
||||
|
||||
Go to [http://localhost:7500](http://localhost:7500). Read the [tutorial](./tutorial.md) to learn more.
|
||||
|
||||
[^1]: I offer Paisa as a free app, and I don't generate any revenue
|
||||
from it. Code signing would require me to pay $99 for Mac and
|
||||
approximately $300 for Windows each and every year to get the
|
||||
necessary certificates. I can't justify spending that much for
|
||||
an app that doesn't generate any income. Unfortunately, as a
|
||||
result, you would have to jump through hoops to get it working.
|
||||
|
|
|
@ -4,17 +4,26 @@ This tutorial will introduce all the concepts necessary to get
|
|||
started. **Paisa** builds on top of **ledger**, so we will first spend
|
||||
some time to familiarize the [terms and concepts](https://github.com/ledger/ledger/blob/master/doc/GLOSSARY.md) used by **ledger**
|
||||
|
||||
## Journal
|
||||
!!! tip
|
||||
|
||||
Even though the tutorial focuses on Indian users, Paisa is
|
||||
capable of handling any currency. You can change the default
|
||||
currency, locale and financial year start etc. Check the
|
||||
[config](../reference/config.md) reference for more details.
|
||||
|
||||
|
||||
## :fontawesome-regular-file-lines: Journal
|
||||
|
||||
A journal file captures all your financial transactions. A transaction
|
||||
may represent a mutual fund purchase, EPF contribution and so
|
||||
on. Let's create a file named `personal.ledger` with the following
|
||||
content
|
||||
may represent a mutual fund purchase, retirement contribution, grocery
|
||||
purchase and so on. Paisa creates a journal named `main.ledger`, Let's
|
||||
add our first transaction there. To open the editor, go to `Ledger`
|
||||
:material-chevron-right: `Editor`
|
||||
|
||||
```ledger
|
||||
2022/01/01/*(1)!*/ Salary/*(2)!*/
|
||||
Income:Salary:Acme/*(3)!*/ -100,000 INR/*(6)!*/
|
||||
Assets:Checking/*(4)!*/ /*(5)!*/100,000 INR
|
||||
Income:Salary:Acme/*(3)!*/ -100,000 INR/*(6)!*/
|
||||
Assets:Checking/*(4)!*/ /*(5)!*/100,000 INR
|
||||
```
|
||||
|
||||
1. Transaction `Date`
|
||||
|
@ -26,185 +35,154 @@ content
|
|||
|
||||
**ledger** follows the double-entry accounting system. In simple terms, it
|
||||
tracks the movement of money from debit account to credit
|
||||
account. Here `Income:Salary:Acme` is the debit account and
|
||||
`Assets:Checking` is the credit account. The date at which the
|
||||
account. Here `#!ledger Income:Salary:Acme` is the debit account and
|
||||
`#!ledger Assets:Checking` is the credit account. The date at which the
|
||||
transaction took place and a description of the transaction is written
|
||||
in the first line followed by the list of credit or debit
|
||||
entry. Account [naming conventions](../reference/accounts.md) are explained later. The `:` in the account name
|
||||
represents hierarchy. Its use will become more clear when we look at
|
||||
the reports generated by ledger. The sum of the balance of all the
|
||||
accounts must be zero.
|
||||
represents hierarchy.
|
||||
|
||||
```ledger
|
||||
2022/01/01 Salary
|
||||
Income:Salary:Acme -100,000 INR
|
||||
Assets:Checking 100,000 INR
|
||||
Income:Salary:Acme -100,000 INR
|
||||
Assets:Checking 100,000 INR
|
||||
|
||||
2022/02/01 Salary
|
||||
Income:Salary:Acme -100,000 INR
|
||||
Assets:Checking 100,000 INR
|
||||
Income:Salary:Acme -100,000 INR
|
||||
Assets:Checking 100,000 INR
|
||||
|
||||
2022/03/01 Salary
|
||||
Income:Salary:Acme -100,000 INR
|
||||
Assets:Checking 100,000 INR
|
||||
Income:Salary:Acme -100,000 INR
|
||||
Assets:Checking 100,000 INR
|
||||
```
|
||||
|
||||
let's add few more entries. The total balance in your savings account
|
||||
could be found by
|
||||
Let's add few more transactions. As you edit your journal file, the
|
||||
balance of the journal will be shown on the right hand side.
|
||||
|
||||
```console
|
||||
# ledger -f personal.ledger balance Assets:Checking
|
||||
300,000 INR Assets:Checking
|
||||
```
|
||||
300,000 INR Assets:Checking
|
||||
-300,000 INR Income:Salary:Acme
|
||||
--------------------
|
||||
0
|
||||
```
|
||||
|
||||
Let's say your company deducts 12,000 INR and contributes it to EPF,
|
||||
You would notice zero balance and a checking account with 3 lakhs and
|
||||
an income account with -3 lakhs. Double-entry accounting will always
|
||||
results in 0 balance since you have to always enter both the credit
|
||||
and debit side.
|
||||
|
||||
|
||||
Let's say your company deducts `#!ledger 12,000 INR` and contributes it to EPF,
|
||||
we could represent it as follows
|
||||
|
||||
```ledger
|
||||
2022/01/01 Salary
|
||||
Income:Salary:Acme -100,000 INR
|
||||
Assets:Checking 88,000 INR
|
||||
Assets:Debt:EPF 12,000 INR
|
||||
Income:Salary:Acme -100,000 INR
|
||||
Assets:Checking 88,000 INR
|
||||
Assets:Debt:EPF 12,000 INR
|
||||
|
||||
2022/02/01 Salary
|
||||
Income:Salary:Acme -100,000 INR
|
||||
Assets:Checking 88,000 INR
|
||||
Assets:Debt:EPF 12,000 INR
|
||||
Income:Salary:Acme -100,000 INR
|
||||
Assets:Checking 88,000 INR
|
||||
Assets:Debt:EPF 12,000 INR
|
||||
|
||||
2022/03/01 Salary
|
||||
Income:Salary:Acme -100,000 INR
|
||||
Assets:Checking 88,000 INR
|
||||
Assets:Debt:EPF 12,000 INR
|
||||
Income:Salary:Acme -100,000 INR
|
||||
Assets:Checking 88,000 INR
|
||||
Assets:Debt:EPF 12,000 INR
|
||||
```
|
||||
|
||||
You can now see the use of `:` hierarchy in the account name.
|
||||
|
||||
```console
|
||||
# ledger -f personal.ledger balance Assets
|
||||
36,000 INR Assets:Debt:EPF
|
||||
264,000 INR Assets:Checking
|
||||
```
|
||||
300,000 INR Assets
|
||||
264,000 INR Checking
|
||||
36,000 INR Debt:EPF
|
||||
-300,000 INR Income:Salary:Acme
|
||||
--------------------
|
||||
300,000 INR
|
||||
0
|
||||
```
|
||||
|
||||
## Commodity
|
||||
## :material-gold: Commodity
|
||||
|
||||
So far we have only dealt with INR. **ledger** can handle commodity as
|
||||
well. Let's say you are also investing 10,000 INR in UTI Nifty Index
|
||||
Fund and 10,000 INR in ICICI Nifty Next 50 Index Fund every
|
||||
well. Let's say you are also investing `#!ledger 10,000 INR` in UTI Nifty Index
|
||||
Fund and `#!ledger 10,000 INR` in ICICI Nifty Next 50 Index Fund every
|
||||
month.
|
||||
|
||||
```ledger
|
||||
2018/01/01 Investment
|
||||
Assets:Checking -20,000 INR
|
||||
Assets:Equity:NIFTY 148.0865 NIFTY @ 67.5281 INR
|
||||
Assets:Equity:NIFTY_JR 358.6659 NIFTY_JR @ 27.8811 INR
|
||||
Assets:Checking -20,000 INR
|
||||
Assets:Equity:NIFTY 148.0865 NIFTY @ 67.5281 INR
|
||||
Assets:Equity:NIFTY_JR 358.6659 NIFTY_JR @ 27.8811 INR
|
||||
|
||||
2018/02/01 Investment
|
||||
Assets:Checking -20,000 INR
|
||||
Assets:Equity:NIFTY 140.2870 NIFTY @ 71.2824 INR
|
||||
Assets:Equity:NIFTY_JR 363.2242 NIFTY_JR @ 27.5312 INR
|
||||
Assets:Checking -20,000 INR
|
||||
Assets:Equity:NIFTY 140.2870 NIFTY @ 71.2824 INR
|
||||
Assets:Equity:NIFTY_JR 363.2242 NIFTY_JR @ 27.5312 INR
|
||||
|
||||
2018/03/01 Investment
|
||||
Assets:Checking -20,000 INR
|
||||
Assets:Equity:NIFTY 147.5908 NIFTY @ 67.7549 INR
|
||||
Assets:Equity:NIFTY_JR 378.4323 NIFTY_JR @ 26.4248 INR
|
||||
Assets:Checking -20,000 INR
|
||||
Assets:Equity:NIFTY 147.5908 NIFTY @ 67.7549 INR
|
||||
Assets:Equity:NIFTY_JR 378.4323 NIFTY_JR @ 26.4248 INR
|
||||
```
|
||||
|
||||
Let's consider `148.0865 NIFTY @ 67.5281 INR`. Here NIFTY is the name
|
||||
of the commodity and we have bought 148.0865 units at 67.5281 INR per
|
||||
unit.
|
||||
Let's consider `#!ledger 148.0865 NIFTY @ 67.5281 INR`. Here `#!ledger
|
||||
NIFTY` is the name of the commodity and we have bought `#!ledger
|
||||
148.0865` units at `#!ledger 67.5281 INR` per unit.
|
||||
|
||||
```console
|
||||
# ledger -f personal.ledger balance Assets --market
|
||||
94,615 INR Assets
|
||||
36,000 INR Debt:EPF
|
||||
58,615 INR Equity
|
||||
29,539 INR NIFTY
|
||||
29,076 INR NIFTY_JR
|
||||
--------------------
|
||||
94,615 INR
|
||||
```
|
||||
Paisa has support for fetching commodity price history from few
|
||||
[providers](../reference/commodities.md). Go to `Config` page and expand the `Commodities`
|
||||
section. You can click the :fontawesome-solid-circle-plus: icon to
|
||||
add a new one. Edit the name to `#!ledger NIFTY`. Click the
|
||||
:fontawesome-solid-pen-to-square: icon near Price section and select
|
||||
the price provider details. Once done, save the config and click the
|
||||
`Update Prices` from the top right hand side menu. If you had done
|
||||
everything correctly, you would see the latest price of the commodity
|
||||
under `Assets` :material-chevron-right: `Balance`
|
||||
|
||||
## Interest
|
||||
## :fontawesome-solid-hand-holding-dollar: Interest
|
||||
|
||||
There are many instruments like EPF, FD, etc that pay interest at
|
||||
regular intervals. We can treat it as just another transaction.
|
||||
regular intervals. We can treat it as just another transaction. Any
|
||||
income account that has a prefix `#!ledger Income:Interest:` can be
|
||||
used as the debit account. It's not mandatory to specify the amount at
|
||||
bot side. If you leave one side, **ledger** will deduct it.
|
||||
|
||||
```ledger
|
||||
2022/03/31 EPF Interest
|
||||
Income:Interest:EPF -15,000 INR
|
||||
Assets:Debt:EPF 15,000 INR
|
||||
Income:Interest:EPF -5,000 INR
|
||||
Assets:Debt:EPF
|
||||
```
|
||||
|
||||
```
|
||||
5,000 INR Assets:Debt:EPF
|
||||
-5,000 INR Income:Interest:EPF
|
||||
--------------------
|
||||
0
|
||||
```
|
||||
|
||||
## Config
|
||||
|
||||
So far we have been mostly talking about **ledger**. We need to know
|
||||
the basics of **ledger** because **paisa** builds on top of
|
||||
**ledger**.
|
||||
All the configuration related to paisa is stored in a yaml file name
|
||||
`paisa.yaml`. The config can be edited via the web interface. The
|
||||
sequence in which it look for the file is described below
|
||||
|
||||
**paisa** checks for a config file named `paisa.yaml` in the current
|
||||
directory. It can also be set via flag `--config` or env variable
|
||||
`PAISA_CONFIG`. The default config is tuned for Indians, users from
|
||||
other countries would have to change the `default_currency` and
|
||||
`locale`. Check the [config](../reference/config.md) section for details.
|
||||
|
||||
```yaml
|
||||
journal_path: /home/john/finance/personal.ledger # (1)!
|
||||
db_path: /home/john/finance/paisa.db # (2)!
|
||||
commodities: # (3)!
|
||||
- name: NIFTY
|
||||
type: mutualfund
|
||||
code: 120716
|
||||
- name: NIFTY_JR
|
||||
type: mutualfund
|
||||
code: 120684
|
||||
```
|
||||
|
||||
1. absolute path of journal file
|
||||
2. absolute path of sql database
|
||||
3. commodities list
|
||||
|
||||
|
||||
**paisa** can fetch the latest price of mutual funds, nps funds and
|
||||
stocks as of today. For other types of commodities, the purchase/sell
|
||||
price of the last transaction would be considered the latest
|
||||
price. The code is the scheme code of the fund. The *search* command
|
||||
can be used to find scheme code
|
||||
|
||||
```console
|
||||
# paisa search mutualfund
|
||||
INFO Using config file: /home/john/finance/paisa.yaml
|
||||
INFO Using cached results; pass '-u' to update the cache
|
||||
✔ ICICI Prudential Asset Management Company Limited
|
||||
✔ ICICI Prudential Nifty Next 50 Index Fund - Direct Plan - Growth
|
||||
INFO Mutual Fund Scheme Code: 120684
|
||||
```
|
||||
|
||||
|
||||
You might see a lot of funds with similar names. The search command
|
||||
supports crude fuzzy search. For example, `nifty next grow direct` would match the above fund.
|
||||
1. `PAISA_CONFIG` environment variable
|
||||
1. via --config flag
|
||||
1. Current working directory
|
||||
1. `paisa/paisa.yaml` folder inside User Documents folder.
|
||||
|
||||
If it can't find the config file, it will create a default config file
|
||||
`paisa/paisa.yaml` folder inside User Documents folder. The default
|
||||
config is tuned for Indians, users from other countries would have to
|
||||
change the `default_currency` and `locale`. Check the [config](../reference/config.md)
|
||||
reference for details.
|
||||
|
||||
## Update
|
||||
|
||||
Once the config file is created, run the update command, followed by
|
||||
the serve command.
|
||||
|
||||
```console
|
||||
# paisa update
|
||||
INFO Using config file: /home/john/finance/paisa.yaml
|
||||
INFO Syncing transactions from journal
|
||||
INFO Fetching commodities price history
|
||||
INFO Fetching commodity NIFTY
|
||||
INFO Fetching commodity NIFTY_JR
|
||||
# paisa serve
|
||||
INFO Using config file: /home/john/finance/paisa.yaml
|
||||
INFO Listening on 7500
|
||||
```
|
||||
|
||||
You can then go to [http://localhost:7500](http://localhost:7500) to view more
|
||||
details. The update command loads all the transactions from journal
|
||||
file and fetch the latest price of all the commodities. Whenever you
|
||||
make any changes to the journal file, you need to run the update
|
||||
command. The serve command starts a local server and provides the
|
||||
Web UI.
|
||||
**paisa** fetches the latest price of the commodities only when
|
||||
*update* command is used. Make sure to run `paisa update` command
|
||||
after you make any changes to your journal file or you want to fetch
|
||||
the latest value of the commodities. The update can be performed from
|
||||
the UI as well via the dropdown in the top right hand side corner.
|
||||
|
|
|
@ -25,5 +25,6 @@ class LedgerLexer(RegexLexer):
|
|||
(r'[$€]', Keyword),
|
||||
(r'\b[A-Z_]+\b', Keyword),
|
||||
(r'^\s{4}[^\][(); \t\n]((?!\s{2})[^/\][();\t\n])*', String),
|
||||
(r'^[^\][(); \t\n]((?!\s{2})[^/\][();\t\n])*$', String),
|
||||
]
|
||||
}
|
||||
|
|
|
@ -6,15 +6,15 @@ tracked as a commodity. Few example transactions can be found below.
|
|||
|
||||
```ledger
|
||||
2019/02/18 NPS
|
||||
Assets:Equity:NPS:SBI:E/*(1)!*/ /*(2)!*/15.9378 NPS_SBI_E/*(3)!*/ @ /*(4)!*/23.5289 INR/*(5)!*/
|
||||
Assets:Equity:NPS:SBI:E/*(1)!*/ /*(2)!*/15.9378 NPS_SBI_E/*(3)!*/ @ /*(4)!*/23.5289 INR/*(5)!*/
|
||||
Assets:Checking
|
||||
|
||||
2019/02/21 NPS
|
||||
Assets:Equity:NPS:SBI:E 1557.2175 NPS_SBI_E @ 23.8406 INR
|
||||
Assets:Equity:NPS:SBI:E 1557.2175 NPS_SBI_E @ 23.8406 INR
|
||||
Assets:Checking
|
||||
|
||||
2020/06/25 Gold
|
||||
Assets:Gold 40 GOLD @ 4650 INR
|
||||
Assets:Gold 40 GOLD @ 4650 INR
|
||||
Assets:Checking
|
||||
```
|
||||
|
||||
|
@ -31,7 +31,18 @@ when you enter the second NPS transaction on `2019/02/21`, the
|
|||
valuation of your existing holdings will be adjusted based on the new
|
||||
purchase price.
|
||||
|
||||
## Mutual Fund
|
||||
To link a commodity with a commodity price provider, Go to `Config`
|
||||
page and expand the `Commodities` section. You can click the
|
||||
:fontawesome-solid-circle-plus: icon to add a new one. Edit the name
|
||||
to commodity name. Click the :fontawesome-solid-pen-to-square: icon
|
||||
near Price section and select the price provider details. Once done,
|
||||
save the config and click the `Update Prices` from the top right hand
|
||||
side menu. If you had done everything correctly, you would see the
|
||||
latest price of the commodity under `Assets` :material-chevron-right:
|
||||
`Balance`. You can also view the full price history on `Ledger`
|
||||
:material-chevron-right: `Price`
|
||||
|
||||
## Mutual Fund <sub>:flag_in:</sub>
|
||||
|
||||
To automatically track the latest value of your mutual funds holdings,
|
||||
you need to link the commodity and the fund scheme code.
|
||||
|
@ -40,30 +51,20 @@ you need to link the commodity and the fund scheme code.
|
|||
commodities:
|
||||
- name: NIFTY # (1)!
|
||||
type: mutualfund # (2)!
|
||||
code: 120716 # (3)!
|
||||
- name: NIFTY_JR
|
||||
type: mutualfund
|
||||
code: 120684
|
||||
price:
|
||||
provider: in-mfapi # (3)!
|
||||
code: 120716 # (4)!
|
||||
```
|
||||
|
||||
1. commodity name
|
||||
1. commodity type
|
||||
1. price provider name
|
||||
1. mutual fund scheme code
|
||||
|
||||
The example config above links two commodities with their respective
|
||||
mutual fund scheme code. The scheme code can be found using the
|
||||
*search* command.
|
||||
The example config above links nifty commodity with the respective
|
||||
mutual fund scheme code.
|
||||
|
||||
```console
|
||||
# paisa search mutualfund
|
||||
INFO Using config file: /home/john/finance/paisa.yaml
|
||||
INFO Using cached results; pass '-u' to update the cache
|
||||
✔ ICICI Prudential Asset Management Company Limited
|
||||
✔ ICICI Prudential Nifty Next 50 Index Fund - Direct Plan - Growth
|
||||
INFO Mutual Fund Scheme Code: 120684
|
||||
```
|
||||
|
||||
## NPS
|
||||
## NPS <sub>:flag_in:</sub>
|
||||
|
||||
To automatically track the latest value of your nps funds holdings,
|
||||
you need to link the commodity and the fund scheme code.
|
||||
|
@ -72,27 +73,20 @@ you need to link the commodity and the fund scheme code.
|
|||
commodities:
|
||||
- name: NPS_HDFC_E # (1)!
|
||||
type: nps # (2)!
|
||||
code: SM008002 # (3)!
|
||||
price:
|
||||
provider: com-purifiedbytes-nps # (3)!
|
||||
code: SM008002 # (4)!
|
||||
```
|
||||
|
||||
1. commodity name
|
||||
1. type
|
||||
1. price provider name
|
||||
1. nps fund scheme code
|
||||
|
||||
The example config above links NPS fund commodity with their
|
||||
respective NPS fund scheme code. The scheme code can be found using
|
||||
the *search* command.
|
||||
respective NPS fund scheme code.
|
||||
|
||||
```console
|
||||
# paisa search nps
|
||||
INFO Using config file: /home/john/finance/paisa.yaml
|
||||
INFO Using cached results; pass '-u' to update the cache
|
||||
✔ HDFC Pension Management Company Limited
|
||||
✔ HDFC PENSION MANAGEMENT COMPANY LIMITED SCHEME C - TIER I
|
||||
INFO NPS Fund Scheme Code: SM008002
|
||||
```
|
||||
|
||||
## Stock
|
||||
## Stock <sub>:globe_with_meridians:</sub>
|
||||
|
||||
To automatically track the latest value of your stock holdings,
|
||||
you need to link the commodity and the stock ticker name.
|
||||
|
@ -101,12 +95,15 @@ you need to link the commodity and the stock ticker name.
|
|||
commodities:
|
||||
- name: APPLE # (1)!
|
||||
type: stock # (2)!
|
||||
code: AAPL # (3)!
|
||||
price:
|
||||
provider: com-yahoo # (3)!
|
||||
code: AAPL # (4)!
|
||||
```
|
||||
|
||||
1. commodity name
|
||||
2. type
|
||||
3. stock ticker code
|
||||
1. type
|
||||
1. price provider name
|
||||
1. stock ticker code
|
||||
|
||||
Stock prices are fetched from yahoo finance website. The ticker code
|
||||
should match the code used in yahoo.
|
||||
|
@ -140,18 +137,20 @@ default\_currency is available, paisa would work without issue
|
|||
P 2023/05/01 00:00:00 USD 81.75 INR
|
||||
|
||||
2023/05/01 Freelance Income
|
||||
;; conversion rate will be picked up from the price directive above
|
||||
Income:Freelance -100 USD
|
||||
;; conversion rate will be picked up
|
||||
;; from the price directive above
|
||||
Income:Freelance -100 USD
|
||||
Assets:Checking
|
||||
|
||||
2023/06/01 Freelance Income
|
||||
;; conversion rate is specified inline
|
||||
Income:Freelance -200 USD @ 82.75 INR
|
||||
Income:Freelance -200 USD @ 82.75 INR
|
||||
Assets:Checking
|
||||
|
||||
2023/07/01 Netflix
|
||||
;; if not available for a date, will use previous known conversion rate (82.75)
|
||||
Expenses:Entertainment 10 USD
|
||||
;; if not available for a date,
|
||||
;; will use previous known conversion rate (82.75)
|
||||
Expenses:Entertainment 10 USD
|
||||
Assets:Checking
|
||||
```
|
||||
|
||||
|
@ -161,4 +160,4 @@ P 2023/05/01 00:00:00 USD 81.75 INR
|
|||
*update* command is used. Make sure to run `paisa update` command
|
||||
after you make any changes to your journal file or you want to fetch
|
||||
the latest value of the commodities. The update can be performed from
|
||||
the UI as well via the dropdown in the top right corner.
|
||||
the UI as well via the dropdown in the top right hand side corner.
|
||||
|
|
|
@ -9,7 +9,7 @@ directory. It can also be set via flag `--config` or env variable
|
|||
# config file. The main journal file can refer other files using
|
||||
# `include` as long as all the files are in the same or sub directory
|
||||
# REQUIRED
|
||||
journal_path: /home/john/finance/personal.ledger
|
||||
journal_path: /home/john/finance/main.ledger
|
||||
|
||||
# Path to your database file. It can be absolute or relative to the
|
||||
# config file. The database file will be created if it does not exist.
|
||||
|
|
|
@ -17,7 +17,7 @@
|
|||
"tag_recurring": "",
|
||||
"transaction_begin_line": 13,
|
||||
"transaction_end_line": 15,
|
||||
"file_name": "personal.ledger",
|
||||
"file_name": "main.ledger",
|
||||
"market_amount": 0
|
||||
},
|
||||
{
|
||||
|
@ -33,14 +33,14 @@
|
|||
"tag_recurring": "",
|
||||
"transaction_begin_line": 13,
|
||||
"transaction_end_line": 15,
|
||||
"file_name": "personal.ledger",
|
||||
"file_name": "main.ledger",
|
||||
"market_amount": 0
|
||||
}
|
||||
],
|
||||
"tag_recurring": "",
|
||||
"beginLine": 13,
|
||||
"endLine": 15,
|
||||
"fileName": "personal.ledger"
|
||||
"fileName": "main.ledger"
|
||||
},
|
||||
{
|
||||
"id": "ae8edbd5-7bfc-5520-a2b5-fd1d7668adc4",
|
||||
|
@ -60,7 +60,7 @@
|
|||
"tag_recurring": "",
|
||||
"transaction_begin_line": 9,
|
||||
"transaction_end_line": 11,
|
||||
"file_name": "personal.ledger",
|
||||
"file_name": "main.ledger",
|
||||
"market_amount": 0
|
||||
},
|
||||
{
|
||||
|
@ -76,14 +76,14 @@
|
|||
"tag_recurring": "",
|
||||
"transaction_begin_line": 9,
|
||||
"transaction_end_line": 11,
|
||||
"file_name": "personal.ledger",
|
||||
"file_name": "main.ledger",
|
||||
"market_amount": 0
|
||||
}
|
||||
],
|
||||
"tag_recurring": "",
|
||||
"beginLine": 9,
|
||||
"endLine": 11,
|
||||
"fileName": "personal.ledger"
|
||||
"fileName": "main.ledger"
|
||||
},
|
||||
{
|
||||
"id": "97f59855-0f29-571c-a529-0fe288597b3b",
|
||||
|
@ -103,7 +103,7 @@
|
|||
"tag_recurring": "",
|
||||
"transaction_begin_line": 5,
|
||||
"transaction_end_line": 7,
|
||||
"file_name": "personal.ledger",
|
||||
"file_name": "main.ledger",
|
||||
"market_amount": 0
|
||||
},
|
||||
{
|
||||
|
@ -119,14 +119,14 @@
|
|||
"tag_recurring": "",
|
||||
"transaction_begin_line": 5,
|
||||
"transaction_end_line": 7,
|
||||
"file_name": "personal.ledger",
|
||||
"file_name": "main.ledger",
|
||||
"market_amount": 0
|
||||
}
|
||||
],
|
||||
"tag_recurring": "",
|
||||
"beginLine": 5,
|
||||
"endLine": 7,
|
||||
"fileName": "personal.ledger"
|
||||
"fileName": "main.ledger"
|
||||
},
|
||||
{
|
||||
"id": "185550f9-08e0-5df8-9381-21d689f889f6",
|
||||
|
@ -146,7 +146,7 @@
|
|||
"tag_recurring": "",
|
||||
"transaction_begin_line": 1,
|
||||
"transaction_end_line": 3,
|
||||
"file_name": "personal.ledger",
|
||||
"file_name": "main.ledger",
|
||||
"market_amount": 0
|
||||
},
|
||||
{
|
||||
|
@ -162,13 +162,13 @@
|
|||
"tag_recurring": "",
|
||||
"transaction_begin_line": 1,
|
||||
"transaction_end_line": 3,
|
||||
"file_name": "personal.ledger",
|
||||
"file_name": "main.ledger",
|
||||
"market_amount": 0
|
||||
}
|
||||
],
|
||||
"tag_recurring": "",
|
||||
"beginLine": 1,
|
||||
"endLine": 3,
|
||||
"fileName": "personal.ledger"
|
||||
"fileName": "main.ledger"
|
||||
}
|
||||
]
|
||||
|
|
|
@ -37,10 +37,15 @@ const (
|
|||
Unknown CommodityType = "unknown"
|
||||
)
|
||||
|
||||
type Price struct {
|
||||
Provider string `json:"provider" yaml:"provider"`
|
||||
Code string `json:"code" yaml:"code"`
|
||||
}
|
||||
|
||||
type Commodity struct {
|
||||
Name string `json:"name" yaml:"name"`
|
||||
Type CommodityType `json:"type" yaml:"type"`
|
||||
Code string `json:"code" yaml:"code"`
|
||||
Price Price `json:"price" yaml:"price"`
|
||||
Harvest int `json:"harvest" yaml:"harvest"`
|
||||
TaxCategory TaxCategoryType `json:"tax_category" yaml:"tax_category"`
|
||||
}
|
||||
|
|
|
@ -75,6 +75,7 @@
|
|||
],
|
||||
"items": {
|
||||
"type": "object",
|
||||
"ui:header": "code",
|
||||
"properties": {
|
||||
"code": {
|
||||
"type": "string",
|
||||
|
@ -108,6 +109,7 @@
|
|||
"default": [{ "name": "Debt", "target": 20, "accounts": ["Assets:Debt:*"] }],
|
||||
"items": {
|
||||
"type": "object",
|
||||
"ui:header": "name",
|
||||
"properties": {
|
||||
"name": {
|
||||
"type": "string"
|
||||
|
@ -134,10 +136,15 @@
|
|||
"commodities": {
|
||||
"type": "array",
|
||||
"default": [
|
||||
{ "name": "AAPL", "type": "stock", "code": "AAPL", "tax_category": "unlisted_equity" }
|
||||
{
|
||||
"name": "AAPL",
|
||||
"price": { "provider": "com-yahoo", "code": "AAPL" },
|
||||
"tax_category": "unlisted_equity"
|
||||
}
|
||||
],
|
||||
"items": {
|
||||
"type": "object",
|
||||
"ui:header": "name",
|
||||
"properties": {
|
||||
"name": {
|
||||
"type": "string",
|
||||
|
@ -147,9 +154,21 @@
|
|||
"type": "string",
|
||||
"enum": ["mutualfund", "stock", "nps", "unknown"]
|
||||
},
|
||||
"code": {
|
||||
"type": ["string", "integer"]
|
||||
"price": {
|
||||
"type": "object",
|
||||
"ui:widget": "price",
|
||||
"properties": {
|
||||
"provider": {
|
||||
"type": "string",
|
||||
"enum": ["in-mfapi", "com-yahoo", "com-purifiedbytes-nps"]
|
||||
},
|
||||
"code": {
|
||||
"type": ["string", "integer"]
|
||||
}
|
||||
},
|
||||
"required": ["provider", "code"]
|
||||
},
|
||||
|
||||
"harvest": {
|
||||
"type": "integer"
|
||||
},
|
||||
|
@ -158,7 +177,7 @@
|
|||
"enum": ["", "debt", "equity", "equity65", "equity35", "unlisted_equity"]
|
||||
}
|
||||
},
|
||||
"required": ["name", "type", "code"],
|
||||
"required": ["name", "type", "price"],
|
||||
"additionalProperties": false
|
||||
}
|
||||
}
|
||||
|
|
|
@ -43,7 +43,7 @@ journal_path: '%s'
|
|||
db_path: '%s'
|
||||
`
|
||||
log.Info("Generating config file: ", configFilePath)
|
||||
journalFilePath := filepath.Join(cwd, "personal.ledger")
|
||||
journalFilePath := filepath.Join(cwd, "main.ledger")
|
||||
dbFilePath := filepath.Join(cwd, "paisa.db")
|
||||
err := os.WriteFile(configFilePath, []byte(fmt.Sprintf(config, journalFilePath, dbFilePath)), 0644)
|
||||
if err != nil {
|
||||
|
@ -104,31 +104,43 @@ schedule_al:
|
|||
commodities:
|
||||
- name: NIFTY
|
||||
type: mutualfund
|
||||
code: 120716
|
||||
price:
|
||||
provider: in-mfapi
|
||||
code: 120716
|
||||
harvest: 365
|
||||
tax_category: equity65
|
||||
- name: PPFAS
|
||||
type: mutualfund
|
||||
code: 122639
|
||||
price:
|
||||
provider: in-mfapi
|
||||
code: 122639
|
||||
harvest: 365
|
||||
tax_category: equity65
|
||||
- name: ABCBF
|
||||
type: mutualfund
|
||||
code: 119533
|
||||
price:
|
||||
provider: in-mfapi
|
||||
code: 119533
|
||||
harvest: 1095
|
||||
tax_category: debt
|
||||
- name: NPS_HDFC_E
|
||||
type: nps
|
||||
code: SM008001
|
||||
price:
|
||||
provider: com-purifiedbytes-nps
|
||||
code: SM008001
|
||||
- name: NPS_HDFC_C
|
||||
type: nps
|
||||
code: SM008002
|
||||
price:
|
||||
provider: com-purifiedbytes-nps
|
||||
code: SM008002
|
||||
- name: NPS_HDFC_G
|
||||
type: nps
|
||||
code: SM008003
|
||||
price:
|
||||
provider: com-purifiedbytes-nps
|
||||
code: SM008003
|
||||
`
|
||||
log.Info("Generating config file: ", configFilePath)
|
||||
journalFilePath := filepath.Join(cwd, "personal.ledger")
|
||||
journalFilePath := filepath.Join(cwd, "main.ledger")
|
||||
dbFilePath := filepath.Join(cwd, "paisa.db")
|
||||
err := os.WriteFile(configFilePath, []byte(fmt.Sprintf(config, journalFilePath, dbFilePath)), 0644)
|
||||
if err != nil {
|
||||
|
@ -362,7 +374,7 @@ func emitInvestment(state *GeneratorState, start time.Time) {
|
|||
}
|
||||
|
||||
func generateJournalFile(cwd string) {
|
||||
journalFilePath := filepath.Join(cwd, "personal.ledger")
|
||||
journalFilePath := filepath.Join(cwd, "main.ledger")
|
||||
log.Info("Generating journal file: ", journalFilePath)
|
||||
ledgerFile, err := os.OpenFile(journalFilePath, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0644)
|
||||
if err != nil {
|
||||
|
|
|
@ -15,7 +15,7 @@ func FindByName(name string) config.Commodity {
|
|||
}
|
||||
|
||||
func FindByCode(name string) config.Commodity {
|
||||
c, _ := lo.Find(All(), func(c config.Commodity) bool { return c.Code == name })
|
||||
c, _ := lo.Find(All(), func(c config.Commodity) bool { return c.Price.Code == name })
|
||||
return c
|
||||
}
|
||||
|
||||
|
|
|
@ -55,7 +55,7 @@ func SyncCommodities(db *gorm.DB) {
|
|||
for _, commodity := range commodities {
|
||||
name := commodity.Name
|
||||
log.Info("Fetching commodity ", aurora.Bold(name))
|
||||
code := commodity.Code
|
||||
code := commodity.Price.Code
|
||||
var prices []*price.Price
|
||||
var err error
|
||||
|
||||
|
@ -93,12 +93,12 @@ func SyncPortfolios(db *gorm.DB) {
|
|||
for _, commodity := range commodities {
|
||||
name := commodity.Name
|
||||
log.Info("Fetching portfolio for ", aurora.Bold(name))
|
||||
portfolios, err := mutualfund.GetPortfolio(commodity.Code, commodity.Name)
|
||||
portfolios, err := mutualfund.GetPortfolio(commodity.Price.Code, commodity.Name)
|
||||
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
portfolio.UpsertAll(db, commodity.Type, commodity.Code, portfolios)
|
||||
portfolio.UpsertAll(db, commodity.Type, commodity.Price.Code, portfolios)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
package scheme
|
||||
|
||||
import (
|
||||
"github.com/ananthakumaran/paisa/internal/model/price"
|
||||
"github.com/samber/lo"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
@ -42,23 +44,18 @@ func UpsertAll(db *gorm.DB, schemes []*Scheme) {
|
|||
}
|
||||
}
|
||||
|
||||
func GetAMCs(db *gorm.DB) []string {
|
||||
func GetAMCCompletions(db *gorm.DB) []price.AutoCompleteItem {
|
||||
var amcs []string
|
||||
db.Model(&Scheme{}).Distinct().Pluck("AMC", &amcs)
|
||||
return amcs
|
||||
return lo.Map(amcs, func(amc string, _ int) price.AutoCompleteItem {
|
||||
return price.AutoCompleteItem{Label: amc, ID: amc}
|
||||
})
|
||||
}
|
||||
|
||||
func GetNAVNames(db *gorm.DB, amc string) []string {
|
||||
var navNames []string
|
||||
db.Model(&Scheme{}).Where("amc = ? and type = 'Open Ended'", amc).Pluck("NAVName", &navNames)
|
||||
return navNames
|
||||
}
|
||||
|
||||
func FindScheme(db *gorm.DB, amc string, NAVName string) Scheme {
|
||||
var scheme Scheme
|
||||
result := db.Where("amc = ? and nav_name = ?", amc, NAVName).First(&scheme)
|
||||
if result.Error != nil {
|
||||
log.Fatal(result.Error)
|
||||
}
|
||||
return scheme
|
||||
func GetNAVNameCompletions(db *gorm.DB, amc string) []price.AutoCompleteItem {
|
||||
var schemes []Scheme
|
||||
db.Model(&Scheme{}).Where("amc = ? and type = 'Open Ended'", amc).Find(&schemes)
|
||||
return lo.Map(schemes, func(scheme Scheme, _ int) price.AutoCompleteItem {
|
||||
return price.AutoCompleteItem{Label: scheme.NAVName, ID: scheme.Code}
|
||||
})
|
||||
}
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
package scheme
|
||||
|
||||
import (
|
||||
"github.com/ananthakumaran/paisa/internal/model/price"
|
||||
"github.com/samber/lo"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
@ -43,23 +45,18 @@ func UpsertAll(db *gorm.DB, schemes []*Scheme) {
|
|||
}
|
||||
}
|
||||
|
||||
func GetPFMs(db *gorm.DB) []string {
|
||||
func GetPFMCompletions(db *gorm.DB) []price.AutoCompleteItem {
|
||||
var pfms []string
|
||||
db.Model(&Scheme{}).Distinct().Pluck("PFMName", &pfms)
|
||||
return pfms
|
||||
return lo.Map(pfms, func(pfm string, _ int) price.AutoCompleteItem {
|
||||
return price.AutoCompleteItem{Label: pfm, ID: pfm}
|
||||
})
|
||||
}
|
||||
|
||||
func GetSchemeNames(db *gorm.DB, pfm string) []string {
|
||||
var schemeNames []string
|
||||
db.Model(&Scheme{}).Where("pfm_name = ?", pfm).Pluck("SchemeName", &schemeNames)
|
||||
return schemeNames
|
||||
}
|
||||
|
||||
func FindScheme(db *gorm.DB, pfm string, schemeName string) Scheme {
|
||||
var scheme Scheme
|
||||
result := db.Where("pfm_name = ? and scheme_name = ?", pfm, schemeName).First(&scheme)
|
||||
if result.Error != nil {
|
||||
log.Fatal(result.Error)
|
||||
}
|
||||
return scheme
|
||||
func GetSchemeNameCompletions(db *gorm.DB, pfm string) []price.AutoCompleteItem {
|
||||
var schemes []Scheme
|
||||
db.Model(&Scheme{}).Where("pfm_name = ?", pfm).Find(&schemes)
|
||||
return lo.Map(schemes, func(scheme Scheme, _ int) price.AutoCompleteItem {
|
||||
return price.AutoCompleteItem{Label: scheme.SchemeName, ID: scheme.SchemeID}
|
||||
})
|
||||
}
|
||||
|
|
|
@ -0,0 +1,23 @@
|
|||
package price
|
||||
|
||||
import "gorm.io/gorm"
|
||||
|
||||
type AutoCompleteItem struct {
|
||||
Label string `json:"label"`
|
||||
ID string `json:"id"`
|
||||
}
|
||||
|
||||
type AutoCompleteField struct {
|
||||
Label string `json:"label"`
|
||||
ID string `json:"id"`
|
||||
Help string `json:"help"`
|
||||
InputType string `json:"inputType"`
|
||||
}
|
||||
|
||||
type PriceProvider interface {
|
||||
Code() string
|
||||
Label() string
|
||||
AutoCompleteFields() []AutoCompleteField
|
||||
AutoComplete(db *gorm.DB, field string, filter map[string]string) []AutoCompleteItem
|
||||
ClearCache(db *gorm.DB)
|
||||
}
|
|
@ -0,0 +1,51 @@
|
|||
package mutualfund
|
||||
|
||||
import (
|
||||
"github.com/ananthakumaran/paisa/internal/model/mutualfund/scheme"
|
||||
"github.com/ananthakumaran/paisa/internal/model/price"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type PriceProvider struct {
|
||||
}
|
||||
|
||||
func (p *PriceProvider) Code() string {
|
||||
return "in-mfapi"
|
||||
}
|
||||
|
||||
func (p *PriceProvider) Label() string {
|
||||
return "MF API India"
|
||||
}
|
||||
|
||||
func (p *PriceProvider) AutoCompleteFields() []price.AutoCompleteField {
|
||||
return []price.AutoCompleteField{
|
||||
{Label: "AMC", ID: "amc", Help: "Asset Management Company"},
|
||||
{Label: "Fund Name", ID: "scheme"},
|
||||
}
|
||||
}
|
||||
|
||||
func (p *PriceProvider) AutoComplete(db *gorm.DB, field string, filter map[string]string) []price.AutoCompleteItem {
|
||||
count := scheme.Count(db)
|
||||
if count == 0 {
|
||||
schemes, err := GetSchemes()
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
scheme.UpsertAll(db, schemes)
|
||||
} else {
|
||||
log.Info("Using cached results")
|
||||
}
|
||||
|
||||
switch field {
|
||||
case "amc":
|
||||
return scheme.GetAMCCompletions(db)
|
||||
case "scheme":
|
||||
return scheme.GetNAVNameCompletions(db, filter["amc"])
|
||||
}
|
||||
return []price.AutoCompleteItem{}
|
||||
}
|
||||
|
||||
func (p *PriceProvider) ClearCache(db *gorm.DB) {
|
||||
db.Exec("DELETE FROM schemes")
|
||||
}
|
|
@ -0,0 +1,51 @@
|
|||
package nps
|
||||
|
||||
import (
|
||||
"github.com/ananthakumaran/paisa/internal/model/nps/scheme"
|
||||
"github.com/ananthakumaran/paisa/internal/model/price"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type PriceProvider struct {
|
||||
}
|
||||
|
||||
func (p *PriceProvider) Code() string {
|
||||
return "com-purifiedbytes-nps"
|
||||
}
|
||||
|
||||
func (p *PriceProvider) Label() string {
|
||||
return "Purified Bytes NPS India"
|
||||
}
|
||||
|
||||
func (p *PriceProvider) AutoCompleteFields() []price.AutoCompleteField {
|
||||
return []price.AutoCompleteField{
|
||||
{Label: "PFM", ID: "pfm", Help: "Pension Fund Manager"},
|
||||
{Label: "Scheme Name", ID: "scheme"},
|
||||
}
|
||||
}
|
||||
|
||||
func (p *PriceProvider) AutoComplete(db *gorm.DB, field string, filter map[string]string) []price.AutoCompleteItem {
|
||||
count := scheme.Count(db)
|
||||
if count == 0 {
|
||||
schemes, err := GetSchemes()
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
scheme.UpsertAll(db, schemes)
|
||||
} else {
|
||||
log.Info("Using cached results")
|
||||
}
|
||||
|
||||
switch field {
|
||||
case "pfm":
|
||||
return scheme.GetPFMCompletions(db)
|
||||
case "scheme":
|
||||
return scheme.GetSchemeNameCompletions(db, filter["pfm"])
|
||||
}
|
||||
return []price.AutoCompleteItem{}
|
||||
}
|
||||
|
||||
func (p *PriceProvider) ClearCache(db *gorm.DB) {
|
||||
db.Exec("DELETE FROM nps_schemes")
|
||||
}
|
|
@ -0,0 +1,31 @@
|
|||
package scraper
|
||||
|
||||
import (
|
||||
"github.com/ananthakumaran/paisa/internal/model/price"
|
||||
"github.com/ananthakumaran/paisa/internal/scraper/mutualfund"
|
||||
"github.com/ananthakumaran/paisa/internal/scraper/nps"
|
||||
"github.com/ananthakumaran/paisa/internal/scraper/stock"
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
func GetAllProviders() []price.PriceProvider {
|
||||
return []price.PriceProvider{
|
||||
&mutualfund.PriceProvider{},
|
||||
&nps.PriceProvider{},
|
||||
&stock.PriceProvider{},
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func GetProviderByCode(code string) price.PriceProvider {
|
||||
switch code {
|
||||
case "in-mfapi":
|
||||
return &mutualfund.PriceProvider{}
|
||||
case "com-purifiedbytes-nps":
|
||||
return &nps.PriceProvider{}
|
||||
case "com-yahoo":
|
||||
return &stock.PriceProvider{}
|
||||
}
|
||||
log.Fatal("Unknown price provider: ", code)
|
||||
return nil
|
||||
}
|
|
@ -0,0 +1,30 @@
|
|||
package stock
|
||||
|
||||
import (
|
||||
"github.com/ananthakumaran/paisa/internal/model/price"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type PriceProvider struct {
|
||||
}
|
||||
|
||||
func (p *PriceProvider) Code() string {
|
||||
return "com-yahoo"
|
||||
}
|
||||
|
||||
func (p *PriceProvider) Label() string {
|
||||
return "Yahoo Finance"
|
||||
}
|
||||
|
||||
func (p *PriceProvider) AutoCompleteFields() []price.AutoCompleteField {
|
||||
return []price.AutoCompleteField{
|
||||
{Label: "Ticker", ID: "ticker", Help: "Stock ticker symbol", InputType: "text"},
|
||||
}
|
||||
}
|
||||
|
||||
func (p *PriceProvider) AutoComplete(db *gorm.DB, field string, filter map[string]string) []price.AutoCompleteItem {
|
||||
return []price.AutoCompleteItem{}
|
||||
}
|
||||
|
||||
func (p *PriceProvider) ClearCache(db *gorm.DB) {
|
||||
}
|
|
@ -142,7 +142,7 @@ func GetAccountPortfolioAllocation(db *gorm.DB, account string) PortfolioAllocat
|
|||
|
||||
func computePortfolioAggregate(db *gorm.DB, commodityName string, total decimal.Decimal) []CommodityBreakdown {
|
||||
commodity := commodity.FindByName(commodityName)
|
||||
portfolios := portfolio.GetPortfolios(db, commodity.Code)
|
||||
portfolios := portfolio.GetPortfolios(db, commodity.Price.Code)
|
||||
return lo.Map(portfolios, func(p portfolio.Portfolio, _ int) CommodityBreakdown {
|
||||
amount := (total.Mul(p.Percentage)).Div(decimal.NewFromInt(100))
|
||||
return CommodityBreakdown{
|
||||
|
|
|
@ -1,11 +1,15 @@
|
|||
package server
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/samber/lo"
|
||||
log "github.com/sirupsen/logrus"
|
||||
|
||||
"github.com/ananthakumaran/paisa/internal/config"
|
||||
"github.com/ananthakumaran/paisa/internal/model/posting"
|
||||
"github.com/ananthakumaran/paisa/internal/model/price"
|
||||
"github.com/ananthakumaran/paisa/internal/scraper"
|
||||
"github.com/ananthakumaran/paisa/internal/service"
|
||||
"github.com/gin-gonic/gin"
|
||||
"gorm.io/gorm"
|
||||
|
@ -24,3 +28,44 @@ func GetPrices(db *gorm.DB) gin.H {
|
|||
}
|
||||
return gin.H{"prices": prices}
|
||||
}
|
||||
|
||||
type AutoCompleteRequest struct {
|
||||
Provider string `json:"provider"`
|
||||
Field string `json:"field"`
|
||||
Filters map[string]string `json:"filters"`
|
||||
}
|
||||
|
||||
func GetPriceProviders(db *gorm.DB) gin.H {
|
||||
providers := scraper.GetAllProviders()
|
||||
return gin.H{
|
||||
"providers": lo.Map(providers, func(provider price.PriceProvider, _ int) gin.H {
|
||||
return gin.H{"code": provider.Code(), "label": provider.Label(), "fields": provider.AutoCompleteFields()}
|
||||
}),
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func ClearPriceProviderCache(db *gorm.DB, code string) gin.H {
|
||||
provider := scraper.GetProviderByCode(code)
|
||||
provider.ClearCache(db)
|
||||
return gin.H{}
|
||||
}
|
||||
|
||||
func GetPriceAutoCompletions(db *gorm.DB, request AutoCompleteRequest) gin.H {
|
||||
provider := scraper.GetProviderByCode(request.Provider)
|
||||
completions := provider.AutoComplete(db, request.Field, request.Filters)
|
||||
|
||||
completions = lo.Filter(completions, func(completion price.AutoCompleteItem, _ int) bool {
|
||||
item := completion.Label
|
||||
item = strings.Replace(strings.ToLower(item), " ", "", -1)
|
||||
words := strings.Split(strings.ToLower(request.Filters[request.Field]), " ")
|
||||
for _, word := range words {
|
||||
if strings.TrimSpace(word) != "" && !strings.Contains(item, word) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
})
|
||||
|
||||
return gin.H{"completions": completions}
|
||||
}
|
||||
|
|
|
@ -116,6 +116,24 @@ func Build(db *gorm.DB) *gin.Engine {
|
|||
router.GET("/api/price", func(c *gin.Context) {
|
||||
c.JSON(200, GetPrices(db))
|
||||
})
|
||||
router.GET("/api/price/providers", func(c *gin.Context) {
|
||||
c.JSON(200, GetPriceProviders(db))
|
||||
})
|
||||
|
||||
router.POST("/api/price/providers/delete/:provider", func(c *gin.Context) {
|
||||
provider := c.Param("provider")
|
||||
c.JSON(200, ClearPriceProviderCache(db, provider))
|
||||
})
|
||||
|
||||
router.POST("/api/price/autocomplete", func(c *gin.Context) {
|
||||
var autoCompleteRequest AutoCompleteRequest
|
||||
if err := c.ShouldBindJSON(&autoCompleteRequest); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(200, GetPriceAutoCompletions(db, autoCompleteRequest))
|
||||
})
|
||||
router.GET("/api/transaction", func(c *gin.Context) {
|
||||
c.JSON(200, GetTransactions(db))
|
||||
})
|
||||
|
@ -233,7 +251,7 @@ func Build(db *gorm.DB) *gin.Engine {
|
|||
func Listen(db *gorm.DB, port int) {
|
||||
router := Build(db)
|
||||
|
||||
log.Info("Listening on ", port)
|
||||
log.Infof("Listening on http://localhost:%d", port)
|
||||
err := router.Run(fmt.Sprintf(":%d", port))
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
|
|
|
@ -56,7 +56,7 @@ theme:
|
|||
- search.highlight
|
||||
|
||||
markdown_extensions:
|
||||
- md_in_html
|
||||
- footnotes
|
||||
- pymdownx.tabbed:
|
||||
alternate_style: true
|
||||
- pymdownx.highlight:
|
||||
|
|
|
@ -20,7 +20,7 @@ describe("bulk_editor", () => {
|
|||
const after = fs.readFileSync(`fixture/bulk_edit/${dir}/${name}.ledger`).toString();
|
||||
const ledgerFile: LedgerFile = {
|
||||
type: "file",
|
||||
name: "personal.ledger",
|
||||
name: "main.ledger",
|
||||
content: before.toString(),
|
||||
versions: []
|
||||
};
|
||||
|
|
|
@ -1,13 +1,22 @@
|
|||
<script lang="ts">
|
||||
import type { JSONSchema7 } from "json-schema";
|
||||
import _ from "lodash";
|
||||
import PriceCodeSearchModal from "./PriceCodeSearchModal.svelte";
|
||||
|
||||
interface Schema extends JSONSchema7 {
|
||||
"ui:header"?: string;
|
||||
"ui:widget"?: string;
|
||||
}
|
||||
|
||||
export let key: string;
|
||||
export let value: any;
|
||||
export let schema: JSONSchema7;
|
||||
export let schema: Schema;
|
||||
export let depth: number = 0;
|
||||
export let required = false;
|
||||
export let deletable: () => void = null;
|
||||
export let disabled: boolean = false;
|
||||
|
||||
export let modalOpen = false;
|
||||
|
||||
let open = depth < 1;
|
||||
$: title = _.startCase(key);
|
||||
|
@ -16,20 +25,17 @@
|
|||
return _.cloneDeep(schema.default[0]);
|
||||
}
|
||||
|
||||
function sortedProperties(schema: JSONSchema7) {
|
||||
return _.sortBy(
|
||||
Object.entries(schema.properties),
|
||||
([key, subSchema]: [string, JSONSchema7]) => {
|
||||
return [
|
||||
_.includes(schema.required || [], key) ? 0 : 1,
|
||||
subSchema.type == "object" ? 2 : subSchema.type == "array" ? 3 : 1,
|
||||
key
|
||||
];
|
||||
}
|
||||
);
|
||||
function sortedProperties(schema: Schema) {
|
||||
return _.sortBy(Object.entries(schema.properties), ([key, subSchema]: [string, Schema]) => {
|
||||
return [
|
||||
_.includes(schema.required || [], key) ? 0 : 1,
|
||||
subSchema.type == "object" ? 2 : subSchema.type == "array" ? 3 : 1,
|
||||
key
|
||||
];
|
||||
});
|
||||
}
|
||||
|
||||
function documentation(schema: JSONSchema7) {
|
||||
function documentation(schema: Schema) {
|
||||
if (schema.description) {
|
||||
return `<p style="max-width: 300px">${schema.description}</p>`;
|
||||
}
|
||||
|
@ -55,7 +61,7 @@
|
|||
<div class="control">
|
||||
{#if schema.enum}
|
||||
<div class="select is-small">
|
||||
<select bind:value {required}>
|
||||
<select {disabled} bind:value {required}>
|
||||
{#each schema.enum as option}
|
||||
<option value={option}>{option}</option>
|
||||
{/each}
|
||||
|
@ -63,6 +69,7 @@
|
|||
</div>
|
||||
{:else}
|
||||
<input
|
||||
{disabled}
|
||||
{required}
|
||||
pattern={schema.pattern}
|
||||
class="input is-small"
|
||||
|
@ -97,6 +104,39 @@
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{:else if schema["ui:widget"] == "price"}
|
||||
<div class="config-header">
|
||||
<a class="is-link" data-tippy-content={documentation(schema)}>
|
||||
<span>{title}</span>
|
||||
</a>
|
||||
|
||||
<a on:click={(_e) => (modalOpen = true)} class="is-link">
|
||||
<span class="icon is-small">
|
||||
<i class="fas fa-pen-to-square"></i>
|
||||
</span>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<PriceCodeSearchModal
|
||||
bind:open={modalOpen}
|
||||
on:select={(e) => {
|
||||
value["code"] = e.detail.code;
|
||||
value["provider"] = e.detail.provider;
|
||||
}}
|
||||
/>
|
||||
|
||||
<div class="config-body {depth % 2 == 1 ? 'odd' : 'even'}">
|
||||
{#each sortedProperties(schema) as [key, subSchema]}
|
||||
<svelte:self
|
||||
required={_.includes(schema.required || [], key)}
|
||||
depth={depth + 1}
|
||||
{key}
|
||||
bind:value={value[key]}
|
||||
schema={subSchema}
|
||||
disabled={true}
|
||||
/>
|
||||
{/each}
|
||||
</div>
|
||||
{:else if schema.type == "object"}
|
||||
<div class="config-header">
|
||||
<a
|
||||
|
@ -104,7 +144,7 @@
|
|||
data-tippy-content={documentation(schema)}
|
||||
on:click={(_e) => (open = !open)}
|
||||
>
|
||||
<span>{value.name || value.code || title}</span>
|
||||
<span>{schema["ui:header"] ? value[schema["ui:header"]] || title : title}</span>
|
||||
<span class="icon is-small">
|
||||
<i class="fas {open ? 'fa-angle-up' : 'fa-angle-down'}"></i>
|
||||
</span>
|
||||
|
|
|
@ -0,0 +1,162 @@
|
|||
<script lang="ts">
|
||||
import Select from "svelte-select";
|
||||
import Modal from "$lib/components/Modal.svelte";
|
||||
import _ from "lodash";
|
||||
import { createEventDispatcher, onMount } from "svelte";
|
||||
import { ajax, type AutoCompleteItem, type PriceProvider } from "$lib/utils";
|
||||
|
||||
let label = "Choose Price Provider";
|
||||
export let open = false;
|
||||
let code = "";
|
||||
|
||||
let providers: PriceProvider[] = [];
|
||||
let selectedProvider: PriceProvider = null;
|
||||
|
||||
let filters: Record<string, AutoCompleteItem> = {};
|
||||
|
||||
onMount(async () => {
|
||||
({ providers } = await ajax("/api/price/providers"));
|
||||
selectedProvider = providers[0];
|
||||
});
|
||||
|
||||
let isLoading = false;
|
||||
async function clearProviderCache() {
|
||||
isLoading = true;
|
||||
try {
|
||||
await ajax(
|
||||
"/api/price/providers/delete/:provider",
|
||||
{ method: "POST" },
|
||||
{ provider: selectedProvider.code }
|
||||
);
|
||||
} finally {
|
||||
isLoading = false;
|
||||
}
|
||||
}
|
||||
|
||||
let autocompleteCache: number[] = [];
|
||||
function clearCache(i: number) {
|
||||
autocompleteCache[i] = (autocompleteCache[i] || 0) + 1;
|
||||
}
|
||||
|
||||
function reset() {
|
||||
code = "";
|
||||
filters = {};
|
||||
for (let i = 0; i < _.max(_.map(providers, (p) => p.fields.length)); i++) {
|
||||
clearCache(i);
|
||||
}
|
||||
}
|
||||
|
||||
function makeAutoComplete(
|
||||
field: string,
|
||||
filters: Record<string, AutoCompleteItem>,
|
||||
i: number,
|
||||
provider: PriceProvider
|
||||
) {
|
||||
return async function autocomplete(filterText: string): Promise<AutoCompleteItem[]> {
|
||||
for (let j = 0; j < i; j++) {
|
||||
if (_.isEmpty(filters[provider.fields[j].id])) {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
const queryFilters = _.mapValues(filters, (v) => v?.id);
|
||||
queryFilters[field] = filterText;
|
||||
const { completions } = await ajax("/api/price/autocomplete", {
|
||||
method: "POST",
|
||||
body: JSON.stringify({
|
||||
field,
|
||||
provider: selectedProvider.code,
|
||||
filters: queryFilters
|
||||
})
|
||||
});
|
||||
return completions;
|
||||
};
|
||||
}
|
||||
|
||||
const dispatch = createEventDispatcher();
|
||||
</script>
|
||||
|
||||
<Modal bind:active={open} footerClass="is-justify-content-space-between">
|
||||
<svelte:fragment slot="head" let:close>
|
||||
<p class="modal-card-title">{label}</p>
|
||||
<button class="delete" aria-label="close" on:click={(e) => close(e)} />
|
||||
</svelte:fragment>
|
||||
<div style="min-height: 500px;" slot="body">
|
||||
{#if selectedProvider}
|
||||
<div class="field">
|
||||
<label class="label" for="price-provider">Provider</label>
|
||||
<div class="control" id="price-provider">
|
||||
<div class="select">
|
||||
<select bind:value={selectedProvider} required on:change={(_e) => reset()}>
|
||||
{#each providers as provider}
|
||||
<option value={provider}>{provider.label}</option>
|
||||
{/each}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="field">
|
||||
{#each selectedProvider.fields as field, i}
|
||||
<div class="field">
|
||||
<label class="label" for="">{field.label}</label>
|
||||
<div class="control">
|
||||
{#if field.inputType == "text"}
|
||||
<input class="input" type="text" bind:value={code} required />
|
||||
{:else}
|
||||
{#key autocompleteCache[i]}
|
||||
<Select
|
||||
bind:value={filters[field.id]}
|
||||
--list-z-index="5"
|
||||
showChevron={true}
|
||||
loadOptions={makeAutoComplete(field.id, filters, i, selectedProvider)}
|
||||
label="label"
|
||||
itemId="id"
|
||||
searchable={true}
|
||||
clearable={false}
|
||||
on:change={() => {
|
||||
_.each(selectedProvider.fields, (f, j) => {
|
||||
if (j > i) {
|
||||
clearCache(j);
|
||||
filters[f.id] = null;
|
||||
}
|
||||
});
|
||||
|
||||
if (i === selectedProvider.fields.length - 1) {
|
||||
code = filters[field.id].id;
|
||||
} else {
|
||||
code = "";
|
||||
}
|
||||
}}
|
||||
></Select>
|
||||
{/key}
|
||||
{/if}
|
||||
<p class="help">{field.help}</p>
|
||||
</div>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
<svelte:fragment slot="foot" let:close>
|
||||
<div>
|
||||
<button
|
||||
class="button is-success"
|
||||
disabled={_.isEmpty(code)}
|
||||
on:click={(e) => {
|
||||
dispatch("select", { code: code, provider: selectedProvider.code });
|
||||
reset();
|
||||
close(e);
|
||||
}}>Select</button
|
||||
>
|
||||
<button class="button" on:click={(e) => close(e)}>Cancel</button>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<button
|
||||
on:click={(_e) => clearProviderCache()}
|
||||
class="button is-danger {isLoading && 'is-loading'}"
|
||||
disabled={!selectedProvider}>Clear Provider Cache</button
|
||||
>
|
||||
</div>
|
||||
</svelte:fragment>
|
||||
</Modal>
|
|
@ -5,6 +5,24 @@ import * as d3 from "d3";
|
|||
import { loading } from "../store";
|
||||
import type { JSONSchema7 } from "json-schema";
|
||||
|
||||
export interface AutoCompleteItem {
|
||||
label: string;
|
||||
id: string;
|
||||
}
|
||||
|
||||
export interface AutoCompleteField {
|
||||
id: string;
|
||||
label: string;
|
||||
help: string;
|
||||
inputType: string;
|
||||
}
|
||||
|
||||
export interface PriceProvider {
|
||||
code: string;
|
||||
fields: AutoCompleteField[];
|
||||
label: string;
|
||||
}
|
||||
|
||||
export interface Posting {
|
||||
id: string;
|
||||
date: dayjs.Dayjs;
|
||||
|
@ -386,7 +404,9 @@ const BACKGROUND = [
|
|||
"/api/editor/file/delete_backups",
|
||||
"/api/templates",
|
||||
"/api/templates/upsert",
|
||||
"/api/templates/delete"
|
||||
"/api/templates/delete",
|
||||
"/api/price/autocomplete",
|
||||
"/api/price/providers/delete/:provider"
|
||||
];
|
||||
|
||||
export function ajax(route: "/api/config"): Promise<{ config: UserConfig; schema: JSONSchema7 }>;
|
||||
|
@ -437,6 +457,7 @@ export function ajax(
|
|||
portfolio_allocation: PortfolioAllocation;
|
||||
asset_breakdown: AssetBreakdown;
|
||||
}>;
|
||||
|
||||
export function ajax(route: "/api/allocation"): Promise<{
|
||||
aggregates: { [key: string]: Aggregate };
|
||||
aggregates_timeline: { [key: string]: Aggregate }[];
|
||||
|
@ -514,6 +535,25 @@ export function ajax(
|
|||
): Promise<{ file: LedgerFile }>;
|
||||
|
||||
export function ajax(route: "/api/sync", options?: RequestInit): Promise<any>;
|
||||
export function ajax(
|
||||
route: "/api/price/providers",
|
||||
options?: RequestInit
|
||||
): Promise<{ providers: PriceProvider[] }>;
|
||||
|
||||
export function ajax(
|
||||
route: "/api/price/providers/delete/:provider",
|
||||
options?: RequestInit,
|
||||
params?: Record<string, string>
|
||||
): Promise<{
|
||||
gain_timeline_breakdown: AccountGain;
|
||||
portfolio_allocation: PortfolioAllocation;
|
||||
asset_breakdown: AssetBreakdown;
|
||||
}>;
|
||||
|
||||
export function ajax(
|
||||
route: "/api/price/autocomplete",
|
||||
options?: RequestInit
|
||||
): Promise<{ completions: AutoCompleteItem[] }>;
|
||||
export function ajax(route: "/api/init", options?: RequestInit): Promise<any>;
|
||||
|
||||
export function ajax(
|
||||
|
|
Loading…
Reference in New Issue