aboutsummaryrefslogtreecommitdiff
path: root/clients/screens_ib.c
diff options
context:
space:
mode:
Diffstat (limited to 'clients/screens_ib.c')
-rw-r--r--clients/screens_ib.c354
1 files changed, 354 insertions, 0 deletions
diff --git a/clients/screens_ib.c b/clients/screens_ib.c
new file mode 100644
index 0000000..ca35562
--- /dev/null
+++ b/clients/screens_ib.c
@@ -0,0 +1,354 @@
+#include <dirent.h>
+#include <errno.h>
+#include <locale.h>
+#include <math.h>
+#include <ncursesw/ncurses.h>
+#include <stdarg.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <strings.h>
+#include <sys/stat.h>
+#include <sys/wait.h>
+#include <time.h>
+#include <unistd.h>
+
+#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;
+ }
+}