#include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "client.h" #include "tui.h" #include "formula.h" #include "util.h" #include "version.h" #include "yyjson.h" #include "ui.h" /* ------------------------------------------------------------------ */ /* ingående balans (series IB) */ /* ------------------------------------------------------------------ */ struct tui_ibrow { char account[16]; char amount[32]; char text[64]; }; /* Loads the net IB per account for the active fiscal year. */ /* Effective opening balances of the selected year: the year's IB vouchers plus all earlier movements, exactly what the reports show. */ static int ib_load(struct app *a, char ***out_acc, int64_t **out_amt, int *out_n) { *out_acc = NULL; *out_amt = NULL; *out_n = 0; char args[128]; snprintf(args, sizeof args, "{\"fiscal_year\":%lld,\"include_zero\":true}", (long long)a->fy); char *resp = client_rpc(&a->conn, "report.trial_balance", a->session, a->org, args); if (!resp || !client_ok(resp)) { show_error("Ingående balans", resp); free(resp); return -1; } size_t n = jarr_size(resp, "result.accounts"); char **accs = xcalloc(n ? n : 1, sizeof(char *)); int64_t *amts = xcalloc(n ? n : 1, sizeof(int64_t)); int count = 0; for (size_t i = 0; i < n; i++) { char path[64]; snprintf(path, sizeof path, "result.accounts.%zu.ib_ore", i); int64_t ib = jint_val(resp, path, 0); if (ib == 0) continue; snprintf(path, sizeof path, "result.accounts.%zu.account", i); char *acc = jstr_dup(resp, path); if (!acc) continue; accs[count] = acc; amts[count] = ib; count++; } free(resp); *out_acc = accs; *out_amt = amts; *out_n = count; return 0; } static int64_t ib_old_lookup(char **accs, int64_t *amts, int n, const char *account) { for (int i = 0; i < n; i++) if (strcmp(accs[i], account) == 0) return amts[i]; return 0; } struct ibform { struct app *a; struct tui_rt rt; struct tui_ibrow rows[64]; char **old_acc; int64_t *old_amt; int nold; }; static void ib_cell(void *ud, int row, int col, char **buf, size_t *cap) { struct ibform *ib = ud; struct tui_ibrow *r = &ib->rows[row]; if (col == 0) { *buf = r->account; *cap = sizeof r->account; } else if (col == 2) { *buf = r->amount; *cap = sizeof r->amount; } else { *buf = r->text; *cap = sizeof r->text; } } static void ib_info(void *ud, int row, char *buf, size_t n) { struct ibform *ib = ud; const char *nm = acct_name(ib->a, ib->rows[row].account); snprintf(buf, n, "%s", nm ? nm : ""); } static void ib_footer(void *ud, char *buf, size_t n) { (void)ud; snprintf(buf, n, "Positiva belopp = debet, negativa = kredit. Summan ska" " bli noll."); } static const char *ib_check(struct ibform *ib, int *used_out, char *msg, size_t msglen) { int64_t sum = 0; int used = 0; for (int i = 0; i < ib->rt.nrows; i++) { if (!ib->rows[i].account[0]) continue; if (!acct_exists(ib->a, ib->rows[i].account)) { snprintf(msg, msglen, "Konto %.15s finns inte (rad %d).", ib->rows[i].account, i + 1); return msg; } double kr = 0; if (ib->rows[i].amount[0] && !parse_x_double(ib->rows[i].amount, &kr)) { snprintf(msg, msglen, "Ogiltigt belopp på rad %d: '%.20s'.", i + 1, ib->rows[i].amount); return msg; } sum += (int64_t)llround(kr * 100.0); used++; } if (used == 0) return "Ange minst ett konto."; if (sum != 0) { snprintf(msg, msglen, "Summan måste bli noll (nu %lld öre).", (long long)sum); return msg; } *used_out = used; return NULL; } /* Posts the delta against the existing IB so nothing is ever edited. */ static int ib_post_delta(struct ibform *ib) { struct app *a = ib->a; yyjson_mut_doc *d = yyjson_mut_doc_new(NULL); yyjson_mut_val *o = yyjson_mut_obj(d); yyjson_mut_doc_set_root(d, o); yyjson_mut_obj_add_strcpy(d, o, "date", a->fy_start); yyjson_mut_obj_add_strcpy(d, o, "description", "Ingående balans"); yyjson_mut_obj_add_strcpy(d, o, "series", "IB"); char *ref = util_random_id("tui-", 8); yyjson_mut_obj_add_strcpy(d, o, "client_ref", ref); free(ref); yyjson_mut_val *arr = yyjson_mut_arr(d); int posted = 0; for (int i = 0; i < ib->rt.nrows; i++) { if (!ib->rows[i].account[0]) continue; double kr = 0; if (ib->rows[i].amount[0] && !parse_x_double(ib->rows[i].amount, &kr)) kr = 0; int64_t new_amt = (int64_t)llround(kr * 100.0); int64_t prev_amt = ib_old_lookup(ib->old_acc, ib->old_amt, ib->nold, ib->rows[i].account); int64_t delta = new_amt - prev_amt; if (delta == 0) continue; yyjson_mut_val *ro = yyjson_mut_arr_add_obj(d, arr); yyjson_mut_obj_add_strcpy(d, ro, "account", ib->rows[i].account); yyjson_mut_obj_add_int(d, ro, "debit_ore", delta > 0 ? delta : 0); yyjson_mut_obj_add_int(d, ro, "credit_ore", delta < 0 ? -delta : 0); if (ib->rows[i].text[0]) yyjson_mut_obj_add_strcpy(d, ro, "description", ib->rows[i].text); posted++; } for (int i = 0; i < ib->nold; i++) { int still = 0; for (int j = 0; j < ib->rt.nrows; j++) if (strcmp(ib->rows[j].account, ib->old_acc[i]) == 0) { still = 1; break; } if (!still && ib->old_amt[i] != 0) { yyjson_mut_val *ro = yyjson_mut_arr_add_obj(d, arr); yyjson_mut_obj_add_strcpy(d, ro, "account", ib->old_acc[i]); yyjson_mut_obj_add_int(d, ro, "debit_ore", ib->old_amt[i] < 0 ? -ib->old_amt[i] : 0); yyjson_mut_obj_add_int(d, ro, "credit_ore", ib->old_amt[i] > 0 ? ib->old_amt[i] : 0); posted++; } } if (posted == 0) { yyjson_mut_doc_free(d); tui_message("Ingående balans", "Ingen ändring."); return 0; } yyjson_mut_obj_add_val(d, o, "rows", arr); char *args = yyjson_mut_write(d, 0, NULL); yyjson_mut_doc_free(d); char *resp = client_rpc(&a->conn, "voucher.post", a->session, a->org, args); free(args); if (resp && client_ok(resp)) { int64_t num = jint_val(resp, "result.number", 0); tui_message("Ingående balans", "Sparat som IB %lld (%d justerade rader).", (long long)num, posted); free(resp); return 1; } show_error("Kunde inte spara ingående balans", resp); free(resp); return 0; } static int ib_form(struct app *a, char **old_acc, int64_t *old_amt, int nold) { struct ibform ib; memset(&ib, 0, sizeof ib); ib.a = a; ib.old_acc = old_acc; ib.old_amt = old_amt; ib.nold = nold; if (nold > 63) { tui_message("Ingående balans", "För många konton (%d) för att redigera här.", nold); return 0; } for (int i = 0; i < nold && i < 63; i++) { snprintf(ib.rows[i].account, sizeof ib.rows[i].account, "%s", old_acc[i]); tui_kr_format(old_amt[i], ib.rows[i].amount, sizeof ib.rows[i].amount); } int text_w = COLS - 51; if (text_w < 6) text_w = 6; struct tui_rt_col cols[4] = { { "Konto", 2, 6, TUI_RT_EDIT }, { "Namn", 9, 24, TUI_RT_INFO }, { "Belopp", 34, 14, TUI_RT_EDIT }, { "Text", 49, text_w, TUI_RT_EDIT }, }; tui_rt_init(&ib.rt, nold > 0 ? nold : 1, 64, 4, cols, 5, ib_cell, ib_info, &ib); tui_rt_set_footer(&ib.rt, ib_footer); for (;;) { int rr = tui_rt_run("Ingående balans", &ib.rt, "Tab = byta fält F5 = validera ^X = rensa rad" " ^Enter = spara Esc = avbryt"); if (rr == -1) return 0; char msg[256]; int used = 0; const char *problem = ib_check(&ib, &used, msg, sizeof msg); if (problem) { tui_message("Ingående balans", "%s", problem); continue; } if (rr == -2) { tui_message("Validering OK", "%d konton, summan är noll.", used); continue; } if (ib_post_delta(&ib)) return 1; } } struct ibpager { struct app *a; char **accs; int64_t *amts; int n; }; static int ib_view_key(void *ud, int ch) { struct ibpager *p = ud; if (ch == 'e' || ch == 'E' || ch == TUI_KEY_CTRL_N) { if (ib_form(p->a, p->accs, p->amts, p->n)) return TUI_HOOK_REFRESH; return TUI_HOOK_STAY; } return TUI_HOOK_NONE; } /* The list is a read-only table with no selection: tui_pager shows the columns and the sum as plain text, and the e/^N keys run the editor. */ void ib_screen(struct app *a) { for (;;) { char **accs = NULL; int64_t *amts = NULL; int n = 0; if (ib_load(a, &accs, &amts, &n) != 0) return; struct buf t; buf_init(&t); buf_line(&t, "Räkenskapsår %s ingående balans (effektiv)\n\n", a->fy_label); buf_line(&t, "%-7s%-53s%s\n\n", "Konto", "Namn", "Belopp"); int64_t sum = 0; for (int i = 0; i < n; i++) { sum += amts[i]; char amount[32], nbuf[128]; tui_kr_format(amts[i], amount, sizeof amount); const char *nm = acct_name(a, accs[i]); snprintf(nbuf, sizeof nbuf, "%s", nm ? nm : ""); tui_pad_field(nbuf, sizeof nbuf, 50); buf_line(&t, "%-6s %s %14s\n", accs[i], nbuf, amount); } if (n == 0) buf_line(&t, "Ingen ingående balans registrerad ännu.\n"); char sumbuf[32]; tui_kr_format(sum, sumbuf, sizeof sumbuf); buf_line(&t, "\nSumma: %s\n", sumbuf); buf_append(&t, "\0", 1); struct ibpager p = { a, accs, amts, n }; int ret = tui_pager_hook("Ingående balans", (const char *)t.p, "e/^N = redigera", 0, ib_view_key, &p); buf_free(&t); for (int i = 0; i < n; i++) free(accs[i]); free(accs); free(amts); if (ret == 0) return; } }