aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorAnders Betts <anders.betts@gmail.com>2026-09-18 08:38:33 +0200
committerAnders Betts <anders.betts@gmail.com>2026-09-18 08:38:33 +0200
commit702d9327e5f979deff392dceeced2dd16d1203e1 (patch)
treea07e73525be134bbd0c92f7128e5501306660232
parent2a98cce8434f7ea27f777d9e6422a1eb92ba8be7 (diff)
downloadbokf-702d9327e5f979deff392dceeced2dd16d1203e1.tar.gz
bokf-702d9327e5f979deff392dceeced2dd16d1203e1.zip
bokftui: Företagsuppgifter screen; org.update dry-runv0.1.7
- owner-editable company details form (read-only for other roles) - org.update honors dry_run; covered in tests
-rw-r--r--clients/bokftui.c161
-rw-r--r--docs/PROTOCOL.md3
-rw-r--r--src/commands.c24
-rw-r--r--tests/test_core.c14
4 files changed, 197 insertions, 5 deletions
diff --git a/clients/bokftui.c b/clients/bokftui.c
index 16c9941..ccbce14 100644
--- a/clients/bokftui.c
+++ b/clients/bokftui.c
@@ -3606,6 +3606,157 @@ static void ib_screen(struct app *a)
}
}
+struct company_field {
+ const char *key;
+ const char *label;
+ int numeric;
+};
+
+static const struct company_field COMPANY_FIELDS[] = {
+ { "name", "Företagsnamn", 0 },
+ { "org_nr", "Organisationsnummer", 0 },
+ { "vat_nr", "Momsregistreringsnummer", 0 },
+ { "address", "Adress", 0 },
+ { "postal_code", "Postnummer", 0 },
+ { "city", "Ort", 0 },
+ { "country", "Land", 0 },
+ { "email", "E-post", 0 },
+ { "phone", "Telefon", 0 },
+ { "moms_period", "Momsperiod (month/quarter/year)", 0 },
+ { "framework", "Regelverk (K2/K3)", 0 },
+ { "fiscal_year_start_month", "Startmånad räkenskapsår (1-12)", 1 },
+};
+
+static int company_field_valid(const struct company_field *f, const char *v)
+{
+ if (f->numeric) {
+ char *end = NULL;
+ long n = strtol(v, &end, 10);
+ return end && *end == '\0' && n >= 1 && n <= 12;
+ }
+ if (strcmp(f->key, "moms_period") == 0)
+ return strcmp(v, "month") == 0 || strcmp(v, "quarter") == 0 ||
+ strcmp(v, "year") == 0;
+ if (strcmp(f->key, "framework") == 0)
+ return strcmp(v, "K2") == 0 || strcmp(v, "K3") == 0;
+ return 1;
+}
+
+static void company_screen(struct app *a)
+{
+ const int nf = (int)(sizeof COMPANY_FIELDS / sizeof COMPANY_FIELDS[0]);
+ int owner = a->role[0] && strcmp(a->role, "owner") == 0;
+ for (;;) {
+ if (g_quit)
+ return;
+ char *resp = client_rpc(&a->conn, "org.get", a->session, a->org, "{}");
+ if (!resp || !client_ok(resp)) {
+ show_error("Företagsuppgifter", resp);
+ free(resp);
+ return;
+ }
+ char **vals = xcalloc(nf, sizeof(char *));
+ for (int i = 0; i < nf; i++) {
+ char path[64];
+ snprintf(path, sizeof path, "result.%s", COMPANY_FIELDS[i].key);
+ if (COMPANY_FIELDS[i].numeric) {
+ char tmp[32];
+ snprintf(tmp, sizeof tmp, "%lld",
+ (long long)jint_val(resp, path, 0));
+ vals[i] = xstrdup(tmp);
+ } else {
+ char *v = jstr_dup(resp, path);
+ vals[i] = v ? v : xstrdup("");
+ }
+ }
+ free(resp);
+
+ int sel = 0;
+ int back = 0;
+ for (;;) {
+ frame("Företagsuppgifter");
+ attron(A_BOLD);
+ mvaddstr(3, 2, "Uppgift");
+ mvaddstr(3, 34, "Värde");
+ attroff(A_BOLD);
+ for (int i = 0; i < nf; i++) {
+ char lbl[128], line[512];
+ snprintf(lbl, sizeof lbl, "%s", COMPANY_FIELDS[i].label);
+ pad_field(lbl, sizeof lbl, 31);
+ snprintf(line, sizeof line, "%s %s", lbl, vals[i]);
+ if (i == sel) {
+ attron(A_REVERSE);
+ mvaddnstr(5 + i, 2, line, COLS - 4);
+ attroff(A_REVERSE);
+ } else {
+ if (!owner)
+ attron(A_DIM);
+ mvaddnstr(5 + i, 2, line, COLS - 4);
+ if (!owner)
+ attroff(A_DIM);
+ }
+ }
+ hints(owner
+ ? "upp/ned Enter = ändra F5 = uppdatera"
+ " Esc/q = tillbaka Ctrl+C = avsluta"
+ : "upp/ned F5 = uppdatera Esc/q = tillbaka"
+ " Ctrl+C = avsluta (endast ägare kan ändra)");
+ refresh();
+ int ch = ui_getch();
+ if (ch == KEY_UP && sel > 0)
+ sel--;
+ else if (ch == KEY_DOWN && sel < nf - 1)
+ sel++;
+ else if (ch == KEY_F(5))
+ break; /* reload */
+ else if (ch == '\n' || ch == '\r' || ch == KEY_ENTER) {
+ if (!owner) {
+ message("Företagsuppgifter",
+ "Endast ägare kan ändra företagsuppgifterna.");
+ continue;
+ }
+ char label[160];
+ snprintf(label, sizeof label, "%s: ",
+ COMPANY_FIELDS[sel].label);
+ char *val = prompt(label, vals[sel], 0);
+ if (!val)
+ continue;
+ if (!company_field_valid(&COMPANY_FIELDS[sel], val)) {
+ message("Ogiltigt värde", "%s",
+ COMPANY_FIELDS[sel].label);
+ continue;
+ }
+ yyjson_mut_doc *d = yyjson_mut_doc_new(NULL);
+ yyjson_mut_val *o = yyjson_mut_obj(d);
+ yyjson_mut_doc_set_root(d, o);
+ if (COMPANY_FIELDS[sel].numeric)
+ yyjson_mut_obj_add_int(d, o, COMPANY_FIELDS[sel].key,
+ strtoll(val, NULL, 10));
+ else
+ yyjson_mut_obj_add_strcpy(d, o, COMPANY_FIELDS[sel].key,
+ val);
+ char *args = yyjson_mut_write(d, 0, NULL);
+ yyjson_mut_doc_free(d);
+ char *r = client_rpc(&a->conn, "org.update", a->session,
+ a->org, args);
+ free(args);
+ if (!r || !client_ok(r))
+ show_error("Kunde inte spara", r);
+ free(r);
+ break; /* reload from the server */
+ } else if (ch == 27 || ch == 'q') {
+ back = 1;
+ break;
+ }
+ }
+ for (int i = 0; i < nf; i++)
+ free(vals[i]);
+ free(vals);
+ if (back || g_quit)
+ return;
+ }
+}
+
static const char *const SETTING_KEYS[] = { "default_series",
"attachment_dir" };
static const char *const SETTING_LABELS[] = { "Standardserie",
@@ -3785,13 +3936,14 @@ static void templates_screen(struct app *a)
static const char *const MAIN_ITEMS[] = {
"Verifikat", "Underlag (inkorg)", "Ingående balans",
"Mallar", "Rapporter", "Revision",
- "Inställningar", "Byt räkenskapsår", "Logga ut / avsluta",
+ "Inställningar", "Företagsuppgifter", "Byt räkenskapsår",
+ "Logga ut / avsluta",
};
static void dashboard(struct app *a)
{
for (;;) {
- int sel = menu("Bokf", MAIN_ITEMS, 9, 1);
+ int sel = menu("Bokf", MAIN_ITEMS, 10, 1);
if (g_quit)
return;
if (sel == -4) {
@@ -3825,9 +3977,12 @@ static void dashboard(struct app *a)
settings_screen(a);
break;
case 7:
- select_fiscal_year(a);
+ company_screen(a);
break;
case 8:
+ select_fiscal_year(a);
+ break;
+ case 9:
return;
}
}
diff --git a/docs/PROTOCOL.md b/docs/PROTOCOL.md
index f3d7cac..cc2cb57 100644
--- a/docs/PROTOCOL.md
+++ b/docs/PROTOCOL.md
@@ -418,6 +418,9 @@ commands. Implemented screens (0.1.0-dev):
- **Underlag** — inbox of unlinked attachments; `a` uploads a file.
- **Rapporter** — saldobalans, resultaträkning, balansräkning, moms and
kontolista (all accounts with type, moms treatment, SRU and status).
+- **Företagsuppgifter** — the org record (name, org number, VAT number,
+ address, e-mail, phone, moms period, framework, fiscal-year start month),
+ editable in place; only owners can save, others see it read-only.
- **Revision** — chain verification and behandlingshistorik.
**Ctrl+N is the universal "add" key**: it starts a new verifikat from the
diff --git a/src/commands.c b/src/commands.c
index 948c42c..64212f7 100644
--- a/src/commands.c
+++ b/src/commands.c
@@ -704,6 +704,28 @@ static yyjson_mut_val *h_org_update(struct req *r)
strcmp(framework, "K3") != 0)
return fail(r, "INVALID_ARGS", "framework must be K2 or K3");
+ static const char *const keys[9] = { "name", "org_nr", "vat_nr",
+ "address", "postal_code", "city",
+ "country", "email", "phone" };
+ const char *texts[9] = { name, org_nr, vat_nr, address, postal,
+ city, country, email, phone };
+
+ if (r->dry_run) {
+ yyjson_mut_val *o = yyjson_mut_obj(r->rdoc);
+ for (int i = 0; i < 9; i++)
+ if (texts[i])
+ yyjson_mut_obj_add_strcpy(r->rdoc, o, keys[i], texts[i]);
+ if (fy_month)
+ yyjson_mut_obj_add_int(r->rdoc, o, "fiscal_year_start_month",
+ fy_month);
+ if (moms)
+ yyjson_mut_obj_add_strcpy(r->rdoc, o, "moms_period", moms);
+ if (framework)
+ yyjson_mut_obj_add_strcpy(r->rdoc, o, "framework", framework);
+ yyjson_mut_obj_add_bool(r->rdoc, o, "dry_run", true);
+ return o;
+ }
+
sqlite3_stmt *st = NULL;
if (sqlite3_prepare_v2(
r->db,
@@ -721,8 +743,6 @@ static yyjson_mut_val *h_org_update(struct req *r)
-1, &st, NULL) != SQLITE_OK)
return fail(r, "INTERNAL", "database error");
sqlite3_bind_int64(st, 1, r->org_id);
- const char *texts[9] = { name, org_nr, vat_nr, address, postal,
- city, country, email, phone };
for (int i = 0; i < 9; i++) {
if (texts[i])
sqlite3_bind_text(st, i + 2, texts[i], -1, SQLITE_TRANSIENT);
diff --git a/tests/test_core.c b/tests/test_core.c
index bf5bfe4..fa808ac 100644
--- a/tests/test_core.c
+++ b/tests/test_core.c
@@ -421,6 +421,20 @@ int main(void)
CHECK_STR(d, "error.code", "INVALID_ARGS");
yyjson_doc_free(d);
+ d = call(reqf("{\"v\":1,\"id\":\"8e\",\"cmd\":\"org.update\","
+ "\"session\":\"%s\",\"org\":%d,\"dry_run\":true,\"args\":"
+ "{\"city\":\"Nyköping\"}}",
+ g_session, (int)org_id));
+ CHECK_OK(d);
+ CHECK(jbool(d, "result.dry_run"));
+ yyjson_doc_free(d);
+ d = call(reqf("{\"v\":1,\"id\":\"8f\",\"cmd\":\"org.get\","
+ "\"session\":\"%s\",\"org\":%d}",
+ g_session, (int)org_id));
+ CHECK_OK(d);
+ CHECK_STR(d, "result.city", "Sollentuna");
+ yyjson_doc_free(d);
+
d = call(reqf("{\"v\":1,\"id\":\"9\",\"cmd\":\"session.use_org\","
"\"session\":\"%s\",\"args\":{\"org\":%d}}",
g_session, (int)org_id));