Skip to content

Commit

Permalink
add auto-completion for narration
Browse files Browse the repository at this point in the history
  • Loading branch information
dtrai2 committed May 28, 2024
1 parent 05ad558 commit 5752562
Show file tree
Hide file tree
Showing 9 changed files with 144 additions and 11 deletions.
2 changes: 2 additions & 0 deletions frontend/src/api/validators.ts
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,7 @@ export const ledgerDataValidator = object({
options,
other_ledgers: array(tuple([string, string])),
payees: array(string),
narrations: array(string),
precisions: record(number),
sidebar_links: array(tuple([string, string])),
tags: array(string),
Expand Down Expand Up @@ -192,6 +193,7 @@ export const getAPIValidators = {
move: string,
payee_accounts: array(string),
payee_transaction: Transaction.validator,
narration_transaction: Transaction.validator,
query_result: object({ chart: unknown, table: string }),
source,
trial_balance: tree_report,
Expand Down
46 changes: 35 additions & 11 deletions frontend/src/entry-forms/Transaction.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
import type { Transaction } from "../entries";
import { _ } from "../i18n";
import { notify_err } from "../notifications";
import { payees } from "../stores";
import { payees, narrations } from "../stores";
import AddMetadataButton from "./AddMetadataButton.svelte";
import EntryMetadata from "./EntryMetadata.svelte";
Expand Down Expand Up @@ -78,6 +78,14 @@
data.date = entry.date;
entry = data;
}
async function autocompleteSelectNarration() {
if (entry.payee || !entry.postings.every((p) => !p.account)) {
return;
}
const data = await get("narration_transaction", { narration: entry.narration });
data.date = entry.date;
entry = data;
}
function movePosting({ from, to }: { from: number; to: number }) {
const moved = entry.postings[from];
Expand All @@ -92,6 +100,24 @@
$: if (!entry.postings.some((p) => p.is_empty())) {
entry.postings = entry.postings.concat(new Posting());
}
function valueExtractor(value: string, input: HTMLInputElement) {
const match = value
.slice(0, input.selectionStart ?? undefined)
.match(/\S*$/);
return match?.[0] ?? value;
}
function valueSelector(value: string, input: HTMLInputElement) {
const selectionStart = input.selectionStart ?? 0;
const match = input.value.slice(0, selectionStart).match(/\S*$/);
const matchLength = match?.[0]?.length;
return matchLength !== undefined
? `${input.value.slice(
0,
selectionStart - matchLength,
)}${value}${input.value.slice(selectionStart)}`
: value;
}
</script>

<div>
Expand All @@ -109,14 +135,17 @@
on:select={autocompleteSelectPayee}
/>
</label>
<!-- svelte-ignore a11y-label-has-associated-control -->
<label>
<span>{_("Narration")}:</span>
<input
type="text"
name="narration"
<AutocompleteInput
className="narration"
placeholder={_("Narration")}
value={narration}
on:change={onNarrationChange}
bind:value={entry.narration}
suggestions={$narrations}
valueExtractor={valueExtractor}
valueSelector={valueSelector}
on:select={autocompleteSelectNarration}
/>
<AddMetadataButton bind:meta={entry.meta} />
</label>
Expand Down Expand Up @@ -152,11 +181,6 @@
flex-grow: 1;
}
input[name="narration"] {
flex-basis: 200px;
flex-grow: 1;
}
label > span:first-child,
.label > span:first-child {
display: none;
Expand Down
2 changes: 2 additions & 0 deletions frontend/src/stores/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,8 @@ export const currencies = derived_array(ledgerData, (v) => v.currencies);
export const links = derived_array(ledgerData, (v) => v.links);
/** The ranked array of all payees. */
export const payees = derived_array(ledgerData, (v) => v.payees);
/** The ranked array of all narrations. */
export const narrations = derived_array(ledgerData, (v) => v.narrations);
/** The ranked array of all tags. */
export const tags = derived_array(ledgerData, (v) => v.tags);
/** The array of all years. */
Expand Down
13 changes: 13 additions & 0 deletions src/fava/core/attributes.py
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ def __init__(self, ledger: FavaLedger) -> None:
self.accounts: list[str] = []
self.currencies: list[str] = []
self.payees: list[str] = []
self.narrations: list[str] = []
self.links: list[str] = []
self.tags: list[str] = []
self.years: list[str] = []
Expand Down Expand Up @@ -91,10 +92,13 @@ def load_file(self) -> None: # noqa: D102
)
currency_ranker = ExponentialDecayRanker()
payee_ranker = ExponentialDecayRanker()
narration_ranker = ExponentialDecayRanker()

for txn in self.ledger.all_entries_by_type.Transaction:
if txn.payee:
payee_ranker.update(txn.payee, txn.date)
if txn.narration:
narration_ranker.update(txn.narration, txn.date)
for posting in txn.postings:
account_ranker.update(posting.account, txn.date)
currency_ranker.update(posting.units.currency, txn.date)
Expand All @@ -104,6 +108,7 @@ def load_file(self) -> None: # noqa: D102
self.accounts = account_ranker.sort()
self.currencies = currency_ranker.sort()
self.payees = payee_ranker.sort()
self.narrations = narration_ranker.sort()

def payee_accounts(self, payee: str) -> list[str]:
"""Rank accounts for the given payee."""
Expand All @@ -122,3 +127,11 @@ def payee_transaction(self, payee: str) -> Transaction | None:
if txn.payee == payee:
return txn
return None

def narration_transaction(self, narration: str) -> Transaction | None:
"""Get the last transaction for a narration."""
transactions = self.ledger.all_entries_by_type.Transaction
for txn in reversed(transactions):
if txn.narration == narration:
return txn
return None
2 changes: 2 additions & 0 deletions src/fava/internal_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ class LedgerData:
links: list[str]
options: dict[str, str | list[str]]
payees: list[str]
narrations: list[str]
precisions: dict[str, int]
tags: list[str]
years: list[str]
Expand Down Expand Up @@ -114,6 +115,7 @@ def get_ledger_data() -> LedgerData:
ledger.attributes.links,
_get_options(),
ledger.attributes.payees,
ledger.attributes.narrations,
ledger.format_decimal.precisions,
ledger.attributes.tags,
ledger.attributes.years,
Expand Down
7 changes: 7 additions & 0 deletions src/fava/json_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -318,6 +318,13 @@ def get_payee_transaction(payee: str) -> Any:
return serialise(entry) if entry else None


@api_endpoint
def get_narration_transaction(narration: str) -> Any:
"""Last transaction for the given narration."""
entry = g.ledger.attributes.narration_transaction(narration)
return serialise(entry) if entry else None


@api_endpoint
def get_source(filename: str) -> dict[str, str]:
"""Load one of the source files."""
Expand Down
37 changes: 37 additions & 0 deletions tests/__snapshots__/test_application-test_client_side_reports
Original file line number Diff line number Diff line change
Expand Up @@ -941,6 +941,43 @@
"links": [
"test-link"
],
"narrations": [
"Investing 40% of cash in VBMPX",
"Investing 60% of cash in RGAGX",
"Payroll",
"Buying groceries",
"Eating out alone",
"Employer match for contribution",
"Eating out with Julie",
"Eating out ",
"Eating out after work",
"Eating out with Bill",
"Eating out with Natasha",
"Monthly bank fee",
"Paying off credit card",
"Eating out with Joe",
"Paying the rent",
"Tram tickets",
"Eating out with work buddies",
"Buy shares of VEA",
"Buy shares of GLD",
"Dividends on portfolio",
"Transfering accumulated savings to other account",
"Buy shares of ITOT",
"Buy shares of VHT",
"Consume vacation days",
"Sell shares of GLD",
"STATE TAX \u0026 FINANC PYMT",
"FEDERAL TAXPYMT",
"Allowed contributions for one year",
"Sell shares of VEA",
"Filing taxes for 2015",
"Sell shares of VHT",
"Sell shares of ITOT",
"Filing taxes for 2014",
"Opening Balance for checking account",
"\u00c1rv\u00edzt\u0171r\u0151 t\u00fck\u00f6rf\u00far\u00f3g\u00e9p"
],
"options": {
"documents": [],
"filename": "TEST_DATA_DIR/long-example.beancount",
Expand Down
37 changes: 37 additions & 0 deletions tests/__snapshots__/test_internal_api-test_get_ledger_data.json
Original file line number Diff line number Diff line change
Expand Up @@ -929,6 +929,43 @@
"links": [
"test-link"
],
"narrations": [
"Investing 40% of cash in VBMPX",
"Investing 60% of cash in RGAGX",
"Payroll",
"Buying groceries",
"Eating out alone",
"Employer match for contribution",
"Eating out with Julie",
"Eating out ",
"Eating out after work",
"Eating out with Bill",
"Eating out with Natasha",
"Monthly bank fee",
"Paying off credit card",
"Eating out with Joe",
"Paying the rent",
"Tram tickets",
"Eating out with work buddies",
"Buy shares of VEA",
"Buy shares of GLD",
"Dividends on portfolio",
"Transfering accumulated savings to other account",
"Buy shares of ITOT",
"Buy shares of VHT",
"Consume vacation days",
"Sell shares of GLD",
"STATE TAX & FINANC PYMT",
"FEDERAL TAXPYMT",
"Allowed contributions for one year",
"Sell shares of VEA",
"Filing taxes for 2015",
"Sell shares of VHT",
"Sell shares of ITOT",
"Filing taxes for 2014",
"Opening Balance for checking account",
"\u00c1rv\u00edzt\u0171r\u0151 t\u00fck\u00f6rf\u00far\u00f3g\u00e9p"
],
"options": {
"documents": [],
"filename": "TEST_DATA_DIR/long-example.beancount",
Expand Down
9 changes: 9 additions & 0 deletions tests/test_core_attributes.py
Original file line number Diff line number Diff line change
Expand Up @@ -60,3 +60,12 @@ def test_payee_transaction(example_ledger: FavaLedger) -> None:
txn = attr.payee_transaction("BayBook")
assert txn
assert str(txn.date) == "2016-05-05"


def test_narration_transaction(example_ledger: FavaLedger) -> None:
attr = example_ledger.attributes
assert attr.narration_transaction("NOTANARRATION") is None

txn = attr.narration_transaction("Monthly bank fee")
assert txn
assert str(txn.date) == "2016-05-04"

0 comments on commit 5752562

Please sign in to comment.