summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--clients/screens_bank.c76
-rw-r--r--clients/screens_vouchers.c28
-rw-r--r--clients/ui.h9
-rw-r--r--docs/PROTOCOL.md8
-rw-r--r--docs/STATE.md5
-rwxr-xr-xscripts/tui-golden.py59
6 files changed, 175 insertions, 10 deletions
diff --git a/clients/screens_bank.c b/clients/screens_bank.c
index 63651bf..5131055 100644
--- a/clients/screens_bank.c
+++ b/clients/screens_bank.c
@@ -41,6 +41,7 @@ struct bank_sugg {
struct bank_tx {
int64_t id;
+ char account[16];
char booked_at[16];
char text[256];
int64_t amount_ore;
@@ -101,10 +102,56 @@ static void bank_tx_row(const struct bank_tx *t, char *line, size_t cap)
t->booked_at, text, amt, tail);
}
+/* Serie+nummer for a posted voucher, via voucher.get. */
+static void bank_voucher_ver(struct app *a, int64_t vid, char *buf, size_t n)
+{
+ buf[0] = '\0';
+ char args[64];
+ snprintf(args, sizeof args, "{\"id\":%lld}", (long long)vid);
+ char *resp = client_rpc(&a->conn, "voucher.get", a->session, a->org, args);
+ if (resp && client_ok(resp)) {
+ char *ser = jstr_dup(resp, "result.series");
+ bank_ver(buf, n, ser ? ser : "", jint_val(resp, "result.number", 0));
+ free(ser);
+ }
+ free(resp);
+}
+
+/* Prefill Nytt verifikat from the transaction, then match the posted voucher.
+ Returns 1 when a voucher was posted (matched or not), 0 on cancel. */
+static int bank_new_voucher(struct app *a, const struct bank_tx *t)
+{
+ struct voucher_prefill p;
+ memset(&p, 0, sizeof p);
+ p.date = t->booked_at;
+ p.description = t->text;
+ p.bank_account = t->account;
+ p.amount_ore = t->amount_ore;
+ int64_t vid = vouchers_new_prefill(a, &p);
+ if (vid <= 0)
+ return 0;
+ char args[64];
+ snprintf(args, sizeof args,
+ "{\"transaction_id\":%lld,\"voucher_id\":%lld}",
+ (long long)t->id, (long long)vid);
+ char *resp = client_rpc(&a->conn, "bank.match", a->session, a->org, args);
+ if (!resp || !client_ok(resp)) {
+ show_error("Bankavstämning", resp);
+ free(resp);
+ return 1;
+ }
+ free(resp);
+ char ver[32];
+ bank_voucher_ver(a, vid, ver, sizeof ver);
+ tui_message("Bankavstämning", "Matchat med %s.", ver);
+ return 1;
+}
+
/* Enter on an unmatched row: suggest vouchers, or pick one freely. */
static void bank_match_tx(struct app *a, const struct bank_tx *t)
{
- char **items = xcalloc(t->nsuggestions + 1, sizeof(char *));
+ size_t nact = t->nsuggestions + 2;
+ char **items = xcalloc(nact, sizeof(char *));
for (size_t i = 0; i < t->nsuggestions; i++) {
const struct bank_sugg *s = &t->suggestions[i];
char ver[32], amt[48], diff[48], line[256];
@@ -116,14 +163,18 @@ static void bank_match_tx(struct app *a, const struct bank_tx *t)
items[i] = xstrdup(line);
}
items[t->nsuggestions] = xstrdup("Välj annat verifikat…");
- int sel = tui_select_list("Matcha transaktion", items,
- (int)t->nsuggestions + 1, 0, 0, NULL, 0, NULL,
- 0);
- for (size_t i = 0; i <= t->nsuggestions; i++)
+ items[t->nsuggestions + 1] = xstrdup("Skapa nytt verifikat… (^N)");
+ int sel = tui_select_list("Matcha transaktion", items, (int)nact, 0, 0,
+ NULL, 0, NULL, 0);
+ for (size_t i = 0; i < nact; i++)
free(items[i]);
free(items);
if (sel < 0)
return;
+ if ((size_t)sel == nact - 1) {
+ bank_new_voucher(a, t);
+ return;
+ }
int64_t vid;
if ((size_t)sel < t->nsuggestions) {
vid = t->suggestions[sel].voucher_id;
@@ -278,6 +329,8 @@ void bank_screen(struct app *a)
for (size_t i = 0; i < n; i++) {
yyjson_val *it = yyjson_arr_get(items, i);
txs[i].id = vint(it, "id");
+ snprintf(txs[i].account, sizeof txs[i].account, "%s",
+ vstr(it, "account"));
snprintf(txs[i].booked_at, sizeof txs[i].booked_at, "%s",
vstr(it, "booked_at"));
snprintf(txs[i].text, sizeof txs[i].text, "%s", vstr(it, "text"));
@@ -333,7 +386,7 @@ void bank_screen(struct app *a)
size_t rows = n > 0 ? n : 1;
char **lines = xcalloc(rows, sizeof(char *));
if (n == 0) {
- lines[0] = xstrdup("+ Importera bankfil (^N)");
+ lines[0] = xstrdup("+ Importera bankfil (a)");
} else {
for (size_t i = 0; i < n; i++) {
char line[512];
@@ -354,7 +407,16 @@ void bank_screen(struct app *a)
yyjson_doc_free(doc);
return;
}
- if (sel >= 0 && (size_t)sel < n) {
+ if (sel == -4) {
+ if (n > 0 && ctx.cursor >= 0 && (size_t)ctx.cursor < n) {
+ keep_id = txs[ctx.cursor].id;
+ if (txs[ctx.cursor].nmatches == 0)
+ bank_new_voucher(a, &txs[ctx.cursor]);
+ else
+ tui_message("Bankavstämning",
+ "Transaktionen är redan matchad.");
+ }
+ } else if (sel >= 0 && (size_t)sel < n) {
keep_id = txs[sel].id;
if (txs[sel].nmatches == 0)
bank_match_tx(a, &txs[sel]);
diff --git a/clients/screens_vouchers.c b/clients/screens_vouchers.c
index d87cfd2..fedffe9 100644
--- a/clients/screens_vouchers.c
+++ b/clients/screens_vouchers.c
@@ -769,6 +769,11 @@ static int vform_key(void *ud, int ch)
int64_t vouchers_new(struct app *a)
{
+ return vouchers_new_prefill(a, NULL);
+}
+
+int64_t vouchers_new_prefill(struct app *a, const struct voucher_prefill *p)
+{
struct vform vf;
memset(&vf, 0, sizeof vf);
vf.a = a;
@@ -778,6 +783,28 @@ int64_t vouchers_new(struct app *a)
char *ref = util_random_id("tui-", 8);
snprintf(vf.client_ref, sizeof vf.client_ref, "%s", ref);
free(ref);
+ if (p) {
+ if (p->date && *p->date && util_parse_iso_date(p->date))
+ snprintf(vf.date, sizeof vf.date, "%s", p->date);
+ if (p->description && *p->description)
+ snprintf(vf.desc, sizeof vf.desc, "%s", p->description);
+ if (p->bank_account && *p->bank_account) {
+ snprintf(vf.rows[0].account, sizeof vf.rows[0].account, "%s",
+ p->bank_account);
+ int64_t amt = p->amount_ore < 0 ? -p->amount_ore : p->amount_ore;
+ char tmp[32];
+ tui_kr_format(amt, tmp, sizeof tmp);
+ if (p->amount_ore > 0)
+ snprintf(vf.rows[0].debit, sizeof vf.rows[0].debit, "%s",
+ tmp);
+ else if (p->amount_ore < 0)
+ snprintf(vf.rows[0].credit, sizeof vf.rows[0].credit, "%s",
+ tmp);
+ }
+ if (p->description && *p->description)
+ snprintf(vf.rows[0].text, sizeof vf.rows[0].text, "%s",
+ p->description);
+ }
int text_w = COLS - 60;
if (text_w < 8)
text_w = 8;
@@ -806,6 +833,7 @@ int64_t vouchers_new(struct app *a)
ff[2].cap = sizeof vf.desc;
ff[2].kind = TUI_F_TEXT;
tui_rt_set_fields(&vf.rt, ff, 3);
+ tui_rt_normalize(&vf.rt);
const char *hint = "Enter = ändra fält Tab = byta fält F4 = mall"
" ^F = bifoga fil F5 = validera ^X = rensa rad"
" ^Enter = bokför Esc = avbryt";
diff --git a/clients/ui.h b/clients/ui.h
index 1737259..d10f680 100644
--- a/clients/ui.h
+++ b/clients/ui.h
@@ -99,10 +99,19 @@ extern const struct is_group IS_GROUPS[IS_GROUPS_N];
void open_scene(struct app *a, const char *name);
void config_mkdirs(const char *path);
+/* Optional prefill for the new-voucher form (bank reconciliation). */
+struct voucher_prefill {
+ const char *date; /* YYYY-MM-DD, may be NULL */
+ const char *description; /* voucher text, may be NULL */
+ const char *bank_account; /* account number for the prefilled row */
+ int64_t amount_ore; /* > 0 debits the bank account, < 0 credits it */
+};
+
/* screen entry points (scene registry) */
void dashboard(struct app *a);
void update_status(struct app *a);
int64_t vouchers_new(struct app *a);
+int64_t vouchers_new_prefill(struct app *a, const struct voucher_prefill *p);
void vouchers_screen(struct app *a);
void acct_cache_free(void);
const char *acct_name(struct app *a, const char *number);
diff --git a/docs/PROTOCOL.md b/docs/PROTOCOL.md
index f24e917..aafa1c9 100644
--- a/docs/PROTOCOL.md
+++ b/docs/PROTOCOL.md
@@ -771,8 +771,12 @@ commands. Implemented screens (0.1.0-dev):
- **Underlag** — inbox of unlinked attachments; `a` uploads a file.
- **Bankavstämning** — imported bank transactions (`bank.import`) matched
against vouchers on the bank account, with suggestions; Enter matches the
- suggested voucher (or picks another), `u` unmatches, `a`/`Ctrl+N` imports
- a SEB CSV. Phase 1 never books anything.
+ suggested voucher (or picks another), `u` unmatches, `a` imports a SEB CSV.
+ `Ctrl+N` (or the last row `Skapa nytt verifikat…` in the match list) opens
+ **Nytt verifikat** prefilled with the transaction's date, text, signed
+ amount and bank account; posting it auto-matches the new voucher and
+ returns to the list. A failed auto-match keeps the posted voucher and
+ shows the server error.
- **Fakturor** — invoice list (`invoice.list`, newest first) with number,
date, customer, total and status (`utfärdad`/`krediterad`). Ctrl+N opens
the form, Enter the detail. The form has the customer picker, invoice/due
diff --git a/docs/STATE.md b/docs/STATE.md
index b05689f..21995c6 100644
--- a/docs/STATE.md
+++ b/docs/STATE.md
@@ -134,7 +134,10 @@ check.
prefilled (F4 template still there); after posting, auto-match the new
voucher and stay in the list. Later: `bank_rule.*` pattern suggestions
and "skapa verifikat från transaktion". TUI screen **Bankavstämning**
- under Bokföring.
+ under Bokföring. **Phase 2 implemented 2026-09-20**: Ctrl+N (or
+ `Skapa nytt verifikat…` in the match list) prefills the form from the
+ transaction and auto-matches the posted voucher, returning to the list
+ with the cursor kept.
20. **Menu IA (2026-09-20)**: one top-level section per workflow domain
(Bokföring, Fakturering, Lön, …), Register owns master data, Rapporter
owns read-only output, Räkenskapsår its own, and a feature adds at most
diff --git a/scripts/tui-golden.py b/scripts/tui-golden.py
index 9bb1edf..25cfdc3 100755
--- a/scripts/tui-golden.py
+++ b/scripts/tui-golden.py
@@ -55,6 +55,7 @@ KEYS = {
"pgdn": "\x1b[6~",
"ctrlc": "\x03",
"ctrln": "\x0e",
+ "ctrlenter": "\x1b[27;5;13~",
}
# {org_name} {org_nr} {fy_label} {fy_start} {fy_end} are substituted at run
@@ -117,6 +118,30 @@ SCENARIOS = [
],
},
{
+ "name": "bank-new-voucher",
+ "screen": "bank",
+ "expect": ["Bankavstämning", "GOLDEN INSÄTTNING", "CDON"],
+ "steps": [
+ {
+ "keys": ["ctrln"],
+ "expect": ["Nytt verifikat", "GOLDEN INSÄTTNING", "123,00",
+ "1930"],
+ },
+ {
+ "keys": ["esc"],
+ "expect": ["Bankavstämning", "GOLDEN INSÄTTNING"],
+ },
+ {
+ "keys": ["enter"],
+ "expect": ["Matcha transaktion", "Skapa nytt verifikat…"],
+ },
+ {
+ "keys": ["esc"],
+ "expect": ["Bankavstämning", "GOLDEN INSÄTTNING"],
+ },
+ ],
+ },
+ {
"name": "bank",
"screen": "bank",
"expect": ["Bankavstämning", "GOLDEN INSÄTTNING", "CDON",
@@ -133,6 +158,40 @@ SCENARIOS = [
],
},
{
+ "name": "bank-new-voucher-post",
+ "screen": "bank",
+ "expect": ["Bankavstämning", "CDON", "1 omatchade"],
+ "steps": [
+ {
+ "keys": ["ctrln"],
+ "expect": ["Nytt verifikat", "CDON", "24,09", "1930"],
+ },
+ {
+ "keys": ["\t\t\t\t\t\t\t", "3001", "\t", "24,09"],
+ },
+ {
+ "keys": ["ctrlenter"],
+ "expect": ["Bokfört"],
+ },
+ {
+ "keys": ["enter"],
+ "expect": ["Matchat med A3"],
+ },
+ {
+ "keys": ["enter"],
+ "expect": ["→ A3", "0 omatchade"],
+ },
+ {
+ "keys": ["ctrln"],
+ "expect": ["Transaktionen är redan matchad."],
+ },
+ {
+ "keys": ["enter"],
+ "expect": ["0 omatchade"],
+ },
+ ],
+ },
+ {
"name": "invoices",
"screen": "invoices",
"expect": ["Fakturor", "Faktura", "Testkund AB", "12 500,00"],