Count hand-valued assets into the wealth figure

A wealth figure that ignores the house is not a wealth figure. Assets
without a market feed - a house, a car, a private loan - are now added
by hand on the Wealth page with a stated value, a currency and the day
the estimate was made; a negative value records a liability. They are
registry entities in assets.finance like everything else, join the
per-currency totals immediately, and a currency held only in an asset
earns its own line.
This commit is contained in:
Lars Nolden
2026-09-14 09:29:19 +02:00
parent a1480af74d
commit 77f4ea5655
15 changed files with 564 additions and 29 deletions
+18 -2
View File
@@ -171,7 +171,7 @@ func NewDataset() Dataset {
return Dataset{Accounts: []Account{}, Categories: []Category{
{ID: "cat_expenses", Name: "Expenses", Kind: "expense"}, {ID: ExpenseFallback, Name: "Unclassified", ParentID: "cat_expenses", Kind: "expense"},
{ID: "cat_income", Name: "Income", Kind: "income"}, {ID: IncomeFallback, Name: "Unclassified", ParentID: "cat_income", Kind: "income"},
}, Tags: []Tag{}, Merchants: []Merchant{}, Instruments: []Instrument{}, Transactions: []Transaction{}}
}, Tags: []Tag{}, Merchants: []Merchant{}, Instruments: []Instrument{}, Assets: []Asset{}, Transactions: []Transaction{}}
}
// InstrumentID derives a stable registry ID from an ISIN so re-importing the
@@ -181,7 +181,7 @@ func InstrumentID(isin string) string {
return "ins_" + hex.EncodeToString(sum[:16])
}
func Clone(d Dataset) Dataset {
c := Dataset{Accounts: append([]Account{}, d.Accounts...), Categories: append([]Category{}, d.Categories...), Tags: append([]Tag{}, d.Tags...), Merchants: append([]Merchant{}, d.Merchants...), Instruments: append([]Instrument{}, d.Instruments...), Transactions: append([]Transaction{}, d.Transactions...)}
c := Dataset{Accounts: append([]Account{}, d.Accounts...), Categories: append([]Category{}, d.Categories...), Tags: append([]Tag{}, d.Tags...), Merchants: append([]Merchant{}, d.Merchants...), Instruments: append([]Instrument{}, d.Instruments...), Assets: append([]Asset{}, d.Assets...), Transactions: append([]Transaction{}, d.Transactions...)}
for i := range c.Merchants {
c.Merchants[i].Aliases = append([]string{}, d.Merchants[i].Aliases...)
c.Merchants[i].DefaultTagIDs = append([]string{}, d.Merchants[i].DefaultTagIDs...)
@@ -398,6 +398,22 @@ func Validate(d Dataset) error {
isins[v.ISIN] = v.ID
instruments[v.ID] = v
}
for _, v := range d.Assets {
if err := register(v.ID, "asset"); err != nil {
return err
}
if !nonblank(v.Name) || !currencyPattern.MatchString(v.Currency) || !validText(v.Kind) {
return fmt.Errorf("asset %q: valid UTF-8 name and three-letter uppercase currency required", v.ID)
}
// A hand-stated value without its day cannot be judged stale, so the
// two are recorded together, always.
if _, err := v.Value.Minor(); err != nil {
return fmt.Errorf("asset %q: %w", v.ID, err)
}
if !validDate(v.ValuedAt) {
return fmt.Errorf("asset %q: invalid valuation date %q", v.ID, v.ValuedAt)
}
}
for _, t := range d.Transactions {
f := t.Facts
if err := register(f.ID, "transaction"); err != nil {