aboutsummaryrefslogtreecommitdiff
path: root/clients
diff options
context:
space:
mode:
authorAnders Betts <anders.betts@gmail.com>2026-09-19 23:53:09 +0200
committerAnders Betts <anders.betts@gmail.com>2026-09-19 23:53:09 +0200
commit1f2d7a8190876e3d1c541251e079a11a2cc637f2 (patch)
tree284427385e9c140dddb0c97df318fc85852fe8e1 /clients
parent86c0ef0e30f6cb24db04f20a326cf5fb4b616c3c (diff)
downloadbokf-1f2d7a8190876e3d1c541251e079a11a2cc637f2.tar.gz
bokf-1f2d7a8190876e3d1c541251e079a11a2cc637f2.zip
tui: bokslut hub with year fields, reports and the posting actionv0.1.43
Diffstat (limited to 'clients')
-rw-r--r--clients/bokftui.c168
-rw-r--r--clients/tui.c192
-rw-r--r--clients/tui.h35
3 files changed, 311 insertions, 84 deletions
diff --git a/clients/bokftui.c b/clients/bokftui.c
index aa7233b..3258f22 100644
--- a/clients/bokftui.c
+++ b/clients/bokftui.c
@@ -2950,6 +2950,38 @@ static void sru_save(struct app *a)
saved[0], saved[1]);
}
+static void ink2_screen(struct app *a)
+{
+ char sargs[64];
+ snprintf(sargs, sizeof sargs, "{\"fiscal_year\":%lld}", (long long)a->fy);
+ for (;;) {
+ char *resp = client_rpc(&a->conn, "sru.export", a->session, a->org,
+ sargs);
+ if (!resp || !client_ok(resp)) {
+ show_error("Inkomstdeklaration", resp);
+ free(resp);
+ break;
+ }
+ yyjson_doc *sd = parse(resp);
+ yyjson_val *sres = sd ? jget(yyjson_doc_get_root(sd), "result") : NULL;
+ struct buf out;
+ buf_init(&out);
+ if (sres)
+ fmt_ink2(&out, a, sres);
+ else
+ buf_line(&out, "%s", resp);
+ buf_append(&out, "", 1);
+ int sagain = tui_pager("Rapport", (const char *)out.p, "s = spara SRU");
+ buf_free(&out);
+ yyjson_doc_free(sd);
+ free(resp);
+ if (sagain == 2)
+ sru_save(a);
+ if (!sagain)
+ break;
+ }
+}
+
static void reports_screen(struct app *a)
{
static const char *const report_items[] = {
@@ -2959,56 +2991,16 @@ static void reports_screen(struct app *a)
"Huvudbok",
"Verifikationslista",
"Momsdeklaration",
- "Inkomstdeklaration (INK2/SRU)",
- "Årsredovisning (K2)",
"Kontolista (alla konton)",
};
static int last_sel = 0;
for (;;) {
if (g_quit)
return;
- int sel = tui_menu("Rapporter", report_items, 9, 0, &last_sel);
+ int sel = tui_menu("Rapporter", report_items, 7, 0, &last_sel);
if (sel < 0)
return;
if (sel == 6) {
- char sargs[64];
- snprintf(sargs, sizeof sargs, "{\"fiscal_year\":%lld}",
- (long long)a->fy);
- for (;;) {
- char *resp = client_rpc(&a->conn, "sru.export", a->session,
- a->org, sargs);
- if (!resp || !client_ok(resp)) {
- show_error("Inkomstdeklaration", resp);
- free(resp);
- break;
- }
- yyjson_doc *sd = parse(resp);
- yyjson_val *sres =
- sd ? jget(yyjson_doc_get_root(sd), "result") : NULL;
- struct buf out;
- buf_init(&out);
- if (sres)
- fmt_ink2(&out, a, sres);
- else
- buf_line(&out, "%s", resp);
- buf_append(&out, "", 1);
- int sagain = tui_pager("Rapport", (const char *)out.p,
- "s = spara SRU");
- buf_free(&out);
- yyjson_doc_free(sd);
- free(resp);
- if (sagain == 2)
- sru_save(a);
- if (!sagain)
- break;
- }
- continue;
- }
- if (sel == 7) {
- arsredovisning_screen(a);
- continue;
- }
- if (sel == 8) {
accounts_report(a);
continue;
}
@@ -4474,9 +4466,10 @@ static void bokslut_plan(struct app *a, const char *fond, const char *rate,
free(presp);
}
-/* Bokslut: the per-year årsredovisning details (fields 0-5, saved one by
- one with fiscal_year.update) plus the local periodiseringsfond and tax
- rate inputs. New years inherit the stable fields. */
+/* Bokslut: the year-end hub. The year's årsredovisning details (fields 0-5,
+ saved one by one with fiscal_year.update) and the local periodiseringsfond
+ and tax rate inputs plus a status row and the year-end actions, all in one
+ view. New years inherit the stable fields. */
static void bokslut_screen(struct app *a)
{
static const char *const labels[6] = {
@@ -4506,6 +4499,9 @@ static void bokslut_screen(struct app *a)
free(resp);
return;
}
+ char *fystat = jstr_dup(resp, "result.status");
+ int closed = fystat && strcmp(fystat, "closed") == 0;
+ free(fystat);
char *vals[6];
struct tui_form_field ff[8];
int64_t div_ore = jint_val(resp, "result.dividend_ore", 0);
@@ -4524,6 +4520,41 @@ static void bokslut_screen(struct app *a)
ff[i].cap = 512;
}
free(resp);
+
+ /* A closed year counts as posted; an open one only when the tax
+ voucher that bokslut.post creates is already in the books. */
+ int posted = closed;
+ if (!closed) {
+ char vargs[160];
+ snprintf(vargs, sizeof vargs,
+ "{\"fiscal_year\":%lld,"
+ "\"text\":\"Skatt på årets resultat\",\"limit\":1}",
+ (long long)a->fy);
+ char *vresp = client_rpc(&a->conn, "voucher.list", a->session,
+ a->org, vargs);
+ if (vresp && client_ok(vresp))
+ posted = jarr_size(vresp, "result.items") > 0;
+ free(vresp);
+ }
+
+ char status_line[96];
+ snprintf(status_line, sizeof status_line, "Bokslut bokfört: %s",
+ posted ? "ja" : "nej");
+ const char *reason = NULL;
+ if (closed)
+ reason = "året är stängt";
+ else if (posted)
+ reason = "bokslutet är redan bokfört";
+ else if (!can_edit)
+ reason = "du har inte behörighet";
+ struct tui_form_action acts[5] = {
+ { status_line, -1, NULL },
+ { "Årsredovisning (K2)", 1, NULL },
+ { "Inkomstdeklaration (INK2/SRU)", 1, NULL },
+ { "Bokslutsplan (torrkörning)", 1, NULL },
+ { "Bokför bokslut", reason ? 0 : 1, reason },
+ };
+
ff[0].kind = TUI_F_TEXT;
ff[1].kind = TUI_F_DATE;
ff[2].kind = TUI_F_AMOUNT;
@@ -4539,10 +4570,11 @@ static void bokslut_screen(struct app *a)
ff[7].value = rate;
ff[7].cap = sizeof rate;
ff[7].kind = TUI_F_TEXT;
- tui_form_hint("upp/ned/Home/End Enter = ändra F5 = visa"
+ tui_form_hint("upp/ned/Tab = flytta Enter = ändra/utför F5 = visa"
" bokslutsplan ^Enter = bokför planen (frågar"
" först) Esc/q = tillbaka ^C = avsluta");
- int r = tui_form_run("Bokslut", ff, nf + 2, can_edit);
+ int r = tui_form_run_actions("Bokslut", ff, nf + 2, can_edit, acts, 5,
+ NULL);
if (r >= 0 && r < nf) {
yyjson_mut_doc *d = yyjson_mut_doc_new(NULL);
yyjson_mut_val *o = yyjson_mut_obj(d);
@@ -4563,6 +4595,22 @@ static void bokslut_screen(struct app *a)
}
for (int i = 0; i < nf; i++)
free(vals[i]);
+ if (r == TUI_FORM_ACTION + 1) {
+ arsredovisning_screen(a);
+ continue;
+ }
+ if (r == TUI_FORM_ACTION + 2) {
+ ink2_screen(a);
+ continue;
+ }
+ if (r == TUI_FORM_ACTION + 3) {
+ bokslut_plan(a, fond, rate, 0);
+ continue;
+ }
+ if (r == TUI_FORM_ACTION + 4) {
+ bokslut_plan(a, fond, rate, 1);
+ continue;
+ }
if (r == TUI_FORM_REFRESH)
bokslut_plan(a, fond, rate, 0);
else if (r == TUI_FORM_SUBMIT)
@@ -5337,13 +5385,15 @@ static void do_reload(struct app *a, const char *self)
static const char *const MAIN_ITEMS[] = {
MK_SECTION "Bokföring",
- "Verifikat", "Underlag (inkorg)", "Ingående balans",
+ "Verifikat", "Underlag (inkorg)",
MK_SECTION "Rapporter & bokslut",
"Rapporter", "Bokslut",
MK_SECTION "Register",
"Mallar", "Företagsuppgifter", "Inställningar",
+ MK_SECTION "Räkenskapsår",
+ "Ingående balans", "Räkenskapsår",
MK_SECTION "Övrigt",
- "Revision", "Räkenskapsår", "Logga ut / avsluta",
+ "Revision", "Logga ut / avsluta",
};
static void dashboard(struct app *a)
@@ -5373,31 +5423,31 @@ static void dashboard(struct app *a)
case 2:
open_scene(a, "inbox");
break;
- case 3:
- open_scene(a, "ib");
- break;
- case 5:
+ case 4:
open_scene(a, "reports");
break;
- case 6:
+ case 5:
open_scene(a, "bokslut");
break;
- case 8:
+ case 7:
open_scene(a, "templates");
break;
- case 9:
+ case 8:
open_scene(a, "company");
break;
- case 10:
+ case 9:
open_scene(a, "settings");
break;
- case 12:
- open_scene(a, "audit");
+ case 11:
+ open_scene(a, "ib");
break;
- case 13:
+ case 12:
select_fiscal_year(a);
break;
case 14:
+ open_scene(a, "audit");
+ break;
+ case 15:
return;
default:
break; /* section header */
diff --git a/clients/tui.c b/clients/tui.c
index a964f65..a5b0cd2 100644
--- a/clients/tui.c
+++ b/clients/tui.c
@@ -1341,27 +1341,122 @@ int tui_form_next(int sel, int n, int ch)
return TUI_NAV_NONE;
}
-int tui_form_run(const char *title, struct tui_form_field *f, int n,
- int can_edit)
+static int act_selectable(const struct tui_form_action *acts, int i, int na)
{
- return tui_form_run_hook(title, f, n, can_edit, NULL, NULL);
+ return acts && i >= 0 && i < na && acts[i].enabled != -1;
}
-int tui_form_run_hook(const char *title, struct tui_form_field *f, int n,
- int can_edit, int (*key)(void *ud, int ch), void *ud)
+int tui_form_act_step(int focus, int nf, const struct tui_form_action *acts,
+ int na, int dir)
+{
+ if (nf < 0)
+ nf = 0;
+ if (na < 0)
+ na = 0;
+ int n = nf + na;
+ if (n <= 0)
+ return -1;
+ int pos = focus >= 0 && focus < n ? focus : 0;
+ for (int i = 0; i < n; i++) {
+ pos = (pos + (dir >= 0 ? 1 : -1) + n) % n;
+ if (pos < nf || act_selectable(acts, pos - nf, na))
+ return pos;
+ }
+ return focus;
+}
+
+int tui_form_act_first(int nf, const struct tui_form_action *acts, int na)
+{
+ if (nf > 0)
+ return 0;
+ for (int i = 0; i < na; i++)
+ if (act_selectable(acts, i, na))
+ return i;
+ return -1;
+}
+
+int tui_form_act_key(int sel, int nf, const struct tui_form_action *acts,
+ int na, int ch)
+{
+ int n = (nf > 0 ? nf : 0) + (na > 0 ? na : 0);
+ if (ch == KEY_UP || ch == KEY_DOWN || ch == '\t' || ch == KEY_BTAB) {
+ int dir = ch == KEY_UP || ch == KEY_BTAB ? -1 : 1;
+ return tui_form_act_step(sel, nf, acts, na, dir);
+ }
+ if (ch == KEY_HOME)
+ return tui_form_act_first(nf, acts, na);
+ if (ch == KEY_END) {
+ for (int i = n - 1; i >= 0; i--) {
+ if (i < nf || act_selectable(acts, i - nf, na))
+ return i;
+ }
+ return tui_form_act_first(nf, acts, na);
+ }
+ if (ch == KEY_NPAGE || ch == KEY_PPAGE) {
+ int dir = ch == KEY_NPAGE ? 1 : -1;
+ int pos = sel;
+ for (int i = 0; i < TUI_FORM_PAGE; i++)
+ pos = tui_form_act_step(pos, nf, acts, na, dir);
+ return pos;
+ }
+ return tui_form_next(sel, nf, ch);
+}
+
+void tui_form_act_label(const struct tui_form_action *a, char *buf, size_t n)
+{
+ const char *label = a && a->label ? a->label : "";
+ if (a && a->enabled == 0) {
+ const char *reason = a->disabled_reason && *a->disabled_reason
+ ? a->disabled_reason
+ : "otillgänglig";
+ snprintf(buf, n, "%s (%s)", label, reason);
+ } else {
+ snprintf(buf, n, "%s", label);
+ }
+}
+
+/* Action rows sit under the fields, one blank line below them. */
+static void form_draw_actions(const struct tui_form_action *acts, int na,
+ int nf, int sel)
+{
+ int y = 5 + nf + 1;
+ for (int i = 0; i < na; i++) {
+ if (y + i >= LINES - 2)
+ break;
+ char line[832];
+ tui_form_act_label(&acts[i], line, sizeof line);
+ int dim = acts[i].enabled != 1;
+ if (dim)
+ attron(tui_style_attrs(TUI_DIM, g_colours, 0));
+ if (sel == nf + i)
+ attron(A_REVERSE);
+ mvaddnstr(y + i, 2, line, COLS - 4);
+ if (sel == nf + i)
+ attroff(A_REVERSE);
+ if (dim)
+ attroff(tui_style_attrs(TUI_DIM, g_colours, 0));
+ }
+}
+
+static int form_run(const char *title, struct tui_form_field *f, int nf,
+ int can_edit, const struct tui_form_action *acts, int na,
+ const char *hint, int (*key)(void *ud, int ch), void *ud)
{
- int sel = 0;
+ int sel = nf > 0 ? 0 : tui_form_act_first(0, acts, na);
for (;;) {
- if (sel >= n)
- sel = n - 1;
+ int ntot = nf + (na > 0 ? na : 0);
+ if (ntot <= 0)
+ return TUI_FORM_BACK;
if (sel < 0)
- sel = 0;
+ sel = tui_form_act_first(nf, acts, na);
+ if (sel >= ntot)
+ sel = ntot - 1;
tui_frame(title);
tui_style(TUI_HEADING);
mvaddstr(3, 2, "Uppgift");
mvaddstr(3, 34, "Värde");
tui_style_reset();
- for (int i = 0; i < n; i++) {
+ for (int i = 0; i < nf; i++) {
char lbl[160], val[640], line[832];
snprintf(lbl, sizeof lbl, "%s", f[i].label);
tui_pad_field(lbl, sizeof lbl, 31);
@@ -1396,19 +1491,32 @@ int tui_form_run_hook(const char *title, struct tui_form_field *f, int n,
else if (!can_edit)
attroff(tui_style_attrs(TUI_DIM, g_colours, 0));
}
- tui_hints(g_form_hint
- ? g_form_hint
- : (can_edit
- ? "upp/ned/Home/End Enter = ändra"
- " F5 = uppdatera ^Enter = spara"
- " Esc/q = tillbaka ^C = avsluta"
- : "upp/ned/Home/End F5 = uppdatera"
- " Esc/q = tillbaka ^C = avsluta"
- " (endast behöriga kan ändra)"));
+ if (na > 0)
+ form_draw_actions(acts, na, nf, sel);
+ const char *h = g_form_hint ? g_form_hint : hint;
+ if (!h)
+ h = na > 0
+ ? (can_edit
+ ? "upp/ned/Tab = flytta Enter = ändra/utför"
+ " F5 = uppdatera ^Enter = spara"
+ " Esc/q = tillbaka ^C = avsluta"
+ : "upp/ned/Tab = flytta Enter = utför"
+ " F5 = uppdatera Esc/q = tillbaka"
+ " ^C = avsluta"
+ " (endast behöriga kan ändra)")
+ : (can_edit
+ ? "upp/ned/Home/End Enter = ändra"
+ " F5 = uppdatera ^Enter = spara"
+ " Esc/q = tillbaka ^C = avsluta"
+ : "upp/ned/Home/End F5 = uppdatera"
+ " Esc/q = tillbaka ^C = avsluta"
+ " (endast behöriga kan ändra)");
+ tui_hints(h);
g_form_hint = NULL;
refresh();
int ch = in_key();
- int nxt = tui_form_next(sel, n, ch);
+ int nxt = na > 0 ? tui_form_act_key(sel, nf, acts, na, ch)
+ : tui_form_next(sel, nf, ch);
if (nxt >= 0) {
sel = nxt;
continue;
@@ -1417,18 +1525,33 @@ int tui_form_run_hook(const char *title, struct tui_form_field *f, int n,
nxt == TUI_FORM_BACK)
return nxt;
if (key) {
- int h = key(ud, ch);
- if (h == TUI_HOOK_BACK)
+ int hk = key(ud, ch);
+ if (hk == TUI_HOOK_BACK)
return TUI_FORM_BACK;
- if (h == TUI_HOOK_REFRESH)
+ if (hk == TUI_HOOK_REFRESH)
return TUI_FORM_REFRESH;
- if (h == TUI_HOOK_SUBMIT)
+ if (hk == TUI_HOOK_SUBMIT)
return TUI_FORM_SUBMIT;
- if (h != TUI_HOOK_NONE)
+ if (hk != TUI_HOOK_NONE)
continue;
}
if (ch != '\n' && ch != '\r' && ch != KEY_ENTER)
continue;
+ if (sel < 0)
+ continue; /* no focusable row: only the return keys work */
+ if (sel >= nf) {
+ const struct tui_form_action *act = &acts[sel - nf];
+ if (act->enabled == 0) {
+ tui_message(title, "%s",
+ act->disabled_reason && *act->disabled_reason
+ ? act->disabled_reason
+ : "Otillgänglig.");
+ continue;
+ }
+ if (act->enabled == -1)
+ continue;
+ return TUI_FORM_ACTION + (sel - nf);
+ }
if (!can_edit) {
tui_message(title, "Endast behöriga kan ändra.");
continue;
@@ -1461,6 +1584,25 @@ int tui_form_run_hook(const char *title, struct tui_form_field *f, int n,
}
}
+int tui_form_run(const char *title, struct tui_form_field *f, int n,
+ int can_edit)
+{
+ return form_run(title, f, n, can_edit, NULL, 0, NULL, NULL, NULL);
+}
+
+int tui_form_run_hook(const char *title, struct tui_form_field *f, int n,
+ int can_edit, int (*key)(void *ud, int ch), void *ud)
+{
+ return form_run(title, f, n, can_edit, NULL, 0, NULL, key, ud);
+}
+
+int tui_form_run_actions(const char *title, struct tui_form_field *f, int nf,
+ int can_edit, const struct tui_form_action *acts,
+ int na, const char *hint)
+{
+ return form_run(title, f, nf, can_edit, acts, na, hint, NULL, NULL);
+}
+
/* ------------------------------------------------------------------ */
/* row table (dynamic multi-column editor with a trailing blank row) */
/* ------------------------------------------------------------------ */
diff --git a/clients/tui.h b/clients/tui.h
index 0950eac..d0656da 100644
--- a/clients/tui.h
+++ b/clients/tui.h
@@ -185,6 +185,41 @@ void tui_form_hint(const char *hint); /* overrides the footer for the next run *
int tui_form_run_hook(const char *title, struct tui_form_field *f, int n,
int can_edit, int (*key)(void *ud, int ch), void *ud);
+/* --- action list in a form (tui_form_run_actions) --- */
+/* enabled: 1 = selectable and runs; 0 = dimmed but selectable (Enter shows
+ disabled_reason in a message); -1 = non-selectable heading/status row,
+ drawn dim like a menu section and skipped by navigation. */
+struct tui_form_action {
+ const char *label;
+ int enabled;
+ const char *disabled_reason;
+};
+
+#define TUI_FORM_ACTION 1000 /* action i is returned as TUI_FORM_ACTION + i */
+
+/* Focus ring over fields (0..nf-1) and actions (nf..nf+na-1); heading rows
+ are skipped and the ring wraps. dir +1 = Tab/Down, -1 = Shift-Tab/Up. */
+int tui_form_act_step(int focus, int nf, const struct tui_form_action *acts,
+ int na, int dir);
+/* Same ring for one key, plus Home/End/PgUp/PgDn and the form return keys
+ (F5/^Enter/F9/Esc/q). Returns the new focus, a TUI_FORM_* code or
+ TUI_NAV_NONE. */
+int tui_form_act_key(int sel, int nf, const struct tui_form_action *acts,
+ int na, int ch);
+/* Index of the first focusable row (field 0 when fields exist, else the
+ first selectable action), or -1. */
+int tui_form_act_first(int nf, const struct tui_form_action *acts, int na);
+/* "label", or "label (reason)" for a disabled selectable action. */
+void tui_form_act_label(const struct tui_form_action *a, char *buf, size_t n);
+
+/* Form with fields and an action list below them (see TUI-GUIDELINES.md).
+ Returns a field index after an edit, TUI_FORM_ACTION + i for a chosen
+ action, or TUI_FORM_BACK/REFRESH/SUBMIT. `hint` overrides the default
+ footer; tui_form_hint() still takes precedence. */
+int tui_form_run_actions(const char *title, struct tui_form_field *f, int nf,
+ int can_edit, const struct tui_form_action *acts,
+ int na, const char *hint);
+
/* --- row table --- */
enum { TUI_RT_EDIT = 0, TUI_RT_INFO, TUI_RT_DATE };