Skip to content

Commit 68b33e5

Browse files
dtrai2yagebu
andauthored
add auto-completion for narration (#1888)
Co-authored-by: Jakob Schnitzer <[email protected]>
1 parent ec7f8f7 commit 68b33e5

File tree

9 files changed

+174
-11
lines changed

9 files changed

+174
-11
lines changed

frontend/src/api/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,8 @@ interface GetAPIParams {
7575
options: undefined;
7676
payee_accounts: { payee: string };
7777
payee_transaction: { payee: string };
78+
narration_transaction: { narration: string };
79+
narrations: undefined;
7880
query: Filters & { query_string: string };
7981
source: { filename: string };
8082
}

frontend/src/api/validators.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -196,6 +196,8 @@ export const getAPIValidators = {
196196
}),
197197
payee_accounts: array(string),
198198
payee_transaction: Transaction.validator,
199+
narration_transaction: Transaction.validator,
200+
narrations: array(string),
199201
query: query_validator,
200202
source,
201203
trial_balance: tree_report,

frontend/src/entry-forms/Transaction.svelte

Lines changed: 30 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,19 @@
3939
});
4040
4141
let narration = $derived(entry.get_narration_tags_links());
42+
let narration_suggestions: string[] = $state.raw([]);
43+
$effect(() => {
44+
get("narrations")
45+
.then((s) => {
46+
narration_suggestions = s;
47+
})
48+
.catch((error: unknown) => {
49+
notify_err(
50+
error,
51+
(err) => `Fetching narration suggestions failed: ${err.message}`,
52+
);
53+
});
54+
});
4255
4356
// Autofill complete transactions.
4457
async function autocompleteSelectPayee() {
@@ -50,6 +63,16 @@
5063
});
5164
entry = payee_transaction.set("date", entry.date);
5265
}
66+
async function autocompleteSelectNarration() {
67+
if (entry.payee || entry.postings.every((p) => !p.is_empty())) {
68+
return;
69+
}
70+
const data = await get("narration_transaction", {
71+
narration: narration,
72+
});
73+
entry = data;
74+
narration = entry.get_narration_tags_links();
75+
}
5376
5477
// Always have one empty posting at the end.
5578
$effect(() => {
@@ -99,13 +122,14 @@
99122
</label>
100123
<label>
101124
<span>{_("Narration")}:</span>
102-
<input
103-
type="text"
104-
name="narration"
125+
<AutocompleteInput
126+
className="narration"
105127
placeholder={_("Narration")}
106-
value={narration}
107-
onchange={({ currentTarget }) => {
108-
entry = entry.set_narration_tags_links(currentTarget.value);
128+
bind:value={narration}
129+
suggestions={narration_suggestions}
130+
onSelect={autocompleteSelectNarration}
131+
onBlur={() => {
132+
entry = entry.set_narration_tags_links(narration);
109133
}}
110134
/>
111135
<AddMetadataButton
@@ -168,11 +192,6 @@
168192
flex-basis: 100px;
169193
}
170194

171-
input[name="narration"] {
172-
flex-grow: 1;
173-
flex-basis: 200px;
174-
}
175-
176195
label > span:first-child,
177196
.label > span:first-child {
178197
display: none;

src/fava/core/attributes.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -124,3 +124,20 @@ def payee_transaction(self, payee: str) -> Transaction | None:
124124
if txn.payee == payee:
125125
return txn
126126
return None
127+
128+
def narration_transaction(self, narration: str) -> Transaction | None:
129+
"""Get the last transaction for a narration."""
130+
transactions = self.ledger.all_entries_by_type.Transaction
131+
for txn in reversed(transactions):
132+
if txn.narration == narration:
133+
return txn
134+
return None
135+
136+
@property
137+
def narrations(self) -> Sequence[str]:
138+
"""Get the narrations of all transactions."""
139+
narration_ranker = ExponentialDecayRanker()
140+
for txn in self.ledger.all_entries_by_type.Transaction:
141+
if txn.narration:
142+
narration_ranker.update(txn.narration, txn.date)
143+
return narration_ranker.sort()

src/fava/json_api.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -345,6 +345,19 @@ def get_payee_transaction(payee: str) -> Any:
345345
return serialise(entry) if entry else None
346346

347347

348+
@api_endpoint
349+
def get_narration_transaction(narration: str) -> Any:
350+
"""Last transaction for the given narration."""
351+
entry = g.ledger.attributes.narration_transaction(narration)
352+
return serialise(entry) if entry else None
353+
354+
355+
@api_endpoint
356+
def get_narrations() -> Sequence[str]:
357+
"""List of all narrations in the ledger."""
358+
return g.ledger.attributes.narrations
359+
360+
348361
@api_endpoint
349362
def get_source(filename: str) -> Mapping[str, str]:
350363
"""Load one of the source files."""
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
[
2+
"Investing 40% of cash in VBMPX",
3+
"Investing 60% of cash in RGAGX",
4+
"Payroll",
5+
"Buying groceries",
6+
"Eating out alone",
7+
"Employer match for contribution",
8+
"Eating out with Julie",
9+
"Eating out ",
10+
"Eating out after work",
11+
"Eating out with Bill",
12+
"Eating out with Natasha",
13+
"Monthly bank fee",
14+
"Paying off credit card",
15+
"Eating out with Joe",
16+
"Paying the rent",
17+
"Tram tickets",
18+
"Eating out with work buddies",
19+
"Buy shares of VEA",
20+
"Buy shares of GLD",
21+
"Dividends on portfolio",
22+
"Transfering accumulated savings to other account",
23+
"Buy shares of ITOT",
24+
"Buy shares of VHT",
25+
"Consume vacation days",
26+
"Sell shares of GLD",
27+
"STATE TAX & FINANC PYMT",
28+
"FEDERAL TAXPYMT",
29+
"Allowed contributions for one year",
30+
"Sell shares of VEA",
31+
"Filing taxes for 2015",
32+
"Sell shares of VHT",
33+
"Sell shares of ITOT",
34+
"Filing taxes for 2014",
35+
"Opening Balance for checking account",
36+
"\u00c1rv\u00edzt\u0171r\u0151 t\u00fck\u00f6rf\u00far\u00f3g\u00e9p"
37+
]
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
[
2+
"Investing 40% of cash in VBMPX",
3+
"Investing 60% of cash in RGAGX",
4+
"Payroll",
5+
"Buying groceries",
6+
"Eating out alone",
7+
"Employer match for contribution",
8+
"Eating out with Julie",
9+
"Eating out ",
10+
"Eating out after work",
11+
"Eating out with Bill",
12+
"Eating out with Natasha",
13+
"Monthly bank fee",
14+
"Paying off credit card",
15+
"Eating out with Joe",
16+
"Paying the rent",
17+
"Tram tickets",
18+
"Eating out with work buddies",
19+
"Buy shares of VEA",
20+
"Buy shares of GLD",
21+
"Dividends on portfolio",
22+
"Transfering accumulated savings to other account",
23+
"Buy shares of ITOT",
24+
"Buy shares of VHT",
25+
"Consume vacation days",
26+
"Sell shares of GLD",
27+
"STATE TAX & FINANC PYMT",
28+
"FEDERAL TAXPYMT",
29+
"Allowed contributions for one year",
30+
"Sell shares of VEA",
31+
"Filing taxes for 2015",
32+
"Sell shares of VHT",
33+
"Sell shares of ITOT",
34+
"Filing taxes for 2014",
35+
"Opening Balance for checking account",
36+
"\u00c1rv\u00edzt\u0171r\u0151 t\u00fck\u00f6rf\u00far\u00f3g\u00e9p"
37+
]

tests/test_core_attributes.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@
99
from fava.beans.abc import Directive
1010
from fava.core import FavaLedger
1111

12+
from .conftest import SnapshotFunc
13+
1214

1315
def test_get_active_years(load_doc_entries: list[Directive]) -> None:
1416
"""
@@ -63,3 +65,21 @@ def test_payee_transaction(example_ledger: FavaLedger) -> None:
6365
txn = attr.payee_transaction("BayBook")
6466
assert txn
6567
assert str(txn.date) == "2016-05-05"
68+
69+
70+
def test_narration_transaction(example_ledger: FavaLedger) -> None:
71+
attr = example_ledger.attributes
72+
assert attr.narration_transaction("NOTANARRATION") is None
73+
74+
txn = attr.narration_transaction("Monthly bank fee")
75+
assert txn
76+
assert str(txn.date) == "2016-05-04"
77+
78+
79+
def test_narrations(
80+
example_ledger: FavaLedger,
81+
snapshot: SnapshotFunc,
82+
) -> None:
83+
narrations = example_ledger.attributes.narrations
84+
assert narrations
85+
snapshot(narrations, json=True)

tests/test_json_api.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -337,6 +337,21 @@ def test_api_payee_transaction(
337337
snapshot(data, json=True)
338338

339339

340+
def test_api_narration_transaction(
341+
test_client: FlaskClient,
342+
) -> None:
343+
response = test_client.get(
344+
"/long-example/api/narration_transaction",
345+
query_string={"narration": "Buying groceries"},
346+
)
347+
data = assert_api_success(response)
348+
assert data["date"] == "2016-04-21"
349+
assert data["narration"] == "Buying groceries"
350+
assert data["payee"] == "Farmer Fresh"
351+
assert len(data["postings"]) == 2
352+
assert data["t"] == "Transaction"
353+
354+
340355
def test_api_imports(
341356
test_client: FlaskClient,
342357
snapshot: SnapshotFunc,
@@ -769,6 +784,7 @@ def test_api_filter_error(
769784
("documents", "/example/api/documents"),
770785
("events", "/long-example/api/events"),
771786
("income_statement", "/long-example/api/income_statement?time=2014"),
787+
("narrations", "/long-example/api/narrations"),
772788
("trial_balance", "/long-example/api/trial_balance?time=2014"),
773789
("balance_sheet", "/long-example/api/balance_sheet"),
774790
(

0 commit comments

Comments
 (0)