diff options
| -rw-r--r-- | clients/screens_bank.c | 76 | ||||
| -rw-r--r-- | clients/screens_vouchers.c | 28 | ||||
| -rw-r--r-- | clients/ui.h | 9 | ||||
| -rw-r--r-- | docs/PROTOCOL.md | 8 | ||||
| -rw-r--r-- | docs/STATE.md | 5 | ||||
| -rwxr-xr-x | scripts/tui-golden.py | 59 |
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"], |
