Compare commits

...

4 Commits

Author SHA1 Message Date
Anantha Kumaran 64bee2c438 bump version 2024-02-02 20:57:09 +05:30
Anantha Kumaran 798154bdf0 [cache] cache XIRR computation
refer #127
2024-02-02 19:57:38 +05:30
Anantha Kumaran 4551a854bf [income statement] use second level breakdown on tooltips 2024-02-01 08:52:43 +05:30
Anantha Kumaran 29a77e2775 [editor] fix editor focus 2024-02-01 08:15:11 +05:30
22 changed files with 169 additions and 43 deletions

View File

@ -34,10 +34,10 @@ jobs:
wails doctor
wails build -tags webkit2_40
cp build/bin/Paisa build/linux/usr/local/bin
cp -r build/linux paisa_0.6.4_amd64
dpkg --build paisa_0.6.4_amd64
cp -r build/linux paisa_0.6.5_amd64
dpkg --build paisa_0.6.5_amd64
cd ..
mv desktop/paisa_0.6.4_amd64.deb paisa-app-linux-amd64.deb
mv desktop/paisa_0.6.5_amd64.deb paisa-app-linux-amd64.deb
- name: Release
uses: softprops/action-gh-release@v1
with:

View File

@ -1,5 +1,11 @@
# CHANGELOG
### 0.6.5 (2024-02-02)
* Add Liabilities > [Credit Card](https://paisa.fyi/reference/credit-cards) page
* Support password protected XLSX file
* Allow user to configure timezone
* Bug fixes
### 0.6.4 (2024-01-22)

View File

@ -9,7 +9,7 @@ var versionCmd = &cobra.Command{
Use: "version",
Short: "Print the version information",
Run: func(cmd *cobra.Command, args []string) {
fmt.Println("Version:", "0.6.4")
fmt.Println("Version:", "0.6.5")
},
}

View File

@ -1,5 +1,5 @@
Package: paisa
Version: 0.6.4
Version: 0.6.5
Section: misc
Priority: optional
Architecture: amd64

View File

@ -61,7 +61,7 @@ func main() {
Mac: &mac.Options{
About: &mac.AboutInfo{
Title: "Paisa",
Message: "Version 0.6.4 \nCopyright © 2022 - 2024 \nAnantha Kumaran",
Message: "Version 0.6.5 \nCopyright © 2022 - 2024 \nAnantha Kumaran",
Icon: icon,
},
},

View File

@ -11,7 +11,7 @@
"Info": {
"companyName": "Paisa",
"productName": "Paisa",
"productVersion": "0.6.4",
"productVersion": "0.6.5",
"copyright": "Copyright © 2022 - 2024 Anantha Kumaran",
"comments": "Personal finance manager"
}

View File

@ -18,13 +18,13 @@
packages.default = pkgs.buildGoModule {
pname = "paisa-cli";
version = "0.6.4";
version = "0.6.5";
src = ./.;
nativeBuildInputs = [ pkgs.nodejs-18_x ];
vendorHash = "sha256-9VM6+0h6DMtlA+RvCfpDffAvaq7jRmBVVFTbdUfYwsA=";
vendorHash = "sha256-KnHJ6+aMahTeNdbRcRAgBERGVYen/tM/tDcFI/NyLdE=";
CGO_ENABLED = 1;

10
flake/node-package.nix generated
View File

@ -1399,13 +1399,13 @@ let
sha512 = "6lMvf7xYEJ+oGeR5L8DFJJrowkefTK6ZgA4JiMqoClMkKq0s6yvsd3FZfCFvX1fQ0tpCD7fkuRVHsnUVgsHyNg==";
};
};
"@sveltejs/kit-2.0.6" = {
"@sveltejs/kit-2.5.0" = {
name = "_at_sveltejs_slash_kit";
packageName = "@sveltejs/kit";
version = "2.0.6";
version = "2.5.0";
src = fetchurl {
url = "https://registry.npmjs.org/@sveltejs/kit/-/kit-2.0.6.tgz";
sha512 = "dnHtyjBLGXx+hrZQ9GuqLlSfTBixewJaByUVWai7LmB4dgV3FwkK155OltEgONDQW6KW64hLNS/uojdx3uC2/g==";
url = "https://registry.npmjs.org/@sveltejs/kit/-/kit-2.5.0.tgz";
sha512 = "1uyXvzC2Lu1FZa30T4y5jUAC21R309ZMRG0TPt+PPPbNUoDpy8zSmSNVWYaBWxYDqLGQ5oPNWvjvvF2IjJ1jmA==";
};
};
"@sveltejs/vite-plugin-svelte-3.0.1" = {
@ -9706,7 +9706,7 @@ let
sources."@rollup/rollup-win32-x64-msvc-4.9.2"
sources."@sveltejs/adapter-auto-3.0.1"
sources."@sveltejs/adapter-static-3.0.1"
sources."@sveltejs/kit-2.0.6"
sources."@sveltejs/kit-2.5.0"
sources."@sveltejs/vite-plugin-svelte-3.0.1"
sources."@sveltejs/vite-plugin-svelte-inspector-2.0.0"
sources."@tokenizer/token-0.3.0"

2
go.mod
View File

@ -47,6 +47,7 @@ require (
github.com/jinzhu/inflection v1.0.0 // indirect
github.com/jinzhu/now v1.1.5 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/kelindar/binary v1.0.18 // indirect
github.com/klauspost/cpuid/v2 v2.2.6 // indirect
github.com/labstack/echo/v4 v4.11.1 // indirect
github.com/labstack/gommon v0.4.0 // indirect
@ -57,6 +58,7 @@ require (
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mattn/go-sqlite3 v1.14.19 // indirect
github.com/mitchellh/hashstructure/v2 v2.0.2 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/pelletier/go-toml/v2 v2.1.1 // indirect

4
go.sum
View File

@ -97,6 +97,8 @@ github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
github.com/kelindar/binary v1.0.18 h1:xGKmvb6Q3bpvvhKULfB0rG5vLOjtpoZn/FgPpnTl+aI=
github.com/kelindar/binary v1.0.18/go.mod h1:/twdz8gRLNMffx0U4UOgqm1LywPs6nd9YK2TX52MDh8=
github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
github.com/klauspost/cpuid/v2 v2.2.6 h1:ndNyv040zDGIDh8thGkXYjnFtiN02M1PVVF+JE/48xc=
github.com/klauspost/cpuid/v2 v2.2.6/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws=
@ -136,6 +138,8 @@ github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWE
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-sqlite3 v1.14.19 h1:fhGleo2h1p8tVChob4I9HpmVFIAkKGpiukdrgQbWfGI=
github.com/mattn/go-sqlite3 v1.14.19/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg=
github.com/mitchellh/hashstructure/v2 v2.0.2 h1:vGKWl0YJqUNxE8d+h8f6NJLcCJrgbhC4NcD46KavDd4=
github.com/mitchellh/hashstructure/v2 v2.0.2/go.mod h1:MG3aRVU/N29oo/V/IhBX8GR/zz4kQkprJgF2EVszyDE=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=

58
internal/model/cache/cache.go vendored Normal file
View File

@ -0,0 +1,58 @@
package cache
import (
"fmt"
"time"
"github.com/kelindar/binary"
"github.com/mitchellh/hashstructure/v2"
"gorm.io/gorm"
)
type Cache struct {
ID uint `gorm:"primaryKey" json:"id"`
ExpiresAt time.Time `json:"expires_at"`
HashKey string `json:"hash_key"`
Value []byte `gorm:"type:BLOB" json:"item"`
}
func DeleteExpired(db *gorm.DB) error {
err := db.Exec("DELETE FROM caches WHERE expires_at < ?", time.Now()).Error
if err != nil {
return err
}
return nil
}
func Lookup[I any, K any](db *gorm.DB, key K, fallback func() I) I {
var item I
var cache Cache
hash, err := hashstructure.Hash(key, hashstructure.FormatV2, nil)
hashKey := fmt.Sprintf("%d", hash)
if err == nil {
err := db.Where("hash_key = ?", hashKey).First(&cache).Error
if err == nil {
if time.Now().Before(cache.ExpiresAt) {
err := binary.Unmarshal(cache.Value, &item)
if err == nil {
return item
}
} else {
DeleteExpired(db)
}
}
}
item = fallback()
bytes, err := binary.Marshal(item)
if err == nil {
cache = Cache{
ExpiresAt: time.Now().Add(24 * time.Hour),
HashKey: hashKey,
Value: bytes,
}
db.Save(&cache)
}
return item
}

View File

@ -6,6 +6,7 @@ import (
"github.com/ananthakumaran/paisa/internal/config"
"github.com/ananthakumaran/paisa/internal/ledger"
"github.com/ananthakumaran/paisa/internal/model/cache"
"github.com/ananthakumaran/paisa/internal/model/cii"
"github.com/ananthakumaran/paisa/internal/model/commodity"
mutualfundModel "github.com/ananthakumaran/paisa/internal/model/mutualfund/scheme"
@ -29,6 +30,7 @@ func AutoMigrate(db *gorm.DB) {
db.AutoMigrate(&portfolio.Portfolio{})
db.AutoMigrate(&price.Price{})
db.AutoMigrate(&cii.CII{})
db.AutoMigrate(&cache.Cache{})
}
func SyncJournal(db *gorm.DB) (string, error) {

View File

@ -1,6 +1,7 @@
package service
import (
"github.com/ananthakumaran/paisa/internal/model/cache"
"github.com/ananthakumaran/paisa/internal/model/posting"
"github.com/ananthakumaran/paisa/internal/utils"
"github.com/ananthakumaran/paisa/internal/xirr"
@ -26,7 +27,9 @@ func XIRR(db *gorm.DB, ps []posting.Posting) decimal.Decimal {
}))
cashflows = append(cashflows, xirr.Cashflow{Date: today, Amount: marketAmount.Round(4).InexactFloat64()})
return xirr.XIRR(cashflows)
return cache.Lookup(db, cashflows, func() decimal.Decimal {
return xirr.XIRR(cashflows)
})
}
func APR(db *gorm.DB, ps []posting.Posting) decimal.Decimal {
@ -38,5 +41,8 @@ func APR(db *gorm.DB, ps []posting.Posting) decimal.Decimal {
return xirr.Cashflow{Date: p.Date, Amount: p.Amount.Round(4).InexactFloat64()}
})
cashflows = append(cashflows, xirr.Cashflow{Date: today, Amount: marketAmount.Neg().Round(4).InexactFloat64()})
return xirr.XIRR(cashflows)
return cache.Lookup(db, cashflows, func() decimal.Decimal {
return xirr.XIRR(cashflows)
})
}

View File

@ -68,7 +68,7 @@
<span class="ml-0 icon is-small">
<i class="fas {$obscure ? 'fa-eye-slash' : 'fa-eye'}" />
</span>
<span>Hide numbers</span>
<span>{$obscure ? "Show" : "Hide"} numbers</span>
</label>
</a>
{#if showLogout}

View File

@ -150,3 +150,21 @@ export function updateContent(editor: EditorView, content: string) {
const newColumn = Math.min(newLine.from + column, newLine.to);
editor.dispatch({ selection: { anchor: newColumn, head: newColumn } });
}
export function focus(editor: EditorView, retry = 5) {
if (!editor.hasFocus) {
editor.focus();
if (!editor.hasFocus && retry > 0) {
setTimeout(
() => {
try {
focus(editor, retry - 1);
} catch (e) {
// ignore
}
},
(5 - retry) * 100 + 100
);
}
}
}

View File

@ -1,5 +1,12 @@
import * as d3 from "d3";
import { formatCurrency, formatCurrencyCrude, tooltip, type IncomeStatement, rem } from "./utils";
import {
formatCurrency,
formatCurrencyCrude,
tooltip,
type IncomeStatement,
rem,
firstNames
} from "./utils";
import COLORS from "./colors";
import _ from "lodash";
import { iconGlyph, iconify } from "./icon";
@ -204,8 +211,15 @@ export function renderIncomeStatement(element: Element) {
.attr("fill", (d) => d.color)
.attr("fill-opacity", 0.5)
.attr("data-tippy-content", (d) => {
const secondLevelBreakdown = _.chain(d.breakdown)
.toPairs()
.groupBy((pair) => firstNames(pair[0], 2))
.map((pairs, label) => [label, _.sumBy(pairs, (pair) => pair[1])])
.fromPairs()
.value();
return tooltip(
_.map(d.breakdown, (value, label) => [
_.map(secondLevelBreakdown, (value, label) => [
iconify(label),
[formatCurrency(value * d.multiplier), "has-text-right has-text-weight-bold"]
]),

View File

@ -1010,6 +1010,10 @@ export function secondName(account: string) {
return account.split(":")[1];
}
export function firstNames(account: string, n: number) {
return _.take(account.split(":"), n).join(":");
}
export function restName(account: string) {
return _.drop(account.split(":")).join(":");
}

View File

@ -159,7 +159,7 @@
<div class="tile is-child">
<div class="content">
<p class="subtitle">
<a class="secondary-link" href="/assets/networth">Assets</a>
<a class="secondary-link has-text-grey" href="/assets/networth">Assets</a>
</p>
<div class="content">
<div>
@ -201,7 +201,8 @@
<article class="tile is-child">
<div class="content">
<p class="subtitle">
<a class="secondary-link" href="/assets/balance">Checking Balance</a>
<a class="secondary-link has-text-grey" href="/assets/balance">Checking Balance</a
>
</p>
<div class="content">
<UntypedMasonryGrid gap={10} maxStretchColumnSize={400} align="stretch">
@ -220,7 +221,7 @@
<div class="tile is-parent">
<article class="tile is-child min-w-0">
<p class="subtitle">
<a class="secondary-link" href="/cash_flow/monthly">Cash Flow</a>
<a class="secondary-link has-text-grey" href="/cash_flow/monthly">Cash Flow</a>
</p>
<div class="content box px-2 pb-0">
<ZeroState item={cashFlows}>
@ -243,7 +244,7 @@
<div class="tile is-child">
<div class="content">
<p class="subtitle">
<a class="secondary-link" href="/expense/budget">Budget</a>
<a class="secondary-link has-text-grey" href="/expense/budget">Budget</a>
</p>
<div class="content">
<div>
@ -262,7 +263,7 @@
<article class="tile is-child">
<div class="content">
<p class="subtitle">
<a class="secondary-link" href="/more/goals">Goals</a>
<a class="secondary-link has-text-grey" href="/more/goals">Goals</a>
</p>
<div class="content">
{#each goalSummaries as goal}
@ -280,7 +281,7 @@
<article class="tile is-child">
<p class="subtitle is-flex is-justify-content-space-between is-align-items-end">
<span
><a class="secondary-link" href="/expense/monthly">Expenses</a>
><a class="secondary-link has-text-grey" href="/expense/monthly">Expenses</a>
<span class="is-size-5 has-text-weight-bold px-2" style="color: {COLORS.expenses}"
>{formatCurrency(totalExpense)}</span
></span
@ -301,7 +302,8 @@
<article class="tile is-child">
<div class="content">
<p class="subtitle">
<a class="secondary-link" href="/cash_flow/recurring">Recurring</a>
<a class="secondary-link has-text-grey" href="/cash_flow/recurring">Recurring</a
>
</p>
<div class="content box">
<div
@ -324,7 +326,9 @@
<article class="tile is-child">
<div class="content">
<p class="subtitle">
<a class="secondary-link" href="/ledger/transaction">Recent Transactions</a>
<a class="secondary-link has-text-grey" href="/ledger/transaction"
>Recent Transactions</a
>
</p>
<div>
<UntypedMasonryGrid gap={10} maxStretchColumnSize={500} align="stretch">
@ -349,4 +353,9 @@
p.subtitle {
margin-bottom: 0.5rem !important;
}
p.subtitle a.secondary-link {
text-transform: uppercase;
font-size: 1rem;
}
</style>

View File

@ -1,5 +1,12 @@
<script lang="ts">
import { createEditor, editorState, moveToEnd, moveToLine, updateContent } from "$lib/editor";
import {
createEditor,
editorState,
focus,
moveToEnd,
moveToLine,
updateContent
} from "$lib/editor";
import { insertTab } from "@codemirror/commands";
import { ajax, buildDirectoryTree, type LedgerFile } from "$lib/utils";
import { redo, undo } from "@codemirror/commands";
@ -164,10 +171,8 @@
}
});
if (lineNumber > 0) {
if (!editor.hasFocus) {
editor.focus();
}
moveToLine(editor, lineNumber, true);
focus(editor);
lineNumber = 0;
} else {
moveToEnd(editor);

View File

@ -104,15 +104,15 @@
<div class="column is-9-widescreen is-8">
{#if currentBill}
<div class="flex flex-wrap gap-4 mb-4">
<div class="box py-2 m-0 flex-grow" style="border: 1px solid transparent">
<div
class="is-flex mr-2 is-align-items-baseline overflow-x-scroll"
style="min-width: fit-content"
>
<div class="ml-3 custom-icon is-size-5">
<div
class="box py-2 m-0 flex-grow overflow-x-scroll"
style="border: 1px solid transparent"
>
<div class="is-flex mr-2 is-align-items-baseline" style="min-width: fit-content">
<div class="ml-3 custom-icon is-size-5 whitespace-nowrap">
<span>{iconify(creditCard.account)}</span>
</div>
<div class="ml-3">
<div class="ml-3 whitespace-nowrap">
<span class="mr-1 is-size-7 has-text-grey">Payment</span>
<span
><DueDate dueDate={currentBill.dueDate} paidDate={currentBill.paidDate} /></span

View File

@ -43,7 +43,7 @@
<div><Logo size={128} /></div>
<div class="is-size-3 is-primary-color">Paisa</div>
<div>
Version: <b>0.6.4</b>
Version: <b>0.6.5</b>
</div>
</div>

View File

@ -1,6 +1,6 @@
<script lang="ts">
import { createEditor, sheetEditorState } from "$lib/sheet";
import { moveToLine, updateContent } from "$lib/editor";
import { focus, moveToLine, updateContent } from "$lib/editor";
import {
ajax,
buildDirectoryTree,
@ -157,10 +157,8 @@
}
});
if (lineNumber > 0) {
if (!editor.hasFocus) {
editor.focus();
}
moveToLine(editor, lineNumber, true);
focus(editor);
lineNumber = 0;
}
}